@coderook/cli 0.23.0 → 0.25.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.
@@ -0,0 +1,331 @@
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.commandTransfer = commandTransfer;
7
+ /**
8
+ * Moving a whole repository off a git host and onto CodeRook.
9
+ *
10
+ * `cbx import` takes a snapshot: the files as they are now, one version, no
11
+ * history. That is the right answer for "get this onto CodeRook quickly" and
12
+ * the wrong one for "move off GitHub", where the history, the branches, the
13
+ * tags and the issues are most of what somebody is worried about losing.
14
+ *
15
+ * ## It uses the public route on purpose
16
+ *
17
+ * The history goes across by running `git push` against the CodeRook remote —
18
+ * the same command anybody else would type, through the same helper. Nothing
19
+ * here reaches past it into a private path.
20
+ *
21
+ * That is deliberate, because the claim being made is interoperability. A
22
+ * transfer that worked through a back door would prove only that a back door
23
+ * exists. This one works because `git push coderook --all` works; if that ever
24
+ * breaks, this breaks with it, loudly, rather than quietly diverging from what
25
+ * users are told to do.
26
+ *
27
+ * ## What crosses, and what cannot
28
+ *
29
+ * Commits, branches, tags and the project's own description come across.
30
+ * Issues come across when a read token is supplied. Pull requests, reviews,
31
+ * CI configuration, stars and collaborator lists do not — some because
32
+ * CodeRook has no such object, and one because it would be wrong: adding
33
+ * somebody to a project is an invitation they have to accept, not a field to
34
+ * copy. The report at the end says which is which rather than leaving the
35
+ * absence to be discovered.
36
+ *
37
+ * ## Tokens
38
+ *
39
+ * Read from the environment, never from a flag and never stored. A read-only
40
+ * token is enough, and no OAuth flow is offered: holding somebody's forge
41
+ * credentials to run a one-off migration sits badly beside a front page that
42
+ * promises their work is not handed to anybody.
43
+ */
44
+ const node_child_process_1 = require("node:child_process");
45
+ const promises_1 = require("node:fs/promises");
46
+ const node_os_1 = __importDefault(require("node:os"));
47
+ const node_path_1 = __importDefault(require("node:path"));
48
+ const node_process_1 = __importDefault(require("node:process"));
49
+ const api_js_1 = require("./api.js");
50
+ const forge_url_js_1 = require("./forge_url.js");
51
+ const dim = (value) => `${value}`;
52
+ const bold = (value) => `${value}`;
53
+ const red = (value) => `${value}`;
54
+ const green = (value) => `${value}`;
55
+ const accent = (value) => `${value}`;
56
+ /** A read token from the environment, or nothing. Never a flag. */
57
+ function forgeToken(forge) {
58
+ for (const name of forge.tokenNames) {
59
+ const value = node_process_1.default.env[name]?.trim();
60
+ if (value)
61
+ return value;
62
+ }
63
+ return null;
64
+ }
65
+ async function readJson(url, token, forge) {
66
+ const headers = {
67
+ accept: "application/json",
68
+ "user-agent": "cbx-transfer",
69
+ };
70
+ if (token) {
71
+ headers.authorization =
72
+ forge.kind === "gitlab" ? `Bearer ${token}` : `token ${token}`;
73
+ }
74
+ const response = await fetch(url, { headers });
75
+ if (!response.ok) {
76
+ throw new Error(`${response.status} from ${new URL(url).pathname}`);
77
+ }
78
+ return response.json();
79
+ }
80
+ /** What the forge says about the repository itself. */
81
+ async function describeSource(forge, token) {
82
+ const encoded = encodeURIComponent(`${forge.owner}/${forge.repo}`);
83
+ const url = forge.kind === "github"
84
+ ? `https://api.${forge.host}/repos/${forge.owner}/${forge.repo}`
85
+ : forge.kind === "gitlab"
86
+ ? `https://${forge.host}/api/v4/projects/${encoded}`
87
+ : `https://${forge.host}/api/v1/repos/${forge.owner}/${forge.repo}`;
88
+ try {
89
+ const body = (await readJson(url, token, forge));
90
+ const isPrivate = body.private === true || body.visibility === "private" || body.internal === true;
91
+ return {
92
+ description: String(body.description ?? "").slice(0, 1000),
93
+ visibility: isPrivate ? "private" : "public",
94
+ defaultBranch: body.default_branch ?? null,
95
+ };
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ /** Open issues on the source, oldest first so numbering reads sensibly. */
102
+ async function readIssues(forge, token) {
103
+ const encoded = encodeURIComponent(`${forge.owner}/${forge.repo}`);
104
+ const url = forge.kind === "github"
105
+ ? `https://api.${forge.host}/repos/${forge.owner}/${forge.repo}/issues?state=open&per_page=100`
106
+ : forge.kind === "gitlab"
107
+ ? `https://${forge.host}/api/v4/projects/${encoded}/issues?state=opened&per_page=100`
108
+ : `https://${forge.host}/api/v1/repos/${forge.owner}/${forge.repo}/issues?state=open&limit=100`;
109
+ const body = (await readJson(url, token, forge));
110
+ return body
111
+ /*
112
+ GitHub returns pull requests through the issues endpoint. They are not
113
+ issues, and CodeRook has nothing that a pull request becomes, so copying
114
+ them across as issues would be inventing content rather than moving it.
115
+ */
116
+ .filter((one) => !one.pull_request)
117
+ .map((one) => ({
118
+ title: String(one.title ?? "").slice(0, 200),
119
+ body: String(one.body ?? one.description ?? "").slice(0, 20_000),
120
+ number: Number(one.number ?? one.iid ?? 0),
121
+ closed: false,
122
+ }))
123
+ .filter((one) => one.title)
124
+ .reverse();
125
+ }
126
+ function run(command, args, cwd) {
127
+ const result = (0, node_child_process_1.spawnSync)(command, args, {
128
+ cwd,
129
+ encoding: "utf8",
130
+ maxBuffer: 1024 * 1024 * 64,
131
+ });
132
+ return {
133
+ ok: result.status === 0,
134
+ out: `${result.stdout ?? ""}${result.stderr ?? ""}`,
135
+ };
136
+ }
137
+ async function commandTransfer(parsed) {
138
+ const address = parsed.positional[0];
139
+ if (!address) {
140
+ console.error(red("Which repository? Try: cbx transfer https://github.com/owner/project"));
141
+ return 1;
142
+ }
143
+ let forge;
144
+ try {
145
+ forge = (0, forge_url_js_1.readForgeUrl)(address);
146
+ }
147
+ catch (error) {
148
+ console.error(red(error instanceof Error ? error.message : String(error)));
149
+ return 1;
150
+ }
151
+ if (!run("git", ["--version"]).ok) {
152
+ console.error(red("Transferring needs git on this machine."));
153
+ return 1;
154
+ }
155
+ /*
156
+ The push goes through the remote helper, which git finds on PATH by name.
157
+ Checking now turns a confusing failure halfway through a clone into one
158
+ sentence before anything is downloaded.
159
+ */
160
+ if (!run("git", ["remote-coderook", "--probe"]).ok) {
161
+ const help = run("git", ["help", "-a"]);
162
+ if (!/remote-coderook/.test(help.out)) {
163
+ console.error(red("git cannot find `git-remote-coderook` on this machine."));
164
+ console.error("It ships with this tool, so this usually means cbx was run from a\n" +
165
+ "checkout rather than installed. Install it globally and try again:\n" +
166
+ ` ${accent("npm install --global @coderook/cli")}`);
167
+ return 1;
168
+ }
169
+ }
170
+ const wanted = (typeof parsed.flags.get("name") === "string"
171
+ ? String(parsed.flags.get("name"))
172
+ : forge.repo).toLowerCase();
173
+ const existing = await (0, api_js_1.findProject)(wanted);
174
+ if (existing) {
175
+ console.error(red(`You already have a project called ${existing.slug}.`));
176
+ console.error(`Transfer creates a project; it does not merge into one. Choose another\n` +
177
+ `name with ${accent("--name")}, or delete that project first.`);
178
+ return 1;
179
+ }
180
+ const token = forgeToken(forge);
181
+ console.log(`${bold("Transferring")} ${forge.owner}/${forge.repo} ${dim(`from ${forge.label}`)}`);
182
+ console.log(dim(token
183
+ ? ` Using the read token in ${forge.tokenNames.find((name) => node_process_1.default.env[name]?.trim())}.`
184
+ : ` No token set. Public repositories work; set ${forge.tokenNames[0]} for a private one or for issues.`));
185
+ const scratch = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "cbx-transfer-"));
186
+ const checkout = node_path_1.default.join(scratch, forge.repo);
187
+ const report = [];
188
+ try {
189
+ /*
190
+ A full clone, not the shallow one `import` uses. The whole point here is
191
+ the history, and `--depth 1` would silently deliver a single commit under
192
+ a command whose name promises everything.
193
+ */
194
+ console.log(`\n${dim("Fetching the repository with its full history…")}`);
195
+ const cloned = run("git", ["clone", "--no-single-branch", forge.clone, checkout]);
196
+ if (!cloned.ok) {
197
+ console.error(red(`\nCould not clone ${forge.clone}`));
198
+ console.error(cloned.out.trim().split("\n").slice(-4).join("\n"));
199
+ if (!token) {
200
+ console.error(dim(`\nIf it is private, set ${forge.tokenNames[0]} and try again.`));
201
+ }
202
+ return 1;
203
+ }
204
+ /*
205
+ Every branch, not only the one checked out. A fresh clone has remote
206
+ tracking refs for the rest, and nothing local pointing at them, so
207
+ `--all` would push exactly one branch.
208
+ */
209
+ const remoteBranches = run("git", [
210
+ "for-each-ref", "--format=%(refname:short)", "refs/remotes/origin",
211
+ ], checkout).out
212
+ .split("\n").map((one) => one.trim()).filter(Boolean)
213
+ /*
214
+ The remote's HEAD symref comes back as plain `origin`, not
215
+ `origin/HEAD`, so filtering on the latter leaves a phantom entry that
216
+ inflates the branch count and sends git looking for `origin/origin`.
217
+ */
218
+ .filter((one) => one.startsWith("origin/"))
219
+ .map((one) => one.slice("origin/".length))
220
+ .filter((one) => one && one !== "HEAD");
221
+ for (const branch of remoteBranches) {
222
+ run("git", ["branch", "--force", branch, `origin/${branch}`], checkout);
223
+ }
224
+ const commits = Number(run("git", ["rev-list", "--all", "--count"], checkout).out.trim() || "0");
225
+ const tags = run("git", ["tag", "-l"], checkout).out.split("\n").filter(Boolean);
226
+ console.log(` ${commits} commit${commits === 1 ? "" : "s"} · ` +
227
+ `${remoteBranches.length} branch${remoteBranches.length === 1 ? "" : "es"} · ` +
228
+ `${tags.length} tag${tags.length === 1 ? "" : "s"}`);
229
+ if (commits > 200) {
230
+ console.log(dim(` Each commit becomes a version, so this will take a while —\n` +
231
+ ` roughly ${Math.round((commits * 11) / 60)} minutes. It resumes if interrupted.`));
232
+ }
233
+ run("git", ["remote", "add", "coderook", `coderook://${wanted}`], checkout);
234
+ console.log(`\n${dim("Publishing the history…")}`);
235
+ const pushed = run("git", ["push", "coderook", "--all"], checkout);
236
+ node_process_1.default.stderr.write(pushed.out);
237
+ if (!pushed.ok) {
238
+ console.error(red("\nThe history did not transfer completely."));
239
+ return 1;
240
+ }
241
+ report.push(`${commits} commits across ${remoteBranches.length} branch(es)`);
242
+ if (tags.length) {
243
+ const pushedTags = run("git", ["push", "coderook", "--tags"], checkout);
244
+ node_process_1.default.stderr.write(pushedTags.out);
245
+ report.push(pushedTags.ok
246
+ ? `${tags.length} tag(s) as releases`
247
+ : `tags were refused — see above`);
248
+ }
249
+ const project = await (0, api_js_1.findProject)(wanted);
250
+ if (!project) {
251
+ console.error(red("\nThe project was not created. Nothing else was changed."));
252
+ return 1;
253
+ }
254
+ // What the repository says about itself.
255
+ const facts = await describeSource(forge, token);
256
+ if (facts) {
257
+ const asked = parsed.flags.get("visibility");
258
+ /*
259
+ Private unless asked otherwise, even when the source is public.
260
+ Mirroring visibility is the faithful thing and publishing somebody's
261
+ code by side effect is the unrecoverable thing, so the safe reading
262
+ wins and the report says what was chosen.
263
+ */
264
+ const visibility = asked === "same"
265
+ ? facts.visibility
266
+ : asked === "public"
267
+ ? "public"
268
+ : "private";
269
+ await (0, api_js_1.updateProject)(project.id, {
270
+ ...(facts.description ? { description: facts.description } : {}),
271
+ visibility,
272
+ });
273
+ report.push(`description and visibility (${visibility}` +
274
+ `${visibility !== facts.visibility ? `, source was ${facts.visibility}` : ""})`);
275
+ /*
276
+ Said rather than left to be noticed. Creating a project always makes a
277
+ `main` line, so a repository whose default is anything else arrives
278
+ with an empty one beside its real branches — and `cbx tracks` marks it
279
+ as the current line, which reads as though the transfer lost the work.
280
+ */
281
+ if (facts.defaultBranch && facts.defaultBranch !== "main") {
282
+ report.push(`default line is ${facts.defaultBranch}; the empty "main" beside it ` +
283
+ `was made when the project was created`);
284
+ }
285
+ }
286
+ else {
287
+ report.push("description could not be read from the source");
288
+ }
289
+ // Issues, only when asked and only with a token.
290
+ if (parsed.flags.has("issues")) {
291
+ if (!token) {
292
+ report.push(`issues skipped — set ${forge.tokenNames[0]} and run with --issues again`);
293
+ }
294
+ else {
295
+ try {
296
+ const issues = await readIssues(forge, token);
297
+ let made = 0;
298
+ for (const issue of issues) {
299
+ await (0, api_js_1.createIssue)(project.id, {
300
+ title: issue.title,
301
+ body: `${issue.body}\n\n` +
302
+ `— transferred from ${forge.label} ${forge.owner}/${forge.repo}#${issue.number}`,
303
+ });
304
+ made += 1;
305
+ }
306
+ report.push(`${made} open issue(s)`);
307
+ }
308
+ catch (error) {
309
+ report.push(`issues failed: ${error instanceof Error ? error.message : String(error)}`);
310
+ }
311
+ }
312
+ }
313
+ console.log(`\n${green("Transferred")} ${bold(project.slug)}`);
314
+ for (const line of report)
315
+ console.log(` ${green("·")} ${line}`);
316
+ console.log(`\n${dim("Not carried across:")}`);
317
+ console.log(dim(" · pull requests and reviews — CodeRook has no equivalent object"));
318
+ console.log(dim(" · CI configuration — runs are started deliberately, not by a push"));
319
+ console.log(dim(" · collaborators — access is an invitation to accept, not a field to copy"));
320
+ console.log(dim(" · stars, forks and watchers"));
321
+ if (!parsed.flags.has("issues")) {
322
+ console.log(dim(" · issues — pass --issues to bring the open ones"));
323
+ }
324
+ console.log(`\nFetch it anywhere with ${accent(`cbx clone ${project.slug}`)}` +
325
+ `${dim(", or ")}${accent(`git clone coderook://${project.slug}`)}${dim(".")}`);
326
+ return 0;
327
+ }
328
+ finally {
329
+ await (0, promises_1.rm)(scratch, { recursive: true, force: true });
330
+ }
331
+ }
@@ -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
- const body = await this.call(`/v1/repositories/${repositoryId}/tracks`, { method: "POST", body: JSON.stringify({ name }) });
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "CodeRook from the command line, on any operating system",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",
@@ -17,7 +17,9 @@
17
17
  "node": ">=20.11.0"
18
18
  },
