@polymorphism-tech/morph-spec 8.38.0 → 8.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +19 -8
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -2
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +36 -25
- package/framework/docs/CLI.md +2 -2
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/mcp/registry.json +33 -2
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +5 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +6 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +4 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +6 -5
- package/framework/standards/STANDARDS.json +3 -3
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/architecture/code-comments.md +52 -16
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +3 -5
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +59 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +57 -12
- package/src/utils/banner.js +1 -1
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -1,17 +1,134 @@
|
|
|
1
|
-
import { scanAll } from './scanner/index.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { scanAll } from './scanner/index.js';
|
|
2
|
+
import { classifyChange, watchTargets, isMorphDataReload } from './scanner/project-api.js';
|
|
3
|
+
import { createPortfolio } from './scanner/portfolio.js';
|
|
4
|
+
|
|
5
|
+
const VIRTUAL_MODULE_ID = 'virtual:morph-data';
|
|
6
|
+
const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_MODULE_ID;
|
|
7
|
+
const DEBOUNCE_MS = 300;
|
|
8
|
+
const RESCAN_MS = 30_000;
|
|
9
|
+
const WARM_GAP_MS = 400;
|
|
10
|
+
const MAX_BODY = 64 * 1024;
|
|
11
|
+
|
|
12
|
+
function readBody(req) {
|
|
13
|
+
return new Promise((resolveBody) => {
|
|
14
|
+
let size = 0;
|
|
15
|
+
const chunks = [];
|
|
16
|
+
req.on('data', (c) => { size += c.length; if (size <= MAX_BODY) chunks.push(c); });
|
|
17
|
+
req.on('end', () => {
|
|
18
|
+
try { resolveBody(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : null); } catch { resolveBody(null); }
|
|
19
|
+
});
|
|
20
|
+
req.on('error', () => resolveBody(null));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Two data paths:
|
|
26
|
+
* - `virtual:morph-data` — the framework's knowledge (agents, phases, skills,
|
|
27
|
+
* standards, rules, hooks, docs). Static for the life of the server.
|
|
28
|
+
* - `/api/morph/*` — the portfolio (`/projects`) and each project's features
|
|
29
|
+
* and activity (`/p/:id/…`), read on demand and pushed as `morph:changed`
|
|
30
|
+
* over the HMR socket when a project's `.morph/` changes.
|
|
31
|
+
*/
|
|
32
|
+
export default function morphDataPlugin(frameworkDir, projectDir) {
|
|
33
|
+
return {
|
|
34
|
+
name: 'vite-plugin-morph-data',
|
|
35
|
+
resolveId(id) { if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_ID; },
|
|
36
|
+
load(id) {
|
|
37
|
+
if (id === RESOLVED_VIRTUAL_ID) {
|
|
38
|
+
const data = scanAll(frameworkDir);
|
|
39
|
+
return `export default ${JSON.stringify(data)}`;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
configureServer(server) {
|
|
43
|
+
const portfolio = createPortfolio({ launchDir: projectDir });
|
|
44
|
+
const push = (data) => server.ws.send({ type: 'custom', event: 'morph:changed', data });
|
|
45
|
+
|
|
46
|
+
// Vite reloads the whole page for any changed .html it watches, even when
|
|
47
|
+
// handleHotUpdate returns [] (vite 6: no module matched + .html = full
|
|
48
|
+
// reload). A project's .morph/ carries .html design sources, and an agent
|
|
49
|
+
// writing one reloaded the dashboard under the person reading it. The
|
|
50
|
+
// dashboard's own code never lives under .morph/.
|
|
51
|
+
const clientHot = server.environments?.client?.hot;
|
|
52
|
+
if (clientHot?.send) {
|
|
53
|
+
const send = clientHot.send.bind(clientHot);
|
|
54
|
+
clientHot.send = (payload, ...rest) => {
|
|
55
|
+
if (payload?.type === 'full-reload' && isMorphDataReload(payload.path)) return;
|
|
56
|
+
send(payload, ...rest);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const watched = new Set();
|
|
61
|
+
function watchNew() {
|
|
62
|
+
const fresh = portfolio.list().flatMap((p) => watchTargets(p.dir)).filter((p) => !watched.has(p));
|
|
63
|
+
for (const p of fresh) watched.add(p);
|
|
64
|
+
if (fresh.length) server.watcher.add(fresh);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Read the projects in the background, one at a time, so neither the first
|
|
68
|
+
// page nor the portfolio waits for a large project. Reading one blocks the
|
|
69
|
+
// event loop for seconds, so each step waits WARM_GAP_MS first: the reads
|
|
70
|
+
// a push (or an add) just caused are served before the next block.
|
|
71
|
+
let warming = false;
|
|
72
|
+
function warmSoon() {
|
|
73
|
+
if (warming) return;
|
|
74
|
+
warming = true;
|
|
75
|
+
const step = () => {
|
|
76
|
+
const warmed = portfolio.warmNext();
|
|
77
|
+
if (!warmed) { warming = false; return; }
|
|
78
|
+
push({ scope: 'projects' });
|
|
79
|
+
setTimeout(step, WARM_GAP_MS);
|
|
80
|
+
};
|
|
81
|
+
setTimeout(step, WARM_GAP_MS);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
server.middlewares.use('/api/morph', async (req, res) => {
|
|
85
|
+
const body = req.method === 'POST' ? await readBody(req) : null;
|
|
86
|
+
const { status, body: out } = portfolio.handle(req.method || 'GET', req.url || '/', body);
|
|
87
|
+
res.statusCode = status;
|
|
88
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
89
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
90
|
+
res.end(JSON.stringify(out));
|
|
91
|
+
// The portfolio's list changed: watch and read any project just added.
|
|
92
|
+
if (req.method !== 'GET' && status === 200) { watchNew(); push({ scope: 'projects' }); warmSoon(); }
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
watchNew();
|
|
96
|
+
warmSoon();
|
|
97
|
+
const rescan = setInterval(watchNew, RESCAN_MS);
|
|
98
|
+
rescan.unref?.();
|
|
99
|
+
|
|
100
|
+
const timers = new Map();
|
|
101
|
+
const changed = new Map(); // `${project}:${scope}` -> path -> watcher event, in the debounce window
|
|
102
|
+
const onFsEvent = (event, path) => {
|
|
103
|
+
const scope = classifyChange(path);
|
|
104
|
+
if (!scope) return;
|
|
105
|
+
const project = portfolio.ownerOf(path);
|
|
106
|
+
if (!project) return;
|
|
107
|
+
const key = `${project.id}:${scope}`;
|
|
108
|
+
if (!changed.has(key)) changed.set(key, new Map());
|
|
109
|
+
const seen = changed.get(key);
|
|
110
|
+
// A path added then changed in one window is still an add.
|
|
111
|
+
if (!seen.has(path) || seen.get(path) === 'change') seen.set(path, event);
|
|
112
|
+
clearTimeout(timers.get(key));
|
|
113
|
+
timers.set(key, setTimeout(() => {
|
|
114
|
+
timers.delete(key);
|
|
115
|
+
const paths = changed.get(key) || new Map();
|
|
116
|
+
changed.delete(key);
|
|
117
|
+
for (const [p, event] of paths) portfolio.invalidate(project, scope, p, event);
|
|
118
|
+
if (scope === 'features') watchNew();
|
|
119
|
+
push({ scope, project: project.id });
|
|
120
|
+
// A log append is tailed on read, so the portfolio can refresh now; a
|
|
121
|
+
// feature change is re-read in the background first, then pushed.
|
|
122
|
+
if (scope === 'activity') push({ scope: 'projects' });
|
|
123
|
+
else warmSoon();
|
|
124
|
+
}, DEBOUNCE_MS));
|
|
125
|
+
};
|
|
126
|
+
for (const evt of ['add', 'change', 'unlink', 'addDir', 'unlinkDir']) server.watcher.on(evt, (path) => onFsEvent(evt, path));
|
|
127
|
+
server.httpServer?.once('close', () => clearInterval(rescan));
|
|
128
|
+
},
|
|
129
|
+
// A changed `.morph/` file is data, not a module: never let it reach HMR.
|
|
130
|
+
handleHotUpdate(ctx) {
|
|
131
|
+
if (classifyChange(ctx.file)) return [];
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -15,13 +15,12 @@ import { homedir } from 'os';
|
|
|
15
15
|
* Detect Claude Code configuration for a project.
|
|
16
16
|
*
|
|
17
17
|
* @param {string} targetPath - Root path of the project
|
|
18
|
-
* @returns {{ plugins: string[], mcpServers: Array<{name: string, config: Object}>,
|
|
18
|
+
* @returns {{ plugins: string[], mcpServers: Array<{name: string, config: Object}>, customSkills: string[], customCommands: string[], hooks: string[] }}
|
|
19
19
|
*/
|
|
20
20
|
export function detectClaudeConfig(targetPath) {
|
|
21
21
|
const result = {
|
|
22
22
|
plugins: [],
|
|
23
23
|
mcpServers: [],
|
|
24
|
-
superpowers: false,
|
|
25
24
|
customSkills: [],
|
|
26
25
|
customCommands: [],
|
|
27
26
|
hooks: [],
|
|
@@ -86,7 +85,6 @@ export function detectClaudeConfig(targetPath) {
|
|
|
86
85
|
const pluginsData = JSON.parse(readFileSync(pluginsPath, 'utf8'));
|
|
87
86
|
const plugins = pluginsData.plugins || {};
|
|
88
87
|
result.plugins = Object.keys(plugins);
|
|
89
|
-
result.superpowers = 'superpowers@claude-plugins-official' in plugins;
|
|
90
88
|
} catch { /* invalid JSON */ }
|
|
91
89
|
}
|
|
92
90
|
|
|
@@ -166,7 +164,7 @@ export function mcpAvailabilityByPhase(mcpServers, phaseMatrix = {}) {
|
|
|
166
164
|
/**
|
|
167
165
|
* Format a human-readable one-liner summary of detected config.
|
|
168
166
|
*
|
|
169
|
-
* @param {{ plugins: string[], mcpServers: Array<{name: string}
|
|
167
|
+
* @param {{ plugins: string[], mcpServers: Array<{name: string}> }} config
|
|
170
168
|
* @returns {string}
|
|
171
169
|
*/
|
|
172
170
|
export function formatConfigSummary(config) {
|
|
@@ -177,8 +175,5 @@ export function formatConfigSummary(config) {
|
|
|
177
175
|
if (config.plugins.length > 0) {
|
|
178
176
|
parts.push(`Plugins: ${config.plugins.length}`);
|
|
179
177
|
}
|
|
180
|
-
if (config.superpowers) {
|
|
181
|
-
parts.push('superpowers: yes');
|
|
182
|
-
}
|
|
183
178
|
return parts.join(' | ') || 'No Claude config detected';
|
|
184
179
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @fileoverview Project Detector
|
|
3
3
|
*
|
|
4
4
|
* Scans a project directory for stack, architecture, UI library, and integrations.
|
|
5
|
-
* Used by
|
|
5
|
+
* Used by `morph-spec init`, `mcp setup` and the test-node fallback (test-nodes.js).
|
|
6
6
|
*
|
|
7
7
|
* @module detectors/index
|
|
8
8
|
*/
|
|
@@ -106,6 +106,10 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
106
106
|
if (content.includes('Hangfire')) {
|
|
107
107
|
if (!structure.integrations.includes('hangfire')) structure.integrations.push('hangfire');
|
|
108
108
|
}
|
|
109
|
+
// Same heuristic as the package.json side (`pg`/`postgres` → neon).
|
|
110
|
+
if (content.includes('Npgsql')) {
|
|
111
|
+
if (!structure.integrations.includes('neon')) structure.integrations.push('neon');
|
|
112
|
+
}
|
|
109
113
|
} catch { /* ignore */ }
|
|
110
114
|
}
|
|
111
115
|
|
|
@@ -349,3 +349,25 @@ export function mediaProofScore(proofScores) {
|
|
|
349
349
|
const value = proofScores?.media;
|
|
350
350
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
351
351
|
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Os dois vereditos do relatório lidos de um TEXTO já em mãos — o score
|
|
355
|
+
* composto vigente e os Proof-Scores vigentes, pela mesma regra append-only
|
|
356
|
+
* (o último token vale) dos leitores por `cwd` acima.
|
|
357
|
+
*
|
|
358
|
+
* Existe para quem tem o relatório fora do caminho canônico — o dashboard lê
|
|
359
|
+
* features arquivadas em `.morph/archive/{f}/4-review/` —, sem que ele
|
|
360
|
+
* precise de uma segunda cópia dos regexes.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} text
|
|
363
|
+
* @returns {{ score: number|undefined, proofScores: Record<string, number>|undefined }}
|
|
364
|
+
*/
|
|
365
|
+
export function evaluatorVerdictFromText(text) {
|
|
366
|
+
if (typeof text !== 'string') return { score: undefined, proofScores: undefined };
|
|
367
|
+
const scores = compositeScoresIn(text);
|
|
368
|
+
const lines = [...text.matchAll(PROOF_SCORES_RE)];
|
|
369
|
+
return {
|
|
370
|
+
score: scores.length === 0 ? undefined : scores[scores.length - 1],
|
|
371
|
+
proofScores: lines.length === 0 ? undefined : parseProofScores(lines[lines.length - 1][1]),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
@@ -1,209 +1,208 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Settings Generator
|
|
3
|
-
*
|
|
4
|
-
* Generates .claude/settings.local.json for projects based on detected
|
|
5
|
-
* stack, MCPs, and morph-spec configuration. Uses _morph_managed marker
|
|
6
|
-
* to track managed entries for safe reset.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { existsSync } from 'fs';
|
|
11
|
-
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
12
|
-
|
|
13
|
-
const MORPH_MANAGED_KEY = '_morph_managed';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Generate and merge settings for a project
|
|
17
|
-
* @param {string} targetPath - Project root directory
|
|
18
|
-
* @param {Object} detectedConfig - Output from detectClaudeConfig()
|
|
19
|
-
* @param {Object} morphConfig - morph-spec config (from .morph/config/config.json)
|
|
20
|
-
* @returns {Promise<Object>} Generated settings summary
|
|
21
|
-
*/
|
|
22
|
-
export async function generateSettings(targetPath, detectedConfig, morphConfig = {}) {
|
|
23
|
-
const claudeDir = join(targetPath, '.claude');
|
|
24
|
-
const settingsPath = join(claudeDir, 'settings.local.json');
|
|
25
|
-
|
|
26
|
-
// Read existing settings
|
|
27
|
-
const existing = await readSettingsSafe(settingsPath);
|
|
28
|
-
|
|
29
|
-
// Build stack-based permissions
|
|
30
|
-
const stack = morphConfig?.project?.architecture || 'unknown';
|
|
31
|
-
const permissions = buildPermissions(stack, detectedConfig);
|
|
32
|
-
|
|
33
|
-
// Build generated settings
|
|
34
|
-
const generated = {};
|
|
35
|
-
|
|
36
|
-
if (permissions.length > 0) {
|
|
37
|
-
generated.permissions = permissions;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Track what we manage
|
|
41
|
-
const managed = {
|
|
42
|
-
permissions_added: permissions,
|
|
43
|
-
mcps_detected: detectedConfig.mcpServers.map(s => s.name),
|
|
44
|
-
plugins_detected: detectedConfig.plugins.map(p => p.name),
|
|
45
|
-
generated_at: new Date().toISOString()
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Merge without overwriting user config
|
|
49
|
-
const merged = mergeSettings(existing, generated);
|
|
50
|
-
merged[MORPH_MANAGED_KEY] = managed;
|
|
51
|
-
|
|
52
|
-
// Ensure directory exists
|
|
53
|
-
if (!existsSync(claudeDir)) {
|
|
54
|
-
await mkdir(claudeDir, { recursive: true });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
await writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
settingsPath,
|
|
61
|
-
permissionsAdded: permissions.length,
|
|
62
|
-
managed
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Build permission rules based on detected stack
|
|
68
|
-
* @param {string} stack - Detected project stack
|
|
69
|
-
* @param {Object} detectedConfig - Detected Claude config
|
|
70
|
-
* @returns {Array<string>} Permission patterns
|
|
71
|
-
*/
|
|
72
|
-
export function buildPermissions(stack, detectedConfig = {}) {
|
|
73
|
-
const permissions = [];
|
|
74
|
-
|
|
75
|
-
// Stack-based permissions
|
|
76
|
-
const stackPermissions = {
|
|
77
|
-
nodejs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
78
|
-
typescript: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)', 'Bash(tsc:*)'],
|
|
79
|
-
nextjs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
80
|
-
react: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
81
|
-
vue: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
82
|
-
dotnet: ['Bash(dotnet:*)', 'Bash(nuget:*)'],
|
|
83
|
-
python: ['Bash(python:*)', 'Bash(pip:*)', 'Bash(pytest:*)'],
|
|
84
|
-
go: ['Bash(go:*)']
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
if (stackPermissions[stack]) {
|
|
88
|
-
permissions.push(...stackPermissions[stack]);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// morph-spec CLI always needs node/npx
|
|
92
|
-
if (!permissions.includes('Bash(npx:*)')) {
|
|
93
|
-
permissions.push('Bash(npx:*)');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return permissions;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Deep merge settings without overwriting user entries
|
|
101
|
-
* @param {Object} existing - Current settings.local.json
|
|
102
|
-
* @param {Object} generated - Generated settings to merge
|
|
103
|
-
* @returns {Object} Merged settings
|
|
104
|
-
*/
|
|
105
|
-
export function mergeSettings(existing, generated) {
|
|
106
|
-
const result = { ...existing };
|
|
107
|
-
|
|
108
|
-
for (const [key, value] of Object.entries(generated)) {
|
|
109
|
-
if (key === MORPH_MANAGED_KEY) continue;
|
|
110
|
-
|
|
111
|
-
if (Array.isArray(value) && Array.isArray(result[key])) {
|
|
112
|
-
// Merge arrays without duplicates
|
|
113
|
-
const set = new Set(result[key]);
|
|
114
|
-
for (const item of value) {
|
|
115
|
-
set.add(item);
|
|
116
|
-
}
|
|
117
|
-
result[key] = [...set];
|
|
118
|
-
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
119
|
-
// Recursively merge objects
|
|
120
|
-
result[key] = mergeSettings(result[key] || {}, value);
|
|
121
|
-
} else if (!(key in result)) {
|
|
122
|
-
// Only add new keys, never overwrite existing
|
|
123
|
-
result[key] = value;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return result;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Remove only morph-spec managed entries from settings
|
|
132
|
-
* @param {string} settingsPath - Path to settings.local.json
|
|
133
|
-
* @returns {Promise<Object>} Summary of what was removed
|
|
134
|
-
*/
|
|
135
|
-
export async function resetMorphSettings(settingsPath) {
|
|
136
|
-
if (!existsSync(settingsPath)) {
|
|
137
|
-
return { removed: false, reason: 'file not found' };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const settings = await readSettingsSafe(settingsPath);
|
|
141
|
-
const managed = settings[MORPH_MANAGED_KEY];
|
|
142
|
-
|
|
143
|
-
if (!managed) {
|
|
144
|
-
return { removed: false, reason: 'no _morph_managed section found' };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Remove managed permissions
|
|
148
|
-
if (managed.permissions_added && Array.isArray(settings.permissions)) {
|
|
149
|
-
settings.permissions = settings.permissions.filter(
|
|
150
|
-
p => !managed.permissions_added.includes(p)
|
|
151
|
-
);
|
|
152
|
-
if (settings.permissions.length === 0) {
|
|
153
|
-
delete settings.permissions;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Remove the managed marker itself
|
|
158
|
-
delete settings[MORPH_MANAGED_KEY];
|
|
159
|
-
|
|
160
|
-
// Write back
|
|
161
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
removed: true,
|
|
165
|
-
permissionsRemoved: managed.permissions_added || [],
|
|
166
|
-
previouslyGenerated: managed.generated_at
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Save integrations.json with detected config
|
|
172
|
-
* @param {string} targetPath - Project root directory
|
|
173
|
-
* @param {Object} detectedConfig - Output from detectClaudeConfig()
|
|
174
|
-
*/
|
|
175
|
-
export async function saveIntegrations(targetPath, detectedConfig) {
|
|
176
|
-
const integrationsPath = join(targetPath, '.morph', 'config', 'integrations.json');
|
|
177
|
-
const integrationsDir = join(targetPath, '.morph', 'config');
|
|
178
|
-
|
|
179
|
-
if (!existsSync(integrationsDir)) {
|
|
180
|
-
await mkdir(integrationsDir, { recursive: true });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const integrations = {
|
|
184
|
-
version: '1.0.0',
|
|
185
|
-
detectedAt: new Date().toISOString(),
|
|
186
|
-
plugins: detectedConfig.plugins,
|
|
187
|
-
mcpServers: detectedConfig.mcpServers,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Settings Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates .claude/settings.local.json for projects based on detected
|
|
5
|
+
* stack, MCPs, and morph-spec configuration. Uses _morph_managed marker
|
|
6
|
+
* to track managed entries for safe reset.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
12
|
+
|
|
13
|
+
const MORPH_MANAGED_KEY = '_morph_managed';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate and merge settings for a project
|
|
17
|
+
* @param {string} targetPath - Project root directory
|
|
18
|
+
* @param {Object} detectedConfig - Output from detectClaudeConfig()
|
|
19
|
+
* @param {Object} morphConfig - morph-spec config (from .morph/config/config.json)
|
|
20
|
+
* @returns {Promise<Object>} Generated settings summary
|
|
21
|
+
*/
|
|
22
|
+
export async function generateSettings(targetPath, detectedConfig, morphConfig = {}) {
|
|
23
|
+
const claudeDir = join(targetPath, '.claude');
|
|
24
|
+
const settingsPath = join(claudeDir, 'settings.local.json');
|
|
25
|
+
|
|
26
|
+
// Read existing settings
|
|
27
|
+
const existing = await readSettingsSafe(settingsPath);
|
|
28
|
+
|
|
29
|
+
// Build stack-based permissions
|
|
30
|
+
const stack = morphConfig?.project?.architecture || 'unknown';
|
|
31
|
+
const permissions = buildPermissions(stack, detectedConfig);
|
|
32
|
+
|
|
33
|
+
// Build generated settings
|
|
34
|
+
const generated = {};
|
|
35
|
+
|
|
36
|
+
if (permissions.length > 0) {
|
|
37
|
+
generated.permissions = permissions;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Track what we manage
|
|
41
|
+
const managed = {
|
|
42
|
+
permissions_added: permissions,
|
|
43
|
+
mcps_detected: detectedConfig.mcpServers.map(s => s.name),
|
|
44
|
+
plugins_detected: detectedConfig.plugins.map(p => p.name),
|
|
45
|
+
generated_at: new Date().toISOString()
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Merge without overwriting user config
|
|
49
|
+
const merged = mergeSettings(existing, generated);
|
|
50
|
+
merged[MORPH_MANAGED_KEY] = managed;
|
|
51
|
+
|
|
52
|
+
// Ensure directory exists
|
|
53
|
+
if (!existsSync(claudeDir)) {
|
|
54
|
+
await mkdir(claudeDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
settingsPath,
|
|
61
|
+
permissionsAdded: permissions.length,
|
|
62
|
+
managed
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build permission rules based on detected stack
|
|
68
|
+
* @param {string} stack - Detected project stack
|
|
69
|
+
* @param {Object} detectedConfig - Detected Claude config
|
|
70
|
+
* @returns {Array<string>} Permission patterns
|
|
71
|
+
*/
|
|
72
|
+
export function buildPermissions(stack, detectedConfig = {}) {
|
|
73
|
+
const permissions = [];
|
|
74
|
+
|
|
75
|
+
// Stack-based permissions
|
|
76
|
+
const stackPermissions = {
|
|
77
|
+
nodejs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
78
|
+
typescript: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)', 'Bash(tsc:*)'],
|
|
79
|
+
nextjs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
80
|
+
react: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
81
|
+
vue: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
82
|
+
dotnet: ['Bash(dotnet:*)', 'Bash(nuget:*)'],
|
|
83
|
+
python: ['Bash(python:*)', 'Bash(pip:*)', 'Bash(pytest:*)'],
|
|
84
|
+
go: ['Bash(go:*)']
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (stackPermissions[stack]) {
|
|
88
|
+
permissions.push(...stackPermissions[stack]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// morph-spec CLI always needs node/npx
|
|
92
|
+
if (!permissions.includes('Bash(npx:*)')) {
|
|
93
|
+
permissions.push('Bash(npx:*)');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return permissions;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Deep merge settings without overwriting user entries
|
|
101
|
+
* @param {Object} existing - Current settings.local.json
|
|
102
|
+
* @param {Object} generated - Generated settings to merge
|
|
103
|
+
* @returns {Object} Merged settings
|
|
104
|
+
*/
|
|
105
|
+
export function mergeSettings(existing, generated) {
|
|
106
|
+
const result = { ...existing };
|
|
107
|
+
|
|
108
|
+
for (const [key, value] of Object.entries(generated)) {
|
|
109
|
+
if (key === MORPH_MANAGED_KEY) continue;
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(value) && Array.isArray(result[key])) {
|
|
112
|
+
// Merge arrays without duplicates
|
|
113
|
+
const set = new Set(result[key]);
|
|
114
|
+
for (const item of value) {
|
|
115
|
+
set.add(item);
|
|
116
|
+
}
|
|
117
|
+
result[key] = [...set];
|
|
118
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
119
|
+
// Recursively merge objects
|
|
120
|
+
result[key] = mergeSettings(result[key] || {}, value);
|
|
121
|
+
} else if (!(key in result)) {
|
|
122
|
+
// Only add new keys, never overwrite existing
|
|
123
|
+
result[key] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Remove only morph-spec managed entries from settings
|
|
132
|
+
* @param {string} settingsPath - Path to settings.local.json
|
|
133
|
+
* @returns {Promise<Object>} Summary of what was removed
|
|
134
|
+
*/
|
|
135
|
+
export async function resetMorphSettings(settingsPath) {
|
|
136
|
+
if (!existsSync(settingsPath)) {
|
|
137
|
+
return { removed: false, reason: 'file not found' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const settings = await readSettingsSafe(settingsPath);
|
|
141
|
+
const managed = settings[MORPH_MANAGED_KEY];
|
|
142
|
+
|
|
143
|
+
if (!managed) {
|
|
144
|
+
return { removed: false, reason: 'no _morph_managed section found' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Remove managed permissions
|
|
148
|
+
if (managed.permissions_added && Array.isArray(settings.permissions)) {
|
|
149
|
+
settings.permissions = settings.permissions.filter(
|
|
150
|
+
p => !managed.permissions_added.includes(p)
|
|
151
|
+
);
|
|
152
|
+
if (settings.permissions.length === 0) {
|
|
153
|
+
delete settings.permissions;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Remove the managed marker itself
|
|
158
|
+
delete settings[MORPH_MANAGED_KEY];
|
|
159
|
+
|
|
160
|
+
// Write back
|
|
161
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
removed: true,
|
|
165
|
+
permissionsRemoved: managed.permissions_added || [],
|
|
166
|
+
previouslyGenerated: managed.generated_at
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Save integrations.json with detected config
|
|
172
|
+
* @param {string} targetPath - Project root directory
|
|
173
|
+
* @param {Object} detectedConfig - Output from detectClaudeConfig()
|
|
174
|
+
*/
|
|
175
|
+
export async function saveIntegrations(targetPath, detectedConfig) {
|
|
176
|
+
const integrationsPath = join(targetPath, '.morph', 'config', 'integrations.json');
|
|
177
|
+
const integrationsDir = join(targetPath, '.morph', 'config');
|
|
178
|
+
|
|
179
|
+
if (!existsSync(integrationsDir)) {
|
|
180
|
+
await mkdir(integrationsDir, { recursive: true });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const integrations = {
|
|
184
|
+
version: '1.0.0',
|
|
185
|
+
detectedAt: new Date().toISOString(),
|
|
186
|
+
plugins: detectedConfig.plugins,
|
|
187
|
+
mcpServers: detectedConfig.mcpServers,
|
|
188
|
+
customSkills: detectedConfig.customSkills.length,
|
|
189
|
+
customCommands: detectedConfig.customCommands.length,
|
|
190
|
+
hooks: detectedConfig.hooks.length
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
await writeFile(integrationsPath, JSON.stringify(integrations, null, 2) + '\n', 'utf-8');
|
|
194
|
+
return integrationsPath;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// Helpers
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
async function readSettingsSafe(settingsPath) {
|
|
202
|
+
try {
|
|
203
|
+
if (!existsSync(settingsPath)) return {};
|
|
204
|
+
return JSON.parse(await readFile(settingsPath, 'utf-8'));
|
|
205
|
+
} catch {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
}
|