tend-cli 0.3.0 → 0.4.1

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/README.md CHANGED
@@ -25,8 +25,8 @@ Run the latest published package directly from the registry:
25
25
 
26
26
  ```bash
27
27
  npx tend-cli@latest # changed files vs HEAD (the default)
28
- npx tend-cli@latest run src/app lib/ # only findings under these paths
29
- npx tend-cli@latest run --all # the entire backlog, repo-wide
28
+ npx tend-cli@latest src/scanners # only findings under this path
29
+ npx tend-cli@latest --all # the entire backlog, repo-wide
30
30
  ```
31
31
 
32
32
  Or install it and use the product command:
@@ -34,8 +34,9 @@ Or install it and use the product command:
34
34
  ```bash
35
35
  npm install -g tend-cli
36
36
  tend # changed files vs HEAD (the default)
37
- tend run src/app lib/
38
- tend run --all
37
+ tend src/scanners
38
+ tend --all
39
+ tend run src/scanners # explicit form is also available
39
40
  ```
40
41
 
41
42
  Requires **Node ≥ 20**, a git repo, and the [Claude Code](https://www.anthropic.com/claude-code)
@@ -47,7 +48,7 @@ do not need to match: `tend` is the command users run, and `tend-cli` is the reg
47
48
  When developing inside this repo, use the local script instead of `npx tend-cli`:
48
49
 
49
50
  ```bash
50
- pnpm cli run src/scanners
51
+ pnpm cli -- src/scanners
51
52
  ```
52
53
 
53
54
  ## What it does
package/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { ClaudeSession, EFFORT_LEVELS, EventBus, ReportBuilder, ReportSchema, Snapshot, addUsage, applyCliOverrides, assertGitRepo, buildProgram, changedVsHead, createGit, detectPackageManager, filesUnder, filterToChanged, formatClock, loadConfig, makeTheme, normalize, orchestrate, planWork, reasonLabel, renderSummary, resolveRetryTarget, retryCommand, runScanner, scannerStatus, showCommand, zeroUsage } from "./config-DMOjMbMD.js";
2
+ import { ClaudeSession, EFFORT_LEVELS, EventBus, ReportBuilder, ReportSchema, Snapshot, addUsage, applyCliOverrides, assertGitRepo, buildProgram, changedVsHead, createGit, detectPackageManager, filesUnder, filterToChanged, formatClock, loadConfig, makeTheme, normalize, orchestrate, planWork, reasonLabel, renderSummary, resolveRetryTarget, retryCommand, runScanner, scannerStatus, showCommand, zeroUsage } from "./config-pmGLB0x1.js";
3
3
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { execa } from "execa";
@@ -1763,8 +1763,7 @@ const program = buildProgram({
1763
1763
  },
1764
1764
  retry: (id) => runRetry(id)
1765
1765
  });
