@glasstrace/sdk 0.15.1 → 0.17.0

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 (78) hide show
  1. package/README.md +148 -1
  2. package/dist/adapters/drizzle.js +2 -5
  3. package/dist/adapters/drizzle.js.map +1 -1
  4. package/dist/{chunk-PD2SKFQQ.js → chunk-55FBXXER.js} +4 -8
  5. package/dist/{chunk-PD2SKFQQ.js.map → chunk-55FBXXER.js.map} +1 -1
  6. package/dist/{chunk-ZNOD6FC7.js → chunk-CTJI2YKA.js} +8 -15
  7. package/dist/{chunk-ZNOD6FC7.js.map → chunk-CTJI2YKA.js.map} +1 -1
  8. package/dist/{chunk-WK7MPK2T.js → chunk-DQ25VOKK.js} +1 -89
  9. package/dist/chunk-DQ25VOKK.js.map +1 -0
  10. package/dist/{chunk-BL3YDC6V.js → chunk-DXRZKKSO.js} +1 -6
  11. package/dist/{chunk-BL3YDC6V.js.map → chunk-DXRZKKSO.js.map} +1 -1
  12. package/dist/{chunk-2LDBR3F3.js → chunk-E33Y7BQH.js} +36 -74
  13. package/dist/chunk-E33Y7BQH.js.map +1 -0
  14. package/dist/{chunk-YMEXDDTA.js → chunk-GSGX76Q5.js} +4 -99
  15. package/dist/chunk-GSGX76Q5.js.map +1 -0
  16. package/dist/{chunk-ECEN724Y.js → chunk-J5BW7V2D.js} +4 -8
  17. package/dist/{chunk-ECEN724Y.js.map → chunk-J5BW7V2D.js.map} +1 -1
  18. package/dist/{chunk-BGZ7J74D.js → chunk-NSBPE2FW.js} +2 -16
  19. package/dist/{chunk-A2AZL6MZ.js → chunk-O63DJKIJ.js} +169 -18
  20. package/dist/chunk-O63DJKIJ.js.map +1 -0
  21. package/dist/chunk-UUUKI65I.js +851 -0
  22. package/dist/chunk-UUUKI65I.js.map +1 -0
  23. package/dist/chunk-VUZCLMIX.js +57 -0
  24. package/dist/chunk-VUZCLMIX.js.map +1 -0
  25. package/dist/{chunk-OSXIUKD5.js → chunk-WZXVS2EO.js} +1 -6
  26. package/dist/{chunk-OSXIUKD5.js.map → chunk-WZXVS2EO.js.map} +1 -1
  27. package/dist/{chunk-ROFOJQWN.js → chunk-XNDHQN4S.js} +7 -11
  28. package/dist/{chunk-ROFOJQWN.js.map → chunk-XNDHQN4S.js.map} +1 -1
  29. package/dist/cli/init.cjs +673 -161
  30. package/dist/cli/init.cjs.map +1 -1
  31. package/dist/cli/init.d.cts +54 -1
  32. package/dist/cli/init.d.ts +54 -1
  33. package/dist/cli/init.js +146 -37
  34. package/dist/cli/init.js.map +1 -1
  35. package/dist/cli/mcp-add.cjs +16 -16
  36. package/dist/cli/mcp-add.cjs.map +1 -1
  37. package/dist/cli/mcp-add.js +10 -13
  38. package/dist/cli/mcp-add.js.map +1 -1
  39. package/dist/cli/status.cjs +2 -2
  40. package/dist/cli/status.js +4 -7
  41. package/dist/cli/status.js.map +1 -1
  42. package/dist/cli/uninit.cjs +56 -59
  43. package/dist/cli/uninit.cjs.map +1 -1
  44. package/dist/cli/uninit.js +4 -4
  45. package/dist/cli/validate.cjs +2 -2
  46. package/dist/cli/validate.js +3 -6
  47. package/dist/cli/validate.js.map +1 -1
  48. package/dist/{esm-MDK7CZID.js → esm-KBPHCVB4.js} +3 -3
  49. package/dist/{getMachineId-bsd-4NIRBWME.js → getMachineId-bsd-345PYXFX.js} +4 -7
  50. package/dist/{getMachineId-bsd-4NIRBWME.js.map → getMachineId-bsd-345PYXFX.js.map} +1 -1
  51. package/dist/{getMachineId-darwin-2XNOCCJQ.js → getMachineId-darwin-5L2D25AD.js} +4 -7
  52. package/dist/{getMachineId-darwin-2XNOCCJQ.js.map → getMachineId-darwin-5L2D25AD.js.map} +1 -1
  53. package/dist/{getMachineId-linux-V6YSQEY7.js → getMachineId-linux-KJR4P5HN.js} +3 -6
  54. package/dist/{getMachineId-linux-V6YSQEY7.js.map → getMachineId-linux-KJR4P5HN.js.map} +1 -1
  55. package/dist/{getMachineId-unsupported-4FKBJNVO.js → getMachineId-unsupported-NDNXDYDY.js} +3 -6
  56. package/dist/{getMachineId-unsupported-4FKBJNVO.js.map → getMachineId-unsupported-NDNXDYDY.js.map} +1 -1
  57. package/dist/{getMachineId-win-WLRZBKVG.js → getMachineId-win-T7PJNJXG.js} +4 -7
  58. package/dist/{getMachineId-win-WLRZBKVG.js.map → getMachineId-win-T7PJNJXG.js.map} +1 -1
  59. package/dist/index.cjs +565 -463
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.d.cts +93 -4
  62. package/dist/index.d.ts +93 -4
  63. package/dist/index.js +286 -702
  64. package/dist/index.js.map +1 -1
  65. package/dist/{monorepo-YILKGQXQ.js → monorepo-N5Z63XP7.js} +4 -4
  66. package/dist/{source-map-uploader-3GWUQDTS.js → source-map-uploader-26QPRSCG.js} +5 -4
  67. package/dist/source-map-uploader-26QPRSCG.js.map +1 -0
  68. package/package.json +1 -1
  69. package/dist/chunk-2LDBR3F3.js.map +0 -1
  70. package/dist/chunk-A2AZL6MZ.js.map +0 -1
  71. package/dist/chunk-BGZ7J74D.js.map +0 -1
  72. package/dist/chunk-UPS5BGER.js +0 -182
  73. package/dist/chunk-UPS5BGER.js.map +0 -1
  74. package/dist/chunk-WK7MPK2T.js.map +0 -1
  75. package/dist/chunk-YMEXDDTA.js.map +0 -1
  76. /package/dist/{esm-MDK7CZID.js.map → chunk-NSBPE2FW.js.map} +0 -0
  77. /package/dist/{monorepo-YILKGQXQ.js.map → esm-KBPHCVB4.js.map} +0 -0
  78. /package/dist/{source-map-uploader-3GWUQDTS.js.map → monorepo-N5Z63XP7.js.map} +0 -0
@@ -1,13 +1,9 @@
1
1
  import {
2
2
  AnonApiKeySchema,
3
3
  createAnonApiKey
4
- } from "./chunk-YMEXDDTA.js";
5
- import {
6
- init_esm_shims
7
- } from "./chunk-BGZ7J74D.js";
4
+ } from "./chunk-GSGX76Q5.js";
8
5
 
9
6
  // src/anon-key.ts
10
- init_esm_shims();
11
7
  var GLASSTRACE_DIR = ".glasstrace";
12
8
  var ANON_KEY_FILE = "anon_key";
13
9
  var fsPathCache;
