@coderook/cli 0.36.0 → 0.38.1
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 +297 -44
- 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 +108 -9
- package/dist/cli/src/service_commands.js +121 -5
- package/dist/cli/src/verify_command.js +135 -0
- package/dist/cli/src/workspace_command.js +198 -0
- package/package.json +66 -66
- package/skills/coderook/SKILL.md +413 -371
- package/dist/cli/src/github_bridge.js +0 -403
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commandPrivate = commandPrivate;
|
|
4
|
+
/**
|
|
5
|
+
* `cbx private` — files and folders only the project's own people may see.
|
|
6
|
+
*
|
|
7
|
+
* Patterns in the shape of a .gitignore, kept on the project rather than in a
|
|
8
|
+
* file inside it, so they cover every version from the moment they are set,
|
|
9
|
+
* old ones included, and a save can never undo them. Visitors, the public
|
|
10
|
+
* API, git clone, zips and Pages leave those files out; members see them.
|
|
11
|
+
*/
|
|
12
|
+
const api_js_1 = require("./api.js");
|
|
13
|
+
const colour_js_1 = require("./colour.js");
|
|
14
|
+
const project_commands_js_1 = require("./project_commands.js");
|
|
15
|
+
async function commandPrivate(parsed) {
|
|
16
|
+
const sub = parsed.positional[0] ?? "list";
|
|
17
|
+
const named = parsed.flags.get("project");
|
|
18
|
+
const project = await (0, project_commands_js_1.resolveProject)(typeof named === "string" ? named : undefined);
|
|
19
|
+
if (!project)
|
|
20
|
+
return 1;
|
|
21
|
+
const current = await (0, api_js_1.getPrivatePaths)(project.id);
|
|
22
|
+
if (sub === "list") {
|
|
23
|
+
if (!current.length) {
|
|
24
|
+
console.log(`Nothing in ${(0, colour_js_1.bold)(project.slug)} is private on its own.`);
|
|
25
|
+
console.log((0, colour_js_1.dim)(` Keep files to the project's people: cbx private add "design/" "*.psd"`));
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
console.log((0, colour_js_1.bold)(`Private in ${project.slug}`) + (0, colour_js_1.dim)(" — only the project's people see these"));
|
|
29
|
+
for (const pattern of current)
|
|
30
|
+
console.log(` ${pattern}`);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
const given = parsed.positional.slice(1);
|
|
34
|
+
if (!given.length) {
|
|
35
|
+
console.error((0, colour_js_1.red)(`Name at least one pattern: cbx private ${sub} "design/"`));
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
let next;
|
|
39
|
+
if (sub === "add") {
|
|
40
|
+
next = [...current, ...given.filter((one) => !current.includes(one))];
|
|
41
|
+
}
|
|
42
|
+
else if (sub === "remove") {
|
|
43
|
+
const missing = given.filter((one) => !current.includes(one));
|
|
44
|
+
if (missing.length) {
|
|
45
|
+
console.error((0, colour_js_1.red)(`Not in the list: ${missing.join(", ")}. See: cbx private list`));
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
next = current.filter((one) => !given.includes(one));
|
|
49
|
+
}
|
|
50
|
+
else if (sub === "set") {
|
|
51
|
+
next = given;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.error((0, colour_js_1.red)(`cbx private has no "${sub}". `) + "See: " + (0, colour_js_1.accent)("cbx help private"));
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const saved = await (0, api_js_1.putPrivatePaths)(project.id, next);
|
|
58
|
+
console.log(`${(0, colour_js_1.green)("Saved.")} ${saved.length ? `Private in ${(0, colour_js_1.bold)(project.slug)}:` : `Nothing in ${project.slug} is private on its own now.`}`);
|
|
59
|
+
for (const pattern of saved)
|
|
60
|
+
console.log(` ${pattern}`);
|
|
61
|
+
if (sub !== "remove" && saved.length) {
|
|
62
|
+
console.log((0, colour_js_1.dim)(" Every version obeys this now, old ones included. Members still see everything."));
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
@@ -42,7 +42,8 @@ function bytes(value) {
|
|
|
42
42
|
* its name every time.
|
|
43
43
|
*/
|
|
44
44
|
async function resolveProject(reference) {
|
|
45
|
-
|
|
45
|
+
/* Inside a run, the project it is running for (CODEROOK_PROJECT, handed to every run). */
|
|
46
|
+
const wanted = reference ?? (await (0, config_js_1.readLink)(node_process_1.default.cwd()))?.slug ?? node_process_1.default.env.CODEROOK_PROJECT?.trim();
|
|
46
47
|
if (!wanted) {
|
|
47
48
|
console.error((0, colour_js_1.red)("Which project? Name one, or run this inside a linked folder."));
|
|
48
49
|
return null;
|
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;
|
|
@@ -31,6 +32,9 @@ exports.report = report;
|
|
|
31
32
|
const node_child_process_1 = require("node:child_process");
|
|
32
33
|
const node_crypto_1 = require("node:crypto");
|
|
33
34
|
const promises_1 = require("node:fs/promises");
|
|
35
|
+
const node_fs_1 = require("node:fs");
|
|
36
|
+
const node_stream_1 = require("node:stream");
|
|
37
|
+
const promises_2 = require("node:stream/promises");
|
|
34
38
|
const node_os_1 = require("node:os");
|
|
35
39
|
const node_os_2 = __importDefault(require("node:os"));
|
|
36
40
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -38,6 +42,15 @@ const download_js_1 = require("../../cbx/src/core/download.js");
|
|
|
38
42
|
const identify_js_1 = require("../../cbx/src/core/identify.js");
|
|
39
43
|
const config_js_1 = require("./config.js");
|
|
40
44
|
const progress_js_1 = require("./progress.js");
|
|
45
|
+
/** Replace each secret's value with ***, longest first. */
|
|
46
|
+
function maskSecrets(text, secrets) {
|
|
47
|
+
let out = text;
|
|
48
|
+
for (const value of secrets) {
|
|
49
|
+
if (value.length >= 4 && out.includes(value))
|
|
50
|
+
out = out.split(value).join("***");
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
41
54
|
/** How often to tell the service the machine is still working. */
|
|
42
55
|
const HEARTBEAT_MS = 30_000;
|
|
43
56
|
/** How often log lines are sent up while a command is running. */
|
|
@@ -65,7 +78,9 @@ async function call(route, options = {}) {
|
|
|
65
78
|
const text = await response.text();
|
|
66
79
|
const body = text ? JSON.parse(text) : {};
|
|
67
80
|
if (!response.ok) {
|
|
68
|
-
|
|
81
|
+
const error = new Error(body?.error?.message ?? `${route} failed (${response.status})`);
|
|
82
|
+
error.code = body?.error?.code;
|
|
83
|
+
throw error;
|
|
69
84
|
}
|
|
70
85
|
return body;
|
|
71
86
|
}
|
|
@@ -333,6 +348,32 @@ function patternMatcher(pattern) {
|
|
|
333
348
|
*/
|
|
334
349
|
async function performRun(repositoryId, run, runner) {
|
|
335
350
|
const progress = new progress_js_1.RunProgress();
|
|
351
|
+
/* Set when somebody cancels the run on CodeRook: the work stops now rather than when it ends. */
|
|
352
|
+
let cancelled = false;
|
|
353
|
+
/* Set when the workflow's time limit passes on this machine. */
|
|
354
|
+
let timedOut = false;
|
|
355
|
+
let limitTimer;
|
|
356
|
+
const secrets = (run.secretNames ?? [])
|
|
357
|
+
.map((name) => run.environment?.[name] ?? "")
|
|
358
|
+
.filter(Boolean)
|
|
359
|
+
.sort((a, b) => b.length - a.length);
|
|
360
|
+
let child = null;
|
|
361
|
+
const stopChild = () => {
|
|
362
|
+
if (!child?.pid || child.exitCode !== null)
|
|
363
|
+
return;
|
|
364
|
+
if (process.platform === "win32") {
|
|
365
|
+
/* The command runs through a shell; the whole tree goes, not only the shell. */
|
|
366
|
+
(0, node_child_process_1.spawn)("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
try {
|
|
370
|
+
process.kill(-child.pid, "SIGTERM");
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
child.kill("SIGTERM");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
336
377
|
const logs = new LogShipper(repositoryId, run.runId, (line) => progress.say(line));
|
|
337
378
|
const started = Date.now();
|
|
338
379
|
const workspace = await (0, promises_1.mkdtemp)(node_path_1.default.join((0, node_os_1.tmpdir)(), "coderook-run-"));
|
|
@@ -343,16 +384,52 @@ async function performRun(repositoryId, run, runner) {
|
|
|
343
384
|
void call(`/v1/repositories/${repositoryId}/runs/${run.runId}/heartbeat`, {
|
|
344
385
|
method: "POST",
|
|
345
386
|
body: { runner },
|
|
346
|
-
}).catch(() => {
|
|
387
|
+
}).catch((error) => {
|
|
347
388
|
/*
|
|
348
|
-
A lost lease means the service
|
|
349
|
-
and the local process is
|
|
350
|
-
|
|
351
|
-
|
|
389
|
+
Cancelled: end the work now. A lost lease otherwise means the service
|
|
390
|
+
has given this run to somebody else, and the local process is doing
|
|
391
|
+
work nobody will accept; the report at the end changes nothing, and
|
|
392
|
+
the run that matters is the one the other machine is doing.
|
|
352
393
|
*/
|
|
394
|
+
if (error?.code === "run_cancelled" && !cancelled) {
|
|
395
|
+
cancelled = true;
|
|
396
|
+
logs.add("Cancelled on CodeRook. Stopping.", "stderr");
|
|
397
|
+
stopChild();
|
|
398
|
+
}
|
|
353
399
|
});
|
|
354
400
|
}, HEARTBEAT_MS);
|
|
355
|
-
if (run.
|
|
401
|
+
if (run.workspaceInput) {
|
|
402
|
+
const input = await call(`/v1/repositories/${repositoryId}/workspace-runs/${run.runId}/input`);
|
|
403
|
+
run.command = input.command;
|
|
404
|
+
run.workingDirectory = input.workingDirectory;
|
|
405
|
+
run.artifactPaths = input.artifactPaths;
|
|
406
|
+
logs.add(`Fetching ${input.files.length} selected files from frozen workspace revision ${input.revision}…`, 'stdout');
|
|
407
|
+
const token = await (0, config_js_1.loadToken)();
|
|
408
|
+
for (const file of input.files) {
|
|
409
|
+
if (cancelled)
|
|
410
|
+
throw new Error('Cancelled while fetching workspace input');
|
|
411
|
+
if (!/^[a-f0-9]{64}$/.test(file.sha256 ?? ''))
|
|
412
|
+
throw new Error(`The frozen file ${file.path} has no verified content digest`);
|
|
413
|
+
const destination = insideWorkspace(workspace, file.path);
|
|
414
|
+
await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
|
|
415
|
+
const response = await fetch(`${(0, config_js_1.apiOrigin)()}/v1/repositories/${repositoryId}/workspace-runs/${run.runId}/input/${file.index}`, { headers: { authorization: `Bearer ${token}`, ...(0, identify_js_1.clientHeaders)() }, signal: AbortSignal.timeout(REQUEST_DEADLINE_MS) });
|
|
416
|
+
if (!response.ok || !response.body)
|
|
417
|
+
throw new Error(`Could not fetch frozen workspace file ${file.path} (${response.status})`);
|
|
418
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
419
|
+
let bytes = 0;
|
|
420
|
+
const verify = new node_stream_1.Transform({ transform(chunk, _encoding, callback) { bytes += chunk.length; if (bytes > file.size) {
|
|
421
|
+
callback(new Error('Frozen workspace file exceeds its declared size'));
|
|
422
|
+
return;
|
|
423
|
+
} hash.update(chunk); callback(null, chunk); } });
|
|
424
|
+
await (0, promises_2.pipeline)(node_stream_1.Readable.fromWeb(response.body), verify, (0, node_fs_1.createWriteStream)(destination));
|
|
425
|
+
if (bytes !== file.size || hash.digest('hex') !== file.sha256)
|
|
426
|
+
throw new Error(`Frozen content verification failed for ${file.path}; credential-bearing source must use separately configured credentials`);
|
|
427
|
+
if (file.executable && process.platform !== 'win32')
|
|
428
|
+
await (0, promises_1.chmod)(destination, 0o755);
|
|
429
|
+
}
|
|
430
|
+
logs.add('Selected workspace inputs verified.', 'stdout');
|
|
431
|
+
}
|
|
432
|
+
else if (run.versionId) {
|
|
356
433
|
logs.add(`Fetching v${run.versionSequence ?? "?"}…`, "stdout");
|
|
357
434
|
/*
|
|
358
435
|
The one phase that knows its own size. The version says how many files
|
|
@@ -384,11 +461,18 @@ async function performRun(repositoryId, run, runner) {
|
|
|
384
461
|
Which shell is the platform's own, so a Windows machine reads it the
|
|
385
462
|
way a Windows user wrote it.
|
|
386
463
|
*/
|
|
387
|
-
|
|
464
|
+
if (cancelled) {
|
|
465
|
+
settle(1);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
child = (0, node_child_process_1.spawn)(command, {
|
|
388
469
|
cwd: directory,
|
|
389
470
|
shell: true,
|
|
471
|
+
/* Its own process group off Windows, so cancelling can stop everything it started. */
|
|
472
|
+
detached: process.platform !== "win32",
|
|
390
473
|
env: {
|
|
391
474
|
...withoutCredentials(process.env),
|
|
475
|
+
...(run.environment ?? {}),
|
|
392
476
|
CI: "true",
|
|
393
477
|
CODEROOK_RUN: String(run.number),
|
|
394
478
|
CODEROOK_VERSION: run.versionSequence
|
|
@@ -402,7 +486,7 @@ async function performRun(repositoryId, run, runner) {
|
|
|
402
486
|
can tell a slow build from a stuck one.
|
|
403
487
|
*/
|
|
404
488
|
const tee = (chunk, stream) => {
|
|
405
|
-
const text = chunk.toString("utf8");
|
|
489
|
+
const text = maskSecrets(chunk.toString("utf8"), secrets);
|
|
406
490
|
logs.add(text, stream);
|
|
407
491
|
progress.say(text);
|
|
408
492
|
};
|
|
@@ -413,7 +497,16 @@ async function performRun(repositoryId, run, runner) {
|
|
|
413
497
|
settle(1);
|
|
414
498
|
});
|
|
415
499
|
child.on("close", (status) => settle(status ?? 1));
|
|
500
|
+
if (run.timeoutMinutes) {
|
|
501
|
+
limitTimer = setTimeout(() => {
|
|
502
|
+
timedOut = true;
|
|
503
|
+
logs.add(`Stopped: past this workflow's time limit of ${run.timeoutMinutes} minutes.`, "stderr");
|
|
504
|
+
stopChild();
|
|
505
|
+
}, run.timeoutMinutes * 60_000);
|
|
506
|
+
}
|
|
416
507
|
});
|
|
508
|
+
if (limitTimer)
|
|
509
|
+
clearTimeout(limitTimer);
|
|
417
510
|
/*
|
|
418
511
|
What it built, before the folder goes.
|
|
419
512
|
|
|
@@ -433,6 +526,10 @@ async function performRun(repositoryId, run, runner) {
|
|
|
433
526
|
}
|
|
434
527
|
await logs.flush();
|
|
435
528
|
const seconds = Math.round((Date.now() - started) / 1000);
|
|
529
|
+
if (timedOut)
|
|
530
|
+
return { status: "failed", summary: `Stopped after its time limit of ${run.timeoutMinutes} minutes` };
|
|
531
|
+
if (cancelled)
|
|
532
|
+
return { status: "cancelled", summary: `Cancelled after ${seconds}s` };
|
|
436
533
|
return code === 0
|
|
437
534
|
? { status: "passed", summary: `Passed in ${seconds}s` }
|
|
438
535
|
: { status: "failed", summary: `Exited ${code} after ${seconds}s` };
|
|
@@ -448,6 +545,8 @@ async function performRun(repositoryId, run, runner) {
|
|
|
448
545
|
goes out would overwrite the verdict the caller is about to print.
|
|
449
546
|
*/
|
|
450
547
|
progress.done();
|
|
548
|
+
if (limitTimer)
|
|
549
|
+
clearTimeout(limitTimer);
|
|
451
550
|
if (beating)
|
|
452
551
|
clearInterval(beating);
|
|
453
552
|
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
|
+
}
|