unfold-deploy 0.0.0-stage → 0.2.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -2
  4. package/bin/unfold.js +14 -0
  5. package/demos/claude-success.json +72 -0
  6. package/demos/codex-checks-failing.json +62 -0
  7. package/demos/gcloud-deploy-failure.json +616 -0
  8. package/demos/gcloud-success.json +608 -0
  9. package/demos/vercel-build-failure.json +331 -0
  10. package/demos/vercel-interrupted.json +271 -0
  11. package/demos/vercel-success.json +343 -0
  12. package/dist/cli.js +121 -0
  13. package/dist/commands/demo.js +75 -0
  14. package/dist/commands/observe.js +137 -0
  15. package/dist/commands/replay.js +76 -0
  16. package/dist/commands/run.js +24 -0
  17. package/dist/commands/show.js +44 -0
  18. package/dist/core/errors.js +3 -0
  19. package/dist/core/fix-prompt.js +85 -0
  20. package/dist/core/flow.js +211 -0
  21. package/dist/core/launch.js +196 -0
  22. package/dist/core/model.js +3 -0
  23. package/dist/core/redact.js +79 -0
  24. package/dist/core/server.js +109 -0
  25. package/dist/core/session.js +136 -0
  26. package/dist/core/store.js +52 -0
  27. package/dist/core/terminal.js +109 -0
  28. package/dist/providers/agents/claude.js +205 -0
  29. package/dist/providers/agents/codex.js +152 -0
  30. package/dist/providers/agents/shared.js +119 -0
  31. package/dist/providers/gcloud/index.js +39 -0
  32. package/dist/providers/gcloud/parser.js +149 -0
  33. package/dist/providers/gcloud/stages.js +55 -0
  34. package/dist/providers/index.js +5 -0
  35. package/dist/providers/provider.js +1 -0
  36. package/dist/providers/vercel/index.js +56 -0
  37. package/dist/providers/vercel/parser.js +239 -0
  38. package/dist/providers/vercel/stages.js +54 -0
  39. package/dist/tui/ansi.js +71 -0
  40. package/dist/tui/explore.js +220 -0
  41. package/dist/tui/panel.js +118 -0
  42. package/dist/tui/render.js +140 -0
  43. package/dist/tui/restore.js +29 -0
  44. package/dist/ui/app.js +332 -0
  45. package/dist/ui/index.html +60 -0
  46. package/dist/ui/styles.css +220 -0
  47. package/package.json +64 -4
  48. package/scripts/postinstall.mjs +13 -0
