@csark0812/skeleton 1.0.0 → 1.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/README.md +134 -3
- package/dist/cli.js +483 -75
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,138 @@
|
|
|
1
1
|
# Skeleton
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Single source of truth (SSOT) linter for agent-enabled repos.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Agent repos accumulate skills, rules, registries, and cross-linked docs faster than anyone can keep them straight by hand. Code repos solved this decades ago with ESLint — deterministic checks, CI gates, fix what you can before merge. Skeleton does the same job for documentation architecture: what's canonical, what links where, what must not exist, and whether your skill overrides are wired correctly.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Skeleton is **not** a runtime agent harness. It does not execute tools, enforce permissions, or manage agent memory. It is a CLI that audits docs, skills, and registries, and fails CI when invariants break.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Code has linters. Agent repos need the same thing for docs and SSOT.
|
|
12
|
+
|
|
13
|
+
| Code repos | Agent repos |
|
|
14
|
+
| ------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
15
|
+
| ESLint catches broken imports, unused vars, style drift | Skeleton catches broken links, missing registry rows, stale doc-meta, banned artifacts |
|
|
16
|
+
| `eslint --fix` on changed files | `skeleton validate changed` on changed docs and skills |
|
|
17
|
+
| Pre-commit + CI gate | `--staged` pre-commit + `--base` CI gate |
|
|
18
|
+
|
|
19
|
+
Skill linters (skillmark, agentlint, skillscheck) answer: _"Is this SKILL.md well-formed?"_ Skeleton answers: _"Does this repo's documentation system hold together?"_
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -D @csark0812/skeleton
|
|
25
|
+
npx skeleton init --skills
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Init writes `.skeleton/`, merges validate scripts into `package.json`, and wires customize hooks for Cursor, Claude Code, and Codex.
|
|
29
|
+
|
|
30
|
+
Edit `.skeleton/config.yaml` for your repo layout, then verify:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx skeleton audit self
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
See [install](docs/developer/install.md) for flags and options.
|
|
37
|
+
|
|
38
|
+
## What it checks
|
|
39
|
+
|
|
40
|
+
- **Registry integrity** — `.skeleton/registry.md` topic → canonical file pointers; banner format on registered docs
|
|
41
|
+
- **Link audit** — broken refs, skill links, anchors in scanned markdown
|
|
42
|
+
- **Skill index** — disk matches taxonomy READMEs in detected skill roots
|
|
43
|
+
- **Banned paths** — session artifacts and other files that must not exist
|
|
44
|
+
- **Coverage gaps** — markdown outside the scan perimeter (warn-only)
|
|
45
|
+
- **Doc meta + stale dates** — owner and `last-reviewed` on index and registry-listed files
|
|
46
|
+
- **Shell / JSON syntax** — lightweight checks on changed `.sh` and `.json` files
|
|
47
|
+
|
|
48
|
+
Code validation (TypeScript, Python, Nx, pytest) stays in your repo. Skeleton handles SSOT-adjacent paths only.
|
|
49
|
+
|
|
50
|
+
## The `.skeleton/` contract
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
.skeleton/
|
|
54
|
+
├── config.yaml # scan perimeter (required)
|
|
55
|
+
├── registry.md # topic → canonical file (required)
|
|
56
|
+
└── customize/ # project-specific skill overrides (optional)
|
|
57
|
+
└── code-review.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Every canonical doc carries a banner:
|
|
61
|
+
|
|
62
|
+
```markdown
|
|
63
|
+
**Source of truth for** Backend API conventions.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Register it:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
skeleton register docs/developer/api.md
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Synced toolbox skills stay pristine. Project overrides live in `.skeleton/customize/<slug>.md` and inject via IDE hooks on skill read — no editing synced `SKILL.md` files.
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
skeleton init [--skills] [--force-hooks]
|
|
78
|
+
skeleton register <path> [--topic=…]
|
|
79
|
+
skeleton audit docs|skills|self [--strict] [--paths=a,b]
|
|
80
|
+
skeleton validate changed [--staged | --base <ref>] [paths…]
|
|
81
|
+
skeleton customize resolve <slug>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Validate changed** routes git diffs to the right audit:
|
|
85
|
+
|
|
86
|
+
| Path | Action |
|
|
87
|
+
| -------------------------------------------- | --------------------------- |
|
|
88
|
+
| Docs/skills in scan perimeter | path-scoped audit |
|
|
89
|
+
| `.sh`, `.bash`, `.zsh` | shellcheck or `bash -n` |
|
|
90
|
+
| Other `.json` | JSONC-tolerant syntax check |
|
|
91
|
+
| `.ts`, `.py`, `package.json`, `project.json` | skip |
|
|
92
|
+
|
|
93
|
+
Pre-commit: `skeleton validate changed --staged` (path-scoped, fast).
|
|
94
|
+
CI: `skeleton validate changed --base origin/main` (global rules first, then changed files).
|
|
95
|
+
|
|
96
|
+
## Ecosystem
|
|
97
|
+
|
|
98
|
+
Skeleton is the shared validation layer in a three-tier setup:
|
|
99
|
+
|
|
100
|
+
| Repo | Role |
|
|
101
|
+
| -------------------- | -------------------------------------------------- |
|
|
102
|
+
| **skeleton** | SSOT audit CLI (this repo) |
|
|
103
|
+
| **toolbox** | Team skills + public agent preferences |
|
|
104
|
+
| **personal-toolbox** | Private skills + personal preferences |
|
|
105
|
+
| **Consumer apps** | Call skeleton for SSOT; keep code validation local |
|
|
106
|
+
|
|
107
|
+
Skeleton never calls Nx or other task runners — consumers call skeleton for doc and skill paths, then handle code paths themselves.
|
|
108
|
+
|
|
109
|
+
See [tiers](docs/tiers.md).
|
|
110
|
+
|
|
111
|
+
## Distribution
|
|
112
|
+
|
|
113
|
+
| Channel | Installs |
|
|
114
|
+
| ---------------------------------------------------- | ------------------------------------------------------- |
|
|
115
|
+
| `npm install -D @csark0812/skeleton` | CLI, schemas, audit engine, hook script |
|
|
116
|
+
| `npx skills add csark0812/skeleton --skill skeleton` | `/skeleton` agent skill (ops manual, not the installer) |
|
|
117
|
+
|
|
118
|
+
One command for humans: `npx skeleton init --skills`.
|
|
119
|
+
|
|
120
|
+
## Docs
|
|
121
|
+
|
|
122
|
+
- [Install](docs/developer/install.md)
|
|
123
|
+
- [Doc system](docs/developer/doc-system.md)
|
|
124
|
+
- [Validation](docs/developer/validation.md)
|
|
125
|
+
- [Audit rules](docs/developer/audit.md)
|
|
126
|
+
- [Customize](docs/developer/customize.md)
|
|
127
|
+
- [Authoring conventions](docs/authoring.md)
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
Requires Node ≥ 22. Uses Bun for dev and tests.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
bun install
|
|
135
|
+
bun test
|
|
136
|
+
bun run build
|
|
137
|
+
bun run audit:self
|
|
138
|
+
```
|
package/dist/cli.js
CHANGED
|
@@ -28582,6 +28582,28 @@ var scanRootsRule = { id: "scan-roots", run: runScanRootsRule };
|
|
|
28582
28582
|
// src/audit/rules/skill-index.ts
|
|
28583
28583
|
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync3 } from "node:fs";
|
|
28584
28584
|
import { join as join8, relative as relative5 } from "node:path";
|
|
28585
|
+
|
|
28586
|
+
// src/references/constants.ts
|
|
28587
|
+
var CANONICAL_REFS_DIR = ".skeleton/references";
|
|
28588
|
+
var GENERATED_MARKER_START = "<!-- skeleton: generated-reference";
|
|
28589
|
+
var GENERATED_MARKER_RE = /<!-- skeleton: generated-reference\s*\nsource: ([^\n]+)\s*\nredundancy: intentional\s*\n-->\s*\n?/;
|
|
28590
|
+
var SHARED_REF_LINK_RE = /\((?:\.\.\/)+references\/([^)]+)\)/g;
|
|
28591
|
+
function formatGeneratedHeader(sourceRelPath) {
|
|
28592
|
+
return `${GENERATED_MARKER_START}
|
|
28593
|
+
source: ${sourceRelPath}
|
|
28594
|
+
redundancy: intentional
|
|
28595
|
+
-->
|
|
28596
|
+
|
|
28597
|
+
`;
|
|
28598
|
+
}
|
|
28599
|
+
function stripGeneratedHeader(content3) {
|
|
28600
|
+
return content3.replace(GENERATED_MARKER_RE, "");
|
|
28601
|
+
}
|
|
28602
|
+
function isGeneratedReference(content3) {
|
|
28603
|
+
return content3.startsWith(GENERATED_MARKER_START);
|
|
28604
|
+
}
|
|
28605
|
+
|
|
28606
|
+
// src/audit/rules/skill-index.ts
|
|
28585
28607
|
var NON_PUBLIC_SLUGS = new Set(["align-commands"]);
|
|
28586
28608
|
function walkSkillMarkdown(dir) {
|
|
28587
28609
|
const files = [];
|
|
@@ -28618,6 +28640,8 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
|
|
|
28618
28640
|
const issues = [];
|
|
28619
28641
|
const rel = relative5(ctx.root, filePath).replace(/\\/g, "/");
|
|
28620
28642
|
const content3 = readFileSync7(filePath, "utf8");
|
|
28643
|
+
if (isGeneratedReference(content3))
|
|
28644
|
+
return issues;
|
|
28621
28645
|
for (const match of content3.matchAll(SKILL_LINK_RE)) {
|
|
28622
28646
|
const slug2 = match[1];
|
|
28623
28647
|
if (!slug2)
|
|
@@ -28690,6 +28714,219 @@ function skillCountOnDisk(ctx) {
|
|
|
28690
28714
|
return listSkillSlugs(ctx.skillIndex).length;
|
|
28691
28715
|
}
|
|
28692
28716
|
|
|
28717
|
+
// src/references/check.ts
|
|
28718
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync5 } from "node:fs";
|
|
28719
|
+
import { join as join10, relative as relative7 } from "node:path";
|
|
28720
|
+
|
|
28721
|
+
// src/references/discover.ts
|
|
28722
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
28723
|
+
import { join as join9, relative as relative6 } from "node:path";
|
|
28724
|
+
function walkMarkdownFiles(dir, root2) {
|
|
28725
|
+
const files = [];
|
|
28726
|
+
if (!existsSync9(dir))
|
|
28727
|
+
return files;
|
|
28728
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
28729
|
+
if (entry.name.startsWith("."))
|
|
28730
|
+
continue;
|
|
28731
|
+
const fullPath = join9(dir, entry.name);
|
|
28732
|
+
if (entry.isDirectory()) {
|
|
28733
|
+
files.push(...walkMarkdownFiles(fullPath, root2));
|
|
28734
|
+
continue;
|
|
28735
|
+
}
|
|
28736
|
+
if (entry.name.endsWith(".md")) {
|
|
28737
|
+
files.push(normalizeRelPath(relative6(root2, fullPath)));
|
|
28738
|
+
}
|
|
28739
|
+
}
|
|
28740
|
+
return files;
|
|
28741
|
+
}
|
|
28742
|
+
function canonicalExists(root2, refPath) {
|
|
28743
|
+
return existsSync9(join9(root2, CANONICAL_REFS_DIR, refPath));
|
|
28744
|
+
}
|
|
28745
|
+
function findSharedRefLinks(content3, sourceFile) {
|
|
28746
|
+
const links = [];
|
|
28747
|
+
for (const match of content3.matchAll(SHARED_REF_LINK_RE)) {
|
|
28748
|
+
const refPath = match[1];
|
|
28749
|
+
if (!refPath)
|
|
28750
|
+
continue;
|
|
28751
|
+
links.push({ refPath: normalizeRelPath(refPath), sourceFile });
|
|
28752
|
+
}
|
|
28753
|
+
return links;
|
|
28754
|
+
}
|
|
28755
|
+
function findLocalCanonicalLinks(root2, content3, sourceFile) {
|
|
28756
|
+
const links = [];
|
|
28757
|
+
const localRefRe = /\((?:\.\/)?references\/([^)]+)\)/g;
|
|
28758
|
+
for (const match of content3.matchAll(localRefRe)) {
|
|
28759
|
+
const refPath = normalizeRelPath(match[1] ?? "");
|
|
28760
|
+
if (!refPath || !canonicalExists(root2, refPath))
|
|
28761
|
+
continue;
|
|
28762
|
+
links.push({ refPath, sourceFile });
|
|
28763
|
+
}
|
|
28764
|
+
const inReferencesDir = /\/references\//.test(sourceFile);
|
|
28765
|
+
if (inReferencesDir) {
|
|
28766
|
+
const siblingRe = /\((?!https?:|#|\.\.\/)([a-z0-9./_-]+\.md)\)/gi;
|
|
28767
|
+
for (const match of content3.matchAll(siblingRe)) {
|
|
28768
|
+
const refPath = normalizeRelPath(match[1] ?? "");
|
|
28769
|
+
if (!refPath || !canonicalExists(root2, refPath))
|
|
28770
|
+
continue;
|
|
28771
|
+
links.push({ refPath, sourceFile });
|
|
28772
|
+
}
|
|
28773
|
+
}
|
|
28774
|
+
return links;
|
|
28775
|
+
}
|
|
28776
|
+
function discoverSkillReferencePlans(root2) {
|
|
28777
|
+
const index2 = buildSkillIndex(root2);
|
|
28778
|
+
const plans = [];
|
|
28779
|
+
for (const slug2 of index2.slugs) {
|
|
28780
|
+
const skillDir = join9(root2, slug2);
|
|
28781
|
+
if (!existsSync9(join9(skillDir, "SKILL.md")))
|
|
28782
|
+
continue;
|
|
28783
|
+
const refPaths = new Set;
|
|
28784
|
+
const links = [];
|
|
28785
|
+
for (const relFile of walkMarkdownFiles(skillDir, root2)) {
|
|
28786
|
+
const content3 = readFileSync8(join9(root2, relFile), "utf8");
|
|
28787
|
+
if (isGeneratedReference(content3))
|
|
28788
|
+
continue;
|
|
28789
|
+
for (const link2 of findSharedRefLinks(content3, relFile)) {
|
|
28790
|
+
refPaths.add(link2.refPath);
|
|
28791
|
+
links.push(link2);
|
|
28792
|
+
}
|
|
28793
|
+
for (const link2 of findLocalCanonicalLinks(root2, content3, relFile)) {
|
|
28794
|
+
refPaths.add(link2.refPath);
|
|
28795
|
+
links.push(link2);
|
|
28796
|
+
}
|
|
28797
|
+
}
|
|
28798
|
+
if (refPaths.size > 0) {
|
|
28799
|
+
plans.push({ skill: slug2, refPaths, links });
|
|
28800
|
+
}
|
|
28801
|
+
}
|
|
28802
|
+
return plans.sort((a, b) => a.skill.localeCompare(b.skill));
|
|
28803
|
+
}
|
|
28804
|
+
function generatedRefPath(skill, refPath) {
|
|
28805
|
+
return normalizeRelPath(join9(skill, "references", refPath));
|
|
28806
|
+
}
|
|
28807
|
+
function rewriteSharedRefTarget(sourceFile, skill, refPath) {
|
|
28808
|
+
const sourceDir = sourceFile.slice(0, sourceFile.lastIndexOf("/"));
|
|
28809
|
+
const target = generatedRefPath(skill, refPath);
|
|
28810
|
+
if (!sourceDir)
|
|
28811
|
+
return target;
|
|
28812
|
+
const fromParts = sourceDir.split("/");
|
|
28813
|
+
const toParts = target.split("/");
|
|
28814
|
+
let i = 0;
|
|
28815
|
+
while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) {
|
|
28816
|
+
i++;
|
|
28817
|
+
}
|
|
28818
|
+
const ups = fromParts.length - i;
|
|
28819
|
+
const down = toParts.slice(i);
|
|
28820
|
+
const rel = [...Array(ups).fill(".."), ...down].join("/");
|
|
28821
|
+
return rel || (toParts.at(-1) ?? refPath);
|
|
28822
|
+
}
|
|
28823
|
+
function rewriteSharedRefLinks(content3, sourceFile, skill) {
|
|
28824
|
+
return content3.replace(SHARED_REF_LINK_RE, (_match, refPath) => {
|
|
28825
|
+
const rewritten = rewriteSharedRefTarget(sourceFile, skill, normalizeRelPath(refPath));
|
|
28826
|
+
return `(${rewritten})`;
|
|
28827
|
+
});
|
|
28828
|
+
}
|
|
28829
|
+
|
|
28830
|
+
// src/references/check.ts
|
|
28831
|
+
function listAllGeneratedFiles(root2) {
|
|
28832
|
+
const files = [];
|
|
28833
|
+
const walk = (dir) => {
|
|
28834
|
+
if (!existsSync10(dir))
|
|
28835
|
+
return;
|
|
28836
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
28837
|
+
if (entry.name.startsWith("."))
|
|
28838
|
+
continue;
|
|
28839
|
+
const fullPath = join10(dir, entry.name);
|
|
28840
|
+
if (entry.isDirectory()) {
|
|
28841
|
+
walk(fullPath);
|
|
28842
|
+
continue;
|
|
28843
|
+
}
|
|
28844
|
+
if (!entry.name.endsWith(".md"))
|
|
28845
|
+
continue;
|
|
28846
|
+
const content3 = readFileSync9(fullPath, "utf8");
|
|
28847
|
+
if (isGeneratedReference(content3)) {
|
|
28848
|
+
files.push(normalizeRelPath(relative7(root2, fullPath)));
|
|
28849
|
+
}
|
|
28850
|
+
}
|
|
28851
|
+
};
|
|
28852
|
+
walk(root2);
|
|
28853
|
+
return files;
|
|
28854
|
+
}
|
|
28855
|
+
function runGeneratedReferencesCheck(root2) {
|
|
28856
|
+
const issues = [];
|
|
28857
|
+
const canonicalDir = join10(root2, CANONICAL_REFS_DIR);
|
|
28858
|
+
if (!existsSync10(canonicalDir))
|
|
28859
|
+
return issues;
|
|
28860
|
+
const plans = discoverSkillReferencePlans(root2);
|
|
28861
|
+
const needed = new Set;
|
|
28862
|
+
for (const plan of plans) {
|
|
28863
|
+
for (const refPath of plan.refPaths) {
|
|
28864
|
+
needed.add(generatedRefPath(plan.skill, refPath));
|
|
28865
|
+
}
|
|
28866
|
+
}
|
|
28867
|
+
for (const targetRel of needed) {
|
|
28868
|
+
const targetPath = join10(root2, targetRel);
|
|
28869
|
+
if (!existsSync10(targetPath)) {
|
|
28870
|
+
issues.push(issue("generated-references", targetRel, "missing generated copy — run skeleton references sync"));
|
|
28871
|
+
continue;
|
|
28872
|
+
}
|
|
28873
|
+
const generated = readFileSync9(targetPath, "utf8");
|
|
28874
|
+
if (!isGeneratedReference(generated)) {
|
|
28875
|
+
issues.push(issue("generated-references", targetRel, "expected generated-reference provenance header"));
|
|
28876
|
+
continue;
|
|
28877
|
+
}
|
|
28878
|
+
const body = stripGeneratedHeader(generated);
|
|
28879
|
+
const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join10(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
|
|
28880
|
+
const canonicalPath = join10(root2, sourceRel);
|
|
28881
|
+
if (!existsSync10(canonicalPath)) {
|
|
28882
|
+
issues.push(issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`));
|
|
28883
|
+
continue;
|
|
28884
|
+
}
|
|
28885
|
+
const canonical = readFileSync9(canonicalPath, "utf8");
|
|
28886
|
+
if (body !== canonical) {
|
|
28887
|
+
issues.push(issue("generated-references", targetRel, "stale generated copy — run skeleton references sync"));
|
|
28888
|
+
}
|
|
28889
|
+
}
|
|
28890
|
+
for (const generatedRel of listAllGeneratedFiles(root2)) {
|
|
28891
|
+
if (!needed.has(generatedRel)) {
|
|
28892
|
+
issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
|
|
28893
|
+
}
|
|
28894
|
+
}
|
|
28895
|
+
for (const plan of plans) {
|
|
28896
|
+
const skillDir = join10(root2, plan.skill);
|
|
28897
|
+
if (!existsSync10(skillDir))
|
|
28898
|
+
continue;
|
|
28899
|
+
const walk = (dir) => {
|
|
28900
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
28901
|
+
if (entry.name.startsWith("."))
|
|
28902
|
+
continue;
|
|
28903
|
+
const fullPath = join10(dir, entry.name);
|
|
28904
|
+
if (entry.isDirectory()) {
|
|
28905
|
+
walk(fullPath);
|
|
28906
|
+
continue;
|
|
28907
|
+
}
|
|
28908
|
+
if (!entry.name.endsWith(".md"))
|
|
28909
|
+
continue;
|
|
28910
|
+
const relFile = normalizeRelPath(relative7(root2, fullPath));
|
|
28911
|
+
const content3 = readFileSync9(fullPath, "utf8");
|
|
28912
|
+
if (content3.match(SHARED_REF_LINK_RE)) {
|
|
28913
|
+
issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
|
|
28914
|
+
}
|
|
28915
|
+
}
|
|
28916
|
+
};
|
|
28917
|
+
walk(skillDir);
|
|
28918
|
+
}
|
|
28919
|
+
return issues;
|
|
28920
|
+
}
|
|
28921
|
+
function runGeneratedReferencesRule(ctx) {
|
|
28922
|
+
return runGeneratedReferencesCheck(ctx.root);
|
|
28923
|
+
}
|
|
28924
|
+
var generatedReferencesRule = {
|
|
28925
|
+
id: "generated-references",
|
|
28926
|
+
global: true,
|
|
28927
|
+
run: runGeneratedReferencesRule
|
|
28928
|
+
};
|
|
28929
|
+
|
|
28693
28930
|
// src/audit/rules/index.ts
|
|
28694
28931
|
var docsRules = [
|
|
28695
28932
|
{ ...scanRootsRule, global: true },
|
|
@@ -28699,7 +28936,10 @@ var docsRules = [
|
|
|
28699
28936
|
docMetaRule,
|
|
28700
28937
|
{ ...bannedRule, global: true }
|
|
28701
28938
|
];
|
|
28702
|
-
var skillsRules = [
|
|
28939
|
+
var skillsRules = [
|
|
28940
|
+
{ ...skillIndexRule, global: true },
|
|
28941
|
+
{ ...generatedReferencesRule, global: true }
|
|
28942
|
+
];
|
|
28703
28943
|
var allRules = [...docsRules, ...skillsRules];
|
|
28704
28944
|
function rulesForSuite(suite) {
|
|
28705
28945
|
switch (suite) {
|
|
@@ -28781,16 +29021,16 @@ function runAudit(options) {
|
|
|
28781
29021
|
}
|
|
28782
29022
|
|
|
28783
29023
|
// src/customize/resolve.ts
|
|
28784
|
-
import { existsSync as
|
|
28785
|
-
import { join as
|
|
29024
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
|
|
29025
|
+
import { join as join11, relative as relative8 } from "node:path";
|
|
28786
29026
|
var CUSTOMIZE_PREFIX = "Customize: ";
|
|
28787
29027
|
function customizePathForSlug(root2, slug2) {
|
|
28788
|
-
return
|
|
29028
|
+
return join11(root2, REGISTRY_DIR_REL, "customize", `${slug2}.md`);
|
|
28789
29029
|
}
|
|
28790
29030
|
function findCustomizeViaRegistry(root2, slug2) {
|
|
28791
29031
|
for (const rel of parseRegistryPaths(root2)) {
|
|
28792
29032
|
const expected = `${REGISTRY_DIR_REL}/customize/${slug2}.md`;
|
|
28793
|
-
if (normalizeRelPath(rel) === expected &&
|
|
29033
|
+
if (normalizeRelPath(rel) === expected && existsSync11(join11(root2, rel))) {
|
|
28794
29034
|
return rel;
|
|
28795
29035
|
}
|
|
28796
29036
|
}
|
|
@@ -28798,19 +29038,19 @@ function findCustomizeViaRegistry(root2, slug2) {
|
|
|
28798
29038
|
}
|
|
28799
29039
|
function resolveCustomize(root2, slug2) {
|
|
28800
29040
|
const direct = customizePathForSlug(root2, slug2);
|
|
28801
|
-
if (
|
|
29041
|
+
if (existsSync11(direct)) {
|
|
28802
29042
|
return {
|
|
28803
29043
|
slug: slug2,
|
|
28804
|
-
content:
|
|
28805
|
-
path: normalizeRelPath(
|
|
29044
|
+
content: readFileSync10(direct, "utf8"),
|
|
29045
|
+
path: normalizeRelPath(relative8(root2, direct))
|
|
28806
29046
|
};
|
|
28807
29047
|
}
|
|
28808
29048
|
const registryPath = findCustomizeViaRegistry(root2, slug2);
|
|
28809
29049
|
if (registryPath) {
|
|
28810
|
-
const abs =
|
|
29050
|
+
const abs = join11(root2, registryPath);
|
|
28811
29051
|
return {
|
|
28812
29052
|
slug: slug2,
|
|
28813
|
-
content:
|
|
29053
|
+
content: readFileSync10(abs, "utf8"),
|
|
28814
29054
|
path: registryPath
|
|
28815
29055
|
};
|
|
28816
29056
|
}
|
|
@@ -28823,37 +29063,37 @@ function resolveCustomizeFromRoot(slug2, startDir) {
|
|
|
28823
29063
|
|
|
28824
29064
|
// src/init/init.ts
|
|
28825
29065
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
28826
|
-
import { copyFileSync, existsSync as
|
|
28827
|
-
import { join as
|
|
29066
|
+
import { copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync2, readFileSync as readFileSync12 } from "node:fs";
|
|
29067
|
+
import { join as join15 } from "node:path";
|
|
28828
29068
|
|
|
28829
29069
|
// src/init/merge-hooks.ts
|
|
28830
|
-
import { existsSync as
|
|
28831
|
-
import { dirname as dirname6, join as
|
|
29070
|
+
import { existsSync as existsSync14, mkdirSync, readFileSync as readFileSync11, writeFileSync } from "node:fs";
|
|
29071
|
+
import { dirname as dirname6, join as join14 } from "node:path";
|
|
28832
29072
|
|
|
28833
29073
|
// src/init/resolve-hook-command.ts
|
|
28834
|
-
import { existsSync as
|
|
29074
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
28835
29075
|
import { createRequire as createRequire3 } from "node:module";
|
|
28836
|
-
import { dirname as dirname5, join as
|
|
29076
|
+
import { dirname as dirname5, join as join13, relative as relative9, resolve as resolve5 } from "node:path";
|
|
28837
29077
|
|
|
28838
29078
|
// src/init/package-paths.ts
|
|
28839
|
-
import { existsSync as
|
|
28840
|
-
import { dirname as dirname4, join as
|
|
29079
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
29080
|
+
import { dirname as dirname4, join as join12 } from "node:path";
|
|
28841
29081
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
28842
29082
|
var MODULE_DIR = dirname4(fileURLToPath4(import.meta.url));
|
|
28843
29083
|
var PACKAGE_ROOT_CANDIDATES = [
|
|
28844
|
-
|
|
28845
|
-
|
|
29084
|
+
join12(MODULE_DIR, "../.."),
|
|
29085
|
+
join12(MODULE_DIR, "..")
|
|
28846
29086
|
];
|
|
28847
29087
|
function resolvePackageRoot() {
|
|
28848
29088
|
for (const candidate of PACKAGE_ROOT_CANDIDATES) {
|
|
28849
|
-
if (
|
|
29089
|
+
if (existsSync12(join12(candidate, "package.json")))
|
|
28850
29090
|
return candidate;
|
|
28851
29091
|
}
|
|
28852
29092
|
throw new Error("Could not resolve @csark0812/skeleton package root");
|
|
28853
29093
|
}
|
|
28854
29094
|
function resolveTemplatesDir() {
|
|
28855
|
-
const dir =
|
|
28856
|
-
if (!
|
|
29095
|
+
const dir = join12(resolvePackageRoot(), "templates/skeleton-init");
|
|
29096
|
+
if (!existsSync12(dir)) {
|
|
28857
29097
|
throw new Error("Missing templates/skeleton-init in package");
|
|
28858
29098
|
}
|
|
28859
29099
|
return dir;
|
|
@@ -28865,12 +29105,12 @@ var HOOK_DIST = "dist/hooks/customize-on-skill-read.js";
|
|
|
28865
29105
|
var HOOK_SRC = "src/hooks/customize-on-skill-read.ts";
|
|
28866
29106
|
var PACKAGE_ROOT = resolvePackageRoot();
|
|
28867
29107
|
function toRepoRelative(cwd, absPath) {
|
|
28868
|
-
const rel =
|
|
29108
|
+
const rel = relative9(cwd, absPath).replace(/\\/g, "/");
|
|
28869
29109
|
return rel.startsWith("..") ? absPath.replace(/\\/g, "/") : rel;
|
|
28870
29110
|
}
|
|
28871
29111
|
function tryResolvePublished(cwd) {
|
|
28872
29112
|
try {
|
|
28873
|
-
const req = createRequire3(
|
|
29113
|
+
const req = createRequire3(join13(cwd, "package.json"));
|
|
28874
29114
|
return req.resolve(`${PACKAGE_NAME}/${HOOK_DIST}`);
|
|
28875
29115
|
} catch {
|
|
28876
29116
|
return null;
|
|
@@ -28879,8 +29119,8 @@ function tryResolvePublished(cwd) {
|
|
|
28879
29119
|
function walkNodeModules(cwd) {
|
|
28880
29120
|
let dir = cwd;
|
|
28881
29121
|
while (true) {
|
|
28882
|
-
const candidate =
|
|
28883
|
-
if (
|
|
29122
|
+
const candidate = join13(dir, "node_modules", PACKAGE_NAME, HOOK_DIST);
|
|
29123
|
+
if (existsSync13(candidate))
|
|
28884
29124
|
return candidate;
|
|
28885
29125
|
const parent = dirname5(dir);
|
|
28886
29126
|
if (parent === dir)
|
|
@@ -28890,7 +29130,7 @@ function walkNodeModules(cwd) {
|
|
|
28890
29130
|
return null;
|
|
28891
29131
|
}
|
|
28892
29132
|
function isInsidePackageRoot(cwd) {
|
|
28893
|
-
const rel =
|
|
29133
|
+
const rel = relative9(PACKAGE_ROOT, resolve5(cwd)).replace(/\\/g, "/");
|
|
28894
29134
|
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
|
|
28895
29135
|
}
|
|
28896
29136
|
function resolveHookCommand(cwd) {
|
|
@@ -28901,11 +29141,11 @@ function resolveHookCommand(cwd) {
|
|
|
28901
29141
|
if (hoisted)
|
|
28902
29142
|
return toRepoRelative(cwd, hoisted);
|
|
28903
29143
|
if (isInsidePackageRoot(cwd)) {
|
|
28904
|
-
const distHook =
|
|
28905
|
-
if (
|
|
29144
|
+
const distHook = join13(PACKAGE_ROOT, HOOK_DIST);
|
|
29145
|
+
if (existsSync13(distHook))
|
|
28906
29146
|
return toRepoRelative(cwd, distHook);
|
|
28907
|
-
const srcHook =
|
|
28908
|
-
if (
|
|
29147
|
+
const srcHook = join13(PACKAGE_ROOT, HOOK_SRC);
|
|
29148
|
+
if (existsSync13(srcHook)) {
|
|
28909
29149
|
const rel = toRepoRelative(cwd, srcHook);
|
|
28910
29150
|
return rel.includes("/") ? `bun ${rel}` : `bun ./${rel}`;
|
|
28911
29151
|
}
|
|
@@ -28924,14 +29164,14 @@ function identityKey(platform, event, matcher) {
|
|
|
28924
29164
|
return `skeleton:customize:${platform}:${event}:${matcher}`;
|
|
28925
29165
|
}
|
|
28926
29166
|
function loadFragment(name, hookCommand) {
|
|
28927
|
-
const raw =
|
|
29167
|
+
const raw = readFileSync11(join14(TEMPLATES_DIR, name), "utf8");
|
|
28928
29168
|
return JSON.parse(raw.replaceAll("{{HOOK_COMMAND}}", hookCommand));
|
|
28929
29169
|
}
|
|
28930
29170
|
function readJson(path2) {
|
|
28931
|
-
if (!
|
|
29171
|
+
if (!existsSync14(path2))
|
|
28932
29172
|
return null;
|
|
28933
29173
|
try {
|
|
28934
|
-
return JSON.parse(
|
|
29174
|
+
return JSON.parse(readFileSync11(path2, "utf8"));
|
|
28935
29175
|
} catch (error) {
|
|
28936
29176
|
throw new Error(`Invalid JSON in ${path2}: ${error}`);
|
|
28937
29177
|
}
|
|
@@ -29037,14 +29277,14 @@ function mergeNestedHooks(platform, targetPath, fragment, eventName, opts) {
|
|
|
29037
29277
|
}
|
|
29038
29278
|
function mergeHookConfigs(opts) {
|
|
29039
29279
|
const results = [];
|
|
29040
|
-
const cursorPath =
|
|
29280
|
+
const cursorPath = join14(opts.cwd, ".cursor/hooks.json");
|
|
29041
29281
|
const cursorFragment = loadFragment("cursor-hooks.fragment.json", opts.hookCommand);
|
|
29042
29282
|
results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
|
|
29043
|
-
const claudePath =
|
|
29283
|
+
const claudePath = join14(opts.cwd, ".claude/settings.json");
|
|
29044
29284
|
const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
|
|
29045
29285
|
results.push(mergeNestedHooks("claude", claudePath, claudeFragment, "PostToolUse", opts));
|
|
29046
|
-
const codexPath =
|
|
29047
|
-
if (
|
|
29286
|
+
const codexPath = join14(opts.cwd, ".codex/hooks.json");
|
|
29287
|
+
if (existsSync14(join14(opts.cwd, ".codex"))) {
|
|
29048
29288
|
const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
|
|
29049
29289
|
results.push(mergeNestedHooks("codex", codexPath, codexFragment, "PostToolUse", opts));
|
|
29050
29290
|
} else {
|
|
@@ -29053,11 +29293,11 @@ function mergeHookConfigs(opts) {
|
|
|
29053
29293
|
return results;
|
|
29054
29294
|
}
|
|
29055
29295
|
function mergePackageJsonScripts(cwd) {
|
|
29056
|
-
const pkgPath =
|
|
29057
|
-
if (!
|
|
29296
|
+
const pkgPath = join14(cwd, "package.json");
|
|
29297
|
+
if (!existsSync14(pkgPath))
|
|
29058
29298
|
return "skipped";
|
|
29059
|
-
const fragment = JSON.parse(
|
|
29060
|
-
const pkg = JSON.parse(
|
|
29299
|
+
const fragment = JSON.parse(readFileSync11(join14(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
|
|
29300
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
|
|
29061
29301
|
pkg.scripts ??= {};
|
|
29062
29302
|
let changed = false;
|
|
29063
29303
|
for (const [key, value] of Object.entries(fragment)) {
|
|
@@ -29113,27 +29353,27 @@ function skillsAddArgs(options = {}) {
|
|
|
29113
29353
|
// src/init/init.ts
|
|
29114
29354
|
var TEMPLATES_DIR2 = resolveTemplatesDir();
|
|
29115
29355
|
function writeScaffold(cwd) {
|
|
29116
|
-
const skeletonDir =
|
|
29356
|
+
const skeletonDir = join15(cwd, ".skeleton");
|
|
29117
29357
|
mkdirSync2(skeletonDir, { recursive: true });
|
|
29118
29358
|
let created = false;
|
|
29119
|
-
const configPath =
|
|
29120
|
-
if (!
|
|
29121
|
-
copyFileSync(
|
|
29359
|
+
const configPath = join15(skeletonDir, "config.yaml");
|
|
29360
|
+
if (!existsSync15(configPath)) {
|
|
29361
|
+
copyFileSync(join15(TEMPLATES_DIR2, "config.yaml"), configPath);
|
|
29122
29362
|
created = true;
|
|
29123
29363
|
}
|
|
29124
|
-
const registryPath =
|
|
29125
|
-
if (!
|
|
29126
|
-
copyFileSync(
|
|
29364
|
+
const registryPath = join15(skeletonDir, "registry.md");
|
|
29365
|
+
if (!existsSync15(registryPath)) {
|
|
29366
|
+
copyFileSync(join15(TEMPLATES_DIR2, "registry.md"), registryPath);
|
|
29127
29367
|
created = true;
|
|
29128
29368
|
}
|
|
29129
|
-
mkdirSync2(
|
|
29369
|
+
mkdirSync2(join15(skeletonDir, "customize"), { recursive: true });
|
|
29130
29370
|
return created ? "created" : "skipped";
|
|
29131
29371
|
}
|
|
29132
29372
|
function assertPackageResolvable(cwd) {
|
|
29133
|
-
const pkgPath =
|
|
29134
|
-
if (!
|
|
29373
|
+
const pkgPath = join15(cwd, "package.json");
|
|
29374
|
+
if (!existsSync15(pkgPath))
|
|
29135
29375
|
return;
|
|
29136
|
-
const pkg = JSON.parse(
|
|
29376
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
|
|
29137
29377
|
const hasDep = pkg.devDependencies?.["@csark0812/skeleton"] || pkg.dependencies?.["@csark0812/skeleton"];
|
|
29138
29378
|
if (!hasDep) {
|
|
29139
29379
|
try {
|
|
@@ -29219,8 +29459,8 @@ function parseInitArgs(argv) {
|
|
|
29219
29459
|
}
|
|
29220
29460
|
|
|
29221
29461
|
// src/register.ts
|
|
29222
|
-
import { existsSync as
|
|
29223
|
-
import { dirname as dirname7, join as
|
|
29462
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync2 } from "node:fs";
|
|
29463
|
+
import { dirname as dirname7, join as join16, relative as relative10 } from "node:path";
|
|
29224
29464
|
var REGISTRY_TABLE_ROW_RE2 = /^\|\s*([^|]+)\|\s*\[[^\]]*\]\(([^)]+)\)\s*\|/;
|
|
29225
29465
|
var REGISTRY_TABLE_HEADER = "| Topic | Canonical file |";
|
|
29226
29466
|
function extractTopic(content3) {
|
|
@@ -29228,8 +29468,8 @@ function extractTopic(content3) {
|
|
|
29228
29468
|
return match?.[1]?.trim().replace(/\s+$/, "") ?? null;
|
|
29229
29469
|
}
|
|
29230
29470
|
function toRegistryLink(root2, absPath) {
|
|
29231
|
-
const fromRegistry =
|
|
29232
|
-
return normalizeRelPath(
|
|
29471
|
+
const fromRegistry = join16(root2, REGISTRY_DIR_REL);
|
|
29472
|
+
return normalizeRelPath(relative10(fromRegistry, absPath));
|
|
29233
29473
|
}
|
|
29234
29474
|
function inferSection(registryLink) {
|
|
29235
29475
|
return registryLink.startsWith("customize/") ? "Customizations" : "Documentation";
|
|
@@ -29253,12 +29493,12 @@ function parseRegistryRows(content3) {
|
|
|
29253
29493
|
return rows;
|
|
29254
29494
|
}
|
|
29255
29495
|
function pathFromRegistryLink(root2, link2) {
|
|
29256
|
-
return normalizeRelPath(
|
|
29496
|
+
return normalizeRelPath(relative10(root2, join16(root2, REGISTRY_DIR_REL, link2)));
|
|
29257
29497
|
}
|
|
29258
29498
|
function isOutsideScan(root2, relPath2) {
|
|
29259
29499
|
const config = loadConfig(root2);
|
|
29260
29500
|
const skillIndex = buildSkillIndex(root2);
|
|
29261
|
-
const scanned = collectScanFiles(config, root2, skillIndex).map((abs) => normalizeRelPath(
|
|
29501
|
+
const scanned = collectScanFiles(config, root2, skillIndex).map((abs) => normalizeRelPath(relative10(root2, abs)));
|
|
29262
29502
|
if (scanned.includes(relPath2))
|
|
29263
29503
|
return false;
|
|
29264
29504
|
return !config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
|
|
@@ -29320,11 +29560,11 @@ ${newLine}
|
|
|
29320
29560
|
function registerPath(options) {
|
|
29321
29561
|
const root2 = options.root ?? findRepoRoot();
|
|
29322
29562
|
const relPath2 = normalizeRelPath(options.path);
|
|
29323
|
-
const absPath =
|
|
29324
|
-
if (!
|
|
29563
|
+
const absPath = join16(root2, relPath2);
|
|
29564
|
+
if (!existsSync16(absPath)) {
|
|
29325
29565
|
throw new Error(`File not found: ${relPath2}`);
|
|
29326
29566
|
}
|
|
29327
|
-
const content3 =
|
|
29567
|
+
const content3 = readFileSync13(absPath, "utf8");
|
|
29328
29568
|
let topic = options.topic ?? extractTopic(content3);
|
|
29329
29569
|
if (!topic) {
|
|
29330
29570
|
throw new Error(`No **Source of truth for** banner in ${relPath2} — add banner or pass --topic`);
|
|
@@ -29332,9 +29572,9 @@ function registerPath(options) {
|
|
|
29332
29572
|
const registryLink = toRegistryLink(root2, absPath);
|
|
29333
29573
|
topic = ensureCustomizeTopic(topic, registryLink);
|
|
29334
29574
|
const section = inferSection(registryLink);
|
|
29335
|
-
const registryAbs =
|
|
29336
|
-
let registryContent =
|
|
29337
|
-
if (!
|
|
29575
|
+
const registryAbs = join16(root2, REGISTRY_REL_PATH);
|
|
29576
|
+
let registryContent = existsSync16(registryAbs) ? readFileSync13(registryAbs, "utf8") : defaultRegistryContent();
|
|
29577
|
+
if (!existsSync16(registryAbs) && !existsSync16(join16(root2, ".skeleton/config.yaml"))) {
|
|
29338
29578
|
throw new Error("Missing .skeleton/config.yaml — run skeleton init first");
|
|
29339
29579
|
}
|
|
29340
29580
|
const { content: updated, action } = upsertRow(registryContent, topic, registryLink, section, root2);
|
|
@@ -29348,7 +29588,7 @@ function registerPath(options) {
|
|
|
29348
29588
|
};
|
|
29349
29589
|
if (!options.dryRun && action !== "noop") {
|
|
29350
29590
|
const dir = dirname7(registryAbs);
|
|
29351
|
-
if (!
|
|
29591
|
+
if (!existsSync16(dir)) {
|
|
29352
29592
|
throw new Error(`Missing ${REGISTRY_DIR_REL}/ directory`);
|
|
29353
29593
|
}
|
|
29354
29594
|
writeFileSync2(registryAbs, registryContent, "utf8");
|
|
@@ -29369,8 +29609,8 @@ function registerPath(options) {
|
|
|
29369
29609
|
}
|
|
29370
29610
|
|
|
29371
29611
|
// src/validate/changed.ts
|
|
29372
|
-
import { existsSync as
|
|
29373
|
-
import { basename as basename2, extname, join as
|
|
29612
|
+
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
|
|
29613
|
+
import { basename as basename2, extname, join as join17 } from "node:path";
|
|
29374
29614
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
29375
29615
|
|
|
29376
29616
|
// src/validate/git-diff.ts
|
|
@@ -29437,9 +29677,9 @@ function parseJsonContent(content3) {
|
|
|
29437
29677
|
}
|
|
29438
29678
|
}
|
|
29439
29679
|
function validateJson(relPath2, root2) {
|
|
29440
|
-
const abs =
|
|
29680
|
+
const abs = join17(root2, relPath2);
|
|
29441
29681
|
try {
|
|
29442
|
-
parseJsonContent(
|
|
29682
|
+
parseJsonContent(readFileSync14(abs, "utf8"));
|
|
29443
29683
|
return 0;
|
|
29444
29684
|
} catch (error) {
|
|
29445
29685
|
console.error(`validate changed: invalid JSON in ${relPath2}: ${error}`);
|
|
@@ -29447,7 +29687,7 @@ function validateJson(relPath2, root2) {
|
|
|
29447
29687
|
}
|
|
29448
29688
|
}
|
|
29449
29689
|
function validateShell(relPath2, root2) {
|
|
29450
|
-
const abs =
|
|
29690
|
+
const abs = join17(root2, relPath2);
|
|
29451
29691
|
const shellcheck = spawnSync4("shellcheck", [abs], { encoding: "utf8" });
|
|
29452
29692
|
if (shellcheck.status === 0)
|
|
29453
29693
|
return 0;
|
|
@@ -29482,8 +29722,8 @@ function runValidateChanged(options = {}) {
|
|
|
29482
29722
|
};
|
|
29483
29723
|
let skipped = 0;
|
|
29484
29724
|
for (const relPath2 of relPaths) {
|
|
29485
|
-
const abs =
|
|
29486
|
-
if (!
|
|
29725
|
+
const abs = join17(root2, relPath2);
|
|
29726
|
+
if (!existsSync17(abs))
|
|
29487
29727
|
continue;
|
|
29488
29728
|
const bucket = bucketFor(relPath2, root2);
|
|
29489
29729
|
if (bucket === "skip") {
|
|
@@ -29547,6 +29787,154 @@ function runValidateChanged(options = {}) {
|
|
|
29547
29787
|
return exitCode;
|
|
29548
29788
|
}
|
|
29549
29789
|
|
|
29790
|
+
// src/references/sync.ts
|
|
29791
|
+
import {
|
|
29792
|
+
existsSync as existsSync18,
|
|
29793
|
+
mkdirSync as mkdirSync3,
|
|
29794
|
+
readFileSync as readFileSync15,
|
|
29795
|
+
readdirSync as readdirSync6,
|
|
29796
|
+
unlinkSync,
|
|
29797
|
+
writeFileSync as writeFileSync3
|
|
29798
|
+
} from "node:fs";
|
|
29799
|
+
import { dirname as dirname8, join as join18, relative as relative11 } from "node:path";
|
|
29800
|
+
function walkMarkdownFiles2(dir, root2) {
|
|
29801
|
+
const files = [];
|
|
29802
|
+
if (!existsSync18(dir))
|
|
29803
|
+
return files;
|
|
29804
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
29805
|
+
if (entry.name.startsWith("."))
|
|
29806
|
+
continue;
|
|
29807
|
+
const fullPath = join18(dir, entry.name);
|
|
29808
|
+
if (entry.isDirectory()) {
|
|
29809
|
+
files.push(...walkMarkdownFiles2(fullPath, root2));
|
|
29810
|
+
continue;
|
|
29811
|
+
}
|
|
29812
|
+
if (entry.name.endsWith(".md")) {
|
|
29813
|
+
files.push(normalizeRelPath(relative11(root2, fullPath)));
|
|
29814
|
+
}
|
|
29815
|
+
}
|
|
29816
|
+
return files;
|
|
29817
|
+
}
|
|
29818
|
+
function listGeneratedReferenceFiles(skillDir, skill) {
|
|
29819
|
+
const refsDir = join18(skillDir, "references");
|
|
29820
|
+
if (!existsSync18(refsDir))
|
|
29821
|
+
return [];
|
|
29822
|
+
const files = [];
|
|
29823
|
+
const walk = (dir) => {
|
|
29824
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
29825
|
+
const fullPath = join18(dir, entry.name);
|
|
29826
|
+
if (entry.isDirectory()) {
|
|
29827
|
+
walk(fullPath);
|
|
29828
|
+
continue;
|
|
29829
|
+
}
|
|
29830
|
+
if (!entry.name.endsWith(".md"))
|
|
29831
|
+
continue;
|
|
29832
|
+
const content3 = readFileSync15(fullPath, "utf8");
|
|
29833
|
+
if (isGeneratedReference(content3)) {
|
|
29834
|
+
const refPath = normalizeRelPath(relative11(refsDir, fullPath));
|
|
29835
|
+
files.push(generatedRefPath(skill, refPath));
|
|
29836
|
+
}
|
|
29837
|
+
}
|
|
29838
|
+
};
|
|
29839
|
+
walk(refsDir);
|
|
29840
|
+
return files;
|
|
29841
|
+
}
|
|
29842
|
+
function syncReferences(options = {}) {
|
|
29843
|
+
const root2 = options.root ?? process.cwd();
|
|
29844
|
+
const canonicalDir = join18(root2, CANONICAL_REFS_DIR);
|
|
29845
|
+
if (!existsSync18(canonicalDir)) {
|
|
29846
|
+
throw new Error(`canonical references dir not found: ${CANONICAL_REFS_DIR}`);
|
|
29847
|
+
}
|
|
29848
|
+
const result = {
|
|
29849
|
+
written: [],
|
|
29850
|
+
rewritten: [],
|
|
29851
|
+
removed: [],
|
|
29852
|
+
skipped: []
|
|
29853
|
+
};
|
|
29854
|
+
const plans = discoverSkillReferencePlans(root2);
|
|
29855
|
+
for (const plan of plans) {
|
|
29856
|
+
const skillDir = join18(root2, plan.skill);
|
|
29857
|
+
for (const refPath of plan.refPaths) {
|
|
29858
|
+
const sourceRel = normalizeRelPath(join18(CANONICAL_REFS_DIR, refPath));
|
|
29859
|
+
const canonicalPath = join18(root2, sourceRel);
|
|
29860
|
+
if (!existsSync18(canonicalPath)) {
|
|
29861
|
+
throw new Error(`canonical reference missing: ${sourceRel}`);
|
|
29862
|
+
}
|
|
29863
|
+
const targetRel = generatedRefPath(plan.skill, refPath);
|
|
29864
|
+
const targetPath = join18(root2, targetRel);
|
|
29865
|
+
const canonicalContent = readFileSync15(canonicalPath, "utf8");
|
|
29866
|
+
const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
|
|
29867
|
+
if (!options.dryRun) {
|
|
29868
|
+
mkdirSync3(dirname8(targetPath), { recursive: true });
|
|
29869
|
+
}
|
|
29870
|
+
const existing = existsSync18(targetPath) ? readFileSync15(targetPath, "utf8") : null;
|
|
29871
|
+
if (existing !== nextContent) {
|
|
29872
|
+
if (!options.dryRun)
|
|
29873
|
+
writeFileSync3(targetPath, nextContent, "utf8");
|
|
29874
|
+
result.written.push(targetRel);
|
|
29875
|
+
} else {
|
|
29876
|
+
result.skipped.push(targetRel);
|
|
29877
|
+
}
|
|
29878
|
+
}
|
|
29879
|
+
if (options.rewriteLinks !== false) {
|
|
29880
|
+
for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
|
|
29881
|
+
const filePath = join18(root2, relFile);
|
|
29882
|
+
const content3 = readFileSync15(filePath, "utf8");
|
|
29883
|
+
const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
|
|
29884
|
+
if (next !== content3) {
|
|
29885
|
+
if (!options.dryRun)
|
|
29886
|
+
writeFileSync3(filePath, next, "utf8");
|
|
29887
|
+
result.rewritten.push(relFile);
|
|
29888
|
+
}
|
|
29889
|
+
}
|
|
29890
|
+
}
|
|
29891
|
+
for (const generatedRel of listGeneratedReferenceFiles(skillDir, plan.skill)) {
|
|
29892
|
+
const refPath = generatedRel.slice(`${plan.skill}/references/`.length);
|
|
29893
|
+
if (!plan.refPaths.has(refPath)) {
|
|
29894
|
+
const fullPath = join18(root2, generatedRel);
|
|
29895
|
+
if (!options.dryRun)
|
|
29896
|
+
unlinkSync(fullPath);
|
|
29897
|
+
result.removed.push(generatedRel);
|
|
29898
|
+
}
|
|
29899
|
+
}
|
|
29900
|
+
}
|
|
29901
|
+
return result;
|
|
29902
|
+
}
|
|
29903
|
+
|
|
29904
|
+
// src/references/run.ts
|
|
29905
|
+
function runReferencesSync(options = {}) {
|
|
29906
|
+
return syncReferences(options);
|
|
29907
|
+
}
|
|
29908
|
+
function runReferencesCheck(options = {}) {
|
|
29909
|
+
const root2 = options.root ?? process.cwd();
|
|
29910
|
+
const issues = runGeneratedReferencesCheck(root2);
|
|
29911
|
+
return printReport(issues, {
|
|
29912
|
+
strict: options.strict,
|
|
29913
|
+
json: options.json,
|
|
29914
|
+
label: "References check"
|
|
29915
|
+
});
|
|
29916
|
+
}
|
|
29917
|
+
function printSyncResult(result) {
|
|
29918
|
+
if (result.written.length > 0) {
|
|
29919
|
+
console.log(`references sync: wrote ${result.written.length} file(s)`);
|
|
29920
|
+
for (const file of result.written)
|
|
29921
|
+
console.log(` + ${file}`);
|
|
29922
|
+
}
|
|
29923
|
+
if (result.rewritten.length > 0) {
|
|
29924
|
+
console.log(`references sync: rewrote links in ${result.rewritten.length} file(s)`);
|
|
29925
|
+
for (const file of result.rewritten)
|
|
29926
|
+
console.log(` ~ ${file}`);
|
|
29927
|
+
}
|
|
29928
|
+
if (result.removed.length > 0) {
|
|
29929
|
+
console.log(`references sync: removed ${result.removed.length} stale file(s)`);
|
|
29930
|
+
for (const file of result.removed)
|
|
29931
|
+
console.log(` - ${file}`);
|
|
29932
|
+
}
|
|
29933
|
+
if (result.written.length === 0 && result.rewritten.length === 0 && result.removed.length === 0) {
|
|
29934
|
+
console.log(`references sync: up to date (${result.skipped.length} file(s) checked)`);
|
|
29935
|
+
}
|
|
29936
|
+
}
|
|
29937
|
+
|
|
29550
29938
|
// src/cli.ts
|
|
29551
29939
|
function usage() {
|
|
29552
29940
|
console.error(`Usage: skeleton <command>
|
|
@@ -29556,7 +29944,9 @@ Commands:
|
|
|
29556
29944
|
audit docs|self|skills [--strict] [--json] [--paths=a,b] [--only=rule]
|
|
29557
29945
|
validate changed [paths…] [--staged] [--base <ref>]
|
|
29558
29946
|
register <path> [--topic=…] [--dry-run] [--json]
|
|
29559
|
-
customize resolve <slug> [--json]
|
|
29947
|
+
customize resolve <slug> [--json]
|
|
29948
|
+
references sync [--dry-run] [--no-rewrite-links]
|
|
29949
|
+
references check [--json] [--strict]`);
|
|
29560
29950
|
}
|
|
29561
29951
|
function parseRegisterArgs(argv) {
|
|
29562
29952
|
let path2 = null;
|
|
@@ -29640,6 +30030,24 @@ function main() {
|
|
|
29640
30030
|
runInit(parsed);
|
|
29641
30031
|
process.exit(0);
|
|
29642
30032
|
}
|
|
30033
|
+
if (command === "references") {
|
|
30034
|
+
const sub = argv[1];
|
|
30035
|
+
if (sub === "sync") {
|
|
30036
|
+
const dryRun = argv.includes("--dry-run");
|
|
30037
|
+
const rewriteLinks = !argv.includes("--no-rewrite-links");
|
|
30038
|
+
const result = runReferencesSync({ dryRun, rewriteLinks });
|
|
30039
|
+
printSyncResult(result);
|
|
30040
|
+
process.exit(0);
|
|
30041
|
+
}
|
|
30042
|
+
if (sub === "check") {
|
|
30043
|
+
process.exit(runReferencesCheck({
|
|
30044
|
+
json: argv.includes("--json"),
|
|
30045
|
+
strict: argv.includes("--strict")
|
|
30046
|
+
}));
|
|
30047
|
+
}
|
|
30048
|
+
usage();
|
|
30049
|
+
process.exit(1);
|
|
30050
|
+
}
|
|
29643
30051
|
usage();
|
|
29644
30052
|
process.exit(1);
|
|
29645
30053
|
} catch (error) {
|