@sleds/cli 0.1.40 → 0.1.41
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 +1 -1
- package/dist/sleds.mjs +103 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ The flags `sleds route run` and `sleds engine` share:
|
|
|
87
87
|
--repo <path> | --repos-dir <dir> where the node's repo is checked out
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
`sleds engine` also takes `--name <n>` (default the hostname; it signs as `engine:<n>`), `--interval <s>`, `--lease <min>`, `--idle <min>` (default 20: a session showing nothing new and no PR is parked for a person, its last message the question, and the answer is typed back into it) and `--once` for a single tick. `--jira` reads `JIRA_BASE_URL`, `JIRA_EMAIL` and `JIRA_TOKEN
|
|
90
|
+
`sleds engine` also takes `--name <n>` (default the hostname; it signs as `engine:<n>`), `--interval <s>`, `--lease <min>`, `--idle <min>` (default 20: a session showing nothing new and no PR is parked for a person, its last message the question, and the answer is typed back into it) and `--once` for a single tick. `--jira` reads `JIRA_BASE_URL`, `JIRA_EMAIL` and `JIRA_TOKEN`, and still needs a seat: before every write to Jira (a status move, a comment) it asks SLEDS, and writes nothing when the org runs Jira read only, the switch is off, the issue's project is not watched, SLEDS cannot be reached, or `JIRA_WRITES=off` is set in its environment (spec 39 rule 4). It will not start when SLEDS says no writes for the epic's project. `sleds help` has the full list.
|
|
91
91
|
|
|
92
92
|
`sleds engine status [--name <n>] [--json]` says what that engine holds now: the nodes it runs (with where to watch each), the PRs it follows and their checks, the next check, and whether it is alive. It reads the engine's state file and the lock the engine keeps beside it (`<name>.lock`: its pid, written each tick, removed on a clean stop), not SLEDS, so it answers offline. `--json` is the contract a front end reads; both it and the state file carry a version, `v` (spec 28 §4.2). For a `sleds route run` engine, pass `--name <n>-<KEY>`.
|
|
93
93
|
|
package/dist/sleds.mjs
CHANGED
|
@@ -10422,7 +10422,7 @@ function beatBody(state, flags, host = {}, cap = {}) {
|
|
|
10422
10422
|
name: flags.name,
|
|
10423
10423
|
os: host.os ?? process.platform,
|
|
10424
10424
|
runner: flags.runner,
|
|
10425
|
-
version: host.version ?? "0.1.
|
|
10425
|
+
version: host.version ?? "0.1.41" ?? "dev",
|
|
10426
10426
|
running: live.length,
|
|
10427
10427
|
items: live.slice(0, BEAT_ITEMS).map((r) => `${r.item}/${r.node}`),
|
|
10428
10428
|
...cap.atOnce !== undefined ? { atOnce: cap.atOnce } : {},
|
|
@@ -11025,6 +11025,29 @@ function adfComment(text) {
|
|
|
11025
11025
|
}
|
|
11026
11026
|
};
|
|
11027
11027
|
}
|
|
11028
|
+
function localWritesOff(env) {
|
|
11029
|
+
return env.JIRA_WRITES?.trim().toLowerCase() === "off";
|
|
11030
|
+
}
|
|
11031
|
+
function sledsJiraGate(client, env = process.env) {
|
|
11032
|
+
return {
|
|
11033
|
+
async ask(w) {
|
|
11034
|
+
if (localWritesOff(env))
|
|
11035
|
+
return "JIRA_WRITES=off is set here";
|
|
11036
|
+
const r = await client.postFull("/proxy/jira/write", { ...w }).catch(() => null);
|
|
11037
|
+
if (!r)
|
|
11038
|
+
return "SLEDS could not be reached to say whether writes to Jira are on";
|
|
11039
|
+
if (r.status !== 200 || !r.json)
|
|
11040
|
+
return `SLEDS would not say whether writes to Jira are on (HTTP ${r.status})`;
|
|
11041
|
+
const refusal = r.json.refusal;
|
|
11042
|
+
if (refusal === null)
|
|
11043
|
+
return null;
|
|
11044
|
+
return str6(r.json.why) ?? (typeof refusal === "string" ? refusal : "SLEDS refused the write");
|
|
11045
|
+
},
|
|
11046
|
+
async told(w, outcome) {
|
|
11047
|
+
await client.postFull("/proxy/jira/write", { ...w, outcome }).catch(() => null);
|
|
11048
|
+
}
|
|
11049
|
+
};
|
|
11050
|
+
}
|
|
11028
11051
|
function jiraSource(o) {
|
|
11029
11052
|
const fetchFn = o.fetchFn ?? fetch;
|
|
11030
11053
|
const base = o.baseUrl.replace(/\/+$/, "");
|
|
@@ -11065,23 +11088,35 @@ function jiraSource(o) {
|
|
|
11065
11088
|
byNode.set(n.node, n);
|
|
11066
11089
|
return [epicItem(epic, nodes)];
|
|
11067
11090
|
};
|
|
11091
|
+
const write = async (issue, sw, what, body) => {
|
|
11092
|
+
const w = {
|
|
11093
|
+
issueKey: issue,
|
|
11094
|
+
switch: sw,
|
|
11095
|
+
method: "POST",
|
|
11096
|
+
endpoint: `/issue/{key}/${what}`
|
|
11097
|
+
};
|
|
11098
|
+
const refused = await o.gate.ask(w).catch((e) => `SLEDS could not be reached (${e instanceof Error ? e.message : String(e)})`);
|
|
11099
|
+
if (refused)
|
|
11100
|
+
return { ok: false, status: 0, json: null, refused };
|
|
11101
|
+
const r = await call(`/rest/api/3/issue/${issue}/${what}`, { method: "POST", body });
|
|
11102
|
+
await o.gate.told(w, r.ok ? "sent" : `failed:${r.status}`).catch(() => {
|
|
11103
|
+
return;
|
|
11104
|
+
});
|
|
11105
|
+
return r;
|
|
11106
|
+
};
|
|
11068
11107
|
const transitionTo = async (issue, category) => {
|
|
11069
11108
|
const t = await call(`/rest/api/3/issue/${issue}/transitions`);
|
|
11070
11109
|
const list = Array.isArray(t.json?.transitions) ? t.json.transitions.filter(isObject8) : [];
|
|
11071
11110
|
const hit = list.find((x) => isObject8(x.to) && categoryOf({ status: x.to }) === category && x.id !== undefined);
|
|
11072
11111
|
if (!hit)
|
|
11073
|
-
return false;
|
|
11074
|
-
const r = await
|
|
11075
|
-
|
|
11076
|
-
body: { transition: { id: String(hit.id) } }
|
|
11077
|
-
});
|
|
11078
|
-
return r.ok;
|
|
11112
|
+
return { ok: false };
|
|
11113
|
+
const r = await write(issue, "status", "transitions", { transition: { id: String(hit.id) } });
|
|
11114
|
+
return { ok: r.ok, ...r.refused ? { refused: r.refused } : {} };
|
|
11079
11115
|
};
|
|
11080
11116
|
const comment = async (issue, text) => {
|
|
11081
|
-
const r = await
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
});
|
|
11117
|
+
const r = await write(issue, "comments", "comment", adfComment(text));
|
|
11118
|
+
if (r.refused)
|
|
11119
|
+
throw new Error(`Jira: not written to ${issue} — ${r.refused}`);
|
|
11085
11120
|
if (!r.ok)
|
|
11086
11121
|
throw new Error(`Jira: could not comment on ${issue} — ${errorOf(r)}`);
|
|
11087
11122
|
};
|
|
@@ -11107,8 +11142,16 @@ function jiraSource(o) {
|
|
|
11107
11142
|
const pack = `# ${n.issue} · ${n.summary}
|
|
11108
11143
|
|
|
11109
11144
|
${body}`.trimEnd();
|
|
11110
|
-
const moved = await transitionTo(n.issue, "indeterminate").catch(() =>
|
|
11111
|
-
|
|
11145
|
+
const moved = await transitionTo(n.issue, "indeterminate").catch(() => ({
|
|
11146
|
+
ok: false,
|
|
11147
|
+
refused: undefined
|
|
11148
|
+
}));
|
|
11149
|
+
if (moved.refused)
|
|
11150
|
+
return {
|
|
11151
|
+
ok: false,
|
|
11152
|
+
error: `not taken: not written to Jira (${n.issue}) — ${moved.refused}`
|
|
11153
|
+
};
|
|
11154
|
+
if (!moved.ok)
|
|
11112
11155
|
return {
|
|
11113
11156
|
ok: false,
|
|
11114
11157
|
error: `Jira would not move ${n.issue} to In Progress (no such transition, or no permission) — the engine needs it to mark the node taken`
|
|
@@ -11135,8 +11178,8 @@ ${body}`.trimEnd();
|
|
|
11135
11178
|
if (outcome.kind === "pr")
|
|
11136
11179
|
return comment(issue, `PR opened by ${by}: ${outcome.ref}`);
|
|
11137
11180
|
if (outcome.kind === "merged") {
|
|
11138
|
-
const done = await transitionTo(issue, "done").catch(() => false);
|
|
11139
|
-
return comment(issue, done ? `PR merged by ${by}'s engine: ${outcome.ref}` : `PR merged: ${outcome.ref}. Jira would not move this to Done (no such transition, or no permission) — move it so the nodes it blocks can start.`);
|
|
11181
|
+
const done = await transitionTo(issue, "done").catch(() => ({ ok: false }));
|
|
11182
|
+
return comment(issue, done.ok ? `PR merged by ${by}'s engine: ${outcome.ref}` : `PR merged: ${outcome.ref}. Jira would not move this to Done (no such transition, or no permission) — move it so the nodes it blocks can start.`);
|
|
11140
11183
|
}
|
|
11141
11184
|
if (outcome.kind === "park")
|
|
11142
11185
|
return comment(issue, outcome.question);
|
|
@@ -11148,20 +11191,21 @@ ${outcome.log.slice(-JIRA_LOG_MAX)}`);
|
|
|
11148
11191
|
}
|
|
11149
11192
|
};
|
|
11150
11193
|
}
|
|
11151
|
-
function
|
|
11152
|
-
const
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11194
|
+
function jiraEnvError(env) {
|
|
11195
|
+
const missing = ["JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_TOKEN"].filter((k) => !env[k]?.trim());
|
|
11196
|
+
return missing.length > 0 ? `Jira source needs ${missing.join(", ")} (e.g. JIRA_BASE_URL=https://qwick.atlassian.net, an API token in JIRA_TOKEN)` : null;
|
|
11197
|
+
}
|
|
11198
|
+
function jiraFromEnv(epic, gate, env = process.env) {
|
|
11199
|
+
const error = jiraEnvError(env);
|
|
11200
|
+
if (error)
|
|
11201
|
+
return { error };
|
|
11202
|
+
return jiraSource({
|
|
11203
|
+
epic,
|
|
11204
|
+
baseUrl: String(env.JIRA_BASE_URL).trim(),
|
|
11205
|
+
email: String(env.JIRA_EMAIL).trim(),
|
|
11206
|
+
token: String(env.JIRA_TOKEN).trim(),
|
|
11207
|
+
gate
|
|
11208
|
+
});
|
|
11165
11209
|
}
|
|
11166
11210
|
|
|
11167
11211
|
// src/engine/loop.ts
|
|
@@ -16270,7 +16314,7 @@ async function driveEngine(o, flags, deps, until = {}) {
|
|
|
16270
16314
|
runner: deps.runner.name,
|
|
16271
16315
|
atOnce: flags.atOnce,
|
|
16272
16316
|
interval: flags.interval,
|
|
16273
|
-
cliVersion: "0.1.
|
|
16317
|
+
cliVersion: "0.1.41"
|
|
16274
16318
|
};
|
|
16275
16319
|
const beat = (checkedAt) => {
|
|
16276
16320
|
const m = deps.machine;
|
|
@@ -18087,6 +18131,15 @@ function orgClient2(o, flag) {
|
|
|
18087
18131
|
const org = engineOrg2(o, flag);
|
|
18088
18132
|
return engineClient2(o, org.seated ? org.slug ?? undefined : flag);
|
|
18089
18133
|
}
|
|
18134
|
+
async function jiraWritesAtStart(gate, epic) {
|
|
18135
|
+
const no = await gate.ask({
|
|
18136
|
+
issueKey: epic.trim().toUpperCase(),
|
|
18137
|
+
switch: "status",
|
|
18138
|
+
method: "POST",
|
|
18139
|
+
endpoint: "/issue/{key}/transitions"
|
|
18140
|
+
});
|
|
18141
|
+
return no ? `--jira ${epic}: not starting — SLEDS says no writes to Jira: ${no}. A take moves the issue to In Progress, so the engine cannot take a node without one.` : null;
|
|
18142
|
+
}
|
|
18090
18143
|
function realPidAlive2(pid) {
|
|
18091
18144
|
try {
|
|
18092
18145
|
process.kill(pid, 0);
|
|
@@ -18323,7 +18376,7 @@ async function driveEngine2(o, flags, deps, until = {}) {
|
|
|
18323
18376
|
runner: deps.runner.name,
|
|
18324
18377
|
atOnce: flags.atOnce,
|
|
18325
18378
|
interval: flags.interval,
|
|
18326
|
-
cliVersion: "0.1.
|
|
18379
|
+
cliVersion: "0.1.41"
|
|
18327
18380
|
};
|
|
18328
18381
|
const beat = (checkedAt) => {
|
|
18329
18382
|
const m = deps.machine;
|
|
@@ -18574,7 +18627,24 @@ var engineCmd = async (o) => {
|
|
|
18574
18627
|
if (o.source)
|
|
18575
18628
|
source = o.source;
|
|
18576
18629
|
else if (flags.jira) {
|
|
18577
|
-
const
|
|
18630
|
+
const env = o.env ?? process.env;
|
|
18631
|
+
const missing = jiraEnvError(env);
|
|
18632
|
+
if (missing) {
|
|
18633
|
+
error(missing);
|
|
18634
|
+
return 1;
|
|
18635
|
+
}
|
|
18636
|
+
const c = o.client ?? orgClient2(o, given);
|
|
18637
|
+
if ("error" in c) {
|
|
18638
|
+
error(`--jira asks SLEDS before every write to Jira (spec 39), so it needs a seat: ${c.error}`);
|
|
18639
|
+
return 1;
|
|
18640
|
+
}
|
|
18641
|
+
const gate = sledsJiraGate(c, env);
|
|
18642
|
+
const no = await jiraWritesAtStart(gate, flags.jira);
|
|
18643
|
+
if (no) {
|
|
18644
|
+
error(no);
|
|
18645
|
+
return 1;
|
|
18646
|
+
}
|
|
18647
|
+
const j = jiraFromEnv(flags.jira, gate, env);
|
|
18578
18648
|
if ("error" in j) {
|
|
18579
18649
|
error(j.error);
|
|
18580
18650
|
return 1;
|
|
@@ -24783,7 +24853,7 @@ ${renderHelpText(argv[1])}`);
|
|
|
24783
24853
|
case "version":
|
|
24784
24854
|
case "--version":
|
|
24785
24855
|
case "-v":
|
|
24786
|
-
console.log("sleds 0.1.
|
|
24856
|
+
console.log("sleds 0.1.41");
|
|
24787
24857
|
return 0;
|
|
24788
24858
|
default:
|
|
24789
24859
|
console.error(`unknown command: ${cmd}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleds/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"description": "SLEDS CLI — the cost and limits of AI agent work, decided at the wall. Point Claude Code at a SLEDS proxy; estimate before you run; hold, switch model, or run overnight when you hit a limit.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|