@getargvio/commander 0.1.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) 2026 getargvio
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 ADDED
@@ -0,0 +1,39 @@
1
+ # @getargvio/commander
2
+
3
+ Auto-instruments a [commander](https://github.com/tj/commander.js)
4
+ command tree with
5
+ [`@getargvio/argvio`](https://www.npmjs.com/package/@getargvio/argvio)
6
+ CLI usage telemetry.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install @getargvio/commander
12
+ ```
13
+
14
+ `commander` is a peer dependency — bring your own (`>=12.0.0`).
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { Command } from 'commander'
20
+ import { instrument } from '@getargvio/commander'
21
+
22
+ const program = new Command()
23
+ program.name('mycli').version('1.4.0')
24
+ // ... add commands as usual ...
25
+
26
+ const { client } = instrument(program, process.env.MYCLI_ARGVIO_KEY ?? '', '1.4.0')
27
+
28
+ await program.parseAsync()
29
+ await client.shutdown(3000)
30
+ ```
31
+
32
+ See [docs/commander-integration.md](docs/commander-integration.md) for
33
+ exactly what's captured automatically, the `exitOverride()` requirement,
34
+ and why `--help`/`--version` are treated as control flow rather than
35
+ errors.
36
+
37
+ ## License
38
+
39
+ MIT — see [LICENSE](../../LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ var commander = require('commander');
4
+ var argvio = require('@getargvio/argvio');
5
+
6
+ // src/index.ts
7
+ function changedOptionNames(cmd) {
8
+ const names = [];
9
+ for (const option of cmd.options) {
10
+ const key = option.attributeName();
11
+ if (cmd.getOptionValueSource(key) === "cli") {
12
+ names.push(option.long?.replace(/^--/, "") ?? option.short?.replace(/^-/, "") ?? key);
13
+ }
14
+ }
15
+ return names;
16
+ }
17
+ function commandPath(cmd) {
18
+ const names = [];
19
+ for (let current = cmd; current; current = current.parent) {
20
+ names.unshift(current.name());
21
+ }
22
+ return names.join(" ");
23
+ }
24
+ function isControlFlowExit(err) {
25
+ return err instanceof commander.CommanderError && err.exitCode === 0;
26
+ }
27
+ function instrumentCommander(program, client, options = {}) {
28
+ const classify = options.classify ?? (() => argvio.ErrorCategory.Unknown);
29
+ const captureHelp = options.captureHelp ?? true;
30
+ const captureInvocation = options.captureInvocation ?? true;
31
+ program.exitOverride();
32
+ let lastCommandPath = program.name();
33
+ if (captureInvocation) {
34
+ program.hook("preAction", (_thisCommand, actionCommand) => {
35
+ lastCommandPath = commandPath(actionCommand);
36
+ client.recordCommandInvocation(lastCommandPath, changedOptionNames(actionCommand));
37
+ });
38
+ }
39
+ if (captureHelp) {
40
+ const originalOutputHelp = program.outputHelp.bind(program);
41
+ program.outputHelp = ((...args) => {
42
+ client.recordHelpFlagUsage(lastCommandPath);
43
+ return originalOutputHelp(...args);
44
+ });
45
+ }
46
+ const originalParseAsync = program.parseAsync.bind(program);
47
+ program.parseAsync = (async (...args) => {
48
+ const start = Date.now();
49
+ try {
50
+ const result = await originalParseAsync(...args);
51
+ client.recordExitCode(lastCommandPath, 0);
52
+ client.recordLatency(lastCommandPath, Date.now() - start);
53
+ return result;
54
+ } 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);
60
+ const error = err instanceof Error ? err : new Error(String(err));
61
+ client.recordError(lastCommandPath, classify(error));
62
+ }
63
+ throw err;
64
+ }
65
+ });
66
+ return program;
67
+ }
68
+ function instrument(program, apiKey, cliVersion, clientOptions = {}, hookOptions = {}) {
69
+ const client = argvio.createClient(apiKey, program.name(), cliVersion, clientOptions);
70
+ instrumentCommander(program, client, hookOptions);
71
+ return { client, program };
72
+ }
73
+
74
+ exports.instrument = instrument;
75
+ exports.instrumentCommander = instrumentCommander;
76
+ //# sourceMappingURL=index.cjs.map
77
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,51 @@
1
+ import { Command } from 'commander';
2
+ import { ErrorCategory, ClientOptions, Client } from '@getargvio/argvio';
3
+
4
+ /** Configures {@link instrumentCommander}. */
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;
8
+ /** Skip the `outputHelp` wrapping. */
9
+ captureHelp?: boolean;
10
+ /** Skip the `preAction` hook-based command-invocation capture. */
11
+ captureInvocation?: boolean;
12
+ }
13
+ /**
14
+ * Wires automatic telemetry capture into a commander command tree:
15
+ *
16
+ * - Command path + option names (see `Client.recordCommandInvocation`)
17
+ * are captured via `program.hook('preAction', ...)` — fires reliably
18
+ * for every resolved action command (verified against commander's own
19
+ * source: unlike `postAction`, `preAction` runs unconditionally before
20
+ * the action handler).
21
+ * - Exit code and latency are captured by wrapping `program.parseAsync`
22
+ * — `postAction` does **not** run when an action handler throws or its
23
+ * returned promise rejects, so this package does not rely on it at
24
+ * all, measuring whole-invocation latency instead.
25
+ * - Errors are captured in that same `parseAsync` wrapper's `catch`,
26
+ * distinguishing a real failure from commander's own `--help`/
27
+ * `--version` control-flow exits (see {@link isControlFlowExit}).
28
+ * - Help usage is captured by wrapping the public, chainable
29
+ * `outputHelp` method.
30
+ *
31
+ * Calls `program.exitOverride()`: without it, commander calls
32
+ * `process.exit()` itself on `--help`, `--version`, or a command
33
+ * failure, before this package's wrapped `parseAsync` ever gets a
34
+ * chance to record anything. After instrumenting, your own code owns
35
+ * the real process exit — see docs/commander-integration.md.
36
+ */
37
+ declare function instrumentCommander(program: Command, client: Client, options?: CommanderHookOptions): Command;
38
+ /**
39
+ * Builds a `Client` (using `program.name()`/`program.version()`) and
40
+ * calls {@link instrumentCommander} to wire up capture.
41
+ *
42
+ * const { client } = instrument(program, apiKey, '1.4.0')
43
+ * await program.parseAsync()
44
+ * await client.shutdown()
45
+ */
46
+ declare function instrument(program: Command, apiKey: string, cliVersion: string, clientOptions?: ClientOptions, hookOptions?: CommanderHookOptions): {
47
+ client: Client;
48
+ program: Command;
49
+ };
50
+
51
+ export { type CommanderHookOptions, instrument, instrumentCommander };
@@ -0,0 +1,51 @@
1
+ import { Command } from 'commander';
2
+ import { ErrorCategory, ClientOptions, Client } from '@getargvio/argvio';
3
+
4
+ /** Configures {@link instrumentCommander}. */
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;
8
+ /** Skip the `outputHelp` wrapping. */
9
+ captureHelp?: boolean;
10
+ /** Skip the `preAction` hook-based command-invocation capture. */
11
+ captureInvocation?: boolean;
12
+ }
13
+ /**
14
+ * Wires automatic telemetry capture into a commander command tree:
15
+ *
16
+ * - Command path + option names (see `Client.recordCommandInvocation`)
17
+ * are captured via `program.hook('preAction', ...)` — fires reliably
18
+ * for every resolved action command (verified against commander's own
19
+ * source: unlike `postAction`, `preAction` runs unconditionally before
20
+ * the action handler).
21
+ * - Exit code and latency are captured by wrapping `program.parseAsync`
22
+ * — `postAction` does **not** run when an action handler throws or its
23
+ * returned promise rejects, so this package does not rely on it at
24
+ * all, measuring whole-invocation latency instead.
25
+ * - Errors are captured in that same `parseAsync` wrapper's `catch`,
26
+ * distinguishing a real failure from commander's own `--help`/
27
+ * `--version` control-flow exits (see {@link isControlFlowExit}).
28
+ * - Help usage is captured by wrapping the public, chainable
29
+ * `outputHelp` method.
30
+ *
31
+ * Calls `program.exitOverride()`: without it, commander calls
32
+ * `process.exit()` itself on `--help`, `--version`, or a command
33
+ * failure, before this package's wrapped `parseAsync` ever gets a
34
+ * chance to record anything. After instrumenting, your own code owns
35
+ * the real process exit — see docs/commander-integration.md.
36
+ */
37
+ declare function instrumentCommander(program: Command, client: Client, options?: CommanderHookOptions): Command;
38
+ /**
39
+ * Builds a `Client` (using `program.name()`/`program.version()`) and
40
+ * calls {@link instrumentCommander} to wire up capture.
41
+ *
42
+ * const { client } = instrument(program, apiKey, '1.4.0')
43
+ * await program.parseAsync()
44
+ * await client.shutdown()
45
+ */
46
+ declare function instrument(program: Command, apiKey: string, cliVersion: string, clientOptions?: ClientOptions, hookOptions?: CommanderHookOptions): {
47
+ client: Client;
48
+ program: Command;
49
+ };
50
+
51
+ export { type CommanderHookOptions, instrument, instrumentCommander };
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ import { CommanderError } from 'commander';
2
+ import { ErrorCategory, createClient } from '@getargvio/argvio';
3
+
4
+ // src/index.ts
5
+ function changedOptionNames(cmd) {
6
+ const names = [];
7
+ for (const option of cmd.options) {
8
+ const key = option.attributeName();
9
+ if (cmd.getOptionValueSource(key) === "cli") {
10
+ names.push(option.long?.replace(/^--/, "") ?? option.short?.replace(/^-/, "") ?? key);
11
+ }
12
+ }
13
+ return names;
14
+ }
15
+ function commandPath(cmd) {
16
+ const names = [];
17
+ for (let current = cmd; current; current = current.parent) {
18
+ names.unshift(current.name());
19
+ }
20
+ return names.join(" ");
21
+ }
22
+ function isControlFlowExit(err) {
23
+ return err instanceof CommanderError && err.exitCode === 0;
24
+ }
25
+ function instrumentCommander(program, client, options = {}) {
26
+ const classify = options.classify ?? (() => ErrorCategory.Unknown);
27
+ const captureHelp = options.captureHelp ?? true;
28
+ const captureInvocation = options.captureInvocation ?? true;
29
+ program.exitOverride();
30
+ let lastCommandPath = program.name();
31
+ if (captureInvocation) {
32
+ program.hook("preAction", (_thisCommand, actionCommand) => {
33
+ lastCommandPath = commandPath(actionCommand);
34
+ client.recordCommandInvocation(lastCommandPath, changedOptionNames(actionCommand));
35
+ });
36
+ }
37
+ if (captureHelp) {
38
+ const originalOutputHelp = program.outputHelp.bind(program);
39
+ program.outputHelp = ((...args) => {
40
+ client.recordHelpFlagUsage(lastCommandPath);
41
+ return originalOutputHelp(...args);
42
+ });
43
+ }
44
+ const originalParseAsync = program.parseAsync.bind(program);
45
+ program.parseAsync = (async (...args) => {
46
+ const start = Date.now();
47
+ try {
48
+ const result = await originalParseAsync(...args);
49
+ client.recordExitCode(lastCommandPath, 0);
50
+ client.recordLatency(lastCommandPath, Date.now() - start);
51
+ return result;
52
+ } 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);
58
+ const error = err instanceof Error ? err : new Error(String(err));
59
+ client.recordError(lastCommandPath, classify(error));
60
+ }
61
+ throw err;
62
+ }
63
+ });
64
+ return program;
65
+ }
66
+ function instrument(program, apiKey, cliVersion, clientOptions = {}, hookOptions = {}) {
67
+ const client = createClient(apiKey, program.name(), cliVersion, clientOptions);
68
+ instrumentCommander(program, client, hookOptions);
69
+ return { client, program };
70
+ }
71
+
72
+ export { instrument, instrumentCommander };
73
+ //# sourceMappingURL=index.js.map
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
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 } 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"]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@getargvio/commander",
3
+ "description": "Auto-instruments a commander command tree with @getargvio/argvio CLI usage telemetry",
4
+ "version": "0.1.0",
5
+ "author": "getargvio",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/getargvio/argvio-node.git",
11
+ "directory": "packages/commander"
12
+ },
13
+ "homepage": "https://github.com/getargvio/argvio-node/tree/main/packages/commander",
14
+ "bugs": {
15
+ "url": "https://github.com/getargvio/argvio-node/issues"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "keywords": [
21
+ "commander",
22
+ "cli",
23
+ "telemetry",
24
+ "opentelemetry",
25
+ "analytics"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "require": "./dist/index.cjs"
32
+ }
33
+ },
34
+ "main": "./dist/index.cjs",
35
+ "module": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "files": [
38
+ "dist/",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "engines": {
43
+ "node": ">=22.0.0"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "vitest run",
49
+ "prepublishOnly": "npm run build"
50
+ },
51
+ "peerDependencies": {
52
+ "commander": ">=12.0.0"
53
+ },
54
+ "dependencies": {
55
+ "@getargvio/argvio": "^0.1.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^24",
59
+ "commander": "^15",
60
+ "typescript": "^5",
61
+ "vitest": "^4.1.11"
62
+ }
63
+ }