@polymorphism-tech/morph-spec 4.7.1 → 4.7.2
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/.morph/.morphversion +5 -0
- package/.morph/analytics/threads-log.jsonl +5 -0
- package/.morph/config/config.json +8 -0
- package/.morph/framework/agents.json +1815 -0
- package/.morph/framework/hooks/README.md +205 -0
- package/.morph/framework/hooks/claude-code/notification/approval-reminder.js +54 -0
- package/.morph/framework/hooks/claude-code/post-tool-use/dispatch.js +83 -0
- package/.morph/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +42 -0
- package/.morph/framework/hooks/claude-code/pre-compact/save-morph-context.js +61 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +71 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +58 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +64 -0
- package/.morph/framework/hooks/claude-code/session-start/inject-morph-context.js +94 -0
- package/.morph/framework/hooks/claude-code/statusline.py +538 -0
- package/.morph/framework/hooks/claude-code/statusline.sh +7 -0
- package/.morph/framework/hooks/claude-code/stop/validate-completion.js +88 -0
- package/.morph/framework/hooks/claude-code/user-prompt/enrich-prompt.js +91 -0
- package/.morph/framework/hooks/git/commit-msg/conventional-commits.sh +33 -0
- package/.morph/framework/hooks/git/pre-commit/agents.sh +25 -0
- package/.morph/framework/hooks/git/pre-commit/orchestrator.sh +64 -0
- package/.morph/framework/hooks/git/pre-commit/specs.sh +50 -0
- package/.morph/framework/hooks/git/pre-push/run-tests.sh +44 -0
- package/.morph/framework/hooks/shared/hook-response.js +45 -0
- package/.morph/framework/hooks/shared/phase-utils.js +129 -0
- package/.morph/framework/hooks/shared/state-reader.js +138 -0
- package/.morph/framework/hooks/shared/stdin-reader.js +26 -0
- package/.morph/framework/standards/STANDARDS.json +933 -0
- package/.morph/framework/standards/ai-agents/blazor-ui.md +364 -0
- package/.morph/framework/standards/ai-agents/production.md +415 -0
- package/.morph/framework/standards/ai-agents/setup.md +418 -0
- package/.morph/framework/standards/ai-agents/team-orchestration.md +479 -0
- package/.morph/framework/standards/ai-agents/workflows.md +354 -0
- package/.morph/framework/standards/architecture/ddd/aggregates.md +120 -0
- package/.morph/framework/standards/architecture/ddd/bounded-contexts.md +105 -0
- package/.morph/framework/standards/architecture/ddd/complexity-levels.md +108 -0
- package/.morph/framework/standards/architecture/ddd/entities.md +99 -0
- package/.morph/framework/standards/architecture/ddd/ubiquitous-language.md +58 -0
- package/.morph/framework/standards/architecture/ddd/value-objects.md +124 -0
- package/.morph/framework/standards/backend/api/minimal-api.md +494 -0
- package/.morph/framework/standards/backend/api/rest.md +492 -0
- package/.morph/framework/standards/backend/api/validation.md +88 -0
- package/.morph/framework/standards/backend/authentication/passkeys.md +428 -0
- package/.morph/framework/standards/backend/database/ef-core.md +199 -0
- package/.morph/framework/standards/backend/database/migrations.md +393 -0
- package/.morph/framework/standards/backend/database/postgresql/database.md +352 -0
- package/.morph/framework/standards/backend/database/repository-patterns.md +528 -0
- package/.morph/framework/standards/backend/database/vector-search-rag.md +541 -0
- package/.morph/framework/standards/backend/dotnet/async.md +366 -0
- package/.morph/framework/standards/backend/dotnet/core.md +117 -0
- package/.morph/framework/standards/backend/dotnet/di.md +439 -0
- package/.morph/framework/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/.morph/framework/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/.morph/framework/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/.morph/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/.morph/framework/standards/backend/integrations/resend/resend-email.md +385 -0
- package/.morph/framework/standards/context/analytics.md +96 -0
- package/.morph/framework/standards/context/bundles.md +110 -0
- package/.morph/framework/standards/context/priming.md +78 -0
- package/.morph/framework/standards/core/architecture.md +185 -0
- package/.morph/framework/standards/core/coding.md +214 -0
- package/.morph/framework/standards/core/git-branching-strategy.md +403 -0
- package/.morph/framework/standards/core/git.md +185 -0
- package/.morph/framework/standards/core/testing.md +295 -0
- package/.morph/framework/standards/data/nosql/blob-storage.md +102 -0
- package/.morph/framework/standards/data/nosql/cache/redis.md +97 -0
- package/.morph/framework/standards/data/nosql/cosmos-db.md +118 -0
- package/.morph/framework/standards/data/vector-search/azure-ai-search.md +121 -0
- package/.morph/framework/standards/data/vector-search/rag-chunking.md +104 -0
- package/.morph/framework/standards/frontend/blazor/design-checklist.md +222 -0
- package/.morph/framework/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/.morph/framework/standards/frontend/blazor/fluent-ui.md +137 -0
- package/.morph/framework/standards/frontend/blazor/html-conversion.md +184 -0
- package/.morph/framework/standards/frontend/blazor/lifecycle.md +195 -0
- package/.morph/framework/standards/frontend/blazor/pitfalls.md +198 -0
- package/.morph/framework/standards/frontend/blazor/state.md +191 -0
- package/.morph/framework/standards/frontend/design-system/animations.md +151 -0
- package/.morph/framework/standards/frontend/design-system/naming.md +64 -0
- package/.morph/framework/standards/frontend/nextjs/app-router.md +123 -0
- package/.morph/framework/standards/frontend/nextjs/components.md +132 -0
- package/.morph/framework/standards/frontend/nextjs/data-fetching.md +126 -0
- package/.morph/framework/standards/frontend/nextjs/forms.md +128 -0
- package/.morph/framework/standards/frontend/nextjs/naming-conventions.md +67 -0
- package/.morph/framework/standards/frontend/nextjs/nextjs-patterns.md +215 -0
- package/.morph/framework/standards/frontend/nextjs/project-structure.md +102 -0
- package/.morph/framework/standards/frontend/nextjs/state-management.md +72 -0
- package/.morph/framework/standards/frontend/nextjs/testing.md +111 -0
- package/.morph/framework/standards/infrastructure/azure/azure.md +624 -0
- package/.morph/framework/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/.morph/framework/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/.morph/framework/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/.morph/framework/standards/infrastructure/azure/services/functions.md +486 -0
- package/.morph/framework/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/.morph/framework/standards/infrastructure/azure/services/storage.md +407 -0
- package/.morph/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/.morph/framework/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/.morph/framework/standards/integration/api/graphql.md +91 -0
- package/.morph/framework/standards/integration/api/grpc.md +114 -0
- package/.morph/framework/standards/integration/api/rest-design.md +95 -0
- package/.morph/framework/standards/integration/event-driven/cqrs.md +101 -0
- package/.morph/framework/standards/integration/event-driven/event-sourcing.md +124 -0
- package/.morph/framework/standards/integration/event-driven/service-bus.md +95 -0
- package/.morph/framework/standards/integration/mcp/mcp-tools.md +384 -0
- package/.morph/framework/standards/observability/logging.md +131 -0
- package/.morph/framework/standards/observability/metrics.md +121 -0
- package/.morph/framework/standards/observability/monitoring.md +114 -0
- package/.morph/framework/standards/observability/tracing.md +132 -0
- package/.morph/framework/standards/workflows/parallel-execution.md +112 -0
- package/.morph/framework/standards/workflows/thread-management.md +113 -0
- package/.morph/framework/templates/.idea/morph-templates.xml +92 -0
- package/.morph/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/.morph/framework/templates/IDE-SNIPPETS.md +266 -0
- package/.morph/framework/templates/README.md +814 -0
- package/.morph/framework/templates/REGISTRY.json +1888 -0
- package/.morph/framework/templates/code/dotnet/backend/repository.cs +141 -0
- package/.morph/framework/templates/code/dotnet/backend/service.cs +139 -0
- package/.morph/framework/templates/code/dotnet/contracts/Commands.cs +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/Entities.cs +25 -0
- package/.morph/framework/templates/code/dotnet/contracts/Queries.cs +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/README.md +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level1.cs +69 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level2.cs +86 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level3.cs +41 -0
- package/.morph/framework/templates/code/dotnet/database/migration.cs +83 -0
- package/.morph/framework/templates/code/dotnet/frontend/component.razor +239 -0
- package/.morph/framework/templates/code/dotnet/jobs/agent.cs +163 -0
- package/.morph/framework/templates/code/dotnet/jobs/job.cs +171 -0
- package/.morph/framework/templates/code/dotnet/test.cs +239 -0
- package/.morph/framework/templates/code/sql/rls-policy.sql +57 -0
- package/.morph/framework/templates/code/sql/supabase-migration.sql +100 -0
- package/.morph/framework/templates/code/sql/supabase-migration.template.sql +113 -0
- package/.morph/framework/templates/code/typescript/contracts.ts +168 -0
- package/.morph/framework/templates/context/CONTEXT-FEATURE.md +276 -0
- package/.morph/framework/templates/context/CONTEXT.md +181 -0
- package/.morph/framework/templates/docs/clarifications.md +253 -0
- package/.morph/framework/templates/docs/onboarding.md +123 -0
- package/.morph/framework/templates/docs/proposal.md +182 -0
- package/.morph/framework/templates/docs/schema-analysis.md +119 -0
- package/.morph/framework/templates/docs/spec.md +198 -0
- package/.morph/framework/templates/docs/ui-components.md +124 -0
- package/.morph/framework/templates/docs/ui-design-system.md +76 -0
- package/.morph/framework/templates/docs/ui-flows.md +167 -0
- package/.morph/framework/templates/docs/ui-mockups.md +98 -0
- package/.morph/framework/templates/docs/user-stories.md +34 -0
- package/.morph/framework/templates/examples/design-system-examples.md +357 -0
- package/.morph/framework/templates/examples/spec-examples.md +90 -0
- package/.morph/framework/templates/feature/decisions.md +187 -0
- package/.morph/framework/templates/feature/recap.md +146 -0
- package/.morph/framework/templates/feature/tasks.md +199 -0
- package/.morph/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -0
- package/.morph/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -0
- package/.morph/framework/templates/frontend/nextjs/env.mjs.hbs +32 -0
- package/.morph/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -0
- package/.morph/framework/templates/frontend/nextjs/page.tsx.hbs +22 -0
- package/.morph/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -0
- package/.morph/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -0
- package/.morph/framework/templates/infrastructure/azure/Dockerfile.example +82 -0
- package/.morph/framework/templates/infrastructure/azure/README.md +286 -0
- package/.morph/framework/templates/infrastructure/azure/app-insights.bicep +63 -0
- package/.morph/framework/templates/infrastructure/azure/app-service.bicep +164 -0
- package/.morph/framework/templates/infrastructure/azure/container-app-env.bicep +49 -0
- package/.morph/framework/templates/infrastructure/azure/container-app.bicep +156 -0
- package/.morph/framework/templates/infrastructure/azure/deploy-checklist.md +426 -0
- package/.morph/framework/templates/infrastructure/azure/deploy.ps1 +229 -0
- package/.morph/framework/templates/infrastructure/azure/deploy.sh +208 -0
- package/.morph/framework/templates/infrastructure/azure/key-vault.bicep +91 -0
- package/.morph/framework/templates/infrastructure/azure/main.bicep +189 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.dev.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.prod.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.staging.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/sql-database.bicep +103 -0
- package/.morph/framework/templates/infrastructure/azure/storage.bicep +106 -0
- package/.morph/framework/templates/infrastructure/docker/Dockerfile.template +58 -0
- package/.morph/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -0
- package/.morph/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -0
- package/.morph/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -0
- package/.morph/framework/templates/infrastructure/docker/easypanel.template.json +54 -0
- package/.morph/framework/templates/infrastructure/github/README.md +593 -0
- package/.morph/framework/templates/infrastructure/github/actions/azure-auth/action.yml.hbs +22 -0
- package/.morph/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -0
- package/.morph/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -0
- package/.morph/framework/templates/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +61 -0
- package/.morph/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +31 -0
- package/.morph/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -0
- package/.morph/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -0
- package/.morph/framework/templates/integrations/asaas-client.cs +387 -0
- package/.morph/framework/templates/integrations/asaas-webhook.cs +351 -0
- package/.morph/framework/templates/integrations/azure-identity-config.cs +288 -0
- package/.morph/framework/templates/integrations/clerk-config.cs +258 -0
- package/.morph/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -0
- package/.morph/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-retry.md +78 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-validation.md +97 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-wrapper.md +36 -0
- package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -0
- package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +126 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/squad-leader.md +43 -0
- package/.morph/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -0
- package/.morph/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -0
- package/.morph/framework/templates/project-structure/dotnet-ddd.md +70 -0
- package/.morph/framework/templates/saas/subscription.cs +347 -0
- package/.morph/framework/templates/saas/tenant.cs +338 -0
- package/.morph/framework/templates/state.template.json +17 -0
- package/.morph/framework/templates/ui/FluentDesignTheme.cs +149 -0
- package/.morph/framework/templates/ui/MudTheme.cs +281 -0
- package/.morph/framework/templates/ui/design-system.css +226 -0
- package/.morph/logs/tool-failures.log +17 -0
- package/.morph/memory/pre-compact-2026-02-24T17-43-30-049Z.json +16 -0
- package/.morph/plans/eager-watching-bunny.md +105 -0
- package/.morph/plans/temporal-seeking-nebula.md +45 -0
- package/.morph/state.json +48 -0
- package/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +0 -9
- package/framework/CLAUDE.md +1 -1
- package/framework/hooks/README.md +10 -6
- package/framework/hooks/claude-code/notification/approval-reminder.js +2 -0
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +1 -1
- package/framework/hooks/claude-code/stop/validate-completion.js +1 -1
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +1 -1
- package/package.json +1 -1
- package/src/commands/project/init.js +15 -42
- package/src/commands/project/update.js +22 -37
- package/src/lib/installers/mcp-installer.js +18 -3
- package/src/utils/hooks-installer.js +5 -15
- package/src/commands/project/detect.js +0 -114
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stop Hook: Validate Completion (Advisory)
|
|
5
|
+
*
|
|
6
|
+
* Event: Stop
|
|
7
|
+
*
|
|
8
|
+
* When Claude stops, checks for incomplete work:
|
|
9
|
+
* - In implement phase: checks uncompleted tasks
|
|
10
|
+
* - In spec phases: checks required outputs still created: false
|
|
11
|
+
* - Returns additionalContext with what remains (advisory, not blocking)
|
|
12
|
+
*
|
|
13
|
+
* Uses stop_hook_active env check to prevent infinite loops.
|
|
14
|
+
*
|
|
15
|
+
* Fail-open: exits 0 on any error.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { stateExists, getActiveFeature, getMissingOutputs } from '../../shared/state-reader.js';
|
|
19
|
+
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Prevent infinite loop
|
|
23
|
+
if (process.env.MORPH_STOP_HOOK_ACTIVE === '1') pass();
|
|
24
|
+
|
|
25
|
+
if (!stateExists()) pass();
|
|
26
|
+
|
|
27
|
+
const active = getActiveFeature();
|
|
28
|
+
if (!active) pass();
|
|
29
|
+
|
|
30
|
+
const { name, feature } = active;
|
|
31
|
+
const warnings = [];
|
|
32
|
+
|
|
33
|
+
// Check for incomplete tasks during implement phase
|
|
34
|
+
if (feature.phase === 'implement' && feature.tasks) {
|
|
35
|
+
const remaining = (feature.tasks.total || 0) - (feature.tasks.completed || 0);
|
|
36
|
+
if (remaining > 0) {
|
|
37
|
+
warnings.push(`${remaining} task(s) remaining for feature '${name}'`);
|
|
38
|
+
if (feature.tasks.inProgress > 0) {
|
|
39
|
+
warnings.push(` ${feature.tasks.inProgress} task(s) still in progress`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check for missing outputs in spec phases
|
|
45
|
+
if (['proposal', 'design', 'clarify', 'tasks', 'uiux'].includes(feature.phase)) {
|
|
46
|
+
const missing = getMissingOutputs(name);
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
warnings.push(`Missing outputs for '${name}' (${feature.phase} phase):`);
|
|
49
|
+
for (const output of missing.slice(0, 5)) {
|
|
50
|
+
warnings.push(` - ${output.type}: ${output.path}`);
|
|
51
|
+
}
|
|
52
|
+
if (missing.length > 5) {
|
|
53
|
+
warnings.push(` ... and ${missing.length - 5} more`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check pending approval gates
|
|
59
|
+
if (feature.approvalGates) {
|
|
60
|
+
const pendingGates = Object.entries(feature.approvalGates)
|
|
61
|
+
.filter(([, gate]) => !gate.approved && !gate.timestamp)
|
|
62
|
+
.map(([name]) => name);
|
|
63
|
+
|
|
64
|
+
// Only warn about gates relevant to current/past phases
|
|
65
|
+
const relevantGates = pendingGates.filter(gate => {
|
|
66
|
+
const gatePhaseMap = { proposal: 'proposal', uiux: 'uiux', design: 'design', tasks: 'tasks' };
|
|
67
|
+
return gatePhaseMap[gate] !== undefined;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (relevantGates.length > 0) {
|
|
71
|
+
warnings.push(`Pending approvals: ${relevantGates.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (warnings.length === 0) pass();
|
|
76
|
+
|
|
77
|
+
const message = [
|
|
78
|
+
'MORPH-SPEC: Incomplete work detected:',
|
|
79
|
+
...warnings.map(w => ` ${w}`),
|
|
80
|
+
'',
|
|
81
|
+
'Resume with: morph-spec status ' + name
|
|
82
|
+
].join('\n');
|
|
83
|
+
|
|
84
|
+
injectContext(message);
|
|
85
|
+
} catch {
|
|
86
|
+
// Fail-open
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UserPromptSubmit Hook: Enrich User Prompts with Context
|
|
5
|
+
*
|
|
6
|
+
* Event: UserPromptSubmit
|
|
7
|
+
*
|
|
8
|
+
* Scans user prompt for morph-spec keywords and injects relevant context:
|
|
9
|
+
* - Feature name mentioned → inject its current status/phase
|
|
10
|
+
* - "implement"/"code" while not in implement phase → warn about wrong phase
|
|
11
|
+
* - "approve"/"looks good" → inject approval command syntax
|
|
12
|
+
* - "next task" → inject current task list
|
|
13
|
+
*
|
|
14
|
+
* Fail-open: exits 0 on any error.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readStdin } from '../../shared/stdin-reader.js';
|
|
18
|
+
import { stateExists, loadState, getActiveFeature, getFeature, getPendingGates } from '../../shared/state-reader.js';
|
|
19
|
+
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (!stateExists()) pass();
|
|
23
|
+
|
|
24
|
+
const payload = await readStdin();
|
|
25
|
+
if (!payload) pass();
|
|
26
|
+
|
|
27
|
+
const userPrompt = payload?.prompt || payload?.user_prompt || payload?.content || '';
|
|
28
|
+
if (!userPrompt || userPrompt.length < 3) pass();
|
|
29
|
+
|
|
30
|
+
const promptLower = userPrompt.toLowerCase();
|
|
31
|
+
const state = loadState();
|
|
32
|
+
if (!state?.features) pass();
|
|
33
|
+
|
|
34
|
+
const context = [];
|
|
35
|
+
|
|
36
|
+
// Check if a feature name is mentioned
|
|
37
|
+
for (const [featureName, feature] of Object.entries(state.features)) {
|
|
38
|
+
if (promptLower.includes(featureName.toLowerCase())) {
|
|
39
|
+
context.push(`[morph-spec] Feature '${featureName}': phase=${feature.phase}, status=${feature.status}`);
|
|
40
|
+
if (feature.tasks?.total > 0) {
|
|
41
|
+
context.push(` Tasks: ${feature.tasks.completed || 0}/${feature.tasks.total} completed`);
|
|
42
|
+
}
|
|
43
|
+
break; // Only inject for the first matched feature
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for implement/code intent while not in implement phase
|
|
48
|
+
const active = getActiveFeature();
|
|
49
|
+
if (active) {
|
|
50
|
+
const { name, feature } = active;
|
|
51
|
+
|
|
52
|
+
const codeKeywords = ['implement', 'code', 'start coding', 'write the code', 'build it', 'let\'s build'];
|
|
53
|
+
const wantsToCode = codeKeywords.some(kw => promptLower.includes(kw));
|
|
54
|
+
|
|
55
|
+
if (wantsToCode && feature.phase !== 'implement' && feature.phase !== 'sync') {
|
|
56
|
+
context.push(
|
|
57
|
+
`[morph-spec] WARNING: Feature '${name}' is in '${feature.phase}' phase, not 'implement'.` +
|
|
58
|
+
` Complete the current phase first or advance: morph-spec phase advance ${name}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for approval intent
|
|
63
|
+
const approvalKeywords = ['approve', 'approved', 'looks good', 'lgtm', 'ship it'];
|
|
64
|
+
const wantsToApprove = approvalKeywords.some(kw => promptLower.includes(kw));
|
|
65
|
+
|
|
66
|
+
if (wantsToApprove) {
|
|
67
|
+
const pending = getPendingGates(name);
|
|
68
|
+
if (pending.length > 0) {
|
|
69
|
+
context.push(
|
|
70
|
+
`[morph-spec] To approve, use: morph-spec approve ${name} ${pending[0]}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for "next task" intent
|
|
76
|
+
if (promptLower.includes('next task') || promptLower.includes('what\'s next')) {
|
|
77
|
+
if (feature.phase === 'implement') {
|
|
78
|
+
context.push(
|
|
79
|
+
`[morph-spec] Use: morph-spec task next ${name}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (context.length === 0) pass();
|
|
86
|
+
|
|
87
|
+
injectContext(context.join('\n'));
|
|
88
|
+
} catch {
|
|
89
|
+
// Fail-open
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scope: universal | Validates commit message format (Conventional Commits)
|
|
3
|
+
# MORPH-SPEC Commit Message Hook
|
|
4
|
+
# Enforces Conventional Commits specification
|
|
5
|
+
|
|
6
|
+
COMMIT_MSG_FILE="$1"
|
|
7
|
+
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
|
8
|
+
|
|
9
|
+
# Conventional Commits pattern: type(scope): description
|
|
10
|
+
# Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci, revert
|
|
11
|
+
PATTERN="^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .{1,100}"
|
|
12
|
+
|
|
13
|
+
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
|
|
14
|
+
echo "❌ Invalid commit message format!"
|
|
15
|
+
echo ""
|
|
16
|
+
echo "Required format: <type>(<scope>): <description>"
|
|
17
|
+
echo ""
|
|
18
|
+
echo "Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci, revert"
|
|
19
|
+
echo ""
|
|
20
|
+
echo "Examples:"
|
|
21
|
+
echo " feat(auth): add JWT refresh token rotation"
|
|
22
|
+
echo " fix(api): resolve null reference in UserService"
|
|
23
|
+
echo " docs(readme): update installation instructions"
|
|
24
|
+
echo ""
|
|
25
|
+
echo "Your message:"
|
|
26
|
+
echo " $COMMIT_MSG"
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Override with: git commit --no-verify"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo "✅ Commit message format valid"
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Scope: universal | Validates agents.json Schema
|
|
3
|
+
# MORPH-SPEC Pre-Commit Hook: Agent Configuration Validation
|
|
4
|
+
# Uses validate-agents.js to check agents.json
|
|
5
|
+
|
|
6
|
+
echo "🤖 Validating agent configuration..."
|
|
7
|
+
|
|
8
|
+
# Check if agents.json is being modified
|
|
9
|
+
if git diff --cached --name-only | grep -q 'agents\.json$'; then
|
|
10
|
+
echo "Detected changes to agents.json"
|
|
11
|
+
|
|
12
|
+
# Run validator
|
|
13
|
+
if ! npx morph-spec validate-agents-skills; then
|
|
14
|
+
echo ""
|
|
15
|
+
echo "❌ COMMIT BLOCKED: agents.json validation failed"
|
|
16
|
+
echo " Fix errors above before committing"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
echo "✓ agents.json is valid"
|
|
21
|
+
else
|
|
22
|
+
echo "✓ No changes to agents.json"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
exit 0
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scope: universal | Orchestrates framework + stack-specific pre-commit hooks
|
|
3
|
+
# MORPH-SPEC Master Pre-Commit Hook
|
|
4
|
+
# Runs universal framework hooks + stack-specific hooks in sequence
|
|
5
|
+
|
|
6
|
+
echo "╔════════════════════════════════════════════════╗"
|
|
7
|
+
echo "║ MORPH-SPEC PRE-COMMIT VALIDATION ║"
|
|
8
|
+
echo "╚════════════════════════════════════════════════╝"
|
|
9
|
+
echo ""
|
|
10
|
+
|
|
11
|
+
FRAMEWORK_HOOKS_DIR="$(dirname "$0")"
|
|
12
|
+
STACK_HOOKS_DIR=".morph/hooks/pre-commit"
|
|
13
|
+
|
|
14
|
+
HAS_FAILURES=false
|
|
15
|
+
|
|
16
|
+
# Run framework hooks (universal - all stacks)
|
|
17
|
+
echo "📦 Running framework hooks (universal)..."
|
|
18
|
+
for hook in "$FRAMEWORK_HOOKS_DIR"/*.sh; do
|
|
19
|
+
# Skip orchestrator itself
|
|
20
|
+
if [[ "$hook" == */orchestrator.sh ]]; then
|
|
21
|
+
continue
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [[ -f "$hook" ]]; then
|
|
25
|
+
hook_name=$(basename "$hook")
|
|
26
|
+
echo "─────────────────────────────────────────────────"
|
|
27
|
+
if ! bash "$hook"; then
|
|
28
|
+
HAS_FAILURES=true
|
|
29
|
+
echo "❌ Framework hook failed: $hook_name"
|
|
30
|
+
fi
|
|
31
|
+
echo ""
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
# Run stack-specific hooks (if exist)
|
|
36
|
+
if [[ -d "$STACK_HOOKS_DIR" ]]; then
|
|
37
|
+
echo "🎯 Running stack-specific hooks..."
|
|
38
|
+
for hook in "$STACK_HOOKS_DIR"/*.sh; do
|
|
39
|
+
if [[ -f "$hook" ]]; then
|
|
40
|
+
hook_name=$(basename "$hook")
|
|
41
|
+
echo "─────────────────────────────────────────────────"
|
|
42
|
+
if ! bash "$hook"; then
|
|
43
|
+
HAS_FAILURES=true
|
|
44
|
+
echo "❌ Stack hook failed: $hook_name"
|
|
45
|
+
fi
|
|
46
|
+
echo ""
|
|
47
|
+
fi
|
|
48
|
+
done
|
|
49
|
+
else
|
|
50
|
+
echo "ℹ️ No stack-specific hooks found (optional)"
|
|
51
|
+
echo ""
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
echo "═════════════════════════════════════════════════"
|
|
55
|
+
|
|
56
|
+
if [[ "$HAS_FAILURES" == "true" ]]; then
|
|
57
|
+
echo "❌ PRE-COMMIT VALIDATION FAILED"
|
|
58
|
+
echo " Fix errors above before committing"
|
|
59
|
+
echo " Or use: git commit --no-verify to skip"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
echo "✅ ALL PRE-COMMIT VALIDATIONS PASSED"
|
|
64
|
+
exit 0
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Scope: universal | Validates spec.md required sections
|
|
3
|
+
# MORPH-SPEC Pre-Commit Hook: Spec Validation
|
|
4
|
+
# Validates that spec.md files have required sections
|
|
5
|
+
|
|
6
|
+
echo "🔍 Validating spec files..."
|
|
7
|
+
|
|
8
|
+
# Find modified spec.md files
|
|
9
|
+
SPEC_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep 'spec\.md$')
|
|
10
|
+
|
|
11
|
+
if [ -z "$SPEC_FILES" ]; then
|
|
12
|
+
echo "✓ No spec files modified"
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
HAS_ERRORS=false
|
|
17
|
+
|
|
18
|
+
for spec_file in $SPEC_FILES; do
|
|
19
|
+
echo "Checking: $spec_file"
|
|
20
|
+
|
|
21
|
+
# Required sections
|
|
22
|
+
REQUIRED_SECTIONS=(
|
|
23
|
+
"## 📋 Metadata"
|
|
24
|
+
"## 🎯 Overview"
|
|
25
|
+
"## 🏗️ Technical Design"
|
|
26
|
+
"## ✅ Acceptance Criteria"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
for section in "${REQUIRED_SECTIONS[@]}"; do
|
|
30
|
+
if ! grep -q "$section" "$spec_file"; then
|
|
31
|
+
echo " ❌ Missing section: $section"
|
|
32
|
+
HAS_ERRORS=true
|
|
33
|
+
fi
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
# Check if has at least one user story or requirement
|
|
37
|
+
if ! grep -qi "user story\|requirement\|acceptance criteria" "$spec_file"; then
|
|
38
|
+
echo " ⚠️ Warning: No user stories or requirements found"
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
if [ "$HAS_ERRORS" = true ]; then
|
|
43
|
+
echo ""
|
|
44
|
+
echo "❌ COMMIT BLOCKED: spec.md files are incomplete"
|
|
45
|
+
echo " Add missing sections before committing"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo "✓ All spec files are valid"
|
|
50
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scope: universal | Runs test suite before push
|
|
3
|
+
# MORPH-SPEC Pre-Push Hook
|
|
4
|
+
# Ensures all tests pass before pushing to remote
|
|
5
|
+
|
|
6
|
+
echo "🧪 Running test suite before push..."
|
|
7
|
+
echo ""
|
|
8
|
+
|
|
9
|
+
# Detect project type and run appropriate tests
|
|
10
|
+
if [[ -f *.csproj ]] || find . -maxdepth 2 -name "*.csproj" 2>/dev/null | grep -q .; then
|
|
11
|
+
# .NET project detected
|
|
12
|
+
echo "📦 Detected .NET project"
|
|
13
|
+
|
|
14
|
+
if ! dotnet test --verbosity minimal --no-build --no-restore 2>/dev/null; then
|
|
15
|
+
echo ""
|
|
16
|
+
echo "❌ .NET tests failed!"
|
|
17
|
+
echo ""
|
|
18
|
+
echo "Fix failing tests before pushing."
|
|
19
|
+
echo "Override with: git push --no-verify"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
elif [[ -f package.json ]]; then
|
|
24
|
+
# Node.js project detected
|
|
25
|
+
echo "📦 Detected Node.js project"
|
|
26
|
+
|
|
27
|
+
if ! npm test 2>/dev/null; then
|
|
28
|
+
echo ""
|
|
29
|
+
echo "❌ Node.js tests failed!"
|
|
30
|
+
echo ""
|
|
31
|
+
echo "Fix failing tests before pushing."
|
|
32
|
+
echo "Override with: git push --no-verify"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
else
|
|
37
|
+
echo "⚠️ No recognized test framework found"
|
|
38
|
+
echo " Skipping test execution"
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo ""
|
|
43
|
+
echo "✅ All tests passed - safe to push"
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hook response builders for Claude Code hooks.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code hooks communicate via JSON to stdout.
|
|
5
|
+
* - PreToolUse: { "decision": "block"|"approve", "reason": "..." }
|
|
6
|
+
* - SessionStart/Stop/etc: { "additionalContext": "..." }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Block a tool use with a reason.
|
|
11
|
+
* @param {string} reason - Human-readable reason for blocking
|
|
12
|
+
*/
|
|
13
|
+
export function block(reason) {
|
|
14
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Approve a tool use, optionally injecting context.
|
|
20
|
+
* @param {string} [context] - Additional context to inject
|
|
21
|
+
*/
|
|
22
|
+
export function approve(context) {
|
|
23
|
+
if (context) {
|
|
24
|
+
console.log(JSON.stringify({ decision: 'approve', additionalContext: context }));
|
|
25
|
+
}
|
|
26
|
+
// No output = implicit approve
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Inject additional context (for SessionStart, Stop, UserPromptSubmit, etc.).
|
|
32
|
+
* @param {string} text - Context text to inject
|
|
33
|
+
* @param {Object} [extra] - Additional fields to include in response
|
|
34
|
+
*/
|
|
35
|
+
export function injectContext(text, extra = {}) {
|
|
36
|
+
console.log(JSON.stringify({ additionalContext: text, ...extra }));
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Silent exit — no output, no blocking.
|
|
42
|
+
*/
|
|
43
|
+
export function pass() {
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// GENERATED by scripts/generate-refs.js — DO NOT EDIT manually
|
|
2
|
+
// Source of truth: src/core/paths/output-schema.js
|
|
3
|
+
// Regenerate with: node scripts/generate-refs.js
|
|
4
|
+
/**
|
|
5
|
+
* Shared phase utilities for Claude Code hooks.
|
|
6
|
+
*
|
|
7
|
+
* Maps phases to directories and output types.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Phase order */
|
|
11
|
+
export const PHASE_ORDER = ["proposal","setup","uiux","design","clarify","tasks","implement","sync"];
|
|
12
|
+
|
|
13
|
+
/** Map phase → allowed output subdirectory */
|
|
14
|
+
export const PHASE_DIRS = {
|
|
15
|
+
proposal: '0-proposal',
|
|
16
|
+
setup: '0-proposal',
|
|
17
|
+
uiux: '2-ui',
|
|
18
|
+
design: '1-design',
|
|
19
|
+
clarify: '1-design',
|
|
20
|
+
tasks: '3-tasks',
|
|
21
|
+
implement: '4-implement',
|
|
22
|
+
sync: '4-implement',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Map output type (camelCase) → phase that produces it */
|
|
26
|
+
export const OUTPUT_PHASE_MAP = {
|
|
27
|
+
proposal: 'proposal',
|
|
28
|
+
schemaAnalysis: 'design',
|
|
29
|
+
spec: 'design',
|
|
30
|
+
clarifications: 'clarify',
|
|
31
|
+
contracts: 'design',
|
|
32
|
+
tasks: 'tasks',
|
|
33
|
+
uiDesignSystem: 'uiux',
|
|
34
|
+
uiMockups: 'uiux',
|
|
35
|
+
uiComponents: 'uiux',
|
|
36
|
+
uiFlows: 'uiux',
|
|
37
|
+
decisions: 'design',
|
|
38
|
+
recap: 'implement',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Map filename → output type (camelCase) */
|
|
42
|
+
export const FILENAME_TO_OUTPUT = {
|
|
43
|
+
'proposal.md': 'proposal',
|
|
44
|
+
'schema-analysis.md': 'schemaAnalysis',
|
|
45
|
+
'spec.md': 'spec',
|
|
46
|
+
'clarifications.md': 'clarifications',
|
|
47
|
+
'contracts.cs': 'contracts',
|
|
48
|
+
'tasks.md': 'tasks',
|
|
49
|
+
'design-system.md': 'uiDesignSystem',
|
|
50
|
+
'mockups.md': 'uiMockups',
|
|
51
|
+
'components.md': 'uiComponents',
|
|
52
|
+
'flows.md': 'uiFlows',
|
|
53
|
+
'decisions.md': 'decisions',
|
|
54
|
+
'recap.md': 'recap',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Protected spec files and the approval gate that locks them */
|
|
58
|
+
export const PROTECTED_SPEC_FILES = {
|
|
59
|
+
'schema-analysis.md': 'design',
|
|
60
|
+
'spec.md': 'design',
|
|
61
|
+
'contracts.cs': 'design',
|
|
62
|
+
'tasks.md': 'tasks',
|
|
63
|
+
'design-system.md': 'uiux',
|
|
64
|
+
'mockups.md': 'uiux',
|
|
65
|
+
'components.md': 'uiux',
|
|
66
|
+
'flows.md': 'uiux',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract feature name from a .morph/features/{feature}/... path
|
|
71
|
+
* @param {string} filePath - File path to analyze
|
|
72
|
+
* @returns {string|null} Feature name or null
|
|
73
|
+
*/
|
|
74
|
+
export function extractFeatureName(filePath) {
|
|
75
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
76
|
+
const match = normalized.match(/\.morph\/features\/([^/]+)\//);
|
|
77
|
+
return match ? match[1] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extract phase subdirectory from a .morph/features/{feature}/{phaseDir}/... path
|
|
82
|
+
* @param {string} filePath - File path to analyze
|
|
83
|
+
* @returns {string|null} Phase directory (e.g., '0-proposal', '1-design') or null
|
|
84
|
+
*/
|
|
85
|
+
export function extractPhaseDir(filePath) {
|
|
86
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
87
|
+
const match = normalized.match(/\.morph\/features\/[^/]+\/(\d+-[^/]+)\//);
|
|
88
|
+
return match ? match[1] : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if a file path is inside .morph/features/
|
|
93
|
+
* @param {string} filePath
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function isFeaturePath(filePath) {
|
|
97
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
98
|
+
return normalized.includes('.morph/features/');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a file path is inside .morph/framework/ (readonly)
|
|
103
|
+
* @param {string} filePath
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
export function isFrameworkPath(filePath) {
|
|
107
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
108
|
+
return normalized.includes('.morph/framework/');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if a file path is state.json
|
|
113
|
+
* @param {string} filePath
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
export function isStatePath(filePath) {
|
|
117
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
118
|
+
return normalized.endsWith('.morph/state.json');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the basename of a file path
|
|
123
|
+
* @param {string} filePath
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
export function getBasename(filePath) {
|
|
127
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
128
|
+
return normalized.split('/').pop();
|
|
129
|
+
}
|