autotel-cli 0.20.0 → 0.21.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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/dist/index.js +268 -132
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -201,6 +201,9 @@ npx autotel-cli map --min-score 70
201
201
 
202
202
  # Fail CI when a check that used to pass now fails
203
203
  npx autotel-cli map --baseline git:origin/main
204
+
205
+ # Annotate the pull request diff in GitHub Actions
206
+ npx autotel-cli map --format github --baseline git:origin/main --no-write
204
207
  ```
205
208
 
206
209
  **Detected frameworks:** `next`, `nitro` (Nuxt), `tanstack-start`, `sveltekit`, `hono`, `express`, `fastify`, `elysia`, `cloudflare`. Override with `--framework <name>`.
@@ -242,7 +245,17 @@ costs no score and is never counted as coverage;
242
245
  - `--min-score <n>` - Exit 1 when the global score is below this threshold
243
246
  - `--baseline [source]` - Compare against a committed map (path, or `git:<ref>`); exit 1 on regression
244
247
  - `--no-write` - Skip writing `autotel.map.json`
245
- - `--json` / `--output-file <path>` - Machine-readable output
248
+ - `--json` / `--output-file <path>` - Machine-readable output (`--json` is the same as `--format json`)
249
+ - `--format github` - GitHub Actions [workflow annotations](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions) on stdout; the report goes to stderr and the exit code is unchanged
250
+ - `--limit <n>` - Most annotations `--format github` emits (default 10, how many GitHub keeps per step)
251
+
252
+ **Annotating pull requests:** with `--baseline`, `--format github` emits the regressions as errors: the checks this pull request broke, nothing already on the map. Without one, it emits the "Fix these first" list, worst entry point first, as warnings. The first line carries the score, as an error when `--min-score` or the baseline failed the run, and says how many findings `--limit` left out. Paths are rebased on `GITHUB_WORKSPACE`, so a package scanned with `--cwd` inside a monorepo still annotates the right file.
253
+
254
+ ```
255
+ ::warning file=src/index.ts,line=6,title=autotel map%3A trace::no trace() or span() — this handler is invisible when it breaks
256
+ ::warning file=src/index.ts,line=6,title=autotel map%3A audit::no audit trail (money: path says "payment") — withAudit() survives tail sampling
257
+ ::notice title=autotel map::score 15/100 (at-risk): 0 instrumented, 0 partial, 3 dark; 6 more findings not shown
258
+ ```
246
259
 
247
260
  ### `autotel codemod trace <path>`
248
261
 
package/dist/index.js CHANGED
@@ -5659,129 +5659,6 @@ function hasRegressed(comparison) {
5659
5659
  return comparison.regressions.length > 0 || comparison.delta < 0;
5660
5660
  }
5661
5661
  //#endregion
5662
- //#region src/lib/map/project-facts.ts
5663
- const LLM_PACKAGES = [
5664
- "openai",
5665
- "@anthropic-ai/sdk",
5666
- "ai",
5667
- "@ai-sdk/openai",
5668
- "@ai-sdk/anthropic",
5669
- "@ai-sdk/google",
5670
- "ai-sdk-ollama",
5671
- "ollama",
5672
- "@google/generative-ai",
5673
- "@mistralai/mistralai",
5674
- "cohere-ai",
5675
- "langchain",
5676
- "@langchain/core"
5677
- ];
5678
- const RUNTIME_AUTOTEL_PACKAGES = [
5679
- "autotel",
5680
- "autotel-edge",
5681
- "autotel-cloudflare",
5682
- "autotel-nuxt",
5683
- "autotel-tanstack",
5684
- "autotel-hono",
5685
- "autotel-adapters"
5686
- ];
5687
- /**
5688
- * Files where a framework integration is wired up.
5689
- *
5690
- * Kept to a fixed list rather than a glob: this answers "did anyone install the
5691
- * middleware", and the answer is always in the app's entry file or its hooks.
5692
- */
5693
- const WIRING_FILES = [
5694
- "src/index.ts",
5695
- "src/app.ts",
5696
- "src/server.ts",
5697
- "src/main.ts",
5698
- "index.ts",
5699
- "app.ts",
5700
- "server.ts",
5701
- "middleware.ts",
5702
- "src/middleware.ts",
5703
- "src/hooks.server.ts",
5704
- "hooks.server.ts",
5705
- "nuxt.config.ts",
5706
- "src/router.tsx",
5707
- "app/router.tsx"
5708
- ];
5709
- /** Autotel framework integrations that create a request span when called. */
5710
- const AMBIENT_APIS = ["autotelMiddleware", "autotelHandle"];
5711
- function hasAmbientTracing(projectRoot, instrumentationPath) {
5712
- const parse = createParser();
5713
- const candidates = new Set([instrumentationPath, ...WIRING_FILES.map((file) => path$1.join(projectRoot, file))].filter((file) => file !== null && fileExists(file)));
5714
- for (const file of candidates) {
5715
- const source = parse(file);
5716
- if (source && usesAutotelApi$1(buildFileFacts(source), AMBIENT_APIS)) return true;
5717
- }
5718
- return false;
5719
- }
5720
- function projectSource(projectRoot) {
5721
- const files = globSync("{src,app,server,routes,pages}/**/*.{ts,tsx,js,jsx,mts,mjs}", {
5722
- cwd: projectRoot,
5723
- absolute: true,
5724
- ignore: [
5725
- "**/node_modules/**",
5726
- "**/dist/**",
5727
- "**/*.test.*",
5728
- "**/*.spec.*"
5729
- ]
5730
- });
5731
- const occurrences = /* @__PURE__ */ new Map();
5732
- const chunks = [];
5733
- const call = /createStructuredError\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
5734
- for (const file of files) {
5735
- const source = readFileSafe(file) ?? "";
5736
- chunks.push(source);
5737
- for (const match of source.matchAll(call)) {
5738
- const body = match[1] ?? "";
5739
- const message = /\bmessage\s*:\s*(['"`])([\s\S]*?)\1/.exec(body)?.[2];
5740
- const status = /\bstatus\s*:\s*(\d+)/.exec(body)?.[1];
5741
- if (!message || !status) continue;
5742
- const signature = `${status}|${message}`;
5743
- const entry = occurrences.get(signature) ?? {
5744
- label: message,
5745
- files: /* @__PURE__ */ new Set()
5746
- };
5747
- entry.files.add(path$1.relative(projectRoot, file).split(path$1.sep).join("/"));
5748
- occurrences.set(signature, entry);
5749
- }
5750
- }
5751
- return {
5752
- text: chunks.join("\n"),
5753
- repeatedErrors: new Map([...occurrences].filter(([, entry]) => entry.files.size > 1).map(([signature, entry]) => [signature, {
5754
- label: entry.label,
5755
- files: [...entry.files].toSorted()
5756
- }]))
5757
- };
5758
- }
5759
- function collectDeps(pkg) {
5760
- return /* @__PURE__ */ new Set([...Object.keys(pkg?.dependencies ?? {}), ...Object.keys(pkg?.devDependencies ?? {})]);
5761
- }
5762
- function collectProjectFacts(projectRoot) {
5763
- const pkg = readJsonSafe(path$1.join(projectRoot, "package.json"));
5764
- const deps = collectDeps(pkg);
5765
- const instrumentation = findInstrumentationFile(projectRoot);
5766
- const instrumentationText = instrumentation ? readFileSafe(instrumentation.path) ?? "" : "";
5767
- const ambientText = `${instrumentationText}\n${WIRING_FILES.map((candidate) => fileExists(path$1.join(projectRoot, candidate)) ? readFileSafe(path$1.join(projectRoot, candidate)) ?? "" : "").join("\n")}`;
5768
- const source = projectSource(projectRoot);
5769
- const projectSignalText = `${ambientText}\n${source.text}`;
5770
- return {
5771
- name: pkg?.name ?? path$1.basename(projectRoot),
5772
- deps,
5773
- hasAutotel: RUNTIME_AUTOTEL_PACKAGES.some((name) => deps.has(name)),
5774
- ambientTracing: deps.has("autotel-nuxt") || hasAmbientTracing(projectRoot, instrumentation?.path ?? null),
5775
- hasRedaction: instrumentationText.includes("attributeRedactor"),
5776
- instrumentationFile: instrumentation ? path$1.relative(projectRoot, instrumentation.path).split(path$1.sep).join("/") : null,
5777
- hasLlmDependency: LLM_PACKAGES.some((name) => deps.has(name)),
5778
- hasZod: deps.has("zod"),
5779
- hasAuditUsage: deps.has("autotel-audit") && /securityEvent|withSecurity|withAudit|defineAuditCatalog/.test(projectSignalText),
5780
- hasErrorCatalog: projectSignalText.includes("defineErrorCatalog"),
5781
- repeatedErrors: source.repeatedErrors
5782
- };
5783
- }
5784
- //#endregion
5785
5662
  //#region src/lib/map/sensitivity.ts
