@skillit/client 0.2.0 → 0.3.1

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 (77) 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 +20 -0
  11. package/dist/commands/gen.d.ts.map +1 -0
  12. package/dist/commands/gen.js +108 -0
  13. package/dist/commands/gen.js.map +1 -0
  14. package/dist/commands/init.d.ts +3 -22
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +23 -84
  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/generate.d.ts +67 -0
  23. package/dist/generate.d.ts.map +1 -0
  24. package/dist/generate.js +55 -0
  25. package/dist/generate.js.map +1 -0
  26. package/dist/mcp-mode.d.ts +22 -0
  27. package/dist/mcp-mode.d.ts.map +1 -0
  28. package/dist/mcp-mode.js +27 -0
  29. package/dist/mcp-mode.js.map +1 -0
  30. package/dist/model/anthropic.d.ts +11 -0
  31. package/dist/model/anthropic.d.ts.map +1 -1
  32. package/dist/model/anthropic.js +16 -2
  33. package/dist/model/anthropic.js.map +1 -1
  34. package/dist/model/cli/adapters.d.ts.map +1 -1
  35. package/dist/model/cli/adapters.js +35 -2
  36. package/dist/model/cli/adapters.js.map +1 -1
  37. package/dist/model/cli/cli-client.d.ts.map +1 -1
  38. package/dist/model/cli/cli-client.js +4 -2
  39. package/dist/model/cli/cli-client.js.map +1 -1
  40. package/dist/program.d.ts +12 -0
  41. package/dist/program.d.ts.map +1 -0
  42. package/dist/program.js +42 -0
  43. package/dist/program.js.map +1 -0
  44. package/dist/typedoc-entry.d.ts +6 -0
  45. package/dist/typedoc-entry.d.ts.map +1 -0
  46. package/dist/typedoc-entry.js +14 -0
  47. package/dist/typedoc-entry.js.map +1 -0
  48. package/package.json +18 -7
  49. package/skills/skillit-bootstrap/SKILL.md +115 -0
  50. package/skills/skillit-bootstrap/references/surface-routing.md +97 -0
  51. package/CHANGELOG.md +0 -32
  52. package/scripts/gen-refine-cli-skill.mjs +0 -48
  53. package/src/__tests__/anthropic-model.test.ts +0 -45
  54. package/src/__tests__/anthropic-prompt.test.ts +0 -102
  55. package/src/__tests__/cli-adapters.test.ts +0 -119
  56. package/src/__tests__/cli-client.test.ts +0 -58
  57. package/src/__tests__/cli-run.test.ts +0 -52
  58. package/src/__tests__/detect-mode.test.ts +0 -114
  59. package/src/__tests__/detect-source.test.ts +0 -158
  60. package/src/__tests__/fixtures/bin-with-program.mjs +0 -6
  61. package/src/__tests__/init.test.ts +0 -218
  62. package/src/__tests__/model-client-factory.test.ts +0 -28
  63. package/src/__tests__/refine-resolve.test.ts +0 -72
  64. package/src/bin.ts +0 -18
  65. package/src/commands/init.ts +0 -207
  66. package/src/commands/refine.ts +0 -261
  67. package/src/detect-mode.ts +0 -94
  68. package/src/detect-source.ts +0 -123
  69. package/src/index.ts +0 -3
  70. package/src/model/anthropic.ts +0 -116
  71. package/src/model/cli/adapters.ts +0 -178
  72. package/src/model/cli/cli-client.ts +0 -52
  73. package/src/model/cli/run.ts +0 -83
  74. package/src/model/model-client-factory.ts +0 -62
  75. package/src/model/models.ts +0 -7
  76. package/tsconfig.build.json +0 -9
  77. package/tsconfig.json +0 -4
