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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +237 -0
  3. package/dist/agents/targets.js +111 -0
  4. package/dist/commands/add.js +121 -0
  5. package/dist/commands/compile.js +159 -0
  6. package/dist/commands/doctor.js +205 -0
  7. package/dist/commands/init.js +98 -0
  8. package/dist/commands/inject.js +24 -0
  9. package/dist/commands/learn.js +145 -0
  10. package/dist/commands/list.js +55 -0
  11. package/dist/commands/remove.js +38 -0
  12. package/dist/commands/update.js +80 -0
  13. package/dist/commands/validate.js +82 -0
  14. package/dist/compiler/agentsmd.js +17 -0
  15. package/dist/compiler/block.js +25 -0
  16. package/dist/compiler/claude.js +16 -0
  17. package/dist/compiler/copilot.js +29 -0
  18. package/dist/compiler/cursor.js +38 -0
  19. package/dist/compiler/kiro.js +24 -0
  20. package/dist/detector/agents.js +24 -0
  21. package/dist/detector/stack.js +113 -0
  22. package/dist/index.js +52 -0
  23. package/dist/registry/cache.js +60 -0
  24. package/dist/registry/client.js +135 -0
  25. package/dist/registry/resolver.js +29 -0
  26. package/dist/registry/types.js +2 -0
  27. package/dist/templates/bootstrap.js +97 -0
  28. package/dist/templates/cicada-json.js +11 -0
  29. package/dist/templates/nymor-json.js +11 -0
  30. package/dist/utils/manifest.js +32 -0
  31. package/dist/utils/paths.js +30 -0
  32. package/dist/utils/skills.js +114 -0
  33. package/package.json +32 -0
  34. package/src/agents/targets.ts +141 -0
  35. package/src/commands/compile.ts +202 -0
  36. package/src/commands/doctor.ts +253 -0
  37. package/src/commands/init.ts +113 -0
  38. package/src/commands/learn.ts +175 -0
  39. package/src/commands/list.ts +57 -0
  40. package/src/commands/validate.ts +89 -0
  41. package/src/compiler/block.ts +26 -0
  42. package/src/compiler/claude.ts +13 -0
  43. package/src/compiler/copilot.ts +28 -0
  44. package/src/compiler/cursor.ts +38 -0
  45. package/src/compiler/kiro.ts +22 -0
  46. package/src/detector/agents.ts +26 -0
  47. package/src/detector/stack.ts +135 -0
  48. package/src/index.ts +59 -0
  49. package/src/templates/bootstrap.ts +109 -0
  50. package/src/templates/nymor-json.ts +15 -0
  51. package/src/utils/manifest.ts +38 -0
  52. package/src/utils/paths.ts +25 -0
  53. package/src/utils/skills.ts +152 -0
  54. package/tests/compiler/__snapshots__/claude.test.ts.snap +65 -0
  55. package/tests/compiler/__snapshots__/copilot.test.ts.snap +54 -0
  56. package/tests/compiler/__snapshots__/cursor.test.ts.snap +62 -0
  57. package/tests/compiler/__snapshots__/kiro.test.ts.snap +54 -0
  58. package/tests/compiler/block.test.ts +24 -0
  59. package/tests/compiler/claude.test.ts +46 -0
  60. package/tests/compiler/copilot.test.ts +15 -0
  61. package/tests/compiler/cursor.test.ts +15 -0
  62. package/tests/compiler/kiro.test.ts +15 -0
  63. package/tests/detector/agents.test.ts +48 -0
  64. package/tests/detector/stack.test.ts +29 -0
  65. package/tests/e2e/init-and-compile.test.ts +227 -0
  66. package/tests/fixtures/skills/scoped/SKILL.md +18 -0
  67. package/tests/fixtures/skills/simple/SKILL.md +16 -0
  68. package/tests/fixtures/skills/with-examples/SKILL.md +18 -0
  69. package/tests/fixtures/skills/with-examples/examples/example.md +3 -0
  70. package/tests/fixtures/stacks/django/manage.py +2 -0
  71. package/tests/fixtures/stacks/django/requirements.txt +1 -0
  72. package/tests/fixtures/stacks/fastapi/requirements.txt +1 -0
  73. package/tests/fixtures/stacks/go/go.mod +3 -0
  74. package/tests/fixtures/stacks/nodejs/package.json +5 -0
  75. package/tests/fixtures/stacks/react/package.json +5 -0
  76. package/tests/fixtures/stacks/rust/Cargo.toml +4 -0
  77. package/tests/fixtures/stacks/vue/package.json +8 -0
  78. package/tests/fixtures/stacks/vue/vite.config.ts +5 -0
  79. package/tests/utils/manifest.test.ts +31 -0
  80. package/tests/utils/paths.test.ts +23 -0
  81. package/tests/utils/skills.test.ts +49 -0
  82. package/tsconfig.json +14 -0
  83. 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
+ });