superdev-cli 0.1.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.
Files changed (182) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +21 -0
  3. package/.codex-plugin/plugin.json +27 -0
  4. package/CODE_OF_CONDUCT.md +109 -0
  5. package/CONTRIBUTING.md +205 -0
  6. package/LICENSE +202 -0
  7. package/NOTICE +11 -0
  8. package/README.md +1051 -0
  9. package/SECURITY.md +135 -0
  10. package/THIRD-PARTY-NOTICES.md +253 -0
  11. package/hooks/hooks.json +61 -0
  12. package/hooks/run.mjs +88 -0
  13. package/package.json +65 -0
  14. package/references/confidentiality.md +48 -0
  15. package/references/evidence-and-risk.md +73 -0
  16. package/references/operating-model.md +105 -0
  17. package/references/platform-capabilities.md +51 -0
  18. package/references/project-record.md +91 -0
  19. package/references/provider-contracts.md +102 -0
  20. package/scripts/doctor/doctor.mjs +539 -0
  21. package/scripts/privacy/scan-history.mjs +163 -0
  22. package/scripts/privacy/scan.mjs +368 -0
  23. package/scripts/validate/README.md +94 -0
  24. package/scripts/validate/common.mjs +149 -0
  25. package/scripts/validate/data-model.mjs +225 -0
  26. package/scripts/validate/dependencies.mjs +69 -0
  27. package/scripts/validate/docs-templates.mjs +99 -0
  28. package/scripts/validate/footprint.mjs +78 -0
  29. package/scripts/validate/imports.mjs +61 -0
  30. package/scripts/validate/manifests.mjs +173 -0
  31. package/scripts/validate/markdown.mjs +123 -0
  32. package/scripts/validate/migrations.mjs +187 -0
  33. package/scripts/validate/no-tests.mjs +136 -0
  34. package/scripts/validate/privacy.mjs +115 -0
  35. package/scripts/validate/record-links.mjs +127 -0
  36. package/scripts/validate/skill-commands.mjs +190 -0
  37. package/scripts/validate/skills.mjs +112 -0
  38. package/scripts/validate/specification.mjs +88 -0
  39. package/scripts/validate/style.mjs +78 -0
  40. package/scripts/validate/validate-all.mjs +175 -0
  41. package/skills/debug/SKILL.md +68 -0
  42. package/skills/decision/SKILL.md +99 -0
  43. package/skills/docs/SKILL.md +86 -0
  44. package/skills/docs/assets/fragments/api/events.md +14 -0
  45. package/skills/docs/assets/fragments/api/graphql.md +14 -0
  46. package/skills/docs/assets/fragments/api/local-only.md +13 -0
  47. package/skills/docs/assets/fragments/api/rest.md +13 -0
  48. package/skills/docs/assets/fragments/api/rpc.md +13 -0
  49. package/skills/docs/assets/fragments/async/event-bus.md +13 -0
  50. package/skills/docs/assets/fragments/async/none.md +11 -0
  51. package/skills/docs/assets/fragments/async/platform-jobs.md +13 -0
  52. package/skills/docs/assets/fragments/async/queue.md +13 -0
  53. package/skills/docs/assets/fragments/async/scheduler.md +13 -0
  54. package/skills/docs/assets/fragments/auth/detected-provider.md +15 -0
  55. package/skills/docs/assets/fragments/auth/neutral-base.md +14 -0
  56. package/skills/docs/assets/fragments/data/document.md +13 -0
  57. package/skills/docs/assets/fragments/data/external-saas.md +13 -0
  58. package/skills/docs/assets/fragments/data/key-value.md +13 -0
  59. package/skills/docs/assets/fragments/data/none.md +12 -0
  60. package/skills/docs/assets/fragments/data/sql-orm.md +13 -0
  61. package/skills/docs/assets/fragments/data/sql-plain.md +13 -0
  62. package/skills/docs/assets/fragments/env/conventional.md +13 -0
  63. package/skills/docs/assets/fragments/env/envx.md +13 -0
  64. package/skills/docs/assets/fragments/env/managed-platform.md +13 -0
  65. package/skills/docs/assets/fragments/env/none.md +11 -0
  66. package/skills/docs/assets/fragments/ui/api-only.md +13 -0
  67. package/skills/docs/assets/fragments/ui/cli.md +14 -0
  68. package/skills/docs/assets/fragments/ui/desktop.md +14 -0
  69. package/skills/docs/assets/fragments/ui/mobile.md +14 -0
  70. package/skills/docs/assets/fragments/ui/web.md +14 -0
  71. package/skills/docs/assets/templates/adr.md +62 -0
  72. package/skills/docs/assets/templates/api.md +30 -0
  73. package/skills/docs/assets/templates/architecture.md +52 -0
  74. package/skills/docs/assets/templates/change-impact-drift-report.md +29 -0
  75. package/skills/docs/assets/templates/compliance.md +29 -0
  76. package/skills/docs/assets/templates/data-schema.md +43 -0
  77. package/skills/docs/assets/templates/feature.md +43 -0
  78. package/skills/docs/assets/templates/foundations.md +55 -0
  79. package/skills/docs/assets/templates/jobs-webhooks.md +36 -0
  80. package/skills/docs/assets/templates/module-inventory.md +19 -0
  81. package/skills/docs/assets/templates/module.md +50 -0
  82. package/skills/docs/assets/templates/nfr.md +22 -0
  83. package/skills/docs/assets/templates/observability.md +28 -0
  84. package/skills/docs/assets/templates/pages-ui-actions.md +47 -0
  85. package/skills/docs/assets/templates/project-summary.md +44 -0
  86. package/skills/docs/assets/templates/roles-permissions.md +26 -0
  87. package/skills/docs/assets/templates/test-plan.md +23 -0
  88. package/skills/docs/assets/templates/workflow-state-machine.md +43 -0
  89. package/skills/docs/references/adr-authoring.md +24 -0
  90. package/skills/docs/references/apis-and-data.md +23 -0
  91. package/skills/docs/references/capability-fragments.md +25 -0
  92. package/skills/docs/references/change-tracking.md +62 -0
  93. package/skills/docs/references/diagrams.md +31 -0
  94. package/skills/docs/references/discovery.md +59 -0
  95. package/skills/docs/references/edge-cases.md +45 -0
  96. package/skills/docs/references/foundations-modules.md +18 -0
  97. package/skills/docs/references/ingestion.md +52 -0
  98. package/skills/docs/references/initialize-adopt.md +25 -0
  99. package/skills/docs/references/module-decomposition.md +38 -0
  100. package/skills/docs/references/profiles.md +60 -0
  101. package/skills/docs/references/quality-attributes.md +27 -0
  102. package/skills/docs/references/reverse-engineer.md +21 -0
  103. package/skills/docs/references/spec-depths.md +31 -0
  104. package/skills/docs/references/summarize.md +18 -0
  105. package/skills/docs/references/surfaces-and-actions.md +24 -0
  106. package/skills/docs/references/validation.md +47 -0
  107. package/skills/docs/references/workflows-and-jobs.md +25 -0
  108. package/skills/docs/scripts/ingest.mjs +984 -0
  109. package/skills/docs/scripts/profile-detect.mjs +299 -0
  110. package/skills/docs/scripts/screen.mjs +96 -0
  111. package/skills/docs/scripts/template-lint.mjs +143 -0
  112. package/skills/docs/scripts/validate-docs.mjs +363 -0
  113. package/skills/doctor/SKILL.md +167 -0
  114. package/skills/feature/SKILL.md +132 -0
  115. package/skills/init/SKILL.md +137 -0
  116. package/skills/project/SKILL.md +261 -0
  117. package/skills/project/references/commands.md +213 -0
  118. package/skills/resume/SKILL.md +79 -0
  119. package/skills/review/SKILL.md +91 -0
  120. package/skills/status/SKILL.md +85 -0
  121. package/skills/task/SKILL.md +183 -0
  122. package/src/cli/product-map.mjs +468 -0
  123. package/src/cli/render.mjs +544 -0
  124. package/src/cli.mjs +2774 -0
  125. package/src/cloud/crypto.mjs +118 -0
  126. package/src/cloud/merge.mjs +186 -0
  127. package/src/cloud/policy.mjs +115 -0
  128. package/src/cloud/sync.mjs +512 -0
  129. package/src/cloud/transport.mjs +116 -0
  130. package/src/db/connect.mjs +189 -0
  131. package/src/db/maintenance.mjs +419 -0
  132. package/src/db/migrate.mjs +188 -0
  133. package/src/db/migrations/001_initial.sql +1024 -0
  134. package/src/db/migrations/002_docs_coverage.sql +126 -0
  135. package/src/db/migrations/003_memory_retrieval_and_integrity.sql +168 -0
  136. package/src/db/migrations/004_task_categories.sql +59 -0
  137. package/src/db/migrations/005_executable_evidence.sql +32 -0
  138. package/src/db/migrations/006_module_and_feature_boundaries.sql +27 -0
  139. package/src/db/migrations/007_drop_redundant_module_users.sql +13 -0
  140. package/src/db/migrations/008_changes_assumptions_test_plans_api_services.sql +166 -0
  141. package/src/db/migrations/009_retired_documents.sql +54 -0
  142. package/src/db/migrations/010_test_plan_evidence.sql +20 -0
  143. package/src/db/migrations/011_criterion_waiver.sql +13 -0
  144. package/src/db/migrations/012_synchronization.sql +56 -0
  145. package/src/db/store.mjs +327 -0
  146. package/src/decisions/record.mjs +167 -0
  147. package/src/docs/proposals.mjs +871 -0
  148. package/src/docs/render.mjs +1424 -0
  149. package/src/docs/templates.mjs +1281 -0
  150. package/src/features/acceptance.mjs +315 -0
  151. package/src/features/specify.mjs +248 -0
  152. package/src/init/discovery.mjs +901 -0
  153. package/src/init/index.mjs +784 -0
  154. package/src/init/questions.mjs +562 -0
  155. package/src/memory/benchmark.mjs +171 -0
  156. package/src/memory/capture.mjs +135 -0
  157. package/src/memory/consolidate.mjs +255 -0
  158. package/src/memory/index.mjs +810 -0
  159. package/src/model/ids.mjs +185 -0
  160. package/src/model/screening.mjs +150 -0
  161. package/src/model/toolkit.mjs +258 -0
  162. package/src/model/vocabulary.mjs +190 -0
  163. package/src/product/assumptions.mjs +120 -0
  164. package/src/product/changes.mjs +180 -0
  165. package/src/product/test-plans.mjs +183 -0
  166. package/src/progress/index.mjs +1364 -0
  167. package/src/runtime/harness.mjs +264 -0
  168. package/src/runtime/hooks.mjs +1021 -0
  169. package/src/runtime/identity.mjs +305 -0
  170. package/src/runtime/resume.mjs +343 -0
  171. package/src/runtime/session.mjs +679 -0
  172. package/src/runtime/version.mjs +315 -0
  173. package/src/service/assets/control-center.html +206 -0
  174. package/src/service/assets/control-center.manifest.json +7 -0
  175. package/src/service/manage.mjs +542 -0
  176. package/src/service/mutations.mjs +860 -0
  177. package/src/service/read-model.mjs +1557 -0
  178. package/src/service/server.mjs +783 -0
  179. package/src/tasks/categories.mjs +154 -0
  180. package/src/tasks/derive.mjs +977 -0
  181. package/src/tasks/lifecycle.mjs +830 -0
  182. package/src/verify/index.mjs +236 -0
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Documentation-profile detection with evidence and confidence.
4
+ * Never guesses silently: low confidence is reported as such; an adapter, when
5
+ * present, is authoritative. Read-only; writes only with an explicit --out.
6
+ *
7
+ * Exit codes: 0 detection completed (including "none"), 2 usage error.
8
+ */
9
+ import { parseArgs } from "node:util";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { pathToFileURL } from "node:url";
13
+
14
+ const USAGE = `Usage: node profile-detect.mjs [--root <path>] [--json] [--out <file>] [--help]
15
+ --root <path> project root to inspect (default: current directory)
16
+ --json machine-readable JSON on stdout
17
+ --out <file> also write the JSON report to this exact path
18
+ --help show this help`;
19
+
20
+ /** Minimal YAML subset reader for the talks adapter: nested "key: value" maps,
21
+ * block and flow string lists, inline comments. The adapter file format is
22
+ * owned by this project and stays inside this documented subset; content
23
+ * outside the subset is ignored rather than misparsed into other keys. */
24
+ export function readYamlSubset(text) {
25
+ const root = {};
26
+ // Each frame: the map receiving keys at deeper indents, plus (for empty-value
27
+ // keys) where that map hangs, so list items can convert it into an array.
28
+ const stack = [{ indent: -1, node: root, parentNode: null, key: null }];
29
+ for (const raw of text.split(/\r?\n/)) {
30
+ if (!raw.trim() || raw.trim().startsWith("#")) continue;
31
+ const indent = raw.length - raw.trimStart().length;
32
+ const line = raw.trim();
33
+ const isList = /^-\s+/.test(line);
34
+ // List items may sit at the same indent as their key; keys at that indent
35
+ // close the frame. Hence strict "<" for list lines, "<=" for key lines.
36
+ while (
37
+ stack.length > 1 &&
38
+ (isList ? indent < stack[stack.length - 1].indent : indent <= stack[stack.length - 1].indent)
39
+ )
40
+ stack.pop();
41
+ const frame = stack[stack.length - 1];
42
+ if (isList) {
43
+ const item = line.replace(/^-\s+/, "");
44
+ if (frame.parentNode && frame.key) {
45
+ if (!Array.isArray(frame.parentNode[frame.key])) frame.parentNode[frame.key] = [];
46
+ frame.parentNode[frame.key].push(stripQuotes(stripComment(item)));
47
+ }
48
+ continue;
49
+ }
50
+ const kv = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
51
+ if (!kv) continue;
52
+ const [, key, rawValue] = kv;
53
+ const value = stripComment(rawValue);
54
+ if (value === "") {
55
+ frame.node[key] = {};
56
+ stack.push({ indent, node: frame.node[key], parentNode: frame.node, key });
57
+ } else if (value.startsWith("[") && value.endsWith("]")) {
58
+ frame.node[key] = value
59
+ .slice(1, -1)
60
+ .split(",")
61
+ .map((s) => stripQuotes(s.trim()))
62
+ .filter(Boolean);
63
+ } else {
64
+ frame.node[key] = stripQuotes(value);
65
+ }
66
+ }
67
+ return root;
68
+ }
69
+
70
+ /** Inline comments on unquoted values: `talks-v1 # note` → `talks-v1`. */
71
+ function stripComment(value) {
72
+ if (/^["']/.test(value)) return value;
73
+ return value.replace(/\s+#.*$/, "").trim();
74
+ }
75
+
76
+ function stripQuotes(s) {
77
+ const m = s.match(/^["'](.*)["']$/);
78
+ return m ? m[1] : s;
79
+ }
80
+
81
+ function exists(...parts) {
82
+ return fs.existsSync(path.join(...parts));
83
+ }
84
+
85
+ function dirNames(dir) {
86
+ if (!fs.existsSync(dir)) return [];
87
+ return fs
88
+ .readdirSync(dir, { withFileTypes: true })
89
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
90
+ .map((e) => e.name);
91
+ }
92
+
93
+ const TYPE_FOLDERS = ["features", "apis", "api", "data", "database", "workflows", "schemas"];
94
+ const DOCS_ROOT_CANDIDATES = ["docs/project", "docs", "documentation"];
95
+
96
+ /** First docs-root candidate that exists - used so adapter-declared profiles
97
+ * still learn where documentation lives when canonicalRoot is omitted. */
98
+ function findDocsRoot(root) {
99
+ for (const candidate of DOCS_ROOT_CANDIDATES) {
100
+ if (fs.existsSync(path.join(root, candidate))) return candidate;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ export function detectProfile(root) {
106
+ const evidence = [];
107
+ const adapterPath = path.join(root, "talks", "project.yaml");
108
+ if (fs.existsSync(adapterPath)) {
109
+ const adapter = readYamlSubset(fs.readFileSync(adapterPath, "utf8"));
110
+ const profile = adapter.docs?.profile;
111
+ if (profile) {
112
+ evidence.push({ what: "adapter declares profile", where: "talks/project.yaml" });
113
+ const docsRoot = adapter.docs?.canonicalRoot ?? findDocsRoot(root);
114
+ return { profile, confidence: "high", source: "adapter", evidence, adapter, docsRoot };
115
+ }
116
+ evidence.push({ what: "adapter present without docs.profile", where: "talks/project.yaml" });
117
+ }
118
+
119
+ if (exists(root, "talks", "state", "schema-version.json")) {
120
+ evidence.push({ what: "talks/state/schema-version.json present", where: "talks/state/schema-version.json" });
121
+ return { profile: "talks-v1", confidence: "high", source: "structure", evidence, docsRoot: "talks" };
122
+ }
123
+
124
+ for (const candidate of DOCS_ROOT_CANDIDATES) {
125
+ const docsRoot = path.join(root, candidate);
126
+ if (!fs.existsSync(docsRoot)) continue;
127
+
128
+ const modulesDir = path.join(docsRoot, "modules");
129
+ const moduleDirs = dirNames(modulesDir);
130
+ const registered = moduleDirs.filter((m) => exists(modulesDir, m, "module.md"));
131
+ if (registered.length) {
132
+ evidence.push({ what: `modules root with ${registered.length} registered module(s)`, where: path.relative(root, modulesDir) });
133
+ return { profile: "module-docs", confidence: "high", source: "structure", evidence, docsRoot: candidate };
134
+ }
135
+ if (moduleDirs.length) {
136
+ evidence.push({ what: "modules folder present but no module.md registrations", where: path.relative(root, modulesDir) });
137
+ return { profile: "module-docs", confidence: "low", source: "structure", evidence, docsRoot: candidate };
138
+ }
139
+
140
+ const typeFolders = dirNames(docsRoot).filter((d) => TYPE_FOLDERS.includes(d));
141
+ if (typeFolders.length >= 2) {
142
+ evidence.push({ what: `sibling type folders: ${typeFolders.join(", ")}`, where: candidate });
143
+ return { profile: "legacy-flat-docs", confidence: "medium", source: "structure", evidence, docsRoot: candidate };
144
+ }
145
+
146
+ const mdFiles = fs.readdirSync(docsRoot).filter((f) => f.endsWith(".md"));
147
+ if (mdFiles.length || dirNames(docsRoot).length) {
148
+ evidence.push({ what: "documentation present matching no known profile", where: candidate });
149
+ return { profile: "custom", confidence: "low", source: "structure", evidence, docsRoot: candidate };
150
+ }
151
+ }
152
+
153
+ evidence.push({ what: "no documentation structure found", where: "." });
154
+ return { profile: "none", confidence: "high", source: "structure", evidence };
155
+ }
156
+
157
+ /** Workspace-aware dependency inventory: root manifest plus every workspace
158
+ * member. Supported declaration shapes (documented safe subset):
159
+ * - npm/yarn `workspaces` as an array, or an object with a `packages` array
160
+ * - pnpm-workspace.yaml `packages` list
161
+ * - exact member paths ("services/api") at any depth
162
+ * - one trailing-level globs ("packages/*", "apps/web/plugins/*")
163
+ * Anything else (multi-star, "**", mid-path stars, traversal, absolute paths,
164
+ * non-string entries, malformed declarations) is NEVER silently ignored - it
165
+ * is reported in `unsupported` so callers can surface incomplete inspection. */
166
+ export function dependencyInventory(root) {
167
+ const packages = [];
168
+ const unsupported = [];
169
+ const seen = new Set();
170
+ const rootPkgPath = path.join(root, "package.json");
171
+ if (!fs.existsSync(rootPkgPath)) return { packages, unsupported };
172
+ const readPkg = (p, rel) => {
173
+ const resolved = path.resolve(p);
174
+ if (seen.has(resolved)) return null;
175
+ seen.add(resolved);
176
+ try {
177
+ const pkg = JSON.parse(fs.readFileSync(p, "utf8"));
178
+ packages.push({
179
+ path: rel.split(path.sep).join("/"),
180
+ name: pkg.name ?? rel,
181
+ dependencies: Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }),
182
+ scripts: pkg.scripts ?? {},
183
+ });
184
+ return pkg;
185
+ } catch {
186
+ unsupported.push({ pattern: rel, reason: "unreadable or malformed package manifest" });
187
+ return null;
188
+ }
189
+ };
190
+ const rootPkg = readPkg(rootPkgPath, "package.json");
191
+ const globs = [];
192
+ const ws = rootPkg?.workspaces;
193
+ if (Array.isArray(ws)) globs.push(...ws);
194
+ else if (ws && typeof ws === "object" && Array.isArray(ws.packages)) globs.push(...ws.packages);
195
+ else if (ws !== undefined) unsupported.push({ pattern: String(ws), reason: "malformed workspaces declaration" });
196
+ const pnpmWs = path.join(root, "pnpm-workspace.yaml");
197
+ if (fs.existsSync(pnpmWs)) {
198
+ const y = readYamlSubset(fs.readFileSync(pnpmWs, "utf8"));
199
+ if (Array.isArray(y.packages)) globs.push(...y.packages);
200
+ else if (y.packages !== undefined) unsupported.push({ pattern: "pnpm-workspace.yaml#packages", reason: "malformed packages declaration" });
201
+ }
202
+ for (const glob of globs) {
203
+ if (typeof glob !== "string" || !glob.trim()) {
204
+ unsupported.push({ pattern: String(glob), reason: "non-string workspace entry" });
205
+ continue;
206
+ }
207
+ const g = glob.replace(/^\.\//, "").replace(/\/$/, "");
208
+ if (path.isAbsolute(g) || /^[A-Za-z]:[\\/]/.test(g) || g.split(/[\\/]/).includes("..")) {
209
+ unsupported.push({ pattern: g, reason: "absolute or traversing workspace path rejected" });
210
+ continue;
211
+ }
212
+ const stars = (g.match(/\*/g) ?? []).length;
213
+ if (stars === 0) {
214
+ const p = path.join(root, g, "package.json");
215
+ if (fs.existsSync(p)) readPkg(p, path.join(g, "package.json"));
216
+ continue;
217
+ }
218
+ if (stars === 1 && g.endsWith("/*")) {
219
+ const base = g.slice(0, -2);
220
+ for (const member of dirNames(path.join(root, base))) {
221
+ const p = path.join(root, base, member, "package.json");
222
+ if (fs.existsSync(p)) readPkg(p, path.join(base, member, "package.json"));
223
+ }
224
+ continue;
225
+ }
226
+ unsupported.push({ pattern: g, reason: "unsupported glob shape (supported: exact paths and one trailing '/*')" });
227
+ }
228
+ return { packages, unsupported };
229
+ }
230
+
231
+ /** Presence-only environment markers; never opens or decrypts anything. */
232
+ export function envMarkers(root) {
233
+ const markers = [];
234
+ let entries = [];
235
+ try {
236
+ entries = fs.readdirSync(root);
237
+ } catch {
238
+ return markers;
239
+ }
240
+ for (const name of entries) {
241
+ if (/^\.env.*\.gpg$/.test(name) || name === ".envxrc") markers.push({ kind: "envx", file: name });
242
+ else if (/^\.env\.(example|template|sample)$/.test(name)) markers.push({ kind: "conventional-example", file: name });
243
+ else if (/^\.env(\..+)?$/.test(name)) markers.push({ kind: "conventional", file: name });
244
+ }
245
+ return markers;
246
+ }
247
+
248
+ function main() {
249
+ let args;
250
+ try {
251
+ args = parseArgs({
252
+ options: {
253
+ root: { type: "string", default: "." },
254
+ json: { type: "boolean", default: false },
255
+ out: { type: "string" },
256
+ help: { type: "boolean", default: false },
257
+ },
258
+ allowPositionals: false,
259
+ }).values;
260
+ } catch (err) {
261
+ console.error(String(err.message ?? err));
262
+ console.error(USAGE);
263
+ process.exit(2);
264
+ }
265
+ if (args.help) {
266
+ console.log(USAGE);
267
+ process.exit(0);
268
+ }
269
+ if (!fs.existsSync(args.root) || !fs.statSync(args.root).isDirectory()) {
270
+ console.error(`root is not a directory: ${args.root}`);
271
+ console.error(USAGE);
272
+ process.exit(2);
273
+ }
274
+ const report = {
275
+ version: 1,
276
+ ...detectProfile(args.root),
277
+ workspace: dependencyInventory(args.root),
278
+ envMarkers: envMarkers(args.root),
279
+ };
280
+ if (args.json) console.log(JSON.stringify(report, null, 2));
281
+ else {
282
+ console.log(`profile: ${report.profile} (confidence: ${report.confidence}, source: ${report.source})`);
283
+ for (const e of report.evidence) console.log(` evidence: ${e.what} [${e.where}]`);
284
+ console.log(`workspace packages: ${report.workspace.packages.length}`);
285
+ if (report.envMarkers.length) console.log(`env markers: ${report.envMarkers.map((m) => m.kind).join(", ")}`);
286
+ }
287
+ if (args.out) {
288
+ const outDir = path.dirname(path.resolve(args.out));
289
+ const tmpOut = path.join(outDir, `.tmp-report-${process.pid}`);
290
+ fs.mkdirSync(outDir, { recursive: true });
291
+ fs.writeFileSync(tmpOut, JSON.stringify(report, null, 2) + "\n");
292
+ fs.renameSync(tmpOut, path.resolve(args.out));
293
+ }
294
+ process.exit(0);
295
+ }
296
+
297
+ // Main-module guard: realpath both sides - Node resolves symlinks (e.g. /tmp
298
+ // on macOS) when building import.meta.url for the entry module.
299
+ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) main();
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Source-material screening (library, not a CLI): secret-shaped values,
3
+ * credential URLs, key material, PII shapes, prompt-injection shapes,
4
+ * executable-instruction shapes, and unsupported authority claims.
5
+ *
6
+ * Defense in depth, stated honestly: these detectors are tripwires that catch
7
+ * known shapes - they do not prove absence. The PRIMARY control is structural:
8
+ * ingested content is never executed and never treated as instructions,
9
+ * whether or not a detector fires.
10
+ *
11
+ * Safety: findings carry stable codes, severity, and span coordinates.
12
+ * Sensitive matches (secrets/PII) NEVER include the matched excerpt; injection
13
+ * and authority matches may carry a short quoted excerpt as evidence - they are
14
+ * quotes of untrusted data, never directives.
15
+ */
16
+
17
+ const SENSITIVE_RULES = [
18
+ { code: "SCR-KEY", severity: "P0", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
19
+ { code: "SCR-SECRET", severity: "P0", re: /\b[A-Za-z0-9_]*(?:password|passwd|secret|token|api[_-]?key|credential)[A-Za-z0-9_]*s?\s*[:=]\s*["']?[^\s"']{6,}/i },
20
+ { code: "SCR-URLCRED", severity: "P0", re: /:\/\/[^/\s:@]+:[^@\s]{4,}@/ },
21
+ { code: "SCR-JWT", severity: "P0", re: /\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}/ },
22
+ { code: "SCR-PII-EMAIL", severity: "P1", re: /[A-Za-z0-9._%+-]+@(?!example\.(?:com|org|net))[A-Za-z0-9.-]+\.[A-Za-z]{2,}/ },
23
+ { code: "SCR-PII-PHONE", severity: "P1", re: /(?:^|\s)\+?\d{1,3}[-. (]{1,2}\d{3}[-. )]{1,2}\d{3}[-. ]?\d{4}\b/ },
24
+ ];
25
+
26
+ const EVIDENCE_RULES = [
27
+ { code: "SCR-INJECT", severity: "P0", re: /ignore\s+(?:all\s+)?(?:previous|prior|above)\s+instructions|disregard\s+(?:your|all)\s+(?:instructions|rules)|you\s+are\s+now\s+(?:a|an|in)\b|<\/?system>|\[system\]|jailbreak/i },
28
+ { code: "SCR-EXEC", severity: "P0", re: /^\s*(?:\$|#|>)\s*(?:rm|curl|wget|bash|sh|eval|sudo)\b|```(?:bash|sh|shell)[\s\S]{0,40}\b(?:rm -|curl |wget )|"tool_call"|"function_call"|<tool_use>/im },
29
+ { code: "SCR-AUTHORITY", severity: "P1", re: /\b(?:the\s+)?(?:CEO|CTO|CFO|legal(?:\s+team)?|compliance(?:\s+team)?|board)\s+(?:has\s+)?(?:decided|approved|requires|mandated|signed\s+off)|\blegally\s+required\b|\bregulation\s+requires\b/i },
30
+ ];
31
+
32
+ /** Screen text; returns findings with line/span coordinates. Sensitive rules
33
+ * never echo the excerpt; evidence rules carry a bounded quote. */
34
+ export function screenText(text) {
35
+ const findings = [];
36
+ const lines = text.split("\n");
37
+ let offset = 0;
38
+ lines.forEach((line, i) => {
39
+ for (const rule of SENSITIVE_RULES) {
40
+ const m = rule.re.exec(line);
41
+ if (m) findings.push({ code: rule.code, severity: rule.severity, line: i + 1, span: { start: offset + m.index, length: m[0].length } });
42
+ rule.re.lastIndex = 0;
43
+ }
44
+ for (const rule of EVIDENCE_RULES) {
45
+ const m = rule.re.exec(line);
46
+ if (m)
47
+ findings.push({
48
+ code: rule.code, severity: rule.severity, line: i + 1,
49
+ span: { start: offset + m.index, length: m[0].length },
50
+ quotedEvidence: m[0].slice(0, 120),
51
+ });
52
+ rule.re.lastIndex = 0;
53
+ }
54
+ offset += line.length + 1;
55
+ });
56
+ // Multi-line EXEC shapes (fenced blocks) checked against the whole text once.
57
+ const fence = /```(?:bash|sh|shell)[\s\S]{0,200}?```/g;
58
+ let fm;
59
+ while ((fm = fence.exec(text))) {
60
+ if (/\b(?:rm -rf|curl |wget |sudo )/.test(fm[0]))
61
+ findings.push({ code: "SCR-EXEC", severity: "P0", line: text.slice(0, fm.index).split("\n").length, span: { start: fm.index, length: fm[0].length }, quotedEvidence: "fenced shell block with destructive/network commands" });
62
+ }
63
+ return findings;
64
+ }
65
+
66
+ const BINARY_EXTENSIONS = new Set([".exe", ".dll", ".so", ".dylib", ".bin", ".o", ".class", ".jar", ".wasm", ".app", ".msi", ".sh", ".bat", ".cmd", ".ps1"]);
67
+ export const MAX_SOURCE_BYTES = 2 * 1024 * 1024; // boundary, reported not guessed
68
+
69
+ // Codes whose matched value is sensitive and must NEVER be excerpted, stored,
70
+ // or echoed anywhere (secrets AND PII - the latter is P1 by report severity but
71
+ // sensitive for persistence). Injection/exec/authority are NOT here: those are
72
+ // facts about source data, carried as bounded quoted evidence.
73
+ export const SENSITIVE_CODES = new Set(["SCR-KEY", "SCR-SECRET", "SCR-URLCRED", "SCR-JWT", "SCR-PII-EMAIL", "SCR-PII-PHONE"]);
74
+
75
+ /** True if a text value carries any sensitive (secret/PII) shape. */
76
+ export function hasSensitive(text) {
77
+ return screenText(String(text)).some((f) => SENSITIVE_CODES.has(f.code));
78
+ }
79
+
80
+ /** Intake safety on raw bytes: executables/unsupported/oversize/malformed. */
81
+ export function screenIntake(name, buf) {
82
+ const findings = [];
83
+ const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
84
+ if (BINARY_EXTENSIONS.has(ext)) findings.push({ code: "SCR-EXECUTABLE", severity: "P0", detail: "executable or script file - rejected from extraction; never executed" });
85
+ if (buf.length > MAX_SOURCE_BYTES) findings.push({ code: "SCR-OVERSIZE", severity: "P1", detail: `exceeds ${MAX_SOURCE_BYTES} byte boundary` });
86
+ const probe = buf.subarray(0, 8000);
87
+ if (probe.includes(0)) findings.push({ code: "SCR-BINARY", severity: "P1", detail: "binary content - inventoried but not extracted" });
88
+ else {
89
+ try {
90
+ new TextDecoder("utf-8", { fatal: true }).decode(probe);
91
+ } catch {
92
+ findings.push({ code: "SCR-ENCODING", severity: "P1", detail: "malformed text encoding - inventoried but not extracted" });
93
+ }
94
+ }
95
+ return findings;
96
+ }
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Template neutrality lint: checks universal templates and capability fragments
4
+ * against known vendor, latency, cloud-region, compliance-regime, and env-var
5
+ * patterns. A denylist scan - it catches known violations; it does not prove
6
+ * absence. Placeholders ({{...}}) and HTML comments are exempt.
7
+ *
8
+ * Finding codes:
9
+ * TL-001 P0 concrete latency/availability number
10
+ * TL-002 P0 cloud region identifier
11
+ * TL-003 P0 concrete compliance regime asserted
12
+ * TL-004 P1 concrete environment-variable name
13
+ * TL-005 P0 vendor/product name
14
+ *
15
+ * Exit codes: 0 clean, 1 findings, 2 usage error.
16
+ */
17
+ import { parseArgs } from "node:util";
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
+
22
+ const USAGE = `Usage: node template-lint.mjs [--dir <path>] [--json] [--out <file>] [--help]
23
+ --dir <path> assets directory to lint (default: the skill's own assets/)
24
+ --json machine-readable JSON on stdout
25
+ --out <file> also write the JSON report to this exact path
26
+ --help show this help`;
27
+
28
+ /** Shared CLI reporting contract: --out writes exactly the machine-readable
29
+ * report, atomically, with a trailing newline; no --out, no report file. */
30
+ function writeReport(outPath, report) {
31
+ if (!outPath) return;
32
+ const dir = path.dirname(path.resolve(outPath));
33
+ const tmp = path.join(dir, `.tmp-report-${process.pid}`);
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ fs.writeFileSync(tmp, JSON.stringify(report, null, 2) + "\n");
36
+ fs.renameSync(tmp, path.resolve(outPath));
37
+ }
38
+
39
+ // Public product names that must never be baked into neutral templates.
40
+ // (Naming them here is the enforcement mechanism, not an endorsement or a leak -
41
+ // these are all public products.) "envx" is excluded: it is a declared external
42
+ // provider (see the repository's references/provider-contracts.md).
43
+ const VENDORS =
44
+ /\b(aws|amazon|azure|gcp|vercel|netlify|heroku|cloudflare|firebase|supabase|stripe|paypal|razorpay|twilio|sendgrid|mailgun|postmark|postgres(?:ql)?|mysql|mariadb|mongodb|dynamodb|redis|kafka|rabbitmq|sqs|auth0|okta|cognito|keycloak|sentry|datadog|posthog|mixpanel|amplitude|segment|launchdarkly|algolia|elasticsearch|next\.?js|react|vue|angular|svelte|drizzle|prisma|hasura|graphql-yoga|expo)\b/i;
45
+ const LATENCY = /\b\d+(?:\.\d+)?\s?(?:ms|milliseconds|seconds?)\b|\b\d{2}\.\d+\s?%|\b99\.\d+\b/;
46
+ // Region shapes: AWS-style (us-east-1), GCP-style (us-central1, europe-west1),
47
+ // Azure-style (westus2, northeurope).
48
+ const REGION =
49
+ /\b(?:us|eu|ap|sa|ca|me|af)-(?:east|west|north|south|central|northeast|southeast|southwest|northwest)-\d\b|\b(?:us|europe|asia|australia|northamerica|southamerica)-(?:east|west|north|south|central)\d\b|\b(?:east|west|north|south|central)(?:us|europe|asia)\d?\b/i;
50
+ const REGIME = /\b(GDPR|HIPAA|CCPA|CPRA|DPDP|PCI[- ]?DSS|SOC\s?2|ISO\s?27001|FedRAMP|COPPA|FERPA)\b/;
51
+ const ENVVAR = /\b[A-Z][A-Z0-9]{1,}(?:_[A-Z0-9]+){1,}\b/;
52
+
53
+ export function stripExempt(text) {
54
+ return text.replace(/<!--[\s\S]*?-->/g, "").replace(/\{\{[^}]*\}\}/g, "");
55
+ }
56
+
57
+ export function lintFile(relFile, text) {
58
+ const findings = [];
59
+ const lines = text.split("\n");
60
+ let inComment = false;
61
+ lines.forEach((rawLine, i) => {
62
+ let line = rawLine;
63
+ if (inComment) {
64
+ const end = line.indexOf("-->");
65
+ if (end === -1) return;
66
+ line = line.slice(end + 3);
67
+ inComment = false;
68
+ }
69
+ line = line.replace(/<!--[\s\S]*?-->/g, "");
70
+ const open = line.indexOf("<!--");
71
+ if (open !== -1) {
72
+ line = line.slice(0, open);
73
+ inComment = true;
74
+ }
75
+ line = line.replace(/\{\{[^}]*\}\}/g, "");
76
+ const loc = { file: relFile, line: i + 1 };
77
+ if (LATENCY.test(line)) findings.push({ ruleId: "TL-001", severity: "P0", ...loc, detail: "concrete latency/availability number" });
78
+ if (REGION.test(line)) findings.push({ ruleId: "TL-002", severity: "P0", ...loc, detail: "cloud region identifier" });
79
+ if (REGIME.test(line)) findings.push({ ruleId: "TL-003", severity: "P0", ...loc, detail: "concrete compliance regime" });
80
+ if (ENVVAR.test(line)) findings.push({ ruleId: "TL-004", severity: "P1", ...loc, detail: "concrete env-var name" });
81
+ if (VENDORS.test(line)) findings.push({ ruleId: "TL-005", severity: "P0", ...loc, detail: "vendor/product name" });
82
+ });
83
+ return findings;
84
+ }
85
+
86
+ export function lintDir(assetsDir) {
87
+ const findings = [];
88
+ const stack = [assetsDir];
89
+ while (stack.length) {
90
+ const dir = stack.pop();
91
+ if (!fs.existsSync(dir)) continue;
92
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
93
+ const full = path.join(dir, entry.name);
94
+ if (entry.isDirectory()) stack.push(full);
95
+ else if (entry.name.endsWith(".md"))
96
+ findings.push(...lintFile(path.relative(assetsDir, full), fs.readFileSync(full, "utf8")));
97
+ }
98
+ }
99
+ return findings;
100
+ }
101
+
102
+ function main() {
103
+ let args;
104
+ try {
105
+ args = parseArgs({
106
+ options: {
107
+ dir: { type: "string" },
108
+ json: { type: "boolean", default: false },
109
+ out: { type: "string" },
110
+ help: { type: "boolean", default: false },
111
+ },
112
+ allowPositionals: false,
113
+ }).values;
114
+ } catch (err) {
115
+ console.error(String(err.message ?? err));
116
+ console.error(USAGE);
117
+ process.exit(2);
118
+ }
119
+ if (args.help) {
120
+ console.log(USAGE);
121
+ process.exit(0);
122
+ }
123
+ const assetsDir = args.dir ?? path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets");
124
+ if (!fs.existsSync(assetsDir) || !fs.statSync(assetsDir).isDirectory()) {
125
+ console.error(`assets directory not found: ${assetsDir}`);
126
+ console.error(USAGE);
127
+ process.exit(2);
128
+ }
129
+ const findings = lintDir(assetsDir);
130
+ const report = { version: 1, dir: path.resolve(assetsDir), count: findings.length, findings };
131
+ if (args.json) console.log(JSON.stringify(report, null, 2));
132
+ else {
133
+ console.log(`template lint - ${findings.length} finding(s)`);
134
+ for (const f of findings) console.log(` [${f.severity}] ${f.ruleId} ${f.file}:${f.line} - ${f.detail}`);
135
+ if (!findings.length) console.log(" clean");
136
+ }
137
+ writeReport(args.out, report);
138
+ process.exit(findings.length ? 1 : 0);
139
+ }
140
+
141
+ // Main-module guard: realpath both sides - Node resolves symlinks (e.g. /tmp
142
+ // on macOS) when building import.meta.url for the entry module.
143
+ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) main();