@soleri/cli 9.0.2 → 9.2.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/agent.js +116 -3
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/create.js +6 -2
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/hooks.js +43 -49
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +61 -12
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/pack.js +0 -1
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/staging.d.ts +2 -0
- package/dist/commands/staging.js +175 -0
- package/dist/commands/staging.js.map +1 -0
- package/dist/hook-packs/full/manifest.json +2 -2
- package/dist/hook-packs/installer.d.ts +4 -11
- package/dist/hook-packs/installer.js +192 -23
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +173 -60
- package/dist/hook-packs/registry.d.ts +16 -13
- package/dist/hook-packs/registry.js +13 -28
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +33 -46
- package/dist/hook-packs/yolo-safety/manifest.json +23 -0
- package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
- package/dist/hooks/templates.js +1 -1
- package/dist/hooks/templates.js.map +1 -1
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/create.test.ts +6 -2
- package/src/__tests__/hook-packs.test.ts +66 -44
- package/src/__tests__/wizard-e2e.mjs +5 -0
- package/src/commands/agent.ts +146 -3
- package/src/commands/create.ts +8 -2
- package/src/commands/hooks.ts +88 -187
- package/src/commands/install.ts +62 -22
- package/src/commands/pack.ts +0 -1
- package/src/commands/staging.ts +208 -0
- package/src/hook-packs/full/manifest.json +2 -2
- package/src/hook-packs/installer.ts +173 -60
- package/src/hook-packs/registry.ts +33 -46
- package/src/hook-packs/yolo-safety/manifest.json +23 -0
- package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
- package/src/hooks/templates.ts +1 -1
- package/src/main.ts +2 -0
- package/dist/commands/cognee.d.ts +0 -10
- package/dist/commands/cognee.js +0 -364
- package/dist/commands/cognee.js.map +0 -1
|
@@ -3,7 +3,6 @@ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
|
|
6
|
-
// Mock homedir to use a temp directory instead of real ~/.claude/
|
|
7
6
|
const tempHome = join(tmpdir(), `cli-hookpacks-test-${Date.now()}`);
|
|
8
7
|
|
|
9
8
|
vi.mock('node:os', async () => {
|
|
@@ -24,18 +23,11 @@ describe('hook-packs', () => {
|
|
|
24
23
|
});
|
|
25
24
|
|
|
26
25
|
describe('registry', () => {
|
|
27
|
-
it('should list all
|
|
26
|
+
it('should list all 6 built-in packs', () => {
|
|
28
27
|
const packs = listPacks();
|
|
29
|
-
expect(packs.length).toBe(
|
|
30
|
-
|
|
28
|
+
expect(packs.length).toBe(6);
|
|
31
29
|
const names = packs.map((p) => p.name).sort();
|
|
32
|
-
expect(names).toEqual([
|
|
33
|
-
'a11y',
|
|
34
|
-
'clean-commits',
|
|
35
|
-
'css-discipline',
|
|
36
|
-
'full',
|
|
37
|
-
'typescript-safety',
|
|
38
|
-
]);
|
|
30
|
+
expect(names).toEqual(['a11y', 'clean-commits', 'css-discipline', 'full', 'typescript-safety', 'yolo-safety']);
|
|
39
31
|
});
|
|
40
32
|
|
|
41
33
|
it('should get a specific pack by name', () => {
|
|
@@ -50,35 +42,40 @@ describe('hook-packs', () => {
|
|
|
50
42
|
expect(getPack('nonexistent')).toBeNull();
|
|
51
43
|
});
|
|
52
44
|
|
|
53
|
-
it('should return full pack with composedFrom', () => {
|
|
45
|
+
it('should return full pack with composedFrom including yolo-safety', () => {
|
|
54
46
|
const pack = getPack('full');
|
|
55
47
|
expect(pack).not.toBeNull();
|
|
56
48
|
expect(pack!.manifest.composedFrom).toEqual([
|
|
57
|
-
'typescript-safety',
|
|
58
|
-
'a11y',
|
|
59
|
-
'css-discipline',
|
|
60
|
-
'clean-commits',
|
|
49
|
+
'typescript-safety', 'a11y', 'css-discipline', 'clean-commits', 'yolo-safety',
|
|
61
50
|
]);
|
|
62
51
|
expect(pack!.manifest.hooks).toHaveLength(8);
|
|
63
52
|
});
|
|
64
53
|
|
|
65
54
|
it('should return empty installed packs when none installed', () => {
|
|
66
|
-
|
|
55
|
+
const installed = getInstalledPacks();
|
|
56
|
+
expect(installed.filter((p) => p !== 'yolo-safety')).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should get yolo-safety pack with scripts and lifecycleHooks', () => {
|
|
60
|
+
const pack = getPack('yolo-safety');
|
|
61
|
+
expect(pack).not.toBeNull();
|
|
62
|
+
expect(pack!.manifest.name).toBe('yolo-safety');
|
|
63
|
+
expect(pack!.manifest.hooks).toEqual([]);
|
|
64
|
+
expect(pack!.manifest.scripts).toHaveLength(1);
|
|
65
|
+
expect(pack!.manifest.scripts![0].name).toBe('anti-deletion');
|
|
66
|
+
expect(pack!.manifest.lifecycleHooks).toHaveLength(1);
|
|
67
|
+
expect(pack!.manifest.lifecycleHooks![0].event).toBe('PreToolUse');
|
|
67
68
|
});
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
describe('installer', () => {
|
|
71
72
|
it('should install a simple pack', () => {
|
|
72
73
|
const result = installPack('typescript-safety');
|
|
73
|
-
|
|
74
74
|
expect(result.installed).toEqual(['no-any-types', 'no-console-log']);
|
|
75
75
|
expect(result.skipped).toEqual([]);
|
|
76
|
-
|
|
77
76
|
const claudeDir = join(tempHome, '.claude');
|
|
78
77
|
expect(existsSync(join(claudeDir, 'hookify.no-any-types.local.md'))).toBe(true);
|
|
79
78
|
expect(existsSync(join(claudeDir, 'hookify.no-console-log.local.md'))).toBe(true);
|
|
80
|
-
|
|
81
|
-
// Verify content was copied correctly
|
|
82
79
|
const content = readFileSync(join(claudeDir, 'hookify.no-any-types.local.md'), 'utf-8');
|
|
83
80
|
expect(content).toContain('name: no-any-types');
|
|
84
81
|
expect(content).toContain('Soleri Hook Pack: typescript-safety');
|
|
@@ -87,40 +84,29 @@ describe('hook-packs', () => {
|
|
|
87
84
|
it('should be idempotent — skip existing files', () => {
|
|
88
85
|
installPack('typescript-safety');
|
|
89
86
|
const result = installPack('typescript-safety');
|
|
90
|
-
|
|
91
87
|
expect(result.installed).toEqual([]);
|
|
92
88
|
expect(result.skipped).toEqual(['no-any-types', 'no-console-log']);
|
|
93
89
|
});
|
|
94
90
|
|
|
95
91
|
it('should install composed pack (full)', () => {
|
|
96
92
|
const result = installPack('full');
|
|
97
|
-
|
|
98
93
|
expect(result.installed).toHaveLength(8);
|
|
99
94
|
expect(result.skipped).toEqual([]);
|
|
100
|
-
|
|
101
95
|
const claudeDir = join(tempHome, '.claude');
|
|
102
|
-
const
|
|
103
|
-
'no-any-types',
|
|
104
|
-
'no-console-log',
|
|
105
|
-
'no-important',
|
|
106
|
-
'no-inline-styles',
|
|
107
|
-
'semantic-html',
|
|
108
|
-
'focus-ring-required',
|
|
109
|
-
'ux-touch-targets',
|
|
110
|
-
'no-ai-attribution',
|
|
111
|
-
];
|
|
112
|
-
for (const hook of expectedHooks) {
|
|
96
|
+
for (const hook of ['no-any-types', 'no-console-log', 'no-important', 'no-inline-styles', 'semantic-html', 'focus-ring-required', 'ux-touch-targets', 'no-ai-attribution']) {
|
|
113
97
|
expect(existsSync(join(claudeDir, `hookify.${hook}.local.md`))).toBe(true);
|
|
114
98
|
}
|
|
99
|
+
expect(result.scripts).toHaveLength(1);
|
|
100
|
+
expect(result.scripts[0]).toBe('hooks/anti-deletion.sh');
|
|
101
|
+
expect(existsSync(join(claudeDir, 'hooks', 'anti-deletion.sh'))).toBe(true);
|
|
115
102
|
});
|
|
116
103
|
|
|
117
104
|
it('should skip already-installed hooks when installing full after partial', () => {
|
|
118
105
|
installPack('typescript-safety');
|
|
119
106
|
const result = installPack('full');
|
|
120
|
-
|
|
121
107
|
expect(result.skipped).toContain('no-any-types');
|
|
122
108
|
expect(result.skipped).toContain('no-console-log');
|
|
123
|
-
expect(result.installed).toHaveLength(6);
|
|
109
|
+
expect(result.installed).toHaveLength(6);
|
|
124
110
|
});
|
|
125
111
|
|
|
126
112
|
it('should throw for unknown pack', () => {
|
|
@@ -130,9 +116,7 @@ describe('hook-packs', () => {
|
|
|
130
116
|
it('should remove a pack', () => {
|
|
131
117
|
installPack('a11y');
|
|
132
118
|
const result = removePack('a11y');
|
|
133
|
-
|
|
134
119
|
expect(result.removed).toEqual(['semantic-html', 'focus-ring-required', 'ux-touch-targets']);
|
|
135
|
-
|
|
136
120
|
const claudeDir = join(tempHome, '.claude');
|
|
137
121
|
expect(existsSync(join(claudeDir, 'hookify.semantic-html.local.md'))).toBe(false);
|
|
138
122
|
});
|
|
@@ -145,6 +129,42 @@ describe('hook-packs', () => {
|
|
|
145
129
|
it('should throw for unknown pack on remove', () => {
|
|
146
130
|
expect(() => removePack('nonexistent')).toThrow('Unknown hook pack: "nonexistent"');
|
|
147
131
|
});
|
|
132
|
+
|
|
133
|
+
it('should install yolo-safety pack with scripts and lifecycle hooks', () => {
|
|
134
|
+
const result = installPack('yolo-safety');
|
|
135
|
+
expect(result.installed).toEqual([]);
|
|
136
|
+
expect(result.scripts).toHaveLength(1);
|
|
137
|
+
expect(result.scripts[0]).toBe('hooks/anti-deletion.sh');
|
|
138
|
+
expect(result.lifecycleHooks).toHaveLength(1);
|
|
139
|
+
expect(result.lifecycleHooks[0]).toBe('PreToolUse:Bash');
|
|
140
|
+
const claudeDir = join(tempHome, '.claude');
|
|
141
|
+
expect(existsSync(join(claudeDir, 'hooks', 'anti-deletion.sh'))).toBe(true);
|
|
142
|
+
const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
|
|
143
|
+
expect(settings.hooks).toBeDefined();
|
|
144
|
+
expect(settings.hooks.PreToolUse).toHaveLength(1);
|
|
145
|
+
expect(settings.hooks.PreToolUse[0].command).toBe('bash ~/.claude/hooks/anti-deletion.sh');
|
|
146
|
+
expect(settings.hooks.PreToolUse[0]._soleriPack).toBe('yolo-safety');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should remove yolo-safety pack including scripts and lifecycle hooks', () => {
|
|
150
|
+
installPack('yolo-safety');
|
|
151
|
+
const result = removePack('yolo-safety');
|
|
152
|
+
expect(result.scripts).toHaveLength(1);
|
|
153
|
+
expect(result.lifecycleHooks).toHaveLength(1);
|
|
154
|
+
const claudeDir = join(tempHome, '.claude');
|
|
155
|
+
expect(existsSync(join(claudeDir, 'hooks', 'anti-deletion.sh'))).toBe(false);
|
|
156
|
+
const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
|
|
157
|
+
expect(settings.hooks.PreToolUse).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should be idempotent for yolo-safety lifecycle hooks', () => {
|
|
161
|
+
installPack('yolo-safety');
|
|
162
|
+
const result2 = installPack('yolo-safety');
|
|
163
|
+
expect(result2.lifecycleHooks).toEqual([]);
|
|
164
|
+
const claudeDir = join(tempHome, '.claude');
|
|
165
|
+
const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
|
|
166
|
+
expect(settings.hooks.PreToolUse).toHaveLength(1);
|
|
167
|
+
});
|
|
148
168
|
});
|
|
149
169
|
|
|
150
170
|
describe('isPackInstalled', () => {
|
|
@@ -158,7 +178,6 @@ describe('hook-packs', () => {
|
|
|
158
178
|
});
|
|
159
179
|
|
|
160
180
|
it('should return partial when some hooks present', () => {
|
|
161
|
-
// Install just one of the two hooks
|
|
162
181
|
const claudeDir = join(tempHome, '.claude');
|
|
163
182
|
writeFileSync(join(claudeDir, 'hookify.no-any-types.local.md'), 'test');
|
|
164
183
|
expect(isPackInstalled('typescript-safety')).toBe('partial');
|
|
@@ -167,29 +186,32 @@ describe('hook-packs', () => {
|
|
|
167
186
|
it('should return false for unknown pack', () => {
|
|
168
187
|
expect(isPackInstalled('nonexistent')).toBe(false);
|
|
169
188
|
});
|
|
189
|
+
|
|
190
|
+
it('should detect yolo-safety as installed when script is present', () => {
|
|
191
|
+
installPack('yolo-safety');
|
|
192
|
+
expect(isPackInstalled('yolo-safety')).toBe(true);
|
|
193
|
+
});
|
|
170
194
|
});
|
|
171
195
|
|
|
172
196
|
describe('getInstalledPacks', () => {
|
|
173
197
|
it('should list installed packs', () => {
|
|
174
198
|
installPack('typescript-safety');
|
|
175
199
|
installPack('a11y');
|
|
176
|
-
|
|
177
200
|
const installed = getInstalledPacks();
|
|
178
201
|
expect(installed).toContain('typescript-safety');
|
|
179
202
|
expect(installed).toContain('a11y');
|
|
180
203
|
expect(installed).not.toContain('css-discipline');
|
|
181
204
|
});
|
|
182
205
|
|
|
183
|
-
it('should include full when all
|
|
206
|
+
it('should include full when all hooks and scripts are present', () => {
|
|
184
207
|
installPack('full');
|
|
185
|
-
|
|
186
208
|
const installed = getInstalledPacks();
|
|
187
|
-
// All packs should show as installed since full installs all hooks
|
|
188
209
|
expect(installed).toContain('full');
|
|
189
210
|
expect(installed).toContain('typescript-safety');
|
|
190
211
|
expect(installed).toContain('a11y');
|
|
191
212
|
expect(installed).toContain('css-discipline');
|
|
192
213
|
expect(installed).toContain('clean-commits');
|
|
214
|
+
expect(installed).toContain('yolo-safety');
|
|
193
215
|
});
|
|
194
216
|
});
|
|
195
217
|
});
|
|
@@ -35,11 +35,13 @@ function assert(cond, msg, ctx = '') {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function stripAnsi(s) {
|
|
38
|
+
/* oxlint-disable eslint(no-control-regex) -- intentional ANSI control char stripping */
|
|
38
39
|
// eslint-disable-next-line no-control-regex
|
|
39
40
|
return s
|
|
40
41
|
.replace(new RegExp('\x1B\\[[0-9;]*[A-Za-z]', 'g'), '')
|
|
41
42
|
.replace(new RegExp('\x1B\\].*?\x07', 'g'), '')
|
|
42
43
|
.replace(new RegExp('\r', 'g'), '');
|
|
44
|
+
/* oxlint-enable eslint(no-control-regex) */
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function sleep(ms) {
|
|
@@ -82,6 +84,7 @@ function runWizard(name, actions, opts = {}) {
|
|
|
82
84
|
|
|
83
85
|
if (matched) {
|
|
84
86
|
actionIndex++;
|
|
87
|
+
// oxlint-disable-next-line eslint(no-await-in-loop)
|
|
85
88
|
await sleep(a.delay || 150);
|
|
86
89
|
if (!state.completed) {
|
|
87
90
|
try {
|
|
@@ -89,6 +92,7 @@ function runWizard(name, actions, opts = {}) {
|
|
|
89
92
|
} catch {}
|
|
90
93
|
}
|
|
91
94
|
} else {
|
|
95
|
+
// oxlint-disable-next-line eslint(no-await-in-loop)
|
|
92
96
|
await sleep(100);
|
|
93
97
|
}
|
|
94
98
|
}
|
|
@@ -455,6 +459,7 @@ await testDeclineConfirm();
|
|
|
455
459
|
|
|
456
460
|
// All 7 archetypes (each scaffolds + builds, slower)
|
|
457
461
|
for (let i = 0; i < ARCHETYPES.length; i++) {
|
|
462
|
+
// oxlint-disable-next-line eslint(no-await-in-loop)
|
|
458
463
|
await testArchetype(ARCHETYPES[i], i);
|
|
459
464
|
}
|
|
460
465
|
|
package/src/commands/agent.ts
CHANGED
|
@@ -5,12 +5,22 @@
|
|
|
5
5
|
* `soleri agent update` — OTA engine upgrade with migration support.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import {
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
cpSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
} from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
10
20
|
import { execFileSync } from 'node:child_process';
|
|
11
21
|
import type { Command } from 'commander';
|
|
12
22
|
import * as p from '@clack/prompts';
|
|
13
|
-
import { PackLockfile, checkNpmVersion, checkVersionCompat } from '@soleri/core';
|
|
23
|
+
import { PackLockfile, checkNpmVersion, checkVersionCompat, SOLERI_HOME } from '@soleri/core';
|
|
14
24
|
import {
|
|
15
25
|
generateClaudeMdTemplate,
|
|
16
26
|
generateInjectClaudeMd,
|
|
@@ -18,6 +28,7 @@ import {
|
|
|
18
28
|
} from '@soleri/forge/lib';
|
|
19
29
|
import type { AgentConfig } from '@soleri/forge/lib';
|
|
20
30
|
import { detectAgent } from '../utils/agent-context.js';
|
|
31
|
+
import { installClaude } from './install.js';
|
|
21
32
|
|
|
22
33
|
export function registerAgent(program: Command): void {
|
|
23
34
|
const agent = program.command('agent').description('Agent lifecycle management');
|
|
@@ -330,6 +341,138 @@ export function registerAgent(program: Command): void {
|
|
|
330
341
|
}
|
|
331
342
|
});
|
|
332
343
|
|
|
344
|
+
// ─── migrate ──────────────────────────────────────────────
|
|
345
|
+
// Temporary command — moves agent data from ~/.{agentId}/ to ~/.soleri/{agentId}/.
|
|
346
|
+
// Will be removed in the next major version after all users migrate.
|
|
347
|
+
agent
|
|
348
|
+
.command('migrate')
|
|
349
|
+
.argument('<agentId>', 'Agent ID to migrate (e.g. ernesto, salvador)')
|
|
350
|
+
.option('--dry-run', 'Preview what would be moved without executing')
|
|
351
|
+
.description('Move agent data from ~/.{agentId}/ to ~/.soleri/{agentId}/ (one-time migration)')
|
|
352
|
+
.action((agentId: string, opts: { dryRun?: boolean }) => {
|
|
353
|
+
const legacyHome = join(homedir(), `.${agentId}`);
|
|
354
|
+
const newHome = join(SOLERI_HOME, agentId);
|
|
355
|
+
|
|
356
|
+
// Data files to migrate (relative to agent home)
|
|
357
|
+
const dataFiles = [
|
|
358
|
+
'vault.db',
|
|
359
|
+
'vault.db-shm',
|
|
360
|
+
'vault.db-wal',
|
|
361
|
+
'plans.json',
|
|
362
|
+
'keys.json',
|
|
363
|
+
'flags.json',
|
|
364
|
+
];
|
|
365
|
+
const dataDirs = ['templates'];
|
|
366
|
+
|
|
367
|
+
// Check if legacy data exists
|
|
368
|
+
if (!existsSync(legacyHome)) {
|
|
369
|
+
p.log.info(`No legacy data found at ${legacyHome} — nothing to migrate.`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check if already migrated
|
|
374
|
+
if (existsSync(join(newHome, 'vault.db'))) {
|
|
375
|
+
p.log.warn(`Data already exists at ${newHome}/vault.db — migration may have already run.`);
|
|
376
|
+
p.log.info('If you want to force re-migration, remove the new directory first.');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Discover what to move
|
|
381
|
+
const toMove: Array<{ src: string; dst: string; type: 'file' | 'dir' }> = [];
|
|
382
|
+
|
|
383
|
+
for (const file of dataFiles) {
|
|
384
|
+
const src = join(legacyHome, file);
|
|
385
|
+
if (existsSync(src)) {
|
|
386
|
+
toMove.push({ src, dst: join(newHome, file), type: 'file' });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const dir of dataDirs) {
|
|
391
|
+
const src = join(legacyHome, dir);
|
|
392
|
+
if (existsSync(src)) {
|
|
393
|
+
toMove.push({ src, dst: join(newHome, dir), type: 'dir' });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (toMove.length === 0) {
|
|
398
|
+
p.log.info(`No data files found in ${legacyHome} — nothing to migrate.`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Preview
|
|
403
|
+
console.log(`\n Migration: ${legacyHome} → ${newHome}\n`);
|
|
404
|
+
for (const item of toMove) {
|
|
405
|
+
const label = item.type === 'dir' ? '(dir) ' : '';
|
|
406
|
+
console.log(` ${label}${item.src} → ${item.dst}`);
|
|
407
|
+
}
|
|
408
|
+
console.log('');
|
|
409
|
+
|
|
410
|
+
if (opts.dryRun) {
|
|
411
|
+
p.log.info(
|
|
412
|
+
`Dry run — ${toMove.length} items would be moved. Run without --dry-run to execute.`,
|
|
413
|
+
);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Execute migration
|
|
418
|
+
const s = p.spinner();
|
|
419
|
+
s.start('Migrating agent data...');
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
// Create new home directory
|
|
423
|
+
mkdirSync(newHome, { recursive: true });
|
|
424
|
+
|
|
425
|
+
let moved = 0;
|
|
426
|
+
for (const item of toMove) {
|
|
427
|
+
mkdirSync(dirname(item.dst), { recursive: true });
|
|
428
|
+
try {
|
|
429
|
+
// Try atomic rename first (same filesystem)
|
|
430
|
+
renameSync(item.src, item.dst);
|
|
431
|
+
} catch {
|
|
432
|
+
// Cross-filesystem: copy then remove
|
|
433
|
+
if (item.type === 'dir') {
|
|
434
|
+
cpSync(item.src, item.dst, { recursive: true });
|
|
435
|
+
rmSync(item.src, { recursive: true });
|
|
436
|
+
} else {
|
|
437
|
+
cpSync(item.src, item.dst);
|
|
438
|
+
rmSync(item.src);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
moved++;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
s.stop(`Migrated ${moved} items to ${newHome}`);
|
|
445
|
+
|
|
446
|
+
// Detect agent definition (agent.yaml) to re-register MCP
|
|
447
|
+
const agentYaml = join(newHome, 'agent.yaml');
|
|
448
|
+
const legacyAgentYaml = join(legacyHome, 'agent.yaml');
|
|
449
|
+
|
|
450
|
+
if (existsSync(agentYaml) || existsSync(legacyAgentYaml)) {
|
|
451
|
+
// If agent.yaml is still in legacy dir, move it too
|
|
452
|
+
if (!existsSync(agentYaml) && existsSync(legacyAgentYaml)) {
|
|
453
|
+
p.log.info(
|
|
454
|
+
'Note: agent.yaml is still at the old location. Move the entire agent folder if needed.',
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Re-register MCP pointing to new location
|
|
460
|
+
const agentDir = existsSync(agentYaml) ? newHome : legacyHome;
|
|
461
|
+
if (existsSync(join(agentDir, 'agent.yaml'))) {
|
|
462
|
+
installClaude(agentId, agentDir, true);
|
|
463
|
+
p.log.success('MCP registration updated to new path.');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
p.log.info(
|
|
467
|
+
`Legacy directory preserved at ${legacyHome} (safe to remove manually after verifying).`,
|
|
468
|
+
);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
s.stop('Migration failed');
|
|
471
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
333
476
|
// ─── validate ──────────────────────────────────────────────
|
|
334
477
|
agent
|
|
335
478
|
.command('validate')
|
package/src/commands/create.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import type { Command } from 'commander';
|
|
4
5
|
import * as p from '@clack/prompts';
|
|
5
6
|
import {
|
|
@@ -14,6 +15,9 @@ import { runCreateWizard } from '../prompts/create-wizard.js';
|
|
|
14
15
|
import { listPacks } from '../hook-packs/registry.js';
|
|
15
16
|
import { installPack } from '../hook-packs/installer.js';
|
|
16
17
|
|
|
18
|
+
/** Default parent directory for new agents: ~/.soleri/ */
|
|
19
|
+
const SOLERI_HOME = process.env.SOLERI_HOME ?? join(homedir(), '.soleri');
|
|
20
|
+
|
|
17
21
|
function parseSetupTarget(value?: string): SetupTarget | undefined {
|
|
18
22
|
if (!value) return undefined;
|
|
19
23
|
if ((SETUP_TARGETS as readonly string[]).includes(value)) {
|
|
@@ -37,6 +41,7 @@ export function registerCreate(program: Command): void {
|
|
|
37
41
|
`Setup target: ${SETUP_TARGETS.join(', ')} (default: claude)`,
|
|
38
42
|
)
|
|
39
43
|
.option('-y, --yes', 'Skip confirmation prompts (use with --config for fully non-interactive)')
|
|
44
|
+
.option('--dir <path>', `Parent directory for the agent (default: ~/.soleri/)`)
|
|
40
45
|
.option('--filetree', 'Create a file-tree agent (v7 — no TypeScript, no build step)')
|
|
41
46
|
.option('--legacy', 'Create a legacy TypeScript agent (v6 — requires npm install + build)')
|
|
42
47
|
.description('Create a new Soleri agent')
|
|
@@ -46,6 +51,7 @@ export function registerCreate(program: Command): void {
|
|
|
46
51
|
opts?: {
|
|
47
52
|
config?: string;
|
|
48
53
|
yes?: boolean;
|
|
54
|
+
dir?: string;
|
|
49
55
|
setupTarget?: string;
|
|
50
56
|
filetree?: boolean;
|
|
51
57
|
legacy?: boolean;
|
|
@@ -148,7 +154,7 @@ export function registerCreate(program: Command): void {
|
|
|
148
154
|
})),
|
|
149
155
|
};
|
|
150
156
|
|
|
151
|
-
const outputDir = config.outputDir ??
|
|
157
|
+
const outputDir = opts?.dir ? resolve(opts.dir) : (config.outputDir ?? SOLERI_HOME);
|
|
152
158
|
const nonInteractive = !!(opts?.yes || opts?.config);
|
|
153
159
|
|
|
154
160
|
if (!nonInteractive) {
|