tryassay 0.21.2 → 0.22.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 (132) hide show
  1. package/demo/.claude/.truth_last_prompt +1 -0
  2. package/demo/.claude/truth_status +1 -0
  3. package/demo/css/style.css +840 -0
  4. package/demo/index.html +78 -0
  5. package/demo/js/chat.js +535 -0
  6. package/demo/js/code-panel.js +206 -0
  7. package/demo/js/preview.js +456 -0
  8. package/demo/js/sse-client.js +600 -0
  9. package/demo/js/state.js +172 -0
  10. package/demo/js/timeline.js +80 -0
  11. package/dist/api/server.d.ts +3 -0
  12. package/dist/api/server.js +127 -20
  13. package/dist/api/server.js.map +1 -1
  14. package/dist/cli.js +11 -0
  15. package/dist/cli.js.map +1 -1
  16. package/dist/commands/assess.d.ts +2 -0
  17. package/dist/commands/assess.js +132 -164
  18. package/dist/commands/assess.js.map +1 -1
  19. package/dist/commands/demo.d.ts +5 -0
  20. package/dist/commands/demo.js +357 -0
  21. package/dist/commands/demo.js.map +1 -0
  22. package/dist/lib/__tests__/arithmetic-quick-test.d.ts +6 -0
  23. package/dist/lib/__tests__/arithmetic-quick-test.js +197 -0
  24. package/dist/lib/__tests__/arithmetic-quick-test.js.map +1 -0
  25. package/dist/lib/__tests__/arithmetic-real-llm-test.d.ts +13 -0
  26. package/dist/lib/__tests__/arithmetic-real-llm-test.js +284 -0
  27. package/dist/lib/__tests__/arithmetic-real-llm-test.js.map +1 -0
  28. package/dist/lib/__tests__/arithmetic-value-demo.d.ts +10 -0
  29. package/dist/lib/__tests__/arithmetic-value-demo.js +193 -0
  30. package/dist/lib/__tests__/arithmetic-value-demo.js.map +1 -0
  31. package/dist/lib/__tests__/flow-to-claims.test.d.ts +1 -0
  32. package/dist/lib/__tests__/flow-to-claims.test.js +91 -0
  33. package/dist/lib/__tests__/flow-to-claims.test.js.map +1 -0
  34. package/dist/lib/__tests__/formal-verifier-api-misuse.test.d.ts +9 -0
  35. package/dist/lib/__tests__/formal-verifier-api-misuse.test.js +391 -0
  36. package/dist/lib/__tests__/formal-verifier-api-misuse.test.js.map +1 -0
  37. package/dist/lib/__tests__/formal-verifier-arithmetic.test.d.ts +7 -0
  38. package/dist/lib/__tests__/formal-verifier-arithmetic.test.js +318 -0
  39. package/dist/lib/__tests__/formal-verifier-arithmetic.test.js.map +1 -0
  40. package/dist/lib/__tests__/intent-extractor.test.d.ts +1 -0
  41. package/dist/lib/__tests__/intent-extractor.test.js +97 -0
  42. package/dist/lib/__tests__/intent-extractor.test.js.map +1 -0
  43. package/dist/lib/__tests__/intent-reviewer.test.d.ts +1 -0
  44. package/dist/lib/__tests__/intent-reviewer.test.js +55 -0
  45. package/dist/lib/__tests__/intent-reviewer.test.js.map +1 -0
  46. package/dist/lib/__tests__/mr-gsm8k-benchmark.d.ts +11 -0
  47. package/dist/lib/__tests__/mr-gsm8k-benchmark.js +224 -0
  48. package/dist/lib/__tests__/mr-gsm8k-benchmark.js.map +1 -0
  49. package/dist/lib/anthropic.js +25 -33
  50. package/dist/lib/anthropic.js.map +1 -1
  51. package/dist/lib/assessment-reporter.js +9 -13
  52. package/dist/lib/assessment-reporter.js.map +1 -1
  53. package/dist/lib/claim-extractor.js +10 -19
  54. package/dist/lib/claim-extractor.js.map +1 -1
  55. package/dist/lib/code-verifier.js +16 -36
  56. package/dist/lib/code-verifier.js.map +1 -1
  57. package/dist/lib/constraint-engine.js +10 -19
  58. package/dist/lib/constraint-engine.js.map +1 -1
  59. package/dist/lib/formal-verifier.d.ts +1 -1
  60. package/dist/lib/formal-verifier.js +454 -0
  61. package/dist/lib/formal-verifier.js.map +1 -1
  62. package/dist/lib/guided-generator.js +19 -37
  63. package/dist/lib/guided-generator.js.map +1 -1
  64. package/dist/lib/intent-extractor.d.ts +47 -0
  65. package/dist/lib/intent-extractor.js +427 -0
  66. package/dist/lib/intent-extractor.js.map +1 -0
  67. package/dist/lib/intent-reviewer.d.ts +14 -0
  68. package/dist/lib/intent-reviewer.js +148 -0
  69. package/dist/lib/intent-reviewer.js.map +1 -0
  70. package/dist/lib/intent-types.d.ts +89 -0
  71. package/dist/lib/intent-types.js +5 -0
  72. package/dist/lib/intent-types.js.map +1 -0
  73. package/dist/lib/inventory-extractor.js +9 -22
  74. package/dist/lib/inventory-extractor.js.map +1 -1
  75. package/dist/lib/llm-provider.d.ts +23 -0
  76. package/dist/lib/llm-provider.js +130 -0
  77. package/dist/lib/llm-provider.js.map +1 -0
  78. package/dist/lib/remediator.js +20 -28
  79. package/dist/lib/remediator.js.map +1 -1
  80. package/dist/lib/requirements-generator.js +14 -19
  81. package/dist/lib/requirements-generator.js.map +1 -1
  82. package/dist/lib/spec-synthesizer.js +10 -19
  83. package/dist/lib/spec-synthesizer.js.map +1 -1
  84. package/dist/runtime/agents/planner-agent.d.ts +5 -2
  85. package/dist/runtime/agents/planner-agent.js +232 -1
  86. package/dist/runtime/agents/planner-agent.js.map +1 -1
  87. package/dist/runtime/app-create-orchestrator.d.ts +9 -1
  88. package/dist/runtime/app-create-orchestrator.js +265 -87
  89. package/dist/runtime/app-create-orchestrator.js.map +1 -1
  90. package/dist/runtime/check-catalog.js +5 -3
  91. package/dist/runtime/check-catalog.js.map +1 -1
  92. package/dist/runtime/check-definitions.d.ts +10 -0
  93. package/dist/runtime/check-definitions.js +52 -2
  94. package/dist/runtime/check-definitions.js.map +1 -1
  95. package/dist/runtime/composition-verifier.js +8 -12
  96. package/dist/runtime/composition-verifier.js.map +1 -1
  97. package/dist/runtime/gap-detector.js +8 -10
  98. package/dist/runtime/gap-detector.js.map +1 -1
  99. package/dist/runtime/input-validator.d.ts +7 -0
  100. package/dist/runtime/input-validator.js +162 -0
  101. package/dist/runtime/input-validator.js.map +1 -0
  102. package/dist/runtime/model-router.d.ts +10 -0
  103. package/dist/runtime/model-router.js +42 -0
  104. package/dist/runtime/model-router.js.map +1 -0
  105. package/dist/runtime/pattern-extractor.js +8 -10
  106. package/dist/runtime/pattern-extractor.js.map +1 -1
  107. package/dist/runtime/planner.js +11 -16
  108. package/dist/runtime/planner.js.map +1 -1
  109. package/dist/runtime/prompt-guard.d.ts +2 -0
  110. package/dist/runtime/prompt-guard.js +180 -0
  111. package/dist/runtime/prompt-guard.js.map +1 -0
  112. package/dist/runtime/prompt-safety-analyzer.js +8 -13
  113. package/dist/runtime/prompt-safety-analyzer.js.map +1 -1
  114. package/dist/runtime/reasoner.js +19 -33
  115. package/dist/runtime/reasoner.js.map +1 -1
  116. package/dist/runtime/rule-meta-verifier.js +9 -11
  117. package/dist/runtime/rule-meta-verifier.js.map +1 -1
  118. package/dist/runtime/safe-executor.d.ts +23 -0
  119. package/dist/runtime/safe-executor.js +151 -0
  120. package/dist/runtime/safe-executor.js.map +1 -0
  121. package/dist/runtime/specialized-agent.js +10 -14
  122. package/dist/runtime/specialized-agent.js.map +1 -1
  123. package/dist/runtime/strategy-library.js +8 -10
  124. package/dist/runtime/strategy-library.js.map +1 -1
  125. package/dist/runtime/supabase-experience-store.js.map +1 -1
  126. package/dist/runtime/supabase-provisioner.d.ts +35 -0
  127. package/dist/runtime/supabase-provisioner.js +192 -0
  128. package/dist/runtime/supabase-provisioner.js.map +1 -0
  129. package/dist/runtime/types.d.ts +116 -0
  130. package/dist/sdk/forward-verify.js +16 -33
  131. package/dist/sdk/forward-verify.js.map +1 -1
  132. package/package.json +2 -1
