@riddledc/riddle-proof 0.5.1 → 0.5.2

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.
@@ -0,0 +1,391 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { execSync } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ symlinkSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ function commandEnv() {
17
+ return { ...process.env, HOME: "/root" };
18
+ }
19
+
20
+ export function shellQuote(value) {
21
+ return `'${String(value ?? "").replace(/'/g, `'"'"'`)}'`;
22
+ }
23
+
24
+ export function run(cmd, cwd, timeoutMs = 30000) {
25
+ return execSync(cmd, {
26
+ cwd,
27
+ encoding: "utf-8",
28
+ timeout: timeoutMs,
29
+ env: commandEnv(),
30
+ }).trim();
31
+ }
32
+
33
+ export function runSafe(cmd, cwd, timeoutMs = 30000) {
34
+ try {
35
+ return { ok: true, output: run(cmd, cwd, timeoutMs) };
36
+ } catch (error) {
37
+ return {
38
+ ok: false,
39
+ output: error?.stderr?.toString?.() || error?.message || String(error),
40
+ };
41
+ }
42
+ }
43
+
44
+ export function sanitizeFragment(value, fallback = "item") {
45
+ const sanitized = String(value || "")
46
+ .trim()
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9]+/g, "-")
49
+ .replace(/^-+|-+$/g, "")
50
+ .slice(0, 48);
51
+ return sanitized || fallback;
52
+ }
53
+
54
+ export function uniqueToken(now = new Date()) {
55
+ const stamp = now.toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
56
+ return `${stamp}-${randomUUID().slice(0, 8)}`;
57
+ }
58
+
59
+ export function workspaceRoots({ workspaceRoot = "", currentRepoDir = "" } = {}) {
60
+ const roots = [];
61
+ for (const candidate of [
62
+ workspaceRoot,
63
+ process.env.OPENCLAW_WORKSPACE,
64
+ "/mnt/efs/openclaw/workspace",
65
+ path.resolve(process.env.HOME || "/root", ".openclaw/workspace"),
66
+ currentRepoDir ? path.dirname(currentRepoDir) : "",
67
+ ]) {
68
+ if (candidate && !roots.includes(candidate)) {
69
+ roots.push(candidate);
70
+ }
71
+ }
72
+ return roots;
73
+ }
74
+
75
+ export function resolveRepoDir({ repoName, repoDir = "", workspaceRoot = "", currentRepoDir = "" }) {
76
+ const candidates = [];
77
+ if (repoDir) candidates.push(repoDir);
78
+ if (currentRepoDir) candidates.push(currentRepoDir);
79
+ for (const root of workspaceRoots({ workspaceRoot, currentRepoDir })) {
80
+ candidates.push(path.join(root, repoName));
81
+ }
82
+ for (const candidate of candidates) {
83
+ if (candidate && existsSync(path.join(candidate, ".git"))) {
84
+ return candidate;
85
+ }
86
+ }
87
+ return candidates.find(Boolean) || path.join(workspaceRoots({ workspaceRoot, currentRepoDir })[0] || "/tmp", repoName);
88
+ }
89
+
90
+ export function requireCleanIndex(repoDir, label) {
91
+ const unmerged = runSafe("git ls-files -u", repoDir);
92
+ if (unmerged.ok && unmerged.output.trim()) {
93
+ throw new Error(
94
+ `${label} has unmerged index entries. Resolve conflicts before continuing.\n${unmerged.output
95
+ .trim()
96
+ .split("\n")
97
+ .slice(0, 20)
98
+ .join("\n")}`,
99
+ );
100
+ }
101
+ }
102
+
103
+ export function prepareRepo({
104
+ repo,
105
+ branch,
106
+ repoDir = "",
107
+ baseBranch = "main",
108
+ workspaceRoot = "",
109
+ ensureHttpsRemote = false,
110
+ fetch = true,
111
+ } = {}) {
112
+ if (!repo) throw new Error("repo is required");
113
+ if (!branch) throw new Error("branch is required");
114
+
115
+ const repoName = repo.split("/").pop() || repo;
116
+ const resolvedRepoDir = resolveRepoDir({ repoName, repoDir, workspaceRoot });
117
+ const hadExistingRepo = existsSync(path.join(resolvedRepoDir, ".git"));
118
+ mkdirSync(path.dirname(resolvedRepoDir), { recursive: true });
119
+
120
+ if (hadExistingRepo) {
121
+ if (ensureHttpsRemote) {
122
+ runSafe(`git remote set-url origin https://github.com/${repo}.git`, resolvedRepoDir);
123
+ }
124
+ if (fetch) {
125
+ const fetchResult = runSafe("git fetch --prune origin", resolvedRepoDir, 60000);
126
+ if (!fetchResult.ok) {
127
+ throw new Error(`git fetch failed for ${resolvedRepoDir}: ${fetchResult.output.slice(0, 300)}`);
128
+ }
129
+ }
130
+ } else {
131
+ run(`git clone https://github.com/${repo}.git ${shellQuote(resolvedRepoDir)}`, undefined, 120000);
132
+ }
133
+
134
+ const localBranchRef = `refs/heads/${branch}`;
135
+ const remoteBranchRef = `refs/remotes/origin/${branch}`;
136
+ const localExists = runSafe(`git show-ref --verify --quiet ${shellQuote(localBranchRef)}`, resolvedRepoDir);
137
+ if (!localExists.ok) {
138
+ const remoteExists = runSafe(`git show-ref --verify --quiet ${shellQuote(remoteBranchRef)}`, resolvedRepoDir);
139
+ let branchResult;
140
+ if (remoteExists.ok) {
141
+ branchResult = runSafe(`git branch ${shellQuote(branch)} ${shellQuote(`origin/${branch}`)}`, resolvedRepoDir);
142
+ } else {
143
+ const remoteBase = `origin/${baseBranch}`;
144
+ branchResult = runSafe(`git branch ${shellQuote(branch)} ${shellQuote(remoteBase)}`, resolvedRepoDir);
145
+ if (!branchResult.ok) {
146
+ branchResult = runSafe(`git branch ${shellQuote(branch)} ${shellQuote(baseBranch)}`, resolvedRepoDir);
147
+ }
148
+ }
149
+ if (!branchResult.ok) {
150
+ throw new Error(`Failed to prepare workspace branch ${branch}: ${branchResult.output.slice(0, 300)}`);
151
+ }
152
+ }
153
+
154
+ return {
155
+ repo,
156
+ repoName,
157
+ repoDir: resolvedRepoDir,
158
+ branch,
159
+ source: hadExistingRepo ? "existing_repo" : "cloned_repo",
160
+ };
161
+ }
162
+
163
+ export function listWorktrees(repoDir) {
164
+ const result = runSafe("git worktree list --porcelain", repoDir, 30000);
165
+ if (!result.ok) return [];
166
+ const worktrees = [];
167
+ let current = {};
168
+ for (const line of [...result.output.split(/\r?\n/), ""]) {
169
+ if (!line.trim()) {
170
+ if (Object.keys(current).length) {
171
+ worktrees.push(current);
172
+ current = {};
173
+ }
174
+ continue;
175
+ }
176
+ const [key, ...rest] = line.split(" ");
177
+ const value = rest.join(" ").trim();
178
+ if (key === "worktree") current.path = value;
179
+ if (key === "branch") current.branch = value.replace(/^refs\/heads\//, "");
180
+ if (key === "HEAD") current.head = value;
181
+ if (key === "detached") current.detached = true;
182
+ if (key === "locked") current.locked = value || true;
183
+ if (key === "prunable") current.prunable = value || true;
184
+ }
185
+ return worktrees;
186
+ }
187
+
188
+ export function findWorktreeByBranch(repoDir, branch) {
189
+ return listWorktrees(repoDir).find((entry) => entry.branch === branch) || null;
190
+ }
191
+
192
+ export function removePath(targetPath) {
193
+ if (!targetPath || !existsSync(targetPath)) return;
194
+ const stat = lstatSync(targetPath);
195
+ if (stat.isSymbolicLink() || stat.isFile()) {
196
+ unlinkSync(targetPath);
197
+ return;
198
+ }
199
+ rmSync(targetPath, { recursive: true, force: true });
200
+ }
201
+
202
+ function samePath(left, right) {
203
+ if (!left || !right) return false;
204
+ return path.resolve(left) === path.resolve(right);
205
+ }
206
+
207
+ export function removeWorktree(repoDir, worktreeDir) {
208
+ if (!worktreeDir) return;
209
+ const registered = listWorktrees(repoDir).some((entry) => samePath(entry.path, worktreeDir));
210
+ if (registered) {
211
+ runSafe(`git worktree remove --force ${shellQuote(worktreeDir)}`, repoDir, 30000);
212
+ }
213
+ if (existsSync(worktreeDir)) {
214
+ removePath(worktreeDir);
215
+ }
216
+ if (registered) {
217
+ runSafe("git worktree prune", repoDir, 30000);
218
+ }
219
+ }
220
+
221
+ export function buildWorktreeDir({ workspaceRoot = "", repoName, branch, role = "after", token = uniqueToken() }) {
222
+ const root = workspaceRoot || workspaceRoots({})[0] || "/tmp";
223
+ const pieces = [sanitizeFragment(repoName, "repo"), sanitizeFragment(role, "after")];
224
+ const branchFragment = sanitizeFragment(branch, "branch");
225
+ if (branchFragment) pieces.push(branchFragment);
226
+ pieces.push(token);
227
+ return path.join(root, pieces.join("-"));
228
+ }
229
+
230
+ export function ensureWorktree({
231
+ repoDir,
232
+ worktreeDir,
233
+ ref,
234
+ branchName = "",
235
+ detach = false,
236
+ resetBranch = false,
237
+ cleanupPaths = [],
238
+ cleanupBranches = [],
239
+ verifyPackageJson = false,
240
+ } = {}) {
241
+ if (!repoDir) throw new Error("repoDir is required");
242
+ if (!worktreeDir) throw new Error("worktreeDir is required");
243
+ if (!ref) throw new Error("ref is required");
244
+
245
+ for (const candidate of cleanupPaths) {
246
+ if (candidate) removeWorktree(repoDir, candidate);
247
+ }
248
+ runSafe("git worktree prune", repoDir, 30000);
249
+ for (const candidate of cleanupBranches) {
250
+ const branchWorktree = candidate ? findWorktreeByBranch(repoDir, candidate) : null;
251
+ if (branchWorktree?.path) removeWorktree(repoDir, branchWorktree.path);
252
+ if (candidate) runSafe(`git branch -D ${shellQuote(candidate)}`, repoDir, 30000);
253
+ }
254
+
255
+ if (existsSync(worktreeDir)) {
256
+ removePath(worktreeDir);
257
+ }
258
+
259
+ const command = detach
260
+ ? `git worktree add --detach ${shellQuote(worktreeDir)} ${shellQuote(ref)}`
261
+ : resetBranch
262
+ ? `git worktree add -B ${shellQuote(branchName)} ${shellQuote(worktreeDir)} ${shellQuote(ref)}`
263
+ : `git worktree add ${shellQuote(worktreeDir)} ${shellQuote(branchName || ref)}`;
264
+
265
+ const addResult = runSafe(command, repoDir, 60000);
266
+ if (!addResult.ok) {
267
+ throw new Error(addResult.output.slice(0, 300));
268
+ }
269
+
270
+ if (verifyPackageJson && !existsSync(path.join(worktreeDir, "package.json"))) {
271
+ const contents = existsSync(worktreeDir) ? JSON.stringify(runSafe(`ls -1 ${shellQuote(worktreeDir)}`).output.split(/\r?\n/).filter(Boolean).slice(0, 50)) : '"DIR NOT FOUND"';
272
+ throw new Error(`Worktree created but package.json missing. Dir contents: ${contents}`);
273
+ }
274
+
275
+ return { worktreeDir, branchName: branchName || null, ref, detach };
276
+ }
277
+
278
+ const DEPS_MANIFEST = ".workspace-core-deps.json";
279
+
280
+ function depsManifestPath(projectDir) {
281
+ return path.join(projectDir, "node_modules", DEPS_MANIFEST);
282
+ }
283
+
284
+ export function computeDependencyFingerprint(projectDir) {
285
+ const packageJson = path.join(projectDir, "package.json");
286
+ if (!existsSync(packageJson)) return "";
287
+ const digest = createHash("sha256");
288
+ for (const name of ["package.json", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock"]) {
289
+ const filePath = path.join(projectDir, name);
290
+ if (!existsSync(filePath)) continue;
291
+ digest.update(name);
292
+ digest.update(readFileSync(filePath));
293
+ }
294
+ return digest.digest("hex");
295
+ }
296
+
297
+ export function detectInstallCommand(projectDir) {
298
+ if (!existsSync(path.join(projectDir, "package.json"))) return "";
299
+ if (existsSync(path.join(projectDir, "package-lock.json")) || existsSync(path.join(projectDir, "npm-shrinkwrap.json"))) {
300
+ return "npm ci";
301
+ }
302
+ return "npm install";
303
+ }
304
+
305
+ function readDepsManifest(projectDir) {
306
+ const manifestPath = depsManifestPath(projectDir);
307
+ if (!existsSync(manifestPath)) return {};
308
+ try {
309
+ return JSON.parse(readFileSync(manifestPath, "utf-8"));
310
+ } catch {
311
+ return {};
312
+ }
313
+ }
314
+
315
+ function writeDepsManifest(projectDir, fingerprint, installCmd) {
316
+ const manifestPath = depsManifestPath(projectDir);
317
+ mkdirSync(path.dirname(manifestPath), { recursive: true });
318
+ writeFileSync(manifestPath, JSON.stringify({ fingerprint, install_cmd: installCmd }, null, 2));
319
+ }
320
+
321
+ export function ensureDeps({ projectDir, reuseFrom = "" } = {}) {
322
+ const fingerprint = computeDependencyFingerprint(projectDir);
323
+ if (!fingerprint) return "no_package_json";
324
+
325
+ const existingManifest = readDepsManifest(projectDir);
326
+ if (existingManifest.fingerprint === fingerprint && existsSync(path.join(projectDir, "node_modules"))) {
327
+ return "already_installed";
328
+ }
329
+
330
+ if (reuseFrom && path.resolve(reuseFrom) !== path.resolve(projectDir)) {
331
+ const sourceFingerprint = computeDependencyFingerprint(reuseFrom);
332
+ const sourceManifest = readDepsManifest(reuseFrom);
333
+ const sourceModules = path.join(reuseFrom, "node_modules");
334
+ if (sourceFingerprint === fingerprint && sourceManifest.fingerprint === fingerprint && existsSync(sourceModules)) {
335
+ const projectModules = path.join(projectDir, "node_modules");
336
+ removePath(projectModules);
337
+ symlinkSync(sourceModules, projectModules);
338
+ return `reused_from:${reuseFrom}`;
339
+ }
340
+ }
341
+
342
+ const installCmd = detectInstallCommand(projectDir);
343
+ if (!installCmd) return "no_install_command";
344
+ const installResult = runSafe(`${installCmd} 2>&1 | tail -5`, projectDir, 300000);
345
+ if (!installResult.ok) {
346
+ throw new Error(`dependency install failed in ${projectDir}: ${installResult.output.slice(0, 300)}`);
347
+ }
348
+ writeDepsManifest(projectDir, fingerprint, installCmd);
349
+ return installCmd;
350
+ }
351
+
352
+ function ok(payload) {
353
+ process.stdout.write(`${JSON.stringify({ ok: true, ...payload })}\n`);
354
+ }
355
+
356
+ function fail(error) {
357
+ const message = error instanceof Error ? error.message : String(error);
358
+ process.stderr.write(`${message}\n`);
359
+ process.exitCode = 1;
360
+ }
361
+
362
+ async function main() {
363
+ const [, , command, rawPayload = "{}"] = process.argv;
364
+ if (!command) throw new Error("command is required");
365
+ const payload = JSON.parse(rawPayload);
366
+
367
+ switch (command) {
368
+ case "prepare-repo":
369
+ ok(prepareRepo(payload));
370
+ return;
371
+ case "find-worktree-by-branch":
372
+ ok({ worktree: findWorktreeByBranch(payload.repoDir, payload.branch) });
373
+ return;
374
+ case "build-worktree-dir":
375
+ ok({ worktreeDir: buildWorktreeDir(payload) });
376
+ return;
377
+ case "ensure-worktree":
378
+ ok(ensureWorktree(payload));
379
+ return;
380
+ case "ensure-deps":
381
+ ok({ status: ensureDeps(payload) });
382
+ return;
383
+ default:
384
+ throw new Error(`Unsupported command: ${command}`);
385
+ }
386
+ }
387
+
388
+ const executedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
389
+ if (executedPath && fileURLToPath(import.meta.url) === executedPath) {
390
+ main().catch(fail);
391
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -53,10 +53,22 @@
53
53
  "types": "./dist/openclaw.d.ts",
54
54
  "import": "./dist/openclaw.js",
55
55
  "require": "./dist/openclaw.cjs"
56
+ },
57
+ "./proof-run-core": {
58
+ "types": "./dist/proof-run-core.d.ts",
59
+ "import": "./dist/proof-run-core.js",
60
+ "require": "./dist/proof-run-core.cjs"
61
+ },
62
+ "./proof-run-engine": {
63
+ "types": "./dist/proof-run-engine.d.ts",
64
+ "import": "./dist/proof-run-engine.js",
65
+ "require": "./dist/proof-run-engine.cjs"
56
66
  }
57
67
  },
58
68
  "files": [
59
69
  "dist",
70
+ "lib",
71
+ "runtime",
60
72
  "README.md",
61
73
  "LICENSE"
62
74
  ],
@@ -70,9 +82,9 @@
70
82
  "typescript": "^5.4.5"
71
83
  },
72
84
  "scripts": {
73
- "build": "tsup src/index.ts src/types.ts src/result.ts src/state.ts src/runner.ts src/engine-harness.ts src/diagnostics.ts src/openclaw.ts --format cjs,esm --dts --out-dir dist --clean",
85
+ "build": "tsup src/index.ts src/types.ts src/result.ts src/state.ts src/runner.ts src/engine-harness.ts src/diagnostics.ts src/openclaw.ts src/proof-run-core.ts src/proof-run-engine.ts --format cjs,esm --dts --out-dir dist --clean",
74
86
  "clean": "rm -rf dist",
75
87
  "lint": "echo 'lint: (not configured)'",
76
- "test": "npm run build && node test.js"
88
+ "test": "npm run build && node test.js && node proof-run.test.js"
77
89
  }
78
90
  }