superdev-cli 0.1.1 → 0.2.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 +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/package.json +2 -1
- package/scripts/providers/adapter.mjs +196 -0
- package/scripts/providers/detect.mjs +519 -0
- package/scripts/providers/registry.mjs +248 -0
- package/scripts/validate/packaging.mjs +116 -0
- package/scripts/validate/validate-all.mjs +2 -1
- package/skills/docs/scripts/profile-detect.mjs +56 -2
- package/src/cli/product-map.mjs +10 -2
- package/src/cli/render.mjs +11 -3
- package/src/cli.mjs +540 -33
- package/src/db/migrations/013_question_options.sql +28 -0
- package/src/docs/proposals.mjs +19 -1
- package/src/init/discovery.mjs +46 -0
- package/src/init/index.mjs +142 -22
- package/src/init/questions.mjs +115 -5
- package/src/product/authoring.mjs +649 -0
- package/src/service/assets/control-center.html +62 -62
- package/src/service/assets/control-center.manifest.json +4 -4
- package/src/service/mutations.mjs +78 -30
- package/src/tasks/lifecycle.mjs +45 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Provider registry - the canonical adapter contract for every external
|
|
4
|
+
* capability Superdev orchestrates.
|
|
5
|
+
*
|
|
6
|
+
* WHAT THIS FILE IS: identity, ownership boundary, and the machine-checkable
|
|
7
|
+
* parts of each adapter contract - how to DETECT a provider, how to probe its
|
|
8
|
+
* readiness without side effects, where it is installed from, what consent its
|
|
9
|
+
* installation needs, what a bounded context packet may contain, and what
|
|
10
|
+
* evidence its output must carry.
|
|
11
|
+
*
|
|
12
|
+
* WHAT THIS FILE IS NOT: a copy of any provider's methodology. Superdev never
|
|
13
|
+
* vendors, renames, reimplements, or approximates provider guidance. Each entry
|
|
14
|
+
* records WHERE the capability lives and HOW to reach it - never what it says.
|
|
15
|
+
* If a provider is absent, Superdev reports the truthful capability state and a
|
|
16
|
+
* remediation plan; it never silently substitutes its own approximation.
|
|
17
|
+
*
|
|
18
|
+
* Identities below were VERIFIED against a live environment (installed plugin
|
|
19
|
+
* records, marketplace sources, CLI/npm metadata) - never guessed. Anything that
|
|
20
|
+
* could not be verified is marked `unresolved` and reported as such.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Capability states (contract §21.1). */
|
|
24
|
+
export const CAPABILITY_STATES = [
|
|
25
|
+
"available-and-ready", "installed-but-disabled", "installed-but-incompatible",
|
|
26
|
+
"installed-but-unhealthy", "missing", "marketplace-unavailable", "policy-blocked",
|
|
27
|
+
"authentication-required", "optional-and-absent", "unknown",
|
|
28
|
+
];
|
|
29
|
+
export const READY_STATES = new Set(["available-and-ready"]);
|
|
30
|
+
|
|
31
|
+
/** How a provider is delivered - determines detection and installation. */
|
|
32
|
+
export const DELIVERY = { CLAUDE_PLUGIN: "claude-plugin", AGENT_SKILL: "agent-skill", CLI: "cli" };
|
|
33
|
+
|
|
34
|
+
/** Failure classes every adapter maps its errors onto. */
|
|
35
|
+
export const FAILURE_CLASSES = [
|
|
36
|
+
"not-installed", "disabled", "incompatible-version", "misconfigured",
|
|
37
|
+
"unauthenticated", "invocation-error", "partial-output", "malformed-output",
|
|
38
|
+
"timeout", "policy-blocked",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The seven mandatory provider integrations. Each entry carries the complete
|
|
43
|
+
* 15-element adapter contract. `identity` is the verified canonical identity.
|
|
44
|
+
*/
|
|
45
|
+
export const PROVIDERS = [
|
|
46
|
+
{
|
|
47
|
+
id: "superpowers",
|
|
48
|
+
title: "Superpowers",
|
|
49
|
+
// 1. canonical provider identity (VERIFIED)
|
|
50
|
+
identity: { delivery: DELIVERY.CLAUDE_PLUGIN, plugin: "superpowers", marketplace: "claude-plugins-official", ref: "superpowers@claude-plugins-official" },
|
|
51
|
+
// 2. ownership boundary
|
|
52
|
+
ownership: "Externally owned by the claude-plugins-official marketplace. Superdev routes to it and consumes its outputs; it never reproduces its brainstorming, planning, TDD, debugging, review, or finishing methodology.",
|
|
53
|
+
// 3. applicable intents
|
|
54
|
+
intents: ["brainstorm", "plan", "implement-tdd", "debug", "review", "finish"],
|
|
55
|
+
// 4. detection mechanism (read-only)
|
|
56
|
+
detection: { kind: "claude-plugin-record", key: "superpowers@claude-plugins-official" },
|
|
57
|
+
// 5. readiness probe (non-destructive)
|
|
58
|
+
readiness: { kind: "skill-namespace", expect: "superpowers:", note: "the loaded session exposes superpowers:* skills" },
|
|
59
|
+
// 6. installation source
|
|
60
|
+
install: { marketplaceSource: { source: "github", repo: "anthropics/claude-plugins-official" }, command: "claude plugin install superpowers@claude-plugins-official", verified: true },
|
|
61
|
+
// 7. consent requirements
|
|
62
|
+
consent: { required: true, class: "install-plugin", prompt: "Install the Superpowers plugin from the claude-plugins-official marketplace?" },
|
|
63
|
+
// 8. invocation contract
|
|
64
|
+
invocation: { kind: "skill", pattern: "superpowers:<skill>", examples: ["superpowers:brainstorming", "superpowers:systematic-debugging", "superpowers:test-driven-development"], superdevNeverInlines: true },
|
|
65
|
+
// 9. structured context packet (bounded - never the whole record)
|
|
66
|
+
contextPacket: ["objective", "acceptedConstraints", "relevantDecisionIds", "openOwnerQuestionIds", "riskTier", "requiredRecordOutputs"],
|
|
67
|
+
// 10. expected output / evidence contract
|
|
68
|
+
output: { form: "narrative + artifacts authored into talks/", evidence: ["providerInvoked", "providerIdentity", "artifactPaths"], mustBeTraceable: true },
|
|
69
|
+
// 11. failure classification
|
|
70
|
+
failures: ["not-installed", "disabled", "incompatible-version", "invocation-error", "partial-output"],
|
|
71
|
+
// 12. unavailable-provider behavior
|
|
72
|
+
unavailable: "State plainly that Superpowers was unavailable, run Superdev's OWN gates (scope contract, spec depth, completion evidence), and record that the specialist methodology was not applied.",
|
|
73
|
+
// 13. no-substitution behavior
|
|
74
|
+
noSubstitution: "Superdev must not present its own gates as Superpowers' methodology, and must not claim brainstorming/TDD/debugging discipline it did not obtain from the provider.",
|
|
75
|
+
// 14. privacy and secret boundary
|
|
76
|
+
privacy: { sends: "bounded context packet only", never: ["raw talks/ tree", "secrets", "PII", "model-private reasoning"], screened: true },
|
|
77
|
+
// 15. tests - see tests/integration/provider-contracts.test.mjs
|
|
78
|
+
testsRef: "tests/integration/provider-contracts.test.mjs",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "claude-mem",
|
|
82
|
+
title: "Claude Mem",
|
|
83
|
+
identity: { delivery: DELIVERY.CLAUDE_PLUGIN, plugin: "claude-mem", marketplace: "thedotmack", ref: "claude-mem@thedotmack" },
|
|
84
|
+
ownership: "Externally owned (github thedotmack/claude-mem). A recall/search CACHE - never project authority.",
|
|
85
|
+
intents: ["recall", "search-prior-work"],
|
|
86
|
+
detection: { kind: "claude-plugin-record", key: "claude-mem@thedotmack" },
|
|
87
|
+
readiness: { kind: "skill-namespace", expect: "claude-mem:", note: "the loaded session exposes claude-mem:* skills" },
|
|
88
|
+
install: { marketplaceSource: { source: "github", repo: "thedotmack/claude-mem" }, command: "claude plugin install claude-mem@thedotmack", verified: true },
|
|
89
|
+
consent: { required: true, class: "install-plugin", prompt: "Install the Claude Mem plugin from thedotmack/claude-mem?" },
|
|
90
|
+
invocation: { kind: "skill", pattern: "claude-mem:<skill>", examples: ["claude-mem:mem-search"], superdevNeverInlines: true },
|
|
91
|
+
contextPacket: ["narrowQuery", "timeWindow", "projectScope"],
|
|
92
|
+
output: { form: "recalled observations", evidence: ["providerInvoked", "recallIds", "verificationStatus"], mustBeTraceable: true },
|
|
93
|
+
failures: ["not-installed", "disabled", "invocation-error", "malformed-output", "partial-output"],
|
|
94
|
+
unavailable: "Proceed from talks/ state and session summaries alone, and say that cross-session recall was unavailable.",
|
|
95
|
+
// The critical authority rule for this provider.
|
|
96
|
+
noSubstitution: "Recall is NEVER authority. Every recalled fact must be verified against current artifacts before consequential use, and recall-sourced claims must be labelled with their verification status.",
|
|
97
|
+
privacy: { sends: "narrow query only", never: ["secrets", "PII", "whole-record dumps"], screened: true },
|
|
98
|
+
testsRef: "tests/integration/provider-contracts.test.mjs",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "frontend-design",
|
|
102
|
+
title: "Frontend Design",
|
|
103
|
+
identity: { delivery: DELIVERY.CLAUDE_PLUGIN, plugin: "frontend-design", marketplace: "claude-plugins-official", ref: "frontend-design@claude-plugins-official" },
|
|
104
|
+
ownership: "Externally owned by the claude-plugins-official marketplace. Superdev never reproduces its design methodology or aesthetic system.",
|
|
105
|
+
intents: ["frontend-direction", "component-implementation", "visual-design"],
|
|
106
|
+
detection: { kind: "claude-plugin-record", key: "frontend-design@claude-plugins-official" },
|
|
107
|
+
readiness: { kind: "skill-namespace", expect: "frontend-design:", note: "the loaded session exposes frontend-design:* skills" },
|
|
108
|
+
install: { marketplaceSource: { source: "github", repo: "anthropics/claude-plugins-official" }, command: "claude plugin install frontend-design@claude-plugins-official", verified: true },
|
|
109
|
+
consent: { required: true, class: "install-plugin", prompt: "Install the Frontend Design plugin from the claude-plugins-official marketplace?" },
|
|
110
|
+
invocation: { kind: "skill", pattern: "frontend-design:<skill>", examples: ["frontend-design:frontend-design"], superdevNeverInlines: true },
|
|
111
|
+
contextPacket: ["productContext", "userGoals", "designConstraints", "designSystemEvidence"],
|
|
112
|
+
output: { form: "design direction + implementation", evidence: ["providerInvoked", "artifactPaths"], mustBeTraceable: true },
|
|
113
|
+
failures: ["not-installed", "disabled", "incompatible-version", "invocation-error", "partial-output"],
|
|
114
|
+
unavailable: "Report that the design specialist was unavailable; implement only what the accepted design system already specifies, and do not invent visual direction.",
|
|
115
|
+
noSubstitution: "Superdev must not generate design direction and present it as the provider's, nor imitate its aesthetic methodology.",
|
|
116
|
+
privacy: { sends: "bounded design context", never: ["secrets", "PII", "customer data"], screened: true },
|
|
117
|
+
// Version was 'unknown' in the verified local install record - identity is
|
|
118
|
+
// confirmed, the version constraint is not. Reported honestly, never guessed.
|
|
119
|
+
versionResolution: "unresolved",
|
|
120
|
+
testsRef: "tests/integration/provider-contracts.test.mjs",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "impeccable",
|
|
124
|
+
title: "Impeccable",
|
|
125
|
+
identity: { delivery: DELIVERY.CLAUDE_PLUGIN, plugin: "impeccable", marketplace: "impeccable", ref: "impeccable@impeccable" },
|
|
126
|
+
ownership: "Externally owned (github pbakaus/impeccable). Superdev never reproduces its UI critique/refinement methodology.",
|
|
127
|
+
intents: ["ui-audit", "ui-polish", "design-critique", "accessibility-review"],
|
|
128
|
+
detection: { kind: "claude-plugin-record", key: "impeccable@impeccable" },
|
|
129
|
+
readiness: { kind: "skill-namespace", expect: "impeccable:", note: "the loaded session exposes impeccable:* skills" },
|
|
130
|
+
install: { marketplaceSource: { source: "github", repo: "pbakaus/impeccable" }, command: "claude plugin install impeccable@impeccable", verified: true },
|
|
131
|
+
consent: { required: true, class: "install-plugin", prompt: "Install the Impeccable plugin from pbakaus/impeccable?" },
|
|
132
|
+
invocation: { kind: "skill", pattern: "impeccable:<skill>", examples: ["impeccable:impeccable"], superdevNeverInlines: true },
|
|
133
|
+
contextPacket: ["surfaceUnderReview", "designSystemEvidence", "accessibilityRequirements", "knownConstraints"],
|
|
134
|
+
output: { form: "critique + concrete refinements", evidence: ["providerInvoked", "surfaceRefs"], mustBeTraceable: true },
|
|
135
|
+
failures: ["not-installed", "disabled", "incompatible-version", "invocation-error", "partial-output"],
|
|
136
|
+
unavailable: "Report that the UI specialist was unavailable and limit work to the accepted design system; do not improvise a critique framework.",
|
|
137
|
+
noSubstitution: "Superdev must not produce a UI critique and attribute it to Impeccable, nor imitate its refinement methodology.",
|
|
138
|
+
privacy: { sends: "surface + design constraints", never: ["secrets", "PII", "user data in screenshots"], screened: true },
|
|
139
|
+
testsRef: "tests/integration/provider-contracts.test.mjs",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "find-skills",
|
|
143
|
+
title: "Find Skills / skills.sh",
|
|
144
|
+
// Delivered as an agent skill; the ecosystem CLI is `skills` on npm.
|
|
145
|
+
identity: { delivery: DELIVERY.AGENT_SKILL, skill: "find-skills", cli: "skills", npm: "skills", ref: "find-skills (agent skill) + skills CLI" },
|
|
146
|
+
ownership: "Externally owned (the open agent skills ecosystem / skills.sh). Superdev never reimplements skill discovery or the package manager.",
|
|
147
|
+
intents: ["discover-capability", "install-skill"],
|
|
148
|
+
// COMPOSITE: the discovery skill and the ecosystem CLI are BOTH required -
|
|
149
|
+
// the skill without the CLI cannot install anything.
|
|
150
|
+
components: [
|
|
151
|
+
{ id: "skill", kind: "agent-skill", skill: "find-skills", required: true },
|
|
152
|
+
{ id: "cli", kind: "cli", cli: "skills", versionArgs: ["--version"], required: true },
|
|
153
|
+
],
|
|
154
|
+
detection: { kind: "composite", skill: "find-skills", alsoCli: "skills" },
|
|
155
|
+
readiness: { kind: "cli-version", cli: "skills", args: ["--version"], note: "npx skills --version; the CLI is the ecosystem package manager" },
|
|
156
|
+
install: { npm: "skills", command: "npx skills add <skill>", verified: true, note: "skill discovery/installation is the provider's own flow" },
|
|
157
|
+
// The installation-flags rule is a hard safety boundary for this provider.
|
|
158
|
+
consent: { required: true, class: "install-skill", prompt: "Install a skill through the skills CLI?", forbiddenFlags: ["--all", "-y", "--yes"] },
|
|
159
|
+
invocation: { kind: "skill", pattern: "find-skills", examples: ["find-skills"], superdevNeverInlines: true },
|
|
160
|
+
contextPacket: ["capabilityGap", "intent", "constraints"],
|
|
161
|
+
output: { form: "candidate skills + install plan", evidence: ["providerInvoked", "candidateIds"], mustBeTraceable: true },
|
|
162
|
+
failures: ["not-installed", "misconfigured", "invocation-error", "malformed-output", "partial-output"],
|
|
163
|
+
unavailable: "State that skill discovery was unavailable and proceed with existing capabilities only; never fabricate a catalogue of skills.",
|
|
164
|
+
noSubstitution: "Superdev must not present its own guesses as ecosystem search results, and must never install with --all or a silent -y.",
|
|
165
|
+
privacy: { sends: "capability gap description", never: ["secrets", "PII", "proprietary code"], screened: true },
|
|
166
|
+
testsRef: "tests/integration/provider-contracts.test.mjs",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "task-observer",
|
|
170
|
+
title: "Task Observer",
|
|
171
|
+
identity: { delivery: DELIVERY.AGENT_SKILL, skill: "task-observer", repo: "rebelytics/one-skill-to-rule-them-all", ref: "task-observer (agent skill)" },
|
|
172
|
+
ownership: "Externally owned (github rebelytics/one-skill-to-rule-them-all). Superdev never reproduces its observation taxonomy or skill-extraction methodology.",
|
|
173
|
+
intents: ["observe-session", "capture-skill-opportunity"],
|
|
174
|
+
detection: { kind: "agent-skill", skill: "task-observer" },
|
|
175
|
+
readiness: { kind: "agent-skill-files", skill: "task-observer", expectFiles: ["SKILL.md"] },
|
|
176
|
+
install: { source: { source: "github", repo: "rebelytics/one-skill-to-rule-them-all" }, command: "npx skills add task-observer", verified: true },
|
|
177
|
+
consent: { required: true, class: "install-skill", prompt: "Install the Task Observer skill?", forbiddenFlags: ["--all", "-y", "--yes"] },
|
|
178
|
+
invocation: { kind: "skill", pattern: "task-observer", examples: ["task-observer"], superdevNeverInlines: true },
|
|
179
|
+
contextPacket: ["sessionObjective", "toolUsagePattern", "userCorrections"],
|
|
180
|
+
output: { form: "observation log entries", evidence: ["providerInvoked", "observationIds"], mustBeTraceable: true },
|
|
181
|
+
failures: ["not-installed", "misconfigured", "invocation-error", "partial-output", "malformed-output"],
|
|
182
|
+
unavailable: "Record session outcomes through Superdev's own session summaries and state that observation capture was unavailable.",
|
|
183
|
+
noSubstitution: "Superdev must not label its own session summary as Task Observer output or imitate its skill-extraction taxonomy.",
|
|
184
|
+
privacy: { sends: "session outcome signals", never: ["secrets", "PII", "model-private reasoning"], screened: true },
|
|
185
|
+
testsRef: "tests/integration/provider-contracts.test.mjs",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "envx",
|
|
189
|
+
title: "envx",
|
|
190
|
+
identity: { delivery: DELIVERY.CLI, cli: "envx", npm: "envx-cli", skill: "envx", ref: "envx-cli (CLI) + envx (agent skill)" },
|
|
191
|
+
ownership: "Externally owned (envx-cli). Superdev never reimplements secret encryption, decryption, or injection.",
|
|
192
|
+
intents: ["secret-management", "env-injection"],
|
|
193
|
+
// COMPOSITE: the CLI does the work, the skill carries the safe-usage
|
|
194
|
+
// contract. Either alone is only partially ready.
|
|
195
|
+
components: [
|
|
196
|
+
{ id: "cli", kind: "cli", cli: "envx", versionArgs: ["--version"], required: true },
|
|
197
|
+
{ id: "skill", kind: "agent-skill", skill: "envx", required: true },
|
|
198
|
+
],
|
|
199
|
+
detection: { kind: "composite", cli: "envx", alsoSkill: "envx" },
|
|
200
|
+
readiness: { kind: "cli-version", cli: "envx", args: ["--version"] },
|
|
201
|
+
install: { npm: "envx-cli", command: "npm install -g envx-cli", verified: true, note: "the skill installs via `envx skill add`" },
|
|
202
|
+
consent: { required: true, class: "install-cli", prompt: "Install the envx CLI (envx-cli) globally?" },
|
|
203
|
+
invocation: { kind: "cli", pattern: "envx <command>", examples: ["envx run", "envx skill add"], superdevNeverInlines: true },
|
|
204
|
+
// The privacy boundary is the whole point of this provider.
|
|
205
|
+
contextPacket: ["stageName", "requiredVariableNames"],
|
|
206
|
+
output: { form: "marker presence / exit status only", evidence: ["providerInvoked", "stage"], mustBeTraceable: true },
|
|
207
|
+
failures: ["not-installed", "misconfigured", "unauthenticated", "invocation-error", "policy-blocked"],
|
|
208
|
+
unavailable: "Report that envx was unavailable and refuse to improvise secret handling; never read, print, or copy a decrypted secret.",
|
|
209
|
+
noSubstitution: "Superdev must NEVER decrypt, read, echo, or re-implement secret handling itself. It detects marker presence only.",
|
|
210
|
+
privacy: { sends: "variable NAMES only", never: ["secret values", "decrypted content", "passphrases"], screened: true, readsSecretValues: false },
|
|
211
|
+
testsRef: "tests/integration/provider-contracts.test.mjs",
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
/** Version policy, stated explicitly rather than implied.
|
|
216
|
+
* No provider declares a minimum version today: Superdev integrates through
|
|
217
|
+
* stable, documented surfaces (skill namespaces, CLI subcommands) and has no
|
|
218
|
+
* verified evidence of a breaking floor. `minVersion` is therefore absent, and
|
|
219
|
+
* detection reports versions WITHOUT claiming compatibility validation. Adding
|
|
220
|
+
* a `minVersion` to a registry entry activates real enforcement (tested). */
|
|
221
|
+
export const VERSION_POLICY = {
|
|
222
|
+
enforced: false,
|
|
223
|
+
statement: "No minimum version is asserted for any provider; detection reports the observed version and never implies a compatibility check that was not performed. Set `minVersion` on a registry entry to enforce one.",
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const PROVIDER_IDS = PROVIDERS.map((p) => p.id);
|
|
227
|
+
export function getProvider(id) {
|
|
228
|
+
return PROVIDERS.find((p) => p.id === id) ?? null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The 15 contract elements every provider entry must define. */
|
|
232
|
+
export const CONTRACT_ELEMENTS = [
|
|
233
|
+
"identity", "ownership", "intents", "detection", "readiness", "install", "consent",
|
|
234
|
+
"invocation", "contextPacket", "output", "failures", "unavailable", "noSubstitution",
|
|
235
|
+
"privacy", "testsRef",
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
/** Which Superdev skill routes each intent to a provider (proactive routing). */
|
|
239
|
+
export const INTENT_ROUTING = {
|
|
240
|
+
brainstorm: "superpowers", plan: "superpowers", "implement-tdd": "superpowers",
|
|
241
|
+
debug: "superpowers", review: "superpowers", finish: "superpowers",
|
|
242
|
+
recall: "claude-mem", "search-prior-work": "claude-mem",
|
|
243
|
+
"frontend-direction": "frontend-design", "component-implementation": "frontend-design", "visual-design": "frontend-design",
|
|
244
|
+
"ui-audit": "impeccable", "ui-polish": "impeccable", "design-critique": "impeccable", "accessibility-review": "impeccable",
|
|
245
|
+
"discover-capability": "find-skills", "install-skill": "find-skills",
|
|
246
|
+
"observe-session": "task-observer", "capture-skill-opportunity": "task-observer",
|
|
247
|
+
"secret-management": "envx", "env-injection": "envx",
|
|
248
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Every path the published package's own code reaches must be inside the
|
|
2
|
+
// published package.
|
|
3
|
+
//
|
|
4
|
+
// scripts/providers/detect.mjs was imported by src/cli.mjs and src/init/index.mjs
|
|
5
|
+
// and was not listed in package.json's files array. So on every npm install of
|
|
6
|
+
// superdev-cli, provider readiness reported "could not be determined: Cannot find
|
|
7
|
+
// module", the init skill's step about checking readiness before relying on it
|
|
8
|
+
// could never succeed, and doctor printed Pass beside the failure. It worked
|
|
9
|
+
// everywhere except where users install it, because the plugin copy carries the
|
|
10
|
+
// whole repository and the npm package carries an allowlist.
|
|
11
|
+
//
|
|
12
|
+
// Nothing caught it because nothing compared what the code imports against what
|
|
13
|
+
// the package ships. The files array is edited by hand, and a missing entry is
|
|
14
|
+
// invisible locally and fatal remotely, which is the exact shape of defect a
|
|
15
|
+
// validator is for.
|
|
16
|
+
|
|
17
|
+
import { join, posix, relative, sep } from "node:path";
|
|
18
|
+
|
|
19
|
+
import { ERROR, WARNING, finding, importSpecifiers, isDirectory, readJson, readText, walk } from "./common.mjs";
|
|
20
|
+
|
|
21
|
+
export const name = "packaging";
|
|
22
|
+
|
|
23
|
+
const SCANNED_ROOTS = ["src", "scripts", "hooks"];
|
|
24
|
+
|
|
25
|
+
const slash = (path) => path.split(sep).join("/");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Would `npm pack` include this path, given the files array?
|
|
29
|
+
*
|
|
30
|
+
* Deliberately stricter than npm's own matching. A false finding costs somebody
|
|
31
|
+
* a reading; a missed one costs a release that fails in a stranger's terminal.
|
|
32
|
+
*/
|
|
33
|
+
export function packaged(patterns, path) {
|
|
34
|
+
// npm includes these whatever the files array says.
|
|
35
|
+
if (path === "package.json" || /^(README|LICENSE|LICENCE|NOTICE|CHANGELOG)/i.test(path)) return true;
|
|
36
|
+
|
|
37
|
+
let included = false;
|
|
38
|
+
for (const raw of patterns) {
|
|
39
|
+
const negated = raw.startsWith("!");
|
|
40
|
+
const pattern = negated ? raw.slice(1) : raw;
|
|
41
|
+
if (matches(pattern, path)) included = !negated;
|
|
42
|
+
}
|
|
43
|
+
return included;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
|
|
48
|
+
function matches(pattern, path) {
|
|
49
|
+
if (pattern.endsWith("/")) return path.startsWith(pattern);
|
|
50
|
+
if (pattern.includes("*")) {
|
|
51
|
+
const source = pattern
|
|
52
|
+
.split("**").map((part) => part.split("*").map(escape).join("[^/]*"))
|
|
53
|
+
.join(".*");
|
|
54
|
+
return new RegExp(`^${source}$`).test(path);
|
|
55
|
+
}
|
|
56
|
+
return path === pattern || path.startsWith(`${pattern}/`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function run(root) {
|
|
60
|
+
const findings = [];
|
|
61
|
+
const read = readJson(join(root, "package.json"));
|
|
62
|
+
if (read.error) {
|
|
63
|
+
findings.push(finding("PK-000", ERROR, "package.json", `cannot be read: ${read.error}`));
|
|
64
|
+
return { name, findings };
|
|
65
|
+
}
|
|
66
|
+
const manifest = read.value;
|
|
67
|
+
const patterns = manifest?.files;
|
|
68
|
+
|
|
69
|
+
if (!Array.isArray(patterns) || !patterns.length) {
|
|
70
|
+
findings.push(finding("PK-000", ERROR, "package.json",
|
|
71
|
+
"there is no files array, so the package would ship whatever happens to be in the directory"));
|
|
72
|
+
return { name, findings };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The bin entry has to be in the package before anything else matters.
|
|
76
|
+
const bin = typeof manifest.bin === "string" ? manifest.bin : Object.values(manifest.bin ?? {})[0];
|
|
77
|
+
if (bin) {
|
|
78
|
+
const target = posix.normalize(String(bin).replace(/^\.\//, ""));
|
|
79
|
+
if (!packaged(patterns, target)) {
|
|
80
|
+
findings.push(finding("PK-001", ERROR, "package.json",
|
|
81
|
+
`bin points at ${target}, which the files array does not include, so the installed command would not exist`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const files = SCANNED_ROOTS
|
|
86
|
+
.filter((dir) => isDirectory(join(root, dir)))
|
|
87
|
+
.flatMap((dir) => walk(join(root, dir)))
|
|
88
|
+
.filter((file) => file.endsWith(".mjs") || file.endsWith(".js"))
|
|
89
|
+
.map((file) => slash(relative(root, file)))
|
|
90
|
+
// Only shipped code can fail at runtime in somebody's install. A script that
|
|
91
|
+
// is deliberately repository-only may import whatever it likes.
|
|
92
|
+
.filter((path) => packaged(patterns, path) && !path.endsWith(".test.mjs"));
|
|
93
|
+
|
|
94
|
+
for (const path of files) {
|
|
95
|
+
const from = posix.dirname(path);
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const spec of importSpecifiers(readText(join(root, path)))) {
|
|
98
|
+
if (!spec.startsWith("./") && !spec.startsWith("../")) continue;
|
|
99
|
+
const target = posix.normalize(posix.join(from, spec));
|
|
100
|
+
if (seen.has(target)) continue;
|
|
101
|
+
seen.add(target);
|
|
102
|
+
|
|
103
|
+
if (!/\.(mjs|js|json|css|md)$/.test(target)) {
|
|
104
|
+
findings.push(finding("PK-003", WARNING, path,
|
|
105
|
+
`imports ${spec}, which names no file extension, so whether the package ships it cannot be decided here`));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!packaged(patterns, target)) {
|
|
109
|
+
findings.push(finding("PK-002", ERROR, path,
|
|
110
|
+
`imports ${target}, which package.json files does not include, so the published package cannot resolve it`));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { name, findings };
|
|
116
|
+
}
|
|
@@ -30,11 +30,12 @@ import * as dataModel from "./data-model.mjs";
|
|
|
30
30
|
import * as recordLinks from "./record-links.mjs";
|
|
31
31
|
import * as skillCommands from "./skill-commands.mjs";
|
|
32
32
|
import * as specification from "./specification.mjs";
|
|
33
|
+
import * as packaging from "./packaging.mjs";
|
|
33
34
|
|
|
34
35
|
/** Declaration order is report order, so two runs read the same way. */
|
|
35
36
|
export const VALIDATORS = [
|
|
36
37
|
manifests, skills, docsTemplates, migrations, markdown,
|
|
37
|
-
style, privacy, imports, dependencies, noTests, footprint, dataModel, recordLinks, skillCommands, specification,
|
|
38
|
+
style, privacy, imports, dependencies, noTests, footprint, dataModel, recordLinks, skillCommands, specification, packaging,
|
|
38
39
|
];
|
|
39
40
|
|
|
40
41
|
const USAGE = `Usage: node validate-all.mjs [--root <path>] [--only <name,...>] [--json] [--out <file>] [--help]
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Exit codes: 0 detection completed (including "none"), 2 usage error.
|
|
8
8
|
*/
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
10
11
|
import fs from "node:fs";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { pathToFileURL } from "node:url";
|
|
@@ -90,6 +91,58 @@ function dirNames(dir) {
|
|
|
90
91
|
.map((e) => e.name);
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Markdown under a directory that Git is not ignoring, at most three levels down.
|
|
96
|
+
*
|
|
97
|
+
* The bare existence of a subdirectory used to count as documentation. A real
|
|
98
|
+
* repository had `docs/` holding nothing but `.DS_Store` and a git-ignored
|
|
99
|
+
* `docs/client-shared/` of material the client had sent in. That is input to a
|
|
100
|
+
* project, not a projection of one, and detection called it "documentation
|
|
101
|
+
* present matching no known profile" and routed a brand new repository to adopt,
|
|
102
|
+
* which refuses to initialize.
|
|
103
|
+
*
|
|
104
|
+
* Git's own ignore list is the right authority for "is this part of the
|
|
105
|
+
* repository": it is the file the user already maintains to say so. Without Git
|
|
106
|
+
* the scan simply keeps everything, which is the old behaviour minus the
|
|
107
|
+
* empty-directory case.
|
|
108
|
+
*/
|
|
109
|
+
function trackedMarkdown(root, dir) {
|
|
110
|
+
const found = markdownUnder(dir, 3);
|
|
111
|
+
const ignored = ignoredByGit(root, found);
|
|
112
|
+
return found.filter((file) => !ignored.has(file));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function markdownUnder(dir, depth) {
|
|
116
|
+
if (depth < 0 || !fs.existsSync(dir)) return [];
|
|
117
|
+
const found = [];
|
|
118
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
119
|
+
if (entry.name.startsWith(".")) continue;
|
|
120
|
+
const full = path.join(dir, entry.name);
|
|
121
|
+
if (entry.isDirectory()) found.push(...markdownUnder(full, depth - 1));
|
|
122
|
+
else if (entry.name.endsWith(".md")) found.push(full);
|
|
123
|
+
}
|
|
124
|
+
return found;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** One `git check-ignore` for the whole list: a call per file is a call per file. */
|
|
128
|
+
function ignoredByGit(root, files) {
|
|
129
|
+
if (!files.length || !fs.existsSync(path.join(root, ".git"))) return new Set();
|
|
130
|
+
try {
|
|
131
|
+
const out = execFileSync("git", ["-C", root, "check-ignore", "--stdin"], {
|
|
132
|
+
input: files.join("\n"),
|
|
133
|
+
encoding: "utf8",
|
|
134
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
135
|
+
timeout: 10000,
|
|
136
|
+
});
|
|
137
|
+
return new Set(out.split(/\r?\n/).filter(Boolean).map((line) => path.resolve(root, line)));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// Exit 1 means nothing matched, which is an answer, not a failure. Anything
|
|
140
|
+
// else means git could not tell us, and then nothing is treated as ignored.
|
|
141
|
+
if (error?.status === 1) return new Set();
|
|
142
|
+
return new Set();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
93
146
|
const TYPE_FOLDERS = ["features", "apis", "api", "data", "database", "workflows", "schemas"];
|
|
94
147
|
const DOCS_ROOT_CANDIDATES = ["docs/project", "docs", "documentation"];
|
|
95
148
|
|
|
@@ -143,8 +196,9 @@ export function detectProfile(root) {
|
|
|
143
196
|
return { profile: "legacy-flat-docs", confidence: "medium", source: "structure", evidence, docsRoot: candidate };
|
|
144
197
|
}
|
|
145
198
|
|
|
146
|
-
|
|
147
|
-
|
|
199
|
+
// Markdown Git is keeping, not a directory that happens to be here.
|
|
200
|
+
const mdFiles = trackedMarkdown(root, docsRoot);
|
|
201
|
+
if (mdFiles.length) {
|
|
148
202
|
evidence.push({ what: "documentation present matching no known profile", where: candidate });
|
|
149
203
|
return { profile: "custom", confidence: "low", source: "structure", evidence, docsRoot: candidate };
|
|
150
204
|
}
|
package/src/cli/product-map.mjs
CHANGED
|
@@ -163,9 +163,13 @@ export async function milestoneShow(root, id) {
|
|
|
163
163
|
const { milestone, features } = found;
|
|
164
164
|
// Both shapes are read: a plain string from before conditions could be
|
|
165
165
|
// judged, and the object a judged condition is stored as now.
|
|
166
|
-
const
|
|
167
|
-
|
|
166
|
+
const asCondition = (c) => (typeof c === "string" ? { condition: c, met: false, reading: null, check: null } : c);
|
|
167
|
+
const exits = json(milestone.exit_conditions_json, []).map(asCondition);
|
|
168
|
+
// Entry conditions were stored and never shown, so a milestone could be
|
|
169
|
+
// blocked from starting by something the reader had no way to see.
|
|
170
|
+
const entries = json(milestone.entry_conditions_json, []).map(asCondition);
|
|
168
171
|
const met = exits.filter((c) => c.met).length;
|
|
172
|
+
const entryMet = entries.filter((c) => c.met).length;
|
|
169
173
|
const done = features.filter((f) => ["complete", "delivered", "implemented"].includes(f.status)).length;
|
|
170
174
|
return {
|
|
171
175
|
data: found,
|
|
@@ -181,6 +185,10 @@ export async function milestoneShow(root, id) {
|
|
|
181
185
|
// that decided it, and was being printed straight into a bullet, which
|
|
182
186
|
// rendered twenty five of them as [object Object]. The control centre
|
|
183
187
|
// normalises the same shape; the command line was never taught to.
|
|
188
|
+
entries.length
|
|
189
|
+
? R.block(`Entry conditions (${entryMet} of ${entries.length} met)`,
|
|
190
|
+
entries.map((c) => ` [${c.met ? "met" : "not met"}] ${c.condition}`).join("\n"))
|
|
191
|
+
: null,
|
|
184
192
|
R.block(`Exit conditions (${met} of ${exits.length} met)`, exits.length
|
|
185
193
|
? exits.map((c) => R.stitch([
|
|
186
194
|
` [${c.met ? "met" : "not met"}] ${c.condition}`,
|
package/src/cli/render.mjs
CHANGED
|
@@ -443,15 +443,23 @@ export function renderProposals(report) {
|
|
|
443
443
|
* in one line, so the failing one is findable without reading the rest.
|
|
444
444
|
*/
|
|
445
445
|
export function renderDoctor({ checks = [], findings = [] } = {}) {
|
|
446
|
-
|
|
447
|
-
|
|
446
|
+
// Three verdicts, because a check that threw is neither. Pass beside "could
|
|
447
|
+
// not be determined" was a real defect: it is the answer a reader trusts least
|
|
448
|
+
// once they notice it, and most when they do not.
|
|
449
|
+
const verdict = (ok) => (ok === true ? "Pass" : ok === false ? "Problem" : "Unknown");
|
|
450
|
+
const rows = checks.map((c) => [c.name, verdict(c.ok), c.detail ?? ""]);
|
|
451
|
+
const bad = checks.filter((c) => c.ok === false).length;
|
|
452
|
+
const unknown = checks.filter((c) => c.ok !== true && c.ok !== false).length;
|
|
448
453
|
return stitch([
|
|
449
454
|
heading("Doctor"),
|
|
450
455
|
table(["Check", "Verdict", "Detail"], rows),
|
|
451
456
|
"",
|
|
452
457
|
bad
|
|
453
458
|
? `${plural(bad, "check")} found a problem.`
|
|
454
|
-
:
|
|
459
|
+
: unknown
|
|
460
|
+
? "No check found a problem."
|
|
461
|
+
: "Every check passed.",
|
|
462
|
+
unknown ? `${plural(unknown, "check")} could not determine an answer, so nothing here vouches for it.` : null,
|
|
455
463
|
findings.length ? "" : null,
|
|
456
464
|
findings.length ? block(`Findings (${findings.length})`, findingList(findings)) : null,
|
|
457
465
|
]);
|