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/dist/git.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
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.ShadowGit = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
/**
|
|
41
|
+
* Thin wrapper over the system `git`, run against an ISOLATED shadow repo:
|
|
42
|
+
* - GIT_DIR points at our per-project store, GIT_WORK_TREE at the project root,
|
|
43
|
+
* so the project's own .git is never read or written.
|
|
44
|
+
* - GIT_CONFIG_GLOBAL/SYSTEM are forced to /dev/null so the user's git config,
|
|
45
|
+
* hooks, and signing settings can't leak in or fire.
|
|
46
|
+
* - identity is fixed, LANG=C for stable parsing, and mutating commands pass
|
|
47
|
+
* --no-verify so no hooks run.
|
|
48
|
+
*
|
|
49
|
+
* ponytail: ~130-line hand wrapper instead of simple-git. Ceiling: covers the
|
|
50
|
+
* handful of plumbing commands we need. Upgrade path: if the command surface
|
|
51
|
+
* grows past ~a dozen calls, swap in a real git library.
|
|
52
|
+
*/
|
|
53
|
+
class ShadowGit {
|
|
54
|
+
store;
|
|
55
|
+
config;
|
|
56
|
+
constructor(store, config) {
|
|
57
|
+
this.store = store;
|
|
58
|
+
this.config = config;
|
|
59
|
+
}
|
|
60
|
+
get gitDir() {
|
|
61
|
+
return this.store.gitDir;
|
|
62
|
+
}
|
|
63
|
+
env(extra = {}) {
|
|
64
|
+
return {
|
|
65
|
+
...process.env,
|
|
66
|
+
GIT_DIR: this.store.gitDir,
|
|
67
|
+
GIT_WORK_TREE: this.store.projectRoot,
|
|
68
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
69
|
+
GIT_CONFIG_SYSTEM: "/dev/null",
|
|
70
|
+
GIT_AUTHOR_NAME: "rewindex",
|
|
71
|
+
GIT_AUTHOR_EMAIL: "rewindex@localhost",
|
|
72
|
+
GIT_COMMITTER_NAME: "rewindex",
|
|
73
|
+
GIT_COMMITTER_EMAIL: "rewindex@localhost",
|
|
74
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
75
|
+
LANG: "C",
|
|
76
|
+
LC_ALL: "C",
|
|
77
|
+
...extra,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Env for `git init`: identical to {@link env} but WITHOUT GIT_WORK_TREE.
|
|
82
|
+
* git refuses `init` when a worktree is set without an explicit --git-dir arg,
|
|
83
|
+
* so we create the repo first, then every later command uses the worktree env.
|
|
84
|
+
*/
|
|
85
|
+
initEnv() {
|
|
86
|
+
const e = this.env();
|
|
87
|
+
delete e.GIT_WORK_TREE;
|
|
88
|
+
return e;
|
|
89
|
+
}
|
|
90
|
+
run(args, extraEnv = {}) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
(0, child_process_1.execFile)("git", args, { env: this.env(extraEnv), maxBuffer: 256 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
93
|
+
const code = err && typeof err.code === "number" ? err.code : err ? 1 : 0;
|
|
94
|
+
if (err && code === 0) {
|
|
95
|
+
reject(err);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
resolve({ stdout: stdout ?? "", stderr: stderr ?? "", code });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Run and throw on non-zero exit. */
|
|
103
|
+
async runOk(args, extraEnv = {}) {
|
|
104
|
+
const r = await this.run(args, extraEnv);
|
|
105
|
+
if (r.code !== 0) {
|
|
106
|
+
throw new Error(`git ${args.join(" ")} failed (${r.code}): ${r.stderr.trim()}`);
|
|
107
|
+
}
|
|
108
|
+
return r.stdout;
|
|
109
|
+
}
|
|
110
|
+
async isInitialized() {
|
|
111
|
+
try {
|
|
112
|
+
await fs.promises.access(path.join(this.store.gitDir, "HEAD"));
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Create the shadow repo and an empty root commit if it doesn't exist yet. */
|
|
120
|
+
async ensureInit() {
|
|
121
|
+
if (await this.isInitialized())
|
|
122
|
+
return;
|
|
123
|
+
await fs.promises.mkdir(this.store.dir, { recursive: true });
|
|
124
|
+
// Init into GIT_DIR WITHOUT a worktree set (git forbids init when a worktree
|
|
125
|
+
// is set but no --git-dir arg is given). Every later command uses the full
|
|
126
|
+
// env with GIT_WORK_TREE so snapshots read/write the project files.
|
|
127
|
+
await new Promise((resolve, reject) => {
|
|
128
|
+
(0, child_process_1.execFile)("git", ["init", "--quiet", "--initial-branch=rewindex"], { env: this.initEnv() }, (err, _o, stderr) => (err ? reject(new Error(`git init failed: ${stderr}`)) : resolve()));
|
|
129
|
+
});
|
|
130
|
+
await this.runOk(["config", "core.bare", "false"]);
|
|
131
|
+
await this.runOk(["config", "core.logAllRefUpdates", "true"]);
|
|
132
|
+
// Root commit so the first real snapshot always has a parent to diff against.
|
|
133
|
+
await this.runOk([
|
|
134
|
+
"commit",
|
|
135
|
+
"--allow-empty",
|
|
136
|
+
"--no-verify",
|
|
137
|
+
"-m",
|
|
138
|
+
"rewindex root\n\nrewindex-event: root",
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Build the pathspec that excludes configured dirs and (separately computed)
|
|
143
|
+
* oversized files. We force-add so gitignored project files ARE captured.
|
|
144
|
+
*/
|
|
145
|
+
excludePathspecs(extraExcludes) {
|
|
146
|
+
const specs = ["."];
|
|
147
|
+
const all = [...this.config.exclude, ...extraExcludes];
|
|
148
|
+
for (const e of all) {
|
|
149
|
+
// :(exclude) pathspec magic; works for both dirs and files.
|
|
150
|
+
specs.push(`:(exclude)${e}`);
|
|
151
|
+
specs.push(`:(exclude)**/${e}`);
|
|
152
|
+
}
|
|
153
|
+
return specs;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Walk the worktree to find files over the size cap so we can exclude them.
|
|
157
|
+
* ponytail: full synchronous walk per snapshot. Ceiling: fine for normal
|
|
158
|
+
* projects once node_modules is excluded. Upgrade path: cache stat results or
|
|
159
|
+
* lean on `git ls-files -z` + stat only the candidates.
|
|
160
|
+
*/
|
|
161
|
+
findOversized() {
|
|
162
|
+
const over = [];
|
|
163
|
+
const cap = this.config.maxFileBytes;
|
|
164
|
+
const skip = new Set(this.config.exclude);
|
|
165
|
+
const root = this.store.projectRoot;
|
|
166
|
+
const walk = (dir, rel) => {
|
|
167
|
+
let entries;
|
|
168
|
+
try {
|
|
169
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const ent of entries) {
|
|
175
|
+
if (skip.has(ent.name))
|
|
176
|
+
continue;
|
|
177
|
+
const abs = path.join(dir, ent.name);
|
|
178
|
+
const relPath = rel ? `${rel}/${ent.name}` : ent.name;
|
|
179
|
+
if (ent.isDirectory()) {
|
|
180
|
+
walk(abs, relPath);
|
|
181
|
+
}
|
|
182
|
+
else if (ent.isFile()) {
|
|
183
|
+
try {
|
|
184
|
+
if (fs.statSync(abs).size > cap)
|
|
185
|
+
over.push(relPath);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
/* ignore unreadable */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
walk(root, "");
|
|
194
|
+
return over;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Stage the current worktree (honoring excludes + size cap, force-adding so
|
|
198
|
+
* gitignored files are included) and commit. Returns null when nothing
|
|
199
|
+
* changed unless forceCommit is set for a metadata-only prompt boundary.
|
|
200
|
+
*/
|
|
201
|
+
async snapshot(message, authorDateISO, forceCommit = false) {
|
|
202
|
+
await this.ensureInit();
|
|
203
|
+
const oversized = this.config.captureIgnored ? this.findOversized() : [];
|
|
204
|
+
// Clear the index first so deletions are captured too.
|
|
205
|
+
await this.runOk(["read-tree", "--empty"]);
|
|
206
|
+
const addArgs = ["add", "-A"];
|
|
207
|
+
if (this.config.captureIgnored)
|
|
208
|
+
addArgs.push("-f");
|
|
209
|
+
addArgs.push("--", ...this.excludePathspecs(oversized));
|
|
210
|
+
const addRes = await this.run(addArgs);
|
|
211
|
+
if (addRes.code !== 0) {
|
|
212
|
+
throw new Error(`git add failed: ${addRes.stderr.trim()}`);
|
|
213
|
+
}
|
|
214
|
+
// Did anything actually change vs HEAD?
|
|
215
|
+
const diff = await this.run(["diff", "--cached", "--quiet", "HEAD"]);
|
|
216
|
+
if (diff.code === 0 && !forceCommit) {
|
|
217
|
+
return null; // no change
|
|
218
|
+
}
|
|
219
|
+
const extraEnv = {};
|
|
220
|
+
if (authorDateISO) {
|
|
221
|
+
extraEnv.GIT_AUTHOR_DATE = authorDateISO;
|
|
222
|
+
extraEnv.GIT_COMMITTER_DATE = authorDateISO;
|
|
223
|
+
}
|
|
224
|
+
await this.runOk(["commit", "--no-verify", "--allow-empty", "-m", message], extraEnv);
|
|
225
|
+
return (await this.runOk(["rev-parse", "HEAD"])).trim();
|
|
226
|
+
}
|
|
227
|
+
async revParse(ref) {
|
|
228
|
+
return (await this.runOk(["rev-parse", ref])).trim();
|
|
229
|
+
}
|
|
230
|
+
async changesSince(ref) {
|
|
231
|
+
const out = await this.runOk([
|
|
232
|
+
"diff",
|
|
233
|
+
"--name-status",
|
|
234
|
+
"--no-renames",
|
|
235
|
+
"-z",
|
|
236
|
+
ref,
|
|
237
|
+
"--",
|
|
238
|
+
]);
|
|
239
|
+
const fields = out.split("\0").filter(Boolean);
|
|
240
|
+
const changes = [];
|
|
241
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
242
|
+
changes.push({ status: fields[i], path: fields[i + 1] });
|
|
243
|
+
}
|
|
244
|
+
return changes;
|
|
245
|
+
}
|
|
246
|
+
async deletePaths(paths) {
|
|
247
|
+
const root = path.resolve(this.store.projectRoot) + path.sep;
|
|
248
|
+
for (const file of paths) {
|
|
249
|
+
const absolute = path.resolve(this.store.projectRoot, file);
|
|
250
|
+
if (!absolute.startsWith(root))
|
|
251
|
+
throw new Error(`unsafe snapshot path: ${file}`);
|
|
252
|
+
try {
|
|
253
|
+
await fs.promises.unlink(absolute);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
if (err.code !== "ENOENT")
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Restore the given paths from a commit into the worktree. */
|
|
262
|
+
async checkoutPaths(ref, paths) {
|
|
263
|
+
if (paths.length === 0)
|
|
264
|
+
return;
|
|
265
|
+
await this.runOk(["checkout", ref, "--", ...paths]);
|
|
266
|
+
}
|
|
267
|
+
/** Content of a file at a commit, or null if it didn't exist then. */
|
|
268
|
+
async fileAt(ref, file) {
|
|
269
|
+
const r = await this.run(["show", `${ref}:${file}`]);
|
|
270
|
+
if (r.code !== 0)
|
|
271
|
+
return null;
|
|
272
|
+
return Buffer.from(r.stdout, "binary");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
exports.ShadowGit = ShadowGit;
|
package/dist/hook.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hookCommand = hookCommand;
|
|
4
|
+
const paths_1 = require("./paths");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const git_1 = require("./git");
|
|
7
|
+
const snapshot_1 = require("./snapshot");
|
|
8
|
+
function readStdin() {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
let data = "";
|
|
11
|
+
process.stdin.on("data", (d) => (data += d));
|
|
12
|
+
process.stdin.on("end", () => resolve(data));
|
|
13
|
+
process.stdin.on("error", () => resolve(data));
|
|
14
|
+
// If nothing is piped, don't hang forever.
|
|
15
|
+
setTimeout(() => resolve(data), 2000).unref();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function labelFor(p) {
|
|
19
|
+
const event = p.hook_event_name || "unknown";
|
|
20
|
+
const meta = {
|
|
21
|
+
turnId: p.turn_id,
|
|
22
|
+
sessionId: p.session_id,
|
|
23
|
+
event,
|
|
24
|
+
tool: p.tool_name,
|
|
25
|
+
prompt: p.prompt,
|
|
26
|
+
};
|
|
27
|
+
let label;
|
|
28
|
+
if (event === "UserPromptSubmit" && p.prompt) {
|
|
29
|
+
label = p.prompt;
|
|
30
|
+
}
|
|
31
|
+
else if (event === "PreToolUse") {
|
|
32
|
+
label = `before ${p.tool_name || "tool"}`;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
label = `${event} snapshot`;
|
|
36
|
+
}
|
|
37
|
+
return { label, meta };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The `rewindex hook` entrypoint. Reads one JSON payload, takes a SYNCHRONOUS
|
|
41
|
+
* snapshot, then exits 0. It must NEVER block the tool call: any failure is
|
|
42
|
+
* logged to stderr and we still exit 0 (stdout stays empty, which Codex ignores
|
|
43
|
+
* for PreToolUse so the tool proceeds unchanged).
|
|
44
|
+
*/
|
|
45
|
+
async function hookCommand() {
|
|
46
|
+
const raw = await readStdin();
|
|
47
|
+
let payload;
|
|
48
|
+
try {
|
|
49
|
+
payload = JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Can't parse — nothing to snapshot against, but never block the tool.
|
|
53
|
+
process.stderr.write("rewindex: unparseable hook payload, skipping snapshot\n");
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
const projectRoot = payload.cwd || process.cwd();
|
|
57
|
+
try {
|
|
58
|
+
const store = (0, paths_1.storeFor)(projectRoot);
|
|
59
|
+
const config = (0, config_1.loadConfig)();
|
|
60
|
+
const git = new git_1.ShadowGit(store, config);
|
|
61
|
+
const { label, meta } = labelFor(payload);
|
|
62
|
+
const sha = await git.snapshot((0, snapshot_1.buildMessage)(label, meta), undefined, meta.event === "UserPromptSubmit");
|
|
63
|
+
if (sha) {
|
|
64
|
+
process.stderr.write(`rewindex: snapshot ${sha.slice(0, 10)} (${meta.event})\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
// Log and warn only — the snapshot failing must not stop Codex.
|
|
69
|
+
process.stderr.write(`rewindex: snapshot failed (non-blocking): ${err.message}\n`);
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
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.installCommand = installCommand;
|
|
37
|
+
exports.uninstallCommand = uninstallCommand;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* Install/uninstall the rewindex hooks into Codex's USER-level hook config.
|
|
43
|
+
*
|
|
44
|
+
* ponytail / DELIBERATE DEVIATION: the task said "merge into ~/.codex/config.toml
|
|
45
|
+
* ... parse TOML properly". We instead write ~/.codex/hooks.json, which Codex
|
|
46
|
+
* loads as a first-class user-level hook source and MERGES with config.toml
|
|
47
|
+
* (verified: learn.chatgpt.com/docs/hooks lists `~/.codex/hooks.json` alongside
|
|
48
|
+
* `~/.codex/config.toml`). Rationale: hooks.json is pure JSON, so a round-trip
|
|
49
|
+
* can't clobber the user's hand-written config.toml comments/formatting/other
|
|
50
|
+
* hooks — the single biggest footgun here. Net effect is identical. Ceiling:
|
|
51
|
+
* if a user keeps hooks ONLY in config.toml and nowhere else, ours live in a
|
|
52
|
+
* separate file (still both load). Upgrade path: add a `--into config.toml`
|
|
53
|
+
* mode backed by a real TOML round-tripper if anyone needs it.
|
|
54
|
+
*/
|
|
55
|
+
const EVENTS = ["PreToolUse", "UserPromptSubmit"];
|
|
56
|
+
const MARKER = "rewindex"; // how we recognize our own entries for idempotency
|
|
57
|
+
const SKILL_MARKER = "<!-- generated by rewindex -->";
|
|
58
|
+
const STATUS_MESSAGE = "rewindex snapshot";
|
|
59
|
+
function codexHome() {
|
|
60
|
+
const env = process.env.CODEX_HOME;
|
|
61
|
+
return env && env.trim() ? path.resolve(env) : path.join(os.homedir(), ".codex");
|
|
62
|
+
}
|
|
63
|
+
function hooksJsonPath() {
|
|
64
|
+
return path.join(codexHome(), "hooks.json");
|
|
65
|
+
}
|
|
66
|
+
function skillDirPath() {
|
|
67
|
+
return path.join(codexHome(), "skills", "rewindex");
|
|
68
|
+
}
|
|
69
|
+
function skillFilePath() {
|
|
70
|
+
return path.join(skillDirPath(), "SKILL.md");
|
|
71
|
+
}
|
|
72
|
+
function cliScript() {
|
|
73
|
+
// Resolve the installed binary. When run via `npx rewindex` / a global
|
|
74
|
+
// install, process.argv[1] points at dist/cli.js; prefer the `rewindex` bin
|
|
75
|
+
// on PATH for portability, falling back to the absolute script path.
|
|
76
|
+
const abs = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
77
|
+
return abs ? `node ${JSON.stringify(abs)}` : "rewindex";
|
|
78
|
+
}
|
|
79
|
+
function rewindexCommand() {
|
|
80
|
+
return `${cliScript()} hook`;
|
|
81
|
+
}
|
|
82
|
+
function ourHandler() {
|
|
83
|
+
return {
|
|
84
|
+
type: "command",
|
|
85
|
+
command: rewindexCommand(),
|
|
86
|
+
timeout: 30,
|
|
87
|
+
statusMessage: STATUS_MESSAGE,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function isOurGroup(g) {
|
|
91
|
+
return (g.hooks || []).some((h) => h.statusMessage === STATUS_MESSAGE ||
|
|
92
|
+
(typeof h.command === "string" && h.command.includes(MARKER)));
|
|
93
|
+
}
|
|
94
|
+
function readHooksFile() {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(fs.readFileSync(hooksJsonPath(), "utf8"));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function writeHooksFile(data) {
|
|
103
|
+
fs.mkdirSync(codexHome(), { recursive: true });
|
|
104
|
+
fs.writeFileSync(hooksJsonPath(), JSON.stringify(data, null, 2) + "\n");
|
|
105
|
+
}
|
|
106
|
+
function skillBody() {
|
|
107
|
+
const cli = cliScript();
|
|
108
|
+
return `---
|
|
109
|
+
name: rewindex
|
|
110
|
+
description: Use when the user asks to list, inspect, restore, undo, or rewind rewindex file snapshots in the current Codex workspace.
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
${SKILL_MARKER}
|
|
114
|
+
|
|
115
|
+
# Rewindex
|
|
116
|
+
|
|
117
|
+
Interpret the text after \`$rewindex\` as exactly one command:
|
|
118
|
+
|
|
119
|
+
- \`$rewindex list\`: run \`${cli} list -C "$PWD"\`.
|
|
120
|
+
- \`$rewindex diff <number|id>\`: validate the selector, then run \`${cli} diff "<number|id>" -C "$PWD"\`.
|
|
121
|
+
- \`$rewindex restore <number|id>\`: validate the selector, run the diff command first, show its output, and ask for explicit confirmation. Only after the user confirms, run \`${cli} restore "<number|id>" -y -C "$PWD"\`.
|
|
122
|
+
- \`$rewindex undo\`: ask for explicit confirmation. Only after the user confirms, run \`${cli} restore --undo -y -C "$PWD"\`.
|
|
123
|
+
|
|
124
|
+
A selector is valid only when it is a decimal number from \`1\` through \`50\`, hexadecimal, or exactly \`latest\` or \`last\`. Never pass other user input to the shell, infer a selector, or add user-supplied flags. For missing or unsupported arguments, show the four forms above and make no changes.
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
function installSkill() {
|
|
128
|
+
const file = skillFilePath();
|
|
129
|
+
if (fs.existsSync(file) && !fs.readFileSync(file, "utf8").includes(SKILL_MARKER)) {
|
|
130
|
+
throw new Error(`Refusing to overwrite existing unowned skill: ${file}`);
|
|
131
|
+
}
|
|
132
|
+
fs.mkdirSync(skillDirPath(), { recursive: true });
|
|
133
|
+
fs.writeFileSync(file, skillBody());
|
|
134
|
+
}
|
|
135
|
+
function uninstallSkill() {
|
|
136
|
+
const file = skillFilePath();
|
|
137
|
+
if (!fs.existsSync(file))
|
|
138
|
+
return false;
|
|
139
|
+
if (!fs.readFileSync(file, "utf8").includes(SKILL_MARKER))
|
|
140
|
+
return false;
|
|
141
|
+
fs.unlinkSync(file);
|
|
142
|
+
try {
|
|
143
|
+
fs.rmdirSync(skillDirPath());
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (err.code !== "ENOTEMPTY")
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
function installCommand() {
|
|
152
|
+
installSkill();
|
|
153
|
+
const data = readHooksFile();
|
|
154
|
+
data.hooks = data.hooks || {};
|
|
155
|
+
let added = 0;
|
|
156
|
+
for (const event of EVENTS) {
|
|
157
|
+
const groups = (data.hooks[event] = data.hooks[event] || []);
|
|
158
|
+
// Idempotent: replace any existing rewindex group, leave others untouched.
|
|
159
|
+
const kept = groups.filter((g) => !isOurGroup(g));
|
|
160
|
+
kept.push({ hooks: [ourHandler()] });
|
|
161
|
+
data.hooks[event] = kept;
|
|
162
|
+
added++;
|
|
163
|
+
}
|
|
164
|
+
writeHooksFile(data);
|
|
165
|
+
console.log(`Installed rewindex hooks into ${hooksJsonPath()} (${added} events).`);
|
|
166
|
+
console.log(`Installed Codex skill: ${skillFilePath()}`);
|
|
167
|
+
console.log("Use it in a new Codex session: $rewindex list");
|
|
168
|
+
console.log("");
|
|
169
|
+
printTrustNote();
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
function uninstallCommand() {
|
|
173
|
+
const removedSkill = uninstallSkill();
|
|
174
|
+
const data = readHooksFile();
|
|
175
|
+
if (!data.hooks) {
|
|
176
|
+
console.log("No rewindex hooks found.");
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
let removed = 0;
|
|
180
|
+
for (const event of Object.keys(data.hooks)) {
|
|
181
|
+
const groups = data.hooks[event] || [];
|
|
182
|
+
const before = groups.length;
|
|
183
|
+
const kept = groups.filter((g) => !isOurGroup(g));
|
|
184
|
+
removed += before - kept.length;
|
|
185
|
+
if (kept.length > 0)
|
|
186
|
+
data.hooks[event] = kept;
|
|
187
|
+
else
|
|
188
|
+
delete data.hooks[event];
|
|
189
|
+
}
|
|
190
|
+
if (Object.keys(data.hooks).length === 0)
|
|
191
|
+
delete data.hooks;
|
|
192
|
+
writeHooksFile(data);
|
|
193
|
+
console.log(`Removed ${removed} rewindex hook group(s) from ${hooksJsonPath()}.`);
|
|
194
|
+
console.log(removedSkill ? "Removed the rewindex Codex skill." : "No owned rewindex skill found.");
|
|
195
|
+
console.log("Your other hooks were left untouched.");
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
function printTrustNote() {
|
|
199
|
+
console.log("IMPORTANT — Codex requires you to TRUST new hooks before they run:");
|
|
200
|
+
console.log(" 1. Start (or restart) Codex.");
|
|
201
|
+
console.log(" 2. Run /hooks in the Codex CLI.");
|
|
202
|
+
console.log(" 3. Review the two `rewindex hook` entries and mark them trusted.");
|
|
203
|
+
console.log("");
|
|
204
|
+
console.log("Until trusted, Codex SKIPS them (it warns at startup), so no snapshots");
|
|
205
|
+
console.log("are taken. Trust is tied to the hook's hash — if you reinstall/upgrade");
|
|
206
|
+
console.log("rewindex, re-run /hooks to re-trust the changed command.");
|
|
207
|
+
}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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.rewindexHome = rewindexHome;
|
|
37
|
+
exports.projectHash = projectHash;
|
|
38
|
+
exports.storeFor = storeFor;
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const crypto = __importStar(require("crypto"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
/**
|
|
44
|
+
* Where rewindex keeps its shadow repos and state. Overridable via REWINDEX_HOME
|
|
45
|
+
* (used by the self-check to stay out of the real ~/.rewindex).
|
|
46
|
+
*/
|
|
47
|
+
function rewindexHome() {
|
|
48
|
+
const override = process.env.REWINDEX_HOME;
|
|
49
|
+
if (override && override.trim())
|
|
50
|
+
return path.resolve(override);
|
|
51
|
+
return path.join(os.homedir(), ".rewindex");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A stable per-project id. We hash the real (symlink-resolved) absolute path so
|
|
55
|
+
* two checkouts at different paths get separate stores and the same path always
|
|
56
|
+
* maps to the same store.
|
|
57
|
+
*/
|
|
58
|
+
function projectHash(projectRoot) {
|
|
59
|
+
const real = safeRealpath(projectRoot);
|
|
60
|
+
return crypto.createHash("sha256").update(real).digest("hex").slice(0, 16);
|
|
61
|
+
}
|
|
62
|
+
function safeRealpath(p) {
|
|
63
|
+
try {
|
|
64
|
+
return fs.realpathSync(path.resolve(p));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return path.resolve(p);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function storeFor(projectRoot) {
|
|
71
|
+
const root = safeRealpath(projectRoot);
|
|
72
|
+
const hash = projectHash(root);
|
|
73
|
+
const dir = path.join(rewindexHome(), hash);
|
|
74
|
+
return {
|
|
75
|
+
projectRoot: root,
|
|
76
|
+
hash,
|
|
77
|
+
dir,
|
|
78
|
+
gitDir: path.join(dir, "git"),
|
|
79
|
+
stateFile: path.join(dir, "state.json"),
|
|
80
|
+
};
|
|
81
|
+
}
|