sisyphi 1.1.24 → 1.1.25
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 +306 -56
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +4 -0
- package/dist/daemon.js.map +1 -1
- package/dist/templates/agent-plugin/skills/humanloop/SKILL.md +2 -1
- package/dist/templates/orchestrator-plugin/skills/humanloop/SKILL.md +2 -1
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/templates/agent-plugin/skills/humanloop/SKILL.md +2 -1
- package/templates/orchestrator-plugin/skills/humanloop/SKILL.md +2 -1
package/dist/cli.js
CHANGED
|
@@ -111,6 +111,9 @@ function deployStatePath(provider) {
|
|
|
111
111
|
function deployStateBackupPath(provider) {
|
|
112
112
|
return join(deployProviderDir(provider), "terraform.tfstate.bak");
|
|
113
113
|
}
|
|
114
|
+
function deployRuntimePath(provider) {
|
|
115
|
+
return join(deployProviderDir(provider), "runtime.json");
|
|
116
|
+
}
|
|
114
117
|
function deployCredsPath(provider) {
|
|
115
118
|
return join(deployDir(), `${provider}.env`);
|
|
116
119
|
}
|
|
@@ -169,7 +172,7 @@ __export(creds_exports, {
|
|
|
169
172
|
writeTailscaleEnv: () => writeTailscaleEnv
|
|
170
173
|
});
|
|
171
174
|
import { chmodSync as chmodSync3, existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync27 } from "fs";
|
|
172
|
-
import { createInterface as
|
|
175
|
+
import { createInterface as createInterface4 } from "readline";
|
|
173
176
|
function ensureDeployDir() {
|
|
174
177
|
const dir = deployDir();
|
|
175
178
|
if (!existsSync24(dir)) mkdirSync12(dir, { recursive: true, mode: 448 });
|
|
@@ -207,7 +210,7 @@ function writeEnvFile(path, values) {
|
|
|
207
210
|
chmodSync3(path, 384);
|
|
208
211
|
}
|
|
209
212
|
async function promptLine(question, hidden) {
|
|
210
|
-
const rl =
|
|
213
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout, terminal: true });
|
|
211
214
|
if (hidden) {
|
|
212
215
|
const stdout = process.stdout;
|
|
213
216
|
const originalWrite = stdout.write.bind(stdout);
|
|
@@ -293,7 +296,7 @@ var init_creds = __esm({
|
|
|
293
296
|
|
|
294
297
|
// src/cli/index.ts
|
|
295
298
|
import { Command } from "commander";
|
|
296
|
-
import { existsSync as
|
|
299
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync14, readFileSync as readFileSync31 } from "fs";
|
|
297
300
|
import { dirname as dirname12, join as join26 } from "path";
|
|
298
301
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
299
302
|
|
|
@@ -354,6 +357,7 @@ import { execSync } from "child_process";
|
|
|
354
357
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, unlinkSync } from "fs";
|
|
355
358
|
import { homedir as homedir2 } from "os";
|
|
356
359
|
import { join as join2 } from "path";
|
|
360
|
+
import { createInterface } from "readline";
|
|
357
361
|
|
|
358
362
|
// src/shared/keymap.ts
|
|
359
363
|
function formatHelpForKeymap(km) {
|
|
@@ -1717,7 +1721,22 @@ function getExistingBinding(key, table = "root") {
|
|
|
1717
1721
|
function isSisyphusBinding(binding) {
|
|
1718
1722
|
return binding.includes("sisyphus");
|
|
1719
1723
|
}
|
|
1720
|
-
function
|
|
1724
|
+
async function confirmConfAppend(userConf, line) {
|
|
1725
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
1726
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1727
|
+
return new Promise((resolve11) => {
|
|
1728
|
+
const question = `
|
|
1729
|
+
Sisyphus needs to append one line to ${userConf} so its tmux keybindings persist:
|
|
1730
|
+
${line}
|
|
1731
|
+
|
|
1732
|
+
Append it now? (y/N) `;
|
|
1733
|
+
rl.question(question, (answer) => {
|
|
1734
|
+
rl.close();
|
|
1735
|
+
resolve11(answer.trim().toLowerCase() === "y");
|
|
1736
|
+
});
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
async function setupTmuxKeybind(cycleKey = DEFAULT_CYCLE_KEY, prefixKey = DEFAULT_PREFIX_KEY, opts = {}) {
|
|
1721
1740
|
installAllScripts();
|
|
1722
1741
|
if (!tmuxVersionAtLeast(3, 2)) {
|
|
1723
1742
|
let version = "unknown";
|
|
@@ -1757,14 +1776,22 @@ ${bindings.join("\n")}
|
|
|
1757
1776
|
const userConf = userTmuxConfPath();
|
|
1758
1777
|
const markedSourceLine = `source-file ${confPath} ${SISYPHUS_CONF_MARKER}`;
|
|
1759
1778
|
let persistedToConf = false;
|
|
1779
|
+
let appendDeclined = false;
|
|
1760
1780
|
if (userConf !== null) {
|
|
1761
1781
|
const contents = readFileSync(userConf, "utf8");
|
|
1762
|
-
if (
|
|
1763
|
-
|
|
1764
|
-
|
|
1782
|
+
if (contents.includes(confPath)) {
|
|
1783
|
+
persistedToConf = true;
|
|
1784
|
+
} else {
|
|
1785
|
+
const shouldAppend = opts.assumeYes ? true : await confirmConfAppend(userConf, markedSourceLine);
|
|
1786
|
+
if (shouldAppend) {
|
|
1787
|
+
const separator = contents.endsWith("\n") ? "" : "\n";
|
|
1788
|
+
writeFileSync(userConf, `${contents}${separator}${markedSourceLine}
|
|
1765
1789
|
`, "utf8");
|
|
1790
|
+
persistedToConf = true;
|
|
1791
|
+
} else {
|
|
1792
|
+
appendDeclined = true;
|
|
1793
|
+
}
|
|
1766
1794
|
}
|
|
1767
|
-
persistedToConf = true;
|
|
1768
1795
|
}
|
|
1769
1796
|
try {
|
|
1770
1797
|
for (const b of bindings) {
|
|
@@ -1772,6 +1799,16 @@ ${bindings.join("\n")}
|
|
|
1772
1799
|
}
|
|
1773
1800
|
} catch {
|
|
1774
1801
|
}
|
|
1802
|
+
if (appendDeclined && userConf !== null) {
|
|
1803
|
+
return {
|
|
1804
|
+
status: "conf-modification-declined",
|
|
1805
|
+
message: `Tmux keybindings applied to the live session, but not persisted.
|
|
1806
|
+
To persist them, add this line to ${userConf}:
|
|
1807
|
+
${markedSourceLine}`,
|
|
1808
|
+
manualLine: markedSourceLine,
|
|
1809
|
+
userConf
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1775
1812
|
if (getExistingBinding(cycleKey) !== null && isSisyphusBinding(getExistingBinding(cycleKey))) {
|
|
1776
1813
|
return {
|
|
1777
1814
|
status: "already-installed",
|
|
@@ -2061,7 +2098,7 @@ async function ensureDaemonInstalled() {
|
|
|
2061
2098
|
const plist = generatePlist(nodePath, daemonPath, logPath);
|
|
2062
2099
|
writeFileSync2(plistPath(), plist, "utf8");
|
|
2063
2100
|
execSync3(`launchctl load -w ${plistPath()}`);
|
|
2064
|
-
const keybindResult = setupTmuxKeybind();
|
|
2101
|
+
const keybindResult = await setupTmuxKeybind();
|
|
2065
2102
|
await ensureRequiredPlugins(process.cwd());
|
|
2066
2103
|
printGettingStarted(keybindResult, sisyphusPlugin);
|
|
2067
2104
|
}
|
|
@@ -2102,6 +2139,8 @@ function printGettingStarted(keybindResult, sisyphusPlugin) {
|
|
|
2102
2139
|
lines.push(`Tmux keybind: ${keybindResult.message}`, "");
|
|
2103
2140
|
} else if (keybindResult.status === "conflict") {
|
|
2104
2141
|
lines.push(`Keybind: ${keybindResult.message}`, "");
|
|
2142
|
+
} else if (keybindResult.status === "conf-modification-declined") {
|
|
2143
|
+
lines.push(keybindResult.message, "");
|
|
2105
2144
|
}
|
|
2106
2145
|
if (sisyphusPlugin.installed && sisyphusPlugin.autoInstalled) {
|
|
2107
2146
|
lines.push(`Sisyphus plugin installed: sisyphus@sisyphus \u2192 ${sisyphusPlugin.installPath}`, "");
|
|
@@ -3073,17 +3112,21 @@ function inlineBodyPath(deckPath, bodyPath) {
|
|
|
3073
3112
|
const deckDir = dirname2(deckPath);
|
|
3074
3113
|
const joined = resolve2(deckDir, bodyPath);
|
|
3075
3114
|
if (!existsSync5(joined)) {
|
|
3076
|
-
throw new Error(
|
|
3115
|
+
throw new Error(
|
|
3116
|
+
`bodyPath does not exist: '${bodyPath}' (resolved against deck dir '${deckDir}'). bodyPath is interpreted relative to the deck JSON's directory; place the body file there and use a relative path (e.g. "completion-summary.md").`
|
|
3117
|
+
);
|
|
3077
3118
|
}
|
|
3078
3119
|
const stat = lstatSync(joined);
|
|
3079
3120
|
if (!stat.isFile()) {
|
|
3080
|
-
throw new Error(`bodyPath must be a regular file: ${bodyPath}`);
|
|
3121
|
+
throw new Error(`bodyPath must be a regular file (not a symlink, directory, or special file): ${bodyPath}`);
|
|
3081
3122
|
}
|
|
3082
3123
|
const realResolved = realpathSync(joined);
|
|
3083
3124
|
const realDeckDir = realpathSync(deckDir);
|
|
3084
3125
|
const prefix = realDeckDir + sep;
|
|
3085
3126
|
if (realResolved !== realDeckDir && !realResolved.startsWith(prefix)) {
|
|
3086
|
-
throw new Error(
|
|
3127
|
+
throw new Error(
|
|
3128
|
+
`bodyPath '${bodyPath}' escapes the deck's directory ('${realDeckDir}'). bodyPath is resolved relative to the deck JSON file and must stay inside its directory (no '..', absolute paths pointing elsewhere, or symlinks out). Fix: write the deck JSON next to the body file (e.g. both inside $SISYPHUS_SESSION_DIR/context/) and use a relative path like "completion-summary.md".`
|
|
3129
|
+
);
|
|
3087
3130
|
}
|
|
3088
3131
|
return readFileSync8(joined, "utf-8");
|
|
3089
3132
|
}
|
|
@@ -3469,7 +3512,42 @@ The CLI always blocks until the user answers (which can take 10+ minutes).
|
|
|
3469
3512
|
|
|
3470
3513
|
For guidance on when to use a deck, how to design options the user can actually choose between, and how to bundle related questions into one deck, read the \`humanloop\` skill before authoring.
|
|
3471
3514
|
|
|
3472
|
-
|
|
3515
|
+
DECK JSON SCHEMA
|
|
3516
|
+
{ "title"?: string, "interactions": Interaction[] } // interactions[] non-empty
|
|
3517
|
+
|
|
3518
|
+
Interaction:
|
|
3519
|
+
id string, /^[A-Za-z0-9_-]+$/, max 64 chars, unique within deck
|
|
3520
|
+
title string (required, non-empty)
|
|
3521
|
+
subtitle? string
|
|
3522
|
+
body? string // markdown rendered in dashboard
|
|
3523
|
+
bodyPath? string // path RELATIVE to the deck JSON's directory
|
|
3524
|
+
// and must resolve INSIDE that directory
|
|
3525
|
+
// (no '..', no symlinks out, no absolute
|
|
3526
|
+
// paths pointing elsewhere). Mutually
|
|
3527
|
+
// exclusive with 'body'. To use bodyPath,
|
|
3528
|
+
// write the deck JSON next to the markdown
|
|
3529
|
+
// file (e.g. both in
|
|
3530
|
+
// $SISYPHUS_SESSION_DIR/context/) and pass
|
|
3531
|
+
// a basename like "summary.md".
|
|
3532
|
+
kind? "notify" | "validation" | "decision" | "context" | "error"
|
|
3533
|
+
// display hint for inbox icon/sort weight.
|
|
3534
|
+
// No other values accepted.
|
|
3535
|
+
options Option[] // 2\u20134 options recommended (see humanloop)
|
|
3536
|
+
allowFreetext? boolean
|
|
3537
|
+
freetextLabel? string
|
|
3538
|
+
|
|
3539
|
+
Option:
|
|
3540
|
+
id string (required)
|
|
3541
|
+
label string (required)
|
|
3542
|
+
description? string
|
|
3543
|
+
shortcut? string
|
|
3544
|
+
|
|
3545
|
+
OUTPUT
|
|
3546
|
+
On answer, stdout is one line of JSON:
|
|
3547
|
+
{ "responses": [{ "id", "selectedOptionId"?, "freetext"? }, ...], "completedAt" }
|
|
3548
|
+
Branch on each response by its interaction \`id\`.
|
|
3549
|
+
|
|
3550
|
+
Validation errors at submit are precise \u2014 read them, don't guess.
|
|
3473
3551
|
`).action(async (file, opts) => {
|
|
3474
3552
|
if (!file) {
|
|
3475
3553
|
ask.help();
|
|
@@ -4823,7 +4901,7 @@ function printResults(result, daemonOk, keybindMsg) {
|
|
|
4823
4901
|
console.log("");
|
|
4824
4902
|
}
|
|
4825
4903
|
function registerSetup(program2) {
|
|
4826
|
-
program2.command("setup").description("One-time setup: install dependencies, daemon, keybindings, and commands").action(async () => {
|
|
4904
|
+
program2.command("setup").description("One-time setup: install dependencies, daemon, keybindings, and commands").option("-y, --yes", "Skip confirmation prompts (e.g. before modifying ~/.tmux.conf)").action(async (opts) => {
|
|
4827
4905
|
const result = runOnboarding();
|
|
4828
4906
|
let daemonOk = false;
|
|
4829
4907
|
try {
|
|
@@ -4832,7 +4910,11 @@ function registerSetup(program2) {
|
|
|
4832
4910
|
} catch {
|
|
4833
4911
|
daemonOk = isInstalled();
|
|
4834
4912
|
}
|
|
4835
|
-
const keybindResult = setupTmuxKeybind(
|
|
4913
|
+
const keybindResult = await setupTmuxKeybind(
|
|
4914
|
+
DEFAULT_CYCLE_KEY,
|
|
4915
|
+
DEFAULT_PREFIX_KEY,
|
|
4916
|
+
{ assumeYes: opts.yes }
|
|
4917
|
+
);
|
|
4836
4918
|
let keybindMsg;
|
|
4837
4919
|
if (keybindResult.status === "installed" || keybindResult.status === "already-installed") {
|
|
4838
4920
|
keybindMsg = `${DEFAULT_CYCLE_KEY} cycle, ${DEFAULT_PREFIX_KEY} prefix (h=dashboard, x=kill)`;
|
|
@@ -4845,9 +4927,9 @@ function registerSetup(program2) {
|
|
|
4845
4927
|
|
|
4846
4928
|
// src/cli/commands/setup-keybind.ts
|
|
4847
4929
|
function registerSetupKeybind(program2) {
|
|
4848
|
-
program2.command("setup-keybind [cycle-key]").description("Install sisyphus tmux keybindings (default: M-s cycle, C-s prefix)").action(async (key) => {
|
|
4930
|
+
program2.command("setup-keybind [cycle-key]").description("Install sisyphus tmux keybindings (default: M-s cycle, C-s prefix)").option("-y, --yes", "Skip confirmation prompt before modifying ~/.tmux.conf").action(async (key, opts) => {
|
|
4849
4931
|
const resolvedKey = key ?? DEFAULT_CYCLE_KEY;
|
|
4850
|
-
const result = setupTmuxKeybind(resolvedKey);
|
|
4932
|
+
const result = await setupTmuxKeybind(resolvedKey, void 0, { assumeYes: opts.yes });
|
|
4851
4933
|
switch (result.status) {
|
|
4852
4934
|
case "installed":
|
|
4853
4935
|
console.log(result.message);
|
|
@@ -4868,6 +4950,11 @@ function registerSetupKeybind(program2) {
|
|
|
4868
4950
|
case "unsupported-tmux":
|
|
4869
4951
|
console.log(result.message);
|
|
4870
4952
|
break;
|
|
4953
|
+
case "conf-modification-declined":
|
|
4954
|
+
console.log(result.message);
|
|
4955
|
+
console.log("");
|
|
4956
|
+
console.log("Re-run with --yes to skip the prompt.");
|
|
4957
|
+
break;
|
|
4871
4958
|
}
|
|
4872
4959
|
});
|
|
4873
4960
|
}
|
|
@@ -5208,9 +5295,9 @@ function registerInit(program2) {
|
|
|
5208
5295
|
}
|
|
5209
5296
|
|
|
5210
5297
|
// src/cli/commands/uninstall.ts
|
|
5211
|
-
import { createInterface } from "readline";
|
|
5298
|
+
import { createInterface as createInterface2 } from "readline";
|
|
5212
5299
|
async function confirm(question) {
|
|
5213
|
-
const rl =
|
|
5300
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
5214
5301
|
return new Promise((resolve11) => {
|
|
5215
5302
|
rl.question(question, (answer) => {
|
|
5216
5303
|
rl.close();
|
|
@@ -5235,12 +5322,12 @@ function registerUninstall(program2) {
|
|
|
5235
5322
|
// src/cli/commands/configure-upload.ts
|
|
5236
5323
|
init_paths();
|
|
5237
5324
|
import { chmodSync as chmodSync2, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync19, writeFileSync as writeFileSync10 } from "fs";
|
|
5238
|
-
import { createInterface as
|
|
5325
|
+
import { createInterface as createInterface3 } from "readline";
|
|
5239
5326
|
import { dirname as dirname6 } from "path";
|
|
5240
5327
|
async function readUrlFromInput(interactive) {
|
|
5241
5328
|
if (interactive) {
|
|
5242
5329
|
return new Promise((resolve11) => {
|
|
5243
|
-
const rl =
|
|
5330
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
5244
5331
|
rl.question("Paste the upload URL (with embedded ?token=): ", (answer) => {
|
|
5245
5332
|
rl.close();
|
|
5246
5333
|
resolve11(answer.trim());
|
|
@@ -9366,7 +9453,7 @@ import { join as join25 } from "path";
|
|
|
9366
9453
|
// src/cli/deploy/runner.ts
|
|
9367
9454
|
init_paths();
|
|
9368
9455
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
|
|
9369
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
9456
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync27, mkdirSync as mkdirSync13, readFileSync as readFileSync29 } from "fs";
|
|
9370
9457
|
init_creds();
|
|
9371
9458
|
|
|
9372
9459
|
// src/cli/deploy/pricing.ts
|
|
@@ -9399,16 +9486,82 @@ function formatCostLine(provider, instanceType) {
|
|
|
9399
9486
|
return `Estimated cost: ~$${cost.toFixed(2)}/mo (pricing last verified ${LAST_VERIFIED}; verify against your bill for current rates).`;
|
|
9400
9487
|
}
|
|
9401
9488
|
|
|
9489
|
+
// src/cli/deploy/runtime.ts
|
|
9490
|
+
init_atomic();
|
|
9491
|
+
init_paths();
|
|
9492
|
+
import { existsSync as existsSync25, readFileSync as readFileSync28, unlinkSync as unlinkSync4 } from "fs";
|
|
9493
|
+
function readRuntimeState(provider) {
|
|
9494
|
+
const path = deployRuntimePath(provider);
|
|
9495
|
+
if (!existsSync25(path)) return null;
|
|
9496
|
+
try {
|
|
9497
|
+
return JSON.parse(readFileSync28(path, "utf-8"));
|
|
9498
|
+
} catch {
|
|
9499
|
+
return null;
|
|
9500
|
+
}
|
|
9501
|
+
}
|
|
9502
|
+
function writeRuntimeState(provider, state) {
|
|
9503
|
+
atomicWrite(deployRuntimePath(provider), JSON.stringify(state, null, 2) + "\n");
|
|
9504
|
+
}
|
|
9505
|
+
function clearRuntimeState(provider) {
|
|
9506
|
+
const path = deployRuntimePath(provider);
|
|
9507
|
+
if (existsSync25(path)) unlinkSync4(path);
|
|
9508
|
+
}
|
|
9509
|
+
|
|
9510
|
+
// src/cli/deploy/tailnet.ts
|
|
9511
|
+
function discoverNode(requestedName) {
|
|
9512
|
+
const json = execSafe("tailscale status --json");
|
|
9513
|
+
if (!json) return null;
|
|
9514
|
+
let status;
|
|
9515
|
+
try {
|
|
9516
|
+
status = JSON.parse(json);
|
|
9517
|
+
} catch {
|
|
9518
|
+
return null;
|
|
9519
|
+
}
|
|
9520
|
+
const peers = status.Peer === void 0 ? [] : Object.values(status.Peer);
|
|
9521
|
+
const candidates = peers.filter(
|
|
9522
|
+
(p) => p.HostName === requestedName && p.Online === true
|
|
9523
|
+
);
|
|
9524
|
+
if (candidates.length === 0) return null;
|
|
9525
|
+
candidates.sort((a, b) => {
|
|
9526
|
+
const ac = a.Created === void 0 ? "" : a.Created;
|
|
9527
|
+
const bc = b.Created === void 0 ? "" : b.Created;
|
|
9528
|
+
return bc.localeCompare(ac);
|
|
9529
|
+
});
|
|
9530
|
+
const peer = candidates[0];
|
|
9531
|
+
const dnsRaw = peer.DNSName === void 0 ? "" : peer.DNSName;
|
|
9532
|
+
const dns = dnsRaw.replace(/\.$/, "");
|
|
9533
|
+
if (!dns) return null;
|
|
9534
|
+
const shortName = dns.split(".")[0];
|
|
9535
|
+
const ips = peer.TailscaleIPs === void 0 ? [] : peer.TailscaleIPs;
|
|
9536
|
+
const ipv4 = ips.find((ip) => /^\d+\.\d+\.\d+\.\d+$/.test(ip));
|
|
9537
|
+
if (!ipv4) return null;
|
|
9538
|
+
const ipv6 = ips.find((ip) => ip.includes(":")) ?? null;
|
|
9539
|
+
return { shortName, magicDnsName: dns, ipv4, ipv6 };
|
|
9540
|
+
}
|
|
9541
|
+
async function discoverNodeWithRetry(requestedName, maxRetries = 30, intervalMs = 2e3) {
|
|
9542
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
9543
|
+
const node = discoverNode(requestedName);
|
|
9544
|
+
if (node) return node;
|
|
9545
|
+
if (i < maxRetries - 1) {
|
|
9546
|
+
await new Promise((resolve11) => setTimeout(resolve11, intervalMs));
|
|
9547
|
+
}
|
|
9548
|
+
}
|
|
9549
|
+
return null;
|
|
9550
|
+
}
|
|
9551
|
+
function isTailscaleAvailable() {
|
|
9552
|
+
return execSafe("tailscale version") !== null;
|
|
9553
|
+
}
|
|
9554
|
+
|
|
9402
9555
|
// src/cli/deploy/templates.ts
|
|
9403
|
-
import { existsSync as
|
|
9556
|
+
import { existsSync as existsSync26 } from "fs";
|
|
9404
9557
|
import { dirname as dirname11, resolve as resolve10 } from "path";
|
|
9405
9558
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
9406
9559
|
function deployRoot() {
|
|
9407
9560
|
const here = dirname11(fileURLToPath4(import.meta.url));
|
|
9408
9561
|
const bundled = resolve10(here, "..", "deploy");
|
|
9409
|
-
if (
|
|
9562
|
+
if (existsSync26(bundled)) return bundled;
|
|
9410
9563
|
const sourceRoot = resolve10(here, "..", "..", "..", "deploy");
|
|
9411
|
-
if (
|
|
9564
|
+
if (existsSync26(sourceRoot)) return sourceRoot;
|
|
9412
9565
|
throw new Error(
|
|
9413
9566
|
`Could not locate deploy/ templates. Looked at:
|
|
9414
9567
|
${bundled}
|
|
@@ -9445,9 +9598,10 @@ async function firstRunPrompt() {
|
|
|
9445
9598
|
console.log("");
|
|
9446
9599
|
console.log("Tailscale credentials not configured. Pick one:");
|
|
9447
9600
|
console.log("");
|
|
9448
|
-
console.log(" 1) OAuth client (recommended) \u2014 mints fresh ephemeral keys per box
|
|
9601
|
+
console.log(" 1) OAuth client (recommended) \u2014 mints fresh ephemeral keys per box");
|
|
9602
|
+
console.log(" and auto-cleans stale offline nodes so hostnames don't get suffixed.");
|
|
9449
9603
|
console.log(" Create at https://login.tailscale.com/admin/settings/oauth");
|
|
9450
|
-
console.log(" with
|
|
9604
|
+
console.log(" with scopes `auth_keys:write` + `devices:write` and tag `tag:sisyphus`.");
|
|
9451
9605
|
console.log(" 2) Reusable auth key (simpler) \u2014 paste from");
|
|
9452
9606
|
console.log(" https://login.tailscale.com/admin/settings/keys");
|
|
9453
9607
|
console.log("");
|
|
@@ -9469,6 +9623,14 @@ async function firstRunPrompt() {
|
|
|
9469
9623
|
}
|
|
9470
9624
|
async function mintViaOAuth(clientId, clientSecret, tag, hostname) {
|
|
9471
9625
|
const token = await fetchAccessToken(clientId, clientSecret);
|
|
9626
|
+
try {
|
|
9627
|
+
const removed = await deleteStaleDevicesForHostname(token, hostname);
|
|
9628
|
+
if (removed > 0) {
|
|
9629
|
+
console.log(`Tailscale: removed ${removed} stale offline node(s) named "${hostname}".`);
|
|
9630
|
+
}
|
|
9631
|
+
} catch (err) {
|
|
9632
|
+
console.log(`Tailscale: skipped stale-node cleanup (${err.message}). Add 'devices:write' scope to clean up automatically.`);
|
|
9633
|
+
}
|
|
9472
9634
|
const body = {
|
|
9473
9635
|
capabilities: {
|
|
9474
9636
|
devices: {
|
|
@@ -9499,6 +9661,32 @@ async function mintViaOAuth(clientId, clientSecret, tag, hostname) {
|
|
|
9499
9661
|
if (!data.key) throw new Error("Tailscale API returned no key.");
|
|
9500
9662
|
return data.key;
|
|
9501
9663
|
}
|
|
9664
|
+
async function deleteStaleDevicesForHostname(token, hostname) {
|
|
9665
|
+
const res = await fetch(`${TS_API}/tailnet/-/devices`, {
|
|
9666
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
9667
|
+
});
|
|
9668
|
+
if (!res.ok) {
|
|
9669
|
+
throw new Error(`HTTP ${res.status} listing devices`);
|
|
9670
|
+
}
|
|
9671
|
+
const data = await res.json();
|
|
9672
|
+
const STALE_AFTER_MS = 5 * 60 * 1e3;
|
|
9673
|
+
const now = Date.now();
|
|
9674
|
+
const stale = data.devices.filter((d) => {
|
|
9675
|
+
if (d.hostname !== hostname) return false;
|
|
9676
|
+
const seen = Date.parse(d.lastSeen);
|
|
9677
|
+
if (Number.isNaN(seen)) return false;
|
|
9678
|
+
return now - seen > STALE_AFTER_MS;
|
|
9679
|
+
});
|
|
9680
|
+
let removed = 0;
|
|
9681
|
+
for (const device of stale) {
|
|
9682
|
+
const r = await fetch(`${TS_API}/device/${device.id}`, {
|
|
9683
|
+
method: "DELETE",
|
|
9684
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
9685
|
+
});
|
|
9686
|
+
if (r.ok) removed++;
|
|
9687
|
+
}
|
|
9688
|
+
return removed;
|
|
9689
|
+
}
|
|
9502
9690
|
async function fetchAccessToken(clientId, clientSecret) {
|
|
9503
9691
|
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
9504
9692
|
const res = await fetch(`${TS_API}/oauth/token`, {
|
|
@@ -9557,14 +9745,14 @@ function ensureTerraformInstalled() {
|
|
|
9557
9745
|
function ensureProviderStateDir(provider) {
|
|
9558
9746
|
ensureDeployDir();
|
|
9559
9747
|
const dir = deployProviderDir(provider);
|
|
9560
|
-
if (!
|
|
9748
|
+
if (!existsSync27(dir)) mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
9561
9749
|
}
|
|
9562
9750
|
function backupState(provider) {
|
|
9563
9751
|
const src = deployStatePath(provider);
|
|
9564
|
-
if (
|
|
9752
|
+
if (existsSync27(src)) copyFileSync2(src, deployStateBackupPath(provider));
|
|
9565
9753
|
}
|
|
9566
9754
|
function readSshPubkey(path) {
|
|
9567
|
-
if (!
|
|
9755
|
+
if (!existsSync27(path)) {
|
|
9568
9756
|
const privateKeyPath = path.replace(/\.pub$/, "");
|
|
9569
9757
|
throw new Error(
|
|
9570
9758
|
`SSH pubkey not found at ${path}. Generate one with:
|
|
@@ -9572,7 +9760,7 @@ function readSshPubkey(path) {
|
|
|
9572
9760
|
or pass --ssh-key <path>.`
|
|
9573
9761
|
);
|
|
9574
9762
|
}
|
|
9575
|
-
return
|
|
9763
|
+
return readFileSync29(path, "utf-8").trim();
|
|
9576
9764
|
}
|
|
9577
9765
|
function readOutputs(provider) {
|
|
9578
9766
|
const result = spawnSync3("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
|
|
@@ -9599,10 +9787,13 @@ function readOutputs(provider) {
|
|
|
9599
9787
|
}
|
|
9600
9788
|
}
|
|
9601
9789
|
function isProvisioned(provider) {
|
|
9602
|
-
if (!
|
|
9790
|
+
if (!existsSync27(deployStatePath(provider))) return false;
|
|
9603
9791
|
return readOutputs(provider) !== null;
|
|
9604
9792
|
}
|
|
9605
9793
|
async function deployUp(provider, opts) {
|
|
9794
|
+
if (isProvisioned(provider) && !await confirmReprovision(provider, opts.yes)) {
|
|
9795
|
+
return;
|
|
9796
|
+
}
|
|
9606
9797
|
const sshPubkey = readSshPubkey(opts.sshKey);
|
|
9607
9798
|
const creds = await loadProviderCreds(provider);
|
|
9608
9799
|
const tsAuthKey = await mintTailscaleKey({ hostname: opts.name });
|
|
@@ -9650,16 +9841,39 @@ Applied \u2014 but could not parse outputs. Run \`sisyphus deploy ${provider} st
|
|
|
9650
9841
|
console.log("");
|
|
9651
9842
|
console.log("Box provisioned. Cloud-init will run for ~3\u20135 minutes before the daemon is reachable.");
|
|
9652
9843
|
console.log("");
|
|
9653
|
-
console.log(` IP:
|
|
9654
|
-
console.log(`
|
|
9655
|
-
|
|
9844
|
+
console.log(` Public IP: ${outputs.ipv4}`);
|
|
9845
|
+
console.log(` Requested hostname: ${outputs.tailscale_hostname}`);
|
|
9846
|
+
if (isTailscaleAvailable()) {
|
|
9847
|
+
process.stdout.write(" Waiting for tailnet join...");
|
|
9848
|
+
const node = await discoverNodeWithRetry(opts.name);
|
|
9849
|
+
if (node) {
|
|
9850
|
+
writeRuntimeState(provider, {
|
|
9851
|
+
tailscaleHostname: node.shortName,
|
|
9852
|
+
tailscaleFqdn: node.magicDnsName,
|
|
9853
|
+
tailscaleIpv4: node.ipv4,
|
|
9854
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9855
|
+
});
|
|
9856
|
+
process.stdout.write(` joined as ${node.shortName} (${node.ipv4})
|
|
9857
|
+
`);
|
|
9858
|
+
if (node.shortName !== opts.name) {
|
|
9859
|
+
console.log(` Note: Tailscale suffixed the hostname because "${opts.name}" was already`);
|
|
9860
|
+
console.log(" claimed by an offline node. Delete the stale node at");
|
|
9861
|
+
console.log(" https://login.tailscale.com/admin/machines to reuse the original name.");
|
|
9862
|
+
}
|
|
9863
|
+
} else {
|
|
9864
|
+
process.stdout.write(" no peer matched after 60s\n");
|
|
9865
|
+
console.log(" (Cloud-init may still be installing Tailscale. Re-run `status` later.)");
|
|
9866
|
+
}
|
|
9867
|
+
} else {
|
|
9868
|
+
console.log(" (Local `tailscale` CLI not on PATH \u2014 skipping tailnet discovery.)");
|
|
9869
|
+
}
|
|
9656
9870
|
console.log("");
|
|
9657
9871
|
console.log(` Tail provisioning: sisyphus deploy ${provider} logs`);
|
|
9658
9872
|
console.log(` Verify daemon: sisyphus deploy ${provider} ssh -- sisyphus admin doctor`);
|
|
9659
9873
|
console.log("");
|
|
9660
9874
|
}
|
|
9661
9875
|
async function deployDown(provider, opts) {
|
|
9662
|
-
if (!
|
|
9876
|
+
if (!existsSync27(deployStatePath(provider))) {
|
|
9663
9877
|
console.log(`No ${provider} state found at ${deployStatePath(provider)}. Nothing to destroy.`);
|
|
9664
9878
|
return;
|
|
9665
9879
|
}
|
|
@@ -9690,6 +9904,7 @@ async function deployDown(provider, opts) {
|
|
|
9690
9904
|
creds
|
|
9691
9905
|
);
|
|
9692
9906
|
if (code !== 0) throw new Error(`terraform destroy failed (exit ${code})`);
|
|
9907
|
+
clearRuntimeState(provider);
|
|
9693
9908
|
console.log(`
|
|
9694
9909
|
${provider} box destroyed.`);
|
|
9695
9910
|
}
|
|
@@ -9703,10 +9918,21 @@ function deployStatus(provider) {
|
|
|
9703
9918
|
console.log(`${provider}: state present but outputs unreadable. Try \`sisyphus deploy ${provider} up\` to reconcile.`);
|
|
9704
9919
|
return;
|
|
9705
9920
|
}
|
|
9921
|
+
const runtime = readRuntimeState(provider);
|
|
9922
|
+
const effectiveHost = runtime ? runtime.tailscaleHostname : outputs.tailscale_hostname;
|
|
9706
9923
|
console.log(`${provider}: provisioned`);
|
|
9707
|
-
console.log(` IP:
|
|
9708
|
-
console.log(` Tailscale hostname: ${
|
|
9709
|
-
|
|
9924
|
+
console.log(` Public IP: ${outputs.ipv4}`);
|
|
9925
|
+
console.log(` Tailscale hostname: ${effectiveHost}`);
|
|
9926
|
+
if (runtime) {
|
|
9927
|
+
console.log(` Tailscale IPv4: ${runtime.tailscaleIpv4}`);
|
|
9928
|
+
console.log(` MagicDNS FQDN: ${runtime.tailscaleFqdn}`);
|
|
9929
|
+
if (runtime.tailscaleHostname !== outputs.tailscale_hostname) {
|
|
9930
|
+
console.log(` (Requested "${outputs.tailscale_hostname}" but Tailscale assigned "${runtime.tailscaleHostname}".)`);
|
|
9931
|
+
}
|
|
9932
|
+
} else {
|
|
9933
|
+
console.log(" (No tailnet runtime state \u2014 re-run `up` or check that local tailscale is logged in.)");
|
|
9934
|
+
}
|
|
9935
|
+
console.log(` SSH: ssh sisyphus@${effectiveHost}`);
|
|
9710
9936
|
console.log(` Instance type: ${outputs.instance_type}`);
|
|
9711
9937
|
console.log(` ${formatCostLine(provider, outputs.instance_type)}`);
|
|
9712
9938
|
}
|
|
@@ -9717,16 +9943,17 @@ function deployListProviders() {
|
|
|
9717
9943
|
console.log(` ${p.padEnd(10)} ${status}`);
|
|
9718
9944
|
}
|
|
9719
9945
|
}
|
|
9720
|
-
function
|
|
9721
|
-
const
|
|
9722
|
-
if (
|
|
9946
|
+
function effectiveSshTarget(provider) {
|
|
9947
|
+
const runtime = readRuntimeState(provider);
|
|
9948
|
+
if (runtime) return `sisyphus@${runtime.tailscaleHostname}`;
|
|
9949
|
+
const outputs = readOutputs(provider);
|
|
9950
|
+
if (!outputs) {
|
|
9723
9951
|
throw new Error(`${provider} not provisioned. Run \`sisyphus deploy ${provider} up\`.`);
|
|
9724
9952
|
}
|
|
9725
|
-
return
|
|
9953
|
+
return `sisyphus@${outputs.tailscale_hostname}`;
|
|
9726
9954
|
}
|
|
9727
9955
|
function deploySsh(provider, remoteCmd) {
|
|
9728
|
-
const
|
|
9729
|
-
const target = `sisyphus@${outputs.tailscale_hostname}`;
|
|
9956
|
+
const target = effectiveSshTarget(provider);
|
|
9730
9957
|
const moshAvailable = spawnSync3("mosh", ["--version"], { stdio: "pipe", env: EXEC_ENV }).status === 0;
|
|
9731
9958
|
const bin = moshAvailable && remoteCmd.length === 0 ? "mosh" : "ssh";
|
|
9732
9959
|
const args2 = remoteCmd.length > 0 ? [target, ...remoteCmd] : [target];
|
|
@@ -9734,15 +9961,13 @@ function deploySsh(provider, remoteCmd) {
|
|
|
9734
9961
|
child.on("exit", (code) => process.exit(code === null ? 1 : code));
|
|
9735
9962
|
}
|
|
9736
9963
|
function deployLogs(provider) {
|
|
9737
|
-
const
|
|
9738
|
-
const target = `sisyphus@${outputs.tailscale_hostname}`;
|
|
9964
|
+
const target = effectiveSshTarget(provider);
|
|
9739
9965
|
const remoteCmd = "tail -F -n 200 /var/log/cloud-init-output.log ~/.sisyphus/daemon.log 2>/dev/null";
|
|
9740
9966
|
const child = spawn2("ssh", [target, remoteCmd], { stdio: "inherit", env: EXEC_ENV });
|
|
9741
9967
|
child.on("exit", (code) => process.exit(code === null ? 1 : code));
|
|
9742
9968
|
}
|
|
9743
9969
|
function deployUpdate(provider) {
|
|
9744
|
-
const
|
|
9745
|
-
const target = `sisyphus@${outputs.tailscale_hostname}`;
|
|
9970
|
+
const target = effectiveSshTarget(provider);
|
|
9746
9971
|
const remoteCmd = "sudo npm i -g sisyphi@latest && systemctl --user restart sisyphusd && sisyphusd --version || true";
|
|
9747
9972
|
const child = spawn2("ssh", [target, remoteCmd], { stdio: "inherit", env: EXEC_ENV });
|
|
9748
9973
|
child.on("exit", (code) => process.exit(code === null ? 1 : code));
|
|
@@ -9752,6 +9977,30 @@ async function confirm2(prompt) {
|
|
|
9752
9977
|
const answer = await promptLine2(`${prompt} `, false);
|
|
9753
9978
|
return answer.toLowerCase() === "yes";
|
|
9754
9979
|
}
|
|
9980
|
+
async function confirmReprovision(provider, yes) {
|
|
9981
|
+
const outputs = readOutputs(provider);
|
|
9982
|
+
console.log("");
|
|
9983
|
+
if (outputs) {
|
|
9984
|
+
console.log(`${provider} is already provisioned: "${outputs.tailscale_hostname}" (${outputs.instance_type}, ${outputs.ipv4}).`);
|
|
9985
|
+
} else {
|
|
9986
|
+
console.log(`${provider} state already exists at ${deployStatePath(provider)}.`);
|
|
9987
|
+
}
|
|
9988
|
+
if (provider === "hetzner") {
|
|
9989
|
+
console.log("Re-running `up` on Hetzner will DESTROY and RECREATE the box (user_data is ForceNew).");
|
|
9990
|
+
console.log("All on-box state \u2014 daemon history, sessions, anything not in your repo \u2014 will be lost.");
|
|
9991
|
+
} else {
|
|
9992
|
+
console.log("Re-running `up` on AWS updates user_data in state but does NOT recreate the instance.");
|
|
9993
|
+
console.log("Cloud-init won't re-run on the live box, and the freshly-minted Tailscale key will be wasted.");
|
|
9994
|
+
}
|
|
9995
|
+
console.log("");
|
|
9996
|
+
console.log(`To push a new sisyphus version: sisyphus deploy ${provider} update`);
|
|
9997
|
+
console.log(`To rebuild from scratch: sisyphus deploy ${provider} down && sisyphus deploy ${provider} up`);
|
|
9998
|
+
console.log("");
|
|
9999
|
+
if (yes) return true;
|
|
10000
|
+
const confirmed = await confirm2('Type "yes" to proceed anyway:');
|
|
10001
|
+
if (!confirmed) console.log("Aborted.");
|
|
10002
|
+
return confirmed;
|
|
10003
|
+
}
|
|
9755
10004
|
|
|
9756
10005
|
// src/cli/commands/deploy.ts
|
|
9757
10006
|
var PROVIDERS = ["hetzner", "aws"];
|
|
@@ -9771,7 +10020,8 @@ function resolveUpOptions(provider, raw) {
|
|
|
9771
10020
|
const size = raw.size === void 0 ? null : raw.size;
|
|
9772
10021
|
const withChromium = raw.chromium !== false;
|
|
9773
10022
|
const enableAutoUpdate = raw.autoUpdate !== false;
|
|
9774
|
-
|
|
10023
|
+
const yes = raw.yes === true;
|
|
10024
|
+
return { region, arch, size, sshKey: raw.sshKey, name: raw.name, withChromium, enableAutoUpdate, yes };
|
|
9775
10025
|
}
|
|
9776
10026
|
function registerDeploy(program2) {
|
|
9777
10027
|
const deploy = program2.command("deploy").description("Provision a Tailscale-only sisyphus box on Hetzner or AWS via Terraform.");
|
|
@@ -9788,7 +10038,7 @@ function registerDeploy(program2) {
|
|
|
9788
10038
|
});
|
|
9789
10039
|
for (const provider of PROVIDERS) {
|
|
9790
10040
|
const sub = deploy.command(provider).description(`${provider} commands.`);
|
|
9791
|
-
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.", join25(homedir11(), ".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").action(async (raw) => {
|
|
10041
|
+
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.", join25(homedir11(), ".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) => {
|
|
9792
10042
|
const opts = resolveUpOptions(provider, raw);
|
|
9793
10043
|
await deployUp(provider, opts);
|
|
9794
10044
|
});
|
|
@@ -9840,7 +10090,7 @@ function attachTmuxStatus(diagnostic2) {
|
|
|
9840
10090
|
// src/cli/commands/tmux-sessions.ts
|
|
9841
10091
|
init_paths();
|
|
9842
10092
|
import { execSync as execSync15 } from "child_process";
|
|
9843
|
-
import { readFileSync as
|
|
10093
|
+
import { readFileSync as readFileSync30, existsSync as existsSync28 } from "fs";
|
|
9844
10094
|
var DOT_MAP = {
|
|
9845
10095
|
"orchestrator:processing": { icon: "\u25CF", color: "#d4ad6a" },
|
|
9846
10096
|
"orchestrator:idle": { icon: "\u25CF", color: "#d47766" },
|
|
@@ -9851,9 +10101,9 @@ var DOT_MAP = {
|
|
|
9851
10101
|
};
|
|
9852
10102
|
function readManifest() {
|
|
9853
10103
|
const p = sessionsManifestPath();
|
|
9854
|
-
if (!
|
|
10104
|
+
if (!existsSync28(p)) return null;
|
|
9855
10105
|
try {
|
|
9856
|
-
return JSON.parse(
|
|
10106
|
+
return JSON.parse(readFileSync30(p, "utf-8"));
|
|
9857
10107
|
} catch {
|
|
9858
10108
|
return null;
|
|
9859
10109
|
}
|
|
@@ -9899,7 +10149,7 @@ if (nodeVersion < 22) {
|
|
|
9899
10149
|
var program = new Command();
|
|
9900
10150
|
program.name("sisyphus").description("tmux-integrated orchestration daemon for Claude Code").version(
|
|
9901
10151
|
JSON.parse(
|
|
9902
|
-
|
|
10152
|
+
readFileSync31(join26(dirname12(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf-8")
|
|
9903
10153
|
).version
|
|
9904
10154
|
);
|
|
9905
10155
|
program.configureHelp({
|
|
@@ -9969,7 +10219,7 @@ Run 'sisyphus admin getting-started' for a complete usage guide.
|
|
|
9969
10219
|
var args = process.argv.slice(2);
|
|
9970
10220
|
var firstArg = args[0];
|
|
9971
10221
|
var skipWelcome = ["admin", "help", "--help", "-h", "--version", "-V"];
|
|
9972
|
-
if (!
|
|
10222
|
+
if (!existsSync29(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
|
|
9973
10223
|
mkdirSync14(globalDir(), { recursive: true });
|
|
9974
10224
|
console.log("");
|
|
9975
10225
|
console.log(" Welcome to Sisyphus. Run 'sisyphus admin setup' to get started.");
|