Skip to content

Commit d3f6f7b

Browse files
committed
fix(cli): resolve bundled skills from the package root, not the tshy dist stub
The skills loader resolved the package.json nearest the bundled code, which is tshy's `dist/esm` dialect stub ({"type":"module"}), so it looked for skills in `dist/esm/skills` (which does not exist) and reported "No agent skills found" from every published build. It only worked when run from source via tsx. Walk up to the first package.json with a name (the real package root) instead, which resolves correctly both bundled and from source, and also fixes the version stamp falling back to 0.0.0.
1 parent d07913f commit d3f6f7b

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { afterAll, describe, expect, it } from "vitest";
2+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { dirname, join } from "node:path";
5+
import { resolveBundledPackageJSON } from "./skills.js";
6+
7+
/**
8+
* Reproduces the published layout: tshy emits a dialect stub `package.json`
9+
* ({"type":"module"}) in `dist/esm`, which shadows the real package root when resolving
10+
* from the bundled code. The skills dir ships at the package root. Resolution must skip
11+
* the stub and land on the root, otherwise `trigger skills` silently finds no skills.
12+
*/
13+
async function makeBundledPackage(): Promise<{ root: string; distEsm: string }> {
14+
const root = await mkdtemp(join(tmpdir(), "bundled-cli-"));
15+
16+
await writeFile(
17+
join(root, "package.json"),
18+
JSON.stringify({ name: "trigger.dev", version: "9.9.9-test.1" })
19+
);
20+
21+
await mkdir(join(root, "skills", "authoring-tasks"), { recursive: true });
22+
await writeFile(join(root, "skills", "authoring-tasks", "SKILL.md"), "# Authoring");
23+
24+
const distEsm = join(root, "dist", "esm");
25+
await mkdir(distEsm, { recursive: true });
26+
// The tshy dialect stub that caused the bug.
27+
await writeFile(join(distEsm, "package.json"), JSON.stringify({ type: "module" }));
28+
29+
return { root, distEsm };
30+
}
31+
32+
describe("resolveBundledPackageJSON", () => {
33+
const roots: string[] = [];
34+
35+
afterAll(async () => {
36+
await Promise.all(roots.map((d) => rm(d, { recursive: true, force: true })));
37+
});
38+
39+
it("skips the tshy dist/esm dialect stub and resolves the package root", async () => {
40+
const { root, distEsm } = await makeBundledPackage();
41+
roots.push(root);
42+
43+
const resolved = await resolveBundledPackageJSON(distEsm);
44+
45+
// Must be the root package.json (which has `skills/` beside it), not the dist/esm stub.
46+
expect(resolved).toBe(join(root, "package.json"));
47+
expect(dirname(resolved!)).toBe(root);
48+
});
49+
50+
it("resolves directly when started from a dir under the named package root (source/tsx path)", async () => {
51+
const { root } = await makeBundledPackage();
52+
roots.push(root);
53+
54+
const srcDir = join(root, "src");
55+
await mkdir(srcDir, { recursive: true });
56+
57+
const resolved = await resolveBundledPackageJSON(srcDir);
58+
59+
expect(resolved).toBe(join(root, "package.json"));
60+
});
61+
});

packages/cli-v3/src/commands/skills.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,45 @@ export async function installSkillsCommand(options: unknown) {
100100
* `sourceDir` (the CLI's location) rather than the user's cwd. The CLI is the only source
101101
* of skills (there is no remote fallback), so this only returns null in the unexpected
102102
* case that the CLI ships without any skills.
103+
*
104+
* tshy emits a dialect stub `package.json` ({"type":"module"}) in `dist/esm`, so the
105+
* package.json nearest the bundled code is NOT the package root and has no `skills/`
106+
* beside it. We walk up to the first package.json that has a `name` (the real root);
107+
* that resolves correctly both when bundled (`<root>/dist/esm`) and from source
108+
* (`<root>/src`, run via tsx in dev/tests).
103109
*/
110+
export async function resolveBundledPackageJSON(startDir: string = sourceDir): Promise<
111+
string | null
112+
> {
113+
let searchDir = startDir;
114+
115+
for (let i = 0; i < 10; i++) {
116+
const candidate = await resolvePackageJSON(searchDir);
117+
const pkg = await readPackageJSON(candidate);
118+
119+
if (pkg.name) {
120+
return candidate;
121+
}
122+
123+
// Climb above this (stub) package.json and keep looking for the real root.
124+
const above = dirname(dirname(candidate));
125+
if (above === searchDir) {
126+
return null;
127+
}
128+
searchDir = above;
129+
}
130+
131+
return null;
132+
}
133+
104134
async function loadSkillsManifest(): Promise<RulesManifest | null> {
105135
try {
106-
const packageJsonPath = await resolvePackageJSON(sourceDir);
136+
const packageJsonPath = await resolveBundledPackageJSON();
137+
138+
if (!packageJsonPath) {
139+
return null;
140+
}
141+
107142
const pkg = await readPackageJSON(packageJsonPath);
108143
const skillsDir = join(dirname(packageJsonPath), "skills");
109144
const version = typeof pkg.version === "string" ? pkg.version : "0.0.0";

0 commit comments

Comments
 (0)