espcli 0.0.1 → 0.0.3

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.
@@ -1,214 +0,0 @@
1
- import { execa } from 'execa';
2
- import { readdir, access, readFile } from 'fs/promises';
3
- import { join } from 'path';
4
-
5
- export interface HealthStatus {
6
- ok: boolean;
7
- name: string;
8
- version?: string;
9
- path?: string;
10
- error?: string;
11
- hint?: string;
12
- }
13
-
14
- export interface SystemHealth {
15
- python: HealthStatus;
16
- idf: HealthStatus;
17
- pyserial: HealthStatus;
18
- esptool: HealthStatus;
19
- git: HealthStatus;
20
- idfPython?: string; // Path to IDF's Python environment
21
- }
22
-
23
- let healthCache: SystemHealth | null = null;
24
-
25
- export async function getHealth(): Promise<SystemHealth> {
26
- if (healthCache) return healthCache;
27
-
28
- healthCache = await runHealthChecks();
29
- return healthCache;
30
- }
31
-
32
- export function clearHealthCache(): void {
33
- healthCache = null;
34
- }
35
-
36
- async function runHealthChecks(): Promise<SystemHealth> {
37
- // Run checks in parallel for speed
38
- const [python, git] = await Promise.all([checkPython(), checkGit()]);
39
-
40
- // IDF check depends on knowing if Python exists
41
- const idf = await checkIdf();
42
-
43
- // These depend on IDF Python environment
44
- const idfPython = idf.ok ? await findIdfPython() : undefined;
45
- const [pyserial, esptool] = idfPython
46
- ? await Promise.all([checkPyserial(idfPython), checkEsptool(idfPython)])
47
- : [
48
- { ok: false, name: 'pyserial', error: 'ESP-IDF not installed', hint: 'Run: espcli install' },
49
- { ok: false, name: 'esptool', error: 'ESP-IDF not installed', hint: 'Run: espcli install' },
50
- ];
51
-
52
- return { python, idf, pyserial, esptool, git, idfPython };
53
- }
54
-
55
- async function checkPython(): Promise<HealthStatus> {
56
- try {
57
- const { stdout } = await execa('python3', ['--version'], { timeout: 3000 });
58
- const version = stdout.replace('Python ', '').trim();
59
- return { ok: true, name: 'Python', version };
60
- } catch {
61
- return {
62
- ok: false,
63
- name: 'Python',
64
- error: 'Python 3 not found',
65
- hint: 'Install Python 3: https://www.python.org/downloads/',
66
- };
67
- }
68
- }
69
-
70
- async function checkGit(): Promise<HealthStatus> {
71
- try {
72
- const { stdout } = await execa('git', ['--version'], { timeout: 3000 });
73
- const version = stdout.replace('git version ', '').trim();
74
- return { ok: true, name: 'Git', version };
75
- } catch {
76
- return {
77
- ok: false,
78
- name: 'Git',
79
- error: 'Git not found',
80
- hint: 'Install Git: https://git-scm.com/downloads',
81
- };
82
- }
83
- }
84
-
85
- async function checkIdf(): Promise<HealthStatus> {
86
- const home = process.env.HOME || '';
87
- const possiblePaths = [
88
- process.env.IDF_PATH,
89
- join(home, 'esp', 'esp-idf'),
90
- join(home, 'esp-idf'),
91
- ].filter(Boolean) as string[];
92
-
93
- for (const idfPath of possiblePaths) {
94
- try {
95
- await access(idfPath);
96
-
97
- // Try to read version from version.txt or git
98
- let version: string | undefined;
99
- try {
100
- const versionFile = join(idfPath, 'version.txt');
101
- version = (await readFile(versionFile, 'utf-8')).trim();
102
- } catch {
103
- try {
104
- const { stdout } = await execa('git', ['describe', '--tags'], {
105
- cwd: idfPath,
106
- timeout: 3000,
107
- });
108
- version = stdout.trim();
109
- } catch {
110
- version = 'unknown';
111
- }
112
- }
113
-
114
- return { ok: true, name: 'ESP-IDF', version, path: idfPath };
115
- } catch {
116
- continue;
117
- }
118
- }
119
-
120
- return {
121
- ok: false,
122
- name: 'ESP-IDF',
123
- error: 'ESP-IDF not found',
124
- hint: 'Run: espcli install',
125
- };
126
- }
127
-
128
- async function findIdfPython(): Promise<string | undefined> {
129
- const home = process.env.HOME || '';
130
- const envDir = join(home, '.espressif', 'python_env');
131
-
132
- try {
133
- const files = await readdir(envDir);
134
- for (const file of files) {
135
- if (file.startsWith('idf')) {
136
- const pythonPath = join(envDir, file, 'bin', 'python');
137
- try {
138
- await execa(pythonPath, ['--version'], { timeout: 2000 });
139
- return pythonPath;
140
- } catch {
141
- continue;
142
- }
143
- }
144
- }
145
- } catch {
146
- // Directory doesn't exist
147
- }
148
-
149
- return undefined;
150
- }
151
-
152
- async function checkPyserial(python: string): Promise<HealthStatus> {
153
- try {
154
- const { stdout } = await execa(
155
- python,
156
- ['-c', 'import serial; print(serial.__version__)'],
157
- { timeout: 3000 }
158
- );
159
- return { ok: true, name: 'pyserial', version: stdout.trim() };
160
- } catch {
161
- return {
162
- ok: false,
163
- name: 'pyserial',
164
- error: 'pyserial not installed in IDF environment',
165
- hint: 'Reinstall ESP-IDF: espcli install',
166
- };
167
- }
168
- }
169
-
170
- async function checkEsptool(python: string): Promise<HealthStatus> {
171
- try {
172
- const { stdout } = await execa(python, ['-m', 'esptool', 'version'], {
173
- timeout: 3000,
174
- });
175
- // Parse version from "esptool.py v4.8.1"
176
- const versionMatch = stdout.match(/v?([\d.]+)/);
177
- const version = versionMatch?.[1] || 'unknown';
178
- return { ok: true, name: 'esptool', version };
179
- } catch {
180
- return {
181
- ok: false,
182
- name: 'esptool',
183
- error: 'esptool not installed in IDF environment',
184
- hint: 'Reinstall ESP-IDF: espcli install',
185
- };
186
- }
187
- }
188
-
189
- export async function requireIdf(): Promise<{ ok: true; python: string } | { ok: false; error: string }> {
190
- const health = await getHealth();
191
-
192
- if (!health.idf.ok) {
193
- return { ok: false, error: `${health.idf.error}. ${health.idf.hint}` };
194
- }
195
-
196
- if (!health.idfPython) {
197
- return { ok: false, error: 'ESP-IDF Python environment not found. Run: espcli install' };
198
- }
199
-
200
- return { ok: true, python: health.idfPython };
201
- }
202
-
203
- export async function requireTool(
204
- tool: 'python' | 'git' | 'idf' | 'pyserial' | 'esptool'
205
- ): Promise<{ ok: true } | { ok: false; error: string }> {
206
- const health = await getHealth();
207
- const status = health[tool];
208
-
209
- if (!status.ok) {
210
- return { ok: false, error: `${status.error}. ${status.hint}` };
211
- }
212
-
213
- return { ok: true };
214
- }
@@ -1,98 +0,0 @@
1
- import type { IdfStatus, Result } from '@/core/types';
2
- import { DEFAULT_IDF_PATH } from '@/core/constants';
3
- import { access, readFile } from 'fs/promises';
4
- import { join, dirname } from 'path';
5
- import { execa } from 'execa';
6
-
7
- export async function findIdfPath(): Promise<string | null> {
8
- if (process.env.IDF_PATH) {
9
- try {
10
- await access(process.env.IDF_PATH);
11
- return process.env.IDF_PATH;
12
- } catch {}
13
- }
14
-
15
- try {
16
- await access(DEFAULT_IDF_PATH);
17
- return DEFAULT_IDF_PATH;
18
- } catch {}
19
-
20
- return null;
21
- }
22
-
23
- export async function getIdfVersion(idfPath: string): Promise<string | null> {
24
- const versionFile = join(idfPath, 'version.txt');
25
-
26
- try {
27
- const content = await readFile(versionFile, 'utf-8');
28
- return content.trim();
29
- } catch {}
30
-
31
- try {
32
- const result = await execa('git', ['describe', '--tags'], { cwd: idfPath });
33
- return result.stdout.trim();
34
- } catch {}
35
-
36
- return null;
37
- }
38
-
39
- export async function getIdfStatus(): Promise<IdfStatus> {
40
- const path = await findIdfPath();
41
-
42
- if (!path) {
43
- return { installed: false };
44
- }
45
-
46
- const version = await getIdfVersion(path);
47
-
48
- return {
49
- installed: true,
50
- path,
51
- version: version || undefined,
52
- };
53
- }
54
-
55
- export async function isIdfProject(dir: string): Promise<boolean> {
56
- const cmakePath = join(dir, 'CMakeLists.txt');
57
-
58
- try {
59
- const content = await readFile(cmakePath, 'utf-8');
60
- return content.includes('$ENV{IDF_PATH}') || content.includes('idf_component_register');
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- export async function findProjectRoot(startDir: string): Promise<string | null> {
67
- let current = startDir;
68
-
69
- while (current !== dirname(current)) {
70
- if (await isIdfProject(current)) {
71
- return current;
72
- }
73
- current = dirname(current);
74
- }
75
-
76
- return null;
77
- }
78
-
79
- export function getExportScript(idfPath: string): string {
80
- return join(idfPath, 'export.sh');
81
- }
82
-
83
- export async function validateIdfInstallation(idfPath: string): Promise<Result<void>> {
84
- try {
85
- await access(idfPath);
86
- } catch {
87
- return { ok: false, error: `IDF path does not exist: ${idfPath}` };
88
- }
89
-
90
- const exportScript = getExportScript(idfPath);
91
- try {
92
- await access(exportScript);
93
- } catch {
94
- return { ok: false, error: `export.sh not found in ${idfPath}` };
95
- }
96
-
97
- return { ok: true, data: undefined };
98
- }
@@ -1,172 +0,0 @@
1
- import type { SerialDevice, ConnectionType } from '@/core/types';
2
- import { USB_VENDORS } from '@/core/constants';
3
- import { requireIdf } from '@/core/services/health';
4
- import { execa } from 'execa';
5
-
6
- // Espressif's USB Vendor ID (0x303A = 12346 decimal)
7
- // @see https://github.com/espressif/usb-pids
8
- const ESPRESSIF_VID = '303A';
9
-
10
- /**
11
- * List serial ports and detect connected ESP32 chips
12
- *
13
- * Uses pyserial's CLI: python -m serial.tools.list_ports -v
14
- * Output format:
15
- * /dev/cu.usbmodem1101
16
- * desc: USB JTAG/serial debug unit
17
- * hwid: USB VID:PID=303A:1001 SER=C0:4E:30:37:1B:BC LOCATION=1-1
18
- *
19
- * @see https://pyserial.readthedocs.io/en/latest/tools.html#module-serial.tools.list_ports
20
- */
21
- export async function listPorts(): Promise<SerialDevice[]> {
22
- const idf = await requireIdf();
23
- if (!idf.ok) return []; // IDF not installed
24
-
25
- const python = idf.python;
26
-
27
- try {
28
- // pyserial CLI tool - cross-platform serial port enumeration
29
- // @see https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py
30
- const { stdout } = await execa(python, ['-m', 'serial.tools.list_ports', '-v'], {
31
- timeout: 5000,
32
- });
33
-
34
- const devices: SerialDevice[] = [];
35
- const lines = stdout.split('\n');
36
-
37
- let currentPort: string | null = null;
38
- let desc = '';
39
- let hwid = '';
40
-
41
- for (const line of lines) {
42
- if (line.startsWith('/dev/')) {
43
- // Save previous device if exists
44
- if (currentPort) {
45
- const device = parseDevice(currentPort, desc, hwid);
46
- if (device) devices.push(device);
47
- }
48
- currentPort = line.trim();
49
- desc = '';
50
- hwid = '';
51
- } else if (line.includes('desc:')) {
52
- desc = line.split('desc:')[1]?.trim() || '';
53
- } else if (line.includes('hwid:')) {
54
- hwid = line.split('hwid:')[1]?.trim() || '';
55
- }
56
- }
57
-
58
- // Don't forget last device
59
- if (currentPort) {
60
- const device = parseDevice(currentPort, desc, hwid);
61
- if (device) devices.push(device);
62
- }
63
-
64
- // Detect ESP chips for all compatible devices
65
- return await detectEspChips(python, devices);
66
- } catch {
67
- return [];
68
- }
69
- }
70
-
71
- /**
72
- * Parse device info from pyserial output
73
- */
74
- function parseDevice(port: string, desc: string, hwid: string): SerialDevice | null {
75
- // Skip non-USB ports (Bluetooth, debug-console, etc.)
76
- if (hwid === 'n/a' || !hwid.includes('VID:PID=')) {
77
- return null;
78
- }
79
-
80
- // Parse VID:PID from hwid like "USB VID:PID=303A:1001 SER=... LOCATION=..."
81
- const vidPidMatch = hwid.match(/VID:PID=([0-9A-Fa-f]+):([0-9A-Fa-f]+)/);
82
- const vendorId = vidPidMatch?.[1]?.toUpperCase();
83
- const productId = vidPidMatch?.[2]?.toUpperCase();
84
-
85
- const connectionType = getConnectionType(vendorId);
86
- const chip = identifyChip(vendorId, productId);
87
-
88
- return {
89
- port,
90
- connectionType,
91
- vendorId,
92
- productId,
93
- manufacturer: USB_VENDORS[vendorId || '']?.name,
94
- chip,
95
- description: desc !== 'n/a' ? desc : undefined,
96
- };
97
- }
98
-
99
- function getConnectionType(vendorId?: string): ConnectionType {
100
- if (!vendorId) return 'unknown';
101
- return vendorId.toUpperCase() === ESPRESSIF_VID ? 'native-usb' : 'uart-bridge';
102
- }
103
-
104
- function identifyChip(vendorId?: string, productId?: string): string | undefined {
105
- if (!vendorId) return undefined;
106
-
107
- const vendor = USB_VENDORS[vendorId.toUpperCase()];
108
- if (!vendor) return undefined;
109
-
110
- if (productId) {
111
- const chip = vendor.chips[productId.toUpperCase()];
112
- if (chip) return chip;
113
- }
114
-
115
- return vendor.name;
116
- }
117
-
118
- /**
119
- * @see https://github.com/espressif/esptool#chip-id
120
- */
121
- async function detectEspChips(python: string, devices: SerialDevice[]): Promise<SerialDevice[]> {
122
- const results = await Promise.all(
123
- devices.map(async (device) => {
124
- // Only detect on ESP-compatible ports
125
- const isEspCompatible =
126
- device.connectionType === 'native-usb' ||
127
- device.chip?.includes('CH34') ||
128
- device.chip?.includes('CP210') ||
129
- device.chip?.includes('FT232') ||
130
- device.chip?.includes('CH9');
131
-
132
- if (isEspCompatible) {
133
- const espChip = await detectEspChip(python, device.port);
134
- return { ...device, espChip };
135
- }
136
-
137
- return device;
138
- })
139
- );
140
-
141
- return results;
142
- }
143
-
144
- /**
145
- * @see https://github.com/espressif/esptool/blob/master/esptool/cmds.py (chip_id command)
146
- * @see https://github.com/espressif/esptool/tree/master/esptool/targets (chip definitions)
147
- */
148
- async function detectEspChip(python: string, port: string): Promise<string | undefined> {
149
- try {
150
- const { stdout, stderr } = await execa(python, ['-m', 'esptool', '--port', port, 'chip_id'], {
151
- timeout: 10000,
152
- reject: false,
153
- });
154
-
155
- const output = stdout + stderr;
156
-
157
- const patterns = [
158
- /Chip is (ESP32[A-Za-z0-9-]*)/i, // "Chip is ESP32-S3 (revision v0.2)"
159
- /Detecting chip type[.\s]*(ESP32[A-Za-z0-9-]*)/i, // "Detecting chip type... ESP32-S3"
160
- /Chip type:\s*(ESP32[A-Za-z0-9-]*)/i, // "Chip type: ESP32-S3"
161
- ];
162
-
163
- for (const pattern of patterns) {
164
- const match = output.match(pattern);
165
- if (match) return match[1].toUpperCase();
166
- }
167
- } catch {
168
- // esptool failed (port busy, chip not responding, etc.)
169
- }
170
-
171
- return undefined;
172
- }
@@ -1,144 +0,0 @@
1
- import type { Result } from '@/core/types';
2
- import { execa, type Options as ExecaOptions, type ResultPromise } from 'execa';
3
- import { findIdfPath, getExportScript } from '@/core/services/idf';
4
- import { emitter } from '@/core/emitter';
5
-
6
- export interface RunOptions {
7
- cwd?: string;
8
- operationId?: string;
9
- env?: Record<string, string>;
10
- }
11
-
12
- export interface RunResult {
13
- stdout: string;
14
- stderr: string;
15
- exitCode: number;
16
- }
17
-
18
- export async function run(
19
- command: string,
20
- args: string[],
21
- options: RunOptions = {}
22
- ): Promise<Result<RunResult>> {
23
- const { cwd, operationId, env } = options;
24
-
25
- const execaOpts: ExecaOptions = {
26
- cwd,
27
- env: { ...process.env, ...env },
28
- reject: false,
29
- };
30
-
31
- try {
32
- const proc = execa(command, args, execaOpts);
33
-
34
- if (operationId) {
35
- proc.stdout?.on('data', (data: Buffer) => {
36
- emitter.emit(operationId, { type: 'stdout', text: data.toString() });
37
- });
38
-
39
- proc.stderr?.on('data', (data: Buffer) => {
40
- emitter.emit(operationId, { type: 'stderr', text: data.toString() });
41
- });
42
- }
43
-
44
- const result = await proc;
45
-
46
- return {
47
- ok: true,
48
- data: {
49
- stdout: String(result.stdout ?? ''),
50
- stderr: String(result.stderr ?? ''),
51
- exitCode: result.exitCode ?? 0,
52
- },
53
- };
54
- } catch (err) {
55
- return { ok: false, error: `Failed to execute ${command}: ${err}` };
56
- }
57
- }
58
-
59
- export async function runWithIdf(
60
- command: string,
61
- args: string[],
62
- options: RunOptions = {}
63
- ): Promise<Result<RunResult>> {
64
- const idfPath = await findIdfPath();
65
-
66
- if (!idfPath) {
67
- return { ok: false, error: 'ESP-IDF not found. Run `espcli install` first.', code: 'IDF_NOT_FOUND' };
68
- }
69
-
70
- const exportScript = getExportScript(idfPath);
71
- const fullCommand = `source ${exportScript} > /dev/null 2>&1 && ${command} ${args.join(' ')}`;
72
-
73
- const { cwd, operationId, env } = options;
74
-
75
- const execaOpts: ExecaOptions = {
76
- cwd,
77
- env: { ...process.env, ...env },
78
- shell: true,
79
- reject: false,
80
- };
81
-
82
- try {
83
- const proc = execa(fullCommand, [], execaOpts);
84
-
85
- if (operationId) {
86
- proc.stdout?.on('data', (data: Buffer) => {
87
- emitter.emit(operationId, { type: 'stdout', text: data.toString() });
88
- });
89
-
90
- proc.stderr?.on('data', (data: Buffer) => {
91
- emitter.emit(operationId, { type: 'stderr', text: data.toString() });
92
- });
93
- }
94
-
95
- const result = await proc;
96
-
97
- return {
98
- ok: true,
99
- data: {
100
- stdout: String(result.stdout ?? ''),
101
- stderr: String(result.stderr ?? ''),
102
- exitCode: result.exitCode ?? 0,
103
- },
104
- };
105
- } catch (err) {
106
- return { ok: false, error: `Failed to execute ${command}: ${err}` };
107
- }
108
- }
109
-
110
- export function spawnWithIdf(
111
- command: string,
112
- args: string[],
113
- options: RunOptions = {}
114
- ): ResultPromise | null {
115
- const idfPath = process.env.IDF_PATH;
116
-
117
- if (!idfPath) {
118
- return null;
119
- }
120
-
121
- const exportScript = getExportScript(idfPath);
122
- const fullCommand = `source ${exportScript} > /dev/null 2>&1 && ${command} ${args.join(' ')}`;
123
-
124
- const { cwd, operationId, env } = options;
125
-
126
- const proc = execa(fullCommand, [], {
127
- cwd,
128
- env: { ...process.env, ...env },
129
- shell: true,
130
- reject: false,
131
- });
132
-
133
- if (operationId) {
134
- proc.stdout?.on('data', (data: Buffer) => {
135
- emitter.emit(operationId, { type: 'stdout', text: data.toString() });
136
- });
137
-
138
- proc.stderr?.on('data', (data: Buffer) => {
139
- emitter.emit(operationId, { type: 'stderr', text: data.toString() });
140
- });
141
- }
142
-
143
- return proc;
144
- }
@@ -1,74 +0,0 @@
1
- import type { ShellInfo, ShellType, Result } from '@/core/types';
2
- import { SHELL_CONFIGS } from '@/core/constants';
3
- import { homedir } from 'os';
4
- import { join } from 'path';
5
- import { readFile, appendFile, access } from 'fs/promises';
6
-
7
- export function detectShell(): ShellType {
8
- const shell = process.env.SHELL || '';
9
- if (shell.includes('zsh')) return 'zsh';
10
- if (shell.includes('bash')) return 'bash';
11
- if (shell.includes('fish')) return 'fish';
12
- return 'unknown';
13
- }
14
-
15
- export function getShellInfo(): ShellInfo {
16
- const type = detectShell();
17
- const configFile = SHELL_CONFIGS[type] || '';
18
- const configPath = configFile ? join(homedir(), configFile) : '';
19
-
20
- return { type, configPath };
21
- }
22
-
23
- export async function shellConfigExists(): Promise<boolean> {
24
- const { configPath } = getShellInfo();
25
- if (!configPath) return false;
26
-
27
- try {
28
- await access(configPath);
29
- return true;
30
- } catch {
31
- return false;
32
- }
33
- }
34
-
35
- export async function isLineInShellConfig(line: string): Promise<boolean> {
36
- const { configPath } = getShellInfo();
37
- if (!configPath) return false;
38
-
39
- try {
40
- const content = await readFile(configPath, 'utf-8');
41
- return content.includes(line);
42
- } catch {
43
- return false;
44
- }
45
- }
46
-
47
- export async function addToShellConfig(line: string): Promise<Result<void>> {
48
- const { configPath, type } = getShellInfo();
49
-
50
- if (!configPath) {
51
- return { ok: false, error: `Unsupported shell: ${type}` };
52
- }
53
-
54
- const alreadyExists = await isLineInShellConfig(line);
55
- if (alreadyExists) {
56
- return { ok: true, data: undefined };
57
- }
58
-
59
- try {
60
- await appendFile(configPath, `\n${line}\n`);
61
- return { ok: true, data: undefined };
62
- } catch (err) {
63
- return { ok: false, error: `Failed to modify ${configPath}: ${err}` };
64
- }
65
- }
66
-
67
- export function getExportCommand(idfPath: string): string {
68
- const shellType = detectShell();
69
-
70
- if (shellType === 'fish') {
71
- return `source ${idfPath}/export.fish`;
72
- }
73
- return `. ${idfPath}/export.sh`;
74
- }