@coderook/cli 0.39.0 → 0.41.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(/^#/, ""));
@@ -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", "features", "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();
@@ -308,6 +308,56 @@ async function commandCollaborators(parsed) {
308
308
  const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[0]);
309
309
  if (!project)
310
310
  return 1;
311
+ /*
312
+ Changing a role, taking somebody off, and withdrawing an invite: the rest
313
+ of the website's Collaborators screen. Each names the person by email or
314
+ display name, which is what the listing below shows.
315
+ */
316
+ const changing = parsed.flags.get("change");
317
+ const removing = parsed.flags.get("remove");
318
+ const cancelling = parsed.flags.get("cancel-invite");
319
+ if (changing !== undefined || removing !== undefined || cancelling !== undefined) {
320
+ const wanted = changing ?? removing ?? cancelling;
321
+ if (typeof wanted !== "string" || !wanted.trim()) {
322
+ console.error((0, colour_js_1.red)("Who? Name them by email address or display name."));
323
+ return 1;
324
+ }
325
+ const who = wanted.trim().toLowerCase();
326
+ const people = await (0, api_js_1.collaborators)(project.id);
327
+ if (cancelling !== undefined) {
328
+ const pending = people.invites.find((one) => one.email.toLowerCase() === who || one.id === wanted.trim());
329
+ if (!pending) {
330
+ console.error((0, colour_js_1.red)(`No invite waiting for ${wanted.trim()} on ${project.name}.`));
331
+ return 1;
332
+ }
333
+ await (0, api_js_1.revokeInvite)(project.id, pending.id);
334
+ console.log((0, colour_js_1.green)(`Withdrew the invite to ${pending.email}. Its link no longer works.`));
335
+ return 0;
336
+ }
337
+ const person = people.collaborators.find((one) => one.email.toLowerCase() === who ||
338
+ one.displayName.toLowerCase() === who ||
339
+ one.userId === wanted.trim());
340
+ if (!person) {
341
+ console.error((0, colour_js_1.red)(`${wanted.trim()} is not one of ${project.name}'s people.`));
342
+ console.error((0, colour_js_1.dim)(` See who is: cbx people ${project.slug}`));
343
+ return 1;
344
+ }
345
+ const label = person.displayName || person.email;
346
+ if (removing !== undefined) {
347
+ await (0, api_js_1.removeCollaborator)(project.id, person.userId);
348
+ console.log((0, colour_js_1.green)(`${label} is no longer on ${project.name}.`));
349
+ return 0;
350
+ }
351
+ const flag = parsed.flags.get("role");
352
+ const role = typeof flag === "string" ? flag.trim().toLowerCase() : "";
353
+ if (!["write", "read", "admin"].includes(role)) {
354
+ console.error((0, colour_js_1.red)(role ? `"${role}" is not a role. Use write, read or admin.` : "Which role? Add --role write, read or admin."));
355
+ return 1;
356
+ }
357
+ await (0, api_js_1.setCollaboratorRole)(project.id, person.userId, role);
358
+ console.log((0, colour_js_1.green)(`${label} now has ${role} access to ${project.name}.`));
359
+ return 0;
360
+ }
311
361
  const email = parsed.flags.get("invite");
312
362
  if (email === true) {
313
363
  console.error((0, colour_js_1.red)("Whose address? Try: --invite someone@example.com"));
@@ -807,6 +857,76 @@ async function commandHooks(parsed) {
807
857
  }
808
858
  return 0;
809
859
  }
860
+ /*
861
+ Everything else the website's Webhooks card does to one hook: send a test,
862
+ read what was delivered, send one again, change its events, switch it back
863
+ on. Each names the hook by id or address, as --remove does.
864
+ */
865
+ const tested = parsed.flags.get("test");
866
+ const history = parsed.flags.get("deliveries");
867
+ const resend = parsed.flags.get("redeliver");
868
+ const resume = parsed.flags.get("on");
869
+ const named = parsed.flags.get("hook");
870
+ const changingEvents = parsed.flags.has("events");
871
+ if (tested !== undefined || history !== undefined || resend !== undefined || resume !== undefined || changingEvents) {
872
+ const wanted = [tested, history, resume, named].find((value) => typeof value === "string" && value.trim());
873
+ const hook = await whichHook(project.id, project.name, typeof wanted === "string" ? wanted.trim() : null);
874
+ if (!hook)
875
+ return 1;
876
+ if (tested !== undefined) {
877
+ const delivery = await (0, api_js_1.testWebhook)(project.id, hook.id);
878
+ console.log(`Sent a test to ${hook.url}.`);
879
+ console.log(` ${deliveryLine(delivery)}`);
880
+ return delivery.state === "delivered" ? 0 : 1;
881
+ }
882
+ if (history !== undefined) {
883
+ const found = await (0, api_js_1.webhookDeliveries)(project.id, hook.id);
884
+ if (!found.length) {
885
+ console.log((0, colour_js_1.dim)(`Nothing has been sent to ${hook.url} yet. Send a test: cbx hooks --test ${hook.id}`));
886
+ return 0;
887
+ }
888
+ console.log((0, colour_js_1.bold)(hook.url));
889
+ for (const delivery of found)
890
+ console.log(` ${deliveryLine(delivery)}`);
891
+ console.log("");
892
+ console.log((0, colour_js_1.dim)(` Send one again: cbx hooks --redeliver <delivery> --hook ${hook.id}`));
893
+ return 0;
894
+ }
895
+ if (resend !== undefined) {
896
+ if (typeof resend !== "string" || !resend.trim()) {
897
+ console.error((0, colour_js_1.red)("Which delivery?") + (0, colour_js_1.dim)(` cbx hooks --deliveries ${hook.id} lists them`));
898
+ return 1;
899
+ }
900
+ /* The start of an id is enough, as long as it picks out one. */
901
+ const prefix = resend.trim().toLowerCase();
902
+ const matches = (await (0, api_js_1.webhookDeliveries)(project.id, hook.id)).filter((one) => one.id.toLowerCase().startsWith(prefix));
903
+ if (matches.length !== 1) {
904
+ console.error((0, colour_js_1.red)(matches.length ? `${resend.trim()} matches more than one delivery; give more of its id.` : `No recent delivery ${resend.trim()} for ${hook.url}.`));
905
+ return 1;
906
+ }
907
+ const delivery = await (0, api_js_1.redeliverWebhook)(project.id, hook.id, matches[0].id);
908
+ console.log(`Sent ${matches[0].event} to ${hook.url} again.`);
909
+ console.log(` ${deliveryLine(delivery)}`);
910
+ return delivery.state === "delivered" ? 0 : 1;
911
+ }
912
+ if (changingEvents) {
913
+ const value = parsed.flags.get("events");
914
+ const text = typeof value === "string" ? value.trim() : "";
915
+ const events = text === "all" ? [] : text.split(",").map((one) => one.trim()).filter(Boolean);
916
+ if (!events.length && text !== "all") {
917
+ console.error((0, colour_js_1.red)("Which events? A comma separated list, or all.") + (0, colour_js_1.dim)(" cbx hooks --hook <id> --events version.published"));
918
+ return 1;
919
+ }
920
+ await (0, api_js_1.updateWebhook)(project.id, hook.id, { events });
921
+ }
922
+ if (resume !== undefined) {
923
+ await (0, api_js_1.updateWebhook)(project.id, hook.id, { active: true });
924
+ }
925
+ const after = (await (0, api_js_1.webhooks)(project.id)).find((one) => one.id === hook.id) ?? hook;
926
+ console.log((0, colour_js_1.green)(`Updated ${hook.url}.`));
927
+ console.log(` ${after.active ? (0, colour_js_1.green)("on") : (0, colour_js_1.red)("off")} ${(0, colour_js_1.dim)(after.events.length ? after.events.join(", ") : "everything")}`);
928
+ return 0;
929
+ }
810
930
  const removing = parsed.flags.get("remove");
811
931
  if (removing !== undefined) {
812
932
  if (typeof removing !== "string" || !removing.trim()) {
@@ -847,5 +967,39 @@ async function commandHooks(parsed) {
847
967
  console.log(` ${(0, colour_js_1.dim)(hook.id)} ${health}` +
848
968
  (hook.events.length ? ` ${(0, colour_js_1.dim)(hook.events.join(", "))}` : ` ${(0, colour_js_1.dim)("everything")}`));
849
969
  }
970
+ if (found.some((hook) => !hook.active)) {
971
+ console.log("");
972
+ console.log((0, colour_js_1.dim)(" Switch one back on: cbx hooks --on <id>"));
973
+ }
850
974
  return 0;
851
975
  }
976
+ /**
977
+ * The hook a command is about: named by id or address, or the only one.
978
+ *
979
+ * Naming none is fine when there is exactly one, which is the usual case and
980
+ * the one where asking "which?" would be a question with one answer.
981
+ */
982
+ async function whichHook(projectId, projectName, wanted) {
983
+ const found = await (0, api_js_1.webhooks)(projectId);
984
+ if (!wanted) {
985
+ if (found.length === 1)
986
+ return found[0];
987
+ console.error((0, colour_js_1.red)(found.length ? `${projectName} has ${found.length} webhooks. Say which: --hook <id>` : `${projectName} notifies nothing.`));
988
+ return null;
989
+ }
990
+ const hook = found.find((one) => one.id === wanted || one.url === wanted);
991
+ if (!hook)
992
+ console.error((0, colour_js_1.red)(`${projectName} has no webhook called ${wanted}.`));
993
+ return hook ?? null;
994
+ }
995
+ /** One delivery on one line: when, what, how it went, and its id. */
996
+ function deliveryLine(delivery) {
997
+ const when = new Date(delivery.createdAt).toLocaleString();
998
+ const how = delivery.state === "delivered"
999
+ ? (0, colour_js_1.green)(`delivered${delivery.status ? ` (${delivery.status})` : ""}`)
1000
+ : delivery.state === "pending"
1001
+ ? (0, colour_js_1.dim)(`waiting, ${delivery.attempts} ${delivery.attempts === 1 ? "try" : "tries"} so far`)
1002
+ : (0, colour_js_1.red)(`${delivery.state}${delivery.status ? ` (${delivery.status})` : ""}`);
1003
+ const detail = delivery.detail && delivery.state !== "delivered" ? (0, colour_js_1.dim)(` ${delivery.detail}`) : "";
1004
+ return `${(0, colour_js_1.dim)(delivery.id.slice(0, 8))} ${when.padEnd(22)} ${delivery.event.padEnd(24)} ${how}${detail}`;
1005
+ }