autotel-cli 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +581 -73
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/skills/autotel-cli/SKILL.md +23 -21
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
-
import
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import * as path$1 from "node:path";
|
|
6
|
+
import path from "node:path";
|
|
5
7
|
import { execSync } from "node:child_process";
|
|
6
|
-
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
7
9
|
import { glob } from "glob";
|
|
8
10
|
import chalk from "chalk";
|
|
9
11
|
import { checkbox, confirm, select } from "@inquirer/prompts";
|
|
@@ -12,20 +14,23 @@ import * as semver from "semver";
|
|
|
12
14
|
import { Project, SyntaxKind } from "ts-morph";
|
|
13
15
|
import { fileURLToPath } from "node:url";
|
|
14
16
|
import { buildCollectorGuide, buildInstrumentationGuide, clearSemanticConventionCache, collectUsage, detectAnomalies, discoverLogFields, discoverServices, discoverTraceFields, findRootCause, getCollectorComponentReadme, getCollectorComponentSchema, getModelStats, getSemanticConventionNamespace, listCollectorComponents, listCollectorVersions, listModels, listSemanticConventionNamespaces, listToolUsage, pickErrorMessage, rankExpensiveTraces, rankSlowTraces, refreshCollectorCatalog, resolveCollectorVersion, scoreSpan, suggestCollectorConfig, suggestInstrumentationFixes, toLogSearchQuery, toMetricSearchQuery, toSpanSearchQuery, toTraceSearchQuery, validateCollectorComponentConfig, validateOtlpReceiverConfig } from "autotel-mcp";
|
|
17
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
|
+
import { appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
19
|
+
import { homedir } from "node:os";
|
|
15
20
|
//#region src/lib/fs.ts
|
|
16
21
|
/**
|
|
17
22
|
* Check if a path is within the allowed root directory
|
|
18
23
|
*/
|
|
19
24
|
function isPathWithinRoot(targetPath, rootPath) {
|
|
20
|
-
const resolvedTarget = path.resolve(targetPath);
|
|
21
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
22
|
-
return resolvedTarget.startsWith(resolvedRoot + path.sep) || resolvedTarget === resolvedRoot;
|
|
25
|
+
const resolvedTarget = path$1.resolve(targetPath);
|
|
26
|
+
const resolvedRoot = path$1.resolve(rootPath);
|
|
27
|
+
return resolvedTarget.startsWith(resolvedRoot + path$1.sep) || resolvedTarget === resolvedRoot;
|
|
23
28
|
}
|
|
24
29
|
/**
|
|
25
30
|
* Ensure parent directory exists
|
|
26
31
|
*/
|
|
27
32
|
function ensureDir(filePath) {
|
|
28
|
-
const dir = path.dirname(filePath);
|
|
33
|
+
const dir = path$1.dirname(filePath);
|
|
29
34
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
30
35
|
}
|
|
31
36
|
/**
|
|
@@ -71,7 +76,7 @@ function createBackup(filePath) {
|
|
|
71
76
|
* Atomic write - write to temp file then rename
|
|
72
77
|
*/
|
|
73
78
|
function atomicWrite(filePath, content, options) {
|
|
74
|
-
const resolvedPath = path.resolve(filePath);
|
|
79
|
+
const resolvedPath = path$1.resolve(filePath);
|
|
75
80
|
if (!isPathWithinRoot(resolvedPath, options.root)) throw new Error(`Path traversal detected: ${filePath} resolves outside root ${options.root}`);
|
|
76
81
|
let backupPath = null;
|
|
77
82
|
if (options.backup && fileExists(resolvedPath)) backupPath = createBackup(resolvedPath);
|
|
@@ -104,17 +109,17 @@ function readJsonSafe(filePath) {
|
|
|
104
109
|
* Find a file by walking up the directory tree
|
|
105
110
|
*/
|
|
106
111
|
function findUpward(startDir, filename, stopAtRoot = true) {
|
|
107
|
-
let currentDir = path.resolve(startDir);
|
|
108
|
-
const root = path.parse(currentDir).root;
|
|
112
|
+
let currentDir = path$1.resolve(startDir);
|
|
113
|
+
const root = path$1.parse(currentDir).root;
|
|
109
114
|
while (currentDir !== root) {
|
|
110
|
-
const filePath = path.join(currentDir, filename);
|
|
115
|
+
const filePath = path$1.join(currentDir, filename);
|
|
111
116
|
if (fileExists(filePath)) return filePath;
|
|
112
|
-
const parentDir = path.dirname(currentDir);
|
|
117
|
+
const parentDir = path$1.dirname(currentDir);
|
|
113
118
|
if (parentDir === currentDir) break;
|
|
114
119
|
currentDir = parentDir;
|
|
115
120
|
if (stopAtRoot && currentDir === root) break;
|
|
116
121
|
}
|
|
117
|
-
const rootFilePath = path.join(root, filename);
|
|
122
|
+
const rootFilePath = path$1.join(root, filename);
|
|
118
123
|
if (fileExists(rootFilePath)) return rootFilePath;
|
|
119
124
|
return null;
|
|
120
125
|
}
|
|
@@ -123,14 +128,14 @@ function findUpward(startDir, filename, stopAtRoot = true) {
|
|
|
123
128
|
*/
|
|
124
129
|
function findAllUpward(startDir, filenames) {
|
|
125
130
|
const found = /* @__PURE__ */ new Map();
|
|
126
|
-
let currentDir = path.resolve(startDir);
|
|
127
|
-
const root = path.parse(currentDir).root;
|
|
131
|
+
let currentDir = path$1.resolve(startDir);
|
|
132
|
+
const root = path$1.parse(currentDir).root;
|
|
128
133
|
while (currentDir !== root) {
|
|
129
134
|
for (const filename of filenames) if (!found.has(filename)) {
|
|
130
|
-
const filePath = path.join(currentDir, filename);
|
|
135
|
+
const filePath = path$1.join(currentDir, filename);
|
|
131
136
|
if (fileExists(filePath)) found.set(filename, filePath);
|
|
132
137
|
}
|
|
133
|
-
const parentDir = path.dirname(currentDir);
|
|
138
|
+
const parentDir = path$1.dirname(currentDir);
|
|
134
139
|
if (parentDir === currentDir) break;
|
|
135
140
|
currentDir = parentDir;
|
|
136
141
|
}
|
|
@@ -179,7 +184,7 @@ function detectPackageManager(startDir) {
|
|
|
179
184
|
let closestDepth = -1;
|
|
180
185
|
let closestPM = "npm";
|
|
181
186
|
for (const [lockfileName, lockfilePath] of foundLockfiles) {
|
|
182
|
-
const depth = lockfilePath.split(path.sep).length;
|
|
187
|
+
const depth = lockfilePath.split(path$1.sep).length;
|
|
183
188
|
const pm = LOCKFILE_MAP[lockfileName];
|
|
184
189
|
if (pm === void 0) continue;
|
|
185
190
|
if (depth > closestDepth || depth === closestDepth && PM_PRIORITY.indexOf(pm) < PM_PRIORITY.indexOf(closestPM)) {
|
|
@@ -210,28 +215,28 @@ function getInstallCommand(pm, packages, options = {}) {
|
|
|
210
215
|
* Detect workspace root markers
|
|
211
216
|
*/
|
|
212
217
|
function detectWorkspaceRoot(startDir) {
|
|
213
|
-
let currentDir = path.resolve(startDir);
|
|
214
|
-
const root = path.parse(currentDir).root;
|
|
218
|
+
let currentDir = path$1.resolve(startDir);
|
|
219
|
+
const root = path$1.parse(currentDir).root;
|
|
215
220
|
while (currentDir !== root) {
|
|
216
|
-
if (fileExists(path.join(currentDir, "pnpm-workspace.yaml"))) return {
|
|
221
|
+
if (fileExists(path$1.join(currentDir, "pnpm-workspace.yaml"))) return {
|
|
217
222
|
workspaceRoot: currentDir,
|
|
218
223
|
workspaceType: "pnpm"
|
|
219
224
|
};
|
|
220
|
-
if (fileExists(path.join(currentDir, "lerna.json"))) return {
|
|
225
|
+
if (fileExists(path$1.join(currentDir, "lerna.json"))) return {
|
|
221
226
|
workspaceRoot: currentDir,
|
|
222
227
|
workspaceType: "lerna"
|
|
223
228
|
};
|
|
224
|
-
const pkgJsonContent = readFileSafe(path.join(currentDir, "package.json"));
|
|
229
|
+
const pkgJsonContent = readFileSafe(path$1.join(currentDir, "package.json"));
|
|
225
230
|
if (pkgJsonContent) try {
|
|
226
231
|
if (JSON.parse(pkgJsonContent).workspaces) {
|
|
227
|
-
const hasYarnLock = fileExists(path.join(currentDir, "yarn.lock"));
|
|
232
|
+
const hasYarnLock = fileExists(path$1.join(currentDir, "yarn.lock"));
|
|
228
233
|
return {
|
|
229
234
|
workspaceRoot: currentDir,
|
|
230
235
|
workspaceType: hasYarnLock ? "yarn" : "npm"
|
|
231
236
|
};
|
|
232
237
|
}
|
|
233
238
|
} catch {}
|
|
234
|
-
const parentDir = path.dirname(currentDir);
|
|
239
|
+
const parentDir = path$1.dirname(currentDir);
|
|
235
240
|
if (parentDir === currentDir) break;
|
|
236
241
|
currentDir = parentDir;
|
|
237
242
|
}
|
|
@@ -250,15 +255,15 @@ function findPackageJson(startDir) {
|
|
|
250
255
|
if (!packageJsonPath) return null;
|
|
251
256
|
return {
|
|
252
257
|
packageJsonPath,
|
|
253
|
-
packageRoot: path.dirname(packageJsonPath)
|
|
258
|
+
packageRoot: path$1.dirname(packageJsonPath)
|
|
254
259
|
};
|
|
255
260
|
}
|
|
256
261
|
/**
|
|
257
262
|
* Detect if project uses TypeScript
|
|
258
263
|
*/
|
|
259
264
|
function detectTypeScript(packageRoot) {
|
|
260
|
-
if (fileExists(path.join(packageRoot, "tsconfig.json"))) return true;
|
|
261
|
-
const pkgJson = readJsonSafe(path.join(packageRoot, "package.json"));
|
|
265
|
+
if (fileExists(path$1.join(packageRoot, "tsconfig.json"))) return true;
|
|
266
|
+
const pkgJson = readJsonSafe(path$1.join(packageRoot, "package.json"));
|
|
262
267
|
if (pkgJson) {
|
|
263
268
|
if ("typescript" in {
|
|
264
269
|
...pkgJson.dependencies,
|
|
@@ -289,7 +294,7 @@ function buildWorkspaceInfo(packageRoot, cwd) {
|
|
|
289
294
|
* Discover project context from a directory
|
|
290
295
|
*/
|
|
291
296
|
function discoverProject(cwd) {
|
|
292
|
-
const resolvedCwd = path.resolve(cwd);
|
|
297
|
+
const resolvedCwd = path$1.resolve(cwd);
|
|
293
298
|
const pkgResult = findPackageJson(resolvedCwd);
|
|
294
299
|
if (!pkgResult) return null;
|
|
295
300
|
const { packageJsonPath, packageRoot } = pkgResult;
|
|
@@ -325,15 +330,15 @@ function getEntrypointCandidates(packageRoot) {
|
|
|
325
330
|
"server.js",
|
|
326
331
|
"app.js",
|
|
327
332
|
"index.js"
|
|
328
|
-
].map((c) => path.join(packageRoot, c)).filter((p) => fileExists(p));
|
|
333
|
+
].map((c) => path$1.join(packageRoot, c)).filter((p) => fileExists(p));
|
|
329
334
|
}
|
|
330
335
|
/**
|
|
331
336
|
* Get instrumentation file path based on project conventions
|
|
332
337
|
*/
|
|
333
338
|
function getInstrumentationPath(packageRoot, hasTypeScript) {
|
|
334
|
-
const dir = dirExists(path.join(packageRoot, "src")) || fileExists(path.join(packageRoot, "src", "index.ts")) || fileExists(path.join(packageRoot, "src", "index.js")) ? path.join(packageRoot, "src") : packageRoot;
|
|
339
|
+
const dir = dirExists(path$1.join(packageRoot, "src")) || fileExists(path$1.join(packageRoot, "src", "index.ts")) || fileExists(path$1.join(packageRoot, "src", "index.js")) ? path$1.join(packageRoot, "src") : packageRoot;
|
|
335
340
|
const ext = hasTypeScript ? "mts" : "mjs";
|
|
336
|
-
return path.join(dir, `instrumentation.${ext}`);
|
|
341
|
+
return path$1.join(dir, `instrumentation.${ext}`);
|
|
337
342
|
}
|
|
338
343
|
//#endregion
|
|
339
344
|
//#region src/lib/code-builder.ts
|
|
@@ -559,7 +564,7 @@ const INSTRUMENTATION_LOCATIONS = [
|
|
|
559
564
|
*/
|
|
560
565
|
function findInstrumentationFile(packageRoot) {
|
|
561
566
|
for (const location of INSTRUMENTATION_LOCATIONS) {
|
|
562
|
-
const filePath = path.join(packageRoot, location);
|
|
567
|
+
const filePath = path$1.join(packageRoot, location);
|
|
563
568
|
if (fileExists(filePath)) {
|
|
564
569
|
const content = readFileSafe(filePath);
|
|
565
570
|
if (content === null) continue;
|
|
@@ -589,7 +594,7 @@ function findAutotelYaml(packageRoot) {
|
|
|
589
594
|
".autotelrc.yaml",
|
|
590
595
|
".autotelrc.yml"
|
|
591
596
|
]) {
|
|
592
|
-
const filePath = path.join(packageRoot, candidate);
|
|
597
|
+
const filePath = path$1.join(packageRoot, candidate);
|
|
593
598
|
if (fileExists(filePath)) return filePath;
|
|
594
599
|
}
|
|
595
600
|
return null;
|
|
@@ -700,7 +705,7 @@ async function findEnvFiles(packageRoot) {
|
|
|
700
705
|
* Check if env var is present in any env file
|
|
701
706
|
*/
|
|
702
707
|
async function checkEnvVarPresent(packageRoot, varName, specificFile) {
|
|
703
|
-
const files = specificFile ? [path.resolve(packageRoot, specificFile)] : await findEnvFiles(packageRoot);
|
|
708
|
+
const files = specificFile ? [path$1.resolve(packageRoot, specificFile)] : await findEnvFiles(packageRoot);
|
|
704
709
|
for (const file of files) {
|
|
705
710
|
const content = readFileSafe(file);
|
|
706
711
|
if (content === null) continue;
|
|
@@ -741,6 +746,10 @@ const DEP_TO_PRESET = {
|
|
|
741
746
|
"@tanstack/start": "tanstack",
|
|
742
747
|
"@tanstack/start-server": "tanstack",
|
|
743
748
|
"@tanstack/start-client": "tanstack",
|
|
749
|
+
"@nestjs/core": "nestjs",
|
|
750
|
+
"@sveltejs/kit": "sveltekit",
|
|
751
|
+
elysia: "elysia",
|
|
752
|
+
nuxt: "nuxt",
|
|
744
753
|
"posthog-node": "posthog",
|
|
745
754
|
"@posthog/node": "posthog",
|
|
746
755
|
mixpanel: "mixpanel",
|
|
@@ -752,7 +761,6 @@ const DEP_TO_PRESET = {
|
|
|
752
761
|
"drizzle-orm": "drizzle",
|
|
753
762
|
express: "auto-instr",
|
|
754
763
|
fastify: "auto-instr",
|
|
755
|
-
"@nestjs/core": "auto-instr",
|
|
756
764
|
next: "auto-instr",
|
|
757
765
|
pg: "auto-instr",
|
|
758
766
|
mysql: "auto-instr",
|
|
@@ -858,7 +866,7 @@ function collectEnvKeys(opts) {
|
|
|
858
866
|
const sources = [];
|
|
859
867
|
const tryRead = (name, requiresConsent) => {
|
|
860
868
|
if (requiresConsent && !opts.envConsent) return;
|
|
861
|
-
const content = readFileSafe(path.join(opts.packageRoot, name));
|
|
869
|
+
const content = readFileSafe(path$1.join(opts.packageRoot, name));
|
|
862
870
|
if (content === null) return;
|
|
863
871
|
sources.push(name);
|
|
864
872
|
for (const k of parseEnvKeys(content)) keys.add(k);
|
|
@@ -892,7 +900,7 @@ function detectBackend(opts) {
|
|
|
892
900
|
};
|
|
893
901
|
}
|
|
894
902
|
function detectPlatform(packageRoot) {
|
|
895
|
-
if (fileExists(path.join(packageRoot, "wrangler.toml")) || fileExists(path.join(packageRoot, "wrangler.jsonc")) || fileExists(path.join(packageRoot, "wrangler.json"))) return "cloudflare";
|
|
903
|
+
if (fileExists(path$1.join(packageRoot, "wrangler.toml")) || fileExists(path$1.join(packageRoot, "wrangler.jsonc")) || fileExists(path$1.join(packageRoot, "wrangler.json"))) return "cloudflare";
|
|
896
904
|
return null;
|
|
897
905
|
}
|
|
898
906
|
/**
|
|
@@ -901,7 +909,7 @@ function detectPlatform(packageRoot) {
|
|
|
901
909
|
function detectInProject(opts) {
|
|
902
910
|
const { project } = opts;
|
|
903
911
|
let rootPkg = null;
|
|
904
|
-
if (project.workspace.workspaceRoot !== null && project.workspace.workspaceRoot !== project.packageRoot) rootPkg = readJsonSafe(path.join(project.workspace.workspaceRoot, "package.json"));
|
|
912
|
+
if (project.workspace.workspaceRoot !== null && project.workspace.workspaceRoot !== project.packageRoot) rootPkg = readJsonSafe(path$1.join(project.workspace.workspaceRoot, "package.json"));
|
|
905
913
|
const deps = mergeDeps(project.packageJson, rootPkg);
|
|
906
914
|
const detectedPackages = [];
|
|
907
915
|
const presetSet = /* @__PURE__ */ new Set();
|
|
@@ -944,7 +952,7 @@ function detectInProject(opts) {
|
|
|
944
952
|
* to read it). .env.example is committed and doesn't trigger this.
|
|
945
953
|
*/
|
|
946
954
|
function envFilesRequireConsent(packageRoot) {
|
|
947
|
-
return fileExists(path.join(packageRoot, ".env")) || fileExists(path.join(packageRoot, ".env.local"));
|
|
955
|
+
return fileExists(path$1.join(packageRoot, ".env")) || fileExists(path$1.join(packageRoot, ".env.local"));
|
|
948
956
|
}
|
|
949
957
|
//#endregion
|
|
950
958
|
//#region src/presets/backends/datadog.ts
|
|
@@ -1802,6 +1810,120 @@ const tanstack = {
|
|
|
1802
1810
|
]
|
|
1803
1811
|
};
|
|
1804
1812
|
//#endregion
|
|
1813
|
+
//#region src/presets/plugins/nestjs.ts
|
|
1814
|
+
const nestjs = {
|
|
1815
|
+
name: "NestJS",
|
|
1816
|
+
slug: "nestjs",
|
|
1817
|
+
type: "plugin",
|
|
1818
|
+
description: "Request-scoped logging via AutotelInterceptor",
|
|
1819
|
+
packages: {
|
|
1820
|
+
required: ["autotel-adapters"],
|
|
1821
|
+
optional: [],
|
|
1822
|
+
devOnly: []
|
|
1823
|
+
},
|
|
1824
|
+
env: {
|
|
1825
|
+
required: [],
|
|
1826
|
+
optional: []
|
|
1827
|
+
},
|
|
1828
|
+
imports: [{
|
|
1829
|
+
source: "autotel-adapters/nestjs",
|
|
1830
|
+
specifiers: ["AutotelInterceptor"]
|
|
1831
|
+
}, {
|
|
1832
|
+
source: "@nestjs/core",
|
|
1833
|
+
specifiers: ["APP_INTERCEPTOR"]
|
|
1834
|
+
}],
|
|
1835
|
+
configBlock: {
|
|
1836
|
+
type: "plugin",
|
|
1837
|
+
code: `// Register globally in AppModule providers:
|
|
1838
|
+
// { provide: APP_INTERCEPTOR, useClass: AutotelInterceptor }`,
|
|
1839
|
+
section: "PLUGIN_INIT"
|
|
1840
|
+
},
|
|
1841
|
+
nextSteps: ["Register AutotelInterceptor via APP_INTERCEPTOR in AppModule", "Call useLogger() from controllers/services inside requests"]
|
|
1842
|
+
};
|
|
1843
|
+
//#endregion
|
|
1844
|
+
//#region src/presets/plugins/sveltekit.ts
|
|
1845
|
+
const sveltekit = {
|
|
1846
|
+
name: "SvelteKit",
|
|
1847
|
+
slug: "sveltekit",
|
|
1848
|
+
type: "plugin",
|
|
1849
|
+
description: "Server handle hook with request-scoped Autotel logger",
|
|
1850
|
+
packages: {
|
|
1851
|
+
required: ["autotel-adapters"],
|
|
1852
|
+
optional: [],
|
|
1853
|
+
devOnly: []
|
|
1854
|
+
},
|
|
1855
|
+
env: {
|
|
1856
|
+
required: [],
|
|
1857
|
+
optional: []
|
|
1858
|
+
},
|
|
1859
|
+
imports: [{
|
|
1860
|
+
source: "autotel-adapters/sveltekit",
|
|
1861
|
+
specifiers: ["autotelHandle"]
|
|
1862
|
+
}],
|
|
1863
|
+
configBlock: {
|
|
1864
|
+
type: "plugin",
|
|
1865
|
+
code: `// src/hooks.server.ts
|
|
1866
|
+
// export const handle = autotelHandle();`,
|
|
1867
|
+
section: "PLUGIN_INIT"
|
|
1868
|
+
},
|
|
1869
|
+
nextSteps: ["Export autotelHandle() from src/hooks.server.ts", "Use useLogger() in +server.ts routes and server loads"]
|
|
1870
|
+
};
|
|
1871
|
+
//#endregion
|
|
1872
|
+
//#region src/presets/plugins/elysia.ts
|
|
1873
|
+
const elysia = {
|
|
1874
|
+
name: "Elysia",
|
|
1875
|
+
slug: "elysia",
|
|
1876
|
+
type: "plugin",
|
|
1877
|
+
description: "Route handler wrapper with request-scoped logger",
|
|
1878
|
+
packages: {
|
|
1879
|
+
required: ["autotel-adapters"],
|
|
1880
|
+
optional: [],
|
|
1881
|
+
devOnly: []
|
|
1882
|
+
},
|
|
1883
|
+
env: {
|
|
1884
|
+
required: [],
|
|
1885
|
+
optional: []
|
|
1886
|
+
},
|
|
1887
|
+
imports: [{
|
|
1888
|
+
source: "autotel-adapters/elysia",
|
|
1889
|
+
specifiers: ["withAutotelHandler", "useLogger"]
|
|
1890
|
+
}],
|
|
1891
|
+
configBlock: {
|
|
1892
|
+
type: "plugin",
|
|
1893
|
+
code: `// app.get('/health', withAutotelHandler(async ({ path }) => {
|
|
1894
|
+
// useLogger().set({ route: path });
|
|
1895
|
+
// return { ok: true };
|
|
1896
|
+
// }));`,
|
|
1897
|
+
section: "PLUGIN_INIT"
|
|
1898
|
+
},
|
|
1899
|
+
nextSteps: ["Wrap route handlers with withAutotelHandler()", "Call useLogger() inside handlers for request-scoped context"]
|
|
1900
|
+
};
|
|
1901
|
+
//#endregion
|
|
1902
|
+
//#region src/presets/plugins/nuxt.ts
|
|
1903
|
+
const nuxt = {
|
|
1904
|
+
name: "Nuxt",
|
|
1905
|
+
slug: "nuxt",
|
|
1906
|
+
type: "plugin",
|
|
1907
|
+
description: "Nuxt module exposing Nitro Autotel adapters",
|
|
1908
|
+
packages: {
|
|
1909
|
+
required: ["autotel-nuxt", "autotel-adapters"],
|
|
1910
|
+
optional: [],
|
|
1911
|
+
devOnly: []
|
|
1912
|
+
},
|
|
1913
|
+
env: {
|
|
1914
|
+
required: [],
|
|
1915
|
+
optional: []
|
|
1916
|
+
},
|
|
1917
|
+
imports: [],
|
|
1918
|
+
configBlock: {
|
|
1919
|
+
type: "plugin",
|
|
1920
|
+
code: `// nuxt.config.ts
|
|
1921
|
+
// export default defineNuxtConfig({ modules: ['autotel-nuxt'] });`,
|
|
1922
|
+
section: "PLUGIN_INIT"
|
|
1923
|
+
},
|
|
1924
|
+
nextSteps: ["Add modules: ['autotel-nuxt'] to nuxt.config.ts", "Wrap server/api handlers with withAutotelEventHandler from autotel-nuxt/runtime/nitro"]
|
|
1925
|
+
};
|
|
1926
|
+
//#endregion
|
|
1805
1927
|
//#region src/presets/platforms/aws.ts
|
|
1806
1928
|
/**
|
|
1807
1929
|
* AWS Lambda platform preset
|
|
@@ -1926,7 +2048,11 @@ const plugins = /* @__PURE__ */ new Map([
|
|
|
1926
2048
|
["sentry", sentry],
|
|
1927
2049
|
["hono", hono],
|
|
1928
2050
|
["mcp", mcp],
|
|
1929
|
-
["tanstack", tanstack]
|
|
2051
|
+
["tanstack", tanstack],
|
|
2052
|
+
["nestjs", nestjs],
|
|
2053
|
+
["sveltekit", sveltekit],
|
|
2054
|
+
["elysia", elysia],
|
|
2055
|
+
["nuxt", nuxt]
|
|
1930
2056
|
]);
|
|
1931
2057
|
/**
|
|
1932
2058
|
* Platform presets registry
|
|
@@ -2061,12 +2187,12 @@ function buildPlanFromDetection(opts) {
|
|
|
2061
2187
|
}
|
|
2062
2188
|
const instrumentationPath = getInstrumentationPath(project.packageRoot, project.hasTypeScript);
|
|
2063
2189
|
const filesToWrite = [{
|
|
2064
|
-
path: path.relative(project.cwd, instrumentationPath),
|
|
2190
|
+
path: path$1.relative(project.cwd, instrumentationPath),
|
|
2065
2191
|
action: fileExists(instrumentationPath) ? "merge" : "create"
|
|
2066
2192
|
}];
|
|
2067
|
-
const envExamplePath = path.join(project.packageRoot, ".env.example");
|
|
2193
|
+
const envExamplePath = path$1.join(project.packageRoot, ".env.example");
|
|
2068
2194
|
if (envVars.length > 0 && !fileExists(envExamplePath)) filesToWrite.push({
|
|
2069
|
-
path: path.relative(project.cwd, envExamplePath),
|
|
2195
|
+
path: path$1.relative(project.cwd, envExamplePath),
|
|
2070
2196
|
action: "create"
|
|
2071
2197
|
});
|
|
2072
2198
|
const nextSteps = presets.flatMap((p) => p.nextSteps);
|
|
@@ -2425,10 +2551,10 @@ function printJson(data) {
|
|
|
2425
2551
|
}
|
|
2426
2552
|
}
|
|
2427
2553
|
function writeArtifactFile(filePath, content) {
|
|
2428
|
-
const resolved = path.resolve(filePath);
|
|
2554
|
+
const resolved = path$1.resolve(filePath);
|
|
2429
2555
|
if (outputRoot !== null) atomicWrite(resolved, content, { root: outputRoot });
|
|
2430
2556
|
else {
|
|
2431
|
-
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
2557
|
+
fs.mkdirSync(path$1.dirname(resolved), { recursive: true });
|
|
2432
2558
|
fs.writeFileSync(resolved, content, "utf8");
|
|
2433
2559
|
}
|
|
2434
2560
|
}
|
|
@@ -2789,7 +2915,7 @@ function applyPlan(args) {
|
|
|
2789
2915
|
if (codeFile.backendConfig === null) setBackendConfig(codeFile, "// Local/console mode - no backend configured");
|
|
2790
2916
|
const newContent = renderCodeFile(codeFile);
|
|
2791
2917
|
const instrumentationPath = resolveInstrumentationPath(project);
|
|
2792
|
-
const envExamplePath = path.join(project.packageRoot, ".env.example");
|
|
2918
|
+
const envExamplePath = path$1.join(project.packageRoot, ".env.example");
|
|
2793
2919
|
const existing = readFileSafe(instrumentationPath);
|
|
2794
2920
|
if (existing !== null) {
|
|
2795
2921
|
const parsed = parseInstrumentation(existing);
|
|
@@ -2809,24 +2935,24 @@ function applyPlan(args) {
|
|
|
2809
2935
|
].map((i) => i.source));
|
|
2810
2936
|
const addedAuto = diffAutoInstrumentations(parsed, codeFile.autoInstrumentations);
|
|
2811
2937
|
const contentChanged = existing !== newContent;
|
|
2812
|
-
if (addedImports.length === 0 && addedAuto.length === 0 && parsed.cliOwned && !contentChanged) result.wroteFiles.push(`${path.relative(project.cwd, instrumentationPath)} (no changes)`);
|
|
2938
|
+
if (addedImports.length === 0 && addedAuto.length === 0 && parsed.cliOwned && !contentChanged) result.wroteFiles.push(`${path$1.relative(project.cwd, instrumentationPath)} (no changes)`);
|
|
2813
2939
|
else {
|
|
2814
2940
|
atomicWrite(instrumentationPath, newContent, {
|
|
2815
2941
|
root: project.packageRoot,
|
|
2816
2942
|
backup: true
|
|
2817
2943
|
});
|
|
2818
|
-
result.wroteFiles.push(path.relative(project.cwd, instrumentationPath));
|
|
2944
|
+
result.wroteFiles.push(path$1.relative(project.cwd, instrumentationPath));
|
|
2819
2945
|
}
|
|
2820
2946
|
} else {
|
|
2821
2947
|
atomicWrite(instrumentationPath, newContent, { root: project.packageRoot });
|
|
2822
|
-
result.wroteFiles.push(path.relative(project.cwd, instrumentationPath));
|
|
2948
|
+
result.wroteFiles.push(path$1.relative(project.cwd, instrumentationPath));
|
|
2823
2949
|
}
|
|
2824
2950
|
const envVars = [];
|
|
2825
2951
|
for (const p of presets) envVars.push(...p.env.required, ...p.env.optional);
|
|
2826
2952
|
const envExampleContent = generateEnvExample(envVars);
|
|
2827
2953
|
if (envExampleContent.length > 0 && !fileExists(envExamplePath)) {
|
|
2828
2954
|
atomicWrite(envExamplePath, envExampleContent, { root: project.packageRoot });
|
|
2829
|
-
result.wroteFiles.push(path.relative(project.cwd, envExamplePath));
|
|
2955
|
+
result.wroteFiles.push(path$1.relative(project.cwd, envExamplePath));
|
|
2830
2956
|
}
|
|
2831
2957
|
const prod = plan.packagesToInstall.prod;
|
|
2832
2958
|
const dev = plan.packagesToInstall.dev;
|
|
@@ -2862,10 +2988,10 @@ function applyPlan(args) {
|
|
|
2862
2988
|
return result;
|
|
2863
2989
|
}
|
|
2864
2990
|
function resolveInstrumentationPath(project) {
|
|
2865
|
-
const srcDir = path.join(project.packageRoot, "src");
|
|
2866
|
-
const dir = fs.existsSync(srcDir) || fileExists(path.join(project.packageRoot, "src", "index.ts")) || fileExists(path.join(project.packageRoot, "src", "index.js")) ? srcDir : project.packageRoot;
|
|
2991
|
+
const srcDir = path$1.join(project.packageRoot, "src");
|
|
2992
|
+
const dir = fs.existsSync(srcDir) || fileExists(path$1.join(project.packageRoot, "src", "index.ts")) || fileExists(path$1.join(project.packageRoot, "src", "index.js")) ? srcDir : project.packageRoot;
|
|
2867
2993
|
const ext = project.hasTypeScript ? "mts" : "mjs";
|
|
2868
|
-
return path.join(dir, `instrumentation.${ext}`);
|
|
2994
|
+
return path$1.join(dir, `instrumentation.${ext}`);
|
|
2869
2995
|
}
|
|
2870
2996
|
function resolvePreset(slug) {
|
|
2871
2997
|
for (const type of [
|
|
@@ -3007,12 +3133,12 @@ function checkEsmHook(project) {
|
|
|
3007
3133
|
const result = checkRegisterImportOrder(content);
|
|
3008
3134
|
if (result.found && !result.isFirst) return {
|
|
3009
3135
|
status: "warn",
|
|
3010
|
-
message: `autotel/register import found but not first in ${path.basename(entrypoint)}:${result.lineNumber}`,
|
|
3136
|
+
message: `autotel/register import found but not first in ${path$1.basename(entrypoint)}:${result.lineNumber}`,
|
|
3011
3137
|
details: ["autotel/register must be the first import for instrumentation to work", "Move it to the top of the file, before any other imports"]
|
|
3012
3138
|
};
|
|
3013
3139
|
if (result.found && result.isFirst) return {
|
|
3014
3140
|
status: "ok",
|
|
3015
|
-
message: `autotel/register correctly imported first in ${path.basename(entrypoint)}`
|
|
3141
|
+
message: `autotel/register correctly imported first in ${path$1.basename(entrypoint)}`
|
|
3016
3142
|
};
|
|
3017
3143
|
}
|
|
3018
3144
|
return {
|
|
@@ -3059,12 +3185,12 @@ function extractAutoInstrumentations(content) {
|
|
|
3059
3185
|
*/
|
|
3060
3186
|
function findSourceFiles(packageRoot) {
|
|
3061
3187
|
const sourceFiles = [];
|
|
3062
|
-
const dirsToCheck = [packageRoot, path.join(packageRoot, "src")].filter((dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory());
|
|
3188
|
+
const dirsToCheck = [packageRoot, path$1.join(packageRoot, "src")].filter((dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory());
|
|
3063
3189
|
for (const dir of dirsToCheck) {
|
|
3064
3190
|
const files = fs.readdirSync(dir, { recursive: true });
|
|
3065
3191
|
for (const file of files) {
|
|
3066
3192
|
if (typeof file !== "string") continue;
|
|
3067
|
-
const filePath = path.join(dir, file);
|
|
3193
|
+
const filePath = path$1.join(dir, file);
|
|
3068
3194
|
try {
|
|
3069
3195
|
if (fs.statSync(filePath).isFile() && /\.(ts|js|mts|mjs|tsx|jsx)$/.test(file)) sourceFiles.push(filePath);
|
|
3070
3196
|
} catch {}
|
|
@@ -3746,8 +3872,8 @@ function addTraceImport(sourceFile) {
|
|
|
3746
3872
|
});
|
|
3747
3873
|
}
|
|
3748
3874
|
function expandNamePattern(pattern, name, filePath, cwd) {
|
|
3749
|
-
const file = path.basename(filePath, path.extname(filePath));
|
|
3750
|
-
const relPath = path.relative(cwd, filePath).replaceAll("\\", "/");
|
|
3875
|
+
const file = path$1.basename(filePath, path$1.extname(filePath));
|
|
3876
|
+
const relPath = path$1.relative(cwd, filePath).replaceAll("\\", "/");
|
|
3751
3877
|
return pattern.replaceAll("{name}", name).replaceAll("{file}", file).replaceAll("{path}", relPath);
|
|
3752
3878
|
}
|
|
3753
3879
|
function getSpanName(name, filePath, options) {
|
|
@@ -3816,8 +3942,14 @@ function transformFile(content, filePath, options) {
|
|
|
3816
3942
|
skip(spanName, "already wrapped");
|
|
3817
3943
|
continue;
|
|
3818
3944
|
}
|
|
3819
|
-
const
|
|
3820
|
-
const
|
|
3945
|
+
const modifiers = fn.getModifiers().map((m) => m.getText());
|
|
3946
|
+
const isAsync = modifiers.includes("async");
|
|
3947
|
+
const declModifiers = modifiers.filter((m) => m !== "async");
|
|
3948
|
+
const modPrefix = declModifiers.length > 0 ? declModifiers.join(" ") + " " : "";
|
|
3949
|
+
const fnText = fn.getText();
|
|
3950
|
+
const rest = fnText.replace(/^(?:\b(?:export|default|async|declare)\b\s*)*function\s*(\*?)\s*\w*\s*/, "");
|
|
3951
|
+
const star = /^(?:\b(?:export|default|async|declare)\b\s*)*function\s*\*/.test(fnText) ? "*" : "";
|
|
3952
|
+
const newText = `${modPrefix}const ${name} = trace('${spanName}', ${isAsync ? "async " : ""}function${star} ${name}${rest});`;
|
|
3821
3953
|
edits.push({
|
|
3822
3954
|
node: fn,
|
|
3823
3955
|
newText
|
|
@@ -3832,7 +3964,7 @@ function transformFile(content, filePath, options) {
|
|
|
3832
3964
|
if (shouldSkipName(spanName, options)) skip(spanName, "name match");
|
|
3833
3965
|
else {
|
|
3834
3966
|
const fn = defaultFn;
|
|
3835
|
-
const full = `const ${name} = trace('${spanName}', function ${name}(${(fn.getParameters?.() ?? []).map((p) => p.getText()).join(", ")}) ${(fn.getBody?.())?.getText() ?? "{}"};
|
|
3967
|
+
const full = `const ${name} = trace('${spanName}', function ${name}(${(fn.getParameters?.() ?? []).map((p) => p.getText()).join(", ")}) ${(fn.getBody?.())?.getText() ?? "{}"});
|
|
3836
3968
|
export default ` + name + ";";
|
|
3837
3969
|
edits.push({
|
|
3838
3970
|
node: defaultFn,
|
|
@@ -3849,7 +3981,7 @@ export default ` + name + ";";
|
|
|
3849
3981
|
const isArrow = kind === SyntaxKind.ArrowFunction;
|
|
3850
3982
|
const isFnExpr = kind === SyntaxKind.FunctionExpression;
|
|
3851
3983
|
if (kind === SyntaxKind.CallExpression) {
|
|
3852
|
-
if (init.getExpression().getText() === "trace") {
|
|
3984
|
+
if (init.asKindOrThrow(SyntaxKind.CallExpression).getExpression().getText() === "trace") {
|
|
3853
3985
|
const name = decl.getName();
|
|
3854
3986
|
if (typeof name === "string") {
|
|
3855
3987
|
const spanName = getSpanName(name, filePath, options);
|
|
@@ -3924,7 +4056,7 @@ export default ` + name + ";";
|
|
|
3924
4056
|
for (const stmt of sourceFile.getVariableStatements()) for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
3925
4057
|
const init = decl.getInitializer();
|
|
3926
4058
|
if (!init || init.getKind() !== SyntaxKind.ObjectLiteralExpression) continue;
|
|
3927
|
-
const obj = init;
|
|
4059
|
+
const obj = init.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
3928
4060
|
for (const prop of obj.getProperties()) {
|
|
3929
4061
|
if (prop.getKind() !== SyntaxKind.MethodDeclaration) continue;
|
|
3930
4062
|
const method = prop;
|
|
@@ -3979,19 +4111,19 @@ const GLOB_META = /[*?[\]]/;
|
|
|
3979
4111
|
*/
|
|
3980
4112
|
async function resolveCodemodFiles(pathArg, cwd) {
|
|
3981
4113
|
if (!GLOB_META.test(pathArg)) {
|
|
3982
|
-
const absolute = path.isAbsolute(pathArg) ? pathArg : path.resolve(cwd, pathArg);
|
|
4114
|
+
const absolute = path$1.isAbsolute(pathArg) ? pathArg : path$1.resolve(cwd, pathArg);
|
|
3983
4115
|
if (fileExists(absolute)) {
|
|
3984
|
-
const ext = path.extname(absolute);
|
|
4116
|
+
const ext = path$1.extname(absolute);
|
|
3985
4117
|
if (CODEMOD_EXTENSIONS.has(ext) && !absolute.endsWith(".d.ts")) return [absolute];
|
|
3986
4118
|
return [];
|
|
3987
4119
|
}
|
|
3988
4120
|
}
|
|
3989
|
-
return (await glob(path.isAbsolute(pathArg) ? pathArg : path.join(cwd, pathArg), {
|
|
4121
|
+
return (await glob(path$1.isAbsolute(pathArg) ? pathArg : path$1.join(cwd, pathArg), {
|
|
3990
4122
|
cwd,
|
|
3991
4123
|
absolute: true,
|
|
3992
4124
|
ignore: ["**/node_modules/**", "**/*.d.ts"]
|
|
3993
4125
|
})).filter((f) => {
|
|
3994
|
-
const ext = path.extname(f);
|
|
4126
|
+
const ext = path$1.extname(f);
|
|
3995
4127
|
return CODEMOD_EXTENSIONS.has(ext) && !f.endsWith(".d.ts");
|
|
3996
4128
|
});
|
|
3997
4129
|
}
|
|
@@ -4028,7 +4160,7 @@ async function runCodemodTrace(options) {
|
|
|
4028
4160
|
process.exitCode = 1;
|
|
4029
4161
|
continue;
|
|
4030
4162
|
}
|
|
4031
|
-
const relativePath = path.relative(cwd, filePath);
|
|
4163
|
+
const relativePath = path$1.relative(cwd, filePath);
|
|
4032
4164
|
if (result.changed) {
|
|
4033
4165
|
totalWrapped += result.wrappedCount;
|
|
4034
4166
|
totalChanged += 1;
|
|
@@ -4633,6 +4765,68 @@ const INVESTIGATE_COMMANDS = [
|
|
|
4633
4765
|
})
|
|
4634
4766
|
];
|
|
4635
4767
|
COMMANDS.push(...INVESTIGATE_COMMANDS);
|
|
4768
|
+
COMMANDS.push(...[
|
|
4769
|
+
{
|
|
4770
|
+
name: "telemetry",
|
|
4771
|
+
description: "Manage opt-in CLI usage telemetry (parent)",
|
|
4772
|
+
mutating: false,
|
|
4773
|
+
network: false,
|
|
4774
|
+
writesFiles: false,
|
|
4775
|
+
supportsDryRun: false,
|
|
4776
|
+
requiresPackageJson: false,
|
|
4777
|
+
mayReadEnv: false,
|
|
4778
|
+
supportsJson: false,
|
|
4779
|
+
flags: []
|
|
4780
|
+
},
|
|
4781
|
+
{
|
|
4782
|
+
name: "telemetry status",
|
|
4783
|
+
description: "Show telemetry consent status",
|
|
4784
|
+
mutating: false,
|
|
4785
|
+
network: false,
|
|
4786
|
+
writesFiles: false,
|
|
4787
|
+
supportsDryRun: false,
|
|
4788
|
+
requiresPackageJson: false,
|
|
4789
|
+
mayReadEnv: false,
|
|
4790
|
+
supportsJson: true,
|
|
4791
|
+
flags: []
|
|
4792
|
+
},
|
|
4793
|
+
{
|
|
4794
|
+
name: "telemetry enable",
|
|
4795
|
+
description: "Enable telemetry for this tool",
|
|
4796
|
+
mutating: true,
|
|
4797
|
+
network: false,
|
|
4798
|
+
writesFiles: true,
|
|
4799
|
+
supportsDryRun: false,
|
|
4800
|
+
requiresPackageJson: false,
|
|
4801
|
+
mayReadEnv: false,
|
|
4802
|
+
supportsJson: false,
|
|
4803
|
+
flags: []
|
|
4804
|
+
},
|
|
4805
|
+
{
|
|
4806
|
+
name: "telemetry disable",
|
|
4807
|
+
description: "Disable telemetry and purge undelivered events",
|
|
4808
|
+
mutating: true,
|
|
4809
|
+
network: false,
|
|
4810
|
+
writesFiles: true,
|
|
4811
|
+
supportsDryRun: false,
|
|
4812
|
+
requiresPackageJson: false,
|
|
4813
|
+
mayReadEnv: false,
|
|
4814
|
+
supportsJson: false,
|
|
4815
|
+
flags: []
|
|
4816
|
+
},
|
|
4817
|
+
{
|
|
4818
|
+
name: "telemetry disclosure",
|
|
4819
|
+
description: "Print telemetry disclosure markdown",
|
|
4820
|
+
mutating: false,
|
|
4821
|
+
network: false,
|
|
4822
|
+
writesFiles: false,
|
|
4823
|
+
supportsDryRun: false,
|
|
4824
|
+
requiresPackageJson: false,
|
|
4825
|
+
mayReadEnv: false,
|
|
4826
|
+
supportsJson: false,
|
|
4827
|
+
flags: []
|
|
4828
|
+
}
|
|
4829
|
+
]);
|
|
4636
4830
|
function getCommand(name) {
|
|
4637
4831
|
return COMMANDS.find((c) => c.name === name);
|
|
4638
4832
|
}
|
|
@@ -4714,14 +4908,14 @@ function configure(opts) {
|
|
|
4714
4908
|
}
|
|
4715
4909
|
function readSelfVersion() {
|
|
4716
4910
|
try {
|
|
4717
|
-
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
4911
|
+
let dir = path$1.dirname(fileURLToPath(import.meta.url));
|
|
4718
4912
|
for (let i = 0; i < 5; i++) {
|
|
4719
|
-
const candidate = path.join(dir, "package.json");
|
|
4913
|
+
const candidate = path$1.join(dir, "package.json");
|
|
4720
4914
|
if (fs.existsSync(candidate)) {
|
|
4721
4915
|
const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
4722
4916
|
if (pkg.name === "autotel-cli") return String(pkg.version ?? "0.0.0");
|
|
4723
4917
|
}
|
|
4724
|
-
dir = path.dirname(dir);
|
|
4918
|
+
dir = path$1.dirname(dir);
|
|
4725
4919
|
}
|
|
4726
4920
|
} catch {}
|
|
4727
4921
|
return "0.0.0";
|
|
@@ -6187,6 +6381,298 @@ function registerSecurityCommands(program) {
|
|
|
6187
6381
|
program.addCommand(securityCmd);
|
|
6188
6382
|
}
|
|
6189
6383
|
//#endregion
|
|
6384
|
+
//#region ../autotel-telemetry/dist/index.js
|
|
6385
|
+
function getTelemetryDir(toolName) {
|
|
6386
|
+
const safe = toolName.replaceAll(/[^a-zA-Z0-9._-]/g, "-");
|
|
6387
|
+
return path.join(homedir(), ".autotel", "telemetry", safe);
|
|
6388
|
+
}
|
|
6389
|
+
var TelemetryOutbox = class {
|
|
6390
|
+
dir;
|
|
6391
|
+
filePath;
|
|
6392
|
+
constructor(options) {
|
|
6393
|
+
this.dir = getTelemetryDir(options.toolName);
|
|
6394
|
+
this.filePath = path.join(this.dir, "outbox.ndjson");
|
|
6395
|
+
}
|
|
6396
|
+
async append(event) {
|
|
6397
|
+
await mkdir(this.dir, { recursive: true });
|
|
6398
|
+
await appendFile(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
|
|
6399
|
+
}
|
|
6400
|
+
async readAll() {
|
|
6401
|
+
try {
|
|
6402
|
+
return (await readFile(this.filePath, "utf8")).split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
6403
|
+
} catch {
|
|
6404
|
+
return [];
|
|
6405
|
+
}
|
|
6406
|
+
}
|
|
6407
|
+
async purge() {
|
|
6408
|
+
try {
|
|
6409
|
+
await unlink(this.filePath);
|
|
6410
|
+
} catch {}
|
|
6411
|
+
}
|
|
6412
|
+
};
|
|
6413
|
+
function resolveConsent(toolName) {
|
|
6414
|
+
if (process.env.DO_NOT_TRACK === "1") return false;
|
|
6415
|
+
if (process.env.AUTOTEL_TELEMETRY === "0") return false;
|
|
6416
|
+
if (process.env.AUTOTEL_TELEMETRY === "1") return true;
|
|
6417
|
+
const pref = readPreferenceSync(toolName);
|
|
6418
|
+
if (pref === "disabled") return false;
|
|
6419
|
+
if (pref === "enabled") return true;
|
|
6420
|
+
return false;
|
|
6421
|
+
}
|
|
6422
|
+
function readPreferenceSync(toolName) {
|
|
6423
|
+
try {
|
|
6424
|
+
const raw = readFileSync(path.join(getTelemetryDir(toolName), "preference.json"), "utf8");
|
|
6425
|
+
return JSON.parse(raw).preference ?? "unset";
|
|
6426
|
+
} catch {
|
|
6427
|
+
return "unset";
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
async function writePreference(toolName, preference) {
|
|
6431
|
+
const dir = getTelemetryDir(toolName);
|
|
6432
|
+
await mkdir(dir, { recursive: true });
|
|
6433
|
+
await writeFile(path.join(dir, "preference.json"), JSON.stringify({ preference }), "utf8");
|
|
6434
|
+
}
|
|
6435
|
+
async function purgeOutbox(toolName) {
|
|
6436
|
+
await new TelemetryOutbox({ toolName }).purge();
|
|
6437
|
+
}
|
|
6438
|
+
function createNoopDrain() {
|
|
6439
|
+
return async () => {};
|
|
6440
|
+
}
|
|
6441
|
+
function createDebugDrain() {
|
|
6442
|
+
return async (events) => {
|
|
6443
|
+
if (process.env.AUTOTEL_TELEMETRY_DEBUG === "1") for (const event of events) console.error("[autotel-telemetry]", JSON.stringify(event));
|
|
6444
|
+
};
|
|
6445
|
+
}
|
|
6446
|
+
const RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
6447
|
+
408,
|
|
6448
|
+
425,
|
|
6449
|
+
429
|
|
6450
|
+
]);
|
|
6451
|
+
function createHttpDrain(endpoint) {
|
|
6452
|
+
return async (events) => {
|
|
6453
|
+
if (events.length === 0) return;
|
|
6454
|
+
const response = await fetch(endpoint, {
|
|
6455
|
+
method: "POST",
|
|
6456
|
+
headers: { "content-type": "application/json" },
|
|
6457
|
+
body: JSON.stringify({ events })
|
|
6458
|
+
});
|
|
6459
|
+
if (response.ok) return;
|
|
6460
|
+
if (response.status >= 400 && response.status < 500 && !RETRYABLE_STATUS_CODES.has(response.status)) return;
|
|
6461
|
+
throw new Error(`Telemetry delivery failed with HTTP ${response.status}`);
|
|
6462
|
+
};
|
|
6463
|
+
}
|
|
6464
|
+
function composeDrains(...drains) {
|
|
6465
|
+
return async (events) => {
|
|
6466
|
+
await Promise.all(drains.map((drain) => drain(events)));
|
|
6467
|
+
};
|
|
6468
|
+
}
|
|
6469
|
+
function resolveEndpoint(explicit) {
|
|
6470
|
+
return explicit ?? process.env.AUTOTEL_TELEMETRY_ENDPOINT;
|
|
6471
|
+
}
|
|
6472
|
+
const SENSITIVE_PATTERN = /(?:token|secret|password|key|auth|credential|api[_-]?key|private)/i;
|
|
6473
|
+
function sanitizeFlags(argv, allowlistedStringFlags = []) {
|
|
6474
|
+
const out = {};
|
|
6475
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6476
|
+
const arg = argv[i];
|
|
6477
|
+
if (arg === void 0 || !arg.startsWith("-")) continue;
|
|
6478
|
+
const key = arg.replace(/^-+/, "").split("=")[0] ?? arg;
|
|
6479
|
+
if (SENSITIVE_PATTERN.test(key)) {
|
|
6480
|
+
out[key] = { present: true };
|
|
6481
|
+
continue;
|
|
6482
|
+
}
|
|
6483
|
+
if (arg.includes("=")) {
|
|
6484
|
+
const [, rawValue] = arg.split("=");
|
|
6485
|
+
if (rawValue === void 0) continue;
|
|
6486
|
+
if (/^\d+$/.test(rawValue)) out[key] = Number(rawValue);
|
|
6487
|
+
else if (allowlistedStringFlags.includes(key)) out[key] = rawValue;
|
|
6488
|
+
else out[key] = { present: true };
|
|
6489
|
+
continue;
|
|
6490
|
+
}
|
|
6491
|
+
const next = argv[i + 1];
|
|
6492
|
+
if (next && !next.startsWith("-")) if (/^\d+$/.test(next)) {
|
|
6493
|
+
out[key] = Number(next);
|
|
6494
|
+
i++;
|
|
6495
|
+
} else if (allowlistedStringFlags.includes(key)) {
|
|
6496
|
+
out[key] = next;
|
|
6497
|
+
i++;
|
|
6498
|
+
} else {
|
|
6499
|
+
out[key] = { present: true };
|
|
6500
|
+
i++;
|
|
6501
|
+
}
|
|
6502
|
+
else out[key] = true;
|
|
6503
|
+
}
|
|
6504
|
+
return out;
|
|
6505
|
+
}
|
|
6506
|
+
function sanitizeCustom(fields) {
|
|
6507
|
+
const out = {};
|
|
6508
|
+
for (const [key, value] of Object.entries(fields)) if (typeof value === "boolean" || typeof value === "number") out[key] = value;
|
|
6509
|
+
else if (value !== null && value !== void 0) out[key] = { present: true };
|
|
6510
|
+
return out;
|
|
6511
|
+
}
|
|
6512
|
+
const runStorage = new AsyncLocalStorage();
|
|
6513
|
+
let activeHandle = null;
|
|
6514
|
+
var InternalTelemetry = class {
|
|
6515
|
+
enabled;
|
|
6516
|
+
outbox;
|
|
6517
|
+
drain;
|
|
6518
|
+
options;
|
|
6519
|
+
constructor(options, enabled) {
|
|
6520
|
+
this.options = options;
|
|
6521
|
+
this.enabled = enabled;
|
|
6522
|
+
this.outbox = new TelemetryOutbox({ toolName: options.name });
|
|
6523
|
+
const endpoint = resolveEndpoint(options.endpoint);
|
|
6524
|
+
const drains = [createDebugDrain(), endpoint ? createHttpDrain(endpoint) : createNoopDrain()];
|
|
6525
|
+
this.drain = composeDrains(...drains);
|
|
6526
|
+
}
|
|
6527
|
+
set(fields) {
|
|
6528
|
+
const ctx = runStorage.getStore();
|
|
6529
|
+
if (!ctx || !this.enabled) return;
|
|
6530
|
+
ctx.custom = sanitizeCustom({
|
|
6531
|
+
...ctx.custom,
|
|
6532
|
+
...fields
|
|
6533
|
+
});
|
|
6534
|
+
}
|
|
6535
|
+
async finish(outcome) {
|
|
6536
|
+
if (!this.enabled || !resolveConsent(this.options.name)) return;
|
|
6537
|
+
const ctx = runStorage.getStore();
|
|
6538
|
+
if (!ctx) return;
|
|
6539
|
+
const event = {
|
|
6540
|
+
tool: this.options.name,
|
|
6541
|
+
version: this.options.version,
|
|
6542
|
+
command: ctx.command,
|
|
6543
|
+
outcome,
|
|
6544
|
+
durationMs: Date.now() - ctx.startedAt,
|
|
6545
|
+
flags: ctx.flags,
|
|
6546
|
+
custom: Object.keys(ctx.custom).length > 0 ? ctx.custom : void 0,
|
|
6547
|
+
ci: Boolean(process.env.CI),
|
|
6548
|
+
machineId: hashMachineId()
|
|
6549
|
+
};
|
|
6550
|
+
try {
|
|
6551
|
+
await this.outbox.append(event);
|
|
6552
|
+
const pending = await this.outbox.readAll();
|
|
6553
|
+
await this.drain(pending);
|
|
6554
|
+
await this.outbox.purge();
|
|
6555
|
+
} catch {}
|
|
6556
|
+
}
|
|
6557
|
+
async flush() {
|
|
6558
|
+
if (!this.enabled || !resolveConsent(this.options.name)) return;
|
|
6559
|
+
await Promise.race([(async () => {
|
|
6560
|
+
const pending = await this.outbox.readAll();
|
|
6561
|
+
await this.drain(pending);
|
|
6562
|
+
await this.outbox.purge();
|
|
6563
|
+
})(), new Promise((resolve) => setTimeout(resolve, 500))]);
|
|
6564
|
+
}
|
|
6565
|
+
};
|
|
6566
|
+
function hashMachineId() {
|
|
6567
|
+
const seed = `${process.platform}:${process.arch}:${process.env.USER ?? "unknown"}`;
|
|
6568
|
+
return createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
6569
|
+
}
|
|
6570
|
+
function createTelemetry(options) {
|
|
6571
|
+
activeHandle = new InternalTelemetry(options, resolveConsent(options.name));
|
|
6572
|
+
return activeHandle;
|
|
6573
|
+
}
|
|
6574
|
+
async function runWithTelemetry(options, command, argv, fn) {
|
|
6575
|
+
const handle = createTelemetry(options);
|
|
6576
|
+
const ctx = {
|
|
6577
|
+
command,
|
|
6578
|
+
startedAt: Date.now(),
|
|
6579
|
+
custom: {},
|
|
6580
|
+
flags: sanitizeFlags(argv, options.allowlistedStringFlags)
|
|
6581
|
+
};
|
|
6582
|
+
return runStorage.run(ctx, async () => {
|
|
6583
|
+
let outcome = "success";
|
|
6584
|
+
try {
|
|
6585
|
+
return await fn();
|
|
6586
|
+
} catch (error) {
|
|
6587
|
+
outcome = "failure";
|
|
6588
|
+
throw error;
|
|
6589
|
+
} finally {
|
|
6590
|
+
if (handle.enabled) await handle.finish(outcome);
|
|
6591
|
+
}
|
|
6592
|
+
});
|
|
6593
|
+
}
|
|
6594
|
+
function withCommanderTelemetry(program, options) {
|
|
6595
|
+
const originalParse = program.parseAsync.bind(program);
|
|
6596
|
+
program.parseAsync = async (argv) => {
|
|
6597
|
+
const command = argv[2] ?? "help";
|
|
6598
|
+
return runWithTelemetry({
|
|
6599
|
+
name: options?.name ?? program.name(),
|
|
6600
|
+
version: options?.version ?? program.version(),
|
|
6601
|
+
...options
|
|
6602
|
+
}, command, argv.slice(3), () => originalParse(argv));
|
|
6603
|
+
};
|
|
6604
|
+
return program;
|
|
6605
|
+
}
|
|
6606
|
+
function getTelemetryStatus(toolName) {
|
|
6607
|
+
return {
|
|
6608
|
+
enabled: resolveConsent(toolName),
|
|
6609
|
+
preference: readPreferenceSync(toolName)
|
|
6610
|
+
};
|
|
6611
|
+
}
|
|
6612
|
+
async function enableTelemetry(toolName) {
|
|
6613
|
+
await writePreference(toolName, "enabled");
|
|
6614
|
+
}
|
|
6615
|
+
async function disableTelemetry(toolName) {
|
|
6616
|
+
await writePreference(toolName, "disabled");
|
|
6617
|
+
await purgeOutbox(toolName);
|
|
6618
|
+
}
|
|
6619
|
+
function generateDisclosure(options) {
|
|
6620
|
+
const json = {
|
|
6621
|
+
tool: options.name,
|
|
6622
|
+
version: options.version,
|
|
6623
|
+
collected: [
|
|
6624
|
+
"command",
|
|
6625
|
+
"outcome",
|
|
6626
|
+
"durationMs",
|
|
6627
|
+
"sanitized flags",
|
|
6628
|
+
"optional custom counters"
|
|
6629
|
+
],
|
|
6630
|
+
notCollected: [
|
|
6631
|
+
"raw argv strings",
|
|
6632
|
+
"paths",
|
|
6633
|
+
"tokens",
|
|
6634
|
+
"secrets"
|
|
6635
|
+
],
|
|
6636
|
+
optOut: [
|
|
6637
|
+
"DO_NOT_TRACK=1",
|
|
6638
|
+
"AUTOTEL_TELEMETRY=0",
|
|
6639
|
+
"autotel telemetry disable"
|
|
6640
|
+
]
|
|
6641
|
+
};
|
|
6642
|
+
return {
|
|
6643
|
+
markdown: `# ${options.name} telemetry disclosure
|
|
6644
|
+
|
|
6645
|
+
Collects one structured event per command: command name, outcome, duration, sanitized flags, and optional counters via \`telemetry.set()\`.
|
|
6646
|
+
|
|
6647
|
+
Does not collect raw paths, tokens, or secret-shaped flag values.
|
|
6648
|
+
|
|
6649
|
+
Opt out with \`DO_NOT_TRACK=1\`, \`AUTOTEL_TELEMETRY=0\`, or \`autotel telemetry disable\`.
|
|
6650
|
+
`,
|
|
6651
|
+
json
|
|
6652
|
+
};
|
|
6653
|
+
}
|
|
6654
|
+
//#endregion
|
|
6655
|
+
//#region src/commands/telemetry.ts
|
|
6656
|
+
async function runTelemetryStatus(toolName) {
|
|
6657
|
+
const status = getTelemetryStatus(toolName);
|
|
6658
|
+
console.log(JSON.stringify(status, null, 2));
|
|
6659
|
+
}
|
|
6660
|
+
async function runTelemetryEnable(toolName) {
|
|
6661
|
+
await enableTelemetry(toolName);
|
|
6662
|
+
console.log(`Telemetry enabled for ${toolName}`);
|
|
6663
|
+
}
|
|
6664
|
+
async function runTelemetryDisable(toolName) {
|
|
6665
|
+
await disableTelemetry(toolName);
|
|
6666
|
+
console.log(`Telemetry disabled for ${toolName}`);
|
|
6667
|
+
}
|
|
6668
|
+
function runTelemetryDisclosure(toolName, version) {
|
|
6669
|
+
const disclosure = generateDisclosure({
|
|
6670
|
+
name: toolName,
|
|
6671
|
+
version
|
|
6672
|
+
});
|
|
6673
|
+
console.log(disclosure.markdown);
|
|
6674
|
+
}
|
|
6675
|
+
//#endregion
|
|
6190
6676
|
//#region src/cli.ts
|
|
6191
6677
|
/**
|
|
6192
6678
|
* Create the CLI program
|
|
@@ -6325,6 +6811,20 @@ function createProgram() {
|
|
|
6325
6811
|
registerScoreCommands(program);
|
|
6326
6812
|
registerCollectorCommands(program);
|
|
6327
6813
|
registerSecurityCommands(program);
|
|
6814
|
+
const telemetryCmd = new Command("telemetry").description("Manage opt-in CLI usage telemetry");
|
|
6815
|
+
telemetryCmd.command("status").description("Show telemetry consent status").action(async () => {
|
|
6816
|
+
await runTelemetryStatus("autotel");
|
|
6817
|
+
});
|
|
6818
|
+
telemetryCmd.command("enable").description("Enable telemetry for this tool").action(async () => {
|
|
6819
|
+
await runTelemetryEnable("autotel");
|
|
6820
|
+
});
|
|
6821
|
+
telemetryCmd.command("disable").description("Disable telemetry and purge undelivered events").action(async () => {
|
|
6822
|
+
await runTelemetryDisable("autotel");
|
|
6823
|
+
});
|
|
6824
|
+
telemetryCmd.command("disclosure").description("Print telemetry disclosure markdown").action(() => {
|
|
6825
|
+
runTelemetryDisclosure("autotel", program.version());
|
|
6826
|
+
});
|
|
6827
|
+
program.addCommand(telemetryCmd);
|
|
6328
6828
|
return program;
|
|
6329
6829
|
}
|
|
6330
6830
|
/**
|
|
@@ -6338,6 +6838,14 @@ function createProgram() {
|
|
|
6338
6838
|
*/
|
|
6339
6839
|
async function run() {
|
|
6340
6840
|
const program = createProgram();
|
|
6841
|
+
const parseWithTelemetry = withCommanderTelemetry({
|
|
6842
|
+
name: () => program.name(),
|
|
6843
|
+
version: () => program.version() ?? "",
|
|
6844
|
+
parseAsync: (argv) => program.parseAsync(argv)
|
|
6845
|
+
}, {
|
|
6846
|
+
name: "autotel",
|
|
6847
|
+
version: program.version()
|
|
6848
|
+
});
|
|
6341
6849
|
program.exitOverride();
|
|
6342
6850
|
const argvJoined = process.argv.slice(2).join(" ");
|
|
6343
6851
|
const isJsonOnly = process.argv.includes("--json") || /^(schema|commands|examples|version|health|capabilities|discover|query|trace|diagnose|topology|correlate|llm|semconv|score|collector)\b/.test(argvJoined);
|
|
@@ -6349,7 +6857,7 @@ async function run() {
|
|
|
6349
6857
|
if (isJsonOnly) cmd.configureOutput({ writeErr: () => {} });
|
|
6350
6858
|
stack.push(...cmd.commands);
|
|
6351
6859
|
}
|
|
6352
|
-
await
|
|
6860
|
+
await parseWithTelemetry.parseAsync(process.argv);
|
|
6353
6861
|
}
|
|
6354
6862
|
//#endregion
|
|
6355
6863
|
//#region src/lib/commander-error.ts
|