@@ -0,0 +1,206 @@
1
+ // code-panel.js — Bottom streaming code display with typewriter effect
2
+ // Subscribes to AppState code_generated events, renders tabs + syntax-highlighted code
3
+
4
+ const files = []; // { path, code }
5
+ let activeIndex = -1;
6
+ let typewriterRaf = 0; // requestAnimationFrame ID
7
+
8
+ const KEYWORDS = new Set([
9
+ 'const', 'let', 'var', 'function', 'return', 'import', 'export', 'from',
10
+ 'if', 'else', 'async', 'await', 'class', 'extends', 'new', 'this',
11
+ 'throw', 'try', 'catch', 'typeof', 'interface', 'type'
12
+ ]);
13
+
14
+ const BUILDING_PHASES = new Set(['building_feature', 'scaffolding']);
15
+
16
+ function highlightSyntax(code) {
17
+ let result = '';
18
+ let i = 0;
19
+ const len = code.length;
20
+
21
+ while (i < len) {
22
+ // Comments: // to end of line
23
+ if (code[i] === '/' && code[i + 1] === '/') {
24
+ const end = code.indexOf('\n', i);
25
+ const slice = end === -1 ? code.slice(i) : code.slice(i, end);
26
+ result += `<span class="comment">${escapeHtml(slice)}</span>`;
27
+ i += slice.length;
28
+ continue;
29
+ }
30
+
31
+ // Strings: single or double quoted
32
+ if (code[i] === '"' || code[i] === "'") {
33
+ const quote = code[i];
34
+ let j = i + 1;
35
+ while (j < len && code[j] !== quote) {
36
+ if (code[j] === '\\') j++; // skip escaped chars
37
+ j++;
38
+ }
39
+ j++; // include closing quote
40
+ result += `<span class="string">${escapeHtml(code.slice(i, j))}</span>`;
41
+ i = j;
42
+ continue;
43
+ }
44
+
45
+ // Template literals
46
+ if (code[i] === '`') {
47
+ let j = i + 1;
48
+ while (j < len && code[j] !== '`') {
49
+ if (code[j] === '\\') j++;
50
+ j++;
51
+ }
52
+ j++;
53
+ result += `<span class="string">${escapeHtml(code.slice(i, j))}</span>`;
54
+ i = j;
55
+ continue;
56
+ }
57
+
58
+ // Numbers
59
+ if (/\d/.test(code[i]) && (i === 0 || /[\s,;:=+\-*/([\]{}<>!&|^~?]/.test(code[i - 1]))) {
60
+ let j = i;
61
+ while (j < len && /[\d.xXa-fA-F_]/.test(code[j])) j++;
62
+ result += `<span class="number">${escapeHtml(code.slice(i, j))}</span>`;
63
+ i = j;
64
+ continue;
65
+ }
66
+
67
+ // Words (potential keywords)
68
+ if (/[a-zA-Z_$]/.test(code[i])) {
69
+ let j = i;
70
+ while (j < len && /[a-zA-Z0-9_$]/.test(code[j])) j++;
71
+ const word = code.slice(i, j);
72
+ if (KEYWORDS.has(word)) {
73
+ result += `<span class="keyword">${word}</span>`;
74
+ } else {
75
+ result += escapeHtml(word);
76
+ }
77
+ i = j;
78
+ continue;
79
+ }
80
+
81
+ // Everything else
82
+ result += escapeHtml(code[i]);
83
+ i++;
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ function escapeHtml(str) {
90
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
91
+ }
92
+
93
+ function renderTabs() {
94
+ const tabsEl = document.getElementById('code-tabs');
95
+ if (!tabsEl) return;
96
+
97
+ tabsEl.innerHTML = '';
98
+
99
+ for (let i = 0; i < files.length; i++) {
100
+ const btn = document.createElement('button');
101
+ btn.className = 'code-tab';
102
+ if (i === activeIndex) btn.classList.add('active');
103
+ // Show just the filename, not the full path
104
+ const name = files[i].path.split('/').pop();
105
+ btn.textContent = name;
106
+ btn.addEventListener('click', () => showFile(i));
107
+ tabsEl.appendChild(btn);
108
+ }
109
+ }
110
+
111
+ function showFile(index) {
112
+ if (index < 0 || index >= files.length) return;
113
+ activeIndex = index;
114
+ renderTabs();
115
+ startTypewriter(files[index].code);
116
+ }
117
+
118
+ function startTypewriter(code) {
119
+ const contentEl = document.getElementById('code-content');
120
+ if (!contentEl) return;
121
+
122
+ // Cancel any ongoing typewriter
123
+ if (typewriterRaf) cancelAnimationFrame(typewriterRaf);
124
+
125
+ const highlighted = highlightSyntax(code);
126
+ let charIndex = 0;
127
+ const charsPerFrame = 30;
128
+
129
+ contentEl.innerHTML = '';
130
+ contentEl.classList.add('cursor-blink');
131
+
132
+ function tick() {
133
+ charIndex += charsPerFrame;
134
+ if (charIndex >= highlighted.length) {
135
+ contentEl.innerHTML = highlighted;
136
+ contentEl.classList.remove('cursor-blink');
137
+ typewriterRaf = 0;
138
+ return;
139
+ }
140
+ // Render up to charIndex, but don't break inside HTML tags
141
+ let safeEnd = charIndex;
142
+ const openBracket = highlighted.lastIndexOf('<', safeEnd);
143
+ const closeBracket = highlighted.lastIndexOf('>', safeEnd);
144
+ if (openBracket > closeBracket) {
145
+ // We're inside a tag — extend to close it
146
+ const nextClose = highlighted.indexOf('>', safeEnd);
147
+ if (nextClose !== -1) safeEnd = nextClose + 1;
148
+ }
149
+ contentEl.innerHTML = highlighted.slice(0, safeEnd);
150
+ typewriterRaf = requestAnimationFrame(tick);
151
+ }
152
+
153
+ typewriterRaf = requestAnimationFrame(tick);
154
+ }
155
+
156
+ function updatePanelVisibility(phase) {
157
+ const panel = document.getElementById('code-panel');
158
+ if (!panel) return;
159
+ if (BUILDING_PHASES.has(phase)) {
160
+ panel.classList.add('active');
161
+ } else {
162
+ panel.classList.remove('active');
163
+ }
164
+ }
165
+
166
+ export function initCodePanel() {
167
+ const state = window.AppState;
168
+ if (!state) return;
169
+
170
+ state.on('code_generated', (file) => {
171
+ if (!file || !file.path) return;
172
+ const existing = files.findIndex(f => f.path === file.path);
173
+ const normalized = { path: file.path, code: file.content || file.code || '' };
174
+ if (existing >= 0) {
175
+ files[existing] = normalized;
176
+ if (existing === activeIndex) {
177
+ startTypewriter(normalized.code);
178
+ }
179
+ } else {
180
+ files.push(normalized);
181
+ // Auto-select newly added file
182
+ showFile(files.length - 1);
183
+ }
184
+ renderTabs();
185
+ });
186
+
187
+ state.on('phase_change', ({ to }) => {
188
+ updatePanelVisibility(to);
189
+ });
190
+
191
+ state.on('reset', () => {
192
+ files.length = 0;
193
+ activeIndex = -1;
194
+ if (typewriterRaf) cancelAnimationFrame(typewriterRaf);
195
+ typewriterRaf = 0;
196
+ const tabsEl = document.getElementById('code-tabs');
197
+ const contentEl = document.getElementById('code-content');
198
+ if (tabsEl) tabsEl.innerHTML = '';
199
+ if (contentEl) {
200
+ contentEl.innerHTML = '';
201
+ contentEl.classList.remove('cursor-blink');
202
+ }
203
+ const panel = document.getElementById('code-panel');
204
+ if (panel) panel.classList.remove('active');
205
+ });
206
+ }
@@ -0,0 +1,456 @@
1
+ // preview.js — WebContainer boot, file sync, iframe management
2
+ // Boots WebContainer on page load, pre-warms with Next.js scaffold,
3
+ // then syncs files from code_generated SSE events via HMR.
4
+
5
+ import { WebContainer } from 'https://esm.sh/@webcontainer/api';
6
+
7
+ const AppState = window.AppState;
8
+
9
+ let container = null;
10
+ let serverProcess = null;
11
+ let isServerReady = false;
12
+
13
+ // Track installed packages to avoid redundant installs
14
+ const installedPackages = new Set(['next', 'react', 'react-dom', '@types/node', '@types/react', 'typescript', 'tailwindcss', 'postcss', 'autoprefixer']);
15
+
16
+ // Static scaffold — same every time
17
+ const SCAFFOLD = {
18
+ 'package.json': {
19
+ file: {
20
+ contents: JSON.stringify({
21
+ name: 'assay-preview',
22
+ version: '0.1.0',
23
+ private: true,
24
+ scripts: {
25
+ dev: 'next dev --port 3333'
26
+ },
27
+ dependencies: {
28
+ next: '14.2.21',
29
+ react: '^18.3.0',
30
+ 'react-dom': '^18.3.0'
31
+ },
32
+ devDependencies: {
33
+ '@types/node': '^20.0.0',
34
+ '@types/react': '^18.3.0',
35
+ typescript: '^5.7.0',
36
+ tailwindcss: '^3.4.0',
37
+ postcss: '^8.4.0',
38
+ autoprefixer: '^10.4.0'
39
+ }
40
+ }, null, 2)
41
+ }
42
+ },
43
+ 'next.config.js': {
44
+ file: {
45
+ contents: `/** @type {import('next').NextConfig} */\nconst nextConfig = {};\nmodule.exports = nextConfig;\n`
46
+ }
47
+ },
48
+ 'tsconfig.json': {
49
+ file: {
50
+ contents: JSON.stringify({
51
+ compilerOptions: {
52
+ target: 'ES2017',
53
+ lib: ['dom', 'dom.iterable', 'esnext'],
54
+ allowJs: true,
55
+ skipLibCheck: true,
56
+ strict: true,
57
+ noEmit: true,
58
+ esModuleInterop: true,
59
+ module: 'esnext',
60
+ moduleResolution: 'bundler',
61
+ resolveJsonModule: true,
62
+ isolatedModules: true,
63
+ jsx: 'preserve',
64
+ incremental: true,
65
+ plugins: [{ name: 'next' }],
66
+ paths: { '@/*': ['./*'] }
67
+ },
68
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
69
+ exclude: ['node_modules']
70
+ }, null, 2)
71
+ }
72
+ },
73
+ 'tailwind.config.js': {
74
+ file: {
75
+ contents: `/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n content: ['./app/**/*.{js,ts,jsx,tsx}'],\n theme: { extend: {} },\n plugins: [],\n};\n`
76
+ }
77
+ },
78
+ 'postcss.config.js': {
79
+ file: {
80
+ contents: `module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n};\n`
81
+ }
82
+ },
83
+ 'app': {
84
+ directory: {
85
+ 'layout.tsx': {
86
+ file: {
87
+ contents: `import type { Metadata } from 'next';\nimport './globals.css';\n\nexport const metadata: Metadata = { title: 'Preview' };\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n <html lang="en">\n <body>{children}</body>\n </html>\n );\n}\n`
88
+ }
89
+ },
90
+ 'globals.css': {
91
+ file: {
92
+ contents: `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`
93
+ }
94
+ },
95
+ 'page.tsx': {
96
+ file: {
97
+ contents: `'use client';\n\nexport default function Home() {\n return (\n <div className="flex items-center justify-center min-h-screen bg-gray-950 text-white">\n <p className="text-gray-500 text-sm">Waiting for files...</p>\n </div>\n );\n}\n`
98
+ }
99
+ }
100
+ }
101
+ }
102
+ };
103
+
104
+ /**
105
+ * Boot WebContainer, mount scaffold, install deps, start dev server.
106
+ * Call on page load — runs silently in background during chat phase.
107
+ */
108
+ export async function initPreview() {
109
+ const iframe = document.getElementById('preview-iframe');
110
+ updatePreviewStatus('cold', 'Setting up environment...');
111
+ AppState.update({ containerState: 'cold' });
112
+
113
+ try {
114
+ container = await WebContainer.boot();
115
+ await container.mount(SCAFFOLD);
116
+
117
+ updatePreviewStatus('cold', 'Installing dependencies...');
118
+ const installProcess = await container.spawn('npm', ['install']);
119
+ const installCode = await installProcess.exit;
120
+
121
+ if (installCode !== 0) {
122
+ console.error('[Preview] npm install failed with code', installCode);
123
+ updatePreviewStatus('cold', 'Install failed — retrying...');
124
+ const retry = await container.spawn('npm', ['install']);
125
+ await retry.exit;
126
+ }
127
+
128
+ updatePreviewStatus('cold', 'Starting dev server...');
129
+ serverProcess = await container.spawn('npm', ['run', 'dev']);
130
+
131
+ serverProcess.output.pipeTo(new WritableStream({
132
+ write(data) {
133
+ console.log('[Preview:server]', data);
134
+ }
135
+ }));
136
+
137
+ container.on('server-ready', (port, url) => {
138
+ console.log('[Preview] Server ready on port', port, 'at', url);
139
+ isServerReady = true;
140
+ AppState.update({ containerState: 'warm', previewUrl: url });
141
+
142
+ if (iframe) {
143
+ iframe.src = url;
144
+ }
145
+
146
+ updatePreviewStatus('warm', 'Describe your app and watch it appear here.');
147
+
148
+ const queue = AppState.get().fileQueue;
149
+ if (queue.length > 0) {
150
+ console.log(`[Preview] Flushing ${queue.length} queued files`);
151
+ AppState.update({ containerState: 'building', fileQueue: [] });
152
+ for (const file of queue) {
153
+ collectFileForInstall(file);
154
+ }
155
+ }
156
+ });
157
+
158
+ AppState.on('code_generated', (file) => {
159
+ handleFileGenerated(file);
160
+ });
161
+
162
+ AppState.on('phase_change', ({ to }) => {
163
+ if (to === 'verifying' || to === 'verification') {
164
+ AppState.update({ containerState: 'verifying' });
165
+ updatePreviewStatus('verifying', `Verifying... ${AppState.get().metrics.claimsVerified} claims checked`);
166
+ } else if (to === 'completed') {
167
+ AppState.update({ containerState: 'done' });
168
+ const m = AppState.get().metrics;
169
+ const clockEl = document.getElementById('clock-value');
170
+ const elapsed = clockEl ? clockEl.textContent : '';
171
+ updatePreviewStatus('done', `${m.filesGenerated} files | ${m.totalTasks} features | ${elapsed}`);
172
+ showDownloadButton();
173
+ } else if (to === 'failed') {
174
+ updatePreviewStatus('done', 'Build failed');
175
+ }
176
+ });
177
+
178
+ } catch (err) {
179
+ console.error('[Preview] Boot failed:', err);
180
+ updatePreviewStatus('cold', `Boot failed: ${err.message}`);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Extract external package names from import/require statements.
186
+ * Ignores relative imports (./ ../), path aliases (@/), and built-ins.
187
+ */
188
+ function extractDependencies(content) {
189
+ const deps = new Set();
190
+ const importRe = /(?:import\s+.*?\s+from\s+|import\s+|require\s*\(\s*)['"]([^'"./][^'"]*)['"]/g;
191
+ let match;
192
+ while ((match = importRe.exec(content)) !== null) {
193
+ let pkg = match[1];
194
+ // Skip path aliases like @/lib/foo
195
+ if (pkg.startsWith('@/')) continue;
196
+ // Scoped packages: @scope/name/sub → @scope/name
197
+ if (pkg.startsWith('@')) {
198
+ const parts = pkg.split('/');
199
+ if (parts.length < 2) continue;
200
+ pkg = parts.slice(0, 2).join('/');
201
+ } else {
202
+ pkg = pkg.split('/')[0];
203
+ }
204
+ // Skip scaffold packages and Node.js built-ins
205
+ if (['next', 'react', 'react-dom', 'fs', 'path', 'http', 'https', 'url', 'crypto', 'stream', 'util', 'events', 'buffer', 'os', 'child_process', 'net', 'tls', 'dns', 'querystring', 'assert', 'zlib'].includes(pkg)) continue;
206
+ deps.add(pkg);
207
+ }
208
+ return deps;
209
+ }
210
+
211
+ // Pending files held back until deps are installed
212
+ let pendingFiles = [];
213
+ let pendingDeps = new Set();
214
+ let flushTimer = null;
215
+
216
+ /**
217
+ * Collect a file and its dependencies. After a brief pause (all files in a
218
+ * burst have arrived), install missing deps FIRST, then write all files.
219
+ */
220
+ function collectFileForInstall(file) {
221
+ if (!container) return;
222
+
223
+ pendingFiles.push(file);
224
+
225
+ if (file.path.match(/\.(ts|tsx|js|jsx)$/)) {
226
+ for (const dep of extractDependencies(file.content)) {
227
+ if (!installedPackages.has(dep)) {
228
+ pendingDeps.add(dep);
229
+ }
230
+ }
231
+ }
232
+
233
+ // Debounce: 1.5s after last file, flush everything
234
+ clearTimeout(flushTimer);
235
+ flushTimer = setTimeout(flushPendingFiles, 1500);
236
+ }
237
+
238
+ async function flushPendingFiles() {
239
+ const files = [...pendingFiles];
240
+ const deps = [...pendingDeps];
241
+ pendingFiles = [];
242
+ pendingDeps.clear();
243
+
244
+ // Install missing dependencies FIRST
245
+ if (deps.length > 0 && container) {
246
+ console.log(`[Preview] Installing ${deps.length} deps before writing files: ${deps.join(', ')}`);
247
+ updatePreviewStatus('building', `Installing ${deps.join(', ')}...`);
248
+ try {
249
+ const proc = await container.spawn('npm', ['install', '--legacy-peer-deps', ...deps]);
250
+ proc.output.pipeTo(new WritableStream({
251
+ write(data) { console.log('[Preview:install]', data); }
252
+ }));
253
+ const code = await proc.exit;
254
+ if (code === 0) {
255
+ for (const d of deps) installedPackages.add(d);
256
+ console.log(`[Preview] Installed: ${deps.join(', ')}`);
257
+ } else {
258
+ console.warn(`[Preview] npm install exited ${code} for: ${deps.join(', ')}`);
259
+ }
260
+ } catch (err) {
261
+ console.error('[Preview] Install failed:', err);
262
+ }
263
+ }
264
+
265
+ // NOW write all files
266
+ let envFileWritten = false;
267
+ for (const file of files) {
268
+ await writeFileToContainer(file.path, file.content);
269
+ if (file.path === '.env.local' || file.path === '.env') {
270
+ envFileWritten = true;
271
+ }
272
+ }
273
+ updatePreviewStatus('building', `Building... ${AppState.get().metrics.filesGenerated} files`);
274
+
275
+ // Next.js only reads .env.local at startup — restart the dev server
276
+ // so both server-side and client-side code pick up the new values
277
+ if (envFileWritten) {
278
+ await restartDevServer();
279
+ }
280
+
281
+ // If more files arrived while installing, flush again
282
+ if (pendingFiles.length > 0) {
283
+ clearTimeout(flushTimer);
284
+ flushTimer = setTimeout(flushPendingFiles, 1500);
285
+ }
286
+ }
287
+
288
+ function handleFileGenerated(file) {
289
+ if (!file || !file.path || !file.content) return;
290
+
291
+ const currentContainer = AppState.get().containerState;
292
+ if (currentContainer === 'warm' || currentContainer === 'cold') {
293
+ AppState.update({ containerState: 'building' });
294
+ }
295
+
296
+ if (isServerReady && container) {
297
+ // Collect files, install deps, then write — all batched
298
+ collectFileForInstall(file);
299
+ } else {
300
+ const queue = AppState.get().fileQueue;
301
+ queue.push({ path: file.path, content: file.content });
302
+ AppState.update({ fileQueue: queue });
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Kill the running Next.js dev server and start a fresh one.
308
+ * Required after .env.local changes — Next.js reads env files only at startup.
309
+ */
310
+ async function restartDevServer() {
311
+ if (!container) return;
312
+ console.log('[Preview] Restarting dev server to pick up new .env.local...');
313
+ updatePreviewStatus('building', 'Restarting dev server for env changes...');
314
+
315
+ // Kill the current server process
316
+ if (serverProcess) {
317
+ serverProcess.kill();
318
+ serverProcess = null;
319
+ }
320
+ isServerReady = false;
321
+
322
+ // Start a new dev server
323
+ serverProcess = await container.spawn('npm', ['run', 'dev']);
324
+ serverProcess.output.pipeTo(new WritableStream({
325
+ write(data) {
326
+ console.log('[Preview:server]', data);
327
+ }
328
+ }));
329
+
330
+ // Wait for server-ready via a one-time listener
331
+ await new Promise((resolve) => {
332
+ const onReady = (port, url) => {
333
+ console.log('[Preview] Server restarted on port', port, 'at', url);
334
+ isServerReady = true;
335
+ AppState.update({ containerState: 'warm', previewUrl: url });
336
+ const iframe = document.getElementById('preview-iframe');
337
+ if (iframe) iframe.src = url;
338
+ updatePreviewStatus('building', `Building... ${AppState.get().metrics.filesGenerated} files`);
339
+ resolve();
340
+ };
341
+ container.on('server-ready', onReady);
342
+ // Safety timeout — don't hang forever if server fails to start
343
+ setTimeout(() => {
344
+ isServerReady = true; // Allow file writes to continue regardless
345
+ resolve();
346
+ }, 30000);
347
+ });
348
+ }
349
+
350
+ async function writeFileToContainer(filePath, content) {
351
+ if (!container) return;
352
+
353
+ try {
354
+ const parts = filePath.split('/');
355
+ if (parts.length > 1) {
356
+ const dir = parts.slice(0, -1).join('/');
357
+ await container.fs.mkdir(dir, { recursive: true });
358
+ }
359
+
360
+ await container.fs.writeFile(filePath, content);
361
+ console.log(`[Preview] Wrote: ${filePath}`);
362
+ } catch (err) {
363
+ console.error(`[Preview] Failed to write ${filePath}:`, err);
364
+ }
365
+ }
366
+
367
+ function updatePreviewStatus(state, message) {
368
+ const statusEl = document.getElementById('preview-status');
369
+ if (statusEl) {
370
+ statusEl.textContent = message;
371
+ statusEl.dataset.state = state;
372
+ }
373
+
374
+ const fileCountEl = document.getElementById('file-count');
375
+ if (fileCountEl) {
376
+ const count = AppState.get().metrics.filesGenerated;
377
+ fileCountEl.textContent = count;
378
+ fileCountEl.parentElement.classList.toggle('hidden', count === 0);
379
+ }
380
+ }
381
+
382
+ function showDownloadButton() {
383
+ const previewPanel = document.getElementById('preview-panel');
384
+ if (!previewPanel) return;
385
+
386
+ if (previewPanel.querySelector('.preview-download-btn')) return;
387
+
388
+ const btn = document.createElement('button');
389
+ btn.className = 'preview-download-btn';
390
+ btn.textContent = 'Download Project';
391
+ btn.addEventListener('click', downloadProject);
392
+ previewPanel.appendChild(btn);
393
+ }
394
+
395
+ /**
396
+ * Collect all project files from the WebContainer as an array of {path, content}.
397
+ * Excludes node_modules and .next directories.
398
+ */
399
+ export async function collectProjectFiles() {
400
+ if (!container) return [];
401
+
402
+ const files = [];
403
+ async function walk(dirPath) {
404
+ const entries = await container.fs.readdir(dirPath, { withFileTypes: true });
405
+ for (const entry of entries) {
406
+ const fullPath = dirPath === '.' ? entry.name : `${dirPath}/${entry.name}`;
407
+ if (entry.name === 'node_modules' || entry.name === '.next') continue;
408
+
409
+ if (entry.isDirectory()) {
410
+ await walk(fullPath);
411
+ } else {
412
+ const content = await container.fs.readFile(fullPath, 'utf-8');
413
+ files.push({ path: fullPath, content });
414
+ }
415
+ }
416
+ }
417
+ await walk('.');
418
+ return files;
419
+ }
420
+
421
+ export async function downloadProject() {
422
+ if (!container) return;
423
+
424
+ try {
425
+ const { default: JSZip } = await import('https://esm.sh/jszip@3.10.1');
426
+ const zip = new JSZip();
427
+
428
+ async function addToZip(dirPath, zipFolder) {
429
+ const entries = await container.fs.readdir(dirPath, { withFileTypes: true });
430
+ for (const entry of entries) {
431
+ const fullPath = dirPath === '.' ? entry.name : `${dirPath}/${entry.name}`;
432
+ if (entry.name === 'node_modules' || entry.name === '.next') continue;
433
+
434
+ if (entry.isDirectory()) {
435
+ const subfolder = zipFolder.folder(entry.name);
436
+ await addToZip(fullPath, subfolder);
437
+ } else {
438
+ const content = await container.fs.readFile(fullPath, 'utf-8');
439
+ zipFolder.file(entry.name, content);
440
+ }
441
+ }
442
+ }
443
+
444
+ await addToZip('.', zip);
445
+
446
+ const blob = await zip.generateAsync({ type: 'blob' });
447
+ const url = URL.createObjectURL(blob);
448
+ const a = document.createElement('a');
449
+ a.href = url;
450
+ a.download = `${AppState.get().planSummary?.appName || 'assay-app'}.zip`;
451
+ a.click();
452
+ URL.revokeObjectURL(url);
453
+ } catch (err) {
454
+ console.error('[Preview] Download failed:', err);
455
+ }
456
+ }