mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
feat(cli): let ktx setup --agents choose an install directory (#298)
Split the fused directory concept into projectDir (what the agent config references) and installRoot (where project-scoped files are written), so users can install .claude/, .mcp.json, skills, and rules where they open their agent instead of only in the ktx project directory. - Add --install-dir <path> (resolved against cwd, created if missing, mutually exclusive with --global/--local, rejected for claude-desktop). - Add an interactive directory menu: ktx project dir / Current directory (hidden when it equals the project dir) / Custom directory… / Global scope (shown only when every target supports it). - Expand a leading ~ in typed/quoted paths so the ~/… menu hints round-trip. - Record installRoot in the install manifest and merge key; thread it through file planning, MCP config paths, summaries, and next actions. - Refresh uv.lock to 0.12.0 for the editable ktx-sl and ktx-daemon packages.
This commit is contained in:
parent
ed44f46f2a
commit
4e61020089
10 changed files with 650 additions and 70 deletions
|
|
@ -526,6 +526,7 @@ describe('runKtxCli', () => {
|
|||
expect(stdout).toContain('--target <target>');
|
||||
expect(stdout).toContain('--global');
|
||||
expect(stdout).toContain('--local');
|
||||
expect(stdout).toContain('--install-dir <path>');
|
||||
expect(stdout).toContain('--yes');
|
||||
expect(stdout).toContain('--no-input');
|
||||
expect(stdout).toContain('Global Options:');
|
||||
|
|
@ -1486,6 +1487,94 @@ describe('runKtxCli', () => {
|
|||
expect(setup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches --install-dir as the agent install root', async () => {
|
||||
const setup = vi.fn(async () => 0);
|
||||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'setup', '--agents', '--target', 'claude-code', '--install-dir', '.', '--no-input'],
|
||||
setupIo.io,
|
||||
{ setup },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(setup).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agents: true, target: 'claude-code', agentScope: 'project', installRoot: '.' }),
|
||||
setupIo.io,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects --install-dir together with --global', async () => {
|
||||
const setup = vi.fn(async () => 0);
|
||||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'setup', '--agents', '--target', 'claude-code', '--install-dir', '.', '--global', '--no-input'],
|
||||
setupIo.io,
|
||||
{ setup },
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(setupIo.stderr()).toContain('Choose either --install-dir or a scope flag (--global / --local), not both.');
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects --install-dir together with --local', async () => {
|
||||
const setup = vi.fn(async () => 0);
|
||||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'setup', '--agents', '--target', 'claude-code', '--install-dir', '.', '--local', '--no-input'],
|
||||
setupIo.io,
|
||||
{ setup },
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(setupIo.stderr()).toContain('Choose either --install-dir or a scope flag (--global / --local), not both.');
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('treats an empty --install-dir as not provided', async () => {
|
||||
const setup = vi.fn(async () => 0);
|
||||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'setup', '--agents', '--target', 'claude-code', '--install-dir', '', '--global', '--no-input'],
|
||||
setupIo.io,
|
||||
{ setup },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(setup).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agents: true, target: 'claude-code', agentScope: 'global' }),
|
||||
setupIo.io,
|
||||
);
|
||||
expect(setup).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({ installRoot: expect.anything() }),
|
||||
setupIo.io,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects --install-dir with --target claude-desktop', async () => {
|
||||
const setup = vi.fn(async () => 0);
|
||||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'setup', '--agents', '--target', 'claude-desktop', '--install-dir', '.', '--no-input'],
|
||||
setupIo.io,
|
||||
{ setup },
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(setupIo.stderr()).toContain('--install-dir does not apply to --target claude-desktop, which is always global.');
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects source-path with source-git-url', async () => {
|
||||
const setup = vi.fn(async () => 0);
|
||||
const testIo = makeIo();
|
||||
|
|
|
|||
|
|
@ -383,6 +383,7 @@ describe('setup agents', () => {
|
|||
const prompts = {
|
||||
select: vi.fn(async ({ message }: { message: string }) => (message.startsWith('Where') ? 'project' : 'mcp')),
|
||||
multiselect: vi.fn(async () => ['claude-code']),
|
||||
text: vi.fn(async () => undefined),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -439,6 +440,7 @@ describe('setup agents', () => {
|
|||
multiselect: vi.fn(async () => {
|
||||
throw new Error('target selection should not run');
|
||||
}),
|
||||
text: vi.fn(async () => undefined),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -495,6 +497,7 @@ describe('setup agents', () => {
|
|||
message.startsWith('Where should') ? 'global' : 'mcp',
|
||||
),
|
||||
multiselect: vi.fn(async () => ['claude-code']),
|
||||
text: vi.fn(async () => undefined),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -508,6 +511,7 @@ describe('setup agents', () => {
|
|||
scope: 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
cwd: tempDir,
|
||||
},
|
||||
io.io,
|
||||
{ prompts },
|
||||
|
|
@ -518,13 +522,10 @@ describe('setup agents', () => {
|
|||
});
|
||||
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: `Where should ktx install supported agent config?\n\nktx project: ${tempDir}`,
|
||||
message: `Where should ktx install agent config?\n\nktx project: ${tempDir}`,
|
||||
options: [
|
||||
{
|
||||
value: 'project',
|
||||
label: 'Project scope (ktx project directory)',
|
||||
hint: 'Only agents opened from this ktx project path load the project-scoped config.',
|
||||
},
|
||||
{ value: 'project', label: 'ktx project directory', hint: tempDir },
|
||||
{ value: 'custom', label: 'Custom directory…', hint: 'Enter a path' },
|
||||
{
|
||||
value: 'global',
|
||||
label: 'Global scope (user config)',
|
||||
|
|
@ -978,6 +979,7 @@ describe('setup agents', () => {
|
|||
const prompts = {
|
||||
select: vi.fn(async () => 'back'),
|
||||
multiselect: vi.fn(async () => ['codex']),
|
||||
text: vi.fn(async () => undefined),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -1003,6 +1005,7 @@ describe('setup agents', () => {
|
|||
const prompts = {
|
||||
select: vi.fn(async () => 'mcp-cli'),
|
||||
multiselect: vi.fn(async () => ['back']),
|
||||
text: vi.fn(async () => undefined),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -1136,6 +1139,7 @@ describe('setup agents', () => {
|
|||
message.startsWith('Where should') ? 'project' : 'mcp',
|
||||
),
|
||||
multiselect: vi.fn(async () => ['claude-code', 'claude-desktop']),
|
||||
text: vi.fn(async () => undefined),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -1228,8 +1232,11 @@ describe('setup agents', () => {
|
|||
it('explains next actions for Codex, Cursor, OpenCode, and universal MCP targets', async () => {
|
||||
const io = makeIo();
|
||||
const prompts = {
|
||||
select: vi.fn(async () => 'mcp-cli'),
|
||||
select: vi.fn(async ({ message }: { message: string }) =>
|
||||
message.startsWith('Where') ? 'project' : 'mcp-cli',
|
||||
),
|
||||
multiselect: vi.fn(async () => ['codex', 'cursor', 'opencode', 'universal']),
|
||||
text: vi.fn(async () => undefined),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -1274,6 +1281,347 @@ describe('setup agents', () => {
|
|||
expect(output).toContain('.agents guidance installed');
|
||||
});
|
||||
|
||||
describe('install root', () => {
|
||||
it('plans project-scoped files under installRoot, leaving projectDir as the default', () => {
|
||||
const installRoot = join(tempDir, 'opened-here');
|
||||
expect(
|
||||
plannedKtxAgentFiles({
|
||||
projectDir: tempDir,
|
||||
installRoot,
|
||||
target: 'claude-code',
|
||||
scope: 'project',
|
||||
mode: 'mcp-cli',
|
||||
}),
|
||||
).toEqual([
|
||||
{ kind: 'file', path: join(installRoot, '.claude/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
|
||||
{ kind: 'file', path: join(installRoot, '.claude/skills/ktx/SKILL.md'), role: 'skill' },
|
||||
{ kind: 'file', path: join(installRoot, '.claude/rules/ktx.md'), role: 'rule' },
|
||||
]);
|
||||
|
||||
expect(
|
||||
plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-code', scope: 'project', mode: 'mcp' }),
|
||||
).toEqual([
|
||||
{ kind: 'file', path: join(tempDir, '.claude/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows the install path in the summary title only when installRoot differs from projectDir', () => {
|
||||
const installRoot = join(tempDir, 'app');
|
||||
const custom = formatInstallSummaryLines(
|
||||
[{ target: 'claude-code', scope: 'project', mode: 'mcp', installRoot }],
|
||||
[
|
||||
{ kind: 'file', path: join(installRoot, '.claude/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
|
||||
{ kind: 'json-key', path: join(installRoot, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
|
||||
],
|
||||
tempDir,
|
||||
);
|
||||
expect(custom[0].title).toBe(`Claude Code · ${installRoot}`);
|
||||
|
||||
const same = formatInstallSummaryLines(
|
||||
[{ target: 'claude-code', scope: 'project', mode: 'mcp', installRoot: tempDir }],
|
||||
[],
|
||||
tempDir,
|
||||
);
|
||||
expect(same[0].title).toBe('Claude Code · Project scope');
|
||||
});
|
||||
|
||||
it('installs project files and next actions under an explicit installRoot', async () => {
|
||||
const io = makeIo();
|
||||
const installRoot = join(tempDir, 'workspace');
|
||||
|
||||
const result = await runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
yes: true,
|
||||
agents: true,
|
||||
target: 'claude-code',
|
||||
scope: 'project',
|
||||
mode: 'mcp-cli',
|
||||
skipAgents: false,
|
||||
installRoot,
|
||||
},
|
||||
io.io,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'ready',
|
||||
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp-cli', installRoot }],
|
||||
});
|
||||
await expect(stat(join(installRoot, '.claude/skills/ktx/SKILL.md'))).resolves.toBeDefined();
|
||||
const mcp = JSON.parse(await readFile(join(installRoot, '.mcp.json'), 'utf-8')) as {
|
||||
mcpServers?: Record<string, unknown>;
|
||||
};
|
||||
expect(mcp.mcpServers).toHaveProperty('ktx');
|
||||
await expect(stat(join(tempDir, '.claude/skills/ktx/SKILL.md'))).rejects.toThrow();
|
||||
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('Open Claude Code from the install directory:');
|
||||
expect(output).toContain(`cd '${installRoot}'`);
|
||||
expect(output).toContain(`ktx mcp start --project-dir ${tempDir}`);
|
||||
|
||||
expect(await readKtxAgentInstallManifest(tempDir)).toMatchObject({
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp-cli', installRoot }],
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when an explicit installRoot points at an existing file', async () => {
|
||||
const io = makeIo();
|
||||
const filePath = join(tempDir, 'not-a-dir');
|
||||
await writeFile(filePath, 'x', 'utf-8');
|
||||
|
||||
await expect(
|
||||
runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
yes: true,
|
||||
agents: true,
|
||||
target: 'claude-code',
|
||||
scope: 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
installRoot: filePath,
|
||||
},
|
||||
io.io,
|
||||
),
|
||||
).resolves.toEqual({ status: 'failed', projectDir: tempDir });
|
||||
expect(io.stderr()).toContain('is a file, not a directory');
|
||||
});
|
||||
|
||||
it('installs into the current directory and records it in the manifest', async () => {
|
||||
const io = makeIo();
|
||||
const openedDir = join(tempDir, 'opened');
|
||||
await mkdir(openedDir, { recursive: true });
|
||||
const prompts = {
|
||||
select: vi.fn(async ({ message }: { message: string }) =>
|
||||
message.startsWith('Where') ? 'current' : 'mcp',
|
||||
),
|
||||
multiselect: vi.fn(async () => ['claude-code', 'cursor']),
|
||||
text: vi.fn(async () => undefined),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'auto',
|
||||
yes: false,
|
||||
agents: true,
|
||||
scope: 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
cwd: openedDir,
|
||||
},
|
||||
io.io,
|
||||
{ prompts },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'ready',
|
||||
installs: [
|
||||
{ target: 'claude-code', scope: 'project', mode: 'mcp', installRoot: openedDir },
|
||||
{ target: 'cursor', scope: 'project', mode: 'mcp', installRoot: openedDir },
|
||||
],
|
||||
});
|
||||
await expect(stat(join(openedDir, '.claude/skills/ktx-analytics/SKILL.md'))).resolves.toBeDefined();
|
||||
await expect(stat(join(openedDir, '.cursor/mcp.json'))).resolves.toBeDefined();
|
||||
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('Open Cursor from the install directory:');
|
||||
expect(output).toContain(openedDir);
|
||||
|
||||
expect(await readKtxAgentInstallManifest(tempDir)).toMatchObject({
|
||||
installs: [
|
||||
{ target: 'claude-code', scope: 'project', mode: 'mcp', installRoot: openedDir },
|
||||
{ target: 'cursor', scope: 'project', mode: 'mcp', installRoot: openedDir },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('creates and installs into a typed custom directory', async () => {
|
||||
const io = makeIo();
|
||||
const customDir = join(tempDir, 'custom-target');
|
||||
const prompts = {
|
||||
select: vi.fn(async ({ message }: { message: string }) =>
|
||||
message.startsWith('Where') ? 'custom' : 'mcp',
|
||||
),
|
||||
multiselect: vi.fn(async () => ['claude-code']),
|
||||
text: vi.fn(async () => customDir),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'auto',
|
||||
yes: false,
|
||||
agents: true,
|
||||
scope: 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
cwd: tempDir,
|
||||
},
|
||||
io.io,
|
||||
{ prompts },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'ready',
|
||||
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp', installRoot: customDir }],
|
||||
});
|
||||
await expect(stat(join(customDir, '.claude/skills/ktx-analytics/SKILL.md'))).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('hides the current directory row when cwd equals the ktx project directory', async () => {
|
||||
const io = makeIo();
|
||||
const prompts = {
|
||||
select: vi.fn(async ({ message }: { message: string; options: Array<{ value: string }> }) =>
|
||||
message.startsWith('Where') ? 'project' : 'mcp',
|
||||
),
|
||||
multiselect: vi.fn(async () => ['claude-code']),
|
||||
text: vi.fn(async () => undefined),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
await runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'auto',
|
||||
yes: false,
|
||||
agents: true,
|
||||
scope: 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
cwd: tempDir,
|
||||
},
|
||||
io.io,
|
||||
{ prompts },
|
||||
);
|
||||
|
||||
const directoryCall = prompts.select.mock.calls.find(([opts]) => opts.message.startsWith('Where'));
|
||||
expect(directoryCall).toBeDefined();
|
||||
expect(directoryCall?.[0].options.map((option) => option.value)).toEqual(['project', 'custom', 'global']);
|
||||
});
|
||||
|
||||
it('re-prompts when a typed custom directory is an existing file', async () => {
|
||||
const io = makeIo();
|
||||
const filePath = join(tempDir, 'afile');
|
||||
await writeFile(filePath, 'x', 'utf-8');
|
||||
const validDir = join(tempDir, 'valid');
|
||||
const prompts = {
|
||||
select: vi.fn(async ({ message }: { message: string }) =>
|
||||
message.startsWith('Where') ? 'custom' : 'mcp',
|
||||
),
|
||||
multiselect: vi.fn(async () => ['claude-code']),
|
||||
text: vi.fn<() => Promise<string | undefined>>().mockResolvedValueOnce(filePath).mockResolvedValueOnce(validDir),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'auto',
|
||||
yes: false,
|
||||
agents: true,
|
||||
scope: 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
cwd: tempDir,
|
||||
},
|
||||
io.io,
|
||||
{ prompts },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'ready',
|
||||
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp', installRoot: validDir }],
|
||||
});
|
||||
expect(prompts.text).toHaveBeenCalledTimes(2);
|
||||
expect(io.stderr()).toContain('is a file, not a directory');
|
||||
await expect(stat(join(validDir, '.claude/skills/ktx-analytics/SKILL.md'))).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('expands a leading ~ in a typed custom directory', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = home;
|
||||
try {
|
||||
const io = makeIo();
|
||||
const prompts = {
|
||||
select: vi.fn(async ({ message }: { message: string }) =>
|
||||
message.startsWith('Where') ? 'custom' : 'mcp',
|
||||
),
|
||||
multiselect: vi.fn(async () => ['claude-code']),
|
||||
text: vi.fn(async () => '~/opened-here'),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
const expected = join(home, 'opened-here');
|
||||
const result = await runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'auto',
|
||||
yes: false,
|
||||
agents: true,
|
||||
scope: 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
cwd: tempDir,
|
||||
},
|
||||
io.io,
|
||||
{ prompts },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'ready',
|
||||
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp', installRoot: expected }],
|
||||
});
|
||||
await expect(stat(join(expected, '.claude/skills/ktx-analytics/SKILL.md'))).resolves.toBeDefined();
|
||||
await expect(stat(join(tempDir, '~'))).rejects.toThrow();
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('expands a leading ~ in an explicit installRoot', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = home;
|
||||
try {
|
||||
const io = makeIo();
|
||||
const expected = join(home, 'flagged');
|
||||
|
||||
const result = await runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
yes: true,
|
||||
agents: true,
|
||||
target: 'claude-code',
|
||||
scope: 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
installRoot: '~/flagged',
|
||||
cwd: tempDir,
|
||||
},
|
||||
io.io,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'ready',
|
||||
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp', installRoot: expected }],
|
||||
});
|
||||
await expect(stat(join(expected, '.claude/skills/ktx-analytics/SKILL.md'))).resolves.toBeDefined();
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAgentNextActionsLineFormatter', () => {
|
||||
function makeColorStdout(): { write: (chunk: string) => boolean; hasColors: () => boolean } {
|
||||
return { write: () => true, hasColors: () => true };
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ describe('runDemoTour', () => {
|
|||
const mockAgents = vi.fn().mockResolvedValue({
|
||||
status: 'ready',
|
||||
projectDir: '/tmp/test',
|
||||
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp-cli' }],
|
||||
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp-cli', installRoot: '/tmp/test' }],
|
||||
} satisfies KtxSetupAgentsResult);
|
||||
|
||||
const navigation = vi.fn().mockResolvedValue('forward');
|
||||
|
|
|
|||
|
|
@ -2025,7 +2025,7 @@ describe('setup status', () => {
|
|||
return {
|
||||
status: 'ready',
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli', installRoot: tempDir }],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -2078,7 +2078,7 @@ describe('setup status', () => {
|
|||
agents: async () => ({
|
||||
status: 'ready',
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli', installRoot: tempDir }],
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
|
@ -2133,7 +2133,7 @@ describe('setup status', () => {
|
|||
return {
|
||||
status: 'ready',
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli', installRoot: tempDir }],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -2150,7 +2150,7 @@ describe('setup status', () => {
|
|||
const agents = vi.fn(async () => ({
|
||||
status: 'ready' as const,
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'codex' as const, scope: 'project' as const, mode: 'mcp-cli' as const }],
|
||||
installs: [{ target: 'codex' as const, scope: 'project' as const, mode: 'mcp-cli' as const, installRoot: tempDir }],
|
||||
}));
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
||||
|
||||
|
|
@ -2193,7 +2193,7 @@ describe('setup status', () => {
|
|||
const agents = vi.fn(async () => ({
|
||||
status: 'ready' as const,
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'claude-code' as const, scope: 'project' as const, mode: 'mcp' as const }],
|
||||
installs: [{ target: 'claude-code' as const, scope: 'project' as const, mode: 'mcp' as const, installRoot: tempDir }],
|
||||
}));
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
||||
|
||||
|
|
@ -2272,7 +2272,7 @@ describe('setup status', () => {
|
|||
version: 1,
|
||||
projectDir: tempDir,
|
||||
installedAt: '2026-05-07T00:00:00.000Z',
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli', installRoot: tempDir }],
|
||||
entries: [],
|
||||
},
|
||||
null,
|
||||
|
|
@ -2347,7 +2347,7 @@ describe('setup status', () => {
|
|||
return {
|
||||
status: 'ready',
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli', installRoot: tempDir }],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -2453,7 +2453,7 @@ describe('setup status', () => {
|
|||
return {
|
||||
status: 'ready',
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
||||
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli', installRoot: tempDir }],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -2636,7 +2636,7 @@ describe('setup status', () => {
|
|||
const agents = vi.fn(async () => ({
|
||||
status: 'ready' as const,
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'universal' as const, scope: 'project' as const, mode: 'mcp-cli' as const }],
|
||||
installs: [{ target: 'universal' as const, scope: 'project' as const, mode: 'mcp-cli' as const, installRoot: tempDir }],
|
||||
}));
|
||||
|
||||
await expect(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue