@coderook/cli 0.43.0 → 0.45.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/dist/cli/src/api.js +2 -0
- package/dist/cli/src/cli.js +40 -20
- package/dist/cli/src/project_commands.js +10 -4
- package/dist/cli/src/registry.js +1 -1
- package/dist/cli/src/update_command.js +159 -0
- package/dist/cli/src/workspace_command.js +8 -0
- package/dist/cli/src/workspace_review.js +99 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "coderook",
|
|
3
3
|
"displayName": "CodeRook",
|
|
4
4
|
"description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, with cbx or plain git, from Claude Code.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.45.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/api.js
CHANGED
|
@@ -427,6 +427,8 @@ async function aiAccess(repositoryId) {
|
|
|
427
427
|
const allowed = (key) => ai[key] !== false;
|
|
428
428
|
return {
|
|
429
429
|
aiRead: allowed("read"),
|
|
430
|
+
/* The one answer that starts as no: a service that does not send it has not been asked. */
|
|
431
|
+
aiTrain: ai.train === true,
|
|
430
432
|
aiDownload: allowed("download"),
|
|
431
433
|
aiContribute: allowed("contribute"),
|
|
432
434
|
aiRequest: allowed("request"),
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -33,6 +33,7 @@ const upload_progress_js_1 = require("./upload_progress.js");
|
|
|
33
33
|
const publish_js_1 = require("./publish.js");
|
|
34
34
|
const attach_command_js_1 = require("./attach_command.js");
|
|
35
35
|
const build_command_js_1 = require("./build_command.js");
|
|
36
|
+
const update_command_js_1 = require("./update_command.js");
|
|
36
37
|
const node_check_js_1 = require("./node_check.js");
|
|
37
38
|
const pages_command_js_1 = require("./pages_command.js");
|
|
38
39
|
const mods_command_js_1 = require("./mods_command.js");
|
|
@@ -739,7 +740,7 @@ async function commandImport(parsed) {
|
|
|
739
740
|
flags: submitFlags,
|
|
740
741
|
});
|
|
741
742
|
if (code === 0 && plan.temporary) {
|
|
742
|
-
console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
|
|
743
|
+
console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
|
|
743
744
|
` +
|
|
744
745
|
`Run ${accent("cbx get " + plan.name)} anywhere to fetch it fresh.`));
|
|
745
746
|
}
|
|
@@ -1025,17 +1026,17 @@ async function commandSubmit(parsed) {
|
|
|
1025
1026
|
*/
|
|
1026
1027
|
const shielded = (await (0, worktree_js_1.detectPrivateDirectories)(folder)).filter((finding) => [...sending].some((file) => file === finding.path || file.startsWith(`${finding.path}/`)));
|
|
1027
1028
|
if (shielded.length && !hasFlag(parsed, "allow-private")) {
|
|
1028
|
-
console.log(red(`
|
|
1029
|
+
console.log(red(`
|
|
1029
1030
|
${shielded.length} folder${shielded.length === 1 ? "" : "s"} here belong${shielded.length === 1 ? "s" : ""} to a program, not to your project:`));
|
|
1030
1031
|
for (const finding of shielded) {
|
|
1031
1032
|
console.log(` ${finding.path} ${dim(`— ${finding.because}`)}`);
|
|
1032
1033
|
}
|
|
1033
|
-
console.log(`
|
|
1034
|
+
console.log(`
|
|
1034
1035
|
Nothing was sent. To leave them behind:`);
|
|
1035
1036
|
for (const finding of shielded) {
|
|
1036
1037
|
console.log(` ${accent(`echo "${finding.rule}" >> .gitignore`)}`);
|
|
1037
1038
|
}
|
|
1038
|
-
console.error(`
|
|
1039
|
+
console.error(`
|
|
1039
1040
|
Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
1040
1041
|
return 1;
|
|
1041
1042
|
}
|
|
@@ -1084,7 +1085,7 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
|
1084
1085
|
`rest were not opened.`));
|
|
1085
1086
|
}
|
|
1086
1087
|
if (stillPasted.length) {
|
|
1087
|
-
console.log(red(`
|
|
1088
|
+
console.log(red(`
|
|
1088
1089
|
${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
|
|
1089
1090
|
for (const finding of stillPasted.slice(0, 20)) {
|
|
1090
1091
|
console.log(` ${finding.path}`);
|
|
@@ -1099,11 +1100,11 @@ ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s
|
|
|
1099
1100
|
console.log(dim(` …and ${stillPasted.length - 20} more`));
|
|
1100
1101
|
}
|
|
1101
1102
|
if (!hasFlag(parsed, "allow-secrets")) {
|
|
1102
|
-
console.error(`
|
|
1103
|
+
console.error(`
|
|
1103
1104
|
Nothing was sent. Move the key into an environment variable, and if` +
|
|
1104
1105
|
` it has ever been published, replace it at the service that issued` +
|
|
1105
1106
|
` it — a key that has leaked stays leaked.` +
|
|
1106
|
-
`
|
|
1107
|
+
`
|
|
1107
1108
|
Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
1108
1109
|
return 1;
|
|
1109
1110
|
}
|
|
@@ -1309,7 +1310,7 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
1309
1310
|
continue;
|
|
1310
1311
|
}
|
|
1311
1312
|
if (failure?.kind === "interrupted") {
|
|
1312
|
-
console.error(red(`
|
|
1313
|
+
console.error(red(`
|
|
1313
1314
|
The connection failed: ${text}`));
|
|
1314
1315
|
console.error(`Your work may already have been saved. Run the same command again —` +
|
|
1315
1316
|
` it will not create a second version.`);
|
|
@@ -1566,7 +1567,7 @@ async function commandGet(parsed) {
|
|
|
1566
1567
|
`changed while the interrupted fetch was stopped:`));
|
|
1567
1568
|
for (const file of changedInTheGap.slice(0, 20))
|
|
1568
1569
|
console.error(` ${file.path}`);
|
|
1569
|
-
console.error(`
|
|
1570
|
+
console.error(`
|
|
1570
1571
|
Save them with ${accent("cbx submit")}, or finish the fetch and ` +
|
|
1571
1572
|
`discard them with ${accent("cbx get --replace")}.`);
|
|
1572
1573
|
return 1;
|
|
@@ -1849,8 +1850,8 @@ async function suggestRules(folder, apply) {
|
|
|
1849
1850
|
const addition = (0, detect_js_1.rulesFromSuggestions)(recommended);
|
|
1850
1851
|
const shared = rules.shared.trimEnd();
|
|
1851
1852
|
await (0, worktree_js_1.writeRules)(folder, {
|
|
1852
|
-
shared: shared ? `${shared}
|
|
1853
|
-
|
|
1853
|
+
shared: shared ? `${shared}
|
|
1854
|
+
|
|
1854
1855
|
${addition}` : addition,
|
|
1855
1856
|
local: rules.local,
|
|
1856
1857
|
});
|
|
@@ -2055,7 +2056,7 @@ async function commandMerges(parsed) {
|
|
|
2055
2056
|
const counts = merge.conflicts;
|
|
2056
2057
|
console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
|
|
2057
2058
|
}
|
|
2058
|
-
console.log(dim(`
|
|
2059
|
+
console.log(dim(`
|
|
2059
2060
|
Run cbx merge <reference> to look at one.`));
|
|
2060
2061
|
return 0;
|
|
2061
2062
|
}
|
|
@@ -2158,7 +2159,7 @@ async function commandMerge(parsed) {
|
|
|
2158
2159
|
console.log(dim(` waiting on ${waiting.map((person) => person.displayName).join(", ")}`));
|
|
2159
2160
|
}
|
|
2160
2161
|
}
|
|
2161
|
-
console.log(`
|
|
2162
|
+
console.log(`
|
|
2162
2163
|
${accent(now.mergeTrack.reference)} · ` +
|
|
2163
2164
|
/*
|
|
2164
2165
|
The combined version is only counted once it can be built; until then
|
|
@@ -2177,7 +2178,7 @@ ${accent(now.mergeTrack.reference)} · ` +
|
|
|
2177
2178
|
return 1;
|
|
2178
2179
|
}
|
|
2179
2180
|
const applied = await (0, api_js_1.applyMerge)(summary.id);
|
|
2180
|
-
console.log(`
|
|
2181
|
+
console.log(`
|
|
2181
2182
|
Applied as ${accent(`v${applied.version.sequence}`)}.`);
|
|
2182
2183
|
if (applied.closedIssues?.length) {
|
|
2183
2184
|
console.log(`Closed ${applied.closedIssues.map((number) => `#${number}`).join(", ")}.`);
|
|
@@ -2187,13 +2188,13 @@ Applied as ${accent(`v${applied.version.sequence}`)}.`);
|
|
|
2187
2188
|
}
|
|
2188
2189
|
const stillOpen = now.conflicts.some((conflict) => !conflict.resolvedAt);
|
|
2189
2190
|
if (!decision && stillOpen) {
|
|
2190
|
-
console.log(dim(`
|
|
2191
|
+
console.log(dim(`
|
|
2191
2192
|
--mine keeps yours, --theirs keeps what was already saved,` +
|
|
2192
|
-
` --both keeps theirs and saves yours beside it, --drop removes the file.
|
|
2193
|
+
` --both keeps theirs and saves yours beside it, --drop removes the file.
|
|
2193
2194
|
Add --path <file> for one file, then --apply when ready.`));
|
|
2194
2195
|
}
|
|
2195
2196
|
else if (now.provisional.ready) {
|
|
2196
|
-
console.log(dim(`
|
|
2197
|
+
console.log(dim(`
|
|
2197
2198
|
Run cbx merge ${now.mergeTrack.reference} --apply to publish it.`));
|
|
2198
2199
|
}
|
|
2199
2200
|
return 0;
|
|
@@ -2786,9 +2787,9 @@ const SPECS = [
|
|
|
2786
2787
|
name: "workspace",
|
|
2787
2788
|
group: "Workspaces",
|
|
2788
2789
|
summary: "edit, save and run a private project remotely without a checkout",
|
|
2789
|
-
usage: "workspace | workspace <usage|open|list|show|read|files|search|diff|patch|put|starter|save|run|logs|cancel|deploy|preview|handoff|close|recipe|assign|assignments|unassign|checkout|say|thread|mute|unmute> <project> [workspace] [job] | workspace put <project> <workspace> <path…> | workspace push [folder] | workspace watch [project] [workspace]",
|
|
2790
|
+
usage: "workspace | workspace <usage|open|list|show|read|files|search|diff|patch|put|starter|review|reviewers|save|run|logs|cancel|deploy|preview|handoff|close|recipe|assign|assignments|unassign|checkout|say|thread|mute|unmute> <project> [workspace] [job] | workspace put <project> <workspace> <path…> | workspace push [folder] | workspace watch [project] [workspace]",
|
|
2790
2791
|
brief: "workspace <action> <project>",
|
|
2791
|
-
detail: "Works on CodeRook's durable remote draft, never the current local folder. Use --data-file for structured edits or recipe configuration. Mutations need the reviewed --revision and a unique --key; reuse the same key and request when retrying. Recipes reference existing project workflows. Usage reads the shared workspace credits and separate PR allowance without a draft ID. put copies files from this folder into a draft: text as text, images, fonts and other binary files as bytes (up to 1 MiB each), a file that is gone here is removed there, and each file must still be the version the draft holds, so a teammate's edit is never overwritten. starter reads this folder (package.json scripts, Cargo.toml, go.mod, Python) and suggests one check recipe per package so drafts have something to run; --apply adds them, never replacing a recipe that exists. Saving and deploying are separate operations. Run on its own at a terminal, it lists your open workspaces with their unread messages: choose one to see its changes and what is being discussed, and reply, save, run checks, hand it over or close it with one key. say and thread post to and read a workspace's discussion as JSON; a machine signed in to your account can name itself with --as (or CBX_NAME) so its messages and unread count are its own. watch prints each new message, each message addressed to you (@your name, or the name given with --as) and each new saved version as it happens, until stopped; --json gives one JSON object per line for a program.",
|
|
2792
|
+
detail: "Works on CodeRook's durable remote draft, never the current local folder. Use --data-file for structured edits or recipe configuration. Mutations need the reviewed --revision and a unique --key; reuse the same key and request when retrying. Recipes reference existing project workflows. Usage reads the shared workspace credits and separate PR allowance without a draft ID. put copies files from this folder into a draft: text as text, images, fonts and other binary files as bytes (up to 1 MiB each), a file that is gone here is removed there, and each file must still be the version the draft holds, so a teammate's edit is never overwritten. starter reads this folder (package.json scripts, Cargo.toml, go.mod, Python) and suggests one check recipe per package so drafts have something to run; --apply adds them, never replacing a recipe that exists. review shows who must OK a draft before it is saved or deployed; --ok or --changes -m gives your verdict on the current revision, and counts only from the reviewer's own sign-in or token, never a name given with --as. reviewers lists the project's rules, and an administrator sets them with --data-file. Saving and deploying are separate operations. Run on its own at a terminal, it lists your open workspaces with their unread messages: choose one to see its changes and what is being discussed, and reply, save, run checks, hand it over or close it with one key. say and thread post to and read a workspace's discussion as JSON; a machine signed in to your account can name itself with --as (or CBX_NAME) so its messages and unread count are its own. watch prints each new message, each message addressed to you (@your name, or the name given with --as) and each new saved version as it happens, until stopped; --json gives one JSON object per line for a program.",
|
|
2792
2793
|
options: [
|
|
2793
2794
|
{ flags: "--data-file <path>", description: "read an operation's JSON from this local file" },
|
|
2794
2795
|
{ flags: "--name <name>", description: "name a new workspace" },
|
|
@@ -2815,10 +2816,12 @@ const SPECS = [
|
|
|
2815
2816
|
{ flags: "--after <time>", description: "thread: continue from the next page's time" },
|
|
2816
2817
|
{ flags: "--root <folder>", description: "put/starter: the project folder (default: here)" },
|
|
2817
2818
|
{ flags: "--apply", description: "starter: create the suggested workflows and recipes" },
|
|
2819
|
+
{ flags: "--ok", description: "review: approve the draft's current revision" },
|
|
2820
|
+
{ flags: "--changes", description: "review: ask for changes (say what with -m)" },
|
|
2818
2821
|
{ flags: "--json", description: "watch: one JSON object per line; starter: the plan as JSON" },
|
|
2819
2822
|
{ flags: "--interval <seconds>", description: "watch: how often to look (default 20, at least 5)" },
|
|
2820
2823
|
],
|
|
2821
|
-
examples: ["cbx workspace open my-project --name \"Laptop work\" --key laptop-open-001", "cbx workspace list my-project", "cbx workspace assign my-project <draft> --name Laptop", "cbx workspace checkout my-project <draft>", "cbx workspace push --save -m \"Fix the export dialog\"", "cbx workspace", "cbx workspace say my-project <draft> -m \"Tests updated\" --as GrokBot", "cbx workspace thread my-project <draft> --unread", "cbx workspace watch my-project --as GrokBot --json", "cbx workspace put my-project <draft> website/app/page.tsx website/public/og.png", "cbx workspace starter my-project --apply"],
|
|
2824
|
+
examples: ["cbx workspace open my-project --name \"Laptop work\" --key laptop-open-001", "cbx workspace list my-project", "cbx workspace assign my-project <draft> --name Laptop", "cbx workspace checkout my-project <draft>", "cbx workspace push --save -m \"Fix the export dialog\"", "cbx workspace", "cbx workspace say my-project <draft> -m \"Tests updated\" --as GrokBot", "cbx workspace thread my-project <draft> --unread", "cbx workspace watch my-project --as GrokBot --json", "cbx workspace put my-project <draft> website/app/page.tsx website/public/og.png", "cbx workspace starter my-project --apply", "cbx workspace review my-project <draft> --ok -m \"Matches the concept\""],
|
|
2822
2825
|
run: workspace_command_js_1.commandWorkspace,
|
|
2823
2826
|
},
|
|
2824
2827
|
{
|
|
@@ -4048,6 +4051,23 @@ const SPECS = [
|
|
|
4048
4051
|
examples: ["claude mcp add coderook -- cbx mcp"],
|
|
4049
4052
|
run: parsed => parsed.flags.has("workspaces") ? (0, workspace_command_js_1.commandWorkspaceMcp)() : (0, mcp_js_1.commandMcp)(version_js_1.VERSION),
|
|
4050
4053
|
},
|
|
4054
|
+
{
|
|
4055
|
+
name: "update",
|
|
4056
|
+
aliases: ["upgrade"],
|
|
4057
|
+
group: "Getting started",
|
|
4058
|
+
summary: "install the newest cbx",
|
|
4059
|
+
usage: "update",
|
|
4060
|
+
options: [
|
|
4061
|
+
{ flags: "--check", description: "only say whether a newer cbx is out" },
|
|
4062
|
+
],
|
|
4063
|
+
detail: "Updates cbx itself, not a folder: `cbx get` brings a project's files\n" +
|
|
4064
|
+
"up to date. Asks the npm registry for the newest @coderook/cli and\n" +
|
|
4065
|
+
"installs it the way this copy was installed: globally with npm, or in\n" +
|
|
4066
|
+
"the project whose packages hold it. Through npx there is nothing to\n" +
|
|
4067
|
+
"install. Your folders, sign-in and settings are left as they are.",
|
|
4068
|
+
examples: ["cbx update", "cbx update --check"],
|
|
4069
|
+
run: parsed => (0, update_command_js_1.commandUpdate)(parsed.flags),
|
|
4070
|
+
},
|
|
4051
4071
|
{
|
|
4052
4072
|
name: "doctor",
|
|
4053
4073
|
group: "Getting started",
|
|
@@ -56,13 +56,19 @@ async function resolveProject(reference) {
|
|
|
56
56
|
}
|
|
57
57
|
return project;
|
|
58
58
|
}
|
|
59
|
-
/** The
|
|
59
|
+
/** The answers, in the order the site shows them. */
|
|
60
60
|
const AI_SWITCHES = [
|
|
61
61
|
[
|
|
62
62
|
"read",
|
|
63
63
|
"aiRead",
|
|
64
64
|
"Can AI read this?",
|
|
65
|
-
"reading, indexing, summarising and
|
|
65
|
+
"reading, indexing, summarising and quoting in answers",
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"train",
|
|
69
|
+
"aiTrain",
|
|
70
|
+
"Can AI train on this?",
|
|
71
|
+
"using what it reads to train a model",
|
|
66
72
|
],
|
|
67
73
|
[
|
|
68
74
|
"download",
|
|
@@ -122,8 +128,8 @@ async function commandAi(parsed) {
|
|
|
122
128
|
console.log(` ${(0, colour_js_1.dim)(meaning)}`);
|
|
123
129
|
}
|
|
124
130
|
console.log("");
|
|
125
|
-
console.log((0, colour_js_1.dim)("Everything is allowed until somebody says otherwise
|
|
126
|
-
console.log((0, colour_js_1.dim)("
|
|
131
|
+
console.log((0, colour_js_1.dim)("Everything but training is allowed until somebody says otherwise; training"));
|
|
132
|
+
console.log((0, colour_js_1.dim)("is refused until the owner allows it, and reading off refuses it too."));
|
|
127
133
|
console.log((0, colour_js_1.dim)("Change these on the website (Settings → Access & AI) or in the desktop app."));
|
|
128
134
|
return 0;
|
|
129
135
|
}
|
package/dist/cli/src/registry.js
CHANGED
|
@@ -37,7 +37,7 @@ exports.GROUP_ORDER = [
|
|
|
37
37
|
* from this list still appears, after the ones that are in it.
|
|
38
38
|
*/
|
|
39
39
|
exports.LISTING_ORDER = [
|
|
40
|
-
"sign-in", "whoami", "sign-out", "doctor", "projects", "clone", "link", "unlink", "import", "transfer",
|
|
40
|
+
"sign-in", "whoami", "sign-out", "doctor", "update", "projects", "clone", "link", "unlink", "import", "transfer",
|
|
41
41
|
"status", "submit", "diff", "get", "track", "ignore", "licence",
|
|
42
42
|
"init", "save", "log", "restore", "switch", "push", "pull",
|
|
43
43
|
"tracks", "line-rules", "merges", "merge", "propose", "proposals", "pr",
|
|
@@ -0,0 +1,159 @@
|
|
|
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.compareVersions = compareVersions;
|
|
7
|
+
exports.installKind = installKind;
|
|
8
|
+
exports.projectFolder = projectFolder;
|
|
9
|
+
exports.commandUpdate = commandUpdate;
|
|
10
|
+
/**
|
|
11
|
+
* `cbx update` — bring this copy of cbx itself up to the newest release.
|
|
12
|
+
*
|
|
13
|
+
* Not the folder: `cbx get` (and `cbx pull`) bring a project's files up to
|
|
14
|
+
* date. This asks the npm registry for the newest @coderook/cli and installs it
|
|
15
|
+
* the way this copy was installed, because the right command differs:
|
|
16
|
+
*
|
|
17
|
+
* global npm install npm install -g @coderook/cli@<newest>, run here
|
|
18
|
+
* npx nothing to install; npx fetches what it is told to
|
|
19
|
+
* a project's packages npm install @coderook/cli@<newest> in that project
|
|
20
|
+
* anything else the command to run, printed rather than guessed at
|
|
21
|
+
*
|
|
22
|
+
* Every outward call has a deadline, so a registry or npm that never answers
|
|
23
|
+
* ends in a message instead of a hang.
|
|
24
|
+
*/
|
|
25
|
+
const node_child_process_1 = require("node:child_process");
|
|
26
|
+
const node_fs_1 = require("node:fs");
|
|
27
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
28
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
29
|
+
const version_js_1 = require("./version.js");
|
|
30
|
+
const PACKAGE = "@coderook/cli";
|
|
31
|
+
/** A release number and nothing else: it is put on an npm command line. */
|
|
32
|
+
const RELEASE = /^\d{1,6}\.\d{1,6}\.\d{1,6}(?:-[0-9A-Za-z.]{1,40})?$/;
|
|
33
|
+
const REGISTRY = "https://registry.npmjs.org/" + PACKAGE.replace("/", "%2F") + "/latest";
|
|
34
|
+
const colour = node_process_1.default.stdout.isTTY && !node_process_1.default.env.NO_COLOR;
|
|
35
|
+
const paint = (code) => (text) => (colour ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
36
|
+
const dim = paint(2), accent = paint(33), red = paint(31), green = paint(32);
|
|
37
|
+
/** Compares two release numbers (1.2.3, pre-release tags rank below the release). Negative when a is older. */
|
|
38
|
+
function compareVersions(a, b) {
|
|
39
|
+
const parse = (v) => {
|
|
40
|
+
const [core = "", pre = ""] = v.trim().replace(/^v/, "").split("-", 2);
|
|
41
|
+
return { parts: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre };
|
|
42
|
+
};
|
|
43
|
+
const x = parse(a), y = parse(b);
|
|
44
|
+
for (let i = 0; i < 3; i += 1) {
|
|
45
|
+
const d = (x.parts[i] ?? 0) - (y.parts[i] ?? 0);
|
|
46
|
+
if (d)
|
|
47
|
+
return d;
|
|
48
|
+
}
|
|
49
|
+
if (x.pre === y.pre)
|
|
50
|
+
return 0;
|
|
51
|
+
if (!x.pre)
|
|
52
|
+
return 1;
|
|
53
|
+
if (!y.pre)
|
|
54
|
+
return -1;
|
|
55
|
+
return x.pre < y.pre ? -1 : 1;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* How the running copy was installed, from where its files are.
|
|
59
|
+
* `globalRoot` is `npm root -g` (null when npm could not say).
|
|
60
|
+
*/
|
|
61
|
+
function installKind(script, globalRoot, platform = node_process_1.default.platform) {
|
|
62
|
+
const norm = (p) => {
|
|
63
|
+
const s = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
64
|
+
return platform === "win32" ? s.toLowerCase() : s;
|
|
65
|
+
};
|
|
66
|
+
const here = norm(script);
|
|
67
|
+
if (!here.includes("/node_modules/@coderook/cli/"))
|
|
68
|
+
return "other";
|
|
69
|
+
if (/\/_npx\//.test(here))
|
|
70
|
+
return "npx";
|
|
71
|
+
if (globalRoot && here.startsWith(norm(globalRoot) + "/"))
|
|
72
|
+
return "global";
|
|
73
|
+
return "project";
|
|
74
|
+
}
|
|
75
|
+
/** The folder holding the node_modules this copy sits in (for a project install). */
|
|
76
|
+
function projectFolder(script) {
|
|
77
|
+
const s = script.replace(/\\/g, "/");
|
|
78
|
+
const at = s.lastIndexOf("/node_modules/@coderook/cli/");
|
|
79
|
+
return at < 0 ? node_path_1.default.dirname(script) : s.slice(0, at);
|
|
80
|
+
}
|
|
81
|
+
async function newestRelease() {
|
|
82
|
+
const response = await fetch(REGISTRY, { signal: AbortSignal.timeout(10_000), headers: { accept: "application/json" } });
|
|
83
|
+
if (!response.ok)
|
|
84
|
+
throw new Error(`the npm registry answered ${response.status}`);
|
|
85
|
+
const body = (await response.json());
|
|
86
|
+
if (typeof body.version !== "string" || !RELEASE.test(body.version))
|
|
87
|
+
throw new Error("the npm registry gave no usable version");
|
|
88
|
+
return body.version;
|
|
89
|
+
}
|
|
90
|
+
/** Runs npm with a deadline; output goes to this terminal when `show` is set. */
|
|
91
|
+
function npm(args, options) {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
/*
|
|
94
|
+
npm is npm.cmd on Windows, which Node only starts through a shell, and a
|
|
95
|
+
shell takes one command line rather than an argument list. Every word
|
|
96
|
+
here is ours or a release number checked by RELEASE, so joining is safe.
|
|
97
|
+
*/
|
|
98
|
+
const stdio = options.show ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "ignore"];
|
|
99
|
+
const child = node_process_1.default.platform === "win32"
|
|
100
|
+
? (0, node_child_process_1.spawn)(["npm", ...args].join(" "), { cwd: options.cwd, shell: true, stdio })
|
|
101
|
+
: (0, node_child_process_1.spawn)("npm", args, { cwd: options.cwd, stdio });
|
|
102
|
+
let out = "";
|
|
103
|
+
child.stdout?.on("data", (chunk) => { out += chunk.toString(); });
|
|
104
|
+
const timer = setTimeout(() => { child.kill(); resolve({ code: -1, out }); }, options.deadlineMs);
|
|
105
|
+
child.on("error", () => { clearTimeout(timer); resolve({ code: -2, out }); });
|
|
106
|
+
child.on("close", (code) => { clearTimeout(timer); resolve({ code: code ?? 1, out }); });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function commandUpdate(flags) {
|
|
110
|
+
const checkOnly = flags.has("check");
|
|
111
|
+
let newest;
|
|
112
|
+
try {
|
|
113
|
+
newest = await newestRelease();
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error(red("Could not find out the newest cbx: " + (error instanceof Error ? error.message : String(error)) + "."));
|
|
117
|
+
console.error(dim("Check the connection and try again. Nothing was changed."));
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
const order = compareVersions(version_js_1.VERSION, newest);
|
|
121
|
+
if (order >= 0) {
|
|
122
|
+
console.log(green(`cbx ${version_js_1.VERSION} is the newest.`) + (order > 0 ? dim(` (the registry's newest is ${newest})`) : ""));
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
console.log(`cbx ${version_js_1.VERSION} is installed; ${accent(newest)} is out.`);
|
|
126
|
+
let script = node_process_1.default.argv[1] ?? "";
|
|
127
|
+
try {
|
|
128
|
+
script = (0, node_fs_1.realpathSync)(script);
|
|
129
|
+
}
|
|
130
|
+
catch { /* keep what we were given */ }
|
|
131
|
+
const root = await npm(["root", "-g"], { show: false, deadlineMs: 20_000 });
|
|
132
|
+
const kind = installKind(script, root.code === 0 ? root.out.trim() : null);
|
|
133
|
+
const target = `${PACKAGE}@${newest}`;
|
|
134
|
+
if (kind === "npx") {
|
|
135
|
+
console.log(`This copy runs through npx. Ask for the newest each time with ${accent(`npx ${PACKAGE}@latest`)}.`);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
if (kind === "other") {
|
|
139
|
+
console.log(`This copy was not installed by npm, so install the newest with ${accent(`npm install -g ${target}`)}.`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
const folder = kind === "project" ? projectFolder(script) : undefined;
|
|
143
|
+
const args = kind === "global" ? ["install", "-g", target] : ["install", target];
|
|
144
|
+
if (checkOnly) {
|
|
145
|
+
console.log(`Update with ${accent("cbx update")}, or ${accent("npm " + args.join(" "))}${folder ? dim(` in ${folder}`) : ""}.`);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
console.log(dim(`Running npm ${args.join(" ")}${folder ? ` in ${folder}` : ""}…`));
|
|
149
|
+
const result = await npm(args, { show: true, cwd: folder, deadlineMs: 5 * 60_000 });
|
|
150
|
+
if (result.code !== 0) {
|
|
151
|
+
console.error(red(result.code === -1 ? "npm did not finish within five minutes." : result.code === -2 ? "npm could not be started." : `npm stopped with code ${result.code}.`));
|
|
152
|
+
if (kind === "global")
|
|
153
|
+
console.error(dim("If it was refused for permissions, run the same command from a terminal that can install global packages."));
|
|
154
|
+
console.error(dim(`Your current cbx ${version_js_1.VERSION} is untouched.`));
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
console.log(green(`cbx is now ${newest}.`) + dim(" Your folders, sign-in and settings are as they were."));
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
@@ -10,6 +10,7 @@ const config_js_1 = require("./config.js");
|
|
|
10
10
|
const workspace_machine_js_1 = require("./workspace_machine.js");
|
|
11
11
|
const workspace_threads_js_1 = require("./workspace_threads.js");
|
|
12
12
|
const workspace_put_js_1 = require("./workspace_put.js");
|
|
13
|
+
const workspace_review_js_1 = require("./workspace_review.js");
|
|
13
14
|
const workspace_starter_js_1 = require("./workspace_starter.js");
|
|
14
15
|
const workspace_watch_js_1 = require("./workspace_watch.js");
|
|
15
16
|
async function commandWorkspace(parsed) {
|
|
@@ -30,6 +31,13 @@ async function commandWorkspace(parsed) {
|
|
|
30
31
|
const root = `/v1/repositories/${encodeURIComponent(project.id)}`;
|
|
31
32
|
if (operation === "starter")
|
|
32
33
|
return (0, workspace_starter_js_1.commandWorkspaceStarter)(parsed, root);
|
|
34
|
+
if (operation === "reviewers")
|
|
35
|
+
return (0, workspace_review_js_1.commandWorkspaceReviewers)(parsed, root);
|
|
36
|
+
if (operation === "review") {
|
|
37
|
+
if (!workspace)
|
|
38
|
+
throw new Error("Supply the workspace ID after the project");
|
|
39
|
+
return (0, workspace_review_js_1.commandWorkspaceReview)(parsed, root, workspace);
|
|
40
|
+
}
|
|
33
41
|
if (operation === "put") {
|
|
34
42
|
if (!workspace)
|
|
35
43
|
throw new Error("Supply the workspace ID after the project");
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeReviewer = describeReviewer;
|
|
4
|
+
exports.commandWorkspaceReview = commandWorkspaceReview;
|
|
5
|
+
exports.commandWorkspaceReviewers = commandWorkspaceReviewers;
|
|
6
|
+
/**
|
|
7
|
+
* cbx workspace review <project> <workspace> --ok [-m "…"] | --changes -m "…"
|
|
8
|
+
* cbx workspace reviewers <project> [--data-file rules.json]
|
|
9
|
+
*
|
|
10
|
+
* The CLI side of the review gate (backend workspace_reviews.ts). A review
|
|
11
|
+
* counts only from the reviewer's own sign-in or token, so --as is refused
|
|
12
|
+
* here before the service refuses it, with the reason.
|
|
13
|
+
*/
|
|
14
|
+
const promises_1 = require("node:fs/promises");
|
|
15
|
+
const api_js_1 = require("./api.js");
|
|
16
|
+
const colour_js_1 = require("./colour.js");
|
|
17
|
+
const flag = (parsed, ...names) => {
|
|
18
|
+
for (const name of names) {
|
|
19
|
+
const value = parsed.flags.get(name);
|
|
20
|
+
if (typeof value === "string")
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
};
|
|
25
|
+
/** One line per reviewer: a word for where it stands, never only a colour. */
|
|
26
|
+
function describeReviewer(s, revision) {
|
|
27
|
+
const when = [s.beforeSave && "save", s.beforeDeploy && "deploy"].filter(Boolean).join(" + ");
|
|
28
|
+
if (!s.applies)
|
|
29
|
+
return `${(0, colour_js_1.dim)("not needed")} ${s.label} ${(0, colour_js_1.dim)(`(no changes in its paths)`)}`;
|
|
30
|
+
const word = s.state === "ok"
|
|
31
|
+
? (0, colour_js_1.green)("OK ")
|
|
32
|
+
: s.state === "changes"
|
|
33
|
+
? (0, colour_js_1.red)("changes ")
|
|
34
|
+
: s.state === "outdated"
|
|
35
|
+
? "outdated "
|
|
36
|
+
: (0, colour_js_1.dim)("waiting ");
|
|
37
|
+
const detail = s.state === "outdated"
|
|
38
|
+
? `OK was for revision ${s.revision}, draft is at ${revision}`
|
|
39
|
+
: s.state === "changes" || (s.state === "ok" && s.note)
|
|
40
|
+
? s.note
|
|
41
|
+
: "";
|
|
42
|
+
return `${word}${(0, colour_js_1.bold)(s.label)} ${(0, colour_js_1.dim)(`before ${when}`)}${detail ? ` ${detail}` : ""}`;
|
|
43
|
+
}
|
|
44
|
+
async function commandWorkspaceReview(parsed, root, workspace) {
|
|
45
|
+
if (parsed.flags.has("as") || process.env.CBX_NAME)
|
|
46
|
+
throw new Error("A review counts only from the reviewer's own sign-in or token; run it without --as or CBX_NAME.");
|
|
47
|
+
const ok = parsed.flags.has("ok");
|
|
48
|
+
const changes = parsed.flags.has("changes");
|
|
49
|
+
const draft = `${root}/workspaces/${encodeURIComponent(workspace)}`;
|
|
50
|
+
if (ok === changes) {
|
|
51
|
+
/* Neither: show where the draft stands. */
|
|
52
|
+
const shown = await (0, api_js_1.callApi)(draft);
|
|
53
|
+
const review = shown.review ?? [];
|
|
54
|
+
if (parsed.flags.has("json"))
|
|
55
|
+
console.log(JSON.stringify({ revision: shown.workspace.revision, review }, null, 2));
|
|
56
|
+
else if (!review.length)
|
|
57
|
+
console.log("This project names no reviewers. An administrator sets them with cbx workspace reviewers.");
|
|
58
|
+
else
|
|
59
|
+
for (const s of review)
|
|
60
|
+
console.log(describeReviewer(s, shown.workspace.revision));
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
const note = flag(parsed, "m", "message") ?? "";
|
|
64
|
+
if (changes && !note.trim())
|
|
65
|
+
throw new Error('Say what needs to change: --changes -m "…"');
|
|
66
|
+
const revision = parsed.flags.has("revision")
|
|
67
|
+
? Number(parsed.flags.get("revision"))
|
|
68
|
+
: (await (0, api_js_1.callApi)(draft)).workspace.revision;
|
|
69
|
+
const out = await (0, api_js_1.callApi)(`${draft}/reviews`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: { verdict: ok ? "ok" : "changes", note, expectedRevision: revision },
|
|
72
|
+
});
|
|
73
|
+
if (parsed.flags.has("json"))
|
|
74
|
+
console.log(JSON.stringify(out, null, 2));
|
|
75
|
+
else {
|
|
76
|
+
console.log(`${ok ? (0, colour_js_1.green)("OK") : (0, colour_js_1.red)("Changes asked")} on revision ${revision}.`);
|
|
77
|
+
for (const s of out.review)
|
|
78
|
+
console.log(` ${describeReviewer(s, revision)}`);
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
async function commandWorkspaceReviewers(parsed, root) {
|
|
83
|
+
const source = flag(parsed, "data-file");
|
|
84
|
+
const rules = source
|
|
85
|
+
? (await (0, api_js_1.callApi)(`${root}/workspace-review-rules`, {
|
|
86
|
+
method: "PUT",
|
|
87
|
+
body: { rules: JSON.parse(await (0, promises_1.readFile)(source, "utf8")) },
|
|
88
|
+
})).rules
|
|
89
|
+
: (await (0, api_js_1.callApi)(`${root}/workspace-review-rules`)).rules;
|
|
90
|
+
if (parsed.flags.has("json") || source)
|
|
91
|
+
console.log(JSON.stringify({ rules }, null, 2));
|
|
92
|
+
else if (!rules.length)
|
|
93
|
+
console.log(`No reviewers. Set them from a JSON list with ${(0, colour_js_1.accent)("cbx workspace reviewers <project> --data-file rules.json")}:\n` +
|
|
94
|
+
(0, colour_js_1.dim)(' [{"reviewer":"token:<id>","label":"UX Bot","paths":["website/"],"beforeSave":true,"beforeDeploy":false,"ownerMayOverride":false}]'));
|
|
95
|
+
else
|
|
96
|
+
for (const r of rules)
|
|
97
|
+
console.log(`${(0, colour_js_1.bold)(r.label)} ${(0, colour_js_1.dim)(r.reviewer)} ${r.paths.length ? r.paths.join(", ") : "every path"} ${(0, colour_js_1.dim)(`before ${[r.beforeSave && "save", r.beforeDeploy && "deploy"].filter(Boolean).join(" + ")}${r.ownerMayOverride ? "; owner may go ahead without" : ""}`)}`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "The command line for CodeRook (coderook.com): version control and project hosting for projects of any size. Every save is a compressed snapshot, private or public, never used to train AI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://coderook.com",
|