1766
- const argv = process.argv.slice(2).length === 0 ? [...process.argv, "run"] : process.argv;
1767
- program.parseAsync(argv).catch((e) => {
1766
+ program.parseAsync(process.argv).catch((e) => {
1768
1767
  if (e instanceof Error && e.name === "CommanderError") {
1769
1768
  process.exitCode = e.exitCode ?? 1;
1770
1769
  return;
@@ -13,12 +13,65 @@ import gradient from "gradient-string";
13
13
  import { cosmiconfig } from "cosmiconfig";
14
14
 
15
15
  //#region src/cli.ts
16
+ const COMMAND_NAMES = new Set([
17
+ "diff",
18
+ "help",
19
+ "retry",
20
+ "run",
21
+ "show",
22
+ "undo"
23
+ ]);
24
+ const RUN_OPTION_NAMES = new Set([
25
+ "--all",
26
+ "--effort",
27
+ "--include-tests",
28
+ "--max-loops",
29
+ "--max-sessions",
30
+ "--model",
31
+ "--no-color",
32
+ "--plain",
33
+ "--verbose"
34
+ ]);
35
+ function addRunOptions(command) {
36
+ return command.option("--all", "fix the entire backlog, not just changed files").option("--max-loops <n>", "cap on fix loops", (v) => parseInt(v, 10)).option("--max-sessions <n>", "concurrent AI sessions", (v) => parseInt(v, 10)).option("--model <model>", "model for fixes: sonnet (default), opus, haiku, or a full model id").option("--effort <level>", "reasoning effort for fixes: low | medium | high | xhigh | max").option("--include-tests", "also fix findings in test files (excluded by default)").option("--plain", "plain one-line-per-event output for pipes/CI (no color, no spinners)").option("--no-color", "disable color output").option("--verbose", "show the full per-tool / per-finding breakdown in the summary");
37
+ }
38
+ function looksLikePath(value) {
39
+ return value.includes("/") || value.includes("\\") || value.startsWith(".") || existsSync(value);
40
+ }
41
+ function isRunOption(value) {
42
+ const name = value.split("=")[0] ?? value;
43
+ return RUN_OPTION_NAMES.has(name);
44
+ }
45
+ function shouldInsertRun(args) {
46
+ const first = args[0];
47
+ if (!first) return true;
48
+ if (first === "--help" || first === "-h" || COMMAND_NAMES.has(first)) return false;
49
+ return isRunOption(first) || looksLikePath(first);
50
+ }
51
+ function withDefaultRun(argv, from) {
52
+ const prefixLength = from === "user" ? 0 : 2;
53
+ const prefix = argv.slice(0, prefixLength);
54
+ const args = argv.slice(prefixLength);
55
+ return shouldInsertRun(args) ? [
56
+ ...prefix,
57
+ "run",
58
+ ...args
59
+ ] : [...argv];
60
+ }
61
+ function enableDefaultRun(program) {
62
+ const parse = program.parse.bind(program);
63
+ const parseAsync = program.parseAsync.bind(program);
64
+ program.parse = (argv, options) => parse(withDefaultRun(argv ?? process.argv, options?.from), options);
65
+ program.parseAsync = (argv, options) => parseAsync(withDefaultRun(argv ?? process.argv, options?.from), options);
66
+ }
16
67
  /** Build the commander program wiring each subcommand to a handler. */
17
68
  function buildProgram(handlers) {
18
69
  const program = new Command();
19
70
  program.name("tend").description("Audit a JS/TS repo and fix findings with AI in a safe loop.");
20
71
  program.exitOverride();
21
- program.command("run").description("snapshot → audit → fix loop → report (changed files)").argument("[paths...]", "fix only findings under these files/dirs (committed or not)").option("--all", "fix the entire backlog, not just changed files").option("--max-loops <n>", "cap on fix loops", (v) => parseInt(v, 10)).option("--max-sessions <n>", "concurrent AI sessions", (v) => parseInt(v, 10)).option("--model <model>", "model for fixes: sonnet (default), opus, haiku, or a full model id").option("--effort <level>", "reasoning effort for fixes: low | medium | high | xhigh | max").option("--include-tests", "also fix findings in test files (excluded by default)").option("--plain", "plain one-line-per-event output for pipes/CI (no color, no spinners)").option("--no-color", "disable color output").option("--verbose", "show the full per-tool / per-finding breakdown in the summary").action((paths, opts) => handlers.run({
72
+ program.addHelpCommand(true);
73
+ const run = program.command("run").description("snapshot → audit → fix loop → report (changed files)").argument("[paths...]", "fix only findings under these files/dirs (committed or not)");
74
+ addRunOptions(run).action((paths, opts) => handlers.run({
22
75
  ...opts,
23
76
  paths
24
77
  }));
@@ -26,6 +79,7 @@ function buildProgram(handlers) {
26
79
  program.command("undo").description("restore the pre-run snapshot").action(() => handlers.undo());
27
80
  program.command("show <id>").description("full detail on one finding").action((id) => handlers.show(id));
28
81
  program.command("retry <id>").description("re-attempt a stubborn finding with a larger budget").action((id) => handlers.retry(id));
82
+ enableDefaultRun(program);
29
83
  return program;
30
84
  }
31
85
 
@@ -328,9 +382,11 @@ function revertFile(snapshot, file) {
328
382
  //#endregion
329
383
  //#region src/git/client.ts
330
384
  const UNSAFE_GIT_ENV_KEYS = [
385
+ "EDITOR",
386
+ "VISUAL",
331
387
  "GIT_EDITOR",
332
- "GIT_PAGER",
333
388
  "GIT_SEQUENCE_EDITOR",
389
+ "GIT_PAGER",
334
390
  "PAGER"
335
391
  ];
336
392
  function gitEnv(extra = {}) {
@@ -341,8 +397,8 @@ function gitEnv(extra = {}) {
341
397
  for (const key of UNSAFE_GIT_ENV_KEYS) delete env[key];
342
398
  return env;
343
399
  }
344
- function createGit(root) {
345
- return simpleGit(root).env(gitEnv());
400
+ function createGit(root, extraEnv = {}) {
401
+ return simpleGit(root).env(gitEnv(extraEnv));
346
402
  }
347
403
 
348
404
  //#endregion
@@ -360,7 +416,7 @@ let indexCounter = 0;
360
416
  async function writeWorkingTree(root) {
361
417
  const idxPath = join(tmpdir(), `tend-index-${process.pid}-${indexCounter++}`);
362
418
  try {
363
- const g = createGit(root).env(gitEnv({ GIT_INDEX_FILE: idxPath }));
419
+ const g = createGit(root, { GIT_INDEX_FILE: idxPath });
364
420
  await g.raw(["add", "-A"]);
365
421
  return (await g.raw(["write-tree"])).trim();
366
422
  } finally {
@@ -404,7 +460,8 @@ var Snapshot = class Snapshot {
404
460
  this.root = root;
405
461
  this.sha = sha;
406
462
  }
407
- static async capture(git, cwd) {
463
+ static async capture(_git, cwd) {
464
+ const git = createGit(cwd);
408
465
  const root = (await git.revparse(["--show-toplevel"])).trim();
409
466
  const gitDir = (await git.revparse(["--absolute-git-dir"])).trim();
410
467
  ensureTendIgnored(gitDir);
package/dist/index.d.ts CHANGED
@@ -404,7 +404,7 @@ declare class Snapshot {
404
404
  private readonly root;
405
405
  private readonly sha;
406
406
  private constructor();
407
- static capture(git: SimpleGit, cwd: string): Promise<Snapshot>;
407
+ static capture(_git: SimpleGit, cwd: string): Promise<Snapshot>;
408
408
  /** Serialize to a tiny object for `.tend/snapshot.json` (powers `undo` across invocations). */
409
409
  toJSON(): {
410
410
  cwd: string;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ClaudeSession, ConfigSchema, EventBus, FindingSchema, FindingStore, ReportBuilder, ReportSchema, Snapshot, addUsage, applyCliOverrides, assertGitRepo, buildProgram, changedFiles, changedVsHead, detectPackageManager, dispatch, filterToChanged, fingerprint, groupRemaining, isAvailable, loadConfig, normalize, orchestrate, planWork, renderSummary, retryCommand, revertFile, route, runScanner, scopeFindings, showCommand, trackForTool, zeroUsage } from "./config-DMOjMbMD.js";
1
+ import { ClaudeSession, ConfigSchema, EventBus, FindingSchema, FindingStore, ReportBuilder, ReportSchema, Snapshot, addUsage, applyCliOverrides, assertGitRepo, buildProgram, changedFiles, changedVsHead, detectPackageManager, dispatch, filterToChanged, fingerprint, groupRemaining, isAvailable, loadConfig, normalize, orchestrate, planWork, renderSummary, retryCommand, revertFile, route, runScanner, scopeFindings, showCommand, trackForTool, zeroUsage } from "./config-pmGLB0x1.js";
2
2
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tend-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Audit a JS/TS repo with established scanners, then fix the findings with parallel AI sessions in a safe scan-fix-rescan loop.",
5
5
  "keywords": [
6
6
  "lint",
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "scripts": {
41
41
  "build": "tsdown",
42
- "cli": "pnpm build && node dist/bin.js",
42
+ "cli": "node scripts/cli-dev.mjs",
43
43
  "prepublishOnly": "tsdown",
44
44
  "test": "vitest run",
45
45
  "test:watch": "vitest",