paperlint 2.1.0 → 3.0.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.md +6 -1
- package/README.md +233 -224
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.js.map +1 -0
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +38 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +28 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +277 -265
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +40 -72
- package/dist/doctor.js.map +1 -1
- package/dist/facts-file.d.ts +2 -2
- package/dist/facts-file.js +2 -2
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +39 -29
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +143 -172
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +2 -13
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +63 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +69 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +13 -41
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +71 -71
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -6
- package/dist/presets.js.map +1 -1
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +5 -3
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +15 -6
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +3 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.js +2 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +20 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +119 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +60 -23
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +267 -192
- package/eslint-rules/paper-typography.test.mjs +297 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -31
- package/eslint-rules/pdf-last-page-balance.mjs +4 -7
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -25
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +83 -105
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +4 -13
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build.harness.mjs +8 -4
- package/src/build.ts +62 -6
- package/src/cli.harness.mjs +147 -292
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +344 -304
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/facts-file.test.ts +0 -7
- package/src/facts-file.ts +2 -2
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +186 -230
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/new-paper.test.ts +1 -13
- package/src/new-paper.ts +2 -17
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +84 -121
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/ports/check-references.ts +28 -0
- package/src/presets.ts +5 -15
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +19 -7
- package/src/structure.harness.mjs +3 -1
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +20 -11
- package/src/venue-rules.ts +11 -13
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
package/src/init.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `paperlint init` — the whole install, in the terminal it was typed in.
|
|
3
3
|
*
|
|
4
|
-
* 🔴
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* watching nothing is byte-identical, from outside, to a guard that is working (issue #33).
|
|
4
|
+
* 🔴 THE HOOKS, THE RULES AND THE CLI READ THE PAPERS DIRECTORY FROM ONE PLACE — the root
|
|
5
|
+
* `paperlint.json`, or its default `papers` when the file is absent. An install that wrote it
|
|
6
|
+
* anywhere else would give a `paper-edit-guard` watching a directory that does not exist — and a
|
|
7
|
+
* guard watching nothing is byte-identical, from outside, to a guard that is working (issue #33).
|
|
9
8
|
*
|
|
10
9
|
* The yardstick is how many actions happen between "I want this" and "it works": two, `npm i` and
|
|
11
10
|
* this command (`docs/install.md`). Nothing is left to edit by hand.
|
|
@@ -13,9 +12,9 @@
|
|
|
13
12
|
* ── THE DECISIONS, AND HOW EACH ONE IS MADE ─────────────────────────────────
|
|
14
13
|
* papers directory MEASURED — `detectPapers` walks the repo for a directory whose CHILDREN
|
|
15
14
|
* carry a paper marker. Several hits is the only case a human is asked about.
|
|
16
|
-
* declaration WRITTEN into `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* declaration WRITTEN into the root `paperlint.json` only when it differs from the
|
|
16
|
+
* default `papers`; merged, never overwriting a value that is already there.
|
|
17
|
+
* A project on the defaults gets no config file at all.
|
|
19
18
|
* skills LINKED — one relative symlink per shipped skill into `.claude/skills/`, the
|
|
20
19
|
* only place Claude Code looks for project skills (`link-skills.ts`). An
|
|
21
20
|
* entry of the same name that paperlint did not make is reported, never replaced.
|
|
@@ -27,7 +26,9 @@
|
|
|
27
26
|
* Without a human: NO.
|
|
28
27
|
* first paper OFFERED only to a human and only when the papers directory holds none;
|
|
29
28
|
* without a human only `--paper <name>` creates one (`new-paper.ts`).
|
|
30
|
-
*
|
|
29
|
+
* TeX Live OFFERED to a human, with its size in the question and NO as the default;
|
|
30
|
+
* without a human only named as the next step (`offerTexLive`).
|
|
31
|
+
* other programs REPORTED, never installed. npm's own rule, quoted in husky's write-up:
|
|
31
32
|
* "The only valid use of install or preinstall scripts is for compilation."
|
|
32
33
|
*
|
|
33
34
|
* 🔴 NOTHING IS ASKED WITHOUT A HUMAN — stdin AND stdout a terminal, `CI` unset, no `--yes`
|
|
@@ -39,7 +40,6 @@ import {
|
|
|
39
40
|
existsSync,
|
|
40
41
|
mkdirSync,
|
|
41
42
|
readFileSync,
|
|
42
|
-
unlinkSync,
|
|
43
43
|
writeFileSync,
|
|
44
44
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
45
45
|
} from "node:fs";
|
|
@@ -50,11 +50,9 @@ import { doctor, detectPapers, found, PROGRAMS } from "./doctor.ts";
|
|
|
50
50
|
import { PAPER_MARKERS, papersIn } from "./build.ts";
|
|
51
51
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
52
52
|
import { actionRef } from "./action-ref.ts";
|
|
53
|
-
import { LEGACY_PACKAGE_NAME } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
54
53
|
import {
|
|
55
54
|
FRESH_CLONE_NOTE,
|
|
56
55
|
SETTINGS_PATH,
|
|
57
|
-
UNINSTALL_PLUGIN,
|
|
58
56
|
shippedWiring,
|
|
59
57
|
wireHooks,
|
|
60
58
|
type Merge,
|
|
@@ -69,16 +67,10 @@ import {
|
|
|
69
67
|
// The one source for the consumer's config key lives in the .mjs half of the package (the ESLint
|
|
70
68
|
// rules and the skill scripts import it too); its types are in lib/paper-config.d.mts.
|
|
71
69
|
import {
|
|
72
|
-
|
|
70
|
+
CONFIG_FILE,
|
|
73
71
|
DEFAULT_PAPERS_ROOT,
|
|
74
|
-
LEGACY_CONFIG_KEY,
|
|
75
|
-
LEGACY_PAPER_SETTINGS_FILE,
|
|
76
72
|
PAPERS_DIR_FIELD,
|
|
77
|
-
PAPER_SETTINGS_FILE,
|
|
78
|
-
declaredSettings,
|
|
79
|
-
renamedFieldMessage,
|
|
80
73
|
} from "../lib/paper-config.mjs";
|
|
81
|
-
import { migrationOf } from "./paper-settings.ts";
|
|
82
74
|
|
|
83
75
|
/** How the papers directory was arrived at. Printed, because a guess must not read as a fact. */
|
|
84
76
|
export type PapersHow =
|
|
@@ -151,7 +143,8 @@ export interface PapersChoice {
|
|
|
151
143
|
/**
|
|
152
144
|
* One hit is used, several are asked about, none falls back to the documented default — and the
|
|
153
145
|
* fallback is labelled a guess in the same breath, because the whole class of defect this command
|
|
154
|
-
* exists to close is a guess that later reads as a measurement.
|
|
146
|
+
* exists to close is a guess that later reads as a measurement. Called only when no
|
|
147
|
+
* `paperlint.json` declares a directory yet (`declarePapers`).
|
|
155
148
|
*/
|
|
156
149
|
export async function choosePapers(
|
|
157
150
|
root: string,
|
|
@@ -184,88 +177,74 @@ export async function choosePapers(
|
|
|
184
177
|
|
|
185
178
|
export type DeclarationResult =
|
|
186
179
|
| {
|
|
180
|
+
/** The measured directory differs from the default and was written into `paperlint.json`. */
|
|
187
181
|
readonly status: "written";
|
|
188
182
|
readonly path: string;
|
|
189
183
|
readonly papers: string;
|
|
190
|
-
/**
|
|
191
|
-
readonly
|
|
184
|
+
/** How the directory that was written was arrived at. */
|
|
185
|
+
readonly choice: PapersChoice;
|
|
186
|
+
}
|
|
187
|
+
| {
|
|
188
|
+
/** The measured directory IS the default, so nothing needs writing. */
|
|
189
|
+
readonly status: "default";
|
|
190
|
+
readonly path: string;
|
|
191
|
+
readonly papers: string;
|
|
192
|
+
readonly choice: PapersChoice;
|
|
192
193
|
}
|
|
193
194
|
| {
|
|
194
195
|
readonly status: "kept";
|
|
195
196
|
readonly path: string;
|
|
196
197
|
readonly papers: unknown;
|
|
197
|
-
readonly migrated: boolean;
|
|
198
198
|
}
|
|
199
199
|
| {
|
|
200
200
|
readonly status: "unparsable";
|
|
201
201
|
readonly path: string;
|
|
202
202
|
readonly reason: string;
|
|
203
|
-
}
|
|
204
|
-
| { readonly status: "absent"; readonly path: string }
|
|
205
|
-
| {
|
|
206
|
-
readonly status: "renamed";
|
|
207
|
-
readonly path: string;
|
|
208
|
-
readonly message: string;
|
|
209
203
|
};
|
|
210
204
|
|
|
211
205
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* it is able to spell, and the only path it can always spell is the project's own `package.json`.
|
|
216
|
-
* That asymmetry is the whole reason the declaration lives here rather than in a file of its own:
|
|
217
|
-
* many readers against one (`docs/install.md`, "One declaration").
|
|
206
|
+
* Declares the papers directory in the root `paperlint.json` — the one file the hooks, the rules
|
|
207
|
+
* and the CLI all read — and only when it is not the default: a project whose papers are in
|
|
208
|
+
* `papers/` needs no config file.
|
|
218
209
|
*
|
|
219
210
|
* ⚠️ Merged, not rewritten, and never over a value the consumer set — an `init` that silently
|
|
220
211
|
* replaces a setting is worse than an `init` that does nothing, because the consumer keeps
|
|
221
212
|
* believing the old value.
|
|
222
213
|
*/
|
|
223
|
-
export function declarePapers(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// Settings under the old key move to the new one, in the same position in the file.
|
|
240
|
-
const migrated = found.legacy;
|
|
241
|
-
if (migrated) pkg = renameKey(pkg, LEGACY_CONFIG_KEY, CONFIG_KEY);
|
|
242
|
-
const write = (): void =>
|
|
243
|
-
// Two-space indent and the file's own trailing newline: a declaration is not a licence to
|
|
244
|
-
// reformat somebody else's file, and a one-line diff is a diff a consumer will actually read.
|
|
245
|
-
writeFileSync(
|
|
246
|
-
path,
|
|
247
|
-
JSON.stringify(pkg, null, 2) + (raw.endsWith("\n") ? "\n" : ""),
|
|
248
|
-
"utf8",
|
|
249
|
-
);
|
|
250
|
-
const existing = pkg?.[CONFIG_KEY]?.[PAPERS_DIR_FIELD];
|
|
251
|
-
if (existing !== undefined) {
|
|
252
|
-
if (migrated) write();
|
|
253
|
-
return { status: "kept", path, papers: existing, migrated };
|
|
214
|
+
export async function declarePapers(
|
|
215
|
+
root: string,
|
|
216
|
+
choose: () => Promise<PapersChoice>,
|
|
217
|
+
): Promise<DeclarationResult> {
|
|
218
|
+
const path = join(root, CONFIG_FILE);
|
|
219
|
+
const raw = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
220
|
+
let settings: Record<string, unknown> = {};
|
|
221
|
+
if (raw !== null) {
|
|
222
|
+
try {
|
|
223
|
+
settings = JSON.parse(raw) as Record<string, unknown>;
|
|
224
|
+
} catch (e) {
|
|
225
|
+
return { status: "unparsable", path, reason: (e as Error).message };
|
|
226
|
+
}
|
|
227
|
+
const existing = settings[PAPERS_DIR_FIELD];
|
|
228
|
+
if (existing !== undefined)
|
|
229
|
+
return { status: "kept", path, papers: existing };
|
|
254
230
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
231
|
+
// 🔴 ONLY NOW is the directory measured (or asked about). A declared one is the answer, and a
|
|
232
|
+
// candidate measured beside it is a decision nobody takes.
|
|
233
|
+
const choice = await choose();
|
|
234
|
+
if (choice.papers === DEFAULT_PAPERS_ROOT)
|
|
235
|
+
return { status: "default", path, papers: choice.papers, choice };
|
|
236
|
+
// Two-space indent and the file's own trailing newline: a declaration is not a licence to
|
|
237
|
+
// reformat somebody else's file, and a one-line diff is a diff a consumer will actually read.
|
|
238
|
+
writeFileSync(
|
|
239
|
+
path,
|
|
240
|
+
JSON.stringify(
|
|
241
|
+
{ ...settings, [PAPERS_DIR_FIELD]: choice.papers },
|
|
242
|
+
null,
|
|
243
|
+
2,
|
|
244
|
+
) + (raw === null || raw.endsWith("\n") ? "\n" : ""),
|
|
245
|
+
"utf8",
|
|
268
246
|
);
|
|
247
|
+
return { status: "written", path, papers: choice.papers, choice };
|
|
269
248
|
}
|
|
270
249
|
|
|
271
250
|
export const WORKFLOW_PATH = join(".github", "workflows", "papers.yml");
|
|
@@ -387,10 +366,18 @@ export function missingPrograms(
|
|
|
387
366
|
* session at all. `init` now writes the same three hook commands into `.claude/settings.json`
|
|
388
367
|
* itself (`hooks-settings.ts`), so there is nothing left to type anywhere but here.
|
|
389
368
|
*/
|
|
390
|
-
export function nextSteps(
|
|
369
|
+
export function nextSteps(
|
|
370
|
+
papersDir: string = DEFAULT_PAPERS_ROOT,
|
|
371
|
+
{ toolchain = false }: { toolchain?: boolean } = {},
|
|
372
|
+
): string {
|
|
391
373
|
return [
|
|
392
374
|
``,
|
|
393
375
|
`next: npx paperlint new <name> # start a paper in ${papersDir}/ from the template`,
|
|
376
|
+
...(toolchain
|
|
377
|
+
? [
|
|
378
|
+
` npx paperlint toolchain # TeX Live for building (${TOOLCHAIN_COST})`,
|
|
379
|
+
]
|
|
380
|
+
: []),
|
|
394
381
|
` npx paperlint lint # runs every rule over ${papersDir}`,
|
|
395
382
|
``,
|
|
396
383
|
].join("\n");
|
|
@@ -503,18 +490,8 @@ export function reportHooks(
|
|
|
503
490
|
: ` ✓ already wired in ${here(outcome.path)} — nothing changed`,
|
|
504
491
|
);
|
|
505
492
|
out.push(` ${how}`);
|
|
506
|
-
if (outcome.status === "written" && outcome.replaced > 0)
|
|
507
|
-
out.push(
|
|
508
|
-
` replaced ${String(outcome.replaced)} command(s) that pointed into ${LEGACY_PACKAGE_NAME}, the package's old name`,
|
|
509
|
-
);
|
|
510
493
|
out.push(` ${FRESH_CLONE_NOTE}`);
|
|
511
494
|
}
|
|
512
|
-
if (outcome.plugin.length > 0)
|
|
513
|
-
out.push(
|
|
514
|
-
` ⚠ this project also enables the plugin (${outcome.plugin.join(", ")}) — with it every hook runs twice.`,
|
|
515
|
-
` the plugin no longer carries the hooks: ${UNINSTALL_PLUGIN}`,
|
|
516
|
-
` and remove it from "enabledPlugins" in ${SETTINGS_PATH}`,
|
|
517
|
-
);
|
|
518
495
|
return out;
|
|
519
496
|
}
|
|
520
497
|
|
|
@@ -539,8 +516,7 @@ export function reportSkillLinks(
|
|
|
539
516
|
return out;
|
|
540
517
|
}
|
|
541
518
|
const by = (s: string) => report.links.filter((l) => l.status === s);
|
|
542
|
-
const
|
|
543
|
-
const created = [...by("created"), ...replaced];
|
|
519
|
+
const created = by("created");
|
|
544
520
|
const present = by("present");
|
|
545
521
|
const skipped = by("foreign");
|
|
546
522
|
// Only a read-only call leaves anything `missing`; counted anyway, so the sum always adds up.
|
|
@@ -555,10 +531,6 @@ export function reportSkillLinks(
|
|
|
555
531
|
out.push(
|
|
556
532
|
` ${join(here(report.home), "<name>")} → ${join(dirname(report.example), "<name>")}`,
|
|
557
533
|
);
|
|
558
|
-
if (replaced.length)
|
|
559
|
-
out.push(
|
|
560
|
-
` ${String(replaced.length)} of them replaced a link into ${LEGACY_PACKAGE_NAME}, the package's old name`,
|
|
561
|
-
);
|
|
562
534
|
if (skipped.length) {
|
|
563
535
|
out.push(
|
|
564
536
|
` left untouched — the name is taken by something paperlint did not make:`,
|
|
@@ -615,76 +587,14 @@ export interface InitOptions {
|
|
|
615
587
|
* workflow is pinned to its release tag (`actionRef`); absent or unreleased, the placeholder.
|
|
616
588
|
*/
|
|
617
589
|
version?: string;
|
|
590
|
+
/**
|
|
591
|
+
* TeX Live for `paperlint build`: whether paperlint's own tree is installed, and how to install
|
|
592
|
+
* it (`paperlint toolchain`). Passed in by the CLI; without them the step only names the command.
|
|
593
|
+
*/
|
|
594
|
+
tex?: { readonly installed: () => boolean; readonly install?: () => number };
|
|
618
595
|
}
|
|
619
596
|
|
|
620
597
|
/** Reads one line from a real terminal. Kept out of `init` so the command stays testable. */
|
|
621
|
-
/**
|
|
622
|
-
* Move every paper's pre-2.1.0 `venue.json` to `paperlint.json`, the way the old package.json key
|
|
623
|
-
* is moved, and in the same step `"venue": "aisec"` becomes `"extends": "paperlint:aisec"`: written
|
|
624
|
-
* as `paperlint.json` when it is alone, removed when `paperlint.json` already says the same, and
|
|
625
|
-
* REFUSED — both files left as they are — when they differ, since there is no way to know which
|
|
626
|
-
* one the author means. `code` is 2 when anything was refused.
|
|
627
|
-
*/
|
|
628
|
-
export function migratePaperSettings(papersAbs: string): {
|
|
629
|
-
readonly code: number;
|
|
630
|
-
readonly lines: readonly string[];
|
|
631
|
-
} {
|
|
632
|
-
const lines: string[] = [];
|
|
633
|
-
let code = 0;
|
|
634
|
-
for (const dir of papersIn(papersAbs, [
|
|
635
|
-
...PAPER_MARKERS,
|
|
636
|
-
LEGACY_PAPER_SETTINGS_FILE,
|
|
637
|
-
])) {
|
|
638
|
-
const r = migrateOne(dir, (p) => relative(papersAbs, p));
|
|
639
|
-
if (r === null) continue;
|
|
640
|
-
lines.push(r.line);
|
|
641
|
-
if (r.refused) code = 2;
|
|
642
|
-
}
|
|
643
|
-
return {
|
|
644
|
-
code,
|
|
645
|
-
lines: lines.length ? ["", "paper settings", ...lines] : [],
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/** One paper's move: done, and the line that says so — or null when there is nothing to move. */
|
|
650
|
-
function migrateOne(
|
|
651
|
-
dir: string,
|
|
652
|
-
shown: (p: string) => string,
|
|
653
|
-
): { readonly line: string; readonly refused: boolean } | null {
|
|
654
|
-
const [from, to] = [LEGACY_PAPER_SETTINGS_FILE, PAPER_SETTINGS_FILE].map(
|
|
655
|
-
(f) => join(dir, f),
|
|
656
|
-
) as [string, string];
|
|
657
|
-
const read = (p: string) => (existsSync(p) ? readFileSync(p) : null);
|
|
658
|
-
const plan = migrationOf(read(from), read(to));
|
|
659
|
-
switch (plan.kind) {
|
|
660
|
-
case "none":
|
|
661
|
-
return null;
|
|
662
|
-
case "move":
|
|
663
|
-
writeFileSync(to, plan.text);
|
|
664
|
-
unlinkSync(from);
|
|
665
|
-
return {
|
|
666
|
-
line: ` ✓ ${shown(from)} → ${shown(to)} ("venue" is now "extends": "paperlint:<name>"; renamed in paperlint 2.1.0)`,
|
|
667
|
-
refused: false,
|
|
668
|
-
};
|
|
669
|
-
case "drop-legacy":
|
|
670
|
-
unlinkSync(from);
|
|
671
|
-
return {
|
|
672
|
-
line: ` ✓ ${shown(from)} removed — ${shown(to)} already says the same`,
|
|
673
|
-
refused: false,
|
|
674
|
-
};
|
|
675
|
-
case "conflict":
|
|
676
|
-
return {
|
|
677
|
-
line: ` ✗ ${shown(from)} and ${shown(to)} both exist and differ — nothing was moved. Keep ${PAPER_SETTINGS_FILE}, copy what you need from ${LEGACY_PAPER_SETTINGS_FILE} into it (its "venue": "x" is "extends": "paperlint:x"), delete ${LEGACY_PAPER_SETTINGS_FILE}, then run init again`,
|
|
678
|
-
refused: true,
|
|
679
|
-
};
|
|
680
|
-
case "broken":
|
|
681
|
-
return {
|
|
682
|
-
line: ` ✗ ${shown(from)} was not moved: ${plan.why}`,
|
|
683
|
-
refused: true,
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
598
|
export async function askOnTerminal(question: string): Promise<string> {
|
|
689
599
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
690
600
|
const { createInterface } = await import("node:readline/promises");
|
|
@@ -697,6 +607,92 @@ export async function askOnTerminal(question: string): Promise<string> {
|
|
|
697
607
|
}
|
|
698
608
|
}
|
|
699
609
|
|
|
610
|
+
/** What `paperlint toolchain` costs, said wherever it is offered. */
|
|
611
|
+
export const TOOLCHAIN_COST = "~270 MB, ~3 min, once";
|
|
612
|
+
|
|
613
|
+
export type TexOutcome =
|
|
614
|
+
"installed" | "was-installed" | "failed" | "declined" | "not-asked";
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* TeX Live, for `paperlint build` — `paperlint lint` needs none. OFFERED to a human with its cost
|
|
618
|
+
* in the question, default NO (270 MB is not a default anyone should get by pressing Enter);
|
|
619
|
+
* without a human only named as the next step. Nothing is ever installed unasked.
|
|
620
|
+
*/
|
|
621
|
+
export async function offerTexLive(
|
|
622
|
+
tex: InitOptions["tex"],
|
|
623
|
+
{
|
|
624
|
+
ask,
|
|
625
|
+
interactive,
|
|
626
|
+
}: { ask?: (q: string) => Promise<string>; interactive: boolean },
|
|
627
|
+
): Promise<TexOutcome> {
|
|
628
|
+
if (tex?.installed()) return "was-installed";
|
|
629
|
+
if (!interactive || !ask || !tex?.install) return "not-asked";
|
|
630
|
+
const answer = (
|
|
631
|
+
await askOrDefault(
|
|
632
|
+
ask,
|
|
633
|
+
` install TeX Live now, for \`paperlint build\`? (${TOOLCHAIN_COST}) [y/N] `,
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
?.trim()
|
|
637
|
+
.toLowerCase();
|
|
638
|
+
if (answer !== "y" && answer !== "yes") return "declined";
|
|
639
|
+
return tex.install() === 0 ? "installed" : "failed";
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** What `init` says about TeX Live. */
|
|
643
|
+
export function reportTexLive(t: TexOutcome, why: string): string[] {
|
|
644
|
+
const cmd = `npx paperlint toolchain # ${TOOLCHAIN_COST}`;
|
|
645
|
+
if (t === "was-installed") return [` ✓ paperlint's TeX Live is installed`];
|
|
646
|
+
if (t === "installed") return [` ✓ installed`];
|
|
647
|
+
if (t === "failed")
|
|
648
|
+
return [
|
|
649
|
+
` ✗ the install failed — see above. Run it again:`,
|
|
650
|
+
` ${cmd}`,
|
|
651
|
+
];
|
|
652
|
+
return [
|
|
653
|
+
t === "declined"
|
|
654
|
+
? ` · declined — nothing installed. When you want to build:`
|
|
655
|
+
: ` · not installed — ${why}, so nothing was asked. To build, run once:`,
|
|
656
|
+
` ${cmd}`,
|
|
657
|
+
];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** The "papers directory" section of init's report: how the directory was arrived at. */
|
|
661
|
+
function papersLines(decl: DeclarationResult, why: string): string[] {
|
|
662
|
+
const out = [``, `papers directory`];
|
|
663
|
+
if (decl.status === "kept" && typeof decl.papers === "string")
|
|
664
|
+
return [
|
|
665
|
+
...out,
|
|
666
|
+
` ✓ ${decl.papers} — declared in ${CONFIG_FILE} → "${PAPERS_DIR_FIELD}"; nothing measured`,
|
|
667
|
+
];
|
|
668
|
+
if (decl.status !== "written" && decl.status !== "default") return [];
|
|
669
|
+
const { choice } = decl;
|
|
670
|
+
if (choice.how === "detected")
|
|
671
|
+
return [
|
|
672
|
+
...out,
|
|
673
|
+
` ✓ ${choice.papers} — measured: its subdirectories carry ${PAPER_MARKERS.join(" / ")}`,
|
|
674
|
+
];
|
|
675
|
+
if (choice.how === "chosen")
|
|
676
|
+
return [
|
|
677
|
+
...out,
|
|
678
|
+
` ✓ ${choice.papers} — you picked it out of ${String(choice.candidates.length)} candidates`,
|
|
679
|
+
];
|
|
680
|
+
if (choice.how === "not-asked" || choice.how === "no-answer")
|
|
681
|
+
return [
|
|
682
|
+
...out,
|
|
683
|
+
` ✓ ${choice.papers} — ${String(choice.candidates.length)} candidates, ` +
|
|
684
|
+
(choice.how === "not-asked"
|
|
685
|
+
? `${why} so nothing was asked`
|
|
686
|
+
: `no answer was given, so the first one was taken`),
|
|
687
|
+
` the others: ${choice.candidates.slice(1).join(", ")} — set "${PAPERS_DIR_FIELD}" in ${CONFIG_FILE} if this is the wrong one`,
|
|
688
|
+
];
|
|
689
|
+
return [
|
|
690
|
+
...out,
|
|
691
|
+
` · ${choice.papers} — the default. Nothing here looks like a papers directory yet;`,
|
|
692
|
+
` \`npx paperlint new <name>\` creates the first paper there.`,
|
|
693
|
+
];
|
|
694
|
+
}
|
|
695
|
+
|
|
700
696
|
// Documented in README.md#install-and-set-up — update it when this changes.
|
|
701
697
|
export async function init(
|
|
702
698
|
dir: string,
|
|
@@ -735,44 +731,21 @@ export async function init(
|
|
|
735
731
|
log(``);
|
|
736
732
|
log(`paperlint init — each decision below says HOW it was decided`);
|
|
737
733
|
|
|
738
|
-
// ── 1. where the papers are
|
|
739
|
-
|
|
734
|
+
// ── 1. where the papers are, and 2. the declaration, in paperlint.json when not the default ─
|
|
735
|
+
// The declaration is read FIRST: the directory is measured only when none is declared.
|
|
736
|
+
const decl = await declarePapers(root, () =>
|
|
737
|
+
choosePapers(root, { ask, interactive }),
|
|
738
|
+
);
|
|
739
|
+
for (const line of papersLines(decl, why)) log(line);
|
|
740
740
|
log(``);
|
|
741
|
-
log(`
|
|
742
|
-
if (
|
|
743
|
-
log(
|
|
744
|
-
` ✓ ${choice.papers} — measured: its subdirectories carry ${PAPER_MARKERS.join(" / ")}`,
|
|
745
|
-
);
|
|
746
|
-
else if (choice.how === "chosen")
|
|
747
|
-
log(
|
|
748
|
-
` ✓ ${choice.papers} — you picked it out of ${String(choice.candidates.length)} candidates`,
|
|
749
|
-
);
|
|
750
|
-
else if (choice.how === "not-asked" || choice.how === "no-answer") {
|
|
751
|
-
log(
|
|
752
|
-
` ✓ ${choice.papers} — ${String(choice.candidates.length)} candidates, ` +
|
|
753
|
-
(choice.how === "not-asked"
|
|
754
|
-
? `${why} so nothing was asked`
|
|
755
|
-
: `no answer was given, so the first one was taken`),
|
|
756
|
-
);
|
|
757
|
-
log(
|
|
758
|
-
` the others: ${choice.candidates.slice(1).join(", ")} — change it in package.json if this is the wrong one`,
|
|
759
|
-
);
|
|
760
|
-
} else {
|
|
761
|
-
log(
|
|
762
|
-
` ⚠ ${choice.papers} — A GUESS. Nothing here looks like a papers directory yet.`,
|
|
763
|
-
);
|
|
741
|
+
log(`settings`);
|
|
742
|
+
if (decl.status === "written")
|
|
764
743
|
log(
|
|
765
|
-
`
|
|
744
|
+
` ✓ ${here(decl.path)} → { "${PAPERS_DIR_FIELD}": ${JSON.stringify(decl.papers)} }`,
|
|
766
745
|
);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
// ── 2. one declaration, in package.json ───────────────────────────────────────────────
|
|
770
|
-
log(``);
|
|
771
|
-
log(`declaration`);
|
|
772
|
-
const decl = declarePapers(root, choice.papers);
|
|
773
|
-
if (decl.status === "written")
|
|
746
|
+
else if (decl.status === "default")
|
|
774
747
|
log(
|
|
775
|
-
` ✓
|
|
748
|
+
` ✓ nothing to write — "${decl.papers}" is the default, so no ${CONFIG_FILE} is needed`,
|
|
776
749
|
);
|
|
777
750
|
else if (decl.status === "kept") {
|
|
778
751
|
if (typeof decl.papers !== "string") {
|
|
@@ -784,13 +757,7 @@ export async function init(
|
|
|
784
757
|
log(
|
|
785
758
|
` ✓ ${here(decl.path)} already declares ${PAPERS_DIR_FIELD} = ${JSON.stringify(decl.papers)} — kept, nothing overwritten`,
|
|
786
759
|
);
|
|
787
|
-
} else
|
|
788
|
-
err(` ✗ ${decl.message}`);
|
|
789
|
-
err(
|
|
790
|
-
` nothing was written. Fix it in ${here(decl.path)}, then run init again.`,
|
|
791
|
-
);
|
|
792
|
-
return 2;
|
|
793
|
-
} else if (decl.status === "unparsable") {
|
|
760
|
+
} else {
|
|
794
761
|
err(` ✗ ${here(decl.path)} is not valid JSON: ${decl.reason}`);
|
|
795
762
|
err(
|
|
796
763
|
` nothing was written. The hooks read their papers directory from this file and`,
|
|
@@ -799,32 +766,11 @@ export async function init(
|
|
|
799
766
|
` refuse every Bash command while it cannot be parsed — fix the JSON first.`,
|
|
800
767
|
);
|
|
801
768
|
return 2;
|
|
802
|
-
} else {
|
|
803
|
-
err(
|
|
804
|
-
` ✗ no package.json at ${here(root)} — there is nowhere to put the declaration.`,
|
|
805
|
-
);
|
|
806
|
-
err(
|
|
807
|
-
` The hooks can only read a path they are able to name, and that path is`,
|
|
808
|
-
);
|
|
809
|
-
err(
|
|
810
|
-
` package.json. Run \`npm init -y\` here, then \`npx paperlint init\` again.`,
|
|
811
|
-
);
|
|
812
|
-
return 2;
|
|
813
769
|
}
|
|
814
|
-
if (decl.migrated)
|
|
815
|
-
log(
|
|
816
|
-
` ✓ moved the settings from "${LEGACY_CONFIG_KEY}" (the old key) to "${CONFIG_KEY}"`,
|
|
817
|
-
);
|
|
818
|
-
log(
|
|
819
|
-
` one declaration — the hooks, the rules and the CLI all read this one key`,
|
|
820
|
-
);
|
|
821
770
|
// Every step below uses the DECLARED directory. A kept declaration outranks what init
|
|
822
771
|
// measured or guessed: otherwise the first paper and the workflow would land in the
|
|
823
772
|
// guessed directory while lint and the hooks keep reading the declared one.
|
|
824
|
-
const papersDir =
|
|
825
|
-
decl.status === "kept" ? (decl.papers as string) : choice.papers;
|
|
826
|
-
const moved = migratePaperSettings(resolve(root, papersDir));
|
|
827
|
-
for (const line of moved.lines) (moved.code ? err : log)(line);
|
|
773
|
+
const papersDir = decl.papers as string;
|
|
828
774
|
|
|
829
775
|
// ── 3. the skills, linked where Claude Code looks for them ─────────────────────────────
|
|
830
776
|
for (const line of reportSkillLinks(link(root), here)) log(line);
|
|
@@ -893,7 +839,13 @@ export async function init(
|
|
|
893
839
|
` · none yet${interactive ? "" : ` — ${why}, so nothing was asked`}. \`npx paperlint new <name>\` or \`--paper <name>\` creates one`,
|
|
894
840
|
);
|
|
895
841
|
|
|
896
|
-
// ── 7.
|
|
842
|
+
// ── 7. TeX Live — offered to a human with its cost, never installed unasked ─────────────
|
|
843
|
+
log(``);
|
|
844
|
+
log(`TeX Live (for \`paperlint build\`; \`paperlint lint\` needs none)`);
|
|
845
|
+
const texOutcome = await offerTexLive(opts.tex, { ask, interactive });
|
|
846
|
+
for (const line of reportTexLive(texOutcome, why)) log(line);
|
|
847
|
+
|
|
848
|
+
// ── 8. the other programs are reported, never installed ────────────────────────────────
|
|
897
849
|
log(``);
|
|
898
850
|
log(
|
|
899
851
|
`external programs (the skills shell out to these; \`paperlint lint\` needs none of them)`,
|
|
@@ -921,9 +873,13 @@ export async function init(
|
|
|
921
873
|
log(` ${cmd}`);
|
|
922
874
|
}
|
|
923
875
|
|
|
924
|
-
log(
|
|
876
|
+
log(
|
|
877
|
+
nextSteps(papersDir, {
|
|
878
|
+
toolchain: texOutcome !== "was-installed" && texOutcome !== "installed",
|
|
879
|
+
}),
|
|
880
|
+
);
|
|
925
881
|
|
|
926
|
-
// ──
|
|
882
|
+
// ── 9. the install states its own condition ───────────────────────────────────────────
|
|
927
883
|
log(`── paperlint doctor ${"─".repeat(56)}`);
|
|
928
884
|
const cliPapers = resolveCliPapers ? resolveCliPapers(root) : papersDir;
|
|
929
885
|
const code = doctor({ log, cwd: root, projectDir: root, run, cliPapers });
|
|
@@ -932,5 +888,5 @@ export async function init(
|
|
|
932
888
|
`doctor exits ${String(code)} — the install is NOT finished. The lines marked ✗ above say what is\n` +
|
|
933
889
|
`left; re-run \`npx paperlint doctor\` once you have done them.`,
|
|
934
890
|
);
|
|
935
|
-
return paperCode ||
|
|
891
|
+
return paperCode || code;
|
|
936
892
|
}
|
|
@@ -35,11 +35,7 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
35
35
|
const { linkSkills, locatePackage, shippedSkills } = await import(
|
|
36
36
|
join(HERE, "link-skills.ts")
|
|
37
37
|
);
|
|
38
|
-
const {
|
|
39
|
-
SHIPPED_SKILLS_DIR: SHIPS,
|
|
40
|
-
PACKAGE_NAME: PKG,
|
|
41
|
-
LEGACY_PACKAGE_NAME,
|
|
42
|
-
} = await import(
|
|
38
|
+
const { SHIPPED_SKILLS_DIR: SHIPS, PACKAGE_NAME: PKG } = await import(
|
|
43
39
|
join(HERE, "..", "skills", "paper-pipeline", "scripts", "consumer.mjs")
|
|
44
40
|
);
|
|
45
41
|
|
|
@@ -222,39 +218,6 @@ try {
|
|
|
222
218
|
}
|
|
223
219
|
|
|
224
220
|
// ── VI-bis. LINKS LEFT BY AN INSTALL UNDER THE OLD NAME ARE REPLACED, NOT SKIPPED ─────
|
|
225
|
-
// After `npm rm research-paper-pipeline && npm i -D paperlint` every old link dangles. They are
|
|
226
|
-
// ours, spelled the way an older `init` wrote them, so the next `init` replaces them.
|
|
227
|
-
{
|
|
228
|
-
const dir = consumer("renamed");
|
|
229
|
-
mkdirSync(home(dir), { recursive: true });
|
|
230
|
-
const old = join(
|
|
231
|
-
"..",
|
|
232
|
-
"..",
|
|
233
|
-
"node_modules",
|
|
234
|
-
LEGACY_PACKAGE_NAME,
|
|
235
|
-
SHIPS,
|
|
236
|
-
"alpha",
|
|
237
|
-
);
|
|
238
|
-
symlinkSync(old, join(home(dir), "alpha"), "dir");
|
|
239
|
-
const seen = linkSkills(dir, { write: false });
|
|
240
|
-
check(
|
|
241
|
-
"doctor's read names a link into the old package name, with the command that fixes it",
|
|
242
|
-
status(seen, "alpha")?.status === "foreign" &&
|
|
243
|
-
/old name — `npx paperlint init` replaces it/.test(
|
|
244
|
-
status(seen, "alpha")?.reason ?? "",
|
|
245
|
-
),
|
|
246
|
-
);
|
|
247
|
-
const r = linkSkills(dir);
|
|
248
|
-
check(
|
|
249
|
-
"🔴 init REPLACES it: status `replaced`, and SKILL.md is reachable through the new link",
|
|
250
|
-
status(r, "alpha")?.status === "replaced" &&
|
|
251
|
-
existsSync(join(home(dir), "alpha", "SKILL.md")) &&
|
|
252
|
-
readlinkSync(join(home(dir), "alpha")).includes(
|
|
253
|
-
join("node_modules", PKG),
|
|
254
|
-
),
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
221
|
// ── VII. A PACKAGE WITHOUT ITS SKILLS DIRECTORY IS AN ERROR, NOT ZERO SKILLS ─────────
|
|
259
222
|
{
|
|
260
223
|
const pkg = join(work, "no-skills");
|