@polymorphism-tech/morph-spec 4.8.1 → 4.8.4

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 (44) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/hooks/dev/guard-version-numbers.js +1 -1
  6. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
  7. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
  8. package/framework/skills/level-1-workflows/phase-design/SKILL.md +1 -1
  9. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +1 -1
  10. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
  11. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +1 -1
  12. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
  13. package/package.json +4 -4
  14. package/.morph/analytics/threads-log.jsonl +0 -54
  15. package/.morph/state.json +0 -198
  16. package/docs/ARCHITECTURE.md +0 -328
  17. package/docs/COMMAND-FLOWS.md +0 -398
  18. package/docs/plans/2026-02-22-claude-docs-morph-alignment-analysis.md +0 -514
  19. package/docs/plans/2026-02-22-claude-settings.md +0 -517
  20. package/docs/plans/2026-02-22-morph-cc-alignment-impl.md +0 -730
  21. package/docs/plans/2026-02-22-morph-spec-next.md +0 -480
  22. package/docs/plans/2026-02-22-native-alignment-design.md +0 -201
  23. package/docs/plans/2026-02-22-native-alignment-impl.md +0 -927
  24. package/docs/plans/2026-02-22-native-enrichment-design.md +0 -246
  25. package/docs/plans/2026-02-22-native-enrichment.md +0 -737
  26. package/docs/plans/2026-02-23-ddd-architecture-refactor.md +0 -1155
  27. package/docs/plans/2026-02-23-ddd-nextsteps.md +0 -684
  28. package/docs/plans/2026-02-23-infra-architect-refactor.md +0 -439
  29. package/docs/plans/2026-02-23-nextjs-code-review-design.md +0 -157
  30. package/docs/plans/2026-02-23-nextjs-code-review-impl.md +0 -1256
  31. package/docs/plans/2026-02-23-nextjs-standards-design.md +0 -150
  32. package/docs/plans/2026-02-23-nextjs-standards-impl.md +0 -1848
  33. package/docs/plans/2026-02-24-cli-radical-simplification.md +0 -592
  34. package/docs/plans/2026-02-24-framework-failure-points.md +0 -125
  35. package/docs/plans/2026-02-24-morph-init-design.md +0 -337
  36. package/docs/plans/2026-02-24-morph-init-impl.md +0 -1269
  37. package/docs/plans/2026-02-24-tutorial-command-design.md +0 -71
  38. package/docs/plans/2026-02-24-tutorial-command.md +0 -298
  39. package/scripts/bump-version.js +0 -248
  40. package/scripts/generate-refs.js +0 -336
  41. package/scripts/generate-standards-registry.js +0 -44
  42. package/scripts/install-dev-hooks.js +0 -138
  43. package/scripts/scan-nextjs.mjs +0 -169
  44. package/scripts/validate-real.mjs +0 -255
