@chimix/agentscan 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/cli.js +134 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -164,6 +164,7 @@ Flags for `check`:
164
164
  | `--json` | JSON report (alias for `--output json`) |
165
165
  | `--output <format>` | `human` (default) · `json` · `prompt` |
166
166
  | `--copy` | Also copy the report to the system clipboard |
167
+ | `--no-color` | Never colour, even on a terminal (`NO_COLOR=1` does the same) |
167
168
  | `--quiet` | Summary line only |
168
169
  | `--verbose` | Show KEEP + info-severity findings |
169
170
  | `--fail-on <level>` | `never` (default) · `warning` · `error` |
@@ -176,7 +177,12 @@ Flags for `check`:
176
177
  ## Sample output
177
178
 
178
179
  ```text
179
- agentscan v0.3.0 — touchagency
180
+ ┌─────┐ 40/100 broken · touchagency
181
+ │ ✕ ✕ │ ██████████░░░░░░░░░░░░░░░
182
+ │ ⌒ │ 6 warn · 4 info hidden (--verbose)
183
+ └─────┘
184
+
185
+ agentscan v0.4.0 — touchagency
180
186
 
181
187
  Scanned: 46 deps · 54 skills · 1 mcp · 2 agents · packageManager=bun
182
188
 
@@ -190,6 +196,11 @@ WARN rule:hook.missing-script ×6
190
196
  Summary: 6 warn · 4 info hidden (--verbose) · score 40/100 broken
191
197
  ```
192
198
 
199
+ On a terminal the header is coloured green, yellow or red and the face tracks
200
+ the score. Redirect or pipe it and you get exactly the text above, with no
201
+ escape sequences — the header box is dropped too, so `--json`, `--output
202
+ prompt`, CI logs and `--copy` are unchanged by any of this.
203
+
193
204
  The `Stack:` line is orientation only — which project, how big. The summary lists
194
205
  only actions that actually occurred; a clean project prints `Summary: no findings`.
195
206
 
package/dist/cli.js CHANGED
@@ -12549,6 +12549,30 @@ function scoreLabel(points) {
12549
12549
  }
12550
12550
  return "broken";
12551
12551
  }
12552
+ function scoreTone(points) {
12553
+ if (points >= 90) {
12554
+ return "green";
12555
+ }
12556
+ if (points >= 50) {
12557
+ return "yellow";
12558
+ }
12559
+ return "red";
12560
+ }
12561
+ function scoreFace(points) {
12562
+ if (points === 100) {
12563
+ return ["◠ ◠", " ▽ "];
12564
+ }
12565
+ if (points >= 90) {
12566
+ return ["• •", " ‿ "];
12567
+ }
12568
+ if (points >= 70) {
12569
+ return ["• •", " ─ "];
12570
+ }
12571
+ if (points >= 50) {
12572
+ return ["◡ ◡", " ⌒ "];
12573
+ }
12574
+ return ["✕ ✕", " ⌒ "];
12575
+ }
12552
12576
 
12553
12577
  // src/report/exit-code.ts
