nymor 1.0.1
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 +21 -0
- package/README.md +237 -0
- package/dist/agents/targets.js +111 -0
- package/dist/commands/add.js +121 -0
- package/dist/commands/compile.js +159 -0
- package/dist/commands/doctor.js +205 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/inject.js +24 -0
- package/dist/commands/learn.js +145 -0
- package/dist/commands/list.js +55 -0
- package/dist/commands/remove.js +38 -0
- package/dist/commands/update.js +80 -0
- package/dist/commands/validate.js +82 -0
- package/dist/compiler/agentsmd.js +17 -0
- package/dist/compiler/block.js +25 -0
- package/dist/compiler/claude.js +16 -0
- package/dist/compiler/copilot.js +29 -0
- package/dist/compiler/cursor.js +38 -0
- package/dist/compiler/kiro.js +24 -0
- package/dist/detector/agents.js +24 -0
- package/dist/detector/stack.js +113 -0
- package/dist/index.js +52 -0
- package/dist/registry/cache.js +60 -0
- package/dist/registry/client.js +135 -0
- package/dist/registry/resolver.js +29 -0
- package/dist/registry/types.js +2 -0
- package/dist/templates/bootstrap.js +97 -0
- package/dist/templates/cicada-json.js +11 -0
- package/dist/templates/nymor-json.js +11 -0
- package/dist/utils/manifest.js +32 -0
- package/dist/utils/paths.js +30 -0
- package/dist/utils/skills.js +114 -0
- package/package.json +32 -0
- package/src/agents/targets.ts +141 -0
- package/src/commands/compile.ts +202 -0
- package/src/commands/doctor.ts +253 -0
- package/src/commands/init.ts +113 -0
- package/src/commands/learn.ts +175 -0
- package/src/commands/list.ts +57 -0
- package/src/commands/validate.ts +89 -0
- package/src/compiler/block.ts +26 -0
- package/src/compiler/claude.ts +13 -0
- package/src/compiler/copilot.ts +28 -0
- package/src/compiler/cursor.ts +38 -0
- package/src/compiler/kiro.ts +22 -0
- package/src/detector/agents.ts +26 -0
- package/src/detector/stack.ts +135 -0
- package/src/index.ts +59 -0
- package/src/templates/bootstrap.ts +109 -0
- package/src/templates/nymor-json.ts +15 -0
- package/src/utils/manifest.ts +38 -0
- package/src/utils/paths.ts +25 -0
- package/src/utils/skills.ts +152 -0
- package/tests/compiler/__snapshots__/claude.test.ts.snap +65 -0
- package/tests/compiler/__snapshots__/copilot.test.ts.snap +54 -0
- package/tests/compiler/__snapshots__/cursor.test.ts.snap +62 -0
- package/tests/compiler/__snapshots__/kiro.test.ts.snap +54 -0
- package/tests/compiler/block.test.ts +24 -0
- package/tests/compiler/claude.test.ts +46 -0
- package/tests/compiler/copilot.test.ts +15 -0
- package/tests/compiler/cursor.test.ts +15 -0
- package/tests/compiler/kiro.test.ts +15 -0
- package/tests/detector/agents.test.ts +48 -0
- package/tests/detector/stack.test.ts +29 -0
- package/tests/e2e/init-and-compile.test.ts +227 -0
- package/tests/fixtures/skills/scoped/SKILL.md +18 -0
- package/tests/fixtures/skills/simple/SKILL.md +16 -0
- package/tests/fixtures/skills/with-examples/SKILL.md +18 -0
- package/tests/fixtures/skills/with-examples/examples/example.md +3 -0
- package/tests/fixtures/stacks/django/manage.py +2 -0
- package/tests/fixtures/stacks/django/requirements.txt +1 -0
- package/tests/fixtures/stacks/fastapi/requirements.txt +1 -0
- package/tests/fixtures/stacks/go/go.mod +3 -0
- package/tests/fixtures/stacks/nodejs/package.json +5 -0
- package/tests/fixtures/stacks/react/package.json +5 -0
- package/tests/fixtures/stacks/rust/Cargo.toml +4 -0
- package/tests/fixtures/stacks/vue/package.json +8 -0
- package/tests/fixtures/stacks/vue/vite.config.ts +5 -0
- package/tests/utils/manifest.test.ts +31 -0
- package/tests/utils/paths.test.ts +23 -0
- package/tests/utils/skills.test.ts +49 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
8
|
+
const cliPath = path.join(repoRoot, "dist", "index.js");
|
|
9
|
+
|
|
10
|
+
describe("nymor CLI", () => {
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
execFileSync("npm", ["run", "build"], { cwd: repoRoot, stdio: "inherit" });
|
|
13
|
+
}, 60_000);
|
|
14
|
+
|
|
15
|
+
it("initializes empty local memory, compiles idempotently, and passes doctor", async () => {
|
|
16
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-e2e-"));
|
|
17
|
+
await fs.writeJson(path.join(projectRoot, "package.json"), { name: "fixture", version: "1.0.0" });
|
|
18
|
+
|
|
19
|
+
runCli(["init"], projectRoot);
|
|
20
|
+
|
|
21
|
+
await expect(fs.readdir(path.join(projectRoot, ".nymor", "skills"))).resolves.toEqual([]);
|
|
22
|
+
await expect(fs.readJson(path.join(projectRoot, "nymor.json"))).resolves.toMatchObject({ local: [] });
|
|
23
|
+
await expect(fs.pathExists(path.join(projectRoot, ".nymor", "skills", "commit-conventions"))).resolves.toBe(false);
|
|
24
|
+
|
|
25
|
+
runCli(["compile"], projectRoot);
|
|
26
|
+
const before = await readTree(projectRoot);
|
|
27
|
+
runCli(["compile"], projectRoot);
|
|
28
|
+
const after = await readTree(projectRoot);
|
|
29
|
+
|
|
30
|
+
expect(after).toEqual(before);
|
|
31
|
+
runCli(["doctor"], projectRoot);
|
|
32
|
+
}, 60_000);
|
|
33
|
+
|
|
34
|
+
it("hides internal and removed commands from public help", () => {
|
|
35
|
+
const result = spawnSync(process.execPath, [cliPath, "--help"], {
|
|
36
|
+
cwd: repoRoot,
|
|
37
|
+
encoding: "utf8"
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.status).toBe(0);
|
|
41
|
+
expect(result.stdout).toContain("nymor");
|
|
42
|
+
expect(result.stdout).toContain("init");
|
|
43
|
+
expect(result.stdout).not.toContain("learn");
|
|
44
|
+
expect(result.stdout).not.toContain("add");
|
|
45
|
+
expect(result.stdout).not.toContain("remove");
|
|
46
|
+
expect(result.stdout).not.toContain("update");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps the README focused on Nymor repo memory", async () => {
|
|
50
|
+
const readme = await fs.readFile(path.join(repoRoot, "README.md"), "utf8");
|
|
51
|
+
|
|
52
|
+
expect(readme).toContain("# Nymor");
|
|
53
|
+
expect(readme).toContain("/nymor-learn");
|
|
54
|
+
for (const forbidden of ["Cicada", "cicada", "registry", "draft", "approve", "starter skills", "nymor learn"]) {
|
|
55
|
+
expect(readme).not.toContain(forbidden);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("keeps the hidden learn fallback working", async () => {
|
|
60
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-learn-"));
|
|
61
|
+
await fs.writeJson(path.join(projectRoot, "nymor.json"), {
|
|
62
|
+
version: "1",
|
|
63
|
+
agents: [],
|
|
64
|
+
local: []
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
runCli(
|
|
68
|
+
[
|
|
69
|
+
"learn",
|
|
70
|
+
"Use Server Actions for all mutations",
|
|
71
|
+
"--id",
|
|
72
|
+
"server-actions-only",
|
|
73
|
+
"--name",
|
|
74
|
+
"Server Actions Only",
|
|
75
|
+
"--description",
|
|
76
|
+
"Use this when changing app mutations",
|
|
77
|
+
"--globs",
|
|
78
|
+
"app/**/*.ts,app/**/*.tsx",
|
|
79
|
+
"--why",
|
|
80
|
+
"Keeps mutations close to UI and simplifies auth.",
|
|
81
|
+
"--example",
|
|
82
|
+
"Prefer an exported 'use server' action over a new API route."
|
|
83
|
+
],
|
|
84
|
+
projectRoot
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const skillPath = path.join(projectRoot, ".nymor", "skills", "server-actions-only", "SKILL.md");
|
|
88
|
+
await expect(fs.readFile(skillPath, "utf8")).resolves.toContain("Keeps mutations close to UI and simplifies auth.");
|
|
89
|
+
await expect(fs.readJson(path.join(projectRoot, "nymor.json"))).resolves.toMatchObject({
|
|
90
|
+
local: ["server-actions-only"]
|
|
91
|
+
});
|
|
92
|
+
}, 60_000);
|
|
93
|
+
|
|
94
|
+
it("writes Nymor outputs for the common agent set", async () => {
|
|
95
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-agents-"));
|
|
96
|
+
await fs.writeJson(path.join(projectRoot, "nymor.json"), {
|
|
97
|
+
version: "1",
|
|
98
|
+
agents: ["claude", "cursor", "copilot", "kiro", "agents-md", "gemini", "windsurf", "goose", "opencode"],
|
|
99
|
+
local: ["demo"]
|
|
100
|
+
});
|
|
101
|
+
await writeDemoSkill(projectRoot);
|
|
102
|
+
|
|
103
|
+
runCli(["compile"], projectRoot);
|
|
104
|
+
|
|
105
|
+
await expect(fs.pathExists(path.join(projectRoot, ".claude", "skills", "demo", "SKILL.md"))).resolves.toBe(true);
|
|
106
|
+
await expect(fs.pathExists(path.join(projectRoot, ".cursor", "rules", "nymor-demo.mdc"))).resolves.toBe(true);
|
|
107
|
+
await expect(fs.pathExists(path.join(projectRoot, ".github", "instructions", "nymor-demo.instructions.md"))).resolves.toBe(true);
|
|
108
|
+
await expect(fs.pathExists(path.join(projectRoot, ".github", "prompts", "nymor-learn.prompt.md"))).resolves.toBe(true);
|
|
109
|
+
await expect(fs.pathExists(path.join(projectRoot, ".kiro", "steering", "nymor-demo.md"))).resolves.toBe(true);
|
|
110
|
+
await expect(fs.pathExists(path.join(projectRoot, ".goose", "skills", "demo", "SKILL.md"))).resolves.toBe(true);
|
|
111
|
+
await expect(fs.pathExists(path.join(projectRoot, ".opencode", "skill", "demo", "SKILL.md"))).resolves.toBe(true);
|
|
112
|
+
|
|
113
|
+
await expect(fs.readFile(path.join(projectRoot, ".claude", "commands", "nymor-learn.md"), "utf8")).resolves.toContain(
|
|
114
|
+
"This looks like a reusable repo rule. Want me to capture it with /nymor-learn?"
|
|
115
|
+
);
|
|
116
|
+
await expect(fs.readFile(path.join(projectRoot, ".cursor", "commands", "nymor-learn.md"), "utf8")).resolves.toContain(
|
|
117
|
+
"Only create a skill after the user explicitly invokes /nymor-learn."
|
|
118
|
+
);
|
|
119
|
+
await expect(fs.readFile(path.join(projectRoot, ".github", "prompts", "nymor-learn.prompt.md"), "utf8")).resolves.toContain(
|
|
120
|
+
"Only create a skill after the user explicitly invokes /nymor-learn."
|
|
121
|
+
);
|
|
122
|
+
await expect(fs.readFile(path.join(projectRoot, "AGENTS.md"), "utf8")).resolves.toContain("<!-- nymor:start -->");
|
|
123
|
+
await expect(fs.readFile(path.join(projectRoot, "GEMINI.md"), "utf8")).resolves.toContain("/nymor-learn");
|
|
124
|
+
await expect(fs.readFile(path.join(projectRoot, ".windsurf", "rules", "nymor.md"), "utf8")).resolves.toContain(
|
|
125
|
+
".nymor/skills/"
|
|
126
|
+
);
|
|
127
|
+
}, 60_000);
|
|
128
|
+
|
|
129
|
+
it("doctor flags broken globs", async () => {
|
|
130
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-doctor-"));
|
|
131
|
+
await fs.writeJson(path.join(projectRoot, "nymor.json"), {
|
|
132
|
+
version: "1",
|
|
133
|
+
agents: [],
|
|
134
|
+
local: ["broken"]
|
|
135
|
+
});
|
|
136
|
+
await fs.outputFile(
|
|
137
|
+
path.join(projectRoot, ".nymor", "skills", "broken", "SKILL.md"),
|
|
138
|
+
[
|
|
139
|
+
"---",
|
|
140
|
+
"name: Broken Glob",
|
|
141
|
+
"globs:",
|
|
142
|
+
" - nope/**/*.fake",
|
|
143
|
+
"alwaysApply: false",
|
|
144
|
+
"---",
|
|
145
|
+
"",
|
|
146
|
+
"## Rule",
|
|
147
|
+
"Use matching globs.",
|
|
148
|
+
"",
|
|
149
|
+
"## Why",
|
|
150
|
+
"Doctor should catch broken scope.",
|
|
151
|
+
"",
|
|
152
|
+
"## Example",
|
|
153
|
+
"nope"
|
|
154
|
+
].join("\n"),
|
|
155
|
+
"utf8"
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
runCli(["compile"], projectRoot);
|
|
159
|
+
const result = spawnSync(process.execPath, [cliPath, "doctor"], {
|
|
160
|
+
cwd: projectRoot,
|
|
161
|
+
encoding: "utf8"
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(result.status).toBe(1);
|
|
165
|
+
expect(`${result.stdout}${result.stderr}`).toContain("no matches: nope/**/*.fake");
|
|
166
|
+
}, 60_000);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function runCli(args: string[], cwd: string, env: Record<string, string> = {}): void {
|
|
170
|
+
execFileSync(process.execPath, [cliPath, ...args], {
|
|
171
|
+
cwd,
|
|
172
|
+
env: { ...process.env, ...env },
|
|
173
|
+
stdio: "pipe"
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function writeDemoSkill(projectRoot: string): Promise<void> {
|
|
178
|
+
await fs.outputFile(
|
|
179
|
+
path.join(projectRoot, ".nymor", "skills", "demo", "SKILL.md"),
|
|
180
|
+
[
|
|
181
|
+
"---",
|
|
182
|
+
"name: Demo",
|
|
183
|
+
"description: Demo skill",
|
|
184
|
+
"globs:",
|
|
185
|
+
" - \"**/*\"",
|
|
186
|
+
"alwaysApply: true",
|
|
187
|
+
"---",
|
|
188
|
+
"",
|
|
189
|
+
"## Rule",
|
|
190
|
+
"Use demos.",
|
|
191
|
+
"",
|
|
192
|
+
"## Why",
|
|
193
|
+
"For tests.",
|
|
194
|
+
"",
|
|
195
|
+
"## Example",
|
|
196
|
+
"demo"
|
|
197
|
+
].join("\n"),
|
|
198
|
+
"utf8"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function readTree(root: string): Promise<Record<string, string>> {
|
|
203
|
+
const files = (await listFiles(root)).filter((file) => !file.includes(`${path.sep}.git${path.sep}`));
|
|
204
|
+
const output: Record<string, string> = {};
|
|
205
|
+
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
output[path.relative(root, file)] = await fs.readFile(file, "utf8");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return output;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function listFiles(root: string): Promise<string[]> {
|
|
214
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
215
|
+
const files: string[] = [];
|
|
216
|
+
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const entryPath = path.join(root, entry.name);
|
|
219
|
+
if (entry.isDirectory()) {
|
|
220
|
+
files.push(...(await listFiles(entryPath)));
|
|
221
|
+
} else {
|
|
222
|
+
files.push(entryPath);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return files.sort();
|
|
227
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Scoped Skill
|
|
3
|
+
description: Applies to TypeScript source files.
|
|
4
|
+
globs:
|
|
5
|
+
- "src/**/*.ts"
|
|
6
|
+
alwaysApply: false
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: Scoped Skill
|
|
10
|
+
|
|
11
|
+
## Rule
|
|
12
|
+
Use explicit return types on exported functions.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
Exported APIs should be clear at the boundary.
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
export function run(): void {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Simple Skill
|
|
3
|
+
description: Applies everywhere.
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Skill: Simple Skill
|
|
8
|
+
|
|
9
|
+
## Rule
|
|
10
|
+
Always keep the code readable.
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
Readable code is easier to review and maintain.
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
Prefer a named helper over a dense inline expression.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: With Examples
|
|
3
|
+
description: Includes extra example files.
|
|
4
|
+
globs:
|
|
5
|
+
- "src/**/*.ts"
|
|
6
|
+
alwaysApply: false
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: With Examples
|
|
10
|
+
|
|
11
|
+
## Rule
|
|
12
|
+
Keep examples near the skill.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
Examples make a convention concrete.
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
See examples/example.md.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django==5.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastapi==0.110.0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { DEFAULT_AGENT_TARGETS } from "../../src/agents/targets";
|
|
6
|
+
import { readManifest, writeManifest } from "../../src/utils/manifest";
|
|
7
|
+
|
|
8
|
+
describe("manifest utilities", () => {
|
|
9
|
+
it("returns defaults when nymor.json is missing", async () => {
|
|
10
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-manifest-"));
|
|
11
|
+
|
|
12
|
+
await expect(readManifest(projectRoot)).resolves.toMatchObject({
|
|
13
|
+
version: "1",
|
|
14
|
+
agents: DEFAULT_AGENT_TARGETS,
|
|
15
|
+
local: []
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("roundtrips manifest JSON", async () => {
|
|
20
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-manifest-"));
|
|
21
|
+
const manifest = {
|
|
22
|
+
version: "1",
|
|
23
|
+
agents: ["claude" as const],
|
|
24
|
+
local: ["local-demo"]
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
await writeManifest(projectRoot, manifest);
|
|
28
|
+
|
|
29
|
+
await expect(readManifest(projectRoot)).resolves.toEqual(manifest);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
getIndexJsonPath,
|
|
5
|
+
getIndexMarkdownPath,
|
|
6
|
+
getManifestPath,
|
|
7
|
+
getNymorDir,
|
|
8
|
+
getRepoRoot,
|
|
9
|
+
getSkillsDir
|
|
10
|
+
} from "../../src/utils/paths";
|
|
11
|
+
|
|
12
|
+
describe("path utilities", () => {
|
|
13
|
+
it("derives project paths from a root", () => {
|
|
14
|
+
const root = path.join(path.sep, "tmp", "project");
|
|
15
|
+
|
|
16
|
+
expect(getRepoRoot(root)).toBe(root);
|
|
17
|
+
expect(getNymorDir(root)).toBe(path.join(root, ".nymor"));
|
|
18
|
+
expect(getSkillsDir(root)).toBe(path.join(root, ".nymor", "skills"));
|
|
19
|
+
expect(getIndexMarkdownPath(root)).toBe(path.join(root, ".nymor", "index.md"));
|
|
20
|
+
expect(getIndexJsonPath(root)).toBe(path.join(root, ".nymor", "index.json"));
|
|
21
|
+
expect(getManifestPath(root)).toBe(path.join(root, "nymor.json"));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildSkillIndex, parseSkillContent } from "../../src/utils/skills";
|
|
3
|
+
|
|
4
|
+
describe("parseSkillContent", () => {
|
|
5
|
+
it("normalizes valid frontmatter", () => {
|
|
6
|
+
const parsed = parseSkillContent(
|
|
7
|
+
["---", "name: Demo", "globs:", " - src/**/*.ts", "alwaysApply: true", "---", "", "## Rule", "Use demos."].join("\n"),
|
|
8
|
+
"demo"
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
expect(parsed.frontmatter).toEqual({
|
|
12
|
+
name: "Demo",
|
|
13
|
+
description: "",
|
|
14
|
+
globs: ["src/**/*.ts"],
|
|
15
|
+
alwaysApply: true
|
|
16
|
+
});
|
|
17
|
+
expect(parsed.body).toContain("## Rule");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("throws when frontmatter is missing", () => {
|
|
21
|
+
expect(() => parseSkillContent("## Rule", "missing")).toThrow("missing frontmatter");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("throws when frontmatter is malformed", () => {
|
|
25
|
+
expect(() => parseSkillContent(["---", "name: [", "---"].join("\n"), "bad")).toThrow();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("throws when name is missing", () => {
|
|
29
|
+
expect(() => parseSkillContent(["---", "description: nope", "---"].join("\n"), "bad")).toThrow("missing a name");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("buildSkillIndex", () => {
|
|
34
|
+
it("renders markdown and json entries", () => {
|
|
35
|
+
const index = buildSkillIndex([
|
|
36
|
+
{
|
|
37
|
+
id: "demo",
|
|
38
|
+
dirPath: "/tmp/demo",
|
|
39
|
+
skillPath: "/tmp/demo/SKILL.md",
|
|
40
|
+
frontmatter: { name: "Demo", description: "Desc", globs: ["src/**/*.ts"], alwaysApply: false },
|
|
41
|
+
body: "body",
|
|
42
|
+
raw: "raw"
|
|
43
|
+
}
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
expect(index.markdown).toContain("| Demo | demo | Desc | src/**/*.ts | no |");
|
|
47
|
+
expect(JSON.parse(index.json).skills[0].id).toBe("demo");
|
|
48
|
+
});
|
|
49
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"skipLibCheck": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"],
|
|
13
|
+
"exclude": ["node_modules", "dist"]
|
|
14
|
+
}
|