@coderook/cli 0.35.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.
@@ -2,7 +2,7 @@
2
2
  "name": "coderook",
3
3
  "displayName": "CodeRook",
4
4
  "description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, with cbx or plain git, from Claude Code.",
5
- "version": "0.35.0",
5
+ "version": "0.37.0",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
package/README.md CHANGED
@@ -169,7 +169,7 @@ Once a folder has a history, `status`, `diff`, `log`, `switch`, `merge`,
169
169
  unsaved change. `--force` discards those changes. Files the history has never
170
170
  held are never touched.
171
171
 
172
- ## Proposals
172
+ ## Proposals and pull requests
173
173
 
174
174
  A line gets back to main by being proposed. The proposal is checked and
175
175
  reviewed before it lands, and it follows the line: saving onto it afterwards
@@ -194,11 +194,56 @@ Tests whenever a line is proposed or saved onto, and its result is a required
194
194
  check. `--on-save` runs an action on every save, and `--by-hand` only when
195
195
  somebody starts it.
196
196
 
197
+ Every proposal also has a `coderook/malware` check: what the line adds or
198
+ changes is scanned before it can land, and a finding names the file and stops
199
+ the proposal being applied.
200
+
197
201
  A **protected** line refuses saves made straight onto it — from `cbx submit`,
198
202
  the desktop application and `git push` alike — so the way in is a line of your
199
203
  own and a proposal. It also cannot be put back on an earlier save, and a merge
200
204
  cannot land on it over a conflict or a failing check.
201
205
 
206
+ ### Pull requests from the terminal
207
+
208
+ ```
209
+ cbx pr list [--all] [--mine] [--label bug] this project's pull requests
210
+ cbx pr view M-3 description, reviews, checks, conversation, what blocks it
211
+ cbx pr files M-3 what it changes
212
+ cbx pr comment M-3 -m "…" [--line src/app.ts:42]
213
+ cbx pr approve M-3 or: cbx pr request-changes M-3 -m "…"
214
+ cbx pr ask M-3 sam,priya ask people to review it
215
+ cbx pr label M-3 bug,ui
216
+ cbx pr ready M-3 or: draft, title, retarget, close, reopen
217
+ cbx pr merge M-3
218
+ ```
219
+
220
+ An approval stops counting when new saves land on the line, and a draft
221
+ cannot be merged until it is marked ready. The website's Pull requests tab
222
+ does the same, with comments on lines and suggested changes.
223
+
224
+ ## Keeping files private
225
+
226
+ ```
227
+ cbx private add "design/" "*.psd" a folder anywhere, a file type
228
+ cbx private list
229
+ cbx private remove "*.psd"
230
+ ```
231
+
232
+ In a public project, everybody outside it is served every version without
233
+ those files: the file list, zips, git clone, Pages and search leave them out.
234
+ Members see everything. Only the owner or an admin can change the list.
235
+
236
+ ### Signed releases and dependencies
237
+
238
+ ```
239
+ cbx release v2.0 --draft --generate-notes nothing public until: cbx release publish --version 41
240
+ cbx release v2.1.0-beta.1 --prerelease --channel beta
241
+ cbx verify sam/editor v2.0.0 ./setup.exe check a download against the signed manifest
242
+ cbx deps known vulnerabilities in the lockfiles (OSV)
243
+ cbx pr merge M-3 --squash
244
+ cbx pr auto-merge M-3
245
+ ```
246
+
202
247
  ## Who can reach a project
203
248
 
204
249
  cbx visibility # what it is now, and what each level means
@@ -436,6 +481,56 @@ artifacts or with `cbx attach <file>`, so a release made by pushing a tag
436
481
  simply has none until something adds them — which is what a release without a
437
482
  build has always looked like.
438
483
 
