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,430 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* reference-capture.js — Visual Reference Capture for UX Planning
|
|
4
|
+
*
|
|
5
|
+
* Uses Playwright to capture screenshots + extract real CSS tokens from
|
|
6
|
+
* reference websites. Outputs data the UX Brand Architect uses to base
|
|
7
|
+
* visual direction proposals on REAL data, not AI guesses.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node reference-capture.js --urls "https://oryzo.ai" "https://landonorris.com" --output artifacts/4-UX/references/
|
|
11
|
+
*
|
|
12
|
+
* Per-site output:
|
|
13
|
+
* <output>/<slug>/
|
|
14
|
+
* ├── full-page.png
|
|
15
|
+
* ├── scroll-{0,25,50,75,100}.png
|
|
16
|
+
* ├── hover-{0..4}.png
|
|
17
|
+
* ├── mobile.png, tablet.png
|
|
18
|
+
* └── extracted-tokens.json
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { mkdirSync, writeFileSync, readdirSync, renameSync, existsSync } from 'fs';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
import { execSync } from 'child_process';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Args
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
let urls = [];
|
|
30
|
+
let output = 'artifacts/4-UX/references';
|
|
31
|
+
const TIMEOUT_PER_SITE = 45000;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
if (args[i] === '--urls') {
|
|
35
|
+
while (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
36
|
+
urls.push(args[++i]);
|
|
37
|
+
}
|
|
38
|
+
} else if (args[i] === '--output') output = args[++i];
|
|
39
|
+
else if (args[i] === '--help') {
|
|
40
|
+
console.log(`Usage: node reference-capture.js [options]
|
|
41
|
+
--urls "https://site1.com" "https://site2.com" Reference URLs to analyze
|
|
42
|
+
--output artifacts/4-UX/references/ Output directory (default)
|
|
43
|
+
--help Show this help`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (urls.length === 0) {
|
|
49
|
+
console.error('Error: No URLs provided. Use --urls "https://example.com"');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// URL to slug
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
function urlToSlug(url) {
|
|
57
|
+
try {
|
|
58
|
+
const u = new URL(url);
|
|
59
|
+
return u.hostname.replace(/\./g, '-').replace(/^www-/, '');
|
|
60
|
+
} catch {
|
|
61
|
+
return url.replace(/[^a-z0-9]/gi, '-').slice(0, 40);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// CSS Token Extraction (runs inside the browser via page.evaluate)
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
const extractTokensScript = () => {
|
|
69
|
+
const tokens = {
|
|
70
|
+
colors: { backgrounds: {}, text: {}, all: {} },
|
|
71
|
+
typography: { families: {}, sizes: {}, weights: {} },
|
|
72
|
+
spacing: { sectionGaps: [], contentMaxWidth: null },
|
|
73
|
+
animations: {
|
|
74
|
+
gsap_loaded: typeof gsap !== 'undefined',
|
|
75
|
+
lenis_active: !!window.lenis,
|
|
76
|
+
scroll_trigger_count: 0,
|
|
77
|
+
transition_durations: {},
|
|
78
|
+
},
|
|
79
|
+
layout: { breakpoints: [], gridColumns: null },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Helper: normalize color to hex
|
|
83
|
+
function rgbToHex(rgb) {
|
|
84
|
+
if (!rgb || rgb === 'transparent' || rgb === 'rgba(0, 0, 0, 0)') return null;
|
|
85
|
+
const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
86
|
+
if (!match) return rgb;
|
|
87
|
+
return '#' + [match[1], match[2], match[3]].map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sample up to 500 visible elements for style data
|
|
91
|
+
const elements = document.querySelectorAll('body *');
|
|
92
|
+
const sample = Array.from(elements).slice(0, 500);
|
|
93
|
+
|
|
94
|
+
for (const el of sample) {
|
|
95
|
+
const style = getComputedStyle(el);
|
|
96
|
+
const rect = el.getBoundingClientRect();
|
|
97
|
+
if (rect.width === 0 && rect.height === 0) continue; // skip invisible
|
|
98
|
+
|
|
99
|
+
// Colors
|
|
100
|
+
const bg = rgbToHex(style.backgroundColor);
|
|
101
|
+
if (bg) tokens.colors.backgrounds[bg] = (tokens.colors.backgrounds[bg] || 0) + 1;
|
|
102
|
+
const color = rgbToHex(style.color);
|
|
103
|
+
if (color) tokens.colors.text[color] = (tokens.colors.text[color] || 0) + 1;
|
|
104
|
+
if (bg) tokens.colors.all[bg] = (tokens.colors.all[bg] || 0) + 1;
|
|
105
|
+
if (color) tokens.colors.all[color] = (tokens.colors.all[color] || 0) + 1;
|
|
106
|
+
|
|
107
|
+
// Typography (text elements only)
|
|
108
|
+
if (el.textContent.trim().length > 0 && ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SPAN', 'A', 'LI', 'TD', 'TH', 'LABEL', 'BUTTON'].includes(el.tagName)) {
|
|
109
|
+
const family = style.fontFamily.split(',')[0].trim().replace(/['"]/g, '');
|
|
110
|
+
tokens.typography.families[family] = (tokens.typography.families[family] || 0) + 1;
|
|
111
|
+
tokens.typography.sizes[style.fontSize] = (tokens.typography.sizes[style.fontSize] || 0) + 1;
|
|
112
|
+
tokens.typography.weights[style.fontWeight] = (tokens.typography.weights[style.fontWeight] || 0) + 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Transition durations
|
|
116
|
+
if (style.transitionDuration && style.transitionDuration !== '0s') {
|
|
117
|
+
tokens.animations.transition_durations[style.transitionDuration] =
|
|
118
|
+
(tokens.animations.transition_durations[style.transitionDuration] || 0) + 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Spacing: section-level gaps
|
|
123
|
+
const sections = document.querySelectorAll('section, [class*="section"], main > div');
|
|
124
|
+
for (const sec of Array.from(sections).slice(0, 20)) {
|
|
125
|
+
const style = getComputedStyle(sec);
|
|
126
|
+
const gap = parseInt(style.paddingTop) + parseInt(style.paddingBottom);
|
|
127
|
+
if (gap > 20) tokens.spacing.sectionGaps.push(`${gap}px`);
|
|
128
|
+
}
|
|
129
|
+
tokens.spacing.sectionGaps = [...new Set(tokens.spacing.sectionGaps)].sort((a, b) => parseInt(a) - parseInt(b));
|
|
130
|
+
|
|
131
|
+
// Content max-width
|
|
132
|
+
const main = document.querySelector('main') || document.querySelector('[class*="container"]');
|
|
133
|
+
if (main) tokens.spacing.contentMaxWidth = getComputedStyle(main).maxWidth;
|
|
134
|
+
|
|
135
|
+
// ScrollTrigger count
|
|
136
|
+
try {
|
|
137
|
+
if (typeof ScrollTrigger !== 'undefined') {
|
|
138
|
+
tokens.animations.scroll_trigger_count = ScrollTrigger.getAll().length;
|
|
139
|
+
}
|
|
140
|
+
} catch { /* not available */ }
|
|
141
|
+
|
|
142
|
+
// CSS custom properties count
|
|
143
|
+
const rootStyles = getComputedStyle(document.documentElement);
|
|
144
|
+
let customPropCount = 0;
|
|
145
|
+
for (let i = 0; i < rootStyles.length; i++) {
|
|
146
|
+
if (rootStyles[i].startsWith('--')) customPropCount++;
|
|
147
|
+
}
|
|
148
|
+
tokens.layout.customProperties = customPropCount;
|
|
149
|
+
|
|
150
|
+
// Sort colors/typography by frequency, keep top entries
|
|
151
|
+
function topN(obj, n = 10) {
|
|
152
|
+
return Object.entries(obj)
|
|
153
|
+
.sort((a, b) => b[1] - a[1])
|
|
154
|
+
.slice(0, n)
|
|
155
|
+
.map(([value, count]) => ({ value, count }));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
colors: {
|
|
160
|
+
backgrounds: topN(tokens.colors.backgrounds),
|
|
161
|
+
text: topN(tokens.colors.text),
|
|
162
|
+
all: topN(tokens.colors.all, 15),
|
|
163
|
+
},
|
|
164
|
+
typography: {
|
|
165
|
+
families: topN(tokens.typography.families, 5),
|
|
166
|
+
sizes: topN(tokens.typography.sizes, 10),
|
|
167
|
+
weights: topN(tokens.typography.weights, 5),
|
|
168
|
+
},
|
|
169
|
+
spacing: tokens.spacing,
|
|
170
|
+
animations: {
|
|
171
|
+
gsap_loaded: tokens.animations.gsap_loaded,
|
|
172
|
+
lenis_active: tokens.animations.lenis_active,
|
|
173
|
+
scroll_trigger_count: tokens.animations.scroll_trigger_count,
|
|
174
|
+
transition_durations: topN(tokens.animations.transition_durations, 5),
|
|
175
|
+
},
|
|
176
|
+
layout: {
|
|
177
|
+
customProperties: tokens.layout.customProperties,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Capture single site
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
async function captureSite(browser, url, siteOutput) {
|
|
186
|
+
mkdirSync(siteOutput, { recursive: true });
|
|
187
|
+
const exec = execSync;
|
|
188
|
+
|
|
189
|
+
const consoleErrors = [];
|
|
190
|
+
const VIEWPORTS = [
|
|
191
|
+
{ name: 'desktop', width: 1280, height: 800 },
|
|
192
|
+
{ name: 'tablet', width: 768, height: 1024 },
|
|
193
|
+
{ name: 'mobile', width: 375, height: 812 },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
// -------------------------------------------------------------------------
|
|
197
|
+
// Phase 1: Scroll videos — 3 viewports IN PARALLEL
|
|
198
|
+
// Each viewport: 5s load + up to 120s scroll = max 125s video
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
console.log(` Recording 3 viewports in parallel...`);
|
|
201
|
+
|
|
202
|
+
const scrollResults = await Promise.all(VIEWPORTS.map(async (vp) => {
|
|
203
|
+
const vpDir = join(siteOutput, vp.name);
|
|
204
|
+
mkdirSync(vpDir, { recursive: true });
|
|
205
|
+
const framesDir = join(vpDir, 'frames');
|
|
206
|
+
mkdirSync(framesDir, { recursive: true });
|
|
207
|
+
|
|
208
|
+
let ctx, page, scrollHeight = 0, maxScroll = 0, durationS = 0, frameCount = 0;
|
|
209
|
+
|
|
210
|
+
// Step 1: Open browser + navigate + scroll (video records automatically)
|
|
211
|
+
try {
|
|
212
|
+
ctx = await browser.newContext({
|
|
213
|
+
viewport: { width: vp.width, height: vp.height },
|
|
214
|
+
recordVideo: { dir: vpDir, size: { width: vp.width, height: vp.height } },
|
|
215
|
+
});
|
|
216
|
+
page = await ctx.newPage();
|
|
217
|
+
if (vp.name === 'desktop') {
|
|
218
|
+
page.on('console', msg => { if (msg.type() === 'error') consoleErrors.push(msg.text()); });
|
|
219
|
+
page.on('pageerror', err => consoleErrors.push(err.message));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: TIMEOUT_PER_SITE });
|
|
223
|
+
await page.waitForTimeout(5000); // 5s load — user looks at hero
|
|
224
|
+
|
|
225
|
+
scrollHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
226
|
+
maxScroll = Math.max(0, scrollHeight - vp.height);
|
|
227
|
+
|
|
228
|
+
const MAX_SCROLL_MS = 120_000;
|
|
229
|
+
const PREFERRED_SPEED = 30;
|
|
230
|
+
const durationMs = Math.min(MAX_SCROLL_MS, Math.round((maxScroll / PREFERRED_SPEED) * 1000));
|
|
231
|
+
durationS = Math.round(durationMs / 1000);
|
|
232
|
+
|
|
233
|
+
// Time-based smooth scroll
|
|
234
|
+
const scrollStart = Date.now();
|
|
235
|
+
while (Date.now() - scrollStart < durationMs) {
|
|
236
|
+
const elapsed = Date.now() - scrollStart;
|
|
237
|
+
const targetY = Math.floor(maxScroll * elapsed / durationMs);
|
|
238
|
+
await page.evaluate(y => window.scrollTo(0, y), targetY);
|
|
239
|
+
await page.waitForTimeout(16);
|
|
240
|
+
}
|
|
241
|
+
await page.evaluate(y => window.scrollTo(0, y), maxScroll);
|
|
242
|
+
await page.waitForTimeout(1500);
|
|
243
|
+
} catch (scrollErr) {
|
|
244
|
+
// Scroll crashed — video is still partially recorded
|
|
245
|
+
console.log(` ${vp.name}: scroll error (${scrollErr.message?.slice(0, 60)}) — extracting from partial video`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Step 2: Close page/context to FINALIZE video file (even after crash)
|
|
249
|
+
try { if (page) await page.close(); } catch { /* already closed */ }
|
|
250
|
+
try { if (ctx) await ctx.close(); } catch { /* already closed */ }
|
|
251
|
+
|
|
252
|
+
// Step 3: Rename ANY .webm found (works even if scroll crashed)
|
|
253
|
+
const videoPath = join(vpDir, `scroll-${vp.name}.webm`);
|
|
254
|
+
try {
|
|
255
|
+
const vids = readdirSync(vpDir).filter(f => f.endsWith('.webm') && !f.startsWith('scroll-'));
|
|
256
|
+
if (vids.length > 0) renameSync(join(vpDir, vids[0]), videoPath);
|
|
257
|
+
} catch { /* ignore */ }
|
|
258
|
+
|
|
259
|
+
// Step 4: Extract frames from video (works on partial videos too)
|
|
260
|
+
try {
|
|
261
|
+
const videoToExtract = existsSync(videoPath) ? videoPath : readdirSync(vpDir).find(f => f.endsWith('.webm'));
|
|
262
|
+
if (videoToExtract) {
|
|
263
|
+
const fullVideoPath = existsSync(videoPath) ? videoPath : join(vpDir, videoToExtract);
|
|
264
|
+
exec(`ffmpeg -i "${fullVideoPath}" -vf "fps=0.5" "${join(framesDir, 'frame-%03d.png')}" -y`, { stdio: 'ignore', timeout: 60000 });
|
|
265
|
+
frameCount = readdirSync(framesDir).filter(f => f.endsWith('.png')).length;
|
|
266
|
+
}
|
|
267
|
+
} catch { /* ffmpeg not available or video corrupt */ }
|
|
268
|
+
|
|
269
|
+
return { viewport: vp.name, scrollHeight, maxScroll, durationS, frameCount };
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
for (const r of scrollResults) {
|
|
273
|
+
console.log(` ${r.viewport}: ${r.maxScroll}px in ${r.durationS}s, ${r.frameCount} frames`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// -------------------------------------------------------------------------
|
|
277
|
+
// Phase 2: Static captures (desktop only, no video recording)
|
|
278
|
+
// Full-page screenshot + hover states
|
|
279
|
+
// -------------------------------------------------------------------------
|
|
280
|
+
const ctx2 = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
281
|
+
const page2 = await ctx2.newPage();
|
|
282
|
+
await page2.goto(url, { waitUntil: 'networkidle', timeout: TIMEOUT_PER_SITE });
|
|
283
|
+
await page2.waitForTimeout(2000);
|
|
284
|
+
|
|
285
|
+
// Full-page screenshot
|
|
286
|
+
await page2.screenshot({ path: join(siteOutput, 'desktop', 'full-page.png'), fullPage: true });
|
|
287
|
+
|
|
288
|
+
// Scroll through once to trigger animations for token extraction
|
|
289
|
+
const scrollHeight = await page2.evaluate(() => document.documentElement.scrollHeight);
|
|
290
|
+
const maxScroll = Math.max(0, scrollHeight - 800);
|
|
291
|
+
const quickScrollStart = Date.now();
|
|
292
|
+
while (Date.now() - quickScrollStart < 10000) {
|
|
293
|
+
const elapsed = Date.now() - quickScrollStart;
|
|
294
|
+
await page2.evaluate(y => window.scrollTo(0, y), Math.floor(maxScroll * elapsed / 10000));
|
|
295
|
+
await page2.waitForTimeout(16);
|
|
296
|
+
}
|
|
297
|
+
await page2.evaluate(y => window.scrollTo(0, y), maxScroll);
|
|
298
|
+
await page2.waitForTimeout(1000);
|
|
299
|
+
|
|
300
|
+
// Hover states
|
|
301
|
+
await page2.evaluate(() => window.scrollTo(0, 0));
|
|
302
|
+
await page2.waitForTimeout(500);
|
|
303
|
+
const interactive = await page2.locator('button:visible, a[href]:visible, [role="button"]:visible').all();
|
|
304
|
+
for (let i = 0; i < Math.min(interactive.length, 5); i++) {
|
|
305
|
+
try {
|
|
306
|
+
await interactive[i].hover({ timeout: 3000 });
|
|
307
|
+
await page2.waitForTimeout(500);
|
|
308
|
+
await page2.screenshot({ path: join(siteOutput, 'desktop', `hover-${i}.png`) });
|
|
309
|
+
} catch { /* element may be obscured */ }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Extract CSS tokens after full scroll
|
|
313
|
+
const tokens = await page2.evaluate(extractTokensScript);
|
|
314
|
+
tokens.url = url;
|
|
315
|
+
tokens.capturedAt = new Date().toISOString();
|
|
316
|
+
tokens.consoleErrors = consoleErrors;
|
|
317
|
+
tokens.scroll = {
|
|
318
|
+
height: scrollHeight,
|
|
319
|
+
viewports: scrollResults.map(r => ({ name: r.viewport, maxScroll: r.maxScroll, duration: r.durationS, frames: r.frameCount })),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
writeFileSync(join(siteOutput, 'extracted-tokens.json'), JSON.stringify(tokens, null, 2));
|
|
323
|
+
await page2.close();
|
|
324
|
+
await ctx2.close();
|
|
325
|
+
|
|
326
|
+
return tokens;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Main
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
async function main() {
|
|
333
|
+
mkdirSync(output, { recursive: true });
|
|
334
|
+
|
|
335
|
+
let chromium;
|
|
336
|
+
// Try multiple resolution paths for playwright:
|
|
337
|
+
// 1. Project node_modules (normal npm install)
|
|
338
|
+
// 2. Bundled _cli/node_modules (chati.dev self-contained install)
|
|
339
|
+
// 3. NODE_PATH (set by handleAdvance)
|
|
340
|
+
const pwCandidates = [
|
|
341
|
+
() => import('playwright'),
|
|
342
|
+
() => import(join('..', '_cli', 'node_modules', 'playwright', 'index.mjs')),
|
|
343
|
+
];
|
|
344
|
+
for (const tryImport of pwCandidates) {
|
|
345
|
+
try {
|
|
346
|
+
const pw = await tryImport();
|
|
347
|
+
chromium = pw.chromium || pw.default?.chromium;
|
|
348
|
+
if (chromium) break;
|
|
349
|
+
} catch { /* try next */ }
|
|
350
|
+
}
|
|
351
|
+
if (!chromium) {
|
|
352
|
+
console.error('Error: Playwright not installed. Run: npx playwright install chromium');
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const browser = await chromium.launch({ headless: true });
|
|
357
|
+
const results = [];
|
|
358
|
+
|
|
359
|
+
for (const url of urls) {
|
|
360
|
+
const slug = urlToSlug(url);
|
|
361
|
+
const siteOutput = join(output, slug);
|
|
362
|
+
console.log(`\nCapturing: ${url} → ${slug}/`);
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const tokens = await captureSite(browser, url, siteOutput);
|
|
366
|
+
console.log(` Colors: ${tokens.colors.all.length} unique`);
|
|
367
|
+
console.log(` Fonts: ${tokens.typography.families.map(f => f.value).join(', ')}`);
|
|
368
|
+
console.log(` GSAP: ${tokens.animations.gsap_loaded}, Lenis: ${tokens.animations.lenis_active}`);
|
|
369
|
+
console.log(` Screenshots: full-page + 5 scroll + 5 hover + tablet + mobile`);
|
|
370
|
+
results.push({ url, slug, status: 'ok', tokens });
|
|
371
|
+
} catch (err) {
|
|
372
|
+
console.error(` FAILED: ${err.message}`);
|
|
373
|
+
results.push({ url, slug, status: 'error', error: err.message });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await browser.close();
|
|
378
|
+
|
|
379
|
+
// Summary + completion marker
|
|
380
|
+
// capture-complete.json is the single file that Brand Architect polls for.
|
|
381
|
+
// Written ONLY when ALL sites finished (success or failure).
|
|
382
|
+
const siteResults = {};
|
|
383
|
+
for (const r of results) {
|
|
384
|
+
const framesDesktop = join(output, r.slug, 'desktop', 'frames');
|
|
385
|
+
const framesMobile = join(output, r.slug, 'mobile', 'frames');
|
|
386
|
+
const desktopCount = existsSync(framesDesktop) ? readdirSync(framesDesktop).filter(f => f.endsWith('.png')).length : 0;
|
|
387
|
+
const mobileCount = existsSync(framesMobile) ? readdirSync(framesMobile).filter(f => f.endsWith('.png')).length : 0;
|
|
388
|
+
siteResults[r.slug] = {
|
|
389
|
+
url: r.url,
|
|
390
|
+
status: r.status,
|
|
391
|
+
error: r.error || null,
|
|
392
|
+
frames: { desktop: desktopCount, mobile: mobileCount },
|
|
393
|
+
hasTokens: existsSync(join(output, r.slug, 'extracted-tokens.json')),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const captureComplete = {
|
|
398
|
+
completed: true,
|
|
399
|
+
completedAt: new Date().toISOString(),
|
|
400
|
+
totalSites: results.length,
|
|
401
|
+
totalCaptured: results.filter(r => r.status === 'ok').length,
|
|
402
|
+
totalFailed: results.filter(r => r.status === 'error').length,
|
|
403
|
+
sites: siteResults,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
writeFileSync(join(output, 'capture-complete.json'), JSON.stringify(captureComplete, null, 2));
|
|
407
|
+
writeFileSync(join(output, 'capture-summary.json'), JSON.stringify(captureComplete, null, 2));
|
|
408
|
+
console.log(`\nSummary: ${captureComplete.totalCaptured} captured, ${captureComplete.totalFailed} failed`);
|
|
409
|
+
console.log(`Output: ${output}/`);
|
|
410
|
+
|
|
411
|
+
process.exit(captureComplete.totalFailed > 0 && captureComplete.totalCaptured === 0 ? 1 : 0);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
main().catch(err => {
|
|
415
|
+
// Always write capture-complete.json even on crash — so wait-for-capture
|
|
416
|
+
// doesn't hang forever waiting for a file that will never appear.
|
|
417
|
+
try {
|
|
418
|
+
mkdirSync(output, { recursive: true });
|
|
419
|
+
writeFileSync(join(output, 'capture-complete.json'), JSON.stringify({
|
|
420
|
+
completed: true,
|
|
421
|
+
completedAt: new Date().toISOString(),
|
|
422
|
+
totalSites: 0,
|
|
423
|
+
totalCaptured: 0,
|
|
424
|
+
totalFailed: 1,
|
|
425
|
+
sites: {},
|
|
426
|
+
error: err.message?.slice(0, 200) || 'Unknown crash',
|
|
427
|
+
}, null, 2));
|
|
428
|
+
} catch { /* last resort — nothing we can do */ }
|
|
429
|
+
process.exit(1);
|
|
430
|
+
});
|