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,152 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import yaml from "yaml";
|
|
4
|
+
|
|
5
|
+
export interface SkillFrontmatter {
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
globs?: string[];
|
|
9
|
+
alwaysApply?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SkillFile {
|
|
13
|
+
id: string;
|
|
14
|
+
dirPath: string;
|
|
15
|
+
skillPath: string;
|
|
16
|
+
frontmatter: SkillFrontmatter;
|
|
17
|
+
body: string;
|
|
18
|
+
raw: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SkillIndexEntry {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
globs: string[];
|
|
26
|
+
alwaysApply: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function listSkillDirectories(skillsDir: string): Promise<string[]> {
|
|
30
|
+
if (!(await fs.pathExists(skillsDir))) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
35
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function loadSkills(skillsDir: string): Promise<SkillFile[]> {
|
|
39
|
+
const skillDirs = await listSkillDirectories(skillsDir);
|
|
40
|
+
const skills: SkillFile[] = [];
|
|
41
|
+
|
|
42
|
+
for (const dirName of skillDirs) {
|
|
43
|
+
const skillPath = path.join(skillsDir, dirName, "SKILL.md");
|
|
44
|
+
if (!(await fs.pathExists(skillPath))) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
skills.push(await readSkillFile(dirName, skillPath));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return skills.sort((a, b) => a.frontmatter.name.localeCompare(b.frontmatter.name));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function readSkillFile(id: string, skillPath: string): Promise<SkillFile> {
|
|
55
|
+
const raw = await fs.readFile(skillPath, "utf8");
|
|
56
|
+
const { frontmatter, body } = parseSkillContent(raw, id);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
dirPath: path.dirname(skillPath),
|
|
61
|
+
skillPath,
|
|
62
|
+
frontmatter,
|
|
63
|
+
body,
|
|
64
|
+
raw
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseSkillContent(content: string, id: string): { frontmatter: SkillFrontmatter; body: string } {
|
|
69
|
+
const lines = content.split(/\r?\n/);
|
|
70
|
+
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
71
|
+
throw new Error(`Skill ${id} is missing frontmatter`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const endIndex = lines.slice(1).findIndex((line) => line.trim() === "---");
|
|
75
|
+
if (endIndex === -1) {
|
|
76
|
+
throw new Error(`Skill ${id} frontmatter is not closed`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const frontmatterText = lines.slice(1, endIndex + 1).join("\n");
|
|
80
|
+
const body = lines.slice(endIndex + 2).join("\n").trimStart();
|
|
81
|
+
const data = yaml.parse(frontmatterText) as SkillFrontmatter;
|
|
82
|
+
|
|
83
|
+
if (!data?.name) {
|
|
84
|
+
throw new Error(`Skill ${id} is missing a name in frontmatter`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const globs = normalizeGlobs(data.globs);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
frontmatter: {
|
|
91
|
+
name: data.name,
|
|
92
|
+
description: data.description ?? "",
|
|
93
|
+
globs,
|
|
94
|
+
alwaysApply: Boolean(data.alwaysApply)
|
|
95
|
+
},
|
|
96
|
+
body
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildSkillIndex(skills: SkillFile[]): { markdown: string; json: string } {
|
|
101
|
+
const entries = skills.map(toIndexEntry);
|
|
102
|
+
|
|
103
|
+
const rows = entries.map((entry) => {
|
|
104
|
+
const globs = entry.globs.length > 0 ? entry.globs.join(", ") : "-";
|
|
105
|
+
const alwaysApply = entry.alwaysApply ? "yes" : "no";
|
|
106
|
+
return `| ${entry.name} | ${entry.id} | ${entry.description || "-"} | ${globs} | ${alwaysApply} |`;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const markdown = [
|
|
110
|
+
"# Nymor Skills Index",
|
|
111
|
+
"",
|
|
112
|
+
"This index is regenerated by Nymor on every compile.",
|
|
113
|
+
"",
|
|
114
|
+
"| Skill | Folder | Description | Globs | Always Apply |",
|
|
115
|
+
"|---|---|---|---|---|",
|
|
116
|
+
...rows,
|
|
117
|
+
"",
|
|
118
|
+
"## How to use this index",
|
|
119
|
+
"1. Review the Description and Globs columns",
|
|
120
|
+
"2. Load the relevant skill folders from .nymor/skills/",
|
|
121
|
+
"3. Apply their rules for the current task",
|
|
122
|
+
""
|
|
123
|
+
].join("\n");
|
|
124
|
+
|
|
125
|
+
const json = JSON.stringify({ skills: entries }, null, 2);
|
|
126
|
+
|
|
127
|
+
return { markdown, json };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function toIndexEntry(skill: SkillFile): SkillIndexEntry {
|
|
131
|
+
const globs = normalizeGlobs(skill.frontmatter.globs);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
id: skill.id,
|
|
135
|
+
name: skill.frontmatter.name,
|
|
136
|
+
description: skill.frontmatter.description ?? "",
|
|
137
|
+
globs,
|
|
138
|
+
alwaysApply: Boolean(skill.frontmatter.alwaysApply)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeGlobs(globs: SkillFrontmatter["globs"]): string[] {
|
|
143
|
+
if (!globs) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (Array.isArray(globs)) {
|
|
148
|
+
return globs.map((glob) => String(glob)).filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [String(globs)];
|
|
152
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`compileClaudeSkills > copies every skill folder 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"scoped/SKILL.md": "---
|
|
6
|
+
name: Scoped Skill
|
|
7
|
+
description: Applies to TypeScript source files.
|
|
8
|
+
globs:
|
|
9
|
+
- "src/**/*.ts"
|
|
10
|
+
alwaysApply: false
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Skill: Scoped Skill
|
|
14
|
+
|
|
15
|
+
## Rule
|
|
16
|
+
Use explicit return types on exported functions.
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
Exported APIs should be clear at the boundary.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
export function run(): void {}
|
|
23
|
+
",
|
|
24
|
+
"simple/SKILL.md": "---
|
|
25
|
+
name: Simple Skill
|
|
26
|
+
description: Applies everywhere.
|
|
27
|
+
alwaysApply: true
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# Skill: Simple Skill
|
|
31
|
+
|
|
32
|
+
## Rule
|
|
33
|
+
Always keep the code readable.
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
Readable code is easier to review and maintain.
|
|
37
|
+
|
|
38
|
+
## Example
|
|
39
|
+
Prefer a named helper over a dense inline expression.
|
|
40
|
+
",
|
|
41
|
+
"with-examples/SKILL.md": "---
|
|
42
|
+
name: With Examples
|
|
43
|
+
description: Includes extra example files.
|
|
44
|
+
globs:
|
|
45
|
+
- "src/**/*.ts"
|
|
46
|
+
alwaysApply: false
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
# Skill: With Examples
|
|
50
|
+
|
|
51
|
+
## Rule
|
|
52
|
+
Keep examples near the skill.
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
Examples make a convention concrete.
|
|
56
|
+
|
|
57
|
+
## Example
|
|
58
|
+
See examples/example.md.
|
|
59
|
+
",
|
|
60
|
+
"with-examples/examples/example.md": "# Example
|
|
61
|
+
|
|
62
|
+
Use a small fixture alongside the skill.
|
|
63
|
+
",
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`renderCopilotInstructions > renders GitHub instruction content for fixture skills 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"scoped": "---
|
|
6
|
+
applyTo: "src/**/*.ts"
|
|
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 {}
|
|
19
|
+
|
|
20
|
+
",
|
|
21
|
+
"simple": "---
|
|
22
|
+
applyTo: "**/*"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Skill: Simple Skill
|
|
26
|
+
|
|
27
|
+
## Rule
|
|
28
|
+
Always keep the code readable.
|
|
29
|
+
|
|
30
|
+
## Why
|
|
31
|
+
Readable code is easier to review and maintain.
|
|
32
|
+
|
|
33
|
+
## Example
|
|
34
|
+
Prefer a named helper over a dense inline expression.
|
|
35
|
+
|
|
36
|
+
",
|
|
37
|
+
"with-examples": "---
|
|
38
|
+
applyTo: "src/**/*.ts"
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
# Skill: With Examples
|
|
42
|
+
|
|
43
|
+
## Rule
|
|
44
|
+
Keep examples near the skill.
|
|
45
|
+
|
|
46
|
+
## Why
|
|
47
|
+
Examples make a convention concrete.
|
|
48
|
+
|
|
49
|
+
## Example
|
|
50
|
+
See examples/example.md.
|
|
51
|
+
|
|
52
|
+
",
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`renderCursorRule > renders .mdc rule content for fixture skills 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"scoped": "---
|
|
6
|
+
description: "Applies to TypeScript source files."
|
|
7
|
+
globs:
|
|
8
|
+
- "src/**/*.ts"
|
|
9
|
+
alwaysApply: false
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Skill: Scoped Skill
|
|
13
|
+
|
|
14
|
+
## Rule
|
|
15
|
+
Use explicit return types on exported functions.
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
Exported APIs should be clear at the boundary.
|
|
19
|
+
|
|
20
|
+
## Example
|
|
21
|
+
export function run(): void {}
|
|
22
|
+
|
|
23
|
+
",
|
|
24
|
+
"simple": "---
|
|
25
|
+
description: "Applies everywhere."
|
|
26
|
+
globs: []
|
|
27
|
+
alwaysApply: true
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# Skill: Simple Skill
|
|
31
|
+
|
|
32
|
+
## Rule
|
|
33
|
+
Always keep the code readable.
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
Readable code is easier to review and maintain.
|
|
37
|
+
|
|
38
|
+
## Example
|
|
39
|
+
Prefer a named helper over a dense inline expression.
|
|
40
|
+
|
|
41
|
+
",
|
|
42
|
+
"with-examples": "---
|
|
43
|
+
description: "Includes extra example files."
|
|
44
|
+
globs:
|
|
45
|
+
- "src/**/*.ts"
|
|
46
|
+
alwaysApply: false
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
# Skill: With Examples
|
|
50
|
+
|
|
51
|
+
## Rule
|
|
52
|
+
Keep examples near the skill.
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
Examples make a convention concrete.
|
|
56
|
+
|
|
57
|
+
## Example
|
|
58
|
+
See examples/example.md.
|
|
59
|
+
|
|
60
|
+
",
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`renderKiroSteering > renders Kiro steering content for fixture skills 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"scoped": "---
|
|
6
|
+
inclusion: manual
|
|
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 {}
|
|
19
|
+
|
|
20
|
+
",
|
|
21
|
+
"simple": "---
|
|
22
|
+
inclusion: always
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Skill: Simple Skill
|
|
26
|
+
|
|
27
|
+
## Rule
|
|
28
|
+
Always keep the code readable.
|
|
29
|
+
|
|
30
|
+
## Why
|
|
31
|
+
Readable code is easier to review and maintain.
|
|
32
|
+
|
|
33
|
+
## Example
|
|
34
|
+
Prefer a named helper over a dense inline expression.
|
|
35
|
+
|
|
36
|
+
",
|
|
37
|
+
"with-examples": "---
|
|
38
|
+
inclusion: manual
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
# Skill: With Examples
|
|
42
|
+
|
|
43
|
+
## Rule
|
|
44
|
+
Keep examples near the skill.
|
|
45
|
+
|
|
46
|
+
## Why
|
|
47
|
+
Examples make a convention concrete.
|
|
48
|
+
|
|
49
|
+
## Example
|
|
50
|
+
See examples/example.md.
|
|
51
|
+
|
|
52
|
+
",
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { upsertManagedBlock } from "../../src/compiler/block";
|
|
3
|
+
|
|
4
|
+
describe("upsertManagedBlock", () => {
|
|
5
|
+
it("is idempotent", () => {
|
|
6
|
+
const once = upsertManagedBlock(null, "generated");
|
|
7
|
+
expect(upsertManagedBlock(once, "generated")).toBe(once);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("preserves user content outside the managed block", () => {
|
|
11
|
+
const existing = ["user header", "", "<!-- nymor:start -->", "old", "<!-- nymor:end -->", "", "user footer", ""].join("\n");
|
|
12
|
+
|
|
13
|
+
expect(upsertManagedBlock(existing, "new")).toMatchInlineSnapshot(`
|
|
14
|
+
"user header
|
|
15
|
+
|
|
16
|
+
<!-- nymor:start -->
|
|
17
|
+
new
|
|
18
|
+
<!-- nymor:end -->
|
|
19
|
+
|
|
20
|
+
user footer
|
|
21
|
+
"
|
|
22
|
+
`);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { compileClaudeSkills } from "../../src/compiler/claude";
|
|
6
|
+
import { loadSkills } from "../../src/utils/skills";
|
|
7
|
+
|
|
8
|
+
const fixturesDir = path.resolve(__dirname, "..", "fixtures", "skills");
|
|
9
|
+
|
|
10
|
+
describe("compileClaudeSkills", () => {
|
|
11
|
+
it("copies every skill folder", async () => {
|
|
12
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-claude-"));
|
|
13
|
+
const skills = await loadSkills(fixturesDir);
|
|
14
|
+
|
|
15
|
+
await compileClaudeSkills(skills, projectRoot);
|
|
16
|
+
|
|
17
|
+
expect(await readTree(path.join(projectRoot, ".claude", "skills"))).toMatchSnapshot();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function readTree(root: string): Promise<Record<string, string>> {
|
|
22
|
+
const files = await listFiles(root);
|
|
23
|
+
const output: Record<string, string> = {};
|
|
24
|
+
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
output[path.relative(root, file)] = await fs.readFile(file, "utf8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return output;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function listFiles(root: string): Promise<string[]> {
|
|
33
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
34
|
+
const files: string[] = [];
|
|
35
|
+
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const entryPath = path.join(root, entry.name);
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
files.push(...(await listFiles(entryPath)));
|
|
40
|
+
} else {
|
|
41
|
+
files.push(entryPath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return files.sort();
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { renderCopilotInstructions } from "../../src/compiler/copilot";
|
|
4
|
+
import { loadSkills } from "../../src/utils/skills";
|
|
5
|
+
|
|
6
|
+
const fixturesDir = path.resolve(__dirname, "..", "fixtures", "skills");
|
|
7
|
+
|
|
8
|
+
describe("renderCopilotInstructions", () => {
|
|
9
|
+
it("renders GitHub instruction content for fixture skills", async () => {
|
|
10
|
+
const skills = await loadSkills(fixturesDir);
|
|
11
|
+
const output = Object.fromEntries(skills.map((skill) => [skill.id, renderCopilotInstructions(skill)]));
|
|
12
|
+
|
|
13
|
+
expect(output).toMatchSnapshot();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { renderCursorRule } from "../../src/compiler/cursor";
|
|
4
|
+
import { loadSkills } from "../../src/utils/skills";
|
|
5
|
+
|
|
6
|
+
const fixturesDir = path.resolve(__dirname, "..", "fixtures", "skills");
|
|
7
|
+
|
|
8
|
+
describe("renderCursorRule", () => {
|
|
9
|
+
it("renders .mdc rule content for fixture skills", async () => {
|
|
10
|
+
const skills = await loadSkills(fixturesDir);
|
|
11
|
+
const output = Object.fromEntries(skills.map((skill) => [skill.id, renderCursorRule(skill)]));
|
|
12
|
+
|
|
13
|
+
expect(output).toMatchSnapshot();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { renderKiroSteering } from "../../src/compiler/kiro";
|
|
4
|
+
import { loadSkills } from "../../src/utils/skills";
|
|
5
|
+
|
|
6
|
+
const fixturesDir = path.resolve(__dirname, "..", "fixtures", "skills");
|
|
7
|
+
|
|
8
|
+
describe("renderKiroSteering", () => {
|
|
9
|
+
it("renders Kiro steering content for fixture skills", async () => {
|
|
10
|
+
const skills = await loadSkills(fixturesDir);
|
|
11
|
+
const output = Object.fromEntries(skills.map((skill) => [skill.id, renderKiroSteering(skill)]));
|
|
12
|
+
|
|
13
|
+
expect(output).toMatchSnapshot();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { detectAgents } from "../../src/detector/agents";
|
|
6
|
+
|
|
7
|
+
describe("detectAgents", () => {
|
|
8
|
+
it("detects known agent surfaces", async () => {
|
|
9
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-agents-"));
|
|
10
|
+
await fs.ensureDir(path.join(projectRoot, ".claude"));
|
|
11
|
+
await fs.ensureDir(path.join(projectRoot, ".cursor"));
|
|
12
|
+
await fs.ensureDir(path.join(projectRoot, ".github", "instructions"));
|
|
13
|
+
await fs.ensureDir(path.join(projectRoot, ".kiro"));
|
|
14
|
+
await fs.writeFile(path.join(projectRoot, "AGENTS.md"), "agents", "utf8");
|
|
15
|
+
await fs.writeFile(path.join(projectRoot, "GEMINI.md"), "gemini", "utf8");
|
|
16
|
+
await fs.ensureDir(path.join(projectRoot, ".windsurf"));
|
|
17
|
+
await fs.ensureDir(path.join(projectRoot, ".goose"));
|
|
18
|
+
await fs.ensureDir(path.join(projectRoot, ".opencode"));
|
|
19
|
+
|
|
20
|
+
await expect(detectAgents(projectRoot)).resolves.toEqual({
|
|
21
|
+
claude: true,
|
|
22
|
+
cursor: true,
|
|
23
|
+
copilot: true,
|
|
24
|
+
kiro: true,
|
|
25
|
+
"agents-md": true,
|
|
26
|
+
gemini: true,
|
|
27
|
+
windsurf: true,
|
|
28
|
+
goose: true,
|
|
29
|
+
opencode: true
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns false for absent agents", async () => {
|
|
34
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-agents-"));
|
|
35
|
+
|
|
36
|
+
await expect(detectAgents(projectRoot)).resolves.toEqual({
|
|
37
|
+
claude: false,
|
|
38
|
+
cursor: false,
|
|
39
|
+
copilot: false,
|
|
40
|
+
kiro: false,
|
|
41
|
+
"agents-md": false,
|
|
42
|
+
gemini: false,
|
|
43
|
+
windsurf: false,
|
|
44
|
+
goose: false,
|
|
45
|
+
opencode: false
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { detectStack, Stack } from "../../src/detector/stack";
|
|
6
|
+
|
|
7
|
+
const fixturesRoot = path.resolve(__dirname, "..", "fixtures", "stacks");
|
|
8
|
+
|
|
9
|
+
describe("detectStack", () => {
|
|
10
|
+
const cases: Array<[string, Stack]> = [
|
|
11
|
+
["nodejs", "nodejs"],
|
|
12
|
+
["react", "react"],
|
|
13
|
+
["vue", "vue"],
|
|
14
|
+
["django", "django"],
|
|
15
|
+
["fastapi", "fastapi"],
|
|
16
|
+
["rust", "rust"],
|
|
17
|
+
["go", "go"]
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
it.each(cases)("detects %s", async (fixture, expected) => {
|
|
21
|
+
await expect(detectStack(path.join(fixturesRoot, fixture))).resolves.toBe(expected);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns null when no stack signals are present", async () => {
|
|
25
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), "nymor-stack-"));
|
|
26
|
+
|
|
27
|
+
await expect(detectStack(projectRoot)).resolves.toBeNull();
|
|
28
|
+
});
|
|
29
|
+
});
|