@tiens.nguyen/gu-cli 1.0.768 → 1.0.770
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/agent-stop.mjs +22 -5
- package/client-expert-api.mjs +89 -8
- package/client-expert-run.mjs +104 -12
- package/gonext_agent_chat.py +217 -11
- package/gu-cli.mjs +56 -29
- package/gu-repl.mjs +311 -31
- package/image-paste.mjs +10 -0
- package/model-setup-ui.mjs +114 -0
- package/ollama-setup.mjs +128 -23
- package/package.json +5 -1
- package/setup-prompts.mjs +82 -0
- package/terminal-layout.mjs +31 -1
- package/turn-stop.mjs +78 -0
- package/workspace-revert.mjs +106 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UNDO AN AGENT RUN'S FILE CHANGES — shared by `gu-cli workspace revert` and the terminal's /undo
|
|
3
|
+
* (task #302).
|
|
4
|
+
*
|
|
5
|
+
* Every agent run keeps ~/.gonext/workspace-backups/<runId>/: a copy of each file taken before its
|
|
6
|
+
* FIRST edit in that run, and manifest.json listing every change (gonext_agent_chat.py
|
|
7
|
+
* _ws_snapshot / _ws_record_change). Undo puts the ORIGINAL state back.
|
|
8
|
+
*
|
|
9
|
+
* WHY A PLAN, NOT A REPLAY. The manifest is a log, and a file can appear in it more than once. The
|
|
10
|
+
* old revert replayed it in order — so a file the run CREATED and then edited was deleted (the
|
|
11
|
+
* "create" entry) and then brought back by the "edit" entry's backup, which is a copy of the file
|
|
12
|
+
* the run had just created. What matters is each path's EARLIEST entry: it says what the file was
|
|
13
|
+
* before the run touched it. Created → delete it; edited → restore the copy taken before the first
|
|
14
|
+
* edit; a created folder → remove it only if empty.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
17
|
+
import { copyFile, readFile, rmdir, unlink } from "node:fs/promises";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
export const BACKUPS_ROOT = join(homedir(), ".gonext", "workspace-backups");
|
|
22
|
+
|
|
23
|
+
/** The manifest's changes → one step per path, from its earliest entry. Pure. */
|
|
24
|
+
export function planRevert(changes) {
|
|
25
|
+
const first = new Map();
|
|
26
|
+
for (const c of Array.isArray(changes) ? changes : []) {
|
|
27
|
+
if (!c || typeof c.path !== "string" || !c.path) continue;
|
|
28
|
+
if (!first.has(c.path)) first.set(c.path, c);
|
|
29
|
+
}
|
|
30
|
+
const steps = [];
|
|
31
|
+
for (const c of first.values()) {
|
|
32
|
+
if (c.action === "edit" && c.backup) steps.push({ op: "restore", path: c.path, backup: c.backup });
|
|
33
|
+
else if (c.action === "create") steps.push({ op: "delete", path: c.path });
|
|
34
|
+
else if (c.action === "create_dir") steps.push({ op: "rmdir", path: c.path });
|
|
35
|
+
}
|
|
36
|
+
// Files before folders, deepest folders first — a created folder can only go once it is empty.
|
|
37
|
+
const rank = { restore: 0, delete: 0, rmdir: 1 };
|
|
38
|
+
return steps.sort((a, b) => rank[a.op] - rank[b.op] || (a.op === "rmdir" ? b.path.length - a.path.length : 0));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The run directories, oldest first (names are timestamps, so they sort). */
|
|
42
|
+
export function listRuns(root = BACKUPS_ROOT) {
|
|
43
|
+
try {
|
|
44
|
+
return readdirSync(root).filter((d) => !d.startsWith(".")).sort();
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The run a turn produced: the newest run whose manifest was written at or after `sinceMs`.
|
|
52
|
+
* Every run is one agent process, and every turn is one process — so a run touched after the turn
|
|
53
|
+
* began belongs to it. null when the turn changed nothing (or ran on another machine).
|
|
54
|
+
*/
|
|
55
|
+
export function runSince(sinceMs, root = BACKUPS_ROOT) {
|
|
56
|
+
const runs = listRuns(root).reverse();
|
|
57
|
+
for (const id of runs) {
|
|
58
|
+
try {
|
|
59
|
+
if (statSync(join(root, id, "manifest.json")).mtimeMs >= sinceMs - 1000) return id;
|
|
60
|
+
} catch {
|
|
61
|
+
/* a run with no manifest changed nothing */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A run's changes, or null when it has no readable manifest. */
|
|
68
|
+
export async function readRunChanges(runId, root = BACKUPS_ROOT) {
|
|
69
|
+
try {
|
|
70
|
+
const m = JSON.parse(await readFile(join(root, runId, "manifest.json"), "utf8"));
|
|
71
|
+
return Array.isArray(m?.changes) ? m.changes : [];
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Carry out a plan. → { restored, deleted, failed, lines } — `lines` says what happened to each
|
|
79
|
+
* path, for the caller to print. Never throws; a step that fails is reported and the rest go on.
|
|
80
|
+
*/
|
|
81
|
+
export async function applyRevert(steps, { exists = existsSync } = {}) {
|
|
82
|
+
let restored = 0, deleted = 0, failed = 0;
|
|
83
|
+
const lines = [];
|
|
84
|
+
for (const s of steps) {
|
|
85
|
+
try {
|
|
86
|
+
if (s.op === "restore" && exists(s.backup)) {
|
|
87
|
+
await copyFile(s.backup, s.path);
|
|
88
|
+
restored += 1;
|
|
89
|
+
lines.push(`restored ${s.path}`);
|
|
90
|
+
} else if (s.op === "delete" && exists(s.path)) {
|
|
91
|
+
await unlink(s.path);
|
|
92
|
+
deleted += 1;
|
|
93
|
+
lines.push(`deleted ${s.path}`);
|
|
94
|
+
} else if (s.op === "rmdir" && exists(s.path)) {
|
|
95
|
+
// rmdir removes only an EMPTY folder — undo never destroys what someone put inside later.
|
|
96
|
+
await rmdir(s.path);
|
|
97
|
+
deleted += 1;
|
|
98
|
+
lines.push(`removed ${s.path}/`);
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
failed += 1;
|
|
102
|
+
lines.push(`FAILED ${s.path}: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { restored, deleted, failed, lines };
|
|
106
|
+
}
|