@@ -15,8 +11,8 @@ async function loadFsPath() {
15
11
  if (fsPathCache !== void 0) return fsPathCache;
16
12
  try {
17
13
  const [fs, path] = await Promise.all([
18
- import("fs/promises"),
19
- import("path")
14
+ import("node:fs/promises"),
15
+ import("node:path")
20
16
  ]);
21
17
  fsPathCache = { fs, path };
22
18
  return fsPathCache;
@@ -99,4 +95,4 @@ export {
99
95
  readAnonKey,
100
96
  getOrCreateAnonKey
101
97
  };
102
- //# sourceMappingURL=chunk-ECEN724Y.js.map
98
+ //# sourceMappingURL=chunk-J5BW7V2D.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/anon-key.ts"],"sourcesContent":["import { AnonApiKeySchema, createAnonApiKey } from \"@glasstrace/protocol\";\nimport type { AnonApiKey } from \"@glasstrace/protocol\";\n\nconst GLASSTRACE_DIR = \".glasstrace\";\nconst ANON_KEY_FILE = \"anon_key\";\n\n/**\n * Lazily imports `node:fs/promises` and `node:path`. Returns `null` if\n * the modules are unavailable (non-Node environments). The result is\n * cached after first resolution.\n */\nlet fsPathCache: { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") } | null | undefined;\n\nasync function loadFsPath(): Promise<{ fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") } | null> {\n if (fsPathCache !== undefined) return fsPathCache;\n try {\n const [fs, path] = await Promise.all([\n import(\"node:fs/promises\"),\n import(\"node:path\"),\n ]);\n fsPathCache = { fs, path };\n return fsPathCache;\n } catch {\n fsPathCache = null;\n return null;\n }\n}\n\n/**\n * In-memory cache for ephemeral keys when filesystem persistence fails.\n * Keyed by resolved project root to support multiple roots in tests.\n */\nconst ephemeralKeyCache = new Map<string, AnonApiKey>();\n\n/**\n * Reads an existing anonymous key from the filesystem.\n * Returns the key if valid, or null if:\n * - The file does not exist\n * - The file content is invalid\n * - An I/O error occurs\n * - `node:fs` is unavailable (non-Node environment)\n */\nexport async function readAnonKey(projectRoot?: string): Promise<AnonApiKey | null> {\n const root = projectRoot ?? process.cwd();\n\n const modules = await loadFsPath();\n if (modules) {\n const keyPath = modules.path.join(root, GLASSTRACE_DIR, ANON_KEY_FILE);\n try {\n const content = await modules.fs.readFile(keyPath, \"utf-8\");\n const result = AnonApiKeySchema.safeParse(content);\n if (result.success) {\n return result.data;\n }\n } catch {\n // Fall through to check ephemeral cache\n }\n }\n\n // Check in-memory cache (used when filesystem persistence failed\n // or when node:fs is unavailable)\n const cached = ephemeralKeyCache.get(root);\n if (cached !== undefined) {\n return cached;\n }\n\n return null;\n}\n\n/**\n * Gets an existing anonymous key from the filesystem, or creates a new one.\n *\n * - If file exists and contains a valid key, returns it\n * - If file does not exist or content is invalid, generates a new key via createAnonApiKey()\n * - Writes the new key to `.glasstrace/anon_key`, creating the directory if needed\n * - On file write failure: logs a warning, caches an ephemeral in-memory key so\n * repeated calls in the same process return the same key\n * - In non-Node environments: returns an ephemeral in-memory key\n */\nexport async function getOrCreateAnonKey(projectRoot?: string): Promise<AnonApiKey> {\n const root = projectRoot ?? process.cwd();\n\n // Try reading existing key from filesystem\n const existingKey = await readAnonKey(root);\n if (existingKey !== null) {\n return existingKey;\n }\n\n // Check in-memory cache (used when filesystem is unavailable)\n const cached = ephemeralKeyCache.get(root);\n if (cached !== undefined) {\n return cached;\n }\n\n // Generate a new key\n const newKey = createAnonApiKey();\n\n // Attempt filesystem persistence (only in Node.js environments)\n const modules = await loadFsPath();\n if (!modules) {\n // No filesystem access — cache in memory\n ephemeralKeyCache.set(root, newKey);\n return newKey;\n }\n\n const dirPath = modules.path.join(root, GLASSTRACE_DIR);\n const keyPath = modules.path.join(dirPath, ANON_KEY_FILE);\n\n // Persist to filesystem using atomic create-or-fail (O_CREAT | O_EXCL)\n // to prevent TOCTOU races where concurrent cold starts both generate keys.\n try {\n await modules.fs.mkdir(dirPath, { recursive: true, mode: 0o700 });\n await modules.fs.writeFile(keyPath, newKey, { flag: \"wx\", mode: 0o600 });\n return newKey;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"EEXIST\") {\n // Another process won the race. Retry reading their key with\n // short delays — the winner's writeFile is atomic for small\n // payloads but the filesystem may not have flushed yet.\n for (let attempt = 0; attempt < 3; attempt++) {\n const winnerKey = await readAnonKey(root);\n if (winnerKey !== null) {\n return winnerKey;\n }\n // Short delay before next retry (50ms), skip after final attempt\n if (attempt < 2) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n }\n // All retries exhausted — overwrite as last resort.\n // Use explicit chmod after overwrite since writeFile mode only\n // applies on creation on some platforms.\n try {\n await modules.fs.writeFile(keyPath, newKey, { mode: 0o600 });\n await modules.fs.chmod(keyPath, 0o600);\n return newKey;\n } catch {\n // Overwrite failed — fall through to ephemeral cache\n }\n }\n\n // Non-EEXIST error (EACCES, ENOTDIR, etc.) — cache in memory so\n // repeated calls get the same ephemeral key within this process.\n ephemeralKeyCache.set(root, newKey);\n console.warn(\n `[glasstrace] Failed to persist anonymous key to ${keyPath}: ${err instanceof Error ? err.message : String(err)}. Using ephemeral key.`,\n );\n return newKey;\n }\n}\n"],"mappings":";;;;;;;;;AAAA;AAGA,IAAM,iBAAiB;AACvB,IAAM,gBAAgB;AAOtB,IAAI;AAEJ,eAAe,aAA0G;AACvH,MAAI,gBAAgB,OAAW,QAAO;AACtC,MAAI;AACF,UAAM,CAAC,IAAI,IAAI,IAAI,MAAM,QAAQ,IAAI;AAAA,MACnC,OAAO,aAAkB;AAAA,MACzB,OAAO,MAAW;AAAA,IACpB,CAAC;AACD,kBAAc,EAAE,IAAI,KAAK;AACzB,WAAO;AAAA,EACT,QAAQ;AACN,kBAAc;AACd,WAAO;AAAA,EACT;AACF;AAMA,IAAM,oBAAoB,oBAAI,IAAwB;AAUtD,eAAsB,YAAY,aAAkD;AAClF,QAAM,OAAO,eAAe,QAAQ,IAAI;AAExC,QAAM,UAAU,MAAM,WAAW;AACjC,MAAI,SAAS;AACX,UAAM,UAAU,QAAQ,KAAK,KAAK,MAAM,gBAAgB,aAAa;AACrE,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,GAAG,SAAS,SAAS,OAAO;AAC1D,YAAM,SAAS,iBAAiB,UAAU,OAAO;AACjD,UAAI,OAAO,SAAS;AAClB,eAAO,OAAO;AAAA,MAChB;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAIA,QAAM,SAAS,kBAAkB,IAAI,IAAI;AACzC,MAAI,WAAW,QAAW;AACxB,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAYA,eAAsB,mBAAmB,aAA2C;AAClF,QAAM,OAAO,eAAe,QAAQ,IAAI;AAGxC,QAAM,cAAc,MAAM,YAAY,IAAI;AAC1C,MAAI,gBAAgB,MAAM;AACxB,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,kBAAkB,IAAI,IAAI;AACzC,MAAI,WAAW,QAAW;AACxB,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,iBAAiB;AAGhC,QAAM,UAAU,MAAM,WAAW;AACjC,MAAI,CAAC,SAAS;AAEZ,sBAAkB,IAAI,MAAM,MAAM;AAClC,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,QAAQ,KAAK,KAAK,MAAM,cAAc;AACtD,QAAM,UAAU,QAAQ,KAAK,KAAK,SAAS,aAAa;AAIxD,MAAI;AACF,UAAM,QAAQ,GAAG,MAAM,SAAS,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAChE,UAAM,QAAQ,GAAG,UAAU,SAAS,QAAQ,EAAE,MAAM,MAAM,MAAM,IAAM,CAAC;AACvE,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,UAAM,OAAQ,IAA8B;AAC5C,QAAI,SAAS,UAAU;AAIrB,eAAS,UAAU,GAAG,UAAU,GAAG,WAAW;AAC5C,cAAM,YAAY,MAAM,YAAY,IAAI;AACxC,YAAI,cAAc,MAAM;AACtB,iBAAO;AAAA,QACT;AAEA,YAAI,UAAU,GAAG;AACf,gBAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,QACxD;AAAA,MACF;AAIA,UAAI;AACF,cAAM,QAAQ,GAAG,UAAU,SAAS,QAAQ,EAAE,MAAM,IAAM,CAAC;AAC3D,cAAM,QAAQ,GAAG,MAAM,SAAS,GAAK;AACrC,eAAO;AAAA,MACT,QAAQ;AAAA,MAER;AAAA,IACF;AAIA,sBAAkB,IAAI,MAAM,MAAM;AAClC,YAAQ;AAAA,MACN,mDAAmD,OAAO,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IACjH;AACA,WAAO;AAAA,EACT;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/anon-key.ts"],"sourcesContent":["import { AnonApiKeySchema, createAnonApiKey } from \"@glasstrace/protocol\";\nimport type { AnonApiKey } from \"@glasstrace/protocol\";\n\nconst GLASSTRACE_DIR = \".glasstrace\";\nconst ANON_KEY_FILE = \"anon_key\";\n\n/**\n * Lazily imports `node:fs/promises` and `node:path`. Returns `null` if\n * the modules are unavailable (non-Node environments). The result is\n * cached after first resolution.\n */\nlet fsPathCache: { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") } | null | undefined;\n\nasync function loadFsPath(): Promise<{ fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") } | null> {\n if (fsPathCache !== undefined) return fsPathCache;\n try {\n const [fs, path] = await Promise.all([\n import(\"node:fs/promises\"),\n import(\"node:path\"),\n ]);\n fsPathCache = { fs, path };\n return fsPathCache;\n } catch {\n fsPathCache = null;\n return null;\n }\n}\n\n/**\n * In-memory cache for ephemeral keys when filesystem persistence fails.\n * Keyed by resolved project root to support multiple roots in tests.\n */\nconst ephemeralKeyCache = new Map<string, AnonApiKey>();\n\n/**\n * Reads an existing anonymous key from the filesystem.\n * Returns the key if valid, or null if:\n * - The file does not exist\n * - The file content is invalid\n * - An I/O error occurs\n * - `node:fs` is unavailable (non-Node environment)\n */\nexport async function readAnonKey(projectRoot?: string): Promise<AnonApiKey | null> {\n const root = projectRoot ?? process.cwd();\n\n const modules = await loadFsPath();\n if (modules) {\n const keyPath = modules.path.join(root, GLASSTRACE_DIR, ANON_KEY_FILE);\n try {\n const content = await modules.fs.readFile(keyPath, \"utf-8\");\n const result = AnonApiKeySchema.safeParse(content);\n if (result.success) {\n return result.data;\n }\n } catch {\n // Fall through to check ephemeral cache\n }\n }\n\n // Check in-memory cache (used when filesystem persistence failed\n // or when node:fs is unavailable)\n const cached = ephemeralKeyCache.get(root);\n if (cached !== undefined) {\n return cached;\n }\n\n return null;\n}\n\n/**\n * Gets an existing anonymous key from the filesystem, or creates a new one.\n *\n * - If file exists and contains a valid key, returns it\n * - If file does not exist or content is invalid, generates a new key via createAnonApiKey()\n * - Writes the new key to `.glasstrace/anon_key`, creating the directory if needed\n * - On file write failure: logs a warning, caches an ephemeral in-memory key so\n * repeated calls in the same process return the same key\n * - In non-Node environments: returns an ephemeral in-memory key\n */\nexport async function getOrCreateAnonKey(projectRoot?: string): Promise<AnonApiKey> {\n const root = projectRoot ?? process.cwd();\n\n // Try reading existing key from filesystem\n const existingKey = await readAnonKey(root);\n if (existingKey !== null) {\n return existingKey;\n }\n\n // Check in-memory cache (used when filesystem is unavailable)\n const cached = ephemeralKeyCache.get(root);\n if (cached !== undefined) {\n return cached;\n }\n\n // Generate a new key\n const newKey = createAnonApiKey();\n\n // Attempt filesystem persistence (only in Node.js environments)\n const modules = await loadFsPath();\n if (!modules) {\n // No filesystem access — cache in memory\n ephemeralKeyCache.set(root, newKey);\n return newKey;\n }\n\n const dirPath = modules.path.join(root, GLASSTRACE_DIR);\n const keyPath = modules.path.join(dirPath, ANON_KEY_FILE);\n\n // Persist to filesystem using atomic create-or-fail (O_CREAT | O_EXCL)\n // to prevent TOCTOU races where concurrent cold starts both generate keys.\n try {\n await modules.fs.mkdir(dirPath, { recursive: true, mode: 0o700 });\n await modules.fs.writeFile(keyPath, newKey, { flag: \"wx\", mode: 0o600 });\n return newKey;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"EEXIST\") {\n // Another process won the race. Retry reading their key with\n // short delays — the winner's writeFile is atomic for small\n // payloads but the filesystem may not have flushed yet.\n for (let attempt = 0; attempt < 3; attempt++) {\n const winnerKey = await readAnonKey(root);\n if (winnerKey !== null) {\n return winnerKey;\n }\n // Short delay before next retry (50ms), skip after final attempt\n if (attempt < 2) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n }\n // All retries exhausted — overwrite as last resort.\n // Use explicit chmod after overwrite since writeFile mode only\n // applies on creation on some platforms.\n try {\n await modules.fs.writeFile(keyPath, newKey, { mode: 0o600 });\n await modules.fs.chmod(keyPath, 0o600);\n return newKey;\n } catch {\n // Overwrite failed — fall through to ephemeral cache\n }\n }\n\n // Non-EEXIST error (EACCES, ENOTDIR, etc.) — cache in memory so\n // repeated calls get the same ephemeral key within this process.\n ephemeralKeyCache.set(root, newKey);\n console.warn(\n `[glasstrace] Failed to persist anonymous key to ${keyPath}: ${err instanceof Error ? err.message : String(err)}. Using ephemeral key.`,\n );\n return newKey;\n }\n}\n"],"mappings":";;;;;;AAGA,IAAM,iBAAiB;AACvB,IAAM,gBAAgB;AAOtB,IAAI;AAEJ,eAAe,aAA0G;AACvH,MAAI,gBAAgB,OAAW,QAAO;AACtC,MAAI;AACF,UAAM,CAAC,IAAI,IAAI,IAAI,MAAM,QAAQ,IAAI;AAAA,MACnC,OAAO,kBAAkB;AAAA,MACzB,OAAO,WAAW;AAAA,IACpB,CAAC;AACD,kBAAc,EAAE,IAAI,KAAK;AACzB,WAAO;AAAA,EACT,QAAQ;AACN,kBAAc;AACd,WAAO;AAAA,EACT;AACF;AAMA,IAAM,oBAAoB,oBAAI,IAAwB;AAUtD,eAAsB,YAAY,aAAkD;AAClF,QAAM,OAAO,eAAe,QAAQ,IAAI;AAExC,QAAM,UAAU,MAAM,WAAW;AACjC,MAAI,SAAS;AACX,UAAM,UAAU,QAAQ,KAAK,KAAK,MAAM,gBAAgB,aAAa;AACrE,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,GAAG,SAAS,SAAS,OAAO;AAC1D,YAAM,SAAS,iBAAiB,UAAU,OAAO;AACjD,UAAI,OAAO,SAAS;AAClB,eAAO,OAAO;AAAA,MAChB;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAIA,QAAM,SAAS,kBAAkB,IAAI,IAAI;AACzC,MAAI,WAAW,QAAW;AACxB,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAYA,eAAsB,mBAAmB,aAA2C;AAClF,QAAM,OAAO,eAAe,QAAQ,IAAI;AAGxC,QAAM,cAAc,MAAM,YAAY,IAAI;AAC1C,MAAI,gBAAgB,MAAM;AACxB,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,kBAAkB,IAAI,IAAI;AACzC,MAAI,WAAW,QAAW;AACxB,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,iBAAiB;AAGhC,QAAM,UAAU,MAAM,WAAW;AACjC,MAAI,CAAC,SAAS;AAEZ,sBAAkB,IAAI,MAAM,MAAM;AAClC,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,QAAQ,KAAK,KAAK,MAAM,cAAc;AACtD,QAAM,UAAU,QAAQ,KAAK,KAAK,SAAS,aAAa;AAIxD,MAAI;AACF,UAAM,QAAQ,GAAG,MAAM,SAAS,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAChE,UAAM,QAAQ,GAAG,UAAU,SAAS,QAAQ,EAAE,MAAM,MAAM,MAAM,IAAM,CAAC;AACvE,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,UAAM,OAAQ,IAA8B;AAC5C,QAAI,SAAS,UAAU;AAIrB,eAAS,UAAU,GAAG,UAAU,GAAG,WAAW;AAC5C,cAAM,YAAY,MAAM,YAAY,IAAI;AACxC,YAAI,cAAc,MAAM;AACtB,iBAAO;AAAA,QACT;AAEA,YAAI,UAAU,GAAG;AACf,gBAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,QACxD;AAAA,MACF;AAIA,UAAI;AACF,cAAM,QAAQ,GAAG,UAAU,SAAS,QAAQ,EAAE,MAAM,IAAM,CAAC;AAC3D,cAAM,QAAQ,GAAG,MAAM,SAAS,GAAK;AACrC,eAAO;AAAA,MACT,QAAQ;AAAA,MAER;AAAA,IACF;AAIA,sBAAkB,IAAI,MAAM,MAAM;AAClC,YAAQ;AAAA,MACN,mDAAmD,OAAO,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IACjH;AACA,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,31 +1,17 @@
1
1
  var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
2
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
3
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
4
  }) : x)(function(x) {
6
5
  if (typeof require !== "undefined") return require.apply(this, arguments);
7
6
  throw Error('Dynamic require of "' + x + '" is not supported');
8
7
  });
