@jlcpcb/cli 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,11 +5,19 @@
5
5
 
6
6
  import { createComponentService, type SearchOptions } from '@jlcpcb/core';
7
7
  import { renderApp } from '../app/App.js';
8
+ import { printJson, printJsonError, getErrorMessage } from '../utils/agent-output.js';
9
+ import { formatSearchResultForJson } from '../utils/search-result-output.js';
8
10
 
9
11
  const componentService = createComponentService();
10
12
 
11
- export async function searchCommand(query: string, options: SearchOptions): Promise<void> {
12
- console.log(`Searching for "${query}"...`);
13
+ interface SearchCommandOptions extends SearchOptions {
14
+ json?: boolean;
15
+ }
16
+
17
+ export async function searchCommand(query: string, options: SearchCommandOptions): Promise<void> {
18
+ if (!options.json) {
19
+ console.log(`Searching for "${query}"...`);
20
+ }
13
21
 
14
22
  try {
15
23
  let results = await componentService.search(query, options);
@@ -24,15 +32,39 @@ export async function searchCommand(query: string, options: SearchOptions): Prom
24
32
  }
25
33
 
26
34
  if (results.length === 0) {
35
+ if (options.json) {
36
+ printJson({
37
+ success: true,
38
+ query,
39
+ count: 0,
40
+ results: [],
41
+ });
42
+ return;
43
+ }
44
+
27
45
  console.log('No components found. Try a different search term.');
28
46
  return;
29
47
  }
30
48
 
49
+ if (options.json) {
50
+ printJson({
51
+ success: true,
52
+ query,
53
+ count: results.length,
54
+ results: results.map(formatSearchResultForJson),
55
+ });
56
+ return;
57
+ }
58
+
31
59
  // Clear the "Searching..." line and launch interactive UI
32
60
  process.stdout.write('\x1b[1A\x1b[2K');
33
61
  await renderApp('search', { query, results });
34
62
  } catch (error) {
35
- console.error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
63
+ if (options.json) {
64
+ printJsonError('search_failed', getErrorMessage(error), { retryable: true });
65
+ } else {
66
+ console.error(`Search failed: ${getErrorMessage(error)}`);
67
+ }
36
68
  process.exit(1);
37
69
  }
38
70
  }
@@ -30,6 +30,7 @@ import {
30
30
  footprintConverter,
31
31
  symbolConverter,
32
32
  } from '@jlcpcb/core';
33
+ import { printJson, printJsonError } from '../utils/agent-output.js';
33
34
 
