@viraatdas/rudder 3.0.3 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/dist/cloud.d.ts +17 -0
- package/dist/cloud.js +275 -4
- package/dist/cloud.js.map +1 -1
- package/dist/jj.d.ts +17 -0
- package/dist/jj.js +2 -2
- package/dist/jj.js.map +1 -1
- package/dist/main.js +41 -2
- package/dist/main.js.map +1 -1
- package/dist/move-home.d.ts +80 -0
- package/dist/move-home.js +418 -0
- package/dist/move-home.js.map +1 -0
- package/dist/native/darwin-arm64/rudder-native +0 -0
- package/dist/native/darwin-x64/rudder-native +0 -0
- package/dist/native/linux-x64/rudder-native +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -479,6 +479,7 @@ control surface; you decide whether a task runs locally or is handed to the clou
|
|
|
479
479
|
rudder login # sign in with Google (opens straight to the account picker)
|
|
480
480
|
rudder cloud # open this repo on your home machine and attach to it
|
|
481
481
|
rudder cloud home # home machine status: projects, agents, previews
|
|
482
|
+
rudder cloud move # move this repo's running agents to the home machine
|
|
482
483
|
rudder cloud app # the phone app link
|
|
483
484
|
rudder cloud list # list cloud workers
|
|
484
485
|
rudder cloud logs <id> # worker status
|
|
@@ -503,7 +504,9 @@ machine of its own.
|
|
|
503
504
|
|
|
504
505
|
**Phone.** Open `rudder cloud app` on your phone and sign in with the same
|
|
505
506
|
Google account. You see every project and agent, read conversations as they
|
|
506
|
-
happen, text any agent, start tasks
|
|
507
|
+
happen, text any agent, start tasks (`/model` picks Claude or Codex for new ones),
|
|
508
|
+
stop, merge or delete agents (swipe left), and open previews. A message that can't
|
|
509
|
+
reach an agent (a menu is open in its pane) says so instead of vanishing. Claude
|
|
507
510
|
panes in the cloud also run with Remote Control, so they appear in the Claude
|
|
508
511
|
app too.
|
|
509
512
|
|
|
@@ -520,11 +523,16 @@ running when this laptop disconnects (`/cloud off` returns to local workers).
|
|
|
520
523
|
selected auth/config) so the whole dashboard runs on a cloud machine. Completed
|
|
521
524
|
cloud work returns through the same review and merge path as local work.
|
|
522
525
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
526
|
+
**Taking agents with you.** About to close the laptop? Press `H` in the
|
|
527
|
+
dashboard and every running agent moves to your home machine: its work so far
|
|
528
|
+
(the jj change, including unpushed local changes it builds on) and its
|
|
529
|
+
conversation go along, nothing is pushed to GitHub, and the conversation resumes
|
|
530
|
+
there, so it keeps going with the laptop closed. Quitting with agents running
|
|
531
|
+
offers the same thing ("3 agents are running here. Move them to the home
|
|
532
|
+
machine?"), and `rudder cloud` in a repo with agents asks too. Bare `/handoff`
|
|
533
|
+
moves just the selected agent. (`/handoff <session-id>` still adopts an outside
|
|
534
|
+
CLI chat, like `/resume`.) Delete an agent anywhere (`dd` here, swipe left on the
|
|
535
|
+
phone) and it is gone everywhere.
|
|
528
536
|
|
|
529
537
|
Cloud comes in two shapes: a **sail** is an ephemeral, task-scoped worker (it goes away
|
|
530
538
|
when the task is done; idle sails pause and can resume), while a **workspace** is a
|
package/dist/cloud.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type MigrationCandidate, type MigrationPlan } from "./migration.js";
|
|
2
|
+
import { type MoveCandidate } from "./move-home.js";
|
|
3
|
+
import type { JsonValue } from "./types.js";
|
|
2
4
|
type CloudCommandOptions = {
|
|
3
5
|
json?: boolean;
|
|
4
6
|
homePaths?: string[];
|
|
@@ -9,6 +11,13 @@ type CloudCommandOptions = {
|
|
|
9
11
|
/** Skip the y/N confirmation of a destructive command (`--yes`/`--force`). */
|
|
10
12
|
yes?: boolean;
|
|
11
13
|
};
|
|
14
|
+
type CloudClient = {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
request<T>(pathOrUrl: string, init: {
|
|
17
|
+
method: string;
|
|
18
|
+
body?: JsonValue;
|
|
19
|
+
}): Promise<T>;
|
|
20
|
+
};
|
|
12
21
|
export declare function runCloudCommand(command: string, args: string[], options?: CloudCommandOptions): Promise<void>;
|
|
13
22
|
/**
|
|
14
23
|
* Scope a fleet migration plan down to one run: only `runId` moves, every
|
|
@@ -25,4 +34,12 @@ export declare function copyRudderState(repoRoot: string, repoStage: string, onl
|
|
|
25
34
|
runs: number;
|
|
26
35
|
files: string[];
|
|
27
36
|
}>;
|
|
37
|
+
/** Bundle, upload, wait for the home machine to stage them, then hand the local records over. */
|
|
38
|
+
export declare function moveAgentsToHome(client: CloudClient, repoRoot: string, slug: string, candidates: MoveCandidate[], options: CloudCommandOptions): Promise<{
|
|
39
|
+
adopted: string[];
|
|
40
|
+
failed: Array<{
|
|
41
|
+
runId: string;
|
|
42
|
+
error: string;
|
|
43
|
+
}>;
|
|
44
|
+
}>;
|
|
28
45
|
export {};
|
package/dist/cloud.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import fsp from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { WebSocket } from "ws";
|
|
7
7
|
import { currentBranch, currentCommit, findRepoRoot } from "./git.js";
|
|
8
8
|
import { applyDefaultDecisions, buildFreshHandoffPrompt, cloudWorkspaceAbsolutePath, findMigrationCandidates, formatCandidateReason, migrationSummary, summaryAsJson, } from "./migration.js";
|
|
9
|
+
import { buildMoveBundle, findMoveCandidates, markRunsMovedHome } from "./move-home.js";
|
|
9
10
|
import { cloudAuthPath } from "./state.js";
|
|
10
11
|
import { ensureDir, commandExists, expandHome, isTty, newRunId, nowIso, pathExists, promptConfirm, promptText, promptSelect, promptSecret, readJson, runCommand, shortenHome, shellQuote, writeJson, } from "./util.js";
|
|
11
12
|
const DEFAULT_LOGIN_INTERVAL_MS = 2000;
|
|
@@ -180,6 +181,9 @@ export async function runCloudCommand(command, args, options = {}) {
|
|
|
180
181
|
case "onload":
|
|
181
182
|
await onload(rest, options);
|
|
182
183
|
return;
|
|
184
|
+
case "move":
|
|
185
|
+
await moveCommand(rest, options);
|
|
186
|
+
return;
|
|
183
187
|
case "bootstrap":
|
|
184
188
|
await bootstrap(rest, options);
|
|
185
189
|
return;
|
|
@@ -544,6 +548,9 @@ async function handoffRunToCloud(repoRoot, runId, options) {
|
|
|
544
548
|
body,
|
|
545
549
|
});
|
|
546
550
|
await printResult(result, options);
|
|
551
|
+
// The sail owns the conversation now. The dashboard watches this record
|
|
552
|
+
// for exactly that ("migrated") instead of assuming it at spawn time.
|
|
553
|
+
await markRunsMigrated(repoRoot, [runId]);
|
|
547
554
|
await maybeAutoAttach(result, options);
|
|
548
555
|
// The handoff SUCCEEDED the moment the sail launched: the cloud machine
|
|
549
556
|
// owns the conversation now. An attach that drops afterwards (network
|
|
@@ -3407,6 +3414,10 @@ Usage:
|
|
|
3407
3414
|
holds every repo) and attach to its dashboard. --snapshot uploads this
|
|
3408
3415
|
folder to its own machine instead.
|
|
3409
3416
|
rudder cloud home [status|start|stop|open <owner/repo>]
|
|
3417
|
+
rudder cloud move [runId...] [--yes]
|
|
3418
|
+
move this repo's unfinished agents (or just the named ones) to the
|
|
3419
|
+
home machine: their jj changes and conversations go along, nothing is
|
|
3420
|
+
pushed to GitHub, and they keep working with this laptop closed
|
|
3410
3421
|
rudder cloud app
|
|
3411
3422
|
a one-time code that signs the phone app in to this account
|
|
3412
3423
|
rudder cloud doctor
|
|
@@ -3530,6 +3541,7 @@ async function openOnHome(forceSnapshot, options) {
|
|
|
3530
3541
|
await ensureVaultGithubToken(client, slug);
|
|
3531
3542
|
await ensureVaultAws(client);
|
|
3532
3543
|
await offerEnvFilesToVault(client, repoRoot, name);
|
|
3544
|
+
await offerToMoveAgents(client, repoRoot, slug);
|
|
3533
3545
|
process.stderr.write(`Opening ${slug} on your home machine...\n`);
|
|
3534
3546
|
}
|
|
3535
3547
|
const branch = await pushedBranch(repoRoot);
|
|
@@ -3638,7 +3650,7 @@ async function localGithubToken() {
|
|
|
3638
3650
|
// The cloud machine clones from GitHub, so a private repo needs a GitHub token
|
|
3639
3651
|
// in the vault. gh keeps its token in the OS keychain (hosts.yml has none), so
|
|
3640
3652
|
// it never arrived with the synced gh config; offer to put it there.
|
|
3641
|
-
async function ensureVaultGithubToken(client, slug) {
|
|
3653
|
+
async function ensureVaultGithubToken(client, slug, interactive = true) {
|
|
3642
3654
|
const vault = await client.request("/api/rudder/secrets", { method: "GET" }).catch(() => null);
|
|
3643
3655
|
if (!vault || (vault.secrets ?? []).some((secret) => secret.name === "GH_TOKEN" || secret.name === "GITHUB_TOKEN")) {
|
|
3644
3656
|
return;
|
|
@@ -3648,7 +3660,7 @@ async function ensureVaultGithubToken(client, slug) {
|
|
|
3648
3660
|
process.stderr.write(`The cloud vault has no GitHub token, so private repos can't be cloned there. Run \`gh auth login\` here, or \`rudder cloud secrets set GH_TOKEN\`.\n`);
|
|
3649
3661
|
return;
|
|
3650
3662
|
}
|
|
3651
|
-
if (!isTty()) {
|
|
3663
|
+
if (!isTty() || !interactive) {
|
|
3652
3664
|
process.stderr.write("The cloud vault has no GitHub token; private repos will fail to clone. Run `rudder cloud secrets sync` from a terminal.\n");
|
|
3653
3665
|
return;
|
|
3654
3666
|
}
|
|
@@ -3662,6 +3674,211 @@ async function ensureVaultGithubToken(client, slug) {
|
|
|
3662
3674
|
});
|
|
3663
3675
|
process.stderr.write("Stored GH_TOKEN in the cloud vault.\n");
|
|
3664
3676
|
}
|
|
3677
|
+
const HOME_MOVE_POLL_MS = 1500;
|
|
3678
|
+
const HOME_MOVE_DEADLINE_MS = 15 * 60 * 1000;
|
|
3679
|
+
async function moveCommand(args, options) {
|
|
3680
|
+
const yes = options.yes || args.includes("--yes") || args.includes("-y");
|
|
3681
|
+
const runIds = args.filter((arg) => !arg.startsWith("-"));
|
|
3682
|
+
const repoRoot = findRepoRoot();
|
|
3683
|
+
const slug = await githubSlugFromOrigin(repoRoot);
|
|
3684
|
+
if (!slug) {
|
|
3685
|
+
throw new Error("This repo has no GitHub origin, so the home machine can't clone it. `rudder cloud workspace attach` moves agents into a snapshot workspace instead.");
|
|
3686
|
+
}
|
|
3687
|
+
const candidates = await findMoveCandidates(repoRoot, runIds.length > 0 ? runIds : undefined);
|
|
3688
|
+
const missing = runIds.filter((id) => !candidates.some((candidate) => candidate.runId === id));
|
|
3689
|
+
if (missing.length > 0) {
|
|
3690
|
+
throw new Error(`Not movable (finished, not in a workspace of its own, or unknown): ${missing.join(", ")}`);
|
|
3691
|
+
}
|
|
3692
|
+
if (candidates.length === 0) {
|
|
3693
|
+
if (!options.json) {
|
|
3694
|
+
process.stderr.write("No unfinished agents with workspaces of their own to move.\n");
|
|
3695
|
+
}
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
if (!yes && !options.json) {
|
|
3699
|
+
if (!isTty()) {
|
|
3700
|
+
throw new Error("Refusing to move agents without a terminal to confirm in; pass --yes.");
|
|
3701
|
+
}
|
|
3702
|
+
process.stderr.write(`${describeMoveCandidates(candidates)}\n`);
|
|
3703
|
+
if (!(await promptConfirm(`Move ${agentCount(candidates.length)} to the home machine?`, true))) {
|
|
3704
|
+
process.stderr.write("Nothing moved.\n");
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
const client = await cloudClient({ requireToken: true });
|
|
3709
|
+
if (!options.json) {
|
|
3710
|
+
await ensureVaultGithubToken(client, slug, !yes);
|
|
3711
|
+
}
|
|
3712
|
+
const moved = await moveAgentsToHome(client, repoRoot, slug, candidates, options);
|
|
3713
|
+
if (options.json) {
|
|
3714
|
+
printJson(moved);
|
|
3715
|
+
}
|
|
3716
|
+
if (moved.adopted.length === 0) {
|
|
3717
|
+
process.exitCode = 1;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
function agentCount(count) {
|
|
3721
|
+
return `${count} agent${count === 1 ? "" : "s"}`;
|
|
3722
|
+
}
|
|
3723
|
+
function describeMoveCandidates(candidates) {
|
|
3724
|
+
return candidates
|
|
3725
|
+
.map((candidate) => ` ${candidate.status.padEnd(8)} ${(candidate.taskSummary || candidate.task || candidate.runId).replace(/\s+/g, " ").slice(0, 72)}`)
|
|
3726
|
+
.join("\n");
|
|
3727
|
+
}
|
|
3728
|
+
/** Bundle, upload, wait for the home machine to stage them, then hand the local records over. */
|
|
3729
|
+
export async function moveAgentsToHome(client, repoRoot, slug, candidates, options) {
|
|
3730
|
+
const say = (line) => {
|
|
3731
|
+
if (!options.json) {
|
|
3732
|
+
process.stderr.write(`${line}\n`);
|
|
3733
|
+
}
|
|
3734
|
+
};
|
|
3735
|
+
const stageDir = await fsp.mkdtemp(path.join(os.tmpdir(), "rudder-move-stage-"));
|
|
3736
|
+
const archiveDir = await fsp.mkdtemp(path.join(os.tmpdir(), "rudder-move-"));
|
|
3737
|
+
let job;
|
|
3738
|
+
try {
|
|
3739
|
+
say(`Packing ${agentCount(candidates.length)} (their jj changes and conversations)...`);
|
|
3740
|
+
await buildMoveBundle(repoRoot, candidates, stageDir);
|
|
3741
|
+
const archivePath = path.join(archiveDir, "move.tgz");
|
|
3742
|
+
await runCommand("tar", ["-czf", archivePath, "-C", stageDir, "."], { cwd: stageDir });
|
|
3743
|
+
const branch = await pushedBranch(repoRoot);
|
|
3744
|
+
job = await client.request("/api/rudder/home/move", {
|
|
3745
|
+
method: "POST",
|
|
3746
|
+
body: {
|
|
3747
|
+
repo: slug,
|
|
3748
|
+
...(branch ? { branch } : {}),
|
|
3749
|
+
bundle: await fsp.readFile(archivePath, "base64"),
|
|
3750
|
+
},
|
|
3751
|
+
});
|
|
3752
|
+
}
|
|
3753
|
+
finally {
|
|
3754
|
+
await fsp.rm(stageDir, { recursive: true, force: true });
|
|
3755
|
+
await fsp.rm(archiveDir, { recursive: true, force: true });
|
|
3756
|
+
}
|
|
3757
|
+
const moveId = job.move?.id;
|
|
3758
|
+
if (!moveId) {
|
|
3759
|
+
throw new Error("the control plane did not start the move");
|
|
3760
|
+
}
|
|
3761
|
+
const deadline = Date.now() + HOME_MOVE_DEADLINE_MS;
|
|
3762
|
+
let lastDetail = "";
|
|
3763
|
+
while (job.move?.status !== "done" && job.move?.status !== "failed") {
|
|
3764
|
+
if (Date.now() > deadline) {
|
|
3765
|
+
throw new Error("the home machine did not take the agents in time; they stay here");
|
|
3766
|
+
}
|
|
3767
|
+
const detail = job.move?.detail ?? "";
|
|
3768
|
+
if (detail && detail !== lastDetail) {
|
|
3769
|
+
say(detail);
|
|
3770
|
+
lastDetail = detail;
|
|
3771
|
+
}
|
|
3772
|
+
await new Promise((resolve) => setTimeout(resolve, HOME_MOVE_POLL_MS));
|
|
3773
|
+
job = await client.request(`/api/rudder/home/move/${encodeURIComponent(moveId)}`, { method: "GET" });
|
|
3774
|
+
}
|
|
3775
|
+
if (job.move?.status === "failed") {
|
|
3776
|
+
throw new Error(`the home machine could not take the agents: ${job.move.error || "unknown error"}`);
|
|
3777
|
+
}
|
|
3778
|
+
const adopted = (job.move?.result?.adopted ?? []).map((entry) => entry.runId);
|
|
3779
|
+
const failed = job.move?.result?.failed ?? [];
|
|
3780
|
+
await markRunsMovedHome(repoRoot, adopted, slug);
|
|
3781
|
+
if (adopted.length > 0) {
|
|
3782
|
+
const fresh = (job.move?.result?.adopted ?? []).filter((entry) => entry.resumable === false).length;
|
|
3783
|
+
say(`Moved ${agentCount(adopted.length)} to the home machine. They keep working there with this laptop closed.`);
|
|
3784
|
+
if (fresh > 0) {
|
|
3785
|
+
say(`${agentCount(fresh)} had no saved conversation and restart from a handoff over their diff.`);
|
|
3786
|
+
}
|
|
3787
|
+
say(`Watch them: \`rudder cloud\` here, or on your phone: ${appUrl(client.baseUrl)}`);
|
|
3788
|
+
}
|
|
3789
|
+
for (const entry of failed) {
|
|
3790
|
+
say(`Could not move ${entry.runId}: ${entry.error}. It stays here.`);
|
|
3791
|
+
}
|
|
3792
|
+
return { adopted, failed };
|
|
3793
|
+
}
|
|
3794
|
+
/**
|
|
3795
|
+
* `rudder cloud` in a repo with agents: offer to take them along. A live
|
|
3796
|
+
* dashboard owns its agents' processes, so it does the move (it pauses them
|
|
3797
|
+
* first); with no dashboard running the agents are already stopped and this
|
|
3798
|
+
* process can move them itself.
|
|
3799
|
+
*/
|
|
3800
|
+
async function offerToMoveAgents(client, repoRoot, slug) {
|
|
3801
|
+
if (!isTty() || process.env.RUDDER_CLOUD_RUNTIME === "home" || process.env.RUDDER_CLOUD_NO_MOVE_OFFER === "1") {
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
const candidates = await findMoveCandidates(repoRoot).catch(() => []);
|
|
3805
|
+
if (candidates.length === 0) {
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
const dashboardLive = await dashboardServing(repoRoot);
|
|
3809
|
+
const running = candidates.filter((candidate) => candidate.status === "running");
|
|
3810
|
+
if (dashboardLive && running.length > 0) {
|
|
3811
|
+
const yes = await promptConfirm(`${agentCount(running.length)} ${running.length === 1 ? "is" : "are"} running here. Move them to the home machine?`, true);
|
|
3812
|
+
if (!yes) {
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
const receipt = await requestDashboardMove(repoRoot);
|
|
3816
|
+
process.stderr.write(receipt === "failed"
|
|
3817
|
+
? "The dashboard could not start the move; press H there to try again.\n"
|
|
3818
|
+
: "The dashboard is moving them; they appear on the home machine as they land.\n");
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
if (dashboardLive) {
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
process.stderr.write(`${describeMoveCandidates(candidates)}\n`);
|
|
3825
|
+
const yes = await promptConfirm(`${agentCount(candidates.length)} ${candidates.length === 1 ? "was" : "were"} left unfinished here. Move them to the home machine?`, true);
|
|
3826
|
+
if (!yes) {
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
try {
|
|
3830
|
+
await moveAgentsToHome(client, repoRoot, slug, candidates, {});
|
|
3831
|
+
}
|
|
3832
|
+
catch (error) {
|
|
3833
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
async function dashboardServing(repoRoot) {
|
|
3837
|
+
let socketPath;
|
|
3838
|
+
try {
|
|
3839
|
+
socketPath = await fsp.realpath(path.join(repoRoot, ".rudder", "attach.sock"));
|
|
3840
|
+
}
|
|
3841
|
+
catch {
|
|
3842
|
+
return false;
|
|
3843
|
+
}
|
|
3844
|
+
const net = await import("node:net");
|
|
3845
|
+
return await new Promise((resolve) => {
|
|
3846
|
+
const socket = net.connect(socketPath);
|
|
3847
|
+
const timer = setTimeout(() => {
|
|
3848
|
+
socket.destroy();
|
|
3849
|
+
resolve(false);
|
|
3850
|
+
}, 500);
|
|
3851
|
+
socket.once("connect", () => {
|
|
3852
|
+
clearTimeout(timer);
|
|
3853
|
+
socket.destroy();
|
|
3854
|
+
resolve(true);
|
|
3855
|
+
});
|
|
3856
|
+
socket.once("error", () => {
|
|
3857
|
+
clearTimeout(timer);
|
|
3858
|
+
resolve(false);
|
|
3859
|
+
});
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
/** Ask the live dashboard (through its steer inbox) to move every agent it is running. */
|
|
3863
|
+
async function requestDashboardMove(repoRoot) {
|
|
3864
|
+
const requestId = randomUUID();
|
|
3865
|
+
const inbox = path.join(repoRoot, ".rudder", "steer");
|
|
3866
|
+
await ensureDir(inbox);
|
|
3867
|
+
const file = path.join(inbox, `${String(Date.now()).padStart(13, "0")}-move-home-${requestId}.json`);
|
|
3868
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
3869
|
+
await fsp.writeFile(temp, JSON.stringify({ requestId, kind: "move-home", taskId: "", instruction: "move-home", ts: nowIso() }));
|
|
3870
|
+
await fsp.rename(temp, file);
|
|
3871
|
+
const receiptPath = path.join(repoRoot, ".rudder", "steer-receipts", `${requestId}.json`);
|
|
3872
|
+
const deadline = Date.now() + 10_000;
|
|
3873
|
+
while (Date.now() < deadline) {
|
|
3874
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
3875
|
+
const receipt = await readJson(receiptPath);
|
|
3876
|
+
if (receipt?.status === "delivered" || receipt?.status === "failed") {
|
|
3877
|
+
return receipt.status;
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
return "queued";
|
|
3881
|
+
}
|
|
3665
3882
|
async function localIdentities() {
|
|
3666
3883
|
const [github, claude] = await Promise.all([
|
|
3667
3884
|
commandExists("gh")
|
|
@@ -3852,6 +4069,56 @@ async function printAppLink(options) {
|
|
|
3852
4069
|
or open ${pairing.link}
|
|
3853
4070
|
Browser: ${pairing.url}`);
|
|
3854
4071
|
}
|
|
4072
|
+
/**
|
|
4073
|
+
* `rudder cloud home runs <runId...>`: which of these agents (moved here from a
|
|
4074
|
+
* laptop) still exist on the home machine; `known: false` when it cannot
|
|
4075
|
+
* answer, which callers must read as "unknown", never "deleted".
|
|
4076
|
+
* `rudder cloud home delete <runId...>`: delete them there, through that repo's
|
|
4077
|
+
* dashboard, which also takes them off the phone. Queued if the machine is
|
|
4078
|
+
* asleep. This is what `dd` on a moved row in the laptop's dashboard runs.
|
|
4079
|
+
*/
|
|
4080
|
+
async function homeAgentsCommand(client, sub, args, options) {
|
|
4081
|
+
const yes = options.yes || args.includes("--yes") || args.includes("-y");
|
|
4082
|
+
const runIds = args.filter((arg) => !arg.startsWith("-"));
|
|
4083
|
+
if (runIds.length === 0) {
|
|
4084
|
+
throw new Error(`Usage: rudder cloud home ${sub} <runId...>`);
|
|
4085
|
+
}
|
|
4086
|
+
const repo = await githubSlugFromOrigin(findRepoRoot());
|
|
4087
|
+
if (!repo) {
|
|
4088
|
+
throw new Error("This repo has no GitHub origin, so none of its agents live on the home machine.");
|
|
4089
|
+
}
|
|
4090
|
+
if (sub === "delete" && !yes && !options.json) {
|
|
4091
|
+
if (!isTty()) {
|
|
4092
|
+
throw new Error("Refusing to delete agents without a terminal to confirm in; pass --yes.");
|
|
4093
|
+
}
|
|
4094
|
+
if (!(await promptConfirm(`Delete ${runIds.length === 1 ? "this agent" : `${runIds.length} agents`} on the home machine? This cannot be undone.`, false))) {
|
|
4095
|
+
console.log("Nothing deleted.");
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
const result = await client.request("/api/rudder/home/agents", {
|
|
4100
|
+
method: "POST",
|
|
4101
|
+
body: { repo, action: sub === "runs" ? "check" : "delete", runIds },
|
|
4102
|
+
});
|
|
4103
|
+
if (options.json) {
|
|
4104
|
+
printJson(result);
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
if (sub === "runs") {
|
|
4108
|
+
if (!result.known) {
|
|
4109
|
+
console.log("The home machine is not connected, so it can't say.");
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
const existing = new Set(result.existing ?? []);
|
|
4113
|
+
for (const id of runIds) {
|
|
4114
|
+
console.log(`${existing.has(id) ? "there " : "gone "} ${id}`);
|
|
4115
|
+
}
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
console.log(result.status === "queued"
|
|
4119
|
+
? "The home machine is asleep; it deletes them as soon as it is back."
|
|
4120
|
+
: "Deleted on the home machine.");
|
|
4121
|
+
}
|
|
3855
4122
|
async function homeCommand(args, options) {
|
|
3856
4123
|
const client = await cloudClient({ requireToken: true });
|
|
3857
4124
|
const sub = args[0] ?? "status";
|
|
@@ -3885,8 +4152,12 @@ async function homeCommand(args, options) {
|
|
|
3885
4152
|
}
|
|
3886
4153
|
return;
|
|
3887
4154
|
}
|
|
4155
|
+
if (sub === "runs" || sub === "delete") {
|
|
4156
|
+
await homeAgentsCommand(client, sub, args.slice(1), options);
|
|
4157
|
+
return;
|
|
4158
|
+
}
|
|
3888
4159
|
if (sub !== "status") {
|
|
3889
|
-
throw new Error("Usage: rudder cloud home [status|start|stop|open <owner/repo
|
|
4160
|
+
throw new Error("Usage: rudder cloud home [status|start|stop|open <owner/repo>|runs <runId...>|delete <runId...>]");
|
|
3890
4161
|
}
|
|
3891
4162
|
const result = await client.request("/api/rudder/home", { method: "GET" });
|
|
3892
4163
|
if (options.json) {
|