@ebay-dt/bonfire-context 0.1.0-alpha.4

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/discover.d.ts +9 -0
  4. package/dist/discover.d.ts.map +1 -0
  5. package/dist/discover.js +103 -0
  6. package/dist/discover.js.map +1 -0
  7. package/dist/errors.d.ts +5 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +9 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +9 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/install.d.ts +33 -0
  16. package/dist/install.d.ts.map +1 -0
  17. package/dist/install.js +488 -0
  18. package/dist/install.js.map +1 -0
  19. package/dist/internal/filesystem.d.ts +5 -0
  20. package/dist/internal/filesystem.d.ts.map +1 -0
  21. package/dist/internal/filesystem.js +82 -0
  22. package/dist/internal/filesystem.js.map +1 -0
  23. package/dist/mcp.d.ts +26 -0
  24. package/dist/mcp.d.ts.map +1 -0
  25. package/dist/mcp.js +178 -0
  26. package/dist/mcp.js.map +1 -0
  27. package/dist/migration.d.ts +23 -0
  28. package/dist/migration.d.ts.map +1 -0
  29. package/dist/migration.js +206 -0
  30. package/dist/migration.js.map +1 -0
  31. package/dist/paths.d.ts +58 -0
  32. package/dist/paths.d.ts.map +1 -0
  33. package/dist/paths.js +191 -0
  34. package/dist/paths.js.map +1 -0
  35. package/dist/prototype-project.d.ts +7 -0
  36. package/dist/prototype-project.d.ts.map +1 -0
  37. package/dist/prototype-project.js +38 -0
  38. package/dist/prototype-project.js.map +1 -0
  39. package/dist/registry.d.ts +36 -0
  40. package/dist/registry.d.ts.map +1 -0
  41. package/dist/registry.js +189 -0
  42. package/dist/registry.js.map +1 -0
  43. package/dist/render.d.ts +15 -0
  44. package/dist/render.d.ts.map +1 -0
  45. package/dist/render.js +64 -0
  46. package/dist/render.js.map +1 -0
  47. package/dist/root-project.d.ts +11 -0
  48. package/dist/root-project.d.ts.map +1 -0
  49. package/dist/root-project.js +152 -0
  50. package/dist/root-project.js.map +1 -0
  51. package/dist/skills.d.ts +13 -0
  52. package/dist/skills.d.ts.map +1 -0
  53. package/dist/skills.js +170 -0
  54. package/dist/skills.js.map +1 -0
  55. package/dist/sync.d.ts +23 -0
  56. package/dist/sync.d.ts.map +1 -0
  57. package/dist/sync.js +232 -0
  58. package/dist/sync.js.map +1 -0
  59. package/dist/template.d.ts +18 -0
  60. package/dist/template.d.ts.map +1 -0
  61. package/dist/template.js +114 -0
  62. package/dist/template.js.map +1 -0
  63. package/dist/types.d.ts +5 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +2 -0
  66. package/dist/types.js.map +1 -0
  67. package/package.json +44 -0
