@inneranimalmedia/agentsam-sdk 1.5.1 → 1.7.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.
@@ -0,0 +1,175 @@
1
+ /**
2
+ * CMS Site Scaffold Wizard
3
+ *
4
+ * Guides the user step-by-step through scaffolding a
5
+ * Cloudflare Worker + D1 + R2 CMS site with:
6
+ * - global nav
7
+ * - reusable page templates
8
+ * - optional blog, contact, about pages
9
+ * - wrangler.toml ready to deploy
10
+ */
11
+
12
+ import {
13
+ text,
14
+ select,
15
+ multiselect,
16
+ confirm,
17
+ spinner,
18
+ note,
19
+ isCancel,
20
+ cancel,
21
+ } from '@clack/prompts';
22
+ import pc from 'picocolors';
23
+ import { writeFileTree } from '../writer.js';
24
+ import { cmsTemplates } from '../templates/cms/index.js';
25
+
26
+ export async function runCmsWizard() {
27
+ note('A Cloudflare Worker + D1 + R2 CMS site with reusable page templates.', 'CMS Site');
28
+
29
+ // ── Step 1: Project name ──────────────────────────────────────────────────
30
+ const projectName = await text({
31
+ message: 'Project name?',
32
+ placeholder: 'my-client-site',
33
+ validate(val) {
34
+ if (!val || val.trim().length === 0) return 'Required.';
35
+ if (!/^[a-z0-9-]+$/.test(val.trim())) return 'Lowercase letters, numbers, and hyphens only.';
36
+ },
37
+ });
38
+ if (isCancel(projectName)) { cancel('Cancelled.'); process.exit(0); }
39
+
40
+ // ── Step 2: Site title ────────────────────────────────────────────────────
41
+ const siteTitle = await text({
42
+ message: 'Site title?',
43
+ placeholder: 'Acme Corp',
44
+ validate(val) {
45
+ if (!val || val.trim().length === 0) return 'Required.';
46
+ },
47
+ });
48
+ if (isCancel(siteTitle)) { cancel('Cancelled.'); process.exit(0); }
49
+
50
+ // ── Step 3: Nav style ─────────────────────────────────────────────────────
51
+ const navStyle = await select({
52
+ message: 'Global nav style?',
53
+ options: [
54
+ { value: 'topbar', label: 'Top bar', hint: 'Horizontal links, fixed to top' },
55
+ { value: 'sidebar', label: 'Sidebar', hint: 'Left rail, collapsible on mobile' },
56
+ { value: 'minimal', label: 'Minimal', hint: 'Logo + hamburger only' },
57
+ ],
58
+ });
59
+ if (isCancel(navStyle)) { cancel('Cancelled.'); process.exit(0); }
60
+
61
+ // ── Step 4: Pages to include ──────────────────────────────────────────────
62
+ const pages = await multiselect({
63
+ message: 'Which pages do you want? (space to toggle, enter to confirm)',
64
+ options: [
65
+ { value: 'home', label: 'Home', hint: 'Landing / hero section' },
66
+ { value: 'about', label: 'About', hint: 'About us / team' },
67
+ { value: 'services', label: 'Services', hint: 'Services or products list' },
68
+ { value: 'blog', label: 'Blog', hint: 'D1-backed post list + detail' },
69
+ { value: 'contact', label: 'Contact', hint: 'Contact form → Resend email' },
70
+ { value: 'privacy', label: 'Privacy', hint: 'Privacy policy (static)' },
71
+ ],
72
+ required: true,
73
+ });
74
+ if (isCancel(pages)) { cancel('Cancelled.'); process.exit(0); }
75
+
76
+ // ── Step 5: Page template style ───────────────────────────────────────────
77
+ const templateStyle = await select({
78
+ message: 'Page template style?',
79
+ options: [
80
+ {
81
+ value: 'fragment',
82
+ label: 'R2 HTML fragments',
83
+ hint: 'Each section is a .html fragment stored in R2, assembled by the Worker',
84
+ },
85
+ {
86
+ value: 'json',
87
+ label: 'D1-driven JSON blocks',
88
+ hint: 'Page content stored in D1 as JSON blocks, rendered server-side',
89
+ },
90
+ {
91
+ value: 'static',
92
+ label: 'Static HTML',
93
+ hint: 'Pre-rendered HTML files, no D1 dependency',
94
+ },
95
+ ],
96
+ });
97
+ if (isCancel(templateStyle)) { cancel('Cancelled.'); process.exit(0); }
98
+
99
+ // ── Step 6: Contact form destination (only if contact page selected) ──────
100
+ let contactEmail = null;
101
+ if (pages.includes('contact')) {
102
+ contactEmail = await text({
103
+ message: 'Where should contact form submissions go? (email)',
104
+ placeholder: 'hello@example.com',
105
+ validate(val) {
106
+ if (!val || val.trim().length === 0) return 'Required for contact page.';
107
+ if (!val.includes('@')) return 'Enter a valid email.';
108
+ },
109
+ });
110
+ if (isCancel(contactEmail)) { cancel('Cancelled.'); process.exit(0); }
111
+ }
112
+
113
+ // ── Step 7: Cloudflare account details ────────────────────────────────────
114
+ const cfAccountId = await text({
115
+ message: 'Cloudflare account ID? (from dash.cloudflare.com → right sidebar)',
116
+ placeholder: 'abc123...',
117
+ validate(val) {
118
+ if (!val || val.trim().length === 0) return 'Required for wrangler.toml.';
119
+ },
120
+ });
121
+ if (isCancel(cfAccountId)) { cancel('Cancelled.'); process.exit(0); }
122
+
123
+ // ── Step 8: Confirm before writing ───────────────────────────────────────
124
+ const summary = [
125
+ ` Project: ${pc.cyan(projectName)}`,
126
+ ` Title: ${pc.cyan(siteTitle)}`,
127
+ ` Nav: ${pc.cyan(navStyle)}`,
128
+ ` Pages: ${pc.cyan(pages.join(', '))}`,
129
+ ` Template: ${pc.cyan(templateStyle)}`,
130
+ contactEmail ? ` Contact: ${pc.cyan(contactEmail)}` : null,
131
+ ].filter(Boolean).join('\n');
132
+
133
+ note(summary, 'Your scaffold');
134
+
135
+ const confirmed = await confirm({ message: 'Write files now?' });
136
+ if (isCancel(confirmed) || !confirmed) { cancel('Cancelled.'); process.exit(0); }
137
+
138
+ // ── Write files ───────────────────────────────────────────────────────────
139
+ const s = spinner();
140
+ s.start('Generating files...');
141
+
142
+ const config = {
143
+ projectName: projectName.trim(),
144
+ siteTitle: siteTitle.trim(),
145
+ navStyle,
146
+ pages,
147
+ templateStyle,
148
+ contactEmail: contactEmail?.trim() ?? null,
149
+ cfAccountId: cfAccountId.trim(),
150
+ };
151
+
152
+ const fileTree = cmsTemplates(config);
153
+
154
+ await writeFileTree(`./${config.projectName}`, fileTree);
155
+
156
+ s.stop(pc.green(`Files written to ./${config.projectName}/`));
157
+
158
+ note(
159
+ [
160
+ `cd ${config.projectName}`,
161
+ `npm install`,
162
+ ``,
163
+ `# Create your D1 database:`,
164
+ `npx wrangler d1 create ${config.projectName}`,
165
+ `# Copy the database_id into wrangler.toml [[d1_databases]] binding`,
166
+ ``,
167
+ `# Run the D1 migration:`,
168
+ `npx wrangler d1 execute ${config.projectName} --file=migrations/001_init.sql --remote`,
169
+ ``,
170
+ `# Deploy:`,
171
+ `npx wrangler deploy`,
172
+ ].join('\n'),
173
+ 'Next steps'
174
+ );
175
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Worker API Scaffold Wizard
3
+ * Bare Cloudflare Worker with typed route handlers and D1 binding.
4
+ */
5
+
6
+ import {
7
+ text,
8
+ multiselect,
9
+ confirm,
10
+ spinner,
11
+ note,
12
+ isCancel,
13
+ cancel,
14
+ } from '@clack/prompts';
15
+ import pc from 'picocolors';
16
+ import { writeFileTree } from '../writer.js';
17
+ import { workerApiTemplates } from '../templates/worker-api/index.js';
18
+
19
+ export async function runWorkerApiWizard() {
20
+ note('A typed Cloudflare Worker with route handlers, D1 binding, and CORS ready.', 'Worker API');
21
+
22
+ const projectName = await text({
23
+ message: 'Project name?',
24
+ placeholder: 'my-api-worker',
25
+ validate(val) {
26
+ if (!val || val.trim().length === 0) return 'Required.';
27
+ if (!/^[a-z0-9-]+$/.test(val.trim())) return 'Lowercase letters, numbers, and hyphens only.';
28
+ },
29
+ });
30
+ if (isCancel(projectName)) { cancel('Cancelled.'); process.exit(0); }
31
+
32
+ const routes = await multiselect({
33
+ message: 'Which route groups do you want?',
34
+ options: [
35
+ { value: 'health', label: 'GET /health', hint: 'Liveness check' },
36
+ { value: 'auth', label: 'POST /auth/*', hint: 'Token issue + verify' },
37
+ { value: 'users', label: 'CRUD /users', hint: 'D1-backed user records' },
38
+ { value: 'content', label: 'CRUD /content', hint: 'Generic content/pages' },
39
+ { value: 'webhook', label: 'POST /webhooks/:type', hint: 'Inbound webhook receiver' },
40
+ ],
41
+ required: true,
42
+ });
43
+ if (isCancel(routes)) { cancel('Cancelled.'); process.exit(0); }
44
+
45
+ const cfAccountId = await text({
46
+ message: 'Cloudflare account ID?',
47
+ placeholder: 'abc123...',
48
+ validate(val) {
49
+ if (!val || val.trim().length === 0) return 'Required.';
50
+ },
51
+ });
52
+ if (isCancel(cfAccountId)) { cancel('Cancelled.'); process.exit(0); }
53
+
54
+ const confirmed = await confirm({ message: `Write files to ./${projectName.trim()}/?` });
55
+ if (isCancel(confirmed) || !confirmed) { cancel('Cancelled.'); process.exit(0); }
56
+
57
+ const s = spinner();
58
+ s.start('Generating files...');
59
+
60
+ const config = { projectName: projectName.trim(), routes, cfAccountId: cfAccountId.trim() };
61
+ const fileTree = workerApiTemplates(config);
62
+ await writeFileTree(`./${config.projectName}`, fileTree);
63
+
64
+ s.stop(pc.green(`Files written to ./${config.projectName}/`));
65
+
66
+ note(
67
+ [
68
+ `cd ${config.projectName}`,
69
+ `npm install`,
70
+ `npx wrangler d1 create ${config.projectName}`,
71
+ `npx wrangler d1 execute ${config.projectName} --file=migrations/001_init.sql --remote`,
72
+ `npx wrangler deploy`,
73
+ ].join('\n'),
74
+ 'Next steps'
75
+ );
76
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Writes a nested file-tree object to disk.
3
+ *
4
+ * fileTree shape:
5
+ * {
6
+ * 'src/index.js': '// content',
7
+ * 'wrangler.toml': '...',
8
+ * 'migrations/001_init.sql': '...',
9
+ * }
10
+ */
11
+
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
14
+
15
+ export async function writeFileTree(baseDir, fileTree) {
16
+ for (const [relativePath, content] of Object.entries(fileTree)) {
17
+ const fullPath = path.join(baseDir, relativePath);
18
+ const dir = path.dirname(fullPath);
19
+ await fs.mkdir(dir, { recursive: true });
20
+ await fs.writeFile(fullPath, content, 'utf8');
21
+ }
22
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * @file src/ui/splash-xterm.js
3
+ * @description In-app splash renderer for xterm.js panels.
4
+ *
5
+ * Same choreography as splash.js (CLI) but writes to an xterm Terminal
6
+ * instance instead of process.stdout. Used by XTermShell.tsx.
7
+ *
8
+ * Usage in XTermShell.tsx:
9
+ * import { runXtermSplash } from '@inneranimalmedia/agentsam-sdk/ui/splash-xterm';
10
+ * import { getXtermOptions } from '@inneranimalmedia/agentsam-sdk/ui/theme';
11
+ *
12
+ * const targetType = connection?.target_type ?? 'platform_vm';
13
+ * const term = new Terminal({
14
+ * ...getXtermOptions(targetType),
15
+ * cols: dimensions.cols,
16
+ * rows: dimensions.rows,
17
+ * });
18
+ *
19
+ * // After term.open(containerRef.current):
20
+ * const stopFlicker = await runXtermSplash(term, {
21
+ * apiBase: 'https://inneranimalmedia.com',
22
+ * workspaceId: activeWorkspace?.id,
23
+ * authToken: session?.token,
24
+ * targetType,
25
+ * skipArt: !isFirstSession,
26
+ * });
27
+ *
28
+ * // On cleanup:
29
+ * return () => { stopFlicker(); term.dispose(); };
30
+ */
31
+
32
+ import { PALETTE, getLaneTheme } from './theme.js';
33
+ import { probeAll } from './splash.js';
34
+
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ // xterm write helpers
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+
39
+ const ESC = '\x1b';
40
+ const CSI = `${ESC}[`;
41
+
42
+ const X = {
43
+ reset: `${CSI}0m`,
44
+ bold: `${CSI}1m`,
45
+ hide: `${CSI}?25l`,
46
+ show: `${CSI}?25h`,
47
+ clearLine: `${CSI}2K\r`,
48
+ up: (n = 1) => `${CSI}${n}A`,
49
+ fg: (hex) => {
50
+ const h = hex.replace('#', '');
51
+ const r = parseInt(h.slice(0, 2), 16);
52
+ const g = parseInt(h.slice(2, 4), 16);
53
+ const b = parseInt(h.slice(4, 6), 16);
54
+ return `${CSI}38;2;${r};${g};${b}m`;
55
+ },
56
+ };
57
+
58
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
59
+
60
+ // ─────────────────────────────────────────────────────────────────────────────
61
+ // COLOR BUILDER (per-lane)
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+
64
+ function buildColors() {
65
+ const R = X.reset;
66
+ const B = X.bold;
67
+ return {
68
+ R, B,
69
+ TEAL: X.fg(PALETTE.teal300),
70
+ AMBER: X.fg(PALETTE.amber300),
71
+ STONE: X.fg('#3a3530'),
72
+ STONE2: X.fg('#2a2520'),
73
+ VINE: X.fg('#1a4a2a'),
74
+ VINE2: X.fg('#0f6b3a'),
75
+ FIRE1: X.fg('#f97316'),
76
+ FIRE2: X.fg('#ef4444'),
77
+ FIREY: X.fg('#fef08a'),
78
+ GRILL: X.fg('#1e2830'),
79
+ GRILL2: X.fg('#2d3d4a'),
80
+ PELT: X.fg('#8b6f5a'),
81
+ DIM: X.fg('#444444'),
82
+ GHOST: X.fg('#888888'),
83
+ WHT: X.fg('#e2e8f0'),
84
+ };
85
+ }
86
+
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+ // SCENE DATA
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+
91
+ function gorillaRows(C) {
92
+ const { R, GRILL, GRILL2, PELT, DIM } = C;
93
+ return [
94
+ ` ${DIM}░░▒${R}${GRILL2}▓▓▓▓▓▓▓▓${R}${DIM}▒░░${R}`,
95
+ ` ${DIM}░${R}${GRILL2}▒▓▓${R}${GRILL}█████████████${R}${GRILL2}▓▓▒${R}${DIM}░${R}`,
96
+ ` ${GRILL2}▒▓${R}${GRILL}███${R}${GRILL2}▒▒${R}${GRILL}████████${R}${GRILL2}▒▒${R}${GRILL}███${R}${GRILL2}▓▒${R}`,
97
+ ` ${GRILL2}▓${R}${GRILL}████${R} ${DIM}▒▒▒▒▒▒▒▒${R} ${GRILL}████${R}${GRILL2}▓${R}`,
98
+ ` ${GRILL}▓███${R}${GRILL2}▒${R} ${PELT}██${R}${DIM}▒${R}${PELT}████${R}${DIM}▒${R}${PELT}██${R} ${GRILL2}▒${R}${GRILL}███▓${R}`,
99
+ ` ${GRILL}████${R} ${PELT}███${R}${DIM}▒▒▒▒${R}${PELT}███${R} ${GRILL}████${R}`,
100
+ ` ${GRILL}████${R}${GRILL2}▒${R} ${PELT}██████████${R} ${GRILL2}▒${R}${GRILL}████${R}`,
101
+ ` ${GRILL}████${R} ${DIM}▒▒▒▒▒▒▒▒▒▒${R} ${GRILL}████${R}`,
102
+ ` ${GRILL2}▒${R}${GRILL}████${R}${GRILL2}▓▓▓${R}${GRILL}████████████${R}${GRILL2}▓▓▓${R}${GRILL}████${R}${GRILL2}▒${R}`,
103
+ ` ${GRILL}██████████████████████████${R}`,
104
+ ` ${DIM}▒▒${R}${GRILL}████████${R}${GRILL2}▒▒▒▒▒▒▒▒▒▒▒▒${R}${GRILL}████████${R}${DIM}▒▒${R}`,
105
+ ` ${PELT}▓${R}${GRILL}██████████${R}${DIM}░░░░░░░░░░░░░░${R}${GRILL}██████████${R}${PELT}▓${R}`,
106
+ ];
107
+ }
108
+
109
+ function titleBoxLines(C) {
110
+ const { R, B, TEAL, AMBER } = C;
111
+ const wide = 26;
112
+ const h = `${TEAL}═${R}`;
113
+ return [
114
+ `${TEAL}╔${R}${h.repeat(wide)}${TEAL}╗${R}`,
115
+ `${TEAL}║${R} ${B}${AMBER}INNERANIMAL MEDIA${R} ${TEAL}║${R}`,
116
+ `${TEAL}║${R} ${TEAL}inneranimalmedia.com${R} ${TEAL}║${R}`,
117
+ `${TEAL}╚${R}${h.repeat(wide)}${TEAL}╝${R}`,
118
+ ];
119
+ }
120
+
121
+ function ladderRows(C) {
122
+ const { R, TEAL } = C;
123
+ return [
124
+ ` ${TEAL}║${R} ${TEAL}║${R}`,
125
+ ` ${TEAL}╠═══╣${R}`,
126
+ ` ${TEAL}║${R} ${TEAL}║${R}`,
127
+ ` ${TEAL}╠═══╣${R}`,
128
+ ` ${TEAL}║${R} ${TEAL}║${R}`,
129
+ ` ${TEAL}╠═══╣${R}`,
130
+ ` ${TEAL}║${R} ${TEAL}║${R}`,
131
+ ];
132
+ }
133
+
134
+ // ─────────────────────────────────────────────────────────────────────────────
135
+ // HUD RENDER
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+
138
+ const HUD_ITEMS = [
139
+ { key: 'workspace', icon: '⊞', label: 'Workspace' },
140
+ { key: 'runtime', icon: '▣', label: 'Runtime' },
141
+ { key: 'tunnel', icon: '⟁', label: 'Tunnel' },
142
+ { key: 'agent', icon: '⬡', label: 'Agent' },
143
+ ];
144
+
145
+ const READY_LABELS = {
146
+ workspace: 'active',
147
+ runtime: 'ready',
148
+ tunnel: 'connected',
149
+ agent: 'online',
150
+ };
151
+
152
+ function renderHUDLine(states, C) {
153
+ const { R, B, TEAL, GHOST, WHT, DIM } = C;
154
+ return HUD_ITEMS.map((item, i) => {
155
+ const state = states[item.key] || 'checking';
156
+ let stateStr;
157
+ if (state === 'ready') {
158
+ stateStr = `${TEAL}● ${READY_LABELS[item.key]}${R}`;
159
+ } else if (state === 'checking') {
160
+ stateStr = `${GHOST}◌ checking...${R}`;
161
+ } else if (state === 'error') {
162
+ stateStr = `${X.fg('#ef4444')}✗ error${R}`;
163
+ } else {
164
+ stateStr = `${X.fg('#666666')}○ offline${R}`;
165
+ }
166
+ const div = i < HUD_ITEMS.length - 1 ? ` ${DIM}│${R} ` : '';
167
+ return `${GHOST}${item.icon}${R} ${B}${WHT}${item.label}${R} ${stateStr}${div}`;
168
+ }).join('');
169
+ }
170
+
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ // MAIN XTERM SPLASH
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+
175
+ /**
176
+ * @param {import('@xterm/xterm').Terminal} term
177
+ * @param {object} opts
178
+ * @param {string} opts.apiBase
179
+ * @param {string} opts.workspaceId
180
+ * @param {string} opts.authToken
181
+ * @param {'user_hosted_tunnel'|'platform_vm'|'sandbox'} opts.targetType
182
+ * @param {boolean} opts.skipArt
183
+ * @returns {Promise<() => void>} stopFlicker — call on unmount
184
+ */
185
+ export async function runXtermSplash(term, {
186
+ apiBase = '',
187
+ workspaceId = '',
188
+ authToken = '',
189
+ targetType = 'platform_vm',
190
+ skipArt = false,
191
+ } = {}) {
192
+
193
+ const lane = getLaneTheme(targetType);
194
+ const C = buildColors();
195
+ const { R, B, TEAL, AMBER, GHOST, WHT, DIM, STONE, STONE2, FIRE1, FIRE2 } = C;
196
+ const PRIMARY = X.fg(lane.colors.primary);
197
+ const W = term.cols || 80;
198
+
199
+ const w = (s) => term.write(s);
200
+ const wl = (s = '') => term.write(s + '\r\n');
201
+
202
+ w(X.hide);
203
+ w(`${ESC}c`); // full reset
204
+ await sleep(60);
205
+
206
+ // cd verb flash
207
+ w(` ${GHOST}$ ${R}${AMBER}cd ~/inneranimalmedia${R}`);
208
+ await sleep(340);
209
+ w(`\r${X.clearLine}`);
210
+ await sleep(60);
211
+
212
+ if (!skipArt) {
213
+ wl();
214
+
215
+ const rows = gorillaRows(C);
216
+ const title = titleBoxLines(C);
217
+ const TITLE_START = 3;
218
+
219
+ for (let i = 0; i < rows.length; i++) {
220
+ const titleIdx = i - TITLE_START;
221
+ const titlePart = (titleIdx >= 0 && titleIdx < title.length)
222
+ ? ' ' + title[titleIdx]
223
+ : '';
224
+ wl(` ${rows[i]}${titlePart}`);
225
+ await sleep(24);
226
+ }
227
+
228
+ wl();
229
+
230
+ // Torches
231
+ w(` ${FIRE1} ▓▓▓${R}`);
232
+ await sleep(90);
233
+ wl(`${' '.repeat(Math.max(0, W - 14))}${FIRE2}▓▓▓${R}`);
234
+ await sleep(30);
235
+
236
+ // Ledge
237
+ w(' ');
238
+ for (let i = 0; i < W - 4; i++) {
239
+ w((i % 2 === 0) ? `${STONE}█${R}` : `${STONE2}▓${R}`);
240
+ await sleep(4);
241
+ }
242
+ wl();
243
+ wl(` ${DIM}${'╌'.repeat(W - 4)}${R}`);
244
+ await sleep(50);
245
+
246
+ // Ladder
247
+ for (const rung of ladderRows(C)) {
248
+ wl(rung);
249
+ await sleep(30);
250
+ }
251
+ }
252
+
253
+ wl();
254
+
255
+ // Start prompt
256
+ await sleep(skipArt ? 0 : 70);
257
+ wl(` ${B}${AMBER}Start ${PRIMARY}▸${R}`);
258
+ await sleep(40);
259
+ wl(` ${GHOST}Type a command to begin.${R}`);
260
+ await sleep(skipArt ? 0 : 70);
261
+ wl();
262
+
263
+ // HUD — fire probes in parallel
264
+ const probePromise = probeAll({ apiBase, workspaceId, authToken });
265
+ const initStates = { workspace: 'checking', runtime: 'checking', tunnel: 'checking', agent: 'checking' };
266
+
267
+ wl(` ${DIM}${'─'.repeat(W - 4)}${R}`);
268
+ wl();
269
+ w(` ${renderHUDLine(initStates, C)}`);
270
+ wl(); wl();
271
+ wl(` ${DIM}${'─'.repeat(W - 4)}${R}`);
272
+ wl();
273
+
274
+ // Update HUD in-place when probes resolve
275
+ const LINES_BELOW = 4;
276
+ probePromise.then(resolved => {
277
+ w(X.up(LINES_BELOW));
278
+ w(`\r${X.clearLine}`);
279
+ w(` ${renderHUDLine(resolved, C)}`);
280
+ w('\r\n'.repeat(LINES_BELOW));
281
+ w(X.show);
282
+ w(` ${PRIMARY}>${R} `);
283
+ });
284
+
285
+ // Torch flicker is handled via CSS animation on the panel wrapper
286
+ // (terminal-lanes.css) — xterm.js decoration API in Phase 2.
287
+ return () => {};
288
+ }
289
+
290
+ export default { runXtermSplash };