@@ -0,0 +1,343 @@
1
+ {
2
+ "provider": "vercel",
3
+ "description": "A preview deploy of a small Next.js app that succeeds.",
4
+ "command": [
5
+ "vercel",
6
+ "deploy",
7
+ "--logs"
8
+ ],
9
+ "cwd": "~/projects/acme-web",
10
+ "exitCode": 0,
11
+ "signal": null,
12
+ "interrupted": false,
13
+ "chunks": [
14
+ {
15
+ "t": 0,
16
+ "d": "Vercel CLI 65.2.0\r\n"
17
+ },
18
+ {
19
+ "t": 300,
20
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Retrieving project…"
21
+ },
22
+ {
23
+ "t": 380,
24
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Retrieving project…"
25
+ },
26
+ {
27
+ "t": 460,
28
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Retrieving project…"
29
+ },
30
+ {
31
+ "t": 540,
32
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Retrieving project…"
33
+ },
34
+ {
35
+ "t": 620,
36
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Retrieving project…"
37
+ },
38
+ {
39
+ "t": 700,
40
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Retrieving project…"
41
+ },
42
+ {
43
+ "t": 780,
44
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Retrieving project…"
45
+ },
46
+ {
47
+ "t": 860,
48
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Retrieving project…"
49
+ },
50
+ {
51
+ "t": 940,
52
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Retrieving project…"
53
+ },
54
+ {
55
+ "t": 1020,
56
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Deploying \u001b[1macme/acme-web\u001b[22m"
57
+ },
58
+ {
59
+ "t": 1100,
60
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Deploying \u001b[1macme/acme-web\u001b[22m"
61
+ },
62
+ {
63
+ "t": 1180,
64
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Deploying \u001b[1macme/acme-web\u001b[22m"
65
+ },
66
+ {
67
+ "t": 1260,
68
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Deploying \u001b[1macme/acme-web\u001b[22m"
69
+ },
70
+ {
71
+ "t": 1340,
72
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Deploying \u001b[1macme/acme-web\u001b[22m"
73
+ },
74
+ {
75
+ "t": 1420,
76
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Deploying \u001b[1macme/acme-web\u001b[22m"
77
+ },
78
+ {
79
+ "t": 1500,
80
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Deploying \u001b[1macme/acme-web\u001b[22m"
81
+ },
82
+ {
83
+ "t": 1580,
84
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Deploying \u001b[1macme/acme-web\u001b[22m"
85
+ },
86
+ {
87
+ "t": 1660,
88
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Deploying \u001b[1macme/acme-web\u001b[22m"
89
+ },
90
+ {
91
+ "t": 1780,
92
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Uploading \u001b[0m[--------------------] (0.0KB/184.6KB)"
93
+ },
94
+ {
95
+ "t": 2040,
96
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Uploading \u001b[0m[==------------------] (23.1KB/184.6KB)"
97
+ },
98
+ {
99
+ "t": 2300,
100
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Uploading \u001b[0m[=====---------------] (46.1KB/184.6KB)"
101
+ },
102
+ {
103
+ "t": 2560,
104
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Uploading \u001b[0m[=======-------------] (69.2KB/184.6KB)"
105
+ },
106
+ {
107
+ "t": 2820,
108
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Uploading \u001b[0m[==========----------] (92.3KB/184.6KB)"
109
+ },
110
+ {
111
+ "t": 3080,
112
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Uploading \u001b[0m[============--------] (115.4KB/184.6KB)"
113
+ },
114
+ {
115
+ "t": 3340,
116
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Uploading \u001b[0m[===============-----] (138.4KB/184.6KB)"
117
+ },
118
+ {
119
+ "t": 3600,
120
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Uploading \u001b[0m[=================---] (161.5KB/184.6KB)"
121
+ },
122
+ {
123
+ "t": 3860,
124
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Uploading \u001b[0m[====================] (184.6KB/184.6KB)"
125
+ },
126
+ {
127
+ "t": 4160,
128
+ "d": "\u001b[2K\u001b[1G"
129
+ },
130
+ {
131
+ "t": 4160,
132
+ "d": " \u001b[1mInspect \u001b[22m\u001b[1m\u001b[36mhttps://vercel.com/acme/acme-web/6TfY2pQkzR1mWcN8xLbV3hGd\u001b[39m\u001b[22m\r\n"
133
+ },
134
+ {
135
+ "t": 4180,
136
+ "d": " \u001b[1mPreview \u001b[22m\u001b[1m\u001b[36mhttps://acme-web-7k2m9xq4p-acme.vercel.app\u001b[39m\u001b[22m\r\n"
137
+ },
138
+ {
139
+ "t": 4300,
140
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Queued…"
141
+ },
142
+ {
143
+ "t": 4380,
144
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Queued…"
145
+ },
146
+ {
147
+ "t": 4460,
148
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Queued…"
149
+ },
150
+ {
151
+ "t": 4540,
152
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Queued…"
153
+ },
154
+ {
155
+ "t": 4620,
156
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Queued…"
157
+ },
158
+ {
159
+ "t": 4700,
160
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Queued…"
161
+ },
162
+ {
163
+ "t": 4780,
164
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Queued…"
165
+ },
166
+ {
167
+ "t": 4860,
168
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Queued…"
169
+ },
170
+ {
171
+ "t": 4940,
172
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Queued…"
173
+ },
174
+ {
175
+ "t": 5020,
176
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Queued…"
177
+ },
178
+ {
179
+ "t": 5100,
180
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Queued…"
181
+ },
182
+ {
183
+ "t": 5180,
184
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Queued…"
185
+ },
186
+ {
187
+ "t": 5260,
188
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Queued…"
189
+ },
190
+ {
191
+ "t": 5340,
192
+ "d": "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Queued…"
193
+ },
194
+ {
195
+ "t": 5340,
196
+ "d": "\u001b[2K\u001b[1G"
197
+ },
198
+ {
199
+ "t": 5540,
200
+ "d": "\u001b[2m2026-11-05T18:04:03.460Z\u001b[22m Running build in Washington, D.C., USA (East) – iad1\r\n"
201
+ },
202
+ {
203
+ "t": 5680,
204
+ "d": "\u001b[2m2026-11-05T18:04:04.482Z\u001b[22m Build machine configuration: 2 cores, 8 GB\r\n"
205
+ },
206
+ {
207
+ "t": 5820,
208
+ "d": "\u001b[2m2026-11-05T18:04:05.504Z\u001b[22m Retrieving list of deployment files...\r\n"
209
+ },
210
+ {
211
+ "t": 6120,
212
+ "d": "\u001b[2m2026-11-05T18:04:07.694Z\u001b[22m Downloading 41 deployment files...\r\n"
213
+ },
214
+ {
215
+ "t": 6620,
216
+ "d": "\u001b[2m2026-11-05T18:04:11.344Z\u001b[22m Restored build cache from previous deployment (9QwErTy2uIoPaSdF)\r\n"
217
+ },
218
+ {
219
+ "t": 6820,
220
+ "d": "\u001b[2m2026-11-05T18:04:12.804Z\u001b[22m Running \"vercel build\"\r\n"
221
+ },
222
+ {
223
+ "t": 7120,
224
+ "d": "\u001b[2m2026-11-05T18:04:14.994Z\u001b[22m Vercel CLI 65.2.0\r\n"
225
+ },
226
+ {
227
+ "t": 7320,
228
+ "d": "\u001b[2m2026-11-05T18:04:16.454Z\u001b[22m Installing dependencies...\r\n"
229
+ },
230
+ {
231
+ "t": 8920,
232
+ "d": "\u001b[2m2026-11-05T18:04:28.134Z\u001b[22m added 3 packages, and audited 318 packages in 3s\r\n"
233
+ },
234
+ {
235
+ "t": 9120,
236
+ "d": "\u001b[2m2026-11-05T18:04:29.594Z\u001b[22m Detected Next.js version: 16.0.1\r\n"
237
+ },
238
+ {
239
+ "t": 9320,
240
+ "d": "\u001b[2m2026-11-05T18:04:31.054Z\u001b[22m Running \"npm run build\"\r\n"
241
+ },
242
+ {
243
+ "t": 9460,
244
+ "d": "\u001b[2m2026-11-05T18:04:32.076Z\u001b[22m \r\n"
245
+ },
246
+ {
247
+ "t": 9660,
248
+ "d": "\u001b[2m2026-11-05T18:04:33.536Z\u001b[22m > acme-web@0.1.0 build\r\n"
249
+ },
250
+ {
251
+ "t": 9800,
252
+ "d": "\u001b[2m2026-11-05T18:04:34.558Z\u001b[22m > next build\r\n"
253
+ },
254
+ {
255
+ "t": 9940,
256
+ "d": "\u001b[2m2026-11-05T18:04:35.580Z\u001b[22m \r\n"
257
+ },
258
+ {
259
+ "t": 10340,
260
+ "d": "\u001b[2m2026-11-05T18:04:38.500Z\u001b[22m ▲ Next.js 16.0.1\r\n"
261
+ },
262
+ {
263
+ "t": 10540,
264
+ "d": "\u001b[2m2026-11-05T18:04:39.960Z\u001b[22m Creating an optimized production build ...\r\n"
265
+ },
266
+ {
267
+ "t": 12940,
268
+ "d": "\u001b[2m2026-11-05T18:04:57.480Z\u001b[22m ✓ Compiled successfully in 7.4s\r\n"
269
+ },
270
+ {
271
+ "t": 13140,
272
+ "d": "\u001b[2m2026-11-05T18:04:58.940Z\u001b[22m Linting and checking validity of types ...\r\n"
273
+ },
274
+ {
275
+ "t": 14540,
276
+ "d": "\u001b[2m2026-11-05T18:05:09.160Z\u001b[22m Collecting page data ...\r\n"
277
+ },
278
+ {
279
+ "t": 15240,
280
+ "d": "\u001b[2m2026-11-05T18:05:14.270Z\u001b[22m ✓ Generating static pages (7/7)\r\n"
281
+ },
282
+ {
283
+ "t": 15540,
284
+ "d": "\u001b[2m2026-11-05T18:05:16.460Z\u001b[22m Finalizing page optimization ...\r\n"
285
+ },
286
+ {
287
+ "t": 15680,
288
+ "d": "\u001b[2m2026-11-05T18:05:17.482Z\u001b[22m \r\n"
289
+ },
290
+ {
291
+ "t": 15880,
292
+ "d": "\u001b[2m2026-11-05T18:05:18.942Z\u001b[22m Route (app) Size First Load JS\r\n"
293
+ },
294
+ {
295
+ "t": 15900,
296
+ "d": "\u001b[2m2026-11-05T18:05:19.088Z\u001b[22m ┌ ○ / 5.21 kB 112 kB\r\n"
297
+ },
298
+ {
299
+ "t": 15920,
300
+ "d": "\u001b[2m2026-11-05T18:05:19.234Z\u001b[22m ├ ○ /_not-found 0 B 106 kB\r\n"
301
+ },
302
+ {
303
+ "t": 15940,
304
+ "d": "\u001b[2m2026-11-05T18:05:19.380Z\u001b[22m └ ○ /checkout 3.14 kB 110 kB\r\n"
305
+ },
306
+ {
307
+ "t": 15960,
308
+ "d": "\u001b[2m2026-11-05T18:05:19.526Z\u001b[22m ○ (Static) prerendered as static content\r\n"
309
+ },
310
+ {
311
+ "t": 16460,
312
+ "d": "\u001b[2m2026-11-05T18:05:23.176Z\u001b[22m Build Completed in /vercel/output [24s]\r\n"
313
+ },
314
+ {
315
+ "t": 16760,
316
+ "d": "\u001b[2m2026-11-05T18:05:25.366Z\u001b[22m Deploying outputs...\r\n"
317
+ },
318
+ {
319
+ "t": 18460,
320
+ "d": "\u001b[2m2026-11-05T18:05:37.776Z\u001b[22m Deployment completed\r\n"
321
+ },
322
+ {
323
+ "t": 18660,
324
+ "d": "\u001b[2m2026-11-05T18:05:39.236Z\u001b[22m Creating build cache...\r\n"
325
+ },
326
+ {
327
+ "t": 19560,
328
+ "d": "\u001b[2m2026-11-05T18:05:45.806Z\u001b[22m Created build cache: 3.212s\r\n"
329
+ },
330
+ {
331
+ "t": 19660,
332
+ "d": "\u001b[2m2026-11-05T18:05:46.536Z\u001b[22m Uploading build cache [41.2 MB]...\r\n"
333
+ },
334
+ {
335
+ "t": 20260,
336
+ "d": "\r\n\u001b[32m✓\u001b[39m \u001b[1mReady\u001b[22m \u001b[2min 31s\u001b[22m\r\n\r\n"
337
+ },
338
+ {
339
+ "t": 20300,
340
+ "d": "\r\n\u001b[33m!\u001b[39m Deployment Protection is on for this project (Vercel Authentication). Use \u001b[36m`vercel curl`\u001b[39m to access it.\r\n"
341
+ }
342
+ ]
343
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,121 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { runDemo, DEMO_LIST } from './commands/demo.js';
3
+ import { DEFAULT_FLAGS } from './commands/observe.js';
4
+ import { listRuns, runReplay } from './commands/replay.js';
5
+ import { runLive } from './commands/run.js';
6
+ import { runShow } from './commands/show.js';
7
+ import { UsageError } from './core/errors.js';
8
+ import { providers } from './providers/index.js';
9
+ const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
10
+ const HELP = `unfold — Watch what happens after you hit Enter.
11
+
12
+ Usage
13
+ unfold vercel [vercel deploy flags] Vercel preview deploy: Prepare → Upload → Build → Preview
14
+ unfold gcloud [gcloud run deploy flags]
15
+ Cloud Run deploy: Prepare → Build → Deploy → Live
16
+ unfold claude "<task>" [claude flags] Claude Code task: Start → Explore → Change → Verify → Done
17
+ unfold codex "<task>" [codex flags] Codex task: Start → Explore → Change → Verify → Done
18
+ unfold show [run] Explore a run in the terminal (defaults to the latest)
19
+ unfold demo [provider] <name> Play a recorded demo, no account needed:
20
+ ${DEMO_LIST}
21
+ unfold replay [run] Play a saved run again
22
+ unfold runs List saved runs
23
+
24
+ Unfold options (anywhere on the line)
25
+ --fix <agent> If the run fails, start "claude" or "codex" with the fix prompt
26
+ (a fix prompt is always saved on failure; this only launches the agent)
27
+ --web Also show the flow in a local browser page (off by default)
28
+ --no-open With --web: don't open the browser automatically
29
+ --once With --web: exit when the run ends instead of keeping the page served
30
+ --port <n> With --web: port for the page (default 4317)
31
+ --no-panel Don't pin the live flow panel to the bottom of the terminal
32
+ --speed <n> Playback speed for demo/replay (default 1)
33
+
34
+ Examples
35
+ unfold vercel
36
+ unfold vercel --fix claude
37
+ unfold gcloud --source . --region us-central1
38
+ unfold claude "fix the failing test" --allowedTools Read Edit Bash
39
+ unfold codex "add a --json flag to the CLI" --sandbox workspace-write
40
+ unfold show
41
+ unfold demo build-failure
42
+ unfold demo claude success
43
+ `;
44
+ export { UsageError };
45
+ /** Pulls Unfold's own flags out of argv; everything else goes to the wrapped CLI. */
46
+ export function parseArgs(argv) {
47
+ const rest = [];
48
+ const flags = { ...DEFAULT_FLAGS };
49
+ let speed = 1;
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const a = argv[i];
52
+ if (a === '--web')
53
+ flags.web = true;
54
+ else if (a === '--no-open')
55
+ flags.open = false;
56
+ else if (a === '--once')
57
+ flags.once = true;
58
+ else if (a === '--no-panel')
59
+ flags.panel = false;
60
+ else if (a === '--port')
61
+ flags.port = Number(argv[++i]);
62
+ else if (a.startsWith('--port='))
63
+ flags.port = Number(a.slice(7));
64
+ else if (a === '--fix' || a.startsWith('--fix=')) {
65
+ const v = a === '--fix' ? argv[++i] : a.slice(6);
66
+ if (v !== 'claude' && v !== 'codex')
67
+ throw new UsageError(`--fix expects "claude" or "codex", got "${v ?? ''}"`);
68
+ flags.fix = v;
69
+ }
70
+ else if (a === '--speed')
71
+ speed = Number(argv[++i]) || 1;
72
+ else if (a.startsWith('--speed='))
73
+ speed = Number(a.slice(8)) || 1;
74
+ else
75
+ rest.push(a);
76
+ }
77
+ return { rest, flags, speed };
78
+ }
79
+ export async function main(argv) {
80
+ try {
81
+ return await run(argv);
82
+ }
83
+ catch (err) {
84
+ if (!(err instanceof UsageError))
85
+ throw err;
86
+ process.stderr.write(`unfold: ${err.message}\nRun \`unfold --help\` for usage.\n`);
87
+ return 2;
88
+ }
89
+ }
90
+ async function run(argv) {
91
+ const [cmd, ...tail] = argv;
92
+ if (cmd === '-v' || cmd === '--version' || cmd === 'version') {
93
+ process.stdout.write(VERSION + '\n');
94
+ return 0;
95
+ }
96
+ if (!cmd || cmd === '-h' || cmd === '--help' || cmd === 'help') {
97
+ process.stdout.write(HELP);
98
+ return 0;
99
+ }
100
+ const provider = providers[cmd];
101
+ if (provider) {
102
+ // Only strip Unfold flags; the wrapped CLI receives everything else verbatim.
103
+ const { rest, flags } = parseArgs(tail);
104
+ return runLive(provider, rest, flags);
105
+ }
106
+ const { rest, flags, speed } = parseArgs(tail);
107
+ switch (cmd) {
108
+ case 'show':
109
+ return runShow(rest[0], flags);
110
+ case 'demo':
111
+ return runDemo(rest, flags, speed);
112
+ case 'replay':
113
+ return runReplay(rest[0], flags, speed);
114
+ case 'runs':
115
+ for (const r of listRuns())
116
+ process.stdout.write(r + '\n');
117
+ return 0;
118
+ default:
119
+ throw new UsageError(`unknown command "${cmd}". Supported: ${[...Object.keys(providers), 'show', 'demo', 'replay', 'runs'].join(', ')}.`);
120
+ }
121
+ }
@@ -0,0 +1,75 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { providers } from '../providers/index.js';
5
+ import { observe, say } from './observe.js';
6
+ export const DEMOS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'demos');
7
+ /** Demos per provider; the first provider's demos also work without naming it. */
8
+ export const DEMOS = {
9
+ vercel: ['success', 'build-failure', 'interrupted'],
10
+ gcloud: ['success', 'deploy-failure'],
11
+ claude: ['success'],
12
+ codex: ['checks-failing'],
13
+ };
14
+ export const DEMO_LIST = Object.entries(DEMOS)
15
+ .flatMap(([p, names]) => names.map((n) => `${p} ${n}`))
16
+ .join(', ');
17
+ /** Loads `vercel-success.json` from `loadDemo('success')` or `loadDemo('gcloud', 'success')`. */
18
+ export function loadDemo(providerOrName, name) {
19
+ const file = name ? `${providerOrName}-${name}` : `vercel-${providerOrName}`;
20
+ return JSON.parse(readFileSync(join(DEMOS_DIR, `${file}.json`), 'utf8'));
21
+ }
22
+ export async function runDemo(args, flags, speed) {
23
+ const [first = 'success', second] = args;
24
+ const providerId = DEMOS[first] ? first : 'vercel';
25
+ const name = DEMOS[first] ? (second ?? DEMOS[first][0]) : first;
26
+ if (!DEMOS[providerId].includes(name)) {
27
+ say(`Unknown demo "${args.join(' ')}". Try one of: ${DEMO_LIST}`);
28
+ return 2;
29
+ }
30
+ const t = loadDemo(providerId, name);
31
+ const provider = providers[t.provider];
32
+ say(`Demo: ${t.description} Recorded output; no account or network needed.`);
33
+ return observe({
34
+ provider,
35
+ command: t.command,
36
+ cwd: t.cwd,
37
+ mode: 'demo',
38
+ flags,
39
+ save: true,
40
+ waitForBrowser: true,
41
+ run: (session, term) => play(t, session, speed, term),
42
+ });
43
+ }
44
+ /** Plays a transcript to the terminal and the session. Ctrl+C stops playback. */
45
+ export function play(t, session, speed, term) {
46
+ const write = (d) => (term ? term.write(d) : process.stdout.write(d));
47
+ return new Promise((resolve) => {
48
+ let i = 0;
49
+ let timer;
50
+ const start = Date.now();
51
+ const onSigint = () => {
52
+ if (timer)
53
+ clearTimeout(timer);
54
+ process.off('SIGINT', onSigint);
55
+ write('^C\r\n');
56
+ session.write('^C\r\n');
57
+ resolve({ exitCode: 130, signal: null, interrupted: true });
58
+ };
59
+ process.on('SIGINT', onSigint);
60
+ const step = () => {
61
+ while (i < t.chunks.length && t.chunks[i].t / speed <= Date.now() - start) {
62
+ const { d } = t.chunks[i++];
63
+ write(d);
64
+ session.write(d);
65
+ }
66
+ if (i >= t.chunks.length) {
67
+ process.off('SIGINT', onSigint);
68
+ resolve({ exitCode: t.exitCode, signal: t.signal, interrupted: t.interrupted });
69
+ return;
70
+ }
71
+ timer = setTimeout(step, Math.max(0, t.chunks[i].t / speed - (Date.now() - start)));
72
+ };
73
+ step();
74
+ });
75
+ }
@@ -0,0 +1,137 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { terminalMode, which } from '../core/launch.js';
3
+ import { Redactor } from '../core/redact.js';
4
+ import { startServer } from '../core/server.js';
5
+ import { Session } from '../core/session.js';
6
+ import { RunRecorder, saveFixPrompt } from '../core/store.js';
7
+ import { style } from '../tui/ansi.js';
8
+ import { LivePanel, panelSupported } from '../tui/panel.js';
9
+ import { summary } from '../tui/render.js';
10
+ export const DEFAULT_FLAGS = { web: false, open: true, once: false, panel: true };
11
+ const err = style(!!process.stderr.isTTY && !process.env.NO_COLOR);
12
+ export const say = (msg) => process.stderr.write(`${err.dim('[unfold]')} ${msg}\n`);
13
+ export async function observe(opts) {
14
+ const session = new Session({
15
+ provider: opts.provider,
16
+ command: opts.command,
17
+ cwd: opts.cwd,
18
+ mode: opts.mode,
19
+ // Secret-looking env values are matched in memory only; the environment is never saved.
20
+ redactor: new Redactor({ env: opts.mode === 'live' ? process.env : {} }),
21
+ // Only real runs write files; replays and `show --web` reuse what's saved.
22
+ saveFixPrompt: opts.save ? saveFixPrompt : undefined,
23
+ });
24
+ let recorder;
25
+ if (opts.save) {
26
+ recorder = new RunRecorder(session.runId);
27
+ session.subscribe((event) => recorder.write(event));
28
+ }
29
+ session.start();
30
+ if (recorder)
31
+ session.state.savedTo = recorder.path;
32
+ let server;
33
+ if (opts.flags.web) {
34
+ server = await startServer(session, opts.flags.port);
35
+ say(`Flow view in your browser: ${server.url}`);
36
+ if (opts.flags.open)
37
+ openBrowser(server.url);
38
+ if (opts.waitForBrowser && opts.flags.open)
39
+ await server.waitForClient(6000);
40
+ }
41
+ // The live flow panel, pinned below the command's own output.
42
+ const rowsListeners = new Set();
43
+ const panel =
44
+ // The panel needs to control the child's terminal size, which only node-pty allows.
45
+ opts.flags.panel && panelSupported() && (opts.mode !== 'live' || terminalMode() === 'pty')
46
+ ? new LivePanel({ title: [opts.command[0].split('/').pop(), ...opts.command.slice(1)].join(' '), onRowsChange: (r) => rowsListeners.forEach((fn) => fn(r)) })
47
+ : undefined;
48
+ if (panel) {
49
+ panel.update(session.state);
50
+ session.subscribe((_e, state) => panel.update(state));
51
+ panel.start();
52
+ }
53
+ const show = (chunk) => {
54
+ process.stdout.write(chunk);
55
+ panel?.afterChildOutput(chunk);
56
+ };
57
+ // Structured CLIs (JSON events) are shown as readable lines, not raw output.
58
+ const formatted = !!opts.provider.createPresenter;
59
+ if (formatted) {
60
+ let shown = 0;
61
+ session.subscribe((e) => {
62
+ if (e.type !== 'log' || e.line.seq <= shown)
63
+ return;
64
+ shown = e.line.seq;
65
+ show(e.line.text.replace(/\r?\n/g, '\r\n') + '\r\n');
66
+ });
67
+ }
68
+ const term = {
69
+ write(chunk) {
70
+ if (!formatted)
71
+ show(chunk);
72
+ },
73
+ rows: () => panel?.childRows ?? (process.stdout.rows || 30),
74
+ onRowsChange: (fn) => rowsListeners.add(fn),
75
+ };
76
+ let exit;
77
+ try {
78
+ exit = await opts.run(session, term);
79
+ }
80
+ finally {
81
+ panel?.stop();
82
+ }
83
+ session.end(exit.exitCode, exit.signal, exit.interrupted);
84
+ process.stderr.write(summary(session.state, session.logs, { cols: process.stderr.columns, s: err, webUrl: server?.url }));
85
+ if (session.state.fix && opts.flags.fix)
86
+ await startAgent(opts.flags.fix, session.state.fix.prompt, opts);
87
+ if (server) {
88
+ if (opts.flags.once) {
89
+ await new Promise((r) => setTimeout(r, 300));
90
+ }
91
+ else {
92
+ say(err.dim('Still serving the browser view. Press Ctrl+C to exit.'));
93
+ await new Promise((resolve) => {
94
+ process.stdin.pause();
95
+ process.once('SIGINT', () => resolve());
96
+ process.once('SIGTERM', () => resolve());
97
+ });
98
+ }
99
+ await server.close();
100
+ }
101
+ return exit.exitCode ?? (session.state.outcome === 'interrupted' ? 130 : 1);
102
+ }
103
+ /** Hands the terminal to a coding agent, started with the fix prompt, in the project folder. */
104
+ async function startAgent(agent, prompt, opts) {
105
+ if (opts.mode === 'demo') {
106
+ say(`--fix is ignored for demos: the demo project isn't on your machine. Use the command above in a real project.`);
107
+ return;
108
+ }
109
+ const bin = which(agent);
110
+ if (!bin) {
111
+ say(`Could not find "${agent}" on your PATH, so it wasn't started. The prompt is saved above.`);
112
+ return;
113
+ }
114
+ say(`Starting ${agent === 'claude' ? 'Claude Code' : 'Codex'} with the fix prompt in ${opts.cwd}…\n`);
115
+ await new Promise((resolve) => {
116
+ const child = spawn(bin, [prompt], { cwd: opts.cwd, stdio: 'inherit' });
117
+ // Ctrl+C belongs to the agent while it runs.
118
+ const ignore = () => { };
119
+ process.on('SIGINT', ignore);
120
+ child.on('error', (e) => say(`Could not start ${agent}: ${e.message}`));
121
+ child.on('close', () => {
122
+ process.off('SIGINT', ignore);
123
+ resolve();
124
+ });
125
+ });
126
+ }
127
+ export function openBrowser(url) {
128
+ const [cmd, args] = process.platform === 'darwin' ? ['open', [url]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : ['xdg-open', [url]];
129
+ try {
130
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
131
+ child.on('error', () => say(`Open ${url} in your browser.`));
132
+ child.unref();
133
+ }
134
+ catch {
135
+ say(`Open ${url} in your browser.`);
136
+ }
137
+ }