@pcbjs/cli 0.0.0 → 0.0.1-next.9029beb783788cfbb471cc9d57e1b47864c6eaa6
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/LICENSE +21 -0
- package/dist/boards.d.ts +92 -0
- package/dist/boards.js +126 -0
- package/dist/bridge.d.ts +53 -0
- package/dist/bridge.js +147 -0
- package/dist/build.d.ts +21 -0
- package/dist/build.js +165 -0
- package/dist/check.d.ts +83 -0
- package/dist/check.js +261 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +278 -0
- package/dist/config.d.ts +70 -0
- package/dist/config.js +141 -0
- package/dist/create.d.ts +18 -0
- package/dist/create.js +73 -0
- package/dist/dev-reuse.d.ts +36 -0
- package/dist/dev-reuse.js +51 -0
- package/dist/docs.d.ts +21 -0
- package/dist/docs.js +101 -0
- package/dist/doctor.d.ts +31 -0
- package/dist/doctor.js +151 -0
- package/dist/drafts.d.ts +47 -0
- package/dist/drafts.js +113 -0
- package/dist/fetch-model-child.d.ts +1 -0
- package/dist/fetch-model-child.js +12 -0
- package/dist/fetch-model.d.ts +32 -0
- package/dist/fetch-model.js +177 -0
- package/dist/host-client.d.ts +60 -0
- package/dist/host-client.js +182 -0
- package/dist/ide.d.ts +38 -0
- package/dist/ide.js +119 -0
- package/dist/import.d.ts +9 -0
- package/dist/import.js +120 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/inspect-sch.d.ts +6 -0
- package/dist/inspect-sch.js +28 -0
- package/dist/inspect.d.ts +259 -0
- package/dist/inspect.js +451 -0
- package/dist/lsp.d.ts +25 -0
- package/dist/lsp.js +104 -0
- package/dist/model-child.d.ts +1 -0
- package/dist/model-child.js +26 -0
- package/dist/model.d.ts +101 -0
- package/dist/model.js +634 -0
- package/dist/models.d.ts +28 -0
- package/dist/models.js +236 -0
- package/dist/plugin.d.ts +59 -0
- package/dist/plugin.js +920 -0
- package/dist/prerender.d.ts +18 -0
- package/dist/prerender.js +79 -0
- package/dist/preview.d.ts +33 -0
- package/dist/preview.js +100 -0
- package/dist/project.d.ts +38 -0
- package/dist/project.js +154 -0
- package/dist/reference-child.d.ts +1 -0
- package/dist/reference-child.js +15 -0
- package/dist/references.d.ts +42 -0
- package/dist/references.js +328 -0
- package/dist/repo.d.ts +11 -0
- package/dist/repo.js +64 -0
- package/dist/scope-plugin.d.ts +2 -0
- package/dist/scope-plugin.js +22 -0
- package/dist/scope-state.d.ts +70 -0
- package/dist/scope-state.js +42 -0
- package/dist/scopes.d.ts +27 -0
- package/dist/scopes.js +96 -0
- package/dist/server.d.ts +55 -0
- package/dist/server.js +141 -0
- package/dist/shot.d.ts +74 -0
- package/dist/shot.js +300 -0
- package/dist/skills.d.ts +5 -0
- package/dist/skills.js +38 -0
- package/dist/source-ranges.d.ts +29 -0
- package/dist/source-ranges.js +261 -0
- package/dist/typecheck.d.ts +17 -0
- package/dist/typecheck.js +66 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +12 -0
- package/dist/vite.d.ts +10 -0
- package/dist/vite.js +71 -0
- package/dist/workspace.d.ts +11 -0
- package/dist/workspace.js +53 -0
- package/docs/3d.md +100 -0
- package/docs/autorouter.md +108 -0
- package/docs/board.md +142 -0
- package/docs/circuits.md +140 -0
- package/docs/cli.md +158 -0
- package/docs/design.md +69 -0
- package/docs/development.md +94 -0
- package/docs/editor.md +112 -0
- package/docs/fab-set.md +87 -0
- package/docs/fab.md +99 -0
- package/docs/findings.md +86 -0
- package/docs/fonts.md +93 -0
- package/docs/highlight.md +45 -0
- package/docs/ide-bridge.md +145 -0
- package/docs/import.md +94 -0
- package/docs/inspect.md +25 -0
- package/docs/kicad.md +93 -0
- package/docs/layout.md +137 -0
- package/docs/length.md +37 -0
- package/docs/models.md +40 -0
- package/docs/pairs.md +126 -0
- package/docs/pours.md +74 -0
- package/docs/quickstart.md +126 -0
- package/docs/references.md +88 -0
- package/docs/routing.md +146 -0
- package/docs/rules.md +118 -0
- package/docs/schematic-graphic.md +137 -0
- package/docs/schematic.md +172 -0
- package/docs/skills.md +42 -0
- package/docs/sourcing.md +68 -0
- package/docs/stackup-and-rules.md +144 -0
- package/docs/stdlib.md +176 -0
- package/docs/viewer.md +107 -0
- package/docs/workspace.md +53 -0
- package/package.json +67 -3
- package/templates/board/README.md +14 -0
- package/templates/board/_gitignore +2 -0
- package/templates/board/package.json +16 -0
- package/templates/board/pcb.config.ts +8 -0
- package/templates/board/pcb.tsx +35 -0
- package/templates/board/tsconfig.json +15 -0
- package/vscode/manifest.json +9 -0
- package/vscode/vscode-pcbjs-0.0.1000.vsix +0 -0
- package/vscode/vscode-pcbjs.vsix +0 -0
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type Logger, type LogLevel, type ViteDevServer } from 'vite';
|
|
2
|
+
import type { ResolvedConfig } from './config.js';
|
|
3
|
+
import type { PluginState } from './plugin.js';
|
|
4
|
+
import { type Compiled } from './project.js';
|
|
5
|
+
import { type BoardSlot } from './boards.js';
|
|
6
|
+
import { counts, type Finding } from '@pcbjs/export/findings';
|
|
7
|
+
import { countModels } from '@pcbjs/core/model-source';
|
|
8
|
+
export interface OutputOptions {
|
|
9
|
+
/** summary lines and findings only */
|
|
10
|
+
quiet?: boolean;
|
|
11
|
+
/** Vite's own logging and the stack of a compile error */
|
|
12
|
+
verbose?: boolean;
|
|
13
|
+
json?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Silent unless --verbose or the project config asks for one. */
|
|
16
|
+
export declare function logLevelFor(cfg: ResolvedConfig, verbose?: boolean): 'info' | 'warn' | 'error' | 'silent';
|
|
17
|
+
/** Without Vite's report of a module that fails to evaluate: pcbjs prints the one line, the stack with --verbose. */
|
|
18
|
+
export declare function quietLogger(level: LogLevel): Logger;
|
|
19
|
+
/** No HTTP, no HMR socket, no file watcher unless `watch`. */
|
|
20
|
+
export declare function compileServer(cfg: ResolvedConfig, state: PluginState, o?: {
|
|
21
|
+
verbose?: boolean;
|
|
22
|
+
watch?: boolean;
|
|
23
|
+
}): Promise<ViteDevServer>;
|
|
24
|
+
/** One line, then with --verbose the code frame and the stack below it. */
|
|
25
|
+
export declare function errorText(e: any, verbose?: boolean, root?: string): {
|
|
26
|
+
line: string;
|
|
27
|
+
detail: string[];
|
|
28
|
+
};
|
|
29
|
+
/** Keeps on after a failure; the error of a failed one goes in `slot.error`. */
|
|
30
|
+
export declare function compileSlots(server: ViteDevServer, cfg: ResolvedConfig, slots: BoardSlot[], errors: Map<BoardSlot, unknown>): Promise<void>;
|
|
31
|
+
/** One per target of a board with variants. */
|
|
32
|
+
export interface ReportBoard {
|
|
33
|
+
name: string;
|
|
34
|
+
target: string | null;
|
|
35
|
+
entry: string;
|
|
36
|
+
status: 'ok' | 'fail' | 'error';
|
|
37
|
+
stats: Compiled['board']['stats'] | null;
|
|
38
|
+
/** phase timings in ms when the compiler records them */
|
|
39
|
+
timings: unknown;
|
|
40
|
+
/** absent on a board without automatic routes */
|
|
41
|
+
routing?: Compiled['board']['routing'];
|
|
42
|
+
ms: number | null;
|
|
43
|
+
counts: ReturnType<typeof counts> | null;
|
|
44
|
+
/** see docs/schematic-graphic.md */
|
|
45
|
+
schematic?: import('@pcbjs/core/sch-model').SchStats;
|
|
46
|
+
/** parts by where their 3D body comes from */
|
|
47
|
+
models?: ReturnType<typeof countModels>;
|
|
48
|
+
drc: Finding[];
|
|
49
|
+
erc: Finding[];
|
|
50
|
+
fab: {
|
|
51
|
+
dir: string;
|
|
52
|
+
zip: string;
|
|
53
|
+
} | null;
|
|
54
|
+
pages: string[];
|
|
55
|
+
error?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface Report {
|
|
58
|
+
boards: ReportBoard[];
|
|
59
|
+
}
|
|
60
|
+
export declare function reportOf(cfg: ResolvedConfig, s: BoardSlot, e: unknown, extra?: {
|
|
61
|
+
fab?: (c: Compiled) => ReportBoard['fab'];
|
|
62
|
+
pages?: string[];
|
|
63
|
+
}): ReportBoard[];
|
|
64
|
+
export declare function writeReport(cfg: ResolvedConfig, file: string, report: Report): string;
|
|
65
|
+
/** A line per target, then the findings of every target merged, at most `cap` lines per rule; every one with `verbose`. */
|
|
66
|
+
export declare function slotLines(s: BoardSlot, e: unknown, o: {
|
|
67
|
+
root: string;
|
|
68
|
+
cap: number;
|
|
69
|
+
verbose?: boolean;
|
|
70
|
+
more?: string;
|
|
71
|
+
}): string[];
|
|
72
|
+
/** Every board's counts added up. */
|
|
73
|
+
export declare function totalLine(slots: BoardSlot[]): string;
|
|
74
|
+
export declare const slotsFailed: (slots: BoardSlot[]) => boolean;
|
|
75
|
+
/** Mark a failed entry's outputs stale: its board JSON gets `stale: true` and the error, each fab directory a STALE file. */
|
|
76
|
+
export declare function markStale(cfg: ResolvedConfig, entry: string, error: string): string[];
|
|
77
|
+
export declare const CHECK_CAP = 10;
|
|
78
|
+
/** No fab files and no site. With `watch`, recompile on every change and print the diff. */
|
|
79
|
+
export declare function check(opts: OutputOptions & {
|
|
80
|
+
cfg: ResolvedConfig;
|
|
81
|
+
state: PluginState;
|
|
82
|
+
watch?: boolean;
|
|
83
|
+
}): Promise<number>;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { createLogger, createServer } from 'vite';
|
|
4
|
+
import { viteConfig } from './vite.js';
|
|
5
|
+
import { compileEntry } from './project.js';
|
|
6
|
+
import { markConflicts } from './boards.js';
|
|
7
|
+
import { boardFindings, boardLine, counts, routingLines, diffFindings, diffLine, failing, findingLine, formatFindings, orderFindings } from '@pcbjs/export/findings';
|
|
8
|
+
import { variantSlug } from '@pcbjs/core/variants';
|
|
9
|
+
import { countModels } from '@pcbjs/core/model-source';
|
|
10
|
+
import { typecheck, typeErrorLine } from './typecheck.js';
|
|
11
|
+
/** Silent unless --verbose or the project config asks for one. */
|
|
12
|
+
export function logLevelFor(cfg, verbose) {
|
|
13
|
+
if (verbose)
|
|
14
|
+
return 'info';
|
|
15
|
+
return cfg.vite?.logLevel ?? 'silent';
|
|
16
|
+
}
|
|
17
|
+
/** Without Vite's report of a module that fails to evaluate: pcbjs prints the one line, the stack with --verbose. */
|
|
18
|
+
export function quietLogger(level) {
|
|
19
|
+
const logger = createLogger(level, { allowClearScreen: false });
|
|
20
|
+
const error = logger.error.bind(logger);
|
|
21
|
+
logger.error = (msg, o) => { if (!strip(msg).includes('Error when evaluating SSR module'))
|
|
22
|
+
error(msg, o); };
|
|
23
|
+
return logger;
|
|
24
|
+
}
|
|
25
|
+
/** No HTTP, no HMR socket, no file watcher unless `watch`. */
|
|
26
|
+
export async function compileServer(cfg, state, o = {}) {
|
|
27
|
+
const vc = viteConfig(cfg, state, 'build');
|
|
28
|
+
return createServer({
|
|
29
|
+
...vc, logLevel: logLevelFor(cfg, o.verbose), customLogger: quietLogger(logLevelFor(cfg, o.verbose)),
|
|
30
|
+
server: { ...vc.server, middlewareMode: true, ws: false, hmr: false, watch: o.watch ? {} : null },
|
|
31
|
+
optimizeDeps: { noDiscovery: true, include: [] }, appType: 'custom', mode: 'production',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const strip = (t) => t.replace(/\x1b\[[0-9;]*m/g, '');
|
|
35
|
+
/** One line, then with --verbose the code frame and the stack below it. */
|
|
36
|
+
export function errorText(e, verbose, root) {
|
|
37
|
+
const msg = strip(String(e?.message ?? e));
|
|
38
|
+
const first = msg.split('\n').find(l => l.trim()) ?? msg;
|
|
39
|
+
const rel = (f) => (root && f.startsWith(root + '/') ? f.slice(root.length + 1) : f);
|
|
40
|
+
// Vite's location of a syntax error, else the first frame of the stack in the project
|
|
41
|
+
let loc = e?.loc?.file ? `${rel(e.loc.file)}:${e.loc.line}:${e.loc.column}: ` : '';
|
|
42
|
+
if (!loc && root && !/\.(m?[jt]sx?):\d+:\d+/.test(first)) {
|
|
43
|
+
for (const l of strip(String(e?.stack ?? '')).split('\n')) {
|
|
44
|
+
const m = /^\s+at (?:.*\()?(?:file:\/\/)?(\/[^()]+?):(\d+):(\d+)\)?$/.exec(l);
|
|
45
|
+
if (m && m[1].startsWith(root + '/') && !m[1].includes('/node_modules/')) {
|
|
46
|
+
loc = `${rel(m[1])}:${m[2]}:${m[3]}: `;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const detail = [];
|
|
52
|
+
if (verbose) {
|
|
53
|
+
const rest = msg.split('\n').slice(msg.split('\n').indexOf(first) + 1).filter(l => l.trim());
|
|
54
|
+
detail.push(...rest);
|
|
55
|
+
if (e?.frame)
|
|
56
|
+
detail.push(...strip(String(e.frame)).split('\n'));
|
|
57
|
+
const stack = strip(String(e?.stack ?? '')).split('\n').filter(l => /^\s+at /.test(l));
|
|
58
|
+
detail.push(...stack);
|
|
59
|
+
}
|
|
60
|
+
return { line: `${loc}${first}`, detail };
|
|
61
|
+
}
|
|
62
|
+
/** Keeps on after a failure; the error of a failed one goes in `slot.error`. */
|
|
63
|
+
export async function compileSlots(server, cfg, slots, errors) {
|
|
64
|
+
for (const s of slots) {
|
|
65
|
+
try {
|
|
66
|
+
s.compiled = await compileEntry(server, cfg, s.entry, false);
|
|
67
|
+
s.error = null;
|
|
68
|
+
errors.delete(s);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
s.error = strip(String(e?.message ?? e));
|
|
72
|
+
s.compiled = null;
|
|
73
|
+
errors.set(s, e);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
markConflicts(slots);
|
|
77
|
+
}
|
|
78
|
+
export function reportOf(cfg, s, e, extra = {}) {
|
|
79
|
+
const rel = (p) => relative(cfg.root, p) || '.';
|
|
80
|
+
if (!s.compiled || s.conflict) {
|
|
81
|
+
const err = s.conflict ?? errorText(e, false, cfg.root).line;
|
|
82
|
+
return [{ name: s.entry.rel, target: null, entry: s.entry.rel, status: 'error', stats: null, timings: null, ms: null, counts: null, drc: [], erc: [], fab: null, pages: [], error: err }];
|
|
83
|
+
}
|
|
84
|
+
return s.compiled.map(c => {
|
|
85
|
+
const b = c.board;
|
|
86
|
+
const f = boardFindings(b, cfg.root);
|
|
87
|
+
const slug = b.variant ? variantSlug(b.variant.name) : null;
|
|
88
|
+
const pages = (extra.pages ?? []).filter(p => p === `board/${b.name}` || (slug ? p === `board/${b.name}/${slug}` : false));
|
|
89
|
+
const fab = extra.fab?.(c) ?? null;
|
|
90
|
+
return {
|
|
91
|
+
name: b.name, target: b.variant?.name ?? null, entry: c.entry.rel, status: failing(b) ? 'fail' : 'ok',
|
|
92
|
+
stats: b.stats, timings: b.timings ?? null, ...(b.routing ? { routing: b.routing } : {}), ms: Math.round(c.ms), counts: counts(b), ...(b.schematic.drawing ? { schematic: b.schematic.drawing.stats } : {}), models: countModels(c.site.footprints),
|
|
93
|
+
drc: orderFindings(f.filter(x => x.kind === 'drc')), erc: orderFindings(f.filter(x => x.kind === 'erc')),
|
|
94
|
+
fab: fab ? { dir: rel(fab.dir), zip: rel(fab.zip) } : null, pages,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export function writeReport(cfg, file, report) {
|
|
99
|
+
const abs = resolve(cfg.root, file);
|
|
100
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
101
|
+
writeFileSync(abs, JSON.stringify(report, null, 2) + '\n');
|
|
102
|
+
return relative(cfg.root, abs) || abs;
|
|
103
|
+
}
|
|
104
|
+
/** A line per target, then the findings of every target merged, at most `cap` lines per rule; every one with `verbose`. */
|
|
105
|
+
export function slotLines(s, e, o) {
|
|
106
|
+
if (s.conflict)
|
|
107
|
+
return [`${s.entry.rel} error: ${s.conflict}`];
|
|
108
|
+
if (!s.compiled) {
|
|
109
|
+
const t = errorText(e, o.verbose, o.root);
|
|
110
|
+
return [`${s.entry.rel} compile error: ${t.line}`, ...t.detail.map(l => ` ${l}`)];
|
|
111
|
+
}
|
|
112
|
+
const out = s.compiled.map(c => boardLine(c.board, c.entry.rel, c.ms));
|
|
113
|
+
out.push(...routingLines(s.compiled.map(c => c.board)));
|
|
114
|
+
const targets = s.compiled.map(c => ({ target: c.board.variant?.name, findings: boardFindings(c.board, o.root) }));
|
|
115
|
+
out.push(...formatFindings(targets, { cap: o.cap, more: o.more, all: o.verbose }));
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/** Every board's counts added up. */
|
|
119
|
+
export function totalLine(slots) {
|
|
120
|
+
let de = 0, dw = 0, un = 0, ee = 0, ew = 0, bad = 0, failed = 0;
|
|
121
|
+
for (const s of slots) {
|
|
122
|
+
if (!s.compiled || s.conflict) {
|
|
123
|
+
failed++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const c of s.compiled) {
|
|
127
|
+
const k = counts(c.board);
|
|
128
|
+
de += k.drcErrors;
|
|
129
|
+
dw += k.drcWarnings;
|
|
130
|
+
un += k.unconnected;
|
|
131
|
+
ee += k.ercErrors;
|
|
132
|
+
ew += k.ercWarnings;
|
|
133
|
+
if (failing(c.board))
|
|
134
|
+
bad++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const ok = !bad && !failed;
|
|
138
|
+
const n = (k, w) => `${k} ${w}${k === 1 ? '' : 's'}`;
|
|
139
|
+
return `${ok ? 'ok ' : 'FAIL'} DRC ${n(de, 'error')}, ${n(dw, 'warning')}, ${un} unconnected ERC ${n(ee, 'error')}, ${n(ew, 'warning')}${failed ? ` ${failed} entr${failed === 1 ? 'y' : 'ies'} failed to compile` : ''}`;
|
|
140
|
+
}
|
|
141
|
+
export const slotsFailed = (slots) => slots.some(s => !s.compiled || s.conflict || s.compiled.some(c => failing(c.board)));
|
|
142
|
+
/** Mark a failed entry's outputs stale: its board JSON gets `stale: true` and the error, each fab directory a STALE file. */
|
|
143
|
+
export function markStale(cfg, entry, error) {
|
|
144
|
+
const index = join(cfg.outDir, 'boards.json');
|
|
145
|
+
if (!existsSync(index))
|
|
146
|
+
return [];
|
|
147
|
+
let boards = [];
|
|
148
|
+
try {
|
|
149
|
+
boards = JSON.parse(readFileSync(index, 'utf8')).boards ?? [];
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const marked = [];
|
|
155
|
+
const note = `STALE: the last compile of ${entry} failed, these files are from an earlier build.\n${error}\n`;
|
|
156
|
+
for (const b of boards.filter(b => b.entry === entry)) {
|
|
157
|
+
const file = join(cfg.outDir, ...b.file.split('/').map(decodeURIComponent));
|
|
158
|
+
if (existsSync(file)) {
|
|
159
|
+
try {
|
|
160
|
+
const data = JSON.parse(readFileSync(file, 'utf8'));
|
|
161
|
+
writeFileSync(file, JSON.stringify({ ...data, stale: true, staleError: error }));
|
|
162
|
+
marked.push(relative(cfg.root, file));
|
|
163
|
+
}
|
|
164
|
+
catch { /* not ours */ }
|
|
165
|
+
}
|
|
166
|
+
const segs = [b.name, ...(b.variant ? [variantSlug(b.variant)] : [])];
|
|
167
|
+
for (const dir of [join(cfg.fabDir, ...segs), join(cfg.outDir, 'fab', ...segs)]) {
|
|
168
|
+
if (!existsSync(dir))
|
|
169
|
+
continue;
|
|
170
|
+
writeFileSync(join(dir, 'STALE'), note);
|
|
171
|
+
marked.push(relative(cfg.root, join(dir, 'STALE')));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return marked;
|
|
175
|
+
}
|
|
176
|
+
export const CHECK_CAP = 10;
|
|
177
|
+
/** No fab files and no site. With `watch`, recompile on every change and print the diff. */
|
|
178
|
+
export async function check(opts) {
|
|
179
|
+
const { cfg, state } = opts;
|
|
180
|
+
const server = await compileServer(cfg, state, { verbose: opts.verbose, watch: opts.watch });
|
|
181
|
+
const errors = new Map();
|
|
182
|
+
const run = async () => {
|
|
183
|
+
await compileSlots(server, cfg, state.slots, errors);
|
|
184
|
+
const now = new Map();
|
|
185
|
+
for (const s of state.slots)
|
|
186
|
+
if (s.compiled && !s.conflict)
|
|
187
|
+
for (const c of s.compiled)
|
|
188
|
+
now.set(`${c.board.name}|${c.board.variant?.name ?? ''}`, boardFindings(c.board, cfg.root));
|
|
189
|
+
return now;
|
|
190
|
+
};
|
|
191
|
+
const print = () => {
|
|
192
|
+
for (const s of state.slots)
|
|
193
|
+
for (const l of slotLines(s, errors.get(s), { root: cfg.root, cap: CHECK_CAP, verbose: opts.verbose, more: opts.json ? `all in ${opts.json}` : 'all with --verbose or --json <file>' }))
|
|
194
|
+
console.log(l);
|
|
195
|
+
console.log(totalLine(state.slots));
|
|
196
|
+
if (opts.json)
|
|
197
|
+
console.log(`json ${writeReport(cfg, opts.json, { boards: state.slots.flatMap(s => reportOf(cfg, s, errors.get(s))) })}`);
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
let last = await run();
|
|
201
|
+
print();
|
|
202
|
+
// cached per file, see typecheck.ts
|
|
203
|
+
const tc = typecheck(cfg.root, cfg.entries.map(e => e.file));
|
|
204
|
+
if (tc) {
|
|
205
|
+
console.log(`typecheck ${tc.errors.length ? `${tc.errors.length} error${tc.errors.length === 1 ? '' : 's'}` : 'ok'} ${Math.round(tc.ms)} ms${tc.cached ? ', cached' : ''}`);
|
|
206
|
+
for (const e of tc.errors.slice(0, 20))
|
|
207
|
+
console.log(typeErrorLine(e, opts.verbose));
|
|
208
|
+
if (tc.errors.length > 20)
|
|
209
|
+
console.log(` and ${tc.errors.length - 20} more, all with npx tsc --noEmit`);
|
|
210
|
+
}
|
|
211
|
+
if (!opts.watch)
|
|
212
|
+
return slotsFailed(state.slots) || tc?.errors.length ? 1 : 0;
|
|
213
|
+
console.log(`watching ${cfg.root} for changes, Ctrl-C to stop`);
|
|
214
|
+
const graph = server.environments.ssr.moduleGraph;
|
|
215
|
+
let timer = null, busy = null;
|
|
216
|
+
const again = async () => {
|
|
217
|
+
graph.invalidateAll();
|
|
218
|
+
const t0 = performance.now();
|
|
219
|
+
const now = await run();
|
|
220
|
+
const lines = [];
|
|
221
|
+
for (const s of state.slots) {
|
|
222
|
+
if (!s.compiled || s.conflict) {
|
|
223
|
+
lines.push(...slotLines(s, errors.get(s), { root: cfg.root, cap: 10, verbose: opts.verbose }));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
for (const c of s.compiled) {
|
|
227
|
+
const key = `${c.board.name}|${c.board.variant?.name ?? ''}`;
|
|
228
|
+
const d = diffFindings(last.get(key) ?? [], now.get(key) ?? []);
|
|
229
|
+
lines.push(`${boardLine(c.board, c.entry.rel, c.ms)} ${diffLine(d)}`);
|
|
230
|
+
for (const f of d.added)
|
|
231
|
+
lines.push(` new ${findingLine(f)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
last = now;
|
|
235
|
+
console.log(`\n${new Date().toLocaleTimeString()} recompiled in ${Math.round(performance.now() - t0)} ms`);
|
|
236
|
+
for (const l of lines)
|
|
237
|
+
console.log(l);
|
|
238
|
+
console.log(totalLine(state.slots));
|
|
239
|
+
if (opts.json)
|
|
240
|
+
writeReport(cfg, opts.json, { boards: state.slots.flatMap(s => reportOf(cfg, s, errors.get(s))) });
|
|
241
|
+
};
|
|
242
|
+
const inProject = (f) => f.startsWith(cfg.root) && !f.includes('/.pcbjs/') && !f.includes('/node_modules/') && /\.(m?[jt]sx?|json)$/.test(f);
|
|
243
|
+
const changed = (f) => {
|
|
244
|
+
if (!inProject(f))
|
|
245
|
+
return;
|
|
246
|
+
if (timer)
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
timer = setTimeout(() => { timer = null; busy = (busy ?? Promise.resolve()).then(again).catch(e => console.error(`pcbjs: ${e?.message ?? e}`)); }, 80);
|
|
249
|
+
};
|
|
250
|
+
server.watcher.add(cfg.root);
|
|
251
|
+
server.watcher.on('change', changed);
|
|
252
|
+
server.watcher.on('add', changed);
|
|
253
|
+
server.watcher.on('unlink', changed);
|
|
254
|
+
await new Promise(done => { for (const sig of ['SIGINT', 'SIGTERM'])
|
|
255
|
+
process.once(sig, () => done()); });
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
await server.close();
|
|
260
|
+
}
|
|
261
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { InspectRequest } from './dev-reuse.js';
|
|
2
|
+
/** The first argument is the entry when it names a module; a bare `--lengths` makes each query a filter, or adds the whole table. */
|
|
3
|
+
export declare function inspectRequests(args: string[], lengths: unknown, layers?: string[]): {
|
|
4
|
+
entry?: string;
|
|
5
|
+
requests: InspectRequest[];
|
|
6
|
+
};
|
|
7
|
+
export declare function run(argv?: string[]): Promise<void>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { cac } from 'cac';
|
|
2
|
+
import { dev } from './server.js';
|
|
3
|
+
import { devWorkspace } from './workspace.js';
|
|
4
|
+
import { build, preview } from './build.js';
|
|
5
|
+
import { ideInstall, ideStatus } from './ide.js';
|
|
6
|
+
import { create } from './create.js';
|
|
7
|
+
import { check } from './check.js';
|
|
8
|
+
import { inspect } from './inspect.js';
|
|
9
|
+
import { modelAdd, modelAlign, modelCheck, modelFetch } from './model.js';
|
|
10
|
+
import { docs } from './docs.js';
|
|
11
|
+
import { skills } from './skills.js';
|
|
12
|
+
import { importCommand } from './import.js';
|
|
13
|
+
import { parseSize, shot } from './shot.js';
|
|
14
|
+
import { resolveConfig } from './config.js';
|
|
15
|
+
import { pluginState } from './plugin.js';
|
|
16
|
+
import { cliVersion, doctor, warnVersionSkew } from './doctor.js';
|
|
17
|
+
import { versionLine } from './version.js';
|
|
18
|
+
import { fabDate } from '@pcbjs/export/fab';
|
|
19
|
+
const cli = cac('pcbjs');
|
|
20
|
+
cli.usage('<build|check|dev|preview> [entry...] [options]\n pcbjs dev --workspace <folder>... [options]\n pcbjs inspect [entry] [ref|net|REF.PAD|references|sch...] [--lengths [net|pair|group]] [--layer <name>] [--json]\n pcbjs model add <file-or-url|lcsc:C123> [--ref [<board>/]<REF> | --footprint <name>] [--entry <board>] [--as <name>] [--write]\n pcbjs model add --ref <REF> [--write], the model of its MPN:LCSC number\n pcbjs model align [entry] [<board>/]<ref> [--file <model>]\n pcbjs model check [entry...]\n pcbjs model fetch [entry...]\n pcbjs shot [entry...] [-o <dir>] [-p <port>] [--view 2d|3d|sch|both|all] [--select <ref>...] [--layer <name>...] [--side top|bottom] [--size WxH]\n pcbjs import footprint <Lib:Name|file.kicad_mod> [-o lib/<name>.tsx] [--force]\n pcbjs import board <file.kicad_pcb> [-o pcb.tsx] [--force]\n pcbjs docs [topic] [--grep <word|regex>]\n pcbjs ide <install|status>\n pcbjs skills <install [--to <dir>]|list|path>\n pcbjs doctor\n pcbjs create <dir> [--template board]\n\n The entries are `entries` in pcb.config.ts, paths relative to it, globs allowed; entries on the command line win for that run.\n Each entry is a .tsx or .ts module that default-exports <PCB>. Several entries make a multi-board site.\n The current directory is the project root. The config is pcb.config.ts, .mts, .js or .mjs in it; pcbjs.config.* still loads with a warning.');
|
|
21
|
+
cli.command('dev [...entries]', 'dev server: compiles the boards, updates the viewer on every change, serves fab files on demand; --workspace serves every project under the folders')
|
|
22
|
+
.option('--workspace [folder]', 'serve every directory with a pcb.config.* under the folders, given here or as the arguments; each loads on first use')
|
|
23
|
+
.option('-c, --config <file>', 'config file (default: pcb.config.ts, .mts, .js or .mjs)')
|
|
24
|
+
.option('-p, --port <port>', 'port (default 5173)')
|
|
25
|
+
.option('--host [host]', 'listen on all addresses or the given one')
|
|
26
|
+
.option('--open', 'open the browser')
|
|
27
|
+
.option('--ide <url>', 'dial an editor host, ws://127.0.0.1:<port>/<token>, for the hosted IDE mode')
|
|
28
|
+
.option('--ide-allow-remote', 'let --ide dial a host other than 127.0.0.1, localhost or [::1]')
|
|
29
|
+
.option('--verbose', 'keep the stack of a compile error')
|
|
30
|
+
.action(async (entries, o) => {
|
|
31
|
+
if (o.ide === true || o.ide === '')
|
|
32
|
+
usageError('--ide needs a URL, for example --ide=ws://127.0.0.1:5220/<token>');
|
|
33
|
+
warnVersionSkew();
|
|
34
|
+
const common = { port: portOf(o.port), host: o.host, open: o.open, ide: o.ide, ideAllowRemote: !!o.ideAllowRemote, verbose: !!o.verbose };
|
|
35
|
+
if (o.workspace !== undefined && o.workspace !== false) {
|
|
36
|
+
if (o.config)
|
|
37
|
+
usageError('--config names one project; with --workspace each project uses its own pcb.config.*');
|
|
38
|
+
const given = (Array.isArray(o.workspace) ? o.workspace : [o.workspace]).filter((f) => typeof f === 'string' && f);
|
|
39
|
+
const folders = [...given, ...entries].map(String);
|
|
40
|
+
await devWorkspace({ folders: folders.length ? folders : ['.'], ...common });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await dev({ entries, config: o.config, ...common });
|
|
44
|
+
});
|
|
45
|
+
cli.command('build [...entries]', 'compile the boards, write the fab files and the static site; exit 1 on DRC or ERC errors')
|
|
46
|
+
.option('-c, --config <file>', 'config file (default: pcb.config.ts, .mts, .js or .mjs)')
|
|
47
|
+
.option('--out-dir <dir>', 'site output (default .pcbjs/dist)')
|
|
48
|
+
.option('-q, --quiet', 'print only the board lines, the findings and the total')
|
|
49
|
+
.option('--verbose', "print every finding, Vite's output, the prerendered pages and the stack of a compile error")
|
|
50
|
+
.option('--json <file>', 'write every board, its findings with stable ids, its fab set and pages to <file>')
|
|
51
|
+
.option('--date <iso>', 'the date every fab file carries, such as 2026-01-31T12:00:00Z, so two builds of one tree are byte identical (default SOURCE_DATE_EPOCH, else now)')
|
|
52
|
+
.action(async (entries, o) => {
|
|
53
|
+
if (o.date === true || o.date === '')
|
|
54
|
+
usageError('--date needs a date, for example --date 2026-01-31T12:00:00Z');
|
|
55
|
+
if (o.date !== undefined)
|
|
56
|
+
try {
|
|
57
|
+
fabDate(String(o.date));
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
usageError(`--date: ${e.message}`);
|
|
61
|
+
}
|
|
62
|
+
warnVersionSkew();
|
|
63
|
+
await build({ entries, config: o.config, outDir: o.outDir, quiet: !!o.quiet, verbose: !!o.verbose, json: jsonFile(o.json), date: o.date === undefined ? undefined : String(o.date) });
|
|
64
|
+
});
|
|
65
|
+
cli.command('check [...entries]', 'compile and run DRC and ERC only: no fab files, no site; exit 1 on errors')
|
|
66
|
+
.option('-c, --config <file>', 'config file (default: pcb.config.ts, .mts, .js or .mjs)')
|
|
67
|
+
.option('-w, --watch', 'recompile on every change and print what is new, fixed and unchanged')
|
|
68
|
+
.option('-q, --quiet', 'the same output as without it; accepted for symmetry with build')
|
|
69
|
+
.option('--verbose', "print every finding, unfolded and uncapped, Vite's output and the stack of a compile error")
|
|
70
|
+
.option('--json <file>', 'write every board and its findings with stable ids to <file>')
|
|
71
|
+
.option('--fab', 'also write the fab set in memory and read its copper and outline Gerbers back, as build does: a difference is a fab-mismatch error')
|
|
72
|
+
.action(async (entries, o) => {
|
|
73
|
+
warnVersionSkew();
|
|
74
|
+
const cfg = await resolveConfig(entries, 'build', o.config);
|
|
75
|
+
if (o.fab)
|
|
76
|
+
cfg.fabCheck = true;
|
|
77
|
+
process.exitCode = await check({ cfg, state: pluginState(cfg), watch: !!o.watch, quiet: !!o.quiet, verbose: !!o.verbose, json: jsonFile(o.json) });
|
|
78
|
+
});
|
|
79
|
+
cli.command('inspect [...queries]', 'compile one board and print its frame and footprints, or each query: a ref with its pads, a net with its pads, vias and routes, a pad as REF.PAD; --lengths prints the route table; one compile answers them all')
|
|
80
|
+
.option('-c, --config <file>', 'config file (default: pcb.config.ts, .mts, .js or .mjs)')
|
|
81
|
+
.option('-t, --target <name>', 'the target of a board with variants, by name or slug (default the first)')
|
|
82
|
+
.option('--json', 'print JSON instead of text')
|
|
83
|
+
.option('--lengths [filter]', 'print every route with its length, target, match group, status and source, or those of one net, pair or match group; repeat it for several')
|
|
84
|
+
.option('--layer <name>', 'print every item on the layer, by id or alias, with its bounds, text, ref and source: strokes and glyphs of text, drawings, tracks, pours, pads; a query filters it by part or net; repeatable')
|
|
85
|
+
.option('--fresh', 'compile here even when a running dev server of the project holds a current board')
|
|
86
|
+
.option('--verbose', "show Vite's output and the stack of a compile error")
|
|
87
|
+
.action(async (args, o) => {
|
|
88
|
+
const layers = (o.layer === undefined ? [] : Array.isArray(o.layer) ? o.layer : [o.layer]).map(String);
|
|
89
|
+
if (layers.some(l => l === 'true' || l === ''))
|
|
90
|
+
usageError('--layer needs a layer, for example --layer F.SilkS');
|
|
91
|
+
const { entry, requests } = inspectRequests(args.map(String), o.lengths, layers);
|
|
92
|
+
process.exitCode = await inspect({ entry, requests, config: o.config, json: !!o.json, target: o.target, verbose: !!o.verbose, fresh: !!o.fresh });
|
|
93
|
+
});
|
|
94
|
+
cli.command('shot [...entries]', 'PNG screenshots of the viewer with a headless Chrome: one per board, target and view, to .pcbjs/shots/<board>-<view>[-<options>].png')
|
|
95
|
+
.option('-c, --config <file>', 'config file (default: pcb.config.ts, .mts, .js or .mjs)')
|
|
96
|
+
.option('-o, --out <dir>', 'output directory (default .pcbjs/shots)')
|
|
97
|
+
.option('-p, --port <port>', 'port of the dev server the shots are taken from (default a free port)')
|
|
98
|
+
.option('--view <view>', '2d, 3d, sch, both or all (default both, 2d and 3d; all adds the schematic)')
|
|
99
|
+
.option('--explode <amount>', 'the exploded 3D view, 0 to 1 of the largest layer separation')
|
|
100
|
+
.option('--layer <name>', 'show one layer alone in 2D, by id or alias, the outline kept; repeat it for one shot per layer')
|
|
101
|
+
.option('--select <ref>', 'select a part and zoom to it: its courtyard in 2D, the camera on it in 3D; repeat it for one shot per part')
|
|
102
|
+
.option('--fresh', 'compile here even when a running dev server of the project holds current boards')
|
|
103
|
+
.option('--size <WxH>', 'page size in CSS pixels (default 1600x1000)')
|
|
104
|
+
.option('--theme <theme>', 'light or dark (default the system theme of the headless browser, light)')
|
|
105
|
+
.option('--side <side>', 'top or bottom: bottom views the board from below, the 2D view flipped to the back and the 3D camera under the board (default top)')
|
|
106
|
+
.option('--flip', 'the same as --side bottom')
|
|
107
|
+
.option('--verbose', "show Vite's output")
|
|
108
|
+
.action(async (entries, o) => {
|
|
109
|
+
const view = o.view === undefined ? 'both' : String(o.view).toLowerCase();
|
|
110
|
+
if (!['2d', '3d', 'sch', 'both', 'all'].includes(view))
|
|
111
|
+
usageError(`bad --view: ${o.view}, use 2d, 3d, sch, both or all`);
|
|
112
|
+
const explode = o.explode === undefined ? undefined : Number(o.explode);
|
|
113
|
+
if (explode !== undefined && !(explode >= 0 && explode <= 1))
|
|
114
|
+
usageError(`bad --explode: ${o.explode}, use a number from 0 to 1`);
|
|
115
|
+
const size = o.size === undefined ? undefined : parseSize(String(o.size));
|
|
116
|
+
if (size === null)
|
|
117
|
+
usageError(`bad --size: ${o.size}, use WxH such as 1600x1000`);
|
|
118
|
+
const theme = o.theme === undefined ? undefined : String(o.theme).toLowerCase();
|
|
119
|
+
if (theme !== undefined && theme !== 'light' && theme !== 'dark')
|
|
120
|
+
usageError(`bad --theme: ${o.theme}, use light or dark`);
|
|
121
|
+
const side = o.side === undefined ? (o.flip ? 'bottom' : undefined) : String(o.side).toLowerCase();
|
|
122
|
+
if (side !== undefined && side !== 'top' && side !== 'bottom')
|
|
123
|
+
usageError(`bad --side: ${o.side}, use top or bottom`);
|
|
124
|
+
if (o.flip && side === 'top')
|
|
125
|
+
usageError('--flip views the bottom; give it or --side top, not both');
|
|
126
|
+
const many = (v) => (v === undefined ? undefined : (Array.isArray(v) ? v : [v]).map(String));
|
|
127
|
+
for (const k of ['out', 'layer', 'select'])
|
|
128
|
+
if (many(o[k])?.some(v => v === 'true' || v === ''))
|
|
129
|
+
usageError(`--${k} needs a value`);
|
|
130
|
+
if (Array.isArray(o.out))
|
|
131
|
+
usageError('give --out once');
|
|
132
|
+
process.exitCode = await shot({ entries, config: o.config, outDir: o.out, view: view, explode, layer: many(o.layer), select: many(o.select), size, theme: theme, side: side, verbose: !!o.verbose, port: portOf(o.port), fresh: !!o.fresh });
|
|
133
|
+
});
|
|
134
|
+
cli.command('model <action> [...args]', 'model files: `add <file-or-url|lcsc:C123>` copies or downloads one into models/ and fits it with --ref, and with --ref alone fetches the part\'s MPN:LCSC model; `align [entry] <ref>` prints the transform that puts a model on its pads; `check [entry...]` lists every model and flags missing, unread and misplaced ones; `fetch [entry...]` downloads the models the parts name, KiCad library, LCSC or URL, into .pcbjs/models/fetched')
|
|
135
|
+
.option('-c, --config <file>', 'config file (default: pcb.config.ts, .mts, .js or .mjs)')
|
|
136
|
+
.option('--ref <ref>', 'add: fit the model to this placed part and print its model option; `<board>/<ref>`, such as Sense/SW1, names its board')
|
|
137
|
+
.option('--entry <entry>', 'add, align: the board the ref is on, by entry file, board name or folder; needed when the ref is on several boards')
|
|
138
|
+
.option('--footprint <name>', 'add: fit the model to the first part with this footprint id, such as RJ45_JK0654219NL, and write it on the <footprint> element')
|
|
139
|
+
.option('--as <name>', 'add: the file name in models/, the extension added when missing')
|
|
140
|
+
.option('--write', 'add: write the model option into the code, the path relative to the file it is written in')
|
|
141
|
+
.option('--force', 'add: replace a file of the same name with other content')
|
|
142
|
+
.option('--file <model>', 'align: fit this file instead of the one the part uses')
|
|
143
|
+
.option('--verbose', "show Vite's output and the stack of a compile error")
|
|
144
|
+
.action(async (action, args, o) => {
|
|
145
|
+
const str = (v) => (v === undefined || v === true || v === '' ? undefined : String(v));
|
|
146
|
+
for (const k of ['ref', 'footprint', 'as', 'file', 'entry'])
|
|
147
|
+
if (o[k] === true || o[k] === '')
|
|
148
|
+
usageError(`--${k} needs a value`);
|
|
149
|
+
const common = { config: o.config, verbose: !!o.verbose };
|
|
150
|
+
if (action === 'add') {
|
|
151
|
+
if (args.length > 1 || (!args.length && !o.ref))
|
|
152
|
+
usageError('model add takes one file, URL or lcsc:<C-number>, or --ref alone for a part with an MPN:LCSC number: pcbjs model add <file-or-url> [--ref J1]');
|
|
153
|
+
if (o.ref && o.footprint)
|
|
154
|
+
usageError('give --ref or --footprint, not both');
|
|
155
|
+
if (o.write && !o.ref && !o.footprint)
|
|
156
|
+
usageError('--write needs --ref or --footprint');
|
|
157
|
+
process.exitCode = await modelAdd({ source: args.length ? String(args[0]) : undefined, ref: str(o.ref), entry: str(o.entry), footprint: str(o.footprint), as: str(o.as), write: !!o.write, force: !!o.force, ...common });
|
|
158
|
+
}
|
|
159
|
+
else if (action === 'align') {
|
|
160
|
+
if (args.length < 1 || args.length > 2)
|
|
161
|
+
usageError('model align takes a ref, and an entry first when the config lists several: pcbjs model align [entry] <ref>');
|
|
162
|
+
const [entry, ref] = args.length === 2 ? args.map(String) : [str(o.entry), String(args[0])];
|
|
163
|
+
if (args.length === 2 && o.entry !== undefined)
|
|
164
|
+
usageError('give the entry once, as the first argument or with --entry');
|
|
165
|
+
process.exitCode = await modelAlign({ entry, ref, file: str(o.file), ...common });
|
|
166
|
+
}
|
|
167
|
+
else if (action === 'check')
|
|
168
|
+
process.exitCode = await modelCheck({ entries: args.map(String), ...common });
|
|
169
|
+
else if (action === 'fetch')
|
|
170
|
+
process.exitCode = await modelFetch({ entries: args.map(String), ...common });
|
|
171
|
+
else
|
|
172
|
+
usageError(`unknown model action: ${action}, use add, align, check or fetch`);
|
|
173
|
+
});
|
|
174
|
+
cli.command('import <kind> <source>', 'KiCad files as code: `import footprint <Lib:Name|file.kicad_mod>` writes a defineFootprint module, `import board <file.kicad_pcb>` a board skeleton with its schematic, copper, footprints and rules')
|
|
175
|
+
.option('-o, --out <file>', 'footprint: the module to write (default lib/<name>.tsx); board: the board file (default pcb.tsx), the others beside it')
|
|
176
|
+
.option('--force', 'replace files that exist')
|
|
177
|
+
.action(async (kind, source, o) => {
|
|
178
|
+
if (o.out === true || o.out === '')
|
|
179
|
+
usageError('--out needs a file');
|
|
180
|
+
process.exitCode = await importCommand({ kind: String(kind), source: String(source), out: o.out === undefined ? undefined : String(o.out), force: !!o.force });
|
|
181
|
+
});
|
|
182
|
+
cli.command('preview [...entries]', 'serve the built site')
|
|
183
|
+
.option('-c, --config <file>', 'config file (default: pcb.config.ts, .mts, .js or .mjs)')
|
|
184
|
+
.option('-p, --port <port>', 'port (default 4173)')
|
|
185
|
+
.option('--host [host]', 'listen on all addresses')
|
|
186
|
+
.option('--out-dir <dir>', 'site to serve (default .pcbjs/dist)')
|
|
187
|
+
.action(async (entries, o) => { await preview({ entries, config: o.config, outDir: o.outDir, port: portOf(o.port), host: o.host }); });
|
|
188
|
+
cli.command('ide <action>', 'the bundled VS Code extension: `ide install` installs it with the code CLI, `ide status` compares it with the installed one')
|
|
189
|
+
.action(async (action) => {
|
|
190
|
+
if (action === 'install')
|
|
191
|
+
process.exitCode = await ideInstall();
|
|
192
|
+
else if (action === 'status')
|
|
193
|
+
process.exitCode = await ideStatus();
|
|
194
|
+
else
|
|
195
|
+
usageError(`unknown ide action: ${action}, use install or status`);
|
|
196
|
+
});
|
|
197
|
+
cli.command('skills <action>', 'the agent skills of @pcbjs/skills: `skills install` copies them into .claude/skills or --to <dir>, `skills list` names each, `skills path` prints where they ship')
|
|
198
|
+
.option('--to <dir>', 'install: the skills folder to copy into (default .claude/skills)')
|
|
199
|
+
.action(async (action, o) => {
|
|
200
|
+
if (o.to !== undefined && action !== 'install')
|
|
201
|
+
usageError('--to goes with skills install');
|
|
202
|
+
process.exitCode = await skills(action, { to: o.to === undefined ? undefined : String(o.to) });
|
|
203
|
+
});
|
|
204
|
+
cli.command('docs [topic]', 'print the docs: the topic list, one topic such as routing, or with --grep every line that matches')
|
|
205
|
+
.option('--grep <word>', 'print the lines of every topic, or of the one given, that mention <word>; anything but letters, digits, spaces and dashes is a regular expression, such as neck|escape')
|
|
206
|
+
.action((topic, o) => {
|
|
207
|
+
if (o.grep === true || o.grep === '')
|
|
208
|
+
usageError('--grep needs a word, for example --grep keepout');
|
|
209
|
+
process.exitCode = docs({ topic: topic === undefined ? undefined : String(topic), grep: o.grep === undefined ? undefined : String(o.grep) });
|
|
210
|
+
});
|
|
211
|
+
cli.command('doctor', 'print the installed @pcbjs versions, Node, Chrome, the step converter, the file format versions and the config; exit 1 when the packages disagree on major.minor')
|
|
212
|
+
.option('-c, --config <file>', 'config file (default: pcb.config.ts, .mts, .js or .mjs)')
|
|
213
|
+
.action(async (o) => { process.exitCode = await doctor({ config: o.config }); });
|
|
214
|
+
cli.command('create <dir>', 'start a new project in <dir> from a template: a board in pcb.tsx, pcb.config.ts listing it, package.json and tsconfig.json')
|
|
215
|
+
.option('-t, --template <name>', 'template (default board)')
|
|
216
|
+
.action((dir, o) => { create({ dir, template: o.template }); });
|
|
217
|
+
cli.help(sections => [{ body: versionLine() }, ...sections.slice(1)]);
|
|
218
|
+
cli.version(cliVersion());
|
|
219
|
+
cli.outputVersion = () => console.log(versionLine());
|
|
220
|
+
/** 0 asks the system for a free port */
|
|
221
|
+
function portOf(v) {
|
|
222
|
+
if (v === undefined || v === null || v === '' || v === true)
|
|
223
|
+
return undefined;
|
|
224
|
+
const n = Number(v);
|
|
225
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535)
|
|
226
|
+
usageError(`bad port: ${v}`);
|
|
227
|
+
return n;
|
|
228
|
+
}
|
|
229
|
+
/** a bare --json would read the next entry as its file */
|
|
230
|
+
function jsonFile(v) {
|
|
231
|
+
if (v === undefined || v === false)
|
|
232
|
+
return undefined;
|
|
233
|
+
if (v === true || v === '')
|
|
234
|
+
usageError('--json needs a file, for example --json .pcbjs/report.json');
|
|
235
|
+
return String(v);
|
|
236
|
+
}
|
|
237
|
+
/** The first argument is the entry when it names a module; a bare `--lengths` makes each query a filter, or adds the whole table. */
|
|
238
|
+
export function inspectRequests(args, lengths, layers = []) {
|
|
239
|
+
const entry = args.length && looksLikeEntry(args[0]) ? args[0] : undefined;
|
|
240
|
+
const queries = entry === undefined ? args : args.slice(1);
|
|
241
|
+
// --layer filters by each query; --lengths beside it adds its route tables
|
|
242
|
+
if (layers.length) {
|
|
243
|
+
const lengthFlags = (Array.isArray(lengths) ? lengths : [lengths]).filter(v => v !== undefined && v !== false);
|
|
244
|
+
const tables = lengthFlags.map(v => (typeof v === 'string' && v !== '' ? { query: v, lengths: true } : { lengths: true }));
|
|
245
|
+
return { entry, requests: [...layers.flatMap(layer => (queries.length ? queries.map(q => ({ query: q, layer })) : [{ layer }])), ...tables] };
|
|
246
|
+
}
|
|
247
|
+
const flags = (Array.isArray(lengths) ? lengths : [lengths]).filter(v => v !== undefined && v !== false);
|
|
248
|
+
const filters = flags.filter(v => typeof v === 'string' || typeof v === 'number').map(String);
|
|
249
|
+
const bare = flags.some(v => v === true || v === '');
|
|
250
|
+
if (bare && !filters.length)
|
|
251
|
+
return { entry, requests: queries.length ? queries.map(q => ({ query: q, lengths: true })) : [{ lengths: true }] };
|
|
252
|
+
const requests = [...queries.map(q => ({ query: q })), ...filters.map(q => ({ query: q, lengths: true })), ...(bare ? [{ lengths: true }] : [])];
|
|
253
|
+
return { entry, requests: requests.length ? requests : [{}] };
|
|
254
|
+
}
|
|
255
|
+
/** A module ends in .ts, .tsx, .js, .mts and the like; anything else is a ref, net or pad. */
|
|
256
|
+
const looksLikeEntry = (a) => /\.[cm]?[jt]sx?$/i.test(a);
|
|
257
|
+
function usageError(message) {
|
|
258
|
+
cli.unsetMatchedCommand();
|
|
259
|
+
cli.outputHelp();
|
|
260
|
+
console.error(`pcbjs: ${message}`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
export async function run(argv = process.argv) {
|
|
264
|
+
try {
|
|
265
|
+
cli.parse(argv, { run: false });
|
|
266
|
+
if (!cli.matchedCommand && !cli.options.help && !cli.options.version)
|
|
267
|
+
usageError(cli.args[0] ? `unknown command: ${cli.args[0]}` : 'no command given');
|
|
268
|
+
await cli.runMatchedCommand();
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
// the stack only when asked for
|
|
272
|
+
const msg = String(e?.message ?? e).replace(/\x1b\[[0-9;]*m/g, '');
|
|
273
|
+
console.error(`pcbjs: ${msg.split('\n')[0]}`);
|
|
274
|
+
if (cli.options.verbose || process.env.DEBUG)
|
|
275
|
+
console.error(String(e?.stack ?? '').split('\n').filter(l => /^\s+at /.test(l)).join('\n'));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|