tryassay 0.22.0 → 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.
- package/demo/css/style.css +495 -836
- package/demo/index.html +40 -184
- package/demo/js/chat.js +385 -142
- package/demo/js/preview.js +456 -0
- package/demo/js/sse-client.js +262 -135
- package/demo/js/state.js +11 -1
- package/demo/js/timeline.js +57 -371
- package/dist/api/server.d.ts +2 -0
- package/dist/api/server.js +63 -19
- package/dist/api/server.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/assess.d.ts +2 -0
- package/dist/commands/assess.js +132 -164
- package/dist/commands/assess.js.map +1 -1
- package/dist/commands/demo.js +259 -9
- package/dist/commands/demo.js.map +1 -1
- package/dist/lib/__tests__/arithmetic-quick-test.d.ts +6 -0
- package/dist/lib/__tests__/arithmetic-quick-test.js +197 -0
- package/dist/lib/__tests__/arithmetic-quick-test.js.map +1 -0
- package/dist/lib/__tests__/arithmetic-real-llm-test.d.ts +13 -0
- package/dist/lib/__tests__/arithmetic-real-llm-test.js +284 -0
- package/dist/lib/__tests__/arithmetic-real-llm-test.js.map +1 -0
- package/dist/lib/__tests__/arithmetic-value-demo.d.ts +10 -0
- package/dist/lib/__tests__/arithmetic-value-demo.js +193 -0
- package/dist/lib/__tests__/arithmetic-value-demo.js.map +1 -0
- package/dist/lib/__tests__/flow-to-claims.test.d.ts +1 -0
- package/dist/lib/__tests__/flow-to-claims.test.js +91 -0
- package/dist/lib/__tests__/flow-to-claims.test.js.map +1 -0
- package/dist/lib/__tests__/formal-verifier-api-misuse.test.d.ts +9 -0
- package/dist/lib/__tests__/formal-verifier-api-misuse.test.js +391 -0
- package/dist/lib/__tests__/formal-verifier-api-misuse.test.js.map +1 -0
- package/dist/lib/__tests__/formal-verifier-arithmetic.test.d.ts +7 -0
- package/dist/lib/__tests__/formal-verifier-arithmetic.test.js +318 -0
- package/dist/lib/__tests__/formal-verifier-arithmetic.test.js.map +1 -0
- package/dist/lib/__tests__/intent-extractor.test.d.ts +1 -0
- package/dist/lib/__tests__/intent-extractor.test.js +97 -0
- package/dist/lib/__tests__/intent-extractor.test.js.map +1 -0
- package/dist/lib/__tests__/intent-reviewer.test.d.ts +1 -0
- package/dist/lib/__tests__/intent-reviewer.test.js +55 -0
- package/dist/lib/__tests__/intent-reviewer.test.js.map +1 -0
- package/dist/lib/__tests__/mr-gsm8k-benchmark.d.ts +11 -0
- package/dist/lib/__tests__/mr-gsm8k-benchmark.js +224 -0
- package/dist/lib/__tests__/mr-gsm8k-benchmark.js.map +1 -0
- package/dist/lib/anthropic.js +25 -33
- package/dist/lib/anthropic.js.map +1 -1
- package/dist/lib/assessment-reporter.js +9 -13
- package/dist/lib/assessment-reporter.js.map +1 -1
- package/dist/lib/claim-extractor.js +10 -19
- package/dist/lib/claim-extractor.js.map +1 -1
- package/dist/lib/code-verifier.js +16 -36
- package/dist/lib/code-verifier.js.map +1 -1
- package/dist/lib/constraint-engine.js +10 -19
- package/dist/lib/constraint-engine.js.map +1 -1
- package/dist/lib/formal-verifier.d.ts +1 -1
- package/dist/lib/formal-verifier.js +454 -0
- package/dist/lib/formal-verifier.js.map +1 -1
- package/dist/lib/guided-generator.js +19 -37
- package/dist/lib/guided-generator.js.map +1 -1
- package/dist/lib/intent-extractor.d.ts +47 -0
- package/dist/lib/intent-extractor.js +427 -0
- package/dist/lib/intent-extractor.js.map +1 -0
- package/dist/lib/intent-reviewer.d.ts +14 -0
- package/dist/lib/intent-reviewer.js +148 -0
- package/dist/lib/intent-reviewer.js.map +1 -0
- package/dist/lib/intent-types.d.ts +89 -0
- package/dist/lib/intent-types.js +5 -0
- package/dist/lib/intent-types.js.map +1 -0
- package/dist/lib/inventory-extractor.js +9 -22
- package/dist/lib/inventory-extractor.js.map +1 -1
- package/dist/lib/llm-provider.d.ts +23 -0
- package/dist/lib/llm-provider.js +130 -0
- package/dist/lib/llm-provider.js.map +1 -0
- package/dist/lib/remediator.js +20 -28
- package/dist/lib/remediator.js.map +1 -1
- package/dist/lib/requirements-generator.js +14 -19
- package/dist/lib/requirements-generator.js.map +1 -1
- package/dist/lib/spec-synthesizer.js +10 -19
- package/dist/lib/spec-synthesizer.js.map +1 -1
- package/dist/runtime/app-create-orchestrator.d.ts +5 -1
- package/dist/runtime/app-create-orchestrator.js +114 -39
- package/dist/runtime/app-create-orchestrator.js.map +1 -1
- package/dist/runtime/check-catalog.js +5 -3
- package/dist/runtime/check-catalog.js.map +1 -1
- package/dist/runtime/check-definitions.d.ts +10 -0
- package/dist/runtime/check-definitions.js +52 -2
- package/dist/runtime/check-definitions.js.map +1 -1
- package/dist/runtime/composition-verifier.js +8 -12
- package/dist/runtime/composition-verifier.js.map +1 -1
- package/dist/runtime/gap-detector.js +8 -10
- package/dist/runtime/gap-detector.js.map +1 -1
- package/dist/runtime/input-validator.d.ts +7 -0
- package/dist/runtime/input-validator.js +162 -0
- package/dist/runtime/input-validator.js.map +1 -0
- package/dist/runtime/model-router.d.ts +10 -0
- package/dist/runtime/model-router.js +42 -0
- package/dist/runtime/model-router.js.map +1 -0
- package/dist/runtime/pattern-extractor.js +8 -10
- package/dist/runtime/pattern-extractor.js.map +1 -1
- package/dist/runtime/planner.js +11 -16
- package/dist/runtime/planner.js.map +1 -1
- package/dist/runtime/prompt-guard.d.ts +2 -0
- package/dist/runtime/prompt-guard.js +180 -0
- package/dist/runtime/prompt-guard.js.map +1 -0
- package/dist/runtime/prompt-safety-analyzer.js +8 -13
- package/dist/runtime/prompt-safety-analyzer.js.map +1 -1
- package/dist/runtime/reasoner.js +19 -33
- package/dist/runtime/reasoner.js.map +1 -1
- package/dist/runtime/rule-meta-verifier.js +9 -11
- package/dist/runtime/rule-meta-verifier.js.map +1 -1
- package/dist/runtime/safe-executor.d.ts +23 -0
- package/dist/runtime/safe-executor.js +151 -0
- package/dist/runtime/safe-executor.js.map +1 -0
- package/dist/runtime/specialized-agent.js +10 -14
- package/dist/runtime/specialized-agent.js.map +1 -1
- package/dist/runtime/strategy-library.js +8 -10
- package/dist/runtime/strategy-library.js.map +1 -1
- package/dist/runtime/supabase-experience-store.js.map +1 -1
- package/dist/runtime/supabase-provisioner.d.ts +35 -0
- package/dist/runtime/supabase-provisioner.js +192 -0
- package/dist/runtime/supabase-provisioner.js.map +1 -0
- package/dist/runtime/types.d.ts +88 -0
- package/dist/sdk/forward-verify.js +16 -33
- package/dist/sdk/forward-verify.js.map +1 -1
- package/package.json +1 -1
- package/demo/data/demo-events.json +0 -103
- package/demo/js/demo-mode.js +0 -107
- package/demo/js/orb.js +0 -634
- package/demo/js/question-cards.js +0 -207
- package/demo/js/voice.js +0 -154
|
@@ -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
|
+
}
|