@coderook/cli 0.16.0 → 0.18.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,9 +2,17 @@
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.16.0",
6
- "author": { "name": "ACCA Gaming Productions", "url": "https://coderook.com" },
5
+ "version": "0.18.0",
6
+ "author": {
7
+ "name": "ACCA Gaming Productions",
8
+ "url": "https://coderook.com"
9
+ },
7
10
  "homepage": "https://coderook.com/docs#assistants",
8
11
  "license": "SEE LICENSE IN LICENSE.txt",
9
- "keywords": ["coderook", "versioning", "snapshots", "backup"]
12
+ "keywords": [
13
+ "coderook",
14
+ "versioning",
15
+ "snapshots",
16
+ "backup"
17
+ ]
10
18
  }
@@ -27,6 +27,7 @@ const project_commands_js_1 = require("./project_commands.js");
27
27
  const track_commands_js_1 = require("./track_commands.js");
28
28
  const mcp_js_1 = require("./mcp.js");
29
29
  const skill_command_js_1 = require("./skill_command.js");
30
+ const licence_commands_js_1 = require("./licence_commands.js");
30
31
  const service_commands_js_1 = require("./service_commands.js");
31
32
  const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
32
33
  const upload_js_1 = require("../../desktop-app/src/main/upload.js");
@@ -299,6 +300,29 @@ async function commandSubmit(parsed) {
299
300
  const folder = folderFor(parsed);
300
301
  const message = flagText(parsed, "m", "message") ?? "";
301
302
  const { link, baseline, behind } = await reconcile(folder);
303
+ /*
304
+ A first save gets a licence unless it is turned down.
305
+
306
+ Work published with no licence cannot legally be used by anybody, which is
307
+ the opposite of what nearly everyone uploading their work intends — so the
308
+ default says the generous thing rather than the silent one. It happens
309
+ before the scan below so the file goes up with this same version rather
310
+ than trailing a save behind.
311
+
312
+ Only on a project's first save. After that an absent LICENSE is somebody's
313
+ decision and putting one back would be arguing with them.
314
+ */
315
+ const added = link
316
+ ? null
317
+ : await (0, licence_commands_js_1.licenceNewProject)(folder,
318
+ /*
319
+ Whose name goes on the copyright line. Asked of the account rather
320
+ than guessed from the folder — and when it cannot be had, the shared
321
+ helper writes nothing rather than naming nobody.
322
+ */
323
+ await (0, api_js_2.whoami)()
324
+ .then((account) => account.displayName || account.username || "")
325
+ .catch(() => ""), hasFlag(parsed, "no-licence"));
302
326
  const rules = await (0, worktree_js_1.readRules)(folder);
303
327
  const files = await (0, worktree_js_1.changedFiles)(folder, rules, baseline);
304
328
  /*
@@ -352,6 +376,31 @@ async function commandSubmit(parsed) {
352
376
  const secrets = await (0, worktree_js_1.detectSecrets)(folder);
353
377
  const sending = new Set(files.map((file) => file.path));
354
378
  const exposed = secrets.filter((secret) => sending.has(secret));
379
+ /*
380
+ Whole folders that belong to a program rather than to the work.
381
+
382
+ The credential check answers by name, one file at a time, and cannot see
383
+ into a dependency folder at all — which is where a real cached login sat
384
+ while a project went up carrying a browser profile, an emulated database
385
+ and an unrelated work tree. Naming the folder is the only answer that
386
+ keeps working after the program next runs and rewrites what is inside it.
387
+ */
388
+ const shielded = (await (0, worktree_js_1.detectPrivateDirectories)(folder)).filter((finding) => [...sending].some((file) => file === finding.path || file.startsWith(`${finding.path}/`)));
389
+ if (shielded.length && !hasFlag(parsed, "allow-private")) {
390
+ console.log(red(`
391
+ ${shielded.length} folder${shielded.length === 1 ? "" : "s"} here belong${shielded.length === 1 ? "s" : ""} to a program, not to your project:`));
392
+ for (const finding of shielded) {
393
+ console.log(` ${finding.path} ${dim(`— ${finding.because}`)}`);
394
+ }
395
+ console.log(`
396
+ Nothing was sent. To leave them behind:`);
397
+ for (const finding of shielded) {
398
+ console.log(` ${accent(`echo "${finding.rule}" >> .gitignore`)}`);
399
+ }
400
+ console.error(`
401
+ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
402
+ return 1;
403
+ }
355
404
  if (exposed.length) {
356
405
  console.log(red(`\n${exposed.length} file${exposed.length === 1 ? " looks like a credential" : "s look like credentials"}:`));
357
406
  for (const secret of exposed.slice(0, 20))
@@ -373,6 +422,15 @@ async function commandSubmit(parsed) {
373
422
  console.log(` ${file.path}`);
374
423
  return 0;
375
424
  }
425
+ if (added) {
426
+ console.log(`${accent("Added an MIT licence")}, so other people may use this.`);
427
+ console.log(dim(` Change it with `) +
428
+ accent("coderook licence <name>") +
429
+ dim(`, or remove it with `) +
430
+ accent("coderook licence none") +
431
+ dim("."));
432
+ console.log("");
433
+ }
376
434
  const line = progressLine();
377
435
  const uploader = new upload_js_1.Uploader(config_js_1.credentials);
378
436
  let result;
@@ -1155,6 +1213,10 @@ const SPECS = [
1155
1213
  flags: "--track <name>",
1156
1214
  description: "save onto this line, just this once",
1157
1215
  },
1216
+ {
1217
+ flags: "--no-licence",
1218
+ description: "do not add a licence to a new project; nobody may reuse it",
1219
+ },
1158
1220
  ],
1159
1221
  examples: [
1160
1222
  'coderook submit -m "Fix the export dialog"',
@@ -1217,6 +1279,36 @@ const SPECS = [
1217
1279
  usage: "clone <project> [dir]",
1218
1280
  run: commandClone,
1219
1281
  },
1282
+ {
1283
+ name: "licence",
1284
+ aliases: ["license"],
1285
+ group: "Working with a folder",
1286
+ summary: "choose a licence, so people may actually use your work",
1287
+ usage: "licence [name] [folder]",
1288
+ detail: "A project with no licence cannot be used by anybody, whatever else it\n" +
1289
+ "says: with nothing granting permission, the law grants none. That is\n" +
1290
+ "the state most public projects are in, and the people most careful\n" +
1291
+ "about it are the ones who quietly walk away.\n" +
1292
+ "\n" +
1293
+ "Run it with no name to see what there is, each with a line saying what\n" +
1294
+ "it lets people do. Naming one writes LICENSE into the folder, and it\n" +
1295
+ "goes up with your next save like any other file.\n" +
1296
+ "\n" +
1297
+ "The texts are SPDX's own. Nothing here is paraphrased, because the\n" +
1298
+ "wording of a licence is the licence.\n",
1299
+ options: [
1300
+ {
1301
+ flags: "--holder <name>",
1302
+ description: "whose name goes on it, instead of your account name",
1303
+ },
1304
+ {
1305
+ flags: "--force",
1306
+ description: "replace a licence that is already there",
1307
+ },
1308
+ ],
1309
+ examples: ["coderook licence", "coderook licence MIT", "coderook licence Apache-2.0"],
1310
+ run: licence_commands_js_1.commandLicence,
1311
+ },
1220
1312
  {
1221
1313
  name: "ignore",
1222
1314
  aliases: ["rules"],
@@ -1236,8 +1328,21 @@ const SPECS = [
1236
1328
  flags: "--apply",
1237
1329
  description: "with --suggest, add the confident ones to .gitignore",
1238
1330
  },
1331
+ {
1332
+ flags: "--template <name>",
1333
+ description: "add a ready-made set of rules; `list` shows all 309 of them",
1334
+ },
1239
1335
  ],
1240
- run: commandRules,
1336
+ run: (parsed) =>
1337
+ /*
1338
+ Two commands under one name, because they are one question. Somebody
1339
+ asking about ignore rules either wants to see what theirs are or wants
1340
+ a set they do not have to write, and making the second a separate
1341
+ command would hide it from the person who needs it most.
1342
+ */
1343
+ parsed.flags.has("template")
1344
+ ? (0, licence_commands_js_1.commandIgnoreTemplate)(parsed)
1345
+ : commandRules(parsed),
1241
1346
  },
1242
1347
  {
1243
1348
  name: "projects",
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ /**
3
+ * Choosing a licence, and borrowing somebody else's ignore rules.
4
+ *
5
+ * Both are the same shape of problem: a thing every project needs, that
6
+ * almost nobody writes from scratch, and that is quietly consequential when
7
+ * it is missing. A project with no licence cannot legally be used by anybody,
8
+ * however public it is — and a project with no ignore rules publishes a
9
+ * dependency tree and whatever else was lying in the folder.
10
+ *
11
+ * Neither is invented here. The licence texts come from SPDX and the ignore
12
+ * templates from github/gitignore, vendored by scripts in the desktop package
13
+ * so that the wording matches what every other tool in the world agrees on.
14
+ */
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.licenceNewProject = void 0;
20
+ exports.commandLicence = commandLicence;
21
+ exports.commandIgnoreTemplate = commandIgnoreTemplate;
22
+ const promises_1 = require("node:fs/promises");
23
+ const node_path_1 = __importDefault(require("node:path"));
24
+ const licences_js_1 = require("../../desktop-app/src/main/licences.js");
25
+ Object.defineProperty(exports, "licenceNewProject", { enumerable: true, get: function () { return licences_js_1.licenceNewProject; } });
26
+ const ignore_templates_js_1 = require("../../desktop-app/src/main/ignore_templates.js");
27
+ const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
28
+ const api_js_1 = require("./api.js");
29
+ const dim = (value) => `${value}`;
30
+ const bold = (value) => `${value}`;
31
+ const red = (value) => `${value}`;
32
+ const green = (value) => `${value}`;
33
+ const accent = (value) => `${value}`;
34
+ const SHAPES = {
35
+ permissive: "Do what you like, keep the notice",
36
+ "weak-copyleft": "Changes to these files come back",
37
+ copyleft: "Anything built on it carries the same terms",
38
+ "public-domain": "Given away entirely",
39
+ };
40
+ /** The file a licence goes in, matching what every tool looks for. */
41
+ /**
42
+ * The folder a command acts on.
43
+ *
44
+ * Indexed the way the rest of the command line does: positional[0] is the
45
+ * first argument after the command name, so a licence command reads its
46
+ * licence there and its folder one along.
47
+ */
48
+ function folderFor(parsed, index = 0) {
49
+ return node_path_1.default.resolve(parsed.positional[index] ?? process.cwd());
50
+ }
51
+ async function commandLicence(parsed) {
52
+ const wanted = parsed.positional[0];
53
+ const folder = folderFor(parsed, 1);
54
+ if (!wanted) {
55
+ const existing = await (0, promises_1.readFile)(node_path_1.default.join(folder, licences_js_1.LICENCE_FILE), "utf8")
56
+ .then((text) => text.split("\n")[0]?.trim() ?? "")
57
+ .catch(() => null);
58
+ if (existing !== null) {
59
+ console.log(`${bold("This folder has a licence")} — ${existing}`);
60
+ console.log(dim(` ${node_path_1.default.join(folder, licences_js_1.LICENCE_FILE)}`));
61
+ console.log("");
62
+ }
63
+ console.log(bold("Licences you can put on this project"));
64
+ let shape = null;
65
+ for (const licence of licences_js_1.LICENCES) {
66
+ if (licence.shape !== shape) {
67
+ shape = licence.shape;
68
+ console.log("");
69
+ console.log(dim(` ${SHAPES[shape].toUpperCase()}`));
70
+ }
71
+ const mark = licence.id === licences_js_1.DEFAULT_LICENCE ? accent(" ·") : " ";
72
+ console.log(`${mark} ${bold(licence.id.padEnd(20))} ${licence.name}`);
73
+ console.log(` ${dim(licence.summary)}`);
74
+ }
75
+ console.log("");
76
+ console.log(`Add one with ${accent("coderook licence MIT")}. ` +
77
+ dim(`Nothing is written until you name one.`));
78
+ return 0;
79
+ }
80
+ /*
81
+ "none" is a real answer, and has to be, or the default below is a trap:
82
+ somebody who does not want their work reusable needs a way to say so that
83
+ is as easy as the way it arrived.
84
+ */
85
+ if (wanted.toLowerCase() === "none") {
86
+ const target = node_path_1.default.join(folder, licences_js_1.LICENCE_FILE);
87
+ const already = await (0, promises_1.readFile)(target, "utf8").catch(() => null);
88
+ if (already === null) {
89
+ console.log("There is no licence here to remove.");
90
+ return 0;
91
+ }
92
+ await (0, promises_1.rm)(target);
93
+ console.log(green(`Removed ${licences_js_1.LICENCE_FILE}.`));
94
+ console.log(dim("With no licence, nobody else may legally use this — which is a"));
95
+ console.log(dim("position you can hold, as long as you are holding it on purpose."));
96
+ return 0;
97
+ }
98
+ const licence = (0, licences_js_1.licenceById)(wanted);
99
+ if (!licence) {
100
+ console.error(red(`No licence called ${wanted}.`));
101
+ console.error(`Run ${accent("coderook licence")} to see the list.`);
102
+ return 1;
103
+ }
104
+ /*
105
+ Whose name goes on it.
106
+
107
+ Asked of the account rather than guessed from the folder, and only for the
108
+ licences that carry a copyright line. Getting this wrong is not a typo —
109
+ it is a legal document naming the wrong person — so a licence that needs a
110
+ holder and cannot find one stops rather than writing "unknown".
111
+ */
112
+ let holder = flagValue(parsed, "holder") ?? "";
113
+ if (licence.personalised && !holder) {
114
+ holder = await (0, api_js_1.whoami)()
115
+ .then((account) => account.displayName || account.username || "")
116
+ .catch(() => "");
117
+ if (!holder) {
118
+ console.error(red("Could not work out whose name goes on the licence."));
119
+ console.error(`Sign in, or pass ${accent('--holder "Your Name"')} to say directly.`);
120
+ return 1;
121
+ }
122
+ }
123
+ const target = node_path_1.default.join(folder, licences_js_1.LICENCE_FILE);
124
+ const already = await (0, promises_1.readFile)(target, "utf8").catch(() => null);
125
+ if (already !== null && !parsed.flags.has("force")) {
126
+ console.error(red("This folder already has a LICENSE."));
127
+ console.error(dim(` ${already.split("\n")[0]?.trim() ?? ""}`));
128
+ console.error(`\nReplacing a licence changes the terms other people already received.` +
129
+ `\nPass ${accent("--force")} if that is what you mean.`);
130
+ return 1;
131
+ }
132
+ await (0, promises_1.writeFile)(target, (0, licences_js_1.licenceText)(licence.id, holder), "utf8");
133
+ console.log(green(`Wrote ${licence.name} to ${licences_js_1.LICENCE_FILE}.`));
134
+ if (licence.personalised)
135
+ console.log(dim(` in the name of ${holder}`));
136
+ console.log(dim(` ${licence.summary}`));
137
+ console.log("");
138
+ console.log(dim("It is a file like any other, so it goes up with your next save."));
139
+ return 0;
140
+ }
141
+ /**
142
+ * Borrow a set of ignore rules somebody else already worked out.
143
+ *
144
+ * Appended under a heading of its own rather than replacing what is there.
145
+ * A folder often needs two or three of these — a language, an editor, an
146
+ * operating system — and a template that overwrote the last one would make
147
+ * the second choice undo the first.
148
+ */
149
+ async function commandIgnoreTemplate(parsed) {
150
+ const folder = folderFor(parsed);
151
+ const wanted = flagValue(parsed, "template") ?? "";
152
+ if (!wanted || wanted === "list") {
153
+ const names = Object.values(ignore_templates_js_1.IGNORE_TEMPLATES)
154
+ .map((template) => template.name)
155
+ .sort((left, right) => left.localeCompare(right));
156
+ console.log(bold(`${names.length} sets of ignore rules`));
157
+ console.log("");
158
+ /* Four columns, because a list this long as one column is unreadable. */
159
+ const width = Math.max(...names.map((name) => name.length)) + 2;
160
+ const perRow = Math.max(1, Math.floor(96 / width));
161
+ for (let at = 0; at < names.length; at += perRow) {
162
+ console.log(" " + names.slice(at, at + perRow).map((name) => name.padEnd(width)).join(""));
163
+ }
164
+ console.log("");
165
+ console.log(`Add one with ${accent("coderook ignore --template Python")}. ` +
166
+ dim("Several is normal — a language, an editor, an operating system."));
167
+ return 0;
168
+ }
169
+ const template = ignore_templates_js_1.IGNORE_TEMPLATES[wanted.toLowerCase()];
170
+ if (!template) {
171
+ console.error(red(`No rules called ${wanted}.`));
172
+ console.error(`Run ${accent("coderook ignore --template list")} to see what there is.`);
173
+ return 1;
174
+ }
175
+ const rules = await (0, worktree_js_1.readRules)(folder);
176
+ const heading = `# ${template.name}, from github/gitignore`;
177
+ if (rules.shared.includes(heading)) {
178
+ console.log(`${template.name} is already in this folder's rules.`);
179
+ return 0;
180
+ }
181
+ const body = rules.shared.replace(/\s*$/, "");
182
+ const shared = `${body ? `${body}\n\n` : ""}${heading}\n${template.body}`;
183
+ await (0, worktree_js_1.writeRules)(folder, { ...rules, shared });
184
+ const lines = template.body.split("\n").filter((line) => line.trim() && !line.startsWith("#"));
185
+ console.log(green(`Added ${template.name} — ${lines.length} rules.`));
186
+ console.log(dim(` ${node_path_1.default.join(folder, ".gitignore")}`));
187
+ console.log("");
188
+ console.log(dim(`See what it leaves behind with `) + accent("coderook status"));
189
+ return 0;
190
+ }
191
+ function flagValue(parsed, name) {
192
+ const value = parsed.flags.get(name);
193
+ return typeof value === "string" && value.trim() ? value.trim() : null;
194
+ }