graftfork 0.1.0 → 0.2.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/README.md +3 -0
- package/graft.mjs +248 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,9 @@ graft connect you/project # connect a repository Graft's GitHub app is i
|
|
|
15
15
|
graft sync # start a sync now (inside the clone, or pass the copy)
|
|
16
16
|
graft why # why the copy stayed, and what to do next
|
|
17
17
|
graft pull # fetch the latest move as graft/<release>; your branch is untouched
|
|
18
|
+
graft upstream "fix the parser" # Graft builds that one change alone on your release and runs your tests;
|
|
19
|
+
# this fetches graft/upstream/<change>. Nothing is pushed: you open the PR
|
|
20
|
+
graft upstream-pr <build> <url> # the pull request you opened; Graft tells you when it's merged
|
|
18
21
|
```
|
|
19
22
|
|
|
20
23
|
Also `graft whoami`, `graft status` and `graft usage`; `--json` prints the API's answers as they are. API docs:
|
package/graft.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/client/cli.ts
|
|
4
|
-
import { spawnSync as
|
|
4
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
|
|
@@ -53,7 +53,7 @@ async function pull(o) {
|
|
|
53
53
|
rmSync(dir, { recursive: true, force: true });
|
|
54
54
|
}
|
|
55
55
|
const sha = git(o.cwd, ["rev-parse", "--short", t.branch]).stdout.trim();
|
|
56
|
-
return { ok: true, branch: t.branch, message: `Fetched ${t.branch} (${sha}): your changes on ${t.release}. Your branch is untouched.
|
|
56
|
+
return { ok: true, branch: t.branch, message: `Fetched ${t.branch} (${sha}): ${t.release ? `your changes on ${t.release}` : "the branch Graft delivered"}. Your branch is untouched.
|
|
57
57
|
Switch with: git switch ${t.branch}
|
|
58
58
|
Or take it into ${t.copy.branch}: git checkout ${t.copy.branch} && git merge --ff-only ${t.branch}` };
|
|
59
59
|
} catch (e) {
|
|
@@ -61,6 +61,69 @@ Or take it into ${t.copy.branch}: git checkout ${t.copy.branch} && git merge --f
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// src/client/upstream.ts
|
|
65
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
66
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
67
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
68
|
+
import { join as join2 } from "node:path";
|
|
69
|
+
var git2 = (cwd, args) => spawnSync2("git", args, { cwd, encoding: "utf8" });
|
|
70
|
+
async function upstream(o) {
|
|
71
|
+
const started = await o.call("POST", `/api/v1/copies/${o.copyId}/upstream`, { change: o.change });
|
|
72
|
+
let u = started.upstream;
|
|
73
|
+
if (o.noWait) return { ok: true, message: `${started.message ?? "Building."}
|
|
74
|
+
Build: ${u.id}. Check it with graft upstream-status ${u.id}.` };
|
|
75
|
+
const until = Date.now() + (o.waitMs ?? 30 * 6e4);
|
|
76
|
+
while (u.status === "queued" || u.status === "running") {
|
|
77
|
+
if (Date.now() > until) return { ok: false, message: `Still building after ${Math.round((o.waitMs ?? 30 * 6e4) / 6e4)} minutes. Check it later with graft upstream-status ${u.id}.` };
|
|
78
|
+
await new Promise((r) => setTimeout(r, o.pollMs ?? 1e4));
|
|
79
|
+
u = (await o.call("GET", `/api/v1/upstreams/${u.id}`)).upstream;
|
|
80
|
+
}
|
|
81
|
+
if (u.status !== "built") return { ok: false, message: `Not built: ${u.note ?? "the build failed"}` };
|
|
82
|
+
return take(o, u);
|
|
83
|
+
}
|
|
84
|
+
async function take(o, u) {
|
|
85
|
+
if (!u.downloads?.bundle) return { ok: false, message: `Built ${u.branch}, but Graft doesn't keep it any more. Build it again with graft upstream.` };
|
|
86
|
+
if (git2(o.cwd, ["rev-parse", "--git-dir"]).status !== 0) return { ok: false, message: `Built ${u.branch}. Run this inside your clone of the copy to fetch it, or download it: ${u.downloads.bundle}` };
|
|
87
|
+
const r = await o.fetchImpl(u.downloads.bundle);
|
|
88
|
+
if (!r.ok) return { ok: false, message: `Downloading the branch failed (${r.status}). Try again; graft upstream-status makes a fresh link.` };
|
|
89
|
+
const dir = mkdtempSync2(join2(tmpdir2(), "graft-upstream-"));
|
|
90
|
+
const file = join2(dir, "upstream.bundle");
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync2(file, new Uint8Array(await r.arrayBuffer()));
|
|
93
|
+
const v = git2(o.cwd, ["bundle", "verify", "-q", file]);
|
|
94
|
+
if (v.status !== 0) return { ok: false, message: `This clone is missing what the branch builds on (${v.stderr.trim().split("\n").pop()}). Bring your copy's branch up to date, then run it again.` };
|
|
95
|
+
const f = git2(o.cwd, ["fetch", "-q", file, `+refs/heads/${u.branch}:refs/heads/${u.branch}`]);
|
|
96
|
+
if (f.status !== 0) return { ok: false, message: `git fetch failed: ${f.stderr.trim().split("\n").pop()}` };
|
|
97
|
+
} finally {
|
|
98
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
let saved = "";
|
|
101
|
+
if (o.patchDir && u.downloads.patch) {
|
|
102
|
+
const p = await o.fetchImpl(u.downloads.patch);
|
|
103
|
+
if (p.ok) {
|
|
104
|
+
const path = join2(o.patchDir, `${String(u.branch).split("/").pop()}.patch`);
|
|
105
|
+
writeFileSync2(path, await p.text());
|
|
106
|
+
saved = `
|
|
107
|
+
The patch, for projects that take them by email: ${path}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const t = u.tests ?? {};
|
|
111
|
+
const pr = u.pullRequest ?? {};
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
branch: u.branch,
|
|
115
|
+
message: [
|
|
116
|
+
`Fetched ${u.branch}: only "${u.change?.subject}" on ${u.release}${t.ran ? `; your tests: ${t.verdict ?? "ran"}` : "; no tests ran"}.${saved}`,
|
|
117
|
+
"Nothing was pushed. To send it, from your account:",
|
|
118
|
+
...(u.commands ?? []).slice(2).map((c) => ` ${c}`),
|
|
119
|
+
`Pull request title: ${pr.title}`,
|
|
120
|
+
"Pull request text:",
|
|
121
|
+
...String(pr.body ?? "").split("\n").map((l) => ` ${l}`),
|
|
122
|
+
`Then: graft upstream-pr ${u.id} <the pull request's URL>, and Graft tells you when it's merged.`
|
|
123
|
+
].join("\n")
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
64
127
|
// src/client/cli.ts
|
|
65
128
|
var USAGE = `graft: the command line for graftfork.com (a thin client; syncs run on graftfork.com)
|
|
66
129
|
|
|
@@ -72,8 +135,25 @@ var USAGE = `graft: the command line for graftfork.com (a thin client; syncs run
|
|
|
72
135
|
graft status [copy] a copy and its latest runs
|
|
73
136
|
graft why [copy] why the copy stayed, and what to do next
|
|
74
137
|
graft pull [copy] fetch the latest move as graft/<release> in your clone
|
|
138
|
+
graft upgrade <dependency> [version] [--copy <copy>]
|
|
139
|
+
upgrade a dependency (npm or Python), delivered only if the tests pass
|
|
140
|
+
graft upgrades [id] your upgrades, or one with what its tests said
|
|
141
|
+
graft upstream <change> [copy] build one change alone on your release, tested; fetch graft/upstream/<change>
|
|
142
|
+
[--patch <dir>] [--no-wait] (nothing is pushed: you open the pull request yourself)
|
|
143
|
+
graft upstream-status <build> an upstream build's status, tests and links
|
|
144
|
+
graft upstream-pr <build> <url> the pull request you opened upstream; Graft checks it until it's merged
|
|
75
145
|
graft usage this month's syncs and agent work
|
|
76
|
-
graft verify-record <file> check a run's signed record (from /app or GET /api/v1/runs/{id}/record)
|
|
146
|
+
graft verify-record <file> check a run's signed record (from /app or GET /api/v1/runs/{id}/record),
|
|
147
|
+
or an organization's agent change record
|
|
148
|
+
graft agent-record <org> an organization's signed agent change record, as JSON
|
|
149
|
+
[--from <YYYY-MM-DD> --to <YYYY-MM-DD>] (save it: graft agent-record <org> > record.json)
|
|
150
|
+
graft check <base> <head> check a change against the project's own tests (no push, no agent)
|
|
151
|
+
[--pr <n>] [--repo <owner/name> | --clone-url <url>] [--wait]
|
|
152
|
+
graft checks [id] your recent change checks, or one check's result
|
|
153
|
+
graft regen connect <owner/name or URL> --branch <b> --record <path> --impl <dir> --lang ts|py [--builds 1-3] [--on-push]
|
|
154
|
+
connect a spec (a duramen record): Graft rebuilds its implementation blind
|
|
155
|
+
graft regen run <spec> rebuild it now
|
|
156
|
+
graft regen status [id] your specs, a spec's rebuilds, or one rebuild and how to take it
|
|
77
157
|
|
|
78
158
|
[copy] is a copy id or owner/name; left out, it's this clone's origin remote.
|
|
79
159
|
Token: GRAFT_TOKEN or --token (make one at https://graftfork.com/app#settings). --json prints raw answers.`;
|
|
@@ -84,9 +164,9 @@ async function run(argv, io) {
|
|
|
84
164
|
args: argv,
|
|
85
165
|
allowPositionals: true,
|
|
86
166
|
strict: false,
|
|
87
|
-
options: { token: { type: "string" }, api: { type: "string" }, json: { type: "boolean" }, branch: { type: "string" }, upstream: { type: "string" }, base: { type: "string" }, tags: { type: "string" }, help: { type: "boolean", short: "h" } }
|
|
167
|
+
options: { token: { type: "string" }, api: { type: "string" }, json: { type: "boolean" }, patch: { type: "string" }, "no-wait": { type: "boolean" }, branch: { type: "string" }, upstream: { type: "string" }, base: { type: "string" }, tags: { type: "string" }, from: { type: "string" }, to: { type: "string" }, repo: { type: "string" }, "clone-url": { type: "string" }, pr: { type: "string" }, wait: { type: "boolean" }, copy: { type: "string" }, record: { type: "string" }, impl: { type: "string" }, lang: { type: "string" }, builds: { type: "string" }, "on-push": { type: "boolean" }, help: { type: "boolean", short: "h" } }
|
|
88
168
|
});
|
|
89
|
-
const [cmd, arg] = positionals;
|
|
169
|
+
const [cmd, arg, arg2] = positionals;
|
|
90
170
|
if (!cmd || a.help || cmd === "help") {
|
|
91
171
|
io.out(USAGE);
|
|
92
172
|
return cmd || a.help ? 0 : 1;
|
|
@@ -107,7 +187,7 @@ async function run(argv, io) {
|
|
|
107
187
|
const show = (b, text) => io.out(a.json ? JSON.stringify(b, null, 1) : text);
|
|
108
188
|
const copyId = async (given) => {
|
|
109
189
|
if (given && !given.includes("/") && /^[A-Za-z0-9_-]{8,64}$/.test(given)) return given;
|
|
110
|
-
const repo = given ?? repoOfRemote(
|
|
190
|
+
const repo = given ?? repoOfRemote(spawnSync3("git", ["remote", "get-url", "origin"], { cwd: io.cwd, encoding: "utf8" }).stdout ?? "");
|
|
111
191
|
if (!repo) throw new ApiError("Couldn't tell which copy: give its id or owner/name, or run this inside its clone.");
|
|
112
192
|
const { copies } = await call("GET", "/api/v1/copies");
|
|
113
193
|
const c = copies.find((x) => String(x.repo).toLowerCase() === repo.toLowerCase());
|
|
@@ -147,6 +227,73 @@ Run: ${b.run}`);
|
|
|
147
227
|
const r = await pull({ cwd: io.cwd, copy: arg, token, api, fetchImpl: io.fetch });
|
|
148
228
|
io.out(r.message);
|
|
149
229
|
return r.ok ? 0 : 1;
|
|
230
|
+
} else if (cmd === "agent-record") {
|
|
231
|
+
if (!arg) {
|
|
232
|
+
io.out("graft agent-record <organization id> [--from <YYYY-MM-DD> --to <YYYY-MM-DD>]");
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
const q = new URLSearchParams({ ...a.from && { from: String(a.from) }, ...a.to && { to: String(a.to) } }).toString();
|
|
236
|
+
const b = await call("GET", `/api/v1/orgs/${encodeURIComponent(arg)}/agent-record${q ? `?${q}` : ""}`);
|
|
237
|
+
io.out(JSON.stringify(b, null, 1));
|
|
238
|
+
} else if (cmd === "check") {
|
|
239
|
+
return await check(positionals[1], positionals[2]);
|
|
240
|
+
} else if (cmd === "checks") {
|
|
241
|
+
if (arg) {
|
|
242
|
+
const b = await call("GET", `/api/v1/checks/${encodeURIComponent(arg)}`);
|
|
243
|
+
show(b, checkText(b.check));
|
|
244
|
+
} else {
|
|
245
|
+
const b = await call("GET", "/api/v1/checks");
|
|
246
|
+
const rows = b.checks.map((c) => `${c.id} ${c.repo} ${c.head} vs ${c.base}${c.pr ? ` (#${c.pr})` : ""} ${c.outcome ?? c.status}`);
|
|
247
|
+
show(b, [...rows.length ? rows : ["No change checks yet. Start one with: graft check <base> <head>"], ...b.usage ? [`${b.usage.used} of ${b.usage.limit} checks this month.`] : []].join("\n"));
|
|
248
|
+
}
|
|
249
|
+
} else if (cmd === "upgrade") {
|
|
250
|
+
if (!arg) {
|
|
251
|
+
io.out("graft upgrade <dependency> [version] [--copy <copy id or owner/name>]");
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
const b = await call("POST", "/api/v1/upgrades", { copy_id: await copyId(a.copy), dependency: arg, ...positionals[2] && { version: positionals[2] } });
|
|
255
|
+
show(b, `${b.message}
|
|
256
|
+
Upgrade: ${b.upgrade} (graft upgrades ${b.upgrade})`);
|
|
257
|
+
} else if (cmd === "upgrades") {
|
|
258
|
+
if (arg) {
|
|
259
|
+
const { upgrade: u } = await call("GET", `/api/v1/upgrades/${encodeURIComponent(arg)}`);
|
|
260
|
+
const lines = [`${u.dependency} ${u.from ?? ""} to ${u.version ?? u.requested} on ${u.repo ?? "?"}: ${u.status}`, ...u.why ? [` ${u.why}`] : []];
|
|
261
|
+
if (u.tests?.newFailures?.length) lines.push("Broken by the upgrade:", ...u.tests.newFailures.map((f) => ` ${f}`));
|
|
262
|
+
if (u.fix) lines.push(`Graft's agent: ${u.fix.applied ? `changed ${(u.fix.files ?? []).join(", ")}` : `not used (${u.fix.why})`}`);
|
|
263
|
+
if (u.delivered) lines.push(`Delivered as ${u.delivered.branch}. Take it in your clone with: graft pull ${u.run}`);
|
|
264
|
+
if (u.prUrl) lines.push(`Pull request: ${u.prUrl}`);
|
|
265
|
+
show({ upgrade: u }, lines.join("\n"));
|
|
266
|
+
} else {
|
|
267
|
+
const b = await call("GET", "/api/v1/upgrades");
|
|
268
|
+
const rows = b.upgrades.map((u) => `${u.id} ${u.repo ?? "?"} ${u.dependency} to ${u.version ?? u.requested}: ${u.status}`);
|
|
269
|
+
show(b, rows.length ? rows.join("\n") : "No upgrades yet. Start one with: graft upgrade <dependency> [version]");
|
|
270
|
+
}
|
|
271
|
+
} else if (cmd === "upstream") {
|
|
272
|
+
if (!arg) {
|
|
273
|
+
io.out("graft upstream <change> [copy] [--patch <dir>] [--no-wait]: the change's subject (or part of it), or its number in the latest move");
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
const r = await upstream({ cwd: io.cwd, copyId: await copyId(arg2), change: arg, call, fetchImpl: io.fetch, noWait: !!a["no-wait"], ...a.patch && { patchDir: String(a.patch) }, ...io.env.GRAFT_POLL_MS && { pollMs: Number(io.env.GRAFT_POLL_MS) } });
|
|
277
|
+
io.out(r.message);
|
|
278
|
+
return r.ok ? 0 : 1;
|
|
279
|
+
} else if (cmd === "upstream-status") {
|
|
280
|
+
if (!arg) {
|
|
281
|
+
io.out("graft upstream-status <build>");
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
const b = await call("GET", `/api/v1/upstreams/${encodeURIComponent(arg)}`);
|
|
285
|
+
const u = b.upstream;
|
|
286
|
+
const pr = u.pullRequest ?? {};
|
|
287
|
+
show(b, [`${u.change?.subject} on ${u.release}: ${u.status}${u.phase ? ` (${u.phase})` : ""}${u.note ? `. ${u.note}` : ""}`, ...(u.commands ?? []).map((c) => ` ${c}`), ...pr.url ? [`Pull request: ${pr.url}${pr.state ? ` (${pr.state})` : ""}`] : []].join("\n"));
|
|
288
|
+
} else if (cmd === "upstream-pr") {
|
|
289
|
+
if (!arg || !arg2) {
|
|
290
|
+
io.out("graft upstream-pr <build> <https://github.com/owner/name/pull/123>");
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
const b = await call("POST", `/api/v1/upstreams/${encodeURIComponent(arg)}/pr`, { url: arg2 });
|
|
294
|
+
show(b, b.message);
|
|
295
|
+
} else if (cmd === "regen") {
|
|
296
|
+
return await regen(arg, arg2, a, call, show, io);
|
|
150
297
|
} else if (cmd === "usage") {
|
|
151
298
|
const b = await call("GET", "/api/v1/usage");
|
|
152
299
|
show(b, `${b.plan}: ${b.syncs?.used ?? 0} of ${b.syncs?.limit ?? "?"} syncs this month; Graft's agent $${Number(b.graftAgentUsd ?? 0).toFixed(2)}. Resets ${b.resetsOn}.`);
|
|
@@ -159,6 +306,97 @@ Run: ${b.run}`);
|
|
|
159
306
|
io.out(e instanceof ApiError ? e.message : `graft: ${e.message}`);
|
|
160
307
|
return 1;
|
|
161
308
|
}
|
|
309
|
+
async function check(base, head) {
|
|
310
|
+
const pr = a.pr === void 0 ? void 0 : Number(a.pr);
|
|
311
|
+
if ((!base || !head) && pr === void 0) {
|
|
312
|
+
io.out("graft check <base> <head> [--pr <n>] [--repo <owner/name> | --clone-url <url>] [--wait]");
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
let where;
|
|
316
|
+
if (a.repo) where = { repo: a.repo };
|
|
317
|
+
else if (a["clone-url"]) where = { clone_url: a["clone-url"] };
|
|
318
|
+
else {
|
|
319
|
+
const remote = (spawnSync3("git", ["remote", "get-url", "origin"], { cwd: io.cwd, encoding: "utf8" }).stdout ?? "").trim();
|
|
320
|
+
const repo = repoOfRemote(remote);
|
|
321
|
+
if (/github\.com[:/]/.test(remote) && repo) where = { repo };
|
|
322
|
+
else if (/^https:\/\//.test(remote) && !/@/.test(remote.split("/")[2] ?? "")) where = { clone_url: remote };
|
|
323
|
+
else {
|
|
324
|
+
io.out("Couldn't tell which repository: pass --repo <owner/name> or --clone-url <url>, or run this inside its clone.");
|
|
325
|
+
return 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const b = await call("POST", "/api/v1/checks", { ...where, ...base && { base }, ...head && { head }, ...pr !== void 0 && { pr } });
|
|
329
|
+
if (!a.wait) {
|
|
330
|
+
show(b, `${b.message}
|
|
331
|
+
Check: ${b.check} (graft checks ${b.check})`);
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
const sleep = io.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
335
|
+
for (let i = 0; i < 200; i++) {
|
|
336
|
+
const c = (await call("GET", `/api/v1/checks/${encodeURIComponent(b.check)}`)).check;
|
|
337
|
+
if (!["queued", "running"].includes(c.status)) {
|
|
338
|
+
show({ check: c }, checkText(c));
|
|
339
|
+
return c.outcome === "pass" ? 0 : c.outcome === "fail" ? 1 : 2;
|
|
340
|
+
}
|
|
341
|
+
await sleep(15e3);
|
|
342
|
+
}
|
|
343
|
+
io.out(`Still running. Read it later with: graft checks ${b.check}`);
|
|
344
|
+
return 2;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function checkText(c) {
|
|
348
|
+
const word = c.outcome === "pass" ? "PASS" : c.outcome === "fail" ? "FAIL" : c.outcome === "could_not_run" ? "COULD NOT RUN" : String(c.status).toUpperCase();
|
|
349
|
+
const sha = (r) => r?.sha ? ` (${String(r.sha).slice(0, 7)})` : "";
|
|
350
|
+
return [
|
|
351
|
+
`${word}: ${c.repo} ${c.head}${sha(c.headRun)} against ${c.base}${sha(c.baseRun)}${c.pr ? `, pull request #${c.pr}` : ""}`,
|
|
352
|
+
...c.reason ? [c.reason] : [],
|
|
353
|
+
...(c.newFailures ?? []).length ? ["New failures:", ...c.newFailures.map((f) => ` ${f}`)] : [],
|
|
354
|
+
...(c.alreadyFailing ?? []).length ? [`${c.alreadyFailing.length} already failing on the base (not counted against the change).`] : [],
|
|
355
|
+
...(c.testFilesChanged ?? []).length ? [`The change edits files that shape the tests: ${c.testFilesChanged.join(", ")}. Read them before trusting the result.`] : []
|
|
356
|
+
].join("\n");
|
|
357
|
+
}
|
|
358
|
+
async function regen(sub, id, a, call, show, io) {
|
|
359
|
+
const line = (r) => `${r.id} ${r.status}${r.phase ? ` (${r.phase})` : ""}${r.suite?.total ? ` suite ${r.suite.passed}/${r.suite.total}` : ""}${r.choicesDiffer ? ` ${r.choicesDiffer} choice(s) differ` : ""}${r.why ? `: ${r.why}` : ""}`;
|
|
360
|
+
if (sub === "connect") {
|
|
361
|
+
if (!id || !a.record || !a.impl || !a.lang || !a.branch) {
|
|
362
|
+
io.out("graft regen connect <owner/name or https URL> --branch <b> --record <path> --impl <dir> --lang ts|py [--builds 1-3] [--on-push]");
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
const b = await call("POST", "/api/v1/regen", { .../^https:\/\//.test(id) ? { clone_url: id } : { repo: id }, branch: a.branch, record: a.record, impl: a.impl, lang: a.lang, ...a.builds && { builds: Number(a.builds) }, ...a["on-push"] && { on_push: true } });
|
|
366
|
+
show(b, `${b.message ?? "Connected."}
|
|
367
|
+
Spec id: ${b.id}`);
|
|
368
|
+
} else if (sub === "run") {
|
|
369
|
+
if (!id) {
|
|
370
|
+
io.out("graft regen run <spec id>");
|
|
371
|
+
return 1;
|
|
372
|
+
}
|
|
373
|
+
const b = await call("POST", `/api/v1/regen/${encodeURIComponent(id)}/run`);
|
|
374
|
+
show(b, `${b.message}
|
|
375
|
+
Rebuild: ${b.run}`);
|
|
376
|
+
} else if (sub === "status") {
|
|
377
|
+
if (!id) {
|
|
378
|
+
const b = await call("GET", "/api/v1/regen");
|
|
379
|
+
const rows = b.specs.map((s) => `${s.id} ${s.repo} ${s.branch}: ${s.record} -> ${s.impl} (${s.lang})${s.last ? `
|
|
380
|
+
last: ${line(s.last)}` : "\n never rebuilt"}`);
|
|
381
|
+
show(b, rows.length ? rows.join("\n") : b.available ? "No specs yet. Connect one with: graft regen connect <owner/name> --branch main --record spec/ --impl impl --lang ts" : "Rebuilding from a spec is on Premium, Team and Enterprise.");
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
const spec = await call("GET", `/api/v1/regen/${encodeURIComponent(id)}`).catch(() => null);
|
|
385
|
+
if (spec) {
|
|
386
|
+
show(spec, [`${spec.spec.repo} ${spec.spec.branch}: ${spec.spec.record} -> ${spec.spec.impl} (${spec.spec.lang})`, ...spec.runs.length ? spec.runs.map((r) => ` ${line(r)}`) : [" No rebuilds yet."]].join("\n"));
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
const { run: run2 } = await call("GET", `/api/v1/regen/runs/${encodeURIComponent(id)}`);
|
|
390
|
+
const differ = run2.choicesDiff?.previous;
|
|
391
|
+
const lines = [line(run2), ...run2.builds.map((b) => ` build ${b.n}: ${b.verdict?.why ?? b.note ?? b.status}`)];
|
|
392
|
+
if (differ) for (const [k, label] of [["changed", "chosen differently"], ["added", "new"], ["removed", "gone"]]) for (const c of differ[k]) lines.push(` ${label}: ${c.title}`);
|
|
393
|
+
if (run2.take) lines.push("Take it, in your clone:", ...run2.take.commands.map((c) => ` ${c}`));
|
|
394
|
+
show({ run: run2 }, lines.join("\n"));
|
|
395
|
+
} else {
|
|
396
|
+
io.out("graft regen connect|run|status (graft help)");
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
return 0;
|
|
162
400
|
}
|
|
163
401
|
var unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
164
402
|
async function verify(file, api, io) {
|
|
@@ -194,6 +432,10 @@ async function verify(file, api, io) {
|
|
|
194
432
|
return 1;
|
|
195
433
|
}
|
|
196
434
|
const r = JSON.parse(e.payload);
|
|
435
|
+
if (r.graft_agent_record === 1) {
|
|
436
|
+
io.out(`Valid: signed by graftfork.com (key ${k.id}). Agent change record for ${r.org?.name ?? r.org?.id}, ${r.range?.from} to ${r.range?.to}: ${(r.changes ?? []).length} change(s) in ${r.counts?.runs ?? 0} run(s), ${(r.agent_actions ?? []).length} agent action(s).`);
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
197
439
|
io.out(`Valid: signed by graftfork.com (key ${k.id}). Run ${r.run}, ${r.outcome}${r.upstream?.to ? ` onto ${r.upstream.to}` : ""}, ${(r.changes ?? []).length} change(s).`);
|
|
198
440
|
return 0;
|
|
199
441
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graftfork",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The command line for graftfork.com: connect a copy, sync it, see why it stayed, and take each move. A thin client; syncs run on graftfork.com.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://graftfork.com/docs/api",
|