@polymorphism-tech/morph-spec 7.3.0 → 7.5.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 (61) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +22 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +10 -5
  8. package/framework/MORPH.md +31 -14
  9. package/framework/agents.json +132 -132
  10. package/framework/commands/create-umbraco-lp.md +35 -0
  11. package/framework/commands/migrate-lp-to-umbraco.md +33 -0
  12. package/framework/commands/morph-archive.md +17 -27
  13. package/framework/evals/rubrics/README.md +9 -8
  14. package/framework/hooks/README.md +2 -0
  15. package/framework/hooks/claude-code/core/enforce-phase-writes.js +115 -90
  16. package/framework/hooks/claude-code/core/gate-guard.js +160 -0
  17. package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
  18. package/framework/hooks/claude-code/core/state-sync.js +16 -36
  19. package/framework/hooks/claude-code/core/validate-completion.js +105 -0
  20. package/framework/hooks/shared/state-reader.js +302 -221
  21. package/framework/hooks/shared/state-writer.js +124 -0
  22. package/framework/rules/morph-task-tracking.md +69 -49
  23. package/framework/rules/morph-workflow.md +192 -191
  24. package/framework/skills/README.md +1 -1
  25. package/framework/skills/core/create-umbraco-lp/SKILL.md +252 -0
  26. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +452 -0
  27. package/framework/skills/core/morph-apply/SKILL.md +69 -27
  28. package/framework/skills/core/morph-brainstorming/SKILL.md +6 -2
  29. package/framework/skills/core/morph-eval/SKILL.md +4 -5
  30. package/framework/skills/core/morph-implement/SKILL.md +51 -22
  31. package/framework/skills/core/morph-plan/SKILL.md +10 -4
  32. package/framework/skills/core/morph-proposal/SKILL.md +8 -7
  33. package/framework/skills/core/morph-review/SKILL.md +15 -8
  34. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  35. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  36. package/framework/standards/ai-agents/setup.md +258 -244
  37. package/framework/templates/REGISTRY.json +1 -3
  38. package/framework/templates/context/CONTEXT-FEATURE.md +10 -17
  39. package/framework/templates/context/CONTEXT.md +181 -180
  40. package/framework/templates/docs/onboarding.md +122 -118
  41. package/framework/templates/docs/proposal.md +10 -0
  42. package/framework/templates/docs/spec.md +228 -228
  43. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -80
  44. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -89
  45. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +2 -1
  46. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +44 -43
  47. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -107
  48. package/package.json +1 -1
  49. package/src/commands/project/init.js +517 -515
  50. package/src/commands/project/update.js +415 -368
  51. package/src/commands/project/worktree.js +212 -210
  52. package/src/commands/state/advance.js +82 -0
  53. package/src/commands/state/approve.js +251 -213
  54. package/src/commands/state/archive.js +111 -0
  55. package/src/commands/validation/validate-feature.js +244 -79
  56. package/src/core/state/state-manager.js +256 -74
  57. package/src/dashboard/scanner/state-scanner.js +35 -8
  58. package/src/lib/generators/context-generator.js +12 -8
  59. package/src/lib/risk-detector.js +2 -2
  60. package/src/utils/file-copier.js +7 -0
  61. package/src/utils/hooks-installer.js +527 -516
