sisyphi 1.1.35 → 1.1.37
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/dist/cli.js +397 -91
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +2726 -1410
- package/dist/daemon.js.map +1 -1
- package/dist/templates/companion-plugin/hooks/user-prompt-context.sh +5 -2
- package/dist/templates/orchestrator-base.md +8 -12
- package/dist/templates/orchestrator-discovery.md +1 -1
- package/dist/templates/orchestrator-planning.md +1 -1
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/templates/companion-plugin/hooks/user-prompt-context.sh +5 -2
- package/templates/orchestrator-base.md +8 -12
- package/templates/orchestrator-discovery.md +1 -1
- package/templates/orchestrator-planning.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -539,14 +539,14 @@ __export(creds_exports, {
|
|
|
539
539
|
readTailscaleEnv: () => readTailscaleEnv,
|
|
540
540
|
writeTailscaleEnv: () => writeTailscaleEnv
|
|
541
541
|
});
|
|
542
|
-
import { chmodSync as chmodSync3, existsSync as existsSync27, mkdirSync as
|
|
542
|
+
import { chmodSync as chmodSync3, existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync31 } from "fs";
|
|
543
543
|
import { createInterface as createInterface4 } from "readline";
|
|
544
544
|
function isValidProvider(value) {
|
|
545
545
|
return PROVIDERS.includes(value);
|
|
546
546
|
}
|
|
547
547
|
function ensureDeployDir() {
|
|
548
548
|
const dir = deployDir();
|
|
549
|
-
if (!existsSync27(dir))
|
|
549
|
+
if (!existsSync27(dir)) mkdirSync15(dir, { recursive: true, mode: 448 });
|
|
550
550
|
}
|
|
551
551
|
function parseEnvFile(text) {
|
|
552
552
|
const out = {};
|
|
@@ -573,7 +573,7 @@ function serializeEnvFile(values) {
|
|
|
573
573
|
}
|
|
574
574
|
function readEnvFile(path) {
|
|
575
575
|
if (!existsSync27(path)) return null;
|
|
576
|
-
return parseEnvFile(
|
|
576
|
+
return parseEnvFile(readFileSync31(path, "utf-8"));
|
|
577
577
|
}
|
|
578
578
|
function writeEnvFile(path, values) {
|
|
579
579
|
ensureDeployDir();
|
|
@@ -668,8 +668,8 @@ var init_creds = __esm({
|
|
|
668
668
|
|
|
669
669
|
// src/cli/index.ts
|
|
670
670
|
import { Command } from "commander";
|
|
671
|
-
import { existsSync as
|
|
672
|
-
import { dirname as
|
|
671
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync17, readFileSync as readFileSync36 } from "fs";
|
|
672
|
+
import { dirname as dirname13, join as join32 } from "path";
|
|
673
673
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
674
674
|
|
|
675
675
|
// src/cli/commands/start.ts
|
|
@@ -2628,7 +2628,7 @@ function rawSend2(request, timeoutMs = 1e4) {
|
|
|
2628
2628
|
return rawSend(request, timeoutMs);
|
|
2629
2629
|
}
|
|
2630
2630
|
async function sendRequest(request, timeoutMs) {
|
|
2631
|
-
const
|
|
2631
|
+
const sleep3 = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms));
|
|
2632
2632
|
const MAX_ATTEMPTS = 5;
|
|
2633
2633
|
const RETRY_DELAY_MS = 2e3;
|
|
2634
2634
|
let installedDaemon = false;
|
|
@@ -2650,7 +2650,7 @@ async function sendRequest(request, timeoutMs) {
|
|
|
2650
2650
|
} else {
|
|
2651
2651
|
process.stderr.write(`Daemon not ready, retrying (${attempt}/${MAX_ATTEMPTS - 1})...
|
|
2652
2652
|
`);
|
|
2653
|
-
await
|
|
2653
|
+
await sleep3(RETRY_DELAY_MS);
|
|
2654
2654
|
}
|
|
2655
2655
|
}
|
|
2656
2656
|
}
|
|
@@ -3102,6 +3102,21 @@ ${BOLD}Session: ${session2.id}${RESET}`);
|
|
|
3102
3102
|
console.log(` Last activity: ${formatDuration(Date.now() - lastActivity.getTime())} ago`);
|
|
3103
3103
|
}
|
|
3104
3104
|
console.log(` Orchestrator cycles: ${session2.orchestratorCycles.length}`);
|
|
3105
|
+
if (session2.handoff) {
|
|
3106
|
+
const h = session2.handoff;
|
|
3107
|
+
if (h.lastError) {
|
|
3108
|
+
console.log(` Handoff: ${COLOR_CODES.red}error${RESET} \u2014 ${h.lastError}`);
|
|
3109
|
+
} else if (h.reclaimedAt) {
|
|
3110
|
+
const where = h.target ? `${h.target.provider}:${h.target.repo}` : "cloud";
|
|
3111
|
+
console.log(` Handoff: reclaimed from ${where} at ${h.reclaimedAt}`);
|
|
3112
|
+
} else if (h.sentAt && h.target) {
|
|
3113
|
+
console.log(` Handoff: running on ${h.target.provider}:${h.target.repo} since ${h.sentAt}`);
|
|
3114
|
+
} else if (h.target) {
|
|
3115
|
+
console.log(` Handoff: queued \u2192 ${h.target.provider}:${h.target.repo} (since ${h.queuedAt})`);
|
|
3116
|
+
} else {
|
|
3117
|
+
console.log(` Handoff: quiesce queued (since ${h.queuedAt})`);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3105
3120
|
const runningAgents = session2.agents.filter((a) => a.status === "running");
|
|
3106
3121
|
if (runningAgents.length > 0) {
|
|
3107
3122
|
console.log(`
|
|
@@ -3219,6 +3234,24 @@ var STATUS_COLORS = {
|
|
|
3219
3234
|
var RESET2 = "\x1B[0m";
|
|
3220
3235
|
var BOLD2 = "\x1B[1m";
|
|
3221
3236
|
var DIM2 = "\x1B[2m";
|
|
3237
|
+
var CLOUD = "\x1B[35m";
|
|
3238
|
+
var RED = "\x1B[31m";
|
|
3239
|
+
function handoffAnnotation(h) {
|
|
3240
|
+
if (!h) return "";
|
|
3241
|
+
if (h.lastError) {
|
|
3242
|
+
return ` ${RED}handoff error: ${h.lastError}${RESET2}`;
|
|
3243
|
+
}
|
|
3244
|
+
if (h.reclaimedAt) {
|
|
3245
|
+
return ` ${DIM2}(reclaimed)${RESET2}`;
|
|
3246
|
+
}
|
|
3247
|
+
if (h.sentAt && h.target) {
|
|
3248
|
+
return ` ${CLOUD}\u2192 ${h.target.provider}:${h.target.repo}${RESET2}`;
|
|
3249
|
+
}
|
|
3250
|
+
if (h.target) {
|
|
3251
|
+
return ` ${CLOUD}handoff queued \u2192 ${h.target.provider}:${h.target.repo}${RESET2}`;
|
|
3252
|
+
}
|
|
3253
|
+
return ` ${CLOUD}quiesce queued${RESET2}`;
|
|
3254
|
+
}
|
|
3222
3255
|
function truncateTask(task, max) {
|
|
3223
3256
|
if (task.length <= max) return task;
|
|
3224
3257
|
return task.slice(0, max - 1) + "\u2026";
|
|
@@ -3248,7 +3281,8 @@ function registerList(program2) {
|
|
|
3248
3281
|
const task = truncateTask(s.task, 60);
|
|
3249
3282
|
const label = s.name ? `${s.name} ${DIM2}(${s.id.slice(0, 8)})${RESET2}` : s.id;
|
|
3250
3283
|
const cwdLabel = opts.all && s.cwd ? ` ${DIM2}${basename3(s.cwd)}${RESET2}` : "";
|
|
3251
|
-
|
|
3284
|
+
const handoffLabel = handoffAnnotation(s.handoff);
|
|
3285
|
+
console.log(` ${BOLD2}${label}${RESET2} ${status} ${agents} ${task}${cwdLabel}${handoffLabel}`);
|
|
3252
3286
|
}
|
|
3253
3287
|
if (filtered && totalCount && totalCount > sessions.length) {
|
|
3254
3288
|
const otherCount = totalCount - sessions.length;
|
|
@@ -3877,6 +3911,7 @@ var ACTIONABLE_KINDS = /* @__PURE__ */ new Set([
|
|
|
3877
3911
|
"error"
|
|
3878
3912
|
]);
|
|
3879
3913
|
var HEARTBEAT_ASKED_BY = "system:heartbeat";
|
|
3914
|
+
var ORPHAN_ASKED_BY = "system:orphan-handler";
|
|
3880
3915
|
function maybeNotifyOnAskCreated(cwd, sessionId, meta) {
|
|
3881
3916
|
if (process.env.NODE_ENV === "test" || process.env.SISYPHUS_DISABLE_NOTIFY === "1") return;
|
|
3882
3917
|
const isActionable = meta.kind !== void 0 && ACTIONABLE_KINDS.has(meta.kind);
|
|
@@ -3986,6 +4021,7 @@ async function autoResolveAsk(cwd, sessionId, askId, deck) {
|
|
|
3986
4021
|
async function maybeAutoResolveAsk(cwd, sessionId, askId, deck) {
|
|
3987
4022
|
try {
|
|
3988
4023
|
if (!isSessionDangerous(cwd, sessionId)) return;
|
|
4024
|
+
if (deck.source?.askedBy === ORPHAN_ASKED_BY) return;
|
|
3989
4025
|
await autoResolveAsk(cwd, sessionId, askId, deck);
|
|
3990
4026
|
} catch {
|
|
3991
4027
|
}
|
|
@@ -4559,16 +4595,55 @@ function readFileSafe(filePath) {
|
|
|
4559
4595
|
function escapeXml(s) {
|
|
4560
4596
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4561
4597
|
}
|
|
4562
|
-
function
|
|
4598
|
+
function buildSessionBlock(cwd, session2) {
|
|
4599
|
+
const lines = [];
|
|
4600
|
+
const nameAttr = session2.name ? ` name="${escapeXml(session2.name)}"` : "";
|
|
4601
|
+
lines.push(` <session id="${escapeXml(session2.id)}"${nameAttr} status="${escapeXml(session2.status)}">`);
|
|
4602
|
+
lines.push(` <task>${escapeXml(session2.task)}</task>`);
|
|
4603
|
+
lines.push(` <created>${escapeXml(session2.createdAt)}</created>`);
|
|
4604
|
+
lines.push(` <cycles>${session2.orchestratorCycles.length}</cycles>`);
|
|
4605
|
+
if (session2.status === "completed") {
|
|
4606
|
+
if (session2.completionReport) {
|
|
4607
|
+
const snippet = session2.completionReport.slice(0, 300).replace(/\n+/g, " ").trim();
|
|
4608
|
+
lines.push(` <completion-report>${escapeXml(snippet)}${session2.completionReport.length > 300 ? "\u2026" : ""}</completion-report>`);
|
|
4609
|
+
}
|
|
4610
|
+
} else {
|
|
4611
|
+
if (session2.agents.length > 0) {
|
|
4612
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4613
|
+
for (const agent2 of session2.agents) {
|
|
4614
|
+
counts.set(agent2.status, (counts.get(agent2.status) ?? 0) + 1);
|
|
4615
|
+
}
|
|
4616
|
+
const summary = [...counts.entries()].map(([status, n]) => `${n} ${status}`).join(", ");
|
|
4617
|
+
lines.push(` <agents>${escapeXml(summary)}</agents>`);
|
|
4618
|
+
}
|
|
4619
|
+
const goalContent = readFileSafe(goalPath(cwd, session2.id));
|
|
4620
|
+
if (goalContent) {
|
|
4621
|
+
const firstLine = goalContent.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("#"));
|
|
4622
|
+
if (firstLine) lines.push(` <goal>${escapeXml(firstLine)}</goal>`);
|
|
4623
|
+
}
|
|
4624
|
+
const roadmapContent = readFileSafe(roadmapPath(cwd, session2.id));
|
|
4625
|
+
if (roadmapContent) {
|
|
4626
|
+
const todos = roadmapContent.split("\n").filter((l) => l.includes("- [ ]")).slice(0, 5).map((l) => l.trim());
|
|
4627
|
+
if (todos.length > 0) {
|
|
4628
|
+
lines.push(" <todos>");
|
|
4629
|
+
for (const todo of todos) lines.push(` ${escapeXml(todo)}`);
|
|
4630
|
+
lines.push(" </todos>");
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
lines.push(" </session>");
|
|
4635
|
+
return lines.join("\n");
|
|
4636
|
+
}
|
|
4637
|
+
function buildCompanionContextBlocks(cwd) {
|
|
4563
4638
|
let sessionDirs;
|
|
4564
4639
|
try {
|
|
4565
4640
|
sessionDirs = readdirSync4(sessionsDir(cwd));
|
|
4566
4641
|
} catch {
|
|
4567
|
-
return
|
|
4642
|
+
return {};
|
|
4568
4643
|
}
|
|
4569
4644
|
const now = Date.now();
|
|
4570
4645
|
const sevenDaysMs = 7 * 24 * 60 * 60 * 1e3;
|
|
4571
|
-
const
|
|
4646
|
+
const blocks = {};
|
|
4572
4647
|
for (const sessionId of sessionDirs) {
|
|
4573
4648
|
const stateRaw = readFileSafe(statePath(cwd, sessionId));
|
|
4574
4649
|
if (!stateRaw) continue;
|
|
@@ -4581,48 +4656,31 @@ function buildCompanionContext(cwd) {
|
|
|
4581
4656
|
if (session2.status === "completed" && session2.completedAt) {
|
|
4582
4657
|
if (now - new Date(session2.completedAt).getTime() > sevenDaysMs) continue;
|
|
4583
4658
|
}
|
|
4584
|
-
|
|
4585
|
-
const nameAttr = session2.name ? ` name="${escapeXml(session2.name)}"` : "";
|
|
4586
|
-
lines.push(` <session id="${escapeXml(session2.id)}"${nameAttr} status="${escapeXml(session2.status)}">`);
|
|
4587
|
-
lines.push(` <task>${escapeXml(session2.task)}</task>`);
|
|
4588
|
-
lines.push(` <created>${escapeXml(session2.createdAt)}</created>`);
|
|
4589
|
-
lines.push(` <cycles>${session2.orchestratorCycles.length}</cycles>`);
|
|
4590
|
-
if (session2.status === "completed") {
|
|
4591
|
-
if (session2.completionReport) {
|
|
4592
|
-
const snippet = session2.completionReport.slice(0, 300).replace(/\n+/g, " ").trim();
|
|
4593
|
-
lines.push(` <completion-report>${escapeXml(snippet)}${session2.completionReport.length > 300 ? "\u2026" : ""}</completion-report>`);
|
|
4594
|
-
}
|
|
4595
|
-
} else {
|
|
4596
|
-
if (session2.agents.length > 0) {
|
|
4597
|
-
const counts = /* @__PURE__ */ new Map();
|
|
4598
|
-
for (const agent2 of session2.agents) {
|
|
4599
|
-
counts.set(agent2.status, (counts.get(agent2.status) ?? 0) + 1);
|
|
4600
|
-
}
|
|
4601
|
-
const summary = [...counts.entries()].map(([status, n]) => `${n} ${status}`).join(", ");
|
|
4602
|
-
lines.push(` <agents>${escapeXml(summary)}</agents>`);
|
|
4603
|
-
}
|
|
4604
|
-
const goalContent = readFileSafe(goalPath(cwd, session2.id));
|
|
4605
|
-
if (goalContent) {
|
|
4606
|
-
const firstLine = goalContent.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("#"));
|
|
4607
|
-
if (firstLine) lines.push(` <goal>${escapeXml(firstLine)}</goal>`);
|
|
4608
|
-
}
|
|
4609
|
-
const roadmapContent = readFileSafe(roadmapPath(cwd, session2.id));
|
|
4610
|
-
if (roadmapContent) {
|
|
4611
|
-
const todos = roadmapContent.split("\n").filter((l) => l.includes("- [ ]")).slice(0, 5).map((l) => l.trim());
|
|
4612
|
-
if (todos.length > 0) {
|
|
4613
|
-
lines.push(" <todos>");
|
|
4614
|
-
for (const todo of todos) lines.push(` ${escapeXml(todo)}`);
|
|
4615
|
-
lines.push(" </todos>");
|
|
4616
|
-
}
|
|
4617
|
-
}
|
|
4618
|
-
}
|
|
4619
|
-
lines.push(" </session>");
|
|
4620
|
-
sessionBlocks.push(lines.join("\n"));
|
|
4659
|
+
blocks[session2.id] = buildSessionBlock(cwd, session2);
|
|
4621
4660
|
}
|
|
4622
|
-
|
|
4623
|
-
|
|
4661
|
+
return blocks;
|
|
4662
|
+
}
|
|
4663
|
+
function renderFullContext(blocks) {
|
|
4664
|
+
const entries = Object.values(blocks);
|
|
4665
|
+
if (entries.length === 0) return "<sessions>No sessions found.</sessions>";
|
|
4666
|
+
return ["<sessions>", ...entries, "</sessions>"].join("\n");
|
|
4667
|
+
}
|
|
4668
|
+
function renderContextDelta(prev, next) {
|
|
4669
|
+
const ids = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
4670
|
+
const entries = [];
|
|
4671
|
+
for (const id of ids) {
|
|
4672
|
+
const p = prev[id];
|
|
4673
|
+
const n = next[id];
|
|
4674
|
+
if (p === void 0 && n !== void 0) {
|
|
4675
|
+
entries.push(n.replace(/^(\s*)<session /, `$1<session change="added" `));
|
|
4676
|
+
} else if (p !== void 0 && n === void 0) {
|
|
4677
|
+
entries.push(` <session id="${escapeXml(id)}" change="removed" />`);
|
|
4678
|
+
} else if (p !== n && n !== void 0) {
|
|
4679
|
+
entries.push(n.replace(/^(\s*)<session /, `$1<session change="updated" `));
|
|
4680
|
+
}
|
|
4624
4681
|
}
|
|
4625
|
-
|
|
4682
|
+
if (entries.length === 0) return null;
|
|
4683
|
+
return ["<sessions-changed-since-last-prompt>", ...entries, "</sessions-changed-since-last-prompt>"].join("\n");
|
|
4626
4684
|
}
|
|
4627
4685
|
function buildSessionContext(session2, cwd) {
|
|
4628
4686
|
const goal = readFileSafe(goalPath(cwd, session2.id));
|
|
@@ -6194,6 +6252,30 @@ function setSessionCwd(name, cwd) {
|
|
|
6194
6252
|
);
|
|
6195
6253
|
}
|
|
6196
6254
|
|
|
6255
|
+
// src/cli/commands/quiesce.ts
|
|
6256
|
+
function registerQuiesce(parent) {
|
|
6257
|
+
parent.command("quiesce <session-id>").description("Pause a session at the next quiesce point (or now with --force). No cloud push.").option("--force", "Interrupt running orchestrator/agents immediately.").action(async (sessionId, opts) => {
|
|
6258
|
+
const cwd = process.env["SISYPHUS_CWD"] ?? process.cwd();
|
|
6259
|
+
const request = {
|
|
6260
|
+
type: "admin-quiesce",
|
|
6261
|
+
sessionId,
|
|
6262
|
+
cwd,
|
|
6263
|
+
force: opts.force === true
|
|
6264
|
+
};
|
|
6265
|
+
const response = await sendRequest(request);
|
|
6266
|
+
if (!response.ok) {
|
|
6267
|
+
console.error(`Error: ${response.error}`);
|
|
6268
|
+
process.exit(1);
|
|
6269
|
+
}
|
|
6270
|
+
const data = response.data;
|
|
6271
|
+
if (data?.force) {
|
|
6272
|
+
console.log(`Session ${sessionId} quiescing now (--force).`);
|
|
6273
|
+
} else {
|
|
6274
|
+
console.log(`Session ${sessionId} will pause at next quiesce point.`);
|
|
6275
|
+
}
|
|
6276
|
+
});
|
|
6277
|
+
}
|
|
6278
|
+
|
|
6197
6279
|
// src/cli/commands/doctor.ts
|
|
6198
6280
|
init_paths();
|
|
6199
6281
|
import { execSync as execSync14 } from "child_process";
|
|
@@ -8996,7 +9078,9 @@ function renderRequirementsMarkdown(json) {
|
|
|
8996
9078
|
}
|
|
8997
9079
|
|
|
8998
9080
|
// src/cli/commands/companion.ts
|
|
8999
|
-
import { basename as basename5 } from "path";
|
|
9081
|
+
import { basename as basename5, dirname as dirname11, join as join28 } from "path";
|
|
9082
|
+
import { mkdirSync as mkdirSync14, readFileSync as readFileSync30, writeFileSync as writeFileSync17 } from "fs";
|
|
9083
|
+
init_paths();
|
|
9000
9084
|
|
|
9001
9085
|
// src/shared/companion-render.ts
|
|
9002
9086
|
import stringWidth from "string-width";
|
|
@@ -10678,9 +10762,25 @@ function registerCompanion(program2) {
|
|
|
10678
10762
|
companion.command("memory").description("Show accumulated companion observations grouped by category").option("--repo <path>", "Filter observations by repo path").action(async (opts) => {
|
|
10679
10763
|
await runCompanionMemory(opts);
|
|
10680
10764
|
});
|
|
10681
|
-
companion.command("context").description("
|
|
10682
|
-
const
|
|
10683
|
-
|
|
10765
|
+
companion.command("context").description("Emit per-prompt context for the companion plugin hook. Caches the last emission per claude session and writes only the delta on subsequent calls (or nothing, when unchanged).").requiredOption("--cwd <path>", "Project directory whose sessions to summarise").requiredOption("--session-id <id>", "Claude session id (from the UserPromptSubmit stdin payload) \u2014 keys the per-session cache").action((opts) => {
|
|
10766
|
+
const cachePath = join28(globalDir(), "companion-context-cache", `${opts.sessionId}.json`);
|
|
10767
|
+
let prev = {};
|
|
10768
|
+
try {
|
|
10769
|
+
prev = JSON.parse(readFileSync30(cachePath, "utf-8"));
|
|
10770
|
+
} catch {
|
|
10771
|
+
prev = {};
|
|
10772
|
+
}
|
|
10773
|
+
const next = buildCompanionContextBlocks(opts.cwd);
|
|
10774
|
+
const hadPrev = Object.keys(prev).length > 0;
|
|
10775
|
+
if (hadPrev) {
|
|
10776
|
+
const delta = renderContextDelta(prev, next);
|
|
10777
|
+
if (delta === null) return;
|
|
10778
|
+
process.stdout.write(delta);
|
|
10779
|
+
} else {
|
|
10780
|
+
process.stdout.write(renderFullContext(next));
|
|
10781
|
+
}
|
|
10782
|
+
mkdirSync14(dirname11(cachePath), { recursive: true });
|
|
10783
|
+
writeFileSync17(cachePath, JSON.stringify(next), "utf-8");
|
|
10684
10784
|
});
|
|
10685
10785
|
companion.command("pane").description("Open (or focus) a side claude pane next to the dashboard").option("--cwd <path>", "Project directory", process.cwd()).action(async (opts) => {
|
|
10686
10786
|
const { openCompanionPane: openCompanionPane2 } = await Promise.resolve().then(() => (init_tmux(), tmux_exports));
|
|
@@ -10703,14 +10803,14 @@ function registerCompanion(program2) {
|
|
|
10703
10803
|
|
|
10704
10804
|
// src/cli/commands/deploy.ts
|
|
10705
10805
|
import { homedir as homedir13 } from "os";
|
|
10706
|
-
import { join as
|
|
10806
|
+
import { join as join29 } from "path";
|
|
10707
10807
|
|
|
10708
10808
|
// src/cli/deploy/runner.ts
|
|
10709
10809
|
init_paths();
|
|
10710
10810
|
init_exec();
|
|
10711
10811
|
init_creds();
|
|
10712
10812
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
|
|
10713
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as
|
|
10813
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as mkdirSync16, readFileSync as readFileSync33 } from "fs";
|
|
10714
10814
|
|
|
10715
10815
|
// src/cli/deploy/pricing.ts
|
|
10716
10816
|
var LAST_VERIFIED = "2026-05-06";
|
|
@@ -10745,12 +10845,12 @@ function formatCostLine(provider, instanceType) {
|
|
|
10745
10845
|
// src/cli/deploy/runtime.ts
|
|
10746
10846
|
init_atomic();
|
|
10747
10847
|
init_paths();
|
|
10748
|
-
import { existsSync as existsSync28, readFileSync as
|
|
10848
|
+
import { existsSync as existsSync28, readFileSync as readFileSync32, unlinkSync as unlinkSync4 } from "fs";
|
|
10749
10849
|
function readRuntimeState(provider) {
|
|
10750
10850
|
const path = deployRuntimePath(provider);
|
|
10751
10851
|
if (!existsSync28(path)) return null;
|
|
10752
10852
|
try {
|
|
10753
|
-
return JSON.parse(
|
|
10853
|
+
return JSON.parse(readFileSync32(path, "utf-8"));
|
|
10754
10854
|
} catch {
|
|
10755
10855
|
return null;
|
|
10756
10856
|
}
|
|
@@ -10811,10 +10911,10 @@ function isTailscaleAvailable() {
|
|
|
10811
10911
|
|
|
10812
10912
|
// src/cli/deploy/templates.ts
|
|
10813
10913
|
import { existsSync as existsSync29 } from "fs";
|
|
10814
|
-
import { dirname as
|
|
10914
|
+
import { dirname as dirname12, resolve as resolve10 } from "path";
|
|
10815
10915
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10816
10916
|
function deployRoot() {
|
|
10817
|
-
const here =
|
|
10917
|
+
const here = dirname12(fileURLToPath4(import.meta.url));
|
|
10818
10918
|
const bundled = resolve10(here, "..", "deploy");
|
|
10819
10919
|
if (existsSync29(bundled)) return bundled;
|
|
10820
10920
|
const sourceRoot = resolve10(here, "..", "..", "..", "deploy");
|
|
@@ -11002,7 +11102,7 @@ function ensureTerraformInstalled() {
|
|
|
11002
11102
|
function ensureProviderStateDir(provider) {
|
|
11003
11103
|
ensureDeployDir();
|
|
11004
11104
|
const dir = deployProviderDir(provider);
|
|
11005
|
-
if (!existsSync30(dir))
|
|
11105
|
+
if (!existsSync30(dir)) mkdirSync16(dir, { recursive: true, mode: 448 });
|
|
11006
11106
|
}
|
|
11007
11107
|
function backupState(provider) {
|
|
11008
11108
|
const src = deployStatePath(provider);
|
|
@@ -11017,7 +11117,7 @@ function readSshPubkey(path) {
|
|
|
11017
11117
|
or pass --ssh-key <path>.`
|
|
11018
11118
|
);
|
|
11019
11119
|
}
|
|
11020
|
-
return
|
|
11120
|
+
return readFileSync33(path, "utf-8").trim();
|
|
11021
11121
|
}
|
|
11022
11122
|
function readOutputs(provider) {
|
|
11023
11123
|
const result = spawnSync3("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
|
|
@@ -11295,7 +11395,7 @@ function registerDeploy(program2) {
|
|
|
11295
11395
|
});
|
|
11296
11396
|
for (const provider of PROVIDERS) {
|
|
11297
11397
|
const sub = deploy.command(provider).description(`${provider} commands.`);
|
|
11298
|
-
sub.command("up").description(`Provision the ${provider} box (terraform init \u2192 plan \u2192 apply).`).option("--region <region>", `Provider region (defaults: hetzner=nbg1, aws=us-east-1).`).option("--arch <arch>", "'arm' (default) or 'x86'. Picks the default --size and image.", "arm").option("--size <size>", "Instance type override (defaults follow --arch).").option("--ssh-key <path>", "Path to SSH public key.",
|
|
11398
|
+
sub.command("up").description(`Provision the ${provider} box (terraform init \u2192 plan \u2192 apply).`).option("--region <region>", `Provider region (defaults: hetzner=nbg1, aws=us-east-1).`).option("--arch <arch>", "'arm' (default) or 'x86'. Picks the default --size and image.", "arm").option("--size <size>", "Instance type override (defaults follow --arch).").option("--ssh-key <path>", "Path to SSH public key.", join29(homedir13(), ".ssh", "id_ed25519.pub")).option("--no-chromium", "Skip headless Chromium install.").option("--no-auto-update", "Skip the daily auto-update systemd timer.").option("--name <name>", "Box hostname / Tailscale node name.", "sisyphus").option("-y, --yes", "Skip the re-provision confirmation prompt when state already exists.").action(async (raw) => {
|
|
11299
11399
|
const opts = resolveUpOptions(provider, raw);
|
|
11300
11400
|
await deployUp(provider, opts);
|
|
11301
11401
|
});
|
|
@@ -11380,31 +11480,32 @@ function ensureGroveRegistered(provider, repo, instancePath) {
|
|
|
11380
11480
|
init_exec();
|
|
11381
11481
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
11382
11482
|
import { existsSync as existsSync31 } from "fs";
|
|
11383
|
-
import { basename as basename6, join as
|
|
11384
|
-
function captureGit(args2) {
|
|
11483
|
+
import { basename as basename6, join as join30 } from "path";
|
|
11484
|
+
function captureGit(args2, cwd) {
|
|
11385
11485
|
const result = spawnSync5("git", args2, {
|
|
11386
11486
|
encoding: "utf-8",
|
|
11387
|
-
env: EXEC_ENV
|
|
11487
|
+
env: EXEC_ENV,
|
|
11488
|
+
cwd: cwd ?? process.cwd()
|
|
11388
11489
|
});
|
|
11389
11490
|
if (typeof result.stdout !== "string") {
|
|
11390
11491
|
throw new Error("Internal: git spawn did not capture stdout as string");
|
|
11391
11492
|
}
|
|
11392
11493
|
return { stdout: result.stdout.trim(), ok: result.status === 0 };
|
|
11393
11494
|
}
|
|
11394
|
-
function inferRepoName() {
|
|
11395
|
-
const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"]);
|
|
11495
|
+
function inferRepoName(cwd) {
|
|
11496
|
+
const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"], cwd);
|
|
11396
11497
|
if (ok && stdout) return basename6(stdout);
|
|
11397
|
-
return basename6(process.cwd());
|
|
11498
|
+
return basename6(cwd ?? process.cwd());
|
|
11398
11499
|
}
|
|
11399
|
-
function getOriginUrl() {
|
|
11400
|
-
const { stdout, ok } = captureGit(["remote", "get-url", "origin"]);
|
|
11500
|
+
function getOriginUrl(cwd) {
|
|
11501
|
+
const { stdout, ok } = captureGit(["remote", "get-url", "origin"], cwd);
|
|
11401
11502
|
if (!ok) return null;
|
|
11402
11503
|
return stdout.length > 0 ? stdout : null;
|
|
11403
11504
|
}
|
|
11404
|
-
function getRepoToplevel() {
|
|
11405
|
-
const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"]);
|
|
11505
|
+
function getRepoToplevel(cwd) {
|
|
11506
|
+
const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"], cwd);
|
|
11406
11507
|
if (ok && stdout) return stdout;
|
|
11407
|
-
return process.cwd();
|
|
11508
|
+
return cwd ?? process.cwd();
|
|
11408
11509
|
}
|
|
11409
11510
|
var DEFAULT_EXCLUDES = [
|
|
11410
11511
|
".sisyphus/",
|
|
@@ -11431,10 +11532,10 @@ function buildRsyncArgs(localDir, remoteTarget) {
|
|
|
11431
11532
|
];
|
|
11432
11533
|
}
|
|
11433
11534
|
function detectPackageManager(toplevel) {
|
|
11434
|
-
if (existsSync31(
|
|
11435
|
-
if (existsSync31(
|
|
11436
|
-
if (existsSync31(
|
|
11437
|
-
if (existsSync31(
|
|
11535
|
+
if (existsSync31(join30(toplevel, "pnpm-lock.yaml"))) return "pnpm";
|
|
11536
|
+
if (existsSync31(join30(toplevel, "bun.lockb"))) return "bun";
|
|
11537
|
+
if (existsSync31(join30(toplevel, "yarn.lock"))) return "yarn";
|
|
11538
|
+
if (existsSync31(join30(toplevel, "package-lock.json"))) return "npm";
|
|
11438
11539
|
return null;
|
|
11439
11540
|
}
|
|
11440
11541
|
function packageManagerInstallCmd(pm) {
|
|
@@ -11483,10 +11584,10 @@ function writeSidecar(provider, repo, data) {
|
|
|
11483
11584
|
}
|
|
11484
11585
|
|
|
11485
11586
|
// src/cli/cloud/runner.ts
|
|
11486
|
-
async function cloudSync(provider, repo, opts) {
|
|
11587
|
+
async function cloudSync(provider, repo, opts, cwd) {
|
|
11487
11588
|
const target = effectiveSshTarget(provider);
|
|
11488
11589
|
const remoteDir = boxRepoPath(repo);
|
|
11489
|
-
const localOrigin = getOriginUrl();
|
|
11590
|
+
const localOrigin = getOriginUrl(cwd);
|
|
11490
11591
|
ensureGroveInstalled(provider);
|
|
11491
11592
|
const existing = readSidecar(provider, repo);
|
|
11492
11593
|
if (existing && existing.originUrl && localOrigin && existing.originUrl !== localOrigin) {
|
|
@@ -11524,7 +11625,7 @@ Pass --name <slug> to disambiguate, or --fresh to overwrite.`
|
|
|
11524
11625
|
if (mkdir.exitCode !== 0) {
|
|
11525
11626
|
throw new Error(`Failed to mkdir on box: ${mkdir.stderr}`);
|
|
11526
11627
|
}
|
|
11527
|
-
const toplevel = getRepoToplevel();
|
|
11628
|
+
const toplevel = getRepoToplevel(cwd);
|
|
11528
11629
|
const args2 = buildRsyncArgs(toplevel, `${target}:${remoteDir}/`);
|
|
11529
11630
|
console.log(`\u2192 rsync ${toplevel}/ \u2192 ${target}:${remoteDir}/`);
|
|
11530
11631
|
const code = await runRsync(args2);
|
|
@@ -11548,9 +11649,9 @@ function runRsync(args2) {
|
|
|
11548
11649
|
child.on("exit", (code) => resolve12(code === null ? 1 : code));
|
|
11549
11650
|
});
|
|
11550
11651
|
}
|
|
11551
|
-
async function cloudInstall(provider, repo) {
|
|
11652
|
+
async function cloudInstall(provider, repo, cwd) {
|
|
11552
11653
|
const remoteDir = boxRepoPath(repo);
|
|
11553
|
-
const toplevel = getRepoToplevel();
|
|
11654
|
+
const toplevel = getRepoToplevel(cwd);
|
|
11554
11655
|
const pm = detectPackageManager(toplevel);
|
|
11555
11656
|
const cmd = packageManagerInstallCmd(pm);
|
|
11556
11657
|
if (!cmd) {
|
|
@@ -11563,7 +11664,7 @@ async function cloudInstall(provider, repo) {
|
|
|
11563
11664
|
if (code !== 0) throw new Error(`${pm} install failed (exit ${code})`);
|
|
11564
11665
|
const existing = readSidecar(provider, repo);
|
|
11565
11666
|
const sidecar = {
|
|
11566
|
-
originUrl: existing && existing.originUrl !== void 0 ? existing.originUrl : getOriginUrl(),
|
|
11667
|
+
originUrl: existing && existing.originUrl !== void 0 ? existing.originUrl : getOriginUrl(cwd),
|
|
11567
11668
|
localHostname: existing ? existing.localHostname : hostname(),
|
|
11568
11669
|
lastSync: existing?.lastSync,
|
|
11569
11670
|
lastInstall: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -11641,6 +11742,13 @@ function cloudStatus(provider, repo) {
|
|
|
11641
11742
|
}
|
|
11642
11743
|
}
|
|
11643
11744
|
|
|
11745
|
+
// src/cli/cloud/handoff.ts
|
|
11746
|
+
import { spawn as spawn5 } from "child_process";
|
|
11747
|
+
import { existsSync as existsSync32, readFileSync as readFileSync34 } from "fs";
|
|
11748
|
+
init_exec();
|
|
11749
|
+
init_paths();
|
|
11750
|
+
init_shell();
|
|
11751
|
+
|
|
11644
11752
|
// src/cli/deploy/provider-pick.ts
|
|
11645
11753
|
init_creds();
|
|
11646
11754
|
function pickProvider(explicit) {
|
|
@@ -11662,6 +11770,188 @@ function pickProvider(explicit) {
|
|
|
11662
11770
|
);
|
|
11663
11771
|
}
|
|
11664
11772
|
|
|
11773
|
+
// src/cli/cloud/handoff.ts
|
|
11774
|
+
import { join as join31 } from "path";
|
|
11775
|
+
async function cloudHandoff(sessionId, opts) {
|
|
11776
|
+
const cwd = process.env["SISYPHUS_CWD"] ?? process.cwd();
|
|
11777
|
+
const request = {
|
|
11778
|
+
type: "cloud-handoff",
|
|
11779
|
+
sessionId,
|
|
11780
|
+
cwd,
|
|
11781
|
+
provider: opts.provider,
|
|
11782
|
+
repo: opts.repo,
|
|
11783
|
+
force: opts.force
|
|
11784
|
+
};
|
|
11785
|
+
const response = await sendRequest(request);
|
|
11786
|
+
if (!response.ok) {
|
|
11787
|
+
console.error(`Error: ${response.error}`);
|
|
11788
|
+
process.exit(1);
|
|
11789
|
+
}
|
|
11790
|
+
const data = response.data;
|
|
11791
|
+
const where = `${data?.provider ?? opts.provider}:${data?.repo ?? opts.repo}`;
|
|
11792
|
+
if (data?.force) {
|
|
11793
|
+
console.log(`Forcing handoff of ${sessionId} \u2192 ${where} (interrupting in-flight work).`);
|
|
11794
|
+
} else {
|
|
11795
|
+
console.log(`Handoff of ${sessionId} \u2192 ${where} queued; will fire at next quiesce point.`);
|
|
11796
|
+
}
|
|
11797
|
+
if (!opts.wait) {
|
|
11798
|
+
if (!opts.force) {
|
|
11799
|
+
console.log(`Tip: run \`sis cloud handoff ${sessionId} --cancel\` to cancel before quiesce.`);
|
|
11800
|
+
}
|
|
11801
|
+
return;
|
|
11802
|
+
}
|
|
11803
|
+
console.log("Waiting for handoff to complete...");
|
|
11804
|
+
await waitForSentOrError(cwd, sessionId);
|
|
11805
|
+
}
|
|
11806
|
+
async function cloudHandoffCancel(sessionId) {
|
|
11807
|
+
const cwd = process.env["SISYPHUS_CWD"] ?? process.cwd();
|
|
11808
|
+
const response = await sendRequest({ type: "cloud-handoff-cancel", sessionId, cwd });
|
|
11809
|
+
if (!response.ok) {
|
|
11810
|
+
console.error(`Error: ${response.error}`);
|
|
11811
|
+
process.exit(1);
|
|
11812
|
+
}
|
|
11813
|
+
console.log(`Handoff for ${sessionId} cancelled.`);
|
|
11814
|
+
}
|
|
11815
|
+
async function waitForSentOrError(cwd, sessionId) {
|
|
11816
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
11817
|
+
const MAX_WAIT_MS = 30 * 60 * 1e3;
|
|
11818
|
+
const start = Date.now();
|
|
11819
|
+
const path = statePath(cwd, sessionId);
|
|
11820
|
+
while (Date.now() - start < MAX_WAIT_MS) {
|
|
11821
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
11822
|
+
if (!existsSync32(path)) continue;
|
|
11823
|
+
let session2;
|
|
11824
|
+
try {
|
|
11825
|
+
session2 = JSON.parse(readFileSync34(path, "utf-8"));
|
|
11826
|
+
} catch (err) {
|
|
11827
|
+
void err;
|
|
11828
|
+
continue;
|
|
11829
|
+
}
|
|
11830
|
+
if (session2.handoff?.lastError) {
|
|
11831
|
+
console.error(`Handoff failed: ${session2.handoff.lastError}`);
|
|
11832
|
+
process.exit(1);
|
|
11833
|
+
}
|
|
11834
|
+
if (session2.handoff?.sentAt) {
|
|
11835
|
+
const where = session2.handoff.target ? `${session2.handoff.target.provider}:${session2.handoff.target.repo}` : "cloud";
|
|
11836
|
+
console.log(`Handoff complete \u2192 ${where} at ${session2.handoff.sentAt}.`);
|
|
11837
|
+
return;
|
|
11838
|
+
}
|
|
11839
|
+
}
|
|
11840
|
+
console.error(`Timed out waiting for handoff after ${Math.round(MAX_WAIT_MS / 6e4)}m.`);
|
|
11841
|
+
process.exit(1);
|
|
11842
|
+
}
|
|
11843
|
+
function sleep2(ms) {
|
|
11844
|
+
return new Promise((resolve12) => setTimeout(resolve12, ms));
|
|
11845
|
+
}
|
|
11846
|
+
async function cloudReclaim(sessionId, opts) {
|
|
11847
|
+
const cwd = process.env["SISYPHUS_CWD"] ?? process.cwd();
|
|
11848
|
+
const local = readLocalSession(cwd, sessionId);
|
|
11849
|
+
if (!local.handoff?.sentAt) {
|
|
11850
|
+
console.error(`Session ${sessionId} is not on cloud (no handoff.sentAt). Nothing to reclaim.`);
|
|
11851
|
+
process.exit(1);
|
|
11852
|
+
}
|
|
11853
|
+
if (local.handoff.reclaimedAt) {
|
|
11854
|
+
console.error(`Session ${sessionId} was already reclaimed at ${local.handoff.reclaimedAt}.`);
|
|
11855
|
+
process.exit(1);
|
|
11856
|
+
}
|
|
11857
|
+
if (!local.handoff.target) {
|
|
11858
|
+
console.error(`Session ${sessionId} has handoff.sentAt but no target \u2014 corrupted state.`);
|
|
11859
|
+
process.exit(1);
|
|
11860
|
+
}
|
|
11861
|
+
const provider = pickProvider(opts.providerOverride ?? local.handoff.target.provider);
|
|
11862
|
+
const repo = local.handoff.target.repo;
|
|
11863
|
+
const target = effectiveSshTarget(provider);
|
|
11864
|
+
const remoteSessionDir = `${boxRepoPath(repo)}/.sisyphus/sessions/${sessionId}`;
|
|
11865
|
+
const remoteRepoDir = boxRepoPath(repo);
|
|
11866
|
+
console.log(`Reclaiming ${sessionId} from ${provider}:${repo}...`);
|
|
11867
|
+
const quiesceBase = opts.force ? `sis admin quiesce ${shellQuote(sessionId)} --force` : `sis admin quiesce ${shellQuote(sessionId)}`;
|
|
11868
|
+
const quiesceCmd = `cd ${shellQuoteHomePath(remoteRepoDir)} && ${quiesceBase}`;
|
|
11869
|
+
console.log(`\u2192 ssh box: ${quiesceCmd}`);
|
|
11870
|
+
const quiesceCode = await runOnBoxStreaming(provider, quiesceCmd);
|
|
11871
|
+
if (quiesceCode !== 0) {
|
|
11872
|
+
console.error(`Box-side quiesce failed (exit ${quiesceCode}).`);
|
|
11873
|
+
process.exit(1);
|
|
11874
|
+
}
|
|
11875
|
+
console.log(`\u2192 waiting for box-side session to reach paused...`);
|
|
11876
|
+
await waitForBoxPaused(provider, remoteSessionDir);
|
|
11877
|
+
console.log(`\u2192 rsync session state down`);
|
|
11878
|
+
await rsyncDown(target, `${remoteSessionDir}/`, `${sessionDir(cwd, sessionId)}/`, { withDelete: true });
|
|
11879
|
+
console.log(`\u2192 rsync working tree down`);
|
|
11880
|
+
await rsyncDown(target, `${remoteRepoDir}/`, `${cwd}/`, { withDelete: false });
|
|
11881
|
+
for (const name of ["config.json", "orchestrator.md", "orchestrator-settings.json"]) {
|
|
11882
|
+
const remotePath = `${remoteRepoDir}/.sisyphus/${name}`;
|
|
11883
|
+
const localPath = join31(projectDir(cwd), name);
|
|
11884
|
+
const probe = runOnBox(provider, `test -f ${shellQuote(remotePath.replace(/^~\//, ""))} && echo y || echo n`);
|
|
11885
|
+
if (probe.stdout.trim() !== "y") continue;
|
|
11886
|
+
await rsyncDown(target, remotePath, localPath, { withDelete: false });
|
|
11887
|
+
}
|
|
11888
|
+
const reclaimMessage = `Session reclaimed from cloud (${provider}:${repo}). Resuming locally.`;
|
|
11889
|
+
console.log(`\u2192 local sis resume`);
|
|
11890
|
+
const resumeResp = await sendRequest({
|
|
11891
|
+
type: "resume",
|
|
11892
|
+
sessionId,
|
|
11893
|
+
cwd,
|
|
11894
|
+
message: reclaimMessage
|
|
11895
|
+
});
|
|
11896
|
+
if (!resumeResp.ok) {
|
|
11897
|
+
console.error(`Local resume failed: ${resumeResp.error}`);
|
|
11898
|
+
process.exit(1);
|
|
11899
|
+
}
|
|
11900
|
+
const killCmd = `cd ${shellQuoteHomePath(remoteRepoDir)} && sis session kill ${shellQuote(sessionId)}`;
|
|
11901
|
+
console.log(`\u2192 ssh box: ${killCmd}`);
|
|
11902
|
+
const killResult = runOnBox(provider, killCmd);
|
|
11903
|
+
if (killResult.exitCode !== 0) {
|
|
11904
|
+
console.warn(`Warning: box-side kill exited ${killResult.exitCode}: ${killResult.stderr.trim()}`);
|
|
11905
|
+
}
|
|
11906
|
+
const finalizeResp = await sendRequest({ type: "cloud-reclaim-finalize", sessionId, cwd });
|
|
11907
|
+
if (!finalizeResp.ok) {
|
|
11908
|
+
console.warn(`Warning: failed to finalize reclaim state: ${finalizeResp.error}`);
|
|
11909
|
+
}
|
|
11910
|
+
console.log(`\u2713 ${sessionId} reclaimed; orchestrator respawning locally.`);
|
|
11911
|
+
}
|
|
11912
|
+
function readLocalSession(cwd, sessionId) {
|
|
11913
|
+
const path = statePath(cwd, sessionId);
|
|
11914
|
+
if (!existsSync32(path)) {
|
|
11915
|
+
console.error(`No local state.json for ${sessionId} at ${path}.`);
|
|
11916
|
+
process.exit(1);
|
|
11917
|
+
}
|
|
11918
|
+
return JSON.parse(readFileSync34(path, "utf-8"));
|
|
11919
|
+
}
|
|
11920
|
+
async function waitForBoxPaused(provider, remoteSessionDir) {
|
|
11921
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
11922
|
+
const MAX_WAIT_MS = 30 * 60 * 1e3;
|
|
11923
|
+
const start = Date.now();
|
|
11924
|
+
const cmd = `cat ${shellQuote(remoteSessionDir.replace(/^~\//, ""))}/state.json 2>/dev/null | head -c 200000`;
|
|
11925
|
+
while (Date.now() - start < MAX_WAIT_MS) {
|
|
11926
|
+
const r = runOnBox(provider, cmd);
|
|
11927
|
+
if (r.exitCode === 0 && r.stdout.trim()) {
|
|
11928
|
+
try {
|
|
11929
|
+
const s = JSON.parse(r.stdout);
|
|
11930
|
+
if (s.status === "paused") return;
|
|
11931
|
+
} catch (err) {
|
|
11932
|
+
console.warn(`Could not parse box-side state.json yet: ${err instanceof Error ? err.message : String(err)}`);
|
|
11933
|
+
}
|
|
11934
|
+
}
|
|
11935
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
11936
|
+
}
|
|
11937
|
+
console.error(`Timed out waiting for box-side session to pause.`);
|
|
11938
|
+
process.exit(1);
|
|
11939
|
+
}
|
|
11940
|
+
function rsyncDown(target, remotePath, localPath, opts) {
|
|
11941
|
+
return new Promise((resolve12, reject) => {
|
|
11942
|
+
const args2 = ["-avz"];
|
|
11943
|
+
if (opts.withDelete) args2.push("--delete");
|
|
11944
|
+
args2.push("--exclude=.terraform/", "--exclude=node_modules/", "--exclude=dist/", "--exclude=.next/", "--exclude=.turbo/", "--exclude=coverage/", "--exclude=tmp/", "--exclude=.git/lfs/", "--exclude=.DS_Store");
|
|
11945
|
+
args2.push("-e", "ssh", `${target}:${remotePath}`, localPath);
|
|
11946
|
+
const child = spawn5("rsync", args2, { stdio: "inherit", env: EXEC_ENV });
|
|
11947
|
+
child.on("error", reject);
|
|
11948
|
+
child.on("exit", (code) => {
|
|
11949
|
+
if (code === 0) resolve12();
|
|
11950
|
+
else reject(new Error(`rsync exited ${code} for ${remotePath}`));
|
|
11951
|
+
});
|
|
11952
|
+
});
|
|
11953
|
+
}
|
|
11954
|
+
|
|
11665
11955
|
// src/cli/commands/cloud.ts
|
|
11666
11956
|
init_shell();
|
|
11667
11957
|
function resolve11(raw) {
|
|
@@ -11702,6 +11992,21 @@ function registerCloud(program2) {
|
|
|
11702
11992
|
const { provider, repo } = resolve11(raw);
|
|
11703
11993
|
cloudStatus(provider, repo);
|
|
11704
11994
|
});
|
|
11995
|
+
cloud.command("handoff <session-id>").description("Hand off a live session to the cloud box. Queues at next quiesce; --force interrupts now.").option("--provider <name>", "Cloud provider (default: auto-pick).").option("--name <repo>", "Override the repo name on the box.").option("--force", "Interrupt in-flight orchestrator/agents now instead of queueing.").option("--cancel", "Cancel a queued handoff before it fires.").option("--wait", "Block until the handoff completes (or fails).").action(async (sessionId, raw) => {
|
|
11996
|
+
if (raw.cancel) {
|
|
11997
|
+
await cloudHandoffCancel(sessionId);
|
|
11998
|
+
return;
|
|
11999
|
+
}
|
|
12000
|
+
const provider = pickProvider(raw.provider);
|
|
12001
|
+
const repo = raw.name ? raw.name : inferRepoName();
|
|
12002
|
+
if (!validateRepoName(repo)) {
|
|
12003
|
+
throw new Error(`Invalid --name "${repo}": must not contain '/' '\\' or '..'.`);
|
|
12004
|
+
}
|
|
12005
|
+
await cloudHandoff(sessionId, { provider, repo, force: raw.force === true, wait: raw.wait === true });
|
|
12006
|
+
});
|
|
12007
|
+
cloud.command("reclaim <session-id>").description("Pull a handed-off session back from the cloud box and resume locally.").option("--provider <name>", "Override the cloud provider (default: read from session.handoff).").option("--force", "Force the box-side session to stop now instead of waiting for quiesce.").action(async (sessionId, raw) => {
|
|
12008
|
+
await cloudReclaim(sessionId, { providerOverride: raw.provider, force: raw.force === true });
|
|
12009
|
+
});
|
|
11705
12010
|
}
|
|
11706
12011
|
|
|
11707
12012
|
// src/cli/commands/notify.ts
|
|
@@ -11719,7 +12024,7 @@ function attachNotify(diagnostic2) {
|
|
|
11719
12024
|
// src/cli/commands/tmux-sessions.ts
|
|
11720
12025
|
init_paths();
|
|
11721
12026
|
import { execSync as execSync18 } from "child_process";
|
|
11722
|
-
import { readFileSync as
|
|
12027
|
+
import { readFileSync as readFileSync35, existsSync as existsSync33 } from "fs";
|
|
11723
12028
|
var DOT_MAP = {
|
|
11724
12029
|
"orchestrator:processing": { icon: "\u25CF", color: "#d4ad6a" },
|
|
11725
12030
|
"orchestrator:idle": { icon: "\u25CF", color: "#d47766" },
|
|
@@ -11730,9 +12035,9 @@ var DOT_MAP = {
|
|
|
11730
12035
|
};
|
|
11731
12036
|
function readManifest() {
|
|
11732
12037
|
const p = sessionsManifestPath();
|
|
11733
|
-
if (!
|
|
12038
|
+
if (!existsSync33(p)) return null;
|
|
11734
12039
|
try {
|
|
11735
|
-
return JSON.parse(
|
|
12040
|
+
return JSON.parse(readFileSync35(p, "utf-8"));
|
|
11736
12041
|
} catch {
|
|
11737
12042
|
return null;
|
|
11738
12043
|
}
|
|
@@ -11778,7 +12083,7 @@ if (nodeVersion < 22) {
|
|
|
11778
12083
|
var program = new Command();
|
|
11779
12084
|
program.name("sis").description("tmux-integrated orchestration daemon for Claude Code").version(
|
|
11780
12085
|
JSON.parse(
|
|
11781
|
-
|
|
12086
|
+
readFileSync36(join32(dirname13(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf-8")
|
|
11782
12087
|
).version
|
|
11783
12088
|
);
|
|
11784
12089
|
program.configureHelp({
|
|
@@ -11823,6 +12128,7 @@ registerSetupKeybind(admin);
|
|
|
11823
12128
|
registerCheckKeybinds(admin);
|
|
11824
12129
|
registerCheckStatusbar(admin);
|
|
11825
12130
|
registerHomeInit(admin);
|
|
12131
|
+
registerQuiesce(admin);
|
|
11826
12132
|
registerDoctor(admin);
|
|
11827
12133
|
registerInit(admin);
|
|
11828
12134
|
registerUninstall(admin);
|
|
@@ -11852,8 +12158,8 @@ Run 'sis admin getting-started' for a complete usage guide.
|
|
|
11852
12158
|
var args = process.argv.slice(2);
|
|
11853
12159
|
var firstArg = args[0];
|
|
11854
12160
|
var skipWelcome = ["admin", "help", "--help", "-h", "--version", "-V"];
|
|
11855
|
-
if (!
|
|
11856
|
-
|
|
12161
|
+
if (!existsSync34(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
|
|
12162
|
+
mkdirSync17(globalDir(), { recursive: true });
|
|
11857
12163
|
console.log("");
|
|
11858
12164
|
console.log(" Welcome to Sisyphus. Run 'sis admin setup' to get started.");
|
|
11859
12165
|
console.log("");
|