@viraatdas/rudder 1.0.5 → 1.0.7

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 (45) hide show
  1. package/README.md +369 -23
  2. package/dist/agent-attention.d.ts +5 -0
  3. package/dist/agent-attention.js +54 -0
  4. package/dist/agent-attention.js.map +1 -0
  5. package/dist/backends.js +58 -10
  6. package/dist/backends.js.map +1 -1
  7. package/dist/brain.js +3 -3
  8. package/dist/brain.js.map +1 -1
  9. package/dist/cloud.d.ts +9 -0
  10. package/dist/cloud.js +2255 -0
  11. package/dist/cloud.js.map +1 -0
  12. package/dist/main.js +112 -2
  13. package/dist/main.js.map +1 -1
  14. package/dist/migration.d.ts +82 -0
  15. package/dist/migration.js +174 -0
  16. package/dist/migration.js.map +1 -0
  17. package/dist/native/rudder-native +0 -0
  18. package/dist/native-agents.d.ts +1 -0
  19. package/dist/native-agents.js +126 -8
  20. package/dist/native-agents.js.map +1 -1
  21. package/dist/plan-mode.d.ts +2 -0
  22. package/dist/plan-mode.js +22 -0
  23. package/dist/plan-mode.js.map +1 -0
  24. package/dist/repl.js +11 -3
  25. package/dist/repl.js.map +1 -1
  26. package/dist/run-manager.d.ts +11 -1
  27. package/dist/run-manager.js +164 -19
  28. package/dist/run-manager.js.map +1 -1
  29. package/dist/state.d.ts +10 -1
  30. package/dist/state.js +51 -1
  31. package/dist/state.js.map +1 -1
  32. package/dist/task-summary.d.ts +5 -0
  33. package/dist/task-summary.js +128 -0
  34. package/dist/task-summary.js.map +1 -0
  35. package/dist/tmux-dashboard.js +248 -64
  36. package/dist/tmux-dashboard.js.map +1 -1
  37. package/dist/tmux.js +1 -0
  38. package/dist/tmux.js.map +1 -1
  39. package/dist/tui.js +228 -46
  40. package/dist/tui.js.map +1 -1
  41. package/dist/types.d.ts +23 -0
  42. package/dist/util.d.ts +1 -0
  43. package/dist/util.js +23 -1
  44. package/dist/util.js.map +1 -1
  45. package/package.json +4 -2
