chati-dev 4.2.1 → 4.3.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/README.md +80 -53
- package/bin/chati.js +150 -5
- package/framework/agents/build/dev.md +509 -76
- package/framework/agents/deploy/devops.md +40 -25
- package/framework/agents/discover/brief.md +156 -22
- package/framework/agents/discover/brownfield-wu.md +24 -14
- package/framework/agents/discover/greenfield-wu.md +100 -14
- package/framework/agents/plan/architect-data-engineer.md +6 -6
- package/framework/agents/plan/architect-system.md +46 -12
- package/framework/agents/plan/architect.md +40 -20
- package/framework/agents/plan/detail.md +36 -24
- package/framework/agents/plan/phases.md +36 -26
- package/framework/agents/plan/tasks.md +114 -33
- package/framework/agents/plan/ux-brand-architect.md +240 -8
- package/framework/agents/plan/ux-component-engineer.md +28 -13
- package/framework/agents/plan/ux-researcher.md +7 -6
- package/framework/agents/plan/ux.md +55 -22
- package/framework/agents/quality/qa-implementation.md +143 -74
- package/framework/agents/quality/qa-planning.md +115 -42
- package/framework/agents/quality/qa-visual.md +439 -0
- package/framework/agents/shared/visualizer.md +128 -0
- package/framework/config.yaml +8 -7
- package/framework/constitution.md +127 -43
- package/framework/context/governance.md +12 -7
- package/framework/context/quality.md +6 -5
- package/framework/context/root.md +6 -6
- package/framework/data/entity-registry.yaml +377 -4
- package/framework/data/model-limits.json +19 -0
- package/framework/domains/agents/qa-visual.yaml +74 -0
- package/framework/domains/constitution.yaml +46 -2
- package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
- package/framework/hooks/advance-trigger.js +131 -0
- package/framework/hooks/brief-validator.js +83 -0
- package/framework/hooks/constitution-guard.js +91 -36
- package/framework/hooks/license-guard.js +129 -190
- package/framework/hooks/mode-governance.js +68 -16
- package/framework/hooks/model-governance.js +19 -8
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/post-dev.js +207 -0
- package/framework/hooks/prism-engine.js +288 -105
- package/framework/hooks/read-protection.js +37 -9
- package/framework/hooks/reasoning-escalator.js +371 -0
- package/framework/hooks/reference-trigger.js +117 -0
- package/framework/hooks/session-digest.js +94 -20
- package/framework/hooks/settings.json +32 -1
- package/framework/hooks/style-guard.js +52 -13
- package/framework/hooks/team-quality-gate.js +58 -25
- package/framework/hooks/undercover-guard.js +34 -13
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/context-engine.md +4 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/intelligence/hooks-performance.md +3 -3
- package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
- package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
- package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
- package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
- package/framework/orchestrator/chati-router.js +387 -0
- package/framework/orchestrator/chati-update.md +40 -40
- package/framework/orchestrator/chati.md +318 -115
- package/framework/scaffold/motion-premium/README.md +89 -0
- package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
- package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
- package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
- package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
- package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
- package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
- package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
- package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
- package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
- package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
- package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
- package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
- package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
- package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
- package/framework/scaffold/motion-premium-3d/README.md +80 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
- package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
- package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
- package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
- package/framework/schemas/session.schema.json +109 -21
- package/framework/scripts/reference-capture.js +430 -0
- package/framework/scripts/visual-qa.js +674 -0
- package/framework/tasks/orchestrator-handoff.md +1 -1
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/orchestrator-route.md +1 -1
- package/framework/tasks/orchestrator-status.md +3 -3
- package/framework/tasks/qa-planning-gate-define.md +1 -1
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/brandbook-tmpl.yaml +1 -1
- package/framework/templates/component-spec-tmpl.yaml +1 -1
- package/framework/templates/design-token-tmpl.yaml +1 -1
- package/framework/templates/icon-system-tmpl.yaml +1 -1
- package/framework/templates/team-planning-tasks.yaml +6 -5
- package/framework/workflows/brownfield-discovery.yaml +2 -2
- package/framework/workflows/brownfield-fullstack.yaml +15 -11
- package/framework/workflows/brownfield-service.yaml +14 -10
- package/framework/workflows/brownfield-ui.yaml +15 -11
- package/framework/workflows/greenfield-fullstack.yaml +16 -13
- package/framework/workflows/quick-flow.yaml +3 -3
- package/framework/workflows/standard-flow.yaml +12 -9
- package/package.json +10 -5
- package/src/autonomy/autonomous-gate.js +1 -0
- package/src/autonomy/build-state.js +1 -2
- package/src/autonomy/progress-reporter.js +1 -1
- package/src/config/agent-customizer.js +11 -3
- package/src/config/claude-settings-generator.js +226 -0
- package/src/config/context-file-generator.js +41 -21
- package/src/config/framework-adapter.js +1 -0
- package/src/config/gemini-hooks-generator.js +76 -6
- package/src/config/mcp-configs.js +1 -0
- package/src/context/layers/l1-global.js +2 -1
- package/src/dashboard/data-reader.js +4 -3
- package/src/dashboard/layout.js +2 -1
- package/src/decision/analyzer.js +6 -30
- package/src/decision/engine.js +4 -28
- package/src/decision/registry-healer.js +3 -2
- package/src/decision/registry-updater.js +23 -14
- package/src/extensions/loader.js +2 -8
- package/src/gates/g1-planning-complete.js +2 -1
- package/src/gates/g2-qa-planning.js +2 -1
- package/src/gates/g3-implementation.js +2 -1
- package/src/gates/g4-qa-implementation.js +3 -2
- package/src/gates/g5-deploy-ready.js +2 -1
- package/src/health/engine.js +4 -3
- package/src/installer/core.js +557 -81
- package/src/installer/preflight.js +131 -0
- package/src/installer/provider-overlay.js +3 -3
- package/src/installer/scaffold-applier.js +358 -0
- package/src/installer/templates.js +52 -31
- package/src/installer/validator.js +17 -12
- package/src/intelligence/registry-manager.js +22 -29
- package/src/intelligence/timeline.js +11 -6
- package/src/license/commands.js +1 -1
- package/src/license/wait.js +102 -0
- package/src/memory/agent-memory.js +81 -0
- package/src/memory/dream.js +32 -1
- package/src/merger/replace-merger.js +28 -15
- package/src/orchestrator/agent-selector.js +2 -1
- package/src/orchestrator/cli.js +1908 -80
- package/src/orchestrator/doctor.js +270 -0
- package/src/orchestrator/handoff-engine.js +4 -3
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/pipeline-manager.js +306 -15
- package/src/orchestrator/session-manager.js +331 -6
- package/src/tasks/handoff.js +3 -2
- package/src/telemetry/config.js +4 -3
- package/src/telemetry/schema.js +1 -0
- package/src/terminal/collector.js +3 -2
- package/src/terminal/index.js +1 -2
- package/src/terminal/isolation.js +52 -18
- package/src/terminal/prompt-builder.js +42 -25
- package/src/terminal/run-parallel.js +1 -1
- package/src/terminal/run-team.js +3 -3
- package/src/terminal/team-task-list.js +43 -4
- package/src/upgrade/backup.js +3 -2
- package/src/upgrade/checker.js +3 -2
- package/src/upgrade/migrator.js +65 -7
- package/src/upgrade/tracked-files-detector.js +86 -0
- package/src/upgrade/user-messages.js +94 -0
- package/src/utils/config-parser.js +2 -1
- package/src/utils/feature-flags.js +2 -1
- package/src/utils/flatten-entities.js +69 -0
- package/src/utils/framework-dir.js +16 -0
- package/src/utils/model-id.js +85 -0
- package/src/utils/provider-limits.js +84 -23
- package/src/utils/schema-validator.js +1 -1
- package/src/wizard/i18n.js +5 -4
- package/src/wizard/index.js +14 -0
- package/assets/logo - c/303/263pia.png +0 -0
- package/assets/logo.svg +0 -42
- package/assets/logo2.png +0 -0
- package/assets/social-preview.png +0 -0
- package/scripts/bundle-framework.js +0 -69
- package/scripts/changelog-generator.js +0 -222
- package/scripts/codebase-mapper.js +0 -728
- package/scripts/commit-message-generator.js +0 -167
- package/scripts/coverage-analyzer.js +0 -260
- package/scripts/dependency-analyzer.js +0 -280
- package/scripts/doctor/checks/agents.js +0 -77
- package/scripts/doctor/checks/constitution.js +0 -41
- package/scripts/doctor/checks/domain-alignment.js +0 -58
- package/scripts/doctor/checks/prism-layers.js +0 -84
- package/scripts/doctor/checks/registry.js +0 -55
- package/scripts/doctor/checks/schemas.js +0 -61
- package/scripts/doctor/fixes/reference-fix.js +0 -100
- package/scripts/doctor/fixes/registry-fix.js +0 -56
- package/scripts/doctor/index.js +0 -212
- package/scripts/framework-analyzer.js +0 -308
- package/scripts/generate-constitution-domain.js +0 -253
- package/scripts/generate-signing-key.js +0 -33
- package/scripts/health-check.js +0 -481
- package/scripts/ide-sync.js +0 -326
- package/scripts/performance-analyzer.js +0 -325
- package/scripts/plan-tracker.js +0 -278
- package/scripts/populate-entity-registry.js +0 -481
- package/scripts/pr-review.js +0 -317
- package/scripts/rollback-manager.js +0 -310
- package/scripts/semantic-lint.js +0 -328
- package/scripts/sign-manifest.js +0 -53
- package/scripts/stuck-detector.js +0 -343
- package/scripts/test-quality-assessment.js +0 -257
- package/scripts/validate-agents.js +0 -368
- package/scripts/validate-package.js +0 -367
- package/scripts/validate-tasks.js +0 -465
- package/src/autonomy/worktree-manager.js +0 -250
- package/src/intelligence/decision-engine.js +0 -256
- package/src/intelligence/document-sharder.js +0 -221
- package/src/intelligence/elicitation.js +0 -265
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Preflight system check — validates prerequisites before install.
|
|
3
|
+
*
|
|
4
|
+
* Runs BEFORE the installer wizard collects config. Checks:
|
|
5
|
+
* 1. Node.js >= 20 (mandatory — already running, so just version check)
|
|
6
|
+
* 2. npm >= 9 (mandatory)
|
|
7
|
+
* 3. Git available (mandatory)
|
|
8
|
+
* 4. Playwright (optional — auto-installs chromium if missing)
|
|
9
|
+
*
|
|
10
|
+
* If a mandatory check fails, the installer should abort.
|
|
11
|
+
* If an optional check fails, it warns but continues.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{ name: string, status: 'pass'|'fail'|'warn', version?: string, message: string }} PreflightCheck
|
|
18
|
+
* @typedef {{ passed: boolean, checks: PreflightCheck[] }} PreflightResult
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run all preflight checks.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ silent?: boolean }} options
|
|
25
|
+
* @returns {Promise<PreflightResult>}
|
|
26
|
+
*/
|
|
27
|
+
export async function runPreflightCheck(options = {}) {
|
|
28
|
+
const checks = [];
|
|
29
|
+
let mandatoryFailed = false;
|
|
30
|
+
|
|
31
|
+
// 1. Node.js >= 20 — we're already running Node, just check version
|
|
32
|
+
const nodeVersion = process.version;
|
|
33
|
+
const nodeMajor = parseInt(nodeVersion.replace('v', '').split('.')[0], 10);
|
|
34
|
+
if (nodeMajor >= 20) {
|
|
35
|
+
checks.push({ name: 'node', status: 'pass', version: nodeVersion, message: `Node.js ${nodeVersion}` });
|
|
36
|
+
} else {
|
|
37
|
+
checks.push({ name: 'node', status: 'fail', version: nodeVersion, message: `Node.js ${nodeVersion} — requires >= 20.0.0` });
|
|
38
|
+
mandatoryFailed = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. npm >= 9
|
|
42
|
+
try {
|
|
43
|
+
const npmVersion = execSync('npm --version', { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
44
|
+
const npmMajor = parseInt(npmVersion.split('.')[0], 10);
|
|
45
|
+
if (npmMajor >= 9) {
|
|
46
|
+
checks.push({ name: 'npm', status: 'pass', version: npmVersion, message: `npm v${npmVersion}` });
|
|
47
|
+
} else {
|
|
48
|
+
checks.push({ name: 'npm', status: 'fail', version: npmVersion, message: `npm v${npmVersion} — requires >= 9.0.0` });
|
|
49
|
+
mandatoryFailed = true;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
checks.push({ name: 'npm', status: 'fail', message: 'npm not found' });
|
|
53
|
+
mandatoryFailed = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. Git
|
|
57
|
+
try {
|
|
58
|
+
const gitOut = execSync('git --version', { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
59
|
+
const gitVersion = gitOut.replace('git version ', '');
|
|
60
|
+
checks.push({ name: 'git', status: 'pass', version: gitVersion, message: `Git ${gitVersion}` });
|
|
61
|
+
} catch {
|
|
62
|
+
checks.push({ name: 'git', status: 'fail', message: 'Git not found — install from https://git-scm.com' });
|
|
63
|
+
mandatoryFailed = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Playwright (optional — auto-install chromium if missing)
|
|
67
|
+
const playwrightCheck = await checkPlaywright(options);
|
|
68
|
+
checks.push(playwrightCheck);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
passed: !mandatoryFailed,
|
|
72
|
+
checks,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if Playwright + chromium browser are available.
|
|
78
|
+
* If Playwright is installed but chromium is missing, auto-install it.
|
|
79
|
+
* If Playwright itself is missing, try to install it via npx.
|
|
80
|
+
*
|
|
81
|
+
* @param {{ silent?: boolean }} options
|
|
82
|
+
* @returns {Promise<PreflightCheck>}
|
|
83
|
+
*/
|
|
84
|
+
async function checkPlaywright(options = {}) {
|
|
85
|
+
// Try importing playwright
|
|
86
|
+
let pw;
|
|
87
|
+
try {
|
|
88
|
+
pw = await import('playwright');
|
|
89
|
+
} catch {
|
|
90
|
+
// Playwright not installed — try to install via npx
|
|
91
|
+
if (!options.silent) {
|
|
92
|
+
process.stderr.write(' Installing Playwright chromium (required for visual analysis)...\n');
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
execSync('npx playwright install chromium', {
|
|
96
|
+
stdio: options.silent ? 'ignore' : 'inherit',
|
|
97
|
+
timeout: 120000,
|
|
98
|
+
});
|
|
99
|
+
// Verify installation succeeded
|
|
100
|
+
try {
|
|
101
|
+
pw = await import('playwright');
|
|
102
|
+
} catch {
|
|
103
|
+
return { name: 'playwright', status: 'warn', message: 'Playwright install attempted but import still fails — visual features disabled' };
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
return { name: 'playwright', status: 'warn', message: 'Playwright not available — visual features (reference capture, QA-Visual) disabled' };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Playwright module found — verify chromium browser is installed
|
|
111
|
+
try {
|
|
112
|
+
const browser = await pw.chromium.launch({ headless: true });
|
|
113
|
+
const version = browser.version();
|
|
114
|
+
await browser.close();
|
|
115
|
+
return { name: 'playwright', status: 'pass', version, message: `Playwright chromium ${version}` };
|
|
116
|
+
} catch {
|
|
117
|
+
// Browser missing — auto-install
|
|
118
|
+
if (!options.silent) {
|
|
119
|
+
process.stderr.write(' Installing Playwright chromium browser...\n');
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
execSync('npx playwright install chromium', {
|
|
123
|
+
stdio: options.silent ? 'ignore' : 'inherit',
|
|
124
|
+
timeout: 120000,
|
|
125
|
+
});
|
|
126
|
+
return { name: 'playwright', status: 'pass', message: 'Playwright chromium installed' };
|
|
127
|
+
} catch {
|
|
128
|
+
return { name: 'playwright', status: 'warn', message: 'Playwright chromium install failed — visual features disabled' };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -32,7 +32,7 @@ import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter
|
|
|
32
32
|
*/
|
|
33
33
|
export function generateProviderOverlays(targetDir, frameworkSource, primaryProvider, allProviders) {
|
|
34
34
|
const result = { generated: [], skipped: [] };
|
|
35
|
-
const frameworkDir = join(targetDir, 'chati.dev');
|
|
35
|
+
const frameworkDir = join(targetDir, '.chati.dev');
|
|
36
36
|
|
|
37
37
|
for (const provider of allProviders) {
|
|
38
38
|
if (provider === primaryProvider) {
|
|
@@ -74,9 +74,9 @@ export function generateProviderOverlays(targetDir, frameworkSource, primaryProv
|
|
|
74
74
|
*/
|
|
75
75
|
export function resolveOverlayPath(projectDir, relativePath, provider) {
|
|
76
76
|
// Check overlay first
|
|
77
|
-
const overlayPath = join(projectDir, 'chati.dev', '.adapted', provider, relativePath);
|
|
77
|
+
const overlayPath = join(projectDir, '.chati.dev', '.adapted', provider, relativePath);
|
|
78
78
|
if (existsSync(overlayPath)) return overlayPath;
|
|
79
79
|
|
|
80
80
|
// Fallback to main chati.dev/
|
|
81
|
-
return join(projectDir, 'chati.dev', relativePath);
|
|
81
|
+
return join(projectDir, '.chati.dev', relativePath);
|
|
82
82
|
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/installer/scaffold-applier.js
|
|
3
|
+
*
|
|
4
|
+
* Applies a scaffold preset (e.g. motion-premium) to a user project. Reads
|
|
5
|
+
* the preset's scaffold.yaml, resolves placeholder values (from an existing
|
|
6
|
+
* brand.ts or the yaml's defaults), writes each template's resolved content
|
|
7
|
+
* to the target path, and emits .chati/scaffold-manifest.json for later
|
|
8
|
+
* upgrade diffing.
|
|
9
|
+
*
|
|
10
|
+
* Design (see .planning/scaffold-premium-v1.md, Decisions 1 and 3):
|
|
11
|
+
* - Copy-on-apply, NOT on install. The installer only copies the scaffold
|
|
12
|
+
* SOURCE files to .chati.dev/scaffold/; this module is invoked when the
|
|
13
|
+
* orchestrator or CLI user decides to apply.
|
|
14
|
+
* - Placeholder resolution is install-time and one-shot. Re-running the
|
|
15
|
+
* applier after the user edited brand.ts is the documented --sync-brand
|
|
16
|
+
* workflow (stub in v1; see TODO below).
|
|
17
|
+
* - First-apply never overwrites existing files — a safe default. A
|
|
18
|
+
* dedicated upgrade path (scaffold --upgrade, future) uses hash diffing
|
|
19
|
+
* via brownfield-upgrader.js.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync,
|
|
24
|
+
} from 'fs';
|
|
25
|
+
import { join, dirname } from 'path';
|
|
26
|
+
import { fileURLToPath } from 'url';
|
|
27
|
+
import yaml from 'js-yaml';
|
|
28
|
+
import { hashContent } from './file-hasher.js';
|
|
29
|
+
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
|
|
32
|
+
// Resolution chain for the scaffold source directory.
|
|
33
|
+
// 1. Installed framework in the target project: {targetDir}/.chati.dev/scaffold/
|
|
34
|
+
// 2. npm-bundled framework next to this file: {pkgRoot}/framework/scaffold/
|
|
35
|
+
// 3. Monorepo canonical framework (dev only): {repoRoot}/chati.dev/scaffold/
|
|
36
|
+
const BUNDLED_SCAFFOLD = join(__dirname, '..', '..', 'framework', 'scaffold');
|
|
37
|
+
const MONOREPO_SCAFFOLD = join(__dirname, '..', '..', '..', '..', 'chati.dev', 'scaffold');
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the directory containing scaffold presets for a given project.
|
|
41
|
+
* Preference: installed > bundled > monorepo.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} [targetDir] - Project root (looks for installed .chati.dev/)
|
|
44
|
+
* @returns {string} Absolute path to the scaffold root
|
|
45
|
+
* @throws {Error} If no candidate exists on disk
|
|
46
|
+
*/
|
|
47
|
+
export function resolveScaffoldSource(targetDir) {
|
|
48
|
+
if (targetDir) {
|
|
49
|
+
const installed = join(targetDir, '.chati.dev', 'scaffold');
|
|
50
|
+
if (existsSync(installed)) return installed;
|
|
51
|
+
}
|
|
52
|
+
if (existsSync(BUNDLED_SCAFFOLD)) return BUNDLED_SCAFFOLD;
|
|
53
|
+
if (existsSync(MONOREPO_SCAFFOLD)) return MONOREPO_SCAFFOLD;
|
|
54
|
+
throw new Error(
|
|
55
|
+
'Could not locate scaffold source directory. Checked: ' +
|
|
56
|
+
[
|
|
57
|
+
targetDir ? join(targetDir, '.chati.dev', 'scaffold') : null,
|
|
58
|
+
BUNDLED_SCAFFOLD,
|
|
59
|
+
MONOREPO_SCAFFOLD,
|
|
60
|
+
].filter(Boolean).join(', ')
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* List available scaffold presets at a given scaffold source directory.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} scaffoldSourceDir - From resolveScaffoldSource()
|
|
68
|
+
* @returns {Array<{ id: string, version: string, description: string }>}
|
|
69
|
+
*/
|
|
70
|
+
export function listPresets(scaffoldSourceDir) {
|
|
71
|
+
if (!existsSync(scaffoldSourceDir)) return [];
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const name of readdirSync(scaffoldSourceDir)) {
|
|
74
|
+
const presetDir = join(scaffoldSourceDir, name);
|
|
75
|
+
let s;
|
|
76
|
+
try { s = statSync(presetDir); } catch { continue; }
|
|
77
|
+
if (!s.isDirectory()) continue;
|
|
78
|
+
const yamlPath = join(presetDir, 'scaffold.yaml');
|
|
79
|
+
if (!existsSync(yamlPath)) continue;
|
|
80
|
+
try {
|
|
81
|
+
const parsed = yaml.load(readFileSync(yamlPath, 'utf-8'));
|
|
82
|
+
out.push({
|
|
83
|
+
id: parsed.id || name,
|
|
84
|
+
version: parsed.version || '0.0.0',
|
|
85
|
+
description: parsed.description || '',
|
|
86
|
+
});
|
|
87
|
+
} catch { /* skip unparseable */ }
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load and parse scaffold.yaml for a specific preset.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} scaffoldSourceDir - From resolveScaffoldSource()
|
|
96
|
+
* @param {string} preset - Preset id (e.g. 'motion-premium')
|
|
97
|
+
* @returns {object} Parsed manifest with an injected `sourceDir` property
|
|
98
|
+
* @throws {Error} If preset or scaffold.yaml is not found
|
|
99
|
+
*/
|
|
100
|
+
export function loadScaffoldManifest(scaffoldSourceDir, preset) {
|
|
101
|
+
const presetDir = join(scaffoldSourceDir, preset);
|
|
102
|
+
const yamlPath = join(presetDir, 'scaffold.yaml');
|
|
103
|
+
if (!existsSync(yamlPath)) {
|
|
104
|
+
throw new Error(`Scaffold preset "${preset}" not found at ${presetDir}`);
|
|
105
|
+
}
|
|
106
|
+
const parsed = yaml.load(readFileSync(yamlPath, 'utf-8'));
|
|
107
|
+
return { ...parsed, sourceDir: presetDir };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Replace {{PLACEHOLDER}} tokens in template content with resolved values.
|
|
112
|
+
* Unmatched placeholders are left intact (aids debugging — a missed value
|
|
113
|
+
* shows up as a visible {{FOO}} in the generated file rather than silently
|
|
114
|
+
* empty).
|
|
115
|
+
*
|
|
116
|
+
* @param {string} templateContent
|
|
117
|
+
* @param {Record<string, string>} values
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
export function resolvePlaceholders(templateContent, values) {
|
|
121
|
+
return templateContent.replace(/\{\{([A-Z_][A-Z0-9_]*)\}\}/g, (match, key) => {
|
|
122
|
+
return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : match;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse a brand.ts source file and extract brand.colors values.
|
|
128
|
+
* Regex-based and tolerant of formatting variations. Not a full TypeScript
|
|
129
|
+
* parser — assumes the brand.ts shape shipped by the scaffold (object
|
|
130
|
+
* literal with `colors: { key: 'value', ... } as const`).
|
|
131
|
+
*
|
|
132
|
+
* @param {string} source - brand.ts file content
|
|
133
|
+
* @returns {Record<string, string>} { primary: '#...', primaryRgb: '...', ... }
|
|
134
|
+
*/
|
|
135
|
+
export function parseBrandTs(source) {
|
|
136
|
+
// Match `colors: { ... }` and capture the body non-greedily.
|
|
137
|
+
// The body is bounded by its own closing brace at the same indent level.
|
|
138
|
+
const colorsMatch = source.match(/colors\s*:\s*\{([\s\S]*?)^\s*\}/m);
|
|
139
|
+
if (!colorsMatch) return {};
|
|
140
|
+
const body = colorsMatch[1];
|
|
141
|
+
const out = {};
|
|
142
|
+
// Match lines like: primary: '#7C3AED', or primaryRgb: "124, 58, 237",
|
|
143
|
+
const pairRe = /([a-zA-Z_][\w]*)\s*:\s*['"]([^'"]+)['"]/g;
|
|
144
|
+
let m;
|
|
145
|
+
while ((m = pairRe.exec(body)) !== null) {
|
|
146
|
+
out[m[1]] = m[2];
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Map camelCase brand.colors keys to UPPER_SNAKE placeholder names, using
|
|
153
|
+
* the scaffold.yaml `placeholders` block as the authority on source paths.
|
|
154
|
+
*
|
|
155
|
+
* @param {Record<string, string>} brandColors
|
|
156
|
+
* @param {object} placeholdersMap - scaffold.yaml `placeholders`
|
|
157
|
+
* @returns {Record<string, string>}
|
|
158
|
+
*/
|
|
159
|
+
export function mapBrandToPlaceholders(brandColors, placeholdersMap) {
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const [phKey, phDef] of Object.entries(placeholdersMap || {})) {
|
|
162
|
+
const pathMatch = phDef && phDef.source && phDef.source.match(/^brand\.colors\.(\w+)$/);
|
|
163
|
+
if (!pathMatch) continue;
|
|
164
|
+
const colorKey = pathMatch[1];
|
|
165
|
+
if (Object.prototype.hasOwnProperty.call(brandColors, colorKey)) {
|
|
166
|
+
out[phKey] = brandColors[colorKey];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the full set of placeholder values for a project.
|
|
174
|
+
*
|
|
175
|
+
* Strategy (per scaffold.yaml brand_sources, in priority order):
|
|
176
|
+
* 1. Defaults from scaffold.yaml — always the baseline
|
|
177
|
+
* 2. Parsed values from {projectDir}/lib/brand.ts — if present
|
|
178
|
+
* 3. Caller-supplied overrides — highest priority
|
|
179
|
+
*
|
|
180
|
+
* Interactive prompt (brand_sources entry `type: interactive`) is NOT
|
|
181
|
+
* implemented in v1 — the applier is always non-interactive and falls
|
|
182
|
+
* through to defaults if no brand source exists.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} projectDir
|
|
185
|
+
* @param {object} scaffoldManifest - From loadScaffoldManifest
|
|
186
|
+
* @param {Record<string, string>} [overrides]
|
|
187
|
+
* @returns {{ values: Record<string, string>, source: string }}
|
|
188
|
+
*/
|
|
189
|
+
export function resolveBrandValues(projectDir, scaffoldManifest, overrides = {}) {
|
|
190
|
+
const placeholdersMap = scaffoldManifest.placeholders || {};
|
|
191
|
+
const values = {};
|
|
192
|
+
for (const [k, v] of Object.entries(placeholdersMap)) {
|
|
193
|
+
if (v && v.default !== undefined) values[k] = v.default;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let source = 'defaults';
|
|
197
|
+
const brandTsPath = join(projectDir, 'lib', 'brand.ts');
|
|
198
|
+
if (existsSync(brandTsPath)) {
|
|
199
|
+
try {
|
|
200
|
+
const brandColors = parseBrandTs(readFileSync(brandTsPath, 'utf-8'));
|
|
201
|
+
const mapped = mapBrandToPlaceholders(brandColors, placeholdersMap);
|
|
202
|
+
if (Object.keys(mapped).length > 0) {
|
|
203
|
+
Object.assign(values, mapped);
|
|
204
|
+
source = 'lib/brand.ts';
|
|
205
|
+
}
|
|
206
|
+
} catch { /* keep defaults on parse failure */ }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (Object.keys(overrides).length > 0) {
|
|
210
|
+
Object.assign(values, overrides);
|
|
211
|
+
source = source === 'defaults' ? 'overrides' : `${source} + overrides`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { values, source };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Apply a scaffold preset to a project directory.
|
|
219
|
+
*
|
|
220
|
+
* First-apply semantics:
|
|
221
|
+
* - Existing target files are SKIPPED (never overwritten without --force).
|
|
222
|
+
* - Fresh files are written with placeholders resolved.
|
|
223
|
+
* - A scaffold-manifest.json is written to .chati/ recording hashes,
|
|
224
|
+
* source template names, phase, override class, and the placeholder
|
|
225
|
+
* values used. The manifest is the input for future upgrade runs.
|
|
226
|
+
*
|
|
227
|
+
* Dry-run semantics:
|
|
228
|
+
* - No files written (neither targets nor manifest).
|
|
229
|
+
* - Return value describes what WOULD happen.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} opts
|
|
232
|
+
* @param {string} opts.projectDir - Target project root
|
|
233
|
+
* @param {string} opts.scaffoldSourceDir - From resolveScaffoldSource()
|
|
234
|
+
* @param {string} opts.preset - Preset id
|
|
235
|
+
* @param {Record<string, string>} [opts.values] - Placeholder overrides
|
|
236
|
+
* @param {boolean} [opts.dryRun=false]
|
|
237
|
+
* @param {boolean} [opts.force=false] - Overwrite existing files
|
|
238
|
+
* @returns {{
|
|
239
|
+
* preset: string,
|
|
240
|
+
* version: string,
|
|
241
|
+
* applied: string[],
|
|
242
|
+
* skipped: Array<{ target: string, reason: string }>,
|
|
243
|
+
* placeholders: Record<string, string>,
|
|
244
|
+
* brandSource: string,
|
|
245
|
+
* dryRun: boolean,
|
|
246
|
+
* manifestPath: string | null,
|
|
247
|
+
* }}
|
|
248
|
+
*/
|
|
249
|
+
export function applyScaffold(opts) {
|
|
250
|
+
const {
|
|
251
|
+
projectDir,
|
|
252
|
+
scaffoldSourceDir,
|
|
253
|
+
preset,
|
|
254
|
+
values: overrides = {},
|
|
255
|
+
dryRun = false,
|
|
256
|
+
force = false,
|
|
257
|
+
} = opts;
|
|
258
|
+
|
|
259
|
+
if (!projectDir) throw new Error('applyScaffold: projectDir is required');
|
|
260
|
+
if (!scaffoldSourceDir) throw new Error('applyScaffold: scaffoldSourceDir is required');
|
|
261
|
+
if (!preset) throw new Error('applyScaffold: preset is required');
|
|
262
|
+
|
|
263
|
+
const manifest = loadScaffoldManifest(scaffoldSourceDir, preset);
|
|
264
|
+
const { values, source: brandSource } = resolveBrandValues(projectDir, manifest, overrides);
|
|
265
|
+
|
|
266
|
+
const applied = [];
|
|
267
|
+
const skipped = [];
|
|
268
|
+
const manifestFiles = {};
|
|
269
|
+
|
|
270
|
+
for (const fileSpec of manifest.files || []) {
|
|
271
|
+
const sourcePath = join(manifest.sourceDir, fileSpec.source);
|
|
272
|
+
const targetPath = join(projectDir, fileSpec.target);
|
|
273
|
+
|
|
274
|
+
if (!existsSync(sourcePath)) {
|
|
275
|
+
skipped.push({ target: fileSpec.target, reason: 'source-missing' });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (existsSync(targetPath) && !force) {
|
|
279
|
+
skipped.push({ target: fileSpec.target, reason: 'already-exists' });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const raw = readFileSync(sourcePath, 'utf-8');
|
|
284
|
+
const resolved = resolvePlaceholders(raw, values);
|
|
285
|
+
|
|
286
|
+
if (!dryRun) {
|
|
287
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
288
|
+
writeFileSync(targetPath, resolved, 'utf-8');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
manifestFiles[fileSpec.target] = {
|
|
292
|
+
hash: hashContent(resolved),
|
|
293
|
+
source: fileSpec.source,
|
|
294
|
+
phase: fileSpec.phase,
|
|
295
|
+
override: fileSpec.override,
|
|
296
|
+
};
|
|
297
|
+
applied.push(fileSpec.target);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let manifestPath = null;
|
|
301
|
+
if (!dryRun) {
|
|
302
|
+
manifestPath = join(projectDir, '.chati', 'scaffold-manifest.json');
|
|
303
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
304
|
+
const manifestBody = {
|
|
305
|
+
scaffold: manifest.id,
|
|
306
|
+
version: manifest.version,
|
|
307
|
+
appliedAt: new Date().toISOString(),
|
|
308
|
+
brandSource,
|
|
309
|
+
placeholders: values,
|
|
310
|
+
files: manifestFiles,
|
|
311
|
+
};
|
|
312
|
+
writeFileSync(manifestPath, JSON.stringify(manifestBody, null, 2) + '\n', 'utf-8');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
preset: manifest.id,
|
|
317
|
+
version: manifest.version,
|
|
318
|
+
applied,
|
|
319
|
+
skipped,
|
|
320
|
+
placeholders: values,
|
|
321
|
+
brandSource,
|
|
322
|
+
dryRun,
|
|
323
|
+
manifestPath,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Stub: re-resolve brand placeholders against an existing scaffold apply.
|
|
329
|
+
* Not implemented in v1. The workflow is documented in the motion-premium
|
|
330
|
+
* README — users edit brand.ts, then manually reconcile globals.css OR
|
|
331
|
+
* wait for this command to ship.
|
|
332
|
+
*
|
|
333
|
+
* @returns {never}
|
|
334
|
+
*/
|
|
335
|
+
export function syncBrand() {
|
|
336
|
+
throw new Error(
|
|
337
|
+
'scaffold --sync-brand is not yet implemented. ' +
|
|
338
|
+
'Edit the --color-* block in app/globals.css manually to reflect ' +
|
|
339
|
+
'brand.ts changes, or wait for the next scaffold-applier release.'
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Stub: upgrade an existing scaffold apply to a newer preset version.
|
|
345
|
+
* Not implemented in v1. Design exists: will reuse brownfield-upgrader's
|
|
346
|
+
* hash-diff logic, compare stored hashes in scaffold-manifest.json with
|
|
347
|
+
* current on-disk hashes, classify files as preserved (user-modified) vs
|
|
348
|
+
* upgradable (scaffold-owned), and apply the diff.
|
|
349
|
+
*
|
|
350
|
+
* @returns {never}
|
|
351
|
+
*/
|
|
352
|
+
export function upgradeScaffold() {
|
|
353
|
+
throw new Error(
|
|
354
|
+
'scaffold --upgrade is not yet implemented. The manifest written by ' +
|
|
355
|
+
'applyScaffold is forward-compatible — a future version will diff ' +
|
|
356
|
+
'stored hashes against current files and apply minimal updates.'
|
|
357
|
+
);
|
|
358
|
+
}
|