@@ -1,515 +1,517 @@
1
- import { join, dirname } from 'path';
2
- import { fileURLToPath } from 'url';
3
- import { homedir } from 'os';
4
- import fs from 'fs-extra';
5
- import ora from 'ora';
6
- import { execSync } from 'child_process';
7
- import { logger } from '../../utils/logger.js';
8
- import {
9
- pathExists,
10
- readJson,
11
- writeJson,
12
- writeFile
13
- } from '../../utils/file-copier.js';
14
- import { getInstalledCLIVersion, checkProjectOutdated } from '../../utils/version-checker.js';
15
- import { installClaudeHooks } from '../../utils/claude-settings-manager.js';
16
- import inquirer from 'inquirer';
17
- import { detectProject } from '../../lib/detectors/index.js';
18
- import { detectClaudeConfig, mapMcpsToPhases, formatConfigSummary } from '../../lib/detectors/claude-config-detector.js';
19
- import { generateSettings, saveIntegrations } from '../../lib/generators/settings-generator.js';
20
- import {
21
- orchestrateMcpSetup,
22
- installAutoMcps,
23
- installCompanionSkills,
24
- installMcpWithCredentials,
25
- generateSetupInstructions,
26
- formatMcpStatusTable,
27
- isRemoteMcp,
28
- setupStitch
29
- } from '../../lib/installers/mcp-installer.js';
30
- import { setupInfra, installRulesForStack } from '../../scripts/setup-infra.js';
31
- import { installSkills } from '../../utils/skills-installer.js';
32
- import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
33
- import { getProjectTags } from '../../lib/stack-filter.js';
34
- import { updateCommand } from './update.js';
35
-
36
- export async function initCommand(options) {
37
- const targetPath = options.path || process.cwd();
38
-
39
- logger.header('MORPH-SPEC Framework Installation');
40
- logger.dim(`Target: ${targetPath}`);
41
- logger.blank();
42
-
43
- // Check if already initialized
44
- const morphPath = join(targetPath, '.morph');
45
- if (await pathExists(morphPath)) {
46
- if (!options.force) {
47
- // Check if project version is outdated — auto-update instead of blocking
48
- const projectCheck = await checkProjectOutdated(targetPath);
49
- if (projectCheck.isOutdated) {
50
- logger.info(`MORPH already initialized (v${projectCheck.current}). Framework update available (v${projectCheck.cliVersion}).`);
51
- logger.blank();
52
- return updateCommand(options);
53
- }
54
- logger.warn('MORPH already initialized in this directory (up to date).');
55
- logger.dim('Use --force to overwrite existing installation.');
56
- process.exit(1);
57
- }
58
- logger.warn('Overwriting existing MORPH installation...');
59
- }
60
-
61
- const spinner = ora('Installing MORPH-SPEC...').start();
62
-
63
- try {
64
-
65
- // Steps 1-10: Infrastructure (headless, no prompts)
66
- spinner.stop();
67
- const infraResult = await setupInfra(targetPath);
68
- spinner.start('Continuing...');
69
-
70
- // Derive display variables from what setupInfra installed
71
- const cliVersion = getInstalledCLIVersion();
72
- const commandsCopied = await pathExists(join(targetPath, '.claude', 'commands'));
73
- const rulesCopied = await pathExists(join(targetPath, '.claude', 'rules'));
74
- // Re-run installClaudeHooks to get the result object (idempotent — hooks already installed)
75
- const hooksResult = await installClaudeHooks(targetPath);
76
-
77
- // Re-derive config path for stack detection updates
78
- const configDir = join(morphPath, 'config');
79
- const configPath = join(configDir, 'config.json');
80
- const contextDir = join(morphPath, 'context');
81
-
82
- // Read config written by setupInfra so stack detection can update it
83
- let config = (await pathExists(configPath)) ? await readJson(configPath) : { project: {} };
84
- if (!config.project) config.project = {};
85
-
86
- // 11b. Detect Claude Code environment (plugins, MCPs, skills)
87
- spinner.text = 'Detecting Claude Code environment...';
88
- let claudeConfigDetected = null;
89
- let integrationsCreated = false;
90
- try {
91
- claudeConfigDetected = await detectClaudeConfig(targetPath);
92
- const hasIntegrations = claudeConfigDetected.plugins.length > 0
93
- || claudeConfigDetected.mcpServers.length > 0
94
- || claudeConfigDetected.superpowers.installed;
95
-
96
- if (hasIntegrations) {
97
- spinner.stop();
98
- logger.blank();
99
- logger.header('Claude Code Environment Detected');
100
-
101
- const summary = formatConfigSummary(claudeConfigDetected);
102
- if (summary) {
103
- logger.info(summary);
104
- }
105
-
106
- // Show MCP-to-phase mapping
107
- if (claudeConfigDetected.mcpServers.length > 0) {
108
- const phaseMap = mapMcpsToPhases(claudeConfigDetected.mcpServers);
109
- logger.blank();
110
- logger.dim('MCP usage by phase:');
111
- for (const [phase, mcps] of Object.entries(phaseMap)) {
112
- if (mcps.length > 0) {
113
- logger.dim(` ${phase}: ${mcps.map(m => m.name).join(', ')}`);
114
- }
115
- }
116
- }
117
-
118
- logger.blank();
119
- const { saveIntegrationsChoice } = await inquirer.prompt([{
120
- type: 'confirm',
121
- name: 'saveIntegrationsChoice',
122
- message: 'Save detected integrations and generate optimized settings?',
123
- default: true
124
- }]);
125
-
126
- if (saveIntegrationsChoice) {
127
- spinner.start('Saving integrations...');
128
- await saveIntegrations(targetPath, claudeConfigDetected);
129
- await generateSettings(targetPath, claudeConfigDetected, config);
130
- integrationsCreated = true;
131
- spinner.succeed('Integrations saved to .morph/config/integrations.json');
132
- } else {
133
- spinner.start('Continuing...');
134
- }
135
- }
136
- } catch (error) {
137
- // Non-fatal: continue without integrations
138
- logger.dim(` Claude config detection skipped: ${error.message}`);
139
- }
140
-
141
- // 11c. Stack detection (moved before MCP setup so we know the stack)
142
- spinner.text = 'Detecting project stack...';
143
- let detectedStack = 'unknown';
144
- const hasProjectFiles = await pathExists(join(targetPath, 'package.json'))
145
- || await pathExists(join(targetPath, 'src'))
146
- || (await fs.readdir(targetPath)).some(f => f.endsWith('.csproj'));
147
-
148
- if (hasProjectFiles) {
149
- try {
150
- const quickResults = await detectProject(targetPath, { conversation: false, generateStandards: false });
151
- const structure = quickResults.structure;
152
-
153
- if (structure?.stack && structure.stack !== 'unknown') {
154
- detectedStack = structure.stack;
155
-
156
- // Persist detected stack to config.json
157
- if (structure?.stack && structure.stack !== 'unknown') {
158
- config.project.stack = structure.stack;
159
- }
160
- if (structure?.architecture && structure.architecture !== 'unknown') {
161
- config.project.architecture = structure.architecture;
162
- }
163
- if (structure?.uiLibrary) {
164
- config.project.uiLibrary = structure.uiLibrary;
165
- }
166
- if (structure?.integrations?.length > 0) {
167
- config.project.integrations = structure.integrations;
168
- }
169
- await writeJson(configPath, config);
170
-
171
- spinner.stop();
172
- logger.blank();
173
- logger.header('Existing Project Detected');
174
- logger.info(`Stack detected: ${structure.stack}`);
175
- if (structure?.architecture) logger.dim(`Architecture: ${structure.architecture}`);
176
- if (structure?.uiLibrary) logger.dim(`UI Library: ${structure.uiLibrary}`);
177
- if (structure?.integrations?.length > 0) logger.dim(`Integrations: ${structure.integrations.join(', ')}`);
178
-
179
- const { standardsChoice } = await inquirer.prompt([{
180
- type: 'list',
181
- name: 'standardsChoice',
182
- message: 'How should morph-spec handle your project standards?',
183
- choices: [
184
- { name: 'Keep morph-spec defaults (recommended)', value: 'morph-spec' },
185
- { name: 'Detect project-specific standards', value: 'detect' }
186
- ]
187
- }]);
188
-
189
- if (standardsChoice === 'detect') {
190
- spinner.start('Generating project-specific standards...');
191
- const fullResults = await detectProject(targetPath, { conversation: false });
192
- const inferredPath = join(contextDir, 'standards.md');
193
- await writeFile(inferredPath, fullResults.inferred.markdown);
194
- spinner.succeed('Generated .morph/context/standards.md');
195
- logger.dim('Review and edit .morph/context/standards.md as needed.');
196
- }
197
-
198
- // Re-install stack-filtered assets now that we know the stack
199
- const stackTags = getProjectTags(config);
200
- if (stackTags.length > 0) {
201
- spinner.start('Re-installing stack-filtered assets...');
202
- const frameworkDir = join(targetPath, '.morph', 'framework');
203
- const fwSrcDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'framework');
204
-
205
- // Rules
206
- const rulesSrc = join(fwSrcDir, 'rules');
207
- const rulesDest = join(targetPath, '.claude', 'rules');
208
- installRulesForStack(rulesSrc, rulesDest, stackTags);
209
-
210
- // Skills
211
- await installSkills(targetPath, { projectTags: stackTags });
212
-
213
- // Agents (domain personas from agents.json)
214
- await installAgents(targetPath, fwSrcDir, { projectTags: stackTags });
215
- await installDomainAgents(targetPath, fwSrcDir, { projectTags: stackTags });
216
-
217
- spinner.succeed(`Stack-filtered assets reinstalled for [${stackTags.join(', ')}]`);
218
- }
219
-
220
- spinner.start('Continuing...');
221
- }
222
- } catch (error) {
223
- logger.dim(` Stack detection skipped: ${error.message}`);
224
- }
225
- }
226
-
227
- // 11d. MCP Auto-Setup
228
- let mcpSetupResult = null;
229
- if (!options.skipMcp) {
230
- try {
231
- spinner.stop();
232
- const existingMcps = claudeConfigDetected?.mcpServers || [];
233
- const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps);
234
-
235
- const autoNames = Object.keys(orchestration.autoInstallable);
236
- const manualNames = Object.keys(orchestration.needsManualSetup);
237
- const hasAutoMcps = autoNames.length > 0;
238
- const hasManualMcps = manualNames.length > 0;
239
-
240
- if (hasAutoMcps || hasManualMcps) {
241
- logger.blank();
242
- logger.header('MCP Server Setup');
243
-
244
- // Auto-install prompt
245
- let autoInstalled = {};
246
- if (hasAutoMcps) {
247
- const { installAuto } = await inquirer.prompt([{
248
- type: 'confirm',
249
- name: 'installAuto',
250
- message: `Install recommended MCP servers? (${autoNames.join(', ')})`,
251
- default: true
252
- }]);
253
-
254
- if (installAuto) {
255
- spinner.start('Installing MCP servers...');
256
- const result = await installAutoMcps(targetPath, orchestration.autoInstallable);
257
- spinner.succeed(`Installed ${result.added.length} MCP server(s)`);
258
- for (const name of result.added) {
259
- autoInstalled[name] = orchestration.autoInstallable[name];
260
- }
261
-
262
- // Install companion skills for MCPs that define them (e.g., Stitch agent skills)
263
- const skillResults = installCompanionSkills(orchestration.autoInstallable);
264
- for (const r of skillResults) {
265
- if (r.success) {
266
- logger.info(`✓ Companion skills installed: ${r.repo}`);
267
- } else {
268
- logger.warn(`Failed to install companion skills: ${r.repo}`);
269
- }
270
- }
271
- }
272
- }
273
-
274
- // Credential MCPs — inline setup or instructions
275
- const manualInstalled = {};
276
- if (hasManualMcps) {
277
- for (const [name, entry] of Object.entries(orchestration.needsManualSetup)) {
278
- logger.blank();
279
-
280
- // Show warnings
281
- for (const warning of entry.install.warnings || []) {
282
- logger.warn(` ${warning}`);
283
- }
284
-
285
- const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
286
- const { setupNow } = await inquirer.prompt([{
287
- type: 'confirm',
288
- name: 'setupNow',
289
- message: `Set up ${capitalizedName} MCP now? (${entry.usage})`,
290
- default: false
291
- }]);
292
-
293
- if (setupNow) {
294
- // Stitch — automated setup via setupStitch()
295
- if (name === 'stitch' && !entry.install.autoInstall) {
296
- spinner.start('Setting up Stitch MCP...');
297
- const stitchResult = await setupStitch(targetPath);
298
- if (stitchResult.success) {
299
- spinner.succeed('Stitch MCP setup complete');
300
- manualInstalled[name] = entry;
301
- } else {
302
- const failedStep = stitchResult.steps.find(s => !s.success && s.step !== 'enable-api' && s.step !== 'health-check');
303
- spinner.fail(`Stitch setup failed: ${failedStep?.error || 'unknown'}`);
304
- }
305
- if (entry.companionSkills?.installCommand) {
306
- const skillResults = installCompanionSkills({ stitch: entry });
307
- for (const r of skillResults) {
308
- if (r.success) logger.success(` Companion skills installed: ${r.repo}`);
309
- }
310
- }
311
- continue;
312
- }
313
-
314
- // Remote MCPs — launch OAuth via claude mcp add
315
- if (isRemoteMcp(entry.install.config)) {
316
- const url = entry.install.config.url;
317
- logger.info(` Launching ${capitalizedName} OAuth authentication...`);
318
- try {
319
- execSync(`claude mcp add --transport http ${name} ${url}`, {
320
- stdio: 'inherit',
321
- timeout: 120000
322
- });
323
- logger.success(` ${capitalizedName} MCP configured`);
324
- manualInstalled[name] = entry;
325
- } catch {
326
- logger.warn(` ${capitalizedName} setup cancelled. Run later: claude mcp add --transport http ${name} ${url}`);
327
- }
328
- continue;
329
- }
330
-
331
- // Collect credentials
332
- const credentialValues = {};
333
- for (const cred of entry.install.credentials) {
334
- const { value } = await inquirer.prompt([{
335
- type: cred.secret ? 'password' : 'input',
336
- name: 'value',
337
- message: `${cred.name}:`,
338
- mask: cred.secret ? '*' : undefined
339
- }]);
340
- credentialValues[cred.envVar] = value;
341
- }
342
-
343
- spinner.start(`Configuring ${name}...`);
344
- await installMcpWithCredentials(targetPath, name, entry, credentialValues);
345
- spinner.succeed(`${capitalizedName} MCP configured`);
346
- manualInstalled[name] = entry;
347
- } else {
348
- // Show setup instructions
349
- const instructions = generateSetupInstructions(name, entry);
350
- logger.blank();
351
- logger.dim(' Add to .mcp.json → mcpServers:');
352
- logger.dim(' ' + '─'.repeat(55));
353
- for (const line of instructions.configSnippet.split('\n')) {
354
- logger.dim(` ${line}`);
355
- }
356
- logger.dim(' ' + '─'.repeat(55));
357
-
358
- for (const cred of instructions.credentialUrls) {
359
- logger.dim(` Get ${cred.name}: ${cred.helpUrl}`);
360
- }
361
- logger.dim(` Or run later: ${instructions.cliCommand}`);
362
- }
363
- }
364
- }
365
-
366
- // Summary table
367
- const justInstalled = { ...autoInstalled, ...manualInstalled };
368
- const statusRows = formatMcpStatusTable(orchestration, justInstalled);
369
-
370
- if (statusRows.length > 0) {
371
- logger.blank();
372
- logger.info('MCP Servers');
373
- logger.dim('─'.repeat(60));
374
-
375
- const STATUS_ICONS = {
376
- installed: '✓',
377
- available: '○',
378
- already_configured: '✓',
379
- needs_setup: '⚠',
380
- prereq_missing: '✗',
381
- not_relevant: '─'
382
- };
383
-
384
- for (const row of statusRows) {
385
- const icon = STATUS_ICONS[row.status] || '?';
386
- const statusLabel = row.status.replace(/_/g, ' ');
387
- logger.dim(` ${icon} ${row.name.padEnd(15)} ${statusLabel.padEnd(20)} ${row.detail}`);
388
- }
389
- }
390
-
391
- mcpSetupResult = { autoInstalled, manualInstalled, orchestration };
392
- }
393
- } catch (error) {
394
- logger.dim(` MCP setup skipped: ${error.message}`);
395
- }
396
- } else {
397
- logger.dim('\nSkipped MCP setup (--skip-mcp flag)');
398
- }
399
-
400
- // Auto-generate architecture context (non-blocking on failure)
401
- spinner.text = 'Generating architecture context...';
402
- try {
403
- const { scanArchitecture, renderArchitectureContext } = await import('../../lib/generators/architecture-scanner.js');
404
- const scanResult = await scanArchitecture(targetPath);
405
- await renderArchitectureContext(targetPath, scanResult);
406
- spinner.succeed('Architecture context generated');
407
- } catch {
408
- // Non-fatal: placeholder README.md remains
409
- spinner.warn('Architecture context generation skipped');
410
- }
411
-
412
- spinner.succeed('MORPH-SPEC installed successfully!');
413
-
414
- // Show next steps
415
- logger.blank();
416
- logger.success(`MORPH-SPEC v${cliVersion} installed successfully!`);
417
- logger.blank();
418
- logger.header('Next Steps');
419
-
420
- logger.step(1, 'Review .morph/config/config.json');
421
- logger.step(2, 'Open project in VS Code with Claude Code');
422
- logger.step(3, 'Start your first feature:');
423
- logger.blank();
424
- logger.box([
425
- 'Ask Claude Code to implement a feature'
426
- ]);
427
- logger.blank();
428
-
429
- logger.info('Files installed:');
430
- logger.dim(` ✓ CLAUDE.md`);
431
- logger.dim(` ✓ .morph/config/ (config.json)`);
432
- logger.dim(` ✓ .morph/framework/ (agents.json, standards/, templates/, hooks/)`);
433
- logger.dim(` ✓ .morph/context/ (project context)`);
434
- logger.dim(` ✓ .morph/features/ (feature outputs)`);
435
- if (commandsCopied) {
436
- logger.dim(` ✓ .claude/commands/ (slash commands)`);
437
- }
438
- if (rulesCopied) {
439
- logger.dim(` ✓ .claude/rules/ (5 path-scoped rules)`);
440
- }
441
- logger.dim(` ✓ .claude/settings.local.json (${hooksResult.installed} hooks installed)`);
442
- if (integrationsCreated) {
443
- logger.dim(` ✓ .morph/config/integrations.json (detected plugins & MCPs)`);
444
- }
445
- if (mcpSetupResult) {
446
- const autoCount = Object.keys(mcpSetupResult.autoInstalled).length;
447
- const manualCount = Object.keys(mcpSetupResult.manualInstalled).length;
448
- if (autoCount + manualCount > 0) {
449
- logger.dim(` ✓ MCP servers (${autoCount + manualCount} configured)`);
450
- }
451
- }
452
-
453
- logger.dim(' ✓ .claude/skills/ (installed as skill directories)');
454
-
455
- // Show agent team breakdown from setup-infra result
456
- const agents = infraResult?.agents;
457
- if (agents) {
458
- const total = (agents.tier1 || 0) + (agents.tier2 || 0) + (agents.specialists || 0) + (agents.personas || 0);
459
- logger.dim(` ✓ .claude/agents/ (${total} domain personas installed)`);
460
- } else {
461
- logger.dim(' ✓ .claude/agents/ (native subagents installed)');
462
- }
463
-
464
- logger.dim(' ✓ ~/.claude/statusline.sh (global statusline installed)');
465
-
466
- // Check CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS in ~/.claude/settings.local.json
467
- logger.blank();
468
- const globalSettingsPath = join(homedir(), '.claude', 'settings.local.json');
469
- let agentTeamsEnabled = false;
470
- try {
471
- const globalSettings = await readJson(globalSettingsPath);
472
- agentTeamsEnabled = globalSettings?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === '1';
473
- } catch {
474
- // File missing or unreadable — treat as not enabled
475
- }
476
-
477
- if (agentTeamsEnabled) {
478
- logger.success('Agent Teams: CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS enabled ✓');
479
- } else {
480
- logger.warn('Agent Teams not enabled. To activate multi-agent workflows, add to ~/.claude/settings.local.json:');
481
- logger.dim(' {"env": {"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"}}');
482
- }
483
-
484
- logger.blank();
485
-
486
- // Populate Claude Code persistent memory so future sessions enter contextualized
487
- try {
488
- const { installProjectMemory } = await import('../../lib/installers/memory-installer.js');
489
- const memResult = await installProjectMemory(targetPath, {
490
- projectName: config.project?.name ?? targetPath.split(/[\\/]/).pop(),
491
- frameworkVersion: config.frameworkVersion ?? cliVersion,
492
- stack: config.project?.stack,
493
- });
494
- logger.dim(` ✓ Claude Code memory populated (${memResult.projectMemoryPath})`);
495
- } catch (err) {
496
- logger.dim(` ⚠ Memory install skipped: ${err.message}`);
497
- }
498
-
499
- // Suggest tutorial for first-time users
500
- if (integrationsCreated) {
501
- logger.blank();
502
- logger.info('First time using morph-spec? Run `/morph-proposal <feature>` to start a new feature.');
503
- }
504
-
505
- logger.blank();
506
- logger.warn('IMPORTANT: Restart your Claude Code session for morph-* skills to be available.');
507
- logger.dim('Skills installed in .claude/skills/ are only registered at session start.');
508
- logger.blank();
509
-
510
- } catch (error) {
511
- spinner.fail('Installation failed');
512
- logger.error(error.message);
513
- process.exit(1);
514
- }
515
- }
1
+ import { join, dirname } from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { homedir } from 'os';
4
+ import fs from 'fs-extra';
5
+ import ora from 'ora';
6
+ import { execSync } from 'child_process';
7
+ import { logger } from '../../utils/logger.js';
8
+ import {
9
+ pathExists,
10
+ readJson,
11
+ writeJson,
12
+ writeFile
13
+ } from '../../utils/file-copier.js';
14
+ import { getInstalledCLIVersion, checkProjectOutdated } from '../../utils/version-checker.js';
15
+ import { installClaudeHooks } from '../../utils/claude-settings-manager.js';
16
+ import inquirer from 'inquirer';
17
+ import { detectProject } from '../../lib/detectors/index.js';
18
+ import { detectClaudeConfig, mapMcpsToPhases, formatConfigSummary } from '../../lib/detectors/claude-config-detector.js';
19
+ import { generateSettings, saveIntegrations } from '../../lib/generators/settings-generator.js';
20
+ import {
21
+ orchestrateMcpSetup,
22
+ installAutoMcps,
23
+ installCompanionSkills,
24
+ installMcpWithCredentials,
25
+ generateSetupInstructions,
26
+ formatMcpStatusTable,
27
+ isRemoteMcp,
28
+ setupStitch
29
+ } from '../../lib/installers/mcp-installer.js';
30
+ import { setupInfra, installRulesForStack } from '../../scripts/setup-infra.js';
31
+ import { installSkills } from '../../utils/skills-installer.js';
32
+ import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
33
+ import { getProjectTags } from '../../lib/stack-filter.js';
34
+ import { updateCommand } from './update.js';
35
+
36
+ export async function initCommand(options) {
37
+ const targetPath = options.path || process.cwd();
38
+
39
+ logger.header('MORPH-SPEC Framework Installation');
40
+ logger.dim(`Target: ${targetPath}`);
41
+ logger.blank();
42
+
43
+ // Check if already initialized
44
+ const morphPath = join(targetPath, '.morph');
45
+ if (await pathExists(morphPath)) {
46
+ if (!options.force) {
47
+ // Check if project version is outdated — auto-update instead of blocking
48
+ const projectCheck = await checkProjectOutdated(targetPath);
49
+ if (projectCheck.isOutdated) {
50
+ logger.info(`MORPH already initialized (v${projectCheck.current}). Framework update available (v${projectCheck.cliVersion}).`);
51
+ logger.blank();
52
+ return updateCommand(options);
53
+ }
54
+ logger.warn('MORPH already initialized in this directory (up to date).');
55
+ logger.dim('Use --force to overwrite existing installation.');
56
+ process.exit(1);
57
+ }
58
+ logger.warn('Overwriting existing MORPH installation...');
59
+ }
60
+
61
+ const spinner = ora('Installing MORPH-SPEC...').start();
62
+
63
+ try {
64
+
65
+ // Steps 1-10: Infrastructure (headless, no prompts)
66
+ spinner.stop();
67
+ const infraResult = await setupInfra(targetPath);
68
+ spinner.start('Continuing...');
69
+
70
+ // Derive display variables from what setupInfra installed
71
+ const cliVersion = getInstalledCLIVersion();
72
+ const commandsCopied = await pathExists(join(targetPath, '.claude', 'commands'));
73
+ const rulesCopied = await pathExists(join(targetPath, '.claude', 'rules'));
74
+ // Re-run installClaudeHooks to get the result object (idempotent — hooks already installed)
75
+ const hooksResult = await installClaudeHooks(targetPath);
76
+
77
+ // Re-derive config path for stack detection updates
78
+ const configDir = join(morphPath, 'config');
79
+ const configPath = join(configDir, 'config.json');
80
+ const contextDir = join(morphPath, 'context');
81
+
82
+ // Read config written by setupInfra so stack detection can update it
83
+ let config = (await pathExists(configPath)) ? await readJson(configPath) : { project: {} };
84
+ if (!config.project) config.project = {};
85
+
86
+ // 11b. Detect Claude Code environment (plugins, MCPs, skills)
87
+ spinner.text = 'Detecting Claude Code environment...';
88
+ let claudeConfigDetected = null;
89
+ let integrationsCreated = false;
90
+ try {
91
+ claudeConfigDetected = await detectClaudeConfig(targetPath);
92
+ const hasIntegrations = claudeConfigDetected.plugins.length > 0
93
+ || claudeConfigDetected.mcpServers.length > 0
94
+ || claudeConfigDetected.superpowers.installed;
95
+
96
+ if (hasIntegrations) {
97
+ spinner.stop();
98
+ logger.blank();
99
+ logger.header('Claude Code Environment Detected');
100
+
101
+ const summary = formatConfigSummary(claudeConfigDetected);
102
+ if (summary) {
103
+ logger.info(summary);
104
+ }
105
+
106
+ // Show MCP-to-phase mapping
107
+ if (claudeConfigDetected.mcpServers.length > 0) {
108
+ const phaseMap = mapMcpsToPhases(claudeConfigDetected.mcpServers);
109
+ logger.blank();
110
+ logger.dim('MCP usage by phase:');
111
+ for (const [phase, mcps] of Object.entries(phaseMap)) {
112
+ if (mcps.length > 0) {
113
+ logger.dim(` ${phase}: ${mcps.map(m => m.name).join(', ')}`);
114
+ }
115
+ }
116
+ }
117
+
118
+ logger.blank();
119
+ const saveIntegrationsChoice = process.stdin.isTTY
120
+ ? (await inquirer.prompt([{
121
+ type: 'confirm',
122
+ name: 'saveIntegrationsChoice',
123
+ message: 'Save detected integrations and generate optimized settings?',
124
+ default: true
125
+ }])).saveIntegrationsChoice
126
+ : true;
127
+
128
+ if (saveIntegrationsChoice) {
129
+ spinner.start('Saving integrations...');
130
+ await saveIntegrations(targetPath, claudeConfigDetected);
131
+ await generateSettings(targetPath, claudeConfigDetected, config);
132
+ integrationsCreated = true;
133
+ spinner.succeed('Integrations saved to .morph/config/integrations.json');
134
+ } else {
135
+ spinner.start('Continuing...');
136
+ }
137
+ }
138
+ } catch (error) {
139
+ // Non-fatal: continue without integrations
140
+ logger.dim(` Claude config detection skipped: ${error.message}`);
141
+ }
142
+
143
+ // 11c. Stack detection (moved before MCP setup so we know the stack)
144
+ spinner.text = 'Detecting project stack...';
145
+ let detectedStack = 'unknown';
146
+ const hasProjectFiles = await pathExists(join(targetPath, 'package.json'))
147
+ || await pathExists(join(targetPath, 'src'))
148
+ || (await fs.readdir(targetPath)).some(f => f.endsWith('.csproj'));
149
+
150
+ if (hasProjectFiles) {
151
+ try {
152
+ const quickResults = await detectProject(targetPath, { conversation: false, generateStandards: false });
153
+ const structure = quickResults.structure;
154
+
155
+ if (structure?.stack && structure.stack !== 'unknown') {
156
+ detectedStack = structure.stack;
157
+
158
+ // Persist detected stack to config.json
159
+ if (structure?.stack && structure.stack !== 'unknown') {
160
+ config.project.stack = structure.stack;
161
+ }
162
+ if (structure?.architecture && structure.architecture !== 'unknown') {
163
+ config.project.architecture = structure.architecture;
164
+ }
165
+ if (structure?.uiLibrary) {
166
+ config.project.uiLibrary = structure.uiLibrary;
167
+ }
168
+ if (structure?.integrations?.length > 0) {
169
+ config.project.integrations = structure.integrations;
170
+ }
171
+ await writeJson(configPath, config);
172
+
173
+ spinner.stop();
174
+ logger.blank();
175
+ logger.header('Existing Project Detected');
176
+ logger.info(`Stack detected: ${structure.stack}`);
177
+ if (structure?.architecture) logger.dim(`Architecture: ${structure.architecture}`);
178
+ if (structure?.uiLibrary) logger.dim(`UI Library: ${structure.uiLibrary}`);
179
+ if (structure?.integrations?.length > 0) logger.dim(`Integrations: ${structure.integrations.join(', ')}`);
180
+
181
+ const { standardsChoice } = await inquirer.prompt([{
182
+ type: 'list',
183
+ name: 'standardsChoice',
184
+ message: 'How should morph-spec handle your project standards?',
185
+ choices: [
186
+ { name: 'Keep morph-spec defaults (recommended)', value: 'morph-spec' },
187
+ { name: 'Detect project-specific standards', value: 'detect' }
188
+ ]
189
+ }]);
190
+
191
+ if (standardsChoice === 'detect') {
192
+ spinner.start('Generating project-specific standards...');
193
+ const fullResults = await detectProject(targetPath, { conversation: false });
194
+ const inferredPath = join(contextDir, 'standards.md');
195
+ await writeFile(inferredPath, fullResults.inferred.markdown);
196
+ spinner.succeed('Generated .morph/context/standards.md');
197
+ logger.dim('Review and edit .morph/context/standards.md as needed.');
198
+ }
199
+
200
+ // Re-install stack-filtered assets now that we know the stack
201
+ const stackTags = getProjectTags(config);
202
+ if (stackTags.length > 0) {
203
+ spinner.start('Re-installing stack-filtered assets...');
204
+ const frameworkDir = join(targetPath, '.morph', 'framework');
205
+ const fwSrcDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'framework');
206
+
207
+ // Rules
208
+ const rulesSrc = join(fwSrcDir, 'rules');
209
+ const rulesDest = join(targetPath, '.claude', 'rules');
210
+ installRulesForStack(rulesSrc, rulesDest, stackTags);
211
+
212
+ // Skills
213
+ await installSkills(targetPath, { projectTags: stackTags });
214
+
215
+ // Agents (domain personas from agents.json)
216
+ await installAgents(targetPath, fwSrcDir, { projectTags: stackTags });
217
+ await installDomainAgents(targetPath, fwSrcDir, { projectTags: stackTags });
218
+
219
+ spinner.succeed(`Stack-filtered assets reinstalled for [${stackTags.join(', ')}]`);
220
+ }
221
+
222
+ spinner.start('Continuing...');
223
+ }
224
+ } catch (error) {
225
+ logger.dim(` Stack detection skipped: ${error.message}`);
226
+ }
227
+ }
228
+
229
+ // 11d. MCP Auto-Setup
230
+ let mcpSetupResult = null;
231
+ if (!options.skipMcp) {
232
+ try {
233
+ spinner.stop();
234
+ const existingMcps = claudeConfigDetected?.mcpServers || [];
235
+ const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps);
236
+
237
+ const autoNames = Object.keys(orchestration.autoInstallable);
238
+ const manualNames = Object.keys(orchestration.needsManualSetup);
239
+ const hasAutoMcps = autoNames.length > 0;
240
+ const hasManualMcps = manualNames.length > 0;
241
+
242
+ if (hasAutoMcps || hasManualMcps) {
243
+ logger.blank();
244
+ logger.header('MCP Server Setup');
245
+
246
+ // Auto-install prompt
247
+ let autoInstalled = {};
248
+ if (hasAutoMcps) {
249
+ const { installAuto } = await inquirer.prompt([{
250
+ type: 'confirm',
251
+ name: 'installAuto',
252
+ message: `Install recommended MCP servers? (${autoNames.join(', ')})`,
253
+ default: true
254
+ }]);
255
+
256
+ if (installAuto) {
257
+ spinner.start('Installing MCP servers...');
258
+ const result = await installAutoMcps(targetPath, orchestration.autoInstallable);
259
+ spinner.succeed(`Installed ${result.added.length} MCP server(s)`);
260
+ for (const name of result.added) {
261
+ autoInstalled[name] = orchestration.autoInstallable[name];
262
+ }
263
+
264
+ // Install companion skills for MCPs that define them (e.g., Stitch agent skills)
265
+ const skillResults = installCompanionSkills(orchestration.autoInstallable);
266
+ for (const r of skillResults) {
267
+ if (r.success) {
268
+ logger.info(`✓ Companion skills installed: ${r.repo}`);
269
+ } else {
270
+ logger.warn(`Failed to install companion skills: ${r.repo}`);
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ // Credential MCPs — inline setup or instructions
277
+ const manualInstalled = {};
278
+ if (hasManualMcps) {
279
+ for (const [name, entry] of Object.entries(orchestration.needsManualSetup)) {
280
+ logger.blank();
281
+
282
+ // Show warnings
283
+ for (const warning of entry.install.warnings || []) {
284
+ logger.warn(` ${warning}`);
285
+ }
286
+
287
+ const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
288
+ const { setupNow } = await inquirer.prompt([{
289
+ type: 'confirm',
290
+ name: 'setupNow',
291
+ message: `Set up ${capitalizedName} MCP now? (${entry.usage})`,
292
+ default: false
293
+ }]);
294
+
295
+ if (setupNow) {
296
+ // Stitch — automated setup via setupStitch()
297
+ if (name === 'stitch' && !entry.install.autoInstall) {
298
+ spinner.start('Setting up Stitch MCP...');
299
+ const stitchResult = await setupStitch(targetPath);
300
+ if (stitchResult.success) {
301
+ spinner.succeed('Stitch MCP setup complete');
302
+ manualInstalled[name] = entry;
303
+ } else {
304
+ const failedStep = stitchResult.steps.find(s => !s.success && s.step !== 'enable-api' && s.step !== 'health-check');
305
+ spinner.fail(`Stitch setup failed: ${failedStep?.error || 'unknown'}`);
306
+ }
307
+ if (entry.companionSkills?.installCommand) {
308
+ const skillResults = installCompanionSkills({ stitch: entry });
309
+ for (const r of skillResults) {
310
+ if (r.success) logger.success(` Companion skills installed: ${r.repo}`);
311
+ }
312
+ }
313
+ continue;
314
+ }
315
+
316
+ // Remote MCPs — launch OAuth via claude mcp add
317
+ if (isRemoteMcp(entry.install.config)) {
318
+ const url = entry.install.config.url;
319
+ logger.info(` Launching ${capitalizedName} OAuth authentication...`);
320
+ try {
321
+ execSync(`claude mcp add --transport http ${name} ${url}`, {
322
+ stdio: 'inherit',
323
+ timeout: 120000
324
+ });
325
+ logger.success(` ${capitalizedName} MCP configured`);
326
+ manualInstalled[name] = entry;
327
+ } catch {
328
+ logger.warn(` ${capitalizedName} setup cancelled. Run later: claude mcp add --transport http ${name} ${url}`);
329
+ }
330
+ continue;
331
+ }
332
+
333
+ // Collect credentials
334
+ const credentialValues = {};
335
+ for (const cred of entry.install.credentials) {
336
+ const { value } = await inquirer.prompt([{
337
+ type: cred.secret ? 'password' : 'input',
338
+ name: 'value',
339
+ message: `${cred.name}:`,
340
+ mask: cred.secret ? '*' : undefined
341
+ }]);
342
+ credentialValues[cred.envVar] = value;
343
+ }
344
+
345
+ spinner.start(`Configuring ${name}...`);
346
+ await installMcpWithCredentials(targetPath, name, entry, credentialValues);
347
+ spinner.succeed(`${capitalizedName} MCP configured`);
348
+ manualInstalled[name] = entry;
349
+ } else {
350
+ // Show setup instructions
351
+ const instructions = generateSetupInstructions(name, entry);
352
+ logger.blank();
353
+ logger.dim(' Add to .mcp.json → mcpServers:');
354
+ logger.dim(' ' + '─'.repeat(55));
355
+ for (const line of instructions.configSnippet.split('\n')) {
356
+ logger.dim(` ${line}`);
357
+ }
358
+ logger.dim(' ' + '─'.repeat(55));
359
+
360
+ for (const cred of instructions.credentialUrls) {
361
+ logger.dim(` Get ${cred.name}: ${cred.helpUrl}`);
362
+ }
363
+ logger.dim(` Or run later: ${instructions.cliCommand}`);
364
+ }
365
+ }
366
+ }
367
+
368
+ // Summary table
369
+ const justInstalled = { ...autoInstalled, ...manualInstalled };
370
+ const statusRows = formatMcpStatusTable(orchestration, justInstalled);
371
+
372
+ if (statusRows.length > 0) {
373
+ logger.blank();
374
+ logger.info('MCP Servers');
375
+ logger.dim('─'.repeat(60));
376
+
377
+ const STATUS_ICONS = {
378
+ installed: '✓',
379
+ available: '○',
380
+ already_configured: '✓',
381
+ needs_setup: '⚠',
382
+ prereq_missing: '✗',
383
+ not_relevant: '─'
384
+ };
385
+
386
+ for (const row of statusRows) {
387
+ const icon = STATUS_ICONS[row.status] || '?';
388
+ const statusLabel = row.status.replace(/_/g, ' ');
389
+ logger.dim(` ${icon} ${row.name.padEnd(15)} ${statusLabel.padEnd(20)} ${row.detail}`);
390
+ }
391
+ }
392
+
393
+ mcpSetupResult = { autoInstalled, manualInstalled, orchestration };
394
+ }
395
+ } catch (error) {
396
+ logger.dim(` MCP setup skipped: ${error.message}`);
397
+ }
398
+ } else {
399
+ logger.dim('\nSkipped MCP setup (--skip-mcp flag)');
400
+ }
401
+
402
+ // Auto-generate architecture context (non-blocking on failure)
403
+ spinner.text = 'Generating architecture context...';
404
+ try {
405
+ const { scanArchitecture, renderArchitectureContext } = await import('../../lib/generators/architecture-scanner.js');
406
+ const scanResult = await scanArchitecture(targetPath);
407
+ await renderArchitectureContext(targetPath, scanResult);
408
+ spinner.succeed('Architecture context generated');
409
+ } catch {
410
+ // Non-fatal: placeholder README.md remains
411
+ spinner.warn('Architecture context generation skipped');
412
+ }
413
+
414
+ spinner.succeed('MORPH-SPEC installed successfully!');
415
+
416
+ // Show next steps
417
+ logger.blank();
418
+ logger.success(`MORPH-SPEC v${cliVersion} installed successfully!`);
419
+ logger.blank();
420
+ logger.header('Next Steps');
421
+
422
+ logger.step(1, 'Review .morph/config/config.json');
423
+ logger.step(2, 'Open project in VS Code with Claude Code');
424
+ logger.step(3, 'Start your first feature:');
425
+ logger.blank();
426
+ logger.box([
427
+ 'Ask Claude Code to implement a feature'
428
+ ]);
429
+ logger.blank();
430
+
431
+ logger.info('Files installed:');
432
+ logger.dim(` ✓ CLAUDE.md`);
433
+ logger.dim(` ✓ .morph/config/ (config.json)`);
434
+ logger.dim(` ✓ .morph/framework/ (agents.json, standards/, templates/, hooks/)`);
435
+ logger.dim(` ✓ .morph/context/ (project context)`);
436
+ logger.dim(` ✓ .morph/features/ (feature outputs)`);
437
+ if (commandsCopied) {
438
+ logger.dim(` ✓ .claude/commands/ (slash commands)`);
439
+ }
440
+ if (rulesCopied) {
441
+ logger.dim(` ✓ .claude/rules/ (5 path-scoped rules)`);
442
+ }
443
+ logger.dim(` ✓ .claude/settings.local.json (${hooksResult.installed} hooks installed)`);
444
+ if (integrationsCreated) {
445
+ logger.dim(` ✓ .morph/config/integrations.json (detected plugins & MCPs)`);
446
+ }
447
+ if (mcpSetupResult) {
448
+ const autoCount = Object.keys(mcpSetupResult.autoInstalled).length;
449
+ const manualCount = Object.keys(mcpSetupResult.manualInstalled).length;
450
+ if (autoCount + manualCount > 0) {
451
+ logger.dim(` ✓ MCP servers (${autoCount + manualCount} configured)`);
452
+ }
453
+ }
454
+
455
+ logger.dim(' ✓ .claude/skills/ (installed as skill directories)');
456
+
457
+ // Show agent team breakdown from setup-infra result
458
+ const agents = infraResult?.agents;
459
+ if (agents) {
460
+ const total = (agents.tier1 || 0) + (agents.tier2 || 0) + (agents.specialists || 0) + (agents.personas || 0);
461
+ logger.dim(` ✓ .claude/agents/ (${total} domain personas installed)`);
462
+ } else {
463
+ logger.dim(' ✓ .claude/agents/ (native subagents installed)');
464
+ }
465
+
466
+ logger.dim(' ✓ ~/.claude/statusline.sh (global statusline installed)');
467
+
468
+ // Check CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS in ~/.claude/settings.local.json
469
+ logger.blank();
470
+ const globalSettingsPath = join(homedir(), '.claude', 'settings.local.json');
471
+ let agentTeamsEnabled = false;
472
+ try {
473
+ const globalSettings = await readJson(globalSettingsPath);
474
+ agentTeamsEnabled = globalSettings?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === '1';
475
+ } catch {
476
+ // File missing or unreadable — treat as not enabled
477
+ }
478
+
479
+ if (agentTeamsEnabled) {
480
+ logger.success('Agent Teams: CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS enabled ✓');
481
+ } else {
482
+ logger.warn('Agent Teams not enabled. To activate multi-agent workflows, add to ~/.claude/settings.local.json:');
483
+ logger.dim(' {"env": {"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"}}');
484
+ }
485
+
486
+ logger.blank();
487
+
488
+ // Populate Claude Code persistent memory so future sessions enter contextualized
489
+ try {
490
+ const { installProjectMemory } = await import('../../lib/installers/memory-installer.js');
491
+ const memResult = await installProjectMemory(targetPath, {
492
+ projectName: config.project?.name ?? targetPath.split(/[\\/]/).pop(),
493
+ frameworkVersion: config.frameworkVersion ?? cliVersion,
494
+ stack: config.project?.stack,
495
+ });
496
+ logger.dim(` ✓ Claude Code memory populated (${memResult.projectMemoryPath})`);
497
+ } catch (err) {
498
+ logger.dim(` ⚠ Memory install skipped: ${err.message}`);
499
+ }
500
+
501
+ // Suggest tutorial for first-time users
502
+ if (integrationsCreated) {
503
+ logger.blank();
504
+ logger.info('First time using morph-spec? Run `/morph-proposal <feature>` to start a new feature.');
505
+ }
506
+
507
+ logger.blank();
508
+ logger.warn('IMPORTANT: Restart your Claude Code session for morph-* skills to be available.');
509
+ logger.dim('Skills installed in .claude/skills/ are only registered at session start.');
510
+ logger.blank();
511
+
512
+ } catch (error) {
513
+ spinner.fail('Installation failed');
514
+ logger.error(error.message);
515
+ process.exit(1);
516
+ }
517
+ }