@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.6
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/.opencode/plugins/gsd-core.js +20 -0
- package/agents/gsd-doc-classifier.md +105 -0
- package/agents/gsd-doc-synthesizer.md +61 -0
- package/agents/gsd-ui-checker.md +30 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +1568 -622
- package/gsd-core/bin/gsd-tools.cjs +40 -1
- package/gsd-core/bin/lib/api-coverage.cjs +466 -0
- package/gsd-core/bin/lib/audit.cjs +6 -3
- package/gsd-core/bin/lib/capability-loader.cjs +11 -9
- package/gsd-core/bin/lib/capability-registry.cjs +761 -84
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/capability-writer.cjs +10 -1
- package/gsd-core/bin/lib/check-command-router.cjs +242 -3
- package/gsd-core/bin/lib/commands.cjs +7 -5
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +96 -0
- package/gsd-core/bin/lib/core-utils.cjs +4 -1
- package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
- package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
- package/gsd-core/bin/lib/host-integration.cjs +45 -4
- package/gsd-core/bin/lib/init.cjs +76 -39
- package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
- package/gsd-core/bin/lib/install-engine.cjs +228 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +7 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +3 -3
- package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
- package/gsd-core/bin/lib/phase-id.cjs +132 -3
- package/gsd-core/bin/lib/phase.cjs +78 -16
- package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
- package/gsd-core/bin/lib/roadmap.cjs +42 -56
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +248 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +39 -23
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +30 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +534 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/security.cjs +6 -36
- package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
- package/gsd-core/bin/lib/spec-section.cjs +111 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +1 -1
- package/gsd-core/bin/lib/state.cjs +24 -24
- package/gsd-core/bin/lib/surface.cjs +40 -6
- package/gsd-core/bin/lib/uat.cjs +4 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/validate.cjs +15 -6
- package/gsd-core/bin/lib/verify.cjs +33 -37
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +14 -9
- package/gsd-core/references/api-coverage.md +104 -0
- package/gsd-core/references/model-profiles.md +2 -2
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/specless-probe-fallback.md +172 -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/templates/config.json +2 -1
- package/gsd-core/workflows/audit-fix.md +9 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/code-review-fix.md +7 -3
- package/gsd-core/workflows/code-review.md +4 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
- package/gsd-core/workflows/execute-phase.md +1 -25
- package/gsd-core/workflows/plan-phase.md +37 -2
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/review.md +59 -13
- package/gsd-core/workflows/settings-advanced.md +12 -9
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/gsd-core/workflows/verify-phase.md +3 -2
- package/gsd-core/workflows/verify-work.md +38 -0
- package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
- package/hooks/dist/gsd-cursor-stop.js +48 -0
- package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
- package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
- 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 +6 -0
- package/hooks/gsd-cursor-pre-tool.js +76 -0
- package/hooks/gsd-cursor-stop.js +48 -0
- package/hooks/gsd-cursor-subagent-start.js +50 -0
- package/hooks/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +6 -0
- package/package.json +9 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +8 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-phase-id-drift.cjs +150 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/run-tests.cjs +21 -1
- package/scripts/validate-registry.cjs +117 -0
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.7.0-rc.
|
|
3
|
+
"version": "1.7.0-rc.6",
|
|
4
4
|
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
5
5
|
"main": ".opencode/plugins/gsd-core.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
".opencode",
|
|
21
21
|
"GEMINI.md",
|
|
22
22
|
"hooks",
|
|
23
|
-
"scripts"
|
|
23
|
+
"scripts",
|
|
24
|
+
"pi",
|
|
25
|
+
"vscode"
|
|
24
26
|
],
|
|
25
27
|
"keywords": [
|
|
26
28
|
"claude",
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
"check:env": "node scripts/check-env.cjs",
|
|
79
81
|
"check:alias-drift": "node scripts/check-alias-drift.cjs",
|
|
80
82
|
"check:identity-drift": "node scripts/lint-package-identity-drift.cjs",
|
|
83
|
+
"check:phase-id-drift": "node scripts/lint-phase-id-drift.cjs",
|
|
81
84
|
"check:integrity": "node scripts/check-npm-integrity.cjs",
|
|
82
85
|
"build": "npm run generate:identity && npm run build:lib && npm run gen:plugin-skills && npm run gen:loop-host-contract && npm run gen:capability-registry && npm run build:hooks",
|
|
83
86
|
"build:hooks": "node scripts/build-hooks.js",
|
|
@@ -86,6 +89,8 @@
|
|
|
86
89
|
"gen:loop-host-contract": "node scripts/gen-loop-host-contract.cjs --write",
|
|
87
90
|
"gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write",
|
|
88
91
|
"gen:capability-registry": "node scripts/gen-capability-registry.cjs --write",
|
|
92
|
+
"gen:registry": "node scripts/gen-registry.cjs --write",
|
|
93
|
+
"validate:registry": "node scripts/validate-registry.cjs",
|
|
89
94
|
"prepack": "npm run build:lib",
|
|
90
95
|
"prepare": "npm run build:lib",
|
|
91
96
|
"version": "node scripts/sync-manifest-versions.cjs --stage && node scripts/gen-capability-registry.cjs --write && git add gsd-core/bin/lib/capability-registry.cjs",
|
|
@@ -94,7 +99,7 @@
|
|
|
94
99
|
"pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
|
|
95
100
|
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
|
96
101
|
"lint:fix": "eslint . --fix",
|
|
97
|
-
"lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs",
|
|
102
|
+
"lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/validate-registry.cjs",
|
|
98
103
|
"lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
|
|
99
104
|
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
|
|
100
105
|
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
|
@@ -102,7 +107,7 @@
|
|
|
102
107
|
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
|
|
103
108
|
"lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
|
|
104
109
|
"lint:changeset": "node scripts/changeset/lint.cjs",
|
|
105
|
-
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check",
|
|
110
|
+
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check",
|
|
106
111
|
"lint:docs": "node scripts/lint-docs-required.cjs",
|
|
107
112
|
"lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
|
|
108
113
|
"ci:test-scope": "node scripts/ci-test-scope.cjs",
|
package/pi/gsd.cjs
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD extension for pi (pi.dev) — ADR-1239 Phase D / #1944, upgraded #2102 Stage 2.
|
|
5
|
+
*
|
|
6
|
+
* pi is a Programmatic-CLI host whose TS extensions implement the ExtensionAPI
|
|
7
|
+
* (`@earendil-works/pi-coding-agent`): registerCommand({handler(args, ctx)}) /
|
|
8
|
+
* registerTool({execute(toolCallId, params, signal, onUpdate, ctx)}) / pi.on(event, handler).
|
|
9
|
+
* This extension binds GSD's command surface to pi via the imperative adapter
|
|
10
|
+
* path — the programmatic-CLI peer of the OpenCode worked binding.
|
|
11
|
+
*
|
|
12
|
+
* Installation: copy this file to ~/.pi/agent/extensions/gsd.cjs (pi loads
|
|
13
|
+
* extensions via jiti from that dir). The engine is resolved from the installed
|
|
14
|
+
* GSD tree (walk-up like the OpenCode plugin). pi's shared hooks/ bundle
|
|
15
|
+
* (hooks/*.js + hooks/lib/git-cmd.js) is installed alongside the extension —
|
|
16
|
+
* capabilities/pi/capability.json does NOT set
|
|
17
|
+
* `hostBehaviors.skipSharedHooksInstall` (#2102 Stage 2 fix; pi is
|
|
18
|
+
* architecturally identical to OpenCode here: `hooksSurface: 'none'` + a
|
|
19
|
+
* native extension that spawns the staged hooks — not Kilo/ZCode's
|
|
20
|
+
* no-plugin-surface case, where the same hooks would be genuine dead weight).
|
|
21
|
+
* This is what makes the event bridges below (and the tokenizer require)
|
|
22
|
+
* resolve for real in an installed tree, not just in this dev repo.
|
|
23
|
+
*
|
|
24
|
+
* Engine entry: dispatch is SUBPROCESS-REUSE to gsd-tools.cjs (bounded,
|
|
25
|
+
* no-throw — dispatchGsdCommand in shell-command-projection.cjs), NOT an
|
|
26
|
+
* in-process command-routing hub. No fully-populated hub factory exists
|
|
27
|
+
* anywhere in gsd-core — every createHub() caller in the tree builds a
|
|
28
|
+
* single-family hub for its own narrow purpose — so the "in-process createHub"
|
|
29
|
+
* framing of the original #1944 cut was aspirational and is not achievable
|
|
30
|
+
* without a hub factory that doesn't exist. This mirrors the precedent already
|
|
31
|
+
* established for the OpenCode/Kilo hook bridge (.opencode/plugins/gsd-core.js
|
|
32
|
+
* header: "Architecture: SUBPROCESS REUSE ... spawns existing hook scripts as
|
|
33
|
+
* child processes") — the same pattern, applied to command dispatch. The
|
|
34
|
+
* companion MCP server (gsd-mcp-server) dispatches through the SAME shared
|
|
35
|
+
* helper for out-of-process hosts.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} pi pi ExtensionAPI (registerTool/registerCommand/on/…)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const path = require('path');
|
|
42
|
+
const { spawnSync } = require('child_process');
|
|
43
|
+
|
|
44
|
+
// Resolve the GSD engine tree (the dir holding gsd-core/ + hooks/).
|
|
45
|
+
// Works across dev (<root>/pi/gsd.cjs → <root>) and installed layouts.
|
|
46
|
+
function resolveEngineRoot(startDir) {
|
|
47
|
+
let dir = startDir;
|
|
48
|
+
for (let i = 0; i < 6; i++) {
|
|
49
|
+
if (fs.existsSync(path.join(dir, 'gsd-core'))) return dir;
|
|
50
|
+
const parent = path.dirname(dir);
|
|
51
|
+
if (parent === dir) break;
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
return path.resolve(startDir, '..');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ENGINE_ROOT = resolveEngineRoot(__dirname);
|
|
58
|
+
const GSD_CORE = path.join(ENGINE_ROOT, 'gsd-core');
|
|
59
|
+
|
|
60
|
+
// ── curated top-level command families (gsd-tools.cjs TOP_LEVEL_USAGE) ──────
|
|
61
|
+
// readCmdNames() (scripts/fix-slash-commands.cjs) reads commands/, which pi
|
|
62
|
+
// does NOT install (it ships a single native-extension file, no shared
|
|
63
|
+
// commands/ dir) — it would always return []. This is a self-contained,
|
|
64
|
+
// hand-curated subset of the STABLE top-level families documented by
|
|
65
|
+
// `node gsd-core/bin/gsd-tools.cjs --help` (gsd-tools.cjs:689-705). Named +
|
|
66
|
+
// exported (via _internals) so a test can assert against it directly.
|
|
67
|
+
const PI_COMMAND_FAMILIES = Object.freeze([
|
|
68
|
+
'agent', 'capability', 'check', 'commit', 'config-get', 'config-path',
|
|
69
|
+
'config-set', 'effort', 'git', 'graphify', 'init', 'intel', 'learnings',
|
|
70
|
+
'list-todos', 'loop', 'milestone', 'phase', 'phases', 'progress',
|
|
71
|
+
'requirements', 'research-plan', 'research-store', 'resolve-granularity',
|
|
72
|
+
'resolve-model', 'roadmap', 'scaffold', 'smart-entry', 'state', 'task',
|
|
73
|
+
'template', 'user-story', 'validate', 'verify', 'workstream', 'worktree',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Filter PI_COMMAND_FAMILIES by prefix (startsWith). Returns null when there
|
|
78
|
+
* are no matches, per pi's `AutocompleteItem[]|null` contract.
|
|
79
|
+
* @param {string} prefix
|
|
80
|
+
* @returns {{value: string, label: string}[] | null}
|
|
81
|
+
*/
|
|
82
|
+
function getArgumentCompletions(prefix) {
|
|
83
|
+
const p = typeof prefix === 'string' ? prefix : '';
|
|
84
|
+
const matches = PI_COMMAND_FAMILIES.filter((name) => name.startsWith(p));
|
|
85
|
+
if (matches.length === 0) return null;
|
|
86
|
+
return matches.map((value) => ({ value, label: value }));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tokenize the raw `/gsd <args>` string into { family, subcommand, args }.
|
|
91
|
+
* Reuses the quote-aware whitespace tokenizer already shipped for hooks
|
|
92
|
+
* (hooks/lib/git-cmd.js's `tokenize`) rather than re-implementing shell-word
|
|
93
|
+
* splitting a second time. #2102 Stage 2: pi's capability descriptor no
|
|
94
|
+
* longer sets `hostBehaviors.skipSharedHooksInstall` (adversarial-review
|
|
95
|
+
* finding #1/#2 — pi ships NO hooks/ with that flag set, so this require was
|
|
96
|
+
* dead in a real install), so the shared hooks/ bundle — including
|
|
97
|
+
* hooks/lib/git-cmd.js — is installed alongside the extension for real
|
|
98
|
+
* (mirrors OpenCode, whose native plugin also spawns the staged hooks/*.js
|
|
99
|
+
* bundle). The require below is therefore the PRIMARY, live path in an
|
|
100
|
+
* installed pi tree; the whitespace-split fallback stays as defense-in-depth
|
|
101
|
+
* for a corrupted/partial install (e.g. a user who deleted hooks/lib/ by
|
|
102
|
+
* hand) rather than the only-ever-taken path.
|
|
103
|
+
* @param {string} rawArgs
|
|
104
|
+
* @returns {{ family: string, subcommand?: string, args: string[] }}
|
|
105
|
+
*/
|
|
106
|
+
function parseGsdCommandArgs(rawArgs) {
|
|
107
|
+
let tokenize;
|
|
108
|
+
try {
|
|
109
|
+
({ tokenize } = require(path.join(ENGINE_ROOT, 'hooks', 'lib', 'git-cmd.js')));
|
|
110
|
+
} catch {
|
|
111
|
+
tokenize = (s) => String(s || '').split(/\s+/).filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
const tokens = tokenize(typeof rawArgs === 'string' ? rawArgs : '');
|
|
114
|
+
return {
|
|
115
|
+
// Empty args → dispatch gsd-tools.cjs's own --help surface (a real,
|
|
116
|
+
// working, ok:true default — NOT the 'query'/'help' pairing the original
|
|
117
|
+
// #1944 cut used, which is not a valid gsd-tools.cjs command).
|
|
118
|
+
family: tokens[0] || '--help',
|
|
119
|
+
subcommand: tokens[1],
|
|
120
|
+
args: tokens.slice(2),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Best-effort TypeBox schema for gsd_invoke's `parameters`, falling back to a
|
|
126
|
+
* plain JSON-Schema object when the `typebox` package is unavailable (it is
|
|
127
|
+
* NOT a gsd-core dependency — pi's own ExtensionAPI contract expects TypeBox,
|
|
128
|
+
* but nothing in this repo installs it). TypeBox schemas ARE JSON Schema, so
|
|
129
|
+
* the fallback object is structurally equivalent for hosts that accept plain
|
|
130
|
+
* JSON Schema; this is a best-effort shim for the flat-file extension case.
|
|
131
|
+
* @returns {object}
|
|
132
|
+
*/
|
|
133
|
+
function buildGsdInvokeParameters() {
|
|
134
|
+
try {
|
|
135
|
+
const typebox = require('typebox');
|
|
136
|
+
const Type = typebox && typebox.Type;
|
|
137
|
+
if (Type) {
|
|
138
|
+
return Type.Object({
|
|
139
|
+
family: Type.String(),
|
|
140
|
+
subcommand: Type.Optional(Type.String()),
|
|
141
|
+
args: Type.Optional(Type.Array(Type.String())),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// typebox is not installed in this environment — fall through.
|
|
146
|
+
}
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
'gsd: typebox unavailable — gsd_invoke "parameters" falling back to a plain JSON-schema object.\n',
|
|
149
|
+
);
|
|
150
|
+
return {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
family: { type: 'string' },
|
|
154
|
+
subcommand: { type: 'string' },
|
|
155
|
+
args: { type: 'array', items: { type: 'string' } },
|
|
156
|
+
},
|
|
157
|
+
required: ['family'],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build the `before_provider_request` handler that steers pi's model
|
|
163
|
+
* selection to GSD's tier-resolved id (modelMode: 'active' per
|
|
164
|
+
* capabilities/pi/capability.json). GSD does NOT call `pi.registerProvider` —
|
|
165
|
+
* that registers a NEW model provider; GSD's job here is only to pick a
|
|
166
|
+
* tier-appropriate id AMONG pi's EXISTING built-in anthropic models, so
|
|
167
|
+
* registerProvider would be the wrong primitive (it would wrongly add a fake
|
|
168
|
+
* provider instead of steering the real one).
|
|
169
|
+
*
|
|
170
|
+
* v1 tier policy: GSD does not yet expose a per-turn/per-agent tier signal to
|
|
171
|
+
* this event, so a conservative fixed default tier is used (parameterized —
|
|
172
|
+
* default 'sonnet' — so a future richer signal, or a test, can override it).
|
|
173
|
+
*
|
|
174
|
+
* ASSUMPTION (flagged — verify against a live pi host): the event payload's
|
|
175
|
+
* model field is named `model`, matching the anthropic-messages payload shape
|
|
176
|
+
* (Context7-confirmed for the wire protocol; pi's own before_provider_request
|
|
177
|
+
* event schema was not independently verifiable in this environment). If pi's
|
|
178
|
+
* actual field name differs, this returns the WRONG key and pi's fail-open
|
|
179
|
+
* default takes over only because bare-model-id mismatches degrade to
|
|
180
|
+
* provider-level errors, not GSD-level ones — a discrepancy here needs a
|
|
181
|
+
* live-host smoke test before shipping past this stage.
|
|
182
|
+
*
|
|
183
|
+
* Fail-open: any resolution failure (or a null/falsy resolved model — e.g. an
|
|
184
|
+
* unrecognized tier) returns `undefined`, leaving pi's model choice untouched.
|
|
185
|
+
* NEVER returns a payload with a missing/empty model id.
|
|
186
|
+
*
|
|
187
|
+
* @param {{ tier?: string }} [opts]
|
|
188
|
+
* @returns {(event: object, ctx: object) => Promise<object|undefined>}
|
|
189
|
+
*/
|
|
190
|
+
function buildBeforeProviderRequestHandler({ tier = 'sonnet' } = {}) {
|
|
191
|
+
return async function onBeforeProviderRequest(event, ctx) {
|
|
192
|
+
try {
|
|
193
|
+
const effectiveCwd = (ctx && ctx.cwd) || process.cwd();
|
|
194
|
+
const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs'));
|
|
195
|
+
const { loadConfig } = require(path.join(GSD_CORE, 'bin', 'lib', 'config-loader.cjs'));
|
|
196
|
+
const config = loadConfig(effectiveCwd);
|
|
197
|
+
const overrides = (config && config.model_profile_overrides) || undefined;
|
|
198
|
+
const entry = resolveTierEntry({ runtime: 'pi', tier, overrides });
|
|
199
|
+
const modelId = entry && typeof entry.model === 'string' && entry.model.length > 0 ? entry.model : null;
|
|
200
|
+
if (!modelId) return undefined; // fail-open — leave pi's model untouched
|
|
201
|
+
const basePayload = (event && typeof event === 'object' && event.payload && typeof event.payload === 'object')
|
|
202
|
+
? event.payload
|
|
203
|
+
: {};
|
|
204
|
+
return { ...basePayload, model: modelId };
|
|
205
|
+
} catch {
|
|
206
|
+
return undefined; // fail-open on any resolution error
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Bounded subprocess bridge to GSD's Claude Code hook scripts. Mirrors
|
|
213
|
+
* .opencode/plugins/gsd-core.js's `runHook` (SUBPROCESS-REUSE): spawns
|
|
214
|
+
* `node <hooks/hookFile>` with the payload piped to stdin, on a bounded
|
|
215
|
+
* timeout. NEVER throws — a missing hook file, a spawn error, or a timeout
|
|
216
|
+
* all degrade to a silent-allow result so a hook problem can never block pi.
|
|
217
|
+
* @param {string} hookFile filename under hooks/, e.g. "gsd-context-monitor.js"
|
|
218
|
+
* @param {object} payload
|
|
219
|
+
* @param {{ timeout?: number, cwd?: string }} [opts]
|
|
220
|
+
* @returns {{ stdout: string, exitCode: number, timedOut: boolean }}
|
|
221
|
+
*/
|
|
222
|
+
function runHook(hookFile, payload, opts = {}) {
|
|
223
|
+
const hookPath = path.join(ENGINE_ROOT, 'hooks', hookFile);
|
|
224
|
+
if (!fs.existsSync(hookPath)) return { stdout: '', exitCode: 0, timedOut: false };
|
|
225
|
+
const timeout = opts.timeout || 8000;
|
|
226
|
+
let result;
|
|
227
|
+
try {
|
|
228
|
+
result = spawnSync(process.execPath, [hookPath], {
|
|
229
|
+
input: JSON.stringify(payload || {}),
|
|
230
|
+
encoding: 'utf8',
|
|
231
|
+
timeout,
|
|
232
|
+
cwd: opts.cwd || process.cwd(),
|
|
233
|
+
windowsHide: true,
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
return { stdout: '', exitCode: 0, timedOut: false };
|
|
237
|
+
}
|
|
238
|
+
const stdout = (result && typeof result.stdout === 'string') ? result.stdout.trim() : '';
|
|
239
|
+
const exitCode = (result && result.status != null) ? result.status : 0;
|
|
240
|
+
return { stdout, exitCode, timedOut: !!(result && result.signal === 'SIGTERM') };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = function gsdPiExtension(pi) {
|
|
244
|
+
if (!pi || typeof pi !== 'object') {
|
|
245
|
+
throw new TypeError('gsdPiExtension: pi ExtensionAPI is required');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── /gsd command: dispatch through gsd-tools.cjs (subprocess-reuse) ──────
|
|
249
|
+
pi.registerCommand('gsd', {
|
|
250
|
+
description: 'Invoke a GSD command via the embedded engine (subprocess-reuse adapter).',
|
|
251
|
+
getArgumentCompletions,
|
|
252
|
+
handler: async (args, ctx) => {
|
|
253
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
254
|
+
const { family, subcommand, args: rest } = parseGsdCommandArgs(args);
|
|
255
|
+
let dispatchGsdCommand;
|
|
256
|
+
try {
|
|
257
|
+
({ dispatchGsdCommand } = require(path.join(GSD_CORE, 'bin', 'lib', 'shell-command-projection.cjs')));
|
|
258
|
+
} catch (e) {
|
|
259
|
+
return `GSD engine unavailable: ${e && e.message ? e.message : String(e)}`;
|
|
260
|
+
}
|
|
261
|
+
const result = dispatchGsdCommand({ family, subcommand, args: rest, cwd });
|
|
262
|
+
if (result.ok) return result.stdout;
|
|
263
|
+
return `GSD error: ${result.stderr || result.stdout || `dispatch failed (exit ${result.code})`}`;
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ── gsd_invoke tool: programmatic command invocation ────────────────────
|
|
268
|
+
pi.registerTool({
|
|
269
|
+
name: 'gsd_invoke',
|
|
270
|
+
label: 'GSD Invoke',
|
|
271
|
+
description: 'Invoke a GSD command family/subcommand through the engine.',
|
|
272
|
+
parameters: buildGsdInvokeParameters(),
|
|
273
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
274
|
+
const p = (params && typeof params === 'object') ? params : {};
|
|
275
|
+
const family = typeof p.family === 'string' ? p.family : '';
|
|
276
|
+
if (!family) {
|
|
277
|
+
return { content: [{ type: 'text', text: 'gsd_invoke requires a non-empty string "family".' }] };
|
|
278
|
+
}
|
|
279
|
+
const subcommand = typeof p.subcommand === 'string' ? p.subcommand : undefined;
|
|
280
|
+
const invokeArgs = Array.isArray(p.args) ? p.args : [];
|
|
281
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
282
|
+
let dispatchGsdCommand;
|
|
283
|
+
try {
|
|
284
|
+
({ dispatchGsdCommand } = require(path.join(GSD_CORE, 'bin', 'lib', 'shell-command-projection.cjs')));
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return { content: [{ type: 'text', text: `GSD engine unavailable: ${e && e.message ? e.message : String(e)}` }] };
|
|
287
|
+
}
|
|
288
|
+
const result = dispatchGsdCommand({ family, subcommand, args: invokeArgs, cwd });
|
|
289
|
+
const text = result.ok ? result.stdout : (result.stderr || result.stdout || `dispatch failed (exit ${result.code})`);
|
|
290
|
+
return { content: [{ type: 'text', text }] };
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ── before_provider_request: active-model steering (modelMode: 'active') ──
|
|
295
|
+
// GSD steers pi's EXISTING built-in anthropic models; it does NOT call
|
|
296
|
+
// pi.registerProvider (that would wrongly register a NEW fake provider —
|
|
297
|
+
// see buildBeforeProviderRequestHandler's doc comment).
|
|
298
|
+
pi.on('before_provider_request', buildBeforeProviderRequestHandler());
|
|
299
|
+
|
|
300
|
+
// ── Event bindings: bounded subprocess bridge to GSD's hook scripts ──────
|
|
301
|
+
// Each binding fails open — a hook error/timeout/missing-file never blocks
|
|
302
|
+
// pi (mirrors .opencode/plugins/gsd-core.js's runHook SUBPROCESS-REUSE
|
|
303
|
+
// pattern, applied to pi's ExtensionAPI event names).
|
|
304
|
+
|
|
305
|
+
// session_start → SessionStart-equivalent bootstrap.
|
|
306
|
+
pi.on('session_start', async (event, ctx) => {
|
|
307
|
+
try {
|
|
308
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
309
|
+
runHook('gsd-ensure-canonical-path.js', { hook_event_name: 'SessionStart', cwd }, { cwd });
|
|
310
|
+
} catch { /* fail-open */ }
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// before_agent_start → workflow-guard bridge. Forward-compatible binding:
|
|
314
|
+
// gsd-workflow-guard.js's current triggers are tool-scoped (Write/Edit/
|
|
315
|
+
// Bash via tool_name/tool_input), so with no tool_name in the payload it
|
|
316
|
+
// fires as a safe no-op today — wired so a future agent-start-scoped check
|
|
317
|
+
// can attach without a plugin change (mirrors the OpenCode session.idle
|
|
318
|
+
// recognized-but-unused sentinel pattern).
|
|
319
|
+
pi.on('before_agent_start', async (event, ctx) => {
|
|
320
|
+
try {
|
|
321
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
322
|
+
runHook('gsd-workflow-guard.js', { hook_event_name: 'before_agent_start', cwd }, { cwd });
|
|
323
|
+
} catch { /* fail-open */ }
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// session_before_compact → PreCompact-equivalent (context-usage bridge).
|
|
327
|
+
pi.on('session_before_compact', async (event, ctx) => {
|
|
328
|
+
try {
|
|
329
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
330
|
+
runHook('gsd-context-monitor.js', { hook_event_name: 'PreCompact', cwd }, { cwd });
|
|
331
|
+
} catch { /* fail-open */ }
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// tool_call event: lifecycle hook bridge attachment point (kept from the
|
|
335
|
+
// original cut — the PreToolUse/PostToolUse tool_name/tool_input mapping
|
|
336
|
+
// is a follow-up once pi's tool_call payload shape is verified against a
|
|
337
|
+
// live host).
|
|
338
|
+
pi.on('tool_call', async function () {
|
|
339
|
+
/* GSD hook bridge attachment point (PreToolUse/PostToolUse mapping). */
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Test-only internals (mirrors the OpenCode plugin pattern) — wired to the
|
|
344
|
+
// real functions (not stubs) so tests can exercise parsing/completions/model
|
|
345
|
+
// resolution WITHOUT a live pi runtime.
|
|
346
|
+
module.exports._internals = {
|
|
347
|
+
resolveEngineRoot,
|
|
348
|
+
parseGsdCommandArgs,
|
|
349
|
+
getArgumentCompletions,
|
|
350
|
+
PI_COMMAND_FAMILIES,
|
|
351
|
+
buildBeforeProviderRequestHandler,
|
|
352
|
+
buildGsdInvokeParameters,
|
|
353
|
+
runHook,
|
|
354
|
+
};
|
package/scripts/build-hooks.js
CHANGED
|
@@ -37,9 +37,16 @@ const HOOKS_TO_COPY = [
|
|
|
37
37
|
// so require('./managed-hooks-registry.cjs') resolves in the installed hooks/ dir.
|
|
38
38
|
'managed-hooks-registry.cjs',
|
|
39
39
|
'gsd-context-monitor.js',
|
|
40
|
-
// Cursor lifecycle hooks (
|
|
40
|
+
// Cursor lifecycle hooks (#777 + ADR-1239/#2089): 6 managed events
|
|
41
41
|
'gsd-cursor-session-start.js',
|
|
42
42
|
'gsd-cursor-post-tool.js',
|
|
43
|
+
'gsd-cursor-pre-tool.js',
|
|
44
|
+
'gsd-cursor-stop.js',
|
|
45
|
+
'gsd-cursor-subagent-start.js',
|
|
46
|
+
'gsd-cursor-subagent-stop.js',
|
|
47
|
+
// Windsurf/Cascade lifecycle hooks (ADR-1239/#2100 Stage 2): 2 blocking events
|
|
48
|
+
'gsd-windsurf-pre-write.js',
|
|
49
|
+
'gsd-windsurf-pre-command.js',
|
|
43
50
|
// Claude Code FileChanged hook (#770) — hot-reloads gsd config when
|
|
44
51
|
// .planning/config.json changes mid-session. Must ship to dist so the
|
|
45
52
|
// installer can copy it to the target hooks/ dir and register FileChanged.
|
|
@@ -26,7 +26,16 @@ const VOLATILE_FILES = new Set([
|
|
|
26
26
|
'.gsd-source',
|
|
27
27
|
'gsd-core/CHANGELOG.md',
|
|
28
28
|
]);
|
|
29
|
-
|
|
29
|
+
// Must match tests/golden-install-parity.test.cjs exactly — settings.local.json
|
|
30
|
+
// (Claude LOCAL hook surface, #338/#2086) embeds the same platform-varying
|
|
31
|
+
// node-runner command and is excluded there; omitting it here mis-generated the
|
|
32
|
+
// claude-local fixture (#2100).
|
|
33
|
+
const HOOK_CONFIG_FILES = new Set(['settings.json', 'settings.local.json', 'hooks.json']);
|
|
34
|
+
// Kimi's native config.toml (#2095) — see tests/golden-install-parity.test.cjs'
|
|
35
|
+
// HOOK_CONFIG_RELATIVE_PATHS comment for why this is an exact relative-path
|
|
36
|
+
// exclusion rather than a HOOK_CONFIG_FILES basename entry (a basename entry
|
|
37
|
+
// would also blind Codex's stable, platform-independent config.toml fixture).
|
|
38
|
+
const HOOK_CONFIG_RELATIVE_PATHS = new Set(['.kimi/config.toml']);
|
|
30
39
|
const EXCLUDED_PREFIXES = ['gsd-core/bin/lib/'];
|
|
31
40
|
|
|
32
41
|
function buildParityManifest(configDir, root) {
|
|
@@ -36,6 +45,7 @@ function buildParityManifest(configDir, root) {
|
|
|
36
45
|
const rel = path.relative(configDir, full).split(path.sep).join('/');
|
|
37
46
|
if (VOLATILE_FILES.has(rel)) continue;
|
|
38
47
|
if (HOOK_CONFIG_FILES.has(path.basename(rel))) continue;
|
|
48
|
+
if (HOOK_CONFIG_RELATIVE_PATHS.has(rel)) continue;
|
|
39
49
|
if (EXCLUDED_PREFIXES.some((p) => rel.startsWith(p))) continue;
|
|
40
50
|
const content = fs.readFileSync(full);
|
|
41
51
|
const normalized = content.toString('utf8').split(root).join('<HOME>').split(PKG_VERSION).join('<VERSION>');
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* scripts/gen-registry.cjs — generates docs/registries/capability-registry.md
|
|
6
|
+
* (and, once PR2 ships docs/registries/eos.json, docs/registries/eos-registry.md)
|
|
7
|
+
* from the corresponding source JSON, via registry-schema.cjs#renderMarkdown.
|
|
8
|
+
* Issue #2182.
|
|
9
|
+
*
|
|
10
|
+
* NOT to be confused with `scripts/gen-capability-registry.cjs`: that script
|
|
11
|
+
* generates the RUNTIME capability manifest consumed by the host at runtime
|
|
12
|
+
* (`gsd-core/bin/lib/capability-registry.cjs`, built from every
|
|
13
|
+
* `capabilities/<id>/capability.json` declaration). THIS script instead
|
|
14
|
+
* generates the human-facing DOCUMENTATION catalog pages
|
|
15
|
+
* (`docs/registries/*.md`) from the third-party discoverability registry
|
|
16
|
+
* source JSON (`docs/registries/{capabilities,eos}.json`). The two pipelines
|
|
17
|
+
* are independent — do not conflate them.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node scripts/gen-registry.cjs # print rendered markdown(s) to stdout
|
|
21
|
+
* node scripts/gen-registry.cjs --write # write the *-registry.md file(s)
|
|
22
|
+
* node scripts/gen-registry.cjs --check # exit 1 if a committed *-registry.md is stale
|
|
23
|
+
*
|
|
24
|
+
* Root is resolved from process.cwd() (not __dirname) — mirrors
|
|
25
|
+
* scripts/validate-registry.cjs so both are drivable as subprocesses against
|
|
26
|
+
* isolated temp-fixture directories via `cwd`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
|
|
32
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
33
|
+
const { renderMarkdown } = require('./registry-schema.cjs');
|
|
34
|
+
|
|
35
|
+
const SOURCES = [
|
|
36
|
+
{ type: 'capability', jsonFile: 'capabilities.json', mdFile: 'capability-registry.md' },
|
|
37
|
+
{ type: 'eos', jsonFile: 'eos.json', mdFile: 'eos-registry.md' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The generator always writes LF; a Windows checkout (autocrlf) may present
|
|
42
|
+
* committed files with CRLF. Normalize before comparing so `--check` only
|
|
43
|
+
* fails on real content drift, not checkout-introduced line-ending noise.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} content
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function normalizeLineEndings(content) {
|
|
49
|
+
return content.replace(/\r/g, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getRegistriesDir() {
|
|
53
|
+
return path.join(process.cwd(), 'docs', 'registries');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render the markdown for a single registry type from its committed source
|
|
58
|
+
* JSON.
|
|
59
|
+
*
|
|
60
|
+
* Only `eos.json` is optional (pre-PR2, before that source JSON ships) —
|
|
61
|
+
* an absent `eos.json` returns null and callers treat that as "nothing to
|
|
62
|
+
* do". `capabilities.json` is the primary registry source: a missing
|
|
63
|
+
* `capabilities.json` is ALWAYS an error (never a silent "up to date"
|
|
64
|
+
* pass), mirroring the type distinction in `scripts/validate-registry.cjs`
|
|
65
|
+
* (`type === 'eos' && !exists → continue`).
|
|
66
|
+
*
|
|
67
|
+
* @param {'capability'|'eos'} type
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
function renderFor(type) {
|
|
71
|
+
const source = SOURCES.find((s) => s.type === type);
|
|
72
|
+
if (!source) throw new Error(`gen-registry: unknown registry type "${type}"`);
|
|
73
|
+
|
|
74
|
+
const jsonPath = path.join(getRegistriesDir(), source.jsonFile);
|
|
75
|
+
if (!fs.existsSync(jsonPath)) {
|
|
76
|
+
if (type === 'eos') return null;
|
|
77
|
+
throw new ExitError(
|
|
78
|
+
1,
|
|
79
|
+
`${source.jsonFile} does not exist at ${jsonPath}. Run:\n node scripts/gen-registry.cjs --write\n(after adding docs/registries/${source.jsonFile})`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const entries = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
84
|
+
return renderMarkdown(entries, { type, sourceFile: source.jsonFile });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function main() {
|
|
88
|
+
const [, , flag] = process.argv;
|
|
89
|
+
const registriesDir = getRegistriesDir();
|
|
90
|
+
let anyDrift = false;
|
|
91
|
+
|
|
92
|
+
for (const { type, mdFile } of SOURCES) {
|
|
93
|
+
const rendered = renderFor(type);
|
|
94
|
+
if (rendered === null) continue; // source JSON absent (eos.json before PR2)
|
|
95
|
+
|
|
96
|
+
const mdPath = path.join(registriesDir, mdFile);
|
|
97
|
+
|
|
98
|
+
if (flag === '--check') {
|
|
99
|
+
if (!fs.existsSync(mdPath)) {
|
|
100
|
+
process.stderr.write(`${mdFile} does not exist. Run:\n node scripts/gen-registry.cjs --write\n`);
|
|
101
|
+
anyDrift = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const committed = fs.readFileSync(mdPath, 'utf8');
|
|
105
|
+
if (normalizeLineEndings(committed) !== normalizeLineEndings(rendered)) {
|
|
106
|
+
process.stderr.write(`${mdFile} is stale. Run:\n node scripts/gen-registry.cjs --write\n`);
|
|
107
|
+
anyDrift = true;
|
|
108
|
+
}
|
|
109
|
+
} else if (flag === '--write') {
|
|
110
|
+
fs.mkdirSync(registriesDir, { recursive: true });
|
|
111
|
+
fs.writeFileSync(mdPath, rendered);
|
|
112
|
+
process.stdout.write(`Wrote ${mdPath}\n`);
|
|
113
|
+
} else {
|
|
114
|
+
process.stdout.write(rendered + '\n');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (flag === '--check') {
|
|
119
|
+
if (anyDrift) throw new ExitError(1, 'registry markdown is stale');
|
|
120
|
+
process.stdout.write('docs/registries/*.md are up to date.\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (require.main === module) runMain(main);
|
|
127
|
+
|
|
128
|
+
module.exports = { main, renderFor, SOURCES, normalizeLineEndings };
|