@polymorphism-tech/morph-spec 4.10.6 → 4.10.8
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 +3 -0
- package/LICENSE +71 -71
- package/README.md +379 -379
- package/bin/morph-spec.js +89 -0
- package/bin/task-manager.js +59 -6
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +206 -206
- package/framework/agents/README.md +14 -14
- package/framework/agents/architecture/standards-architect.md +159 -159
- package/framework/agents/frontend/blazor-builder.md +13 -0
- package/framework/agents/frontend/css-specialist.md +87 -0
- package/framework/agents/frontend/nextjs-expert.md +13 -0
- package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
- package/framework/agents/frontend/ui-ux-designer.md +80 -10
- package/framework/agents/infrastructure/azure-architect.md +149 -149
- package/framework/agents/infrastructure/infra-architect.md +48 -48
- package/framework/agents.json +2268 -2567
- package/framework/hooks/README.md +204 -204
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
- package/framework/hooks/shared/phase-utils.js +139 -139
- package/framework/hooks/shared/worktree-helpers.js +30 -8
- package/framework/rules/csharp-standards.md +13 -13
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +16 -16
- package/framework/rules/testing-standards.md +14 -14
- package/framework/skills/README.md +0 -1
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
- package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
- package/framework/skills/level-0-meta/morph-init/SKILL.md +88 -3
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -471
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -258
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +424 -323
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +2040 -2040
- package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
- package/framework/templates/README.md +814 -814
- package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
- package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
- package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
- package/framework/templates/code/dotnet/contracts/README.md +74 -74
- package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
- package/framework/templates/code/dotnet/frontend/component.razor +239 -239
- package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
- package/framework/templates/code/dotnet/jobs/job.cs +171 -171
- package/framework/templates/code/dotnet/test.cs +239 -239
- package/framework/templates/code/sql/rls-policy.sql +57 -57
- package/framework/templates/code/sql/supabase-migration.sql +100 -100
- package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
- package/framework/templates/code/typescript/contracts.ts +168 -168
- package/framework/templates/context/CONTEXT.md +181 -181
- package/framework/templates/context/design-system.md +211 -0
- package/framework/templates/docs/proposal.md +182 -182
- package/framework/templates/docs/user-stories.md +34 -34
- package/framework/templates/examples/design-system-examples.md +357 -357
- package/framework/templates/examples/spec-examples.md +90 -90
- package/framework/templates/feature/decisions.md +187 -187
- package/framework/templates/feature/recap.md +146 -146
- package/framework/templates/feature/tasks.md +199 -199
- package/framework/templates/infrastructure/azure/README.md +286 -286
- package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
- package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
- package/package.json +82 -82
- package/src/commands/agents/agent-metrics.js +174 -0
- package/src/commands/agents/audit-dispatch.js +82 -0
- package/src/commands/agents/detect-agents.js +275 -0
- package/src/commands/agents/dispatch-agents.js +41 -135
- package/src/commands/agents/index.js +13 -4
- package/src/commands/agents/manage-agents.js +352 -0
- package/src/commands/project/doctor.js +59 -0
- package/src/commands/project/install-plugin-cmd.js +28 -28
- package/src/commands/project/setup-infra-cmd.js +12 -12
- package/src/commands/project/update.js +1 -17
- package/src/commands/project/worktree.js +58 -2
- package/src/commands/state/advance-phase.js +11 -3
- package/src/commands/tasks/index.js +4 -4
- package/src/commands/templates/index.js +5 -5
- package/src/commands/utils/index.js +3 -3
- package/src/commands/validation/index.js +4 -4
- package/src/core/state/phase-state-machine.js +217 -217
- package/src/core/state/state-manager.js +81 -0
- package/src/lib/agents/agents-manager.js +349 -0
- package/src/lib/agents/metrics-calculator.js +125 -0
- package/src/lib/agents/output-collector.js +75 -0
- package/src/lib/generators/context-generator.js +8 -7
- package/src/lib/generators/design-system-generator.js +298 -298
- package/src/lib/generators/index.js +10 -10
- package/src/lib/generators/metadata-extractor.js +387 -387
- package/src/lib/stack-filter.js +65 -65
- package/src/lib/standards/index.js +7 -7
- package/src/lib/standards/standards-context-injector.js +302 -302
- package/src/lib/tasks/task-parser.js +94 -3
- package/src/lib/validators/architecture/architecture-validator.js +387 -387
- package/src/lib/validators/architecture/index.js +7 -7
- package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
- package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
- package/src/lib/validators/blazor/blazor-validator.js +369 -369
- package/src/lib/validators/blazor/index.js +9 -9
- package/src/lib/validators/checkpoint-persistence.js +39 -0
- package/src/lib/validators/content/content-validator.js +287 -279
- package/src/lib/validators/content/index.js +7 -7
- package/src/lib/validators/contracts/index.js +7 -7
- package/src/lib/validators/css/css-validator.js +352 -352
- package/src/lib/validators/css/index.js +7 -7
- package/src/lib/validators/design-system/design-system-validator.js +231 -231
- package/src/lib/validators/design-system/index.js +7 -7
- package/src/lib/validators/nextjs/index.js +6 -6
- package/src/lib/validators/nextjs/next-component-validator.js +183 -183
- package/src/lib/validators/packages/index.js +7 -7
- package/src/lib/validators/packages/package-validator.js +360 -360
- package/src/lib/validators/shared/index.js +12 -12
- package/src/lib/validators/shared/issue-counter.js +18 -18
- package/src/lib/validators/shared/result-formatter.js +124 -124
- package/src/lib/validators/spec-validator.js +2 -2
- package/src/lib/validators/ui/index.js +7 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
- package/src/lib/validators/validation-runner.js +20 -0
- package/src/scripts/global-install.js +58 -58
- package/src/scripts/install-plugin.js +126 -126
- package/src/scripts/setup-infra.js +1 -17
- package/src/utils/agent-id-mapper.js +11 -0
- package/src/utils/agents-installer.js +40 -8
- package/src/utils/claude-md-injector.js +90 -90
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +200 -196
- package/src/utils/hooks-installer.js +556 -739
- package/src/utils/logger.js +32 -32
- package/src/utils/skills-installer.js +178 -178
- package/src/utils/tier-helpers.js +24 -0
- package/src/utils/version-checker.js +175 -175
- package/framework/agents/architecture/po-pm-advisor.md +0 -203
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
|
@@ -20,6 +20,15 @@ const DOTNET_ONLY_VALIDATORS = new Set([
|
|
|
20
20
|
'blazor-state', 'contract-compliance'
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Heavy validators skipped in 'light' mode (checkpoint-only).
|
|
25
|
+
* Light mode runs only spec, content, and stack-specific lightweight validators.
|
|
26
|
+
*/
|
|
27
|
+
const HEAVY_VALIDATORS = new Set([
|
|
28
|
+
'architecture', 'packages', 'contract-compliance',
|
|
29
|
+
'blazor-concurrency', 'css', 'design-system'
|
|
30
|
+
]);
|
|
31
|
+
|
|
23
32
|
/**
|
|
24
33
|
* Load agent → validators map from agents.json (data-driven)
|
|
25
34
|
* @param {string} projectPath - Root path of the project
|
|
@@ -74,6 +83,17 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
74
83
|
// Determine which validators to run
|
|
75
84
|
const validatorIds = options.validators || detectValidators(featureName, projectPath);
|
|
76
85
|
|
|
86
|
+
// Filter by mode: 'light' skips heavy validators, 'full' (default) runs all
|
|
87
|
+
const mode = options.mode || 'full';
|
|
88
|
+
if (mode === 'light') {
|
|
89
|
+
const beforeCount = validatorIds.length;
|
|
90
|
+
for (let i = validatorIds.length - 1; i >= 0; i--) {
|
|
91
|
+
if (HEAVY_VALIDATORS.has(validatorIds[i])) {
|
|
92
|
+
validatorIds.splice(i, 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
// Merge constraint-enabled validators
|
|
78
98
|
validatorIds.push(...constraints.enabledValidators);
|
|
79
99
|
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
// src/scripts/global-install.js
|
|
2
|
-
import { join, dirname } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { homedir } from 'os';
|
|
5
|
-
import { mkdir, copyFile } from 'fs/promises';
|
|
6
|
-
import { existsSync } from 'fs';
|
|
7
|
-
import { installGlobalStatusline } from '../utils/hooks-installer.js';
|
|
8
|
-
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const FRAMEWORK_DIR = join(__dirname, '..', '..', 'framework');
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Install morph-init skill to a Claude global skills directory.
|
|
14
|
-
* @param {string} claudeDir - target ~/.claude dir (defaults to real ~/.claude)
|
|
15
|
-
*/
|
|
16
|
-
export async function installGlobalSkill(claudeDir = join(homedir(), '.claude')) {
|
|
17
|
-
const src = join(FRAMEWORK_DIR, 'skills', 'level-0-meta', 'morph-init', 'SKILL.md');
|
|
18
|
-
const destDir = join(claudeDir, 'skills', 'morph-init');
|
|
19
|
-
const dest = join(destDir, 'SKILL.md');
|
|
20
|
-
|
|
21
|
-
if (!existsSync(src)) {
|
|
22
|
-
console.warn(`morph-init skill not found at ${src} — skipping global install`);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
await mkdir(destDir, { recursive: true });
|
|
27
|
-
await copyFile(src, dest);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Run as postinstall script
|
|
31
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
32
|
-
const HOOKS_SRC = join(FRAMEWORK_DIR, 'hooks', 'claude-code');
|
|
33
|
-
const { readFileSync } = await import('fs');
|
|
34
|
-
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
35
|
-
|
|
36
|
-
const [skillResult, statuslineResult] = await Promise.allSettled([
|
|
37
|
-
installGlobalSkill(),
|
|
38
|
-
installGlobalStatusline(HOOKS_SRC),
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
const results = [
|
|
42
|
-
{
|
|
43
|
-
ok: skillResult.status === 'fulfilled',
|
|
44
|
-
label: 'morph-init skill',
|
|
45
|
-
path: '~/.claude/skills/',
|
|
46
|
-
error: skillResult.reason?.message,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
ok: statuslineResult.status === 'fulfilled',
|
|
50
|
-
label: 'statusline',
|
|
51
|
-
path: '~/.claude/',
|
|
52
|
-
error: statuslineResult.reason?.message,
|
|
53
|
-
},
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const { printInstallBanner } = await import('../utils/banner.js');
|
|
57
|
-
printInstallBanner({ version: pkg.version, nodeVersion: process.version, results });
|
|
58
|
-
}
|
|
1
|
+
// src/scripts/global-install.js
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { mkdir, copyFile } from 'fs/promises';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { installGlobalStatusline } from '../utils/hooks-installer.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const FRAMEWORK_DIR = join(__dirname, '..', '..', 'framework');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Install morph-init skill to a Claude global skills directory.
|
|
14
|
+
* @param {string} claudeDir - target ~/.claude dir (defaults to real ~/.claude)
|
|
15
|
+
*/
|
|
16
|
+
export async function installGlobalSkill(claudeDir = join(homedir(), '.claude')) {
|
|
17
|
+
const src = join(FRAMEWORK_DIR, 'skills', 'level-0-meta', 'morph-init', 'SKILL.md');
|
|
18
|
+
const destDir = join(claudeDir, 'skills', 'morph-init');
|
|
19
|
+
const dest = join(destDir, 'SKILL.md');
|
|
20
|
+
|
|
21
|
+
if (!existsSync(src)) {
|
|
22
|
+
console.warn(`morph-init skill not found at ${src} — skipping global install`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await mkdir(destDir, { recursive: true });
|
|
27
|
+
await copyFile(src, dest);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Run as postinstall script
|
|
31
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
32
|
+
const HOOKS_SRC = join(FRAMEWORK_DIR, 'hooks', 'claude-code');
|
|
33
|
+
const { readFileSync } = await import('fs');
|
|
34
|
+
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
35
|
+
|
|
36
|
+
const [skillResult, statuslineResult] = await Promise.allSettled([
|
|
37
|
+
installGlobalSkill(),
|
|
38
|
+
installGlobalStatusline(HOOKS_SRC),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const results = [
|
|
42
|
+
{
|
|
43
|
+
ok: skillResult.status === 'fulfilled',
|
|
44
|
+
label: 'morph-init skill',
|
|
45
|
+
path: '~/.claude/skills/',
|
|
46
|
+
error: skillResult.reason?.message,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
ok: statuslineResult.status === 'fulfilled',
|
|
50
|
+
label: 'statusline',
|
|
51
|
+
path: '~/.claude/',
|
|
52
|
+
error: statuslineResult.reason?.message,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const { printInstallBanner } = await import('../utils/banner.js');
|
|
57
|
+
printInstallBanner({ version: pkg.version, nodeVersion: process.version, results });
|
|
58
|
+
}
|
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
// src/scripts/install-plugin.js
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
5
|
-
import { mkdir, writeFile } from 'fs/promises';
|
|
6
|
-
import https from 'node:https';
|
|
7
|
-
|
|
8
|
-
const PLUGINS_DIR = join(homedir(), '.claude', 'plugins');
|
|
9
|
-
const CACHE_DIR = join(PLUGINS_DIR, 'cache', 'claude-plugins-official');
|
|
10
|
-
const INSTALLED_FILE = join(PLUGINS_DIR, 'installed_plugins.json');
|
|
11
|
-
const GH_REPO = 'anthropics/claude-plugins-official';
|
|
12
|
-
|
|
13
|
-
/** Fetch JSON from GitHub API (no auth — public repo) */
|
|
14
|
-
async function fetchGitHub(path) {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const opts = {
|
|
17
|
-
hostname: 'api.github.com',
|
|
18
|
-
path,
|
|
19
|
-
headers: { 'User-Agent': 'morph-spec', 'Accept': 'application/vnd.github.v3+json' }
|
|
20
|
-
};
|
|
21
|
-
https.get(opts, (res) => {
|
|
22
|
-
let data = '';
|
|
23
|
-
res.on('data', c => data += c);
|
|
24
|
-
res.on('end', () => {
|
|
25
|
-
try { resolve(JSON.parse(data)); }
|
|
26
|
-
catch (e) { reject(new Error(`GitHub API parse error: ${data.slice(0, 200)}`)); }
|
|
27
|
-
});
|
|
28
|
-
}).on('error', reject);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Download raw file content from a URL, following one redirect */
|
|
33
|
-
async function downloadFile(rawUrl) {
|
|
34
|
-
return new Promise((resolve, reject) => {
|
|
35
|
-
const doGet = (url) => {
|
|
36
|
-
const parsed = new URL(url);
|
|
37
|
-
https.get({
|
|
38
|
-
hostname: parsed.hostname,
|
|
39
|
-
path: parsed.pathname + parsed.search,
|
|
40
|
-
headers: { 'User-Agent': 'morph-spec' }
|
|
41
|
-
}, (res) => {
|
|
42
|
-
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
43
|
-
return doGet(res.headers.location);
|
|
44
|
-
}
|
|
45
|
-
const chunks = [];
|
|
46
|
-
res.on('data', c => chunks.push(c));
|
|
47
|
-
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
48
|
-
}).on('error', reject);
|
|
49
|
-
};
|
|
50
|
-
doGet(rawUrl);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Read installed_plugins.json safely */
|
|
55
|
-
export function readInstalledPlugins() {
|
|
56
|
-
if (!existsSync(INSTALLED_FILE)) return { version: 2, plugins: {} };
|
|
57
|
-
try { return JSON.parse(readFileSync(INSTALLED_FILE, 'utf8')); }
|
|
58
|
-
catch { return { version: 2, plugins: {} }; }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Check if plugin key exists in the plugins map */
|
|
62
|
-
export function isPluginInstalled(plugins, name) {
|
|
63
|
-
return `${name}@claude-plugins-official` in plugins;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Build an installed_plugins.json entry */
|
|
67
|
-
export function buildInstallEntry(pluginName, installPath, gitCommitSha, version) {
|
|
68
|
-
const now = new Date().toISOString();
|
|
69
|
-
return { scope: 'user', installPath, version, installedAt: now, lastUpdated: now, gitCommitSha };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Resolve latest commit SHA for a plugin path in the repo */
|
|
73
|
-
export async function resolvePluginVersion(pluginName) {
|
|
74
|
-
const commits = await fetchGitHub(`/repos/${GH_REPO}/commits?path=${pluginName}&per_page=1`);
|
|
75
|
-
if (!Array.isArray(commits) || commits.length === 0) {
|
|
76
|
-
throw new Error(`Could not resolve version for plugin: ${pluginName}`);
|
|
77
|
-
}
|
|
78
|
-
const sha = commits[0].sha;
|
|
79
|
-
return { sha, shortSha: sha.slice(0, 12) };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Download all files for a plugin from the repo tree */
|
|
83
|
-
async function downloadPluginFiles(pluginName, sha, destDir) {
|
|
84
|
-
const tree = await fetchGitHub(`/repos/${GH_REPO}/git/trees/${sha}?recursive=1`);
|
|
85
|
-
if (!tree.tree) throw new Error('GitHub tree API returned unexpected shape');
|
|
86
|
-
|
|
87
|
-
const pluginFiles = tree.tree.filter(f => f.path.startsWith(`${pluginName}/`) && f.type === 'blob');
|
|
88
|
-
if (pluginFiles.length === 0) throw new Error(`Plugin "${pluginName}" not found in repository`);
|
|
89
|
-
|
|
90
|
-
for (const file of pluginFiles) {
|
|
91
|
-
const relativePath = file.path.slice(pluginName.length + 1);
|
|
92
|
-
const destPath = join(destDir, relativePath);
|
|
93
|
-
await mkdir(join(destPath, '..'), { recursive: true });
|
|
94
|
-
const content = await downloadFile(
|
|
95
|
-
`https://raw.githubusercontent.com/${GH_REPO}/${sha}/${file.path}`
|
|
96
|
-
);
|
|
97
|
-
await writeFile(destPath, content);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Install a Claude Code plugin from GitHub.
|
|
103
|
-
* @param {string} pluginName - e.g. "superpowers"
|
|
104
|
-
* @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
|
|
105
|
-
*/
|
|
106
|
-
export async function installPlugin(pluginName) {
|
|
107
|
-
const data = readInstalledPlugins();
|
|
108
|
-
if (isPluginInstalled(data.plugins, pluginName)) {
|
|
109
|
-
const existing = data.plugins[`${pluginName}@claude-plugins-official`];
|
|
110
|
-
const version = Array.isArray(existing) ? existing[0]?.version : 'unknown';
|
|
111
|
-
return { installed: true, alreadyInstalled: true, version };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const { sha, shortSha } = await resolvePluginVersion(pluginName);
|
|
115
|
-
const installPath = join(CACHE_DIR, pluginName, shortSha);
|
|
116
|
-
|
|
117
|
-
await mkdir(installPath, { recursive: true });
|
|
118
|
-
await downloadPluginFiles(pluginName, sha, installPath);
|
|
119
|
-
|
|
120
|
-
const entry = buildInstallEntry(pluginName, installPath, sha, shortSha);
|
|
121
|
-
data.plugins[`${pluginName}@claude-plugins-official`] = [entry];
|
|
122
|
-
mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
123
|
-
writeFileSync(INSTALLED_FILE, JSON.stringify(data, null, 2));
|
|
124
|
-
|
|
125
|
-
return { installed: true, alreadyInstalled: false, version: shortSha };
|
|
126
|
-
}
|
|
1
|
+
// src/scripts/install-plugin.js
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
5
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
6
|
+
import https from 'node:https';
|
|
7
|
+
|
|
8
|
+
const PLUGINS_DIR = join(homedir(), '.claude', 'plugins');
|
|
9
|
+
const CACHE_DIR = join(PLUGINS_DIR, 'cache', 'claude-plugins-official');
|
|
10
|
+
const INSTALLED_FILE = join(PLUGINS_DIR, 'installed_plugins.json');
|
|
11
|
+
const GH_REPO = 'anthropics/claude-plugins-official';
|
|
12
|
+
|
|
13
|
+
/** Fetch JSON from GitHub API (no auth — public repo) */
|
|
14
|
+
async function fetchGitHub(path) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const opts = {
|
|
17
|
+
hostname: 'api.github.com',
|
|
18
|
+
path,
|
|
19
|
+
headers: { 'User-Agent': 'morph-spec', 'Accept': 'application/vnd.github.v3+json' }
|
|
20
|
+
};
|
|
21
|
+
https.get(opts, (res) => {
|
|
22
|
+
let data = '';
|
|
23
|
+
res.on('data', c => data += c);
|
|
24
|
+
res.on('end', () => {
|
|
25
|
+
try { resolve(JSON.parse(data)); }
|
|
26
|
+
catch (e) { reject(new Error(`GitHub API parse error: ${data.slice(0, 200)}`)); }
|
|
27
|
+
});
|
|
28
|
+
}).on('error', reject);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Download raw file content from a URL, following one redirect */
|
|
33
|
+
async function downloadFile(rawUrl) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const doGet = (url) => {
|
|
36
|
+
const parsed = new URL(url);
|
|
37
|
+
https.get({
|
|
38
|
+
hostname: parsed.hostname,
|
|
39
|
+
path: parsed.pathname + parsed.search,
|
|
40
|
+
headers: { 'User-Agent': 'morph-spec' }
|
|
41
|
+
}, (res) => {
|
|
42
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
43
|
+
return doGet(res.headers.location);
|
|
44
|
+
}
|
|
45
|
+
const chunks = [];
|
|
46
|
+
res.on('data', c => chunks.push(c));
|
|
47
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
48
|
+
}).on('error', reject);
|
|
49
|
+
};
|
|
50
|
+
doGet(rawUrl);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read installed_plugins.json safely */
|
|
55
|
+
export function readInstalledPlugins() {
|
|
56
|
+
if (!existsSync(INSTALLED_FILE)) return { version: 2, plugins: {} };
|
|
57
|
+
try { return JSON.parse(readFileSync(INSTALLED_FILE, 'utf8')); }
|
|
58
|
+
catch { return { version: 2, plugins: {} }; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Check if plugin key exists in the plugins map */
|
|
62
|
+
export function isPluginInstalled(plugins, name) {
|
|
63
|
+
return `${name}@claude-plugins-official` in plugins;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Build an installed_plugins.json entry */
|
|
67
|
+
export function buildInstallEntry(pluginName, installPath, gitCommitSha, version) {
|
|
68
|
+
const now = new Date().toISOString();
|
|
69
|
+
return { scope: 'user', installPath, version, installedAt: now, lastUpdated: now, gitCommitSha };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolve latest commit SHA for a plugin path in the repo */
|
|
73
|
+
export async function resolvePluginVersion(pluginName) {
|
|
74
|
+
const commits = await fetchGitHub(`/repos/${GH_REPO}/commits?path=${pluginName}&per_page=1`);
|
|
75
|
+
if (!Array.isArray(commits) || commits.length === 0) {
|
|
76
|
+
throw new Error(`Could not resolve version for plugin: ${pluginName}`);
|
|
77
|
+
}
|
|
78
|
+
const sha = commits[0].sha;
|
|
79
|
+
return { sha, shortSha: sha.slice(0, 12) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Download all files for a plugin from the repo tree */
|
|
83
|
+
async function downloadPluginFiles(pluginName, sha, destDir) {
|
|
84
|
+
const tree = await fetchGitHub(`/repos/${GH_REPO}/git/trees/${sha}?recursive=1`);
|
|
85
|
+
if (!tree.tree) throw new Error('GitHub tree API returned unexpected shape');
|
|
86
|
+
|
|
87
|
+
const pluginFiles = tree.tree.filter(f => f.path.startsWith(`${pluginName}/`) && f.type === 'blob');
|
|
88
|
+
if (pluginFiles.length === 0) throw new Error(`Plugin "${pluginName}" not found in repository`);
|
|
89
|
+
|
|
90
|
+
for (const file of pluginFiles) {
|
|
91
|
+
const relativePath = file.path.slice(pluginName.length + 1);
|
|
92
|
+
const destPath = join(destDir, relativePath);
|
|
93
|
+
await mkdir(join(destPath, '..'), { recursive: true });
|
|
94
|
+
const content = await downloadFile(
|
|
95
|
+
`https://raw.githubusercontent.com/${GH_REPO}/${sha}/${file.path}`
|
|
96
|
+
);
|
|
97
|
+
await writeFile(destPath, content);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Install a Claude Code plugin from GitHub.
|
|
103
|
+
* @param {string} pluginName - e.g. "superpowers"
|
|
104
|
+
* @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
|
|
105
|
+
*/
|
|
106
|
+
export async function installPlugin(pluginName) {
|
|
107
|
+
const data = readInstalledPlugins();
|
|
108
|
+
if (isPluginInstalled(data.plugins, pluginName)) {
|
|
109
|
+
const existing = data.plugins[`${pluginName}@claude-plugins-official`];
|
|
110
|
+
const version = Array.isArray(existing) ? existing[0]?.version : 'unknown';
|
|
111
|
+
return { installed: true, alreadyInstalled: true, version };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { sha, shortSha } = await resolvePluginVersion(pluginName);
|
|
115
|
+
const installPath = join(CACHE_DIR, pluginName, shortSha);
|
|
116
|
+
|
|
117
|
+
await mkdir(installPath, { recursive: true });
|
|
118
|
+
await downloadPluginFiles(pluginName, sha, installPath);
|
|
119
|
+
|
|
120
|
+
const entry = buildInstallEntry(pluginName, installPath, sha, shortSha);
|
|
121
|
+
data.plugins[`${pluginName}@claude-plugins-official`] = [entry];
|
|
122
|
+
mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
123
|
+
writeFileSync(INSTALLED_FILE, JSON.stringify(data, null, 2));
|
|
124
|
+
|
|
125
|
+
return { installed: true, alreadyInstalled: false, version: shortSha };
|
|
126
|
+
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import { parseStacks, getProjectTags, shouldInstall } from '../lib/stack-filter.js';
|
|
28
28
|
import { injectMorphImport } from '../utils/claude-md-injector.js';
|
|
29
29
|
import { saveProjectMorphVersion, getInstalledCLIVersion } from '../utils/version-checker.js';
|
|
30
|
-
import { installClaudeHooks, installGlobalStatusline
|
|
30
|
+
import { installClaudeHooks, installGlobalStatusline } from '../utils/claude-settings-manager.js';
|
|
31
31
|
import { installSkills } from '../utils/skills-installer.js';
|
|
32
32
|
import { installAgents, installDomainAgents } from '../utils/agents-installer.js';
|
|
33
33
|
|
|
@@ -238,22 +238,6 @@ export async function setupInfra(targetPath, { _exec = execSync } = {}) {
|
|
|
238
238
|
log('Step 16: Installing Claude Code hooks...');
|
|
239
239
|
await installClaudeHooks(targetPath);
|
|
240
240
|
|
|
241
|
-
// --- 16a. installShellIntegration (non-critical) ---
|
|
242
|
-
log('Step 16a: Installing shell integration...');
|
|
243
|
-
try {
|
|
244
|
-
await installShellIntegration();
|
|
245
|
-
} catch {
|
|
246
|
-
// Non-critical — shell profile may not be writable
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// --- 16b. installVSCodeTerminalSettings (non-critical) ---
|
|
250
|
-
log('Step 16b: Configuring VS Code terminal settings...');
|
|
251
|
-
try {
|
|
252
|
-
await installVSCodeTerminalSettings();
|
|
253
|
-
} catch {
|
|
254
|
-
// Non-critical — VS Code settings may not be present
|
|
255
|
-
}
|
|
256
|
-
|
|
257
241
|
// --- 17. saveProjectMorphVersion ---
|
|
258
242
|
log('Step 17: Saving version info...');
|
|
259
243
|
const cliVersion = getInstalledCLIVersion();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map an agents.json ID to the Claude Code subagent_type.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
|
|
5
|
+
* @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
|
|
6
|
+
* @returns {string} subagent_type matching .claude/agents/ filename stem
|
|
7
|
+
*/
|
|
8
|
+
export function agentIdToSubagentType(id, tier) {
|
|
9
|
+
const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
|
|
10
|
+
return `${prefix}${id}`;
|
|
11
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* maxTurns, skills, memory) followed by the agent's spawn prompt as the body.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
11
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
|
|
14
14
|
|
|
@@ -88,6 +88,13 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
88
88
|
if (allPossible.has(file) && !installed.has(file)) {
|
|
89
89
|
unlinkSync(join(targetDir, file));
|
|
90
90
|
}
|
|
91
|
+
// Remove morph-domain-{name}.md when morph-{name}.md was installed from agents.json
|
|
92
|
+
if (file.startsWith('morph-domain-')) {
|
|
93
|
+
const suffix = file.slice('morph-domain-'.length);
|
|
94
|
+
if (installed.has(`morph-${suffix}`)) {
|
|
95
|
+
unlinkSync(join(targetDir, file));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
return { tier1, tier2, tier3, tier4 };
|
|
@@ -170,6 +177,19 @@ function buildDescription(agent) {
|
|
|
170
177
|
return base.length > 200 ? base.slice(0, 197) + '...' : base;
|
|
171
178
|
}
|
|
172
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Serialize an AgentStandard to a readable markdown line.
|
|
182
|
+
*
|
|
183
|
+
* @param {string|{id: string, scope?: string, priority?: string}} standard
|
|
184
|
+
* @returns {string} e.g., "- core-coding (all, required)" or "- core-coding"
|
|
185
|
+
*/
|
|
186
|
+
export function formatStandardRef(standard) {
|
|
187
|
+
if (typeof standard === 'string') return `- ${standard}`;
|
|
188
|
+
const id = standard.id || String(standard);
|
|
189
|
+
const parts = [standard.scope, standard.priority].filter(Boolean);
|
|
190
|
+
return parts.length > 0 ? `- ${id} (${parts.join(', ')})` : `- ${id}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
173
193
|
function buildBody(agent) {
|
|
174
194
|
const lines = [];
|
|
175
195
|
if (agent.teammate?.spawn_prompt) {
|
|
@@ -181,7 +201,7 @@ function buildBody(agent) {
|
|
|
181
201
|
}
|
|
182
202
|
}
|
|
183
203
|
if (agent.standards?.length) {
|
|
184
|
-
lines.push(`\n## Standards to Reference\n${agent.standards.map(
|
|
204
|
+
lines.push(`\n## Standards to Reference\n${agent.standards.map(formatStandardRef).join('\n')}`);
|
|
185
205
|
}
|
|
186
206
|
return lines.join('\n');
|
|
187
207
|
}
|
|
@@ -270,11 +290,19 @@ export async function installDomainAgents(projectDir, frameworkDir = 'framework'
|
|
|
270
290
|
'',
|
|
271
291
|
].join('\n');
|
|
272
292
|
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
293
|
+
// If agents.json already generated morph-{name}.md, append domain content there
|
|
294
|
+
const agentsJsonFile = `morph-${name}.md`;
|
|
295
|
+
const agentsJsonPath = join(targetDir, agentsJsonFile);
|
|
296
|
+
if (existsSync(agentsJsonPath)) {
|
|
297
|
+
appendFileSync(agentsJsonPath, `\n\n---\n\n## Domain Knowledge\n\n${body.trimStart()}`, 'utf-8');
|
|
298
|
+
specialists++;
|
|
299
|
+
} else {
|
|
300
|
+
const filename = `morph-domain-${name}.md`;
|
|
301
|
+
const targetPath = join(targetDir, filename);
|
|
302
|
+
writeFileSync(targetPath, `---\n${frontmatter}---\n\n${body.trimStart()}`, 'utf-8');
|
|
303
|
+
installed.add(filename);
|
|
304
|
+
specialists++;
|
|
305
|
+
}
|
|
278
306
|
}
|
|
279
307
|
|
|
280
308
|
// Cleanup orphan morph-domain-* files
|
|
@@ -333,7 +361,7 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
|
333
361
|
}
|
|
334
362
|
}
|
|
335
363
|
|
|
336
|
-
// Check domain agents
|
|
364
|
+
// Check domain agents (skip if merged into agents.json entry)
|
|
337
365
|
const domainsDir = join(frameworkDir, 'agents');
|
|
338
366
|
if (existsSync(domainsDir)) {
|
|
339
367
|
for (const filePath of collectSkillFiles(domainsDir)) {
|
|
@@ -342,6 +370,10 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
|
342
370
|
if (!name) continue;
|
|
343
371
|
if (!shouldInstall(stacks, projectTags)) continue;
|
|
344
372
|
|
|
373
|
+
// If agents.json has morph-{name}.md, domain content is merged there
|
|
374
|
+
const agentsJsonFile = `morph-${name}.md`;
|
|
375
|
+
if (installedFiles.has(agentsJsonFile)) continue;
|
|
376
|
+
|
|
345
377
|
expected++;
|
|
346
378
|
const filename = `morph-domain-${name}.md`;
|
|
347
379
|
if (!installedFiles.has(filename)) {
|