@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bonfire contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # `@ebay-dt/bonfire-context`
2
+
3
+ Context projection and installation mechanics for Bonfire instances.
4
+
5
+ The package keeps instance-authored context in the consuming repository and
6
+ provides deterministic repository projections, freshness checks, lifecycle
7
+ destination projection, and explicit machine installation.
8
+
9
+ The public API and fixed source layout are documented in the Bonfire
10
+ repository's context projection plan.
11
+
12
+ Machine installation metadata is stored in a versioned, app-ID-keyed registry
13
+ under `~/.bonfire/context/registry.json`. The registry stores
14
+ source paths and hashes, never context contents or MCP definitions.
15
+
16
+ Use `sync --check` for a read-only repository projection check and
17
+ `install --check` for a read-only machine-state check.
@@ -0,0 +1,9 @@
1
+ import { type ContextPathOptions } from "./paths.js";
2
+ export type DiscoverPrototypeOptions = ContextPathOptions;
3
+ export declare class ContextDiscoveryError extends Error {
4
+ readonly path: string;
5
+ constructor(path: string, message: string);
6
+ }
7
+ export declare function discoverPrototypeDirectoryPaths(options: DiscoverPrototypeOptions): Promise<readonly string[]>;
8
+ export declare function discoverPrototypePaths(options: DiscoverPrototypeOptions): Promise<readonly string[]>;
9
+ //# sourceMappingURL=discover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,wBAAwB,GAAG,kBAAkB,CAAC;AAE1D,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxC;CACF;AAiDD,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAqC5B;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAS5B","sourcesContent":["import { lstat, readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n assertSafeContextPaths,\n PROTOTYPE_METADATA_FILE,\n resolveContextPaths,\n type ContextPathOptions,\n} from \"./paths.js\";\n\nexport type DiscoverPrototypeOptions = ContextPathOptions;\n\nexport class ContextDiscoveryError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to discover prototypes at ${path}: ${message}`);\n this.name = \"ContextDiscoveryError\";\n this.path = path;\n }\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\nasync function readDirectory(path: string, allowMissing: boolean) {\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (allowMissing && isNotFound(error)) return undefined;\n throw new ContextDiscoveryError(path, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextDiscoveryError(path, \"must not be a symbolic link\");\n }\n if (!stats.isDirectory()) {\n throw new ContextDiscoveryError(path, \"must be a directory\");\n }\n try {\n return await readdir(path, { withFileTypes: true });\n } catch {\n throw new ContextDiscoveryError(path, \"directory could not be enumerated\");\n }\n}\n\nasync function isMetadataFile(path: string) {\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) return false;\n throw new ContextDiscoveryError(path, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextDiscoveryError(path, \"must not be a symbolic link\");\n }\n if (!stats.isFile()) {\n throw new ContextDiscoveryError(path, \"must be a regular file\");\n }\n return true;\n}\n\nexport async function discoverPrototypeDirectoryPaths(\n options: DiscoverPrototypeOptions,\n): Promise<readonly string[]> {\n const paths = resolveContextPaths(options);\n assertSafeContextPaths(paths);\n const loginEntries = await readDirectory(paths.prototypeRoot, true);\n if (loginEntries === undefined) return [];\n\n const prototypes: string[] = [];\n for (const loginEntry of loginEntries.sort((left, right) =>\n left.name < right.name ? -1 : left.name > right.name ? 1 : 0,\n )) {\n if (loginEntry.name.startsWith(\"__\")) continue;\n const loginPath = join(paths.prototypeRoot, loginEntry.name);\n if (loginEntry.isSymbolicLink()) {\n throw new ContextDiscoveryError(loginPath, \"must not be a symbolic link\");\n }\n if (!loginEntry.isDirectory()) continue;\n const slugEntries = await readDirectory(loginPath, false);\n if (slugEntries === undefined) continue;\n\n for (const slugEntry of slugEntries.sort((left, right) =>\n left.name < right.name ? -1 : left.name > right.name ? 1 : 0,\n )) {\n if (slugEntry.name.startsWith(\"__\")) continue;\n const prototypePath = join(loginPath, slugEntry.name);\n if (slugEntry.isSymbolicLink()) {\n throw new ContextDiscoveryError(\n prototypePath,\n \"must not be a symbolic link\",\n );\n }\n if (!slugEntry.isDirectory()) continue;\n await readDirectory(prototypePath, false);\n prototypes.push(prototypePath);\n }\n }\n\n return prototypes;\n}\n\nexport async function discoverPrototypePaths(\n options: DiscoverPrototypeOptions,\n): Promise<readonly string[]> {\n const prototypeDirectories = await discoverPrototypeDirectoryPaths(options);\n const prototypes: string[] = [];\n for (const prototypePath of prototypeDirectories) {\n if (await isMetadataFile(join(prototypePath, PROTOTYPE_METADATA_FILE))) {\n prototypes.push(prototypePath);\n }\n }\n return prototypes;\n}\n"]}
@@ -0,0 +1,103 @@
1
+ import { lstat, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { assertSafeContextPaths, PROTOTYPE_METADATA_FILE, resolveContextPaths, } from "./paths.js";
4
+ export class ContextDiscoveryError extends Error {
5
+ path;
6
+ constructor(path, message) {
7
+ super(`Unable to discover prototypes at ${path}: ${message}`);
8
+ this.name = "ContextDiscoveryError";
9
+ this.path = path;
10
+ }
11
+ }
12
+ function isNotFound(error) {
13
+ return (error !== null &&
14
+ typeof error === "object" &&
15
+ "code" in error &&
16
+ error.code === "ENOENT");
17
+ }
18
+ async function readDirectory(path, allowMissing) {
19
+ let stats;
20
+ try {
21
+ stats = await lstat(path);
22
+ }
23
+ catch (error) {
24
+ if (allowMissing && isNotFound(error))
25
+ return undefined;
26
+ throw new ContextDiscoveryError(path, "could not be inspected safely");
27
+ }
28
+ if (stats.isSymbolicLink()) {
29
+ throw new ContextDiscoveryError(path, "must not be a symbolic link");
30
+ }
31
+ if (!stats.isDirectory()) {
32
+ throw new ContextDiscoveryError(path, "must be a directory");
33
+ }
34
+ try {
35
+ return await readdir(path, { withFileTypes: true });
36
+ }
37
+ catch {
38
+ throw new ContextDiscoveryError(path, "directory could not be enumerated");
39
+ }
40
+ }
41
+ async function isMetadataFile(path) {
42
+ let stats;
43
+ try {
44
+ stats = await lstat(path);
45
+ }
46
+ catch (error) {
47
+ if (isNotFound(error))
48
+ return false;
49
+ throw new ContextDiscoveryError(path, "could not be inspected safely");
50
+ }
51
+ if (stats.isSymbolicLink()) {
52
+ throw new ContextDiscoveryError(path, "must not be a symbolic link");
53
+ }
54
+ if (!stats.isFile()) {
55
+ throw new ContextDiscoveryError(path, "must be a regular file");
56
+ }
57
+ return true;
58
+ }
59
+ export async function discoverPrototypeDirectoryPaths(options) {
60
+ const paths = resolveContextPaths(options);
61
+ assertSafeContextPaths(paths);
62
+ const loginEntries = await readDirectory(paths.prototypeRoot, true);
63
+ if (loginEntries === undefined)
64
+ return [];
65
+ const prototypes = [];
66
+ for (const loginEntry of loginEntries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
67
+ if (loginEntry.name.startsWith("__"))
68
+ continue;
69
+ const loginPath = join(paths.prototypeRoot, loginEntry.name);
70
+ if (loginEntry.isSymbolicLink()) {
71
+ throw new ContextDiscoveryError(loginPath, "must not be a symbolic link");
72
+ }
73
+ if (!loginEntry.isDirectory())
74
+ continue;
75
+ const slugEntries = await readDirectory(loginPath, false);
76
+ if (slugEntries === undefined)
77
+ continue;
78
+ for (const slugEntry of slugEntries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
79
+ if (slugEntry.name.startsWith("__"))
80
+ continue;
81
+ const prototypePath = join(loginPath, slugEntry.name);
82
+ if (slugEntry.isSymbolicLink()) {
83
+ throw new ContextDiscoveryError(prototypePath, "must not be a symbolic link");
84
+ }
85
+ if (!slugEntry.isDirectory())
86
+ continue;
87
+ await readDirectory(prototypePath, false);
88
+ prototypes.push(prototypePath);
89
+ }
90
+ }
91
+ return prototypes;
92
+ }
93
+ export async function discoverPrototypePaths(options) {
94
+ const prototypeDirectories = await discoverPrototypeDirectoryPaths(options);
95
+ const prototypes = [];
96
+ for (const prototypePath of prototypeDirectories) {
97
+ if (await isMetadataFile(join(prototypePath, PROTOTYPE_METADATA_FILE))) {
98
+ prototypes.push(prototypePath);
99
+ }
100
+ }
101
+ return prototypes;
102
+ }
103
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n assertSafeContextPaths,\n PROTOTYPE_METADATA_FILE,\n resolveContextPaths,\n type ContextPathOptions,\n} from \"./paths.js\";\n\nexport type DiscoverPrototypeOptions = ContextPathOptions;\n\nexport class ContextDiscoveryError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to discover prototypes at ${path}: ${message}`);\n this.name = \"ContextDiscoveryError\";\n this.path = path;\n }\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\nasync function readDirectory(path: string, allowMissing: boolean) {\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (allowMissing && isNotFound(error)) return undefined;\n throw new ContextDiscoveryError(path, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextDiscoveryError(path, \"must not be a symbolic link\");\n }\n if (!stats.isDirectory()) {\n throw new ContextDiscoveryError(path, \"must be a directory\");\n }\n try {\n return await readdir(path, { withFileTypes: true });\n } catch {\n throw new ContextDiscoveryError(path, \"directory could not be enumerated\");\n }\n}\n\nasync function isMetadataFile(path: string) {\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) return false;\n throw new ContextDiscoveryError(path, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextDiscoveryError(path, \"must not be a symbolic link\");\n }\n if (!stats.isFile()) {\n throw new ContextDiscoveryError(path, \"must be a regular file\");\n }\n return true;\n}\n\nexport async function discoverPrototypeDirectoryPaths(\n options: DiscoverPrototypeOptions,\n): Promise<readonly string[]> {\n const paths = resolveContextPaths(options);\n assertSafeContextPaths(paths);\n const loginEntries = await readDirectory(paths.prototypeRoot, true);\n if (loginEntries === undefined) return [];\n\n const prototypes: string[] = [];\n for (const loginEntry of loginEntries.sort((left, right) =>\n left.name < right.name ? -1 : left.name > right.name ? 1 : 0,\n )) {\n if (loginEntry.name.startsWith(\"__\")) continue;\n const loginPath = join(paths.prototypeRoot, loginEntry.name);\n if (loginEntry.isSymbolicLink()) {\n throw new ContextDiscoveryError(loginPath, \"must not be a symbolic link\");\n }\n if (!loginEntry.isDirectory()) continue;\n const slugEntries = await readDirectory(loginPath, false);\n if (slugEntries === undefined) continue;\n\n for (const slugEntry of slugEntries.sort((left, right) =>\n left.name < right.name ? -1 : left.name > right.name ? 1 : 0,\n )) {\n if (slugEntry.name.startsWith(\"__\")) continue;\n const prototypePath = join(loginPath, slugEntry.name);\n if (slugEntry.isSymbolicLink()) {\n throw new ContextDiscoveryError(\n prototypePath,\n \"must not be a symbolic link\",\n );\n }\n if (!slugEntry.isDirectory()) continue;\n await readDirectory(prototypePath, false);\n prototypes.push(prototypePath);\n }\n }\n\n return prototypes;\n}\n\nexport async function discoverPrototypePaths(\n options: DiscoverPrototypeOptions,\n): Promise<readonly string[]> {\n const prototypeDirectories = await discoverPrototypeDirectoryPaths(options);\n const prototypes: string[] = [];\n for (const prototypePath of prototypeDirectories) {\n if (await isMetadataFile(join(prototypePath, PROTOTYPE_METADATA_FILE))) {\n prototypes.push(prototypePath);\n }\n }\n return prototypes;\n}\n"]}
@@ -0,0 +1,5 @@
1
+ export declare class ContextProjectionError extends Error {
2
+ readonly path: string;
3
+ constructor(path: string, message: string);
4
+ }
5
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxC;CACF","sourcesContent":["export class ContextProjectionError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to project context at ${path}: ${message}`);\n this.name = \"ContextProjectionError\";\n this.path = path;\n }\n}\n"]}
package/dist/errors.js ADDED
@@ -0,0 +1,9 @@
1
+ export class ContextProjectionError extends Error {
2
+ path;
3
+ constructor(path, message) {
4
+ super(`Unable to project context at ${path}: ${message}`);
5
+ this.name = "ContextProjectionError";
6
+ this.path = path;
7
+ }
8
+ }
9
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE,OAAe;QACvC,KAAK,CAAC,gCAAgC,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF","sourcesContent":["export class ContextProjectionError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to project context at ${path}: ${message}`);\n this.name = \"ContextProjectionError\";\n this.path = path;\n }\n}\n"]}
@@ -0,0 +1,10 @@
1
+ export { ContextProjectionError } from "./errors.js";
2
+ export { projectRootContext, type ProjectRootContextOptions, } from "./root-project.js";
3
+ export type { ContextProjectionResult } from "./types.js";
4
+ export { projectPrototypeContext, type ProjectPrototypeContextOptions, } from "./prototype-project.js";
5
+ export { checkContext, syncContext, type CheckContextOptions, type ContextCheckIssue, type ContextCheckResult, type ContextIssueKind, type ContextSyncResult, type SyncContextOptions, } from "./sync.js";
6
+ export { CONTEXT_REGISTRY_FILE, CONTEXT_REGISTRY_SCHEMA_VERSION, ContextRegistryError, createEmptyContextRegistry, readContextRegistry, resolveContextRegistryPaths, validateContextAppId, writeContextRegistry, type ContextRegistry, type ContextRegistryApp, type ContextRegistryLocationOptions, type ContextRegistryMcpServer, type ContextRegistryPaths, type ContextRegistrySkill, } from "./registry.js";
7
+ export { ContextInstallError, checkContextInstallation, installContext, type ContextInstallCheckIssue, type ContextInstallCheckResult, type ContextInstallOptions, type ContextInstallResult, type ContextSkillInstall, } from "./install.js";
8
+ export { ContextMcpError, type ContextMcpInstall } from "./mcp.js";
9
+ export { ContextMigrationError } from "./migration.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EACL,uBAAuB,EACvB,KAAK,8BAA8B,GACpC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,YAAY,EACZ,WAAW,EACX,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,+BAA+B,EAC/B,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACnB,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,8BAA8B,EACnC,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,GACzB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["export { ContextProjectionError } from \"./errors.js\";\nexport {\n projectRootContext,\n type ProjectRootContextOptions,\n} from \"./root-project.js\";\nexport type { ContextProjectionResult } from \"./types.js\";\nexport {\n projectPrototypeContext,\n type ProjectPrototypeContextOptions,\n} from \"./prototype-project.js\";\nexport {\n checkContext,\n syncContext,\n type CheckContextOptions,\n type ContextCheckIssue,\n type ContextCheckResult,\n type ContextIssueKind,\n type ContextSyncResult,\n type SyncContextOptions,\n} from \"./sync.js\";\nexport {\n CONTEXT_REGISTRY_FILE,\n CONTEXT_REGISTRY_SCHEMA_VERSION,\n ContextRegistryError,\n createEmptyContextRegistry,\n readContextRegistry,\n resolveContextRegistryPaths,\n validateContextAppId,\n writeContextRegistry,\n type ContextRegistry,\n type ContextRegistryApp,\n type ContextRegistryLocationOptions,\n type ContextRegistryMcpServer,\n type ContextRegistryPaths,\n type ContextRegistrySkill,\n} from \"./registry.js\";\nexport {\n ContextInstallError,\n checkContextInstallation,\n installContext,\n type ContextInstallCheckIssue,\n type ContextInstallCheckResult,\n type ContextInstallOptions,\n type ContextInstallResult,\n type ContextSkillInstall,\n} from \"./install.js\";\nexport { ContextMcpError, type ContextMcpInstall } from \"./mcp.js\";\nexport { ContextMigrationError } from \"./migration.js\";\n"]}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { ContextProjectionError } from "./errors.js";
2
+ export { projectRootContext, } from "./root-project.js";
3
+ export { projectPrototypeContext, } from "./prototype-project.js";
4
+ export { checkContext, syncContext, } from "./sync.js";
5
+ export { CONTEXT_REGISTRY_FILE, CONTEXT_REGISTRY_SCHEMA_VERSION, ContextRegistryError, createEmptyContextRegistry, readContextRegistry, resolveContextRegistryPaths, validateContextAppId, writeContextRegistry, } from "./registry.js";
6
+ export { ContextInstallError, checkContextInstallation, installContext, } from "./install.js";
7
+ export { ContextMcpError } from "./mcp.js";
8
+ export { ContextMigrationError } from "./migration.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,uBAAuB,GAExB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,YAAY,EACZ,WAAW,GAOZ,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,+BAA+B,EAC/B,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACnB,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,GAOrB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,GAMf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAA0B,MAAM,UAAU,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["export { ContextProjectionError } from \"./errors.js\";\nexport {\n projectRootContext,\n type ProjectRootContextOptions,\n} from \"./root-project.js\";\nexport type { ContextProjectionResult } from \"./types.js\";\nexport {\n projectPrototypeContext,\n type ProjectPrototypeContextOptions,\n} from \"./prototype-project.js\";\nexport {\n checkContext,\n syncContext,\n type CheckContextOptions,\n type ContextCheckIssue,\n type ContextCheckResult,\n type ContextIssueKind,\n type ContextSyncResult,\n type SyncContextOptions,\n} from \"./sync.js\";\nexport {\n CONTEXT_REGISTRY_FILE,\n CONTEXT_REGISTRY_SCHEMA_VERSION,\n ContextRegistryError,\n createEmptyContextRegistry,\n readContextRegistry,\n resolveContextRegistryPaths,\n validateContextAppId,\n writeContextRegistry,\n type ContextRegistry,\n type ContextRegistryApp,\n type ContextRegistryLocationOptions,\n type ContextRegistryMcpServer,\n type ContextRegistryPaths,\n type ContextRegistrySkill,\n} from \"./registry.js\";\nexport {\n ContextInstallError,\n checkContextInstallation,\n installContext,\n type ContextInstallCheckIssue,\n type ContextInstallCheckResult,\n type ContextInstallOptions,\n type ContextInstallResult,\n type ContextSkillInstall,\n} from \"./install.js\";\nexport { ContextMcpError, type ContextMcpInstall } from \"./mcp.js\";\nexport { ContextMigrationError } from \"./migration.js\";\n"]}
@@ -0,0 +1,33 @@
1
+ import { type ContextRegistryLocationOptions } from "./registry.js";
2
+ import { type ContextPathOptions } from "./paths.js";
3
+ import { type ContextMcpInstall } from "./mcp.js";
4
+ export type ContextInstallOptions = ContextPathOptions & Readonly<{
5
+ appId: string;
6
+ machineHomeDirectory?: string;
7
+ registryLocation?: ContextRegistryLocationOptions;
8
+ }>;
9
+ export type ContextSkillInstall = Readonly<{
10
+ name: string;
11
+ path: string;
12
+ status: "linked" | "adopted" | "repaired";
13
+ }>;
14
+ export type ContextInstallResult = Readonly<{
15
+ appId: string;
16
+ skills: readonly ContextSkillInstall[];
17
+ mcp: readonly ContextMcpInstall[];
18
+ }>;
19
+ export type ContextInstallCheckIssue = Readonly<{
20
+ kind: "missing" | "stale" | "conflict";
21
+ path: string;
22
+ }>;
23
+ export type ContextInstallCheckResult = Readonly<{
24
+ ok: boolean;
25
+ issues: readonly ContextInstallCheckIssue[];
26
+ }>;
27
+ export declare class ContextInstallError extends Error {
28
+ readonly path: string;
29
+ constructor(path: string, message: string);
30
+ }
31
+ export declare function checkContextInstallation(options: ContextInstallOptions): Promise<ContextInstallCheckResult>;
32
+ export declare function installContext(options: ContextInstallOptions): Promise<ContextInstallResult>;
33
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,8BAA8B,EAMpC,MAAM,eAAe,CAAC;AACvB,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,YAAY,CAAC;AACpB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,UAAU,CAAC;AAgBlB,MAAM,MAAM,qBAAqB,GAAG,kBAAkB,GACpD,QAAQ,CAAC;IACP,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;CACnD,CAAC,CAAC;AAEL,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;CAC3C,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACvC,GAAG,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACnC,CAAC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC9C,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,UAAU,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC,CAAC;AAEH,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,SAAS,wBAAwB,EAAE,CAAC;CAC7C,CAAC,CAAC;AAEH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxC;CACF;AAoRD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,yBAAyB,CAAC,CAsJpC;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAmH/B","sourcesContent":["import { createHash } from \"node:crypto\";\nimport {\n lstat,\n mkdir,\n readFile,\n readdir,\n realpath,\n symlink,\n unlink,\n} from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport {\n type ContextRegistry,\n type ContextRegistryLocationOptions,\n type ContextRegistryMcpServer,\n readContextRegistry,\n resolveContextRegistryPaths,\n validateContextAppId,\n writeContextRegistry,\n} from \"./registry.js\";\nimport {\n resolveContextPaths,\n assertSafeContextPaths,\n type ContextPathOptions,\n} from \"./paths.js\";\nimport {\n applyMcpInstallation,\n planMcpInstallation,\n rollbackMcpInstallation,\n type ContextMcpInstall,\n} from \"./mcp.js\";\nimport {\n planLegacyMigration,\n removeLegacyManifest,\n rollbackLegacyMigration,\n type LegacyMigrationPlan,\n} from \"./migration.js\";\nimport { renderSkillFiles, type RenderedSkillFile } from \"./skills.js\";\n\nconst MACHINE_SKILLS_DIRECTORY = join(\".claude\", \"skills\");\n\ntype SkillFile = Readonly<{\n relativePath: string;\n content: Buffer;\n}>;\n\nexport type ContextInstallOptions = ContextPathOptions &\n Readonly<{\n appId: string;\n machineHomeDirectory?: string;\n registryLocation?: ContextRegistryLocationOptions;\n }>;\n\nexport type ContextSkillInstall = Readonly<{\n name: string;\n path: string;\n status: \"linked\" | \"adopted\" | \"repaired\";\n}>;\n\nexport type ContextInstallResult = Readonly<{\n appId: string;\n skills: readonly ContextSkillInstall[];\n mcp: readonly ContextMcpInstall[];\n}>;\n\nexport type ContextInstallCheckIssue = Readonly<{\n kind: \"missing\" | \"stale\" | \"conflict\";\n path: string;\n}>;\n\nexport type ContextInstallCheckResult = Readonly<{\n ok: boolean;\n issues: readonly ContextInstallCheckIssue[];\n}>;\n\nexport class ContextInstallError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to install context at ${path}: ${message}`);\n this.name = \"ContextInstallError\";\n this.path = path;\n }\n}\n\nfunction compareFiles(left: SkillFile, right: SkillFile) {\n return left.relativePath < right.relativePath\n ? -1\n : left.relativePath > right.relativePath\n ? 1\n : 0;\n}\n\nfunction hashFiles(files: readonly SkillFile[]) {\n const hash = createHash(\"sha256\");\n for (const file of [...files].sort(compareFiles)) {\n hash.update(file.relativePath);\n hash.update(\"\\0\");\n hash.update(file.content);\n hash.update(\"\\0\");\n }\n return hash.digest(\"hex\");\n}\n\nfunction sourceSkillFiles(\n name: string,\n files: readonly RenderedSkillFile[],\n): SkillFile[] {\n const prefix = `${name}/`;\n return files\n .filter((file) => file.relativePath.startsWith(prefix))\n .map((file) => ({\n relativePath: file.relativePath.slice(prefix.length),\n content: file.content,\n }));\n}\n\nasync function readSkillFiles(\n root: string,\n relativeDirectory = \"\",\n): Promise<SkillFile[]> {\n let entries;\n try {\n entries = await readdir(root, { withFileTypes: true });\n } catch {\n throw new ContextInstallError(root, \"skill directory could not be read\");\n }\n\n const files: SkillFile[] = [];\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 ContextInstallError(path, \"skill entry could not be inspected\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextInstallError(\n path,\n \"skill content must not be a symbolic link\",\n );\n }\n if (stats.isDirectory()) {\n files.push(...(await readSkillFiles(path, relativePath)));\n } else if (stats.isFile()) {\n try {\n files.push({ relativePath, content: await readFile(path) });\n } catch {\n throw new ContextInstallError(path, \"skill file could not be read\");\n }\n } else {\n throw new ContextInstallError(\n path,\n \"skill content must be a regular file or directory\",\n );\n }\n }\n return files;\n}\n\nasync function assertNoSymlinkAncestors(path: string) {\n let current = resolve(path);\n while (true) {\n try {\n const stats = await lstat(current);\n if (stats.isSymbolicLink()) {\n throw new ContextInstallError(\n current,\n \"must not pass through a symbolic link\",\n );\n }\n } catch (error) {\n if (error instanceof ContextInstallError) throw error;\n if (!isNotFound(error)) {\n throw new ContextInstallError(current, \"could not be inspected safely\");\n }\n }\n const parent = dirname(current);\n if (parent === current) return;\n current = parent;\n }\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\nasync function resolveMachineHomeDirectory(options: ContextInstallOptions) {\n const configured = resolve(\n options.machineHomeDirectory ??\n options.registryLocation?.homeDirectory ??\n homedir(),\n );\n try {\n return await realpath(configured);\n } catch {\n throw new ContextInstallError(\n configured,\n \"machine home directory could not be resolved safely\",\n );\n }\n}\n\nasync function ensureMachineSkillsDirectory(path: string) {\n await assertNoSymlinkAncestors(path);\n try {\n await mkdir(path, { recursive: true, mode: 0o700 });\n const stats = await lstat(path);\n if (stats.isSymbolicLink() || !stats.isDirectory()) {\n throw new ContextInstallError(path, \"must be a regular directory\");\n }\n } catch (error) {\n if (error instanceof ContextInstallError) throw error;\n throw new ContextInstallError(path, \"could not be created safely\");\n }\n}\n\nasync function targetStatus(\n target: string,\n source: string,\n expectedHash: string,\n previousSource: string | undefined,\n legacySource: string | undefined,\n): Promise<\"linked\" | \"adopted\" | \"repaired\"> {\n let stats;\n try {\n stats = await lstat(target);\n } catch (error) {\n if (isNotFound(error)) return \"linked\";\n throw new ContextInstallError(\n target,\n \"existing skill could not be inspected\",\n );\n }\n\n if (stats.isSymbolicLink()) {\n let linkedPath: string;\n try {\n linkedPath = await realpath(target);\n } catch {\n if (previousSource === source) return \"repaired\";\n throw new ContextInstallError(\n target,\n \"broken skill link is not owned by this app\",\n );\n }\n const resolvedSource = await realpath(source);\n if (linkedPath === resolvedSource) return \"adopted\";\n if (legacySource !== undefined) {\n const resolvedLegacySource = await realpath(legacySource);\n if (linkedPath === resolvedLegacySource) return \"repaired\";\n }\n\n let linkedHash: string;\n try {\n linkedHash = hashFiles(await readSkillFiles(linkedPath));\n } catch {\n throw new ContextInstallError(\n target,\n \"existing skill link could not be compared safely\",\n );\n }\n if (linkedHash === expectedHash) return \"adopted\";\n throw new ContextInstallError(target, \"conflicts with an existing skill\");\n }\n\n if (!stats.isDirectory()) {\n throw new ContextInstallError(target, \"conflicts with an existing file\");\n }\n let existingHash: string;\n try {\n existingHash = hashFiles(await readSkillFiles(target));\n } catch {\n throw new ContextInstallError(\n target,\n \"existing skill could not be compared safely\",\n );\n }\n if (existingHash === expectedHash) return \"adopted\";\n throw new ContextInstallError(target, \"conflicts with an existing skill\");\n}\n\nasync function linkSkill(\n target: string,\n source: string,\n status: \"linked\" | \"adopted\" | \"repaired\",\n) {\n if (status === \"adopted\") return;\n if (status === \"repaired\") {\n try {\n await unlink(target);\n } catch {\n throw new ContextInstallError(\n target,\n \"owned broken skill link could not be repaired\",\n );\n }\n }\n try {\n await symlink(source, target, \"dir\");\n } catch {\n throw new ContextInstallError(target, \"skill link could not be created\");\n }\n}\n\nfunction registryForSkills(\n registry: ContextRegistry,\n options: {\n appId: string;\n repositoryRoot: string;\n skills: Readonly<\n Record<string, { sourcePath: string; contentHash: string }>\n >;\n mcp: Readonly<Record<string, ContextRegistryMcpServer>>;\n },\n): ContextRegistry {\n const previous = registry.apps[options.appId];\n return {\n ...registry,\n apps: {\n ...registry.apps,\n [options.appId]: {\n repositoryRoot: options.repositoryRoot,\n skills: {\n ...(previous?.skills ?? {}),\n ...options.skills,\n },\n mcp: {\n ...(previous?.mcp ?? {}),\n ...options.mcp,\n },\n },\n },\n };\n}\n\nfunction errorPath(error: unknown, fallback: string) {\n if (\n error !== null &&\n typeof error === \"object\" &&\n \"path\" in error &&\n typeof error.path === \"string\"\n ) {\n return error.path;\n }\n return fallback;\n}\n\nexport async function checkContextInstallation(\n options: ContextInstallOptions,\n): Promise<ContextInstallCheckResult> {\n const appId = validateContextAppId(options.appId);\n const paths = resolveContextPaths(options);\n assertSafeContextPaths(paths);\n const renderedFiles = await renderSkillFiles(paths);\n const skillNames = [\n ...new Set(\n renderedFiles.map((file) => {\n const separator = file.relativePath.indexOf(\"/\");\n return separator === -1\n ? file.relativePath\n : file.relativePath.slice(0, separator);\n }),\n ),\n ].sort();\n const machineHomeDirectory = await resolveMachineHomeDirectory(options);\n const machineSkillsRoot = join(\n machineHomeDirectory,\n MACHINE_SKILLS_DIRECTORY,\n );\n const registryOptions = options.registryLocation ?? {\n homeDirectory: machineHomeDirectory,\n };\n const registry = await readContextRegistry(registryOptions);\n const registryPath = resolveContextRegistryPaths(registryOptions).registry;\n const app = registry.apps[appId];\n let registryPresent = true;\n try {\n await lstat(registryPath);\n } catch (error) {\n if (isNotFound(error)) registryPresent = false;\n else throw error;\n }\n let registryNeedsUpdate = app === undefined;\n const issues: ContextInstallCheckIssue[] = [];\n const issuePaths = new Set<string>();\n const addIssue = (kind: ContextInstallCheckIssue[\"kind\"], path: string) => {\n if (issuePaths.has(path)) return;\n issuePaths.add(path);\n issues.push({ kind, path });\n };\n\n let machineSkillsPresent = true;\n try {\n const stats = await lstat(machineSkillsRoot);\n if (stats.isSymbolicLink() || !stats.isDirectory()) {\n addIssue(\"conflict\", machineSkillsRoot);\n machineSkillsPresent = false;\n }\n } catch (error) {\n if (isNotFound(error)) machineSkillsPresent = false;\n else addIssue(\"conflict\", machineSkillsRoot);\n }\n\n for (const name of skillNames) {\n const files = sourceSkillFiles(name, renderedFiles);\n const sourcePath = join(paths.sources.skills, name);\n const contentHash = hashFiles(files);\n const target = join(machineSkillsRoot, name);\n let targetOkay = false;\n if (machineSkillsPresent) {\n try {\n const stats = await lstat(target);\n if (stats.isSymbolicLink()) {\n try {\n targetOkay =\n (await realpath(target)) === (await realpath(sourcePath));\n } catch {\n addIssue(\"stale\", target);\n }\n if (!targetOkay) {\n try {\n targetOkay =\n hashFiles(await readSkillFiles(await realpath(target))) ===\n contentHash;\n } catch {\n addIssue(\"conflict\", target);\n }\n if (targetOkay) addIssue(\"stale\", target);\n }\n } else if (stats.isDirectory()) {\n try {\n targetOkay =\n hashFiles(await readSkillFiles(target)) === contentHash;\n } catch {\n addIssue(\"conflict\", target);\n }\n if (targetOkay) addIssue(\"stale\", target);\n } else {\n addIssue(\"conflict\", target);\n }\n } catch (error) {\n if (isNotFound(error)) addIssue(\"missing\", target);\n else addIssue(\"conflict\", target);\n }\n } else {\n addIssue(\"missing\", target);\n }\n\n const record = app?.skills[name];\n if (\n record === undefined ||\n record.sourcePath !== sourcePath ||\n record.contentHash !== contentHash\n ) {\n if (record !== undefined) addIssue(\"stale\", target);\n registryNeedsUpdate = true;\n }\n }\n\n let mcpPlan;\n try {\n mcpPlan = await planMcpInstallation(paths, machineHomeDirectory);\n for (const installation of mcpPlan.installations) {\n if (installation.status === \"added\") {\n addIssue(\"missing\", `${installation.path}#${installation.name}`);\n }\n }\n for (const [name, record] of Object.entries(mcpPlan.records)) {\n const registered = app?.mcp[name];\n if (\n registered === undefined ||\n registered.sourcePath !== record.sourcePath ||\n registered.normalizedDefinitionHash !== record.normalizedDefinitionHash\n ) {\n registryNeedsUpdate = true;\n }\n }\n } catch (error) {\n addIssue(\"conflict\", errorPath(error, machineHomeDirectory));\n }\n\n if (machineSkillsPresent) {\n try {\n const migration = await planLegacyMigration({\n repositoryRoot: paths.repositoryRoot,\n machineSkillsRoot,\n canonicalSkillsRoot: paths.sources.skills,\n skillNames,\n });\n if (migration !== undefined) addIssue(\"stale\", migration.manifestPath);\n } catch (error) {\n addIssue(\"conflict\", errorPath(error, machineSkillsRoot));\n }\n }\n\n if (registryNeedsUpdate) {\n addIssue(registryPresent ? \"stale\" : \"missing\", registryPath);\n }\n return { ok: issues.length === 0, issues };\n}\n\nexport async function installContext(\n options: ContextInstallOptions,\n): Promise<ContextInstallResult> {\n const appId = validateContextAppId(options.appId);\n const paths = resolveContextPaths(options);\n assertSafeContextPaths(paths);\n const renderedFiles = await renderSkillFiles(paths);\n const skillNames = [\n ...new Set(\n renderedFiles.map((file) => {\n const separator = file.relativePath.indexOf(\"/\");\n return separator === -1\n ? file.relativePath\n : file.relativePath.slice(0, separator);\n }),\n ),\n ].sort();\n const machineHomeDirectory = await resolveMachineHomeDirectory(options);\n const machineSkillsRoot = join(\n machineHomeDirectory,\n MACHINE_SKILLS_DIRECTORY,\n );\n const mcpPlan = await planMcpInstallation(paths, machineHomeDirectory);\n await ensureMachineSkillsDirectory(machineSkillsRoot);\n\n const registryOptions = options.registryLocation ?? {\n homeDirectory: machineHomeDirectory,\n };\n const registry = await readContextRegistry(registryOptions);\n const previous = registry.apps[appId];\n const legacyMigration: LegacyMigrationPlan | undefined =\n await planLegacyMigration({\n repositoryRoot: paths.repositoryRoot,\n machineSkillsRoot,\n canonicalSkillsRoot: paths.sources.skills,\n skillNames,\n });\n const legacySources = new Map(\n legacyMigration?.repoints.map((repoint) => [\n repoint.name,\n repoint.legacySource,\n ]),\n );\n const plans = [] as Array<{\n name: string;\n sourcePath: string;\n contentHash: string;\n target: string;\n status: \"linked\" | \"adopted\" | \"repaired\";\n }>;\n\n for (const name of skillNames) {\n const files = sourceSkillFiles(name, renderedFiles);\n const sourcePath = join(paths.sources.skills, name);\n const contentHash = hashFiles(files);\n const target = join(machineSkillsRoot, name);\n const status = await targetStatus(\n target,\n sourcePath,\n contentHash,\n previous?.skills[name]?.sourcePath,\n legacySources.get(name),\n );\n plans.push({ name, sourcePath, contentHash, target, status });\n }\n\n const created: string[] = [];\n let mcpApplied = false;\n try {\n for (const plan of plans) {\n await linkSkill(plan.target, plan.sourcePath, plan.status);\n if (plan.status !== \"adopted\") created.push(plan.target);\n }\n await applyMcpInstallation(mcpPlan);\n mcpApplied = mcpPlan.content !== undefined;\n const nextRegistry = registryForSkills(registry, {\n appId,\n repositoryRoot: paths.repositoryRoot,\n skills: Object.fromEntries(\n plans.map((plan) => [\n plan.name,\n {\n sourcePath: plan.sourcePath,\n contentHash: plan.contentHash,\n },\n ]),\n ),\n mcp: mcpPlan.records,\n });\n await writeContextRegistry(nextRegistry, registryOptions);\n if (legacyMigration !== undefined) {\n await removeLegacyManifest(legacyMigration);\n }\n } catch (error) {\n if (mcpApplied) {\n await rollbackMcpInstallation(mcpPlan).catch(() => undefined);\n }\n await Promise.all(\n created.map(async (path) => {\n await unlink(path).catch(() => undefined);\n }),\n );\n if (legacyMigration !== undefined) {\n await rollbackLegacyMigration(legacyMigration).catch(() => undefined);\n }\n throw error;\n }\n\n return {\n appId,\n skills: plans.map(({ name, target: path, status }) => ({\n name,\n path,\n status,\n })),\n mcp: mcpPlan.installations,\n };\n}\n"]}