@coderook/cli 0.17.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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/src/cli.js +81 -1
- package/dist/cli/src/licence_commands.js +194 -0
- package/dist/desktop-app/src/main/ignore_templates.js +1246 -0
- package/dist/desktop-app/src/main/licence_texts.js +27 -0
- package/dist/desktop-app/src/main/licences.js +298 -0
- package/dist/desktop-app/src/main/rules.js +29 -1
- package/dist/desktop-app/src/main/worktree.js +25 -0
- package/package.json +1 -1
|
@@ -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.
|
|
5
|
+
"version": "0.18.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -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
|
/*
|
|
@@ -398,6 +422,15 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
|
398
422
|
console.log(` ${file.path}`);
|
|
399
423
|
return 0;
|
|
400
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
|
+
}
|
|
401
434
|
const line = progressLine();
|
|
402
435
|
const uploader = new upload_js_1.Uploader(config_js_1.credentials);
|
|
403
436
|
let result;
|
|
@@ -1180,6 +1213,10 @@ const SPECS = [
|
|
|
1180
1213
|
flags: "--track <name>",
|
|
1181
1214
|
description: "save onto this line, just this once",
|
|
1182
1215
|
},
|
|
1216
|
+
{
|
|
1217
|
+
flags: "--no-licence",
|
|
1218
|
+
description: "do not add a licence to a new project; nobody may reuse it",
|
|
1219
|
+
},
|
|
1183
1220
|
],
|
|
1184
1221
|
examples: [
|
|
1185
1222
|
'coderook submit -m "Fix the export dialog"',
|
|
@@ -1242,6 +1279,36 @@ const SPECS = [
|
|
|
1242
1279
|
usage: "clone <project> [dir]",
|
|
1243
1280
|
run: commandClone,
|
|
1244
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
|
+
},
|
|
1245
1312
|
{
|
|
1246
1313
|
name: "ignore",
|
|
1247
1314
|
aliases: ["rules"],
|
|
@@ -1261,8 +1328,21 @@ const SPECS = [
|
|
|
1261
1328
|
flags: "--apply",
|
|
1262
1329
|
description: "with --suggest, add the confident ones to .gitignore",
|
|
1263
1330
|
},
|
|
1331
|
+
{
|
|
1332
|
+
flags: "--template <name>",
|
|
1333
|
+
description: "add a ready-made set of rules; `list` shows all 309 of them",
|
|
1334
|
+
},
|
|
1264
1335
|
],
|
|
1265
|
-
run:
|
|
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),
|
|
1266
1346
|
},
|
|
1267
1347
|
{
|
|
1268
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) => `[2m${value}[0m`;
|
|
30
|
+
const bold = (value) => `[1m${value}[0m`;
|
|
31
|
+
const red = (value) => `[31m${value}[0m`;
|
|
32
|
+
const green = (value) => `[32m${value}[0m`;
|
|
33
|
+
const accent = (value) => `[33m${value}[0m`;
|
|
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
|
+
}
|