5786
5663
  const MONEY_PACKAGES = [
5787
5664
  "stripe",
@@ -5920,6 +5797,10 @@ function lostPoints(route) {
5920
5797
  const weight = route.sensitivity.level === "high" ? 2 : 1;
5921
5798
  return (100 - route.score) * weight;
5922
5799
  }
5800
+ /** Entry points with gaps, most points lost first: the "Fix these first" order. */
5801
+ function fixFirst(routes) {
5802
+ return routes.filter((route) => classifyRouteObservability(route) !== "exempt" && route.score < 100).toSorted((a, b) => lostPoints(b) - lostPoints(a));
5803
+ }
5923
5804
  /**
5924
5805
  * The default view: the number, the tally, and the three entry points worth
5925
5806
  * fixing first.
@@ -5936,7 +5817,7 @@ function formatReport(result, options = {}) {
5936
5817
  lines.push("");
5937
5818
  lines.push(` ${colour(chalk.bold(`${map.score}/100`))} ${colour(grade)}`);
5938
5819
  lines.push(chalk.dim(` ${map.routes.length} entry point${map.routes.length === 1 ? "" : "s"} · ${summary.instrumented} instrumented · ${summary.partial} partial · ${summary.dark} dark · ${summary.exempt} exempt` + (summary.suppressedChecks > 0 ? ` · ${summary.suppressedChecks} suppressed` : "")));
5939
- const worst = map.routes.filter((route) => classifyRouteObservability(route) !== "exempt" && route.score < 100).toSorted((a, b) => lostPoints(b) - lostPoints(a)).slice(0, 3);
5820
+ const worst = fixFirst(map.routes).slice(0, 3);
5940
5821
  if (worst.length > 0) {
5941
5822
  lines.push("");
5942
5823
  lines.push(chalk.bold("Fix these first"));
@@ -6045,6 +5926,207 @@ function formatGate(score, threshold) {
6045
5926
  return score < threshold ? chalk.red(`\nScore ${score} is below --min-score ${threshold}.`) : chalk.green(`\nScore ${score} meets --min-score ${threshold}.`);
6046
5927
  }
6047
5928
  //#endregion
5929
+ //#region src/lib/map/github.ts
5930
+ function rebase(file, location) {
5931
+ if (!location.workspace) return file;
5932
+ return path$1.relative(location.workspace, path$1.resolve(location.projectRoot, file)).split(path$1.sep).join("/");
5933
+ }
5934
+ /** The failing requirements of one entry point, at the evidence line when a check has one. */
5935
+ function failures(route) {
5936
+ const out = [];
5937
+ for (const [check, result] of Object.entries(route.checks)) {
5938
+ if (result.status !== "fail") continue;
5939
+ out.push({
5940
+ file: result.evidence?.file ?? route.file,
5941
+ line: result.evidence?.line ?? route.handler?.line ?? 1,
5942
+ check,
5943
+ message: result.message ?? getRule(check)?.question ?? check
5944
+ });
5945
+ }
5946
+ return out;
5947
+ }
5948
+ /**
5949
+ * Checks that passed in the baseline and no longer do. These are the pull
5950
+ * request's; everything else on the map predates it and is already in the score.
5951
+ */
5952
+ function regressions(scan, baseline) {
5953
+ const routes = new Map(scan.map.routes.map((route) => [route.id, route]));
5954
+ return baseline.regressions.flatMap((regression) => {
5955
+ const route = routes.get(regression.routeId);
5956
+ if (!route) return [];
5957
+ const failed = failures(route).find((finding) => finding.check === regression.check);
5958
+ if (failed) return [failed];
5959
+ return [{
5960
+ file: route.file,
5961
+ line: route.handler?.line ?? 1,
5962
+ check: regression.check,
5963
+ message: `${regression.check} passed in ${baseline.source.label} and is now suppressed`
5964
+ }];
5965
+ });
5966
+ }
5967
+ function escapeMessage(text) {
5968
+ return text.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
5969
+ }
5970
+ function escapeProperty(text) {
5971
+ return escapeMessage(text).replaceAll(":", "%3A").replaceAll(",", "%2C");
5972
+ }
5973
+ function annotation(level, properties, message) {
5974
+ const props = Object.entries(properties).map(([key, value]) => `${key}=${escapeProperty(value)}`).join(",");
5975
+ return `::${level}${props ? ` ${props}` : ""}::${escapeMessage(message)}`;
5976
+ }
5977
+ /**
5978
+ * GitHub Actions workflow commands, one line per finding, for stdout.
5979
+ *
5980
+ * With a baseline, the findings are the regressions and they are errors: the
5981
+ * pull request caused them. Without one, they are the "Fix these first" list,
5982
+ * worst entry point first, as warnings. Either list stops at `limit`. The
5983
+ * first line carries the score, as an error when the gate failed, so the run
5984
+ * is readable without the log. It leads because GitHub keeps only the first
5985
+ * ten errors of a step.
5986
+ */
5987
+ function formatGithubAnnotations(scan, baseline, location, options) {
5988
+ const findings = baseline ? regressions(scan, baseline) : fixFirst(scan.map.routes).flatMap(failures);
5989
+ const level = baseline ? "error" : "warning";
5990
+ const lines = findings.slice(0, options.limit).map((finding) => annotation(level, {
5991
+ file: rebase(finding.file, location),
5992
+ line: String(finding.line),
5993
+ title: `autotel map: ${finding.check}`
5994
+ }, finding.message));
5995
+ const { score } = scan.map;
5996
+ const { dark, instrumented, partial } = scan.summary;
5997
+ const hidden = findings.length - lines.length;
5998
+ const summary = `score ${score}/100 (${scan.grade}): ${instrumented} instrumented, ${partial} partial, ${dark} dark` + (hidden > 0 ? `; ${hidden} more finding${hidden === 1 ? "" : "s"} not shown` : "");
5999
+ const title = { title: "autotel map" };
6000
+ let headline;
6001
+ if (options.minScore !== void 0 && score < options.minScore) headline = annotation("error", title, `${summary}; below --min-score ${options.minScore}`);
6002
+ else if (baseline && hasRegressed(baseline)) headline = annotation("error", title, `${summary}; regressed against ${baseline.source.label}`);
6003
+ else headline = annotation("notice", title, summary);
6004
+ return [headline, ...lines].join("\n");
6005
+ }
6006
+ //#endregion
6007
+ //#region src/lib/map/project-facts.ts
6008
+ const LLM_PACKAGES = [
6009
+ "openai",
6010
+ "@anthropic-ai/sdk",
6011
+ "ai",
6012
+ "@ai-sdk/openai",
6013
+ "@ai-sdk/anthropic",
6014
+ "@ai-sdk/google",
6015
+ "ai-sdk-ollama",
6016
+ "ollama",
6017
+ "@google/generative-ai",
6018
+ "@mistralai/mistralai",
6019
+ "cohere-ai",
6020
+ "langchain",
6021
+ "@langchain/core"
6022
+ ];
6023
+ const RUNTIME_AUTOTEL_PACKAGES = [
6024
+ "autotel",
6025
+ "autotel-edge",
6026
+ "autotel-cloudflare",
6027
+ "autotel-nuxt",
6028
+ "autotel-tanstack",
6029
+ "autotel-hono",
6030
+ "autotel-adapters"
6031
+ ];
6032
+ /**
6033
+ * Files where a framework integration is wired up.
6034
+ *
6035
+ * Kept to a fixed list rather than a glob: this answers "did anyone install the
6036
+ * middleware", and the answer is always in the app's entry file or its hooks.
6037
+ */
6038
+ const WIRING_FILES = [
6039
+ "src/index.ts",
6040
+ "src/app.ts",
6041
+ "src/server.ts",
6042
+ "src/main.ts",
6043
+ "index.ts",
6044
+ "app.ts",
6045
+ "server.ts",
6046
+ "middleware.ts",
6047
+ "src/middleware.ts",
6048
+ "src/hooks.server.ts",
6049
+ "hooks.server.ts",
6050
+ "nuxt.config.ts",
6051
+ "src/router.tsx",
6052
+ "app/router.tsx"
6053
+ ];
6054
+ /** Autotel framework integrations that create a request span when called. */
6055
+ const AMBIENT_APIS = ["autotelMiddleware", "autotelHandle"];
6056
+ function hasAmbientTracing(projectRoot, instrumentationPath) {
6057
+ const parse = createParser();
6058
+ const candidates = new Set([instrumentationPath, ...WIRING_FILES.map((file) => path$1.join(projectRoot, file))].filter((file) => file !== null && fileExists(file)));
6059
+ for (const file of candidates) {
6060
+ const source = parse(file);
6061
+ if (source && usesAutotelApi$1(buildFileFacts(source), AMBIENT_APIS)) return true;
6062
+ }
6063
+ return false;
6064
+ }
6065
+ function projectSource(projectRoot) {
6066
+ const files = globSync("{src,app,server,routes,pages}/**/*.{ts,tsx,js,jsx,mts,mjs}", {
6067
+ cwd: projectRoot,
6068
+ absolute: true,
6069
+ ignore: [
6070
+ "**/node_modules/**",
6071
+ "**/dist/**",
6072
+ "**/*.test.*",
6073
+ "**/*.spec.*"
6074
+ ]
6075
+ });
6076
+ const occurrences = /* @__PURE__ */ new Map();
6077
+ const chunks = [];
6078
+ const call = /createStructuredError\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
6079
+ for (const file of files) {
6080
+ const source = readFileSafe(file) ?? "";
6081
+ chunks.push(source);
6082
+ for (const match of source.matchAll(call)) {
6083
+ const body = match[1] ?? "";
6084
+ const message = /\bmessage\s*:\s*(['"`])([\s\S]*?)\1/.exec(body)?.[2];
6085
+ const status = /\bstatus\s*:\s*(\d+)/.exec(body)?.[1];
6086
+ if (!message || !status) continue;
6087
+ const signature = `${status}|${message}`;
6088
+ const entry = occurrences.get(signature) ?? {
6089
+ label: message,
6090
+ files: /* @__PURE__ */ new Set()
6091
+ };
6092
+ entry.files.add(path$1.relative(projectRoot, file).split(path$1.sep).join("/"));
6093
+ occurrences.set(signature, entry);
6094
+ }
6095
+ }
6096
+ return {
6097
+ text: chunks.join("\n"),
6098
+ repeatedErrors: new Map([...occurrences].filter(([, entry]) => entry.files.size > 1).map(([signature, entry]) => [signature, {
6099
+ label: entry.label,
6100
+ files: [...entry.files].toSorted()
6101
+ }]))
6102
+ };
6103
+ }
6104
+ function collectDeps(pkg) {
6105
+ return /* @__PURE__ */ new Set([...Object.keys(pkg?.dependencies ?? {}), ...Object.keys(pkg?.devDependencies ?? {})]);
6106
+ }
6107
+ function collectProjectFacts(projectRoot) {
6108
+ const pkg = readJsonSafe(path$1.join(projectRoot, "package.json"));
6109
+ const deps = collectDeps(pkg);
6110
+ const instrumentation = findInstrumentationFile(projectRoot);
6111
+ const instrumentationText = instrumentation ? readFileSafe(instrumentation.path) ?? "" : "";
6112
+ const ambientText = `${instrumentationText}\n${WIRING_FILES.map((candidate) => fileExists(path$1.join(projectRoot, candidate)) ? readFileSafe(path$1.join(projectRoot, candidate)) ?? "" : "").join("\n")}`;
6113
+ const source = projectSource(projectRoot);
6114
+ const projectSignalText = `${ambientText}\n${source.text}`;
6115
+ return {
6116
+ name: pkg?.name ?? path$1.basename(projectRoot),
6117
+ deps,
6118
+ hasAutotel: RUNTIME_AUTOTEL_PACKAGES.some((name) => deps.has(name)),
6119
+ ambientTracing: deps.has("autotel-nuxt") || hasAmbientTracing(projectRoot, instrumentation?.path ?? null),
6120
+ hasRedaction: instrumentationText.includes("attributeRedactor"),
6121
+ instrumentationFile: instrumentation ? path$1.relative(projectRoot, instrumentation.path).split(path$1.sep).join("/") : null,
6122
+ hasLlmDependency: LLM_PACKAGES.some((name) => deps.has(name)),
6123
+ hasZod: deps.has("zod"),
6124
+ hasAuditUsage: deps.has("autotel-audit") && /securityEvent|withSecurity|withAudit|defineAuditCatalog/.test(projectSignalText),
6125
+ hasErrorCatalog: projectSignalText.includes("defineErrorCatalog"),
6126
+ repeatedErrors: source.repeatedErrors
6127
+ };
6128
+ }
6129
+ //#endregion
6048
6130
  //#region src/lib/map/scan.ts
6049
6131
  /** Stable identity for an entry point, so a baseline can match it across runs. */
6050
6132
  function routeId(route) {
@@ -6170,6 +6252,46 @@ function parseMinScore(value) {
6170
6252
  });
6171
6253
  return threshold;
6172
6254
  }
6255
+ const FORMATS = [
6256
+ "human",
6257
+ "json",
6258
+ "github"
6259
+ ];
6260
+ /**
6261
+ * Read `--format`, with `--json` as the long-standing spelling of `--format
6262
+ * json`. Two formats cannot share stdout, so asking for both is refused rather
6263
+ * than letting one silently win.
6264
+ */
6265
+ function parseFormat(value, json) {
6266
+ if (value === void 0 || value === "") return json ? "json" : "human";
6267
+ const format = FORMATS.find((name) => name === value);
6268
+ if (!format) throw new AutotelError({
6269
+ type: "validation",
6270
+ code: AutotelErrorCodes.E_INVALID_FLAG,
6271
+ message: `Unknown --format "${value}"`,
6272
+ fix: `Pass one of: ${FORMATS.join(", ")}`,
6273
+ expected: { formats: [...FORMATS] }
6274
+ });
6275
+ if (json && format !== "json") throw new AutotelError({
6276
+ type: "validation",
6277
+ code: AutotelErrorCodes.E_INVALID_FLAG,
6278
+ message: `--json and --format ${format} both claim stdout`,
6279
+ fix: "Drop --json, or pass --format json"
6280
+ });
6281
+ return format;
6282
+ }
6283
+ /** Read `--limit`; the whole string has to parse, for the same reason as `--min-score`. */
6284
+ function parseLimit(value) {
6285
+ if (value === void 0 || value === "") return 10;
6286
+ const limit = Number(value);
6287
+ if (!Number.isInteger(limit) || limit < 1) throw new AutotelError({
6288
+ type: "validation",
6289
+ code: AutotelErrorCodes.E_INVALID_FLAG,
6290
+ message: `--limit expects a whole number of 1 or more, got "${value}"`,
6291
+ fix: "Pass e.g. --limit 10"
6292
+ });
6293
+ return limit;
6294
+ }
6173
6295
  function resolveFramework(requested, projectRoot, deps) {
6174
6296
  if (requested !== void 0 && requested !== "") {
6175
6297
  if (!isFramework(requested)) throw new AutotelError({
@@ -6256,9 +6378,25 @@ function runMap(options) {
6256
6378
  ...options.outputFile !== void 0 ? { outputFile: options.outputFile } : {},
6257
6379
  outputRoot: options.cwd
6258
6380
  });
6381
+ const format = parseFormat(options.format, options.json);
6382
+ const limit = parseLimit(options.limit);
6259
6383
  const minScore = parseMinScore(options.minScore);
6260
6384
  const result = runMapScan(options);
6261
- if (options.json) printJson({
6385
+ const human = () => formatMapResult(result, {
6386
+ all: options.all,
6387
+ ...options.entry !== void 0 ? { entry: options.entry } : {},
6388
+ ...minScore !== void 0 ? { minScore } : {}
6389
+ });
6390
+ if (format === "github") {
6391
+ process.stdout.write(`${formatGithubAnnotations(result.scan, result.baseline, {
6392
+ projectRoot: result.projectRoot,
6393
+ ...process.env.GITHUB_WORKSPACE ? { workspace: process.env.GITHUB_WORKSPACE } : {}
6394
+ }, {
6395
+ limit,
6396
+ ...minScore !== void 0 ? { minScore } : {}
6397
+ })}\n`);
6398
+ process.stderr.write(`${human()}\n`);
6399
+ } else if (format === "json") printJson({
6262
6400
  ok: true,
6263
6401
  command: "map",
6264
6402
  mapFile: MAP_FILE_NAME,
@@ -6271,11 +6409,7 @@ function runMap(options) {
6271
6409
  ...result.baseline ? { baseline: result.baseline } : {},
6272
6410
  map: result.scan.map
6273
6411
  });
6274
- else process.stdout.write(`${formatMapResult(result, {
6275
- all: options.all,
6276
- ...options.entry !== void 0 ? { entry: options.entry } : {},
6277
- ...minScore !== void 0 ? { minScore } : {}
6278
- })}\n`);
6412
+ else process.stdout.write(`${human()}\n`);
6279
6413
  if (minScore !== void 0 && result.scan.map.score < minScore) {
6280
6414
  process.exitCode = 1;
6281
6415
  return;
@@ -9619,7 +9753,7 @@ function createProgram() {
9619
9753
  });
9620
9754
  addGlobalOptions(addCmd);
9621
9755
  program.addCommand(addCmd);
9622
- const mapCmd = new Command("map").description("Score the observability of every entry point in your project").argument("[entry]", "Inspect one entry point by route or file path").option("--framework <name>", "Override framework detection (next, nitro, tanstack-start, sveltekit, hono, express, fastify, elysia, cloudflare)").option("--all", "Show every entry point as a check matrix").option("--min-score <n>", "Exit 1 when the global score is below this threshold").option("--baseline [source]", "Compare against a committed autotel.map.json and exit 1 on regression (path, or git:<ref>)").option("--no-write", "Skip writing autotel.map.json").option("--json", "Output machine-readable JSON").option("--output-file <path>", "Persist JSON output to this path").option("--workspace-root", "Scan from the workspace root instead of the package root").action((entry, opts) => {
9756
+ const mapCmd = new Command("map").description("Score the observability of every entry point in your project").argument("[entry]", "Inspect one entry point by route or file path").option("--framework <name>", "Override framework detection (next, nitro, tanstack-start, sveltekit, hono, express, fastify, elysia, cloudflare)").option("--all", "Show every entry point as a check matrix").option("--min-score <n>", "Exit 1 when the global score is below this threshold").option("--baseline [source]", "Compare against a committed autotel.map.json and exit 1 on regression (path, or git:<ref>)").option("--no-write", "Skip writing autotel.map.json").option("--json", "Output machine-readable JSON (same as --format json)").option("--format <name>", "Output: human (default), json, github (workflow annotations on stdout)").option("--limit <n>", "Most annotations --format github emits (default 10, GitHub's per-step cap)").option("--output-file <path>", "Persist JSON output to this path").option("--workspace-root", "Scan from the workspace root instead of the package root").action((entry, opts) => {
9623
9757
  runMap({
9624
9758
  cwd: opts.cwd ?? process.cwd(),
9625
9759
  dryRun: false,
@@ -9635,6 +9769,8 @@ function createProgram() {
9635
9769
  baseline: opts.baseline,
9636
9770
  write: opts.write !== false,
9637
9771
  json: opts.json ?? false,
9772
+ format: opts.format,
9773
+ limit: opts.limit,
9638
9774
  outputFile: opts.outputFile
9639
9775
  });
9640
9776
  });
@@ -9760,7 +9896,7 @@ async function run() {
9760
9896
  });
9761
9897
  program.exitOverride();
9762
9898
  const argvJoined = process.argv.slice(2).join(" ");
9763
- 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);
9899
+ const isJsonOnly = process.argv.includes("--json") || /(^|\s)--format(=|\s+)json\b/.test(argvJoined) || /^(schema|commands|examples|version|health|capabilities|discover|query|trace|diagnose|topology|correlate|llm|semconv|score|collector)\b/.test(argvJoined);
9764
9900
  if (isJsonOnly) program.configureOutput({ writeErr: () => {} });
9765
9901
  const stack = [...program.commands];
9766
9902
  while (stack.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-cli",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "CLI for autotel - setup wizard, diagnostics, and incremental features",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "@inquirer/prompts": "^8.7.2",
22
22
  "@opentelemetry/otlp-transformer": "^0.222.0",
23
- "autotel-mcp": "0.12.0",
23
+ "autotel-mcp": "0.13.0",
24
24
  "chalk": "^6.0.1",
25
25
  "commander": "^15.0.0",
26
26
  "glob": "^13.0.6",