autotel-cli 0.1.0 → 0.4.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Jag Reehal 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -107,6 +107,47 @@ npx autotel add backend datadog --help
107
107
  - `--dry-run` - Skip installation and print what would be done
108
108
  - `--force` - Overwrite non-CLI-owned config (creates backup first)
109
109
 
110
+ ### `autotel codemod trace <path>`
111
+
112
+ Wrap functions in `trace()` with a span name derived from the function/variable/method name. Use this to adopt autotel on existing code without changing function bodies.
113
+
114
+ **Supported file types:** `.ts`, `.tsx`, `.js`, `.jsx`
115
+
116
+ ```bash
117
+ # Single file (TypeScript or JavaScript)
118
+ npx autotel codemod trace src/index.ts
119
+ npx autotel codemod trace src/utils.js
120
+
121
+ # Glob pattern - TypeScript only
122
+ npx autotel codemod trace "src/**/*.ts"
123
+
124
+ # Glob pattern - all supported files
125
+ npx autotel codemod trace "src/**/*.{ts,tsx,js,jsx}"
126
+
127
+ # Dry run - print what would change without writing
128
+ npx autotel codemod trace "src/**/*.ts" --dry-run
129
+
130
+ # Custom span name template: {name}, {file} (basename), {path} (relative)
131
+ npx autotel codemod trace "src/**/*.ts" --name-pattern "{file}.{name}"
132
+
133
+ # Skip functions whose name matches a regex (repeatable)
134
+ npx autotel codemod trace "src/**/*.ts" --skip "^_" --skip "test|mock"
135
+
136
+ # Print per-file summary (wrapped count, skipped)
137
+ npx autotel codemod trace "src/**/*.ts" --print-files
138
+ ```
139
+
140
+ **Options:**
141
+
142
+ - `--dry-run` - Print changes without writing files
143
+ - `--name-pattern <pattern>` - Span name template. Placeholders: `{name}`, `{file}`, `{path}`. Default: `{name}` only.
144
+ - `--skip <regex>...` - Skip functions whose name matches (repeatable; combined as OR).
145
+ - `--print-files` - Print per-file summary (e.g. `✔ path (N wrapped)`, `↷ path (skipped: reason)`).
146
+
147
+ **Supported patterns:** Function declarations, arrow/function expressions in `const`/`let`/`var`, class and static methods (body wrap), object method shorthand, named default export function. Works with both TypeScript and JavaScript files. Span name defaults to function/variable/method name (or `ClassName.methodName` for methods).
148
+
149
+ **Exclusions:** Generator functions (`function*`, `async function*`); getters/setters; constructors; CJS (`require('autotel')`); anonymous default export; class/object methods that use `super` in the body; `.d.ts` files; `node_modules/`. The codemod does not modify files when no eligible functions are found (no unused `trace` import added). The `{path}` placeholder uses the relative path from `--cwd` with forward slashes; moving files will change span names.
150
+
110
151
  ## Global Options
111
152
 
112
153
  All commands support these options:
