@coderook/cli 0.36.0 → 0.37.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/.claude-plugin/plugin.json +1 -1
- package/README.md +90 -22
- package/dist/cli/src/actions_command.js +254 -0
- package/dist/cli/src/api.js +32 -15
- package/dist/cli/src/build_command.js +8 -0
- package/dist/cli/src/cli.js +249 -25
- package/dist/cli/src/community_command.js +136 -0
- package/dist/cli/src/config.js +2 -1
- package/dist/cli/src/issue_command.js +186 -0
- package/dist/cli/src/pr_command.js +318 -0
- package/dist/cli/src/private_command.js +65 -0
- package/dist/cli/src/project_commands.js +2 -1
- package/dist/cli/src/runner.js +73 -8
- package/dist/cli/src/service_commands.js +121 -5
- package/dist/cli/src/verify_command.js +135 -0
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +45 -13
- package/dist/cli/src/github_bridge.js +0 -403
package/dist/cli/src/runner.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.maskSecrets = maskSecrets;
|
|
6
7
|
exports.withoutCredentials = withoutCredentials;
|
|
7
8
|
exports.claim = claim;
|
|
8
9
|
exports.defaultLabels = defaultLabels;
|
|
@@ -38,6 +39,15 @@ const download_js_1 = require("../../cbx/src/core/download.js");
|
|
|
38
39
|
const identify_js_1 = require("../../cbx/src/core/identify.js");
|
|
39
40
|
const config_js_1 = require("./config.js");
|
|
40
41
|
const progress_js_1 = require("./progress.js");
|
|
42
|
+
/** Replace each secret's value with ***, longest first. */
|
|
43
|
+
function maskSecrets(text, secrets) {
|
|
44
|
+
let out = text;
|
|
45
|
+
for (const value of secrets) {
|
|
46
|
+
if (value.length >= 4 && out.includes(value))
|
|
47
|
+
out = out.split(value).join("***");
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
41
51
|
/** How often to tell the service the machine is still working. */
|
|
42
52
|
const HEARTBEAT_MS = 30_000;
|
|
43
53
|
/** How often log lines are sent up while a command is running. */
|
|
@@ -65,7 +75,9 @@ async function call(route, options = {}) {
|
|
|
65
75
|
const text = await response.text();
|
|
66
76
|
const body = text ? JSON.parse(text) : {};
|
|
67
77
|
if (!response.ok) {
|
|
68
|
-
|
|
78
|
+
const error = new Error(body?.error?.message ?? `${route} failed (${response.status})`);
|
|
79
|
+
error.code = body?.error?.code;
|
|
80
|
+
throw error;
|
|
69
81
|
}
|
|
70
82
|
return body;
|
|
71
83
|
}
|
|
@@ -333,6 +345,32 @@ function patternMatcher(pattern) {
|
|
|
333
345
|
*/
|
|
334
346
|
async function performRun(repositoryId, run, runner) {
|
|
335
347
|
const progress = new progress_js_1.RunProgress();
|
|
348
|
+
/* Set when somebody cancels the run on CodeRook: the work stops now rather than when it ends. */
|
|
349
|
+
let cancelled = false;
|
|
350
|
+
/* Set when the workflow's time limit passes on this machine. */
|
|
351
|
+
let timedOut = false;
|
|
352
|
+
let limitTimer;
|
|
353
|
+
const secrets = (run.secretNames ?? [])
|
|
354
|
+
.map((name) => run.environment?.[name] ?? "")
|
|
355
|
+
.filter(Boolean)
|
|
356
|
+
.sort((a, b) => b.length - a.length);
|
|
357
|
+
let child = null;
|
|
358
|
+
const stopChild = () => {
|
|
359
|
+
if (!child?.pid || child.exitCode !== null)
|
|
360
|
+
return;
|
|
361
|
+
if (process.platform === "win32") {
|
|
362
|
+
/* The command runs through a shell; the whole tree goes, not only the shell. */
|
|
363
|
+
(0, node_child_process_1.spawn)("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
try {
|
|
367
|
+
process.kill(-child.pid, "SIGTERM");
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
child.kill("SIGTERM");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
336
374
|
const logs = new LogShipper(repositoryId, run.runId, (line) => progress.say(line));
|
|
337
375
|
const started = Date.now();
|
|
338
376
|
const workspace = await (0, promises_1.mkdtemp)(node_path_1.default.join((0, node_os_1.tmpdir)(), "coderook-run-"));
|
|
@@ -343,13 +381,18 @@ async function performRun(repositoryId, run, runner) {
|
|
|
343
381
|
void call(`/v1/repositories/${repositoryId}/runs/${run.runId}/heartbeat`, {
|
|
344
382
|
method: "POST",
|
|
345
383
|
body: { runner },
|
|
346
|
-
}).catch(() => {
|
|
384
|
+
}).catch((error) => {
|
|
347
385
|
/*
|
|
348
|
-
A lost lease means the service
|
|
349
|
-
and the local process is
|
|
350
|
-
|
|
351
|
-
|
|
386
|
+
Cancelled: end the work now. A lost lease otherwise means the service
|
|
387
|
+
has given this run to somebody else, and the local process is doing
|
|
388
|
+
work nobody will accept; the report at the end changes nothing, and
|
|
389
|
+
the run that matters is the one the other machine is doing.
|
|
352
390
|
*/
|
|
391
|
+
if (error?.code === "run_cancelled" && !cancelled) {
|
|
392
|
+
cancelled = true;
|
|
393
|
+
logs.add("Cancelled on CodeRook. Stopping.", "stderr");
|
|
394
|
+
stopChild();
|
|
395
|
+
}
|
|
353
396
|
});
|
|
354
397
|
}, HEARTBEAT_MS);
|
|
355
398
|
if (run.versionId) {
|
|
@@ -384,11 +427,18 @@ async function performRun(repositoryId, run, runner) {
|
|
|
384
427
|
Which shell is the platform's own, so a Windows machine reads it the
|
|
385
428
|
way a Windows user wrote it.
|
|
386
429
|
*/
|
|
387
|
-
|
|
430
|
+
if (cancelled) {
|
|
431
|
+
settle(1);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
child = (0, node_child_process_1.spawn)(command, {
|
|
388
435
|
cwd: directory,
|
|
389
436
|
shell: true,
|
|
437
|
+
/* Its own process group off Windows, so cancelling can stop everything it started. */
|
|
438
|
+
detached: process.platform !== "win32",
|
|
390
439
|
env: {
|
|
391
440
|
...withoutCredentials(process.env),
|
|
441
|
+
...(run.environment ?? {}),
|
|
392
442
|
CI: "true",
|
|
393
443
|
CODEROOK_RUN: String(run.number),
|
|
394
444
|
CODEROOK_VERSION: run.versionSequence
|
|
@@ -402,7 +452,7 @@ async function performRun(repositoryId, run, runner) {
|
|
|
402
452
|
can tell a slow build from a stuck one.
|
|
403
453
|
*/
|
|
404
454
|
const tee = (chunk, stream) => {
|
|
405
|
-
const text = chunk.toString("utf8");
|
|
455
|
+
const text = maskSecrets(chunk.toString("utf8"), secrets);
|
|
406
456
|
logs.add(text, stream);
|
|
407
457
|
progress.say(text);
|
|
408
458
|
};
|
|
@@ -413,7 +463,16 @@ async function performRun(repositoryId, run, runner) {
|
|
|
413
463
|
settle(1);
|
|
414
464
|
});
|
|
415
465
|
child.on("close", (status) => settle(status ?? 1));
|
|
466
|
+
if (run.timeoutMinutes) {
|
|
467
|
+
limitTimer = setTimeout(() => {
|
|
468
|
+
timedOut = true;
|
|
469
|
+
logs.add(`Stopped: past this workflow's time limit of ${run.timeoutMinutes} minutes.`, "stderr");
|
|
470
|
+
stopChild();
|
|
471
|
+
}, run.timeoutMinutes * 60_000);
|
|
472
|
+
}
|
|
416
473
|
});
|
|
474
|
+
if (limitTimer)
|
|
475
|
+
clearTimeout(limitTimer);
|
|
417
476
|
/*
|
|
418
477
|
What it built, before the folder goes.
|
|
419
478
|
|
|
@@ -433,6 +492,10 @@ async function performRun(repositoryId, run, runner) {
|
|
|
433
492
|
}
|
|
434
493
|
await logs.flush();
|
|
435
494
|
const seconds = Math.round((Date.now() - started) / 1000);
|
|
495
|
+
if (timedOut)
|
|
496
|
+
return { status: "failed", summary: `Stopped after its time limit of ${run.timeoutMinutes} minutes` };
|
|
497
|
+
if (cancelled)
|
|
498
|
+
return { status: "cancelled", summary: `Cancelled after ${seconds}s` };
|
|
436
499
|
return code === 0
|
|
437
500
|
? { status: "passed", summary: `Passed in ${seconds}s` }
|
|
438
501
|
: { status: "failed", summary: `Exited ${code} after ${seconds}s` };
|
|
@@ -448,6 +511,8 @@ async function performRun(repositoryId, run, runner) {
|
|
|
448
511
|
goes out would overwrite the verdict the caller is about to print.
|
|
449
512
|
*/
|
|
450
513
|
progress.done();
|
|
514
|
+
if (limitTimer)
|
|
515
|
+
clearTimeout(limitTimer);
|
|
451
516
|
if (beating)
|
|
452
517
|
clearInterval(beating);
|
|
453
518
|
if (flushing)
|
|
@@ -19,9 +19,12 @@ exports.commandWatch = commandWatch;
|
|
|
19
19
|
exports.commandWorkflows = commandWorkflows;
|
|
20
20
|
exports.commandRuns = commandRuns;
|
|
21
21
|
exports.commandLogs = commandLogs;
|
|
22
|
+
exports.commandCancel = commandCancel;
|
|
23
|
+
exports.commandRerun = commandRerun;
|
|
22
24
|
exports.commandTokens = commandTokens;
|
|
23
25
|
exports.commandDelete = commandDelete;
|
|
24
26
|
exports.commandHooks = commandHooks;
|
|
27
|
+
const actions_command_js_1 = require("./actions_command.js");
|
|
25
28
|
const node_process_1 = __importDefault(require("node:process"));
|
|
26
29
|
const promises_1 = require("node:readline/promises");
|
|
27
30
|
const api_js_1 = require("./api.js");
|
|
@@ -129,14 +132,33 @@ async function commandRelease(parsed) {
|
|
|
129
132
|
console.error((0, colour_js_1.red)("--downloads is public or private: cbx release v1.0 --downloads public"));
|
|
130
133
|
return 1;
|
|
131
134
|
}
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
/* `cbx release publish` publishes a draft (backend release_extras.ts). */
|
|
136
|
+
const publishing = parsed.positional[0] === "publish";
|
|
137
|
+
const name = publishing ? undefined : parsed.positional[0];
|
|
138
|
+
if (!name && downloads === undefined && !publishing) {
|
|
134
139
|
console.error((0, colour_js_1.red)("Name the release: cbx release v1.0"));
|
|
135
140
|
return 1;
|
|
136
141
|
}
|
|
137
|
-
const project = await (0, project_commands_js_1.resolveProject)(name ? parsed.positional[1] : parsed.positional[0]);
|
|
142
|
+
const project = await (0, project_commands_js_1.resolveProject)(name || publishing ? parsed.positional[1] : parsed.positional[0]);
|
|
138
143
|
if (!project)
|
|
139
144
|
return 1;
|
|
145
|
+
if (publishing) {
|
|
146
|
+
const listing = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/releases`);
|
|
147
|
+
const drafts = listing.drafts ?? [];
|
|
148
|
+
const asked = parsed.flags.get("version");
|
|
149
|
+
const draft = typeof asked === "string"
|
|
150
|
+
? drafts.find((one) => String(one.sequence) === asked.replace(/^v/i, ""))
|
|
151
|
+
: drafts.length === 1
|
|
152
|
+
? drafts[0]
|
|
153
|
+
: undefined;
|
|
154
|
+
if (!draft) {
|
|
155
|
+
console.error((0, colour_js_1.red)(drafts.length ? `Which draft? ${drafts.map((one) => `${one.name} (--version ${one.sequence})`).join(", ")}` : "This project has no draft releases."));
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
const published = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/versions/${encodeURIComponent(draft.versionId)}/release-draft/publish`, { method: "POST" });
|
|
159
|
+
console.log(`${(0, colour_js_1.green)("Published")} ${(0, colour_js_1.bold)(published.name)} ${(0, colour_js_1.dim)("at")} v${published.sequence}`);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
140
162
|
const all = await (0, api_js_1.versions)(project.id);
|
|
141
163
|
if (!all.length) {
|
|
142
164
|
console.error((0, colour_js_1.red)("This project has no versions to release."));
|
|
@@ -170,10 +192,29 @@ async function commandRelease(parsed) {
|
|
|
170
192
|
console.error((0, colour_js_1.red)(`This project has no version ${String(wanted)}.`));
|
|
171
193
|
return 1;
|
|
172
194
|
}
|
|
173
|
-
|
|
195
|
+
let notes = parsed.flags.get("notes");
|
|
196
|
+
/* Notes written from what changed since the last release (release_extras.ts). */
|
|
197
|
+
if (parsed.flags.has("generate-notes")) {
|
|
198
|
+
const written = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/versions/${encodeURIComponent(target.id)}/release-notes`);
|
|
199
|
+
notes = typeof notes === "string" ? `${notes}\n\n${written.notes}` : written.notes;
|
|
200
|
+
}
|
|
201
|
+
const channelFlag = parsed.flags.get("channel");
|
|
202
|
+
const options = {
|
|
203
|
+
...(parsed.flags.has("prerelease") ? { prerelease: true } : {}),
|
|
204
|
+
...(typeof channelFlag === "string" ? { channel: channelFlag } : {}),
|
|
205
|
+
};
|
|
206
|
+
if (name && parsed.flags.has("draft")) {
|
|
207
|
+
await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/versions/${encodeURIComponent(target.id)}/release-draft`, {
|
|
208
|
+
method: "PUT",
|
|
209
|
+
body: { name, notes: typeof notes === "string" ? notes : null, ...options },
|
|
210
|
+
});
|
|
211
|
+
console.log(`${(0, colour_js_1.green)("Drafted")} ${(0, colour_js_1.bold)(name)} ${(0, colour_js_1.dim)("at")} v${target.sequence}${(0, colour_js_1.dim)(" — nothing is public yet.")}`);
|
|
212
|
+
console.log((0, colour_js_1.dim)(` Attach builds with cbx attach, then: cbx release publish --version ${target.sequence}`));
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
174
215
|
if (name) {
|
|
175
216
|
try {
|
|
176
|
-
await (0, api_js_1.markRelease)(project.id, target.id, name, typeof notes === "string" ? notes : null);
|
|
217
|
+
await (0, api_js_1.markRelease)(project.id, target.id, name, typeof notes === "string" ? notes : null, options);
|
|
177
218
|
}
|
|
178
219
|
catch (error) {
|
|
179
220
|
console.error((0, colour_js_1.red)(error instanceof Error ? error.message : String(error)));
|
|
@@ -346,6 +387,11 @@ async function commandWatch(parsed) {
|
|
|
346
387
|
}
|
|
347
388
|
/** The automations a project has, and how they have been going. */
|
|
348
389
|
async function commandWorkflows(parsed) {
|
|
390
|
+
/* `cbx actions check …` and `cbx actions set …` (actions_command.ts). */
|
|
391
|
+
if (parsed.positional[0] === "check")
|
|
392
|
+
return (0, actions_command_js_1.commandCheckWorkflows)(parsed);
|
|
393
|
+
if (parsed.positional[0] === "set")
|
|
394
|
+
return (0, actions_command_js_1.commandSetWorkflow)(parsed);
|
|
349
395
|
const project = await (0, project_commands_js_1.resolveProject)(typeof parsed.flags.get("project") === "string"
|
|
350
396
|
? String(parsed.flags.get("project"))
|
|
351
397
|
: parsed.positional[0]);
|
|
@@ -559,6 +605,27 @@ async function commandLogs(parsed) {
|
|
|
559
605
|
}
|
|
560
606
|
return 1;
|
|
561
607
|
}
|
|
608
|
+
/*
|
|
609
|
+
--follow: keep printing as the runner sends, until the run ends. A build
|
|
610
|
+
that takes ten minutes shows its progress rather than nothing.
|
|
611
|
+
*/
|
|
612
|
+
if (parsed.flags.has("follow")) {
|
|
613
|
+
console.log(`${(0, colour_js_1.accent)(`#${run.number}`)} ${(0, colour_js_1.bold)(run.workflow)} — following until it ends`);
|
|
614
|
+
let shown = 0;
|
|
615
|
+
let state = run;
|
|
616
|
+
for (;;) {
|
|
617
|
+
const lines = await (0, api_js_1.runLogs)(project.id, run.id);
|
|
618
|
+
for (const line of lines.slice(shown))
|
|
619
|
+
console.log(line.stream === "stderr" ? (0, colour_js_1.red)(line.line) : line.line);
|
|
620
|
+
shown = Math.max(shown, lines.length);
|
|
621
|
+
if (state.status !== "queued" && state.status !== "running")
|
|
622
|
+
break;
|
|
623
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
624
|
+
state = (await (0, api_js_1.runs)(project.id)).find((one) => one.id === run.id) ?? state;
|
|
625
|
+
}
|
|
626
|
+
console.log(`${statusText(state.status)}${state.summary ? ` · ${(0, colour_js_1.dim)(state.summary)}` : ""}`);
|
|
627
|
+
return state.status === "failed" ? 1 : 0;
|
|
628
|
+
}
|
|
562
629
|
const lines = await (0, api_js_1.runLogs)(project.id, run.id);
|
|
563
630
|
console.log(`${(0, colour_js_1.accent)(`#${run.number}`)} ${(0, colour_js_1.bold)(run.workflow)} — ${statusText(run.status)}` +
|
|
564
631
|
`${run.summary ? ` · ${(0, colour_js_1.dim)(run.summary)}` : ""}`);
|
|
@@ -578,6 +645,55 @@ async function commandLogs(parsed) {
|
|
|
578
645
|
}
|
|
579
646
|
return run.status === "failed" ? 1 : 0;
|
|
580
647
|
}
|
|
648
|
+
/** Stop a run that is waiting or working, by the number people say. */
|
|
649
|
+
async function commandCancel(parsed) {
|
|
650
|
+
const asked = parsed.positional[0];
|
|
651
|
+
const number = Number((asked ?? "").replace(/^#/, ""));
|
|
652
|
+
if (!asked || !Number.isInteger(number) || number < 1) {
|
|
653
|
+
console.error((0, colour_js_1.red)("Which run? Try: cbx cancel 12"));
|
|
654
|
+
return 1;
|
|
655
|
+
}
|
|
656
|
+
const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[1]);
|
|
657
|
+
if (!project)
|
|
658
|
+
return 1;
|
|
659
|
+
const run = (await (0, api_js_1.runs)(project.id)).find((one) => one.number === number);
|
|
660
|
+
if (!run) {
|
|
661
|
+
console.error((0, colour_js_1.red)(`${project.name} has no run #${number}.`));
|
|
662
|
+
return 1;
|
|
663
|
+
}
|
|
664
|
+
if (run.status !== "queued" && run.status !== "running") {
|
|
665
|
+
console.error((0, colour_js_1.red)(`#${number} has already finished (${run.status}).`));
|
|
666
|
+
return 1;
|
|
667
|
+
}
|
|
668
|
+
await (0, api_js_1.cancelRun)(project.id, run.id);
|
|
669
|
+
console.log(`${(0, colour_js_1.accent)(`#${number}`)} cancelled. A runner working on it stops at its next heartbeat.`);
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
/** Run a finished run again, against the same save. */
|
|
673
|
+
async function commandRerun(parsed) {
|
|
674
|
+
const asked = parsed.positional[0];
|
|
675
|
+
const number = Number((asked ?? "").replace(/^#/, ""));
|
|
676
|
+
if (!asked || !Number.isInteger(number) || number < 1) {
|
|
677
|
+
console.error((0, colour_js_1.red)("Which run? Try: cbx rerun 12"));
|
|
678
|
+
return 1;
|
|
679
|
+
}
|
|
680
|
+
const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[1]);
|
|
681
|
+
if (!project)
|
|
682
|
+
return 1;
|
|
683
|
+
const run = (await (0, api_js_1.runs)(project.id)).find((one) => one.number === number);
|
|
684
|
+
if (!run) {
|
|
685
|
+
console.error((0, colour_js_1.red)(`${project.name} has no run #${number}.`));
|
|
686
|
+
return 1;
|
|
687
|
+
}
|
|
688
|
+
if (run.status === "queued" || run.status === "running") {
|
|
689
|
+
console.error((0, colour_js_1.red)(`#${number} is still going. Cancel it first with cbx cancel ${number}.`));
|
|
690
|
+
return 1;
|
|
691
|
+
}
|
|
692
|
+
const queued = await (0, api_js_1.rerunRun)(project.id, run.id);
|
|
693
|
+
console.log(`${(0, colour_js_1.accent)(`#${queued.number}`)} queued: #${number} again, against the same save.`);
|
|
694
|
+
console.log((0, colour_js_1.dim)(`Follow it with: cbx logs ${queued.number} --follow`));
|
|
695
|
+
return 0;
|
|
696
|
+
}
|
|
581
697
|
/** The tokens that can act as this account. */
|
|
582
698
|
async function commandTokens(parsed) {
|
|
583
699
|
const revoke = parsed.flags.get("revoke");
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PINNED_RELEASE_KEY = void 0;
|
|
7
|
+
exports.manifestBytes = manifestBytes;
|
|
8
|
+
exports.signatureValid = signatureValid;
|
|
9
|
+
exports.commandVerify = commandVerify;
|
|
10
|
+
exports.commandDeps = commandDeps;
|
|
11
|
+
/**
|
|
12
|
+
* `cbx verify` — check a downloaded file against its release's signed manifest.
|
|
13
|
+
* `cbx deps` — known vulnerabilities in a save's dependencies.
|
|
14
|
+
*
|
|
15
|
+
* verify fetches the release's manifest (every file with its size and SHA-256,
|
|
16
|
+
* signed with CodeRook's Ed25519 release key), checks the signature, then
|
|
17
|
+
* hashes the local file and compares. The key is the one this copy of cbx was
|
|
18
|
+
* built with when it has one, otherwise the one the service publishes, and
|
|
19
|
+
* --key pins another.
|
|
20
|
+
*/
|
|
21
|
+
const node_crypto_1 = require("node:crypto");
|
|
22
|
+
const node_fs_1 = require("node:fs");
|
|
23
|
+
const promises_1 = require("node:fs/promises");
|
|
24
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
25
|
+
const api_js_1 = require("./api.js");
|
|
26
|
+
const colour_js_1 = require("./colour.js");
|
|
27
|
+
const config_js_1 = require("./config.js");
|
|
28
|
+
const project_commands_js_1 = require("./project_commands.js");
|
|
29
|
+
/** CodeRook's release key as published when this version of cbx was built; empty means ask the service. */
|
|
30
|
+
exports.PINNED_RELEASE_KEY = "H8/DZlJXqNjUBOT7OXIoR4aEM+gcfhxurfdCrk83wNA=";
|
|
31
|
+
/** The exact bytes the service signs: the same key order, files by name. */
|
|
32
|
+
function manifestBytes(manifest) {
|
|
33
|
+
return new TextEncoder().encode(JSON.stringify({
|
|
34
|
+
format: manifest.format,
|
|
35
|
+
project: manifest.project,
|
|
36
|
+
release: manifest.release,
|
|
37
|
+
version: manifest.version,
|
|
38
|
+
channel: manifest.channel,
|
|
39
|
+
prerelease: manifest.prerelease,
|
|
40
|
+
publishedAt: manifest.publishedAt,
|
|
41
|
+
files: [...manifest.files]
|
|
42
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
43
|
+
.map((file) => ({ name: file.name, size: file.size, sha256: file.sha256 })),
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
async function signatureValid(publicKey, manifest, signature) {
|
|
47
|
+
const key = await node_crypto_1.webcrypto.subtle.importKey("raw", Buffer.from(publicKey, "base64"), { name: "Ed25519" }, false, ["verify"]);
|
|
48
|
+
return node_crypto_1.webcrypto.subtle.verify({ name: "Ed25519" }, key, Buffer.from(signature, "base64"), manifestBytes(manifest));
|
|
49
|
+
}
|
|
50
|
+
async function sha256File(file) {
|
|
51
|
+
const hash = (0, node_crypto_1.createHash)("sha256");
|
|
52
|
+
await new Promise((resolve, reject) => {
|
|
53
|
+
(0, node_fs_1.createReadStream)(file)
|
|
54
|
+
.on("data", (chunk) => hash.update(chunk))
|
|
55
|
+
.on("end", () => resolve())
|
|
56
|
+
.on("error", reject);
|
|
57
|
+
});
|
|
58
|
+
return hash.digest("hex");
|
|
59
|
+
}
|
|
60
|
+
async function commandVerify(parsed) {
|
|
61
|
+
const [project, release, ...files] = parsed.positional;
|
|
62
|
+
if (!project || !project.includes("/") || !release || !files.length) {
|
|
63
|
+
console.error((0, colour_js_1.red)("Try: cbx verify owner/project v1.0 ./app-setup.exe"));
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
const [owner, slug] = project.split("/");
|
|
67
|
+
const base = `${(0, config_js_1.apiOrigin)()}/v1/public/projects/${encodeURIComponent(owner)}/${encodeURIComponent(slug)}/releases/${encodeURIComponent(release)}`;
|
|
68
|
+
const response = await fetch(`${base}/manifest.json`, { signal: AbortSignal.timeout(60_000) });
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
console.error((0, colour_js_1.red)(`No signed manifest for ${project} ${release} (${response.status}).`));
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
const answer = (await response.json());
|
|
74
|
+
const pinned = parsed.flags.get("key");
|
|
75
|
+
const key = typeof pinned === "string" ? pinned : exports.PINNED_RELEASE_KEY && (0, config_js_1.apiOrigin)() === config_js_1.DEFAULT_API ? exports.PINNED_RELEASE_KEY : answer.publicKey;
|
|
76
|
+
if (!(await signatureValid(key, answer.manifest, answer.signature))) {
|
|
77
|
+
console.error((0, colour_js_1.red)("The manifest's signature does not check out. Do not trust these files."));
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
console.log(`${(0, colour_js_1.green)("✓")} Signed manifest for ${(0, colour_js_1.bold)(`${answer.manifest.project} ${answer.manifest.release}`)} ${(0, colour_js_1.dim)(`(key ${key.slice(0, 12)}…)`)}`);
|
|
81
|
+
let failed = false;
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const name = node_path_1.default.basename(file);
|
|
84
|
+
const entry = answer.manifest.files.find((one) => one.name === name);
|
|
85
|
+
if (!entry) {
|
|
86
|
+
console.log(`${(0, colour_js_1.red)("×")} ${name}: not one of this release's files`);
|
|
87
|
+
failed = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const size = (await (0, promises_1.stat)(file)).size;
|
|
91
|
+
const digest = await sha256File(file);
|
|
92
|
+
if (size !== entry.size || digest !== entry.sha256) {
|
|
93
|
+
console.log(`${(0, colour_js_1.red)("×")} ${name}: does not match (${size} bytes, ${digest.slice(0, 16)}…; expected ${entry.size} bytes, ${entry.sha256.slice(0, 16)}…)`);
|
|
94
|
+
failed = true;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log(`${(0, colour_js_1.green)("✓")} ${name} ${(0, colour_js_1.dim)(`${digest.slice(0, 16)}…`)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return failed ? 1 : 0;
|
|
101
|
+
}
|
|
102
|
+
async function commandDeps(parsed) {
|
|
103
|
+
const named = parsed.flags.get("project");
|
|
104
|
+
const project = await (0, project_commands_js_1.resolveProject)(typeof named === "string" ? named : undefined);
|
|
105
|
+
if (!project)
|
|
106
|
+
return 1;
|
|
107
|
+
const all = await (0, api_js_1.versions)(project.id);
|
|
108
|
+
const asked = parsed.positional[0];
|
|
109
|
+
const target = asked
|
|
110
|
+
? all.find((one) => String(one.sequence) === asked.replace(/^v/i, ""))
|
|
111
|
+
: all.reduce((newest, one) => (!newest || one.sequence > newest.sequence ? one : newest), undefined);
|
|
112
|
+
if (!target) {
|
|
113
|
+
console.error((0, colour_js_1.red)(asked ? `${project.name} has no version ${asked}.` : "This project has no saves yet."));
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
const report = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/versions/${encodeURIComponent(target.id)}/dependencies${parsed.flags.has("fresh") ? "?fresh=1" : ""}`);
|
|
117
|
+
if (report.state === "none") {
|
|
118
|
+
console.log((0, colour_js_1.dim)(`v${target.sequence} has no lockfile to check.`));
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
if (report.state === "failed") {
|
|
122
|
+
console.error((0, colour_js_1.red)(`Could not ask OSV: ${report.error ?? "unknown"}`));
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
if (!report.findings.length) {
|
|
126
|
+
console.log(`${(0, colour_js_1.green)("✓")} v${target.sequence}: ${report.packages} packages, no known vulnerabilities.`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
console.log((0, colour_js_1.red)(`v${target.sequence}: ${report.findings.length} of ${report.packages} packages have known vulnerabilities`));
|
|
130
|
+
for (const finding of report.findings) {
|
|
131
|
+
console.log(` ${(0, colour_js_1.accent)(`${finding.name}@${finding.version}`)} ${(0, colour_js_1.dim)(`${finding.ecosystem} · ${finding.file}`)}`);
|
|
132
|
+
console.log(` ${finding.ids.slice(0, 4).join(", ")}${finding.summary ? (0, colour_js_1.dim)(` — ${finding.summary}`) : ""}`);
|
|
133
|
+
}
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "The command line for CodeRook (coderook.com): version control and project hosting for projects of any size. Every save is a compressed snapshot, private or public, never used to train AI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://coderook.com",
|
package/skills/coderook/SKILL.md
CHANGED
|
@@ -306,24 +306,56 @@ unasked. `--ship` attaches the results to the newest save, so **ask before
|
|
|
306
306
|
`--ship`**, as before any save. A project's own build description lives in
|
|
307
307
|
`coderook.build.json`.
|
|
308
308
|
|
|
309
|
-
##
|
|
309
|
+
## Pull requests
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
proposal as a pull request, and the results come back as checks named
|
|
314
|
-
`github/<check>` and `review/<reviewer>`.
|
|
311
|
+
A pull request is a proposal: a line offered to another line (`cbx propose`
|
|
312
|
+
opens one). Read and review them with `cbx pr`:
|
|
315
313
|
|
|
316
314
|
```bash
|
|
317
|
-
cbx
|
|
318
|
-
cbx
|
|
319
|
-
cbx
|
|
315
|
+
cbx pr list # what is open on this project
|
|
316
|
+
cbx pr view M-3 # reviews, checks, conversation, what blocks merging
|
|
317
|
+
cbx pr files M-3 # what it changes
|
|
318
|
+
cbx pr comment M-3 --line src/app.ts:42 -m "…"
|
|
319
|
+
cbx pr approve M-3 # or request-changes M-3 -m "…"
|
|
320
|
+
cbx pr merge M-3
|
|
320
321
|
```
|
|
321
322
|
|
|
322
|
-
**Ask before `
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
failure names the file. Do not
|
|
323
|
+
**Ask before** `approve`, `request-changes`, `merge`, `close` and any comment:
|
|
324
|
+
each is the person speaking or acting, and is seen by others. An approval
|
|
325
|
+
stops counting when new saves land on the line, so re-check after pushing.
|
|
326
|
+
Every pull request carries a `coderook/malware` check: what the line adds or
|
|
327
|
+
changes is scanned before it can land, and a failure names the file. Do not
|
|
328
|
+
try to get round it.
|
|
329
|
+
|
|
330
|
+
## Private files
|
|
331
|
+
|
|
332
|
+
`cbx private add "design/"` keeps matching files out of everything outside the
|
|
333
|
+
project (listings, zips, clone, Pages). Ask before changing the list: it
|
|
334
|
+
decides what strangers can see.
|
|
335
|
+
|
|
336
|
+
## Runs
|
|
337
|
+
|
|
338
|
+
`cbx runs`, `cbx logs 12 --follow` to watch one, `cbx cancel 12` to stop one,
|
|
339
|
+
`cbx rerun 12` to run it again, `cbx approve 14` for one waiting for approval
|
|
340
|
+
(ask before each). Workflows can be YAML files in `.rook/workflows`; check
|
|
341
|
+
them with `cbx actions check` before saving. Never print or echo secrets:
|
|
342
|
+
`cbx secret set NAME` asks for the value itself, so let the person type it.
|
|
343
|
+
|
|
344
|
+
Inside a run, `CODEROOK_TOKEN`, `CODEROOK_API` and `CODEROOK_PROJECT` are set,
|
|
345
|
+
so `cbx issue comment`, `cbx pr label` and `cbx release` work with no sign-in.
|
|
346
|
+
|
|
347
|
+
## Issues, forks and security
|
|
348
|
+
|
|
349
|
+
`cbx issue view|comment|close|label|assign`, `cbx fork`, `cbx security`. Ask
|
|
350
|
+
before closing, labelling, assigning, forking, or sending anything: each is the
|
|
351
|
+
person acting where others see it. A security report is private to the
|
|
352
|
+
project's owner; never put vulnerability details in a public issue.
|
|
353
|
+
|
|
354
|
+
## Releases
|
|
355
|
+
|
|
356
|
+
`cbx release v2.0 --draft --generate-notes` then `cbx release publish`;
|
|
357
|
+
`cbx verify owner/project v2.0 ./file` checks a download; `cbx deps` lists
|
|
358
|
+
known vulnerabilities in the lockfiles. Ask before publishing.
|
|
327
359
|
|
|
328
360
|
## Mod repositories for games
|
|
329
361
|
|