@coderook/cli 0.31.3 → 0.31.4
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/cli.js +14 -1
- package/dist/cli/src/git_remote.js +26 -4
- package/dist/desktop-app/src/main/cbx.js +6 -16
- package/dist/desktop-app/src/main/download.js +19 -17
- package/dist/desktop-app/src/shared/safe_path.js +88 -0
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +200 -154
|
@@ -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, from Claude Code.",
|
|
5
|
-
"version": "0.31.
|
|
5
|
+
"version": "0.31.4",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -39,6 +39,7 @@ const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
|
|
|
39
39
|
const rules_js_1 = require("../../desktop-app/src/main/rules.js");
|
|
40
40
|
const upload_js_1 = require("../../desktop-app/src/main/upload.js");
|
|
41
41
|
const download_js_1 = require("../../desktop-app/src/main/download.js");
|
|
42
|
+
const safe_path_js_1 = require("../../desktop-app/src/shared/safe_path.js");
|
|
42
43
|
const faults_js_1 = require("../../desktop-app/src/main/faults.js");
|
|
43
44
|
const version_commands_js_1 = require("./version_commands.js");
|
|
44
45
|
const identify_js_1 = require("../../desktop-app/src/main/identify.js");
|
|
@@ -1477,7 +1478,19 @@ async function commandUnbundle(parsed) {
|
|
|
1477
1478
|
return 1;
|
|
1478
1479
|
}
|
|
1479
1480
|
const manifest = await (0, cbx_js_1.readManifest)(node_path_1.default.resolve(source));
|
|
1480
|
-
|
|
1481
|
+
/*
|
|
1482
|
+
The bundle's own name, never its path. `source_name` comes from the file
|
|
1483
|
+
being opened, and taken whole it could be `/home/you/.ssh` or `C:\Users` —
|
|
1484
|
+
and the folder it names is deleted and replaced. Only the last name
|
|
1485
|
+
survives, into the current folder, and only if it is a plain name.
|
|
1486
|
+
*/
|
|
1487
|
+
const suggested = node_path_1.default.posix.basename((manifest.source_name ?? "").replace(/\\/g, "/"));
|
|
1488
|
+
const plainName = suggested && !(0, safe_path_js_1.whyUnsafe)(suggested) && !/^[.]+$/.test(suggested) && !/[:]/.test(suggested);
|
|
1489
|
+
if (!parsed.positional[1] && !plainName) {
|
|
1490
|
+
console.error(red("That bundle does not name a usable folder. Try: cbx unbundle <file.cbx> <folder>"));
|
|
1491
|
+
return 1;
|
|
1492
|
+
}
|
|
1493
|
+
const destination = node_path_1.default.resolve(parsed.positional[1] ?? suggested);
|
|
1481
1494
|
const line = progressLine();
|
|
1482
1495
|
const result = await (0, cbx_js_1.unpackBundle)(node_path_1.default.resolve(source), destination, (progress) => {
|
|
1483
1496
|
line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
|
|
@@ -109,6 +109,7 @@ exports.main = main;
|
|
|
109
109
|
* where the original branched.
|
|
110
110
|
*/
|
|
111
111
|
const node_child_process_1 = require("node:child_process");
|
|
112
|
+
const safe_path_js_1 = require("../../desktop-app/src/shared/safe_path.js");
|
|
112
113
|
const promises_1 = require("node:fs/promises");
|
|
113
114
|
const node_os_1 = __importDefault(require("node:os"));
|
|
114
115
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -136,6 +137,17 @@ class ImportFailed extends Error {
|
|
|
136
137
|
class AlreadyReported extends Error {
|
|
137
138
|
}
|
|
138
139
|
exports.AlreadyReported = AlreadyReported;
|
|
140
|
+
/**
|
|
141
|
+
* A name as fast-import may carry it on an author, committer or tagger line.
|
|
142
|
+
*
|
|
143
|
+
* Every other line of the stream is built from values the helper made; these
|
|
144
|
+
* come from what people typed. A newline in a display name ended the line and
|
|
145
|
+
* began another — any command the stream accepts, a symlink or a ref update
|
|
146
|
+
* included — and `<` or `>` would end the name early. Each becomes a space.
|
|
147
|
+
*/
|
|
148
|
+
function identity(name) {
|
|
149
|
+
return name.replace(/[\x00-\x1f\x7f<>]/g, " ").replace(/\s+/g, " ").trim() || "CodeRook";
|
|
150
|
+
}
|
|
139
151
|
/** Everything the helper writes for a person goes to stderr; stdout is protocol. */
|
|
140
152
|
function say(text) {
|
|
141
153
|
node_process_1.default.stderr.write(`${text}\n`);
|
|
@@ -808,8 +820,8 @@ depth = null) {
|
|
|
808
820
|
worse than an obviously synthetic one: it survives being copied into a
|
|
809
821
|
mailing list or a CONTRIBUTORS file.
|
|
810
822
|
*/
|
|
811
|
-
send(`author ${author} <noreply@coderook.com> ${when}`);
|
|
812
|
-
send(`committer ${author} <noreply@coderook.com> ${when}`);
|
|
823
|
+
send(`author ${identity(author)} <noreply@coderook.com> ${when}`);
|
|
824
|
+
send(`committer ${identity(author)} <noreply@coderook.com> ${when}`);
|
|
813
825
|
send(`data ${body.length}`);
|
|
814
826
|
node_process_1.default.stdout.write(body);
|
|
815
827
|
node_process_1.default.stdout.write("\n");
|
|
@@ -819,6 +831,13 @@ depth = null) {
|
|
|
819
831
|
send(`merge :${extra}`);
|
|
820
832
|
send("deleteall");
|
|
821
833
|
for (const file of files) {
|
|
834
|
+
/*
|
|
835
|
+
Never into git's own directory. Git would refuse to check one out,
|
|
836
|
+
but it would sit in the history of every clone; new versions cannot
|
|
837
|
+
hold one, and this keeps older ones from carrying it into git.
|
|
838
|
+
*/
|
|
839
|
+
if (file.path.split("/").some(safe_path_js_1.isGitDirectoryName))
|
|
840
|
+
continue;
|
|
822
841
|
send(`M 100644 :${blobMark.get(file.sha256)} ${(0, git_history_js_1.quotePath)(file.path)}`);
|
|
823
842
|
}
|
|
824
843
|
send("");
|
|
@@ -880,9 +899,12 @@ depth = null) {
|
|
|
880
899
|
if (mark === undefined)
|
|
881
900
|
continue;
|
|
882
901
|
const body = Buffer.from(release.notes || release.name, "utf8");
|
|
883
|
-
|
|
902
|
+
const tagName = release.name.replace(/[\s~^:?*[\\\x00-\x1f]/g, "-");
|
|
903
|
+
if (!tagName || tagName.startsWith("-") || tagName.includes(".."))
|
|
904
|
+
continue;
|
|
905
|
+
send(`tag ${tagName}`);
|
|
884
906
|
send(`from :${mark}`);
|
|
885
|
-
send(`tagger ${release.releasedBy || "CodeRook"} <noreply@coderook.com> ` +
|
|
907
|
+
send(`tagger ${identity(release.releasedBy || "CodeRook")} <noreply@coderook.com> ` +
|
|
886
908
|
`${(0, git_history_js_1.stamp)(release.releasedAt)} +0000`);
|
|
887
909
|
send(`data ${body.length}`);
|
|
888
910
|
node_process_1.default.stdout.write(body);
|
|
@@ -29,6 +29,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
29
29
|
const node_fs_1 = require("node:fs");
|
|
30
30
|
const promises_1 = require("node:fs/promises");
|
|
31
31
|
const node_path_1 = __importDefault(require("node:path"));
|
|
32
|
+
const safe_path_js_1 = require("../shared/safe_path.js");
|
|
32
33
|
const node_zlib_1 = require("node:zlib");
|
|
33
34
|
const download_js_1 = require("./download.js");
|
|
34
35
|
const chunking_js_1 = require("../shared/chunking.js");
|
|
@@ -343,9 +344,9 @@ async function unpackBundle(source, destination, report) {
|
|
|
343
344
|
let written = 0;
|
|
344
345
|
let bytes = 0;
|
|
345
346
|
for (const directory of manifest.directories ?? []) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
347
|
+
if (!directory || directory === ".")
|
|
348
|
+
continue;
|
|
349
|
+
await (0, promises_1.mkdir)((0, safe_path_js_1.inside)(staging, directory), { recursive: true });
|
|
349
350
|
}
|
|
350
351
|
for (const record of manifest.files) {
|
|
351
352
|
report?.({
|
|
@@ -356,10 +357,8 @@ async function unpackBundle(source, destination, report) {
|
|
|
356
357
|
path: record.path,
|
|
357
358
|
percent: (0, download_js_1.underway)(bytes, totalBytes),
|
|
358
359
|
});
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
throw new Error(`Unsafe path in bundle: ${record.path}`);
|
|
362
|
-
const full = node_path_1.default.join(staging, ...parts);
|
|
360
|
+
/* Refuses `..`, absolute paths and git's own directory; see safe_path.ts. */
|
|
361
|
+
const full = (0, safe_path_js_1.inside)(staging, record.path);
|
|
363
362
|
await (0, promises_1.mkdir)(node_path_1.default.dirname(full), { recursive: true });
|
|
364
363
|
const pieces = [];
|
|
365
364
|
if (record.segments) {
|
|
@@ -405,15 +404,6 @@ async function unpackBundle(source, destination, report) {
|
|
|
405
404
|
await handle.close();
|
|
406
405
|
}
|
|
407
406
|
}
|
|
408
|
-
/** Reject anything that would escape the destination before it is used. */
|
|
409
|
-
function safeParts(relative) {
|
|
410
|
-
const parts = relative.split(/[\\/]/).filter((part) => part && part !== ".");
|
|
411
|
-
if (parts.some((part) => part === ".." || part.includes("\0")))
|
|
412
|
-
return [];
|
|
413
|
-
if (node_path_1.default.isAbsolute(relative) || /^[a-zA-Z]:/.test(relative))
|
|
414
|
-
return [];
|
|
415
|
-
return parts;
|
|
416
|
-
}
|
|
417
407
|
/** Every file under `root`, relative and POSIX-separated, sorted. */
|
|
418
408
|
async function collectEntries(root) {
|
|
419
409
|
const found = [];
|
|
@@ -19,6 +19,7 @@ const promises_1 = require("node:fs/promises");
|
|
|
19
19
|
const promises_2 = require("node:stream/promises");
|
|
20
20
|
const node_stream_1 = require("node:stream");
|
|
21
21
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const safe_path_js_1 = require("../shared/safe_path.js");
|
|
22
23
|
const identify_js_1 = require("./identify.js");
|
|
23
24
|
const faults_js_1 = require("./faults.js");
|
|
24
25
|
const retry_js_1 = require("./retry.js");
|
|
@@ -557,14 +558,18 @@ class Downloader {
|
|
|
557
558
|
jobs.push({ kind: "file", file });
|
|
558
559
|
}
|
|
559
560
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
561
|
+
/*
|
|
562
|
+
Every path checked before a byte is fetched, so a version carrying one
|
|
563
|
+
it must not write is refused whole rather than half-written. The check
|
|
564
|
+
used to refuse `..` alone, which let `.git/config` through to be
|
|
565
|
+
renamed over a checkout's real one — see shared/safe_path.ts.
|
|
566
|
+
*/
|
|
567
|
+
for (const file of files) {
|
|
568
|
+
const reason = (0, safe_path_js_1.whyUnsafe)(file.path);
|
|
569
|
+
if (reason) {
|
|
570
|
+
throw new Error(`That version contains an unsafe path (${reason}): ${file.path}`);
|
|
565
571
|
}
|
|
566
|
-
|
|
567
|
-
};
|
|
572
|
+
}
|
|
568
573
|
const told = { at: 0 };
|
|
569
574
|
const sayProgress = (where) => {
|
|
570
575
|
const now = Date.now();
|
|
@@ -599,7 +604,6 @@ class Downloader {
|
|
|
599
604
|
}
|
|
600
605
|
for (const member of packed.get(packId) ?? []) {
|
|
601
606
|
this.check();
|
|
602
|
-
const memberParts = safeParts(member.path);
|
|
603
607
|
const offset = member.pack.offset;
|
|
604
608
|
const length = member.pack.length;
|
|
605
609
|
if (offset < 0 || length < 0 || offset + length > body.byteLength) {
|
|
@@ -610,7 +614,7 @@ class Downloader {
|
|
|
610
614
|
if (member.sha256 && digest !== member.sha256) {
|
|
611
615
|
throw new Error(`${member.path} did not arrive intact`);
|
|
612
616
|
}
|
|
613
|
-
const full =
|
|
617
|
+
const full = (0, safe_path_js_1.inside)(staging, member.path);
|
|
614
618
|
await (0, promises_1.mkdir)(node_path_1.default.dirname(full), { recursive: true });
|
|
615
619
|
await (0, promises_1.writeFile)(full, contents);
|
|
616
620
|
manifest[member.path] = digest;
|
|
@@ -620,8 +624,7 @@ class Downloader {
|
|
|
620
624
|
}
|
|
621
625
|
};
|
|
622
626
|
const restoreFile = async (file) => {
|
|
623
|
-
const
|
|
624
|
-
const full = node_path_1.default.join(staging, ...parts);
|
|
627
|
+
const full = (0, safe_path_js_1.inside)(staging, file.path);
|
|
625
628
|
await (0, promises_1.mkdir)(node_path_1.default.dirname(full), { recursive: true });
|
|
626
629
|
const digest = await this.fetchInto(repositoryId, versionId, file, full);
|
|
627
630
|
/*
|
|
@@ -721,8 +724,7 @@ class Downloader {
|
|
|
721
724
|
await (0, promises_1.mkdir)(destination, { recursive: true });
|
|
722
725
|
let placed = 0;
|
|
723
726
|
for (const file of files) {
|
|
724
|
-
const
|
|
725
|
-
const target = node_path_1.default.join(destination, ...parts);
|
|
727
|
+
const target = (0, safe_path_js_1.inside)(destination, file.path);
|
|
726
728
|
/*
|
|
727
729
|
Reconciling leaves alone whatever the incoming version did not
|
|
728
730
|
change. Writing it anyway would replace an edit in progress with a
|
|
@@ -737,7 +739,7 @@ class Downloader {
|
|
|
737
739
|
}
|
|
738
740
|
await (0, promises_1.mkdir)(node_path_1.default.dirname(target), { recursive: true });
|
|
739
741
|
await (0, promises_1.rm)(target, { recursive: true, force: true });
|
|
740
|
-
await (0, promises_1.rename)(
|
|
742
|
+
await (0, promises_1.rename)((0, safe_path_js_1.inside)(staging, file.path), target);
|
|
741
743
|
placed += 1;
|
|
742
744
|
if (placed === 1)
|
|
743
745
|
(0, faults_js_1.maybeFail)("get:after-first-file");
|
|
@@ -751,10 +753,10 @@ class Downloader {
|
|
|
751
753
|
for (const [gone, digest] of Object.entries(held ?? {})) {
|
|
752
754
|
if (manifest[gone] !== undefined)
|
|
753
755
|
continue;
|
|
754
|
-
|
|
755
|
-
if (
|
|
756
|
+
/* A path it must not write is not one it may delete either. */
|
|
757
|
+
if ((0, safe_path_js_1.whyUnsafe)(gone))
|
|
756
758
|
continue;
|
|
757
|
-
const target =
|
|
759
|
+
const target = (0, safe_path_js_1.inside)(destination, gone);
|
|
758
760
|
/*
|
|
759
761
|
The version dropped it — but if the bytes here are no longer the
|
|
760
762
|
ones this folder received, somebody has been working on it, and
|
|
@@ -0,0 +1,88 @@
|
|
|
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.isGitDirectoryName = isGitDirectoryName;
|
|
7
|
+
exports.whyUnsafe = whyUnsafe;
|
|
8
|
+
exports.safeParts = safeParts;
|
|
9
|
+
exports.inside = inside;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a path from a version may be written into somebody's folder.
|
|
12
|
+
*
|
|
13
|
+
* Written after Gitea's CVE-2026-60004, where repository content landed in a
|
|
14
|
+
* hooks directory and git ran it. Nothing here runs git, but the folder a
|
|
15
|
+
* project is fetched into very often is a git checkout — and a version holding
|
|
16
|
+
* `.git/config` would have been written over the real one. A `core.fsmonitor`
|
|
17
|
+
* line in that file runs a command on the next `git status`, with no
|
|
18
|
+
* executable bit needed; a plain file named `.git` replaced the whole
|
|
19
|
+
* directory with a pointer to one the version could also supply.
|
|
20
|
+
*
|
|
21
|
+
* The service refuses these paths when a version is saved. They are refused
|
|
22
|
+
* here as well because this is the side that writes to disk: it must not rest
|
|
23
|
+
* on the server having been right, having been uncompromised, or having held
|
|
24
|
+
* the same rules when an older version was saved.
|
|
25
|
+
*
|
|
26
|
+
* Shared by every writer — fetching, restoring, unbundling — so there is one
|
|
27
|
+
* rule, not one per place that happened to think of it.
|
|
28
|
+
*/
|
|
29
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
30
|
+
/**
|
|
31
|
+
* A name git treats as its own directory.
|
|
32
|
+
*
|
|
33
|
+
* The same comparison git's own `verify_path` makes with its NTFS and HFS
|
|
34
|
+
* protections on: any case, trailing dots and spaces dropped, an alternate
|
|
35
|
+
* data stream dropped, and the 8.3 short name Windows gives `.git`.
|
|
36
|
+
*/
|
|
37
|
+
function isGitDirectoryName(part) {
|
|
38
|
+
const name = part.split(":")[0].replace(/[. ]+$/, "").toLowerCase();
|
|
39
|
+
return name === ".git" || /^git~\d+$/.test(name);
|
|
40
|
+
}
|
|
41
|
+
/** Why this path must not be written, or null when it may. */
|
|
42
|
+
function whyUnsafe(relative) {
|
|
43
|
+
if (!relative)
|
|
44
|
+
return "it is empty";
|
|
45
|
+
if (relative.includes("\0"))
|
|
46
|
+
return "it contains a NUL character";
|
|
47
|
+
if (/^[\\/]/.test(relative) || /^[a-zA-Z]:/.test(relative)) {
|
|
48
|
+
return "it is absolute";
|
|
49
|
+
}
|
|
50
|
+
for (const part of relative.split(/[\\/]/)) {
|
|
51
|
+
if (part === "..")
|
|
52
|
+
return "it climbs out of the folder";
|
|
53
|
+
if (isGitDirectoryName(part))
|
|
54
|
+
return "it writes into git's own directory";
|
|
55
|
+
/* A stream or a drive-relative name on Windows; refused by the service too. */
|
|
56
|
+
if (process.platform === "win32" && part.includes(":")) {
|
|
57
|
+
return "it names a Windows data stream";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The path's parts, or an error naming the path and why.
|
|
64
|
+
*
|
|
65
|
+
* Both separators split it, because on Windows either one is a separator:
|
|
66
|
+
* `a\..\..\x` is three climbs there, whatever a check splitting on `/` saw.
|
|
67
|
+
*/
|
|
68
|
+
function safeParts(relative) {
|
|
69
|
+
const reason = whyUnsafe(relative);
|
|
70
|
+
if (reason) {
|
|
71
|
+
throw new Error(`That version contains an unsafe path (${reason}): ${relative}`);
|
|
72
|
+
}
|
|
73
|
+
return relative.split(/[\\/]/).filter((part) => part && part !== ".");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Where a checked path lands under `root`, confirmed to be inside it.
|
|
77
|
+
*
|
|
78
|
+
* The part checks above should make this impossible to fail. It is here so
|
|
79
|
+
* that the next check somebody forgets does not become the next escape.
|
|
80
|
+
*/
|
|
81
|
+
function inside(root, relative) {
|
|
82
|
+
const base = node_path_1.default.resolve(root);
|
|
83
|
+
const full = node_path_1.default.resolve(base, ...safeParts(relative));
|
|
84
|
+
if (full !== base && !full.startsWith(base + node_path_1.default.sep)) {
|
|
85
|
+
throw new Error(`That version contains a path outside the folder: ${relative}`);
|
|
86
|
+
}
|
|
87
|
+
return full;
|
|
88
|
+
}
|
package/package.json
CHANGED
package/skills/coderook/SKILL.md
CHANGED
|
@@ -1,154 +1,200 @@
|
|
|
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
|
-
## Versions, and going back
|
|
85
|
-
|
|
86
|
-
Every save is a commit. A commit becomes a *version* — the thing the public
|
|
87
|
-
side of a project offers, and the thing a collaborator pulls — only when
|
|
88
|
-
somebody promotes it.
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
cbx promote # pick from the last ten and name one
|
|
92
|
-
cbx mark 41 --pin # pin, hide, rename or label one save
|
|
93
|
-
cbx labels add shipped green # the labels a project can wear
|
|
94
|
-
cbx undo # put the project back on the save before this one
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
`undo` moves where the project stands. Nothing is deleted and nothing is
|
|
98
|
-
renumbered — the saves passed over stay in the history, and the next save
|
|
99
|
-
carries on from wherever it now stands. It is the answer to "I pushed the
|
|
100
|
-
wrong thing"; hiding a version stops strangers reading it but leaves the
|
|
101
|
-
project standing on it, so the next person to pull still lands on the mistake.
|
|
102
|
-
|
|
103
|
-
**Ask before `undo`, `promote` and `take-down`.** They change what other
|
|
104
|
-
people see. `cbx versions` first, so the person can say which save they mean.
|
|
105
|
-
|
|
106
|
-
## When two saves collide
|
|
107
|
-
|
|
108
|
-
If somebody saved while this folder was behind, the second save becomes a merge
|
|
109
|
-
waiting on a decision rather than overwriting anything.
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
cbx merges # this folder's saves waiting on a decision
|
|
113
|
-
cbx merge <ref> # look at one, and decide
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Read the conflict out to the person and let them choose. Do not pick a side for
|
|
117
|
-
them: the whole reason it stopped is that the service could not tell which copy
|
|
118
|
-
was wanted.
|
|
119
|
-
|
|
120
|
-
## What is worth leaving out
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
cbx ignore --suggest # dependency directories, build output, virtual environments
|
|
124
|
-
cbx ignore --suggest --apply # add the confident ones to .gitignore
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Rules live in the project's own `.gitignore`, so CodeRook, git and the website
|
|
128
|
-
all read one file.
|
|
129
|
-
|
|
130
|
-
## Bundles
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
cbx bundle . project.cbx # the whole project, every version, as one file
|
|
134
|
-
cbx inspect project.cbx # what is inside, without unpacking
|
|
135
|
-
cbx unbundle project.cbx ./restored
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
##
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
## Versions, and going back
|
|
85
|
+
|
|
86
|
+
Every save is a commit. A commit becomes a *version* — the thing the public
|
|
87
|
+
side of a project offers, and the thing a collaborator pulls — only when
|
|
88
|
+
somebody promotes it.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cbx promote # pick from the last ten and name one
|
|
92
|
+
cbx mark 41 --pin # pin, hide, rename or label one save
|
|
93
|
+
cbx labels add shipped green # the labels a project can wear
|
|
94
|
+
cbx undo # put the project back on the save before this one
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`undo` moves where the project stands. Nothing is deleted and nothing is
|
|
98
|
+
renumbered — the saves passed over stay in the history, and the next save
|
|
99
|
+
carries on from wherever it now stands. It is the answer to "I pushed the
|
|
100
|
+
wrong thing"; hiding a version stops strangers reading it but leaves the
|
|
101
|
+
project standing on it, so the next person to pull still lands on the mistake.
|
|
102
|
+
|
|
103
|
+
**Ask before `undo`, `promote` and `take-down`.** They change what other
|
|
104
|
+
people see. `cbx versions` first, so the person can say which save they mean.
|
|
105
|
+
|
|
106
|
+
## When two saves collide
|
|
107
|
+
|
|
108
|
+
If somebody saved while this folder was behind, the second save becomes a merge
|
|
109
|
+
waiting on a decision rather than overwriting anything.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cbx merges # this folder's saves waiting on a decision
|
|
113
|
+
cbx merge <ref> # look at one, and decide
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Read the conflict out to the person and let them choose. Do not pick a side for
|
|
117
|
+
them: the whole reason it stopped is that the service could not tell which copy
|
|
118
|
+
was wanted.
|
|
119
|
+
|
|
120
|
+
## What is worth leaving out
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
cbx ignore --suggest # dependency directories, build output, virtual environments
|
|
124
|
+
cbx ignore --suggest --apply # add the confident ones to .gitignore
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Rules live in the project's own `.gitignore`, so CodeRook, git and the website
|
|
128
|
+
all read one file.
|
|
129
|
+
|
|
130
|
+
## Bundles
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
cbx bundle . project.cbx # the whole project, every version, as one file
|
|
134
|
+
cbx inspect project.cbx # what is inside, without unpacking
|
|
135
|
+
cbx unbundle project.cbx ./restored
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Reading a public project without signing in
|
|
139
|
+
|
|
140
|
+
Public projects can be read by anything, with no token, from
|
|
141
|
+
`https://api.coderook.com/v1/public`. There is no need to read the command
|
|
142
|
+
line's source to find these — this is the whole of it:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
GET /projects?q= public projects, newest first
|
|
146
|
+
GET /projects/{owner}/{slug} one project
|
|
147
|
+
GET /projects/{owner}/{slug}/versions the versions it offers
|
|
148
|
+
GET /projects/{owner}/{slug}/releases/latest newest release and its files
|
|
149
|
+
GET /projects/{owner}/{slug}/updates/latest.yml electron-updater feed
|
|
150
|
+
GET /projects/{owner}/{slug}/versions/{v}/files every file, with digests
|
|
151
|
+
GET /projects/{owner}/{slug}/versions/{v}/file?path= one file
|
|
152
|
+
GET /projects/{owner}/{slug}/versions/{v}/archive the version as a zip
|
|
153
|
+
GET /projects/{owner}/{slug}/versions/{v}/attachments files attached to it
|
|
154
|
+
GET /projects/{owner}/{slug}/versions/{v}/attachments/{id} download one
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`{v}` is a version id or `latest`. A private project answers 404, exactly as
|
|
158
|
+
if it did not exist.
|
|
159
|
+
|
|
160
|
+
### An app that updates itself from CodeRook
|
|
161
|
+
|
|
162
|
+
Release a version named like a version number and attach the installer:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cbx release 1.4.0
|
|
166
|
+
cbx attach ./dist/MyApp-Setup-1.4.0.exe
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
An Electron app then needs nothing custom — electron-builder's generic
|
|
170
|
+
provider pointed at the feed:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
"publish": [{ "provider": "generic",
|
|
174
|
+
"url": "https://api.coderook.com/v1/public/projects/OWNER/SLUG/updates" }]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The feed serves `latest.yml`, `latest-mac.yml` and `latest-linux.yml` with
|
|
178
|
+
each file's size and SHA-512, and redirects file names to their downloads.
|
|
179
|
+
Anything else can read `releases/latest` as JSON: version, notes, and every
|
|
180
|
+
file with its size, SHA-256 and `url`. Updaters have no account, so the
|
|
181
|
+
project they read must be public; keep private code in a separate project and
|
|
182
|
+
ship releases from a public one.
|
|
183
|
+
|
|
184
|
+
## Things not to do
|
|
185
|
+
|
|
186
|
+
- Do not run `cbx delete` unless the person has asked for that project to
|
|
187
|
+
be deleted, by name, in this conversation.
|
|
188
|
+
- Do not run `cbx sign-out`. It costs them a token they have to fetch
|
|
189
|
+
again and gains nothing.
|
|
190
|
+
- Do not guess a project name. `cbx projects` lists them; a folder that is
|
|
191
|
+
already linked needs no name at all.
|
|
192
|
+
- Do not run `cbx take-down`. It destroys the files in a version and they do
|
|
193
|
+
not come back. Say it exists and let its owner run it.
|
|
194
|
+
|
|
195
|
+
## If something refuses
|
|
196
|
+
|
|
197
|
+
The commands explain themselves — pass the message on rather than rewording it.
|
|
198
|
+
A refusal that says a token was not accepted means signing in; one that says a
|
|
199
|
+
project does not allow machines means its owner turned that off deliberately,
|
|
200
|
+
and the answer is to say so, not to find another route in.
|