GitNexus/gitnexus/src/cli/cli-message.ts
Gergő Magyar 8a9b13fc3b
feat(cli): add .gitnexusrc config and --default-branch for analyze (#243) (#1996)
* feat(cli): add .gitnexusrc config and --default-branch for analyze (#243)

Let a repo preconfigure recurring `gitnexus analyze` options via a
project-local `.gitnexusrc` (JSON) plus a new `--default-branch` flag, so
projects on `develop`/`master` no longer get the generated regression
example rewritten to `base_ref: "main"` on every analyze run.

- New `cli/analyze-config.ts`: locate/parse/validate `.gitnexusrc` (flat +
  nested `analyze` form, alias mapping, fail-closed on unknown keys / bad
  types / hidden chars), merge with CLI (CLI overrides config), and resolve
  the default branch (CLI > config defaultBranch/branch > auto-detected
  origin/HEAD > "main").
- `getDefaultBranch()` in storage/git.ts (best-effort, local-only, no network).
- Thread `defaultBranch` through analyze -> run-analyze -> ai-context so the
  generated regression-compare example uses the configured branch,
  JSON-escaped; the --skills re-generation path uses the same branch.
- `skipContextFiles`/`skipAiContext` alias `skipAgentsMd` (block only, does
  not imply skipSkills); `indexOnly` stays the stronger "skip all injection".
- README + CLI help; unit tests for the config module and end-to-end wiring
  tests that fail if config is parsed but not threaded into analyze/context.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): harden .gitnexusrc against Markdown injection and stale base_ref (#243)

Addresses the tri-review findings on PR #1996.

- P1 (Markdown injection into generated AGENTS.md/CLAUDE.md): reject the
  backtick in validateBranchName (covers --default-branch, .gitnexusrc, and the
  origin/HEAD auto-detect via sanitizeDetectedBranch) and strip it at the
  ai-context sink (markdownSafeBranch); reject Markdown-significant chars
  (` * [ ] < >) in the config `name` (it lands in generated bold/code-spans),
  while still allowing `_ . - /`. Corrected the false "can't break the code
  span" comment.
- P2 (configured defaultBranch silently no-ops on an up-to-date repo): on the
  alreadyUpToDate fast path, surgically refresh only the `base_ref:` line in
  AGENTS.md/CLAUDE.md (refreshBaseRefLine), preserving the rest of the block
  incl. --skills community rows; no-op when unchanged.
- P3: gate the .gitnexusrc key lookup with Object.hasOwn so inherited keys
  (__proto__, constructor, …) hit the actionable "Unknown key" error.
- Cleanups: strip a leading UTF-8 BOM before JSON.parse; give --default-branch
  CLI validation its own `default-branch-invalid` recovery hint; drop the dead
  `options.defaultBranch` write and the now-redundant `options?.` chaining.
- Tests: backtick rejection + even-backtick generated output, 255-char branch
  bound, config `name` Markdown rejection, __proto__ → Unknown key, BOM,
  mergeAnalyzeOptions omits defaultBranch, willGenerateContext suppression, and
  the fast-path base_ref refresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 06:48:55 +01:00

129 lines
4.4 KiB
TypeScript

/**
* CLI message helpers — for user-facing banners, error guidance, and
* recovery hints emitted by `gitnexus` subcommands.
*
* These functions write **plain text** directly to `process.stderr` AND
* tee a structured pino record through the singleton `logger`. Plain text
* preserves the human-readable contract for users running `gitnexus`
* interactively, redirecting to a file, or piping to `cat`/`grep`. The
* structured tee keeps log aggregators happy.
*
* **Use these for:**
* - User-facing banners ("Server listening on http://...:N")
* - Validation errors ("--worker-timeout must be at least 1 second")
* - Recovery hints ("Suggestions: 1. Clear the npm cache, 2. ...")
* - One-line user notices ("No indexed repositories found.")
*
* **Do NOT use these for:**
* - Internal diagnostics (worker progress, retry counts, telemetry)
* — use `logger.info`/`warn`/`error` directly. Internal logs only
* need structured fields, not double-output to stderr.
* - High-volume hot paths — every `cliMessage` call writes twice (raw
* + structured). Acceptable for user-facing messages, wasteful for
* ingestion pipeline events.
*
* Design note: stderr is the right channel even for non-error messages
* because GitNexus CLI tools (`query`, `cypher`, `impact`) emit JSON
* data on stdout for piping (`gitnexus query | jq`). User banners on
* stdout would corrupt that pipeline.
*/
import { logger } from '../core/logger.js';
import { t, type CliMessageKey, type CliMessageVars } from './i18n/index.js';
/**
* String-literal union of all `recoveryHint` tags emitted by the CLI.
*
* Centralized so a new recovery branch added in `analyze.ts` cannot land
* without updating this union — TypeScript will reject the unknown literal
* passed via `cliError({ recoveryHint: '...' })`. To add a new hint:
* 1. Add the tag string to this union.
* 2. Pass it as the `recoveryHint` field at the relevant `cliError`
* call site.
*
* Consumers can import this type to narrow log-record `recoveryHint`
* fields without restating the literal list.
*/
export type RecoveryHint =
| 'wal-corruption'
| 'wal-checkpoint-threshold'
| 'heap-oom-respawn'
| 'native-worker-abort'
| 'hf-endpoint-unreachable'
| 'local-embedding-unsupported'
| 'large-repo'
| 'npm-resolution'
| 'module-not-found'
| 'gitnexusrc-invalid'
| 'default-branch-invalid';
/**
* Common shape for the optional structured-field bag passed to
* `cliError`/`cliWarn`/`cliInfo`. Typed so the `recoveryHint` slot is
* checked against the {@link RecoveryHint} union.
*/
export interface CliMessageFields extends Record<string, unknown> {
recoveryHint?: RecoveryHint;
}
function writeStderr(msg: string): void {
// Direct write — bypassing `console.*` so it cannot be intercepted by
// progress-bar redirection (see `cli/analyze.ts:barLog`) or other
// routing. The structured tee below still goes through the logger so
// log aggregation works either way.
process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n');
}
/**
* User-facing informational message. Use for banners, listening URLs,
* and any message the user expects to read in plain text.
*/
export function cliInfo(msg: string, fields?: CliMessageFields): void {
writeStderr(msg);
logger.info(fields ?? {}, msg);
}
/**
* Key-based informational message. Keeps the legacy string API intact while
* allowing commands to opt into localized user-facing stderr output.
*/
export function cliInfoKey(
key: CliMessageKey,
vars?: CliMessageVars,
fields?: Record<string, unknown>,
): void {
cliInfo(t(key, vars), fields);
}
/**
* User-facing warning. Operator-actionable but non-fatal — `cliWarn`
* indicates the command can still proceed in some form.
*/
export function cliWarn(msg: string, fields?: CliMessageFields): void {
writeStderr(msg);
logger.warn(fields ?? {}, msg);
}
export function cliWarnKey(
key: CliMessageKey,
vars?: CliMessageVars,
fields?: Record<string, unknown>,
): void {
cliWarn(t(key, vars), fields);
}
/**
* User-facing error. Indicates the command cannot proceed; usually
* paired with a non-zero exit code at the call site.
*/
export function cliError(msg: string, fields?: CliMessageFields): void {
writeStderr(msg);
logger.error(fields ?? {}, msg);
}
export function cliErrorKey(
key: CliMessageKey,
vars?: CliMessageVars,
fields?: Record<string, unknown>,
): void {
cliError(t(key, vars), fields);
}