@jlcpcb/cli 0.1.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 +15 -0
- package/README.md +251 -0
- package/dist/index.js +51677 -0
- package/package.json +53 -0
- package/src/app/App.tsx +85 -0
- package/src/app/components/DetailView.tsx +173 -0
- package/src/app/components/Divider.tsx +10 -0
- package/src/app/components/InstalledView.tsx +70 -0
- package/src/app/components/ListView.tsx +93 -0
- package/src/app/hooks/useTerminalSize.ts +29 -0
- package/src/app/navigation/NavigationContext.tsx +87 -0
- package/src/app/navigation/types.ts +66 -0
- package/src/app/screens/InfoScreen.tsx +203 -0
- package/src/app/screens/InstallScreen.tsx +54 -0
- package/src/app/screens/InstalledScreen.tsx +39 -0
- package/src/app/screens/LibraryScreen.tsx +259 -0
- package/src/app/screens/LibrarySetupScreen.tsx +120 -0
- package/src/app/screens/SearchScreen.tsx +74 -0
- package/src/app/state/AppStateContext.tsx +64 -0
- package/src/commands/info.ts +37 -0
- package/src/commands/install.ts +113 -0
- package/src/commands/library.ts +56 -0
- package/src/commands/search.ts +38 -0
- package/src/index.ts +70 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { createComponentService, createLibraryService, type InstalledComponent } from '@jlcpcb/core';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
|
|
6
|
+
import type { InfoParams, ComponentInfo } from '../navigation/types.js';
|
|
7
|
+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
8
|
+
import { DetailView, type DetailViewComponent } from '../components/DetailView.js';
|
|
9
|
+
|
|
10
|
+
const componentService = createComponentService();
|
|
11
|
+
const libraryService = createLibraryService();
|
|
12
|
+
|
|
13
|
+
export function InfoScreen() {
|
|
14
|
+
const { push } = useNavigation();
|
|
15
|
+
const { params } = useCurrentScreen() as { screen: 'info'; params: InfoParams };
|
|
16
|
+
const { columns: terminalWidth } = useTerminalSize();
|
|
17
|
+
|
|
18
|
+
const [component, setComponent] = useState<ComponentInfo | null>(
|
|
19
|
+
params.component || null
|
|
20
|
+
);
|
|
21
|
+
const [installedInfo, setInstalledInfo] = useState<InstalledComponent | null>(null);
|
|
22
|
+
const [libraryStatus, setLibraryStatus] = useState<Awaited<ReturnType<typeof libraryService.getStatus>> | null>(null);
|
|
23
|
+
const [isLoading, setIsLoading] = useState(!params.component);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const [isCheckingLibrary, setIsCheckingLibrary] = useState(false);
|
|
26
|
+
const checkingRef = useRef(false);
|
|
27
|
+
|
|
28
|
+
// Check if component is installed and fetch full details
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const componentId = params.componentId;
|
|
31
|
+
if (!componentId) return;
|
|
32
|
+
|
|
33
|
+
const fetchData = async () => {
|
|
34
|
+
setIsLoading(true);
|
|
35
|
+
try {
|
|
36
|
+
// Fetch library status (for paths)
|
|
37
|
+
const status = await libraryService.getStatus();
|
|
38
|
+
setLibraryStatus(status);
|
|
39
|
+
|
|
40
|
+
// Check if already installed
|
|
41
|
+
const installed = await libraryService.listInstalled({});
|
|
42
|
+
const found = installed.find(c => c.lcscId === componentId);
|
|
43
|
+
if (found) {
|
|
44
|
+
setInstalledInfo(found);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fetch full details from API (always, to get price/stock/attributes)
|
|
48
|
+
const searchResults = await componentService.search(componentId, { limit: 1 });
|
|
49
|
+
if (searchResults.length > 0) {
|
|
50
|
+
setComponent(searchResults[0]);
|
|
51
|
+
} else if (!params.component) {
|
|
52
|
+
// Fallback to getDetails if search didn't work
|
|
53
|
+
const details = await componentService.getDetails(componentId);
|
|
54
|
+
if (details) {
|
|
55
|
+
setComponent(details);
|
|
56
|
+
} else {
|
|
57
|
+
setError('Component not found');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (!params.component) {
|
|
62
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch component');
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
setIsLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
fetchData();
|
|
70
|
+
}, [params.componentId, params.component]);
|
|
71
|
+
|
|
72
|
+
// Get datasheet URL (different field names in different types)
|
|
73
|
+
const datasheetUrl = component && ('datasheetPdf' in component ? component.datasheetPdf : 'datasheet' in component ? component.datasheet : undefined);
|
|
74
|
+
|
|
75
|
+
const [isRegenerating, setIsRegenerating] = useState(false);
|
|
76
|
+
const [regenerateMessage, setRegenerateMessage] = useState<string | null>(null);
|
|
77
|
+
|
|
78
|
+
useInput((input, key) => {
|
|
79
|
+
if (isLoading || !component || isCheckingLibrary || isRegenerating) return;
|
|
80
|
+
|
|
81
|
+
const lowerInput = input.toLowerCase();
|
|
82
|
+
|
|
83
|
+
// S - Open Symbol in KiCad
|
|
84
|
+
if (lowerInput === 's' && installedInfo && libraryStatus) {
|
|
85
|
+
const symbolPath = `${libraryStatus.paths.symbolsDir}/JLC-MCP-${installedInfo.category}.kicad_sym`;
|
|
86
|
+
open(symbolPath);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// F - Open Footprint in KiCad
|
|
91
|
+
if (lowerInput === 'f' && installedInfo && libraryStatus) {
|
|
92
|
+
if (installedInfo.footprintRef?.startsWith('JLC-MCP:')) {
|
|
93
|
+
const fpName = installedInfo.footprintRef.split(':')[1];
|
|
94
|
+
const footprintPath = `${libraryStatus.paths.footprintsDir}/JLC-MCP.pretty/${fpName}.kicad_mod`;
|
|
95
|
+
open(footprintPath);
|
|
96
|
+
}
|
|
97
|
+
// Standard KiCad footprints can't be opened directly
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// M - Open 3D Model
|
|
102
|
+
if (lowerInput === 'm' && installedInfo && libraryStatus && installedInfo.has3dModel) {
|
|
103
|
+
const modelPath = `${libraryStatus.paths.models3dDir}/${installedInfo.name}.step`;
|
|
104
|
+
open(modelPath);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// R - Regenerate symbol and footprint
|
|
109
|
+
if (lowerInput === 'r' && installedInfo) {
|
|
110
|
+
setIsRegenerating(true);
|
|
111
|
+
setRegenerateMessage('Regenerating symbol and footprint...');
|
|
112
|
+
|
|
113
|
+
libraryService.install(component.lcscId, { force: true })
|
|
114
|
+
.then((result) => {
|
|
115
|
+
setRegenerateMessage(`✓ Regenerated: ${result.symbolAction}`);
|
|
116
|
+
// Clear message after 2 seconds
|
|
117
|
+
setTimeout(() => setRegenerateMessage(null), 2000);
|
|
118
|
+
})
|
|
119
|
+
.catch((err) => {
|
|
120
|
+
setRegenerateMessage(`✗ Failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
121
|
+
setTimeout(() => setRegenerateMessage(null), 3000);
|
|
122
|
+
})
|
|
123
|
+
.finally(() => setIsRegenerating(false));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// D - Open Datasheet
|
|
128
|
+
if (lowerInput === 'd' && datasheetUrl) {
|
|
129
|
+
open(datasheetUrl);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Enter - Install (only when not installed)
|
|
134
|
+
if (key.return && !installedInfo) {
|
|
135
|
+
if (checkingRef.current) return;
|
|
136
|
+
checkingRef.current = true;
|
|
137
|
+
setIsCheckingLibrary(true);
|
|
138
|
+
|
|
139
|
+
if (libraryStatus && (!libraryStatus.installed || !libraryStatus.linked)) {
|
|
140
|
+
// Libraries not set up - show setup screen
|
|
141
|
+
push('library-setup', {
|
|
142
|
+
componentId: component.lcscId,
|
|
143
|
+
component,
|
|
144
|
+
});
|
|
145
|
+
setIsCheckingLibrary(false);
|
|
146
|
+
checkingRef.current = false;
|
|
147
|
+
} else {
|
|
148
|
+
// Libraries ready - proceed to install
|
|
149
|
+
push('install', {
|
|
150
|
+
componentId: component.lcscId,
|
|
151
|
+
component,
|
|
152
|
+
});
|
|
153
|
+
setIsCheckingLibrary(false);
|
|
154
|
+
checkingRef.current = false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (isLoading) {
|
|
160
|
+
return (
|
|
161
|
+
<Box flexDirection="column">
|
|
162
|
+
<Text color="yellow">⏳ Loading component {params.componentId}...</Text>
|
|
163
|
+
</Box>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (isCheckingLibrary) {
|
|
168
|
+
return (
|
|
169
|
+
<Box flexDirection="column">
|
|
170
|
+
<Text color="yellow">Checking library status...</Text>
|
|
171
|
+
</Box>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (error || !component) {
|
|
176
|
+
return (
|
|
177
|
+
<Box flexDirection="column">
|
|
178
|
+
<Text color="red">✗ {error || 'Component not found'}</Text>
|
|
179
|
+
<Text dimColor>Press Esc to go back</Text>
|
|
180
|
+
</Box>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Box flexDirection="column">
|
|
186
|
+
<Box marginBottom={1}>
|
|
187
|
+
<Text bold>
|
|
188
|
+
Component: <Text color="cyan">{component.lcscId}</Text>
|
|
189
|
+
{' '}
|
|
190
|
+
<Text dimColor>({component.name})</Text>
|
|
191
|
+
{installedInfo && <Text color="green"> ✓ Installed</Text>}
|
|
192
|
+
</Text>
|
|
193
|
+
</Box>
|
|
194
|
+
<DetailView
|
|
195
|
+
component={component}
|
|
196
|
+
terminalWidth={terminalWidth}
|
|
197
|
+
isInstalled={!!installedInfo}
|
|
198
|
+
installedInfo={installedInfo}
|
|
199
|
+
statusMessage={regenerateMessage}
|
|
200
|
+
/>
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { createLibraryService, type InstallResult } from '@jlcpcb/core';
|
|
4
|
+
import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
|
|
5
|
+
import type { InstallParams } from '../navigation/types.js';
|
|
6
|
+
|
|
7
|
+
const libraryService = createLibraryService();
|
|
8
|
+
|
|
9
|
+
export function InstallScreen() {
|
|
10
|
+
const { replace } = useNavigation();
|
|
11
|
+
const { params } = useCurrentScreen() as { screen: 'install'; params: InstallParams };
|
|
12
|
+
|
|
13
|
+
const [status, setStatus] = useState<'installing' | 'done'>('installing');
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let mounted = true;
|
|
17
|
+
|
|
18
|
+
async function install() {
|
|
19
|
+
try {
|
|
20
|
+
const result: InstallResult = await libraryService.install(params.componentId, {});
|
|
21
|
+
if (mounted) {
|
|
22
|
+
replace('installed', {
|
|
23
|
+
componentId: params.componentId,
|
|
24
|
+
component: params.component,
|
|
25
|
+
result,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (mounted) {
|
|
30
|
+
replace('installed', {
|
|
31
|
+
componentId: params.componentId,
|
|
32
|
+
component: params.component,
|
|
33
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
install();
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
mounted = false;
|
|
43
|
+
};
|
|
44
|
+
}, [params.componentId, params.component, replace]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Box flexDirection="column">
|
|
48
|
+
<Text color="yellow">⏳ Installing {params.componentId}...</Text>
|
|
49
|
+
<Box marginTop={1}>
|
|
50
|
+
<Text dimColor>Fetching from EasyEDA and converting to KiCad format...</Text>
|
|
51
|
+
</Box>
|
|
52
|
+
</Box>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
|
|
4
|
+
import type { InstalledParams } from '../navigation/types.js';
|
|
5
|
+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
6
|
+
import { InstalledView } from '../components/InstalledView.js';
|
|
7
|
+
|
|
8
|
+
export function InstalledScreen() {
|
|
9
|
+
const { pop } = useNavigation();
|
|
10
|
+
const { params } = useCurrentScreen() as { screen: 'installed'; params: InstalledParams };
|
|
11
|
+
const { columns: terminalWidth } = useTerminalSize();
|
|
12
|
+
|
|
13
|
+
useInput(() => {
|
|
14
|
+
pop();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const result = params.result
|
|
18
|
+
? {
|
|
19
|
+
symbolRef: params.result.symbolRef || `JLC:${params.component.name}`,
|
|
20
|
+
footprintRef: params.result.footprintRef || `JLC:${params.component.name}`,
|
|
21
|
+
}
|
|
22
|
+
: null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Box flexDirection="column">
|
|
26
|
+
<Box marginBottom={1}>
|
|
27
|
+
<Text bold>
|
|
28
|
+
Install: <Text color="cyan">{params.componentId}</Text>
|
|
29
|
+
</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
<InstalledView
|
|
32
|
+
component={params.component}
|
|
33
|
+
result={result}
|
|
34
|
+
error={params.error || null}
|
|
35
|
+
terminalWidth={terminalWidth}
|
|
36
|
+
/>
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { createLibraryService, createComponentService, type InstalledComponent, type LibraryStatus } from '@jlcpcb/core';
|
|
4
|
+
import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
|
|
5
|
+
import type { LibraryParams } from '../navigation/types.js';
|
|
6
|
+
import { useAppState } from '../state/AppStateContext.js';
|
|
7
|
+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
8
|
+
import { Divider } from '../components/Divider.js';
|
|
9
|
+
|
|
10
|
+
const libraryService = createLibraryService();
|
|
11
|
+
const componentService = createComponentService();
|
|
12
|
+
|
|
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
|
+
function StatusBadge({ installed, linked }: { installed: boolean; linked: boolean }) {
|
|
19
|
+
if (installed && linked) {
|
|
20
|
+
return <Text color="green">Installed & Linked</Text>;
|
|
21
|
+
} else if (installed && !linked) {
|
|
22
|
+
return <Text color="yellow">Installed (not linked)</Text>;
|
|
23
|
+
} else {
|
|
24
|
+
return <Text color="red">Not Installed</Text>;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function LibraryScreen() {
|
|
29
|
+
const { push } = useNavigation();
|
|
30
|
+
const { params } = useCurrentScreen() as { screen: 'library'; params: LibraryParams };
|
|
31
|
+
const { selectedIndex, setSelectedIndex } = useAppState();
|
|
32
|
+
const { columns: terminalWidth } = useTerminalSize();
|
|
33
|
+
|
|
34
|
+
const [status, setStatus] = useState<LibraryStatus | null>(params.status || null);
|
|
35
|
+
const [installed, setInstalled] = useState<InstalledComponent[]>(params.installed || []);
|
|
36
|
+
const [descriptions, setDescriptions] = useState<Record<string, string>>({});
|
|
37
|
+
const [isLoading, setIsLoading] = useState(!params.status);
|
|
38
|
+
const [isSettingUp, setIsSettingUp] = useState(false);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!params.status) {
|
|
42
|
+
setIsLoading(true);
|
|
43
|
+
Promise.all([libraryService.getStatus(), libraryService.listInstalled({})])
|
|
44
|
+
.then(([statusResult, installedResult]) => {
|
|
45
|
+
setStatus(statusResult);
|
|
46
|
+
setInstalled(installedResult);
|
|
47
|
+
})
|
|
48
|
+
.catch(() => {
|
|
49
|
+
setStatus(null);
|
|
50
|
+
setInstalled([]);
|
|
51
|
+
})
|
|
52
|
+
.finally(() => setIsLoading(false));
|
|
53
|
+
}
|
|
54
|
+
}, [params.status, params.installed]);
|
|
55
|
+
|
|
56
|
+
// Fetch descriptions for installed components
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (installed.length === 0) return;
|
|
59
|
+
|
|
60
|
+
const fetchDescriptions = async () => {
|
|
61
|
+
const newDescriptions: Record<string, string> = {};
|
|
62
|
+
|
|
63
|
+
// Fetch in batches of 5 to avoid overwhelming the API
|
|
64
|
+
for (let i = 0; i < installed.length; i += 5) {
|
|
65
|
+
const batch = installed.slice(i, i + 5);
|
|
66
|
+
const results = await Promise.allSettled(
|
|
67
|
+
batch.map(async (item) => {
|
|
68
|
+
const details = await componentService.search(item.lcscId, { limit: 1 });
|
|
69
|
+
return { id: item.lcscId, description: details[0]?.description || '' };
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
for (const result of results) {
|
|
74
|
+
if (result.status === 'fulfilled' && result.value.description) {
|
|
75
|
+
newDescriptions[result.value.id] = result.value.description;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Update state incrementally
|
|
80
|
+
setDescriptions(prev => ({ ...prev, ...newDescriptions }));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
fetchDescriptions();
|
|
85
|
+
}, [installed]);
|
|
86
|
+
|
|
87
|
+
useInput((input, key) => {
|
|
88
|
+
if (isLoading || isSettingUp) return;
|
|
89
|
+
|
|
90
|
+
// If not installed, Enter triggers setup
|
|
91
|
+
if (status && (!status.installed || !status.linked)) {
|
|
92
|
+
if (key.return) {
|
|
93
|
+
setIsSettingUp(true);
|
|
94
|
+
libraryService
|
|
95
|
+
.ensureGlobalTables()
|
|
96
|
+
.then(() => libraryService.getStatus())
|
|
97
|
+
.then((newStatus) => {
|
|
98
|
+
setStatus(newStatus);
|
|
99
|
+
setIsSettingUp(false);
|
|
100
|
+
})
|
|
101
|
+
.catch(() => setIsSettingUp(false));
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (key.upArrow) {
|
|
107
|
+
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
|
108
|
+
} else if (key.downArrow) {
|
|
109
|
+
setSelectedIndex(Math.min(installed.length - 1, selectedIndex + 1));
|
|
110
|
+
} else if (key.return && installed[selectedIndex]) {
|
|
111
|
+
push('info', {
|
|
112
|
+
componentId: installed[selectedIndex].lcscId,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (isLoading) {
|
|
118
|
+
return (
|
|
119
|
+
<Box flexDirection="column">
|
|
120
|
+
<Text color="yellow">Loading library status...</Text>
|
|
121
|
+
</Box>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isSettingUp) {
|
|
126
|
+
return (
|
|
127
|
+
<Box flexDirection="column">
|
|
128
|
+
<Text color="yellow">Setting up JLC-MCP libraries...</Text>
|
|
129
|
+
<Box marginTop={1}>
|
|
130
|
+
<Text dimColor>Creating directories and registering with KiCad...</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
</Box>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Not installed state
|
|
137
|
+
if (!status || !status.installed || !status.linked) {
|
|
138
|
+
return (
|
|
139
|
+
<Box flexDirection="column">
|
|
140
|
+
<Box marginBottom={1}>
|
|
141
|
+
<Text bold>JLC-MCP Libraries</Text>
|
|
142
|
+
</Box>
|
|
143
|
+
<Box marginBottom={1}>
|
|
144
|
+
<Text>Status: </Text>
|
|
145
|
+
<StatusBadge installed={status?.installed ?? false} linked={status?.linked ?? false} />
|
|
146
|
+
{status && <Text dimColor> (KiCad {status.version})</Text>}
|
|
147
|
+
</Box>
|
|
148
|
+
<Box marginTop={1} flexDirection="column">
|
|
149
|
+
<Text dimColor>The JLC-MCP libraries have not been set up yet.</Text>
|
|
150
|
+
<Text dimColor>This is required to install components.</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
<Box marginTop={1} flexDirection="column">
|
|
153
|
+
<Divider width={terminalWidth} />
|
|
154
|
+
<Text>Press <Text color="cyan">Enter</Text> to set up libraries now</Text>
|
|
155
|
+
<Text dimColor>Esc Exit</Text>
|
|
156
|
+
<Divider width={terminalWidth} />
|
|
157
|
+
</Box>
|
|
158
|
+
</Box>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Installed but empty state
|
|
163
|
+
if (installed.length === 0) {
|
|
164
|
+
return (
|
|
165
|
+
<Box flexDirection="column">
|
|
166
|
+
<Box marginBottom={1}>
|
|
167
|
+
<Text bold>JLC-MCP Libraries</Text>
|
|
168
|
+
</Box>
|
|
169
|
+
<Box>
|
|
170
|
+
<Text>Status: </Text>
|
|
171
|
+
<StatusBadge installed={status.installed} linked={status.linked} />
|
|
172
|
+
<Text dimColor> (KiCad {status.version})</Text>
|
|
173
|
+
</Box>
|
|
174
|
+
<Box>
|
|
175
|
+
<Text dimColor>Installed: 0 Components</Text>
|
|
176
|
+
</Box>
|
|
177
|
+
<Box marginBottom={1}>
|
|
178
|
+
<Text dimColor>Location: {status.paths.symbolsDir.replace(process.env.HOME || '', '~')}</Text>
|
|
179
|
+
</Box>
|
|
180
|
+
<Divider width={terminalWidth} />
|
|
181
|
+
<Box marginTop={1}>
|
|
182
|
+
<Text dimColor>No components installed yet. Use 'jlc search' to find and install components.</Text>
|
|
183
|
+
</Box>
|
|
184
|
+
<Box marginTop={1}>
|
|
185
|
+
<Divider width={terminalWidth} />
|
|
186
|
+
<Text dimColor>Esc Exit</Text>
|
|
187
|
+
<Divider width={terminalWidth} />
|
|
188
|
+
</Box>
|
|
189
|
+
</Box>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Components table - full width responsive layout
|
|
194
|
+
// Fixed columns: selector(2) + name + category + status columns(15)
|
|
195
|
+
// Description takes remaining space
|
|
196
|
+
const nameWidth = 20;
|
|
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);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Box flexDirection="column" width="100%">
|
|
203
|
+
<Box marginBottom={1}>
|
|
204
|
+
<Text bold>JLC-MCP Libraries</Text>
|
|
205
|
+
</Box>
|
|
206
|
+
<Box>
|
|
207
|
+
<Text>Status: </Text>
|
|
208
|
+
<StatusBadge installed={status.installed} linked={status.linked} />
|
|
209
|
+
<Text dimColor> (KiCad {status.version})</Text>
|
|
210
|
+
</Box>
|
|
211
|
+
<Box>
|
|
212
|
+
<Text dimColor>Installed: {installed.length} Component{installed.length !== 1 ? 's' : ''}</Text>
|
|
213
|
+
</Box>
|
|
214
|
+
<Box marginBottom={1}>
|
|
215
|
+
<Text dimColor>Location: {status.paths.symbolsDir.replace(process.env.HOME || '', '~')}</Text>
|
|
216
|
+
</Box>
|
|
217
|
+
<Divider width={terminalWidth} />
|
|
218
|
+
<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>
|
|
228
|
+
</Box>
|
|
229
|
+
{installed.map((item, i) => {
|
|
230
|
+
const isSelected = i === selectedIndex;
|
|
231
|
+
const desc = descriptions[item.lcscId] || '';
|
|
232
|
+
// Check if footprint is standard KiCad (not JLC-MCP custom)
|
|
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';
|
|
236
|
+
return (
|
|
237
|
+
<Box key={`${item.lcscId}-${i}`}>
|
|
238
|
+
<Text color={isSelected ? 'cyan' : undefined}>
|
|
239
|
+
{isSelected ? '> ' : ' '}
|
|
240
|
+
</Text>
|
|
241
|
+
<Text inverse={isSelected}>
|
|
242
|
+
<Text color="cyan">{truncate(item.name, nameWidth - 1).padEnd(nameWidth)}</Text>
|
|
243
|
+
{truncate(item.category, categoryWidth - 1).padEnd(categoryWidth)}
|
|
244
|
+
<Text dimColor>{truncate(desc, descWidth - 1).padEnd(descWidth)}</Text>
|
|
245
|
+
<Text color="green">{'Y'.padEnd(5)}</Text>
|
|
246
|
+
<Text color={fpColor}>{fpLabel.padEnd(5)}</Text>
|
|
247
|
+
<Text color={item.has3dModel ? 'green' : 'red'}>{item.has3dModel ? 'Y' : 'N'}</Text>
|
|
248
|
+
</Text>
|
|
249
|
+
</Box>
|
|
250
|
+
);
|
|
251
|
+
})}
|
|
252
|
+
<Box marginTop={1} flexDirection="column">
|
|
253
|
+
<Divider width={terminalWidth} />
|
|
254
|
+
<Text dimColor>↑/↓ Navigate • Enter View Details • Esc Exit</Text>
|
|
255
|
+
<Divider width={terminalWidth} />
|
|
256
|
+
</Box>
|
|
257
|
+
</Box>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { createLibraryService } from '@jlcpcb/core';
|
|
4
|
+
import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
|
|
5
|
+
import type { LibrarySetupParams } from '../navigation/types.js';
|
|
6
|
+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
7
|
+
import { Divider } from '../components/Divider.js';
|
|
8
|
+
|
|
9
|
+
const libraryService = createLibraryService();
|
|
10
|
+
|
|
11
|
+
export function LibrarySetupScreen() {
|
|
12
|
+
const { push, pop } = useNavigation();
|
|
13
|
+
const { params } = useCurrentScreen() as { screen: 'library-setup'; params: LibrarySetupParams };
|
|
14
|
+
const { columns: terminalWidth } = useTerminalSize();
|
|
15
|
+
|
|
16
|
+
const [selectedOption, setSelectedOption] = useState<'install' | 'cancel'>('install');
|
|
17
|
+
const [isInstalling, setIsInstalling] = useState(false);
|
|
18
|
+
const [error, setError] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
useInput((input, key) => {
|
|
21
|
+
if (isInstalling) return;
|
|
22
|
+
|
|
23
|
+
if (key.leftArrow || key.rightArrow || input === 'h' || input === 'l') {
|
|
24
|
+
setSelectedOption(selectedOption === 'install' ? 'cancel' : 'install');
|
|
25
|
+
} else if (key.return) {
|
|
26
|
+
if (selectedOption === 'cancel') {
|
|
27
|
+
pop();
|
|
28
|
+
} else {
|
|
29
|
+
setIsInstalling(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
libraryService
|
|
32
|
+
.ensureGlobalTables()
|
|
33
|
+
.then(() => {
|
|
34
|
+
// Success - continue to install the component
|
|
35
|
+
push('install', {
|
|
36
|
+
componentId: params.componentId,
|
|
37
|
+
component: params.component,
|
|
38
|
+
});
|
|
39
|
+
})
|
|
40
|
+
.catch((err) => {
|
|
41
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
42
|
+
setIsInstalling(false);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
} else if (key.escape) {
|
|
46
|
+
pop();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (isInstalling) {
|
|
51
|
+
return (
|
|
52
|
+
<Box flexDirection="column">
|
|
53
|
+
<Box marginBottom={1}>
|
|
54
|
+
<Text bold color="yellow">Setting Up Libraries</Text>
|
|
55
|
+
</Box>
|
|
56
|
+
<Box marginTop={1}>
|
|
57
|
+
<Text color="yellow">Creating library directories...</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
<Box marginTop={1}>
|
|
60
|
+
<Text dimColor>Registering with KiCad symbol and footprint tables...</Text>
|
|
61
|
+
</Box>
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Box flexDirection="column">
|
|
68
|
+
<Box marginBottom={1}>
|
|
69
|
+
<Text bold color="yellow">Library Setup Required</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
|
|
72
|
+
<Box marginBottom={1}>
|
|
73
|
+
<Text>
|
|
74
|
+
JLC-MCP libraries need to be installed and linked to KiCad before you can install components.
|
|
75
|
+
</Text>
|
|
76
|
+
</Box>
|
|
77
|
+
|
|
78
|
+
<Box marginTop={1} flexDirection="column">
|
|
79
|
+
<Text dimColor>This will:</Text>
|
|
80
|
+
<Text dimColor> • Create library directories in ~/Documents/KiCad/*/3rdparty/</Text>
|
|
81
|
+
<Text dimColor> • Register libraries in KiCad's symbol and footprint tables</Text>
|
|
82
|
+
</Box>
|
|
83
|
+
|
|
84
|
+
{error && (
|
|
85
|
+
<Box marginTop={1}>
|
|
86
|
+
<Text color="red">Error: {error}</Text>
|
|
87
|
+
</Box>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<Box marginTop={2}>
|
|
91
|
+
<Divider width={terminalWidth} />
|
|
92
|
+
</Box>
|
|
93
|
+
|
|
94
|
+
<Box marginTop={1} gap={2}>
|
|
95
|
+
<Text
|
|
96
|
+
inverse={selectedOption === 'install'}
|
|
97
|
+
color={selectedOption === 'install' ? 'green' : undefined}
|
|
98
|
+
>
|
|
99
|
+
{' Install Libraries '}
|
|
100
|
+
</Text>
|
|
101
|
+
<Text
|
|
102
|
+
inverse={selectedOption === 'cancel'}
|
|
103
|
+
color={selectedOption === 'cancel' ? 'red' : undefined}
|
|
104
|
+
>
|
|
105
|
+
{' Cancel '}
|
|
106
|
+
</Text>
|
|
107
|
+
</Box>
|
|
108
|
+
|
|
109
|
+
<Box marginTop={1}>
|
|
110
|
+
<Divider width={terminalWidth} />
|
|
111
|
+
</Box>
|
|
112
|
+
<Box>
|
|
113
|
+
<Text dimColor>←/→ Select • Enter Confirm • Esc Cancel</Text>
|
|
114
|
+
</Box>
|
|
115
|
+
<Box>
|
|
116
|
+
<Divider width={terminalWidth} />
|
|
117
|
+
</Box>
|
|
118
|
+
</Box>
|
|
119
|
+
);
|
|
120
|
+
}
|