@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.
- package/CHANGELOG.md +12 -0
- package/dist/index.js +501 -172
- package/package.json +1 -1
- package/src/app/components/ListView.tsx +1 -1
- package/src/app/components/list-view-format.test.ts +2 -0
- package/src/app/screens/InfoScreen.tsx +22 -6
- package/src/app/screens/InstalledScreen.tsx +1 -1
- package/src/app/screens/SearchScreen.tsx +11 -4
- package/src/commands/agent-json.test.ts +203 -0
- package/src/commands/easyeda.ts +63 -10
- package/src/commands/info.ts +6 -9
- package/src/commands/install.ts +39 -8
- package/src/commands/library.ts +13 -23
- package/src/commands/search.ts +35 -3
- package/src/commands/validate.ts +95 -43
- package/src/index.ts +38 -6
- package/src/utils/agent-output.ts +35 -0
- package/src/utils/search-result-output.ts +20 -0
package/package.json
CHANGED
|
@@ -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(
|
|
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:
|
|
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:
|
|
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">{
|
|
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}
|
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
});
|
package/src/commands/easyeda.ts
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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:
|
|
129
|
+
force: options.force,
|
|
89
130
|
})
|
|
90
131
|
|
|
91
|
-
spinner
|
|
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
|
|
105
|
-
|
|
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
|
package/src/commands/info.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
21
|
+
printJson({
|
|
22
|
+
success: true,
|
|
23
|
+
component: details,
|
|
24
|
+
});
|
|
27
25
|
} catch (error) {
|
|
28
|
-
|
|
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;
|
package/src/commands/install.ts
CHANGED
|
@@ -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 --
|
|
34
|
-
if (id && options.
|
|
35
|
-
const spinner = p.spinner();
|
|
36
|
-
spinner
|
|
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:
|
|
65
|
+
force: options.force,
|
|
47
66
|
});
|
|
48
67
|
|
|
49
|
-
spinner
|
|
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
|
|
63
|
-
|
|
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;
|
package/src/commands/library.ts
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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;
|