@coderook/cli 0.22.2 → 0.24.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 +342 -220
- package/dist/cli/src/api.js +27 -2
- package/dist/cli/src/cli.js +323 -87
- package/dist/cli/src/git_history.js +188 -0
- package/dist/cli/src/git_remote.js +1118 -0
- package/dist/cli/src/git_remote_bin.js +22 -0
- package/dist/cli/src/help.js +8 -8
- package/dist/cli/src/import_command.js +150 -0
- package/dist/cli/src/licence_commands.js +5 -5
- package/dist/cli/src/mcp.js +1 -1
- package/dist/cli/src/publish.js +37 -0
- package/dist/cli/src/registry.js +1 -1
- package/dist/cli/src/runner.js +2 -2
- package/dist/cli/src/service_commands.js +53 -3
- package/dist/cli/src/skill_command.js +3 -3
- package/dist/cli/src/track_commands.js +7 -7
- package/dist/desktop-app/src/main/tracks.js +12 -2
- package/dist/desktop-app/src/main/upload.js +11 -1
- package/package.json +53 -51
- package/skills/coderook/SKILL.md +130 -130
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
/*
|
|
8
|
+
The executable half of `git-remote-coderook`.
|
|
9
|
+
|
|
10
|
+
Split from the module that does the work so that module can be imported by a
|
|
11
|
+
test without starting the protocol loop. Merging the two costs nothing to
|
|
12
|
+
write and means every test of a pure function first has to stop a process
|
|
13
|
+
from reading stdin and answering git.
|
|
14
|
+
*/
|
|
15
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
16
|
+
const git_remote_js_1 = require("./git_remote.js");
|
|
17
|
+
(0, git_remote_js_1.main)(node_process_1.default.argv.slice(2))
|
|
18
|
+
.then((code) => node_process_1.default.exit(code))
|
|
19
|
+
.catch((error) => {
|
|
20
|
+
node_process_1.default.stderr.write(`git-remote-coderook: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
21
|
+
node_process_1.default.exit(1);
|
|
22
|
+
});
|
package/dist/cli/src/help.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* What `
|
|
3
|
+
* What `cbx help` prints.
|
|
4
4
|
*
|
|
5
5
|
* Generated from the registry rather than written out, so a command that
|
|
6
6
|
* exists is listed and a command that is listed exists. The previous usage
|
|
@@ -30,7 +30,7 @@ function renderHelp(registry, version) {
|
|
|
30
30
|
const listed = registry.specs.filter((spec) => !spec.deprecatedBy);
|
|
31
31
|
const column = Math.min(nameColumn(listed), 34);
|
|
32
32
|
const lines = [
|
|
33
|
-
`${bold("
|
|
33
|
+
`${bold("cbx")} ${dim(version)} — the CodeBox engine, for CodeRook`,
|
|
34
34
|
"",
|
|
35
35
|
];
|
|
36
36
|
for (const group of registry_js_1.GROUP_ORDER) {
|
|
@@ -43,18 +43,18 @@ function renderHelp(registry, version) {
|
|
|
43
43
|
}
|
|
44
44
|
lines.push("");
|
|
45
45
|
}
|
|
46
|
-
lines.push(dim("
|
|
46
|
+
lines.push(dim("cbx help <command> what one command does, in full"), dim("CODEROOK_TOKEN is used when set, so automated runs need nothing on disk."), dim("CODEROOK_API_URL points at another service."));
|
|
47
47
|
return lines.join("\n");
|
|
48
48
|
}
|
|
49
49
|
function renderCommandHelp(spec) {
|
|
50
50
|
const lines = [
|
|
51
|
-
`${bold("
|
|
51
|
+
`${bold("cbx " + spec.name)} — ${spec.summary}`,
|
|
52
52
|
"",
|
|
53
53
|
bold("Usage"),
|
|
54
|
-
`
|
|
54
|
+
` cbx ${spec.usage}`,
|
|
55
55
|
];
|
|
56
56
|
if (spec.aliases?.length) {
|
|
57
|
-
lines.push("", bold("Also"), ` ${spec.aliases.map((alias) => `
|
|
57
|
+
lines.push("", bold("Also"), ` ${spec.aliases.map((alias) => `cbx ${alias}`).join(" ")}`);
|
|
58
58
|
}
|
|
59
59
|
if (spec.detail) {
|
|
60
60
|
lines.push("", spec.detail.trim());
|
|
@@ -82,7 +82,7 @@ function renderCommandHelp(spec) {
|
|
|
82
82
|
function renderUnknown(typed, suggestion) {
|
|
83
83
|
const lines = [`Unknown command: ${typed}`];
|
|
84
84
|
if (suggestion)
|
|
85
|
-
lines.push(`Did you mean ${accent("
|
|
86
|
-
lines.push(dim("Run
|
|
85
|
+
lines.push(`Did you mean ${accent("cbx " + suggestion)}?`);
|
|
86
|
+
lines.push(dim("Run cbx help to see everything."));
|
|
87
87
|
return lines.join("\n");
|
|
88
88
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
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.projectNameFromUrl = projectNameFromUrl;
|
|
7
|
+
exports.looksLikeRepositoryUrl = looksLikeRepositoryUrl;
|
|
8
|
+
exports.gitAvailable = gitAvailable;
|
|
9
|
+
exports.fetchSnapshot = fetchSnapshot;
|
|
10
|
+
exports.measure = measure;
|
|
11
|
+
exports.humanBytes = humanBytes;
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const promises_1 = require("node:fs/promises");
|
|
14
|
+
const node_os_1 = require("node:os");
|
|
15
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
+
const node_util_1 = require("node:util");
|
|
17
|
+
const run = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
18
|
+
/**
|
|
19
|
+
* The repository's own name, from the last path segment.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately not the host: an address with no path at all is not a
|
|
22
|
+
* repository, and naming somebody's project `github.com` because the URL was
|
|
23
|
+
* incomplete is the sort of thing they would only notice later.
|
|
24
|
+
*/
|
|
25
|
+
function projectNameFromUrl(url) {
|
|
26
|
+
const trimmed = url.trim().replace(/\/+$/, "");
|
|
27
|
+
/* Drop the scheme and authority so only path segments remain. */
|
|
28
|
+
const withoutScheme = trimmed.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
|
|
29
|
+
const afterHost = /^[^/]*:/.test(withoutScheme)
|
|
30
|
+
? /* scp-style git@host:owner/name */
|
|
31
|
+
withoutScheme.slice(withoutScheme.indexOf(":") + 1)
|
|
32
|
+
: withoutScheme.slice(withoutScheme.indexOf("/") + 1);
|
|
33
|
+
const hasPath = withoutScheme.includes("/") || /^[^/]*:/.test(withoutScheme);
|
|
34
|
+
if (!hasPath)
|
|
35
|
+
return "imported-project";
|
|
36
|
+
const tail = afterHost.split("/").filter(Boolean).pop() ?? "";
|
|
37
|
+
const name = tail.replace(/\.git$/i, "").trim();
|
|
38
|
+
return name || "imported-project";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Refuse anything that is not a repository location.
|
|
42
|
+
*
|
|
43
|
+
* `git clone` will happily treat a local path as a source, and a URL typed
|
|
44
|
+
* with a scheme this does not expect is more likely a mistake than an
|
|
45
|
+
* intention. Being narrow here keeps the command from doing something
|
|
46
|
+
* surprising with an argument that was meant for something else.
|
|
47
|
+
*/
|
|
48
|
+
function looksLikeRepositoryUrl(url) {
|
|
49
|
+
const value = url.trim();
|
|
50
|
+
if (/^(https?|git|ssh):\/\//i.test(value))
|
|
51
|
+
return true;
|
|
52
|
+
/* scp-style: git@host:owner/name.git */
|
|
53
|
+
if (/^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:[^\s]+$/.test(value))
|
|
54
|
+
return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
async function exists(target) {
|
|
58
|
+
try {
|
|
59
|
+
await (0, promises_1.access)(target);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function isEmptyDirectory(target) {
|
|
67
|
+
try {
|
|
68
|
+
const entries = await (0, promises_1.readdir)(target);
|
|
69
|
+
return entries.length === 0;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function gitAvailable() {
|
|
76
|
+
try {
|
|
77
|
+
await run("git", ["--version"], { windowsHide: true });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Shallow-clone into a working folder and strip the Git metadata.
|
|
86
|
+
*
|
|
87
|
+
* Returns where the files landed. The caller publishes from there with the
|
|
88
|
+
* ordinary save path, so an import produces exactly the Version an ordinary
|
|
89
|
+
* save of the same files would.
|
|
90
|
+
*/
|
|
91
|
+
async function fetchSnapshot(url, into, log = () => { }) {
|
|
92
|
+
const name = projectNameFromUrl(url);
|
|
93
|
+
let folder;
|
|
94
|
+
let temporary = false;
|
|
95
|
+
if (into) {
|
|
96
|
+
folder = node_path_1.default.resolve(into);
|
|
97
|
+
if ((await exists(folder)) && !(await isEmptyDirectory(folder))) {
|
|
98
|
+
throw new Error(`${folder} already has files in it. Import needs an empty folder, ` +
|
|
99
|
+
`so that nothing here is overwritten by what arrives.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const base = await (0, promises_1.mkdtemp)(node_path_1.default.join((0, node_os_1.tmpdir)(), "coderook-import-"));
|
|
104
|
+
folder = node_path_1.default.join(base, name);
|
|
105
|
+
temporary = true;
|
|
106
|
+
}
|
|
107
|
+
log(`Fetching ${url}`);
|
|
108
|
+
/*
|
|
109
|
+
--depth 1 for the reason in the header. --single-branch keeps it to the
|
|
110
|
+
default branch: an import takes a snapshot of one line of work, and
|
|
111
|
+
fetching every branch's tip would cost time to produce content this
|
|
112
|
+
command then discards.
|
|
113
|
+
*/
|
|
114
|
+
await run("git", ["clone", "--depth", "1", "--single-branch", url, folder], { windowsHide: true, maxBuffer: 32 * 1024 * 1024 });
|
|
115
|
+
const gitDirectory = node_path_1.default.join(folder, ".git");
|
|
116
|
+
if (await exists(gitDirectory)) {
|
|
117
|
+
await (0, promises_1.rm)(gitDirectory, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
return { folder, name, temporary };
|
|
120
|
+
}
|
|
121
|
+
/** Total bytes and file count of the fetched tree, for the summary line. */
|
|
122
|
+
async function measure(folder) {
|
|
123
|
+
let files = 0;
|
|
124
|
+
let bytes = 0;
|
|
125
|
+
const walk = async (directory) => {
|
|
126
|
+
for (const entry of await (0, promises_1.readdir)(directory, { withFileTypes: true })) {
|
|
127
|
+
const full = node_path_1.default.join(directory, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
await walk(full);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!entry.isFile())
|
|
133
|
+
continue;
|
|
134
|
+
files += 1;
|
|
135
|
+
bytes += (await (0, promises_1.stat)(full)).size;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
await walk(folder);
|
|
139
|
+
return { files, bytes };
|
|
140
|
+
}
|
|
141
|
+
function humanBytes(value) {
|
|
142
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
143
|
+
let size = value;
|
|
144
|
+
let unit = 0;
|
|
145
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
146
|
+
size /= 1024;
|
|
147
|
+
unit += 1;
|
|
148
|
+
}
|
|
149
|
+
return `${unit === 0 ? size : size.toFixed(size < 10 ? 2 : 1)} ${units[unit]}`;
|
|
150
|
+
}
|
|
@@ -73,7 +73,7 @@ async function commandLicence(parsed) {
|
|
|
73
73
|
console.log(` ${dim(licence.summary)}`);
|
|
74
74
|
}
|
|
75
75
|
console.log("");
|
|
76
|
-
console.log(`Add one with ${accent("
|
|
76
|
+
console.log(`Add one with ${accent("cbx licence MIT")}. ` +
|
|
77
77
|
dim(`Nothing is written until you name one.`));
|
|
78
78
|
return 0;
|
|
79
79
|
}
|
|
@@ -98,7 +98,7 @@ async function commandLicence(parsed) {
|
|
|
98
98
|
const licence = (0, licences_js_1.licenceById)(wanted);
|
|
99
99
|
if (!licence) {
|
|
100
100
|
console.error(red(`No licence called ${wanted}.`));
|
|
101
|
-
console.error(`Run ${accent("
|
|
101
|
+
console.error(`Run ${accent("cbx licence")} to see the list.`);
|
|
102
102
|
return 1;
|
|
103
103
|
}
|
|
104
104
|
/*
|
|
@@ -162,14 +162,14 @@ async function commandIgnoreTemplate(parsed) {
|
|
|
162
162
|
console.log(" " + names.slice(at, at + perRow).map((name) => name.padEnd(width)).join(""));
|
|
163
163
|
}
|
|
164
164
|
console.log("");
|
|
165
|
-
console.log(`Add one with ${accent("
|
|
165
|
+
console.log(`Add one with ${accent("cbx ignore --template Python")}. ` +
|
|
166
166
|
dim("Several is normal — a language, an editor, an operating system."));
|
|
167
167
|
return 0;
|
|
168
168
|
}
|
|
169
169
|
const template = ignore_templates_js_1.IGNORE_TEMPLATES[wanted.toLowerCase()];
|
|
170
170
|
if (!template) {
|
|
171
171
|
console.error(red(`No rules called ${wanted}.`));
|
|
172
|
-
console.error(`Run ${accent("
|
|
172
|
+
console.error(`Run ${accent("cbx ignore --template list")} to see what there is.`);
|
|
173
173
|
return 1;
|
|
174
174
|
}
|
|
175
175
|
const rules = await (0, worktree_js_1.readRules)(folder);
|
|
@@ -185,7 +185,7 @@ async function commandIgnoreTemplate(parsed) {
|
|
|
185
185
|
console.log(green(`Added ${template.name} — ${lines.length} rules.`));
|
|
186
186
|
console.log(dim(` ${node_path_1.default.join(folder, ".gitignore")}`));
|
|
187
187
|
console.log("");
|
|
188
|
-
console.log(dim(`See what it leaves behind with `) + accent("
|
|
188
|
+
console.log(dim(`See what it leaves behind with `) + accent("cbx status"));
|
|
189
189
|
return 0;
|
|
190
190
|
}
|
|
191
191
|
function flagValue(parsed, name) {
|
package/dist/cli/src/mcp.js
CHANGED
|
@@ -196,7 +196,7 @@ const TOOLS = [
|
|
|
196
196
|
throw new Error(`No file "${wanted}" in v${version.sequence}.`);
|
|
197
197
|
if (file.sourceSize > READ_LIMIT) {
|
|
198
198
|
return (`${wanted} is ${bytes(file.sourceSize)}, larger than this tool will ` +
|
|
199
|
-
`paste into a conversation. Fetch the project with \`
|
|
199
|
+
`paste into a conversation. Fetch the project with \`cbx get\` ` +
|
|
200
200
|
`and read it from disk.`);
|
|
201
201
|
}
|
|
202
202
|
/*
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.uploadRequestFor = uploadRequestFor;
|
|
4
|
+
exports.classifyPublishFailure = classifyPublishFailure;
|
|
5
|
+
/** Turn what a caller has into what the uploader expects. */
|
|
6
|
+
function uploadRequestFor(input) {
|
|
7
|
+
return {
|
|
8
|
+
localPath: input.localPath,
|
|
9
|
+
// Both lists. See the note at the top of this file.
|
|
10
|
+
include: [...input.changed, ...input.deleted],
|
|
11
|
+
deletions: input.deleted,
|
|
12
|
+
message: input.message,
|
|
13
|
+
projectName: input.projectName,
|
|
14
|
+
repositoryId: input.repositoryId,
|
|
15
|
+
baseVersionId: input.baseVersionId,
|
|
16
|
+
...(input.track ? { track: input.track } : {}),
|
|
17
|
+
...(input.allowIgnored ? { allowIgnored: true } : {}),
|
|
18
|
+
...(input.acknowledged ? { acknowledged: true } : {}),
|
|
19
|
+
...(input.acknowledgedNoLicence ? { acknowledgedNoLicence: true } : {}),
|
|
20
|
+
...(input.baseVersionId
|
|
21
|
+
? { expectedHeadVersionId: input.baseVersionId }
|
|
22
|
+
: {}),
|
|
23
|
+
...(input.known ? { known: input.known } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function classifyPublishFailure(error) {
|
|
27
|
+
const code = error?.code;
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
if (code === "merge_required" || code === "track_moved") {
|
|
30
|
+
return { kind: "conflict", message };
|
|
31
|
+
}
|
|
32
|
+
if (!code &&
|
|
33
|
+
/fetch failed|ECONNRESET|socket hang up|network|ETIMEDOUT/i.test(message)) {
|
|
34
|
+
return { kind: "interrupted", message };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
package/dist/cli/src/registry.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* command gets added and the text does not mention it, or an option is renamed
|
|
8
8
|
* and the text still lists the old one. Nobody notices, because nothing checks.
|
|
9
9
|
*
|
|
10
|
-
* Here the description is part of the command. `
|
|
10
|
+
* Here the description is part of the command. `cbx help` is generated
|
|
11
11
|
* from the same rows that decide what runs, so the two cannot disagree — and a
|
|
12
12
|
* command added without a summary is a type error rather than an omission.
|
|
13
13
|
*/
|
package/dist/cli/src/runner.js
CHANGED
|
@@ -45,7 +45,7 @@ const FLUSH_MS = 2_000;
|
|
|
45
45
|
async function call(route, options = {}) {
|
|
46
46
|
const token = await (0, config_js_1.loadToken)();
|
|
47
47
|
if (!token)
|
|
48
|
-
throw new Error("Not signed in. Run:
|
|
48
|
+
throw new Error("Not signed in. Run: cbx sign-in");
|
|
49
49
|
const response = await fetch(`${(0, config_js_1.apiOrigin)()}${route}`, {
|
|
50
50
|
method: options.method ?? "GET",
|
|
51
51
|
headers: {
|
|
@@ -127,7 +127,7 @@ class LogShipper {
|
|
|
127
127
|
*
|
|
128
128
|
* Running somebody's command on your machine is the deal a runner makes, and
|
|
129
129
|
* it is stated at every startup. Handing them the token that machine signs in
|
|
130
|
-
* with is not: `
|
|
130
|
+
* with is not: `cbx runner` reads CODEROOK_TOKEN from the environment so
|
|
131
131
|
* automated runs can supply it, and every child process inherited it. A
|
|
132
132
|
* workflow set to `env` by anyone with write access printed it straight into
|
|
133
133
|
* the run log, which read access is enough to see — so a collaborator could
|
|
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.commandIssues = commandIssues;
|
|
15
|
+
exports.commandRelease = commandRelease;
|
|
15
16
|
exports.commandReleases = commandReleases;
|
|
16
17
|
exports.commandCollaborators = commandCollaborators;
|
|
17
18
|
exports.commandWatch = commandWatch;
|
|
@@ -72,6 +73,55 @@ async function commandIssues(parsed) {
|
|
|
72
73
|
return 0;
|
|
73
74
|
}
|
|
74
75
|
/** Published releases, newest first. */
|
|
76
|
+
/**
|
|
77
|
+
* Name a version, which is what a release is.
|
|
78
|
+
*
|
|
79
|
+
* The service has had `markRelease` since releases existed, and nothing on
|
|
80
|
+
* this side could reach it — a project could be given a release by the
|
|
81
|
+
* website and by nothing else. That gap is why `git push --tags` needs this:
|
|
82
|
+
* a git tag is a name for a version, so it has to call the same thing a person
|
|
83
|
+
* calls, not a path of its own. A capability git can reach and the CLI cannot
|
|
84
|
+
* would be a feature that exists twice and agrees by luck.
|
|
85
|
+
*/
|
|
86
|
+
async function commandRelease(parsed) {
|
|
87
|
+
const name = parsed.positional[0];
|
|
88
|
+
if (!name) {
|
|
89
|
+
console.error(red("Name the release: cbx release v1.0"));
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[1]);
|
|
93
|
+
if (!project)
|
|
94
|
+
return 1;
|
|
95
|
+
const all = await (0, api_js_1.versions)(project.id);
|
|
96
|
+
if (!all.length) {
|
|
97
|
+
console.error(red("This project has no versions to release."));
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
/*
|
|
101
|
+
The newest version unless one is named. A release usually means "what I
|
|
102
|
+
have just finished", and asking for the number every time would make the
|
|
103
|
+
common case the awkward one.
|
|
104
|
+
*/
|
|
105
|
+
const wanted = parsed.flags.get("version");
|
|
106
|
+
const target = typeof wanted === "string"
|
|
107
|
+
? all.find((version) => String(version.sequence) === wanted.replace(/^v/i, ""))
|
|
108
|
+
: all[0];
|
|
109
|
+
if (!target) {
|
|
110
|
+
console.error(red(`This project has no version ${String(wanted)}.`));
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
const notes = parsed.flags.get("notes");
|
|
114
|
+
try {
|
|
115
|
+
await (0, api_js_1.markRelease)(project.id, target.id, name, typeof notes === "string" ? notes : null);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
console.log(`${green("Released")} ${bold(name)} ${dim("at")} v${target.sequence}` +
|
|
122
|
+
`${dim(" — ")}${target.message.split("\n")[0].slice(0, 48)}`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
75
125
|
async function commandReleases(parsed) {
|
|
76
126
|
const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[0]);
|
|
77
127
|
if (!project)
|
|
@@ -243,7 +293,7 @@ async function commandRuns(parsed) {
|
|
|
243
293
|
console.log(` ${dim(aside.join(" · "))}`);
|
|
244
294
|
}
|
|
245
295
|
console.log("");
|
|
246
|
-
console.log(dim(" Read one with:
|
|
296
|
+
console.log(dim(" Read one with: cbx logs <number>"));
|
|
247
297
|
return 0;
|
|
248
298
|
}
|
|
249
299
|
/**
|
|
@@ -255,7 +305,7 @@ async function commandRuns(parsed) {
|
|
|
255
305
|
async function commandLogs(parsed) {
|
|
256
306
|
const asked = parsed.positional[0];
|
|
257
307
|
if (!asked) {
|
|
258
|
-
console.error(red("Which run? Try:
|
|
308
|
+
console.error(red("Which run? Try: cbx logs 12"));
|
|
259
309
|
return 1;
|
|
260
310
|
}
|
|
261
311
|
const number = Number(asked.replace(/^#/, ""));
|
|
@@ -314,7 +364,7 @@ async function commandTokens(parsed) {
|
|
|
314
364
|
console.log(` ${token.name.padEnd(26)} ${used.padEnd(16)} ${dim(token.id)}`);
|
|
315
365
|
}
|
|
316
366
|
console.log("");
|
|
317
|
-
console.log(dim(" Revoke one with:
|
|
367
|
+
console.log(dim(" Revoke one with: cbx tokens --revoke <id>"));
|
|
318
368
|
return 0;
|
|
319
369
|
}
|
|
320
370
|
/**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* A skill is a file in a folder. The command line is already installed and
|
|
11
11
|
* already knows where that folder is, so it writes it: no marketplace, no
|
|
12
|
-
* clone, no second account. `
|
|
12
|
+
* clone, no second account. `cbx skill` and the assistant knows what
|
|
13
13
|
* CodeRook is and how to drive it from then on, in every project.
|
|
14
14
|
*
|
|
15
15
|
* The skill teaches it the command line rather than the MCP server. Both work,
|
|
@@ -106,8 +106,8 @@ async function commandSkill(parsed) {
|
|
|
106
106
|
console.log(dim("or run it by name — ") + accent("/coderook"));
|
|
107
107
|
console.log("");
|
|
108
108
|
console.log(dim("Signed in? ") +
|
|
109
|
-
bold("
|
|
109
|
+
bold("cbx whoami") +
|
|
110
110
|
dim(" · if not: ") +
|
|
111
|
-
bold("
|
|
111
|
+
bold("cbx sign-in"));
|
|
112
112
|
return 0;
|
|
113
113
|
}
|
|
@@ -88,7 +88,7 @@ async function commandTracks(parsed) {
|
|
|
88
88
|
console.log(bold("Waiting on a decision"));
|
|
89
89
|
for (const track of merges)
|
|
90
90
|
console.log(` ${describe(track, current)}`);
|
|
91
|
-
console.log(dim(" Finish one with ") + "
|
|
91
|
+
console.log(dim(" Finish one with ") + "cbx merge");
|
|
92
92
|
}
|
|
93
93
|
console.log("");
|
|
94
94
|
console.log(dim("This folder saves to ") + accent(current));
|
|
@@ -100,20 +100,20 @@ async function commandTracks(parsed) {
|
|
|
100
100
|
* Switching is a statement about where the next save goes and nothing else:
|
|
101
101
|
* no files move and nothing is fetched, which is why it is instant and why
|
|
102
102
|
* it is safe to change your mind. Getting the other line's files is
|
|
103
|
-
* `
|
|
103
|
+
* `cbx get`, deliberately a separate act.
|
|
104
104
|
*/
|
|
105
105
|
async function commandTrack(parsed) {
|
|
106
106
|
const folder = node_process_1.default.cwd();
|
|
107
107
|
const link = await (0, config_js_1.readLink)(folder);
|
|
108
108
|
if (!link) {
|
|
109
|
-
console.error(red("This folder is not linked to a project. Run ") + "
|
|
109
|
+
console.error(red("This folder is not linked to a project. Run ") + "cbx get" + red(" first."));
|
|
110
110
|
return 1;
|
|
111
111
|
}
|
|
112
112
|
const wanted = parsed.positional[0];
|
|
113
113
|
const current = link.track?.trim() || exports.DEFAULT_TRACK;
|
|
114
114
|
if (!wanted) {
|
|
115
115
|
console.log(accent(current));
|
|
116
|
-
console.log(dim("Switch with ") + "
|
|
116
|
+
console.log(dim("Switch with ") + "cbx track <name>");
|
|
117
117
|
return 0;
|
|
118
118
|
}
|
|
119
119
|
const starting = parsed.flags.has("new") || parsed.flags.has("n");
|
|
@@ -130,12 +130,12 @@ async function commandTrack(parsed) {
|
|
|
130
130
|
work somewhere nobody will look for it.
|
|
131
131
|
*/
|
|
132
132
|
console.error(red(`No line called "${wanted}" on this project.`));
|
|
133
|
-
console.error(dim("Start one with ") + `
|
|
133
|
+
console.error(dim("Start one with ") + `cbx track ${wanted} --new`);
|
|
134
134
|
return 1;
|
|
135
135
|
}
|
|
136
136
|
if (existing?.kind === "merge") {
|
|
137
137
|
console.error(red(`"${wanted}" is a merge waiting on a decision, not a line to save onto.`));
|
|
138
|
-
console.error(dim("Finish it with ") + "
|
|
138
|
+
console.error(dim("Finish it with ") + "cbx merge");
|
|
139
139
|
return 1;
|
|
140
140
|
}
|
|
141
141
|
if (starting) {
|
|
@@ -151,7 +151,7 @@ async function commandTrack(parsed) {
|
|
|
151
151
|
await (0, config_js_1.writeLink)(folder, { ...link, track: wanted });
|
|
152
152
|
console.log(dim("This folder now saves to ") + accent(wanted));
|
|
153
153
|
if (existing) {
|
|
154
|
-
console.log(dim("Its files are not here yet — fetch them with ") + `
|
|
154
|
+
console.log(dim("Its files are not here yet — fetch them with ") + `cbx get`);
|
|
155
155
|
}
|
|
156
156
|
return 0;
|
|
157
157
|
}
|
|
@@ -77,8 +77,18 @@ class Tracks {
|
|
|
77
77
|
* accept, so this passes the name through rather than pre-judging it —
|
|
78
78
|
* a rule enforced in two places is a rule that will disagree with itself.
|
|
79
79
|
*/
|
|
80
|
-
async create(repositoryId, name
|
|
81
|
-
|
|
80
|
+
async create(repositoryId, name,
|
|
81
|
+
/*
|
|
82
|
+
Where the line starts, when the caller knows better than "wherever the
|
|
83
|
+
project is now". Somebody clicking New line means from here; a git branch
|
|
84
|
+
means from the version matching the commit it forked at, which is
|
|
85
|
+
usually not the head. Omitted keeps the original behaviour exactly.
|
|
86
|
+
*/
|
|
87
|
+
fromVersionId) {
|
|
88
|
+
const body = await this.call(`/v1/repositories/${repositoryId}/tracks`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: JSON.stringify(fromVersionId ? { name, fromVersionId } : { name }),
|
|
91
|
+
});
|
|
82
92
|
return body.track;
|
|
83
93
|
}
|
|
84
94
|
/** Merges that have not been applied or abandoned. */
|
|
@@ -327,7 +327,16 @@ class Uploader {
|
|
|
327
327
|
// A version is a snapshot, not a delta, so it has to name every file in
|
|
328
328
|
// the project — not merely the ones being sent this time. Anything
|
|
329
329
|
// unchanged keeps the object the previous version already pointed at.
|
|
330
|
-
|
|
330
|
+
/*
|
|
331
|
+
An import filters nothing. The rules keep local mess out of a working
|
|
332
|
+
folder, but an imported tree is exactly what the source repository
|
|
333
|
+
tracked — there is no mess in it, and anything the source tracked in
|
|
334
|
+
spite of its own rules (which git does, for files committed before the
|
|
335
|
+
rule) would otherwise be dropped without being mentioned.
|
|
336
|
+
*/
|
|
337
|
+
const rules = request.allowIgnored
|
|
338
|
+
? { shared: "", local: "" }
|
|
339
|
+
: await (0, worktree_js_1.readRules)(request.localPath);
|
|
331
340
|
/*
|
|
332
341
|
Surveyed, not listed.
|
|
333
342
|
|
|
@@ -1152,6 +1161,7 @@ class Uploader {
|
|
|
1152
1161
|
? {}
|
|
1153
1162
|
: { expectedHeadVersionId: request.expectedHeadVersionId }),
|
|
1154
1163
|
...(request.track ? { track: request.track } : {}),
|
|
1164
|
+
...(request.allowIgnored ? { allowIgnored: true } : {}),
|
|
1155
1165
|
/*
|
|
1156
1166
|
Names this attempt so a retry after a lost connection is answered
|
|
1157
1167
|
with the version already made, rather than making a second one.
|