Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/host-daemon/src/injected-skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,51 @@ describe("injected skill staging", () => {
});
});

it("stages built-in skill sources into the shared catalog", async () => {
const dataDir = await makeTempDir();
const bundledRoot = await makeTempDir();
const skillRootPath = await writeSkill({
rootPath: bundledRoot,
name: "building-bb-apps",
});

const staged = await stageInjectedSkillSources({
dataDir,
injectedSkillSources: [
{
sourceType: "builtin",
applicationId: null,
name: "building-bb-apps",
description: "Use building-bb-apps when host staging tests run.",
sourceRootPath: skillRootPath,
skillFilePath: path.join(skillRootPath, "SKILL.md"),
},
],
});

const claudeRoot = staged.skillRoots.find(isClaudeCodeSkillRoot);
if (!claudeRoot) {
throw new Error("Expected Claude Code skill root");
}
expect(claudeRoot.skillNames).toEqual(["building-bb-apps"]);
await expect(
readFile(
path.join(claudeRoot.localPluginPath, "catalog.json"),
"utf8",
).then((content) => JSON.parse(content)),
).resolves.toMatchObject({
catalogHash: staged.catalogHash,
skills: [
{
applicationId: null,
name: "building-bb-apps",
sourceRootPath: skillRootPath,
sourceType: "builtin",
},
],
});
});

it("changes the catalog hash when skill content changes", async () => {
const dataDir = await makeTempDir();
const sourceRootPath = path.join(dataDir, "source-skills");
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"private": true,
"scripts": {
"build": "node ../../scripts/build-node-entry.mjs src/index.ts dist/index.js --clean-dist --templates --external ./start-server.js --copy-dir ../../packages/db/drizzle dist/drizzle && node --import tsx scripts/copy-app-scaffold-template.ts && node ../../scripts/build-node-entry.mjs src/start-server.ts dist/start-server.js",
"build": "node ../../scripts/build-node-entry.mjs src/index.ts dist/index.js --clean-dist --templates --external ./start-server.js --copy-dir ../../packages/db/drizzle dist/drizzle && node --import tsx scripts/copy-app-scaffold-template.ts && node --import tsx scripts/copy-builtin-skills.ts && node ../../scripts/build-node-entry.mjs src/start-server.ts dist/start-server.js",
"bench": "vitest bench --config vitest.config.ts",
"build:app-scaffold-template": "node ./scripts/build-app-scaffold-template.mjs",
"start": "node dist/index.js",
Expand Down
22 changes: 22 additions & 0 deletions apps/server/scripts/copy-builtin-skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { rm } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
BUILTIN_SKILLS_DIRECTORY_NAME,
copyBuiltinSkills,
resolveBuiltinSkillsRootPath,
} from "../src/services/skills/builtin-skills-copy.js";

// Build step: copies the built-in injected skills into dist so the bundled
// server resolves them beside its dist entry points, mirroring the app
// scaffold template copy.
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const targetPath = path.resolve(
scriptDir,
"../dist",
BUILTIN_SKILLS_DIRECTORY_NAME,
);

const skillsRootPath = resolveBuiltinSkillsRootPath();
await rm(targetPath, { force: true, recursive: true });
await copyBuiltinSkills({ skillsRootPath, targetPath });
72 changes: 72 additions & 0 deletions apps/server/src/services/skills/builtin-skills-copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { cp } from "node:fs/promises";
import { constants as fsConstants, existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

/**
* Locates and copies the built-in skills that ship beside this module.
*
* This module is shared between the server runtime (built-in injected skill
* discovery in ./injected-skills.ts) and the build step that copies the
* skills into dist (scripts/copy-builtin-skills.ts, loaded with tsx before
* workspace packages are built). Keep it free of workspace and third-party
* imports.
*/

interface CopyBuiltinSkillsArgs {
skillsRootPath: string;
targetPath: string;
}

interface ResolveBuiltinSkillsRootPathArgs {
moduleDir: string;
}

export const BUILTIN_SKILLS_DIRECTORY_NAME = "builtin-skills";
// Structural essential the server itself depends on. Skill content may change
// without breaking root detection.
const BUILTIN_SKILLS_SENTINEL_PATH = path.join(
"building-bb-apps",
"SKILL.md",
);
const BUILTIN_SKILLS_COPY_MODE = fsConstants.COPYFILE_FICLONE;
const builtinSkillsModuleDir = path.dirname(fileURLToPath(import.meta.url));

function hasBuiltinSkillsRoot(skillsRootPath: string): boolean {
return existsSync(path.join(skillsRootPath, BUILTIN_SKILLS_SENTINEL_PATH));
}

/**
* The built-in skills directory sits beside this module in both layouts:
* src/services/skills/ in the source tree, and dist/ in the bundled server
* (the build copies the skills to dist/builtin-skills and esbuild bundles
* this module into the dist entry points).
*/
export function resolveBuiltinSkillsRootPathForModuleDir(
args: ResolveBuiltinSkillsRootPathArgs,
): string {
const skillsRootPath = path.resolve(
args.moduleDir,
BUILTIN_SKILLS_DIRECTORY_NAME,
);
if (!hasBuiltinSkillsRoot(skillsRootPath)) {
throw new Error(`Missing built-in skills at ${skillsRootPath}`);
}
return skillsRootPath;
}

export function resolveBuiltinSkillsRootPath(): string {
return resolveBuiltinSkillsRootPathForModuleDir({
moduleDir: builtinSkillsModuleDir,
});
}

export async function copyBuiltinSkills(
args: CopyBuiltinSkillsArgs,
): Promise<void> {
await cp(args.skillsRootPath, args.targetPath, {
force: false,
mode: BUILTIN_SKILLS_COPY_MODE,
recursive: true,
});
}
Loading
Loading