@polymorphism-tech/morph-spec 2.4.0 → 3.0.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 +158 -26
- package/LICENSE +72 -72
- package/bin/detect-agents.js +225 -225
- package/bin/morph-spec.js +8 -0
- package/bin/render-template.js +302 -302
- package/bin/semantic-detect-agents.js +246 -246
- package/bin/validate-agents-skills.js +251 -251
- package/bin/validate-agents.js +69 -69
- package/bin/validate-phase.js +263 -263
- 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-archive.md +79 -79
- package/content/.claude/commands/morph-deploy.md +529 -0
- package/content/.claude/commands/morph-infra.md +209 -209
- package/content/.claude/commands/morph-preflight.md +227 -227
- package/content/.claude/commands/morph-troubleshoot.md +122 -122
- package/content/.claude/settings.local.json +15 -15
- package/content/.claude/skills/infra/azure-deploy-specialist.md +699 -0
- package/content/.claude/skills/level-0-meta/README.md +7 -0
- package/content/.claude/skills/{checklists → level-0-meta}/morph-checklist.md +117 -117
- package/content/.claude/skills/level-1-workflows/README.md +7 -0
- package/content/.claude/skills/{workflows → level-1-workflows}/morph-replicate.md +213 -213
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-clarify.md +131 -131
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-design.md +213 -205
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-setup.md +106 -92
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-tasks.md +164 -164
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-uiux.md +169 -138
- package/content/.claude/skills/level-2-domains/README.md +14 -0
- package/content/.claude/skills/{specialists → level-2-domains/quality}/testing-specialist.md +126 -126
- package/content/.claude/skills/level-3-technologies/README.md +7 -0
- package/content/.claude/skills/level-4-patterns/README.md +7 -0
- package/content/.claude/skills/specialists/prompt-engineer.md +189 -0
- package/content/.claude/skills/specialists/seo-growth-hacker.md +320 -0
- package/content/.morph/.morphversion +5 -5
- package/content/.morph/archive/.gitkeep +25 -25
- package/content/.morph/config/agents.json +742 -358
- package/content/.morph/config/config.template.json +33 -0
- package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -392
- package/content/.morph/docs/workflows/enforcement-pipeline.md +668 -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 -158
- package/content/.morph/examples/scheduled-reports/proposal.md +95 -95
- package/content/.morph/examples/scheduled-reports/spec.md +267 -267
- package/content/.morph/examples/state-v3.json +188 -188
- package/content/.morph/features/.gitkeep +25 -25
- package/content/.morph/hooks/README.md +158 -0
- 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/hooks/task-completed.js +73 -0
- package/content/.morph/hooks/teammate-idle.js +68 -0
- package/content/.morph/project.md +160 -160
- package/content/.morph/schemas/agent.schema.json +296 -296
- package/content/.morph/schemas/tasks.schema.json +220 -220
- package/content/.morph/specs/.gitkeep +20 -20
- package/content/.morph/standards/agent-teams-workflow.md +474 -0
- package/content/.morph/standards/coding.md +377 -377
- 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/CONTEXT-FEATURE.md +276 -0
- package/content/.morph/templates/CONTEXT.md +170 -0
- package/content/.morph/templates/FluentDesignTheme.cs +149 -149
- package/content/.morph/templates/MudTheme.cs +281 -281
- package/content/.morph/templates/clarify-questions.md +159 -159
- package/content/.morph/templates/component.razor +239 -239
- package/content/.morph/templates/contracts/Commands.cs +74 -74
- package/content/.morph/templates/contracts/Entities.cs +25 -25
- package/content/.morph/templates/contracts/Queries.cs +74 -74
- package/content/.morph/templates/contracts/README.md +74 -74
- package/content/.morph/templates/contracts.cs +217 -217
- 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/azure-pipelines-deploy.yml +480 -0
- 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 -426
- 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/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/sprint-status.yaml +68 -68
- package/content/.morph/templates/story.md +143 -143
- package/content/.morph/templates/test.cs +239 -239
- 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/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/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/templates.md +418 -418
- package/package.json +1 -1
- package/scripts/postinstall.js +132 -132
- package/src/commands/advance-phase.js +83 -0
- package/src/commands/analyze-blazor-concurrency.js +193 -193
- package/src/commands/create-story.js +351 -351
- package/src/commands/deploy.js +780 -0
- package/src/commands/detect-agents.js +34 -6
- package/src/commands/detect.js +104 -104
- package/src/commands/generate-context.js +40 -0
- package/src/commands/generate.js +149 -149
- package/src/commands/lint-fluent.js +352 -352
- package/src/commands/rollback-phase.js +185 -185
- package/src/commands/session-summary.js +291 -291
- 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/troubleshoot.js +222 -222
- package/src/commands/validate-blazor-state.js +210 -210
- package/src/commands/validate-blazor.js +156 -156
- package/src/commands/validate-css.js +84 -84
- package/src/commands/validate-phase.js +221 -221
- package/src/lib/blazor-concurrency-analyzer.js +288 -288
- package/src/lib/blazor-state-validator.js +291 -291
- package/src/lib/blazor-validator.js +374 -374
- package/src/lib/context-generator.js +513 -0
- package/src/lib/css-validator.js +352 -352
- package/src/lib/design-system-detector.js +187 -0
- package/src/lib/design-system-generator.js +298 -298
- package/src/lib/design-system-scaffolder.js +299 -0
- package/src/lib/hook-executor.js +256 -0
- package/src/lib/learning-system.js +520 -520
- package/src/lib/mockup-generator.js +366 -366
- package/src/lib/spec-validator.js +258 -0
- package/src/lib/standards-context-injector.js +287 -0
- package/src/lib/team-orchestrator.js +322 -0
- package/src/lib/troubleshoot-grep.js +194 -194
- package/src/lib/troubleshoot-index.js +144 -144
- package/src/lib/ui-detector.js +350 -350
- package/src/lib/validation-runner.js +65 -13
- package/src/lib/validators/architecture-validator.js +387 -387
- package/src/lib/validators/design-system-validator.js +231 -0
- package/src/lib/validators/package-validator.js +360 -360
- package/src/lib/validators/ui-contrast-validator.js +422 -422
- package/src/utils/file-copier.js +9 -1
- package/src/utils/logger.js +32 -32
- package/src/utils/version-checker.js +175 -175
- /package/content/.claude/skills/{checklists → level-0-meta}/code-review.md +0 -0
- /package/content/.claude/skills/{checklists → level-0-meta}/simulation-checklist.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/ai-agents}/ai-system-architect.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/architecture}/po-pm-advisor.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/architecture}/standards-architect.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/backend}/dotnet-senior.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/backend}/ef-modeler.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/backend}/hangfire-orchestrator.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/backend}/ms-agent-expert.md +0 -0
- /package/content/.claude/skills/{stacks/dotnet-blazor.md → level-2-domains/frontend/blazor-builder.md} +0 -0
- /package/content/.claude/skills/{stacks/dotnet-nextjs.md → level-2-domains/frontend/nextjs-expert.md} +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/frontend}/ui-ux-designer.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/infrastructure}/azure-architect.md +0 -0
- /package/content/.claude/skills/{infra → level-2-domains/infrastructure}/bicep-architect.md +0 -0
- /package/content/.claude/skills/{infra → level-2-domains/infrastructure}/container-specialist.md +0 -0
- /package/content/.claude/skills/{infra → level-2-domains/infrastructure}/devops-engineer.md +0 -0
- /package/content/.claude/skills/{integrations → level-2-domains/integrations}/asaas-financial.md +0 -0
- /package/content/.claude/skills/{integrations → level-2-domains/integrations}/azure-identity.md +0 -0
- /package/content/.claude/skills/{integrations → level-2-domains/integrations}/clerk-auth.md +0 -0
- /package/content/.claude/skills/{integrations → level-2-domains/integrations}/resend-email.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/quality}/code-analyzer.md +0 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Deploy Command
|
|
3
|
+
* Azure Container Apps deployment orchestrator with playbook-driven approach
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { execSync, exec } from 'child_process';
|
|
11
|
+
import { logger } from '../utils/logger.js';
|
|
12
|
+
import * as CostCalculator from '../lib/cost-calculator.js';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Configuration Detection
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find appsettings.json files in project
|
|
20
|
+
* @param {string} projectPath - Root path to search
|
|
21
|
+
* @returns {string[]} - Array of appsettings file paths
|
|
22
|
+
*/
|
|
23
|
+
export function findAppSettingsFiles(projectPath = '.') {
|
|
24
|
+
const files = [];
|
|
25
|
+
const patterns = ['appsettings.json', 'appsettings.*.json'];
|
|
26
|
+
|
|
27
|
+
function searchDir(dir) {
|
|
28
|
+
try {
|
|
29
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const fullPath = path.join(dir, entry.name);
|
|
32
|
+
|
|
33
|
+
// Skip node_modules, bin, obj, .git
|
|
34
|
+
if (entry.isDirectory() && !['node_modules', 'bin', 'obj', '.git', '.morph'].includes(entry.name)) {
|
|
35
|
+
searchDir(fullPath);
|
|
36
|
+
} else if (entry.isFile() && patterns.some(p =>
|
|
37
|
+
entry.name === 'appsettings.json' || entry.name.match(/^appsettings\..+\.json$/)
|
|
38
|
+
)) {
|
|
39
|
+
files.push(fullPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// Ignore permission errors
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
searchDir(projectPath);
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find Program.cs files in project
|
|
53
|
+
* @param {string} projectPath - Root path to search
|
|
54
|
+
* @returns {string[]} - Array of Program.cs paths
|
|
55
|
+
*/
|
|
56
|
+
export function findProgramCsFiles(projectPath = '.') {
|
|
57
|
+
const files = [];
|
|
58
|
+
|
|
59
|
+
function searchDir(dir) {
|
|
60
|
+
try {
|
|
61
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const fullPath = path.join(dir, entry.name);
|
|
64
|
+
|
|
65
|
+
if (entry.isDirectory() && !['node_modules', 'bin', 'obj', '.git'].includes(entry.name)) {
|
|
66
|
+
searchDir(fullPath);
|
|
67
|
+
} else if (entry.isFile() && entry.name === 'Program.cs') {
|
|
68
|
+
files.push(fullPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Ignore permission errors
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
searchDir(projectPath);
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse appsettings.json and extract configuration paths
|
|
82
|
+
* @param {string} filePath - Path to appsettings.json
|
|
83
|
+
* @returns {Object} - Parsed configuration with paths
|
|
84
|
+
*/
|
|
85
|
+
export function parseAppSettings(filePath) {
|
|
86
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
87
|
+
const config = JSON.parse(content);
|
|
88
|
+
const paths = [];
|
|
89
|
+
|
|
90
|
+
function extractPaths(obj, prefix = '') {
|
|
91
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
92
|
+
const currentPath = prefix ? `${prefix}:${key}` : key;
|
|
93
|
+
|
|
94
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
95
|
+
extractPaths(value, currentPath);
|
|
96
|
+
} else {
|
|
97
|
+
paths.push({
|
|
98
|
+
configPath: currentPath,
|
|
99
|
+
envVar: currentPath.replace(/:/g, '__').toUpperCase(),
|
|
100
|
+
value: typeof value === 'string' && value.includes('your-') ? '(placeholder)' :
|
|
101
|
+
typeof value === 'string' && value.length > 50 ? '(long-value)' : value,
|
|
102
|
+
isSecret: key.toLowerCase().includes('secret') ||
|
|
103
|
+
key.toLowerCase().includes('password') ||
|
|
104
|
+
key.toLowerCase().includes('key') ||
|
|
105
|
+
key.toLowerCase().includes('connection') ||
|
|
106
|
+
currentPath.toLowerCase().includes('connectionstrings')
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
extractPaths(config);
|
|
113
|
+
return { file: filePath, paths };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Detect Blazor Server patterns in Program.cs
|
|
118
|
+
* @param {string} filePath - Path to Program.cs
|
|
119
|
+
* @returns {Object} - Detection results
|
|
120
|
+
*/
|
|
121
|
+
export function detectBlazorServer(filePath) {
|
|
122
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
123
|
+
|
|
124
|
+
const patterns = {
|
|
125
|
+
interactiveServer: /\.AddInteractiveServerComponents\s*\(/,
|
|
126
|
+
serverSideBlazor: /\.AddServerSideBlazor\s*\(/,
|
|
127
|
+
razorComponents: /\.AddRazorComponents\s*\(\)\.AddInteractiveServerComponents\s*\(/,
|
|
128
|
+
blazorHub: /\.MapBlazorHub\s*\(/,
|
|
129
|
+
signalR: /\.AddSignalR\s*\(/
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const detected = {};
|
|
133
|
+
for (const [name, pattern] of Object.entries(patterns)) {
|
|
134
|
+
detected[name] = pattern.test(content);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isBlazorServer = detected.interactiveServer ||
|
|
138
|
+
detected.serverSideBlazor ||
|
|
139
|
+
detected.razorComponents ||
|
|
140
|
+
detected.blazorHub;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
isBlazorServer,
|
|
144
|
+
requiresStickySessions: isBlazorServer,
|
|
145
|
+
patterns: detected
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Detect authentication patterns in Program.cs
|
|
151
|
+
* @param {string} filePath - Path to Program.cs
|
|
152
|
+
* @returns {Object} - Auth detection results
|
|
153
|
+
*/
|
|
154
|
+
export function detectAuthentication(filePath) {
|
|
155
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
156
|
+
|
|
157
|
+
const patterns = {
|
|
158
|
+
azureAd: /\.AddMicrosoftIdentityWebApp\s*\(|GetSection\s*\(\s*["']AzureAd["']\s*\)/,
|
|
159
|
+
clerk: /Clerk|ClerkOptions|AddClerk/i,
|
|
160
|
+
identity: /\.AddIdentity\s*\(|\.AddDefaultIdentity\s*\(/,
|
|
161
|
+
jwt: /\.AddJwtBearer\s*\(/,
|
|
162
|
+
cookie: /\.AddCookie\s*\(/
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const detected = {};
|
|
166
|
+
for (const [name, pattern] of Object.entries(patterns)) {
|
|
167
|
+
detected[name] = pattern.test(content);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
hasAuth: Object.values(detected).some(v => v),
|
|
172
|
+
providers: detected
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Detect Hangfire patterns
|
|
178
|
+
* @param {string} filePath - Path to Program.cs
|
|
179
|
+
* @returns {boolean} - Whether Hangfire is detected
|
|
180
|
+
*/
|
|
181
|
+
export function detectHangfire(filePath) {
|
|
182
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
183
|
+
return /AddHangfire\s*\(|UseHangfireDashboard\s*\(/i.test(content);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Generate complete configuration mapping
|
|
188
|
+
* @param {string} projectPath - Root path
|
|
189
|
+
* @returns {Object} - Complete configuration analysis
|
|
190
|
+
*/
|
|
191
|
+
export function detectProjectConfig(projectPath = '.') {
|
|
192
|
+
const appSettingsFiles = findAppSettingsFiles(projectPath);
|
|
193
|
+
const programCsFiles = findProgramCsFiles(projectPath);
|
|
194
|
+
|
|
195
|
+
const configs = appSettingsFiles.map(f => parseAppSettings(f));
|
|
196
|
+
|
|
197
|
+
let blazorServer = { isBlazorServer: false, requiresStickySessions: false };
|
|
198
|
+
let auth = { hasAuth: false, providers: {} };
|
|
199
|
+
let hangfire = false;
|
|
200
|
+
|
|
201
|
+
for (const file of programCsFiles) {
|
|
202
|
+
const blazorResult = detectBlazorServer(file);
|
|
203
|
+
if (blazorResult.isBlazorServer) {
|
|
204
|
+
blazorServer = blazorResult;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const authResult = detectAuthentication(file);
|
|
208
|
+
if (authResult.hasAuth) {
|
|
209
|
+
auth = authResult;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (detectHangfire(file)) {
|
|
213
|
+
hangfire = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Consolidate all config paths
|
|
218
|
+
const allPaths = [];
|
|
219
|
+
const seen = new Set();
|
|
220
|
+
|
|
221
|
+
for (const config of configs) {
|
|
222
|
+
for (const p of config.paths) {
|
|
223
|
+
if (!seen.has(p.configPath)) {
|
|
224
|
+
seen.add(p.configPath);
|
|
225
|
+
allPaths.push(p);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
appSettingsFiles,
|
|
232
|
+
programCsFiles,
|
|
233
|
+
configs: allPaths,
|
|
234
|
+
blazorServer,
|
|
235
|
+
auth,
|
|
236
|
+
hangfire,
|
|
237
|
+
envVarsMapping: allPaths.map(p => ({
|
|
238
|
+
configPath: p.configPath,
|
|
239
|
+
envVar: p.envVar,
|
|
240
|
+
isSecret: p.isSecret,
|
|
241
|
+
type: p.isSecret ? 'Secret' : 'Plain'
|
|
242
|
+
}))
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate environment variables mapping for display
|
|
248
|
+
* @param {Object} config - Project configuration
|
|
249
|
+
* @returns {string} - Formatted mapping table
|
|
250
|
+
*/
|
|
251
|
+
export function generateEnvVarsMapping(config) {
|
|
252
|
+
const secrets = config.envVarsMapping.filter(e => e.isSecret);
|
|
253
|
+
const plain = config.envVarsMapping.filter(e => !e.isSecret);
|
|
254
|
+
|
|
255
|
+
let output = '\n## Environment Variables Mapping\n\n';
|
|
256
|
+
|
|
257
|
+
if (secrets.length > 0) {
|
|
258
|
+
output += '### Secrets (require user input)\n';
|
|
259
|
+
output += '| Config Path | Environment Variable | Type |\n';
|
|
260
|
+
output += '|-------------|---------------------|------|\n';
|
|
261
|
+
secrets.forEach(s => {
|
|
262
|
+
output += `| ${s.configPath} | ${s.envVar} | Secret |\n`;
|
|
263
|
+
});
|
|
264
|
+
output += '\n';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (plain.length > 0) {
|
|
268
|
+
output += '### Plain Values\n';
|
|
269
|
+
output += '| Config Path | Environment Variable |\n';
|
|
270
|
+
output += '|-------------|---------------------|\n';
|
|
271
|
+
plain.slice(0, 10).forEach(p => {
|
|
272
|
+
output += `| ${p.configPath} | ${p.envVar} |\n`;
|
|
273
|
+
});
|
|
274
|
+
if (plain.length > 10) {
|
|
275
|
+
output += `| ... and ${plain.length - 10} more |\n`;
|
|
276
|
+
}
|
|
277
|
+
output += '\n';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return output;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Prerequisites Validation
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Validate all prerequisites for deployment
|
|
289
|
+
* @returns {Object} - Validation results
|
|
290
|
+
*/
|
|
291
|
+
export function validatePrerequisites() {
|
|
292
|
+
const checks = [];
|
|
293
|
+
|
|
294
|
+
// Azure CLI
|
|
295
|
+
try {
|
|
296
|
+
const azVersion = execSync('az --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
297
|
+
const versionMatch = azVersion.match(/azure-cli\s+(\d+\.\d+\.\d+)/);
|
|
298
|
+
checks.push({
|
|
299
|
+
name: 'Azure CLI',
|
|
300
|
+
passed: true,
|
|
301
|
+
version: versionMatch ? versionMatch[1] : 'unknown',
|
|
302
|
+
message: `Azure CLI ${versionMatch ? versionMatch[1] : ''} installed`
|
|
303
|
+
});
|
|
304
|
+
} catch {
|
|
305
|
+
checks.push({
|
|
306
|
+
name: 'Azure CLI',
|
|
307
|
+
passed: false,
|
|
308
|
+
message: 'Azure CLI not found',
|
|
309
|
+
fix: 'Download from: https://aka.ms/installazurecliwindows'
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Azure Login
|
|
314
|
+
try {
|
|
315
|
+
const account = execSync('az account show --output json', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
316
|
+
const accountInfo = JSON.parse(account);
|
|
317
|
+
checks.push({
|
|
318
|
+
name: 'Azure Login',
|
|
319
|
+
passed: true,
|
|
320
|
+
subscription: accountInfo.name,
|
|
321
|
+
subscriptionId: accountInfo.id,
|
|
322
|
+
message: `Logged in to ${accountInfo.name}`
|
|
323
|
+
});
|
|
324
|
+
} catch {
|
|
325
|
+
checks.push({
|
|
326
|
+
name: 'Azure Login',
|
|
327
|
+
passed: false,
|
|
328
|
+
message: 'Not logged in to Azure',
|
|
329
|
+
fix: 'Run: az login --tenant <TENANT-ID>'
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Docker
|
|
334
|
+
try {
|
|
335
|
+
execSync('docker info', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
336
|
+
const dockerVersion = execSync('docker --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
337
|
+
checks.push({
|
|
338
|
+
name: 'Docker',
|
|
339
|
+
passed: true,
|
|
340
|
+
message: dockerVersion.trim()
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
checks.push({
|
|
344
|
+
name: 'Docker',
|
|
345
|
+
passed: false,
|
|
346
|
+
message: 'Docker not running',
|
|
347
|
+
fix: 'Start Docker Desktop'
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// .NET SDK
|
|
352
|
+
try {
|
|
353
|
+
const dotnetVersion = execSync('dotnet --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
354
|
+
checks.push({
|
|
355
|
+
name: '.NET SDK',
|
|
356
|
+
passed: true,
|
|
357
|
+
version: dotnetVersion.trim(),
|
|
358
|
+
message: `.NET ${dotnetVersion.trim()} installed`
|
|
359
|
+
});
|
|
360
|
+
} catch {
|
|
361
|
+
checks.push({
|
|
362
|
+
name: '.NET SDK',
|
|
363
|
+
passed: false,
|
|
364
|
+
message: '.NET SDK not found',
|
|
365
|
+
fix: 'Download from: https://dot.net/download'
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// dotnet-ef
|
|
370
|
+
try {
|
|
371
|
+
const efList = execSync('dotnet tool list --global', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
372
|
+
const hasEf = efList.includes('dotnet-ef');
|
|
373
|
+
if (hasEf) {
|
|
374
|
+
checks.push({
|
|
375
|
+
name: 'dotnet-ef',
|
|
376
|
+
passed: true,
|
|
377
|
+
message: 'EF Core tools installed'
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
throw new Error('Not found');
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
checks.push({
|
|
384
|
+
name: 'dotnet-ef',
|
|
385
|
+
passed: false,
|
|
386
|
+
message: 'EF Core tools not found',
|
|
387
|
+
fix: 'Run: dotnet tool install --global dotnet-ef'
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Bicep files
|
|
392
|
+
const hasBicep = fs.existsSync('infra/main.bicep');
|
|
393
|
+
checks.push({
|
|
394
|
+
name: 'Bicep templates',
|
|
395
|
+
passed: hasBicep,
|
|
396
|
+
message: hasBicep ? 'infra/main.bicep found' : 'No Bicep templates found',
|
|
397
|
+
fix: hasBicep ? null : 'Run: /morph-infra init'
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Dockerfile
|
|
401
|
+
const hasDockerfile = fs.existsSync('Dockerfile');
|
|
402
|
+
checks.push({
|
|
403
|
+
name: 'Dockerfile',
|
|
404
|
+
passed: hasDockerfile,
|
|
405
|
+
message: hasDockerfile ? 'Dockerfile found' : 'No Dockerfile found',
|
|
406
|
+
fix: hasDockerfile ? null : 'Create Dockerfile for the project'
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
allPassed: checks.every(c => c.passed),
|
|
411
|
+
checks
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Validate password for special characters
|
|
417
|
+
* @param {string} password - Password to validate
|
|
418
|
+
* @returns {Object} - Validation result
|
|
419
|
+
*/
|
|
420
|
+
export function validatePassword(password) {
|
|
421
|
+
const specialChars = /[!@#$%^&*()+=\[\]{}|;:'",<>?/\\`~]/;
|
|
422
|
+
const hasSpecial = specialChars.test(password);
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
valid: !hasSpecial && password.length >= 16,
|
|
426
|
+
hasSpecialChars: hasSpecial,
|
|
427
|
+
length: password.length,
|
|
428
|
+
warnings: [
|
|
429
|
+
hasSpecial ? 'Contains special characters that may cause escape issues' : null,
|
|
430
|
+
password.length < 16 ? 'Password should be at least 16 characters' : null
|
|
431
|
+
].filter(Boolean)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ============================================================================
|
|
436
|
+
// Rollback Functions
|
|
437
|
+
// ============================================================================
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* List available revisions for rollback
|
|
441
|
+
* @param {string} appName - Container App name
|
|
442
|
+
* @param {string} resourceGroup - Resource group name
|
|
443
|
+
* @returns {string[]} - List of revision names
|
|
444
|
+
*/
|
|
445
|
+
export function listRevisions(appName, resourceGroup) {
|
|
446
|
+
try {
|
|
447
|
+
const result = execSync(
|
|
448
|
+
`az containerapp revision list --name ${appName} --resource-group ${resourceGroup} --query "[].{name:name,active:properties.active,traffic:properties.trafficWeight,created:properties.createdTime}" -o json`,
|
|
449
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
450
|
+
);
|
|
451
|
+
return JSON.parse(result);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throw new Error(`Failed to list revisions: ${error.message}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Execute rollback to previous revision
|
|
459
|
+
* @param {string} appName - Container App name
|
|
460
|
+
* @param {string} resourceGroup - Resource group name
|
|
461
|
+
* @param {string} revision - Target revision name
|
|
462
|
+
*/
|
|
463
|
+
export function executeRollback(appName, resourceGroup, revision) {
|
|
464
|
+
try {
|
|
465
|
+
execSync(
|
|
466
|
+
`az containerapp revision activate --name ${appName} --resource-group ${resourceGroup} --revision ${revision}`,
|
|
467
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
468
|
+
);
|
|
469
|
+
return { success: true, revision };
|
|
470
|
+
} catch (error) {
|
|
471
|
+
throw new Error(`Rollback failed: ${error.message}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ============================================================================
|
|
476
|
+
// Azure DevOps Pipeline Generation
|
|
477
|
+
// ============================================================================
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Generate Azure DevOps pipeline YAML
|
|
481
|
+
* @param {Object} options - Pipeline options
|
|
482
|
+
* @returns {string} - YAML content
|
|
483
|
+
*/
|
|
484
|
+
export function generatePipelineYaml(options = {}) {
|
|
485
|
+
const {
|
|
486
|
+
projectName = 'myapp',
|
|
487
|
+
acrName = 'myacr',
|
|
488
|
+
environments = ['dev', 'staging', 'prod']
|
|
489
|
+
} = options;
|
|
490
|
+
|
|
491
|
+
let yaml = `# Azure DevOps Pipeline for ${projectName}
|
|
492
|
+
# Generated by MORPH-SPEC Azure Deploy Specialist
|
|
493
|
+
|
|
494
|
+
trigger:
|
|
495
|
+
branches:
|
|
496
|
+
include:
|
|
497
|
+
- main
|
|
498
|
+
- develop
|
|
499
|
+
|
|
500
|
+
pr:
|
|
501
|
+
branches:
|
|
502
|
+
include:
|
|
503
|
+
- main
|
|
504
|
+
|
|
505
|
+
variables:
|
|
506
|
+
- group: 'deploy-secrets'
|
|
507
|
+
- name: projectName
|
|
508
|
+
value: '${projectName}'
|
|
509
|
+
- name: acrName
|
|
510
|
+
value: '${acrName}'
|
|
511
|
+
|
|
512
|
+
stages:
|
|
513
|
+
- stage: Build
|
|
514
|
+
displayName: 'Build and Push Docker Image'
|
|
515
|
+
jobs:
|
|
516
|
+
- job: BuildAndPush
|
|
517
|
+
pool:
|
|
518
|
+
vmImage: 'ubuntu-latest'
|
|
519
|
+
steps:
|
|
520
|
+
- task: Docker@2
|
|
521
|
+
displayName: 'Build and Push'
|
|
522
|
+
inputs:
|
|
523
|
+
containerRegistry: 'acr-service-connection'
|
|
524
|
+
repository: '\$(projectName)'
|
|
525
|
+
command: 'buildAndPush'
|
|
526
|
+
Dockerfile: '**/Dockerfile'
|
|
527
|
+
tags: |
|
|
528
|
+
\$(Build.BuildId)
|
|
529
|
+
latest
|
|
530
|
+
|
|
531
|
+
`;
|
|
532
|
+
|
|
533
|
+
// Add deployment stages for each environment
|
|
534
|
+
for (const env of environments) {
|
|
535
|
+
const condition = env === 'prod'
|
|
536
|
+
? `and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))`
|
|
537
|
+
: env === 'staging'
|
|
538
|
+
? `and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))`
|
|
539
|
+
: `succeeded()`;
|
|
540
|
+
|
|
541
|
+
yaml += `
|
|
542
|
+
- stage: Deploy${env.charAt(0).toUpperCase() + env.slice(1)}
|
|
543
|
+
displayName: 'Deploy to ${env.toUpperCase()}'
|
|
544
|
+
dependsOn: Build
|
|
545
|
+
condition: ${condition}
|
|
546
|
+
jobs:
|
|
547
|
+
- deployment: DeployTo${env.charAt(0).toUpperCase() + env.slice(1)}
|
|
548
|
+
displayName: 'Deploy to ${env}'
|
|
549
|
+
pool:
|
|
550
|
+
vmImage: 'ubuntu-latest'
|
|
551
|
+
environment: '${env}'
|
|
552
|
+
strategy:
|
|
553
|
+
runOnce:
|
|
554
|
+
deploy:
|
|
555
|
+
steps:
|
|
556
|
+
- task: AzureCLI@2
|
|
557
|
+
displayName: 'Deploy Container App'
|
|
558
|
+
inputs:
|
|
559
|
+
azureSubscription: 'azure-service-connection'
|
|
560
|
+
scriptType: 'bash'
|
|
561
|
+
scriptLocation: 'inlineScript'
|
|
562
|
+
inlineScript: |
|
|
563
|
+
npx @polymorphism-tech/morph-spec deploy ${env} --auto \\
|
|
564
|
+
--sql-password "\$(SqlPassword${env.charAt(0).toUpperCase() + env.slice(1)})" \\
|
|
565
|
+
--skip-confirmation
|
|
566
|
+
|
|
567
|
+
`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return yaml;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ============================================================================
|
|
574
|
+
// Main Command
|
|
575
|
+
// ============================================================================
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Main deploy command
|
|
579
|
+
* @param {string} environment - Target environment (dev, staging, prod)
|
|
580
|
+
* @param {Object} options - CLI options
|
|
581
|
+
*/
|
|
582
|
+
export async function deployCommand(environment, options) {
|
|
583
|
+
logger.header('MORPH-SPEC Azure Deploy Specialist');
|
|
584
|
+
logger.blank();
|
|
585
|
+
|
|
586
|
+
// Validate environment
|
|
587
|
+
const validEnvs = ['dev', 'staging', 'prod'];
|
|
588
|
+
if (environment && !validEnvs.includes(environment)) {
|
|
589
|
+
logger.error(`Invalid environment: ${environment}`);
|
|
590
|
+
logger.dim(` Valid options: ${validEnvs.join(', ')}`);
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Handle special commands
|
|
595
|
+
if (options.rollback) {
|
|
596
|
+
return handleRollback(options);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (options.generatePipeline) {
|
|
600
|
+
return handleGeneratePipeline(options);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Default: analyze project
|
|
604
|
+
if (!environment) {
|
|
605
|
+
return analyzeProject(options);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Full deploy flow
|
|
609
|
+
return executeDeployFlow(environment, options);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Analyze project configuration
|
|
614
|
+
*/
|
|
615
|
+
async function analyzeProject(options) {
|
|
616
|
+
logger.info('Analyzing project configuration...');
|
|
617
|
+
logger.blank();
|
|
618
|
+
|
|
619
|
+
const spinner = ora('Detecting configuration...').start();
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const config = detectProjectConfig('.');
|
|
623
|
+
spinner.succeed('Configuration detected');
|
|
624
|
+
logger.blank();
|
|
625
|
+
|
|
626
|
+
// Prerequisites
|
|
627
|
+
logger.header('Prerequisites');
|
|
628
|
+
const prereqs = validatePrerequisites();
|
|
629
|
+
prereqs.checks.forEach(check => {
|
|
630
|
+
if (check.passed) {
|
|
631
|
+
logger.success(` ${check.name}: ${check.message}`);
|
|
632
|
+
} else {
|
|
633
|
+
logger.error(` ${check.name}: ${check.message}`);
|
|
634
|
+
if (check.fix) {
|
|
635
|
+
logger.dim(` Fix: ${check.fix}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
logger.blank();
|
|
640
|
+
|
|
641
|
+
// Configuration files
|
|
642
|
+
logger.header('Configuration Files');
|
|
643
|
+
logger.dim(` appsettings.json files: ${config.appSettingsFiles.length}`);
|
|
644
|
+
config.appSettingsFiles.forEach(f => logger.dim(` - ${f}`));
|
|
645
|
+
logger.dim(` Program.cs files: ${config.programCsFiles.length}`);
|
|
646
|
+
logger.blank();
|
|
647
|
+
|
|
648
|
+
// Blazor Server
|
|
649
|
+
if (config.blazorServer.isBlazorServer) {
|
|
650
|
+
logger.header('Blazor Server Detected');
|
|
651
|
+
logger.warn(' Sticky sessions REQUIRED for Blazor Server');
|
|
652
|
+
logger.dim(' Will be configured automatically during deploy');
|
|
653
|
+
logger.blank();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Authentication
|
|
657
|
+
if (config.auth.hasAuth) {
|
|
658
|
+
logger.header('Authentication Detected');
|
|
659
|
+
Object.entries(config.auth.providers).forEach(([provider, detected]) => {
|
|
660
|
+
if (detected) {
|
|
661
|
+
logger.dim(` - ${provider}`);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
logger.blank();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Hangfire
|
|
668
|
+
if (config.hangfire) {
|
|
669
|
+
logger.header('Hangfire Detected');
|
|
670
|
+
logger.dim(' Background jobs will be configured');
|
|
671
|
+
logger.blank();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Environment Variables
|
|
675
|
+
logger.header('Environment Variables');
|
|
676
|
+
const secrets = config.envVarsMapping.filter(e => e.isSecret);
|
|
677
|
+
const plain = config.envVarsMapping.filter(e => !e.isSecret);
|
|
678
|
+
logger.dim(` Secrets: ${secrets.length}`);
|
|
679
|
+
logger.dim(` Plain: ${plain.length}`);
|
|
680
|
+
logger.blank();
|
|
681
|
+
|
|
682
|
+
if (secrets.length > 0) {
|
|
683
|
+
logger.header('Secrets Required (will prompt during deploy)');
|
|
684
|
+
secrets.forEach(s => {
|
|
685
|
+
logger.dim(` - ${s.configPath} -> ${s.envVar}`);
|
|
686
|
+
});
|
|
687
|
+
logger.blank();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// JSON output
|
|
691
|
+
if (options.json) {
|
|
692
|
+
console.log(JSON.stringify({
|
|
693
|
+
prerequisites: prereqs,
|
|
694
|
+
configuration: config
|
|
695
|
+
}, null, 2));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Next steps
|
|
699
|
+
logger.header('Next Steps');
|
|
700
|
+
if (!prereqs.allPassed) {
|
|
701
|
+
logger.warn(' 1. Fix prerequisites issues listed above');
|
|
702
|
+
}
|
|
703
|
+
logger.dim(' 1. Run: morph-spec deploy dev');
|
|
704
|
+
logger.dim(' 2. Provide secrets when prompted');
|
|
705
|
+
logger.dim(' 3. Confirm what-if before deploy');
|
|
706
|
+
logger.blank();
|
|
707
|
+
|
|
708
|
+
} catch (error) {
|
|
709
|
+
spinner.fail('Analysis failed');
|
|
710
|
+
logger.error(error.message);
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Handle rollback command
|
|
717
|
+
*/
|
|
718
|
+
async function handleRollback(options) {
|
|
719
|
+
logger.header('Rollback');
|
|
720
|
+
logger.warn('Rollback functionality requires interactive mode');
|
|
721
|
+
logger.dim(' Use: /morph-deploy --rollback in Claude Code');
|
|
722
|
+
logger.blank();
|
|
723
|
+
process.exit(0);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Handle pipeline generation
|
|
728
|
+
*/
|
|
729
|
+
async function handleGeneratePipeline(options) {
|
|
730
|
+
logger.header('Generate Azure DevOps Pipeline');
|
|
731
|
+
logger.blank();
|
|
732
|
+
|
|
733
|
+
const yaml = generatePipelineYaml({
|
|
734
|
+
projectName: options.projectName || 'myapp',
|
|
735
|
+
acrName: options.acrName || 'myacr',
|
|
736
|
+
environments: ['dev', 'staging', 'prod']
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const outputPath = 'azure-pipelines.yml';
|
|
740
|
+
fs.writeFileSync(outputPath, yaml);
|
|
741
|
+
|
|
742
|
+
logger.success(`Pipeline generated: ${outputPath}`);
|
|
743
|
+
logger.blank();
|
|
744
|
+
logger.dim('Next steps:');
|
|
745
|
+
logger.dim(' 1. Review and customize the generated pipeline');
|
|
746
|
+
logger.dim(' 2. Create service connections in Azure DevOps');
|
|
747
|
+
logger.dim(' 3. Create variable group "deploy-secrets" with your secrets');
|
|
748
|
+
logger.dim(' 4. Commit and push to trigger the pipeline');
|
|
749
|
+
logger.blank();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Execute full deploy flow
|
|
754
|
+
*/
|
|
755
|
+
async function executeDeployFlow(environment, options) {
|
|
756
|
+
logger.info(`Starting deploy to ${environment.toUpperCase()}...`);
|
|
757
|
+
logger.blank();
|
|
758
|
+
|
|
759
|
+
if (options.auto) {
|
|
760
|
+
logger.warn('Running in AUTO mode (CI/CD)');
|
|
761
|
+
logger.blank();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// This is primarily for analysis - full deployment is orchestrated by Claude
|
|
765
|
+
// using the slash command /morph-deploy which has interactive capabilities
|
|
766
|
+
|
|
767
|
+
logger.warn('Full deployment requires interactive mode');
|
|
768
|
+
logger.dim(' Use: /morph-deploy ' + environment + ' in Claude Code');
|
|
769
|
+
logger.blank();
|
|
770
|
+
logger.dim(' The slash command provides:');
|
|
771
|
+
logger.dim(' - Interactive secret collection');
|
|
772
|
+
logger.dim(' - What-if confirmation');
|
|
773
|
+
logger.dim(' - Progress tracking');
|
|
774
|
+
logger.dim(' - Automatic rollback on failure');
|
|
775
|
+
logger.blank();
|
|
776
|
+
|
|
777
|
+
if (options.dryRun) {
|
|
778
|
+
logger.info('Dry run completed. No changes made.');
|
|
779
|
+
}
|
|
780
|
+
}
|