sisyphi 1.1.24 → 1.1.26
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 +35 -35
- package/deploy/aws/main.tf +1 -1
- package/deploy/aws/variables.tf +1 -1
- package/deploy/aws/versions.tf +1 -1
- package/deploy/hetzner/variables.tf +1 -1
- package/deploy/hetzner/versions.tf +1 -1
- package/deploy/shared/cloud-init.yaml.tpl +1 -1
- package/dist/cli.js +911 -242
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +27 -11
- package/dist/daemon.js.map +1 -1
- package/dist/deploy/aws/main.tf +1 -1
- package/dist/deploy/aws/variables.tf +1 -1
- package/dist/deploy/aws/versions.tf +1 -1
- package/dist/deploy/hetzner/variables.tf +1 -1
- package/dist/deploy/hetzner/versions.tf +1 -1
- package/dist/deploy/shared/cloud-init.yaml.tpl +1 -1
- package/dist/templates/agent-plugin/agents/explore.md +2 -2
- package/dist/templates/agent-plugin/agents/implementor.md +2 -2
- package/dist/templates/agent-plugin/agents/operator.md +3 -3
- package/dist/templates/agent-plugin/agents/plan.md +2 -2
- package/dist/templates/agent-plugin/agents/problem.md +8 -8
- package/dist/templates/agent-plugin/agents/review-plan/CLAUDE.md +1 -1
- package/dist/templates/agent-plugin/agents/spec/requirements-writer.md +1 -1
- package/dist/templates/agent-plugin/agents/spec.md +19 -19
- package/dist/templates/agent-plugin/skills/humanloop/SKILL.md +9 -8
- package/dist/templates/agent-plugin/skills/perspective-fanout/SKILL.md +2 -2
- package/dist/templates/agent-plugin/skills/problem-plateau-breakers/SKILL.md +2 -2
- package/dist/templates/agent-suffix.md +3 -3
- package/dist/templates/dashboard-claude.md +13 -13
- package/dist/templates/orchestrator-base.md +13 -13
- package/dist/templates/orchestrator-completion.md +11 -11
- package/dist/templates/orchestrator-discovery.md +5 -5
- package/dist/templates/orchestrator-impl.md +8 -8
- package/dist/templates/orchestrator-planning.md +6 -6
- package/dist/templates/orchestrator-plugin/commands/sisyphus/scratch.md +1 -1
- package/dist/templates/orchestrator-plugin/commands/sisyphus/strategize.md +2 -2
- package/dist/templates/orchestrator-plugin/skills/humanloop/SKILL.md +11 -10
- package/dist/templates/orchestrator-plugin/skills/orchestration/CLAUDE.md +1 -1
- package/dist/templates/orchestrator-plugin/skills/orchestration/SKILL.md +1 -1
- package/dist/templates/orchestrator-plugin/skills/orchestration/strategy.md +4 -4
- package/dist/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +2 -2
- package/dist/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +1 -1
- package/dist/templates/orchestrator-validation.md +5 -5
- package/dist/templates/termrender-haiku-system.md +1 -1
- package/dist/tui.js +8 -8
- package/dist/tui.js.map +1 -1
- package/package.json +2 -1
- package/templates/agent-plugin/agents/explore.md +2 -2
- package/templates/agent-plugin/agents/implementor.md +2 -2
- package/templates/agent-plugin/agents/operator.md +3 -3
- package/templates/agent-plugin/agents/plan.md +2 -2
- package/templates/agent-plugin/agents/problem.md +8 -8
- package/templates/agent-plugin/agents/review-plan/CLAUDE.md +1 -1
- package/templates/agent-plugin/agents/spec/requirements-writer.md +1 -1
- package/templates/agent-plugin/agents/spec.md +19 -19
- package/templates/agent-plugin/skills/humanloop/SKILL.md +9 -8
- package/templates/agent-plugin/skills/perspective-fanout/SKILL.md +2 -2
- package/templates/agent-plugin/skills/problem-plateau-breakers/SKILL.md +2 -2
- package/templates/agent-suffix.md +3 -3
- package/templates/dashboard-claude.md +13 -13
- package/templates/orchestrator-base.md +13 -13
- package/templates/orchestrator-completion.md +11 -11
- package/templates/orchestrator-discovery.md +5 -5
- package/templates/orchestrator-impl.md +8 -8
- package/templates/orchestrator-planning.md +6 -6
- package/templates/orchestrator-plugin/commands/sisyphus/scratch.md +1 -1
- package/templates/orchestrator-plugin/commands/sisyphus/strategize.md +2 -2
- package/templates/orchestrator-plugin/skills/humanloop/SKILL.md +11 -10
- package/templates/orchestrator-plugin/skills/orchestration/CLAUDE.md +1 -1
- package/templates/orchestrator-plugin/skills/orchestration/SKILL.md +1 -1
- package/templates/orchestrator-plugin/skills/orchestration/strategy.md +4 -4
- package/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +2 -2
- package/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +1 -1
- package/templates/orchestrator-validation.md +5 -5
- package/templates/termrender-haiku-system.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -111,12 +111,24 @@ 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
|
}
|
|
117
120
|
function deployTailscaleEnvPath() {
|
|
118
121
|
return join(deployDir(), "tailscale.env");
|
|
119
122
|
}
|
|
123
|
+
function boxRepoPath(repo) {
|
|
124
|
+
return `~/projects/${repo}`;
|
|
125
|
+
}
|
|
126
|
+
function boxCloudSidecarPath(repo) {
|
|
127
|
+
return `~/.sisyphus/cloud/${repo}.json`;
|
|
128
|
+
}
|
|
129
|
+
function boxCloudSidecarDir() {
|
|
130
|
+
return `~/.sisyphus/cloud`;
|
|
131
|
+
}
|
|
120
132
|
var init_paths = __esm({
|
|
121
133
|
"src/shared/paths.ts"() {
|
|
122
134
|
"use strict";
|
|
@@ -135,16 +147,16 @@ function atomicWrite(filePath, data) {
|
|
|
135
147
|
}
|
|
136
148
|
async function withLock(key, fn) {
|
|
137
149
|
const prev = locks.get(key) ?? Promise.resolve();
|
|
138
|
-
let
|
|
150
|
+
let resolve12;
|
|
139
151
|
const next = new Promise((r) => {
|
|
140
|
-
|
|
152
|
+
resolve12 = r;
|
|
141
153
|
});
|
|
142
154
|
locks.set(key, next);
|
|
143
155
|
await prev;
|
|
144
156
|
try {
|
|
145
157
|
return fn();
|
|
146
158
|
} finally {
|
|
147
|
-
|
|
159
|
+
resolve12();
|
|
148
160
|
if (locks.get(key) === next) {
|
|
149
161
|
locks.delete(key);
|
|
150
162
|
}
|
|
@@ -161,7 +173,9 @@ var init_atomic = __esm({
|
|
|
161
173
|
// src/cli/deploy/creds.ts
|
|
162
174
|
var creds_exports = {};
|
|
163
175
|
__export(creds_exports, {
|
|
176
|
+
PROVIDERS: () => PROVIDERS,
|
|
164
177
|
ensureDeployDir: () => ensureDeployDir,
|
|
178
|
+
isValidProvider: () => isValidProvider,
|
|
165
179
|
loadProviderCreds: () => loadProviderCreds,
|
|
166
180
|
maskValue: () => maskValue,
|
|
167
181
|
promptLine: () => promptLine,
|
|
@@ -169,7 +183,10 @@ __export(creds_exports, {
|
|
|
169
183
|
writeTailscaleEnv: () => writeTailscaleEnv
|
|
170
184
|
});
|
|
171
185
|
import { chmodSync as chmodSync3, existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync27 } from "fs";
|
|
172
|
-
import { createInterface as
|
|
186
|
+
import { createInterface as createInterface4 } from "readline";
|
|
187
|
+
function isValidProvider(value) {
|
|
188
|
+
return PROVIDERS.includes(value);
|
|
189
|
+
}
|
|
173
190
|
function ensureDeployDir() {
|
|
174
191
|
const dir = deployDir();
|
|
175
192
|
if (!existsSync24(dir)) mkdirSync12(dir, { recursive: true, mode: 448 });
|
|
@@ -191,7 +208,7 @@ function parseEnvFile(text) {
|
|
|
191
208
|
return out;
|
|
192
209
|
}
|
|
193
210
|
function serializeEnvFile(values) {
|
|
194
|
-
const lines = ["# Managed by `
|
|
211
|
+
const lines = ["# Managed by `sis deploy`. Do not edit unless you know what you are doing."];
|
|
195
212
|
for (const [k, v] of Object.entries(values)) {
|
|
196
213
|
lines.push(`${k}=${v}`);
|
|
197
214
|
}
|
|
@@ -207,7 +224,7 @@ function writeEnvFile(path, values) {
|
|
|
207
224
|
chmodSync3(path, 384);
|
|
208
225
|
}
|
|
209
226
|
async function promptLine(question, hidden) {
|
|
210
|
-
const rl =
|
|
227
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout, terminal: true });
|
|
211
228
|
if (hidden) {
|
|
212
229
|
const stdout = process.stdout;
|
|
213
230
|
const originalWrite = stdout.write.bind(stdout);
|
|
@@ -219,7 +236,7 @@ async function promptLine(question, hidden) {
|
|
|
219
236
|
}
|
|
220
237
|
};
|
|
221
238
|
}
|
|
222
|
-
const answer = await new Promise((
|
|
239
|
+
const answer = await new Promise((resolve12) => rl.question(question, resolve12));
|
|
223
240
|
rl.close();
|
|
224
241
|
if (hidden) process.stdout.write("\n");
|
|
225
242
|
return answer.trim();
|
|
@@ -269,12 +286,13 @@ function writeTailscaleEnv(env) {
|
|
|
269
286
|
if (env.tag) values.TS_TAG = env.tag;
|
|
270
287
|
writeEnvFile(deployTailscaleEnvPath(), values);
|
|
271
288
|
}
|
|
272
|
-
var SPECS;
|
|
289
|
+
var PROVIDERS, SPECS;
|
|
273
290
|
var init_creds = __esm({
|
|
274
291
|
"src/cli/deploy/creds.ts"() {
|
|
275
292
|
"use strict";
|
|
276
293
|
init_atomic();
|
|
277
294
|
init_paths();
|
|
295
|
+
PROVIDERS = ["hetzner", "aws"];
|
|
278
296
|
SPECS = {
|
|
279
297
|
hetzner: {
|
|
280
298
|
provider: "hetzner",
|
|
@@ -293,8 +311,8 @@ var init_creds = __esm({
|
|
|
293
311
|
|
|
294
312
|
// src/cli/index.ts
|
|
295
313
|
import { Command } from "commander";
|
|
296
|
-
import { existsSync as
|
|
297
|
-
import { dirname as dirname12, join as
|
|
314
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync14, readFileSync as readFileSync31 } from "fs";
|
|
315
|
+
import { dirname as dirname12, join as join27 } from "path";
|
|
298
316
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
299
317
|
|
|
300
318
|
// src/cli/commands/start.ts
|
|
@@ -306,13 +324,13 @@ init_paths();
|
|
|
306
324
|
import { connect } from "net";
|
|
307
325
|
function rawSend(request, timeoutMs = 1e4) {
|
|
308
326
|
const sock = socketPath();
|
|
309
|
-
return new Promise((
|
|
327
|
+
return new Promise((resolve12, reject) => {
|
|
310
328
|
const socket = connect(sock);
|
|
311
329
|
let data = "";
|
|
312
330
|
const timeout = setTimeout(() => {
|
|
313
331
|
socket.destroy();
|
|
314
332
|
reject(new Error(`Request timed out after ${(timeoutMs / 1e3).toFixed(0)}s. The daemon may be overloaded.
|
|
315
|
-
Check:
|
|
333
|
+
Check: sis admin doctor
|
|
316
334
|
Logs: tail -20 ~/.sisyphus/daemon.log`));
|
|
317
335
|
}, timeoutMs);
|
|
318
336
|
socket.on("connect", () => {
|
|
@@ -326,7 +344,7 @@ function rawSend(request, timeoutMs = 1e4) {
|
|
|
326
344
|
const line = data.slice(0, newlineIdx);
|
|
327
345
|
socket.destroy();
|
|
328
346
|
try {
|
|
329
|
-
|
|
347
|
+
resolve12(JSON.parse(line));
|
|
330
348
|
} catch {
|
|
331
349
|
reject(new Error(`Invalid JSON response from daemon: ${line}`));
|
|
332
350
|
}
|
|
@@ -354,6 +372,7 @@ import { execSync } from "child_process";
|
|
|
354
372
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, unlinkSync } from "fs";
|
|
355
373
|
import { homedir as homedir2 } from "os";
|
|
356
374
|
import { join as join2 } from "path";
|
|
375
|
+
import { createInterface } from "readline";
|
|
357
376
|
|
|
358
377
|
// src/shared/keymap.ts
|
|
359
378
|
function formatHelpForKeymap(km) {
|
|
@@ -590,7 +609,7 @@ while IFS=$'\\t' read -r type name scwd phase sid; do
|
|
|
590
609
|
[ "$sid" = "$current_id" ] && { cwd="$scwd"; break; }
|
|
591
610
|
done < "$MANIFEST"
|
|
592
611
|
if [ -z "$cwd" ]; then
|
|
593
|
-
tmux display-message "sisyphus: '$current_name' has no @sisyphus_cwd \u2014 run '
|
|
612
|
+
tmux display-message "sisyphus: '$current_name' has no @sisyphus_cwd \u2014 run 'sis start' here to register"
|
|
594
613
|
exit 0
|
|
595
614
|
fi
|
|
596
615
|
session_ids=()
|
|
@@ -695,7 +714,7 @@ tmpfile=$(mktemp /tmp/sisyphus-new-XXXXXX.md)
|
|
|
695
714
|
trap 'rm -f "$tmpfile"' EXIT
|
|
696
715
|
nvim "$tmpfile"
|
|
697
716
|
grep -q '[^[:space:]]' "$tmpfile" || exit 0
|
|
698
|
-
exec
|
|
717
|
+
exec sis start "$(cat "$tmpfile")"
|
|
699
718
|
`;
|
|
700
719
|
var MESSAGE_SCRIPT = `#!/bin/bash
|
|
701
720
|
# Open nvim to compose a message for the current session's orchestrator
|
|
@@ -726,7 +745,7 @@ tmpfile=$(mktemp /tmp/sisyphus-msg-XXXXXX.md)
|
|
|
726
745
|
trap 'rm -f "$tmpfile"' EXIT
|
|
727
746
|
nvim "$tmpfile"
|
|
728
747
|
grep -q '[^[:space:]]' "$tmpfile" || exit 0
|
|
729
|
-
exec
|
|
748
|
+
exec sis message --session "$session_id" "$(cat "$tmpfile")"
|
|
730
749
|
`;
|
|
731
750
|
var SESSION_RESOLVE = `
|
|
732
751
|
tmux_sid=$(tmux display-message -p '#{session_id}')
|
|
@@ -762,7 +781,7 @@ var KILL_SESSION_SCRIPT = `#!/bin/bash
|
|
|
762
781
|
# Kill the sisyphus session associated with the current tmux session
|
|
763
782
|
${SESSION_RESOLVE}
|
|
764
783
|
|
|
765
|
-
|
|
784
|
+
sis session kill "$session_id" >/dev/null 2>&1
|
|
766
785
|
${GO_HOME_AFTER}
|
|
767
786
|
`;
|
|
768
787
|
var DELETE_SESSION_SCRIPT = `#!/bin/bash
|
|
@@ -772,7 +791,7 @@ ${SESSION_RESOLVE}
|
|
|
772
791
|
printf "\\033[31mType 'yes' to confirm:\\033[0m "
|
|
773
792
|
read -r answer
|
|
774
793
|
[ "$answer" = "yes" ] || exit 0
|
|
775
|
-
|
|
794
|
+
sis session delete "$session_id" --cwd "$cwd" >/dev/null 2>&1
|
|
776
795
|
${GO_HOME_AFTER}
|
|
777
796
|
`;
|
|
778
797
|
var HELP_SCRIPT = `#!/bin/bash
|
|
@@ -806,9 +825,9 @@ if [ -z "$session_id" ]; then
|
|
|
806
825
|
fi
|
|
807
826
|
|
|
808
827
|
if [ -n "$session_id" ]; then
|
|
809
|
-
|
|
828
|
+
sis status "$session_id" 2>&1 | less -R
|
|
810
829
|
else
|
|
811
|
-
|
|
830
|
+
sis list 2>&1 | less -R
|
|
812
831
|
fi
|
|
813
832
|
`;
|
|
814
833
|
var PICK_SESSION_SCRIPT = `#!/bin/bash
|
|
@@ -867,7 +886,7 @@ short_id="\${session_id:0:8}"
|
|
|
867
886
|
printf "\\033[33mContinue session %s...?\\033[0m (y/n) " "$short_id"
|
|
868
887
|
read -r answer
|
|
869
888
|
[ "$answer" = "y" ] || [ "$answer" = "yes" ] || exit 0
|
|
870
|
-
|
|
889
|
+
sis session continue --session "$session_id"
|
|
871
890
|
sleep 1
|
|
872
891
|
`;
|
|
873
892
|
var OPEN_ROADMAP_SCRIPT = `#!/bin/bash
|
|
@@ -891,18 +910,18 @@ var EXPORT_SESSION_SCRIPT = `#!/bin/bash
|
|
|
891
910
|
${SESSION_RESOLVE}
|
|
892
911
|
|
|
893
912
|
echo "Exporting session \${session_id:0:8}..."
|
|
894
|
-
|
|
913
|
+
sis admin export "$session_id" --cwd "$cwd"
|
|
895
914
|
echo ""
|
|
896
915
|
read -n 1 -s -r -p "Press a key to close."
|
|
897
916
|
`;
|
|
898
917
|
var RESTART_AGENT_SCRIPT = `#!/bin/bash
|
|
899
918
|
# Pick a sisyphus agent and restart it (fzf picker with confirm for running agents).
|
|
900
|
-
# Assumes macOS (fzf optional). Requires \`
|
|
919
|
+
# Assumes macOS (fzf optional). Requires \`sis status --json\`.
|
|
901
920
|
${SESSION_RESOLVE}
|
|
902
921
|
|
|
903
922
|
command -v jq &>/dev/null || { echo "jq required"; sleep 1; exit 1; }
|
|
904
923
|
|
|
905
|
-
agents_json=$(
|
|
924
|
+
agents_json=$(sis status "$session_id" --json 2>/dev/null)
|
|
906
925
|
if [ -z "$agents_json" ]; then
|
|
907
926
|
echo "Failed to read session status"; sleep 1; exit 1
|
|
908
927
|
fi
|
|
@@ -939,7 +958,7 @@ if [ "\${statuses[$idx]}" = "running" ]; then
|
|
|
939
958
|
[ "$answer" = "yes" ] || exit 0
|
|
940
959
|
fi
|
|
941
960
|
|
|
942
|
-
|
|
961
|
+
sis agent restart "\${ids[$idx]}" --session "$session_id"
|
|
943
962
|
echo ""
|
|
944
963
|
read -n 1 -s -r -p "Press a key to close."
|
|
945
964
|
`;
|
|
@@ -989,9 +1008,9 @@ nvim "$tmpfile"
|
|
|
989
1008
|
body=$(grep -v '^[[:space:]]*#' "$tmpfile" | sed '/^[[:space:]]*$/d')
|
|
990
1009
|
|
|
991
1010
|
if [ -z "$body" ]; then
|
|
992
|
-
exec
|
|
1011
|
+
exec sis session resume "$session_id"
|
|
993
1012
|
else
|
|
994
|
-
exec
|
|
1013
|
+
exec sis session resume "$session_id" "$body"
|
|
995
1014
|
fi
|
|
996
1015
|
`;
|
|
997
1016
|
var ROLLBACK_SESSION_SCRIPT = `#!/bin/bash
|
|
@@ -1018,7 +1037,7 @@ if [ "$cycle_input" -lt 1 ]; then
|
|
|
1018
1037
|
exit 0
|
|
1019
1038
|
fi
|
|
1020
1039
|
|
|
1021
|
-
|
|
1040
|
+
sis session rollback "$session_id" "$cycle_input"
|
|
1022
1041
|
echo ""
|
|
1023
1042
|
echo "Rolled back to cycle $cycle_input \u2014 use [C-s S r] to resume."
|
|
1024
1043
|
read -n 1 -s -r -p "Press a key to close."
|
|
@@ -1055,12 +1074,12 @@ fi
|
|
|
1055
1074
|
|
|
1056
1075
|
# Fallback: orchestrator window is gone. Open last claude session in a popup.
|
|
1057
1076
|
state="$cwd/.sisyphus/sessions/$session_id/state.json"
|
|
1058
|
-
[ ! -f "$state" ] && { tmux display-message "Window dead and no state.json \u2014 try
|
|
1077
|
+
[ ! -f "$state" ] && { tmux display-message "Window dead and no state.json \u2014 try sis session resume"; exit 0; }
|
|
1059
1078
|
|
|
1060
1079
|
claude_sid=$(jq -r '[.orchestratorCycles[].claudeSessionId] | last // empty' "$state")
|
|
1061
1080
|
|
|
1062
1081
|
if [ -z "$claude_sid" ]; then
|
|
1063
|
-
tmux display-message "No orchestrator claude session id found \u2014 try
|
|
1082
|
+
tmux display-message "No orchestrator claude session id found \u2014 try sis session resume"
|
|
1064
1083
|
exit 0
|
|
1065
1084
|
fi
|
|
1066
1085
|
|
|
@@ -1082,7 +1101,7 @@ nvim "$tmpfile"
|
|
|
1082
1101
|
body=$(grep -v '^[[:space:]]*#' "$tmpfile" | sed '/^[[:space:]]*$/d')
|
|
1083
1102
|
[ -z "$body" ] && exit 0
|
|
1084
1103
|
|
|
1085
|
-
exec
|
|
1104
|
+
exec sis agent spawn --session "$session_id" --name "agent" --instruction "$body"
|
|
1086
1105
|
`;
|
|
1087
1106
|
var SEARCH_REPORTS_SCRIPT = `#!/bin/bash
|
|
1088
1107
|
# fzf over reports/*.md across all sessions for the current cwd.
|
|
@@ -1121,12 +1140,12 @@ fi
|
|
|
1121
1140
|
`;
|
|
1122
1141
|
var JUMP_TO_PANE_SCRIPT = `#!/bin/bash
|
|
1123
1142
|
# Pick a sisyphus agent and jump to its tmux pane.
|
|
1124
|
-
# Assumes macOS (pbcopy, fzf optional). Requires \`
|
|
1143
|
+
# Assumes macOS (pbcopy, fzf optional). Requires \`sis status --json\`.
|
|
1125
1144
|
${SESSION_RESOLVE}
|
|
1126
1145
|
|
|
1127
1146
|
command -v jq &>/dev/null || { echo "jq required"; sleep 1; exit 1; }
|
|
1128
1147
|
|
|
1129
|
-
agents_json=$(
|
|
1148
|
+
agents_json=$(sis status "$session_id" --json 2>/dev/null)
|
|
1130
1149
|
if [ -z "$agents_json" ]; then
|
|
1131
1150
|
echo "Failed to read session status"; sleep 1; exit 1
|
|
1132
1151
|
fi
|
|
@@ -1167,12 +1186,12 @@ tmux select-pane -t "$target_pane"
|
|
|
1167
1186
|
`;
|
|
1168
1187
|
var MSG_AGENT_SCRIPT = `#!/bin/bash
|
|
1169
1188
|
# Pick a sisyphus agent and send it a message via nvim.
|
|
1170
|
-
# Assumes macOS (fzf optional). Requires \`
|
|
1189
|
+
# Assumes macOS (fzf optional). Requires \`sis status --json\` and \`--agent\` on message.
|
|
1171
1190
|
${SESSION_RESOLVE}
|
|
1172
1191
|
|
|
1173
1192
|
command -v jq &>/dev/null || { echo "jq required"; sleep 1; exit 1; }
|
|
1174
1193
|
|
|
1175
|
-
agents_json=$(
|
|
1194
|
+
agents_json=$(sis status "$session_id" --json 2>/dev/null)
|
|
1176
1195
|
if [ -z "$agents_json" ]; then
|
|
1177
1196
|
echo "Failed to read session status"; sleep 1; exit 1
|
|
1178
1197
|
fi
|
|
@@ -1205,16 +1224,16 @@ tmpfile=$(mktemp /tmp/sisyphus-msg-agent-XXXX.md)
|
|
|
1205
1224
|
trap 'rm -f "$tmpfile"' EXIT
|
|
1206
1225
|
nvim "$tmpfile"
|
|
1207
1226
|
grep -q '[^[:space:]]' "$tmpfile" || exit 0
|
|
1208
|
-
exec
|
|
1227
|
+
exec sis message --session "$session_id" --agent "\${ids[$idx]}" "$(cat "$tmpfile")"
|
|
1209
1228
|
`;
|
|
1210
1229
|
var RERUN_AGENT_SCRIPT = `#!/bin/bash
|
|
1211
1230
|
# Pick a sisyphus agent and spawn a retry with its original instruction.
|
|
1212
|
-
# Assumes macOS (fzf optional). Requires \`
|
|
1231
|
+
# Assumes macOS (fzf optional). Requires \`sis status --json\`.
|
|
1213
1232
|
${SESSION_RESOLVE}
|
|
1214
1233
|
|
|
1215
1234
|
command -v jq &>/dev/null || { echo "jq required"; sleep 1; exit 1; }
|
|
1216
1235
|
|
|
1217
|
-
agents_json=$(
|
|
1236
|
+
agents_json=$(sis status "$session_id" --json 2>/dev/null)
|
|
1218
1237
|
if [ -z "$agents_json" ]; then
|
|
1219
1238
|
echo "Failed to read session status"; sleep 1; exit 1
|
|
1220
1239
|
fi
|
|
@@ -1256,11 +1275,11 @@ if [ "\${#instr}" -lt 20 ]; then
|
|
|
1256
1275
|
exit 1
|
|
1257
1276
|
fi
|
|
1258
1277
|
|
|
1259
|
-
exec
|
|
1278
|
+
exec sis agent spawn --session "$session_id" --agent-type "\${atypes[$idx]}" --name "\${anames[$idx]}-retry-$(date +%s)" --instruction "$instr"
|
|
1260
1279
|
`;
|
|
1261
1280
|
var OPEN_CLAUDE_AGENT_SCRIPT = `#!/bin/bash
|
|
1262
1281
|
# Pick a sisyphus agent or orchestrator cycle and resume its Claude session.
|
|
1263
|
-
# Assumes macOS (fzf optional). Requires \`
|
|
1282
|
+
# Assumes macOS (fzf optional). Requires \`sis status --json\`.
|
|
1264
1283
|
${SESSION_RESOLVE}
|
|
1265
1284
|
|
|
1266
1285
|
command -v jq &>/dev/null || { echo "jq required"; sleep 1; exit 1; }
|
|
@@ -1268,7 +1287,7 @@ command -v jq &>/dev/null || { echo "jq required"; sleep 1; exit 1; }
|
|
|
1268
1287
|
state="$cwd/.sisyphus/sessions/$session_id/state.json"
|
|
1269
1288
|
[ ! -f "$state" ] && { echo "No state.json for this session"; sleep 1; exit 1; }
|
|
1270
1289
|
|
|
1271
|
-
agents_json=$(
|
|
1290
|
+
agents_json=$(sis status "$session_id" --json 2>/dev/null)
|
|
1272
1291
|
if [ -z "$agents_json" ]; then
|
|
1273
1292
|
echo "Failed to read session status"; sleep 1; exit 1
|
|
1274
1293
|
fi
|
|
@@ -1308,12 +1327,12 @@ cd "$cwd" && exec claude --resume "$cid"
|
|
|
1308
1327
|
var TAIL_AGENT_LOGS_SCRIPT = `#!/bin/bash
|
|
1309
1328
|
# Pick a sisyphus agent and view its tmux pane scrollback (last 2000 lines) in less.
|
|
1310
1329
|
# Uses tmux capture-pane \u2014 no tail -f, no pipe-pane side effects.
|
|
1311
|
-
# Assumes macOS (fzf optional). Requires \`
|
|
1330
|
+
# Assumes macOS (fzf optional). Requires \`sis status --json\`.
|
|
1312
1331
|
${SESSION_RESOLVE}
|
|
1313
1332
|
|
|
1314
1333
|
command -v jq &>/dev/null || { echo "jq required"; sleep 1; exit 1; }
|
|
1315
1334
|
|
|
1316
|
-
agents_json=$(
|
|
1335
|
+
agents_json=$(sis status "$session_id" --json 2>/dev/null)
|
|
1317
1336
|
if [ -z "$agents_json" ]; then
|
|
1318
1337
|
echo "Failed to read session status"; sleep 1; exit 1
|
|
1319
1338
|
fi
|
|
@@ -1350,12 +1369,12 @@ tmux capture-pane -t "$target_pane" -p -S -2000 | less +G
|
|
|
1350
1369
|
`;
|
|
1351
1370
|
var KILL_AGENT_SCRIPT = `#!/bin/bash
|
|
1352
1371
|
# Pick a sisyphus agent and kill it (with red confirmation prompt).
|
|
1353
|
-
# Assumes macOS (fzf optional). Requires \`
|
|
1372
|
+
# Assumes macOS (fzf optional). Requires \`sis status --json\` and \`sis agent kill\`.
|
|
1354
1373
|
${SESSION_RESOLVE}
|
|
1355
1374
|
|
|
1356
1375
|
command -v jq &>/dev/null || { echo "jq required"; sleep 1; exit 1; }
|
|
1357
1376
|
|
|
1358
|
-
agents_json=$(
|
|
1377
|
+
agents_json=$(sis status "$session_id" --json 2>/dev/null)
|
|
1359
1378
|
if [ -z "$agents_json" ]; then
|
|
1360
1379
|
echo "Failed to read session status"; sleep 1; exit 1
|
|
1361
1380
|
fi
|
|
@@ -1387,18 +1406,18 @@ fi
|
|
|
1387
1406
|
printf '\\033[31mKill %s? (yes/no): \\033[0m' "\${ids[$idx]}"
|
|
1388
1407
|
read -r answer
|
|
1389
1408
|
[ "$answer" = "yes" ] || exit 0
|
|
1390
|
-
|
|
1409
|
+
sis agent kill "\${ids[$idx]}" --session "$session_id"
|
|
1391
1410
|
echo ""
|
|
1392
1411
|
read -n 1 -s -r -p "Press a key to close."
|
|
1393
1412
|
`;
|
|
1394
1413
|
var COPY_AGENT_ID_SCRIPT = `#!/bin/bash
|
|
1395
1414
|
# Pick a sisyphus agent and copy its ID to clipboard.
|
|
1396
|
-
# Assumes macOS (pbcopy, fzf optional). Requires \`
|
|
1415
|
+
# Assumes macOS (pbcopy, fzf optional). Requires \`sis status --json\`.
|
|
1397
1416
|
${SESSION_RESOLVE}
|
|
1398
1417
|
|
|
1399
1418
|
command -v jq &>/dev/null || { echo "jq required"; sleep 1; exit 1; }
|
|
1400
1419
|
|
|
1401
|
-
agents_json=$(
|
|
1420
|
+
agents_json=$(sis status "$session_id" --json 2>/dev/null)
|
|
1402
1421
|
if [ -z "$agents_json" ]; then
|
|
1403
1422
|
echo "Failed to read session status"; sleep 1; exit 1
|
|
1404
1423
|
fi
|
|
@@ -1473,10 +1492,10 @@ tmux display-message "Copied session ID"
|
|
|
1473
1492
|
`;
|
|
1474
1493
|
var COPY_CONTEXT_SCRIPT = `#!/bin/bash
|
|
1475
1494
|
# Copy the session context XML to clipboard.
|
|
1476
|
-
# Assumes macOS (pbcopy). Requires \`
|
|
1495
|
+
# Assumes macOS (pbcopy). Requires \`sis session context\`.
|
|
1477
1496
|
${SESSION_RESOLVE}
|
|
1478
1497
|
|
|
1479
|
-
|
|
1498
|
+
sis session context "$session_id" --cwd "$cwd" | pbcopy
|
|
1480
1499
|
tmux display-message "Copied session context (XML)"
|
|
1481
1500
|
`;
|
|
1482
1501
|
var EDIT_CONTEXT_FILE_SCRIPT = `#!/bin/bash
|
|
@@ -1537,7 +1556,7 @@ if [ "\${#instruction}" -lt 20 ]; then
|
|
|
1537
1556
|
fi
|
|
1538
1557
|
|
|
1539
1558
|
name="explore-$(date +%s)"
|
|
1540
|
-
|
|
1559
|
+
sis agent spawn \\
|
|
1541
1560
|
--agent-type sisyphus:explore \\
|
|
1542
1561
|
--name "$name" \\
|
|
1543
1562
|
--session "$session_id" \\
|
|
@@ -1571,7 +1590,7 @@ if [ "\${#instruction}" -lt 20 ]; then
|
|
|
1571
1590
|
fi
|
|
1572
1591
|
|
|
1573
1592
|
name="debug-$(date +%s)"
|
|
1574
|
-
|
|
1593
|
+
sis agent spawn \\
|
|
1575
1594
|
--agent-type sisyphus:debug \\
|
|
1576
1595
|
--name "$name" \\
|
|
1577
1596
|
--session "$session_id" \\
|
|
@@ -1619,7 +1638,7 @@ args=()
|
|
|
1619
1638
|
[ -n "$clone_name" ] && args+=(--name "$clone_name")
|
|
1620
1639
|
[ "$copy_strategy" = "y" ] || [ "$copy_strategy" = "Y" ] && args+=(--strategy)
|
|
1621
1640
|
|
|
1622
|
-
|
|
1641
|
+
sis session clone "\${args[@]}" "$goal"
|
|
1623
1642
|
exit_code=$?
|
|
1624
1643
|
read -n 1 -s -r -p "Press a key to close."
|
|
1625
1644
|
exit $exit_code
|
|
@@ -1627,12 +1646,12 @@ exit $exit_code
|
|
|
1627
1646
|
var HISTORY_SCRIPT = `#!/bin/bash
|
|
1628
1647
|
# Show rich session detail (history command's per-session view) in a popup.
|
|
1629
1648
|
${SESSION_RESOLVE}
|
|
1630
|
-
|
|
1649
|
+
sis admin history "$session_id" 2>&1 | less -R
|
|
1631
1650
|
`;
|
|
1632
1651
|
var RECONNECT_SCRIPT = `#!/bin/bash
|
|
1633
1652
|
# Reconnect daemon to an orphaned tmux session for the current cwd.
|
|
1634
1653
|
${SESSION_RESOLVE}
|
|
1635
|
-
|
|
1654
|
+
sis session reconnect "$session_id"
|
|
1636
1655
|
exit_code=$?
|
|
1637
1656
|
read -n 1 -s -r -p "Press a key to close."
|
|
1638
1657
|
exit $exit_code
|
|
@@ -1640,7 +1659,7 @@ exit $exit_code
|
|
|
1640
1659
|
var OPEN_SCRATCH_SCRIPT = `#!/bin/bash
|
|
1641
1660
|
# Open a standalone Claude scratch window in the home tmux session for this cwd.
|
|
1642
1661
|
# scratch resolves the home session itself via @sisyphus_cwd; no session_id needed.
|
|
1643
|
-
exec
|
|
1662
|
+
exec sis admin scratch
|
|
1644
1663
|
`;
|
|
1645
1664
|
function installScript(name, content) {
|
|
1646
1665
|
mkdirSync(join2(globalDir(), "bin"), { recursive: true });
|
|
@@ -1717,7 +1736,22 @@ function getExistingBinding(key, table = "root") {
|
|
|
1717
1736
|
function isSisyphusBinding(binding) {
|
|
1718
1737
|
return binding.includes("sisyphus");
|
|
1719
1738
|
}
|
|
1720
|
-
function
|
|
1739
|
+
async function confirmConfAppend(userConf, line) {
|
|
1740
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
1741
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1742
|
+
return new Promise((resolve12) => {
|
|
1743
|
+
const question = `
|
|
1744
|
+
Sisyphus needs to append one line to ${userConf} so its tmux keybindings persist:
|
|
1745
|
+
${line}
|
|
1746
|
+
|
|
1747
|
+
Append it now? (y/N) `;
|
|
1748
|
+
rl.question(question, (answer) => {
|
|
1749
|
+
rl.close();
|
|
1750
|
+
resolve12(answer.trim().toLowerCase() === "y");
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
async function setupTmuxKeybind(cycleKey = DEFAULT_CYCLE_KEY, prefixKey = DEFAULT_PREFIX_KEY, opts = {}) {
|
|
1721
1755
|
installAllScripts();
|
|
1722
1756
|
if (!tmuxVersionAtLeast(3, 2)) {
|
|
1723
1757
|
let version = "unknown";
|
|
@@ -1735,7 +1769,7 @@ function setupTmuxKeybind(cycleKey = DEFAULT_CYCLE_KEY, prefixKey = DEFAULT_PREF
|
|
|
1735
1769
|
if (existing !== null && !isSisyphusBinding(existing)) {
|
|
1736
1770
|
return {
|
|
1737
1771
|
status: "conflict",
|
|
1738
|
-
message: `Tmux key ${key} (${label}) is already bound to something else. Run "
|
|
1772
|
+
message: `Tmux key ${key} (${label}) is already bound to something else. Run "sis admin setup-keybind <key>" to use a different key.`,
|
|
1739
1773
|
existingBinding: existing
|
|
1740
1774
|
};
|
|
1741
1775
|
}
|
|
@@ -1757,14 +1791,22 @@ ${bindings.join("\n")}
|
|
|
1757
1791
|
const userConf = userTmuxConfPath();
|
|
1758
1792
|
const markedSourceLine = `source-file ${confPath} ${SISYPHUS_CONF_MARKER}`;
|
|
1759
1793
|
let persistedToConf = false;
|
|
1794
|
+
let appendDeclined = false;
|
|
1760
1795
|
if (userConf !== null) {
|
|
1761
1796
|
const contents = readFileSync(userConf, "utf8");
|
|
1762
|
-
if (
|
|
1763
|
-
|
|
1764
|
-
|
|
1797
|
+
if (contents.includes(confPath)) {
|
|
1798
|
+
persistedToConf = true;
|
|
1799
|
+
} else {
|
|
1800
|
+
const shouldAppend = opts.assumeYes ? true : await confirmConfAppend(userConf, markedSourceLine);
|
|
1801
|
+
if (shouldAppend) {
|
|
1802
|
+
const separator = contents.endsWith("\n") ? "" : "\n";
|
|
1803
|
+
writeFileSync(userConf, `${contents}${separator}${markedSourceLine}
|
|
1765
1804
|
`, "utf8");
|
|
1805
|
+
persistedToConf = true;
|
|
1806
|
+
} else {
|
|
1807
|
+
appendDeclined = true;
|
|
1808
|
+
}
|
|
1766
1809
|
}
|
|
1767
|
-
persistedToConf = true;
|
|
1768
1810
|
}
|
|
1769
1811
|
try {
|
|
1770
1812
|
for (const b of bindings) {
|
|
@@ -1772,6 +1814,16 @@ ${bindings.join("\n")}
|
|
|
1772
1814
|
}
|
|
1773
1815
|
} catch {
|
|
1774
1816
|
}
|
|
1817
|
+
if (appendDeclined && userConf !== null) {
|
|
1818
|
+
return {
|
|
1819
|
+
status: "conf-modification-declined",
|
|
1820
|
+
message: `Tmux keybindings applied to the live session, but not persisted.
|
|
1821
|
+
To persist them, add this line to ${userConf}:
|
|
1822
|
+
${markedSourceLine}`,
|
|
1823
|
+
manualLine: markedSourceLine,
|
|
1824
|
+
userConf
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1775
1827
|
if (getExistingBinding(cycleKey) !== null && isSisyphusBinding(getExistingBinding(cycleKey))) {
|
|
1776
1828
|
return {
|
|
1777
1829
|
status: "already-installed",
|
|
@@ -2061,7 +2113,7 @@ async function ensureDaemonInstalled() {
|
|
|
2061
2113
|
const plist = generatePlist(nodePath, daemonPath, logPath);
|
|
2062
2114
|
writeFileSync2(plistPath(), plist, "utf8");
|
|
2063
2115
|
execSync3(`launchctl load -w ${plistPath()}`);
|
|
2064
|
-
const keybindResult = setupTmuxKeybind();
|
|
2116
|
+
const keybindResult = await setupTmuxKeybind();
|
|
2065
2117
|
await ensureRequiredPlugins(process.cwd());
|
|
2066
2118
|
printGettingStarted(keybindResult, sisyphusPlugin);
|
|
2067
2119
|
}
|
|
@@ -2102,24 +2154,26 @@ function printGettingStarted(keybindResult, sisyphusPlugin) {
|
|
|
2102
2154
|
lines.push(`Tmux keybind: ${keybindResult.message}`, "");
|
|
2103
2155
|
} else if (keybindResult.status === "conflict") {
|
|
2104
2156
|
lines.push(`Keybind: ${keybindResult.message}`, "");
|
|
2157
|
+
} else if (keybindResult.status === "conf-modification-declined") {
|
|
2158
|
+
lines.push(keybindResult.message, "");
|
|
2105
2159
|
}
|
|
2106
2160
|
if (sisyphusPlugin.installed && sisyphusPlugin.autoInstalled) {
|
|
2107
2161
|
lines.push(`Sisyphus plugin installed: sisyphus@sisyphus \u2192 ${sisyphusPlugin.installPath}`, "");
|
|
2108
2162
|
} else if (!sisyphusPlugin.installed) {
|
|
2109
|
-
lines.push("Sisyphus plugin: failed to install (run `
|
|
2163
|
+
lines.push("Sisyphus plugin: failed to install (run `sis admin setup` to retry; needs `claude` CLI)", "");
|
|
2110
2164
|
}
|
|
2111
2165
|
lines.push(
|
|
2112
|
-
"Run `
|
|
2166
|
+
"Run `sis admin getting-started` for a complete usage guide.",
|
|
2113
2167
|
""
|
|
2114
2168
|
);
|
|
2115
2169
|
console.log(lines.join("\n"));
|
|
2116
2170
|
}
|
|
2117
2171
|
function testConnection() {
|
|
2118
|
-
return new Promise((
|
|
2172
|
+
return new Promise((resolve12, reject) => {
|
|
2119
2173
|
const sock = connect2(socketPath());
|
|
2120
2174
|
sock.on("connect", () => {
|
|
2121
2175
|
sock.destroy();
|
|
2122
|
-
|
|
2176
|
+
resolve12();
|
|
2123
2177
|
});
|
|
2124
2178
|
sock.on("error", (err) => {
|
|
2125
2179
|
sock.destroy();
|
|
@@ -2128,7 +2182,7 @@ function testConnection() {
|
|
|
2128
2182
|
});
|
|
2129
2183
|
}
|
|
2130
2184
|
function sleep(ms) {
|
|
2131
|
-
return new Promise((
|
|
2185
|
+
return new Promise((resolve12) => setTimeout(resolve12, ms));
|
|
2132
2186
|
}
|
|
2133
2187
|
async function waitForDaemon(maxWaitMs = 6e3) {
|
|
2134
2188
|
const start = Date.now();
|
|
@@ -2164,7 +2218,7 @@ function rawSend2(request, timeoutMs = 1e4) {
|
|
|
2164
2218
|
return rawSend(request, timeoutMs);
|
|
2165
2219
|
}
|
|
2166
2220
|
async function sendRequest(request, timeoutMs) {
|
|
2167
|
-
const sleep2 = (ms) => new Promise((
|
|
2221
|
+
const sleep2 = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms));
|
|
2168
2222
|
const MAX_ATTEMPTS = 5;
|
|
2169
2223
|
const RETRY_DELAY_MS = 2e3;
|
|
2170
2224
|
let installedDaemon = false;
|
|
@@ -2218,7 +2272,7 @@ async function sendRequest(request, timeoutMs) {
|
|
|
2218
2272
|
}
|
|
2219
2273
|
lines.push(
|
|
2220
2274
|
"",
|
|
2221
|
-
" Diagnose:
|
|
2275
|
+
" Diagnose: sis admin doctor",
|
|
2222
2276
|
" Logs: tail -f ~/.sisyphus/daemon.log"
|
|
2223
2277
|
);
|
|
2224
2278
|
throw new Error(lines.join("\n"));
|
|
@@ -2252,6 +2306,9 @@ function getTmuxSessionInfo() {
|
|
|
2252
2306
|
function shellQuote(s) {
|
|
2253
2307
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
2254
2308
|
}
|
|
2309
|
+
function validateRepoName(repo) {
|
|
2310
|
+
return !repo.includes("/") && !repo.includes("\\") && !repo.includes("..");
|
|
2311
|
+
}
|
|
2255
2312
|
function escapeAppleScript(s) {
|
|
2256
2313
|
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2257
2314
|
}
|
|
@@ -2353,7 +2410,7 @@ function registerStart(program2) {
|
|
|
2353
2410
|
console.log(`Tmux session: ${tmuxSessionName}`);
|
|
2354
2411
|
console.log(` tmux attach -t ${tmuxSessionName}`);
|
|
2355
2412
|
}
|
|
2356
|
-
console.log(`Monitor:
|
|
2413
|
+
console.log(`Monitor: sis status ${sessionId}`);
|
|
2357
2414
|
return;
|
|
2358
2415
|
}
|
|
2359
2416
|
let tmuxSession;
|
|
@@ -2397,7 +2454,7 @@ function registerStart(program2) {
|
|
|
2397
2454
|
if (!process.env["TMUX"]) {
|
|
2398
2455
|
attachToTmuxSession(tmuxSession);
|
|
2399
2456
|
}
|
|
2400
|
-
console.log(`Monitor:
|
|
2457
|
+
console.log(`Monitor: sis status ${sessionId}`);
|
|
2401
2458
|
});
|
|
2402
2459
|
}
|
|
2403
2460
|
|
|
@@ -2720,7 +2777,7 @@ function registerList(program2) {
|
|
|
2720
2777
|
if (sessions.length === 0) {
|
|
2721
2778
|
if (filtered && totalCount && totalCount > 0) {
|
|
2722
2779
|
console.log(`No sessions in this project. ${totalCount} session(s) in other projects.`);
|
|
2723
|
-
console.log(`${DIM2}Run ${RESET2}
|
|
2780
|
+
console.log(`${DIM2}Run ${RESET2}sis list --all${DIM2} to show all.${RESET2}`);
|
|
2724
2781
|
} else {
|
|
2725
2782
|
console.log("No sessions");
|
|
2726
2783
|
}
|
|
@@ -2738,7 +2795,7 @@ function registerList(program2) {
|
|
|
2738
2795
|
if (filtered && totalCount && totalCount > sessions.length) {
|
|
2739
2796
|
const otherCount = totalCount - sessions.length;
|
|
2740
2797
|
console.log(`
|
|
2741
|
-
${DIM2}${otherCount} more session(s) in other projects. Run ${RESET2}
|
|
2798
|
+
${DIM2}${otherCount} more session(s) in other projects. Run ${RESET2}sis list --all${DIM2} to show all.${RESET2}`);
|
|
2742
2799
|
}
|
|
2743
2800
|
} else {
|
|
2744
2801
|
console.error(`Error: ${response.error}`);
|
|
@@ -3073,17 +3130,21 @@ function inlineBodyPath(deckPath, bodyPath) {
|
|
|
3073
3130
|
const deckDir = dirname2(deckPath);
|
|
3074
3131
|
const joined = resolve2(deckDir, bodyPath);
|
|
3075
3132
|
if (!existsSync5(joined)) {
|
|
3076
|
-
throw new Error(
|
|
3133
|
+
throw new Error(
|
|
3134
|
+
`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").`
|
|
3135
|
+
);
|
|
3077
3136
|
}
|
|
3078
3137
|
const stat = lstatSync(joined);
|
|
3079
3138
|
if (!stat.isFile()) {
|
|
3080
|
-
throw new Error(`bodyPath must be a regular file: ${bodyPath}`);
|
|
3139
|
+
throw new Error(`bodyPath must be a regular file (not a symlink, directory, or special file): ${bodyPath}`);
|
|
3081
3140
|
}
|
|
3082
3141
|
const realResolved = realpathSync(joined);
|
|
3083
3142
|
const realDeckDir = realpathSync(deckDir);
|
|
3084
3143
|
const prefix = realDeckDir + sep;
|
|
3085
3144
|
if (realResolved !== realDeckDir && !realResolved.startsWith(prefix)) {
|
|
3086
|
-
throw new Error(
|
|
3145
|
+
throw new Error(
|
|
3146
|
+
`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".`
|
|
3147
|
+
);
|
|
3087
3148
|
}
|
|
3088
3149
|
return readFileSync8(joined, "utf-8");
|
|
3089
3150
|
}
|
|
@@ -3464,12 +3525,47 @@ Posts a deck of questions to the user's dashboard inbox. They walk through it an
|
|
|
3464
3525
|
|
|
3465
3526
|
The CLI always blocks until the user answers (which can take 10+ minutes).
|
|
3466
3527
|
|
|
3467
|
-
- **Orchestrator:** invoke synchronously so the orchestrator's pane stays alive while the bash blocks. Daemon refuses \`
|
|
3528
|
+
- **Orchestrator:** invoke synchronously so the orchestrator's pane stays alive while the bash blocks. Daemon refuses \`sis orch yield\` while orchestrator owns a pending deck; foreground is the supported pattern.
|
|
3468
3529
|
- **Agents / one-off Claude Code sessions:** invoke through the Bash tool with \`run_in_background: true\` and end your turn \u2014 the bash completion notification wakes you with stdout ready to parse.
|
|
3469
3530
|
|
|
3470
3531
|
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
3532
|
|
|
3472
|
-
|
|
3533
|
+
DECK JSON SCHEMA
|
|
3534
|
+
{ "title"?: string, "interactions": Interaction[] } // interactions[] non-empty
|
|
3535
|
+
|
|
3536
|
+
Interaction:
|
|
3537
|
+
id string, /^[A-Za-z0-9_-]+$/, max 64 chars, unique within deck
|
|
3538
|
+
title string (required, non-empty)
|
|
3539
|
+
subtitle? string
|
|
3540
|
+
body? string // markdown rendered in dashboard
|
|
3541
|
+
bodyPath? string // path RELATIVE to the deck JSON's directory
|
|
3542
|
+
// and must resolve INSIDE that directory
|
|
3543
|
+
// (no '..', no symlinks out, no absolute
|
|
3544
|
+
// paths pointing elsewhere). Mutually
|
|
3545
|
+
// exclusive with 'body'. To use bodyPath,
|
|
3546
|
+
// write the deck JSON next to the markdown
|
|
3547
|
+
// file (e.g. both in
|
|
3548
|
+
// $SISYPHUS_SESSION_DIR/context/) and pass
|
|
3549
|
+
// a basename like "summary.md".
|
|
3550
|
+
kind? "notify" | "validation" | "decision" | "context" | "error"
|
|
3551
|
+
// display hint for inbox icon/sort weight.
|
|
3552
|
+
// No other values accepted.
|
|
3553
|
+
options Option[] // 2\u20134 options recommended (see humanloop)
|
|
3554
|
+
allowFreetext? boolean
|
|
3555
|
+
freetextLabel? string
|
|
3556
|
+
|
|
3557
|
+
Option:
|
|
3558
|
+
id string (required)
|
|
3559
|
+
label string (required)
|
|
3560
|
+
description? string
|
|
3561
|
+
shortcut? string
|
|
3562
|
+
|
|
3563
|
+
OUTPUT
|
|
3564
|
+
On answer, stdout is one line of JSON:
|
|
3565
|
+
{ "responses": [{ "id", "selectedOptionId"?, "freetext"? }, ...], "completedAt" }
|
|
3566
|
+
Branch on each response by its interaction \`id\`.
|
|
3567
|
+
|
|
3568
|
+
Validation errors at submit are precise \u2014 read them, don't guess.
|
|
3473
3569
|
`).action(async (file, opts) => {
|
|
3474
3570
|
if (!file) {
|
|
3475
3571
|
ask.help();
|
|
@@ -3739,7 +3835,7 @@ function registerComplete(program2) {
|
|
|
3739
3835
|
console.log("Session completed.");
|
|
3740
3836
|
console.log(`
|
|
3741
3837
|
To keep working in this session:`);
|
|
3742
|
-
console.log(`
|
|
3838
|
+
console.log(` sis session continue # reactivate session and clear roadmap for new work`);
|
|
3743
3839
|
} else {
|
|
3744
3840
|
console.error(`Error: ${response.error}`);
|
|
3745
3841
|
process.exit(1);
|
|
@@ -3761,7 +3857,7 @@ function registerRollback(program2) {
|
|
|
3761
3857
|
if (response.ok) {
|
|
3762
3858
|
const data = response.data;
|
|
3763
3859
|
console.log(`Session ${sessionId} rolled back to cycle ${data.restoredToCycle}.`);
|
|
3764
|
-
console.log(`Session is now paused. Use '
|
|
3860
|
+
console.log(`Session is now paused. Use 'sis session resume ${sessionId}' to respawn the orchestrator.`);
|
|
3765
3861
|
} else {
|
|
3766
3862
|
console.error(`Error: ${response.error}`);
|
|
3767
3863
|
process.exit(1);
|
|
@@ -3797,7 +3893,7 @@ function registerClone(program2) {
|
|
|
3797
3893
|
}
|
|
3798
3894
|
const agentId = process.env.SISYPHUS_AGENT_ID;
|
|
3799
3895
|
if (agentId !== "orchestrator") {
|
|
3800
|
-
console.error("Error: clone can only be called by the orchestrator. Use
|
|
3896
|
+
console.error("Error: clone can only be called by the orchestrator. Use sis message to ask the orchestrator to clone.");
|
|
3801
3897
|
process.exit(1);
|
|
3802
3898
|
}
|
|
3803
3899
|
const request = {
|
|
@@ -4037,12 +4133,12 @@ import { join as join14, resolve as resolve5 } from "path";
|
|
|
4037
4133
|
// src/cli/stdin.ts
|
|
4038
4134
|
function readStdin2() {
|
|
4039
4135
|
if (process.stdin.isTTY) return Promise.resolve(null);
|
|
4040
|
-
return new Promise((
|
|
4136
|
+
return new Promise((resolve12, reject) => {
|
|
4041
4137
|
const chunks = [];
|
|
4042
4138
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
4043
4139
|
process.stdin.on("end", () => {
|
|
4044
4140
|
const text = Buffer.concat(chunks).toString("utf-8").trim();
|
|
4045
|
-
|
|
4141
|
+
resolve12(text || null);
|
|
4046
4142
|
});
|
|
4047
4143
|
process.stdin.on("error", reject);
|
|
4048
4144
|
});
|
|
@@ -4198,11 +4294,11 @@ function registerSpawn(program2) {
|
|
|
4198
4294
|
if (response.ok) {
|
|
4199
4295
|
const agentId = response.data?.agentId;
|
|
4200
4296
|
console.log(`Agent spawned: ${agentId}`);
|
|
4201
|
-
console.log(`Tip: \`
|
|
4202
|
-
console.log("Run `
|
|
4297
|
+
console.log(`Tip: \`sis agent await ${agentId}\` blocks for the report and consumes it inline (won't appear in next cycle).`);
|
|
4298
|
+
console.log("Run `sis orch yield` when done spawning agents.");
|
|
4203
4299
|
} else {
|
|
4204
4300
|
console.error(`Error: ${response.error}`);
|
|
4205
|
-
if (response.error?.includes("Unknown session")) console.error("Hint: run `
|
|
4301
|
+
if (response.error?.includes("Unknown session")) console.error("Hint: run `sis list` to see active sessions.");
|
|
4206
4302
|
process.exit(1);
|
|
4207
4303
|
}
|
|
4208
4304
|
});
|
|
@@ -4819,11 +4915,11 @@ function printResults(result, daemonOk, keybindMsg) {
|
|
|
4819
4915
|
}
|
|
4820
4916
|
}
|
|
4821
4917
|
console.log("");
|
|
4822
|
-
console.log("Run '
|
|
4918
|
+
console.log("Run 'sis admin getting-started' for a usage guide.");
|
|
4823
4919
|
console.log("");
|
|
4824
4920
|
}
|
|
4825
4921
|
function registerSetup(program2) {
|
|
4826
|
-
program2.command("setup").description("One-time setup: install dependencies, daemon, keybindings, and commands").action(async () => {
|
|
4922
|
+
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
4923
|
const result = runOnboarding();
|
|
4828
4924
|
let daemonOk = false;
|
|
4829
4925
|
try {
|
|
@@ -4832,7 +4928,11 @@ function registerSetup(program2) {
|
|
|
4832
4928
|
} catch {
|
|
4833
4929
|
daemonOk = isInstalled();
|
|
4834
4930
|
}
|
|
4835
|
-
const keybindResult = setupTmuxKeybind(
|
|
4931
|
+
const keybindResult = await setupTmuxKeybind(
|
|
4932
|
+
DEFAULT_CYCLE_KEY,
|
|
4933
|
+
DEFAULT_PREFIX_KEY,
|
|
4934
|
+
{ assumeYes: opts.yes }
|
|
4935
|
+
);
|
|
4836
4936
|
let keybindMsg;
|
|
4837
4937
|
if (keybindResult.status === "installed" || keybindResult.status === "already-installed") {
|
|
4838
4938
|
keybindMsg = `${DEFAULT_CYCLE_KEY} cycle, ${DEFAULT_PREFIX_KEY} prefix (h=dashboard, x=kill)`;
|
|
@@ -4845,9 +4945,9 @@ function registerSetup(program2) {
|
|
|
4845
4945
|
|
|
4846
4946
|
// src/cli/commands/setup-keybind.ts
|
|
4847
4947
|
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) => {
|
|
4948
|
+
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
4949
|
const resolvedKey = key ?? DEFAULT_CYCLE_KEY;
|
|
4850
|
-
const result = setupTmuxKeybind(resolvedKey);
|
|
4950
|
+
const result = await setupTmuxKeybind(resolvedKey, void 0, { assumeYes: opts.yes });
|
|
4851
4951
|
switch (result.status) {
|
|
4852
4952
|
case "installed":
|
|
4853
4953
|
console.log(result.message);
|
|
@@ -4861,20 +4961,56 @@ function registerSetupKeybind(program2) {
|
|
|
4861
4961
|
console.log(` ${result.existingBinding}`);
|
|
4862
4962
|
console.log("");
|
|
4863
4963
|
console.log("Use a different key, e.g.:");
|
|
4864
|
-
console.log("
|
|
4865
|
-
console.log("
|
|
4866
|
-
console.log("
|
|
4964
|
+
console.log(" sis admin setup-keybind M-S");
|
|
4965
|
+
console.log(" sis admin setup-keybind M-w");
|
|
4966
|
+
console.log(" sis admin setup-keybind M-j");
|
|
4867
4967
|
break;
|
|
4868
4968
|
case "unsupported-tmux":
|
|
4869
4969
|
console.log(result.message);
|
|
4870
4970
|
break;
|
|
4971
|
+
case "conf-modification-declined":
|
|
4972
|
+
console.log(result.message);
|
|
4973
|
+
console.log("");
|
|
4974
|
+
console.log("Re-run with --yes to skip the prompt.");
|
|
4975
|
+
break;
|
|
4871
4976
|
}
|
|
4872
4977
|
});
|
|
4873
4978
|
}
|
|
4874
4979
|
|
|
4980
|
+
// src/cli/commands/home-init.ts
|
|
4981
|
+
import { execSync as execSync11 } from "child_process";
|
|
4982
|
+
function registerHomeInit(parent) {
|
|
4983
|
+
parent.command("home-init <name> <cwd>").description("Bootstrap a tmux home session with the sisyphus dashboard.").action((name, cwd) => {
|
|
4984
|
+
ensureSession(name, cwd);
|
|
4985
|
+
setSessionCwd(name, cwd);
|
|
4986
|
+
openDashboardWindow(name, cwd);
|
|
4987
|
+
});
|
|
4988
|
+
}
|
|
4989
|
+
function sessionExists(name) {
|
|
4990
|
+
try {
|
|
4991
|
+
execSync11(`tmux has-session -t ${shellQuote(name)}`, { stdio: "pipe" });
|
|
4992
|
+
return true;
|
|
4993
|
+
} catch {
|
|
4994
|
+
return false;
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
function ensureSession(name, cwd) {
|
|
4998
|
+
if (sessionExists(name)) return;
|
|
4999
|
+
execSync11(
|
|
5000
|
+
`tmux new-session -d -s ${shellQuote(name)} -c ${shellQuote(cwd)}`,
|
|
5001
|
+
{ stdio: "pipe" }
|
|
5002
|
+
);
|
|
5003
|
+
}
|
|
5004
|
+
function setSessionCwd(name, cwd) {
|
|
5005
|
+
execSync11(
|
|
5006
|
+
`tmux set-option -t ${shellQuote(name)} @sisyphus_cwd ${shellQuote(cwd.replace(/\/+$/, ""))}`,
|
|
5007
|
+
{ stdio: "pipe" }
|
|
5008
|
+
);
|
|
5009
|
+
}
|
|
5010
|
+
|
|
4875
5011
|
// src/cli/commands/doctor.ts
|
|
4876
5012
|
init_paths();
|
|
4877
|
-
import { execSync as
|
|
5013
|
+
import { execSync as execSync12 } from "child_process";
|
|
4878
5014
|
import { existsSync as existsSync15, statSync as statSync3 } from "fs";
|
|
4879
5015
|
import { homedir as homedir9 } from "os";
|
|
4880
5016
|
import { join as join16 } from "path";
|
|
@@ -4887,7 +5023,7 @@ function checkNodeVersion() {
|
|
|
4887
5023
|
}
|
|
4888
5024
|
function checkClaudeCli() {
|
|
4889
5025
|
try {
|
|
4890
|
-
|
|
5026
|
+
execSync12("which claude", { stdio: "pipe" });
|
|
4891
5027
|
return { name: "Claude CLI", status: "ok", detail: "Found on PATH" };
|
|
4892
5028
|
} catch {
|
|
4893
5029
|
return {
|
|
@@ -4900,7 +5036,7 @@ function checkClaudeCli() {
|
|
|
4900
5036
|
}
|
|
4901
5037
|
function checkGit() {
|
|
4902
5038
|
try {
|
|
4903
|
-
const version =
|
|
5039
|
+
const version = execSync12("git --version", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
4904
5040
|
return { name: "git", status: "ok", detail: version };
|
|
4905
5041
|
} catch {
|
|
4906
5042
|
return { name: "git", status: "fail", detail: "Not found on PATH", fix: "Install git: https://git-scm.com/downloads" };
|
|
@@ -4908,7 +5044,7 @@ function checkGit() {
|
|
|
4908
5044
|
}
|
|
4909
5045
|
function checkTmuxVersion() {
|
|
4910
5046
|
try {
|
|
4911
|
-
const version =
|
|
5047
|
+
const version = execSync12("tmux -V", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
4912
5048
|
const match = version.match(/(\d+\.\d+)/);
|
|
4913
5049
|
if (!match) return { name: "tmux version", status: "warn", detail: `Could not parse version: ${version}` };
|
|
4914
5050
|
const ver = parseFloat(match[1]);
|
|
@@ -4930,7 +5066,7 @@ function checkDaemonInstalled() {
|
|
|
4930
5066
|
name: "Daemon plist",
|
|
4931
5067
|
status: "fail",
|
|
4932
5068
|
detail: "Not installed",
|
|
4933
|
-
fix: 'Run any
|
|
5069
|
+
fix: 'Run any sis command to auto-install, or: sis start "test"'
|
|
4934
5070
|
};
|
|
4935
5071
|
}
|
|
4936
5072
|
const pid = daemonPidPath();
|
|
@@ -4957,7 +5093,7 @@ function checkDaemonRunning() {
|
|
|
4957
5093
|
}
|
|
4958
5094
|
try {
|
|
4959
5095
|
const sock = socketPath();
|
|
4960
|
-
|
|
5096
|
+
execSync12(`test -S "${sock}"`, { stdio: "pipe" });
|
|
4961
5097
|
return { name: "Daemon process", status: "ok", detail: `Socket at ${sock}` };
|
|
4962
5098
|
} catch {
|
|
4963
5099
|
return {
|
|
@@ -4970,13 +5106,13 @@ function checkDaemonRunning() {
|
|
|
4970
5106
|
}
|
|
4971
5107
|
function checkTmux() {
|
|
4972
5108
|
try {
|
|
4973
|
-
|
|
5109
|
+
execSync12("which tmux", { stdio: "pipe" });
|
|
4974
5110
|
} catch {
|
|
4975
5111
|
const installHint = process.platform === "darwin" ? "brew install tmux" : "apt install tmux (Debian/Ubuntu) or your package manager";
|
|
4976
5112
|
return { name: "tmux", status: "fail", detail: "Not found on PATH", fix: installHint };
|
|
4977
5113
|
}
|
|
4978
5114
|
try {
|
|
4979
|
-
|
|
5115
|
+
execSync12("tmux list-sessions", { stdio: "pipe" });
|
|
4980
5116
|
return { name: "tmux", status: "ok", detail: "Running" };
|
|
4981
5117
|
} catch {
|
|
4982
5118
|
return { name: "tmux", status: "warn", detail: "Installed but no server running" };
|
|
@@ -4989,7 +5125,7 @@ function checkCycleScript() {
|
|
|
4989
5125
|
name: "Cycle script",
|
|
4990
5126
|
status: "fail",
|
|
4991
5127
|
detail: `Not found at ${path}`,
|
|
4992
|
-
fix: "
|
|
5128
|
+
fix: "sis admin setup-keybind"
|
|
4993
5129
|
};
|
|
4994
5130
|
}
|
|
4995
5131
|
try {
|
|
@@ -5020,7 +5156,7 @@ function checkTmuxKeybind() {
|
|
|
5020
5156
|
name: `Tmux keybind (${DEFAULT_CYCLE_KEY})`,
|
|
5021
5157
|
status: "fail",
|
|
5022
5158
|
detail: "Not bound",
|
|
5023
|
-
fix: "
|
|
5159
|
+
fix: "sis admin setup-keybind"
|
|
5024
5160
|
};
|
|
5025
5161
|
}
|
|
5026
5162
|
if (isSisyphusBinding(existing)) {
|
|
@@ -5030,7 +5166,7 @@ function checkTmuxKeybind() {
|
|
|
5030
5166
|
name: `Tmux keybind (${DEFAULT_CYCLE_KEY})`,
|
|
5031
5167
|
status: "warn",
|
|
5032
5168
|
detail: `Bound to something else: ${existing}`,
|
|
5033
|
-
fix: "
|
|
5169
|
+
fix: "sis admin setup-keybind M-S (or another free key)"
|
|
5034
5170
|
};
|
|
5035
5171
|
}
|
|
5036
5172
|
function checkGlobalDir() {
|
|
@@ -5081,13 +5217,13 @@ function checkSisyphusPlugin() {
|
|
|
5081
5217
|
name: "sisyphus@sisyphus plugin",
|
|
5082
5218
|
status: "warn",
|
|
5083
5219
|
detail: "Not installed (slash commands /sisyphus:begin, /sisyphus:autopsy, /sisyphus:configure-upload unavailable)",
|
|
5084
|
-
fix: "
|
|
5220
|
+
fix: "sis admin setup"
|
|
5085
5221
|
};
|
|
5086
5222
|
}
|
|
5087
5223
|
function checkTermrender() {
|
|
5088
5224
|
if (isTermrenderAvailable()) {
|
|
5089
5225
|
try {
|
|
5090
|
-
const version =
|
|
5226
|
+
const version = execSync12("termrender --version", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
5091
5227
|
return { name: "termrender", status: "ok", detail: version };
|
|
5092
5228
|
} catch {
|
|
5093
5229
|
return { name: "termrender", status: "ok", detail: "installed" };
|
|
@@ -5106,7 +5242,7 @@ function checkNvim() {
|
|
|
5106
5242
|
return { name: "nvim", status: "warn", detail: "Not installed", fix };
|
|
5107
5243
|
}
|
|
5108
5244
|
try {
|
|
5109
|
-
const version =
|
|
5245
|
+
const version = execSync12("nvim --version", { encoding: "utf-8", stdio: "pipe" }).split("\n")[0]?.replace("NVIM ", "");
|
|
5110
5246
|
return { name: "nvim", status: "ok", detail: version ?? "installed" };
|
|
5111
5247
|
} catch {
|
|
5112
5248
|
return { name: "nvim", status: "ok", detail: "installed" };
|
|
@@ -5208,13 +5344,13 @@ function registerInit(program2) {
|
|
|
5208
5344
|
}
|
|
5209
5345
|
|
|
5210
5346
|
// src/cli/commands/uninstall.ts
|
|
5211
|
-
import { createInterface } from "readline";
|
|
5347
|
+
import { createInterface as createInterface2 } from "readline";
|
|
5212
5348
|
async function confirm(question) {
|
|
5213
|
-
const rl =
|
|
5214
|
-
return new Promise((
|
|
5349
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
5350
|
+
return new Promise((resolve12) => {
|
|
5215
5351
|
rl.question(question, (answer) => {
|
|
5216
5352
|
rl.close();
|
|
5217
|
-
|
|
5353
|
+
resolve12(answer.trim().toLowerCase() === "y");
|
|
5218
5354
|
});
|
|
5219
5355
|
});
|
|
5220
5356
|
}
|
|
@@ -5235,31 +5371,31 @@ function registerUninstall(program2) {
|
|
|
5235
5371
|
// src/cli/commands/configure-upload.ts
|
|
5236
5372
|
init_paths();
|
|
5237
5373
|
import { chmodSync as chmodSync2, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync19, writeFileSync as writeFileSync10 } from "fs";
|
|
5238
|
-
import { createInterface as
|
|
5374
|
+
import { createInterface as createInterface3 } from "readline";
|
|
5239
5375
|
import { dirname as dirname6 } from "path";
|
|
5240
5376
|
async function readUrlFromInput(interactive) {
|
|
5241
5377
|
if (interactive) {
|
|
5242
|
-
return new Promise((
|
|
5243
|
-
const rl =
|
|
5378
|
+
return new Promise((resolve12) => {
|
|
5379
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
5244
5380
|
rl.question("Paste the upload URL (with embedded ?token=): ", (answer) => {
|
|
5245
5381
|
rl.close();
|
|
5246
|
-
|
|
5382
|
+
resolve12(answer.trim());
|
|
5247
5383
|
});
|
|
5248
5384
|
});
|
|
5249
5385
|
}
|
|
5250
|
-
return new Promise((
|
|
5386
|
+
return new Promise((resolve12) => {
|
|
5251
5387
|
const chunks = [];
|
|
5252
5388
|
process.stdin.setEncoding("utf-8");
|
|
5253
5389
|
process.stdin.on("data", (chunk) => {
|
|
5254
5390
|
chunks.push(chunk);
|
|
5255
5391
|
});
|
|
5256
5392
|
process.stdin.on("end", () => {
|
|
5257
|
-
|
|
5393
|
+
resolve12(chunks.join("").trim());
|
|
5258
5394
|
});
|
|
5259
5395
|
});
|
|
5260
5396
|
}
|
|
5261
5397
|
function registerConfigureUpload(program2) {
|
|
5262
|
-
program2.command("configure-upload").description("Configure the upload proxy from a token-bearing URL (writes ~/.sisyphus/config.json)").argument("[url]", "Worker URL with embedded ?token= query (https://worker/upload?token=sisyphus_pat_...); omit to read from stdin").option("--stdin", "Read URL from stdin (pipe-friendly: pbpaste |
|
|
5398
|
+
program2.command("configure-upload").description("Configure the upload proxy from a token-bearing URL (writes ~/.sisyphus/config.json)").argument("[url]", "Worker URL with embedded ?token= query (https://worker/upload?token=sisyphus_pat_...); omit to read from stdin").option("--stdin", "Read URL from stdin (pipe-friendly: pbpaste | sis admin configure-upload --stdin)").action(async (urlArg, opts) => {
|
|
5263
5399
|
let rawUrl;
|
|
5264
5400
|
const fromStdin = opts.stdin || urlArg === "-" || !urlArg && process.stdin.isTTY === false;
|
|
5265
5401
|
const fromInteractive = !urlArg && !opts.stdin && process.stdin.isTTY === true;
|
|
@@ -5268,7 +5404,7 @@ function registerConfigureUpload(program2) {
|
|
|
5268
5404
|
} else {
|
|
5269
5405
|
rawUrl = urlArg;
|
|
5270
5406
|
console.warn(
|
|
5271
|
-
"warning: passing the token on argv exposes it via `ps` and shell history; pipe it on stdin instead: `pbpaste |
|
|
5407
|
+
"warning: passing the token on argv exposes it via `ps` and shell history; pipe it on stdin instead: `pbpaste | sis admin configure-upload --stdin`"
|
|
5272
5408
|
);
|
|
5273
5409
|
}
|
|
5274
5410
|
let parsed;
|
|
@@ -5309,7 +5445,7 @@ function registerConfigureUpload(program2) {
|
|
|
5309
5445
|
}
|
|
5310
5446
|
|
|
5311
5447
|
// src/cli/commands/getting-started.ts
|
|
5312
|
-
import { execSync as
|
|
5448
|
+
import { execSync as execSync13 } from "child_process";
|
|
5313
5449
|
import { dirname as dirname7, join as join18 } from "path";
|
|
5314
5450
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5315
5451
|
function templatePath(name) {
|
|
@@ -5321,7 +5457,7 @@ function isClaudeCode() {
|
|
|
5321
5457
|
function printNonClaudeMessage() {
|
|
5322
5458
|
console.log(`
|
|
5323
5459
|
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
5324
|
-
\u2551
|
|
5460
|
+
\u2551 sis admin getting-started \u2014 Interactive Tutorial \u2551
|
|
5325
5461
|
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
5326
5462
|
|
|
5327
5463
|
This command provides an interactive tutorial best experienced
|
|
@@ -5329,11 +5465,11 @@ function printNonClaudeMessage() {
|
|
|
5329
5465
|
|
|
5330
5466
|
To start:
|
|
5331
5467
|
1. Open Claude Code: claude
|
|
5332
|
-
2. Run:
|
|
5468
|
+
2. Run: sis admin getting-started
|
|
5333
5469
|
|
|
5334
5470
|
If you just want the quick reference, run:
|
|
5335
|
-
|
|
5336
|
-
|
|
5471
|
+
sis --help
|
|
5472
|
+
sis admin doctor
|
|
5337
5473
|
`);
|
|
5338
5474
|
}
|
|
5339
5475
|
function printStep0() {
|
|
@@ -5363,14 +5499,14 @@ This tutorial has 6 steps. Share this overview so the user knows what's coming a
|
|
|
5363
5499
|
|
|
5364
5500
|
| Step | Topic | Command |
|
|
5365
5501
|
|------|-------|---------|
|
|
5366
|
-
| 0 | Entry & tmux gate (you are here) | \`
|
|
5502
|
+
| 0 | Entry & tmux gate (you are here) | \`sis admin getting-started\` |
|
|
5367
5503
|
| 1 | Tmux basics \u2014 sessions, panes, navigation | \`--tutorial 1\` |
|
|
5368
5504
|
| 2 | Nvim basics \u2014 open, save, quit (optional) | \`--tutorial 2\` |
|
|
5369
5505
|
| 3 | Sisyphus concepts \u2014 session model & keybinds | \`--tutorial 3\` |
|
|
5370
5506
|
| 4 | Live demo \u2014 launch and observe a real session | \`--tutorial 4\` |
|
|
5371
5507
|
| 5 | What's next \u2014 real usage guidance & suggestions | \`--tutorial 5\` |
|
|
5372
5508
|
|
|
5373
|
-
Tell the user they can skip to any step with \`
|
|
5509
|
+
Tell the user they can skip to any step with \`sis admin getting-started --tutorial <N>\`.
|
|
5374
5510
|
|
|
5375
5511
|
## Instructions for Claude
|
|
5376
5512
|
|
|
@@ -5380,8 +5516,8 @@ You are guiding a user through the Sisyphus interactive tutorial.
|
|
|
5380
5516
|
|
|
5381
5517
|
Ask the user if they'd like the interactive walkthrough. If they decline, give this quick summary and stop:
|
|
5382
5518
|
|
|
5383
|
-
> Sisyphus is a multi-agent orchestrator for Claude Code. Start a session with \`
|
|
5384
|
-
> monitor with \`
|
|
5519
|
+
> Sisyphus is a multi-agent orchestrator for Claude Code. Start a session with \`sis start "task"\`,
|
|
5520
|
+
> monitor with \`sis dashboard\`, and check health with \`sis admin doctor\`.
|
|
5385
5521
|
|
|
5386
5522
|
### If they want the tutorial:
|
|
5387
5523
|
|
|
@@ -5403,17 +5539,17 @@ Ask the user if they'd like the interactive walkthrough. If they decline, give t
|
|
|
5403
5539
|
- Tell them to install tmux:
|
|
5404
5540
|
- macOS: \`brew install tmux\`
|
|
5405
5541
|
- Linux: \`apt install tmux\` or their package manager
|
|
5406
|
-
- After install, re-run: \`
|
|
5542
|
+
- After install, re-run: \`sis admin getting-started --tutorial 0\` to verify
|
|
5407
5543
|
|
|
5408
5544
|
**Case 2: tmux installed but NOT in a tmux session (inTmux: false)**
|
|
5409
5545
|
- Tell the user they need to be inside a tmux session for the tutorial
|
|
5410
5546
|
- Have them run: \`tmux new-session\`
|
|
5411
5547
|
- Then resume the conversation with Claude in the new tmux session: \`claude\`
|
|
5412
|
-
- Then re-run: \`
|
|
5548
|
+
- Then re-run: \`sis admin getting-started --tutorial 0\` to verify
|
|
5413
5549
|
|
|
5414
5550
|
**Case 3: In tmux (inTmux: true)**
|
|
5415
5551
|
- Tell the user they're all set \u2014 tmux is running
|
|
5416
|
-
- Proceed by running: \`
|
|
5552
|
+
- Proceed by running: \`sis admin getting-started --tutorial 1\`
|
|
5417
5553
|
</claude-instructions>
|
|
5418
5554
|
`);
|
|
5419
5555
|
}
|
|
@@ -5491,7 +5627,7 @@ Ask the user to confirm: "Can you navigate between panes with Ctrl+h and Ctrl+l?
|
|
|
5491
5627
|
|
|
5492
5628
|
Once confirmed, proceed:
|
|
5493
5629
|
\`\`\`
|
|
5494
|
-
|
|
5630
|
+
sis admin getting-started --tutorial 2
|
|
5495
5631
|
\`\`\`
|
|
5496
5632
|
</claude-instructions>
|
|
5497
5633
|
`);
|
|
@@ -5533,7 +5669,7 @@ Ask if they were able to edit and save the file (or if they skipped).
|
|
|
5533
5669
|
|
|
5534
5670
|
Proceed:
|
|
5535
5671
|
\`\`\`
|
|
5536
|
-
|
|
5672
|
+
sis admin getting-started --tutorial 3
|
|
5537
5673
|
\`\`\`
|
|
5538
5674
|
</claude-instructions>
|
|
5539
5675
|
`);
|
|
@@ -5594,7 +5730,7 @@ function printStep3() {
|
|
|
5594
5730
|
> typing special characters (accents, symbols). The right Option key
|
|
5595
5731
|
> becomes your "Meta" key for tmux/sisyphus keybinds.
|
|
5596
5732
|
|
|
5597
|
-
After they change it, have them verify by re-running \`
|
|
5733
|
+
After they change it, have them verify by re-running \`sis admin doctor\` \u2014 look for "Right Option Key: Esc+".
|
|
5598
5734
|
|
|
5599
5735
|
- **rightOptionKeyStatus: not-iterm** \u2014 They're not using iTerm2. Explain:
|
|
5600
5736
|
> Sisyphus keybinds use Option as Meta. In iTerm2 this is configured via
|
|
@@ -5640,12 +5776,12 @@ Two keybinds to remember (both use the RIGHT Option key):
|
|
|
5640
5776
|
|
|
5641
5777
|
### 4. Verify keybinds are installed
|
|
5642
5778
|
|
|
5643
|
-
Run \`
|
|
5779
|
+
Run \`sis admin doctor\` and check the output. Look for:
|
|
5644
5780
|
- "Cycle script" \u2014 should be \u2713
|
|
5645
5781
|
- "Tmux keybind" \u2014 should be \u2713
|
|
5646
5782
|
- "Right Option Key" \u2014 should be "Esc+"
|
|
5647
5783
|
|
|
5648
|
-
If cycle script or keybind is missing, run: \`
|
|
5784
|
+
If cycle script or keybind is missing, run: \`sis admin setup-keybind\`
|
|
5649
5785
|
|
|
5650
5786
|
### 5. Test the keybind
|
|
5651
5787
|
|
|
@@ -5657,12 +5793,12 @@ If they see \`\xDF\` or similar, circle back to the Right Option Key setup above
|
|
|
5657
5793
|
|
|
5658
5794
|
Confirm:
|
|
5659
5795
|
- They understand the two-session model (their session vs sisyphus session)
|
|
5660
|
-
- \`
|
|
5796
|
+
- \`sis admin doctor\` shows keybinds installed AND Right Option Key: Esc+
|
|
5661
5797
|
- Right Option + s doesn't produce a special character
|
|
5662
5798
|
|
|
5663
5799
|
Proceed:
|
|
5664
5800
|
\`\`\`
|
|
5665
|
-
|
|
5801
|
+
sis admin getting-started --tutorial 4
|
|
5666
5802
|
\`\`\`
|
|
5667
5803
|
</claude-instructions>
|
|
5668
5804
|
`);
|
|
@@ -5678,12 +5814,12 @@ This is the grand finale \u2014 a live demo session.
|
|
|
5678
5814
|
|
|
5679
5815
|
### 1. Health check
|
|
5680
5816
|
|
|
5681
|
-
Run \`
|
|
5817
|
+
Run \`sis admin doctor\` first. If any checks are failing, help the user fix them before proceeding.
|
|
5682
5818
|
All core checks (tmux, daemon, keybinds) should be \u2713.
|
|
5683
5819
|
|
|
5684
5820
|
### 2. BEFORE launching: Teach navigation
|
|
5685
5821
|
|
|
5686
|
-
**This is critical.** When \`
|
|
5822
|
+
**This is critical.** When \`sis start\` runs, it auto-opens the dashboard in a new tmux window. The user will suddenly be looking at the dashboard and may feel "stuck". Teach them how to navigate BEFORE launching:
|
|
5687
5823
|
|
|
5688
5824
|
Explain clearly:
|
|
5689
5825
|
|
|
@@ -5728,7 +5864,7 @@ Tell the user:
|
|
|
5728
5864
|
|
|
5729
5865
|
Then launch from the demo directory:
|
|
5730
5866
|
\`\`\`
|
|
5731
|
-
cd /tmp/sisyphus-tutorial-demo &&
|
|
5867
|
+
cd /tmp/sisyphus-tutorial-demo && sis start "Add three improvements to this todo app: (1) add a priority field (high/medium/low) to todos, (2) add a GET /todos/stats endpoint that returns counts of total/done/pending todos, (3) add tests for the new features. Explain your thinking at each step." -c "TUTORIAL DEMO: A user is watching this session to learn how sisyphus works. Be EXTRA VERBOSE \u2014 explain your reasoning, narrate what you're doing, and make your planning visible. When spawning agents, give each agent context that this is a tutorial demo and they should explain their work clearly. Keep scope small: 2-3 agents, 1-2 cycles."
|
|
5732
5868
|
\`\`\`
|
|
5733
5869
|
|
|
5734
5870
|
After launching, tell them:
|
|
@@ -5741,7 +5877,7 @@ Wait for them to confirm they're back, then start live commentary.
|
|
|
5741
5877
|
|
|
5742
5878
|
**This is the most important part of the demo.** Don't just launch and wait \u2014 actively narrate.
|
|
5743
5879
|
|
|
5744
|
-
Once the user is back, start a polling loop. Every ~45 seconds, run \`
|
|
5880
|
+
Once the user is back, start a polling loop. Every ~45 seconds, run \`sis status --verbose <session-id>\` and provide SHORT, contextual commentary about what's happening. The \`--verbose\` flag shows agent instructions, full roadmap, cycle logs, and live pane output from the orchestrator and running agents \u2014 use this rich data to narrate what's actually happening, not just phase names.
|
|
5745
5881
|
|
|
5746
5882
|
**How to narrate each phase:**
|
|
5747
5883
|
|
|
@@ -5780,7 +5916,7 @@ Once the session shows "completed":
|
|
|
5780
5916
|
|
|
5781
5917
|
Tell the user the demo is done. Then run:
|
|
5782
5918
|
\`\`\`
|
|
5783
|
-
|
|
5919
|
+
sis admin getting-started --tutorial 5
|
|
5784
5920
|
\`\`\`
|
|
5785
5921
|
</claude-instructions>
|
|
5786
5922
|
`);
|
|
@@ -5789,11 +5925,11 @@ function printStep5() {
|
|
|
5789
5925
|
let recentCommits = "";
|
|
5790
5926
|
let topLevelFiles = "";
|
|
5791
5927
|
try {
|
|
5792
|
-
recentCommits =
|
|
5928
|
+
recentCommits = execSync13("git log --oneline -15 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
5793
5929
|
} catch {
|
|
5794
5930
|
}
|
|
5795
5931
|
try {
|
|
5796
|
-
topLevelFiles =
|
|
5932
|
+
topLevelFiles = execSync13("ls -1 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
5797
5933
|
} catch {
|
|
5798
5934
|
}
|
|
5799
5935
|
console.log(`
|
|
@@ -5856,7 +5992,7 @@ This is the most important part. Explain clearly:
|
|
|
5856
5992
|
The easiest way is the \`/sisyphus:begin\` slash command inside Claude Code. Just tell Claude
|
|
5857
5993
|
what you want to build and it'll hand it off to sisyphus with the right context.
|
|
5858
5994
|
|
|
5859
|
-
Or directly: \`
|
|
5995
|
+
Or directly: \`sis start "your task" -c "any background context"\`
|
|
5860
5996
|
|
|
5861
5997
|
### 4. Suggest real tasks for THEIR codebase
|
|
5862
5998
|
|
|
@@ -5878,7 +6014,7 @@ Tell them:
|
|
|
5878
6014
|
> to understand the philosophy, or you want a deeper rundown on the dashboard,
|
|
5879
6015
|
> monitoring, configuration, or how to steer sessions \u2014 just ask and I'll explain.
|
|
5880
6016
|
|
|
5881
|
-
If the user says yes or asks to learn more, run \`
|
|
6017
|
+
If the user says yes or asks to learn more, run \`sis admin getting-started --explain\`
|
|
5882
6018
|
and use its output to explain the system to them conversationally. Don't dump the whole
|
|
5883
6019
|
thing \u2014 answer what they're curious about, using the reference as your source material.
|
|
5884
6020
|
</claude-instructions>
|
|
@@ -5894,17 +6030,17 @@ function buildCommandTable(program2) {
|
|
|
5894
6030
|
const fmtArgs = (c2) => c2.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
|
|
5895
6031
|
if (subs.length === 0) {
|
|
5896
6032
|
const args2 = fmtArgs(cmd);
|
|
5897
|
-
const usage = args2 ? `
|
|
6033
|
+
const usage = args2 ? `sis ${cmd.name()} ${args2}` : `sis ${cmd.name()}`;
|
|
5898
6034
|
lines.push(`| \`${usage}\` | ${cmd.description()} |`);
|
|
5899
6035
|
} else {
|
|
5900
6036
|
if (hasOwnAction) {
|
|
5901
6037
|
const args2 = fmtArgs(cmd);
|
|
5902
|
-
const usage = args2 ? `
|
|
6038
|
+
const usage = args2 ? `sis ${cmd.name()} ${args2}` : `sis ${cmd.name()}`;
|
|
5903
6039
|
lines.push(`| \`${usage}\` | ${cmd.description()} |`);
|
|
5904
6040
|
}
|
|
5905
6041
|
for (const sub of subs) {
|
|
5906
6042
|
const args2 = fmtArgs(sub);
|
|
5907
|
-
const usage = args2 ? `
|
|
6043
|
+
const usage = args2 ? `sis ${cmd.name()} ${sub.name()} ${args2}` : `sis ${cmd.name()} ${sub.name()}`;
|
|
5908
6044
|
lines.push(`| \`${usage}\` | ${sub.description()} |`);
|
|
5909
6045
|
}
|
|
5910
6046
|
}
|
|
@@ -6054,7 +6190,7 @@ code that looks right and code that works.
|
|
|
6054
6190
|
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
6055
6191
|
\u2502 SESSION LIFECYCLE \u2502
|
|
6056
6192
|
\u2502 \u2502
|
|
6057
|
-
\u2502
|
|
6193
|
+
\u2502 sis start "task" \u2502
|
|
6058
6194
|
\u2502 \u2502 \u2502
|
|
6059
6195
|
\u2502 \u25BC \u2502
|
|
6060
6196
|
\u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 spawn agents \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502
|
|
@@ -6062,7 +6198,7 @@ code that looks right and code that works.
|
|
|
6062
6198
|
\u2502 \u2502 plans \u2502 then yields \u2502 in parallel \u2502 \u2502
|
|
6063
6199
|
\u2502 \u2514\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502
|
|
6064
6200
|
\u2502 \u2502 \u2502 each calls \u2502
|
|
6065
|
-
\u2502 \u2502 orchestrator \u2502
|
|
6201
|
+
\u2502 \u2502 orchestrator \u2502 sis agent submit \u2502
|
|
6066
6202
|
\u2502 \u2502 is KILLED \u2502 when done \u2502
|
|
6067
6203
|
\u2502 \u2502 \u25BC \u2502
|
|
6068
6204
|
\u2502 \u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502
|
|
@@ -6097,7 +6233,7 @@ This means it never runs out of context, no matter how many cycles a session tak
|
|
|
6097
6233
|
|
|
6098
6234
|
## The Dashboard
|
|
6099
6235
|
|
|
6100
|
-
The dashboard is a real-time TUI that shows session state. Launch with \`
|
|
6236
|
+
The dashboard is a real-time TUI that shows session state. Launch with \`sis dashboard\`
|
|
6101
6237
|
or it auto-opens when a session starts.
|
|
6102
6238
|
|
|
6103
6239
|
**Dashboard sections:**
|
|
@@ -6136,14 +6272,14 @@ Sisyphus sessions should be actively monitored. Here's what to watch for:
|
|
|
6136
6272
|
|
|
6137
6273
|
**When to intervene:**
|
|
6138
6274
|
- Use \`m\` in the dashboard to message the orchestrator with corrections
|
|
6139
|
-
- Use \`
|
|
6140
|
-
- Use \`
|
|
6275
|
+
- Use \`sis session kill <id>\` to stop a runaway session
|
|
6276
|
+
- Use \`sis session resume <id> "new instructions"\` to restart with different direction
|
|
6141
6277
|
|
|
6142
6278
|
**Useful monitoring commands:**
|
|
6143
6279
|
\`\`\`
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6280
|
+
sis status <id> # Quick status check
|
|
6281
|
+
sis status --verbose <id> # Full detail: roadmap, pane output, agent instructions
|
|
6282
|
+
sis dashboard # Interactive TUI
|
|
6147
6283
|
tail -f ~/.sisyphus/daemon.log # Daemon activity log
|
|
6148
6284
|
\`\`\`
|
|
6149
6285
|
|
|
@@ -6209,11 +6345,11 @@ Then describe your task. Claude will hand it off with the right context.
|
|
|
6209
6345
|
|
|
6210
6346
|
**Direct CLI:**
|
|
6211
6347
|
\`\`\`
|
|
6212
|
-
|
|
6213
|
-
|
|
6348
|
+
sis start "task description" -c "background context"
|
|
6349
|
+
sis start "Implement @requirements.md" -n my-feature
|
|
6214
6350
|
\`\`\`
|
|
6215
6351
|
|
|
6216
|
-
**Reference files with @**: \`
|
|
6352
|
+
**Reference files with @**: \`sis start "Build @docs/spec.md"\` \u2014 the orchestrator
|
|
6217
6353
|
will read the referenced file as part of its planning.
|
|
6218
6354
|
|
|
6219
6355
|
**The -c flag** adds background context the orchestrator sees but doesn't act on directly.
|
|
@@ -6235,10 +6371,10 @@ sisyphusd restart
|
|
|
6235
6371
|
**Keybinds not working (special characters appear):**
|
|
6236
6372
|
iTerm2 \u2192 Settings \u2192 Profiles \u2192 Keys \u2192 Right Option Key \u2192 Esc+
|
|
6237
6373
|
|
|
6238
|
-
**Agents stuck:** Check \`
|
|
6374
|
+
**Agents stuck:** Check \`sis status --verbose <id>\` to see pane output. If an
|
|
6239
6375
|
agent is waiting for input, kill the session and restart with clearer instructions.
|
|
6240
6376
|
|
|
6241
|
-
**Dashboard not opening:** Run \`
|
|
6377
|
+
**Dashboard not opening:** Run \`sis dashboard\` manually. Must be inside tmux.
|
|
6242
6378
|
|
|
6243
6379
|
**Session seems hung:** Check \`tail -20 ~/.sisyphus/daemon.log\` for errors.
|
|
6244
6380
|
The daemon polls panes every 2s \u2014 if a pane dies unexpectedly, it'll be detected.
|
|
@@ -6558,7 +6694,7 @@ function showSession(idOrName, opts) {
|
|
|
6558
6694
|
console.log(`${DIM3}Compute:${RESET3} ${formatDuration(computeMs)} ${DIM3}Interactive:${RESET3} ${formatDuration(interactiveMs)} ${DIM3}(TUI wait time, not compute)${RESET3}`);
|
|
6559
6695
|
}
|
|
6560
6696
|
if (s.userBlockedMs > 0) {
|
|
6561
|
-
console.log(`${DIM3}Waiting on user:${RESET3} ${formatDuration(s.userBlockedMs)} ${DIM3}(blocked on
|
|
6697
|
+
console.log(`${DIM3}Waiting on user:${RESET3} ${formatDuration(s.userBlockedMs)} ${DIM3}(blocked on sis ask, not compute)${RESET3}`);
|
|
6562
6698
|
}
|
|
6563
6699
|
console.log("");
|
|
6564
6700
|
console.log(`${BOLD3}Task${RESET3}`);
|
|
@@ -6960,7 +7096,7 @@ function registerExport(program2) {
|
|
|
6960
7096
|
}
|
|
6961
7097
|
if (!sessionId) {
|
|
6962
7098
|
console.error("Error: No session ID provided and no active session found.");
|
|
6963
|
-
console.error("Usage:
|
|
7099
|
+
console.error("Usage: sis admin export [session-id]");
|
|
6964
7100
|
process.exit(1);
|
|
6965
7101
|
}
|
|
6966
7102
|
try {
|
|
@@ -7084,13 +7220,13 @@ function registerUpload(program2) {
|
|
|
7084
7220
|
}
|
|
7085
7221
|
if (!sessionId) {
|
|
7086
7222
|
console.error("Error: No session ID provided and no active session found.");
|
|
7087
|
-
console.error("Usage:
|
|
7223
|
+
console.error("Usage: sis admin upload [session-id]");
|
|
7088
7224
|
process.exit(1);
|
|
7089
7225
|
}
|
|
7090
7226
|
const config = loadConfig(cwd);
|
|
7091
7227
|
if (!isUploadConfigured(config.upload)) {
|
|
7092
7228
|
console.error(
|
|
7093
|
-
"Error: upload not configured. Run '
|
|
7229
|
+
"Error: upload not configured. Run 'sis admin configure-upload <url-with-token>' or set { upload: { url, token } } in .sisyphus/config.json."
|
|
7094
7230
|
);
|
|
7095
7231
|
process.exit(1);
|
|
7096
7232
|
}
|
|
@@ -7149,12 +7285,12 @@ function registerUpload(program2) {
|
|
|
7149
7285
|
}
|
|
7150
7286
|
|
|
7151
7287
|
// src/cli/commands/scratch.ts
|
|
7152
|
-
import { execSync as
|
|
7288
|
+
import { execSync as execSync14 } from "child_process";
|
|
7153
7289
|
function findHomeSession(cwd) {
|
|
7154
7290
|
const normalizedCwd = cwd.replace(/\/+$/, "");
|
|
7155
7291
|
let output;
|
|
7156
7292
|
try {
|
|
7157
|
-
output =
|
|
7293
|
+
output = execSync14('tmux list-sessions -F "#{session_id}|#{session_name}"', {
|
|
7158
7294
|
encoding: "utf-8",
|
|
7159
7295
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7160
7296
|
}).trim();
|
|
@@ -7168,7 +7304,7 @@ function findHomeSession(cwd) {
|
|
|
7168
7304
|
const name = line.slice(pipeIdx + 1);
|
|
7169
7305
|
if (name.startsWith("ssyph_")) continue;
|
|
7170
7306
|
try {
|
|
7171
|
-
const val =
|
|
7307
|
+
const val = execSync14(
|
|
7172
7308
|
`tmux show-options -t ${shellQuote(sessId)} -v @sisyphus_cwd`,
|
|
7173
7309
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
7174
7310
|
).trim();
|
|
@@ -7184,7 +7320,7 @@ function registerScratch(program2) {
|
|
|
7184
7320
|
const cwd = opts.cwd ?? process.env["SISYPHUS_CWD"] ?? process.cwd();
|
|
7185
7321
|
const homeSession = findHomeSession(cwd);
|
|
7186
7322
|
if (!homeSession) {
|
|
7187
|
-
const current =
|
|
7323
|
+
const current = execSync14('tmux display-message -p "#{session_name}"', {
|
|
7188
7324
|
encoding: "utf-8"
|
|
7189
7325
|
}).trim();
|
|
7190
7326
|
openScratchWindow(current, cwd, promptParts.join(" "));
|
|
@@ -7194,7 +7330,7 @@ function registerScratch(program2) {
|
|
|
7194
7330
|
});
|
|
7195
7331
|
}
|
|
7196
7332
|
function openScratchWindow(tmuxSession, cwd, prompt) {
|
|
7197
|
-
const windowId =
|
|
7333
|
+
const windowId = execSync14(
|
|
7198
7334
|
`tmux new-window -t ${shellQuote(tmuxSession + ":")} -n "scratch" -c ${shellQuote(cwd)} -P -F "#{window_id}"`,
|
|
7199
7335
|
{ encoding: "utf-8" }
|
|
7200
7336
|
).trim();
|
|
@@ -7202,7 +7338,7 @@ function openScratchWindow(tmuxSession, cwd, prompt) {
|
|
|
7202
7338
|
if (prompt) {
|
|
7203
7339
|
cmd += ` -p ${shellQuote(prompt)}`;
|
|
7204
7340
|
}
|
|
7205
|
-
|
|
7341
|
+
execSync14(
|
|
7206
7342
|
`tmux send-keys -t ${shellQuote(windowId)} ${shellQuote(cmd)} Enter`
|
|
7207
7343
|
);
|
|
7208
7344
|
console.log(`Scratch session opened in ${tmuxSession}`);
|
|
@@ -7244,14 +7380,14 @@ File resolution (first match wins):
|
|
|
7244
7380
|
3. Most recent session with a requirements.json
|
|
7245
7381
|
|
|
7246
7382
|
Examples:
|
|
7247
|
-
$
|
|
7248
|
-
$
|
|
7249
|
-
$
|
|
7250
|
-
$
|
|
7251
|
-
$
|
|
7252
|
-
$
|
|
7253
|
-
$
|
|
7254
|
-
$
|
|
7383
|
+
$ sis admin requirements Auto-detect from current session
|
|
7384
|
+
$ sis admin requirements path/to/requirements.json Open a specific file
|
|
7385
|
+
$ sis admin requirements --session-id abc123 Target a specific session
|
|
7386
|
+
$ sis admin requirements --schema Print the JSON schema
|
|
7387
|
+
$ sis admin requirements --annotated Print schema with writing guidance
|
|
7388
|
+
$ sis admin requirements --export Render requirements.md from JSON
|
|
7389
|
+
$ sis admin requirements --export --session-id abc123 Target a specific session
|
|
7390
|
+
$ sis admin requirements --export --force Overwrite even if hand-edited
|
|
7255
7391
|
`).action(async (file, opts) => {
|
|
7256
7392
|
if (opts.force && !opts.export) {
|
|
7257
7393
|
console.error("Error: --force requires --export");
|
|
@@ -7405,7 +7541,7 @@ var REQUIREMENTS_SCHEMA = {
|
|
|
7405
7541
|
var REQUIREMENTS_ANNOTATED = `# requirements.json \u2014 Annotated Writing Guide
|
|
7406
7542
|
#
|
|
7407
7543
|
# This is NOT valid JSON \u2014 it's a reference showing every field with
|
|
7408
|
-
# inline guidance. Run \`
|
|
7544
|
+
# inline guidance. Run \`sis admin requirements --schema\` for the raw
|
|
7409
7545
|
# JSON Schema.
|
|
7410
7546
|
#
|
|
7411
7547
|
# Safe assumptions must satisfy the same EARS shape requirements as
|
|
@@ -9366,7 +9502,7 @@ import { join as join25 } from "path";
|
|
|
9366
9502
|
// src/cli/deploy/runner.ts
|
|
9367
9503
|
init_paths();
|
|
9368
9504
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
|
|
9369
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
9505
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync27, mkdirSync as mkdirSync13, readFileSync as readFileSync29 } from "fs";
|
|
9370
9506
|
init_creds();
|
|
9371
9507
|
|
|
9372
9508
|
// src/cli/deploy/pricing.ts
|
|
@@ -9399,16 +9535,82 @@ function formatCostLine(provider, instanceType) {
|
|
|
9399
9535
|
return `Estimated cost: ~$${cost.toFixed(2)}/mo (pricing last verified ${LAST_VERIFIED}; verify against your bill for current rates).`;
|
|
9400
9536
|
}
|
|
9401
9537
|
|
|
9538
|
+
// src/cli/deploy/runtime.ts
|
|
9539
|
+
init_atomic();
|
|
9540
|
+
init_paths();
|
|
9541
|
+
import { existsSync as existsSync25, readFileSync as readFileSync28, unlinkSync as unlinkSync4 } from "fs";
|
|
9542
|
+
function readRuntimeState(provider) {
|
|
9543
|
+
const path = deployRuntimePath(provider);
|
|
9544
|
+
if (!existsSync25(path)) return null;
|
|
9545
|
+
try {
|
|
9546
|
+
return JSON.parse(readFileSync28(path, "utf-8"));
|
|
9547
|
+
} catch {
|
|
9548
|
+
return null;
|
|
9549
|
+
}
|
|
9550
|
+
}
|
|
9551
|
+
function writeRuntimeState(provider, state) {
|
|
9552
|
+
atomicWrite(deployRuntimePath(provider), JSON.stringify(state, null, 2) + "\n");
|
|
9553
|
+
}
|
|
9554
|
+
function clearRuntimeState(provider) {
|
|
9555
|
+
const path = deployRuntimePath(provider);
|
|
9556
|
+
if (existsSync25(path)) unlinkSync4(path);
|
|
9557
|
+
}
|
|
9558
|
+
|
|
9559
|
+
// src/cli/deploy/tailnet.ts
|
|
9560
|
+
function discoverNode(requestedName) {
|
|
9561
|
+
const json = execSafe("tailscale status --json");
|
|
9562
|
+
if (!json) return null;
|
|
9563
|
+
let status;
|
|
9564
|
+
try {
|
|
9565
|
+
status = JSON.parse(json);
|
|
9566
|
+
} catch {
|
|
9567
|
+
return null;
|
|
9568
|
+
}
|
|
9569
|
+
const peers = status.Peer === void 0 ? [] : Object.values(status.Peer);
|
|
9570
|
+
const candidates = peers.filter(
|
|
9571
|
+
(p) => p.HostName === requestedName && p.Online === true
|
|
9572
|
+
);
|
|
9573
|
+
if (candidates.length === 0) return null;
|
|
9574
|
+
candidates.sort((a, b) => {
|
|
9575
|
+
const ac = a.Created === void 0 ? "" : a.Created;
|
|
9576
|
+
const bc = b.Created === void 0 ? "" : b.Created;
|
|
9577
|
+
return bc.localeCompare(ac);
|
|
9578
|
+
});
|
|
9579
|
+
const peer = candidates[0];
|
|
9580
|
+
const dnsRaw = peer.DNSName === void 0 ? "" : peer.DNSName;
|
|
9581
|
+
const dns = dnsRaw.replace(/\.$/, "");
|
|
9582
|
+
if (!dns) return null;
|
|
9583
|
+
const shortName = dns.split(".")[0];
|
|
9584
|
+
const ips = peer.TailscaleIPs === void 0 ? [] : peer.TailscaleIPs;
|
|
9585
|
+
const ipv4 = ips.find((ip) => /^\d+\.\d+\.\d+\.\d+$/.test(ip));
|
|
9586
|
+
if (!ipv4) return null;
|
|
9587
|
+
const ipv6 = ips.find((ip) => ip.includes(":")) ?? null;
|
|
9588
|
+
return { shortName, magicDnsName: dns, ipv4, ipv6 };
|
|
9589
|
+
}
|
|
9590
|
+
async function discoverNodeWithRetry(requestedName, maxRetries = 30, intervalMs = 2e3) {
|
|
9591
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
9592
|
+
const node = discoverNode(requestedName);
|
|
9593
|
+
if (node) return node;
|
|
9594
|
+
if (i < maxRetries - 1) {
|
|
9595
|
+
await new Promise((resolve12) => setTimeout(resolve12, intervalMs));
|
|
9596
|
+
}
|
|
9597
|
+
}
|
|
9598
|
+
return null;
|
|
9599
|
+
}
|
|
9600
|
+
function isTailscaleAvailable() {
|
|
9601
|
+
return execSafe("tailscale version") !== null;
|
|
9602
|
+
}
|
|
9603
|
+
|
|
9402
9604
|
// src/cli/deploy/templates.ts
|
|
9403
|
-
import { existsSync as
|
|
9605
|
+
import { existsSync as existsSync26 } from "fs";
|
|
9404
9606
|
import { dirname as dirname11, resolve as resolve10 } from "path";
|
|
9405
9607
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
9406
9608
|
function deployRoot() {
|
|
9407
9609
|
const here = dirname11(fileURLToPath4(import.meta.url));
|
|
9408
9610
|
const bundled = resolve10(here, "..", "deploy");
|
|
9409
|
-
if (
|
|
9611
|
+
if (existsSync26(bundled)) return bundled;
|
|
9410
9612
|
const sourceRoot = resolve10(here, "..", "..", "..", "deploy");
|
|
9411
|
-
if (
|
|
9613
|
+
if (existsSync26(sourceRoot)) return sourceRoot;
|
|
9412
9614
|
throw new Error(
|
|
9413
9615
|
`Could not locate deploy/ templates. Looked at:
|
|
9414
9616
|
${bundled}
|
|
@@ -9432,22 +9634,23 @@ async function mintTailscaleKey(opts) {
|
|
|
9432
9634
|
}
|
|
9433
9635
|
if (env.oauthClientId && env.oauthClientSecret) {
|
|
9434
9636
|
if (!env.tag) {
|
|
9435
|
-
throw new Error("Tailscale tag is missing from ~/.sisyphus/deploy/tailscale.env. Re-run `
|
|
9637
|
+
throw new Error("Tailscale tag is missing from ~/.sisyphus/deploy/tailscale.env. Re-run `sis deploy auth tailscale`.");
|
|
9436
9638
|
}
|
|
9437
9639
|
return mintViaOAuth(env.oauthClientId, env.oauthClientSecret, env.tag, opts.hostname);
|
|
9438
9640
|
}
|
|
9439
9641
|
if (env.authKey) {
|
|
9440
9642
|
return env.authKey;
|
|
9441
9643
|
}
|
|
9442
|
-
throw new Error("Tailscale not configured. Run `
|
|
9644
|
+
throw new Error("Tailscale not configured. Run `sis deploy auth tailscale`.");
|
|
9443
9645
|
}
|
|
9444
9646
|
async function firstRunPrompt() {
|
|
9445
9647
|
console.log("");
|
|
9446
9648
|
console.log("Tailscale credentials not configured. Pick one:");
|
|
9447
9649
|
console.log("");
|
|
9448
|
-
console.log(" 1) OAuth client (recommended) \u2014 mints fresh ephemeral keys per box
|
|
9650
|
+
console.log(" 1) OAuth client (recommended) \u2014 mints fresh ephemeral keys per box");
|
|
9651
|
+
console.log(" and auto-cleans stale offline nodes so hostnames don't get suffixed.");
|
|
9449
9652
|
console.log(" Create at https://login.tailscale.com/admin/settings/oauth");
|
|
9450
|
-
console.log(" with
|
|
9653
|
+
console.log(" with scopes `auth_keys:write` + `devices:write` and tag `tag:sisyphus`.");
|
|
9451
9654
|
console.log(" 2) Reusable auth key (simpler) \u2014 paste from");
|
|
9452
9655
|
console.log(" https://login.tailscale.com/admin/settings/keys");
|
|
9453
9656
|
console.log("");
|
|
@@ -9467,8 +9670,16 @@ async function firstRunPrompt() {
|
|
|
9467
9670
|
}
|
|
9468
9671
|
throw new Error(`Invalid choice: ${choice}`);
|
|
9469
9672
|
}
|
|
9470
|
-
async function mintViaOAuth(clientId, clientSecret, tag,
|
|
9673
|
+
async function mintViaOAuth(clientId, clientSecret, tag, hostname2) {
|
|
9471
9674
|
const token = await fetchAccessToken(clientId, clientSecret);
|
|
9675
|
+
try {
|
|
9676
|
+
const removed = await deleteStaleDevicesForHostname(token, hostname2);
|
|
9677
|
+
if (removed > 0) {
|
|
9678
|
+
console.log(`Tailscale: removed ${removed} stale offline node(s) named "${hostname2}".`);
|
|
9679
|
+
}
|
|
9680
|
+
} catch (err) {
|
|
9681
|
+
console.log(`Tailscale: skipped stale-node cleanup (${err.message}). Add 'devices:write' scope to clean up automatically.`);
|
|
9682
|
+
}
|
|
9472
9683
|
const body = {
|
|
9473
9684
|
capabilities: {
|
|
9474
9685
|
devices: {
|
|
@@ -9481,7 +9692,7 @@ async function mintViaOAuth(clientId, clientSecret, tag, hostname) {
|
|
|
9481
9692
|
}
|
|
9482
9693
|
},
|
|
9483
9694
|
expirySeconds: KEY_EXPIRY_SECONDS,
|
|
9484
|
-
description: `sisyphus deploy: ${
|
|
9695
|
+
description: `sisyphus deploy: ${hostname2}`
|
|
9485
9696
|
};
|
|
9486
9697
|
const res = await fetch(`${TS_API}/tailnet/-/keys`, {
|
|
9487
9698
|
method: "POST",
|
|
@@ -9499,6 +9710,32 @@ async function mintViaOAuth(clientId, clientSecret, tag, hostname) {
|
|
|
9499
9710
|
if (!data.key) throw new Error("Tailscale API returned no key.");
|
|
9500
9711
|
return data.key;
|
|
9501
9712
|
}
|
|
9713
|
+
async function deleteStaleDevicesForHostname(token, hostname2) {
|
|
9714
|
+
const res = await fetch(`${TS_API}/tailnet/-/devices`, {
|
|
9715
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
9716
|
+
});
|
|
9717
|
+
if (!res.ok) {
|
|
9718
|
+
throw new Error(`HTTP ${res.status} listing devices`);
|
|
9719
|
+
}
|
|
9720
|
+
const data = await res.json();
|
|
9721
|
+
const STALE_AFTER_MS = 5 * 60 * 1e3;
|
|
9722
|
+
const now = Date.now();
|
|
9723
|
+
const stale = data.devices.filter((d) => {
|
|
9724
|
+
if (d.hostname !== hostname2) return false;
|
|
9725
|
+
const seen = Date.parse(d.lastSeen);
|
|
9726
|
+
if (Number.isNaN(seen)) return false;
|
|
9727
|
+
return now - seen > STALE_AFTER_MS;
|
|
9728
|
+
});
|
|
9729
|
+
let removed = 0;
|
|
9730
|
+
for (const device of stale) {
|
|
9731
|
+
const r = await fetch(`${TS_API}/device/${device.id}`, {
|
|
9732
|
+
method: "DELETE",
|
|
9733
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
9734
|
+
});
|
|
9735
|
+
if (r.ok) removed++;
|
|
9736
|
+
}
|
|
9737
|
+
return removed;
|
|
9738
|
+
}
|
|
9502
9739
|
async function fetchAccessToken(clientId, clientSecret) {
|
|
9503
9740
|
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
9504
9741
|
const res = await fetch(`${TS_API}/oauth/token`, {
|
|
@@ -9527,7 +9764,7 @@ async function authTailscale() {
|
|
|
9527
9764
|
console.log("");
|
|
9528
9765
|
console.log("Verifying credentials...");
|
|
9529
9766
|
await fetchAccessToken(env.oauthClientId, env.oauthClientSecret);
|
|
9530
|
-
console.log("OAuth client verified \u2014 `
|
|
9767
|
+
console.log("OAuth client verified \u2014 `sis deploy <provider> up` will mint ephemeral keys.");
|
|
9531
9768
|
} else {
|
|
9532
9769
|
console.log("");
|
|
9533
9770
|
console.log("Auth key saved. Note: reusable auth keys are less secure than OAuth clients.");
|
|
@@ -9557,14 +9794,14 @@ function ensureTerraformInstalled() {
|
|
|
9557
9794
|
function ensureProviderStateDir(provider) {
|
|
9558
9795
|
ensureDeployDir();
|
|
9559
9796
|
const dir = deployProviderDir(provider);
|
|
9560
|
-
if (!
|
|
9797
|
+
if (!existsSync27(dir)) mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
9561
9798
|
}
|
|
9562
9799
|
function backupState(provider) {
|
|
9563
9800
|
const src = deployStatePath(provider);
|
|
9564
|
-
if (
|
|
9801
|
+
if (existsSync27(src)) copyFileSync2(src, deployStateBackupPath(provider));
|
|
9565
9802
|
}
|
|
9566
9803
|
function readSshPubkey(path) {
|
|
9567
|
-
if (!
|
|
9804
|
+
if (!existsSync27(path)) {
|
|
9568
9805
|
const privateKeyPath = path.replace(/\.pub$/, "");
|
|
9569
9806
|
throw new Error(
|
|
9570
9807
|
`SSH pubkey not found at ${path}. Generate one with:
|
|
@@ -9572,7 +9809,7 @@ function readSshPubkey(path) {
|
|
|
9572
9809
|
or pass --ssh-key <path>.`
|
|
9573
9810
|
);
|
|
9574
9811
|
}
|
|
9575
|
-
return
|
|
9812
|
+
return readFileSync29(path, "utf-8").trim();
|
|
9576
9813
|
}
|
|
9577
9814
|
function readOutputs(provider) {
|
|
9578
9815
|
const result = spawnSync3("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
|
|
@@ -9599,10 +9836,13 @@ function readOutputs(provider) {
|
|
|
9599
9836
|
}
|
|
9600
9837
|
}
|
|
9601
9838
|
function isProvisioned(provider) {
|
|
9602
|
-
if (!
|
|
9839
|
+
if (!existsSync27(deployStatePath(provider))) return false;
|
|
9603
9840
|
return readOutputs(provider) !== null;
|
|
9604
9841
|
}
|
|
9605
9842
|
async function deployUp(provider, opts) {
|
|
9843
|
+
if (isProvisioned(provider) && !await confirmReprovision(provider, opts.yes)) {
|
|
9844
|
+
return;
|
|
9845
|
+
}
|
|
9606
9846
|
const sshPubkey = readSshPubkey(opts.sshKey);
|
|
9607
9847
|
const creds = await loadProviderCreds(provider);
|
|
9608
9848
|
const tsAuthKey = await mintTailscaleKey({ hostname: opts.name });
|
|
@@ -9644,22 +9884,45 @@ async function deployUp(provider, opts) {
|
|
|
9644
9884
|
const outputs = readOutputs(provider);
|
|
9645
9885
|
if (!outputs) {
|
|
9646
9886
|
console.log(`
|
|
9647
|
-
Applied \u2014 but could not parse outputs. Run \`
|
|
9887
|
+
Applied \u2014 but could not parse outputs. Run \`sis deploy ${provider} status\`.`);
|
|
9648
9888
|
return;
|
|
9649
9889
|
}
|
|
9650
9890
|
console.log("");
|
|
9651
9891
|
console.log("Box provisioned. Cloud-init will run for ~3\u20135 minutes before the daemon is reachable.");
|
|
9652
9892
|
console.log("");
|
|
9653
|
-
console.log(` IP:
|
|
9654
|
-
console.log(`
|
|
9655
|
-
|
|
9893
|
+
console.log(` Public IP: ${outputs.ipv4}`);
|
|
9894
|
+
console.log(` Requested hostname: ${outputs.tailscale_hostname}`);
|
|
9895
|
+
if (isTailscaleAvailable()) {
|
|
9896
|
+
process.stdout.write(" Waiting for tailnet join...");
|
|
9897
|
+
const node = await discoverNodeWithRetry(opts.name);
|
|
9898
|
+
if (node) {
|
|
9899
|
+
writeRuntimeState(provider, {
|
|
9900
|
+
tailscaleHostname: node.shortName,
|
|
9901
|
+
tailscaleFqdn: node.magicDnsName,
|
|
9902
|
+
tailscaleIpv4: node.ipv4,
|
|
9903
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9904
|
+
});
|
|
9905
|
+
process.stdout.write(` joined as ${node.shortName} (${node.ipv4})
|
|
9906
|
+
`);
|
|
9907
|
+
if (node.shortName !== opts.name) {
|
|
9908
|
+
console.log(` Note: Tailscale suffixed the hostname because "${opts.name}" was already`);
|
|
9909
|
+
console.log(" claimed by an offline node. Delete the stale node at");
|
|
9910
|
+
console.log(" https://login.tailscale.com/admin/machines to reuse the original name.");
|
|
9911
|
+
}
|
|
9912
|
+
} else {
|
|
9913
|
+
process.stdout.write(" no peer matched after 60s\n");
|
|
9914
|
+
console.log(" (Cloud-init may still be installing Tailscale. Re-run `status` later.)");
|
|
9915
|
+
}
|
|
9916
|
+
} else {
|
|
9917
|
+
console.log(" (Local `tailscale` CLI not on PATH \u2014 skipping tailnet discovery.)");
|
|
9918
|
+
}
|
|
9656
9919
|
console.log("");
|
|
9657
|
-
console.log(` Tail provisioning:
|
|
9658
|
-
console.log(` Verify daemon:
|
|
9920
|
+
console.log(` Tail provisioning: sis deploy ${provider} logs`);
|
|
9921
|
+
console.log(` Verify daemon: sis deploy ${provider} ssh -- sis admin doctor`);
|
|
9659
9922
|
console.log("");
|
|
9660
9923
|
}
|
|
9661
9924
|
async function deployDown(provider, opts) {
|
|
9662
|
-
if (!
|
|
9925
|
+
if (!existsSync27(deployStatePath(provider))) {
|
|
9663
9926
|
console.log(`No ${provider} state found at ${deployStatePath(provider)}. Nothing to destroy.`);
|
|
9664
9927
|
return;
|
|
9665
9928
|
}
|
|
@@ -9690,6 +9953,7 @@ async function deployDown(provider, opts) {
|
|
|
9690
9953
|
creds
|
|
9691
9954
|
);
|
|
9692
9955
|
if (code !== 0) throw new Error(`terraform destroy failed (exit ${code})`);
|
|
9956
|
+
clearRuntimeState(provider);
|
|
9693
9957
|
console.log(`
|
|
9694
9958
|
${provider} box destroyed.`);
|
|
9695
9959
|
}
|
|
@@ -9700,13 +9964,24 @@ function deployStatus(provider) {
|
|
|
9700
9964
|
}
|
|
9701
9965
|
const outputs = readOutputs(provider);
|
|
9702
9966
|
if (!outputs) {
|
|
9703
|
-
console.log(`${provider}: state present but outputs unreadable. Try \`
|
|
9967
|
+
console.log(`${provider}: state present but outputs unreadable. Try \`sis deploy ${provider} up\` to reconcile.`);
|
|
9704
9968
|
return;
|
|
9705
9969
|
}
|
|
9970
|
+
const runtime = readRuntimeState(provider);
|
|
9971
|
+
const effectiveHost = runtime ? runtime.tailscaleHostname : outputs.tailscale_hostname;
|
|
9706
9972
|
console.log(`${provider}: provisioned`);
|
|
9707
|
-
console.log(` IP:
|
|
9708
|
-
console.log(` Tailscale hostname: ${
|
|
9709
|
-
|
|
9973
|
+
console.log(` Public IP: ${outputs.ipv4}`);
|
|
9974
|
+
console.log(` Tailscale hostname: ${effectiveHost}`);
|
|
9975
|
+
if (runtime) {
|
|
9976
|
+
console.log(` Tailscale IPv4: ${runtime.tailscaleIpv4}`);
|
|
9977
|
+
console.log(` MagicDNS FQDN: ${runtime.tailscaleFqdn}`);
|
|
9978
|
+
if (runtime.tailscaleHostname !== outputs.tailscale_hostname) {
|
|
9979
|
+
console.log(` (Requested "${outputs.tailscale_hostname}" but Tailscale assigned "${runtime.tailscaleHostname}".)`);
|
|
9980
|
+
}
|
|
9981
|
+
} else {
|
|
9982
|
+
console.log(" (No tailnet runtime state \u2014 re-run `up` or check that local tailscale is logged in.)");
|
|
9983
|
+
}
|
|
9984
|
+
console.log(` SSH: ssh sisyphus@${effectiveHost}`);
|
|
9710
9985
|
console.log(` Instance type: ${outputs.instance_type}`);
|
|
9711
9986
|
console.log(` ${formatCostLine(provider, outputs.instance_type)}`);
|
|
9712
9987
|
}
|
|
@@ -9717,16 +9992,17 @@ function deployListProviders() {
|
|
|
9717
9992
|
console.log(` ${p.padEnd(10)} ${status}`);
|
|
9718
9993
|
}
|
|
9719
9994
|
}
|
|
9720
|
-
function
|
|
9721
|
-
const
|
|
9722
|
-
if (
|
|
9723
|
-
|
|
9995
|
+
function effectiveSshTarget(provider) {
|
|
9996
|
+
const runtime = readRuntimeState(provider);
|
|
9997
|
+
if (runtime) return `sisyphus@${runtime.tailscaleHostname}`;
|
|
9998
|
+
const outputs = readOutputs(provider);
|
|
9999
|
+
if (!outputs) {
|
|
10000
|
+
throw new Error(`${provider} not provisioned. Run \`sis deploy ${provider} up\`.`);
|
|
9724
10001
|
}
|
|
9725
|
-
return
|
|
10002
|
+
return `sisyphus@${outputs.tailscale_hostname}`;
|
|
9726
10003
|
}
|
|
9727
10004
|
function deploySsh(provider, remoteCmd) {
|
|
9728
|
-
const
|
|
9729
|
-
const target = `sisyphus@${outputs.tailscale_hostname}`;
|
|
10005
|
+
const target = effectiveSshTarget(provider);
|
|
9730
10006
|
const moshAvailable = spawnSync3("mosh", ["--version"], { stdio: "pipe", env: EXEC_ENV }).status === 0;
|
|
9731
10007
|
const bin = moshAvailable && remoteCmd.length === 0 ? "mosh" : "ssh";
|
|
9732
10008
|
const args2 = remoteCmd.length > 0 ? [target, ...remoteCmd] : [target];
|
|
@@ -9734,15 +10010,13 @@ function deploySsh(provider, remoteCmd) {
|
|
|
9734
10010
|
child.on("exit", (code) => process.exit(code === null ? 1 : code));
|
|
9735
10011
|
}
|
|
9736
10012
|
function deployLogs(provider) {
|
|
9737
|
-
const
|
|
9738
|
-
const target = `sisyphus@${outputs.tailscale_hostname}`;
|
|
10013
|
+
const target = effectiveSshTarget(provider);
|
|
9739
10014
|
const remoteCmd = "tail -F -n 200 /var/log/cloud-init-output.log ~/.sisyphus/daemon.log 2>/dev/null";
|
|
9740
10015
|
const child = spawn2("ssh", [target, remoteCmd], { stdio: "inherit", env: EXEC_ENV });
|
|
9741
10016
|
child.on("exit", (code) => process.exit(code === null ? 1 : code));
|
|
9742
10017
|
}
|
|
9743
10018
|
function deployUpdate(provider) {
|
|
9744
|
-
const
|
|
9745
|
-
const target = `sisyphus@${outputs.tailscale_hostname}`;
|
|
10019
|
+
const target = effectiveSshTarget(provider);
|
|
9746
10020
|
const remoteCmd = "sudo npm i -g sisyphi@latest && systemctl --user restart sisyphusd && sisyphusd --version || true";
|
|
9747
10021
|
const child = spawn2("ssh", [target, remoteCmd], { stdio: "inherit", env: EXEC_ENV });
|
|
9748
10022
|
child.on("exit", (code) => process.exit(code === null ? 1 : code));
|
|
@@ -9752,9 +10026,33 @@ async function confirm2(prompt) {
|
|
|
9752
10026
|
const answer = await promptLine2(`${prompt} `, false);
|
|
9753
10027
|
return answer.toLowerCase() === "yes";
|
|
9754
10028
|
}
|
|
10029
|
+
async function confirmReprovision(provider, yes) {
|
|
10030
|
+
const outputs = readOutputs(provider);
|
|
10031
|
+
console.log("");
|
|
10032
|
+
if (outputs) {
|
|
10033
|
+
console.log(`${provider} is already provisioned: "${outputs.tailscale_hostname}" (${outputs.instance_type}, ${outputs.ipv4}).`);
|
|
10034
|
+
} else {
|
|
10035
|
+
console.log(`${provider} state already exists at ${deployStatePath(provider)}.`);
|
|
10036
|
+
}
|
|
10037
|
+
if (provider === "hetzner") {
|
|
10038
|
+
console.log("Re-running `up` on Hetzner will DESTROY and RECREATE the box (user_data is ForceNew).");
|
|
10039
|
+
console.log("All on-box state \u2014 daemon history, sessions, anything not in your repo \u2014 will be lost.");
|
|
10040
|
+
} else {
|
|
10041
|
+
console.log("Re-running `up` on AWS updates user_data in state but does NOT recreate the instance.");
|
|
10042
|
+
console.log("Cloud-init won't re-run on the live box, and the freshly-minted Tailscale key will be wasted.");
|
|
10043
|
+
}
|
|
10044
|
+
console.log("");
|
|
10045
|
+
console.log(`To push a new sisyphus version: sis deploy ${provider} update`);
|
|
10046
|
+
console.log(`To rebuild from scratch: sis deploy ${provider} down && sis deploy ${provider} up`);
|
|
10047
|
+
console.log("");
|
|
10048
|
+
if (yes) return true;
|
|
10049
|
+
const confirmed = await confirm2('Type "yes" to proceed anyway:');
|
|
10050
|
+
if (!confirmed) console.log("Aborted.");
|
|
10051
|
+
return confirmed;
|
|
10052
|
+
}
|
|
9755
10053
|
|
|
9756
10054
|
// src/cli/commands/deploy.ts
|
|
9757
|
-
|
|
10055
|
+
init_creds();
|
|
9758
10056
|
function assertArch(raw) {
|
|
9759
10057
|
if (raw === "arm" || raw === "x86") return raw;
|
|
9760
10058
|
throw new Error(`Invalid --arch: ${raw}. Must be 'arm' or 'x86'.`);
|
|
@@ -9771,7 +10069,8 @@ function resolveUpOptions(provider, raw) {
|
|
|
9771
10069
|
const size = raw.size === void 0 ? null : raw.size;
|
|
9772
10070
|
const withChromium = raw.chromium !== false;
|
|
9773
10071
|
const enableAutoUpdate = raw.autoUpdate !== false;
|
|
9774
|
-
|
|
10072
|
+
const yes = raw.yes === true;
|
|
10073
|
+
return { region, arch, size, sshKey: raw.sshKey, name: raw.name, withChromium, enableAutoUpdate, yes };
|
|
9775
10074
|
}
|
|
9776
10075
|
function registerDeploy(program2) {
|
|
9777
10076
|
const deploy = program2.command("deploy").description("Provision a Tailscale-only sisyphus box on Hetzner or AWS via Terraform.");
|
|
@@ -9788,7 +10087,7 @@ function registerDeploy(program2) {
|
|
|
9788
10087
|
});
|
|
9789
10088
|
for (const provider of PROVIDERS) {
|
|
9790
10089
|
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) => {
|
|
10090
|
+
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
10091
|
const opts = resolveUpOptions(provider, raw);
|
|
9793
10092
|
await deployUp(provider, opts);
|
|
9794
10093
|
});
|
|
@@ -9810,6 +10109,374 @@ function registerDeploy(program2) {
|
|
|
9810
10109
|
}
|
|
9811
10110
|
}
|
|
9812
10111
|
|
|
10112
|
+
// src/cli/cloud/runner.ts
|
|
10113
|
+
init_paths();
|
|
10114
|
+
import { spawn as spawn4 } from "child_process";
|
|
10115
|
+
import { hostname } from "os";
|
|
10116
|
+
init_creds();
|
|
10117
|
+
|
|
10118
|
+
// src/cli/deploy/ssh-exec.ts
|
|
10119
|
+
import { spawn as spawn3, spawnSync as spawnSync4 } from "child_process";
|
|
10120
|
+
function runOnBox(provider, cmd) {
|
|
10121
|
+
const target = effectiveSshTarget(provider);
|
|
10122
|
+
const result = spawnSync4("ssh", [target, cmd], {
|
|
10123
|
+
encoding: "utf-8",
|
|
10124
|
+
env: EXEC_ENV
|
|
10125
|
+
});
|
|
10126
|
+
if (typeof result.stdout !== "string" || typeof result.stderr !== "string") {
|
|
10127
|
+
throw new Error("Internal: ssh spawn did not capture output as string");
|
|
10128
|
+
}
|
|
10129
|
+
return {
|
|
10130
|
+
stdout: result.stdout,
|
|
10131
|
+
stderr: result.stderr,
|
|
10132
|
+
// status is null when killed by signal — treat as failure.
|
|
10133
|
+
exitCode: result.status === null ? 1 : result.status
|
|
10134
|
+
};
|
|
10135
|
+
}
|
|
10136
|
+
function runOnBoxStreaming(provider, cmd) {
|
|
10137
|
+
const target = effectiveSshTarget(provider);
|
|
10138
|
+
return new Promise((resolve12, reject) => {
|
|
10139
|
+
const child = spawn3("ssh", [target, cmd], {
|
|
10140
|
+
stdio: "inherit",
|
|
10141
|
+
env: EXEC_ENV
|
|
10142
|
+
});
|
|
10143
|
+
child.on("error", reject);
|
|
10144
|
+
child.on("exit", (code) => resolve12(code === null ? 1 : code));
|
|
10145
|
+
});
|
|
10146
|
+
}
|
|
10147
|
+
|
|
10148
|
+
// src/cli/cloud/grove.ts
|
|
10149
|
+
var GROVE_VERSION = "0.2.13";
|
|
10150
|
+
function ensureGroveInstalled(provider) {
|
|
10151
|
+
const probe = runOnBox(provider, "command -v grove >/dev/null 2>&1");
|
|
10152
|
+
if (probe.exitCode === 0) return;
|
|
10153
|
+
process.stderr.write("Installing grove on box...\n");
|
|
10154
|
+
const install = runOnBox(provider, `sudo npm i -g @crouton-kit/grove@${GROVE_VERSION}`);
|
|
10155
|
+
if (install.exitCode !== 0) {
|
|
10156
|
+
throw new Error(`Failed to install grove: ${install.stderr || install.stdout}`);
|
|
10157
|
+
}
|
|
10158
|
+
}
|
|
10159
|
+
function ensureGroveRegistered(provider, repo, instancePath) {
|
|
10160
|
+
const cmd = `grove register --update --name ${shellQuote(repo)} ${shellQuote(instancePath)}`;
|
|
10161
|
+
const result = runOnBox(provider, cmd);
|
|
10162
|
+
if (result.exitCode !== 0) {
|
|
10163
|
+
throw new Error(`Failed to register grove project ${repo}: ${result.stderr || result.stdout}`);
|
|
10164
|
+
}
|
|
10165
|
+
}
|
|
10166
|
+
|
|
10167
|
+
// src/cli/cloud/repo.ts
|
|
10168
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
10169
|
+
import { existsSync as existsSync28 } from "fs";
|
|
10170
|
+
import { basename as basename6, join as join26 } from "path";
|
|
10171
|
+
function captureGit(args2) {
|
|
10172
|
+
const result = spawnSync5("git", args2, {
|
|
10173
|
+
encoding: "utf-8",
|
|
10174
|
+
env: EXEC_ENV
|
|
10175
|
+
});
|
|
10176
|
+
if (typeof result.stdout !== "string") {
|
|
10177
|
+
throw new Error("Internal: git spawn did not capture stdout as string");
|
|
10178
|
+
}
|
|
10179
|
+
return { stdout: result.stdout.trim(), ok: result.status === 0 };
|
|
10180
|
+
}
|
|
10181
|
+
function inferRepoName() {
|
|
10182
|
+
const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"]);
|
|
10183
|
+
if (!ok) {
|
|
10184
|
+
throw new Error("Not inside a git repository. Run from a repo or pass --name.");
|
|
10185
|
+
}
|
|
10186
|
+
if (!stdout) {
|
|
10187
|
+
throw new Error("git rev-parse returned empty toplevel.");
|
|
10188
|
+
}
|
|
10189
|
+
return basename6(stdout);
|
|
10190
|
+
}
|
|
10191
|
+
function getOriginUrl() {
|
|
10192
|
+
const { stdout, ok } = captureGit(["remote", "get-url", "origin"]);
|
|
10193
|
+
if (!ok) return null;
|
|
10194
|
+
return stdout.length > 0 ? stdout : null;
|
|
10195
|
+
}
|
|
10196
|
+
function getRepoToplevel() {
|
|
10197
|
+
const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"]);
|
|
10198
|
+
if (!ok) {
|
|
10199
|
+
throw new Error("Not inside a git repository.");
|
|
10200
|
+
}
|
|
10201
|
+
return stdout;
|
|
10202
|
+
}
|
|
10203
|
+
var DEFAULT_EXCLUDES = [
|
|
10204
|
+
".sisyphus/",
|
|
10205
|
+
".terraform/",
|
|
10206
|
+
"node_modules/",
|
|
10207
|
+
"dist/",
|
|
10208
|
+
".next/",
|
|
10209
|
+
".turbo/",
|
|
10210
|
+
"coverage/",
|
|
10211
|
+
"tmp/",
|
|
10212
|
+
".git/lfs/",
|
|
10213
|
+
".DS_Store"
|
|
10214
|
+
];
|
|
10215
|
+
function buildRsyncArgs(localDir, remoteTarget) {
|
|
10216
|
+
const src = localDir.endsWith("/") ? localDir : `${localDir}/`;
|
|
10217
|
+
return [
|
|
10218
|
+
"-avz",
|
|
10219
|
+
"--filter=:- .gitignore",
|
|
10220
|
+
...DEFAULT_EXCLUDES.map((e) => `--exclude=${e}`),
|
|
10221
|
+
"-e",
|
|
10222
|
+
"ssh",
|
|
10223
|
+
src,
|
|
10224
|
+
remoteTarget
|
|
10225
|
+
];
|
|
10226
|
+
}
|
|
10227
|
+
function detectPackageManager(toplevel) {
|
|
10228
|
+
if (existsSync28(join26(toplevel, "pnpm-lock.yaml"))) return "pnpm";
|
|
10229
|
+
if (existsSync28(join26(toplevel, "bun.lockb"))) return "bun";
|
|
10230
|
+
if (existsSync28(join26(toplevel, "yarn.lock"))) return "yarn";
|
|
10231
|
+
if (existsSync28(join26(toplevel, "package-lock.json"))) return "npm";
|
|
10232
|
+
return null;
|
|
10233
|
+
}
|
|
10234
|
+
function packageManagerInstallCmd(pm) {
|
|
10235
|
+
switch (pm) {
|
|
10236
|
+
case "pnpm":
|
|
10237
|
+
return "pnpm install";
|
|
10238
|
+
case "bun":
|
|
10239
|
+
return "bun install";
|
|
10240
|
+
case "yarn":
|
|
10241
|
+
return "yarn install";
|
|
10242
|
+
case "npm":
|
|
10243
|
+
return "npm install";
|
|
10244
|
+
default:
|
|
10245
|
+
return null;
|
|
10246
|
+
}
|
|
10247
|
+
}
|
|
10248
|
+
|
|
10249
|
+
// src/cli/cloud/sidecar.ts
|
|
10250
|
+
init_paths();
|
|
10251
|
+
function readSidecar(provider, repo) {
|
|
10252
|
+
const path = boxCloudSidecarPath(repo);
|
|
10253
|
+
const result = runOnBox(provider, `cat ${shellQuote(path)} 2>/dev/null`);
|
|
10254
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) return null;
|
|
10255
|
+
try {
|
|
10256
|
+
const parsed = JSON.parse(result.stdout);
|
|
10257
|
+
return parsed;
|
|
10258
|
+
} catch {
|
|
10259
|
+
return null;
|
|
10260
|
+
}
|
|
10261
|
+
}
|
|
10262
|
+
function writeSidecar(provider, repo, data) {
|
|
10263
|
+
const dir = boxCloudSidecarDir();
|
|
10264
|
+
const path = boxCloudSidecarPath(repo);
|
|
10265
|
+
const json = JSON.stringify(data, null, 2);
|
|
10266
|
+
const cmd = [
|
|
10267
|
+
`mkdir -p ${shellQuote(dir)}`,
|
|
10268
|
+
`cat > ${shellQuote(path)} <<'SISYPHUS_CLOUD_SIDECAR_EOF'`,
|
|
10269
|
+
json,
|
|
10270
|
+
"SISYPHUS_CLOUD_SIDECAR_EOF"
|
|
10271
|
+
].join("\n");
|
|
10272
|
+
const result = runOnBox(provider, cmd);
|
|
10273
|
+
if (result.exitCode !== 0) {
|
|
10274
|
+
throw new Error(`Failed to write sidecar for ${repo}: ${result.stderr || result.stdout}`);
|
|
10275
|
+
}
|
|
10276
|
+
}
|
|
10277
|
+
|
|
10278
|
+
// src/cli/cloud/runner.ts
|
|
10279
|
+
async function cloudSync(provider, repo, opts) {
|
|
10280
|
+
const target = effectiveSshTarget(provider);
|
|
10281
|
+
const remoteDir = boxRepoPath(repo);
|
|
10282
|
+
const localOrigin = getOriginUrl();
|
|
10283
|
+
ensureGroveInstalled(provider);
|
|
10284
|
+
const existing = readSidecar(provider, repo);
|
|
10285
|
+
if (existing && existing.originUrl && localOrigin && existing.originUrl !== localOrigin) {
|
|
10286
|
+
throw new Error(
|
|
10287
|
+
`Repo "${repo}" on the box is registered to a different origin:
|
|
10288
|
+
box: ${existing.originUrl}
|
|
10289
|
+
local: ${localOrigin}
|
|
10290
|
+
Pass --name <slug> to disambiguate, or --fresh to overwrite.`
|
|
10291
|
+
);
|
|
10292
|
+
}
|
|
10293
|
+
if (opts.fresh) {
|
|
10294
|
+
if (!localOrigin) {
|
|
10295
|
+
throw new Error("--fresh requires an `origin` remote on the local repo.");
|
|
10296
|
+
}
|
|
10297
|
+
if (!opts.yes) {
|
|
10298
|
+
console.log(`This will wipe ~/projects/${repo} on the box and re-clone from ${localOrigin}.`);
|
|
10299
|
+
const confirmed = (await promptLine('Continue? Type "yes": ', false)).toLowerCase() === "yes";
|
|
10300
|
+
if (!confirmed) {
|
|
10301
|
+
console.log("Aborted.");
|
|
10302
|
+
return;
|
|
10303
|
+
}
|
|
10304
|
+
}
|
|
10305
|
+
console.log(`\u2192 wiping ${remoteDir} and cloning ${localOrigin} on box...`);
|
|
10306
|
+
const cloneCmd = [
|
|
10307
|
+
`rm -rf ${shellQuote(remoteDir)}`,
|
|
10308
|
+
`mkdir -p ${shellQuote("~/projects")}`,
|
|
10309
|
+
`git clone ${shellQuote(localOrigin)} ${shellQuote(remoteDir)}`
|
|
10310
|
+
].join(" && ");
|
|
10311
|
+
const code = await runOnBoxStreaming(provider, cloneCmd);
|
|
10312
|
+
if (code !== 0) throw new Error(`fresh clone failed (exit ${code})`);
|
|
10313
|
+
} else {
|
|
10314
|
+
const mkdir = runOnBox(provider, `mkdir -p ${shellQuote(remoteDir)}`);
|
|
10315
|
+
if (mkdir.exitCode !== 0) {
|
|
10316
|
+
throw new Error(`Failed to mkdir on box: ${mkdir.stderr}`);
|
|
10317
|
+
}
|
|
10318
|
+
const toplevel = getRepoToplevel();
|
|
10319
|
+
const args2 = buildRsyncArgs(toplevel, `${target}:${remoteDir}/`);
|
|
10320
|
+
console.log(`\u2192 rsync ${toplevel}/ \u2192 ${target}:${remoteDir}/`);
|
|
10321
|
+
const code = await runRsync(args2);
|
|
10322
|
+
if (code !== 0) throw new Error(`rsync failed (exit ${code})`);
|
|
10323
|
+
}
|
|
10324
|
+
ensureGroveRegistered(provider, repo, remoteDir);
|
|
10325
|
+
const sidecar = {
|
|
10326
|
+
originUrl: localOrigin,
|
|
10327
|
+
localHostname: hostname(),
|
|
10328
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10329
|
+
packageManager: existing?.packageManager,
|
|
10330
|
+
lastInstall: existing?.lastInstall
|
|
10331
|
+
};
|
|
10332
|
+
writeSidecar(provider, repo, sidecar);
|
|
10333
|
+
console.log(`\u2713 synced ${repo} \u2192 ${target}:${remoteDir}/`);
|
|
10334
|
+
}
|
|
10335
|
+
function runRsync(args2) {
|
|
10336
|
+
return new Promise((resolve12, reject) => {
|
|
10337
|
+
const child = spawn4("rsync", args2, { stdio: "inherit", env: EXEC_ENV });
|
|
10338
|
+
child.on("error", reject);
|
|
10339
|
+
child.on("exit", (code) => resolve12(code === null ? 1 : code));
|
|
10340
|
+
});
|
|
10341
|
+
}
|
|
10342
|
+
async function cloudInstall(provider, repo) {
|
|
10343
|
+
const remoteDir = boxRepoPath(repo);
|
|
10344
|
+
const toplevel = getRepoToplevel();
|
|
10345
|
+
const pm = detectPackageManager(toplevel);
|
|
10346
|
+
const cmd = packageManagerInstallCmd(pm);
|
|
10347
|
+
if (!cmd) {
|
|
10348
|
+
console.log("No lockfile detected \u2014 skipping install.");
|
|
10349
|
+
return;
|
|
10350
|
+
}
|
|
10351
|
+
console.log(`\u2192 ${pm} install in ${remoteDir} on box...`);
|
|
10352
|
+
const remoteCmd = `cd ${shellQuote(remoteDir)} && ${cmd}`;
|
|
10353
|
+
const code = await runOnBoxStreaming(provider, remoteCmd);
|
|
10354
|
+
if (code !== 0) throw new Error(`${pm} install failed (exit ${code})`);
|
|
10355
|
+
const existing = readSidecar(provider, repo);
|
|
10356
|
+
const sidecar = {
|
|
10357
|
+
originUrl: existing && existing.originUrl !== void 0 ? existing.originUrl : getOriginUrl(),
|
|
10358
|
+
localHostname: existing ? existing.localHostname : hostname(),
|
|
10359
|
+
lastSync: existing?.lastSync,
|
|
10360
|
+
lastInstall: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10361
|
+
packageManager: pm
|
|
10362
|
+
};
|
|
10363
|
+
writeSidecar(provider, repo, sidecar);
|
|
10364
|
+
console.log(`\u2713 installed ${repo} (${pm})`);
|
|
10365
|
+
}
|
|
10366
|
+
async function cloudSession(provider, repo) {
|
|
10367
|
+
const remoteDir = boxRepoPath(repo);
|
|
10368
|
+
const cmd = `sis admin home-init ${shellQuote(repo)} ${shellQuote(remoteDir)}`;
|
|
10369
|
+
console.log(`\u2192 initializing tmux home session "${repo}" on box...`);
|
|
10370
|
+
const result = runOnBox(provider, cmd);
|
|
10371
|
+
if (result.exitCode !== 0) {
|
|
10372
|
+
throw new Error(`home-init failed: ${result.stderr || result.stdout}`);
|
|
10373
|
+
}
|
|
10374
|
+
if (result.stdout.trim()) console.log(result.stdout.trim());
|
|
10375
|
+
console.log(`\u2713 session "${repo}" ready on box`);
|
|
10376
|
+
}
|
|
10377
|
+
function cloudAttach(provider, repo) {
|
|
10378
|
+
if (process.env.TMUX) {
|
|
10379
|
+
throw new Error(
|
|
10380
|
+
`Refusing to attach from inside tmux \u2014 would nest the cloud tmux client.
|
|
10381
|
+
Use a fresh terminal, or run from outside tmux:
|
|
10382
|
+
tmux new-window 'ssh -t ${effectiveSshTarget(provider)} tmux attach -t ${repo}'`
|
|
10383
|
+
);
|
|
10384
|
+
}
|
|
10385
|
+
const target = effectiveSshTarget(provider);
|
|
10386
|
+
const child = spawn4("ssh", ["-t", target, `tmux attach-session -t ${shellQuote(repo)}`], {
|
|
10387
|
+
stdio: "inherit",
|
|
10388
|
+
env: EXEC_ENV
|
|
10389
|
+
});
|
|
10390
|
+
child.on("exit", (code) => process.exit(code === null ? 1 : code));
|
|
10391
|
+
}
|
|
10392
|
+
async function cloudStart(provider, repo, opts) {
|
|
10393
|
+
await cloudSync(provider, repo, { fresh: opts.fresh, yes: opts.yes });
|
|
10394
|
+
await cloudInstall(provider, repo);
|
|
10395
|
+
await cloudSession(provider, repo);
|
|
10396
|
+
console.log("");
|
|
10397
|
+
console.log(`Box-side dashboard ready. Attach with:`);
|
|
10398
|
+
console.log(` tmux new-window 'ssh -t ${effectiveSshTarget(provider)} tmux attach -t ${repo}'`);
|
|
10399
|
+
console.log("(or run inside the slash command, which does this for you.)");
|
|
10400
|
+
}
|
|
10401
|
+
function cloudStatus(provider, repo) {
|
|
10402
|
+
const target = effectiveSshTarget(provider);
|
|
10403
|
+
const sidecar = readSidecar(provider, repo);
|
|
10404
|
+
const sessionProbe = runOnBox(provider, `tmux has-session -t ${shellQuote(repo)} 2>/dev/null`);
|
|
10405
|
+
const sessionRunning = sessionProbe.exitCode === 0;
|
|
10406
|
+
console.log(`Cloud status for "${repo}":`);
|
|
10407
|
+
console.log(` Provider: ${provider}`);
|
|
10408
|
+
console.log(` Target: ${target}`);
|
|
10409
|
+
console.log(` Planted: ${sidecar ? "yes" : "no"}`);
|
|
10410
|
+
if (sidecar) {
|
|
10411
|
+
console.log(` Origin: ${sidecar.originUrl ? sidecar.originUrl : "(none)"}`);
|
|
10412
|
+
console.log(` Last sync: ${sidecar.lastSync ? sidecar.lastSync : "(never)"}`);
|
|
10413
|
+
console.log(` Last install: ${sidecar.lastInstall ? sidecar.lastInstall : "(never)"}`);
|
|
10414
|
+
console.log(` Package manager: ${sidecar.packageManager ? sidecar.packageManager : "(none)"}`);
|
|
10415
|
+
}
|
|
10416
|
+
console.log(` Session: ${sessionRunning ? "running" : "absent"}`);
|
|
10417
|
+
if (sessionRunning) {
|
|
10418
|
+
console.log(` Attach: tmux new-window 'ssh -t ${target} tmux attach -t ${repo}'`);
|
|
10419
|
+
}
|
|
10420
|
+
}
|
|
10421
|
+
|
|
10422
|
+
// src/cli/deploy/provider-pick.ts
|
|
10423
|
+
init_creds();
|
|
10424
|
+
function pickProvider(explicit) {
|
|
10425
|
+
if (explicit) {
|
|
10426
|
+
if (!isValidProvider(explicit)) {
|
|
10427
|
+
throw new Error(`Unknown provider "${explicit}". Valid: ${PROVIDERS.join(", ")}.`);
|
|
10428
|
+
}
|
|
10429
|
+
return explicit;
|
|
10430
|
+
}
|
|
10431
|
+
const provisioned = PROVIDERS.filter((p) => isProvisioned(p));
|
|
10432
|
+
if (provisioned.length === 1) return provisioned[0];
|
|
10433
|
+
if (provisioned.length === 0) {
|
|
10434
|
+
throw new Error(
|
|
10435
|
+
"No cloud provider provisioned. Run `sis deploy <hetzner|aws> up` first."
|
|
10436
|
+
);
|
|
10437
|
+
}
|
|
10438
|
+
throw new Error(
|
|
10439
|
+
`Multiple providers provisioned (${provisioned.join(", ")}). Pass --provider <name>.`
|
|
10440
|
+
);
|
|
10441
|
+
}
|
|
10442
|
+
|
|
10443
|
+
// src/cli/commands/cloud.ts
|
|
10444
|
+
function resolve11(raw) {
|
|
10445
|
+
const provider = pickProvider(raw.provider);
|
|
10446
|
+
const repo = raw.name ? raw.name : inferRepoName();
|
|
10447
|
+
if (!validateRepoName(repo)) {
|
|
10448
|
+
throw new Error(`Invalid --name "${repo}": must not contain '/' '\\' or '..'.`);
|
|
10449
|
+
}
|
|
10450
|
+
return { provider, repo };
|
|
10451
|
+
}
|
|
10452
|
+
function registerCloud(program2) {
|
|
10453
|
+
const cloud = program2.command("cloud").description("Per-repo workflow on the shared cloud box (sync, install, dashboard).");
|
|
10454
|
+
cloud.command("sync").description("Rsync this repo to the cloud box; ensures grove is installed and the repo is registered.").option("--fresh", "Wipe the box-side dir and `git clone` from origin instead of rsync.").option("-y, --yes", "Skip the --fresh confirmation prompt.").option("--name <repo>", "Override the repo name (default: basename of git toplevel).").option("--provider <name>", "Cloud provider (default: auto-pick if exactly one is provisioned).").action(async (raw) => {
|
|
10455
|
+
const { provider, repo } = resolve11(raw);
|
|
10456
|
+
await cloudSync(provider, repo, { fresh: raw.fresh === true, yes: raw.yes === true });
|
|
10457
|
+
});
|
|
10458
|
+
cloud.command("install").description("Run the repo's package-manager install on the box.").option("--name <repo>", "Override the repo name.").option("--provider <name>", "Cloud provider.").action(async (raw) => {
|
|
10459
|
+
const { provider, repo } = resolve11(raw);
|
|
10460
|
+
await cloudInstall(provider, repo);
|
|
10461
|
+
});
|
|
10462
|
+
cloud.command("session").description("Create or refresh the box-side tmux home session for this repo.").option("--name <repo>", "Override the repo name.").option("--provider <name>", "Cloud provider.").action(async (raw) => {
|
|
10463
|
+
const { provider, repo } = resolve11(raw);
|
|
10464
|
+
await cloudSession(provider, repo);
|
|
10465
|
+
});
|
|
10466
|
+
cloud.command("attach").description("Attach to the box-side tmux home session for this repo.").option("--name <repo>", "Override the repo name.").option("--provider <name>", "Cloud provider.").action((raw) => {
|
|
10467
|
+
const { provider, repo } = resolve11(raw);
|
|
10468
|
+
cloudAttach(provider, repo);
|
|
10469
|
+
});
|
|
10470
|
+
cloud.command("start").description("Sync, install, and start the dashboard session in one shot. (Stops short of attach.)").option("--fresh", "Wipe the box-side dir and `git clone` from origin instead of rsync.").option("-y, --yes", "Skip the --fresh confirmation prompt.").option("--name <repo>", "Override the repo name.").option("--provider <name>", "Cloud provider.").action(async (raw) => {
|
|
10471
|
+
const { provider, repo } = resolve11(raw);
|
|
10472
|
+
await cloudStart(provider, repo, { fresh: raw.fresh === true, yes: raw.yes === true });
|
|
10473
|
+
});
|
|
10474
|
+
cloud.command("status").description("Print box-side status for this repo (planted, session running, last sync/install).").option("--name <repo>", "Override the repo name.").option("--provider <name>", "Cloud provider.").action((raw) => {
|
|
10475
|
+
const { provider, repo } = resolve11(raw);
|
|
10476
|
+
cloudStatus(provider, repo);
|
|
10477
|
+
});
|
|
10478
|
+
}
|
|
10479
|
+
|
|
9813
10480
|
// src/cli/commands/notify.ts
|
|
9814
10481
|
function attachNotify(diagnostic2) {
|
|
9815
10482
|
const notify = diagnostic2.command("notify").description("Internal notifications (fire-and-forget)");
|
|
@@ -9823,11 +10490,11 @@ function attachNotify(diagnostic2) {
|
|
|
9823
10490
|
}
|
|
9824
10491
|
|
|
9825
10492
|
// src/cli/commands/tmux-status.ts
|
|
9826
|
-
import { execSync as
|
|
10493
|
+
import { execSync as execSync15 } from "child_process";
|
|
9827
10494
|
function attachTmuxStatus(diagnostic2) {
|
|
9828
10495
|
diagnostic2.command("tmux-status").description("Output session status dots for tmux status bar").action(() => {
|
|
9829
10496
|
try {
|
|
9830
|
-
const status =
|
|
10497
|
+
const status = execSync15(
|
|
9831
10498
|
"tmux show-option -gv @sisyphus_status",
|
|
9832
10499
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
9833
10500
|
).trim();
|
|
@@ -9839,8 +10506,8 @@ function attachTmuxStatus(diagnostic2) {
|
|
|
9839
10506
|
|
|
9840
10507
|
// src/cli/commands/tmux-sessions.ts
|
|
9841
10508
|
init_paths();
|
|
9842
|
-
import { execSync as
|
|
9843
|
-
import { readFileSync as
|
|
10509
|
+
import { execSync as execSync16 } from "child_process";
|
|
10510
|
+
import { readFileSync as readFileSync30, existsSync as existsSync29 } from "fs";
|
|
9844
10511
|
var DOT_MAP = {
|
|
9845
10512
|
"orchestrator:processing": { icon: "\u25CF", color: "#d4ad6a" },
|
|
9846
10513
|
"orchestrator:idle": { icon: "\u25CF", color: "#d47766" },
|
|
@@ -9851,16 +10518,16 @@ var DOT_MAP = {
|
|
|
9851
10518
|
};
|
|
9852
10519
|
function readManifest() {
|
|
9853
10520
|
const p = sessionsManifestPath();
|
|
9854
|
-
if (!
|
|
10521
|
+
if (!existsSync29(p)) return null;
|
|
9855
10522
|
try {
|
|
9856
|
-
return JSON.parse(
|
|
10523
|
+
return JSON.parse(readFileSync30(p, "utf-8"));
|
|
9857
10524
|
} catch {
|
|
9858
10525
|
return null;
|
|
9859
10526
|
}
|
|
9860
10527
|
}
|
|
9861
10528
|
function tmuxExec(cmd) {
|
|
9862
10529
|
try {
|
|
9863
|
-
return
|
|
10530
|
+
return execSync16(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
9864
10531
|
} catch {
|
|
9865
10532
|
return null;
|
|
9866
10533
|
}
|
|
@@ -9897,9 +10564,9 @@ if (nodeVersion < 22) {
|
|
|
9897
10564
|
process.exit(1);
|
|
9898
10565
|
}
|
|
9899
10566
|
var program = new Command();
|
|
9900
|
-
program.name("
|
|
10567
|
+
program.name("sis").description("tmux-integrated orchestration daemon for Claude Code").version(
|
|
9901
10568
|
JSON.parse(
|
|
9902
|
-
|
|
10569
|
+
readFileSync31(join27(dirname12(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf-8")
|
|
9903
10570
|
).version
|
|
9904
10571
|
);
|
|
9905
10572
|
program.configureHelp({
|
|
@@ -9940,6 +10607,7 @@ registerSegmentUnregister(segment);
|
|
|
9940
10607
|
var admin = program.command("admin").description("Admin / setup commands");
|
|
9941
10608
|
registerSetup(admin);
|
|
9942
10609
|
registerSetupKeybind(admin);
|
|
10610
|
+
registerHomeInit(admin);
|
|
9943
10611
|
registerDoctor(admin);
|
|
9944
10612
|
registerInit(admin);
|
|
9945
10613
|
registerUninstall(admin);
|
|
@@ -9952,27 +10620,28 @@ registerScratch(admin);
|
|
|
9952
10620
|
registerReview(admin);
|
|
9953
10621
|
registerCompanion(program);
|
|
9954
10622
|
registerDeploy(program);
|
|
10623
|
+
registerCloud(program);
|
|
9955
10624
|
var diagnostic = program.command("diagnostic", { hidden: true });
|
|
9956
10625
|
attachNotify(diagnostic);
|
|
9957
10626
|
attachTmuxStatus(diagnostic);
|
|
9958
10627
|
attachTmuxSessions(diagnostic);
|
|
9959
10628
|
program.addHelpText("after", `
|
|
9960
10629
|
Examples:
|
|
9961
|
-
$
|
|
9962
|
-
$
|
|
9963
|
-
$
|
|
9964
|
-
$
|
|
9965
|
-
$
|
|
10630
|
+
$ sis start "Implement auth system" Start a new session
|
|
10631
|
+
$ sis start "Build @reqs.md" -n auth Start with name + requirements
|
|
10632
|
+
$ sis status Check current sessions
|
|
10633
|
+
$ sis dashboard Open the TUI
|
|
10634
|
+
$ sis admin doctor Verify installation
|
|
9966
10635
|
|
|
9967
|
-
Run '
|
|
10636
|
+
Run 'sis admin getting-started' for a complete usage guide.
|
|
9968
10637
|
`);
|
|
9969
10638
|
var args = process.argv.slice(2);
|
|
9970
10639
|
var firstArg = args[0];
|
|
9971
10640
|
var skipWelcome = ["admin", "help", "--help", "-h", "--version", "-V"];
|
|
9972
|
-
if (!
|
|
10641
|
+
if (!existsSync30(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
|
|
9973
10642
|
mkdirSync14(globalDir(), { recursive: true });
|
|
9974
10643
|
console.log("");
|
|
9975
|
-
console.log(" Welcome to Sisyphus. Run '
|
|
10644
|
+
console.log(" Welcome to Sisyphus. Run 'sis admin setup' to get started.");
|
|
9976
10645
|
console.log("");
|
|
9977
10646
|
}
|
|
9978
10647
|
program.parseAsync(process.argv).catch((err) => {
|