484
+ ## Actions
485
+
486
+ ```
487
+ cbx runs what has run, and how it went
488
+ cbx logs 12 --follow watch a run as it happens
489
+ cbx cancel 12 stop a run that is waiting or working
490
+ cbx runner run this project's work on this machine
491
+ ```
492
+
493
+ CodeRook's own runners give every account 1,000 minutes a month; your own
494
+ machine has no limit. A run nobody picks up within a day is ended with the
495
+ reason.
496
+
497
+ ### More from Actions
498
+
499
+ ```
500
+ cbx rerun 12 run it again, against the same save
501
+ cbx approve 14 a run waiting for approval
502
+ cbx actions check check .rook/workflows/*.yml before saving
503
+ cbx actions set Nightly --schedule "0 3 * * *" --timeout 30
504
+ cbx actions set Deploy --approval on --concurrency deploy --cancel-old
505
+ cbx secret set DEPLOY_TOKEN asked for at a hidden prompt; never shown again
506
+ cbx secret list
507
+ ```
508
+
509
+ Every run gets `CODEROOK_TOKEN`, `CODEROOK_API` and `CODEROOK_PROJECT`, so
510
+ `cbx issue comment`, `cbx pr label` or `cbx release` inside a workflow need no
511
+ setup. Secrets reach only runs started by people who can change the project.
512
+
513
+ ## Issues
514
+
515
+ ```
516
+ cbx issue view 12
517
+ cbx issue comment 12 -m "Fixed in v41"
518
+ cbx issue close 12 --duplicate-of 9 or --reason not-planned
519
+ cbx issue label 12 bug,ui --remove to take them off
520
+ cbx issue assign 12 sam or none
521
+ cbx issue labels --starter
522
+ ```
523
+
524
+ ## Forks and security reports
525
+
526
+ ```
527
+ cbx fork sam/editor your own copy
528
+ cbx fork offer fix-export --title "…" offer a line back as a pull request
529
+ cbx fork sync the original's latest, on "upstream"
530
+ cbx security report sam/editor --title "…" --severity high
531
+ cbx security list | view S-3 | reply S-3 -m "…" | mark S-3 fixed
532
+ ```
533
+
439
534
  ## Releases and app downloads
440
535
 
