@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.
- package/README.md +40 -0
- package/dist/bin.d.ts +2 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +21 -13
- package/dist/bin.js.map +1 -1
- package/dist/commands/audit.d.ts +34 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +139 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/gen.d.ts +20 -0
- package/dist/commands/gen.d.ts.map +1 -0
- package/dist/commands/gen.js +108 -0
- package/dist/commands/gen.js.map +1 -0
- package/dist/commands/init.d.ts +3 -22
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +23 -84
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/refine.d.ts +34 -2
- package/dist/commands/refine.d.ts.map +1 -1
- package/dist/commands/refine.js +61 -29
- package/dist/commands/refine.js.map +1 -1
- package/dist/generate.d.ts +67 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +55 -0
- package/dist/generate.js.map +1 -0
- package/dist/mcp-mode.d.ts +22 -0
- package/dist/mcp-mode.d.ts.map +1 -0
- package/dist/mcp-mode.js +27 -0
- package/dist/mcp-mode.js.map +1 -0
- package/dist/model/anthropic.d.ts +11 -0
- package/dist/model/anthropic.d.ts.map +1 -1
- package/dist/model/anthropic.js +16 -2
- package/dist/model/anthropic.js.map +1 -1
- package/dist/model/cli/adapters.d.ts.map +1 -1
- package/dist/model/cli/adapters.js +35 -2
- package/dist/model/cli/adapters.js.map +1 -1
- package/dist/model/cli/cli-client.d.ts.map +1 -1
- package/dist/model/cli/cli-client.js +4 -2
- package/dist/model/cli/cli-client.js.map +1 -1
- package/dist/program.d.ts +12 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +42 -0
- package/dist/program.js.map +1 -0
- package/dist/typedoc-entry.d.ts +6 -0
- package/dist/typedoc-entry.d.ts.map +1 -0
- package/dist/typedoc-entry.js +14 -0
- package/dist/typedoc-entry.js.map +1 -0
- package/package.json +18 -7
- package/skills/skillit-bootstrap/SKILL.md +115 -0
- package/skills/skillit-bootstrap/references/surface-routing.md +97 -0
- package/CHANGELOG.md +0 -32
- package/scripts/gen-refine-cli-skill.mjs +0 -48
- package/src/__tests__/anthropic-model.test.ts +0 -45
- package/src/__tests__/anthropic-prompt.test.ts +0 -102
- package/src/__tests__/cli-adapters.test.ts +0 -119
- package/src/__tests__/cli-client.test.ts +0 -58
- package/src/__tests__/cli-run.test.ts +0 -52
- package/src/__tests__/detect-mode.test.ts +0 -114
- package/src/__tests__/detect-source.test.ts +0 -158
- package/src/__tests__/fixtures/bin-with-program.mjs +0 -6
- package/src/__tests__/init.test.ts +0 -218
- package/src/__tests__/model-client-factory.test.ts +0 -28
- package/src/__tests__/refine-resolve.test.ts +0 -72
- package/src/bin.ts +0 -18
- package/src/commands/init.ts +0 -207
- package/src/commands/refine.ts +0 -261
- package/src/detect-mode.ts +0 -94
- package/src/detect-source.ts +0 -123
- package/src/index.ts +0 -3
- package/src/model/anthropic.ts +0 -116
- package/src/model/cli/adapters.ts +0 -178
- package/src/model/cli/cli-client.ts +0 -52
- package/src/model/cli/run.ts +0 -83
- package/src/model/model-client-factory.ts +0 -62
- package/src/model/models.ts +0 -7
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -4
|
@@ -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
|
-
});
|
package/src/commands/init.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
// packages/client/src/commands/init.ts
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { Command } from 'commander';
|
|
6
|
-
import { extractCliSkill, loadProgram, writeCliSkill } from '@skillit/cli';
|
|
7
|
-
import {
|
|
8
|
-
detectPackageManager,
|
|
9
|
-
detectProjectNature,
|
|
10
|
-
type RefineSourceKind
|
|
11
|
-
} from '../detect-source.js';
|
|
12
|
-
import { runRefineCommand, type RefineCommandOpts } from './refine.js';
|
|
13
|
-
|
|
14
|
-
type PackageManager = 'pnpm' | 'yarn' | 'npm';
|
|
15
|
-
|
|
16
|
-
/** Options passed to the (CLI-path) skill generator. */
|
|
17
|
-
export interface GenerateSkillOpts {
|
|
18
|
-
/** Project root being initialized. */
|
|
19
|
-
cwd: string;
|
|
20
|
-
/** Resolved project nature. */
|
|
21
|
-
nature: RefineSourceKind;
|
|
22
|
-
/** Skill name (consumer package name, scope stripped). */
|
|
23
|
-
name: string;
|
|
24
|
-
/** Absolute output directory (`<cwd>/<out>`). */
|
|
25
|
-
outDir: string;
|
|
26
|
-
/** `--program <file#export>` entry, if provided. */
|
|
27
|
-
program?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Injectable side-effecting steps for {@link buildInitCommand}. All optional;
|
|
32
|
-
* real defaults run install / generate / refine. Tests inject stubs so no real
|
|
33
|
-
* install, network, or model call runs.
|
|
34
|
-
*/
|
|
35
|
-
export interface InitDeps {
|
|
36
|
-
/** Install `pkg` as a dev dependency in `cwd` using package manager `pm`. */
|
|
37
|
-
runInstall?(pkg: string, pm: PackageManager, cwd: string): Promise<void>;
|
|
38
|
-
/** Generate the initial skill (CLI path only). */
|
|
39
|
-
generateSkill?(opts: GenerateSkillOpts): Promise<void>;
|
|
40
|
-
/** Dispatch the refine loop for the resolved source. */
|
|
41
|
-
runRefine?(opts: RefineCommandOpts): Promise<void>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface InitOpts {
|
|
45
|
-
source?: string;
|
|
46
|
-
program?: string;
|
|
47
|
-
out: string;
|
|
48
|
-
modelClient?: string;
|
|
49
|
-
modelCliTimeout?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const VALID_SOURCES: readonly RefineSourceKind[] = ['cli', 'mcp', 'typedoc'];
|
|
53
|
-
|
|
54
|
-
/** Map a project nature to the `@skillit/*` package that handles it. */
|
|
55
|
-
function natureToPackage(nature: RefineSourceKind): string {
|
|
56
|
-
if (nature === 'cli') return '@skillit/cli';
|
|
57
|
-
if (nature === 'mcp') return '@skillit/mcp';
|
|
58
|
-
return 'typedoc-plugin-skillit';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Build the package manager's add-dev command line (for messaging + spawn). */
|
|
62
|
-
function addDevCommand(pm: PackageManager, pkg: string): string {
|
|
63
|
-
if (pm === 'pnpm') return `pnpm add -D ${pkg}`;
|
|
64
|
-
if (pm === 'yarn') return `yarn add -D ${pkg}`;
|
|
65
|
-
return `npm install -D ${pkg}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Strip a leading `@scope/` from a package name for use as a skill dir name. */
|
|
69
|
-
function skillNameFrom(packageName: string): string {
|
|
70
|
-
const slash = packageName.indexOf('/');
|
|
71
|
-
if (packageName.startsWith('@') && slash !== -1) {
|
|
72
|
-
return packageName.slice(slash + 1);
|
|
73
|
-
}
|
|
74
|
-
return packageName;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function readPackageName(cwd: string): Promise<string> {
|
|
78
|
-
try {
|
|
79
|
-
const raw = await readFile(join(cwd, 'package.json'), 'utf8');
|
|
80
|
-
const pkg = JSON.parse(raw) as { name?: string };
|
|
81
|
-
return pkg.name ?? 'skill';
|
|
82
|
-
} catch {
|
|
83
|
-
return 'skill';
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Default install: spawn the package manager's add-dev command, cwd-scoped. */
|
|
88
|
-
function defaultRunInstall(pkg: string, pm: PackageManager, cwd: string): Promise<void> {
|
|
89
|
-
const command = addDevCommand(pm, pkg);
|
|
90
|
-
const [bin, ...args] = command.split(' ');
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
const child = spawn(bin!, args, { cwd, stdio: 'inherit' });
|
|
93
|
-
child.on('error', reject);
|
|
94
|
-
child.on('close', (code) => {
|
|
95
|
-
if (code === 0) resolve();
|
|
96
|
-
else reject(new Error(`exited with code ${code}`));
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Default CLI-path skill generation: loadProgram → extractCliSkill → writeCliSkill. */
|
|
102
|
-
async function defaultGenerateSkill(opts: GenerateSkillOpts): Promise<void> {
|
|
103
|
-
const program = await loadProgram({ program: opts.program, cwd: opts.cwd });
|
|
104
|
-
const skill = await extractCliSkill({ program, metadata: { name: opts.name } });
|
|
105
|
-
writeCliSkill(skill, { outDir: opts.outDir });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Default refine dispatch: reuse the shared refine command body. */
|
|
109
|
-
function defaultRunRefine(opts: RefineCommandOpts): Promise<void> {
|
|
110
|
-
return runRefineCommand(opts);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function buildInitCommand(deps: InitDeps = {}): Command {
|
|
114
|
-
const runInstall = deps.runInstall ?? defaultRunInstall;
|
|
115
|
-
const generateSkill = deps.generateSkill ?? defaultGenerateSkill;
|
|
116
|
-
const runRefine = deps.runRefine ?? defaultRunRefine;
|
|
117
|
-
|
|
118
|
-
return new Command('init')
|
|
119
|
-
.description(
|
|
120
|
-
'Detect the project, install the right @skillit package, generate + refine a skill'
|
|
121
|
-
)
|
|
122
|
-
.option('--source <kind>', 'cli | mcp | typedoc (auto-detected if omitted)')
|
|
123
|
-
.option('--program <file#export>', 'commander program entry (cli source)')
|
|
124
|
-
.option('--out <dir>', 'output directory for the generated skill', 'skills')
|
|
125
|
-
.option(
|
|
126
|
-
'--model-client <kind>',
|
|
127
|
-
'model backend for refine: api | claude | codex | copilot',
|
|
128
|
-
'api'
|
|
129
|
-
)
|
|
130
|
-
.option('--model-cli-timeout <ms>', 'per-call timeout for cli model backends (ms)')
|
|
131
|
-
.action(async (opts: InitOpts) => {
|
|
132
|
-
const cwd = process.cwd();
|
|
133
|
-
|
|
134
|
-
// 1. Resolve nature: explicit --source wins, else detect.
|
|
135
|
-
let nature: RefineSourceKind;
|
|
136
|
-
if (opts.source !== undefined) {
|
|
137
|
-
if (!VALID_SOURCES.includes(opts.source as RefineSourceKind)) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`Invalid --source value: ${opts.source}. Use --source <cli|mcp|typedoc>.`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
nature = opts.source as RefineSourceKind;
|
|
143
|
-
} else {
|
|
144
|
-
nature = await detectProjectNature(cwd);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 2 + 3. Map nature → package, install via the detected package manager.
|
|
148
|
-
const pkg = natureToPackage(nature);
|
|
149
|
-
const pm = detectPackageManager(cwd);
|
|
150
|
-
const command = addDevCommand(pm, pkg);
|
|
151
|
-
try {
|
|
152
|
-
await runInstall(pkg, pm, cwd);
|
|
153
|
-
} catch (error) {
|
|
154
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
155
|
-
// Surface the exact command and STOP — do not generate or refine.
|
|
156
|
-
throw new Error(`Install failed (${reason}). Run it manually:\n ${command}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 4 + 5. Generate + refine — CLI-first: only the cli source is fully
|
|
160
|
-
// automated this pass. For mcp/typedoc, skip both and print actionable
|
|
161
|
-
// next-step guidance rather than silently no-opping (refine needs
|
|
162
|
-
// source-specific flags like --mcp that init can't supply yet).
|
|
163
|
-
if (nature === 'cli') {
|
|
164
|
-
const outDir = join(cwd, opts.out);
|
|
165
|
-
const name = skillNameFrom(await readPackageName(cwd));
|
|
166
|
-
const generateOpts: GenerateSkillOpts = {
|
|
167
|
-
cwd,
|
|
168
|
-
nature,
|
|
169
|
-
name,
|
|
170
|
-
outDir,
|
|
171
|
-
...(opts.program !== undefined ? { program: opts.program } : {})
|
|
172
|
-
};
|
|
173
|
-
try {
|
|
174
|
-
await generateSkill(generateOpts);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
// The cli source only auto-loads a commander program. A yargs/other
|
|
177
|
-
// CLI (still classified as 'cli') can't be generated yet — degrade
|
|
178
|
-
// gracefully: surface the reason and how to proceed, do NOT crash,
|
|
179
|
-
// and do NOT run refine/regenerate on a failed generate.
|
|
180
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
181
|
-
console.log(
|
|
182
|
-
`Installed ${pkg}, but couldn't auto-load a commander program (${reason}). ` +
|
|
183
|
-
`If this is a commander CLI, run: skillit refine --source cli --program <file#export>. ` +
|
|
184
|
-
`(yargs/other CLIs aren't auto-generated yet.)`
|
|
185
|
-
);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
await runRefine({
|
|
189
|
-
source: nature,
|
|
190
|
-
...(opts.program !== undefined ? { program: opts.program } : {}),
|
|
191
|
-
...(opts.modelClient !== undefined ? { modelClient: opts.modelClient } : {}),
|
|
192
|
-
...(opts.modelCliTimeout !== undefined ? { modelCliTimeout: opts.modelCliTimeout } : {}),
|
|
193
|
-
maxIterations: '5',
|
|
194
|
-
items: '5'
|
|
195
|
-
});
|
|
196
|
-
// refine only writes JSDoc back into source files; regenerate so the
|
|
197
|
-
// on-disk skill reflects the freshly-written annotations. Runs only
|
|
198
|
-
// after a successful refine (a refine throw propagates above).
|
|
199
|
-
await generateSkill(generateOpts);
|
|
200
|
-
} else {
|
|
201
|
-
const extra = nature === 'mcp' ? ' [--mcp <path>]' : '';
|
|
202
|
-
console.log(
|
|
203
|
-
`Installed ${pkg}. Skill generation + refine for the ${nature} source isn't automated yet — run: skillit refine --source ${nature}${extra}`
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|