2026-08-08 09:34:15 +01:00
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
|
|
|
import { writeFileSync, readFileSync, mkdtempSync, rmSync, existsSync } from 'node:fs';
|
2026-08-08 09:34:15 +01:00
|
|
|
import { join } from 'node:path';
|
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
|
|
|
import { tmpdir, homedir } from 'node:os';
|
2026-08-08 09:34:15 +01:00
|
|
|
import { createConfigCommand } from '../../src/commands/config.js';
|
|
|
|
|
import type { ApiClient } from '../../src/api-client.js';
|
|
|
|
|
import { DEFAULT_MCPCTL_GATEWAY_URL } from '../../src/config/prime-agent.js';
|
|
|
|
|
|
|
|
|
|
function mockClient(): ApiClient {
|
|
|
|
|
return {
|
|
|
|
|
get: vi.fn(async () => ({})),
|
|
|
|
|
post: vi.fn(async () => ({ token: 'impersonated-tok', user: { email: 'other@test.com' } })),
|
|
|
|
|
put: vi.fn(async () => ({})),
|
|
|
|
|
delete: vi.fn(async () => {}),
|
|
|
|
|
} as unknown as ApiClient;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('config prime-agent', () => {
|
|
|
|
|
let client: ReturnType<typeof mockClient>;
|
|
|
|
|
let output: string[];
|
|
|
|
|
let tmpDir: string;
|
|
|
|
|
const log = (...args: string[]) => output.push(args.join(' '));
|
|
|
|
|
|
|
|
|
|
let prevCwd: string;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
client = mockClient();
|
|
|
|
|
output = [];
|
|
|
|
|
tmpDir = mkdtempSync(join(tmpdir(), 'mcpctl-config-prime-agent-'));
|
|
|
|
|
// config prime-agent writes the .mcpctl-project marker into cwd, so run
|
|
|
|
|
// every test from an isolated temp dir to avoid polluting the repo.
|
|
|
|
|
prevCwd = process.cwd();
|
|
|
|
|
process.chdir(tmpDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
process.chdir(prevCwd);
|
|
|
|
|
process.exitCode = 0;
|
|
|
|
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('requires --project', async () => {
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--skip-skills'], { from: 'user' });
|
|
|
|
|
expect(output.join('\n')).toContain('--project is required');
|
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('writes proxy MCP entry into prime-agent settings.json', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'homeautomation', '-o', settingsPath, '--skip-skills'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
const written = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
|
|
|
expect(written.mcpServers['homeautomation']).toEqual({
|
|
|
|
|
type: 'http',
|
|
|
|
|
url: `${DEFAULT_MCPCTL_GATEWAY_URL}/projects/homeautomation/mcp`,
|
|
|
|
|
});
|
|
|
|
|
expect(output.join('\n')).toContain('homeautomation');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('merges with existing servers and preserves other settings', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
writeFileSync(settingsPath, JSON.stringify({
|
|
|
|
|
defaultProvider: 'itaz',
|
|
|
|
|
mcpServers: {
|
|
|
|
|
sre: { type: 'http', url: `${DEFAULT_MCPCTL_GATEWAY_URL}/projects/sre/mcp` },
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'proj-1', '-o', settingsPath, '--skip-skills'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
const written = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
|
|
|
expect(written.defaultProvider).toBe('itaz'); // untouched
|
|
|
|
|
expect(written.mcpServers['sre']).toBeDefined(); // preserved
|
|
|
|
|
expect(written.mcpServers['proj-1']).toEqual({
|
|
|
|
|
type: 'http',
|
|
|
|
|
url: `${DEFAULT_MCPCTL_GATEWAY_URL}/projects/proj-1/mcp`,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('writes a project marker for later skills sync', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'sre', '-o', settingsPath, '--skip-skills'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
const markerPath = join(tmpDir, '.mcpctl-project');
|
|
|
|
|
expect(readFileSync(markerPath, 'utf-8').trim()).toBe('sre');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('--dry-run prints the change without writing', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'proj-2', '-o', settingsPath, '--dry-run'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
expect(output.join('\n')).toContain('proj-2');
|
|
|
|
|
// No file should have been created.
|
|
|
|
|
expect(exceptionSafeRead(settingsPath)).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
|
|
|
it('does not call the API when --skip-skills and --token are given', async () => {
|
2026-08-08 09:34:15 +01:00
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'proj-3', '-o', settingsPath, '--skip-skills', '--token', 'mcpctl_pat_test'], { from: 'user' });
|
2026-08-08 09:34:15 +01:00
|
|
|
|
|
|
|
|
expect(client.get).not.toHaveBeenCalled();
|
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
|
|
|
expect(client.post).not.toHaveBeenCalled();
|
2026-08-08 09:34:15 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('backward compat: prime-agent-generate still works', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent-generate', '--project', 'proj-1', '-o', settingsPath, '--skip-skills'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
const written = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
|
|
|
expect(written.mcpServers['proj-1']).toBeDefined();
|
|
|
|
|
});
|
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
|
|
|
|
|
|
|
|
it('provisions auth.json by minting a project token', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'labctl', '-o', settingsPath, '--skip-skills', '--skip-extension'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/mcptokens', expect.objectContaining({ projectName: 'labctl' }));
|
|
|
|
|
const auth = JSON.parse(readFileSync(join(tmpDir, 'auth.json'), 'utf-8'));
|
|
|
|
|
expect(auth['mcp:labctl']).toEqual({ type: 'api_key', key: 'impersonated-tok' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses --token without calling the API', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'docmost', '-o', settingsPath, '--skip-skills', '--skip-extension', '--token', 'mcpctl_pat_custom'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
expect(client.post).not.toHaveBeenCalled();
|
|
|
|
|
const auth = JSON.parse(readFileSync(join(tmpDir, 'auth.json'), 'utf-8'));
|
|
|
|
|
expect(auth['mcp:docmost']).toEqual({ type: 'api_key', key: 'mcpctl_pat_custom' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('keeps an existing credential and does not re-mint', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
writeFileSync(join(tmpDir, 'auth.json'), JSON.stringify({ 'mcp:labctl': { type: 'api_key', key: 'existing' } }));
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'labctl', '-o', settingsPath, '--skip-skills', '--skip-extension'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
expect(client.post).not.toHaveBeenCalled();
|
|
|
|
|
const auth = JSON.parse(readFileSync(join(tmpDir, 'auth.json'), 'utf-8'));
|
|
|
|
|
expect(auth['mcp:labctl'].key).toBe('existing');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('installs the /mcpctl switcher extension by default, and skips with --skip-extension', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'ha', '-o', settingsPath, '--skip-skills', '--token', 'mcpctl_pat_x'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
const extPath = join(tmpDir, 'extensions', 'mcpctl-switch.ts');
|
|
|
|
|
expect(existsSync(extPath)).toBe(true);
|
|
|
|
|
expect(readFileSync(extPath, 'utf-8')).toContain("registerCommand('mcpctl'");
|
|
|
|
|
|
|
|
|
|
output.length = 0;
|
|
|
|
|
const cmd2 = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd2.parseAsync(['prime-agent', '--project', 'ha', '-o', settingsPath, '--skip-skills', '--skip-extension', '--token', 'mcpctl_pat_x'], { from: 'user' });
|
|
|
|
|
expect(output.join('\n')).not.toContain('switcher extension');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not write a .mcpctl-project marker when run from $HOME', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
const prevCwd = process.cwd();
|
|
|
|
|
process.chdir(homedir());
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
try {
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'proj-x', '-o', settingsPath, '--skip-skills', '--skip-extension', '--token', 'mcpctl_pat_x'], { from: 'user' });
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(prevCwd);
|
|
|
|
|
}
|
|
|
|
|
expect(output.join('\n')).toContain('Skipped .mcpctl-project marker');
|
|
|
|
|
expect(exceptionSafeRead(join(homedir(), '.mcpctl-project'))).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('refuses to overwrite a corrupt settings.json', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
writeFileSync(settingsPath, '{ this is not valid json !!!');
|
|
|
|
|
const prevCwd = process.cwd();
|
|
|
|
|
process.chdir(tmpDir);
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
try {
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'proj-9', '-o', settingsPath, '--skip-skills', '--skip-extension', '--token', 'mcpctl_pat_x'], { from: 'user' });
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(prevCwd);
|
|
|
|
|
}
|
|
|
|
|
expect(output.join('\n')).toContain('refusing to overwrite');
|
|
|
|
|
// The corrupt file is untouched.
|
|
|
|
|
expect(readFileSync(settingsPath, 'utf-8')).toBe('{ this is not valid json !!!');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('merges a re-configured project entry, preserving user-added fields', async () => {
|
|
|
|
|
const settingsPath = join(tmpDir, 'settings.json');
|
|
|
|
|
writeFileSync(settingsPath, JSON.stringify({
|
|
|
|
|
mcpServers: {
|
|
|
|
|
ha: { type: 'http', url: 'https://old/projects/ha/mcp', headers: { Authorization: 'Bearer u' } },
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
const cmd = createConfigCommand(
|
|
|
|
|
{ configDeps: { configDir: tmpDir }, log },
|
|
|
|
|
{ client, credentialsDeps: { configDir: tmpDir }, log },
|
|
|
|
|
);
|
|
|
|
|
await cmd.parseAsync(['prime-agent', '--project', 'ha', '-o', settingsPath, '--skip-skills', '--skip-extension', '--token', 'mcpctl_pat_x'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
const written = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
|
|
|
expect(written.mcpServers['ha']).toEqual({
|
|
|
|
|
type: 'http',
|
|
|
|
|
url: `${DEFAULT_MCPCTL_GATEWAY_URL}/projects/ha/mcp`,
|
|
|
|
|
headers: { Authorization: 'Bearer u' }, // user-added field preserved
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-08-08 09:34:15 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function exceptionSafeRead(path: string): string | null {
|
|
|
|
|
try {
|
|
|
|
|
return readFileSync(path, 'utf-8');
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|