rewindex 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +100 -2
- package/dist/cli.js +99 -0
- package/dist/commands.js +227 -0
- package/dist/config.js +75 -0
- package/dist/gc.js +97 -0
- package/dist/git.js +275 -0
- package/dist/hook.js +72 -0
- package/dist/install.js +207 -0
- package/dist/paths.js +81 -0
- package/dist/snapshot.js +118 -0
- package/dist/state.js +50 -0
- package/package.json +47 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zenonix Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# rewindex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
File-level rewind for OpenAI Codex CLI, including projects that do not use Git.
|
|
4
|
+
|
|
5
|
+
Codex can rewind a conversation, but that does not restore files already written to disk. `rewindex` takes automatic snapshots in an isolated shadow Git repository and lets you return the working tree to the state before an earlier prompt.
|
|
6
|
+
|
|
7
|
+
It never reads or modifies your project's own Git repository.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install --global rewindex
|
|
13
|
+
rewindex install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Open Codex in an unimportant folder, run `/hooks`, and trust the two `rewindex` hooks. Codex requires this explicit review before third-party hooks can run.
|
|
17
|
+
|
|
18
|
+
## Use inside Codex
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
$rewindex list
|
|
22
|
+
$rewindex diff 3
|
|
23
|
+
$rewindex restore 3
|
|
24
|
+
$rewindex undo
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`list` shows the newest 50 prompt checkpoints:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
1. dcdf6bb9f1 3 dk önce
|
|
31
|
+
1 değişen · 0 eklenen · 0 silinen
|
|
32
|
+
Prompt: README dosyasını yeni kurulum adımlarıyla güncelle.
|
|
33
|
+
Özet: README.md önceki haline döner.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The visible number is a convenient selector for the current list. The hash is a stable selector that remains valid if newer prompts change the numbering.
|
|
37
|
+
|
|
38
|
+
Restore always shows a diff first and asks for confirmation. Before changing files, it takes a safety snapshot; `$rewindex undo` restores that pre-restore state.
|
|
39
|
+
|
|
40
|
+
## Direct CLI
|
|
41
|
+
|
|
42
|
+
The same operations are available outside Codex:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
rewindex list
|
|
46
|
+
rewindex diff 3
|
|
47
|
+
rewindex restore 3
|
|
48
|
+
rewindex restore --undo
|
|
49
|
+
rewindex gc
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Add `-C <directory>` to target another project and `-y` to skip restore confirmation in scripts.
|
|
53
|
+
|
|
54
|
+
## How it works
|
|
55
|
+
|
|
56
|
+
1. `UserPromptSubmit` records the state before each prompt.
|
|
57
|
+
2. `PreToolUse` provides extra safety checkpoints before shell and file tools.
|
|
58
|
+
3. Snapshots are stored in `~/.rewindex/<project-hash>/`, with `GIT_DIR` isolated from the project.
|
|
59
|
+
4. Prompt history is presented as cumulative rewind points: restoring item 4 undoes that prompt and every newer file change.
|
|
60
|
+
|
|
61
|
+
Tracked, untracked, and Git-ignored files are captured by default. Common build directories and files larger than 16 MiB are excluded.
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
Optional `~/.rewindex/config.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"exclude": [".git", "node_modules", "dist", "build"],
|
|
70
|
+
"maxFileBytes": 16777216,
|
|
71
|
+
"retentionDays": 7,
|
|
72
|
+
"retentionBytes": 524288000,
|
|
73
|
+
"captureIgnored": true
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Security
|
|
78
|
+
|
|
79
|
+
Because Git-ignored files are captured by default, local snapshots may contain `.env` files, credentials, or other secrets. They remain unencrypted on the same machine under `~/.rewindex/` and are never added to the project repository.
|
|
80
|
+
|
|
81
|
+
Set `"captureIgnored": false` or add sensitive paths to `exclude` when local secret snapshots are not acceptable.
|
|
82
|
+
|
|
83
|
+
## Limits
|
|
84
|
+
|
|
85
|
+
- Rewinds files, not the Codex conversation. Use Codex's conversation controls separately.
|
|
86
|
+
- Cannot undo external effects such as database writes, network calls, or files changed outside the project.
|
|
87
|
+
- Does not currently support Windows.
|
|
88
|
+
- Cannot observe tools that bypass Codex hooks.
|
|
89
|
+
|
|
90
|
+
## Uninstall
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
rewindex uninstall
|
|
94
|
+
npm uninstall --global rewindex
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`rewindex uninstall` removes only hook entries owned by rewindex.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
[MIT](LICENSE) © 2026 Zenonix Studio
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const hook_1 = require("./hook");
|
|
5
|
+
const commands_1 = require("./commands");
|
|
6
|
+
const install_1 = require("./install");
|
|
7
|
+
const VERSION = "0.1.0";
|
|
8
|
+
function usage() {
|
|
9
|
+
console.log(`rewindex ${VERSION} — git-free /undo for OpenAI Codex CLI
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
rewindex install Add snapshot hooks and the $rewindex Codex skill
|
|
13
|
+
rewindex uninstall Remove rewindex hooks and skill (leaves yours alone)
|
|
14
|
+
rewindex list List snapshots (grouped by turn, with prompt + files changed)
|
|
15
|
+
rewindex diff <number|id> Show what restoring that prompt would change
|
|
16
|
+
rewindex restore <number|id> Restore that prompt baseline (preview + confirm)
|
|
17
|
+
rewindex restore --undo Undo the last restore (via its safety snapshot)
|
|
18
|
+
rewindex gc Prune old snapshots (default: 7 days / 500 MB)
|
|
19
|
+
rewindex hook (internal) read a hook payload on stdin and snapshot
|
|
20
|
+
|
|
21
|
+
Inside Codex after install:
|
|
22
|
+
$rewindex list
|
|
23
|
+
$rewindex diff <number|id>
|
|
24
|
+
$rewindex restore <number|id>
|
|
25
|
+
$rewindex undo
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
-y, --yes Skip the restore confirmation prompt
|
|
29
|
+
-C, --cwd <dir> Operate on <dir> instead of the current directory
|
|
30
|
+
-h, --help Show this help
|
|
31
|
+
-v, --version Show version
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
function parse(argv) {
|
|
35
|
+
const out = {
|
|
36
|
+
positionals: [],
|
|
37
|
+
yes: false,
|
|
38
|
+
undo: false,
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
};
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i];
|
|
43
|
+
if (a === "-y" || a === "--yes")
|
|
44
|
+
out.yes = true;
|
|
45
|
+
else if (a === "--undo")
|
|
46
|
+
out.undo = true;
|
|
47
|
+
else if (a === "-C" || a === "--cwd")
|
|
48
|
+
out.cwd = argv[++i] ?? out.cwd;
|
|
49
|
+
else if (!out.cmd && !a.startsWith("-"))
|
|
50
|
+
out.cmd = a;
|
|
51
|
+
else if (!a.startsWith("-"))
|
|
52
|
+
out.positionals.push(a);
|
|
53
|
+
else
|
|
54
|
+
out.positionals.push(a); // keep unknown flags as positionals for help
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
async function main() {
|
|
59
|
+
const argv = process.argv.slice(2);
|
|
60
|
+
if (argv.includes("-h") || argv.includes("--help") || argv.length === 0) {
|
|
61
|
+
usage();
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (argv.includes("-v") || argv.includes("--version")) {
|
|
65
|
+
console.log(VERSION);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
const args = parse(argv);
|
|
69
|
+
switch (args.cmd) {
|
|
70
|
+
case "hook":
|
|
71
|
+
return (0, hook_1.hookCommand)();
|
|
72
|
+
case "install":
|
|
73
|
+
return (0, install_1.installCommand)();
|
|
74
|
+
case "uninstall":
|
|
75
|
+
return (0, install_1.uninstallCommand)();
|
|
76
|
+
case "list":
|
|
77
|
+
case "ls":
|
|
78
|
+
return (0, commands_1.listCommand)(args.cwd);
|
|
79
|
+
case "diff":
|
|
80
|
+
return (0, commands_1.diffCommand)(args.cwd, args.positionals[0]);
|
|
81
|
+
case "restore":
|
|
82
|
+
return (0, commands_1.restoreCommand)(args.cwd, args.positionals[0], {
|
|
83
|
+
yes: args.yes,
|
|
84
|
+
undo: args.undo,
|
|
85
|
+
});
|
|
86
|
+
case "gc":
|
|
87
|
+
return (0, commands_1.gcCommand)(args.cwd);
|
|
88
|
+
default:
|
|
89
|
+
console.error(`Unknown command: ${args.cmd}\n`);
|
|
90
|
+
usage();
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
main()
|
|
95
|
+
.then((code) => process.exit(code))
|
|
96
|
+
.catch((err) => {
|
|
97
|
+
console.error(`rewindex: ${err?.message ?? err}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.listCommand = listCommand;
|
|
37
|
+
exports.diffCommand = diffCommand;
|
|
38
|
+
exports.restoreCommand = restoreCommand;
|
|
39
|
+
exports.gcCommand = gcCommand;
|
|
40
|
+
const readline = __importStar(require("readline"));
|
|
41
|
+
const paths_1 = require("./paths");
|
|
42
|
+
const config_1 = require("./config");
|
|
43
|
+
const git_1 = require("./git");
|
|
44
|
+
const snapshot_1 = require("./snapshot");
|
|
45
|
+
const state_1 = require("./state");
|
|
46
|
+
const gc_1 = require("./gc");
|
|
47
|
+
function git(projectRoot) {
|
|
48
|
+
return new git_1.ShadowGit((0, paths_1.storeFor)(projectRoot), (0, config_1.loadConfig)());
|
|
49
|
+
}
|
|
50
|
+
function relativeTime(ms) {
|
|
51
|
+
const diff = Date.now() - ms;
|
|
52
|
+
const s = Math.round(diff / 1000);
|
|
53
|
+
if (s < 60)
|
|
54
|
+
return `${s} sn önce`;
|
|
55
|
+
const m = Math.round(s / 60);
|
|
56
|
+
if (m < 60)
|
|
57
|
+
return `${m} dk önce`;
|
|
58
|
+
const h = Math.round(m / 60);
|
|
59
|
+
if (h < 48)
|
|
60
|
+
return `${h} sa önce`;
|
|
61
|
+
return `${Math.round(h / 24)} gün önce`;
|
|
62
|
+
}
|
|
63
|
+
const OUTPUT_WIDTH = 88;
|
|
64
|
+
async function listCommand(projectRoot) {
|
|
65
|
+
const g = git(projectRoot);
|
|
66
|
+
const checkpoints = await (0, snapshot_1.listPromptCheckpoints)(g);
|
|
67
|
+
if (checkpoints.length === 0) {
|
|
68
|
+
console.log("Henüz geri alınabilecek bir prompt değişikliği yok.");
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
console.log("Son 50 değişiklik\n");
|
|
72
|
+
for (const [index, checkpoint] of checkpoints.entries()) {
|
|
73
|
+
const groups = changeGroups(checkpoint);
|
|
74
|
+
console.log(alignRight(`${index + 1}. ${checkpoint.id}`, relativeTime(checkpoint.unixTime)));
|
|
75
|
+
console.log(alignRight("", `${groups.changed.length} değişen · ${groups.added.length} eklenen · ${groups.deleted.length} silinen`));
|
|
76
|
+
printWrapped(" Prompt: ", checkpoint.meta.prompt || checkpoint.label, 2);
|
|
77
|
+
printWrapped(" Özet: ", summary(groups));
|
|
78
|
+
if (index + 1 < checkpoints.length)
|
|
79
|
+
console.log("");
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
function changeGroups(checkpoint) {
|
|
84
|
+
return {
|
|
85
|
+
added: checkpoint.changes.filter((change) => change.status === "A").map((change) => change.path),
|
|
86
|
+
deleted: checkpoint.changes.filter((change) => change.status === "D").map((change) => change.path),
|
|
87
|
+
changed: checkpoint.changes.filter((change) => !["A", "D"].includes(change.status)).map((change) => change.path),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function summary(groups) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
if (groups.changed.length)
|
|
93
|
+
parts.push(`${pathList(groups.changed)} önceki haline döner`);
|
|
94
|
+
if (groups.added.length)
|
|
95
|
+
parts.push(`${pathList(groups.added)} kaldırılır`);
|
|
96
|
+
if (groups.deleted.length)
|
|
97
|
+
parts.push(`${pathList(groups.deleted)} yeniden oluşturulur`);
|
|
98
|
+
return `${parts.join("; ")}.`;
|
|
99
|
+
}
|
|
100
|
+
function pathList(paths) {
|
|
101
|
+
const shown = paths.slice(0, 3);
|
|
102
|
+
const names = shown.length === 1
|
|
103
|
+
? shown[0]
|
|
104
|
+
: `${shown.slice(0, -1).join(", ")} ve ${shown.at(-1)}`;
|
|
105
|
+
return paths.length > shown.length ? `${names} ve ${paths.length - shown.length} dosya daha` : names;
|
|
106
|
+
}
|
|
107
|
+
function alignRight(left, right) {
|
|
108
|
+
return `${left}${" ".repeat(Math.max(1, OUTPUT_WIDTH - left.length - right.length))}${right}`;
|
|
109
|
+
}
|
|
110
|
+
function printWrapped(prefix, text, maxLines = Number.POSITIVE_INFINITY) {
|
|
111
|
+
const lines = wrap(text, OUTPUT_WIDTH - prefix.length, maxLines);
|
|
112
|
+
for (const [index, line] of lines.entries()) {
|
|
113
|
+
console.log(`${index === 0 ? prefix : " ".repeat(prefix.length)}${line}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function wrap(text, width, maxLines) {
|
|
117
|
+
let remaining = text.replace(/\s+/g, " ").trim();
|
|
118
|
+
const lines = [];
|
|
119
|
+
while (remaining && lines.length < maxLines) {
|
|
120
|
+
if (remaining.length <= width) {
|
|
121
|
+
lines.push(remaining);
|
|
122
|
+
remaining = "";
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const space = remaining.lastIndexOf(" ", width);
|
|
126
|
+
const cut = space > 0 ? space : width;
|
|
127
|
+
lines.push(remaining.slice(0, cut));
|
|
128
|
+
remaining = remaining.slice(cut).trim();
|
|
129
|
+
}
|
|
130
|
+
if (remaining)
|
|
131
|
+
lines[lines.length - 1] = `${lines.at(-1).slice(0, width - 1)}…`;
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
async function diffCommand(projectRoot, id) {
|
|
135
|
+
const g = git(projectRoot);
|
|
136
|
+
const c = await (0, snapshot_1.resolveCheckpoint)(g, id);
|
|
137
|
+
if (!c) {
|
|
138
|
+
console.error(`No snapshot matching "${id}". Try \`rewindex list\`.`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
const r = await g.run(["diff", c.fullId, "--"]);
|
|
142
|
+
if (!r.stdout.trim()) {
|
|
143
|
+
console.log(`Snapshot ${c.id} matches the working tree for its files (nothing to restore).`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write(r.stdout);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
async function confirm(question) {
|
|
150
|
+
if (!process.stdin.isTTY)
|
|
151
|
+
return false;
|
|
152
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
153
|
+
const answer = await new Promise((resolve) => rl.question(question, resolve));
|
|
154
|
+
rl.close();
|
|
155
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
156
|
+
}
|
|
157
|
+
async function restoreCommand(projectRoot, id, opts) {
|
|
158
|
+
const store = (0, paths_1.storeFor)(projectRoot);
|
|
159
|
+
const g = new git_1.ShadowGit(store, (0, config_1.loadConfig)());
|
|
160
|
+
await g.snapshot((0, snapshot_1.buildMessage)("before rewindex restore", { event: "manual" }));
|
|
161
|
+
let target;
|
|
162
|
+
if (opts.undo) {
|
|
163
|
+
const st = (0, state_1.readState)(store);
|
|
164
|
+
if (!st.lastSafetySnapshot) {
|
|
165
|
+
console.error("Nothing to undo: no restore has run yet in this project.");
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
target = st.lastSafetySnapshot;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
if (!id) {
|
|
172
|
+
console.error("Usage: rewindex restore <id> (or --undo)");
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
target = await (0, snapshot_1.resolveCheckpoint)(g, id);
|
|
176
|
+
if (!target) {
|
|
177
|
+
console.error(`No snapshot matching "${id}". Try \`rewindex list\`.`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const targetRef = typeof target === "string" ? target : target.fullId;
|
|
182
|
+
const changes = await g.changesSince(targetRef);
|
|
183
|
+
const changed = changes.map((change) => change.path);
|
|
184
|
+
if (changed.length === 0) {
|
|
185
|
+
console.log("Working tree already matches that snapshot. Nothing to do.");
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
const label = typeof target === "string" ? "safety snapshot" : target.label;
|
|
189
|
+
console.log(`Restore ${typeof target === "string" ? target.slice(0, 10) : target.id} "${label}"`);
|
|
190
|
+
console.log(`This will change ${changed.length} file(s) in ${projectRoot}:`);
|
|
191
|
+
for (const f of changed)
|
|
192
|
+
console.log(` ~ ${f}`);
|
|
193
|
+
if (changed.length > 0) {
|
|
194
|
+
console.log(`\n note: ${changed.length} file(s) differ from the snapshot; ` +
|
|
195
|
+
`those edits will be replaced. A safety snapshot is taken first — ` +
|
|
196
|
+
`run \`rewindex restore --undo\` to get them back.`);
|
|
197
|
+
}
|
|
198
|
+
if (!opts.yes) {
|
|
199
|
+
const ok = await confirm("\nProceed? [y/N] ");
|
|
200
|
+
if (!ok) {
|
|
201
|
+
console.log("Aborted.");
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Safety snapshot of the CURRENT state so the restore is reversible.
|
|
206
|
+
const safety = (await g.snapshot((0, snapshot_1.buildMessage)("pre-restore safety snapshot", { event: "pre-restore" }))) ?? await g.revParse("HEAD");
|
|
207
|
+
(0, state_1.writeState)(store, {
|
|
208
|
+
lastSafetySnapshot: safety,
|
|
209
|
+
lastRestoredTo: targetRef,
|
|
210
|
+
});
|
|
211
|
+
await g.deletePaths(changes.filter((change) => change.status === "A").map((change) => change.path));
|
|
212
|
+
await g.checkoutPaths(targetRef, changes.filter((change) => change.status !== "A").map((change) => change.path));
|
|
213
|
+
console.log(`\nRestored ${changed.length} file(s).`);
|
|
214
|
+
if (!opts.undo) {
|
|
215
|
+
console.log(`Undo with: rewindex restore --undo`);
|
|
216
|
+
}
|
|
217
|
+
console.log(`Tip: to also rewind the conversation, press Esc-Esc in Codex — rewindex only touches files.`);
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
async function gcCommand(projectRoot) {
|
|
221
|
+
const store = (0, paths_1.storeFor)(projectRoot);
|
|
222
|
+
const config = (0, config_1.loadConfig)();
|
|
223
|
+
const result = await (0, gc_1.gcStore)(store, config);
|
|
224
|
+
console.log(`GC: dropped ${result.dropped} snapshot(s), kept ${result.kept}. ` +
|
|
225
|
+
`Store now ~${(result.bytesAfter / (1024 * 1024)).toFixed(1)} MB.`);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_CONFIG = void 0;
|
|
37
|
+
exports.loadConfig = loadConfig;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const paths_1 = require("./paths");
|
|
41
|
+
exports.DEFAULT_CONFIG = {
|
|
42
|
+
exclude: [
|
|
43
|
+
".git",
|
|
44
|
+
"node_modules",
|
|
45
|
+
".rewindex",
|
|
46
|
+
".DS_Store",
|
|
47
|
+
// common heavy build/output dirs
|
|
48
|
+
"dist",
|
|
49
|
+
"build",
|
|
50
|
+
".next",
|
|
51
|
+
".venv",
|
|
52
|
+
"target",
|
|
53
|
+
"__pycache__",
|
|
54
|
+
],
|
|
55
|
+
maxFileBytes: 16 * 1024 * 1024,
|
|
56
|
+
retentionDays: 7,
|
|
57
|
+
retentionBytes: 500 * 1024 * 1024,
|
|
58
|
+
captureIgnored: true,
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Config is read from ~/.rewindex/config.json if present, else defaults.
|
|
62
|
+
* ponytail: single global config file, no per-project override and no schema
|
|
63
|
+
* validation. Ceiling: fine for v1 (one user, one machine). Upgrade path: layer
|
|
64
|
+
* a project-local .rewindex.json and validate with a tiny schema check.
|
|
65
|
+
*/
|
|
66
|
+
function loadConfig() {
|
|
67
|
+
const file = path.join((0, paths_1.rewindexHome)(), "config.json");
|
|
68
|
+
try {
|
|
69
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
70
|
+
return { ...exports.DEFAULT_CONFIG, ...raw };
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { ...exports.DEFAULT_CONFIG };
|
|
74
|
+
}
|
|
75
|
+
}
|
package/dist/gc.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gcStore = gcStore;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const git_1 = require("./git");
|
|
6
|
+
/**
|
|
7
|
+
* Retention GC. Snapshots live as a commit chain on the `rewindex` branch; to
|
|
8
|
+
* drop old ones we REBUILD the chain from the survivors' trees with `commit-tree`
|
|
9
|
+
* (preserving each message + author date), then expire reflogs and `git gc` so
|
|
10
|
+
* the dropped objects are actually reclaimed.
|
|
11
|
+
*
|
|
12
|
+
* ponytail: O(n) history rewrite on every gc run. Ceiling: fine at hundreds of
|
|
13
|
+
* snapshots. Upgrade path: a generational/packfile strategy, or only rewrite
|
|
14
|
+
* when the drop count crosses a threshold.
|
|
15
|
+
*/
|
|
16
|
+
async function gcStore(store, config) {
|
|
17
|
+
const git = new git_1.ShadowGit(store, config);
|
|
18
|
+
if (!(await git.isInitialized())) {
|
|
19
|
+
return { dropped: 0, kept: 0, bytesAfter: 0 };
|
|
20
|
+
}
|
|
21
|
+
const commits = await readCommits(git);
|
|
22
|
+
if (commits.length === 0) {
|
|
23
|
+
return { dropped: 0, kept: 0, bytesAfter: dirBytes(store.gitDir) };
|
|
24
|
+
}
|
|
25
|
+
const cutoffSec = Date.now() / 1000 - config.retentionDays * 86400;
|
|
26
|
+
// Keep the empty root (anchor) plus any snapshot newer than the cutoff.
|
|
27
|
+
// Oldest -> newest order for rebuilding.
|
|
28
|
+
const chronological = [...commits].reverse();
|
|
29
|
+
const survivors = chronological.filter((c) => c.isRoot || c.time >= cutoffSec);
|
|
30
|
+
// Size cap: if still over budget, drop oldest snapshots (never the root) until
|
|
31
|
+
// under budget or only the root + newest remain.
|
|
32
|
+
let bytes = dirBytes(store.gitDir);
|
|
33
|
+
if (bytes > config.retentionBytes) {
|
|
34
|
+
// survivors is oldest->newest; peel from the front (after the root).
|
|
35
|
+
const root = survivors.filter((c) => c.isRoot);
|
|
36
|
+
let snaps = survivors.filter((c) => !c.isRoot);
|
|
37
|
+
while (bytes > config.retentionBytes && snaps.length > 1) {
|
|
38
|
+
snaps = snaps.slice(1);
|
|
39
|
+
// Rough proportional estimate; real bytes recomputed after rewrite.
|
|
40
|
+
bytes = bytes * (snaps.length / (snaps.length + 1));
|
|
41
|
+
}
|
|
42
|
+
survivors.length = 0;
|
|
43
|
+
survivors.push(...root, ...snaps);
|
|
44
|
+
}
|
|
45
|
+
const droppedCount = commits.length - survivors.length;
|
|
46
|
+
if (droppedCount === 0) {
|
|
47
|
+
return { dropped: 0, kept: commits.length, bytesAfter: dirBytes(store.gitDir) };
|
|
48
|
+
}
|
|
49
|
+
// Rebuild the chain from survivor trees.
|
|
50
|
+
let parent = null;
|
|
51
|
+
for (const c of survivors) {
|
|
52
|
+
const args = ["commit-tree", c.tree];
|
|
53
|
+
if (parent)
|
|
54
|
+
args.push("-p", parent);
|
|
55
|
+
args.push("-m", c.message);
|
|
56
|
+
const env = {
|
|
57
|
+
GIT_AUTHOR_DATE: `${c.time} +0000`,
|
|
58
|
+
GIT_COMMITTER_DATE: `${c.time} +0000`,
|
|
59
|
+
};
|
|
60
|
+
parent = (await git.runOk(args, env)).trim();
|
|
61
|
+
}
|
|
62
|
+
if (parent) {
|
|
63
|
+
await git.runOk(["update-ref", "refs/heads/rewindex", parent]);
|
|
64
|
+
}
|
|
65
|
+
// Reclaim space.
|
|
66
|
+
await git.run(["reflog", "expire", "--all", "--expire=now"]);
|
|
67
|
+
await git.run(["gc", "--prune=now", "--quiet"]);
|
|
68
|
+
return {
|
|
69
|
+
dropped: droppedCount,
|
|
70
|
+
kept: survivors.length,
|
|
71
|
+
bytesAfter: dirBytes(store.gitDir),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function readCommits(git) {
|
|
75
|
+
const SEP = "\x1e";
|
|
76
|
+
const REC = "\x1f";
|
|
77
|
+
const fmt = `%H${SEP}%T${SEP}%ct${SEP}%B${REC}`;
|
|
78
|
+
const out = (await git.run(["log", `--pretty=format:${fmt}`])).stdout;
|
|
79
|
+
const records = out.split(REC).map((r) => r.trim()).filter(Boolean);
|
|
80
|
+
// newest-first (git log default)
|
|
81
|
+
return records.map((rec) => {
|
|
82
|
+
const [sha, tree, ct, message = ""] = rec.split(SEP);
|
|
83
|
+
const isRoot = /rewindex-event: root/.test(message);
|
|
84
|
+
return { sha, tree, time: Number(ct), message: message.trim(), isRoot };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Disk usage of the shadow git dir in bytes (best-effort). */
|
|
88
|
+
function dirBytes(dir) {
|
|
89
|
+
try {
|
|
90
|
+
const out = (0, child_process_1.execSync)(`du -sk "${dir}"`, { encoding: "utf8" });
|
|
91
|
+
const kb = parseInt(out.trim().split(/\s+/)[0], 10);
|
|
92
|
+
return kb * 1024;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
}
|