package/dist/index.js CHANGED
@@ -362,7 +362,7 @@ function addPluginInit(file, init) {
362
362
  function sortImports(imports) {
363
363
  const sideEffect = [];
364
364
  const external = [];
365
- const relative3 = [];
365
+ const relative5 = [];
366
366
  for (const imp of imports) {
367
367
  if (imp.sideEffect) {
368
368
  if (imp.source === "autotel/register") {
@@ -371,15 +371,15 @@ function sortImports(imports) {
371
371
  sideEffect.push(imp);
372
372
  }
373
373
  } else if (imp.source.startsWith(".") || imp.source.startsWith("/")) {
374
- relative3.push(imp);
374
+ relative5.push(imp);
375
375
  } else {
376
376
  external.push(imp);
377
377
  }
378
378
  }
379
379
  const sortBySource = (a, b) => a.source.localeCompare(b.source);
380
380
  external.sort(sortBySource);
381
- relative3.sort(sortBySource);
382
- return [...sideEffect, ...external, ...relative3];
381
+ relative5.sort(sortBySource);
382
+ return [...sideEffect, ...external, ...relative5];
383
383
  }
384
384
  function renderImport(imp) {
385
385
  if (imp.sideEffect) {
@@ -406,9 +406,7 @@ function renderImports(imports) {
406
406
  return sorted.map(renderImport).join("\n");
407
407
  }
408
408
  function renderCodeFile(file) {
409
- const lines = [];
410
- lines.push(CLI_HEADER);
411
- lines.push("");
409
+ const lines = [CLI_HEADER, ""];
412
410
  const mainImports = sortImports(file.imports);
413
411
  if (mainImports.length > 0) {
414
412
  lines.push(renderImports(mainImports));
@@ -2188,6 +2186,97 @@ function checkEsmHook(project) {
2188
2186
  };
2189
2187
  }
2190
2188
 
2189
+ // src/lib/logger-checker.ts
2190
+ import * as path8 from "path";
2191
+ import * as fs2 from "fs";
2192
+ var LOGGER_INSTRUMENTATION = {
2193
+ winston: "@opentelemetry/instrumentation-winston",
2194
+ bunyan: "@opentelemetry/instrumentation-bunyan",
2195
+ pino: "@opentelemetry/instrumentation-pino"
2196
+ };
2197
+ function extractAutoInstrumentations(content) {
2198
+ const instrumentations = [];
2199
+ const arrayPattern = /autoInstrumentations\s*:\s*\[(.*?)\]/s;
2200
+ const arrayMatch = content.match(arrayPattern);
2201
+ if (arrayMatch && arrayMatch[1]) {
2202
+ const items = arrayMatch[1].split(",").map((item) => item.trim().replaceAll(/['"]/g, "")).filter((item) => item.length > 0);
2203
+ instrumentations.push(...items);
2204
+ }
2205
+ const objectPattern = /autoInstrumentations\s*:\s*\{([^}]+)\}/s;
2206
+ const objectMatch = content.match(objectPattern);
2207
+ if (objectMatch && objectMatch[1]) {
2208
+ const props = objectMatch[1];
2209
+ const enabledPattern = /(\w+)\s*:\s*\{[^}]*enabled\s*:\s*true[^}]*\}/g;
2210
+ let enabledMatch;
2211
+ while ((enabledMatch = enabledPattern.exec(props)) !== null) {
2212
+ if (enabledMatch[1]) {
2213
+ instrumentations.push(enabledMatch[1]);
2214
+ }
2215
+ }
2216
+ }
2217
+ return [...new Set(instrumentations)];
2218
+ }
2219
+ function findSourceFiles(packageRoot) {
2220
+ const sourceFiles = [];
2221
+ const srcDir = path8.join(packageRoot, "src");
2222
+ const dirsToCheck = [packageRoot, srcDir].filter(
2223
+ (dir) => fs2.existsSync(dir) && fs2.statSync(dir).isDirectory()
2224
+ );
2225
+ for (const dir of dirsToCheck) {
2226
+ const files = fs2.readdirSync(dir, { recursive: true });
2227
+ for (const file of files) {
2228
+ if (typeof file !== "string") continue;
2229
+ const filePath = path8.join(dir, file);
2230
+ try {
2231
+ if (fs2.statSync(filePath).isFile() && /\.(ts|js|mts|mjs|tsx|jsx)$/.test(file)) {
2232
+ sourceFiles.push(filePath);
2233
+ }
2234
+ } catch {
2235
+ }
2236
+ }
2237
+ }
2238
+ return sourceFiles;
2239
+ }
2240
+ function checkLoggerInstrumentation(packageRoot, deps) {
2241
+ const hasWinston = !!deps["winston"];
2242
+ const hasBunyan = !!deps["bunyan"];
2243
+ const hasPino = !!deps["pino"];
2244
+ let logger = null;
2245
+ if (hasWinston) logger = "winston";
2246
+ else if (hasBunyan) logger = "bunyan";
2247
+ else if (hasPino) logger = "pino";
2248
+ if (!logger) {
2249
+ return {
2250
+ logger: null,
2251
+ hasLogger: false,
2252
+ hasInstrumentation: false,
2253
+ configuredInCode: false,
2254
+ instrumentationPackage: null
2255
+ };
2256
+ }
2257
+ const instrumentationPackage = LOGGER_INSTRUMENTATION[logger];
2258
+ const hasInstrumentation = !!deps[instrumentationPackage];
2259
+ const sourceFiles = findSourceFiles(packageRoot);
2260
+ let configuredInCode = false;
2261
+ for (const filePath of sourceFiles) {
2262
+ const content = readFileSafe(filePath);
2263
+ if (!content) continue;
2264
+ if (!content.includes("init(")) continue;
2265
+ const instrumentations = extractAutoInstrumentations(content);
2266
+ if (instrumentations.includes(logger)) {
2267
+ configuredInCode = true;
2268
+ break;
2269
+ }
2270
+ }
2271
+ return {
2272
+ logger,
2273
+ hasLogger: true,
2274
+ hasInstrumentation,
2275
+ configuredInCode,
2276
+ instrumentationPackage
2277
+ };
2278
+ }
2279
+
2191
2280
  // src/commands/doctor.ts
2192
2281
  var CHECK_DEFINITIONS = [
2193
2282
  { id: "autotel-installed", title: "Autotel installed", description: "Check if autotel is installed" },
@@ -2195,7 +2284,8 @@ var CHECK_DEFINITIONS = [
2195
2284
  { id: "esm-hook", title: "ESM hook setup", description: "Check if autotel/register is imported correctly" },
2196
2285
  { id: "env-vars", title: "Environment variables", description: "Check if required env vars are present" },
2197
2286
  { id: "version-compat", title: "Version compatibility", description: "Check autotel package versions match" },
2198
- { id: "config-found", title: "Configuration found", description: "Check if instrumentation config exists" }
2287
+ { id: "config-found", title: "Configuration found", description: "Check if instrumentation config exists" },
2288
+ { id: "logger-instrumentation", title: "Logger instrumentation", description: "Check if logger instrumentation packages are installed" }
2199
2289
  ];
2200
2290
  async function inferBackend(packageRoot, deps) {
2201
2291
  if (deps["autotel-backends"]) {
@@ -2423,6 +2513,61 @@ async function runChecks(options, projectRoot) {
2423
2513
  message: esmCheck.message,
2424
2514
  details: esmCheck.details
2425
2515
  });
2516
+ const loggerCheck = checkLoggerInstrumentation(project.packageRoot, deps);
2517
+ if (loggerCheck.hasLogger) {
2518
+ if (loggerCheck.configuredInCode && !loggerCheck.hasInstrumentation) {
2519
+ checks.push({
2520
+ id: "logger-instrumentation",
2521
+ title: "Logger instrumentation",
2522
+ level: "warning",
2523
+ status: "warn",
2524
+ message: `${loggerCheck.logger} is configured but instrumentation package is missing`,
2525
+ details: [
2526
+ `${loggerCheck.logger} is used in autoInstrumentations but ${loggerCheck.instrumentationPackage} is not installed`,
2527
+ `Install it: ${getInstallCommand(project.packageManager, [loggerCheck.instrumentationPackage])}`
2528
+ ],
2529
+ fix: {
2530
+ cmd: getInstallCommand(project.packageManager, [loggerCheck.instrumentationPackage]),
2531
+ description: `Install ${loggerCheck.instrumentationPackage}`
2532
+ }
2533
+ });
2534
+ } else if (loggerCheck.hasInstrumentation && loggerCheck.configuredInCode) {
2535
+ checks.push({
2536
+ id: "logger-instrumentation",
2537
+ title: "Logger instrumentation",
2538
+ level: "info",
2539
+ status: "ok",
2540
+ message: `${loggerCheck.logger} instrumentation is properly configured`
2541
+ });
2542
+ } else if (loggerCheck.hasInstrumentation && !loggerCheck.configuredInCode) {
2543
+ checks.push({
2544
+ id: "logger-instrumentation",
2545
+ title: "Logger instrumentation",
2546
+ level: "info",
2547
+ status: "ok",
2548
+ message: `${loggerCheck.logger} instrumentation package is installed`,
2549
+ details: [
2550
+ `Add '${loggerCheck.logger}' to autoInstrumentations in your init() call to enable trace context injection`
2551
+ ]
2552
+ });
2553
+ } else {
2554
+ checks.push({
2555
+ id: "logger-instrumentation",
2556
+ title: "Logger instrumentation",
2557
+ level: "info",
2558
+ status: "skip",
2559
+ message: `${loggerCheck.logger} is installed but not configured in code`
2560
+ });
2561
+ }
2562
+ } else {
2563
+ checks.push({
2564
+ id: "logger-instrumentation",
2565
+ title: "Logger instrumentation",
2566
+ level: "info",
2567
+ status: "skip",
2568
+ message: "No logger packages detected (winston, bunyan, pino)"
2569
+ });
2570
+ }
2426
2571
  return checks;
2427
2572
  }
2428
2573
  function calculateSummary(checks) {
@@ -2806,6 +2951,346 @@ ${presetType} presets:
2806
2951
  }
2807
2952
  }
2808
2953
 
2954
+ // src/commands/codemod-trace.ts
2955
+ import * as path10 from "path";
2956
+ import { glob as glob2 } from "glob";
2957
+
2958
+ // src/lib/codemod-trace.ts
2959
+ import * as path9 from "path";
2960
+ import { Project, SyntaxKind } from "ts-morph";
2961
+ var TRACE_IMPORT_MODULE = "autotel";
2962
+ function hasTraceImport(sourceFile) {
2963
+ for (const imp of sourceFile.getImportDeclarations()) {
2964
+ if (imp.getModuleSpecifierValue() !== TRACE_IMPORT_MODULE) continue;
2965
+ for (const spec of imp.getNamedImports()) {
2966
+ const name = spec.getName();
2967
+ if (name === "trace") return true;
2968
+ const alias = spec.getAliasNode()?.getText();
2969
+ if (alias === "trace" || name === "trace") return true;
2970
+ }
2971
+ }
2972
+ return false;
2973
+ }
2974
+ function addTraceImport(sourceFile) {
2975
+ if (hasTraceImport(sourceFile)) return;
2976
+ sourceFile.insertImportDeclaration(0, {
2977
+ moduleSpecifier: TRACE_IMPORT_MODULE,
2978
+ namedImports: ["trace"]
2979
+ });
2980
+ }
2981
+ function expandNamePattern(pattern, name, filePath, cwd) {
2982
+ const file = path9.basename(filePath, path9.extname(filePath));
2983
+ const relPath = path9.relative(cwd, filePath).replaceAll("\\", "/");
2984
+ return pattern.replaceAll("{name}", name).replaceAll("{file}", file).replaceAll("{path}", relPath);
2985
+ }
2986
+ function getSpanName(name, filePath, options) {
2987
+ if (options.namePattern) {
2988
+ return expandNamePattern(options.namePattern, name, filePath, process.cwd());
2989
+ }
2990
+ return name;
2991
+ }
2992
+ function shouldSkipName(name, options) {
2993
+ if (!options.skip?.length) return false;
2994
+ return options.skip.some((re) => re.test(name));
2995
+ }
2996
+ function isAlreadyWrapped(node) {
2997
+ const text = node.getText();
2998
+ const trimmed = text.trimStart();
2999
+ return trimmed.startsWith("trace(");
3000
+ }
3001
+ function bodyContainsSuper(body) {
3002
+ let found = false;
3003
+ body.forEachDescendant((desc) => {
3004
+ if (desc.getKind() === SyntaxKind.SuperKeyword) found = true;
3005
+ });
3006
+ return found;
3007
+ }
3008
+ function isGenerator(method) {
3009
+ return method.isGenerator?.() ?? false;
3010
+ }
3011
+ function isInsideTraceCall(node) {
3012
+ let current = node.getParent();
3013
+ while (current) {
3014
+ if (current.getKind() === SyntaxKind.CallExpression) {
3015
+ const expr = current.getExpression?.();
3016
+ if (expr?.getText() === "trace") return true;
3017
+ }
3018
+ current = current.getParent();
3019
+ }
3020
+ return false;
3021
+ }
3022
+ function transformFile(content, filePath, options) {
3023
+ const skipped = [];
3024
+ let wrappedCount = 0;
3025
+ const project = new Project({ useInMemoryFileSystem: true });
3026
+ const sourceFile = project.createSourceFile(filePath, content);
3027
+ const edits = [];
3028
+ function skip(name, reason) {
3029
+ skipped.push({ name, reason });
3030
+ return true;
3031
+ }
3032
+ const defaultDecls = sourceFile.getExportedDeclarations().get("default");
3033
+ const defaultFn = defaultDecls?.[0] && (defaultDecls[0].getKind() === SyntaxKind.FunctionDeclaration || defaultDecls[0].getKind() === SyntaxKind.FunctionExpression) ? defaultDecls[0] : void 0;
3034
+ const allFns = sourceFile.getFunctions().filter((f) => !f.isOverload());
3035
+ const onlyDefaultExportFn = defaultFn && allFns.length === 1 && (allFns[0] === defaultFn || allFns[0].getStart() === defaultFn.getStart());
3036
+ for (const fn of allFns) {
3037
+ if (onlyDefaultExportFn) continue;
3038
+ if (isInsideTraceCall(fn)) continue;
3039
+ const name = fn.getName();
3040
+ if (!name) continue;
3041
+ const spanName = getSpanName(name, filePath, options);
3042
+ if (shouldSkipName(spanName, options)) {
3043
+ skip(spanName, "name match");
3044
+ continue;
3045
+ }
3046
+ if (isAlreadyWrapped(fn)) {
3047
+ skip(spanName, "already wrapped");
3048
+ continue;
3049
+ }
3050
+ const mod = fn.getModifiers().map((m) => m.getText()).join(" ");
3051
+ const modPrefix = mod ? mod + " " : "";
3052
+ const fnText = fn.getText();
3053
+ const rest = fnText.replace(/^function\s*\w*\s*/, "");
3054
+ const newText = `${modPrefix}const ${name} = trace('${spanName}', function ${name}${rest};`;
3055
+ edits.push({ node: fn, newText });
3056
+ wrappedCount += 1;
3057
+ }
3058
+ if (defaultFn) {
3059
+ const name = defaultFn.getName?.();
3060
+ if (!name) {
3061
+ skip("(default export)", "anonymous default export");
3062
+ } else {
3063
+ const spanName = getSpanName(name, filePath, options);
3064
+ if (shouldSkipName(spanName, options)) {
3065
+ skip(spanName, "name match");
3066
+ } else {
3067
+ const fn = defaultFn;
3068
+ const params = fn.getParameters?.() ?? [];
3069
+ const paramsText = params.map((p) => p.getText()).join(", ");
3070
+ const body = fn.getBody?.();
3071
+ const bodyText = body?.getText() ?? "{}";
3072
+ const decl = `const ${name} = trace('${spanName}', function ${name}(${paramsText}) ${bodyText};`;
3073
+ const full = decl + "\nexport default " + name + ";";
3074
+ edits.push({ node: defaultFn, newText: full });
3075
+ wrappedCount += 1;
3076
+ }
3077
+ }
3078
+ }
3079
+ for (const stmt of sourceFile.getVariableStatements()) {
3080
+ for (const decl of stmt.getDeclarationList().getDeclarations()) {
3081
+ const init = decl.getInitializer();
3082
+ if (!init) continue;
3083
+ const kind = init.getKind();
3084
+ const isArrow = kind === SyntaxKind.ArrowFunction;
3085
+ const isFnExpr = kind === SyntaxKind.FunctionExpression;
3086
+ const isCall = kind === SyntaxKind.CallExpression;
3087
+ if (isCall) {
3088
+ const callExpr = init;
3089
+ const exprText = callExpr.getExpression().getText();
3090
+ if (exprText === "trace") {
3091
+ const name2 = decl.getName();
3092
+ if (typeof name2 === "string") {
3093
+ const spanName2 = getSpanName(name2, filePath, options);
3094
+ skipped.push({ name: spanName2, reason: "already wrapped" });
3095
+ }
3096
+ continue;
3097
+ }
3098
+ }
3099
+ if (!isArrow && !isFnExpr) continue;
3100
+ const name = decl.getName();
3101
+ if (typeof name !== "string") continue;
3102
+ const spanName = getSpanName(name, filePath, options);
3103
+ if (shouldSkipName(spanName, options)) {
3104
+ skip(spanName, "name match");
3105
+ continue;
3106
+ }
3107
+ if (isAlreadyWrapped(init)) {
3108
+ skipped.push({ name: spanName, reason: "already wrapped" });
3109
+ continue;
3110
+ }
3111
+ const initText = init.getText();
3112
+ const newInit = `trace('${spanName}', ${initText})`;
3113
+ edits.push({ node: init, newText: newInit });
3114
+ wrappedCount += 1;
3115
+ }
3116
+ }
3117
+ for (const clazz of sourceFile.getClasses()) {
3118
+ const className = clazz.getName();
3119
+ if (!className) continue;
3120
+ for (const _ctor of clazz.getConstructors()) {
3121
+ skipped.push({ name: `${className}.constructor`, reason: "constructor" });
3122
+ }
3123
+ for (const method of clazz.getMethods()) {
3124
+ const getter = method.getFirstChildByKind(SyntaxKind.GetKeyword);
3125
+ const setter = method.getFirstChildByKind(SyntaxKind.SetKeyword);
3126
+ if (getter || setter) {
3127
+ skip(method.getName() ?? "(getter/setter)", "getter/setter");
3128
+ continue;
3129
+ }
3130
+ if (isGenerator(method)) {
3131
+ skip(`${className}.${method.getName()}`, "generator");
3132
+ continue;
3133
+ }
3134
+ const methodName = method.getName();
3135
+ const spanName = getSpanName(`${className}.${methodName}`, filePath, options);
3136
+ if (shouldSkipName(spanName, options)) {
3137
+ skip(spanName, "name match");
3138
+ continue;
3139
+ }
3140
+ const body = method.getBody();
3141
+ if (!body) continue;
3142
+ if (bodyContainsSuper(body)) {
3143
+ skip(spanName, "super");
3144
+ continue;
3145
+ }
3146
+ const bodyText = body.getText();
3147
+ const innerBody = bodyText.slice(1, -1).trim();
3148
+ const isAsync = method.isAsync();
3149
+ const prefix = isAsync ? "async " : "";
3150
+ const newBody = `{
3151
+ return trace('${spanName}', ${prefix}() => {
3152
+ ${innerBody}
3153
+ })();
3154
+ }`;
3155
+ edits.push({ node: body, newText: newBody });
3156
+ wrappedCount += 1;
3157
+ }
3158
+ }
3159
+ for (const stmt of sourceFile.getVariableStatements()) {
3160
+ for (const decl of stmt.getDeclarationList().getDeclarations()) {
3161
+ const init = decl.getInitializer();
3162
+ if (!init || init.getKind() !== SyntaxKind.ObjectLiteralExpression) continue;
3163
+ const obj = init;
3164
+ for (const prop of obj.getProperties()) {
3165
+ if (prop.getKind() !== SyntaxKind.MethodDeclaration) continue;
3166
+ const method = prop;
3167
+ const methodName = method.getName();
3168
+ const spanName = getSpanName(methodName, filePath, options);
3169
+ if (shouldSkipName(spanName, options)) {
3170
+ skip(spanName, "name match");
3171
+ continue;
3172
+ }
3173
+ if (method.isGenerator?.()) {
3174
+ skip(spanName, "generator");
3175
+ continue;
3176
+ }
3177
+ const body = method.getBody();
3178
+ if (!body) continue;
3179
+ if (bodyContainsSuper(body)) {
3180
+ skip(spanName, "super");
3181
+ continue;
3182
+ }
3183
+ const bodyText = body.getText();
3184
+ const innerBody = bodyText.slice(1, -1).trim();
3185
+ const newBody = `{
3186
+ return trace('${spanName}', () => {
3187
+ ${innerBody}
3188
+ })();
3189
+ }`;
3190
+ edits.push({ node: body, newText: newBody });
3191
+ wrappedCount += 1;
3192
+ }
3193
+ }
3194
+ }
3195
+ edits.sort((a, b) => b.node.getStart() - a.node.getStart());
3196
+ for (const { node, newText } of edits) {
3197
+ node.replaceWithText(newText);
3198
+ }
3199
+ if (wrappedCount > 0 && !hasTraceImport(sourceFile)) {
3200
+ addTraceImport(sourceFile);
3201
+ }
3202
+ const modified = sourceFile.getFullText();
3203
+ const changed = wrappedCount > 0;
3204
+ return {
3205
+ modified: changed ? modified : content,
3206
+ changed,
3207
+ wrappedCount,
3208
+ skipped
3209
+ };
3210
+ }
3211
+
3212
+ // src/commands/codemod-trace.ts
3213
+ var CODEMOD_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
3214
+ var GLOB_META = /[*?[\]]/;
3215
+ async function resolveCodemodFiles(pathArg, cwd) {
3216
+ const isGlob = GLOB_META.test(pathArg);
3217
+ if (!isGlob) {
3218
+ const absolute = path10.isAbsolute(pathArg) ? pathArg : path10.resolve(cwd, pathArg);
3219
+ if (fileExists(absolute)) {
3220
+ const ext = path10.extname(absolute);
3221
+ if (CODEMOD_EXTENSIONS.has(ext) && !absolute.endsWith(".d.ts")) {
3222
+ return [absolute];
3223
+ }
3224
+ return [];
3225
+ }
3226
+ }
3227
+ const pattern = path10.isAbsolute(pathArg) ? pathArg : path10.join(cwd, pathArg);
3228
+ const matches = await glob2(pattern, {
3229
+ cwd,
3230
+ absolute: true,
3231
+ ignore: ["**/node_modules/**", "**/*.d.ts"]
3232
+ });
3233
+ return matches.filter((f) => {
3234
+ const ext = path10.extname(f);
3235
+ return CODEMOD_EXTENSIONS.has(ext) && !f.endsWith(".d.ts");
3236
+ });
3237
+ }
3238
+ async function runCodemodTrace(options) {
3239
+ const { path: pathArg, cwd, dryRun, namePattern, skip, printFiles, verbose: verbose2, quiet } = options;
3240
+ const files = await resolveCodemodFiles(pathArg, cwd);
3241
+ if (files.length === 0) {
3242
+ if (!quiet) {
3243
+ error(`No matching files found for: ${pathArg}`);
3244
+ }
3245
+ process.exitCode = 1;
3246
+ return;
3247
+ }
3248
+ const skipRegExps = skip?.map((s) => new RegExp(s)) ?? [];
3249
+ const transformOptions = { namePattern, skip: skipRegExps.length > 0 ? skipRegExps : void 0 };
3250
+ let totalWrapped = 0;
3251
+ let totalChanged = 0;
3252
+ for (const filePath of files) {
3253
+ const content = readFileSafe(filePath);
3254
+ if (content === null) {
3255
+ if (verbose2) dim(`Skip ${filePath} (unreadable)`);
3256
+ continue;
3257
+ }
3258
+ let result;
3259
+ try {
3260
+ result = transformFile(content, filePath, transformOptions);
3261
+ } catch (error2) {
3262
+ if (!quiet) {
3263
+ error(`Failed to transform ${filePath}: ${error2 instanceof Error ? error2.message : String(error2)}`);
3264
+ }
3265
+ if (verbose2 && error2 instanceof Error && error2.stack) dim(error2.stack);
3266
+ process.exitCode = 1;
3267
+ continue;
3268
+ }
3269
+ const relativePath = path10.relative(cwd, filePath);
3270
+ if (result.changed) {
3271
+ totalWrapped += result.wrappedCount;
3272
+ totalChanged += 1;
3273
+ if (!dryRun) {
3274
+ const fs3 = await import("fs");
3275
+ fs3.writeFileSync(filePath, result.modified, "utf8");
3276
+ }
3277
+ }
3278
+ const showSummary = printFiles || dryRun || result.changed;
3279
+ if (showSummary && !quiet) {
3280
+ if (result.changed) {
3281
+ console.log(`\u2714 ${relativePath} (${result.wrappedCount} wrapped)`);
3282
+ } else if (result.skipped.length > 0) {
3283
+ const reasons = [...new Set(result.skipped.map((s) => s.reason))].join("; ");
3284
+ console.log(`\u21B7 ${relativePath} (skipped: ${reasons})`);
3285
+ }
3286
+ }
3287
+ }
3288
+ if (dryRun && totalChanged > 0 && !quiet) {
3289
+ console.log("");
3290
+ dim(`Dry run: ${totalChanged} file(s) would be updated, ${totalWrapped} function(s) wrapped.`);
3291
+ }
3292
+ }
3293
+
2809
3294
  // src/cli.ts
2810
3295
  function createProgram() {
2811
3296
  const program = new Command();
@@ -2874,6 +3359,27 @@ function createProgram() {
2874
3359
  });
2875
3360
  addGlobalOptions(addCmd);
2876
3361
  program.addCommand(addCmd);
3362
+ const codemodCmd = new Command("codemod").description("Codemod commands for adopting autotel");
3363
+ const traceCmd = new Command("trace").description("Wrap functions in trace() with span name from function/variable/method name").argument("<path>", "File path or glob (e.g. src/index.ts, src/**/*.ts)").option("--dry-run", "Print changes without writing files").option("--name-pattern <pattern>", "Span name template: {name}, {file}, {path}").option("--skip <regex>...", "Skip functions whose name matches (repeatable)").option("--print-files", "Print per-file summary (wrapped count, skipped)").action(async (pathArg, opts) => {
3364
+ const options = {
3365
+ cwd: opts.cwd ?? process.cwd(),
3366
+ dryRun: opts.dryRun ?? false,
3367
+ noInstall: false,
3368
+ printInstallCmd: false,
3369
+ verbose: opts.verbose ?? false,
3370
+ quiet: opts.quiet ?? false,
3371
+ workspaceRoot: false,
3372
+ path: pathArg,
3373
+ namePattern: opts.namePattern,
3374
+ skip: Array.isArray(opts.skip) && opts.skip.length > 0 ? opts.skip : void 0,
3375
+ printFiles: opts.printFiles ?? false
3376
+ };
3377
+ await runCodemodTrace(options);
3378
+ });
3379
+ addGlobalOptions(traceCmd);
3380
+ codemodCmd.addCommand(traceCmd);
3381
+ addGlobalOptions(codemodCmd);
3382
+ program.addCommand(codemodCmd);
2877
3383
  return program;
2878
3384
  }
2879
3385
  async function run() {