@@ -1,1269 +0,0 @@
1
- # morph-init Implementation Plan
2
-
3
- **Status:** COMPLETE
4
-
5
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
6
-
7
- **Goal:** Replace `morph-spec init` as user-facing entry point with a `/morph-init` skill that uses LLM analysis for robust project detection across any stack or layout, and auto-installs required Claude Code plugins.
8
-
9
- **Architecture:** Extract infrastructure setup from `init.js` into a headless `setup-infra.js` script. Add `install-plugin.js` to auto-install superpowers + context7 from GitHub. Create `global-install.js` postinstall to distribute the `morph-init` skill globally. Write the `morph-init` SKILL.md as the single user-facing entry point.
10
-
11
- **Key constraint:** The `morph-init` skill runs inside the *user's project*, not inside the morph-spec-framework repo. It cannot call `node src/scripts/...` directly. All script logic must be exposed as CLI subcommands (`morph-spec setup-infra`, `morph-spec install-plugin <name>`) so the skill can call them via the globally-installed `morph-spec` binary.
12
-
13
- **Tech Stack:** Node.js ESM, fs-extra, node:https, Commander.js, Claude Code skill format (YAML frontmatter + markdown)
14
-
15
- **Design doc:** `docs/plans/2026-02-24-morph-init-design.md`
16
-
17
- ---
18
-
19
- ## Task 1: Create `src/scripts/setup-infra.js` + `morph-spec setup-infra` command
20
-
21
- Extract all infrastructure logic from `init.js` into a standalone headless module, then expose it as a CLI subcommand so the `morph-init` skill can call `morph-spec setup-infra` from any user project.
22
-
23
- **Files:**
24
-
25
- - Create: `src/scripts/setup-infra.js` — pure logic, exported function
26
- - Create: `src/commands/project/setup-infra-cmd.js` — thin CLI wrapper
27
- - Modify: `bin/morph-spec.js` — register `morph-spec setup-infra`
28
- - Create: `test/scripts/setup-infra.test.js`
29
-
30
- **Step 1: Write failing tests**
31
-
32
- ```js
33
- // test/scripts/setup-infra.test.js
34
- import { test, describe, beforeEach, afterEach } from 'node:test';
35
- import assert from 'node:assert/strict';
36
- import { existsSync, readFileSync } from 'fs';
37
- import { join } from 'path';
38
- import { createTempDir, cleanupTempDir } from '../helpers/test-utils.js';
39
- import { setupInfra } from '../../src/scripts/setup-infra.js';
40
-
41
- describe('setup-infra', () => {
42
- let tempDir;
43
- beforeEach(() => { tempDir = createTempDir(); });
44
- afterEach(() => { cleanupTempDir(tempDir); });
45
-
46
- test('creates .morph/ directory structure', async () => {
47
- await setupInfra(tempDir);
48
- const dirs = ['config', 'framework', 'context', 'features', 'checkpoints', 'memory', 'archive'];
49
- for (const d of dirs) {
50
- assert.ok(existsSync(join(tempDir, '.morph', d)), `.morph/${d} must exist`);
51
- }
52
- });
53
-
54
- test('writes minimal config.json with name and version only', async () => {
55
- await setupInfra(tempDir);
56
- const config = JSON.parse(readFileSync(join(tempDir, '.morph', 'config', 'config.json'), 'utf8'));
57
- assert.ok(config.project.name, 'must have project.name');
58
- assert.ok(config.frameworkVersion, 'must have frameworkVersion');
59
- assert.strictEqual(config.project.stack, undefined, 'must NOT have stack — skill sets this');
60
- assert.strictEqual(config.project.architecture, undefined, 'must NOT have architecture — skill sets this');
61
- });
62
-
63
- test('writes placeholder context/README.md', async () => {
64
- await setupInfra(tempDir);
65
- const readme = readFileSync(join(tempDir, '.morph', 'context', 'README.md'), 'utf8');
66
- assert.ok(readme.includes('Project Context'), 'must have header');
67
- });
68
-
69
- test('installs .claude/skills/ directory', async () => {
70
- await setupInfra(tempDir);
71
- assert.ok(existsSync(join(tempDir, '.claude', 'skills')), '.claude/skills/ must exist');
72
- });
73
-
74
- test('installs .claude/agents/ directory', async () => {
75
- await setupInfra(tempDir);
76
- assert.ok(existsSync(join(tempDir, '.claude', 'agents')), '.claude/agents/ must exist');
77
- });
78
-
79
- test('installs .claude/rules/ directory', async () => {
80
- await setupInfra(tempDir);
81
- assert.ok(existsSync(join(tempDir, '.claude', 'rules')), '.claude/rules/ must exist');
82
- });
83
-
84
- test('copies framework files to .morph/framework/', async () => {
85
- await setupInfra(tempDir);
86
- assert.ok(existsSync(join(tempDir, '.morph', 'framework', 'agents.json')), 'agents.json must be copied');
87
- });
88
-
89
- test('updates .gitignore with .morph/state.json entry', async () => {
90
- await setupInfra(tempDir);
91
- // gitignore update is best-effort, only check if it runs without error
92
- });
93
-
94
- test('is idempotent — safe to run twice', async () => {
95
- await setupInfra(tempDir);
96
- await setupInfra(tempDir); // should not throw
97
- assert.ok(existsSync(join(tempDir, '.morph', 'config', 'config.json')));
98
- });
99
- });
100
- ```
101
-
102
- **Step 2: Run tests to verify they fail**
103
-
104
- ```bash
105
- cd "R:/Polymorphism Tech/repos/morph-spec-framework"
106
- node --test test/scripts/setup-infra.test.js
107
- ```
108
-
109
- Expected: FAIL — `setup-infra.js` not found
110
-
111
- **Step 3: Implement `src/scripts/setup-infra.js`**
112
-
113
- Extract steps 1–10 from `src/commands/project/init.js` (lines 54–220 approximately). Key differences from init.js:
114
-
115
- - Export a `setupInfra(targetPath)` async function (no Commander, no spinner output in test mode)
116
- - Use `process.env.MORPH_QUIET` to suppress spinner in tests
117
- - config.json: write `{ framework, frameworkVersion, project: { name } }` only — no stack/architecture
118
- - context/README.md: write the same minimal placeholder as today
119
- - Reuse ALL existing imports from init.js: `installSkills`, `installAgents`, `installDomainAgents`, `installClaudeHooks`, `installGlobalStatusline`, `copyDirectory`, `copyFile`, etc.
120
-
121
- ```js
122
- // src/scripts/setup-infra.js
123
- import { join } from 'path';
124
- import { dirname } from 'path';
125
- import { fileURLToPath } from 'url';
126
- import fs from 'fs-extra';
127
- import ora from 'ora';
128
- import { logger } from '../utils/logger.js';
129
- import {
130
- copyDirectory, copyFile, pathExists,
131
- writeJson, ensureDir, writeFile, updateGitignore
132
- } from '../utils/file-copier.js';
133
- import { saveProjectMorphVersion, getInstalledCLIVersion } from '../utils/version-checker.js';
134
- import { installClaudeHooks, installGlobalStatusline } from '../utils/claude-settings-manager.js';
135
- import { installSkills } from '../utils/skills-installer.js';
136
- import { installAgents, installDomainAgents } from '../utils/agents-installer.js';
137
-
138
- const __dirname = dirname(fileURLToPath(import.meta.url));
139
- const FRAMEWORK_DIR = join(__dirname, '..', '..', 'framework');
140
-
141
- /**
142
- * Headless infrastructure setup — no prompts, no stack detection, no MCPs.
143
- * Called by /morph-init skill and by morph-spec init (delegated).
144
- * @param {string} targetPath - Project root
145
- */
146
- export async function setupInfra(targetPath) {
147
- const quiet = process.env.MORPH_QUIET === '1';
148
- const spinner = quiet ? { text: '', start() {}, stop() {}, succeed() {}, warn() {} } : ora('Installing MORPH-SPEC infrastructure...').start();
149
-
150
- const morphPath = join(targetPath, '.morph');
151
- const configDir = join(morphPath, 'config');
152
- const frameworkDestDir = join(morphPath, 'framework');
153
- const contextDir = join(morphPath, 'context');
154
- const featuresDir = join(morphPath, 'features');
155
- const claudeDest = join(targetPath, '.claude');
156
- const dirName = targetPath.split(/[\\/]/).pop();
157
-
158
- // 1. Copy CLAUDE.md
159
- const claudeMdSrc = join(FRAMEWORK_DIR, 'CLAUDE.md');
160
- const claudeMdDest = join(targetPath, 'CLAUDE.md');
161
- if (await pathExists(claudeMdSrc)) {
162
- if (await pathExists(claudeMdDest)) {
163
- const existing = await fs.readFile(claudeMdDest, 'utf8');
164
- if (!existing.includes('MORPH-SPEC')) {
165
- await copyFile(claudeMdDest, `${claudeMdDest}.backup`);
166
- }
167
- }
168
- await copyFile(claudeMdSrc, claudeMdDest);
169
- }
170
-
171
- // 2. Create .morph structure
172
- for (const d of [configDir, frameworkDestDir, contextDir, featuresDir,
173
- join(morphPath, 'checkpoints'), join(morphPath, 'memory'), join(morphPath, 'archive')]) {
174
- await ensureDir(d);
175
- }
176
-
177
- // 3. Write minimal config.json (NO stack/architecture — skill fills these)
178
- const configPath = join(configDir, 'config.json');
179
- if (!(await pathExists(configPath))) {
180
- await writeJson(configPath, {
181
- framework: 'global',
182
- frameworkVersion: getInstalledCLIVersion(),
183
- project: { name: dirName }
184
- });
185
- }
186
-
187
- // 4. Write placeholder context/README.md
188
- const contextReadme = join(contextDir, 'README.md');
189
- if (!(await pathExists(contextReadme))) {
190
- await writeFile(contextReadme, `# ${dirName} — Project Context\n\nRun /morph-init to generate project context.\n`);
191
- }
192
-
193
- // 5. Copy framework templates
194
- const templatesSrc = join(FRAMEWORK_DIR, 'templates');
195
- if (await pathExists(templatesSrc)) {
196
- await copyDirectory(templatesSrc, join(frameworkDestDir, 'templates'));
197
- }
198
-
199
- // 6. Copy framework standards
200
- const standardsSrc = join(FRAMEWORK_DIR, 'standards');
201
- if (await pathExists(standardsSrc)) {
202
- await copyDirectory(standardsSrc, join(frameworkDestDir, 'standards'));
203
- }
204
-
205
- // 6c. Copy hooks
206
- const hooksSrc = join(FRAMEWORK_DIR, 'hooks');
207
- if (await pathExists(hooksSrc)) {
208
- await copyDirectory(hooksSrc, join(frameworkDestDir, 'hooks'));
209
- }
210
-
211
- // 7. Copy agents.json
212
- const agentsSrc = join(FRAMEWORK_DIR, 'agents.json');
213
- if (await pathExists(agentsSrc)) {
214
- await copyFile(agentsSrc, join(frameworkDestDir, 'agents.json'));
215
- }
216
-
217
- // 8. Copy commands
218
- const commandsSrc = join(FRAMEWORK_DIR, 'commands');
219
- if (await pathExists(commandsSrc)) {
220
- await copyDirectory(commandsSrc, join(claudeDest, 'commands'));
221
- }
222
-
223
- // 9a. Install rules
224
- const rulesSrc = join(FRAMEWORK_DIR, 'rules');
225
- if (await pathExists(rulesSrc)) {
226
- await copyDirectory(rulesSrc, join(claudeDest, 'rules'));
227
- }
228
-
229
- // 9b. Install skills, agents, domain agents, CLAUDE.md
230
- await installSkills(targetPath);
231
- await installAgents(targetPath, FRAMEWORK_DIR, { projectStack: null });
232
- await installDomainAgents(targetPath, FRAMEWORK_DIR);
233
- const runtimeClaudeMdSrc = join(FRAMEWORK_DIR, 'CLAUDE.md');
234
- if (await pathExists(runtimeClaudeMdSrc)) {
235
- await copyFile(runtimeClaudeMdSrc, join(claudeDest, 'CLAUDE.md'));
236
- }
237
-
238
- // 9c. Statusline (global, non-critical)
239
- try {
240
- await installGlobalStatusline(join(FRAMEWORK_DIR, 'hooks', 'claude-code'));
241
- } catch { /* non-critical */ }
242
-
243
- // 9d. Hooks
244
- await installClaudeHooks(targetPath);
245
-
246
- // 10. Version info
247
- await saveProjectMorphVersion(targetPath);
248
-
249
- // 11. .gitignore
250
- await updateGitignore(targetPath);
251
-
252
- spinner.succeed('MORPH-SPEC infrastructure installed.');
253
- return { targetPath, dirName };
254
- }
255
- ```
256
-
257
- **Step 4: Run tests**
258
-
259
- ```bash
260
- node --test test/scripts/setup-infra.test.js
261
- ```
262
-
263
- Expected: all tests PASS
264
-
265
- **Step 5: Create `src/commands/project/setup-infra-cmd.js` (thin CLI wrapper)**
266
-
267
- ```js
268
- // src/commands/project/setup-infra-cmd.js
269
- import { setupInfra } from '../../scripts/setup-infra.js';
270
- import { logger } from '../../utils/logger.js';
271
-
272
- export async function setupInfraCommand(options) {
273
- const targetPath = options.path || process.cwd();
274
- try {
275
- await setupInfra(targetPath);
276
- } catch (err) {
277
- logger.error(`setup-infra failed: ${err.message}`);
278
- process.exit(1);
279
- }
280
- }
281
- ```
282
-
283
- **Step 6: Register in `bin/morph-spec.js`**
284
-
285
- Add import at top (with other project commands):
286
- ```js
287
- import { setupInfraCommand } from '../src/commands/project/setup-infra-cmd.js';
288
- ```
289
-
290
- Add command registration after the `init` block (line ~109):
291
- ```js
292
- program
293
- .command('setup-infra')
294
- .description('Install MORPH-SPEC infrastructure (headless, no prompts). Called by /morph-init skill.')
295
- .option('-p, --path <path>', 'Target path (default: current directory)')
296
- .action(setupInfraCommand);
297
- ```
298
-
299
- **Step 7: Verify CLI command works**
300
-
301
- ```bash
302
- morph-spec setup-infra --help
303
- ```
304
- Expected: shows description and `--path` option.
305
-
306
- **Step 8: Commit**
307
-
308
- ```bash
309
- git add src/scripts/setup-infra.js src/commands/project/setup-infra-cmd.js bin/morph-spec.js test/scripts/setup-infra.test.js
310
- git commit -m "feat(cli): add setup-infra.js + morph-spec setup-infra command — headless infra extracted from init.js"
311
- ```
312
-
313
- ---
314
-
315
- ## Task 2: Create `src/scripts/install-plugin.js` + `morph-spec install-plugin` command
316
-
317
- Downloads a plugin from `anthropics/claude-plugins-official` GitHub, registers it in `~/.claude/plugins/installed_plugins.json`, and exposes the logic as a CLI subcommand callable from any user project.
318
-
319
- **Files:**
320
-
321
- - Create: `src/scripts/install-plugin.js` — pure logic
322
- - Create: `src/commands/project/install-plugin-cmd.js` — thin CLI wrapper
323
- - Modify: `bin/morph-spec.js` — register `morph-spec install-plugin <name>`
324
- - Create: `test/scripts/install-plugin.test.js`
325
-
326
- **Step 1: Write failing tests**
327
-
328
- ```js
329
- // test/scripts/install-plugin.test.js
330
- import { test, describe } from 'node:test';
331
- import assert from 'node:assert/strict';
332
- import {
333
- isPluginInstalled,
334
- buildInstallEntry,
335
- resolvePluginVersion
336
- } from '../../src/scripts/install-plugin.js';
337
-
338
- describe('install-plugin utils', () => {
339
- test('isPluginInstalled returns true when plugin key exists', () => {
340
- const plugins = {
341
- 'superpowers@claude-plugins-official': [{ scope: 'user', version: '4.3.1' }]
342
- };
343
- assert.strictEqual(isPluginInstalled(plugins, 'superpowers'), true);
344
- });
345
-
346
- test('isPluginInstalled returns false when plugin is absent', () => {
347
- assert.strictEqual(isPluginInstalled({}, 'superpowers'), false);
348
- });
349
-
350
- test('buildInstallEntry returns valid entry shape', () => {
351
- const entry = buildInstallEntry('superpowers', '/some/path', 'abc123', '4.3.1');
352
- assert.strictEqual(entry.scope, 'user');
353
- assert.strictEqual(entry.version, '4.3.1');
354
- assert.strictEqual(entry.gitCommitSha, 'abc123');
355
- assert.ok(entry.installedAt);
356
- assert.ok(entry.lastUpdated);
357
- assert.ok(entry.installPath.includes('superpowers'));
358
- });
359
-
360
- test('resolvePluginVersion returns sha string for known plugin', async () => {
361
- // Uses real GitHub API — skip in offline CI
362
- if (process.env.CI) return;
363
- const { sha } = await resolvePluginVersion('context7');
364
- assert.ok(typeof sha === 'string' && sha.length > 0, 'sha must be non-empty string');
365
- });
366
- });
367
- ```
368
-
369
- **Step 2: Run to verify failure**
370
-
371
- ```bash
372
- node --test test/scripts/install-plugin.test.js
373
- ```
374
-
375
- Expected: FAIL — module not found
376
-
377
- **Step 3: Implement `src/scripts/install-plugin.js`**
378
-
379
- ```js
380
- // src/scripts/install-plugin.js
381
- import { join } from 'path';
382
- import { homedir } from 'os';
383
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
384
- import { mkdir, writeFile } from 'fs/promises';
385
- import https from 'node:https';
386
-
387
- const PLUGINS_DIR = join(homedir(), '.claude', 'plugins');
388
- const CACHE_DIR = join(PLUGINS_DIR, 'cache', 'claude-plugins-official');
389
- const INSTALLED_FILE = join(PLUGINS_DIR, 'installed_plugins.json');
390
- const GH_REPO = 'anthropics/claude-plugins-official';
391
- const GH_API = 'https://api.github.com';
392
-
393
- /** Fetch JSON from GitHub API */
394
- async function fetchGitHub(path) {
395
- return new Promise((resolve, reject) => {
396
- const opts = {
397
- hostname: 'api.github.com',
398
- path,
399
- headers: { 'User-Agent': 'morph-spec', 'Accept': 'application/vnd.github.v3+json' }
400
- };
401
- https.get(opts, (res) => {
402
- let data = '';
403
- res.on('data', c => data += c);
404
- res.on('end', () => {
405
- try { resolve(JSON.parse(data)); }
406
- catch (e) { reject(new Error(`GitHub API parse error: ${data.slice(0, 200)}`)); }
407
- });
408
- }).on('error', reject);
409
- });
410
- }
411
-
412
- /** Download raw file from GitHub */
413
- async function downloadFile(downloadUrl) {
414
- return new Promise((resolve, reject) => {
415
- https.get(downloadUrl, { headers: { 'User-Agent': 'morph-spec' } }, (res) => {
416
- // Follow redirects
417
- if (res.statusCode === 302 || res.statusCode === 301) {
418
- const url = new URL(res.headers.location);
419
- https.get({ hostname: url.hostname, path: url.pathname + url.search,
420
- headers: { 'User-Agent': 'morph-spec' } }, (res2) => {
421
- const chunks = [];
422
- res2.on('data', c => chunks.push(c));
423
- res2.on('end', () => resolve(Buffer.concat(chunks)));
424
- }).on('error', reject);
425
- return;
426
- }
427
- const chunks = [];
428
- res.on('data', c => chunks.push(c));
429
- res.on('end', () => resolve(Buffer.concat(chunks)));
430
- }).on('error', reject);
431
- });
432
- }
433
-
434
- /** Read installed_plugins.json, return parsed object */
435
- function readInstalledPlugins() {
436
- if (!existsSync(INSTALLED_FILE)) return { version: 2, plugins: {} };
437
- try { return JSON.parse(readFileSync(INSTALLED_FILE, 'utf8')); }
438
- catch { return { version: 2, plugins: {} }; }
439
- }
440
-
441
- /** Check if plugin is already installed */
442
- export function isPluginInstalled(plugins, name) {
443
- return `${name}@claude-plugins-official` in plugins;
444
- }
445
-
446
- /** Resolve plugin version + commit SHA from GitHub */
447
- export async function resolvePluginVersion(pluginName) {
448
- const commits = await fetchGitHub(`/repos/${GH_REPO}/commits?path=${pluginName}&per_page=1`);
449
- if (!Array.isArray(commits) || commits.length === 0) {
450
- throw new Error(`Could not resolve version for plugin: ${pluginName}`);
451
- }
452
- const sha = commits[0].sha;
453
- const shortSha = sha.slice(0, 12);
454
- return { sha, shortSha };
455
- }
456
-
457
- /** Build installed_plugins.json entry */
458
- export function buildInstallEntry(pluginName, installPath, gitCommitSha, version) {
459
- const now = new Date().toISOString();
460
- return { scope: 'user', installPath, version, installedAt: now, lastUpdated: now, gitCommitSha };
461
- }
462
-
463
- /** Download plugin directory recursively from GitHub */
464
- async function downloadPluginFiles(pluginName, sha, destDir) {
465
- const tree = await fetchGitHub(`/repos/${GH_REPO}/git/trees/${sha}?recursive=1`);
466
- if (!tree.tree) throw new Error('GitHub tree API returned unexpected shape');
467
-
468
- const pluginFiles = tree.tree.filter(
469
- f => f.path.startsWith(`${pluginName}/`) && f.type === 'blob'
470
- );
471
-
472
- if (pluginFiles.length === 0) {
473
- throw new Error(`Plugin "${pluginName}" not found in repository`);
474
- }
475
-
476
- for (const file of pluginFiles) {
477
- const relativePath = file.path.slice(pluginName.length + 1); // strip "pluginName/"
478
- const destPath = join(destDir, relativePath);
479
- const destParent = join(destPath, '..');
480
- await mkdir(destParent, { recursive: true });
481
-
482
- const content = await downloadFile(
483
- `https://raw.githubusercontent.com/${GH_REPO}/${sha}/${file.path}`
484
- );
485
- await writeFile(destPath, content);
486
- }
487
- }
488
-
489
- /**
490
- * Install a Claude Code plugin from GitHub.
491
- * @param {string} pluginName - e.g. "superpowers"
492
- * @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
493
- */
494
- export async function installPlugin(pluginName) {
495
- const data = readInstalledPlugins();
496
-
497
- if (isPluginInstalled(data.plugins, pluginName)) {
498
- return { installed: true, alreadyInstalled: true, version: 'existing' };
499
- }
500
-
501
- const { sha, shortSha } = await resolvePluginVersion(pluginName);
502
- const installPath = join(CACHE_DIR, pluginName, shortSha);
503
-
504
- await mkdir(installPath, { recursive: true });
505
- await downloadPluginFiles(pluginName, sha, installPath);
506
-
507
- const entry = buildInstallEntry(pluginName, installPath, sha, shortSha);
508
- data.plugins[`${pluginName}@claude-plugins-official`] = [entry];
509
-
510
- mkdirSync(PLUGINS_DIR, { recursive: true });
511
- writeFileSync(INSTALLED_FILE, JSON.stringify(data, null, 2));
512
-
513
- return { installed: true, alreadyInstalled: false, version: shortSha };
514
- }
515
- ```
516
-
517
- **Step 4: Run tests**
518
-
519
- ```bash
520
- node --test test/scripts/install-plugin.test.js
521
- ```
522
-
523
- Expected: unit tests PASS (resolvePluginVersion skipped in CI)
524
-
525
- **Step 5: Create `src/commands/project/install-plugin-cmd.js` (thin CLI wrapper)**
526
-
527
- ```js
528
- // src/commands/project/install-plugin-cmd.js
529
- import { installPlugin } from '../../scripts/install-plugin.js';
530
- import { logger } from '../../utils/logger.js';
531
-
532
- export async function installPluginCommand(pluginName, _options) {
533
- if (!pluginName) {
534
- logger.error('Plugin name required. Usage: morph-spec install-plugin <name>');
535
- logger.dim(' Available: superpowers, context7');
536
- process.exit(1);
537
- }
538
- try {
539
- const result = await installPlugin(pluginName);
540
- if (result.alreadyInstalled) {
541
- logger.dim(` ✓ ${pluginName} already installed (${result.version})`);
542
- } else {
543
- logger.success(` ✓ ${pluginName} installed (${result.version})`);
544
- logger.dim(' Restart Claude Code to activate.');
545
- }
546
- } catch (err) {
547
- logger.error(`Failed to install plugin "${pluginName}": ${err.message}`);
548
- logger.blank();
549
- logger.warn('Manual installation:');
550
- logger.dim(' 1. Claude Code → Settings (Cmd/Ctrl+,) → Extensions');
551
- logger.dim(` 2. Browse → search "${pluginName}" → Install`);
552
- logger.dim(' 3. Restart Claude Code');
553
- process.exit(1);
554
- }
555
- }
556
- ```
557
-
558
- **Step 6: Register in `bin/morph-spec.js`**
559
-
560
- Add import at top (with other project commands):
561
- ```js
562
- import { installPluginCommand } from '../src/commands/project/install-plugin-cmd.js';
563
- ```
564
-
565
- Add command registration after `setup-infra` block:
566
- ```js
567
- program
568
- .command('install-plugin <name>')
569
- .description('Install a Claude Code plugin (superpowers, context7). Called by /morph-init skill.')
570
- .action(installPluginCommand);
571
- ```
572
-
573
- **Step 7: Verify CLI command works**
574
-
575
- ```bash
576
- morph-spec install-plugin --help
577
- morph-spec install-plugin superpowers
578
- ```
579
- Expected: help shows `<name>` argument; install succeeds or prints manual guide on failure.
580
-
581
- **Step 8: Commit**
582
-
583
- ```bash
584
- git add src/scripts/install-plugin.js src/commands/project/install-plugin-cmd.js bin/morph-spec.js test/scripts/install-plugin.test.js
585
- git commit -m "feat(cli): add install-plugin.js + morph-spec install-plugin command — auto-install CC plugins from GitHub"
586
- ```
587
-
588
- ---
589
-
590
- ## Task 3: Create `src/scripts/global-install.js` + package.json postinstall
591
-
592
- Copies `morph-init/SKILL.md` to `~/.claude/skills/morph-init/SKILL.md` on npm install.
593
-
594
- **Files:**
595
-
596
- - Create: `src/scripts/global-install.js`
597
- - Create: `test/scripts/global-install.test.js`
598
- - Modify: `package.json`
599
-
600
- **Step 1: Write failing tests**
601
-
602
- ```js
603
- // test/scripts/global-install.test.js
604
- import { test, describe, beforeEach, afterEach } from 'node:test';
605
- import assert from 'node:assert/strict';
606
- import { existsSync } from 'fs';
607
- import { join } from 'path';
608
- import { createTempDir, cleanupTempDir } from '../helpers/test-utils.js';
609
- import { installGlobalSkill } from '../../src/scripts/global-install.js';
610
-
611
- describe('global-install', () => {
612
- let tempGlobalDir;
613
- beforeEach(() => { tempGlobalDir = createTempDir(); });
614
- afterEach(() => { cleanupTempDir(tempGlobalDir); });
615
-
616
- test('copies morph-init/SKILL.md to target global skills dir', async () => {
617
- await installGlobalSkill(tempGlobalDir);
618
- const skillPath = join(tempGlobalDir, 'skills', 'morph-init', 'SKILL.md');
619
- assert.ok(existsSync(skillPath), 'morph-init/SKILL.md must be copied to global skills dir');
620
- });
621
-
622
- test('is idempotent — safe to run twice', async () => {
623
- await installGlobalSkill(tempGlobalDir);
624
- await installGlobalSkill(tempGlobalDir);
625
- assert.ok(existsSync(join(tempGlobalDir, 'skills', 'morph-init', 'SKILL.md')));
626
- });
627
- });
628
- ```
629
-
630
- **Step 2: Run to verify failure**
631
-
632
- ```bash
633
- node --test test/scripts/global-install.test.js
634
- ```
635
-
636
- Expected: FAIL — module not found (and `morph-init/SKILL.md` doesn't exist yet — that's Task 4)
637
-
638
- **Step 3: Implement `src/scripts/global-install.js`**
639
-
640
- ```js
641
- // src/scripts/global-install.js
642
- import { join, dirname } from 'path';
643
- import { fileURLToPath } from 'url';
644
- import { homedir } from 'os';
645
- import { mkdir, copyFile, writeFile } from 'fs/promises';
646
- import { existsSync } from 'fs';
647
-
648
- const __dirname = dirname(fileURLToPath(import.meta.url));
649
- const FRAMEWORK_DIR = join(__dirname, '..', '..', 'framework');
650
-
651
- /**
652
- * Install morph-init skill to a target global Claude dir.
653
- * @param {string} claudeDir - defaults to ~/.claude/
654
- */
655
- export async function installGlobalSkill(claudeDir = join(homedir(), '.claude')) {
656
- const src = join(FRAMEWORK_DIR, 'skills', 'level-0-meta', 'morph-init', 'SKILL.md');
657
- const destDir = join(claudeDir, 'skills', 'morph-init');
658
- const dest = join(destDir, 'SKILL.md');
659
-
660
- if (!existsSync(src)) {
661
- console.warn(`morph-init skill not found at ${src} — skipping global install`);
662
- return;
663
- }
664
-
665
- await mkdir(destDir, { recursive: true });
666
- await copyFile(src, dest);
667
- }
668
-
669
- // Entrypoint when run as postinstall script
670
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
671
- installGlobalSkill()
672
- .then(() => console.log('✓ morph-init skill installed to ~/.claude/skills/'))
673
- .catch(e => console.warn('Could not install morph-init skill globally:', e.message));
674
- }
675
- ```
676
-
677
- **Step 4: Add postinstall to `package.json`**
678
-
679
- In `package.json`, add to `"scripts"`:
680
-
681
- ```json
682
- "postinstall": "node src/scripts/global-install.js"
683
- ```
684
-
685
- **Step 5: Run tests** (after Task 4 creates the SKILL.md)
686
-
687
- ```bash
688
- node --test test/scripts/global-install.test.js
689
- ```
690
-
691
- Expected: PASS after Task 4
692
-
693
- **Step 6: Commit**
694
-
695
- ```bash
696
- git add src/scripts/global-install.js test/scripts/global-install.test.js package.json
697
- git commit -m "feat(scripts): add global-install.js — postinstall copies morph-init skill to ~/.claude/skills/"
698
- ```
699
-
700
- ---
701
-
702
- ## Task 4: `framework/skills/level-0-meta/morph-init/SKILL.md`
703
-
704
- The main user-facing skill. Already created — verify it uses the correct CLI commands.
705
-
706
- **Key constraint:** All `morph-spec` CLI calls in the skill work in any user project because `morph-spec` is globally installed. Do NOT reference `node src/scripts/...` or any path relative to the framework repo.
707
-
708
- **Files:**
709
-
710
- - Verify: `framework/skills/level-0-meta/morph-init/SKILL.md` — already exists
711
-
712
- **Step 1: Verify correct CLI commands in SKILL.md**
713
-
714
- The skill must use:
715
- ```bash
716
- morph-spec install-plugin superpowers # ✓ CLI subcommand — works anywhere
717
- morph-spec install-plugin context7 # ✓ CLI subcommand — works anywhere
718
- morph-spec setup-infra # ✓ CLI subcommand — works anywhere
719
- ```
720
-
721
- Not:
722
- ```bash
723
- node {MORPH_ROOT}/src/scripts/... # ✗ path only exists in framework repo
724
- npx morph-spec ... # ✗ may download wrong version
725
- ```
726
-
727
- Read the file and confirm:
728
- ```bash
729
- grep -n "morph-spec\|node.*scripts\|MORPH_ROOT" framework/skills/level-0-meta/morph-init/SKILL.md
730
- ```
731
- Expected: only `morph-spec install-plugin` and `morph-spec setup-infra` patterns.
732
-
733
- **Step 2 (previously Step 1): The skill file content is**
734
-
735
- ```markdown
736
- ---
737
- name: morph-init
738
- description: >
739
- LLM-powered project initialization. Installs morph-spec infrastructure if
740
- needed, auto-installs required plugins (superpowers, context7), analyzes any
741
- project structure intelligently, generates rich context/README.md and
742
- config.json, and configures MCPs. Use once per project after installing
743
- @polymorphism-tech/morph-spec. Re-run with /morph-init refresh to update
744
- context as the project evolves.
745
- argument-hint: "[refresh]"
746
- user-invocable: true
747
- allowed-tools: Read, Write, Edit, Bash, Glob, Grep
748
- ---
749
-
750
- # morph-init — LLM-Powered Project Initialization
751
-
752
- > Run once after `npm install @polymorphism-tech/morph-spec`.
753
- > Re-run as `/morph-init refresh` when your project evolves.
754
-
755
- ---
756
-
757
- ## Step 0 — Required Plugins
758
-
759
- Check for required Claude Code plugins: **superpowers** and **context7**.
760
-
761
- ```bash
762
- # Detect morph-spec package root
763
- node -e "const r=require.resolve('@polymorphism-tech/morph-spec/package.json'); console.log(require('path').dirname(r))"
764
- ```
765
-
766
- Then check `~/.claude/plugins/installed_plugins.json` for:
767
-
768
- - `superpowers@claude-plugins-official`
769
- - `context7@claude-plugins-official`
770
-
771
- For each missing plugin, run:
772
-
773
- ```bash
774
- node {MORPH_ROOT}/src/scripts/install-plugin.js {plugin-name}
775
- ```
776
-
777
- **If auto-install succeeds:** `✓ {plugin} installed.`
778
-
779
- **If auto-install fails:** Show this and STOP:
780
-
781
- ```
782
- ┌─────────────────────────────────────────────────────────────┐
783
- │ Plugin {plugin} requires manual installation: │
784
- │ │
785
- │ 1. Claude Code → Settings (Cmd/Ctrl+,) → Extensions │
786
- │ 2. Browse → search "{plugin}" → Install │
787
- │ 3. Restart Claude Code │
788
- │ 4. Re-run /morph-init │
789
- └─────────────────────────────────────────────────────────────┘
790
- ```
791
-
792
- Do not continue if either plugin is missing or failed to install.
793
-
794
- ---
795
-
796
- ## Step 1 — Infrastructure
797
-
798
- Check if `.morph/` exists in the current directory.
799
-
800
- **If MISSING:**
801
-
802
- ```bash
803
- npx morph-spec setup-infra
804
- ```
805
-
806
- Output: `✓ Infrastructure installed`
807
-
808
- **If EXISTS and argument is "refresh":** Continue.
809
-
810
- **If EXISTS and no argument:**
811
- Ask: *"MORPH already initialized. Refresh context and config? (y/n)"*
812
- If `n` → STOP.
813
-
814
- ---
815
-
816
- ## Step 2 — Analyze Project
817
-
818
- Gather evidence to build a stack map. Run these searches:
819
-
820
-
821
- | Tool | What to look for |
822
- | --------------------------------- | ---------------------------------------------------- |
823
- | `Glob **/package.json` | Detect next, supabase, clerk, stripe, shadcn in deps |
824
- | `Glob **/*.csproj` | .NET projects at any depth |
825
- | `Glob **/next.config.{js,ts,mjs}` | Next.js in any subdirectory |
826
- | `Glob **/components.json` | shadcn/ui |
827
- | `Glob **/docker-compose*.yml` | Container services |
828
- | `Glob database/migrations/`** | Supabase local dev |
829
- | `Glob supabase/**` | Supabase project config |
830
- | `Glob **/*.razor` | Blazor |
831
- | `Read .env.example` | Env vars reveal integrations |
832
- | `Read README.md` | Existing project description |
833
- | `Grep @supabase/supabase-js` | Supabase dependency |
834
- | `Grep clerk|auth0|nextauth` | Auth provider |
835
- | `Grep stripe|asaas` | Payment provider |
836
-
837
-
838
- Read each found `package.json` to extract dependency names.
839
-
840
- Build an **evidence map**:
841
-
842
- ```
843
- stack: [detected stacks]
844
- integrations: [detected services]
845
- uiLibrary: [shadcn | fluent-ui | mudblazor | tailwind | null]
846
- monorepo: [true | false]
847
- frontendPath: [path if monorepo]
848
- backendPath: [path if monorepo]
849
- ```
850
-
851
- ---
852
-
853
- ## Step 3 — Ask Targeted Questions
854
-
855
- **Rule: only ask what cannot be inferred with ≥90% confidence from files.**
856
-
857
- Ask at most 3 questions, only those that apply:
858
-
859
- 1. **(Always)** "Em uma frase: qual é o objetivo principal do **[ProjectName]**?" - use askUser tool and suggest 3 + one to user input
860
- 2. **(If monorepo detected)** "Confirmo: frontend em `{frontendPath}`, backend em `{backendPath}`. Correto?"
861
- — If no: "Como está organizado? Descreva brevemente."
862
- 3. **(If Supabase detected)** "Supabase Cloud (managed) ou self-hosted?"
863
-
864
- Do **not** ask about technology confirmed by file evidence.
865
-
866
- ---
867
-
868
- ## Step 4 — Generate `context/README.md`
869
-
870
- Write `.morph/context/README.md` using the evidence map and answers:
871
-
872
- ```markdown
873
- # [ProjectName] — Project Context
874
-
875
- ## Overview
876
- [Answer from question 1]
877
-
878
- ## Tech Stack
879
- | Layer | Technology | Location |
880
- |------------|-----------|----------|
881
- [One row per detected layer]
882
-
883
- ## Architecture
884
- [monorepo / single-stack / etc — describe structure and responsibilities]
885
-
886
- ## Key Integrations
887
- [service → purpose — detected via: evidence]
888
-
889
- ## Agent Notes
890
- [Patterns detected from eslint/tsconfig/.editorconfig, important paths,
891
- conventions relevant to MORPH agents working on this project]
892
- ```
893
-
894
- ---
895
-
896
- ## Step 5 — Update `config.json`
897
-
898
- Read `.morph/config/config.json` and update `project` section:
899
-
900
- ```json
901
- {
902
- "framework": "global",
903
- "frameworkVersion": "[current version]",
904
- "project": {
905
- "name": "[ProjectName]",
906
- "description": "[answer from question 1]",
907
- "stack": "[detected stacks joined with +]",
908
- "architecture": "[monorepo | single-stack | detected pattern]",
909
- "uiLibrary": "[detected or null]",
910
- "integrations": ["[list of detected integrations]"],
911
- "frontendPath": "[if monorepo]",
912
- "backendPath": "[if monorepo]"
913
- }
914
- }
915
- ```
916
-
917
- ---
918
-
919
- ## Step 6 — Configure MCPs
920
-
921
- For each detected integration with an available MCP:
922
-
923
- **Supabase:**
924
-
925
- > "Configure Supabase MCP now? I'll need `SUPABASE_URL` and `SERVICE_ROLE_KEY`."
926
-
927
- - YES: collect credentials → add to `.claude/settings.local.json` under `mcpServers`
928
- - NO: show snippet + `morph-spec mcp setup supabase`
929
-
930
- **GitHub:**
931
-
932
- > "Configure GitHub MCP? I'll need a `GITHUB_PERSONAL_ACCESS_TOKEN`."
933
-
934
- - YES/NO: same pattern
935
-
936
- Only ask about Figma, Docker, Azure if explicitly detected in `.env.example`.
937
-
938
- ---
939
-
940
- ## Step 7 — Final Output
941
-
942
- Print summary:
943
-
944
- ```
945
- ✓ Plugins: superpowers ✓ context7 ✓
946
- ✓ Infrastructure: .morph/ structure, hooks, agents, rules, skills
947
- ✓ context/README.md generated
948
- ✓ config.json updated
949
- Stack: [stack] | Architecture: [architecture]
950
- Integrations: [integrations]
951
- ✓ MCPs: [configured]
952
-
953
- [If any plugins were newly installed:]
954
- ○ Restart Claude Code to activate newly installed plugins.
955
-
956
- Next: /morph-proposal [feature-name]
957
- ```
958
-
959
- ```
960
-
961
- **Step 2: Verify file is valid**
962
-
963
- ```bash
964
- # Verify YAML frontmatter parses correctly
965
- node -e "
966
- const fs = require('fs');
967
- const content = fs.readFileSync('framework/skills/level-0-meta/morph-init/SKILL.md', 'utf8');
968
- const match = content.match(/^---\n([\s\S]*?)\n---/);
969
- console.log(match ? 'YAML frontmatter present ✓' : 'MISSING frontmatter ✗');
970
- console.log('File size:', content.length, 'bytes');
971
- "
972
- ```
973
-
974
- **Step 3: Update `skills-installer.js` to include morph-init**
975
-
976
- In `src/utils/skills-installer.js`, verify that `morph-init` is picked up from `level-0-meta/` — it should be included automatically if the installer scans all subdirectories. Confirm with:
977
-
978
- ```bash
979
- node -e "
980
- import { installSkills } from './src/utils/skills-installer.js';
981
- " 2>/dev/null || node --input-type=module <<'EOF'
982
- import { createRequire } from 'module';
983
- import { join, dirname } from 'path';
984
- import { fileURLToPath } from 'url';
985
- // Just verify the SKILL.md exists at the right path
986
- import { existsSync } from 'fs';
987
- const p = 'framework/skills/level-0-meta/morph-init/SKILL.md';
988
- console.log(existsSync(p) ? `✓ ${p} exists` : `✗ ${p} MISSING`);
989
- EOF
990
- ```
991
-
992
- **Step 4: Run global-install tests (from Task 3)**
993
-
994
- ```bash
995
- node --test test/scripts/global-install.test.js
996
- ```
997
-
998
- Expected: PASS now that SKILL.md exists
999
-
1000
- **Step 5: Commit**
1001
-
1002
- ```bash
1003
- git add framework/skills/level-0-meta/morph-init/SKILL.md
1004
- git commit -m "feat(skills): add morph-init skill — LLM-powered project initialization entry point"
1005
- ```
1006
-
1007
- ---
1008
-
1009
- ## Task 5: Refactor `init.js` to delegate to `setup-infra.js`
1010
-
1011
- `init.js` keeps its interactive flow but delegates infrastructure work to `setupInfra()`. Remove duplicate code.
1012
-
1013
- **Files:**
1014
-
1015
- - Modify: `src/commands/project/init.js`
1016
- - Review: `test/commands/init.test.js` (should still pass)
1017
-
1018
- **Step 1: Run existing init tests to establish baseline**
1019
-
1020
- ```bash
1021
- node --test test/commands/init.test.js
1022
- ```
1023
-
1024
- Note how many tests pass. Target: same count after refactor.
1025
-
1026
- **Step 2: Refactor `init.js`**
1027
-
1028
- Replace steps 1–10 in `init.js` (lines 54–220 approximately) with a single call:
1029
-
1030
- ```js
1031
- import { setupInfra } from '../../scripts/setup-infra.js';
1032
-
1033
- // Replace steps 1-10 with:
1034
- spinner.stop();
1035
- await setupInfra(targetPath);
1036
- spinner.start('Continuing...');
1037
-
1038
- // Then keep: steps 11b (Claude config detection), 11c (stack detection → update config),
1039
- // 11d (MCP setup), final output
1040
- ```
1041
-
1042
- The stack detection block (step 11c) still runs — it now UPDATES the config.json that `setupInfra` created. The key difference: `setupInfra` writes a minimal config first, then `init.js` enriches it.
1043
-
1044
- **Step 3: Run init tests**
1045
-
1046
- ```bash
1047
- node --test test/commands/init.test.js
1048
- ```
1049
-
1050
- Expected: same pass count as baseline
1051
-
1052
- **Step 4: Fix `structure-detector.js` — Next.js subdirectory patterns**
1053
-
1054
- In `src/lib/detectors/structure-detector.js`, change the `nextjs` patterns in `detectStack()`:
1055
-
1056
- ```js
1057
- // BEFORE (line 60-65):
1058
- nextjs: [
1059
- 'next.config.js',
1060
- 'next.config.mjs',
1061
- 'next.config.ts',
1062
- 'pages/**/*.tsx',
1063
- 'app/**/*.tsx'
1064
- ],
1065
-
1066
- // AFTER:
1067
- nextjs: [
1068
- '**/next.config.js',
1069
- '**/next.config.mjs',
1070
- '**/next.config.ts',
1071
- 'pages/**/*.tsx',
1072
- 'app/**/*.tsx',
1073
- '**/pages/**/*.tsx',
1074
- '**/app/**/*.tsx'
1075
- ],
1076
- ```
1077
-
1078
- **Step 5: Verify ProspectPRO detection works**
1079
-
1080
- ```bash
1081
- node -e "
1082
- import { detectStructure } from './src/lib/detectors/structure-detector.js';
1083
- " 2>/dev/null || node --input-type=module <<'EOF'
1084
- import { detectStructure } from './src/lib/detectors/structure-detector.js';
1085
- const result = await detectStructure('R:/Polymorphism Tech/repos/ProspectPRO');
1086
- console.log('Stack:', result.stack); // expect: nextjs
1087
- console.log('Architecture:', result.architecture);
1088
- EOF
1089
- ```
1090
-
1091
- Expected: stack = `nextjs` (no longer `dotnet`)
1092
-
1093
- **Step 6: Commit**
1094
-
1095
- ```bash
1096
- git add src/commands/project/init.js src/lib/detectors/structure-detector.js
1097
- git commit -m "refactor(init): delegate infrastructure to setup-infra.js; fix nextjs subdirectory detection"
1098
- ```
1099
-
1100
- ---
1101
-
1102
- ## Task 6: Add plugin checks to `morph-spec doctor`
1103
-
1104
- **Files:**
1105
-
1106
- - Modify: `src/commands/project/doctor.js`
1107
- - Modify: `test/commands/doctor.test.js`
1108
-
1109
- **Step 1: Write failing tests**
1110
-
1111
- In `test/commands/doctor.test.js`, add to the existing test suite:
1112
-
1113
- ```js
1114
- describe('doctor plugin checks', () => {
1115
- test('checkInstalledPlugins returns present for superpowers when in installed_plugins.json', async () => {
1116
- const { checkInstalledPlugins } = await import('../../src/commands/project/doctor.js');
1117
- const fakePlugins = {
1118
- 'superpowers@claude-plugins-official': [{ scope: 'user', version: '4.3.1' }],
1119
- 'context7@claude-plugins-official': [{ scope: 'user', version: 'abc123' }]
1120
- };
1121
- const result = checkInstalledPlugins(fakePlugins);
1122
- assert.strictEqual(result.superpowers, true);
1123
- assert.strictEqual(result.context7, true);
1124
- });
1125
-
1126
- test('checkInstalledPlugins returns false for missing plugin', async () => {
1127
- const { checkInstalledPlugins } = await import('../../src/commands/project/doctor.js');
1128
- const result = checkInstalledPlugins({ 'superpowers@claude-plugins-official': [] });
1129
- assert.strictEqual(result.superpowers, true);
1130
- assert.strictEqual(result.context7, false);
1131
- });
1132
- });
1133
- ```
1134
-
1135
- **Step 2: Run to verify failure**
1136
-
1137
- ```bash
1138
- node --test test/commands/doctor.test.js 2>&1 | tail -20
1139
- ```
1140
-
1141
- Expected: new tests FAIL — `checkInstalledPlugins` not exported
1142
-
1143
- **Step 3: Add `checkInstalledPlugins` and plugin check section to `doctor.js`**
1144
-
1145
- Add exported function:
1146
-
1147
- ```js
1148
- export function checkInstalledPlugins(pluginsData) {
1149
- const REQUIRED = ['superpowers', 'context7'];
1150
- return Object.fromEntries(
1151
- REQUIRED.map(name => [name, `${name}@claude-plugins-official` in pluginsData])
1152
- );
1153
- }
1154
- ```
1155
-
1156
- In `doctorProjectCommand()` function, after the skills/agents checks block, add:
1157
-
1158
- ```js
1159
- // ── Claude Code Plugins ──────────────────────────────────────────────────
1160
- console.log(chalk.cyan('\n Claude Code Plugins (required)'));
1161
- const pluginsFile = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
1162
- let pluginData = { plugins: {} };
1163
- try {
1164
- pluginData = JSON.parse(await fs.readFile(pluginsFile, 'utf8'));
1165
- } catch { /* file may not exist */ }
1166
- const pluginStatus = checkInstalledPlugins(pluginData.plugins || {});
1167
- check('superpowers plugin', pluginStatus.superpowers, false,
1168
- 'Install via: Claude Code → Settings → Extensions → superpowers');
1169
- check('context7 plugin', pluginStatus.context7, false,
1170
- 'Install via: Claude Code → Settings → Extensions → context7. Or run /morph-init');
1171
- ```
1172
-
1173
- **Step 4: Run tests**
1174
-
1175
- ```bash
1176
- node --test test/commands/doctor.test.js
1177
- ```
1178
-
1179
- Expected: all tests PASS
1180
-
1181
- **Step 5: Commit**
1182
-
1183
- ```bash
1184
- git add src/commands/project/doctor.js test/commands/doctor.test.js
1185
- git commit -m "feat(doctor): add Claude Code plugin checks for superpowers and context7"
1186
- ```
1187
-
1188
- ---
1189
-
1190
- ## Task 7: Full test suite + final validation
1191
-
1192
- **Step 1: Run full test suite**
1193
-
1194
- ```bash
1195
- cd "R:/Polymorphism Tech/repos/morph-spec-framework"
1196
- node --test --test-concurrency=1 2>&1 | tail -20
1197
- ```
1198
-
1199
- Expected: all existing tests pass + new tests pass. Zero failures.
1200
-
1201
- **Step 2: Verify `morph-init` skill is picked up by skills-installer**
1202
-
1203
- ```bash
1204
- node --input-type=module <<'EOF'
1205
- import { installSkills } from './src/utils/skills-installer.js';
1206
- import { createTempDir, cleanupTempDir } from './test/helpers/test-utils.js';
1207
- import { existsSync } from 'fs';
1208
- import { join } from 'path';
1209
- const tmp = createTempDir();
1210
- await installSkills(tmp);
1211
- const p = join(tmp, '.claude', 'skills', 'morph-init', 'SKILL.md');
1212
- console.log(existsSync(p) ? '✓ morph-init installed by skills-installer' : '✗ MISSING');
1213
- cleanupTempDir(tmp);
1214
- EOF
1215
- ```
1216
-
1217
- **Step 3: Manual smoke test — simulate `/morph-init` on ProspectPRO**
1218
-
1219
- Open Claude Code in `R:/Polymorphism Tech/repos/ProspectPRO` and run:
1220
-
1221
- ```
1222
- /morph-init refresh
1223
- ```
1224
-
1225
- Verify:
1226
-
1227
- - Plugins detected as already installed (superpowers + context7)
1228
- - `.morph/` infrastructure confirmed present
1229
- - Project analyzed: detects nextjs + dotnet + supabase
1230
- - 3 targeted questions asked (not more)
1231
- - `.morph/context/README.md` contains rich content (no `(To be detected)`)
1232
- - `.morph/config/config.json` has `stack: "nextjs+dotnet"`, `integrations: ["supabase", ...]`
1233
- - MCP setup offered for Supabase
1234
-
1235
- **Step 4: Final commit**
1236
-
1237
- ```bash
1238
- git add .
1239
- git commit -m "feat(morph-init): complete LLM-powered initialization system
1240
-
1241
- - setup-infra.js: headless infrastructure extracted from init.js
1242
- - install-plugin.js: auto-install superpowers/context7 from GitHub
1243
- - global-install.js: postinstall copies morph-init skill globally
1244
- - morph-init/SKILL.md: single user-facing entry point
1245
- - init.js: refactored to delegate to setup-infra
1246
- - doctor.js: plugin presence checks
1247
- - structure-detector.js: nextjs subdirectory detection fixed"
1248
- ```
1249
-
1250
- ---
1251
-
1252
- ## Summary
1253
-
1254
-
1255
- | Task | New Files / Changes | Tests |
1256
- |------|--------------------|----|
1257
- | 1 | `src/scripts/setup-infra.js` + `src/commands/project/setup-infra-cmd.js` + `bin/morph-spec.js` (`morph-spec setup-infra`) | `test/scripts/setup-infra.test.js` (9 tests) |
1258
- | 2 | `src/scripts/install-plugin.js` + `src/commands/project/install-plugin-cmd.js` + `bin/morph-spec.js` (`morph-spec install-plugin <name>`) | `test/scripts/install-plugin.test.js` (4 tests) |
1259
- | 3 | `src/scripts/global-install.js` + `package.json` postinstall | `test/scripts/global-install.test.js` (2 tests) |
1260
- | 4 | `framework/skills/level-0-meta/morph-init/SKILL.md` (verify CLI commands) | Manual grep validation |
1261
- | 5 | Refactor `init.js` + fix `structure-detector.js` | Existing `init.test.js` |
1262
- | 6 | `doctor.js` plugin checks | 2 new tests in `doctor.test.js` |
1263
- | 7 | Full suite + smoke test | All tests green |
1264
-
1265
- **CLI commands exposed by this feature:**
1266
- - `morph-spec setup-infra` — headless infra setup, called by `/morph-init` skill
1267
- - `morph-spec install-plugin <name>` — install superpowers/context7, called by `/morph-init` skill
1268
-
1269
-