@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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/src/cli.js +126 -2
- 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/secret_patterns.js +371 -0
- package/dist/desktop-app/src/main/worktree.js +96 -0
- package/package.json +1 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The licences somebody can put on a project, and what each one means.
|
|
4
|
+
*
|
|
5
|
+
* A licence is the difference between code a stranger may use and code they
|
|
6
|
+
* may only look at. Most public projects carry none, which reads as generous
|
|
7
|
+
* and is the opposite: with no licence at all, nobody has permission to do
|
|
8
|
+
* anything with the work, and the people most careful about the law are
|
|
9
|
+
* exactly the ones who will walk away.
|
|
10
|
+
*
|
|
11
|
+
* The summaries below are written for somebody deciding, not for a lawyer
|
|
12
|
+
* checking. They say what the licence lets other people do and what it asks
|
|
13
|
+
* of them in return, in one line each, because a picker that only lists
|
|
14
|
+
* identifiers is a picker that makes people choose MIT out of recognition.
|
|
15
|
+
*/
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.LICENCE_FILE = exports.DEFAULT_LICENCE = exports.LICENCES = void 0;
|
|
21
|
+
exports.licenceById = licenceById;
|
|
22
|
+
exports.licenceText = licenceText;
|
|
23
|
+
exports.detectLicence = detectLicence;
|
|
24
|
+
exports.licenceNewProject = licenceNewProject;
|
|
25
|
+
const promises_1 = require("node:fs/promises");
|
|
26
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
27
|
+
const licence_texts_1 = require("./licence_texts");
|
|
28
|
+
exports.LICENCES = [
|
|
29
|
+
{
|
|
30
|
+
id: "MIT",
|
|
31
|
+
name: "MIT",
|
|
32
|
+
shape: "permissive",
|
|
33
|
+
summary: "Anyone may use, change and sell it, as long as they keep your copyright notice.",
|
|
34
|
+
personalised: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "Apache-2.0",
|
|
38
|
+
name: "Apache 2.0",
|
|
39
|
+
shape: "permissive",
|
|
40
|
+
summary: "Like MIT, and it also grants patent rights and asks that changes be marked. What most companies prefer.",
|
|
41
|
+
personalised: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "BSD-3-Clause",
|
|
45
|
+
name: "BSD 3-Clause",
|
|
46
|
+
shape: "permissive",
|
|
47
|
+
summary: "Like MIT, and nobody may use your name to promote what they built with it.",
|
|
48
|
+
personalised: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "BSD-2-Clause",
|
|
52
|
+
name: "BSD 2-Clause",
|
|
53
|
+
shape: "permissive",
|
|
54
|
+
summary: "MIT in different words, without the naming clause.",
|
|
55
|
+
personalised: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "ISC",
|
|
59
|
+
name: "ISC",
|
|
60
|
+
shape: "permissive",
|
|
61
|
+
summary: "MIT, said shorter. Used across the Node ecosystem.",
|
|
62
|
+
personalised: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "0BSD",
|
|
66
|
+
name: "Zero-Clause BSD",
|
|
67
|
+
shape: "permissive",
|
|
68
|
+
summary: "Use it however you like, and you do not even have to credit it.",
|
|
69
|
+
personalised: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "Zlib",
|
|
73
|
+
name: "zlib",
|
|
74
|
+
shape: "permissive",
|
|
75
|
+
summary: "Permissive, with a request that altered versions are not passed off as the original. Common in games.",
|
|
76
|
+
personalised: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "BSL-1.0",
|
|
80
|
+
name: "Boost",
|
|
81
|
+
shape: "permissive",
|
|
82
|
+
summary: "Permissive, and the notice is not required in compiled form — so it does not follow a binary around.",
|
|
83
|
+
personalised: false,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "MPL-2.0",
|
|
87
|
+
name: "Mozilla Public License 2.0",
|
|
88
|
+
shape: "weak-copyleft",
|
|
89
|
+
summary: "Changes to your files must be shared. Anything else built alongside them stays private.",
|
|
90
|
+
personalised: false,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "EPL-2.0",
|
|
94
|
+
name: "Eclipse Public License 2.0",
|
|
95
|
+
shape: "weak-copyleft",
|
|
96
|
+
summary: "Like MPL, from the Java world, with its own patent terms. Common in Eclipse and Clojure projects.",
|
|
97
|
+
personalised: false,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "LGPL-3.0-or-later",
|
|
101
|
+
name: "LGPL 3.0",
|
|
102
|
+
shape: "weak-copyleft",
|
|
103
|
+
summary: "A closed program may link to it, but changes to the library itself must be published.",
|
|
104
|
+
personalised: false,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "GPL-3.0-or-later",
|
|
108
|
+
name: "GPL 3.0",
|
|
109
|
+
shape: "copyleft",
|
|
110
|
+
summary: "Anything built on it must be published under these same terms. Keeps derivatives open.",
|
|
111
|
+
personalised: false,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "GPL-2.0-or-later",
|
|
115
|
+
name: "GPL 2.0",
|
|
116
|
+
shape: "copyleft",
|
|
117
|
+
summary: "The older GPL, still used by the Linux kernel and much beside it.",
|
|
118
|
+
personalised: false,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "AGPL-3.0-or-later",
|
|
122
|
+
name: "AGPL 3.0",
|
|
123
|
+
shape: "copyleft",
|
|
124
|
+
summary: "GPL, and it also covers running it as a service — a hosted version must publish its source too.",
|
|
125
|
+
personalised: false,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "Unlicense",
|
|
129
|
+
name: "The Unlicense",
|
|
130
|
+
shape: "public-domain",
|
|
131
|
+
summary: "Given away entirely. No conditions, no attribution, nothing asked.",
|
|
132
|
+
personalised: false,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "CC0-1.0",
|
|
136
|
+
name: "CC0 1.0",
|
|
137
|
+
shape: "public-domain",
|
|
138
|
+
summary: "Public domain, worded to work in countries that do not have one. For data and assets more than code.",
|
|
139
|
+
personalised: false,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "CC-BY-4.0",
|
|
143
|
+
name: "Creative Commons BY 4.0",
|
|
144
|
+
shape: "permissive",
|
|
145
|
+
summary: "Use it however you like with credit. Meant for writing, images and data — not for code.",
|
|
146
|
+
personalised: false,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "CC-BY-SA-4.0",
|
|
150
|
+
name: "Creative Commons BY-SA 4.0",
|
|
151
|
+
shape: "copyleft",
|
|
152
|
+
summary: "Credit, and anything built from it carries the same terms. Meant for writing and images.",
|
|
153
|
+
personalised: false,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
exports.DEFAULT_LICENCE = "MIT";
|
|
157
|
+
function licenceById(id) {
|
|
158
|
+
const wanted = id.trim().toLowerCase();
|
|
159
|
+
return (exports.LICENCES.find((licence) => licence.id.toLowerCase() === wanted) ??
|
|
160
|
+
/* `gpl-3.0` for `GPL-3.0-or-later`, because that is what people type. */
|
|
161
|
+
exports.LICENCES.find((licence) => licence.id.toLowerCase().startsWith(`${wanted}-or-later`)) ??
|
|
162
|
+
null);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The text as it should be written, with the year and holder filled in.
|
|
166
|
+
*
|
|
167
|
+
* Only the licences that carry a copyright line take these. The rest are the
|
|
168
|
+
* same words for everybody, and substituting into them would be editing a
|
|
169
|
+
* legal document — which is exactly what the placeholders exist to avoid.
|
|
170
|
+
*/
|
|
171
|
+
function licenceText(id, holder, year = new Date().getFullYear()) {
|
|
172
|
+
const licence = licenceById(id);
|
|
173
|
+
if (!licence)
|
|
174
|
+
throw new Error(`No licence called ${id}`);
|
|
175
|
+
const text = licence_texts_1.LICENCE_TEXTS[licence.id];
|
|
176
|
+
if (!text)
|
|
177
|
+
throw new Error(`No text vendored for ${licence.id}`);
|
|
178
|
+
if (!licence.personalised)
|
|
179
|
+
return text;
|
|
180
|
+
return text
|
|
181
|
+
.replace(/<year>/g, String(year))
|
|
182
|
+
.replace(/<copyright holders?>/g, holder);
|
|
183
|
+
}
|
|
184
|
+
/** The file a licence goes in, which is what every tool looks for. */
|
|
185
|
+
exports.LICENCE_FILE = "LICENSE";
|
|
186
|
+
/**
|
|
187
|
+
* Which licence a file holds, by reading it.
|
|
188
|
+
*
|
|
189
|
+
* Matched on a distinctive line rather than the whole text, because a licence
|
|
190
|
+
* people have edited — a name filled in, a year changed, trailing notes added
|
|
191
|
+
* — is still that licence and should be named as one. A file nobody
|
|
192
|
+
* recognises returns null rather than a guess: saying "MIT" about something
|
|
193
|
+
* that is not MIT is worse than saying nothing.
|
|
194
|
+
*/
|
|
195
|
+
const FLAT = (text) => text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
196
|
+
/*
|
|
197
|
+
A line that appears in one licence and in no other.
|
|
198
|
+
|
|
199
|
+
The first attempt matched each licence's longest line, which is wrong in a
|
|
200
|
+
way that only shows up between relatives: LGPL 3 quotes GPL 3 almost whole,
|
|
201
|
+
BSD 3-Clause is BSD 2-Clause plus a paragraph, and the Unlicense borrows a
|
|
202
|
+
sentence from MIT. Every one of those pairs was answered with the wrong
|
|
203
|
+
member, and a licence named wrongly is worse than one not named at all.
|
|
204
|
+
|
|
205
|
+
So the marker for each is the longest line no other licence contains.
|
|
206
|
+
Computed once from the vendored texts rather than written down, because a
|
|
207
|
+
line chosen by hand stops being unique the moment the catalogue grows.
|
|
208
|
+
*/
|
|
209
|
+
const MARKERS = (() => {
|
|
210
|
+
const flattened = exports.LICENCES.map((licence) => ({
|
|
211
|
+
id: licence.id,
|
|
212
|
+
text: FLAT(licence_texts_1.LICENCE_TEXTS[licence.id] ?? ""),
|
|
213
|
+
}));
|
|
214
|
+
const markers = [];
|
|
215
|
+
for (const licence of exports.LICENCES) {
|
|
216
|
+
const canonical = licence_texts_1.LICENCE_TEXTS[licence.id];
|
|
217
|
+
if (!canonical)
|
|
218
|
+
continue;
|
|
219
|
+
const candidates = canonical
|
|
220
|
+
.split(String.fromCharCode(10))
|
|
221
|
+
.map((line) => line.trim())
|
|
222
|
+
.filter((line) => line.length > 40)
|
|
223
|
+
.sort((left, right) => right.length - left.length);
|
|
224
|
+
const unique = candidates.find((line) => {
|
|
225
|
+
const needle = FLAT(line);
|
|
226
|
+
return !flattened.some((other) => other.id !== licence.id && other.text.includes(needle));
|
|
227
|
+
});
|
|
228
|
+
/*
|
|
229
|
+
A licence with no unique line at all is a subset of another one: every
|
|
230
|
+
line of BSD 2-Clause appears in BSD 3-Clause, which is BSD 2-Clause plus
|
|
231
|
+
a paragraph. Those get their longest line instead, and the ordering
|
|
232
|
+
below is what keeps the answer right — the superset is asked first, so
|
|
233
|
+
the subset is only reached once the longer one has said no.
|
|
234
|
+
*/
|
|
235
|
+
const marker = unique ?? candidates[0];
|
|
236
|
+
if (marker) {
|
|
237
|
+
markers.push({
|
|
238
|
+
id: licence.id,
|
|
239
|
+
marker: FLAT(marker),
|
|
240
|
+
length: canonical.length,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/*
|
|
245
|
+
Longest text first. A licence that quotes another whole — LGPL 3 quoting
|
|
246
|
+
GPL 3, BSD 3-Clause extending BSD 2-Clause — is always the longer of the
|
|
247
|
+
pair, so length is the ordering that puts the more specific one first.
|
|
248
|
+
*/
|
|
249
|
+
return markers.sort((left, right) => right.length - left.length);
|
|
250
|
+
})();
|
|
251
|
+
/**
|
|
252
|
+
* Which licence a file holds, by reading it.
|
|
253
|
+
*
|
|
254
|
+
* Matched on one distinctive line rather than the whole text, because a
|
|
255
|
+
* licence somebody has edited — a name filled in, a year changed, notes
|
|
256
|
+
* appended — is still that licence and should be named as one. A file nobody
|
|
257
|
+
* recognises returns null rather than a guess.
|
|
258
|
+
*/
|
|
259
|
+
function detectLicence(text) {
|
|
260
|
+
const flat = FLAT(text);
|
|
261
|
+
if (!flat)
|
|
262
|
+
return null;
|
|
263
|
+
for (const { id, marker } of MARKERS) {
|
|
264
|
+
if (flat.includes(marker))
|
|
265
|
+
return id;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Give a new project a licence unless somebody has said otherwise.
|
|
271
|
+
*
|
|
272
|
+
* Work published with no licence cannot legally be used by anybody, which is
|
|
273
|
+
* the opposite of what nearly everyone uploading their work intends. Most
|
|
274
|
+
* people never think about it, so the default decides for them either way,
|
|
275
|
+
* and defaulting to "nobody may use this" serves nobody.
|
|
276
|
+
*
|
|
277
|
+
* Two things keep it a nudge rather than a trick: the caller says so where
|
|
278
|
+
* somebody will read it, and it only ever applies to a project's first save.
|
|
279
|
+
* After that an absent LICENSE is a decision, and putting one back would be
|
|
280
|
+
* arguing with them.
|
|
281
|
+
*
|
|
282
|
+
* Returns the licence written, or null when nothing was.
|
|
283
|
+
*/
|
|
284
|
+
async function licenceNewProject(folder, holder, skip = false) {
|
|
285
|
+
/*
|
|
286
|
+
No name, no licence. A copyright line naming nobody is worse than the
|
|
287
|
+
absence it replaces, and a save is not the moment to interrupt somebody
|
|
288
|
+
to ask.
|
|
289
|
+
*/
|
|
290
|
+
if (skip || !holder.trim())
|
|
291
|
+
return null;
|
|
292
|
+
const target = node_path_1.default.join(folder, exports.LICENCE_FILE);
|
|
293
|
+
const already = await (0, promises_1.readFile)(target, "utf8").catch(() => null);
|
|
294
|
+
if (already !== null)
|
|
295
|
+
return null;
|
|
296
|
+
await (0, promises_1.writeFile)(target, licenceText(exports.DEFAULT_LICENCE, holder.trim()), "utf8");
|
|
297
|
+
return exports.DEFAULT_LICENCE;
|
|
298
|
+
}
|
|
@@ -144,8 +144,36 @@ function decide(relativePath, isDirectory, layers) {
|
|
|
144
144
|
}
|
|
145
145
|
return found;
|
|
146
146
|
}
|
|
147
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Whether these layers exclude the path, negations included.
|
|
149
|
+
*
|
|
150
|
+
* An excluded directory takes everything under it, whatever a later negation
|
|
151
|
+
* says. This is git's rule — "it is not possible to re-include a file if a
|
|
152
|
+
* parent directory of that file is excluded" — and it is followed here for a
|
|
153
|
+
* plain reason: the file is called .gitignore, git reads it too, and a rule
|
|
154
|
+
* that means one thing to one reader and something else to the other is worse
|
|
155
|
+
* than either behaviour on its own.
|
|
156
|
+
*
|
|
157
|
+
* It was the other way once, deliberately, on the reasoning that somebody
|
|
158
|
+
* writing `!` meant it. What that produced was a project excluding a folder
|
|
159
|
+
* and publishing two files out of it anyway, because an unrelated
|
|
160
|
+
* `!.env.example` further down reached inside — git had ignored them, so
|
|
161
|
+
* nothing else in the toolchain gave any sign. Being more permissive than git
|
|
162
|
+
* is the dangerous direction to differ in: it uploads what somebody thought
|
|
163
|
+
* they had excluded.
|
|
164
|
+
*/
|
|
148
165
|
function excludes(relativePath, isDirectory, layers) {
|
|
166
|
+
/*
|
|
167
|
+
Ancestors first, outermost in. The moment one is excluded the answer is
|
|
168
|
+
settled and nothing below it can appeal.
|
|
169
|
+
*/
|
|
170
|
+
const parts = relativePath.split("/");
|
|
171
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
172
|
+
const ancestor = parts.slice(0, depth).join("/");
|
|
173
|
+
const above = decide(ancestor, true, layers);
|
|
174
|
+
if (above && !above.negated)
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
149
177
|
const rule = decide(relativePath, isDirectory, layers);
|
|
150
178
|
return Boolean(rule && !rule.negated);
|
|
151
179
|
}
|