package/dist/cloud.js ADDED
@@ -0,0 +1,2255 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import fsp from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { WebSocket } from "ws";
7
+ import { currentBranch, currentCommit, findRepoRoot } from "./git.js";
8
+ import { applyDefaultDecisions, buildFreshHandoffPrompt, cloudWorktreeAbsolutePath, findMigrationCandidates, migrationSummary, summaryAsJson, } from "./migration.js";
9
+ import { cloudAuthPath } from "./state.js";
10
+ import { ensureDir, commandExists, expandHome, isTty, newRunId, nowIso, pathExists, promptConfirm, promptText, promptSelect, promptSecret, readJson, runCommand, shortenHome, shellQuote, writeJson, } from "./util.js";
11
+ const DEFAULT_LOGIN_INTERVAL_MS = 2000;
12
+ const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
13
+ const DEFAULT_CLOUD_URL = "https://rudder-cloud-control.fly.dev";
14
+ const GITHUB_CLI_CLIENT_ID = "178c6fc778ccc68e1d6a";
15
+ const MAX_HOME_SECRET_SCAN_BYTES = 1024 * 1024;
16
+ const DEFAULT_HOME_PATHS = [
17
+ // Agent CLIs
18
+ "~/.claude/.credentials.json",
19
+ "~/.claude/settings.json",
20
+ "~/.claude/CLAUDE.md",
21
+ "~/.claude.json",
22
+ "~/.codex/auth.json",
23
+ "~/.codex/config.toml",
24
+ "~/.codex/AGENTS.md",
25
+ "~/.codex/hooks.json",
26
+ "~/.codex/rules",
27
+ // Git + GitHub
28
+ "~/.gitconfig",
29
+ "~/.config/gh",
30
+ // Shell rc so PATH/aliases/env exports come along
31
+ "~/.zshrc",
32
+ "~/.zprofile",
33
+ "~/.bashrc",
34
+ "~/.bash_profile",
35
+ "~/.profile",
36
+ "~/.envrc",
37
+ // Package managers
38
+ "~/.npmrc",
39
+ "~/.yarnrc",
40
+ "~/.yarnrc.yml",
41
+ "~/.cargo/config",
42
+ "~/.cargo/config.toml",
43
+ // Cloud provider CLIs
44
+ "~/.vercel",
45
+ "~/.config/vercel",
46
+ "~/.aws/config",
47
+ "~/.aws/credentials",
48
+ "~/.config/gcloud/configurations",
49
+ "~/.config/gcloud/active_config",
50
+ "~/.config/gcloud/credentials.db",
51
+ "~/.config/gcloud/access_tokens.db",
52
+ "~/.config/gcloud/application_default_credentials.json",
53
+ "~/.kube/config",
54
+ // Netrc for tools that auth via netrc
55
+ "~/.netrc",
56
+ // Rudder + Hunk
57
+ "~/.config/hunk",
58
+ ];
59
+ // Paths or path components that should never be uploaded under any
60
+ // circumstance — even if a user's DEFAULT_HOME_PATHS entry references them.
61
+ // Specifically leaving out .aws/.kube/.docker so the corresponding configs
62
+ // can ship; .ssh/.gnupg/keychains stay blocked because they contain
63
+ // private key material that isn't recoverable if leaked.
64
+ const SECRET_PATH_PARTS = new Set([
65
+ ".ssh",
66
+ ".gnupg",
67
+ "keychains",
68
+ ]);
69
+ const BULKY_HOME_PATH_PARTS = new Set([
70
+ "archived_sessions",
71
+ "backups",
72
+ "cache",
73
+ "file-history",
74
+ "log",
75
+ "paste-cache",
76
+ "plugins",
77
+ "projects",
78
+ "session",
79
+ "sessions",
80
+ "shell_snapshots",
81
+ "skills",
82
+ "telemetry",
83
+ "todos",
84
+ "worktrees",
85
+ ]);
86
+ const SECRET_BASENAMES = new Set([
87
+ ".env",
88
+ ".env.local",
89
+ ".env.production",
90
+ ".env.development",
91
+ "id_rsa",
92
+ "id_ed25519",
93
+ "known_hosts",
94
+ ]);
95
+ const BULKY_HOME_BASENAME_PATTERNS = [
96
+ /^history\./,
97
+ /^logs?_/,
98
+ /^state_\d+\.sqlite/,
99
+ /\.sqlite(?:-(?:wal|shm))?$/,
100
+ /\.log$/,
101
+ /\.jsonl$/,
102
+ ];
103
+ export async function runCloudCommand(command, args, options = {}) {
104
+ const subcommand = args[0] ?? "";
105
+ const rest = args.slice(1);
106
+ if (command === "cloud" && subcommand === "help") {
107
+ printCloudHelp();
108
+ return;
109
+ }
110
+ // `rudder cloud` with no further args means "open the cloud workspace for
111
+ // this repo". Explicit subcommands (sail, launch, etc.) keep their old
112
+ // behavior. `rudder sail` and `rudder cloud sail` still launch a sail.
113
+ if (command === "cloud" && subcommand === "") {
114
+ await workspaceCommand([], options);
115
+ return;
116
+ }
117
+ switch (subcommand) {
118
+ case "login":
119
+ await login(options);
120
+ return;
121
+ case "launch":
122
+ await launch(rest, options, "task");
123
+ return;
124
+ case "sail":
125
+ await launch(rest, options);
126
+ return;
127
+ case "byoc":
128
+ await setupByoc(rest, options);
129
+ return;
130
+ case "vm":
131
+ case "byo-vm":
132
+ await launch(rest, options, "task", "byo-vm");
133
+ return;
134
+ case "list":
135
+ case "ls":
136
+ await listSails(options);
137
+ return;
138
+ case "status":
139
+ await status(options);
140
+ return;
141
+ case "logs":
142
+ await logs(rest, options);
143
+ return;
144
+ case "attach":
145
+ await attach(rest, options);
146
+ return;
147
+ case "workspace":
148
+ await workspaceCommand(rest, options);
149
+ return;
150
+ case "onload":
151
+ await onload(rest, options);
152
+ return;
153
+ case "bootstrap":
154
+ await bootstrap(rest, options);
155
+ return;
156
+ case "pause":
157
+ await mutateSail("pause", rest, options);
158
+ return;
159
+ case "resume":
160
+ await mutateSail("resume", rest, options);
161
+ return;
162
+ case "stop":
163
+ await mutateSail("stop", rest, options);
164
+ return;
165
+ case "setup-github":
166
+ await setupOAuthProvider("github", rest, options);
167
+ return;
168
+ case "setup-google":
169
+ await setupOAuthProvider("google", rest, options);
170
+ return;
171
+ case "setup-byoc":
172
+ await setupByoc(rest, options);
173
+ return;
174
+ case "setup-vm":
175
+ await setupByoc(rest, options);
176
+ return;
177
+ case "setup-fly":
178
+ await configureDefaultRuntime("fly", options);
179
+ return;
180
+ case "setup":
181
+ if (rest[0] === "byoc" || rest[0] === "vm" || rest[0] === "byo-vm") {
182
+ await setupByoc(rest.slice(1), options);
183
+ return;
184
+ }
185
+ if (rest[0] === "fly") {
186
+ await configureDefaultRuntime("fly", options);
187
+ return;
188
+ }
189
+ throw new Error("Usage: rudder cloud setup byoc | rudder cloud setup fly");
190
+ case "runtime":
191
+ await runtime(rest, options);
192
+ return;
193
+ default:
194
+ await launch(command === "sail" ? args : [subcommand, ...rest], options);
195
+ return;
196
+ }
197
+ }
198
+ async function login(options) {
199
+ const client = await cloudClient({ requireToken: false });
200
+ const browserLogin = await tryBrowserLogin(client, options).catch((error) => {
201
+ if (!options.json) {
202
+ console.warn(`Browser login unavailable: ${error instanceof Error ? error.message : String(error)}`);
203
+ console.warn("Trying local GitHub auth fallback...");
204
+ }
205
+ return null;
206
+ });
207
+ if (browserLogin?.token || browserLogin?.accessToken) {
208
+ const token = browserLogin.token ?? browserLogin.accessToken;
209
+ if (token) {
210
+ await saveCloudLogin(client, browserLogin, token, options, "browser");
211
+ return;
212
+ }
213
+ }
214
+ const githubLogin = await tryGithubCliLogin(client).catch(() => null);
215
+ if (githubLogin?.token || githubLogin?.accessToken) {
216
+ const token = githubLogin.token ?? githubLogin.accessToken;
217
+ if (token) {
218
+ await saveCloudLogin(client, githubLogin, token, options, "GitHub CLI");
219
+ return;
220
+ }
221
+ }
222
+ const githubDeviceLogin = await tryGithubDeviceLogin(client, options).catch(() => null);
223
+ if (githubDeviceLogin?.token || githubDeviceLogin?.accessToken) {
224
+ const token = githubDeviceLogin.token ?? githubDeviceLogin.accessToken;
225
+ if (token) {
226
+ await saveCloudLogin(client, githubDeviceLogin, token, options, "GitHub device");
227
+ return;
228
+ }
229
+ }
230
+ }
231
+ async function tryBrowserLogin(client, options) {
232
+ const response = await client.request("/api/cli/login", {
233
+ method: "POST",
234
+ body: {
235
+ deviceName: os.hostname(),
236
+ client: "rudder",
237
+ },
238
+ });
239
+ const deviceCode = response.deviceCode;
240
+ const loginUrl = response.loginUrl ?? response.verificationUri ?? withQuery(client.baseUrl, "/cli/login", deviceCode ? { device_code: deviceCode } : {});
241
+ const pollPath = response.pollUrl ?? "/api/cli/login/poll";
242
+ const intervalMs = Math.max(1000, (response.interval ?? DEFAULT_LOGIN_INTERVAL_MS / 1000) * 1000);
243
+ const timeoutMs = Math.max(intervalMs, (response.expiresIn ?? DEFAULT_LOGIN_TIMEOUT_MS / 1000) * 1000);
244
+ console.log(`Opening ${loginUrl}`);
245
+ if (!options.json) {
246
+ openBrowser(loginUrl);
247
+ }
248
+ console.log("Waiting for browser login to complete...");
249
+ const startedAt = Date.now();
250
+ while (Date.now() - startedAt < timeoutMs) {
251
+ await sleep(intervalMs);
252
+ const poll = await pollLogin(client, pollPath, deviceCode);
253
+ const token = poll.token ?? poll.accessToken;
254
+ if (token) {
255
+ return poll;
256
+ }
257
+ if (poll.pending === false) {
258
+ throw new Error("Cloud login was not approved.");
259
+ }
260
+ }
261
+ throw new Error("Timed out waiting for cloud login.");
262
+ }
263
+ async function tryGithubCliLogin(client) {
264
+ if (process.env.RUDDER_SKIP_GH_CLI === "1") {
265
+ return null;
266
+ }
267
+ const gh = await runCommand("gh", ["auth", "token"], { allowFailure: true });
268
+ const token = gh.stdout.trim();
269
+ if (gh.code !== 0 || !token) {
270
+ return null;
271
+ }
272
+ return await client.request("/api/cli/login/github-token", {
273
+ method: "POST",
274
+ body: { token },
275
+ });
276
+ }
277
+ async function tryGithubDeviceLogin(client, options) {
278
+ const start = await githubOAuthRequest("https://github.com/login/device/code", {
279
+ client_id: GITHUB_CLI_CLIENT_ID,
280
+ scope: "read:user user:email",
281
+ });
282
+ if (!start.device_code || !start.user_code || !start.verification_uri) {
283
+ return null;
284
+ }
285
+ if (!options.json) {
286
+ const url = start.verification_uri_complete ?? start.verification_uri;
287
+ console.log(`Opening ${url}`);
288
+ console.log(`GitHub code: ${start.user_code}`);
289
+ openBrowser(url);
290
+ }
291
+ const intervalMs = Math.max(1000, (start.interval ?? 5) * 1000);
292
+ const timeoutMs = Math.max(intervalMs, (start.expires_in ?? 900) * 1000);
293
+ const startedAt = Date.now();
294
+ while (Date.now() - startedAt < timeoutMs) {
295
+ await sleep(intervalMs);
296
+ const poll = await githubOAuthRequest("https://github.com/login/oauth/access_token", {
297
+ client_id: GITHUB_CLI_CLIENT_ID,
298
+ device_code: start.device_code,
299
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
300
+ });
301
+ if (poll.access_token) {
302
+ return await client.request("/api/cli/login/github-token", {
303
+ method: "POST",
304
+ body: { token: poll.access_token },
305
+ });
306
+ }
307
+ if (poll.error === "authorization_pending") {
308
+ continue;
309
+ }
310
+ if (poll.error === "slow_down") {
311
+ await sleep(Math.max(intervalMs, (poll.interval ?? 5) * 1000));
312
+ continue;
313
+ }
314
+ return null;
315
+ }
316
+ return null;
317
+ }
318
+ async function githubOAuthRequest(url, body) {
319
+ const response = await fetch(url, {
320
+ method: "POST",
321
+ headers: {
322
+ Accept: "application/json",
323
+ "Content-Type": "application/json",
324
+ },
325
+ body: JSON.stringify(body),
326
+ });
327
+ const text = await response.text();
328
+ const parsed = text ? parseJson(text) : null;
329
+ if (!response.ok) {
330
+ throw new Error(responseErrorMessage(parsed) ?? text.trim() ?? `${response.status} ${response.statusText}`);
331
+ }
332
+ return parsed;
333
+ }
334
+ async function saveCloudLogin(client, login, token, options, source) {
335
+ const previous = await loadCloudAuth();
336
+ const previousRuntime = previous?.cloudUrl === client.baseUrl ? parseCloudRuntime(previous.defaultRuntime) : undefined;
337
+ const previousByocHost = previous?.cloudUrl === client.baseUrl ? previous.byocSshHost : undefined;
338
+ await saveCloudAuth({
339
+ version: 1,
340
+ token,
341
+ cloudUrl: client.baseUrl,
342
+ defaultRuntime: previousRuntime,
343
+ byocSshHost: previousByocHost,
344
+ accountId: login.accountId,
345
+ email: login.email,
346
+ expiresAt: login.expiresAt ?? (login.expiresIn ? new Date(Date.now() + login.expiresIn * 1000).toISOString() : undefined),
347
+ updatedAt: nowIso(),
348
+ });
349
+ if (options.json) {
350
+ const result = { ok: true, cloudUrl: client.baseUrl, source };
351
+ if (login.email) {
352
+ result.email = login.email;
353
+ }
354
+ if (login.accountId) {
355
+ result.accountId = login.accountId;
356
+ }
357
+ printJson(result);
358
+ }
359
+ else {
360
+ console.log(`Logged in to ${client.baseUrl}${login.email ? ` as ${login.email}` : ""} via ${source}.`);
361
+ }
362
+ }
363
+ async function launch(args, options, mode = "name", explicitRuntime) {
364
+ const raw = args.join(" ").trim();
365
+ const repoRoot = findRepoRoot();
366
+ const snapshot = await createSnapshot(repoRoot, options.homePaths ?? []);
367
+ try {
368
+ const client = await cloudClient({ requireToken: true });
369
+ const runtime = await selectedCloudRuntime(explicitRuntime);
370
+ const task = mode === "task" || runtime === "byo-vm" ? raw : "";
371
+ const name = task ? cloudNameFromTask(task) : raw || randomCloudName();
372
+ const body = {
373
+ repoName: path.basename(repoRoot),
374
+ name,
375
+ snapshot: {
376
+ name: path.basename(snapshot.archivePath),
377
+ contentType: "application/gzip",
378
+ base64: await fsp.readFile(snapshot.archivePath, "base64"),
379
+ manifest: snapshot.manifest,
380
+ },
381
+ };
382
+ if (runtime !== "fly") {
383
+ body.runtime = runtime;
384
+ }
385
+ if (task) {
386
+ body.task = task;
387
+ }
388
+ const result = await client.request("/api/rudder/sail/launch", {
389
+ method: "POST",
390
+ body,
391
+ });
392
+ await printResult(result, options);
393
+ await maybeAutoAttach(result, options);
394
+ }
395
+ finally {
396
+ await fsp.rm(snapshot.tempDir, { recursive: true, force: true });
397
+ }
398
+ }
399
+ async function onload(args, options) {
400
+ const runId = args[0];
401
+ const repoRoot = findRepoRoot();
402
+ const runRecord = runId
403
+ ? await readJson(path.join(repoRoot, ".rudder", "runs", runId, "run.json"))
404
+ : null;
405
+ const worktreePath = runRecord && typeof runRecord === "object" && !Array.isArray(runRecord)
406
+ ? runRecord.worktree
407
+ : undefined;
408
+ const sourceRoot = worktreePath && typeof worktreePath === "object" && !Array.isArray(worktreePath)
409
+ ? worktreePath.path
410
+ : undefined;
411
+ const snapshotRoot = sourceRoot && await pathExists(sourceRoot) ? sourceRoot : repoRoot;
412
+ const snapshot = await createSnapshot(snapshotRoot, options.homePaths ?? [], { includeRudderState: !runId });
413
+ try {
414
+ const client = await cloudClient({ requireToken: true });
415
+ const runtime = await selectedCloudRuntime();
416
+ const name = runId ? undefined : `workspace-${path.basename(repoRoot)}`;
417
+ const body = {
418
+ repoName: path.basename(repoRoot),
419
+ run: runRecord ?? null,
420
+ workspace: !runId,
421
+ snapshot: {
422
+ name: path.basename(snapshot.archivePath),
423
+ contentType: "application/gzip",
424
+ base64: await fsp.readFile(snapshot.archivePath, "base64"),
425
+ manifest: snapshot.manifest,
426
+ },
427
+ };
428
+ if (runId) {
429
+ body.runId = runId;
430
+ }
431
+ else {
432
+ body.name = name ?? `workspace-${path.basename(repoRoot)}`;
433
+ }
434
+ if (runtime !== "fly") {
435
+ body.runtime = runtime;
436
+ }
437
+ const result = await client.request("/api/rudder/sail/onload", {
438
+ method: "POST",
439
+ body,
440
+ });
441
+ await printResult(result, options);
442
+ await maybeAutoAttach(result, options);
443
+ }
444
+ finally {
445
+ await fsp.rm(snapshot.tempDir, { recursive: true, force: true });
446
+ }
447
+ }
448
+ async function logs(args, options) {
449
+ const sailId = args[0];
450
+ if (!sailId) {
451
+ throw new Error("Usage: rudder cloud logs <id>");
452
+ }
453
+ const client = await cloudClient({ requireToken: true });
454
+ const result = await client.request("/api/rudder/sail", { method: "GET" });
455
+ const sails = Array.isArray(result)
456
+ ? result
457
+ : result && typeof result === "object" && !Array.isArray(result) && Array.isArray(result.sails)
458
+ ? result.sails
459
+ : [];
460
+ const match = sails.find((item) => item && typeof item === "object" && !Array.isArray(item) && item.id === sailId);
461
+ if (!match) {
462
+ throw new Error(`Cloud worker not found: ${sailId}`);
463
+ }
464
+ if (options.json) {
465
+ printJson(match);
466
+ return;
467
+ }
468
+ console.log("Cloud log streaming is not available yet.");
469
+ console.log("Worker status:");
470
+ printSailList([match]);
471
+ }
472
+ async function listSails(options) {
473
+ const client = await cloudClient({ requireToken: true });
474
+ const result = await client.request("/api/rudder/sail", { method: "GET" });
475
+ await printResult(result, options);
476
+ }
477
+ async function status(options) {
478
+ const client = await cloudClient({ requireToken: true });
479
+ const state = await loadCloudAuth();
480
+ const runtime = await selectedCloudRuntime();
481
+ const sails = await client.request("/api/rudder/sail", { method: "GET" });
482
+ const sailRows = Array.isArray(sails)
483
+ ? sails
484
+ : sails && typeof sails === "object" && !Array.isArray(sails) && Array.isArray(sails.sails)
485
+ ? sails.sails
486
+ : [];
487
+ const sailCount = sailRows.length;
488
+ const result = {
489
+ ok: true,
490
+ cloudUrl: client.baseUrl,
491
+ runtime,
492
+ sails: sailCount,
493
+ };
494
+ if (state?.cloudUrl === client.baseUrl) {
495
+ if (state.email) {
496
+ result.email = state.email;
497
+ }
498
+ if (state.accountId) {
499
+ result.accountId = state.accountId;
500
+ }
501
+ if (state.byocSshHost) {
502
+ result.byocSshHost = state.byocSshHost;
503
+ }
504
+ }
505
+ if (options.json) {
506
+ printJson(result);
507
+ return;
508
+ }
509
+ console.log(`Logged in to ${client.baseUrl}${state?.email ? ` as ${state.email}` : ""}.`);
510
+ console.log(`Runtime: ${runtime}`);
511
+ if (state?.byocSshHost) {
512
+ console.log(`BYOC SSH host: ${state.byocSshHost}`);
513
+ }
514
+ console.log(`Cloud workers: ${sailCount}`);
515
+ }
516
+ async function bootstrap(args, options) {
517
+ const sailId = args[0];
518
+ if (!sailId) {
519
+ throw new Error("Missing sail id. Usage: rudder cloud bootstrap <id>");
520
+ }
521
+ const client = await cloudClient({ requireToken: true });
522
+ const result = await client.request(`/api/rudder/sail/${encodeURIComponent(sailId)}/bootstrap`, {
523
+ method: "POST",
524
+ body: {},
525
+ });
526
+ await printResult(result, options);
527
+ }
528
+ async function mutateSail(action, args, options) {
529
+ const sailId = args[0];
530
+ if (!sailId) {
531
+ throw new Error(`Missing sail id. Usage: rudder sail ${action} <id>`);
532
+ }
533
+ const client = await cloudClient({ requireToken: true });
534
+ const result = await client.request(`/api/rudder/sail/${encodeURIComponent(sailId)}/${action}`, {
535
+ method: "POST",
536
+ body: args.length > 1 ? { args: args.slice(1) } : {},
537
+ });
538
+ await printResult(result, options);
539
+ }
540
+ async function setupOAuthProvider(provider, args, options) {
541
+ const envPrefix = provider === "github" ? "RUDDER_GITHUB" : "RUDDER_GOOGLE";
542
+ const clientId = args[0]?.trim() || process.env[`${envPrefix}_CLIENT_ID`]?.trim();
543
+ const clientSecret = process.env[`${envPrefix}_CLIENT_SECRET`]?.trim() ||
544
+ args[1]?.trim() ||
545
+ await promptSecret(`${provider === "github" ? "GitHub App" : "Google OAuth"} client secret`);
546
+ if (!clientId || !clientSecret) {
547
+ throw new Error([
548
+ `Missing ${provider === "github" ? "GitHub" : "Google"} OAuth credentials.`,
549
+ `Usage: rudder cloud setup-${provider} <client-id>`,
550
+ `Or set ${envPrefix}_CLIENT_ID and ${envPrefix}_CLIENT_SECRET.`,
551
+ ].join("\n"));
552
+ }
553
+ const client = await cloudClient({ requireToken: true });
554
+ const result = await client.request(`/api/rudder/setup/${provider}`, {
555
+ method: "POST",
556
+ body: {
557
+ clientId,
558
+ clientSecret,
559
+ },
560
+ });
561
+ await printResult(result, options);
562
+ }
563
+ async function setupByoc(args, options) {
564
+ const sshConfigPath = path.join(os.homedir(), ".ssh", "config");
565
+ const configuredHosts = await listSshConfigHosts(sshConfigPath);
566
+ const host = (options.sshHost ?? args.join(" ").trim()) || await chooseByocHost(configuredHosts);
567
+ if (!host) {
568
+ throw new Error([
569
+ "Missing BYOC SSH host.",
570
+ "Add your workstation/server to ~/.ssh/config, then run:",
571
+ "",
572
+ " rudder cloud byoc <ssh-host>",
573
+ "",
574
+ "Example ~/.ssh/config:",
575
+ " Host rudder-workstation",
576
+ " HostName 203.0.113.10",
577
+ " User ubuntu",
578
+ " IdentityFile ~/.ssh/id_ed25519",
579
+ "",
580
+ configuredHosts.length
581
+ ? `Detected SSH hosts: ${configuredHosts.slice(0, 12).join(", ")}`
582
+ : `No usable hosts found in ${shortenHome(sshConfigPath)}.`,
583
+ ].join("\n"));
584
+ }
585
+ const configMentionsHost = configuredHosts.includes(host) || await sshConfigMentions(sshConfigPath, host);
586
+ const diagnostics = await checkByocHost(host);
587
+ const client = await cloudClient({ requireToken: true });
588
+ const state = await loadCloudAuth();
589
+ if (!state || state.cloudUrl !== client.baseUrl) {
590
+ throw new Error("Not logged in to this Rudder Cloud control plane. Run `rudder login` first.");
591
+ }
592
+ await saveCloudAuth({
593
+ ...state,
594
+ defaultRuntime: state.defaultRuntime === "byo-vm" ? "fly" : state.defaultRuntime,
595
+ byocSshHost: host,
596
+ updatedAt: nowIso(),
597
+ });
598
+ if (options.json) {
599
+ const result = {
600
+ ok: true,
601
+ cloudUrl: client.baseUrl,
602
+ byocSshHost: host,
603
+ };
604
+ const defaultRuntime = state.defaultRuntime === "byo-vm" ? "fly" : state.defaultRuntime;
605
+ if (defaultRuntime) {
606
+ result.defaultRuntime = defaultRuntime;
607
+ }
608
+ printJson(result);
609
+ return;
610
+ }
611
+ console.log(`Rudder BYOC host set to ${host}.`);
612
+ console.log("Plain `rudder cloud` and dashboard `/cloud` continue to use Fly by default.");
613
+ console.log("Use `rudder cloud vm <task>` when you want to run a task on this BYOC host.");
614
+ if (!configMentionsHost) {
615
+ console.log(`\nNote: ${shortenHome(sshConfigPath)} does not appear to define Host ${host}.`);
616
+ console.log("Rudder can still use it if SSH resolves it, but a ~/.ssh/config entry is recommended:");
617
+ console.log(` Host ${host}`);
618
+ console.log(" HostName <server-ip-or-dns>");
619
+ console.log(" User <user>");
620
+ console.log(" IdentityFile ~/.ssh/<private-key>");
621
+ }
622
+ if (diagnostics.ok) {
623
+ console.log(`SSH check passed for ${host}. Docker is available on the BYOC host.`);
624
+ }
625
+ else {
626
+ console.log(`\nSSH check did not fully pass for ${host}: ${diagnostics.message}`);
627
+ console.log("Fix SSH/Docker before launching, or run the printed Docker command manually on that host.");
628
+ }
629
+ }
630
+ async function chooseByocHost(hosts) {
631
+ if (hosts.length === 0) {
632
+ return await promptText("SSH host from ~/.ssh/config");
633
+ }
634
+ return await promptSelect("Choose a BYOC SSH host from ~/.ssh/config", hosts.slice(0, 24).map((host) => ({ value: host, label: host })), hosts[0]);
635
+ }
636
+ async function configureDefaultRuntime(runtime, options, byocSshHost) {
637
+ const client = await cloudClient({ requireToken: true });
638
+ const state = await loadCloudAuth();
639
+ if (!state || state.cloudUrl !== client.baseUrl) {
640
+ throw new Error("Not logged in to this Rudder Cloud control plane. Run `rudder login` first.");
641
+ }
642
+ await saveCloudAuth({
643
+ ...state,
644
+ defaultRuntime: runtime,
645
+ byocSshHost: runtime === "byo-vm" ? byocSshHost ?? state.byocSshHost : undefined,
646
+ updatedAt: nowIso(),
647
+ });
648
+ const result = {
649
+ ok: true,
650
+ cloudUrl: client.baseUrl,
651
+ defaultRuntime: runtime,
652
+ };
653
+ const savedByocHost = byocSshHost ?? state.byocSshHost;
654
+ if (runtime === "byo-vm" && savedByocHost) {
655
+ result.byocSshHost = savedByocHost;
656
+ }
657
+ const envRuntime = envCloudRuntime();
658
+ if (envRuntime) {
659
+ result.envOverride = envRuntime;
660
+ }
661
+ if (options.json) {
662
+ printJson(result);
663
+ return;
664
+ }
665
+ console.log(`Rudder Cloud runtime set to ${runtime}.`);
666
+ if (runtime === "byo-vm") {
667
+ const host = byocSshHost ?? state.byocSshHost;
668
+ console.log("Future `rudder cloud <task>` and `/sail <task>` launches will prepare a BYOC worker instead of creating a Fly Machine.");
669
+ console.log(host
670
+ ? `Rudder will try to start the worker over SSH on ${host}.`
671
+ : "Run `rudder cloud byoc <ssh-host>` to let Rudder start workers over SSH.");
672
+ }
673
+ else {
674
+ console.log("Future `rudder cloud <task>` and `/sail <task>` launches will create Fly Machines.");
675
+ }
676
+ if (envRuntime) {
677
+ console.log(`RUDDER_CLOUD_RUNTIME=${envRuntime} is set and will override this saved default.`);
678
+ }
679
+ }
680
+ async function runtime(args, options) {
681
+ const next = args[0] ? parseCloudRuntime(args[0]) : undefined;
682
+ if (args[0] && !next) {
683
+ throw new Error("Runtime must be `fly`, `byoc`, or `byo-vm`.");
684
+ }
685
+ if (next) {
686
+ await configureDefaultRuntime(next, options);
687
+ return;
688
+ }
689
+ const client = await cloudClient({ requireToken: true });
690
+ const current = await selectedCloudRuntime();
691
+ const state = await loadCloudAuth();
692
+ const savedRuntime = parseCloudRuntime(state?.defaultRuntime);
693
+ const result = {
694
+ cloudUrl: client.baseUrl,
695
+ runtime: current,
696
+ };
697
+ const envRuntime = envCloudRuntime();
698
+ if (state?.cloudUrl === client.baseUrl && savedRuntime) {
699
+ result.savedDefaultRuntime = savedRuntime;
700
+ }
701
+ if (state?.cloudUrl === client.baseUrl && state.byocSshHost) {
702
+ result.byocSshHost = state.byocSshHost;
703
+ }
704
+ if (envRuntime) {
705
+ result.envOverride = envRuntime;
706
+ }
707
+ if (options.json) {
708
+ printJson(result);
709
+ }
710
+ else {
711
+ console.log(`Rudder Cloud runtime: ${current}`);
712
+ if (envRuntime) {
713
+ console.log(`Set by RUDDER_CLOUD_RUNTIME=${envRuntime}.`);
714
+ }
715
+ else if (state?.cloudUrl === client.baseUrl && savedRuntime) {
716
+ console.log("Set in local Rudder Cloud config.");
717
+ }
718
+ else {
719
+ console.log("Using default Fly Machines runtime.");
720
+ }
721
+ if (state?.cloudUrl === client.baseUrl && state.byocSshHost) {
722
+ console.log(`BYOC SSH host: ${state.byocSshHost}`);
723
+ }
724
+ }
725
+ }
726
+ async function sshConfigMentions(configPath, host) {
727
+ const text = await fsp.readFile(configPath, "utf8").catch(() => "");
728
+ if (!text.trim()) {
729
+ return false;
730
+ }
731
+ const target = host.toLowerCase();
732
+ for (const line of text.split(/\r?\n/)) {
733
+ const trimmed = line.trim();
734
+ if (!trimmed || trimmed.startsWith("#")) {
735
+ continue;
736
+ }
737
+ const match = /^Host\s+(.+)$/i.exec(trimmed);
738
+ if (!match) {
739
+ continue;
740
+ }
741
+ const patterns = match[1].split(/\s+/).map((part) => part.toLowerCase());
742
+ if (patterns.includes(target)) {
743
+ return true;
744
+ }
745
+ }
746
+ return false;
747
+ }
748
+ async function listSshConfigHosts(configPath) {
749
+ const text = await fsp.readFile(configPath, "utf8").catch(() => "");
750
+ const hosts = [];
751
+ const seen = new Set();
752
+ for (const line of text.split(/\r?\n/)) {
753
+ const trimmed = line.trim();
754
+ if (!trimmed || trimmed.startsWith("#")) {
755
+ continue;
756
+ }
757
+ const match = /^Host\s+(.+)$/i.exec(trimmed);
758
+ if (!match) {
759
+ continue;
760
+ }
761
+ for (const host of match[1].split(/\s+/)) {
762
+ if (!host || host.includes("*") || host.includes("?") || host.startsWith("!")) {
763
+ continue;
764
+ }
765
+ if (seen.has(host)) {
766
+ continue;
767
+ }
768
+ seen.add(host);
769
+ hosts.push(host);
770
+ }
771
+ }
772
+ return hosts;
773
+ }
774
+ async function checkByocHost(host) {
775
+ if (!commandExists("ssh")) {
776
+ return { ok: false, message: "ssh is not installed or not on PATH" };
777
+ }
778
+ const result = await runCommand("ssh", [
779
+ "-o",
780
+ "BatchMode=yes",
781
+ "-o",
782
+ "ConnectTimeout=8",
783
+ host,
784
+ "command -v docker >/dev/null && docker info >/dev/null 2>&1",
785
+ ], { allowFailure: true });
786
+ if (result.code === 0) {
787
+ return { ok: true, message: "ok" };
788
+ }
789
+ const detail = (result.stderr || result.stdout || `ssh exited ${result.code}`).trim();
790
+ return { ok: false, message: detail };
791
+ }
792
+ async function startByocWorkerOverSsh(host, bootstrapCommand) {
793
+ if (!commandExists("ssh")) {
794
+ throw new Error("ssh is not installed or not on PATH");
795
+ }
796
+ const remoteCommand = [
797
+ "mkdir -p ~/.rudder/byoc",
798
+ `nohup sh -lc ${shellQuote(nonInteractiveDockerCommand(bootstrapCommand))} > ~/.rudder/byoc/worker.log 2>&1 < /dev/null &`,
799
+ ].join(" && ");
800
+ await runCommand("ssh", [
801
+ "-o",
802
+ "BatchMode=yes",
803
+ "-o",
804
+ "ConnectTimeout=10",
805
+ host,
806
+ remoteCommand,
807
+ ]);
808
+ }
809
+ function nonInteractiveDockerCommand(command) {
810
+ return command
811
+ .replace(/\bdocker run --rm -it\b/g, "docker run --rm")
812
+ .replace(/\bdocker run --rm -i -t\b/g, "docker run --rm")
813
+ .replace(/\bdocker run --rm -t -i\b/g, "docker run --rm");
814
+ }
815
+ async function cloudClient(options) {
816
+ const baseUrl = normalizeCloudUrl(process.env.RUDDER_CLOUD_URL);
817
+ const state = await loadCloudAuth();
818
+ const envToken = process.env.RUDDER_CLOUD_TOKEN?.trim();
819
+ const token = envToken || (state?.cloudUrl === baseUrl ? state.token : undefined);
820
+ if (options.requireToken && !token) {
821
+ throw new Error("Not logged in to Rudder Cloud. Run `rudder login` first.");
822
+ }
823
+ return {
824
+ baseUrl,
825
+ async request(pathOrUrl, init) {
826
+ const url = pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")
827
+ ? pathOrUrl
828
+ : new URL(pathOrUrl, `${baseUrl}/`).toString();
829
+ const headers = {
830
+ Accept: "application/json",
831
+ };
832
+ let body;
833
+ if (init.body !== undefined) {
834
+ headers["Content-Type"] = "application/json";
835
+ body = JSON.stringify(init.body);
836
+ }
837
+ if (token) {
838
+ headers.Authorization = `Bearer ${token}`;
839
+ }
840
+ const response = await fetch(url, {
841
+ method: init.method,
842
+ headers,
843
+ body,
844
+ });
845
+ const text = await response.text();
846
+ const parsed = text ? parseJson(text) : null;
847
+ if (!response.ok) {
848
+ const message = responseErrorMessage(parsed) ?? text.trim() ?? `${response.status} ${response.statusText}`;
849
+ throw new Error(`Rudder Cloud request failed: ${message}`);
850
+ }
851
+ return parsed;
852
+ },
853
+ };
854
+ }
855
+ function normalizeCloudUrl(raw) {
856
+ const value = raw?.trim() || DEFAULT_CLOUD_URL;
857
+ if (!value) {
858
+ throw new Error("RUDDER_CLOUD_URL is not configured. Set it to your Rudder Cloud control plane URL.");
859
+ }
860
+ try {
861
+ const url = new URL(value);
862
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
863
+ throw new Error("bad protocol");
864
+ }
865
+ url.hash = "";
866
+ url.search = "";
867
+ return url.toString().replace(/\/$/, "");
868
+ }
869
+ catch {
870
+ throw new Error("RUDDER_CLOUD_URL must be a valid http(s) URL.");
871
+ }
872
+ }
873
+ async function selectedCloudRuntime(explicit) {
874
+ if (explicit) {
875
+ return explicit;
876
+ }
877
+ const envRuntime = envCloudRuntime();
878
+ if (envRuntime) {
879
+ return envRuntime;
880
+ }
881
+ const baseUrl = normalizeCloudUrl(process.env.RUDDER_CLOUD_URL);
882
+ const state = await loadCloudAuth();
883
+ const savedRuntime = parseCloudRuntime(state?.defaultRuntime);
884
+ return state?.cloudUrl === baseUrl && savedRuntime ? savedRuntime : "fly";
885
+ }
886
+ function parseCloudRuntime(raw) {
887
+ const value = raw?.trim().toLowerCase();
888
+ if (!value) {
889
+ return undefined;
890
+ }
891
+ if (value === "fly" || value === "fly-machine" || value === "fly-machines") {
892
+ return "fly";
893
+ }
894
+ if (value === "byo" || value === "byoc" || value === "byo-vm" || value === "manual" || value === "self-hosted" || value === "vm") {
895
+ return "byo-vm";
896
+ }
897
+ return undefined;
898
+ }
899
+ function envCloudRuntime() {
900
+ const runtime = parseCloudRuntime(process.env.RUDDER_CLOUD_RUNTIME);
901
+ if (process.env.RUDDER_CLOUD_RUNTIME?.trim() && !runtime) {
902
+ throw new Error("RUDDER_CLOUD_RUNTIME must be `fly`, `byoc`, or `byo-vm`.");
903
+ }
904
+ return runtime;
905
+ }
906
+ async function pollLogin(client, pollPath, deviceCode) {
907
+ if (pollPath.startsWith("http://") || pollPath.startsWith("https://") || !deviceCode) {
908
+ return await client.request(pollPath, { method: "GET" });
909
+ }
910
+ return await client.request(pollPath, {
911
+ method: "POST",
912
+ body: { deviceCode },
913
+ });
914
+ }
915
+ async function loadCloudAuth() {
916
+ const state = await readJson(cloudAuthPath());
917
+ return state?.version === 1 && typeof state.token === "string" ? state : null;
918
+ }
919
+ async function saveCloudAuth(state) {
920
+ await writeJson(cloudAuthPath(), state, { mode: 0o600 });
921
+ }
922
+ async function createSnapshot(repoRoot, requestedHomePaths, options = {}) {
923
+ const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), "rudder-cloud-"));
924
+ const stageDir = path.join(tempDir, "snapshot");
925
+ const repoStage = path.join(stageDir, "repo");
926
+ const homeStage = path.join(stageDir, "home");
927
+ await ensureDir(repoStage);
928
+ await copyRepoFiles(repoRoot, repoStage);
929
+ const rudderState = options.includeRudderState ? await copyRudderState(repoRoot, repoStage) : undefined;
930
+ const homePaths = normalizeHomePaths(requestedHomePaths);
931
+ const includedHomePaths = [];
932
+ for (const homePath of homePaths) {
933
+ const copied = await copyHomePath(homePath, homeStage);
934
+ if (copied) {
935
+ includedHomePaths.push(shortenHome(homePath));
936
+ }
937
+ }
938
+ // On macOS, Claude Code stores its OAuth token in the Keychain rather than
939
+ // ~/.claude/.credentials.json, so the home-paths copy above doesn't pick it
940
+ // up. Extract it from the Keychain and stage it as a credentials file so
941
+ // the cloud worker boots already logged in.
942
+ if (await stageClaudeKeychainCredentials(homeStage)) {
943
+ includedHomePaths.push("~/.claude/.credentials.json (keychain)");
944
+ }
945
+ const capturedEnv = captureCloudEnv();
946
+ let capturedEnvCount = 0;
947
+ if (Object.keys(capturedEnv).length > 0) {
948
+ await ensureDir(path.join(stageDir, "env"));
949
+ await writeJson(path.join(stageDir, "env", "cloud-env.json"), capturedEnv);
950
+ capturedEnvCount = Object.keys(capturedEnv).length;
951
+ }
952
+ let migratedAgentsCount = 0;
953
+ if (options.migration && options.migration.plan.migrated.length > 0) {
954
+ const entries = await stageMigratedAgents(stageDir, repoRoot, options.migration.repoName, options.migration.plan.migrated);
955
+ const migrationManifest = {
956
+ version: 1,
957
+ createdAt: nowIso(),
958
+ agents: entries,
959
+ };
960
+ await writeJson(path.join(stageDir, "migration.json"), migrationManifest);
961
+ migratedAgentsCount = entries.length;
962
+ }
963
+ const manifest = {
964
+ version: 1,
965
+ createdAt: nowIso(),
966
+ repo: {
967
+ root: path.basename(repoRoot),
968
+ branch: await currentBranch(repoRoot),
969
+ commit: await currentCommit(repoRoot),
970
+ },
971
+ homePaths: includedHomePaths,
972
+ ...(rudderState ? { rudderState } : {}),
973
+ ...(migratedAgentsCount > 0 ? { migratedAgents: migratedAgentsCount } : {}),
974
+ ...(capturedEnvCount > 0 ? { capturedEnvVars: capturedEnvCount } : {}),
975
+ };
976
+ await writeJson(path.join(stageDir, "manifest.json"), manifest);
977
+ const archivePath = path.join(tempDir, `${newRunId("cloud-snapshot")}.tgz`);
978
+ await runCommand("tar", ["-czf", archivePath, "-C", stageDir, "."], { cwd: stageDir });
979
+ return { tempDir, archivePath, manifest };
980
+ }
981
+ const CLOUD_ENV_DEFAULT_NAMES = new Set([
982
+ "ANTHROPIC_API_KEY",
983
+ "ANTHROPIC_AUTH_TOKEN",
984
+ "OPENAI_API_KEY",
985
+ "GOOGLE_API_KEY",
986
+ "AWS_ACCESS_KEY_ID",
987
+ "AWS_SECRET_ACCESS_KEY",
988
+ "AWS_SESSION_TOKEN",
989
+ "AWS_REGION",
990
+ "AWS_DEFAULT_REGION",
991
+ "AWS_PROFILE",
992
+ "VERCEL_TOKEN",
993
+ "VERCEL_ORG_ID",
994
+ "VERCEL_PROJECT_ID",
995
+ "NETLIFY_AUTH_TOKEN",
996
+ "GITHUB_TOKEN",
997
+ "GH_TOKEN",
998
+ "GITLAB_TOKEN",
999
+ "NPM_TOKEN",
1000
+ "CARGO_REGISTRY_TOKEN",
1001
+ "HUGGING_FACE_HUB_TOKEN",
1002
+ "HF_TOKEN",
1003
+ "DATABASE_URL",
1004
+ "REDIS_URL",
1005
+ "POSTGRES_URL",
1006
+ "STRIPE_API_KEY",
1007
+ "STRIPE_SECRET_KEY",
1008
+ "SLACK_BOT_TOKEN",
1009
+ "DISCORD_TOKEN",
1010
+ ]);
1011
+ const CLOUD_ENV_SUFFIX_PATTERNS = [/_API_KEY$/, /_AUTH_TOKEN$/, /_ACCESS_TOKEN$/, /_TOKEN$/, /_SECRET$/, /_SECRET_KEY$/];
1012
+ const CLOUD_ENV_BLOCKLIST = new Set([
1013
+ // Things we explicitly do not want shipping
1014
+ "PATH",
1015
+ "HOME",
1016
+ "USER",
1017
+ "PWD",
1018
+ "SHELL",
1019
+ "TERM",
1020
+ "TMPDIR",
1021
+ "LOGNAME",
1022
+ "SHLVL",
1023
+ "OLDPWD",
1024
+ "DISPLAY",
1025
+ "EDITOR",
1026
+ "PAGER",
1027
+ "LANG",
1028
+ "LC_ALL",
1029
+ // Rudder-internal vars that are set by the worker itself
1030
+ "RUDDER_WORKSPACE_ID",
1031
+ "RUDDER_SAIL_ID",
1032
+ "RUDDER_WORKER_TOKEN",
1033
+ "RUDDER_CLOUD_URL",
1034
+ "RUDDER_SNAPSHOT_URL",
1035
+ "RUDDER_CLOUD_TOKEN",
1036
+ "RUDDER_TASK",
1037
+ "RUDDER_REPO_NAME",
1038
+ "RUDDER_ACCOUNT_ID",
1039
+ "RUDDER_HANDOFF_PATH",
1040
+ ]);
1041
+ function captureCloudEnv() {
1042
+ const extra = (process.env.RUDDER_CLOUD_ENV_VARS || "")
1043
+ .split(",")
1044
+ .map((name) => name.trim())
1045
+ .filter(Boolean);
1046
+ const blockExtra = (process.env.RUDDER_CLOUD_ENV_BLOCKLIST || "")
1047
+ .split(",")
1048
+ .map((name) => name.trim())
1049
+ .filter(Boolean);
1050
+ const blocked = new Set([...CLOUD_ENV_BLOCKLIST, ...blockExtra]);
1051
+ const out = {};
1052
+ for (const [name, value] of Object.entries(process.env)) {
1053
+ if (!name || typeof value !== "string" || !value) {
1054
+ continue;
1055
+ }
1056
+ if (blocked.has(name)) {
1057
+ continue;
1058
+ }
1059
+ const matches = CLOUD_ENV_DEFAULT_NAMES.has(name)
1060
+ || CLOUD_ENV_SUFFIX_PATTERNS.some((pattern) => pattern.test(name))
1061
+ || extra.includes(name);
1062
+ if (!matches) {
1063
+ continue;
1064
+ }
1065
+ out[name] = value;
1066
+ }
1067
+ return out;
1068
+ }
1069
+ async function stageMigratedAgents(stageDir, repoRoot, repoName, migrated) {
1070
+ const worktreesStage = path.join(stageDir, "migrated-worktrees");
1071
+ const sessionsStage = path.join(stageDir, "migrated-sessions");
1072
+ const entries = [];
1073
+ for (const candidate of migrated) {
1074
+ if (!(await pathExists(candidate.worktreePath))) {
1075
+ continue;
1076
+ }
1077
+ const hasSession = Boolean(candidate.sessionId
1078
+ && candidate.sessionJsonlPath
1079
+ && (await pathExists(candidate.sessionJsonlPath)));
1080
+ const worktreeDest = path.join(worktreesStage, candidate.runId);
1081
+ await ensureDir(worktreeDest);
1082
+ await copyWorktreeFiles(candidate.worktreePath, worktreeDest, repoRoot);
1083
+ let sessionJsonlSnapshotPath;
1084
+ if (hasSession) {
1085
+ const jsonlDest = path.join(sessionsStage, `${candidate.runId}.jsonl`);
1086
+ await ensureDir(path.dirname(jsonlDest));
1087
+ await fsp.cp(candidate.sessionJsonlPath, jsonlDest, { force: true });
1088
+ sessionJsonlSnapshotPath = path.posix.join("migrated-sessions", `${candidate.runId}.jsonl`);
1089
+ }
1090
+ const cloudWorktreeAbs = cloudWorktreeAbsolutePath(repoName, candidate.runId);
1091
+ // For fresh restarts, build a prompt-engineered handoff from the local
1092
+ // run record so the new agent gets context instead of just the bare task.
1093
+ let freshPrompt;
1094
+ if (!hasSession) {
1095
+ const runJsonPath = path.join(repoRoot, ".rudder", "runs", candidate.runId, "run.json");
1096
+ const record = await readJson(runJsonPath);
1097
+ const turns = Array.isArray(record?.turns) ? record.turns : [];
1098
+ freshPrompt = buildFreshHandoffPrompt(candidate, turns);
1099
+ }
1100
+ entries.push({
1101
+ runId: candidate.runId,
1102
+ task: candidate.task,
1103
+ taskSummary: candidate.taskSummary,
1104
+ backend: candidate.backend,
1105
+ sessionId: candidate.sessionId ?? "",
1106
+ localWorktreePath: candidate.worktreePath,
1107
+ cloudWorktreeRelativePath: cloudWorktreeAbs,
1108
+ sessionJsonlSnapshotPath: sessionJsonlSnapshotPath ?? "",
1109
+ worktreeBranch: candidate.worktreeBranch,
1110
+ freshPrompt,
1111
+ });
1112
+ }
1113
+ return entries;
1114
+ }
1115
+ async function copyWorktreeFiles(worktreePath, target, _repoRoot) {
1116
+ const result = await runCommand("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
1117
+ cwd: worktreePath,
1118
+ allowFailure: true,
1119
+ });
1120
+ const files = result.code === 0
1121
+ ? result.stdout.split("\0").filter(Boolean)
1122
+ : await listFiles(worktreePath);
1123
+ for (const relative of files) {
1124
+ if (!relative || relative.startsWith(".git/") || relative === ".git" || relative.startsWith(".rudder/")) {
1125
+ continue;
1126
+ }
1127
+ const source = path.join(worktreePath, relative);
1128
+ const dest = path.join(target, relative);
1129
+ if (!isInside(worktreePath, source) || !isInside(target, dest)) {
1130
+ continue;
1131
+ }
1132
+ const stat = await fsp.lstat(source).catch(() => null);
1133
+ if (!stat || stat.isDirectory() || !(await shouldIncludeSnapshotPath(source))) {
1134
+ continue;
1135
+ }
1136
+ await ensureDir(path.dirname(dest));
1137
+ await fsp.cp(source, dest, { dereference: false, force: true });
1138
+ }
1139
+ }
1140
+ async function copyRudderState(repoRoot, repoStage) {
1141
+ const copied = [];
1142
+ const rudderMd = path.join(repoRoot, "RUDDER.md");
1143
+ if (await pathExists(rudderMd)) {
1144
+ const target = path.join(repoStage, "RUDDER.md");
1145
+ await fsp.cp(rudderMd, target, { force: true });
1146
+ copied.push("RUDDER.md");
1147
+ }
1148
+ const runsDir = path.join(repoRoot, ".rudder", "runs");
1149
+ const entries = await fsp.readdir(runsDir, { withFileTypes: true }).catch(() => []);
1150
+ let runs = 0;
1151
+ for (const entry of entries) {
1152
+ if (!entry.isDirectory() || entry.name.includes("/") || entry.name.includes("\\")) {
1153
+ continue;
1154
+ }
1155
+ const runJson = path.join(runsDir, entry.name, "run.json");
1156
+ if (!(await pathExists(runJson))) {
1157
+ continue;
1158
+ }
1159
+ const relative = path.join(".rudder", "runs", entry.name, "run.json");
1160
+ const target = path.join(repoStage, relative);
1161
+ if (!isInside(repoRoot, runJson) || !isInside(repoStage, target)) {
1162
+ continue;
1163
+ }
1164
+ await ensureDir(path.dirname(target));
1165
+ await fsp.cp(runJson, target, { force: true });
1166
+ copied.push(relative);
1167
+ runs += 1;
1168
+ }
1169
+ return { runs, files: copied };
1170
+ }
1171
+ async function copyRepoFiles(repoRoot, repoStage) {
1172
+ const result = await runCommand("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
1173
+ cwd: repoRoot,
1174
+ allowFailure: true,
1175
+ });
1176
+ const files = result.code === 0
1177
+ ? result.stdout.split("\0").filter(Boolean)
1178
+ : await listFiles(repoRoot);
1179
+ for (const relative of files) {
1180
+ if (!relative || relative.startsWith(".git/") || relative.startsWith(".rudder/")) {
1181
+ continue;
1182
+ }
1183
+ const source = path.join(repoRoot, relative);
1184
+ const target = path.join(repoStage, relative);
1185
+ if (!isInside(repoRoot, source) || !isInside(repoStage, target)) {
1186
+ continue;
1187
+ }
1188
+ const stat = await fsp.lstat(source).catch(() => null);
1189
+ if (!stat || stat.isDirectory() || !(await shouldIncludeSnapshotPath(source))) {
1190
+ continue;
1191
+ }
1192
+ await ensureDir(path.dirname(target));
1193
+ await fsp.cp(source, target, { dereference: false, force: true });
1194
+ }
1195
+ }
1196
+ async function listFiles(dir) {
1197
+ const files = [];
1198
+ async function walk(current) {
1199
+ const entries = await fsp.readdir(current, { withFileTypes: true }).catch(() => []);
1200
+ for (const entry of entries) {
1201
+ if (entry.name === ".git" || entry.name === ".rudder" || entry.name === "node_modules") {
1202
+ continue;
1203
+ }
1204
+ const full = path.join(current, entry.name);
1205
+ if (entry.isDirectory()) {
1206
+ await walk(full);
1207
+ }
1208
+ else {
1209
+ files.push(path.relative(dir, full));
1210
+ }
1211
+ }
1212
+ }
1213
+ await walk(dir);
1214
+ return files;
1215
+ }
1216
+ function normalizeHomePaths(requested) {
1217
+ const raw = [
1218
+ ...DEFAULT_HOME_PATHS,
1219
+ ...requested,
1220
+ ...(process.env.RUDDER_CLOUD_HOME_PATHS?.split(",") ?? []),
1221
+ ];
1222
+ const home = os.homedir();
1223
+ const seen = new Set();
1224
+ const paths = [];
1225
+ for (const item of raw) {
1226
+ const trimmed = item.trim();
1227
+ if (!trimmed) {
1228
+ continue;
1229
+ }
1230
+ const resolved = path.resolve(expandHome(trimmed));
1231
+ if (!isInside(home, resolved) || seen.has(resolved)) {
1232
+ continue;
1233
+ }
1234
+ seen.add(resolved);
1235
+ paths.push(resolved);
1236
+ }
1237
+ return paths;
1238
+ }
1239
+ async function stageClaudeKeychainCredentials(homeStage) {
1240
+ if (process.platform !== "darwin") {
1241
+ return false;
1242
+ }
1243
+ if (!commandExists("security")) {
1244
+ return false;
1245
+ }
1246
+ const result = await runCommand("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { allowFailure: true });
1247
+ if (result.code !== 0) {
1248
+ return false;
1249
+ }
1250
+ const payload = result.stdout.trim();
1251
+ if (!payload || !payload.startsWith("{")) {
1252
+ return false;
1253
+ }
1254
+ try {
1255
+ JSON.parse(payload);
1256
+ }
1257
+ catch {
1258
+ return false;
1259
+ }
1260
+ const targetDir = path.join(homeStage, ".claude");
1261
+ await ensureDir(targetDir);
1262
+ await fsp.writeFile(path.join(targetDir, ".credentials.json"), payload + "\n", { mode: 0o600 });
1263
+ return true;
1264
+ }
1265
+ async function copyHomePath(source, homeStage) {
1266
+ if (!(await pathExists(source)) || !(await shouldIncludeSnapshotPath(source))) {
1267
+ return false;
1268
+ }
1269
+ const relative = path.relative(os.homedir(), source);
1270
+ const target = path.join(homeStage, relative);
1271
+ if (!isInside(homeStage, target)) {
1272
+ return false;
1273
+ }
1274
+ await fsp.cp(source, target, {
1275
+ dereference: false,
1276
+ recursive: true,
1277
+ force: true,
1278
+ filter: async (candidate) => await shouldIncludeSnapshotPath(candidate),
1279
+ });
1280
+ return true;
1281
+ }
1282
+ async function shouldIncludeSnapshotPath(candidate) {
1283
+ const normalized = path.resolve(candidate);
1284
+ const parts = normalized.split(path.sep).map((part) => part.toLowerCase());
1285
+ const basename = path.basename(normalized).toLowerCase();
1286
+ if (basename.startsWith("._")) {
1287
+ return false;
1288
+ }
1289
+ if (parts.some((part) => SECRET_PATH_PARTS.has(part)) || SECRET_BASENAMES.has(basename)) {
1290
+ return false;
1291
+ }
1292
+ if (isInside(os.homedir(), normalized)) {
1293
+ if (parts.some((part) => BULKY_HOME_PATH_PARTS.has(part))) {
1294
+ return false;
1295
+ }
1296
+ if (BULKY_HOME_BASENAME_PATTERNS.some((pattern) => pattern.test(basename))) {
1297
+ return false;
1298
+ }
1299
+ }
1300
+ const stat = await fsp.lstat(normalized).catch(() => null);
1301
+ if (!stat || !stat.isFile() || stat.size > MAX_HOME_SECRET_SCAN_BYTES) {
1302
+ return true;
1303
+ }
1304
+ const text = await fsp.readFile(normalized, "utf8").catch(() => "");
1305
+ return !/(aws_access_key_id|aws_secret_access_key|aws_session_token|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN)/.test(text);
1306
+ }
1307
+ function isInside(parent, child) {
1308
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
1309
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
1310
+ }
1311
+ function openBrowser(url) {
1312
+ const platform = process.platform;
1313
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
1314
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
1315
+ const child = spawn(command, args, {
1316
+ detached: true,
1317
+ stdio: "ignore",
1318
+ });
1319
+ child.on("error", () => undefined);
1320
+ child.unref();
1321
+ }
1322
+ function withQuery(baseUrl, pathname, query) {
1323
+ const url = new URL(pathname, `${baseUrl}/`);
1324
+ for (const [key, value] of Object.entries(query)) {
1325
+ url.searchParams.set(key, value);
1326
+ }
1327
+ return url.toString();
1328
+ }
1329
+ function parseJson(text) {
1330
+ try {
1331
+ return JSON.parse(text);
1332
+ }
1333
+ catch {
1334
+ return text;
1335
+ }
1336
+ }
1337
+ function responseErrorMessage(value) {
1338
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1339
+ return undefined;
1340
+ }
1341
+ const record = value;
1342
+ return typeof record.error === "string"
1343
+ ? record.error
1344
+ : typeof record.message === "string"
1345
+ ? record.message
1346
+ : undefined;
1347
+ }
1348
+ async function printResult(result, options) {
1349
+ if (options.json) {
1350
+ printJson(result);
1351
+ return;
1352
+ }
1353
+ if (Array.isArray(result)) {
1354
+ printSailList(result);
1355
+ return;
1356
+ }
1357
+ if (result && typeof result === "object" && !Array.isArray(result)) {
1358
+ const record = result;
1359
+ if (typeof record.bootstrapCommand === "string") {
1360
+ const id = typeof record.id === "string" ? record.id : "BYOC sail";
1361
+ const status = typeof record.status === "string" ? record.status : undefined;
1362
+ const state = await loadCloudAuth();
1363
+ const host = options.sshHost ?? state?.byocSshHost;
1364
+ console.log(`${id}${status ? ` (${status})` : ""} is ready for BYOC.`);
1365
+ if (host && process.env.RUDDER_BYOC_AUTOSTART !== "0") {
1366
+ try {
1367
+ await startByocWorkerOverSsh(host, record.bootstrapCommand);
1368
+ console.log(`Started BYOC worker over SSH on ${host}.`);
1369
+ console.log(`Remote log: ssh ${host} 'tail -f ~/.rudder/byoc/worker.log'`);
1370
+ }
1371
+ catch (error) {
1372
+ console.log(`Could not start BYOC worker over SSH on ${host}: ${error instanceof Error ? error.message : String(error)}`);
1373
+ console.log("Run this manually on your workstation/server:");
1374
+ console.log(record.bootstrapCommand);
1375
+ }
1376
+ }
1377
+ else {
1378
+ console.log("Run this on your workstation/server:");
1379
+ console.log(record.bootstrapCommand);
1380
+ if (!host) {
1381
+ console.log("\nTip: run `rudder cloud byoc <ssh-host>` to have Rudder start this over SSH next time.");
1382
+ }
1383
+ }
1384
+ if (typeof record.updatedAt === "string") {
1385
+ console.log(`\nIf the command expires, run: rudder cloud bootstrap ${id}`);
1386
+ }
1387
+ return;
1388
+ }
1389
+ const sails = record.sails ?? record.items;
1390
+ if (Array.isArray(sails)) {
1391
+ printSailList(sails);
1392
+ return;
1393
+ }
1394
+ if (typeof record.id === "string" && (typeof record.status === "string" || typeof record.runtime === "string")) {
1395
+ const parts = [
1396
+ record.id,
1397
+ typeof record.status === "string" ? record.status : undefined,
1398
+ typeof record.runtime === "string" ? record.runtime : undefined,
1399
+ typeof record.repoName === "string" ? record.repoName : undefined,
1400
+ ].filter(Boolean);
1401
+ console.log(parts.join(" "));
1402
+ if (record.workspace === true || (record.run === null && typeof record.task !== "string")) {
1403
+ console.log("Rudder workspace uploaded. Use /cloud list to track it.");
1404
+ }
1405
+ else {
1406
+ console.log("Cloud worker created. Use /cloud list to track it.");
1407
+ }
1408
+ return;
1409
+ }
1410
+ }
1411
+ console.log(JSON.stringify(result, null, 2));
1412
+ }
1413
+ function printSailList(items) {
1414
+ if (items.length === 0) {
1415
+ console.log("No cloud sails.");
1416
+ return;
1417
+ }
1418
+ for (const item of items) {
1419
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1420
+ console.log(String(item));
1421
+ continue;
1422
+ }
1423
+ const sail = item;
1424
+ console.log([
1425
+ sail.id,
1426
+ sail.status,
1427
+ sail.runtime,
1428
+ typeof sail.task === "string" && sail.task ? sail.task : undefined,
1429
+ typeof sail.repoName === "string" && sail.repoName ? sail.repoName : undefined,
1430
+ sail.branch,
1431
+ sail.url,
1432
+ sail.updatedAt ?? sail.createdAt,
1433
+ ].filter(Boolean).join(" "));
1434
+ }
1435
+ }
1436
+ function printJson(value) {
1437
+ console.log(JSON.stringify(value, null, 2));
1438
+ }
1439
+ function sleep(ms) {
1440
+ return new Promise((resolve) => setTimeout(resolve, ms));
1441
+ }
1442
+ async function workspaceCommand(args, options) {
1443
+ const sub = args[0] ?? "";
1444
+ const rest = args.slice(1);
1445
+ if (sub === "" || sub === "attach") {
1446
+ await workspaceAttach(rest, options);
1447
+ return;
1448
+ }
1449
+ if (sub === "share") {
1450
+ await workspaceShare(options);
1451
+ return;
1452
+ }
1453
+ if (sub === "status") {
1454
+ await workspaceStatus(options);
1455
+ return;
1456
+ }
1457
+ if (sub === "stop") {
1458
+ await workspaceMutate("stop", rest, options);
1459
+ return;
1460
+ }
1461
+ if (sub === "list" || sub === "ls") {
1462
+ await workspaceList(options);
1463
+ return;
1464
+ }
1465
+ throw new Error("Usage: rudder cloud workspace [attach [id]|share|status|stop|list]");
1466
+ }
1467
+ function computeWorkspaceKey(repoRoot) {
1468
+ const normalized = path.resolve(repoRoot);
1469
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 32);
1470
+ }
1471
+ let cachedFlyRegion;
1472
+ async function detectFlyRegion(baseUrl) {
1473
+ if (process.env.RUDDER_CLOUD_REGION) {
1474
+ return process.env.RUDDER_CLOUD_REGION.trim().toLowerCase();
1475
+ }
1476
+ if (cachedFlyRegion) {
1477
+ return cachedFlyRegion;
1478
+ }
1479
+ try {
1480
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, { method: "GET" });
1481
+ const requestId = response.headers.get("fly-request-id");
1482
+ if (requestId) {
1483
+ // Fly request-id format: <ulid>-<region>
1484
+ const dash = requestId.lastIndexOf("-");
1485
+ const region = dash > 0 ? requestId.slice(dash + 1).trim().toLowerCase() : "";
1486
+ if (region && region.length <= 6 && /^[a-z]+$/.test(region)) {
1487
+ cachedFlyRegion = region;
1488
+ return region;
1489
+ }
1490
+ }
1491
+ }
1492
+ catch {
1493
+ // ignore — server will fall back to its default region
1494
+ }
1495
+ return undefined;
1496
+ }
1497
+ async function computeSnapshotFingerprint(repoRoot, _requestedHomePaths) {
1498
+ const hash = createHash("sha256");
1499
+ // Repo state: HEAD commit + the porcelain dirty file list. Two attaches
1500
+ // from the same repo at the same commit with no edits should produce the
1501
+ // same fingerprint.
1502
+ const headCommit = await currentCommit(repoRoot).catch(() => "");
1503
+ hash.update(`repo:head:${headCommit}\n`);
1504
+ const status = await runCommand("git", ["status", "--porcelain", "-z"], {
1505
+ cwd: repoRoot,
1506
+ allowFailure: true,
1507
+ });
1508
+ if (status.code === 0) {
1509
+ hash.update(`repo:status:${status.stdout}\n`);
1510
+ }
1511
+ // macOS Keychain claude credentials: hash content so re-logging in
1512
+ // invalidates the cache but a steady-state user keeps it.
1513
+ if (process.platform === "darwin") {
1514
+ const creds = await runCommand("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], {
1515
+ allowFailure: true,
1516
+ });
1517
+ if (creds.code === 0) {
1518
+ hash.update(`keychain:claude:${createHash("sha256").update(creds.stdout).digest("hex")}\n`);
1519
+ }
1520
+ }
1521
+ // Captured env vars (excluding ones we know rotate like AWS session tokens).
1522
+ const env = captureCloudEnv();
1523
+ const unstable = new Set(["AWS_SESSION_TOKEN", "AWS_SECURITY_TOKEN"]);
1524
+ for (const key of Object.keys(env).sort()) {
1525
+ if (unstable.has(key))
1526
+ continue;
1527
+ hash.update(`env:${key}:${createHash("sha256").update(env[key] ?? "").digest("hex")}\n`);
1528
+ }
1529
+ return hash.digest("hex").slice(0, 32);
1530
+ }
1531
+ async function workspaceAttach(args, options) {
1532
+ const explicitId = args[0];
1533
+ if (explicitId) {
1534
+ await workspaceAttachById(explicitId, options);
1535
+ return;
1536
+ }
1537
+ const repoRoot = findRepoRoot();
1538
+ const workspaceKey = computeWorkspaceKey(repoRoot);
1539
+ const repoName = path.basename(repoRoot);
1540
+ const client = await cloudClient({ requireToken: true });
1541
+ if (!options.json) {
1542
+ process.stderr.write(`Resolving cloud workspace for ${repoName}...\n`);
1543
+ }
1544
+ const migrationPlan = await planAgentMigration(repoRoot, options);
1545
+ const mustUploadSnapshot = Boolean(migrationPlan && migrationPlan.migrated.length > 0);
1546
+ const region = await detectFlyRegion(client.baseUrl).catch(() => undefined);
1547
+ const fingerprint = await computeSnapshotFingerprint(repoRoot, options.homePaths ?? []);
1548
+ const baseBody = {
1549
+ workspaceKey,
1550
+ repoName,
1551
+ snapshotFingerprint: fingerprint,
1552
+ ...(region ? { region } : {}),
1553
+ };
1554
+ let result = null;
1555
+ if (!mustUploadSnapshot) {
1556
+ try {
1557
+ result = await client.request("/api/rudder/workspace/attach", {
1558
+ method: "POST",
1559
+ body: baseBody,
1560
+ });
1561
+ }
1562
+ catch (error) {
1563
+ const message = error instanceof Error ? error.message : String(error);
1564
+ if (!/snapshot/i.test(message)) {
1565
+ throw error;
1566
+ }
1567
+ result = null;
1568
+ }
1569
+ }
1570
+ if (!result) {
1571
+ if (!options.json) {
1572
+ process.stderr.write(`Uploading workspace snapshot...\n`);
1573
+ }
1574
+ const snapshot = await createSnapshot(repoRoot, options.homePaths ?? [], {
1575
+ includeRudderState: true,
1576
+ migration: migrationPlan ? { repoName, plan: migrationPlan } : undefined,
1577
+ });
1578
+ try {
1579
+ const body = {
1580
+ ...baseBody,
1581
+ snapshot: {
1582
+ name: path.basename(snapshot.archivePath),
1583
+ contentType: "application/gzip",
1584
+ base64: await fsp.readFile(snapshot.archivePath, "base64"),
1585
+ manifest: snapshot.manifest,
1586
+ },
1587
+ };
1588
+ if (migrationPlan && migrationPlan.migrated.length > 0) {
1589
+ body.migratedAgents = summaryAsJson(migrationPlan);
1590
+ }
1591
+ result = await client.request("/api/rudder/workspace/attach", {
1592
+ method: "POST",
1593
+ body,
1594
+ });
1595
+ }
1596
+ finally {
1597
+ await fsp.rm(snapshot.tempDir, { recursive: true, force: true });
1598
+ }
1599
+ }
1600
+ if (migrationPlan && !options.json) {
1601
+ process.stderr.write(`${migrationSummary(migrationPlan)}\n`);
1602
+ }
1603
+ if (!result) {
1604
+ throw new Error("Workspace attach returned no result");
1605
+ }
1606
+ await attachToWorkspaceResult(result, options);
1607
+ }
1608
+ async function planAgentMigration(repoRoot, options) {
1609
+ const candidates = await findMigrationCandidates(repoRoot);
1610
+ if (candidates.length === 0) {
1611
+ return null;
1612
+ }
1613
+ const plan = applyDefaultDecisions(candidates);
1614
+ if (options.json) {
1615
+ return plan;
1616
+ }
1617
+ if (!isTty()) {
1618
+ return plan;
1619
+ }
1620
+ console.log(migrationSummary(plan));
1621
+ if (plan.migrated.length === 0) {
1622
+ return plan;
1623
+ }
1624
+ const confirmed = await promptConfirm("Move resumable agents to cloud?", true);
1625
+ if (confirmed) {
1626
+ return plan;
1627
+ }
1628
+ return {
1629
+ candidates,
1630
+ migrated: [],
1631
+ stayedLocal: candidates.map((c) => ({ ...c, decision: "stay" })),
1632
+ };
1633
+ }
1634
+ async function attachToWorkspaceResult(result, options) {
1635
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
1636
+ throw new Error("Unexpected workspace response from cloud");
1637
+ }
1638
+ const record = result;
1639
+ const workspaceId = typeof record.id === "string" ? record.id : undefined;
1640
+ if (!workspaceId) {
1641
+ throw new Error("Workspace response is missing id");
1642
+ }
1643
+ if (options.json) {
1644
+ printJson(record);
1645
+ return;
1646
+ }
1647
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1648
+ process.stderr.write(`Workspace ${workspaceId} is ready. Run \`rudder cloud workspace attach\` from a TTY to take over.\n`);
1649
+ return;
1650
+ }
1651
+ if (process.env.RUDDER_CLOUD_NO_ATTACH === "1") {
1652
+ return;
1653
+ }
1654
+ await waitForWorkspaceWorker(workspaceId);
1655
+ await runAttach({ kind: "workspace", id: workspaceId, label: `workspace ${workspaceId}` }, { ...options, quietBanner: false });
1656
+ }
1657
+ async function waitForWorkspaceWorker(workspaceId) {
1658
+ // Give the Fly machine a moment to boot before the WS attach so users see the dashboard, not a wait-for-worker banner.
1659
+ await new Promise((resolve) => setTimeout(resolve, 1500));
1660
+ void workspaceId;
1661
+ }
1662
+ async function workspaceAttachById(workspaceId, options) {
1663
+ if (options.json) {
1664
+ printJson({ id: workspaceId, attaching: true });
1665
+ }
1666
+ else if (!process.stdin.isTTY || !process.stdout.isTTY) {
1667
+ process.stderr.write(`Workspace ${workspaceId}: attach requires a TTY.\n`);
1668
+ return;
1669
+ }
1670
+ if (process.env.RUDDER_CLOUD_NO_ATTACH === "1") {
1671
+ return;
1672
+ }
1673
+ await runAttach({ kind: "workspace", id: workspaceId, label: `workspace ${workspaceId}` }, { ...options, quietBanner: false });
1674
+ }
1675
+ async function lookupWorkspaceForRepo(options) {
1676
+ void options;
1677
+ const repoRoot = findRepoRoot();
1678
+ const workspaceKey = computeWorkspaceKey(repoRoot);
1679
+ const client = await cloudClient({ requireToken: true });
1680
+ try {
1681
+ const result = await client.request(`/api/rudder/workspace/lookup?key=${encodeURIComponent(workspaceKey)}`, { method: "GET" });
1682
+ if (result && typeof result === "object" && !Array.isArray(result)) {
1683
+ return result;
1684
+ }
1685
+ return null;
1686
+ }
1687
+ catch (error) {
1688
+ const message = error instanceof Error ? error.message : String(error);
1689
+ if (/not found/i.test(message) || /404/.test(message)) {
1690
+ return null;
1691
+ }
1692
+ throw error;
1693
+ }
1694
+ }
1695
+ async function workspaceShare(options) {
1696
+ const workspace = await lookupWorkspaceForRepo(options);
1697
+ if (!workspace) {
1698
+ if (options.json) {
1699
+ printJson({ workspace: null });
1700
+ return;
1701
+ }
1702
+ console.log("No cloud workspace exists for this repo yet. Run `rudder cloud workspace attach` to create one.");
1703
+ return;
1704
+ }
1705
+ const id = typeof workspace.id === "string" ? workspace.id : "";
1706
+ if (!id) {
1707
+ throw new Error("Workspace lookup returned no id");
1708
+ }
1709
+ if (options.json) {
1710
+ printJson({
1711
+ id,
1712
+ attachCommand: `rudder cloud workspace attach ${id}`,
1713
+ status: workspace.status ?? null,
1714
+ });
1715
+ return;
1716
+ }
1717
+ console.log("Share this workspace with a teammate by sending them:");
1718
+ console.log("");
1719
+ console.log(` rudder cloud workspace attach ${id}`);
1720
+ console.log("");
1721
+ console.log("They must already be logged in to Rudder Cloud with their own account (run `rudder cloud login` if not).");
1722
+ }
1723
+ async function workspaceStatus(options) {
1724
+ if (process.env.RUDDER_OFFLINE === "1") {
1725
+ if (options.json) {
1726
+ printJson({ offline: true, workspace: null });
1727
+ }
1728
+ else {
1729
+ console.log("RUDDER_OFFLINE is set; skipping cloud workspace status check.");
1730
+ }
1731
+ return;
1732
+ }
1733
+ const workspace = await lookupWorkspaceForRepo(options).catch((error) => {
1734
+ if (!options.json) {
1735
+ console.warn(`Could not reach Rudder Cloud: ${error instanceof Error ? error.message : String(error)}`);
1736
+ }
1737
+ return null;
1738
+ });
1739
+ if (!workspace) {
1740
+ if (options.json) {
1741
+ printJson({ workspace: null });
1742
+ }
1743
+ else {
1744
+ console.log("No cloud workspace for this repo.");
1745
+ }
1746
+ return;
1747
+ }
1748
+ const id = typeof workspace.id === "string" ? workspace.id : "";
1749
+ const status = typeof workspace.status === "string" ? workspace.status : "unknown";
1750
+ const clientCount = typeof workspace.clientCount === "number" ? workspace.clientCount : 0;
1751
+ const lastActivityAt = typeof workspace.lastActivityAt === "string" ? workspace.lastActivityAt : undefined;
1752
+ const idleMinutes = computeIdleMinutes(lastActivityAt);
1753
+ const activeAgents = clientCount > 0 || (idleMinutes !== null && idleMinutes < 5);
1754
+ if (options.json) {
1755
+ printJson({
1756
+ id,
1757
+ status,
1758
+ clientCount,
1759
+ lastActivityAt: lastActivityAt ?? null,
1760
+ idleMinutes,
1761
+ activeAgents,
1762
+ repoName: typeof workspace.repoName === "string" ? workspace.repoName : null,
1763
+ });
1764
+ return;
1765
+ }
1766
+ const idlePart = idleMinutes !== null ? ` idle ${idleMinutes}m` : "";
1767
+ console.log(`workspace ${id} ${status} clients=${clientCount}${idlePart}`);
1768
+ if (activeAgents) {
1769
+ console.log("Active agents likely running.");
1770
+ }
1771
+ else {
1772
+ console.log("No recent activity.");
1773
+ }
1774
+ }
1775
+ function computeIdleMinutes(lastActivityAt) {
1776
+ if (!lastActivityAt) {
1777
+ return null;
1778
+ }
1779
+ const ms = Date.parse(lastActivityAt);
1780
+ if (!Number.isFinite(ms)) {
1781
+ return null;
1782
+ }
1783
+ const diff = Date.now() - ms;
1784
+ if (diff < 0) {
1785
+ return 0;
1786
+ }
1787
+ return Math.floor(diff / 60_000);
1788
+ }
1789
+ async function workspaceMutate(action, args, options) {
1790
+ const id = args[0];
1791
+ if (!id) {
1792
+ throw new Error(`Usage: rudder cloud workspace ${action} <id>`);
1793
+ }
1794
+ const client = await cloudClient({ requireToken: true });
1795
+ const result = await client.request(`/api/rudder/workspace/${encodeURIComponent(id)}/${action}`, {
1796
+ method: "POST",
1797
+ body: {},
1798
+ });
1799
+ await printResult(result, options);
1800
+ }
1801
+ async function workspaceList(options) {
1802
+ const client = await cloudClient({ requireToken: true });
1803
+ const result = await client.request("/api/rudder/workspace", { method: "GET" });
1804
+ await printResult(result, options);
1805
+ }
1806
+ async function attach(args, options) {
1807
+ const sailId = args[0];
1808
+ if (!sailId) {
1809
+ throw new Error("Usage: rudder cloud attach <id>");
1810
+ }
1811
+ await runAttach({ kind: "sail", id: sailId, label: sailId }, options);
1812
+ }
1813
+ async function runAttach(target, options) {
1814
+ const client = await cloudClient({ requireToken: true });
1815
+ const baseUrl = client.baseUrl;
1816
+ const state = await loadCloudAuth();
1817
+ const envToken = process.env.RUDDER_CLOUD_TOKEN?.trim();
1818
+ const token = envToken || (state?.cloudUrl === baseUrl ? state.token : undefined);
1819
+ if (!token) {
1820
+ throw new Error("Not logged in to Rudder Cloud. Run `rudder login` first.");
1821
+ }
1822
+ const wsUrl = baseUrl.replace(/^http/, "ws").replace(/\/$/, "")
1823
+ + `/api/rudder/${target.kind}/${encodeURIComponent(target.id)}/attach`;
1824
+ const stdin = process.stdin;
1825
+ const stdout = process.stdout;
1826
+ const isInteractive = Boolean(stdin.isTTY && stdout.isTTY);
1827
+ return await new Promise((resolve, reject) => {
1828
+ const socket = new WebSocket(wsUrl, {
1829
+ headers: { authorization: `Bearer ${token}` },
1830
+ });
1831
+ socket.binaryType = "nodebuffer";
1832
+ let opened = false;
1833
+ let cleaned = false;
1834
+ let firstFrameRendered = false;
1835
+ let result = "exited";
1836
+ const splashAllowed = isInteractive && !options.json && !options.quietBanner;
1837
+ const splash = splashAllowed ? new AttachSplash(stdout, target.label) : null;
1838
+ const sendResize = () => {
1839
+ if (socket.readyState !== WebSocket.OPEN) {
1840
+ return;
1841
+ }
1842
+ const cols = stdout.columns ?? 120;
1843
+ const rows = stdout.rows ?? 32;
1844
+ socket.send(JSON.stringify({ type: "resize", cols, rows }));
1845
+ };
1846
+ let lastCtrlC = 0;
1847
+ const onStdin = (chunk) => {
1848
+ if (socket.readyState !== WebSocket.OPEN) {
1849
+ return;
1850
+ }
1851
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
1852
+ const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
1853
+ // While the loading splash is up (no remote frame has rendered yet),
1854
+ // Ctrl+C should cancel the local attach instead of being forwarded to
1855
+ // a remote dashboard the user can't see.
1856
+ if (isCtrlC && !firstFrameRendered) {
1857
+ try {
1858
+ socket.close(1000, "client-cancel");
1859
+ }
1860
+ catch { /* ignore */ }
1861
+ cleanup();
1862
+ if (!opened) {
1863
+ reject(new Error("Cloud attach cancelled"));
1864
+ }
1865
+ else {
1866
+ process.stderr.write("\nCancelled.\n");
1867
+ resolve("exited");
1868
+ }
1869
+ return;
1870
+ }
1871
+ // After handoff: forward Ctrl+C to the remote so Claude/codex can be
1872
+ // cancelled, but if the user mashes Ctrl+C twice within 2 seconds we
1873
+ // take it as "the remote is unresponsive; get me out".
1874
+ if (isCtrlC && firstFrameRendered) {
1875
+ const now = Date.now();
1876
+ if (now - lastCtrlC < 2000) {
1877
+ process.stderr.write("\nForce-exiting local attach (press Ctrl+C again to re-enter).\n");
1878
+ try {
1879
+ socket.close(1000, "client-force-quit");
1880
+ }
1881
+ catch { /* ignore */ }
1882
+ cleanup();
1883
+ resolve("exited");
1884
+ return;
1885
+ }
1886
+ lastCtrlC = now;
1887
+ }
1888
+ socket.send(buffer, { binary: true });
1889
+ };
1890
+ const onResize = () => {
1891
+ sendResize();
1892
+ splash?.redraw();
1893
+ };
1894
+ const cleanup = () => {
1895
+ if (cleaned) {
1896
+ return;
1897
+ }
1898
+ cleaned = true;
1899
+ stdin.off("data", onStdin);
1900
+ stdout.off("resize", onResize);
1901
+ process.off("SIGINT", onSigint);
1902
+ splash?.dispose();
1903
+ if (isInteractive && stdin.isTTY) {
1904
+ try {
1905
+ stdin.setRawMode(false);
1906
+ }
1907
+ catch {
1908
+ // ignore
1909
+ }
1910
+ }
1911
+ stdin.pause();
1912
+ };
1913
+ const onSigint = () => {
1914
+ // Belt-and-suspenders: if raw mode failed for any reason, Node still
1915
+ // sees SIGINT. Close cleanly so the user never gets a frozen terminal.
1916
+ process.stderr.write("\nReceived SIGINT, closing cloud attach.\n");
1917
+ try {
1918
+ socket.close(1000, "sigint");
1919
+ }
1920
+ catch { /* ignore */ }
1921
+ cleanup();
1922
+ if (!opened) {
1923
+ reject(new Error("Cloud attach cancelled"));
1924
+ }
1925
+ else {
1926
+ resolve("exited");
1927
+ }
1928
+ };
1929
+ process.once("SIGINT", onSigint);
1930
+ socket.on("open", () => {
1931
+ opened = true;
1932
+ // Disable Nagle on the underlying TCP socket so single keystrokes don't
1933
+ // sit in the send buffer waiting for piggyback ACKs (~40ms savings per
1934
+ // keypress on the WAN).
1935
+ try {
1936
+ const underlying = socket._socket;
1937
+ underlying?.setNoDelay?.(true);
1938
+ }
1939
+ catch {
1940
+ // ignore
1941
+ }
1942
+ if (splash) {
1943
+ splash.start();
1944
+ splash.setStatus(`Booting cloud workspace · ${target.label}`);
1945
+ }
1946
+ else if (!options.json && !options.quietBanner) {
1947
+ const tail = isInteractive ? " (Ctrl+C sends to remote; close this pane to detach)" : "";
1948
+ process.stderr.write(`Attached to ${target.label}${tail}\n`);
1949
+ }
1950
+ sendResize();
1951
+ if (isInteractive) {
1952
+ try {
1953
+ stdin.setRawMode(true);
1954
+ }
1955
+ catch {
1956
+ // ignore
1957
+ }
1958
+ }
1959
+ stdin.resume();
1960
+ stdin.on("data", onStdin);
1961
+ stdout.on("resize", onResize);
1962
+ });
1963
+ socket.on("message", (data, isBinary) => {
1964
+ if (isBinary && Buffer.isBuffer(data)) {
1965
+ if (!firstFrameRendered) {
1966
+ firstFrameRendered = true;
1967
+ splash?.handoff();
1968
+ // Re-send resize right after handoff and once more on a small
1969
+ // delay; some terminals don't surface accurate columns/rows
1970
+ // until after alt-screen exits, so the initial resize at WS
1971
+ // open can race.
1972
+ sendResize();
1973
+ setTimeout(sendResize, 150);
1974
+ }
1975
+ stdout.write(data);
1976
+ return;
1977
+ }
1978
+ const text = Buffer.isBuffer(data)
1979
+ ? data.toString("utf8")
1980
+ : Array.isArray(data)
1981
+ ? Buffer.concat(data).toString("utf8")
1982
+ : Buffer.from(data).toString("utf8");
1983
+ handleControlText(text);
1984
+ });
1985
+ socket.on("close", (code, reason) => {
1986
+ cleanup();
1987
+ if (!opened) {
1988
+ const reasonText = reason && reason.length ? reason.toString("utf8") : "";
1989
+ reject(new Error(`Cloud attach failed (code ${code}${reasonText ? `: ${reasonText}` : ""})`));
1990
+ return;
1991
+ }
1992
+ resolve(result);
1993
+ });
1994
+ socket.on("error", (err) => {
1995
+ if (!opened) {
1996
+ cleanup();
1997
+ reject(new Error(`Cloud attach failed: ${err.message}`));
1998
+ }
1999
+ });
2000
+ function handleControlText(text) {
2001
+ let payload;
2002
+ try {
2003
+ payload = JSON.parse(text);
2004
+ }
2005
+ catch {
2006
+ stdout.write(text);
2007
+ return;
2008
+ }
2009
+ if (!payload || typeof payload !== "object") {
2010
+ return;
2011
+ }
2012
+ const message = payload;
2013
+ if (message.type === "exit") {
2014
+ result = message.code === 0 ? "exited" : "failed";
2015
+ if (typeof process.exitCode !== "number" && message.code !== undefined) {
2016
+ process.exitCode = message.code;
2017
+ }
2018
+ return;
2019
+ }
2020
+ if (message.type === "status") {
2021
+ // If the worker dies before we've ever seen a remote frame, the
2022
+ // session is effectively dead. Don't sit on a splash spinner pretending
2023
+ // it'll come back; bail.
2024
+ if (message.state === "worker-disconnected" && !firstFrameRendered) {
2025
+ splash?.dispose();
2026
+ try {
2027
+ socket.close(1000, "worker-gone");
2028
+ }
2029
+ catch { /* ignore */ }
2030
+ cleanup();
2031
+ if (!opened) {
2032
+ reject(new Error("Cloud worker exited before the dashboard started"));
2033
+ }
2034
+ else {
2035
+ process.stderr.write("\nCloud worker exited before the dashboard started.\n");
2036
+ result = "failed";
2037
+ resolve(result);
2038
+ }
2039
+ return;
2040
+ }
2041
+ if (splash && !firstFrameRendered) {
2042
+ if (message.state === "worker-waiting") {
2043
+ splash.setStatus(`Waiting for cloud worker · ${target.label}`);
2044
+ }
2045
+ else if (message.state === "worker-connected") {
2046
+ splash.setStatus(`Cloud worker connected · loading dashboard`);
2047
+ }
2048
+ }
2049
+ else if (!options.json && !options.quietBanner) {
2050
+ if (message.state === "worker-disconnected") {
2051
+ process.stderr.write("\nCloud worker disconnected; waiting for reconnect...\n");
2052
+ }
2053
+ else if (message.state === "worker-connected") {
2054
+ process.stderr.write("Cloud worker connected.\n");
2055
+ }
2056
+ }
2057
+ }
2058
+ }
2059
+ });
2060
+ }
2061
+ class AttachSplash {
2062
+ stdout;
2063
+ label;
2064
+ frame = 0;
2065
+ status;
2066
+ timer;
2067
+ active = false;
2068
+ static FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2069
+ constructor(stdout, label) {
2070
+ this.stdout = stdout;
2071
+ this.label = label;
2072
+ this.status = `Connecting to ${label}`;
2073
+ }
2074
+ start() {
2075
+ if (this.active) {
2076
+ return;
2077
+ }
2078
+ this.active = true;
2079
+ this.stdout.write("\x1b[?1049h\x1b[?25l");
2080
+ this.redraw();
2081
+ this.timer = setInterval(() => {
2082
+ this.frame = (this.frame + 1) % AttachSplash.FRAMES.length;
2083
+ this.redraw();
2084
+ }, 100);
2085
+ this.timer.unref?.();
2086
+ }
2087
+ setStatus(status) {
2088
+ this.status = status;
2089
+ if (this.active) {
2090
+ this.redraw();
2091
+ }
2092
+ }
2093
+ redraw() {
2094
+ if (!this.active) {
2095
+ return;
2096
+ }
2097
+ const cols = this.stdout.columns ?? 80;
2098
+ const rows = this.stdout.rows ?? 24;
2099
+ const spinner = AttachSplash.FRAMES[this.frame] ?? "·";
2100
+ const lines = [
2101
+ `${spinner} ${this.status}`,
2102
+ ` Ctrl+C to disconnect`,
2103
+ ];
2104
+ const top = Math.max(1, Math.floor(rows / 2) - 1);
2105
+ this.stdout.write("\x1b[2J");
2106
+ for (let i = 0; i < lines.length; i++) {
2107
+ const line = lines[i] ?? "";
2108
+ const visible = stripAnsi(line);
2109
+ const left = Math.max(1, Math.floor((cols - visible.length) / 2) + 1);
2110
+ this.stdout.write(`\x1b[${top + i};${left}H${line}`);
2111
+ }
2112
+ }
2113
+ handoff() {
2114
+ if (!this.active) {
2115
+ return;
2116
+ }
2117
+ this.stop();
2118
+ this.stdout.write("\x1b[?1049l\x1b[?25h");
2119
+ }
2120
+ dispose() {
2121
+ if (!this.active) {
2122
+ return;
2123
+ }
2124
+ this.stop();
2125
+ this.stdout.write("\x1b[?25h");
2126
+ }
2127
+ stop() {
2128
+ this.active = false;
2129
+ if (this.timer) {
2130
+ clearInterval(this.timer);
2131
+ this.timer = undefined;
2132
+ }
2133
+ }
2134
+ }
2135
+ function stripAnsi(value) {
2136
+ return value.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
2137
+ }
2138
+ async function maybeAutoAttach(result, options) {
2139
+ if (options.json || options.noAttach) {
2140
+ return;
2141
+ }
2142
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2143
+ return;
2144
+ }
2145
+ if (process.env.RUDDER_CLOUD_NO_ATTACH === "1") {
2146
+ return;
2147
+ }
2148
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
2149
+ return;
2150
+ }
2151
+ const record = result;
2152
+ if (typeof record.bootstrapCommand === "string") {
2153
+ return;
2154
+ }
2155
+ const sailId = extractSailId(record);
2156
+ if (!sailId) {
2157
+ return;
2158
+ }
2159
+ try {
2160
+ await runAttach({ kind: "sail", id: sailId, label: sailId }, { ...options, quietBanner: false });
2161
+ }
2162
+ catch (error) {
2163
+ console.warn(`Could not attach to ${sailId}: ${error instanceof Error ? error.message : String(error)}`);
2164
+ }
2165
+ }
2166
+ function extractSailId(record) {
2167
+ if (typeof record.id === "string" && record.id) {
2168
+ return record.id;
2169
+ }
2170
+ return undefined;
2171
+ }
2172
+ function printCloudHelp() {
2173
+ console.log(`rudder cloud
2174
+
2175
+ Usage:
2176
+ rudder cloud login
2177
+ rudder cloud help
2178
+ rudder cloud [name or task]
2179
+ rudder cloud launch [--home-path <path>] ["task"]
2180
+ rudder cloud byoc [ssh-host]
2181
+ rudder cloud vm ["task"]
2182
+ rudder cloud list
2183
+ rudder cloud onload [runId]
2184
+ no runId uploads the current Rudder workspace state
2185
+ rudder cloud logs <id>
2186
+ rudder cloud attach <id>
2187
+ stream the live cloud worker terminal into this pane
2188
+ rudder cloud workspace [attach [id]|share|status [--json]|stop <id>|list]
2189
+ shared cloud workspace for this repo
2190
+ rudder cloud bootstrap <id>
2191
+ rudder cloud runtime [fly|byoc]
2192
+ rudder cloud setup-byoc <ssh-host> compatibility alias
2193
+ rudder cloud setup-fly
2194
+ rudder sail [name or task]
2195
+ rudder sail list
2196
+ rudder sail pause <id>
2197
+ rudder sail resume <id>
2198
+ rudder cloud setup-github <client-id>
2199
+ rudder cloud setup-google <client-id>
2200
+
2201
+ Environment:
2202
+ RUDDER_CLOUD_URL Cloud control plane URL (defaults to ${DEFAULT_CLOUD_URL})
2203
+ RUDDER_CLOUD_RUNTIME fly, byoc, or byo-vm (overrides saved local default)
2204
+ RUDDER_CLOUD_HOME_PATHS Extra comma-separated HOME paths to include in snapshots
2205
+ RUDDER_GITHUB_CLIENT_ID GitHub App OAuth client ID for setup-github
2206
+ RUDDER_GITHUB_CLIENT_SECRET GitHub App OAuth client secret for setup-github
2207
+ RUDDER_GOOGLE_CLIENT_ID Google OAuth client ID for setup-google
2208
+ RUDDER_GOOGLE_CLIENT_SECRET Google OAuth client secret for setup-google
2209
+ `);
2210
+ }
2211
+ const CLOUD_ADJECTIVES = [
2212
+ "amber",
2213
+ "bright",
2214
+ "calm",
2215
+ "clear",
2216
+ "cosmic",
2217
+ "gentle",
2218
+ "golden",
2219
+ "lucky",
2220
+ "rapid",
2221
+ "silver",
2222
+ "steady",
2223
+ "swift",
2224
+ ];
2225
+ const CLOUD_NOUNS = [
2226
+ "atlas",
2227
+ "harbor",
2228
+ "signal",
2229
+ "summit",
2230
+ "orbit",
2231
+ "ranger",
2232
+ "river",
2233
+ "rocket",
2234
+ "sparrow",
2235
+ "station",
2236
+ "voyager",
2237
+ "wave",
2238
+ ];
2239
+ function randomCloudName() {
2240
+ const seed = Date.now() + process.pid + Math.floor(Math.random() * 1_000_000);
2241
+ return [
2242
+ CLOUD_ADJECTIVES[Math.abs(seed) % CLOUD_ADJECTIVES.length],
2243
+ CLOUD_NOUNS[Math.abs(Math.floor(seed / CLOUD_ADJECTIVES.length)) % CLOUD_NOUNS.length],
2244
+ ].join("-");
2245
+ }
2246
+ function cloudNameFromTask(task) {
2247
+ const slug = task
2248
+ .toLowerCase()
2249
+ .replace(/[^a-z0-9]+/g, "-")
2250
+ .replace(/^-+|-+$/g, "")
2251
+ .slice(0, 36)
2252
+ .replace(/-+$/g, "");
2253
+ return slug || randomCloudName();
2254
+ }
2255
+ //# sourceMappingURL=cloud.js.map