Skip to content

Commit c4ad9f0

Browse files
committed
chore(mcp): add tsc build pipeline + convert small lib utilities to TypeScript
Adds packages/loopover-mcp/tsconfig.json and switches the package's build script from a node --check-only chain to a real tsc compile (in-place .ts -> .js emit, import paths unchanged) plus a glob-driven syntax verification pass, mirroring loopover-miner's own build pipeline. Converts the four smallest, lowest-risk lib files: cli-error.js, format-table.js, redact-local-path.js, telemetry.js. Refs #7328
1 parent 09b028c commit c4ad9f0

16 files changed

Lines changed: 499 additions & 120 deletions

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/loopover-mcp/lib/cli-error.js

Lines changed: 10 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/** Shared CLI failure output (#5928): when `--json` is set, emit a parseable `{ ok: false, error }` object on
2+
* stdout (matching each command's success-path JSON stream); otherwise log plain text to stderr. */
3+
4+
export function reportCliFailure(wantsJson: boolean, message: string, exitCode = 2): number {
5+
if (wantsJson) {
6+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
7+
} else {
8+
console.error(message);
9+
}
10+
return exitCode;
11+
}
12+
13+
/** True when argv includes `--json` or `--json=...` (used before a full parse result exists). */
14+
export function argsWantJson(args: readonly string[]): boolean {
15+
return args.some((arg) => arg === "--json" || arg?.startsWith("--json="));
16+
}
17+
18+
/** Normalize a thrown value to a safe error string for CLI output. */
19+
export function describeCliError(error: unknown): string {
20+
return error instanceof Error ? error.message : String(error);
21+
}

packages/loopover-mcp/lib/format-table.js

Lines changed: 29 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Pure, dependency-free monospace table renderer shared by the stdio CLI's report-shaped commands
2+
// (#2231). Kept in lib/ (not the bin) so it can be unit-tested in isolation: the bin auto-runs its
3+
// CLI/MCP entrypoint on import, so importable helpers live here instead.
4+
5+
export type TableHeader = { key: string; label?: string; align?: "left" | "right" };
6+
export type TableRowObject = Record<string, unknown>;
7+
export type TableRow = TableRowObject | unknown[] | null | undefined;
8+
export type TableInput = TableRow[] | { headers?: (string | TableHeader)[]; rows?: TableRow[] };
9+
export type FormatTableOptions = { align?: Record<string, "left" | "right">; gap?: number };
10+
11+
type NormalizedHeader = { key: string; label: string; align?: "left" | "right" | undefined };
12+
type Normalized = { headers: NormalizedHeader[]; rows: TableRow[] };
13+
14+
// Normalize either an array of row objects or an explicit { headers, rows } shape into a common
15+
// { headers, rows } form. For an array of objects the column set is the union of keys in first-seen
16+
// order, and each key doubles as its own header label.
17+
function normalizeInput(input: TableInput | undefined | null): Normalized {
18+
if (Array.isArray(input)) {
19+
const keys: string[] = [];
20+
for (const row of input) {
21+
for (const key of Object.keys(row ?? {})) if (!keys.includes(key)) keys.push(key);
22+
}
23+
return { headers: keys.map((key) => ({ key, label: key })), rows: input };
24+
}
25+
const headers = (input?.headers ?? []).map((header) =>
26+
typeof header === "string" ? { key: header, label: header } : { key: header.key, label: header.label ?? header.key, align: header.align },
27+
);
28+
return { headers, rows: input?.rows ?? [] };
29+
}
30+
31+
function stringifyCell(value: unknown): string {
32+
return value === undefined || value === null ? "" : String(value);
33+
}
34+
35+
// A row is either an object keyed by column key or a positional array; read the matching cell.
36+
function readCell(row: TableRow, header: NormalizedHeader, columnIndex: number): unknown {
37+
if (Array.isArray(row)) return row[columnIndex];
38+
return (row as TableRowObject | undefined)?.[header.key];
39+
}
40+
41+
function resolveAlign(header: NormalizedHeader, opts: FormatTableOptions): "left" | "right" {
42+
const fromOpts = opts.align && (opts.align[header.key] ?? opts.align[header.label]);
43+
return header.align ?? fromOpts ?? "left";
44+
}
45+
46+
/**
47+
* Render tabular data as an aligned, monospace plain-text table (header row + one line per row).
48+
* Accepts an array of row objects, or `{ headers, rows }` with string/`{ key, label, align }`
49+
* headers and object/array rows. `opts.align` maps a column key/label to `"left"`|`"right"`;
50+
* `opts.gap` sets the space count between columns (default 2). Pure — no I/O, no dependencies.
51+
* Returns "" when there are no columns.
52+
*/
53+
export function formatTable(input?: TableInput | null, opts: FormatTableOptions = {}): string {
54+
const { headers, rows } = normalizeInput(input);
55+
if (headers.length === 0) return "";
56+
const gap = " ".repeat(Math.max(1, opts.gap ?? 2));
57+
const aligns = headers.map((header) => resolveAlign(header, opts));
58+
// Precompute every cell's text so column widths and the rendered rows read the same strings.
59+
const bodyCells = rows.map((row) => headers.map((header, column) => stringifyCell(readCell(row, header, column))));
60+
// Every `cells`/`widths` array here has exactly `headers.length` entries (built via headers.map), so
61+
// indexing by a column index drawn from that same range is always in bounds.
62+
const widths = headers.map((header, column) =>
63+
Math.max(header.label.length, ...bodyCells.map((cells) => cells[column]!.length), 0),
64+
);
65+
const renderRow = (cells: string[]) =>
66+
// Trim trailing padding so a left-aligned final column never emits dangling spaces.
67+
cells.map((text, column) => (aligns[column] === "right" ? text.padStart(widths[column]!) : text.padEnd(widths[column]!))).join(gap).replace(/\s+$/, "");
68+
return [renderRow(headers.map((header) => header.label)), ...bodyCells.map(renderRow)].join("\n");
69+
}

0 commit comments

Comments
 (0)