@coderook/cli 0.38.2 → 0.40.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.
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.commandFork = commandFork;
4
7
  exports.commandSecurity = commandSecurity;
@@ -12,7 +15,10 @@ exports.commandSecurity = commandSecurity;
12
15
  *
13
16
  * cbx security report sam/editor --title "…" [--severity high] [--body "…"]
14
17
  * cbx security list | view S-3 | reply S-3 -m "…" | mark S-3 fixed
18
+ * cbx security policy [--on | --off] [--text <file or text>] [--clear]
15
19
  */
20
+ const node_fs_1 = require("node:fs");
21
+ const node_path_1 = __importDefault(require("node:path"));
16
22
  const api_js_1 = require("./api.js");
17
23
  const colour_js_1 = require("./colour.js");
18
24
  const project_commands_js_1 = require("./project_commands.js");
@@ -86,6 +92,8 @@ async function commandSecurity(parsed) {
86
92
  const project = await (0, project_commands_js_1.resolveProject)(flagText(parsed, "project"));
87
93
  if (!project)
88
94
  return 1;
95
+ if (sub === "policy")
96
+ return securityPolicy(parsed, project);
89
97
  const base = `/v1/repositories/${encodeURIComponent(project.id)}/security-reports`;
90
98
  if (sub === "list") {
91
99
  const { reports } = await (0, api_js_1.callApi)(base);
@@ -131,6 +139,51 @@ async function commandSecurity(parsed) {
131
139
  console.log((0, colour_js_1.green)(`S-${number} is ${state}. Whoever reported it is told.`));
132
140
  return 0;
133
141
  }
134
- console.error((0, colour_js_1.red)(`cbx security has no "${sub}". `) + "Try: report, list, view, reply, mark");
142
+ console.error((0, colour_js_1.red)(`cbx security has no "${sub}". `) + "Try: report, list, view, reply, mark, policy");
135
143
  return 1;
136
144
  }
145
+ /**
146
+ * `cbx security policy` — whether the project takes private reports, and what
147
+ * it tells people who want to send one. The website's Security section.
148
+ *
149
+ * --text takes a file when one by that name exists, because a policy is a
150
+ * page of prose and nobody wants to type it as an argument; anything else is
151
+ * used as the text itself.
152
+ */
153
+ async function securityPolicy(parsed, project) {
154
+ const route = `/v1/repositories/${encodeURIComponent(project.id)}/security-policy`;
155
+ const on = parsed.flags.has("on");
156
+ const off = parsed.flags.has("off");
157
+ if (on && off) {
158
+ console.error((0, colour_js_1.red)("Pick one: --on or --off."));
159
+ return 1;
160
+ }
161
+ const change = {};
162
+ if (on || off)
163
+ change.enabled = on;
164
+ if (parsed.flags.has("clear"))
165
+ change.policy = null;
166
+ const given = parsed.flags.get("text");
167
+ if (given !== undefined) {
168
+ if (typeof given !== "string" || !given.trim()) {
169
+ console.error((0, colour_js_1.red)("What should it say? --text SECURITY.md, or --text \"Email security@…\""));
170
+ return 1;
171
+ }
172
+ const file = node_path_1.default.resolve(given);
173
+ change.policy = (0, node_fs_1.existsSync)(file) && (0, node_fs_1.statSync)(file).isFile() ? (0, node_fs_1.readFileSync)(file, "utf8") : given;
174
+ }
175
+ if (Object.keys(change).length) {
176
+ await (0, api_js_1.callApi)(route, { method: "PUT", body: change });
177
+ console.log((0, colour_js_1.green)(`Saved ${project.name}'s security policy.`));
178
+ }
179
+ const policy = await (0, api_js_1.callApi)(route);
180
+ console.log(`${(0, colour_js_1.bold)(project.name)} ${policy.enabled ? (0, colour_js_1.green)("takes private vulnerability reports") : (0, colour_js_1.dim)("does not take private vulnerability reports")}`);
181
+ if (policy.policy?.trim()) {
182
+ console.log("");
183
+ console.log(policy.policy.trim());
184
+ }
185
+ else {
186
+ console.log((0, colour_js_1.dim)(" No reporting policy written. Add one: cbx security policy --text SECURITY.md"));
187
+ }
188
+ return 0;
189
+ }
@@ -15,33 +15,48 @@
15
15
  * afford whole sentences and examples.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.listedUsage = exports.USAGE_COLUMN = void 0;
18
19
  exports.renderHelp = renderHelp;
19
20
  exports.renderCommandHelp = renderCommandHelp;
20
21
  exports.renderUnknown = renderUnknown;
21
22
  const registry_js_1 = require("./registry.js");
22
23
  const colour_js_1 = require("./colour.js");
23
- /** Longest visible name, so the summary column lines up across every group. */
24
+ /** The widest the usage column may be; a longer usage needs a `brief`. */
25
+ exports.USAGE_COLUMN = 30;
26
+ /** What the command list shows in the usage column. */
27
+ const listedUsage = (spec) => spec.brief ?? spec.usage;
28
+ exports.listedUsage = listedUsage;
29
+ /** Longest visible usage, so the summary column lines up across every group. */
24
30
  function nameColumn(specs) {
25
- return specs.reduce((widest, spec) => Math.max(widest, spec.usage.length), 0);
31
+ return specs.reduce((widest, spec) => Math.max(widest, (0, exports.listedUsage)(spec).length), 0);
32
+ }
33
+ /** Where a command sits inside its group: by LISTING_ORDER, then as declared. */
34
+ function rank(spec, declared) {
35
+ const at = registry_js_1.LISTING_ORDER.indexOf(spec.name);
36
+ return at === -1 ? registry_js_1.LISTING_ORDER.length + declared : at;
26
37
  }
27
38
  function renderHelp(registry, version) {
28
39
  const listed = registry.specs.filter((spec) => !spec.deprecatedBy);
29
- const column = Math.min(nameColumn(listed), 34);
40
+ const column = Math.min(nameColumn(listed), exports.USAGE_COLUMN);
30
41
  const lines = [
31
42
  `${(0, colour_js_1.bold)("cbx")} ${(0, colour_js_1.dim)(version)} — the CodeBox engine, for CodeRook`,
32
43
  "",
33
44
  ];
34
45
  for (const group of registry_js_1.GROUP_ORDER) {
35
- const inGroup = listed.filter((spec) => spec.group === group);
46
+ const inGroup = listed
47
+ .map((spec, declared) => ({ spec, order: rank(spec, declared) }))
48
+ .filter(({ spec }) => spec.group === group)
49
+ .sort((a, b) => a.order - b.order)
50
+ .map(({ spec }) => spec);
36
51
  if (!inGroup.length)
37
52
  continue;
38
53
  lines.push((0, colour_js_1.bold)(group));
39
54
  for (const spec of inGroup) {
40
- lines.push(` ${spec.usage.padEnd(column)} ${spec.summary}`);
55
+ lines.push(` ${(0, exports.listedUsage)(spec).padEnd(column)} ${spec.summary}`);
41
56
  }
42
57
  lines.push("");
43
58
  }
44
- lines.push((0, colour_js_1.dim)("cbx help <command> what one command does, in full"), (0, colour_js_1.dim)("CODEROOK_TOKEN is used when set, so automated runs need nothing on disk."), (0, colour_js_1.dim)("CODEROOK_API_URL points at another service."));
59
+ lines.push((0, colour_js_1.dim)("cbx help <command> the full usage, options and examples for one command"), (0, colour_js_1.dim)("CODEROOK_TOKEN used instead of the stored sign-in, for automated runs"), (0, colour_js_1.dim)("CODEROOK_API_URL point at another service"));
45
60
  return lines.join("\n");
46
61
  }
47
62
  function renderCommandHelp(spec) {
@@ -11,7 +11,10 @@ exports.commandIssue = commandIssue;
11
11
  * cbx issue label 12 bug,ui [--remove]
12
12
  * cbx issue assign 12 sam | none
13
13
  * cbx issue labels [--starter] the project's labels
14
- * cbx issue rules the project's issue rules
14
+ * cbx issue labels add|change|remove <name> [--colour #hex] [--description …] [--name …]
15
+ * cbx issue rules the project's issue rules, numbered
16
+ * cbx issue rules add <auto-label|auto-assign|welcome|stale> [settings]
17
+ * cbx issue rules on|off|change|remove <n>
15
18
  *
16
19
  * Listing and opening issues stays with `cbx issues` and `cbx issues --new`.
17
20
  * Inside a run, CODEROOK_TOKEN and CODEROOK_PROJECT make these work with no
@@ -20,6 +23,155 @@ exports.commandIssue = commandIssue;
20
23
  const api_js_1 = require("./api.js");
21
24
  const colour_js_1 = require("./colour.js");
22
25
  const project_commands_js_1 = require("./project_commands.js");
26
+ const RULE_KINDS = ["auto_label", "auto_assign", "welcome", "stale"];
27
+ function flagValue(parsed, ...names) {
28
+ for (const name of names) {
29
+ const value = parsed.flags.get(name);
30
+ if (typeof value === "string")
31
+ return value;
32
+ }
33
+ return undefined;
34
+ }
35
+ /**
36
+ * `cbx issue labels add|change|remove <name>`. Answers 0 when the list should
37
+ * follow, as it does after a change.
38
+ */
39
+ async function changeLabel(parsed, base, action) {
40
+ const name = parsed.positional[2]?.trim();
41
+ if (!["add", "change", "remove"].includes(action)) {
42
+ console.error((0, colour_js_1.red)(`cbx issue labels has no "${action}". `) + "Try: add, change, remove");
43
+ return 1;
44
+ }
45
+ if (!name) {
46
+ console.error((0, colour_js_1.red)(`Which label? cbx issue labels ${action} bug`));
47
+ return 1;
48
+ }
49
+ const route = `${base}/issue-labels/${encodeURIComponent(name)}`;
50
+ if (action === "remove") {
51
+ await (0, api_js_1.callApi)(route, { method: "DELETE" });
52
+ console.log((0, colour_js_1.green)(`Removed the label ${name}. Issues that had it keep everything else.`));
53
+ return 0;
54
+ }
55
+ const { labels } = await (0, api_js_1.callApi)(`${base}/issue-labels`);
56
+ const exists = labels.some((label) => label.name === name);
57
+ /*
58
+ The service creates a label it does not have and changes one it does,
59
+ through the same request. Asked first here so a typo in `change` is not
60
+ quietly a new label, and `add` of an existing one is not a quiet edit.
61
+ */
62
+ if (action === "add" && exists) {
63
+ console.error((0, colour_js_1.red)(`There is already a label called ${name}. Change it: cbx issue labels change ${name} --colour d73a4a`));
64
+ return 1;
65
+ }
66
+ if (action === "change" && !exists) {
67
+ console.error((0, colour_js_1.red)(`No label called ${name}. Add it: cbx issue labels add ${name}`));
68
+ return 1;
69
+ }
70
+ const change = {};
71
+ const rename = flagValue(parsed, "name");
72
+ const colour = flagValue(parsed, "colour", "color");
73
+ const description = flagValue(parsed, "description");
74
+ if (rename !== undefined)
75
+ change.name = rename.trim();
76
+ if (colour !== undefined)
77
+ change.colour = colour.trim().startsWith("#") ? colour.trim() : `#${colour.trim()}`;
78
+ if (description !== undefined)
79
+ change.description = description.trim() || null;
80
+ if (action === "change" && !Object.keys(change).length) {
81
+ console.error((0, colour_js_1.red)("Change what? --name, --colour or --description"));
82
+ return 1;
83
+ }
84
+ await (0, api_js_1.callApi)(route, { method: "PUT", body: change });
85
+ console.log((0, colour_js_1.green)(action === "add" ? `Added the label ${name}.` : `Changed the label ${name}${change.name && change.name !== name ? ` (now ${change.name})` : ""}.`));
86
+ return 0;
87
+ }
88
+ /** A rule's settings from the flags, over what it had. */
89
+ function ruleConfig(parsed, kind, current) {
90
+ const config = { ...current };
91
+ const list = (value) => value.split(",").map((one) => one.trim()).filter(Boolean);
92
+ const words = flagValue(parsed, "words");
93
+ const label = flagValue(parsed, "label");
94
+ const where = flagValue(parsed, "in");
95
+ const assignees = flagValue(parsed, "assignees");
96
+ const message = flagValue(parsed, "m", "message");
97
+ const days = flagValue(parsed, "days");
98
+ const closeAfter = flagValue(parsed, "close-after");
99
+ const exempt = flagValue(parsed, "exempt");
100
+ if (kind === "auto_label") {
101
+ if (words !== undefined)
102
+ config.words = list(words);
103
+ if (label !== undefined)
104
+ config.label = label.trim();
105
+ if (where !== undefined)
106
+ config.in = where.trim();
107
+ }
108
+ else if (kind === "auto_assign") {
109
+ if (assignees !== undefined)
110
+ config.assignees = list(assignees).map((one) => one.replace(/^@/, ""));
111
+ if (label !== undefined)
112
+ config.label = label.trim() || null;
113
+ }
114
+ else if (kind === "welcome") {
115
+ if (message !== undefined)
116
+ config.message = message;
117
+ }
118
+ else {
119
+ if (days !== undefined)
120
+ config.days = Number(days);
121
+ if (closeAfter !== undefined)
122
+ config.closeAfterDays = Number(closeAfter);
123
+ if (label !== undefined)
124
+ config.label = label.trim();
125
+ if (message !== undefined)
126
+ config.message = message;
127
+ if (exempt !== undefined)
128
+ config.exemptLabels = list(exempt);
129
+ }
130
+ return config;
131
+ }
132
+ /**
133
+ * `cbx issue rules add <kind> …`, and `on|off|change|remove <n>`, where n is
134
+ * the number the list shows (or the rule's id). Answers 0 when the list
135
+ * should follow.
136
+ */
137
+ async function changeRule(parsed, base, action) {
138
+ if (action === "add") {
139
+ const kind = (parsed.positional[2] ?? "").replace("-", "_");
140
+ if (!RULE_KINDS.includes(kind)) {
141
+ console.error((0, colour_js_1.red)("Which kind? auto-label, auto-assign, welcome or stale."));
142
+ return 1;
143
+ }
144
+ await (0, api_js_1.callApi)(`${base}/issue-rules`, { method: "POST", body: { kind, config: ruleConfig(parsed, kind, {}) } });
145
+ console.log((0, colour_js_1.green)(`Added a ${kind.replace("_", "-")} rule.`));
146
+ return 0;
147
+ }
148
+ if (!["on", "off", "change", "remove"].includes(action)) {
149
+ console.error((0, colour_js_1.red)(`cbx issue rules has no "${action}". `) + "Try: add, on, off, change, remove");
150
+ return 1;
151
+ }
152
+ const wanted = (parsed.positional[2] ?? "").replace(/^#/, "").trim();
153
+ const { rules } = await (0, api_js_1.callApi)(`${base}/issue-rules`);
154
+ const index = /^\d+$/.test(wanted) ? Number(wanted) - 1 : -1;
155
+ const rule = rules[index] ?? rules.find((one) => one.id === wanted);
156
+ if (!rule) {
157
+ console.error((0, colour_js_1.red)(wanted ? `No rule ${wanted}. cbx issue rules lists them, numbered.` : `Which rule? cbx issue rules ${action} 2`));
158
+ return 1;
159
+ }
160
+ const route = `${base}/issue-rules/${encodeURIComponent(rule.id)}`;
161
+ if (action === "remove") {
162
+ await (0, api_js_1.callApi)(route, { method: "DELETE" });
163
+ console.log((0, colour_js_1.green)(`Removed the ${rule.kind.replace("_", "-")} rule.`));
164
+ return 0;
165
+ }
166
+ if (action === "change") {
167
+ await (0, api_js_1.callApi)(route, { method: "PATCH", body: { config: ruleConfig(parsed, rule.kind, rule.config) } });
168
+ }
169
+ else {
170
+ await (0, api_js_1.callApi)(route, { method: "PATCH", body: { enabled: action === "on" } });
171
+ }
172
+ console.log((0, colour_js_1.green)(action === "change" ? `Changed the ${rule.kind.replace("_", "-")} rule.` : `The ${rule.kind.replace("_", "-")} rule is ${action}.`));
173
+ return 0;
174
+ }
23
175
  function text(parsed) {
24
176
  const message = parsed.flags.get("m") ?? parsed.flags.get("message");
25
177
  return typeof message === "string" && message.trim() ? message : null;
@@ -58,6 +210,12 @@ async function commandIssue(parsed) {
58
210
  return 1;
59
211
  const base = `/v1/repositories/${encodeURIComponent(project.id)}`;
60
212
  if (sub === "labels") {
213
+ const action = parsed.positional[1];
214
+ if (action) {
215
+ const changed = await changeLabel(parsed, base, action);
216
+ if (changed !== 0)
217
+ return changed;
218
+ }
61
219
  if (parsed.flags.has("starter")) {
62
220
  const { added } = await (0, api_js_1.callApi)(`${base}/issue-labels/starter`, { method: "POST" });
63
221
  console.log((0, colour_js_1.green)(added ? `Added ${added} labels.` : "The usual labels are already there."));
@@ -73,14 +231,22 @@ async function commandIssue(parsed) {
73
231
  return 0;
74
232
  }
75
233
  if (sub === "rules") {
234
+ const action = parsed.positional[1];
235
+ if (action) {
236
+ const changed = await changeRule(parsed, base, action);
237
+ if (changed !== 0)
238
+ return changed;
239
+ }
76
240
  const { rules } = await (0, api_js_1.callApi)(`${base}/issue-rules`);
77
241
  if (!rules.length) {
78
- console.log(`${(0, colour_js_1.bold)(project.slug)} has no issue rules. ${(0, colour_js_1.dim)("Add them in the project's Settings, under Issue rules.")}`);
242
+ console.log(`${(0, colour_js_1.bold)(project.slug)} has no issue rules. ${(0, colour_js_1.dim)('Add one: cbx issue rules add welcome -m "Thanks for the report!"')}`);
79
243
  return 0;
80
244
  }
81
- for (const rule of rules) {
82
- console.log(` ${rule.enabled ? (0, colour_js_1.green)("on ") : (0, colour_js_1.dim)("off")} ${(0, colour_js_1.accent)(rule.kind.padEnd(12))} ${(0, colour_js_1.dim)(JSON.stringify(rule.config))}`);
83
- }
245
+ rules.forEach((rule, index) => {
246
+ console.log(` ${(0, colour_js_1.accent)(String(index + 1).padStart(2))} ${rule.enabled ? (0, colour_js_1.green)("on ") : (0, colour_js_1.dim)("off")} ${(0, colour_js_1.accent)(rule.kind.padEnd(12))} ${(0, colour_js_1.dim)(JSON.stringify(rule.config))}`);
247
+ });
248
+ if (!action)
249
+ console.log((0, colour_js_1.dim)("\n Change one by its number: cbx issue rules off 2, cbx issue rules remove 2"));
84
250
  return 0;
85
251
  }
86
252
  const number = Number((parsed.positional[1] ?? "").replace(/^#/, ""));
@@ -0,0 +1,80 @@
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.machineIdentity = machineIdentity;
7
+ exports.checkIn = checkIn;
8
+ /**
9
+ * This machine, as a project's Actions page shows it (backend machines.ts).
10
+ *
11
+ * Each machine gets a random id the first time cbx runs there, kept in its
12
+ * settings folder, and is named after the computer. When cbx works in a
13
+ * folder linked to a project, or on a draft handed to this machine, it says
14
+ * so: which version it holds, which line it saves to, how many files it has
15
+ * not saved. Only the folder's name goes, never its path.
16
+ *
17
+ * Saying so is best effort. It never delays a command by more than a moment,
18
+ * never fails one, and is skipped when CODEROOK_NO_PRESENCE is set.
19
+ */
20
+ const node_crypto_1 = require("node:crypto");
21
+ const promises_1 = require("node:fs/promises");
22
+ const node_os_1 = require("node:os");
23
+ const node_path_1 = __importDefault(require("node:path"));
24
+ const identify_js_1 = require("../../cbx/src/core/identify.js");
25
+ const config_js_1 = require("./config.js");
26
+ let cached = null;
27
+ /** This machine's id and name, made once and kept. */
28
+ async function machineIdentity() {
29
+ if (cached)
30
+ return cached;
31
+ const file = node_path_1.default.join((0, config_js_1.configDirectory)(), "machine.json");
32
+ let id = "";
33
+ try {
34
+ const saved = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
35
+ if (typeof saved.id === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(saved.id))
36
+ id = saved.id;
37
+ }
38
+ catch {
39
+ /* First run on this machine, or the file was removed. */
40
+ }
41
+ if (!id) {
42
+ id = (0, node_crypto_1.randomBytes)(12).toString("base64url");
43
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(file), { recursive: true }).catch(() => undefined);
44
+ await (0, promises_1.writeFile)(file, JSON.stringify({ id }, null, 2) + "\n").catch(() => undefined);
45
+ }
46
+ cached = { id, name: (process.env.CODEROOK_MACHINE_NAME || (0, node_os_1.hostname)() || "This machine").slice(0, 120) };
47
+ return cached;
48
+ }
49
+ /** Tell the project this machine is here. Never throws, and gives up after a moment. */
50
+ async function checkIn(repositoryId, state) {
51
+ if (!repositoryId || process.env.CODEROOK_NO_PRESENCE)
52
+ return;
53
+ try {
54
+ const token = await (0, config_js_1.loadToken)();
55
+ if (!token)
56
+ return;
57
+ const me = await machineIdentity();
58
+ const client = (0, identify_js_1.clientHeaders)()["x-coderook-client"] ?? "";
59
+ await fetch(`${(0, config_js_1.apiOrigin)()}/v1/repositories/${encodeURIComponent(repositoryId)}/machines`, {
60
+ method: "POST",
61
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...(0, identify_js_1.clientHeaders)() },
62
+ body: JSON.stringify({
63
+ machineId: me.id,
64
+ name: me.name,
65
+ kind: state.kind,
66
+ platform: `${(0, node_os_1.platform)()} ${(0, node_os_1.release)()}`.slice(0, 80),
67
+ clientVersion: client.startsWith("cli/") ? client.slice(4) : null,
68
+ folderName: state.folder ? node_path_1.default.basename(node_path_1.default.resolve(state.folder)) : null,
69
+ track: state.track ?? null,
70
+ baseVersionId: state.baseVersionId ?? null,
71
+ changedFiles: state.changedFiles ?? null,
72
+ workspaceId: state.workspaceId ?? null,
73
+ }),
74
+ signal: AbortSignal.timeout(2500),
75
+ });
76
+ }
77
+ catch {
78
+ /* Offline, signed out or slow: the command's own work is what matters. */
79
+ }
80
+ }
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.resolveProject = resolveProject;
16
+ exports.aiChangeAttempted = aiChangeAttempted;
16
17
  exports.commandAi = commandAi;
17
18
  exports.readLevel = readLevel;
18
19
  exports.splitVisibility = splitVisibility;
@@ -83,45 +84,47 @@ const AI_SWITCHES = [
83
84
  ],
84
85
  ];
85
86
  /**
86
- * Show or change what a project says about machines.
87
+ * The flags `cbx ai` used to take, named when somebody types one.
87
88
  *
88
- * The one part of the product with no command at all. Showing is the default
89
- * because most of the time the question is "what is this set to", and a
90
- * command that needs arguments to answer that is one people stop using.
89
+ * Returned rather than printed so the refusal can be checked without a
90
+ * service: it must happen before anything is asked of one.
91
+ */
92
+ function aiChangeAttempted(parsed) {
93
+ return AI_SWITCHES.map(([name]) => name).filter((name) => parsed.flags.has(name));
94
+ }
95
+ /**
96
+ * Show what a project says about machines.
97
+ *
98
+ * Read only. What AI may do with a project is changed by a person, on the
99
+ * website or in the desktop app, and the service refuses the change from any
100
+ * token — which is what this tool, the MCP servers, runs and assistants all
101
+ * hold. An assistant able to switch off the rule that keeps it out would make
102
+ * the rule decoration, so the old switches are refused here, plainly, before
103
+ * anything is sent.
91
104
  */
92
105
  async function commandAi(parsed) {
106
+ const attempted = aiChangeAttempted(parsed);
107
+ if (attempted.length) {
108
+ console.error((0, colour_js_1.red)(`AI access cannot be changed from the command line (${attempted.map((name) => "--" + name).join(", ")}).`));
109
+ console.error("A person changes it on the website, in the project's Settings → Access & AI, or in the desktop app.");
110
+ console.error((0, colour_js_1.dim)(" cbx ai shows what it is set to now."));
111
+ return 1;
112
+ }
93
113
  const project = await resolveProject(parsed.positional[0]);
94
114
  if (!project)
95
115
  return 1;
96
- const change = {};
97
- for (const [name, field] of AI_SWITCHES) {
98
- const value = parsed.flags.get(name);
99
- if (value === undefined)
100
- continue;
101
- /*
102
- `--read off` and `--read=false` both read naturally, and a bare `--read`
103
- means on, because that is what a flag without a value means everywhere
104
- else in this tool.
105
- */
106
- const text = typeof value === "string" ? value.trim().toLowerCase() : "";
107
- change[field] =
108
- value === true ? true : !["off", "false", "no", "n", "0"].includes(text);
109
- }
110
- if (Object.keys(change).length) {
111
- await (0, api_js_1.setAiAccess)(project.id, change);
112
- console.log((0, colour_js_1.green)(`Updated ${project.name}.`));
113
- }
114
116
  const access = await (0, api_js_1.aiAccess)(project.id);
115
117
  console.log("");
116
118
  console.log((0, colour_js_1.bold)(project.name));
117
- for (const [name, field, question, meaning] of AI_SWITCHES) {
119
+ for (const [, field, question, meaning] of AI_SWITCHES) {
118
120
  const mark = access[field] ? (0, colour_js_1.green)("allowed") : (0, colour_js_1.red)("refused");
119
- console.log(` ${question.padEnd(30)} ${mark} ${(0, colour_js_1.dim)("--" + name)}`);
121
+ console.log(` ${question.padEnd(30)} ${mark}`);
120
122
  console.log(` ${(0, colour_js_1.dim)(meaning)}`);
121
123
  }
122
124
  console.log("");
123
- console.log((0, colour_js_1.dim)("Everything is allowed until you say otherwise. Turning reading off is the"));
124
- console.log((0, colour_js_1.dim)("one that refuses model training."));
125
+ console.log((0, colour_js_1.dim)("Everything is allowed until somebody says otherwise. Reading off is the one"));
126
+ console.log((0, colour_js_1.dim)("that refuses model training."));
127
+ console.log((0, colour_js_1.dim)("Change these on the website (Settings → Access & AI) or in the desktop app."));
125
128
  return 0;
126
129
  }
127
130
  /**
@@ -12,20 +12,44 @@
12
12
  * command added without a summary is a type error rather than an omission.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.GROUP_ORDER = void 0;
15
+ exports.LISTING_ORDER = exports.GROUP_ORDER = void 0;
16
16
  exports.buildRegistry = buildRegistry;
17
17
  exports.nearestCommand = nearestCommand;
18
18
  /** The order groups appear in help, which is the order somebody meets them. */
19
19
  exports.GROUP_ORDER = [
20
20
  "Getting started",
21
- "Working with a folder",
21
+ "Saving work",
22
22
  "History on this machine",
23
- "Your projects",
24
- "People",
25
- "When somebody saved first",
26
- "Bundles",
27
- "Actions",
28
- "Other",
23
+ "Lines, merges and pull requests",
24
+ "Versions and releases",
25
+ "Project settings",
26
+ "Issues",
27
+ "Actions and builds",
28
+ "Workspaces",
29
+ "Mods and bundles",
30
+ "For assistants",
31
+ ];
32
+ /**
33
+ * The order commands appear inside their group in `cbx help`.
34
+ *
35
+ * Kept apart from where the commands are declared, which follows the history
36
+ * of the file rather than what somebody looks for first. A command missing
37
+ * from this list still appears, after the ones that are in it.
38
+ */
39
+ exports.LISTING_ORDER = [
40
+ "sign-in", "whoami", "sign-out", "doctor", "projects", "clone", "link", "unlink", "import", "transfer",
41
+ "status", "submit", "diff", "get", "track", "ignore", "licence",
42
+ "init", "save", "log", "restore", "switch", "push", "pull",
43
+ "tracks", "line-rules", "merges", "merge", "propose", "proposals", "pr",
44
+ "versions", "mark", "notes", "labels", "promote", "release", "releases", "attach", "held", "review", "undo",
45
+ "take-down", "verify", "deps",
46
+ "about", "visibility", "ai", "private", "upload-rules", "people", "watch", "hooks", "secret", "assistant",
47
+ "security", "pages", "fork", "hand-over", "delete",
48
+ "issues", "issue",
49
+ "actions", "runs", "logs", "cancel", "rerun", "approve", "runner", "build", "machines",
50
+ "workspace",
51
+ "mods", "bundle", "unbundle", "inspect",
52
+ "mcp", "skill", "tokens",
29
53
  ];
30
54
  function buildRegistry(specs) {
31
55
  const lookup = new Map();