@glasstrace/sdk 0.14.2 → 0.15.1
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/dist/chunk-A2AZL6MZ.js +309 -0
- package/dist/chunk-A2AZL6MZ.js.map +1 -0
- package/dist/{chunk-ARAOZCZT.js → chunk-ROFOJQWN.js} +118 -16
- package/dist/chunk-ROFOJQWN.js.map +1 -0
- package/dist/{chunk-WV3NIPWJ.js → chunk-ZNOD6FC7.js} +18 -276
- package/dist/chunk-ZNOD6FC7.js.map +1 -0
- package/dist/cli/init.cjs +458 -115
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.d.cts +33 -1
- package/dist/cli/init.d.ts +33 -1
- package/dist/cli/init.js +144 -42
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp-add.cjs.map +1 -1
- package/dist/cli/mcp-add.js +4 -2
- package/dist/cli/mcp-add.js.map +1 -1
- package/dist/cli/uninit.cjs +181 -60
- package/dist/cli/uninit.cjs.map +1 -1
- package/dist/cli/uninit.d.cts +38 -8
- package/dist/cli/uninit.d.ts +38 -8
- package/dist/cli/uninit.js +6 -3
- package/dist/cli/validate.cjs +135 -0
- package/dist/cli/validate.cjs.map +1 -0
- package/dist/cli/validate.d.cts +60 -0
- package/dist/cli/validate.d.ts +60 -0
- package/dist/cli/validate.js +103 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/index.cjs +76 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +76 -39
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/dist/chunk-ARAOZCZT.js.map +0 -1
- package/dist/chunk-WV3NIPWJ.js.map +0 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NEXT_CONFIG_NAMES
|
|
3
|
+
} from "./chunk-BL3YDC6V.js";
|
|
4
|
+
import {
|
|
5
|
+
init_esm_shims
|
|
6
|
+
} from "./chunk-BGZ7J74D.js";
|
|
7
|
+
|
|
8
|
+
// src/cli/scaffolder.ts
|
|
9
|
+
init_esm_shims();
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
function identityFingerprint(token) {
|
|
14
|
+
return `sha256:${createHash("sha256").update(token).digest("hex")}`;
|
|
15
|
+
}
|
|
16
|
+
function hasRegisterGlasstraceCall(content) {
|
|
17
|
+
return content.split("\n").some((line) => {
|
|
18
|
+
const uncommented = line.replace(/\/\/.*$/, "");
|
|
19
|
+
return /\bregisterGlasstrace\s*\(/.test(uncommented);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function injectRegisterGlasstrace(content) {
|
|
23
|
+
if (hasRegisterGlasstraceCall(content)) {
|
|
24
|
+
return { injected: false, content };
|
|
25
|
+
}
|
|
26
|
+
const registerFnRegex = /export\s+(?:async\s+)?function\s+register\s*\([^)]*\)\s*\{/;
|
|
27
|
+
const match = registerFnRegex.exec(content);
|
|
28
|
+
if (!match) {
|
|
29
|
+
return { injected: false, content };
|
|
30
|
+
}
|
|
31
|
+
const afterBrace = content.slice(match.index + match[0].length);
|
|
32
|
+
const indentMatch = /\n([ \t]+)/.exec(afterBrace);
|
|
33
|
+
const indent = indentMatch ? indentMatch[1] : " ";
|
|
34
|
+
const importLine = 'import { registerGlasstrace } from "@glasstrace/sdk";\n';
|
|
35
|
+
const hasGlasstraceImport = content.includes("@glasstrace/sdk");
|
|
36
|
+
const insertPoint = match.index + match[0].length;
|
|
37
|
+
const callInjection = `
|
|
38
|
+
${indent}// Glasstrace must be registered before other instrumentation
|
|
39
|
+
${indent}registerGlasstrace();
|
|
40
|
+
`;
|
|
41
|
+
let modified;
|
|
42
|
+
if (hasGlasstraceImport) {
|
|
43
|
+
const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']@glasstrace\/sdk["']/;
|
|
44
|
+
const importMatch = importRegex.exec(content);
|
|
45
|
+
if (importMatch) {
|
|
46
|
+
const specifiers = importMatch[1];
|
|
47
|
+
const alreadyImported = specifiers.split(",").some((s) => s.trim() === "registerGlasstrace");
|
|
48
|
+
if (alreadyImported) {
|
|
49
|
+
modified = content.slice(0, insertPoint) + callInjection + content.slice(insertPoint);
|
|
50
|
+
} else {
|
|
51
|
+
const existingImports = specifiers.trimEnd();
|
|
52
|
+
const separator = existingImports.endsWith(",") ? " " : ", ";
|
|
53
|
+
const updatedImport = `import { ${existingImports.trim()}${separator}registerGlasstrace } from "@glasstrace/sdk"`;
|
|
54
|
+
modified = content.replace(importMatch[0], updatedImport);
|
|
55
|
+
const newMatch = registerFnRegex.exec(modified);
|
|
56
|
+
if (newMatch) {
|
|
57
|
+
const newInsertPoint = newMatch.index + newMatch[0].length;
|
|
58
|
+
modified = modified.slice(0, newInsertPoint) + callInjection + modified.slice(newInsertPoint);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
modified = importLine + content;
|
|
63
|
+
const newMatch = registerFnRegex.exec(modified);
|
|
64
|
+
if (newMatch) {
|
|
65
|
+
const newInsertPoint = newMatch.index + newMatch[0].length;
|
|
66
|
+
modified = modified.slice(0, newInsertPoint) + callInjection + modified.slice(newInsertPoint);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
modified = importLine + content.slice(0, insertPoint) + callInjection + content.slice(insertPoint);
|
|
71
|
+
}
|
|
72
|
+
return { injected: true, content: modified };
|
|
73
|
+
}
|
|
74
|
+
async function scaffoldInstrumentation(projectRoot) {
|
|
75
|
+
const filePath = path.join(projectRoot, "instrumentation.ts");
|
|
76
|
+
if (!fs.existsSync(filePath)) {
|
|
77
|
+
const content = `import { registerGlasstrace } from "@glasstrace/sdk";
|
|
78
|
+
|
|
79
|
+
export async function register() {
|
|
80
|
+
// Glasstrace must be registered before Prisma instrumentation
|
|
81
|
+
// to ensure all ORM spans are captured correctly.
|
|
82
|
+
// If you use @prisma/instrumentation, import it after this call.
|
|
83
|
+
registerGlasstrace();
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
87
|
+
return { action: "created" };
|
|
88
|
+
}
|
|
89
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
90
|
+
if (hasRegisterGlasstraceCall(existing)) {
|
|
91
|
+
return { action: "already-registered" };
|
|
92
|
+
}
|
|
93
|
+
const result = injectRegisterGlasstrace(existing);
|
|
94
|
+
if (result.injected) {
|
|
95
|
+
fs.writeFileSync(filePath, result.content, "utf-8");
|
|
96
|
+
return { action: "injected" };
|
|
97
|
+
}
|
|
98
|
+
return { action: "unrecognized" };
|
|
99
|
+
}
|
|
100
|
+
async function scaffoldNextConfig(projectRoot) {
|
|
101
|
+
let configPath;
|
|
102
|
+
let configName;
|
|
103
|
+
for (const name of NEXT_CONFIG_NAMES) {
|
|
104
|
+
const candidate = path.join(projectRoot, name);
|
|
105
|
+
if (fs.existsSync(candidate)) {
|
|
106
|
+
configPath = candidate;
|
|
107
|
+
configName = name;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (configPath === void 0 || configName === void 0) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const existing = fs.readFileSync(configPath, "utf-8");
|
|
115
|
+
if (existing.trim().length === 0) {
|
|
116
|
+
return { modified: false, reason: "empty-file" };
|
|
117
|
+
}
|
|
118
|
+
if (existing.includes("withGlasstraceConfig")) {
|
|
119
|
+
return { modified: false, reason: "already-wrapped" };
|
|
120
|
+
}
|
|
121
|
+
const isESM = configName.endsWith(".ts") || configName.endsWith(".mjs");
|
|
122
|
+
if (isESM) {
|
|
123
|
+
const importLine = 'import { withGlasstraceConfig } from "@glasstrace/sdk";\n';
|
|
124
|
+
const wrapResult2 = wrapExport(existing);
|
|
125
|
+
if (!wrapResult2.wrapped) {
|
|
126
|
+
return { modified: false, reason: "no-export" };
|
|
127
|
+
}
|
|
128
|
+
const modified2 = importLine + "\n" + wrapResult2.content;
|
|
129
|
+
fs.writeFileSync(configPath, modified2, "utf-8");
|
|
130
|
+
return { modified: true };
|
|
131
|
+
}
|
|
132
|
+
const requireLine = 'const { withGlasstraceConfig } = require("@glasstrace/sdk");\n';
|
|
133
|
+
const wrapResult = wrapCJSExport(existing);
|
|
134
|
+
if (!wrapResult.wrapped) {
|
|
135
|
+
return { modified: false, reason: "no-export" };
|
|
136
|
+
}
|
|
137
|
+
const modified = requireLine + "\n" + wrapResult.content;
|
|
138
|
+
fs.writeFileSync(configPath, modified, "utf-8");
|
|
139
|
+
return { modified: true };
|
|
140
|
+
}
|
|
141
|
+
function wrapExport(content) {
|
|
142
|
+
const marker = "export default";
|
|
143
|
+
const idx = content.lastIndexOf(marker);
|
|
144
|
+
if (idx === -1) {
|
|
145
|
+
return { content, wrapped: false };
|
|
146
|
+
}
|
|
147
|
+
const preamble = content.slice(0, idx);
|
|
148
|
+
const exprRaw = content.slice(idx + marker.length);
|
|
149
|
+
const expr = exprRaw.trim().replace(/;?\s*$/, "");
|
|
150
|
+
if (expr.length === 0) {
|
|
151
|
+
return { content, wrapped: false };
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
content: preamble + `export default withGlasstraceConfig(${expr});
|
|
155
|
+
`,
|
|
156
|
+
wrapped: true
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function wrapCJSExport(content) {
|
|
160
|
+
const cjsMarker = "module.exports";
|
|
161
|
+
const cjsIdx = content.lastIndexOf(cjsMarker);
|
|
162
|
+
if (cjsIdx === -1) {
|
|
163
|
+
return { content, wrapped: false };
|
|
164
|
+
}
|
|
165
|
+
const preamble = content.slice(0, cjsIdx);
|
|
166
|
+
const afterMarker = content.slice(cjsIdx + cjsMarker.length);
|
|
167
|
+
const eqMatch = /^\s*=\s*/.exec(afterMarker);
|
|
168
|
+
if (!eqMatch) {
|
|
169
|
+
return { content, wrapped: false };
|
|
170
|
+
}
|
|
171
|
+
const exprRaw = afterMarker.slice(eqMatch[0].length);
|
|
172
|
+
const expr = exprRaw.trim().replace(/;?\s*$/, "");
|
|
173
|
+
if (expr.length === 0) {
|
|
174
|
+
return { content, wrapped: false };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
content: preamble + `module.exports = withGlasstraceConfig(${expr});
|
|
178
|
+
`,
|
|
179
|
+
wrapped: true
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function readEnvLocalApiKey(content) {
|
|
183
|
+
let last = null;
|
|
184
|
+
const regex = /^\s*GLASSTRACE_API_KEY\s*=\s*(.*)$/gm;
|
|
185
|
+
let match;
|
|
186
|
+
while ((match = regex.exec(content)) !== null) {
|
|
187
|
+
const raw = match[1].trim();
|
|
188
|
+
if (raw === "") continue;
|
|
189
|
+
const unquoted = raw.replace(/^(['"])(.*)\1$/, "$2");
|
|
190
|
+
if (unquoted === "" || unquoted === "your_key_here") continue;
|
|
191
|
+
last = unquoted;
|
|
192
|
+
}
|
|
193
|
+
return last;
|
|
194
|
+
}
|
|
195
|
+
function isDevApiKey(value) {
|
|
196
|
+
if (value === null || value === void 0) return false;
|
|
197
|
+
return value.trim().startsWith("gt_dev_");
|
|
198
|
+
}
|
|
199
|
+
async function scaffoldEnvLocal(projectRoot) {
|
|
200
|
+
const filePath = path.join(projectRoot, ".env.local");
|
|
201
|
+
if (fs.existsSync(filePath)) {
|
|
202
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
203
|
+
if (/^\s*#?\s*GLASSTRACE_API_KEY\s*=/m.test(existing)) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
207
|
+
fs.writeFileSync(filePath, existing + separator + "# GLASSTRACE_API_KEY=your_key_here\n", "utf-8");
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
fs.writeFileSync(filePath, "# GLASSTRACE_API_KEY=your_key_here\n", "utf-8");
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
async function addCoverageMapEnv(projectRoot) {
|
|
214
|
+
const filePath = path.join(projectRoot, ".env.local");
|
|
215
|
+
if (!fs.existsSync(filePath)) {
|
|
216
|
+
fs.writeFileSync(filePath, "GLASSTRACE_COVERAGE_MAP=true\n", "utf-8");
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
220
|
+
const keyRegex = /^(\s*GLASSTRACE_COVERAGE_MAP\s*=\s*)(.*)$/m;
|
|
221
|
+
const keyMatch = keyRegex.exec(existing);
|
|
222
|
+
if (keyMatch) {
|
|
223
|
+
const currentValue = keyMatch[2].trim();
|
|
224
|
+
if (currentValue === "true") {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const updated = existing.replace(keyRegex, `${keyMatch[1]}true`);
|
|
228
|
+
fs.writeFileSync(filePath, updated, "utf-8");
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
232
|
+
fs.writeFileSync(filePath, existing + separator + "GLASSTRACE_COVERAGE_MAP=true\n", "utf-8");
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
async function scaffoldGitignore(projectRoot) {
|
|
236
|
+
const filePath = path.join(projectRoot, ".gitignore");
|
|
237
|
+
if (fs.existsSync(filePath)) {
|
|
238
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
239
|
+
const lines = existing.split("\n").map((l) => l.trim());
|
|
240
|
+
if (lines.includes(".glasstrace/")) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
244
|
+
fs.writeFileSync(filePath, existing + separator + ".glasstrace/\n", "utf-8");
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
fs.writeFileSync(filePath, ".glasstrace/\n", "utf-8");
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
function mcpConfigMatches(existingContent, expectedContent) {
|
|
251
|
+
const trimmedExpected = expectedContent.trim();
|
|
252
|
+
try {
|
|
253
|
+
const existingParsed = JSON.parse(existingContent);
|
|
254
|
+
const expectedParsed = JSON.parse(trimmedExpected);
|
|
255
|
+
return JSON.stringify(canonicalize(existingParsed)) === JSON.stringify(canonicalize(expectedParsed));
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
return existingContent.trim() === trimmedExpected;
|
|
259
|
+
}
|
|
260
|
+
function canonicalize(value) {
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
return value.map(canonicalize);
|
|
263
|
+
}
|
|
264
|
+
if (value !== null && typeof value === "object") {
|
|
265
|
+
const obj = value;
|
|
266
|
+
const sorted = {};
|
|
267
|
+
for (const key of Object.keys(obj).sort()) {
|
|
268
|
+
sorted[key] = canonicalize(obj[key]);
|
|
269
|
+
}
|
|
270
|
+
return sorted;
|
|
271
|
+
}
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
async function scaffoldMcpMarker(projectRoot, anonKey) {
|
|
275
|
+
const dirPath = path.join(projectRoot, ".glasstrace");
|
|
276
|
+
const markerPath = path.join(dirPath, "mcp-connected");
|
|
277
|
+
const keyHash = identityFingerprint(anonKey);
|
|
278
|
+
if (fs.existsSync(markerPath)) {
|
|
279
|
+
try {
|
|
280
|
+
const existing = JSON.parse(fs.readFileSync(markerPath, "utf-8"));
|
|
281
|
+
if (existing.keyHash === keyHash) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 448 });
|
|
288
|
+
const marker = JSON.stringify(
|
|
289
|
+
{ keyHash, configuredAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
290
|
+
null,
|
|
291
|
+
2
|
|
292
|
+
);
|
|
293
|
+
fs.writeFileSync(markerPath, marker, { mode: 384 });
|
|
294
|
+
fs.chmodSync(markerPath, 384);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export {
|
|
299
|
+
scaffoldInstrumentation,
|
|
300
|
+
scaffoldNextConfig,
|
|
301
|
+
readEnvLocalApiKey,
|
|
302
|
+
isDevApiKey,
|
|
303
|
+
scaffoldEnvLocal,
|
|
304
|
+
addCoverageMapEnv,
|
|
305
|
+
scaffoldGitignore,
|
|
306
|
+
mcpConfigMatches,
|
|
307
|
+
scaffoldMcpMarker
|
|
308
|
+
};
|
|
309
|
+
//# sourceMappingURL=chunk-A2AZL6MZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/scaffolder.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { NEXT_CONFIG_NAMES } from \"./constants.js\";\n\n/**\n * Computes a stable identity fingerprint for deduplication purposes.\n * This is NOT password hashing — the input is an opaque token used\n * as a marker identity, not a credential stored for authentication.\n *\n * @internal Exported for unit testing only.\n */\nexport function identityFingerprint(token: string): string {\n return `sha256:${createHash(\"sha256\").update(token).digest(\"hex\")}`;\n}\n\n/**\n * Checks whether `content` contains a real (non-commented) `registerGlasstrace()` call.\n *\n * Strips single-line `// ...` comments before matching so that\n * `// registerGlasstrace()` is not treated as a real invocation.\n * Block comments are not stripped — block-commenting a function call\n * while keeping it syntactically valid is extremely unlikely in practice.\n *\n * @internal Exported for unit testing only.\n */\nexport function hasRegisterGlasstraceCall(content: string): boolean {\n return content.split(\"\\n\").some((line) => {\n const uncommented = line.replace(/\\/\\/.*$/, \"\");\n return /\\bregisterGlasstrace\\s*\\(/.test(uncommented);\n });\n}\n\n/** Result of attempting to inject registerGlasstrace into an existing file. */\nexport interface InjectResult {\n content: string;\n injected: boolean;\n}\n\n/** Result of the instrumentation.ts scaffolding step. */\nexport type InstrumentationAction = \"created\" | \"injected\" | \"already-registered\" | \"unrecognized\";\n\n/** Structured result from scaffoldInstrumentation. */\nexport interface ScaffoldInstrumentationResult {\n action: InstrumentationAction;\n}\n\n/** Result of attempting to wrap next.config with withGlasstraceConfig. */\nexport interface ScaffoldNextConfigResult {\n modified: boolean;\n reason?: \"already-wrapped\" | \"empty-file\" | \"no-export\";\n}\n\n/**\n * Injects `registerGlasstrace()` into an existing instrumentation.ts file.\n *\n * Strategy:\n * 1. If the file already contains a real `registerGlasstrace()` call — no-op\n * (commented-out calls are ignored)\n * 2. Find `export [async] function register()` pattern\n * 3. Add `import { registerGlasstrace } from \"@glasstrace/sdk\"` at top\n * (or extend existing `@glasstrace/sdk` import, skipping if already imported)\n * 4. Insert `registerGlasstrace()` as the first statement in the function body\n *\n * @param content - The existing file content\n * @returns The modified content if injection succeeded, or the original content\n * with `injected: false` if the pattern was not recognized\n */\nexport function injectRegisterGlasstrace(content: string): InjectResult {\n // Already has a registerGlasstrace() call — no-op.\n // Uses a helper that strips single-line comments before matching\n // so that `// registerGlasstrace()` is not treated as a real call.\n if (hasRegisterGlasstraceCall(content)) {\n return { injected: false, content };\n }\n\n // Find the register() function: export [async] function register(...) {\n const registerFnRegex = /export\\s+(?:async\\s+)?function\\s+register\\s*\\([^)]*\\)\\s*\\{/;\n const match = registerFnRegex.exec(content);\n\n if (!match) {\n return { injected: false, content };\n }\n\n // Determine indentation from the function body by looking at the first\n // indented line after the opening brace. Only capture spaces and tabs\n // (not newlines) to avoid blank lines corrupting the detected indent.\n // Default to 2-space indent (matches the scaffolded template).\n const afterBrace = content.slice(match.index + match[0].length);\n const indentMatch = /\\n([ \\t]+)/.exec(afterBrace);\n const indent = indentMatch ? indentMatch[1] : \" \";\n\n // Build the import line\n const importLine = 'import { registerGlasstrace } from \"@glasstrace/sdk\";\\n';\n\n // Check if the file already imports from @glasstrace/sdk\n const hasGlasstraceImport = content.includes(\"@glasstrace/sdk\");\n\n // Insert registerGlasstrace() as the first statement in the function body\n const insertPoint = match.index + match[0].length;\n const callInjection = `\\n${indent}// Glasstrace must be registered before other instrumentation\\n${indent}registerGlasstrace();\\n`;\n\n let modified: string;\n if (hasGlasstraceImport) {\n // File already imports from @glasstrace/sdk — check whether registerGlasstrace\n // is already among the specifiers to avoid producing a duplicate like\n // `import { registerGlasstrace, registerGlasstrace }`.\n const importRegex = /import\\s*\\{([^}]+)\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']/;\n const importMatch = importRegex.exec(content);\n if (importMatch) {\n const specifiers = importMatch[1];\n const alreadyImported = specifiers\n .split(\",\")\n .some((s) => s.trim() === \"registerGlasstrace\");\n\n if (alreadyImported) {\n // Import already has registerGlasstrace — only inject the call\n modified = content.slice(0, insertPoint) + callInjection + content.slice(insertPoint);\n } else {\n // Add registerGlasstrace to existing import specifiers\n const existingImports = specifiers.trimEnd();\n const separator = existingImports.endsWith(\",\") ? \" \" : \", \";\n const updatedImport = `import { ${existingImports.trim()}${separator}registerGlasstrace } from \"@glasstrace/sdk\"`;\n modified = content.replace(importMatch[0], updatedImport);\n // Re-find the function in the shifted content and inject the call\n const newMatch = registerFnRegex.exec(modified);\n if (newMatch) {\n const newInsertPoint = newMatch.index + newMatch[0].length;\n modified = modified.slice(0, newInsertPoint) + callInjection + modified.slice(newInsertPoint);\n }\n }\n } else {\n // Non-destructured import (e.g., import * as sdk) — add a separate import\n modified = importLine + content;\n // Re-find the function in the shifted content and inject the call\n const newMatch = registerFnRegex.exec(modified);\n if (newMatch) {\n const newInsertPoint = newMatch.index + newMatch[0].length;\n modified = modified.slice(0, newInsertPoint) + callInjection + modified.slice(newInsertPoint);\n }\n }\n } else {\n // Add import at the top of the file and the call in the function body\n modified = importLine + content.slice(0, insertPoint) + callInjection + content.slice(insertPoint);\n }\n\n return { injected: true, content: modified };\n}\n\n/**\n * Ensures `instrumentation.ts` exists and contains a `registerGlasstrace()` call.\n *\n * - If the file does not exist, creates it with the standard template.\n * - If the file exists and already contains `registerGlasstrace`, skips it.\n * - If the file exists without `registerGlasstrace`, attempts to inject the\n * call into the existing `register()` function.\n * - If injection fails (no recognizable `register()` function), returns\n * `\"unrecognized\"` so the caller can display manual instructions.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns A structured result describing what action was taken.\n */\nexport async function scaffoldInstrumentation(\n projectRoot: string,\n): Promise<ScaffoldInstrumentationResult> {\n const filePath = path.join(projectRoot, \"instrumentation.ts\");\n\n if (!fs.existsSync(filePath)) {\n const content = `import { registerGlasstrace } from \"@glasstrace/sdk\";\n\nexport async function register() {\n // Glasstrace must be registered before Prisma instrumentation\n // to ensure all ORM spans are captured correctly.\n // If you use @prisma/instrumentation, import it after this call.\n registerGlasstrace();\n}\n`;\n fs.writeFileSync(filePath, content, \"utf-8\");\n return { action: \"created\" };\n }\n\n // File exists — check whether registerGlasstrace() is already called.\n // Uses a helper that strips single-line comments before matching\n // so that `// registerGlasstrace()` is not treated as a real call.\n const existing = fs.readFileSync(filePath, \"utf-8\");\n\n if (hasRegisterGlasstraceCall(existing)) {\n return { action: \"already-registered\" };\n }\n\n // Attempt injection into the existing file\n const result = injectRegisterGlasstrace(existing);\n\n if (result.injected) {\n fs.writeFileSync(filePath, result.content, \"utf-8\");\n return { action: \"injected\" };\n }\n\n return { action: \"unrecognized\" };\n}\n\n/**\n * Detects `next.config.js`, `next.config.ts`, or `next.config.mjs` and wraps\n * with `withGlasstraceConfig()`. If the config already contains\n * `withGlasstraceConfig`, the file is not modified.\n *\n * For CJS `.js` configs, adds a `require()` call and wraps `module.exports`.\n * The SDK ships dual ESM/CJS builds via tsup + conditional exports, so\n * `require(\"@glasstrace/sdk\")` resolves to the CJS entrypoint natively.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns A result object describing what happened, or `null` if no config\n * file was found at all.\n */\nexport async function scaffoldNextConfig(\n projectRoot: string,\n): Promise<ScaffoldNextConfigResult | null> {\n let configPath: string | undefined;\n let configName: string | undefined;\n\n for (const name of NEXT_CONFIG_NAMES) {\n const candidate = path.join(projectRoot, name);\n if (fs.existsSync(candidate)) {\n configPath = candidate;\n configName = name;\n break;\n }\n }\n\n if (configPath === undefined || configName === undefined) {\n return null;\n }\n\n const existing = fs.readFileSync(configPath, \"utf-8\");\n\n // Guard: empty or whitespace-only files have no export to wrap\n if (existing.trim().length === 0) {\n return { modified: false, reason: \"empty-file\" };\n }\n\n // Already wrapped — skip even in force mode to avoid double-wrapping\n if (existing.includes(\"withGlasstraceConfig\")) {\n return { modified: false, reason: \"already-wrapped\" };\n }\n\n const isESM = configName.endsWith(\".ts\") || configName.endsWith(\".mjs\");\n\n if (isESM) {\n // ESM: static import at top of file, wrap the export\n const importLine = 'import { withGlasstraceConfig } from \"@glasstrace/sdk\";\\n';\n const wrapResult = wrapExport(existing);\n if (!wrapResult.wrapped) {\n return { modified: false, reason: \"no-export\" };\n }\n const modified = importLine + \"\\n\" + wrapResult.content;\n fs.writeFileSync(configPath, modified, \"utf-8\");\n return { modified: true };\n }\n\n // CJS (.js): require() the SDK (resolves to the CJS dist build) and\n // wrap the module.exports expression in place — no file renaming needed.\n const requireLine = 'const { withGlasstraceConfig } = require(\"@glasstrace/sdk\");\\n';\n const wrapResult = wrapCJSExport(existing);\n if (!wrapResult.wrapped) {\n return { modified: false, reason: \"no-export\" };\n }\n const modified = requireLine + \"\\n\" + wrapResult.content;\n fs.writeFileSync(configPath, modified, \"utf-8\");\n return { modified: true };\n}\n\n/** @internal Exported for unit testing only. */\nexport interface WrapResult {\n content: string;\n wrapped: boolean;\n}\n\n/**\n * Wraps an ESM `export default` expression with `withGlasstraceConfig()`.\n *\n * Strategy: find the last `export default` in the file. Everything from\n * that statement to EOF is the exported expression. Strip optional trailing\n * semicolons/whitespace and wrap with `withGlasstraceConfig(...)`.\n *\n * @param content - The full file content containing an ESM default export.\n * @returns `{ wrapped: true, content }` on success, or `{ wrapped: false }` if\n * no recognizable export pattern was found (content returned unchanged).\n * @internal Exported for unit testing only.\n */\nexport function wrapExport(content: string): WrapResult {\n // Find the last `export default` — use lastIndexOf for robustness\n const marker = \"export default\";\n const idx = content.lastIndexOf(marker);\n if (idx === -1) {\n return { content, wrapped: false };\n }\n\n const preamble = content.slice(0, idx);\n const exprRaw = content.slice(idx + marker.length);\n // Trim leading whitespace; strip trailing semicolon + whitespace\n const expr = exprRaw.trim().replace(/;?\\s*$/, \"\");\n if (expr.length === 0) {\n return { content, wrapped: false };\n }\n\n return {\n content: preamble + `export default withGlasstraceConfig(${expr});\\n`,\n wrapped: true,\n };\n}\n\n/**\n * Wraps a CJS `module.exports = expr` with `withGlasstraceConfig()`.\n *\n * Strategy: find the last `module.exports =` in the file. Everything from\n * that statement to EOF is the exported expression. Strip optional trailing\n * semicolons/whitespace and wrap with `module.exports = withGlasstraceConfig(...)`.\n *\n * @param content - The full CJS file content containing `module.exports = ...`.\n * @returns `{ wrapped: true, content }` on success, or `{ wrapped: false }` if\n * no recognizable `module.exports` pattern was found (content returned unchanged).\n * @internal Exported for unit testing only.\n */\nexport function wrapCJSExport(content: string): WrapResult {\n const cjsMarker = \"module.exports\";\n const cjsIdx = content.lastIndexOf(cjsMarker);\n if (cjsIdx === -1) {\n return { content, wrapped: false };\n }\n\n const preamble = content.slice(0, cjsIdx);\n const afterMarker = content.slice(cjsIdx + cjsMarker.length);\n const eqMatch = /^\\s*=\\s*/.exec(afterMarker);\n if (!eqMatch) {\n return { content, wrapped: false };\n }\n\n const exprRaw = afterMarker.slice(eqMatch[0].length);\n const expr = exprRaw.trim().replace(/;?\\s*$/, \"\");\n if (expr.length === 0) {\n return { content, wrapped: false };\n }\n\n return {\n content: preamble + `module.exports = withGlasstraceConfig(${expr});\\n`,\n wrapped: true,\n };\n}\n\n/**\n * Extracts the value of `GLASSTRACE_API_KEY` from a `.env.local`-style\n * string. Returns the raw (unquoted) value, or `null` if the key is\n * absent, commented out, or empty.\n *\n * Only uncommented assignments are considered — a `# GLASSTRACE_API_KEY=...`\n * placeholder is treated as if the key is not set.\n *\n * When multiple uncommented assignments are present, the **last**\n * effective value wins — matching typical `.env` override semantics\n * (later lines override earlier ones when loaded by dotenv-style\n * loaders). Placeholder values (empty or `your_key_here`) are skipped\n * so a trailing placeholder does not mask a real earlier value.\n *\n * @internal Exported for unit testing only.\n */\nexport function readEnvLocalApiKey(content: string): string | null {\n let last: string | null = null;\n const regex = /^\\s*GLASSTRACE_API_KEY\\s*=\\s*(.*)$/gm;\n let match: RegExpExecArray | null;\n while ((match = regex.exec(content)) !== null) {\n const raw = match[1].trim();\n if (raw === \"\") continue;\n const unquoted = raw.replace(/^(['\"])(.*)\\1$/, \"$2\");\n if (unquoted === \"\" || unquoted === \"your_key_here\") continue;\n last = unquoted;\n }\n return last;\n}\n\n/**\n * Returns true when the given API key value is a claimed developer key\n * (prefix `gt_dev_`). Defensive against leading/trailing whitespace.\n *\n * @internal Exported for unit testing only.\n */\nexport function isDevApiKey(value: string | null | undefined): boolean {\n if (value === null || value === undefined) return false;\n return value.trim().startsWith(\"gt_dev_\");\n}\n\n/**\n * Creates `.env.local` with `GLASSTRACE_API_KEY=` placeholder, or appends\n * to an existing file if it does not already contain `GLASSTRACE_API_KEY`.\n *\n * Preservation behavior (DISC-1247 Scenario 6): if an existing `.env.local`\n * already defines a developer key (`gt_dev_*`), the file is left untouched\n * so re-running `init` after an account claim does not overwrite the\n * claimed dev key.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns True if the file was created or modified, false if already configured.\n */\nexport async function scaffoldEnvLocal(projectRoot: string): Promise<boolean> {\n const filePath = path.join(projectRoot, \".env.local\");\n\n if (fs.existsSync(filePath)) {\n const existing = fs.readFileSync(filePath, \"utf-8\");\n if (/^\\s*#?\\s*GLASSTRACE_API_KEY\\s*=/m.test(existing)) {\n return false;\n }\n // Append with a newline separator if needed\n const separator = existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n fs.writeFileSync(filePath, existing + separator + \"# GLASSTRACE_API_KEY=your_key_here\\n\", \"utf-8\");\n return true;\n }\n\n fs.writeFileSync(filePath, \"# GLASSTRACE_API_KEY=your_key_here\\n\", \"utf-8\");\n return true;\n}\n\n/**\n * Adds `GLASSTRACE_COVERAGE_MAP=true` to `.env.local`.\n * Creates the file if it does not exist. If the key is already present\n * with a value other than `true`, it is updated in place.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns True if the file was created or modified, false if already set to `true`.\n */\nexport async function addCoverageMapEnv(projectRoot: string): Promise<boolean> {\n const filePath = path.join(projectRoot, \".env.local\");\n\n if (!fs.existsSync(filePath)) {\n fs.writeFileSync(filePath, \"GLASSTRACE_COVERAGE_MAP=true\\n\", \"utf-8\");\n return true;\n }\n\n const existing = fs.readFileSync(filePath, \"utf-8\");\n const keyRegex = /^(\\s*GLASSTRACE_COVERAGE_MAP\\s*=\\s*)(.*)$/m;\n const keyMatch = keyRegex.exec(existing);\n\n if (keyMatch) {\n const currentValue = keyMatch[2].trim();\n if (currentValue === \"true\") {\n // Already set to true — nothing to do\n return false;\n }\n // Key exists but is not `true` — update in place\n const updated = existing.replace(keyRegex, `${keyMatch[1]}true`);\n fs.writeFileSync(filePath, updated, \"utf-8\");\n return true;\n }\n\n const separator = existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n fs.writeFileSync(filePath, existing + separator + \"GLASSTRACE_COVERAGE_MAP=true\\n\", \"utf-8\");\n return true;\n}\n\n/**\n * Adds `.glasstrace/` to `.gitignore`, or creates `.gitignore` if missing.\n * Does not add duplicate entries.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns True if the file was created or modified, false if already configured.\n */\nexport async function scaffoldGitignore(projectRoot: string): Promise<boolean> {\n const filePath = path.join(projectRoot, \".gitignore\");\n\n if (fs.existsSync(filePath)) {\n const existing = fs.readFileSync(filePath, \"utf-8\");\n // Check line-by-line to avoid false positive partial matches\n const lines = existing.split(\"\\n\").map((l) => l.trim());\n if (lines.includes(\".glasstrace/\")) {\n return false;\n }\n const separator = existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n fs.writeFileSync(filePath, existing + separator + \".glasstrace/\\n\", \"utf-8\");\n return true;\n }\n\n fs.writeFileSync(filePath, \".glasstrace/\\n\", \"utf-8\");\n return true;\n}\n\n/**\n * Compares an existing MCP config file against the content init would\n * write. Returns `true` when they are semantically equal (JSON configs\n * are parsed and compared deeply; TOML configs use trimmed string\n * comparison). Returns `false` on parse errors or mismatch.\n *\n * Used by `init` to detect manually-edited MCP configs before\n * overwriting them (DISC-1247 Scenario 2c).\n *\n * @internal Exported for unit testing only.\n */\nexport function mcpConfigMatches(\n existingContent: string,\n expectedContent: string,\n): boolean {\n const trimmedExpected = expectedContent.trim();\n\n // Attempt JSON comparison first — init writes JSON for most agents.\n try {\n const existingParsed: unknown = JSON.parse(existingContent);\n const expectedParsed: unknown = JSON.parse(trimmedExpected);\n return JSON.stringify(canonicalize(existingParsed)) === JSON.stringify(canonicalize(expectedParsed));\n } catch {\n // Fall through to text comparison for TOML and other non-JSON formats.\n }\n\n return existingContent.trim() === trimmedExpected;\n}\n\n/**\n * Sorts object keys recursively to produce a canonical form suitable\n * for structural equality comparison via JSON.stringify.\n */\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n if (value !== null && typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(obj).sort()) {\n sorted[key] = canonicalize(obj[key]);\n }\n return sorted;\n }\n return value;\n}\n\n/**\n * Creates the `.glasstrace/mcp-connected` marker file, or overwrites it\n * if the key has changed (key rotation).\n *\n * The marker file records a SHA-256 fingerprint of the anonymous key and\n * the ISO 8601 timestamp when it was written. It is used by the nudge\n * system to suppress \"MCP not configured\" prompts.\n *\n * If the marker already exists with the same key fingerprint, this is a\n * no-op (the timestamp is NOT refreshed).\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @param anonKey - The anonymous API key to fingerprint.\n * @returns True if the marker was created or updated, false if it already\n * exists with the same key fingerprint.\n */\nexport async function scaffoldMcpMarker(\n projectRoot: string,\n anonKey: string,\n): Promise<boolean> {\n const dirPath = path.join(projectRoot, \".glasstrace\");\n const markerPath = path.join(dirPath, \"mcp-connected\");\n const keyHash = identityFingerprint(anonKey);\n\n // Check if marker already exists with the same key hash\n if (fs.existsSync(markerPath)) {\n try {\n const existing = JSON.parse(fs.readFileSync(markerPath, \"utf-8\")) as {\n keyHash?: string;\n };\n if (existing.keyHash === keyHash) {\n return false;\n }\n } catch {\n // Corrupted marker — overwrite\n }\n }\n\n // Create directory with restricted permissions\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });\n\n const marker = JSON.stringify(\n { keyHash, configuredAt: new Date().toISOString() },\n null,\n 2,\n );\n\n fs.writeFileSync(markerPath, marker, { mode: 0o600 });\n\n // Ensure permissions even if file pre-existed (writeFile mode only\n // applies on creation on some platforms)\n fs.chmodSync(markerPath, 0o600);\n\n return true;\n}\n"],"mappings":"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","names":["wrapResult","modified"]}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDevApiKey,
|
|
3
|
+
readEnvLocalApiKey
|
|
4
|
+
} from "./chunk-A2AZL6MZ.js";
|
|
1
5
|
import {
|
|
2
6
|
NEXT_CONFIG_NAMES
|
|
3
7
|
} from "./chunk-BL3YDC6V.js";
|
|
@@ -315,12 +319,75 @@ function processTomlMcpConfig(content) {
|
|
|
315
319
|
}
|
|
316
320
|
return { action: "removed-section", content: result + "\n" };
|
|
317
321
|
}
|
|
322
|
+
function writeShutdownMarker(projectRoot) {
|
|
323
|
+
const dirPath = path.join(projectRoot, ".glasstrace");
|
|
324
|
+
if (!fs.existsSync(dirPath)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
const markerPath = path.join(dirPath, "shutdown-requested");
|
|
328
|
+
const tmpPath = `${markerPath}.tmp`;
|
|
329
|
+
const body = JSON.stringify({ requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
330
|
+
try {
|
|
331
|
+
fs.writeFileSync(tmpPath, body, { encoding: "utf-8", mode: 384 });
|
|
332
|
+
try {
|
|
333
|
+
fs.chmodSync(tmpPath, 384);
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
fs.renameSync(tmpPath, markerPath);
|
|
337
|
+
return true;
|
|
338
|
+
} catch {
|
|
339
|
+
try {
|
|
340
|
+
fs.unlinkSync(tmpPath);
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function defaultPrompt(question, defaultValue) {
|
|
347
|
+
if (!process.stdin.isTTY) return defaultValue;
|
|
348
|
+
const readline = await import("readline");
|
|
349
|
+
const rl = readline.createInterface({
|
|
350
|
+
input: process.stdin,
|
|
351
|
+
output: process.stdout
|
|
352
|
+
});
|
|
353
|
+
return new Promise((resolve) => {
|
|
354
|
+
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
355
|
+
rl.question(question + suffix, (answer) => {
|
|
356
|
+
rl.close();
|
|
357
|
+
const trimmed = answer.trim().toLowerCase();
|
|
358
|
+
if (trimmed === "") {
|
|
359
|
+
resolve(defaultValue);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
resolve(trimmed === "y" || trimmed === "yes");
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
}
|
|
318
366
|
async function runUninit(options) {
|
|
319
367
|
const { projectRoot, dryRun } = options;
|
|
368
|
+
const force = options.force === true;
|
|
369
|
+
const prompt = options.prompt ?? defaultPrompt;
|
|
320
370
|
const summary = [];
|
|
321
371
|
const warnings = [];
|
|
322
372
|
const errors = [];
|
|
323
373
|
const prefix = dryRun ? "[dry run] " : "";
|
|
374
|
+
try {
|
|
375
|
+
if (!dryRun) {
|
|
376
|
+
const markerWritten = writeShutdownMarker(projectRoot);
|
|
377
|
+
if (markerWritten) {
|
|
378
|
+
summary.push("Wrote .glasstrace/shutdown-requested marker");
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
const dirPath = path.join(projectRoot, ".glasstrace");
|
|
382
|
+
if (fs.existsSync(dirPath)) {
|
|
383
|
+
summary.push(`${prefix}Would write .glasstrace/shutdown-requested marker`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} catch (err) {
|
|
387
|
+
warnings.push(
|
|
388
|
+
`Shutdown marker write failed: ${err instanceof Error ? err.message : String(err)}`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
324
391
|
try {
|
|
325
392
|
let configHandled = false;
|
|
326
393
|
for (const name of NEXT_CONFIG_NAMES) {
|
|
@@ -403,23 +470,57 @@ async function runUninit(options) {
|
|
|
403
470
|
const envPath = path.join(projectRoot, ".env.local");
|
|
404
471
|
if (fs.existsSync(envPath)) {
|
|
405
472
|
const content = fs.readFileSync(envPath, "utf-8");
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
fs.unlinkSync(envPath);
|
|
416
|
-
}
|
|
417
|
-
summary.push(`${prefix}Deleted .env.local (no remaining entries)`);
|
|
473
|
+
const existingKey = readEnvLocalApiKey(content);
|
|
474
|
+
const hasDevKey = isDevApiKey(existingKey);
|
|
475
|
+
let proceed = true;
|
|
476
|
+
let devKeyPath = "none";
|
|
477
|
+
if (hasDevKey) {
|
|
478
|
+
if (dryRun) {
|
|
479
|
+
devKeyPath = "dry-run-preview";
|
|
480
|
+
} else if (force) {
|
|
481
|
+
devKeyPath = "force-bypass";
|
|
418
482
|
} else {
|
|
419
|
-
|
|
420
|
-
|
|
483
|
+
const confirmed = await prompt(
|
|
484
|
+
".env.local contains a claimed Glasstrace developer API key (gt_dev_...). Removing it will require you to re-authenticate. Continue?",
|
|
485
|
+
false
|
|
486
|
+
);
|
|
487
|
+
proceed = confirmed;
|
|
488
|
+
if (confirmed) devKeyPath = "interactive-confirmed";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (!proceed) {
|
|
492
|
+
warnings.push(
|
|
493
|
+
"Preserved GLASSTRACE_API_KEY in .env.local (claimed dev key; re-run with --force to remove)"
|
|
494
|
+
);
|
|
495
|
+
} else {
|
|
496
|
+
const lines = content.split("\n");
|
|
497
|
+
const filtered = lines.filter((line) => {
|
|
498
|
+
const trimmed = line.trim();
|
|
499
|
+
return !(/^\s*#?\s*GLASSTRACE_API_KEY\s*=/.test(trimmed) || /^\s*#?\s*GLASSTRACE_COVERAGE_MAP\s*=/.test(trimmed));
|
|
500
|
+
});
|
|
501
|
+
if (filtered.length !== lines.length) {
|
|
502
|
+
const result = filtered.join("\n");
|
|
503
|
+
if (result.trim().length === 0) {
|
|
504
|
+
if (!dryRun) {
|
|
505
|
+
fs.unlinkSync(envPath);
|
|
506
|
+
}
|
|
507
|
+
summary.push(`${prefix}Deleted .env.local (no remaining entries)`);
|
|
508
|
+
} else {
|
|
509
|
+
if (!dryRun) {
|
|
510
|
+
fs.writeFileSync(envPath, result, "utf-8");
|
|
511
|
+
}
|
|
512
|
+
let devKeyAnnotation = "";
|
|
513
|
+
if (devKeyPath === "interactive-confirmed") {
|
|
514
|
+
devKeyAnnotation = " (dev key confirmed)";
|
|
515
|
+
} else if (devKeyPath === "force-bypass") {
|
|
516
|
+
devKeyAnnotation = " (dev key removed via --force)";
|
|
517
|
+
} else if (devKeyPath === "dry-run-preview") {
|
|
518
|
+
devKeyAnnotation = " (dev key would be removed; real run would require confirmation)";
|
|
519
|
+
}
|
|
520
|
+
summary.push(
|
|
521
|
+
`${prefix}Removed GLASSTRACE entries from .env.local${devKeyAnnotation}`
|
|
522
|
+
);
|
|
421
523
|
}
|
|
422
|
-
summary.push(`${prefix}Removed GLASSTRACE entries from .env.local`);
|
|
423
524
|
}
|
|
424
525
|
}
|
|
425
526
|
}
|
|
@@ -579,6 +680,7 @@ export {
|
|
|
579
680
|
removeMarkerSection,
|
|
580
681
|
processJsonMcpConfig,
|
|
581
682
|
processTomlMcpConfig,
|
|
683
|
+
writeShutdownMarker,
|
|
582
684
|
runUninit
|
|
583
685
|
};
|
|
584
|
-
//# sourceMappingURL=chunk-
|
|
686
|
+
//# sourceMappingURL=chunk-ROFOJQWN.js.map
|