19
19
  "bin": {
20
- "coderook": "dist/cli/src/cli.js"
20
+ "cbx": "dist/cli/src/cli.js",
21
+ "coderook": "dist/cli/src/cli.js",
22
+ "git-remote-coderook": "dist/cli/src/git_remote_bin.js"
21
23
  },
22
24
  "files": [
23
25
  ".claude-plugin",
@@ -34,7 +36,7 @@
34
36
  "test:matrix": "node test/state-matrix.mjs",
35
37
  "test:attempt": "node test/attempt-identity.mjs",
36
38
  "test:get": "node test/get-safety.mjs",
37
- "test:live": "node test/e2e.mjs --allow-production && node test/state-matrix.mjs --allow-production && node test/get-safety.mjs --allow-production && node test/get-protects-edits.mjs --allow-production && node test/upgrade-migration.mjs --allow-production && node test/fault-get.mjs --allow-production && node test/version-floor.mjs --allow-production && node test/fault-submit.mjs --allow-production && node test/race-attempts.mjs --allow-production && node test/attempt-identity.mjs --allow-production",
39
+ "test:live": "node test/e2e.mjs --allow-production && node test/state-matrix.mjs --allow-production && node test/get-safety.mjs --allow-production && node test/get-track.mjs --allow-production && node test/get-protects-edits.mjs --allow-production && node test/upgrade-migration.mjs --allow-production && node test/fault-get.mjs --allow-production && node test/version-floor.mjs --allow-production && node test/fault-submit.mjs --allow-production && node test/race-attempts.mjs --allow-production && node test/attempt-identity.mjs --allow-production",
38
40
  "test:getedits": "node test/get-protects-edits.mjs",
39
41
  "test:upgrade": "node test/upgrade-migration.mjs",
40
42
  "test:faultget": "node test/fault-get.mjs",
@@ -42,7 +44,8 @@
42
44
  "test:faultsubmit": "node test/fault-submit.mjs",
43
45
  "test:race": "node test/race-attempts.mjs",
44
46
  "test:runner": "node test/runner-live.mjs",
45
- "sync:plugin": "node scripts/sync-plugin-version.mjs"
47
+ "sync:plugin": "node scripts/sync-plugin-version.mjs",
48
+ "test:track": "node test/get-track.mjs"
46
49
  },
