chati-dev 4.2.2 → 4.3.1

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.
Files changed (221) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +170 -5
  3. package/framework/agents/build/dev.md +509 -76
  4. package/framework/agents/deploy/devops.md +40 -25
  5. package/framework/agents/discover/brief.md +158 -22
  6. package/framework/agents/discover/brownfield-wu.md +26 -14
  7. package/framework/agents/discover/greenfield-wu.md +102 -14
  8. package/framework/agents/plan/architect-data-engineer.md +6 -6
  9. package/framework/agents/plan/architect-system.md +46 -12
  10. package/framework/agents/plan/architect.md +40 -20
  11. package/framework/agents/plan/detail.md +38 -24
  12. package/framework/agents/plan/phases.md +36 -26
  13. package/framework/agents/plan/tasks.md +114 -33
  14. package/framework/agents/plan/ux-brand-architect.md +240 -8
  15. package/framework/agents/plan/ux-component-engineer.md +28 -13
  16. package/framework/agents/plan/ux-researcher.md +7 -6
  17. package/framework/agents/plan/ux.md +55 -22
  18. package/framework/agents/quality/qa-implementation.md +143 -74
  19. package/framework/agents/quality/qa-planning.md +115 -42
  20. package/framework/agents/quality/qa-visual.md +448 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +7 -6
  23. package/framework/constitution.md +127 -44
  24. package/framework/context/governance.md +12 -7
  25. package/framework/context/quality.md +6 -5
  26. package/framework/context/root.md +6 -6
  27. package/framework/data/entity-registry.yaml +377 -4
  28. package/framework/data/model-limits.json +19 -0
  29. package/framework/domains/agents/qa-visual.yaml +74 -0
  30. package/framework/domains/constitution.yaml +46 -2
  31. package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
  32. package/framework/hooks/advance-trigger.js +131 -0
  33. package/framework/hooks/brief-validator.js +83 -0
  34. package/framework/hooks/constitution-guard.js +24 -5
  35. package/framework/hooks/license-guard.js +62 -27
  36. package/framework/hooks/mode-governance.js +13 -2
  37. package/framework/hooks/model-governance.js +1 -0
  38. package/framework/hooks/post-dev.js +207 -0
  39. package/framework/hooks/prism-engine.js +274 -105
  40. package/framework/hooks/reasoning-escalator.js +371 -0
  41. package/framework/hooks/reference-trigger.js +117 -0
  42. package/framework/hooks/session-digest.js +50 -1
  43. package/framework/hooks/settings.json +32 -1
  44. package/framework/hooks/style-guard.js +25 -6
  45. package/framework/hooks/team-quality-gate.js +19 -12
  46. package/framework/hooks/undercover-guard.js +4 -2
  47. package/framework/i18n/en.yaml +3 -3
  48. package/framework/i18n/es.yaml +3 -3
  49. package/framework/i18n/fr.yaml +3 -3
  50. package/framework/i18n/pt.yaml +3 -3
  51. package/framework/intelligence/context-engine.md +4 -5
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/intelligence/hooks-performance.md +3 -3
  54. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  55. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  56. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  57. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  58. package/framework/orchestrator/chati-router.js +441 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +295 -94
  61. package/framework/rules/human-writing-style.md +47 -0
  62. package/framework/scaffold/motion-premium/README.md +99 -0
  63. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  64. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  65. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  66. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  67. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  68. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  69. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  70. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  71. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  72. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  73. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  76. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  77. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  78. package/framework/scaffold/motion-premium/scaffold.yaml +178 -0
  79. package/framework/scaffold/motion-premium-3d/README.md +90 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  82. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  88. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  90. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  91. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  92. package/framework/scaffold/motion-premium-3d/scaffold.yaml +136 -0
  93. package/framework/schemas/session.schema.json +109 -21
  94. package/framework/scripts/reference-capture.js +430 -0
  95. package/framework/scripts/visual-qa.js +686 -0
  96. package/framework/tasks/orchestrator-handoff.md +1 -1
  97. package/framework/tasks/orchestrator-resume.md +1 -1
  98. package/framework/tasks/orchestrator-route.md +1 -1
  99. package/framework/tasks/orchestrator-status.md +3 -3
  100. package/framework/tasks/qa-planning-gate-define.md +1 -1
  101. package/framework/templates/brandbook-html-tmpl.md +2 -2
  102. package/framework/templates/brandbook-tmpl.yaml +1 -1
  103. package/framework/templates/component-spec-tmpl.yaml +1 -1
  104. package/framework/templates/design-token-tmpl.yaml +1 -1
  105. package/framework/templates/icon-system-tmpl.yaml +1 -1
  106. package/framework/templates/team-planning-tasks.yaml +6 -5
  107. package/framework/workflows/brownfield-discovery.yaml +2 -2
  108. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  109. package/framework/workflows/brownfield-service.yaml +14 -10
  110. package/framework/workflows/brownfield-ui.yaml +15 -11
  111. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  112. package/framework/workflows/quick-flow.yaml +3 -3
  113. package/framework/workflows/standard-flow.yaml +12 -9
  114. package/package.json +12 -6
  115. package/src/autonomy/autonomous-gate.js +1 -0
  116. package/src/autonomy/build-state.js +1 -2
  117. package/src/autonomy/progress-reporter.js +1 -1
  118. package/src/config/agent-customizer.js +11 -3
  119. package/src/config/claude-settings-generator.js +27 -7
  120. package/src/config/context-file-generator.js +41 -21
  121. package/src/config/framework-adapter.js +1 -0
  122. package/src/config/gemini-hooks-generator.js +19 -7
  123. package/src/config/mcp-configs.js +1 -0
  124. package/src/context/layers/l1-global.js +2 -1
  125. package/src/dashboard/data-reader.js +4 -3
  126. package/src/dashboard/layout.js +2 -1
  127. package/src/decision/analyzer.js +6 -30
  128. package/src/decision/engine.js +4 -28
  129. package/src/decision/registry-healer.js +3 -2
  130. package/src/decision/registry-updater.js +23 -14
  131. package/src/extensions/loader.js +2 -8
  132. package/src/gates/g1-planning-complete.js +2 -1
  133. package/src/gates/g2-qa-planning.js +2 -1
  134. package/src/gates/g3-implementation.js +2 -1
  135. package/src/gates/g4-qa-implementation.js +3 -2
  136. package/src/gates/g5-deploy-ready.js +2 -1
  137. package/src/health/engine.js +4 -3
  138. package/src/installer/core.js +431 -81
  139. package/src/installer/preflight.js +131 -0
  140. package/src/installer/provider-overlay.js +3 -3
  141. package/src/installer/scaffold-applier.js +358 -0
  142. package/src/installer/templates.js +46 -29
  143. package/src/installer/validator.js +17 -12
  144. package/src/intelligence/registry-manager.js +22 -29
  145. package/src/intelligence/timeline.js +11 -6
  146. package/src/license/commands.js +1 -1
  147. package/src/license/wait.js +102 -0
  148. package/src/memory/agent-memory.js +81 -0
  149. package/src/memory/dream.js +32 -1
  150. package/src/merger/replace-merger.js +28 -15
  151. package/src/orchestrator/agent-selector.js +2 -1
  152. package/src/orchestrator/cli.js +1880 -71
  153. package/src/orchestrator/doctor.js +270 -0
  154. package/src/orchestrator/handoff-engine.js +4 -3
  155. package/src/orchestrator/index.js +2 -0
  156. package/src/orchestrator/pipeline-manager.js +306 -15
  157. package/src/orchestrator/session-manager.js +339 -6
  158. package/src/tasks/handoff.js +3 -2
  159. package/src/telemetry/config.js +4 -3
  160. package/src/telemetry/schema.js +1 -0
  161. package/src/terminal/collector.js +3 -2
  162. package/src/terminal/index.js +1 -2
  163. package/src/terminal/isolation.js +52 -18
  164. package/src/terminal/prompt-builder.js +42 -25
  165. package/src/terminal/run-parallel.js +1 -1
  166. package/src/terminal/run-team.js +3 -3
  167. package/src/terminal/team-task-list.js +43 -4
  168. package/src/upgrade/backup.js +3 -2
  169. package/src/upgrade/checker.js +3 -2
  170. package/src/upgrade/migrator.js +65 -7
  171. package/src/upgrade/tracked-files-detector.js +86 -0
  172. package/src/upgrade/user-messages.js +94 -0
  173. package/src/utils/config-parser.js +2 -1
  174. package/src/utils/feature-flags.js +2 -1
  175. package/src/utils/flatten-entities.js +69 -0
  176. package/src/utils/framework-dir.js +16 -0
  177. package/src/utils/model-id.js +85 -0
  178. package/src/utils/provider-limits.js +84 -23
  179. package/src/utils/schema-validator.js +1 -1
  180. package/src/wizard/i18n.js +5 -4
  181. package/src/wizard/index.js +14 -0
  182. package/assets/logo - c/303/263pia.png +0 -0
  183. package/assets/logo.svg +0 -42
  184. package/assets/logo2.png +0 -0
  185. package/assets/social-preview.png +0 -0
  186. package/scripts/bundle-framework.js +0 -69
  187. package/scripts/changelog-generator.js +0 -222
  188. package/scripts/codebase-mapper.js +0 -728
  189. package/scripts/commit-message-generator.js +0 -167
  190. package/scripts/coverage-analyzer.js +0 -260
  191. package/scripts/dependency-analyzer.js +0 -280
  192. package/scripts/doctor/checks/agents.js +0 -77
  193. package/scripts/doctor/checks/constitution.js +0 -41
  194. package/scripts/doctor/checks/domain-alignment.js +0 -58
  195. package/scripts/doctor/checks/prism-layers.js +0 -84
  196. package/scripts/doctor/checks/registry.js +0 -55
  197. package/scripts/doctor/checks/schemas.js +0 -61
  198. package/scripts/doctor/fixes/reference-fix.js +0 -100
  199. package/scripts/doctor/fixes/registry-fix.js +0 -56
  200. package/scripts/doctor/index.js +0 -212
  201. package/scripts/framework-analyzer.js +0 -308
  202. package/scripts/generate-constitution-domain.js +0 -253
  203. package/scripts/generate-signing-key.js +0 -33
  204. package/scripts/health-check.js +0 -481
  205. package/scripts/ide-sync.js +0 -326
  206. package/scripts/performance-analyzer.js +0 -325
  207. package/scripts/plan-tracker.js +0 -278
  208. package/scripts/populate-entity-registry.js +0 -481
  209. package/scripts/pr-review.js +0 -317
  210. package/scripts/rollback-manager.js +0 -310
  211. package/scripts/semantic-lint.js +0 -328
  212. package/scripts/sign-manifest.js +0 -53
  213. package/scripts/stuck-detector.js +0 -343
  214. package/scripts/test-quality-assessment.js +0 -257
  215. package/scripts/validate-agents.js +0 -368
  216. package/scripts/validate-package.js +0 -505
  217. package/scripts/validate-tasks.js +0 -465
  218. package/src/autonomy/worktree-manager.js +0 -250
  219. package/src/intelligence/decision-engine.js +0 -256
  220. package/src/intelligence/document-sharder.js +0 -221
  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
+ }