pi-undo-redo 0.1.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/README.md +67 -0
- package/package.json +56 -0
- package/scripts/publish.mjs +34 -0
- package/src/extension.ts +1376 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# pi-undo-redo
|
|
2
|
+
|
|
3
|
+
Undo/redo for Pi conversations and workspace changes with hybrid git/non-git behavior.
|
|
4
|
+
|
|
5
|
+
In git repositories this extension keeps file snapshots in a shadow git repository and records per-message patch metadata in the Pi session. `/undo` navigates the conversation tree back to the previous user message and reverts only the files changed by the undone message(s). `/redo` restores the files and conversation leaf captured before undo.
|
|
6
|
+
|
|
7
|
+
In non-git directories it does **not** scan or snapshot the whole workspace. It snapshots only explicit file paths touched by Pi file tools that provide a path (`write` and `edit`). Shell commands and custom tools without explicit path metadata are not parsed for paths and are not promised to be undo-restorable.
|
|
8
|
+
|
|
9
|
+
A dirty guard blocks `/undo`, `/redo`, and `/tree` when unsnapshotted workspace changes would be overwritten.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
- `/undo` - undo the latest checkpointed user message and restore changed files
|
|
14
|
+
- `/redo` - restore the last undone conversation/file state
|
|
15
|
+
- `/undo-cleanup` - conservatively prune old protected snapshot refs while keeping snapshots referenced by the current session state
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Configure in `~/.pi/agent/settings.json` or `.pi/settings.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"undoRedo": {
|
|
24
|
+
"storageDir": "E:/pi-undo-redo",
|
|
25
|
+
"largeFileLimitBytes": 2097152,
|
|
26
|
+
"gitTimeoutMs": 30000,
|
|
27
|
+
"enabled": true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Defaults:
|
|
33
|
+
|
|
34
|
+
- `storageDir`: `~/.pi/agent/state/pi-undo-redo`
|
|
35
|
+
- `largeFileLimitBytes`: `2097152` (2 MiB; large explicit non-git files are skipped/fail closed with a warning instead of silently claiming undo support)
|
|
36
|
+
- `gitTimeoutMs`: `30000`
|
|
37
|
+
- `enabled`: `true`
|
|
38
|
+
|
|
39
|
+
## Hybrid behavior
|
|
40
|
+
|
|
41
|
+
### Git repositories
|
|
42
|
+
|
|
43
|
+
- Uses the existing shadow git snapshot behavior.
|
|
44
|
+
- Catches tracked modifications and ordinary non-ignored untracked files, including files created by shell/filesystem commands.
|
|
45
|
+
- Respects `.gitignore` and `.git/info/exclude` from the source repository.
|
|
46
|
+
- Maintains a shadow snapshot of source tracked files plus changed ordinary non-ignored untracked files; ignored files and large untracked files are not copied into the shadow repository.
|
|
47
|
+
- `/tree` restores the workspace snapshot that matches the selected conversation node.
|
|
48
|
+
|
|
49
|
+
### Non-git directories
|
|
50
|
+
|
|
51
|
+
- Does not root-wide scan/snapshot the cwd, home directory, or project root.
|
|
52
|
+
- Before-agent snapshots are empty and cheap; when a Pi `write` or `edit` tool is about to run, the extension snapshots only that explicit path's current state.
|
|
53
|
+
- Finalization snapshots after-state for those touched files only, supporting create/modify/delete for normal files where possible.
|
|
54
|
+
- `/undo` and `/redo` restore only `checkpoint.files` from the session checkpoint.
|
|
55
|
+
- `/tree` restores only the cumulative explicit checkpoint files along the branch up to the target node; it never restores or diffs a whole cwd snapshot.
|
|
56
|
+
- Dirty guard checks only affected checkpoint/touched paths, so unrelated sentinel files are not included.
|
|
57
|
+
- Shell-created files in non-git mode are not parsed or restored. The extension warns once per agent turn when a bash tool runs in non-git mode.
|
|
58
|
+
|
|
59
|
+
## Path safety in non-git mode
|
|
60
|
+
|
|
61
|
+
Explicit paths are normalized relative to the workspace root. Relative paths resolve against Pi's current `ctx.cwd`; absolute paths are accepted only when they remain inside the workspace root. Paths containing NUL, escaping via `..`, or targeting dangerous workspace directories such as `.git`, `.pi`, or `node_modules` are rejected. Windows-style paths normalize to `/`-separated relative paths in checkpoint metadata.
|
|
62
|
+
|
|
63
|
+
## Notes
|
|
64
|
+
|
|
65
|
+
- Snapshots are protected with internal shadow-git refs so stored tree objects are not immediately lost to Git GC.
|
|
66
|
+
- Restore avoids recursive directory deletion for file/dir conflicts; unsafe conflicts fail closed instead of deleting unknown directory contents.
|
|
67
|
+
- Non-git mode intentionally trades shell/filesystem auto-discovery for safety and bounded scope. Use a git repository when full OpenCode-style shell/file-system change capture is required.
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-undo-redo",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Undo/redo for Pi conversations and workspace changes with hybrid git and non-git snapshots.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Yueby/pi-undo-redo.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Yueby/pi-undo-redo#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Yueby/pi-undo-redo/issues"
|
|
14
|
+
},
|
|
15
|
+
"main": "./src/extension.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"src/",
|
|
18
|
+
"README.md",
|
|
19
|
+
"package.json",
|
|
20
|
+
"scripts/publish.mjs"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"pi-extension",
|
|
25
|
+
"pi",
|
|
26
|
+
"pi-coding-agent",
|
|
27
|
+
"opencode",
|
|
28
|
+
"undo",
|
|
29
|
+
"redo",
|
|
30
|
+
"snapshot",
|
|
31
|
+
"rollback"
|
|
32
|
+
],
|
|
33
|
+
"pi": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./src/extension.ts"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "node scripts/self-test.mjs",
|
|
42
|
+
"release": "node scripts/publish.mjs"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.15.21",
|
|
46
|
+
"typescript": "^5.8.3"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@earendil-works/pi-coding-agent": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
process.chdir(root);
|
|
9
|
+
|
|
10
|
+
const registry = "https://registry.npmjs.org/";
|
|
11
|
+
|
|
12
|
+
if (!existsSync("package.json")) {
|
|
13
|
+
console.error("package.json not found; script root resolution failed");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
run("npm", ["run", "typecheck"]);
|
|
18
|
+
run("npm", ["test"]);
|
|
19
|
+
ensureNpmLogin();
|
|
20
|
+
run("npm", ["publish", "--access", "public", "--registry", registry, "--auth-type", "web"]);
|
|
21
|
+
|
|
22
|
+
function ensureNpmLogin() {
|
|
23
|
+
const whoami = run("npm", ["whoami", "--registry", registry], { exitOnError: false });
|
|
24
|
+
if (whoami.status === 0) return;
|
|
25
|
+
console.log("\nNot logged in to npm. Starting web login...");
|
|
26
|
+
run("npm", ["login", "--registry", registry, "--auth-type", "web"]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function run(command, args, options = {}) {
|
|
30
|
+
const result = spawnSync(command, args, { stdio: "inherit", shell: process.platform === "win32" });
|
|
31
|
+
if (result.error) throw result.error;
|
|
32
|
+
if (result.status !== 0 && options.exitOnError !== false) process.exit(result.status ?? 1);
|
|
33
|
+
return result;
|
|
34
|
+
}
|