package/dist/render.js ADDED
@@ -0,0 +1,64 @@
1
+ import { lstat, readFile } from "node:fs/promises";
2
+ import { assertSafeContextPaths, assertSafePrototypeContextPaths, } from "./paths.js";
3
+ const ROOT_SOURCE_LABEL = "context/ROOT.md";
4
+ const PROTOTYPE_SOURCE_LABEL = "context/PROTOTYPE.md";
5
+ export const GENERATED_CLAUDE_CONTENT = `<!--
6
+ This file is generated. Do not edit it directly.
7
+ -->
8
+
9
+ @AGENTS.md
10
+ `;
11
+ export class ContextRenderError extends Error {
12
+ source;
13
+ constructor(source, message) {
14
+ super(`Unable to render ${source}: ${message}`);
15
+ this.name = "ContextRenderError";
16
+ this.source = source;
17
+ }
18
+ }
19
+ function generatedNotice(source) {
20
+ return `<!--
21
+ This file is generated from ${source}.
22
+ Do not edit it directly. Run \`bonfire context sync\` after changing the source.
23
+ -->
24
+
25
+ `;
26
+ }
27
+ function ensureFinalNewline(source) {
28
+ return source.endsWith("\n") ? source : `${source}\n`;
29
+ }
30
+ export function renderMarkdownSource(source, sourceLabel) {
31
+ return `${generatedNotice(sourceLabel)}${ensureFinalNewline(source)}`;
32
+ }
33
+ export function renderClaudeBridge() {
34
+ return GENERATED_CLAUDE_CONTENT;
35
+ }
36
+ async function readRequiredMarkdown(path, sourceLabel) {
37
+ try {
38
+ const stats = await lstat(path);
39
+ if (!stats.isFile() || stats.isSymbolicLink()) {
40
+ throw new Error("not a regular file");
41
+ }
42
+ return await readFile(path, "utf8");
43
+ }
44
+ catch {
45
+ throw new ContextRenderError(sourceLabel, "the required source is missing or unreadable");
46
+ }
47
+ }
48
+ export async function renderRootMarkdown(paths) {
49
+ assertSafeContextPaths(paths);
50
+ const source = await readRequiredMarkdown(paths.sources.rootInstructions, ROOT_SOURCE_LABEL);
51
+ return {
52
+ agents: renderMarkdownSource(source, ROOT_SOURCE_LABEL),
53
+ claude: renderClaudeBridge(),
54
+ };
55
+ }
56
+ export async function renderPrototypeMarkdown(paths) {
57
+ assertSafePrototypeContextPaths(paths);
58
+ const source = await readRequiredMarkdown(paths.sources.prototypeInstructions, PROTOTYPE_SOURCE_LABEL);
59
+ return {
60
+ agents: renderMarkdownSource(source, PROTOTYPE_SOURCE_LABEL),
61
+ claude: renderClaudeBridge(),
62
+ };
63
+ }
64
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EACL,sBAAsB,EACtB,+BAA+B,GAGhC,MAAM,YAAY,CAAC;AAEpB,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;AAC5C,MAAM,sBAAsB,GAAG,sBAAsB,CAAC;AAEtD,MAAM,CAAC,MAAM,wBAAwB,GAAG;;;;;CAKvC,CAAC;AAOF,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,MAAM,CAAS;IAExB,YAAY,MAAc,EAAE,OAAe;QACzC,KAAK,CAAC,oBAAoB,MAAM,KAAK,OAAO,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO;gCACuB,MAAM;;;;CAIrC,CAAC;AACF,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAc;IACxC,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAc,EAAE,WAAmB;IACtE,OAAO,GAAG,eAAe,CAAC,WAAW,CAAC,GAAG,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,wBAAwB,CAAC;AAClC,CAAC;AAED,KAAK,UAAU,oBAAoB,CAAC,IAAY,EAAE,WAAmB;IACnE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,kBAAkB,CAC1B,WAAW,EACX,8CAA8C,CAC/C,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAA2B;IAE3B,sBAAsB,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,MAAM,oBAAoB,CACvC,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAC9B,iBAAiB,CAClB,CAAC;IAEF,OAAO;QACL,MAAM,EAAE,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,CAAC;QACvD,MAAM,EAAE,kBAAkB,EAAE;KAC7B,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,KAAoC;IAEpC,+BAA+B,CAAC,KAAK,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,MAAM,oBAAoB,CACvC,KAAK,CAAC,OAAO,CAAC,qBAAqB,EACnC,sBAAsB,CACvB,CAAC;IAEF,OAAO;QACL,MAAM,EAAE,oBAAoB,CAAC,MAAM,EAAE,sBAAsB,CAAC;QAC5D,MAAM,EAAE,kBAAkB,EAAE;KAC7B,CAAC;AACJ,CAAC","sourcesContent":["import { lstat, readFile } from \"node:fs/promises\";\nimport {\n assertSafeContextPaths,\n assertSafePrototypeContextPaths,\n type ResolvedContextPaths,\n type ResolvedPrototypeContextPaths,\n} from \"./paths.js\";\n\nconst ROOT_SOURCE_LABEL = \"context/ROOT.md\";\nconst PROTOTYPE_SOURCE_LABEL = \"context/PROTOTYPE.md\";\n\nexport const GENERATED_CLAUDE_CONTENT = `<!--\n This file is generated. Do not edit it directly.\n-->\n\n@AGENTS.md\n`;\n\nexport type MarkdownProjection = Readonly<{\n agents: string;\n claude: string;\n}>;\n\nexport class ContextRenderError extends Error {\n readonly source: string;\n\n constructor(source: string, message: string) {\n super(`Unable to render ${source}: ${message}`);\n this.name = \"ContextRenderError\";\n this.source = source;\n }\n}\n\nfunction generatedNotice(source: string) {\n return `<!--\n This file is generated from ${source}.\n Do not edit it directly. Run \\`bonfire context sync\\` after changing the source.\n-->\n\n`;\n}\n\nfunction ensureFinalNewline(source: string) {\n return source.endsWith(\"\\n\") ? source : `${source}\\n`;\n}\n\nexport function renderMarkdownSource(source: string, sourceLabel: string) {\n return `${generatedNotice(sourceLabel)}${ensureFinalNewline(source)}`;\n}\n\nexport function renderClaudeBridge() {\n return GENERATED_CLAUDE_CONTENT;\n}\n\nasync function readRequiredMarkdown(path: string, sourceLabel: string) {\n try {\n const stats = await lstat(path);\n if (!stats.isFile() || stats.isSymbolicLink()) {\n throw new Error(\"not a regular file\");\n }\n return await readFile(path, \"utf8\");\n } catch {\n throw new ContextRenderError(\n sourceLabel,\n \"the required source is missing or unreadable\",\n );\n }\n}\n\nexport async function renderRootMarkdown(\n paths: ResolvedContextPaths,\n): Promise<MarkdownProjection> {\n assertSafeContextPaths(paths);\n const source = await readRequiredMarkdown(\n paths.sources.rootInstructions,\n ROOT_SOURCE_LABEL,\n );\n\n return {\n agents: renderMarkdownSource(source, ROOT_SOURCE_LABEL),\n claude: renderClaudeBridge(),\n };\n}\n\nexport async function renderPrototypeMarkdown(\n paths: ResolvedPrototypeContextPaths,\n): Promise<MarkdownProjection> {\n assertSafePrototypeContextPaths(paths);\n const source = await readRequiredMarkdown(\n paths.sources.prototypeInstructions,\n PROTOTYPE_SOURCE_LABEL,\n );\n\n return {\n agents: renderMarkdownSource(source, PROTOTYPE_SOURCE_LABEL),\n claude: renderClaudeBridge(),\n };\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import { type RenderedSkillFile } from "./skills.js";
2
+ import type { ContextProjectionResult } from "./types.js";
3
+ export { ContextProjectionError } from "./errors.js";
4
+ export type ProjectRootContextOptions = Readonly<{
5
+ repositoryRoot: string;
6
+ prototypeRoot?: string;
7
+ }>;
8
+ export declare function listSkillFiles(root: string, relativeDirectory?: string): Promise<string[]>;
9
+ export declare function reconcileSkills(outputRoot: string, renderedFiles: readonly RenderedSkillFile[], repositoryRoot: string, written: string[], removed: string[], writePaths?: ReadonlySet<string>): Promise<void>;
10
+ export declare function projectRootContext(options: ProjectRootContextOptions): Promise<ContextProjectionResult>;
11
+ //# sourceMappingURL=root-project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-project.d.ts","sourceRoot":"","sources":["../src/root-project.ts"],"names":[],"mappings":"AAKA,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAQvE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAE1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC,CAAC;AAEH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,iBAAiB,SAAK,GACrB,OAAO,CAAC,MAAM,EAAE,CAAC,CA+CnB;AAiDD,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,SAAS,iBAAiB,EAAE,EAC3C,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,iBA4BjC;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,uBAAuB,CAAC,CA+DlC","sourcesContent":["import { lstat, readdir, rmdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport { renderOptionalMcp, renderOptionalSettings } from \"./template.js\";\nimport { renderRootMarkdown } from \"./render.js\";\nimport { assertSafeContextPaths, resolveContextPaths } from \"./paths.js\";\nimport { renderSkillFiles, type RenderedSkillFile } from \"./skills.js\";\nimport {\n atomicWrite,\n assertWritableTarget,\n isNotFound,\n removeManagedFile,\n} from \"./internal/filesystem.js\";\nimport { ContextProjectionError } from \"./errors.js\";\nimport type { ContextProjectionResult } from \"./types.js\";\n\nexport { ContextProjectionError } from \"./errors.js\";\n\nexport type ProjectRootContextOptions = Readonly<{\n repositoryRoot: string;\n prototypeRoot?: string;\n}>;\n\nexport async function listSkillFiles(\n root: string,\n relativeDirectory = \"\",\n): Promise<string[]> {\n let entries;\n try {\n entries = await readdir(root, { withFileTypes: true });\n } catch (error) {\n if (isNotFound(error)) return [];\n throw new ContextProjectionError(\n relativeDirectory || \"rootOutputs.skills\",\n \"directory could not be enumerated\",\n );\n }\n\n const files: string[] = [];\n for (const entry of entries.sort((left, right) =>\n left.name < right.name ? -1 : left.name > right.name ? 1 : 0,\n )) {\n const relativePath = relativeDirectory\n ? `${relativeDirectory}/${entry.name}`\n : entry.name;\n const path = join(root, entry.name);\n let stats;\n try {\n stats = await lstat(path);\n } catch {\n throw new ContextProjectionError(\n `rootOutputs.skills/${relativePath}`,\n \"could not be inspected safely\",\n );\n }\n if (stats.isSymbolicLink()) {\n throw new ContextProjectionError(\n `rootOutputs.skills/${relativePath}`,\n \"must not be a symbolic link\",\n );\n }\n if (stats.isDirectory()) {\n files.push(...(await listSkillFiles(path, relativePath)));\n } else if (stats.isFile()) {\n files.push(relativePath);\n } else {\n throw new ContextProjectionError(\n `rootOutputs.skills/${relativePath}`,\n \"must be a regular file or directory\",\n );\n }\n }\n return files;\n}\n\nasync function removeEmptySkillDirectories(root: string): Promise<boolean> {\n let entries;\n try {\n entries = await readdir(root, { withFileTypes: true });\n } catch (error) {\n if (isNotFound(error)) return false;\n throw new ContextProjectionError(\n \"rootOutputs.skills\",\n \"directory could not be enumerated\",\n );\n }\n\n for (const entry of entries) {\n const path = join(root, entry.name);\n let stats;\n try {\n stats = await lstat(path);\n } catch {\n throw new ContextProjectionError(\n `rootOutputs.skills/${entry.name}`,\n \"could not be inspected safely\",\n );\n }\n if (stats.isSymbolicLink()) {\n throw new ContextProjectionError(\n `rootOutputs.skills/${entry.name}`,\n \"must not be a symbolic link\",\n );\n }\n if (stats.isDirectory()) {\n await removeEmptySkillDirectories(path);\n }\n }\n\n const remaining = await readdir(root);\n if (remaining.length !== 0) return false;\n try {\n await rmdir(root);\n } catch {\n throw new ContextProjectionError(\n \"rootOutputs.skills\",\n \"empty directory could not be removed\",\n );\n }\n return true;\n}\n\nexport async function reconcileSkills(\n outputRoot: string,\n renderedFiles: readonly RenderedSkillFile[],\n repositoryRoot: string,\n written: string[],\n removed: string[],\n writePaths?: ReadonlySet<string>,\n) {\n const existingFiles = await listSkillFiles(outputRoot);\n const expected = new Set(renderedFiles.map((file) => file.relativePath));\n\n for (const file of renderedFiles) {\n const path = join(outputRoot, file.relativePath);\n if (writePaths !== undefined && !writePaths.has(path)) continue;\n await atomicWrite(\n path,\n file.content,\n `rootOutputs.skills/${file.relativePath}`,\n file.mode,\n );\n written.push(relative(repositoryRoot, path));\n }\n\n for (const file of existingFiles) {\n if (expected.has(file)) continue;\n const path = join(outputRoot, file);\n if (await removeManagedFile(path, `rootOutputs.skills/${file}`)) {\n removed.push(relative(repositoryRoot, path));\n }\n }\n\n if (await removeEmptySkillDirectories(outputRoot)) {\n removed.push(relative(repositoryRoot, outputRoot));\n }\n}\n\nexport async function projectRootContext(\n options: ProjectRootContextOptions,\n): Promise<ContextProjectionResult> {\n const paths = resolveContextPaths(options);\n assertSafeContextPaths(paths);\n await Promise.all([\n assertWritableTarget(paths.rootOutputs.agents, \"AGENTS.md\"),\n assertWritableTarget(paths.rootOutputs.claude, \"CLAUDE.md\"),\n assertWritableTarget(paths.rootOutputs.settings, \".claude/settings.json\"),\n assertWritableTarget(paths.rootOutputs.mcp, \".mcp.json\"),\n listSkillFiles(paths.rootOutputs.skills),\n ]);\n\n // Render every source before changing a generated target. Invalid source\n // content must not leave a partially updated repository projection.\n const [markdown, settings, mcp, skills] = await Promise.all([\n renderRootMarkdown(paths),\n renderOptionalSettings(paths),\n renderOptionalMcp(paths),\n renderSkillFiles(paths),\n ]);\n\n const written: string[] = [];\n const removed: string[] = [];\n await atomicWrite(paths.rootOutputs.agents, markdown.agents, \"AGENTS.md\");\n written.push(relative(paths.repositoryRoot, paths.rootOutputs.agents));\n await atomicWrite(paths.rootOutputs.claude, markdown.claude, \"CLAUDE.md\");\n written.push(relative(paths.repositoryRoot, paths.rootOutputs.claude));\n\n if (settings === undefined) {\n if (\n await removeManagedFile(\n paths.rootOutputs.settings,\n \".claude/settings.json\",\n )\n ) {\n removed.push(relative(paths.repositoryRoot, paths.rootOutputs.settings));\n }\n } else {\n await atomicWrite(\n paths.rootOutputs.settings,\n settings,\n \".claude/settings.json\",\n );\n written.push(relative(paths.repositoryRoot, paths.rootOutputs.settings));\n }\n\n if (mcp === undefined) {\n if (await removeManagedFile(paths.rootOutputs.mcp, \".mcp.json\")) {\n removed.push(relative(paths.repositoryRoot, paths.rootOutputs.mcp));\n }\n } else {\n await atomicWrite(paths.rootOutputs.mcp, mcp, \".mcp.json\");\n written.push(relative(paths.repositoryRoot, paths.rootOutputs.mcp));\n }\n\n await reconcileSkills(\n paths.rootOutputs.skills,\n skills,\n paths.repositoryRoot,\n written,\n removed,\n );\n\n return { written, removed };\n}\n"]}
@@ -0,0 +1,152 @@
1
+ import { lstat, readdir, rmdir } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ import { renderOptionalMcp, renderOptionalSettings } from "./template.js";
4
+ import { renderRootMarkdown } from "./render.js";
5
+ import { assertSafeContextPaths, resolveContextPaths } from "./paths.js";
6
+ import { renderSkillFiles } from "./skills.js";
7
+ import { atomicWrite, assertWritableTarget, isNotFound, removeManagedFile, } from "./internal/filesystem.js";
8
+ import { ContextProjectionError } from "./errors.js";
9
+ export { ContextProjectionError } from "./errors.js";
10
+ export async function listSkillFiles(root, relativeDirectory = "") {
11
+ let entries;
12
+ try {
13
+ entries = await readdir(root, { withFileTypes: true });
14
+ }
15
+ catch (error) {
16
+ if (isNotFound(error))
17
+ return [];
18
+ throw new ContextProjectionError(relativeDirectory || "rootOutputs.skills", "directory could not be enumerated");
19
+ }
20
+ const files = [];
21
+ for (const entry of entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
22
+ const relativePath = relativeDirectory
23
+ ? `${relativeDirectory}/${entry.name}`
24
+ : entry.name;
25
+ const path = join(root, entry.name);
26
+ let stats;
27
+ try {
28
+ stats = await lstat(path);
29
+ }
30
+ catch {
31
+ throw new ContextProjectionError(`rootOutputs.skills/${relativePath}`, "could not be inspected safely");
32
+ }
33
+ if (stats.isSymbolicLink()) {
34
+ throw new ContextProjectionError(`rootOutputs.skills/${relativePath}`, "must not be a symbolic link");
35
+ }
36
+ if (stats.isDirectory()) {
37
+ files.push(...(await listSkillFiles(path, relativePath)));
38
+ }
39
+ else if (stats.isFile()) {
40
+ files.push(relativePath);
41
+ }
42
+ else {
43
+ throw new ContextProjectionError(`rootOutputs.skills/${relativePath}`, "must be a regular file or directory");
44
+ }
45
+ }
46
+ return files;
47
+ }
48
+ async function removeEmptySkillDirectories(root) {
49
+ let entries;
50
+ try {
51
+ entries = await readdir(root, { withFileTypes: true });
52
+ }
53
+ catch (error) {
54
+ if (isNotFound(error))
55
+ return false;
56
+ throw new ContextProjectionError("rootOutputs.skills", "directory could not be enumerated");
57
+ }
58
+ for (const entry of entries) {
59
+ const path = join(root, entry.name);
60
+ let stats;
61
+ try {
62
+ stats = await lstat(path);
63
+ }
64
+ catch {
65
+ throw new ContextProjectionError(`rootOutputs.skills/${entry.name}`, "could not be inspected safely");
66
+ }
67
+ if (stats.isSymbolicLink()) {
68
+ throw new ContextProjectionError(`rootOutputs.skills/${entry.name}`, "must not be a symbolic link");
69
+ }
70
+ if (stats.isDirectory()) {
71
+ await removeEmptySkillDirectories(path);
72
+ }
73
+ }
74
+ const remaining = await readdir(root);
75
+ if (remaining.length !== 0)
76
+ return false;
77
+ try {
78
+ await rmdir(root);
79
+ }
80
+ catch {
81
+ throw new ContextProjectionError("rootOutputs.skills", "empty directory could not be removed");
82
+ }
83
+ return true;
84
+ }
85
+ export async function reconcileSkills(outputRoot, renderedFiles, repositoryRoot, written, removed, writePaths) {
86
+ const existingFiles = await listSkillFiles(outputRoot);
87
+ const expected = new Set(renderedFiles.map((file) => file.relativePath));
88
+ for (const file of renderedFiles) {
89
+ const path = join(outputRoot, file.relativePath);
90
+ if (writePaths !== undefined && !writePaths.has(path))
91
+ continue;
92
+ await atomicWrite(path, file.content, `rootOutputs.skills/${file.relativePath}`, file.mode);
93
+ written.push(relative(repositoryRoot, path));
94
+ }
95
+ for (const file of existingFiles) {
96
+ if (expected.has(file))
97
+ continue;
98
+ const path = join(outputRoot, file);
99
+ if (await removeManagedFile(path, `rootOutputs.skills/${file}`)) {
100
+ removed.push(relative(repositoryRoot, path));
101
+ }
102
+ }
103
+ if (await removeEmptySkillDirectories(outputRoot)) {
104
+ removed.push(relative(repositoryRoot, outputRoot));
105
+ }
106
+ }
107
+ export async function projectRootContext(options) {
108
+ const paths = resolveContextPaths(options);
109
+ assertSafeContextPaths(paths);
110
+ await Promise.all([
111
+ assertWritableTarget(paths.rootOutputs.agents, "AGENTS.md"),
112
+ assertWritableTarget(paths.rootOutputs.claude, "CLAUDE.md"),
113
+ assertWritableTarget(paths.rootOutputs.settings, ".claude/settings.json"),
114
+ assertWritableTarget(paths.rootOutputs.mcp, ".mcp.json"),
115
+ listSkillFiles(paths.rootOutputs.skills),
116
+ ]);
117
+ // Render every source before changing a generated target. Invalid source
118
+ // content must not leave a partially updated repository projection.
119
+ const [markdown, settings, mcp, skills] = await Promise.all([
120
+ renderRootMarkdown(paths),
121
+ renderOptionalSettings(paths),
122
+ renderOptionalMcp(paths),
123
+ renderSkillFiles(paths),
124
+ ]);
125
+ const written = [];
126
+ const removed = [];
127
+ await atomicWrite(paths.rootOutputs.agents, markdown.agents, "AGENTS.md");
128
+ written.push(relative(paths.repositoryRoot, paths.rootOutputs.agents));
129
+ await atomicWrite(paths.rootOutputs.claude, markdown.claude, "CLAUDE.md");
130
+ written.push(relative(paths.repositoryRoot, paths.rootOutputs.claude));
131
+ if (settings === undefined) {
132
+ if (await removeManagedFile(paths.rootOutputs.settings, ".claude/settings.json")) {
133
+ removed.push(relative(paths.repositoryRoot, paths.rootOutputs.settings));
134
+ }
135
+ }
136
+ else {
137
+ await atomicWrite(paths.rootOutputs.settings, settings, ".claude/settings.json");
138
+ written.push(relative(paths.repositoryRoot, paths.rootOutputs.settings));
139
+ }
140
+ if (mcp === undefined) {
141
+ if (await removeManagedFile(paths.rootOutputs.mcp, ".mcp.json")) {
142
+ removed.push(relative(paths.repositoryRoot, paths.rootOutputs.mcp));
143
+ }
144
+ }
145
+ else {
146
+ await atomicWrite(paths.rootOutputs.mcp, mcp, ".mcp.json");
147
+ written.push(relative(paths.repositoryRoot, paths.rootOutputs.mcp));
148
+ }
149
+ await reconcileSkills(paths.rootOutputs.skills, skills, paths.repositoryRoot, written, removed);
150
+ return { written, removed };
151
+ }
152
+ //# sourceMappingURL=root-project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-project.js","sourceRoot":"","sources":["../src/root-project.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, readdir, rmdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport { renderOptionalMcp, renderOptionalSettings } from \"./template.js\";\nimport { renderRootMarkdown } from \"./render.js\";\nimport { assertSafeContextPaths, resolveContextPaths } from \"./paths.js\";\nimport { renderSkillFiles, type RenderedSkillFile } from \"./skills.js\";\nimport {\n atomicWrite,\n assertWritableTarget,\n isNotFound,\n removeManagedFile,\n} from \"./internal/filesystem.js\";\nimport { ContextProjectionError } from \"./errors.js\";\nimport type { ContextProjectionResult } from \"./types.js\";\n\nexport { ContextProjectionError } from \"./errors.js\";\n\nexport type ProjectRootContextOptions = Readonly<{\n repositoryRoot: string;\n prototypeRoot?: string;\n}>;\n\nexport async function listSkillFiles(\n root: string,\n relativeDirectory = \"\",\n): Promise<string[]> {\n let entries;\n try {\n entries = await readdir(root, { withFileTypes: true });\n } catch (error) {\n if (isNotFound(error)) return [];\n throw new ContextProjectionError(\n relativeDirectory || \"rootOutputs.skills\",\n \"directory could not be enumerated\",\n );\n }\n\n const files: string[] = [];\n for (const entry of entries.sort((left, right) =>\n left.name < right.name ? -1 : left.name > right.name ? 1 : 0,\n )) {\n const relativePath = relativeDirectory\n ? `${relativeDirectory}/${entry.name}`\n : entry.name;\n const path = join(root, entry.name);\n let stats;\n try {\n stats = await lstat(path);\n } catch {\n throw new ContextProjectionError(\n `rootOutputs.skills/${relativePath}`,\n \"could not be inspected safely\",\n );\n }\n if (stats.isSymbolicLink()) {\n throw new ContextProjectionError(\n `rootOutputs.skills/${relativePath}`,\n \"must not be a symbolic link\",\n );\n }\n if (stats.isDirectory()) {\n files.push(...(await listSkillFiles(path, relativePath)));\n } else if (stats.isFile()) {\n files.push(relativePath);\n } else {\n throw new ContextProjectionError(\n `rootOutputs.skills/${relativePath}`,\n \"must be a regular file or directory\",\n );\n }\n }\n return files;\n}\n\nasync function removeEmptySkillDirectories(root: string): Promise<boolean> {\n let entries;\n try {\n entries = await readdir(root, { withFileTypes: true });\n } catch (error) {\n if (isNotFound(error)) return false;\n throw new ContextProjectionError(\n \"rootOutputs.skills\",\n \"directory could not be enumerated\",\n );\n }\n\n for (const entry of entries) {\n const path = join(root, entry.name);\n let stats;\n try {\n stats = await lstat(path);\n } catch {\n throw new ContextProjectionError(\n `rootOutputs.skills/${entry.name}`,\n \"could not be inspected safely\",\n );\n }\n if (stats.isSymbolicLink()) {\n throw new ContextProjectionError(\n `rootOutputs.skills/${entry.name}`,\n \"must not be a symbolic link\",\n );\n }\n if (stats.isDirectory()) {\n await removeEmptySkillDirectories(path);\n }\n }\n\n const remaining = await readdir(root);\n if (remaining.length !== 0) return false;\n try {\n await rmdir(root);\n } catch {\n throw new ContextProjectionError(\n \"rootOutputs.skills\",\n \"empty directory could not be removed\",\n );\n }\n return true;\n}\n\nexport async function reconcileSkills(\n outputRoot: string,\n renderedFiles: readonly RenderedSkillFile[],\n repositoryRoot: string,\n written: string[],\n removed: string[],\n writePaths?: ReadonlySet<string>,\n) {\n const existingFiles = await listSkillFiles(outputRoot);\n const expected = new Set(renderedFiles.map((file) => file.relativePath));\n\n for (const file of renderedFiles) {\n const path = join(outputRoot, file.relativePath);\n if (writePaths !== undefined && !writePaths.has(path)) continue;\n await atomicWrite(\n path,\n file.content,\n `rootOutputs.skills/${file.relativePath}`,\n file.mode,\n );\n written.push(relative(repositoryRoot, path));\n }\n\n for (const file of existingFiles) {\n if (expected.has(file)) continue;\n const path = join(outputRoot, file);\n if (await removeManagedFile(path, `rootOutputs.skills/${file}`)) {\n removed.push(relative(repositoryRoot, path));\n }\n }\n\n if (await removeEmptySkillDirectories(outputRoot)) {\n removed.push(relative(repositoryRoot, outputRoot));\n }\n}\n\nexport async function projectRootContext(\n options: ProjectRootContextOptions,\n): Promise<ContextProjectionResult> {\n const paths = resolveContextPaths(options);\n assertSafeContextPaths(paths);\n await Promise.all([\n assertWritableTarget(paths.rootOutputs.agents, \"AGENTS.md\"),\n assertWritableTarget(paths.rootOutputs.claude, \"CLAUDE.md\"),\n assertWritableTarget(paths.rootOutputs.settings, \".claude/settings.json\"),\n assertWritableTarget(paths.rootOutputs.mcp, \".mcp.json\"),\n listSkillFiles(paths.rootOutputs.skills),\n ]);\n\n // Render every source before changing a generated target. Invalid source\n // content must not leave a partially updated repository projection.\n const [markdown, settings, mcp, skills] = await Promise.all([\n renderRootMarkdown(paths),\n renderOptionalSettings(paths),\n renderOptionalMcp(paths),\n renderSkillFiles(paths),\n ]);\n\n const written: string[] = [];\n const removed: string[] = [];\n await atomicWrite(paths.rootOutputs.agents, markdown.agents, \"AGENTS.md\");\n written.push(relative(paths.repositoryRoot, paths.rootOutputs.agents));\n await atomicWrite(paths.rootOutputs.claude, markdown.claude, \"CLAUDE.md\");\n written.push(relative(paths.repositoryRoot, paths.rootOutputs.claude));\n\n if (settings === undefined) {\n if (\n await removeManagedFile(\n paths.rootOutputs.settings,\n \".claude/settings.json\",\n )\n ) {\n removed.push(relative(paths.repositoryRoot, paths.rootOutputs.settings));\n }\n } else {\n await atomicWrite(\n paths.rootOutputs.settings,\n settings,\n \".claude/settings.json\",\n );\n written.push(relative(paths.repositoryRoot, paths.rootOutputs.settings));\n }\n\n if (mcp === undefined) {\n if (await removeManagedFile(paths.rootOutputs.mcp, \".mcp.json\")) {\n removed.push(relative(paths.repositoryRoot, paths.rootOutputs.mcp));\n }\n } else {\n await atomicWrite(paths.rootOutputs.mcp, mcp, \".mcp.json\");\n written.push(relative(paths.repositoryRoot, paths.rootOutputs.mcp));\n }\n\n await reconcileSkills(\n paths.rootOutputs.skills,\n skills,\n paths.repositoryRoot,\n written,\n removed,\n );\n\n return { written, removed };\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import { type ResolvedContextPaths } from "./paths.js";
2
+ export type RenderedSkillFile = Readonly<{
3
+ relativePath: string;
4
+ content: Buffer;
5
+ mode: number;
6
+ }>;
7
+ export declare class ContextSkillError extends Error {
8
+ readonly relativePath: string;
9
+ constructor(relativePath: string, message: string);
10
+ }
11
+ export declare function validateSkillNames(names: readonly string[]): void;
12
+ export declare function renderSkillFiles(paths: ResolvedContextPaths): Promise<readonly RenderedSkillFile[]>;
13
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAGA,OAAO,EAA0B,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAM/E,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC,CAAC;AAEH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B,YAAY,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIhD;CACF;AAuCD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,QAM1D;AAqFD,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CA0EvC","sourcesContent":["import { constants } from \"node:fs\";\nimport { lstat, open, readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { assertSafeContextPaths, type ResolvedContextPaths } from \"./paths.js\";\n\nconst SKILL_FILE_MODE = 0o644;\nconst SKILL_MARKER_FILE = \"SKILL.md\";\nconst FILE_OPEN_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);\n\nexport type RenderedSkillFile = Readonly<{\n relativePath: string;\n content: Buffer;\n mode: number;\n}>;\n\nexport class ContextSkillError extends Error {\n readonly relativePath: string;\n\n constructor(relativePath: string, message: string) {\n super(`Invalid context skill at ${relativePath}: ${message}`);\n this.name = \"ContextSkillError\";\n this.relativePath = relativePath;\n }\n}\n\nfunction isSafeSegment(segment: string) {\n return (\n segment.length > 0 &&\n segment !== \".\" &&\n segment !== \"..\" &&\n !segment.startsWith(\".\") &&\n !segment.includes(\"/\") &&\n !segment.includes(\"\\\\\") &&\n !segment.includes(\"\\0\")\n );\n}\n\nfunction assertSafeSkillSegment(segment: string, relativePath: string) {\n if (!isSafeSegment(segment)) {\n throw new ContextSkillError(\n relativePath,\n \"contains an unsafe path segment\",\n );\n }\n}\n\nfunction assertUniqueSkillName(\n name: string,\n relativePath: string,\n seenNames: Set<string>,\n) {\n const normalizedName = name.toLowerCase();\n if (seenNames.has(normalizedName)) {\n throw new ContextSkillError(relativePath, \"duplicates another skill name\");\n }\n seenNames.add(normalizedName);\n}\n\nfunction compareNames(left: string, right: string) {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\nexport function validateSkillNames(names: readonly string[]) {\n const seenNames = new Set<string>();\n for (const name of names) {\n assertSafeSkillSegment(name, name);\n assertUniqueSkillName(name, name, seenNames);\n }\n}\n\nasync function readRegularFile(path: string, relativePath: string) {\n let file;\n try {\n file = await open(path, FILE_OPEN_FLAGS);\n const stats = await file.stat();\n if (!stats.isFile()) {\n throw new ContextSkillError(relativePath, \"must be a regular file\");\n }\n return Buffer.from(await file.readFile());\n } catch (error) {\n if (error instanceof ContextSkillError) throw error;\n throw new ContextSkillError(relativePath, \"could not be read safely\");\n } finally {\n await file?.close().catch(() => undefined);\n }\n}\n\nasync function collectSkillFiles(\n root: string,\n relativeDirectory: string,\n): Promise<RenderedSkillFile[]> {\n let entries;\n try {\n entries = await readdir(root, { withFileTypes: true });\n } catch {\n throw new ContextSkillError(\n relativeDirectory,\n \"directory could not be enumerated\",\n );\n }\n\n const files: RenderedSkillFile[] = [];\n for (const entry of entries.sort((left, right) =>\n compareNames(left.name, right.name),\n )) {\n const relativePath = relativeDirectory\n ? `${relativeDirectory}/${entry.name}`\n : entry.name;\n assertSafeSkillSegment(entry.name, relativePath);\n const path = join(root, entry.name);\n\n let stats;\n try {\n stats = await lstat(path);\n } catch {\n throw new ContextSkillError(relativePath, \"could not be inspected\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextSkillError(relativePath, \"must not be a symbolic link\");\n }\n\n if (stats.isDirectory()) {\n files.push(...(await collectSkillFiles(path, relativePath)));\n continue;\n }\n if (!stats.isFile()) {\n throw new ContextSkillError(relativePath, \"must be a regular file\");\n }\n\n files.push({\n relativePath,\n content: await readRegularFile(path, relativePath),\n mode: SKILL_FILE_MODE,\n });\n }\n\n return files;\n}\n\nfunction assertUniqueRenderedPaths(files: readonly RenderedSkillFile[]) {\n const seen = new Set<string>();\n for (const file of files) {\n const normalizedPath = file.relativePath.toLowerCase();\n if (seen.has(normalizedPath)) {\n throw new ContextSkillError(\n file.relativePath,\n \"duplicates another projected file\",\n );\n }\n seen.add(normalizedPath);\n }\n}\n\nexport async function renderSkillFiles(\n paths: ResolvedContextPaths,\n): Promise<readonly RenderedSkillFile[]> {\n assertSafeContextPaths(paths);\n\n let stats;\n try {\n stats = await lstat(paths.sources.skills);\n } catch (error) {\n if (\n error !== null &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n ) {\n return [];\n }\n throw new ContextSkillError(\"context/skills\", \"could not be inspected\");\n }\n\n if (stats.isSymbolicLink()) {\n throw new ContextSkillError(\n \"context/skills\",\n \"must not be a symbolic link\",\n );\n }\n if (!stats.isDirectory()) {\n throw new ContextSkillError(\"context/skills\", \"must be a directory\");\n }\n\n let entries;\n try {\n entries = await readdir(paths.sources.skills, { withFileTypes: true });\n } catch {\n throw new ContextSkillError(\n \"context/skills\",\n \"directory could not be enumerated\",\n );\n }\n\n validateSkillNames(entries.map((entry) => entry.name));\n const files: RenderedSkillFile[] = [];\n for (const entry of entries.sort((left, right) =>\n compareNames(left.name, right.name),\n )) {\n const skillPath = join(paths.sources.skills, entry.name);\n\n let stats;\n try {\n stats = await lstat(skillPath);\n } catch {\n throw new ContextSkillError(entry.name, \"could not be inspected\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextSkillError(entry.name, \"must not be a symbolic link\");\n }\n if (!stats.isDirectory()) {\n throw new ContextSkillError(entry.name, \"skill must be a directory\");\n }\n\n const skillFiles = await collectSkillFiles(skillPath, entry.name);\n if (\n !skillFiles.some(\n (file) => file.relativePath === `${entry.name}/${SKILL_MARKER_FILE}`,\n )\n ) {\n throw new ContextSkillError(\n entry.name,\n `must contain ${SKILL_MARKER_FILE}`,\n );\n }\n files.push(...skillFiles);\n }\n\n assertUniqueRenderedPaths(files);\n return files;\n}\n"]}
package/dist/skills.js ADDED
@@ -0,0 +1,170 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { assertSafeContextPaths } from "./paths.js";
5
+ const SKILL_FILE_MODE = 0o644;
6
+ const SKILL_MARKER_FILE = "SKILL.md";
7
+ const FILE_OPEN_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
8
+ export class ContextSkillError extends Error {
9
+ relativePath;
10
+ constructor(relativePath, message) {
11
+ super(`Invalid context skill at ${relativePath}: ${message}`);
12
+ this.name = "ContextSkillError";
13
+ this.relativePath = relativePath;
14
+ }
15
+ }
16
+ function isSafeSegment(segment) {
17
+ return (segment.length > 0 &&
18
+ segment !== "." &&
19
+ segment !== ".." &&
20
+ !segment.startsWith(".") &&
21
+ !segment.includes("/") &&
22
+ !segment.includes("\\") &&
23
+ !segment.includes("\0"));
24
+ }
25
+ function assertSafeSkillSegment(segment, relativePath) {
26
+ if (!isSafeSegment(segment)) {
27
+ throw new ContextSkillError(relativePath, "contains an unsafe path segment");
28
+ }
29
+ }
30
+ function assertUniqueSkillName(name, relativePath, seenNames) {
31
+ const normalizedName = name.toLowerCase();
32
+ if (seenNames.has(normalizedName)) {
33
+ throw new ContextSkillError(relativePath, "duplicates another skill name");
34
+ }
35
+ seenNames.add(normalizedName);
36
+ }
37
+ function compareNames(left, right) {
38
+ return left < right ? -1 : left > right ? 1 : 0;
39
+ }
40
+ export function validateSkillNames(names) {
41
+ const seenNames = new Set();
42
+ for (const name of names) {
43
+ assertSafeSkillSegment(name, name);
44
+ assertUniqueSkillName(name, name, seenNames);
45
+ }
46
+ }
47
+ async function readRegularFile(path, relativePath) {
48
+ let file;
49
+ try {
50
+ file = await open(path, FILE_OPEN_FLAGS);
51
+ const stats = await file.stat();
52
+ if (!stats.isFile()) {
53
+ throw new ContextSkillError(relativePath, "must be a regular file");
54
+ }
55
+ return Buffer.from(await file.readFile());
56
+ }
57
+ catch (error) {
58
+ if (error instanceof ContextSkillError)
59
+ throw error;
60
+ throw new ContextSkillError(relativePath, "could not be read safely");
61
+ }
62
+ finally {
63
+ await file?.close().catch(() => undefined);
64
+ }
65
+ }
66
+ async function collectSkillFiles(root, relativeDirectory) {
67
+ let entries;
68
+ try {
69
+ entries = await readdir(root, { withFileTypes: true });
70
+ }
71
+ catch {
72
+ throw new ContextSkillError(relativeDirectory, "directory could not be enumerated");
73
+ }
74
+ const files = [];
75
+ for (const entry of entries.sort((left, right) => compareNames(left.name, right.name))) {
76
+ const relativePath = relativeDirectory
77
+ ? `${relativeDirectory}/${entry.name}`
78
+ : entry.name;
79
+ assertSafeSkillSegment(entry.name, relativePath);
80
+ const path = join(root, entry.name);
81
+ let stats;
82
+ try {
83
+ stats = await lstat(path);
84
+ }
85
+ catch {
86
+ throw new ContextSkillError(relativePath, "could not be inspected");
87
+ }
88
+ if (stats.isSymbolicLink()) {
89
+ throw new ContextSkillError(relativePath, "must not be a symbolic link");
90
+ }
91
+ if (stats.isDirectory()) {
92
+ files.push(...(await collectSkillFiles(path, relativePath)));
93
+ continue;
94
+ }
95
+ if (!stats.isFile()) {
96
+ throw new ContextSkillError(relativePath, "must be a regular file");
97
+ }
98
+ files.push({
99
+ relativePath,
100
+ content: await readRegularFile(path, relativePath),
101
+ mode: SKILL_FILE_MODE,
102
+ });
103
+ }
104
+ return files;
105
+ }
106
+ function assertUniqueRenderedPaths(files) {
107
+ const seen = new Set();
108
+ for (const file of files) {
109
+ const normalizedPath = file.relativePath.toLowerCase();
110
+ if (seen.has(normalizedPath)) {
111
+ throw new ContextSkillError(file.relativePath, "duplicates another projected file");
112
+ }
113
+ seen.add(normalizedPath);
114
+ }
115
+ }
116
+ export async function renderSkillFiles(paths) {
117
+ assertSafeContextPaths(paths);
118
+ let stats;
119
+ try {
120
+ stats = await lstat(paths.sources.skills);
121
+ }
122
+ catch (error) {
123
+ if (error !== null &&
124
+ typeof error === "object" &&
125
+ "code" in error &&
126
+ error.code === "ENOENT") {
127
+ return [];
128
+ }
129
+ throw new ContextSkillError("context/skills", "could not be inspected");
130
+ }
131
+ if (stats.isSymbolicLink()) {
132
+ throw new ContextSkillError("context/skills", "must not be a symbolic link");
133
+ }
134
+ if (!stats.isDirectory()) {
135
+ throw new ContextSkillError("context/skills", "must be a directory");
136
+ }
137
+ let entries;
138
+ try {
139
+ entries = await readdir(paths.sources.skills, { withFileTypes: true });
140
+ }
141
+ catch {
142
+ throw new ContextSkillError("context/skills", "directory could not be enumerated");
143
+ }
144
+ validateSkillNames(entries.map((entry) => entry.name));
145
+ const files = [];
146
+ for (const entry of entries.sort((left, right) => compareNames(left.name, right.name))) {
147
+ const skillPath = join(paths.sources.skills, entry.name);
148
+ let stats;
149
+ try {
150
+ stats = await lstat(skillPath);
151
+ }
152
+ catch {
153
+ throw new ContextSkillError(entry.name, "could not be inspected");
154
+ }
155
+ if (stats.isSymbolicLink()) {
156
+ throw new ContextSkillError(entry.name, "must not be a symbolic link");
157
+ }
158
+ if (!stats.isDirectory()) {
159
+ throw new ContextSkillError(entry.name, "skill must be a directory");
160
+ }
161
+ const skillFiles = await collectSkillFiles(skillPath, entry.name);
162
+ if (!skillFiles.some((file) => file.relativePath === `${entry.name}/${SKILL_MARKER_FILE}`)) {
163
+ throw new ContextSkillError(entry.name, `must contain ${SKILL_MARKER_FILE}`);
164
+ }
165
+ files.push(...skillFiles);
166
+ }
167
+ assertUniqueRenderedPaths(files);
168
+ return files;
169
+ }
170
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAA6B,MAAM,YAAY,CAAC;AAE/E,MAAM,eAAe,GAAG,KAAK,CAAC;AAC9B,MAAM,iBAAiB,GAAG,UAAU,CAAC;AACrC,MAAM,eAAe,GAAG,SAAS,CAAC,QAAQ,GAAG,CAAC,SAAS,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;AAQzE,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,YAAY,CAAS;IAE9B,YAAY,YAAoB,EAAE,OAAe;QAC/C,KAAK,CAAC,4BAA4B,YAAY,KAAK,OAAO,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;CACF;AAED,SAAS,aAAa,CAAC,OAAe;IACpC,OAAO,CACL,OAAO,CAAC,MAAM,GAAG,CAAC;QAClB,OAAO,KAAK,GAAG;QACf,OAAO,KAAK,IAAI;QAChB,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QACxB,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QACtB,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;QACvB,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CACxB,CAAC;AACJ,CAAC;AAED,SAAS,sBAAsB,CAAC,OAAe,EAAE,YAAoB;IACnE,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,iBAAiB,CACzB,YAAY,EACZ,iCAAiC,CAClC,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,qBAAqB,CAC5B,IAAY,EACZ,YAAoB,EACpB,SAAsB;IAEtB,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAC1C,IAAI,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,iBAAiB,CAAC,YAAY,EAAE,+BAA+B,CAAC,CAAC;IAC7E,CAAC;IACD,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,KAAa;IAC/C,OAAO,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAwB;IACzD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,sBAAsB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACnC,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,YAAoB;IAC/D,IAAI,IAAI,CAAC;IACT,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAChC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACpB,MAAM,IAAI,iBAAiB,CAAC,YAAY,EAAE,wBAAwB,CAAC,CAAC;QACtE,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,iBAAiB;YAAE,MAAM,KAAK,CAAC;QACpD,MAAM,IAAI,iBAAiB,CAAC,YAAY,EAAE,0BAA0B,CAAC,CAAC;IACxE,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAED,KAAK,UAAU,iBAAiB,CAC9B,IAAY,EACZ,iBAAyB;IAEzB,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,iBAAiB,CACzB,iBAAiB,EACjB,mCAAmC,CACpC,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAwB,EAAE,CAAC;IACtC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAC/C,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CACpC,EAAE,CAAC;QACF,MAAM,YAAY,GAAG,iBAAiB;YACpC,CAAC,CAAC,GAAG,iBAAiB,IAAI,KAAK,CAAC,IAAI,EAAE;YACtC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QACf,sBAAsB,CAAC,KAAK,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;QACjD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAEpC,IAAI,KAAK,CAAC;QACV,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,iBAAiB,CAAC,YAAY,EAAE,wBAAwB,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YAC3B,MAAM,IAAI,iBAAiB,CAAC,YAAY,EAAE,6BAA6B,CAAC,CAAC;QAC3E,CAAC;QAED,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,iBAAiB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;YAC7D,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACpB,MAAM,IAAI,iBAAiB,CAAC,YAAY,EAAE,wBAAwB,CAAC,CAAC;QACtE,CAAC;QAED,KAAK,CAAC,IAAI,CAAC;YACT,YAAY;YACZ,OAAO,EAAE,MAAM,eAAe,CAAC,IAAI,EAAE,YAAY,CAAC;YAClD,IAAI,EAAE,eAAe;SACtB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,yBAAyB,CAAC,KAAmC;IACpE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,cAAc,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;QACvD,IAAI,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,iBAAiB,CACzB,IAAI,CAAC,YAAY,EACjB,mCAAmC,CACpC,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA2B;IAE3B,sBAAsB,CAAC,KAAK,CAAC,CAAC;IAE9B,IAAI,KAAK,CAAC;IACV,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IACE,KAAK,KAAK,IAAI;YACd,OAAO,KAAK,KAAK,QAAQ;YACzB,MAAM,IAAI,KAAK;YACf,KAAK,CAAC,IAAI,KAAK,QAAQ,EACvB,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,IAAI,iBAAiB,CAAC,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;IAC1E,CAAC;IAED,IAAI,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;QAC3B,MAAM,IAAI,iBAAiB,CACzB,gBAAgB,EAChB,6BAA6B,CAC9B,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;QACzB,MAAM,IAAI,iBAAiB,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,CAAC;IACvE,CAAC;IAED,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,iBAAiB,CACzB,gBAAgB,EAChB,mCAAmC,CACpC,CAAC;IACJ,CAAC;IAED,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,MAAM,KAAK,GAAwB,EAAE,CAAC;IACtC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAC/C,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CACpC,EAAE,CAAC;QACF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAEzD,IAAI,KAAK,CAAC;QACV,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,iBAAiB,CAAC,KAAK,CAAC,IAAI,EAAE,wBAAwB,CAAC,CAAC;QACpE,CAAC;QACD,IAAI,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YAC3B,MAAM,IAAI,iBAAiB,CAAC,KAAK,CAAC,IAAI,EAAE,6BAA6B,CAAC,CAAC;QACzE,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACzB,MAAM,IAAI,iBAAiB,CAAC,KAAK,CAAC,IAAI,EAAE,2BAA2B,CAAC,CAAC;QACvE,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAClE,IACE,CAAC,UAAU,CAAC,IAAI,CACd,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,KAAK,GAAG,KAAK,CAAC,IAAI,IAAI,iBAAiB,EAAE,CACrE,EACD,CAAC;YACD,MAAM,IAAI,iBAAiB,CACzB,KAAK,CAAC,IAAI,EACV,gBAAgB,iBAAiB,EAAE,CACpC,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;IAC5B,CAAC;IAED,yBAAyB,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import { constants } from \"node:fs\";\nimport { lstat, open, readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { assertSafeContextPaths, type ResolvedContextPaths } from \"./paths.js\";\n\nconst SKILL_FILE_MODE = 0o644;\nconst SKILL_MARKER_FILE = \"SKILL.md\";\nconst FILE_OPEN_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);\n\nexport type RenderedSkillFile = Readonly<{\n relativePath: string;\n content: Buffer;\n mode: number;\n}>;\n\nexport class ContextSkillError extends Error {\n readonly relativePath: string;\n\n constructor(relativePath: string, message: string) {\n super(`Invalid context skill at ${relativePath}: ${message}`);\n this.name = \"ContextSkillError\";\n this.relativePath = relativePath;\n }\n}\n\nfunction isSafeSegment(segment: string) {\n return (\n segment.length > 0 &&\n segment !== \".\" &&\n segment !== \"..\" &&\n !segment.startsWith(\".\") &&\n !segment.includes(\"/\") &&\n !segment.includes(\"\\\\\") &&\n !segment.includes(\"\\0\")\n );\n}\n\nfunction assertSafeSkillSegment(segment: string, relativePath: string) {\n if (!isSafeSegment(segment)) {\n throw new ContextSkillError(\n relativePath,\n \"contains an unsafe path segment\",\n );\n }\n}\n\nfunction assertUniqueSkillName(\n name: string,\n relativePath: string,\n seenNames: Set<string>,\n) {\n const normalizedName = name.toLowerCase();\n if (seenNames.has(normalizedName)) {\n throw new ContextSkillError(relativePath, \"duplicates another skill name\");\n }\n seenNames.add(normalizedName);\n}\n\nfunction compareNames(left: string, right: string) {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\nexport function validateSkillNames(names: readonly string[]) {\n const seenNames = new Set<string>();\n for (const name of names) {\n assertSafeSkillSegment(name, name);\n assertUniqueSkillName(name, name, seenNames);\n }\n}\n\nasync function readRegularFile(path: string, relativePath: string) {\n let file;\n try {\n file = await open(path, FILE_OPEN_FLAGS);\n const stats = await file.stat();\n if (!stats.isFile()) {\n throw new ContextSkillError(relativePath, \"must be a regular file\");\n }\n return Buffer.from(await file.readFile());\n } catch (error) {\n if (error instanceof ContextSkillError) throw error;\n throw new ContextSkillError(relativePath, \"could not be read safely\");\n } finally {\n await file?.close().catch(() => undefined);\n }\n}\n\nasync function collectSkillFiles(\n root: string,\n relativeDirectory: string,\n): Promise<RenderedSkillFile[]> {\n let entries;\n try {\n entries = await readdir(root, { withFileTypes: true });\n } catch {\n throw new ContextSkillError(\n relativeDirectory,\n \"directory could not be enumerated\",\n );\n }\n\n const files: RenderedSkillFile[] = [];\n for (const entry of entries.sort((left, right) =>\n compareNames(left.name, right.name),\n )) {\n const relativePath = relativeDirectory\n ? `${relativeDirectory}/${entry.name}`\n : entry.name;\n assertSafeSkillSegment(entry.name, relativePath);\n const path = join(root, entry.name);\n\n let stats;\n try {\n stats = await lstat(path);\n } catch {\n throw new ContextSkillError(relativePath, \"could not be inspected\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextSkillError(relativePath, \"must not be a symbolic link\");\n }\n\n if (stats.isDirectory()) {\n files.push(...(await collectSkillFiles(path, relativePath)));\n continue;\n }\n if (!stats.isFile()) {\n throw new ContextSkillError(relativePath, \"must be a regular file\");\n }\n\n files.push({\n relativePath,\n content: await readRegularFile(path, relativePath),\n mode: SKILL_FILE_MODE,\n });\n }\n\n return files;\n}\n\nfunction assertUniqueRenderedPaths(files: readonly RenderedSkillFile[]) {\n const seen = new Set<string>();\n for (const file of files) {\n const normalizedPath = file.relativePath.toLowerCase();\n if (seen.has(normalizedPath)) {\n throw new ContextSkillError(\n file.relativePath,\n \"duplicates another projected file\",\n );\n }\n seen.add(normalizedPath);\n }\n}\n\nexport async function renderSkillFiles(\n paths: ResolvedContextPaths,\n): Promise<readonly RenderedSkillFile[]> {\n assertSafeContextPaths(paths);\n\n let stats;\n try {\n stats = await lstat(paths.sources.skills);\n } catch (error) {\n if (\n error !== null &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n ) {\n return [];\n }\n throw new ContextSkillError(\"context/skills\", \"could not be inspected\");\n }\n\n if (stats.isSymbolicLink()) {\n throw new ContextSkillError(\n \"context/skills\",\n \"must not be a symbolic link\",\n );\n }\n if (!stats.isDirectory()) {\n throw new ContextSkillError(\"context/skills\", \"must be a directory\");\n }\n\n let entries;\n try {\n entries = await readdir(paths.sources.skills, { withFileTypes: true });\n } catch {\n throw new ContextSkillError(\n \"context/skills\",\n \"directory could not be enumerated\",\n );\n }\n\n validateSkillNames(entries.map((entry) => entry.name));\n const files: RenderedSkillFile[] = [];\n for (const entry of entries.sort((left, right) =>\n compareNames(left.name, right.name),\n )) {\n const skillPath = join(paths.sources.skills, entry.name);\n\n let stats;\n try {\n stats = await lstat(skillPath);\n } catch {\n throw new ContextSkillError(entry.name, \"could not be inspected\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextSkillError(entry.name, \"must not be a symbolic link\");\n }\n if (!stats.isDirectory()) {\n throw new ContextSkillError(entry.name, \"skill must be a directory\");\n }\n\n const skillFiles = await collectSkillFiles(skillPath, entry.name);\n if (\n !skillFiles.some(\n (file) => file.relativePath === `${entry.name}/${SKILL_MARKER_FILE}`,\n )\n ) {\n throw new ContextSkillError(\n entry.name,\n `must contain ${SKILL_MARKER_FILE}`,\n );\n }\n files.push(...skillFiles);\n }\n\n assertUniqueRenderedPaths(files);\n return files;\n}\n"]}
package/dist/sync.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { type ContextPathOptions } from "./paths.js";
2
+ export type SyncContextOptions = ContextPathOptions;
3
+ export type CheckContextOptions = ContextPathOptions;
4
+ export type ContextIssueKind = "missing" | "stale" | "unexpected";
5
+ export type ContextCheckIssue = Readonly<{
6
+ kind: ContextIssueKind;
7
+ path: string;
8
+ }>;
9
+ export type ContextCheckResult = Readonly<{
10
+ ok: boolean;
11
+ issues: readonly ContextCheckIssue[];
12
+ }>;
13
+ export type ContextSyncResult = Readonly<ContextCheckResult & {
14
+ written: readonly string[];
15
+ removed: readonly string[];
16
+ }>;
17
+ export declare class ContextSyncError extends Error {
18
+ readonly path: string;
19
+ constructor(path: string, message: string);
20
+ }
21
+ export declare function checkContext(options: CheckContextOptions): Promise<ContextCheckResult>;
22
+ export declare function syncContext(options: SyncContextOptions): Promise<ContextSyncResult>;
23
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAaA,OAAO,EAML,KAAK,kBAAkB,EAGxB,MAAM,YAAY,CAAC;AAIpB,MAAM,MAAM,kBAAkB,GAAG,kBAAkB,CAAC;AACpD,MAAM,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;AAErD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAG,YAAY,CAAC;AAElE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACtC,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CACtC,kBAAkB,GAAG;IACnB,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,CACF,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxC;CACF;AAwRD,wBAAsB,YAAY,CAChC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAE7B;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAsC5B","sourcesContent":["import { lstat, readFile } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\nimport {\n discoverPrototypeDirectoryPaths,\n discoverPrototypePaths,\n} from \"./discover.js\";\nimport { listSkillFiles, reconcileSkills } from \"./root-project.js\";\nimport {\n atomicWrite,\n assertWritableTarget,\n removeManagedFile,\n} from \"./internal/filesystem.js\";\nimport { renderPrototypeMarkdown, renderRootMarkdown } from \"./render.js\";\nimport {\n assertSafeContextPaths,\n assertSafeGeneratedPath,\n assertSafePrototypeContextPaths,\n resolveContextPaths,\n resolvePrototypeContextPaths,\n type ContextPathOptions,\n type ResolvedContextPaths,\n type ResolvedPrototypeContextPaths,\n} from \"./paths.js\";\nimport { renderSkillFiles, type RenderedSkillFile } from \"./skills.js\";\nimport { renderOptionalMcp, renderOptionalSettings } from \"./template.js\";\n\nexport type SyncContextOptions = ContextPathOptions;\nexport type CheckContextOptions = ContextPathOptions;\n\nexport type ContextIssueKind = \"missing\" | \"stale\" | \"unexpected\";\n\nexport type ContextCheckIssue = Readonly<{\n kind: ContextIssueKind;\n path: string;\n}>;\n\nexport type ContextCheckResult = Readonly<{\n ok: boolean;\n issues: readonly ContextCheckIssue[];\n}>;\n\nexport type ContextSyncResult = Readonly<\n ContextCheckResult & {\n written: readonly string[];\n removed: readonly string[];\n }\n>;\n\nexport class ContextSyncError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to check context at ${path}: ${message}`);\n this.name = \"ContextSyncError\";\n this.path = path;\n }\n}\n\ntype RenderedFile = Readonly<{\n content: string | Uint8Array;\n mode?: number;\n}>;\n\ntype PrototypeProjectionPlan = Readonly<{\n paths: ResolvedPrototypeContextPaths;\n files: ReadonlyMap<string, RenderedFile>;\n}>;\n\ntype ProjectionPlan = Readonly<{\n paths: ResolvedContextPaths;\n rootFiles: ReadonlyMap<string, RenderedFile>;\n rootSkills: readonly RenderedSkillFile[];\n prototypes: readonly PrototypeProjectionPlan[];\n expected: ReadonlyMap<string, RenderedFile>;\n managed: ReadonlySet<string>;\n}>;\n\nfunction isNotFound(error: unknown) {\n return (\n error !== null &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n );\n}\n\nfunction relativePath(repositoryRoot: string, path: string) {\n return relative(repositoryRoot, path).split(sep).join(\"/\");\n}\n\nfunction addExpected(\n expected: Map<string, RenderedFile>,\n path: string,\n content: string | Uint8Array,\n mode?: number,\n) {\n expected.set(path, { content, mode });\n}\n\nasync function addExistingManagedPath(\n managed: Set<string>,\n path: string,\n label: string,\n) {\n assertSafeGeneratedPath(path, label);\n try {\n const stats = await lstat(path);\n if (stats.isSymbolicLink()) {\n throw new ContextSyncError(label, \"must not be a symbolic link\");\n }\n managed.add(path);\n } catch (error) {\n if (error instanceof ContextSyncError) throw error;\n if (!isNotFound(error)) {\n throw new ContextSyncError(label, \"could not be inspected safely\");\n }\n }\n}\n\nasync function buildProjectionPlan(\n options: SyncContextOptions,\n): Promise<ProjectionPlan> {\n const paths = resolveContextPaths(options);\n assertSafeContextPaths(paths);\n\n const [rootMarkdown, rootSettings, rootMcp, rootSkills] = await Promise.all([\n renderRootMarkdown(paths),\n renderOptionalSettings(paths),\n renderOptionalMcp(paths),\n renderSkillFiles(paths),\n ]);\n const prototypeDirectories = await discoverPrototypeDirectoryPaths(options);\n const prototypePaths = await discoverPrototypePaths(options);\n const prototypePathSet = new Set(prototypePaths);\n\n const expected = new Map<string, RenderedFile>();\n const rootFiles = new Map<string, RenderedFile>();\n addExpected(rootFiles, paths.rootOutputs.agents, rootMarkdown.agents);\n addExpected(rootFiles, paths.rootOutputs.claude, rootMarkdown.claude);\n if (rootSettings !== undefined) {\n addExpected(rootFiles, paths.rootOutputs.settings, rootSettings);\n }\n if (rootMcp !== undefined) {\n addExpected(rootFiles, paths.rootOutputs.mcp, rootMcp);\n }\n for (const [path, file] of rootFiles) expected.set(path, file);\n for (const skill of rootSkills) {\n addExpected(\n expected,\n join(paths.rootOutputs.skills, skill.relativePath),\n skill.content,\n skill.mode,\n );\n }\n\n const prototypes: PrototypeProjectionPlan[] = [];\n for (const prototypePath of prototypePaths) {\n const prototypeContextPaths = resolvePrototypeContextPaths({\n ...options,\n prototypePath,\n });\n assertSafePrototypeContextPaths(prototypeContextPaths);\n const [markdown, settings] = await Promise.all([\n renderPrototypeMarkdown(prototypeContextPaths),\n renderOptionalSettings(prototypeContextPaths),\n ]);\n const files = new Map<string, RenderedFile>();\n addExpected(\n files,\n prototypeContextPaths.prototypeOutputs.agents,\n markdown.agents,\n );\n addExpected(\n files,\n prototypeContextPaths.prototypeOutputs.claude,\n markdown.claude,\n );\n if (settings !== undefined) {\n addExpected(\n files,\n prototypeContextPaths.prototypeOutputs.settings,\n settings,\n );\n }\n for (const [path, file] of files) expected.set(path, file);\n prototypes.push({ paths: prototypeContextPaths, files });\n }\n\n const managed = new Set<string>([\n paths.rootOutputs.agents,\n paths.rootOutputs.claude,\n paths.rootOutputs.mcp,\n paths.rootOutputs.settings,\n ]);\n for (const path of await listSkillFiles(paths.rootOutputs.skills)) {\n managed.add(join(paths.rootOutputs.skills, path));\n }\n\n for (const prototypePath of prototypeDirectories) {\n const prototypeContextPaths = resolvePrototypeContextPaths({\n ...options,\n prototypePath,\n });\n if (prototypePathSet.has(prototypePath)) {\n for (const path of Object.values(\n prototypeContextPaths.prototypeOutputs,\n )) {\n managed.add(path);\n }\n continue;\n }\n for (const [label, path] of Object.entries(\n prototypeContextPaths.prototypeOutputs,\n )) {\n await addExistingManagedPath(\n managed,\n path,\n `prototype/${relativePath(paths.repositoryRoot, path)}:${label}`,\n );\n }\n }\n\n return {\n paths,\n rootFiles,\n rootSkills,\n prototypes,\n expected,\n managed,\n };\n}\n\nasync function compareFile(\n path: string,\n expected: RenderedFile,\n repositoryRoot: string,\n): Promise<ContextCheckIssue | undefined> {\n const displayPath = relativePath(repositoryRoot, path);\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) return { kind: \"missing\", path: displayPath };\n throw new ContextSyncError(displayPath, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextSyncError(displayPath, \"must not be a symbolic link\");\n }\n if (!stats.isFile()) return { kind: \"stale\", path: displayPath };\n\n let actual;\n try {\n actual = await readFile(path);\n } catch {\n throw new ContextSyncError(displayPath, \"could not be read safely\");\n }\n const expectedBytes =\n typeof expected.content === \"string\"\n ? Buffer.from(expected.content)\n : Buffer.from(expected.content);\n return actual.equals(expectedBytes)\n ? undefined\n : { kind: \"stale\", path: displayPath };\n}\n\nasync function comparePlan(plan: ProjectionPlan): Promise<ContextCheckResult> {\n const issues: ContextCheckIssue[] = [];\n for (const [path, expected] of [...plan.expected.entries()].sort(\n ([left], [right]) => (left < right ? -1 : left > right ? 1 : 0),\n )) {\n const issue = await compareFile(path, expected, plan.paths.repositoryRoot);\n if (issue !== undefined) issues.push(issue);\n }\n for (const path of [...plan.managed].sort()) {\n if (plan.expected.has(path)) continue;\n const displayPath = relativePath(plan.paths.repositoryRoot, path);\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) continue;\n throw new ContextSyncError(displayPath, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextSyncError(displayPath, \"must not be a symbolic link\");\n }\n if (!stats.isFile()) {\n throw new ContextSyncError(displayPath, \"must be a regular file\");\n }\n issues.push({ kind: \"unexpected\", path: displayPath });\n }\n\n issues.sort((left, right) => {\n if (left.path !== right.path) return left.path < right.path ? -1 : 1;\n return left.kind < right.kind ? -1 : left.kind > right.kind ? 1 : 0;\n });\n return { ok: issues.length === 0, issues };\n}\n\nasync function preflightWrites(\n plan: ProjectionPlan,\n issues: readonly ContextCheckIssue[],\n) {\n const issuePaths = new Set(\n issues\n .filter((issue) => issue.kind === \"missing\" || issue.kind === \"stale\")\n .map((issue) => join(plan.paths.repositoryRoot, issue.path)),\n );\n await Promise.all(\n [...issuePaths].map((path) =>\n assertWritableTarget(path, relativePath(plan.paths.repositoryRoot, path)),\n ),\n );\n return issuePaths;\n}\n\nasync function writeFiles(\n files: ReadonlyMap<string, RenderedFile>,\n writePaths: ReadonlySet<string>,\n repositoryRoot: string,\n written: string[],\n) {\n for (const [path, file] of [...files.entries()].sort(([left], [right]) =>\n left < right ? -1 : left > right ? 1 : 0,\n )) {\n if (!writePaths.has(path)) continue;\n await atomicWrite(\n path,\n file.content,\n relativePath(repositoryRoot, path),\n file.mode,\n );\n written.push(relativePath(repositoryRoot, path));\n }\n}\n\nexport async function checkContext(\n options: CheckContextOptions,\n): Promise<ContextCheckResult> {\n return comparePlan(await buildProjectionPlan(options));\n}\n\nexport async function syncContext(\n options: SyncContextOptions,\n): Promise<ContextSyncResult> {\n const plan = await buildProjectionPlan(options);\n const before = await comparePlan(plan);\n const writePaths = await preflightWrites(plan, before.issues);\n const written: string[] = [];\n const removed: string[] = [];\n\n await writeFiles(\n plan.rootFiles,\n writePaths,\n plan.paths.repositoryRoot,\n written,\n );\n await reconcileSkills(\n plan.paths.rootOutputs.skills,\n plan.rootSkills,\n plan.paths.repositoryRoot,\n written,\n removed,\n writePaths,\n );\n for (const prototype of plan.prototypes) {\n await writeFiles(\n prototype.files,\n writePaths,\n plan.paths.repositoryRoot,\n written,\n );\n }\n\n for (const issue of before.issues) {\n if (issue.kind !== \"unexpected\") continue;\n const path = join(plan.paths.repositoryRoot, issue.path);\n if (await removeManagedFile(path, issue.path)) removed.push(issue.path);\n }\n\n const after = await checkContext(options);\n return { ...after, written, removed };\n}\n"]}