@getargvio/commander 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/dist/index.cjs CHANGED
@@ -30,10 +30,12 @@ function instrumentCommander(program, client, options = {}) {
30
30
  const captureInvocation = options.captureInvocation ?? true;
31
31
  program.exitOverride();
32
32
  let lastCommandPath = program.name();
33
+ let lastFlagNames = [];
33
34
  if (captureInvocation) {
34
35
  program.hook("preAction", (_thisCommand, actionCommand) => {
35
36
  lastCommandPath = commandPath(actionCommand);
36
- client.recordCommandInvocation(lastCommandPath, changedOptionNames(actionCommand));
37
+ lastFlagNames = changedOptionNames(actionCommand);
38
+ client.recordCommandInvocation(lastCommandPath, lastFlagNames);
37
39
  });
38
40
  }
39
41
  if (captureHelp) {
@@ -46,19 +48,27 @@ function instrumentCommander(program, client, options = {}) {
46
48
  const originalParseAsync = program.parseAsync.bind(program);
47
49
  program.parseAsync = (async (...args) => {
48
50
  const start = Date.now();
51
+ client.recordSessionStart();
52
+ const recordCommand = (exitCode) => {
53
+ client.recordCommand({
54
+ path: lastCommandPath,
55
+ flagNames: lastFlagNames,
56
+ startMs: start,
57
+ durationMs: Date.now() - start,
58
+ exitCode
59
+ });
60
+ };
49
61
  try {
50
62
  const result = await originalParseAsync(...args);
51
- client.recordExitCode(lastCommandPath, 0);
52
- client.recordLatency(lastCommandPath, Date.now() - start);
63
+ recordCommand(0);
53
64
  return result;
54
65
  } catch (err) {
55
- client.recordLatency(lastCommandPath, Date.now() - start);
56
- if (isControlFlowExit(err)) {
57
- client.recordExitCode(lastCommandPath, 0);
58
- } else {
59
- client.recordExitCode(lastCommandPath, 1);
66
+ if (!isControlFlowExit(err)) {
67
+ recordCommand(1);
60
68
  const error = err instanceof Error ? err : new Error(String(err));
61
- client.recordError(lastCommandPath, classify(error));
69
+ const classified = classify(error);
70
+ const { category, reason } = typeof classified === "string" ? { category: classified } : classified;
71
+ client.recordError(lastCommandPath, category, reason);
62
72
  }
63
73
  throw err;
64
74
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["CommanderError","ErrorCategory","createClient"],"mappings":";;;;;;AAkBA,SAAS,mBAAmB,GAAA,EAAwB;AAClD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,MAAW,MAAA,IAAU,IAAI,OAAA,EAAS;AAChC,IAAA,MAAM,GAAA,GAAM,OAAO,aAAA,EAAc;AACjC,IAAA,IAAI,GAAA,CAAI,oBAAA,CAAqB,GAAG,CAAA,KAAM,KAAA,EAAO;AAC3C,MAAA,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,IAAA,EAAM,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,IAAK,MAAA,CAAO,KAAA,EAAO,OAAA,CAAQ,IAAA,EAAM,EAAE,KAAK,GAAG,CAAA;AAAA,IACtF;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,YAAY,GAAA,EAAsB;AACzC,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,IAAS,OAAA,GAA0B,GAAA,EAAK,OAAA,EAAS,OAAA,GAAU,QAAQ,MAAA,EAAQ;AACzE,IAAA,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,KAAA,CAAM,KAAK,GAAG,CAAA;AACvB;AAUA,SAAS,kBAAkB,GAAA,EAAqC;AAC9D,EAAA,OAAO,GAAA,YAAeA,wBAAA,IAAkB,GAAA,CAAI,QAAA,KAAa,CAAA;AAC3D;AA0BO,SAAS,mBAAA,CACd,OAAA,EACA,MAAA,EACA,OAAA,GAAgC,EAAC,EACxB;AACT,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,QAAA,KAAa,MAAMC,oBAAA,CAAc,OAAA,CAAA;AAC1D,EAAA,MAAM,WAAA,GAAc,QAAQ,WAAA,IAAe,IAAA;AAC3C,EAAA,MAAM,iBAAA,GAAoB,QAAQ,iBAAA,IAAqB,IAAA;AAEvD,EAAA,OAAA,CAAQ,YAAA,EAAa;AAErB,EAAA,IAAI,eAAA,GAAkB,QAAQ,IAAA,EAAK;AAEnC,EAAA,IAAI,iBAAA,EAAmB;AACrB,IAAA,OAAA,CAAQ,IAAA,CAAK,WAAA,EAAa,CAAC,YAAA,EAAc,aAAA,KAAkB;AACzD,MAAA,eAAA,GAAkB,YAAY,aAAa,CAAA;AAC3C,MAAA,MAAA,CAAO,uBAAA,CAAwB,eAAA,EAAiB,kBAAA,CAAmB,aAAa,CAAC,CAAA;AAAA,IACnF,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,WAAA,EAAa;AACf,IAAA,MAAM,kBAAA,GAAqB,OAAA,CAAQ,UAAA,CAAW,IAAA,CAAK,OAAO,CAAA;AAC1D,IAAA,OAAA,CAAQ,UAAA,IAAc,IAAI,IAAA,KAA4C;AACpE,MAAA,MAAA,CAAO,oBAAoB,eAAe,CAAA;AAC1C,MAAA,OAAO,kBAAA,CAAmB,GAAG,IAAI,CAAA;AAAA,IACnC,CAAA,CAAA;AAAA,EACF;AAEA,EAAA,MAAM,kBAAA,GAAqB,OAAA,CAAQ,UAAA,CAAW,IAAA,CAAK,OAAO,CAAA;AAC1D,EAAA,OAAA,CAAQ,UAAA,IAAc,UAAU,IAAA,KAA4C;AAC1E,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,EAAI;AACvB,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,kBAAA,CAAmB,GAAG,IAAI,CAAA;AAC/C,MAAA,MAAA,CAAO,cAAA,CAAe,iBAAiB,CAAC,CAAA;AACxC,MAAA,MAAA,CAAO,aAAA,CAAc,eAAA,EAAiB,IAAA,CAAK,GAAA,KAAQ,KAAK,CAAA;AACxD,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,GAAA,EAAK;AACZ,MAAA,MAAA,CAAO,aAAA,CAAc,eAAA,EAAiB,IAAA,CAAK,GAAA,KAAQ,KAAK,CAAA;AACxD,MAAA,IAAI,iBAAA,CAAkB,GAAG,CAAA,EAAG;AAC1B,QAAA,MAAA,CAAO,cAAA,CAAe,iBAAiB,CAAC,CAAA;AAAA,MAC1C,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,cAAA,CAAe,iBAAiB,CAAC,CAAA;AACxC,QAAA,MAAM,KAAA,GAAQ,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAChE,QAAA,MAAA,CAAO,WAAA,CAAY,eAAA,EAAiB,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,MACrD;AACA,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF,CAAA,CAAA;AAEA,EAAA,OAAO,OAAA;AACT;AAUO,SAAS,UAAA,CACd,SACA,MAAA,EACA,UAAA,EACA,gBAA+B,EAAC,EAChC,WAAA,GAAoC,EAAC,EACC;AACtC,EAAA,MAAM,SAASC,mBAAA,CAAa,MAAA,EAAQ,QAAQ,IAAA,EAAK,EAAG,YAAY,aAAa,CAAA;AAC7E,EAAA,mBAAA,CAAoB,OAAA,EAAS,QAAQ,WAAW,CAAA;AAChD,EAAA,OAAO,EAAE,QAAQ,OAAA,EAAQ;AAC3B","file":"index.cjs","sourcesContent":["import { CommanderError } from 'commander'\nimport type { Command } from 'commander'\n\nimport type { Client, ClientOptions } from '@getargvio/argvio'\nimport { createClient, ErrorCategory } from '@getargvio/argvio'\nimport type { ErrorCategory as ErrorCategoryType } from '@getargvio/argvio'\n\n/** Configures {@link instrumentCommander}. */\nexport interface CommanderHookOptions {\n /** Maps a caught error to an {@link ErrorCategoryType}. Without it, every error is recorded as `ErrorCategory.Unknown`. */\n classify?: (err: Error) => ErrorCategoryType\n /** Skip the `outputHelp` wrapping. */\n captureHelp?: boolean\n /** Skip the `preAction` hook-based command-invocation capture. */\n captureInvocation?: boolean\n}\n\n/** Names of options the user explicitly passed on the command line — never their values. */\nfunction changedOptionNames(cmd: Command): string[] {\n const names: string[] = []\n for (const option of cmd.options) {\n const key = option.attributeName()\n if (cmd.getOptionValueSource(key) === 'cli') {\n names.push(option.long?.replace(/^--/, '') ?? option.short?.replace(/^-/, '') ?? key)\n }\n }\n return names\n}\n\n/** The full command path (e.g. `\"mycli sub leaf\"`), walking up to the root command. */\nfunction commandPath(cmd: Command): string {\n const names: string[] = []\n for (let current: Command | null = cmd; current; current = current.parent) {\n names.unshift(current.name())\n }\n return names.join(' ')\n}\n\n/**\n * Reports whether `err` represents commander's own control flow (help\n * or version display via `--help`/`-h`/`--version`) rather than an\n * actual failure. `exitOverride()` makes both of those throw a\n * `CommanderError` with `exitCode: 0` instead of calling\n * `process.exit()` directly — this package must not record those as\n * errors or non-zero exit codes.\n */\nfunction isControlFlowExit(err: unknown): err is CommanderError {\n return err instanceof CommanderError && err.exitCode === 0\n}\n\n/**\n * Wires automatic telemetry capture into a commander command tree:\n *\n * - Command path + option names (see `Client.recordCommandInvocation`)\n * are captured via `program.hook('preAction', ...)` — fires reliably\n * for every resolved action command (verified against commander's own\n * source: unlike `postAction`, `preAction` runs unconditionally before\n * the action handler).\n * - Exit code and latency are captured by wrapping `program.parseAsync`\n * — `postAction` does **not** run when an action handler throws or its\n * returned promise rejects, so this package does not rely on it at\n * all, measuring whole-invocation latency instead.\n * - Errors are captured in that same `parseAsync` wrapper's `catch`,\n * distinguishing a real failure from commander's own `--help`/\n * `--version` control-flow exits (see {@link isControlFlowExit}).\n * - Help usage is captured by wrapping the public, chainable\n * `outputHelp` method.\n *\n * Calls `program.exitOverride()`: without it, commander calls\n * `process.exit()` itself on `--help`, `--version`, or a command\n * failure, before this package's wrapped `parseAsync` ever gets a\n * chance to record anything. After instrumenting, your own code owns\n * the real process exit — see docs/commander-integration.md.\n */\nexport function instrumentCommander(\n program: Command,\n client: Client,\n options: CommanderHookOptions = {},\n): Command {\n const classify = options.classify ?? (() => ErrorCategory.Unknown)\n const captureHelp = options.captureHelp ?? true\n const captureInvocation = options.captureInvocation ?? true\n\n program.exitOverride()\n\n let lastCommandPath = program.name()\n\n if (captureInvocation) {\n program.hook('preAction', (_thisCommand, actionCommand) => {\n lastCommandPath = commandPath(actionCommand)\n client.recordCommandInvocation(lastCommandPath, changedOptionNames(actionCommand))\n })\n }\n\n if (captureHelp) {\n const originalOutputHelp = program.outputHelp.bind(program)\n program.outputHelp = ((...args: Parameters<Command['outputHelp']>) => {\n client.recordHelpFlagUsage(lastCommandPath)\n return originalOutputHelp(...args)\n }) as Command['outputHelp']\n }\n\n const originalParseAsync = program.parseAsync.bind(program)\n program.parseAsync = (async (...args: Parameters<Command['parseAsync']>) => {\n const start = Date.now()\n try {\n const result = await originalParseAsync(...args)\n client.recordExitCode(lastCommandPath, 0)\n client.recordLatency(lastCommandPath, Date.now() - start)\n return result\n } catch (err) {\n client.recordLatency(lastCommandPath, Date.now() - start)\n if (isControlFlowExit(err)) {\n client.recordExitCode(lastCommandPath, 0)\n } else {\n client.recordExitCode(lastCommandPath, 1)\n const error = err instanceof Error ? err : new Error(String(err))\n client.recordError(lastCommandPath, classify(error))\n }\n throw err\n }\n }) as Command['parseAsync']\n\n return program\n}\n\n/**\n * Builds a `Client` (using `program.name()`/`program.version()`) and\n * calls {@link instrumentCommander} to wire up capture.\n *\n * const { client } = instrument(program, apiKey, '1.4.0')\n * await program.parseAsync()\n * await client.shutdown()\n */\nexport function instrument(\n program: Command,\n apiKey: string,\n cliVersion: string,\n clientOptions: ClientOptions = {},\n hookOptions: CommanderHookOptions = {},\n): { client: Client; program: Command } {\n const client = createClient(apiKey, program.name(), cliVersion, clientOptions)\n instrumentCommander(program, client, hookOptions)\n return { client, program }\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["CommanderError","ErrorCategory","createClient"],"mappings":";;;;;;AAyBA,SAAS,mBAAmB,GAAA,EAAwB;AAClD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,MAAW,MAAA,IAAU,IAAI,OAAA,EAAS;AAChC,IAAA,MAAM,GAAA,GAAM,OAAO,aAAA,EAAc;AACjC,IAAA,IAAI,GAAA,CAAI,oBAAA,CAAqB,GAAG,CAAA,KAAM,KAAA,EAAO;AAC3C,MAAA,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,IAAA,EAAM,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,IAAK,MAAA,CAAO,KAAA,EAAO,OAAA,CAAQ,IAAA,EAAM,EAAE,KAAK,GAAG,CAAA;AAAA,IACtF;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,YAAY,GAAA,EAAsB;AACzC,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,IAAS,OAAA,GAA0B,GAAA,EAAK,OAAA,EAAS,OAAA,GAAU,QAAQ,MAAA,EAAQ;AACzE,IAAA,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,KAAA,CAAM,KAAK,GAAG,CAAA;AACvB;AAUA,SAAS,kBAAkB,GAAA,EAAqC;AAC9D,EAAA,OAAO,GAAA,YAAeA,wBAAA,IAAkB,GAAA,CAAI,QAAA,KAAa,CAAA;AAC3D;AA2BO,SAAS,mBAAA,CACd,OAAA,EACA,MAAA,EACA,OAAA,GAAgC,EAAC,EACxB;AACT,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,QAAA,KAAa,MAAMC,oBAAA,CAAc,OAAA,CAAA;AAC1D,EAAA,MAAM,WAAA,GAAc,QAAQ,WAAA,IAAe,IAAA;AAC3C,EAAA,MAAM,iBAAA,GAAoB,QAAQ,iBAAA,IAAqB,IAAA;AAEvD,EAAA,OAAA,CAAQ,YAAA,EAAa;AAErB,EAAA,IAAI,eAAA,GAAkB,QAAQ,IAAA,EAAK;AACnC,EAAA,IAAI,gBAA0B,EAAC;AAE/B,EAAA,IAAI,iBAAA,EAAmB;AACrB,IAAA,OAAA,CAAQ,IAAA,CAAK,WAAA,EAAa,CAAC,YAAA,EAAc,aAAA,KAAkB;AACzD,MAAA,eAAA,GAAkB,YAAY,aAAa,CAAA;AAC3C,MAAA,aAAA,GAAgB,mBAAmB,aAAa,CAAA;AAChD,MAAA,MAAA,CAAO,uBAAA,CAAwB,iBAAiB,aAAa,CAAA;AAAA,IAC/D,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,WAAA,EAAa;AACf,IAAA,MAAM,kBAAA,GAAqB,OAAA,CAAQ,UAAA,CAAW,IAAA,CAAK,OAAO,CAAA;AAC1D,IAAA,OAAA,CAAQ,UAAA,IAAc,IAAI,IAAA,KAA4C;AACpE,MAAA,MAAA,CAAO,oBAAoB,eAAe,CAAA;AAC1C,MAAA,OAAO,kBAAA,CAAmB,GAAG,IAAI,CAAA;AAAA,IACnC,CAAA,CAAA;AAAA,EACF;AAEA,EAAA,MAAM,kBAAA,GAAqB,OAAA,CAAQ,UAAA,CAAW,IAAA,CAAK,OAAO,CAAA;AAC1D,EAAA,OAAA,CAAQ,UAAA,IAAc,UAAU,IAAA,KAA4C;AAC1E,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,EAAI;AACvB,IAAA,MAAA,CAAO,kBAAA,EAAmB;AAC1B,IAAA,MAAM,aAAA,GAAgB,CAAC,QAAA,KAA2B;AAChD,MAAA,MAAA,CAAO,aAAA,CAAc;AAAA,QACnB,IAAA,EAAM,eAAA;AAAA,QACN,SAAA,EAAW,aAAA;AAAA,QACX,OAAA,EAAS,KAAA;AAAA,QACT,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA;AAAA,QACzB;AAAA,OACD,CAAA;AAAA,IACH,CAAA;AACA,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,kBAAA,CAAmB,GAAG,IAAI,CAAA;AAC/C,MAAA,aAAA,CAAc,CAAC,CAAA;AACf,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,GAAA,EAAK;AAEZ,MAAA,IAAI,CAAC,iBAAA,CAAkB,GAAG,CAAA,EAAG;AAC3B,QAAA,aAAA,CAAc,CAAC,CAAA;AACf,QAAA,MAAM,KAAA,GAAQ,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAChE,QAAA,MAAM,UAAA,GAAa,SAAS,KAAK,CAAA;AACjC,QAAA,MAAM,EAAE,QAAA,EAAU,MAAA,EAAO,GACvB,OAAO,eAAe,QAAA,GAAW,EAAE,QAAA,EAAU,UAAA,EAAW,GAAI,UAAA;AAC9D,QAAA,MAAA,CAAO,WAAA,CAAY,eAAA,EAAiB,QAAA,EAAU,MAAM,CAAA;AAAA,MACtD;AACA,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF,CAAA,CAAA;AAEA,EAAA,OAAO,OAAA;AACT;AAUO,SAAS,UAAA,CACd,SACA,MAAA,EACA,UAAA,EACA,gBAA+B,EAAC,EAChC,WAAA,GAAoC,EAAC,EACC;AACtC,EAAA,MAAM,SAASC,mBAAA,CAAa,MAAA,EAAQ,QAAQ,IAAA,EAAK,EAAG,YAAY,aAAa,CAAA;AAC7E,EAAA,mBAAA,CAAoB,OAAA,EAAS,QAAQ,WAAW,CAAA;AAChD,EAAA,OAAO,EAAE,QAAQ,OAAA,EAAQ;AAC3B","file":"index.cjs","sourcesContent":["import { CommanderError } from 'commander'\nimport type { Command } from 'commander'\n\nimport type { Client, ClientOptions } from '@getargvio/argvio'\nimport { createClient, ErrorCategory } from '@getargvio/argvio'\nimport type { ErrorCategory as ErrorCategoryType, ErrorClassification } from '@getargvio/argvio'\n\n/** Configures {@link instrumentCommander}. */\nexport interface CommanderHookOptions {\n /**\n * Maps a caught error to an {@link ErrorCategoryType}, or to\n * `{ category, reason }` to also report a fixed-vocabulary reason (e.g.\n * `\"no-token\"`). The reason must be an identifier chosen in code\n * (`^[a-z0-9][a-z0-9._-]*$`, at most 64 bytes), never error text or user\n * input; one that doesn't match is dropped. Without `classify`, every\n * error is recorded as `ErrorCategory.Unknown` with no reason.\n */\n classify?: (err: Error) => ErrorCategoryType | ErrorClassification\n /** Skip the `outputHelp` wrapping. */\n captureHelp?: boolean\n /** Skip the `preAction` hook-based command-invocation capture. */\n captureInvocation?: boolean\n}\n\n/** Names of options the user explicitly passed on the command line — never their values. */\nfunction changedOptionNames(cmd: Command): string[] {\n const names: string[] = []\n for (const option of cmd.options) {\n const key = option.attributeName()\n if (cmd.getOptionValueSource(key) === 'cli') {\n names.push(option.long?.replace(/^--/, '') ?? option.short?.replace(/^-/, '') ?? key)\n }\n }\n return names\n}\n\n/** The full command path (e.g. `\"mycli sub leaf\"`), walking up to the root command. */\nfunction commandPath(cmd: Command): string {\n const names: string[] = []\n for (let current: Command | null = cmd; current; current = current.parent) {\n names.unshift(current.name())\n }\n return names.join(' ')\n}\n\n/**\n * Reports whether `err` represents commander's own control flow (help\n * or version display via `--help`/`-h`/`--version`) rather than an\n * actual failure. `exitOverride()` makes both of those throw a\n * `CommanderError` with `exitCode: 0` instead of calling\n * `process.exit()` directly — this package must not record those as\n * errors or non-zero exit codes.\n */\nfunction isControlFlowExit(err: unknown): err is CommanderError {\n return err instanceof CommanderError && err.exitCode === 0\n}\n\n/**\n * Wires automatic telemetry capture into a commander command tree:\n *\n * - Command path + option names (see `Client.recordCommandInvocation`)\n * are captured via `program.hook('preAction', ...)` — fires reliably\n * for every resolved action command (verified against commander's own\n * source: unlike `postAction`, `preAction` runs unconditionally before\n * the action handler).\n * - The session start, and each completed command's exit code and\n * latency (`Client.recordCommand`), are captured by wrapping `program.parseAsync`\n * — `postAction` does **not** run when an action handler throws or its\n * returned promise rejects, so this package does not rely on it at\n * all, measuring whole-invocation latency instead.\n * - Errors are captured in that same `parseAsync` wrapper's `catch`,\n * distinguishing a real failure from commander's own `--help`/\n * `--version` control-flow exits (see {@link isControlFlowExit}).\n * - Help usage is captured by wrapping the public, chainable\n * `outputHelp` method.\n *\n * Calls `program.exitOverride()`: without it, commander calls\n * `process.exit()` itself on `--help`, `--version`, or a command\n * failure, before this package's wrapped `parseAsync` ever gets a\n * chance to record anything. After instrumenting, your own code owns\n * the real process exit — see docs/commander-integration.md.\n */\nexport function instrumentCommander(\n program: Command,\n client: Client,\n options: CommanderHookOptions = {},\n): Command {\n const classify = options.classify ?? (() => ErrorCategory.Unknown)\n const captureHelp = options.captureHelp ?? true\n const captureInvocation = options.captureInvocation ?? true\n\n program.exitOverride()\n\n let lastCommandPath = program.name()\n let lastFlagNames: string[] = []\n\n if (captureInvocation) {\n program.hook('preAction', (_thisCommand, actionCommand) => {\n lastCommandPath = commandPath(actionCommand)\n lastFlagNames = changedOptionNames(actionCommand)\n client.recordCommandInvocation(lastCommandPath, lastFlagNames)\n })\n }\n\n if (captureHelp) {\n const originalOutputHelp = program.outputHelp.bind(program)\n program.outputHelp = ((...args: Parameters<Command['outputHelp']>) => {\n client.recordHelpFlagUsage(lastCommandPath)\n return originalOutputHelp(...args)\n }) as Command['outputHelp']\n }\n\n const originalParseAsync = program.parseAsync.bind(program)\n program.parseAsync = (async (...args: Parameters<Command['parseAsync']>) => {\n const start = Date.now()\n client.recordSessionStart()\n const recordCommand = (exitCode: number): void => {\n client.recordCommand({\n path: lastCommandPath,\n flagNames: lastFlagNames,\n startMs: start,\n durationMs: Date.now() - start,\n exitCode,\n })\n }\n try {\n const result = await originalParseAsync(...args)\n recordCommand(0)\n return result\n } catch (err) {\n // --help/--version aren't command runs, same as in the cobra integration.\n if (!isControlFlowExit(err)) {\n recordCommand(1)\n const error = err instanceof Error ? err : new Error(String(err))\n const classified = classify(error)\n const { category, reason } =\n typeof classified === 'string' ? { category: classified } : classified\n client.recordError(lastCommandPath, category, reason)\n }\n throw err\n }\n }) as Command['parseAsync']\n\n return program\n}\n\n/**\n * Builds a `Client` (using `program.name()`/`program.version()`) and\n * calls {@link instrumentCommander} to wire up capture.\n *\n * const { client } = instrument(program, apiKey, '1.4.0')\n * await program.parseAsync()\n * await client.shutdown()\n */\nexport function instrument(\n program: Command,\n apiKey: string,\n cliVersion: string,\n clientOptions: ClientOptions = {},\n hookOptions: CommanderHookOptions = {},\n): { client: Client; program: Command } {\n const client = createClient(apiKey, program.name(), cliVersion, clientOptions)\n instrumentCommander(program, client, hookOptions)\n return { client, program }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { Command } from 'commander';
2
- import { ErrorCategory, ClientOptions, Client } from '@getargvio/argvio';
2
+ import { ErrorCategory, ErrorClassification, ClientOptions, Client } from '@getargvio/argvio';
3
3
 
4
4
  /** Configures {@link instrumentCommander}. */
5
5
  interface CommanderHookOptions {
6
- /** Maps a caught error to an {@link ErrorCategoryType}. Without it, every error is recorded as `ErrorCategory.Unknown`. */
7
- classify?: (err: Error) => ErrorCategory;
6
+ /**
7
+ * Maps a caught error to an {@link ErrorCategoryType}, or to
8
+ * `{ category, reason }` to also report a fixed-vocabulary reason (e.g.
9
+ * `"no-token"`). The reason must be an identifier chosen in code
10
+ * (`^[a-z0-9][a-z0-9._-]*$`, at most 64 bytes), never error text or user
11
+ * input; one that doesn't match is dropped. Without `classify`, every
12
+ * error is recorded as `ErrorCategory.Unknown` with no reason.
13
+ */
14
+ classify?: (err: Error) => ErrorCategory | ErrorClassification;
8
15
  /** Skip the `outputHelp` wrapping. */
9
16
  captureHelp?: boolean;
10
17
  /** Skip the `preAction` hook-based command-invocation capture. */
@@ -18,7 +25,8 @@ interface CommanderHookOptions {
18
25
  * for every resolved action command (verified against commander's own
19
26
  * source: unlike `postAction`, `preAction` runs unconditionally before
20
27
  * the action handler).
21
- * - Exit code and latency are captured by wrapping `program.parseAsync`
28
+ * - The session start, and each completed command's exit code and
29
+ * latency (`Client.recordCommand`), are captured by wrapping `program.parseAsync`
22
30
  * — `postAction` does **not** run when an action handler throws or its
23
31
  * returned promise rejects, so this package does not rely on it at
24
32
  * all, measuring whole-invocation latency instead.
package/dist/index.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { Command } from 'commander';
2
- import { ErrorCategory, ClientOptions, Client } from '@getargvio/argvio';
2
+ import { ErrorCategory, ErrorClassification, ClientOptions, Client } from '@getargvio/argvio';
3
3
 
4
4
  /** Configures {@link instrumentCommander}. */
5
5
  interface CommanderHookOptions {
6
- /** Maps a caught error to an {@link ErrorCategoryType}. Without it, every error is recorded as `ErrorCategory.Unknown`. */
7
- classify?: (err: Error) => ErrorCategory;
6
+ /**
7
+ * Maps a caught error to an {@link ErrorCategoryType}, or to
8
+ * `{ category, reason }` to also report a fixed-vocabulary reason (e.g.
9
+ * `"no-token"`). The reason must be an identifier chosen in code
10
+ * (`^[a-z0-9][a-z0-9._-]*$`, at most 64 bytes), never error text or user
11
+ * input; one that doesn't match is dropped. Without `classify`, every
12
+ * error is recorded as `ErrorCategory.Unknown` with no reason.
13
+ */
14
+ classify?: (err: Error) => ErrorCategory | ErrorClassification;
8
15
  /** Skip the `outputHelp` wrapping. */
9
16
  captureHelp?: boolean;
10
17
  /** Skip the `preAction` hook-based command-invocation capture. */
@@ -18,7 +25,8 @@ interface CommanderHookOptions {
18
25
  * for every resolved action command (verified against commander's own
19
26
  * source: unlike `postAction`, `preAction` runs unconditionally before
20
27
  * the action handler).
21
- * - Exit code and latency are captured by wrapping `program.parseAsync`
28
+ * - The session start, and each completed command's exit code and
29
+ * latency (`Client.recordCommand`), are captured by wrapping `program.parseAsync`
22
30
  * — `postAction` does **not** run when an action handler throws or its
23
31
  * returned promise rejects, so this package does not rely on it at
24
32
  * all, measuring whole-invocation latency instead.
package/dist/index.js CHANGED
@@ -28,10 +28,12 @@ function instrumentCommander(program, client, options = {}) {
28
28
  const captureInvocation = options.captureInvocation ?? true;
29
29
  program.exitOverride();
30
30
  let lastCommandPath = program.name();
31
+ let lastFlagNames = [];
31
32
  if (captureInvocation) {
32
33
  program.hook("preAction", (_thisCommand, actionCommand) => {
33
34
  lastCommandPath = commandPath(actionCommand);
34
- client.recordCommandInvocation(lastCommandPath, changedOptionNames(actionCommand));
35
+ lastFlagNames = changedOptionNames(actionCommand);
36
+ client.recordCommandInvocation(lastCommandPath, lastFlagNames);
35
37
  });
36
38
  }
37
39
  if (captureHelp) {
@@ -44,19 +46,27 @@ function instrumentCommander(program, client, options = {}) {
44
46
  const originalParseAsync = program.parseAsync.bind(program);
45
47
  program.parseAsync = (async (...args) => {
46
48
  const start = Date.now();
49
+ client.recordSessionStart();
50
+ const recordCommand = (exitCode) => {
51
+ client.recordCommand({
52
+ path: lastCommandPath,
53
+ flagNames: lastFlagNames,
54
+ startMs: start,
55
+ durationMs: Date.now() - start,
56
+ exitCode
57
+ });
58
+ };
47
59
  try {
48
60
  const result = await originalParseAsync(...args);
49
- client.recordExitCode(lastCommandPath, 0);
50
- client.recordLatency(lastCommandPath, Date.now() - start);
61
+ recordCommand(0);
51
62
  return result;
52
63
  } catch (err) {
53
- client.recordLatency(lastCommandPath, Date.now() - start);
54
- if (isControlFlowExit(err)) {
55
- client.recordExitCode(lastCommandPath, 0);
56
- } else {
57
- client.recordExitCode(lastCommandPath, 1);
64
+ if (!isControlFlowExit(err)) {
65
+ recordCommand(1);
58
66
  const error = err instanceof Error ? err : new Error(String(err));
59
- client.recordError(lastCommandPath, classify(error));
67
+ const classified = classify(error);
68
+ const { category, reason } = typeof classified === "string" ? { category: classified } : classified;
69
+ client.recordError(lastCommandPath, category, reason);
60
70
  }
61
71
  throw err;
62
72
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;AAkBA,SAAS,mBAAmB,GAAA,EAAwB;AAClD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,MAAW,MAAA,IAAU,IAAI,OAAA,EAAS;AAChC,IAAA,MAAM,GAAA,GAAM,OAAO,aAAA,EAAc;AACjC,IAAA,IAAI,GAAA,CAAI,oBAAA,CAAqB,GAAG,CAAA,KAAM,KAAA,EAAO;AAC3C,MAAA,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,IAAA,EAAM,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,IAAK,MAAA,CAAO,KAAA,EAAO,OAAA,CAAQ,IAAA,EAAM,EAAE,KAAK,GAAG,CAAA;AAAA,IACtF;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,YAAY,GAAA,EAAsB;AACzC,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,IAAS,OAAA,GAA0B,GAAA,EAAK,OAAA,EAAS,OAAA,GAAU,QAAQ,MAAA,EAAQ;AACzE,IAAA,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,KAAA,CAAM,KAAK,GAAG,CAAA;AACvB;AAUA,SAAS,kBAAkB,GAAA,EAAqC;AAC9D,EAAA,OAAO,GAAA,YAAe,cAAA,IAAkB,GAAA,CAAI,QAAA,KAAa,CAAA;AAC3D;AA0BO,SAAS,mBAAA,CACd,OAAA,EACA,MAAA,EACA,OAAA,GAAgC,EAAC,EACxB;AACT,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,QAAA,KAAa,MAAM,aAAA,CAAc,OAAA,CAAA;AAC1D,EAAA,MAAM,WAAA,GAAc,QAAQ,WAAA,IAAe,IAAA;AAC3C,EAAA,MAAM,iBAAA,GAAoB,QAAQ,iBAAA,IAAqB,IAAA;AAEvD,EAAA,OAAA,CAAQ,YAAA,EAAa;AAErB,EAAA,IAAI,eAAA,GAAkB,QAAQ,IAAA,EAAK;AAEnC,EAAA,IAAI,iBAAA,EAAmB;AACrB,IAAA,OAAA,CAAQ,IAAA,CAAK,WAAA,EAAa,CAAC,YAAA,EAAc,aAAA,KAAkB;AACzD,MAAA,eAAA,GAAkB,YAAY,aAAa,CAAA;AAC3C,MAAA,MAAA,CAAO,uBAAA,CAAwB,eAAA,EAAiB,kBAAA,CAAmB,aAAa,CAAC,CAAA;AAAA,IACnF,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,WAAA,EAAa;AACf,IAAA,MAAM,kBAAA,GAAqB,OAAA,CAAQ,UAAA,CAAW,IAAA,CAAK,OAAO,CAAA;AAC1D,IAAA,OAAA,CAAQ,UAAA,IAAc,IAAI,IAAA,KAA4C;AACpE,MAAA,MAAA,CAAO,oBAAoB,eAAe,CAAA;AAC1C,MAAA,OAAO,kBAAA,CAAmB,GAAG,IAAI,CAAA;AAAA,IACnC,CAAA,CAAA;AAAA,EACF;AAEA,EAAA,MAAM,kBAAA,GAAqB,OAAA,CAAQ,UAAA,CAAW,IAAA,CAAK,OAAO,CAAA;AAC1D,EAAA,OAAA,CAAQ,UAAA,IAAc,UAAU,IAAA,KAA4C;AAC1E,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,EAAI;AACvB,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,kBAAA,CAAmB,GAAG,IAAI,CAAA;AAC/C,MAAA,MAAA,CAAO,cAAA,CAAe,iBAAiB,CAAC,CAAA;AACxC,MAAA,MAAA,CAAO,aAAA,CAAc,eAAA,EAAiB,IAAA,CAAK,GAAA,KAAQ,KAAK,CAAA;AACxD,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,GAAA,EAAK;AACZ,MAAA,MAAA,CAAO,aAAA,CAAc,eAAA,EAAiB,IAAA,CAAK,GAAA,KAAQ,KAAK,CAAA;AACxD,MAAA,IAAI,iBAAA,CAAkB,GAAG,CAAA,EAAG;AAC1B,QAAA,MAAA,CAAO,cAAA,CAAe,iBAAiB,CAAC,CAAA;AAAA,MAC1C,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,cAAA,CAAe,iBAAiB,CAAC,CAAA;AACxC,QAAA,MAAM,KAAA,GAAQ,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAChE,QAAA,MAAA,CAAO,WAAA,CAAY,eAAA,EAAiB,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,MACrD;AACA,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF,CAAA,CAAA;AAEA,EAAA,OAAO,OAAA;AACT;AAUO,SAAS,UAAA,CACd,SACA,MAAA,EACA,UAAA,EACA,gBAA+B,EAAC,EAChC,WAAA,GAAoC,EAAC,EACC;AACtC,EAAA,MAAM,SAAS,YAAA,CAAa,MAAA,EAAQ,QAAQ,IAAA,EAAK,EAAG,YAAY,aAAa,CAAA;AAC7E,EAAA,mBAAA,CAAoB,OAAA,EAAS,QAAQ,WAAW,CAAA;AAChD,EAAA,OAAO,EAAE,QAAQ,OAAA,EAAQ;AAC3B","file":"index.js","sourcesContent":["import { CommanderError } from 'commander'\nimport type { Command } from 'commander'\n\nimport type { Client, ClientOptions } from '@getargvio/argvio'\nimport { createClient, ErrorCategory } from '@getargvio/argvio'\nimport type { ErrorCategory as ErrorCategoryType } from '@getargvio/argvio'\n\n/** Configures {@link instrumentCommander}. */\nexport interface CommanderHookOptions {\n /** Maps a caught error to an {@link ErrorCategoryType}. Without it, every error is recorded as `ErrorCategory.Unknown`. */\n classify?: (err: Error) => ErrorCategoryType\n /** Skip the `outputHelp` wrapping. */\n captureHelp?: boolean\n /** Skip the `preAction` hook-based command-invocation capture. */\n captureInvocation?: boolean\n}\n\n/** Names of options the user explicitly passed on the command line — never their values. */\nfunction changedOptionNames(cmd: Command): string[] {\n const names: string[] = []\n for (const option of cmd.options) {\n const key = option.attributeName()\n if (cmd.getOptionValueSource(key) === 'cli') {\n names.push(option.long?.replace(/^--/, '') ?? option.short?.replace(/^-/, '') ?? key)\n }\n }\n return names\n}\n\n/** The full command path (e.g. `\"mycli sub leaf\"`), walking up to the root command. */\nfunction commandPath(cmd: Command): string {\n const names: string[] = []\n for (let current: Command | null = cmd; current; current = current.parent) {\n names.unshift(current.name())\n }\n return names.join(' ')\n}\n\n/**\n * Reports whether `err` represents commander's own control flow (help\n * or version display via `--help`/`-h`/`--version`) rather than an\n * actual failure. `exitOverride()` makes both of those throw a\n * `CommanderError` with `exitCode: 0` instead of calling\n * `process.exit()` directly — this package must not record those as\n * errors or non-zero exit codes.\n */\nfunction isControlFlowExit(err: unknown): err is CommanderError {\n return err instanceof CommanderError && err.exitCode === 0\n}\n\n/**\n * Wires automatic telemetry capture into a commander command tree:\n *\n * - Command path + option names (see `Client.recordCommandInvocation`)\n * are captured via `program.hook('preAction', ...)` — fires reliably\n * for every resolved action command (verified against commander's own\n * source: unlike `postAction`, `preAction` runs unconditionally before\n * the action handler).\n * - Exit code and latency are captured by wrapping `program.parseAsync`\n * — `postAction` does **not** run when an action handler throws or its\n * returned promise rejects, so this package does not rely on it at\n * all, measuring whole-invocation latency instead.\n * - Errors are captured in that same `parseAsync` wrapper's `catch`,\n * distinguishing a real failure from commander's own `--help`/\n * `--version` control-flow exits (see {@link isControlFlowExit}).\n * - Help usage is captured by wrapping the public, chainable\n * `outputHelp` method.\n *\n * Calls `program.exitOverride()`: without it, commander calls\n * `process.exit()` itself on `--help`, `--version`, or a command\n * failure, before this package's wrapped `parseAsync` ever gets a\n * chance to record anything. After instrumenting, your own code owns\n * the real process exit — see docs/commander-integration.md.\n */\nexport function instrumentCommander(\n program: Command,\n client: Client,\n options: CommanderHookOptions = {},\n): Command {\n const classify = options.classify ?? (() => ErrorCategory.Unknown)\n const captureHelp = options.captureHelp ?? true\n const captureInvocation = options.captureInvocation ?? true\n\n program.exitOverride()\n\n let lastCommandPath = program.name()\n\n if (captureInvocation) {\n program.hook('preAction', (_thisCommand, actionCommand) => {\n lastCommandPath = commandPath(actionCommand)\n client.recordCommandInvocation(lastCommandPath, changedOptionNames(actionCommand))\n })\n }\n\n if (captureHelp) {\n const originalOutputHelp = program.outputHelp.bind(program)\n program.outputHelp = ((...args: Parameters<Command['outputHelp']>) => {\n client.recordHelpFlagUsage(lastCommandPath)\n return originalOutputHelp(...args)\n }) as Command['outputHelp']\n }\n\n const originalParseAsync = program.parseAsync.bind(program)\n program.parseAsync = (async (...args: Parameters<Command['parseAsync']>) => {\n const start = Date.now()\n try {\n const result = await originalParseAsync(...args)\n client.recordExitCode(lastCommandPath, 0)\n client.recordLatency(lastCommandPath, Date.now() - start)\n return result\n } catch (err) {\n client.recordLatency(lastCommandPath, Date.now() - start)\n if (isControlFlowExit(err)) {\n client.recordExitCode(lastCommandPath, 0)\n } else {\n client.recordExitCode(lastCommandPath, 1)\n const error = err instanceof Error ? err : new Error(String(err))\n client.recordError(lastCommandPath, classify(error))\n }\n throw err\n }\n }) as Command['parseAsync']\n\n return program\n}\n\n/**\n * Builds a `Client` (using `program.name()`/`program.version()`) and\n * calls {@link instrumentCommander} to wire up capture.\n *\n * const { client } = instrument(program, apiKey, '1.4.0')\n * await program.parseAsync()\n * await client.shutdown()\n */\nexport function instrument(\n program: Command,\n apiKey: string,\n cliVersion: string,\n clientOptions: ClientOptions = {},\n hookOptions: CommanderHookOptions = {},\n): { client: Client; program: Command } {\n const client = createClient(apiKey, program.name(), cliVersion, clientOptions)\n instrumentCommander(program, client, hookOptions)\n return { client, program }\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { CommanderError } from 'commander'\nimport type { Command } from 'commander'\n\nimport type { Client, ClientOptions } from '@getargvio/argvio'\nimport { createClient, ErrorCategory } from '@getargvio/argvio'\nimport type { ErrorCategory as ErrorCategoryType, ErrorClassification } from '@getargvio/argvio'\n\n/** Configures {@link instrumentCommander}. */\nexport interface CommanderHookOptions {\n /**\n * Maps a caught error to an {@link ErrorCategoryType}, or to\n * `{ category, reason }` to also report a fixed-vocabulary reason (e.g.\n * `\"no-token\"`). The reason must be an identifier chosen in code\n * (`^[a-z0-9][a-z0-9._-]*$`, at most 64 bytes), never error text or user\n * input; one that doesn't match is dropped. Without `classify`, every\n * error is recorded as `ErrorCategory.Unknown` with no reason.\n */\n classify?: (err: Error) => ErrorCategoryType | ErrorClassification\n /** Skip the `outputHelp` wrapping. */\n captureHelp?: boolean\n /** Skip the `preAction` hook-based command-invocation capture. */\n captureInvocation?: boolean\n}\n\n/** Names of options the user explicitly passed on the command line — never their values. */\nfunction changedOptionNames(cmd: Command): string[] {\n const names: string[] = []\n for (const option of cmd.options) {\n const key = option.attributeName()\n if (cmd.getOptionValueSource(key) === 'cli') {\n names.push(option.long?.replace(/^--/, '') ?? option.short?.replace(/^-/, '') ?? key)\n }\n }\n return names\n}\n\n/** The full command path (e.g. `\"mycli sub leaf\"`), walking up to the root command. */\nfunction commandPath(cmd: Command): string {\n const names: string[] = []\n for (let current: Command | null = cmd; current; current = current.parent) {\n names.unshift(current.name())\n }\n return names.join(' ')\n}\n\n/**\n * Reports whether `err` represents commander's own control flow (help\n * or version display via `--help`/`-h`/`--version`) rather than an\n * actual failure. `exitOverride()` makes both of those throw a\n * `CommanderError` with `exitCode: 0` instead of calling\n * `process.exit()` directly — this package must not record those as\n * errors or non-zero exit codes.\n */\nfunction isControlFlowExit(err: unknown): err is CommanderError {\n return err instanceof CommanderError && err.exitCode === 0\n}\n\n/**\n * Wires automatic telemetry capture into a commander command tree:\n *\n * - Command path + option names (see `Client.recordCommandInvocation`)\n * are captured via `program.hook('preAction', ...)` — fires reliably\n * for every resolved action command (verified against commander's own\n * source: unlike `postAction`, `preAction` runs unconditionally before\n * the action handler).\n * - The session start, and each completed command's exit code and\n * latency (`Client.recordCommand`), are captured by wrapping `program.parseAsync`\n * — `postAction` does **not** run when an action handler throws or its\n * returned promise rejects, so this package does not rely on it at\n * all, measuring whole-invocation latency instead.\n * - Errors are captured in that same `parseAsync` wrapper's `catch`,\n * distinguishing a real failure from commander's own `--help`/\n * `--version` control-flow exits (see {@link isControlFlowExit}).\n * - Help usage is captured by wrapping the public, chainable\n * `outputHelp` method.\n *\n * Calls `program.exitOverride()`: without it, commander calls\n * `process.exit()` itself on `--help`, `--version`, or a command\n * failure, before this package's wrapped `parseAsync` ever gets a\n * chance to record anything. After instrumenting, your own code owns\n * the real process exit — see docs/commander-integration.md.\n */\nexport function instrumentCommander(\n program: Command,\n client: Client,\n options: CommanderHookOptions = {},\n): Command {\n const classify = options.classify ?? (() => ErrorCategory.Unknown)\n const captureHelp = options.captureHelp ?? true\n const captureInvocation = options.captureInvocation ?? true\n\n program.exitOverride()\n\n let lastCommandPath = program.name()\n let lastFlagNames: string[] = []\n\n if (captureInvocation) {\n program.hook('preAction', (_thisCommand, actionCommand) => {\n lastCommandPath = commandPath(actionCommand)\n lastFlagNames = changedOptionNames(actionCommand)\n client.recordCommandInvocation(lastCommandPath, lastFlagNames)\n })\n }\n\n if (captureHelp) {\n const originalOutputHelp = program.outputHelp.bind(program)\n program.outputHelp = ((...args: Parameters<Command['outputHelp']>) => {\n client.recordHelpFlagUsage(lastCommandPath)\n return originalOutputHelp(...args)\n }) as Command['outputHelp']\n }\n\n const originalParseAsync = program.parseAsync.bind(program)\n program.parseAsync = (async (...args: Parameters<Command['parseAsync']>) => {\n const start = Date.now()\n client.recordSessionStart()\n const recordCommand = (exitCode: number): void => {\n client.recordCommand({\n path: lastCommandPath,\n flagNames: lastFlagNames,\n startMs: start,\n durationMs: Date.now() - start,\n exitCode,\n })\n }\n try {\n const result = await originalParseAsync(...args)\n recordCommand(0)\n return result\n } catch (err) {\n // --help/--version aren't command runs, same as in the cobra integration.\n if (!isControlFlowExit(err)) {\n recordCommand(1)\n const error = err instanceof Error ? err : new Error(String(err))\n const classified = classify(error)\n const { category, reason } =\n typeof classified === 'string' ? { category: classified } : classified\n client.recordError(lastCommandPath, category, reason)\n }\n throw err\n }\n }) as Command['parseAsync']\n\n return program\n}\n\n/**\n * Builds a `Client` (using `program.name()`/`program.version()`) and\n * calls {@link instrumentCommander} to wire up capture.\n *\n * const { client } = instrument(program, apiKey, '1.4.0')\n * await program.parseAsync()\n * await client.shutdown()\n */\nexport function instrument(\n program: Command,\n apiKey: string,\n cliVersion: string,\n clientOptions: ClientOptions = {},\n hookOptions: CommanderHookOptions = {},\n): { client: Client; program: Command } {\n const client = createClient(apiKey, program.name(), cliVersion, clientOptions)\n instrumentCommander(program, client, hookOptions)\n return { client, program }\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@getargvio/commander",
3
3
  "description": "Auto-instruments a commander command tree with @getargvio/argvio CLI usage telemetry",
4
- "version": "0.1.0",
4
+ "version": "0.4.0",
5
5
  "author": "getargvio",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -42,22 +42,21 @@
42
42
  "engines": {
43
43
  "node": ">=22.0.0"
44
44
  },
45
- "scripts": {
46
- "build": "tsup",
47
- "typecheck": "tsc --noEmit",
48
- "test": "vitest run",
49
- "prepublishOnly": "npm run build"
50
- },
51
45
  "peerDependencies": {
52
46
  "commander": ">=12.0.0"
53
47
  },
54
48
  "dependencies": {
55
- "@getargvio/argvio": "^0.1.0"
49
+ "@getargvio/argvio": "0.5.0"
56
50
  },
57
51
  "devDependencies": {
58
52
  "@types/node": "^24",
59
53
  "commander": "^15",
60
54
  "typescript": "^5",
61
55
  "vitest": "^4.1.11"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "typecheck": "tsc --noEmit",
60
+ "test": "vitest run"
62
61
  }
63
- }
62
+ }