34
35
  interface ValidateOptions {
35
36
  footprintOnly?: boolean;
@@ -130,46 +131,32 @@ export async function validateCommand(
130
131
  id: string | undefined,
131
132
  options: ValidateOptions
132
133
  ): Promise<void> {
134
+ if (options.footprintOnly && options.symbolOnly) {
135
+ if (options.json) {
136
+ printJsonError('invalid_options', '--footprint-only and --symbol-only are mutually exclusive');
137
+ } else {
138
+ p.log.error('--footprint-only and --symbol-only are mutually exclusive');
139
+ }
140
+ process.exit(1);
141
+ }
142
+
133
143
  const validateFootprint = !options.symbolOnly;
134
144
  const validateSymbol = !options.footprintOnly;
135
145
 
136
146
  // Single component validation
137
147
  if (id && !options.category && !options.all) {
138
- const spinner = p.spinner();
139
- spinner.start(`Validating ${id}...`);
148
+ const spinner = options.json ? null : p.spinner();
149
+ spinner?.start(`Validating ${id}...`);
140
150
 
141
151
  const result = await validateComponent(id, {
142
152
  footprint: validateFootprint,
143
153
  symbol: validateSymbol,
144
154
  });
145
155
 
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
- }
156
+ spinner?.stop(result.passed ? chalk.green('✓ Validation passed') : chalk.red('✗ Validation failed'));
158
157
 
159
- if (result.footprint) {
160
- console.log(formatComparisonResult(result.footprint));
161
- }
158
+ let htmlPath: string | undefined;
162
159
 
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
160
  if (options.html) {
174
161
  const reference = await fetchReferenceSVG(id);
175
162
  const componentData = await easyedaClient.getComponentData(id);
@@ -195,9 +182,42 @@ export async function validateCommand(
195
182
 
196
183
  const html = generateValidationReport(result, svgs);
197
184
  writeFileSync(options.html, html);
198
- console.log(chalk.dim(`HTML report: ${options.html}`));
185
+ htmlPath = options.html;
186
+ }
187
+
188
+ if (options.json) {
189
+ printJson({
190
+ success: result.passed && !result.error,
191
+ result,
192
+ html: htmlPath,
193
+ });
194
+ process.exit(result.passed && !result.error ? 0 : 1);
195
+ }
196
+
197
+ // Display results
198
+ console.log();
199
+ console.log(chalk.bold(`${result.componentName} (${result.lcscCode})`));
200
+ console.log(chalk.dim(`Duration: ${result.durationMs}ms`));
201
+ console.log();
202
+
203
+ if (result.error) {
204
+ console.log(chalk.red(`Error: ${result.error}`));
205
+ process.exit(1);
206
+ }
207
+
208
+ if (result.footprint) {
209
+ console.log(formatComparisonResult(result.footprint));
210
+ }
211
+
212
+ if (result.symbol) {
213
+ console.log(formatSymbolComparisonResult(result.symbol));
199
214
  }
200
215
 
216
+ if (htmlPath) {
217
+ console.log(chalk.dim(`HTML report: ${htmlPath}`));
218
+ }
219
+
220
+
201
221
  process.exit(result.passed ? 0 : 1);
202
222
  }
203
223
 
@@ -209,6 +229,11 @@ export async function validateCommand(
209
229
  : [];
210
230
 
211
231
  if (components.length === 0 && !id) {
232
+ if (options.json) {
233
+ printJsonError('missing_validation_target', 'JSON validation requires an id, --category, or --all');
234
+ process.exit(1);
235
+ }
236
+
212
237
  // Interactive category selection
213
238
  const categories = getCategoryNames();
214
239
  const selected = await p.select({
@@ -232,11 +257,22 @@ export async function validateCommand(
232
257
  components = components.filter((c) => !c.skip);
233
258
 
234
259
  if (components.length === 0) {
260
+ if (options.json) {
261
+ printJson({
262
+ success: true,
263
+ summary: { total: 0, passed: 0, failed: 0, errors: 0 },
264
+ results: [],
265
+ });
266
+ process.exit(0);
267
+ }
268
+
235
269
  p.log.warn('No components to validate');
236
270
  process.exit(0);
237
271
  }
238
272
 
239
- console.log(chalk.bold(`\nValidating ${components.length} components...\n`));
273
+ if (!options.json) {
274
+ console.log(chalk.bold(`\nValidating ${components.length} components...\n`));
275
+ }
240
276
 
241
277
  const results: ValidationResult[] = [];
242
278
  let passed = 0;
@@ -244,7 +280,9 @@ export async function validateCommand(
244
280
  let errors = 0;
245
281
 
246
282
  for (const component of components) {
247
- process.stdout.write(` ${component.lcsc.padEnd(12)} ${component.name.slice(0, 40).padEnd(42)} `);
283
+ if (!options.json) {
284
+ process.stdout.write(` ${component.lcsc.padEnd(12)} ${component.name.slice(0, 40).padEnd(42)} `);
285
+ }
248
286
 
249
287
  const result = await validateComponent(component.lcsc, {
250
288
  footprint: validateFootprint,
@@ -255,32 +293,46 @@ export async function validateCommand(
255
293
 
256
294
  if (result.error) {
257
295
  errors++;
258
- console.log(chalk.yellow('⚠ ERROR'));
296
+ if (!options.json) console.log(chalk.yellow('⚠ ERROR'));
259
297
  } else if (result.passed) {
260
298
  passed++;
261
- console.log(chalk.green('✓ PASS'));
299
+ if (!options.json) console.log(chalk.green('✓ PASS'));
262
300
  } else {
263
301
  failed++;
264
- console.log(chalk.red('✗ FAIL'));
302
+ if (!options.json) console.log(chalk.red('✗ FAIL'));
265
303
  }
266
304
  }
267
305
 
306
+ let htmlPath: string | undefined;
307
+ if (options.html) {
308
+ const html = generateBatchReport(results, 'JLC-CLI Validation Report');
309
+ writeFileSync(options.html, html);
310
+ htmlPath = options.html;
311
+ }
312
+
313
+ if (options.json) {
314
+ printJson({
315
+ success: failed === 0 && errors === 0,
316
+ summary: {
317
+ total: results.length,
318
+ passed,
319
+ failed,
320
+ errors,
321
+ },
322
+ results,
323
+ html: htmlPath,
324
+ });
325
+ process.exit(failed > 0 || errors > 0 ? 1 : 0);
326
+ }
327
+
268
328
  // Summary
269
329
  console.log();
270
330
  console.log(chalk.bold('Summary:'));
271
331
  console.log(` ${chalk.green(`${passed} passed`)} ${chalk.red(`${failed} failed`)} ${chalk.yellow(`${errors} errors`)}`);
272
332
  console.log();
273
333
 
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}`));
334
+ if (htmlPath) {
335
+ console.log(chalk.dim(`HTML report: ${htmlPath}`));
284
336
  }
285
337
 
286
338
  process.exit(failed > 0 || errors > 0 ? 1 : 0);
package/src/index.ts CHANGED
@@ -6,34 +6,49 @@
6
6
  */
7
7
 
8
8
  import { Command } from 'commander';
9
+ import { createRequire } from 'module';
9
10
  import { searchCommand } from './commands/search.js';
10
11
  import { infoCommand } from './commands/info.js';
11
12
  import { installCommand } from './commands/install.js';
12
13
  import { libraryCommand, regenerateCommand } from './commands/library.js';
13
14
  import { easyedaSearchCommand, easyedaInstallCommand } from './commands/easyeda.js';
14
15
  import { validateCommand } from './commands/validate.js';
16
+ import { getErrorMessage } from './utils/agent-output.js';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const { version } = require('../package.json');
15
20
 
16
21
  const program = new Command();
17
22
 
23
+ function parsePositiveInt(value: string): number {
24
+ const parsed = Number.parseInt(value, 10);
25
+ if (!Number.isInteger(parsed) || parsed <= 0) {
26
+ throw new Error(`Expected a positive integer, got "${value}"`);
27
+ }
28
+ return parsed;
29
+ }
30
+
18
31
  program
19
32
  .name('jlc')
20
33
  .description('JLC/EasyEDA component sourcing and KiCad library management')
21
- .version('0.1.0');
34
+ .version(version);
22
35
 
23
36
  program
24
37
  .command('search <query...>')
25
38
  .description('Search for components (basic parts sorted first)')
26
- .option('-l, --limit <number>', 'Maximum results', '20')
39
+ .option('-l, --limit <number>', 'Maximum results', parsePositiveInt, 20)
27
40
  .option('--in-stock', 'Only show in-stock components')
28
41
  .option('--basic-only', 'Only show basic parts (no extended)')
29
42
  .option('--community', 'Search EasyEDA community library')
43
+ .option('--json', 'Output clean JSON and do not launch the TUI')
30
44
  .action(async (queryParts: string[], options) => {
31
45
  const query = queryParts.join(' ');
32
46
  await searchCommand(query, {
33
- limit: parseInt(options.limit, 10),
47
+ limit: options.limit,
34
48
  inStock: options.inStock,
35
49
  basicOnly: options.basicOnly || false,
36
50
  source: options.community ? 'easyeda-community' : 'lcsc',
51
+ json: options.json,
37
52
  });
38
53
  });
39
54
 
@@ -50,11 +65,15 @@ program
50
65
  .description('Install component to KiCad libraries')
51
66
  .option('-p, --project <path>', 'Install to project-local library')
52
67
  .option('--with-3d', 'Include 3D model')
68
+ .option('-y, --yes', 'Install directly without launching the TUI')
69
+ .option('--json', 'Output clean JSON and install directly')
53
70
  .option('-f, --force', 'Force reinstall (regenerate symbol and footprint)')
54
71
  .action(async (id, options) => {
55
72
  await installCommand(id, {
56
73
  projectPath: options.project,
57
74
  include3d: options.with3d,
75
+ yes: options.yes,
76
+ json: options.json,
58
77
  force: options.force,
59
78
  });
60
79
  });
@@ -114,11 +133,17 @@ const easyeda = program
114
133
  easyeda
115
134
  .command('search <query...>')
116
135
  .description('Open browser-based component search')
117
- .option('-p, --port <number>', 'HTTP server port', '3847')
136
+ .option('-p, --port <number>', 'HTTP server port', parsePositiveInt, 3847)
137
+ .option('--json', 'Output clean JSON search results and do not open a browser')
138
+ .option('--no-open', 'Start the browser server without opening a browser')
139
+ .option('--once', 'Print the browser URL and exit after the server is ready')
118
140
  .action(async (queryParts: string[], options) => {
119
141
  const query = queryParts.join(' ');
120
142
  await easyedaSearchCommand(query, {
121
- port: options.port ? parseInt(options.port, 10) : undefined,
143
+ port: options.port,
144
+ json: options.json,
145
+ open: options.open,
146
+ once: options.once,
122
147
  });
123
148
  });
124
149
 
@@ -127,13 +152,20 @@ easyeda
127
152
  .description('Install EasyEDA community component to KiCad libraries')
128
153
  .option('-p, --project <path>', 'Install to project-local library')
129
154
  .option('--with-3d', 'Include 3D model')
155
+ .option('-y, --yes', 'Install directly without launching the TUI')
156
+ .option('--json', 'Output clean JSON and install directly')
130
157
  .option('-f, --force', 'Force reinstall (regenerate symbol and footprint)')
131
158
  .action(async (uuid, options) => {
132
159
  await easyedaInstallCommand(uuid, {
133
160
  projectPath: options.project,
134
161
  include3d: options.with3d,
162
+ yes: options.yes,
163
+ json: options.json,
135
164
  force: options.force,
136
165
  });
137
166
  });
138
167
 
139
- program.parse();
168
+ program.parseAsync().catch((error) => {
169
+ console.error(`Error: ${getErrorMessage(error)}`);
170
+ process.exit(1);
171
+ });
@@ -0,0 +1,35 @@
1
+ export interface JsonError {
2
+ success: false;
3
+ error: {
4
+ code: string;
5
+ message: string;
6
+ retryable: boolean;
7
+ details?: unknown;
8
+ };
9
+ }
10
+
11
+ export function printJson(value: unknown): void {
12
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
13
+ }
14
+
15
+ export function printJsonError(
16
+ code: string,
17
+ message: string,
18
+ options: { retryable?: boolean; details?: unknown } = {}
19
+ ): void {
20
+ const payload: JsonError = {
21
+ success: false,
22
+ error: {
23
+ code,
24
+ message,
25
+ retryable: options.retryable ?? false,
26
+ details: options.details,
27
+ },
28
+ };
29
+
30
+ printJson(payload);
31
+ }
32
+
33
+ export function getErrorMessage(error: unknown): string {
34
+ return error instanceof Error ? error.message : String(error);
35
+ }
@@ -0,0 +1,20 @@
1
+ import type { ComponentSearchResult } from '@jlcpcb/core';
2
+
3
+ export function formatSearchResultForJson(result: ComponentSearchResult) {
4
+ return {
5
+ id: result.id,
6
+ id_type: result.idType,
7
+ lcsc_id: result.idType === 'lcsc' ? result.lcscId : undefined,
8
+ easyeda_uuid: result.idType === 'easyeda_uuid' ? result.easyedaUuid ?? result.id : undefined,
9
+ name: result.name,
10
+ manufacturer: result.manufacturer,
11
+ description: result.description,
12
+ package: result.package,
13
+ datasheet: result.datasheetPdf,
14
+ stock: result.stock,
15
+ price: result.price,
16
+ library_type: result.libraryType,
17
+ category: result.category,
18
+ attributes: result.attributes,
19
+ };
20
+ }