@jlcpcb/cli 0.3.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jlcpcb/cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "CLI for JLC/EasyEDA component sourcing and KiCad library management",
@@ -43,13 +43,13 @@
43
43
  "commander": "^12.0.0",
44
44
  "ink": "6.6.0",
45
45
  "ink-select-input": "6.2.0",
46
- "@jlcpcb/core": "workspace:*",
47
46
  "open": "11.0.0",
48
47
  "react": "^19.0.0",
49
48
  "react-devtools-core": "7.0.1",
50
49
  "string-width": "^8.2.0"
51
50
  },
52
51
  "devDependencies": {
52
+ "@jlcpcb/core": "workspace:*",
53
53
  "@types/node": "^20.0.0",
54
54
  "@types/react": "19.2.7"
55
55
  }
@@ -33,7 +33,7 @@ export function ListView({
33
33
  const row = formatListRow(r, widths);
34
34
 
35
35
  return (
36
- <Box key={r.lcscId}>
36
+ <Box key={r.id ?? r.lcscId}>
37
37
  <Text color="cyan">{isSelected ? '▶' : ' '}</Text>
38
38
  {isSelected ? (
39
39
  <Text inverse>
@@ -13,6 +13,8 @@ function createResult(
13
13
  overrides: Partial<ComponentSearchResult>
14
14
  ): ComponentSearchResult {
15
15
  return {
16
+ id: 'C123',
17
+ idType: 'lcsc',
16
18
  lcscId: 'C123',
17
19
  name: 'LM2940CT-5.0/NOPB',
18
20
  description:
@@ -71,6 +71,7 @@ export function InfoScreen() {
71
71
 
72
72
  // Get datasheet URL (different field names in different types)
73
73
  const datasheetUrl = component && ('datasheetPdf' in component ? component.datasheetPdf : 'datasheet' in component ? component.datasheet : undefined);
74
+ const componentLcscId = component?.lcscId ?? params.componentId;
74
75
 
75
76
  const [isRegenerating, setIsRegenerating] = useState(false);
76
77
  const [isDeleting, setIsDeleting] = useState(false);
@@ -82,11 +83,11 @@ export function InfoScreen() {
82
83
  const lowerInput = input.toLowerCase();
83
84
 
84
85
  // R - Regenerate symbol and footprint
85
- if (lowerInput === 'r' && installedInfo) {
86
+ if (lowerInput === 'r' && installedInfo && componentLcscId) {
86
87
  setIsRegenerating(true);
87
88
  setRegenerateMessage('Regenerating symbol and footprint...');
88
89
 
89
- libraryService.install(component.lcscId, { force: true })
90
+ libraryService.install(componentLcscId, { force: true })
90
91
  .then((result) => {
91
92
  setRegenerateMessage(`✓ Regenerated: ${result.symbolAction}`);
92
93
  // Clear message after 2 seconds
@@ -127,6 +128,12 @@ export function InfoScreen() {
127
128
 
128
129
  // Enter - Install (only when not installed)
129
130
  if (key.return && !installedInfo) {
131
+ if (!componentLcscId) {
132
+ setRegenerateMessage('✗ Missing LCSC part number');
133
+ setTimeout(() => setRegenerateMessage(null), 3000);
134
+ return;
135
+ }
136
+
130
137
  if (checkingRef.current) return;
131
138
  checkingRef.current = true;
132
139
  setIsCheckingLibrary(true);
@@ -134,7 +141,7 @@ export function InfoScreen() {
134
141
  if (libraryStatus && (!libraryStatus.installed || !libraryStatus.linked)) {
135
142
  // Libraries not set up - show setup screen
136
143
  push('library-setup', {
137
- componentId: component.lcscId,
144
+ componentId: componentLcscId,
138
145
  component,
139
146
  });
140
147
  setIsCheckingLibrary(false);
@@ -142,7 +149,7 @@ export function InfoScreen() {
142
149
  } else {
143
150
  // Libraries ready - proceed to install
144
151
  push('install', {
145
- componentId: component.lcscId,
152
+ componentId: componentLcscId,
146
153
  component,
147
154
  });
148
155
  setIsCheckingLibrary(false);
@@ -176,18 +183,27 @@ export function InfoScreen() {
176
183
  );
177
184
  }
178
185
 
186
+ if (!componentLcscId) {
187
+ return (
188
+ <Box flexDirection="column">
189
+ <Text color="red">✗ Missing LCSC part number</Text>
190
+ <Text dimColor>Press Esc to go back</Text>
191
+ </Box>
192
+ );
193
+ }
194
+
179
195
  return (
180
196
  <Box flexDirection="column">
181
197
  <Box marginBottom={1}>
182
198
  <Text bold>
183
- Component: <Text color="cyan">{component.lcscId}</Text>
199
+ Component: <Text color="cyan">{componentLcscId}</Text>
184
200
  {' '}
185
201
  <Text dimColor>({component.name})</Text>
186
202
  {installedInfo && <Text color="green"> ✓ Installed</Text>}
187
203
  </Text>
188
204
  </Box>
189
205
  <DetailView
190
- component={component}
206
+ component={{ ...component, lcscId: componentLcscId }}
191
207
  terminalWidth={terminalWidth}
192
208
  isInstalled={!!installedInfo}
193
209
  installedInfo={installedInfo}
@@ -29,7 +29,7 @@ export function InstalledScreen() {
29
29
  </Text>
30
30
  </Box>
31
31
  <InstalledView
32
- component={params.component}
32
+ component={{ ...params.component, lcscId: params.componentId }}
33
33
  result={result}
34
34
  error={params.error || null}
35
35
  terminalWidth={terminalWidth}
@@ -26,10 +26,17 @@ export function SearchScreen() {
26
26
  } else if (key.downArrow) {
27
27
  setSelectedIndex(Math.min(results.length - 1, selectedIndex + 1));
28
28
  } else if (key.return && results[selectedIndex]) {
29
- push('info', {
30
- componentId: results[selectedIndex].lcscId,
31
- component: results[selectedIndex],
32
- });
29
+ const selected = results[selectedIndex];
30
+ if (selected.idType === 'easyeda_uuid') {
31
+ push('easyeda-info', {
32
+ uuid: selected.easyedaUuid ?? selected.id,
33
+ });
34
+ } else {
35
+ push('info', {
36
+ componentId: selected.lcscId ?? selected.id,
37
+ component: selected,
38
+ });
39
+ }
33
40
  } else if (key.tab) {
34
41
  setIsSearching(true);
35
42
  const newFiltered = !isFiltered;
@@ -0,0 +1,203 @@
1
+ import { describe, expect, it, mock, spyOn } from 'bun:test';
2
+ import { readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ const mockDetails = {
6
+ lcscId: 'C123',
7
+ name: 'Part',
8
+ manufacturer: 'Maker',
9
+ description: 'A part',
10
+ category: 'ICs',
11
+ package: 'SOT-23',
12
+ pinCount: 3,
13
+ padCount: 3,
14
+ has3DModel: false,
15
+ };
16
+
17
+ const mockSearchResults = [{
18
+ id: 'C123',
19
+ idType: 'lcsc' as const,
20
+ lcscId: 'C123',
21
+ name: 'Part',
22
+ manufacturer: 'Maker',
23
+ description: 'A part',
24
+ package: 'SOT-23',
25
+ stock: 10,
26
+ }];
27
+
28
+ const mockInstallResult = {
29
+ success: true,
30
+ id: 'C123',
31
+ source: 'lcsc' as const,
32
+ storageMode: 'global' as const,
33
+ category: 'ICs',
34
+ symbolName: 'Part',
35
+ symbolRef: 'JLC-MCP-ICs:Part',
36
+ footprintRef: 'Package:SOT-23',
37
+ footprintType: 'reference' as const,
38
+ files: {
39
+ symbolLibrary: '/tmp/symbols/JLC-MCP-ICs.kicad_sym',
40
+ },
41
+ symbolAction: 'exists' as const,
42
+ validationData: {
43
+ component: { name: 'Part' },
44
+ symbol: { pin_count: 3, pins: [] },
45
+ footprint: {
46
+ type: 'smd',
47
+ pad_count: 3,
48
+ pads: null,
49
+ is_kicad_standard: true,
50
+ kicad_ref: 'Package:SOT-23',
51
+ },
52
+ checks: {
53
+ pin_pad_count_match: true,
54
+ has_power_pins: false,
55
+ has_ground_pins: false,
56
+ },
57
+ },
58
+ };
59
+
60
+ mock.module('@jlcpcb/core', () => ({
61
+ createComponentService: () => ({
62
+ getDetails: async () => mockDetails,
63
+ search: async () => mockSearchResults,
64
+ }),
65
+ createLibraryService: () => ({
66
+ ensureGlobalTables: async () => undefined,
67
+ install: async () => mockInstallResult,
68
+ getStatus: async () => ({
69
+ installed: true,
70
+ linked: true,
71
+ version: '9.0',
72
+ componentCount: 1,
73
+ paths: {
74
+ symbolsDir: '/tmp/symbols',
75
+ footprintsDir: '/tmp/footprints',
76
+ models3dDir: '/tmp/models',
77
+ symLibTable: '/tmp/sym-lib-table',
78
+ fpLibTable: '/tmp/fp-lib-table',
79
+ },
80
+ }),
81
+ listInstalled: async () => [{
82
+ lcscId: 'C123',
83
+ name: 'Part',
84
+ category: 'ICs',
85
+ symbolRef: 'JLC-MCP-ICs:Part',
86
+ footprintRef: 'Package:SOT-23',
87
+ library: 'JLC-MCP-ICs',
88
+ has3dModel: false,
89
+ }],
90
+ }),
91
+ startHttpServer: () => 3847,
92
+ stopHttpServer: () => undefined,
93
+ }));
94
+
95
+ describe('agent JSON CLI output', () => {
96
+ it('info --json writes only a JSON object', async () => {
97
+ const write = spyOn(process.stdout, 'write').mockImplementation(() => true);
98
+ const { infoCommand } = await import('./info.js');
99
+
100
+ await infoCommand('C123', { json: true });
101
+
102
+ expect(write).toHaveBeenCalledTimes(1);
103
+ expect(JSON.parse(String(write.mock.calls[0][0]))).toEqual({
104
+ success: true,
105
+ component: mockDetails,
106
+ });
107
+ write.mockRestore();
108
+ });
109
+
110
+ it('search --json writes only a JSON object and does not require the TUI', async () => {
111
+ const write = spyOn(process.stdout, 'write').mockImplementation(() => true);
112
+ const { searchCommand } = await import('./search.js');
113
+
114
+ await searchCommand('part', { json: true, limit: 1, source: 'lcsc' });
115
+
116
+ const payload = JSON.parse(String(write.mock.calls[0][0]));
117
+ expect(payload).toMatchObject({
118
+ success: true,
119
+ query: 'part',
120
+ count: 1,
121
+ results: [{
122
+ id: 'C123',
123
+ id_type: 'lcsc',
124
+ lcsc_id: 'C123',
125
+ name: 'Part',
126
+ }],
127
+ });
128
+ expect(write).toHaveBeenCalledTimes(1);
129
+ write.mockRestore();
130
+ });
131
+
132
+ it('library status --json writes only a JSON object', async () => {
133
+ const write = spyOn(process.stdout, 'write').mockImplementation(() => true);
134
+ const { libraryCommand } = await import('./library.js');
135
+
136
+ await libraryCommand({ json: true });
137
+
138
+ const payload = JSON.parse(String(write.mock.calls[0][0]));
139
+ expect(payload.success).toBe(true);
140
+ expect(payload.status.installed).toBe(true);
141
+ expect(payload.components).toHaveLength(1);
142
+ expect(write).toHaveBeenCalledTimes(1);
143
+ write.mockRestore();
144
+ });
145
+
146
+ it('install --json writes only a JSON object and does not require --force', async () => {
147
+ const write = spyOn(process.stdout, 'write').mockImplementation(() => true);
148
+ const { installCommand } = await import('./install.js');
149
+
150
+ await installCommand('C123', { json: true });
151
+
152
+ const payload = JSON.parse(String(write.mock.calls[0][0]));
153
+ expect(payload).toMatchObject({
154
+ success: true,
155
+ result: {
156
+ id: 'C123',
157
+ symbolAction: 'exists',
158
+ },
159
+ });
160
+ expect(write).toHaveBeenCalledTimes(1);
161
+ write.mockRestore();
162
+ });
163
+
164
+ it('easyeda search --json writes normalized search results', async () => {
165
+ const write = spyOn(process.stdout, 'write').mockImplementation(() => true);
166
+ const { easyedaSearchCommand } = await import('./easyeda.js');
167
+
168
+ await easyedaSearchCommand('part', { json: true });
169
+
170
+ const payload = JSON.parse(String(write.mock.calls[0][0]));
171
+ expect(payload.results[0]).toMatchObject({
172
+ id: 'C123',
173
+ id_type: 'lcsc',
174
+ });
175
+ expect(write).toHaveBeenCalledTimes(1);
176
+ write.mockRestore();
177
+ });
178
+
179
+ it('easyeda install --json writes only a JSON object and does not require --force', async () => {
180
+ const write = spyOn(process.stdout, 'write').mockImplementation(() => true);
181
+ const { easyedaInstallCommand } = await import('./easyeda.js');
182
+
183
+ await easyedaInstallCommand('8007c710c0b9406db963b55df6990340', { json: true });
184
+
185
+ const payload = JSON.parse(String(write.mock.calls[0][0]));
186
+ expect(payload).toMatchObject({
187
+ success: true,
188
+ result: {
189
+ symbolRef: 'JLC-MCP-ICs:Part',
190
+ },
191
+ });
192
+ expect(write).toHaveBeenCalledTimes(1);
193
+ write.mockRestore();
194
+ });
195
+
196
+ it('validate --json has a quiet JSON path before human output', () => {
197
+ const source = readFileSync(join(import.meta.dir, 'validate.ts'), 'utf-8');
198
+
199
+ expect(source).toContain('const spinner = options.json ? null : p.spinner()');
200
+ expect(source).toContain('printJson({');
201
+ expect(source.indexOf('if (options.json)')).toBeLessThan(source.indexOf('console.log(chalk.bold'));
202
+ });
203
+ });
@@ -14,12 +14,17 @@ import {
14
14
  type SearchOptions,
15
15
  } from '@jlcpcb/core'
16
16
  import { renderApp } from '../app/App.js'
17
+ import { printJson, printJsonError, getErrorMessage } from '../utils/agent-output.js'
18
+ import { formatSearchResultForJson } from '../utils/search-result-output.js'
17
19
 
18
20
  const componentService = createComponentService()
19
21
  const libraryService = createLibraryService()
20
22
 
21
23
  interface EasyedaSearchOptions {
22
24
  port?: number
25
+ json?: boolean
26
+ open?: boolean
27
+ once?: boolean
23
28
  }
24
29
 
25
30
  /**
@@ -29,6 +34,25 @@ export async function easyedaSearchCommand(
29
34
  query: string,
30
35
  options: EasyedaSearchOptions
31
36
  ): Promise<void> {
37
+ if (options.json) {
38
+ try {
39
+ const results = await componentService.search(query, {
40
+ limit: 20,
41
+ source: 'easyeda-community',
42
+ })
43
+ printJson({
44
+ success: true,
45
+ query,
46
+ count: results.length,
47
+ results: results.map(formatSearchResultForJson),
48
+ })
49
+ } catch (error) {
50
+ printJsonError('easyeda_search_failed', getErrorMessage(error), { retryable: true })
51
+ process.exit(1)
52
+ }
53
+ return
54
+ }
55
+
32
56
  const port = options.port ?? 3847
33
57
 
34
58
  console.log('Starting component browser...')
@@ -41,8 +65,14 @@ export async function easyedaSearchCommand(
41
65
 
42
66
  console.log(`Browser opened at ${searchUrl}`)
43
67
 
44
- // Open browser
45
- await open(searchUrl)
68
+ if (options.open !== false) {
69
+ await open(searchUrl)
70
+ }
71
+
72
+ if (options.once) {
73
+ stopHttpServer()
74
+ process.exit(0)
75
+ }
46
76
 
47
77
  console.log('Press Ctrl+C to stop the server and exit')
48
78
  }
@@ -62,6 +92,8 @@ export async function easyedaSearchCommand(
62
92
  interface EasyedaInstallOptions {
63
93
  projectPath?: string
64
94
  include3d?: boolean
95
+ yes?: boolean
96
+ json?: boolean
65
97
  force?: boolean
66
98
  }
67
99
 
@@ -72,10 +104,19 @@ export async function easyedaInstallCommand(
72
104
  uuid: string | undefined,
73
105
  options: EasyedaInstallOptions
74
106
  ): Promise<void> {
75
- // If UUID provided with --force, do direct install (non-interactive)
76
- if (uuid && options.force) {
77
- const spinner = p.spinner()
78
- spinner.start(`Installing EasyEDA component ${uuid}...`)
107
+ if (!uuid && (options.yes || options.json)) {
108
+ if (options.json) {
109
+ printJsonError('missing_uuid', 'Direct EasyEDA install requires a component UUID')
110
+ } else {
111
+ p.log.error('Direct EasyEDA install requires a component UUID.')
112
+ }
113
+ process.exit(1)
114
+ }
115
+
116
+ // If UUID provided with --yes or --json, do direct install (non-interactive)
117
+ if (uuid && (options.yes || options.json)) {
118
+ const spinner = options.json ? null : p.spinner()
119
+ spinner?.start(`Installing EasyEDA component ${uuid}...`)
79
120
 
80
121
  try {
81
122
  // Ensure libraries are set up
@@ -85,10 +126,18 @@ export async function easyedaInstallCommand(
85
126
  const result = await libraryService.install(uuid, {
86
127
  projectPath: options.projectPath,
87
128
  include3d: options.include3d,
88
- force: true,
129
+ force: options.force,
89
130
  })
90
131
 
91
- spinner.stop(chalk.green('✓ Component installed'))
132
+ spinner?.stop(chalk.green('✓ Component installed'))
133
+
134
+ if (options.json) {
135
+ printJson({
136
+ success: true,
137
+ result,
138
+ })
139
+ return
140
+ }
92
141
 
93
142
  // Display result
94
143
  console.log()
@@ -101,8 +150,12 @@ export async function easyedaInstallCommand(
101
150
  console.log()
102
151
  console.log(chalk.dim(`Library: ${result.files.symbolLibrary}`))
103
152
  } catch (error) {
104
- spinner.stop(chalk.red('✗ Installation failed'))
105
- p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
153
+ spinner?.stop(chalk.red('✗ Installation failed'))
154
+ if (options.json) {
155
+ printJsonError('easyeda_install_failed', getErrorMessage(error), { retryable: true })
156
+ } else {
157
+ p.log.error(`Error: ${getErrorMessage(error)}`)
158
+ }
106
159
  process.exit(1)
107
160
  }
108
161
  return
@@ -3,10 +3,9 @@
3
3
  * Display component details
4
4
  */
5
5
 
6
- import * as p from '@clack/prompts';
7
- import chalk from 'chalk';
8
6
  import { createComponentService } from '@jlcpcb/core';
9
7
  import { renderApp } from '../app/App.js';
8
+ import { printJson, printJsonError, getErrorMessage } from '../utils/agent-output.js';
10
9
 
11
10
  const componentService = createComponentService();
12
11
 
@@ -17,16 +16,14 @@ interface InfoOptions {
17
16
  export async function infoCommand(id: string, options: InfoOptions): Promise<void> {
18
17
  // JSON mode - non-interactive output for scripting
19
18
  if (options.json) {
20
- const spinner = p.spinner();
21
- spinner.start(`Fetching component ${id}...`);
22
-
23
19
  try {
24
20
  const details = await componentService.getDetails(id);
25
- spinner.stop('Component found');
26
- console.log(JSON.stringify(details, null, 2));
21
+ printJson({
22
+ success: true,
23
+ component: details,
24
+ });
27
25
  } catch (error) {
28
- spinner.stop('Failed to fetch component');
29
- p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
26
+ printJsonError('component_fetch_failed', getErrorMessage(error), { retryable: true });
30
27
  process.exit(1);
31
28
  }
32
29
  return;
@@ -7,6 +7,7 @@ import * as p from '@clack/prompts';
7
7
  import chalk from 'chalk';
8
8
  import { createComponentService, createLibraryService, type SearchOptions } from '@jlcpcb/core';
9
9
  import { renderApp } from '../app/App.js';
10
+ import { printJson, printJsonError, getErrorMessage } from '../utils/agent-output.js';
10
11
 
11
12
  const componentService = createComponentService();
12
13
  const libraryService = createLibraryService();
@@ -19,21 +20,39 @@ function isLcscId(id: string): boolean {
19
20
  interface InstallOptions {
20
21
  projectPath?: string;
21
22
  include3d?: boolean;
23
+ yes?: boolean;
24
+ json?: boolean;
22
25
  force?: boolean;
23
26
  }
24
27
 
25
28
  export async function installCommand(id: string | undefined, options: InstallOptions): Promise<void> {
29
+ if (!id && (options.yes || options.json)) {
30
+ if (options.json) {
31
+ printJsonError('missing_id', 'Direct install requires an LCSC part number');
32
+ } else {
33
+ p.log.error('Direct install requires an LCSC part number.');
34
+ }
35
+ process.exit(1);
36
+ }
37
+
26
38
  // Check if ID looks like an EasyEDA UUID (not an LCSC ID)
27
39
  if (id && !isLcscId(id)) {
40
+ if (options.json) {
41
+ printJsonError('invalid_lcsc_id', `"${id}" is not an LCSC part number`, {
42
+ details: { hint: `Use jlc easyeda install ${id}` },
43
+ });
44
+ process.exit(1);
45
+ }
46
+
28
47
  p.log.error(`"${id}" is not an LCSC part number (e.g., C2040).`);
29
48
  p.log.info(`For EasyEDA community components, use: ${chalk.cyan(`jlc easyeda install ${id}`)}`);
30
49
  process.exit(1);
31
50
  }
32
51
 
33
- // If ID provided with --force, do direct install (non-interactive)
34
- if (id && options.force) {
35
- const spinner = p.spinner();
36
- spinner.start(`Installing component ${id}...`);
52
+ // If ID provided with --yes or --json, do direct install (non-interactive).
53
+ if (id && (options.yes || options.json)) {
54
+ const spinner = options.json ? null : p.spinner();
55
+ spinner?.start(`Installing component ${id}...`);
37
56
 
38
57
  try {
39
58
  // Ensure libraries are set up
@@ -43,10 +62,18 @@ export async function installCommand(id: string | undefined, options: InstallOpt
43
62
  const result = await libraryService.install(id, {
44
63
  projectPath: options.projectPath,
45
64
  include3d: options.include3d,
46
- force: true,
65
+ force: options.force,
47
66
  });
48
67
 
49
- spinner.stop(chalk.green('✓ Component installed'));
68
+ spinner?.stop(chalk.green('✓ Component installed'));
69
+
70
+ if (options.json) {
71
+ printJson({
72
+ success: true,
73
+ result,
74
+ });
75
+ return;
76
+ }
50
77
 
51
78
  // Display result
52
79
  console.log();
@@ -59,8 +86,12 @@ export async function installCommand(id: string | undefined, options: InstallOpt
59
86
  console.log();
60
87
  console.log(chalk.dim(`Library: ${result.files.symbolLibrary}`));
61
88
  } catch (error) {
62
- spinner.stop(chalk.red('✗ Installation failed'));
63
- p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
89
+ spinner?.stop(chalk.red('✗ Installation failed'));
90
+ if (options.json) {
91
+ printJsonError('install_failed', getErrorMessage(error), { retryable: true });
92
+ } else {
93
+ p.log.error(`Error: ${getErrorMessage(error)}`);
94
+ }
64
95
  process.exit(1);
65
96
  }
66
97
  return;
@@ -7,6 +7,7 @@ import * as p from '@clack/prompts';
7
7
  import chalk from 'chalk';
8
8
  import { createLibraryService, type InstalledComponent } from '@jlcpcb/core';
9
9
  import { renderApp } from '../app/App.js';
10
+ import { printJson, printJsonError, getErrorMessage } from '../utils/agent-output.js';
10
11
 
11
12
  const libraryService = createLibraryService();
12
13
 
@@ -17,36 +18,25 @@ interface LibraryOptions {
17
18
  export async function libraryCommand(options: LibraryOptions): Promise<void> {
18
19
  // JSON mode - non-interactive output for scripting
19
20
  if (options.json) {
20
- const spinner = p.spinner();
21
- spinner.start('Loading library status...');
22
-
23
21
  try {
24
22
  const [status, components] = await Promise.all([
25
23
  libraryService.getStatus(),
26
24
  libraryService.listInstalled({}),
27
25
  ]);
28
26
 
29
- spinner.stop(`Found ${components.length} installed components`);
30
-
31
- console.log(
32
- JSON.stringify(
33
- {
34
- status: {
35
- installed: status.installed,
36
- linked: status.linked,
37
- version: status.version,
38
- componentCount: status.componentCount,
39
- paths: status.paths,
40
- },
41
- components,
42
- },
43
- null,
44
- 2
45
- )
46
- );
27
+ printJson({
28
+ success: true,
29
+ status: {
30
+ installed: status.installed,
31
+ linked: status.linked,
32
+ version: status.version,
33
+ componentCount: status.componentCount,
34
+ paths: status.paths,
35
+ },
36
+ components,
37
+ });
47
38
  } catch (error) {
48
- spinner.stop('Failed to get library status');
49
- p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
39
+ printJsonError('library_status_failed', getErrorMessage(error), { retryable: false });
50
40
  process.exit(1);
51
41
  }
52
42
  return;