@coderook/cli 0.39.0 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +24 -0
- package/dist/cli/src/api.js +48 -16
- package/dist/cli/src/cli.js +345 -122
- package/dist/cli/src/community_command.js +54 -1
- package/dist/cli/src/help.js +21 -6
- package/dist/cli/src/issue_command.js +171 -5
- package/dist/cli/src/project_commands.js +29 -26
- package/dist/cli/src/registry.js +32 -8
- package/dist/cli/src/service_commands.js +154 -0
- package/dist/cli/src/settings_commands.js +532 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/cli/src/help.js
CHANGED
|
@@ -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
|
-
/**
|
|
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.
|
|
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),
|
|
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
|
|
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.
|
|
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>
|
|
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
|
|
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)(
|
|
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
|
-
|
|
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
|
-
*
|
|
87
|
+
* The flags `cbx ai` used to take, named when somebody types one.
|
|
87
88
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
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 [
|
|
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}
|
|
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
|
|
124
|
-
console.log((0, colour_js_1.dim)("
|
|
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
|
/**
|
package/dist/cli/src/registry.js
CHANGED
|
@@ -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
|
-
"
|
|
21
|
+
"Saving work",
|
|
22
22
|
"History on this machine",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"Actions",
|
|
28
|
-
"
|
|
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();
|
|
@@ -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
|
+
}
|