synartesis 0.4.0 → 0.4.1
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 +56 -0
- package/README.md +35 -0
- package/dist/cli.js +299 -51
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
What changed, and why it mattered. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 0.4.1 — 2026-09-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`synartesis show <session> --live`**, and `l` in the screen. Synartesis
|
|
10
|
+
records what an agent does, not what happens to a file — nothing you do by
|
|
11
|
+
hand goes through the proxy. That is what makes the drift check work, and it
|
|
12
|
+
meant the commonest question about this tool had no answer: has somebody
|
|
13
|
+
edited that file since? You found out by attempting an undo and having it
|
|
14
|
+
refuse.
|
|
15
|
+
|
|
16
|
+
`--live` reads every resource the session touched as it is now and says
|
|
17
|
+
which of them still match what the run left. Nothing is written, no
|
|
18
|
+
reversing call is sent, and no row changes status. Unlike `undo --dry-run`
|
|
19
|
+
it does not stop at the first conflict: a session with five writes reports
|
|
20
|
+
on all five.
|
|
21
|
+
|
|
22
|
+
An earlier write to a resource is reported as superseded rather than
|
|
23
|
+
changed. Undo walks backwards, so each older write is checked against a
|
|
24
|
+
state the one above it restores; measuring them all against the world as it
|
|
25
|
+
is now would call every write but the last one changed.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Undo acted on a session nobody was looking at.** One cursor serves four
|
|
30
|
+
views, and in the held-calls and connections lists its number counts
|
|
31
|
+
something else entirely — but `u`, `p` and `l` read it against the sessions
|
|
32
|
+
regardless. `g`, `j`, `u`, `y`, four keys pressed while looking at the list
|
|
33
|
+
of held calls, undid a session that was never on screen. That is the failure
|
|
34
|
+
this tool exists to prevent, committed by the tool. Those keys now do
|
|
35
|
+
nothing outside the session views and say why.
|
|
36
|
+
|
|
37
|
+
- **`--force` showed one conflict and overwrote several.** The ask ran a
|
|
38
|
+
dry-run rollback, and a rollback halts at the first drift — so with two
|
|
39
|
+
people's edits underneath it, one diff was printed, `--force --yes` was
|
|
40
|
+
typed, and both were written over. It now reads every conflict and prints
|
|
41
|
+
each one.
|
|
42
|
+
|
|
43
|
+
- **One broken server stopped you undoing anything at all.** `install` covers
|
|
44
|
+
every AI on the machine with one policy, so a manifest routinely names
|
|
45
|
+
servers that have nothing to do with the session in hand — and undo started
|
|
46
|
+
all of them. An entry whose command is not there made every session
|
|
47
|
+
unreadable and unundoable. Undo and `--live` now start only the servers the
|
|
48
|
+
session actually went to, and skip with a reported reason any that still
|
|
49
|
+
will not start. `--replan` still starts everything, since it re-resolves
|
|
50
|
+
inverses from the current policy.
|
|
51
|
+
|
|
52
|
+
- **`--force` ignored `--to`.** A change below the floor — an action the undo
|
|
53
|
+
would not touch — refused the whole command.
|
|
54
|
+
|
|
55
|
+
- The overwrite warning in the screen now expires with the diff that
|
|
56
|
+
justified it, so the second `u` cannot be answered after the evidence has
|
|
57
|
+
scrolled away. `--yes` without `--force` says it is being ignored rather
|
|
58
|
+
than being silently dropped, and `--live` no longer starts every server to
|
|
59
|
+
inspect a session that recorded nothing.
|
|
60
|
+
|
|
5
61
|
## 0.4.0 — 2026-09-08
|
|
6
62
|
|
|
7
63
|
### Added
|
package/README.md
CHANGED
|
@@ -526,11 +526,13 @@ was refused with the file untouched.
|
|
|
526
526
|
| `init <server> -- <cmd>` | Introspect a server and draft a manifest |
|
|
527
527
|
| `list` | Every recorded run |
|
|
528
528
|
| `show <runId>` | One run's timeline, with the undo for each step |
|
|
529
|
+
| `show <runId> --live` | The same, plus what has changed in the world since |
|
|
529
530
|
| `gates` | What is waiting for a decision |
|
|
530
531
|
| `approve <actionId>` | Allow a suspended call |
|
|
531
532
|
| `deny <actionId>` | Refuse one |
|
|
532
533
|
| `undo <runId>` | Reverse a run, newest action first |
|
|
533
534
|
| `undo <runId> --replan` | Same, but rebuild each undo from the current manifest |
|
|
535
|
+
| `undo <runId> --force` | Print every change it would write over; `--yes` goes ahead |
|
|
534
536
|
| `check` | Load a manifest and verify it against the servers it names |
|
|
535
537
|
| `prune` | Delete runs older than 30 days and reclaim the space |
|
|
536
538
|
| `close [runId]` | End a run a killed proxy left open. Nothing guesses at this: several proxies can share one journal, so a run left active is indistinguishable from one still being worked on |
|
|
@@ -598,6 +600,39 @@ protocol traffic.
|
|
|
598
600
|
is baked into every run made under it. `undo --replan` rebuilds them from a
|
|
599
601
|
corrected manifest using the state already captured, which is the way out.
|
|
600
602
|
|
|
603
|
+
## Has anybody touched it since?
|
|
604
|
+
|
|
605
|
+
Synartesis records what an agent does, not what happens to a file. Nothing you
|
|
606
|
+
do by hand goes through the proxy, so an edit of your own is invisible to the
|
|
607
|
+
journal — and that is what makes the drift check work: when undo reads a file
|
|
608
|
+
and finds bytes it never recorded, it knows somebody else has been there.
|
|
609
|
+
|
|
610
|
+
The cost used to be that you found out by walking into it. You ran the undo,
|
|
611
|
+
and it refused. To ask first:
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
synartesis show <session> --live
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
It reads every resource the session touched, as it is now, and says which of
|
|
618
|
+
them still match what the run left:
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
1 ← reversible applied filesystem.write_file changed since
|
|
622
|
+
at line 13:
|
|
623
|
+
+ A HUMAN WAS HERE
|
|
624
|
+
0 removed, 1 added.
|
|
625
|
+
|
|
626
|
+
1 changed since this ran; undoing would write over it
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Nothing is written, no reversing call is sent, and no row changes status.
|
|
630
|
+
Unlike `undo --dry-run`, it does not stop at the first conflict — a session
|
|
631
|
+
with five writes reports on all five. `l` in the screen does the same thing.
|
|
632
|
+
|
|
633
|
+
If you decide the recorded value is the one worth keeping, `undo --force`
|
|
634
|
+
prints every line it would write over and stops; `--force --yes` goes ahead.
|
|
635
|
+
|
|
601
636
|
## Watching it work
|
|
602
637
|
|
|
603
638
|
Synartesis is not a daemon and cannot be one. An MCP client spawns a stdio
|
package/dist/cli.js
CHANGED
|
@@ -553,11 +553,11 @@ function describeStep(action) {
|
|
|
553
553
|
return { seq: action.seq, server: action.server, tool: action.tool };
|
|
554
554
|
}
|
|
555
555
|
function overwriteText(current, action) {
|
|
556
|
-
const
|
|
557
|
-
if (
|
|
556
|
+
const intended2 = intendedAfterInverse(action);
|
|
557
|
+
if (intended2 === void 0) {
|
|
558
558
|
return "";
|
|
559
559
|
}
|
|
560
|
-
return changedLines(current,
|
|
560
|
+
return changedLines(current, intended2);
|
|
561
561
|
}
|
|
562
562
|
function intendedAfterInverse(action) {
|
|
563
563
|
return action.snapshot === void 0 ? void 0 : { present: true, value: action.snapshot };
|
|
@@ -595,6 +595,98 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
|
|
|
595
595
|
return { ok: true };
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
// src/rollback/inspect.ts
|
|
599
|
+
import { z as z3 } from "zod";
|
|
600
|
+
var inversePlan2 = z3.object({
|
|
601
|
+
server: z3.string(),
|
|
602
|
+
tool: z3.string(),
|
|
603
|
+
args: z3.record(z3.string(), z3.unknown())
|
|
604
|
+
});
|
|
605
|
+
var observation2 = z3.union([
|
|
606
|
+
z3.object({ present: z3.literal(true), value: z3.unknown() }),
|
|
607
|
+
z3.object({ present: z3.literal(false) })
|
|
608
|
+
]);
|
|
609
|
+
function sameState2(a, b) {
|
|
610
|
+
return canonical(a) === canonical(b);
|
|
611
|
+
}
|
|
612
|
+
function intended(action) {
|
|
613
|
+
return action.snapshot === void 0 ? void 0 : { present: true, value: action.snapshot };
|
|
614
|
+
}
|
|
615
|
+
function resourceKey(plan) {
|
|
616
|
+
return canonical({ server: plan.server, tool: plan.tool, args: plan.args });
|
|
617
|
+
}
|
|
618
|
+
var SETTLED = /* @__PURE__ */ new Set(["failed", "denied", "gated", "approved"]);
|
|
619
|
+
async function inspect(options) {
|
|
620
|
+
const { journal, router, runId } = options;
|
|
621
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
622
|
+
const actions = journal.getActions(runId);
|
|
623
|
+
const resources = [];
|
|
624
|
+
const seen = /* @__PURE__ */ new Set();
|
|
625
|
+
for (const action of [...actions].reverse()) {
|
|
626
|
+
const at = { seq: action.seq, server: action.server, tool: action.tool };
|
|
627
|
+
if (action.class === "readonly") {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (SETTLED.has(action.status)) {
|
|
631
|
+
resources.push({ ...at, condition: "not-applied", note: action.status });
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (action.status === "rolled_back") {
|
|
635
|
+
resources.push({ ...at, condition: "restored" });
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
const verify = inversePlan2.safeParse(action.verify);
|
|
639
|
+
const post = observation2.safeParse(action.postSnapshot);
|
|
640
|
+
if (!verify.success || !post.success) {
|
|
641
|
+
resources.push({
|
|
642
|
+
...at,
|
|
643
|
+
condition: "unknowable",
|
|
644
|
+
note: action.inverse === void 0 ? "nothing was captured to restore" : "no pre-read was declared"
|
|
645
|
+
});
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const key = resourceKey(verify.data);
|
|
649
|
+
if (seen.has(key)) {
|
|
650
|
+
resources.push({ ...at, condition: "superseded" });
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
seen.add(key);
|
|
654
|
+
let current;
|
|
655
|
+
try {
|
|
656
|
+
current = await observeState(router, verify.data, signal);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
resources.push({ ...at, condition: "unknowable", note: `could not read it: ${describe(error)}` });
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (sameState2(current, post.data)) {
|
|
662
|
+
resources.push({ ...at, condition: "unchanged" });
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const back = intended(action);
|
|
666
|
+
if (back !== void 0 && sameState2(current, back)) {
|
|
667
|
+
resources.push({ ...at, condition: "restored", note: "put back outside this journal" });
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
resources.push({
|
|
671
|
+
...at,
|
|
672
|
+
condition: "changed",
|
|
673
|
+
diff: changedLines(post.data, current)
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return { runId, resources: resources.reverse() };
|
|
677
|
+
}
|
|
678
|
+
function verdict(inspection) {
|
|
679
|
+
const changed = inspection.resources.filter((r) => r.condition === "changed").length;
|
|
680
|
+
const undoable = inspection.resources.filter((r) => r.condition === "unchanged").length;
|
|
681
|
+
if (changed > 0) {
|
|
682
|
+
return `${String(changed)} changed since this ran; undoing would write over ${changed === 1 ? "it" : "them"}`;
|
|
683
|
+
}
|
|
684
|
+
if (undoable > 0) {
|
|
685
|
+
return `nothing has been touched since; all ${String(undoable)} would undo cleanly`;
|
|
686
|
+
}
|
|
687
|
+
return "nothing here is still applied";
|
|
688
|
+
}
|
|
689
|
+
|
|
598
690
|
// src/watch.ts
|
|
599
691
|
import { existsSync as existsSync2 } from "fs";
|
|
600
692
|
|
|
@@ -1868,13 +1960,14 @@ function footer(screen, options) {
|
|
|
1868
1960
|
return ["", ` ${style.accent(what)} ${keyHint2("y", "yes")} ${keyHint2("n", "no")}`];
|
|
1869
1961
|
}
|
|
1870
1962
|
const keys = screen.mode === "connections" ? [keyHint2("enter", "connect"), keyHint2("a", "connect all"), keyHint2("r", "rescan"), keyHint2("j/k", "move"), keyHint2("h", "back")] : screen.mode === "gates" ? [keyHint2("a", "approve"), keyHint2("d", "deny"), keyHint2("j/k", "move"), keyHint2("r", "runs")] : screen.mode === "run" ? [
|
|
1963
|
+
keyHint2("l", "check now"),
|
|
1871
1964
|
keyHint2("p", "preview undo"),
|
|
1872
1965
|
keyHint2("u", "undo"),
|
|
1873
|
-
keyHint2("
|
|
1874
|
-
keyHint2("
|
|
1966
|
+
keyHint2("f", "expand"),
|
|
1967
|
+
keyHint2("esc", "back")
|
|
1875
1968
|
] : [
|
|
1876
1969
|
keyHint2("enter", "open"),
|
|
1877
|
-
keyHint2("
|
|
1970
|
+
keyHint2("l", "check now"),
|
|
1878
1971
|
keyHint2("u", "undo"),
|
|
1879
1972
|
keyHint2("j/k", "move"),
|
|
1880
1973
|
keyHint2("g", "held")
|
|
@@ -1962,6 +2055,7 @@ async function openConsole(options) {
|
|
|
1962
2055
|
""
|
|
1963
2056
|
].join("\n");
|
|
1964
2057
|
};
|
|
2058
|
+
const onASession = () => screen.mode === "runs" || screen.mode === "run";
|
|
1965
2059
|
const selectedRun = (ready) => {
|
|
1966
2060
|
if (screen.mode === "run" && screen.openRun !== void 0) {
|
|
1967
2061
|
return ready.getRun(screen.openRun);
|
|
@@ -2049,6 +2143,28 @@ async function openConsole(options) {
|
|
|
2049
2143
|
screen.busy = void 0;
|
|
2050
2144
|
}
|
|
2051
2145
|
};
|
|
2146
|
+
const look = async (runId) => {
|
|
2147
|
+
if (options.check === void 0) {
|
|
2148
|
+
say("no way to read the current state was configured");
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
screen.busy = "reading how things are now...";
|
|
2152
|
+
try {
|
|
2153
|
+
const found = await options.check(runId);
|
|
2154
|
+
const changed = found.resources.filter((one) => one.condition === "changed");
|
|
2155
|
+
say(
|
|
2156
|
+
[
|
|
2157
|
+
verdict(found),
|
|
2158
|
+
...changed.length === 0 ? [] : ["", ...changed.flatMap((one) => [`${String(one.seq)} ${one.server}.${one.tool}`, ...(one.diff ?? "").split("\n")])]
|
|
2159
|
+
].join("\n"),
|
|
2160
|
+
changed.length === 0 ? NOTICE_TICKS2 : READING_TICKS
|
|
2161
|
+
);
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
say(error instanceof Error ? error.message : "could not read the current state");
|
|
2164
|
+
} finally {
|
|
2165
|
+
screen.busy = void 0;
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2052
2168
|
const rescan = () => {
|
|
2053
2169
|
screen.groups = options.scan?.() ?? [];
|
|
2054
2170
|
};
|
|
@@ -2160,7 +2276,26 @@ async function openConsole(options) {
|
|
|
2160
2276
|
decide(false);
|
|
2161
2277
|
}
|
|
2162
2278
|
return;
|
|
2279
|
+
case "l": {
|
|
2280
|
+
if (!onASession()) {
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if (screen.busy !== void 0) {
|
|
2284
|
+
say("still working on the last one");
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const ready = open();
|
|
2288
|
+
const run = ready === void 0 ? void 0 : selectedRun(ready);
|
|
2289
|
+
if (ready === void 0 || run === void 0) {
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
void look(run.id);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2163
2295
|
case "p": {
|
|
2296
|
+
if (!onASession()) {
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2164
2299
|
const ready = open();
|
|
2165
2300
|
const run = ready === void 0 ? void 0 : selectedRun(ready);
|
|
2166
2301
|
if (ready === void 0 || run === void 0) {
|
|
@@ -2175,6 +2310,10 @@ async function openConsole(options) {
|
|
|
2175
2310
|
return;
|
|
2176
2311
|
}
|
|
2177
2312
|
case "u": {
|
|
2313
|
+
if (!onASession()) {
|
|
2314
|
+
say("undo works on a session; r for the list");
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2178
2317
|
if (screen.busy !== void 0) {
|
|
2179
2318
|
say("still working on the last one");
|
|
2180
2319
|
return;
|
|
@@ -2241,6 +2380,7 @@ async function openConsole(options) {
|
|
|
2241
2380
|
for (; !screen.stop; tick += 1) {
|
|
2242
2381
|
if (screen.notice !== "" && tick >= screen.noticeUntil) {
|
|
2243
2382
|
screen.notice = "";
|
|
2383
|
+
screen.warned = void 0;
|
|
2244
2384
|
}
|
|
2245
2385
|
options.write(clear + frame());
|
|
2246
2386
|
if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
|
|
@@ -2289,7 +2429,7 @@ var COMMANDS = `
|
|
|
2289
2429
|
synartesis init <server> -- <command> [args...] [--manifest <path>]
|
|
2290
2430
|
synartesis check [--manifest <path>]
|
|
2291
2431
|
synartesis list [--journal <path>]
|
|
2292
|
-
synartesis show <runId> [--full] [--journal <path>]
|
|
2432
|
+
synartesis show <runId> [--full] [--live] [--journal <path>]
|
|
2293
2433
|
synartesis gates [--journal <path>]
|
|
2294
2434
|
synartesis close [runId] [--journal <path>]
|
|
2295
2435
|
synartesis prune [--older-than <days>] [--dry-run] [--journal <path>]
|
|
@@ -2329,6 +2469,7 @@ resource back as the run left it and --replan, or --force to overwrite.
|
|
|
2329
2469
|
--client claude-code, claude-desktop, cursor or codex; all by default
|
|
2330
2470
|
--print show the entries install would write, and write nothing
|
|
2331
2471
|
--full show every argument, snapshot and inverse in full, nothing elided
|
|
2472
|
+
--live read each resource as it is now and say what has changed since
|
|
2332
2473
|
--server serve one server from the manifest, keeping its tool names
|
|
2333
2474
|
--manifest synartesis.yaml, looked for here and upwards, then in the home
|
|
2334
2475
|
--journal beside the manifest, or the one in the home
|
|
@@ -2743,13 +2884,24 @@ function runList(journal, asJson, journalPath) {
|
|
|
2743
2884
|
}
|
|
2744
2885
|
return 0;
|
|
2745
2886
|
}
|
|
2746
|
-
function runShow(argv, journal, asJson) {
|
|
2887
|
+
async function runShow(argv, journal, asJson) {
|
|
2747
2888
|
const full = argv.includes("--full");
|
|
2748
2889
|
const runs = [...journal.listRuns()].reverse();
|
|
2749
2890
|
const run = pick(runs, positional(argv)[1], RUN, true);
|
|
2750
2891
|
const runId = run.id;
|
|
2892
|
+
const inspection = argv.includes("--live") && journal.getActions(runId).length > 0 ? await withUpstreams(
|
|
2893
|
+
findManifest(flag(argv, "--manifest")),
|
|
2894
|
+
async (router) => await inspect({ journal, router, runId }),
|
|
2895
|
+
serversUsedBy(journal, runId)
|
|
2896
|
+
) : void 0;
|
|
2751
2897
|
if (asJson) {
|
|
2752
|
-
out(
|
|
2898
|
+
out(
|
|
2899
|
+
JSON.stringify({
|
|
2900
|
+
run,
|
|
2901
|
+
actions: journal.getActions(runId),
|
|
2902
|
+
...inspection === void 0 ? {} : { live: inspection.resources }
|
|
2903
|
+
})
|
|
2904
|
+
);
|
|
2753
2905
|
return 0;
|
|
2754
2906
|
}
|
|
2755
2907
|
out("");
|
|
@@ -2772,10 +2924,17 @@ function runShow(argv, journal, asJson) {
|
|
|
2772
2924
|
out(` ${style.label("timeline")}`);
|
|
2773
2925
|
out(` ${rule(72)}`);
|
|
2774
2926
|
out("");
|
|
2927
|
+
const live = new Map((inspection?.resources ?? []).map((found) => [found.seq, found]));
|
|
2775
2928
|
for (const action of actions) {
|
|
2929
|
+
const now = live.get(action.seq);
|
|
2776
2930
|
out(
|
|
2777
|
-
` ${style.quiet(String(action.seq).padStart(3))} ${badgeOf(action)} ${statusOf(action)} ${style.strong(`${action.server}.${action.tool}`)}`
|
|
2931
|
+
` ${style.quiet(String(action.seq).padStart(3))} ${badgeOf(action)} ${statusOf(action)} ${style.strong(`${action.server}.${action.tool}`)}` + (now === void 0 ? "" : ` ${conditionOf(now)}`)
|
|
2778
2932
|
);
|
|
2933
|
+
if (now?.diff !== void 0) {
|
|
2934
|
+
for (const line2 of now.diff.split("\n")) {
|
|
2935
|
+
out(` ${style.quiet(line2)}`);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2779
2938
|
if (full) {
|
|
2780
2939
|
out(` ${style.quiet("arguments")}`);
|
|
2781
2940
|
for (const line2 of block(action.args)) {
|
|
@@ -2812,9 +2971,40 @@ function runShow(argv, journal, asJson) {
|
|
|
2812
2971
|
}
|
|
2813
2972
|
out("");
|
|
2814
2973
|
out(` ${summarise2(actions)}`);
|
|
2974
|
+
if (inspection !== void 0) {
|
|
2975
|
+
out("");
|
|
2976
|
+
const spoiled = inspection.resources.some((found) => found.condition === "changed");
|
|
2977
|
+
out(` ${spoiled ? style.accent(verdict(inspection)) : style.quiet(verdict(inspection))}`);
|
|
2978
|
+
if (spoiled) {
|
|
2979
|
+
out(
|
|
2980
|
+
` ${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")}`
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
} else {
|
|
2984
|
+
out("");
|
|
2985
|
+
out(
|
|
2986
|
+
` ${style.quiet("has anything changed since? ")}${style.strong(`${cliCommand()} show ${runId.slice(0, 8)} --live`)}`
|
|
2987
|
+
);
|
|
2988
|
+
}
|
|
2815
2989
|
out("");
|
|
2816
2990
|
return 0;
|
|
2817
2991
|
}
|
|
2992
|
+
function conditionOf(found) {
|
|
2993
|
+
switch (found.condition) {
|
|
2994
|
+
case "changed":
|
|
2995
|
+
return style.accent("changed since");
|
|
2996
|
+
case "unchanged":
|
|
2997
|
+
return style.quiet("unchanged");
|
|
2998
|
+
case "restored":
|
|
2999
|
+
return style.quiet(found.note === void 0 ? "back to before" : `back to before, ${found.note}`);
|
|
3000
|
+
case "superseded":
|
|
3001
|
+
return style.quiet("older write to the same thing");
|
|
3002
|
+
case "not-applied":
|
|
3003
|
+
return style.quiet(`never applied (${found.note ?? "settled"})`);
|
|
3004
|
+
default:
|
|
3005
|
+
return style.quiet(found.note ?? "cannot tell");
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
2818
3008
|
var CLASS_MARK = {
|
|
2819
3009
|
readonly: "\xB7",
|
|
2820
3010
|
reversible: "\u2190",
|
|
@@ -3105,45 +3295,63 @@ function report(result, alreadyForcing = false) {
|
|
|
3105
3295
|
out("");
|
|
3106
3296
|
return result.status === "rolled_back" ? 0 : 1;
|
|
3107
3297
|
}
|
|
3108
|
-
async function
|
|
3298
|
+
async function withUpstreams(manifestPath, use, only) {
|
|
3109
3299
|
const manifest = loadManifest(manifestPath);
|
|
3110
3300
|
const upstreams = [];
|
|
3301
|
+
const missing = [];
|
|
3111
3302
|
try {
|
|
3112
3303
|
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
...options.replan === true ? { replanWith: manifest } : {}
|
|
3129
|
-
};
|
|
3130
|
-
if (options.preflight === true) {
|
|
3131
|
-
const seen = await rollback({ ...base, dryRun: true });
|
|
3132
|
-
if (seen.halted?.conflict === true) {
|
|
3133
|
-
return seen;
|
|
3304
|
+
if (only !== void 0 && !only.has(name)) {
|
|
3305
|
+
continue;
|
|
3306
|
+
}
|
|
3307
|
+
try {
|
|
3308
|
+
upstreams.push(
|
|
3309
|
+
await connectStdioUpstream({
|
|
3310
|
+
name,
|
|
3311
|
+
command: spec.command,
|
|
3312
|
+
args: spec.args,
|
|
3313
|
+
stderr: "capture",
|
|
3314
|
+
...spec.env === void 0 ? {} : { env: spec.env }
|
|
3315
|
+
})
|
|
3316
|
+
);
|
|
3317
|
+
} catch (error) {
|
|
3318
|
+
missing.push(`${name}: ${describe(error)}`);
|
|
3134
3319
|
}
|
|
3135
3320
|
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3321
|
+
if (upstreams.length === 0 && missing.length > 0) {
|
|
3322
|
+
throw new ManifestError(`no server could be started. ${missing.join("; ")}`);
|
|
3323
|
+
}
|
|
3324
|
+
for (const why of missing) {
|
|
3325
|
+
process.stderr.write(`synartesis: ${why}; anything through it cannot be reached
|
|
3326
|
+
`);
|
|
3327
|
+
}
|
|
3328
|
+
return await use(createRouter(upstreams, manifest), manifest);
|
|
3141
3329
|
} finally {
|
|
3142
3330
|
for (const upstream of upstreams) {
|
|
3143
3331
|
await upstream.close();
|
|
3144
3332
|
}
|
|
3145
3333
|
}
|
|
3146
3334
|
}
|
|
3335
|
+
function serversUsedBy(journal, runId) {
|
|
3336
|
+
return new Set(journal.getActions(runId).map((action) => action.server));
|
|
3337
|
+
}
|
|
3338
|
+
async function performUndo(manifestPath, journal, runId, options) {
|
|
3339
|
+
return await withUpstreams(
|
|
3340
|
+
manifestPath,
|
|
3341
|
+
async (router, manifest) => await rollback({
|
|
3342
|
+
journal,
|
|
3343
|
+
router,
|
|
3344
|
+
runId,
|
|
3345
|
+
...options.toSeq === void 0 ? {} : { toSeq: options.toSeq },
|
|
3346
|
+
...options.replan === true ? { replanWith: manifest } : {},
|
|
3347
|
+
dryRun: options.dryRun,
|
|
3348
|
+
...options.force === true ? { force: true } : {}
|
|
3349
|
+
}),
|
|
3350
|
+
// A replan re-resolves inverses from the current policy, which may name a
|
|
3351
|
+
// server this run never used; everything else needs only what it touched.
|
|
3352
|
+
options.replan === true ? void 0 : serversUsedBy(journal, runId)
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3147
3355
|
async function runUndo(argv, journal) {
|
|
3148
3356
|
const given = positional(argv)[1];
|
|
3149
3357
|
const chosen = pick([...journal.listRuns()].reverse(), given, RUN, true);
|
|
@@ -3195,21 +3403,48 @@ async function runUndo(argv, journal) {
|
|
|
3195
3403
|
}
|
|
3196
3404
|
const forcing = argv.includes("--force");
|
|
3197
3405
|
const said = argv.includes("--yes");
|
|
3198
|
-
const
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3406
|
+
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
3407
|
+
if (said && !forcing) {
|
|
3408
|
+
process.stderr.write("synartesis: --yes only means anything with --force; ignoring it\n");
|
|
3409
|
+
}
|
|
3410
|
+
if (forcing && !said) {
|
|
3411
|
+
const over = (await withUpstreams(
|
|
3412
|
+
manifestPath,
|
|
3413
|
+
async (router) => await inspect({ journal, router, runId }),
|
|
3414
|
+
serversUsedBy(journal, runId)
|
|
3415
|
+
)).resources.filter(
|
|
3416
|
+
// Below --to nothing is undone, so a change down there is not something
|
|
3417
|
+
// this command would write over and must not stand in its way.
|
|
3418
|
+
(one) => one.condition === "changed" && (toSeq === void 0 || one.seq >= toSeq)
|
|
3209
3419
|
);
|
|
3210
|
-
|
|
3420
|
+
if (over.length > 0) {
|
|
3421
|
+
out("");
|
|
3422
|
+
out(
|
|
3423
|
+
` ${style.accent(`${String(over.length)} changed since this ran`)} ` + style.quiet(`undoing would write over ${over.length === 1 ? "it" : "them"}`)
|
|
3424
|
+
);
|
|
3425
|
+
for (const one of over) {
|
|
3426
|
+
out("");
|
|
3427
|
+
out(` ${style.quiet(String(one.seq).padStart(3))} ${style.strong(`${one.server}.${one.tool}`)}`);
|
|
3428
|
+
for (const line2 of (one.diff ?? "").split("\n")) {
|
|
3429
|
+
out(` ${style.quiet(line2)}`);
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
out("");
|
|
3433
|
+
out(` ${style.quiet("nothing has been written. To go ahead and lose that:")}`);
|
|
3434
|
+
out(` ${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force --yes`)}`);
|
|
3435
|
+
out("");
|
|
3436
|
+
return 1;
|
|
3437
|
+
}
|
|
3211
3438
|
}
|
|
3212
|
-
return
|
|
3439
|
+
return report(
|
|
3440
|
+
await performUndo(manifestPath, journal, runId, {
|
|
3441
|
+
dryRun: argv.includes("--dry-run"),
|
|
3442
|
+
...toSeq === void 0 ? {} : { toSeq },
|
|
3443
|
+
replan: argv.includes("--replan"),
|
|
3444
|
+
...forcing && said ? { force: true } : {}
|
|
3445
|
+
}),
|
|
3446
|
+
forcing
|
|
3447
|
+
);
|
|
3213
3448
|
}
|
|
3214
3449
|
var FLAGS = /* @__PURE__ */ new Set([
|
|
3215
3450
|
// Two lists have to agree about a flag: this one decides whether it is
|
|
@@ -3219,6 +3454,7 @@ var FLAGS = /* @__PURE__ */ new Set([
|
|
|
3219
3454
|
"--client",
|
|
3220
3455
|
"--print",
|
|
3221
3456
|
"--full",
|
|
3457
|
+
"--live",
|
|
3222
3458
|
"--manifest",
|
|
3223
3459
|
"--journal",
|
|
3224
3460
|
"--to",
|
|
@@ -3292,6 +3528,18 @@ ${COMMANDS}`);
|
|
|
3292
3528
|
write: (text) => process.stdout.write(text),
|
|
3293
3529
|
live: process.stdout.isTTY,
|
|
3294
3530
|
decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown",
|
|
3531
|
+
check: async (runId) => {
|
|
3532
|
+
const journal2 = openJournal(journalPath2, { mustExist: true });
|
|
3533
|
+
try {
|
|
3534
|
+
return await withUpstreams(
|
|
3535
|
+
manifestPath,
|
|
3536
|
+
async (router) => await inspect({ journal: journal2, router, runId }),
|
|
3537
|
+
serversUsedBy(journal2, runId)
|
|
3538
|
+
);
|
|
3539
|
+
} finally {
|
|
3540
|
+
journal2.close();
|
|
3541
|
+
}
|
|
3542
|
+
},
|
|
3295
3543
|
undo: async (runId, dryRun, force) => {
|
|
3296
3544
|
const journal2 = openJournal(journalPath2, { mustExist: true });
|
|
3297
3545
|
try {
|
|
@@ -3356,7 +3604,7 @@ ${COMMANDS}`);
|
|
|
3356
3604
|
case "list":
|
|
3357
3605
|
return runList(journal, asJson, journalPath);
|
|
3358
3606
|
case "show":
|
|
3359
|
-
return runShow(argv, journal, asJson);
|
|
3607
|
+
return await runShow(argv, journal, asJson);
|
|
3360
3608
|
case "close":
|
|
3361
3609
|
return runClose(argv, journal);
|
|
3362
3610
|
case "prune":
|