@polymorphism-tech/morph-spec 4.8.18 → 4.9.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 (138) hide show
  1. package/CLAUDE.md +98 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +15 -56
  4. package/bin/task-manager.js +115 -14
  5. package/bin/validate.js +67 -33
  6. package/claude-plugin.json +1 -1
  7. package/docs/CHEATSHEET.md +201 -203
  8. package/docs/QUICKSTART.md +2 -2
  9. package/framework/CLAUDE.md +21 -0
  10. package/framework/agents.json +758 -164
  11. package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
  12. package/framework/hooks/claude-code/post-tool-use/dispatch.js +2 -2
  13. package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +155 -0
  14. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +1 -1
  15. package/framework/hooks/claude-code/session-start/inject-morph-context.js +71 -2
  16. package/framework/hooks/claude-code/statusline.py +76 -30
  17. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +14 -6
  18. package/framework/hooks/shared/activity-logger.js +0 -24
  19. package/framework/hooks/shared/phase-utils.js +3 -0
  20. package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
  21. package/framework/hooks/shared/stale-task-reset.js +57 -0
  22. package/framework/hooks/shared/state-reader.js +2 -2
  23. package/framework/hooks/shared/worktree-helpers.js +53 -0
  24. package/framework/phases.json +40 -8
  25. package/framework/skills/level-0-meta/brainstorming/SKILL.md +1 -1
  26. package/framework/skills/level-0-meta/code-review/SKILL.md +1 -1
  27. package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +163 -163
  28. package/framework/skills/level-0-meta/frontend-review/SKILL.md +5 -5
  29. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
  30. package/framework/skills/level-0-meta/morph-init/SKILL.md +5 -5
  31. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +4 -4
  32. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
  33. package/framework/skills/level-0-meta/post-implementation/SKILL.md +59 -12
  34. package/framework/skills/level-0-meta/simulation-checklist/SKILL.md +1 -1
  35. package/framework/skills/level-0-meta/terminal-title/SKILL.md +1 -1
  36. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +1 -1
  37. package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +6 -5
  38. package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +1 -1
  39. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +215 -189
  40. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +251 -251
  41. package/framework/skills/level-1-workflows/phase-design/SKILL.md +382 -365
  42. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +492 -450
  43. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +194 -190
  44. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +270 -270
  45. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +285 -285
  46. package/framework/standards/STANDARDS.json +640 -88
  47. package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
  48. package/framework/templates/REGISTRY.json +1825 -1909
  49. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  50. package/framework/templates/docs/onboarding.md +1 -5
  51. package/framework/workflows/configs/nodejs-cli.json +40 -0
  52. package/package.json +2 -6
  53. package/src/commands/agents/dispatch-agents.js +55 -4
  54. package/src/commands/project/doctor.js +16 -47
  55. package/src/commands/project/init.js +1 -1
  56. package/src/commands/project/status.js +2 -2
  57. package/src/commands/project/update.js +381 -365
  58. package/src/commands/project/worktree.js +154 -0
  59. package/src/commands/state/advance-phase.js +120 -30
  60. package/src/commands/state/approve.js +2 -2
  61. package/src/commands/state/index.js +7 -8
  62. package/src/commands/state/phase-runner.js +1 -1
  63. package/src/commands/state/state.js +61 -6
  64. package/src/commands/tasks/task.js +78 -99
  65. package/src/commands/templates/template-render.js +93 -173
  66. package/src/commands/trust/trust.js +26 -21
  67. package/src/core/paths/output-schema.js +15 -0
  68. package/src/core/state/state-manager.js +28 -54
  69. package/src/core/workflows/workflow-detector.js +9 -87
  70. package/src/lib/phase-chain/phase-validator.js +330 -0
  71. package/src/lib/stack/stack-profile.js +88 -0
  72. package/src/lib/tasks/task-classifier.js +16 -0
  73. package/src/lib/tasks/test-runner.js +77 -0
  74. package/src/lib/trust/trust-manager.js +32 -144
  75. package/src/lib/validators/spec-validator.js +58 -4
  76. package/src/lib/validators/validation-runner.js +23 -11
  77. package/src/scripts/setup-infra.js +240 -224
  78. package/src/utils/agents-installer.js +2 -2
  79. package/src/utils/banner.js +1 -1
  80. package/src/utils/claude-settings-manager.js +1 -1
  81. package/src/utils/file-copier.js +1 -0
  82. package/src/utils/hooks-installer.js +258 -8
  83. package/framework/hooks/dev/check-sync-health.js +0 -117
  84. package/framework/hooks/dev/guard-version-numbers.js +0 -57
  85. package/framework/hooks/dev/sync-standards-registry.js +0 -60
  86. package/framework/hooks/dev/sync-template-registry.js +0 -60
  87. package/framework/hooks/dev/validate-skill-format.js +0 -70
  88. package/framework/hooks/dev/validate-standard-format.js +0 -73
  89. package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
  90. package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
  91. package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
  92. package/framework/workflows/configs/design-impl.json +0 -49
  93. package/framework/workflows/configs/express.json +0 -45
  94. package/framework/workflows/configs/fast-track.json +0 -42
  95. package/framework/workflows/configs/full-morph.json +0 -79
  96. package/framework/workflows/configs/fusion.json +0 -39
  97. package/framework/workflows/configs/long-running.json +0 -33
  98. package/framework/workflows/configs/spec-only.json +0 -43
  99. package/framework/workflows/configs/ui-refresh.json +0 -49
  100. package/framework/workflows/configs/zero-touch.json +0 -82
  101. package/src/commands/project/monitor.js +0 -295
  102. package/src/commands/project/tutorial.js +0 -115
  103. package/src/commands/state/validate-phase.js +0 -238
  104. package/src/commands/templates/generate-contracts.js +0 -445
  105. package/src/core/orchestrator.js +0 -171
  106. package/src/core/registry/command-registry.js +0 -28
  107. package/src/core/registry/index.js +0 -8
  108. package/src/core/registry/validator-registry.js +0 -204
  109. package/src/core/templates/template-validator.js +0 -296
  110. package/src/generator/config-generator.js +0 -206
  111. package/src/generator/templates/config.json.template +0 -40
  112. package/src/generator/templates/project.md.template +0 -67
  113. package/src/lib/agents/micro-agent-factory.js +0 -161
  114. package/src/lib/analysis/complexity-analyzer.js +0 -441
  115. package/src/lib/analysis/index.js +0 -7
  116. package/src/lib/analytics/analytics-engine.js +0 -345
  117. package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
  118. package/src/lib/checkpoints/index.js +0 -7
  119. package/src/lib/context/context-bundler.js +0 -241
  120. package/src/lib/context/context-optimizer.js +0 -212
  121. package/src/lib/context/context-tracker.js +0 -273
  122. package/src/lib/context/core-four-tracker.js +0 -201
  123. package/src/lib/context/mcp-optimizer.js +0 -200
  124. package/src/lib/execution/fusion-executor.js +0 -304
  125. package/src/lib/execution/parallel-executor.js +0 -270
  126. package/src/lib/hooks/stop-hook-executor.js +0 -286
  127. package/src/lib/hops/hop-composer.js +0 -221
  128. package/src/lib/phase-chain/eligibility-checker.js +0 -243
  129. package/src/lib/threads/thread-coordinator.js +0 -238
  130. package/src/lib/threads/thread-manager.js +0 -317
  131. package/src/lib/tracking/artifact-trail.js +0 -202
  132. package/src/scanner/project-scanner.js +0 -242
  133. package/src/ui/diff-display.js +0 -91
  134. package/src/ui/interactive-wizard.js +0 -96
  135. package/src/ui/user-review.js +0 -211
  136. package/src/ui/wizard-questions.js +0 -188
  137. package/src/utils/color-utils.js +0 -70
  138. package/src/utils/process-handler.js +0 -97
