@jlcpcb/cli 0.2.0 → 0.3.1
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/CHANGELOG.md +57 -0
- package/dist/assets/search.html +11 -11
- package/dist/index.js +4790 -1251
- package/package.json +5 -2
- package/src/app/App.tsx +4 -3
- package/src/app/components/DetailView.tsx +2 -2
- package/src/app/components/ListView.tsx +29 -49
- package/src/app/components/library-table-format.test.ts +58 -0
- package/src/app/components/library-table-format.ts +87 -0
- package/src/app/components/list-view-format.test.ts +60 -0
- package/src/app/components/list-view-format.ts +97 -0
- package/src/app/components/table-text.ts +59 -0
- package/src/app/navigation/NavigationContext.tsx +5 -1
- package/src/app/screens/InfoScreen.tsx +23 -28
- package/src/app/screens/LibraryScreen.tsx +27 -30
- package/src/commands/easyeda.ts +6 -8
- package/src/commands/info.ts +1 -1
- package/src/commands/install.ts +10 -15
- package/src/commands/library.ts +51 -2
- package/src/commands/search.ts +1 -1
- package/src/commands/validate.ts +287 -0
- package/src/index.ts +41 -3
|
@@ -6,15 +6,15 @@ import type { LibraryParams } from '../navigation/types.js';
|
|
|
6
6
|
import { useAppState } from '../state/AppStateContext.js';
|
|
7
7
|
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
8
8
|
import { Divider } from '../components/Divider.js';
|
|
9
|
+
import {
|
|
10
|
+
formatLibraryHeader,
|
|
11
|
+
formatLibraryRow,
|
|
12
|
+
getLibraryTableWidths,
|
|
13
|
+
} from '../components/library-table-format.js';
|
|
9
14
|
|
|
10
15
|
const libraryService = createLibraryService();
|
|
11
16
|
const componentService = createComponentService();
|
|
12
17
|
|
|
13
|
-
function truncate(str: string, len: number): string {
|
|
14
|
-
if (!str) return '';
|
|
15
|
-
return str.length > len ? str.slice(0, len - 1) + '…' : str;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
18
|
function StatusBadge({ installed, linked }: { installed: boolean; linked: boolean }) {
|
|
19
19
|
if (installed && linked) {
|
|
20
20
|
return <Text color="green">Installed & Linked</Text>;
|
|
@@ -193,10 +193,7 @@ export function LibraryScreen() {
|
|
|
193
193
|
// Components table - full width responsive layout
|
|
194
194
|
// Fixed columns: selector(2) + name + category + status columns(15)
|
|
195
195
|
// Description takes remaining space
|
|
196
|
-
const
|
|
197
|
-
const categoryWidth = 12;
|
|
198
|
-
const statusWidth = 15; // Sym(5) + FP(5) + 3D(5)
|
|
199
|
-
const descWidth = Math.max(terminalWidth - 2 - nameWidth - categoryWidth - statusWidth, 15);
|
|
196
|
+
const widths = getLibraryTableWidths(terminalWidth);
|
|
200
197
|
|
|
201
198
|
return (
|
|
202
199
|
<Box flexDirection="column" width="100%">
|
|
@@ -216,36 +213,36 @@ export function LibraryScreen() {
|
|
|
216
213
|
</Box>
|
|
217
214
|
<Divider width={terminalWidth} />
|
|
218
215
|
<Box marginBottom={1} marginTop={1}>
|
|
219
|
-
<Text bold dimColor>
|
|
220
|
-
{' '}
|
|
221
|
-
{'Name'.padEnd(nameWidth)}
|
|
222
|
-
{'Category'.padEnd(categoryWidth)}
|
|
223
|
-
{'Description'.padEnd(descWidth)}
|
|
224
|
-
{'Sym'.padEnd(5)}
|
|
225
|
-
{'FP'.padEnd(5)}
|
|
226
|
-
{'3D'}
|
|
227
|
-
</Text>
|
|
216
|
+
<Text bold dimColor>{formatLibraryHeader(widths)}</Text>
|
|
228
217
|
</Box>
|
|
229
218
|
{installed.map((item, i) => {
|
|
230
219
|
const isSelected = i === selectedIndex;
|
|
231
220
|
const desc = descriptions[item.lcscId] || '';
|
|
232
|
-
|
|
233
|
-
const isStandardFp = item.footprintRef && !item.footprintRef.startsWith('JLC-MCP:');
|
|
234
|
-
const fpLabel = !item.footprintRef ? 'N' : isStandardFp ? 'S' : 'Y';
|
|
235
|
-
const fpColor = !item.footprintRef ? 'red' : isStandardFp ? 'cyan' : 'green';
|
|
221
|
+
const row = formatLibraryRow(item, desc, widths);
|
|
236
222
|
return (
|
|
237
223
|
<Box key={`${item.lcscId}-${i}`}>
|
|
238
224
|
<Text color={isSelected ? 'cyan' : undefined}>
|
|
239
225
|
{isSelected ? '> ' : ' '}
|
|
240
226
|
</Text>
|
|
241
|
-
|
|
242
|
-
<Text
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
227
|
+
{isSelected ? (
|
|
228
|
+
<Text inverse>
|
|
229
|
+
{row.name}
|
|
230
|
+
{row.category}
|
|
231
|
+
{row.description}
|
|
232
|
+
{row.sym}
|
|
233
|
+
{row.fp}
|
|
234
|
+
{row.model}
|
|
235
|
+
</Text>
|
|
236
|
+
) : (
|
|
237
|
+
<Text>
|
|
238
|
+
<Text color="cyan">{row.name}</Text>
|
|
239
|
+
{row.category}
|
|
240
|
+
<Text dimColor>{row.description}</Text>
|
|
241
|
+
<Text color="green">{row.sym}</Text>
|
|
242
|
+
<Text color={row.fpColor}>{row.fp}</Text>
|
|
243
|
+
<Text color={row.modelColor}>{row.model}</Text>
|
|
244
|
+
</Text>
|
|
245
|
+
)}
|
|
249
246
|
</Box>
|
|
250
247
|
);
|
|
251
248
|
})}
|
package/src/commands/easyeda.ts
CHANGED
|
@@ -111,7 +111,7 @@ export async function easyedaInstallCommand(
|
|
|
111
111
|
// If UUID provided (without --force), launch TUI to fetch and display
|
|
112
112
|
if (uuid) {
|
|
113
113
|
// Launch TUI at EasyEDA info screen - let it fetch the component
|
|
114
|
-
renderApp('easyeda-info', { uuid })
|
|
114
|
+
await renderApp('easyeda-info', { uuid })
|
|
115
115
|
return
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -130,8 +130,7 @@ export async function easyedaInstallCommand(
|
|
|
130
130
|
process.exit(0)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
spinner.start(`Searching EasyEDA community for "${query}"...`)
|
|
133
|
+
console.log(`Searching EasyEDA community for "${query}"...`)
|
|
135
134
|
|
|
136
135
|
const searchOptions: SearchOptions = {
|
|
137
136
|
limit: 20,
|
|
@@ -139,13 +138,12 @@ export async function easyedaInstallCommand(
|
|
|
139
138
|
}
|
|
140
139
|
const results = await componentService.search(query as string, searchOptions)
|
|
141
140
|
|
|
142
|
-
spinner.stop(`Found ${results.length} results`)
|
|
143
|
-
|
|
144
141
|
if (results.length === 0) {
|
|
145
|
-
|
|
142
|
+
console.log('No components found. Try a different search term.')
|
|
146
143
|
return
|
|
147
144
|
}
|
|
148
145
|
|
|
149
|
-
//
|
|
150
|
-
|
|
146
|
+
// Clear the "Searching..." line and launch interactive UI
|
|
147
|
+
process.stdout.write('\x1b[1A\x1b[2K')
|
|
148
|
+
await renderApp('search', { query: query as string, results })
|
|
151
149
|
}
|
package/src/commands/info.ts
CHANGED
package/src/commands/install.ts
CHANGED
|
@@ -68,18 +68,15 @@ export async function installCommand(id: string | undefined, options: InstallOpt
|
|
|
68
68
|
|
|
69
69
|
// If ID provided (without --force), fetch component and launch TUI for install
|
|
70
70
|
if (id) {
|
|
71
|
-
|
|
72
|
-
spinner.start(`Fetching component ${id}...`);
|
|
71
|
+
console.log(`Fetching component ${id}...`);
|
|
73
72
|
|
|
74
73
|
try {
|
|
75
74
|
const details = await componentService.getDetails(id);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
renderApp('info', { componentId: id, component: details as any });
|
|
75
|
+
// Clear the "Fetching..." line and launch interactive UI
|
|
76
|
+
process.stdout.write('\x1b[1A\x1b[2K');
|
|
77
|
+
await renderApp('info', { componentId: id, component: details as any });
|
|
80
78
|
} catch (error) {
|
|
81
|
-
|
|
82
|
-
p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
79
|
+
console.error(`Failed to fetch component: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
83
80
|
process.exit(1);
|
|
84
81
|
}
|
|
85
82
|
return;
|
|
@@ -100,8 +97,7 @@ export async function installCommand(id: string | undefined, options: InstallOpt
|
|
|
100
97
|
process.exit(0);
|
|
101
98
|
}
|
|
102
99
|
|
|
103
|
-
|
|
104
|
-
spinner.start(`Searching for "${query}"...`);
|
|
100
|
+
console.log(`Searching for "${query}"...`);
|
|
105
101
|
|
|
106
102
|
const searchOptions: SearchOptions = { limit: 20 };
|
|
107
103
|
let results = await componentService.search(query as string, searchOptions);
|
|
@@ -113,13 +109,12 @@ export async function installCommand(id: string | undefined, options: InstallOpt
|
|
|
113
109
|
return 0;
|
|
114
110
|
});
|
|
115
111
|
|
|
116
|
-
spinner.stop(`Found ${results.length} results`);
|
|
117
|
-
|
|
118
112
|
if (results.length === 0) {
|
|
119
|
-
|
|
113
|
+
console.log('No components found. Try a different search term.');
|
|
120
114
|
return;
|
|
121
115
|
}
|
|
122
116
|
|
|
123
|
-
//
|
|
124
|
-
|
|
117
|
+
// Clear the "Searching..." line and launch interactive UI
|
|
118
|
+
process.stdout.write('\x1b[1A\x1b[2K');
|
|
119
|
+
await renderApp('search', { query: query as string, results });
|
|
125
120
|
}
|
package/src/commands/library.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
7
|
-
import
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { createLibraryService, type InstalledComponent } from '@jlcpcb/core';
|
|
8
9
|
import { renderApp } from '../app/App.js';
|
|
9
10
|
|
|
10
11
|
const libraryService = createLibraryService();
|
|
@@ -52,5 +53,53 @@ export async function libraryCommand(options: LibraryOptions): Promise<void> {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
// Interactive mode - launch TUI
|
|
55
|
-
renderApp('library', {});
|
|
56
|
+
await renderApp('library', {});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface RegenerateOptions {
|
|
60
|
+
projectPath?: string;
|
|
61
|
+
include3d?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function regenerateCommand(options: RegenerateOptions): Promise<void> {
|
|
65
|
+
const include3d = options.include3d ?? true;
|
|
66
|
+
|
|
67
|
+
// First get the count
|
|
68
|
+
const components = await libraryService.listInstalled({ projectPath: options.projectPath });
|
|
69
|
+
|
|
70
|
+
if (components.length === 0) {
|
|
71
|
+
p.log.warn('No installed components found to regenerate.');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
p.log.info(`Found ${chalk.cyan(components.length)} installed components to regenerate.`);
|
|
76
|
+
console.log();
|
|
77
|
+
|
|
78
|
+
let currentSpinner: ReturnType<typeof p.spinner> | null = null;
|
|
79
|
+
|
|
80
|
+
const result = await libraryService.regenerate({
|
|
81
|
+
projectPath: options.projectPath,
|
|
82
|
+
include3d,
|
|
83
|
+
onProgress: (current: number, total: number, component: InstalledComponent, status: 'start' | 'success' | 'error', error?: string) => {
|
|
84
|
+
if (status === 'start') {
|
|
85
|
+
currentSpinner = p.spinner();
|
|
86
|
+
currentSpinner.start(`[${current}/${total}] Regenerating ${chalk.cyan(component.name)} (${component.lcscId})...`);
|
|
87
|
+
} else if (status === 'success') {
|
|
88
|
+
currentSpinner?.stop(chalk.green(`✓ [${current}/${total}] ${component.name} (${component.lcscId})`));
|
|
89
|
+
} else if (status === 'error') {
|
|
90
|
+
currentSpinner?.stop(chalk.red(`✗ [${current}/${total}] ${component.name} (${component.lcscId}): ${error}`));
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log();
|
|
96
|
+
p.log.success(`Regeneration complete: ${chalk.green(result.success)} succeeded, ${chalk.red(result.failed)} failed`);
|
|
97
|
+
|
|
98
|
+
if (result.failed > 0) {
|
|
99
|
+
console.log();
|
|
100
|
+
p.log.warn('Failed components:');
|
|
101
|
+
for (const comp of result.components.filter(c => c.status === 'failed')) {
|
|
102
|
+
console.log(chalk.red(` • ${comp.name} (${comp.id}): ${comp.error}`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
56
105
|
}
|
package/src/commands/search.ts
CHANGED
|
@@ -30,7 +30,7 @@ export async function searchCommand(query: string, options: SearchOptions): Prom
|
|
|
30
30
|
|
|
31
31
|
// Clear the "Searching..." line and launch interactive UI
|
|
32
32
|
process.stdout.write('\x1b[1A\x1b[2K');
|
|
33
|
-
renderApp('search', { query, results });
|
|
33
|
+
await renderApp('search', { query, results });
|
|
34
34
|
} catch (error) {
|
|
35
35
|
console.error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
36
36
|
process.exit(1);
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate command
|
|
3
|
+
* Compare generated footprints/symbols against JLCPCB reference
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as p from '@clack/prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { writeFileSync } from 'fs';
|
|
9
|
+
import {
|
|
10
|
+
fetchReferenceSVG,
|
|
11
|
+
extractFromReferenceSVG,
|
|
12
|
+
extractFromKiCadFootprint,
|
|
13
|
+
compareFootprints,
|
|
14
|
+
formatComparisonResult,
|
|
15
|
+
extractSymbolFromReferenceSVG,
|
|
16
|
+
extractFromKiCadSymbol,
|
|
17
|
+
compareSymbols,
|
|
18
|
+
formatSymbolComparisonResult,
|
|
19
|
+
generateValidationReport,
|
|
20
|
+
generateBatchReport,
|
|
21
|
+
TEST_CATEGORIES,
|
|
22
|
+
getAllTestComponents,
|
|
23
|
+
getTestComponentsByCategory,
|
|
24
|
+
getCategoryNames,
|
|
25
|
+
renderFootprintSvg,
|
|
26
|
+
renderSymbolSvg,
|
|
27
|
+
type ValidationResult,
|
|
28
|
+
type ReportSvgs,
|
|
29
|
+
easyedaClient,
|
|
30
|
+
footprintConverter,
|
|
31
|
+
symbolConverter,
|
|
32
|
+
} from '@jlcpcb/core';
|
|
33
|
+
|
|
34
|
+
interface ValidateOptions {
|
|
35
|
+
footprintOnly?: boolean;
|
|
36
|
+
symbolOnly?: boolean;
|
|
37
|
+
category?: string;
|
|
38
|
+
all?: boolean;
|
|
39
|
+
html?: string;
|
|
40
|
+
json?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate a single component
|
|
45
|
+
*/
|
|
46
|
+
async function validateComponent(
|
|
47
|
+
lcscCode: string,
|
|
48
|
+
options: { footprint: boolean; symbol: boolean }
|
|
49
|
+
): Promise<ValidationResult> {
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
const normalizedCode = lcscCode.replace(/^C/i, '');
|
|
52
|
+
const fullCode = `C${normalizedCode}`;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Fetch reference SVG
|
|
56
|
+
const reference = await fetchReferenceSVG(fullCode);
|
|
57
|
+
|
|
58
|
+
// Fetch component data
|
|
59
|
+
const componentData = await easyedaClient.getComponentData(fullCode);
|
|
60
|
+
if (!componentData) {
|
|
61
|
+
return {
|
|
62
|
+
lcscCode: fullCode,
|
|
63
|
+
componentName: fullCode,
|
|
64
|
+
passed: false,
|
|
65
|
+
footprint: null,
|
|
66
|
+
symbol: null,
|
|
67
|
+
timestamp: new Date(),
|
|
68
|
+
durationMs: Date.now() - startTime,
|
|
69
|
+
error: 'Failed to fetch component data from EasyEDA',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let footprintResult = null;
|
|
74
|
+
let symbolResult = null;
|
|
75
|
+
|
|
76
|
+
// Validate footprint
|
|
77
|
+
if (options.footprint && reference.footprintSvg) {
|
|
78
|
+
const kicadContent = footprintConverter.convert(componentData, {});
|
|
79
|
+
|
|
80
|
+
const refData = extractFromReferenceSVG(reference.footprintSvg);
|
|
81
|
+
const genData = extractFromKiCadFootprint(kicadContent);
|
|
82
|
+
|
|
83
|
+
footprintResult = compareFootprints(refData, genData, {
|
|
84
|
+
sizeWarningsOnly: true,
|
|
85
|
+
positionTolerance: 50,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate symbol
|
|
90
|
+
if (options.symbol && reference.symbolSvg) {
|
|
91
|
+
const kicadContent = symbolConverter.convert(componentData, {
|
|
92
|
+
symbolName: componentData.info.name,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const refData = extractSymbolFromReferenceSVG(reference.symbolSvg);
|
|
96
|
+
const genData = extractFromKiCadSymbol(kicadContent);
|
|
97
|
+
|
|
98
|
+
symbolResult = compareSymbols(refData, genData, {
|
|
99
|
+
positionTolerance: 50,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const passed =
|
|
104
|
+
(!footprintResult || footprintResult.passed) && (!symbolResult || symbolResult.passed);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
lcscCode: fullCode,
|
|
108
|
+
componentName: componentData.info.name || fullCode,
|
|
109
|
+
passed,
|
|
110
|
+
footprint: footprintResult,
|
|
111
|
+
symbol: symbolResult,
|
|
112
|
+
timestamp: new Date(),
|
|
113
|
+
durationMs: Date.now() - startTime,
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
lcscCode: fullCode,
|
|
118
|
+
componentName: fullCode,
|
|
119
|
+
passed: false,
|
|
120
|
+
footprint: null,
|
|
121
|
+
symbol: null,
|
|
122
|
+
timestamp: new Date(),
|
|
123
|
+
durationMs: Date.now() - startTime,
|
|
124
|
+
error: error instanceof Error ? error.message : String(error),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function validateCommand(
|
|
130
|
+
id: string | undefined,
|
|
131
|
+
options: ValidateOptions
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const validateFootprint = !options.symbolOnly;
|
|
134
|
+
const validateSymbol = !options.footprintOnly;
|
|
135
|
+
|
|
136
|
+
// Single component validation
|
|
137
|
+
if (id && !options.category && !options.all) {
|
|
138
|
+
const spinner = p.spinner();
|
|
139
|
+
spinner.start(`Validating ${id}...`);
|
|
140
|
+
|
|
141
|
+
const result = await validateComponent(id, {
|
|
142
|
+
footprint: validateFootprint,
|
|
143
|
+
symbol: validateSymbol,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
spinner.stop(result.passed ? chalk.green('✓ Validation passed') : chalk.red('✗ Validation failed'));
|
|
147
|
+
|
|
148
|
+
// Display results
|
|
149
|
+
console.log();
|
|
150
|
+
console.log(chalk.bold(`${result.componentName} (${result.lcscCode})`));
|
|
151
|
+
console.log(chalk.dim(`Duration: ${result.durationMs}ms`));
|
|
152
|
+
console.log();
|
|
153
|
+
|
|
154
|
+
if (result.error) {
|
|
155
|
+
console.log(chalk.red(`Error: ${result.error}`));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (result.footprint) {
|
|
160
|
+
console.log(formatComparisonResult(result.footprint));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (result.symbol) {
|
|
164
|
+
console.log(formatSymbolComparisonResult(result.symbol));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// JSON output
|
|
168
|
+
if (options.json) {
|
|
169
|
+
console.log(JSON.stringify(result, null, 2));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// HTML output
|
|
173
|
+
if (options.html) {
|
|
174
|
+
const reference = await fetchReferenceSVG(id);
|
|
175
|
+
const componentData = await easyedaClient.getComponentData(id);
|
|
176
|
+
|
|
177
|
+
const svgs: ReportSvgs = {
|
|
178
|
+
footprintRef: reference.footprintSvg || undefined,
|
|
179
|
+
symbolRef: reference.symbolSvg || undefined,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Generate our KiCad output and render to SVG
|
|
183
|
+
if (componentData) {
|
|
184
|
+
if (validateFootprint) {
|
|
185
|
+
const kicadFootprint = footprintConverter.convert(componentData, {});
|
|
186
|
+
svgs.footprintGen = renderFootprintSvg(kicadFootprint);
|
|
187
|
+
}
|
|
188
|
+
if (validateSymbol) {
|
|
189
|
+
const kicadSymbol = symbolConverter.convert(componentData, {
|
|
190
|
+
symbolName: componentData.info.name,
|
|
191
|
+
});
|
|
192
|
+
svgs.symbolGen = renderSymbolSvg(kicadSymbol);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const html = generateValidationReport(result, svgs);
|
|
197
|
+
writeFileSync(options.html, html);
|
|
198
|
+
console.log(chalk.dim(`HTML report: ${options.html}`));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
process.exit(result.passed ? 0 : 1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Batch validation
|
|
205
|
+
let components = options.all
|
|
206
|
+
? getAllTestComponents()
|
|
207
|
+
: options.category
|
|
208
|
+
? getTestComponentsByCategory(options.category)
|
|
209
|
+
: [];
|
|
210
|
+
|
|
211
|
+
if (components.length === 0 && !id) {
|
|
212
|
+
// Interactive category selection
|
|
213
|
+
const categories = getCategoryNames();
|
|
214
|
+
const selected = await p.select({
|
|
215
|
+
message: 'Select a category to validate:',
|
|
216
|
+
options: [
|
|
217
|
+
{ value: 'all', label: 'All categories' },
|
|
218
|
+
...categories.map((c) => ({ value: c, label: c })),
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (p.isCancel(selected)) {
|
|
223
|
+
p.cancel('Validation cancelled');
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
components =
|
|
228
|
+
selected === 'all' ? getAllTestComponents() : getTestComponentsByCategory(selected as string);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Filter out skipped components
|
|
232
|
+
components = components.filter((c) => !c.skip);
|
|
233
|
+
|
|
234
|
+
if (components.length === 0) {
|
|
235
|
+
p.log.warn('No components to validate');
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log(chalk.bold(`\nValidating ${components.length} components...\n`));
|
|
240
|
+
|
|
241
|
+
const results: ValidationResult[] = [];
|
|
242
|
+
let passed = 0;
|
|
243
|
+
let failed = 0;
|
|
244
|
+
let errors = 0;
|
|
245
|
+
|
|
246
|
+
for (const component of components) {
|
|
247
|
+
process.stdout.write(` ${component.lcsc.padEnd(12)} ${component.name.slice(0, 40).padEnd(42)} `);
|
|
248
|
+
|
|
249
|
+
const result = await validateComponent(component.lcsc, {
|
|
250
|
+
footprint: validateFootprint,
|
|
251
|
+
symbol: validateSymbol,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
results.push(result);
|
|
255
|
+
|
|
256
|
+
if (result.error) {
|
|
257
|
+
errors++;
|
|
258
|
+
console.log(chalk.yellow('⚠ ERROR'));
|
|
259
|
+
} else if (result.passed) {
|
|
260
|
+
passed++;
|
|
261
|
+
console.log(chalk.green('✓ PASS'));
|
|
262
|
+
} else {
|
|
263
|
+
failed++;
|
|
264
|
+
console.log(chalk.red('✗ FAIL'));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Summary
|
|
269
|
+
console.log();
|
|
270
|
+
console.log(chalk.bold('Summary:'));
|
|
271
|
+
console.log(` ${chalk.green(`${passed} passed`)} ${chalk.red(`${failed} failed`)} ${chalk.yellow(`${errors} errors`)}`);
|
|
272
|
+
console.log();
|
|
273
|
+
|
|
274
|
+
// JSON output
|
|
275
|
+
if (options.json) {
|
|
276
|
+
console.log(JSON.stringify(results, null, 2));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// HTML output
|
|
280
|
+
if (options.html) {
|
|
281
|
+
const html = generateBatchReport(results, 'JLC-CLI Validation Report');
|
|
282
|
+
writeFileSync(options.html, html);
|
|
283
|
+
console.log(chalk.dim(`HTML report: ${options.html}`));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
process.exit(failed > 0 || errors > 0 ? 1 : 0);
|
|
287
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,8 +9,9 @@ import { Command } from 'commander';
|
|
|
9
9
|
import { searchCommand } from './commands/search.js';
|
|
10
10
|
import { infoCommand } from './commands/info.js';
|
|
11
11
|
import { installCommand } from './commands/install.js';
|
|
12
|
-
import { libraryCommand } from './commands/library.js';
|
|
12
|
+
import { libraryCommand, regenerateCommand } from './commands/library.js';
|
|
13
13
|
import { easyedaSearchCommand, easyedaInstallCommand } from './commands/easyeda.js';
|
|
14
|
+
import { validateCommand } from './commands/validate.js';
|
|
14
15
|
|
|
15
16
|
const program = new Command();
|
|
16
17
|
|
|
@@ -58,9 +59,14 @@ program
|
|
|
58
59
|
});
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
// Library subcommand group
|
|
63
|
+
const library = program
|
|
62
64
|
.command('library')
|
|
63
|
-
.description('
|
|
65
|
+
.description('JLC-MCP library management');
|
|
66
|
+
|
|
67
|
+
library
|
|
68
|
+
.command('status', { isDefault: true })
|
|
69
|
+
.description('View library status and installed components')
|
|
64
70
|
.option('--json', 'Output as JSON')
|
|
65
71
|
.action(async (options) => {
|
|
66
72
|
await libraryCommand({
|
|
@@ -68,6 +74,38 @@ program
|
|
|
68
74
|
});
|
|
69
75
|
});
|
|
70
76
|
|
|
77
|
+
library
|
|
78
|
+
.command('regenerate')
|
|
79
|
+
.description('Regenerate all installed components (refetch and reconvert symbols, footprints, 3D models)')
|
|
80
|
+
.option('-p, --project <path>', 'Regenerate project-local library')
|
|
81
|
+
.option('--no-3d', 'Skip 3D model download')
|
|
82
|
+
.action(async (options) => {
|
|
83
|
+
await regenerateCommand({
|
|
84
|
+
projectPath: options.project,
|
|
85
|
+
include3d: options['3d'],
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command('validate [id]')
|
|
91
|
+
.description('Validate footprint/symbol generation against JLCPCB reference')
|
|
92
|
+
.option('--footprint-only', 'Only validate footprint')
|
|
93
|
+
.option('--symbol-only', 'Only validate symbol')
|
|
94
|
+
.option('-c, --category <name>', 'Validate all components in category')
|
|
95
|
+
.option('-a, --all', 'Validate all test fixtures')
|
|
96
|
+
.option('--html <path>', 'Output HTML report to file')
|
|
97
|
+
.option('--json', 'Output as JSON')
|
|
98
|
+
.action(async (id, options) => {
|
|
99
|
+
await validateCommand(id, {
|
|
100
|
+
footprintOnly: options.footprintOnly,
|
|
101
|
+
symbolOnly: options.symbolOnly,
|
|
102
|
+
category: options.category,
|
|
103
|
+
all: options.all,
|
|
104
|
+
html: options.html,
|
|
105
|
+
json: options.json,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
71
109
|
// EasyEDA subcommand group
|
|
72
110
|
const easyeda = program
|
|
73
111
|
.command('easyeda')
|