roborank 0.1.0 → 0.1.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/bin/roborank.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { run } from '../src/index.js';
3
3
 
4
- const { code, stdout, stderr } = await run(process.argv.slice(2));
4
+ const { code, stdout, stderr } = await run(process.argv.slice(2), { tty: process.stdout.isTTY === true });
5
5
  if (stdout) process.stdout.write(stdout + '\n');
6
6
  if (stderr) process.stderr.write(stderr + '\n');
7
7
  process.exit(code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roborank",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Roborank CLI — run your site's SEO findings and fixes from the terminal or from an AI coding agent",
5
5
  "type": "module",
6
6
  "bin": {
package/src/format.js ADDED
@@ -0,0 +1,70 @@
1
+ // Human-readable rendering for terminal use. JSON stays the contract for
2
+ // agents and pipes; this only runs when stdout is a TTY and --json is absent.
3
+
4
+ const MAX_CELL = 48;
5
+
6
+ function isPlainObject(v) {
7
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
8
+ }
9
+
10
+ function cell(v) {
11
+ if (v === null || v === undefined) return '-';
12
+ if (v === true) return 'yes';
13
+ if (v === false) return 'no';
14
+ if (typeof v === 'number') {
15
+ if (Number.isInteger(v)) return v.toLocaleString('en-US');
16
+ // Ratios like ctr read better as percentages, but only obvious ones.
17
+ if (v > 0 && v < 1) return (v * 100).toFixed(1) + '%';
18
+ return String(Math.round(v * 10) / 10);
19
+ }
20
+ if (Array.isArray(v)) return v.length ? `[${v.length} items]` : '[]';
21
+ if (isPlainObject(v)) return '{…}';
22
+ let s = String(v);
23
+ // Timestamps: the date part carries the signal.
24
+ if (/^\d{4}-\d{2}-\d{2}T/.test(s)) s = s.slice(0, 10);
25
+ if (s.length > MAX_CELL) s = s.slice(0, MAX_CELL - 1) + '…';
26
+ return s;
27
+ }
28
+
29
+ function table(rows) {
30
+ const cols = [];
31
+ for (const r of rows) for (const k of Object.keys(r)) if (!cols.includes(k)) cols.push(k);
32
+ const grid = rows.map(r => cols.map(c => cell(r[c])));
33
+ const widths = cols.map((c, i) => Math.max(c.length, ...grid.map(g => g[i].length)));
34
+ const line = (parts) => parts.map((p, i) => p.padEnd(widths[i])).join(' ').trimEnd();
35
+ return [
36
+ line(cols),
37
+ line(widths.map(w => '-'.repeat(w))),
38
+ ...grid.map(line),
39
+ ].join('\n');
40
+ }
41
+
42
+ function keyValues(obj, indent = '') {
43
+ const lines = [];
44
+ for (const [k, v] of Object.entries(obj)) {
45
+ if (Array.isArray(v) && v.length && v.every(isPlainObject)) {
46
+ lines.push(`${indent}${k}:`);
47
+ lines.push(table(v).split('\n').map(l => indent + ' ' + l).join('\n'));
48
+ } else if (Array.isArray(v) && v.length && v.every(x => !isPlainObject(x) && !Array.isArray(x))) {
49
+ lines.push(`${indent}${k}:`);
50
+ for (const x of v) lines.push(`${indent} ${cell(x)}`);
51
+ } else if (isPlainObject(v)) {
52
+ lines.push(`${indent}${k}:`);
53
+ lines.push(keyValues(v, indent + ' '));
54
+ } else {
55
+ lines.push(`${indent}${k}: ${cell(v)}`);
56
+ }
57
+ }
58
+ return lines.join('\n');
59
+ }
60
+
61
+ /** Render any tool payload for humans. */
62
+ export function pretty(payload) {
63
+ if (payload === null || payload === undefined) return '';
64
+ if (typeof payload !== 'object') return String(payload);
65
+ if (Array.isArray(payload)) {
66
+ if (!payload.length) return '(nothing found)';
67
+ return payload.every(isPlainObject) ? table(payload) : payload.map(cell).join('\n');
68
+ }
69
+ return keyValues(payload);
70
+ }
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ import { resolveToken, resolveApi, writeConfig, readConfig, clearConfig, configP
3
3
  import { callTool, listTools, ApiError, ToolError } from './client.js';
4
4
  import { TOOL_COMMANDS, helpText } from './commands.js';
5
5
  import { writeProjectDoc } from './init.js';
6
+ import { pretty } from './format.js';
6
7
  import { readFileSync } from 'node:fs';
7
8
  import { fileURLToPath } from 'node:url';
8
9
 
@@ -21,7 +22,7 @@ const VERSION = (() => {
21
22
  * Returns { code, stdout, stderr } instead of writing/exiting itself, so the
22
23
  * behaviour is testable and the bin shim stays trivial.
23
24
  */
24
- export async function run(argv, { _fetch = fetch, env = process.env } = {}) {
25
+ export async function run(argv, { _fetch = fetch, env = process.env, tty = false } = {}) {
25
26
  const out = [];
26
27
  const err = [];
27
28
  const { flags, positionals } = parseArgv(argv);
@@ -29,8 +30,12 @@ export async function run(argv, { _fetch = fetch, env = process.env } = {}) {
29
30
 
30
31
  // --raw=false should mean false, not "a non-empty string is truthy".
31
32
  const raw = flags.raw === true || flags.raw === 'true' || flags.raw === '1';
33
+ const wantJson = flags.json === true || flags.json === 'true' || flags.json === '1';
34
+ // Humans at a terminal get tables; pipes, agents, --json and --raw get JSON.
35
+ const human = tty && !raw && !wantJson;
32
36
  const print = (obj) => {
33
- out.push(typeof obj === 'string' ? obj : JSON.stringify(obj, null, raw ? 0 : 2));
37
+ if (typeof obj === 'string') { out.push(obj); return; }
38
+ out.push(human ? pretty(obj) : JSON.stringify(obj, null, raw ? 0 : 2));
34
39
  };
35
40
  const done = (code = EXIT.OK) => ({ code, stdout: out.join('\n'), stderr: err.join('\n') });
36
41