@soleri/cli 9.11.0 → 9.13.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.
@@ -0,0 +1,566 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+
6
+ // Mock @clack/prompts to suppress console output during tests
7
+ vi.mock('@clack/prompts', () => ({
8
+ log: {
9
+ success: vi.fn(),
10
+ error: vi.fn(),
11
+ warn: vi.fn(),
12
+ info: vi.fn(),
13
+ },
14
+ confirm: vi.fn(),
15
+ isCancel: vi.fn(),
16
+ }));
17
+
18
+ import {
19
+ detectArtifacts,
20
+ removeDirectory,
21
+ removeClaudeMdBlock,
22
+ removePermissionEntries,
23
+ removeLauncherScript,
24
+ } from '../utils/agent-artifacts.js';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Test helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function makeTempDir(suffix: string): string {
31
+ const dir = join(
32
+ tmpdir(),
33
+ `uninstall-full-test-${suffix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
34
+ );
35
+ mkdirSync(dir, { recursive: true });
36
+ return dir;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // detectArtifacts
41
+ // ---------------------------------------------------------------------------
42
+
43
+ describe('detectArtifacts', () => {
44
+ let tempDir: string;
45
+ let originalHome: string;
46
+
47
+ beforeEach(() => {
48
+ tempDir = makeTempDir('detect');
49
+ originalHome = process.env.HOME ?? '';
50
+ process.env.HOME = tempDir;
51
+ process.env.USERPROFILE = tempDir;
52
+ });
53
+
54
+ afterEach(() => {
55
+ process.env.HOME = originalHome;
56
+ if (originalHome) process.env.USERPROFILE = originalHome;
57
+ rmSync(tempDir, { recursive: true, force: true });
58
+ });
59
+
60
+ it('detects an existing agent project directory', () => {
61
+ const agentDir = join(tempDir, 'my-agent');
62
+ mkdirSync(agentDir, { recursive: true });
63
+ writeFileSync(join(agentDir, 'agent.yaml'), 'name: test\n');
64
+
65
+ const manifest = detectArtifacts('my-agent', agentDir);
66
+ expect(manifest.projectDir).not.toBeNull();
67
+ expect(manifest.projectDir!.exists).toBe(true);
68
+ expect(manifest.projectDir!.path).toBe(agentDir);
69
+ });
70
+
71
+ it('returns exists: false for a non-existent agent directory', () => {
72
+ const nonExistent = join(tempDir, 'does-not-exist');
73
+ const manifest = detectArtifacts('ghost-agent', nonExistent);
74
+
75
+ expect(manifest.projectDir).not.toBeNull();
76
+ expect(manifest.projectDir!.exists).toBe(false);
77
+ expect(manifest.claudeMdBlocks).toEqual([]);
78
+ expect(manifest.mcpServerEntries).toEqual([]);
79
+ expect(manifest.permissionEntries).toEqual([]);
80
+ });
81
+
82
+ it('detects CLAUDE.md block with matching markers', () => {
83
+ const claudeMdPath = join(tempDir, 'CLAUDE.md');
84
+ const content = [
85
+ '# My Config',
86
+ '',
87
+ '<!-- agent:test-agent:mode -->',
88
+ '## Test Agent Mode',
89
+ 'Some content here.',
90
+ '<!-- /agent:test-agent:mode -->',
91
+ '',
92
+ '# Other stuff',
93
+ ].join('\n');
94
+ writeFileSync(claudeMdPath, content);
95
+
96
+ const manifest = detectArtifacts('test-agent', join(tempDir, 'nope'));
97
+ expect(manifest.claudeMdBlocks.length).toBe(1);
98
+ expect(manifest.claudeMdBlocks[0].startLine).toBe(3);
99
+ expect(manifest.claudeMdBlocks[0].endLine).toBe(6);
100
+ expect(manifest.claudeMdBlocks[0].path).toBe(claudeMdPath);
101
+ });
102
+
103
+ it('returns empty blocks when end marker is missing', () => {
104
+ const claudeMdPath = join(tempDir, 'CLAUDE.md');
105
+ const content = [
106
+ '# My Config',
107
+ '<!-- agent:test-agent:mode -->',
108
+ '## Test Agent Mode',
109
+ 'Some content here.',
110
+ // Missing end marker
111
+ ].join('\n');
112
+ writeFileSync(claudeMdPath, content);
113
+
114
+ const manifest = detectArtifacts('test-agent', join(tempDir, 'nope'));
115
+ expect(manifest.claudeMdBlocks).toEqual([]);
116
+ });
117
+
118
+ it('detects permission entries with matching prefix', () => {
119
+ const claudeDir = join(tempDir, '.claude');
120
+ mkdirSync(claudeDir, { recursive: true });
121
+ const settingsPath = join(claudeDir, 'settings.local.json');
122
+ const settings = {
123
+ permissions: {
124
+ allow: [
125
+ 'mcp__maria__design_check',
126
+ 'mcp__maria__color_pairs',
127
+ 'mcp__ernesto__vault_search',
128
+ 'mcp__ernesto__memory_capture',
129
+ 'Bash(*)',
130
+ ],
131
+ },
132
+ };
133
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
134
+
135
+ const manifest = detectArtifacts('maria', join(tempDir, 'nope'));
136
+ expect(manifest.permissionEntries.length).toBe(1);
137
+ expect(manifest.permissionEntries[0].matches).toEqual([
138
+ 'mcp__maria__design_check',
139
+ 'mcp__maria__color_pairs',
140
+ ]);
141
+ // Ernesto entries should NOT be included
142
+ expect(manifest.permissionEntries[0].matches).not.toContain('mcp__ernesto__vault_search');
143
+ });
144
+
145
+ it('does not match permission prefix that is a substring of another agent', () => {
146
+ const claudeDir = join(tempDir, '.claude');
147
+ mkdirSync(claudeDir, { recursive: true });
148
+ const settingsPath = join(claudeDir, 'settings.local.json');
149
+ const settings = {
150
+ permissions: {
151
+ allow: ['mcp__marianne__some_op', 'mcp__maria__design_check'],
152
+ },
153
+ };
154
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
155
+
156
+ const manifest = detectArtifacts('maria', join(tempDir, 'nope'));
157
+ expect(manifest.permissionEntries.length).toBe(1);
158
+ // Only the exact prefix match — mcp__maria__ does NOT match mcp__marianne__
159
+ expect(manifest.permissionEntries[0].matches).toEqual(['mcp__maria__design_check']);
160
+ });
161
+ });
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // removeDirectory
165
+ // ---------------------------------------------------------------------------
166
+
167
+ describe('removeDirectory', () => {
168
+ let tempDir: string;
169
+
170
+ beforeEach(() => {
171
+ tempDir = makeTempDir('rmdir');
172
+ });
173
+
174
+ afterEach(() => {
175
+ rmSync(tempDir, { recursive: true, force: true });
176
+ });
177
+
178
+ it('removes a directory with files', async () => {
179
+ const target = join(tempDir, 'to-remove');
180
+ mkdirSync(join(target, 'sub'), { recursive: true });
181
+ writeFileSync(join(target, 'file.txt'), 'data');
182
+ writeFileSync(join(target, 'sub', 'nested.txt'), 'nested');
183
+
184
+ const result = await removeDirectory(target);
185
+ expect(result.removed).toBe(true);
186
+ expect(result.path).toBe(target);
187
+ expect(existsSync(target)).toBe(false);
188
+ });
189
+
190
+ it('returns removed: false when called on a non-existent path (idempotent)', async () => {
191
+ const gone = join(tempDir, 'already-gone');
192
+ const result = await removeDirectory(gone);
193
+ expect(result.removed).toBe(false);
194
+ expect(result.path).toBe(gone);
195
+ expect(result.error).toBeUndefined();
196
+ });
197
+
198
+ it('is idempotent — second call after removal returns removed: false', async () => {
199
+ const target = join(tempDir, 'once');
200
+ mkdirSync(target);
201
+ writeFileSync(join(target, 'f.txt'), 'x');
202
+
203
+ const first = await removeDirectory(target);
204
+ expect(first.removed).toBe(true);
205
+
206
+ const second = await removeDirectory(target);
207
+ expect(second.removed).toBe(false);
208
+ });
209
+ });
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // removeClaudeMdBlock
213
+ // ---------------------------------------------------------------------------
214
+
215
+ describe('removeClaudeMdBlock', () => {
216
+ let tempDir: string;
217
+
218
+ beforeEach(() => {
219
+ tempDir = makeTempDir('claudemd');
220
+ });
221
+
222
+ afterEach(() => {
223
+ rmSync(tempDir, { recursive: true, force: true });
224
+ });
225
+
226
+ it('removes the block including markers', async () => {
227
+ const filePath = join(tempDir, 'CLAUDE.md');
228
+ const content = [
229
+ '# Config',
230
+ '',
231
+ '<!-- agent:foo:mode -->',
232
+ '## Foo Mode',
233
+ 'content',
234
+ '<!-- /agent:foo:mode -->',
235
+ '',
236
+ '# Other',
237
+ ].join('\n');
238
+ writeFileSync(filePath, content);
239
+
240
+ const result = await removeClaudeMdBlock(filePath, 3, 6);
241
+ expect(result.removed).toBe(true);
242
+
243
+ const after = readFileSync(filePath, 'utf-8');
244
+ expect(after).not.toContain('<!-- agent:foo:mode -->');
245
+ expect(after).not.toContain('## Foo Mode');
246
+ expect(after).not.toContain('<!-- /agent:foo:mode -->');
247
+ expect(after).toContain('# Config');
248
+ expect(after).toContain('# Other');
249
+ });
250
+
251
+ it('collapses triple blank lines after removal', async () => {
252
+ const filePath = join(tempDir, 'CLAUDE.md');
253
+ const content = [
254
+ '# Config',
255
+ '',
256
+ '',
257
+ '<!-- agent:foo:mode -->',
258
+ 'stuff',
259
+ '<!-- /agent:foo:mode -->',
260
+ '',
261
+ '',
262
+ '# Other',
263
+ ].join('\n');
264
+ writeFileSync(filePath, content);
265
+
266
+ const result = await removeClaudeMdBlock(filePath, 4, 6);
267
+ expect(result.removed).toBe(true);
268
+
269
+ const after = readFileSync(filePath, 'utf-8');
270
+ // Should not have 3+ consecutive newlines
271
+ expect(after).not.toMatch(/\n{3,}/);
272
+ });
273
+
274
+ it('returns removed: false for a non-existent file', async () => {
275
+ const result = await removeClaudeMdBlock(join(tempDir, 'nope.md'), 1, 3);
276
+ expect(result.removed).toBe(false);
277
+ });
278
+ });
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // removePermissionEntries
282
+ // ---------------------------------------------------------------------------
283
+
284
+ describe('removePermissionEntries', () => {
285
+ let tempDir: string;
286
+
287
+ beforeEach(() => {
288
+ tempDir = makeTempDir('perms');
289
+ });
290
+
291
+ afterEach(() => {
292
+ rmSync(tempDir, { recursive: true, force: true });
293
+ });
294
+
295
+ it('removes only the matching agent entries, keeps others', async () => {
296
+ const filePath = join(tempDir, 'settings.local.json');
297
+ const settings = {
298
+ permissions: {
299
+ allow: [
300
+ 'mcp__maria__design_check',
301
+ 'mcp__maria__color_pairs',
302
+ 'mcp__ernesto__vault_search',
303
+ 'Bash(*)',
304
+ ],
305
+ },
306
+ };
307
+ writeFileSync(filePath, JSON.stringify(settings, null, 2));
308
+
309
+ const result = await removePermissionEntries(filePath, 'maria');
310
+ expect(result.removed).toBe(true);
311
+
312
+ const after = JSON.parse(readFileSync(filePath, 'utf-8'));
313
+ expect(after.permissions.allow).toEqual(['mcp__ernesto__vault_search', 'Bash(*)']);
314
+ });
315
+
316
+ it('preserves 2-space JSON indentation', async () => {
317
+ const filePath = join(tempDir, 'settings.local.json');
318
+ const settings = {
319
+ permissions: {
320
+ allow: ['mcp__agent__op', 'other'],
321
+ },
322
+ };
323
+ writeFileSync(filePath, JSON.stringify(settings, null, 2));
324
+
325
+ await removePermissionEntries(filePath, 'agent');
326
+
327
+ const raw = readFileSync(filePath, 'utf-8');
328
+ // Verify 2-space indent is present
329
+ expect(raw).toContain(' "permissions"');
330
+ // Should end with trailing newline
331
+ expect(raw.endsWith('\n')).toBe(true);
332
+ });
333
+
334
+ it('returns removed: false when no entries match', async () => {
335
+ const filePath = join(tempDir, 'settings.local.json');
336
+ const settings = {
337
+ permissions: {
338
+ allow: ['mcp__other__op', 'Bash(*)'],
339
+ },
340
+ };
341
+ writeFileSync(filePath, JSON.stringify(settings, null, 2));
342
+
343
+ const result = await removePermissionEntries(filePath, 'maria');
344
+ expect(result.removed).toBe(false);
345
+ });
346
+
347
+ it('returns removed: false for a non-existent file', async () => {
348
+ const result = await removePermissionEntries(join(tempDir, 'nope.json'), 'agent');
349
+ expect(result.removed).toBe(false);
350
+ expect(result.error).toBeUndefined();
351
+ });
352
+
353
+ it('handles empty settings file with no permissions key', async () => {
354
+ const filePath = join(tempDir, 'settings.local.json');
355
+ writeFileSync(filePath, JSON.stringify({}, null, 2));
356
+
357
+ const result = await removePermissionEntries(filePath, 'agent');
358
+ expect(result.removed).toBe(false);
359
+ });
360
+
361
+ it('handles malformed JSON without throwing', async () => {
362
+ const filePath = join(tempDir, 'settings.local.json');
363
+ writeFileSync(filePath, '{ not valid json');
364
+
365
+ const result = await removePermissionEntries(filePath, 'agent');
366
+ expect(result.removed).toBe(false);
367
+ expect(result.error).toBe('Failed to parse JSON');
368
+ });
369
+ });
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // removeLauncherScript
373
+ // ---------------------------------------------------------------------------
374
+
375
+ describe('removeLauncherScript', () => {
376
+ let tempDir: string;
377
+
378
+ beforeEach(() => {
379
+ tempDir = makeTempDir('launcher');
380
+ });
381
+
382
+ afterEach(() => {
383
+ rmSync(tempDir, { recursive: true, force: true });
384
+ });
385
+
386
+ it('removes an existing file', async () => {
387
+ const scriptPath = join(tempDir, 'my-agent');
388
+ writeFileSync(scriptPath, '#!/bin/bash\necho hello\n');
389
+
390
+ const result = await removeLauncherScript(scriptPath);
391
+ expect(result.removed).toBe(true);
392
+ expect(result.path).toBe(scriptPath);
393
+ expect(existsSync(scriptPath)).toBe(false);
394
+ });
395
+
396
+ it('returns removed: false for a non-existent path', async () => {
397
+ const result = await removeLauncherScript(join(tempDir, 'nope'));
398
+ expect(result.removed).toBe(false);
399
+ expect(result.error).toBeUndefined();
400
+ });
401
+ });
402
+
403
+ // ---------------------------------------------------------------------------
404
+ // Integration test: full detect → remove → verify cycle
405
+ // ---------------------------------------------------------------------------
406
+
407
+ describe('integration: detect → remove → verify', () => {
408
+ let tempDir: string;
409
+ let originalHome: string;
410
+ const AGENT_ID = 'test-agent';
411
+
412
+ beforeEach(() => {
413
+ tempDir = makeTempDir('integration');
414
+ originalHome = process.env.HOME ?? '';
415
+ process.env.HOME = tempDir;
416
+ process.env.USERPROFILE = tempDir;
417
+
418
+ // 1. Project directory
419
+ const projectDir = join(tempDir, 'projects', AGENT_ID);
420
+ mkdirSync(projectDir, { recursive: true });
421
+ writeFileSync(join(projectDir, 'agent.yaml'), 'name: test-agent\n');
422
+
423
+ // 2. Legacy data directory
424
+ const legacyDir = join(tempDir, `.${AGENT_ID}`);
425
+ mkdirSync(legacyDir, { recursive: true });
426
+ writeFileSync(join(legacyDir, 'vault.db'), 'fake-db');
427
+
428
+ // 3. CLAUDE.md with agent block
429
+ const claudeMdPath = join(tempDir, 'CLAUDE.md');
430
+ const claudeMdContent = [
431
+ '# Home Config',
432
+ '',
433
+ `<!-- agent:${AGENT_ID}:mode -->`,
434
+ `## ${AGENT_ID} Mode`,
435
+ 'Agent instructions here.',
436
+ `<!-- /agent:${AGENT_ID}:mode -->`,
437
+ '',
438
+ '# Other Stuff',
439
+ ].join('\n');
440
+ writeFileSync(claudeMdPath, claudeMdContent);
441
+
442
+ // 4. Permissions in settings.local.json
443
+ const claudeDir = join(tempDir, '.claude');
444
+ mkdirSync(claudeDir, { recursive: true });
445
+ const settingsPath = join(claudeDir, 'settings.local.json');
446
+ const settings = {
447
+ permissions: {
448
+ allow: [
449
+ `mcp__${AGENT_ID}__vault_search`,
450
+ `mcp__${AGENT_ID}__memory_capture`,
451
+ 'mcp__other__something',
452
+ 'Bash(*)',
453
+ ],
454
+ },
455
+ };
456
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
457
+ });
458
+
459
+ afterEach(() => {
460
+ process.env.HOME = originalHome;
461
+ if (originalHome) process.env.USERPROFILE = originalHome;
462
+ rmSync(tempDir, { recursive: true, force: true });
463
+ });
464
+
465
+ it('detects all artifacts, removes them, and verifies clean state', async () => {
466
+ const projectDir = join(tempDir, 'projects', AGENT_ID);
467
+ const legacyDir = join(tempDir, `.${AGENT_ID}`);
468
+ const claudeMdPath = join(tempDir, 'CLAUDE.md');
469
+ const settingsPath = join(tempDir, '.claude', 'settings.local.json');
470
+
471
+ // --- Phase 1: Detect ---
472
+ const manifest = detectArtifacts(AGENT_ID, projectDir);
473
+
474
+ expect(manifest.agentId).toBe(AGENT_ID);
475
+ expect(manifest.projectDir!.exists).toBe(true);
476
+ expect(manifest.dataDirLegacy!.exists).toBe(true);
477
+ expect(manifest.claudeMdBlocks.length).toBe(1);
478
+ expect(manifest.permissionEntries.length).toBe(1);
479
+ expect(manifest.permissionEntries[0].matches.length).toBe(2);
480
+
481
+ // --- Phase 2: Remove ---
482
+ // Permission entries
483
+ const permResult = await removePermissionEntries(settingsPath, AGENT_ID);
484
+ expect(permResult.removed).toBe(true);
485
+
486
+ // CLAUDE.md block
487
+ const block = manifest.claudeMdBlocks[0];
488
+ const blockResult = await removeClaudeMdBlock(block.path, block.startLine, block.endLine);
489
+ expect(blockResult.removed).toBe(true);
490
+
491
+ // Directories
492
+ const projResult = await removeDirectory(projectDir);
493
+ expect(projResult.removed).toBe(true);
494
+
495
+ const legacyResult = await removeDirectory(legacyDir);
496
+ expect(legacyResult.removed).toBe(true);
497
+
498
+ // --- Phase 3: Verify clean state ---
499
+ expect(existsSync(projectDir)).toBe(false);
500
+ expect(existsSync(legacyDir)).toBe(false);
501
+
502
+ // CLAUDE.md should not contain agent markers
503
+ const claudeAfter = readFileSync(claudeMdPath, 'utf-8');
504
+ expect(claudeAfter).not.toContain(`<!-- agent:${AGENT_ID}:mode -->`);
505
+ expect(claudeAfter).toContain('# Home Config');
506
+ expect(claudeAfter).toContain('# Other Stuff');
507
+
508
+ // Settings should not contain agent permissions but keep other entries
509
+ const settingsAfter = JSON.parse(readFileSync(settingsPath, 'utf-8'));
510
+ expect(settingsAfter.permissions.allow).toEqual(['mcp__other__something', 'Bash(*)']);
511
+ });
512
+ });
513
+
514
+ // ---------------------------------------------------------------------------
515
+ // Edge cases
516
+ // ---------------------------------------------------------------------------
517
+
518
+ describe('edge cases', () => {
519
+ let tempDir: string;
520
+ let originalHome: string;
521
+
522
+ beforeEach(() => {
523
+ tempDir = makeTempDir('edge');
524
+ originalHome = process.env.HOME ?? '';
525
+ process.env.HOME = tempDir;
526
+ process.env.USERPROFILE = tempDir;
527
+ });
528
+
529
+ afterEach(() => {
530
+ process.env.HOME = originalHome;
531
+ if (originalHome) process.env.USERPROFILE = originalHome;
532
+ rmSync(tempDir, { recursive: true, force: true });
533
+ });
534
+
535
+ it('partial install: only project dir exists, no configs', () => {
536
+ const projectDir = join(tempDir, 'projects', 'partial-agent');
537
+ mkdirSync(projectDir, { recursive: true });
538
+ writeFileSync(join(projectDir, 'agent.yaml'), 'name: partial\n');
539
+
540
+ const manifest = detectArtifacts('partial-agent', projectDir);
541
+ expect(manifest.projectDir!.exists).toBe(true);
542
+ expect(manifest.claudeMdBlocks).toEqual([]);
543
+ expect(manifest.mcpServerEntries).toEqual([]);
544
+ expect(manifest.permissionEntries).toEqual([]);
545
+ });
546
+
547
+ it('empty settings.local.json (valid JSON, no permissions key)', () => {
548
+ const claudeDir = join(tempDir, '.claude');
549
+ mkdirSync(claudeDir, { recursive: true });
550
+ writeFileSync(join(claudeDir, 'settings.local.json'), '{}');
551
+
552
+ const manifest = detectArtifacts('some-agent', join(tempDir, 'nope'));
553
+ expect(manifest.permissionEntries).toEqual([]);
554
+ });
555
+
556
+ it('malformed JSON in settings file does not throw', () => {
557
+ const claudeDir = join(tempDir, '.claude');
558
+ mkdirSync(claudeDir, { recursive: true });
559
+ writeFileSync(join(claudeDir, 'settings.local.json'), '{ broken json !!!');
560
+
561
+ expect(() => {
562
+ const manifest = detectArtifacts('some-agent', join(tempDir, 'nope'));
563
+ expect(manifest.permissionEntries).toEqual([]);
564
+ }).not.toThrow();
565
+ });
566
+ });
@@ -1,4 +1,4 @@
1
- import { accessSync, constants as fsConstants, readFileSync, existsSync } from 'node:fs';
1
+ import { accessSync, constants as fsConstants, mkdirSync, readFileSync, existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import type { Command } from 'commander';
4
4
  import * as p from '@clack/prompts';
@@ -185,7 +185,15 @@ export function registerCreate(program: Command): void {
185
185
 
186
186
  const outputDir = opts?.dir ? resolve(opts.dir) : (config.outputDir ?? process.cwd());
187
187
 
188
- // Preflight: check output directory is writable
188
+ // Preflight: ensure output directory exists and is writable
189
+ if (!existsSync(outputDir)) {
190
+ try {
191
+ mkdirSync(outputDir, { recursive: true });
192
+ } catch {
193
+ p.log.error(`Cannot create ${outputDir} — check permissions`);
194
+ process.exit(1);
195
+ }
196
+ }
189
197
  try {
190
198
  accessSync(outputDir, fsConstants.W_OK);
191
199
  } catch {
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { watch, writeFileSync } from 'node:fs';
2
+ import { existsSync, watch, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import type { Command } from 'commander';
5
5
  import * as p from '@clack/prompts';
@@ -26,18 +26,29 @@ export function registerDev(program: Command): void {
26
26
  });
27
27
  }
28
28
 
29
- function runFileTreeDev(agentPath: string, agentId: string): void {
29
+ async function runFileTreeDev(agentPath: string, agentId: string): Promise<void> {
30
30
  p.log.info(`Starting ${agentId} in file-tree dev mode...`);
31
31
  p.log.info('Starting Knowledge Engine + watching for file changes.');
32
32
  p.log.info('CLAUDE.md will be regenerated automatically on changes.');
33
33
  p.log.info('Press Ctrl+C to stop.\n');
34
34
 
35
- regenerateClaudeMd(agentPath);
35
+ await regenerateClaudeMd(agentPath);
36
36
 
37
37
  // Start the engine server
38
38
  let engineBin: string;
39
39
  try {
40
- engineBin = require.resolve('@soleri/core/dist/engine/bin/soleri-engine.js');
40
+ const candidate = join(
41
+ agentPath,
42
+ 'node_modules',
43
+ '@soleri',
44
+ 'core',
45
+ 'dist',
46
+ 'engine',
47
+ 'bin',
48
+ 'soleri-engine.js',
49
+ );
50
+ if (!existsSync(candidate)) throw new Error('Engine not found at ' + candidate);
51
+ engineBin = candidate;
41
52
  } catch {
42
53
  p.log.error('Engine not found. Run: npm install @soleri/core');
43
54
  process.exit(1);
@@ -73,10 +84,10 @@ function runFileTreeDev(agentPath: string, agentId: string): void {
73
84
 
74
85
  // Debounce — regenerate at most once per 200ms
75
86
  if (debounceTimer) clearTimeout(debounceTimer);
76
- debounceTimer = setTimeout(() => {
87
+ debounceTimer = setTimeout(async () => {
77
88
  const changedFile = filename ? ` (${filename})` : '';
78
89
  p.log.info(`Change detected${changedFile} — regenerating CLAUDE.md`);
79
- regenerateClaudeMd(agentPath);
90
+ await regenerateClaudeMd(agentPath);
80
91
  }, 200);
81
92
  });
82
93
  } catch (err: unknown) {
@@ -105,11 +116,10 @@ function runFileTreeDev(agentPath: string, agentId: string): void {
105
116
  });
106
117
  }
107
118
 
108
- function regenerateClaudeMd(agentPath: string): void {
119
+ async function regenerateClaudeMd(agentPath: string): Promise<void> {
109
120
  try {
110
121
  // Dynamic import to avoid loading forge at CLI startup
111
- // eslint-disable-next-line @typescript-eslint/no-require-imports
112
- const { composeClaudeMd } = require('@soleri/forge/lib');
122
+ const { composeClaudeMd } = await import('@soleri/forge/lib');
113
123
  const { content } = composeClaudeMd(agentPath);
114
124
  writeFileSync(join(agentPath, 'CLAUDE.md'), content, 'utf-8');
115
125
  p.log.success('CLAUDE.md regenerated');