Open History to see the last commit
1// @effect-diagnostics nodeBuiltinImport:off globalTimers:off globalDate:off - This host-side fixture creates an isolated local T3 environment.
2import * as NodeChildProcess from "node:child_process";
3import * as NodeFSP from "node:fs/promises";
4import * as NodePath from "node:path";
5import * as NodeSqlite from "node:sqlite";
6import * as NodeUtil from "node:util";
7
8const execFile = NodeUtil.promisify(NodeChildProcess.execFile);
9
10const SHOWCASE_PROJECT_ID = "t3code";
11const SHOWCASE_THREAD_ID = "remote-command-center";
12
13export const SHOWCASE_SCENES = [
14 "threads",
15 "thread",
16 "terminal",
17 "review",
18 "environments",
19 "agent-activity",
20] as const;
21export type ShowcaseScene = (typeof SHOWCASE_SCENES)[number];
22
23const MODEL_SELECTION = JSON.stringify({ instanceId: "codex", model: "gpt-5.4" });
24const PROJECT_SCRIPTS = JSON.stringify([
25 {
26 id: "dev",
27 name: "Dev",
28 command: "pnpm dev",
29 icon: "play",
30 runOnWorktreeCreate: false,
31 },
32 {
33 id: "test",
34 name: "Tests",
35 command: "pnpm test",
36 icon: "test",
37 runOnWorktreeCreate: false,
38 },
39]);
40
41const SHOWCASE_TERMINAL_PROMPT =
42 "\u001b[1;32m→\u001b[0m \u001b[1;36mt3code\u001b[0m \u001b[1;34mgit:(\u001b[1;31mfeat/remote-command-center\u001b[1;34m)\u001b[0m \u001b[1;33m✗\u001b[0m ";
43
44const SHOWCASE_TERMINAL_BUFFER = [
45 `${SHOWCASE_TERMINAL_PROMPT}vpr dev`,
46 "",
47 " \u001b[1;32mVITE\u001b[0m \u001b[32mv7.1.1\u001b[0m \u001b[2mready in\u001b[0m \u001b[1m1.24s\u001b[0m",
48 "",
49 " \u001b[32m→\u001b[0m \u001b[2mLocal:\u001b[0m \u001b[4;36mhttp://127.0.0.1:5173/\u001b[0m",
50 " \u001b[32m→\u001b[0m \u001b[2mNetwork:\u001b[0m \u001b[4;36mhttp://192.168.1.24:5173/\u001b[0m",
51 " \u001b[32m→\u001b[0m \u001b[2mProject:\u001b[0m \u001b[1mt3code\u001b[0m \u001b[2m(~/Code/t3code)\u001b[0m",
52 "",
53 " \u001b[32m✓ 612 passed\u001b[0m \u001b[33m△ 2 warnings\u001b[0m \u001b[31m✗ 0 failed\u001b[0m",
54 "",
55 " \u001b[42;30m READY \u001b[0m \u001b[2mwatching for changes, press\u001b[0m \u001b[1mq\u001b[0m \u001b[2mto quit\u001b[0m",
56 "",
57 SHOWCASE_TERMINAL_PROMPT,
58].join("\r\n");
59
60const BASE_ENVIRONMENT_PRESENCE = `export function environmentLabel(count: number): string {
61 return \`${"${count}"} environments\`;
62}
63`;
64
65const UPDATED_ENVIRONMENT_PRESENCE = `const PULSE = ["✦", "✧", "·", "✧"] as const;
66
67export function environmentLabel(connected: number, total: number, frame: number): string {
68 const pulse = PULSE[frame % PULSE.length];
69 return \`${"${pulse} ${connected}/${total}"} ready\`;
70}
71`;
72
73const REMOTE_HANDOFF_CARD = `import { View, Text } from "react-native";
74
75export function RemoteHandoffCard(props: { machine: string; latencyMs: number }) {
76 return (
77 <View className="rounded-2xl bg-surface-2 p-4">
78 <Text className="font-semibold">Ready on {props.machine}</Text>
79 <Text className="text-success">Handoff in {props.latencyMs}ms</Text>
80 </View>
81 );
82}
83`;
84
85const PROJECT_FAVICONS = {
86 t3code: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
87 <rect width="128" height="128" rx="10" fill="#000"/>
88 <path d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z" fill="#fff"/>
89</svg>`,
90 react: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
91 <rect width="64" height="64" rx="15" fill="#20232a"/>
92 <g fill="none" stroke="#61dafb" stroke-width="2.8"><ellipse cx="32" cy="32" rx="25" ry="9"/><ellipse cx="32" cy="32" rx="25" ry="9" transform="rotate(60 32 32)"/><ellipse cx="32" cy="32" rx="25" ry="9" transform="rotate(120 32 32)"/></g>
93 <circle cx="32" cy="32" r="4.8" fill="#61dafb"/>
94</svg>`,
95 linux: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
96 <rect width="64" height="64" rx="15" fill="#f7c948"/>
97 <ellipse cx="32" cy="35" rx="17" ry="22" fill="#202124"/>
98 <ellipse cx="32" cy="40" rx="12" ry="14" fill="#f5f5f2"/>
99 <circle cx="27" cy="24" r="5" fill="white"/><circle cx="37" cy="24" r="5" fill="white"/>
100 <circle cx="28" cy="25" r="2"/><circle cx="36" cy="25" r="2"/>
101 <path d="M27 31l5-4 5 4-5 4z" fill="#f28c28"/><path d="M16 55h14l-7-5zM34 55h14l-7-5z" fill="#f28c28"/>
102</svg>`,
103} as const;
104
105export const SHOWCASE_PROJECTS = [
106 {
107 id: "t3code",
108 title: "T3 Code",
109 directory: "t3code",
110 repositoryUrl: "https://github.com/pingdotgg/t3code.git",
111 favicon: PROJECT_FAVICONS.t3code,
112 },
113 {
114 id: "react",
115 title: "React",
116 directory: "react",
117 repositoryUrl: "https://github.com/facebook/react.git",
118 favicon: PROJECT_FAVICONS.react,
119 },
120 {
121 id: "linux",
122 title: "Linux",
123 directory: "linux",
124 repositoryUrl: "https://github.com/torvalds/linux.git",
125 favicon: PROJECT_FAVICONS.linux,
126 },
127] as const;
128
129export const SHOWCASE_ENVIRONMENTS = [
130 {
131 id: "moonbase-terminal",
132 label: "Moonbase Terminal",
133 projectIds: ["t3code"],
134 },
135 {
136 id: "suspense-station",
137 label: "Suspense Station",
138 projectIds: ["react"],
139 },
140 {
141 id: "kernel-cabin",
142 label: "Kernel Cabin",
143 projectIds: ["linux"],
144 },
145] as const;
146
147export const SHOWCASE_THREADS = [
148 {
149 id: SHOWCASE_THREAD_ID,
150 projectId: "t3code",
151 title: "Make remote coding feel local ✦",
152 branch: "feat/remote-command-center",
153 minutesAgo: 3,
154 request:
155 "Give T3 Code a remote-first command center. Make three machines feel one tap away, keep agent work in sync, and make every handoff feel instant.",
156 response:
157 "T3 Code now treats every machine like it is right here in the room. ✦\n\n- Moonbase, Suspense Station, and Kernel Cabin stay live together\n- Terminal state follows you without losing a single line\n- Agent work remains perfectly in sync across devices\n- Handoffs land before your train of thought can wander\n\nI also ran the changed workspace: **612 tests passed**.",
158 },
159 {
160 id: "pocket-command-center",
161 projectId: "t3code",
162 title: "Put the command center in your pocket",
163 branch: "feat/pocket-command-center",
164 minutesAgo: 21,
165 state: "approval" as const,
166 request: "Make switching between desktop, phone, and tablet feel like one continuous session.",
167 response:
168 "The handoff flow preserves the selected thread, terminal buffer, and working diff. The final motion treatment is ready for approval.",
169 },
170 {
171 id: "buttery-suspense",
172 projectId: "react",
173 title: "Make Suspense transitions buttery",
174 branch: "perf/buttery-suspense",
175 minutesAgo: 12,
176 state: "working" as const,
177 request:
178 "Trace the last few dropped frames in nested Suspense transitions and make them disappear.",
179 response: null,
180 },
181 {
182 id: "hydration-haikus",
183 projectId: "react",
184 title: "Turn hydration warnings into haikus",
185 branch: "dev/hydration-haikus",
186 minutesAgo: 44,
187 request:
188 "Keep hydration errors precise, but make the development copy unexpectedly delightful.",
189 response:
190 "The diagnostics still lead with the exact mismatch and component stack. A tiny optional haiku now closes the expanded explanation.",
191 snoozeMinutes: 90,
192 },
193 {
194 id: "beautiful-boot",
195 projectId: "linux",
196 title: "Make boot logs oddly beautiful",
197 branch: "feat/beautiful-boot",
198 minutesAgo: 34,
199 state: "plan" as const,
200 request:
201 "Design a clearer boot timeline that remains useful over serial and never hides kernel detail.",
202 response:
203 "The plan groups milestones without changing the underlying log stream, preserves plain-text output, and adds zero work to the hot path.",
204 },
205 {
206 id: "patient-penguins",
207 projectId: "linux",
208 title: "Teach penguins to wait patiently",
209 branch: "feat/patient-penguins",
210 minutesAgo: 52,
211 request: "Make delayed work easier to follow without adding noise to the scheduler trace.",
212 response:
213 "Delayed work now carries a concise reason through the trace, so the wait is legible without changing scheduling behavior.",
214 snoozeMinutes: 8 * 60,
215 },
216 {
217 id: "handoff-haptics",
218 projectId: "t3code",
219 title: "Tune the handoff haptics",
220 branch: "feat/handoff-haptics",
221 minutesAgo: 5 * 60,
222 settled: true,
223 request: "Give the desktop-to-phone handoff a haptic that lands with the animation.",
224 response:
225 "The handoff now taps once as the thread lands and stays silent on failure, so the phone never celebrates a handoff that did not happen.",
226 },
227 {
228 id: "streaming-shell",
229 projectId: "react",
230 title: "Stream the shell before the data",
231 branch: "feat/streaming-shell",
232 minutesAgo: 28 * 60,
233 settled: true,
234 request: "Get the app shell painted before any data request resolves.",
235 response:
236 "The shell now flushes on first byte and the data boundaries hydrate underneath it, so the first paint no longer waits on the slowest query.",
237 },
238 {
239 id: "quieter-oom",
240 projectId: "linux",
241 title: "Make the OOM killer explain itself",
242 branch: "feat/quieter-oom",
243 minutesAgo: 2 * 24 * 60,
244 settled: true,
245 request: "Make out-of-memory kills legible without adding a single allocation to the hot path.",
246 response:
247 "Kills now report the winning heuristic and the runner-up alongside the usual dump, assembled entirely from data the path already had.",
248 },
249] as const;
250
251function minutesBefore(now: number, minutes: number): string {
252 return new Date(now - minutes * 60_000).toISOString();
253}
254
255async function runGit(workspaceRoot: string, args: ReadonlyArray<string>): Promise<void> {
256 await execFile("git", [...args], {
257 cwd: workspaceRoot,
258 env: {
259 ...process.env,
260 GIT_AUTHOR_NAME: "Alex Rivera",
261 GIT_AUTHOR_EMAIL: "alex@lumen.test",
262 GIT_COMMITTER_NAME: "Alex Rivera",
263 GIT_COMMITTER_EMAIL: "alex@lumen.test",
264 },
265 });
266}
267
268async function initializeRepository(input: {
269 readonly workspaceRoot: string;
270 readonly repositoryUrl: string;
271 readonly commitMessage: string;
272}): Promise<void> {
273 await runGit(input.workspaceRoot, ["init", "-b", "main"]);
274 await runGit(input.workspaceRoot, ["remote", "add", "origin", input.repositoryUrl]);
275 await runGit(input.workspaceRoot, ["add", "."]);
276 await runGit(input.workspaceRoot, ["commit", "-m", input.commitMessage]);
277}
278
279async function seedT3CodeWorkspace(workspaceRoot: string): Promise<void> {
280 await NodeFSP.mkdir(NodePath.join(workspaceRoot, "apps/mobile/src/features/home"), {
281 recursive: true,
282 });
283 await NodeFSP.writeFile(
284 NodePath.join(workspaceRoot, "package.json"),
285 `${JSON.stringify({ name: "t3code", private: true, scripts: { test: "vp test" } }, null, 2)}\n`,
286 );
287 await NodeFSP.writeFile(NodePath.join(workspaceRoot, "favicon.svg"), PROJECT_FAVICONS.t3code);
288 await NodeFSP.writeFile(
289 NodePath.join(workspaceRoot, "apps/mobile/src/features/home/environmentPresence.ts"),
290 BASE_ENVIRONMENT_PRESENCE,
291 );
292 await initializeRepository({
293 workspaceRoot,
294 repositoryUrl: "https://github.com/pingdotgg/t3code.git",
295 commitMessage: "Show connected environments",
296 });
297 await runGit(workspaceRoot, ["checkout", "-b", "feat/remote-command-center"]);
298 await NodeFSP.writeFile(
299 NodePath.join(workspaceRoot, "apps/mobile/src/features/home/environmentPresence.ts"),
300 UPDATED_ENVIRONMENT_PRESENCE,
301 );
302 await NodeFSP.writeFile(
303 NodePath.join(workspaceRoot, "apps/mobile/src/features/home/RemoteHandoffCard.tsx"),
304 REMOTE_HANDOFF_CARD,
305 );
306}
307
308async function seedCompanionWorkspace(input: {
309 readonly workspaceRoot: string;
310 readonly title: string;
311 readonly repositoryUrl: string;
312 readonly favicon: string;
313}): Promise<void> {
314 await NodeFSP.mkdir(input.workspaceRoot, { recursive: true });
315 await NodeFSP.writeFile(NodePath.join(input.workspaceRoot, "favicon.svg"), input.favicon);
316 await NodeFSP.writeFile(
317 NodePath.join(input.workspaceRoot, "README.md"),
318 `# ${input.title}\n\nSeeded by the T3 Code mobile screenshot harness.\n`,
319 );
320 await initializeRepository({
321 workspaceRoot: input.workspaceRoot,
322 repositoryUrl: input.repositoryUrl,
323 commitMessage: `Seed ${input.title} workspace`,
324 });
325}
326
327function insertThread(
328 database: NodeSqlite.DatabaseSync,
329 now: number,
330 input: {
331 readonly id: string;
332 readonly projectId: string;
333 readonly title: string;
334 readonly branch: string;
335 readonly minutesAgo: number;
336 readonly state?: "working" | "approval" | "plan";
337 readonly settled?: boolean;
338 readonly snoozeMinutes?: number;
339 readonly workspaceRoot: string;
340 },
341): void {
342 const turnId = `${input.id}-turn`;
343 const updatedAt = minutesBefore(now, input.minutesAgo);
344 const isWorking = input.state === "working";
345 const snoozedUntil =
346 input.snoozeMinutes === undefined
347 ? null
348 : new Date(now + input.snoozeMinutes * 60_000).toISOString();
349 const snoozedAt =
350 input.snoozeMinutes === undefined
351 ? null
352 : minutesBefore(now, Math.max(1, Math.floor(input.minutesAgo / 2)));
353 database
354 .prepare(
355 `INSERT INTO projection_threads (
356 thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode,
357 branch, worktree_path, latest_turn_id, latest_user_message_at, pending_approval_count,
358 pending_user_input_count, has_actionable_proposed_plan, created_at, updated_at,
359 archived_at, deleted_at, settled_override, settled_at, snoozed_until, snoozed_at
360 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, NULL, NULL, ?, ?, ?, ?)`,
361 )
362 .run(
363 input.id,
364 input.projectId,
365 input.title,
366 MODEL_SELECTION,
367 "full-access",
368 input.state === "plan" ? "plan" : "default",
369 input.branch,
370 input.workspaceRoot,
371 turnId,
372 minutesBefore(now, input.minutesAgo + 1),
373 input.state === "approval" ? 1 : 0,
374 input.state === "plan" ? 1 : 0,
375 minutesBefore(now, input.minutesAgo + 120),
376 updatedAt,
377 input.settled ? "settled" : null,
378 input.settled ? updatedAt : null,
379 snoozedUntil,
380 snoozedAt,
381 );
382 database
383 .prepare(
384 `INSERT INTO projection_thread_sessions (
385 thread_id, status, provider_name, provider_instance_id, provider_session_id,
386 provider_thread_id, runtime_mode, active_turn_id, last_error, updated_at
387 ) VALUES (?, ?, 'Codex', 'codex', NULL, NULL, 'full-access', ?, NULL, ?)`,
388 )
389 .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt);
390}
391
392// V2 still owns projection_projects; upsert project rows without deleting them with V1 thread data.
393const SEEDED_V1_TABLES = [
394 "projection_thread_messages",
395 "projection_thread_sessions",
396 "projection_threads",
397] as const;
398
399const SEEDED_PROJECTION_TABLES = [...SEEDED_V1_TABLES, "projection_projects"] as const;
400
401const SEEDED_THREAD_COLUMNS = ["snoozed_until", "snoozed_at"] as const;
402
403function hasSeedableSchema(dbPath: string): boolean {
404 let database: NodeSqlite.DatabaseSync;
405 try {
406 database = new NodeSqlite.DatabaseSync(dbPath, { readOnly: true });
407 } catch {
408 return false;
409 }
410 try {
411 const tableCount = database
412 .prepare(
413 `SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`,
414 )
415 .get(...SEEDED_PROJECTION_TABLES) as { count: number };
416 if (tableCount.count !== SEEDED_PROJECTION_TABLES.length) return false;
417
418 const threadColumns = database.prepare("PRAGMA table_info(projection_threads)").all() as Array<{
419 name: string;
420 }>;
421 const threadColumnNames = new Set(threadColumns.map((column) => column.name));
422 return SEEDED_THREAD_COLUMNS.every((column) => threadColumnNames.has(column));
423 } catch {
424 return false;
425 } finally {
426 database.close();
427 }
428}
429
430async function waitForSeedableSchema(dbPath: string, timeoutMs = 60_000): Promise<void> {
431 const deadline = Date.now() + timeoutMs;
432 while (Date.now() < deadline) {
433 if (hasSeedableSchema(dbPath)) return;
434 await new Promise((resolve) => setTimeout(resolve, 250));
435 }
436 throw new Error(`The environment server did not migrate ${dbPath} within ${timeoutMs}ms.`);
437}
438
439function seedDatabase(
440 dbPath: string,
441 workspaceRoots: ReadonlyMap<string, string>,
442 projects: ReadonlyArray<(typeof SHOWCASE_PROJECTS)[number]>,
443 threads: ReadonlyArray<(typeof SHOWCASE_THREADS)[number]>,
444 now: number,
445): void {
446 // The running server writes migrations/projections concurrently; wait for its lock instead of failing SQLITE_BUSY.
447 const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 });
448 try {
449 database.exec("BEGIN IMMEDIATE");
450 for (const table of SEEDED_V1_TABLES) {
451 database.exec(`DELETE FROM ${table}`);
452 }
453 const insertProject = database.prepare(
454 `INSERT OR REPLACE INTO projection_projects (
455 project_id, title, workspace_root, default_model_selection_json, scripts_json,
456 created_at, updated_at, deleted_at
457 ) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,
458 );
459 for (const [index, project] of projects.entries()) {
460 const workspaceRoot = workspaceRoots.get(project.id);
461 if (!workspaceRoot) throw new Error(`Missing workspace root for ${project.id}.`);
462 const latestThreadMinutes = Math.min(
463 ...threads
464 .filter((thread) => thread.projectId === project.id)
465 .map((thread) => thread.minutesAgo),
466 );
467 insertProject.run(
468 project.id,
469 project.title,
470 workspaceRoot,
471 MODEL_SELECTION,
472 PROJECT_SCRIPTS,
473 minutesBefore(now, 60 * 24 * (90 - index * 12)),
474 minutesBefore(now, latestThreadMinutes),
475 );
476 }
477
478 for (const thread of threads) {
479 const workspaceRoot = workspaceRoots.get(thread.projectId);
480 if (!workspaceRoot) throw new Error(`Missing workspace root for ${thread.projectId}.`);
481 insertThread(database, now, {
482 ...thread,
483 ...("state" in thread ? { state: thread.state } : {}),
484 workspaceRoot,
485 });
486 }
487
488 const insertMessage = database.prepare(
489 `INSERT INTO projection_thread_messages (
490 message_id, thread_id, turn_id, role, text, is_streaming, attachments_json,
491 created_at, updated_at
492 ) VALUES (?, ?, ?, ?, ?, 0, NULL, ?, ?)`,
493 );
494 for (const thread of threads) {
495 const turnId = `${thread.id}-turn`;
496 const requestTime = minutesBefore(now, thread.minutesAgo + 5);
497 insertMessage.run(
498 `${thread.id}-request`,
499 thread.id,
500 turnId,
501 "user",
502 thread.request,
503 requestTime,
504 requestTime,
505 );
506 if (thread.response !== null) {
507 const responseTime = minutesBefore(now, thread.minutesAgo);
508 insertMessage.run(
509 `${thread.id}-answer`,
510 thread.id,
511 turnId,
512 "assistant",
513 thread.response,
514 responseTime,
515 responseTime,
516 );
517 }
518 }
519
520 database.exec("COMMIT");
521 } catch (error) {
522 // Failed BEGIN or automatic rollback leaves no transaction; preserve the original error if rollback fails.
523 try {
524 database.exec("ROLLBACK");
525 } catch {}
526 throw error;
527 } finally {
528 database.close();
529 }
530}
531
532export async function seedShowcaseEnvironment(input: {
533 readonly baseDir: string;
534 readonly projectIds?: ReadonlyArray<string>;
535 readonly now?: number;
536}): Promise<{ readonly dbPath: string; readonly workspaceRoot: string }> {
537 const now = input.now ?? Date.now();
538 const selectedProjectIds = new Set(
539 input.projectIds ?? SHOWCASE_PROJECTS.map((project) => project.id),
540 );
541 const projects = SHOWCASE_PROJECTS.filter((project) => selectedProjectIds.has(project.id));
542 if (projects.length === 0) throw new Error("At least one showcase project must be selected.");
543 const threads = SHOWCASE_THREADS.filter((thread) => selectedProjectIds.has(thread.projectId));
544 const workspaceBase = NodePath.join(input.baseDir, "workspace");
545 const workspaceRoots = new Map(
546 projects.map(
547 (project) => [project.id, NodePath.join(workspaceBase, project.directory)] as const,
548 ),
549 );
550 const primaryProject =
551 projects.find((project) => project.id === SHOWCASE_PROJECT_ID) ?? projects[0];
552 if (!primaryProject) throw new Error("The primary showcase workspace is not configured.");
553 const workspaceRoot = workspaceRoots.get(primaryProject.id);
554 if (!workspaceRoot) throw new Error("The primary showcase workspace is not configured.");
555 const dbPath = NodePath.join(input.baseDir, "userdata", "statev2.sqlite");
556 if (primaryProject.id === SHOWCASE_PROJECT_ID) {
557 await seedT3CodeWorkspace(workspaceRoot);
558 }
559 await Promise.all(
560 projects
561 .filter((project) => project.id !== SHOWCASE_PROJECT_ID)
562 .map(async (project) => {
563 const projectWorkspaceRoot = workspaceRoots.get(project.id);
564 if (!projectWorkspaceRoot) throw new Error(`Missing workspace root for ${project.id}.`);
565 await seedCompanionWorkspace({
566 workspaceRoot: projectWorkspaceRoot,
567 title: project.title,
568 repositoryUrl: project.repositoryUrl,
569 favicon: project.favicon,
570 });
571 }),
572 );
573 // Listening precedes migration completion; wait for the schema before reseeding.
574 await waitForSeedableSchema(dbPath);
575 seedDatabase(dbPath, workspaceRoots, projects, threads, now);
576
577 const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals");
578 if (selectedProjectIds.has(SHOWCASE_PROJECT_ID)) {
579 const safeThreadId = Buffer.from(SHOWCASE_THREAD_ID).toString("base64url");
580 await NodeFSP.mkdir(terminalDirectory, { recursive: true });
581 await NodeFSP.writeFile(
582 NodePath.join(terminalDirectory, `terminal_${safeThreadId}.log`),
583 SHOWCASE_TERMINAL_BUFFER,
584 );
585 }
586 return { dbPath, workspaceRoot };
587}