@dheerajsom/pinhub 0.1.2 → 0.2.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.
- package/{README.md → CLI.md} +53 -13
- package/LICENSE +21 -0
- package/SETUP.md +16 -5
- package/dist/boards/arduino-uno-r3.js +1 -1
- package/dist/boards/esp32-devkit-v1.js +5 -4
- package/dist/boards/generated.js +1044 -218
- package/dist/cli.js +17 -1
- package/dist/model.d.ts +3 -0
- package/dist/render/board.d.ts +4 -2
- package/dist/render/board.js +174 -96
- package/dist/render/chars.d.ts +4 -0
- package/dist/render/chars.js +8 -0
- package/dist/render/meta.d.ts +5 -5
- package/dist/render/meta.js +118 -37
- package/dist/render/text.d.ts +22 -0
- package/dist/render/text.js +163 -0
- package/dist/render/theme.js +4 -1
- package/dist/run.d.ts +4 -2
- package/dist/run.js +96 -50
- package/dist/status.d.ts +28 -0
- package/dist/status.js +63 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +5 -2
- package/package.json +17 -5
package/dist/render/meta.js
CHANGED
|
@@ -1,62 +1,143 @@
|
|
|
1
|
+
import { asciiChars } from "./chars.js";
|
|
2
|
+
import { normalizeWidth, padEndVisible, padStartVisible, toAscii, visibleWidth, wrapText, } from "./text.js";
|
|
1
3
|
import { warningStyle } from "./theme.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
+
function finish(text, opts) {
|
|
5
|
+
return opts.chars === asciiChars ? toAscii(text) : text;
|
|
6
|
+
}
|
|
7
|
+
function warningMarker(chars, severity) {
|
|
8
|
+
switch (severity) {
|
|
9
|
+
case "danger":
|
|
10
|
+
return chars.danger ?? chars.warn;
|
|
11
|
+
case "warning":
|
|
12
|
+
return chars.warn;
|
|
13
|
+
case "info":
|
|
14
|
+
return chars.info;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function renderCardList(rows, opts) {
|
|
18
|
+
const { chalk: c, chars: ch } = opts;
|
|
19
|
+
const width = normalizeWidth(opts.width);
|
|
20
|
+
const lines = [];
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
lines.push(...wrapText(`${row.id} (${row.pins} pins)`, width).map((line) => c.bold(line)));
|
|
23
|
+
if (!opts.compact) {
|
|
24
|
+
lines.push(...wrapText(`${row.name} ${ch.dash ?? "-"} ${row.manufacturer}`, width, " ").map((line) => c.dim(line)));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return lines;
|
|
28
|
+
}
|
|
29
|
+
/** Width-responsive catalog/search table; cells wrap instead of being truncated. */
|
|
30
|
+
export function renderBoardTable(boardList, opts) {
|
|
31
|
+
const { chalk: c } = opts;
|
|
32
|
+
const width = normalizeWidth(opts.width);
|
|
4
33
|
const rows = boardList.map((board) => ({
|
|
5
34
|
id: board.id,
|
|
6
35
|
name: board.name,
|
|
7
36
|
manufacturer: board.manufacturer,
|
|
8
|
-
pins: String(board.headers.reduce((sum,
|
|
37
|
+
pins: String(board.headers.reduce((sum, header) => sum + header.pins.length, 0)),
|
|
9
38
|
}));
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
39
|
+
const lines = [c.bold(`${rows.length} board${rows.length === 1 ? "" : "s"}`)];
|
|
40
|
+
if (rows.length === 0)
|
|
41
|
+
return finish(lines.join("\n"), opts);
|
|
42
|
+
const pinWidth = Math.max(4, ...rows.map((row) => visibleWidth(row.pins)));
|
|
43
|
+
const availableForText = width - pinWidth - 6;
|
|
44
|
+
if (opts.compact || availableForText < 36) {
|
|
45
|
+
lines.push(...renderCardList(rows, opts));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const idWidth = Math.max(14, Math.floor(availableForText * 0.4));
|
|
49
|
+
const nameWidth = Math.max(12, Math.floor(availableForText * 0.34));
|
|
50
|
+
const manufacturerWidth = availableForText - idWidth - nameWidth;
|
|
51
|
+
lines.push(c.dim(`${padEndVisible("ID", idWidth)} ${padEndVisible("BOARD", nameWidth)} ${padEndVisible("MANUFACTURER", manufacturerWidth)} ${padStartVisible("PINS", pinWidth)}`));
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
const idLines = wrapText(row.id, idWidth);
|
|
54
|
+
const nameLines = wrapText(row.name, nameWidth);
|
|
55
|
+
const manufacturerLines = wrapText(row.manufacturer, manufacturerWidth);
|
|
56
|
+
const height = Math.max(idLines.length, nameLines.length, manufacturerLines.length);
|
|
57
|
+
for (let index = 0; index < height; index += 1) {
|
|
58
|
+
const id = padEndVisible(idLines[index] ?? "", idWidth);
|
|
59
|
+
const name = padEndVisible(nameLines[index] ?? "", nameWidth);
|
|
60
|
+
const manufacturer = padEndVisible(manufacturerLines[index] ?? "", manufacturerWidth);
|
|
61
|
+
const pins = index === 0 ? padStartVisible(row.pins, pinWidth) : " ".repeat(pinWidth);
|
|
62
|
+
lines.push(`${c.bold(id)} ${name} ${manufacturer} ${pins}`.trimEnd());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
17
66
|
lines.push("");
|
|
18
|
-
lines.push(
|
|
19
|
-
return lines.join("\n");
|
|
67
|
+
lines.push(...wrapText("Show a pinout with: ph <id> e.g. ph rpi5", width).map((line) => c.dim(line)));
|
|
68
|
+
return finish(lines.join("\n"), opts);
|
|
20
69
|
}
|
|
21
|
-
export function renderSources(board,
|
|
22
|
-
const
|
|
70
|
+
export function renderSources(board, opts) {
|
|
71
|
+
const { chalk: c, chars: ch } = opts;
|
|
72
|
+
const width = normalizeWidth(opts.width);
|
|
73
|
+
const lines = wrapText(`${board.name} ${ch.dash ?? "-"} sources`, width).map((line) => c.bold(line));
|
|
23
74
|
for (const source of board.sources) {
|
|
24
|
-
const tag = source.official ?
|
|
25
|
-
|
|
26
|
-
lines.push(
|
|
75
|
+
const tag = source.official ? "official" : "third-party";
|
|
76
|
+
const style = source.official ? c.green : c.yellow;
|
|
77
|
+
lines.push(...wrapText(`${ch.bullet} ${source.title} (${tag})`, width, " ", " ").map((line) => style(line)));
|
|
78
|
+
lines.push(...wrapText(source.url, width, " ", " ").map((line) => c.dim(line)));
|
|
27
79
|
}
|
|
28
|
-
return lines.join("\n");
|
|
80
|
+
return finish(lines.join("\n"), opts);
|
|
29
81
|
}
|
|
30
|
-
export function renderInfo(board,
|
|
82
|
+
export function renderInfo(board, opts) {
|
|
83
|
+
const { chalk: c, chars: ch } = opts;
|
|
84
|
+
const width = normalizeWidth(opts.width);
|
|
31
85
|
const lines = [];
|
|
32
|
-
lines.push(`${
|
|
86
|
+
lines.push(...wrapText(`${board.name} ${ch.dash ?? "-"} ${board.manufacturer}`, width).map((line) => c.bold(line)));
|
|
33
87
|
if (board.description)
|
|
34
|
-
lines.push(...wrapText(board.description, width
|
|
88
|
+
lines.push(...wrapText(board.description, width));
|
|
35
89
|
if (board.revisionNote) {
|
|
36
90
|
lines.push("");
|
|
37
|
-
lines.push(...wrapText(`Revision note: ${board.revisionNote}`, width
|
|
91
|
+
lines.push(...wrapText(`Revision note: ${board.revisionNote}`, width).map((line) => c.dim(line)));
|
|
38
92
|
}
|
|
39
|
-
lines.push("");
|
|
40
|
-
lines.push(c.bold("Headers"));
|
|
93
|
+
lines.push("", c.bold("Headers"));
|
|
41
94
|
for (const header of board.headers) {
|
|
42
|
-
lines.push(
|
|
95
|
+
lines.push(...wrapText(`${ch.bullet} ${header.name} ${ch.dash ?? "-"} ${header.pins.length} pins`, width, " ", " "));
|
|
43
96
|
}
|
|
44
|
-
|
|
45
|
-
|
|
97
|
+
if (board.headers.length === 0) {
|
|
98
|
+
lines.push(...wrapText("No pinout headers available yet.", width, " ").map((line) => c.dim(line)));
|
|
99
|
+
}
|
|
100
|
+
lines.push("", c.bold("Warnings"));
|
|
46
101
|
for (const warning of board.warnings) {
|
|
47
|
-
const marker = warning.severity
|
|
102
|
+
const marker = warningMarker(ch, warning.severity);
|
|
48
103
|
const style = warningStyle(c, warning.severity);
|
|
49
|
-
lines.push(...wrapText(`${marker} ${warning.text}`, width, " ").map((
|
|
104
|
+
lines.push(...wrapText(`${marker} ${warning.text}`, width, " ", " ").map((line) => style(line)));
|
|
50
105
|
}
|
|
51
|
-
lines.push("");
|
|
52
|
-
lines.push(c.bold("Sources"));
|
|
106
|
+
lines.push("", c.bold("Sources"));
|
|
53
107
|
for (const source of board.sources) {
|
|
54
|
-
const tag = source.official ?
|
|
55
|
-
|
|
56
|
-
lines.push(
|
|
108
|
+
const tag = source.official ? "official" : "third-party";
|
|
109
|
+
const style = source.official ? c.green : c.yellow;
|
|
110
|
+
lines.push(...wrapText(`${ch.bullet} ${source.title} (${tag})`, width, " ", " ").map((line) => style(line)));
|
|
111
|
+
lines.push(...wrapText(source.url, width, " ", " ").map((line) => c.dim(line)));
|
|
112
|
+
}
|
|
113
|
+
if (opts.details) {
|
|
114
|
+
lines.push("", c.bold("Pin details"));
|
|
115
|
+
let detailCount = 0;
|
|
116
|
+
for (const header of board.headers) {
|
|
117
|
+
const pins = [...header.pins]
|
|
118
|
+
.sort((left, right) => left.physical - right.physical)
|
|
119
|
+
.filter((pin) => Boolean(pin.functions?.length || pin.notes?.length));
|
|
120
|
+
if (pins.length === 0)
|
|
121
|
+
continue;
|
|
122
|
+
lines.push(...wrapText(header.name, width, " ").map((line) => c.bold(line)));
|
|
123
|
+
for (const pin of pins) {
|
|
124
|
+
const detail = [];
|
|
125
|
+
if (pin.functions?.length)
|
|
126
|
+
detail.push(`functions: ${pin.functions.join(", ")}`);
|
|
127
|
+
if (pin.notes?.length)
|
|
128
|
+
detail.push(...pin.notes);
|
|
129
|
+
const prefix = ` Pin ${pin.physical}: `;
|
|
130
|
+
const continuation = " ".repeat(visibleWidth(prefix));
|
|
131
|
+
lines.push(...wrapText(detail.join("; "), width, prefix, continuation));
|
|
132
|
+
detailCount += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (detailCount === 0) {
|
|
136
|
+
lines.push(...wrapText("No additional pin details.", width, " ").map((line) => c.dim(line)));
|
|
137
|
+
}
|
|
57
138
|
}
|
|
58
139
|
lines.push("");
|
|
59
|
-
lines.push(
|
|
60
|
-
lines.push(
|
|
61
|
-
return lines.join("\n");
|
|
140
|
+
lines.push(...wrapText(`Aliases: ${board.aliases.join(", ") || "none"}`, width).map((line) => c.dim(line)));
|
|
141
|
+
lines.push(...wrapText(`Pinout: ph ${board.id}`, width).map((line) => c.dim(line)));
|
|
142
|
+
return finish(lines.join("\n"), opts);
|
|
62
143
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const MIN_TERMINAL_WIDTH = 20;
|
|
2
|
+
export declare const MAX_TERMINAL_WIDTH = 1000;
|
|
3
|
+
export declare const DEFAULT_TERMINAL_WIDTH = 80;
|
|
4
|
+
/** Clamp externally detected widths to a usable, integer terminal width. */
|
|
5
|
+
export declare function normalizeWidth(value: number | undefined, fallback?: number): number;
|
|
6
|
+
/** Width in terminal cells, ignoring ANSI styling. */
|
|
7
|
+
export declare function visibleWidth(text: string): number;
|
|
8
|
+
export declare function padEndVisible(text: string, width: number): string;
|
|
9
|
+
export declare function padStartVisible(text: string, width: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* Word-wrap text to terminal cells and hard-wrap long tokens such as URLs.
|
|
12
|
+
* The first and continuation prefixes may differ, which keeps warning markers
|
|
13
|
+
* aligned without accidentally adding columns after wrapping.
|
|
14
|
+
*/
|
|
15
|
+
export declare function wrapText(text: string, width: number, firstIndent?: string, continuationIndent?: string): string[];
|
|
16
|
+
/** Remove terminal control sequences before echoing user-provided arguments. */
|
|
17
|
+
export declare function safeTerminalText(text: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Structural ASCII mode also sanitizes source data punctuation. Replacements
|
|
20
|
+
* stay at one cell so a width-safe render remains width-safe after conversion.
|
|
21
|
+
*/
|
|
22
|
+
export declare function toAscii(text: string): string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import stringWidth from "string-width";
|
|
3
|
+
export const MIN_TERMINAL_WIDTH = 20;
|
|
4
|
+
export const MAX_TERMINAL_WIDTH = 1000;
|
|
5
|
+
export const DEFAULT_TERMINAL_WIDTH = 80;
|
|
6
|
+
/** Clamp externally detected widths to a usable, integer terminal width. */
|
|
7
|
+
export function normalizeWidth(value, fallback = DEFAULT_TERMINAL_WIDTH) {
|
|
8
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0)
|
|
9
|
+
return fallback;
|
|
10
|
+
return Math.min(MAX_TERMINAL_WIDTH, Math.max(MIN_TERMINAL_WIDTH, Math.floor(value)));
|
|
11
|
+
}
|
|
12
|
+
/** Width in terminal cells, ignoring ANSI styling. */
|
|
13
|
+
export function visibleWidth(text) {
|
|
14
|
+
return stringWidth(text);
|
|
15
|
+
}
|
|
16
|
+
export function padEndVisible(text, width) {
|
|
17
|
+
return text + " ".repeat(Math.max(0, width - visibleWidth(text)));
|
|
18
|
+
}
|
|
19
|
+
export function padStartVisible(text, width) {
|
|
20
|
+
return " ".repeat(Math.max(0, width - visibleWidth(text))) + text;
|
|
21
|
+
}
|
|
22
|
+
function splitAtWidth(text, width) {
|
|
23
|
+
if (width <= 0)
|
|
24
|
+
return ["", text];
|
|
25
|
+
let head = "";
|
|
26
|
+
let used = 0;
|
|
27
|
+
let offset = 0;
|
|
28
|
+
let preferredBreak = 0;
|
|
29
|
+
for (const character of text) {
|
|
30
|
+
const characterWidth = visibleWidth(character);
|
|
31
|
+
if (head && used + characterWidth > width)
|
|
32
|
+
break;
|
|
33
|
+
// A single wide glyph is preferable to an infinite loop at a one-cell edge.
|
|
34
|
+
if (!head && characterWidth > width) {
|
|
35
|
+
head = character;
|
|
36
|
+
offset += character.length;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
if (used + characterWidth > width)
|
|
40
|
+
break;
|
|
41
|
+
head += character;
|
|
42
|
+
used += characterWidth;
|
|
43
|
+
offset += character.length;
|
|
44
|
+
if (/[-/_.?&=]/.test(character))
|
|
45
|
+
preferredBreak = offset;
|
|
46
|
+
}
|
|
47
|
+
// Keep identifiers and URLs readable by preferring a nearby punctuation
|
|
48
|
+
// boundary over an arbitrary mid-token split.
|
|
49
|
+
if (offset < text.length && preferredBreak > 0) {
|
|
50
|
+
const preferredHead = text.slice(0, preferredBreak);
|
|
51
|
+
if (visibleWidth(preferredHead) >= Math.max(4, Math.floor(width / 2))) {
|
|
52
|
+
return [preferredHead, text.slice(preferredBreak)];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [head, text.slice(offset)];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Word-wrap text to terminal cells and hard-wrap long tokens such as URLs.
|
|
59
|
+
* The first and continuation prefixes may differ, which keeps warning markers
|
|
60
|
+
* aligned without accidentally adding columns after wrapping.
|
|
61
|
+
*/
|
|
62
|
+
export function wrapText(text, width, firstIndent = "", continuationIndent = firstIndent) {
|
|
63
|
+
const targetWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : DEFAULT_TERMINAL_WIDTH;
|
|
64
|
+
const paragraphs = text.replace(/\r\n?/g, "\n").split("\n");
|
|
65
|
+
const output = [];
|
|
66
|
+
let firstOutputLine = true;
|
|
67
|
+
for (const paragraph of paragraphs) {
|
|
68
|
+
const words = paragraph.trim().split(/\s+/).filter(Boolean);
|
|
69
|
+
if (words.length === 0) {
|
|
70
|
+
output.push("");
|
|
71
|
+
firstOutputLine = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
let indent = firstOutputLine ? firstIndent : continuationIndent;
|
|
75
|
+
let line = "";
|
|
76
|
+
const flush = () => {
|
|
77
|
+
if (!line)
|
|
78
|
+
return;
|
|
79
|
+
output.push(indent + line);
|
|
80
|
+
line = "";
|
|
81
|
+
firstOutputLine = false;
|
|
82
|
+
indent = continuationIndent;
|
|
83
|
+
};
|
|
84
|
+
for (const originalWord of words) {
|
|
85
|
+
let word = originalWord;
|
|
86
|
+
const available = () => Math.max(1, targetWidth - visibleWidth(indent));
|
|
87
|
+
if (line && visibleWidth(`${line} ${word}`) <= available()) {
|
|
88
|
+
line += ` ${word}`;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (line)
|
|
92
|
+
flush();
|
|
93
|
+
while (visibleWidth(word) > available()) {
|
|
94
|
+
const [head, tail] = splitAtWidth(word, available());
|
|
95
|
+
line = head;
|
|
96
|
+
flush();
|
|
97
|
+
word = tail;
|
|
98
|
+
}
|
|
99
|
+
line = word;
|
|
100
|
+
}
|
|
101
|
+
flush();
|
|
102
|
+
}
|
|
103
|
+
return output;
|
|
104
|
+
}
|
|
105
|
+
/** Remove terminal control sequences before echoing user-provided arguments. */
|
|
106
|
+
export function safeTerminalText(text) {
|
|
107
|
+
return Array.from(stripVTControlCharacters(text), (character) => {
|
|
108
|
+
const code = character.codePointAt(0) ?? 0;
|
|
109
|
+
return code <= 0x1f || code === 0x7f ? " " : character;
|
|
110
|
+
}).join("");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Structural ASCII mode also sanitizes source data punctuation. Replacements
|
|
114
|
+
* stay at one cell so a width-safe render remains width-safe after conversion.
|
|
115
|
+
*/
|
|
116
|
+
export function toAscii(text) {
|
|
117
|
+
const replacements = {
|
|
118
|
+
"—": "-",
|
|
119
|
+
"–": "-",
|
|
120
|
+
"−": "-",
|
|
121
|
+
"…": ".",
|
|
122
|
+
"×": "x",
|
|
123
|
+
"µ": "u",
|
|
124
|
+
"μ": "u",
|
|
125
|
+
"°": "o",
|
|
126
|
+
"→": ">",
|
|
127
|
+
"←": "<",
|
|
128
|
+
"↔": "-",
|
|
129
|
+
"≥": ">",
|
|
130
|
+
"≤": "<",
|
|
131
|
+
"“": "\"",
|
|
132
|
+
"”": "\"",
|
|
133
|
+
"‘": "'",
|
|
134
|
+
"’": "'",
|
|
135
|
+
"Ω": "O",
|
|
136
|
+
"ω": "o",
|
|
137
|
+
"·": ".",
|
|
138
|
+
" ": " ",
|
|
139
|
+
};
|
|
140
|
+
let output = "";
|
|
141
|
+
let inUnknownRun = false;
|
|
142
|
+
for (const character of text.normalize("NFKD")) {
|
|
143
|
+
if (replacements[character] !== undefined) {
|
|
144
|
+
output += replacements[character];
|
|
145
|
+
inUnknownRun = false;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const code = character.codePointAt(0) ?? 0;
|
|
149
|
+
if (code <= 0x7f) {
|
|
150
|
+
output += character;
|
|
151
|
+
inUnknownRun = false;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// Combining marks introduced by NFKD can be omitted; unknown glyphs remain
|
|
155
|
+
// visibly represented without breaking strict ASCII output.
|
|
156
|
+
if (/\p{Mark}/u.test(character))
|
|
157
|
+
continue;
|
|
158
|
+
if (!inUnknownRun)
|
|
159
|
+
output += "?";
|
|
160
|
+
inUnknownRun = true;
|
|
161
|
+
}
|
|
162
|
+
return output;
|
|
163
|
+
}
|
package/dist/render/theme.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Chalk } from "chalk";
|
|
2
2
|
export function makeChalk(colorEnabled) {
|
|
3
|
-
|
|
3
|
+
// The palette uses only the portable 16-color range. Avoid emitting truecolor
|
|
4
|
+
// sequences merely because stdout is a TTY; older consoles can still render
|
|
5
|
+
// this level cleanly.
|
|
6
|
+
return new Chalk({ level: colorEnabled ? 1 : 0 });
|
|
4
7
|
}
|
|
5
8
|
/**
|
|
6
9
|
* Category colors. Color is never the only signal: labels and category names
|
package/dist/run.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type RunOptions = {
|
|
|
3
3
|
columns?: number;
|
|
4
4
|
/** Override TTY detection (piped output disables color automatically). */
|
|
5
5
|
isTTY?: boolean;
|
|
6
|
-
/** Override environment (for NO_COLOR handling in tests). */
|
|
6
|
+
/** Override environment (for NO_COLOR/CI/TERM handling in tests). */
|
|
7
7
|
env?: Record<string, string | undefined>;
|
|
8
8
|
};
|
|
9
9
|
export type RunResult = {
|
|
@@ -11,8 +11,10 @@ export type RunResult = {
|
|
|
11
11
|
stdout: string;
|
|
12
12
|
stderr: string;
|
|
13
13
|
};
|
|
14
|
+
export declare function parseWidth(value: string): number;
|
|
14
15
|
/**
|
|
15
16
|
* Runs the CLI against captured buffers instead of process streams, so the
|
|
16
|
-
* whole command surface
|
|
17
|
+
* whole command surface stays deterministic and testable. Executable-only
|
|
18
|
+
* motion lives in cli.ts and never enters these buffers.
|
|
17
19
|
*/
|
|
18
20
|
export declare function runCli(argv: string[], runOpts?: RunOptions): Promise<RunResult>;
|
package/dist/run.js
CHANGED
|
@@ -1,57 +1,67 @@
|
|
|
1
|
-
import { Command, CommanderError } from "commander";
|
|
1
|
+
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
2
2
|
import { boards, resolveBoard, searchBoards, suggestBoards } from "./catalog.js";
|
|
3
3
|
import { VERSION } from "./version.js";
|
|
4
4
|
import { asciiChars, unicodeChars } from "./render/chars.js";
|
|
5
5
|
import { makeChalk } from "./render/theme.js";
|
|
6
6
|
import { renderBoard } from "./render/board.js";
|
|
7
7
|
import { renderBoardTable, renderInfo, renderSources } from "./render/meta.js";
|
|
8
|
+
import { MAX_TERMINAL_WIDTH, MIN_TERMINAL_WIDTH, normalizeWidth, safeTerminalText, } from "./render/text.js";
|
|
9
|
+
import { hasNoColor, isCiEnvironment } from "./status.js";
|
|
10
|
+
export function parseWidth(value) {
|
|
11
|
+
if (!/^\d+$/.test(value)) {
|
|
12
|
+
throw new InvalidArgumentError(`Invalid --width: expected a whole number from ${MIN_TERMINAL_WIDTH} to ${MAX_TERMINAL_WIDTH}.`);
|
|
13
|
+
}
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isSafeInteger(parsed) ||
|
|
16
|
+
parsed < MIN_TERMINAL_WIDTH ||
|
|
17
|
+
parsed > MAX_TERMINAL_WIDTH) {
|
|
18
|
+
throw new InvalidArgumentError(`Invalid --width: expected a whole number from ${MIN_TERMINAL_WIDTH} to ${MAX_TERMINAL_WIDTH}.`);
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
8
22
|
/**
|
|
9
23
|
* Runs the CLI against captured buffers instead of process streams, so the
|
|
10
|
-
* whole command surface
|
|
24
|
+
* whole command surface stays deterministic and testable. Executable-only
|
|
25
|
+
* motion lives in cli.ts and never enters these buffers.
|
|
11
26
|
*/
|
|
12
27
|
export async function runCli(argv, runOpts = {}) {
|
|
13
28
|
let stdout = "";
|
|
14
29
|
let stderr = "";
|
|
15
30
|
let code = 0;
|
|
16
31
|
const print = (text) => {
|
|
17
|
-
stdout += text
|
|
32
|
+
stdout += `${text}\n`;
|
|
18
33
|
};
|
|
19
34
|
const printErr = (text) => {
|
|
20
|
-
stderr += text
|
|
35
|
+
stderr += `${text}\n`;
|
|
21
36
|
};
|
|
22
37
|
const env = runOpts.env ?? process.env;
|
|
23
38
|
const isTTY = runOpts.isTTY ?? Boolean(process.stdout.isTTY);
|
|
24
|
-
const detectedColumns = runOpts.columns ?? (isTTY ? process.stdout.columns : undefined)
|
|
25
|
-
const colorEnabled = (flags) => flags.color !== false &&
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
chars: flags.ascii ? asciiChars : unicodeChars,
|
|
38
|
-
width,
|
|
39
|
-
compact: Boolean(flags.compact),
|
|
40
|
-
};
|
|
41
|
-
};
|
|
39
|
+
const detectedColumns = normalizeWidth(runOpts.columns ?? (isTTY ? process.stdout.columns : undefined));
|
|
40
|
+
const colorEnabled = (flags) => flags.color !== false &&
|
|
41
|
+
!hasNoColor(env) &&
|
|
42
|
+
!isCiEnvironment(env) &&
|
|
43
|
+
env.TERM?.toLowerCase() !== "dumb" &&
|
|
44
|
+
isTTY;
|
|
45
|
+
const buildRender = (flags) => ({
|
|
46
|
+
chalk: makeChalk(colorEnabled(flags)),
|
|
47
|
+
chars: flags.ascii ? asciiChars : unicodeChars,
|
|
48
|
+
width: flags.width ?? detectedColumns,
|
|
49
|
+
compact: Boolean(flags.compact),
|
|
50
|
+
details: Boolean(flags.details),
|
|
51
|
+
});
|
|
42
52
|
const resolveOrSuggest = (words) => {
|
|
43
53
|
const query = words.join(" ");
|
|
54
|
+
const displayQuery = safeTerminalText(query);
|
|
44
55
|
const board = resolveBoard(query);
|
|
45
56
|
if (board)
|
|
46
57
|
return board;
|
|
47
|
-
printErr(`Board "${
|
|
58
|
+
printErr(`Board "${displayQuery}" was not found.`);
|
|
48
59
|
const suggestions = suggestBoards(query);
|
|
49
60
|
if (suggestions.length > 0) {
|
|
50
61
|
printErr("");
|
|
51
62
|
printErr("Did you mean:");
|
|
52
|
-
for (const suggestion of suggestions)
|
|
63
|
+
for (const suggestion of suggestions)
|
|
53
64
|
printErr(` ph ${suggestion.alias}`);
|
|
54
|
-
}
|
|
55
65
|
}
|
|
56
66
|
printErr("");
|
|
57
67
|
printErr("Run `ph list` to see all supported boards.");
|
|
@@ -68,25 +78,45 @@ export async function runCli(argv, runOpts = {}) {
|
|
|
68
78
|
return;
|
|
69
79
|
}
|
|
70
80
|
const render = buildRender(flags);
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
print(flags.source ? renderSources(board, render) : renderBoard(board, render));
|
|
82
|
+
};
|
|
83
|
+
const rejectIncompatibleFlags = (flags, scope) => {
|
|
84
|
+
if (scope === "multi-board" && flags.source) {
|
|
85
|
+
printErr("`--source` needs one board. Use `ph <board> --source`.");
|
|
73
86
|
code = 1;
|
|
74
|
-
return;
|
|
87
|
+
return true;
|
|
75
88
|
}
|
|
76
|
-
if (flags.
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
if (scope === "multi-board" && flags.details) {
|
|
90
|
+
printErr("`--details` needs one board. Use `ph <board> --details`.");
|
|
91
|
+
code = 1;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (flags.source && flags.details) {
|
|
95
|
+
printErr("`--source` and `--details` are separate one-board views; choose one.");
|
|
96
|
+
code = 1;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const jsonConflicts = [
|
|
100
|
+
flags.ascii && "`--ascii`",
|
|
101
|
+
flags.compact && "`--compact`",
|
|
102
|
+
flags.details && "`--details`",
|
|
103
|
+
flags.source && "`--source`",
|
|
104
|
+
flags.width !== undefined && "`--width`",
|
|
105
|
+
].filter((flag) => Boolean(flag));
|
|
106
|
+
if (flags.json && jsonConflicts.length > 0) {
|
|
107
|
+
printErr(`\`--json\` cannot be combined with ${jsonConflicts.join(", ")}.`);
|
|
108
|
+
code = 1;
|
|
109
|
+
return true;
|
|
79
110
|
}
|
|
80
|
-
|
|
111
|
+
return false;
|
|
81
112
|
};
|
|
82
|
-
const metaChalk = (flags = {}) => makeChalk(colorEnabled(flags));
|
|
83
|
-
const metaChars = (flags = {}) => (flags.ascii ? asciiChars : unicodeChars);
|
|
84
113
|
const program = new Command();
|
|
85
114
|
program
|
|
86
115
|
.name("ph")
|
|
87
116
|
.description("Hardware board pinout diagrams in your terminal.")
|
|
88
117
|
.version(VERSION, "-v, --version", "print the version")
|
|
89
118
|
.helpCommand("help [command]", "display help for a command")
|
|
119
|
+
.showHelpAfterError("(add --help for usage)")
|
|
90
120
|
.exitOverride()
|
|
91
121
|
.configureOutput({
|
|
92
122
|
writeOut: (text) => {
|
|
@@ -98,48 +128,63 @@ export async function runCli(argv, runOpts = {}) {
|
|
|
98
128
|
})
|
|
99
129
|
.argument("[board...]", "board id or alias (e.g. rpi5, pico, uno, esp32)")
|
|
100
130
|
.option("--compact", "tighter, borderless layout")
|
|
101
|
-
.option("--ascii", "ASCII-only
|
|
131
|
+
.option("--ascii", "ASCII-only output")
|
|
102
132
|
.option("--no-color", "disable ANSI colors (the NO_COLOR env var is also honored)")
|
|
103
|
-
.option("--
|
|
104
|
-
.option("--
|
|
105
|
-
.option("--
|
|
106
|
-
.
|
|
133
|
+
.option("--no-motion", "disable the interactive signal pulse")
|
|
134
|
+
.option("--details", "show additional alternate pin functions")
|
|
135
|
+
.option("--json", "emit machine-readable JSON")
|
|
136
|
+
.option("--source", "print documentation source links for one board")
|
|
137
|
+
.option("--width <columns>", `override terminal width (${MIN_TERMINAL_WIDTH}-${MAX_TERMINAL_WIDTH})`, parseWidth)
|
|
138
|
+
.addHelpText("after", "\nExamples:\n ph rpi5\n ph raspberry pi 5\n ph pico --compact\n ph esp32 --ascii --no-color\n ph uno --details\n ph uno --json\n ph rpi5 --source\n ph search raspberry")
|
|
107
139
|
.action((words, flags) => {
|
|
108
140
|
if (words.length === 0) {
|
|
109
141
|
program.outputHelp();
|
|
110
142
|
return;
|
|
111
143
|
}
|
|
144
|
+
if (rejectIncompatibleFlags(flags, "single-board"))
|
|
145
|
+
return;
|
|
112
146
|
showBoard(words, flags);
|
|
113
147
|
});
|
|
114
148
|
program
|
|
115
149
|
.command("list")
|
|
116
150
|
.description("list all boards in the catalog")
|
|
117
151
|
.action(() => {
|
|
118
|
-
|
|
152
|
+
const flags = program.opts();
|
|
153
|
+
if (rejectIncompatibleFlags(flags, "multi-board"))
|
|
154
|
+
return;
|
|
155
|
+
if (flags.json) {
|
|
156
|
+
print(JSON.stringify(boards, null, 2));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
print(renderBoardTable(boards, buildRender(flags)));
|
|
119
160
|
});
|
|
120
161
|
program
|
|
121
162
|
.command("search <query...>")
|
|
122
163
|
.description("search boards by name, alias, or manufacturer")
|
|
123
164
|
.action((words) => {
|
|
165
|
+
const flags = program.opts();
|
|
166
|
+
if (rejectIncompatibleFlags(flags, "multi-board"))
|
|
167
|
+
return;
|
|
124
168
|
const query = words.join(" ");
|
|
125
169
|
const results = searchBoards(query);
|
|
126
170
|
if (results.length === 0) {
|
|
127
|
-
printErr(`No boards matched "${query}". Run \`ph list\` to see all boards.`);
|
|
171
|
+
printErr(`No boards matched "${safeTerminalText(query)}". Run \`ph list\` to see all boards.`);
|
|
128
172
|
code = 1;
|
|
129
173
|
return;
|
|
130
174
|
}
|
|
131
|
-
|
|
175
|
+
if (flags.json) {
|
|
176
|
+
print(JSON.stringify(results, null, 2));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
print(renderBoardTable(results, buildRender(flags)));
|
|
132
180
|
});
|
|
133
181
|
program
|
|
134
182
|
.command("info <board...>")
|
|
135
183
|
.description("show board details, warnings, sources, and aliases")
|
|
136
|
-
.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// consumes them before dispatch; merge both option scopes.
|
|
141
|
-
const flags = command.optsWithGlobals();
|
|
142
|
-
void localFlags;
|
|
184
|
+
.action((words) => {
|
|
185
|
+
const flags = program.opts();
|
|
186
|
+
if (rejectIncompatibleFlags(flags, "single-board"))
|
|
187
|
+
return;
|
|
143
188
|
const board = resolveOrSuggest(words);
|
|
144
189
|
if (!board)
|
|
145
190
|
return;
|
|
@@ -147,7 +192,8 @@ export async function runCli(argv, runOpts = {}) {
|
|
|
147
192
|
print(JSON.stringify(board, null, 2));
|
|
148
193
|
return;
|
|
149
194
|
}
|
|
150
|
-
|
|
195
|
+
const render = buildRender(flags);
|
|
196
|
+
print(flags.source ? renderSources(board, render) : renderInfo(board, render));
|
|
151
197
|
});
|
|
152
198
|
try {
|
|
153
199
|
await program.parseAsync(argv, { from: "user" });
|