@glasstrace/sdk 1.1.2 → 1.2.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.
- package/README.md +54 -1
- package/dist/{chunk-Z35HKVSO.js → chunk-6RIH6SFM.js} +58 -11
- package/dist/{chunk-Z35HKVSO.js.map → chunk-6RIH6SFM.js.map} +1 -1
- package/dist/{chunk-C567H5EQ.js → chunk-JKI4OCFV.js} +4 -14
- package/dist/chunk-JKI4OCFV.js.map +1 -0
- package/dist/{chunk-UJ2JC7PZ.js → chunk-TWHCJKRS.js} +17 -16
- package/dist/chunk-TWHCJKRS.js.map +1 -0
- package/dist/{chunk-3LILTM3T.js → chunk-TWTWRJ25.js} +233 -9
- package/dist/chunk-TWTWRJ25.js.map +1 -0
- package/dist/cli/init.cjs +156 -17
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.js +4 -4
- package/dist/cli/mcp-add.cjs.map +1 -1
- package/dist/cli/mcp-add.js +1 -1
- package/dist/cli/uninit.cjs +113 -11
- package/dist/cli/uninit.cjs.map +1 -1
- package/dist/cli/uninit.js +2 -2
- package/dist/cli/validate.cjs.map +1 -1
- package/dist/cli/validate.js +1 -1
- package/dist/index.cjs +257 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -9
- package/dist/index.d.ts +12 -9
- package/dist/index.js +3 -3
- package/dist/node-entry.cjs +257 -27
- package/dist/node-entry.cjs.map +1 -1
- package/dist/node-entry.js +3 -3
- package/dist/trpc/index.cjs +809 -0
- package/dist/trpc/index.cjs.map +1 -0
- package/dist/trpc/index.d.cts +165 -0
- package/dist/trpc/index.d.ts +165 -0
- package/dist/trpc/index.js +65 -0
- package/dist/trpc/index.js.map +1 -0
- package/package.json +12 -1
- package/dist/chunk-3LILTM3T.js.map +0 -1
- package/dist/chunk-C567H5EQ.js.map +0 -1
- package/dist/chunk-UJ2JC7PZ.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasstrace/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Glasstrace server-side debugging SDK for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
},
|
|
26
26
|
"default": null
|
|
27
27
|
},
|
|
28
|
+
"./trpc": {
|
|
29
|
+
"types": "./dist/trpc/index.d.ts",
|
|
30
|
+
"import": "./dist/trpc/index.js",
|
|
31
|
+
"require": "./dist/trpc/index.cjs",
|
|
32
|
+
"default": "./dist/trpc/index.js"
|
|
33
|
+
},
|
|
28
34
|
"./package.json": "./package.json"
|
|
29
35
|
},
|
|
30
36
|
"bin": {
|
|
@@ -50,6 +56,7 @@
|
|
|
50
56
|
},
|
|
51
57
|
"peerDependencies": {
|
|
52
58
|
"@prisma/instrumentation": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
59
|
+
"@trpc/server": "^10.0.0 || ^11.0.0",
|
|
53
60
|
"@vercel/blob": "^2.0.0",
|
|
54
61
|
"@vercel/otel": "^2.0.0",
|
|
55
62
|
"drizzle-orm": "^0.29.0 || ^0.30.0 || ^0.31.0 || ^0.32.0 || ^0.33.0 || ^0.34.0 || ^0.35.0 || ^0.36.0 || ^0.37.0"
|
|
@@ -58,6 +65,9 @@
|
|
|
58
65
|
"@prisma/instrumentation": {
|
|
59
66
|
"optional": true
|
|
60
67
|
},
|
|
68
|
+
"@trpc/server": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
61
71
|
"@vercel/otel": {
|
|
62
72
|
"optional": true
|
|
63
73
|
},
|
|
@@ -74,6 +84,7 @@
|
|
|
74
84
|
"@opentelemetry/core": "^2.7.0",
|
|
75
85
|
"@opentelemetry/exporter-trace-otlp-http": "^0.215.0",
|
|
76
86
|
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
|
87
|
+
"@trpc/server": "^11.17.0",
|
|
77
88
|
"@vercel/blob": "^2.3.3",
|
|
78
89
|
"@vercel/otel": "^2.1.2",
|
|
79
90
|
"esbuild": "^0.28.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/anon-key.ts","../src/mcp-runtime.ts"],"sourcesContent":["import { AnonApiKeySchema, DevApiKeySchema, createAnonApiKey } from \"@glasstrace/protocol\";\nimport type { AnonApiKey, DevApiKey } from \"@glasstrace/protocol\";\n\nconst GLASSTRACE_DIR = \".glasstrace\";\nconst ANON_KEY_FILE = \"anon_key\";\nconst CLAIMED_KEY_FILE = \"claimed-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 * Reads a claimed developer API key persisted at\n * `.glasstrace/claimed-key`. The file is the runtime fallback used by\n * {@link import(\"./init-client.js\").writeClaimedKey} when `.env.local`\n * is not writable.\n *\n * Returns the key when the file exists and its contents pass\n * `DevApiKeySchema` validation. Returns `null` when:\n * - The file does not exist.\n * - The file content fails strict schema validation (a malformed or\n * stale value cannot be distinguished from a valid one without a\n * server roundtrip — callers should treat this as \"no key\").\n * - An I/O error occurs.\n * - `node:fs` is unavailable (non-Node environment).\n */\nexport async function readClaimedKey(projectRoot?: string): Promise<DevApiKey | null> {\n const root = projectRoot ?? process.cwd();\n\n const modules = await loadFsPath();\n if (!modules) return null;\n\n const keyPath = modules.path.join(root, GLASSTRACE_DIR, CLAIMED_KEY_FILE);\n try {\n const content = await modules.fs.readFile(keyPath, \"utf-8\");\n const trimmed = content.trim();\n const parsed = DevApiKeySchema.safeParse(trimmed);\n if (parsed.success) {\n return parsed.data;\n }\n } catch {\n // Fall through — file missing, unreadable, or invalid; treat as absent.\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","import { createHash } from \"node:crypto\";\nimport {\n AnonApiKeySchema,\n DevApiKeySchema,\n type AnonApiKey,\n type DevApiKey,\n} from \"@glasstrace/protocol\";\nimport { readAnonKey, readClaimedKey } from \"./anon-key.js\";\n\n/**\n * Glasstrace MCP endpoint embedded in managed MCP configs and used by\n * the runtime claim-refresh path. Lives here (not in `cli/constants.ts`)\n * so the runtime helper can reach it without crossing the runtime/CLI\n * boundary; CLI callers import it directly from this module.\n */\nexport const MCP_ENDPOINT = \"https://api.glasstrace.dev/mcp\";\n\n/**\n * Runtime-safe MCP credential and config utilities.\n *\n * This module is loaded into user processes at SDK boot. It must not\n * import from `cli/*` or `agent-detection/*` so the runtime bundle does\n * not pull in CLI scaffolding or filesystem scanners. The boundary is\n * enforced by an import-graph guard test.\n *\n * Internal: not re-exported via `node-entry.ts` or `index.ts`.\n *\n * @module\n */\n\nlet fsPathCache:\n | { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") }\n | null\n | undefined;\n\nasync function loadFsPath(): Promise<\n | { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") }\n | null\n> {\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 * Computes a stable identity fingerprint for deduplication purposes.\n * This is NOT password hashing — the input is an opaque token used as\n * a marker identity, not a credential stored for authentication.\n *\n * @internal Exported for unit testing and for `cli/scaffolder.ts`'s\n * marker writer.\n */\nexport function identityFingerprint(token: string): string {\n return `sha256:${createHash(\"sha256\").update(token).digest(\"hex\")}`;\n}\n\n/**\n * Compares two MCP config strings for canonical-JSON equality. Returns\n * `true` when both inputs parse as JSON and produce structurally equal\n * objects after recursive key sorting; falls back to trimmed text\n * comparison for TOML and other non-JSON formats. Returns `false` on\n * parse errors that don't fall through to text comparison.\n *\n * Used to detect manually-edited MCP configs before overwriting them\n * (DISC-1247 Scenario 2c) and as the staleness signal for SDK-managed\n * configs that must be refreshed when the project's effective\n * credential changes.\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 try {\n const existingParsed: unknown = JSON.parse(existingContent);\n const expectedParsed: unknown = JSON.parse(trimmedExpected);\n return (\n JSON.stringify(canonicalize(existingParsed)) ===\n JSON.stringify(canonicalize(expectedParsed))\n );\n } catch {\n // Fall through to text comparison for TOML and other non-JSON formats.\n }\n\n return existingContent.trim() === trimmedExpected;\n}\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 * Parses a `.env.local` file's text content for `GLASSTRACE_API_KEY`,\n * returning the last assignment's value. Empty values\n * (`GLASSTRACE_API_KEY=`) and the `your_key_here` placeholder are\n * filtered out. Surrounding single or double quotes are stripped.\n *\n * The resolver validates the returned value against `DevApiKeySchema`\n * before accepting it; this parser is permissive on purpose so that\n * malformed values can be flagged with a `malformed-env-local`\n * warning rather than silently dropped.\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 looks like a claimed\n * developer key (prefix `gt_dev_`). Defensive against leading or\n * trailing whitespace.\n *\n * **This is a prefix-only check, not strict validation.** Use it as a\n * fast path for \"looks like a claimed key, do not overwrite\". The\n * effective-credential resolver validates with\n * `DevApiKeySchema.safeParse` because a `gt_dev_` prefix alone is not\n * sufficient to authenticate against the backend.\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 * Returns true when the given API key value is a fully-valid anonymous\n * API key (matches `AnonApiKeySchema`). Used by `registerViaCli` as a\n * runtime guard so that a `DevApiKey` cannot be passed via process\n * arguments to vendor MCP CLIs (which would expose it via `ps` on\n * multi-user hosts).\n *\n * @internal Exported for unit testing only.\n */\nexport function isAnonApiKey(value: string | null | undefined): boolean {\n if (value === null || value === undefined) return false;\n return AnonApiKeySchema.safeParse(value).success;\n}\n\n/**\n * The MCP-effective credential, tagged by which on-disk source produced\n * it. `env-local` and `claimed-key` carry a branded `DevApiKey`;\n * `anon` carries a branded `AnonApiKey`. Internal — not re-exported.\n */\nexport type EffectiveMcpCredential =\n | { source: \"env-local\"; key: DevApiKey }\n | { source: \"claimed-key\"; key: DevApiKey }\n | { source: \"anon\"; key: AnonApiKey };\n\n/**\n * Surfaced when the resolver detected a recoverable anomaly the caller\n * should inform the user about without printing key material.\n *\n * - `malformed-env-local`: `.env.local` set `GLASSTRACE_API_KEY` to a\n * value that fails `DevApiKeySchema`. The resolver fell through.\n * - `claimed-key-only`: the effective credential came from\n * `.glasstrace/claimed-key` because `.env.local` had no usable dev\n * key. Suggest the user copy the key into `.env.local`.\n */\nexport type ResolveWarning = \"malformed-env-local\" | \"claimed-key-only\";\n\n/**\n * The resolved credential plus the on-disk anon key (returned\n * separately so the staleness check does not have to re-read the\n * file) and any warnings the caller should surface to the user.\n */\nexport interface ResolveResult {\n effective: EffectiveMcpCredential | null;\n anonKey: AnonApiKey | null;\n warnings: ReadonlyArray<ResolveWarning>;\n}\n\n/**\n * Resolves the MCP-effective credential for a project, in priority\n * order: `.env.local` `GLASSTRACE_API_KEY` (validated as\n * `DevApiKeySchema`) → `.glasstrace/claimed-key` (validated as\n * `DevApiKeySchema`) → `.glasstrace/anon_key` (`AnonApiKey`). Returns\n * `null` for `effective` only when no source produced a usable key.\n *\n * The function is async because it touches the filesystem. It is\n * called only on the post-claim runtime branch and from the CLI\n * commands `glasstrace init` and `glasstrace mcp add`. It is **not**\n * on the steady-state init path.\n */\nexport async function resolveEffectiveMcpCredential(\n projectRoot?: string,\n): Promise<ResolveResult> {\n const root = projectRoot ?? process.cwd();\n const warnings: ResolveWarning[] = [];\n\n const envLocalKey = await readEnvLocalDevKey(root, warnings);\n const claimedKey = envLocalKey === null ? await readClaimedKey(root) : null;\n const anonKey = await readAnonKey(root);\n\n let effective: EffectiveMcpCredential | null = null;\n if (envLocalKey !== null) {\n effective = { source: \"env-local\", key: envLocalKey };\n } else if (claimedKey !== null) {\n effective = { source: \"claimed-key\", key: claimedKey };\n warnings.push(\"claimed-key-only\");\n } else if (anonKey !== null) {\n effective = { source: \"anon\", key: anonKey };\n }\n\n return { effective, anonKey, warnings };\n}\n\nasync function readEnvLocalDevKey(\n root: string,\n warnings: ResolveWarning[],\n): Promise<DevApiKey | null> {\n const modules = await loadFsPath();\n if (!modules) return null;\n\n const envPath = modules.path.join(root, \".env.local\");\n let content: string;\n try {\n content = await modules.fs.readFile(envPath, \"utf-8\");\n } catch {\n return null;\n }\n\n const raw = readEnvLocalApiKey(content);\n if (raw === null) return null;\n\n const parsed = DevApiKeySchema.safeParse(raw);\n if (!parsed.success) {\n warnings.push(\"malformed-env-local\");\n return null;\n }\n return parsed.data;\n}\n\n/**\n * Source label for the credential a marker file describes.\n *\n * @internal\n */\nexport type MarkerCredentialSource = \"env-local\" | \"claimed-key\" | \"anon\";\n\n/**\n * Descriptor passed to {@link writeMcpMarker} and matched by\n * {@link readMcpMarker}. `credentialHash` is the\n * `identityFingerprint` of the credential actually written into the\n * managed MCP config — never the credential itself.\n *\n * @internal\n */\nexport interface MarkerTarget {\n credentialSource: MarkerCredentialSource;\n credentialHash: string;\n}\n\n/**\n * Normalized state of a `.glasstrace/mcp-connected` marker on disk.\n *\n * - `absent`: no marker file present.\n * - `valid`: a v1 or v2 marker that parsed cleanly. v1 markers are\n * reported as `credentialSource = \"anon\"` with `credentialHash`\n * taken from the legacy `keyHash` field (the v1 schema can only\n * describe an anon credential).\n * - `unknown-version`: the marker has `version > 2`. Treat as\n * not-configured so a future SDK that wrote the marker doesn't\n * block this SDK from refreshing.\n * - `corrupted`: parse failure or schema mismatch. Treat as\n * not-configured.\n *\n * @internal\n */\nexport type MarkerState =\n | { status: \"absent\" }\n | { status: \"valid\"; credentialSource: MarkerCredentialSource; credentialHash: string }\n | { status: \"unknown-version\" }\n | { status: \"corrupted\" };\n\nconst MCP_MARKER_FILE = \"mcp-connected\";\nconst GLASSTRACE_DIR = \".glasstrace\";\n\n/**\n * Reads `.glasstrace/mcp-connected` and returns its normalized state.\n * Used by `mcp add` (marker-mismatch detection) and by\n * {@link writeMcpMarker} (skip-if-match optimization).\n *\n * Reader rules per the design (`SDK-034 D3`):\n * - `version === undefined` → v1: `{ keyHash, configuredAt }`. Mapped\n * to `credentialSource: \"anon\"`, `credentialHash: keyHash`. v1's\n * `keyHash` is itself produced by `identityFingerprint`, so the\n * format matches v2 without conversion.\n * - `version === 2` → v2 reader.\n * - `version > 2` → `unknown-version` (conservative-fail).\n * - Parse failure → `corrupted` (conservative-fail).\n *\n * @internal Exported for unit testing only.\n */\nexport async function readMcpMarker(projectRoot?: string): Promise<MarkerState> {\n const root = projectRoot ?? process.cwd();\n const modules = await loadFsPath();\n if (!modules) return { status: \"absent\" };\n\n const markerPath = modules.path.join(root, GLASSTRACE_DIR, MCP_MARKER_FILE);\n let content: string;\n try {\n content = await modules.fs.readFile(markerPath, \"utf-8\");\n } catch {\n return { status: \"absent\" };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n return { status: \"corrupted\" };\n }\n\n if (parsed === null || typeof parsed !== \"object\") {\n return { status: \"corrupted\" };\n }\n\n const obj = parsed as Record<string, unknown>;\n const version = obj[\"version\"];\n\n if (version === undefined) {\n // v1: { keyHash, configuredAt }\n const keyHash = obj[\"keyHash\"];\n if (typeof keyHash !== \"string\" || keyHash === \"\") {\n return { status: \"corrupted\" };\n }\n return {\n status: \"valid\",\n credentialSource: \"anon\",\n credentialHash: keyHash,\n };\n }\n\n if (version === 2) {\n const source = obj[\"credentialSource\"];\n const hash = obj[\"credentialHash\"];\n if (\n (source !== \"env-local\" && source !== \"claimed-key\" && source !== \"anon\") ||\n typeof hash !== \"string\" ||\n hash === \"\"\n ) {\n return { status: \"corrupted\" };\n }\n return {\n status: \"valid\",\n credentialSource: source,\n credentialHash: hash,\n };\n }\n\n if (typeof version === \"number\" && version > 2) {\n return { status: \"unknown-version\" };\n }\n\n return { status: \"corrupted\" };\n}\n\n/**\n * Writes a v2 `.glasstrace/mcp-connected` marker. Returns `true` when\n * the marker was created or updated, `false` when an existing marker\n * already records the same `(credentialSource, credentialHash)` pair\n * and was left untouched.\n *\n * Writer always emits v2 with `version: 2`. The legacy `keyHash`\n * field is intentionally omitted from new writes — v1 readers ignore\n * unknown fields and the duplicate would diverge over time. v3+ and\n * corrupted markers are unconditionally overwritten.\n *\n * The directory is created with `0o700` and the file with `0o600`,\n * matching existing scaffolder behavior.\n *\n * @internal Exported for unit testing only.\n */\nexport async function writeMcpMarker(\n projectRoot: string,\n target: MarkerTarget,\n): Promise<boolean> {\n const modules = await loadFsPath();\n if (!modules) return false;\n\n const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR);\n const markerPath = modules.path.join(dirPath, MCP_MARKER_FILE);\n\n const state = await readMcpMarker(projectRoot);\n if (\n state.status === \"valid\" &&\n state.credentialSource === target.credentialSource &&\n state.credentialHash === target.credentialHash\n ) {\n return false;\n }\n\n await modules.fs.mkdir(dirPath, { recursive: true, mode: 0o700 });\n\n const body = JSON.stringify(\n {\n version: 2,\n credentialSource: target.credentialSource,\n credentialHash: target.credentialHash,\n configuredAt: new Date().toISOString(),\n },\n null,\n 2,\n );\n\n await modules.fs.writeFile(markerPath, body, { mode: 0o600 });\n // writeFile mode only applies on creation on some platforms.\n await modules.fs.chmod(markerPath, 0o600);\n return true;\n}\n\nconst MCP_CONFIG_FILE = \"mcp.json\";\n\n/**\n * The set of outcomes the runtime claim-refresh helper can produce.\n *\n * - `rewrote`: `.glasstrace/mcp.json` matched the SDK-shaped output\n * for the on-disk anon key, was rewritten with the effective\n * credential, and the marker was updated.\n * - `preserved`: `.glasstrace/mcp.json` exists but does not match the\n * SDK-shaped output for the on-disk anon key. The file is left\n * untouched (the user may have hand-edited it). The marker is not\n * touched.\n * - `absent`: `.glasstrace/mcp.json` does not exist (`ENOENT`), or\n * no anon key is on disk so there is nothing to compare against. A\n * project without an anon key never had an SDK-shaped `mcp.json`\n * written by the runtime path, so this branch is a true no-op.\n * - `skipped-anon-source`: the effective credential is `null` or its\n * source is `\"anon\"`. Either way, there is no claim transition to\n * refresh for. Caller should generally gate on\n * `effective.source !== \"anon\"` before invoking the helper; this\n * branch is the runtime-side belt-and-suspenders.\n * - `skipped-not-persisted`: never reached in practice — the caller\n * in `init-client.ts` gates on `writeClaimedKey`'s `persisted` not\n * being `\"none\"`. The variant exists so an exhaustive switch in\n * the caller stays exhaustive if the gate is removed.\n *\n * @internal\n */\nexport type RuntimeRefreshAction =\n | \"rewrote\"\n | \"preserved\"\n | \"absent\"\n | \"skipped-anon-source\"\n | \"skipped-not-persisted\";\n\nlet refreshNudgeEmitted = false;\n\n/**\n * @internal Exported for unit testing only — resets the per-process\n * \"refresh nudge already emitted\" flag.\n */\nexport function __resetRefreshNudgeForTest(): void {\n refreshNudgeEmitted = false;\n}\n\n/**\n * Emits a single redacted stderr line announcing the MCP config\n * refresh. Deduplicated per process via a module-level flag — a\n * second call within the same process is a no-op. Cross-process\n * dedup (the same user running `mcp add` in another terminal moments\n * later) is explicitly out of scope.\n */\nfunction emitRefreshNudge(persistedSource: \"env-local\" | \"claimed-key\"): void {\n if (refreshNudgeEmitted) return;\n refreshNudgeEmitted = true;\n try {\n if (persistedSource === \"claimed-key\") {\n process.stderr.write(\n \"[glasstrace] MCP config refreshed for the new credential. \" +\n \"Copy .glasstrace/claimed-key into .env.local so Codex can pick it up on next restart.\\n\",\n );\n } else {\n process.stderr.write(\n \"[glasstrace] MCP config refreshed for the new credential.\\n\",\n );\n }\n } catch {\n // stderr is best-effort; refresh outcome must not depend on it.\n }\n}\n\n/**\n * Returns the SDK-shaped JSON for `.glasstrace/mcp.json` (the generic\n * MCP config used at runtime). Inlined here — and intentionally not\n * imported from `agent-detection/configs.ts` — because the runtime\n * path must not pull `agent-detection` into the runtime bundle. The\n * shape matches what `generateMcpConfig({ name: \"generic\", ... },\n * endpoint, bearer)` would produce. If the agent-detection version\n * diverges, the staleness check stops detecting SDK-managed configs;\n * a regression test against `generateMcpConfig`'s \"generic\" branch\n * lives in `tests/unit/sdk/mcp-runtime.test.ts`.\n */\nfunction genericMcpConfigContent(endpoint: string, bearer: string): string {\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n}\n\n/**\n * Refreshes `.glasstrace/mcp.json` after a successful account claim\n * transition has persisted a dev/account credential to disk (via\n * `writeClaimedKey`). The file is rewritten only when its content\n * matches the SDK-shaped output for the project's on-disk anon key\n * (canonical-JSON equivalence via `mcpConfigMatches` — whitespace and\n * key order are normalised before comparison). User-edited or\n * third-party `mcp.json` content is preserved.\n *\n * Atomic write protocol: write the replacement to a sibling temp\n * path, set `0o600`, then `rename` into place. This matches the\n * existing pattern at `init-client.ts` for `.glasstrace/config`,\n * `anon-key.ts` for `.glasstrace/anon_key`, and `runtime-state.ts`.\n * The temp must be on the same filesystem as the destination for the\n * `rename` to be atomic.\n *\n * The helper is invoked only on the post-claim runtime branch (see\n * `init-client.ts` `performInit`) and never on the steady-state init\n * path. It must not throw — failures during write/chmod/rename or\n * marker update surface as `\"preserved\"` so the caller's\n * `claimResult` return is preserved. The temp file is best-effort\n * cleaned up on failure to avoid leaving stale `.tmp` siblings on\n * disk.\n *\n * @internal Exported for unit testing only; not re-exported from\n * `node-entry.ts` or `index.ts`.\n */\nexport async function refreshGenericMcpConfigAtRuntime(\n projectRoot: string,\n effective: EffectiveMcpCredential | null,\n anonKeyOnDisk: AnonApiKey | null,\n): Promise<{ action: RuntimeRefreshAction }> {\n if (effective === null || effective.source === \"anon\") {\n return { action: \"skipped-anon-source\" };\n }\n\n // Dev-key-only project (no .glasstrace/anon_key on disk): the\n // staleness check has nothing to compare against. The SDK never\n // wrote mcp.json without an anon key, so there is nothing to\n // refresh.\n if (anonKeyOnDisk === null) {\n return { action: \"absent\" };\n }\n\n const modules = await loadFsPath();\n if (!modules) return { action: \"absent\" };\n\n const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR);\n const configPath = modules.path.join(dirPath, MCP_CONFIG_FILE);\n const tmpPath = configPath + \".tmp\";\n\n let existing: string;\n try {\n existing = await modules.fs.readFile(configPath, \"utf-8\");\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n return { action: \"absent\" };\n }\n return { action: \"preserved\" };\n }\n\n const expectedAnon = genericMcpConfigContent(MCP_ENDPOINT, anonKeyOnDisk);\n if (!mcpConfigMatches(existing, expectedAnon)) {\n return { action: \"preserved\" };\n }\n\n // SDK-managed and stale. Replace atomically. Any failure in the\n // write/chmod/rename or marker update path must produce a non-throw\n // outcome so the caller's claimResult return is preserved; the\n // .tmp sibling is best-effort cleaned up.\n const replacement = genericMcpConfigContent(MCP_ENDPOINT, effective.key);\n try {\n await modules.fs.writeFile(tmpPath, replacement, { mode: 0o600 });\n await modules.fs.chmod(tmpPath, 0o600);\n await modules.fs.rename(tmpPath, configPath);\n\n await writeMcpMarker(projectRoot, {\n credentialSource: effective.source,\n credentialHash: identityFingerprint(effective.key),\n });\n } catch {\n try {\n await modules.fs.unlink(tmpPath);\n } catch {\n // Tmp may not exist (rename succeeded, marker write failed) or\n // unlink itself may fail; either way nothing else to do.\n }\n return { action: \"preserved\" };\n }\n\n emitRefreshNudge(effective.source);\n\n return { action: \"rewrote\" };\n}\n"],"mappings":"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","names":["fsPathCache","loadFsPath","GLASSTRACE_DIR","loadFsPath","GLASSTRACE_DIR"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/health-collector.ts","../src/https-transport.ts","../src/init-client.ts"],"sourcesContent":["import type { SdkHealthReport } from \"@glasstrace/protocol\";\n\n// --- Module-level state (singleton pattern matching init-client.ts) ---\n\n/** Spans successfully forwarded to the delegate exporter since last collect. */\nlet tracesExported = 0;\n\n/** Spans dropped: buffer overflow evictions + spans lost on shutdown. */\nlet tracesDropped = 0;\n\n/** Failed performInit attempts since last collect. */\nlet initFailures = 0;\n\n/** Timestamp (ms) of the last successful config sync (performInit success or cached config load). */\nlet lastConfigSyncAt: number | null = null;\n\n// --- Recording functions (called by other modules) ---\n\n/**\n * Records that spans were submitted to the delegate exporter.\n * Counts submission, not confirmed delivery (DISC-1118).\n * Called by GlasstraceExporter after delegate.export() is invoked.\n */\nexport function recordSpansExported(count: number): void {\n if (!Number.isFinite(count) || count < 0 || !Number.isInteger(count)) return;\n tracesExported += count;\n}\n\n/**\n * Records that spans were dropped (buffer overflow eviction or shutdown loss).\n * Called by GlasstraceExporter on buffer eviction and unresolved-key shutdown.\n */\nexport function recordSpansDropped(count: number): void {\n if (!Number.isFinite(count) || count < 0 || !Number.isInteger(count)) return;\n tracesDropped += count;\n}\n\n/**\n * Records a failed performInit attempt.\n * Called by performInit when the init request fails for any reason.\n */\nexport function recordInitFailure(): void {\n try { initFailures += 1; } catch { /* best-effort */ }\n}\n\n/**\n * Records the timestamp of a successful config sync.\n * Called by performInit on success and by loadCachedConfig when loading a valid cache.\n */\nexport function recordConfigSync(timestamp: number): void {\n try { lastConfigSyncAt = timestamp; } catch { /* best-effort */ }\n}\n\n// --- Collection ---\n\n/**\n * Snapshots the current health metrics into an SdkHealthReport without\n * resetting counters. Counters are only reset when {@link acknowledgeHealthReport}\n * is called after the init request succeeds. This two-phase approach prevents\n * metric loss when `performInit` fails — the counters persist for the next\n * init attempt.\n *\n * On the first init call, all counters will be zero, which is correct.\n *\n * @param sdkVersion - The SDK version string to include in the report.\n * @returns The health report, or null if collection fails unexpectedly.\n */\nexport function collectHealthReport(sdkVersion: string): SdkHealthReport | null {\n try {\n const now = Date.now();\n const configAge = lastConfigSyncAt !== null ? Math.max(0, now - lastConfigSyncAt) : 0;\n\n return {\n tracesExportedSinceLastInit: tracesExported,\n tracesDropped,\n initFailures,\n configAge: Math.round(configAge),\n sdkVersion,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Subtracts the reported values from the running counters after a health\n * report has been successfully delivered to the backend. Called by\n * `performInit` on the success path. If init fails, counters persist\n * for the next attempt.\n *\n * Uses subtraction instead of zeroing to preserve any increments that\n * occurred between the snapshot (`collectHealthReport`) and delivery\n * (e.g., spans exported during the init HTTP call). Values are clamped\n * to 0 to guard against edge cases.\n *\n * Core invariant (DISC-1123): for any finite counter C and finite reported\n * value, after acknowledge:\n * C_new = max(0, C_before_ack - reported_value)\n * This guarantees:\n * 1. Reported finite, non-negative values are removed exactly once\n * (no double-counting).\n * 2. Activity between snapshot and acknowledge is preserved (not lost).\n * 3. The counter never goes negative (clamp prevents underflow).\n * Corruption vectors guarded: non-finite report fields (NaN/±Infinity)\n * preserve the counter; negative finite report fields are clamped to 0\n * before subtraction.\n *\n * `lastConfigSyncAt` is NOT affected — config age measures time since\n * the last successful sync, not the last acknowledgment.\n */\nexport function acknowledgeHealthReport(report: SdkHealthReport): void {\n const exp = Math.max(0, report.tracesExportedSinceLastInit);\n const expVal = tracesExported - exp;\n tracesExported = Number.isFinite(expVal) ? Math.max(0, expVal) : tracesExported;\n\n const drop = Math.max(0, report.tracesDropped);\n const dropVal = tracesDropped - drop;\n tracesDropped = Number.isFinite(dropVal) ? Math.max(0, dropVal) : tracesDropped;\n\n const fail = Math.max(0, report.initFailures);\n const failVal = initFailures - fail;\n initFailures = Number.isFinite(failVal) ? Math.max(0, failVal) : initFailures;\n}\n\n// --- Test support ---\n\n/**\n * Resets all health metrics to initial state. For testing only.\n */\nexport function _resetHealthForTesting(): void {\n tracesExported = 0;\n tracesDropped = 0;\n initFailures = 0;\n lastConfigSyncAt = null;\n}\n","/**\n * Minimal Node-native HTTPS transport used by the SDK init call.\n *\n * ## Why this exists\n *\n * Next.js 16 patches the global `fetch` to add caching, revalidation,\n * and request deduplication. When the SDK is bundled into a Next.js\n * process (instrumentation.ts path), outbound calls to\n * `api.glasstrace.dev` get intercepted by the patched fetch and can\n * silently hang — the fetch promise never resolves (DISC-493 Issue 3).\n *\n * A silent init hang is catastrophic: the SDK stays in \"pending key\"\n * state forever, enriched spans are buffered without ever being\n * exported, and anonymous keys are never registered server-side\n * (DISC-494).\n *\n * ## Why `node:https`\n *\n * `node:https` is a Node.js core module. It has zero bundle weight\n * (important because the SDK is tsup-inlined into every consumer's\n * bundle) and is always available on Node.js >= 20. Using it directly\n * bypasses the global `fetch` patching entirely — Next.js never sees\n * the request.\n *\n * Alternatives considered and rejected:\n *\n * - **`undici` as a runtime dep** — adds ~400KB inlined into every\n * consumer bundle.\n * - **`fetch(..., { cache: \"no-store\", next: { revalidate: 0 } })`** —\n * bandaid. Couples the SDK to Next.js's fetch-extension API and still\n * relies on Next's patched fetch behaving correctly. Explicitly\n * forbidden by the task brief for this reason.\n * - **Monkey-patch `globalThis.fetch`** — forbidden in the public SDK\n * (`glasstrace-sdk/CLAUDE.md`). Bypassing the patched fetch by\n * calling a different API is avoidance, not patching.\n *\n * ## Structure\n *\n * `httpsPostJson` is the only exported function. It:\n * - Sends a POST to a URL with a JSON body\n * - Applies a per-request timeout (default 10s)\n * - Retries transport-level failures (DNS, TCP, TLS) with backoff\n * - Never retries HTTP status errors — those are surfaced immediately\n * - Distinguishes transport failure, server error, and body-parse error\n * so callers can render actionable messages\n */\nimport {\n request as httpsRequest,\n type RequestOptions as HttpsRequestOptions,\n} from \"node:https\";\nimport {\n request as httpRequest,\n type IncomingMessage,\n} from \"node:http\";\nimport { URL } from \"node:url\";\n\n/** Error thrown when the HTTP request never completed (DNS/TCP/TLS/timeout). */\nexport class HttpsTransportError extends Error {\n readonly kind = \"transport\" as const;\n readonly cause?: unknown;\n constructor(message: string, cause?: unknown) {\n super(message);\n this.name = \"HttpsTransportError\";\n this.cause = cause;\n }\n}\n\n/** Error thrown when the server returned a non-2xx HTTP status. */\nexport class HttpsStatusError extends Error {\n readonly kind = \"status\" as const;\n readonly status: number;\n /** Raw response body text (may be truncated by caller if large). */\n readonly body: string;\n constructor(status: number, body: string) {\n super(`Server returned HTTP ${status}`);\n this.name = \"HttpsStatusError\";\n this.status = status;\n this.body = body;\n }\n}\n\n/** Error thrown when the response body was not parseable JSON. */\nexport class HttpsBodyParseError extends Error {\n readonly kind = \"parse\" as const;\n readonly status: number;\n readonly cause?: unknown;\n constructor(status: number, cause?: unknown) {\n super(`Server returned malformed response (HTTP ${status})`);\n this.name = \"HttpsBodyParseError\";\n this.status = status;\n this.cause = cause;\n }\n}\n\n/** Options controlling timeout and retry behavior. */\nexport interface HttpsPostJsonOptions {\n /** Parsed headers (including Content-Type, Authorization, etc). */\n headers: Record<string, string>;\n /** Per-attempt timeout, ms. Defaults to 10000. */\n timeoutMs?: number;\n /**\n * Total number of attempts INCLUDING the first. Defaults to 3\n * (initial + 2 retries). Only transport errors are retried.\n */\n maxAttempts?: number;\n /**\n * Backoff delays between retries, ms. The array length should be\n * `maxAttempts - 1`. Defaults to [500, 1500].\n */\n retryDelaysMs?: readonly number[];\n /**\n * Total deadline across all attempts, ms. Defaults to 20000.\n * If exceeded, no further retries are attempted and the last error\n * is surfaced. Guards against CLI hang on flaky networks.\n */\n totalDeadlineMs?: number;\n /**\n * Abort signal. When aborted, the in-flight request is terminated and\n * no further retries are attempted.\n */\n signal?: AbortSignal;\n /**\n * Scheduler injection point for tests. Defaults to `setTimeout`.\n * Using fake timers in tests requires the injected scheduler to honor\n * `vi.advanceTimersByTime()` — Node's real setTimeout is fine when no\n * fake timers are installed.\n */\n scheduler?: (fn: () => void, ms: number) => { unref?: () => void };\n /**\n * Alternate HTTPS request function. Injected by tests to simulate\n * Next.js-style fetch patching (assert call count stays zero) or to\n * mock transport behavior without opening real sockets.\n */\n requestImpl?: typeof httpsRequest;\n /**\n * Alternate HTTP request function, used when the URL is `http://`.\n * Splitting http/https lets tests use a local non-TLS mock server.\n */\n httpRequestImpl?: typeof httpRequest;\n}\n\n/** Shape of a successful response. */\nexport interface HttpsPostJsonResult {\n /** HTTP status code. Always in [200, 299] for success. */\n status: number;\n /** Parsed JSON body. May be `undefined` for 204 No Content. */\n body: unknown;\n /** Raw body text for diagnostics. */\n raw: string;\n}\n\n/** Delays so a failing test still completes before the suite's timeout. */\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_RETRY_DELAYS_MS = [500, 1500] as const;\nconst DEFAULT_TOTAL_DEADLINE_MS = 20_000;\n\n/**\n * Sends a POST request with a JSON body using `node:https`. Bypasses\n * any `globalThis.fetch` patching (Next.js 16, MSW, etc).\n *\n * @throws {HttpsTransportError} DNS failure, TCP reset, TLS handshake\n * failure, request timeout, or abort.\n * @throws {HttpsStatusError} HTTP response with status >= 400.\n * @throws {HttpsBodyParseError} HTTP 2xx with non-JSON body (status not\n * equal to 204).\n */\nexport async function httpsPostJson(\n url: string,\n jsonBody: unknown,\n options: HttpsPostJsonOptions,\n): Promise<HttpsPostJsonResult> {\n const parsed = new URL(url);\n const isHttps = parsed.protocol === \"https:\";\n const isHttp = parsed.protocol === \"http:\";\n if (!isHttps && !isHttp) {\n throw new HttpsTransportError(\n `Unsupported protocol: ${parsed.protocol} (expected http: or https:)`,\n );\n }\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxAttempts = options.maxAttempts ?? 3;\n const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;\n const totalDeadlineMs = options.totalDeadlineMs ?? DEFAULT_TOTAL_DEADLINE_MS;\n const scheduler = options.scheduler ?? ((fn, ms) => setTimeout(fn, ms));\n const requestImpl = isHttps\n ? (options.requestImpl ?? httpsRequest)\n : (options.httpRequestImpl ?? httpRequest);\n\n // Serialize once so retries use the exact same bytes.\n let payload: string;\n try {\n payload = JSON.stringify(jsonBody);\n } catch (err) {\n throw new HttpsTransportError(\n `Failed to serialize request body: ${err instanceof Error ? err.message : String(err)}`,\n err,\n );\n }\n const payloadBuffer = Buffer.from(payload, \"utf-8\");\n\n const startedAt = Date.now();\n let lastError: unknown;\n\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n if (options.signal?.aborted) {\n throw new HttpsTransportError(\"Request aborted\");\n }\n // Respect the total deadline — don't start another attempt if we'd\n // blow past it even before issuing the request.\n const elapsed = Date.now() - startedAt;\n if (elapsed >= totalDeadlineMs) {\n break;\n }\n const remainingBudget = totalDeadlineMs - elapsed;\n const attemptTimeoutMs = Math.min(timeoutMs, remainingBudget);\n\n try {\n return await sendSingleRequest(\n parsed,\n payloadBuffer,\n options.headers,\n attemptTimeoutMs,\n options.signal,\n requestImpl,\n );\n } catch (err) {\n lastError = err;\n // Never retry status/parse errors — they're server responses, not\n // transient network failures.\n if (err instanceof HttpsStatusError || err instanceof HttpsBodyParseError) {\n throw err;\n }\n const isLast = attempt === maxAttempts - 1;\n if (isLast) break;\n\n const delayMs = retryDelaysMs[attempt] ?? retryDelaysMs[retryDelaysMs.length - 1] ?? 0;\n const elapsedBeforeSleep = Date.now() - startedAt;\n const remaining = totalDeadlineMs - elapsedBeforeSleep;\n if (remaining <= 0) break;\n const actualDelayMs = Math.min(delayMs, remaining);\n await sleep(actualDelayMs, scheduler, options.signal);\n }\n }\n\n if (lastError instanceof HttpsTransportError) throw lastError;\n throw new HttpsTransportError(\n lastError instanceof Error ? lastError.message : \"Request failed\",\n lastError,\n );\n}\n\n/**\n * Fires a single HTTPS request. Resolves with the parsed result on 2xx,\n * throws the appropriate typed error otherwise. Caller handles retries.\n */\nfunction sendSingleRequest(\n url: URL,\n payload: Buffer,\n headers: Record<string, string>,\n timeoutMs: number,\n signal: AbortSignal | undefined,\n requestImpl: typeof httpsRequest,\n): Promise<HttpsPostJsonResult> {\n return new Promise<HttpsPostJsonResult>((resolve, reject) => {\n // Merge caller headers with Content-Length so Node doesn't chunk\n // the body. Explicit content-length also prevents confusion from\n // servers that reject chunked POSTs.\n const finalHeaders: Record<string, string | number> = {\n ...headers,\n \"Content-Length\": payload.byteLength,\n };\n\n const reqOptions: HttpsRequestOptions = {\n method: \"POST\",\n hostname: url.hostname,\n port: url.port === \"\" ? undefined : Number(url.port),\n path: `${url.pathname}${url.search}`,\n headers: finalHeaders,\n // Explicit timeout at the socket level. Still complemented by a\n // manual timer below because `timeout` only fires when the socket\n // is idle — it does not cover \"TLS handshake hangs forever\".\n timeout: timeoutMs,\n };\n\n let settled = false;\n // Hoisted so every settle path can clear the manual timer and drop\n // the optional abort listener. Assigned below once `req`, `timer`,\n // and `onAbort` exist.\n let cleanup = (): void => {};\n const settle = (fn: () => void): void => {\n if (settled) return;\n settled = true;\n cleanup();\n fn();\n };\n\n const req = requestImpl(reqOptions, (res: IncomingMessage) => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (chunk: Buffer | string) => {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk, \"utf-8\") : chunk);\n });\n res.on(\"end\", () => {\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n const status = res.statusCode ?? 0;\n if (status < 200 || status >= 300) {\n settle(() => reject(new HttpsStatusError(status, raw)));\n return;\n }\n // HTTP 204: no body is expected; resolve with undefined.\n if (status === 204 || raw.length === 0) {\n settle(() => resolve({ status, body: undefined, raw }));\n return;\n }\n try {\n const parsed = JSON.parse(raw);\n settle(() => resolve({ status, body: parsed, raw }));\n } catch (err) {\n settle(() => reject(new HttpsBodyParseError(status, err)));\n }\n });\n res.on(\"error\", (err) => {\n settle(() => reject(new HttpsTransportError(`Response stream error: ${err.message}`, err)));\n });\n });\n\n // A single manual timeout guards against handshake/DNS hangs that\n // the `timeout` option in `request()` does not cover. We destroy the\n // socket on timeout so the node:https layer doesn't keep it alive.\n const timer = setTimeout(() => {\n settle(() => {\n req.destroy(new Error(\"Request timed out\"));\n reject(new HttpsTransportError(`Request timed out after ${timeoutMs}ms`));\n });\n }, timeoutMs);\n // Don't block process exit while the timer is running.\n if (typeof timer.unref === \"function\") timer.unref();\n\n // Hoisted so `cleanup` can remove it on settle. Only registered\n // on the signal below when `signal !== undefined`.\n const onAbort = (): void => {\n settle(() => {\n req.destroy(new Error(\"Aborted\"));\n reject(new HttpsTransportError(\"Request aborted\"));\n });\n };\n\n // Install cleanup now that `timer` and `onAbort` exist. Invoked by\n // every settle path (success, status-error, parse-error, transport\n // error, timeout, abort) to clear the manual timer and drop the\n // abort listener so long-lived signals don't accumulate listeners.\n cleanup = (): void => {\n clearTimeout(timer);\n if (signal !== undefined) {\n signal.removeEventListener(\"abort\", onAbort);\n }\n };\n\n req.on(\"error\", (err) => {\n settle(() => reject(new HttpsTransportError(`fetch failed: ${err.message}`, err)));\n });\n\n req.on(\"timeout\", () => {\n settle(() => {\n req.destroy(new Error(\"Request timed out\"));\n reject(new HttpsTransportError(`Request timed out after ${timeoutMs}ms`));\n });\n });\n\n if (signal !== undefined) {\n if (signal.aborted) {\n req.destroy(new Error(\"Aborted\"));\n settle(() => reject(new HttpsTransportError(\"Request aborted\")));\n return;\n }\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n req.end(payload);\n });\n}\n\n/**\n * Delay helper that honors an AbortSignal. We cannot use `setTimeout`'s\n * built-in `signal` option because it is not available in older Node 20\n * patch releases (added in 20.6).\n *\n * Both settle paths (timer fires, signal aborts) clear the pending\n * timer and remove the abort listener so this helper remains leak-free\n * under heavy retry/abort usage.\n */\nfunction sleep(\n ms: number,\n scheduler: (fn: () => void, ms: number) => { unref?: () => void },\n signal: AbortSignal | undefined,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n // Forward-declared so `settle` below can reference it. Assigned\n // once `timer` exists.\n let cleanup = (): void => {};\n const settle = (fn: () => void): void => {\n if (settled) return;\n settled = true;\n cleanup();\n fn();\n };\n const onAbort = (): void => {\n settle(() => reject(new HttpsTransportError(\"Request aborted\")));\n };\n const timer = scheduler(() => {\n settle(resolve);\n }, ms);\n if (typeof timer.unref === \"function\") timer.unref();\n cleanup = (): void => {\n // `scheduler` returns whatever `setTimeout` returns (NodeJS.Timeout\n // in Node, number in jsdom). Both are accepted by `clearTimeout`.\n clearTimeout(timer as unknown as NodeJS.Timeout);\n if (signal !== undefined) {\n signal.removeEventListener(\"abort\", onAbort);\n }\n };\n if (signal !== undefined) {\n if (signal.aborted) {\n onAbort();\n return;\n }\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n });\n}\n","import {\n SdkInitResponseSchema,\n SdkCachedConfigSchema,\n DEFAULT_CAPTURE_CONFIG,\n} from \"@glasstrace/protocol\";\nimport type {\n SdkInitResponse,\n CaptureConfig,\n AnonApiKey,\n ImportGraphPayload,\n SdkHealthReport,\n SdkDiagnosticCode,\n} from \"@glasstrace/protocol\";\nimport type { ResolvedConfig } from \"./env-detection.js\";\nimport { recordInitFailure, recordConfigSync, acknowledgeHealthReport } from \"./health-collector.js\";\nimport {\n httpsPostJson,\n HttpsStatusError,\n HttpsTransportError,\n HttpsBodyParseError,\n} from \"./https-transport.js\";\nimport {\n resolveEffectiveMcpCredential,\n refreshGenericMcpConfigAtRuntime,\n} from \"./mcp-runtime.js\";\n\nconst GLASSTRACE_DIR = \".glasstrace\";\nconst CONFIG_FILE = \"config\";\nconst TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;\nconst INIT_TIMEOUT_MS = 10_000;\n\n/**\n * Lazily imports `node:fs/promises` and `node:path`. Returns `null` if\n * the modules are unavailable (non-Node environments). Cached after first call.\n */\nlet fsPathAsyncCache: { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") } | null | undefined;\n\nasync function loadFsPathAsync(): Promise<{ fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") } | null> {\n if (fsPathAsyncCache !== undefined) return fsPathAsyncCache;\n try {\n const [fs, path] = await Promise.all([\n import(\"node:fs/promises\"),\n import(\"node:path\"),\n ]);\n fsPathAsyncCache = { fs, path };\n return fsPathAsyncCache;\n } catch {\n fsPathAsyncCache = null;\n return null;\n }\n}\n\n/**\n * Lazily imports synchronous `node:fs` and `node:path` via `require()`.\n * Returns `null` when unavailable. Used by `loadCachedConfig` which is\n * synchronous for startup performance.\n */\nfunction loadFsSyncOrNull(): { readFileSync: typeof import(\"node:fs\").readFileSync; join: typeof import(\"node:path\").join } | null {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require(\"node:fs\") as typeof import(\"node:fs\");\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const path = require(\"node:path\") as typeof import(\"node:path\");\n return { readFileSync: fs.readFileSync, join: path.join };\n } catch {\n return null;\n }\n}\n\n/**\n * Test-only transport hook. When set, `sendInitRequest` calls this\n * instead of `httpsPostJson`. Enables unit tests to assert that the\n * SDK never routes through `globalThis.fetch` (Next.js patching) by\n * injecting a pure-function transport that never touches the network.\n *\n * Production code never sets this. Reset via `_resetConfigForTesting()`.\n */\ntype HttpsPostJsonFn = typeof httpsPostJson;\nlet transportOverride: HttpsPostJsonFn | null = null;\n\n/** In-memory config from the latest successful init response. */\nlet currentConfig: SdkInitResponse | null = null;\n\n/** Whether the disk cache has already been checked by getActiveConfig(). */\nlet configCacheChecked = false;\n\n/** Whether the next init call should be skipped (rate-limit backoff). */\nlet rateLimitBackoff = false;\n\n/** Whether the most recent performInit call completed the success path. */\nlet lastInitSucceeded = false;\n\n/**\n * Reads and validates a cached config file from `.glasstrace/config`.\n * Returns the parsed `SdkInitResponse` or `null` on any failure,\n * including when `node:fs` is unavailable (non-Node environments).\n */\nexport function loadCachedConfig(projectRoot?: string): SdkInitResponse | null {\n const modules = loadFsSyncOrNull();\n if (!modules) return null;\n\n const root = projectRoot ?? process.cwd();\n const configPath = modules.join(root, GLASSTRACE_DIR, CONFIG_FILE);\n\n try {\n // Use synchronous read for startup performance (this is called during init)\n const content = modules.readFileSync(configPath, \"utf-8\");\n const parsed = JSON.parse(content);\n const cached = SdkCachedConfigSchema.parse(parsed);\n\n // Warn if cache is stale\n const age = Date.now() - cached.cachedAt;\n if (age > TWENTY_FOUR_HOURS_MS) {\n console.warn(\n `[glasstrace] Cached config is ${Math.round(age / 3600000)}h old. Will refresh on next init.`,\n );\n }\n\n // Parse the response through the schema\n const result = SdkInitResponseSchema.safeParse(cached.response);\n if (result.success) {\n recordConfigSync(cached.cachedAt);\n return result.data;\n }\n\n console.warn(\"[glasstrace] Cached config failed validation. Using defaults.\");\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Persists the init response to `.glasstrace/config` using atomic\n * write-temp + rename semantics. Silently skipped when `node:fs` is\n * unavailable (non-Node environments). On I/O failure, logs a warning.\n *\n * Atomicity: the payload is written to `.glasstrace/config.tmp` and then\n * renamed into place. `rename` is atomic on POSIX filesystems, so readers\n * either see the previous valid config or the new valid config — never a\n * truncated or partially-written file (DISC-1247 Scenario 5). If the\n * rename fails, the temp file is cleaned up on a best-effort basis.\n */\nexport async function saveCachedConfig(\n response: SdkInitResponse,\n projectRoot?: string,\n): Promise<void> {\n const modules = await loadFsPathAsync();\n if (!modules) return;\n\n const root = projectRoot ?? process.cwd();\n const dirPath = modules.path.join(root, GLASSTRACE_DIR);\n const configPath = modules.path.join(dirPath, CONFIG_FILE);\n const tmpPath = `${configPath}.tmp`;\n\n try {\n await modules.fs.mkdir(dirPath, { recursive: true, mode: 0o700 });\n await modules.fs.chmod(dirPath, 0o700);\n const cached = {\n response,\n cachedAt: Date.now(),\n };\n // Write to a sibling temp file first, then atomically rename.\n // Using a sibling (same directory) guarantees the rename stays on\n // the same filesystem, which is required for atomicity.\n await modules.fs.writeFile(tmpPath, JSON.stringify(cached), {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n try {\n await modules.fs.chmod(tmpPath, 0o600);\n await modules.fs.rename(tmpPath, configPath);\n } catch (renameErr) {\n // Rename failed — remove the temp file so it doesn't linger.\n try {\n await modules.fs.unlink(tmpPath);\n } catch {\n // Best-effort cleanup; ignore unlink failures.\n }\n throw renameErr;\n }\n // chmod the final path to defend against platforms that don't honor\n // the mode passed to writeFile/rename on first creation.\n await modules.fs.chmod(configPath, 0o600);\n } catch (err) {\n console.warn(\n `[glasstrace] Failed to cache config to ${configPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\n/**\n * Sends a POST request to `/v1/sdk/init`.\n * Validates the response against `SdkInitResponseSchema`.\n *\n * Uses `node:https` via {@link httpsPostJson} rather than the global\n * `fetch` because Next.js 16 patches `fetch` for caching/revalidation\n * and can cause the init request to silently hang (DISC-493 Issue 3).\n * Retries transport-level failures (DNS, TCP, TLS) twice with 500ms +\n * 1500ms backoff, capped at a 20-second total deadline. Server responses\n * (HTTP 4xx/5xx) are never retried and are surfaced immediately.\n */\nexport async function sendInitRequest(\n config: ResolvedConfig,\n anonKey: AnonApiKey | null,\n sdkVersion: string,\n importGraph?: ImportGraphPayload,\n healthReport?: SdkHealthReport,\n diagnostics?: Array<{ code: SdkDiagnosticCode; message: string; timestamp: number }>,\n signal?: AbortSignal,\n): Promise<SdkInitResponse> {\n // Determine the API key for auth. Use || (not ??) so empty strings\n // fall through to the anonymous key — defense in depth for DISC-467.\n const effectiveKey = config.apiKey || anonKey;\n if (!effectiveKey) {\n throw new Error(\"No API key available for init request\");\n }\n\n // Build the request payload\n const payload: Record<string, unknown> = {\n sdkVersion,\n };\n\n // Straggler linking: if dev key is set AND anonKey is provided\n if (config.apiKey && anonKey) {\n payload.anonKey = anonKey;\n }\n\n if (config.environment) {\n payload.environment = config.environment;\n }\n if (importGraph) {\n payload.importGraph = importGraph;\n }\n if (healthReport) {\n payload.healthReport = healthReport;\n }\n if (diagnostics) {\n payload.diagnostics = diagnostics;\n }\n\n const url = `${config.endpoint}/v1/sdk/init`;\n\n const transport = transportOverride ?? httpsPostJson;\n let result;\n try {\n result = await transport(url, payload, {\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${effectiveKey}`,\n },\n timeoutMs: INIT_TIMEOUT_MS,\n signal,\n });\n } catch (err) {\n if (err instanceof HttpsStatusError) {\n const error = new Error(`Init request failed with status ${err.status}`);\n (error as unknown as Record<string, unknown>).status = err.status;\n throw error;\n }\n if (err instanceof HttpsBodyParseError) {\n // Preserve SyntaxError name so callers can distinguish parse failures\n // (existing test contract uses `name === \"SyntaxError\"`).\n const cause = err.cause;\n if (cause instanceof SyntaxError) throw cause;\n throw err;\n }\n if (err instanceof HttpsTransportError) {\n // Transport error — surface as-is; callers classify via message/name.\n throw err;\n }\n throw err;\n }\n\n return SdkInitResponseSchema.parse(result.body);\n}\n\n/**\n * Result returned by {@link performInit} when the backend reports an\n * account claim transition. `null` means no claim was present.\n */\nexport interface InitClaimResult {\n claimResult: NonNullable<SdkInitResponse[\"claimResult\"]>;\n}\n\n/**\n * Result of {@link writeClaimedKey}. The discriminator tells the\n * caller which on-disk source the key now lives at — and, if both\n * file writes failed, that no refresh of dependent state should be\n * attempted because there is no on-disk credential to back it.\n */\nexport interface WriteClaimedKeyResult {\n persisted: \"env-local\" | \"claimed-key\" | \"none\";\n}\n\n/**\n * Writes a claimed API key to disk using a fallback chain:\n * 1. `.env.local` — update or create with the new key\n * 2. `.glasstrace/claimed-key` — fallback if `.env.local` is not writable\n * 3. Dashboard message — if all file writes fail (key is never logged)\n *\n * The key value MUST NOT appear in any log output or stderr message.\n * In non-Node environments where `node:fs` is unavailable, falls through\n * directly to the dashboard message (step 3).\n *\n * Returns a {@link WriteClaimedKeyResult} so the caller can gate\n * downstream actions (specifically: managed MCP config refresh) on\n * the key actually having reached disk. Returning `persisted: \"none\"`\n * means the SDK could not write the key anywhere; refreshing\n * `.glasstrace/mcp.json` from the new key would put it out of sync\n * with the credential the runtime can actually read on the next\n * cold start.\n */\nexport async function writeClaimedKey(\n newApiKey: string,\n projectRoot?: string,\n): Promise<WriteClaimedKeyResult> {\n const modules = await loadFsPathAsync();\n\n if (modules) {\n const root = projectRoot ?? process.cwd();\n const envLocalPath = modules.path.join(root, \".env.local\");\n\n // Step 1: Try writing to .env.local\n let envLocalWritten = false;\n try {\n let content: string;\n try {\n content = await modules.fs.readFile(envLocalPath, \"utf-8\");\n // Replace all existing GLASSTRACE_API_KEY lines or append\n if (/^GLASSTRACE_API_KEY=.*/m.test(content)) {\n content = content.replace(\n /^GLASSTRACE_API_KEY=.*$/gm,\n `GLASSTRACE_API_KEY=${newApiKey}`,\n );\n } else {\n // Ensure trailing newline before appending\n if (content.length > 0 && !content.endsWith(\"\\n\")) {\n content += \"\\n\";\n }\n content += `GLASSTRACE_API_KEY=${newApiKey}\\n`;\n }\n } catch (readErr: unknown) {\n // Only create a new file when the file genuinely does not exist.\n // Other read errors (e.g., permission denied) should not silently\n // overwrite an existing .env.local that we cannot read.\n const code = readErr instanceof Error ? (readErr as NodeJS.ErrnoException).code : undefined;\n if (code !== \"ENOENT\") {\n throw readErr;\n }\n content = `GLASSTRACE_API_KEY=${newApiKey}\\n`;\n }\n\n await modules.fs.writeFile(envLocalPath, content, { encoding: \"utf-8\", mode: 0o600 });\n await modules.fs.chmod(envLocalPath, 0o600);\n envLocalWritten = true;\n } catch {\n // .env.local write failed — fall through to step 2\n }\n\n if (envLocalWritten) {\n try {\n process.stderr.write(\n \"[glasstrace] Account claimed! API key written to .env.local. Restart your dev server to use it.\\n\",\n );\n } catch { /* stderr is best-effort */ }\n return { persisted: \"env-local\" };\n }\n\n // Step 2: Try writing to .glasstrace/claimed-key\n let claimedKeyWritten = false;\n try {\n const dirPath = modules.path.join(root, GLASSTRACE_DIR);\n await modules.fs.mkdir(dirPath, { recursive: true, mode: 0o700 });\n await modules.fs.chmod(dirPath, 0o700);\n const claimedKeyPath = modules.path.join(dirPath, \"claimed-key\");\n await modules.fs.writeFile(claimedKeyPath, newApiKey, {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n await modules.fs.chmod(claimedKeyPath, 0o600);\n claimedKeyWritten = true;\n } catch {\n // .glasstrace write also failed — fall through to step 3\n }\n\n if (claimedKeyWritten) {\n try {\n process.stderr.write(\n \"[glasstrace] Account claimed! API key written to .glasstrace/claimed-key. Copy it to your .env.local file.\\n\",\n );\n } catch { /* stderr is best-effort */ }\n return { persisted: \"claimed-key\" };\n }\n }\n\n // Step 3: All file writes failed (or node:fs unavailable) — log a message WITHOUT the key\n try {\n process.stderr.write(\n \"[glasstrace] Account claimed but could not write key to disk. Visit your dashboard settings to rotate and retrieve a new API key.\\n\",\n );\n } catch { /* stderr is best-effort */ }\n return { persisted: \"none\" };\n}\n\n/**\n * Orchestrates the full init flow: send request, update config, cache result.\n * This function MUST NOT throw.\n *\n * Returns the claim result when the backend reports an account claim\n * transition, or `null` when no claim result is available (including\n * when init is skipped due to rate-limit backoff, missing API key,\n * or request failure). Callers that do not need claim information\n * can safely ignore the return value.\n */\nexport async function performInit(\n config: ResolvedConfig,\n anonKey: AnonApiKey | null,\n sdkVersion: string,\n healthReport?: SdkHealthReport | null,\n): Promise<InitClaimResult | null> {\n lastInitSucceeded = false;\n\n // Skip if in rate-limit backoff\n if (rateLimitBackoff) {\n rateLimitBackoff = false; // Reset for next call\n return null;\n }\n\n // Guard flag: prevents recordInitFailure() from being called twice if the\n // inner catch body itself throws (e.g., an unexpected error in console.warn\n // or the instanceof checks). Without this flag, the outer safety-net catch\n // would call recordInitFailure() a second time, inflating initFailures in\n // the health report. Fix for DISC-1121.\n let failureRecorded = false;\n\n try {\n const effectiveKey = config.apiKey || anonKey;\n if (!effectiveKey) {\n console.warn(\"[glasstrace] No API key available for init request.\");\n return null;\n }\n\n // No outer AbortController timeout: `httpsPostJson` enforces a\n // per-attempt timeout (INIT_TIMEOUT_MS = 10s) AND a 20s total\n // deadline across retries. An outer 10s abort would race the first\n // attempt's own timeout and prevent the backoff-retry window from\n // ever running, defeating the transport's retry behavior.\n try {\n // Delegate to sendInitRequest to avoid duplicating fetch logic\n const result = await sendInitRequest(\n config,\n anonKey,\n sdkVersion,\n undefined,\n healthReport ?? undefined,\n undefined,\n );\n\n // Update in-memory config\n currentConfig = result;\n recordConfigSync(Date.now());\n if (healthReport) {\n acknowledgeHealthReport(healthReport);\n }\n lastInitSucceeded = true;\n\n // Persist to disk\n await saveCachedConfig(result);\n\n // Handle account claim transition — write key to disk, never to stderr\n if (result.claimResult) {\n let persisted: WriteClaimedKeyResult[\"persisted\"] = \"none\";\n try {\n const w = await writeClaimedKey(result.claimResult.newApiKey);\n persisted = w.persisted;\n } catch {\n // writeClaimedKey handles its own errors internally, but guard\n // against unexpected failures to ensure claimResult is never lost\n }\n\n // When the claimed key actually reached disk, refresh the\n // managed `.glasstrace/mcp.json` (if SDK-shaped) so MCP queries\n // start using the same credential ingestion now writes traces\n // with. Refresh failure must not lose claimResult — wrap the\n // whole thing in its own try/catch.\n if (persisted !== \"none\") {\n try {\n const resolved = await resolveEffectiveMcpCredential();\n await refreshGenericMcpConfigAtRuntime(\n process.cwd(),\n resolved.effective,\n resolved.anonKey,\n );\n } catch {\n // Refresh failure leaves the existing managed config in\n // place. The next CLI-driven `glasstrace mcp add` run will\n // detect the marker mismatch and prompt a re-run.\n }\n }\n\n return { claimResult: result.claimResult };\n }\n\n return null;\n } catch (err) {\n recordInitFailure();\n failureRecorded = true;\n\n // HttpsTransportError covers DNS/TCP/TLS/timeout from the\n // node:https transport itself — `httpsPostJson` raises timeouts\n // via this error class when its internal deadlines expire.\n if (err instanceof HttpsTransportError) {\n if (/timed out|aborted/i.test(err.message)) {\n console.warn(\"[glasstrace] ingestion_unreachable: Init request timed out.\");\n } else {\n console.warn(`[glasstrace] ingestion_unreachable: ${err.message}`);\n }\n return null;\n }\n\n // Check for HTTP status errors attached by sendInitRequest\n const status = (err as Record<string, unknown>).status;\n if (status === 401) {\n console.warn(\n \"[glasstrace] ingestion_auth_failed: Check your GLASSTRACE_API_KEY.\",\n );\n return null;\n }\n\n if (status === 429) {\n console.warn(\"[glasstrace] ingestion_rate_limited: Backing off.\");\n rateLimitBackoff = true;\n return null;\n }\n\n if (typeof status === \"number\" && status >= 400) {\n console.warn(\n `[glasstrace] Init request failed with status ${status}. Using cached config.`,\n );\n return null;\n }\n\n // Schema validation failure from sendInitRequest.parse\n // NOTE: Health report was already sent to the backend (HTTP 200).\n // Not acknowledging here means the next report will double-count\n // these values. This is intentional — over-reporting is preferable\n // to data loss when the response is unparseable (DISC-1120).\n if (err instanceof Error && err.name === \"ZodError\") {\n console.warn(\n \"[glasstrace] Init response failed validation (schema version mismatch?). Using cached config.\",\n );\n return null;\n }\n\n // Network error or other fetch failure\n console.warn(\n `[glasstrace] ingestion_unreachable: ${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n } catch (err) {\n // Outermost catch — safety net for unexpected throws from the inner catch\n // body itself (e.g., an error in console.warn or instanceof checks).\n // Only record the failure if the inner catch did not already do so (DISC-1121).\n if (!failureRecorded) {\n recordInitFailure();\n }\n // Guard console.warn itself: performInit MUST NOT throw. If console.warn\n // throws here (the same failure mode this catch was added to handle), swallow\n // silently rather than violating the \"never throws\" contract.\n try {\n console.warn(\n `[glasstrace] Unexpected init error: ${err instanceof Error ? err.message : String(err)}`,\n );\n } catch { /* best-effort logging; never propagate */ }\n }\n\n return null;\n}\n\n/**\n * Returns the current capture config from the three-tier fallback chain:\n * 1. In-memory config from latest init response\n * 2. File cache (read at most once per process lifetime)\n * 3. DEFAULT_CAPTURE_CONFIG\n *\n * The disk read is cached via `configCacheChecked` to avoid repeated\n * synchronous I/O on the hot path (called by GlasstraceExporter on\n * every span export batch).\n */\nexport function getActiveConfig(): CaptureConfig {\n // Tier 1: in-memory\n if (currentConfig) {\n return currentConfig.config;\n }\n\n // Tier 2: file cache (only attempt once)\n if (!configCacheChecked) {\n configCacheChecked = true;\n const cached = loadCachedConfig();\n if (cached) {\n currentConfig = cached;\n return cached.config;\n }\n }\n\n // Tier 3: defaults\n return { ...DEFAULT_CAPTURE_CONFIG };\n}\n\n/**\n * Returns the `linkedAccountId` from the current in-memory init response,\n * or `undefined` if no init response is available or no account is linked.\n *\n * Used by the discovery endpoint to determine whether `claimed: true`\n * should be included in the response.\n */\nexport function getLinkedAccountId(): string | undefined {\n return currentConfig?.linkedAccountId;\n}\n\n/**\n * Returns the `claimResult` from the current in-memory init response,\n * or `undefined` if no init response is available or no claim occurred.\n *\n * Used by the discovery endpoint to detect in-flight claims: a valid\n * init response can include `claimResult` (claim happening NOW) without\n * `linkedAccountId` being set yet.\n */\nexport function getClaimResult(): SdkInitResponse[\"claimResult\"] {\n return currentConfig?.claimResult;\n}\n\n/**\n * Resets the in-memory config store. For testing only.\n */\nexport function _resetConfigForTesting(): void {\n currentConfig = null;\n configCacheChecked = false;\n rateLimitBackoff = false;\n lastInitSucceeded = false;\n transportOverride = null;\n}\n\n/**\n * Installs a test-only transport that replaces the `node:https` path\n * used by `sendInitRequest` and `performInit`. Tests use this to avoid\n * opening real sockets and to assert the SDK never routes through\n * `globalThis.fetch`. Pass `null` to restore the default transport.\n *\n * @internal Test-only. Never called from production code paths.\n */\nexport function _setTransportForTesting(fn: HttpsPostJsonFn | null): void {\n transportOverride = fn;\n}\n\n/**\n * Sets the in-memory config directly. Used by performInit and the orchestrator.\n */\nexport function _setCurrentConfig(config: SdkInitResponse): void {\n currentConfig = config;\n}\n\n/**\n * Returns whether rate-limit backoff is active. For testing only.\n */\nexport function _isRateLimitBackoff(): boolean {\n return rateLimitBackoff;\n}\n\n/**\n * Reads and clears the rate-limit backoff flag.\n * Called by the heartbeat after performInit returns null to detect 429 responses.\n * Returns true if a 429 occurred, false otherwise.\n */\nexport function consumeRateLimitFlag(): boolean {\n if (rateLimitBackoff) {\n rateLimitBackoff = false;\n return true;\n }\n return false;\n}\n\n/**\n * Returns true if the most recent performInit call completed the success path\n * (recordConfigSync + acknowledgeHealthReport were called).\n * Used by backgroundInit to decide whether to start the heartbeat.\n */\nexport function didLastInitSucceed(): boolean {\n return lastInitSucceeded;\n}\n\n/**\n * Result of {@link verifyInitReachable}.\n *\n * - `ok: true` — server acknowledged the init call with a valid, schema-\n * compliant payload. The anon key (if any) is registered server-side.\n * - `ok: false` with `reason: \"transport\"` — DNS/TCP/TLS/timeout failure.\n * No response reached the server (or couldn't be parsed off the wire).\n * `detail` is the raw cause (e.g. \"ECONNREFUSED\") with any leading\n * `fetch failed: ` prefix stripped; callers that render to the user\n * should add the prefix themselves to avoid doubling it.\n * - `ok: false` with `reason: \"rejected\"` — HTTP 4xx/5xx status. The\n * server received the call but declined it. `status` is set.\n * - `ok: false` with `reason: \"malformed\"` — HTTP 2xx but the body was\n * not valid JSON or did not match the protocol schema.\n */\nexport type VerifyInitResult =\n | { ok: true; response: SdkInitResponse }\n | { ok: false; reason: \"transport\"; detail: string }\n | { ok: false; reason: \"rejected\"; status: number; detail: string }\n | { ok: false; reason: \"malformed\"; detail: string };\n\n/**\n * Synchronously verifies that `/v1/sdk/init` is reachable and that the\n * provided anon key (if any) is registered server-side. Unlike\n * {@link performInit}, this function does NOT swallow errors — it\n * classifies them into the three user-actionable categories and\n * returns them.\n *\n * Used by the CLI `init` command to fail loudly when the init request\n * fails (DISC-493 Issue 3, DISC-494), rather than relying on the\n * runtime fire-and-forget call which can silently fail inside a\n * Next.js 16 process.\n *\n * The anon key is NEVER logged by this function. Error `detail`\n * strings are sanitized to the failure class only — the key does not\n * appear in transport, rejection, or malformed messages.\n */\nexport async function verifyInitReachable(\n config: ResolvedConfig,\n anonKey: AnonApiKey | null,\n sdkVersion: string,\n): Promise<VerifyInitResult> {\n try {\n const response = await sendInitRequest(config, anonKey, sdkVersion);\n return { ok: true, response };\n } catch (err) {\n // HTTP status error — server rejected the key.\n const status = (err as Record<string, unknown>).status;\n if (typeof status === \"number\") {\n return {\n ok: false,\n reason: \"rejected\",\n status,\n detail: `server returned HTTP ${status}`,\n };\n }\n\n // Schema validation failure (ZodError) or JSON parse error\n // (SyntaxError). Both mean the server responded but the body is\n // not a shape we can use.\n if (err instanceof Error && (err.name === \"ZodError\" || err.name === \"SyntaxError\")) {\n return {\n ok: false,\n reason: \"malformed\",\n detail: \"server returned malformed response\",\n };\n }\n\n // Everything else (transport errors, timeouts, abort, unknown) is\n // classified as transport. `detail` is the raw cause without a\n // `fetch failed:` prefix so the CLI (the only caller that renders\n // this) can format it as `fetch failed: <detail>` without risking\n // the double-prefix that would occur when the underlying error\n // already starts with `fetch failed:` (e.g., `HttpsTransportError`\n // from `sendSingleRequest`).\n const rawMessage = err instanceof Error ? err.message : String(err);\n const detail = rawMessage.startsWith(\"fetch failed: \")\n ? rawMessage.slice(\"fetch failed: \".length)\n : rawMessage;\n return { ok: false, reason: \"transport\", detail };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/uninit.ts","../src/cli/discovery-file.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { NEXT_CONFIG_NAMES } from \"./constants.js\";\nimport { readEnvLocalApiKey, isDevApiKey } from \"../mcp-runtime.js\";\nimport {\n removeDiscoveryFile,\n relativeDiscoveryPath,\n} from \"./discovery-file.js\";\n\n/**\n * Options for the uninit command.\n */\nexport interface UninitOptions {\n projectRoot: string;\n dryRun: boolean;\n /**\n * When true, skip interactive confirmation before destructive actions\n * such as removing a claimed developer API key from `.env.local`\n * (DISC-1247 Scenario 6).\n */\n force?: boolean;\n /**\n * Optional prompt callback; when omitted, uninit uses a TTY-based\n * `readline` prompt in interactive mode and defaults to `false`\n * (abort) when no TTY is attached. Exposed for testing.\n */\n prompt?: (question: string, defaultValue: boolean) => Promise<boolean>;\n}\n\n/**\n * Result of running the uninit command.\n */\nexport interface UninitResult {\n exitCode: number;\n summary: string[];\n warnings: string[];\n errors: string[];\n}\n\n/**\n * MCP config files that init may create.\n * These are JSON files containing `mcpServers.glasstrace`.\n */\nconst MCP_CONFIG_FILES = [\".mcp.json\", \".cursor/mcp.json\", \".gemini/settings.json\"] as const;\n\n/**\n * Agent info files that may contain glasstrace marker sections.\n * Both HTML-style (`<!-- glasstrace:mcp:start -->`) and hash-style\n * (`# glasstrace:mcp:start`) markers are supported.\n */\nconst AGENT_INFO_FILES = [\n \"CLAUDE.md\",\n \"codex.md\",\n \".cursorrules\",\n] as const;\n\n/**\n * Advances past a string literal (double-quoted, single-quoted, or template\n * literal), respecting backslash escapes.\n *\n * Note: Template literals with `${...}` interpolations containing nested\n * backticks are not fully supported — the scanner stops at the first\n * unescaped backtick. This is acceptable because config files (the primary\n * use case for `findMatchingParen`/`findMatchingBrace`) do not use nested\n * template literals.\n *\n * @param text - The source text.\n * @param start - The index of the opening quote character.\n * @param quote - The quote character (`\"`, `'`, or `` ` ``).\n * @returns The index immediately after the closing quote.\n * @internal Exported for unit testing only.\n */\nexport function skipString(text: string, start: number, quote: string): number {\n let i = start + 1;\n while (i < text.length) {\n if (text[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (text[i] === quote) {\n return i + 1;\n }\n i++;\n }\n return text.length;\n}\n\n/**\n * Finds the matching closing delimiter for an opening delimiter at the given\n * position, accounting for nesting and skipping delimiters that appear inside\n * string literals (`\"`, `'`, `` ` ``), single-line comments (`//`), and block\n * comments.\n *\n * @param text - The source text to search.\n * @param openPos - The index of the opening delimiter.\n * @param openChar - The opening delimiter character (e.g., `(` or `{`).\n * @param closeChar - The closing delimiter character (e.g., `)` or `}`).\n * @returns The index of the matching closing delimiter, or -1 if not found.\n * @internal Exported for unit testing only.\n */\nexport function findMatchingDelimiter(\n text: string,\n openPos: number,\n openChar: string,\n closeChar: string,\n): number {\n let depth = 0;\n let i = openPos;\n while (i < text.length) {\n const ch = text[i];\n\n // Skip string literals\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(text, i, ch);\n continue;\n }\n\n // Skip single-line comments.\n // Note: This may misidentify regex literals containing `//` (e.g.,\n // `/api\\//`). Config files — the primary use case — do not contain\n // regex literals, so this trade-off is acceptable.\n if (ch === \"/\" && text[i + 1] === \"/\") {\n const newline = text.indexOf(\"\\n\", i);\n if (newline === -1) {\n return -1;\n }\n i = newline + 1;\n continue;\n }\n\n // Skip block comments\n if (ch === \"/\" && text[i + 1] === \"*\") {\n const end = text.indexOf(\"*/\", i + 2);\n if (end === -1) {\n return -1;\n }\n i = end + 2;\n continue;\n }\n\n if (ch === openChar) {\n depth++;\n } else if (ch === closeChar) {\n depth--;\n if (depth === 0) {\n return i;\n }\n }\n i++;\n }\n return -1;\n}\n\n/**\n * Finds the matching closing parenthesis for an opening paren at the given\n * position, accounting for nested parentheses and skipping delimiters inside\n * string literals and comments.\n *\n * @param text - The source text to search.\n * @param openPos - The index of the opening `(`.\n * @returns The index of the matching `)`, or -1 if not found.\n * @internal Exported for unit testing only.\n */\nexport function findMatchingParen(text: string, openPos: number): number {\n return findMatchingDelimiter(text, openPos, \"(\", \")\");\n}\n\n/**\n * Removes the `withGlasstraceConfig(...)` wrapper from an ESM default export,\n * restoring the inner expression.\n *\n * Before: `export default withGlasstraceConfig(innerExpr);`\n * After: `export default innerExpr;`\n *\n * @internal Exported for unit testing only.\n */\nexport function unwrapExport(content: string): { content: string; unwrapped: boolean } {\n const pattern = /export\\s+default\\s+withGlasstraceConfig\\s*\\(/;\n const match = pattern.exec(content);\n if (!match) {\n return { content, unwrapped: false };\n }\n\n // Find the opening paren of withGlasstraceConfig(\n const openParenIdx = match.index + match[0].length - 1;\n const closeParenIdx = findMatchingParen(content, openParenIdx);\n if (closeParenIdx === -1) {\n return { content, unwrapped: false };\n }\n\n const innerExpr = content.slice(openParenIdx + 1, closeParenIdx).trim();\n if (innerExpr.length === 0) {\n return { content, unwrapped: false };\n }\n\n // Everything before `export default ...`\n const before = content.slice(0, match.index);\n // Everything after the closing `)` (skip optional semicolon and trailing whitespace)\n const afterClose = content.slice(closeParenIdx + 1);\n const trailing = afterClose.replace(/^;?\\s*/, \"\");\n\n const result = before + `export default ${innerExpr};\\n` + trailing;\n\n return { content: result, unwrapped: true };\n}\n\n/**\n * Removes the `withGlasstraceConfig(...)` wrapper from a CJS module.exports,\n * restoring the inner expression.\n *\n * Before: `module.exports = withGlasstraceConfig(innerExpr);`\n * After: `module.exports = innerExpr;`\n *\n * @internal Exported for unit testing only.\n */\nexport function unwrapCJSExport(content: string): { content: string; unwrapped: boolean } {\n const pattern = /module\\.exports\\s*=\\s*withGlasstraceConfig\\s*\\(/;\n const match = pattern.exec(content);\n if (!match) {\n return { content, unwrapped: false };\n }\n\n const openParenIdx = match.index + match[0].length - 1;\n const closeParenIdx = findMatchingParen(content, openParenIdx);\n if (closeParenIdx === -1) {\n return { content, unwrapped: false };\n }\n\n const innerExpr = content.slice(openParenIdx + 1, closeParenIdx).trim();\n if (innerExpr.length === 0) {\n return { content, unwrapped: false };\n }\n\n const before = content.slice(0, match.index);\n const afterClose = content.slice(closeParenIdx + 1);\n const trailing = afterClose.replace(/^;?\\s*/, \"\");\n\n const result = before + `module.exports = ${innerExpr};\\n` + trailing;\n\n return { content: result, unwrapped: true };\n}\n\n/**\n * Removes the `import { withGlasstraceConfig } from \"@glasstrace/sdk\"` line\n * from file content. If `withGlasstraceConfig` is the only imported specifier,\n * the entire import line is removed. If other specifiers exist, only\n * `withGlasstraceConfig` is removed from the specifier list.\n *\n * @internal Exported for unit testing only.\n */\nexport function removeGlasstraceConfigImport(content: string): string {\n // ESM: import { withGlasstraceConfig } from \"@glasstrace/sdk\"\n const esmSoleImport =\n /import\\s*\\{\\s*withGlasstraceConfig\\s*\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']\\s*;?\\s*\\n?/;\n if (esmSoleImport.test(content)) {\n return content.replace(esmSoleImport, \"\");\n }\n\n // ESM with multiple specifiers — remove withGlasstraceConfig from the list\n const esmMultiImport =\n /import\\s*\\{([^}]*)\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']/;\n const multiMatch = esmMultiImport.exec(content);\n if (multiMatch) {\n const specifiers = multiMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s !== \"\" && s !== \"withGlasstraceConfig\");\n if (specifiers.length === 0) {\n // All specifiers were withGlasstraceConfig — remove entire import\n return content.replace(\n /import\\s*\\{[^}]*\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']\\s*;?\\s*\\n?/,\n \"\",\n );\n }\n const newImport = `import { ${specifiers.join(\", \")} } from \"@glasstrace/sdk\"`;\n return content.replace(multiMatch[0], newImport);\n }\n\n // CJS: const { withGlasstraceConfig } = require(\"@glasstrace/sdk\")\n const cjsSoleRequire =\n /const\\s*\\{\\s*withGlasstraceConfig\\s*\\}\\s*=\\s*require\\s*\\(\\s*[\"']@glasstrace\\/sdk[\"']\\s*\\)\\s*;?\\s*\\n?/;\n if (cjsSoleRequire.test(content)) {\n return content.replace(cjsSoleRequire, \"\");\n }\n\n // CJS with multiple specifiers\n const cjsMultiRequire =\n /const\\s*\\{([^}]*)\\}\\s*=\\s*require\\s*\\(\\s*[\"']@glasstrace\\/sdk[\"']\\s*\\)/;\n const cjsMultiMatch = cjsMultiRequire.exec(content);\n if (cjsMultiMatch) {\n const specifiers = cjsMultiMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s !== \"\" && s !== \"withGlasstraceConfig\");\n if (specifiers.length === 0) {\n return content.replace(\n /const\\s*\\{[^}]*\\}\\s*=\\s*require\\s*\\(\\s*[\"']@glasstrace\\/sdk[\"']\\s*\\)\\s*;?\\s*\\n?/,\n \"\",\n );\n }\n const newRequire = `const { ${specifiers.join(\", \")} } = require(\"@glasstrace/sdk\")`;\n return content.replace(cjsMultiMatch[0], newRequire);\n }\n\n return content;\n}\n\n/**\n * Removes blank lines that appear consecutively (more than one empty line\n * in a row) at the top of a file, which can occur after removing import lines.\n */\nfunction cleanLeadingBlankLines(content: string): string {\n return content.replace(/^\\n{2,}/, \"\\n\");\n}\n\n/**\n * Determines whether an instrumentation.ts file was created by `glasstrace init`\n * (i.e., contains only the standard template with no user-added code).\n *\n * A file is considered init-created if:\n * - The only import from any package is `@glasstrace/sdk`\n * - The only meaningful statement in `register()` is `registerGlasstrace()`\n * - There are no other top-level statements, exports, or declarations outside\n * the register function (prevents deleting files where users added their own code)\n *\n * @internal Exported for unit testing only.\n */\nexport function isInitCreatedInstrumentation(content: string): boolean {\n const lines = content.split(\"\\n\");\n\n // Check that all imports are from @glasstrace/sdk\n const importLines = lines.filter(\n (l) => /^\\s*import\\s/.test(l) && !l.trim().startsWith(\"//\"),\n );\n const nonGlasstraceImports = importLines.filter(\n (l) => !l.includes(\"@glasstrace/sdk\"),\n );\n if (nonGlasstraceImports.length > 0) {\n return false;\n }\n\n // Check that the register() function body only contains registerGlasstrace()\n // and comments — no other meaningful statements\n const registerFnRegex = /export\\s+(?:async\\s+)?function\\s+register\\s*\\([^)]*\\)\\s*\\{/;\n const match = registerFnRegex.exec(content);\n if (!match) {\n // No register function — not a standard init template\n return false;\n }\n\n // Extract the function body\n const afterBrace = content.slice(match.index + match[0].length);\n const closingBraceIdx = findMatchingBrace(content, match.index + match[0].length - 1);\n if (closingBraceIdx === -1) {\n return false;\n }\n\n const body = afterBrace.slice(0, closingBraceIdx - (match.index + match[0].length));\n const bodyLines = body.split(\"\\n\");\n\n // Filter out comments and blank lines — only meaningful statements remain\n const statements = bodyLines.filter((l) => {\n const trimmed = l.trim();\n return trimmed !== \"\" && !trimmed.startsWith(\"//\");\n });\n\n // The only statement should be registerGlasstrace()\n if (statements.length !== 1) {\n return false;\n }\n if (!/^\\s*registerGlasstrace\\s*\\(\\s*\\)\\s*;?\\s*$/.test(statements[0])) {\n return false;\n }\n\n // Verify no other top-level code exists outside imports and the register function.\n // Extract everything that isn't an import line or inside the register() function.\n const beforeFn = content.slice(0, match.index);\n const afterFn = content.slice(closingBraceIdx + 1);\n\n const topLevelBefore = beforeFn.split(\"\\n\").filter((l) => {\n const trimmed = l.trim();\n return (\n trimmed !== \"\" &&\n !trimmed.startsWith(\"//\") &&\n !trimmed.startsWith(\"import \") &&\n !trimmed.startsWith(\"import{\")\n );\n });\n\n const topLevelAfter = afterFn.split(\"\\n\").filter((l) => {\n const trimmed = l.trim();\n return trimmed !== \"\" && !trimmed.startsWith(\"//\");\n });\n\n return topLevelBefore.length === 0 && topLevelAfter.length === 0;\n}\n\n/**\n * Finds the matching closing brace for an opening brace at the given position,\n * skipping delimiters inside string literals and comments.\n */\nfunction findMatchingBrace(text: string, openPos: number): number {\n return findMatchingDelimiter(text, openPos, \"{\", \"}\");\n}\n\n/**\n * Removes the `registerGlasstrace()` call and its `@glasstrace/sdk` import\n * from an instrumentation.ts file, preserving all other code.\n *\n * @internal Exported for unit testing only.\n */\nexport function removeRegisterGlasstrace(content: string): string {\n let result = content;\n\n // Remove all comment-block + registerGlasstrace() call pairs.\n // The init template creates a multi-line comment block before the call:\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 // Use global flag to handle multiple occurrences.\n result = result.replace(\n /[ \\t]*\\/\\/\\s*Glasstrace must be registered[^\\n]*\\n(?:[ \\t]*\\/\\/[^\\n]*\\n)*[ \\t]*registerGlasstrace\\s*\\(\\s*\\)\\s*;?\\s*\\n?/g,\n \"\",\n );\n\n // Remove any remaining standalone registerGlasstrace() calls (global)\n result = result.replace(\n /[ \\t]*registerGlasstrace\\s*\\(\\s*\\)\\s*;?\\s*\\n?/g,\n \"\",\n );\n\n // Remove the import line for registerGlasstrace from @glasstrace/sdk\n // If it's the sole import, remove the whole line\n const soleImportPattern =\n /import\\s*\\{\\s*registerGlasstrace\\s*\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']\\s*;?\\s*\\n?/;\n if (soleImportPattern.test(result)) {\n result = result.replace(soleImportPattern, \"\");\n } else {\n // Multiple specifiers — remove only registerGlasstrace\n const multiImportPattern =\n /import\\s*\\{([^}]*)\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']/;\n const multiMatch = multiImportPattern.exec(result);\n if (multiMatch) {\n const specifiers = multiMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s !== \"\" && s !== \"registerGlasstrace\");\n if (specifiers.length === 0) {\n result = result.replace(\n /import\\s*\\{[^}]*\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']\\s*;?\\s*\\n?/,\n \"\",\n );\n } else {\n const newImport = `import { ${specifiers.join(\", \")} } from \"@glasstrace/sdk\"`;\n result = result.replace(multiMatch[0], newImport);\n }\n }\n }\n\n return cleanLeadingBlankLines(result);\n}\n\n/**\n * Removes content between glasstrace marker comments from a file.\n * Supports both HTML markers (`<!-- glasstrace:mcp:start/end -->`) and\n * hash markers (`# glasstrace:mcp:start/end`).\n *\n * @internal Exported for unit testing only.\n */\nexport function removeMarkerSection(content: string): { content: string; removed: boolean } {\n const lines = content.split(\"\\n\");\n let startIdx = -1;\n let endIdx = -1;\n\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (\n trimmed === \"<!-- glasstrace:mcp:start -->\" ||\n trimmed === \"# glasstrace:mcp:start\"\n ) {\n startIdx = i;\n } else if (\n (trimmed === \"<!-- glasstrace:mcp:end -->\" ||\n trimmed === \"# glasstrace:mcp:end\") &&\n startIdx !== -1\n ) {\n endIdx = i;\n break;\n }\n }\n\n if (startIdx === -1 || endIdx === -1) {\n return { content, removed: false };\n }\n\n const before = lines.slice(0, startIdx);\n const after = lines.slice(endIdx + 1);\n\n // Remove trailing blank line that may have preceded the marker block\n while (before.length > 0 && before[before.length - 1].trim() === \"\") {\n before.pop();\n }\n\n const result = [...before, ...after].join(\"\\n\");\n // Ensure file ends with newline if it has content\n const trimmedResult = result.trimEnd();\n return {\n content: trimmedResult.length > 0 ? trimmedResult + \"\\n\" : \"\",\n removed: true,\n };\n}\n\n/**\n * Removes the `glasstrace` key from an MCP config JSON file's `mcpServers`\n * object. Only deletes the file when `mcpServers` is the sole top-level key\n * and `glasstrace` is the only server entry. When other top-level keys exist\n * (e.g., `$schema`, metadata), the `mcpServers` key is removed (if empty)\n * and the file is preserved.\n *\n * @returns `\"removed-key\"` if the key was removed (other data remains),\n * `\"deleted\"` if the file should be deleted (no other data),\n * or `\"skipped\"` if no glasstrace config was found.\n * @internal Exported for unit testing only.\n */\nexport function processJsonMcpConfig(content: string): {\n action: \"removed-key\" | \"deleted\" | \"skipped\";\n content?: string;\n} {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(content) as Record<string, unknown>;\n } catch {\n return { action: \"skipped\" };\n }\n\n const mcpServers = parsed[\"mcpServers\"] as Record<string, unknown> | undefined;\n if (!mcpServers || typeof mcpServers !== \"object\" || !(\"glasstrace\" in mcpServers)) {\n return { action: \"skipped\" };\n }\n\n const remainingServers = Object.keys(mcpServers).filter((k) => k !== \"glasstrace\");\n const otherTopLevelKeys = Object.keys(parsed).filter((k) => k !== \"mcpServers\");\n\n if (remainingServers.length === 0 && otherTopLevelKeys.length === 0) {\n // mcpServers.glasstrace is the only data in the file — safe to delete\n return { action: \"deleted\" };\n }\n\n // Remove the glasstrace key, keep other servers\n const { glasstrace: _, ...rest } = mcpServers;\n // Suppress unused variable lint — the destructuring intentionally discards glasstrace\n void _;\n\n if (remainingServers.length > 0) {\n // Other servers remain — keep mcpServers with glasstrace removed\n parsed[\"mcpServers\"] = rest;\n } else {\n // No servers remain but other top-level keys exist — remove mcpServers entirely\n delete parsed[\"mcpServers\"];\n }\n\n return { action: \"removed-key\", content: JSON.stringify(parsed, null, 2) + \"\\n\" };\n}\n\n/**\n * Removes the `[mcp_servers.glasstrace]` section from a TOML config file.\n * Since TOML parsing without a dependency is complex, this uses a line-based\n * approach that handles the standard format written by init.\n *\n * @returns `\"removed-section\"` if the glasstrace section was removed,\n * `\"deleted\"` if the entire file should be deleted (only contained\n * glasstrace config), or `\"skipped\"` if no glasstrace config found.\n * @internal Exported for unit testing only.\n */\nexport function processTomlMcpConfig(content: string): {\n action: \"removed-section\" | \"deleted\" | \"skipped\";\n content?: string;\n} {\n if (!content.includes(\"[mcp_servers.glasstrace]\")) {\n return { action: \"skipped\" };\n }\n\n const lines = content.split(\"\\n\");\n const startIdx = lines.findIndex(\n (l) => l.trim() === \"[mcp_servers.glasstrace]\",\n );\n if (startIdx === -1) {\n return { action: \"skipped\" };\n }\n\n // Find the end of the glasstrace section: next section header or end of file\n let endIdx = lines.length;\n for (let i = startIdx + 1; i < lines.length; i++) {\n if (/^\\s*\\[/.test(lines[i])) {\n endIdx = i;\n break;\n }\n }\n\n // Remove the section and any trailing blank lines\n const before = lines.slice(0, startIdx);\n const after = lines.slice(endIdx);\n\n // Trim trailing blank lines from the before section\n while (before.length > 0 && before[before.length - 1].trim() === \"\") {\n before.pop();\n }\n\n const result = [...before, ...after].join(\"\\n\").trimEnd();\n\n // Check if there are any remaining sections\n if (result.trim().length === 0) {\n return { action: \"deleted\" };\n }\n\n return { action: \"removed-section\", content: result + \"\\n\" };\n}\n\n/**\n * Writes the `.glasstrace/shutdown-requested` marker file atomically so\n * that a running SDK heartbeat tick (or equivalent lifecycle hook) can\n * detect that uninit has been invoked and trigger shutdown (DISC-1247\n * Scenario 1).\n *\n * Uses write-temp + rename semantics so a mid-write crash cannot leave\n * a truncated marker that the running process might misread.\n *\n * Best-effort: if `.glasstrace/` does not exist or the write fails, the\n * marker is silently skipped — uninit's cleanup is not blocked by a\n * missing running process.\n *\n * @internal Exported for unit testing only.\n */\nexport function writeShutdownMarker(projectRoot: string): boolean {\n const dirPath = path.join(projectRoot, \".glasstrace\");\n if (!fs.existsSync(dirPath)) {\n // No .glasstrace/ directory means no running SDK state is tracked —\n // nothing to signal. The filesystem removal step will handle any\n // stray artifacts.\n return false;\n }\n const markerPath = path.join(dirPath, \"shutdown-requested\");\n const tmpPath = `${markerPath}.tmp`;\n const body = JSON.stringify({ requestedAt: new Date().toISOString() });\n try {\n fs.writeFileSync(tmpPath, body, { encoding: \"utf-8\", mode: 0o600 });\n try {\n fs.chmodSync(tmpPath, 0o600);\n } catch {\n // chmod may be unsupported on some filesystems; proceed with rename.\n }\n fs.renameSync(tmpPath, markerPath);\n return true;\n } catch {\n // Best-effort cleanup of the temp file; swallow errors so uninit\n // itself never fails because of a signal-side-channel write.\n try {\n fs.unlinkSync(tmpPath);\n } catch {\n // Ignore — the marker was best-effort to begin with.\n }\n return false;\n }\n}\n\n/**\n * Simple TTY prompt used when `UninitOptions.prompt` is not provided.\n * Returns `defaultValue` when stdin is not a TTY.\n */\nasync function defaultPrompt(question: string, defaultValue: boolean): 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 * Reverses every step of `glasstrace init`, cleanly removing all SDK artifacts\n * from a project.\n *\n * Steps (in order):\n * 1. Write `.glasstrace/shutdown-requested` marker so a running SDK can\n * drain and exit cleanly (DISC-1247 Scenario 1)\n * 2. Unwrap `withGlasstraceConfig` from next.config\n * 3. Remove `registerGlasstrace` from instrumentation.ts (or delete if init-created)\n * 4. Remove `.glasstrace/` directory\n * 4a. Remove `<staticRoot>/.well-known/glasstrace.json` (and prune the\n * enclosing `.well-known/` directory when empty)\n * 5. Remove `GLASSTRACE_*` entries from `.env.local` (with dev-key confirmation)\n * 6. Remove `.glasstrace/` from `.gitignore`\n * 7. Remove MCP config entries\n * 8. Remove info sections from agent files\n *\n * @param options - Configuration for the uninit command.\n * @returns A structured result describing what actions were taken.\n */\nexport async function runUninit(options: UninitOptions): Promise<UninitResult> {\n const { projectRoot, dryRun } = options;\n const force = options.force === true;\n const prompt = options.prompt ?? defaultPrompt;\n const summary: string[] = [];\n const warnings: string[] = [];\n const errors: string[] = [];\n const prefix = dryRun ? \"[dry run] \" : \"\";\n\n // Step 0: Signal any running SDK to shut down via a marker file.\n // Placed first so the running process has maximum time to observe\n // the marker while the remaining cleanup steps execute.\n try {\n if (!dryRun) {\n const markerWritten = writeShutdownMarker(projectRoot);\n if (markerWritten) {\n summary.push(\"Wrote .glasstrace/shutdown-requested marker\");\n }\n } else {\n const dirPath = path.join(projectRoot, \".glasstrace\");\n if (fs.existsSync(dirPath)) {\n summary.push(`${prefix}Would write .glasstrace/shutdown-requested marker`);\n }\n }\n } catch (err) {\n // Marker is best-effort; failure is not an error for uninit.\n warnings.push(\n `Shutdown marker write failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 1: Unwrap withGlasstraceConfig from next.config\n try {\n let configHandled = false;\n for (const name of NEXT_CONFIG_NAMES) {\n const configPath = path.join(projectRoot, name);\n if (!fs.existsSync(configPath)) {\n continue;\n }\n\n const content = fs.readFileSync(configPath, \"utf-8\");\n if (!content.includes(\"withGlasstraceConfig\")) {\n continue;\n }\n\n const isESM = name.endsWith(\".ts\") || name.endsWith(\".mjs\");\n const unwrapResult = isESM\n ? unwrapExport(content)\n : unwrapCJSExport(content);\n\n if (unwrapResult.unwrapped) {\n const cleaned = removeGlasstraceConfigImport(unwrapResult.content);\n const final = cleanLeadingBlankLines(cleaned);\n if (!dryRun) {\n fs.writeFileSync(configPath, final, \"utf-8\");\n }\n summary.push(`${prefix}Unwrapped withGlasstraceConfig from ${name}`);\n configHandled = true;\n break;\n } else {\n warnings.push(\n `${name} contains withGlasstraceConfig but could not be automatically unwrapped. ` +\n \"Please remove withGlasstraceConfig() manually.\",\n );\n configHandled = true;\n break;\n }\n }\n if (!configHandled) {\n // No next.config with withGlasstraceConfig found — nothing to do\n }\n } catch (err) {\n errors.push(\n `Failed to process next.config: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 2: Remove registerGlasstrace from instrumentation.ts\n try {\n const instrPath = path.join(projectRoot, \"instrumentation.ts\");\n if (fs.existsSync(instrPath)) {\n const content = fs.readFileSync(instrPath, \"utf-8\");\n if (content.includes(\"registerGlasstrace\") || content.includes(\"@glasstrace/sdk\")) {\n if (isInitCreatedInstrumentation(content)) {\n if (!dryRun) {\n fs.unlinkSync(instrPath);\n }\n summary.push(`${prefix}Deleted instrumentation.ts (init-created)`);\n } else {\n const cleaned = removeRegisterGlasstrace(content);\n if (cleaned !== content) {\n if (!dryRun) {\n fs.writeFileSync(instrPath, cleaned, \"utf-8\");\n }\n summary.push(\n `${prefix}Removed registerGlasstrace() from instrumentation.ts`,\n );\n }\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process instrumentation.ts: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 3: Remove .glasstrace/ directory\n try {\n const glasstraceDir = path.join(projectRoot, \".glasstrace\");\n if (fs.existsSync(glasstraceDir)) {\n if (!dryRun) {\n fs.rmSync(glasstraceDir, { recursive: true, force: true });\n }\n summary.push(`${prefix}Removed .glasstrace/ directory`);\n }\n } catch (err) {\n errors.push(\n `Failed to remove .glasstrace/: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 3a: Remove the static discovery file at\n // `<staticRoot>/.well-known/glasstrace.json` and, when empty, the\n // enclosing `.well-known/` directory. Sibling files (e.g. a user's\n // own `security.txt`) are never touched.\n try {\n if (dryRun) {\n // Dry-run preview: simulate the removal by checking existence only.\n // `removeDiscoveryFile` is a destructive helper, so the preview path\n // replicates the existence check inline rather than invoking it.\n // This keeps dry-run accurate even if the helper is changed later.\n // Mirrors the real sweep by checking BOTH candidate layouts so an\n // orphaned file in the non-inferred directory still shows up in\n // the preview (heuristic-drift scenario from the Codex re-review).\n for (const previewLayout of [\"public\", \"static\"] as const) {\n const relPath = relativeDiscoveryPath(previewLayout);\n const absPath = path.join(projectRoot, relPath);\n if (fs.existsSync(absPath)) {\n summary.push(`${prefix}Would remove ${relPath}`);\n }\n }\n } else {\n const result = removeDiscoveryFile(projectRoot);\n if (result.action === \"removed\") {\n const relPath = relativeDiscoveryPath(result.layout);\n summary.push(`Removed ${relPath}`);\n if (result.directoryRemoved) {\n const dirRel = relPath.replace(/\\/glasstrace\\.json$/, \"/\");\n summary.push(`Removed empty ${dirRel}`);\n }\n } else if (result.action === \"failed\") {\n warnings.push(\n `Failed to remove ${relativeDiscoveryPath(result.layout)}${\n result.error !== undefined ? `: ${result.error}` : \"\"\n }`,\n );\n }\n }\n } catch (err) {\n warnings.push(\n `Failed to remove discovery file: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 4: Remove GLASSTRACE entries from .env.local\n // DISC-1247 Scenario 6: if the file contains a claimed developer key\n // (`gt_dev_*`), require explicit confirmation before removing it so\n // users don't silently lose authentication state during uninit.\n // `--force` bypasses the prompt.\n try {\n const envPath = path.join(projectRoot, \".env.local\");\n if (fs.existsSync(envPath)) {\n const content = fs.readFileSync(envPath, \"utf-8\");\n const existingKey = readEnvLocalApiKey(content);\n const hasDevKey = isDevApiKey(existingKey);\n\n // Track how the dev-key path is resolved so the summary reflects\n // what actually happened: prompt-confirmed, force-bypassed, or\n // preview-only. Using the literal \"(dev key confirmed)\" for all\n // three paths was misleading (Copilot review).\n let proceed = true;\n let devKeyPath: \"interactive-confirmed\" | \"force-bypass\" | \"dry-run-preview\" | \"none\" = \"none\";\n if (hasDevKey) {\n if (dryRun) {\n devKeyPath = \"dry-run-preview\";\n } else if (force) {\n devKeyPath = \"force-bypass\";\n } else {\n const confirmed = await prompt(\n \".env.local contains a claimed Glasstrace developer API key (gt_dev_...). \" +\n \"Removing it will require you to re-authenticate. Continue?\",\n false,\n );\n proceed = confirmed;\n if (confirmed) devKeyPath = \"interactive-confirmed\";\n }\n }\n\n if (!proceed) {\n warnings.push(\n \"Preserved GLASSTRACE_API_KEY in .env.local (claimed dev key; re-run with --force to remove)\",\n );\n } else {\n const lines = content.split(\"\\n\");\n const filtered = lines.filter((line) => {\n const trimmed = line.trim();\n // Match both commented and uncommented GLASSTRACE_ lines\n return !(\n /^\\s*#?\\s*GLASSTRACE_API_KEY\\s*=/.test(trimmed) ||\n /^\\s*#?\\s*GLASSTRACE_COVERAGE_MAP\\s*=/.test(trimmed)\n );\n });\n\n if (filtered.length !== lines.length) {\n const result = filtered.join(\"\\n\");\n // If the file is now empty (only newlines), don't write it\n if (result.trim().length === 0) {\n if (!dryRun) {\n fs.unlinkSync(envPath);\n }\n summary.push(`${prefix}Deleted .env.local (no remaining entries)`);\n } else {\n if (!dryRun) {\n fs.writeFileSync(envPath, result, \"utf-8\");\n }\n let devKeyAnnotation = \"\";\n if (devKeyPath === \"interactive-confirmed\") {\n devKeyAnnotation = \" (dev key confirmed)\";\n } else if (devKeyPath === \"force-bypass\") {\n devKeyAnnotation = \" (dev key removed via --force)\";\n } else if (devKeyPath === \"dry-run-preview\") {\n devKeyAnnotation =\n \" (dev key would be removed; real run would require confirmation)\";\n }\n summary.push(\n `${prefix}Removed GLASSTRACE entries from .env.local${devKeyAnnotation}`,\n );\n }\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process .env.local: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 5: Remove .glasstrace/ from .gitignore\n try {\n const gitignorePath = path.join(projectRoot, \".gitignore\");\n if (fs.existsSync(gitignorePath)) {\n const content = fs.readFileSync(gitignorePath, \"utf-8\");\n const lines = content.split(\"\\n\");\n\n // Remove lines that are exactly \".glasstrace/\" or MCP config file entries\n // added by init (e.g., \".mcp.json\", \".cursor/mcp.json\", \".gemini/settings.json\",\n // \".codex/config.toml\")\n const mcpGitignoreEntries = new Set([\n \".glasstrace/\",\n \".mcp.json\",\n \".cursor/mcp.json\",\n \".gemini/settings.json\",\n \".codex/config.toml\",\n ]);\n\n const filtered = lines.filter(\n (line) => !mcpGitignoreEntries.has(line.trim()),\n );\n\n if (filtered.length !== lines.length) {\n const result = filtered.join(\"\\n\");\n if (result.trim().length === 0) {\n if (!dryRun) {\n fs.unlinkSync(gitignorePath);\n }\n summary.push(`${prefix}Deleted .gitignore (no remaining entries)`);\n } else {\n if (!dryRun) {\n fs.writeFileSync(gitignorePath, result, \"utf-8\");\n }\n summary.push(`${prefix}Removed Glasstrace entries from .gitignore`);\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process .gitignore: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 6: Remove MCP config entries\n try {\n for (const configFile of MCP_CONFIG_FILES) {\n const configPath = path.join(projectRoot, configFile);\n if (!fs.existsSync(configPath)) {\n continue;\n }\n\n const content = fs.readFileSync(configPath, \"utf-8\");\n const result = processJsonMcpConfig(content);\n\n if (result.action === \"deleted\") {\n if (!dryRun) {\n fs.unlinkSync(configPath);\n }\n summary.push(`${prefix}Deleted ${configFile}`);\n } else if (result.action === \"removed-key\" && result.content !== undefined) {\n if (!dryRun) {\n fs.writeFileSync(configPath, result.content, \"utf-8\");\n }\n summary.push(`${prefix}Removed glasstrace from ${configFile}`);\n }\n }\n // Handle Codex TOML config separately\n const codexConfigPath = path.join(projectRoot, \".codex\", \"config.toml\");\n if (fs.existsSync(codexConfigPath)) {\n const content = fs.readFileSync(codexConfigPath, \"utf-8\");\n const tomlResult = processTomlMcpConfig(content);\n\n if (tomlResult.action === \"deleted\") {\n if (!dryRun) {\n fs.unlinkSync(codexConfigPath);\n }\n summary.push(`${prefix}Deleted .codex/config.toml`);\n } else if (tomlResult.action === \"removed-section\" && tomlResult.content !== undefined) {\n if (!dryRun) {\n fs.writeFileSync(codexConfigPath, tomlResult.content, \"utf-8\");\n }\n summary.push(`${prefix}Removed glasstrace from .codex/config.toml`);\n }\n }\n\n // Handle Windsurf global config at ~/.codeium/windsurf/mcp_config.json\n // Only process if the project has Windsurf markers, to avoid touching\n // global config for non-Windsurf projects\n const hasWindsurfMarkers =\n fs.existsSync(path.join(projectRoot, \".windsurfrules\")) ||\n fs.existsSync(path.join(projectRoot, \".windsurf\"));\n if (hasWindsurfMarkers) {\n const windsurfConfigPath = path.join(\n os.homedir(),\n \".codeium\",\n \"windsurf\",\n \"mcp_config.json\",\n );\n if (fs.existsSync(windsurfConfigPath)) {\n const content = fs.readFileSync(windsurfConfigPath, \"utf-8\");\n const windsurfResult = processJsonMcpConfig(content);\n\n // Display the path with ~ for the home directory to keep output\n // readable, but derive it from the actual path for accuracy.\n const home = os.homedir();\n const displayPath = windsurfConfigPath.startsWith(home)\n ? \"~\" + windsurfConfigPath.slice(home.length)\n : windsurfConfigPath;\n\n if (windsurfResult.action === \"deleted\") {\n if (!dryRun) {\n fs.unlinkSync(windsurfConfigPath);\n }\n summary.push(\n `${prefix}Deleted global Windsurf config (${displayPath})`,\n );\n } else if (\n windsurfResult.action === \"removed-key\" &&\n windsurfResult.content !== undefined\n ) {\n if (!dryRun) {\n fs.writeFileSync(windsurfConfigPath, windsurfResult.content, \"utf-8\");\n }\n summary.push(\n `${prefix}Removed glasstrace from global Windsurf config (${displayPath})`,\n );\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process MCP config: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 7: Remove info sections from agent files\n try {\n for (const infoFile of AGENT_INFO_FILES) {\n const filePath = path.join(projectRoot, infoFile);\n if (!fs.existsSync(filePath)) {\n continue;\n }\n\n const content = fs.readFileSync(filePath, \"utf-8\");\n const result = removeMarkerSection(content);\n\n if (result.removed) {\n if (result.content.trim().length === 0) {\n // File is now empty after removing the marker section —\n // only delete if the file was solely glasstrace content\n if (!dryRun) {\n fs.unlinkSync(filePath);\n }\n summary.push(`${prefix}Deleted ${infoFile} (only contained Glasstrace section)`);\n } else {\n if (!dryRun) {\n fs.writeFileSync(filePath, result.content, \"utf-8\");\n }\n summary.push(`${prefix}Removed Glasstrace section from ${infoFile}`);\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process agent info files: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n if (summary.length === 0 && errors.length === 0) {\n summary.push(\"No Glasstrace artifacts found — nothing to do.\");\n }\n\n return { exitCode: errors.length > 0 ? 1 : 0, summary, warnings, errors };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { AnonApiKeySchema } from \"@glasstrace/protocol\";\nimport type { AnonApiKey } from \"@glasstrace/protocol\";\n\n/**\n * Standardized static discovery-file path, served at\n * `<static-root>/.well-known/glasstrace.json` (per RFC 8615) with\n * MIME type `application/json`.\n *\n * The SDK writes the file to this relative path under the\n * framework-specific static root (`public/` for Next.js, Remix, Astro;\n * `static/` for SvelteKit) and the browser extension fetches it from\n * the same path under the deployed origin.\n *\n * @drift-check RFC 8615 (https://www.rfc-editor.org/rfc/rfc8615) + ../glasstrace-product/docs/component-designs/sdk-2.0.md §7.1 Static discovery file\n */\nexport const WELL_KNOWN_GLASSTRACE_PATH = \".well-known/glasstrace.json\" as const;\n\n/**\n * Current schema version for `.well-known/glasstrace.json`. Consumers\n * (primarily the Glasstrace browser extension) MUST tolerate unknown\n * integers >= 1 per the forward-compatibility rule in the design doc\n * (\"SDK Discovery Endpoint / Static File — Component Design\", §5.3).\n */\nexport const DISCOVERY_FILE_VERSION = 1 as const;\n\n/**\n * Schema of the static discovery file written by `sdk init`.\n *\n * Version 1 defines exactly two required fields: `version` and `key`.\n * Additional fields may appear in later schema versions — consumers MUST\n * ignore unknown fields (forward-compatibility) and MUST reject files\n * whose `key` does not match `^gt_anon_[a-f0-9]{48}$`.\n */\nexport interface DiscoveryFileV1 {\n version: typeof DISCOVERY_FILE_VERSION;\n key: AnonApiKey;\n}\n\n/**\n * Detected framework-specific static root. `public` covers Next.js,\n * Remix, and Astro; `static` covers SvelteKit. No other frameworks\n * differ today per the design doc's §4.3 table.\n */\nexport type StaticRootLayout = \"public\" | \"static\";\n\n/**\n * Result returned by {@link resolveStaticRoot} so callers can report the\n * framework-specific path they targeted (used in init summary lines and\n * rollback output).\n */\nexport interface StaticRootResolution {\n /** Absolute path to the static root directory (may not exist yet). */\n absolutePath: string;\n /** Which layout was chosen. */\n layout: StaticRootLayout;\n}\n\n/**\n * Describes the outcome of a single call to {@link writeDiscoveryFile} so\n * callers can surface an accurate summary line without re-reading the\n * file. Mirrors the DISC-1247 Scenario 2 re-init preservation contract:\n * a valid file whose `key` already matches the on-disk anon key is left\n * alone rather than rewritten.\n */\nexport type WriteDiscoveryAction =\n | \"created\"\n | \"updated-stale\"\n | \"skipped-matches\"\n | \"skipped-foreign\"\n | \"failed\";\n\n/**\n * Structured result from {@link writeDiscoveryFile}.\n */\nexport interface WriteDiscoveryResult {\n action: WriteDiscoveryAction;\n /** Absolute path of the discovery file (whether or not it was written). */\n filePath: string;\n /** Static root that was resolved, useful for `.gitignore` wiring. */\n layout: StaticRootLayout;\n /**\n * When `action === \"failed\"`, a short human-readable reason. Never\n * contains anon key bytes — callers can forward it to logs safely.\n */\n error?: string;\n}\n\n/**\n * Detects the project's framework-specific static root using the ordered\n * check from §4.4 of the design doc:\n *\n * 1. Classify as SvelteKit (→ `static/`) when `package.json` declares\n * `\"type\": \"module\"` AND the project contains `svelte.config.js` (or\n * `svelte.config.ts`) OR `src/app.html`. These signals together are\n * specific enough to avoid false positives on generic ESM projects.\n * 2. Otherwise use `public/` — this covers Next.js, Remix, Astro, and\n * plain Node web apps, which all serve `public/` verbatim.\n *\n * Returns the absolute directory path and the chosen layout. Does NOT\n * create the directory; callers use {@link writeDiscoveryFile}, which\n * creates any missing parents atomically.\n *\n * @internal Exported for unit testing only.\n */\nexport function resolveStaticRoot(projectRoot: string): StaticRootResolution {\n if (isSvelteKitProject(projectRoot)) {\n return {\n absolutePath: path.join(projectRoot, \"static\"),\n layout: \"static\",\n };\n }\n return {\n absolutePath: path.join(projectRoot, \"public\"),\n layout: \"public\",\n };\n}\n\n/**\n * Heuristic for SvelteKit detection. The design doc deliberately scopes\n * the check narrowly so a plain ESM library is never misclassified —\n * `svelte.config.{js,ts}` or `src/app.html` is the SvelteKit fingerprint,\n * and both must coexist with an ESM package.json.\n */\nfunction isSvelteKitProject(projectRoot: string): boolean {\n const pkgPath = path.join(projectRoot, \"package.json\");\n let isEsm = false;\n try {\n const pkgContent = fs.readFileSync(pkgPath, \"utf-8\");\n const parsed = JSON.parse(pkgContent) as { type?: unknown };\n isEsm = parsed.type === \"module\";\n } catch {\n // Missing or malformed package.json — fall through to default layout.\n return false;\n }\n if (!isEsm) return false;\n\n const svelteConfigJs = path.join(projectRoot, \"svelte.config.js\");\n const svelteConfigTs = path.join(projectRoot, \"svelte.config.ts\");\n const appHtml = path.join(projectRoot, \"src\", \"app.html\");\n return (\n fs.existsSync(svelteConfigJs) ||\n fs.existsSync(svelteConfigTs) ||\n fs.existsSync(appHtml)\n );\n}\n\n/**\n * Returns the project-relative path of the discovery file for the given\n * layout, suitable for surfacing in summary lines and `.gitignore` entries.\n */\nexport function relativeDiscoveryPath(layout: StaticRootLayout): string {\n const rootDir = layout === \"static\" ? \"static\" : \"public\";\n return `${rootDir}/${WELL_KNOWN_GLASSTRACE_PATH}`;\n}\n\n/**\n * Parses an existing discovery file and returns its key if the schema is\n * valid, or `null` when the file is missing, unreadable, not JSON, or\n * does not match the version-1 shape. The check is deliberately strict —\n * a corrupt or third-party-authored file is treated as \"no file\" so\n * {@link writeDiscoveryFile} overwrites it with a fresh SDK-managed copy.\n *\n * Extra unknown fields are tolerated (§5.3 forward-compatibility).\n *\n * @internal Exported for unit testing only.\n */\nexport function readExistingDiscoveryFile(\n filePath: string,\n): { key: AnonApiKey; extras: Record<string, unknown> } | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n\n if (\n parsed === null ||\n typeof parsed !== \"object\" ||\n Array.isArray(parsed)\n ) {\n return null;\n }\n\n const obj = parsed as Record<string, unknown>;\n const versionRaw = obj.version;\n if (\n typeof versionRaw !== \"number\" ||\n !Number.isInteger(versionRaw) ||\n versionRaw < 1\n ) {\n return null;\n }\n\n const keyResult = AnonApiKeySchema.safeParse(obj.key);\n if (!keyResult.success) {\n return null;\n }\n\n // Preserve user-added fields (extras) so re-init round-trips any custom\n // keys the consumer added. `version` and `key` are SDK-managed and\n // excluded from the extras object.\n const extras: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (k === \"version\" || k === \"key\") continue;\n extras[k] = v;\n }\n\n return { key: keyResult.data, extras };\n}\n\n/**\n * Serializes the discovery payload deterministically (pretty-printed JSON\n * with a trailing newline). Deterministic output keeps git diffs clean\n * when the file is checked in and matches the atomic-write contract:\n * byte-identical output on re-init when `extras` is unchanged.\n */\nfunction serializeDiscoveryPayload(\n key: AnonApiKey,\n extras: Record<string, unknown>,\n): string {\n // Key ordering: version, key, then extras in their original insertion\n // order. Preserves DISC-1247 Scenario 2 alignment — a user who added\n // `\"note\": \"…\"` after `\"key\"` sees the same ordering on re-init.\n const payload: Record<string, unknown> = {\n version: DISCOVERY_FILE_VERSION,\n key,\n ...extras,\n };\n return JSON.stringify(payload, null, 2) + \"\\n\";\n}\n\n/**\n * Writes the discovery file at `<staticRoot>/.well-known/glasstrace.json`\n * atomically.\n *\n * Behavior (per design doc §6.1 and §6.5):\n *\n * - When the target file does not exist, creates it with `{ version: 1,\n * key: <anonKey> }` after creating the `.well-known/` directory if\n * missing.\n * - When the target exists AND parses as a valid version-1 payload AND\n * its `key` matches the supplied `anonKey`: preserves the file (and\n * any user-added extra fields) and returns `\"skipped-matches\"`.\n * - When the target exists AND parses valid BUT its `key` does not\n * match: rewrites the file with the fresh key, preserving extras.\n * Returns `\"updated-stale\"`.\n * - When the target exists BUT fails to parse (corrupt, foreign-authored,\n * wrong schema): rewrites with a fresh SDK-managed payload and returns\n * `\"skipped-foreign\"` to signal that user content was not preserved.\n * - On any unexpected I/O error: returns `\"failed\"` with an error string.\n *\n * Uses a sibling temp file + `renameSync` for atomicity so concurrent\n * readers (e.g., a browser extension polling during dev server startup)\n * never observe a half-written file.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @param anonKey - The anon key currently on disk (see `anon-key.ts`).\n */\nexport function writeDiscoveryFile(\n projectRoot: string,\n anonKey: AnonApiKey,\n): WriteDiscoveryResult {\n const { absolutePath: staticRoot, layout } = resolveStaticRoot(projectRoot);\n const wellKnownDir = path.join(staticRoot, \".well-known\");\n const filePath = path.join(wellKnownDir, \"glasstrace.json\");\n\n let existingAction: WriteDiscoveryAction;\n let extras: Record<string, unknown> = {};\n\n if (fs.existsSync(filePath)) {\n const existing = readExistingDiscoveryFile(filePath);\n if (existing === null) {\n // Unreadable / malformed / non-SDK content — overwrite with a\n // fresh payload so the extension can discover the current key.\n // Extras are NOT preserved because we cannot safely parse them.\n existingAction = \"skipped-foreign\";\n } else if (existing.key === anonKey) {\n // Valid and already matches — leave the file alone (§6.5 step 2).\n return {\n action: \"skipped-matches\",\n filePath,\n layout,\n };\n } else {\n // Valid but stale — replace the key, preserve extras (§6.5 step 3).\n extras = existing.extras;\n existingAction = \"updated-stale\";\n }\n } else {\n existingAction = \"created\";\n }\n\n const tmpPath = `${filePath}.tmp-${process.pid}`;\n // On Windows, `renameSync` fails with EPERM/EEXIST when the\n // destination already exists. Rather than `unlink` the destination\n // first (which would cause data loss if the subsequent rename fails),\n // move the destination to a sibling backup path, commit the rename,\n // and only then delete the backup. If the rename fails, restore the\n // backup so the original file is preserved.\n const needsWindowsReplace =\n process.platform === \"win32\" && fs.existsSync(filePath);\n const backupPath = needsWindowsReplace\n ? `${filePath}.bak-${process.pid}`\n : null;\n\n try {\n fs.mkdirSync(wellKnownDir, { recursive: true });\n const payload = serializeDiscoveryPayload(anonKey, extras);\n fs.writeFileSync(tmpPath, payload, { encoding: \"utf-8\" });\n\n if (backupPath !== null) {\n fs.renameSync(filePath, backupPath);\n try {\n fs.renameSync(tmpPath, filePath);\n } catch (renameErr) {\n try {\n fs.renameSync(backupPath, filePath);\n } catch {\n // Restoration failed; nothing more we can do. Surface the\n // original rename error below so the caller sees the cause.\n }\n throw renameErr;\n }\n try {\n fs.unlinkSync(backupPath);\n } catch {\n // Backup cleanup is best-effort; a stale `.bak-<pid>` is\n // preferable to a spurious failure after a successful write.\n }\n } else {\n fs.renameSync(tmpPath, filePath);\n }\n\n return { action: existingAction, filePath, layout };\n } catch (err) {\n // Best-effort: remove the temp file if it was created before the\n // failure so a stale `.tmp-<pid>` does not clutter `.well-known/`.\n try {\n if (fs.existsSync(tmpPath)) {\n fs.unlinkSync(tmpPath);\n }\n } catch {\n // Swallow: the write has already failed; do not mask the root cause.\n }\n return {\n action: \"failed\",\n filePath,\n layout,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * Describes the outcome of {@link removeDiscoveryFile}. `\"removed\"` means\n * the file existed and was deleted; `\"not-found\"` means there was nothing\n * to remove (no error). `\"failed\"` preserves an error string.\n */\nexport type RemoveDiscoveryAction = \"removed\" | \"not-found\" | \"failed\";\n\n/** Structured result from {@link removeDiscoveryFile}. */\nexport interface RemoveDiscoveryResult {\n action: RemoveDiscoveryAction;\n filePath: string;\n layout: StaticRootLayout;\n /** True when the enclosing `.well-known/` directory was removed too. */\n directoryRemoved: boolean;\n error?: string;\n}\n\n/**\n * Removes the discovery file written by {@link writeDiscoveryFile} if\n * present, and removes the enclosing `.well-known/` directory when it\n * becomes empty. Tolerant of missing files, missing directories, and\n * user-owned sibling content inside `.well-known/` (never deletes a\n * sibling file).\n *\n * Checks BOTH `public/.well-known/glasstrace.json` and\n * `static/.well-known/glasstrace.json` rather than only the\n * currently-inferred layout: if layout detection changes between\n * init and uninit (for example, a SvelteKit project has its\n * `package.json` modified so the heuristic no longer matches),\n * the file written under the original layout would otherwise\n * be orphaned.\n *\n * Matches the uninit contract from design doc §6.4.\n *\n * @param projectRoot - Absolute path to the project root directory.\n */\nexport function removeDiscoveryFile(\n projectRoot: string,\n): RemoveDiscoveryResult {\n const { layout: inferredLayout } = resolveStaticRoot(projectRoot);\n\n // Sweep both candidate layouts so an orphaned file in the non-inferred\n // location is still cleaned up. The returned layout describes where\n // a file was actually removed (preferring the inferred layout when a\n // file existed in both, which is not a supported state but is\n // tolerated); when neither layout had a file, the returned layout\n // mirrors the inferred one so callers surface a stable relative path.\n const layouts: StaticRootLayout[] = [\"public\", \"static\"];\n\n interface LayoutOutcome {\n layout: StaticRootLayout;\n filePath: string;\n removed: boolean;\n directoryRemoved: boolean;\n }\n const outcomes: LayoutOutcome[] = [];\n\n for (const layout of layouts) {\n const staticRoot = path.join(projectRoot, layout);\n const wellKnownDir = path.join(staticRoot, \".well-known\");\n const filePath = path.join(wellKnownDir, \"glasstrace.json\");\n\n let removed = false;\n try {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n removed = true;\n }\n } catch (err) {\n return {\n action: \"failed\",\n filePath,\n layout,\n directoryRemoved: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n\n // Only attempt to prune the enclosing `.well-known/` when we actually\n // removed the discovery file from this layout. Pruning unconditionally\n // would delete a user-owned empty directory (that Glasstrace never\n // populated) as a silent side effect of `sdk uninit`.\n let directoryRemoved = false;\n if (removed) {\n try {\n if (fs.existsSync(wellKnownDir)) {\n const entries = fs.readdirSync(wellKnownDir);\n if (entries.length === 0) {\n fs.rmdirSync(wellKnownDir);\n directoryRemoved = true;\n }\n }\n } catch {\n // Best-effort cleanup; never surface as an error to uninit.\n }\n }\n\n outcomes.push({ layout, filePath, removed, directoryRemoved });\n }\n\n // Pick the outcome to report: prefer one where a file was removed. When\n // both layouts had a file (not a supported state, but tolerated),\n // prefer the inferred layout. When neither had a file, report the\n // inferred layout so callers receive a stable relative path.\n const removals = outcomes.filter((o) => o.removed);\n const chosen: LayoutOutcome = (() => {\n if (removals.length === 0) {\n return (\n outcomes.find((o) => o.layout === inferredLayout) ?? outcomes[0]!\n );\n }\n if (removals.length === 1) return removals[0]!;\n return (\n removals.find((o) => o.layout === inferredLayout) ?? removals[0]!\n );\n })();\n\n // Propagate directoryRemoved across both sweeps so the uninit summary\n // reflects every pruned directory even when only one was the primary.\n const anyDirectoryRemoved = outcomes.some((o) => o.directoryRemoved);\n\n return {\n action: removals.length > 0 ? \"removed\" : \"not-found\",\n filePath: chosen.filePath,\n layout: chosen.layout,\n directoryRemoved: chosen.directoryRemoved || anyDirectoryRemoved,\n };\n}\n"],"mappings":"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","names":["fs","path"]}
|