47
50
  "devDependencies": {
48
51
  "@types/node": "24.10.1",
@@ -1,130 +1,130 @@
1
- ---
2
- name: coderook
3
- description: Save, browse and restore versions of a project on CodeRook — a version host where every version is a complete snapshot and there is no git to learn. Use when asked to save or submit work to CodeRook, check what has changed, look at a project's versions or files, fetch a project, start or switch a line of work, or resolve a save that landed at the same time as somebody else's.
4
- allowed-tools: Bash Read
5
- ---
6
-
7
- # CodeRook
8
-
9
- CodeRook stores whole snapshots of a folder. A version names every file the
10
- project had at that moment, so restoring one never depends on the versions
11
- around it, and there is no staging area, no branches to rebase and no history
12
- to rewrite.
13
-
14
- Everything here is the `coderook` command line. Run it with Bash.
15
-
16
- ## Before anything else
17
-
18
- `coderook whoami` says who this machine is signed in as. If it refuses, the
19
- person needs to run `coderook sign-in` themselves — it takes a personal access
20
- token, so do not attempt it on their behalf.
21
-
22
- If the command is not found at all, this machine has the skill but not the
23
- command line. Either install it once with
24
- `npm install --global @coderook/cli`, or put `npx -y @coderook/cli` where
25
- `coderook` appears below — the commands are identical, npx is just slower
26
- to start.
27
-
28
- ## Reading
29
-
30
- ```bash
31
- coderook status # what has changed in this folder since its last version
32
- coderook projects # every project on the account
33
- coderook versions [project] # what has been saved, newest first
34
- coderook tracks [project] # the lines a project has, and any waiting on a decision
35
- ```
36
-
37
- `status` is the one to reach for when somebody asks what is uncommitted, what
38
- changed, or whether anything needs saving. It reads the folder and changes
39
- nothing.
40
-
41
- ## Saving
42
-
43
- ```bash
44
- coderook submit -m "What changed, in a sentence"
45
- ```
46
-
47
- Send only what changed; the version still names every file. Write the message
48
- yourself from the actual diff rather than asking for one — a message like
49
- "update" helps nobody reading the history later.
50
-
51
- **Ask before running this.** Saving is the one thing here that leaves a mark on
52
- somebody's account, and a version saved by mistake is a version they have to
53
- explain. Propose it, say what it would send, and let them agree.
54
-
55
- `coderook submit -n` shows exactly what would be sent without sending it. Use
56
- that freely; it is safe and it is the honest way to answer "what would this
57
- upload?".
58
-
59
- ## Fetching
60
-
61
- ```bash
62
- coderook get # bring this folder up to date
63
- coderook clone <project> [dir] # fetch a project into a new folder
64
- ```
65
-
66
- `get` protects local edits by default. `coderook get --replace` discards them
67
- to make an exact copy — only run it when the person has said so in those terms.
68
-
69
- ## Lines of work
70
-
71
- A project can have more than one line, so two people can save without one
72
- landing on top of the other.
73
-
74
- ```bash
75
- coderook track # which line this folder saves to
76
- coderook track spike --new # start a line and switch to it
77
- coderook track main # switch back
78
- coderook submit --track spike -m "…"
79
- ```
80
-
81
- Switching says where the next save goes and nothing else — no files move.
82
- Run `coderook get` afterwards to bring that line's files in.
83
-
84
- ## When two saves collide
85
-
86
- If somebody saved while this folder was behind, the second save becomes a merge
87
- waiting on a decision rather than overwriting anything.
88
-
89
- ```bash
90
- coderook merges # this folder's saves waiting on a decision
91
- coderook merge <ref> # look at one, and decide
92
- ```
93
-
94
- Read the conflict out to the person and let them choose. Do not pick a side for
95
- them: the whole reason it stopped is that the service could not tell which copy
96
- was wanted.
97
-
98
- ## What is worth leaving out
99
-
100
- ```bash
101
- coderook ignore --suggest # dependency directories, build output, virtual environments
102
- coderook ignore --suggest --apply # add the confident ones to .gitignore
103
- ```
104
-
105
- Rules live in the project's own `.gitignore`, so CodeRook, git and the website
106
- all read one file.
107
-
108
- ## Bundles
109
-
110
- ```bash
111
- coderook bundle . project.cbx # the whole project, every version, as one file
112
- coderook inspect project.cbx # what is inside, without unpacking
113
- coderook unbundle project.cbx ./restored
114
- ```
115
-
116
- ## Things not to do
117
-
118
- - Do not run `coderook delete` unless the person has asked for that project to
119
- be deleted, by name, in this conversation.
120
- - Do not run `coderook sign-out`. It costs them a token they have to fetch
121
- again and gains nothing.
122
- - Do not guess a project name. `coderook projects` lists them; a folder that is
123
- already linked needs no name at all.
124
-
125
- ## If something refuses
126
-
127
- The commands explain themselves — pass the message on rather than rewording it.
128
- A refusal that says a token was not accepted means signing in; one that says a
129
- project does not allow machines means its owner turned that off deliberately,
130
- and the answer is to say so, not to find another route in.
1
+ ---
2
+ name: coderook
3
+ description: Save, browse and restore versions of a project on CodeRook — a version host where every version is a complete snapshot and there is no git to learn. Use when asked to save or submit work to CodeRook, check what has changed, look at a project's versions or files, fetch a project, start or switch a line of work, or resolve a save that landed at the same time as somebody else's.
4
+ allowed-tools: Bash Read
5
+ ---
6
+
7
+ # CodeRook
8
+
9
+ CodeRook stores whole snapshots of a folder. A version names every file the
10
+ project had at that moment, so restoring one never depends on the versions
11
+ around it, and there is no staging area, no branches to rebase and no history
12
+ to rewrite.
13
+
14
+ Everything here is the `coderook` command line. Run it with Bash.
15
+
16
+ ## Before anything else
17
+
18
+ `cbx whoami` says who this machine is signed in as. If it refuses, the
19
+ person needs to run `cbx sign-in` themselves — it takes a personal access
20
+ token, so do not attempt it on their behalf.
21
+
22
+ If the command is not found at all, this machine has the skill but not the
23
+ command line. Either install it once with
24
+ `npm install --global @coderook/cli`, or put `npx -y @coderook/cli` where
25
+ `coderook` appears below — the commands are identical, npx is just slower
26
+ to start.
27
+
28
+ ## Reading
29
+
30
+ ```bash
31
+ cbx status # what has changed in this folder since its last version
32
+ cbx projects # every project on the account
33
+ cbx versions [project] # what has been saved, newest first
34
+ cbx tracks [project] # the lines a project has, and any waiting on a decision
35
+ ```
36
+
37
+ `status` is the one to reach for when somebody asks what is uncommitted, what
38
+ changed, or whether anything needs saving. It reads the folder and changes
39
+ nothing.
40
+
41
+ ## Saving
42
+
43
+ ```bash
44
+ cbx submit -m "What changed, in a sentence"
45
+ ```
46
+
47
+ Send only what changed; the version still names every file. Write the message
48
+ yourself from the actual diff rather than asking for one — a message like
49
+ "update" helps nobody reading the history later.
50
+
51
+ **Ask before running this.** Saving is the one thing here that leaves a mark on
52
+ somebody's account, and a version saved by mistake is a version they have to
53
+ explain. Propose it, say what it would send, and let them agree.
54
+
55
+ `cbx submit -n` shows exactly what would be sent without sending it. Use
56
+ that freely; it is safe and it is the honest way to answer "what would this
57
+ upload?".
58
+
59
+ ## Fetching
60
+
61
+ ```bash
62
+ cbx get # bring this folder up to date
63
+ cbx clone <project> [dir] # fetch a project into a new folder
64
+ ```
65
+
66
+ `get` protects local edits by default. `cbx get --replace` discards them
67
+ to make an exact copy — only run it when the person has said so in those terms.
68
+
69
+ ## Lines of work
70
+
71
+ A project can have more than one line, so two people can save without one
72
+ landing on top of the other.
73
+
74
+ ```bash
75
+ cbx track # which line this folder saves to
76
+ cbx track spike --new # start a line and switch to it
77
+ cbx track main # switch back
78
+ cbx submit --track spike -m "…"
79
+ ```
80
+
81
+ Switching says where the next save goes and nothing else — no files move.
82
+ Run `cbx get` afterwards to bring that line's files in.
83
+
84
+ ## When two saves collide
85
+
86
+ If somebody saved while this folder was behind, the second save becomes a merge
87
+ waiting on a decision rather than overwriting anything.
88
+
89
+ ```bash
90
+ cbx merges # this folder's saves waiting on a decision
91
+ cbx merge <ref> # look at one, and decide
92
+ ```
93
+
94
+ Read the conflict out to the person and let them choose. Do not pick a side for
95
+ them: the whole reason it stopped is that the service could not tell which copy
96
+ was wanted.
97
+
98
+ ## What is worth leaving out
99
+
100
+ ```bash
101
+ cbx ignore --suggest # dependency directories, build output, virtual environments
102
+ cbx ignore --suggest --apply # add the confident ones to .gitignore
103
+ ```
104
+
105
+ Rules live in the project's own `.gitignore`, so CodeRook, git and the website
106
+ all read one file.
107
+
108
+ ## Bundles
109
+
110
+ ```bash
111
+ cbx bundle . project.cbx # the whole project, every version, as one file
112
+ cbx inspect project.cbx # what is inside, without unpacking
113
+ cbx unbundle project.cbx ./restored
114
+ ```
115
+
116
+ ## Things not to do
117
+
118
+ - Do not run `cbx delete` unless the person has asked for that project to
119
+ be deleted, by name, in this conversation.
120
+ - Do not run `cbx sign-out`. It costs them a token they have to fetch
121
+ again and gains nothing.
122
+ - Do not guess a project name. `cbx projects` lists them; a folder that is
123
+ already linked needs no name at all.
124
+
125
+ ## If something refuses
126
+
127
+ The commands explain themselves — pass the message on rather than rewording it.
128
+ A refusal that says a token was not accepted means signing in; one that says a
129
+ project does not allow machines means its owner turned that off deliberately,
130
+ and the answer is to say so, not to find another route in.