@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.
@@ -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
- /** Whether these layers exclude the path, negations included. */
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
  }
@@ -19,6 +19,7 @@ exports.fileDiff = fileDiff;
19
19
  exports.projectTree = projectTree;
20
20
  exports.evaluateRules = evaluateRules;
21
21
  exports.detectPrivateDirectories = detectPrivateDirectories;
22
+ exports.uploadConcerns = uploadConcerns;
22
23
  exports.detectSecrets = detectSecrets;
23
24
  /** Reading a project folder: changed files, diffs, and rule measurement. */
24
25
  const node_child_process_1 = require("node:child_process");
@@ -878,6 +879,30 @@ async function detectPrivateDirectories(root) {
878
879
  }
879
880
  return found;
880
881
  }
882
+ /**
883
+ * Whether a selection should be questioned before it is sent.
884
+ *
885
+ * Lifted out of the window's message handler so it can be exercised without
886
+ * starting one. The desktop is the path a real leak took, and a rule that
887
+ * only runs inside a GUI is a rule nobody can prove: this returns the two
888
+ * lists and takes no view, leaving the refusing to the caller that has a
889
+ * person to ask.
890
+ *
891
+ * Narrowed to the selection on purpose. A folder the rules already leave
892
+ * behind is not being published, and raising it would train people to dismiss
893
+ * the question that matters.
894
+ */
895
+ async function uploadConcerns(root, include) {
896
+ const chosen = new Set(include);
897
+ const [named, folders] = await Promise.all([
898
+ detectSecrets(root),
899
+ detectPrivateDirectories(root),
900
+ ]);
901
+ return {
902
+ secrets: named.filter((file) => chosen.has(file)),
903
+ shielded: folders.filter((finding) => [...chosen].some((file) => file === finding.path || file.startsWith(`${finding.path}/`))),
904
+ };
905
+ }
881
906
  /** Files the flow must ask about before the first upload. */
882
907
  async function detectSecrets(root) {
883
908
  const found = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "CodeRook from the command line, on any operating system",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",