@coderook/cli 0.17.0 → 0.19.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.17.0",
5
+ "version": "0.19.0",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -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
  /*
@@ -392,12 +416,62 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
392
416
  }
393
417
  console.log(dim("Sending them anyway, because --allow-secrets was given."));
394
418
  }
419
+ /*
420
+ Keys pasted into files that are not keys.
421
+
422
+ The check above reads names, which finds what a tool wrote and is blind to
423
+ what a person typed. This one reads the files being sent — the case where
424
+ somebody pasted a token into `settings.py` to get something working and it
425
+ stayed there, in a file with an ordinary name that every other check waves
426
+ through.
427
+
428
+ Refused separately from `--allow-secrets`, because the answer is different:
429
+ a `.env` should not be sent and the fix is to leave it out, while a source
430
+ file should be sent and the fix is to take the key out of it.
431
+ */
432
+ const pasted = await (0, worktree_js_1.detectPastedCredentials)(folder, files.map((file) => file.path));
433
+ const stillPasted = pasted.filter((finding) => !exposed.includes(finding.path));
434
+ if (stillPasted.length) {
435
+ console.log(red(`
436
+ ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
437
+ for (const finding of stillPasted.slice(0, 20)) {
438
+ console.log(` ${finding.path}`);
439
+ for (const one of finding.found.slice(0, 3)) {
440
+ console.log(dim(` line ${one.line}: ${one.name}`));
441
+ }
442
+ if (finding.found.length > 3) {
443
+ console.log(dim(` …and ${finding.found.length - 3} more`));
444
+ }
445
+ }
446
+ if (stillPasted.length > 20) {
447
+ console.log(dim(` …and ${stillPasted.length - 20} more`));
448
+ }
449
+ if (!hasFlag(parsed, "allow-secrets")) {
450
+ console.error(`
451
+ Nothing was sent. Move the key into an environment variable, and if` +
452
+ ` it has ever been published, replace it at the service that issued` +
453
+ ` it — a key that has leaked stays leaked.` +
454
+ `
455
+ Pass ${accent("--allow-secrets")} if these are not real keys.`);
456
+ return 1;
457
+ }
458
+ console.log(dim("Sending them anyway, because --allow-secrets was given."));
459
+ }
395
460
  if (hasFlag(parsed, "dry-run", "n")) {
396
461
  console.log(`${files.length} file${files.length === 1 ? "" : "s"} would be sent:`);
397
462
  for (const file of files)
398
463
  console.log(` ${file.path}`);
399
464
  return 0;
400
465
  }
466
+ if (added) {
467
+ console.log(`${accent("Added an MIT licence")}, so other people may use this.`);
468
+ console.log(dim(` Change it with `) +
469
+ accent("coderook licence <name>") +
470
+ dim(`, or remove it with `) +
471
+ accent("coderook licence none") +
472
+ dim("."));
473
+ console.log("");
474
+ }
401
475
  const line = progressLine();
402
476
  const uploader = new upload_js_1.Uploader(config_js_1.credentials);
403
477
  let result;
@@ -1175,11 +1249,18 @@ const SPECS = [
1175
1249
  options: [
1176
1250
  { flags: "-m, --message <text>", description: "what changed, in a sentence" },
1177
1251
  { flags: "-n, --dry-run", description: "show what would be sent, send nothing" },
1178
- { flags: "--allow-secrets", description: "send files that look like credentials" },
1252
+ {
1253
+ flags: "--allow-secrets",
1254
+ description: "send files that look like credentials, and files with keys inside",
1255
+ },
1179
1256
  {
1180
1257
  flags: "--track <name>",
1181
1258
  description: "save onto this line, just this once",
1182
1259
  },
1260
+ {
1261
+ flags: "--no-licence",
1262
+ description: "do not add a licence to a new project; nobody may reuse it",
1263
+ },
1183
1264
  ],
1184
1265
  examples: [
1185
1266
  'coderook submit -m "Fix the export dialog"',
@@ -1242,6 +1323,36 @@ const SPECS = [
1242
1323
  usage: "clone <project> [dir]",
1243
1324
  run: commandClone,
1244
1325
  },
1326
+ {
1327
+ name: "licence",
1328
+ aliases: ["license"],
1329
+ group: "Working with a folder",
1330
+ summary: "choose a licence, so people may actually use your work",
1331
+ usage: "licence [name] [folder]",
1332
+ detail: "A project with no licence cannot be used by anybody, whatever else it\n" +
1333
+ "says: with nothing granting permission, the law grants none. That is\n" +
1334
+ "the state most public projects are in, and the people most careful\n" +
1335
+ "about it are the ones who quietly walk away.\n" +
1336
+ "\n" +
1337
+ "Run it with no name to see what there is, each with a line saying what\n" +
1338
+ "it lets people do. Naming one writes LICENSE into the folder, and it\n" +
1339
+ "goes up with your next save like any other file.\n" +
1340
+ "\n" +
1341
+ "The texts are SPDX's own. Nothing here is paraphrased, because the\n" +
1342
+ "wording of a licence is the licence.\n",
1343
+ options: [
1344
+ {
1345
+ flags: "--holder <name>",
1346
+ description: "whose name goes on it, instead of your account name",
1347
+ },
1348
+ {
1349
+ flags: "--force",
1350
+ description: "replace a licence that is already there",
1351
+ },
1352
+ ],
1353
+ examples: ["coderook licence", "coderook licence MIT", "coderook licence Apache-2.0"],
1354
+ run: licence_commands_js_1.commandLicence,
1355
+ },
1245
1356
  {
1246
1357
  name: "ignore",
1247
1358
  aliases: ["rules"],
@@ -1261,8 +1372,21 @@ const SPECS = [
1261
1372
  flags: "--apply",
1262
1373
  description: "with --suggest, add the confident ones to .gitignore",
1263
1374
  },
1375
+ {
1376
+ flags: "--template <name>",
1377
+ description: "add a ready-made set of rules; `list` shows all 309 of them",
1378
+ },
1264
1379
  ],
1265
- run: commandRules,
1380
+ run: (parsed) =>
1381
+ /*
1382
+ Two commands under one name, because they are one question. Somebody
1383
+ asking about ignore rules either wants to see what theirs are or wants
1384
+ a set they do not have to write, and making the second a separate
1385
+ command would hide it from the person who needs it most.
1386
+ */
1387
+ parsed.flags.has("template")
1388
+ ? (0, licence_commands_js_1.commandIgnoreTemplate)(parsed)
1389
+ : commandRules(parsed),
1266
1390
  },
1267
1391
  {
1268
1392
  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
+ }