@polymorphism-tech/morph-spec 4.10.10 → 4.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +106 -2
- package/README.md +5 -11
- package/bin/morph-spec.js +30 -2
- package/bin/task-manager.js +394 -38
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +2 -8
- package/framework/agents/frontend/css-specialist.md +1 -1
- package/framework/agents/infrastructure/azure-deploy-specialist.md +4 -4
- package/framework/commands/morph-apply.md +39 -19
- package/framework/commands/morph-preflight.md +3 -3
- package/framework/commands/morph-proposal.md +59 -16
- package/framework/commands/morph-status.md +3 -3
- package/framework/commands/morph-troubleshoot.md +1 -1
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +47 -2
- package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +2 -2
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +5 -4
- package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +3 -3
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +2 -2
- package/framework/hooks/git/pre-commit/agents.sh +1 -1
- package/framework/hooks/shared/dispatch-helpers.js +14 -0
- package/framework/hooks/shared/state-reader.js +7 -3
- package/framework/rules/morph-task-tracking.md +49 -0
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +14 -18
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +3 -3
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
- package/framework/skills/level-0-meta/morph-init/SKILL.md +11 -11
- package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +3 -3
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +1 -1
- package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +4 -4
- package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +25 -25
- package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +8 -8
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +16 -7
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +4 -4
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +29 -35
- package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +35 -29
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +4 -4
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +17 -20
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +23 -26
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +21 -29
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +24 -16
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +5 -5
- package/framework/workflows/README.md +18 -18
- package/framework/workflows/docs/enforcement-pipeline.md +1 -1
- package/framework/workflows/docs/full-morph.md +2 -2
- package/package.json +1 -1
- package/src/commands/agents/dispatch-agents.js +25 -1
- package/src/commands/project/doctor.js +3 -4
- package/src/commands/project/status.js +8 -1
- package/src/commands/scope/escalate.js +5 -3
- package/src/commands/tasks/task.js +54 -0
- package/src/commands/validation/validate-feature.js +32 -3
- package/src/lib/build/build-runner.js +99 -0
- package/src/lib/checkpoints/checkpoint-hooks.js +110 -0
- package/src/lib/generators/recap-generator.js +27 -4
- package/src/lib/stack/stack-profile.js +3 -0
- package/src/lib/tasks/task-parser.js +23 -9
- package/src/scripts/global-install.js +57 -3
- package/src/utils/file-copier.js +1 -0
- package/src/utils/version-checker.js +1 -7
- package/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +0 -7
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Runner — Resolves and executes the project's build command.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order for build command:
|
|
5
|
+
* 1. .morph/config/config.json → project.buildCommand
|
|
6
|
+
* 2. Stack profile → profile.buildCommand
|
|
7
|
+
* 3. package.json → scripts.build → "npm run build"
|
|
8
|
+
* 4. tsconfig.json at root → "npx tsc --noEmit"
|
|
9
|
+
* 5. null (no build command detected)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { existsSync, readFileSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { getStackProfile } from '../stack/stack-profile.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the build command for a project.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
21
|
+
* @returns {string | null} The build command to run, or null if none found
|
|
22
|
+
*/
|
|
23
|
+
export function resolveBuildCommand(projectPath = '.') {
|
|
24
|
+
// 1. Explicit override from .morph/config/config.json
|
|
25
|
+
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
26
|
+
if (existsSync(configPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
29
|
+
const cmd = config?.project?.buildCommand;
|
|
30
|
+
if (cmd && typeof cmd === 'string') {
|
|
31
|
+
return cmd;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Malformed config — fall through to detection
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Stack profile default (only when an actual stack is configured)
|
|
39
|
+
const profile = getStackProfile(projectPath);
|
|
40
|
+
if (profile.stack !== 'unknown' && profile.buildCommand) {
|
|
41
|
+
return profile.buildCommand;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. package.json with a build script
|
|
45
|
+
const packageJsonPath = join(projectPath, 'package.json');
|
|
46
|
+
if (existsSync(packageJsonPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
49
|
+
if (pkg?.scripts?.build) {
|
|
50
|
+
return 'npm run build';
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Malformed package.json — fall through
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4. tsconfig.json at root → generic type-check
|
|
58
|
+
if (existsSync(join(projectPath, 'tsconfig.json'))) {
|
|
59
|
+
return 'npx tsc --noEmit';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 5. Nothing found
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run the project's build check.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
70
|
+
* @param {object} [opts] - Optional overrides for testing
|
|
71
|
+
* @param {Function} [opts._execSync] - Override execSync for unit testing
|
|
72
|
+
* @returns {{ passed?: boolean, output?: string, exitCode?: number, command?: string, skipped?: boolean, reason?: string }}
|
|
73
|
+
*/
|
|
74
|
+
export function runBuildCheck(projectPath = '.', opts = {}) {
|
|
75
|
+
const cmd = resolveBuildCommand(projectPath);
|
|
76
|
+
|
|
77
|
+
if (!cmd) {
|
|
78
|
+
return { skipped: true, reason: 'No buildCommand configured and no known project file detected' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const exec = opts._execSync ?? execSync;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const output = exec(cmd, {
|
|
85
|
+
cwd: projectPath,
|
|
86
|
+
timeout: 120_000,
|
|
87
|
+
encoding: 'utf8',
|
|
88
|
+
stdio: 'pipe'
|
|
89
|
+
});
|
|
90
|
+
return { passed: true, output: output ?? '', exitCode: 0, command: cmd };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return {
|
|
93
|
+
passed: false,
|
|
94
|
+
output: (err.stdout || '') + (err.stderr || ''),
|
|
95
|
+
exitCode: err.status ?? 1,
|
|
96
|
+
command: cmd
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Hooks — Orchestrates validation, build, and test phases at checkpoints.
|
|
3
|
+
*
|
|
4
|
+
* This is the module that task-manager.js imports at:
|
|
5
|
+
* import('../src/lib/checkpoints/checkpoint-hooks.js')
|
|
6
|
+
*
|
|
7
|
+
* All three phases run independently (failures don't short-circuit) so the user
|
|
8
|
+
* gets a complete picture of all issues in a single checkpoint report.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { runValidation } from '../validators/validation-runner.js';
|
|
13
|
+
import { runBuildCheck } from '../build/build-runner.js';
|
|
14
|
+
import { runTestSuite } from '../tasks/test-runner.js';
|
|
15
|
+
import { persistCheckpointValidation } from '../validators/checkpoint-persistence.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Determine whether a checkpoint should run based on task count and frequency.
|
|
19
|
+
*
|
|
20
|
+
* @param {number} tasksCompleted - Number of tasks completed so far
|
|
21
|
+
* @param {number} frequency - Run every N tasks (default: 3)
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
export function shouldRunCheckpoint(tasksCompleted, frequency = 3) {
|
|
25
|
+
return tasksCompleted > 0 && tasksCompleted % frequency === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run all checkpoint hooks: static validation, build check, and test suite.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} featureName - The feature being worked on
|
|
32
|
+
* @param {number} checkpointNum - Checkpoint sequence number
|
|
33
|
+
* @param {object} [opts] - Options
|
|
34
|
+
* @param {string} [opts.projectPath] - Project root (defaults to cwd)
|
|
35
|
+
* @returns {Promise<{ passed: boolean, summary: { errors: number, warnings: number }, results: object }>}
|
|
36
|
+
*/
|
|
37
|
+
export async function runCheckpointHooks(featureName, checkpointNum, opts = {}) {
|
|
38
|
+
const projectPath = opts.projectPath ?? process.cwd();
|
|
39
|
+
const results = {};
|
|
40
|
+
let totalErrors = 0;
|
|
41
|
+
let totalWarnings = 0;
|
|
42
|
+
|
|
43
|
+
// Phase 1: Static validators
|
|
44
|
+
console.log(chalk.gray('\n Phase 1/3: Running static validators...'));
|
|
45
|
+
try {
|
|
46
|
+
const valResult = await runValidation(projectPath, featureName, { verbose: false, mode: 'full' });
|
|
47
|
+
results.validators = {
|
|
48
|
+
passed: valResult.passed,
|
|
49
|
+
errors: valResult.errors,
|
|
50
|
+
warnings: valResult.warnings,
|
|
51
|
+
details: valResult.results
|
|
52
|
+
};
|
|
53
|
+
totalErrors += valResult.errors;
|
|
54
|
+
totalWarnings += valResult.warnings;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
results.validators = { passed: false, errors: 1, warnings: 0, details: {}, error: err.message };
|
|
57
|
+
totalErrors += 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Phase 2: Build check
|
|
61
|
+
console.log(chalk.gray(' Phase 2/3: Running build check...'));
|
|
62
|
+
const buildResult = runBuildCheck(projectPath);
|
|
63
|
+
results.build = buildResult;
|
|
64
|
+
if (buildResult.skipped) {
|
|
65
|
+
console.log(chalk.gray(` Skipped: ${buildResult.reason}`));
|
|
66
|
+
} else if (buildResult.passed) {
|
|
67
|
+
console.log(chalk.green(` ✓ Build passed (${buildResult.command})`));
|
|
68
|
+
} else {
|
|
69
|
+
console.log(chalk.red(` ✗ Build failed (${buildResult.command})`));
|
|
70
|
+
totalErrors += 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Phase 3: Test suite
|
|
74
|
+
console.log(chalk.gray(' Phase 3/3: Running test suite...'));
|
|
75
|
+
const testResult = runTestSuite(projectPath);
|
|
76
|
+
results.tests = testResult;
|
|
77
|
+
if (testResult.skipped) {
|
|
78
|
+
console.log(chalk.gray(` Skipped: ${testResult.reason}`));
|
|
79
|
+
} else if (testResult.passed) {
|
|
80
|
+
console.log(chalk.green(' ✓ Tests passed'));
|
|
81
|
+
} else {
|
|
82
|
+
console.log(chalk.red(' ✗ Tests failed'));
|
|
83
|
+
totalErrors += 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Persist results
|
|
87
|
+
const summaryText = [
|
|
88
|
+
`Validators: ${results.validators?.errors || 0} errors, ${results.validators?.warnings || 0} warnings`,
|
|
89
|
+
buildResult.skipped ? 'Build: skipped' : `Build: ${buildResult.passed ? 'passed' : 'FAILED'}`,
|
|
90
|
+
testResult.skipped ? 'Tests: skipped' : `Tests: ${testResult.passed ? 'passed' : 'FAILED'}`
|
|
91
|
+
].join(' | ');
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
persistCheckpointValidation(projectPath, featureName, {
|
|
95
|
+
checkpointNum,
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
passed: totalErrors === 0,
|
|
98
|
+
validators: results,
|
|
99
|
+
summary: summaryText
|
|
100
|
+
});
|
|
101
|
+
} catch {
|
|
102
|
+
// Non-blocking — persistence failure shouldn't break the checkpoint
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
passed: totalErrors === 0,
|
|
107
|
+
summary: { errors: totalErrors, warnings: totalWarnings },
|
|
108
|
+
results
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -233,11 +233,34 @@ function buildRecapMarkdown(data) {
|
|
|
233
233
|
|
|
234
234
|
// Tasks Detail
|
|
235
235
|
if (tasksSummary.items.length > 0) {
|
|
236
|
+
const anyNotes = tasksSummary.items.some(t => t.notes?.length > 0);
|
|
236
237
|
md += `## Tasks\n\n`;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
238
|
+
if (anyNotes) {
|
|
239
|
+
md += `| ID | Title | Status | Notes |\n|----|-------|--------|-------|\n`;
|
|
240
|
+
for (const task of tasksSummary.items) {
|
|
241
|
+
const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
|
|
242
|
+
const noteCount = task.notes?.length || 0;
|
|
243
|
+
const notesCell = noteCount > 0 ? `${noteCount} note${noteCount > 1 ? 's' : ''}` : '-';
|
|
244
|
+
md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} | ${notesCell} |\n`;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
md += `| ID | Title | Status |\n|----|-------|--------|\n`;
|
|
248
|
+
for (const task of tasksSummary.items) {
|
|
249
|
+
const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
|
|
250
|
+
md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} |\n`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
md += `\n`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Deviations from Plan
|
|
257
|
+
const deviations = tasksSummary.items
|
|
258
|
+
.filter(t => t.notes?.some(n => n.type === 'deviation'))
|
|
259
|
+
.flatMap(t => t.notes.filter(n => n.type === 'deviation').map(n => ({ taskId: t.id, text: n.text })));
|
|
260
|
+
if (deviations.length > 0) {
|
|
261
|
+
md += `## Deviations from Plan\n\n`;
|
|
262
|
+
for (const d of deviations) {
|
|
263
|
+
md += `- **${d.taskId}**: ${d.text}\n`;
|
|
241
264
|
}
|
|
242
265
|
md += `\n`;
|
|
243
266
|
}
|
|
@@ -24,6 +24,7 @@ const PROFILES = {
|
|
|
24
24
|
isDotnet: true, isNextjs: false,
|
|
25
25
|
defaultValidators: ['architecture', 'packages'],
|
|
26
26
|
checkpointValidators: ['architecture', 'packages', 'design-system', 'security'],
|
|
27
|
+
buildCommand: 'dotnet build --no-restore',
|
|
27
28
|
contractsOutputType: 'contracts',
|
|
28
29
|
contractsFilename: 'contracts.cs',
|
|
29
30
|
contractsLanguage: 'csharp',
|
|
@@ -32,6 +33,7 @@ const PROFILES = {
|
|
|
32
33
|
isDotnet: false, isNextjs: true,
|
|
33
34
|
defaultValidators: ['nextjs-component'],
|
|
34
35
|
checkpointValidators: ['nextjs-component', 'css'],
|
|
36
|
+
buildCommand: 'npx tsc --noEmit',
|
|
35
37
|
contractsOutputType: 'contractsTs',
|
|
36
38
|
contractsFilename: 'contracts.ts',
|
|
37
39
|
contractsLanguage: 'typescript',
|
|
@@ -40,6 +42,7 @@ const PROFILES = {
|
|
|
40
42
|
isDotnet: false, isNextjs: false,
|
|
41
43
|
defaultValidators: [],
|
|
42
44
|
checkpointValidators: [],
|
|
45
|
+
buildCommand: null,
|
|
43
46
|
contractsOutputType: 'contracts',
|
|
44
47
|
contractsFilename: 'contracts.cs',
|
|
45
48
|
contractsLanguage: 'csharp',
|
|
@@ -122,18 +122,32 @@ export async function parseTasksMd(featureName, cwd = process.cwd()) {
|
|
|
122
122
|
return [];
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// Phase 1: collect heading positions
|
|
126
126
|
const headingRe = /^###\s+(T\d+)\s*[—–:\-]\s*(.+)$/gm;
|
|
127
|
+
const depRe = /\*\*Dependenc(?:y|ies):\*\*\s*(.+)/i;
|
|
128
|
+
const positions = [];
|
|
127
129
|
let match;
|
|
128
130
|
while ((match = headingRe.exec(content)) !== null) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
positions.push({ id: match[1], title: match[2].trim(), pos: match.index });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Phase 2: extract body and parse dependencies
|
|
135
|
+
const tasks = [];
|
|
136
|
+
for (let i = 0; i < positions.length; i++) {
|
|
137
|
+
const { id, title, pos } = positions[i];
|
|
138
|
+
const end = i + 1 < positions.length ? positions[i + 1].pos : content.length;
|
|
139
|
+
const block = content.slice(pos, end);
|
|
140
|
+
|
|
141
|
+
let dependencies = [];
|
|
142
|
+
const depMatch = depRe.exec(block);
|
|
143
|
+
if (depMatch) {
|
|
144
|
+
const val = depMatch[1].trim();
|
|
145
|
+
if (val.toLowerCase() !== 'none') {
|
|
146
|
+
dependencies = val.split(/[,\s]+/).filter(s => /^T\d+$/.test(s));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
tasks.push({ id, title, status: 'pending', dependencies, files: [], checkpoint: null });
|
|
137
151
|
}
|
|
138
152
|
return tasks;
|
|
139
153
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// src/scripts/global-install.js
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { homedir } from 'os';
|
|
5
|
-
import { mkdir, copyFile } from 'fs/promises';
|
|
4
|
+
import { homedir, platform } from 'os';
|
|
5
|
+
import { mkdir, copyFile, readFile, writeFile } from 'fs/promises';
|
|
6
6
|
import { existsSync } from 'fs';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
7
8
|
import { installGlobalStatusline } from '../utils/hooks-installer.js';
|
|
8
9
|
|
|
9
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -27,15 +28,62 @@ export async function installGlobalSkill(claudeDir = join(homedir(), '.claude'))
|
|
|
27
28
|
await copyFile(src, dest);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Ensure the npm global bin directory is in the user's PATH on Windows.
|
|
33
|
+
* Adds it to the PowerShell $PROFILE so new terminals find `morph-spec`.
|
|
34
|
+
*/
|
|
35
|
+
export async function ensureNpmGlobalInPath() {
|
|
36
|
+
if (platform() !== 'win32') return;
|
|
37
|
+
|
|
38
|
+
let npmGlobalDir;
|
|
39
|
+
try {
|
|
40
|
+
npmGlobalDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
|
41
|
+
} catch {
|
|
42
|
+
return; // can't determine npm global dir
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!npmGlobalDir || !existsSync(npmGlobalDir)) return;
|
|
46
|
+
|
|
47
|
+
// --- PowerShell $PROFILE ---
|
|
48
|
+
const psProfile = join(homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
49
|
+
const psProfileLegacy = join(homedir(), 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
50
|
+
|
|
51
|
+
const marker = '# morph-spec: ensure npm global in PATH';
|
|
52
|
+
const snippet = [
|
|
53
|
+
marker,
|
|
54
|
+
`if ($env:PATH -notlike "*${npmGlobalDir.replace(/\\/g, '\\\\')}*") {`,
|
|
55
|
+
` $env:PATH += ";${npmGlobalDir}"`,
|
|
56
|
+
'}',
|
|
57
|
+
'# end morph-spec PATH',
|
|
58
|
+
].join('\n');
|
|
59
|
+
|
|
60
|
+
for (const profilePath of [psProfile, psProfileLegacy]) {
|
|
61
|
+
try {
|
|
62
|
+
let content = '';
|
|
63
|
+
if (existsSync(profilePath)) {
|
|
64
|
+
content = await readFile(profilePath, 'utf8');
|
|
65
|
+
if (content.includes(marker)) continue; // already patched
|
|
66
|
+
} else {
|
|
67
|
+
await mkdir(dirname(profilePath), { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
const newContent = content ? `${content}\n\n${snippet}\n` : `${snippet}\n`;
|
|
70
|
+
await writeFile(profilePath, newContent, 'utf8');
|
|
71
|
+
} catch {
|
|
72
|
+
// non-critical — skip silently
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
30
77
|
// Run as postinstall script
|
|
31
78
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
32
79
|
const HOOKS_SRC = join(FRAMEWORK_DIR, 'hooks', 'claude-code');
|
|
33
80
|
const { readFileSync } = await import('fs');
|
|
34
81
|
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
35
82
|
|
|
36
|
-
const [skillResult, statuslineResult] = await Promise.allSettled([
|
|
83
|
+
const [skillResult, statuslineResult, pathResult] = await Promise.allSettled([
|
|
37
84
|
installGlobalSkill(),
|
|
38
85
|
installGlobalStatusline(HOOKS_SRC),
|
|
86
|
+
ensureNpmGlobalInPath(),
|
|
39
87
|
]);
|
|
40
88
|
|
|
41
89
|
const results = [
|
|
@@ -51,6 +99,12 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
51
99
|
path: '~/.claude/',
|
|
52
100
|
error: statuslineResult.reason?.message,
|
|
53
101
|
},
|
|
102
|
+
{
|
|
103
|
+
ok: pathResult.status === 'fulfilled',
|
|
104
|
+
label: 'PATH (npm global)',
|
|
105
|
+
path: '$PROFILE',
|
|
106
|
+
error: pathResult.reason?.message,
|
|
107
|
+
},
|
|
54
108
|
];
|
|
55
109
|
|
|
56
110
|
const { printInstallBanner } = await import('../utils/banner.js');
|
package/src/utils/file-copier.js
CHANGED
|
@@ -125,16 +125,10 @@ export function getUpdateInstructions(method) {
|
|
|
125
125
|
'morph-spec update'
|
|
126
126
|
];
|
|
127
127
|
case 'npx':
|
|
128
|
-
return [
|
|
129
|
-
'npx @polymorphism-tech/morph-spec@latest update'
|
|
130
|
-
];
|
|
131
128
|
default:
|
|
132
129
|
return [
|
|
133
130
|
'npm install -g @polymorphism-tech/morph-spec@latest',
|
|
134
|
-
'morph-spec update'
|
|
135
|
-
'',
|
|
136
|
-
'Or use npx:',
|
|
137
|
-
'npx @polymorphism-tech/morph-spec@latest update'
|
|
131
|
+
'morph-spec update'
|
|
138
132
|
];
|
|
139
133
|
}
|
|
140
134
|
}
|