@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/dist/boards.d.ts +92 -0
  3. package/dist/boards.js +126 -0
  4. package/dist/bridge.d.ts +53 -0
  5. package/dist/bridge.js +147 -0
  6. package/dist/build.d.ts +21 -0
  7. package/dist/build.js +165 -0
  8. package/dist/check.d.ts +83 -0
  9. package/dist/check.js +261 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.js +278 -0
  12. package/dist/config.d.ts +70 -0
  13. package/dist/config.js +141 -0
  14. package/dist/create.d.ts +18 -0
  15. package/dist/create.js +73 -0
  16. package/dist/dev-reuse.d.ts +36 -0
  17. package/dist/dev-reuse.js +51 -0
  18. package/dist/docs.d.ts +21 -0
  19. package/dist/docs.js +101 -0
  20. package/dist/doctor.d.ts +31 -0
  21. package/dist/doctor.js +151 -0
  22. package/dist/drafts.d.ts +47 -0
  23. package/dist/drafts.js +113 -0
  24. package/dist/fetch-model-child.d.ts +1 -0
  25. package/dist/fetch-model-child.js +12 -0
  26. package/dist/fetch-model.d.ts +32 -0
  27. package/dist/fetch-model.js +177 -0
  28. package/dist/host-client.d.ts +60 -0
  29. package/dist/host-client.js +182 -0
  30. package/dist/ide.d.ts +38 -0
  31. package/dist/ide.js +119 -0
  32. package/dist/import.d.ts +9 -0
  33. package/dist/import.js +120 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.js +8 -0
  36. package/dist/inspect-sch.d.ts +6 -0
  37. package/dist/inspect-sch.js +28 -0
  38. package/dist/inspect.d.ts +259 -0
  39. package/dist/inspect.js +451 -0
  40. package/dist/lsp.d.ts +25 -0
  41. package/dist/lsp.js +104 -0
  42. package/dist/model-child.d.ts +1 -0
  43. package/dist/model-child.js +26 -0
  44. package/dist/model.d.ts +101 -0
  45. package/dist/model.js +634 -0
  46. package/dist/models.d.ts +28 -0
  47. package/dist/models.js +236 -0
  48. package/dist/plugin.d.ts +59 -0
  49. package/dist/plugin.js +920 -0
  50. package/dist/prerender.d.ts +18 -0
  51. package/dist/prerender.js +79 -0
  52. package/dist/preview.d.ts +33 -0
  53. package/dist/preview.js +100 -0
  54. package/dist/project.d.ts +38 -0
  55. package/dist/project.js +154 -0
  56. package/dist/reference-child.d.ts +1 -0
  57. package/dist/reference-child.js +15 -0
  58. package/dist/references.d.ts +42 -0
  59. package/dist/references.js +328 -0
  60. package/dist/repo.d.ts +11 -0
  61. package/dist/repo.js +64 -0
  62. package/dist/scope-plugin.d.ts +2 -0
  63. package/dist/scope-plugin.js +22 -0
  64. package/dist/scope-state.d.ts +70 -0
  65. package/dist/scope-state.js +42 -0
  66. package/dist/scopes.d.ts +27 -0
  67. package/dist/scopes.js +96 -0
  68. package/dist/server.d.ts +55 -0
  69. package/dist/server.js +141 -0
  70. package/dist/shot.d.ts +74 -0
  71. package/dist/shot.js +300 -0
  72. package/dist/skills.d.ts +5 -0
  73. package/dist/skills.js +38 -0
  74. package/dist/source-ranges.d.ts +29 -0
  75. package/dist/source-ranges.js +261 -0
  76. package/dist/typecheck.d.ts +17 -0
  77. package/dist/typecheck.js +66 -0
  78. package/dist/version.d.ts +2 -0
  79. package/dist/version.js +12 -0
  80. package/dist/vite.d.ts +10 -0
  81. package/dist/vite.js +71 -0
  82. package/dist/workspace.d.ts +11 -0
  83. package/dist/workspace.js +53 -0
  84. package/docs/3d.md +100 -0
  85. package/docs/autorouter.md +108 -0
  86. package/docs/board.md +142 -0
  87. package/docs/circuits.md +140 -0
  88. package/docs/cli.md +158 -0
  89. package/docs/design.md +69 -0
  90. package/docs/development.md +94 -0
  91. package/docs/editor.md +112 -0
  92. package/docs/fab-set.md +87 -0
  93. package/docs/fab.md +99 -0
  94. package/docs/findings.md +86 -0
  95. package/docs/fonts.md +93 -0
  96. package/docs/highlight.md +45 -0
  97. package/docs/ide-bridge.md +145 -0
  98. package/docs/import.md +94 -0
  99. package/docs/inspect.md +25 -0
  100. package/docs/kicad.md +93 -0
  101. package/docs/layout.md +137 -0
  102. package/docs/length.md +37 -0
  103. package/docs/models.md +40 -0
  104. package/docs/pairs.md +126 -0
  105. package/docs/pours.md +74 -0
  106. package/docs/quickstart.md +126 -0
  107. package/docs/references.md +88 -0
  108. package/docs/routing.md +146 -0
  109. package/docs/rules.md +118 -0
  110. package/docs/schematic-graphic.md +137 -0
  111. package/docs/schematic.md +172 -0
  112. package/docs/skills.md +42 -0
  113. package/docs/sourcing.md +68 -0
  114. package/docs/stackup-and-rules.md +144 -0
  115. package/docs/stdlib.md +176 -0
  116. package/docs/viewer.md +107 -0
  117. package/docs/workspace.md +53 -0
  118. package/package.json +67 -3
  119. package/templates/board/README.md +14 -0
  120. package/templates/board/_gitignore +2 -0
  121. package/templates/board/package.json +16 -0
  122. package/templates/board/pcb.config.ts +8 -0
  123. package/templates/board/pcb.tsx +35 -0
  124. package/templates/board/tsconfig.json +15 -0
  125. package/vscode/manifest.json +9 -0
  126. package/vscode/vscode-pcbjs-0.0.1000.vsix +0 -0
  127. package/vscode/vscode-pcbjs.vsix +0 -0
