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.
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "espcli",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Modern CLI for ESP-IDF development — install, create, build, flash, and monitor ESP32 projects",
5
- "module": "src/index.ts",
6
5
  "type": "module",
7
6
  "scripts": {
8
7
  "dev": "bun run src/index.ts",
9
8
  "serve": "bun run src/serve.ts",
10
- "build": "bun build ./src/index.ts --outdir ./dist --target bun --minify",
9
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --minify",
11
10
  "test": "bun test",
12
11
  "typecheck": "tsc --noEmit",
13
12
  "web": "cd web && bun run dev",
@@ -16,7 +15,7 @@
16
15
  "publish": "bun run build && bun publish --access public"
17
16
  },
18
17
  "bin": {
19
- "espcli": "./src/index.ts"
18
+ "espcli": "./dist/index.js"
20
19
  },
21
20
  "devDependencies": {
22
21
  "@types/bun": "latest"
@@ -24,8 +23,8 @@
24
23
  "peerDependencies": {
25
24
  "typescript": "^5.0.0"
26
25
  },
26
+ "types": "dist/index.d.ts",
27
27
  "files": [
28
- "src",
29
28
  "dist"
30
29
  ],
31
30
  "repository": {
@@ -1,107 +0,0 @@
1
- import type { EspTarget } from '@/core/types';
2
- import { homedir } from 'os';
3
- import { join } from 'path';
4
-
5
- /**
6
- * Supported ESP32 target chips
7
- * @see https://github.com/espressif/esptool/blob/master/esptool/targets/__init__.py
8
- */
9
- export const ESP_TARGETS: EspTarget[] = [
10
- // Xtensa-based chips
11
- { id: 'esp32', name: 'ESP32', description: 'Original dual-core Xtensa', stable: true },
12
- { id: 'esp32s2', name: 'ESP32-S2', description: 'Single-core Xtensa with USB OTG', stable: true },
13
- { id: 'esp32s3', name: 'ESP32-S3', description: 'Dual-core Xtensa with AI acceleration', stable: true },
14
- { id: 'esp32s31', name: 'ESP32-S3 (rev1)', description: 'ESP32-S3 revision 1', stable: false },
15
-
16
- // RISC-V based chips
17
- { id: 'esp32c2', name: 'ESP32-C2', description: 'Single-core RISC-V (cost-optimized)', stable: true },
18
- { id: 'esp32c3', name: 'ESP32-C3', description: 'Single-core RISC-V', stable: true },
19
- { id: 'esp32c5', name: 'ESP32-C5', description: 'RISC-V with WiFi 6', stable: false },
20
- { id: 'esp32c6', name: 'ESP32-C6', description: 'RISC-V with WiFi 6 & 802.15.4', stable: true },
21
- { id: 'esp32c61', name: 'ESP32-C61', description: 'ESP32-C6 variant', stable: false },
22
-
23
- // 802.15.4/Thread/Zigbee chips
24
- { id: 'esp32h2', name: 'ESP32-H2', description: 'RISC-V with 802.15.4/Zigbee/Thread', stable: true },
25
- { id: 'esp32h21', name: 'ESP32-H21', description: 'ESP32-H2 variant', stable: false },
26
- { id: 'esp32h4', name: 'ESP32-H4', description: 'RISC-V 802.15.4', stable: false },
27
-
28
- // High-performance chips
29
- { id: 'esp32p4', name: 'ESP32-P4', description: 'High-performance dual-core RISC-V', stable: false },
30
- ];
31
-
32
- export const DEFAULT_IDF_PATH = join(homedir(), 'esp', 'esp-idf');
33
- export const DEFAULT_ESP_PATH = join(homedir(), 'esp');
34
- export const IDF_REPO_URL = 'https://github.com/espressif/esp-idf.git';
35
-
36
- export const DEFAULT_FLASH_BAUD = 460800;
37
- export const DEFAULT_MONITOR_BAUD = 115200;
38
-
39
- /**
40
- * USB Vendor/Product IDs for ESP32 development boards
41
- *
42
- * USB-IF Vendor IDs:
43
- * - 0x303A = Espressif Systems (registered USB vendor): https://github.com/espressif/usb-pids
44
- * - 0x10C4 = Silicon Labs (CP210x USB-UART bridges)
45
- * - 0x1A86 = QinHeng Electronics (CH340/CH343 USB-UART bridges)
46
- * - 0x0403 = FTDI (FT232 USB-UART bridges)
47
- *
48
- * References:
49
- * - USB ID Database: http://www.linux-usb.org/usb.ids
50
- * - Espressif USB PIDs: https://github.com/espressif/esptool/blob/master/esptool/loader.py
51
- * (search for USB_JTAG_SERIAL_PID = 0x1001)
52
- *
53
- * Important: Espressif PIDs identify USB MODE, not specific chip variant:
54
- * - 0x1001 = USB-JTAG/Serial mode (used by ESP32-S2, S3, C3, C6, H2 with built-in USB)
55
- * - 0x1002 = USB-OTG mode (ESP32-S2, S3)
56
- * To identify the actual chip, use esptool.py chip_id command.
57
- */
58
- export const USB_VENDORS: Record<string, { name: string; chips: Record<string, string> }> = {
59
- // Silicon Labs CP210x - common on older ESP32 dev boards
60
- // @see https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers
61
- '10C4': {
62
- name: 'Silicon Labs',
63
- chips: {
64
- 'EA60': 'CP210x',
65
- 'EA70': 'CP2105',
66
- },
67
- },
68
- // WCH (QinHeng) CH340/CH343 - common on budget ESP32 boards
69
- // @see https://www.wch-ic.com/products/CH343.html
70
- '1A86': {
71
- name: 'QinHeng Electronics',
72
- chips: {
73
- '7523': 'CH340',
74
- '5523': 'CH341',
75
- '55D3': 'CH343', // High-speed USB-UART, common on newer boards
76
- '55D4': 'CH9102',
77
- },
78
- },
79
- // FTDI - professional-grade USB-UART bridges
80
- // @see https://ftdichip.com/products/ft232r/
81
- '0403': {
82
- name: 'FTDI',
83
- chips: {
84
- '6001': 'FT232R',
85
- '6010': 'FT2232',
86
- '6011': 'FT4232',
87
- '6014': 'FT232H',
88
- },
89
- },
90
- // Espressif native USB - built into ESP32-S2/S3/C3/C6/H2
91
- // @see https://github.com/espressif/esptool/blob/master/esptool/loader.py#L77
92
- '303A': {
93
- name: 'Espressif',
94
- chips: {
95
- '1001': 'ESP32 (USB-JTAG)', // USB_JTAG_SERIAL_PID
96
- '1002': 'ESP32 (USB-OTG)', // USB_OTG_PID
97
- },
98
- },
99
- };
100
-
101
- export const SHELL_CONFIGS: Record<string, string> = {
102
- zsh: '.zshrc',
103
- bash: '.bashrc',
104
- fish: '.config/fish/config.fish',
105
- };
106
-
107
- export const VERSION = '0.0.1';
@@ -1,48 +0,0 @@
1
- import type { CoreEvent, EventData } from '@/core/types';
2
-
3
- type EventHandler = (event: CoreEvent) => void;
4
-
5
- class OperationEmitter {
6
- private handlers = new Map<string, Set<EventHandler>>();
7
- private globalHandlers = new Set<EventHandler>();
8
-
9
- subscribe(operationId: string, handler: EventHandler): () => void {
10
- if (!this.handlers.has(operationId)) {
11
- this.handlers.set(operationId, new Set());
12
- }
13
- this.handlers.get(operationId)!.add(handler);
14
-
15
- return () => {
16
- this.handlers.get(operationId)?.delete(handler);
17
- };
18
- }
19
-
20
- subscribeAll(handler: EventHandler): () => void {
21
- this.globalHandlers.add(handler);
22
- return () => {
23
- this.globalHandlers.delete(handler);
24
- };
25
- }
26
-
27
- emit(operationId: string, data: EventData): void {
28
- const event: CoreEvent = {
29
- type: data.type,
30
- timestamp: Date.now(),
31
- operationId,
32
- data,
33
- };
34
-
35
- this.handlers.get(operationId)?.forEach((h) => h(event));
36
- this.globalHandlers.forEach((h) => h(event));
37
- }
38
-
39
- cleanup(operationId: string): void {
40
- this.handlers.delete(operationId);
41
- }
42
- }
43
-
44
- export const emitter = new OperationEmitter();
45
-
46
- export function createOperationId(): string {
47
- return `op_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
48
- }
@@ -1,70 +0,0 @@
1
- import type { BuildConfig, Result } from '@/core/types';
2
- import { emitter, createOperationId } from '@/core/emitter';
3
- import { runWithIdf } from '@/core/services/process';
4
- import { isIdfProject } from '@/core/services/idf';
5
-
6
- export interface BuildResult {
7
- success: boolean;
8
- projectDir: string;
9
- }
10
-
11
- export async function build(
12
- config: BuildConfig,
13
- operationId?: string
14
- ): Promise<Result<BuildResult>> {
15
- const opId = operationId || createOperationId();
16
- const { projectDir, target, clean } = config;
17
-
18
- const isProject = await isIdfProject(projectDir);
19
- if (!isProject) {
20
- const error = 'Not an ESP-IDF project directory';
21
- emitter.emit(opId, { type: 'error', message: error });
22
- return { ok: false, error };
23
- }
24
-
25
- if (target) {
26
- emitter.emit(opId, { type: 'progress', message: `Setting target to ${target}...` });
27
-
28
- const targetResult = await runWithIdf('idf.py', ['set-target', target], {
29
- cwd: projectDir,
30
- operationId: opId,
31
- });
32
-
33
- if (!targetResult.ok || targetResult.data.exitCode !== 0) {
34
- const error = 'Failed to set target';
35
- emitter.emit(opId, { type: 'error', message: error });
36
- return { ok: false, error };
37
- }
38
- }
39
-
40
- if (clean) {
41
- emitter.emit(opId, { type: 'progress', message: 'Cleaning...' });
42
-
43
- await runWithIdf('idf.py', ['clean'], {
44
- cwd: projectDir,
45
- operationId: opId,
46
- });
47
- }
48
-
49
- emitter.emit(opId, { type: 'progress', message: 'Building project...' });
50
-
51
- const buildResult = await runWithIdf('idf.py', ['build'], {
52
- cwd: projectDir,
53
- operationId: opId,
54
- });
55
-
56
- if (!buildResult.ok) {
57
- emitter.emit(opId, { type: 'error', message: buildResult.error });
58
- return { ok: false, error: buildResult.error };
59
- }
60
-
61
- if (buildResult.data.exitCode !== 0) {
62
- const error = 'Build failed';
63
- emitter.emit(opId, { type: 'error', message: error });
64
- return { ok: false, error };
65
- }
66
-
67
- emitter.emit(opId, { type: 'complete', result: { success: true, projectDir } });
68
-
69
- return { ok: true, data: { success: true, projectDir } };
70
- }
@@ -1,42 +0,0 @@
1
- import type { CleanConfig, Result } from '@/core/types';
2
- import { emitter, createOperationId } from '@/core/emitter';
3
- import { runWithIdf } from '@/core/services/process';
4
- import { isIdfProject } from '@/core/services/idf';
5
-
6
- export async function clean(
7
- config: CleanConfig,
8
- operationId?: string
9
- ): Promise<Result<void>> {
10
- const opId = operationId || createOperationId();
11
- const { projectDir, full } = config;
12
-
13
- const isProject = await isIdfProject(projectDir);
14
- if (!isProject) {
15
- const error = 'Not an ESP-IDF project directory';
16
- emitter.emit(opId, { type: 'error', message: error });
17
- return { ok: false, error };
18
- }
19
-
20
- const command = full ? 'fullclean' : 'clean';
21
- emitter.emit(opId, { type: 'progress', message: `Running ${command}...` });
22
-
23
- const result = await runWithIdf('idf.py', [command], {
24
- cwd: projectDir,
25
- operationId: opId,
26
- });
27
-
28
- if (!result.ok) {
29
- emitter.emit(opId, { type: 'error', message: result.error });
30
- return { ok: false, error: result.error };
31
- }
32
-
33
- if (result.data.exitCode !== 0) {
34
- const error = `${command} failed`;
35
- emitter.emit(opId, { type: 'error', message: error });
36
- return { ok: false, error };
37
- }
38
-
39
- emitter.emit(opId, { type: 'complete', result: null });
40
-
41
- return { ok: true, data: undefined };
42
- }
@@ -1,11 +0,0 @@
1
- import type { SerialDevice, Result } from '@/core/types';
2
- import { listPorts } from '@/core/services/ports';
3
-
4
- export async function listDevices(): Promise<Result<SerialDevice[]>> {
5
- try {
6
- const devices = await listPorts();
7
- return { ok: true, data: devices };
8
- } catch (err) {
9
- return { ok: false, error: `Failed to list devices: ${err}` };
10
- }
11
- }
@@ -1,49 +0,0 @@
1
- import type { FlashConfig, Result } from '@/core/types';
2
- import { emitter, createOperationId } from '@/core/emitter';
3
- import { runWithIdf } from '@/core/services/process';
4
- import { isIdfProject } from '@/core/services/idf';
5
- import { DEFAULT_FLASH_BAUD } from '@/core/constants';
6
-
7
- export interface FlashResult {
8
- success: boolean;
9
- port: string;
10
- }
11
-
12
- export async function flash(
13
- config: FlashConfig,
14
- operationId?: string
15
- ): Promise<Result<FlashResult>> {
16
- const opId = operationId || createOperationId();
17
- const { projectDir, port, baud = DEFAULT_FLASH_BAUD } = config;
18
-
19
- const isProject = await isIdfProject(projectDir);
20
- if (!isProject) {
21
- const error = 'Not an ESP-IDF project directory';
22
- emitter.emit(opId, { type: 'error', message: error });
23
- return { ok: false, error };
24
- }
25
-
26
- emitter.emit(opId, { type: 'progress', message: `Flashing to ${port}...` });
27
-
28
- const args = ['-p', port, '-b', String(baud), 'flash'];
29
-
30
- const flashResult = await runWithIdf('idf.py', args, {
31
- cwd: projectDir,
32
- operationId: opId,
33
- });
34
-
35
- if (!flashResult.ok) {
36
- emitter.emit(opId, { type: 'error', message: flashResult.error });
37
- return { ok: false, error: flashResult.error };
38
- }
39
-
40
- if (flashResult.data.exitCode !== 0) {
41
- const error = 'Flash failed';
42
- emitter.emit(opId, { type: 'error', message: error });
43
- return { ok: false, error };
44
- }
45
-
46
- emitter.emit(opId, { type: 'complete', result: { success: true, port } });
47
-
48
- return { ok: true, data: { success: true, port } };
49
- }
@@ -1,39 +0,0 @@
1
- import type { InitConfig, InitResult, Result } from '@/core/types';
2
- import { createProject } from '@/core/templates';
3
- import { emitter, createOperationId } from '@/core/emitter';
4
- import { runWithIdf } from '@/core/services/process';
5
-
6
- export async function init(
7
- config: InitConfig,
8
- operationId?: string
9
- ): Promise<Result<InitResult>> {
10
- const opId = operationId || createOperationId();
11
-
12
- emitter.emit(opId, { type: 'progress', message: `Creating project ${config.name}...` });
13
-
14
- const result = await createProject(config);
15
-
16
- if (!result.ok) {
17
- emitter.emit(opId, { type: 'error', message: result.error });
18
- return result;
19
- }
20
-
21
- emitter.emit(opId, { type: 'progress', message: 'Setting target...', percent: 80 });
22
-
23
- const setTargetResult = await runWithIdf('idf.py', ['set-target', config.target], {
24
- cwd: result.data.projectPath,
25
- operationId: opId,
26
- });
27
-
28
- if (!setTargetResult.ok || setTargetResult.data.exitCode !== 0) {
29
- emitter.emit(opId, {
30
- type: 'log',
31
- level: 'warn',
32
- message: 'Failed to set target. You may need to run: idf.py set-target ' + config.target,
33
- });
34
- }
35
-
36
- emitter.emit(opId, { type: 'complete', result: result.data });
37
-
38
- return result;
39
- }
@@ -1,99 +0,0 @@
1
- import type { InstallConfig, InstallResult, Result } from '@/core/types';
2
- import { emitter, createOperationId } from '@/core/emitter';
3
- import { DEFAULT_ESP_PATH, IDF_REPO_URL } from '@/core/constants';
4
- import { run } from '@/core/services/process';
5
- import { addToShellConfig, getExportCommand } from '@/core/services/shell';
6
- import { getIdfVersion } from '@/core/services/idf';
7
- import { mkdir, access } from 'fs/promises';
8
- import { join } from 'path';
9
-
10
- export async function install(
11
- config: Partial<InstallConfig> = {},
12
- operationId?: string
13
- ): Promise<Result<InstallResult>> {
14
- const opId = operationId || createOperationId();
15
- const espPath = config.path || DEFAULT_ESP_PATH;
16
- const target = config.target || 'all';
17
- const addToShell = config.addToShell ?? true;
18
- const idfPath = join(espPath, 'esp-idf');
19
-
20
- try {
21
- await access(idfPath);
22
- const version = await getIdfVersion(idfPath);
23
- emitter.emit(opId, { type: 'log', level: 'info', message: `ESP-IDF already installed at ${idfPath}` });
24
- return {
25
- ok: true,
26
- data: {
27
- idfPath,
28
- version: version || 'unknown',
29
- addedToShell: false,
30
- },
31
- };
32
- } catch {}
33
-
34
- emitter.emit(opId, { type: 'progress', message: 'Creating ESP directory...' });
35
-
36
- try {
37
- await mkdir(espPath, { recursive: true });
38
- } catch (err) {
39
- return { ok: false, error: `Failed to create directory: ${err}` };
40
- }
41
-
42
- emitter.emit(opId, { type: 'progress', message: 'Cloning ESP-IDF repository...', percent: 10 });
43
-
44
- const cloneResult = await run('git', ['clone', '--recursive', IDF_REPO_URL], {
45
- cwd: espPath,
46
- operationId: opId,
47
- });
48
-
49
- if (!cloneResult.ok) {
50
- return { ok: false, error: cloneResult.error };
51
- }
52
-
53
- if (cloneResult.data.exitCode !== 0) {
54
- return { ok: false, error: `Git clone failed with exit code ${cloneResult.data.exitCode}` };
55
- }
56
-
57
- emitter.emit(opId, { type: 'progress', message: 'Running install script...', percent: 50 });
58
-
59
- const installArgs = target === 'all' ? ['all'] : [target];
60
- const installResult = await run('./install.sh', installArgs, {
61
- cwd: idfPath,
62
- operationId: opId,
63
- });
64
-
65
- if (!installResult.ok) {
66
- return { ok: false, error: installResult.error };
67
- }
68
-
69
- if (installResult.data.exitCode !== 0) {
70
- return { ok: false, error: `Install script failed with exit code ${installResult.data.exitCode}` };
71
- }
72
-
73
- let addedToShell = false;
74
-
75
- if (addToShell) {
76
- emitter.emit(opId, { type: 'progress', message: 'Configuring shell...', percent: 90 });
77
- const exportCmd = getExportCommand(idfPath);
78
- const shellResult = await addToShellConfig(exportCmd);
79
-
80
- if (shellResult.ok) {
81
- addedToShell = true;
82
- } else {
83
- emitter.emit(opId, { type: 'log', level: 'warn', message: shellResult.error });
84
- }
85
- }
86
-
87
- const version = await getIdfVersion(idfPath);
88
-
89
- emitter.emit(opId, { type: 'complete', result: { idfPath, version, addedToShell } });
90
-
91
- return {
92
- ok: true,
93
- data: {
94
- idfPath,
95
- version: version || 'unknown',
96
- addedToShell,
97
- },
98
- };
99
- }
@@ -1,67 +0,0 @@
1
- import type { MonitorConfig, Result } from '@/core/types';
2
- import { emitter, createOperationId } from '@/core/emitter';
3
- import { spawnWithIdf } from '@/core/services/process';
4
- import { DEFAULT_MONITOR_BAUD } from '@/core/constants';
5
- import type { ResultPromise } from 'execa';
6
-
7
- const activeMonitors = new Map<string, ResultPromise>();
8
-
9
- export interface MonitorHandle {
10
- operationId: string;
11
- stop: () => void;
12
- sendInput: (data: string) => void;
13
- }
14
-
15
- export function startMonitor(
16
- config: MonitorConfig,
17
- operationId?: string
18
- ): Result<MonitorHandle> {
19
- const opId = operationId || createOperationId();
20
- const { port, baud = DEFAULT_MONITOR_BAUD, projectDir } = config;
21
-
22
- const args = ['-p', port, '-b', String(baud), 'monitor'];
23
-
24
- const proc = spawnWithIdf('idf.py', args, {
25
- cwd: projectDir || process.cwd(),
26
- operationId: opId,
27
- });
28
-
29
- if (!proc) {
30
- const error = 'ESP-IDF not found';
31
- emitter.emit(opId, { type: 'error', message: error });
32
- return { ok: false, error };
33
- }
34
-
35
- activeMonitors.set(opId, proc);
36
-
37
- proc.then(() => {
38
- activeMonitors.delete(opId);
39
- emitter.emit(opId, { type: 'complete', result: { stopped: true } });
40
- });
41
-
42
- const handle: MonitorHandle = {
43
- operationId: opId,
44
- stop: () => {
45
- proc.kill('SIGTERM');
46
- activeMonitors.delete(opId);
47
- },
48
- sendInput: (data: string) => {
49
- proc.stdin?.write(data);
50
- },
51
- };
52
-
53
- return { ok: true, data: handle };
54
- }
55
-
56
- export function stopMonitor(operationId: string): boolean {
57
- const proc = activeMonitors.get(operationId);
58
- if (!proc) return false;
59
-
60
- proc.kill('SIGTERM');
61
- activeMonitors.delete(operationId);
62
- return true;
63
- }
64
-
65
- export function getActiveMonitors(): string[] {
66
- return Array.from(activeMonitors.keys());
67
- }