12554
12578
  function exitCode(findings, failOn, failUnder) {
@@ -12673,6 +12697,50 @@ function headingFor(severity) {
12673
12697
 
12674
12698
  // src/report/text.ts
12675
12699
  import { basename as basename4 } from "node:path";
12700
+
12701
+ // src/report/ansi.ts
12702
+ var CODES = {
12703
+ green: "\x1B[32m",
12704
+ yellow: "\x1B[33m",
12705
+ red: "\x1B[31m",
12706
+ dim: "\x1B[2m",
12707
+ bold: "\x1B[1m",
12708
+ reset: "\x1B[0m"
12709
+ };
12710
+ var PLAIN = {
12711
+ tone: (_tone, text) => text,
12712
+ strong: (_tone, text) => text,
12713
+ dim: (text) => text,
12714
+ bold: (text) => text,
12715
+ enabled: false
12716
+ };
12717
+ function paint(enabled) {
12718
+ if (!enabled) {
12719
+ return PLAIN;
12720
+ }
12721
+ return {
12722
+ tone: (tone, text) => `${CODES[tone]}${text}${CODES.reset}`,
12723
+ strong: (tone, text) => `${CODES.bold}${CODES[tone]}${text}${CODES.reset}`,
12724
+ dim: (text) => `${CODES.dim}${text}${CODES.reset}`,
12725
+ bold: (text) => `${CODES.bold}${text}${CODES.reset}`,
12726
+ enabled: true
12727
+ };
12728
+ }
12729
+ function shouldColour(args) {
12730
+ if (args.noColorFlag === true) {
12731
+ return false;
12732
+ }
12733
+ const { NO_COLOR, FORCE_COLOR } = args.env;
12734
+ if (NO_COLOR !== undefined && NO_COLOR !== "") {
12735
+ return false;
12736
+ }
12737
+ if (FORCE_COLOR !== undefined && FORCE_COLOR !== "" && FORCE_COLOR !== "0") {
12738
+ return true;
12739
+ }
12740
+ return args.isTTY;
12741
+ }
12742
+
12743
+ // src/report/text.ts
12676
12744
  var ACTION_LABEL = {
12677
12745
  keep: "KEEP",
12678
12746
  delete: "DELETE",
@@ -12686,24 +12754,28 @@ function actionColumn(action) {
12686
12754
  }
12687
12755
  function renderText(args) {
12688
12756
  const { version, facts, verbose, quiet } = args;
12757
+ const ink = paint(args.colour === true);
12689
12758
  const sorted = sortFindings(args.findings);
12690
12759
  const visible = verbose ? sorted : sorted.filter((f) => f.action !== "keep" && f.severity !== "info");
12691
- const summary = formatSummary(sorted, verbose);
12760
+ const summary = formatSummary(sorted, verbose, ink);
12692
12761
  if (quiet) {
12693
12762
  return `${summary}
12694
12763
  `;
12695
12764
  }
12696
12765
  const lines = [];
12697
12766
  const name = projectLabel(facts.root);
12698
- lines.push(`agentscan v${version} — ${safe(name)}`);
12767
+ if (ink.enabled) {
12768
+ lines.push(...formatHeaderBox(sorted, verbose, safe(name), ink));
12769
+ }
12770
+ lines.push(ink.dim(`agentscan v${version} — ${safe(name)}`));
12699
12771
  if (args.resolvedFrom !== undefined) {
12700
12772
  lines.push(` no package.json in ${safe(args.resolvedFrom)} — scanned ${safe(facts.root)}`);
12701
12773
  }
12702
12774
  lines.push("");
12703
- lines.push(`Scanned: ${formatStack(facts)}`);
12775
+ lines.push(ink.dim(`Scanned: ${formatStack(facts)}`));
12704
12776
  lines.push("");
12705
12777
  for (const group of groupByRule(visible)) {
12706
- lines.push(...formatGroup(group, facts.root));
12778
+ lines.push(...formatGroup(group, facts.root, ink));
12707
12779
  lines.push("");
12708
12780
  }
12709
12781
  lines.push(summary);
@@ -12711,6 +12783,22 @@ function renderText(args) {
12711
12783
  return lines.join(`
12712
12784
  `);
12713
12785
  }
12786
+ var BAR_CELLS = 25;
12787
+ function formatHeaderBox(findings, verbose, name, ink) {
12788
+ const points = score(findings);
12789
+ const tone = scoreTone(points);
12790
+ const [eyes, mouth] = scoreFace(points);
12791
+ const filled = Math.round(points / 100 * BAR_CELLS);
12792
+ const bar = ink.tone(tone, "█".repeat(filled)) + ink.dim("░".repeat(BAR_CELLS - filled));
12793
+ const verdict = `${ink.strong(tone, `${points}/100`)} ${ink.tone(tone, scoreLabel(points))}${ink.dim(` · ${name}`)}`;
12794
+ return [
12795
+ ` ${ink.tone(tone, "┌─────┐")} ${verdict}`,
12796
+ ` ${ink.tone(tone, `│ ${eyes} │`)} ${bar}`,
12797
+ ` ${ink.tone(tone, `│ ${mouth} │`)} ${ink.dim(countsLine(findings, verbose))}`,
12798
+ ` ${ink.tone(tone, "└─────┘")}`,
12799
+ ""
12800
+ ];
12801
+ }
12714
12802
  function projectLabel(root) {
12715
12803
  const base = basename4(root);
12716
12804
  return base.length > 0 ? base : root;
@@ -12742,16 +12830,25 @@ function groupByRule(findings) {
12742
12830
  function shortPath(value, root) {
12743
12831
  return value.split(`${root}/`).join("");
12744
12832
  }
12745
- function formatGroup(group, root) {
12833
+ var ACTION_TONE = {
12834
+ delete: "red",
12835
+ warn: "yellow",
12836
+ drift: "yellow",
12837
+ add: "green",
12838
+ refresh: "green"
12839
+ };
12840
+ function formatGroup(group, root, ink) {
12746
12841
  const first = group[0];
12747
12842
  const count = group.length;
12748
12843
  const out = [];
12749
- out.push(`${actionColumn(first.action)} rule:${safe(first.ruleId)}${count > 1 ? ` ×${count}` : ""}`);
12844
+ const tone = ACTION_TONE[first.action];
12845
+ const label = actionColumn(first.action);
12846
+ out.push(`${tone === undefined ? ink.dim(label) : ink.tone(tone, label)} ${ink.dim(`rule:${safe(first.ruleId)}`)}${count > 1 ? ink.bold(` ×${count}`) : ""}`);
12750
12847
  out.push(` ${safe(count === 1 ? first.message : ruleHeadline(first))}`);
12751
12848
  for (const f of group) {
12752
12849
  const where = f.evidence.filter((e) => e.kind !== "count" && e.kind !== "threshold").map((e) => shortPath(e.value, root));
12753
12850
  const location = where.length > 0 ? where.join(" · ") : f.subject;
12754
- out.push(` ${safe(shortPath(location, root))}`);
12851
+ out.push(ink.dim(` ${safe(shortPath(location, root))}`));
12755
12852
  }
12756
12853
  return out;
12757
12854
  }
@@ -12759,7 +12856,11 @@ function ruleHeadline(f) {
12759
12856
  const cut = f.message.indexOf(": ");
12760
12857
  return cut === -1 ? f.message : f.message.slice(0, cut);
12761
12858
  }
12762
- function formatSummary(findings, verbose) {
12859
+ function countsLine(findings, verbose) {
12860
+ const parts = actionParts(findings, verbose);
12861
+ return parts.length === 0 ? "no findings" : parts.join(" · ");
12862
+ }
12863
+ function actionParts(findings, verbose) {
12763
12864
  const counts = {
12764
12865
  keep: 0,
12765
12866
  delete: 0,
@@ -12781,8 +12882,12 @@ function formatSummary(findings, verbose) {
12781
12882
  if (!verbose && infoHidden > 0) {
12782
12883
  parts.push(`${infoHidden} info hidden (--verbose)`);
12783
12884
  }
12885
+ return parts;
12886
+ }
12887
+ function formatSummary(findings, verbose, ink) {
12888
+ const parts = actionParts(findings, verbose);
12784
12889
  const points = score(findings);
12785
- const scored = `score ${points}/100 ${scoreLabel(points)}`;
12890
+ const scored = `score ${ink.tone(scoreTone(points), `${points}/100 ${scoreLabel(points)}`)}`;
12786
12891
  if (parts.length === 0) {
12787
12892
  return `Summary: no findings · ${scored}`;
12788
12893
  }
@@ -12790,7 +12895,7 @@ function formatSummary(findings, verbose) {
12790
12895
  }
12791
12896
 
12792
12897
  // src/version.ts
12793
- var VERSION = "0.3.0";
12898
+ var VERSION = "0.4.0";
12794
12899
 
12795
12900
  // src/commands/check.ts
12796
12901
  async function runCheck(options) {
@@ -12802,23 +12907,30 @@ async function runCheck(options) {
12802
12907
  });
12803
12908
  const format = options.output ?? (options.json === true ? "json" : "human");
12804
12909
  let stdout;
12910
+ let plain;
12805
12911
  if (format === "json") {
12806
12912
  stdout = renderJson({ version: VERSION, root, facts, findings, resolvedFrom });
12807
12913
  } else if (format === "prompt") {
12808
12914
  stdout = renderPrompt({ version: VERSION, facts, findings });
12809
12915
  } else {
12810
- stdout = renderText({
12916
+ const text = (colour) => renderText({
12811
12917
  version: VERSION,
12812
12918
  facts,
12813
12919
  findings,
12814
12920
  resolvedFrom,
12815
12921
  verbose: options.verbose ?? false,
12816
- quiet: options.quiet ?? false
12922
+ quiet: options.quiet ?? false,
12923
+ colour
12817
12924
  });
12925
+ stdout = text(options.colour === true);
12926
+ if (options.colour === true) {
12927
+ plain = text(false);
12928
+ }
12818
12929
  }
12819
12930
  return {
12820
12931
  exitCode: exitCode(findings, config.failOn, options.failUnder),
12821
- stdout
12932
+ stdout,
12933
+ ...plain === undefined ? {} : { plain }
12822
12934
  };
12823
12935
  }
12824
12936
 
@@ -12987,6 +13099,7 @@ check options:
12987
13099
  --output <format> human (default) | json | prompt
12988
13100
  prompt: a paste-ready handoff for a fixing agent
12989
13101
  --copy Also copy the report to the system clipboard
13102
+ --no-color Never colour, even on a terminal (also: NO_COLOR=1)
12990
13103
  --quiet Summary only
12991
13104
  --verbose Show KEEP findings
12992
13105
  --fail-on <level> never | warning | error (default: never)
@@ -13016,6 +13129,7 @@ async function main(argv) {
13016
13129
  json: { type: "boolean", default: false },
13017
13130
  output: { type: "string" },
13018
13131
  copy: { type: "boolean", default: false },
13132
+ "no-color": { type: "boolean", default: false },
13019
13133
  quiet: { type: "boolean", default: false },
13020
13134
  verbose: { type: "boolean", default: false },
13021
13135
  "fail-on": { type: "string" },
@@ -13082,11 +13196,16 @@ async function main(argv) {
13082
13196
  failOn: failOnRaw,
13083
13197
  ...failUnder === undefined ? {} : { failUnder },
13084
13198
  global: values.global,
13085
- configPath: values.config
13199
+ configPath: values.config,
13200
+ colour: shouldColour({
13201
+ isTTY: process.stdout.isTTY === true,
13202
+ env: process.env,
13203
+ noColorFlag: values["no-color"]
13204
+ })
13086
13205
  });
13087
13206
  process.stdout.write(result.stdout);
13088
13207
  if (values.copy === true) {
13089
- const copied = await copyToClipboard(result.stdout);
13208
+ const copied = await copyToClipboard(result.plain ?? result.stdout);
13090
13209
  process.stderr.write(copied.ok ? `Copied to clipboard via ${copied.tool}.
13091
13210
  ` : `Could not copy: ${safe(copied.reason)}. Redirect instead: agentscan check > report.md
13092
13211
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chimix/agentscan",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Find broken agent configuration in a project — hooks whose scripts are gone, MCP servers that cannot start, skills that disagree with their lockfile",
5
5
  "type": "module",
6
6
  "bin": {