cyber-sdd 0.7.0 → 0.7.2

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.
Files changed (150) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/CHANGELOG.md +320 -0
  4. package/agents/sdd-scanner.md +1 -1
  5. package/package.json +11 -12
  6. package/plugin.json +55 -0
  7. package/skills/align-spec/SKILL.md +1 -1
  8. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  9. package/skills/blast-estimate/README.md +3 -3
  10. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  11. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  12. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  13. package/skills/check-plan-safety/SKILL.md +2 -3
  14. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  15. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  16. package/skills/check-project-specs/SKILL.md +1 -1
  17. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  18. package/skills/check-retired-terms/SKILL.md +2 -3
  19. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  20. package/skills/check-scenario-overlap/SKILL.md +1 -2
  21. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  22. package/skills/check-spec-references/SKILL.md +2 -2
  23. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  24. package/skills/check-spec-structure/SKILL.md +1 -2
  25. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  26. package/skills/collision-ladder/README.md +2 -2
  27. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  28. package/skills/combat-log-governance/SKILL.md +1 -1
  29. package/skills/concept-index/SKILL.md +1 -1
  30. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  31. package/skills/discover-plans/README.md +3 -3
  32. package/skills/discover-plans/SKILL.md +1 -1
  33. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  34. package/skills/discover-specs/README.md +3 -3
  35. package/skills/discover-specs/SKILL.md +1 -1
  36. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  37. package/skills/doctrine-loop/SKILL.md +1 -1
  38. package/skills/gate-validation-governance/SKILL.md +1 -1
  39. package/skills/init/README.md +2 -2
  40. package/skills/init/scripts/wire-statusline.mjs +168 -12
  41. package/skills/manage-ignore/README.md +3 -3
  42. package/skills/manage-ignore/SKILL.md +1 -2
  43. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  44. package/skills/manage-scenario-bridge/README.md +3 -3
  45. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  46. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  47. package/skills/manage-spec-anchors/README.md +3 -3
  48. package/skills/manage-spec-anchors/SKILL.md +1 -1
  49. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  50. package/skills/mission-graph/README.md +2 -2
  51. package/skills/mission-graph/SKILL.md +1 -1
  52. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  53. package/skills/pause-mission/SKILL.md +1 -1
  54. package/skills/place-node/SKILL.md +2 -2
  55. package/skills/place-node/scripts/place-node.mjs +117 -12
  56. package/skills/plan-retirement/README.md +2 -2
  57. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  58. package/skills/resolve-governances/README.md +3 -3
  59. package/skills/resolve-governances/SKILL.md +1 -1
  60. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  61. package/skills/resolve-tracking/SKILL.md +1 -1
  62. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  63. package/skills/scaffold-project-spec/SKILL.md +1 -1
  64. package/skills/spec-gate/README.md +12 -9
  65. package/skills/spec-gate/SKILL.md +2 -2
  66. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  67. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  68. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  69. package/skills/spec-producer-governance/SKILL.md +2 -2
  70. package/skills/start-mission/SKILL.md +1 -1
  71. package/skills/suite-format-governance/README.md +1 -1
  72. package/skills/suite-format-governance/SKILL.md +1 -1
  73. package/skills/touch-set-correction/README.md +5 -5
  74. package/skills/touch-set-correction/SKILL.md +1 -1
  75. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  76. package/skills/verify-scenarios/README.md +6 -6
  77. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  78. package/.plugin/pins.json +0 -3
  79. package/.plugin/plugin.json +0 -19
  80. package/dist/align-spec/scripts/align-spec.js +0 -102
  81. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  82. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  83. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  84. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  85. package/dist/concept-index/scripts/concept-index.js +0 -181
  86. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  87. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  88. package/dist/spec-gate/scripts/check-suite.js +0 -317
  89. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  90. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  91. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  92. package/skills/align-spec/scripts/align-spec.mts +0 -188
  93. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  94. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  95. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  96. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  97. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  98. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  99. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  100. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  102. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  103. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  104. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  105. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  106. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  108. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  109. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  110. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  111. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  112. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  113. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  114. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  115. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  116. package/skills/concept-index/scripts/concept-index.mts +0 -246
  117. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  118. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  119. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  120. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  121. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  122. package/skills/init/scripts/wire-statusline.mts +0 -279
  123. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  124. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  126. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  128. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  129. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  130. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  131. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  132. package/skills/place-node/scripts/place-node.mts +0 -158
  133. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  134. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  135. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  136. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  137. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  138. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  139. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  140. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  141. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  142. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  143. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  144. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  145. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  146. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  147. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  148. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  149. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  150. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
