mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
2e19c97de0
Found while differentially regression-testing the 0.22.1 release against a bundle built from `v0.22.0`. ## The problem `argent telemetry status --help` exits 2 with `Error: Unknown flag: --help`. So do `-h`, and the same after `enable` / `disable`. On 0.22.0 all of them exited 0. ``` $ argent telemetry status --help 0.22.0 telemetry: / state: enabled / device id: not created (rc=0) 0.22.1 Error: Unknown flag: --help (rc=2) ``` `telemetry` is the only subcommand that behaves this way. Checked against every sibling on both versions — `server status|logs|start`, `config list|get|set`, `flow list|run`, `secrets`, `flags`, `enable`, `link`, `unlink` all return 0 on both. And the top-level help tells the user to run exactly this: "Run `argent <command> --help` for command-specific help." ## Cause `TELEMETRY_OPTIONS` declares only `scope`, and `parseCommandArgs(args.slice(1), …)` reads everything after the subcommand — so `--help` arrives as an unknown flag. The `case "--help"` arm in the switch only fires when help is `args[0]`, which is why bare `argent telemetry --help` still works. Introduced when `telemetry` was converted to the declarative parser (#952, extended by #953). ## The fix Hoist the help check above the parser, matching what `config.ts:220` and `flags.ts:92` already do; `server`, `link` and `lens` instead declare `help` in their option spec. Telemetry did neither. Ordering of `telemetryInit` / `telemetryShutdown` around the help path is unchanged, so a help invocation behaves exactly as `argent telemetry --help` did before. ## Tests Six cases added to `telemetry-command.test.ts` covering `--help` / `-h`, bare and after each subcommand, asserting usage is printed and no config file is written. Mutation-checked: with the source change reverted, 4 of the 6 fail (the 2 bare-help cases pass either way, since those already worked) — so the test pins the actual defect rather than the behaviour around it. `@argent/cli`: 498 passed, 3 skipped. `tsc --build` clean, prettier clean. ## Docs No docs update needed — this restores previously documented behaviour rather than changing a documented surface. Co-authored-by: Ignacy Łątka <f.kaminski2000@gmail.com>
147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
import pc from "picocolors";
|
|
import type { FlagScope } from "@argent/configuration-core";
|
|
import { parseCommandArgs, UsageError, type OptionSpecs } from "./command-args.js";
|
|
import {
|
|
init as telemetryInit,
|
|
isEnabled as telemetryIsEnabled,
|
|
markDisabled,
|
|
markEnabled,
|
|
shutdown as telemetryShutdown,
|
|
status as telemetryStatus,
|
|
} from "@argent/telemetry";
|
|
|
|
const SCOPES: readonly FlagScope[] = ["global", "project"];
|
|
|
|
const TELEMETRY_OPTIONS = {
|
|
scope: { kind: "value", choices: SCOPES },
|
|
} as const satisfies OptionSpecs;
|
|
|
|
// Telemetry is opt-out: on by default.
|
|
export async function telemetry(args: string[]): Promise<void> {
|
|
const sub = args[0];
|
|
let scope: FlagScope = "global";
|
|
// Help is accepted in any position, including after a subcommand; the option
|
|
// parser below knows only `--scope` and would reject it as an unknown flag.
|
|
const wantsHelp = args.includes("--help") || args.includes("-h");
|
|
if (!wantsHelp) {
|
|
try {
|
|
const { positionals, options } = parseCommandArgs(args.slice(1), TELEMETRY_OPTIONS);
|
|
if (positionals.length > 0) {
|
|
throw new UsageError(`Unexpected argument "${positionals[0]}".`);
|
|
}
|
|
if (options.scope !== undefined) scope = options.scope as FlagScope;
|
|
} catch (err) {
|
|
if (!(err instanceof UsageError)) throw err;
|
|
console.error(`Error: ${err.message}`);
|
|
printUsage();
|
|
process.exit(2);
|
|
}
|
|
}
|
|
telemetryInit("cli");
|
|
|
|
if (wantsHelp) {
|
|
printUsage();
|
|
await telemetryShutdown();
|
|
return;
|
|
}
|
|
|
|
switch (sub) {
|
|
case undefined:
|
|
printUsage();
|
|
await telemetryShutdown();
|
|
return;
|
|
case "status":
|
|
printStatus();
|
|
await telemetryShutdown();
|
|
return;
|
|
case "enable":
|
|
await cmdEnable(scope);
|
|
return;
|
|
case "disable":
|
|
await cmdDisable(scope);
|
|
return;
|
|
default:
|
|
console.error(`Unknown subcommand: telemetry ${sub}`);
|
|
await telemetryShutdown();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function printUsage(): void {
|
|
console.log(`Usage:
|
|
argent telemetry status Show telemetry state and device id
|
|
argent telemetry enable [--scope global|project] Enable telemetry
|
|
argent telemetry disable [--scope global|project] Disable telemetry
|
|
|
|
The default scope is global (~/.argent/config.json). \`--scope project\` writes
|
|
<project-root>/.argent/config.json instead — commit it and telemetry stays off
|
|
for everyone who clones the repository. \`false\` in either scope wins.
|
|
`);
|
|
}
|
|
|
|
function scopeLabel(scope: FlagScope): string {
|
|
return scope === "project" ? "project scope" : "global scope";
|
|
}
|
|
|
|
function printStatus(): void {
|
|
const s = telemetryStatus();
|
|
|
|
const idLabel = s.anonIdPrefix
|
|
? `${s.anonIdPrefix}...`
|
|
: s.hasAnonIdOnDisk
|
|
? "present"
|
|
: "not created";
|
|
|
|
console.log("telemetry:");
|
|
console.log(` state: ${s.enabled ? "enabled" : "disabled"}`);
|
|
console.log(` source: ${describeSource(s.source)}`);
|
|
console.log(` device id: ${idLabel}`);
|
|
}
|
|
|
|
function describeSource(source: ReturnType<typeof telemetryStatus>["source"]): string {
|
|
switch (source.source) {
|
|
case "env_do_not_track":
|
|
case "env_argent_telemetry":
|
|
return `environment (${source.detail ?? source.source})`;
|
|
case "config_file":
|
|
return source.detail ?? "config.json";
|
|
case "session_override":
|
|
return "this session";
|
|
case "default":
|
|
return "default (no opt-out set)";
|
|
}
|
|
}
|
|
|
|
async function cmdEnable(scope: FlagScope): Promise<void> {
|
|
const wasEnabled = telemetryIsEnabled();
|
|
markEnabled(scope);
|
|
const nowEnabled = telemetryIsEnabled();
|
|
if (!nowEnabled) {
|
|
// The other scope (or an env override) still says no — restrictive merge.
|
|
console.log(
|
|
pc.yellow(
|
|
`Telemetry set to enabled at ${scopeLabel(scope)}, but it stays disabled: ` +
|
|
`${describeSource(telemetryStatus().source)} still opts out.`
|
|
)
|
|
);
|
|
} else if (wasEnabled) {
|
|
console.log(pc.dim(`Telemetry was already enabled (written at ${scopeLabel(scope)}).`));
|
|
} else {
|
|
console.log(pc.green(`Telemetry enabled (${scopeLabel(scope)}).`));
|
|
}
|
|
await telemetryShutdown();
|
|
}
|
|
|
|
async function cmdDisable(scope: FlagScope): Promise<void> {
|
|
const wasEnabled = telemetryIsEnabled();
|
|
// Still write when already off: a global opt-out and a committed project
|
|
// opt-out are different guarantees, and the user asked for this one.
|
|
await markDisabled(scope);
|
|
if (!wasEnabled) {
|
|
console.log(pc.dim(`Telemetry was already disabled (written at ${scopeLabel(scope)}).`));
|
|
} else {
|
|
console.log(pc.red(`Telemetry disabled (${scopeLabel(scope)}).`));
|
|
}
|
|
await telemetryShutdown();
|
|
}
|