9
- var __esm = (fn, res) => function __init() {
10
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
- };
12
8
  var __export = (target, all) => {
13
9
  for (var name in all)
14
10
  __defProp(target, name, { get: all[name], enumerable: true });
15
11
  };
16
12
 
17
- // ../../node_modules/tsup/assets/esm_shims.js
18
- import path from "path";
19
- import { fileURLToPath } from "url";
20
- var init_esm_shims = __esm({
21
- "../../node_modules/tsup/assets/esm_shims.js"() {
22
- "use strict";
23
- }
24
- });
25
-
26
13
  export {
27
14
  __require,
28
- __export,
29
- init_esm_shims
15
+ __export
30
16
  };
31
- //# sourceMappingURL=chunk-BGZ7J74D.js.map
17
+ //# sourceMappingURL=chunk-NSBPE2FW.js.map
@@ -1,15 +1,11 @@
1
1
  import {
2
2
  NEXT_CONFIG_NAMES
3
- } from "./chunk-BL3YDC6V.js";
4
- import {
5
- init_esm_shims
6
- } from "./chunk-BGZ7J74D.js";
3
+ } from "./chunk-DXRZKKSO.js";
7
4
 
8
5
  // src/cli/scaffolder.ts
9
- init_esm_shims();
10
- import { createHash } from "crypto";
11
- import * as fs from "fs";
12
- import * as path from "path";
6
+ import { createHash } from "node:crypto";
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
13
9
  function identityFingerprint(token) {
14
10
  return `sha256:${createHash("sha256").update(token).digest("hex")}`;
15
11
  }