441
536
  ```
@@ -452,6 +547,10 @@ no need for a second project that only holds releases. `--downloads private`
452
547
  takes them back. Without a release name it changes an existing release and
453
548
  leaves its notes alone.
454
549
 
550
+ Every attached file is scanned for malware before anybody outside the project
551
+ can download it, usually within a minute; one that fails is never offered, and
552
+ whoever attached it and the owner are told what was found.
553
+
455
554
  `cbx releases` marks each one `public`, `downloads public` or `private`, and
456
555
  for a public project prints the addresses an app or a script reads, which
457
556
  need no account:
@@ -0,0 +1,254 @@
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.commandSecret = commandSecret;
7
+ exports.commandCheckWorkflows = commandCheckWorkflows;
8
+ exports.commandSetWorkflow = commandSetWorkflow;
9
+ exports.commandApprove = commandApprove;
10
+ /**
11
+ * The newer parts of Actions from the terminal:
12
+ *
13
+ * cbx secret list | set NAME | remove NAME a project's secrets
14
+ * cbx actions check [files…] check workflow files before saving them
15
+ * cbx actions set <action> --schedule … --timeout … --approval on|off --concurrency …
16
+ * cbx approve <run> let a run that waits for approval start
17
+ *
18
+ * A secret's value is never printed and never taken as an argument (it would
19
+ * sit in shell history): it is read from standard input, from another
20
+ * environment variable with --from-env, or typed at a hidden prompt.
21
+ */
22
+ const promises_1 = require("node:fs/promises");
23
+ const node_path_1 = __importDefault(require("node:path"));
24
+ const api_js_1 = require("./api.js");
25
+ const colour_js_1 = require("./colour.js");
26
+ const project_commands_js_1 = require("./project_commands.js");
27
+ function projectFlag(parsed) {
28
+ const named = parsed.flags.get("project");
29
+ return typeof named === "string" ? named : undefined;
30
+ }
31
+ /** Read a secret without it appearing anywhere: piped in, from another variable, or typed hidden. */
32
+ async function readSecretValue(name, parsed) {
33
+ const fromEnv = parsed.flags.get("from-env");
34
+ if (typeof fromEnv === "string" && fromEnv.trim()) {
35
+ const value = process.env[fromEnv.trim()];
36
+ if (!value) {
37
+ console.error((0, colour_js_1.red)(`The environment variable ${fromEnv.trim()} is empty or not set.`));
38
+ return null;
39
+ }
40
+ return value;
41
+ }
42
+ if (!process.stdin.isTTY) {
43
+ const chunks = [];
44
+ for await (const chunk of process.stdin)
45
+ chunks.push(Buffer.from(chunk));
46
+ return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
47
+ }
48
+ process.stdout.write(`Value for ${name} (hidden; Enter to finish): `);
49
+ return new Promise((resolve) => {
50
+ const input = process.stdin;
51
+ let value = "";
52
+ input.setRawMode(true);
53
+ input.resume();
54
+ input.setEncoding("utf8");
55
+ const done = (result) => {
56
+ input.setRawMode(false);
57
+ input.pause();
58
+ input.removeListener("data", onData);
59
+ process.stdout.write("\n");
60
+ resolve(result);
61
+ };
62
+ const onData = (key) => {
63
+ for (const character of key) {
64
+ if (character === "\r" || character === "\n")
65
+ return done(value);
66
+ if (character === "\u0003")
67
+ return done(null); // Ctrl+C
68
+ if (character === "\u007f" || character === "\b")
69
+ value = value.slice(0, -1);
70
+ else
71
+ value += character;
72
+ }
73
+ };
74
+ input.on("data", onData);
75
+ });
76
+ }
77
+ async function commandSecret(parsed) {
78
+ const sub = parsed.positional[0] ?? "list";
79
+ const project = await (0, project_commands_js_1.resolveProject)(projectFlag(parsed));
80
+ if (!project)
81
+ return 1;
82
+ const base = `/v1/repositories/${encodeURIComponent(project.id)}/secrets`;
83
+ if (sub === "list") {
84
+ const { secrets } = await (0, api_js_1.callApi)(base);
85
+ if (!secrets.length) {
86
+ console.log(`${(0, colour_js_1.bold)(project.slug)} has no secrets.`);
87
+ console.log((0, colour_js_1.dim)(" Add one: cbx secret set DEPLOY_TOKEN (you are asked for the value; it is never shown)"));
88
+ return 0;
89
+ }
90
+ console.log((0, colour_js_1.bold)(`Secrets in ${project.slug}`) + (0, colour_js_1.dim)(" — values are never shown"));
91
+ for (const secret of secrets) {
92
+ console.log(` ${(0, colour_js_1.accent)(secret.name.padEnd(28))} ${(0, colour_js_1.dim)(`set ${new Date(secret.updatedAt).toLocaleString()}${secret.updatedBy ? ` by ${secret.updatedBy}` : ""}`)}`);
93
+ }
94
+ return 0;
95
+ }
96
+ const name = (parsed.positional[1] ?? "").trim().toUpperCase();
97
+ if (!name) {
98
+ console.error((0, colour_js_1.red)(`Name the secret: cbx secret ${sub} DEPLOY_TOKEN`));
99
+ return 1;
100
+ }
101
+ if (sub === "set") {
102
+ const value = await readSecretValue(name, parsed);
103
+ if (value === null)
104
+ return 1;
105
+ if (!value) {
106
+ console.error((0, colour_js_1.red)("Nothing was entered, so nothing was saved."));
107
+ return 1;
108
+ }
109
+ await (0, api_js_1.callApi)(`${base}/${encodeURIComponent(name)}`, { method: "PUT", body: { value } });
110
+ console.log(`${(0, colour_js_1.green)("Saved.")} Runs started by people who can change ${project.slug} get ${(0, colour_js_1.accent)(name)} as an environment variable.`);
111
+ console.log((0, colour_js_1.dim)(" It is replaced by *** in run logs. A run from outside the project never gets it."));
112
+ return 0;
113
+ }
114
+ if (sub === "remove") {
115
+ await (0, api_js_1.callApi)(`${base}/${encodeURIComponent(name)}`, { method: "DELETE" });
116
+ console.log(`${(0, colour_js_1.green)("Removed")} ${name}.`);
117
+ return 0;
118
+ }
119
+ console.error((0, colour_js_1.red)(`cbx secret has no "${sub}". `) + "Try: list, set, remove");
120
+ return 1;
121
+ }
122
+ const EVENT_WORDS = {
123
+ manual: "by hand",
124
+ version_saved: "on save",
125
+ merge_opened: "on pull requests",
126
+ merge_applied: "when merged",
127
+ issue_opened: "issue opened",
128
+ issue_labeled: "issue labelled",
129
+ issue_commented: "issue comment",
130
+ release_published: "on release",
131
+ schedule: "on a schedule",
132
+ };
133
+ /** `cbx actions check [files…]`: what each file would make, and what is wrong with it. */
134
+ async function commandCheckWorkflows(parsed) {
135
+ let files = parsed.positional.slice(1);
136
+ if (!files.length) {
137
+ const folder = node_path_1.default.join(process.cwd(), ".rook", "workflows");
138
+ try {
139
+ files = (await (0, promises_1.readdir)(folder)).filter((name) => /\.ya?ml$/.test(name)).map((name) => node_path_1.default.join(folder, name));
140
+ }
141
+ catch {
142
+ files = [];
143
+ }
144
+ }
145
+ if (!files.length) {
146
+ console.log("No workflow files here. They live in .rook/workflows/*.yml");
147
+ console.log((0, colour_js_1.dim)(" See: https://coderook.com/docs#workflow-files"));
148
+ return 0;
149
+ }
150
+ const project = await (0, project_commands_js_1.resolveProject)(projectFlag(parsed));
151
+ if (!project)
152
+ return 1;
153
+ let failed = false;
154
+ for (const file of files) {
155
+ const relative = node_path_1.default.relative(process.cwd(), file).split(node_path_1.default.sep).join("/");
156
+ const text = await (0, promises_1.readFile)(file, "utf8");
157
+ const answer = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/workflow-files/check`, {
158
+ method: "POST",
159
+ body: { path: relative.startsWith(".rook/") ? relative : `.rook/workflows/${node_path_1.default.basename(file)}`, text },
160
+ });
161
+ console.log(answer.ok ? `${(0, colour_js_1.green)("✓")} ${(0, colour_js_1.bold)(relative)}` : `${(0, colour_js_1.red)("×")} ${(0, colour_js_1.bold)(relative)}`);
162
+ for (const error of answer.errors)
163
+ console.log(` ${(0, colour_js_1.red)(error)}`);
164
+ for (const workflow of answer.workflows) {
165
+ const details = [
166
+ workflow.on.map((event) => EVENT_WORDS[event] ?? event).join(", "),
167
+ workflow.schedule ? `"${workflow.schedule}" UTC` : null,
168
+ workflow.runsOn !== "hosted" ? `on ${workflow.runsOn}` : null,
169
+ workflow.matrix ? `${Object.values(workflow.matrix).reduce((all, values) => all * values.length, 1)} runs (matrix)` : null,
170
+ workflow.needs.length ? `after ${workflow.needs.join(", ")}` : null,
171
+ workflow.needsApproval ? "needs approval" : null,
172
+ workflow.timeoutMinutes ? `${workflow.timeoutMinutes} min limit` : null,
173
+ ].filter(Boolean);
174
+ console.log(` ${(0, colour_js_1.accent)(workflow.name)} ${(0, colour_js_1.dim)(details.join(" · "))}`);
175
+ }
176
+ failed ||= !answer.ok;
177
+ }
178
+ if (!failed)
179
+ console.log((0, colour_js_1.dim)("\nSave them to the project's main line and they take effect."));
180
+ return failed ? 1 : 0;
181
+ }
182
+ /** `cbx actions set <action> …`: the options a workflow made on the website can have. */
183
+ async function commandSetWorkflow(parsed) {
184
+ const named = (parsed.positional[1] ?? "").trim();
185
+ if (!named) {
186
+ console.error((0, colour_js_1.red)('Which action? cbx actions set "Nightly build" --schedule "0 3 * * *"'));
187
+ return 1;
188
+ }
189
+ const project = await (0, project_commands_js_1.resolveProject)(projectFlag(parsed));
190
+ if (!project)
191
+ return 1;
192
+ const target = (await (0, api_js_1.workflows)(project.id)).find((one) => one.name.toLowerCase() === named.toLowerCase());
193
+ if (!target) {
194
+ console.error((0, colour_js_1.red)(`${project.name} has no action called ${named}.`));
195
+ return 1;
196
+ }
197
+ const change = {};
198
+ const schedule = parsed.flags.get("schedule");
199
+ if (typeof schedule === "string") {
200
+ change.schedule = schedule === "off" ? null : schedule;
201
+ if (schedule !== "off")
202
+ change.trigger = "schedule";
203
+ }
204
+ const timeout = parsed.flags.get("timeout");
205
+ if (typeof timeout === "string")
206
+ change.timeoutMinutes = timeout === "off" ? null : Number(timeout);
207
+ const approval = parsed.flags.get("approval");
208
+ if (typeof approval === "string")
209
+ change.needsApproval = approval === "on";
210
+ const concurrency = parsed.flags.get("concurrency");
211
+ if (typeof concurrency === "string")
212
+ change.concurrencyGroup = concurrency === "off" ? null : concurrency;
213
+ if (parsed.flags.has("cancel-old"))
214
+ change.cancelInProgress = true;
215
+ if (parsed.flags.has("keep-old"))
216
+ change.cancelInProgress = false;
217
+ if (!Object.keys(change).length) {
218
+ console.error((0, colour_js_1.red)("Nothing to change. ") + (0, colour_js_1.dim)("Options: --schedule, --timeout, --approval on|off, --concurrency, --cancel-old"));
219
+ return 1;
220
+ }
221
+ await (0, api_js_1.changeWorkflow)(project.id, target.id, change);
222
+ console.log(`${(0, colour_js_1.green)("Saved.")} ${target.name}:`);
223
+ if ("schedule" in change)
224
+ console.log(` ${change.schedule ? `runs on "${change.schedule}" (UTC)` : "no schedule"}`);
225
+ if ("timeoutMinutes" in change)
226
+ console.log(` ${change.timeoutMinutes ? `stopped after ${change.timeoutMinutes} minutes` : "no time limit of its own"}`);
227
+ if ("needsApproval" in change)
228
+ console.log(` ${change.needsApproval ? "each run waits for approval (cbx approve)" : "runs start without approval"}`);
229
+ if ("concurrencyGroup" in change)
230
+ console.log(` ${change.concurrencyGroup ? `one at a time in "${change.concurrencyGroup}"` : "no concurrency group"}`);
231
+ if ("cancelInProgress" in change)
232
+ console.log(` ${change.cancelInProgress ? "a new run cancels the group's older ones" : "a new run waits its turn"}`);
233
+ return 0;
234
+ }
235
+ /** `cbx approve <run>`: start a run that is waiting for approval. */
236
+ async function commandApprove(parsed) {
237
+ const asked = parsed.positional[0];
238
+ const number = Number((asked ?? "").replace(/^#/, ""));
239
+ if (!asked || !Number.isInteger(number) || number < 1) {
240
+ console.error((0, colour_js_1.red)("Which run? Try: cbx approve 12"));
241
+ return 1;
242
+ }
243
+ const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[1] ?? projectFlag(parsed));
244
+ if (!project)
245
+ return 1;
246
+ const run = (await (0, api_js_1.runs)(project.id)).find((one) => one.number === number);
247
+ if (!run) {
248
+ console.error((0, colour_js_1.red)(`${project.name} has no run #${number}.`));
249
+ return 1;
250
+ }
251
+ await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/runs/${encodeURIComponent(run.id)}/approve`, { method: "POST" });
252
+ console.log(`${(0, colour_js_1.accent)(`#${number}`)} approved. It is queued and starts when a runner takes it.`);
253
+ return 0;
254
+ }
@@ -43,8 +43,13 @@ exports.setWatch = setWatch;
43
43
  exports.workflows = workflows;
44
44
  exports.changeWorkflow = changeWorkflow;
45
45
  exports.runs = runs;
46
+ exports.rerunRun = rerunRun;
47
+ exports.cancelRun = cancelRun;
46
48
  exports.runLogs = runLogs;
47
49
  exports.tokens = tokens;
50
+ exports.getPrivatePaths = getPrivatePaths;
51
+ exports.putPrivatePaths = putPrivatePaths;
52
+ exports.callApi = callApi;
48
53
  exports.revokeToken = revokeToken;
49
54
  exports.deleteProject = deleteProject;
50
55
  exports.projectLabels = projectLabels;
@@ -330,7 +335,10 @@ async function findProject(reference) {
330
335
  return [];
331
336
  throw error;
332
337
  });
333
- const ownersMatch = (project) => project.slug.toLowerCase() === bare || project.name.toLowerCase() === bare;
338
+ const ownersMatch = (project) => project.slug.toLowerCase() === bare ||
339
+ project.name.toLowerCase() === bare ||
340
+ /* By id too: a run is handed its project's id as CODEROOK_PROJECT. */
341
+ project.id.toLowerCase() === trimmed.toLowerCase();
334
342
  if (!owner) {
335
343
  const mine = all.find(ownersMatch);
336
344
  if (mine)
@@ -618,12 +626,12 @@ async function versions(repositoryId) {
618
626
  * — pushing the same tag twice renames the same version to the same thing
619
627
  * rather than making a second release.
620
628
  */
621
- async function markRelease(repositoryId, versionId, name, notes) {
629
+ async function markRelease(repositoryId, versionId, name, notes, options = {}) {
622
630
  const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}` +
623
631
  `/versions/${encodeURIComponent(versionId)}/release`,
624
632
  // `call` serialises the body itself; handing it a string would send a
625
633
  // JSON-encoded string where the service expects an object.
626
- { method: "PUT", body: { name, ...(notes ? { notes } : {}) } });
634
+ { method: "PUT", body: { name, ...(notes ? { notes } : {}), ...options } });
627
635
  return {
628
636
  name: String(body.release?.name ?? name),
629
637
  versionId: String(body.release?.versionId ?? versionId),
@@ -790,6 +798,14 @@ async function runs(repositoryId, filter = {}) {
790
798
  };
791
799
  });
