@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.
Files changed (49) hide show
  1. package/dist/commands/agent.js +116 -3
  2. package/dist/commands/agent.js.map +1 -1
  3. package/dist/commands/create.js +6 -2
  4. package/dist/commands/create.js.map +1 -1
  5. package/dist/commands/hooks.js +43 -49
  6. package/dist/commands/hooks.js.map +1 -1
  7. package/dist/commands/install.d.ts +1 -0
  8. package/dist/commands/install.js +61 -12
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/pack.js +0 -1
  11. package/dist/commands/pack.js.map +1 -1
  12. package/dist/commands/staging.d.ts +2 -0
  13. package/dist/commands/staging.js +175 -0
  14. package/dist/commands/staging.js.map +1 -0
  15. package/dist/hook-packs/full/manifest.json +2 -2
  16. package/dist/hook-packs/installer.d.ts +4 -11
  17. package/dist/hook-packs/installer.js +192 -23
  18. package/dist/hook-packs/installer.js.map +1 -1
  19. package/dist/hook-packs/installer.ts +173 -60
  20. package/dist/hook-packs/registry.d.ts +16 -13
  21. package/dist/hook-packs/registry.js +13 -28
  22. package/dist/hook-packs/registry.js.map +1 -1
  23. package/dist/hook-packs/registry.ts +33 -46
  24. package/dist/hook-packs/yolo-safety/manifest.json +23 -0
  25. package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
  26. package/dist/hooks/templates.js +1 -1
  27. package/dist/hooks/templates.js.map +1 -1
  28. package/dist/main.js +2 -0
  29. package/dist/main.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/__tests__/create.test.ts +6 -2
  32. package/src/__tests__/hook-packs.test.ts +66 -44
  33. package/src/__tests__/wizard-e2e.mjs +5 -0
  34. package/src/commands/agent.ts +146 -3
  35. package/src/commands/create.ts +8 -2
  36. package/src/commands/hooks.ts +88 -187
  37. package/src/commands/install.ts +62 -22
  38. package/src/commands/pack.ts +0 -1
  39. package/src/commands/staging.ts +208 -0
  40. package/src/hook-packs/full/manifest.json +2 -2
  41. package/src/hook-packs/installer.ts +173 -60
  42. package/src/hook-packs/registry.ts +33 -46
  43. package/src/hook-packs/yolo-safety/manifest.json +23 -0
  44. package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
  45. package/src/hooks/templates.ts +1 -1
  46. package/src/main.ts +2 -0
  47. package/dist/commands/cognee.d.ts +0 -10
  48. package/dist/commands/cognee.js +0 -364
  49. 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 5 built-in packs', () => {
26
+ it('should list all 6 built-in packs', () => {
28
27
  const packs = listPacks();
29
- expect(packs.length).toBe(5);
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
- expect(getInstalledPacks()).toEqual([]);
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 expectedHooks = [
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); // 8 total - 2 already installed
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 8 hooks are present', () => {
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
 
@@ -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 { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs';
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')
@@ -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 ?? process.cwd();
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) {