mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-25 08:48:08 +02:00
feat(cli): split Claude Desktop skills and polish setup-agents output (#141)
* fix(cli): package Claude Desktop skills in one zip * Polish ktx setup-agents output: hint, summaries, outro * test: update setup agents output polish assertion * Add output-polish follow-up plan * docs: align Claude Desktop split-ZIP wording Update README and the agent-clients docs page to reflect that ktx setup now produces one uploadable ZIP per Claude Desktop skill under .ktx/agents/claude/ (ktx-analytics.zip and optionally ktx.zip) instead of a single combined ktx-skills.zip. * feat(cli): style next-actions note in TTY mode Add createAgentNextActionsLineFormatter, an ANSI line transformer wired into the "Required before using agents" Clack note. It activates only when the target stream reports hasColors(), so non-TTY pipelines and tests keep the existing plain-text output byte-identical. Per-line rules: cyan-bold step numbers + bold titles; dim sub-prose aligned under the title; dim-cyan bullet for .zip paths with HOME shortened to ~; dim "›" replaces " > " breadcrumbs; RUN/PASTE/USE/OPEN markers dimmed; already-styled lines pass through to avoid double-wrap. * docs: move output polish specs out of ktx
This commit is contained in:
parent
86afff56d0
commit
56f4f9c9e8
5 changed files with 427 additions and 169 deletions
|
|
@ -5,7 +5,8 @@ import { readKtxSetupState } from '@ktx/context/project';
|
|||
import { strFromU8, unzipSync } from 'fflate';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
formatInstallSummary,
|
||||
createAgentNextActionsLineFormatter,
|
||||
formatInstallSummaryLines,
|
||||
plannedKtxAgentFiles,
|
||||
readKtxAgentInstallManifest,
|
||||
removeKtxAgentInstall,
|
||||
|
|
@ -82,7 +83,11 @@ describe('setup agents', () => {
|
|||
]);
|
||||
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-desktop', scope: 'global', mode: 'mcp' })).toEqual([
|
||||
{ kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' },
|
||||
{ kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'), role: 'claude-plugin' },
|
||||
{
|
||||
kind: 'file',
|
||||
path: join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'),
|
||||
role: 'claude-desktop-skill-bundle',
|
||||
},
|
||||
]);
|
||||
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'codex', scope: 'project', mode: 'mcp' })).toEqual([
|
||||
{ kind: 'file', path: join(tempDir, '.agents/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
|
||||
|
|
@ -123,7 +128,16 @@ describe('setup agents', () => {
|
|||
]);
|
||||
expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-desktop', scope: 'global', mode: 'mcp-cli' })).toEqual([
|
||||
{ kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' },
|
||||
{ kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'), role: 'claude-plugin' },
|
||||
{
|
||||
kind: 'file',
|
||||
path: join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'),
|
||||
role: 'claude-desktop-skill-bundle',
|
||||
},
|
||||
{
|
||||
kind: 'file',
|
||||
path: join(tempDir, '.ktx/agents/claude/ktx.zip'),
|
||||
role: 'claude-desktop-skill-bundle',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -197,6 +211,7 @@ describe('setup agents', () => {
|
|||
expect(io.stdout()).toContain(`ktx mcp start --project-dir ${tempDir}`);
|
||||
expect(io.stdout()).toContain('If you need to stop MCP later:');
|
||||
expect(io.stdout()).toContain(`ktx mcp stop --project-dir ${tempDir}`);
|
||||
expect(io.stdout()).toContain('All set.');
|
||||
expect(io.stdout()).not.toContain('Finish agent setup');
|
||||
expect(io.stdout()).not.toContain('Next actions');
|
||||
});
|
||||
|
|
@ -223,8 +238,10 @@ describe('setup agents', () => {
|
|||
status: 'ready',
|
||||
nextActions: expect.stringContaining(`ktx mcp start --project-dir ${tempDir}`),
|
||||
});
|
||||
expect(io.stdout()).toContain('Agent integration complete');
|
||||
expect(io.stdout()).toContain('Claude Code · Project scope');
|
||||
expect(io.stdout()).not.toContain('Agent integration complete');
|
||||
expect(io.stdout()).not.toContain('Required before using agents');
|
||||
expect(io.stdout()).not.toContain('All set.');
|
||||
});
|
||||
|
||||
it('installs the analytics skill from the runtime asset', async () => {
|
||||
|
|
@ -414,7 +431,7 @@ describe('setup agents', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('registers Claude Desktop MCP via claude_desktop_config.json and ships a skills-only plugin', async () => {
|
||||
it('registers Claude Desktop MCP and ships an uploadable analytics skill zip', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
|
||||
const previousHome = process.env.HOME;
|
||||
const envSnapshot = captureEnvKeys(process.env, ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY']);
|
||||
|
|
@ -444,9 +461,11 @@ describe('setup agents', () => {
|
|||
installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp' }],
|
||||
});
|
||||
|
||||
const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip');
|
||||
const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
|
||||
const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
|
||||
const launcherPath = join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh');
|
||||
await expect(stat(pluginPath)).resolves.toBeDefined();
|
||||
await expect(stat(analyticsSkillPath)).resolves.toBeDefined();
|
||||
await expect(stat(adminSkillPath)).rejects.toThrow();
|
||||
const launcherStat = await stat(launcherPath);
|
||||
expect(launcherStat.mode & 0o111).not.toBe(0);
|
||||
const launcher = await readFile(launcherPath, 'utf-8');
|
||||
|
|
@ -462,23 +481,24 @@ describe('setup agents', () => {
|
|||
args: ['--project-dir', tempDir, 'mcp', 'stdio'],
|
||||
});
|
||||
|
||||
expect(await readZipText(pluginPath, '.claude-plugin/plugin.json')).toContain('"name": "ktx"');
|
||||
await expect(readZipText(pluginPath, '.mcp.json')).rejects.toThrow('Missing zip entry');
|
||||
expect(await readZipText(pluginPath, 'skills/ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
|
||||
const setupMd = await readZipText(pluginPath, 'SETUP.md');
|
||||
expect(setupMd).not.toContain('ktx mcp start');
|
||||
expect(setupMd).toContain('no manual plugin install step is required');
|
||||
expect(setupMd).toContain('claude_desktop_config.json');
|
||||
expect(setupMd).not.toContain('Install this plugin ZIP from Claude Desktop');
|
||||
await expect(readZipText(pluginPath, 'skills/ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
|
||||
expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
|
||||
await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
|
||||
await expect(readZipText(analyticsSkillPath, '.claude-plugin/plugin.json')).rejects.toThrow('Missing zip entry');
|
||||
await expect(readZipText(analyticsSkillPath, 'skills/ktx-analytics/SKILL.md')).rejects.toThrow(
|
||||
'Missing zip entry',
|
||||
);
|
||||
|
||||
expect(io.stdout()).toContain('Claude Desktop');
|
||||
expect(io.stdout()).not.toContain('.ktx/agents/claude/ktx-plugin.zip');
|
||||
expect(io.stdout()).toContain(analyticsSkillPath);
|
||||
expect(io.stdout()).not.toContain(adminSkillPath);
|
||||
expect(io.stdout()).toContain('claude_desktop_config.json');
|
||||
expect(io.stdout()).toContain('Required before using agents');
|
||||
expect(io.stdout()).toContain('1. Restart Claude Desktop');
|
||||
expect(io.stdout()).toContain('Claude Desktop loads KTX after restart.');
|
||||
expect(io.stdout()).not.toContain('install plugin');
|
||||
expect(io.stdout()).toContain('Claude Desktop loads KTX MCP after restart.');
|
||||
expect(io.stdout()).toContain('2. Upload Claude Desktop skills');
|
||||
expect(io.stdout()).toContain('Customize > Skills > + > Create skill > Upload a skill');
|
||||
expect(io.stdout()).toContain('Upload this file:');
|
||||
expect(io.stdout()).toContain('Toggle the uploaded KTX skills on.');
|
||||
expect(io.stdout()).not.toContain('Run `ktx mcp start`');
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
|
|
@ -535,7 +555,7 @@ describe('setup agents', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('includes the admin CLI skill in the Claude Desktop plugin zip when requested', async () => {
|
||||
it('includes an uploadable admin CLI skill zip for Claude Desktop when requested', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = home;
|
||||
|
|
@ -561,12 +581,18 @@ describe('setup agents', () => {
|
|||
installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp-cli' }],
|
||||
});
|
||||
|
||||
const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip');
|
||||
const adminSkill = await readZipText(pluginPath, 'skills/ktx/SKILL.md');
|
||||
const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
|
||||
const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
|
||||
expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
|
||||
await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
|
||||
const adminSkill = await readZipText(adminSkillPath, 'ktx/SKILL.md');
|
||||
expect(adminSkill).toContain(`--project-dir ${tempDir}`);
|
||||
expect(adminSkill).toContain('status --json');
|
||||
expect(await readZipText(pluginPath, 'SETUP.md')).toContain('admin CLI skill');
|
||||
await expect(readZipText(pluginPath, '.mcp.json')).rejects.toThrow('Missing zip entry');
|
||||
await expect(readZipText(adminSkillPath, '.mcp.json')).rejects.toThrow('Missing zip entry');
|
||||
await expect(readZipText(adminSkillPath, 'ktx-analytics/SKILL.md')).rejects.toThrow('Missing zip entry');
|
||||
expect(io.stdout()).toContain(analyticsSkillPath);
|
||||
expect(io.stdout()).toContain(adminSkillPath);
|
||||
expect(io.stdout()).toContain('Upload each file separately:');
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
|
|
@ -798,7 +824,7 @@ describe('setup agents', () => {
|
|||
await expect(readKtxAgentInstallManifest(tempDir)).resolves.toEqual(null);
|
||||
});
|
||||
|
||||
it('removes generated Claude Desktop plugin from the manifest', async () => {
|
||||
it('removes generated Claude Desktop skill zips from the manifest', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = home;
|
||||
|
|
@ -817,10 +843,12 @@ describe('setup agents', () => {
|
|||
},
|
||||
io.io,
|
||||
);
|
||||
const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip');
|
||||
const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
|
||||
const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
|
||||
const launcherPath = join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh');
|
||||
const configPath = join(home, 'Library/Application Support/Claude/claude_desktop_config.json');
|
||||
await expect(stat(pluginPath)).resolves.toBeDefined();
|
||||
await expect(stat(analyticsSkillPath)).resolves.toBeDefined();
|
||||
await expect(stat(adminSkillPath)).resolves.toBeDefined();
|
||||
await expect(stat(launcherPath)).resolves.toBeDefined();
|
||||
const beforeConfig = JSON.parse(await readFile(configPath, 'utf-8')) as {
|
||||
mcpServers: Record<string, unknown>;
|
||||
|
|
@ -829,7 +857,8 @@ describe('setup agents', () => {
|
|||
|
||||
await expect(removeKtxAgentInstall(tempDir, io.io)).resolves.toBe(0);
|
||||
|
||||
await expect(stat(pluginPath)).rejects.toThrow();
|
||||
await expect(stat(analyticsSkillPath)).rejects.toThrow();
|
||||
await expect(stat(adminSkillPath)).rejects.toThrow();
|
||||
await expect(stat(launcherPath)).rejects.toThrow();
|
||||
const afterConfig = JSON.parse(await readFile(configPath, 'utf-8')) as {
|
||||
mcpServers: Record<string, unknown>;
|
||||
|
|
@ -867,7 +896,7 @@ describe('setup agents', () => {
|
|||
).resolves.toEqual({ status: 'skipped', projectDir: tempDir });
|
||||
});
|
||||
|
||||
it('explains how to select multiple agent targets in interactive mode', async () => {
|
||||
it('prints one navigation hint before interactive agent target prompts', async () => {
|
||||
const io = makeIo();
|
||||
const prompts = {
|
||||
select: vi.fn(async () => 'mcp-cli'),
|
||||
|
|
@ -891,10 +920,11 @@ describe('setup agents', () => {
|
|||
),
|
||||
).resolves.toEqual({ status: 'back', projectDir: tempDir });
|
||||
|
||||
expect(io.stdout()).toContain('Space to select, Enter to confirm, Esc to go back.');
|
||||
expect(io.stdout().match(/Space to select/g)).toHaveLength(1);
|
||||
expect(prompts.multiselect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
'Which agent targets should KTX install?\nUse Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
|
||||
message: 'Which agent targets should KTX install?',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -917,21 +947,21 @@ describe('setup agents', () => {
|
|||
);
|
||||
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('Agent integration complete');
|
||||
expect(output).toContain(`KTX project\n ${tempDir}`);
|
||||
expect(output).toContain('Installed agents');
|
||||
expect(output).toContain('Claude Code');
|
||||
expect(output).toContain(`Project scope\n ${join(tempDir, '.mcp.json')}`);
|
||||
expect(output).toContain('Requires MCP to be started');
|
||||
expect(output).toContain('Analytics skill installed');
|
||||
expect(output).toContain('Admin CLI skill installed');
|
||||
expect(output).toContain('Claude Code · Project scope');
|
||||
expect(output).toContain(join(tempDir, '.mcp.json'));
|
||||
expect(output).toContain('Requires MCP to be started.');
|
||||
expect(output).toContain('Analytics skill installed.');
|
||||
expect(output).toContain('Admin CLI skill installed.');
|
||||
expect(output).not.toContain('Agent integration complete');
|
||||
expect(output).not.toContain(`KTX project\n ${tempDir}`);
|
||||
expect(output).not.toContain('Installed agents');
|
||||
expect(output).not.toContain('.claude/skills/ktx-analytics/SKILL.md');
|
||||
expect(output).not.toContain('.claude/skills/ktx/SKILL.md');
|
||||
expect(output).not.toContain('.claude/rules/ktx.md');
|
||||
});
|
||||
|
||||
it('formats summary with explicit project-scoped config paths', () => {
|
||||
const summary = formatInstallSummary(
|
||||
const summary = formatInstallSummaryLines(
|
||||
[{ target: 'cursor', scope: 'project', mode: 'mcp-cli' }],
|
||||
[
|
||||
{ kind: 'file', path: join(tempDir, '.cursor/rules/ktx-analytics.mdc'), role: 'analytics-skill' },
|
||||
|
|
@ -941,16 +971,20 @@ describe('setup agents', () => {
|
|||
tempDir,
|
||||
);
|
||||
|
||||
expect(summary).toContain('Cursor');
|
||||
expect(summary).toContain(`Project scope\n ${join(tempDir, '.cursor/mcp.json')}`);
|
||||
expect(summary).toContain('Requires MCP to be started');
|
||||
expect(summary).toContain('Cursor rules installed');
|
||||
expect(summary).not.toContain('.cursor/rules/ktx-analytics.mdc');
|
||||
expect(summary).not.toContain('.cursor/rules/ktx.mdc');
|
||||
expect(summary).toEqual([
|
||||
{
|
||||
title: 'Cursor · Project scope',
|
||||
lines: [
|
||||
join(tempDir, '.cursor/mcp.json'),
|
||||
'Requires MCP to be started.',
|
||||
'Cursor rules installed.',
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('formats summary with multiple agent targets', () => {
|
||||
const summary = formatInstallSummary(
|
||||
const summary = formatInstallSummaryLines(
|
||||
[
|
||||
{ target: 'claude-code', scope: 'project', mode: 'mcp-cli' },
|
||||
{ target: 'codex', scope: 'project', mode: 'mcp-cli' },
|
||||
|
|
@ -967,16 +1001,25 @@ describe('setup agents', () => {
|
|||
tempDir,
|
||||
);
|
||||
|
||||
expect(summary).toContain('Claude Code');
|
||||
expect(summary).toContain('Project scope\n ');
|
||||
expect(summary).toContain('Analytics skill installed');
|
||||
expect(summary).toContain('Admin CLI skill installed');
|
||||
expect(summary).toContain('\n\n Codex\n');
|
||||
expect(summary).toContain('MCP config\n Add the snippet shown below to ~/.codex/config.toml.');
|
||||
expect(summary).toContain('Codex');
|
||||
expect(summary).toContain('Codex guidance installed');
|
||||
expect(summary).not.toContain('.agents/skills/ktx-analytics/SKILL.md');
|
||||
expect(summary).not.toContain('.agents/skills/ktx/SKILL.md');
|
||||
expect(summary).toEqual([
|
||||
{
|
||||
title: 'Claude Code · Project scope',
|
||||
lines: [
|
||||
join(tempDir, '.mcp.json'),
|
||||
'Requires MCP to be started.',
|
||||
'Analytics skill installed.',
|
||||
'Admin CLI skill installed.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Codex · Project scope',
|
||||
lines: [
|
||||
'Add the snippet shown below to ~/.codex/config.toml.',
|
||||
'Requires MCP to be started.',
|
||||
'Codex guidance installed.',
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('prints one target-aware next actions block for mixed agent targets', async () => {
|
||||
|
|
@ -1026,9 +1069,13 @@ describe('setup agents', () => {
|
|||
expect(output).toContain('RUN:');
|
||||
expect(output).toContain(`cd '${tempDir}'`);
|
||||
expect(output).toContain('3. Restart Claude Desktop');
|
||||
expect(output).toContain('Claude Desktop loads KTX after restart.');
|
||||
expect(output).not.toContain('install plugin');
|
||||
expect(output).not.toContain(join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'));
|
||||
expect(output).toContain('Claude Desktop loads KTX MCP after restart.');
|
||||
expect(output).toContain('4. Upload Claude Desktop skills');
|
||||
expect(output).toContain('Customize > Skills > + > Create skill > Upload a skill');
|
||||
expect(output).toContain(join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'));
|
||||
expect(output).not.toContain(join(tempDir, '.ktx/agents/claude/ktx.zip'));
|
||||
expect(output).toContain('Upload this file:');
|
||||
expect(output).toContain('All set.');
|
||||
expect(output).not.toContain('Finish Claude Desktop setup');
|
||||
expect(output).not.toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
|
||||
} finally {
|
||||
|
|
@ -1123,4 +1170,63 @@ describe('setup agents', () => {
|
|||
expect(output).toContain('OpenCode commands installed');
|
||||
expect(output).toContain('.agents guidance installed');
|
||||
});
|
||||
|
||||
describe('createAgentNextActionsLineFormatter', () => {
|
||||
function makeColorStdout(): { write: (chunk: string) => boolean; hasColors: () => boolean } {
|
||||
return { write: () => true, hasColors: () => true };
|
||||
}
|
||||
|
||||
function makePlainStdout(): { write: (chunk: string) => boolean; hasColors: () => boolean } {
|
||||
return { write: () => true, hasColors: () => false };
|
||||
}
|
||||
|
||||
const ESC = String.fromCharCode(27);
|
||||
|
||||
it('returns the line untouched when the stream cannot render colors', () => {
|
||||
const format = createAgentNextActionsLineFormatter(makePlainStdout());
|
||||
expect(format('2. Upload Claude Desktop skills')).toBe('2. Upload Claude Desktop skills');
|
||||
expect(format(' /tmp/ktx/.ktx/agents/claude/ktx.zip')).toBe(' /tmp/ktx/.ktx/agents/claude/ktx.zip');
|
||||
});
|
||||
|
||||
it('styles step headings and aligns sub-prose under the title', () => {
|
||||
const format = createAgentNextActionsLineFormatter(makeColorStdout());
|
||||
const heading = format('2. Upload Claude Desktop skills');
|
||||
expect(heading).toContain(ESC);
|
||||
expect(heading).toContain('2');
|
||||
expect(heading).toContain('Upload Claude Desktop skills');
|
||||
expect(heading).not.toMatch(/^2\. /);
|
||||
|
||||
const sub = format(' Toggle the uploaded KTX skills on.');
|
||||
expect(sub).toMatch(/^ {3}/);
|
||||
expect(sub).toContain('Toggle the uploaded KTX skills on.');
|
||||
});
|
||||
|
||||
it('renders skill bundle .zip paths as bullets and shortens HOME to ~', () => {
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = '/tmp/test-home';
|
||||
try {
|
||||
const format = createAgentNextActionsLineFormatter(makeColorStdout());
|
||||
const line = format(' /tmp/test-home/.ktx/agents/claude/ktx-analytics.zip');
|
||||
expect(line).toContain('•');
|
||||
expect(line).toContain('~/.ktx/agents/claude/ktx-analytics.zip');
|
||||
expect(line).not.toContain('/tmp/test-home/');
|
||||
} finally {
|
||||
if (previousHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = previousHome;
|
||||
}
|
||||
});
|
||||
|
||||
it('replaces breadcrumb separators with a typographic chevron', () => {
|
||||
const format = createAgentNextActionsLineFormatter(makeColorStdout());
|
||||
const line = format(' Open Claude Desktop: Customize > Skills > + > Create skill > Upload a skill.');
|
||||
expect(line).toContain('›');
|
||||
expect(line).not.toContain(' > ');
|
||||
});
|
||||
|
||||
it('leaves already-styled lines untouched to avoid double-wrapping', () => {
|
||||
const format = createAgentNextActionsLineFormatter(makeColorStdout());
|
||||
const preStyled = `${ESC}[1m2. Already styled${ESC}[22m`;
|
||||
expect(format(preStyled)).toBe(preStyled);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { existsSync } from 'node:fs';
|
||||
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
import type { Writable } from 'node:stream';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { styleText } from 'node:util';
|
||||
import { log, outro } from '@clack/prompts';
|
||||
import {
|
||||
loadKtxProject,
|
||||
markKtxSetupStateStepComplete,
|
||||
|
|
@ -9,7 +12,6 @@ import {
|
|||
} from '@ktx/context/project';
|
||||
import { strToU8, zipSync } from 'fflate';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { withMultiselectNavigation } from './prompt-navigation.js';
|
||||
import {
|
||||
createKtxSetupPromptAdapter,
|
||||
createKtxSetupUiAdapter,
|
||||
|
|
@ -51,7 +53,11 @@ export interface KtxAgentInstallManifest {
|
|||
installedAt: string;
|
||||
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>;
|
||||
entries: Array<
|
||||
| { kind: 'file'; path: string; role?: 'skill' | 'rule' | 'analytics-skill' | 'claude-plugin' | 'launcher' }
|
||||
| {
|
||||
kind: 'file';
|
||||
path: string;
|
||||
role?: 'skill' | 'rule' | 'analytics-skill' | 'claude-desktop-skill-bundle' | 'launcher';
|
||||
}
|
||||
| { kind: 'json-key'; path: string; jsonPath: string[] }
|
||||
>;
|
||||
}
|
||||
|
|
@ -77,6 +83,88 @@ interface KtxCliLauncher {
|
|||
args: string[];
|
||||
}
|
||||
|
||||
function isWritableTtyOutput(output: KtxCliIo['stdout']): output is KtxCliIo['stdout'] & Writable {
|
||||
return (
|
||||
output.isTTY === true &&
|
||||
typeof (output as { on?: unknown }).on === 'function' &&
|
||||
typeof (output as { columns?: unknown }).columns !== 'undefined'
|
||||
);
|
||||
}
|
||||
|
||||
function writeSetupInfo(io: KtxCliIo, message: string): void {
|
||||
if (isWritableTtyOutput(io.stdout)) {
|
||||
log.info(message, { output: io.stdout });
|
||||
return;
|
||||
}
|
||||
io.stdout.write(`${message}\n`);
|
||||
}
|
||||
|
||||
function writeSetupStep(io: KtxCliIo, message: string): void {
|
||||
if (isWritableTtyOutput(io.stdout)) {
|
||||
log.step(message, { output: io.stdout });
|
||||
return;
|
||||
}
|
||||
io.stdout.write(`\n${message}\n`);
|
||||
}
|
||||
|
||||
function writeSetupOutro(io: KtxCliIo, message: string): void {
|
||||
if (isWritableTtyOutput(io.stdout)) {
|
||||
outro(message, { output: io.stdout });
|
||||
return;
|
||||
}
|
||||
io.stdout.write(`\n${message}\n`);
|
||||
}
|
||||
|
||||
const STEP_HEADING_RE = /^(\d+)\. (.+)$/;
|
||||
const ACTION_MARKER_RE = /^(RUN|PASTE|USE|OPEN):$/;
|
||||
|
||||
export function createAgentNextActionsLineFormatter(
|
||||
stdout: KtxCliIo['stdout'],
|
||||
): (line: string) => string {
|
||||
const maybeHasColors = (stdout as { hasColors?: unknown }).hasColors;
|
||||
const supportsColor = typeof maybeHasColors === 'function' && Boolean(maybeHasColors.call(stdout));
|
||||
if (!supportsColor) return (line) => line;
|
||||
|
||||
const homeDir = process.env.HOME ? resolve(process.env.HOME) : '';
|
||||
const styleOptions = { validateStream: false } as const;
|
||||
const dim = (s: string) => styleText('dim', s, styleOptions);
|
||||
const bold = (s: string) => styleText('bold', s, styleOptions);
|
||||
const cyanBold = (s: string) => styleText(['cyan', 'bold'], s, styleOptions);
|
||||
const dimCyan = (s: string) => styleText(['dim', 'cyan'], s, styleOptions);
|
||||
const shortenPath = (path: string): string => {
|
||||
if (!homeDir) return path;
|
||||
if (path === homeDir) return '~';
|
||||
if (path.startsWith(`${homeDir}/`)) return `~/${path.slice(homeDir.length + 1)}`;
|
||||
return path;
|
||||
};
|
||||
|
||||
return (rawLine: string): string => {
|
||||
if (rawLine.length === 0 || rawLine.includes('[')) return rawLine;
|
||||
|
||||
const heading = rawLine.match(STEP_HEADING_RE);
|
||||
if (heading) {
|
||||
return `${cyanBold(heading[1])} ${bold(heading[2])}`;
|
||||
}
|
||||
|
||||
if (!rawLine.startsWith(' ')) return rawLine;
|
||||
const body = rawLine.slice(2);
|
||||
|
||||
if (ACTION_MARKER_RE.test(body)) {
|
||||
return ` ${dim(body)}`;
|
||||
}
|
||||
|
||||
if (body.endsWith('.zip') && (body.startsWith('/') || body.startsWith('~'))) {
|
||||
return ` ${dimCyan('•')} ${shortenPath(body)}`;
|
||||
}
|
||||
|
||||
if (body.includes(' > ')) {
|
||||
return ` ${body.replaceAll(' > ', ` ${dim('›')} `)}`;
|
||||
}
|
||||
|
||||
return ` ${dim(body)}`;
|
||||
};
|
||||
}
|
||||
|
||||
async function readJsonObject(path: string): Promise<Record<string, unknown>> {
|
||||
if (!existsSync(path)) return {};
|
||||
const parsed = JSON.parse(await readFile(path, 'utf-8')) as unknown;
|
||||
|
|
@ -310,8 +398,12 @@ export function agentInstallManifestPath(projectDir: string): string {
|
|||
return join(resolve(projectDir), '.ktx/agents/install-manifest.json');
|
||||
}
|
||||
|
||||
function claudeDesktopPluginPath(projectDir: string): string {
|
||||
return join(resolve(projectDir), '.ktx/agents/claude/ktx-plugin.zip');
|
||||
function claudeDesktopAnalyticsSkillBundlePath(projectDir: string): string {
|
||||
return join(resolve(projectDir), '.ktx/agents/claude/ktx-analytics.zip');
|
||||
}
|
||||
|
||||
function claudeDesktopAdminSkillBundlePath(projectDir: string): string {
|
||||
return join(resolve(projectDir), '.ktx/agents/claude/ktx.zip');
|
||||
}
|
||||
|
||||
function claudeDesktopLauncherPath(projectDir: string): string {
|
||||
|
|
@ -357,7 +449,20 @@ export function plannedKtxAgentFiles(input: {
|
|||
if (input.target === 'claude-desktop') {
|
||||
return [
|
||||
{ kind: 'file', path: claudeDesktopLauncherPath(input.projectDir), role: 'launcher' as const },
|
||||
{ kind: 'file', path: claudeDesktopPluginPath(input.projectDir), role: 'claude-plugin' as const },
|
||||
{
|
||||
kind: 'file',
|
||||
path: claudeDesktopAnalyticsSkillBundlePath(input.projectDir),
|
||||
role: 'claude-desktop-skill-bundle' as const,
|
||||
},
|
||||
...(withAdminCli
|
||||
? [
|
||||
{
|
||||
kind: 'file' as const,
|
||||
path: claudeDesktopAdminSkillBundlePath(input.projectDir),
|
||||
role: 'claude-desktop-skill-bundle' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
throw new Error(`Global ${input.target} installation is not supported; omit --global.`);
|
||||
|
|
@ -487,43 +592,7 @@ function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLaun
|
|||
].join('\n');
|
||||
}
|
||||
|
||||
function claudePluginJsonContent(): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
name: 'ktx',
|
||||
version: '0.0.0-local',
|
||||
description: 'KTX analytics workflow guidance and local MCP tools.',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
function claudePluginVersionContent(): string {
|
||||
return `${JSON.stringify({ version: '0.0.0-local' }, null, 2)}\n`;
|
||||
}
|
||||
|
||||
function claudePluginSetupContent(input: { projectDir: string; withAdminCli: boolean }): string {
|
||||
return [
|
||||
'# KTX Claude Plugin',
|
||||
'',
|
||||
'This package is generated by KTX setup. Claude Desktop loads KTX through the registered `claude_desktop_config.json` entry after restart; no manual plugin install step is required.',
|
||||
'',
|
||||
`KTX project: \`${input.projectDir}\``,
|
||||
'',
|
||||
'Included:',
|
||||
'',
|
||||
'- `ktx-analytics` skill for the MCP analytics workflow',
|
||||
...(input.withAdminCli ? ['- `ktx` admin CLI skill for KTX maintenance commands'] : []),
|
||||
'',
|
||||
'The KTX MCP server is registered separately in `claude_desktop_config.json` by `ktx setup` and runs as a local stdio child of Claude Desktop — no daemon to start.',
|
||||
'',
|
||||
'If this checkout or project directory moves, rerun `ktx setup --agents` and restart Claude Desktop.',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function claudePluginLauncherContent(input: { launcher: KtxCliLauncher }): string {
|
||||
function claudeDesktopLauncherContent(input: { launcher: KtxCliLauncher }): string {
|
||||
const binPath = input.launcher.args[0];
|
||||
if (!binPath) {
|
||||
throw new Error('Expected KTX CLI launcher to include a bin path.');
|
||||
|
|
@ -572,40 +641,45 @@ function claudePluginLauncherContent(input: { launcher: KtxCliLauncher }): strin
|
|||
' run_with_node "$(command -v node)" "$@"',
|
||||
'fi',
|
||||
'',
|
||||
'echo "KTX plugin could not find Node.js. Set KTX_NODE to a Node executable and reinstall the plugin." >&2',
|
||||
'echo "KTX Claude Desktop launcher could not find Node.js. Set KTX_NODE to a Node executable and rerun ktx setup --agents." >&2',
|
||||
'exit 127',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function writeClaudeDesktopPlugin(input: {
|
||||
async function writeClaudeDesktopSkillBundle(input: {
|
||||
projectDir: string;
|
||||
path: string;
|
||||
mode: KtxAgentInstallMode;
|
||||
skillName: 'ktx-analytics' | 'ktx';
|
||||
launcher: KtxCliLauncher;
|
||||
}): Promise<void> {
|
||||
const withAdminCli = input.mode === 'mcp-cli';
|
||||
const content =
|
||||
input.skillName === 'ktx-analytics'
|
||||
? await readAnalyticsSkillContent()
|
||||
: cliInstructionContent({ projectDir: input.projectDir, launcher: input.launcher });
|
||||
const files: Record<string, Uint8Array> = {
|
||||
'.claude-plugin/plugin.json': strToU8(claudePluginJsonContent()),
|
||||
'version.json': strToU8(claudePluginVersionContent()),
|
||||
'skills/ktx-analytics/SKILL.md': strToU8(await readAnalyticsSkillContent()),
|
||||
'SETUP.md': strToU8(claudePluginSetupContent({ projectDir: input.projectDir, withAdminCli })),
|
||||
[`${input.skillName}/SKILL.md`]: strToU8(content),
|
||||
};
|
||||
if (withAdminCli) {
|
||||
files['skills/ktx/SKILL.md'] = strToU8(
|
||||
cliInstructionContent({ projectDir: input.projectDir, launcher: input.launcher }),
|
||||
);
|
||||
}
|
||||
await mkdir(dirname(input.path), { recursive: true });
|
||||
await writeFile(input.path, Buffer.from(zipSync(files)));
|
||||
}
|
||||
|
||||
function claudeDesktopSkillNameForBundle(path: string): 'ktx-analytics' | 'ktx' {
|
||||
if (path.endsWith('/ktx-analytics.zip')) {
|
||||
return 'ktx-analytics';
|
||||
}
|
||||
if (path.endsWith('/ktx.zip')) {
|
||||
return 'ktx';
|
||||
}
|
||||
throw new Error(`Unsupported Claude Desktop skill bundle path: ${path}`);
|
||||
}
|
||||
|
||||
async function writeClaudeDesktopLauncher(input: {
|
||||
path: string;
|
||||
launcher: KtxCliLauncher;
|
||||
}): Promise<void> {
|
||||
await mkdir(dirname(input.path), { recursive: true });
|
||||
await writeFile(input.path, claudePluginLauncherContent({ launcher: input.launcher }), 'utf-8');
|
||||
await writeFile(input.path, claudeDesktopLauncherContent({ launcher: input.launcher }), 'utf-8');
|
||||
await chmod(input.path, 0o755);
|
||||
}
|
||||
|
||||
|
|
@ -764,7 +838,6 @@ function guidanceInstallLine(target: KtxAgentTarget): string {
|
|||
if (target === 'cursor') return 'Cursor rules installed';
|
||||
if (target === 'opencode') return 'OpenCode commands installed';
|
||||
if (target === 'universal') return '.agents guidance installed';
|
||||
if (target === 'claude-desktop') return 'Claude Desktop skills bundled';
|
||||
return 'Agent guidance installed';
|
||||
}
|
||||
|
||||
|
|
@ -780,12 +853,27 @@ function hasAdminCliEntries(entries: InstallEntry[]): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export function formatInstallSummary(
|
||||
export interface InstallSummaryEntry {
|
||||
title: string;
|
||||
lines: string[];
|
||||
}
|
||||
|
||||
function formatInlinePath(path: string): string {
|
||||
const home = process.env.HOME;
|
||||
if (!home) return path;
|
||||
const resolvedHome = resolve(home);
|
||||
if (path === resolvedHome) return '~';
|
||||
if (path.startsWith(`${resolvedHome}/`)) {
|
||||
return `~/${relative(resolvedHome, path)}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function formatInstallSummaryLines(
|
||||
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>,
|
||||
entries: InstallEntry[],
|
||||
projectDir: string,
|
||||
): string {
|
||||
const resolvedProjectDir = resolve(projectDir);
|
||||
): InstallSummaryEntry[] {
|
||||
const entriesByTarget = new Map<KtxAgentTarget, InstallEntry[]>();
|
||||
for (const install of installs) {
|
||||
const plannedFilePaths = new Set(
|
||||
|
|
@ -807,41 +895,71 @@ export function formatInstallSummary(
|
|||
);
|
||||
}
|
||||
|
||||
const lines: string[] = ['KTX project', ` ${resolvedProjectDir}`, '', 'Installed agents'];
|
||||
for (const install of installs) {
|
||||
return installs.map((install) => {
|
||||
const targetEntries = entriesByTarget.get(install.target) ?? [];
|
||||
const mcpEntry = mcpEntriesByTarget
|
||||
.get(install.target)
|
||||
?.find((entry): entry is Extract<InstallEntry, { kind: 'json-key' }> => entry.kind === 'json-key');
|
||||
lines.push('', ` ${targetDisplayName(install.target)}`);
|
||||
const lines: string[] = [];
|
||||
|
||||
if (mcpEntry) {
|
||||
lines.push(` ${scopeDisplayName(install.scope)}`);
|
||||
lines.push(` ${mcpEntry.path}`);
|
||||
lines.push(formatInlinePath(mcpEntry.path));
|
||||
} else if (install.target !== 'claude-desktop') {
|
||||
lines.push(' MCP config');
|
||||
lines.push(` ${manualMcpConfigInstruction(install.target, install.scope)}`);
|
||||
lines.push(manualMcpConfigInstruction(install.target, install.scope));
|
||||
}
|
||||
|
||||
if (targetUsesHttpMcpDaemon(install.target)) {
|
||||
lines.push(' Requires MCP to be started');
|
||||
lines.push('Requires MCP to be started.');
|
||||
}
|
||||
|
||||
const hasAnalytics = hasEntryRole(targetEntries, 'analytics-skill');
|
||||
const hasAdmin = hasAdminCliEntries(targetEntries);
|
||||
const hasPlugin = hasEntryRole(targetEntries, 'claude-plugin');
|
||||
const claudeDesktopSkillBundles = targetEntries.filter(
|
||||
(entry): entry is Extract<InstallEntry, { kind: 'file' }> =>
|
||||
entry.kind === 'file' && entry.role === 'claude-desktop-skill-bundle',
|
||||
);
|
||||
|
||||
if (install.target === 'claude-code') {
|
||||
if (hasAnalytics) {
|
||||
lines.push(' Analytics skill installed');
|
||||
lines.push('Analytics skill installed.');
|
||||
}
|
||||
if (hasAdmin) {
|
||||
lines.push(' Admin CLI skill installed');
|
||||
lines.push('Admin CLI skill installed.');
|
||||
}
|
||||
} else if (hasAnalytics || hasAdmin || hasPlugin) {
|
||||
lines.push(` ${guidanceInstallLine(install.target)}`);
|
||||
} else if (install.target === 'claude-desktop') {
|
||||
if (claudeDesktopSkillBundles.length > 0) {
|
||||
lines.push('Skill bundles:');
|
||||
for (const bundle of claudeDesktopSkillBundles) {
|
||||
lines.push(` ${bundle.path}`);
|
||||
}
|
||||
}
|
||||
} else if (hasAnalytics || hasAdmin) {
|
||||
lines.push(`${guidanceInstallLine(install.target)}.`);
|
||||
}
|
||||
|
||||
if (hasEntryRole(targetEntries, 'launcher')) {
|
||||
lines.push(' Starts KTX over stdio from Claude Desktop');
|
||||
lines.push('Starts KTX over stdio from Claude Desktop.');
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
|
||||
return {
|
||||
title: `${targetDisplayName(install.target)} · ${scopeDisplayName(install.scope)}`,
|
||||
lines,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function claudeDesktopSkillBundlePathsForInstalls(
|
||||
projectDir: string,
|
||||
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>,
|
||||
): string[] {
|
||||
return installs
|
||||
.filter((install) => install.target === 'claude-desktop')
|
||||
.flatMap((install) => plannedKtxAgentFiles({ projectDir, ...install }))
|
||||
.filter(
|
||||
(entry): entry is Extract<InstallEntry, { kind: 'file' }> =>
|
||||
entry.kind === 'file' && entry.role === 'claude-desktop-skill-bundle',
|
||||
)
|
||||
.map((entry) => entry.path);
|
||||
}
|
||||
|
||||
function humanList(values: string[]): string {
|
||||
|
|
@ -981,9 +1099,22 @@ function formatAgentNextActions(input: {
|
|||
|
||||
if (input.installs.some((install) => install.target === 'claude-desktop')) {
|
||||
lines.push(`${step}. Restart Claude Desktop`);
|
||||
lines.push(' Claude Desktop loads KTX after restart.');
|
||||
lines.push(' Claude Desktop loads KTX MCP after restart.');
|
||||
pushBlankLine(lines);
|
||||
step += 1;
|
||||
|
||||
const skillBundlePaths = claudeDesktopSkillBundlePathsForInstalls(projectDir, input.installs);
|
||||
if (skillBundlePaths.length > 0) {
|
||||
lines.push(`${step}. Upload Claude Desktop skills`);
|
||||
lines.push(' Open Claude Desktop: Customize > Skills > + > Create skill > Upload a skill.');
|
||||
lines.push(skillBundlePaths.length === 1 ? ' Upload this file:' : ' Upload each file separately:');
|
||||
for (const path of skillBundlePaths) {
|
||||
lines.push(` ${path}`);
|
||||
}
|
||||
lines.push(' Toggle the uploaded KTX skills on.');
|
||||
pushBlankLine(lines);
|
||||
step += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
|
|
@ -1008,11 +1139,11 @@ async function installTarget(input: {
|
|||
await writeClaudeDesktopLauncher({ path: entry.path, launcher });
|
||||
continue;
|
||||
}
|
||||
if (entry.role === 'claude-plugin') {
|
||||
await writeClaudeDesktopPlugin({
|
||||
if (entry.role === 'claude-desktop-skill-bundle') {
|
||||
await writeClaudeDesktopSkillBundle({
|
||||
projectDir: input.projectDir,
|
||||
path: entry.path,
|
||||
mode: input.mode,
|
||||
skillName: claudeDesktopSkillNameForBundle(entry.path),
|
||||
launcher,
|
||||
});
|
||||
continue;
|
||||
|
|
@ -1049,6 +1180,9 @@ export async function runKtxSetupAgentsStep(
|
|||
}
|
||||
|
||||
const prompts = deps.prompts ?? createPromptAdapter();
|
||||
if (args.inputMode === 'auto' && args.target === undefined) {
|
||||
writeSetupInfo(io, 'Space to select, Enter to confirm, Esc to go back.');
|
||||
}
|
||||
const mode =
|
||||
args.inputMode === 'disabled'
|
||||
? args.mode
|
||||
|
|
@ -1075,7 +1209,7 @@ export async function runKtxSetupAgentsStep(
|
|||
: args.inputMode === 'disabled'
|
||||
? []
|
||||
: ((await prompts.multiselect({
|
||||
message: withMultiselectNavigation('Which agent targets should KTX install?'),
|
||||
message: 'Which agent targets should KTX install?',
|
||||
options: [
|
||||
{ value: 'claude-code', label: 'Claude Code' },
|
||||
{ value: 'claude-desktop', label: 'Claude Desktop' },
|
||||
|
|
@ -1139,11 +1273,12 @@ export async function runKtxSetupAgentsStep(
|
|||
);
|
||||
await markAgentsComplete(args.projectDir);
|
||||
const setupUi = createKtxSetupUiAdapter();
|
||||
setupUi.note(
|
||||
formatInstallSummary(installs, entries, args.projectDir),
|
||||
'Agent integration complete',
|
||||
io,
|
||||
);
|
||||
for (const summary of formatInstallSummaryLines(installs, entries, args.projectDir)) {
|
||||
writeSetupStep(
|
||||
io,
|
||||
summary.lines.length > 0 ? `${summary.title}\n${summary.lines.join('\n')}` : summary.title,
|
||||
);
|
||||
}
|
||||
const nextActions = formatAgentNextActions({
|
||||
projectDir: args.projectDir,
|
||||
installs,
|
||||
|
|
@ -1151,7 +1286,10 @@ export async function runKtxSetupAgentsStep(
|
|||
snippets,
|
||||
});
|
||||
if (args.showNextActions !== false) {
|
||||
setupUi.note(nextActions, 'Required before using agents', io, { format: (line) => line });
|
||||
setupUi.note(nextActions, 'Required before using agents', io, {
|
||||
format: createAgentNextActionsLineFormatter(io.stdout),
|
||||
});
|
||||
writeSetupOutro(io, 'All set.');
|
||||
}
|
||||
return { status: 'ready', projectDir: args.projectDir, installs, nextActions };
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -517,7 +517,11 @@ describe('setup status', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
const output = testIo.stdout();
|
||||
expect(output).toContain('Agent integration complete');
|
||||
expect(output).toContain('Claude Code · Project scope');
|
||||
expect(output).toContain(join(tempDir, '.mcp.json'));
|
||||
expect(output).toContain('Requires MCP to be started.');
|
||||
expect(output).toContain('Analytics skill installed.');
|
||||
expect(output).not.toContain('Agent integration complete');
|
||||
expect(output).toContain('Finish KTX agent setup');
|
||||
expect(output).not.toContain('KTX project ready');
|
||||
expect(output).toContain('REQUIRED BEFORE USING AGENTS');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue