@coderook/cli 0.42.2 → 0.42.3

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.
@@ -2,7 +2,7 @@
2
2
  "name": "coderook",
3
3
  "displayName": "CodeRook",
4
4
  "description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, with cbx or plain git, from Claude Code.",
5
- "version": "0.42.2",
5
+ "version": "0.42.3",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -11,7 +11,7 @@ exports.readManifest = readManifest;
11
11
  exports.unpackBundle = unpackBundle;
12
12
  exports.collectEntries = collectEntries;
13
13
  /**
14
- * The CodeBox `.cbx` bundle, as `prototype/cbx_benchmark.py` writes it.
14
+ * The CBX `.cbx` bundle, as `prototype/cbx_benchmark.py` writes it.
15
15
  *
16
16
  * This is a port of that format, not a second one. The byte layout, the
17
17
  * manifest keys, the codec rule and the chunk index are taken from the
@@ -294,12 +294,12 @@ async function readManifest(source) {
294
294
  try {
295
295
  const size = (await handle.stat()).size;
296
296
  if (size < HEADER_BYTES + FOOTER_BYTES) {
297
- throw new Error("That file is too small to be a CodeBox bundle");
297
+ throw new Error("That file is too small to be a CBX bundle");
298
298
  }
299
299
  const header = Buffer.alloc(HEADER_BYTES);
300
300
  await handle.read(header, 0, HEADER_BYTES, 0);
301
301
  if (header.subarray(0, 4).toString("latin1") !== exports.HEADER_MAGIC) {
302
- throw new Error("That file is not a CodeBox bundle");
302
+ throw new Error("That file is not a CBX bundle");
303
303
  }
304
304
  const version = header.readUInt16LE(4);
305
305
  if (!SUPPORTED_FORMAT_VERSIONS.has(version)) {
@@ -110,7 +110,7 @@ const RULES = [
110
110
  { extension: ".ckpt", reason: "Model weights — large, and usually downloadable", recommended: false },
111
111
  { extension: ".pt", reason: "Model weights — large, and usually downloadable", recommended: false },
112
112
  { extension: ".pth", reason: "Model weights — large, and usually downloadable", recommended: false },
113
- { extension: ".cbx", reason: "A CodeBox bundle of this project, inside the project", recommended: true },
113
+ { extension: ".cbx", reason: "A CBX bundle of this project, inside the project", recommended: true },
114
114
  { extension: ".pyc", reason: "Python bytecode", recommended: true },
115
115
  { extension: ".log", reason: "Log files", recommended: false },
116
116
  { file: "Local State", reason: "Browser profile data", recommended: true },
@@ -6,7 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.REGISTRY = void 0;
8
8
  /**
9
- * cbx — the CodeBox engine from the command line, talking to CodeRook.
9
+ * cbx — the CBX engine from the command line, talking to CodeRook.
10
10
  *
11
11
  * The same engines the desktop application uses — the same scanner, the same
12
12
  * ignore rules, the same upload and download — with a terminal in front of
@@ -65,6 +65,7 @@ const diff_command_js_1 = require("./diff_command.js");
65
65
  const network_js_1 = require("./network.js");
66
66
  const local_commands_js_1 = require("./local_commands.js");
67
67
  const profile_js_1 = require("../../cbx/src/core/profile.js");
68
+ const remote_restore_js_1 = require("./remote_restore.js");
68
69
  const detect_js_1 = require("../../cbx/src/core/detect.js");
69
70
  const cbx_js_1 = require("../../cbx/src/core/cbx.js");
70
71
  const api_js_2 = require("./api.js");
@@ -2483,15 +2484,17 @@ const SPECS = [
2483
2484
  detail: "With no paths, makes the whole folder match the save; with paths,\n" +
2484
2485
  "only those files and folders. The default is the last save, which\n" +
2485
2486
  "throws away what you changed since.\n\n" +
2487
+ "In a folder linked to a CodeRook project, the save is a Version on\n" +
2488
+ "CodeRook: the one the folder is on, or --from vN.\n\n" +
2486
2489
  "Unsaved changes to a file it would overwrite stop it, and it lists\n" +
2487
2490
  "them. Add --force to discard them. Files the history has never held\n" +
2488
2491
  "are left alone either way, and your line does not move.",
2489
2492
  options: [
2490
- { flags: "--from <save>", description: "a line, line~N, or the start of a save id" },
2493
+ { flags: "--from <save>", description: "a line, line~N, or the start of a save id; vN in a linked folder" },
2491
2494
  { flags: "-f, --force", description: "overwrite unsaved changes" },
2492
2495
  ],
2493
- examples: ["cbx restore src/player.ts --force", "cbx restore --from main~3"],
2494
- run: local_commands_js_1.commandRestore,
2496
+ examples: ["cbx restore src/player.ts --force", "cbx restore --from main~3", "cbx restore website/app --from v272"],
2497
+ run: (0, local_commands_js_1.localFirst)((parsed) => (0, local_commands_js_1.commandRestore)(parsed), remote_restore_js_1.commandRemoteRestore),
2495
2498
  },
2496
2499
  {
2497
2500
  name: "switch",
@@ -39,7 +39,7 @@ function renderHelp(registry, version) {
39
39
  const listed = registry.specs.filter((spec) => !spec.deprecatedBy);
40
40
  const column = Math.min(nameColumn(listed), exports.USAGE_COLUMN);
41
41
  const lines = [
42
- `${(0, colour_js_1.bold)("cbx")} ${(0, colour_js_1.dim)(version)} — the CodeBox engine, for CodeRook`,
42
+ `${(0, colour_js_1.bold)("cbx")} ${(0, colour_js_1.dim)(version)} — the CBX engine, for CodeRook`,
43
43
  "",
44
44
  ];
45
45
  for (const group of registry_js_1.GROUP_ORDER) {
@@ -0,0 +1,174 @@
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.linkedRoot = linkedRoot;
7
+ exports.selectFiles = selectFiles;
8
+ exports.projectPath = projectPath;
9
+ exports.commandRemoteRestore = commandRemoteRestore;
10
+ /**
11
+ * `cbx restore` in a folder linked to a CodeRook project.
12
+ *
13
+ * `restore` used to know only the local history (`cbx init` and `cbx save`),
14
+ * so in a folder made by `cbx clone` or `cbx link`, the kind nearly everyone
15
+ * has, it answered "No history here" while `cbx log` beside it worked.
16
+ * The folder does have a history; it lives on CodeRook. This puts files back
17
+ * the way a saved Version had them, by default the Version the folder is on.
18
+ *
19
+ * Same rules as the local restore:
20
+ * - an unsaved edit to a file it would overwrite stops it, listed, unless
21
+ * --force is given;
22
+ * - files the Version does not hold are left alone;
23
+ * - the folder's link does not move, so `cbx submit` afterwards saves the
24
+ * folder as it now is, and `cbx get` still sees restored-from-older files
25
+ * as edits rather than silently overwriting them.
26
+ */
27
+ const node_crypto_1 = require("node:crypto");
28
+ const node_fs_1 = require("node:fs");
29
+ const node_path_1 = __importDefault(require("node:path"));
30
+ const node_process_1 = __importDefault(require("node:process"));
31
+ const download_js_1 = require("../../cbx/src/core/download.js");
32
+ const safe_path_js_1 = require("../../cbx/src/shared/safe_path.js");
33
+ const config_js_1 = require("./config.js");
34
+ const ESC = String.fromCharCode(27);
35
+ const paint = (code) => (value) => `${ESC}[${code}m${value}${ESC}[0m`;
36
+ const dim = paint("2");
37
+ const red = paint("31");
38
+ const green = paint("32");
39
+ const accent = paint("33");
40
+ const plural = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
41
+ async function digestOfFile(full) {
42
+ try {
43
+ const hash = (0, node_crypto_1.createHash)("sha256");
44
+ for await (const block of (0, node_fs_1.createReadStream)(full))
45
+ hash.update(block);
46
+ return hash.digest("hex");
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /** The linked folder this directory is in, walking up as `git` does. */
53
+ async function linkedRoot(from) {
54
+ let at = node_path_1.default.resolve(from);
55
+ for (;;) {
56
+ const link = await (0, config_js_1.readLink)(at);
57
+ if (link)
58
+ return { root: at, link };
59
+ const parent = node_path_1.default.dirname(at);
60
+ if (parent === at)
61
+ return null;
62
+ at = parent;
63
+ }
64
+ }
65
+ /** Project paths a restore covers: the named files and everything under named folders. */
66
+ function selectFiles(files, wanted) {
67
+ if (!wanted.length)
68
+ return files;
69
+ return files.filter((file) => wanted.some((one) => one === "" || file.path === one || file.path.startsWith(`${one}/`)));
70
+ }
71
+ /** A path typed on the command line, as a path inside the project ("" for the root). */
72
+ function projectPath(root, cwd, typed) {
73
+ const relative = node_path_1.default.relative(root, node_path_1.default.resolve(cwd, typed));
74
+ if (relative.startsWith("..") || node_path_1.default.isAbsolute(relative))
75
+ return null;
76
+ return relative.split(node_path_1.default.sep).join("/").replace(/\/+$/, "");
77
+ }
78
+ async function commandRemoteRestore(parsed) {
79
+ const found = await linkedRoot(node_process_1.default.cwd());
80
+ if (!found) {
81
+ console.error(red("No history here."));
82
+ console.error(dim(" This folder is not linked to a CodeRook project and has no local history. ") +
83
+ accent("cbx link <project>") + dim(" connects it; ") + accent("cbx init") +
84
+ dim(" starts a history on this machine."));
85
+ return 1;
86
+ }
87
+ const { root, link } = found;
88
+ const force = parsed.flags.has("force") || parsed.flags.has("f");
89
+ const wanted = [];
90
+ for (const typed of parsed.positional) {
91
+ const one = projectPath(root, node_process_1.default.cwd(), typed);
92
+ if (one === null) {
93
+ console.error(red(`${typed} is outside the project folder (${root}).`));
94
+ return 1;
95
+ }
96
+ wanted.push(one);
97
+ }
98
+ const downloader = new download_js_1.Downloader(config_js_1.credentials);
99
+ const versions = await downloader.versions(link.repositoryId);
100
+ const from = parsed.flags.get("from");
101
+ let version;
102
+ if (typeof from === "string") {
103
+ const sequence = /^v?(\d+)$/i.exec(from.trim());
104
+ version = sequence
105
+ ? versions.find((one) => one.sequence === Number(sequence[1]))
106
+ : versions.find((one) => one.id.startsWith(from.trim()));
107
+ if (!version) {
108
+ console.error(red(`No version ${from} in ${link.slug}.`) + dim(" cbx versions lists them."));
109
+ return 1;
110
+ }
111
+ }
112
+ else {
113
+ const base = link.baseVersionId ?? link.versionId;
114
+ version =
115
+ versions.find((one) => one.id === base) ??
116
+ versions.find((one) => one.sequence === link.sequence);
117
+ if (!version) {
118
+ console.error(red("This folder's saved version could not be found.") +
119
+ dim(" Name one with --from vN."));
120
+ return 1;
121
+ }
122
+ }
123
+ const files = selectFiles(await downloader.files(link.repositoryId, version.id), wanted);
124
+ if (!files.length) {
125
+ console.error(red(`v${version.sequence} holds nothing at ${wanted.join(", ") || "the project root"}.`));
126
+ return 1;
127
+ }
128
+ const toWrite = [];
129
+ const edited = [];
130
+ for (const file of files) {
131
+ const unsafe = (0, safe_path_js_1.whyUnsafe)(file.path);
132
+ if (unsafe) {
133
+ console.error(red(`Skipped ${JSON.stringify(file.path)}: ${unsafe}.`));
134
+ continue;
135
+ }
136
+ const here = await digestOfFile(node_path_1.default.join(root, ...file.path.split("/")));
137
+ if (here === file.sha256)
138
+ continue;
139
+ /*
140
+ Different from the version. If it still matches what CodeRook last placed
141
+ here, it is only an older or newer copy and may be replaced. Otherwise it
142
+ is somebody's unsaved work, and that needs --force.
143
+ */
144
+ const placed = link.local?.[file.path];
145
+ if (here !== null && here !== placed && !force)
146
+ edited.push(file.path);
147
+ toWrite.push(file);
148
+ }
149
+ if (edited.length) {
150
+ console.error(red(`${plural(edited.length, "file")} here ${edited.length === 1 ? "has" : "have"} ` +
151
+ `unsaved changes that restoring would overwrite:`));
152
+ for (const one of edited.slice(0, 20))
153
+ console.error(` ${one}`);
154
+ if (edited.length > 20)
155
+ console.error(dim(` …and ${edited.length - 20} more`));
156
+ console.error(`\nAdd ${accent("--force")} to discard them, or save them first with ${accent("cbx submit")}.`);
157
+ return 1;
158
+ }
159
+ if (!toWrite.length) {
160
+ console.log(`Already matches ${accent(`v${version.sequence}`)}; nothing to put back.`);
161
+ return 0;
162
+ }
163
+ for (const file of toWrite) {
164
+ await downloader.fileTo(link.repositoryId, version.id, file, node_path_1.default.join(root, ...file.path.split("/")));
165
+ }
166
+ console.log(`Put back ${plural(toWrite.length, "file")} from ${accent(`v${version.sequence}`)}: ` +
167
+ `${version.message.split("\n")[0]}`);
168
+ for (const file of toWrite.slice(0, 20))
169
+ console.log(` ${green("restored")} ${file.path}`);
170
+ if (toWrite.length > 20)
171
+ console.log(dim(` …and ${toWrite.length - 20} more`));
172
+ console.log(dim("The folder's link has not moved. cbx submit saves the folder as it is now."));
173
+ return 0;
174
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.42.2",
3
+ "version": "0.42.3",
4
4
  "description": "The command line for CodeRook (coderook.com): version control and project hosting for projects of any size. Every save is a compressed snapshot, private or public, never used to train AI.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://coderook.com",