@csark0812/skeleton 1.1.0 → 1.1.2
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 +18 -7
- package/dist/cli.js +105 -14
- package/dist/hooks/customize-on-skill-read.js +6385 -13
- package/package.json +3 -2
- package/schemas/config.schema.json +22 -1
- package/templates/skeleton-init/config.yaml +6 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Skeleton
|
|
2
2
|
|
|
3
|
+
**Source of truth for** Package overview.
|
|
4
|
+
|
|
5
|
+
<!-- doc-meta: owner=eng | last-reviewed=2026-07-13 -->
|
|
6
|
+
|
|
3
7
|
Single source of truth (SSOT) linter for agent-enabled repos.
|
|
4
8
|
|
|
5
9
|
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.
|
|
@@ -83,12 +87,12 @@ skeleton customize resolve <slug>
|
|
|
83
87
|
|
|
84
88
|
**Validate changed** routes git diffs to the right audit:
|
|
85
89
|
|
|
86
|
-
| Path
|
|
87
|
-
|
|
|
88
|
-
| Docs/skills in scan perimeter
|
|
89
|
-
| `.sh`, `.bash`, `.zsh`
|
|
90
|
-
| Other `.json`
|
|
91
|
-
| `.ts`, `.py`, `package.json`, `project.json` | skip
|
|
90
|
+
| Path | Action |
|
|
91
|
+
| ----------------------------------------------------------------- | --------------------------- |
|
|
92
|
+
| Docs/skills in scan perimeter | path-scoped audit |
|
|
93
|
+
| `.sh`, `.bash`, `.zsh` | shellcheck or `bash -n` |
|
|
94
|
+
| Other `.json` | JSONC-tolerant syntax check |
|
|
95
|
+
| `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.py`, `package.json`, `project.json` | skip (exits 1 if all skip) |
|
|
92
96
|
|
|
93
97
|
Pre-commit: `skeleton validate changed --staged` (path-scoped, fast).
|
|
94
98
|
CI: `skeleton validate changed --base origin/main` (global rules first, then changed files).
|
|
@@ -128,11 +132,18 @@ One command for humans: `npx skeleton init --skills`.
|
|
|
128
132
|
|
|
129
133
|
## Development
|
|
130
134
|
|
|
131
|
-
Requires Node ≥ 22. Uses Bun for dev and tests.
|
|
135
|
+
Requires Node ≥ 22. Uses Bun for dev and tests. Agent cold-start: [AGENTS.md](AGENTS.md).
|
|
132
136
|
|
|
133
137
|
```bash
|
|
134
138
|
bun install
|
|
135
139
|
bun test
|
|
140
|
+
bun run typecheck
|
|
136
141
|
bun run build
|
|
137
142
|
bun run audit:self
|
|
138
143
|
```
|
|
144
|
+
|
|
145
|
+
`validate:changed` is docs/skills only — it skips code/config extensions (see table). All-skip inputs exit non-zero. Use `bun test` and `bun run typecheck` for code.
|
|
146
|
+
|
|
147
|
+
### Publishing 1.1.2 (human)
|
|
148
|
+
|
|
149
|
+
Keep version `1.1.2` until `npm publish`. Preflight: `bun test && bun run typecheck && bun run build && bun run audit:self`. Then publish and tag when ready (npm is still on `1.1.1` until then).
|
package/dist/cli.js
CHANGED
|
@@ -16536,7 +16536,8 @@ var $visitAsync = visit.visitAsync;
|
|
|
16536
16536
|
// src/audit/config/load.ts
|
|
16537
16537
|
var SCHEMA_CANDIDATES = [
|
|
16538
16538
|
join(dirname2(fileURLToPath2(import.meta.url)), "../../../schemas/config.schema.json"),
|
|
16539
|
-
join(dirname2(fileURLToPath2(import.meta.url)), "../schemas/config.schema.json")
|
|
16539
|
+
join(dirname2(fileURLToPath2(import.meta.url)), "../schemas/config.schema.json"),
|
|
16540
|
+
join(dirname2(fileURLToPath2(import.meta.url)), "../../schemas/config.schema.json")
|
|
16540
16541
|
];
|
|
16541
16542
|
function resolveSchemaPath() {
|
|
16542
16543
|
for (const candidate of SCHEMA_CANDIDATES) {
|
|
@@ -16594,6 +16595,9 @@ function mergedExcludes(config) {
|
|
|
16594
16595
|
function retiredSkills(config) {
|
|
16595
16596
|
return config.scan.retiredSkills ?? [];
|
|
16596
16597
|
}
|
|
16598
|
+
function nonPublicSkills(config) {
|
|
16599
|
+
return config.scan.nonPublicSkills ?? [];
|
|
16600
|
+
}
|
|
16597
16601
|
|
|
16598
16602
|
// src/audit/core/skill-roots.ts
|
|
16599
16603
|
import { existsSync as existsSync2, readdirSync as readdirSync2, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
@@ -28604,7 +28608,6 @@ function isGeneratedReference(content3) {
|
|
|
28604
28608
|
}
|
|
28605
28609
|
|
|
28606
28610
|
// src/audit/rules/skill-index.ts
|
|
28607
|
-
var NON_PUBLIC_SLUGS = new Set(["align-commands"]);
|
|
28608
28611
|
function walkSkillMarkdown(dir) {
|
|
28609
28612
|
const files = [];
|
|
28610
28613
|
if (!existsSync8(dir))
|
|
@@ -28658,6 +28661,7 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
|
|
|
28658
28661
|
}
|
|
28659
28662
|
function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
|
|
28660
28663
|
const issues = [];
|
|
28664
|
+
const nonPublic = new Set(nonPublicSkills(ctx.config));
|
|
28661
28665
|
for (const skillRoot of index2.roots) {
|
|
28662
28666
|
if (skillRoot.kind !== "nested")
|
|
28663
28667
|
continue;
|
|
@@ -28669,7 +28673,7 @@ function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
|
|
|
28669
28673
|
continue;
|
|
28670
28674
|
const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
|
|
28671
28675
|
const nestedSlugs = diskSlugs.filter((slug2) => existsSync8(join8(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
|
|
28672
|
-
const publicSlugs = nestedSlugs.filter((slug2) => !
|
|
28676
|
+
const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2));
|
|
28673
28677
|
const relReadme = `${skillRoot.relPath}/README.md`;
|
|
28674
28678
|
for (const slug2 of publicSlugs) {
|
|
28675
28679
|
if (!taxonomySlugs.includes(slug2)) {
|
|
@@ -28763,10 +28767,16 @@ function findLocalCanonicalLinks(root2, content3, sourceFile) {
|
|
|
28763
28767
|
}
|
|
28764
28768
|
const inReferencesDir = /\/references\//.test(sourceFile);
|
|
28765
28769
|
if (inReferencesDir) {
|
|
28770
|
+
const refsIdx = sourceFile.lastIndexOf("/references/");
|
|
28771
|
+
const withinRefs = sourceFile.slice(refsIdx + "/references/".length);
|
|
28772
|
+
const withinDir = withinRefs.includes("/") ? withinRefs.slice(0, withinRefs.lastIndexOf("/")) : "";
|
|
28766
28773
|
const siblingRe = /\((?!https?:|#|\.\.\/)([a-z0-9./_-]+\.md)\)/gi;
|
|
28767
28774
|
for (const match of content3.matchAll(siblingRe)) {
|
|
28768
|
-
const
|
|
28769
|
-
if (!
|
|
28775
|
+
const raw = normalizeRelPath(match[1] ?? "");
|
|
28776
|
+
if (!raw)
|
|
28777
|
+
continue;
|
|
28778
|
+
const refPath = withinDir ? normalizeRelPath(join9(withinDir, raw)) : raw;
|
|
28779
|
+
if (!canonicalExists(root2, refPath))
|
|
28770
28780
|
continue;
|
|
28771
28781
|
links.push({ refPath, sourceFile });
|
|
28772
28782
|
}
|
|
@@ -28795,6 +28805,21 @@ function discoverSkillReferencePlans(root2) {
|
|
|
28795
28805
|
links.push(link2);
|
|
28796
28806
|
}
|
|
28797
28807
|
}
|
|
28808
|
+
const queue = [...refPaths];
|
|
28809
|
+
while (queue.length > 0) {
|
|
28810
|
+
const refPath = queue.pop();
|
|
28811
|
+
if (!refPath || !canonicalExists(root2, refPath))
|
|
28812
|
+
continue;
|
|
28813
|
+
const canonicalContent = readFileSync8(join9(root2, CANONICAL_REFS_DIR, refPath), "utf8");
|
|
28814
|
+
const syntheticSource = generatedRefPath(slug2, refPath);
|
|
28815
|
+
for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
|
|
28816
|
+
if (refPaths.has(link2.refPath))
|
|
28817
|
+
continue;
|
|
28818
|
+
refPaths.add(link2.refPath);
|
|
28819
|
+
links.push(link2);
|
|
28820
|
+
queue.push(link2.refPath);
|
|
28821
|
+
}
|
|
28822
|
+
}
|
|
28798
28823
|
if (refPaths.size > 0) {
|
|
28799
28824
|
plans.push({ skill: slug2, refPaths, links });
|
|
28800
28825
|
}
|
|
@@ -29022,10 +29047,13 @@ function runAudit(options) {
|
|
|
29022
29047
|
|
|
29023
29048
|
// src/customize/resolve.ts
|
|
29024
29049
|
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
|
|
29025
|
-
import { join as join11, relative as relative8 } from "node:path";
|
|
29050
|
+
import { basename as basename2, join as join11, relative as relative8 } from "node:path";
|
|
29026
29051
|
var CUSTOMIZE_PREFIX = "Customize: ";
|
|
29052
|
+
function customizeDir(root2) {
|
|
29053
|
+
return join11(root2, REGISTRY_DIR_REL, "customize");
|
|
29054
|
+
}
|
|
29027
29055
|
function customizePathForSlug(root2, slug2) {
|
|
29028
|
-
return join11(root2,
|
|
29056
|
+
return join11(customizeDir(root2), `${slug2}.md`);
|
|
29029
29057
|
}
|
|
29030
29058
|
function findCustomizeViaRegistry(root2, slug2) {
|
|
29031
29059
|
for (const rel of parseRegistryPaths(root2)) {
|
|
@@ -29036,11 +29064,10 @@ function findCustomizeViaRegistry(root2, slug2) {
|
|
|
29036
29064
|
}
|
|
29037
29065
|
return null;
|
|
29038
29066
|
}
|
|
29039
|
-
function
|
|
29067
|
+
function resolveSlugFile(root2, slug2) {
|
|
29040
29068
|
const direct = customizePathForSlug(root2, slug2);
|
|
29041
29069
|
if (existsSync11(direct)) {
|
|
29042
29070
|
return {
|
|
29043
|
-
slug: slug2,
|
|
29044
29071
|
content: readFileSync10(direct, "utf8"),
|
|
29045
29072
|
path: normalizeRelPath(relative8(root2, direct))
|
|
29046
29073
|
};
|
|
@@ -29049,12 +29076,64 @@ function resolveCustomize(root2, slug2) {
|
|
|
29049
29076
|
if (registryPath) {
|
|
29050
29077
|
const abs = join11(root2, registryPath);
|
|
29051
29078
|
return {
|
|
29052
|
-
slug: slug2,
|
|
29053
29079
|
content: readFileSync10(abs, "utf8"),
|
|
29054
29080
|
path: registryPath
|
|
29055
29081
|
};
|
|
29056
29082
|
}
|
|
29057
|
-
return {
|
|
29083
|
+
return { content: null, path: null };
|
|
29084
|
+
}
|
|
29085
|
+
function alwaysIncludeBasenames(root2) {
|
|
29086
|
+
try {
|
|
29087
|
+
const config = loadConfig(root2);
|
|
29088
|
+
return config.customize?.alwaysInclude ?? [];
|
|
29089
|
+
} catch {
|
|
29090
|
+
return [];
|
|
29091
|
+
}
|
|
29092
|
+
}
|
|
29093
|
+
function readAlwaysInclude(root2, basenames, skipBasename) {
|
|
29094
|
+
const parts = [];
|
|
29095
|
+
const paths = [];
|
|
29096
|
+
const dir = customizeDir(root2);
|
|
29097
|
+
for (const name of basenames) {
|
|
29098
|
+
const file = basename2(name);
|
|
29099
|
+
if (skipBasename && file === skipBasename)
|
|
29100
|
+
continue;
|
|
29101
|
+
const abs = join11(dir, file);
|
|
29102
|
+
if (!existsSync11(abs))
|
|
29103
|
+
continue;
|
|
29104
|
+
parts.push(readFileSync10(abs, "utf8").trimEnd());
|
|
29105
|
+
paths.push(normalizeRelPath(relative8(root2, abs)));
|
|
29106
|
+
}
|
|
29107
|
+
return { parts, paths };
|
|
29108
|
+
}
|
|
29109
|
+
function resolveCustomize(root2, slug2) {
|
|
29110
|
+
const slugFile = resolveSlugFile(root2, slug2);
|
|
29111
|
+
const alwaysNames = alwaysIncludeBasenames(root2);
|
|
29112
|
+
const skip = slugFile.path != null ? basename2(slugFile.path) : null;
|
|
29113
|
+
const always = readAlwaysInclude(root2, alwaysNames, skip);
|
|
29114
|
+
const parts = [];
|
|
29115
|
+
const included = [];
|
|
29116
|
+
if (slugFile.content != null && slugFile.content.trim().length > 0) {
|
|
29117
|
+
parts.push(slugFile.content.trimEnd());
|
|
29118
|
+
if (slugFile.path)
|
|
29119
|
+
included.push(slugFile.path);
|
|
29120
|
+
}
|
|
29121
|
+
parts.push(...always.parts);
|
|
29122
|
+
included.push(...always.paths);
|
|
29123
|
+
if (parts.length === 0) {
|
|
29124
|
+
return { slug: slug2, content: null, path: slugFile.path, included: [] };
|
|
29125
|
+
}
|
|
29126
|
+
return {
|
|
29127
|
+
slug: slug2,
|
|
29128
|
+
content: parts.join(`
|
|
29129
|
+
|
|
29130
|
+
---
|
|
29131
|
+
|
|
29132
|
+
`) + `
|
|
29133
|
+
`,
|
|
29134
|
+
path: slugFile.path ?? always.paths[0] ?? null,
|
|
29135
|
+
included
|
|
29136
|
+
};
|
|
29058
29137
|
}
|
|
29059
29138
|
function resolveCustomizeFromRoot(slug2, startDir) {
|
|
29060
29139
|
const root2 = findRepoRoot(startDir);
|
|
@@ -29610,7 +29689,7 @@ function registerPath(options) {
|
|
|
29610
29689
|
|
|
29611
29690
|
// src/validate/changed.ts
|
|
29612
29691
|
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
|
|
29613
|
-
import { basename as
|
|
29692
|
+
import { basename as basename3, extname, join as join17 } from "node:path";
|
|
29614
29693
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
29615
29694
|
|
|
29616
29695
|
// src/validate/git-diff.ts
|
|
@@ -29641,7 +29720,7 @@ var COMMAND_CONFIG_NAMES = new Set(["package.json", "project.json"]);
|
|
|
29641
29720
|
function bucketFor(relPath2, root2) {
|
|
29642
29721
|
const normalized = normalizeRelPath(relPath2);
|
|
29643
29722
|
const ext = extname(normalized).toLowerCase();
|
|
29644
|
-
const name =
|
|
29723
|
+
const name = basename3(normalized);
|
|
29645
29724
|
if (SKIP_EXTENSIONS.has(ext))
|
|
29646
29725
|
return "skip";
|
|
29647
29726
|
if (COMMAND_CONFIG_NAMES.has(name))
|
|
@@ -29732,6 +29811,7 @@ function runValidateChanged(options = {}) {
|
|
|
29732
29811
|
}
|
|
29733
29812
|
buckets[bucket].push(relPath2);
|
|
29734
29813
|
}
|
|
29814
|
+
const audited = buckets.docs.length + buckets.skills.length + buckets.shell.length + buckets.json.length;
|
|
29735
29815
|
let exitCode = 0;
|
|
29736
29816
|
if (options.base) {
|
|
29737
29817
|
const globalExit = runAudit({
|
|
@@ -29746,6 +29826,12 @@ function runValidateChanged(options = {}) {
|
|
|
29746
29826
|
if (globalExit !== 0)
|
|
29747
29827
|
exitCode = 1;
|
|
29748
29828
|
}
|
|
29829
|
+
if (skipped > 0 && audited === 0) {
|
|
29830
|
+
console.error(`validate changed: all paths were skipped (code/config). This does not verify TypeScript or app code.
|
|
29831
|
+
` + ` In this repo: bun test && bun run typecheck
|
|
29832
|
+
` + " Consumers: run your local code validation gates.");
|
|
29833
|
+
return 1;
|
|
29834
|
+
}
|
|
29749
29835
|
if (buckets.docs.length > 0) {
|
|
29750
29836
|
const docExit = runAudit({
|
|
29751
29837
|
suite: "docs",
|
|
@@ -30007,7 +30093,12 @@ function main() {
|
|
|
30007
30093
|
console.error("register: path required");
|
|
30008
30094
|
process.exit(1);
|
|
30009
30095
|
}
|
|
30010
|
-
registerPath(
|
|
30096
|
+
registerPath({
|
|
30097
|
+
path: opts.path,
|
|
30098
|
+
topic: opts.topic,
|
|
30099
|
+
dryRun: opts.dryRun,
|
|
30100
|
+
json: opts.json
|
|
30101
|
+
});
|
|
30011
30102
|
process.exit(0);
|
|
30012
30103
|
}
|
|
30013
30104
|
if (command === "customize" && argv[1] === "resolve") {
|