opencode-agent-skills-md 1.0.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/.beads/.local_version +1 -0
- package/.beads/README.md +81 -0
- package/.beads/config.yaml +61 -0
- package/.beads/deletions.jsonl +1 -0
- package/.beads/issues.jsonl +64 -0
- package/.beads/metadata.json +4 -0
- package/.gitattributes +3 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/copilot-instructions.md +78 -0
- package/.github/dependabot.yml +13 -0
- package/.github/workflows/release.yml +51 -0
- package/.opencode/command/test-compaction.md +9 -0
- package/.opencode/command/test-find-skills.md +7 -0
- package/.opencode/command/test-read-skill-file.md +14 -0
- package/.opencode/command/test-run-skill-script.md +13 -0
- package/.opencode/command/test-skills.md +14 -0
- package/.opencode/command/test-use-skill.md +10 -0
- package/.opencode/skills/git-helper/SKILL.md +65 -0
- package/.opencode/skills/test-skill/SKILL.md +43 -0
- package/.opencode/skills/test-skill/example-config.json +16 -0
- package/.opencode/skills/test-skill/helper-docs.md +29 -0
- package/.opencode/skills/test-skill/scripts/echo-args +14 -0
- package/.opencode/skills/test-skill/scripts/greet +6 -0
- package/AGENTS.md +43 -0
- package/CHANGELOG.md +178 -0
- package/Justfile +39 -0
- package/LICENSE +9 -0
- package/README.md +189 -0
- package/openspec/changes/archive/2026-06-14-skills-core-decouple/specs/core-decoupling/spec.md +74 -0
- package/openspec/changes/archive/2026-06-14-skills-core-decouple/tasks.md +64 -0
- package/openspec/changes/archive/2026-06-14-skills-core-decouple/verify-report.md +75 -0
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/apply-progress.md +136 -0
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/archive-report.md +77 -0
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/design.md +89 -0
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/proposal.md +65 -0
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/specs/core-decoupling/spec.md +77 -0
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/tasks.md +65 -0
- package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/verify-report.md +165 -0
- package/openspec/specs/core-decoupling/spec.md +110 -0
- package/package.json +35 -0
- package/packages/core/package.json +30 -0
- package/packages/core/src/content.d.ts +16 -0
- package/packages/core/src/content.ts +30 -0
- package/packages/core/src/debug.ts +16 -0
- package/packages/core/src/discovery.d.ts +86 -0
- package/packages/core/src/discovery.ts +257 -0
- package/packages/core/src/index.d.ts +20 -0
- package/packages/core/src/index.ts +55 -0
- package/packages/core/src/match.d.ts +19 -0
- package/packages/core/src/match.ts +75 -0
- package/packages/core/src/parse.d.ts +26 -0
- package/packages/core/src/parse.ts +141 -0
- package/packages/core/src/scripts.d.ts +17 -0
- package/packages/core/src/scripts.ts +79 -0
- package/packages/core/src/search.d.ts +83 -0
- package/packages/core/src/search.ts +188 -0
- package/packages/core/src/types.d.ts +82 -0
- package/packages/core/src/types.ts +131 -0
- package/packages/core/src/walk.ts +109 -0
- package/packages/core/tests/agnostic.test.ts +346 -0
- package/packages/core/tests/content.test.ts +65 -0
- package/packages/core/tests/discovery.test.ts +370 -0
- package/packages/core/tests/package-boundary.test.ts +310 -0
- package/packages/core/tests/parse-trigger.test.ts +282 -0
- package/packages/core/tests/search.test.ts +374 -0
- package/packages/core/tests/subpath.test.ts +87 -0
- package/packages/core/tsconfig.json +10 -0
- package/packages/opencode-agent-skills-md/package.json +42 -0
- package/packages/opencode-agent-skills-md/rolldown.config.js +48 -0
- package/packages/opencode-agent-skills-md/src/cli/config.ts +522 -0
- package/packages/opencode-agent-skills-md/src/cli/install.ts +111 -0
- package/packages/opencode-agent-skills-md/src/cli/main.ts +201 -0
- package/packages/opencode-agent-skills-md/src/cli/real-fs.ts +51 -0
- package/packages/opencode-agent-skills-md/src/cli/status.ts +183 -0
- package/packages/opencode-agent-skills-md/src/cli/uninstall.ts +157 -0
- package/packages/opencode-agent-skills-md/src/host.ts +119 -0
- package/packages/opencode-agent-skills-md/src/index.ts +25 -0
- package/packages/opencode-agent-skills-md/src/plugin.ts +343 -0
- package/packages/opencode-agent-skills-md/src/sdk.ts +71 -0
- package/packages/opencode-agent-skills-md/src/tools.ts +373 -0
- package/packages/opencode-agent-skills-md/tests/cli-commands.test.ts +1423 -0
- package/packages/opencode-agent-skills-md/tests/e2e/startup-smoke.test.ts +66 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.claude/skills/claude-user-only-skill/SKILL.md +8 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/shared-skill/SKILL.md +8 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/user-only-skill/SKILL.md +8 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.claude/skills/claude-project-only-skill/SKILL.md +8 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/go-tester/SKILL.md +12 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/nested/team/nested-skill/SKILL.md +8 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/rust-tester/SKILL.md +11 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/SKILL.md +8 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/bin/echo.sh +2 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/docs/reference.md +1 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/shared-skill/SKILL.md +8 -0
- package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/using-superpowers/SKILL.md +8 -0
- package/packages/opencode-agent-skills-md/tests/integration/helpers/mock-opencode.ts +114 -0
- package/packages/opencode-agent-skills-md/tests/integration/plugin.test.ts +316 -0
- package/packages/opencode-agent-skills-md/tests/integration/skill-discovery.test.ts +315 -0
- package/packages/opencode-agent-skills-md/tests/opencode/host.test.ts +179 -0
- package/packages/opencode-agent-skills-md/tests/opencode/plugin.test.ts +551 -0
- package/packages/opencode-agent-skills-md/tests/opencode/subpath.test.ts +66 -0
- package/packages/opencode-agent-skills-md/tests/opencode/tools.test.ts +213 -0
- package/packages/opencode-agent-skills-md/tests/package-boundary.test.ts +346 -0
- package/packages/opencode-agent-skills-md/tests/tools-security.test.ts +72 -0
- package/packages/opencode-agent-skills-md/tsconfig.build.json +11 -0
- package/packages/opencode-agent-skills-md/tsconfig.json +10 -0
- package/plans/001-ci-gate.md +177 -0
- package/plans/002-is-path-safe.md +243 -0
- package/plans/003-escape-prompts.md +310 -0
- package/plans/004-test-security-paths.md +228 -0
- package/plans/005-stop-swallowing-errors.md +246 -0
- package/plans/006-preserve-jsonc-commas.md +144 -0
- package/plans/007-write-before-purge.md +144 -0
- package/plans/008-reuse-walkdir-for-list-skill-files.md +164 -0
- package/plans/README.md +43 -0
- package/pnpm-workspace.yaml +6 -0
- package/tests/workspace.test.ts +367 -0
- package/tsconfig.json +15 -0
package/plans/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Implementation Plans
|
|
2
|
+
|
|
3
|
+
Generated by the improve skill on 2026-07-03. Execute in the order below
|
|
4
|
+
unless dependencies say otherwise. Each executor: read the plan fully before
|
|
5
|
+
starting, honor its STOP conditions, and update your row when done.
|
|
6
|
+
|
|
7
|
+
## Execution order & status
|
|
8
|
+
|
|
9
|
+
| Plan | Title | Priority | Effort | Depends on | Status |
|
|
10
|
+
|------|-------|----------|--------|------------|--------|
|
|
11
|
+
| 001 | CI gating before release | P1 | S | — | TODO |
|
|
12
|
+
| 002 | `isPathSafe` symlink hardening | P1 | S | — | DONE |
|
|
13
|
+
| 003 | Escape XML and shell boundaries | P1 | M | — | DONE |
|
|
14
|
+
| 004 | Tests for security-sensitive paths | P1 | M | 002, 003 | DONE |
|
|
15
|
+
| 005 | Surface swallowed discovery errors | P2 | S | — | DONE |
|
|
16
|
+
| 006 | Preserve commas inside JSONC string values | P1 | S | — | DONE |
|
|
17
|
+
| 007 | Write config before purging plugin-owned directories | P1 | S | — | DONE |
|
|
18
|
+
| 008 | Reuse the shared walker in `listSkillFiles` | P2 | S | — | DONE |
|
|
19
|
+
|
|
20
|
+
Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale)
|
|
21
|
+
|
|
22
|
+
## Dependency notes
|
|
23
|
+
|
|
24
|
+
- 004 requires 002 and 003 because it adds tests that exercise the helpers
|
|
25
|
+
(`escapeXml`, `escapeShellArg`) and the hardened `isPathSafe` that those
|
|
26
|
+
plans introduce. Run 004 after both are done.
|
|
27
|
+
- Plans 006, 007, and 008 are independent of each other and of 001–005.
|
|
28
|
+
Execute 006 and 007 before 008 because they fix user-visible CLI correctness
|
|
29
|
+
bugs; 008 is characterization plus cleanup.
|
|
30
|
+
- All plans are otherwise independent and can execute in any order.
|
|
31
|
+
|
|
32
|
+
## Findings considered and rejected
|
|
33
|
+
|
|
34
|
+
- **Lint/format tooling (finding 6)**: Not worth doing independently because
|
|
35
|
+
adding Biome or ESLint without a style guide and CI integration creates
|
|
36
|
+
noise. The CI workflow (plan 001) is a prerequisite for lint gates.
|
|
37
|
+
- **Collapse repeated discovery (finding 7)**: Higher effort (M) for a purely
|
|
38
|
+
performance-oriented fix. Worth doing after the security/correctness surface
|
|
39
|
+
is hardened. Deferred.
|
|
40
|
+
- **Package entrypoint / dependency drift**: Not substantiated by the live
|
|
41
|
+
code — entrypoints are consistent across both packages.
|
|
42
|
+
- **Triple-discovery claim**: Overcounted. It's double discovery per tool call,
|
|
43
|
+
not triple. Still a real performance issue but lower priority.
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace boundary contract for the `opencode-agent-skills-md` repo root.
|
|
3
|
+
*
|
|
4
|
+
* PR 3 of `split-core-opencode-packages` turns the repo root into a pure
|
|
5
|
+
* pnpm workspace manifest: no source, no fixtures, no root-level build
|
|
6
|
+
* config. The two real packages live under `packages/core/` and
|
|
7
|
+
* `packages/opencode-agent-skills-md/`. This test pins the contracts that
|
|
8
|
+
* prove the consolidation actually happened:
|
|
9
|
+
*
|
|
10
|
+
* 1. Root manifest is a workspace manifest (private, no exports of its own,
|
|
11
|
+
* scripts delegate to packages via `pnpm -r`).
|
|
12
|
+
* 2. Legacy root sources (`src/`, root `tests/fixtures/`, root
|
|
13
|
+
* `rolldown.config.js`, root `tsconfig.build.json`) are gone — they
|
|
14
|
+
* are now owned by the per-package directories.
|
|
15
|
+
* 3. Docs (README, CHANGELOG, Justfile, AGENTS.md) route users to the
|
|
16
|
+
* correct package for each harness and reflect the workspace
|
|
17
|
+
* structure.
|
|
18
|
+
* 4. Both packages resolve from the repo root through the pnpm
|
|
19
|
+
* workspace link (the symlink pnpm install wires into
|
|
20
|
+
* `node_modules/`).
|
|
21
|
+
*
|
|
22
|
+
* The test runs from the repo root's `tests/` directory, so the relative
|
|
23
|
+
* paths use `..` to resolve against the repo itself.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
|
+
import { readFile, stat } from "node:fs/promises";
|
|
29
|
+
import { createRequire } from "node:module";
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
import * as path from "node:path";
|
|
32
|
+
import { describe, test } from "node:test";
|
|
33
|
+
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const REPO_ROOT = path.resolve(import.meta.dirname, "..");
|
|
36
|
+
|
|
37
|
+
describe("opencode-agent-skills-md workspace root", () => {
|
|
38
|
+
test("root package.json is a private workspace manifest with no exports of its own", async () => {
|
|
39
|
+
const pkgPath = path.join(REPO_ROOT, "package.json");
|
|
40
|
+
const raw = await readFile(pkgPath, "utf8");
|
|
41
|
+
const manifest = JSON.parse(raw) as Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
// Workspace roots must be private — pnpm treats them as metadata
|
|
44
|
+
// containers, not packages to publish. The plugin package
|
|
45
|
+
// (`opencode-agent-skills-md`) remains the installable artifact.
|
|
46
|
+
assert.equal(
|
|
47
|
+
manifest.private,
|
|
48
|
+
true,
|
|
49
|
+
"root package.json must be private (it is a workspace manifest, not a publishable package)",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// The root manifest must not pretend to expose an entrypoint — the
|
|
53
|
+
// packages under `packages/*` own those exports. Carrying a stale
|
|
54
|
+
// `main`/`exports` would invite consumers to import the workspace
|
|
55
|
+
// by mistake.
|
|
56
|
+
assert.equal(
|
|
57
|
+
manifest.main,
|
|
58
|
+
undefined,
|
|
59
|
+
"root package.json must not declare a `main` (the packages own their own entrypoints)",
|
|
60
|
+
);
|
|
61
|
+
assert.equal(
|
|
62
|
+
manifest.exports,
|
|
63
|
+
undefined,
|
|
64
|
+
"root package.json must not declare `exports` (the packages own their own entrypoints)",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Version + author metadata is fine to keep at the root.
|
|
68
|
+
assert.equal(typeof manifest.version, "string", "root package.json must carry a version string");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("root scripts delegate to packages via `pnpm -r` so a single command covers both packages", async () => {
|
|
72
|
+
const raw = await readFile(path.join(REPO_ROOT, "package.json"), "utf8");
|
|
73
|
+
const manifest = JSON.parse(raw) as Record<string, unknown>;
|
|
74
|
+
const scripts = (manifest.scripts ?? {}) as Record<string, string>;
|
|
75
|
+
|
|
76
|
+
for (const name of ["build", "test", "typecheck"]) {
|
|
77
|
+
const script = scripts[name];
|
|
78
|
+
assert.ok(
|
|
79
|
+
typeof script === "string" && script.length > 0,
|
|
80
|
+
`root scripts.${name} must be defined`,
|
|
81
|
+
);
|
|
82
|
+
assert.match(
|
|
83
|
+
script,
|
|
84
|
+
/pnpm\s+-r\b/,
|
|
85
|
+
`root scripts.${name} must delegate to packages via \`pnpm -r\`, got: ${script}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("root package.json declares a workspace itself via pnpm-workspace.yaml", async () => {
|
|
91
|
+
// Sanity check: the pnpm workspace declaration exists and lists the
|
|
92
|
+
// two expected package globs. pnpm-workspace.yaml is the source of
|
|
93
|
+
// truth for "this is a workspace".
|
|
94
|
+
const raw = await readFile(path.join(REPO_ROOT, "pnpm-workspace.yaml"), "utf8");
|
|
95
|
+
assert.match(
|
|
96
|
+
raw,
|
|
97
|
+
/packages:\s*\n\s*-\s+["']packages\/\*["']/,
|
|
98
|
+
"pnpm-workspace.yaml must declare the `packages/*` glob so pnpm wires the two packages into the workspace",
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("legacy root sources and build config are removed (packages now own them)", async () => {
|
|
103
|
+
// These were the legacy "root owns the build" surfaces from the
|
|
104
|
+
// pre-split layout. After PR 3 each one has a per-package home:
|
|
105
|
+
// - src/core/index.ts -> packages/core/src/index.ts
|
|
106
|
+
// - src/opencode/{4 files} -> packages/opencode-agent-skills-md/src/{4 files}
|
|
107
|
+
// - rolldown.config.js -> packages/opencode-agent-skills-md/rolldown.config.js
|
|
108
|
+
// - tsconfig.build.json -> packages/opencode-agent-skills-md/tsconfig.build.json
|
|
109
|
+
// - tests/fixtures/skills/** -> packages/opencode-agent-skills-md/tests/fixtures/skills/**
|
|
110
|
+
// The root sources/test config that remain (workspace manifest,
|
|
111
|
+
// AGENTS.md, README, etc.) are non-source artifacts.
|
|
112
|
+
for (const legacyPath of [
|
|
113
|
+
"src",
|
|
114
|
+
"rolldown.config.js",
|
|
115
|
+
"tsconfig.build.json",
|
|
116
|
+
"tests/fixtures",
|
|
117
|
+
]) {
|
|
118
|
+
const fullPath = path.join(REPO_ROOT, legacyPath);
|
|
119
|
+
assert.ok(
|
|
120
|
+
!existsSync(fullPath),
|
|
121
|
+
`legacy root path must be removed by PR 3, but still exists at ${fullPath}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The root `tsconfig.json` survives (it is used as a base for the
|
|
126
|
+
// per-package tsconfigs) — confirm it still exists and points at
|
|
127
|
+
// something reasonable.
|
|
128
|
+
const rootTsconfig = path.join(REPO_ROOT, "tsconfig.json");
|
|
129
|
+
assert.ok(existsSync(rootTsconfig), "root tsconfig.json must survive as the base for per-package tsconfigs");
|
|
130
|
+
const rootTsconfigRaw = await readFile(rootTsconfig, "utf8");
|
|
131
|
+
assert.match(
|
|
132
|
+
rootTsconfigRaw,
|
|
133
|
+
/"strict"\s*:\s*true/,
|
|
134
|
+
"root tsconfig.json must keep the strict-mode compiler options",
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("root src/core/index.ts compatibility shim has been removed (all callers use the workspace import now)", async () => {
|
|
139
|
+
// The shim only existed to keep legacy relative imports resolvable
|
|
140
|
+
// during PR 1+2; PR 3 deletes it because there are no more legacy
|
|
141
|
+
// callers (every consumer resolves `opencode-agent-skills-md-core`
|
|
142
|
+
// through the workspace link).
|
|
143
|
+
const shimPath = path.join(REPO_ROOT, "src", "core", "index.ts");
|
|
144
|
+
assert.ok(
|
|
145
|
+
!existsSync(shimPath),
|
|
146
|
+
`legacy compatibility shim must be removed by PR 3, but still exists at ${shimPath}`,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("README routes users to the correct package for each harness type", async () => {
|
|
151
|
+
const readme = await readFile(path.join(REPO_ROOT, "README.md"), "utf8");
|
|
152
|
+
|
|
153
|
+
// Both package names must appear so consumers can find each one.
|
|
154
|
+
assert.match(
|
|
155
|
+
readme,
|
|
156
|
+
/opencode-agent-skills-md-core/,
|
|
157
|
+
"README must mention the standalone core package",
|
|
158
|
+
);
|
|
159
|
+
assert.match(
|
|
160
|
+
readme,
|
|
161
|
+
/\bopencode-agent-skills-md\b/,
|
|
162
|
+
"README must mention the OpenCode plugin package",
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// The README must explicitly tell consumers which package to install
|
|
166
|
+
// for an OpenCode harness vs a custom/non-OpenCode harness. This is
|
|
167
|
+
// the R-3 routing scenario from the spec.
|
|
168
|
+
assert.match(
|
|
169
|
+
readme,
|
|
170
|
+
/OpenCode/i,
|
|
171
|
+
"README must still describe the OpenCode plugin install path",
|
|
172
|
+
);
|
|
173
|
+
assert.match(
|
|
174
|
+
readme,
|
|
175
|
+
/(custom harness|portable engine|standalone|non-OpenCode|without pulling the OpenCode SDK)/i,
|
|
176
|
+
"README must explain how to consume the standalone core package for custom harnesses",
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// The legacy "Programmatic subpath exports" section that pointed
|
|
180
|
+
// users at `opencode-agent-skills-md/core` must be gone — the spec
|
|
181
|
+
// (R4 REMOVED) explicitly retired that import path in favor of
|
|
182
|
+
// the standalone `opencode-agent-skills-md-core` package.
|
|
183
|
+
assert.doesNotMatch(
|
|
184
|
+
readme,
|
|
185
|
+
/opencode-agent-skills-md\/core/,
|
|
186
|
+
"README must not document the removed `opencode-agent-skills-md/core` subpath (spec R4 REMOVED)",
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("CHANGELOG records the workspace split under [Unreleased]", async () => {
|
|
191
|
+
const changelog = await readFile(path.join(REPO_ROOT, "CHANGELOG.md"), "utf8");
|
|
192
|
+
|
|
193
|
+
// The [Unreleased] section must mention the package split so anyone
|
|
194
|
+
// tracking the project knows the install surface changed.
|
|
195
|
+
const unreleasedMatch = changelog.match(/##\s*\[Unreleased\]([\s\S]*?)(?=\n##\s|\n$)/);
|
|
196
|
+
assert.ok(unreleasedMatch, "CHANGELOG.md must contain a populated [Unreleased] section");
|
|
197
|
+
|
|
198
|
+
const unreleased = unreleasedMatch![1]!;
|
|
199
|
+
assert.match(
|
|
200
|
+
unreleased,
|
|
201
|
+
/opencode-agent-skills-md-core/,
|
|
202
|
+
"CHANGELOG [Unreleased] must mention the new core package",
|
|
203
|
+
);
|
|
204
|
+
assert.match(
|
|
205
|
+
unreleased,
|
|
206
|
+
/(workspace|split|extract)/i,
|
|
207
|
+
"CHANGELOG [Unreleased] must describe the workspace split as a Changed/Added entry",
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("Justfile recipes align with the two-package workspace layout", async () => {
|
|
212
|
+
const justfile = await readFile(path.join(REPO_ROOT, "Justfile"), "utf8");
|
|
213
|
+
|
|
214
|
+
// `test` and `build` recipes must delegate to pnpm so both packages
|
|
215
|
+
// are covered; hard-coded `npm test` from a single package would
|
|
216
|
+
// silently skip the other. The exact pnpm flag shape varies (e.g.
|
|
217
|
+
// `pnpm -r run build`, `pnpm -r --workspace-concurrency=1 run build`,
|
|
218
|
+
// `pnpm test`) so we accept any `pnpm` invocation that ends in the
|
|
219
|
+
// right subcommand.
|
|
220
|
+
const testRecipe = justfile.match(/^test:\s*\n([\s\S]*?)(?=\n\n|\n[a-z]+\s*:|\n#|$)/m);
|
|
221
|
+
assert.ok(testRecipe, "Justfile must define a `test` recipe");
|
|
222
|
+
assert.match(
|
|
223
|
+
testRecipe![1]!,
|
|
224
|
+
/pnpm\b[\s\S]*\btest\b/,
|
|
225
|
+
`Justfile \`test\` recipe must delegate via pnpm so both packages run, got:\n${testRecipe![1]!}`,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const buildRecipe = justfile.match(/^build:\s*\n([\s\S]*?)(?=\n\n|\n[a-z]+\s*:|\n#|$)/m);
|
|
229
|
+
assert.ok(buildRecipe, "Justfile must define a `build` recipe");
|
|
230
|
+
assert.match(
|
|
231
|
+
buildRecipe![1]!,
|
|
232
|
+
/pnpm\b[\s\S]*\bbuild\b/,
|
|
233
|
+
`Justfile \`build\` recipe must delegate via pnpm so both packages build, got:\n${buildRecipe![1]!}`,
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("AGENTS.md repo structure section reflects the two-package layout", async () => {
|
|
238
|
+
const agents = await readFile(path.join(REPO_ROOT, "AGENTS.md"), "utf8");
|
|
239
|
+
|
|
240
|
+
// The repo-structure section must point at the package directories,
|
|
241
|
+
// not the old `src/opencode/...` paths.
|
|
242
|
+
assert.match(
|
|
243
|
+
agents,
|
|
244
|
+
/packages\/core\/src/,
|
|
245
|
+
"AGENTS.md must reference packages/core/src in its repo structure section",
|
|
246
|
+
);
|
|
247
|
+
assert.match(
|
|
248
|
+
agents,
|
|
249
|
+
/packages\/opencode-agent-skills-md\/src/,
|
|
250
|
+
"AGENTS.md must reference packages/opencode-agent-skills-md/src in its repo structure section",
|
|
251
|
+
);
|
|
252
|
+
assert.doesNotMatch(
|
|
253
|
+
agents,
|
|
254
|
+
/^src\/opencode\//m,
|
|
255
|
+
"AGENTS.md must not describe legacy src/opencode/ paths (those moved into the plugin package)",
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// The commands section must use `pnpm -r` for the umbrella commands
|
|
259
|
+
// so a single command covers both packages.
|
|
260
|
+
assert.match(
|
|
261
|
+
agents,
|
|
262
|
+
/pnpm\s+-r/,
|
|
263
|
+
"AGENTS.md must reference `pnpm -r` so the umbrella commands cover both packages",
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("both packages resolve from the repo root through the pnpm workspace link", async () => {
|
|
268
|
+
// pnpm install wires the two packages into `node_modules/` as
|
|
269
|
+
// symlinks so they resolve by name from the repo root. This is the
|
|
270
|
+
// end-to-end "the workspace link is alive" check — a stale lockfile
|
|
271
|
+
// or a typo in `pnpm-workspace.yaml` would surface here.
|
|
272
|
+
const coreResolved = require.resolve("opencode-agent-skills-md-core");
|
|
273
|
+
assert.match(
|
|
274
|
+
coreResolved,
|
|
275
|
+
/[\\/]packages[\\/]core[\\/]src[\\/]index\.ts$/,
|
|
276
|
+
`expected opencode-agent-skills-md-core to resolve to packages/core/src/index.ts, got: ${coreResolved}`,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const pluginResolved = require.resolve("opencode-agent-skills-md");
|
|
280
|
+
assert.match(
|
|
281
|
+
pluginResolved,
|
|
282
|
+
/[\\/]packages[\\/]opencode-agent-skills-md[\\/]src[\\/]index\.ts$/,
|
|
283
|
+
`expected opencode-agent-skills-md to resolve to packages/opencode-agent-skills-md/src/index.ts, got: ${pluginResolved}`,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
assert.ok(coreResolved.startsWith(REPO_ROOT), `core resolution must live under the repo root: ${coreResolved}`);
|
|
287
|
+
assert.ok(
|
|
288
|
+
pluginResolved.startsWith(REPO_ROOT),
|
|
289
|
+
`plugin resolution must live under the repo root: ${pluginResolved}`,
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("both packages' source trees exist at the documented paths", async () => {
|
|
294
|
+
// Cross-check that the per-package sources are in place — without
|
|
295
|
+
// these the workspace link above would resolve to nothing.
|
|
296
|
+
const coreSrc = path.join(REPO_ROOT, "packages", "core", "src");
|
|
297
|
+
const pluginSrc = path.join(REPO_ROOT, "packages", "opencode-agent-skills-md", "src");
|
|
298
|
+
assert.ok(existsSync(coreSrc), `${coreSrc} must exist`);
|
|
299
|
+
assert.ok(existsSync(pluginSrc), `${pluginSrc} must exist`);
|
|
300
|
+
|
|
301
|
+
const coreSrcStat = await stat(coreSrc);
|
|
302
|
+
const pluginSrcStat = await stat(pluginSrc);
|
|
303
|
+
assert.ok(coreSrcStat.isDirectory(), `${coreSrc} must be a directory`);
|
|
304
|
+
assert.ok(pluginSrcStat.isDirectory(), `${pluginSrc} must be a directory`);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("`pnpm run typecheck` from the repo root exits 0 (both packages typecheck clean)", () => {
|
|
308
|
+
// Triangulation: beyond the static `pnpm -r` script check above, the
|
|
309
|
+
// strongest behavior contract is that the documented root command
|
|
310
|
+
// actually works. A regression in the delegation (e.g. a typo, or a
|
|
311
|
+
// package silently missing a `typecheck` script) would surface here.
|
|
312
|
+
const result = spawnSync("pnpm", ["run", "typecheck"], {
|
|
313
|
+
cwd: REPO_ROOT,
|
|
314
|
+
encoding: "utf8",
|
|
315
|
+
timeout: 180_000,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
assert.equal(
|
|
319
|
+
result.status,
|
|
320
|
+
0,
|
|
321
|
+
`pnpm run typecheck from the repo root must exit 0. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("`pnpm test` from the repo root executes both packages' test suites end-to-end", () => {
|
|
326
|
+
// Triangulation: the umbrella `pnpm test` command must actually visit
|
|
327
|
+
// both workspace packages — a typo in the delegation script or a
|
|
328
|
+
// missing per-package `test` script would leave one suite silent.
|
|
329
|
+
//
|
|
330
|
+
// We deliberately do NOT assert exit code 0 here. The plugin package
|
|
331
|
+
// carries two pre-existing env-dependent test failures (21-vs-26
|
|
332
|
+
// user skills and `ast-grep` not installed; documented in the PR 2b
|
|
333
|
+
// apply progress). Those existed before PR 3 and are not part of
|
|
334
|
+
// this work — gating the root test on them would convert a known
|
|
335
|
+
// local-only issue into a workspace-level red. We assert the
|
|
336
|
+
// delegation reaches both packages; the per-package suites own the
|
|
337
|
+
// exit-code contract.
|
|
338
|
+
//
|
|
339
|
+
// `--no-bail` lets pnpm continue past the first failing package so
|
|
340
|
+
// the workspace contract test (this file) still runs even when the
|
|
341
|
+
// plugin's pre-existing failures fire. The overall `pnpm test` exit
|
|
342
|
+
// code is the conjunction of per-package results and the workspace
|
|
343
|
+
// test result, which is the correct gate.
|
|
344
|
+
const result = spawnSync("pnpm", ["-r", "--no-bail", "test"], {
|
|
345
|
+
cwd: REPO_ROOT,
|
|
346
|
+
encoding: "utf8",
|
|
347
|
+
timeout: 240_000,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const combined = `${result.stdout}\n${result.stderr}`;
|
|
351
|
+
|
|
352
|
+
// pnpm prefixes each package's output with its directory path
|
|
353
|
+
// (`packages/core test$ ...`, `packages/opencode-agent-skills-md test$ ...`).
|
|
354
|
+
// Both prefixes appearing in the output is the strongest signal that
|
|
355
|
+
// the delegation reached both packages.
|
|
356
|
+
assert.match(
|
|
357
|
+
combined,
|
|
358
|
+
/packages\/core\b/,
|
|
359
|
+
`pnpm test must execute the core package's test suite. output:\n${combined}`,
|
|
360
|
+
);
|
|
361
|
+
assert.match(
|
|
362
|
+
combined,
|
|
363
|
+
/packages\/opencode-agent-skills-md\b/,
|
|
364
|
+
`pnpm test must execute the plugin package's test suite. output:\n${combined}`,
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"types": ["node"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|