@polymorphism-tech/morph-spec 2.2.0 → 2.4.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 +314 -1673
- package/LICENSE +72 -72
- package/README.md +515 -516
- package/bin/detect-agents.js +225 -225
- package/bin/morph-spec.js +358 -173
- package/bin/render-template.js +302 -302
- package/bin/semantic-detect-agents.js +246 -246
- package/bin/task-manager.js +429 -0
- package/bin/validate-agents-skills.js +251 -251
- package/bin/validate-agents.js +69 -69
- package/bin/validate-phase.js +263 -263
- package/bin/validate.js +369 -0
- package/content/.azure/README.md +293 -293
- package/content/.azure/docs/azure-devops-setup.md +454 -454
- package/content/.azure/docs/branch-strategy.md +398 -398
- package/content/.azure/docs/local-development.md +515 -515
- package/content/.azure/pipelines/pipeline-variables.yml +34 -34
- package/content/.azure/pipelines/prod-pipeline.yml +319 -319
- package/content/.azure/pipelines/staging-pipeline.yml +234 -234
- package/content/.azure/pipelines/templates/build-dotnet.yml +75 -75
- package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -94
- package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -120
- package/content/.azure/pipelines/templates/infra-deploy.yml +90 -90
- package/content/.claude/commands/morph-apply.md +221 -158
- package/content/.claude/commands/morph-archive.md +79 -79
- package/content/.claude/commands/morph-infra.md +209 -209
- package/content/.claude/commands/morph-preflight.md +227 -0
- package/content/.claude/commands/morph-proposal.md +122 -101
- package/content/.claude/commands/morph-status.md +86 -86
- package/content/.claude/commands/morph-troubleshoot.md +122 -0
- package/content/.claude/settings.local.json +15 -15
- package/content/.claude/skills/checklists/code-review.md +226 -0
- package/content/.claude/skills/checklists/morph-checklist.md +117 -0
- package/content/.claude/skills/checklists/simulation-checklist.md +77 -0
- package/content/.claude/skills/infra/bicep-architect.md +126 -419
- package/content/.claude/skills/infra/container-specialist.md +131 -437
- package/content/.claude/skills/infra/devops-engineer.md +119 -405
- package/content/.claude/skills/integrations/asaas-financial.md +130 -333
- package/content/.claude/skills/integrations/azure-identity.md +142 -309
- package/content/.claude/skills/integrations/clerk-auth.md +108 -290
- package/content/.claude/skills/integrations/resend-email.md +119 -0
- package/content/.claude/skills/specialists/ai-system-architect.md +192 -604
- package/content/.claude/skills/specialists/azure-architect.md +142 -142
- package/content/.claude/skills/specialists/code-analyzer.md +235 -0
- package/content/.claude/skills/specialists/dotnet-senior.md +287 -0
- package/content/.claude/skills/specialists/ef-modeler.md +113 -200
- package/content/.claude/skills/specialists/hangfire-orchestrator.md +126 -245
- package/content/.claude/skills/specialists/ms-agent-expert.md +109 -263
- package/content/.claude/skills/specialists/po-pm-advisor.md +197 -197
- package/content/.claude/skills/specialists/standards-architect.md +156 -78
- package/content/.claude/skills/specialists/testing-specialist.md +126 -0
- package/content/.claude/skills/specialists/ui-ux-designer.md +191 -1060
- package/content/.claude/skills/stacks/dotnet-blazor.md +210 -588
- package/content/.claude/skills/stacks/dotnet-nextjs.md +154 -402
- package/content/.claude/skills/workflows/morph-replicate.md +213 -0
- package/content/.claude/{commands/morph-clarify.md → skills/workflows/phase-clarify.md} +5 -58
- package/content/.claude/{commands/morph-design.md → skills/workflows/phase-design.md} +16 -86
- package/content/.claude/{commands/morph-setup.md → skills/workflows/phase-setup.md} +9 -17
- package/content/.claude/skills/workflows/phase-tasks.md +164 -0
- package/content/.claude/{commands/morph-uiux.md → skills/workflows/phase-uiux.md} +15 -88
- package/content/.morph/.morphversion +5 -5
- package/content/.morph/archive/.gitkeep +25 -25
- package/content/.morph/config/agents.json +378 -242
- package/content/.morph/config/config.template.json +89 -108
- package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -392
- package/content/.morph/docs/workflows/design-impl.md +37 -0
- package/content/.morph/docs/workflows/fast-track.md +29 -0
- package/content/.morph/docs/workflows/full-morph.md +76 -0
- package/content/.morph/docs/workflows/standard.md +44 -0
- package/content/.morph/docs/workflows/ui-refresh.md +39 -0
- package/content/.morph/examples/api-nextjs/README.md +241 -241
- package/content/.morph/examples/api-nextjs/contracts.ts +307 -307
- package/content/.morph/examples/api-nextjs/spec.md +399 -399
- package/content/.morph/examples/api-nextjs/tasks.md +168 -168
- package/content/.morph/examples/micro-saas/README.md +125 -125
- package/content/.morph/examples/micro-saas/contracts.cs +358 -358
- package/content/.morph/examples/micro-saas/decisions.md +246 -246
- package/content/.morph/examples/micro-saas/spec.md +236 -236
- package/content/.morph/examples/micro-saas/tasks.md +150 -150
- package/content/.morph/examples/multi-agent/README.md +309 -309
- package/content/.morph/examples/multi-agent/contracts.cs +433 -433
- package/content/.morph/examples/multi-agent/spec.md +479 -479
- package/content/.morph/examples/multi-agent/tasks.md +185 -185
- package/content/.morph/examples/scheduled-reports/decisions.md +158 -0
- package/content/.morph/examples/scheduled-reports/proposal.md +95 -0
- package/content/.morph/examples/scheduled-reports/spec.md +267 -0
- package/content/.morph/examples/state-v3.json +188 -0
- package/content/.morph/features/.gitkeep +25 -25
- package/content/.morph/hooks/README.md +190 -239
- package/content/.morph/hooks/pre-commit-agents.sh +24 -24
- package/content/.morph/hooks/pre-commit-all.sh +48 -48
- package/content/.morph/hooks/pre-commit-specs.sh +49 -49
- package/content/.morph/hooks/pre-commit-tests.sh +60 -60
- package/content/.morph/project.md +160 -160
- package/content/.morph/schemas/agent.schema.json +296 -296
- package/content/.morph/schemas/tasks.schema.json +220 -0
- package/content/.morph/specs/.gitkeep +20 -20
- package/content/.morph/standards/agent-framework-blazor-ui.md +359 -0
- package/content/.morph/standards/agent-framework-production.md +410 -0
- package/content/.morph/standards/agent-framework-setup.md +413 -453
- package/content/.morph/standards/agent-framework-workflows.md +349 -0
- package/content/.morph/standards/architecture.md +325 -325
- package/content/.morph/standards/azure.md +605 -379
- package/content/.morph/standards/coding.md +377 -377
- package/content/.morph/standards/dotnet10-migration.md +520 -494
- package/content/.morph/standards/fluent-ui-setup.md +590 -590
- package/content/.morph/standards/migration-guide.md +514 -514
- package/content/.morph/standards/passkeys-auth.md +423 -423
- package/content/.morph/standards/vector-search-rag.md +536 -536
- package/content/.morph/state.json +17 -17
- package/content/.morph/templates/FluentDesignTheme.cs +149 -149
- package/content/.morph/templates/MudTheme.cs +281 -281
- package/content/.morph/templates/agent.cs +163 -172
- package/content/.morph/templates/clarify-questions.md +159 -0
- package/content/.morph/templates/component.razor +239 -239
- package/content/.morph/templates/contracts/Commands.cs +74 -0
- package/content/.morph/templates/contracts/Entities.cs +25 -0
- package/content/.morph/templates/contracts/Queries.cs +74 -0
- package/content/.morph/templates/contracts/README.md +74 -0
- package/content/.morph/templates/contracts.cs +217 -217
- package/content/.morph/templates/decisions.md +123 -106
- package/content/.morph/templates/design-system.css +226 -226
- package/content/.morph/templates/infra/.dockerignore.example +89 -89
- package/content/.morph/templates/infra/Dockerfile.example +82 -82
- package/content/.morph/templates/infra/README.md +286 -286
- package/content/.morph/templates/infra/app-insights.bicep +63 -63
- package/content/.morph/templates/infra/app-service.bicep +164 -164
- package/content/.morph/templates/infra/container-app-env.bicep +49 -49
- package/content/.morph/templates/infra/container-app.bicep +156 -156
- package/content/.morph/templates/infra/deploy-checklist.md +426 -0
- package/content/.morph/templates/infra/deploy.ps1 +229 -229
- package/content/.morph/templates/infra/deploy.sh +208 -208
- package/content/.morph/templates/infra/key-vault.bicep +91 -91
- package/content/.morph/templates/infra/main.bicep +189 -189
- package/content/.morph/templates/infra/parameters.dev.json +29 -29
- package/content/.morph/templates/infra/parameters.prod.json +29 -29
- package/content/.morph/templates/infra/parameters.staging.json +29 -29
- package/content/.morph/templates/infra/sql-database.bicep +103 -103
- package/content/.morph/templates/infra/storage.bicep +106 -106
- package/content/.morph/templates/integrations/asaas-client.cs +387 -387
- package/content/.morph/templates/integrations/asaas-webhook.cs +351 -351
- package/content/.morph/templates/integrations/azure-identity-config.cs +288 -288
- package/content/.morph/templates/integrations/clerk-config.cs +258 -258
- package/content/.morph/templates/job.cs +171 -171
- package/content/.morph/templates/migration.cs +83 -83
- package/content/.morph/templates/proposal.md +141 -155
- package/content/.morph/templates/recap.md +94 -105
- package/content/.morph/templates/repository.cs +141 -141
- package/content/.morph/templates/saas/subscription.cs +347 -347
- package/content/.morph/templates/saas/tenant.cs +338 -338
- package/content/.morph/templates/service.cs +139 -139
- package/content/.morph/templates/simulation.md +353 -0
- package/content/.morph/templates/spec.md +149 -148
- package/content/.morph/templates/sprint-status.yaml +68 -68
- package/content/.morph/templates/state.template.json +222 -222
- package/content/.morph/templates/story.md +143 -143
- package/content/.morph/templates/tasks.md +257 -235
- package/content/.morph/templates/test.cs +239 -239
- package/content/.morph/templates/ui-components.md +362 -276
- package/content/.morph/templates/ui-design-system.md +286 -286
- package/content/.morph/templates/ui-flows.md +336 -336
- package/content/.morph/templates/ui-mockups.md +133 -133
- package/content/.morph/test-infra/example.bicep +59 -59
- package/content/CLAUDE.md +150 -442
- package/content/README.md +79 -79
- package/detectors/config-detector.js +223 -223
- package/detectors/conversation-analyzer.js +163 -163
- package/detectors/index.js +84 -84
- package/detectors/standards-generator.js +275 -275
- package/detectors/structure-detector.js +245 -250
- package/docs/README.md +144 -149
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +977 -977
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1048 -1048
- package/docs/api/scripts/collapse.js +38 -38
- package/docs/api/scripts/commonNav.js +28 -28
- package/docs/api/scripts/linenumber.js +25 -25
- package/docs/api/scripts/nav.js +12 -12
- package/docs/api/scripts/polyfill.js +3 -3
- package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -202
- package/docs/api/scripts/prettify/lang-css.js +2 -2
- package/docs/api/scripts/prettify/prettify.js +28 -28
- package/docs/api/scripts/search.js +98 -98
- package/docs/api/styles/jsdoc.css +776 -776
- package/docs/api/styles/prettify.css +80 -80
- package/docs/examples.md +328 -328
- package/docs/getting-started.md +301 -302
- package/docs/installation.md +361 -361
- package/docs/templates.md +418 -418
- package/docs/validation-checklist.md +265 -266
- package/package.json +80 -80
- package/scripts/postinstall.js +132 -132
- package/src/commands/advance-phase.js +183 -0
- package/src/commands/analyze-blazor-concurrency.js +193 -0
- package/src/commands/create-story.js +351 -351
- package/src/commands/detect-agents.js +139 -0
- package/src/commands/detect.js +104 -104
- package/src/commands/doctor.js +356 -280
- package/src/commands/generate.js +149 -149
- package/src/commands/init.js +258 -245
- package/src/commands/lint-fluent.js +352 -0
- package/src/commands/rollback-phase.js +185 -0
- package/src/commands/session-summary.js +291 -0
- package/src/commands/shard-spec.js +224 -224
- package/src/commands/sprint-status.js +250 -250
- package/src/commands/state.js +333 -333
- package/src/commands/sync.js +167 -167
- package/src/commands/task.js +78 -0
- package/src/commands/troubleshoot.js +222 -0
- package/src/commands/update.js +192 -159
- package/src/commands/validate-blazor-state.js +210 -0
- package/src/commands/validate-blazor.js +156 -0
- package/src/commands/validate-css.js +84 -0
- package/src/commands/validate-phase.js +221 -0
- package/src/lib/blazor-concurrency-analyzer.js +288 -0
- package/src/lib/blazor-state-validator.js +291 -0
- package/src/lib/blazor-validator.js +374 -0
- package/src/lib/complexity-analyzer.js +441 -292
- package/src/lib/continuous-validator.js +421 -0
- package/src/lib/css-validator.js +352 -0
- package/src/lib/decision-constraint-loader.js +109 -0
- package/src/lib/design-system-generator.js +298 -298
- package/src/lib/learning-system.js +520 -0
- package/src/lib/mockup-generator.js +366 -0
- package/src/lib/recap-generator.js +205 -0
- package/src/lib/state-manager.js +397 -340
- package/src/lib/troubleshoot-grep.js +194 -0
- package/src/lib/troubleshoot-index.js +144 -0
- package/src/lib/ui-detector.js +350 -0
- package/src/lib/validation-runner.js +231 -0
- package/src/lib/validators/architecture-validator.js +387 -0
- package/src/lib/validators/contract-compliance-validator.js +273 -0
- package/src/lib/validators/package-validator.js +360 -0
- package/src/lib/validators/ui-contrast-validator.js +422 -0
- package/src/utils/file-copier.js +179 -139
- package/src/utils/logger.js +32 -32
- package/src/utils/version-checker.js +175 -175
- package/content/.claude/commands/morph-costs.md +0 -206
- package/content/.claude/commands/morph-tasks.md +0 -319
- package/content/.claude/skills/specialists/cost-guardian.md +0 -110
- package/content/.claude/skills/stacks/shopify.md +0 -445
- package/content/.morph/config/azure-pricing.json +0 -70
- package/content/.morph/config/azure-pricing.schema.json +0 -50
- package/content/.morph/hooks/pre-commit-costs.sh +0 -91
- package/docs/api/cost-calculator.js.html +0 -513
- package/docs/api/design-system-generator.js.html +0 -382
- package/docs/api/global.html +0 -5263
- package/docs/api/index.html +0 -96
- package/docs/api/state-manager.js.html +0 -423
- package/src/commands/cost.js +0 -181
- package/src/commands/update-pricing.js +0 -206
- package/src/lib/cost-calculator.js +0 -429
package/bin/render-template.js
CHANGED
|
@@ -1,302 +1,302 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MORPH-SPEC Template Renderer
|
|
5
|
-
*
|
|
6
|
-
* Renders templates by replacing {{PLACEHOLDER}} with actual values.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* node bin/render-template.js <template-path> <output-path> <variables-json>
|
|
10
|
-
*
|
|
11
|
-
* Example:
|
|
12
|
-
* node bin/render-template.js \
|
|
13
|
-
* content/.morph/templates/spec.md \
|
|
14
|
-
* .morph/project/outputs/my-feature/spec.md \
|
|
15
|
-
* '{"FEATURE_NAME":"my-feature","STACK":"Blazor","DATE":"2024-01-15"}'
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import fs from 'fs';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
import { fileURLToPath } from 'url';
|
|
21
|
-
|
|
22
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
-
const __dirname = path.dirname(__filename);
|
|
24
|
-
|
|
25
|
-
// ANSI color codes
|
|
26
|
-
const colors = {
|
|
27
|
-
green: '\x1b[32m',
|
|
28
|
-
red: '\x1b[31m',
|
|
29
|
-
yellow: '\x1b[33m',
|
|
30
|
-
cyan: '\x1b[36m',
|
|
31
|
-
reset: '\x1b[0m'
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Load config to get default values
|
|
36
|
-
*/
|
|
37
|
-
function loadConfig() {
|
|
38
|
-
const configPaths = [
|
|
39
|
-
path.join(process.cwd(), '.morph/config/config.json'),
|
|
40
|
-
path.join(process.cwd(), 'content/.morph/config/config.json'),
|
|
41
|
-
path.join(__dirname, '../content/.morph/config/config.template.json')
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
for (const configPath of configPaths) {
|
|
45
|
-
if (fs.existsSync(configPath)) {
|
|
46
|
-
try {
|
|
47
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
48
|
-
return JSON.parse(content);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
// Continue to next path
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Get default variables from config and environment
|
|
60
|
-
*/
|
|
61
|
-
function getDefaultVariables() {
|
|
62
|
-
const config = loadConfig();
|
|
63
|
-
const now = new Date();
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
DATE: now.toISOString().split('T')[0],
|
|
67
|
-
YEAR: now.getFullYear().toString(),
|
|
68
|
-
AUTHOR: config?.project?.author || 'MORPH-SPEC',
|
|
69
|
-
PROJECT_NAME: config?.project?.name || 'Project',
|
|
70
|
-
STACK: config?.project?.stack || 'Blazor',
|
|
71
|
-
NAMESPACE: config?.project?.namespace || 'Project'
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Convert string to various case formats
|
|
77
|
-
*/
|
|
78
|
-
function transformCase(str) {
|
|
79
|
-
return {
|
|
80
|
-
// kebab-case → PascalCase
|
|
81
|
-
pascal: str
|
|
82
|
-
.split('-')
|
|
83
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
84
|
-
.join(''),
|
|
85
|
-
|
|
86
|
-
// kebab-case → camelCase
|
|
87
|
-
camel: str
|
|
88
|
-
.split('-')
|
|
89
|
-
.map((word, index) =>
|
|
90
|
-
index === 0
|
|
91
|
-
? word.toLowerCase()
|
|
92
|
-
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
93
|
-
)
|
|
94
|
-
.join(''),
|
|
95
|
-
|
|
96
|
-
// kebab-case → Title Case
|
|
97
|
-
title: str
|
|
98
|
-
.split('-')
|
|
99
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
100
|
-
.join(' '),
|
|
101
|
-
|
|
102
|
-
// kebab-case → UPPER_SNAKE_CASE
|
|
103
|
-
upperSnake: str.toUpperCase().replace(/-/g, '_'),
|
|
104
|
-
|
|
105
|
-
// kebab-case → lower_snake_case
|
|
106
|
-
lowerSnake: str.toLowerCase().replace(/-/g, '_')
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Render template by replacing placeholders
|
|
112
|
-
*/
|
|
113
|
-
function renderTemplate(templatePath, variables) {
|
|
114
|
-
if (!fs.existsSync(templatePath)) {
|
|
115
|
-
throw new Error(`Template file not found: ${templatePath}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
119
|
-
|
|
120
|
-
// Get default variables
|
|
121
|
-
const defaults = getDefaultVariables();
|
|
122
|
-
|
|
123
|
-
// Merge with provided variables (provided takes precedence)
|
|
124
|
-
const allVariables = { ...defaults, ...variables };
|
|
125
|
-
|
|
126
|
-
// Auto-generate case transformations for FEATURE_NAME if provided
|
|
127
|
-
if (allVariables.FEATURE_NAME) {
|
|
128
|
-
const featureName = allVariables.FEATURE_NAME;
|
|
129
|
-
const transformations = transformCase(featureName);
|
|
130
|
-
|
|
131
|
-
allVariables.FEATURE_NAME_PASCAL = transformations.pascal;
|
|
132
|
-
allVariables.FEATURE_NAME_CAMEL = transformations.camel;
|
|
133
|
-
allVariables.FEATURE_NAME_TITLE = transformations.title;
|
|
134
|
-
allVariables.FEATURE_NAME_UPPER_SNAKE = transformations.upperSnake;
|
|
135
|
-
allVariables.FEATURE_NAME_LOWER_SNAKE = transformations.lowerSnake;
|
|
136
|
-
|
|
137
|
-
// Backward compatibility: FEATURE_TITLE maps to FEATURE_NAME_TITLE
|
|
138
|
-
if (!allVariables.FEATURE_TITLE) {
|
|
139
|
-
allVariables.FEATURE_TITLE = transformations.title;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Track which placeholders were replaced
|
|
144
|
-
const replacedPlaceholders = new Set();
|
|
145
|
-
const unreplacedPlaceholders = new Set();
|
|
146
|
-
|
|
147
|
-
// Replace all placeholders
|
|
148
|
-
for (const [key, value] of Object.entries(allVariables)) {
|
|
149
|
-
const placeholder = `{{${key}}}`;
|
|
150
|
-
if (content.includes(placeholder)) {
|
|
151
|
-
content = content.replaceAll(placeholder, value);
|
|
152
|
-
replacedPlaceholders.add(key);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Find unreplaced placeholders
|
|
157
|
-
const placeholderRegex = /\{\{([A-Z_]+)\}\}/g;
|
|
158
|
-
let match;
|
|
159
|
-
while ((match = placeholderRegex.exec(content)) !== null) {
|
|
160
|
-
unreplacedPlaceholders.add(match[1]);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
content,
|
|
165
|
-
replacedPlaceholders: Array.from(replacedPlaceholders),
|
|
166
|
-
unreplacedPlaceholders: Array.from(unreplacedPlaceholders)
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Print help message
|
|
172
|
-
*/
|
|
173
|
-
function printHelp() {
|
|
174
|
-
console.log(`
|
|
175
|
-
${colors.cyan}MORPH-SPEC Template Renderer${colors.reset}
|
|
176
|
-
|
|
177
|
-
${colors.green}Usage:${colors.reset}
|
|
178
|
-
node bin/render-template.js <template-path> <output-path> <variables-json>
|
|
179
|
-
|
|
180
|
-
${colors.green}Arguments:${colors.reset}
|
|
181
|
-
template-path Path to the template file (required)
|
|
182
|
-
output-path Path where rendered output will be written (required)
|
|
183
|
-
variables-json JSON object with variables to replace (required)
|
|
184
|
-
|
|
185
|
-
${colors.green}Standard Placeholders:${colors.reset}
|
|
186
|
-
{{FEATURE_NAME}} - kebab-case feature name (e.g., "scheduled-reports")
|
|
187
|
-
{{FEATURE_NAME_PASCAL}} - PascalCase (e.g., "ScheduledReports")
|
|
188
|
-
{{FEATURE_NAME_CAMEL}} - camelCase (e.g., "scheduledReports")
|
|
189
|
-
{{FEATURE_NAME_TITLE}} - Title Case (e.g., "Scheduled Reports")
|
|
190
|
-
{{FEATURE_NAME_UPPER_SNAKE}} - UPPER_SNAKE_CASE (e.g., "SCHEDULED_REPORTS")
|
|
191
|
-
{{FEATURE_NAME_LOWER_SNAKE}} - lower_snake_case (e.g., "scheduled_reports")
|
|
192
|
-
{{FEATURE_TITLE}} - Alias for FEATURE_NAME_TITLE
|
|
193
|
-
{{STACK}} - Project stack (e.g., "Blazor", "Next.js")
|
|
194
|
-
{{DATE}} - Current date (YYYY-MM-DD)
|
|
195
|
-
{{YEAR}} - Current year
|
|
196
|
-
{{AUTHOR}} - Author name (from config.json)
|
|
197
|
-
{{PROJECT_NAME}} - Project name (from config.json)
|
|
198
|
-
{{NAMESPACE}} - C# namespace (from config.json)
|
|
199
|
-
|
|
200
|
-
${colors.green}Example:${colors.reset}
|
|
201
|
-
node bin/render-template.js \\
|
|
202
|
-
content/.morph/templates/spec.md \\
|
|
203
|
-
.morph/project/outputs/scheduled-reports/spec.md \\
|
|
204
|
-
'{"FEATURE_NAME":"scheduled-reports","STACK":"Blazor"}'
|
|
205
|
-
|
|
206
|
-
${colors.green}Flags:${colors.reset}
|
|
207
|
-
--help, -h Show this help message
|
|
208
|
-
--verbose, -v Show detailed replacement information
|
|
209
|
-
--dry-run, -d Preview output without writing file
|
|
210
|
-
|
|
211
|
-
${colors.yellow}Note:${colors.reset} DATE, YEAR, AUTHOR, PROJECT_NAME, STACK, NAMESPACE are auto-populated from config.json
|
|
212
|
-
`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Main CLI logic
|
|
217
|
-
*/
|
|
218
|
-
function main() {
|
|
219
|
-
const args = process.argv.slice(2);
|
|
220
|
-
|
|
221
|
-
// Check for help flag
|
|
222
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
223
|
-
printHelp();
|
|
224
|
-
process.exit(0);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Parse flags
|
|
228
|
-
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
229
|
-
const dryRun = args.includes('--dry-run') || args.includes('-d');
|
|
230
|
-
|
|
231
|
-
// Filter out flags
|
|
232
|
-
const flags = ['--verbose', '-v', '--dry-run', '-d', '--help', '-h'];
|
|
233
|
-
const cleanArgs = args.filter(arg => !flags.includes(arg));
|
|
234
|
-
|
|
235
|
-
// Validate arguments
|
|
236
|
-
if (cleanArgs.length < 3) {
|
|
237
|
-
console.error(`${colors.red}❌ Error: Missing required arguments${colors.reset}\n`);
|
|
238
|
-
printHelp();
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const [templatePath, outputPath, variablesJson] = cleanArgs;
|
|
243
|
-
|
|
244
|
-
// Parse variables JSON
|
|
245
|
-
let variables;
|
|
246
|
-
try {
|
|
247
|
-
variables = JSON.parse(variablesJson);
|
|
248
|
-
} catch (error) {
|
|
249
|
-
console.error(`${colors.red}❌ Error: Invalid JSON in variables argument${colors.reset}`);
|
|
250
|
-
console.error(` ${error.message}\n`);
|
|
251
|
-
process.exit(1);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
try {
|
|
255
|
-
// Render template
|
|
256
|
-
const { content, replacedPlaceholders, unreplacedPlaceholders } = renderTemplate(
|
|
257
|
-
templatePath,
|
|
258
|
-
variables
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
// Dry run: just preview
|
|
262
|
-
if (dryRun) {
|
|
263
|
-
console.log(`${colors.cyan}📋 DRY RUN - Preview (file will NOT be written)${colors.reset}\n`);
|
|
264
|
-
console.log(content);
|
|
265
|
-
console.log(`\n${colors.cyan}═══════════════════════════════════════════${colors.reset}`);
|
|
266
|
-
} else {
|
|
267
|
-
// Create output directory if it doesn't exist
|
|
268
|
-
const outputDir = path.dirname(outputPath);
|
|
269
|
-
if (!fs.existsSync(outputDir)) {
|
|
270
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Write rendered content
|
|
274
|
-
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
275
|
-
console.log(`${colors.green}✅ Template rendered successfully${colors.reset}`);
|
|
276
|
-
console.log(` Output: ${outputPath}`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Verbose output
|
|
280
|
-
if (verbose || unreplacedPlaceholders.length > 0) {
|
|
281
|
-
console.log(`\n${colors.cyan}📊 Replacement Summary:${colors.reset}`);
|
|
282
|
-
console.log(` ${colors.green}Replaced:${colors.reset} ${replacedPlaceholders.length} placeholders`);
|
|
283
|
-
|
|
284
|
-
if (replacedPlaceholders.length > 0) {
|
|
285
|
-
console.log(` ${replacedPlaceholders.map(p => `{{${p}}}`).join(', ')}`);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (unreplacedPlaceholders.length > 0) {
|
|
289
|
-
console.log(`\n ${colors.yellow}⚠️ Unreplaced:${colors.reset} ${unreplacedPlaceholders.length} placeholders`);
|
|
290
|
-
console.log(` ${unreplacedPlaceholders.map(p => `{{${p}}}`).join(', ')}`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
process.exit(0);
|
|
295
|
-
} catch (error) {
|
|
296
|
-
console.error(`${colors.red}❌ Error: ${error.message}${colors.reset}`);
|
|
297
|
-
process.exit(1);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Run CLI
|
|
302
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MORPH-SPEC Template Renderer
|
|
5
|
+
*
|
|
6
|
+
* Renders templates by replacing {{PLACEHOLDER}} with actual values.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node bin/render-template.js <template-path> <output-path> <variables-json>
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* node bin/render-template.js \
|
|
13
|
+
* content/.morph/templates/spec.md \
|
|
14
|
+
* .morph/project/outputs/my-feature/spec.md \
|
|
15
|
+
* '{"FEATURE_NAME":"my-feature","STACK":"Blazor","DATE":"2024-01-15"}'
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = path.dirname(__filename);
|
|
24
|
+
|
|
25
|
+
// ANSI color codes
|
|
26
|
+
const colors = {
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
red: '\x1b[31m',
|
|
29
|
+
yellow: '\x1b[33m',
|
|
30
|
+
cyan: '\x1b[36m',
|
|
31
|
+
reset: '\x1b[0m'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load config to get default values
|
|
36
|
+
*/
|
|
37
|
+
function loadConfig() {
|
|
38
|
+
const configPaths = [
|
|
39
|
+
path.join(process.cwd(), '.morph/config/config.json'),
|
|
40
|
+
path.join(process.cwd(), 'content/.morph/config/config.json'),
|
|
41
|
+
path.join(__dirname, '../content/.morph/config/config.template.json')
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
for (const configPath of configPaths) {
|
|
45
|
+
if (fs.existsSync(configPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
48
|
+
return JSON.parse(content);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// Continue to next path
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get default variables from config and environment
|
|
60
|
+
*/
|
|
61
|
+
function getDefaultVariables() {
|
|
62
|
+
const config = loadConfig();
|
|
63
|
+
const now = new Date();
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
DATE: now.toISOString().split('T')[0],
|
|
67
|
+
YEAR: now.getFullYear().toString(),
|
|
68
|
+
AUTHOR: config?.project?.author || 'MORPH-SPEC',
|
|
69
|
+
PROJECT_NAME: config?.project?.name || 'Project',
|
|
70
|
+
STACK: config?.project?.stack || 'Blazor',
|
|
71
|
+
NAMESPACE: config?.project?.namespace || 'Project'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert string to various case formats
|
|
77
|
+
*/
|
|
78
|
+
function transformCase(str) {
|
|
79
|
+
return {
|
|
80
|
+
// kebab-case → PascalCase
|
|
81
|
+
pascal: str
|
|
82
|
+
.split('-')
|
|
83
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
84
|
+
.join(''),
|
|
85
|
+
|
|
86
|
+
// kebab-case → camelCase
|
|
87
|
+
camel: str
|
|
88
|
+
.split('-')
|
|
89
|
+
.map((word, index) =>
|
|
90
|
+
index === 0
|
|
91
|
+
? word.toLowerCase()
|
|
92
|
+
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
93
|
+
)
|
|
94
|
+
.join(''),
|
|
95
|
+
|
|
96
|
+
// kebab-case → Title Case
|
|
97
|
+
title: str
|
|
98
|
+
.split('-')
|
|
99
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
100
|
+
.join(' '),
|
|
101
|
+
|
|
102
|
+
// kebab-case → UPPER_SNAKE_CASE
|
|
103
|
+
upperSnake: str.toUpperCase().replace(/-/g, '_'),
|
|
104
|
+
|
|
105
|
+
// kebab-case → lower_snake_case
|
|
106
|
+
lowerSnake: str.toLowerCase().replace(/-/g, '_')
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Render template by replacing placeholders
|
|
112
|
+
*/
|
|
113
|
+
function renderTemplate(templatePath, variables) {
|
|
114
|
+
if (!fs.existsSync(templatePath)) {
|
|
115
|
+
throw new Error(`Template file not found: ${templatePath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
119
|
+
|
|
120
|
+
// Get default variables
|
|
121
|
+
const defaults = getDefaultVariables();
|
|
122
|
+
|
|
123
|
+
// Merge with provided variables (provided takes precedence)
|
|
124
|
+
const allVariables = { ...defaults, ...variables };
|
|
125
|
+
|
|
126
|
+
// Auto-generate case transformations for FEATURE_NAME if provided
|
|
127
|
+
if (allVariables.FEATURE_NAME) {
|
|
128
|
+
const featureName = allVariables.FEATURE_NAME;
|
|
129
|
+
const transformations = transformCase(featureName);
|
|
130
|
+
|
|
131
|
+
allVariables.FEATURE_NAME_PASCAL = transformations.pascal;
|
|
132
|
+
allVariables.FEATURE_NAME_CAMEL = transformations.camel;
|
|
133
|
+
allVariables.FEATURE_NAME_TITLE = transformations.title;
|
|
134
|
+
allVariables.FEATURE_NAME_UPPER_SNAKE = transformations.upperSnake;
|
|
135
|
+
allVariables.FEATURE_NAME_LOWER_SNAKE = transformations.lowerSnake;
|
|
136
|
+
|
|
137
|
+
// Backward compatibility: FEATURE_TITLE maps to FEATURE_NAME_TITLE
|
|
138
|
+
if (!allVariables.FEATURE_TITLE) {
|
|
139
|
+
allVariables.FEATURE_TITLE = transformations.title;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Track which placeholders were replaced
|
|
144
|
+
const replacedPlaceholders = new Set();
|
|
145
|
+
const unreplacedPlaceholders = new Set();
|
|
146
|
+
|
|
147
|
+
// Replace all placeholders
|
|
148
|
+
for (const [key, value] of Object.entries(allVariables)) {
|
|
149
|
+
const placeholder = `{{${key}}}`;
|
|
150
|
+
if (content.includes(placeholder)) {
|
|
151
|
+
content = content.replaceAll(placeholder, value);
|
|
152
|
+
replacedPlaceholders.add(key);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Find unreplaced placeholders
|
|
157
|
+
const placeholderRegex = /\{\{([A-Z_]+)\}\}/g;
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = placeholderRegex.exec(content)) !== null) {
|
|
160
|
+
unreplacedPlaceholders.add(match[1]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
content,
|
|
165
|
+
replacedPlaceholders: Array.from(replacedPlaceholders),
|
|
166
|
+
unreplacedPlaceholders: Array.from(unreplacedPlaceholders)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Print help message
|
|
172
|
+
*/
|
|
173
|
+
function printHelp() {
|
|
174
|
+
console.log(`
|
|
175
|
+
${colors.cyan}MORPH-SPEC Template Renderer${colors.reset}
|
|
176
|
+
|
|
177
|
+
${colors.green}Usage:${colors.reset}
|
|
178
|
+
node bin/render-template.js <template-path> <output-path> <variables-json>
|
|
179
|
+
|
|
180
|
+
${colors.green}Arguments:${colors.reset}
|
|
181
|
+
template-path Path to the template file (required)
|
|
182
|
+
output-path Path where rendered output will be written (required)
|
|
183
|
+
variables-json JSON object with variables to replace (required)
|
|
184
|
+
|
|
185
|
+
${colors.green}Standard Placeholders:${colors.reset}
|
|
186
|
+
{{FEATURE_NAME}} - kebab-case feature name (e.g., "scheduled-reports")
|
|
187
|
+
{{FEATURE_NAME_PASCAL}} - PascalCase (e.g., "ScheduledReports")
|
|
188
|
+
{{FEATURE_NAME_CAMEL}} - camelCase (e.g., "scheduledReports")
|
|
189
|
+
{{FEATURE_NAME_TITLE}} - Title Case (e.g., "Scheduled Reports")
|
|
190
|
+
{{FEATURE_NAME_UPPER_SNAKE}} - UPPER_SNAKE_CASE (e.g., "SCHEDULED_REPORTS")
|
|
191
|
+
{{FEATURE_NAME_LOWER_SNAKE}} - lower_snake_case (e.g., "scheduled_reports")
|
|
192
|
+
{{FEATURE_TITLE}} - Alias for FEATURE_NAME_TITLE
|
|
193
|
+
{{STACK}} - Project stack (e.g., "Blazor", "Next.js")
|
|
194
|
+
{{DATE}} - Current date (YYYY-MM-DD)
|
|
195
|
+
{{YEAR}} - Current year
|
|
196
|
+
{{AUTHOR}} - Author name (from config.json)
|
|
197
|
+
{{PROJECT_NAME}} - Project name (from config.json)
|
|
198
|
+
{{NAMESPACE}} - C# namespace (from config.json)
|
|
199
|
+
|
|
200
|
+
${colors.green}Example:${colors.reset}
|
|
201
|
+
node bin/render-template.js \\
|
|
202
|
+
content/.morph/templates/spec.md \\
|
|
203
|
+
.morph/project/outputs/scheduled-reports/spec.md \\
|
|
204
|
+
'{"FEATURE_NAME":"scheduled-reports","STACK":"Blazor"}'
|
|
205
|
+
|
|
206
|
+
${colors.green}Flags:${colors.reset}
|
|
207
|
+
--help, -h Show this help message
|
|
208
|
+
--verbose, -v Show detailed replacement information
|
|
209
|
+
--dry-run, -d Preview output without writing file
|
|
210
|
+
|
|
211
|
+
${colors.yellow}Note:${colors.reset} DATE, YEAR, AUTHOR, PROJECT_NAME, STACK, NAMESPACE are auto-populated from config.json
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Main CLI logic
|
|
217
|
+
*/
|
|
218
|
+
function main() {
|
|
219
|
+
const args = process.argv.slice(2);
|
|
220
|
+
|
|
221
|
+
// Check for help flag
|
|
222
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
223
|
+
printHelp();
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Parse flags
|
|
228
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
229
|
+
const dryRun = args.includes('--dry-run') || args.includes('-d');
|
|
230
|
+
|
|
231
|
+
// Filter out flags
|
|
232
|
+
const flags = ['--verbose', '-v', '--dry-run', '-d', '--help', '-h'];
|
|
233
|
+
const cleanArgs = args.filter(arg => !flags.includes(arg));
|
|
234
|
+
|
|
235
|
+
// Validate arguments
|
|
236
|
+
if (cleanArgs.length < 3) {
|
|
237
|
+
console.error(`${colors.red}❌ Error: Missing required arguments${colors.reset}\n`);
|
|
238
|
+
printHelp();
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const [templatePath, outputPath, variablesJson] = cleanArgs;
|
|
243
|
+
|
|
244
|
+
// Parse variables JSON
|
|
245
|
+
let variables;
|
|
246
|
+
try {
|
|
247
|
+
variables = JSON.parse(variablesJson);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error(`${colors.red}❌ Error: Invalid JSON in variables argument${colors.reset}`);
|
|
250
|
+
console.error(` ${error.message}\n`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// Render template
|
|
256
|
+
const { content, replacedPlaceholders, unreplacedPlaceholders } = renderTemplate(
|
|
257
|
+
templatePath,
|
|
258
|
+
variables
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Dry run: just preview
|
|
262
|
+
if (dryRun) {
|
|
263
|
+
console.log(`${colors.cyan}📋 DRY RUN - Preview (file will NOT be written)${colors.reset}\n`);
|
|
264
|
+
console.log(content);
|
|
265
|
+
console.log(`\n${colors.cyan}═══════════════════════════════════════════${colors.reset}`);
|
|
266
|
+
} else {
|
|
267
|
+
// Create output directory if it doesn't exist
|
|
268
|
+
const outputDir = path.dirname(outputPath);
|
|
269
|
+
if (!fs.existsSync(outputDir)) {
|
|
270
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Write rendered content
|
|
274
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
275
|
+
console.log(`${colors.green}✅ Template rendered successfully${colors.reset}`);
|
|
276
|
+
console.log(` Output: ${outputPath}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Verbose output
|
|
280
|
+
if (verbose || unreplacedPlaceholders.length > 0) {
|
|
281
|
+
console.log(`\n${colors.cyan}📊 Replacement Summary:${colors.reset}`);
|
|
282
|
+
console.log(` ${colors.green}Replaced:${colors.reset} ${replacedPlaceholders.length} placeholders`);
|
|
283
|
+
|
|
284
|
+
if (replacedPlaceholders.length > 0) {
|
|
285
|
+
console.log(` ${replacedPlaceholders.map(p => `{{${p}}}`).join(', ')}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (unreplacedPlaceholders.length > 0) {
|
|
289
|
+
console.log(`\n ${colors.yellow}⚠️ Unreplaced:${colors.reset} ${unreplacedPlaceholders.length} placeholders`);
|
|
290
|
+
console.log(` ${unreplacedPlaceholders.map(p => `{{${p}}}`).join(', ')}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
process.exit(0);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error(`${colors.red}❌ Error: ${error.message}${colors.reset}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Run CLI
|
|
302
|
+
main();
|