@@ -1,102 +0,0 @@
1
- // packages/client/src/__tests__/anthropic-prompt.test.ts
2
- import { describe, it, expect } from 'vitest';
3
- import { buildDraftPrompt, buildReviewPrompt } from '../model/anthropic.js';
4
- import type { DraftRequest, ReviewRequest, ExtractedSkill } from '@skillit/core';
5
-
6
- const baseSkill = (): ExtractedSkill =>
7
- ({ name: 'my-tool', functions: [] }) as unknown as ExtractedSkill;
8
-
9
- const baseDraftReq = (overrides: Partial<DraftRequest> = {}): DraftRequest => ({
10
- toolName: 'tool_a',
11
- tag: 'useWhen',
12
- suggestion: 'Add @useWhen annotation',
13
- currentValue: undefined,
14
- skill: baseSkill(),
15
- ...overrides
16
- });
17
-
18
- const baseReviewReq = (overrides: Partial<ReviewRequest> = {}): ReviewRequest => ({
19
- toolName: 'tool_a',
20
- tag: 'useWhen',
21
- draft: 'Use this tool when listing files',
22
- suggestion: 'Add @useWhen annotation',
23
- skill: baseSkill(),
24
- ...overrides
25
- });
26
-
27
- describe('buildDraftPrompt', () => {
28
- it('includes a Conventions section with guidance text when guidance is provided', () => {
29
- const req = baseDraftReq({ guidance: 'Always use active voice.' });
30
- const prompt = buildDraftPrompt(req);
31
- expect(prompt).toContain('Conventions');
32
- expect(prompt).toContain('Always use active voice.');
33
- });
34
-
35
- it('does NOT include a Conventions section when guidance is undefined', () => {
36
- const req = baseDraftReq({ guidance: undefined });
37
- const prompt = buildDraftPrompt(req);
38
- expect(prompt).not.toContain('Conventions');
39
- });
40
-
41
- it('does NOT include a Conventions section when guidance is absent', () => {
42
- const req = baseDraftReq();
43
- const prompt = buildDraftPrompt(req);
44
- expect(prompt).not.toContain('Conventions');
45
- });
46
-
47
- it('still includes core prompt content regardless of guidance', () => {
48
- const req = baseDraftReq({ guidance: 'Some guidance.' });
49
- const prompt = buildDraftPrompt(req);
50
- expect(prompt).toContain('tool_a');
51
- expect(prompt).toContain('@useWhen');
52
- });
53
-
54
- it('uses source-neutral framing (no hardcoded MCP) and keeps guidance', () => {
55
- const req = baseDraftReq({ guidance: 'CLI conventions: use --flag syntax.' });
56
- const prompt = buildDraftPrompt(req);
57
- expect(prompt).not.toContain('MCP');
58
- expect(prompt).toContain('skill annotations for "my-tool"');
59
- expect(prompt).toContain('Conventions');
60
- expect(prompt).toContain('CLI conventions: use --flag syntax.');
61
- });
62
- });
63
-
64
- describe('buildReviewPrompt', () => {
65
- it('includes a Conventions section with guidance text when guidance is provided', () => {
66
- const req = baseReviewReq({ guidance: 'Always use active voice.' });
67
- const prompt = buildReviewPrompt(req);
68
- expect(prompt).toContain('Conventions');
69
- expect(prompt).toContain('Always use active voice.');
70
- expect(prompt.indexOf('Conventions')).toBeLessThan(prompt.indexOf('Respond with JSON only'));
71
- });
72
-
73
- it('does NOT include a Conventions section when guidance is undefined', () => {
74
- const req = baseReviewReq({ guidance: undefined });
75
- const prompt = buildReviewPrompt(req);
76
- expect(prompt).not.toContain('Conventions');
77
- });
78
-
79
- it('does NOT include a Conventions section when guidance is absent', () => {
80
- const req = baseReviewReq();
81
- const prompt = buildReviewPrompt(req);
82
- expect(prompt).not.toContain('Conventions');
83
- });
84
-
85
- it('still includes core prompt content regardless of guidance', () => {
86
- const req = baseReviewReq({ guidance: 'Some guidance.' });
87
- const prompt = buildReviewPrompt(req);
88
- expect(prompt).toContain('tool_a');
89
- expect(prompt).toContain('@useWhen');
90
- expect(prompt).toContain('Use this tool when listing files');
91
- });
92
-
93
- it('uses source-neutral framing (no hardcoded MCP) and keeps guidance + JSON instruction', () => {
94
- const req = baseReviewReq({ guidance: 'CLI conventions: use --flag syntax.' });
95
- const prompt = buildReviewPrompt(req);
96
- expect(prompt).not.toContain('MCP');
97
- expect(prompt).toContain('skill annotation draft for "my-tool"');
98
- expect(prompt).toContain('Conventions');
99
- expect(prompt).toContain('CLI conventions: use --flag syntax.');
100
- expect(prompt).toContain('Respond with JSON only');
101
- });
102
- });
@@ -1,119 +0,0 @@
1
- // packages/client/src/__tests__/cli-adapters.test.ts
2
- import { describe, it, expect } from 'vitest';
3
- import { claudeAdapter, codexAdapter, copilotAdapter, adapterFor } from '../model/cli/adapters.js';
4
- import { DRAFTER, REVIEWER } from '../model/models.js';
5
-
6
- describe('claudeAdapter', () => {
7
- it('maps draft role to the drafter model and review role to the reviewer model', () => {
8
- const draft = claudeAdapter.invocation('draft', 'PROMPT');
9
- expect(draft.cmd).toBe('claude');
10
- expect(draft.args).toEqual(['-p', '--output-format', 'json', '--model', DRAFTER]);
11
- expect(draft.input).toBe('PROMPT');
12
- const review = claudeAdapter.invocation('review', 'PROMPT');
13
- expect(review.args).toContain(REVIEWER);
14
- });
15
-
16
- it('extracts result from the claude json envelope', () => {
17
- const stdout = JSON.stringify({ type: 'result', is_error: false, result: 'the answer' });
18
- expect(claudeAdapter.extractResult(stdout)).toBe('the answer');
19
- });
20
-
21
- it('throws when claude reports is_error', () => {
22
- const stdout = JSON.stringify({ type: 'result', is_error: true, result: 'nope' });
23
- expect(() => claudeAdapter.extractResult(stdout)).toThrow(/claude/i);
24
- });
25
-
26
- it('returns an empty result string (does not editorialize)', () => {
27
- const stdout = JSON.stringify({ type: 'result', is_error: false, result: '' });
28
- expect(claudeAdapter.extractResult(stdout)).toBe('');
29
- });
30
- });
31
-
32
- describe('codexAdapter', () => {
33
- it('invokes codex exec --json with the prompt on stdin and no per-role model', () => {
34
- const inv = codexAdapter.invocation('draft', 'PROMPT');
35
- expect(inv.cmd).toBe('codex');
36
- expect(inv.args).toEqual(['exec', '--json']);
37
- expect(inv.input).toBe('PROMPT');
38
- // role-agnostic: review uses the same invocation
39
- expect(codexAdapter.invocation('review', 'PROMPT').args).toEqual(['exec', '--json']);
40
- });
41
-
42
- it('extracts the last agent_message from the jsonl stream, skipping log/noise lines', () => {
43
- const stdout = [
44
- 'some non-json log line',
45
- JSON.stringify({ type: 'thread.started', thread_id: 'x' }),
46
- JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'first' } }),
47
- JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'final' } }),
48
- JSON.stringify({ type: 'turn.completed' })
49
- ].join('\n');
50
- expect(codexAdapter.extractResult(stdout)).toBe('final');
51
- });
52
-
53
- it('returns an empty final agent_message, overriding an earlier non-empty one (last-wins on empty)', () => {
54
- const stdout = [
55
- JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'first' } }),
56
- JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: '' } })
57
- ].join('\n');
58
- expect(codexAdapter.extractResult(stdout)).toBe('');
59
- });
60
-
61
- it('throws when no agent_message is present', () => {
62
- expect(() => codexAdapter.extractResult('{"type":"turn.completed"}')).toThrow(/codex/i);
63
- });
64
-
65
- it('surfaces a turn.failed signal in the no-message error', () => {
66
- const stdout = JSON.stringify({ type: 'turn.failed' });
67
- expect(() => codexAdapter.extractResult(stdout)).toThrow(/turn\.failed/);
68
- });
69
- });
70
-
71
- describe('copilotAdapter', () => {
72
- it('pipes the prompt via stdin (not argv) so untrusted content stays out of argv', () => {
73
- const inv = copilotAdapter.invocation('draft', 'PROMPT');
74
- expect(inv.cmd).toBe('copilot');
75
- expect(inv.args).toEqual(['--output-format', 'json', '--no-color']);
76
- expect(inv.args).not.toContain('PROMPT');
77
- expect(inv.input).toBe('PROMPT');
78
- });
79
-
80
- it('extracts the last assistant.message content from the jsonl stream, skipping deltas/result', () => {
81
- const stdout = [
82
- JSON.stringify({ type: 'assistant.message_delta', data: { deltaContent: 'the ' } }),
83
- JSON.stringify({ type: 'assistant.message_delta', data: { deltaContent: 'answer' } }),
84
- JSON.stringify({
85
- type: 'assistant.message',
86
- data: { content: 'the answer', toolRequests: [] }
87
- }),
88
- JSON.stringify({ type: 'result', exitCode: 0 })
89
- ].join('\n');
90
- expect(copilotAdapter.extractResult(stdout)).toBe('the answer');
91
- });
92
-
93
- it('returns an empty assistant.message content (does not editorialize)', () => {
94
- const stdout = [
95
- JSON.stringify({ type: 'assistant.message', data: { content: 'first', toolRequests: [] } }),
96
- JSON.stringify({ type: 'assistant.message', data: { content: '', toolRequests: [] } })
97
- ].join('\n');
98
- expect(copilotAdapter.extractResult(stdout)).toBe('');
99
- });
100
-
101
- it('throws when no assistant.message is present', () => {
102
- expect(() => copilotAdapter.extractResult('{"type":"result","exitCode":0}')).toThrow(
103
- /copilot/i
104
- );
105
- });
106
-
107
- it('surfaces a nonzero result exitCode in the no-message error', () => {
108
- const stdout = JSON.stringify({ type: 'result', exitCode: 1 });
109
- expect(() => copilotAdapter.extractResult(stdout)).toThrow(/exitCode 1/);
110
- });
111
- });
112
-
113
- describe('adapterFor', () => {
114
- it('returns the matching adapter', () => {
115
- expect(adapterFor('claude')).toBe(claudeAdapter);
116
- expect(adapterFor('codex')).toBe(codexAdapter);
117
- expect(adapterFor('copilot')).toBe(copilotAdapter);
118
- });
119
- });
@@ -1,58 +0,0 @@
1
- // packages/client/src/__tests__/cli-client.test.ts
2
- import { describe, it, expect, vi } from 'vitest';
3
- import { CliModelClient } from '../model/cli/cli-client.js';
4
- import { claudeAdapter } from '../model/cli/adapters.js';
5
- import type { DraftRequest, ReviewRequest, ExtractedSkill } from '@skillit/core';
6
-
7
- const skill = { name: 'demo' } as unknown as ExtractedSkill;
8
- const draftReq: DraftRequest = {
9
- toolName: 'gen',
10
- tag: 'useWhen',
11
- suggestion: 'say when',
12
- currentValue: undefined,
13
- skill
14
- };
15
- const reviewReq: ReviewRequest = {
16
- toolName: 'gen',
17
- tag: 'useWhen',
18
- draft: 'When generating',
19
- suggestion: 'say when',
20
- skill
21
- };
22
-
23
- describe('CliModelClient', () => {
24
- it('draft() returns the trimmed result extracted from the adapter envelope', async () => {
25
- const runner = vi.fn(async () =>
26
- JSON.stringify({ type: 'result', is_error: false, result: ' When generating output ' })
27
- );
28
- const client = new CliModelClient(claudeAdapter, { runner });
29
- const out = await client.draft(draftReq);
30
- expect(out).toBe('When generating output');
31
- // the adapter's invocation was forwarded to the runner
32
- expect(runner).toHaveBeenCalledWith(
33
- expect.objectContaining({ cmd: 'claude', args: expect.arrayContaining(['-p']) })
34
- );
35
- });
36
-
37
- it('review() parses the verdict JSON out of the adapter result', async () => {
38
- const runner = vi.fn(async () =>
39
- JSON.stringify({
40
- type: 'result',
41
- is_error: false,
42
- result: 'Sure: {"verdict":"revise","feedback":"too vague"}'
43
- })
44
- );
45
- const client = new CliModelClient(claudeAdapter, { runner });
46
- const res = await client.review(reviewReq);
47
- expect(res).toEqual({ verdict: 'revise', feedback: 'too vague' });
48
- });
49
-
50
- it('passes the configured timeout through to the runner', async () => {
51
- const runner = vi.fn(async () =>
52
- JSON.stringify({ type: 'result', is_error: false, result: 'x' })
53
- );
54
- const client = new CliModelClient(claudeAdapter, { runner, timeoutMs: 5000 });
55
- await client.draft(draftReq);
56
- expect(runner).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 5000 }));
57
- });
58
- });
@@ -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 };