@skillit/client 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +40 -0
  2. package/dist/bin.d.ts +2 -1
  3. package/dist/bin.d.ts.map +1 -1
  4. package/dist/bin.js +21 -13
  5. package/dist/bin.js.map +1 -1
  6. package/dist/commands/audit.d.ts +34 -0
  7. package/dist/commands/audit.d.ts.map +1 -0
  8. package/dist/commands/audit.js +139 -0
  9. package/dist/commands/audit.js.map +1 -0
  10. package/dist/commands/gen.d.ts +21 -0
  11. package/dist/commands/gen.d.ts.map +1 -0
  12. package/dist/commands/gen.js +112 -0
  13. package/dist/commands/gen.js.map +1 -0
  14. package/dist/commands/init.d.ts +8 -22
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +39 -81
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/commands/refine.d.ts +34 -2
  19. package/dist/commands/refine.d.ts.map +1 -1
  20. package/dist/commands/refine.js +61 -29
  21. package/dist/commands/refine.js.map +1 -1
  22. package/dist/detect-source.d.ts.map +1 -1
  23. package/dist/detect-source.js +12 -5
  24. package/dist/detect-source.js.map +1 -1
  25. package/dist/generate.d.ts +74 -0
  26. package/dist/generate.d.ts.map +1 -0
  27. package/dist/generate.js +68 -0
  28. package/dist/generate.js.map +1 -0
  29. package/dist/mcp-mode.d.ts +22 -0
  30. package/dist/mcp-mode.d.ts.map +1 -0
  31. package/dist/mcp-mode.js +27 -0
  32. package/dist/mcp-mode.js.map +1 -0
  33. package/dist/model/anthropic.d.ts +11 -0
  34. package/dist/model/anthropic.d.ts.map +1 -1
  35. package/dist/model/anthropic.js +16 -2
  36. package/dist/model/anthropic.js.map +1 -1
  37. package/dist/model/cli/adapters.d.ts.map +1 -1
  38. package/dist/model/cli/adapters.js +35 -2
  39. package/dist/model/cli/adapters.js.map +1 -1
  40. package/dist/model/cli/cli-client.d.ts.map +1 -1
  41. package/dist/model/cli/cli-client.js +4 -2
  42. package/dist/model/cli/cli-client.js.map +1 -1
  43. package/dist/postinstall-template.d.ts +2 -0
  44. package/dist/postinstall-template.d.ts.map +1 -0
  45. package/dist/postinstall-template.js +61 -0
  46. package/dist/postinstall-template.js.map +1 -0
  47. package/dist/program.d.ts +12 -0
  48. package/dist/program.d.ts.map +1 -0
  49. package/dist/program.js +42 -0
  50. package/dist/program.js.map +1 -0
  51. package/dist/typedoc-entry.d.ts +6 -0
  52. package/dist/typedoc-entry.d.ts.map +1 -0
  53. package/dist/typedoc-entry.js +14 -0
  54. package/dist/typedoc-entry.js.map +1 -0
  55. package/package.json +18 -7
  56. package/skills/skillit-bootstrap/SKILL.md +115 -0
  57. package/skills/skillit-bootstrap/references/surface-routing.md +108 -0
  58. package/CHANGELOG.md +0 -32
  59. package/scripts/gen-refine-cli-skill.mjs +0 -48
  60. package/src/__tests__/anthropic-model.test.ts +0 -45
  61. package/src/__tests__/anthropic-prompt.test.ts +0 -102
  62. package/src/__tests__/cli-adapters.test.ts +0 -119
  63. package/src/__tests__/cli-client.test.ts +0 -58
  64. package/src/__tests__/cli-run.test.ts +0 -52
  65. package/src/__tests__/detect-mode.test.ts +0 -114
  66. package/src/__tests__/detect-source.test.ts +0 -158
  67. package/src/__tests__/fixtures/bin-with-program.mjs +0 -6
  68. package/src/__tests__/init.test.ts +0 -218
  69. package/src/__tests__/model-client-factory.test.ts +0 -28
  70. package/src/__tests__/refine-resolve.test.ts +0 -72
  71. package/src/bin.ts +0 -18
  72. package/src/commands/init.ts +0 -207
  73. package/src/commands/refine.ts +0 -261
  74. package/src/detect-mode.ts +0 -94
  75. package/src/detect-source.ts +0 -123
  76. package/src/index.ts +0 -3
  77. package/src/model/anthropic.ts +0 -116
  78. package/src/model/cli/adapters.ts +0 -178
  79. package/src/model/cli/cli-client.ts +0 -52
  80. package/src/model/cli/run.ts +0 -83
  81. package/src/model/model-client-factory.ts +0 -62
  82. package/src/model/models.ts +0 -7
  83. package/tsconfig.build.json +0 -9
  84. package/tsconfig.json +0 -4
