@glasstrace/sdk 0.0.1 → 0.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/LICENSE +22 -0
- package/README.md +13 -0
- package/dist/adapters/drizzle.cjs +87 -0
- package/dist/adapters/drizzle.cjs.map +1 -0
- package/dist/adapters/drizzle.d.cts +28 -0
- package/dist/adapters/drizzle.d.ts +28 -0
- package/dist/adapters/drizzle.js +62 -0
- package/dist/adapters/drizzle.js.map +1 -0
- package/dist/chunk-BKMITIEZ.js +169 -0
- package/dist/chunk-BKMITIEZ.js.map +1 -0
- package/dist/cli/init.cjs +537 -0
- package/dist/cli/init.cjs.map +1 -0
- package/dist/cli/init.d.cts +21 -0
- package/dist/cli/init.d.ts +21 -0
- package/dist/cli/init.js +343 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/index.cjs +1561 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +389 -0
- package/dist/index.d.ts +389 -0
- package/dist/index.js +1344 -0
- package/dist/index.js.map +1 -0
- package/package.json +91 -8
- package/index.js +0 -1
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli/init.ts
|
|
32
|
+
var init_exports = {};
|
|
33
|
+
__export(init_exports, {
|
|
34
|
+
runInit: () => runInit
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(init_exports);
|
|
37
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
38
|
+
var path3 = __toESM(require("path"), 1);
|
|
39
|
+
var readline = __toESM(require("readline"), 1);
|
|
40
|
+
|
|
41
|
+
// src/cli/scaffolder.ts
|
|
42
|
+
var fs = __toESM(require("fs"), 1);
|
|
43
|
+
var path = __toESM(require("path"), 1);
|
|
44
|
+
var NEXT_CONFIG_NAMES = ["next.config.ts", "next.config.js", "next.config.mjs"];
|
|
45
|
+
async function scaffoldInstrumentation(projectRoot, force) {
|
|
46
|
+
const filePath = path.join(projectRoot, "instrumentation.ts");
|
|
47
|
+
if (fs.existsSync(filePath) && !force) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const content = `import { registerGlasstrace } from "@glasstrace/sdk";
|
|
51
|
+
|
|
52
|
+
export async function register() {
|
|
53
|
+
// Glasstrace must be registered before Prisma instrumentation
|
|
54
|
+
// to ensure all ORM spans are captured correctly.
|
|
55
|
+
// If you use @prisma/instrumentation, import it after this call.
|
|
56
|
+
registerGlasstrace();
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
async function scaffoldNextConfig(projectRoot) {
|
|
63
|
+
let configPath;
|
|
64
|
+
let configName;
|
|
65
|
+
for (const name of NEXT_CONFIG_NAMES) {
|
|
66
|
+
const candidate = path.join(projectRoot, name);
|
|
67
|
+
if (fs.existsSync(candidate)) {
|
|
68
|
+
configPath = candidate;
|
|
69
|
+
configName = name;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (configPath === void 0 || configName === void 0) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const existing = fs.readFileSync(configPath, "utf-8");
|
|
77
|
+
if (existing.includes("withGlasstraceConfig")) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const isESM = configName.endsWith(".ts") || configName.endsWith(".mjs");
|
|
81
|
+
if (isESM) {
|
|
82
|
+
const importLine = 'import { withGlasstraceConfig } from "@glasstrace/sdk";\n';
|
|
83
|
+
const wrapResult2 = wrapExport(existing);
|
|
84
|
+
if (!wrapResult2.wrapped) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const modified2 = importLine + "\n" + wrapResult2.content;
|
|
88
|
+
fs.writeFileSync(configPath, modified2, "utf-8");
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const requireLine = 'const { withGlasstraceConfig } = require("@glasstrace/sdk");\n';
|
|
92
|
+
const wrapResult = wrapCJSExport(existing);
|
|
93
|
+
if (!wrapResult.wrapped) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const modified = requireLine + "\n" + wrapResult.content;
|
|
97
|
+
fs.writeFileSync(configPath, modified, "utf-8");
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
function wrapExport(content) {
|
|
101
|
+
const marker = "export default";
|
|
102
|
+
const idx = content.lastIndexOf(marker);
|
|
103
|
+
if (idx === -1) {
|
|
104
|
+
return { content, wrapped: false };
|
|
105
|
+
}
|
|
106
|
+
const preamble = content.slice(0, idx);
|
|
107
|
+
const exprRaw = content.slice(idx + marker.length);
|
|
108
|
+
const expr = exprRaw.trim().replace(/;?\s*$/, "");
|
|
109
|
+
if (expr.length === 0) {
|
|
110
|
+
return { content, wrapped: false };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
content: preamble + `export default withGlasstraceConfig(${expr});
|
|
114
|
+
`,
|
|
115
|
+
wrapped: true
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function wrapCJSExport(content) {
|
|
119
|
+
const cjsMarker = "module.exports";
|
|
120
|
+
const cjsIdx = content.lastIndexOf(cjsMarker);
|
|
121
|
+
if (cjsIdx === -1) {
|
|
122
|
+
return { content, wrapped: false };
|
|
123
|
+
}
|
|
124
|
+
const preamble = content.slice(0, cjsIdx);
|
|
125
|
+
const afterMarker = content.slice(cjsIdx + cjsMarker.length);
|
|
126
|
+
const eqMatch = /^\s*=\s*/.exec(afterMarker);
|
|
127
|
+
if (!eqMatch) {
|
|
128
|
+
return { content, wrapped: false };
|
|
129
|
+
}
|
|
130
|
+
const exprRaw = afterMarker.slice(eqMatch[0].length);
|
|
131
|
+
const expr = exprRaw.trim().replace(/;?\s*$/, "");
|
|
132
|
+
if (expr.length === 0) {
|
|
133
|
+
return { content, wrapped: false };
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
content: preamble + `module.exports = withGlasstraceConfig(${expr});
|
|
137
|
+
`,
|
|
138
|
+
wrapped: true
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function scaffoldEnvLocal(projectRoot) {
|
|
142
|
+
const filePath = path.join(projectRoot, ".env.local");
|
|
143
|
+
if (fs.existsSync(filePath)) {
|
|
144
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
145
|
+
if (/^\s*GLASSTRACE_API_KEY\s*=/m.test(existing)) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
149
|
+
fs.writeFileSync(filePath, existing + separator + "GLASSTRACE_API_KEY=\n", "utf-8");
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
fs.writeFileSync(filePath, "GLASSTRACE_API_KEY=\n", "utf-8");
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
async function addCoverageMapEnv(projectRoot) {
|
|
156
|
+
const filePath = path.join(projectRoot, ".env.local");
|
|
157
|
+
if (!fs.existsSync(filePath)) {
|
|
158
|
+
fs.writeFileSync(filePath, "GLASSTRACE_COVERAGE_MAP=true\n", "utf-8");
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
162
|
+
const keyRegex = /^(\s*GLASSTRACE_COVERAGE_MAP\s*=\s*)(.*)$/m;
|
|
163
|
+
const keyMatch = keyRegex.exec(existing);
|
|
164
|
+
if (keyMatch) {
|
|
165
|
+
const currentValue = keyMatch[2].trim();
|
|
166
|
+
if (currentValue === "true") {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const updated = existing.replace(keyRegex, `${keyMatch[1]}true`);
|
|
170
|
+
fs.writeFileSync(filePath, updated, "utf-8");
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
174
|
+
fs.writeFileSync(filePath, existing + separator + "GLASSTRACE_COVERAGE_MAP=true\n", "utf-8");
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
async function scaffoldGitignore(projectRoot) {
|
|
178
|
+
const filePath = path.join(projectRoot, ".gitignore");
|
|
179
|
+
if (fs.existsSync(filePath)) {
|
|
180
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
181
|
+
const lines = existing.split("\n").map((l) => l.trim());
|
|
182
|
+
if (lines.includes(".glasstrace/")) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
186
|
+
fs.writeFileSync(filePath, existing + separator + ".glasstrace/\n", "utf-8");
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
fs.writeFileSync(filePath, ".glasstrace/\n", "utf-8");
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/import-graph.ts
|
|
194
|
+
var fs2 = __toESM(require("fs/promises"), 1);
|
|
195
|
+
var fsSync = __toESM(require("fs"), 1);
|
|
196
|
+
var path2 = __toESM(require("path"), 1);
|
|
197
|
+
var crypto = __toESM(require("crypto"), 1);
|
|
198
|
+
var import_protocol = require("@glasstrace/protocol");
|
|
199
|
+
var MAX_TEST_FILES = 5e3;
|
|
200
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "dist", ".turbo"]);
|
|
201
|
+
var DEFAULT_TEST_PATTERNS = [
|
|
202
|
+
/\.test\.tsx?$/,
|
|
203
|
+
/\.spec\.tsx?$/
|
|
204
|
+
];
|
|
205
|
+
function globToRegExp(glob) {
|
|
206
|
+
const DOUBLE_STAR_PLACEHOLDER = "\0DSTAR\0";
|
|
207
|
+
const regexStr = glob.replace(/\*\*\//g, DOUBLE_STAR_PLACEHOLDER).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]+").replace(new RegExp(DOUBLE_STAR_PLACEHOLDER.replace(/\0/g, "\\0"), "g"), "(?:.+/)?");
|
|
208
|
+
return new RegExp("^" + regexStr + "$");
|
|
209
|
+
}
|
|
210
|
+
function loadCustomTestPatterns(projectRoot) {
|
|
211
|
+
const configNames = [
|
|
212
|
+
"vitest.config.ts",
|
|
213
|
+
"vitest.config.js",
|
|
214
|
+
"vitest.config.mts",
|
|
215
|
+
"vitest.config.mjs",
|
|
216
|
+
"vite.config.ts",
|
|
217
|
+
"vite.config.js",
|
|
218
|
+
"vite.config.mts",
|
|
219
|
+
"vite.config.mjs",
|
|
220
|
+
"jest.config.ts",
|
|
221
|
+
"jest.config.js",
|
|
222
|
+
"jest.config.mts",
|
|
223
|
+
"jest.config.mjs"
|
|
224
|
+
];
|
|
225
|
+
for (const name of configNames) {
|
|
226
|
+
const configPath = path2.join(projectRoot, name);
|
|
227
|
+
let content;
|
|
228
|
+
try {
|
|
229
|
+
content = fsSync.readFileSync(configPath, "utf-8");
|
|
230
|
+
} catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const isJest = name.startsWith("jest.");
|
|
235
|
+
let includeMatch = null;
|
|
236
|
+
if (isJest) {
|
|
237
|
+
includeMatch = /testMatch\s*:\s*\[([^\]]*)\]/s.exec(content);
|
|
238
|
+
} else {
|
|
239
|
+
const testBlockMatch = /\btest\s*[:{]\s*/s.exec(content);
|
|
240
|
+
if (testBlockMatch) {
|
|
241
|
+
const afterTest = content.slice(testBlockMatch.index, testBlockMatch.index + 500);
|
|
242
|
+
includeMatch = /include\s*:\s*\[([^\]]*)\]/s.exec(afterTest);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!includeMatch) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const arrayContent = includeMatch[1];
|
|
249
|
+
const stringRegex = /['"]([^'"]+)['"]/g;
|
|
250
|
+
const patterns = [];
|
|
251
|
+
let match;
|
|
252
|
+
match = stringRegex.exec(arrayContent);
|
|
253
|
+
while (match !== null) {
|
|
254
|
+
patterns.push(globToRegExp(match[1]));
|
|
255
|
+
match = stringRegex.exec(arrayContent);
|
|
256
|
+
}
|
|
257
|
+
if (patterns.length > 0) {
|
|
258
|
+
return patterns;
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
async function discoverTestFiles(projectRoot) {
|
|
267
|
+
const customPatterns = loadCustomTestPatterns(projectRoot);
|
|
268
|
+
const testPatterns = [...DEFAULT_TEST_PATTERNS, ...customPatterns];
|
|
269
|
+
const results = [];
|
|
270
|
+
try {
|
|
271
|
+
await walkForTests(projectRoot, projectRoot, results, testPatterns);
|
|
272
|
+
} catch {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
return results.slice(0, MAX_TEST_FILES);
|
|
276
|
+
}
|
|
277
|
+
async function walkForTests(baseDir, currentDir, results, testPatterns) {
|
|
278
|
+
if (results.length >= MAX_TEST_FILES) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
let entries;
|
|
282
|
+
try {
|
|
283
|
+
entries = await fs2.readdir(currentDir, { withFileTypes: true });
|
|
284
|
+
} catch {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
for (const entry of entries) {
|
|
288
|
+
if (results.length >= MAX_TEST_FILES) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const fullPath = path2.join(currentDir, entry.name);
|
|
292
|
+
if (entry.isDirectory()) {
|
|
293
|
+
if (EXCLUDED_DIRS.has(entry.name)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
await walkForTests(baseDir, fullPath, results, testPatterns);
|
|
297
|
+
} else if (entry.isFile()) {
|
|
298
|
+
const relativePath = path2.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
299
|
+
const isTestFile = testPatterns.some((p) => p.test(entry.name) || p.test(relativePath)) || relativePath.includes("__tests__");
|
|
300
|
+
if (isTestFile && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
|
|
301
|
+
results.push(relativePath);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function extractImports(fileContent) {
|
|
307
|
+
const seen = /* @__PURE__ */ new Set();
|
|
308
|
+
const imports = [];
|
|
309
|
+
const addUnique = (importPath) => {
|
|
310
|
+
if (!seen.has(importPath)) {
|
|
311
|
+
seen.add(importPath);
|
|
312
|
+
imports.push(importPath);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const esImportRegex = /import\s+(?:(?:[\w*{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
316
|
+
let match;
|
|
317
|
+
match = esImportRegex.exec(fileContent);
|
|
318
|
+
while (match !== null) {
|
|
319
|
+
addUnique(match[1]);
|
|
320
|
+
match = esImportRegex.exec(fileContent);
|
|
321
|
+
}
|
|
322
|
+
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
323
|
+
match = requireRegex.exec(fileContent);
|
|
324
|
+
while (match !== null) {
|
|
325
|
+
addUnique(match[1]);
|
|
326
|
+
match = requireRegex.exec(fileContent);
|
|
327
|
+
}
|
|
328
|
+
const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
329
|
+
match = dynamicImportRegex.exec(fileContent);
|
|
330
|
+
while (match !== null) {
|
|
331
|
+
addUnique(match[1]);
|
|
332
|
+
match = dynamicImportRegex.exec(fileContent);
|
|
333
|
+
}
|
|
334
|
+
return imports;
|
|
335
|
+
}
|
|
336
|
+
async function buildImportGraph(projectRoot) {
|
|
337
|
+
const testFiles = await discoverTestFiles(projectRoot);
|
|
338
|
+
const graph = {};
|
|
339
|
+
for (const testFile of testFiles) {
|
|
340
|
+
const fullPath = path2.join(projectRoot, testFile);
|
|
341
|
+
try {
|
|
342
|
+
const content = await fs2.readFile(fullPath, "utf-8");
|
|
343
|
+
const imports = extractImports(content);
|
|
344
|
+
graph[testFile] = imports;
|
|
345
|
+
} catch {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const sortedKeys = Object.keys(graph).sort();
|
|
350
|
+
const serialized = sortedKeys.map((key) => `${key}:${JSON.stringify(graph[key])}`).join("\n");
|
|
351
|
+
const hashHex = crypto.createHash("sha256").update(serialized).digest("hex");
|
|
352
|
+
const buildHash = (0, import_protocol.createBuildHash)(hashHex);
|
|
353
|
+
return { buildHash, graph };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/cli/init.ts
|
|
357
|
+
async function promptYesNo(question, defaultValue) {
|
|
358
|
+
if (!process.stdin.isTTY) {
|
|
359
|
+
return defaultValue;
|
|
360
|
+
}
|
|
361
|
+
const rl = readline.createInterface({
|
|
362
|
+
input: process.stdin,
|
|
363
|
+
output: process.stdout
|
|
364
|
+
});
|
|
365
|
+
return new Promise((resolve) => {
|
|
366
|
+
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
367
|
+
rl.question(question + suffix, (answer) => {
|
|
368
|
+
rl.close();
|
|
369
|
+
const trimmed = answer.trim().toLowerCase();
|
|
370
|
+
if (trimmed === "") {
|
|
371
|
+
resolve(defaultValue);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
resolve(trimmed === "y" || trimmed === "yes");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async function runInit(options) {
|
|
379
|
+
const { projectRoot, yes, coverageMap } = options;
|
|
380
|
+
const summary = [];
|
|
381
|
+
const warnings = [];
|
|
382
|
+
const errors = [];
|
|
383
|
+
const packageJsonPath = path3.join(projectRoot, "package.json");
|
|
384
|
+
if (!fs3.existsSync(packageJsonPath)) {
|
|
385
|
+
errors.push("No package.json found. Run this command from a Node.js project root.");
|
|
386
|
+
return { exitCode: 1, summary, warnings, errors };
|
|
387
|
+
}
|
|
388
|
+
const instrumentationPath = path3.join(projectRoot, "instrumentation.ts");
|
|
389
|
+
const instrumentationExists = fs3.existsSync(instrumentationPath);
|
|
390
|
+
let shouldWriteInstrumentation = true;
|
|
391
|
+
if (instrumentationExists && !yes) {
|
|
392
|
+
shouldWriteInstrumentation = await promptYesNo(
|
|
393
|
+
"instrumentation.ts already exists. Overwrite?",
|
|
394
|
+
false
|
|
395
|
+
);
|
|
396
|
+
} else if (instrumentationExists && yes) {
|
|
397
|
+
shouldWriteInstrumentation = false;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const created = await scaffoldInstrumentation(projectRoot, shouldWriteInstrumentation && instrumentationExists);
|
|
401
|
+
if (created) {
|
|
402
|
+
summary.push("Created instrumentation.ts");
|
|
403
|
+
} else if (instrumentationExists) {
|
|
404
|
+
summary.push("Skipped instrumentation.ts (already exists)");
|
|
405
|
+
}
|
|
406
|
+
} catch (err) {
|
|
407
|
+
errors.push(`Failed to write instrumentation.ts: ${err instanceof Error ? err.message : String(err)}`);
|
|
408
|
+
return { exitCode: 1, summary, warnings, errors };
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
const wrapped = await scaffoldNextConfig(projectRoot);
|
|
412
|
+
if (wrapped) {
|
|
413
|
+
summary.push("Wrapped next.config with withGlasstraceConfig()");
|
|
414
|
+
} else {
|
|
415
|
+
const hasNextConfig = ["next.config.ts", "next.config.js", "next.config.mjs"].some(
|
|
416
|
+
(name) => fs3.existsSync(path3.join(projectRoot, name))
|
|
417
|
+
);
|
|
418
|
+
if (hasNextConfig) {
|
|
419
|
+
summary.push("Skipped next.config (already contains withGlasstraceConfig)");
|
|
420
|
+
} else {
|
|
421
|
+
warnings.push("No next.config.* found. You may need to create one for Next.js projects.");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
errors.push(`Failed to modify next.config: ${err instanceof Error ? err.message : String(err)}`);
|
|
426
|
+
return { exitCode: 1, summary, warnings, errors };
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const envCreated = await scaffoldEnvLocal(projectRoot);
|
|
430
|
+
if (envCreated) {
|
|
431
|
+
summary.push("Updated .env.local with GLASSTRACE_API_KEY placeholder");
|
|
432
|
+
} else {
|
|
433
|
+
summary.push("Skipped .env.local (GLASSTRACE_API_KEY already present)");
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
errors.push(`Failed to update .env.local: ${err instanceof Error ? err.message : String(err)}`);
|
|
437
|
+
return { exitCode: 1, summary, warnings, errors };
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
const gitignoreUpdated = await scaffoldGitignore(projectRoot);
|
|
441
|
+
if (gitignoreUpdated) {
|
|
442
|
+
summary.push("Updated .gitignore with .glasstrace/");
|
|
443
|
+
} else {
|
|
444
|
+
summary.push("Skipped .gitignore (.glasstrace/ already listed)");
|
|
445
|
+
}
|
|
446
|
+
} catch (err) {
|
|
447
|
+
errors.push(`Failed to update .gitignore: ${err instanceof Error ? err.message : String(err)}`);
|
|
448
|
+
return { exitCode: 1, summary, warnings, errors };
|
|
449
|
+
}
|
|
450
|
+
let enableCoverageMap = coverageMap;
|
|
451
|
+
if (!yes && !coverageMap) {
|
|
452
|
+
if (process.stdin.isTTY) {
|
|
453
|
+
enableCoverageMap = await promptYesNo(
|
|
454
|
+
"Would you like to enable test coverage mapping?",
|
|
455
|
+
false
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (enableCoverageMap) {
|
|
460
|
+
try {
|
|
461
|
+
const added = await addCoverageMapEnv(projectRoot);
|
|
462
|
+
if (added) {
|
|
463
|
+
summary.push("Added GLASSTRACE_COVERAGE_MAP=true to .env.local");
|
|
464
|
+
}
|
|
465
|
+
} catch (err) {
|
|
466
|
+
warnings.push(`Failed to add coverage map env: ${err instanceof Error ? err.message : String(err)}`);
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
await buildImportGraph(projectRoot);
|
|
470
|
+
summary.push("Completed initial import graph scan");
|
|
471
|
+
} catch (err) {
|
|
472
|
+
warnings.push(`Import graph scan failed: ${err instanceof Error ? err.message : String(err)}. You can run it later.`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return { exitCode: 0, summary, warnings, errors };
|
|
476
|
+
}
|
|
477
|
+
function parseArgs(argv) {
|
|
478
|
+
const args = argv.slice(2);
|
|
479
|
+
let yes = false;
|
|
480
|
+
let coverageMap = false;
|
|
481
|
+
for (const arg of args) {
|
|
482
|
+
if (arg === "--yes" || arg === "-y") {
|
|
483
|
+
yes = true;
|
|
484
|
+
} else if (arg === "--coverage-map") {
|
|
485
|
+
coverageMap = true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!process.stdin.isTTY) {
|
|
489
|
+
yes = true;
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
projectRoot: process.cwd(),
|
|
493
|
+
yes,
|
|
494
|
+
coverageMap
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
var scriptPath = typeof process !== "undefined" && process.argv[1] !== void 0 ? process.argv[1].replace(/\\/g, "/") : void 0;
|
|
498
|
+
var scriptBasename = scriptPath !== void 0 ? path3.basename(scriptPath) : void 0;
|
|
499
|
+
var isDirectExecution = scriptPath !== void 0 && (scriptPath.endsWith("/cli/init.js") || scriptPath.endsWith("/cli/init.ts") || scriptBasename === "glasstrace");
|
|
500
|
+
if (isDirectExecution) {
|
|
501
|
+
const options = parseArgs(process.argv);
|
|
502
|
+
runInit(options).then((result) => {
|
|
503
|
+
if (result.errors.length > 0) {
|
|
504
|
+
for (const err of result.errors) {
|
|
505
|
+
process.stderr.write(`Error: ${err}
|
|
506
|
+
`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (result.warnings.length > 0) {
|
|
510
|
+
for (const warn of result.warnings) {
|
|
511
|
+
process.stderr.write(`Warning: ${warn}
|
|
512
|
+
`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (result.summary.length > 0) {
|
|
516
|
+
process.stderr.write("\nGlasstrace initialized successfully!\n\n");
|
|
517
|
+
for (const line of result.summary) {
|
|
518
|
+
process.stderr.write(` - ${line}
|
|
519
|
+
`);
|
|
520
|
+
}
|
|
521
|
+
process.stderr.write("\nNext steps:\n");
|
|
522
|
+
process.stderr.write(" 1. Set your GLASSTRACE_API_KEY in .env.local\n");
|
|
523
|
+
process.stderr.write(" 2. Start your Next.js dev server\n");
|
|
524
|
+
process.stderr.write(" 3. Glasstrace will begin capturing traces automatically\n\n");
|
|
525
|
+
}
|
|
526
|
+
process.exit(result.exitCode);
|
|
527
|
+
}).catch((err) => {
|
|
528
|
+
process.stderr.write(`Fatal error: ${err instanceof Error ? err.message : String(err)}
|
|
529
|
+
`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
534
|
+
0 && (module.exports = {
|
|
535
|
+
runInit
|
|
536
|
+
});
|
|
537
|
+
//# sourceMappingURL=init.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/init.ts","../../src/cli/scaffolder.ts","../../src/import-graph.ts"],"sourcesContent":["#!/usr/bin/env node\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as readline from \"node:readline\";\nimport {\n scaffoldInstrumentation,\n scaffoldNextConfig,\n scaffoldEnvLocal,\n scaffoldGitignore,\n addCoverageMapEnv,\n} from \"./scaffolder.js\";\nimport { buildImportGraph } from \"../import-graph.js\";\n\n/** Options for the init command (parsed from CLI args or passed programmatically). */\nexport interface InitOptions {\n projectRoot: string;\n yes: boolean;\n coverageMap: boolean;\n}\n\n/** Result of running the init command. */\nexport interface InitResult {\n exitCode: number;\n summary: string[];\n warnings: string[];\n errors: string[];\n}\n\n/**\n * Prompts the user with a yes/no question. Returns true for yes.\n * In non-interactive mode (no TTY), returns the default value.\n */\nasync function promptYesNo(question: string, defaultValue: boolean): Promise<boolean> {\n if (!process.stdin.isTTY) {\n return defaultValue;\n }\n\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\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 * Core init logic. Exported for testability — the CLI entry point at the\n * bottom calls this function and translates the result to process.exit().\n */\nexport async function runInit(options: InitOptions): Promise<InitResult> {\n const { projectRoot, yes, coverageMap } = options;\n const summary: string[] = [];\n const warnings: string[] = [];\n const errors: string[] = [];\n\n // Step 1: Detect package.json\n const packageJsonPath = path.join(projectRoot, \"package.json\");\n if (!fs.existsSync(packageJsonPath)) {\n errors.push(\"No package.json found. Run this command from a Node.js project root.\");\n return { exitCode: 1, summary, warnings, errors };\n }\n\n // Step 2 + 3: Generate instrumentation.ts\n const instrumentationPath = path.join(projectRoot, \"instrumentation.ts\");\n const instrumentationExists = fs.existsSync(instrumentationPath);\n let shouldWriteInstrumentation = true;\n\n if (instrumentationExists && !yes) {\n shouldWriteInstrumentation = await promptYesNo(\n \"instrumentation.ts already exists. Overwrite?\",\n false,\n );\n } else if (instrumentationExists && yes) {\n // Non-interactive: never overwrite (idempotent)\n shouldWriteInstrumentation = false;\n }\n\n try {\n const created = await scaffoldInstrumentation(projectRoot, shouldWriteInstrumentation && instrumentationExists);\n if (created) {\n summary.push(\"Created instrumentation.ts\");\n } else if (instrumentationExists) {\n summary.push(\"Skipped instrumentation.ts (already exists)\");\n }\n } catch (err) {\n errors.push(`Failed to write instrumentation.ts: ${err instanceof Error ? err.message : String(err)}`);\n return { exitCode: 1, summary, warnings, errors };\n }\n\n // Step 4: Detect and wrap next.config.*\n try {\n const wrapped = await scaffoldNextConfig(projectRoot);\n if (wrapped) {\n summary.push(\"Wrapped next.config with withGlasstraceConfig()\");\n } else {\n // Check if it was skipped because file already wrapped vs not found\n const hasNextConfig = [\"next.config.ts\", \"next.config.js\", \"next.config.mjs\"].some(\n (name) => fs.existsSync(path.join(projectRoot, name)),\n );\n if (hasNextConfig) {\n summary.push(\"Skipped next.config (already contains withGlasstraceConfig)\");\n } else {\n warnings.push(\"No next.config.* found. You may need to create one for Next.js projects.\");\n }\n }\n } catch (err) {\n errors.push(`Failed to modify next.config: ${err instanceof Error ? err.message : String(err)}`);\n return { exitCode: 1, summary, warnings, errors };\n }\n\n // Step 5: Update .env.local\n try {\n const envCreated = await scaffoldEnvLocal(projectRoot);\n if (envCreated) {\n summary.push(\"Updated .env.local with GLASSTRACE_API_KEY placeholder\");\n } else {\n summary.push(\"Skipped .env.local (GLASSTRACE_API_KEY already present)\");\n }\n } catch (err) {\n errors.push(`Failed to update .env.local: ${err instanceof Error ? err.message : String(err)}`);\n return { exitCode: 1, summary, warnings, errors };\n }\n\n // Step 6: Update .gitignore\n try {\n const gitignoreUpdated = await scaffoldGitignore(projectRoot);\n if (gitignoreUpdated) {\n summary.push(\"Updated .gitignore with .glasstrace/\");\n } else {\n summary.push(\"Skipped .gitignore (.glasstrace/ already listed)\");\n }\n } catch (err) {\n errors.push(`Failed to update .gitignore: ${err instanceof Error ? err.message : String(err)}`);\n return { exitCode: 1, summary, warnings, errors };\n }\n\n // Step 7: Coverage map opt-in\n let enableCoverageMap = coverageMap;\n if (!yes && !coverageMap) {\n if (process.stdin.isTTY) {\n enableCoverageMap = await promptYesNo(\n \"Would you like to enable test coverage mapping?\",\n false,\n );\n }\n }\n\n if (enableCoverageMap) {\n try {\n const added = await addCoverageMapEnv(projectRoot);\n if (added) {\n summary.push(\"Added GLASSTRACE_COVERAGE_MAP=true to .env.local\");\n }\n } catch (err) {\n warnings.push(`Failed to add coverage map env: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n // Step 8: Run initial import graph scan\n try {\n await buildImportGraph(projectRoot);\n summary.push(\"Completed initial import graph scan\");\n } catch (err) {\n warnings.push(`Import graph scan failed: ${err instanceof Error ? err.message : String(err)}. You can run it later.`);\n }\n }\n\n return { exitCode: 0, summary, warnings, errors };\n}\n\n/**\n * Parses CLI arguments into InitOptions.\n */\nfunction parseArgs(argv: string[]): InitOptions {\n const args = argv.slice(2); // skip node + script path\n let yes = false;\n let coverageMap = false;\n\n for (const arg of args) {\n if (arg === \"--yes\" || arg === \"-y\") {\n yes = true;\n } else if (arg === \"--coverage-map\") {\n coverageMap = true;\n }\n }\n\n // Auto-detect non-interactive\n if (!process.stdin.isTTY) {\n yes = true;\n }\n\n return {\n projectRoot: process.cwd(),\n yes,\n coverageMap,\n };\n}\n\n/**\n * CLI entry point. Only runs when this module is executed directly\n * (not when imported for testing).\n */\nconst scriptPath =\n typeof process !== \"undefined\" && process.argv[1] !== undefined\n ? process.argv[1].replace(/\\\\/g, \"/\")\n : undefined;\n\nconst scriptBasename = scriptPath !== undefined ? path.basename(scriptPath) : undefined;\n\nconst isDirectExecution =\n scriptPath !== undefined &&\n (scriptPath.endsWith(\"/cli/init.js\") ||\n scriptPath.endsWith(\"/cli/init.ts\") ||\n scriptBasename === \"glasstrace\");\n\nif (isDirectExecution) {\n const options = parseArgs(process.argv);\n\n runInit(options)\n .then((result) => {\n if (result.errors.length > 0) {\n for (const err of result.errors) {\n process.stderr.write(`Error: ${err}\\n`);\n }\n }\n if (result.warnings.length > 0) {\n for (const warn of result.warnings) {\n process.stderr.write(`Warning: ${warn}\\n`);\n }\n }\n if (result.summary.length > 0) {\n process.stderr.write(\"\\nGlasstrace initialized successfully!\\n\\n\");\n for (const line of result.summary) {\n process.stderr.write(` - ${line}\\n`);\n }\n process.stderr.write(\"\\nNext steps:\\n\");\n process.stderr.write(\" 1. Set your GLASSTRACE_API_KEY in .env.local\\n\");\n process.stderr.write(\" 2. Start your Next.js dev server\\n\");\n process.stderr.write(\" 3. Glasstrace will begin capturing traces automatically\\n\\n\");\n }\n process.exit(result.exitCode);\n })\n .catch((err: unknown) => {\n process.stderr.write(`Fatal error: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n });\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\n/** Next.js config file names in priority order */\nconst NEXT_CONFIG_NAMES = [\"next.config.ts\", \"next.config.js\", \"next.config.mjs\"] as const;\n\n/**\n * Generates `instrumentation.ts` with a `registerGlasstrace()` call.\n * If the file exists and `force` is false, the file is not overwritten.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @param force - When true, overwrite an existing instrumentation.ts file.\n * @returns True if the file was written, false if it was skipped.\n */\nexport async function scaffoldInstrumentation(\n projectRoot: string,\n force: boolean,\n): Promise<boolean> {\n const filePath = path.join(projectRoot, \"instrumentation.ts\");\n\n if (fs.existsSync(filePath) && !force) {\n return false;\n }\n\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\n fs.writeFileSync(filePath, content, \"utf-8\");\n return true;\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 True if the config file was modified (or created), false if skipped.\n */\nexport async function scaffoldNextConfig(\n projectRoot: string,\n): Promise<boolean> {\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 false;\n }\n\n const existing = fs.readFileSync(configPath, \"utf-8\");\n\n // Already wrapped — skip even in force mode to avoid double-wrapping\n if (existing.includes(\"withGlasstraceConfig\")) {\n return false;\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 false;\n }\n const modified = importLine + \"\\n\" + wrapResult.content;\n fs.writeFileSync(configPath, modified, \"utf-8\");\n return 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 false;\n }\n const modified = requireLine + \"\\n\" + wrapResult.content;\n fs.writeFileSync(configPath, modified, \"utf-8\");\n return true;\n}\n\ninterface 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 */\nfunction 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 */\nfunction 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 * 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 * @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*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=\\n\", \"utf-8\");\n return true;\n }\n\n fs.writeFileSync(filePath, \"GLASSTRACE_API_KEY=\\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","import * as fs from \"node:fs/promises\";\nimport * as fsSync from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport { createBuildHash, type ImportGraphPayload } from \"@glasstrace/protocol\";\n\n/** Maximum number of test files to process to prevent runaway in large projects */\nconst MAX_TEST_FILES = 5000;\n\n/** Directories to exclude from test file discovery */\nconst EXCLUDED_DIRS = new Set([\"node_modules\", \".next\", \".git\", \"dist\", \".turbo\"]);\n\n/** Conventional test file patterns */\nconst DEFAULT_TEST_PATTERNS = [\n /\\.test\\.tsx?$/,\n /\\.spec\\.tsx?$/,\n];\n\n/**\n * Converts a glob pattern (e.g. \"e2e/**\\/*.ts\") to an anchored RegExp.\n * Uses a placeholder to avoid `*` replacement corrupting the `**\\/` output.\n *\n * @param glob - A file glob pattern such as \"src/**\\/*.test.ts\".\n * @returns A RegExp that matches paths against the glob from start to end.\n */\nfunction globToRegExp(glob: string): RegExp {\n const DOUBLE_STAR_PLACEHOLDER = \"\\0DSTAR\\0\";\n const regexStr = glob\n .replace(/\\*\\*\\//g, DOUBLE_STAR_PLACEHOLDER) // protect **/ first\n .replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\") // escape all regex metacharacters (except *)\n .replace(/\\*/g, \"[^/]+\")\n .replace(new RegExp(DOUBLE_STAR_PLACEHOLDER.replace(/\\0/g, \"\\\\0\"), \"g\"), \"(?:.+/)?\");\n return new RegExp(\"^\" + regexStr + \"$\");\n}\n\n/**\n * Attempts to read include patterns from vitest.config.*, vite.config.*,\n * or jest.config.* files. Returns additional RegExp patterns extracted\n * from the config, or an empty array if no config is found or parsing fails.\n * This is best-effort — it reads the config as text and extracts patterns\n * via regex, without evaluating the JS.\n *\n * For Vitest/Vite configs, looks for `test.include` arrays.\n * For Jest configs, looks for `testMatch` arrays.\n * Does not support `testRegex` (string-based Jest pattern) — that is\n * left as future work.\n */\nfunction loadCustomTestPatterns(projectRoot: string): RegExp[] {\n const configNames = [\n \"vitest.config.ts\",\n \"vitest.config.js\",\n \"vitest.config.mts\",\n \"vitest.config.mjs\",\n \"vite.config.ts\",\n \"vite.config.js\",\n \"vite.config.mts\",\n \"vite.config.mjs\",\n \"jest.config.ts\",\n \"jest.config.js\",\n \"jest.config.mts\",\n \"jest.config.mjs\",\n ];\n\n for (const name of configNames) {\n const configPath = path.join(projectRoot, name);\n let content: string;\n try {\n content = fsSync.readFileSync(configPath, \"utf-8\");\n } catch {\n // Config file does not exist at this path — try next candidate\n continue;\n }\n\n try {\n const isJest = name.startsWith(\"jest.\");\n let includeMatch: RegExpExecArray | null = null;\n\n if (isJest) {\n // Jest: look for testMatch: [...]\n includeMatch = /testMatch\\s*:\\s*\\[([^\\]]*)\\]/s.exec(content);\n } else {\n // Vitest/Vite: look for `test` block's `include` to avoid\n // matching `coverage.include` or other unrelated arrays.\n // Strategy: find `test` property, then look for `include` within\n // the next ~500 chars (heuristic to stay within the test block).\n const testBlockMatch = /\\btest\\s*[:{]\\s*/s.exec(content);\n if (testBlockMatch) {\n const afterTest = content.slice(testBlockMatch.index, testBlockMatch.index + 500);\n includeMatch = /include\\s*:\\s*\\[([^\\]]*)\\]/s.exec(afterTest);\n }\n }\n\n if (!includeMatch) {\n continue;\n }\n\n const arrayContent = includeMatch[1];\n const stringRegex = /['\"]([^'\"]+)['\"]/g;\n const patterns: RegExp[] = [];\n let match: RegExpExecArray | null;\n match = stringRegex.exec(arrayContent);\n while (match !== null) {\n patterns.push(globToRegExp(match[1]));\n match = stringRegex.exec(arrayContent);\n }\n\n if (patterns.length > 0) {\n return patterns;\n }\n } catch {\n // Regex-based config parsing failed — fall through to next config file\n continue;\n }\n }\n\n return [];\n}\n\n/**\n * Discovers test files by scanning the project directory for conventional\n * test file patterns. Also reads vitest/jest config files for custom include\n * patterns and merges them with the defaults. Excludes node_modules/ and .next/.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns Relative POSIX paths from projectRoot, capped at {@link MAX_TEST_FILES}.\n */\nexport async function discoverTestFiles(\n projectRoot: string,\n): Promise<string[]> {\n const customPatterns = loadCustomTestPatterns(projectRoot);\n const testPatterns = [...DEFAULT_TEST_PATTERNS, ...customPatterns];\n const results: string[] = [];\n\n try {\n await walkForTests(projectRoot, projectRoot, results, testPatterns);\n } catch {\n // Project root directory does not exist or is unreadable — return empty\n return [];\n }\n\n return results.slice(0, MAX_TEST_FILES);\n}\n\n/** Recursively walks directories, collecting test file paths into `results`. */\nasync function walkForTests(\n baseDir: string,\n currentDir: string,\n results: string[],\n testPatterns: RegExp[],\n): Promise<void> {\n if (results.length >= MAX_TEST_FILES) {\n return;\n }\n\n let entries: import(\"node:fs\").Dirent[];\n try {\n entries = await fs.readdir(currentDir, { withFileTypes: true });\n } catch {\n // Directory is unreadable (permissions, broken symlink) — skip subtree\n return;\n }\n\n for (const entry of entries) {\n if (results.length >= MAX_TEST_FILES) {\n return;\n }\n\n const fullPath = path.join(currentDir, entry.name);\n\n if (entry.isDirectory()) {\n if (EXCLUDED_DIRS.has(entry.name)) {\n continue;\n }\n await walkForTests(baseDir, fullPath, results, testPatterns);\n } else if (entry.isFile()) {\n const relativePath = path.relative(baseDir, fullPath).replace(/\\\\/g, \"/\");\n\n // Check if it matches test patterns or is in __tests__\n const isTestFile =\n testPatterns.some((p) => p.test(entry.name) || p.test(relativePath)) ||\n relativePath.includes(\"__tests__\");\n\n if (isTestFile && (entry.name.endsWith(\".ts\") || entry.name.endsWith(\".tsx\"))) {\n results.push(relativePath);\n }\n }\n }\n}\n\n/**\n * Extracts import paths from file content using regex.\n * Handles ES module imports, CommonJS requires, and dynamic imports.\n *\n * @param fileContent - The full text content of a TypeScript/JavaScript file.\n * @returns An array of import path strings as written in the source (e.g. \"./foo\", \"react\").\n */\nexport function extractImports(fileContent: string): string[] {\n const seen = new Set<string>();\n const imports: string[] = [];\n\n /** Adds a path to the result if not already present. */\n const addUnique = (importPath: string): void => {\n if (!seen.has(importPath)) {\n seen.add(importPath);\n imports.push(importPath);\n }\n };\n\n // ES module: import ... from 'path' or import 'path'\n const esImportRegex = /import\\s+(?:(?:[\\w*{}\\s,]+)\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\n let match: RegExpExecArray | null;\n\n match = esImportRegex.exec(fileContent);\n while (match !== null) {\n addUnique(match[1]);\n match = esImportRegex.exec(fileContent);\n }\n\n // CommonJS: require('path')\n const requireRegex = /require\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n match = requireRegex.exec(fileContent);\n while (match !== null) {\n addUnique(match[1]);\n match = requireRegex.exec(fileContent);\n }\n\n // Dynamic import: import('path')\n const dynamicImportRegex = /import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n match = dynamicImportRegex.exec(fileContent);\n while (match !== null) {\n addUnique(match[1]);\n match = dynamicImportRegex.exec(fileContent);\n }\n\n return imports;\n}\n\n/**\n * Builds an import graph mapping test file paths to their imported module paths.\n *\n * Discovers test files, reads each, extracts imports, and builds a graph.\n * Computes a deterministic buildHash from the serialized graph content.\n * Individual file read failures are silently skipped.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns An {@link ImportGraphPayload} containing the graph and a deterministic buildHash.\n */\nexport async function buildImportGraph(\n projectRoot: string,\n): Promise<ImportGraphPayload> {\n const testFiles = await discoverTestFiles(projectRoot);\n const graph: Record<string, string[]> = {};\n\n for (const testFile of testFiles) {\n const fullPath = path.join(projectRoot, testFile);\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n const imports = extractImports(content);\n graph[testFile] = imports;\n } catch {\n // File is unreadable (permissions, deleted between discovery and read) — skip\n continue;\n }\n }\n\n // Compute deterministic build hash from graph content\n const sortedKeys = Object.keys(graph).sort();\n const serialized = sortedKeys\n .map((key) => `${key}:${JSON.stringify(graph[key])}`)\n .join(\"\\n\");\n const hashHex = crypto\n .createHash(\"sha256\")\n .update(serialized)\n .digest(\"hex\");\n const buildHash = createBuildHash(hashHex);\n\n return { buildHash, graph };\n}\n"],"mappings":"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","names":["fs","path","wrapResult","modified","fs","path"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Options for the init command (parsed from CLI args or passed programmatically). */
|
|
3
|
+
interface InitOptions {
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
yes: boolean;
|
|
6
|
+
coverageMap: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Result of running the init command. */
|
|
9
|
+
interface InitResult {
|
|
10
|
+
exitCode: number;
|
|
11
|
+
summary: string[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
errors: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Core init logic. Exported for testability — the CLI entry point at the
|
|
17
|
+
* bottom calls this function and translates the result to process.exit().
|
|
18
|
+
*/
|
|
19
|
+
declare function runInit(options: InitOptions): Promise<InitResult>;
|
|
20
|
+
|
|
21
|
+
export { type InitOptions, type InitResult, runInit };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Options for the init command (parsed from CLI args or passed programmatically). */
|
|
3
|
+
interface InitOptions {
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
yes: boolean;
|
|
6
|
+
coverageMap: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Result of running the init command. */
|
|
9
|
+
interface InitResult {
|
|
10
|
+
exitCode: number;
|
|
11
|
+
summary: string[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
errors: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Core init logic. Exported for testability — the CLI entry point at the
|
|
17
|
+
* bottom calls this function and translates the result to process.exit().
|
|
18
|
+
*/
|
|
19
|
+
declare function runInit(options: InitOptions): Promise<InitResult>;
|
|
20
|
+
|
|
21
|
+
export { type InitOptions, type InitResult, runInit };
|