@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filesystem.js","sourceRoot":"","sources":["../../src/internal/filesystem.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { lstat, mkdir, rename, rm, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { ContextProjectionError } from \"../errors.js\";\n\nexport function 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 ensureDirectory(path: string, label: string) {\n try {\n await mkdir(path, { recursive: true });\n const stats = await lstat(path);\n if (stats.isSymbolicLink() || !stats.isDirectory()) {\n throw new ContextProjectionError(label, \"must be a regular directory\");\n }\n } catch (error) {\n if (error instanceof ContextProjectionError) throw error;\n throw new ContextProjectionError(label, \"could not be created safely\");\n }\n}\n\nexport async function assertWritableTarget(path: string, label: string) {\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) return;\n throw new ContextProjectionError(label, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextProjectionError(label, \"must not be a symbolic link\");\n }\n if (!stats.isFile()) {\n throw new ContextProjectionError(label, \"must be a regular file\");\n }\n}\n\nexport async function atomicWrite(\n path: string,\n content: string | Uint8Array,\n label: string,\n mode = 0o644,\n) {\n const parent = dirname(path);\n await ensureDirectory(parent, `${label} parent`);\n await assertWritableTarget(path, label);\n const temporaryPath = `${path}.${randomUUID()}.tmp`;\n\n try {\n await writeFile(temporaryPath, content, { mode, flag: \"wx\" });\n await rename(temporaryPath, path);\n } catch {\n throw new ContextProjectionError(label, \"could not be written safely\");\n } finally {\n await rm(temporaryPath, { force: true }).catch(() => undefined);\n }\n}\n\nexport async function removeManagedFile(path: string, label: string) {\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) return false;\n throw new ContextProjectionError(label, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink()) {\n throw new ContextProjectionError(label, \"must not be a symbolic link\");\n }\n if (!stats.isFile()) {\n throw new ContextProjectionError(label, \"must be a regular file\");\n }\n try {\n await rm(path);\n } catch {\n throw new ContextProjectionError(label, \"could not be removed safely\");\n }\n return true;\n}\n"]}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { ResolvedContextPaths } from "./paths.js";
2
+ type McpRecord = Readonly<{
3
+ sourcePath: string;
4
+ normalizedDefinitionHash: string;
5
+ }>;
6
+ export type ContextMcpInstall = Readonly<{
7
+ name: string;
8
+ path: string;
9
+ status: "added" | "adopted";
10
+ }>;
11
+ export type ContextMcpPlan = Readonly<{
12
+ path: string;
13
+ content: string | undefined;
14
+ originalContent: string | undefined;
15
+ records: Readonly<Record<string, McpRecord>>;
16
+ installations: readonly ContextMcpInstall[];
17
+ }>;
18
+ export declare class ContextMcpError extends Error {
19
+ readonly path: string;
20
+ constructor(path: string, message: string);
21
+ }
22
+ export declare function planMcpInstallation(paths: ResolvedContextPaths, machineHomeDirectory: string): Promise<ContextMcpPlan>;
23
+ export declare function applyMcpInstallation(plan: ContextMcpPlan): Promise<void>;
24
+ export declare function rollbackMcpInstallation(plan: ContextMcpPlan): Promise<void>;
25
+ export {};
26
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAOvD,KAAK,SAAS,GAAG,QAAQ,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,wBAAwB,EAAE,MAAM,CAAC;CAClC,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B,CAAC,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAC7C,aAAa,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC7C,CAAC,CAAC;AAEH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxC;CACF;AAqFD,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,oBAAoB,EAC3B,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CAAC,cAAc,CAAC,CA0EzB;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,cAAc,iBAG9D;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,cAAc,iBAYjE","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { lstat, readFile, unlink } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { atomicWrite, isNotFound } from \"./internal/filesystem.js\";\nimport { renderOptionalMcp } from \"./template.js\";\nimport type { ResolvedContextPaths } from \"./paths.js\";\n\nconst MACHINE_MCP_DIRECTORY = \".cursor\";\nconst MACHINE_MCP_FILE = \"mcp.json\";\n\ntype JsonObject = Record<string, unknown>;\n\ntype McpRecord = Readonly<{\n sourcePath: string;\n normalizedDefinitionHash: string;\n}>;\n\nexport type ContextMcpInstall = Readonly<{\n name: string;\n path: string;\n status: \"added\" | \"adopted\";\n}>;\n\nexport type ContextMcpPlan = Readonly<{\n path: string;\n content: string | undefined;\n originalContent: string | undefined;\n records: Readonly<Record<string, McpRecord>>;\n installations: readonly ContextMcpInstall[];\n}>;\n\nexport class ContextMcpError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to install MCP context at ${path}: ${message}`);\n this.name = \"ContextMcpError\";\n this.path = path;\n }\n}\n\nfunction isObject(value: unknown): value is JsonObject {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalize);\n if (!isObject(value)) return value;\n return Object.fromEntries(\n Object.entries(value)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, entry]) => [key, canonicalize(entry)]),\n );\n}\n\nfunction definitionHash(value: unknown) {\n return createHash(\"sha256\")\n .update(JSON.stringify(canonicalize(value)))\n .digest(\"hex\");\n}\n\nfunction parseObject(source: string, path: string): JsonObject {\n let value: unknown;\n try {\n value = JSON.parse(source);\n } catch {\n throw new ContextMcpError(path, \"must contain valid JSON\");\n }\n if (!isObject(value)) {\n throw new ContextMcpError(path, \"must contain a JSON object\");\n }\n return value;\n}\n\nfunction serverMap(value: unknown, path: string) {\n if (value === undefined) return {};\n if (!isObject(value)) {\n throw new ContextMcpError(path, \"mcpServers must be a JSON object\");\n }\n return value;\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 ContextMcpError(\n current,\n \"must not pass through a symbolic link\",\n );\n }\n } catch (error) {\n if (error instanceof ContextMcpError) throw error;\n if (!isNotFound(error)) {\n throw new ContextMcpError(current, \"could not be inspected safely\");\n }\n }\n const parent = dirname(current);\n if (parent === current) return;\n current = parent;\n }\n}\n\nasync function readExistingMcp(path: string) {\n await assertNoSymlinkAncestors(path);\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw new ContextMcpError(path, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink() || !stats.isFile()) {\n throw new ContextMcpError(path, \"must be a regular file\");\n }\n try {\n return await readFile(path, \"utf8\");\n } catch {\n throw new ContextMcpError(path, \"could not be read safely\");\n }\n}\n\nexport async function planMcpInstallation(\n paths: ResolvedContextPaths,\n machineHomeDirectory: string,\n): Promise<ContextMcpPlan> {\n const path = join(\n machineHomeDirectory,\n MACHINE_MCP_DIRECTORY,\n MACHINE_MCP_FILE,\n );\n const rendered = await renderOptionalMcp(paths);\n const originalContent = await readExistingMcp(path);\n if (rendered === undefined) {\n return {\n path,\n content: undefined,\n originalContent,\n records: {},\n installations: [],\n };\n }\n\n const source = parseObject(rendered, paths.sources.mcp);\n const sourceServers = serverMap(source.mcpServers, paths.sources.mcp);\n const existing =\n originalContent === undefined ? {} : parseObject(originalContent, path);\n const existingServers = serverMap(existing.mcpServers, path);\n const mergedServers: JsonObject = { ...existingServers };\n const records: Record<string, McpRecord> = {};\n const installations: ContextMcpInstall[] = [];\n let changed = originalContent === undefined;\n\n for (const [name, definition] of Object.entries(sourceServers).sort(\n ([left], [right]) => left.localeCompare(right),\n )) {\n if (name.length === 0) {\n throw new ContextMcpError(\n paths.sources.mcp,\n \"server names must not be empty\",\n );\n }\n const hash = definitionHash(definition);\n records[name] = {\n sourcePath: paths.sources.mcp,\n normalizedDefinitionHash: hash,\n };\n const existingDefinition = existingServers[name];\n if (existingDefinition === undefined) {\n mergedServers[name] = definition;\n installations.push({ name, path, status: \"added\" });\n changed = true;\n continue;\n }\n if (definitionHash(existingDefinition) !== hash) {\n throw new ContextMcpError(\n path,\n \"conflicts with an existing MCP server definition\",\n );\n }\n installations.push({ name, path, status: \"adopted\" });\n }\n\n if (!changed) {\n return {\n path,\n content: undefined,\n originalContent,\n records,\n installations,\n };\n }\n return {\n path,\n content: `${JSON.stringify({ ...existing, mcpServers: mergedServers }, null, 2)}\\n`,\n originalContent,\n records,\n installations,\n };\n}\n\nexport async function applyMcpInstallation(plan: ContextMcpPlan) {\n if (plan.content === undefined) return;\n await atomicWrite(plan.path, plan.content, plan.path, 0o600);\n}\n\nexport async function rollbackMcpInstallation(plan: ContextMcpPlan) {\n if (plan.content === undefined) return;\n if (plan.originalContent === undefined) {\n try {\n const stats = await lstat(plan.path);\n if (!stats.isSymbolicLink() && stats.isFile()) await unlink(plan.path);\n } catch (error) {\n if (!isNotFound(error)) throw error;\n }\n return;\n }\n await atomicWrite(plan.path, plan.originalContent, plan.path, 0o600);\n}\n"]}
package/dist/mcp.js ADDED
@@ -0,0 +1,178 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, unlink } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { atomicWrite, isNotFound } from "./internal/filesystem.js";
5
+ import { renderOptionalMcp } from "./template.js";
6
+ const MACHINE_MCP_DIRECTORY = ".cursor";
7
+ const MACHINE_MCP_FILE = "mcp.json";
8
+ export class ContextMcpError extends Error {
9
+ path;
10
+ constructor(path, message) {
11
+ super(`Unable to install MCP context at ${path}: ${message}`);
12
+ this.name = "ContextMcpError";
13
+ this.path = path;
14
+ }
15
+ }
16
+ function isObject(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ function canonicalize(value) {
20
+ if (Array.isArray(value))
21
+ return value.map(canonicalize);
22
+ if (!isObject(value))
23
+ return value;
24
+ return Object.fromEntries(Object.entries(value)
25
+ .sort(([left], [right]) => left.localeCompare(right))
26
+ .map(([key, entry]) => [key, canonicalize(entry)]));
27
+ }
28
+ function definitionHash(value) {
29
+ return createHash("sha256")
30
+ .update(JSON.stringify(canonicalize(value)))
31
+ .digest("hex");
32
+ }
33
+ function parseObject(source, path) {
34
+ let value;
35
+ try {
36
+ value = JSON.parse(source);
37
+ }
38
+ catch {
39
+ throw new ContextMcpError(path, "must contain valid JSON");
40
+ }
41
+ if (!isObject(value)) {
42
+ throw new ContextMcpError(path, "must contain a JSON object");
43
+ }
44
+ return value;
45
+ }
46
+ function serverMap(value, path) {
47
+ if (value === undefined)
48
+ return {};
49
+ if (!isObject(value)) {
50
+ throw new ContextMcpError(path, "mcpServers must be a JSON object");
51
+ }
52
+ return value;
53
+ }
54
+ async function assertNoSymlinkAncestors(path) {
55
+ let current = resolve(path);
56
+ while (true) {
57
+ try {
58
+ const stats = await lstat(current);
59
+ if (stats.isSymbolicLink()) {
60
+ throw new ContextMcpError(current, "must not pass through a symbolic link");
61
+ }
62
+ }
63
+ catch (error) {
64
+ if (error instanceof ContextMcpError)
65
+ throw error;
66
+ if (!isNotFound(error)) {
67
+ throw new ContextMcpError(current, "could not be inspected safely");
68
+ }
69
+ }
70
+ const parent = dirname(current);
71
+ if (parent === current)
72
+ return;
73
+ current = parent;
74
+ }
75
+ }
76
+ async function readExistingMcp(path) {
77
+ await assertNoSymlinkAncestors(path);
78
+ let stats;
79
+ try {
80
+ stats = await lstat(path);
81
+ }
82
+ catch (error) {
83
+ if (isNotFound(error))
84
+ return undefined;
85
+ throw new ContextMcpError(path, "could not be inspected safely");
86
+ }
87
+ if (stats.isSymbolicLink() || !stats.isFile()) {
88
+ throw new ContextMcpError(path, "must be a regular file");
89
+ }
90
+ try {
91
+ return await readFile(path, "utf8");
92
+ }
93
+ catch {
94
+ throw new ContextMcpError(path, "could not be read safely");
95
+ }
96
+ }
97
+ export async function planMcpInstallation(paths, machineHomeDirectory) {
98
+ const path = join(machineHomeDirectory, MACHINE_MCP_DIRECTORY, MACHINE_MCP_FILE);
99
+ const rendered = await renderOptionalMcp(paths);
100
+ const originalContent = await readExistingMcp(path);
101
+ if (rendered === undefined) {
102
+ return {
103
+ path,
104
+ content: undefined,
105
+ originalContent,
106
+ records: {},
107
+ installations: [],
108
+ };
109
+ }
110
+ const source = parseObject(rendered, paths.sources.mcp);
111
+ const sourceServers = serverMap(source.mcpServers, paths.sources.mcp);
112
+ const existing = originalContent === undefined ? {} : parseObject(originalContent, path);
113
+ const existingServers = serverMap(existing.mcpServers, path);
114
+ const mergedServers = { ...existingServers };
115
+ const records = {};
116
+ const installations = [];
117
+ let changed = originalContent === undefined;
118
+ for (const [name, definition] of Object.entries(sourceServers).sort(([left], [right]) => left.localeCompare(right))) {
119
+ if (name.length === 0) {
120
+ throw new ContextMcpError(paths.sources.mcp, "server names must not be empty");
121
+ }
122
+ const hash = definitionHash(definition);
123
+ records[name] = {
124
+ sourcePath: paths.sources.mcp,
125
+ normalizedDefinitionHash: hash,
126
+ };
127
+ const existingDefinition = existingServers[name];
128
+ if (existingDefinition === undefined) {
129
+ mergedServers[name] = definition;
130
+ installations.push({ name, path, status: "added" });
131
+ changed = true;
132
+ continue;
133
+ }
134
+ if (definitionHash(existingDefinition) !== hash) {
135
+ throw new ContextMcpError(path, "conflicts with an existing MCP server definition");
136
+ }
137
+ installations.push({ name, path, status: "adopted" });
138
+ }
139
+ if (!changed) {
140
+ return {
141
+ path,
142
+ content: undefined,
143
+ originalContent,
144
+ records,
145
+ installations,
146
+ };
147
+ }
148
+ return {
149
+ path,
150
+ content: `${JSON.stringify({ ...existing, mcpServers: mergedServers }, null, 2)}\n`,
151
+ originalContent,
152
+ records,
153
+ installations,
154
+ };
155
+ }
156
+ export async function applyMcpInstallation(plan) {
157
+ if (plan.content === undefined)
158
+ return;
159
+ await atomicWrite(plan.path, plan.content, plan.path, 0o600);
160
+ }
161
+ export async function rollbackMcpInstallation(plan) {
162
+ if (plan.content === undefined)
163
+ return;
164
+ if (plan.originalContent === undefined) {
165
+ try {
166
+ const stats = await lstat(plan.path);
167
+ if (!stats.isSymbolicLink() && stats.isFile())
168
+ await unlink(plan.path);
169
+ }
170
+ catch (error) {
171
+ if (!isNotFound(error))
172
+ throw error;
173
+ }
174
+ return;
175
+ }
176
+ await atomicWrite(plan.path, plan.originalContent, plan.path, 0o600);
177
+ }
178
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { lstat, readFile, unlink } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { atomicWrite, isNotFound } from \"./internal/filesystem.js\";\nimport { renderOptionalMcp } from \"./template.js\";\nimport type { ResolvedContextPaths } from \"./paths.js\";\n\nconst MACHINE_MCP_DIRECTORY = \".cursor\";\nconst MACHINE_MCP_FILE = \"mcp.json\";\n\ntype JsonObject = Record<string, unknown>;\n\ntype McpRecord = Readonly<{\n sourcePath: string;\n normalizedDefinitionHash: string;\n}>;\n\nexport type ContextMcpInstall = Readonly<{\n name: string;\n path: string;\n status: \"added\" | \"adopted\";\n}>;\n\nexport type ContextMcpPlan = Readonly<{\n path: string;\n content: string | undefined;\n originalContent: string | undefined;\n records: Readonly<Record<string, McpRecord>>;\n installations: readonly ContextMcpInstall[];\n}>;\n\nexport class ContextMcpError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to install MCP context at ${path}: ${message}`);\n this.name = \"ContextMcpError\";\n this.path = path;\n }\n}\n\nfunction isObject(value: unknown): value is JsonObject {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalize);\n if (!isObject(value)) return value;\n return Object.fromEntries(\n Object.entries(value)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, entry]) => [key, canonicalize(entry)]),\n );\n}\n\nfunction definitionHash(value: unknown) {\n return createHash(\"sha256\")\n .update(JSON.stringify(canonicalize(value)))\n .digest(\"hex\");\n}\n\nfunction parseObject(source: string, path: string): JsonObject {\n let value: unknown;\n try {\n value = JSON.parse(source);\n } catch {\n throw new ContextMcpError(path, \"must contain valid JSON\");\n }\n if (!isObject(value)) {\n throw new ContextMcpError(path, \"must contain a JSON object\");\n }\n return value;\n}\n\nfunction serverMap(value: unknown, path: string) {\n if (value === undefined) return {};\n if (!isObject(value)) {\n throw new ContextMcpError(path, \"mcpServers must be a JSON object\");\n }\n return value;\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 ContextMcpError(\n current,\n \"must not pass through a symbolic link\",\n );\n }\n } catch (error) {\n if (error instanceof ContextMcpError) throw error;\n if (!isNotFound(error)) {\n throw new ContextMcpError(current, \"could not be inspected safely\");\n }\n }\n const parent = dirname(current);\n if (parent === current) return;\n current = parent;\n }\n}\n\nasync function readExistingMcp(path: string) {\n await assertNoSymlinkAncestors(path);\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw new ContextMcpError(path, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink() || !stats.isFile()) {\n throw new ContextMcpError(path, \"must be a regular file\");\n }\n try {\n return await readFile(path, \"utf8\");\n } catch {\n throw new ContextMcpError(path, \"could not be read safely\");\n }\n}\n\nexport async function planMcpInstallation(\n paths: ResolvedContextPaths,\n machineHomeDirectory: string,\n): Promise<ContextMcpPlan> {\n const path = join(\n machineHomeDirectory,\n MACHINE_MCP_DIRECTORY,\n MACHINE_MCP_FILE,\n );\n const rendered = await renderOptionalMcp(paths);\n const originalContent = await readExistingMcp(path);\n if (rendered === undefined) {\n return {\n path,\n content: undefined,\n originalContent,\n records: {},\n installations: [],\n };\n }\n\n const source = parseObject(rendered, paths.sources.mcp);\n const sourceServers = serverMap(source.mcpServers, paths.sources.mcp);\n const existing =\n originalContent === undefined ? {} : parseObject(originalContent, path);\n const existingServers = serverMap(existing.mcpServers, path);\n const mergedServers: JsonObject = { ...existingServers };\n const records: Record<string, McpRecord> = {};\n const installations: ContextMcpInstall[] = [];\n let changed = originalContent === undefined;\n\n for (const [name, definition] of Object.entries(sourceServers).sort(\n ([left], [right]) => left.localeCompare(right),\n )) {\n if (name.length === 0) {\n throw new ContextMcpError(\n paths.sources.mcp,\n \"server names must not be empty\",\n );\n }\n const hash = definitionHash(definition);\n records[name] = {\n sourcePath: paths.sources.mcp,\n normalizedDefinitionHash: hash,\n };\n const existingDefinition = existingServers[name];\n if (existingDefinition === undefined) {\n mergedServers[name] = definition;\n installations.push({ name, path, status: \"added\" });\n changed = true;\n continue;\n }\n if (definitionHash(existingDefinition) !== hash) {\n throw new ContextMcpError(\n path,\n \"conflicts with an existing MCP server definition\",\n );\n }\n installations.push({ name, path, status: \"adopted\" });\n }\n\n if (!changed) {\n return {\n path,\n content: undefined,\n originalContent,\n records,\n installations,\n };\n }\n return {\n path,\n content: `${JSON.stringify({ ...existing, mcpServers: mergedServers }, null, 2)}\\n`,\n originalContent,\n records,\n installations,\n };\n}\n\nexport async function applyMcpInstallation(plan: ContextMcpPlan) {\n if (plan.content === undefined) return;\n await atomicWrite(plan.path, plan.content, plan.path, 0o600);\n}\n\nexport async function rollbackMcpInstallation(plan: ContextMcpPlan) {\n if (plan.content === undefined) return;\n if (plan.originalContent === undefined) {\n try {\n const stats = await lstat(plan.path);\n if (!stats.isSymbolicLink() && stats.isFile()) await unlink(plan.path);\n } catch (error) {\n if (!isNotFound(error)) throw error;\n }\n return;\n }\n await atomicWrite(plan.path, plan.originalContent, plan.path, 0o600);\n}\n"]}
@@ -0,0 +1,23 @@
1
+ export type LegacySkillRepoint = Readonly<{
2
+ name: string;
3
+ target: string;
4
+ legacySource: string;
5
+ canonicalSource: string;
6
+ }>;
7
+ export type LegacyMigrationPlan = Readonly<{
8
+ manifestPath: string;
9
+ repoints: readonly LegacySkillRepoint[];
10
+ }>;
11
+ export declare class ContextMigrationError extends Error {
12
+ readonly path: string;
13
+ constructor(path: string, message: string);
14
+ }
15
+ export declare function planLegacyMigration(options: {
16
+ repositoryRoot: string;
17
+ machineSkillsRoot: string;
18
+ canonicalSkillsRoot: string;
19
+ skillNames: readonly string[];
20
+ }): Promise<LegacyMigrationPlan | undefined>;
21
+ export declare function removeLegacyManifest(plan: LegacyMigrationPlan): Promise<void>;
22
+ export declare function rollbackLegacyMigration(plan: LegacyMigrationPlan): Promise<void>;
23
+ //# sourceMappingURL=migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACzC,CAAC,CAAC;AAEH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxC;CACF;AA0GD,wBAAsB,mBAAmB,CAAC,OAAO,EAAE;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAmG3C;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,mBAAmB,iBAyBnE;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,mBAAmB,iBAYtE","sourcesContent":["import {\n lstat,\n readFile,\n readdir,\n realpath,\n symlink,\n unlink,\n} from \"node:fs/promises\";\nimport { basename, join, relative, resolve, sep } from \"node:path\";\nimport { validateSkillNames } from \"./skills.js\";\n\nconst LEGACY_MANIFEST_PATTERN =\n /^\\.?[A-Za-z0-9][A-Za-z0-9._-]*-manifest\\.json$/u;\n\ntype LegacyManifest = Readonly<{\n source: string;\n skills: readonly string[];\n}>;\n\nexport type LegacySkillRepoint = Readonly<{\n name: string;\n target: string;\n legacySource: string;\n canonicalSource: string;\n}>;\n\nexport type LegacyMigrationPlan = Readonly<{\n manifestPath: string;\n repoints: readonly LegacySkillRepoint[];\n}>;\n\nexport class ContextMigrationError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to migrate context at ${path}: ${message}`);\n this.name = \"ContextMigrationError\";\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\nfunction isContained(root: string, candidate: string) {\n const path = relative(root, candidate);\n return (\n path.length > 0 &&\n path !== \"..\" &&\n !path.startsWith(`..${sep}`) &&\n !path.includes(sep + sep)\n );\n}\n\nasync function requireDirectory(path: string, label: string) {\n let stats;\n try {\n stats = await lstat(path);\n } catch {\n throw new ContextMigrationError(label, \"must exist as a directory\");\n }\n if (stats.isSymbolicLink() || !stats.isDirectory()) {\n throw new ContextMigrationError(label, \"must be a regular directory\");\n }\n}\n\nasync function readLegacyManifest(path: string): Promise<LegacyManifest> {\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) throw error;\n throw new ContextMigrationError(path, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink() || !stats.isFile()) {\n throw new ContextMigrationError(path, \"must be a regular file\");\n }\n\n let value: unknown;\n try {\n value = JSON.parse(await readFile(path, \"utf8\"));\n } catch {\n throw new ContextMigrationError(path, \"must contain valid JSON\");\n }\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new ContextMigrationError(\n path,\n \"must contain a legacy manifest object\",\n );\n }\n const manifest = value as { source?: unknown; skills?: unknown };\n if (typeof manifest.source !== \"string\" || manifest.source.length === 0) {\n throw new ContextMigrationError(\n path,\n \"legacy source must be a nonempty path\",\n );\n }\n if (\n !Array.isArray(manifest.skills) ||\n !manifest.skills.every((skill) => typeof skill === \"string\")\n ) {\n throw new ContextMigrationError(\n path,\n \"legacy skills must be an array of names\",\n );\n }\n try {\n validateSkillNames(manifest.skills);\n } catch {\n throw new ContextMigrationError(\n path,\n \"legacy skills contain unsafe or duplicate names\",\n );\n }\n return { source: manifest.source, skills: manifest.skills };\n}\n\nasync function findLegacyManifest(machineSkillsRoot: string) {\n let entries;\n try {\n entries = await readdir(machineSkillsRoot, { withFileTypes: true });\n } catch {\n throw new ContextMigrationError(\n machineSkillsRoot,\n \"legacy skill directory could not be enumerated\",\n );\n }\n const manifests = entries\n .filter((entry) => LEGACY_MANIFEST_PATTERN.test(entry.name))\n .map((entry) => join(machineSkillsRoot, entry.name));\n if (manifests.length > 1) {\n throw new ContextMigrationError(\n machineSkillsRoot,\n \"multiple legacy manifests require manual review\",\n );\n }\n return manifests[0];\n}\n\nexport async function planLegacyMigration(options: {\n repositoryRoot: string;\n machineSkillsRoot: string;\n canonicalSkillsRoot: string;\n skillNames: readonly string[];\n}): Promise<LegacyMigrationPlan | undefined> {\n const manifestPath = await findLegacyManifest(options.machineSkillsRoot);\n if (manifestPath === undefined) return undefined;\n const manifest = await readLegacyManifest(manifestPath);\n\n const expectedLegacyRoot = resolve(\n options.repositoryRoot,\n \".claude\",\n \"skills\",\n );\n const declaredLegacyRoot = resolve(manifest.source);\n if (declaredLegacyRoot !== expectedLegacyRoot) return undefined;\n await requireDirectory(declaredLegacyRoot, \"legacy skill source\");\n const canonicalNames = new Set(options.skillNames);\n const manifestNames = new Set(manifest.skills);\n const legacyRootReal = await realpath(declaredLegacyRoot);\n const repoints: LegacySkillRepoint[] = [];\n\n for (const name of manifest.skills) {\n if (!canonicalNames.has(name)) {\n throw new ContextMigrationError(\n manifestPath,\n `skill ${name} has no canonical context source`,\n );\n }\n const legacySource = join(declaredLegacyRoot, name);\n const canonicalSource = join(options.canonicalSkillsRoot, name);\n await requireDirectory(legacySource, `legacy skill ${name}`);\n await requireDirectory(canonicalSource, `canonical skill ${name}`);\n\n const target = join(options.machineSkillsRoot, name);\n let targetStats;\n try {\n targetStats = await lstat(target);\n } catch {\n throw new ContextMigrationError(\n target,\n \"managed legacy skill link is missing\",\n );\n }\n if (!targetStats.isSymbolicLink()) {\n throw new ContextMigrationError(\n target,\n \"managed legacy skill must remain a symbolic link\",\n );\n }\n\n let targetRealPath: string;\n try {\n targetRealPath = await realpath(target);\n } catch {\n throw new ContextMigrationError(\n target,\n \"managed legacy skill link is broken\",\n );\n }\n const canonicalRealPath = await realpath(canonicalSource);\n const legacyRealPath = await realpath(legacySource);\n if (targetRealPath === canonicalRealPath) continue;\n if (targetRealPath !== legacyRealPath) {\n throw new ContextMigrationError(\n target,\n \"managed legacy skill link points somewhere unexpected\",\n );\n }\n repoints.push({ name, target, legacySource, canonicalSource });\n }\n\n let entries;\n try {\n entries = await readdir(options.machineSkillsRoot, {\n withFileTypes: true,\n });\n } catch {\n throw new ContextMigrationError(\n options.machineSkillsRoot,\n \"legacy skill directory could not be enumerated\",\n );\n }\n for (const entry of entries) {\n if (entry.name === basename(manifestPath) || manifestNames.has(entry.name))\n continue;\n const target = join(options.machineSkillsRoot, entry.name);\n if (!entry.isSymbolicLink()) continue;\n let targetRealPath;\n try {\n targetRealPath = await realpath(target);\n } catch {\n continue;\n }\n if (isContained(legacyRootReal, targetRealPath)) {\n throw new ContextMigrationError(\n target,\n \"unlisted legacy skill link requires manual review\",\n );\n }\n }\n\n return { manifestPath, repoints };\n}\n\nexport async function removeLegacyManifest(plan: LegacyMigrationPlan) {\n let stats;\n try {\n stats = await lstat(plan.manifestPath);\n } catch (error) {\n if (isNotFound(error)) return;\n throw new ContextMigrationError(\n plan.manifestPath,\n \"could not be inspected safely\",\n );\n }\n if (stats.isSymbolicLink() || !stats.isFile()) {\n throw new ContextMigrationError(\n plan.manifestPath,\n \"must remain a regular file until migration completes\",\n );\n }\n try {\n await unlink(plan.manifestPath);\n } catch {\n throw new ContextMigrationError(\n plan.manifestPath,\n \"could not be removed after migration\",\n );\n }\n}\n\nexport async function rollbackLegacyMigration(plan: LegacyMigrationPlan) {\n for (const repoint of plan.repoints) {\n try {\n const targetRealPath = await realpath(repoint.target);\n const canonicalRealPath = await realpath(repoint.canonicalSource);\n if (targetRealPath !== canonicalRealPath) continue;\n await unlink(repoint.target);\n } catch (error) {\n if (!isNotFound(error)) throw error;\n }\n await symlink(repoint.legacySource, repoint.target, \"dir\");\n }\n}\n"]}
@@ -0,0 +1,206 @@
1
+ import { lstat, readFile, readdir, realpath, symlink, unlink, } from "node:fs/promises";
2
+ import { basename, join, relative, resolve, sep } from "node:path";
3
+ import { validateSkillNames } from "./skills.js";
4
+ const LEGACY_MANIFEST_PATTERN = /^\.?[A-Za-z0-9][A-Za-z0-9._-]*-manifest\.json$/u;
5
+ export class ContextMigrationError extends Error {
6
+ path;
7
+ constructor(path, message) {
8
+ super(`Unable to migrate context at ${path}: ${message}`);
9
+ this.name = "ContextMigrationError";
10
+ this.path = path;
11
+ }
12
+ }
13
+ function isNotFound(error) {
14
+ return (error !== null &&
15
+ typeof error === "object" &&
16
+ "code" in error &&
17
+ error.code === "ENOENT");
18
+ }
19
+ function isContained(root, candidate) {
20
+ const path = relative(root, candidate);
21
+ return (path.length > 0 &&
22
+ path !== ".." &&
23
+ !path.startsWith(`..${sep}`) &&
24
+ !path.includes(sep + sep));
25
+ }
26
+ async function requireDirectory(path, label) {
27
+ let stats;
28
+ try {
29
+ stats = await lstat(path);
30
+ }
31
+ catch {
32
+ throw new ContextMigrationError(label, "must exist as a directory");
33
+ }
34
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
35
+ throw new ContextMigrationError(label, "must be a regular directory");
36
+ }
37
+ }
38
+ async function readLegacyManifest(path) {
39
+ let stats;
40
+ try {
41
+ stats = await lstat(path);
42
+ }
43
+ catch (error) {
44
+ if (isNotFound(error))
45
+ throw error;
46
+ throw new ContextMigrationError(path, "could not be inspected safely");
47
+ }
48
+ if (stats.isSymbolicLink() || !stats.isFile()) {
49
+ throw new ContextMigrationError(path, "must be a regular file");
50
+ }
51
+ let value;
52
+ try {
53
+ value = JSON.parse(await readFile(path, "utf8"));
54
+ }
55
+ catch {
56
+ throw new ContextMigrationError(path, "must contain valid JSON");
57
+ }
58
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
59
+ throw new ContextMigrationError(path, "must contain a legacy manifest object");
60
+ }
61
+ const manifest = value;
62
+ if (typeof manifest.source !== "string" || manifest.source.length === 0) {
63
+ throw new ContextMigrationError(path, "legacy source must be a nonempty path");
64
+ }
65
+ if (!Array.isArray(manifest.skills) ||
66
+ !manifest.skills.every((skill) => typeof skill === "string")) {
67
+ throw new ContextMigrationError(path, "legacy skills must be an array of names");
68
+ }
69
+ try {
70
+ validateSkillNames(manifest.skills);
71
+ }
72
+ catch {
73
+ throw new ContextMigrationError(path, "legacy skills contain unsafe or duplicate names");
74
+ }
75
+ return { source: manifest.source, skills: manifest.skills };
76
+ }
77
+ async function findLegacyManifest(machineSkillsRoot) {
78
+ let entries;
79
+ try {
80
+ entries = await readdir(machineSkillsRoot, { withFileTypes: true });
81
+ }
82
+ catch {
83
+ throw new ContextMigrationError(machineSkillsRoot, "legacy skill directory could not be enumerated");
84
+ }
85
+ const manifests = entries
86
+ .filter((entry) => LEGACY_MANIFEST_PATTERN.test(entry.name))
87
+ .map((entry) => join(machineSkillsRoot, entry.name));
88
+ if (manifests.length > 1) {
89
+ throw new ContextMigrationError(machineSkillsRoot, "multiple legacy manifests require manual review");
90
+ }
91
+ return manifests[0];
92
+ }
93
+ export async function planLegacyMigration(options) {
94
+ const manifestPath = await findLegacyManifest(options.machineSkillsRoot);
95
+ if (manifestPath === undefined)
96
+ return undefined;
97
+ const manifest = await readLegacyManifest(manifestPath);
98
+ const expectedLegacyRoot = resolve(options.repositoryRoot, ".claude", "skills");
99
+ const declaredLegacyRoot = resolve(manifest.source);
100
+ if (declaredLegacyRoot !== expectedLegacyRoot)
101
+ return undefined;
102
+ await requireDirectory(declaredLegacyRoot, "legacy skill source");
103
+ const canonicalNames = new Set(options.skillNames);
104
+ const manifestNames = new Set(manifest.skills);
105
+ const legacyRootReal = await realpath(declaredLegacyRoot);
106
+ const repoints = [];
107
+ for (const name of manifest.skills) {
108
+ if (!canonicalNames.has(name)) {
109
+ throw new ContextMigrationError(manifestPath, `skill ${name} has no canonical context source`);
110
+ }
111
+ const legacySource = join(declaredLegacyRoot, name);
112
+ const canonicalSource = join(options.canonicalSkillsRoot, name);
113
+ await requireDirectory(legacySource, `legacy skill ${name}`);
114
+ await requireDirectory(canonicalSource, `canonical skill ${name}`);
115
+ const target = join(options.machineSkillsRoot, name);
116
+ let targetStats;
117
+ try {
118
+ targetStats = await lstat(target);
119
+ }
120
+ catch {
121
+ throw new ContextMigrationError(target, "managed legacy skill link is missing");
122
+ }
123
+ if (!targetStats.isSymbolicLink()) {
124
+ throw new ContextMigrationError(target, "managed legacy skill must remain a symbolic link");
125
+ }
126
+ let targetRealPath;
127
+ try {
128
+ targetRealPath = await realpath(target);
129
+ }
130
+ catch {
131
+ throw new ContextMigrationError(target, "managed legacy skill link is broken");
132
+ }
133
+ const canonicalRealPath = await realpath(canonicalSource);
134
+ const legacyRealPath = await realpath(legacySource);
135
+ if (targetRealPath === canonicalRealPath)
136
+ continue;
137
+ if (targetRealPath !== legacyRealPath) {
138
+ throw new ContextMigrationError(target, "managed legacy skill link points somewhere unexpected");
139
+ }
140
+ repoints.push({ name, target, legacySource, canonicalSource });
141
+ }
142
+ let entries;
143
+ try {
144
+ entries = await readdir(options.machineSkillsRoot, {
145
+ withFileTypes: true,
146
+ });
147
+ }
148
+ catch {
149
+ throw new ContextMigrationError(options.machineSkillsRoot, "legacy skill directory could not be enumerated");
150
+ }
151
+ for (const entry of entries) {
152
+ if (entry.name === basename(manifestPath) || manifestNames.has(entry.name))
153
+ continue;
154
+ const target = join(options.machineSkillsRoot, entry.name);
155
+ if (!entry.isSymbolicLink())
156
+ continue;
157
+ let targetRealPath;
158
+ try {
159
+ targetRealPath = await realpath(target);
160
+ }
161
+ catch {
162
+ continue;
163
+ }
164
+ if (isContained(legacyRootReal, targetRealPath)) {
165
+ throw new ContextMigrationError(target, "unlisted legacy skill link requires manual review");
166
+ }
167
+ }
168
+ return { manifestPath, repoints };
169
+ }
170
+ export async function removeLegacyManifest(plan) {
171
+ let stats;
172
+ try {
173
+ stats = await lstat(plan.manifestPath);
174
+ }
175
+ catch (error) {
176
+ if (isNotFound(error))
177
+ return;
178
+ throw new ContextMigrationError(plan.manifestPath, "could not be inspected safely");
179
+ }
180
+ if (stats.isSymbolicLink() || !stats.isFile()) {
181
+ throw new ContextMigrationError(plan.manifestPath, "must remain a regular file until migration completes");
182
+ }
183
+ try {
184
+ await unlink(plan.manifestPath);
185
+ }
186
+ catch {
187
+ throw new ContextMigrationError(plan.manifestPath, "could not be removed after migration");
188
+ }
189
+ }
190
+ export async function rollbackLegacyMigration(plan) {
191
+ for (const repoint of plan.repoints) {
192
+ try {
193
+ const targetRealPath = await realpath(repoint.target);
194
+ const canonicalRealPath = await realpath(repoint.canonicalSource);
195
+ if (targetRealPath !== canonicalRealPath)
196
+ continue;
197
+ await unlink(repoint.target);
198
+ }
199
+ catch (error) {
200
+ if (!isNotFound(error))
201
+ throw error;
202
+ }
203
+ await symlink(repoint.legacySource, repoint.target, "dir");
204
+ }
205
+ }
206
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.js","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n lstat,\n readFile,\n readdir,\n realpath,\n symlink,\n unlink,\n} from \"node:fs/promises\";\nimport { basename, join, relative, resolve, sep } from \"node:path\";\nimport { validateSkillNames } from \"./skills.js\";\n\nconst LEGACY_MANIFEST_PATTERN =\n /^\\.?[A-Za-z0-9][A-Za-z0-9._-]*-manifest\\.json$/u;\n\ntype LegacyManifest = Readonly<{\n source: string;\n skills: readonly string[];\n}>;\n\nexport type LegacySkillRepoint = Readonly<{\n name: string;\n target: string;\n legacySource: string;\n canonicalSource: string;\n}>;\n\nexport type LegacyMigrationPlan = Readonly<{\n manifestPath: string;\n repoints: readonly LegacySkillRepoint[];\n}>;\n\nexport class ContextMigrationError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Unable to migrate context at ${path}: ${message}`);\n this.name = \"ContextMigrationError\";\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\nfunction isContained(root: string, candidate: string) {\n const path = relative(root, candidate);\n return (\n path.length > 0 &&\n path !== \"..\" &&\n !path.startsWith(`..${sep}`) &&\n !path.includes(sep + sep)\n );\n}\n\nasync function requireDirectory(path: string, label: string) {\n let stats;\n try {\n stats = await lstat(path);\n } catch {\n throw new ContextMigrationError(label, \"must exist as a directory\");\n }\n if (stats.isSymbolicLink() || !stats.isDirectory()) {\n throw new ContextMigrationError(label, \"must be a regular directory\");\n }\n}\n\nasync function readLegacyManifest(path: string): Promise<LegacyManifest> {\n let stats;\n try {\n stats = await lstat(path);\n } catch (error) {\n if (isNotFound(error)) throw error;\n throw new ContextMigrationError(path, \"could not be inspected safely\");\n }\n if (stats.isSymbolicLink() || !stats.isFile()) {\n throw new ContextMigrationError(path, \"must be a regular file\");\n }\n\n let value: unknown;\n try {\n value = JSON.parse(await readFile(path, \"utf8\"));\n } catch {\n throw new ContextMigrationError(path, \"must contain valid JSON\");\n }\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new ContextMigrationError(\n path,\n \"must contain a legacy manifest object\",\n );\n }\n const manifest = value as { source?: unknown; skills?: unknown };\n if (typeof manifest.source !== \"string\" || manifest.source.length === 0) {\n throw new ContextMigrationError(\n path,\n \"legacy source must be a nonempty path\",\n );\n }\n if (\n !Array.isArray(manifest.skills) ||\n !manifest.skills.every((skill) => typeof skill === \"string\")\n ) {\n throw new ContextMigrationError(\n path,\n \"legacy skills must be an array of names\",\n );\n }\n try {\n validateSkillNames(manifest.skills);\n } catch {\n throw new ContextMigrationError(\n path,\n \"legacy skills contain unsafe or duplicate names\",\n );\n }\n return { source: manifest.source, skills: manifest.skills };\n}\n\nasync function findLegacyManifest(machineSkillsRoot: string) {\n let entries;\n try {\n entries = await readdir(machineSkillsRoot, { withFileTypes: true });\n } catch {\n throw new ContextMigrationError(\n machineSkillsRoot,\n \"legacy skill directory could not be enumerated\",\n );\n }\n const manifests = entries\n .filter((entry) => LEGACY_MANIFEST_PATTERN.test(entry.name))\n .map((entry) => join(machineSkillsRoot, entry.name));\n if (manifests.length > 1) {\n throw new ContextMigrationError(\n machineSkillsRoot,\n \"multiple legacy manifests require manual review\",\n );\n }\n return manifests[0];\n}\n\nexport async function planLegacyMigration(options: {\n repositoryRoot: string;\n machineSkillsRoot: string;\n canonicalSkillsRoot: string;\n skillNames: readonly string[];\n}): Promise<LegacyMigrationPlan | undefined> {\n const manifestPath = await findLegacyManifest(options.machineSkillsRoot);\n if (manifestPath === undefined) return undefined;\n const manifest = await readLegacyManifest(manifestPath);\n\n const expectedLegacyRoot = resolve(\n options.repositoryRoot,\n \".claude\",\n \"skills\",\n );\n const declaredLegacyRoot = resolve(manifest.source);\n if (declaredLegacyRoot !== expectedLegacyRoot) return undefined;\n await requireDirectory(declaredLegacyRoot, \"legacy skill source\");\n const canonicalNames = new Set(options.skillNames);\n const manifestNames = new Set(manifest.skills);\n const legacyRootReal = await realpath(declaredLegacyRoot);\n const repoints: LegacySkillRepoint[] = [];\n\n for (const name of manifest.skills) {\n if (!canonicalNames.has(name)) {\n throw new ContextMigrationError(\n manifestPath,\n `skill ${name} has no canonical context source`,\n );\n }\n const legacySource = join(declaredLegacyRoot, name);\n const canonicalSource = join(options.canonicalSkillsRoot, name);\n await requireDirectory(legacySource, `legacy skill ${name}`);\n await requireDirectory(canonicalSource, `canonical skill ${name}`);\n\n const target = join(options.machineSkillsRoot, name);\n let targetStats;\n try {\n targetStats = await lstat(target);\n } catch {\n throw new ContextMigrationError(\n target,\n \"managed legacy skill link is missing\",\n );\n }\n if (!targetStats.isSymbolicLink()) {\n throw new ContextMigrationError(\n target,\n \"managed legacy skill must remain a symbolic link\",\n );\n }\n\n let targetRealPath: string;\n try {\n targetRealPath = await realpath(target);\n } catch {\n throw new ContextMigrationError(\n target,\n \"managed legacy skill link is broken\",\n );\n }\n const canonicalRealPath = await realpath(canonicalSource);\n const legacyRealPath = await realpath(legacySource);\n if (targetRealPath === canonicalRealPath) continue;\n if (targetRealPath !== legacyRealPath) {\n throw new ContextMigrationError(\n target,\n \"managed legacy skill link points somewhere unexpected\",\n );\n }\n repoints.push({ name, target, legacySource, canonicalSource });\n }\n\n let entries;\n try {\n entries = await readdir(options.machineSkillsRoot, {\n withFileTypes: true,\n });\n } catch {\n throw new ContextMigrationError(\n options.machineSkillsRoot,\n \"legacy skill directory could not be enumerated\",\n );\n }\n for (const entry of entries) {\n if (entry.name === basename(manifestPath) || manifestNames.has(entry.name))\n continue;\n const target = join(options.machineSkillsRoot, entry.name);\n if (!entry.isSymbolicLink()) continue;\n let targetRealPath;\n try {\n targetRealPath = await realpath(target);\n } catch {\n continue;\n }\n if (isContained(legacyRootReal, targetRealPath)) {\n throw new ContextMigrationError(\n target,\n \"unlisted legacy skill link requires manual review\",\n );\n }\n }\n\n return { manifestPath, repoints };\n}\n\nexport async function removeLegacyManifest(plan: LegacyMigrationPlan) {\n let stats;\n try {\n stats = await lstat(plan.manifestPath);\n } catch (error) {\n if (isNotFound(error)) return;\n throw new ContextMigrationError(\n plan.manifestPath,\n \"could not be inspected safely\",\n );\n }\n if (stats.isSymbolicLink() || !stats.isFile()) {\n throw new ContextMigrationError(\n plan.manifestPath,\n \"must remain a regular file until migration completes\",\n );\n }\n try {\n await unlink(plan.manifestPath);\n } catch {\n throw new ContextMigrationError(\n plan.manifestPath,\n \"could not be removed after migration\",\n );\n }\n}\n\nexport async function rollbackLegacyMigration(plan: LegacyMigrationPlan) {\n for (const repoint of plan.repoints) {\n try {\n const targetRealPath = await realpath(repoint.target);\n const canonicalRealPath = await realpath(repoint.canonicalSource);\n if (targetRealPath !== canonicalRealPath) continue;\n await unlink(repoint.target);\n } catch (error) {\n if (!isNotFound(error)) throw error;\n }\n await symlink(repoint.legacySource, repoint.target, \"dir\");\n }\n}\n"]}
@@ -0,0 +1,58 @@
1
+ export declare const DEFAULT_PROTOTYPE_ROOT: string;
2
+ export declare const PROTOTYPE_METADATA_FILE = "metadata.json";
3
+ export type ContextPathOptions = Readonly<{
4
+ repositoryRoot: string;
5
+ prototypeRoot?: string;
6
+ }>;
7
+ export type PrototypeContextPathOptions = Readonly<{
8
+ repositoryRoot: string;
9
+ prototypeRoot?: string;
10
+ prototypePath: string;
11
+ }>;
12
+ export type ContextSources = Readonly<{
13
+ rootInstructions: string;
14
+ prototypeInstructions: string;
15
+ mcp: string;
16
+ settings: string;
17
+ skills: string;
18
+ }>;
19
+ export type RootContextOutputs = Readonly<{
20
+ agents: string;
21
+ claude: string;
22
+ mcp: string;
23
+ settings: string;
24
+ skills: string;
25
+ }>;
26
+ export type PrototypeContextOutputs = Readonly<{
27
+ agents: string;
28
+ claude: string;
29
+ settings: string;
30
+ }>;
31
+ export type ResolvedContextPaths = Readonly<{
32
+ repositoryRoot: string;
33
+ prototypeRoot: string;
34
+ contextRoot: string;
35
+ sources: ContextSources;
36
+ rootOutputs: RootContextOutputs;
37
+ }>;
38
+ export type ResolvedPrototypeContextPaths = Readonly<ResolvedContextPaths & {
39
+ prototypePath: string;
40
+ prototypeMetadata: string;
41
+ prototypeOutputs: PrototypeContextOutputs;
42
+ }>;
43
+ export declare class ContextPathError extends Error {
44
+ readonly path: string;
45
+ constructor(path: string, message: string);
46
+ }
47
+ export declare function assertSafeGeneratedPath(path: string, label: string): void;
48
+ export declare function resolveContextPaths(options: ContextPathOptions): ResolvedContextPaths;
49
+ export declare function resolvePrototypeContextPaths(options: PrototypeContextPathOptions): ResolvedPrototypeContextPaths;
50
+ /**
51
+ * Validate the filesystem boundaries before reading or writing context.
52
+ * Missing roots are allowed so callers can produce a useful source/filesystem
53
+ * error later, but existing roots, parents, and managed targets may not be
54
+ * symbolic links.
55
+ */
56
+ export declare function assertSafeContextPaths(paths: ResolvedContextPaths): void;
57
+ export declare function assertSafePrototypeContextPaths(paths: ResolvedPrototypeContextPaths): void;
58
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,sBAAsB,QAAqC,CAAC;AACzE,eAAO,MAAM,uBAAuB,kBAAkB,CAAC;AASvD,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,WAAW,EAAE,kBAAkB,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAClD,oBAAoB,GAAG;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,uBAAuB,CAAC;CAC3C,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;AAyGD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAElE;AAmCD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,kBAAkB,GAC1B,oBAAoB,CA4BtB;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,2BAA2B,GACnC,6BAA6B,CAiB/B;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,oBAAoB,QAejE;AAED,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,6BAA6B,QAQrC","sourcesContent":["import { lstatSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\n\nexport const DEFAULT_PROTOTYPE_ROOT = join(\"src\", \"app\", \"(prototypes)\");\nexport const PROTOTYPE_METADATA_FILE = \"metadata.json\";\n\nconst CONTEXT_DIRECTORY = \"context\";\nconst ROOT_INSTRUCTIONS_FILE = \"ROOT.md\";\nconst PROTOTYPE_INSTRUCTIONS_FILE = \"PROTOTYPE.md\";\nconst MCP_FILE = \"mcp.json\";\nconst SETTINGS_FILE = \"settings.json\";\nconst SKILLS_DIRECTORY = \"skills\";\n\nexport type ContextPathOptions = Readonly<{\n repositoryRoot: string;\n prototypeRoot?: string;\n}>;\n\nexport type PrototypeContextPathOptions = Readonly<{\n repositoryRoot: string;\n prototypeRoot?: string;\n prototypePath: string;\n}>;\n\nexport type ContextSources = Readonly<{\n rootInstructions: string;\n prototypeInstructions: string;\n mcp: string;\n settings: string;\n skills: string;\n}>;\n\nexport type RootContextOutputs = Readonly<{\n agents: string;\n claude: string;\n mcp: string;\n settings: string;\n skills: string;\n}>;\n\nexport type PrototypeContextOutputs = Readonly<{\n agents: string;\n claude: string;\n settings: string;\n}>;\n\nexport type ResolvedContextPaths = Readonly<{\n repositoryRoot: string;\n prototypeRoot: string;\n contextRoot: string;\n sources: ContextSources;\n rootOutputs: RootContextOutputs;\n}>;\n\nexport type ResolvedPrototypeContextPaths = Readonly<\n ResolvedContextPaths & {\n prototypePath: string;\n prototypeMetadata: string;\n prototypeOutputs: PrototypeContextOutputs;\n }\n>;\n\nexport class ContextPathError extends Error {\n readonly path: string;\n\n constructor(path: string, message: string) {\n super(`Invalid context path at ${path}: ${message}`);\n this.name = \"ContextPathError\";\n this.path = path;\n }\n}\n\nfunction requirePath(value: unknown, label: string) {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new ContextPathError(label, \"must be a nonempty path\");\n }\n return value;\n}\n\nfunction resolveRepositoryRoot(value: unknown) {\n return resolve(requirePath(value, \"repositoryRoot\"));\n}\n\nfunction resolveChildPath(\n root: string,\n value: string | undefined,\n label: string,\n defaultPath: string,\n) {\n const path = value === undefined ? defaultPath : requirePath(value, label);\n return isAbsolute(path) ? resolve(path) : resolve(root, path);\n}\n\nfunction isContained(root: string, candidate: string, allowRoot = false) {\n const pathFromRoot = relative(root, candidate);\n return (\n (allowRoot && pathFromRoot.length === 0) ||\n (pathFromRoot.length > 0 &&\n pathFromRoot !== \"..\" &&\n !pathFromRoot.startsWith(`..${sep}`) &&\n !isAbsolute(pathFromRoot))\n );\n}\n\nfunction assertContained(\n root: string,\n candidate: string,\n label: string,\n allowRoot = false,\n) {\n if (!isContained(root, candidate, allowRoot)) {\n throw new ContextPathError(label, `must be contained within ${root}`);\n }\n}\n\nfunction assertDirectory(path: string, label: string, allowMissing: boolean) {\n let stats;\n try {\n stats = lstatSync(path);\n } catch (error) {\n if (\n allowMissing &&\n error !== null &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n ) {\n return;\n }\n throw new ContextPathError(label, \"could not be inspected\");\n }\n\n if (stats.isSymbolicLink()) {\n throw new ContextPathError(label, \"must not be a symbolic link\");\n }\n if (!stats.isDirectory()) {\n throw new ContextPathError(label, \"must be a directory\");\n }\n}\n\nfunction assertNoSymlinkSegments(path: string, label: string) {\n let current = path;\n while (true) {\n let stats;\n try {\n stats = lstatSync(current);\n } catch (error) {\n if (\n error !== null &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n ) {\n const parent = dirname(current);\n if (parent === current) return;\n current = parent;\n continue;\n }\n throw new ContextPathError(label, \"could not be inspected\");\n }\n\n if (stats.isSymbolicLink()) {\n throw new ContextPathError(\n label,\n \"must not pass through a symbolic link\",\n );\n }\n return;\n }\n}\n\nfunction assertSafeFilePath(path: string, label: string) {\n assertNoSymlinkSegments(path, label);\n}\n\nexport function assertSafeGeneratedPath(path: string, label: string) {\n assertSafeFilePath(path, label);\n}\n\nfunction assertSafeDirectoryPath(path: string, label: string) {\n assertNoSymlinkSegments(path, label);\n assertDirectory(path, label, true);\n}\n\nfunction buildSources(contextRoot: string): ContextSources {\n return {\n rootInstructions: join(contextRoot, ROOT_INSTRUCTIONS_FILE),\n prototypeInstructions: join(contextRoot, PROTOTYPE_INSTRUCTIONS_FILE),\n mcp: join(contextRoot, MCP_FILE),\n settings: join(contextRoot, SETTINGS_FILE),\n skills: join(contextRoot, SKILLS_DIRECTORY),\n };\n}\n\nfunction buildRootOutputs(repositoryRoot: string): RootContextOutputs {\n return {\n agents: join(repositoryRoot, \"AGENTS.md\"),\n claude: join(repositoryRoot, \"CLAUDE.md\"),\n mcp: join(repositoryRoot, \".mcp.json\"),\n settings: join(repositoryRoot, \".claude\", \"settings.json\"),\n skills: join(repositoryRoot, \".claude\", \"skills\"),\n };\n}\n\nfunction buildPrototypeOutputs(prototypePath: string): PrototypeContextOutputs {\n return {\n agents: join(prototypePath, \"AGENTS.md\"),\n claude: join(prototypePath, \"CLAUDE.md\"),\n settings: join(prototypePath, \".claude\", \"settings.json\"),\n };\n}\n\nexport function resolveContextPaths(\n options: ContextPathOptions,\n): ResolvedContextPaths {\n const repositoryRoot = resolveRepositoryRoot(options?.repositoryRoot);\n const prototypeRoot = resolveChildPath(\n repositoryRoot,\n options?.prototypeRoot,\n \"prototypeRoot\",\n DEFAULT_PROTOTYPE_ROOT,\n );\n assertContained(repositoryRoot, prototypeRoot, \"prototypeRoot\");\n\n const contextRoot = join(repositoryRoot, CONTEXT_DIRECTORY);\n const sources = buildSources(contextRoot);\n const rootOutputs = buildRootOutputs(repositoryRoot);\n\n for (const [label, path] of Object.entries(sources)) {\n assertContained(contextRoot, path, `sources.${label}`, true);\n }\n for (const [label, path] of Object.entries(rootOutputs)) {\n assertContained(repositoryRoot, path, `rootOutputs.${label}`, true);\n }\n\n return {\n repositoryRoot,\n prototypeRoot,\n contextRoot,\n sources,\n rootOutputs,\n };\n}\n\nexport function resolvePrototypeContextPaths(\n options: PrototypeContextPathOptions,\n): ResolvedPrototypeContextPaths {\n const paths = resolveContextPaths(options);\n const prototypePath = resolveChildPath(\n paths.prototypeRoot,\n options?.prototypePath,\n \"prototypePath\",\n paths.prototypeRoot,\n );\n assertContained(paths.prototypeRoot, prototypePath, \"prototypePath\");\n const prototypeMetadata = join(prototypePath, PROTOTYPE_METADATA_FILE);\n\n return {\n ...paths,\n prototypePath,\n prototypeMetadata,\n prototypeOutputs: buildPrototypeOutputs(prototypePath),\n };\n}\n\n/**\n * Validate the filesystem boundaries before reading or writing context.\n * Missing roots are allowed so callers can produce a useful source/filesystem\n * error later, but existing roots, parents, and managed targets may not be\n * symbolic links.\n */\nexport function assertSafeContextPaths(paths: ResolvedContextPaths) {\n assertSafeDirectoryPath(paths.repositoryRoot, \"repositoryRoot\");\n assertSafeDirectoryPath(paths.prototypeRoot, \"prototypeRoot\");\n assertSafeDirectoryPath(paths.contextRoot, \"contextRoot\");\n\n for (const [label, path] of Object.entries(paths.sources)) {\n assertSafeFilePath(path, `sources.${label}`);\n }\n for (const [label, path] of Object.entries(paths.rootOutputs)) {\n if (label === \"skills\") {\n assertSafeDirectoryPath(path, `rootOutputs.${label}`);\n } else {\n assertSafeFilePath(path, `rootOutputs.${label}`);\n }\n }\n}\n\nexport function assertSafePrototypeContextPaths(\n paths: ResolvedPrototypeContextPaths,\n) {\n assertSafeContextPaths(paths);\n assertSafeDirectoryPath(paths.prototypePath, \"prototypePath\");\n assertSafeFilePath(paths.prototypeMetadata, \"prototypeMetadata\");\n for (const [label, path] of Object.entries(paths.prototypeOutputs)) {\n assertSafeFilePath(path, `prototypeOutputs.${label}`);\n }\n}\n"]}