@dheerajsom/pinhub 0.1.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 +152 -0
- package/SETUP.md +126 -0
- package/dist/boards/arduino-uno-r3.d.ts +2 -0
- package/dist/boards/arduino-uno-r3.js +133 -0
- package/dist/boards/esp32-devkit-v1.d.ts +2 -0
- package/dist/boards/esp32-devkit-v1.js +143 -0
- package/dist/boards/index.d.ts +2 -0
- package/dist/boards/index.js +12 -0
- package/dist/boards/pico-shared.d.ts +3 -0
- package/dist/boards/pico-shared.js +108 -0
- package/dist/boards/raspberry-pi-5.d.ts +2 -0
- package/dist/boards/raspberry-pi-5.js +108 -0
- package/dist/boards/raspberry-pi-pico-w.d.ts +2 -0
- package/dist/boards/raspberry-pi-pico-w.js +36 -0
- package/dist/boards/raspberry-pi-pico.d.ts +2 -0
- package/dist/boards/raspberry-pi-pico.js +36 -0
- package/dist/catalog.d.ts +22 -0
- package/dist/catalog.js +74 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/model.d.ts +52 -0
- package/dist/model.js +6 -0
- package/dist/render/board.d.ts +12 -0
- package/dist/render/board.js +194 -0
- package/dist/render/chars.d.ts +16 -0
- package/dist/render/chars.js +28 -0
- package/dist/render/meta.d.ts +6 -0
- package/dist/render/meta.js +62 -0
- package/dist/render/theme.d.ts +9 -0
- package/dist/render/theme.js +37 -0
- package/dist/run.d.ts +18 -0
- package/dist/run.js +167 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +54 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { pinStyle, warningStyle } from "./theme.js";
|
|
2
|
+
const INDENT = " ";
|
|
3
|
+
/** Label plus its primary alternate function; reserved pins get a `*` marker. */
|
|
4
|
+
function displayLabel(pin) {
|
|
5
|
+
const fn = pin.functions?.[0];
|
|
6
|
+
const base = fn && fn !== pin.label ? `${pin.label} / ${fn}` : pin.label;
|
|
7
|
+
return pin.category === "reserved" ? `${base} *` : base;
|
|
8
|
+
}
|
|
9
|
+
function pinDetails(pin) {
|
|
10
|
+
return pin.voltage ? `${pin.category}, ${pin.voltage}` : pin.category;
|
|
11
|
+
}
|
|
12
|
+
export function wrapText(text, width, indent) {
|
|
13
|
+
const usable = Math.max(20, width - indent.length);
|
|
14
|
+
const words = text.split(/\s+/);
|
|
15
|
+
const lines = [];
|
|
16
|
+
let line = "";
|
|
17
|
+
for (const word of words) {
|
|
18
|
+
if (line && line.length + 1 + word.length > usable) {
|
|
19
|
+
lines.push(indent + line);
|
|
20
|
+
line = word;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
line = line ? `${line} ${word}` : word;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (line)
|
|
27
|
+
lines.push(indent + line);
|
|
28
|
+
return lines;
|
|
29
|
+
}
|
|
30
|
+
function byPosition(header, column, row) {
|
|
31
|
+
return header.pins.find((p) => p.position.column === column && p.position.row === row);
|
|
32
|
+
}
|
|
33
|
+
/** Bordered two-column diagram: label | pin | pin | label. */
|
|
34
|
+
function renderDualRowTable(header, opts) {
|
|
35
|
+
const { chalk: c, chars: ch } = opts;
|
|
36
|
+
const rows = [];
|
|
37
|
+
for (let r = 1; r <= header.layout.rows; r++) {
|
|
38
|
+
rows.push([byPosition(header, 1, r), byPosition(header, 2, r)]);
|
|
39
|
+
}
|
|
40
|
+
const leftWidth = Math.max(...rows.map(([l]) => (l ? displayLabel(l).length : 0)), 4);
|
|
41
|
+
const rightWidth = Math.max(...rows.map(([, r]) => (r ? displayLabel(r).length : 0)), 4);
|
|
42
|
+
const numWidth = Math.max(...header.pins.map((p) => String(p.physical).length), 2);
|
|
43
|
+
const totalWidth = INDENT.length + leftWidth + rightWidth + 2 * numWidth + 13;
|
|
44
|
+
if (totalWidth > opts.width)
|
|
45
|
+
return undefined;
|
|
46
|
+
const h = ch.horizontal;
|
|
47
|
+
const seg = (n) => h.repeat(n + 2);
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push(`${INDENT}${ch.topLeft}${seg(leftWidth)}${ch.topJoin}${seg(numWidth)}${ch.topJoin}${seg(numWidth)}${ch.topJoin}${seg(rightWidth)}${ch.topRight}`);
|
|
50
|
+
for (const [left, right] of rows) {
|
|
51
|
+
const leftLabel = left ? pinStyle(c, left)(displayLabel(left).padEnd(leftWidth)) : " ".repeat(leftWidth);
|
|
52
|
+
const rightLabel = right ? pinStyle(c, right)(displayLabel(right).padEnd(rightWidth)) : " ".repeat(rightWidth);
|
|
53
|
+
const leftNum = left ? String(left.physical).padStart(numWidth) : " ".repeat(numWidth);
|
|
54
|
+
const rightNum = right ? String(right.physical).padStart(numWidth) : " ".repeat(numWidth);
|
|
55
|
+
lines.push(`${INDENT}${ch.vertical} ${leftLabel} ${ch.vertical} ${leftNum} ${ch.vertical} ${rightNum} ${ch.vertical} ${rightLabel} ${ch.vertical}`);
|
|
56
|
+
}
|
|
57
|
+
lines.push(`${INDENT}${ch.bottomLeft}${seg(leftWidth)}${ch.bottomJoin}${seg(numWidth)}${ch.bottomJoin}${seg(numWidth)}${ch.bottomJoin}${seg(rightWidth)}${ch.bottomRight}`);
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
/** Borderless two-column pairs for --compact / narrower terminals. */
|
|
61
|
+
function renderDualRowCompact(header, opts) {
|
|
62
|
+
const { chalk: c, chars: ch } = opts;
|
|
63
|
+
const rows = [];
|
|
64
|
+
for (let r = 1; r <= header.layout.rows; r++) {
|
|
65
|
+
rows.push([byPosition(header, 1, r), byPosition(header, 2, r)]);
|
|
66
|
+
}
|
|
67
|
+
const leftWidth = Math.max(...rows.map(([l]) => (l ? displayLabel(l).length : 0)), 4);
|
|
68
|
+
const rightWidth = Math.max(...rows.map(([, r]) => (r ? displayLabel(r).length : 0)), 4);
|
|
69
|
+
const numWidth = Math.max(...header.pins.map((p) => String(p.physical).length), 2);
|
|
70
|
+
const totalWidth = INDENT.length + leftWidth + 2 + numWidth + 3 + numWidth + 2 + rightWidth;
|
|
71
|
+
if (totalWidth > opts.width)
|
|
72
|
+
return undefined;
|
|
73
|
+
return rows.map(([left, right]) => {
|
|
74
|
+
const leftLabel = left ? pinStyle(c, left)(displayLabel(left).padStart(leftWidth)) : " ".repeat(leftWidth);
|
|
75
|
+
const rightLabel = right ? pinStyle(c, right)(displayLabel(right)) : "";
|
|
76
|
+
const leftNum = left ? String(left.physical).padStart(numWidth) : " ".repeat(numWidth);
|
|
77
|
+
const rightNum = right ? String(right.physical).padEnd(numWidth) : " ".repeat(numWidth);
|
|
78
|
+
return `${INDENT}${leftLabel} ${leftNum} ${ch.pairSeparator} ${rightNum} ${rightLabel}`.trimEnd();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** One pin per line — always fits; last-resort fallback and 1-column layout. */
|
|
82
|
+
function renderPinList(header, opts) {
|
|
83
|
+
const { chalk: c } = opts;
|
|
84
|
+
const pins = [...header.pins].sort((a, b) => a.physical - b.physical);
|
|
85
|
+
const numWidth = Math.max(...pins.map((p) => String(p.physical).length), 2);
|
|
86
|
+
const labelWidth = Math.max(...pins.map((p) => displayLabel(p).length), 4);
|
|
87
|
+
return pins.map((pin) => {
|
|
88
|
+
const label = pinStyle(c, pin)(displayLabel(pin).padEnd(labelWidth));
|
|
89
|
+
return `${INDENT}${String(pin.physical).padStart(numWidth)} ${label} ${c.dim(pinDetails(pin))}`;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Bordered single-column table: # | pin | details. */
|
|
93
|
+
function renderSingleColumnTable(header, opts) {
|
|
94
|
+
const { chalk: c, chars: ch } = opts;
|
|
95
|
+
const pins = [...header.pins].sort((a, b) => a.physical - b.physical);
|
|
96
|
+
const numWidth = Math.max(...pins.map((p) => String(p.physical).length), 2);
|
|
97
|
+
const labelWidth = Math.max(...pins.map((p) => displayLabel(p).length), 4);
|
|
98
|
+
const detailWidth = Math.max(...pins.map((p) => pinDetails(p).length), 4);
|
|
99
|
+
const totalWidth = INDENT.length + numWidth + labelWidth + detailWidth + 10;
|
|
100
|
+
if (totalWidth > opts.width)
|
|
101
|
+
return undefined;
|
|
102
|
+
const h = ch.horizontal;
|
|
103
|
+
const seg = (n) => h.repeat(n + 2);
|
|
104
|
+
const lines = [];
|
|
105
|
+
lines.push(`${INDENT}${ch.topLeft}${seg(numWidth)}${ch.topJoin}${seg(labelWidth)}${ch.topJoin}${seg(detailWidth)}${ch.topRight}`);
|
|
106
|
+
for (const pin of pins) {
|
|
107
|
+
const label = pinStyle(c, pin)(displayLabel(pin).padEnd(labelWidth));
|
|
108
|
+
lines.push(`${INDENT}${ch.vertical} ${String(pin.physical).padStart(numWidth)} ${ch.vertical} ${label} ${ch.vertical} ${pinDetails(pin).padEnd(detailWidth)} ${ch.vertical}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push(`${INDENT}${ch.bottomLeft}${seg(numWidth)}${ch.bottomJoin}${seg(labelWidth)}${ch.bottomJoin}${seg(detailWidth)}${ch.bottomRight}`);
|
|
111
|
+
return lines;
|
|
112
|
+
}
|
|
113
|
+
function renderHeader(header, opts) {
|
|
114
|
+
const { chalk: c } = opts;
|
|
115
|
+
const lines = [];
|
|
116
|
+
lines.push(c.bold(header.name));
|
|
117
|
+
if (header.description) {
|
|
118
|
+
lines.push(...wrapText(header.description, opts.width, INDENT).map((l) => c.dim(l)));
|
|
119
|
+
}
|
|
120
|
+
let table;
|
|
121
|
+
if (header.layout.columns === 2) {
|
|
122
|
+
table = opts.compact
|
|
123
|
+
? renderDualRowCompact(header, opts) ?? renderPinList(header, opts)
|
|
124
|
+
: renderDualRowTable(header, opts) ??
|
|
125
|
+
renderDualRowCompact(header, opts) ??
|
|
126
|
+
renderPinList(header, opts);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
table = opts.compact ? renderPinList(header, opts) : renderSingleColumnTable(header, opts) ?? renderPinList(header, opts);
|
|
130
|
+
}
|
|
131
|
+
lines.push(...table);
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
function renderLegend(board, opts) {
|
|
135
|
+
const { chalk: c } = opts;
|
|
136
|
+
if (c.level === 0)
|
|
137
|
+
return [];
|
|
138
|
+
const order = [
|
|
139
|
+
"power",
|
|
140
|
+
"ground",
|
|
141
|
+
"gpio",
|
|
142
|
+
"digital",
|
|
143
|
+
"analog",
|
|
144
|
+
"communication",
|
|
145
|
+
"reserved",
|
|
146
|
+
"nc",
|
|
147
|
+
];
|
|
148
|
+
const present = order.filter((cat) => board.headers.some((h) => h.pins.some((p) => p.category === cat)));
|
|
149
|
+
const swatches = present.map((cat) => {
|
|
150
|
+
const sample = board.headers
|
|
151
|
+
.flatMap((h) => h.pins)
|
|
152
|
+
.find((p) => p.category === cat);
|
|
153
|
+
return pinStyle(c, sample)(cat);
|
|
154
|
+
});
|
|
155
|
+
return [c.dim("Legend: ") + swatches.join(c.dim(" · "))];
|
|
156
|
+
}
|
|
157
|
+
export function renderWarnings(board, opts) {
|
|
158
|
+
const { chalk: c, chars: ch } = opts;
|
|
159
|
+
return board.warnings.flatMap((warning) => {
|
|
160
|
+
const marker = warning.severity === "info" ? ch.info : ch.warn;
|
|
161
|
+
const style = warningStyle(c, warning.severity);
|
|
162
|
+
return wrapText(`${marker} ${warning.text}`, opts.width, "").map((line, i) => style(i === 0 ? line : ` ${line}`));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export function renderBoard(board, opts) {
|
|
166
|
+
const { chalk: c, chars: ch } = opts;
|
|
167
|
+
const lines = [];
|
|
168
|
+
lines.push(`${c.bold(board.name)} ${c.dim(`— ${board.manufacturer}`)}`);
|
|
169
|
+
const primarySource = board.sources.find((s) => s.official) ?? board.sources[0];
|
|
170
|
+
if (primarySource) {
|
|
171
|
+
const tag = primarySource.official ? "official" : "third-party";
|
|
172
|
+
lines.push(c.dim(`Source: ${primarySource.title} (${tag}) — see ph ${board.id} --source`));
|
|
173
|
+
}
|
|
174
|
+
if (board.revisionNote) {
|
|
175
|
+
lines.push(...wrapText(`${ch.info} ${board.revisionNote}`, opts.width, "").map((line, i) => c.dim(i === 0 ? line : ` ${line}`)));
|
|
176
|
+
}
|
|
177
|
+
lines.push(...renderWarnings(board, opts));
|
|
178
|
+
lines.push("");
|
|
179
|
+
board.headers.forEach((header, index) => {
|
|
180
|
+
if (index > 0)
|
|
181
|
+
lines.push("");
|
|
182
|
+
lines.push(...renderHeader(header, opts));
|
|
183
|
+
});
|
|
184
|
+
const legend = renderLegend(board, opts);
|
|
185
|
+
if (legend.length > 0) {
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push(...legend);
|
|
188
|
+
}
|
|
189
|
+
const hasReserved = board.headers.some((h) => h.pins.some((p) => p.category === "reserved"));
|
|
190
|
+
if (hasReserved) {
|
|
191
|
+
lines.push(c.dim("* reserved / control pin — see warnings above"));
|
|
192
|
+
}
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type CharSet = {
|
|
2
|
+
topLeft: string;
|
|
3
|
+
topRight: string;
|
|
4
|
+
bottomLeft: string;
|
|
5
|
+
bottomRight: string;
|
|
6
|
+
horizontal: string;
|
|
7
|
+
vertical: string;
|
|
8
|
+
topJoin: string;
|
|
9
|
+
bottomJoin: string;
|
|
10
|
+
warn: string;
|
|
11
|
+
info: string;
|
|
12
|
+
bullet: string;
|
|
13
|
+
pairSeparator: string;
|
|
14
|
+
};
|
|
15
|
+
export declare const unicodeChars: CharSet;
|
|
16
|
+
export declare const asciiChars: CharSet;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const unicodeChars = {
|
|
2
|
+
topLeft: "┌",
|
|
3
|
+
topRight: "┐",
|
|
4
|
+
bottomLeft: "└",
|
|
5
|
+
bottomRight: "┘",
|
|
6
|
+
horizontal: "─",
|
|
7
|
+
vertical: "│",
|
|
8
|
+
topJoin: "┬",
|
|
9
|
+
bottomJoin: "┴",
|
|
10
|
+
warn: "⚠",
|
|
11
|
+
info: "ℹ",
|
|
12
|
+
bullet: "•",
|
|
13
|
+
pairSeparator: "│",
|
|
14
|
+
};
|
|
15
|
+
export const asciiChars = {
|
|
16
|
+
topLeft: "+",
|
|
17
|
+
topRight: "+",
|
|
18
|
+
bottomLeft: "+",
|
|
19
|
+
bottomRight: "+",
|
|
20
|
+
horizontal: "-",
|
|
21
|
+
vertical: "|",
|
|
22
|
+
topJoin: "+",
|
|
23
|
+
bottomJoin: "+",
|
|
24
|
+
warn: "!",
|
|
25
|
+
info: "i",
|
|
26
|
+
bullet: "*",
|
|
27
|
+
pairSeparator: "|",
|
|
28
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ChalkInstance } from "chalk";
|
|
2
|
+
import type { Board } from "../model.js";
|
|
3
|
+
import type { CharSet } from "./chars.js";
|
|
4
|
+
export declare function renderBoardTable(boardList: Board[], c: ChalkInstance): string;
|
|
5
|
+
export declare function renderSources(board: Board, c: ChalkInstance): string;
|
|
6
|
+
export declare function renderInfo(board: Board, c: ChalkInstance, ch: CharSet, width: number): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { warningStyle } from "./theme.js";
|
|
2
|
+
import { wrapText } from "./board.js";
|
|
3
|
+
export function renderBoardTable(boardList, c) {
|
|
4
|
+
const rows = boardList.map((board) => ({
|
|
5
|
+
id: board.id,
|
|
6
|
+
name: board.name,
|
|
7
|
+
manufacturer: board.manufacturer,
|
|
8
|
+
pins: String(board.headers.reduce((sum, h) => sum + h.pins.length, 0)),
|
|
9
|
+
}));
|
|
10
|
+
const idWidth = Math.max(...rows.map((r) => r.id.length), 2);
|
|
11
|
+
const nameWidth = Math.max(...rows.map((r) => r.name.length), 5);
|
|
12
|
+
const mfrWidth = Math.max(...rows.map((r) => r.manufacturer.length), 12);
|
|
13
|
+
const lines = [
|
|
14
|
+
c.dim(`${"ID".padEnd(idWidth)} ${"BOARD".padEnd(nameWidth)} ${"MANUFACTURER".padEnd(mfrWidth)} PINS`),
|
|
15
|
+
...rows.map((r) => `${c.bold(r.id.padEnd(idWidth))} ${r.name.padEnd(nameWidth)} ${r.manufacturer.padEnd(mfrWidth)} ${r.pins.padStart(4)}`),
|
|
16
|
+
];
|
|
17
|
+
lines.push("");
|
|
18
|
+
lines.push(c.dim("Show a pinout with: ph <id> e.g. ph rpi5"));
|
|
19
|
+
return lines.join("\n");
|
|
20
|
+
}
|
|
21
|
+
export function renderSources(board, c) {
|
|
22
|
+
const lines = [`${c.bold(board.name)} ${c.dim("— sources")}`];
|
|
23
|
+
for (const source of board.sources) {
|
|
24
|
+
const tag = source.official ? c.green("(official)") : c.yellow("(third-party)");
|
|
25
|
+
lines.push(` ${source.title} ${tag}`);
|
|
26
|
+
lines.push(c.dim(` ${source.url}`));
|
|
27
|
+
}
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
}
|
|
30
|
+
export function renderInfo(board, c, ch, width) {
|
|
31
|
+
const lines = [];
|
|
32
|
+
lines.push(`${c.bold(board.name)} ${c.dim(`— ${board.manufacturer}`)}`);
|
|
33
|
+
if (board.description)
|
|
34
|
+
lines.push(...wrapText(board.description, width, ""));
|
|
35
|
+
if (board.revisionNote) {
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push(...wrapText(`Revision note: ${board.revisionNote}`, width, "").map((l) => c.dim(l)));
|
|
38
|
+
}
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push(c.bold("Headers"));
|
|
41
|
+
for (const header of board.headers) {
|
|
42
|
+
lines.push(` ${ch.bullet} ${header.name} — ${header.pins.length} pins`);
|
|
43
|
+
}
|
|
44
|
+
lines.push("");
|
|
45
|
+
lines.push(c.bold("Warnings"));
|
|
46
|
+
for (const warning of board.warnings) {
|
|
47
|
+
const marker = warning.severity === "info" ? ch.info : ch.warn;
|
|
48
|
+
const style = warningStyle(c, warning.severity);
|
|
49
|
+
lines.push(...wrapText(`${marker} ${warning.text}`, width, " ").map((l) => style(l)));
|
|
50
|
+
}
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push(c.bold("Sources"));
|
|
53
|
+
for (const source of board.sources) {
|
|
54
|
+
const tag = source.official ? c.green("(official)") : c.yellow("(third-party)");
|
|
55
|
+
lines.push(` ${ch.bullet} ${source.title} ${tag}`);
|
|
56
|
+
lines.push(c.dim(` ${source.url}`));
|
|
57
|
+
}
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push(c.dim(`Aliases: ${board.aliases.join(", ")}`));
|
|
60
|
+
lines.push(c.dim(`Pinout: ph ${board.id}`));
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ChalkInstance } from "chalk";
|
|
2
|
+
import type { Pin, WarningSeverity } from "../model.js";
|
|
3
|
+
export declare function makeChalk(colorEnabled: boolean): ChalkInstance;
|
|
4
|
+
/**
|
|
5
|
+
* Category colors. Color is never the only signal: labels and category names
|
|
6
|
+
* stay in the output so meaning survives `--no-color` and piping.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pinStyle(c: ChalkInstance, pin: Pin): (text: string) => string;
|
|
9
|
+
export declare function warningStyle(c: ChalkInstance, severity: WarningSeverity): (text: string) => string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Chalk } from "chalk";
|
|
2
|
+
export function makeChalk(colorEnabled) {
|
|
3
|
+
return new Chalk({ level: colorEnabled ? 3 : 0 });
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Category colors. Color is never the only signal: labels and category names
|
|
7
|
+
* stay in the output so meaning survives `--no-color` and piping.
|
|
8
|
+
*/
|
|
9
|
+
export function pinStyle(c, pin) {
|
|
10
|
+
switch (pin.category) {
|
|
11
|
+
case "power":
|
|
12
|
+
return pin.voltage?.startsWith("5") ? c.red : c.yellow;
|
|
13
|
+
case "ground":
|
|
14
|
+
return c.gray;
|
|
15
|
+
case "gpio":
|
|
16
|
+
case "digital":
|
|
17
|
+
return c.green;
|
|
18
|
+
case "analog":
|
|
19
|
+
return c.blue;
|
|
20
|
+
case "communication":
|
|
21
|
+
return c.cyan;
|
|
22
|
+
case "reserved":
|
|
23
|
+
return c.magenta;
|
|
24
|
+
case "nc":
|
|
25
|
+
return c.dim;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function warningStyle(c, severity) {
|
|
29
|
+
switch (severity) {
|
|
30
|
+
case "danger":
|
|
31
|
+
return c.red.bold;
|
|
32
|
+
case "warning":
|
|
33
|
+
return c.yellow;
|
|
34
|
+
case "info":
|
|
35
|
+
return c.dim;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type RunOptions = {
|
|
2
|
+
/** Override detected terminal width (defaults to stdout columns or 80). */
|
|
3
|
+
columns?: number;
|
|
4
|
+
/** Override TTY detection (piped output disables color automatically). */
|
|
5
|
+
isTTY?: boolean;
|
|
6
|
+
/** Override environment (for NO_COLOR handling in tests). */
|
|
7
|
+
env?: Record<string, string | undefined>;
|
|
8
|
+
};
|
|
9
|
+
export type RunResult = {
|
|
10
|
+
code: number;
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Runs the CLI against captured buffers instead of process streams, so the
|
|
16
|
+
* whole command surface is testable without spawning child processes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runCli(argv: string[], runOpts?: RunOptions): Promise<RunResult>;
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Command, CommanderError } from "commander";
|
|
2
|
+
import { boards, resolveBoard, searchBoards, suggestBoards } from "./catalog.js";
|
|
3
|
+
import { VERSION } from "./version.js";
|
|
4
|
+
import { asciiChars, unicodeChars } from "./render/chars.js";
|
|
5
|
+
import { makeChalk } from "./render/theme.js";
|
|
6
|
+
import { renderBoard } from "./render/board.js";
|
|
7
|
+
import { renderBoardTable, renderInfo, renderSources } from "./render/meta.js";
|
|
8
|
+
/**
|
|
9
|
+
* Runs the CLI against captured buffers instead of process streams, so the
|
|
10
|
+
* whole command surface is testable without spawning child processes.
|
|
11
|
+
*/
|
|
12
|
+
export async function runCli(argv, runOpts = {}) {
|
|
13
|
+
let stdout = "";
|
|
14
|
+
let stderr = "";
|
|
15
|
+
let code = 0;
|
|
16
|
+
const print = (text) => {
|
|
17
|
+
stdout += text + "\n";
|
|
18
|
+
};
|
|
19
|
+
const printErr = (text) => {
|
|
20
|
+
stderr += text + "\n";
|
|
21
|
+
};
|
|
22
|
+
const env = runOpts.env ?? process.env;
|
|
23
|
+
const isTTY = runOpts.isTTY ?? Boolean(process.stdout.isTTY);
|
|
24
|
+
const detectedColumns = runOpts.columns ?? (isTTY ? process.stdout.columns : undefined) ?? 80;
|
|
25
|
+
const colorEnabled = (flags) => flags.color !== false && !env.NO_COLOR && isTTY;
|
|
26
|
+
const buildRender = (flags) => {
|
|
27
|
+
let width = detectedColumns;
|
|
28
|
+
if (flags.width !== undefined) {
|
|
29
|
+
const parsed = Number.parseInt(flags.width, 10);
|
|
30
|
+
if (!Number.isFinite(parsed) || parsed < 40) {
|
|
31
|
+
return { error: "Invalid --width: expected a number of at least 40." };
|
|
32
|
+
}
|
|
33
|
+
width = parsed;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
chalk: makeChalk(colorEnabled(flags)),
|
|
37
|
+
chars: flags.ascii ? asciiChars : unicodeChars,
|
|
38
|
+
width,
|
|
39
|
+
compact: Boolean(flags.compact),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const resolveOrSuggest = (words) => {
|
|
43
|
+
const query = words.join(" ");
|
|
44
|
+
const board = resolveBoard(query);
|
|
45
|
+
if (board)
|
|
46
|
+
return board;
|
|
47
|
+
printErr(`Board "${query}" was not found.`);
|
|
48
|
+
const suggestions = suggestBoards(query);
|
|
49
|
+
if (suggestions.length > 0) {
|
|
50
|
+
printErr("");
|
|
51
|
+
printErr("Did you mean:");
|
|
52
|
+
for (const suggestion of suggestions) {
|
|
53
|
+
printErr(` ph ${suggestion.alias}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
printErr("");
|
|
57
|
+
printErr("Run `ph list` to see all supported boards.");
|
|
58
|
+
code = 1;
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
61
|
+
const showBoard = (words, flags) => {
|
|
62
|
+
const board = resolveOrSuggest(words);
|
|
63
|
+
if (!board)
|
|
64
|
+
return;
|
|
65
|
+
if (flags.json) {
|
|
66
|
+
// JSON mode must emit only valid JSON to stdout.
|
|
67
|
+
print(JSON.stringify(board, null, 2));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const render = buildRender(flags);
|
|
71
|
+
if ("error" in render) {
|
|
72
|
+
printErr(render.error);
|
|
73
|
+
code = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (flags.source) {
|
|
77
|
+
print(renderSources(board, render.chalk));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
print(renderBoard(board, render));
|
|
81
|
+
};
|
|
82
|
+
const metaChalk = (flags = {}) => makeChalk(colorEnabled(flags));
|
|
83
|
+
const metaChars = (flags = {}) => (flags.ascii ? asciiChars : unicodeChars);
|
|
84
|
+
const program = new Command();
|
|
85
|
+
program
|
|
86
|
+
.name("ph")
|
|
87
|
+
.description("Hardware board pinout diagrams in your terminal.")
|
|
88
|
+
.version(VERSION, "-v, --version", "print the version")
|
|
89
|
+
.helpCommand("help [command]", "display help for a command")
|
|
90
|
+
.exitOverride()
|
|
91
|
+
.configureOutput({
|
|
92
|
+
writeOut: (text) => {
|
|
93
|
+
stdout += text;
|
|
94
|
+
},
|
|
95
|
+
writeErr: (text) => {
|
|
96
|
+
stderr += text;
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
.argument("[board...]", "board id or alias (e.g. rpi5, pico, uno, esp32)")
|
|
100
|
+
.option("--compact", "tighter, borderless layout")
|
|
101
|
+
.option("--ascii", "ASCII-only borders and symbols")
|
|
102
|
+
.option("--no-color", "disable ANSI colors (the NO_COLOR env var is also honored)")
|
|
103
|
+
.option("--json", "emit the board data as JSON")
|
|
104
|
+
.option("--source", "print official source links for the board")
|
|
105
|
+
.option("--width <number>", "override the detected terminal width")
|
|
106
|
+
.addHelpText("after", "\nExamples:\n ph rpi5\n ph raspberry pi 5\n ph pico --compact\n ph esp32 --ascii --no-color\n ph uno --json\n ph rpi5 --source")
|
|
107
|
+
.action((words, flags) => {
|
|
108
|
+
if (words.length === 0) {
|
|
109
|
+
program.outputHelp();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
showBoard(words, flags);
|
|
113
|
+
});
|
|
114
|
+
program
|
|
115
|
+
.command("list")
|
|
116
|
+
.description("list all boards in the catalog")
|
|
117
|
+
.action(() => {
|
|
118
|
+
print(renderBoardTable(boards, metaChalk()));
|
|
119
|
+
});
|
|
120
|
+
program
|
|
121
|
+
.command("search <query...>")
|
|
122
|
+
.description("search boards by name, alias, or manufacturer")
|
|
123
|
+
.action((words) => {
|
|
124
|
+
const query = words.join(" ");
|
|
125
|
+
const results = searchBoards(query);
|
|
126
|
+
if (results.length === 0) {
|
|
127
|
+
printErr(`No boards matched "${query}". Run \`ph list\` to see all boards.`);
|
|
128
|
+
code = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
print(renderBoardTable(results, metaChalk()));
|
|
132
|
+
});
|
|
133
|
+
program
|
|
134
|
+
.command("info <board...>")
|
|
135
|
+
.description("show board details, warnings, sources, and aliases")
|
|
136
|
+
.option("--ascii", "ASCII-only symbols")
|
|
137
|
+
.option("--json", "emit the board data as JSON")
|
|
138
|
+
.action((words, localFlags, command) => {
|
|
139
|
+
// Flags like --json are also declared on the root command, which
|
|
140
|
+
// consumes them before dispatch; merge both option scopes.
|
|
141
|
+
const flags = command.optsWithGlobals();
|
|
142
|
+
void localFlags;
|
|
143
|
+
const board = resolveOrSuggest(words);
|
|
144
|
+
if (!board)
|
|
145
|
+
return;
|
|
146
|
+
if (flags.json) {
|
|
147
|
+
print(JSON.stringify(board, null, 2));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
print(renderInfo(board, metaChalk(flags), metaChars(flags), detectedColumns));
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
await program.parseAsync(argv, { from: "user" });
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error instanceof CommanderError) {
|
|
157
|
+
// Help/version display exits with 0; user mistakes exit non-zero,
|
|
158
|
+
// without a stack trace either way.
|
|
159
|
+
code = error.exitCode;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
printErr(error instanceof Error ? error.message : String(error));
|
|
163
|
+
code = 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { code, stdout, stderr };
|
|
167
|
+
}
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dheerajsom/pinhub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Hardware board pinout diagrams in your terminal. `ph rpi5` and go.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pinout",
|
|
7
|
+
"gpio",
|
|
8
|
+
"raspberry-pi",
|
|
9
|
+
"arduino",
|
|
10
|
+
"esp32",
|
|
11
|
+
"pico",
|
|
12
|
+
"hardware",
|
|
13
|
+
"cli",
|
|
14
|
+
"terminal"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"ph": "dist/cli.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"SETUP.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.18.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "tsx src/cli.ts",
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"lint": "eslint .",
|
|
38
|
+
"package:check": "npm pack --dry-run",
|
|
39
|
+
"prepublishOnly": "npm run lint && npm run test && npm run build"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^5.4.1",
|
|
43
|
+
"commander": "^13.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@eslint/js": "^9.20.0",
|
|
47
|
+
"@types/node": "^22.13.0",
|
|
48
|
+
"eslint": "^9.20.0",
|
|
49
|
+
"tsx": "^4.19.0",
|
|
50
|
+
"typescript": "^5.7.3",
|
|
51
|
+
"typescript-eslint": "^8.24.0",
|
|
52
|
+
"vitest": "^3.0.5"
|
|
53
|
+
}
|
|
54
|
+
}
|