@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 +21 -0
- package/README.md +39 -0
- package/dist/index.cjs +77 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
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":"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","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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|