@@ -0,0 +1,70 @@
1
+ import type { UserConfig } from '@pcbjs/core/config';
2
+ export interface Entry {
3
+ /** absolute path of the module */
4
+ file: string;
5
+ /** the path relative to the project root, forward slashes */
6
+ rel: string;
7
+ }
8
+ export interface ResolvedConfig {
9
+ root: string;
10
+ configFile: string | null;
11
+ /** the design entries in order, from the command line or the config's `entries`, each default-exporting <PCB> */
12
+ entries: Entry[];
13
+ outDir: string;
14
+ fabDir: string;
15
+ base: string;
16
+ vite: UserConfig['vite'];
17
+ repo: UserConfig['repo'];
18
+ /** how the viewer follows a click to the source, from `editor.follow` */
19
+ editor: {
20
+ follow: FollowMode;
21
+ };
22
+ /** read every copper and outline Gerber back and compare it with the board: set by `pcbjs build` and `pcbjs check --fab` */
23
+ fabCheck?: boolean;
24
+ /** the date every fab file carries, ISO 8601; absent means SOURCE_DATE_EPOCH, else the time of each compile */
25
+ fabDate?: string;
26
+ }
27
+ export type FollowMode = 'click' | 'meta-click';
28
+ /** The config file names, looked up in this order in the project root. */
29
+ export declare const CONFIG_NAMES: string[];
30
+ /** The names before the rename, still loaded with a deprecation warning when none of the new ones exists. */
31
+ export declare const LEGACY_CONFIG_NAMES: string[];
32
+ /** The config file of a project: the first new name that exists, else the first old one, flagged `legacy`. */
33
+ export declare function findConfig(root: string, exists?: (file: string) => boolean): {
34
+ file: string;
35
+ legacy: boolean;
36
+ } | null;
37
+ /** The one-line warning for a config file found under its old name. */
38
+ export declare const legacyWarning: (file: string) => string;
39
+ /** `editor.follow` checked: 'click' unless the config says 'meta-click'. */
40
+ export declare function followMode(v: unknown, warn?: (msg: string) => void): FollowMode;
41
+ /** The files of a config's `entries`, relative to its directory, globs expanded and sorted, no duplicates; a missing path or empty glob throws. */
42
+ export declare function expandEntries(patterns: string[], dir: string, shown: string): string[];
43
+ /** Where the entries of a run come from: the command line, else the config, else nothing, which is an error. */
44
+ export interface EntrySource {
45
+ /** absolute entry files */
46
+ files: string[];
47
+ from: 'command line' | 'config';
48
+ /** the one line printed when the command line overrides the config's entries, null otherwise */
49
+ note: string | null;
50
+ }
51
+ /** The entries of a run: the command line's, else the config's `entries`, else an error. */
52
+ export declare function pickEntries(cli: string[], configEntries: unknown, o: {
53
+ root: string;
54
+ configFile: string | null;
55
+ shown: string | null;
56
+ }): EntrySource;
57
+ /** The config file and what it exports, without the entries: for a command that only needs the settings. */
58
+ export declare function loadUserConfig(command: 'serve' | 'build', configArg?: string, root?: string): Promise<{
59
+ root: string;
60
+ file: string | null;
61
+ shown: string | null;
62
+ user: UserConfig & {
63
+ entry?: unknown;
64
+ title?: unknown;
65
+ };
66
+ }>;
67
+ /** The config of the project in the working directory, entries by pickEntries. */
68
+ export declare function resolveConfig(entries: string[], command: 'serve' | 'build', configArg?: string, root?: string, log?: (line: string) => void): Promise<ResolvedConfig>;
69
+ /** A ResolvedConfig from a loaded config and the command line entries. */
70
+ export declare function configFrom(loaded: Awaited<ReturnType<typeof loadUserConfig>>, entries: string[], log?: (line: string) => void): ResolvedConfig;
package/dist/config.js ADDED
@@ -0,0 +1,141 @@
1
+ import { existsSync, globSync, statSync } from 'node:fs';
2
+ import { dirname, isAbsolute, relative, resolve } from 'node:path';
3
+ import { loadConfigFromFile } from 'vite';
4
+ import { setKicadConfig } from '@pcbjs/kicad/library';
5
+ import { setFontConfig } from '@pcbjs/core/outline';
6
+ /** The config file names, looked up in this order in the project root. */
7
+ export const CONFIG_NAMES = ['pcb.config.ts', 'pcb.config.mts', 'pcb.config.js', 'pcb.config.mjs'];
8
+ /** The names before the rename, still loaded with a deprecation warning when none of the new ones exists. */
9
+ export const LEGACY_CONFIG_NAMES = ['pcbjs.config.ts', 'pcbjs.config.mts', 'pcbjs.config.js', 'pcbjs.config.mjs'];
10
+ /** The config file of a project: the first new name that exists, else the first old one, flagged `legacy`. */
11
+ export function findConfig(root, exists = existsSync) {
12
+ for (const c of CONFIG_NAMES)
13
+ if (exists(resolve(root, c)))
14
+ return { file: resolve(root, c), legacy: false };
15
+ for (const c of LEGACY_CONFIG_NAMES)
16
+ if (exists(resolve(root, c)))
17
+ return { file: resolve(root, c), legacy: true };
18
+ return null;
19
+ }
20
+ /** The one-line warning for a config file found under its old name. */
21
+ export const legacyWarning = (file) => {
22
+ const name = file.split(/[\\/]/).pop();
23
+ return `pcbjs: ${name} is deprecated, rename it to ${name.replace(/^pcbjs\./, 'pcb.')}`;
24
+ };
25
+ /** `editor.follow` checked: 'click' unless the config says 'meta-click'. */
26
+ export function followMode(v, warn = console.warn) {
27
+ if (v === undefined || v === 'click')
28
+ return 'click';
29
+ if (v === 'meta-click')
30
+ return 'meta-click';
31
+ warn(`pcbjs: editor.follow must be "click" or "meta-click", got ${JSON.stringify(v)}; using "click"`);
32
+ return 'click';
33
+ }
34
+ const isGlob = (p) => /[*?[\]{}]/.test(p);
35
+ /** The files of a config's `entries`, relative to its directory, globs expanded and sorted, no duplicates; a missing path or empty glob throws. */
36
+ export function expandEntries(patterns, dir, shown) {
37
+ const out = [];
38
+ const seen = new Set();
39
+ for (const p of patterns) {
40
+ if (typeof p !== 'string' || !p)
41
+ throw new Error(`entries in ${shown} must be a list of paths, got ${JSON.stringify(p)}`);
42
+ let files;
43
+ if (isGlob(p)) {
44
+ files = globSync(p, { cwd: dir, exclude: (f) => f === 'node_modules' || f === '.pcbjs' })
45
+ .map(f => resolve(dir, f)).filter(f => statSync(f).isFile()).sort();
46
+ if (!files.length)
47
+ throw new Error(`no file matches the entry pattern ${JSON.stringify(p)} in ${shown}`);
48
+ }
49
+ else {
50
+ const f = resolve(dir, p);
51
+ if (!existsSync(f))
52
+ throw new Error(`entry not found: ${p}, listed in entries in ${shown}`);
53
+ files = [f];
54
+ }
55
+ for (const f of files)
56
+ if (!seen.has(f)) {
57
+ seen.add(f);
58
+ out.push(f);
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ /** The entries of a run: the command line's, else the config's `entries`, else an error. */
64
+ export function pickEntries(cli, configEntries, o) {
65
+ const listed = configEntries === undefined || configEntries === null ? [] : configEntries;
66
+ if (!Array.isArray(listed))
67
+ throw new Error(`entries in ${o.shown} must be a list of paths such as ['pcb.tsx'], got ${JSON.stringify(listed)}`);
68
+ const fromConfig = listed.length && o.configFile ? expandEntries(listed, dirname(o.configFile), o.shown) : [];
69
+ if (cli.length) {
70
+ const files = cli.map(e => {
71
+ const f = isAbsolute(e) ? e : resolve(o.root, e);
72
+ if (!existsSync(f))
73
+ throw new Error(`entry not found: ${e}`);
74
+ return f;
75
+ });
76
+ const same = files.length === fromConfig.length && files.every((f, i) => f === fromConfig[i]);
77
+ const note = fromConfig.length && !same ? `pcbjs: using the entries on the command line for this run, not the ${fromConfig.length} in ${o.shown}` : null;
78
+ return { files, from: 'command line', note };
79
+ }
80
+ if (fromConfig.length)
81
+ return { files: fromConfig, from: 'config', note: null };
82
+ const name = o.shown ?? 'pcb.config.ts';
83
+ throw new Error(o.configFile
84
+ ? `no entries: add entries: ['pcb.tsx'] to ${name}, or pass the entries on the command line`
85
+ : `no entries: create ${name} with export default defineConfig({ entries: ['pcb.tsx'] }), or pass the entries on the command line`);
86
+ }
87
+ /** The config file and what it exports, without the entries: for a command that only needs the settings. */
88
+ export async function loadUserConfig(command, configArg, root = process.cwd()) {
89
+ root = resolve(root);
90
+ let file = configArg ? resolve(root, configArg) : null;
91
+ if (!file) {
92
+ const found = findConfig(root);
93
+ if (found?.legacy)
94
+ console.warn(legacyWarning(found.file));
95
+ file = found?.file ?? null;
96
+ }
97
+ let user = {};
98
+ if (file) {
99
+ if (!existsSync(file))
100
+ throw new Error(`config not found: ${configArg}`);
101
+ const loaded = await loadConfigFromFile({ command, mode: command === 'build' ? 'production' : 'development' }, file, root, 'silent');
102
+ user = (loaded?.config ?? {});
103
+ }
104
+ // kicadFootprint() in the design reads the project's KiCad library folders from here
105
+ setKicadConfig(file ? dirname(file) : root, user.kicad);
106
+ // <text font="name"> finds the outline fonts of the config here, files relative to the config
107
+ setFontConfig(file ? dirname(file) : root, user.fonts);
108
+ const shown = file ? (!relative(root, file).startsWith('..') ? relative(root, file) : file) : null;
109
+ return { root, file, shown, user };
110
+ }
111
+ /** The config of the project in the working directory, entries by pickEntries. */
112
+ export async function resolveConfig(entries, command, configArg, root = process.cwd(), log = l => console.log(l)) {
113
+ const loaded = await loadUserConfig(command, configArg, root);
114
+ return configFrom(loaded, entries, log);
115
+ }
116
+ /** A ResolvedConfig from a loaded config and the command line entries. */
117
+ export function configFrom(loaded, entries, log = l => console.log(l)) {
118
+ const { root, file, shown, user } = loaded;
119
+ if (user.entry !== undefined)
120
+ console.warn(`pcbjs: "entry" in ${shown} is ignored, list the entries as entries: [${JSON.stringify(user.entry)}]`);
121
+ if (user.title !== undefined)
122
+ console.warn(`pcbjs: "title" in ${shown} is ignored, the site shows each board's own title`);
123
+ const picked = pickEntries(entries, user.entries, { root, configFile: file, shown: shown ?? 'pcb.config.ts' });
124
+ if (picked.note)
125
+ log(picked.note);
126
+ const abs = (p) => (isAbsolute(p) ? p : resolve(root, p));
127
+ const seen = new Set();
128
+ const list = [];
129
+ for (const f of picked.files) {
130
+ if (seen.has(f))
131
+ continue;
132
+ seen.add(f);
133
+ list.push({ file: f, rel: relative(root, f).split('\\').join('/') });
134
+ }
135
+ return {
136
+ root, configFile: file, entries: list,
137
+ outDir: abs(user.outDir ?? '.pcbjs/dist'), fabDir: abs(user.fabDir ?? '.pcbjs/fab'),
138
+ base: user.base ?? '/', vite: user.vite, repo: user.repo,
139
+ editor: { follow: followMode(user.editor?.follow) },
140
+ };
141
+ }
@@ -0,0 +1,18 @@
1
+ /** packages/cli/templates, from dist or src alike. */
2
+ export declare const TEMPLATES_DIR: string;
3
+ export interface CreateOptions {
4
+ dir: string;
5
+ template?: string;
6
+ cwd?: string;
7
+ log?: (line: string) => void;
8
+ }
9
+ export interface CreateResult {
10
+ dir: string;
11
+ name: string;
12
+ template: string;
13
+ files: string[];
14
+ }
15
+ export declare function templates(): string[];
16
+ /** A board name from a directory name: letters, digits, dot, dash and underscore, anything else as a dash. */
17
+ export declare function boardName(dir: string): string;
18
+ export declare function create(opts: CreateOptions): CreateResult;
package/dist/create.js ADDED
@@ -0,0 +1,73 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { basename, dirname, join, relative, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { cliVersion, installRange } from './doctor.js';
5
+ const here = dirname(fileURLToPath(import.meta.url));
6
+ /** packages/cli/templates, from dist or src alike. */
7
+ export const TEMPLATES_DIR = resolve(here, '../templates');
8
+ /** The name the templates use for the board, replaced by the new board's name. */
9
+ const PLACEHOLDER = /\bHello\b/g;
10
+ /** Files whose text gets the board name. */
11
+ const TEXT = /\.(tsx?|mts|md)$/;
12
+ /** npm leaves .gitignore out of a published package, so the template keeps it as _gitignore. */
13
+ const RENAME = { _gitignore: '.gitignore' };
14
+ export function templates() {
15
+ return existsSync(TEMPLATES_DIR) ? readdirSync(TEMPLATES_DIR).filter(f => statSync(join(TEMPLATES_DIR, f)).isDirectory()).sort() : [];
16
+ }
17
+ /** A board name from a directory name: letters, digits, dot, dash and underscore, anything else as a dash. */
18
+ export function boardName(dir) {
19
+ return basename(resolve(dir)).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[-.]+|-+$/g, '') || 'board';
20
+ }
21
+ /** An npm package name from a board name: lowercase, no leading dot or underscore. */
22
+ function packageName(name) {
23
+ return name.toLowerCase().replace(/^[._]+/, '') || 'board';
24
+ }
25
+ function walk(dir, base = dir) {
26
+ return readdirSync(dir).sort().flatMap(f => {
27
+ const p = join(dir, f);
28
+ return statSync(p).isDirectory() ? walk(p, base) : [relative(base, p)];
29
+ });
30
+ }
31
+ export function create(opts) {
32
+ const template = opts.template ?? 'board';
33
+ const log = opts.log ?? (line => console.log(line));
34
+ const available = templates();
35
+ if (!available.includes(template))
36
+ throw new Error(`unknown template ${JSON.stringify(template)}, available: ${available.join(', ') || 'none'}`);
37
+ const target = resolve(opts.cwd ?? process.cwd(), opts.dir);
38
+ if (existsSync(target)) {
39
+ if (!statSync(target).isDirectory())
40
+ throw new Error(`${opts.dir} exists and is not a directory`);
41
+ if (readdirSync(target).length)
42
+ throw new Error(`${opts.dir} is not empty; give a new or empty directory`);
43
+ }
44
+ const name = boardName(target);
45
+ const src = join(TEMPLATES_DIR, template);
46
+ const files = [];
47
+ for (const rel of walk(src)) {
48
+ const out = join(dirname(rel), RENAME[basename(rel)] ?? basename(rel));
49
+ let body = readFileSync(join(src, rel));
50
+ if (basename(rel) === 'package.json') {
51
+ const pkg = JSON.parse(body.toString('utf8'));
52
+ pkg.name = packageName(name);
53
+ for (const k of Object.keys(pkg.devDependencies ?? {}))
54
+ if (k.startsWith('@pcbjs/'))
55
+ pkg.devDependencies[k] = installRange(cliVersion());
56
+ body = JSON.stringify(pkg, null, 2) + '\n';
57
+ }
58
+ else if (TEXT.test(rel))
59
+ body = body.toString('utf8').replace(PLACEHOLDER, name);
60
+ mkdirSync(dirname(join(target, out)), { recursive: true });
61
+ writeFileSync(join(target, out), body);
62
+ files.push(out);
63
+ }
64
+ const shown = relative(opts.cwd ?? process.cwd(), target) || '.';
65
+ log(`pcbjs: created the board ${name} in ${shown} from the ${template} template\n`);
66
+ log('Next:');
67
+ if (shown !== '.')
68
+ log(` cd ${shown}`);
69
+ log(' npm install');
70
+ log(' npm run dev # edit pcb.tsx, the viewer updates on every save');
71
+ log(' npm run build # fab files and the site, with the DRC and ERC summary');
72
+ return { dir: target, name, template, files };
73
+ }
@@ -0,0 +1,36 @@
1
+ import type { DevInfo } from './server.js';
2
+ /** What /__pcbjs/cli/board answers for one entry. */
3
+ export interface DevBoard {
4
+ /** false when the server has no current board of the entry: never compiled, failed, stale or edited since */
5
+ current: boolean;
6
+ reason?: string;
7
+ /** the board name, its targets, null for a board without variants, and its refs */
8
+ name?: string;
9
+ targets?: (string | null)[];
10
+ refs?: string[];
11
+ /** the compile time of the board on the server, ms, and how long ago it finished */
12
+ ms?: number;
13
+ age?: number;
14
+ /** the path of the project's pages on the server, `/` or `/<scope>/` */
15
+ route?: string;
16
+ /** a workspace server shows one scope at a time: false when another one is displayed */
17
+ displayed?: boolean;
18
+ }
19
+ /** One request of a batched inspect: a query, the route table, or both as `inspect --lengths <filter>`; `layer` lists one layer's items, of the query's part or net when there is one. */
20
+ export interface InspectRequest {
21
+ query?: string;
22
+ lengths?: boolean;
23
+ layer?: string;
24
+ }
25
+ /** The live dev server of a project: its origin and dev.json; null when there is none or its process is gone. */
26
+ export declare function devServerOf(root: string): {
27
+ origin: string;
28
+ info: DevInfo;
29
+ } | null;
30
+ /** GET a /__pcbjs/cli/ path of a dev server as JSON; null on any failure, so the caller compiles itself. */
31
+ export declare function askDev<T>(origin: string, path: string, params: Record<string, string>, timeout?: number): Promise<T | null>;
32
+ /** An entry file as both sides name it: its real path. */
33
+ export declare const realEntry: (file: string) => string;
34
+ export declare const secs: (ms: number) => string;
35
+ /** The one line saying the answer came from a dev server. */
36
+ export declare const devLine: (origin: string, entries: string[], ms: number, age: number) => string;
@@ -0,0 +1,51 @@
1
+ // inspect and shot answer from a live dev server's current board through /__pcbjs/cli/, served by devCliEndpoint in plugin.ts
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ /** The live dev server of a project: its origin and dev.json; null when there is none or its process is gone. */
5
+ export function devServerOf(root) {
6
+ let info;
7
+ try {
8
+ info = JSON.parse(readFileSync(join(root, '.pcbjs', 'dev.json'), 'utf8'));
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ if (!info || typeof info.port !== 'number' || typeof info.pid !== 'number' || info.pid === process.pid)
14
+ return null;
15
+ try {
16
+ process.kill(info.pid, 0);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ const url = info.url ?? info.urls?.[0] ?? `http://localhost:${info.port}/`;
22
+ try {
23
+ return { origin: new URL(url).origin, info };
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /** GET a /__pcbjs/cli/ path of a dev server as JSON; null on any failure, so the caller compiles itself. */
30
+ export async function askDev(origin, path, params, timeout = 180000) {
31
+ const q = new URLSearchParams(params);
32
+ try {
33
+ const r = await fetch(`${origin}/__pcbjs/cli/${path}?${q}`, { signal: AbortSignal.timeout(timeout) });
34
+ if (!r.ok)
35
+ return null;
36
+ return (await r.json());
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /** An entry file as both sides name it: its real path. */
43
+ export const realEntry = (file) => { try {
44
+ return realpathSync(file);
45
+ }
46
+ catch {
47
+ return file;
48
+ } };
49
+ export const secs = (ms) => (ms >= 1000 ? `${(ms / 1000).toFixed(1)} s` : `${Math.round(ms)} ms`);
50
+ /** The one line saying the answer came from a dev server. */
51
+ export const devLine = (origin, entries, ms, age) => `dev server ${origin}/ ${entries.join(', ')} current, compiled ${secs(age)} ago in ${secs(ms)}; --fresh compiles here`;
package/dist/docs.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /** The topics in reading order; any other top-level .md file in the directory follows them. */
2
+ export declare const TOPICS: string[];
3
+ /** The docs directory: the package's own, else the repository's when running from a checkout. Null when neither exists. */
4
+ export declare function docsDir(): string | null;
5
+ /** Every topic in the directory, in reading order. */
6
+ export declare function topics(dir: string): string[];
7
+ /** The first summary line of a topic: the first non-empty line after the title. */
8
+ export declare function summaryOf(text: string): string;
9
+ /** The topic list as lines. */
10
+ export declare function topicTable(dir: string): string[];
11
+ /** The case-insensitive test of a --grep argument: a substring for plain words, else a regular expression when it parses. */
12
+ export declare function grepTest(word: string): (line: string) => boolean;
13
+ /** Lines of the topics that match `word`, see grepTest, as `topic:line: text`. */
14
+ export declare function grepDocs(dir: string, word: string, only?: string[]): string[];
15
+ /** A topic name as typed: `routing`, `routing.md` or `docs/routing.md`, any case. Null when there is none. */
16
+ export declare function topicOf(dir: string, name: string): string | null;
17
+ /** The command. Returns the exit code. */
18
+ export declare function docs(opts: {
19
+ topic?: string;
20
+ grep?: string;
21
+ }): number;
package/dist/docs.js ADDED
@@ -0,0 +1,101 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /** The topics in reading order; any other top-level .md file in the directory follows them. */
5
+ export const TOPICS = [
6
+ 'quickstart', 'board', 'schematic', 'circuits', 'schematic-graphic', 'sourcing', 'layout', 'pours', 'routing', 'autorouter', 'pairs', 'length', 'rules', 'findings', 'fab',
7
+ 'fab-set', 'cli', 'inspect', 'workspace', 'viewer', 'editor', 'highlight', '3d', 'models', 'references', 'skills', 'stdlib', 'kicad', 'import', 'stackup-and-rules', 'fonts', 'ide-bridge',
8
+ 'design', 'development',
9
+ ];
10
+ /** The docs directory: the package's own, else the repository's when running from a checkout. Null when neither exists. */
11
+ export function docsDir() {
12
+ const pkg = resolve(dirname(fileURLToPath(import.meta.url)), '..');
13
+ for (const dir of [join(pkg, 'docs'), join(pkg, '..', '..', 'docs')])
14
+ if (existsSync(join(dir, 'quickstart.md')))
15
+ return dir;
16
+ return null;
17
+ }
18
+ /** Every topic in the directory, in reading order. */
19
+ export function topics(dir) {
20
+ const have = readdirSync(dir).filter(f => f.endsWith('.md')).map(f => f.slice(0, -3));
21
+ return [...TOPICS.filter(t => have.includes(t)), ...have.filter(t => !TOPICS.includes(t)).sort()];
22
+ }
23
+ /** The first summary line of a topic: the first non-empty line after the title. */
24
+ export function summaryOf(text) {
25
+ const lines = text.split('\n');
26
+ const start = lines.findIndex(l => l.startsWith('# '));
27
+ return lines.slice(start + 1).find(l => l.trim())?.trim() ?? '';
28
+ }
29
+ /** The topic list as lines. */
30
+ export function topicTable(dir) {
31
+ const list = topics(dir);
32
+ const w = Math.max(...list.map(t => t.length));
33
+ return [
34
+ 'pcbjs docs <topic> prints one topic; pcbjs docs --grep <word|regex> searches them all.',
35
+ '',
36
+ ...list.map(t => ` ${t.padEnd(w)} ${summaryOf(readFileSync(join(dir, `${t}.md`), 'utf8'))}`),
37
+ ];
38
+ }
39
+ /** The case-insensitive test of a --grep argument: a substring for plain words, else a regular expression when it parses. */
40
+ export function grepTest(word) {
41
+ const needle = word.toLowerCase();
42
+ if (!/^[\w\s-]*$/.test(word)) {
43
+ try {
44
+ const re = new RegExp(word, 'i');
45
+ return l => re.test(l);
46
+ }
47
+ catch { /* a substring after all */ }
48
+ }
49
+ return l => l.toLowerCase().includes(needle);
50
+ }
51
+ /** Lines of the topics that match `word`, see grepTest, as `topic:line: text`. */
52
+ export function grepDocs(dir, word, only) {
53
+ const test = grepTest(word);
54
+ const out = [];
55
+ for (const t of only ?? topics(dir)) {
56
+ readFileSync(join(dir, `${t}.md`), 'utf8').split('\n').forEach((l, i) => {
57
+ if (test(l))
58
+ out.push(`${t}:${i + 1}: ${l}`);
59
+ });
60
+ }
61
+ return out;
62
+ }
63
+ /** A topic name as typed: `routing`, `routing.md` or `docs/routing.md`, any case. Null when there is none. */
64
+ export function topicOf(dir, name) {
65
+ const t = name.replace(/^.*[\\/]/, '').replace(/\.md$/i, '').toLowerCase();
66
+ return topics(dir).find(x => x.toLowerCase() === t) ?? null;
67
+ }
68
+ /** The command. Returns the exit code. */
69
+ export function docs(opts) {
70
+ const dir = docsDir();
71
+ if (!dir) {
72
+ console.error('pcbjs: the docs are not in this installation of pcbjs');
73
+ return 1;
74
+ }
75
+ let only;
76
+ if (opts.topic !== undefined) {
77
+ const t = topicOf(dir, opts.topic);
78
+ if (!t) {
79
+ console.error(`pcbjs: no topic ${opts.topic}; topics: ${topics(dir).join(', ')}`);
80
+ return 1;
81
+ }
82
+ only = [t];
83
+ }
84
+ if (opts.grep !== undefined) {
85
+ const lines = grepDocs(dir, opts.grep, only);
86
+ for (const l of lines)
87
+ console.log(l);
88
+ if (!lines.length) {
89
+ console.error(`pcbjs: no line matches ${opts.grep}`);
90
+ return 1;
91
+ }
92
+ return 0;
93
+ }
94
+ if (only) {
95
+ process.stdout.write(readFileSync(join(dir, `${only[0]}.md`), 'utf8'));
96
+ return 0;
97
+ }
98
+ for (const l of topicTable(dir))
99
+ console.log(l);
100
+ return 0;
101
+ }
@@ -0,0 +1,31 @@
1
+ export interface InstalledPackage {
2
+ name: string;
3
+ version: string;
4
+ dir: string;
5
+ }
6
+ export declare function cliVersion(): string;
7
+ /** The dist-tag a version is published under: beta for 0.0.1-beta.2, next for a snapshot, else latest. */
8
+ export declare function distTag(version: string): string;
9
+ /** What a new project asks for: a caret on a prerelease also takes next snapshots, so the tag; else the minor a train shares. */
10
+ export declare function installRange(version: string): string;
11
+ /** latest holds the 0.0.0 placeholder while the packages are in beta, so the fix names the running line. */
12
+ export declare const UPDATE_COMMAND: string;
13
+ /** Every @pcbjs package a project resolves, nearest node_modules first, as Node resolves them. */
14
+ export declare function installedPackages(root: string): InstalledPackage[];
15
+ /** major.minor, so 0.0.1 and 0.0.2 agree and 0.1.0-beta.1 and 0.1.3 agree. */
16
+ export declare function minorOf(version: string): string;
17
+ /** Null when every package shares one major.minor, else the line naming each version and the fix. */
18
+ export declare function versionSkew(packages: {
19
+ name: string;
20
+ version: string;
21
+ }[]): string | null;
22
+ /** The installed packages plus the running CLI when it is another copy, such as a global install. */
23
+ export declare function packagesInUse(root: string): InstalledPackage[];
24
+ /** The single warning line for a version skew, printed at most once per process. */
25
+ export declare function warnVersionSkew(root?: string, warn?: (line: string) => void): void;
26
+ export declare function stepConverter(): string | null;
27
+ export declare function doctor(o?: {
28
+ config?: string;
29
+ root?: string;
30
+ log?: (line: string) => void;
31
+ }): Promise<number>;