@@ -71,8 +67,150 @@ ${indent}registerGlasstrace();
71
67
  }
72
68
  return { injected: true, content: modified };
73
69
  }
74
- async function scaffoldInstrumentation(projectRoot) {
75
- const filePath = path.join(projectRoot, "instrumentation.ts");
70
+ var INSTRUMENTATION_FILENAMES = [
71
+ "instrumentation.ts",
72
+ "instrumentation.js",
73
+ "instrumentation.mjs"
74
+ ];
75
+ function resolveInstrumentationTarget(projectRoot) {
76
+ const rootExisting = [];
77
+ const srcExisting = [];
78
+ for (const name of INSTRUMENTATION_FILENAMES) {
79
+ const rootPath = path.join(projectRoot, name);
80
+ if (isRegularFile(rootPath)) {
81
+ rootExisting.push(rootPath);
82
+ }
83
+ const srcPath = path.join(projectRoot, "src", name);
84
+ if (isRegularFile(srcPath)) {
85
+ srcExisting.push(srcPath);
86
+ }
87
+ }
88
+ const existing = [...rootExisting, ...srcExisting];
89
+ if (rootExisting.length > 0 && srcExisting.length > 0) {
90
+ return {
91
+ target: null,
92
+ layout: null,
93
+ existing,
94
+ rootExisting,
95
+ srcExisting,
96
+ conflict: true
97
+ };
98
+ }
99
+ if (srcExisting.length > 0) {
100
+ return {
101
+ target: srcExisting[0],
102
+ layout: "src",
103
+ existing,
104
+ rootExisting,
105
+ srcExisting,
106
+ conflict: false
107
+ };
108
+ }
109
+ if (rootExisting.length > 0) {
110
+ return {
111
+ target: rootExisting[0],
112
+ layout: "root",
113
+ existing,
114
+ rootExisting,
115
+ srcExisting,
116
+ conflict: false
117
+ };
118
+ }
119
+ const srcDir = path.join(projectRoot, "src");
120
+ const layout = isDirectory(srcDir) ? "src" : "root";
121
+ const target = layout === "src" ? path.join(projectRoot, "src", "instrumentation.ts") : path.join(projectRoot, "instrumentation.ts");
122
+ return {
123
+ target,
124
+ layout,
125
+ existing,
126
+ rootExisting,
127
+ srcExisting,
128
+ conflict: false
129
+ };
130
+ }
131
+ function isDirectory(p) {
132
+ try {
133
+ return fs.statSync(p).isDirectory();
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+ function isRegularFile(p) {
139
+ try {
140
+ const stat = fs.lstatSync(p);
141
+ if (stat.isSymbolicLink()) {
142
+ return fs.statSync(p).isFile();
143
+ }
144
+ return stat.isFile();
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+ function appendRegisterFunction(content) {
150
+ const importLine = 'import { registerGlasstrace } from "@glasstrace/sdk";\n';
151
+ const functionBlock = "\nexport async function register() {\n // Glasstrace must be registered before Prisma instrumentation\n // to ensure all ORM spans are captured correctly.\n // If you use @prisma/instrumentation, import it after this call.\n registerGlasstrace();\n}\n";
152
+ let withImport = content;
153
+ const hasGlasstraceImport = content.includes("@glasstrace/sdk");
154
+ if (!hasGlasstraceImport) {
155
+ withImport = importLine + content;
156
+ } else {
157
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']@glasstrace\/sdk["']/;
158
+ const importMatch = importRegex.exec(content);
159
+ if (importMatch) {
160
+ const specifiers = importMatch[1];
161
+ const alreadyImported = specifiers.split(",").some((s) => s.trim() === "registerGlasstrace");
162
+ if (!alreadyImported) {
163
+ const existingImports = specifiers.trimEnd();
164
+ const separator = existingImports.endsWith(",") ? " " : ", ";
165
+ const updatedImport = `import { ${existingImports.trim()}${separator}registerGlasstrace } from "@glasstrace/sdk"`;
166
+ withImport = content.replace(importMatch[0], updatedImport);
167
+ }
168
+ } else {
169
+ withImport = importLine + content;
170
+ }
171
+ }
172
+ const trailingNewline = withImport.endsWith("\n") ? "" : "\n";
173
+ return withImport + trailingNewline + functionBlock;
174
+ }
175
+ async function defaultInstrumentationPrompt(question, defaultValue) {
176
+ if (!process.stdin.isTTY) return defaultValue;
177
+ const readline = await import("node:readline");
178
+ const rl = readline.createInterface({
179
+ input: process.stdin,
180
+ output: process.stdout
181
+ });
182
+ return new Promise((resolve) => {
183
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
184
+ rl.question(question + suffix, (answer) => {
185
+ rl.close();
186
+ const trimmed = answer.trim().toLowerCase();
187
+ if (trimmed === "") {
188
+ resolve(defaultValue);
189
+ return;
190
+ }
191
+ resolve(trimmed === "y" || trimmed === "yes");
192
+ });
193
+ });
194
+ }
195
+ async function scaffoldInstrumentation(projectRoot, options = {}) {
196
+ const target = resolveInstrumentationTarget(projectRoot);
197
+ if (target.conflict) {
198
+ return {
199
+ action: "conflict",
200
+ // Point the user at the `src/` variant — modern Next.js apps with a
201
+ // `src/` directory load from there, so that's the merge target. The
202
+ // competing path is reported separately for the error message.
203
+ filePath: target.srcExisting[0],
204
+ conflictingPath: target.rootExisting[0]
205
+ };
206
+ }
207
+ const filePath = target.target;
208
+ const layout = target.layout;
209
+ if (filePath === null || layout === null) {
210
+ return { action: "unrecognized" };
211
+ }
212
+ const force = options.force === true;
213
+ const prompt = options.prompt ?? defaultInstrumentationPrompt;
76
214
  if (!fs.existsSync(filePath)) {
77
215
  const content = `import { registerGlasstrace } from "@glasstrace/sdk";
78
216
 
@@ -83,19 +221,31 @@ export async function register() {
83
221
  registerGlasstrace();
84
222
  }
85
223
  `;
224
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
86
225
  fs.writeFileSync(filePath, content, "utf-8");
87
- return { action: "created" };
226
+ return { action: "created", filePath, layout };
88
227
  }
89
228
  const existing = fs.readFileSync(filePath, "utf-8");
90
229
  if (hasRegisterGlasstraceCall(existing)) {
91
- return { action: "already-registered" };
230
+ return { action: "already-registered", filePath, layout };
231
+ }
232
+ if (!force) {
233
+ const approved = await prompt(
234
+ `Merge registerGlasstrace() into ${path.relative(projectRoot, filePath)}?`,
235
+ false
236
+ );
237
+ if (!approved) {
238
+ return { action: "skipped", filePath, layout };
239
+ }
92
240
  }
93
- const result = injectRegisterGlasstrace(existing);
94
- if (result.injected) {
95
- fs.writeFileSync(filePath, result.content, "utf-8");
96
- return { action: "injected" };
241
+ const injectResult = injectRegisterGlasstrace(existing);
242
+ if (injectResult.injected) {
243
+ fs.writeFileSync(filePath, injectResult.content, "utf-8");
244
+ return { action: "injected", filePath, layout };
97
245
  }
98
- return { action: "unrecognized" };
246
+ const appended = appendRegisterFunction(existing);
247
+ fs.writeFileSync(filePath, appended, "utf-8");
248
+ return { action: "appended", filePath, layout };
99
249
  }
100
250
  async function scaffoldNextConfig(projectRoot) {
101
251
  let configPath;
@@ -296,6 +446,7 @@ async function scaffoldMcpMarker(projectRoot, anonKey) {
296
446
  }
297
447
 
298
448
  export {
449
+ resolveInstrumentationTarget,
299
450
  scaffoldInstrumentation,
300
451
  scaffoldNextConfig,
301
452
  readEnvLocalApiKey,
@@ -306,4 +457,4 @@ export {
306
457
  mcpConfigMatches,
307
458
  scaffoldMcpMarker
308
459
  };
309
- //# sourceMappingURL=chunk-A2AZL6MZ.js.map
460
+ //# sourceMappingURL=chunk-O63DJKIJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/scaffolder.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { NEXT_CONFIG_NAMES } from \"./constants.js\";\n\n/**\n * Computes a stable identity fingerprint for deduplication purposes.\n * This is NOT password hashing — the input is an opaque token used\n * as a marker identity, not a credential stored for authentication.\n *\n * @internal Exported for unit testing only.\n */\nexport function identityFingerprint(token: string): string {\n return `sha256:${createHash(\"sha256\").update(token).digest(\"hex\")}`;\n}\n\n/**\n * Checks whether `content` contains a real (non-commented) `registerGlasstrace()` call.\n *\n * Strips single-line `// ...` comments before matching so that\n * `// registerGlasstrace()` is not treated as a real invocation.\n * Block comments are not stripped — block-commenting a function call\n * while keeping it syntactically valid is extremely unlikely in practice.\n *\n * @internal Exported for unit testing only.\n */\nexport function hasRegisterGlasstraceCall(content: string): boolean {\n return content.split(\"\\n\").some((line) => {\n const uncommented = line.replace(/\\/\\/.*$/, \"\");\n return /\\bregisterGlasstrace\\s*\\(/.test(uncommented);\n });\n}\n\n/** Result of attempting to inject registerGlasstrace into an existing file. */\nexport interface InjectResult {\n content: string;\n injected: boolean;\n}\n\n/** Result of the instrumentation.ts scaffolding step. */\nexport type InstrumentationAction =\n | \"created\"\n | \"injected\"\n | \"appended\"\n | \"already-registered\"\n | \"skipped\"\n | \"unrecognized\"\n | \"conflict\";\n\n/**\n * Layout detected by {@link resolveInstrumentationTarget}. `root` means\n * the project has no `src/` directory so instrumentation lives at the\n * project root; `src` means Next.js expects `src/instrumentation.ts`.\n */\nexport type InstrumentationLayout = \"root\" | \"src\";\n\n/** Structured result from scaffoldInstrumentation. */\nexport interface ScaffoldInstrumentationResult {\n action: InstrumentationAction;\n /**\n * The instrumentation file path this scaffold step targeted. Absolute\n * path (e.g., `/abs/project/instrumentation.ts` or\n * `/abs/project/src/instrumentation.ts`). Present for every successful\n * action so callers (init.ts summary lines, rollback state) can report\n * and unwind without re-detecting the layout.\n *\n * For `\"conflict\"`, this is the recommended merge target — the `src/`\n * variant when both exist, because that's where Next.js loads from on\n * modern `src/`-layout projects. The competing path is available via\n * {@link ScaffoldInstrumentationResult.conflictingPath}.\n */\n filePath?: string;\n /** Layout the resolver chose. Always set for non-conflict actions. */\n layout?: InstrumentationLayout;\n /**\n * When `action === \"conflict\"`, the path of the other instrumentation\n * file whose presence Next.js treats as undefined. The recommended merge\n * target is {@link ScaffoldInstrumentationResult.filePath}.\n */\n conflictingPath?: string;\n}\n\n/** Options for {@link scaffoldInstrumentation}. */\nexport interface ScaffoldInstrumentationOptions {\n /**\n * When `true`, skip the merge-confirmation prompt and write changes\n * without asking. Matches the DISC-1247 Scenario 2c `--force` behavior\n * already used for MCP config overwrites. Defaults to `false`.\n */\n force?: boolean;\n /**\n * Interactive prompt callback. When omitted, `scaffoldInstrumentation`\n * uses a TTY readline prompt and defaults to `false` (skip the change)\n * in non-interactive shells — the same pattern `decideMcpConfigAction`\n * follows. Exposed for testing.\n */\n prompt?: (question: string, defaultValue: boolean) => Promise<boolean>;\n}\n\n/** Result of attempting to wrap next.config with withGlasstraceConfig. */\nexport interface ScaffoldNextConfigResult {\n modified: boolean;\n reason?: \"already-wrapped\" | \"empty-file\" | \"no-export\";\n}\n\n/**\n * Injects `registerGlasstrace()` into an existing instrumentation.ts file.\n *\n * Strategy:\n * 1. If the file already contains a real `registerGlasstrace()` call — no-op\n * (commented-out calls are ignored)\n * 2. Find `export [async] function register()` pattern\n * 3. Add `import { registerGlasstrace } from \"@glasstrace/sdk\"` at top\n * (or extend existing `@glasstrace/sdk` import, skipping if already imported)\n * 4. Insert `registerGlasstrace()` as the first statement in the function body\n *\n * @param content - The existing file content\n * @returns The modified content if injection succeeded, or the original content\n * with `injected: false` if the pattern was not recognized\n */\nexport function injectRegisterGlasstrace(content: string): InjectResult {\n // Already has a registerGlasstrace() call — no-op.\n // Uses a helper that strips single-line comments before matching\n // so that `// registerGlasstrace()` is not treated as a real call.\n if (hasRegisterGlasstraceCall(content)) {\n return { injected: false, content };\n }\n\n // Find the register() function: export [async] function register(...) {\n const registerFnRegex = /export\\s+(?:async\\s+)?function\\s+register\\s*\\([^)]*\\)\\s*\\{/;\n const match = registerFnRegex.exec(content);\n\n if (!match) {\n return { injected: false, content };\n }\n\n // Determine indentation from the function body by looking at the first\n // indented line after the opening brace. Only capture spaces and tabs\n // (not newlines) to avoid blank lines corrupting the detected indent.\n // Default to 2-space indent (matches the scaffolded template).\n const afterBrace = content.slice(match.index + match[0].length);\n const indentMatch = /\\n([ \\t]+)/.exec(afterBrace);\n const indent = indentMatch ? indentMatch[1] : \" \";\n\n // Build the import line\n const importLine = 'import { registerGlasstrace } from \"@glasstrace/sdk\";\\n';\n\n // Check if the file already imports from @glasstrace/sdk\n const hasGlasstraceImport = content.includes(\"@glasstrace/sdk\");\n\n // Insert registerGlasstrace() as the first statement in the function body\n const insertPoint = match.index + match[0].length;\n const callInjection = `\\n${indent}// Glasstrace must be registered before other instrumentation\\n${indent}registerGlasstrace();\\n`;\n\n let modified: string;\n if (hasGlasstraceImport) {\n // File already imports from @glasstrace/sdk — check whether registerGlasstrace\n // is already among the specifiers to avoid producing a duplicate like\n // `import { registerGlasstrace, registerGlasstrace }`.\n const importRegex = /import\\s*\\{([^}]+)\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']/;\n const importMatch = importRegex.exec(content);\n if (importMatch) {\n const specifiers = importMatch[1];\n const alreadyImported = specifiers\n .split(\",\")\n .some((s) => s.trim() === \"registerGlasstrace\");\n\n if (alreadyImported) {\n // Import already has registerGlasstrace — only inject the call\n modified = content.slice(0, insertPoint) + callInjection + content.slice(insertPoint);\n } else {\n // Add registerGlasstrace to existing import specifiers\n const existingImports = specifiers.trimEnd();\n const separator = existingImports.endsWith(\",\") ? \" \" : \", \";\n const updatedImport = `import { ${existingImports.trim()}${separator}registerGlasstrace } from \"@glasstrace/sdk\"`;\n modified = content.replace(importMatch[0], updatedImport);\n // Re-find the function in the shifted content and inject the call\n const newMatch = registerFnRegex.exec(modified);\n if (newMatch) {\n const newInsertPoint = newMatch.index + newMatch[0].length;\n modified = modified.slice(0, newInsertPoint) + callInjection + modified.slice(newInsertPoint);\n }\n }\n } else {\n // Non-destructured import (e.g., import * as sdk) — add a separate import\n modified = importLine + content;\n // Re-find the function in the shifted content and inject the call\n const newMatch = registerFnRegex.exec(modified);\n if (newMatch) {\n const newInsertPoint = newMatch.index + newMatch[0].length;\n modified = modified.slice(0, newInsertPoint) + callInjection + modified.slice(newInsertPoint);\n }\n }\n } else {\n // Add import at the top of the file and the call in the function body\n modified = importLine + content.slice(0, insertPoint) + callInjection + content.slice(insertPoint);\n }\n\n return { injected: true, content: modified };\n}\n\n/** Instrumentation filename variants Next.js recognizes, in priority order. */\nconst INSTRUMENTATION_FILENAMES = [\n \"instrumentation.ts\",\n \"instrumentation.js\",\n \"instrumentation.mjs\",\n] as const;\n\n/**\n * Result of {@link resolveInstrumentationTarget}. When the project has no\n * conflict, `target` identifies the file the scaffolder should create or\n * merge into. When both root and `src/` variants already exist, `target`\n * is `null` and both detected paths are returned so the caller can surface\n * a clear error — Next.js's loader behavior is undefined in that state\n * (DISC-493 Issue 1).\n */\nexport interface InstrumentationTarget {\n /** Absolute path of the chosen file, or null when a conflict exists. */\n target: string | null;\n /** Which layout was chosen. Null mirrors `target === null`. */\n layout: InstrumentationLayout | null;\n /**\n * Absolute paths of any existing instrumentation files detected. Includes\n * the chosen `target` when it exists, plus the competing file when there\n * is a conflict. Empty when the project has no instrumentation file yet.\n */\n existing: string[];\n /**\n * Absolute paths of root-level instrumentation files (e.g.,\n * `{projectRoot}/instrumentation.ts`). Tracked separately from `existing`\n * so callers can distinguish root from `src/` without string matching on\n * paths that may legitimately contain `src` elsewhere in their ancestry\n * (e.g., `/home/user/src/project/`).\n */\n rootExisting: string[];\n /**\n * Absolute paths of `src/`-level instrumentation files (e.g.,\n * `{projectRoot}/src/instrumentation.ts`). See `rootExisting` for the\n * rationale for tracking these separately.\n */\n srcExisting: string[];\n /**\n * When both a root and `src/` instrumentation file are present, Next.js's\n * behavior is not defined. The scaffolder refuses to create a third\n * competing file and asks the user to merge manually into the preferred\n * target (the `src/` variant when the project uses `src/` layout).\n */\n conflict: boolean;\n}\n\n/**\n * Detects whether the project uses Next.js's `src/` directory layout and\n * picks the instrumentation file path the scaffolder should create or\n * merge into.\n *\n * Selection rules (DISC-493 Issue 1):\n *\n * 1. Prefer an existing file. If `src/instrumentation.{ts,js,mjs}` exists,\n * it wins; otherwise the root variant wins. This preserves user intent\n * (they already chose a location) and matches what Next.js loads.\n * 2. When no instrumentation file exists yet, use `src/` when the project\n * has a `src/` directory at its root — the common Next.js convention.\n * 3. When both a root and a `src/` instrumentation file exist, return\n * `conflict: true`. Next.js's loader behavior is undefined in that\n * state and scaffolding a third write would mask whichever file Next.js\n * ultimately ignores.\n *\n * The resolver is pure: it reads the filesystem but writes nothing and\n * never throws. Callers can invoke it repeatedly (e.g., for validation).\n *\n * @param projectRoot - Absolute path to the project root directory.\n */\nexport function resolveInstrumentationTarget(\n projectRoot: string,\n): InstrumentationTarget {\n const rootExisting: string[] = [];\n const srcExisting: string[] = [];\n\n for (const name of INSTRUMENTATION_FILENAMES) {\n const rootPath = path.join(projectRoot, name);\n if (isRegularFile(rootPath)) {\n rootExisting.push(rootPath);\n }\n const srcPath = path.join(projectRoot, \"src\", name);\n if (isRegularFile(srcPath)) {\n srcExisting.push(srcPath);\n }\n }\n\n const existing = [...rootExisting, ...srcExisting];\n\n // Conflict: a file from each layout exists. Next.js's behavior is\n // undefined (DISC-493 Issue 1). The caller surfaces an error.\n if (rootExisting.length > 0 && srcExisting.length > 0) {\n return {\n target: null,\n layout: null,\n existing,\n rootExisting,\n srcExisting,\n conflict: true,\n };\n }\n\n // Prefer whichever layout already has an instrumentation file — the\n // user has already committed to that location and Next.js loads from it.\n if (srcExisting.length > 0) {\n return {\n target: srcExisting[0],\n layout: \"src\",\n existing,\n rootExisting,\n srcExisting,\n conflict: false,\n };\n }\n if (rootExisting.length > 0) {\n return {\n target: rootExisting[0],\n layout: \"root\",\n existing,\n rootExisting,\n srcExisting,\n conflict: false,\n };\n }\n\n // No file exists yet — default to `src/` when a `src/` directory is\n // present. Many Next.js apps use this layout and the bug in\n // DISC-493 was scaffolding to the root when Next.js ignores it.\n const srcDir = path.join(projectRoot, \"src\");\n const layout: InstrumentationLayout = isDirectory(srcDir) ? \"src\" : \"root\";\n const target = layout === \"src\"\n ? path.join(projectRoot, \"src\", \"instrumentation.ts\")\n : path.join(projectRoot, \"instrumentation.ts\");\n\n return {\n target,\n layout,\n existing,\n rootExisting,\n srcExisting,\n conflict: false,\n };\n}\n\n/** Returns true when `p` is a directory. Never throws. */\nfunction isDirectory(p: string): boolean {\n try {\n return fs.statSync(p).isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Returns true when `p` is a regular file (not a directory, not missing).\n * Uses `lstatSync` so symlinks are evaluated as-is — a symlink to a file\n * counts, a symlink to a directory does not. Never throws.\n */\nfunction isRegularFile(p: string): boolean {\n try {\n const stat = fs.lstatSync(p);\n if (stat.isSymbolicLink()) {\n // Follow the symlink so we correctly classify links to real files.\n return fs.statSync(p).isFile();\n }\n return stat.isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Appends a new `export async function register()` to a file that has no\n * recognizable register function. Used when `src/instrumentation.ts`\n * exists (e.g., Sentry scaffolded it with only a top-level side-effect\n * import) but has no register hook yet.\n *\n * @internal Exported for unit testing only.\n */\nexport function appendRegisterFunction(content: string): string {\n const importLine = 'import { registerGlasstrace } from \"@glasstrace/sdk\";\\n';\n const functionBlock =\n \"\\n\" +\n \"export async function register() {\\n\" +\n \" // Glasstrace must be registered before Prisma instrumentation\\n\" +\n \" // to ensure all ORM spans are captured correctly.\\n\" +\n \" // If you use @prisma/instrumentation, import it after this call.\\n\" +\n \" registerGlasstrace();\\n\" +\n \"}\\n\";\n\n // Avoid a duplicate import if the file already pulls from @glasstrace/sdk.\n // When multiple specifiers are imported, splice registerGlasstrace in\n // rather than adding a second import line (mirrors injectRegisterGlasstrace).\n let withImport = content;\n const hasGlasstraceImport = content.includes(\"@glasstrace/sdk\");\n if (!hasGlasstraceImport) {\n withImport = importLine + content;\n } else {\n const importRegex = /import\\s*\\{([^}]+)\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']/;\n const importMatch = importRegex.exec(content);\n if (importMatch) {\n const specifiers = importMatch[1];\n const alreadyImported = specifiers\n .split(\",\")\n .some((s) => s.trim() === \"registerGlasstrace\");\n if (!alreadyImported) {\n const existingImports = specifiers.trimEnd();\n const separator = existingImports.endsWith(\",\") ? \" \" : \", \";\n const updatedImport = `import { ${existingImports.trim()}${separator}registerGlasstrace } from \"@glasstrace/sdk\"`;\n withImport = content.replace(importMatch[0], updatedImport);\n }\n } else {\n // Non-destructured `import * as sdk from \"@glasstrace/sdk\"` form —\n // add a separate destructured import for registerGlasstrace so we\n // never depend on reading the namespace alias.\n withImport = importLine + content;\n }\n }\n\n // Ensure the file ends with a single newline before appending.\n const trailingNewline = withImport.endsWith(\"\\n\") ? \"\" : \"\\n\";\n return withImport + trailingNewline + functionBlock;\n}\n\n/**\n * Default `confirm`-style prompt used when `scaffoldInstrumentation` is\n * called without a `prompt` callback. Mirrors `init.ts#promptYesNo`:\n * returns the default value when stdin is not a TTY so non-interactive\n * shells do not hang.\n */\nasync function defaultInstrumentationPrompt(\n question: string,\n defaultValue: boolean,\n): Promise<boolean> {\n if (!process.stdin.isTTY) return defaultValue;\n const readline = await import(\"node:readline\");\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n return new Promise<boolean>((resolve) => {\n const suffix = defaultValue ? \" [Y/n] \" : \" [y/N] \";\n rl.question(question + suffix, (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n if (trimmed === \"\") {\n resolve(defaultValue);\n return;\n }\n resolve(trimmed === \"y\" || trimmed === \"yes\");\n });\n });\n}\n\n/**\n * Ensures an instrumentation file exists and contains a `registerGlasstrace()`\n * call, merging into the user's existing file rather than overwriting it.\n *\n * Behavior (DISC-493 Issue 1):\n *\n * - Detects `src/`-layout projects via {@link resolveInstrumentationTarget}\n * and targets `src/instrumentation.ts` instead of the root when a `src/`\n * directory is present.\n * - When both `instrumentation.ts` and `src/instrumentation.ts` already\n * exist, returns `action: \"conflict\"` so the caller can emit a clear\n * error. Next.js's loader behavior is undefined in that state and\n * writing a third file would silently mask the one Next.js ignores.\n * - When the target does not exist, creates it with the standard template.\n * - When the target exists but has no `registerGlasstrace()` call:\n * - If it exposes an `export function register()`, injects the call as\n * the first statement (and imports `registerGlasstrace` if needed).\n * - If it has no register function, appends a new `export async function\n * register()` that calls `registerGlasstrace()` — this matches the\n * Sentry / Datadog / custom-instrumentation case where `register()`\n * hasn't been created yet.\n * - Before either mutation, prompts the user unless `force: true` is\n * passed (DISC-1247 Scenario 2c re-init safety).\n * - When the target already contains `registerGlasstrace()`, returns\n * `action: \"already-registered\"` (idempotent).\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @param options - Prompt and force flags for merge-safe re-init.\n */\nexport async function scaffoldInstrumentation(\n projectRoot: string,\n options: ScaffoldInstrumentationOptions = {},\n): Promise<ScaffoldInstrumentationResult> {\n const target = resolveInstrumentationTarget(projectRoot);\n\n if (target.conflict) {\n return {\n action: \"conflict\",\n // Point the user at the `src/` variant — modern Next.js apps with a\n // `src/` directory load from there, so that's the merge target. The\n // competing path is reported separately for the error message.\n filePath: target.srcExisting[0],\n conflictingPath: target.rootExisting[0],\n };\n }\n\n const filePath = target.target;\n const layout = target.layout;\n // Defensive: resolver always sets these in the non-conflict path.\n if (filePath === null || layout === null) {\n return { action: \"unrecognized\" };\n }\n\n const force = options.force === true;\n const prompt = options.prompt ?? defaultInstrumentationPrompt;\n\n if (!fs.existsSync(filePath)) {\n const content = `import { registerGlasstrace } from \"@glasstrace/sdk\";\n\nexport async function register() {\n // Glasstrace must be registered before Prisma instrumentation\n // to ensure all ORM spans are captured correctly.\n // If you use @prisma/instrumentation, import it after this call.\n registerGlasstrace();\n}\n`;\n // Ensure the target directory exists (e.g., `src/` when the project\n // has the `src/` layout but no src/ folder somehow — defensive).\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n fs.writeFileSync(filePath, content, \"utf-8\");\n return { action: \"created\", filePath, layout };\n }\n\n // File exists — check whether registerGlasstrace() is already called.\n // Uses a helper that strips single-line comments before matching\n // so that `// registerGlasstrace()` is not treated as a real call.\n const existing = fs.readFileSync(filePath, \"utf-8\");\n\n if (hasRegisterGlasstraceCall(existing)) {\n return { action: \"already-registered\", filePath, layout };\n }\n\n // The file is going to change. Before writing, confirm with the user\n // unless --force was passed — otherwise a second init on a custom\n // instrumentation file would silently rewrite it. Non-interactive\n // shells (no TTY) skip the merge by default; pass `force: true` to\n // proceed without prompting, which the CLI does for `--yes`/`--force`.\n if (!force) {\n const approved = await prompt(\n `Merge registerGlasstrace() into ${path.relative(projectRoot, filePath)}?`,\n false,\n );\n if (!approved) {\n return { action: \"skipped\", filePath, layout };\n }\n }\n\n // Attempt injection into the existing register() function first.\n const injectResult = injectRegisterGlasstrace(existing);\n if (injectResult.injected) {\n fs.writeFileSync(filePath, injectResult.content, \"utf-8\");\n return { action: \"injected\", filePath, layout };\n }\n\n // No register() function present — append a fresh one. This is the\n // Sentry/Datadog case where `src/instrumentation.ts` exists with only\n // a top-level import or empty body.\n const appended = appendRegisterFunction(existing);\n fs.writeFileSync(filePath, appended, \"utf-8\");\n return { action: \"appended\", filePath, layout };\n}\n\n/**\n * Detects `next.config.js`, `next.config.ts`, or `next.config.mjs` and wraps\n * with `withGlasstraceConfig()`. If the config already contains\n * `withGlasstraceConfig`, the file is not modified.\n *\n * For CJS `.js` configs, adds a `require()` call and wraps `module.exports`.\n * The SDK ships dual ESM/CJS builds via tsup + conditional exports, so\n * `require(\"@glasstrace/sdk\")` resolves to the CJS entrypoint natively.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns A result object describing what happened, or `null` if no config\n * file was found at all.\n */\nexport async function scaffoldNextConfig(\n projectRoot: string,\n): Promise<ScaffoldNextConfigResult | null> {\n let configPath: string | undefined;\n let configName: string | undefined;\n\n for (const name of NEXT_CONFIG_NAMES) {\n const candidate = path.join(projectRoot, name);\n if (fs.existsSync(candidate)) {\n configPath = candidate;\n configName = name;\n break;\n }\n }\n\n if (configPath === undefined || configName === undefined) {\n return null;\n }\n\n const existing = fs.readFileSync(configPath, \"utf-8\");\n\n // Guard: empty or whitespace-only files have no export to wrap\n if (existing.trim().length === 0) {\n return { modified: false, reason: \"empty-file\" };\n }\n\n // Already wrapped — skip even in force mode to avoid double-wrapping\n if (existing.includes(\"withGlasstraceConfig\")) {\n return { modified: false, reason: \"already-wrapped\" };\n }\n\n const isESM = configName.endsWith(\".ts\") || configName.endsWith(\".mjs\");\n\n if (isESM) {\n // ESM: static import at top of file, wrap the export\n const importLine = 'import { withGlasstraceConfig } from \"@glasstrace/sdk\";\\n';\n const wrapResult = wrapExport(existing);\n if (!wrapResult.wrapped) {\n return { modified: false, reason: \"no-export\" };\n }\n const modified = importLine + \"\\n\" + wrapResult.content;\n fs.writeFileSync(configPath, modified, \"utf-8\");\n return { modified: true };\n }\n\n // CJS (.js): require() the SDK (resolves to the CJS dist build) and\n // wrap the module.exports expression in place — no file renaming needed.\n const requireLine = 'const { withGlasstraceConfig } = require(\"@glasstrace/sdk\");\\n';\n const wrapResult = wrapCJSExport(existing);\n if (!wrapResult.wrapped) {\n return { modified: false, reason: \"no-export\" };\n }\n const modified = requireLine + \"\\n\" + wrapResult.content;\n fs.writeFileSync(configPath, modified, \"utf-8\");\n return { modified: true };\n}\n\n/** @internal Exported for unit testing only. */\nexport interface WrapResult {\n content: string;\n wrapped: boolean;\n}\n\n/**\n * Wraps an ESM `export default` expression with `withGlasstraceConfig()`.\n *\n * Strategy: find the last `export default` in the file. Everything from\n * that statement to EOF is the exported expression. Strip optional trailing\n * semicolons/whitespace and wrap with `withGlasstraceConfig(...)`.\n *\n * @param content - The full file content containing an ESM default export.\n * @returns `{ wrapped: true, content }` on success, or `{ wrapped: false }` if\n * no recognizable export pattern was found (content returned unchanged).\n * @internal Exported for unit testing only.\n */\nexport function wrapExport(content: string): WrapResult {\n // Find the last `export default` — use lastIndexOf for robustness\n const marker = \"export default\";\n const idx = content.lastIndexOf(marker);\n if (idx === -1) {\n return { content, wrapped: false };\n }\n\n const preamble = content.slice(0, idx);\n const exprRaw = content.slice(idx + marker.length);\n // Trim leading whitespace; strip trailing semicolon + whitespace\n const expr = exprRaw.trim().replace(/;?\\s*$/, \"\");\n if (expr.length === 0) {\n return { content, wrapped: false };\n }\n\n return {\n content: preamble + `export default withGlasstraceConfig(${expr});\\n`,\n wrapped: true,\n };\n}\n\n/**\n * Wraps a CJS `module.exports = expr` with `withGlasstraceConfig()`.\n *\n * Strategy: find the last `module.exports =` in the file. Everything from\n * that statement to EOF is the exported expression. Strip optional trailing\n * semicolons/whitespace and wrap with `module.exports = withGlasstraceConfig(...)`.\n *\n * @param content - The full CJS file content containing `module.exports = ...`.\n * @returns `{ wrapped: true, content }` on success, or `{ wrapped: false }` if\n * no recognizable `module.exports` pattern was found (content returned unchanged).\n * @internal Exported for unit testing only.\n */\nexport function wrapCJSExport(content: string): WrapResult {\n const cjsMarker = \"module.exports\";\n const cjsIdx = content.lastIndexOf(cjsMarker);\n if (cjsIdx === -1) {\n return { content, wrapped: false };\n }\n\n const preamble = content.slice(0, cjsIdx);\n const afterMarker = content.slice(cjsIdx + cjsMarker.length);\n const eqMatch = /^\\s*=\\s*/.exec(afterMarker);\n if (!eqMatch) {\n return { content, wrapped: false };\n }\n\n const exprRaw = afterMarker.slice(eqMatch[0].length);\n const expr = exprRaw.trim().replace(/;?\\s*$/, \"\");\n if (expr.length === 0) {\n return { content, wrapped: false };\n }\n\n return {\n content: preamble + `module.exports = withGlasstraceConfig(${expr});\\n`,\n wrapped: true,\n };\n}\n\n/**\n * Extracts the value of `GLASSTRACE_API_KEY` from a `.env.local`-style\n * string. Returns the raw (unquoted) value, or `null` if the key is\n * absent, commented out, or empty.\n *\n * Only uncommented assignments are considered — a `# GLASSTRACE_API_KEY=...`\n * placeholder is treated as if the key is not set.\n *\n * When multiple uncommented assignments are present, the **last**\n * effective value wins — matching typical `.env` override semantics\n * (later lines override earlier ones when loaded by dotenv-style\n * loaders). Placeholder values (empty or `your_key_here`) are skipped\n * so a trailing placeholder does not mask a real earlier value.\n *\n * @internal Exported for unit testing only.\n */\nexport function readEnvLocalApiKey(content: string): string | null {\n let last: string | null = null;\n const regex = /^\\s*GLASSTRACE_API_KEY\\s*=\\s*(.*)$/gm;\n let match: RegExpExecArray | null;\n while ((match = regex.exec(content)) !== null) {\n const raw = match[1].trim();\n if (raw === \"\") continue;\n const unquoted = raw.replace(/^(['\"])(.*)\\1$/, \"$2\");\n if (unquoted === \"\" || unquoted === \"your_key_here\") continue;\n last = unquoted;\n }\n return last;\n}\n\n/**\n * Returns true when the given API key value is a claimed developer key\n * (prefix `gt_dev_`). Defensive against leading/trailing whitespace.\n *\n * @internal Exported for unit testing only.\n */\nexport function isDevApiKey(value: string | null | undefined): boolean {\n if (value === null || value === undefined) return false;\n return value.trim().startsWith(\"gt_dev_\");\n}\n\n/**\n * Creates `.env.local` with `GLASSTRACE_API_KEY=` placeholder, or appends\n * to an existing file if it does not already contain `GLASSTRACE_API_KEY`.\n *\n * Preservation behavior (DISC-1247 Scenario 6): if an existing `.env.local`\n * already defines a developer key (`gt_dev_*`), the file is left untouched\n * so re-running `init` after an account claim does not overwrite the\n * claimed dev key.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns True if the file was created or modified, false if already configured.\n */\nexport async function scaffoldEnvLocal(projectRoot: string): Promise<boolean> {\n const filePath = path.join(projectRoot, \".env.local\");\n\n if (fs.existsSync(filePath)) {\n const existing = fs.readFileSync(filePath, \"utf-8\");\n if (/^\\s*#?\\s*GLASSTRACE_API_KEY\\s*=/m.test(existing)) {\n return false;\n }\n // Append with a newline separator if needed\n const separator = existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n fs.writeFileSync(filePath, existing + separator + \"# GLASSTRACE_API_KEY=your_key_here\\n\", \"utf-8\");\n return true;\n }\n\n fs.writeFileSync(filePath, \"# GLASSTRACE_API_KEY=your_key_here\\n\", \"utf-8\");\n return true;\n}\n\n/**\n * Adds `GLASSTRACE_COVERAGE_MAP=true` to `.env.local`.\n * Creates the file if it does not exist. If the key is already present\n * with a value other than `true`, it is updated in place.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns True if the file was created or modified, false if already set to `true`.\n */\nexport async function addCoverageMapEnv(projectRoot: string): Promise<boolean> {\n const filePath = path.join(projectRoot, \".env.local\");\n\n if (!fs.existsSync(filePath)) {\n fs.writeFileSync(filePath, \"GLASSTRACE_COVERAGE_MAP=true\\n\", \"utf-8\");\n return true;\n }\n\n const existing = fs.readFileSync(filePath, \"utf-8\");\n const keyRegex = /^(\\s*GLASSTRACE_COVERAGE_MAP\\s*=\\s*)(.*)$/m;\n const keyMatch = keyRegex.exec(existing);\n\n if (keyMatch) {\n const currentValue = keyMatch[2].trim();\n if (currentValue === \"true\") {\n // Already set to true — nothing to do\n return false;\n }\n // Key exists but is not `true` — update in place\n const updated = existing.replace(keyRegex, `${keyMatch[1]}true`);\n fs.writeFileSync(filePath, updated, \"utf-8\");\n return true;\n }\n\n const separator = existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n fs.writeFileSync(filePath, existing + separator + \"GLASSTRACE_COVERAGE_MAP=true\\n\", \"utf-8\");\n return true;\n}\n\n/**\n * Adds `.glasstrace/` to `.gitignore`, or creates `.gitignore` if missing.\n * Does not add duplicate entries.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns True if the file was created or modified, false if already configured.\n */\nexport async function scaffoldGitignore(projectRoot: string): Promise<boolean> {\n const filePath = path.join(projectRoot, \".gitignore\");\n\n if (fs.existsSync(filePath)) {\n const existing = fs.readFileSync(filePath, \"utf-8\");\n // Check line-by-line to avoid false positive partial matches\n const lines = existing.split(\"\\n\").map((l) => l.trim());\n if (lines.includes(\".glasstrace/\")) {\n return false;\n }\n const separator = existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n fs.writeFileSync(filePath, existing + separator + \".glasstrace/\\n\", \"utf-8\");\n return true;\n }\n\n fs.writeFileSync(filePath, \".glasstrace/\\n\", \"utf-8\");\n return true;\n}\n\n/**\n * Compares an existing MCP config file against the content init would\n * write. Returns `true` when they are semantically equal (JSON configs\n * are parsed and compared deeply; TOML configs use trimmed string\n * comparison). Returns `false` on parse errors or mismatch.\n *\n * Used by `init` to detect manually-edited MCP configs before\n * overwriting them (DISC-1247 Scenario 2c).\n *\n * @internal Exported for unit testing only.\n */\nexport function mcpConfigMatches(\n existingContent: string,\n expectedContent: string,\n): boolean {\n const trimmedExpected = expectedContent.trim();\n\n // Attempt JSON comparison first — init writes JSON for most agents.\n try {\n const existingParsed: unknown = JSON.parse(existingContent);\n const expectedParsed: unknown = JSON.parse(trimmedExpected);\n return JSON.stringify(canonicalize(existingParsed)) === JSON.stringify(canonicalize(expectedParsed));\n } catch {\n // Fall through to text comparison for TOML and other non-JSON formats.\n }\n\n return existingContent.trim() === trimmedExpected;\n}\n\n/**\n * Sorts object keys recursively to produce a canonical form suitable\n * for structural equality comparison via JSON.stringify.\n */\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n if (value !== null && typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(obj).sort()) {\n sorted[key] = canonicalize(obj[key]);\n }\n return sorted;\n }\n return value;\n}\n\n/**\n * Creates the `.glasstrace/mcp-connected` marker file, or overwrites it\n * if the key has changed (key rotation).\n *\n * The marker file records a SHA-256 fingerprint of the anonymous key and\n * the ISO 8601 timestamp when it was written. It is used by the nudge\n * system to suppress \"MCP not configured\" prompts.\n *\n * If the marker already exists with the same key fingerprint, this is a\n * no-op (the timestamp is NOT refreshed).\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @param anonKey - The anonymous API key to fingerprint.\n * @returns True if the marker was created or updated, false if it already\n * exists with the same key fingerprint.\n */\nexport async function scaffoldMcpMarker(\n projectRoot: string,\n anonKey: string,\n): Promise<boolean> {\n const dirPath = path.join(projectRoot, \".glasstrace\");\n const markerPath = path.join(dirPath, \"mcp-connected\");\n const keyHash = identityFingerprint(anonKey);\n\n // Check if marker already exists with the same key hash\n if (fs.existsSync(markerPath)) {\n try {\n const existing = JSON.parse(fs.readFileSync(markerPath, \"utf-8\")) as {\n keyHash?: string;\n };\n if (existing.keyHash === keyHash) {\n return false;\n }\n } catch {\n // Corrupted marker — overwrite\n }\n }\n\n // Create directory with restricted permissions\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });\n\n const marker = JSON.stringify(\n { keyHash, configuredAt: new Date().toISOString() },\n null,\n 2,\n );\n\n fs.writeFileSync(markerPath, marker, { mode: 0o600 });\n\n // Ensure permissions even if file pre-existed (writeFile mode only\n // applies on creation on some platforms)\n fs.chmodSync(markerPath, 0o600);\n\n return true;\n}\n"],"mappings":"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","names":["wrapResult","modified"]}