@soleri/cli 9.7.2 → 9.8.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.
- package/dist/commands/add-domain.js +1 -0
- package/dist/commands/add-domain.js.map +1 -1
- package/dist/commands/add-pack.js +7 -147
- package/dist/commands/add-pack.js.map +1 -1
- package/dist/commands/agent.js +130 -0
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/create.js +78 -2
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/extend.js +17 -0
- package/dist/commands/extend.js.map +1 -1
- package/dist/commands/install-knowledge.js +1 -0
- package/dist/commands/install-knowledge.js.map +1 -1
- package/dist/commands/test.js +140 -1
- package/dist/commands/test.js.map +1 -1
- package/dist/hook-packs/flock-guard/manifest.json +2 -1
- package/dist/hook-packs/marketing-research/manifest.json +2 -1
- package/dist/hook-packs/registry.d.ts +2 -0
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +2 -0
- package/dist/prompts/create-wizard.d.ts +16 -2
- package/dist/prompts/create-wizard.js +84 -11
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/utils/checks.d.ts +8 -5
- package/dist/utils/checks.js +105 -10
- package/dist/utils/checks.js.map +1 -1
- package/dist/utils/format-paths.d.ts +14 -0
- package/dist/utils/format-paths.js +27 -0
- package/dist/utils/format-paths.js.map +1 -0
- package/dist/utils/git.d.ts +29 -0
- package/dist/utils/git.js +88 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.js +4 -0
- package/dist/utils/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/create-wizard-git.test.ts +208 -0
- package/src/__tests__/git-utils.test.ts +268 -0
- package/src/__tests__/scaffold-git-e2e.test.ts +105 -0
- package/src/commands/add-domain.ts +1 -0
- package/src/commands/add-pack.ts +10 -163
- package/src/commands/agent.ts +161 -0
- package/src/commands/create.ts +89 -3
- package/src/commands/doctor.ts +1 -0
- package/src/commands/extend.ts +20 -1
- package/src/commands/install-knowledge.ts +1 -0
- package/src/commands/test.ts +141 -2
- package/src/hook-packs/flock-guard/manifest.json +2 -1
- package/src/hook-packs/marketing-research/manifest.json +2 -1
- package/src/hook-packs/registry.ts +2 -0
- package/src/prompts/create-wizard.ts +109 -14
- package/src/utils/checks.ts +122 -13
- package/src/utils/format-paths.ts +41 -0
- package/src/utils/git.ts +118 -0
- package/src/utils/logger.ts +5 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock node:child_process before importing the module under test
|
|
4
|
+
vi.mock('node:child_process', () => ({
|
|
5
|
+
execFile: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import {
|
|
10
|
+
isGitInstalled,
|
|
11
|
+
isGhInstalled,
|
|
12
|
+
gitInit,
|
|
13
|
+
gitInitialCommit,
|
|
14
|
+
gitAddRemote,
|
|
15
|
+
gitPush,
|
|
16
|
+
ghCreateRepo,
|
|
17
|
+
} from '../utils/git.js';
|
|
18
|
+
|
|
19
|
+
const mockExecFile = vi.mocked(execFile);
|
|
20
|
+
|
|
21
|
+
/** Helper: make execFile call its callback with success (stdout). */
|
|
22
|
+
function mockSuccess(stdout = '') {
|
|
23
|
+
mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
|
|
24
|
+
(callback as Function)(null, stdout, '');
|
|
25
|
+
return undefined as any;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Helper: make execFile call its callback with an error. */
|
|
30
|
+
function mockFailure(message: string, stderr = '') {
|
|
31
|
+
mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
|
|
32
|
+
const err = new Error(message);
|
|
33
|
+
(callback as Function)(err, '', stderr);
|
|
34
|
+
return undefined as any;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Helper: make execFile succeed N times then fail.
|
|
40
|
+
* Useful for testing gitInitialCommit where `git add` must succeed before `git commit` fails.
|
|
41
|
+
*/
|
|
42
|
+
function mockSequence(calls: Array<{ stdout?: string; error?: string; stderr?: string }>) {
|
|
43
|
+
let callIndex = 0;
|
|
44
|
+
mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
|
|
45
|
+
const spec = calls[callIndex] ?? calls[calls.length - 1];
|
|
46
|
+
callIndex++;
|
|
47
|
+
if (spec.error) {
|
|
48
|
+
const err = new Error(spec.error);
|
|
49
|
+
(callback as Function)(err, '', spec.stderr ?? spec.error);
|
|
50
|
+
} else {
|
|
51
|
+
(callback as Function)(null, spec.stdout ?? '', '');
|
|
52
|
+
}
|
|
53
|
+
return undefined as any;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('git utilities', () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
vi.resetAllMocks();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── isGitInstalled ──────────────────────────────────────────────
|
|
63
|
+
describe('isGitInstalled', () => {
|
|
64
|
+
it('returns true when git binary is found', async () => {
|
|
65
|
+
mockSuccess('/usr/bin/git');
|
|
66
|
+
const result = await isGitInstalled();
|
|
67
|
+
expect(result).toBe(true);
|
|
68
|
+
expect(mockExecFile).toHaveBeenCalledWith(
|
|
69
|
+
'which',
|
|
70
|
+
['git'],
|
|
71
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
72
|
+
expect.any(Function),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns false when git binary is not found', async () => {
|
|
77
|
+
mockFailure('not found');
|
|
78
|
+
const result = await isGitInstalled();
|
|
79
|
+
expect(result).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ── isGhInstalled ──────────────────────────────────────────────
|
|
84
|
+
describe('isGhInstalled', () => {
|
|
85
|
+
it('returns true when gh binary is found', async () => {
|
|
86
|
+
mockSuccess('/usr/bin/gh');
|
|
87
|
+
const result = await isGhInstalled();
|
|
88
|
+
expect(result).toBe(true);
|
|
89
|
+
expect(mockExecFile).toHaveBeenCalledWith(
|
|
90
|
+
'which',
|
|
91
|
+
['gh'],
|
|
92
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
93
|
+
expect.any(Function),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns false when gh binary is not found', async () => {
|
|
98
|
+
mockFailure('not found');
|
|
99
|
+
const result = await isGhInstalled();
|
|
100
|
+
expect(result).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── gitInit ─────────────────────────────────────────────────────
|
|
105
|
+
describe('gitInit', () => {
|
|
106
|
+
it('returns { ok: true } on success', async () => {
|
|
107
|
+
mockSuccess('Initialized empty Git repository');
|
|
108
|
+
const result = await gitInit('/tmp/my-project');
|
|
109
|
+
expect(result).toEqual({ ok: true });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns { ok: false, error } on failure', async () => {
|
|
113
|
+
mockFailure('fatal: not a git repository', 'fatal: not a git repository');
|
|
114
|
+
const result = await gitInit('/tmp/my-project');
|
|
115
|
+
expect(result.ok).toBe(false);
|
|
116
|
+
expect(result.error).toBeDefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('passes correct args with cwd', async () => {
|
|
120
|
+
mockSuccess();
|
|
121
|
+
await gitInit('/tmp/my-project');
|
|
122
|
+
expect(mockExecFile).toHaveBeenCalledWith(
|
|
123
|
+
'git',
|
|
124
|
+
['init'],
|
|
125
|
+
expect.objectContaining({ cwd: '/tmp/my-project' }),
|
|
126
|
+
expect.any(Function),
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── gitInitialCommit ────────────────────────────────────────────
|
|
132
|
+
describe('gitInitialCommit', () => {
|
|
133
|
+
it('returns { ok: true } when both add and commit succeed', async () => {
|
|
134
|
+
mockSequence([{ stdout: '' }, { stdout: '' }]);
|
|
135
|
+
const result = await gitInitialCommit('/tmp/proj', 'Initial commit');
|
|
136
|
+
expect(result).toEqual({ ok: true });
|
|
137
|
+
// Two calls: git add . and git commit -m ...
|
|
138
|
+
expect(mockExecFile).toHaveBeenCalledTimes(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns { ok: false } when git add fails (does not attempt commit)', async () => {
|
|
142
|
+
mockSequence([{ error: 'add failed', stderr: 'add failed' }]);
|
|
143
|
+
const result = await gitInitialCommit('/tmp/proj', 'Initial commit');
|
|
144
|
+
expect(result.ok).toBe(false);
|
|
145
|
+
expect(result.error).toContain('add failed');
|
|
146
|
+
// Only one call — git add; commit should not be attempted
|
|
147
|
+
expect(mockExecFile).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('returns { ok: false } when add succeeds but commit fails', async () => {
|
|
151
|
+
mockSequence([{ stdout: '' }, { error: 'nothing to commit', stderr: 'nothing to commit' }]);
|
|
152
|
+
const result = await gitInitialCommit('/tmp/proj', 'Initial commit');
|
|
153
|
+
expect(result.ok).toBe(false);
|
|
154
|
+
expect(result.error).toContain('nothing to commit');
|
|
155
|
+
expect(mockExecFile).toHaveBeenCalledTimes(2);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('passes the correct commit message', async () => {
|
|
159
|
+
mockSequence([{ stdout: '' }, { stdout: '' }]);
|
|
160
|
+
await gitInitialCommit('/tmp/proj', 'feat: initial scaffold');
|
|
161
|
+
// Second call should be git commit -m <message>
|
|
162
|
+
const commitCall = mockExecFile.mock.calls[1];
|
|
163
|
+
expect(commitCall[0]).toBe('git');
|
|
164
|
+
expect(commitCall[1]).toEqual(['commit', '-m', 'feat: initial scaffold']);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── gitAddRemote ────────────────────────────────────────────────
|
|
169
|
+
describe('gitAddRemote', () => {
|
|
170
|
+
it('returns { ok: true } on success', async () => {
|
|
171
|
+
mockSuccess();
|
|
172
|
+
const result = await gitAddRemote('/tmp/proj', 'https://github.com/user/repo.git');
|
|
173
|
+
expect(result).toEqual({ ok: true });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('passes correct args', async () => {
|
|
177
|
+
mockSuccess();
|
|
178
|
+
await gitAddRemote('/tmp/proj', 'https://github.com/user/repo.git');
|
|
179
|
+
expect(mockExecFile).toHaveBeenCalledWith(
|
|
180
|
+
'git',
|
|
181
|
+
['remote', 'add', 'origin', 'https://github.com/user/repo.git'],
|
|
182
|
+
expect.objectContaining({ cwd: '/tmp/proj' }),
|
|
183
|
+
expect.any(Function),
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns { ok: false } on failure', async () => {
|
|
188
|
+
mockFailure('remote origin already exists', 'remote origin already exists');
|
|
189
|
+
const result = await gitAddRemote('/tmp/proj', 'https://github.com/user/repo.git');
|
|
190
|
+
expect(result.ok).toBe(false);
|
|
191
|
+
expect(result.error).toBeDefined();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── gitPush ─────────────────────────────────────────────────────
|
|
196
|
+
describe('gitPush', () => {
|
|
197
|
+
it('returns { ok: true } on success', async () => {
|
|
198
|
+
mockSuccess();
|
|
199
|
+
const result = await gitPush('/tmp/proj');
|
|
200
|
+
expect(result).toEqual({ ok: true });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('returns { ok: false } on network error', async () => {
|
|
204
|
+
mockFailure('Could not resolve host', 'Could not resolve host');
|
|
205
|
+
const result = await gitPush('/tmp/proj');
|
|
206
|
+
expect(result.ok).toBe(false);
|
|
207
|
+
expect(result.error).toContain('Could not resolve host');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('uses NETWORK_TIMEOUT (60s) via AbortSignal', async () => {
|
|
211
|
+
mockSuccess();
|
|
212
|
+
await gitPush('/tmp/proj');
|
|
213
|
+
const callOpts = mockExecFile.mock.calls[0][2] as { signal: AbortSignal };
|
|
214
|
+
// AbortSignal.timeout(60000) — verify it is an AbortSignal (we can't read the timeout
|
|
215
|
+
// value directly, but we can verify it's present and is an AbortSignal)
|
|
216
|
+
expect(callOpts.signal).toBeInstanceOf(AbortSignal);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ── ghCreateRepo ────────────────────────────────────────────────
|
|
221
|
+
describe('ghCreateRepo', () => {
|
|
222
|
+
it('returns { ok: true, url } on success', async () => {
|
|
223
|
+
mockSuccess('https://github.com/user/my-repo\n');
|
|
224
|
+
const result = await ghCreateRepo('my-repo', { visibility: 'public', dir: '/tmp/proj' });
|
|
225
|
+
expect(result.ok).toBe(true);
|
|
226
|
+
expect(result.url).toBe('https://github.com/user/my-repo');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('returns { ok: false } on failure', async () => {
|
|
230
|
+
mockFailure('authentication required', 'authentication required');
|
|
231
|
+
const result = await ghCreateRepo('my-repo', { visibility: 'public', dir: '/tmp/proj' });
|
|
232
|
+
expect(result.ok).toBe(false);
|
|
233
|
+
expect(result.error).toBeDefined();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('passes --public flag for public visibility', async () => {
|
|
237
|
+
mockSuccess('https://github.com/user/my-repo');
|
|
238
|
+
await ghCreateRepo('my-repo', { visibility: 'public', dir: '/tmp/proj' });
|
|
239
|
+
const args = mockExecFile.mock.calls[0][1] as string[];
|
|
240
|
+
expect(args).toContain('--public');
|
|
241
|
+
expect(args).not.toContain('--private');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('passes --private flag for private visibility', async () => {
|
|
245
|
+
mockSuccess('https://github.com/user/my-repo');
|
|
246
|
+
await ghCreateRepo('my-repo', { visibility: 'private', dir: '/tmp/proj' });
|
|
247
|
+
const args = mockExecFile.mock.calls[0][1] as string[];
|
|
248
|
+
expect(args).toContain('--private');
|
|
249
|
+
expect(args).not.toContain('--public');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('includes --source, --remote, and --push flags', async () => {
|
|
253
|
+
mockSuccess('https://github.com/user/my-repo');
|
|
254
|
+
await ghCreateRepo('my-repo', { visibility: 'public', dir: '/tmp/proj' });
|
|
255
|
+
const args = mockExecFile.mock.calls[0][1] as string[];
|
|
256
|
+
expect(args).toContain('--source=/tmp/proj');
|
|
257
|
+
expect(args).toContain('--remote=origin');
|
|
258
|
+
expect(args).toContain('--push');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('returns undefined url when stdout is empty', async () => {
|
|
262
|
+
mockSuccess('');
|
|
263
|
+
const result = await ghCreateRepo('my-repo', { visibility: 'public', dir: '/tmp/proj' });
|
|
264
|
+
expect(result.ok).toBe(true);
|
|
265
|
+
expect(result.url).toBeUndefined();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { scaffoldFileTree } from '@soleri/forge/lib';
|
|
7
|
+
import { gitInit, gitInitialCommit } from '../utils/git.js';
|
|
8
|
+
import type { AgentYamlInput } from '@soleri/forge/lib';
|
|
9
|
+
|
|
10
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const MINIMAL_AGENT: AgentYamlInput = {
|
|
13
|
+
id: 'test-agent',
|
|
14
|
+
name: 'Test Agent',
|
|
15
|
+
role: 'Testing assistant',
|
|
16
|
+
description: 'A minimal agent used for scaffold + git E2E tests',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function gitCommand(dir: string, ...args: string[]): string {
|
|
20
|
+
return execFileSync('git', args, {
|
|
21
|
+
cwd: dir,
|
|
22
|
+
stdio: 'pipe',
|
|
23
|
+
encoding: 'utf-8',
|
|
24
|
+
}).trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Tests ───────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe('scaffold + git init (E2E)', () => {
|
|
30
|
+
const tempDirs: string[] = [];
|
|
31
|
+
|
|
32
|
+
function makeTempDir(): string {
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), 'soleri-git-e2e-'));
|
|
34
|
+
tempDirs.push(dir);
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
for (const dir of tempDirs) {
|
|
40
|
+
rmSync(dir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
tempDirs.length = 0;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('scaffold with git init produces a valid git repo', async () => {
|
|
46
|
+
const outputDir = makeTempDir();
|
|
47
|
+
const result = scaffoldFileTree(MINIMAL_AGENT, outputDir);
|
|
48
|
+
expect(result.success).toBe(true);
|
|
49
|
+
|
|
50
|
+
const agentDir = result.agentDir;
|
|
51
|
+
|
|
52
|
+
// Initialize git and create initial commit
|
|
53
|
+
const initResult = await gitInit(agentDir);
|
|
54
|
+
expect(initResult.ok).toBe(true);
|
|
55
|
+
|
|
56
|
+
const commitResult = await gitInitialCommit(agentDir, 'feat: scaffold agent "test-agent"');
|
|
57
|
+
expect(commitResult.ok).toBe(true);
|
|
58
|
+
|
|
59
|
+
// .git directory exists
|
|
60
|
+
expect(existsSync(join(agentDir, '.git'))).toBe(true);
|
|
61
|
+
|
|
62
|
+
// Exactly 1 commit
|
|
63
|
+
const log = gitCommand(agentDir, 'log', '--oneline');
|
|
64
|
+
const commits = log.split('\n').filter(Boolean);
|
|
65
|
+
expect(commits).toHaveLength(1);
|
|
66
|
+
|
|
67
|
+
// Commit message contains expected text
|
|
68
|
+
expect(commits[0]).toContain('feat: scaffold agent');
|
|
69
|
+
|
|
70
|
+
// Working tree is clean — no untracked files
|
|
71
|
+
const status = gitCommand(agentDir, 'status', '--porcelain');
|
|
72
|
+
expect(status).toBe('');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('.gitignore exclusions are respected', async () => {
|
|
76
|
+
const outputDir = makeTempDir();
|
|
77
|
+
const result = scaffoldFileTree(MINIMAL_AGENT, outputDir);
|
|
78
|
+
expect(result.success).toBe(true);
|
|
79
|
+
|
|
80
|
+
const agentDir = result.agentDir;
|
|
81
|
+
|
|
82
|
+
await gitInit(agentDir);
|
|
83
|
+
await gitInitialCommit(agentDir, 'feat: scaffold agent "test-agent"');
|
|
84
|
+
|
|
85
|
+
const trackedFiles = gitCommand(agentDir, 'ls-files');
|
|
86
|
+
|
|
87
|
+
// Auto-generated files must NOT be tracked
|
|
88
|
+
expect(trackedFiles).not.toContain('CLAUDE.md');
|
|
89
|
+
expect(trackedFiles).not.toContain('AGENTS.md');
|
|
90
|
+
expect(trackedFiles).not.toContain('instructions/_engine.md');
|
|
91
|
+
|
|
92
|
+
// Important source-of-truth files MUST be tracked
|
|
93
|
+
expect(trackedFiles).toContain('agent.yaml');
|
|
94
|
+
expect(trackedFiles).toContain('.gitignore');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('scaffold without git does not create a .git directory', () => {
|
|
98
|
+
const outputDir = makeTempDir();
|
|
99
|
+
const result = scaffoldFileTree(MINIMAL_AGENT, outputDir);
|
|
100
|
+
expect(result.success).toBe(true);
|
|
101
|
+
|
|
102
|
+
// No git init called — .git must not exist
|
|
103
|
+
expect(existsSync(join(result.agentDir, '.git'))).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
package/src/commands/add-pack.ts
CHANGED
|
@@ -1,170 +1,17 @@
|
|
|
1
|
-
// @ts-nocheck — TODO: rewrite for v7 file-tree agents (uses removed @soleri/core APIs)
|
|
2
1
|
import type { Command } from 'commander';
|
|
3
2
|
import * as p from '@clack/prompts';
|
|
4
|
-
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { detectAgent } from '../utils/agent-context.js';
|
|
8
3
|
|
|
9
4
|
export function registerAddPack(program: Command): void {
|
|
10
5
|
program
|
|
11
6
|
.command('add-pack')
|
|
12
|
-
.argument('<
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'No agent project detected in current directory. Run this from an agent root.',
|
|
23
|
-
);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const s = p.spinner();
|
|
28
|
-
|
|
29
|
-
// Step 1: npm install
|
|
30
|
-
if (opts.install) {
|
|
31
|
-
s.start(`Installing ${packageName}...`);
|
|
32
|
-
try {
|
|
33
|
-
execFileSync('npm', ['install', packageName], {
|
|
34
|
-
cwd: ctx.agentPath,
|
|
35
|
-
stdio: 'pipe',
|
|
36
|
-
});
|
|
37
|
-
s.stop(`Installed ${packageName}`);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
s.stop('npm install failed');
|
|
40
|
-
p.log.error(err instanceof Error ? err.message : String(err));
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Step 2: Validate it's a DomainPack
|
|
46
|
-
s.start('Validating domain pack...');
|
|
47
|
-
let pack;
|
|
48
|
-
try {
|
|
49
|
-
const { loadDomainPack } = await import('@soleri/core');
|
|
50
|
-
pack = await loadDomainPack(packageName);
|
|
51
|
-
s.stop(
|
|
52
|
-
`Validated: ${pack.name} v${pack.version} (${pack.ops.length} ops, ${pack.domains.length} domains${pack.facades?.length ? `, ${pack.facades.length} facades` : ''})`,
|
|
53
|
-
);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
s.stop('Validation failed');
|
|
56
|
-
p.log.error(err instanceof Error ? err.message : String(err));
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Step 3: Update agent-config.json
|
|
61
|
-
s.start('Updating agent config...');
|
|
62
|
-
const configPath = join(ctx.agentPath, 'agent-config.json');
|
|
63
|
-
if (existsSync(configPath)) {
|
|
64
|
-
try {
|
|
65
|
-
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
66
|
-
if (!config.domainPacks) config.domainPacks = [];
|
|
67
|
-
|
|
68
|
-
const existing = config.domainPacks.find(
|
|
69
|
-
(ref: { package: string }) => ref.package === packageName,
|
|
70
|
-
);
|
|
71
|
-
if (existing) {
|
|
72
|
-
existing.version = pack.version;
|
|
73
|
-
s.stop('Updated existing pack reference in config');
|
|
74
|
-
} else {
|
|
75
|
-
config.domainPacks.push({
|
|
76
|
-
name: pack.name,
|
|
77
|
-
package: packageName,
|
|
78
|
-
version: pack.version,
|
|
79
|
-
});
|
|
80
|
-
s.stop('Added pack reference to config');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
84
|
-
} catch (err) {
|
|
85
|
-
s.stop('Config update failed');
|
|
86
|
-
p.log.error(err instanceof Error ? err.message : String(err));
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
s.stop('No agent-config.json found — skipped config update');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Step 4: Install knowledge (tiered)
|
|
94
|
-
s.start('Installing knowledge...');
|
|
95
|
-
try {
|
|
96
|
-
const { installKnowledge, createAgentRuntime } = await import('@soleri/core');
|
|
97
|
-
const runtime = createAgentRuntime({
|
|
98
|
-
agentId: ctx.agentId,
|
|
99
|
-
vaultPath: join(ctx.agentPath, '.vault', 'vault.db'),
|
|
100
|
-
});
|
|
101
|
-
const resolvedDir = join(ctx.agentPath, 'node_modules', packageName);
|
|
102
|
-
const knowledgeResult = await installKnowledge(pack, runtime, resolvedDir);
|
|
103
|
-
runtime.close();
|
|
104
|
-
const total =
|
|
105
|
-
knowledgeResult.canonical + knowledgeResult.curated + knowledgeResult.captured;
|
|
106
|
-
s.stop(
|
|
107
|
-
total > 0
|
|
108
|
-
? `Installed ${total} knowledge entries (${knowledgeResult.canonical} canonical, ${knowledgeResult.curated} curated, ${knowledgeResult.captured} captured)`
|
|
109
|
-
: 'No knowledge to install',
|
|
110
|
-
);
|
|
111
|
-
} catch (err) {
|
|
112
|
-
s.stop('Knowledge install skipped (non-fatal)');
|
|
113
|
-
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Step 5: Install skills
|
|
117
|
-
s.start('Installing skills...');
|
|
118
|
-
try {
|
|
119
|
-
const { installSkills } = await import('@soleri/core');
|
|
120
|
-
const resolvedDir = join(ctx.agentPath, 'node_modules', packageName);
|
|
121
|
-
const skillsDir = join(ctx.agentPath, 'skills');
|
|
122
|
-
const skillsResult = installSkills(pack, skillsDir, resolvedDir, opts.force);
|
|
123
|
-
s.stop(
|
|
124
|
-
skillsResult.installed > 0
|
|
125
|
-
? `Installed ${skillsResult.installed} skills (${skillsResult.skipped} skipped)`
|
|
126
|
-
: 'No skills to install',
|
|
127
|
-
);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
s.stop('Skills install skipped (non-fatal)');
|
|
130
|
-
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Step 6: Inject CLAUDE.md domain rules
|
|
134
|
-
if (pack.rules) {
|
|
135
|
-
s.start('Injecting CLAUDE.md domain rules...');
|
|
136
|
-
try {
|
|
137
|
-
const { injectDomainRules } = await import('@soleri/core');
|
|
138
|
-
const claudeMdPath = join(ctx.agentPath, 'CLAUDE.md');
|
|
139
|
-
injectDomainRules(claudeMdPath, pack.name, pack.rules);
|
|
140
|
-
s.stop('Injected domain rules into CLAUDE.md');
|
|
141
|
-
} catch (err) {
|
|
142
|
-
s.stop('CLAUDE.md injection skipped (non-fatal)');
|
|
143
|
-
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Step 7: Run forge inject (regenerate entry-point, tests)
|
|
148
|
-
if (opts.inject) {
|
|
149
|
-
s.start('Regenerating agent code...');
|
|
150
|
-
try {
|
|
151
|
-
execFileSync('npx', ['soleri', 'agent', 'refresh'], {
|
|
152
|
-
cwd: ctx.agentPath,
|
|
153
|
-
stdio: 'pipe',
|
|
154
|
-
});
|
|
155
|
-
s.stop('Regenerated entry-point, tests, and CLAUDE.md');
|
|
156
|
-
} catch {
|
|
157
|
-
s.stop('Forge inject skipped — run `soleri agent refresh` manually');
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Summary
|
|
162
|
-
p.log.success(`\nDomain pack "${pack.name}" added to ${ctx.agentId}`);
|
|
163
|
-
p.log.info(` Domains: ${pack.domains.join(', ')}`);
|
|
164
|
-
p.log.info(` Ops: ${pack.ops.length} custom operations`);
|
|
165
|
-
if (pack.facades?.length) {
|
|
166
|
-
p.log.info(` Facades: ${pack.facades.map((f) => f.name).join(', ')}`);
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
);
|
|
7
|
+
.argument('<pack>', 'Pack name')
|
|
8
|
+
.description('[DEPRECATED] Use "soleri pack install" or "soleri hooks add-pack" instead')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
p.log.warn(
|
|
11
|
+
'The "add-pack" command is deprecated.\n\n' +
|
|
12
|
+
'Use these commands instead:\n' +
|
|
13
|
+
' • soleri pack install <pack> — install knowledge/domain packs\n' +
|
|
14
|
+
' • soleri hooks add-pack <pack> — install hook packs\n',
|
|
15
|
+
);
|
|
16
|
+
});
|
|
170
17
|
}
|