opencode-agent-skills-md 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/dist/cli.mjs +770 -0
- package/dist/plugin.mjs +1138 -0
- package/dist/src/cli/config.d.ts +144 -0
- package/dist/src/cli/install.d.ts +33 -0
- package/dist/src/cli/main.d.ts +11 -0
- package/dist/src/cli/real-fs.d.ts +6 -0
- package/dist/src/cli/status.d.ts +34 -0
- package/dist/src/cli/uninstall.d.ts +22 -0
- package/dist/src/host.d.ts +51 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/plugin.d.ts +35 -0
- package/dist/src/sdk.d.ts +51 -0
- package/dist/src/tools.d.ts +86 -0
- package/package.json +48 -18
- package/{packages/opencode-agent-skills-md/src → src}/cli/main.ts +20 -4
- package/.beads/.local_version +0 -1
- package/.beads/README.md +0 -81
- package/.beads/config.yaml +0 -61
- package/.beads/deletions.jsonl +0 -1
- package/.beads/issues.jsonl +0 -64
- package/.beads/metadata.json +0 -4
- package/.gitattributes +0 -3
- package/.github/CODEOWNERS +0 -1
- package/.github/copilot-instructions.md +0 -78
- package/.github/dependabot.yml +0 -13
- package/.github/workflows/release.yml +0 -51
- package/.opencode/command/test-compaction.md +0 -9
- package/.opencode/command/test-find-skills.md +0 -7
- package/.opencode/command/test-read-skill-file.md +0 -14
- package/.opencode/command/test-run-skill-script.md +0 -13
- package/.opencode/command/test-skills.md +0 -14
- package/.opencode/command/test-use-skill.md +0 -10
- package/.opencode/skills/git-helper/SKILL.md +0 -65
- package/.opencode/skills/test-skill/SKILL.md +0 -43
- package/.opencode/skills/test-skill/example-config.json +0 -16
- package/.opencode/skills/test-skill/helper-docs.md +0 -29
- package/.opencode/skills/test-skill/scripts/echo-args +0 -14
- package/.opencode/skills/test-skill/scripts/greet +0 -6
- package/AGENTS.md +0 -43
- package/CHANGELOG.md +0 -178
- package/Justfile +0 -39
- package/README.md +0 -189
- package/openspec/changes/archive/2026-06-14-skills-core-decouple/specs/core-decoupling/spec.md +0 -74
- package/openspec/changes/archive/2026-06-14-skills-core-decouple/tasks.md +0 -64
- package/openspec/changes/archive/2026-06-14-skills-core-decouple/verify-report.md +0 -75
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/apply-progress.md +0 -136
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/archive-report.md +0 -77
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/design.md +0 -89
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/proposal.md +0 -65
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/specs/core-decoupling/spec.md +0 -77
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/tasks.md +0 -65
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/verify-report.md +0 -165
- package/openspec/specs/core-decoupling/spec.md +0 -110
- package/packages/core/package.json +0 -30
- package/packages/core/src/content.d.ts +0 -16
- package/packages/core/src/content.ts +0 -30
- package/packages/core/src/debug.ts +0 -16
- package/packages/core/src/discovery.d.ts +0 -86
- package/packages/core/src/discovery.ts +0 -257
- package/packages/core/src/index.d.ts +0 -20
- package/packages/core/src/index.ts +0 -55
- package/packages/core/src/match.d.ts +0 -19
- package/packages/core/src/match.ts +0 -75
- package/packages/core/src/parse.d.ts +0 -26
- package/packages/core/src/parse.ts +0 -141
- package/packages/core/src/scripts.d.ts +0 -17
- package/packages/core/src/scripts.ts +0 -79
- package/packages/core/src/search.d.ts +0 -83
- package/packages/core/src/search.ts +0 -188
- package/packages/core/src/types.d.ts +0 -82
- package/packages/core/src/types.ts +0 -131
- package/packages/core/src/walk.ts +0 -109
- package/packages/core/tests/agnostic.test.ts +0 -346
- package/packages/core/tests/content.test.ts +0 -65
- package/packages/core/tests/discovery.test.ts +0 -370
- package/packages/core/tests/package-boundary.test.ts +0 -310
- package/packages/core/tests/parse-trigger.test.ts +0 -282
- package/packages/core/tests/search.test.ts +0 -374
- package/packages/core/tests/subpath.test.ts +0 -87
- package/packages/core/tsconfig.json +0 -10
- package/packages/opencode-agent-skills-md/package.json +0 -42
- package/packages/opencode-agent-skills-md/rolldown.config.js +0 -48
- package/packages/opencode-agent-skills-md/tests/cli-commands.test.ts +0 -1423
- package/packages/opencode-agent-skills-md/tests/e2e/startup-smoke.test.ts +0 -66
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.claude/skills/claude-user-only-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/shared-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/user-only-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.claude/skills/claude-project-only-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/go-tester/SKILL.md +0 -12
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/nested/team/nested-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/rust-tester/SKILL.md +0 -11
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/bin/echo.sh +0 -2
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/docs/reference.md +0 -1
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/shared-skill/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/using-superpowers/SKILL.md +0 -8
- package/packages/opencode-agent-skills-md/tests/integration/helpers/mock-opencode.ts +0 -114
- package/packages/opencode-agent-skills-md/tests/integration/plugin.test.ts +0 -316
- package/packages/opencode-agent-skills-md/tests/integration/skill-discovery.test.ts +0 -315
- package/packages/opencode-agent-skills-md/tests/opencode/host.test.ts +0 -179
- package/packages/opencode-agent-skills-md/tests/opencode/plugin.test.ts +0 -551
- package/packages/opencode-agent-skills-md/tests/opencode/subpath.test.ts +0 -66
- package/packages/opencode-agent-skills-md/tests/opencode/tools.test.ts +0 -213
- package/packages/opencode-agent-skills-md/tests/package-boundary.test.ts +0 -346
- package/packages/opencode-agent-skills-md/tests/tools-security.test.ts +0 -72
- package/packages/opencode-agent-skills-md/tsconfig.build.json +0 -11
- package/packages/opencode-agent-skills-md/tsconfig.json +0 -10
- package/plans/001-ci-gate.md +0 -177
- package/plans/002-is-path-safe.md +0 -243
- package/plans/003-escape-prompts.md +0 -310
- package/plans/004-test-security-paths.md +0 -228
- package/plans/005-stop-swallowing-errors.md +0 -246
- package/plans/006-preserve-jsonc-commas.md +0 -144
- package/plans/007-write-before-purge.md +0 -144
- package/plans/008-reuse-walkdir-for-list-skill-files.md +0 -164
- package/plans/README.md +0 -43
- package/pnpm-workspace.yaml +0 -6
- package/tests/workspace.test.ts +0 -367
- package/tsconfig.json +0 -15
- /package/{packages/opencode-agent-skills-md/src → src}/cli/config.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/cli/install.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/cli/real-fs.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/cli/status.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/cli/uninstall.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/host.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/index.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/plugin.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/sdk.ts +0 -0
- /package/{packages/opencode-agent-skills-md/src → src}/tools.ts +0 -0
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, mkdir, rm, writeFile, chmod } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { after, before, describe, test } from "node:test";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* `getSkillSummaries` is the preflight path that builds the list of
|
|
9
|
-
* `SkillSummary` records the plugin feeds into the keyword matcher and
|
|
10
|
-
* the `<available-skills>` injection. PR 2 threads the `trigger`
|
|
11
|
-
* frontmatter key through it so the matcher can rank by trigger and
|
|
12
|
-
* the targeted outputs can render trigger text.
|
|
13
|
-
*/
|
|
14
|
-
describe("getSkillSummaries trigger passthrough (PR 2)", () => {
|
|
15
|
-
let workspace: string;
|
|
16
|
-
|
|
17
|
-
before(async () => {
|
|
18
|
-
workspace = await mkdtemp(path.join(tmpdir(), "opencode-agent-skills-md-summaries-"));
|
|
19
|
-
const projectRoot = path.join(workspace, ".opencode", "skills");
|
|
20
|
-
await mkdir(path.join(projectRoot, "with-trigger"), { recursive: true });
|
|
21
|
-
await mkdir(path.join(projectRoot, "no-trigger"), { recursive: true });
|
|
22
|
-
|
|
23
|
-
await writeFile(
|
|
24
|
-
path.join(projectRoot, "with-trigger", "SKILL.md"),
|
|
25
|
-
[
|
|
26
|
-
"---",
|
|
27
|
-
"name: with-trigger",
|
|
28
|
-
"description: skill whose frontmatter carries a trigger",
|
|
29
|
-
"trigger: auth, login",
|
|
30
|
-
"---",
|
|
31
|
-
"",
|
|
32
|
-
"# With Trigger",
|
|
33
|
-
"",
|
|
34
|
-
].join("\n"),
|
|
35
|
-
"utf8"
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
await writeFile(
|
|
39
|
-
path.join(projectRoot, "no-trigger", "SKILL.md"),
|
|
40
|
-
[
|
|
41
|
-
"---",
|
|
42
|
-
"name: no-trigger",
|
|
43
|
-
"description: skill without a trigger",
|
|
44
|
-
"---",
|
|
45
|
-
"",
|
|
46
|
-
"# No Trigger",
|
|
47
|
-
"",
|
|
48
|
-
].join("\n"),
|
|
49
|
-
"utf8"
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
after(async () => {
|
|
54
|
-
if (workspace) {
|
|
55
|
-
await rm(workspace, { recursive: true, force: true });
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("threads `trigger` through to SkillSummary when present", async () => {
|
|
60
|
-
const { getSkillSummaries } = await import("../src/index");
|
|
61
|
-
const summaries = await getSkillSummaries(workspace);
|
|
62
|
-
|
|
63
|
-
const withTrigger = summaries.find((s) => s.name === "with-trigger");
|
|
64
|
-
assert.ok(withTrigger, "with-trigger summary is present");
|
|
65
|
-
assert.equal(withTrigger!.trigger, "auth, login", "trigger is preserved on the summary");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("leaves `trigger` undefined when the skill has no trigger key", async () => {
|
|
69
|
-
const { getSkillSummaries } = await import("../src/index");
|
|
70
|
-
const summaries = await getSkillSummaries(workspace);
|
|
71
|
-
|
|
72
|
-
const noTrigger = summaries.find((s) => s.name === "no-trigger");
|
|
73
|
-
assert.ok(noTrigger, "no-trigger summary is present");
|
|
74
|
-
assert.equal(
|
|
75
|
-
noTrigger!.trigger,
|
|
76
|
-
undefined,
|
|
77
|
-
"trigger is undefined for skills that omit the frontmatter key"
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* `walkDir` (R2 of source-improvements-p1) is the shared directory walker
|
|
84
|
-
* that backs `findSkillsRecursive` and `findScripts`. These tests pin the
|
|
85
|
-
* cross-cutting behavior those callers depend on: hidden / dependency
|
|
86
|
-
* skip rules, depth bounds, per-entry error isolation, and the
|
|
87
|
-
* caller-extensible `skipDirs` option.
|
|
88
|
-
*/
|
|
89
|
-
describe("walkDir (R2 shared walker)", () => {
|
|
90
|
-
let workspace: string;
|
|
91
|
-
|
|
92
|
-
before(async () => {
|
|
93
|
-
workspace = await mkdtemp(path.join(tmpdir(), "opencode-agent-skills-md-walk-"));
|
|
94
|
-
await mkdir(path.join(workspace, "visible"), { recursive: true });
|
|
95
|
-
await mkdir(path.join(workspace, ".hidden"), { recursive: true });
|
|
96
|
-
await mkdir(path.join(workspace, "node_modules", "dep"), { recursive: true });
|
|
97
|
-
await mkdir(path.join(workspace, ".git", "objects"), { recursive: true });
|
|
98
|
-
await mkdir(path.join(workspace, "extra", "sub"), { recursive: true });
|
|
99
|
-
await writeFile(path.join(workspace, "visible", "file.txt"), "ok", "utf8");
|
|
100
|
-
await writeFile(path.join(workspace, ".hidden", "file.txt"), "skip", "utf8");
|
|
101
|
-
await writeFile(path.join(workspace, "node_modules", "dep", "file.txt"), "skip", "utf8");
|
|
102
|
-
await writeFile(path.join(workspace, ".git", "objects", "file.txt"), "skip", "utf8");
|
|
103
|
-
await writeFile(path.join(workspace, "extra", "sub", "file.txt"), "ok", "utf8");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
after(async () => {
|
|
107
|
-
if (workspace) await rm(workspace, { recursive: true, force: true });
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("skips hidden directories and the unconditional dependency dirs", async () => {
|
|
111
|
-
const { walkDir } = await import("../src/walk");
|
|
112
|
-
const visited: string[] = [];
|
|
113
|
-
await walkDir(workspace, 3, (entry) => {
|
|
114
|
-
visited.push(entry.name);
|
|
115
|
-
});
|
|
116
|
-
assert.ok(visited.includes("visible"), "visible dir is visited");
|
|
117
|
-
assert.ok(visited.includes("extra"), "extra dir is visited");
|
|
118
|
-
assert.ok(!visited.includes(".hidden"), "hidden dir is skipped");
|
|
119
|
-
assert.ok(!visited.includes("node_modules"), "node_modules is skipped");
|
|
120
|
-
assert.ok(!visited.includes(".git"), ".git is skipped");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("honors maxDepth and stops recursing beyond it", async () => {
|
|
124
|
-
const { walkDir } = await import("../src/walk");
|
|
125
|
-
const visited: string[] = [];
|
|
126
|
-
await walkDir(path.join(workspace, "extra"), 0, (entry) => {
|
|
127
|
-
visited.push(entry.name);
|
|
128
|
-
});
|
|
129
|
-
assert.deepEqual(visited, ["sub"], "only depth-0 entries are visited at maxDepth=0");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("isolates a throwing visitor so siblings still get visited", async () => {
|
|
133
|
-
const { walkDir } = await import("../src/walk");
|
|
134
|
-
const visited: string[] = [];
|
|
135
|
-
await walkDir(workspace, 1, (entry) => {
|
|
136
|
-
if (entry.name === "visible") throw new Error("boom");
|
|
137
|
-
visited.push(entry.name);
|
|
138
|
-
});
|
|
139
|
-
assert.ok(visited.includes("extra"), "sibling dirs are visited even when a peer throws");
|
|
140
|
-
assert.ok(!visited.includes("file.txt"), "the throwing dir's children are still skipped");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("accepts a caller-supplied skipDirs set", async () => {
|
|
144
|
-
const { walkDir } = await import("../src/walk");
|
|
145
|
-
const customSkip = new Set(["extra"]);
|
|
146
|
-
const visited: string[] = [];
|
|
147
|
-
await walkDir(workspace, 3, (entry) => {
|
|
148
|
-
visited.push(entry.name);
|
|
149
|
-
}, { skipDirs: customSkip });
|
|
150
|
-
assert.ok(visited.includes("visible"), "non-skipped dir is visited");
|
|
151
|
-
assert.ok(!visited.includes("extra"), "caller-supplied skipDirs is honored");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("is graceful when baseDir does not exist", async () => {
|
|
155
|
-
const { walkDir } = await import("../src/walk");
|
|
156
|
-
const ghost = path.join(workspace, "does-not-exist");
|
|
157
|
-
let called = false;
|
|
158
|
-
await walkDir(ghost, 3, () => { called = true; });
|
|
159
|
-
assert.equal(called, false, "visitor is never invoked for a missing baseDir");
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* `findSkillsRecursive` was refactored onto `walkDir`. These tests pin
|
|
165
|
-
* the behavior the refactor must preserve: root-first check, depth-bounded
|
|
166
|
-
* descent, sorted output, and graceful handling of missing roots.
|
|
167
|
-
*/
|
|
168
|
-
describe("findSkillsRecursive on walkDir (R2)", () => {
|
|
169
|
-
let workspace: string;
|
|
170
|
-
|
|
171
|
-
before(async () => {
|
|
172
|
-
workspace = await mkdtemp(path.join(tmpdir(), "opencode-agent-skills-md-fsr-"));
|
|
173
|
-
const root = path.join(workspace, "skills");
|
|
174
|
-
await mkdir(path.join(root, "alpha"), { recursive: true });
|
|
175
|
-
await mkdir(path.join(root, "beta", "nested"), { recursive: true });
|
|
176
|
-
await mkdir(path.join(root, ".hidden-skill"), { recursive: true });
|
|
177
|
-
await mkdir(path.join(root, "node_modules", "skill"), { recursive: true });
|
|
178
|
-
|
|
179
|
-
await writeFile(
|
|
180
|
-
path.join(root, "alpha", "SKILL.md"),
|
|
181
|
-
"---\nname: alpha\ndescription: top skill\n---\n# alpha\n",
|
|
182
|
-
"utf8"
|
|
183
|
-
);
|
|
184
|
-
await writeFile(
|
|
185
|
-
path.join(root, "beta", "SKILL.md"),
|
|
186
|
-
"---\nname: beta\ndescription: mid skill\n---\n# beta\n",
|
|
187
|
-
"utf8"
|
|
188
|
-
);
|
|
189
|
-
await writeFile(
|
|
190
|
-
path.join(root, "beta", "nested", "SKILL.md"),
|
|
191
|
-
"---\nname: beta-nested\ndescription: nested skill\n---\n# beta nested\n",
|
|
192
|
-
"utf8"
|
|
193
|
-
);
|
|
194
|
-
await writeFile(
|
|
195
|
-
path.join(root, ".hidden-skill", "SKILL.md"),
|
|
196
|
-
"---\nname: hidden\ndescription: must be skipped\n---\n",
|
|
197
|
-
"utf8"
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
after(async () => {
|
|
202
|
-
if (workspace) await rm(workspace, { recursive: true, force: true });
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("discovers SKILL.md at every depth the walker reaches", async () => {
|
|
206
|
-
const { findSkillsRecursive } = await import("../src/discovery");
|
|
207
|
-
const results = await findSkillsRecursive(
|
|
208
|
-
path.join(workspace, "skills"),
|
|
209
|
-
"project",
|
|
210
|
-
3
|
|
211
|
-
);
|
|
212
|
-
const names = results.map((r) => r.relativePath).sort();
|
|
213
|
-
assert.deepEqual(
|
|
214
|
-
names,
|
|
215
|
-
["alpha", "beta", "beta/nested"],
|
|
216
|
-
"alpha, beta, and beta/nested are discovered"
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("skips hidden directories and node_modules", async () => {
|
|
221
|
-
const { findSkillsRecursive } = await import("../src/discovery");
|
|
222
|
-
const results = await findSkillsRecursive(
|
|
223
|
-
path.join(workspace, "skills"),
|
|
224
|
-
"project",
|
|
225
|
-
3
|
|
226
|
-
);
|
|
227
|
-
const rels = results.map((r) => r.relativePath);
|
|
228
|
-
assert.ok(!rels.some((r) => r.includes(".hidden-skill")), "hidden dir is skipped");
|
|
229
|
-
assert.ok(!rels.some((r) => r.includes("node_modules")), "node_modules is skipped");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("returns results sorted by relativePath", async () => {
|
|
233
|
-
const { findSkillsRecursive } = await import("../src/discovery");
|
|
234
|
-
const results = await findSkillsRecursive(
|
|
235
|
-
path.join(workspace, "skills"),
|
|
236
|
-
"project",
|
|
237
|
-
3
|
|
238
|
-
);
|
|
239
|
-
const rels = results.map((r) => r.relativePath);
|
|
240
|
-
const sorted = [...rels].sort((a, b) => a.localeCompare(b));
|
|
241
|
-
assert.deepEqual(rels, sorted, "result order is sorted by relativePath");
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test("returns [] when baseDir is missing", async () => {
|
|
245
|
-
const { findSkillsRecursive } = await import("../src/discovery");
|
|
246
|
-
const results = await findSkillsRecursive(
|
|
247
|
-
path.join(workspace, "does-not-exist"),
|
|
248
|
-
"project",
|
|
249
|
-
3
|
|
250
|
-
);
|
|
251
|
-
assert.deepEqual(results, [], "missing baseDir yields no results");
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* `findScripts` was refactored onto `walkDir` with the script-specific
|
|
257
|
-
* `skipDirs` set. These tests pin the executable-bit filter and the
|
|
258
|
-
* `__pycache__` / `.venv` skip semantics the refactor must preserve.
|
|
259
|
-
*/
|
|
260
|
-
describe("findScripts on walkDir (R2)", () => {
|
|
261
|
-
let workspace: string;
|
|
262
|
-
|
|
263
|
-
before(async () => {
|
|
264
|
-
workspace = await mkdtemp(path.join(tmpdir(), "opencode-agent-skills-md-fs-"));
|
|
265
|
-
await mkdir(path.join(workspace, "scripts", "bin"), { recursive: true });
|
|
266
|
-
await mkdir(path.join(workspace, "scripts", "__pycache__"), { recursive: true });
|
|
267
|
-
await mkdir(path.join(workspace, "scripts", ".venv", "bin"), { recursive: true });
|
|
268
|
-
|
|
269
|
-
const exec = path.join(workspace, "scripts", "bin", "run.sh");
|
|
270
|
-
const plain = path.join(workspace, "scripts", "bin", "README.md");
|
|
271
|
-
const pyCache = path.join(workspace, "scripts", "__pycache__", "mod.pyc");
|
|
272
|
-
const venvExec = path.join(workspace, "scripts", ".venv", "bin", "python");
|
|
273
|
-
|
|
274
|
-
await writeFile(exec, "#!/bin/sh\necho hi\n", "utf8");
|
|
275
|
-
await writeFile(plain, "not executable", "utf8");
|
|
276
|
-
await writeFile(pyCache, "fake bytecode", "utf8");
|
|
277
|
-
await writeFile(venvExec, "fake venv binary", "utf8");
|
|
278
|
-
|
|
279
|
-
await chmod(exec, 0o755);
|
|
280
|
-
await chmod(pyCache, 0o755);
|
|
281
|
-
await chmod(venvExec, 0o755);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
after(async () => {
|
|
285
|
-
if (workspace) await rm(workspace, { recursive: true, force: true });
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test("returns only files with the executable bit set", async () => {
|
|
289
|
-
const { findScripts } = await import("../src/scripts");
|
|
290
|
-
const scripts = await findScripts(path.join(workspace, "scripts"), 3);
|
|
291
|
-
const rels = scripts.map((s) => s.relativePath);
|
|
292
|
-
assert.deepEqual(rels, ["bin/run.sh"], "only the executable file is returned");
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("skips __pycache__ and .venv via the caller-supplied skipDirs", async () => {
|
|
296
|
-
const { findScripts } = await import("../src/scripts");
|
|
297
|
-
const scripts = await findScripts(path.join(workspace, "scripts"), 3);
|
|
298
|
-
const rels = scripts.map((s) => s.relativePath);
|
|
299
|
-
assert.ok(!rels.some((r) => r.includes("__pycache__")), "__pycache__ is skipped");
|
|
300
|
-
assert.ok(!rels.some((r) => r.includes(".venv")), ".venv is skipped");
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
describe("listSkillFiles", () => {
|
|
305
|
-
let workspace: string;
|
|
306
|
-
|
|
307
|
-
before(async () => {
|
|
308
|
-
workspace = await mkdtemp(path.join(tmpdir(), "opencode-agent-skills-md-lsf-"));
|
|
309
|
-
const root = path.join(workspace, "skill-dir");
|
|
310
|
-
await mkdir(path.join(root, "docs"), { recursive: true });
|
|
311
|
-
await mkdir(path.join(root, ".hidden"), { recursive: true });
|
|
312
|
-
await mkdir(path.join(root, "node_modules", "some-dep"), { recursive: true });
|
|
313
|
-
await mkdir(path.join(root, ".git", "objects"), { recursive: true });
|
|
314
|
-
await mkdir(path.join(root, "nested", "deep"), { recursive: true });
|
|
315
|
-
|
|
316
|
-
await writeFile(path.join(root, "SKILL.md"), "# skill\n", "utf8");
|
|
317
|
-
await writeFile(path.join(root, "docs", "guide.md"), "# guide\n", "utf8");
|
|
318
|
-
await writeFile(path.join(root, ".hidden", "secret.txt"), "skip\n", "utf8");
|
|
319
|
-
await writeFile(path.join(root, "node_modules", "some-dep", "index.js"), "skip\n", "utf8");
|
|
320
|
-
await writeFile(path.join(root, ".git", "objects", "pack.idx"), "skip\n", "utf8");
|
|
321
|
-
await writeFile(path.join(root, "nested", "deep", "file.txt"), "ok\n", "utf8");
|
|
322
|
-
await writeFile(path.join(root, "nested", "README.md"), "ok\n", "utf8");
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
after(async () => {
|
|
326
|
-
if (workspace) await rm(workspace, { recursive: true, force: true });
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
test("returns sorted relative paths for visible files, excluding SKILL.md", async () => {
|
|
330
|
-
const { listSkillFiles } = await import("../src/discovery");
|
|
331
|
-
const root = path.join(workspace, "skill-dir");
|
|
332
|
-
const files = await listSkillFiles(root, 3);
|
|
333
|
-
assert.deepEqual(files, ["docs/guide.md", "nested/README.md", "nested/deep/file.txt"]);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test("skips hidden directories", async () => {
|
|
337
|
-
const { listSkillFiles } = await import("../src/discovery");
|
|
338
|
-
const root = path.join(workspace, "skill-dir");
|
|
339
|
-
const files = await listSkillFiles(root, 3);
|
|
340
|
-
assert.ok(!files.some((f) => f.includes(".hidden")), "no files from hidden dirs");
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
test("skips node_modules", async () => {
|
|
344
|
-
const { listSkillFiles } = await import("../src/discovery");
|
|
345
|
-
const root = path.join(workspace, "skill-dir");
|
|
346
|
-
const files = await listSkillFiles(root, 3);
|
|
347
|
-
assert.ok(!files.some((f) => f.includes("node_modules")), "node_modules is skipped");
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
test("skips .git", async () => {
|
|
351
|
-
const { listSkillFiles } = await import("../src/discovery");
|
|
352
|
-
const root = path.join(workspace, "skill-dir");
|
|
353
|
-
const files = await listSkillFiles(root, 3);
|
|
354
|
-
assert.ok(!files.some((f) => f.includes(".git")), ".git is skipped");
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test("returns empty array when base directory does not exist", async () => {
|
|
358
|
-
const { listSkillFiles } = await import("../src/discovery");
|
|
359
|
-
const ghost = path.join(workspace, "does-not-exist");
|
|
360
|
-
const files = await listSkillFiles(ghost, 3);
|
|
361
|
-
assert.deepEqual(files, []);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("honors maxDepth", async () => {
|
|
365
|
-
const { listSkillFiles } = await import("../src/discovery");
|
|
366
|
-
const root = path.join(workspace, "skill-dir");
|
|
367
|
-
const files = await listSkillFiles(root, 1);
|
|
368
|
-
assert.deepEqual(files, ["docs/guide.md", "nested/README.md"]);
|
|
369
|
-
});
|
|
370
|
-
});
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package boundary contract for `opencode-agent-skills-md-core`.
|
|
3
|
-
*
|
|
4
|
-
* This is the new home for the agnostic core engine. It encodes the spec
|
|
5
|
-
* scenarios from `sdd/split-core-opencode-packages/spec`:
|
|
6
|
-
*
|
|
7
|
-
* 1. The core package exposes the public API listed in design.md
|
|
8
|
-
* (`discoverAllSkills`, `parseSkillFile`, `resolveSkill`, plus the
|
|
9
|
-
* helpers used by the plugin).
|
|
10
|
-
* 2. Zero files under `packages/core/src/` reference the OpenCode host
|
|
11
|
-
* SDK (`@opencode-ai/plugin`) anywhere in source text.
|
|
12
|
-
* 3. The package manifest declares the runtime entry as a standalone ESM
|
|
13
|
-
* module so other harnesses can depend on it without pulling the
|
|
14
|
-
* OpenCode SDK.
|
|
15
|
-
*
|
|
16
|
-
* The test runs from inside `packages/core/tests/`, so the relative paths
|
|
17
|
-
* use `../src/` to resolve against the package's own sources.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import assert from "node:assert/strict";
|
|
21
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
22
|
-
import { createRequire } from "node:module";
|
|
23
|
-
import * as path from "node:path";
|
|
24
|
-
import { describe, test } from "node:test";
|
|
25
|
-
|
|
26
|
-
const require = createRequire(import.meta.url);
|
|
27
|
-
const SRC_DIR = path.resolve(import.meta.dirname, "..", "src");
|
|
28
|
-
const PKG_DIR = path.resolve(import.meta.dirname, "..");
|
|
29
|
-
const REPO_ROOT = path.resolve(PKG_DIR, "..", "..");
|
|
30
|
-
|
|
31
|
-
describe("opencode-agent-skills-md-core package boundary", () => {
|
|
32
|
-
test("manifest declares an ESM package whose runtime excludes @opencode-ai/plugin", async () => {
|
|
33
|
-
const pkgPath = path.join(PKG_DIR, "package.json");
|
|
34
|
-
const raw = await readFile(pkgPath, "utf8");
|
|
35
|
-
const manifest = JSON.parse(raw) as Record<string, unknown>;
|
|
36
|
-
|
|
37
|
-
assert.equal(manifest.name, "opencode-agent-skills-md-core", "package name");
|
|
38
|
-
assert.equal(manifest.type, "module", "package type must be ESM");
|
|
39
|
-
assert.equal(
|
|
40
|
-
manifest.private,
|
|
41
|
-
true,
|
|
42
|
-
"package must be private until publishing is wired in a later PR"
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const dependencies = (manifest.dependencies ?? {}) as Record<string, string>;
|
|
46
|
-
assert.equal(
|
|
47
|
-
dependencies["@opencode-ai/plugin"],
|
|
48
|
-
undefined,
|
|
49
|
-
"runtime dependencies must not include @opencode-ai/plugin"
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("manifest points its root export at the package's own entry file", async () => {
|
|
54
|
-
const pkgPath = path.join(PKG_DIR, "package.json");
|
|
55
|
-
const raw = await readFile(pkgPath, "utf8");
|
|
56
|
-
const manifest = JSON.parse(raw) as Record<string, unknown>;
|
|
57
|
-
const exports = manifest.exports as Record<string, unknown> | string | undefined;
|
|
58
|
-
|
|
59
|
-
assert.ok(exports && typeof exports === "object", "exports must be an object");
|
|
60
|
-
|
|
61
|
-
const rootExport = (exports as Record<string, unknown>)["."];
|
|
62
|
-
assert.ok(rootExport, "exports['.'] must be defined");
|
|
63
|
-
|
|
64
|
-
const importField = (rootExport as Record<string, unknown>).import;
|
|
65
|
-
assert.ok(
|
|
66
|
-
typeof importField === "string" && importField.includes("src/index"),
|
|
67
|
-
`exports['.'].import must point at src/index, got: ${String(importField)}`
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("public API surface matches the design contract", async () => {
|
|
72
|
-
const core = await import("../src/index.ts");
|
|
73
|
-
|
|
74
|
-
const expectedFunctions = [
|
|
75
|
-
"discoverAllSkills",
|
|
76
|
-
"parseSkillFile",
|
|
77
|
-
"resolveSkill",
|
|
78
|
-
"listSkillFiles",
|
|
79
|
-
"findScripts",
|
|
80
|
-
"isPathSafe",
|
|
81
|
-
"findClosestMatch",
|
|
82
|
-
"levenshtein",
|
|
83
|
-
"renderAvailableSkillsBlock",
|
|
84
|
-
"formatSkillListing",
|
|
85
|
-
"parseYamlFrontmatter",
|
|
86
|
-
"escapeRegex",
|
|
87
|
-
"keywordMatch",
|
|
88
|
-
"scoreSkill",
|
|
89
|
-
"searchSkills",
|
|
90
|
-
"tokenize",
|
|
91
|
-
"getSkillSummaries",
|
|
92
|
-
"getDefaultOpencodeRoots",
|
|
93
|
-
"defaultOnDuplicate",
|
|
94
|
-
"findSkillsRecursive",
|
|
95
|
-
"findFile",
|
|
96
|
-
] as const;
|
|
97
|
-
|
|
98
|
-
for (const name of expectedFunctions) {
|
|
99
|
-
assert.equal(
|
|
100
|
-
typeof (core as Record<string, unknown>)[name],
|
|
101
|
-
"function",
|
|
102
|
-
`${name} must be exported as a function from the package entrypoint`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("zero references to @opencode-ai/plugin in packages/core/src/**", async () => {
|
|
108
|
-
const violations: Array<{ file: string; line: number; text: string }> = [];
|
|
109
|
-
|
|
110
|
-
async function walk(dir: string): Promise<void> {
|
|
111
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
112
|
-
for (const entry of entries) {
|
|
113
|
-
const fullPath = path.join(dir, entry.name);
|
|
114
|
-
const stats = await stat(fullPath);
|
|
115
|
-
if (stats.isDirectory()) {
|
|
116
|
-
await walk(fullPath);
|
|
117
|
-
} else if (stats.isFile() && entry.name.endsWith(".ts")) {
|
|
118
|
-
const text = await readFile(fullPath, "utf8");
|
|
119
|
-
const lines = text.split("\n");
|
|
120
|
-
for (let i = 0; i < lines.length; i++) {
|
|
121
|
-
const line = lines[i] ?? "";
|
|
122
|
-
if (line.includes("@opencode-ai/plugin")) {
|
|
123
|
-
violations.push({ file: fullPath, line: i + 1, text: line.trim() });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
await walk(SRC_DIR);
|
|
131
|
-
|
|
132
|
-
assert.deepEqual(
|
|
133
|
-
violations,
|
|
134
|
-
[],
|
|
135
|
-
`expected zero references to the host SDK under packages/core/src, found: ${JSON.stringify(violations)}`
|
|
136
|
-
);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("core sources are isolated from the OpenCode adapter (no imports across the boundary)", async () => {
|
|
140
|
-
const entries = await readdir(SRC_DIR, { withFileTypes: true });
|
|
141
|
-
const tsFiles = entries.filter(
|
|
142
|
-
(e) => e.isFile() && e.name.endsWith(".ts")
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
for (const entry of tsFiles) {
|
|
146
|
-
const text = await readFile(path.join(SRC_DIR, entry.name), "utf8");
|
|
147
|
-
assert.doesNotMatch(
|
|
148
|
-
text,
|
|
149
|
-
/from\s+["']\.\.\/opencode/,
|
|
150
|
-
`${entry.name} must not import from the OpenCode adapter`
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("SkillHostClient is declared in packages/core/src/types.ts and re-exported from the package entrypoint", async () => {
|
|
156
|
-
// Spec R2 (Boundary Interface Location): `SkillHostClient` SHALL be
|
|
157
|
-
// declared in the core package. The concrete OpenCode implementation
|
|
158
|
-
// lives in the plugin package only.
|
|
159
|
-
const typesSource = await readFile(path.join(SRC_DIR, "types.ts"), "utf8");
|
|
160
|
-
|
|
161
|
-
assert.match(
|
|
162
|
-
typesSource,
|
|
163
|
-
/export\s+interface\s+SkillHostClient\b/,
|
|
164
|
-
"SkillHostClient interface must be declared in packages/core/src/types.ts",
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
// The interface must NOT be redeclared elsewhere under packages/core/src/
|
|
168
|
-
// (a single declaration site is the boundary contract).
|
|
169
|
-
const redeclarationSites: string[] = [];
|
|
170
|
-
async function walk(dir: string): Promise<void> {
|
|
171
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
172
|
-
for (const entry of entries) {
|
|
173
|
-
const fullPath = path.join(dir, entry.name);
|
|
174
|
-
const stats = await stat(fullPath);
|
|
175
|
-
if (stats.isDirectory()) {
|
|
176
|
-
await walk(fullPath);
|
|
177
|
-
} else if (stats.isFile() && entry.name.endsWith(".ts")) {
|
|
178
|
-
const text = await readFile(fullPath, "utf8");
|
|
179
|
-
if (new RegExp(`(?:export\\s+)?interface\\s+SkillHostClient\\b`).test(text) && fullPath !== path.join(SRC_DIR, "types.ts")) {
|
|
180
|
-
redeclarationSites.push(fullPath);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
await walk(SRC_DIR);
|
|
186
|
-
assert.deepEqual(
|
|
187
|
-
redeclarationSites,
|
|
188
|
-
[],
|
|
189
|
-
`SkillHostClient must be declared only in types.ts, also found at: ${redeclarationSites.join(", ")}`,
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
// The interface must be reachable as a TypeScript type from the core
|
|
193
|
-
// entrypoint (this is what the plugin package's host.ts will import).
|
|
194
|
-
// `export type` is erased at runtime, so we verify by reading the
|
|
195
|
-
// entrypoint's source text — and by dynamically importing it (a
|
|
196
|
-
// broken index.ts would throw, so this still catches the failure
|
|
197
|
-
// mode where the re-export line is malformed).
|
|
198
|
-
const indexSource = await readFile(path.join(SRC_DIR, "index.ts"), "utf8");
|
|
199
|
-
assert.match(
|
|
200
|
-
indexSource,
|
|
201
|
-
/\bSkillHostClient\b/,
|
|
202
|
-
"SkillHostClient must be re-exported from packages/core/src/index.ts",
|
|
203
|
-
);
|
|
204
|
-
const core = (await import("../src/index.ts")) as unknown as Record<string, unknown>;
|
|
205
|
-
// Type-only export: not a runtime value, but the import must not throw.
|
|
206
|
-
assert.equal(core.SkillHostClient, undefined, "SkillHostClient is a TypeScript type, not a runtime value");
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("SkillHostSession is declared in packages/core/src/types.ts and re-exported from the package entrypoint", async () => {
|
|
210
|
-
// Spec R2 (Boundary Interface Location): `SkillHostSession` SHALL be
|
|
211
|
-
// declared in the core package. The plugin package implements the
|
|
212
|
-
// session factory but does NOT redeclare the interface.
|
|
213
|
-
const typesSource = await readFile(path.join(SRC_DIR, "types.ts"), "utf8");
|
|
214
|
-
|
|
215
|
-
assert.match(
|
|
216
|
-
typesSource,
|
|
217
|
-
/export\s+interface\s+SkillHostSession\b/,
|
|
218
|
-
"SkillHostSession interface must be declared in packages/core/src/types.ts",
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
// And reachable as a TypeScript type from the core entrypoint.
|
|
222
|
-
const indexSource = await readFile(path.join(SRC_DIR, "index.ts"), "utf8");
|
|
223
|
-
assert.match(
|
|
224
|
-
indexSource,
|
|
225
|
-
/\bSkillHostSession\b/,
|
|
226
|
-
"SkillHostSession must be re-exported from packages/core/src/index.ts",
|
|
227
|
-
);
|
|
228
|
-
const core = (await import("../src/index.ts")) as unknown as Record<string, unknown>;
|
|
229
|
-
assert.equal(core.SkillHostSession, undefined, "SkillHostSession is a TypeScript type, not a runtime value");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("SkillHostContext is declared in packages/core/src/types.ts and re-exported from the package entrypoint", async () => {
|
|
233
|
-
// The minimum shared context type needed to express `SkillHostClient`
|
|
234
|
-
// cleanly in core. Without it, the boundary interface would have to
|
|
235
|
-
// reach into the plugin package for its parameter type, defeating
|
|
236
|
-
// the boundary. The spec calls this out implicitly via R2.
|
|
237
|
-
const typesSource = await readFile(path.join(SRC_DIR, "types.ts"), "utf8");
|
|
238
|
-
|
|
239
|
-
assert.match(
|
|
240
|
-
typesSource,
|
|
241
|
-
/export\s+interface\s+SkillHostContext\b/,
|
|
242
|
-
"SkillHostContext interface must be declared in packages/core/src/types.ts",
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
const indexSource = await readFile(path.join(SRC_DIR, "index.ts"), "utf8");
|
|
246
|
-
assert.match(
|
|
247
|
-
indexSource,
|
|
248
|
-
/\bSkillHostContext\b/,
|
|
249
|
-
"SkillHostContext must be re-exported from packages/core/src/index.ts",
|
|
250
|
-
);
|
|
251
|
-
const core = (await import("../src/index.ts")) as unknown as Record<string, unknown>;
|
|
252
|
-
assert.equal(core.SkillHostContext, undefined, "SkillHostContext is a TypeScript type, not a runtime value");
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test("SkillHostClient declares the four boundary methods the core expects from any host", async () => {
|
|
256
|
-
// Triangulation: beyond proving the NAME exists, the structural shape
|
|
257
|
-
// of the boundary contract matters — the plugin's `createOpencodeSkillHost`
|
|
258
|
-
// returns an object that must be assignable to this interface. A name
|
|
259
|
-
// check alone would let an empty interface slip through.
|
|
260
|
-
const typesSource = await readFile(path.join(SRC_DIR, "types.ts"), "utf8");
|
|
261
|
-
|
|
262
|
-
// Slice the `SkillHostClient` interface body so the method-name checks
|
|
263
|
-
// stay scoped to the interface (avoids false positives from comments or
|
|
264
|
-
// other interfaces that happen to mention these names).
|
|
265
|
-
const interfaceBody = typesSource.match(
|
|
266
|
-
/export\s+interface\s+SkillHostClient\s*\{([\s\S]*?)\n\}/,
|
|
267
|
-
);
|
|
268
|
-
assert.ok(
|
|
269
|
-
interfaceBody,
|
|
270
|
-
"SkillHostClient must be an `export interface` block in packages/core/src/types.ts",
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
const body = interfaceBody![1]!;
|
|
274
|
-
for (const methodName of ["injectContent", "getSessionContext", "readFile", "readdir"]) {
|
|
275
|
-
assert.match(
|
|
276
|
-
body,
|
|
277
|
-
new RegExp(`\\b${methodName}\\s*\\(`),
|
|
278
|
-
`SkillHostClient must declare method ${methodName} (the core relies on it)`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// The interface must NOT be a type alias (`type X = ...`). Spec R2
|
|
283
|
-
// calls these "interfaces" and the plugin-side adapter may extend them
|
|
284
|
-
// structurally; a type alias would still satisfy the name check but
|
|
285
|
-
// break the design intent.
|
|
286
|
-
assert.doesNotMatch(
|
|
287
|
-
typesSource,
|
|
288
|
-
/export\s+type\s+SkillHostClient\b/,
|
|
289
|
-
"SkillHostClient must be declared as `interface`, not `type`",
|
|
290
|
-
);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test("package resolves through the workspace link as opencode-agent-skills-md-core", async () => {
|
|
294
|
-
// The root workspace symlinks packages/core into node_modules so the
|
|
295
|
-
// package can be resolved by name from the repo root after pnpm install.
|
|
296
|
-
const resolved = require.resolve("opencode-agent-skills-md-core");
|
|
297
|
-
|
|
298
|
-
assert.match(
|
|
299
|
-
resolved,
|
|
300
|
-
/[\\/]packages[\\/]core[\\/]src[\\/]index\.ts$/,
|
|
301
|
-
`expected opencode-agent-skills-md-core to resolve to packages/core/src/index.ts, got: ${resolved}`
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
// Sanity check: the resolved file lives inside the repo (no stale cache).
|
|
305
|
-
assert.ok(
|
|
306
|
-
resolved.startsWith(REPO_ROOT),
|
|
307
|
-
`resolved path must live under the repo root: ${resolved}`
|
|
308
|
-
);
|
|
309
|
-
});
|
|
310
|
-
});
|