@opengsd/gsd-core 1.7.0-rc.5 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-executor.md +2 -1
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +975 -196
- package/commands/gsd/mempalace-capture.md +27 -1
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +63 -2
- package/gsd-core/bin/lib/api-coverage.cjs +3 -4
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/capability-registry.cjs +503 -87
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/check-command-router.cjs +1 -1
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +48 -9
- package/gsd-core/bin/lib/config-loader.cjs +6 -2
- package/gsd-core/bin/lib/config.cjs +12 -0
- package/gsd-core/bin/lib/core-utils.cjs +8 -2
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/host-integration.cjs +33 -8
- package/gsd-core/bin/lib/init.cjs +60 -53
- package/gsd-core/bin/lib/install-engine.cjs +93 -22
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +217 -31
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +436 -61
- package/gsd-core/bin/lib/plan-scan.cjs +3 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
- package/gsd-core/bin/lib/roadmap.cjs +100 -49
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +7 -37
- package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
- package/gsd-core/bin/lib/smart-entry.cjs +4 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +100 -45
- package/gsd-core/bin/lib/state.cjs +391 -126
- package/gsd-core/bin/lib/surface.cjs +12 -8
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +54 -8
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/verify.cjs +4 -3
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -3
- package/gsd-core/references/checkpoints.md +12 -0
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/debug.md +2 -0
- package/gsd-core/workflows/execute-phase.md +5 -3
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/plan-phase.md +6 -0
- package/gsd-core/workflows/progress.md +2 -2
- package/gsd-core/workflows/quick.md +2 -0
- package/gsd-core/workflows/review.md +42 -3
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -4
- package/gsd-core/workflows/ship.md +8 -2
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/hooks/dist/gsd-statusline.js +164 -14
- package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-statusline.js +164 -14
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +10 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +3 -0
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/validate-registry.cjs +117 -0
- package/skills/gsd-mempalace-capture/SKILL.md +27 -1
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- package/vscode/package.json +96 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/registry-schema.cjs — pure schema/vocab constants + validation +
|
|
5
|
+
* markdown-generation logic for the two third-party discoverability catalogs
|
|
6
|
+
* (issue #2182):
|
|
7
|
+
*
|
|
8
|
+
* - `docs/registries/capabilities.json` → "GSD Community Capability Registry"
|
|
9
|
+
* - `docs/registries/eos.json` → "GSD EoS Registry" (PR2)
|
|
10
|
+
*
|
|
11
|
+
* The vocabulary constants below are ADDITIVE CONTRACTS that track the
|
|
12
|
+
* runtime/ADR closed vocabularies they describe — they are a documentation-
|
|
13
|
+
* registry-scoped mirror, not the runtime source of truth:
|
|
14
|
+
*
|
|
15
|
+
* - `LOOP_POINTS` mirrors ADR-857 "Loop Extension Points (the 12)"
|
|
16
|
+
* (docs/adr/857-capability-system.md §"Loop Extension Points (the 12)").
|
|
17
|
+
* The canonical runtime set lives in `src/loop-resolver.cts`
|
|
18
|
+
* (`CANONICAL_POINTS` / `CANONICAL_POINTS_FALLBACK`, derived from
|
|
19
|
+
* `loop-host-contract.cjs`) — changing that set requires updating this
|
|
20
|
+
* list too, since a registry entry's `loopExtensionPoints` describes
|
|
21
|
+
* which of those 12 points a third-party capability extends.
|
|
22
|
+
* - `HOOK_KINDS` mirrors ADR-857 Decision 4 "three hook kinds": `step`
|
|
23
|
+
* (runs as its own sequenced unit), `contribution` (injects into the
|
|
24
|
+
* core step's prompt/context), `gate` (checks and optionally blocks).
|
|
25
|
+
* - `INTERFACE_POINTS` mirrors ADR-1239 "The six interface points" (the
|
|
26
|
+
* Host-Integration Interface integration surface): command/workflow
|
|
27
|
+
* invocation, agent dispatch, model invocation, lifecycle hooks,
|
|
28
|
+
* state+config IO, artifact surface.
|
|
29
|
+
* - `PROFILES` mirrors ADR-1239 "Host-capability profiles (negotiation
|
|
30
|
+
* baselines)": `programmatic-cli`, `declarative-cli`, `ide`.
|
|
31
|
+
* - `AXES` mirrors ADR-1239 "the eight negotiated axes" (the negotiated
|
|
32
|
+
* capability schema exchanged at `initialize`): `embeddingMode`,
|
|
33
|
+
* `commandSurface`, `dispatch`, `modelMode`, `hookBus`, `stateIO`,
|
|
34
|
+
* `transport`, `runtime`. Seven of the eight are closed enums here;
|
|
35
|
+
* `dispatch` is ADR-1239's structured negotiated object
|
|
36
|
+
* (`{ namedDispatch, nested, maxDepth, background, subagentToolkit }`) —
|
|
37
|
+
* this registry accepts a free-form human summary string instead, so it
|
|
38
|
+
* carries the `AXES_FREE_STRING` sentinel rather than an enum array.
|
|
39
|
+
* - `CAPABILITY_REQUIRED` / `EOS_REQUIRED` mirror the required top-level
|
|
40
|
+
* fields for each entry type, including `enginesGsd` (ADR-1244 D1
|
|
41
|
+
* "Versioned capability manifest" — the `engines.gsd` semver-range gate,
|
|
42
|
+
* modelled on VS Code's `engines.vscode`).
|
|
43
|
+
*
|
|
44
|
+
* This module is pure — no `fs`/`process`/child-process access — so tests
|
|
45
|
+
* can `require()` it directly and assert on structured return values.
|
|
46
|
+
* `scripts/validate-registry.cjs` and `scripts/gen-registry.cjs` are the thin
|
|
47
|
+
* CLI wrappers that perform I/O around these functions.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
// ─── ADR-857 "Loop Extension Points (the 12)" ────────────────────────────────
|
|
51
|
+
const LOOP_POINTS = Object.freeze([
|
|
52
|
+
'discuss:pre',
|
|
53
|
+
'discuss:post',
|
|
54
|
+
'plan:pre',
|
|
55
|
+
'plan:post',
|
|
56
|
+
'execute:pre',
|
|
57
|
+
'execute:wave:pre',
|
|
58
|
+
'execute:wave:post',
|
|
59
|
+
'execute:post',
|
|
60
|
+
'verify:pre',
|
|
61
|
+
'verify:post',
|
|
62
|
+
'ship:pre',
|
|
63
|
+
'ship:post',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// ─── ADR-857 Decision 4 — three hook kinds ───────────────────────────────────
|
|
67
|
+
const HOOK_KINDS = Object.freeze(['step', 'contribution', 'gate']);
|
|
68
|
+
|
|
69
|
+
// ─── ADR-1239 "The six interface points" ─────────────────────────────────────
|
|
70
|
+
const INTERFACE_POINTS = Object.freeze(['command', 'dispatch', 'model', 'hooks', 'state', 'artifact']);
|
|
71
|
+
|
|
72
|
+
// ─── ADR-1239 "Host-capability profiles (negotiation baselines)" ────────────
|
|
73
|
+
const PROFILES = Object.freeze(['programmatic-cli', 'declarative-cli', 'ide']);
|
|
74
|
+
|
|
75
|
+
// Sentinel marking an AXES entry as a free-form descriptive string rather than
|
|
76
|
+
// a closed enum array. `Array.isArray(AXES_FREE_STRING)` is false, so callers
|
|
77
|
+
// can branch on `Array.isArray(AXES[key])` vs `AXES[key] === AXES_FREE_STRING`
|
|
78
|
+
// without risking confusion with a real enum value.
|
|
79
|
+
const AXES_FREE_STRING = Symbol('registry-schema.AXES_FREE_STRING');
|
|
80
|
+
|
|
81
|
+
// ─── ADR-1239 "the eight negotiated axes" ────────────────────────────────────
|
|
82
|
+
const AXES = Object.freeze({
|
|
83
|
+
embeddingMode: Object.freeze(['imperative', 'declarative']),
|
|
84
|
+
commandSurface: Object.freeze(['slash-file', 'slash-programmatic', 'slash-toml', 'palette', 'prose-only']),
|
|
85
|
+
dispatch: AXES_FREE_STRING,
|
|
86
|
+
modelMode: Object.freeze(['active', 'passive']),
|
|
87
|
+
hookBus: Object.freeze(['host', 'engine', 'none']),
|
|
88
|
+
stateIO: Object.freeze(['filesystem', 'sandboxed-storage', 'session-log-append']),
|
|
89
|
+
transport: Object.freeze(['mcp', 'native-extension']),
|
|
90
|
+
runtime: Object.freeze(['node', 'bun', 'sandboxed-web', 'python', 'go', 'rust', 'electron', 'other']),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ─── Required top-level fields ───────────────────────────────────────────────
|
|
94
|
+
const CAPABILITY_REQUIRED = Object.freeze([
|
|
95
|
+
'id',
|
|
96
|
+
'name',
|
|
97
|
+
'type',
|
|
98
|
+
'repo',
|
|
99
|
+
'description',
|
|
100
|
+
'author',
|
|
101
|
+
'license',
|
|
102
|
+
'enginesGsd',
|
|
103
|
+
'install',
|
|
104
|
+
'uninstall',
|
|
105
|
+
'interactions',
|
|
106
|
+
'discussion',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const EOS_REQUIRED = Object.freeze([
|
|
110
|
+
'id',
|
|
111
|
+
'name',
|
|
112
|
+
'type',
|
|
113
|
+
'repo',
|
|
114
|
+
'description',
|
|
115
|
+
'author',
|
|
116
|
+
'license',
|
|
117
|
+
'enginesGsd',
|
|
118
|
+
'install',
|
|
119
|
+
'uninstall',
|
|
120
|
+
'interactions',
|
|
121
|
+
'discussion',
|
|
122
|
+
'protocolVersion',
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
// Escape Markdown inline metacharacters in UNTRUSTED free text so a registry
|
|
126
|
+
// entry cannot inject links/tables/code-spans into the generated catalog.
|
|
127
|
+
// Neutralizes: link hijack ([ ] ( )), table breakout (|), code span (`),
|
|
128
|
+
// and backslash. Newlines are collapsed to a single space (inline contexts).
|
|
129
|
+
function mdInline(value) {
|
|
130
|
+
return String(value).replace(/[\\`*_[\]()|~<>]/g, '\\$&').replace(/[\r\n]+/g, ' ');
|
|
131
|
+
}
|
|
132
|
+
// A fenced-code fence guaranteed longer than any backtick run in `value`, so a
|
|
133
|
+
// value containing ``` cannot escape the block (CommonMark rule). Min length 3.
|
|
134
|
+
function fenceFor(value) {
|
|
135
|
+
const runs = String(value).match(/`+/g) || [];
|
|
136
|
+
const longest = runs.reduce((m, r) => Math.max(m, r.length), 0);
|
|
137
|
+
return '`'.repeat(Math.max(3, longest + 1));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// A single `engines.gsd` range clause: optional comparison operator, optional
|
|
141
|
+
// leading `v`, exactly three dot-separated numeric segments, optional
|
|
142
|
+
// prerelease (`-...`) and build (`+...`) suffixes. Operator alternation order
|
|
143
|
+
// matters — `>=`/`<=` must be tried before `>`/`<` or the longer operator
|
|
144
|
+
// would never match.
|
|
145
|
+
const GSD_RANGE_CLAUSE_RE = /^(>=|<=|>|<|=|\^|~)?v?\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validate the SHAPE of an `engines.gsd`-style semver range string (ADR-1244
|
|
149
|
+
* D1). Self-contained — no `semver` dependency, modelled on the constraint
|
|
150
|
+
* parsing in `scripts/check-env.cjs` (`satisfiesConstraint`), but this
|
|
151
|
+
* function validates that the range is well-formed rather than comparing it
|
|
152
|
+
* against a concrete version.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} range
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
function isValidGsdRange(range) {
|
|
158
|
+
if (typeof range !== 'string') return false;
|
|
159
|
+
const trimmed = range.trim();
|
|
160
|
+
if (trimmed === '') return false;
|
|
161
|
+
if (trimmed === '*') return true;
|
|
162
|
+
const clauses = trimmed.split(/\s+/);
|
|
163
|
+
return clauses.length > 0 && clauses.every((clause) => clause !== '' && GSD_RANGE_CLAUSE_RE.test(clause));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validate the `interactions` sub-object for a capability entry.
|
|
168
|
+
*
|
|
169
|
+
* @param {object} interactions
|
|
170
|
+
* @param {(field: string, reason: string) => void} addError
|
|
171
|
+
* @returns {void}
|
|
172
|
+
*/
|
|
173
|
+
function validateCapabilityInteractions(interactions, addError) {
|
|
174
|
+
const allowedKeys = new Set([
|
|
175
|
+
'loopExtensionPoints',
|
|
176
|
+
'hookKinds',
|
|
177
|
+
'configKeys',
|
|
178
|
+
'requires',
|
|
179
|
+
'runtimeCompat',
|
|
180
|
+
'produces',
|
|
181
|
+
'consumes',
|
|
182
|
+
]);
|
|
183
|
+
for (const key of Object.keys(interactions)) {
|
|
184
|
+
if (!allowedKeys.has(key)) addError(`interactions.${key}`, 'unknown field');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const field of ['loopExtensionPoints', 'hookKinds']) {
|
|
188
|
+
if (interactions[field] === undefined) addError(`interactions.${field}`, 'missing required field');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (interactions.loopExtensionPoints !== undefined) {
|
|
192
|
+
const v = interactions.loopExtensionPoints;
|
|
193
|
+
if (!Array.isArray(v) || v.length === 0 || !v.every((x) => LOOP_POINTS.includes(x))) {
|
|
194
|
+
addError('interactions.loopExtensionPoints', 'must be a non-empty array of valid loop extension points');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (interactions.hookKinds !== undefined) {
|
|
199
|
+
const v = interactions.hookKinds;
|
|
200
|
+
if (!Array.isArray(v) || !v.every((x) => HOOK_KINDS.includes(x))) {
|
|
201
|
+
addError('interactions.hookKinds', 'must be an array of valid hook kinds');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const field of ['configKeys', 'requires', 'runtimeCompat', 'produces', 'consumes']) {
|
|
206
|
+
if (interactions[field] === undefined) continue;
|
|
207
|
+
const v = interactions[field];
|
|
208
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === 'string')) {
|
|
209
|
+
addError(`interactions.${field}`, 'must be an array of strings');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Validate the `interactions` sub-object for an eos entry.
|
|
216
|
+
*
|
|
217
|
+
* @param {object} interactions
|
|
218
|
+
* @param {(field: string, reason: string) => void} addError
|
|
219
|
+
* @returns {void}
|
|
220
|
+
*/
|
|
221
|
+
function validateEosInteractions(interactions, addError) {
|
|
222
|
+
const allowedKeys = new Set(['interfacePoints', 'profile', 'axes']);
|
|
223
|
+
for (const key of Object.keys(interactions)) {
|
|
224
|
+
if (!allowedKeys.has(key)) addError(`interactions.${key}`, 'unknown field');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const field of ['interfacePoints', 'profile', 'axes']) {
|
|
228
|
+
if (interactions[field] === undefined) addError(`interactions.${field}`, 'missing required field');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (interactions.interfacePoints !== undefined) {
|
|
232
|
+
const v = interactions.interfacePoints;
|
|
233
|
+
if (!Array.isArray(v) || v.length === 0 || !v.every((x) => INTERFACE_POINTS.includes(x))) {
|
|
234
|
+
addError('interactions.interfacePoints', 'must be a non-empty array of valid interface points');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (interactions.profile !== undefined) {
|
|
239
|
+
if (typeof interactions.profile !== 'string' || !PROFILES.includes(interactions.profile)) {
|
|
240
|
+
addError('interactions.profile', 'must be one of the valid negotiation profiles');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (interactions.axes !== undefined) {
|
|
245
|
+
const axes = interactions.axes;
|
|
246
|
+
if (typeof axes !== 'object' || axes === null || Array.isArray(axes)) {
|
|
247
|
+
addError('interactions.axes', 'axes must be an object');
|
|
248
|
+
} else {
|
|
249
|
+
const expectedKeys = Object.keys(AXES);
|
|
250
|
+
const actualKeys = Object.keys(axes);
|
|
251
|
+
const actualKeySet = new Set(actualKeys);
|
|
252
|
+
const keysMatch = expectedKeys.length === actualKeys.length && expectedKeys.every((k) => actualKeySet.has(k));
|
|
253
|
+
if (!keysMatch) {
|
|
254
|
+
addError('interactions.axes', 'axes key set must exactly match the eight negotiated axes');
|
|
255
|
+
} else {
|
|
256
|
+
for (const key of expectedKeys) {
|
|
257
|
+
const allowedValues = AXES[key];
|
|
258
|
+
const v = axes[key];
|
|
259
|
+
if (allowedValues === AXES_FREE_STRING) {
|
|
260
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
261
|
+
addError(`interactions.axes.${key}`, 'must be a non-empty string');
|
|
262
|
+
} else if (v.length > 300) {
|
|
263
|
+
addError(`interactions.axes.${key}`, 'exceeds max length 300');
|
|
264
|
+
}
|
|
265
|
+
} else if (typeof v !== 'string' || !allowedValues.includes(v)) {
|
|
266
|
+
addError(`interactions.axes.${key}`, `must be one of the allowed values for ${key}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Validate an array of registry entries against the closed schema for
|
|
276
|
+
* `opts.type` ('capability' | 'eos').
|
|
277
|
+
*
|
|
278
|
+
* @param {object[]} entries
|
|
279
|
+
* @param {{type: 'capability'|'eos'}} opts
|
|
280
|
+
* @returns {{ok: boolean, errors: Array<{index: number, id?: string, field: string, reason: string}>}}
|
|
281
|
+
*/
|
|
282
|
+
function validateEntries(entries, opts) {
|
|
283
|
+
if (!Array.isArray(entries)) {
|
|
284
|
+
return { ok: false, errors: [{ index: -1, field: '(root)', reason: 'entries must be an array' }] };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Entry-count cap: a pathologically large array (e.g. from an automated or
|
|
288
|
+
// malicious PR) is rejected wholesale rather than validated entry-by-entry.
|
|
289
|
+
if (entries.length > 2000) {
|
|
290
|
+
return { ok: false, errors: [{ index: -1, field: '(root)', reason: 'too many entries (max 2000)' }] };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const required = opts.type === 'eos' ? EOS_REQUIRED : CAPABILITY_REQUIRED;
|
|
294
|
+
const requiredSet = new Set(required);
|
|
295
|
+
const seenIds = new Set();
|
|
296
|
+
const errors = [];
|
|
297
|
+
|
|
298
|
+
entries.forEach((entry, index) => {
|
|
299
|
+
const addError = (field, reason) => {
|
|
300
|
+
const err = { index, field, reason };
|
|
301
|
+
if (entry && typeof entry === 'object' && typeof entry.id === 'string') err.id = entry.id;
|
|
302
|
+
errors.push(err);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Null/non-object element guard — a malformed array element (null,
|
|
306
|
+
// undefined-via-hole, a primitive, or an array) cannot be destructured by
|
|
307
|
+
// the field checks below, so reject it outright rather than throwing.
|
|
308
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
309
|
+
addError('(entry)', 'entry must be a JSON object');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const key of Object.keys(entry)) {
|
|
314
|
+
if (!requiredSet.has(key)) addError(key, 'unknown field');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const missing = new Set();
|
|
318
|
+
for (const field of required) {
|
|
319
|
+
if (entry[field] === undefined) {
|
|
320
|
+
addError(field, 'missing required field');
|
|
321
|
+
missing.add(field);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Control-character rejection (defense in depth): `allowTabNewline` widens
|
|
326
|
+
// the reject-set exception for the two shell-snippet fields (install/
|
|
327
|
+
// uninstall), which legitimately contain tabs/newlines; every other free
|
|
328
|
+
// text field disallows ALL C0 control characters plus DEL (incl. \n/\t).
|
|
329
|
+
// Checked via char codes (not a literal control-char regex range) — same
|
|
330
|
+
// approach as capability-validator.cjs's hooks[].matcher check, which
|
|
331
|
+
// avoids tripping ESLint's no-control-regex rule.
|
|
332
|
+
const hasDisallowedControlChar = (v, allowTabNewline) => {
|
|
333
|
+
for (let c = 0; c < v.length; c += 1) {
|
|
334
|
+
const code = v.charCodeAt(c);
|
|
335
|
+
if (allowTabNewline && (code === 0x09 || code === 0x0a)) continue;
|
|
336
|
+
if (code < 0x20 || code === 0x7f) return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
};
|
|
340
|
+
const checkNoControlChars = (field, allowTabNewline) => {
|
|
341
|
+
if (missing.has(field)) return;
|
|
342
|
+
const v = entry[field];
|
|
343
|
+
if (typeof v !== 'string') return;
|
|
344
|
+
if (hasDisallowedControlChar(v, allowTabNewline)) addError(field, 'must not contain control characters');
|
|
345
|
+
};
|
|
346
|
+
// Length cap: reject oversized fields (untrusted third-party input feeding
|
|
347
|
+
// a committed Markdown catalog should not be allowed to blow up the doc).
|
|
348
|
+
const checkMaxLength = (field, max) => {
|
|
349
|
+
if (missing.has(field)) return;
|
|
350
|
+
const v = entry[field];
|
|
351
|
+
if (typeof v === 'string' && v.length > max) addError(field, `exceeds max length ${max}`);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if (!missing.has('id')) {
|
|
355
|
+
const id = entry.id;
|
|
356
|
+
if (typeof id !== 'string' || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(id)) {
|
|
357
|
+
addError('id', 'id must be kebab-case');
|
|
358
|
+
}
|
|
359
|
+
if (seenIds.has(id)) {
|
|
360
|
+
addError('id', `duplicate id: ${id}`);
|
|
361
|
+
} else {
|
|
362
|
+
seenIds.add(id);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
checkMaxLength('id', 100);
|
|
366
|
+
|
|
367
|
+
for (const field of ['name', 'description', 'author']) {
|
|
368
|
+
if (missing.has(field)) continue;
|
|
369
|
+
const v = entry[field];
|
|
370
|
+
if (typeof v !== 'string' || v.trim() === '') addError(field, 'must be a non-empty string');
|
|
371
|
+
checkNoControlChars(field, false);
|
|
372
|
+
}
|
|
373
|
+
checkMaxLength('name', 120);
|
|
374
|
+
checkMaxLength('author', 120);
|
|
375
|
+
checkMaxLength('description', 1000);
|
|
376
|
+
|
|
377
|
+
if (!missing.has('type') && entry.type !== opts.type) {
|
|
378
|
+
addError('type', `type must be "${opts.type}"`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!missing.has('repo')) {
|
|
382
|
+
if (typeof entry.repo !== 'string' || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(entry.repo)) {
|
|
383
|
+
addError('repo', 'repo must be in "owner/repo" form');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
checkMaxLength('repo', 100);
|
|
387
|
+
|
|
388
|
+
if (!missing.has('license')) {
|
|
389
|
+
const v = entry.license;
|
|
390
|
+
if (typeof v !== 'string' || v.trim() === '' || !/^[A-Za-z0-9.+()\- ]+$/.test(v)) {
|
|
391
|
+
addError('license', 'license must be a non-empty SPDX-like string');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
checkMaxLength('license', 120);
|
|
395
|
+
|
|
396
|
+
if (!missing.has('enginesGsd') && !isValidGsdRange(entry.enginesGsd)) {
|
|
397
|
+
addError('enginesGsd', 'enginesGsd must be a valid semver range');
|
|
398
|
+
}
|
|
399
|
+
checkMaxLength('enginesGsd', 100);
|
|
400
|
+
|
|
401
|
+
for (const field of ['install', 'uninstall']) {
|
|
402
|
+
if (missing.has(field)) continue;
|
|
403
|
+
const v = entry[field];
|
|
404
|
+
if (typeof v !== 'string' || v.trim() === '') addError(field, 'must be a non-empty string');
|
|
405
|
+
checkNoControlChars(field, true);
|
|
406
|
+
}
|
|
407
|
+
checkMaxLength('install', 2000);
|
|
408
|
+
checkMaxLength('uninstall', 2000);
|
|
409
|
+
|
|
410
|
+
if (!missing.has('discussion')) {
|
|
411
|
+
const v = entry.discussion;
|
|
412
|
+
if (typeof v !== 'string' || !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/discussions\/\d+$/.test(v)) {
|
|
413
|
+
addError('discussion', 'discussion must be a GitHub discussions URL');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
checkMaxLength('discussion', 300);
|
|
417
|
+
|
|
418
|
+
if (!missing.has('interactions')) {
|
|
419
|
+
const interactions = entry.interactions;
|
|
420
|
+
if (typeof interactions !== 'object' || interactions === null || Array.isArray(interactions)) {
|
|
421
|
+
addError('interactions', 'interactions must be an object');
|
|
422
|
+
} else if (opts.type === 'eos') {
|
|
423
|
+
validateEosInteractions(interactions, addError);
|
|
424
|
+
} else {
|
|
425
|
+
validateCapabilityInteractions(interactions, addError);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (opts.type === 'eos' && !missing.has('protocolVersion')) {
|
|
430
|
+
if (!Number.isInteger(entry.protocolVersion) || entry.protocolVersion < 1) {
|
|
431
|
+
addError('protocolVersion', 'protocolVersion must be an integer >= 1');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
return { ok: errors.length === 0, errors };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Render the deterministic Markdown document for a registry.
|
|
441
|
+
*
|
|
442
|
+
* @param {object[]} entries
|
|
443
|
+
* @param {{type: 'capability'|'eos', sourceFile?: string}} opts
|
|
444
|
+
* @returns {string}
|
|
445
|
+
*/
|
|
446
|
+
function renderMarkdown(entries, opts) {
|
|
447
|
+
const sorted = [...entries].sort((a, b) => {
|
|
448
|
+
if (a.id < b.id) return -1;
|
|
449
|
+
if (a.id > b.id) return 1;
|
|
450
|
+
return 0;
|
|
451
|
+
});
|
|
452
|
+
const isEos = opts.type === 'eos';
|
|
453
|
+
const lines = [];
|
|
454
|
+
|
|
455
|
+
lines.push(
|
|
456
|
+
`<!-- GENERATED by scripts/gen-registry.cjs from docs/registries/${opts.sourceFile} — do not edit by hand; run \`npm run gen:registry\` -->`,
|
|
457
|
+
);
|
|
458
|
+
lines.push('');
|
|
459
|
+
lines.push(isEos ? '# GSD EoS Registry' : '# GSD Community Capability Registry');
|
|
460
|
+
lines.push('');
|
|
461
|
+
lines.push(
|
|
462
|
+
"> **Not an endorsement.** Inclusion means only that a maintainer merged a PR linking the author's repository — GSD has not reviewed, tested, or verified any listing. See the [registry README](./README.md).",
|
|
463
|
+
);
|
|
464
|
+
lines.push('');
|
|
465
|
+
lines.push(`_To add your ${isEos ? 'integration' : 'capability'}, see the [registry README](./README.md)._`);
|
|
466
|
+
lines.push('');
|
|
467
|
+
|
|
468
|
+
if (sorted.length === 0) {
|
|
469
|
+
lines.push('_No entries yet — be the first: see [README](./README.md)._');
|
|
470
|
+
return `${lines.join('\n')}\n`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
lines.push('| Name | What it is | Latest release | GSD compat | Discussion |');
|
|
474
|
+
lines.push('|---|---|---|---|---|');
|
|
475
|
+
for (const entry of sorted) {
|
|
476
|
+
// entry.repo/enginesGsd/discussion are regex-constrained (validateEntries)
|
|
477
|
+
// and used as link DESTINATIONS / badge URLs here — never mdInline those,
|
|
478
|
+
// it would corrupt the URL. entry.name/description are untrusted free-text
|
|
479
|
+
// link TEXT / body copy and MUST be escaped.
|
|
480
|
+
lines.push(
|
|
481
|
+
`| [${mdInline(entry.name)}](https://github.com/${entry.repo}) | ${mdInline(entry.description)} | ` +
|
|
482
|
+
` | ` +
|
|
483
|
+
`\`${entry.enginesGsd}\` | [discuss](${entry.discussion}) |`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
lines.push('');
|
|
487
|
+
|
|
488
|
+
sorted.forEach((entry, i) => {
|
|
489
|
+
const interactions = entry.interactions || {};
|
|
490
|
+
|
|
491
|
+
lines.push(`## ${mdInline(entry.name)}`);
|
|
492
|
+
lines.push(
|
|
493
|
+
`- **Repository:** https://github.com/${entry.repo} — [latest release](https://github.com/${entry.repo}/releases/latest)`,
|
|
494
|
+
);
|
|
495
|
+
lines.push(`- **What it is:** ${mdInline(entry.description)}`);
|
|
496
|
+
lines.push(`- **Author:** ${mdInline(entry.author)}`);
|
|
497
|
+
|
|
498
|
+
if (isEos) {
|
|
499
|
+
const axesSummary = Object.keys(AXES)
|
|
500
|
+
.map((key) => `${key}=${interactions.axes ? interactions.axes[key] : undefined}`)
|
|
501
|
+
.join(', ');
|
|
502
|
+
const summary =
|
|
503
|
+
`Interface points: ${(interactions.interfacePoints || []).join(', ')}; ` +
|
|
504
|
+
`profile: ${interactions.profile}; protocol v${entry.protocolVersion}; axes: ${axesSummary}`;
|
|
505
|
+
// Single mdInline pass over the fully-assembled summary: none of the
|
|
506
|
+
// literal separator text above contains Markdown metacharacters, so
|
|
507
|
+
// this equally neutralizes every embedded free-text/vocab value
|
|
508
|
+
// (notably interactions.axes.dispatch, a free-form untrusted string).
|
|
509
|
+
lines.push(`- **Every interaction with GSD:** ${mdInline(summary)}`);
|
|
510
|
+
} else {
|
|
511
|
+
let summary =
|
|
512
|
+
`Loop Extension Points: ${(interactions.loopExtensionPoints || []).join(', ')}; ` +
|
|
513
|
+
`hook kinds: ${(interactions.hookKinds || []).join(', ')}`;
|
|
514
|
+
for (const field of ['configKeys', 'requires', 'runtimeCompat', 'produces', 'consumes']) {
|
|
515
|
+
const v = interactions[field];
|
|
516
|
+
if (Array.isArray(v) && v.length > 0) summary += `; ${field}: ${v.join(', ')}`;
|
|
517
|
+
}
|
|
518
|
+
// configKeys/requires/runtimeCompat/produces/consumes are untrusted
|
|
519
|
+
// free-form strings (schema only requires "array of strings") — same
|
|
520
|
+
// single-pass mdInline rationale as the eos branch above.
|
|
521
|
+
lines.push(`- **Every interaction with GSD:** ${mdInline(summary)}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Code-span content (install/uninstall) is NOT mdInline-escaped — it is a
|
|
525
|
+
// verbatim shell snippet, not inline prose. Instead each block picks a
|
|
526
|
+
// fence strictly longer than any backtick run inside its own content, so
|
|
527
|
+
// an embedded ``` cannot prematurely close the fence (CommonMark rule).
|
|
528
|
+
const installFence = fenceFor(entry.install);
|
|
529
|
+
lines.push('- **Install:**');
|
|
530
|
+
lines.push(`${installFence}sh`);
|
|
531
|
+
lines.push(entry.install);
|
|
532
|
+
lines.push(installFence);
|
|
533
|
+
const uninstallFence = fenceFor(entry.uninstall);
|
|
534
|
+
lines.push('- **Uninstall:**');
|
|
535
|
+
lines.push(`${uninstallFence}sh`);
|
|
536
|
+
lines.push(entry.uninstall);
|
|
537
|
+
lines.push(uninstallFence);
|
|
538
|
+
|
|
539
|
+
lines.push(
|
|
540
|
+
isEos
|
|
541
|
+
? `- **GSD compatibility:** \`${entry.enginesGsd}\`, protocol v${entry.protocolVersion}`
|
|
542
|
+
: `- **GSD compatibility:** \`${entry.enginesGsd}\``,
|
|
543
|
+
);
|
|
544
|
+
lines.push(`- **License:** ${mdInline(entry.license)}`);
|
|
545
|
+
lines.push(`- **Discussion / ranking:** ${entry.discussion}`);
|
|
546
|
+
|
|
547
|
+
if (i < sorted.length - 1) lines.push('');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
return `${lines.join('\n')}\n`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
module.exports = {
|
|
554
|
+
LOOP_POINTS,
|
|
555
|
+
HOOK_KINDS,
|
|
556
|
+
INTERFACE_POINTS,
|
|
557
|
+
PROFILES,
|
|
558
|
+
AXES,
|
|
559
|
+
AXES_FREE_STRING,
|
|
560
|
+
CAPABILITY_REQUIRED,
|
|
561
|
+
EOS_REQUIRED,
|
|
562
|
+
isValidGsdRange,
|
|
563
|
+
validateEntries,
|
|
564
|
+
renderMarkdown,
|
|
565
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* scripts/validate-registry.cjs — CLI validator for the third-party
|
|
6
|
+
* discoverability catalogs (issue #2182):
|
|
7
|
+
*
|
|
8
|
+
* - docs/registries/capabilities.json ("GSD Community Capability Registry")
|
|
9
|
+
* - docs/registries/eos.json ("GSD EoS Registry", PR2 — optional
|
|
10
|
+
* until that JSON file ships)
|
|
11
|
+
*
|
|
12
|
+
* Validates each source's JSON array against the closed schema in
|
|
13
|
+
* scripts/registry-schema.cjs (validateEntries). Human-readable errors go to
|
|
14
|
+
* stderr; `--json` additionally prints a structured verdict to stdout.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node scripts/validate-registry.cjs # human-readable report
|
|
18
|
+
* node scripts/validate-registry.cjs --json # structured JSON verdict
|
|
19
|
+
*
|
|
20
|
+
* Exit codes:
|
|
21
|
+
* 0 every present source's entries are all valid
|
|
22
|
+
* 1 one or more entries failed validation (or a source's JSON is malformed)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
29
|
+
const { validateEntries } = require('./registry-schema.cjs');
|
|
30
|
+
|
|
31
|
+
// Resolved relative to process.cwd() (not __dirname) so the CLI validates
|
|
32
|
+
// whichever project it is invoked from — this is what lets tests drive it as
|
|
33
|
+
// a subprocess against isolated temp-fixture directories via `cwd`.
|
|
34
|
+
const SOURCES = [
|
|
35
|
+
{ file: 'capabilities.json', type: 'capability' },
|
|
36
|
+
{ file: 'eos.json', type: 'eos' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load + validate a single registry JSON file.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} jsonPath absolute path to the registry JSON file
|
|
43
|
+
* @param {'capability'|'eos'} type
|
|
44
|
+
* @returns {{ok: boolean, errors: Array<{index: number, id?: string, field: string, reason: string}>}}
|
|
45
|
+
*/
|
|
46
|
+
function validateFile(jsonPath, type) {
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = fs.readFileSync(jsonPath, 'utf8');
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
errors: [{ index: -1, field: '<file>', reason: `could not read ${jsonPath}: ${err.message}` }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = JSON.parse(raw);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
errors: [{ index: -1, field: '<file>', reason: `JSON parse error in ${jsonPath}: ${err.message}` }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!Array.isArray(entries)) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
errors: [{ index: -1, field: '<file>', reason: `${jsonPath} must be a JSON array of entries` }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return validateEntries(entries, { type });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main() {
|
|
78
|
+
const jsonMode = process.argv.includes('--json');
|
|
79
|
+
const registriesDir = path.join(process.cwd(), 'docs', 'registries');
|
|
80
|
+
|
|
81
|
+
const results = [];
|
|
82
|
+
let anyFailed = false;
|
|
83
|
+
|
|
84
|
+
for (const { file, type } of SOURCES) {
|
|
85
|
+
const jsonPath = path.join(registriesDir, file);
|
|
86
|
+
// eos.json is optional until PR2 ships it — skip silently when absent.
|
|
87
|
+
if (type === 'eos' && !fs.existsSync(jsonPath)) continue;
|
|
88
|
+
|
|
89
|
+
const verdict = validateFile(jsonPath, type);
|
|
90
|
+
results.push({ file, type, ok: verdict.ok, errors: verdict.errors });
|
|
91
|
+
if (!verdict.ok) anyFailed = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (jsonMode) {
|
|
95
|
+
process.stdout.write(JSON.stringify({ ok: !anyFailed, results }, null, 2) + '\n');
|
|
96
|
+
} else if (anyFailed) {
|
|
97
|
+
process.stderr.write('\nERROR validate-registry: one or more entries failed validation\n');
|
|
98
|
+
for (const result of results) {
|
|
99
|
+
if (result.ok) continue;
|
|
100
|
+
process.stderr.write(`\n${result.file}:\n`);
|
|
101
|
+
for (const e of result.errors) {
|
|
102
|
+
const idPart = e.id ? ` (id: ${e.id})` : '';
|
|
103
|
+
process.stderr.write(` entry[${e.index}]${idPart} field "${e.field}": ${e.reason}\n`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
process.stderr.write('\n');
|
|
107
|
+
} else {
|
|
108
|
+
process.stdout.write('ok validate-registry: all entries valid\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (anyFailed) throw new ExitError(1, 'registry validation failed');
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (require.main === module) runMain(main);
|
|
116
|
+
|
|
117
|
+
module.exports = { main, validateFile, SOURCES };
|