@@ -1,52 +0,0 @@
1
- // packages/client/src/__tests__/cli-run.test.ts
2
- import { describe, it, expect } from 'vitest';
3
- import { runCli } from '../model/cli/run.js';
4
-
5
- describe('runCli', () => {
6
- it('returns stdout for a successful command', async () => {
7
- const out = await runCli({ cmd: 'node', args: ['-e', 'process.stdout.write("hello")'] });
8
- expect(out).toBe('hello');
9
- });
10
-
11
- it('writes input to stdin when provided', async () => {
12
- const out = await runCli({
13
- cmd: 'node',
14
- args: ['-e', 'process.stdin.pipe(process.stdout)'],
15
- input: 'piped-in'
16
- });
17
- expect(out).toBe('piped-in');
18
- });
19
-
20
- it('throws with the command and a stderr tail on non-zero exit', async () => {
21
- await expect(
22
- runCli({ cmd: 'node', args: ['-e', 'process.stderr.write("boom"); process.exit(3)'] })
23
- ).rejects.toThrow(/node.*code 3.*boom/s);
24
- });
25
-
26
- it('does not leak args (e.g. a prompt passed as an argument) in the error', async () => {
27
- let message = '';
28
- try {
29
- await runCli({ cmd: 'node', args: ['-e', 'process.exit(2)', 'SENSITIVE_PROMPT_TEXT'] });
30
- } catch (error) {
31
- message = error instanceof Error ? error.message : String(error);
32
- }
33
- expect(message).toMatch(/code 2/);
34
- expect(message).not.toContain('SENSITIVE_PROMPT_TEXT');
35
- });
36
-
37
- it('rejects (does not crash on EPIPE) when the child exits before reading a large stdin prompt', async () => {
38
- // 256 KB overflows the OS pipe buffer; the child exits without reading it,
39
- // so the stdin write would EPIPE. Must reject with the exit code, not throw
40
- // an unhandled stream error.
41
- const bigPrompt = 'x'.repeat(256 * 1024);
42
- await expect(
43
- runCli({ cmd: 'node', args: ['-e', 'process.exit(1)'], input: bigPrompt })
44
- ).rejects.toThrow(/code 1/);
45
- });
46
-
47
- it('throws a timeout error when the command exceeds timeoutMs', async () => {
48
- await expect(
49
- runCli({ cmd: 'node', args: ['-e', 'setTimeout(()=>{}, 10000)'], timeoutMs: 100 })
50
- ).rejects.toThrow(/timed out after 100ms/);
51
- });
52
- });
@@ -1,114 +0,0 @@
1
- import { detectRefineMode } from '../detect-mode.js';
2
- import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { afterEach, describe, expect, it } from 'vitest';
6
-
7
- let tmpDir: string;
8
-
9
- afterEach(async () => {
10
- if (tmpDir) await rm(tmpDir, { recursive: true, force: true });
11
- });
12
-
13
- describe('detectRefineMode', () => {
14
- it('returns build when package.json has MCP SDK dependency', async () => {
15
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
16
- await writeFile(
17
- join(tmpDir, 'package.json'),
18
- JSON.stringify({ dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' } })
19
- );
20
- expect(await detectRefineMode(tmpDir)).toBe('build');
21
- });
22
-
23
- it('returns runtime when mcp.json found in cwd', async () => {
24
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
25
- await writeFile(join(tmpDir, 'mcp.json'), '{}');
26
- expect(await detectRefineMode(tmpDir)).toBe('runtime');
27
- });
28
-
29
- it('returns runtime when mcp.json found in ancestor', async () => {
30
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
31
- const sub = join(tmpDir, 'project');
32
- await mkdir(sub, { recursive: true });
33
- await writeFile(join(tmpDir, 'mcp.json'), '{}');
34
- expect(await detectRefineMode(sub)).toBe('runtime');
35
- });
36
-
37
- it('returns ambiguous when both signals present', async () => {
38
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
39
- await writeFile(
40
- join(tmpDir, 'package.json'),
41
- JSON.stringify({ dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' } })
42
- );
43
- await writeFile(join(tmpDir, 'mcp.json'), '{}');
44
- expect(await detectRefineMode(tmpDir)).toBe('ambiguous');
45
- });
46
-
47
- it('returns ambiguous when neither signal present', async () => {
48
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
49
- expect(await detectRefineMode(tmpDir)).toBe('ambiguous');
50
- });
51
-
52
- it('returns build when package.json has a @modelcontextprotocol/server-* package', async () => {
53
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
54
- await writeFile(
55
- join(tmpDir, 'package.json'),
56
- JSON.stringify({ dependencies: { '@modelcontextprotocol/server-node': '^1.0.0' } })
57
- );
58
- expect(await detectRefineMode(tmpDir)).toBe('build');
59
- });
60
-
61
- it('returns runtime when mcpConfigPath is a known runtime config filename', async () => {
62
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
63
- // No mcp.json in cwd, no package.json — but --mcp points to a desktop config
64
- expect(
65
- await detectRefineMode(tmpDir, '/Users/example/.config/claude/claude_desktop_config.json')
66
- ).toBe('runtime');
67
- });
68
-
69
- it('returns ambiguous (not build) when package.json only has @modelcontextprotocol/inspector', async () => {
70
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
71
- await writeFile(
72
- join(tmpDir, 'package.json'),
73
- JSON.stringify({ dependencies: { '@modelcontextprotocol/inspector': '^1.0.0' } })
74
- );
75
- // inspector is a consumer-only tool — should not signal build mode
76
- expect(await detectRefineMode(tmpDir)).toBe('ambiguous');
77
- });
78
-
79
- it('returns ambiguous when cwd has MCP SDK dep AND mcpConfigPath is a runtime config', async () => {
80
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
81
- await writeFile(
82
- join(tmpDir, 'package.json'),
83
- JSON.stringify({ dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' } })
84
- );
85
- expect(
86
- await detectRefineMode(tmpDir, '/Users/example/.config/claude/claude_desktop_config.json')
87
- ).toBe('ambiguous');
88
- });
89
-
90
- it('returns build when package.json is in an ancestor directory (monorepo nested cwd)', async () => {
91
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
92
- await writeFile(
93
- join(tmpDir, 'package.json'),
94
- JSON.stringify({ dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' } })
95
- );
96
- const nested = join(tmpDir, 'packages', 'my-server', 'src');
97
- await mkdir(nested, { recursive: true });
98
- expect(await detectRefineMode(nested)).toBe('build');
99
- });
100
-
101
- it('returns runtime when mcpConfigPath contains mcpServers key (custom basename)', async () => {
102
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
103
- const configPath = join(tmpDir, 'my-custom-config.json');
104
- await writeFile(configPath, JSON.stringify({ mcpServers: { myServer: {} } }));
105
- expect(await detectRefineMode(tmpDir, configPath)).toBe('runtime');
106
- });
107
-
108
- it('returns ambiguous when mcpConfigPath has unknown basename and no mcpServers key', async () => {
109
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-'));
110
- const configPath = join(tmpDir, 'settings.json');
111
- await writeFile(configPath, JSON.stringify({ someOtherKey: true }));
112
- expect(await detectRefineMode(tmpDir, configPath)).toBe('ambiguous');
113
- });
114
- });
@@ -1,158 +0,0 @@
1
- import {
2
- detectInstalledSources,
3
- detectPackageManager,
4
- detectProjectNature,
5
- detectRefineSource
6
- } from '../detect-source.js';
7
- import { mkdtemp, rm, writeFile } from 'node:fs/promises';
8
- import { tmpdir } from 'node:os';
9
- import { join } from 'node:path';
10
- import { fileURLToPath } from 'node:url';
11
- import { afterEach, describe, expect, it } from 'vitest';
12
-
13
- let tmpDir: string;
14
-
15
- afterEach(async () => {
16
- if (tmpDir) await rm(tmpDir, { recursive: true, force: true });
17
- });
18
-
19
- async function writePkg(deps: Record<string, unknown>): Promise<string> {
20
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-source-'));
21
- await writeFile(join(tmpDir, 'package.json'), JSON.stringify(deps));
22
- return tmpDir;
23
- }
24
-
25
- describe('detectRefineSource', () => {
26
- it('returns cli when only @skillit/cli is installed', async () => {
27
- const dir = await writePkg({ dependencies: { '@skillit/cli': 'workspace:*' } });
28
- expect(await detectRefineSource(dir)).toBe('cli');
29
- });
30
-
31
- it('returns mcp when only @skillit/mcp is installed', async () => {
32
- const dir = await writePkg({ dependencies: { '@skillit/mcp': 'workspace:*' } });
33
- expect(await detectRefineSource(dir)).toBe('mcp');
34
- });
35
-
36
- it('returns typedoc when typedoc-plugin-skillit is installed', async () => {
37
- const dir = await writePkg({ devDependencies: { 'typedoc-plugin-skillit': '^1.0.0' } });
38
- expect(await detectRefineSource(dir)).toBe('typedoc');
39
- });
40
-
41
- it('returns typedoc when @skillit/typedoc is installed', async () => {
42
- const dir = await writePkg({ devDependencies: { '@skillit/typedoc': 'workspace:*' } });
43
- expect(await detectRefineSource(dir)).toBe('typedoc');
44
- });
45
-
46
- it('returns ambiguous when cli and mcp are both installed', async () => {
47
- const dir = await writePkg({
48
- dependencies: { '@skillit/cli': 'workspace:*', '@skillit/mcp': 'workspace:*' }
49
- });
50
- expect(await detectRefineSource(dir)).toBe('ambiguous');
51
- });
52
-
53
- it('returns none when no skillit source package is installed', async () => {
54
- const dir = await writePkg({ dependencies: { commander: '^14.0.0' } });
55
- expect(await detectRefineSource(dir)).toBe('none');
56
- });
57
-
58
- it('returns none when package.json is missing or unreadable', async () => {
59
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-source-'));
60
- expect(await detectRefineSource(tmpDir)).toBe('none');
61
- });
62
- });
63
-
64
- describe('detectInstalledSources', () => {
65
- it('returns the deduped candidate list in stable order', async () => {
66
- const dir = await writePkg({
67
- dependencies: { '@skillit/mcp': 'workspace:*', '@skillit/cli': 'workspace:*' }
68
- });
69
- expect(await detectInstalledSources(dir)).toEqual(['cli', 'mcp']);
70
- });
71
-
72
- it('returns an empty list when no source package is installed', async () => {
73
- const dir = await writePkg({ dependencies: { commander: '^14.0.0' } });
74
- expect(await detectInstalledSources(dir)).toEqual([]);
75
- });
76
-
77
- it('returns an empty list when package.json is missing or unreadable', async () => {
78
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-source-'));
79
- expect(await detectInstalledSources(tmpDir)).toEqual([]);
80
- });
81
- });
82
-
83
- describe('detectProjectNature', () => {
84
- it('returns cli when commander is a dependency', async () => {
85
- const dir = await writePkg({ dependencies: { commander: '^14.0.0' } });
86
- expect(await detectProjectNature(dir)).toBe('cli');
87
- });
88
-
89
- it('returns cli when yargs is a dependency', async () => {
90
- const dir = await writePkg({ dependencies: { yargs: '^17.0.0' } });
91
- expect(await detectProjectNature(dir)).toBe('cli');
92
- });
93
-
94
- it('returns mcp when @modelcontextprotocol/sdk is a dependency', async () => {
95
- const dir = await writePkg({ dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' } });
96
- expect(await detectProjectNature(dir)).toBe('mcp');
97
- });
98
-
99
- it('prefers cli over mcp when both indicators are present', async () => {
100
- const dir = await writePkg({
101
- dependencies: { commander: '^14.0.0', '@modelcontextprotocol/sdk': '^1.0.0' }
102
- });
103
- expect(await detectProjectNature(dir)).toBe('cli');
104
- });
105
-
106
- it('returns cli when a loadable bin exports a commander Command (no dep)', async () => {
107
- // The bin fixture lives in-workspace so its `commander` import resolves;
108
- // the temp dir only holds a package.json pointing `bin` at it.
109
- const binPath = fileURLToPath(new URL('./fixtures/bin-with-program.mjs', import.meta.url));
110
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-source-'));
111
- await writeFile(
112
- join(tmpDir, 'package.json'),
113
- JSON.stringify({ name: 'bin-only', bin: binPath })
114
- );
115
- expect(await detectProjectNature(tmpDir)).toBe('cli');
116
- });
117
-
118
- it('falls through to mcp when the bin does not load a commander Command', async () => {
119
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-source-'));
120
- await writeFile(
121
- join(tmpDir, 'package.json'),
122
- JSON.stringify({
123
- name: 'mcp-no-bin',
124
- dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' }
125
- })
126
- );
127
- expect(await detectProjectNature(tmpDir)).toBe('mcp');
128
- });
129
-
130
- it('returns typedoc for a plain TS library', async () => {
131
- const dir = await writePkg({ dependencies: { zod: '^3.0.0' } });
132
- expect(await detectProjectNature(dir)).toBe('typedoc');
133
- });
134
-
135
- it('returns typedoc when package.json is missing', async () => {
136
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-source-'));
137
- expect(await detectProjectNature(tmpDir)).toBe('typedoc');
138
- });
139
- });
140
-
141
- describe('detectPackageManager', () => {
142
- it('returns pnpm when pnpm-lock.yaml is present', async () => {
143
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-source-'));
144
- await writeFile(join(tmpDir, 'pnpm-lock.yaml'), '');
145
- expect(detectPackageManager(tmpDir)).toBe('pnpm');
146
- });
147
-
148
- it('returns yarn when yarn.lock is present', async () => {
149
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-source-'));
150
- await writeFile(join(tmpDir, 'yarn.lock'), '');
151
- expect(detectPackageManager(tmpDir)).toBe('yarn');
152
- });
153
-
154
- it('returns npm when no lockfile is present', async () => {
155
- tmpDir = await mkdtemp(join(tmpdir(), 'detect-source-'));
156
- expect(detectPackageManager(tmpDir)).toBe('npm');
157
- });
158
- });
@@ -1,6 +0,0 @@
1
- import { Command } from 'commander';
2
-
3
- const program = new Command().name('bin-tool');
4
- program.command('build').description('Build the project');
5
-
6
- export { program };
@@ -1,218 +0,0 @@
1
- import { buildInitCommand, type GenerateSkillOpts, type InitDeps } from '../commands/init.js';
2
- import type { RefineCommandOpts } from '../commands/refine.js';
3
- import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
6
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
-
8
- let tmpDir: string;
9
- let prevCwd: string;
10
-
11
- beforeEach(() => {
12
- prevCwd = process.cwd();
13
- });
14
-
15
- afterEach(async () => {
16
- process.chdir(prevCwd);
17
- if (tmpDir) await rm(tmpDir, { recursive: true, force: true });
18
- });
19
-
20
- /**
21
- * A cli-nature project with a pnpm lockfile. Returns the realized cwd
22
- * (`process.cwd()` after chdir), since macOS resolves `/tmp` → `/private/tmp`.
23
- */
24
- async function writeCliFixture(): Promise<string> {
25
- tmpDir = await mkdtemp(join(tmpdir(), 'init-'));
26
- await writeFile(
27
- join(tmpDir, 'package.json'),
28
- JSON.stringify({ name: '@scope/my-tool', dependencies: { commander: '^14.0.0' } })
29
- );
30
- await writeFile(join(tmpDir, 'pnpm-lock.yaml'), '');
31
- process.chdir(tmpDir);
32
- return process.cwd();
33
- }
34
-
35
- /** An mcp-nature project (no commander/yargs, no loadable bin). */
36
- async function writeMcpFixture(): Promise<string> {
37
- tmpDir = await mkdtemp(join(tmpdir(), 'init-'));
38
- await writeFile(
39
- join(tmpDir, 'package.json'),
40
- JSON.stringify({
41
- name: 'my-server',
42
- dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' }
43
- })
44
- );
45
- await writeFile(join(tmpDir, 'pnpm-lock.yaml'), '');
46
- process.chdir(tmpDir);
47
- return process.cwd();
48
- }
49
-
50
- interface InstallCall {
51
- pkg: string;
52
- pm: string;
53
- cwd: string;
54
- }
55
-
56
- function makeStubs(overrides?: Partial<InitDeps>): {
57
- deps: InitDeps;
58
- installCalls: InstallCall[];
59
- generateCalls: GenerateSkillOpts[];
60
- refineCalls: unknown[];
61
- } {
62
- const installCalls: InstallCall[] = [];
63
- const generateCalls: GenerateSkillOpts[] = [];
64
- const refineCalls: unknown[] = [];
65
- const deps: InitDeps = {
66
- runInstall: async (pkg, pm, cwd) => {
67
- installCalls.push({ pkg, pm, cwd });
68
- },
69
- generateSkill: async (opts) => {
70
- generateCalls.push(opts);
71
- },
72
- runRefine: async (opts) => {
73
- refineCalls.push(opts);
74
- },
75
- ...overrides
76
- };
77
- return { deps, installCalls, generateCalls, refineCalls };
78
- }
79
-
80
- async function run(deps: InitDeps, argv: string[] = []): Promise<void> {
81
- const cmd = buildInitCommand(deps);
82
- await cmd.parseAsync(argv, { from: 'user' });
83
- }
84
-
85
- describe('buildInitCommand', () => {
86
- it('chooses @skillit/cli with the detected pnpm add command for a cli project', async () => {
87
- const dir = await writeCliFixture();
88
- const { deps, installCalls } = makeStubs();
89
- await run(deps);
90
- expect(installCalls).toHaveLength(1);
91
- expect(installCalls[0]!.pkg).toBe('@skillit/cli');
92
- expect(installCalls[0]!.pm).toBe('pnpm');
93
- expect(installCalls[0]!.cwd).toBe(dir);
94
- });
95
-
96
- it('generates the skill into <cwd>/skills before and after refine', async () => {
97
- const dir = await writeCliFixture();
98
- const { deps, generateCalls } = makeStubs();
99
- await run(deps);
100
- // Generated once before refine (initial) and once after (reflecting the
101
- // freshly-written JSDoc), both into the same out dir.
102
- expect(generateCalls).toHaveLength(2);
103
- expect(generateCalls[0]!.outDir).toBe(join(dir, 'skills'));
104
- expect(generateCalls[1]!.outDir).toBe(join(dir, 'skills'));
105
- });
106
-
107
- it('runs refine between the initial generate and the post-refine regenerate', async () => {
108
- await writeCliFixture();
109
- const order: string[] = [];
110
- const { deps } = makeStubs({
111
- generateSkill: async () => {
112
- order.push('generate');
113
- },
114
- runRefine: async () => {
115
- order.push('refine');
116
- }
117
- });
118
- await run(deps);
119
- expect(order).toEqual(['generate', 'refine', 'generate']);
120
- });
121
-
122
- it('dispatches refine with the cli source', async () => {
123
- await writeCliFixture();
124
- const { deps, refineCalls } = makeStubs();
125
- await run(deps);
126
- expect(refineCalls).toHaveLength(1);
127
- expect((refineCalls[0] as { source?: string }).source).toBe('cli');
128
- });
129
-
130
- it('respects an explicit --out directory', async () => {
131
- const dir = await writeCliFixture();
132
- const { deps, generateCalls } = makeStubs();
133
- await run(deps, ['--out', 'docs/skills']);
134
- expect(generateCalls[0]!.outDir).toBe(join(dir, 'docs/skills'));
135
- });
136
-
137
- it('throws with the exact command on install failure and skips generate + refine', async () => {
138
- await writeCliFixture();
139
- const generate = vi.fn();
140
- const refine = vi.fn();
141
- const { deps } = makeStubs({
142
- runInstall: async (pkg, pm) => {
143
- throw new Error(`install of ${pkg} via ${pm} failed`);
144
- },
145
- generateSkill: generate,
146
- runRefine: refine
147
- });
148
- await expect(run(deps)).rejects.toThrow(/pnpm add -D @skillit\/cli/);
149
- expect(generate).not.toHaveBeenCalled();
150
- expect(refine).not.toHaveBeenCalled();
151
- });
152
-
153
- it('rejects an invalid --source value', async () => {
154
- await writeCliFixture();
155
- const { deps } = makeStubs();
156
- await expect(run(deps, ['--source', 'bogus'])).rejects.toThrow(/cli\|mcp\|typedoc/);
157
- });
158
-
159
- it("degrades gracefully (no throw, no refine, prints guidance) when the cli program won't load", async () => {
160
- await writeCliFixture();
161
- const refine = vi.fn();
162
- const { deps } = makeStubs({
163
- generateSkill: async () => {
164
- throw new Error('not a commander program');
165
- },
166
- runRefine: refine
167
- });
168
- // Capture via plain reassignment: vitest's console interceptor bypasses
169
- // vi.spyOn(console, 'log') under this config.
170
- const logged: string[] = [];
171
- const originalLog = console.log;
172
- console.log = (...args: unknown[]): void => {
173
- logged.push(String(args[0]));
174
- };
175
- try {
176
- await expect(run(deps)).resolves.toBeUndefined();
177
- } finally {
178
- console.log = originalLog;
179
- }
180
- expect(refine).not.toHaveBeenCalled();
181
- const out = logged.join('\n');
182
- expect(out).toMatch(/Installed @skillit\/cli/);
183
- expect(out).toMatch(/not a commander program/);
184
- expect(out).toMatch(/skillit refine --source cli --program/);
185
- });
186
-
187
- it('installs @skillit/mcp but skips generate + refine for an mcp project', async () => {
188
- await writeMcpFixture();
189
- const { deps, installCalls, generateCalls, refineCalls } = makeStubs();
190
- // Override console.log directly: vitest's console interceptor bypasses
191
- // vi.spyOn(console, 'log') here, so capture via a plain reassignment.
192
- const logged: string[] = [];
193
- const originalLog = console.log;
194
- console.log = (...args: unknown[]): void => {
195
- logged.push(String(args[0]));
196
- };
197
- try {
198
- await run(deps);
199
- } finally {
200
- console.log = originalLog;
201
- }
202
- expect(installCalls).toHaveLength(1);
203
- expect(installCalls[0]!.pkg).toBe('@skillit/mcp');
204
- // CLI-first: no auto generate or refine for the mcp source this pass.
205
- expect(generateCalls).toHaveLength(0);
206
- expect(refineCalls).toHaveLength(0);
207
- expect(logged.join('\n')).toMatch(/skillit refine --source mcp/);
208
- });
209
-
210
- it('threads --model-client and --model-cli-timeout into the refine dispatch', async () => {
211
- await writeCliFixture();
212
- const { deps, refineCalls } = makeStubs();
213
- await run(deps, ['--source', 'cli', '--model-client', 'claude', '--model-cli-timeout', '7000']);
214
- const opts = refineCalls[0] as RefineCommandOpts;
215
- expect(opts.modelClient).toBe('claude');
216
- expect(opts.modelCliTimeout).toBe('7000');
217
- });
218
- });
@@ -1,28 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { createModelClient } from '../model/model-client-factory.js';
3
- import { AnthropicModelClient } from '../model/anthropic.js';
4
- import { CliModelClient } from '../model/cli/cli-client.js';
5
-
6
- describe('createModelClient', () => {
7
- it("returns an AnthropicModelClient for 'api'", () => {
8
- expect(createModelClient('api')).toBeInstanceOf(AnthropicModelClient);
9
- });
10
-
11
- it('returns a CliModelClient for a cli kind whose binary is present', () => {
12
- // inject a pre-flight that reports the binary exists
13
- const client = createModelClient('claude', { hasBinary: () => true });
14
- expect(client).toBeInstanceOf(CliModelClient);
15
- });
16
-
17
- it('throws an actionable error when the cli binary is missing', () => {
18
- expect(() => createModelClient('codex', { hasBinary: () => false })).toThrow(
19
- /codex CLI not found on PATH.*--model-client api/s
20
- );
21
- });
22
-
23
- it('throws on an invalid kind', () => {
24
- expect(() => createModelClient('bogus')).toThrow(
25
- /invalid --model-client.*api\|claude\|codex\|copilot/s
26
- );
27
- });
28
- });
@@ -1,72 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { resolveModelClientKind, resolveRefineSource } from '../commands/refine.js';
3
-
4
- describe('resolveModelClientKind', () => {
5
- it("defaults to 'api' when --model-client is omitted", () => {
6
- expect(resolveModelClientKind(undefined)).toBe('api');
7
- });
8
- it('passes through a provided kind', () => {
9
- expect(resolveModelClientKind('claude')).toBe('claude');
10
- });
11
- });
12
-
13
- describe('resolveRefineSource', () => {
14
- it('explicit --source cli wins over detection', () => {
15
- const res = resolveRefineSource({ source: 'cli' }, 'mcp');
16
- expect(res).toEqual({ kind: 'cli' });
17
- });
18
-
19
- it('explicit --source mcp requires --mcp', () => {
20
- const res = resolveRefineSource({ source: 'mcp' }, 'none');
21
- expect('error' in res && res.error).toMatch(/--mcp/);
22
- });
23
-
24
- it('explicit --source mcp with --mcp resolves to mcp', () => {
25
- const res = resolveRefineSource({ source: 'mcp', mcp: 'mcp.json' }, 'none');
26
- expect(res).toEqual({ kind: 'mcp' });
27
- });
28
-
29
- it('rejects an invalid --source value', () => {
30
- const res = resolveRefineSource({ source: 'bogus' }, 'cli');
31
- expect('error' in res && res.error).toMatch(/cli|mcp|typedoc/);
32
- });
33
-
34
- it('falls back to detection when --source is omitted', () => {
35
- const res = resolveRefineSource({}, 'cli');
36
- expect(res).toEqual({ kind: 'cli' });
37
- });
38
-
39
- it('detected mcp still requires --mcp', () => {
40
- const res = resolveRefineSource({}, 'mcp');
41
- expect('error' in res && res.error).toMatch(/--mcp/);
42
- });
43
-
44
- it('detected mcp with --mcp resolves to mcp', () => {
45
- const res = resolveRefineSource({ mcp: 'mcp.json' }, 'mcp');
46
- expect(res).toEqual({ kind: 'mcp' });
47
- });
48
-
49
- it('errors on ambiguous detection listing candidates and --source form', () => {
50
- const res = resolveRefineSource({}, 'ambiguous', ['cli', 'mcp']);
51
- const error = 'error' in res ? res.error : '';
52
- expect(error).toMatch(/--source <cli\|mcp\|typedoc>/);
53
- expect(error).toMatch(/cli/);
54
- expect(error).toMatch(/mcp/);
55
- expect(error).toMatch(/found: cli, mcp/);
56
- });
57
-
58
- it('errors on no detected source with the --source form', () => {
59
- const res = resolveRefineSource({}, 'none');
60
- expect('error' in res && res.error).toMatch(/--source <cli\|mcp\|typedoc>/);
61
- });
62
-
63
- it('errors that typedoc is not yet supported', () => {
64
- const res = resolveRefineSource({ source: 'typedoc' }, 'none');
65
- expect('error' in res && res.error).toMatch(/not yet supported/);
66
- });
67
-
68
- it('errors that detected typedoc is not yet supported', () => {
69
- const res = resolveRefineSource({}, 'typedoc');
70
- expect('error' in res && res.error).toMatch(/not yet supported/);
71
- });
72
- });
package/src/bin.ts DELETED
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env node
2
- // packages/client/src/bin.ts
3
- import { Command } from 'commander';
4
- import { buildMcpCommand, reportMcpErrorAndExit } from '@skillit/mcp';
5
- import { buildInitCommand } from './commands/init.js';
6
- import { buildRefineCommand } from './commands/refine.js';
7
-
8
- const program = new Command('skillit').description('skillit CLI').version('0.1.0');
9
-
10
- program.addCommand(buildRefineCommand());
11
- program.addCommand(buildInitCommand());
12
- program.addCommand(buildMcpCommand());
13
-
14
- program.parseAsync(process.argv).catch((err: unknown) => {
15
- // Preserve the mcp exit-code contract for the `skillit mcp …` path; other
16
- // errors keep the prior generic behavior (stderr message + exit 1).
17
- reportMcpErrorAndExit(err);
18
- });