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.
- package/.claude-plugin/marketplace.json +33 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.codex-plugin/plugin.json +27 -0
- package/CODE_OF_CONDUCT.md +109 -0
- package/CONTRIBUTING.md +205 -0
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +1051 -0
- package/SECURITY.md +135 -0
- package/THIRD-PARTY-NOTICES.md +253 -0
- package/hooks/hooks.json +61 -0
- package/hooks/run.mjs +88 -0
- package/package.json +65 -0
- package/references/confidentiality.md +48 -0
- package/references/evidence-and-risk.md +73 -0
- package/references/operating-model.md +105 -0
- package/references/platform-capabilities.md +51 -0
- package/references/project-record.md +91 -0
- package/references/provider-contracts.md +102 -0
- package/scripts/doctor/doctor.mjs +539 -0
- package/scripts/privacy/scan-history.mjs +163 -0
- package/scripts/privacy/scan.mjs +368 -0
- package/scripts/validate/README.md +94 -0
- package/scripts/validate/common.mjs +149 -0
- package/scripts/validate/data-model.mjs +225 -0
- package/scripts/validate/dependencies.mjs +69 -0
- package/scripts/validate/docs-templates.mjs +99 -0
- package/scripts/validate/footprint.mjs +78 -0
- package/scripts/validate/imports.mjs +61 -0
- package/scripts/validate/manifests.mjs +173 -0
- package/scripts/validate/markdown.mjs +123 -0
- package/scripts/validate/migrations.mjs +187 -0
- package/scripts/validate/no-tests.mjs +136 -0
- package/scripts/validate/privacy.mjs +115 -0
- package/scripts/validate/record-links.mjs +127 -0
- package/scripts/validate/skill-commands.mjs +190 -0
- package/scripts/validate/skills.mjs +112 -0
- package/scripts/validate/specification.mjs +88 -0
- package/scripts/validate/style.mjs +78 -0
- package/scripts/validate/validate-all.mjs +175 -0
- package/skills/debug/SKILL.md +68 -0
- package/skills/decision/SKILL.md +99 -0
- package/skills/docs/SKILL.md +86 -0
- package/skills/docs/assets/fragments/api/events.md +14 -0
- package/skills/docs/assets/fragments/api/graphql.md +14 -0
- package/skills/docs/assets/fragments/api/local-only.md +13 -0
- package/skills/docs/assets/fragments/api/rest.md +13 -0
- package/skills/docs/assets/fragments/api/rpc.md +13 -0
- package/skills/docs/assets/fragments/async/event-bus.md +13 -0
- package/skills/docs/assets/fragments/async/none.md +11 -0
- package/skills/docs/assets/fragments/async/platform-jobs.md +13 -0
- package/skills/docs/assets/fragments/async/queue.md +13 -0
- package/skills/docs/assets/fragments/async/scheduler.md +13 -0
- package/skills/docs/assets/fragments/auth/detected-provider.md +15 -0
- package/skills/docs/assets/fragments/auth/neutral-base.md +14 -0
- package/skills/docs/assets/fragments/data/document.md +13 -0
- package/skills/docs/assets/fragments/data/external-saas.md +13 -0
- package/skills/docs/assets/fragments/data/key-value.md +13 -0
- package/skills/docs/assets/fragments/data/none.md +12 -0
- package/skills/docs/assets/fragments/data/sql-orm.md +13 -0
- package/skills/docs/assets/fragments/data/sql-plain.md +13 -0
- package/skills/docs/assets/fragments/env/conventional.md +13 -0
- package/skills/docs/assets/fragments/env/envx.md +13 -0
- package/skills/docs/assets/fragments/env/managed-platform.md +13 -0
- package/skills/docs/assets/fragments/env/none.md +11 -0
- package/skills/docs/assets/fragments/ui/api-only.md +13 -0
- package/skills/docs/assets/fragments/ui/cli.md +14 -0
- package/skills/docs/assets/fragments/ui/desktop.md +14 -0
- package/skills/docs/assets/fragments/ui/mobile.md +14 -0
- package/skills/docs/assets/fragments/ui/web.md +14 -0
- package/skills/docs/assets/templates/adr.md +62 -0
- package/skills/docs/assets/templates/api.md +30 -0
- package/skills/docs/assets/templates/architecture.md +52 -0
- package/skills/docs/assets/templates/change-impact-drift-report.md +29 -0
- package/skills/docs/assets/templates/compliance.md +29 -0
- package/skills/docs/assets/templates/data-schema.md +43 -0
- package/skills/docs/assets/templates/feature.md +43 -0
- package/skills/docs/assets/templates/foundations.md +55 -0
- package/skills/docs/assets/templates/jobs-webhooks.md +36 -0
- package/skills/docs/assets/templates/module-inventory.md +19 -0
- package/skills/docs/assets/templates/module.md +50 -0
- package/skills/docs/assets/templates/nfr.md +22 -0
- package/skills/docs/assets/templates/observability.md +28 -0
- package/skills/docs/assets/templates/pages-ui-actions.md +47 -0
- package/skills/docs/assets/templates/project-summary.md +44 -0
- package/skills/docs/assets/templates/roles-permissions.md +26 -0
- package/skills/docs/assets/templates/test-plan.md +23 -0
- package/skills/docs/assets/templates/workflow-state-machine.md +43 -0
- package/skills/docs/references/adr-authoring.md +24 -0
- package/skills/docs/references/apis-and-data.md +23 -0
- package/skills/docs/references/capability-fragments.md +25 -0
- package/skills/docs/references/change-tracking.md +62 -0
- package/skills/docs/references/diagrams.md +31 -0
- package/skills/docs/references/discovery.md +59 -0
- package/skills/docs/references/edge-cases.md +45 -0
- package/skills/docs/references/foundations-modules.md +18 -0
- package/skills/docs/references/ingestion.md +52 -0
- package/skills/docs/references/initialize-adopt.md +25 -0
- package/skills/docs/references/module-decomposition.md +38 -0
- package/skills/docs/references/profiles.md +60 -0
- package/skills/docs/references/quality-attributes.md +27 -0
- package/skills/docs/references/reverse-engineer.md +21 -0
- package/skills/docs/references/spec-depths.md +31 -0
- package/skills/docs/references/summarize.md +18 -0
- package/skills/docs/references/surfaces-and-actions.md +24 -0
- package/skills/docs/references/validation.md +47 -0
- package/skills/docs/references/workflows-and-jobs.md +25 -0
- package/skills/docs/scripts/ingest.mjs +984 -0
- package/skills/docs/scripts/profile-detect.mjs +299 -0
- package/skills/docs/scripts/screen.mjs +96 -0
- package/skills/docs/scripts/template-lint.mjs +143 -0
- package/skills/docs/scripts/validate-docs.mjs +363 -0
- package/skills/doctor/SKILL.md +167 -0
- package/skills/feature/SKILL.md +132 -0
- package/skills/init/SKILL.md +137 -0
- package/skills/project/SKILL.md +261 -0
- package/skills/project/references/commands.md +213 -0
- package/skills/resume/SKILL.md +79 -0
- package/skills/review/SKILL.md +91 -0
- package/skills/status/SKILL.md +85 -0
- package/skills/task/SKILL.md +183 -0
- package/src/cli/product-map.mjs +468 -0
- package/src/cli/render.mjs +544 -0
- package/src/cli.mjs +2774 -0
- package/src/cloud/crypto.mjs +118 -0
- package/src/cloud/merge.mjs +186 -0
- package/src/cloud/policy.mjs +115 -0
- package/src/cloud/sync.mjs +512 -0
- package/src/cloud/transport.mjs +116 -0
- package/src/db/connect.mjs +189 -0
- package/src/db/maintenance.mjs +419 -0
- package/src/db/migrate.mjs +188 -0
- package/src/db/migrations/001_initial.sql +1024 -0
- package/src/db/migrations/002_docs_coverage.sql +126 -0
- package/src/db/migrations/003_memory_retrieval_and_integrity.sql +168 -0
- package/src/db/migrations/004_task_categories.sql +59 -0
- package/src/db/migrations/005_executable_evidence.sql +32 -0
- package/src/db/migrations/006_module_and_feature_boundaries.sql +27 -0
- package/src/db/migrations/007_drop_redundant_module_users.sql +13 -0
- package/src/db/migrations/008_changes_assumptions_test_plans_api_services.sql +166 -0
- package/src/db/migrations/009_retired_documents.sql +54 -0
- package/src/db/migrations/010_test_plan_evidence.sql +20 -0
- package/src/db/migrations/011_criterion_waiver.sql +13 -0
- package/src/db/migrations/012_synchronization.sql +56 -0
- package/src/db/store.mjs +327 -0
- package/src/decisions/record.mjs +167 -0
- package/src/docs/proposals.mjs +871 -0
- package/src/docs/render.mjs +1424 -0
- package/src/docs/templates.mjs +1281 -0
- package/src/features/acceptance.mjs +315 -0
- package/src/features/specify.mjs +248 -0
- package/src/init/discovery.mjs +901 -0
- package/src/init/index.mjs +784 -0
- package/src/init/questions.mjs +562 -0
- package/src/memory/benchmark.mjs +171 -0
- package/src/memory/capture.mjs +135 -0
- package/src/memory/consolidate.mjs +255 -0
- package/src/memory/index.mjs +810 -0
- package/src/model/ids.mjs +185 -0
- package/src/model/screening.mjs +150 -0
- package/src/model/toolkit.mjs +258 -0
- package/src/model/vocabulary.mjs +190 -0
- package/src/product/assumptions.mjs +120 -0
- package/src/product/changes.mjs +180 -0
- package/src/product/test-plans.mjs +183 -0
- package/src/progress/index.mjs +1364 -0
- package/src/runtime/harness.mjs +264 -0
- package/src/runtime/hooks.mjs +1021 -0
- package/src/runtime/identity.mjs +305 -0
- package/src/runtime/resume.mjs +343 -0
- package/src/runtime/session.mjs +679 -0
- package/src/runtime/version.mjs +315 -0
- package/src/service/assets/control-center.html +206 -0
- package/src/service/assets/control-center.manifest.json +7 -0
- package/src/service/manage.mjs +542 -0
- package/src/service/mutations.mjs +860 -0
- package/src/service/read-model.mjs +1557 -0
- package/src/service/server.mjs +783 -0
- package/src/tasks/categories.mjs +154 -0
- package/src/tasks/derive.mjs +977 -0
- package/src/tasks/lifecycle.mjs +830 -0
- 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();
|