@@ -1,365 +1,381 @@
1
- import { join, dirname } from 'path';
2
- import { fileURLToPath } from 'url';
3
- import { spawnSync } from 'child_process';
4
- import { existsSync } from 'fs';
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- import fs from 'fs-extra';
8
- import ora from 'ora';
9
- import chalk from 'chalk';
10
- import { logger } from '../../utils/logger.js';
11
- import {
12
- getContentDir,
13
- copyDirectory,
14
- copyFile,
15
- pathExists,
16
- ensureDir,
17
- readJson,
18
- writeJson,
19
- updateGitignore
20
- } from '../../utils/file-copier.js';
21
- import {
22
- checkCLIOutdated,
23
- checkProjectOutdated,
24
- saveProjectMorphVersion,
25
- getUpdateInstructions,
26
- detectInstallMethod
27
- } from '../../utils/version-checker.js';
28
- import { installClaudeHooks, installGlobalStatusline } from '../../utils/claude-settings-manager.js';
29
- import { installSkills } from '../../utils/skills-installer.js';
30
- import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
31
-
32
- /**
33
- * Backup user's config.json before cleaning
34
- * @param {string} morphPath - Path to .morph directory
35
- * @returns {Promise<object|null>} Backup of config.json or null
36
- */
37
- async function backupUserConfig(morphPath) {
38
- try {
39
- const configPath = join(morphPath, 'config', 'config.json');
40
- if (await pathExists(configPath)) {
41
- const config = await readJson(configPath);
42
- return config;
43
- }
44
- } catch (error) {
45
- // If backup fails, return null and continue
46
- }
47
- return null;
48
- }
49
-
50
- /**
51
- * Clean framework directories but preserve .morph/project/
52
- * @param {string} morphPath - Path to .morph directory
53
- * @param {string} targetPath - Project root path
54
- */
55
- async function cleanFrameworkDirs(morphPath, targetPath) {
56
- const claudeDir = join(targetPath, '.claude');
57
- const dirsToClean = [
58
- join(morphPath, 'framework', 'templates'),
59
- join(morphPath, 'framework', 'standards'),
60
- join(morphPath, 'framework', 'hooks'),
61
- join(morphPath, 'config'),
62
- join(claudeDir, 'commands'),
63
- join(claudeDir, 'rules'),
64
- join(claudeDir, 'skills'),
65
- join(claudeDir, 'agents'),
66
- ];
67
-
68
- for (const dir of dirsToClean) {
69
- try {
70
- if (await pathExists(dir)) {
71
- await fs.remove(dir);
72
- }
73
- } catch (error) {
74
- // Continue even if cleanup fails for one directory
75
- }
76
- }
77
- }
78
-
79
- /**
80
- * Restore user's config.json after reinstallation
81
- * @param {string} morphPath - Path to .morph directory
82
- * @param {string} targetPath - Project root path
83
- * @param {object|null} configBackup - Backup of config.json
84
- * @param {string} currentVersion - Current CLI version
85
- */
86
- async function restoreUserConfig(morphPath, targetPath, configBackup, currentVersion) {
87
- const configDir = join(morphPath, 'config');
88
- await ensureDir(configDir);
89
- const configPath = join(configDir, 'config.json');
90
-
91
- if (configBackup) {
92
- // Preserve user data but update framework version
93
- configBackup.frameworkVersion = currentVersion;
94
- await writeJson(configPath, configBackup);
95
- } else {
96
- // Create basic config if none existed
97
- const dirName = targetPath.split(/[\\/]/).pop();
98
- const defaultConfig = {
99
- framework: 'global',
100
- frameworkVersion: currentVersion,
101
- project: {
102
- name: dirName,
103
- architecture: 'unknown'
104
- }
105
- };
106
- await writeJson(configPath, defaultConfig);
107
- }
108
- }
109
-
110
- export async function updateCommand(options) {
111
- const targetPath = process.cwd();
112
- const contentDir = getContentDir();
113
- const morphPath = join(targetPath, '.morph');
114
-
115
- logger.header('MORPH-SPEC Update');
116
-
117
- // Check if MORPH is initialized
118
- let needsInitialization = false;
119
- if (!(await pathExists(morphPath))) {
120
- logger.warn('MORPH not fully initialized. Creating basic structure...');
121
- needsInitialization = true;
122
-
123
- // Create basic .morph structure
124
- await ensureDir(join(morphPath, 'context'));
125
- await ensureDir(join(morphPath, 'features'));
126
- await ensureDir(join(morphPath, 'framework'));
127
- await ensureDir(join(morphPath, 'config'));
128
- logger.dim(' ✓ Created .morph directory structure');
129
- }
130
-
131
- // Check CLI version
132
- const spinner = ora('Checking versions...').start();
133
- const cliCheck = await checkCLIOutdated();
134
- const projectCheck = await checkProjectOutdated(targetPath);
135
-
136
- spinner.stop();
137
-
138
- // Display version info
139
- logger.blank();
140
- logger.info('Version Status:');
141
- logger.dim(` CLI version: ${cliCheck.current}`);
142
-
143
- if (projectCheck.current) {
144
- logger.dim(` Project MORPH version: ${projectCheck.current}`);
145
- } else {
146
- logger.dim(' Project MORPH version: not found (legacy installation)');
147
- }
148
-
149
- if (cliCheck.latest) {
150
- const latestColor = cliCheck.isOutdated ? chalk.yellow : chalk.green;
151
- logger.dim(` Latest available: ${latestColor(cliCheck.latest)}`);
152
- }
153
- logger.blank();
154
-
155
- // If CLI is outdated, stop and instruct user
156
- if (cliCheck.isOutdated && cliCheck.latest) {
157
- logger.warn(`Your CLI is outdated (${cliCheck.current} → ${cliCheck.latest})`);
158
- logger.blank();
159
- logger.info('Please update the CLI first:');
160
- logger.blank();
161
-
162
- const method = detectInstallMethod();
163
- const instructions = getUpdateInstructions(method);
164
-
165
- instructions.forEach(line => {
166
- if (line === '') {
167
- logger.blank();
168
- } else if (line.startsWith('Or ')) {
169
- logger.dim(line);
170
- } else {
171
- logger.box([line]);
172
- }
173
- });
174
-
175
- logger.blank();
176
- logger.dim('Then run "morph-spec update" again.');
177
- process.exit(0);
178
- }
179
-
180
- // Proceed with update
181
- const updateSpinner = ora('Updating MORPH-SPEC...').start();
182
-
183
- try {
184
- // Backup user config before cleaning
185
- updateSpinner.text = 'Backing up user configuration...';
186
- const configBackup = await backupUserConfig(morphPath);
187
- if (configBackup) {
188
- logger.dim(' ✓ User config backed up');
189
- }
190
-
191
- // Clean framework directories (preserve .morph/project/)
192
- updateSpinner.text = 'Cleaning old framework files...';
193
- await cleanFrameworkDirs(morphPath, targetPath);
194
- logger.dim(' ✓ Old framework files removed');
195
-
196
- const updateTemplates = !options.standards || options.templates;
197
- const updateStandards = !options.templates || options.standards;
198
-
199
- // Update templates
200
- if (updateTemplates) {
201
- updateSpinner.text = 'Updating templates...';
202
- const templatesSrc = join(contentDir, 'framework', 'templates');
203
- const templatesDest = join(morphPath, 'framework', 'templates');
204
- await copyDirectory(templatesSrc, templatesDest);
205
- }
206
-
207
- // Update standards
208
- if (updateStandards) {
209
- updateSpinner.text = 'Updating standards...';
210
- const standardsSrc = join(contentDir, 'framework', 'standards');
211
- const standardsDest = join(morphPath, 'framework', 'standards');
212
- await copyDirectory(standardsSrc, standardsDest);
213
- }
214
-
215
- // Update hooks (runtime hook scripts under .morph/framework/hooks/)
216
- updateSpinner.text = 'Updating hooks...';
217
- const hooksSrc = join(__dirname, '..', '..', '..', 'framework', 'hooks');
218
- const hooksDest = join(morphPath, 'framework', 'hooks');
219
- if (await pathExists(hooksSrc)) {
220
- await copyDirectory(hooksSrc, hooksDest);
221
- }
222
-
223
- // Update agents.json (sourced from framework/ — canonical single source of truth)
224
- updateSpinner.text = 'Updating agents configuration...';
225
- const agentsSrc = join(__dirname, '..', '..', '..', 'framework', 'agents.json');
226
- const agentsDest = join(morphPath, 'framework', 'agents.json');
227
- if (await pathExists(agentsSrc)) {
228
- await copyFile(agentsSrc, agentsDest);
229
- }
230
-
231
- // Regenerate derived files (phase-utils.js, skill output tables) from output-schema.js
232
- // Only runs when the generator script is present (framework dev installs via npm link)
233
- updateSpinner.text = 'Syncing generated refs...';
234
- try {
235
- const generateScript = join(__dirname, '..', '..', '..', 'scripts', 'generate-refs.js');
236
- if (existsSync(generateScript)) {
237
- const result = spawnSync(process.execPath, [generateScript], { encoding: 'utf8' });
238
- if (result.status === 0) {
239
- logger.dim(' ✓ Generated refs synced (phase-utils.js, skill outputs)');
240
- }
241
- }
242
- } catch {
243
- // Non-fatal: generator unavailable or files already in sync
244
- }
245
-
246
- // Update .claude commands and skills
247
- // Source: framework/ (canonical for all stacks)
248
- updateSpinner.text = 'Setting up Claude Code integration...';
249
- const frameworkDir = join(__dirname, '..', '..', '..', 'framework');
250
- const claudeDest = join(targetPath, '.claude');
251
-
252
- let commandsCopied = false;
253
-
254
- // Copy commands directory (slash commands): framework/commands/ → .claude/commands/
255
- const commandsSrc = join(frameworkDir, 'commands');
256
- const commandsDest = join(claudeDest, 'commands');
257
- if (await pathExists(commandsSrc)) {
258
- await copyDirectory(commandsSrc, commandsDest);
259
- commandsCopied = true;
260
- } else {
261
- logger.warn(' ⚠ framework/commands/ source missing — commands not updated');
262
- }
263
-
264
- // Sync morph skills to .claude/skills/ for native Claude Code discovery
265
- updateSpinner.text = 'Syncing morph skills to .claude/skills/...';
266
- await installSkills(targetPath);
267
-
268
- // Sync native subagents to .claude/agents/
269
- updateSpinner.text = 'Syncing agents to .claude/agents/...';
270
- let projectStack = null;
271
- try {
272
- const cfg = await readJson(join(morphPath, 'config', 'config.json'));
273
- projectStack = cfg?.project?.stack ?? null;
274
- } catch { /* ignore — config may not exist yet */ }
275
- await installAgents(targetPath, frameworkDir, { projectStack });
276
- await installDomainAgents(targetPath, frameworkDir);
277
-
278
- // Sync path-scoped rules to .claude/rules/
279
- updateSpinner.text = 'Syncing rules to .claude/rules/...';
280
- const rulesSrc = join(frameworkDir, 'rules');
281
- const rulesDest = join(claudeDest, 'rules');
282
- if (await pathExists(rulesSrc)) {
283
- await copyDirectory(rulesSrc, rulesDest);
284
- }
285
-
286
- // Sync runtime CLAUDE.md to .claude/CLAUDE.md
287
- updateSpinner.text = 'Syncing .claude/CLAUDE.md...';
288
- const runtimeSrc = join(frameworkDir, 'CLAUDE.md');
289
- const runtimeDest = join(claudeDest, 'CLAUDE.md');
290
- if (await pathExists(runtimeSrc)) {
291
- await copyFile(runtimeSrc, runtimeDest);
292
- }
293
-
294
- // Sync statusline globally to ~/.claude/
295
- updateSpinner.text = 'Syncing statusline to ~/.claude/...';
296
- const HOOKS_SRC = join(__dirname, '..', '..', '..', 'framework', 'hooks', 'claude-code');
297
- try {
298
- await installGlobalStatusline(HOOKS_SRC);
299
- } catch {
300
- // Non-critical: global dir may not be writable in all environments
301
- logger.dim(' ⚠ Could not install statusline globally (non-critical)');
302
- }
303
-
304
- // Update Claude Code hooks in .claude/settings.local.json
305
- updateSpinner.text = 'Updating Claude Code hooks...';
306
- const hooksResult = await installClaudeHooks(targetPath);
307
-
308
- // Update CLAUDE.md
309
- updateSpinner.text = 'Updating CLAUDE.md...';
310
- const claudeMdSrc = join(frameworkDir, 'CLAUDE.md');
311
- const claudeMdDest = join(targetPath, 'CLAUDE.md');
312
- await copyFile(claudeMdSrc, claudeMdDest);
313
-
314
- // Restore user config after framework reinstallation
315
- updateSpinner.text = 'Restoring user configuration...';
316
- await restoreUserConfig(morphPath, targetPath, configBackup, cliCheck.current);
317
- logger.dim(' ✓ User config restored with updated framework version');
318
-
319
- // Update .gitignore with current morph rules
320
- updateSpinner.text = 'Updating .gitignore...';
321
- await updateGitignore(targetPath);
322
-
323
- // Update .morphversion
324
- updateSpinner.text = 'Saving version info...';
325
- await saveProjectMorphVersion(targetPath, cliCheck.current);
326
-
327
- updateSpinner.succeed('MORPH-SPEC updated successfully!');
328
- logger.blank();
329
-
330
- // Show version change
331
- if (projectCheck.current && projectCheck.current !== cliCheck.current) {
332
- logger.success(`Updated: ${projectCheck.current} ${cliCheck.current}`);
333
- } else if (!projectCheck.current) {
334
- logger.success(`Updated to v${cliCheck.current}`);
335
- } else {
336
- logger.success(`Already up to date (v${cliCheck.current})`);
337
- }
338
-
339
- logger.blank();
340
- logger.info('Updated files:');
341
- if (updateTemplates) logger.dim(' ✓ .morph/framework/templates/');
342
- if (updateStandards) logger.dim(' ✓ .morph/framework/standards/');
343
- logger.dim(' .morph/framework/hooks/');
344
- logger.dim(' ✓ .morph/framework/agents.json');
345
- if (commandsCopied) logger.dim(' ✓ .claude/commands/');
346
-
347
- logger.dim(` ✓ .claude/settings.local.json (${hooksResult.installed} hooks installed)`);
348
- logger.dim(' ✓ .claude/skills/ (skills installed as directories)');
349
- logger.dim(' ✓ .claude/agents/ (native subagents refreshed)');
350
- logger.dim(' ✓ .claude/rules/ (path-scoped rules synced)');
351
- logger.dim(' ✓ .claude/CLAUDE.md (runtime quick reference)');
352
- logger.dim(' ✓ ~/.claude/statusline.sh (global statusline synced)');
353
- logger.dim(' ✓ CLAUDE.md');
354
- logger.dim(' ✓ .gitignore (morph rules updated)');
355
- logger.blank();
356
- logger.info('Your config.json was preserved.');
357
- logger.dim('Review the updated files for any new features.');
358
- logger.blank();
359
-
360
- } catch (error) {
361
- updateSpinner.fail('Update failed');
362
- logger.error(error.message);
363
- process.exit(1);
364
- }
365
- }
1
+ import { join, dirname } from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { spawnSync } from 'child_process';
4
+ import { existsSync } from 'fs';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ import fs from 'fs-extra';
8
+ import ora from 'ora';
9
+ import chalk from 'chalk';
10
+ import { logger } from '../../utils/logger.js';
11
+ import {
12
+ getContentDir,
13
+ copyDirectory,
14
+ copyFile,
15
+ pathExists,
16
+ ensureDir,
17
+ readJson,
18
+ writeJson,
19
+ updateGitignore
20
+ } from '../../utils/file-copier.js';
21
+ import {
22
+ checkCLIOutdated,
23
+ checkProjectOutdated,
24
+ saveProjectMorphVersion,
25
+ getUpdateInstructions,
26
+ detectInstallMethod
27
+ } from '../../utils/version-checker.js';
28
+ import { installClaudeHooks, installGlobalStatusline, installVSCodeTerminalSettings, installShellIntegration } from '../../utils/claude-settings-manager.js';
29
+ import { installSkills } from '../../utils/skills-installer.js';
30
+ import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
31
+
32
+ /**
33
+ * Backup user's config.json before cleaning
34
+ * @param {string} morphPath - Path to .morph directory
35
+ * @returns {Promise<object|null>} Backup of config.json or null
36
+ */
37
+ async function backupUserConfig(morphPath) {
38
+ try {
39
+ const configPath = join(morphPath, 'config', 'config.json');
40
+ if (await pathExists(configPath)) {
41
+ const config = await readJson(configPath);
42
+ return config;
43
+ }
44
+ } catch (error) {
45
+ // If backup fails, return null and continue
46
+ }
47
+ return null;
48
+ }
49
+
50
+ /**
51
+ * Clean framework directories but preserve .morph/project/
52
+ * @param {string} morphPath - Path to .morph directory
53
+ * @param {string} targetPath - Project root path
54
+ */
55
+ async function cleanFrameworkDirs(morphPath, targetPath) {
56
+ const claudeDir = join(targetPath, '.claude');
57
+ const dirsToClean = [
58
+ join(morphPath, 'framework', 'templates'),
59
+ join(morphPath, 'framework', 'standards'),
60
+ join(morphPath, 'framework', 'hooks'),
61
+ join(morphPath, 'config'),
62
+ join(claudeDir, 'commands'),
63
+ join(claudeDir, 'rules'),
64
+ join(claudeDir, 'skills'),
65
+ join(claudeDir, 'agents'),
66
+ ];
67
+
68
+ for (const dir of dirsToClean) {
69
+ try {
70
+ if (await pathExists(dir)) {
71
+ await fs.remove(dir);
72
+ }
73
+ } catch (error) {
74
+ // Continue even if cleanup fails for one directory
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Restore user's config.json after reinstallation
81
+ * @param {string} morphPath - Path to .morph directory
82
+ * @param {string} targetPath - Project root path
83
+ * @param {object|null} configBackup - Backup of config.json
84
+ * @param {string} currentVersion - Current CLI version
85
+ */
86
+ async function restoreUserConfig(morphPath, targetPath, configBackup, currentVersion) {
87
+ const configDir = join(morphPath, 'config');
88
+ await ensureDir(configDir);
89
+ const configPath = join(configDir, 'config.json');
90
+
91
+ if (configBackup) {
92
+ // Preserve user data but update framework version
93
+ configBackup.frameworkVersion = currentVersion;
94
+ await writeJson(configPath, configBackup);
95
+ } else {
96
+ // Create basic config if none existed
97
+ const dirName = targetPath.split(/[\\/]/).pop();
98
+ const defaultConfig = {
99
+ framework: 'global',
100
+ frameworkVersion: currentVersion,
101
+ project: {
102
+ name: dirName,
103
+ architecture: 'unknown'
104
+ }
105
+ };
106
+ await writeJson(configPath, defaultConfig);
107
+ }
108
+ }
109
+
110
+ export async function updateCommand(options) {
111
+ const targetPath = process.cwd();
112
+ const contentDir = getContentDir();
113
+ const morphPath = join(targetPath, '.morph');
114
+
115
+ logger.header('MORPH-SPEC Update');
116
+
117
+ // Check if MORPH is initialized
118
+ let needsInitialization = false;
119
+ if (!(await pathExists(morphPath))) {
120
+ logger.warn('MORPH not fully initialized. Creating basic structure...');
121
+ needsInitialization = true;
122
+
123
+ // Create basic .morph structure
124
+ await ensureDir(join(morphPath, 'context'));
125
+ await ensureDir(join(morphPath, 'features'));
126
+ await ensureDir(join(morphPath, 'framework'));
127
+ await ensureDir(join(morphPath, 'config'));
128
+ logger.dim(' ✓ Created .morph directory structure');
129
+ }
130
+
131
+ // Check CLI version
132
+ const spinner = ora('Checking versions...').start();
133
+ const cliCheck = await checkCLIOutdated();
134
+ const projectCheck = await checkProjectOutdated(targetPath);
135
+
136
+ spinner.stop();
137
+
138
+ // Display version info
139
+ logger.blank();
140
+ logger.info('Version Status:');
141
+ logger.dim(` CLI version: ${cliCheck.current}`);
142
+
143
+ if (projectCheck.current) {
144
+ logger.dim(` Project MORPH version: ${projectCheck.current}`);
145
+ } else {
146
+ logger.dim(' Project MORPH version: not found (legacy installation)');
147
+ }
148
+
149
+ if (cliCheck.latest) {
150
+ const latestColor = cliCheck.isOutdated ? chalk.yellow : chalk.green;
151
+ logger.dim(` Latest available: ${latestColor(cliCheck.latest)}`);
152
+ }
153
+ logger.blank();
154
+
155
+ // If CLI is outdated, stop and instruct user
156
+ if (cliCheck.isOutdated && cliCheck.latest) {
157
+ logger.warn(`Your CLI is outdated (${cliCheck.current} → ${cliCheck.latest})`);
158
+ logger.blank();
159
+ logger.info('Please update the CLI first:');
160
+ logger.blank();
161
+
162
+ const method = detectInstallMethod();
163
+ const instructions = getUpdateInstructions(method);
164
+
165
+ instructions.forEach(line => {
166
+ if (line === '') {
167
+ logger.blank();
168
+ } else if (line.startsWith('Or ')) {
169
+ logger.dim(line);
170
+ } else {
171
+ logger.box([line]);
172
+ }
173
+ });
174
+
175
+ logger.blank();
176
+ logger.dim('Then run "morph-spec update" again.');
177
+ process.exit(0);
178
+ }
179
+
180
+ // Proceed with update
181
+ const updateSpinner = ora('Updating MORPH-SPEC...').start();
182
+
183
+ try {
184
+ // Backup user config before cleaning
185
+ updateSpinner.text = 'Backing up user configuration...';
186
+ const configBackup = await backupUserConfig(morphPath);
187
+ if (configBackup) {
188
+ logger.dim(' ✓ User config backed up');
189
+ }
190
+
191
+ // Clean framework directories (preserve .morph/project/)
192
+ updateSpinner.text = 'Cleaning old framework files...';
193
+ await cleanFrameworkDirs(morphPath, targetPath);
194
+ logger.dim(' ✓ Old framework files removed');
195
+
196
+ const updateTemplates = !options.standards || options.templates;
197
+ const updateStandards = !options.templates || options.standards;
198
+
199
+ // Update templates
200
+ if (updateTemplates) {
201
+ updateSpinner.text = 'Updating templates...';
202
+ const templatesSrc = join(contentDir, 'framework', 'templates');
203
+ const templatesDest = join(morphPath, 'framework', 'templates');
204
+ await copyDirectory(templatesSrc, templatesDest);
205
+ }
206
+
207
+ // Update standards
208
+ if (updateStandards) {
209
+ updateSpinner.text = 'Updating standards...';
210
+ const standardsSrc = join(contentDir, 'framework', 'standards');
211
+ const standardsDest = join(morphPath, 'framework', 'standards');
212
+ await copyDirectory(standardsSrc, standardsDest);
213
+ }
214
+
215
+ // Update hooks (runtime hook scripts under .morph/framework/hooks/)
216
+ updateSpinner.text = 'Updating hooks...';
217
+ const hooksSrc = join(__dirname, '..', '..', '..', 'framework', 'hooks');
218
+ const hooksDest = join(morphPath, 'framework', 'hooks');
219
+ if (await pathExists(hooksSrc)) {
220
+ await copyDirectory(hooksSrc, hooksDest);
221
+ }
222
+
223
+ // Update agents.json (sourced from framework/ — canonical single source of truth)
224
+ updateSpinner.text = 'Updating agents configuration...';
225
+ const agentsSrc = join(__dirname, '..', '..', '..', 'framework', 'agents.json');
226
+ const agentsDest = join(morphPath, 'framework', 'agents.json');
227
+ if (await pathExists(agentsSrc)) {
228
+ await copyFile(agentsSrc, agentsDest);
229
+ }
230
+
231
+ // Regenerate derived files (phase-utils.js, skill output tables) from output-schema.js
232
+ // Only runs when the generator script is present (framework dev installs via npm link)
233
+ updateSpinner.text = 'Syncing generated refs...';
234
+ try {
235
+ const generateScript = join(__dirname, '..', '..', '..', 'scripts', 'generate-refs.js');
236
+ if (existsSync(generateScript)) {
237
+ const result = spawnSync(process.execPath, [generateScript], { encoding: 'utf8' });
238
+ if (result.status === 0) {
239
+ logger.dim(' ✓ Generated refs synced (phase-utils.js, skill outputs)');
240
+ }
241
+ }
242
+ } catch {
243
+ // Non-fatal: generator unavailable or files already in sync
244
+ }
245
+
246
+ // Update .claude commands and skills
247
+ // Source: framework/ (canonical for all stacks)
248
+ updateSpinner.text = 'Setting up Claude Code integration...';
249
+ const frameworkDir = join(__dirname, '..', '..', '..', 'framework');
250
+ const claudeDest = join(targetPath, '.claude');
251
+
252
+ let commandsCopied = false;
253
+
254
+ // Copy commands directory (slash commands): framework/commands/ → .claude/commands/
255
+ const commandsSrc = join(frameworkDir, 'commands');
256
+ const commandsDest = join(claudeDest, 'commands');
257
+ if (await pathExists(commandsSrc)) {
258
+ await copyDirectory(commandsSrc, commandsDest);
259
+ commandsCopied = true;
260
+ } else {
261
+ logger.warn(' ⚠ framework/commands/ source missing — commands not updated');
262
+ }
263
+
264
+ // Sync morph skills to .claude/skills/ for native Claude Code discovery
265
+ updateSpinner.text = 'Syncing morph skills to .claude/skills/...';
266
+ await installSkills(targetPath);
267
+
268
+ // Sync native subagents to .claude/agents/
269
+ updateSpinner.text = 'Syncing agents to .claude/agents/...';
270
+ let projectStack = null;
271
+ try {
272
+ const cfg = await readJson(join(morphPath, 'config', 'config.json'));
273
+ projectStack = cfg?.project?.stack ?? null;
274
+ } catch { /* ignore — config may not exist yet */ }
275
+ await installAgents(targetPath, frameworkDir, { projectStack });
276
+ await installDomainAgents(targetPath, frameworkDir);
277
+
278
+ // Sync path-scoped rules to .claude/rules/
279
+ updateSpinner.text = 'Syncing rules to .claude/rules/...';
280
+ const rulesSrc = join(frameworkDir, 'rules');
281
+ const rulesDest = join(claudeDest, 'rules');
282
+ if (await pathExists(rulesSrc)) {
283
+ await copyDirectory(rulesSrc, rulesDest);
284
+ }
285
+
286
+ // Sync runtime CLAUDE.md to .claude/CLAUDE.md
287
+ updateSpinner.text = 'Syncing .claude/CLAUDE.md...';
288
+ const runtimeSrc = join(frameworkDir, 'CLAUDE.md');
289
+ const runtimeDest = join(claudeDest, 'CLAUDE.md');
290
+ if (await pathExists(runtimeSrc)) {
291
+ await copyFile(runtimeSrc, runtimeDest);
292
+ }
293
+
294
+ // Sync statusline globally to ~/.claude/
295
+ updateSpinner.text = 'Syncing statusline to ~/.claude/...';
296
+ const HOOKS_SRC = join(__dirname, '..', '..', '..', 'framework', 'hooks', 'claude-code');
297
+ try {
298
+ await installGlobalStatusline(HOOKS_SRC);
299
+ } catch {
300
+ // Non-critical: global dir may not be writable in all environments
301
+ logger.dim(' ⚠ Could not install statusline globally (non-critical)');
302
+ }
303
+
304
+ // Update Claude Code hooks in .claude/settings.local.json
305
+ updateSpinner.text = 'Updating Claude Code hooks...';
306
+ const hooksResult = await installClaudeHooks(targetPath);
307
+
308
+ // Install shell integration (terminal title watcher)
309
+ updateSpinner.text = 'Installing shell integration...';
310
+ try {
311
+ await installShellIntegration();
312
+ } catch {
313
+ logger.dim(' ⚠ Could not install shell integration (non-critical)');
314
+ }
315
+
316
+ // Configure VS Code terminal settings (enable OSC title sequences)
317
+ updateSpinner.text = 'Configuring VS Code terminal settings...';
318
+ try {
319
+ await installVSCodeTerminalSettings();
320
+ } catch {
321
+ logger.dim(' ⚠ Could not configure VS Code terminal settings (non-critical)');
322
+ }
323
+
324
+ // Update CLAUDE.md
325
+ updateSpinner.text = 'Updating CLAUDE.md...';
326
+ const claudeMdSrc = join(frameworkDir, 'CLAUDE.md');
327
+ const claudeMdDest = join(targetPath, 'CLAUDE.md');
328
+ await copyFile(claudeMdSrc, claudeMdDest);
329
+
330
+ // Restore user config after framework reinstallation
331
+ updateSpinner.text = 'Restoring user configuration...';
332
+ await restoreUserConfig(morphPath, targetPath, configBackup, cliCheck.current);
333
+ logger.dim(' ✓ User config restored with updated framework version');
334
+
335
+ // Update .gitignore with current morph rules
336
+ updateSpinner.text = 'Updating .gitignore...';
337
+ await updateGitignore(targetPath);
338
+
339
+ // Update .morphversion
340
+ updateSpinner.text = 'Saving version info...';
341
+ await saveProjectMorphVersion(targetPath, cliCheck.current);
342
+
343
+ updateSpinner.succeed('MORPH-SPEC updated successfully!');
344
+ logger.blank();
345
+
346
+ // Show version change
347
+ if (projectCheck.current && projectCheck.current !== cliCheck.current) {
348
+ logger.success(`Updated: ${projectCheck.current} ${cliCheck.current}`);
349
+ } else if (!projectCheck.current) {
350
+ logger.success(`Updated to v${cliCheck.current}`);
351
+ } else {
352
+ logger.success(`Already up to date (v${cliCheck.current})`);
353
+ }
354
+
355
+ logger.blank();
356
+ logger.info('Updated files:');
357
+ if (updateTemplates) logger.dim(' .morph/framework/templates/');
358
+ if (updateStandards) logger.dim(' ✓ .morph/framework/standards/');
359
+ logger.dim(' ✓ .morph/framework/hooks/');
360
+ logger.dim(' .morph/framework/agents.json');
361
+ if (commandsCopied) logger.dim(' .claude/commands/');
362
+
363
+ logger.dim(` ✓ .claude/settings.local.json (${hooksResult.installed} hooks installed)`);
364
+ logger.dim(' ✓ .claude/skills/ (skills installed as directories)');
365
+ logger.dim(' ✓ .claude/agents/ (native subagents refreshed)');
366
+ logger.dim(' ✓ .claude/rules/ (path-scoped rules synced)');
367
+ logger.dim(' ✓ .claude/CLAUDE.md (runtime quick reference)');
368
+ logger.dim(' ✓ ~/.claude/statusline.sh (global statusline synced)');
369
+ logger.dim(' ✓ CLAUDE.md');
370
+ logger.dim(' ✓ .gitignore (morph rules updated)');
371
+ logger.blank();
372
+ logger.info('Your config.json was preserved.');
373
+ logger.dim('Review the updated files for any new features.');
374
+ logger.blank();
375
+
376
+ } catch (error) {
377
+ updateSpinner.fail('Update failed');
378
+ logger.error(error.message);
379
+ process.exit(1);
380
+ }
381
+ }