mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
Polish ktx setup-agents output: hint, summaries, outro
This commit is contained in:
parent
ddabe517e3
commit
0cbf8121d9
4 changed files with 1151 additions and 87 deletions
|
|
@ -5,7 +5,7 @@ import { readKtxSetupState } from '@ktx/context/project';
|
|||
import { strFromU8, unzipSync } from 'fflate';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
formatInstallSummary,
|
||||
formatInstallSummaryLines,
|
||||
plannedKtxAgentFiles,
|
||||
readKtxAgentInstallManifest,
|
||||
removeKtxAgentInstall,
|
||||
|
|
@ -84,7 +84,7 @@ describe('setup agents', () => {
|
|||
{ kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' },
|
||||
{
|
||||
kind: 'file',
|
||||
path: join(tempDir, '.ktx/agents/claude/ktx-skills.zip'),
|
||||
path: join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'),
|
||||
role: 'claude-desktop-skill-bundle',
|
||||
},
|
||||
]);
|
||||
|
|
@ -129,7 +129,12 @@ describe('setup agents', () => {
|
|||
{ kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' },
|
||||
{
|
||||
kind: 'file',
|
||||
path: join(tempDir, '.ktx/agents/claude/ktx-skills.zip'),
|
||||
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',
|
||||
},
|
||||
]);
|
||||
|
|
@ -205,6 +210,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');
|
||||
});
|
||||
|
|
@ -231,8 +237,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 () => {
|
||||
|
|
@ -452,9 +460,11 @@ describe('setup agents', () => {
|
|||
installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp' }],
|
||||
});
|
||||
|
||||
const skillBundlePath = join(tempDir, '.ktx/agents/claude/ktx-skills.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(skillBundlePath)).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');
|
||||
|
|
@ -470,15 +480,16 @@ describe('setup agents', () => {
|
|||
args: ['--project-dir', tempDir, 'mcp', 'stdio'],
|
||||
});
|
||||
|
||||
expect(await readZipText(skillBundlePath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
|
||||
await expect(readZipText(skillBundlePath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
|
||||
await expect(readZipText(skillBundlePath, '.claude-plugin/plugin.json')).rejects.toThrow('Missing zip entry');
|
||||
await expect(readZipText(skillBundlePath, 'skills/ktx-analytics/SKILL.md')).rejects.toThrow(
|
||||
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()).toContain(skillBundlePath);
|
||||
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');
|
||||
|
|
@ -569,13 +580,18 @@ describe('setup agents', () => {
|
|||
installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp-cli' }],
|
||||
});
|
||||
|
||||
const skillBundlePath = join(tempDir, '.ktx/agents/claude/ktx-skills.zip');
|
||||
expect(await readZipText(skillBundlePath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
|
||||
const adminSkill = await readZipText(skillBundlePath, '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');
|
||||
await expect(readZipText(skillBundlePath, '.mcp.json')).rejects.toThrow('Missing zip entry');
|
||||
expect(io.stdout()).toContain(skillBundlePath);
|
||||
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 });
|
||||
|
|
@ -826,10 +842,12 @@ describe('setup agents', () => {
|
|||
},
|
||||
io.io,
|
||||
);
|
||||
const skillBundlePath = join(tempDir, '.ktx/agents/claude/ktx-skills.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(skillBundlePath)).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>;
|
||||
|
|
@ -838,7 +856,8 @@ describe('setup agents', () => {
|
|||
|
||||
await expect(removeKtxAgentInstall(tempDir, io.io)).resolves.toBe(0);
|
||||
|
||||
await expect(stat(skillBundlePath)).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>;
|
||||
|
|
@ -876,7 +895,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'),
|
||||
|
|
@ -900,10 +919,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?',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -926,21 +946,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' },
|
||||
|
|
@ -950,16 +970,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' },
|
||||
|
|
@ -976,16 +1000,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 () => {
|
||||
|
|
@ -1038,8 +1071,10 @@ describe('setup agents', () => {
|
|||
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-skills.zip'));
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
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 { log, outro } from '@clack/prompts';
|
||||
import {
|
||||
loadKtxProject,
|
||||
markKtxSetupStateStepComplete,
|
||||
|
|
@ -9,7 +11,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,
|
||||
|
|
@ -81,6 +82,38 @@ 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`);
|
||||
}
|
||||
|
||||
async function readJsonObject(path: string): Promise<Record<string, unknown>> {
|
||||
if (!existsSync(path)) return {};
|
||||
const parsed = JSON.parse(await readFile(path, 'utf-8')) as unknown;
|
||||
|
|
@ -314,8 +347,12 @@ export function agentInstallManifestPath(projectDir: string): string {
|
|||
return join(resolve(projectDir), '.ktx/agents/install-manifest.json');
|
||||
}
|
||||
|
||||
function claudeDesktopSkillBundlePath(projectDir: string): string {
|
||||
return join(resolve(projectDir), '.ktx/agents/claude/ktx-skills.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 {
|
||||
|
|
@ -363,9 +400,18 @@ export function plannedKtxAgentFiles(input: {
|
|||
{ kind: 'file', path: claudeDesktopLauncherPath(input.projectDir), role: 'launcher' as const },
|
||||
{
|
||||
kind: 'file',
|
||||
path: claudeDesktopSkillBundlePath(input.projectDir),
|
||||
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.`);
|
||||
|
|
@ -553,21 +599,30 @@ function claudeDesktopLauncherContent(input: { launcher: KtxCliLauncher }): stri
|
|||
async function writeClaudeDesktopSkillBundle(input: {
|
||||
projectDir: string;
|
||||
path: string;
|
||||
mode: KtxAgentInstallMode;
|
||||
skillName: 'ktx-analytics' | 'ktx';
|
||||
launcher: KtxCliLauncher;
|
||||
}): Promise<void> {
|
||||
const content =
|
||||
input.skillName === 'ktx-analytics'
|
||||
? await readAnalyticsSkillContent()
|
||||
: cliInstructionContent({ projectDir: input.projectDir, launcher: input.launcher });
|
||||
const files: Record<string, Uint8Array> = {
|
||||
'ktx-analytics/SKILL.md': strToU8(await readAnalyticsSkillContent()),
|
||||
[`${input.skillName}/SKILL.md`]: strToU8(content),
|
||||
};
|
||||
if (input.mode === 'mcp-cli') {
|
||||
files['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;
|
||||
|
|
@ -747,12 +802,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(
|
||||
|
|
@ -774,51 +844,57 @@ 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 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 (install.target === 'claude-desktop') {
|
||||
if (claudeDesktopSkillBundles.length > 0) {
|
||||
lines.push(' Claude Desktop skill uploads');
|
||||
lines.push('Skill bundles:');
|
||||
for (const bundle of claudeDesktopSkillBundles) {
|
||||
lines.push(` ${bundle.path}`);
|
||||
lines.push(` ${bundle.path}`);
|
||||
}
|
||||
}
|
||||
} else if (hasAnalytics || hasAdmin) {
|
||||
lines.push(` ${guidanceInstallLine(install.target)}`);
|
||||
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(
|
||||
|
|
@ -980,7 +1056,7 @@ function formatAgentNextActions(input: {
|
|||
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(' Upload this file:');
|
||||
lines.push(skillBundlePaths.length === 1 ? ' Upload this file:' : ' Upload each file separately:');
|
||||
for (const path of skillBundlePaths) {
|
||||
lines.push(` ${path}`);
|
||||
}
|
||||
|
|
@ -1016,7 +1092,7 @@ async function installTarget(input: {
|
|||
await writeClaudeDesktopSkillBundle({
|
||||
projectDir: input.projectDir,
|
||||
path: entry.path,
|
||||
mode: input.mode,
|
||||
skillName: claudeDesktopSkillNameForBundle(entry.path),
|
||||
launcher,
|
||||
});
|
||||
continue;
|
||||
|
|
@ -1053,6 +1129,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
|
||||
|
|
@ -1079,7 +1158,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' },
|
||||
|
|
@ -1143,11 +1222,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,
|
||||
|
|
@ -1156,6 +1236,7 @@ export async function runKtxSetupAgentsStep(
|
|||
});
|
||||
if (args.showNextActions !== false) {
|
||||
setupUi.note(nextActions, 'Required before using agents', io, { format: (line) => line });
|
||||
writeSetupOutro(io, 'All set.');
|
||||
}
|
||||
return { status: 'ready', projectDir: args.projectDir, installs, nextActions };
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue