synartesis 0.6.12 → 0.6.15
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/CHANGELOG.md +308 -0
- package/README.md +103 -2
- package/dist/{chunk-FUVEHRJP.js → chunk-OODDXM36.js} +376 -66
- package/dist/cli.js +511 -88
- package/dist/proxy.js +39 -9
- package/manifests/filesystem.yaml +8 -0
- package/manifests/git.yaml +8 -0
- package/manifests/github.yaml +10 -0
- package/manifests/memory.yaml +18 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
IDEMPOTENCY_META_KEY,
|
|
3
4
|
NOTHING_RECORDED_YET,
|
|
5
|
+
PROXY_FLAGS,
|
|
4
6
|
WORDMARK,
|
|
5
7
|
banner,
|
|
6
8
|
canonical,
|
|
@@ -8,6 +10,8 @@ import {
|
|
|
8
10
|
connectStdioUpstream,
|
|
9
11
|
createPolicyResolver,
|
|
10
12
|
createRouter,
|
|
13
|
+
describeStanding,
|
|
14
|
+
errorStyle,
|
|
11
15
|
findJournal,
|
|
12
16
|
findManifest,
|
|
13
17
|
labelFor,
|
|
@@ -16,18 +20,23 @@ import {
|
|
|
16
20
|
openJournal,
|
|
17
21
|
parseManifest,
|
|
18
22
|
pathBinaryMatches,
|
|
23
|
+
pinBlock,
|
|
19
24
|
planInverse,
|
|
20
25
|
planRead,
|
|
21
26
|
proxyCommand,
|
|
22
27
|
qualify,
|
|
23
28
|
resolvedRead,
|
|
24
29
|
rule,
|
|
30
|
+
standing,
|
|
25
31
|
style,
|
|
26
32
|
toPayload,
|
|
27
33
|
toResolvedRead,
|
|
34
|
+
toolShapes,
|
|
35
|
+
untested,
|
|
28
36
|
verifyAgainstServers,
|
|
37
|
+
warnUntested,
|
|
29
38
|
wasRefused
|
|
30
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-OODDXM36.js";
|
|
31
40
|
import {
|
|
32
41
|
DriftConflict,
|
|
33
42
|
ManifestError,
|
|
@@ -95,7 +104,14 @@ ${source}
|
|
|
95
104
|
`,
|
|
96
105
|
path
|
|
97
106
|
).tools;
|
|
98
|
-
|
|
107
|
+
const claimed = /^\s*provenance:\s*(live|documented)\s*$/m.exec(text)?.[1];
|
|
108
|
+
return {
|
|
109
|
+
key,
|
|
110
|
+
rules,
|
|
111
|
+
name: hit.manifest,
|
|
112
|
+
source,
|
|
113
|
+
...claimed === "live" || claimed === "documented" ? { provenance: claimed } : {}
|
|
114
|
+
};
|
|
99
115
|
} catch {
|
|
100
116
|
return void 0;
|
|
101
117
|
}
|
|
@@ -254,12 +270,20 @@ async function draftManifest(options) {
|
|
|
254
270
|
`${options.name} is already declared in the manifest; remove it first or choose another name`
|
|
255
271
|
);
|
|
256
272
|
}
|
|
273
|
+
const known = knownPolicyFor(options.command, options.args);
|
|
274
|
+
const claim = known?.provenance === void 0 ? [] : [
|
|
275
|
+
...known.provenance === "documented" ? [
|
|
276
|
+
` # This policy has never been run against the real server. Check it`,
|
|
277
|
+
` # against your own setup before trusting undo on it.`
|
|
278
|
+
] : [],
|
|
279
|
+
` provenance: ${known.provenance}`
|
|
280
|
+
];
|
|
257
281
|
const server = [
|
|
258
282
|
` ${options.name}:`,
|
|
259
283
|
` command: ${quote(options.command)}`,
|
|
260
|
-
` args: [${options.args.map(quote).join(", ")}]
|
|
284
|
+
` args: [${options.args.map(quote).join(", ")}]`,
|
|
285
|
+
...claim
|
|
261
286
|
].join("\n");
|
|
262
|
-
const known = knownPolicyFor(options.command, options.args);
|
|
263
287
|
const adopted = known === void 0 ? void 0 : adopt(known, options.name, tools);
|
|
264
288
|
const policies = adopted?.source ?? tools.map((tool) => draftTool(options.name, tool)).join("\n\n");
|
|
265
289
|
if (existing === void 0) {
|
|
@@ -284,10 +308,24 @@ async function draftManifest(options) {
|
|
|
284
308
|
policies,
|
|
285
309
|
``
|
|
286
310
|
].join("\n");
|
|
287
|
-
return adopted === void 0 || known === void 0 ? { yaml } : {
|
|
311
|
+
return adopted === void 0 || known === void 0 ? { yaml } : {
|
|
312
|
+
yaml,
|
|
313
|
+
adopted: {
|
|
314
|
+
server: known.name,
|
|
315
|
+
tools: adopted.covered,
|
|
316
|
+
...known.provenance === void 0 ? {} : { provenance: known.provenance }
|
|
317
|
+
}
|
|
318
|
+
};
|
|
288
319
|
}
|
|
289
320
|
const merged = mergeInto(existing, server, policies, options.name);
|
|
290
|
-
return adopted === void 0 || known === void 0 ? { yaml: merged } : {
|
|
321
|
+
return adopted === void 0 || known === void 0 ? { yaml: merged } : {
|
|
322
|
+
yaml: merged,
|
|
323
|
+
adopted: {
|
|
324
|
+
server: known.name,
|
|
325
|
+
tools: adopted.covered,
|
|
326
|
+
...known.provenance === void 0 ? {} : { provenance: known.provenance }
|
|
327
|
+
}
|
|
328
|
+
};
|
|
291
329
|
}
|
|
292
330
|
function mergeInto(existing, server, policies, name) {
|
|
293
331
|
const serversAt2 = existing.indexOf("\nservers:");
|
|
@@ -312,7 +350,6 @@ function mergeInto(existing, server, policies, name) {
|
|
|
312
350
|
|
|
313
351
|
// src/rollback/rollback.ts
|
|
314
352
|
import { z as z2 } from "zod";
|
|
315
|
-
var IDEMPOTENCY_META_KEY = "synartesis.dev/idempotency-key";
|
|
316
353
|
var inversePlan = z2.object({
|
|
317
354
|
server: z2.string(),
|
|
318
355
|
tool: z2.string(),
|
|
@@ -331,8 +368,10 @@ function classify(action, replanning, goAhead) {
|
|
|
331
368
|
case "rolled_back":
|
|
332
369
|
return { kind: "already-reverted", reason: "already rolled back", verified: true };
|
|
333
370
|
case "failed":
|
|
334
|
-
case "denied":
|
|
335
|
-
|
|
371
|
+
case "denied": {
|
|
372
|
+
const why = labelFor(action) === "used" ? "never applied: its approval moved to the call that ran" : `never applied (${action.status})`;
|
|
373
|
+
return { kind: "skip", reason: why, verified: true };
|
|
374
|
+
}
|
|
336
375
|
case "pending":
|
|
337
376
|
return {
|
|
338
377
|
kind: "halt",
|
|
@@ -572,7 +611,7 @@ ${seen}` : seen;
|
|
|
572
611
|
};
|
|
573
612
|
}
|
|
574
613
|
function unverifiedBecause(action) {
|
|
575
|
-
return action.verify === void 0 ? "no
|
|
614
|
+
return action.verify === void 0 ? "no read declared for this tool, so drift could not be ruled out -- a `verify` read in its policy would give it one" : "the post-state was never captured, so drift could not be ruled out";
|
|
576
615
|
}
|
|
577
616
|
function describeStep(action) {
|
|
578
617
|
return { seq: action.seq, server: action.server, tool: action.tool };
|
|
@@ -652,7 +691,11 @@ async function inspect(options) {
|
|
|
652
691
|
continue;
|
|
653
692
|
}
|
|
654
693
|
if (SETTLED.has(action.status)) {
|
|
655
|
-
resources.push({
|
|
694
|
+
resources.push({
|
|
695
|
+
...at,
|
|
696
|
+
condition: "not-applied",
|
|
697
|
+
note: labelFor(action) === "used" ? "its approval moved to the call that ran" : action.status
|
|
698
|
+
});
|
|
656
699
|
continue;
|
|
657
700
|
}
|
|
658
701
|
if (action.status === "rolled_back") {
|
|
@@ -745,24 +788,6 @@ function verdict(inspection) {
|
|
|
745
788
|
return "nothing was recorded in this session";
|
|
746
789
|
}
|
|
747
790
|
|
|
748
|
-
// src/watch.ts
|
|
749
|
-
import { existsSync as existsSync2 } from "fs";
|
|
750
|
-
|
|
751
|
-
// src/keys.ts
|
|
752
|
-
var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
|
|
753
|
-
function keysIn(chunk) {
|
|
754
|
-
const keys = [];
|
|
755
|
-
let at = 0;
|
|
756
|
-
while (at < chunk.length) {
|
|
757
|
-
const rest = chunk.slice(at);
|
|
758
|
-
const sequence = rest.startsWith("\x1B") ? SEQUENCE.exec(rest) : null;
|
|
759
|
-
const key = sequence?.[0] ?? rest.slice(0, 1);
|
|
760
|
-
keys.push(key);
|
|
761
|
-
at += key.length;
|
|
762
|
-
}
|
|
763
|
-
return keys;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
791
|
// src/clock.ts
|
|
767
792
|
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
768
793
|
var pad = (value) => String(value).padStart(2, "0");
|
|
@@ -814,6 +839,169 @@ function ago(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
814
839
|
return `${String(Math.round(hours / 24))}d ago`;
|
|
815
840
|
}
|
|
816
841
|
|
|
842
|
+
// src/hints.ts
|
|
843
|
+
function hintsWanted() {
|
|
844
|
+
return process.env["SYNARTESIS_NO_HINTS"] === void 0;
|
|
845
|
+
}
|
|
846
|
+
function hintLine(hint2, paths = "") {
|
|
847
|
+
if (hint2.run === void 0) {
|
|
848
|
+
return ` ${style.quiet(hint2.why)}`;
|
|
849
|
+
}
|
|
850
|
+
return ` ${style.quiet(`${hint2.why}:`)} ${style.strong(`${cliCommand()} ${hint2.run}${paths}`)}`;
|
|
851
|
+
}
|
|
852
|
+
function short(id) {
|
|
853
|
+
return id.slice(0, 8);
|
|
854
|
+
}
|
|
855
|
+
function heldCalls(journal) {
|
|
856
|
+
const waiting = journal.listGated();
|
|
857
|
+
const [first, ...rest] = waiting;
|
|
858
|
+
if (first === void 0) {
|
|
859
|
+
return void 0;
|
|
860
|
+
}
|
|
861
|
+
if (rest.length === 0) {
|
|
862
|
+
return {
|
|
863
|
+
why: `${first.server}.${first.tool} is held, and the agent is waiting on you`,
|
|
864
|
+
run: `approve ${short(first.id)}`,
|
|
865
|
+
needs: ["journal"]
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
why: `${String(waiting.length)} calls are held, and the agent is waiting on you`,
|
|
870
|
+
run: "approve --all",
|
|
871
|
+
needs: ["journal"]
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function whatChanged(journal) {
|
|
875
|
+
const run = journal.newestUndoable();
|
|
876
|
+
if (run === void 0) {
|
|
877
|
+
return void 0;
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
why: `${run.label ?? "an agent"} changed something ${ago(run.startedAt)}`,
|
|
881
|
+
run: `show ${short(run.id)}`,
|
|
882
|
+
needs: ["journal"]
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function notPinned(manifest) {
|
|
886
|
+
const pinned = Object.keys(manifest.pins ?? {});
|
|
887
|
+
const loose = Object.keys(manifest.servers).filter((name) => !pinned.includes(name));
|
|
888
|
+
if (loose.length === 0) {
|
|
889
|
+
return void 0;
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
why: pinned.length === 0 ? "no tool here is pinned, so a server that changes shape keeps its old policy" : `${loose.join(", ")} ${loose.length === 1 ? "is" : "are"} not pinned`,
|
|
893
|
+
run: "pin",
|
|
894
|
+
needs: ["manifest"]
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
var LEAVE_IT_RUNNING = {
|
|
898
|
+
why: "anything an agent holds will appear here as it happens",
|
|
899
|
+
run: "watch",
|
|
900
|
+
needs: ["journal"]
|
|
901
|
+
};
|
|
902
|
+
function afterStatus(journal) {
|
|
903
|
+
if (journal === void 0) {
|
|
904
|
+
return LEAVE_IT_RUNNING;
|
|
905
|
+
}
|
|
906
|
+
return firstOf(
|
|
907
|
+
() => heldCalls(journal),
|
|
908
|
+
() => whatChanged(journal),
|
|
909
|
+
() => LEAVE_IT_RUNNING
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
function firstOf(...candidates2) {
|
|
913
|
+
for (const candidate of candidates2) {
|
|
914
|
+
const hint2 = candidate();
|
|
915
|
+
if (hint2 !== void 0) {
|
|
916
|
+
return hint2;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
function didYouMean(typed, known) {
|
|
922
|
+
if (typed.length < 3) {
|
|
923
|
+
return void 0;
|
|
924
|
+
}
|
|
925
|
+
const ceiling = typed.length <= 4 ? 1 : 2;
|
|
926
|
+
let best;
|
|
927
|
+
let bestAt = ceiling + 1;
|
|
928
|
+
for (const candidate of known) {
|
|
929
|
+
const gap = distance(typed, candidate);
|
|
930
|
+
if (gap < bestAt) {
|
|
931
|
+
best = candidate;
|
|
932
|
+
bestAt = gap;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return bestAt <= ceiling ? best : void 0;
|
|
936
|
+
}
|
|
937
|
+
function distance(from, to) {
|
|
938
|
+
const rows = [];
|
|
939
|
+
for (let i = 0; i <= from.length; i += 1) {
|
|
940
|
+
const row = new Array(to.length + 1).fill(0);
|
|
941
|
+
row[0] = i;
|
|
942
|
+
rows.push(row);
|
|
943
|
+
}
|
|
944
|
+
const top = rows[0];
|
|
945
|
+
for (let j = 0; j <= to.length && top !== void 0; j += 1) {
|
|
946
|
+
top[j] = j;
|
|
947
|
+
}
|
|
948
|
+
for (let i = 1; i <= from.length; i += 1) {
|
|
949
|
+
for (let j = 1; j <= to.length; j += 1) {
|
|
950
|
+
const same = from[i - 1] === to[j - 1];
|
|
951
|
+
let best = Math.min(
|
|
952
|
+
(rows[i - 1]?.[j] ?? 0) + 1,
|
|
953
|
+
(rows[i]?.[j - 1] ?? 0) + 1,
|
|
954
|
+
(rows[i - 1]?.[j - 1] ?? 0) + (same ? 0 : 1)
|
|
955
|
+
);
|
|
956
|
+
if (i > 1 && j > 1 && from[i - 1] === to[j - 2] && from[i - 2] === to[j - 1]) {
|
|
957
|
+
best = Math.min(best, (rows[i - 2]?.[j - 2] ?? 0) + 1);
|
|
958
|
+
}
|
|
959
|
+
const row = rows[i];
|
|
960
|
+
if (row !== void 0) {
|
|
961
|
+
row[j] = best;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return rows[from.length]?.[to.length] ?? to.length;
|
|
966
|
+
}
|
|
967
|
+
var EVERYDAY = [
|
|
968
|
+
["install", "cover the servers your client already lists"],
|
|
969
|
+
["watch", "see what an agent does, and answer what it holds"],
|
|
970
|
+
["list", "the sessions recorded so far"],
|
|
971
|
+
["show", "what one session did, and whether it still holds"],
|
|
972
|
+
["undo", "put back what one of them changed"]
|
|
973
|
+
];
|
|
974
|
+
function shortList() {
|
|
975
|
+
const self = cliCommand();
|
|
976
|
+
return [
|
|
977
|
+
"",
|
|
978
|
+
...EVERYDAY.map(
|
|
979
|
+
([name, said]) => ` ${errorStyle.strong(`${self} ${name.padEnd(8)}`)} ${errorStyle.quiet(said)}`
|
|
980
|
+
),
|
|
981
|
+
"",
|
|
982
|
+
` ${errorStyle.quiet(`${self} --help for every command.`)}`,
|
|
983
|
+
""
|
|
984
|
+
].join("\n");
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/watch.ts
|
|
988
|
+
import { existsSync as existsSync2 } from "fs";
|
|
989
|
+
|
|
990
|
+
// src/keys.ts
|
|
991
|
+
var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
|
|
992
|
+
function keysIn(chunk) {
|
|
993
|
+
const keys = [];
|
|
994
|
+
let at = 0;
|
|
995
|
+
while (at < chunk.length) {
|
|
996
|
+
const rest = chunk.slice(at);
|
|
997
|
+
const sequence = rest.startsWith("\x1B") ? SEQUENCE.exec(rest) : null;
|
|
998
|
+
const key = sequence?.[0] ?? rest.slice(0, 1);
|
|
999
|
+
keys.push(key);
|
|
1000
|
+
at += key.length;
|
|
1001
|
+
}
|
|
1002
|
+
return keys;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
817
1005
|
// src/describe.ts
|
|
818
1006
|
import { basename } from "path";
|
|
819
1007
|
var SUBJECT_KEYS = [
|
|
@@ -881,6 +1069,19 @@ function size(bytes) {
|
|
|
881
1069
|
}
|
|
882
1070
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
883
1071
|
}
|
|
1072
|
+
function summariseValue(value) {
|
|
1073
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
1074
|
+
if (flat.length <= 48) {
|
|
1075
|
+
return flat;
|
|
1076
|
+
}
|
|
1077
|
+
const looksLikeALocation = !value.includes("\n") && /[/\\]/.test(value);
|
|
1078
|
+
if (looksLikeALocation) {
|
|
1079
|
+
const tail = value.slice(-44);
|
|
1080
|
+
const from = tail.search(/[/\\]/);
|
|
1081
|
+
return `\u2026${from === -1 ? tail : tail.slice(from)}`;
|
|
1082
|
+
}
|
|
1083
|
+
return size(Buffer.byteLength(value));
|
|
1084
|
+
}
|
|
884
1085
|
function summariseArgs(args, limit = 60) {
|
|
885
1086
|
if (!isRecord(args)) {
|
|
886
1087
|
return args === void 0 ? "" : JSON.stringify(args);
|
|
@@ -888,7 +1089,7 @@ function summariseArgs(args, limit = 60) {
|
|
|
888
1089
|
const parts = [];
|
|
889
1090
|
for (const [key, value] of Object.entries(args)) {
|
|
890
1091
|
if (typeof value === "string") {
|
|
891
|
-
parts.push(
|
|
1092
|
+
parts.push(`${key} ${summariseValue(value)}`);
|
|
892
1093
|
continue;
|
|
893
1094
|
}
|
|
894
1095
|
if (Array.isArray(value)) {
|
|
@@ -1459,8 +1660,8 @@ function pruneBackups(path) {
|
|
|
1459
1660
|
try {
|
|
1460
1661
|
const dir = dirname(path);
|
|
1461
1662
|
const prefix = `${basename2(path)}.synartesis-backup-`;
|
|
1462
|
-
const
|
|
1463
|
-
for (const name of
|
|
1663
|
+
const ours2 = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
|
|
1664
|
+
for (const name of ours2.slice(0, Math.max(0, ours2.length - KEEP_BACKUPS))) {
|
|
1464
1665
|
rmSync(join(dir, name), { force: true });
|
|
1465
1666
|
}
|
|
1466
1667
|
} catch {
|
|
@@ -1643,7 +1844,11 @@ async function planInstall(sites, manifestPath, invoker) {
|
|
|
1643
1844
|
name,
|
|
1644
1845
|
original: entry,
|
|
1645
1846
|
wrapped: proxyEntry(manifestPath, key, entry, invoker),
|
|
1646
|
-
...draft.adopted === void 0 ? {} : {
|
|
1847
|
+
...draft.adopted === void 0 ? {} : {
|
|
1848
|
+
adopted: draft.adopted.server,
|
|
1849
|
+
tools: draft.adopted.tools,
|
|
1850
|
+
...draft.adopted.provenance === void 0 ? {} : { provenance: draft.adopted.provenance }
|
|
1851
|
+
}
|
|
1647
1852
|
});
|
|
1648
1853
|
}
|
|
1649
1854
|
plans.push({ site, servers: planned, skipped });
|
|
@@ -1877,7 +2082,7 @@ function runsView(journal, screen, options) {
|
|
|
1877
2082
|
return ` ${here ? style.accent(CURSOR) : " "} ${here ? style.accent(name) : style.strong(name)} ${style.quiet(shortTime(run.startedAt).trimEnd().padEnd(13))} ${style.quiet(run.status.padEnd(11))} ${style.quiet(`${String(actions.length)} actions`)}${note}`;
|
|
1878
2083
|
});
|
|
1879
2084
|
}
|
|
1880
|
-
function
|
|
2085
|
+
function standing2(actions) {
|
|
1881
2086
|
let undoable = 0;
|
|
1882
2087
|
let conflicted = 0;
|
|
1883
2088
|
for (const action of actions) {
|
|
@@ -1897,7 +2102,7 @@ function elsewhere(journal, exceptId) {
|
|
|
1897
2102
|
if (run.id === exceptId) {
|
|
1898
2103
|
continue;
|
|
1899
2104
|
}
|
|
1900
|
-
const found =
|
|
2105
|
+
const found = standing2(journal.getActions(run.id));
|
|
1901
2106
|
if (found.undoable > 0 || found.conflicted > 0) {
|
|
1902
2107
|
return run;
|
|
1903
2108
|
}
|
|
@@ -2138,7 +2343,7 @@ async function openConsole(options) {
|
|
|
2138
2343
|
if (run === void 0) {
|
|
2139
2344
|
return [];
|
|
2140
2345
|
}
|
|
2141
|
-
const here =
|
|
2346
|
+
const here = standing2(ready.getActions(run.id));
|
|
2142
2347
|
const id = run.id.slice(0, 8);
|
|
2143
2348
|
if (here.undoable > 0) {
|
|
2144
2349
|
return [
|
|
@@ -2359,7 +2564,7 @@ async function openConsole(options) {
|
|
|
2359
2564
|
if (ready === void 0 || run === void 0) {
|
|
2360
2565
|
return;
|
|
2361
2566
|
}
|
|
2362
|
-
const here =
|
|
2567
|
+
const here = standing2(ready.getActions(run.id));
|
|
2363
2568
|
if (here.undoable === 0 && here.conflicted === 0) {
|
|
2364
2569
|
say(nothingToUndo(ready, run));
|
|
2365
2570
|
return;
|
|
@@ -2381,7 +2586,7 @@ async function openConsole(options) {
|
|
|
2381
2586
|
if (ready === void 0 || run === void 0) {
|
|
2382
2587
|
return;
|
|
2383
2588
|
}
|
|
2384
|
-
const here =
|
|
2589
|
+
const here = standing2(ready.getActions(run.id));
|
|
2385
2590
|
if (here.undoable === 0 && here.conflicted === 0) {
|
|
2386
2591
|
say(nothingToUndo(ready, run));
|
|
2387
2592
|
return;
|
|
@@ -2541,6 +2746,7 @@ var COMMANDS = `
|
|
|
2541
2746
|
synartesis status
|
|
2542
2747
|
synartesis init <server> -- <command> [args...] [--manifest <path>]
|
|
2543
2748
|
synartesis check [--manifest <path>]
|
|
2749
|
+
synartesis pin [--manifest <path>]
|
|
2544
2750
|
synartesis list [--journal <path>]
|
|
2545
2751
|
synartesis show <runId> [--full] [--live] [--journal <path>]
|
|
2546
2752
|
synartesis gates [--journal <path>]
|
|
@@ -2609,29 +2815,58 @@ it and is found from any directory inside it, the way a version control tool
|
|
|
2609
2815
|
finds its root; anything else lives in ~/.synartesis, which is where the
|
|
2610
2816
|
journal is too. Set SYNARTESIS_HOME to put that somewhere else.
|
|
2611
2817
|
|
|
2818
|
+
Most commands end by naming the one thing worth doing next, worked out from
|
|
2819
|
+
what is actually in the journal rather than from what was typed. Set
|
|
2820
|
+
SYNARTESIS_NO_HINTS to turn that off; --json never carries it.
|
|
2821
|
+
|
|
2612
2822
|
Exit codes: 0 complete, 1 halted or partial, 2 bad usage or configuration.
|
|
2613
2823
|
`;
|
|
2614
2824
|
var UsageError = class extends Error {
|
|
2825
|
+
/**
|
|
2826
|
+
* Whether the command list helps.
|
|
2827
|
+
*
|
|
2828
|
+
* It does for a mistyped flag or command. It does not for "there is nothing
|
|
2829
|
+
* here to act on", which is a fact about the journal rather than about what
|
|
2830
|
+
* was typed -- and answering that with forty lines of unrelated commands
|
|
2831
|
+
* buries the one sentence that matters.
|
|
2832
|
+
*/
|
|
2833
|
+
listCommands;
|
|
2834
|
+
constructor(message, listCommands = true) {
|
|
2835
|
+
super(message);
|
|
2836
|
+
this.listCommands = listCommands;
|
|
2837
|
+
}
|
|
2615
2838
|
};
|
|
2839
|
+
function ours(argv) {
|
|
2840
|
+
const end = argv.indexOf("--");
|
|
2841
|
+
return end === -1 ? argv : argv.slice(0, end);
|
|
2842
|
+
}
|
|
2616
2843
|
function flag(argv, name) {
|
|
2617
|
-
const
|
|
2844
|
+
const mine = ours(argv);
|
|
2845
|
+
const at = mine.indexOf(name);
|
|
2618
2846
|
if (at === -1) {
|
|
2619
2847
|
return void 0;
|
|
2620
2848
|
}
|
|
2621
|
-
const value =
|
|
2849
|
+
const value = mine[at + 1];
|
|
2622
2850
|
if (value === void 0 || value.startsWith("--")) {
|
|
2623
2851
|
throw new UsageError(`${name} needs a value`);
|
|
2624
2852
|
}
|
|
2625
2853
|
return value;
|
|
2626
2854
|
}
|
|
2855
|
+
var TAKES_VALUE = /* @__PURE__ */ new Set([
|
|
2856
|
+
"--manifest",
|
|
2857
|
+
"--journal",
|
|
2858
|
+
"--to",
|
|
2859
|
+
"--by",
|
|
2860
|
+
"--reason",
|
|
2861
|
+
"--older-than",
|
|
2862
|
+
"--client"
|
|
2863
|
+
]);
|
|
2627
2864
|
function positional(argv) {
|
|
2628
|
-
const skip = /* @__PURE__ */ new Set(["--manifest", "--journal", "--to", "--by", "--reason", "--gate-timeout", "--older-than", "--client"]);
|
|
2629
2865
|
const values = [];
|
|
2630
|
-
const
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
if (skip.has(token)) {
|
|
2866
|
+
const mine = ours(argv);
|
|
2867
|
+
for (let i = 0; i < mine.length; i += 1) {
|
|
2868
|
+
const token = mine[i] ?? "";
|
|
2869
|
+
if (TAKES_VALUE.has(token)) {
|
|
2635
2870
|
i += 1;
|
|
2636
2871
|
continue;
|
|
2637
2872
|
}
|
|
@@ -2641,9 +2876,51 @@ function positional(argv) {
|
|
|
2641
2876
|
}
|
|
2642
2877
|
return values;
|
|
2643
2878
|
}
|
|
2879
|
+
async function runPin(argv) {
|
|
2880
|
+
const path = findManifest(flag(argv, "--manifest"));
|
|
2881
|
+
const manifest = loadManifest(path);
|
|
2882
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
2883
|
+
const upstreams = [];
|
|
2884
|
+
try {
|
|
2885
|
+
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
2886
|
+
const upstream = await connectStdioUpstream({
|
|
2887
|
+
name,
|
|
2888
|
+
command: spec.command,
|
|
2889
|
+
args: spec.args,
|
|
2890
|
+
stderr: "capture",
|
|
2891
|
+
...spec.env === void 0 ? {} : { env: spec.env }
|
|
2892
|
+
});
|
|
2893
|
+
upstreams.push(upstream);
|
|
2894
|
+
shapes.set(name, await toolShapes(upstream));
|
|
2895
|
+
}
|
|
2896
|
+
} finally {
|
|
2897
|
+
for (const upstream of upstreams) {
|
|
2898
|
+
await upstream.close();
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
const block2 = pinBlock(shapes, manifest);
|
|
2902
|
+
out("");
|
|
2903
|
+
out(` ${style.label("pins for")} ${style.strong(path)}`);
|
|
2904
|
+
out("");
|
|
2905
|
+
for (const line2 of block2.split("\n")) {
|
|
2906
|
+
out(` ${line2}`);
|
|
2907
|
+
}
|
|
2908
|
+
out("");
|
|
2909
|
+
out(` ${style.quiet("Paste this into the manifest. From then on a tool whose shape")}`);
|
|
2910
|
+
out(` ${style.quiet("changes stops the proxy instead of quietly keeping its old policy.")}`);
|
|
2911
|
+
out("");
|
|
2912
|
+
return 0;
|
|
2913
|
+
}
|
|
2644
2914
|
async function runCheck(argv) {
|
|
2645
2915
|
const path = findManifest(flag(argv, "--manifest"));
|
|
2646
2916
|
const manifest = loadManifest(path);
|
|
2917
|
+
const unproven = untested(manifest);
|
|
2918
|
+
if (unproven.length > 0) {
|
|
2919
|
+
out("");
|
|
2920
|
+
for (const line2 of wrapped(warnUntested(unproven), 74)) {
|
|
2921
|
+
out(` ${style.accent(line2)}`);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2647
2924
|
const upstreams = [];
|
|
2648
2925
|
try {
|
|
2649
2926
|
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
@@ -2673,11 +2950,33 @@ async function runCheck(argv) {
|
|
|
2673
2950
|
out(` ${rule(54)}`);
|
|
2674
2951
|
out("");
|
|
2675
2952
|
out(` ${style.quiet("servers ")} ${Object.keys(manifest.servers).join(", ")}`);
|
|
2953
|
+
for (const entry of standing(manifest)) {
|
|
2954
|
+
const said = describeStanding(entry);
|
|
2955
|
+
out(
|
|
2956
|
+
` ${style.quiet(" ")} ${style.strong(entry.server)} ${entry.provenance === "documented" ? style.accent(said) : style.quiet(said)}`
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2676
2959
|
out(` ${style.quiet("policies")} ${[...counts].map(([k, v]) => `${String(v)} ${k}`).join(", ")}`);
|
|
2677
2960
|
out(` ${style.quiet("guarded ")} ${style.accent(String(gated))}`);
|
|
2961
|
+
const pinned = Object.entries(manifest.pins ?? {});
|
|
2962
|
+
const servers = Object.keys(manifest.servers);
|
|
2963
|
+
const unpinned = servers.filter((name) => !pinned.some(([held]) => held === name));
|
|
2964
|
+
out(
|
|
2965
|
+
` ${style.quiet("pinned ")} ${pinned.length === 0 ? style.quiet("nothing -- run `synartesis pin`") : pinned.map(([name, tools]) => `${name} (${String(Object.keys(tools).length)})`).join(", ") + (unpinned.length === 0 ? "" : style.quiet(`; not ${unpinned.join(", ")}`))}`
|
|
2966
|
+
);
|
|
2678
2967
|
out("");
|
|
2679
2968
|
out(` ${style.quiet("Anything not mentioned here is treated as irreversible and guarded.")}`);
|
|
2680
2969
|
out("");
|
|
2970
|
+
hint(
|
|
2971
|
+
firstOf(
|
|
2972
|
+
() => notPinned(manifest),
|
|
2973
|
+
() => ({
|
|
2974
|
+
why: "this is sound; to see which clients it covers",
|
|
2975
|
+
run: "status",
|
|
2976
|
+
needs: ["manifest", "journal"]
|
|
2977
|
+
})
|
|
2978
|
+
)
|
|
2979
|
+
);
|
|
2681
2980
|
return 0;
|
|
2682
2981
|
}
|
|
2683
2982
|
async function runInstall(argv) {
|
|
@@ -2711,6 +3010,11 @@ async function runInstall(argv) {
|
|
|
2711
3010
|
for (const server of plan.servers) {
|
|
2712
3011
|
const note = server.adopted === void 0 ? style.accent("drafted, every tool held until you say how to undo it") : style.quiet(`the policy that ships for ${server.adopted} (${String(server.tools ?? 0)} tools)`);
|
|
2713
3012
|
out(` ${style.strong(server.name.padEnd(18))} ${note}`);
|
|
3013
|
+
if (server.provenance === "documented") {
|
|
3014
|
+
out(
|
|
3015
|
+
` ${" ".repeat(18)} ${style.accent("never run against the real server -- check it before trusting undo")}`
|
|
3016
|
+
);
|
|
3017
|
+
}
|
|
2714
3018
|
}
|
|
2715
3019
|
for (const skip of plan.skipped) {
|
|
2716
3020
|
out(` ${style.quiet(skip.name.padEnd(18))} ${style.quiet(skip.why)}`);
|
|
@@ -2866,6 +3170,9 @@ function runStatus(argv) {
|
|
|
2866
3170
|
waiting === 0 ? ` ${style.quiet("Everything found is covered.")}` : ` ${style.accent(`${String(waiting)} not covered.`)} ${style.quiet(`${cliCommand()} install covers them.`)}`
|
|
2867
3171
|
);
|
|
2868
3172
|
out("");
|
|
3173
|
+
if (waiting === 0) {
|
|
3174
|
+
hint(afterStatus(journal));
|
|
3175
|
+
}
|
|
2869
3176
|
} finally {
|
|
2870
3177
|
journal?.close();
|
|
2871
3178
|
}
|
|
@@ -2921,18 +3228,19 @@ function pick(candidates2, given, noun, newest = false) {
|
|
|
2921
3228
|
if (given === void 0) {
|
|
2922
3229
|
const [only, ...rest2] = candidates2;
|
|
2923
3230
|
if (only === void 0) {
|
|
2924
|
-
throw new UsageError(`there is no ${noun.one} to act on
|
|
3231
|
+
throw new UsageError(`there is no ${noun.one} to act on`, false);
|
|
2925
3232
|
}
|
|
2926
3233
|
if (rest2.length > 0 && !newest) {
|
|
2927
3234
|
throw new UsageError(
|
|
2928
3235
|
`there are ${String(candidates2.length)} ${noun.many}; name one, or use --all:
|
|
2929
|
-
${listed(candidates2)}
|
|
3236
|
+
${listed(candidates2)}`,
|
|
3237
|
+
false
|
|
2930
3238
|
);
|
|
2931
3239
|
}
|
|
2932
3240
|
return only;
|
|
2933
3241
|
}
|
|
2934
3242
|
if (given === "") {
|
|
2935
|
-
throw new UsageError(`no ${noun.one} was named; an empty id is usually an unset variable
|
|
3243
|
+
throw new UsageError(`no ${noun.one} was named; an empty id is usually an unset variable`, false);
|
|
2936
3244
|
}
|
|
2937
3245
|
const exact = candidates2.find((item) => item.id === given);
|
|
2938
3246
|
if (exact !== void 0) {
|
|
@@ -2941,12 +3249,13 @@ ${listed(candidates2)}`
|
|
|
2941
3249
|
const matches = candidates2.filter((item) => item.id.startsWith(given));
|
|
2942
3250
|
const [first, ...rest] = matches;
|
|
2943
3251
|
if (first === void 0) {
|
|
2944
|
-
throw new UsageError(`no ${noun.one} matches ${given}
|
|
3252
|
+
throw new UsageError(`no ${noun.one} matches ${given}`, false);
|
|
2945
3253
|
}
|
|
2946
3254
|
if (rest.length > 0) {
|
|
2947
3255
|
throw new UsageError(
|
|
2948
3256
|
`${given} matches ${String(matches.length)} ${noun.many}:
|
|
2949
|
-
${listed(matches)}
|
|
3257
|
+
${listed(matches)}`,
|
|
3258
|
+
false
|
|
2950
3259
|
);
|
|
2951
3260
|
}
|
|
2952
3261
|
return first;
|
|
@@ -2963,10 +3272,19 @@ function out(line2) {
|
|
|
2963
3272
|
process.stdout.write(`${line2}
|
|
2964
3273
|
`);
|
|
2965
3274
|
}
|
|
3275
|
+
function hint(candidate) {
|
|
3276
|
+
if (candidate === void 0 || !hintsWanted()) {
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
out(hintLine(candidate, pathArgs(candidate.needs)));
|
|
3280
|
+
out("");
|
|
3281
|
+
}
|
|
2966
3282
|
function runList(journal, asJson, journalPath) {
|
|
2967
3283
|
const runs = [...journal.listRuns()].reverse();
|
|
3284
|
+
const tally2 = journal.tallyRuns();
|
|
3285
|
+
const counted = (id) => tally2.get(id) ?? { actions: 0, unknown: 0, waiting: 0, applied: 0 };
|
|
2968
3286
|
if (asJson) {
|
|
2969
|
-
out(JSON.stringify(runs.map((run) => ({ ...run, actions:
|
|
3287
|
+
out(JSON.stringify(runs.map((run) => ({ ...run, actions: counted(run.id).actions }))));
|
|
2970
3288
|
return 0;
|
|
2971
3289
|
}
|
|
2972
3290
|
if (runs.length === 0) {
|
|
@@ -2983,16 +3301,15 @@ function runList(journal, asJson, journalPath) {
|
|
|
2983
3301
|
)
|
|
2984
3302
|
);
|
|
2985
3303
|
for (const run of runs) {
|
|
2986
|
-
const actions =
|
|
2987
|
-
const unknown = actions
|
|
2988
|
-
const waiting = actions.filter((action) => action.status === "gated").length;
|
|
3304
|
+
const actions = counted(run.id);
|
|
3305
|
+
const { unknown, waiting } = actions;
|
|
2989
3306
|
const notes = [
|
|
2990
3307
|
unknown === 0 ? "" : `${String(unknown)} of unknown outcome`,
|
|
2991
3308
|
waiting === 0 ? "" : `${String(waiting)} awaiting approval`
|
|
2992
3309
|
].filter((note2) => note2 !== "");
|
|
2993
3310
|
const note = notes.length === 0 ? "" : ` ${style.accent(`(${notes.join("; ")})`)}`;
|
|
2994
3311
|
out(
|
|
2995
|
-
` ${style.strong(run.id)} ${style.quiet(shortTime(run.startedAt).trimEnd().padEnd(13))} ${run.status.padEnd(12)} ${String(actions.
|
|
3312
|
+
` ${style.strong(run.id)} ${style.quiet(shortTime(run.startedAt).trimEnd().padEnd(13))} ${run.status.padEnd(12)} ${String(actions.actions).padStart(7)} ${run.label ?? "-"}${note}`
|
|
2996
3313
|
);
|
|
2997
3314
|
}
|
|
2998
3315
|
out("");
|
|
@@ -3000,6 +3317,10 @@ function runList(journal, asJson, journalPath) {
|
|
|
3000
3317
|
out(` ${style.quiet(`This journal is ${sizeOf(journalPath)}; synartesis prune reclaims what is old enough to lose.`)}`);
|
|
3001
3318
|
out("");
|
|
3002
3319
|
}
|
|
3320
|
+
hint(firstOf(
|
|
3321
|
+
() => heldCalls(journal),
|
|
3322
|
+
() => whatChanged(journal)
|
|
3323
|
+
));
|
|
3003
3324
|
return 0;
|
|
3004
3325
|
}
|
|
3005
3326
|
async function runShow(argv, journal, asJson) {
|
|
@@ -3062,7 +3383,7 @@ async function runShow(argv, journal, asJson) {
|
|
|
3062
3383
|
out(` ${style.quiet(summariseArgs(action.args, 96))}`);
|
|
3063
3384
|
}
|
|
3064
3385
|
if (action.approvedAt !== void 0) {
|
|
3065
|
-
const verb = action
|
|
3386
|
+
const verb = labelFor(action) === "denied" ? "denied" : "approved";
|
|
3066
3387
|
out(
|
|
3067
3388
|
` ${style.accent(`${verb} by ${action.approvedBy ?? "nobody"}`)} ${style.quiet(`at ${fullTime(action.approvedAt)}`)}`
|
|
3068
3389
|
);
|
|
@@ -3097,6 +3418,16 @@ async function runShow(argv, journal, asJson) {
|
|
|
3097
3418
|
out(
|
|
3098
3419
|
` ${style.quiet("undo stops at the first of them; ")}${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force`)}${style.quiet(" shows what it would write")}`
|
|
3099
3420
|
);
|
|
3421
|
+
} else if (actions.some((action) => action.status === "applied" && action.inverse !== void 0)) {
|
|
3422
|
+
out("");
|
|
3423
|
+
hint({
|
|
3424
|
+
// Not a restatement of the verdict directly above it, which has just
|
|
3425
|
+
// said in its own words that nothing has moved. A hint that repeats
|
|
3426
|
+
// the line above is a line nobody reads twice.
|
|
3427
|
+
why: "to see the plan, before anything is written",
|
|
3428
|
+
run: `undo ${runId.slice(0, 8)} --dry-run`,
|
|
3429
|
+
needs: ["manifest", "journal"]
|
|
3430
|
+
});
|
|
3100
3431
|
}
|
|
3101
3432
|
} else {
|
|
3102
3433
|
out("");
|
|
@@ -3178,13 +3509,21 @@ function truncate3(text, limit) {
|
|
|
3178
3509
|
function summarise2(actions) {
|
|
3179
3510
|
const counts = /* @__PURE__ */ new Map();
|
|
3180
3511
|
for (const action of actions) {
|
|
3181
|
-
|
|
3512
|
+
const label = labelFor(action);
|
|
3513
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
3182
3514
|
}
|
|
3183
3515
|
const parts = [...counts].sort(([a], [b]) => a < b ? -1 : 1).map(([k, v]) => `${String(v)} ${k}`);
|
|
3184
3516
|
const undoable = actions.filter((a) => a.inverse !== void 0).length;
|
|
3185
3517
|
return `${String(actions.length)} actions: ${parts.join(", ")} | ${String(undoable)} with a recorded undo`;
|
|
3186
3518
|
}
|
|
3187
3519
|
var journalArg = "";
|
|
3520
|
+
var manifestArg = "";
|
|
3521
|
+
function pathArgs(needs) {
|
|
3522
|
+
if (needs === void 0) {
|
|
3523
|
+
return "";
|
|
3524
|
+
}
|
|
3525
|
+
return needs.map((what) => what === "journal" ? journalArg : manifestArg).join("");
|
|
3526
|
+
}
|
|
3188
3527
|
var PRUNE_NAG_BYTES = 100 * 1024 * 1024;
|
|
3189
3528
|
function bytesOf(path) {
|
|
3190
3529
|
try {
|
|
@@ -3258,12 +3597,18 @@ function runPrune(argv, journal, journalPath) {
|
|
|
3258
3597
|
function runClose(argv, journal) {
|
|
3259
3598
|
const all = journal.listRuns();
|
|
3260
3599
|
const given = positional(argv)[1];
|
|
3261
|
-
const
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3600
|
+
const active = [...all.filter((candidate) => candidate.status === "active")].reverse();
|
|
3601
|
+
if (given === void 0 && active.length === 0) {
|
|
3602
|
+
out("");
|
|
3603
|
+
out(` ${style.quiet("nothing is open; every run has ended cleanly")}`);
|
|
3604
|
+
out("");
|
|
3605
|
+
hint(firstOf(
|
|
3606
|
+
() => heldCalls(journal),
|
|
3607
|
+
() => whatChanged(journal)
|
|
3608
|
+
));
|
|
3609
|
+
return 0;
|
|
3610
|
+
}
|
|
3611
|
+
const run = given === void 0 ? pick(active, void 0, RUN, true) : pick([...all].reverse(), given, RUN, true);
|
|
3267
3612
|
const closed = journal.closeAbandonedRun(run.id);
|
|
3268
3613
|
out("");
|
|
3269
3614
|
out(
|
|
@@ -3282,6 +3627,10 @@ function runGates(journal, asJson) {
|
|
|
3282
3627
|
out("");
|
|
3283
3628
|
out(` ${style.quiet("Nothing is waiting for a decision.")}`);
|
|
3284
3629
|
out("");
|
|
3630
|
+
hint(firstOf(
|
|
3631
|
+
() => whatChanged(journal),
|
|
3632
|
+
() => LEAVE_IT_RUNNING
|
|
3633
|
+
));
|
|
3285
3634
|
return 0;
|
|
3286
3635
|
}
|
|
3287
3636
|
out("");
|
|
@@ -3310,10 +3659,10 @@ function runDecision(argv, journal, approving) {
|
|
|
3310
3659
|
const by = flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown";
|
|
3311
3660
|
const reason = flag(argv, "--reason") ?? "denied by operator";
|
|
3312
3661
|
if (given !== void 0) {
|
|
3313
|
-
const
|
|
3314
|
-
if (
|
|
3662
|
+
const settled2 = journal.getAction(given);
|
|
3663
|
+
if (settled2 !== void 0 && settled2.status !== "gated") {
|
|
3315
3664
|
process.stderr.write(
|
|
3316
|
-
`synartesis: ${given} is no longer awaiting approval (it is ${
|
|
3665
|
+
`synartesis: ${given} is no longer awaiting approval (it is ${labelFor(settled2)})
|
|
3317
3666
|
`
|
|
3318
3667
|
);
|
|
3319
3668
|
return 1;
|
|
@@ -3325,6 +3674,7 @@ function runDecision(argv, journal, approving) {
|
|
|
3325
3674
|
return 0;
|
|
3326
3675
|
}
|
|
3327
3676
|
let failed = 0;
|
|
3677
|
+
let settled = 0;
|
|
3328
3678
|
for (const action of targets) {
|
|
3329
3679
|
const changed = approving ? journal.approve(action.id, by) : journal.deny(action.id, by, reason);
|
|
3330
3680
|
if (!changed) {
|
|
@@ -3336,13 +3686,23 @@ function runDecision(argv, journal, approving) {
|
|
|
3336
3686
|
failed += 1;
|
|
3337
3687
|
continue;
|
|
3338
3688
|
}
|
|
3689
|
+
settled += 1;
|
|
3339
3690
|
out(
|
|
3340
3691
|
` ${style.accent(approving ? "approved" : "denied")} ${style.strong(`${action.server}.${action.tool}`)} ${style.quiet(action.id)}`
|
|
3341
3692
|
);
|
|
3342
3693
|
}
|
|
3694
|
+
if (settled > 0) {
|
|
3695
|
+
out("");
|
|
3696
|
+
hint(
|
|
3697
|
+
firstOf(
|
|
3698
|
+
() => heldCalls(journal),
|
|
3699
|
+
() => approving ? { why: "the agent can make that call again now, and it will go through" } : { why: "the call was refused; the agent is told, and decides what to do next" }
|
|
3700
|
+
)
|
|
3701
|
+
);
|
|
3702
|
+
}
|
|
3343
3703
|
return failed === 0 ? 0 : 1;
|
|
3344
3704
|
}
|
|
3345
|
-
function report(result, alreadyForcing = false) {
|
|
3705
|
+
function report(result, alreadyForcing = false, as = "") {
|
|
3346
3706
|
out("");
|
|
3347
3707
|
out(` ${style.label(result.dryRun ? "dry run" : "undo")} ${style.strong(result.runId)}`);
|
|
3348
3708
|
out(` ${rule(72)}`);
|
|
@@ -3362,7 +3722,11 @@ function report(result, alreadyForcing = false) {
|
|
|
3362
3722
|
if (step.plan !== void 0 && step.kind === "revert") {
|
|
3363
3723
|
const verb = `${step.replanned === true ? "replanned, " : ""}${result.dryRun ? "would call" : "called"}`;
|
|
3364
3724
|
out(
|
|
3365
|
-
` ${style.quiet(verb)} ${step.plan.server}.${step.plan.tool} ` +
|
|
3725
|
+
` ${style.quiet(verb)} ${step.plan.server}.${step.plan.tool} ` + // summariseArgs, not truncated json: the raw form spent its whole
|
|
3726
|
+
// budget on a long path's leading directories and cut off before
|
|
3727
|
+
// the filename, so every step read "would call fs.write_file
|
|
3728
|
+
// {path: /Users/.../very/long/pre..." and named nothing.
|
|
3729
|
+
style.quiet(summariseArgs(step.plan.args, 120))
|
|
3366
3730
|
);
|
|
3367
3731
|
}
|
|
3368
3732
|
}
|
|
@@ -3411,6 +3775,13 @@ function report(result, alreadyForcing = false) {
|
|
|
3411
3775
|
` ${style.label("result")} ${result.status === "rolled_back" ? result.status : style.accent(result.status)}`
|
|
3412
3776
|
);
|
|
3413
3777
|
out("");
|
|
3778
|
+
if (result.dryRun && result.status === "rolled_back") {
|
|
3779
|
+
hint({
|
|
3780
|
+
why: "nothing was written; to do exactly this for real",
|
|
3781
|
+
run: `undo ${result.runId.slice(0, 8)}${as}`,
|
|
3782
|
+
needs: ["manifest", "journal"]
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3414
3785
|
return result.status === "rolled_back" ? 0 : 1;
|
|
3415
3786
|
}
|
|
3416
3787
|
async function withUpstreams(manifestPath, use, only) {
|
|
@@ -3471,6 +3842,11 @@ async function performUndo(manifestPath, journal, runId, options) {
|
|
|
3471
3842
|
);
|
|
3472
3843
|
}
|
|
3473
3844
|
async function runUndo(argv, journal) {
|
|
3845
|
+
const rawTo = flag(argv, "--to");
|
|
3846
|
+
const toSeq = rawTo === void 0 ? void 0 : Number(rawTo);
|
|
3847
|
+
if (toSeq !== void 0 && (!Number.isInteger(toSeq) || toSeq < 1)) {
|
|
3848
|
+
throw new UsageError("--to needs a positive whole number");
|
|
3849
|
+
}
|
|
3474
3850
|
const given = positional(argv)[1];
|
|
3475
3851
|
const chosen = pick([...journal.listRuns()].reverse(), given, RUN, true);
|
|
3476
3852
|
const runId = chosen.id;
|
|
@@ -3506,11 +3882,6 @@ async function runUndo(argv, journal) {
|
|
|
3506
3882
|
return 0;
|
|
3507
3883
|
}
|
|
3508
3884
|
}
|
|
3509
|
-
const rawTo = flag(argv, "--to");
|
|
3510
|
-
const toSeq = rawTo === void 0 ? void 0 : Number(rawTo);
|
|
3511
|
-
if (toSeq !== void 0 && (!Number.isInteger(toSeq) || toSeq < 1)) {
|
|
3512
|
-
throw new UsageError("--to needs a positive whole number");
|
|
3513
|
-
}
|
|
3514
3885
|
if (toSeq !== void 0) {
|
|
3515
3886
|
const highest = journal.getActions(runId).reduce((top, action) => Math.max(top, action.seq), 0);
|
|
3516
3887
|
if (toSeq > highest) {
|
|
@@ -3554,16 +3925,42 @@ async function runUndo(argv, journal) {
|
|
|
3554
3925
|
return 1;
|
|
3555
3926
|
}
|
|
3556
3927
|
}
|
|
3928
|
+
const replan = argv.includes("--replan");
|
|
3557
3929
|
return report(
|
|
3558
3930
|
await performUndo(manifestPath, journal, runId, {
|
|
3559
3931
|
dryRun: argv.includes("--dry-run"),
|
|
3560
3932
|
...toSeq === void 0 ? {} : { toSeq },
|
|
3561
|
-
replan
|
|
3933
|
+
replan,
|
|
3562
3934
|
...forcing && said ? { force: true } : {}
|
|
3563
3935
|
}),
|
|
3564
|
-
forcing
|
|
3936
|
+
forcing,
|
|
3937
|
+
// --to is deliberately absent, and cannot reach here: a floor leaves
|
|
3938
|
+
// actions below it alone, which makes the result `partial`, and the hint
|
|
3939
|
+
// is only offered on `rolled_back`.
|
|
3940
|
+
`${replan ? " --replan" : ""}${forcing && said ? " --force --yes" : ""}`
|
|
3565
3941
|
);
|
|
3566
3942
|
}
|
|
3943
|
+
var KNOWN_COMMANDS = [
|
|
3944
|
+
"install",
|
|
3945
|
+
"uninstall",
|
|
3946
|
+
"status",
|
|
3947
|
+
"init",
|
|
3948
|
+
"check",
|
|
3949
|
+
"pin",
|
|
3950
|
+
"list",
|
|
3951
|
+
"show",
|
|
3952
|
+
"gates",
|
|
3953
|
+
"close",
|
|
3954
|
+
"prune",
|
|
3955
|
+
"proxy",
|
|
3956
|
+
"desktop",
|
|
3957
|
+
"watch",
|
|
3958
|
+
"approve",
|
|
3959
|
+
"deny",
|
|
3960
|
+
"undo",
|
|
3961
|
+
"help",
|
|
3962
|
+
"version"
|
|
3963
|
+
];
|
|
3567
3964
|
var FLAGS = /* @__PURE__ */ new Set([
|
|
3568
3965
|
// Two lists have to agree about a flag: this one decides whether it is
|
|
3569
3966
|
// accepted at all, and the skip set in positional() decides whether its
|
|
@@ -3589,7 +3986,8 @@ var FLAGS = /* @__PURE__ */ new Set([
|
|
|
3589
3986
|
"--help",
|
|
3590
3987
|
"-h",
|
|
3591
3988
|
"--version",
|
|
3592
|
-
"-V"
|
|
3989
|
+
"-V",
|
|
3990
|
+
"-v"
|
|
3593
3991
|
]);
|
|
3594
3992
|
function version() {
|
|
3595
3993
|
try {
|
|
@@ -3601,13 +3999,26 @@ function version() {
|
|
|
3601
3999
|
return "unknown";
|
|
3602
4000
|
}
|
|
3603
4001
|
}
|
|
4002
|
+
function unknownCommand(typed) {
|
|
4003
|
+
const meant = didYouMean(typed, KNOWN_COMMANDS);
|
|
4004
|
+
return meant === void 0 ? `unknown command ${typed}` : `unknown command ${typed}; did you mean ${meant}?`;
|
|
4005
|
+
}
|
|
3604
4006
|
function rejectUnknownFlags(argv) {
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
4007
|
+
const mine = ours(argv);
|
|
4008
|
+
for (let i = 0; i < mine.length; i += 1) {
|
|
4009
|
+
const token = mine[i] ?? "";
|
|
4010
|
+
if (TAKES_VALUE.has(token)) {
|
|
4011
|
+
i += 1;
|
|
4012
|
+
continue;
|
|
3608
4013
|
}
|
|
3609
4014
|
if (token.startsWith("-") && token !== "-" && !FLAGS.has(token)) {
|
|
3610
|
-
|
|
4015
|
+
if (PROXY_FLAGS.includes(token)) {
|
|
4016
|
+
throw new UsageError(`${token} is a flag for ${cliCommand()} proxy, not for this`);
|
|
4017
|
+
}
|
|
4018
|
+
const meant = didYouMean(token, [...FLAGS]);
|
|
4019
|
+
throw new UsageError(
|
|
4020
|
+
meant === void 0 ? `unknown flag ${token}` : `unknown flag ${token}; did you mean ${meant}?`
|
|
4021
|
+
);
|
|
3611
4022
|
}
|
|
3612
4023
|
}
|
|
3613
4024
|
}
|
|
@@ -3625,17 +4036,25 @@ async function main(argv) {
|
|
|
3625
4036
|
await import("./proxy.js");
|
|
3626
4037
|
return 0;
|
|
3627
4038
|
}
|
|
3628
|
-
|
|
4039
|
+
const first = positional(argv)[0];
|
|
4040
|
+
if (argv.includes("--help") || argv.includes("-h") || first === "help") {
|
|
3629
4041
|
process.stdout.write(`${banner()}
|
|
3630
4042
|
${COMMANDS}`);
|
|
3631
4043
|
return 0;
|
|
3632
4044
|
}
|
|
3633
|
-
if (argv.includes("--version") || argv.includes("-V")) {
|
|
4045
|
+
if (argv.includes("--version") || argv.includes("-V") || argv.includes("-v") || first === "version") {
|
|
3634
4046
|
process.stdout.write(`${version()}
|
|
3635
4047
|
`);
|
|
3636
4048
|
return 0;
|
|
3637
4049
|
}
|
|
3638
4050
|
rejectUnknownFlags(argv);
|
|
4051
|
+
if (command !== void 0 && !KNOWN_COMMANDS.includes(command)) {
|
|
4052
|
+
throw new UsageError(unknownCommand(command));
|
|
4053
|
+
}
|
|
4054
|
+
const givenJournal = flag(argv, "--journal");
|
|
4055
|
+
const givenManifest = flag(argv, "--manifest");
|
|
4056
|
+
journalArg = givenJournal === void 0 ? "" : ` --journal ${resolve3(givenJournal)}`;
|
|
4057
|
+
manifestArg = givenManifest === void 0 ? "" : ` --manifest ${resolve3(givenManifest)}`;
|
|
3639
4058
|
if (command === void 0) {
|
|
3640
4059
|
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
3641
4060
|
const journalPath2 = findJournal(flag(argv, "--journal"), manifestPath);
|
|
@@ -3677,6 +4096,9 @@ ${COMMANDS}`);
|
|
|
3677
4096
|
if (command === "check") {
|
|
3678
4097
|
return await runCheck(argv);
|
|
3679
4098
|
}
|
|
4099
|
+
if (command === "pin") {
|
|
4100
|
+
return await runPin(argv);
|
|
4101
|
+
}
|
|
3680
4102
|
if (command === "install") {
|
|
3681
4103
|
return await runInstall(argv);
|
|
3682
4104
|
}
|
|
@@ -3701,7 +4123,6 @@ ${COMMANDS}`);
|
|
|
3701
4123
|
decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown"
|
|
3702
4124
|
});
|
|
3703
4125
|
}
|
|
3704
|
-
journalArg = given === void 0 ? "" : ` --journal ${resolve3(given)}`;
|
|
3705
4126
|
if (!existsSync8(journalPath) && (command === "list" || command === "show" || command === "gates")) {
|
|
3706
4127
|
if (asJson) {
|
|
3707
4128
|
out(JSON.stringify(command === "show" ? { run: null, actions: [] } : []));
|
|
@@ -3739,7 +4160,7 @@ ${COMMANDS}`);
|
|
|
3739
4160
|
case "undo":
|
|
3740
4161
|
return await runUndo(argv, journal);
|
|
3741
4162
|
default:
|
|
3742
|
-
throw new UsageError(
|
|
4163
|
+
throw new UsageError(unknownCommand(command));
|
|
3743
4164
|
}
|
|
3744
4165
|
} finally {
|
|
3745
4166
|
journal.close();
|
|
@@ -3764,9 +4185,11 @@ try {
|
|
|
3764
4185
|
process.exitCode = await main(process.argv.slice(2));
|
|
3765
4186
|
} catch (error) {
|
|
3766
4187
|
if (error instanceof UsageError) {
|
|
3767
|
-
process.stderr.write(
|
|
3768
|
-
|
|
3769
|
-
${
|
|
4188
|
+
process.stderr.write(
|
|
4189
|
+
error.listCommands ? `synartesis: ${error.message}
|
|
4190
|
+
${shortList()}` : `synartesis: ${error.message}
|
|
4191
|
+
`
|
|
4192
|
+
);
|
|
3770
4193
|
process.exitCode = 2;
|
|
3771
4194
|
} else if (error instanceof ManifestError) {
|
|
3772
4195
|
process.stderr.write(`synartesis: ${error.message}
|