showdar-skills 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/LICENSE +9 -0
- package/README.md +322 -0
- package/bin/showdar.js +118 -0
- package/bundles/bugfix.yaml +4 -0
- package/bundles/design.yaml +4 -0
- package/bundles/feature.yaml +4 -0
- package/bundles/release.yaml +4 -0
- package/bundles/upgrade.yaml +4 -0
- package/commands/opencode/showdar/build.md +6 -0
- package/commands/opencode/showdar/debug.md +6 -0
- package/commands/opencode/showdar/design.md +6 -0
- package/commands/opencode/showdar/git.md +6 -0
- package/commands/opencode/showdar/ops.md +7 -0
- package/commands/opencode/showdar/plan.md +6 -0
- package/commands/opencode/showdar/quality.md +7 -0
- package/commands/opencode/showdar/recover.md +6 -0
- package/commands/opencode/showdar/requirements.md +7 -0
- package/commands/opencode/showdar/review.md +6 -0
- package/commands/opencode/showdar/security.md +7 -0
- package/commands/opencode/showdar/ship.md +6 -0
- package/commands/opencode/showdar/skill.md +6 -0
- package/commands/opencode/showdar/test.md +6 -0
- package/commands/opencode/showdar/understand.md +6 -0
- package/commands/opencode/showdar/upgrade.md +6 -0
- package/engine/csv.mjs +45 -0
- package/engine/detect-stack.mjs +48 -0
- package/engine/rank.mjs +40 -0
- package/engine/search.mjs +20 -0
- package/package.json +28 -0
- package/profiles/backend.json +1 -0
- package/profiles/developer.json +1 -0
- package/profiles/full.json +1 -0
- package/profiles/minimal.json +1 -0
- package/profiles/product.json +1 -0
- package/profiles/qa.json +1 -0
- package/router/conflicts.yaml +50 -0
- package/router/skill-map.yaml +54 -0
- package/router/triggers.yaml +30 -0
- package/skills/showdar-build/SKILL.md +154 -0
- package/skills/showdar-build/data/change-smells.csv +24 -0
- package/skills/showdar-build/data/index.json +14 -0
- package/skills/showdar-build/examples/change-surface.md +18 -0
- package/skills/showdar-build/references/architecture-boundaries.md +3 -0
- package/skills/showdar-build/references/error-handling.md +3 -0
- package/skills/showdar-build/references/feature-flags.md +3 -0
- package/skills/showdar-build/references/minimal-change.md +3 -0
- package/skills/showdar-build/scripts/change-surface.mjs +5 -0
- package/skills/showdar-build/stacks/dart.md +3 -0
- package/skills/showdar-build/stacks/flutter.md +3 -0
- package/skills/showdar-build/stacks/kotlin.md +3 -0
- package/skills/showdar-build/stacks/nextjs.md +3 -0
- package/skills/showdar-build/stacks/node-fastify.md +3 -0
- package/skills/showdar-build/stacks/node-nestjs.md +3 -0
- package/skills/showdar-build/stacks/react-native.md +3 -0
- package/skills/showdar-build/stacks/react.md +3 -0
- package/skills/showdar-build/stacks/rust-tauri.md +3 -0
- package/skills/showdar-build/stacks/swift.md +3 -0
- package/skills/showdar-build/stacks/typescript.md +3 -0
- package/skills/showdar-debug/SKILL.md +179 -0
- package/skills/showdar-debug/data/failure-patterns.csv +34 -0
- package/skills/showdar-debug/data/index.json +14 -0
- package/skills/showdar-debug/examples/evidence-log.md +19 -0
- package/skills/showdar-debug/references/async-races.md +3 -0
- package/skills/showdar-debug/references/build-failures.md +3 -0
- package/skills/showdar-debug/references/hypothesis-driven-debugging.md +3 -0
- package/skills/showdar-debug/references/memory.md +3 -0
- package/skills/showdar-debug/references/networking.md +3 -0
- package/skills/showdar-debug/references/performance.md +3 -0
- package/skills/showdar-debug/scripts/collect-context.mjs +8 -0
- package/skills/showdar-debug/scripts/lib/detect-stack.mjs +48 -0
- package/skills/showdar-debug/stacks/android.md +21 -0
- package/skills/showdar-debug/stacks/docker.md +21 -0
- package/skills/showdar-debug/stacks/flutter.md +21 -0
- package/skills/showdar-debug/stacks/ios.md +21 -0
- package/skills/showdar-debug/stacks/nextjs.md +21 -0
- package/skills/showdar-debug/stacks/node.md +21 -0
- package/skills/showdar-debug/stacks/react-native.md +21 -0
- package/skills/showdar-debug/stacks/react.md +21 -0
- package/skills/showdar-debug/stacks/tauri.md +21 -0
- package/skills/showdar-design/SKILL.md +179 -0
- package/skills/showdar-design/data/accessibility.csv +16 -0
- package/skills/showdar-design/data/colors.csv +13 -0
- package/skills/showdar-design/data/components.csv +20 -0
- package/skills/showdar-design/data/index.json +25 -0
- package/skills/showdar-design/data/motion.csv +13 -0
- package/skills/showdar-design/data/products.csv +19 -0
- package/skills/showdar-design/data/stacks/compose.csv +9 -0
- package/skills/showdar-design/data/stacks/flutter.csv +9 -0
- package/skills/showdar-design/data/stacks/html-tailwind.csv +9 -0
- package/skills/showdar-design/data/stacks/nextjs.csv +9 -0
- package/skills/showdar-design/data/stacks/nuxt.csv +9 -0
- package/skills/showdar-design/data/stacks/react-native.csv +9 -0
- package/skills/showdar-design/data/stacks/react.csv +9 -0
- package/skills/showdar-design/data/stacks/svelte.csv +9 -0
- package/skills/showdar-design/data/stacks/swiftui.csv +9 -0
- package/skills/showdar-design/data/stacks/tauri.csv +9 -0
- package/skills/showdar-design/data/stacks/vue.csv +9 -0
- package/skills/showdar-design/data/styles.csv +13 -0
- package/skills/showdar-design/data/typography.csv +13 -0
- package/skills/showdar-design/data/ui-patterns.csv +19 -0
- package/skills/showdar-design/examples/design-brief.md +19 -0
- package/skills/showdar-design/references/accessibility.md +5 -0
- package/skills/showdar-design/references/anti-ai-ui.md +5 -0
- package/skills/showdar-design/references/architecture.md +12 -0
- package/skills/showdar-design/references/interaction.md +5 -0
- package/skills/showdar-design/references/layout.md +5 -0
- package/skills/showdar-design/references/release-readiness.md +12 -0
- package/skills/showdar-design/references/responsive.md +5 -0
- package/skills/showdar-design/references/security.md +12 -0
- package/skills/showdar-design/references/visual-hierarchy.md +7 -0
- package/skills/showdar-design/scripts/lib/csv.mjs +40 -0
- package/skills/showdar-design/scripts/lib/rank.mjs +35 -0
- package/skills/showdar-design/scripts/lib/search.mjs +20 -0
- package/skills/showdar-design/scripts/search.mjs +38 -0
- package/skills/showdar-design/stacks/flutter.md +13 -0
- package/skills/showdar-design/stacks/html-tailwind.md +12 -0
- package/skills/showdar-design/stacks/jetpack-compose.md +13 -0
- package/skills/showdar-design/stacks/nextjs.md +12 -0
- package/skills/showdar-design/stacks/nuxt.md +11 -0
- package/skills/showdar-design/stacks/react-native.md +13 -0
- package/skills/showdar-design/stacks/react.md +12 -0
- package/skills/showdar-design/stacks/svelte.md +11 -0
- package/skills/showdar-design/stacks/swiftui.md +12 -0
- package/skills/showdar-design/stacks/tauri.md +12 -0
- package/skills/showdar-design/stacks/vue.md +12 -0
- package/skills/showdar-git/SKILL.md +291 -0
- package/skills/showdar-git/data/git-workflows.csv +7 -0
- package/skills/showdar-git/data/index.json +14 -0
- package/skills/showdar-git/examples/commit-task-only.md +15 -0
- package/skills/showdar-git/examples/conflict-stop.md +7 -0
- package/skills/showdar-git/examples/explicit-push.md +7 -0
- package/skills/showdar-git/examples/feature-merge-no-push.md +13 -0
- package/skills/showdar-git/references/operation-safety.md +34 -0
- package/skills/showdar-git/scripts/inspect-git-state.mjs +53 -0
- package/skills/showdar-ops/SKILL.md +160 -0
- package/skills/showdar-ops/data/index.json +14 -0
- package/skills/showdar-ops/data/ops-patterns.csv +12 -0
- package/skills/showdar-ops/examples/explicit-deployment.md +5 -0
- package/skills/showdar-ops/examples/fix-existing-ci.md +5 -0
- package/skills/showdar-ops/examples/inspect-ci-readonly.md +7 -0
- package/skills/showdar-ops/examples/rollback-plan.md +5 -0
- package/skills/showdar-ops/examples/staging-plan.md +5 -0
- package/skills/showdar-ops/references/ci-cd.md +5 -0
- package/skills/showdar-ops/references/containers.md +5 -0
- package/skills/showdar-ops/references/deployment.md +5 -0
- package/skills/showdar-ops/references/desktop-release.md +3 -0
- package/skills/showdar-ops/references/environments.md +5 -0
- package/skills/showdar-ops/references/mobile-release.md +3 -0
- package/skills/showdar-ops/references/observability.md +5 -0
- package/skills/showdar-ops/references/rollback.md +5 -0
- package/skills/showdar-ops/scripts/inspect-ops-state.mjs +54 -0
- package/skills/showdar-plan/SKILL.md +159 -0
- package/skills/showdar-plan/data/index.json +14 -0
- package/skills/showdar-plan/data/planning-checklists.csv +25 -0
- package/skills/showdar-plan/examples/feature-plan.md +19 -0
- package/skills/showdar-plan/references/architecture.md +5 -0
- package/skills/showdar-plan/references/requirements.md +5 -0
- package/skills/showdar-plan/references/risk-analysis.md +5 -0
- package/skills/showdar-plan/references/scope.md +5 -0
- package/skills/showdar-plan/references/task-decomposition.md +5 -0
- package/skills/showdar-plan/scripts/collect-planning-context.mjs +9 -0
- package/skills/showdar-plan/scripts/lib/detect-stack.mjs +48 -0
- package/skills/showdar-plan/stacks/backend.md +3 -0
- package/skills/showdar-plan/stacks/mobile.md +3 -0
- package/skills/showdar-plan/stacks/web.md +3 -0
- package/skills/showdar-quality/SKILL.md +166 -0
- package/skills/showdar-quality/data/index.json +14 -0
- package/skills/showdar-quality/data/quality-patterns.csv +11 -0
- package/skills/showdar-quality/references/qa-guide.md +20 -0
- package/skills/showdar-recover/SKILL.md +164 -0
- package/skills/showdar-recover/data/index.json +14 -0
- package/skills/showdar-recover/data/recovery-signals.csv +23 -0
- package/skills/showdar-recover/examples/recovery-report.md +18 -0
- package/skills/showdar-recover/references/failed-runs.md +3 -0
- package/skills/showdar-recover/references/interrupted-session.md +3 -0
- package/skills/showdar-recover/references/merge-conflicts.md +3 -0
- package/skills/showdar-recover/references/partial-implementation.md +3 -0
- package/skills/showdar-recover/scripts/inspect-state.mjs +5 -0
- package/skills/showdar-requirements/SKILL.md +172 -0
- package/skills/showdar-requirements/data/index.json +14 -0
- package/skills/showdar-requirements/data/requirements-patterns.csv +11 -0
- package/skills/showdar-requirements/references/analysis-guide.md +30 -0
- package/skills/showdar-review/SKILL.md +155 -0
- package/skills/showdar-review/data/index.json +14 -0
- package/skills/showdar-review/data/review-patterns.csv +27 -0
- package/skills/showdar-review/examples/finding.md +16 -0
- package/skills/showdar-review/references/architecture.md +3 -0
- package/skills/showdar-review/references/correctness.md +3 -0
- package/skills/showdar-review/references/maintainability.md +3 -0
- package/skills/showdar-review/references/performance.md +3 -0
- package/skills/showdar-review/references/security.md +3 -0
- package/skills/showdar-review/references/testing.md +3 -0
- package/skills/showdar-review/scripts/collect-diff.mjs +5 -0
- package/skills/showdar-review/stacks/android.md +3 -0
- package/skills/showdar-review/stacks/flutter.md +3 -0
- package/skills/showdar-review/stacks/ios.md +3 -0
- package/skills/showdar-review/stacks/nextjs.md +3 -0
- package/skills/showdar-review/stacks/node-backend.md +3 -0
- package/skills/showdar-review/stacks/react-native.md +3 -0
- package/skills/showdar-review/stacks/react.md +3 -0
- package/skills/showdar-review/stacks/swift-kotlin.md +3 -0
- package/skills/showdar-review/stacks/typescript.md +3 -0
- package/skills/showdar-security/SKILL.md +169 -0
- package/skills/showdar-security/data/index.json +14 -0
- package/skills/showdar-security/data/security-patterns.csv +13 -0
- package/skills/showdar-security/examples/api-authorization-review.md +9 -0
- package/skills/showdar-security/examples/mobile-deeplink-review.md +7 -0
- package/skills/showdar-security/examples/secret-exposure-review.md +5 -0
- package/skills/showdar-security/examples/threat-model-open-assumptions.md +11 -0
- package/skills/showdar-security/references/authz-secrets.md +7 -0
- package/skills/showdar-security/references/mobile-security.md +7 -0
- package/skills/showdar-security/references/supply-chain.md +5 -0
- package/skills/showdar-security/references/threat-modeling.md +12 -0
- package/skills/showdar-security/scripts/inspect-security-surface.mjs +47 -0
- package/skills/showdar-ship/SKILL.md +234 -0
- package/skills/showdar-ship/data/index.json +14 -0
- package/skills/showdar-ship/data/release-checks.csv +28 -0
- package/skills/showdar-ship/examples/release-report.md +25 -0
- package/skills/showdar-ship/references/migrations.md +12 -0
- package/skills/showdar-ship/references/post-deploy.md +12 -0
- package/skills/showdar-ship/references/release-readiness.md +13 -0
- package/skills/showdar-ship/references/rollback.md +12 -0
- package/skills/showdar-ship/references/secrets.md +3 -0
- package/skills/showdar-ship/scripts/detect-targets.mjs +5 -0
- package/skills/showdar-ship/scripts/lib/detect-stack.mjs +48 -0
- package/skills/showdar-ship/scripts/release-check.mjs +6 -0
- package/skills/showdar-ship/stacks/android.md +33 -0
- package/skills/showdar-ship/stacks/docker.md +30 -0
- package/skills/showdar-ship/stacks/electron.md +7 -0
- package/skills/showdar-ship/stacks/ios.md +34 -0
- package/skills/showdar-ship/stacks/nextjs.md +30 -0
- package/skills/showdar-ship/stacks/node-backend.md +37 -0
- package/skills/showdar-ship/stacks/orchestration.md +12 -0
- package/skills/showdar-ship/stacks/tauri.md +35 -0
- package/skills/showdar-ship/stacks/web.md +35 -0
- package/skills/showdar-test/SKILL.md +164 -0
- package/skills/showdar-test/data/index.json +14 -0
- package/skills/showdar-test/data/test-strategy.csv +22 -0
- package/skills/showdar-test/examples/regression.md +14 -0
- package/skills/showdar-test/references/e2e.md +3 -0
- package/skills/showdar-test/references/integration.md +3 -0
- package/skills/showdar-test/references/regression.md +3 -0
- package/skills/showdar-test/references/test-smells.md +3 -0
- package/skills/showdar-test/references/unit.md +3 -0
- package/skills/showdar-test/scripts/detect-test-tools.mjs +6 -0
- package/skills/showdar-test/stacks/android-testing.md +13 -0
- package/skills/showdar-test/stacks/detox.md +13 -0
- package/skills/showdar-test/stacks/flutter-test.md +13 -0
- package/skills/showdar-test/stacks/integration-test.md +13 -0
- package/skills/showdar-test/stacks/jest.md +13 -0
- package/skills/showdar-test/stacks/playwright.md +13 -0
- package/skills/showdar-test/stacks/react-native-testing.md +17 -0
- package/skills/showdar-test/stacks/react-testing-library.md +13 -0
- package/skills/showdar-test/stacks/vitest.md +13 -0
- package/skills/showdar-test/stacks/xctest.md +13 -0
- package/skills/showdar-understand/SKILL.md +159 -0
- package/skills/showdar-understand/data/architecture-signals.csv +28 -0
- package/skills/showdar-understand/data/index.json +14 -0
- package/skills/showdar-understand/examples/repository-audit.md +17 -0
- package/skills/showdar-understand/references/architecture-signals.md +25 -0
- package/skills/showdar-understand/references/dependency-analysis.md +9 -0
- package/skills/showdar-understand/references/repository-analysis.md +24 -0
- package/skills/showdar-understand/scripts/inspect-repo.mjs +16 -0
- package/skills/showdar-understand/scripts/lib/detect-stack.mjs +48 -0
- package/skills/showdar-understand/stacks/android.md +3 -0
- package/skills/showdar-understand/stacks/backend.md +5 -0
- package/skills/showdar-understand/stacks/electron.md +3 -0
- package/skills/showdar-understand/stacks/fastify.md +3 -0
- package/skills/showdar-understand/stacks/flutter.md +3 -0
- package/skills/showdar-understand/stacks/ios.md +3 -0
- package/skills/showdar-understand/stacks/mobile.md +5 -0
- package/skills/showdar-understand/stacks/nestjs.md +3 -0
- package/skills/showdar-understand/stacks/nextjs.md +3 -0
- package/skills/showdar-understand/stacks/node.md +3 -0
- package/skills/showdar-understand/stacks/react-native.md +3 -0
- package/skills/showdar-understand/stacks/react.md +3 -0
- package/skills/showdar-understand/stacks/tauri.md +3 -0
- package/skills/showdar-upgrade/SKILL.md +162 -0
- package/skills/showdar-upgrade/data/compatibility-checklist.csv +24 -0
- package/skills/showdar-upgrade/data/index.json +14 -0
- package/skills/showdar-upgrade/examples/upgrade-report.md +18 -0
- package/skills/showdar-upgrade/references/breaking-changes.md +3 -0
- package/skills/showdar-upgrade/references/compatibility.md +3 -0
- package/skills/showdar-upgrade/references/dependency-resolution.md +3 -0
- package/skills/showdar-upgrade/references/migration-strategy.md +3 -0
- package/skills/showdar-upgrade/references/rollback.md +3 -0
- package/skills/showdar-upgrade/scripts/inspect-dependencies.mjs +6 -0
- package/skills/showdar-upgrade/scripts/lib/detect-stack.mjs +48 -0
- package/skills/showdar-upgrade/stacks/android.md +15 -0
- package/skills/showdar-upgrade/stacks/cocoapods-spm.md +17 -0
- package/skills/showdar-upgrade/stacks/expo.md +17 -0
- package/skills/showdar-upgrade/stacks/flutter.md +17 -0
- package/skills/showdar-upgrade/stacks/gradle.md +13 -0
- package/skills/showdar-upgrade/stacks/ios.md +15 -0
- package/skills/showdar-upgrade/stacks/nextjs.md +17 -0
- package/skills/showdar-upgrade/stacks/node.md +17 -0
- package/skills/showdar-upgrade/stacks/react-native.md +17 -0
- package/skills/showdar-upgrade/stacks/react.md +17 -0
- package/skills/showdar-upgrade/stacks/tauri-rust.md +15 -0
- package/src/adapters.js +44 -0
- package/src/catalog.js +57 -0
- package/src/project.js +287 -0
- package/src/runtime.js +9 -0
- package/src/validate.js +310 -0
package/src/project.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { access, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { globalCommandRootFor, globalSkillRootFor, NATIVE_TARGETS, opencodeCommandRoot, resolveTargets, skillRootFor } from './adapters.js';
|
|
6
|
+
|
|
7
|
+
const PROJECT_MANIFEST = '.showdar.json';
|
|
8
|
+
const START = '<!-- showdar-skills:start -->';
|
|
9
|
+
const END = '<!-- showdar-skills:end -->';
|
|
10
|
+
|
|
11
|
+
export function globalManifestPath(homeRoot = homedir()) {
|
|
12
|
+
return path.join(homeRoot, '.showdar', 'global.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function exists(target) {
|
|
16
|
+
try { await access(target); return true; } catch { return false; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function hashTree(target) {
|
|
20
|
+
const h = createHash('sha256');
|
|
21
|
+
async function walk(current, relative = '') {
|
|
22
|
+
const info = await stat(current);
|
|
23
|
+
if (info.isDirectory()) {
|
|
24
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
25
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
26
|
+
for (const entry of entries) await walk(path.join(current, entry.name), path.join(relative, entry.name));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
h.update(relative.replaceAll(path.sep, '/'));
|
|
30
|
+
h.update('\0');
|
|
31
|
+
h.update(await readFile(current));
|
|
32
|
+
h.update('\0');
|
|
33
|
+
}
|
|
34
|
+
await walk(target);
|
|
35
|
+
return h.digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readManifest(manifestPath) {
|
|
39
|
+
if (!(await exists(manifestPath))) return null;
|
|
40
|
+
try { return JSON.parse(await readFile(manifestPath, 'utf8')); }
|
|
41
|
+
catch (error) { throw new Error(`Invalid Showdar manifest: ${error.message}`); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function writeJsonAtomic(target, value) {
|
|
45
|
+
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
46
|
+
await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
47
|
+
await rename(tmp, target);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function managedBlock(skillIds) {
|
|
51
|
+
const routes = [
|
|
52
|
+
['map repository architecture, dependencies, or impact', 'showdar-understand'],
|
|
53
|
+
['plan implementation of agreed behavior and scope', 'showdar-plan'],
|
|
54
|
+
['design product UI, UX, responsive layout, accessibility, or visual polish', 'showdar-design'],
|
|
55
|
+
['implement or refactor an agreed application change', 'showdar-build'],
|
|
56
|
+
['debug an observed bug, crash, regression, build, or performance failure', 'showdar-debug'],
|
|
57
|
+
['choose or implement automated tests and coverage', 'showdar-test'],
|
|
58
|
+
['review code or diffs for general correctness, architecture, performance, maintainability, or tests', 'showdar-review'],
|
|
59
|
+
['upgrade dependencies, frameworks, runtimes, or platforms', 'showdar-upgrade'],
|
|
60
|
+
['check release, artifact, or handoff readiness', 'showdar-ship'],
|
|
61
|
+
['recover interrupted or partial engineering work', 'showdar-recover'],
|
|
62
|
+
['perform local Git inspection, staging, commit, merge, rebase, or conflict work', 'showdar-git'],
|
|
63
|
+
['define product behavior, business rules, ambiguity, or acceptance criteria', 'showdar-requirements'],
|
|
64
|
+
['plan QA scenarios, risk coverage, regression, compatibility, or bug evidence', 'showdar-quality'],
|
|
65
|
+
['assess threats, attack surface, trust boundaries, auth, secrets, exposure, or exploitability', 'showdar-security'],
|
|
66
|
+
['inspect or change CI/CD, containers, environments, deployment, observability, rollback, or runtime operations', 'showdar-ops'],
|
|
67
|
+
].filter(([, id]) => skillIds.includes(id));
|
|
68
|
+
const lines = routes.map(([intent, skill]) => `- ${intent} -> \`${skill}\``).join('\n');
|
|
69
|
+
return `${START}\n## Showdar Skills routing\n\nUse the smallest Showdar skill that fully matches the current task. Do not load unrelated Showdar skills.\n\n${lines}\n\nFor debugging, gather evidence before modifying code. For shipping or destructive operations, require explicit user approval and fresh verification. Never print or commit secrets.\n${END}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stripManagedBlock(content) {
|
|
73
|
+
const start = content.indexOf(START);
|
|
74
|
+
if (start === -1) return content;
|
|
75
|
+
const end = content.indexOf(END, start);
|
|
76
|
+
if (end === -1) throw new Error('AGENTS.md contains an incomplete Showdar managed block. Repair it manually before continuing.');
|
|
77
|
+
const before = content.slice(0, start).trimEnd();
|
|
78
|
+
const after = content.slice(end + END.length).trimStart();
|
|
79
|
+
return [before, after].filter(Boolean).join('\n\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function writeAgentsBlock(projectRoot, skillIds) {
|
|
83
|
+
const target = path.join(projectRoot, 'AGENTS.md');
|
|
84
|
+
const current = (await exists(target)) ? await readFile(target, 'utf8') : '';
|
|
85
|
+
const clean = stripManagedBlock(current).trimEnd();
|
|
86
|
+
const block = managedBlock(skillIds);
|
|
87
|
+
await writeFile(target, clean ? `${clean}\n\n${block}\n` : `${block}\n`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function removeAgentsBlock(projectRoot) {
|
|
91
|
+
const target = path.join(projectRoot, 'AGENTS.md');
|
|
92
|
+
if (!(await exists(target))) return;
|
|
93
|
+
const current = await readFile(target, 'utf8');
|
|
94
|
+
const clean = stripManagedBlock(current).trim();
|
|
95
|
+
if (clean) await writeFile(target, `${clean}\n`);
|
|
96
|
+
else await rm(target, { force: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ownedPathSet(manifest) {
|
|
100
|
+
return new Set((manifest?.files ?? []).map((entry) => entry.path));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function safeOwnedPath(baseRoot, relative, allowedRoots = []) {
|
|
104
|
+
if (typeof relative !== 'string' || !relative) return null;
|
|
105
|
+
const roots = [baseRoot, ...allowedRoots].map((root) => path.resolve(root));
|
|
106
|
+
const target = path.resolve(relative.startsWith(path.sep) ? relative : path.join(roots[0], relative));
|
|
107
|
+
if (!roots.some((root) => target === root || target.startsWith(`${root}${path.sep}`))) return null;
|
|
108
|
+
return target;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function manifestPathFor(baseRoot, destination) {
|
|
112
|
+
const relative = path.relative(baseRoot, destination);
|
|
113
|
+
return relative.replaceAll(path.sep, '/');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles }) {
|
|
117
|
+
const relative = manifestPathFor(baseRoot, destination);
|
|
118
|
+
if ((await exists(destination)) && !priorOwned.has(relative)) {
|
|
119
|
+
throw new Error(`Refusing to overwrite existing non-Showdar-managed skill or command: ${destination}`);
|
|
120
|
+
}
|
|
121
|
+
await rm(destination, { recursive: true, force: true });
|
|
122
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
123
|
+
await cp(source, destination, { recursive: true });
|
|
124
|
+
newFiles.push({ path: relative, hash: await hashTree(destination) });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0', scope, skillRootForTarget, commandRoot, managedRoots = [] }) {
|
|
128
|
+
await mkdir(baseRoot, { recursive: true });
|
|
129
|
+
const targets = resolveTargets(ai);
|
|
130
|
+
const prior = await readManifest(manifestPath);
|
|
131
|
+
const priorOwned = ownedPathSet(prior);
|
|
132
|
+
const desiredPaths = new Set();
|
|
133
|
+
|
|
134
|
+
const skillRoots = new Map();
|
|
135
|
+
for (const target of targets) {
|
|
136
|
+
const root = skillRootForTarget(target);
|
|
137
|
+
skillRoots.set(path.resolve(root), root);
|
|
138
|
+
}
|
|
139
|
+
for (const root of skillRoots.values()) {
|
|
140
|
+
for (const skillId of skillIds) desiredPaths.add(manifestPathFor(baseRoot, path.join(root, skillId)));
|
|
141
|
+
}
|
|
142
|
+
if (targets.includes('opencode')) {
|
|
143
|
+
for (const name of commandNames) desiredPaths.add(manifestPathFor(baseRoot, path.join(commandRoot(), `${name}.md`)));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const entry of prior?.files ?? []) {
|
|
147
|
+
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
148
|
+
if (target && !desiredPaths.has(entry.path)) await rm(target, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const files = [];
|
|
152
|
+
for (const root of skillRoots.values()) {
|
|
153
|
+
for (const skillId of skillIds) {
|
|
154
|
+
const source = path.join(packageRoot, 'skills', skillId);
|
|
155
|
+
if (!(await exists(path.join(source, 'SKILL.md')))) throw new Error(`Skill asset not found: ${skillId}`);
|
|
156
|
+
await copyOwned({ baseRoot, source, destination: path.join(root, skillId), priorOwned, newFiles: files });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (targets.includes('opencode')) {
|
|
161
|
+
const root = commandRoot();
|
|
162
|
+
for (const name of commandNames) {
|
|
163
|
+
const source = path.join(packageRoot, 'commands', 'opencode', 'showdar', `${name}.md`);
|
|
164
|
+
if (!(await exists(source))) throw new Error(`OpenCode command asset not found: ${name}`);
|
|
165
|
+
await copyOwned({ baseRoot, source, destination: path.join(root, `${name}.md`), priorOwned, newFiles: files });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const manifest = {
|
|
170
|
+
version: 2,
|
|
171
|
+
scope,
|
|
172
|
+
packageVersion,
|
|
173
|
+
profile,
|
|
174
|
+
ai,
|
|
175
|
+
targets,
|
|
176
|
+
skills: [...skillIds],
|
|
177
|
+
commands: targets.includes('opencode') ? [...commandNames] : [],
|
|
178
|
+
files,
|
|
179
|
+
};
|
|
180
|
+
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
181
|
+
await writeJsonAtomic(manifestPath, manifest);
|
|
182
|
+
if (agentsRoot) await writeAgentsBlock(agentsRoot, skillIds);
|
|
183
|
+
return inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function initProject({ projectRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0' }) {
|
|
187
|
+
return initInstallation({
|
|
188
|
+
baseRoot: projectRoot,
|
|
189
|
+
manifestPath: path.join(projectRoot, PROJECT_MANIFEST),
|
|
190
|
+
agentsRoot: projectRoot,
|
|
191
|
+
packageRoot,
|
|
192
|
+
profile,
|
|
193
|
+
ai,
|
|
194
|
+
skillIds,
|
|
195
|
+
commandNames,
|
|
196
|
+
packageVersion,
|
|
197
|
+
scope: 'project',
|
|
198
|
+
skillRootForTarget: (target) => skillRootFor(target, projectRoot),
|
|
199
|
+
commandRoot: () => opencodeCommandRoot(projectRoot),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function initGlobal({ homeRoot = homedir(), packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0' }) {
|
|
204
|
+
const managedRoots = [...new Set(NATIVE_TARGETS.map((target) => globalSkillRootFor(target, { homeRoot }))), globalCommandRootFor({ homeRoot })];
|
|
205
|
+
return initInstallation({
|
|
206
|
+
baseRoot: homeRoot,
|
|
207
|
+
manifestPath: globalManifestPath(homeRoot),
|
|
208
|
+
agentsRoot: null,
|
|
209
|
+
packageRoot,
|
|
210
|
+
profile,
|
|
211
|
+
ai,
|
|
212
|
+
skillIds,
|
|
213
|
+
commandNames,
|
|
214
|
+
packageVersion,
|
|
215
|
+
scope: 'global',
|
|
216
|
+
skillRootForTarget: (target) => globalSkillRootFor(target, { homeRoot }),
|
|
217
|
+
commandRoot: () => globalCommandRootFor({ homeRoot }),
|
|
218
|
+
managedRoots,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope, managedRoots = [] }) {
|
|
223
|
+
let manifest;
|
|
224
|
+
try { manifest = await readManifest(manifestPath); }
|
|
225
|
+
catch (error) { return { installed: true, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, commands: 0, issues: [error.message] }; }
|
|
226
|
+
if (!manifest) return { installed: false, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, commands: 0, issues: ['Showdar is not installed.'] };
|
|
227
|
+
|
|
228
|
+
const issues = [];
|
|
229
|
+
for (const entry of manifest.files ?? []) {
|
|
230
|
+
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
231
|
+
if (!target) { issues.push(`Invalid managed path: ${entry.path}`); continue; }
|
|
232
|
+
if (!(await exists(target))) { issues.push(`Missing managed path: ${entry.path}`); continue; }
|
|
233
|
+
const actual = await hashTree(target);
|
|
234
|
+
if (actual !== entry.hash) issues.push(`Managed path drift detected: ${entry.path}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (agentsRoot) {
|
|
238
|
+
const agentsFile = path.join(agentsRoot, 'AGENTS.md');
|
|
239
|
+
if (!(await exists(agentsFile))) issues.push('Missing AGENTS.md routing block');
|
|
240
|
+
else {
|
|
241
|
+
const text = await readFile(agentsFile, 'utf8');
|
|
242
|
+
if (!text.includes(START) || !text.includes(END)) issues.push('Missing Showdar AGENTS.md managed block');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
installed: true,
|
|
248
|
+
healthy: issues.length === 0,
|
|
249
|
+
scope: manifest.scope ?? scope,
|
|
250
|
+
profile: manifest.profile ?? null,
|
|
251
|
+
ai: manifest.ai ?? null,
|
|
252
|
+
targets: manifest.targets ?? [],
|
|
253
|
+
skills: (manifest.skills ?? []).length,
|
|
254
|
+
commands: (manifest.commands ?? []).length,
|
|
255
|
+
issues,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function removeInstallation({ baseRoot, manifestPath, agentsRoot, managedRoots = [] }) {
|
|
260
|
+
let manifest;
|
|
261
|
+
try { manifest = await readManifest(manifestPath); }
|
|
262
|
+
catch { manifest = null; }
|
|
263
|
+
for (const entry of manifest?.files ?? []) {
|
|
264
|
+
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
265
|
+
if (target) await rm(target, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
await rm(manifestPath, { force: true });
|
|
268
|
+
if (agentsRoot) await removeAgentsBlock(agentsRoot);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function inspectProject(projectRoot) {
|
|
272
|
+
return inspectInstallation({ baseRoot: projectRoot, manifestPath: path.join(projectRoot, PROJECT_MANIFEST), agentsRoot: projectRoot, scope: 'project' });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function inspectGlobal({ homeRoot = homedir() } = {}) {
|
|
276
|
+
const managedRoots = [...new Set(NATIVE_TARGETS.map((target) => globalSkillRootFor(target, { homeRoot }))), globalCommandRootFor({ homeRoot })];
|
|
277
|
+
return inspectInstallation({ baseRoot: homeRoot, manifestPath: globalManifestPath(homeRoot), agentsRoot: null, scope: 'global', managedRoots });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function removeProject(projectRoot) {
|
|
281
|
+
await removeInstallation({ baseRoot: projectRoot, manifestPath: path.join(projectRoot, PROJECT_MANIFEST), agentsRoot: projectRoot });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function removeGlobal({ homeRoot = homedir() } = {}) {
|
|
285
|
+
const managedRoots = [...new Set(NATIVE_TARGETS.map((target) => globalSkillRootFor(target, { homeRoot }))), globalCommandRootFor({ homeRoot })];
|
|
286
|
+
await removeInstallation({ baseRoot: homeRoot, manifestPath: globalManifestPath(homeRoot), agentsRoot: null, managedRoots });
|
|
287
|
+
}
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const CANONICAL_RUNTIME_FILE = 'engine/detect-stack.mjs';
|
|
2
|
+
|
|
3
|
+
export const VENDORED_RUNTIME_FILES = Object.freeze([
|
|
4
|
+
'skills/showdar-debug/scripts/lib/detect-stack.mjs',
|
|
5
|
+
'skills/showdar-plan/scripts/lib/detect-stack.mjs',
|
|
6
|
+
'skills/showdar-ship/scripts/lib/detect-stack.mjs',
|
|
7
|
+
'skills/showdar-understand/scripts/lib/detect-stack.mjs',
|
|
8
|
+
'skills/showdar-upgrade/scripts/lib/detect-stack.mjs',
|
|
9
|
+
]);
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { access, readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { SKILLS, PROFILES } from './catalog.js';
|
|
5
|
+
import { CANONICAL_RUNTIME_FILE, VENDORED_RUNTIME_FILES } from './runtime.js';
|
|
6
|
+
import { parseCsv } from '../engine/csv.mjs';
|
|
7
|
+
|
|
8
|
+
export const STACK_IDS = new Set([
|
|
9
|
+
'android', 'android-testing', 'ci', 'cocoapods', 'compose', 'dart', 'detox', 'docker', 'electron',
|
|
10
|
+
'expo', 'fastify', 'flutter', 'flutter-test', 'gradle', 'html-tailwind', 'integration_test', 'ios',
|
|
11
|
+
'jest', 'kotlin', 'kubernetes', 'nestjs', 'nextjs', 'node', 'node-fastify', 'nuxt', 'package-manager',
|
|
12
|
+
'playwright', 'react', 'react-native', 'react-testing-library', 'rust', 'svelte', 'spm', 'swift',
|
|
13
|
+
'swiftui', 'tauri', 'typescript', 'vitest', 'vue', 'workspace', 'xctest',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const REQUIRED_SECTIONS = [
|
|
17
|
+
'Purpose',
|
|
18
|
+
'When to use',
|
|
19
|
+
'When not to use',
|
|
20
|
+
'Inputs and assumptions',
|
|
21
|
+
'Non-negotiable rules',
|
|
22
|
+
'Workflow',
|
|
23
|
+
'Decision points',
|
|
24
|
+
'Stack detection',
|
|
25
|
+
'Failure modes',
|
|
26
|
+
'Stop conditions',
|
|
27
|
+
'Escalation conditions',
|
|
28
|
+
'Verification',
|
|
29
|
+
'Output contract',
|
|
30
|
+
'Anti-patterns',
|
|
31
|
+
'Example',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
async function exists(target) {
|
|
35
|
+
try { await access(target); return true; } catch { return false; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseFrontmatter(text) {
|
|
39
|
+
if (!text.startsWith('---\n')) return {};
|
|
40
|
+
const end = text.indexOf('\n---\n', 4);
|
|
41
|
+
if (end === -1) return {};
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const line of text.slice(4, end).split('\n')) {
|
|
44
|
+
const i = line.indexOf(':');
|
|
45
|
+
if (i <= 0) continue;
|
|
46
|
+
out[line.slice(0, i).trim()] = line.slice(i + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function meaningfulLines(text) {
|
|
52
|
+
return text.split(/\r?\n/).filter((line) => {
|
|
53
|
+
const value = line.trim();
|
|
54
|
+
return value && value !== '---' && !value.startsWith('#') && !/^[\-*]?$/.test(value);
|
|
55
|
+
}).length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function referencedLocalAssets(text) {
|
|
59
|
+
const code = [...text.matchAll(/`((?:references|data|stacks|scripts|examples)\/[^`\n]+)`/g)].map((match) => match[1]);
|
|
60
|
+
const links = [...text.matchAll(/\]\(((?:references|data|stacks|scripts|examples)\/[^)#\s]+)(?:#[^)\s]+)?\)/g)].map((match) => match[1]);
|
|
61
|
+
return [...new Set([...code, ...links])];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function safeRelativePath(value) {
|
|
65
|
+
return typeof value === 'string' && value.length > 0 && !path.isAbsolute(value) && !value.split(/[\\/]/).includes('..');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function validateSkillDirectory(skillDir, { minMeaningfulLines = 60 } = {}) {
|
|
69
|
+
const errors = [];
|
|
70
|
+
const warnings = [];
|
|
71
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
72
|
+
if (!(await exists(skillFile))) return { ok: false, errors: ['missing SKILL.md'], warnings };
|
|
73
|
+
|
|
74
|
+
const text = await readFile(skillFile, 'utf8');
|
|
75
|
+
const frontmatter = parseFrontmatter(text);
|
|
76
|
+
const basename = path.basename(skillDir);
|
|
77
|
+
|
|
78
|
+
if (!frontmatter.name) errors.push('frontmatter is missing name');
|
|
79
|
+
else if (frontmatter.name !== basename) errors.push(`frontmatter name "${frontmatter.name}" does not match directory "${basename}"`);
|
|
80
|
+
if (!frontmatter.description || frontmatter.description.length < 30) errors.push('frontmatter description must be at least 30 characters');
|
|
81
|
+
|
|
82
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
83
|
+
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
84
|
+
if (!new RegExp(`^##\\s+${escaped}\\s*$`, 'mi').test(text)) errors.push(`missing section: ${section}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const lines = meaningfulLines(text);
|
|
88
|
+
if (lines < minMeaningfulLines) errors.push(`SKILL.md has ${lines} meaningful lines; expected at least ${minMeaningfulLines}`);
|
|
89
|
+
|
|
90
|
+
const placeholder = text.match(/\b(TODO|TBD|FIXME)\b|fill\s+(?:this\s+)?later|lorem ipsum|placeholder content/i);
|
|
91
|
+
if (placeholder) errors.push(`placeholder marker found: ${placeholder[0]}`);
|
|
92
|
+
|
|
93
|
+
for (const relative of referencedLocalAssets(text)) {
|
|
94
|
+
if (!(await exists(path.join(skillDir, relative)))) errors.push(`referenced asset does not exist: ${relative}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function validateScripts(dir) {
|
|
98
|
+
if (!(await exists(dir))) return;
|
|
99
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
100
|
+
const target = path.join(dir, entry.name);
|
|
101
|
+
if (entry.isDirectory()) {
|
|
102
|
+
await validateScripts(target);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!entry.name.endsWith('.mjs') && !entry.name.endsWith('.js')) continue;
|
|
106
|
+
const syntax = spawnSync(process.execPath, ['--check', target], { encoding: 'utf8' });
|
|
107
|
+
if (syntax.status !== 0) errors.push(`script ${path.relative(skillDir, target)} is not runnable JavaScript: ${(syntax.stderr || syntax.stdout).trim()}`);
|
|
108
|
+
const source = await readFile(target, 'utf8');
|
|
109
|
+
const imports = [
|
|
110
|
+
...source.matchAll(/(?:from\s+|import\s*)['"](\.{1,2}\/[^'"]+)['"]/g),
|
|
111
|
+
].map((match) => match[1]);
|
|
112
|
+
for (const specifier of imports) {
|
|
113
|
+
const resolved = path.resolve(path.dirname(target), specifier);
|
|
114
|
+
if (!(await exists(resolved))) {
|
|
115
|
+
errors.push(`script ${path.relative(skillDir, target)} has missing relative import: ${specifier}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
await validateScripts(path.join(skillDir, 'scripts'));
|
|
121
|
+
await validateCsvFiles(skillDir, errors);
|
|
122
|
+
|
|
123
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function validateCsvFiles(skillDir, errors) {
|
|
127
|
+
const dataDir = path.join(skillDir, 'data');
|
|
128
|
+
const csvFiles = [];
|
|
129
|
+
async function walk(dir) {
|
|
130
|
+
if (!(await exists(dir))) return;
|
|
131
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
132
|
+
const target = path.join(dir, entry.name);
|
|
133
|
+
if (entry.isDirectory()) await walk(target);
|
|
134
|
+
else if (entry.name.endsWith('.csv')) csvFiles.push(target);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
await walk(dataDir);
|
|
138
|
+
if (!csvFiles.length) return;
|
|
139
|
+
|
|
140
|
+
const indexFile = path.join(dataDir, 'index.json');
|
|
141
|
+
if (!(await exists(indexFile))) {
|
|
142
|
+
errors.push('data contains CSV datasets but is missing data/index.json');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let index;
|
|
147
|
+
try { index = JSON.parse(await readFile(indexFile, 'utf8')); }
|
|
148
|
+
catch (error) { errors.push(`data/index.json is invalid JSON: ${error.message}`); return; }
|
|
149
|
+
if (index.version !== 1) errors.push('data/index.json must declare version 1');
|
|
150
|
+
if (index.skill !== path.basename(skillDir)) errors.push('data/index.json skill does not match directory');
|
|
151
|
+
if (!Array.isArray(index.datasets) || !index.datasets.length) { errors.push('data/index.json must list at least one dataset'); return; }
|
|
152
|
+
|
|
153
|
+
const indexed = new Set();
|
|
154
|
+
for (const dataset of index.datasets) {
|
|
155
|
+
if (!dataset || typeof dataset !== 'object' || Array.isArray(dataset)) {
|
|
156
|
+
errors.push('data/index.json contains a dataset that is not an object');
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const listFields = (name) => {
|
|
160
|
+
const value = dataset[name];
|
|
161
|
+
if (value === undefined) return [];
|
|
162
|
+
if (!Array.isArray(value) || value.some((field) => typeof field !== 'string' || !field.trim())) {
|
|
163
|
+
errors.push(`data/index.json ${name} must be an array of non-empty strings`);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const unique = new Set(value);
|
|
167
|
+
if (unique.size !== value.length) errors.push(`data/index.json ${name} contains duplicates`);
|
|
168
|
+
return value;
|
|
169
|
+
};
|
|
170
|
+
const requiredColumns = listFields('requiredColumns');
|
|
171
|
+
const searchableFields = listFields('searchableFields');
|
|
172
|
+
const filterFields = listFields('filterFields');
|
|
173
|
+
if (dataset.idField !== undefined && (typeof dataset.idField !== 'string' || !dataset.idField.trim())) {
|
|
174
|
+
errors.push('data/index.json idField must be a non-empty string');
|
|
175
|
+
}
|
|
176
|
+
const relative = dataset?.file;
|
|
177
|
+
if (!safeRelativePath(relative) || !relative.endsWith('.csv')) { errors.push(`invalid indexed dataset path: ${relative ?? '<missing>'}`); continue; }
|
|
178
|
+
if (indexed.has(relative)) errors.push(`duplicate indexed dataset: ${relative}`);
|
|
179
|
+
indexed.add(relative);
|
|
180
|
+
const target = path.join(skillDir, relative);
|
|
181
|
+
if (!(await exists(target))) { errors.push(`indexed dataset does not exist: ${relative}`); continue; }
|
|
182
|
+
let rows;
|
|
183
|
+
try { rows = parseCsv(await readFile(target, 'utf8')); }
|
|
184
|
+
catch (error) { errors.push(`${relative} is malformed CSV: ${error.message}`); continue; }
|
|
185
|
+
if (!rows.length) { errors.push(`${relative} is an empty dataset`); continue; }
|
|
186
|
+
const columns = Object.keys(rows[0]);
|
|
187
|
+
const required = [...new Set([...requiredColumns, dataset.idField].filter(Boolean))];
|
|
188
|
+
for (const column of required) if (!columns.includes(column)) errors.push(`${relative} is missing required column: ${column}`);
|
|
189
|
+
for (const column of [...searchableFields, ...filterFields]) {
|
|
190
|
+
if (!columns.includes(column)) errors.push(`${relative} indexes missing column: ${column}`);
|
|
191
|
+
}
|
|
192
|
+
const rowKeys = new Set();
|
|
193
|
+
const idKeys = new Set();
|
|
194
|
+
for (const row of rows) {
|
|
195
|
+
const rowKey = JSON.stringify(columns.map((column) => row[column] ?? ''));
|
|
196
|
+
if (rowKeys.has(rowKey)) errors.push(`${relative} contains duplicate row`);
|
|
197
|
+
rowKeys.add(rowKey);
|
|
198
|
+
if (dataset.idField) {
|
|
199
|
+
const id = String(row[dataset.idField] ?? '').trim();
|
|
200
|
+
if (!id) errors.push(`${relative} contains an empty ${dataset.idField}`);
|
|
201
|
+
else if (idKeys.has(id)) errors.push(`${relative} contains duplicate id: ${id}`);
|
|
202
|
+
idKeys.add(id);
|
|
203
|
+
}
|
|
204
|
+
for (const column of ['stack', 'stacks']) {
|
|
205
|
+
for (const stack of String(row[column] ?? '').split(/[,;|]/).map((value) => value.trim()).filter(Boolean)) {
|
|
206
|
+
if (stack !== '*' && !STACK_IDS.has(stack)) errors.push(`${relative} has invalid stack: ${stack}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (row.reference && (!safeRelativePath(row.reference) || !(await exists(path.join(skillDir, row.reference))))) {
|
|
210
|
+
errors.push(`${relative} references missing asset: ${row.reference}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (dataset.reference && (!safeRelativePath(dataset.reference) || !(await exists(path.join(skillDir, dataset.reference))))) {
|
|
214
|
+
errors.push(`${relative} references missing asset: ${dataset.reference}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const target of csvFiles) {
|
|
218
|
+
const relative = path.relative(skillDir, target).replaceAll(path.sep, '/');
|
|
219
|
+
if (!indexed.has(relative)) errors.push(`${relative} is not listed in data/index.json`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function validateRepository(packageRoot) {
|
|
224
|
+
const errors = [];
|
|
225
|
+
const warnings = [];
|
|
226
|
+
const known = new Set(SKILLS.map((skill) => skill.id));
|
|
227
|
+
const skillsRoot = path.join(packageRoot, 'skills');
|
|
228
|
+
|
|
229
|
+
for (const skill of SKILLS) {
|
|
230
|
+
const dir = path.join(skillsRoot, skill.id);
|
|
231
|
+
const result = await validateSkillDirectory(dir);
|
|
232
|
+
for (const error of result.errors) errors.push(`${skill.id}: ${error}`);
|
|
233
|
+
warnings.push(...result.warnings.map((warning) => `${skill.id}: ${warning}`));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let canonicalRuntime;
|
|
237
|
+
try {
|
|
238
|
+
canonicalRuntime = await readFile(path.join(packageRoot, CANONICAL_RUNTIME_FILE));
|
|
239
|
+
} catch (error) {
|
|
240
|
+
errors.push(`missing canonical runtime detector: ${CANONICAL_RUNTIME_FILE} (${error.message})`);
|
|
241
|
+
}
|
|
242
|
+
if (canonicalRuntime) {
|
|
243
|
+
for (const relative of VENDORED_RUNTIME_FILES) {
|
|
244
|
+
try {
|
|
245
|
+
const vendoredRuntime = await readFile(path.join(packageRoot, relative));
|
|
246
|
+
if (!vendoredRuntime.equals(canonicalRuntime)) errors.push(`vendored runtime detector drift: ${relative} differs from ${CANONICAL_RUNTIME_FILE}`);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
errors.push(`missing vendored runtime detector: ${relative} (${error.message})`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (await exists(skillsRoot)) {
|
|
254
|
+
for (const entry of await readdir(skillsRoot, { withFileTypes: true })) {
|
|
255
|
+
if (entry.isDirectory() && entry.name.startsWith('showdar-') && !known.has(entry.name)) errors.push(`unexpected skill directory: ${entry.name}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const [profile, ids] of Object.entries(PROFILES)) {
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
for (const id of ids) {
|
|
262
|
+
if (!known.has(id)) errors.push(`profile ${profile} references unknown skill ${id}`);
|
|
263
|
+
if (seen.has(id)) errors.push(`profile ${profile} duplicates skill ${id}`);
|
|
264
|
+
seen.add(id);
|
|
265
|
+
}
|
|
266
|
+
const profileFile = path.join(packageRoot, 'profiles', `${profile}.json`);
|
|
267
|
+
if (!(await exists(profileFile))) errors.push(`missing profile file: profiles/${profile}.json`);
|
|
268
|
+
else {
|
|
269
|
+
try {
|
|
270
|
+
const disk = JSON.parse(await readFile(profileFile, 'utf8'));
|
|
271
|
+
if (JSON.stringify(disk.skills ?? []) !== JSON.stringify(ids)) errors.push(`profile file ${profile}.json does not match catalog`);
|
|
272
|
+
} catch (error) { errors.push(`invalid profile file ${profile}.json: ${error.message}`); }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const relationshipFiles = [
|
|
277
|
+
'router/skill-map.yaml', 'router/triggers.yaml', 'router/conflicts.yaml',
|
|
278
|
+
'bundles/bugfix.yaml', 'bundles/feature.yaml', 'bundles/design.yaml', 'bundles/upgrade.yaml', 'bundles/release.yaml',
|
|
279
|
+
];
|
|
280
|
+
for (const relative of relationshipFiles) {
|
|
281
|
+
const file = path.join(packageRoot, relative);
|
|
282
|
+
if (!(await exists(file))) { errors.push(`missing routing asset: ${relative}`); continue; }
|
|
283
|
+
const text = await readFile(file, 'utf8');
|
|
284
|
+
for (const id of text.match(/showdar-[a-z-]+/g) ?? []) if (!known.has(id)) errors.push(`${relative} references unknown skill ${id}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const commandMap = {
|
|
288
|
+
understand: 'showdar-understand', plan: 'showdar-plan', design: 'showdar-design', build: 'showdar-build',
|
|
289
|
+
debug: 'showdar-debug', test: 'showdar-test', review: 'showdar-review', upgrade: 'showdar-upgrade',
|
|
290
|
+
ship: 'showdar-ship', recover: 'showdar-recover', git: 'showdar-git',
|
|
291
|
+
requirements: 'showdar-requirements', quality: 'showdar-quality', security: 'showdar-security', ops: 'showdar-ops',
|
|
292
|
+
};
|
|
293
|
+
for (const [command, skill] of Object.entries(commandMap)) {
|
|
294
|
+
const relative = `commands/opencode/showdar/${command}.md`;
|
|
295
|
+
const file = path.join(packageRoot, relative);
|
|
296
|
+
if (!(await exists(file))) { errors.push(`missing OpenCode command: ${relative}`); continue; }
|
|
297
|
+
const text = await readFile(file, 'utf8');
|
|
298
|
+
if (!text.includes('$ARGUMENTS')) errors.push(`${relative} does not accept $ARGUMENTS`);
|
|
299
|
+
if (!text.includes(skill)) errors.push(`${relative} does not route to ${skill}`);
|
|
300
|
+
}
|
|
301
|
+
const skillCommand = path.join(packageRoot, 'commands', 'opencode', 'showdar', 'skill.md');
|
|
302
|
+
if (!(await exists(skillCommand))) errors.push('missing OpenCode command: commands/opencode/showdar/skill.md');
|
|
303
|
+
else {
|
|
304
|
+
const text = await readFile(skillCommand, 'utf8');
|
|
305
|
+
if (!text.includes('$ARGUMENTS')) errors.push('commands/opencode/showdar/skill.md does not accept $ARGUMENTS');
|
|
306
|
+
for (const id of known) if (!text.includes(id)) errors.push(`commands/opencode/showdar/skill.md does not list ${id}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
310
|
+
}
|