@heyditto/cli 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/teleport/api.d.ts +139 -0
- package/dist/teleport/api.js +61 -0
- package/dist/teleport/api.js.map +1 -0
- package/dist/teleport/bundle.d.ts +35 -0
- package/dist/teleport/bundle.js +102 -0
- package/dist/teleport/bundle.js.map +1 -0
- package/dist/teleport/chunks.d.ts +13 -0
- package/dist/teleport/chunks.js +57 -0
- package/dist/teleport/chunks.js.map +1 -0
- package/dist/teleport/commands.d.ts +74 -0
- package/dist/teleport/commands.js +488 -0
- package/dist/teleport/commands.js.map +1 -0
- package/dist/teleport/discover.d.ts +13 -0
- package/dist/teleport/discover.js +64 -0
- package/dist/teleport/discover.js.map +1 -0
- package/dist/teleport/git.d.ts +18 -0
- package/dist/teleport/git.js +41 -0
- package/dist/teleport/git.js.map +1 -0
- package/dist/teleport/harness.d.ts +29 -0
- package/dist/teleport/harness.js +121 -0
- package/dist/teleport/harness.js.map +1 -0
- package/dist/teleport/offload.d.ts +32 -0
- package/dist/teleport/offload.js +83 -0
- package/dist/teleport/offload.js.map +1 -0
- package/dist/teleport/pull.d.ts +12 -0
- package/dist/teleport/pull.js +118 -0
- package/dist/teleport/pull.js.map +1 -0
- package/dist/teleport/push.d.ts +49 -0
- package/dist/teleport/push.js +261 -0
- package/dist/teleport/push.js.map +1 -0
- package/dist/teleport/restore.d.ts +16 -0
- package/dist/teleport/restore.js +163 -0
- package/dist/teleport/restore.js.map +1 -0
- package/dist/teleport/storage.d.ts +53 -0
- package/dist/teleport/storage.js +43 -0
- package/dist/teleport/storage.js.map +1 -0
- package/dist/teleport/types.d.ts +100 -0
- package/dist/teleport/types.js +85 -0
- package/dist/teleport/types.js.map +1 -0
- package/dist/teleport/worktree.d.ts +22 -0
- package/dist/teleport/worktree.js +94 -0
- package/dist/teleport/worktree.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { open } from "node:fs/promises";
|
|
4
|
+
import { CHUNK_BYTES } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Splits a file into content-addressed chunks of at most CHUNK_BYTES. Returns
|
|
7
|
+
* one ChunkRef per chunk in order; the caller uploads by sha256 and records the
|
|
8
|
+
* sequence in the manifest so the file can be reassembled.
|
|
9
|
+
*/
|
|
10
|
+
export async function chunkFile(file) {
|
|
11
|
+
const refs = [];
|
|
12
|
+
const stream = createReadStream(file, { highWaterMark: CHUNK_BYTES });
|
|
13
|
+
let carry = Buffer.alloc(0);
|
|
14
|
+
const flush = (buf) => {
|
|
15
|
+
refs.push({ sha256: sha256(buf), size: buf.length });
|
|
16
|
+
};
|
|
17
|
+
for await (const piece of stream) {
|
|
18
|
+
const buf = Buffer.from(piece);
|
|
19
|
+
carry = carry.length === 0 ? buf : Buffer.concat([carry, buf]);
|
|
20
|
+
while (carry.length >= CHUNK_BYTES) {
|
|
21
|
+
flush(Buffer.from(carry.subarray(0, CHUNK_BYTES)));
|
|
22
|
+
carry = Buffer.from(carry.subarray(CHUNK_BYTES));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (carry.length > 0 || refs.length === 0)
|
|
26
|
+
flush(carry);
|
|
27
|
+
return refs;
|
|
28
|
+
}
|
|
29
|
+
/** Reads chunk N of a file (N-th CHUNK_BYTES window). */
|
|
30
|
+
export async function readChunk(file, index) {
|
|
31
|
+
const fh = await open(file, "r");
|
|
32
|
+
try {
|
|
33
|
+
const buf = Buffer.alloc(CHUNK_BYTES);
|
|
34
|
+
const { bytesRead } = await fh.read(buf, 0, CHUNK_BYTES, index * CHUNK_BYTES);
|
|
35
|
+
return buf.subarray(0, bytesRead);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await fh.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function sha256(buf) {
|
|
42
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
43
|
+
}
|
|
44
|
+
export function sha256String(text) {
|
|
45
|
+
return createHash("sha256").update(text).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
/** Total distinct bytes across a set of chunk refs, deduped by sha256. */
|
|
48
|
+
export function dedupedBytes(refs) {
|
|
49
|
+
const seen = new Map();
|
|
50
|
+
for (const r of refs)
|
|
51
|
+
seen.set(r.sha256, r.size);
|
|
52
|
+
let total = 0;
|
|
53
|
+
for (const size of seen.values())
|
|
54
|
+
total += size;
|
|
55
|
+
return total;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=chunks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunks.js","sourceRoot":"","sources":["../../src/teleport/chunks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type InferenceEndpoint } from "../api.js";
|
|
3
|
+
import { pushCapsule } from "../teleport/push.js";
|
|
4
|
+
interface OutputOptions {
|
|
5
|
+
output?: string;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface PushOptions extends OutputOptions {
|
|
9
|
+
name?: string;
|
|
10
|
+
mirror?: string;
|
|
11
|
+
includeIgnored?: string[];
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
session?: string;
|
|
14
|
+
harness?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function cmdTeleportPush(pathArg: string | undefined, options: PushOptions): Promise<void>;
|
|
17
|
+
/** The JSON shape of a push: capsule identity plus the PushOutput fields. */
|
|
18
|
+
export type PushSummary = {
|
|
19
|
+
capsuleId: string;
|
|
20
|
+
name: string;
|
|
21
|
+
} & Awaited<ReturnType<typeof pushCapsule>>;
|
|
22
|
+
interface PullOptions extends OutputOptions {
|
|
23
|
+
capsule?: string;
|
|
24
|
+
generation?: string;
|
|
25
|
+
into?: string;
|
|
26
|
+
restoreHarness?: boolean;
|
|
27
|
+
resume?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function cmdTeleportPull(nameArg: string | undefined, pathArg: string | undefined, options: PullOptions): Promise<void>;
|
|
30
|
+
export declare function cmdTeleportList(options: OutputOptions): Promise<void>;
|
|
31
|
+
export declare function cmdTeleportStatus(ref: string, options: OutputOptions): Promise<void>;
|
|
32
|
+
export declare function cmdTeleportVerify(ref: string, options: OutputOptions): Promise<void>;
|
|
33
|
+
export declare function cmdTeleportGenerations(ref: string, options: OutputOptions): Promise<void>;
|
|
34
|
+
export declare function cmdTeleportTargets(options: OutputOptions): Promise<void>;
|
|
35
|
+
export declare function cmdTeleportRm(ref: string, options: OutputOptions & {
|
|
36
|
+
yes?: boolean;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
interface TeleportOptions extends PushOptions {
|
|
39
|
+
cloud?: boolean;
|
|
40
|
+
endpoint?: string;
|
|
41
|
+
prompt?: string;
|
|
42
|
+
}
|
|
43
|
+
/** The bare `heyditto teleport` command: push the current dir, optionally launch a cloud session. */
|
|
44
|
+
export declare function cmdTeleport(pathArg: string | undefined, options: TeleportOptions): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Turns `--endpoint <id|slug|name>` into the endpoint the backend expects by
|
|
47
|
+
* UUID. Omitted: the user's only endpoint when exactly one exists, otherwise
|
|
48
|
+
* the backend's default. Unknown values list what is available.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveEndpoint(option: string | undefined): Promise<InferenceEndpoint | undefined>;
|
|
51
|
+
export declare function cmdOffload(pathArg: string | undefined, options: {
|
|
52
|
+
yes?: boolean;
|
|
53
|
+
allowUnpushed?: boolean;
|
|
54
|
+
name?: string;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
interface StorageAddOptions extends OutputOptions {
|
|
57
|
+
name?: string;
|
|
58
|
+
endpoint?: string;
|
|
59
|
+
region?: string;
|
|
60
|
+
bucket?: string;
|
|
61
|
+
accessKey?: string;
|
|
62
|
+
secretKey?: string;
|
|
63
|
+
default?: boolean;
|
|
64
|
+
/** commander maps --no-mirror to mirror=false */
|
|
65
|
+
mirror?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export declare function cmdStorageAdd(options: StorageAddOptions): Promise<void>;
|
|
68
|
+
export declare function cmdStorageList(options: OutputOptions): Promise<void>;
|
|
69
|
+
/** `<bucket>` is a friendly name or an id. */
|
|
70
|
+
export declare function cmdStorageTest(ref: string, options: OutputOptions): Promise<void>;
|
|
71
|
+
export declare function cmdStorageRemove(ref: string): Promise<void>;
|
|
72
|
+
export declare function cmdStorageMirror(ref: string, targetsArg: string): Promise<void>;
|
|
73
|
+
export declare function registerTeleportCommands(program: Command, addExamples: (c: Command, ex: string) => Command): void;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Option } from "commander";
|
|
4
|
+
import { configDir } from "../config.js";
|
|
5
|
+
import { ApiError, listEndpoints } from "../api.js";
|
|
6
|
+
import { listSessions } from "../agents/sessions.js";
|
|
7
|
+
import { launchHarness } from "../agents/launch.js";
|
|
8
|
+
import * as tapi from "../teleport/api.js";
|
|
9
|
+
import { detectCommitter, pushCapsule } from "../teleport/push.js";
|
|
10
|
+
import { pullCapsule, readCachedManifest, writeCachedManifest } from "../teleport/pull.js";
|
|
11
|
+
import { deleteLocalRoot, unpushedRepos, waitForOffloadReady } from "../teleport/offload.js";
|
|
12
|
+
import * as storage from "../teleport/storage.js";
|
|
13
|
+
import { discoverRepos } from "../teleport/discover.js";
|
|
14
|
+
import { formatBytes } from "../teleport/types.js";
|
|
15
|
+
function out(line) {
|
|
16
|
+
process.stdout.write(`${line}\n`);
|
|
17
|
+
}
|
|
18
|
+
function err(line) {
|
|
19
|
+
process.stderr.write(`${line}\n`);
|
|
20
|
+
}
|
|
21
|
+
function json(options) {
|
|
22
|
+
return options.json === true || options.output === "json";
|
|
23
|
+
}
|
|
24
|
+
function pad(s, n) {
|
|
25
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
26
|
+
}
|
|
27
|
+
function harnessKindOf(h) {
|
|
28
|
+
if (h === "claude")
|
|
29
|
+
return "claude-code";
|
|
30
|
+
if (h === "codex")
|
|
31
|
+
return "codex";
|
|
32
|
+
if (h === "claude-code" || h === "codex")
|
|
33
|
+
return h;
|
|
34
|
+
return "none";
|
|
35
|
+
}
|
|
36
|
+
async function requireTty(prompt) {
|
|
37
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
38
|
+
throw new Error("this command needs an interactive terminal; pass the value as an argument instead");
|
|
39
|
+
}
|
|
40
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
41
|
+
try {
|
|
42
|
+
return (await rl.question(prompt)).trim();
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
rl.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Finds or creates the capsule for a root path; `{capsule}` routes accept the name directly. */
|
|
49
|
+
async function resolveCapsule(root, opts) {
|
|
50
|
+
const discovery = await discoverRepos(root);
|
|
51
|
+
const name = opts.name?.trim() || path.basename(path.resolve(root));
|
|
52
|
+
let capsule;
|
|
53
|
+
try {
|
|
54
|
+
capsule = await tapi.getCapsule(name);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (!(e instanceof Error) || !/HTTP 404/.test(e.message))
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
if (!capsule) {
|
|
61
|
+
if (!opts.create)
|
|
62
|
+
throw new Error(`no capsule named "${name}"; push it first with \`heyditto teleport push\``);
|
|
63
|
+
capsule = await tapi.createCapsule({
|
|
64
|
+
name,
|
|
65
|
+
rootKind: discovery.kind,
|
|
66
|
+
harnessKind: opts.harness?.kind === "none" ? undefined : opts.harness?.kind,
|
|
67
|
+
harnessSessionId: opts.harness?.sessionId,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const previous = (await readCachedManifest(configDir(), capsule.id));
|
|
71
|
+
return { capsule, previous };
|
|
72
|
+
}
|
|
73
|
+
/** Pull and cloud sessions need at least one committed generation. */
|
|
74
|
+
function requireGenerations(capsule) {
|
|
75
|
+
if (capsule.headGeneration > 0)
|
|
76
|
+
return;
|
|
77
|
+
throw new Error(`capsule ${capsule.name} has no generations yet — push it first with \`heyditto teleport push\``);
|
|
78
|
+
}
|
|
79
|
+
export async function cmdTeleportPush(pathArg, options) {
|
|
80
|
+
const summary = await runPush(pathArg, options);
|
|
81
|
+
if (!summary)
|
|
82
|
+
return; // dry run already printed its plan
|
|
83
|
+
if (json(options)) {
|
|
84
|
+
out(JSON.stringify(summary, null, 2));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
printPushSummary(summary);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Discovers, pushes and caches the manifest. Prints the dry-run plan itself
|
|
91
|
+
* (and returns null); otherwise returns the summary for the caller to print.
|
|
92
|
+
*/
|
|
93
|
+
async function runPush(pathArg, options) {
|
|
94
|
+
const root = path.resolve(pathArg ?? process.cwd());
|
|
95
|
+
const discovery = await discoverRepos(root);
|
|
96
|
+
const harnessKind = harnessKindOf(options.harness);
|
|
97
|
+
const harness = { kind: harnessKind, sessionId: options.session, cwd: harnessKind === "none" ? undefined : root };
|
|
98
|
+
if (options.dryRun) {
|
|
99
|
+
out(JSON.stringify({ root, rootKind: discovery.kind, repos: discovery.repos, harness, mirror: options.mirror ?? "all" }, null, 2));
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const { capsule, previous } = await resolveCapsule(root, { name: options.name, create: true, harness });
|
|
103
|
+
if (options.mirror)
|
|
104
|
+
await tapi.setMirrorPolicy(capsule.id, parsePolicy(options.mirror));
|
|
105
|
+
err(`Teleporting ${discovery.repos.length} repo(s) from ${root} → capsule ${capsule.name}…`);
|
|
106
|
+
const result = await pushCapsule({
|
|
107
|
+
root,
|
|
108
|
+
capsuleId: capsule.id,
|
|
109
|
+
parentGeneration: capsule.headGeneration || null,
|
|
110
|
+
previousManifest: previous,
|
|
111
|
+
harness,
|
|
112
|
+
ignoredIncludes: options.includeIgnored ?? [],
|
|
113
|
+
rootName: capsule.name,
|
|
114
|
+
rootKind: discovery.kind,
|
|
115
|
+
committedBy: detectCommitter(),
|
|
116
|
+
});
|
|
117
|
+
// Cache the committed manifest for the next thin push.
|
|
118
|
+
const resolved = await tapi.resolveGeneration(capsule.id, result.generation);
|
|
119
|
+
await writeCachedManifest(configDir(), capsule.id, resolved.manifest);
|
|
120
|
+
return { capsuleId: capsule.id, name: capsule.name, ...result };
|
|
121
|
+
}
|
|
122
|
+
function printPushSummary(s) {
|
|
123
|
+
const pct = `${(s.savingsRatio * 100).toFixed(s.savingsRatio > 0.999 ? 2 : 1)}% reused`;
|
|
124
|
+
out(`Pushed generation ${s.generation}: ${formatBytes(s.uploadedBytes)} uploaded ` +
|
|
125
|
+
`(${formatBytes(s.logicalBytes)} logical, ${pct}), ${s.chunkCount} chunks ` +
|
|
126
|
+
`(${s.uploaded} uploaded, ${s.reused} reused).`);
|
|
127
|
+
out(`Pull elsewhere with: heyditto teleport pull ${s.name} <path>`);
|
|
128
|
+
}
|
|
129
|
+
function parsePolicy(raw) {
|
|
130
|
+
if (raw === "all")
|
|
131
|
+
return { mode: "all" };
|
|
132
|
+
return { mode: "some", targets: raw.split(",").map((s) => s.trim()).filter(Boolean) };
|
|
133
|
+
}
|
|
134
|
+
export async function cmdTeleportPull(nameArg, pathArg, options) {
|
|
135
|
+
const ref = options.capsule ?? nameArg;
|
|
136
|
+
if (!ref)
|
|
137
|
+
throw new Error("which capsule? pass a name/id argument or --capsule");
|
|
138
|
+
const capsule = await tapi.getCapsule(ref);
|
|
139
|
+
requireGenerations(capsule);
|
|
140
|
+
const dest = path.resolve(options.into ?? pathArg ?? path.join(process.cwd(), capsule.name));
|
|
141
|
+
const generation = options.generation ? Number(options.generation) : undefined;
|
|
142
|
+
err(`Pulling capsule ${capsule.name}${generation ? ` @${generation}` : ""} → ${dest}…`);
|
|
143
|
+
const result = await pullCapsule(capsule.id, generation, dest, { restoreHarness: options.restoreHarness });
|
|
144
|
+
const resolved = await tapi.resolveGeneration(capsule.id, generation);
|
|
145
|
+
await writeCachedManifest(configDir(), capsule.id, resolved.manifest);
|
|
146
|
+
const harnessKind = resolved.manifest.harness.kind;
|
|
147
|
+
if (json(options)) {
|
|
148
|
+
// Runner contract: the entrypoint reads cwd + harness ids from this line.
|
|
149
|
+
out(JSON.stringify({ cwd: result.harnessCwd ?? result.root, harnessSessionId: result.harnessSessionId, harnessKind, root: result.root, repos: result.repos, generation: resolved.generation }));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
out(`Restored ${result.repos.length} repo(s) into ${result.root} (generation ${resolved.generation}).`);
|
|
153
|
+
if (result.harnessSessionId)
|
|
154
|
+
out(`Harness session ${result.harnessSessionId} restored under ${result.harnessCwd}.`);
|
|
155
|
+
}
|
|
156
|
+
if (options.resume && result.harnessSessionId && harnessKind !== "none") {
|
|
157
|
+
const harness = harnessKind === "claude-code" ? "claude" : "codex";
|
|
158
|
+
err(`Resuming ${harness}…`);
|
|
159
|
+
await launchHarness(harness, [], { resume: result.harnessSessionId });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export async function cmdTeleportList(options) {
|
|
163
|
+
const capsules = await tapi.listCapsules();
|
|
164
|
+
if (json(options))
|
|
165
|
+
return out(JSON.stringify({ capsules }, null, 2));
|
|
166
|
+
if (capsules.length === 0)
|
|
167
|
+
return out("No capsules yet. Push one with `heyditto teleport push`.");
|
|
168
|
+
const rows = capsules.map((c) => [c.name, c.rootKind, `gen ${c.headGeneration}`, formatBytes(c.bytesTotal ?? 0), c.status ?? "active"]);
|
|
169
|
+
const header = ["NAME", "KIND", "HEAD", "SIZE", "STATUS"];
|
|
170
|
+
const w = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
171
|
+
out(header.map((h, i) => pad(h, w[i])).join(" "));
|
|
172
|
+
for (const r of rows)
|
|
173
|
+
out(r.map((c, i) => pad(c, w[i])).join(" "));
|
|
174
|
+
}
|
|
175
|
+
function printMirrors(status) {
|
|
176
|
+
for (const m of status.mirrors) {
|
|
177
|
+
out(` ${pad(m.target, 16)} gen ${m.generation} ${pad(m.status, 9)}${m.required ? " required" : ""}` +
|
|
178
|
+
`${m.verifiedAt ? ` verified ${m.verifiedAt.slice(0, 16)}` : ""}${m.error ? ` (${m.error})` : ""}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export async function cmdTeleportStatus(ref, options) {
|
|
182
|
+
const status = await tapi.capsuleStatus(ref);
|
|
183
|
+
if (json(options))
|
|
184
|
+
return out(JSON.stringify(status, null, 2));
|
|
185
|
+
out(`${status.capsule.name}: head generation ${status.headGeneration}, ${formatBytes(status.bytesTotal ?? 0)}, offload ${status.offloadReady ? "ready" : "not ready"}`);
|
|
186
|
+
printMirrors(status);
|
|
187
|
+
}
|
|
188
|
+
export async function cmdTeleportVerify(ref, options) {
|
|
189
|
+
const status = await tapi.verifyCapsule(ref);
|
|
190
|
+
if (json(options))
|
|
191
|
+
return out(JSON.stringify(status, null, 2));
|
|
192
|
+
out(`${status.capsule.name}: verification ${status.offloadReady ? "complete" : "in progress"}`);
|
|
193
|
+
printMirrors(status);
|
|
194
|
+
}
|
|
195
|
+
export async function cmdTeleportGenerations(ref, options) {
|
|
196
|
+
const gens = await tapi.listGenerations(ref);
|
|
197
|
+
if (json(options))
|
|
198
|
+
return out(JSON.stringify({ generations: gens }, null, 2));
|
|
199
|
+
if (gens.length === 0)
|
|
200
|
+
return out("No generations yet.");
|
|
201
|
+
for (const g of gens)
|
|
202
|
+
out(`gen ${pad(String(g.generation), 4)} ${pad(formatBytes(g.bytes), 10)} ${pad(String(g.chunkCount), 6)} chunks ${g.committedAt.slice(0, 16)} ${g.committedBy}`);
|
|
203
|
+
}
|
|
204
|
+
export async function cmdTeleportTargets(options) {
|
|
205
|
+
const res = await tapi.listTargets();
|
|
206
|
+
if (json(options))
|
|
207
|
+
return out(JSON.stringify(res, null, 2));
|
|
208
|
+
out(`Quota: ${res.quotaGb} GB, capsule limit: ${res.capsuleLimit < 0 ? "unlimited" : res.capsuleLimit}`);
|
|
209
|
+
for (const t of res.targets) {
|
|
210
|
+
out(` ${pad(t.target, 20)} ${pad(t.label, 24)} ${t.required ? "required" : "optional"}${t.available ? "" : " (unavailable)"}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export async function cmdTeleportRm(ref, options) {
|
|
214
|
+
const capsule = await tapi.getCapsule(ref);
|
|
215
|
+
if (!options.yes) {
|
|
216
|
+
const answer = await requireTty(`Delete capsule ${capsule.name} and all its generations? [y/N] `);
|
|
217
|
+
if (answer.toLowerCase() !== "y")
|
|
218
|
+
return err("Aborted.");
|
|
219
|
+
}
|
|
220
|
+
await tapi.deleteCapsule(capsule.id);
|
|
221
|
+
out(`Deleted capsule ${capsule.name}.`);
|
|
222
|
+
}
|
|
223
|
+
/** The bare `heyditto teleport` command: push the current dir, optionally launch a cloud session. */
|
|
224
|
+
export async function cmdTeleport(pathArg, options) {
|
|
225
|
+
const root = path.resolve(pathArg ?? process.cwd());
|
|
226
|
+
// Prefer a coding session whose worktree/cwd matches this root, so the harness travels too.
|
|
227
|
+
const sessions = await listSessions();
|
|
228
|
+
const match = sessions.find((s) => s.cwd === root || s.worktree === root);
|
|
229
|
+
const push = await runPush(root, {
|
|
230
|
+
...options,
|
|
231
|
+
session: match?.harnessSessionId ?? options.session,
|
|
232
|
+
harness: match?.harness ?? options.harness,
|
|
233
|
+
});
|
|
234
|
+
if (!push)
|
|
235
|
+
return; // dry run
|
|
236
|
+
if (!options.cloud) {
|
|
237
|
+
if (json(options))
|
|
238
|
+
return out(JSON.stringify(push, null, 2));
|
|
239
|
+
return printPushSummary(push);
|
|
240
|
+
}
|
|
241
|
+
if (!json(options))
|
|
242
|
+
printPushSummary(push);
|
|
243
|
+
const name = options.name?.trim() || path.basename(root);
|
|
244
|
+
requireGenerations(await tapi.getCapsule(name));
|
|
245
|
+
const harnessKind = harnessKindOf(match?.harness ?? options.harness);
|
|
246
|
+
const endpoint = await resolveEndpoint(options.endpoint);
|
|
247
|
+
if (endpoint)
|
|
248
|
+
err(`Using inference endpoint ${endpoint.slug} (${endpoint.name}).`);
|
|
249
|
+
let session;
|
|
250
|
+
try {
|
|
251
|
+
session = await tapi.launchCloudSession(name, {
|
|
252
|
+
prompt: options.prompt?.trim() || "Resume the teleported session and continue where it left off.",
|
|
253
|
+
harness: harnessKind === "codex" ? "codex" : "claude-code",
|
|
254
|
+
endpointId: endpoint?.id,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
259
|
+
throw new Error("inference endpoint not found; run `heyditto endpoints` and pass --endpoint <slug>.");
|
|
260
|
+
}
|
|
261
|
+
if (e instanceof ApiError && e.status === 503) {
|
|
262
|
+
throw new Error("cloud runner unavailable; try again shortly (the capsule is pushed, re-run `heyditto teleport --cloud`).");
|
|
263
|
+
}
|
|
264
|
+
throw e;
|
|
265
|
+
}
|
|
266
|
+
// One JSON document for scripts: the push result and the session together.
|
|
267
|
+
if (json(options))
|
|
268
|
+
return out(JSON.stringify({ push, cloudSession: session }, null, 2));
|
|
269
|
+
out(`Cloud session started: job ${session.jobId} (${session.harness}, generation ${session.generation}).`);
|
|
270
|
+
// Newer backends return the absolute thread URL for their linked app base;
|
|
271
|
+
// older ones only return ids, so compose the production link as a fallback.
|
|
272
|
+
out(`Open it: ${session.threadUrl || tapi.appThreadUrl(session.threadId)}`);
|
|
273
|
+
}
|
|
274
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
275
|
+
/**
|
|
276
|
+
* Turns `--endpoint <id|slug|name>` into the endpoint the backend expects by
|
|
277
|
+
* UUID. Omitted: the user's only endpoint when exactly one exists, otherwise
|
|
278
|
+
* the backend's default. Unknown values list what is available.
|
|
279
|
+
*/
|
|
280
|
+
export async function resolveEndpoint(option) {
|
|
281
|
+
const { endpoints } = await listEndpoints();
|
|
282
|
+
const wanted = option?.trim();
|
|
283
|
+
if (!wanted) {
|
|
284
|
+
return endpoints.length === 1 ? endpoints[0] : undefined;
|
|
285
|
+
}
|
|
286
|
+
const found = endpoints.find((e) => e.id === wanted) ??
|
|
287
|
+
endpoints.find((e) => e.slug === wanted) ??
|
|
288
|
+
endpoints.find((e) => e.name.toLowerCase() === wanted.toLowerCase());
|
|
289
|
+
if (found)
|
|
290
|
+
return found;
|
|
291
|
+
if (UUID_RE.test(wanted)) {
|
|
292
|
+
// Let the backend judge an id we cannot see (e.g. a shared endpoint).
|
|
293
|
+
return { id: wanted, slug: wanted, name: wanted, model: "" };
|
|
294
|
+
}
|
|
295
|
+
const slugs = endpoints.map((e) => e.slug).join(", ") || "none";
|
|
296
|
+
throw new Error(`no inference endpoint matches "${wanted}"; available: ${slugs}. Create one in Settings → Developer → Inference endpoints.`);
|
|
297
|
+
}
|
|
298
|
+
export async function cmdOffload(pathArg, options) {
|
|
299
|
+
const root = path.resolve(pathArg ?? process.cwd());
|
|
300
|
+
const risky = await unpushedRepos(root);
|
|
301
|
+
if (risky.length > 0 && !options.allowUnpushed) {
|
|
302
|
+
err("Refusing to offload: these repos hold work no remote has:");
|
|
303
|
+
for (const r of risky)
|
|
304
|
+
err(` ${r.relPath} (${r.reason})`);
|
|
305
|
+
err("Push them to a remote, or re-run with --allow-unpushed to keep them only in the capsule.");
|
|
306
|
+
process.exitCode = 1;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
err("Pushing before offload…");
|
|
310
|
+
await cmdTeleportPush(root, { name: options.name });
|
|
311
|
+
const name = options.name?.trim() || path.basename(root);
|
|
312
|
+
const capsule = await tapi.getCapsule(name);
|
|
313
|
+
err("Waiting for redundant mirrors to verify…");
|
|
314
|
+
const readiness = await waitForOffloadReady(capsule.id, {
|
|
315
|
+
onPoll: (s) => err(` mirrors: ${s.mirrors.map((m) => `${m.target}=${m.status}`).join(", ") || "(none)"}`),
|
|
316
|
+
});
|
|
317
|
+
if (!readiness.ready) {
|
|
318
|
+
err("Mirrors are not all verified yet; not deleting anything. Check `heyditto teleport status`.");
|
|
319
|
+
process.exitCode = 1;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!options.yes) {
|
|
323
|
+
const answer = await requireTty(`Verified on ${readiness.mirrors.filter((m) => m.verifiedAt).length} mirror(s). Delete ${root}? [y/N] `);
|
|
324
|
+
if (answer.toLowerCase() !== "y")
|
|
325
|
+
return err("Aborted; capsule kept, local files untouched.");
|
|
326
|
+
}
|
|
327
|
+
const del = await deleteLocalRoot(root);
|
|
328
|
+
await tapi.updateCapsule(capsule.id, { status: "offloaded" });
|
|
329
|
+
out(`Offloaded ${root}.${del.location ? ` Moved to ${del.location}.` : ""}`);
|
|
330
|
+
out(`Recover it with: heyditto teleport pull ${capsule.name} ${root}`);
|
|
331
|
+
}
|
|
332
|
+
export async function cmdStorageAdd(options) {
|
|
333
|
+
const endpoint = options.endpoint ?? (await requireTty("S3 endpoint URL: "));
|
|
334
|
+
const bucket = options.bucket ?? (await requireTty("Bucket name: "));
|
|
335
|
+
const accessKeyID = options.accessKey ?? (await requireTty("Access key id: "));
|
|
336
|
+
const secretAccessKey = options.secretKey ?? (await requireTty("Secret access key: "));
|
|
337
|
+
const input = {
|
|
338
|
+
name: options.name,
|
|
339
|
+
accessKeyID,
|
|
340
|
+
secretAccessKey,
|
|
341
|
+
bucket,
|
|
342
|
+
endpoint,
|
|
343
|
+
region: options.region,
|
|
344
|
+
default: options.default,
|
|
345
|
+
teleportMirror: options.mirror !== false,
|
|
346
|
+
};
|
|
347
|
+
const probe = await storage.testDraft(input);
|
|
348
|
+
if (!probe.ok)
|
|
349
|
+
throw new Error(`could not connect to that bucket${probe.error ? `: ${probe.error}` : ""}`);
|
|
350
|
+
const saved = await storage.addBucket(input);
|
|
351
|
+
if (json(options))
|
|
352
|
+
return out(JSON.stringify(saved, null, 2));
|
|
353
|
+
out(`Added bucket ${saved.name ?? saved.bucket} (${storage.bucketEndpointLabel(saved)}, ${saved.providerKind ?? "s3"}).`);
|
|
354
|
+
}
|
|
355
|
+
export async function cmdStorageList(options) {
|
|
356
|
+
const [buckets, targets] = await Promise.all([storage.listBuckets(), tapi.listTargets().catch(() => undefined)]);
|
|
357
|
+
if (json(options))
|
|
358
|
+
return out(JSON.stringify({ buckets, targets: targets?.targets ?? [] }, null, 2));
|
|
359
|
+
if (targets) {
|
|
360
|
+
out("Mirror targets:");
|
|
361
|
+
for (const t of targets.targets)
|
|
362
|
+
out(` ${pad(t.target, 20)} ${pad(t.label, 24)} ${t.required ? "required" : "optional"}${t.available ? "" : " (unavailable)"}`);
|
|
363
|
+
}
|
|
364
|
+
if (buckets.length === 0)
|
|
365
|
+
return out("No buckets of your own. Add one with `heyditto storage add`.");
|
|
366
|
+
out("Your buckets:");
|
|
367
|
+
for (const b of buckets) {
|
|
368
|
+
const flags = [
|
|
369
|
+
b.default ? "default" : "",
|
|
370
|
+
b.enabled ? "" : "disabled",
|
|
371
|
+
b.teleportMirror ? "mirror" : "",
|
|
372
|
+
b.credentialState && b.credentialState !== "ready" ? b.credentialState : "",
|
|
373
|
+
]
|
|
374
|
+
.filter(Boolean)
|
|
375
|
+
.join(", ");
|
|
376
|
+
out(` ${b.id} ${pad(b.name ?? b.bucket, 20)} ${pad(storage.bucketEndpointLabel(b), 28)}${flags ? ` (${flags})` : ""}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** `<bucket>` is a friendly name or an id. */
|
|
380
|
+
export async function cmdStorageTest(ref, options) {
|
|
381
|
+
const bucket = await storage.resolveBucket(ref);
|
|
382
|
+
const res = await storage.testBucket(bucket.id);
|
|
383
|
+
const label = bucket.name ?? bucket.bucket;
|
|
384
|
+
if (json(options))
|
|
385
|
+
return out(JSON.stringify({ id: bucket.id, ...res }, null, 2));
|
|
386
|
+
out(res.ok ? `Bucket ${label}: connection ok.` : `Bucket ${label}: FAILED${res.error ? ` — ${res.error}` : ""}`);
|
|
387
|
+
if (!res.ok)
|
|
388
|
+
process.exitCode = 1;
|
|
389
|
+
}
|
|
390
|
+
export async function cmdStorageRemove(ref) {
|
|
391
|
+
const bucket = await storage.resolveBucket(ref);
|
|
392
|
+
await storage.removeBucket(bucket.id);
|
|
393
|
+
out(`Removed bucket ${bucket.name ?? bucket.bucket}.`);
|
|
394
|
+
}
|
|
395
|
+
export async function cmdStorageMirror(ref, targetsArg) {
|
|
396
|
+
const capsule = await tapi.setMirrorPolicy(ref, parsePolicy(targetsArg));
|
|
397
|
+
out(`Mirror policy for ${capsule.name}: ${targetsArg}.`);
|
|
398
|
+
}
|
|
399
|
+
// ===== registration =====
|
|
400
|
+
function outputOption() {
|
|
401
|
+
return new Option("--output <format>", "output format").choices(["text", "json"]).default("text");
|
|
402
|
+
}
|
|
403
|
+
function jsonAlias() {
|
|
404
|
+
return new Option("--json", "shorthand for --output json");
|
|
405
|
+
}
|
|
406
|
+
function withOutput(c) {
|
|
407
|
+
return c.addOption(outputOption()).addOption(jsonAlias());
|
|
408
|
+
}
|
|
409
|
+
export function registerTeleportCommands(program, addExamples) {
|
|
410
|
+
const teleport = withOutput(program
|
|
411
|
+
.command("teleport")
|
|
412
|
+
.description("move a coding session and its repos between this machine and Ditto Cloud")
|
|
413
|
+
.summary("teleport repos + session to Ditto Cloud")
|
|
414
|
+
.showHelpAfterError()
|
|
415
|
+
.option("--cloud", "after pushing, resume the session in a Ditto Code cloud job")
|
|
416
|
+
.option("-e, --endpoint <slug>", "inference endpoint for the cloud session (with --cloud)")
|
|
417
|
+
.option("--prompt <text>", "first instruction for the cloud session (with --cloud)")
|
|
418
|
+
.option("--name <name>", "capsule name (default: the directory name)")
|
|
419
|
+
.option("--mirror <policy>", "all | <target>[,<target>…]")
|
|
420
|
+
.option("--include-ignored <glob>", "also capture a git-ignored path (repeatable)", collect, [])
|
|
421
|
+
.option("--dry-run", "print what would be captured, upload nothing")
|
|
422
|
+
.argument("[path]", "root directory (default: current)")
|
|
423
|
+
.action(cmdTeleport));
|
|
424
|
+
withOutput(teleport
|
|
425
|
+
.command("push [path]")
|
|
426
|
+
.description("snapshot a repo or folder of repos to a capsule")
|
|
427
|
+
.option("--name <name>", "capsule name (default: the directory name)")
|
|
428
|
+
.option("--mirror <policy>", "all | <target>[,…]")
|
|
429
|
+
.option("--include-ignored <glob>", "also capture a git-ignored path (repeatable)", collect, [])
|
|
430
|
+
.option("--session <id>", "harness session id to capture with the repos")
|
|
431
|
+
.addOption(new Option("--harness <kind>", "harness whose session to capture").choices(["claude", "codex", "none"]))
|
|
432
|
+
.option("--dry-run", "print what would be captured, upload nothing")
|
|
433
|
+
.action(cmdTeleportPush));
|
|
434
|
+
withOutput(teleport
|
|
435
|
+
.command("pull [name] [path]")
|
|
436
|
+
.description("restore a capsule to a local directory")
|
|
437
|
+
.option("--capsule <ref>", "capsule id or name (alternative to the positional)")
|
|
438
|
+
.option("--generation <n>", "restore a specific generation (default: head)")
|
|
439
|
+
.option("--into <dir>", "destination directory")
|
|
440
|
+
.option("--restore-harness", "also restore the coding-harness session state")
|
|
441
|
+
.option("--resume", "resume the harness after restoring")
|
|
442
|
+
.action((name, pathArg, options) => cmdTeleportPull(name, pathArg, options)));
|
|
443
|
+
withOutput(teleport.command("list").description("list your capsules").action(cmdTeleportList));
|
|
444
|
+
withOutput(teleport.command("status <capsule>").description("mirror + verification status").action(cmdTeleportStatus));
|
|
445
|
+
withOutput(teleport.command("verify <capsule>").description("re-verify a capsule's mirrors").action(cmdTeleportVerify));
|
|
446
|
+
withOutput(teleport.command("generations <capsule>").description("list a capsule's generations").action(cmdTeleportGenerations));
|
|
447
|
+
withOutput(teleport.command("targets").description("mirror targets, quota and capsule limit for your plan").action(cmdTeleportTargets));
|
|
448
|
+
withOutput(teleport.command("rm <capsule>").description("delete a capsule and its generations").option("--yes", "skip the confirmation").action(cmdTeleportRm));
|
|
449
|
+
addExamples(teleport, ` heyditto teleport push the current directory
|
|
450
|
+
heyditto teleport --cloud --endpoint work push, then resume in Ditto Code
|
|
451
|
+
heyditto teleport push ~/code/project --mirror all
|
|
452
|
+
heyditto teleport pull project ~/code/project --restore-harness --resume
|
|
453
|
+
heyditto teleport list`);
|
|
454
|
+
addExamples(program
|
|
455
|
+
.command("offload [path]")
|
|
456
|
+
.description("push a project, verify its mirrors, then delete the local copy")
|
|
457
|
+
.summary("free disk: back up then remove a local project")
|
|
458
|
+
.option("--yes", "skip the delete confirmation")
|
|
459
|
+
.option("--allow-unpushed", "offload even when a repo has commits no remote has")
|
|
460
|
+
.option("--name <name>", "capsule name (default: the directory name)")
|
|
461
|
+
.action(cmdOffload), ` heyditto offload ~/code/old-project
|
|
462
|
+
heyditto offload --yes`);
|
|
463
|
+
const store = program
|
|
464
|
+
.command("storage")
|
|
465
|
+
.description("manage S3-compatible buckets capsules can mirror to")
|
|
466
|
+
.summary("storage mirrors (bring your own bucket)")
|
|
467
|
+
.showHelpAfterError();
|
|
468
|
+
withOutput(store
|
|
469
|
+
.command("add")
|
|
470
|
+
.description("add a bucket (prompts for anything not passed)")
|
|
471
|
+
.option("--name <name>", "friendly label")
|
|
472
|
+
.option("--endpoint <url>", "S3 endpoint URL (AWS, R2, B2, MinIO, Hippius)")
|
|
473
|
+
.option("--no-mirror", "add the bucket without using it as a teleport mirror")
|
|
474
|
+
.option("--region <region>", "region")
|
|
475
|
+
.option("--bucket <bucket>", "bucket name")
|
|
476
|
+
.option("--access-key <id>", "access key id")
|
|
477
|
+
.option("--secret-key <secret>", "secret access key")
|
|
478
|
+
.option("--default", "make this the default bucket")
|
|
479
|
+
.action(cmdStorageAdd));
|
|
480
|
+
withOutput(store.command("list").description("list mirror targets and your buckets").action(cmdStorageList));
|
|
481
|
+
withOutput(store.command("test <bucket>").description("test a bucket's connection (name or id)").action(cmdStorageTest));
|
|
482
|
+
store.command("remove <bucket>").description("remove a bucket (name or id)").action(cmdStorageRemove);
|
|
483
|
+
store.command("mirror <capsule> <targets>").description("set a capsule's mirror policy: all | <target>[,…]").action(cmdStorageMirror);
|
|
484
|
+
}
|
|
485
|
+
function collect(value, previous) {
|
|
486
|
+
return [...previous, value];
|
|
487
|
+
}
|
|
488
|
+
//# sourceMappingURL=commands.js.map
|