@coderook/cli 0.22.2 → 0.23.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.
@@ -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.22.1",
5
+ "version": "0.23.0",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  */
18
18
  const promises_1 = require("node:readline/promises");
19
19
  const node_os_1 = __importDefault(require("node:os"));
20
+ const promises_2 = require("node:fs/promises");
20
21
  const node_path_1 = __importDefault(require("node:path"));
21
22
  const node_process_1 = __importDefault(require("node:process"));
22
23
  const api_js_1 = require("./api.js");
@@ -37,6 +38,7 @@ const identify_js_1 = require("../../desktop-app/src/main/identify.js");
37
38
  const detect_js_1 = require("../../desktop-app/src/main/detect.js");
38
39
  const cbx_js_1 = require("../../desktop-app/src/main/cbx.js");
39
40
  const api_js_2 = require("./api.js");
41
+ const import_command_js_1 = require("./import_command.js");
40
42
  const runner_js_1 = require("./runner.js");
41
43
  const config_js_1 = require("./config.js");
42
44
  /*
@@ -313,6 +315,103 @@ async function commandStatus(parsed) {
313
315
  console.log(dim(` …and ${files.length - 50} more`));
314
316
  return 0;
315
317
  }
318
+ /**
319
+ * Bring an existing repository in from another host.
320
+ *
321
+ * Snapshot only, and it says so. The files arrive, the history does not —
322
+ * see `import_command.ts` for why that is a limit rather than an omission.
323
+ * Everything after the fetch is the ordinary save path, so an import
324
+ * produces exactly the Version that saving the same folder would.
325
+ */
326
+ async function commandImport(parsed) {
327
+ const url = parsed.positional[0];
328
+ if (!url) {
329
+ console.error(red("Nothing to import from."));
330
+ console.error("Give the address of a repository, for example:");
331
+ console.error(` ${accent("coderook import https://github.com/owner/project")}`);
332
+ return 1;
333
+ }
334
+ if (!(0, import_command_js_1.looksLikeRepositoryUrl)(url)) {
335
+ console.error(red(`${url} does not look like a repository address.`));
336
+ console.error("Expected something like https://github.com/owner/project or\n" +
337
+ "git@github.com:owner/project.git");
338
+ return 1;
339
+ }
340
+ if (!(await (0, import_command_js_1.gitAvailable)())) {
341
+ console.error(red("Import needs git on this machine, and it was not found."));
342
+ console.error("Git is used to fetch the files once; nothing about your project\n" +
343
+ "afterwards depends on it.");
344
+ return 1;
345
+ }
346
+ const destination = parsed.positional[1] ?? null;
347
+ let plan;
348
+ try {
349
+ plan = await (0, import_command_js_1.fetchSnapshot)(url, destination, (line) => console.log(dim(line)));
350
+ }
351
+ catch (error) {
352
+ console.error(red("Could not fetch that repository."));
353
+ const detail = error instanceof Error ? error.message : String(error);
354
+ /*
355
+ git puts the useful sentence last and a stack of its own noise first.
356
+ A private repository with no credentials is by far the most common
357
+ failure, so it is named rather than left to be inferred.
358
+ */
359
+ console.error(detail.split("\n").filter(Boolean).slice(-3).join("\n"));
360
+ if (/authentication|denied|not found|could not read/i.test(detail)) {
361
+ console.error("\nIf it is private, sign in to it with git first — CodeRook uses\n" +
362
+ "the credentials git already has and never asks for a token.");
363
+ }
364
+ return 1;
365
+ }
366
+ const { files, bytes } = await (0, import_command_js_1.measure)(plan.folder);
367
+ if (!files) {
368
+ console.error(red("That repository has no files in it."));
369
+ if (plan.temporary)
370
+ await (0, promises_2.rm)(node_path_1.default.dirname(plan.folder), { recursive: true, force: true });
371
+ return 1;
372
+ }
373
+ console.log(`Fetched ${files} file${files === 1 ? "" : "s"}, ${(0, import_command_js_1.humanBytes)(bytes)}, ` +
374
+ `into ${plan.folder}`);
375
+ /*
376
+ Said before the upload rather than after it fails. The window is a share
377
+ of the plan allowance per week, so a large import is a thing somebody
378
+ should know about while they can still choose a smaller repository.
379
+ */
380
+ if (bytes > 1024 ** 3) {
381
+ console.log(dim("This is a large import. Uploads are limited to a share of your\n" +
382
+ "allowance each week, so a project this size may need more than one\n" +
383
+ "sitting; the save resumes rather than starting over."));
384
+ }
385
+ console.log(dim("History is not imported — this becomes the first version."));
386
+ /*
387
+ Hand the fetched folder to the ordinary save path. --allow-ignored is set
388
+ because git tracks files committed before the rule that excludes them,
389
+ and dropping those would make the import quietly lossy.
390
+ */
391
+ const submitFlags = new Map(parsed.flags);
392
+ submitFlags.set("allow-ignored", true);
393
+ /*
394
+ Name the project after the repository, not after wherever the files were
395
+ put. Without this an import into a temporary folder produces a project
396
+ called something like `tmp-4f21`, and the name is the thing somebody
397
+ types afterwards to fetch it.
398
+ */
399
+ if (!submitFlags.has("name"))
400
+ submitFlags.set("name", plan.name);
401
+ if (!submitFlags.has("m") && !submitFlags.has("message")) {
402
+ submitFlags.set("message", `Imported from ${url}`);
403
+ }
404
+ const code = await commandSubmit({
405
+ positional: [plan.folder],
406
+ flags: submitFlags,
407
+ });
408
+ if (code === 0 && plan.temporary) {
409
+ console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
410
+ ` +
411
+ `Run ${accent("coderook get " + plan.name)} anywhere to fetch it fresh.`));
412
+ }
413
+ return code;
414
+ }
316
415
  async function commandSubmit(parsed) {
317
416
  const folder = folderFor(parsed);
318
417
  const message = flagText(parsed, "m", "message") ?? "";
@@ -340,7 +439,22 @@ async function commandSubmit(parsed) {
340
439
  await (0, api_js_2.whoami)()
341
440
  .then((account) => account.displayName || account.username || "")
342
441
  .catch(() => ""), hasFlag(parsed, "no-licence"));
343
- const rules = await (0, worktree_js_1.readRules)(folder);
442
+ /*
443
+ Import sends what it fetched, filtering nothing.
444
+
445
+ The ignore rules exist to keep build output and local mess out of a
446
+ working folder, and applying them to a fresh clone gets the wrong answer
447
+ twice over: a clone holds exactly the files the source repository tracked
448
+ and nothing else, so there is no mess to exclude — while a repository
449
+ that tracks something its own `.gitignore` now names (which git does,
450
+ for anything committed before the rule) would have those files dropped.
451
+ Dropping them makes an import quietly lossy, which is worse than
452
+ refusing to import at all.
453
+ */
454
+ const importing = hasFlag(parsed, "allow-ignored");
455
+ const rules = importing
456
+ ? { shared: "", local: "" }
457
+ : await (0, worktree_js_1.readRules)(folder);
344
458
  const files = await (0, worktree_js_1.changedFiles)(folder, rules, baseline);
345
459
  /*
346
460
  An upgraded folder with no materialisation record and something that
@@ -404,17 +518,17 @@ async function commandSubmit(parsed) {
404
518
  */
405
519
  const shielded = (await (0, worktree_js_1.detectPrivateDirectories)(folder)).filter((finding) => [...sending].some((file) => file === finding.path || file.startsWith(`${finding.path}/`)));
406
520
  if (shielded.length && !hasFlag(parsed, "allow-private")) {
407
- console.log(red(`
521
+ console.log(red(`
408
522
  ${shielded.length} folder${shielded.length === 1 ? "" : "s"} here belong${shielded.length === 1 ? "s" : ""} to a program, not to your project:`));
409
523
  for (const finding of shielded) {
410
524
  console.log(` ${finding.path} ${dim(`— ${finding.because}`)}`);
411
525
  }
412
- console.log(`
526
+ console.log(`
413
527
  Nothing was sent. To leave them behind:`);
414
528
  for (const finding of shielded) {
415
529
  console.log(` ${accent(`echo "${finding.rule}" >> .gitignore`)}`);
416
530
  }
417
- console.error(`
531
+ console.error(`
418
532
  Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
419
533
  return 1;
420
534
  }
@@ -449,7 +563,7 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
449
563
  const pasted = await (0, worktree_js_1.detectPastedCredentials)(folder, files.map((file) => file.path));
450
564
  const stillPasted = pasted.filter((finding) => !exposed.includes(finding.path));
451
565
  if (stillPasted.length) {
452
- console.log(red(`
566
+ console.log(red(`
453
567
  ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
454
568
  for (const finding of stillPasted.slice(0, 20)) {
455
569
  console.log(` ${finding.path}`);
@@ -464,11 +578,11 @@ ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s
464
578
  console.log(dim(` …and ${stillPasted.length - 20} more`));
465
579
  }
466
580
  if (!hasFlag(parsed, "allow-secrets")) {
467
- console.error(`
581
+ console.error(`
468
582
  Nothing was sent. Move the key into an environment variable, and if` +
469
583
  ` it has ever been published, replace it at the service that issued` +
470
584
  ` it — a key that has leaked stays leaked.` +
471
- `
585
+ `
472
586
  Pass ${accent("--allow-secrets")} if these are not real keys.`);
473
587
  return 1;
474
588
  }
@@ -490,13 +604,27 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
490
604
  include: files.map((file) => file.path),
491
605
  deletions: files.filter((file) => file.deleted).map((file) => file.path),
492
606
  message,
493
- projectName: link?.slug ?? node_path_1.default.basename(folder),
607
+ /*
608
+ A folder's name is the right default and the wrong answer for import,
609
+ where the folder is somewhere temporary and the project should carry
610
+ the name it had at the place it came from. An existing link always
611
+ wins: renaming somebody's project because they passed a flag would be
612
+ a surprise, and the flag exists for projects that do not exist yet.
613
+ */
614
+ projectName: link?.slug ?? flagText(parsed, "name") ?? node_path_1.default.basename(folder),
494
615
  repositoryId: link?.repositoryId ?? null,
495
616
  // Every current CLI publish states its ancestry. A brand-new project is
496
617
  // explicitly based on an empty Track; a linked folder names the immutable
497
618
  // Version it was last reconciled with.
498
619
  baseVersionId: link?.baseVersionId ?? null,
499
620
  track: flagText(parsed, "track") ?? (await (0, track_commands_js_1.trackFor)(folder)),
621
+ /*
622
+ Only import sets this. Git keeps tracking files committed before the
623
+ rule that excludes them, so a faithful import carries paths the
624
+ project's own ignore rules now refuse; without the override the server
625
+ would reject the publication and the import would be lossy.
626
+ */
627
+ ...(hasFlag(parsed, "allow-ignored") ? { allowIgnored: true } : {}),
500
628
  ...(link?.baseVersionId
501
629
  ? { expectedHeadVersionId: link.baseVersionId }
502
630
  : {}),
@@ -542,7 +670,7 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
542
670
  difference between a person retrying and a person wondering.
543
671
  */
544
672
  if (!code && /fetch failed|ECONNRESET|socket hang up|network|ETIMEDOUT/i.test(text)) {
545
- console.error(red(`
673
+ console.error(red(`
546
674
  The connection failed: ${text}`));
547
675
  console.error(`Your work may already have been saved. Run the same command again —` +
548
676
  ` it will not create a second version.`);
@@ -724,7 +852,7 @@ async function commandGet(parsed) {
724
852
  `changed while the interrupted fetch was stopped:`));
725
853
  for (const file of changedInTheGap.slice(0, 20))
726
854
  console.error(` ${file.path}`);
727
- console.error(`
855
+ console.error(`
728
856
  Save them with ${accent("coderook submit")}, or finish the fetch and ` +
729
857
  `discard them with ${accent("coderook get --replace")}.`);
730
858
  return 1;
@@ -877,8 +1005,8 @@ async function suggestRules(folder, apply) {
877
1005
  const addition = (0, detect_js_1.rulesFromSuggestions)(recommended);
878
1006
  const shared = rules.shared.trimEnd();
879
1007
  await (0, worktree_js_1.writeRules)(folder, {
880
- shared: shared ? `${shared}
881
-
1008
+ shared: shared ? `${shared}
1009
+
882
1010
  ${addition}` : addition,
883
1011
  local: rules.local,
884
1012
  });
@@ -1005,7 +1133,7 @@ async function commandMerges(parsed) {
1005
1133
  const counts = merge.conflicts;
1006
1134
  console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
1007
1135
  }
1008
- console.log(dim(`
1136
+ console.log(dim(`
1009
1137
  Run coderook merge <reference> to look at one.`));
1010
1138
  return 0;
1011
1139
  }
@@ -1071,7 +1199,7 @@ async function commandMerge(parsed) {
1071
1199
  }
1072
1200
  }
1073
1201
  const now = await (0, api_js_1.mergeTrack)(summary.id);
1074
- console.log(`
1202
+ console.log(`
1075
1203
  ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
1076
1204
  (now.provisional.ready
1077
1205
  ? "ready to apply"
@@ -1086,19 +1214,19 @@ ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
1086
1214
  return 1;
1087
1215
  }
1088
1216
  const applied = await (0, api_js_1.applyMerge)(summary.id);
1089
- console.log(`
1217
+ console.log(`
1090
1218
  Applied as ${accent(`v${applied.version.sequence}`)}.`);
1091
1219
  console.log(dim("Run coderook get to bring it down to this folder."));
1092
1220
  return 0;
1093
1221
  }
1094
1222
  if (!decision) {
1095
- console.log(dim(`
1223
+ console.log(dim(`
1096
1224
  --mine keeps yours, --theirs keeps what was already saved,` +
1097
- ` --drop removes the file.
1225
+ ` --drop removes the file.
1098
1226
  Add --path <file> for one file, then --apply when ready.`));
1099
1227
  }
1100
1228
  else if (now.provisional.ready) {
1101
- console.log(dim(`
1229
+ console.log(dim(`
1102
1230
  Run coderook merge ${now.mergeTrack.reference} --apply to publish it.`));
1103
1231
  }
1104
1232
  return 0;
@@ -1275,6 +1403,10 @@ const SPECS = [
1275
1403
  options: [
1276
1404
  { flags: "-m, --message <text>", description: "what changed, in a sentence" },
1277
1405
  { flags: "-n, --dry-run", description: "show what would be sent, send nothing" },
1406
+ {
1407
+ flags: "--name <name>",
1408
+ description: "name a new project this, instead of after the folder",
1409
+ },
1278
1410
  {
1279
1411
  flags: "--allow-secrets",
1280
1412
  description: "send files that look like credentials, and files with keys inside",
@@ -1294,6 +1426,38 @@ const SPECS = [
1294
1426
  ],
1295
1427
  run: commandSubmit,
1296
1428
  },
1429
+ {
1430
+ name: "import",
1431
+ group: "Getting started",
1432
+ summary: "bring a project in from another host",
1433
+ usage: "import <address> [folder]",
1434
+ detail: "Fetches a repository from GitHub, GitLab or anywhere else git can\n" +
1435
+ "reach, and saves it as the first version of a CodeRook project.\n\n" +
1436
+ "The files come across; the history does not. Every past commit would\n" +
1437
+ "have to be published as its own version, which on a large project\n" +
1438
+ "takes days — so this takes an honest snapshot rather than leaving a\n" +
1439
+ "half-finished import behind. What arrives is the current state of the\n" +
1440
+ "default branch.\n\n" +
1441
+ "A public repository needs nothing. A private one uses the credentials\n" +
1442
+ "git already has on this machine; CodeRook never asks for, stores or\n" +
1443
+ "forwards a token.\n\n" +
1444
+ "Files that the project's own ignore rules exclude are sent anyway,\n" +
1445
+ "because git keeps tracking anything committed before the rule that\n" +
1446
+ "excludes it, and leaving them out would lose files the source has.",
1447
+ options: [
1448
+ { flags: "-m, --message <text>", description: "the first version's message" },
1449
+ { flags: "--track <name>", description: "save onto this line" },
1450
+ {
1451
+ flags: "--no-licence",
1452
+ description: "do not add a licence to the new project",
1453
+ },
1454
+ ],
1455
+ examples: [
1456
+ "coderook import https://github.com/owner/project",
1457
+ "coderook import git@github.com:owner/project.git ./project",
1458
+ ],
1459
+ run: commandImport,
1460
+ },
1297
1461
  {
1298
1462
  /*
1299
1463
  Where the next save goes, which is a property of this folder rather
@@ -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
+ }
@@ -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
- const rules = await (0, worktree_js_1.readRules)(request.localPath);
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.
package/package.json CHANGED
@@ -1,51 +1,51 @@
1
- {
2
- "name": "@coderook/cli",
3
- "version": "0.22.2",
4
- "description": "CodeRook from the command line, on any operating system",
5
- "license": "SEE LICENSE IN LICENSE.txt",
6
- "homepage": "https://coderook.com",
7
- "bugs": {
8
- "url": "https://coderook.com/contact"
9
- },
10
- "keywords": [
11
- "coderook",
12
- "versioning",
13
- "backup",
14
- "cbx"
15
- ],
16
- "engines": {
17
- "node": ">=20.11.0"
18
- },
19
- "bin": {
20
- "coderook": "dist/cli/src/cli.js"
21
- },
22
- "files": [
23
- ".claude-plugin",
24
- "dist",
25
- "skills"
26
- ],
27
- "scripts": {
28
- "build": "tsc -p tsconfig.json",
29
- "check": "tsc -p tsconfig.json --noEmit",
30
- "test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
31
- "start": "node dist/cli/src/cli.js",
32
- "prepublishOnly": "npm run sync:plugin && npm run build",
33
- "test:e2e": "node test/e2e.mjs",
34
- "test:matrix": "node test/state-matrix.mjs",
35
- "test:attempt": "node test/attempt-identity.mjs",
36
- "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",
38
- "test:getedits": "node test/get-protects-edits.mjs",
39
- "test:upgrade": "node test/upgrade-migration.mjs",
40
- "test:faultget": "node test/fault-get.mjs",
41
- "test:floor": "node test/version-floor.mjs",
42
- "test:faultsubmit": "node test/fault-submit.mjs",
43
- "test:race": "node test/race-attempts.mjs",
44
- "test:runner": "node test/runner-live.mjs",
45
- "sync:plugin": "node scripts/sync-plugin-version.mjs"
46
- },
47
- "devDependencies": {
48
- "@types/node": "24.10.1",
49
- "typescript": "5.9.3"
50
- }
51
- }
1
+ {
2
+ "name": "@coderook/cli",
3
+ "version": "0.23.0",
4
+ "description": "CodeRook from the command line, on any operating system",
5
+ "license": "SEE LICENSE IN LICENSE.txt",
6
+ "homepage": "https://coderook.com",
7
+ "bugs": {
8
+ "url": "https://coderook.com/contact"
9
+ },
10
+ "keywords": [
11
+ "coderook",
12
+ "versioning",
13
+ "backup",
14
+ "cbx"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20.11.0"
18
+ },
19
+ "bin": {
20
+ "coderook": "dist/cli/src/cli.js"
21
+ },
22
+ "files": [
23
+ ".claude-plugin",
24
+ "dist",
25
+ "skills"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "check": "tsc -p tsconfig.json --noEmit",
30
+ "test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
31
+ "start": "node dist/cli/src/cli.js",
32
+ "prepublishOnly": "npm run sync:plugin && npm run build",
33
+ "test:e2e": "node test/e2e.mjs",
34
+ "test:matrix": "node test/state-matrix.mjs",
35
+ "test:attempt": "node test/attempt-identity.mjs",
36
+ "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",
38
+ "test:getedits": "node test/get-protects-edits.mjs",
39
+ "test:upgrade": "node test/upgrade-migration.mjs",
40
+ "test:faultget": "node test/fault-get.mjs",
41
+ "test:floor": "node test/version-floor.mjs",
42
+ "test:faultsubmit": "node test/fault-submit.mjs",
43
+ "test:race": "node test/race-attempts.mjs",
44
+ "test:runner": "node test/runner-live.mjs",
45
+ "sync:plugin": "node scripts/sync-plugin-version.mjs"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "24.10.1",
49
+ "typescript": "5.9.3"
50
+ }
51
+ }