792
800
  }
801
+ /** Stop a run that is waiting or working. */
802
+ /** Start a finished run again: same action, same save, same pull request. */
803
+ async function rerunRun(repositoryId, runId) {
804
+ return call(`/v1/repositories/${encodeURIComponent(repositoryId)}/runs/${encodeURIComponent(runId)}/rerun`, { method: "POST" });
805
+ }
806
+ async function cancelRun(repositoryId, runId) {
807
+ await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/runs/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
808
+ }
793
809
  /** Everything a run printed, in the order it printed it. */
794
810
  async function runLogs(repositoryId, runId) {
795
811
  const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/runs/` +
@@ -806,6 +822,23 @@ async function tokens() {
806
822
  createdAt: String(row.createdAt ?? ""),
807
823
  }));
808
824
  }
825
+ /** The project's private paths: patterns only its own people see (members only). */
826
+ async function getPrivatePaths(repositoryId) {
827
+ const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/private-paths`);
828
+ return body.patterns ?? [];
829
+ }
830
+ /** Replace them. The project's owner or an admin only. */
831
+ async function putPrivatePaths(repositoryId, patterns) {
832
+ const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/private-paths`, {
833
+ method: "PUT",
834
+ body: { patterns },
835
+ });
836
+ return body.patterns ?? [];
837
+ }
838
+ /** A signed-in request to any route, for commands with many of them (pr_command.ts). */
839
+ async function callApi(route, options = {}) {
840
+ return call(route, options);
841
+ }
809
842
  async function revokeToken(tokenId) {
810
843
  await call(`/v1/account/tokens/${encodeURIComponent(tokenId)}`, {
811
844
  method: "DELETE",
@@ -832,6 +832,14 @@ async function commandBuild(parsed) {
832
832
  const host = thisHost();
833
833
  console.log(dim(`Checking what this ${host.os} ${host.arch} machine can build with…`));
834
834
  const tools = await detectTools(host);
835
+ /*
836
+ Docker only when asked for. Every target is reached natively, by
837
+ cross-compiling or through WSL where it can be; a container is the last
838
+ resort somebody chooses with --docker, never one picked for them because
839
+ Docker Desktop happened to be running.
840
+ */
841
+ if (!parsed.flags.has("docker"))
842
+ tools.docker = false;
835
843
  const plan = planBuild(project, host, tools, targets);
836
844
  console.log(`${bold(project.name)} ${dim(`(${project.preset})`)}`);
837
845
  const width = Math.max(...plan.map((one) => one.target.length));