@@ -1,148 +0,0 @@
1
- #!/usr/bin/env node
2
- // check-plan-safety — the concrete guard engine for the plan brief's safe-to-publish floor.
3
- // Scans the SDD plan directory (.agents/plans) for the *.md handoff artifacts a mission commits
4
- // — the `<cr-ref>.plan.md` brief and its sibling design docs — and flags any machine-local
5
- // reference that must never enter git history in a tracked, portable artifact:
6
- //
7
- // - a home-directory absolute path (`/home/<user>/…`, `/Users/<user>/…`, `C:\Users\<user>\…`)
8
- // — carries the OS username (privacy) and resolves on no other checkout (portability), and
9
- // - a shell expansion of the user's home / identity (`$HOME`, `${HOME}`, `$USER`, `%USERPROFILE%`).
10
- //
11
- // A bare `~/` is deliberately NOT flagged: it carries no username and legitimately appears in
12
- // design prose describing home-rooted feature paths (e.g. a tool's own `~/.<tool>/` data root).
13
- // The floor mirrors the combat-log's "never committed: … absolute paths, OS usernames" rule
14
- // (combat-log-governance), extended from the ledger to the plan brief.
15
- //
16
- // Pure functions are exported for node:test; running the file directly drives the CLI. No
17
- // dependencies (the repo's node-≥23.6 / no-deps convention). Read-only: it writes nothing.
18
- // Default output is TOON (the token-efficient tabular form); --format json for a flat array.
19
- // --check is the CI guard: exit non-zero iff any leak is found.
20
-
21
- import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
22
- import { join } from 'node:path'
23
- import { pathToFileURL } from 'node:url'
24
-
25
- export interface Leak {
26
- /** Repo-relative path of the file the leak sits in. */
27
- file: string
28
- /** 1-indexed line number. */
29
- line: number
30
- /** The leak class — `home-abs-path`, `env-home`, or `env-user`. */
31
- kind: string
32
- /** The matched fragment (capped), so a reader can locate and scrub it. */
33
- token: string
34
- }
35
-
36
- // The machine-local reference patterns. Each carries the leak `kind` it detects. Global flag so a
37
- // single line with several leaks reports each. USER/HOME env forms use a negative lookahead so
38
- // `$HOMEBREW` / `$USERDATA` (a different variable that merely starts with the name) is not flagged.
39
- export const LEAK_PATTERNS: { kind: string; re: RegExp }[] = [
40
- // /home/<user>/… and /Users/<user>/… — the home-abs path the leak class is named for.
41
- // Also catches a forward-slash Windows profile (`C:/Users/…` contains `/Users/…`).
42
- { kind: 'home-abs-path', re: /\/(?:home|Users)\/[^\s'"`)\]]+/g },
43
- // Backslash Windows user profile — C:\Users\<user>\… (the forward-slash form is caught above).
44
- { kind: 'home-abs-path', re: /[A-Za-z]:\\Users\\[^\s'"`)\]]+/g },
45
- { kind: 'env-home', re: /\$\{?HOME\}?(?![A-Za-z])|%USERPROFILE%|%HOMEPATH%/g },
46
- { kind: 'env-user', re: /\$\{?USER\}?(?![A-Za-z])|%USERNAME%/g },
47
- ]
48
-
49
- /** Cap a matched token so a pathological long path does not blow up the report. */
50
- function capToken(t: string): string {
51
- return t.length > 80 ? `${t.slice(0, 77)}...` : t
52
- }
53
-
54
- // ── Scan one file's text ──
55
- // Every machine-local reference in `text`, tagged with `file` (the repo-relative path the caller
56
- // supplies) and its 1-indexed line. Lines are scanned independently; a line with N leaks yields N.
57
- export function scanText(file: string, text: string): Leak[] {
58
- const out: Leak[] = []
59
- const lines = text.split('\n')
60
- for (let i = 0; i < lines.length; i++) {
61
- const line = lines[i].replace(/\r$/, '')
62
- for (const { kind, re } of LEAK_PATTERNS) {
63
- re.lastIndex = 0
64
- let m = re.exec(line)
65
- while (m !== null) {
66
- out.push({ file, line: i + 1, kind, token: capToken(m[0]) })
67
- if (m.index === re.lastIndex) re.lastIndex++ // guard against a zero-width match
68
- m = re.exec(line)
69
- }
70
- }
71
- }
72
- return out
73
- }
74
-
75
- // ── Scan the plan directory ──
76
- // The handoff artifacts a mission commits: every `*.md` sitting directly under <root>/.agents/plans
77
- // — the `<cr-ref>.plan.md` brief plus any sibling design doc. A missing plans dir yields no leaks.
78
- function planFiles(root: string): string[] {
79
- const dir = join(root, '.agents', 'plans')
80
- if (!existsSync(dir)) return []
81
- let entries: import('node:fs').Dirent[]
82
- try {
83
- entries = readdirSync(dir, { withFileTypes: true })
84
- } catch {
85
- return []
86
- }
87
- return entries
88
- .filter((e) => e.isFile() && e.name.endsWith('.md'))
89
- .map((e) => e.name)
90
- .sort()
91
- }
92
-
93
- // Collect leaks across the plan directory (default) or a caller-named file set (`--path`). File
94
- // paths in the returned leaks are repo-relative when scanning the plan dir, and as-passed for
95
- // explicit `--path` targets. Sorted by file then line for stable output.
96
- export function collectLeaks(root: string, paths?: string[]): Leak[] {
97
- const targets: { rel: string; abs: string }[] = paths
98
- ? paths.map((p) => ({ rel: p, abs: p }))
99
- : planFiles(root).map((name) => ({
100
- rel: join('.agents', 'plans', name),
101
- abs: join(root, '.agents', 'plans', name),
102
- }))
103
- const out: Leak[] = []
104
- for (const { rel, abs } of targets) {
105
- let text: string
106
- try {
107
- text = readFileSync(abs, 'utf8')
108
- } catch {
109
- continue
110
- }
111
- out.push(...scanText(rel, text))
112
- }
113
- return out.sort((a, b) => (a.file !== b.file ? (a.file < b.file ? -1 : 1) : a.line - b.line))
114
- }
115
-
116
- // ── Output ──
117
- const COLUMNS = ['file', 'line', 'kind', 'token'] as const
118
-
119
- function toonField(v: string): string {
120
- if (v === '' || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, '""')}"`
121
- return v
122
- }
123
-
124
- export function toToon(leaks: Leak[]): string {
125
- const header = `leaks[${leaks.length}]{${COLUMNS.join(',')}}:`
126
- const rows = leaks.map((l) => ` ${COLUMNS.map((c) => toonField(String(l[c]))).join(',')}`)
127
- return [header, ...rows].join('\n')
128
- }
129
-
130
- export function main(argv: string[]): number {
131
- const root = argv.includes('--root') ? (argv[argv.indexOf('--root') + 1] ?? '.') : '.'
132
- const format = argv.includes('--format') ? argv[argv.indexOf('--format') + 1] : 'toon'
133
- const check = argv.includes('--check')
134
- // `--path <file>` may repeat to scan an explicit file set (e.g. the one brief a checkpoint
135
- // is about to commit) instead of the whole plan directory.
136
- const paths: string[] = []
137
- for (let i = 0; i < argv.length; i++) if (argv[i] === '--path' && argv[i + 1]) paths.push(argv[++i])
138
-
139
- const leaks = collectLeaks(root, paths.length ? paths : undefined)
140
- const out = format === 'json' ? JSON.stringify(leaks, null, 2) : toToon(leaks)
141
- process.stdout.write(`${out}\n`)
142
- // --check is the CI guard: any leak fails. Audit mode (default) always exits 0.
143
- return check && leaks.length > 0 ? 1 : 0
144
- }
145
-
146
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
147
- process.exit(main(process.argv.slice(2)))
148
- }
@@ -1,248 +0,0 @@
1
- #!/usr/bin/env node
2
- import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3
- import { dirname, join, relative, sep } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- //#region skills/check-plugin-manifests/scripts/check-plugin-manifests.mts
6
- /**
7
- * Directories that hold a manifest one level below the plugin root.
8
- *
9
- * This is a path-shape list, not a provenance one: `.plugin/` holds the **canonical** manifest and
10
- * the other two hold its generated vendor copies, but all three sit one level down, so a pointer
11
- * inside any of them resolves against the parent. Naming it for vendors would contradict the
12
- * spec's own vocabulary and has already misled one reader.
13
- */
14
- const MANIFEST_DIRS = [
15
- ".plugin",
16
- ".claude-plugin",
17
- ".codex-plugin"
18
- ];
19
- const MANIFEST_NAME = "plugin.json";
20
- const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
21
- /**
22
- * Every manifest at a conventional location under `root`, repo-relative and path-sorted.
23
- *
24
- * A manifest is a `plugin.json` either inside a vendor directory, or sitting beside a
25
- * `package.json` (a package-root manifest). Sorted so a sweep's report order is deterministic.
26
- * A symlink is included: it resolves to a real manifest, and a broken one must surface as
27
- * unreadable rather than vanish from the sweep.
28
- */
29
- /** Directory entries, or none where the directory cannot be read. */
30
- function safeReaddir(dir) {
31
- try {
32
- return readdirSync(dir, { withFileTypes: true });
33
- } catch {
34
- return [];
35
- }
36
- }
37
- function discoverManifests(root) {
38
- const found = [];
39
- const walk = (dir) => {
40
- for (const e of safeReaddir(dir)) {
41
- const full = join(dir, e.name);
42
- if (e.isDirectory()) {
43
- if (!SKIP_DIRS.has(e.name)) walk(full);
44
- continue;
45
- }
46
- if (e.name !== MANIFEST_NAME) continue;
47
- const parent = dirname(full);
48
- if (MANIFEST_DIRS.includes(parent.split(sep).pop() ?? "") || existsSync(join(parent, "package.json"))) found.push(full);
49
- }
50
- };
51
- walk(root);
52
- return found.map((f) => relative(root, f).split(sep).join("/")).sort();
53
- }
54
- /**
55
- * The plugin root a manifest's pointers resolve against — NOT the manifest's own directory.
56
- *
57
- * A vendor manifest sits one level down, and its `./skills` still names the plugin's skills
58
- * directory. A guard resolving relative to the file it just read reports every vendor manifest as
59
- * entirely broken.
60
- */
61
- function pluginRootOf(manifestRelPath) {
62
- const parts = manifestRelPath.split("/");
63
- const parentName = parts[parts.length - 2];
64
- const drop = parentName !== void 0 && MANIFEST_DIRS.includes(parentName) ? 2 : 1;
65
- return parts.slice(0, parts.length - drop).join("/");
66
- }
67
- /** Every `./`-prefixed string value in the manifest, paired with the key it sits under. */
68
- function extractPointers(manifest) {
69
- const out = [];
70
- const visit = (value, key) => {
71
- if (typeof value === "string") {
72
- if (value.startsWith("./")) out.push({
73
- key,
74
- pointer: value
75
- });
76
- return;
77
- }
78
- if (Array.isArray(value)) {
79
- for (const item of value) visit(item, key);
80
- return;
81
- }
82
- if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) visit(v, k);
83
- };
84
- visit(manifest, "");
85
- return out;
86
- }
87
- /**
88
- * The nearest `package.json` at or above the plugin root, and whether it publishes.
89
- *
90
- * A package publishes when it is not `private` AND declares a `files` allowlist. A `private`
91
- * package ships no tarball, so its `files` constrains nothing. A package declaring no `files` at
92
- * all ships everything, so there is no allowlist to be outside of — npm's own semantic, and
93
- * inverting it manufactures a finding against every pointer in the package.
94
- */
95
- function owningPackage(root, pluginRoot) {
96
- let dir = pluginRoot;
97
- for (;;) {
98
- const candidate = dir === "" ? "package.json" : `${dir}/package.json`;
99
- const abs = join(root, candidate);
100
- if (existsSync(abs)) try {
101
- const pkg = JSON.parse(readFileSync(abs, "utf8"));
102
- const files = Array.isArray(pkg.files) ? pkg.files.filter((f) => typeof f === "string") : null;
103
- const packs = pkg.private !== true;
104
- return {
105
- path: candidate,
106
- packs,
107
- publishes: packs && files !== null,
108
- files: files ?? []
109
- };
110
- } catch {
111
- return {
112
- path: candidate,
113
- packs: false,
114
- publishes: false,
115
- files: []
116
- };
117
- }
118
- if (dir === "") return null;
119
- dir = dir.split("/").slice(0, -1).join("/");
120
- }
121
- }
122
- /**
123
- * Whether a `files` allowlist covers a pointer, compared on the leading path segment.
124
- *
125
- * Both sides are normalized past a leading `./`, because an allowlist may legitimately spell an
126
- * entry either way and a pointer always carries the prefix — comparing them raw reports a shipped
127
- * component as unpublished.
128
- *
129
- * A `!`-prefixed entry is an exclusion and needs no special case: its leading `!` is part of its
130
- * first segment, so it can never equal a real segment and is never read as an inclusion.
131
- */
132
- function filesCovers(files, pointer) {
133
- const head = (path) => path.replace(/^\.\//, "").split("/")[0];
134
- const segment = head(pointer);
135
- if (segment === void 0 || segment === "") return true;
136
- return files.some((entry) => {
137
- const h = head(entry);
138
- return h === "*" || h === "**" || h === segment;
139
- });
140
- }
141
- /** Whether a package that packs a tarball ships the package-relative path `./<rel>`. */
142
- function ships(pkg, rel) {
143
- return pkg?.packs === true && (!pkg.publishes || filesCovers(pkg.files, `./${rel}`));
144
- }
145
- /** Every symbolic link at or below `abs` — the path itself included, links never followed. */
146
- function findSymlinks(abs) {
147
- let stat;
148
- try {
149
- stat = lstatSync(abs);
150
- } catch {
151
- return [];
152
- }
153
- if (stat.isSymbolicLink()) return [abs];
154
- if (!stat.isDirectory()) return [];
155
- return safeReaddir(abs).filter((e) => !SKIP_DIRS.has(e.name)).flatMap((e) => findSymlinks(join(abs, e.name)));
156
- }
157
- function sweep(root, manifests) {
158
- const findings = [];
159
- const linksSeen = /* @__PURE__ */ new Set();
160
- const reportLinks = (rel, abs, key, pointer) => {
161
- for (const link of findSymlinks(abs)) {
162
- const linkRel = relative(root, link).split(sep).join("/");
163
- if (linksSeen.has(linkRel)) continue;
164
- linksSeen.add(linkRel);
165
- findings.push({
166
- kind: "symlink",
167
- manifest: rel,
168
- ...key === void 0 ? {} : {
169
- key,
170
- pointer
171
- },
172
- detail: `${linkRel} is a symbolic link — the registry rejects a package that ships one`
173
- });
174
- }
175
- };
176
- for (const rel of manifests) {
177
- let parsed;
178
- try {
179
- parsed = JSON.parse(readFileSync(join(root, rel), "utf8"));
180
- } catch {
181
- findings.push({
182
- kind: "unreadable",
183
- manifest: rel,
184
- detail: "not readable as JSON"
185
- });
186
- continue;
187
- }
188
- const pluginRoot = pluginRootOf(rel);
189
- const pkg = owningPackage(root, pluginRoot);
190
- const pkgDir = pkg ? dirname(pkg.path) : "";
191
- const fromPkg = (path) => relative(join(root, pkgDir), join(root, path)).split(sep).join("/");
192
- if (ships(pkg, fromPkg(rel))) reportLinks(rel, join(root, rel));
193
- for (const { key, pointer } of extractPointers(parsed)) {
194
- const target = join(root, pluginRoot, pointer);
195
- if (!existsSync(target)) {
196
- findings.push({
197
- kind: "unresolved",
198
- manifest: rel,
199
- key,
200
- pointer,
201
- detail: "names no path on disk"
202
- });
203
- continue;
204
- }
205
- if (pkg?.publishes && !filesCovers(pkg.files, pointer)) {
206
- findings.push({
207
- kind: "unpublished",
208
- manifest: rel,
209
- key,
210
- pointer,
211
- detail: `declared but not published — ${pkg.path} files omits it`
212
- });
213
- continue;
214
- }
215
- if (ships(pkg, fromPkg(join(pluginRoot, pointer)))) reportLinks(rel, target, key, pointer);
216
- }
217
- }
218
- return findings;
219
- }
220
- function formatReport(manifests, findings) {
221
- if (manifests.length === 0) return "check-plugin-manifests: no plugin manifest found\n";
222
- if (findings.length === 0) return `check-plugin-manifests: ${manifests.length} manifest(s) OK\n`;
223
- return `${findings.map((f) => ` ${f.kind.padEnd(11)} ${f.manifest}${f.key ? ` [${f.key}] ${f.pointer}` : ""} — ${f.detail}`).join("\n")}\ncheck-plugin-manifests: ${findings.length} finding(s) across ${manifests.length} manifest(s)\n`;
224
- }
225
- function main(argv) {
226
- let root = ".";
227
- for (let i = 0; i < argv.length; i++) {
228
- const a = argv[i];
229
- if (a === "--root") {
230
- const next = argv[++i];
231
- if (next === void 0) {
232
- process.stderr.write("check-plugin-manifests: --root needs a directory\n");
233
- return 1;
234
- }
235
- root = next;
236
- continue;
237
- }
238
- process.stderr.write(`check-plugin-manifests: unrecognized flag ${a}\n`);
239
- return 1;
240
- }
241
- const manifests = discoverManifests(root);
242
- const findings = sweep(root, manifests);
243
- process.stdout.write(formatReport(manifests, findings));
244
- return findings.length === 0 ? 0 : 1;
245
- }
246
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
247
- //#endregion
248
- export { discoverManifests, extractPointers, filesCovers, findSymlinks, formatReport, main, owningPackage, pluginRootOf, sweep };