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.
- package/.claude-plugin/plugin.json +1 -2
- package/.codex-plugin/plugin.json +1 -2
- package/CHANGELOG.md +320 -0
- package/agents/sdd-scanner.md +1 -1
- package/package.json +11 -12
- package/plugin.json +55 -0
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.mjs +6755 -12
- package/skills/blast-estimate/README.md +3 -3
- package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
- package/skills/check-plan-safety/SKILL.md +2 -3
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
- package/skills/check-project-specs/SKILL.md +1 -1
- package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
- package/skills/check-retired-terms/SKILL.md +2 -3
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
- package/skills/check-scenario-overlap/SKILL.md +1 -2
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
- package/skills/check-spec-references/SKILL.md +2 -2
- package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
- package/skills/check-spec-structure/SKILL.md +1 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
- package/skills/collision-ladder/README.md +2 -2
- package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.mjs +183 -12
- package/skills/discover-plans/README.md +3 -3
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
- package/skills/discover-specs/README.md +3 -3
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
- package/skills/doctrine-loop/SKILL.md +1 -1
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +2 -2
- package/skills/init/scripts/wire-statusline.mjs +168 -12
- package/skills/manage-ignore/README.md +3 -3
- package/skills/manage-ignore/SKILL.md +1 -2
- package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
- package/skills/manage-scenario-bridge/README.md +3 -3
- package/skills/manage-scenario-bridge/SKILL.md +1 -1
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
- package/skills/manage-spec-anchors/README.md +3 -3
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
- package/skills/mission-graph/README.md +2 -2
- package/skills/mission-graph/SKILL.md +1 -1
- package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/SKILL.md +2 -2
- package/skills/place-node/scripts/place-node.mjs +117 -12
- package/skills/plan-retirement/README.md +2 -2
- package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
- package/skills/resolve-governances/README.md +3 -3
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
- package/skills/scaffold-project-spec/SKILL.md +1 -1
- package/skills/spec-gate/README.md +12 -9
- package/skills/spec-gate/SKILL.md +2 -2
- package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
- package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
- package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
- package/skills/spec-producer-governance/SKILL.md +2 -2
- package/skills/start-mission/SKILL.md +1 -1
- package/skills/suite-format-governance/README.md +1 -1
- package/skills/suite-format-governance/SKILL.md +1 -1
- package/skills/touch-set-correction/README.md +5 -5
- package/skills/touch-set-correction/SKILL.md +1 -1
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
- package/skills/verify-scenarios/README.md +6 -6
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
- package/.plugin/pins.json +0 -3
- package/.plugin/plugin.json +0 -19
- package/dist/align-spec/scripts/align-spec.js +0 -102
- package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
- package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
- package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
- package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
- package/dist/concept-index/scripts/concept-index.js +0 -181
- package/dist/discover-specs/scripts/discover-specs.js +0 -318
- package/dist/spec-gate/scripts/check-spec-state.js +0 -420
- package/dist/spec-gate/scripts/check-suite.js +0 -317
- package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
- package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
- package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
- package/skills/align-spec/scripts/align-spec.mts +0 -188
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
- package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
- package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
- package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
- package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
- package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
- package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
- package/skills/concept-index/scripts/concept-index.mts +0 -246
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
- package/skills/discover-plans/scripts/discover-plans.mts +0 -215
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
- package/skills/discover-specs/scripts/discover-specs.mts +0 -420
- package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
- package/skills/init/scripts/wire-statusline.mts +0 -279
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
- package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
- package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
- package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
- package/skills/place-node/scripts/place-node.mts +0 -158
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
- package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
- package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
- package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
- package/skills/spec-gate/scripts/check-suite.mts +0 -538
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
- package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
- package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
- package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// check-plugin-manifests — a plugin manifest must declare only components the package ships.
|
|
3
|
-
//
|
|
4
|
-
// A manifest pointer naming something the package does not carry is valid JSON, satisfies the
|
|
5
|
-
// schema, and is copied into every generated vendor manifest. It fails only on an installer's
|
|
6
|
-
// machine, after publish, as a component the host runtime cannot load. Nothing else in the repo
|
|
7
|
-
// compares a manifest's pointers against what ships, so this engine does.
|
|
8
|
-
//
|
|
9
|
-
// Two sub-checks, neither subsuming the other (spec: .agents/specs/sdd/plugin/check-plugin-manifests/):
|
|
10
|
-
// disk — does the pointer resolve to a path that exists?
|
|
11
|
-
// publish — for a package that publishes, is the pointer inside its `files` allowlist?
|
|
12
|
-
// pack — for a package that packs a tarball, does it ship a symbolic link? npm's registry
|
|
13
|
-
// rejects the whole package ("Symbolic link is not allowed"), so it never arrives.
|
|
14
|
-
// A directory can exist and be excluded from the tarball; a `files` entry can name a directory
|
|
15
|
-
// nobody created. The disk check short-circuits: a pointer dead on disk is reported once, as
|
|
16
|
-
// unresolved, and is not also asked about `files`.
|
|
17
|
-
//
|
|
18
|
-
// A pointer is any `./`-prefixed string value, at any depth, under any key — NOT a fixed key list.
|
|
19
|
-
// The manifest format grows new component keys, and a guard hardcoding today's set fails open on
|
|
20
|
-
// the next one added.
|
|
21
|
-
//
|
|
22
|
-
// Pure functions are exported for node:test; running the file directly drives the CLI. No
|
|
23
|
-
// dependencies (the repo's node-≥23.6 / no-deps convention).
|
|
24
|
-
|
|
25
|
-
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
26
|
-
import { dirname, join, relative, sep } from 'node:path'
|
|
27
|
-
import { pathToFileURL } from 'node:url'
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Directories that hold a manifest one level below the plugin root.
|
|
31
|
-
*
|
|
32
|
-
* This is a path-shape list, not a provenance one: `.plugin/` holds the **canonical** manifest and
|
|
33
|
-
* the other two hold its generated vendor copies, but all three sit one level down, so a pointer
|
|
34
|
-
* inside any of them resolves against the parent. Naming it for vendors would contradict the
|
|
35
|
-
* spec's own vocabulary and has already misled one reader.
|
|
36
|
-
*/
|
|
37
|
-
const MANIFEST_DIRS = ['.plugin', '.claude-plugin', '.codex-plugin']
|
|
38
|
-
const MANIFEST_NAME = 'plugin.json'
|
|
39
|
-
const SKIP_DIRS = new Set(['node_modules', '.git'])
|
|
40
|
-
|
|
41
|
-
export type FindingKind = 'unreadable' | 'unresolved' | 'unpublished' | 'symlink'
|
|
42
|
-
|
|
43
|
-
export interface Finding {
|
|
44
|
-
kind: FindingKind
|
|
45
|
-
/** Repo-relative path of the manifest carrying the defect. */
|
|
46
|
-
manifest: string
|
|
47
|
-
/** The manifest key the pointer sits under. Absent for an unreadable manifest. */
|
|
48
|
-
key?: string
|
|
49
|
-
/** The pointer value. Absent for an unreadable manifest. */
|
|
50
|
-
pointer?: string
|
|
51
|
-
detail: string
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── Discovery ──
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Every manifest at a conventional location under `root`, repo-relative and path-sorted.
|
|
58
|
-
*
|
|
59
|
-
* A manifest is a `plugin.json` either inside a vendor directory, or sitting beside a
|
|
60
|
-
* `package.json` (a package-root manifest). Sorted so a sweep's report order is deterministic.
|
|
61
|
-
* A symlink is included: it resolves to a real manifest, and a broken one must surface as
|
|
62
|
-
* unreadable rather than vanish from the sweep.
|
|
63
|
-
*/
|
|
64
|
-
/** Directory entries, or none where the directory cannot be read. */
|
|
65
|
-
function safeReaddir(dir: string) {
|
|
66
|
-
try {
|
|
67
|
-
return readdirSync(dir, { withFileTypes: true })
|
|
68
|
-
} catch {
|
|
69
|
-
return []
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function discoverManifests(root: string): string[] {
|
|
74
|
-
const found: string[] = []
|
|
75
|
-
const walk = (dir: string): void => {
|
|
76
|
-
for (const e of safeReaddir(dir)) {
|
|
77
|
-
const full = join(dir, e.name)
|
|
78
|
-
if (e.isDirectory()) {
|
|
79
|
-
if (!SKIP_DIRS.has(e.name)) walk(full)
|
|
80
|
-
continue
|
|
81
|
-
}
|
|
82
|
-
if (e.name !== MANIFEST_NAME) continue
|
|
83
|
-
const parent = dirname(full)
|
|
84
|
-
const inVendorDir = MANIFEST_DIRS.includes(parent.split(sep).pop() ?? '')
|
|
85
|
-
if (inVendorDir || existsSync(join(parent, 'package.json'))) found.push(full)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
walk(root)
|
|
89
|
-
return found.map((f) => relative(root, f).split(sep).join('/')).sort()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* The plugin root a manifest's pointers resolve against — NOT the manifest's own directory.
|
|
94
|
-
*
|
|
95
|
-
* A vendor manifest sits one level down, and its `./skills` still names the plugin's skills
|
|
96
|
-
* directory. A guard resolving relative to the file it just read reports every vendor manifest as
|
|
97
|
-
* entirely broken.
|
|
98
|
-
*/
|
|
99
|
-
export function pluginRootOf(manifestRelPath: string): string {
|
|
100
|
-
const parts = manifestRelPath.split('/')
|
|
101
|
-
const parentName = parts[parts.length - 2]
|
|
102
|
-
const drop = parentName !== undefined && MANIFEST_DIRS.includes(parentName) ? 2 : 1
|
|
103
|
-
return parts.slice(0, parts.length - drop).join('/')
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── Pointer extraction ──
|
|
107
|
-
|
|
108
|
-
/** Every `./`-prefixed string value in the manifest, paired with the key it sits under. */
|
|
109
|
-
export function extractPointers(manifest: unknown): Array<{ key: string; pointer: string }> {
|
|
110
|
-
const out: Array<{ key: string; pointer: string }> = []
|
|
111
|
-
const visit = (value: unknown, key: string): void => {
|
|
112
|
-
if (typeof value === 'string') {
|
|
113
|
-
if (value.startsWith('./')) out.push({ key, pointer: value })
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
if (Array.isArray(value)) {
|
|
117
|
-
for (const item of value) visit(item, key)
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
if (value !== null && typeof value === 'object') {
|
|
121
|
-
for (const [k, v] of Object.entries(value)) visit(v, k)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
visit(manifest, '')
|
|
125
|
-
return out
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ── The publish sub-check ──
|
|
129
|
-
|
|
130
|
-
export interface OwningPackage {
|
|
131
|
-
path: string
|
|
132
|
-
/** Not `private` — the package packs a tarball, whether or not it declares `files`. */
|
|
133
|
-
packs: boolean
|
|
134
|
-
publishes: boolean
|
|
135
|
-
files: string[]
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* The nearest `package.json` at or above the plugin root, and whether it publishes.
|
|
140
|
-
*
|
|
141
|
-
* A package publishes when it is not `private` AND declares a `files` allowlist. A `private`
|
|
142
|
-
* package ships no tarball, so its `files` constrains nothing. A package declaring no `files` at
|
|
143
|
-
* all ships everything, so there is no allowlist to be outside of — npm's own semantic, and
|
|
144
|
-
* inverting it manufactures a finding against every pointer in the package.
|
|
145
|
-
*/
|
|
146
|
-
export function owningPackage(root: string, pluginRoot: string): OwningPackage | null {
|
|
147
|
-
let dir = pluginRoot
|
|
148
|
-
for (;;) {
|
|
149
|
-
const candidate = dir === '' ? 'package.json' : `${dir}/package.json`
|
|
150
|
-
const abs = join(root, candidate)
|
|
151
|
-
if (existsSync(abs)) {
|
|
152
|
-
try {
|
|
153
|
-
const pkg = JSON.parse(readFileSync(abs, 'utf8')) as { private?: unknown; files?: unknown }
|
|
154
|
-
const files = Array.isArray(pkg.files) ? pkg.files.filter((f): f is string => typeof f === 'string') : null
|
|
155
|
-
const packs = pkg.private !== true
|
|
156
|
-
return { path: candidate, packs, publishes: packs && files !== null, files: files ?? [] }
|
|
157
|
-
} catch {
|
|
158
|
-
return { path: candidate, packs: false, publishes: false, files: [] }
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (dir === '') return null
|
|
162
|
-
const up = dir.split('/').slice(0, -1).join('/')
|
|
163
|
-
dir = up
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Whether a `files` allowlist covers a pointer, compared on the leading path segment.
|
|
169
|
-
*
|
|
170
|
-
* Both sides are normalized past a leading `./`, because an allowlist may legitimately spell an
|
|
171
|
-
* entry either way and a pointer always carries the prefix — comparing them raw reports a shipped
|
|
172
|
-
* component as unpublished.
|
|
173
|
-
*
|
|
174
|
-
* A `!`-prefixed entry is an exclusion and needs no special case: its leading `!` is part of its
|
|
175
|
-
* first segment, so it can never equal a real segment and is never read as an inclusion.
|
|
176
|
-
*/
|
|
177
|
-
export function filesCovers(files: string[], pointer: string): boolean {
|
|
178
|
-
const head = (path: string): string | undefined => path.replace(/^\.\//, '').split('/')[0]
|
|
179
|
-
const segment = head(pointer)
|
|
180
|
-
if (segment === undefined || segment === '') return true
|
|
181
|
-
return files.some((entry) => {
|
|
182
|
-
const h = head(entry)
|
|
183
|
-
// A wildcard head ships everything below it, so it covers any segment.
|
|
184
|
-
return h === '*' || h === '**' || h === segment
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── The pack sub-check ──
|
|
189
|
-
|
|
190
|
-
/** Whether a package that packs a tarball ships the package-relative path `./<rel>`. */
|
|
191
|
-
function ships(pkg: OwningPackage | null, rel: string): pkg is OwningPackage {
|
|
192
|
-
return pkg?.packs === true && (!pkg.publishes || filesCovers(pkg.files, `./${rel}`))
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Every symbolic link at or below `abs` — the path itself included, links never followed. */
|
|
196
|
-
export function findSymlinks(abs: string): string[] {
|
|
197
|
-
let stat: ReturnType<typeof lstatSync>
|
|
198
|
-
try {
|
|
199
|
-
stat = lstatSync(abs)
|
|
200
|
-
} catch {
|
|
201
|
-
return []
|
|
202
|
-
}
|
|
203
|
-
if (stat.isSymbolicLink()) return [abs]
|
|
204
|
-
if (!stat.isDirectory()) return []
|
|
205
|
-
return safeReaddir(abs)
|
|
206
|
-
.filter((e) => !SKIP_DIRS.has(e.name))
|
|
207
|
-
.flatMap((e) => findSymlinks(join(abs, e.name)))
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ── The sweep ──
|
|
211
|
-
|
|
212
|
-
export function sweep(root: string, manifests: string[]): Finding[] {
|
|
213
|
-
const findings: Finding[] = []
|
|
214
|
-
// One link reported once, however many manifests ship it.
|
|
215
|
-
const linksSeen = new Set<string>()
|
|
216
|
-
const reportLinks = (rel: string, abs: string, key?: string, pointer?: string): void => {
|
|
217
|
-
for (const link of findSymlinks(abs)) {
|
|
218
|
-
const linkRel = relative(root, link).split(sep).join('/')
|
|
219
|
-
if (linksSeen.has(linkRel)) continue
|
|
220
|
-
linksSeen.add(linkRel)
|
|
221
|
-
findings.push({
|
|
222
|
-
kind: 'symlink',
|
|
223
|
-
manifest: rel,
|
|
224
|
-
...(key === undefined ? {} : { key, pointer }),
|
|
225
|
-
detail: `${linkRel} is a symbolic link — the registry rejects a package that ships one`,
|
|
226
|
-
})
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
for (const rel of manifests) {
|
|
230
|
-
let parsed: unknown
|
|
231
|
-
try {
|
|
232
|
-
parsed = JSON.parse(readFileSync(join(root, rel), 'utf8'))
|
|
233
|
-
} catch {
|
|
234
|
-
findings.push({ kind: 'unreadable', manifest: rel, detail: 'not readable as JSON' })
|
|
235
|
-
continue
|
|
236
|
-
}
|
|
237
|
-
const pluginRoot = pluginRootOf(rel)
|
|
238
|
-
const pkg = owningPackage(root, pluginRoot)
|
|
239
|
-
const pkgDir = pkg ? dirname(pkg.path) : ''
|
|
240
|
-
const fromPkg = (path: string): string => relative(join(root, pkgDir), join(root, path)).split(sep).join('/')
|
|
241
|
-
if (ships(pkg, fromPkg(rel))) reportLinks(rel, join(root, rel))
|
|
242
|
-
for (const { key, pointer } of extractPointers(parsed)) {
|
|
243
|
-
const target = join(root, pluginRoot, pointer)
|
|
244
|
-
if (!existsSync(target)) {
|
|
245
|
-
findings.push({ kind: 'unresolved', manifest: rel, key, pointer, detail: 'names no path on disk' })
|
|
246
|
-
continue
|
|
247
|
-
}
|
|
248
|
-
if (pkg?.publishes && !filesCovers(pkg.files, pointer)) {
|
|
249
|
-
findings.push({
|
|
250
|
-
kind: 'unpublished',
|
|
251
|
-
manifest: rel,
|
|
252
|
-
key,
|
|
253
|
-
pointer,
|
|
254
|
-
detail: `declared but not published — ${pkg.path} files omits it`,
|
|
255
|
-
})
|
|
256
|
-
continue
|
|
257
|
-
}
|
|
258
|
-
if (ships(pkg, fromPkg(join(pluginRoot, pointer)))) reportLinks(rel, target, key, pointer)
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return findings
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ── Report ──
|
|
265
|
-
|
|
266
|
-
export function formatReport(manifests: string[], findings: Finding[]): string {
|
|
267
|
-
if (manifests.length === 0) return 'check-plugin-manifests: no plugin manifest found\n'
|
|
268
|
-
if (findings.length === 0) {
|
|
269
|
-
return `check-plugin-manifests: ${manifests.length} manifest(s) OK\n`
|
|
270
|
-
}
|
|
271
|
-
const rows = findings
|
|
272
|
-
.map((f) => ` ${f.kind.padEnd(11)} ${f.manifest}${f.key ? ` [${f.key}] ${f.pointer}` : ''} — ${f.detail}`)
|
|
273
|
-
.join('\n')
|
|
274
|
-
return `${rows}\ncheck-plugin-manifests: ${findings.length} finding(s) across ${manifests.length} manifest(s)\n`
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ── CLI ──
|
|
278
|
-
|
|
279
|
-
export function main(argv: string[]): number {
|
|
280
|
-
let root = '.'
|
|
281
|
-
for (let i = 0; i < argv.length; i++) {
|
|
282
|
-
const a = argv[i]
|
|
283
|
-
if (a === '--root') {
|
|
284
|
-
const next = argv[++i]
|
|
285
|
-
if (next === undefined) {
|
|
286
|
-
process.stderr.write('check-plugin-manifests: --root needs a directory\n')
|
|
287
|
-
return 1
|
|
288
|
-
}
|
|
289
|
-
root = next
|
|
290
|
-
continue
|
|
291
|
-
}
|
|
292
|
-
process.stderr.write(`check-plugin-manifests: unrecognized flag ${a}\n`)
|
|
293
|
-
return 1
|
|
294
|
-
}
|
|
295
|
-
const manifests = discoverManifests(root)
|
|
296
|
-
const findings = sweep(root, manifests)
|
|
297
|
-
process.stdout.write(formatReport(manifests, findings))
|
|
298
|
-
return findings.length === 0 ? 0 : 1
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
302
|
-
process.exit(main(process.argv.slice(2)))
|
|
303
|
-
}
|