espcli 0.0.1
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/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/index.js +185 -0
- package/package.json +50 -0
- package/src/core/constants.ts +107 -0
- package/src/core/emitter.ts +48 -0
- package/src/core/operations/build.ts +70 -0
- package/src/core/operations/clean.ts +42 -0
- package/src/core/operations/devices.ts +11 -0
- package/src/core/operations/flash.ts +49 -0
- package/src/core/operations/init.ts +39 -0
- package/src/core/operations/install.ts +99 -0
- package/src/core/operations/monitor.ts +67 -0
- package/src/core/services/health.ts +214 -0
- package/src/core/services/idf.ts +98 -0
- package/src/core/services/ports.ts +172 -0
- package/src/core/services/process.ts +144 -0
- package/src/core/services/shell.ts +74 -0
- package/src/core/templates/files.ts +78 -0
- package/src/core/templates/index.ts +52 -0
- package/src/core/types.ts +128 -0
- package/src/index.ts +5 -0
- package/src/serve.ts +8 -0
- package/src/server/index.ts +105 -0
- package/src/server/routes.ts +175 -0
- package/src/server/schema.ts +81 -0
- package/src/tui/commands/build.ts +48 -0
- package/src/tui/commands/clean.ts +36 -0
- package/src/tui/commands/devices.ts +18 -0
- package/src/tui/commands/doctor.ts +70 -0
- package/src/tui/commands/flash.ts +61 -0
- package/src/tui/commands/init.ts +59 -0
- package/src/tui/commands/install.ts +89 -0
- package/src/tui/commands/monitor.ts +63 -0
- package/src/tui/commands/run.ts +99 -0
- package/src/tui/display.ts +96 -0
- package/src/tui/index.ts +96 -0
- package/src/tui/logger.ts +35 -0
- package/src/tui/prompts.ts +99 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export function getRootCMakeLists(projectName: string): string {
|
|
2
|
+
return `cmake_minimum_required(VERSION 3.16)
|
|
3
|
+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
|
4
|
+
project(${projectName})
|
|
5
|
+
`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getMainCMakeLists(mainFile: string): string {
|
|
9
|
+
return `idf_component_register(SRCS "${mainFile}"
|
|
10
|
+
INCLUDE_DIRS ".")
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCMain(projectName: string): string {
|
|
15
|
+
return `#include <stdio.h>
|
|
16
|
+
#include "freertos/FreeRTOS.h"
|
|
17
|
+
#include "freertos/task.h"
|
|
18
|
+
|
|
19
|
+
void app_main(void)
|
|
20
|
+
{
|
|
21
|
+
while (1) {
|
|
22
|
+
printf("Hello from ${projectName}!\\n");
|
|
23
|
+
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getCppMain(projectName: string): string {
|
|
30
|
+
return `#include <cstdio>
|
|
31
|
+
#include "freertos/FreeRTOS.h"
|
|
32
|
+
#include "freertos/task.h"
|
|
33
|
+
|
|
34
|
+
extern "C" void app_main(void)
|
|
35
|
+
{
|
|
36
|
+
while (true) {
|
|
37
|
+
printf("Hello from ${projectName}!\\n");
|
|
38
|
+
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getGitignore(): string {
|
|
45
|
+
return `build/
|
|
46
|
+
sdkconfig
|
|
47
|
+
sdkconfig.old
|
|
48
|
+
.vscode/
|
|
49
|
+
*.pyc
|
|
50
|
+
__pycache__/
|
|
51
|
+
.DS_Store
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getReadme(projectName: string, target: string): string {
|
|
56
|
+
return `# ${projectName}
|
|
57
|
+
|
|
58
|
+
ESP-IDF project targeting ${target}.
|
|
59
|
+
|
|
60
|
+
## Build
|
|
61
|
+
|
|
62
|
+
\`\`\`bash
|
|
63
|
+
idf.py build
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
## Flash
|
|
67
|
+
|
|
68
|
+
\`\`\`bash
|
|
69
|
+
idf.py -p PORT flash
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
## Monitor
|
|
73
|
+
|
|
74
|
+
\`\`\`bash
|
|
75
|
+
idf.py -p PORT monitor
|
|
76
|
+
\`\`\`
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { InitConfig, InitResult, Result, TemplateFile } from '@/core/types';
|
|
2
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
getRootCMakeLists,
|
|
6
|
+
getMainCMakeLists,
|
|
7
|
+
getCMain,
|
|
8
|
+
getCppMain,
|
|
9
|
+
getGitignore,
|
|
10
|
+
getReadme,
|
|
11
|
+
} from '@/core/templates/files';
|
|
12
|
+
|
|
13
|
+
export function generateProjectFiles(config: InitConfig): TemplateFile[] {
|
|
14
|
+
const { name, language, target } = config;
|
|
15
|
+
const mainFile = language === 'cpp' ? 'main.cpp' : 'main.c';
|
|
16
|
+
const mainContent = language === 'cpp' ? getCppMain(name) : getCMain(name);
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
{ path: 'CMakeLists.txt', content: getRootCMakeLists(name) },
|
|
20
|
+
{ path: 'main/CMakeLists.txt', content: getMainCMakeLists(mainFile) },
|
|
21
|
+
{ path: `main/${mainFile}`, content: mainContent },
|
|
22
|
+
{ path: '.gitignore', content: getGitignore() },
|
|
23
|
+
{ path: 'README.md', content: getReadme(name, target) },
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function createProject(config: InitConfig): Promise<Result<InitResult>> {
|
|
28
|
+
const projectPath = join(config.directory, config.name);
|
|
29
|
+
const files = generateProjectFiles(config);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await mkdir(join(projectPath, 'main'), { recursive: true });
|
|
33
|
+
|
|
34
|
+
const createdFiles: string[] = [];
|
|
35
|
+
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const filePath = join(projectPath, file.path);
|
|
38
|
+
await writeFile(filePath, file.content);
|
|
39
|
+
createdFiles.push(file.path);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
data: {
|
|
45
|
+
projectPath,
|
|
46
|
+
files: createdFiles,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return { ok: false, error: `Failed to create project: ${err}` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export type Result<T> =
|
|
2
|
+
| { ok: true; data: T }
|
|
3
|
+
| { ok: false; error: string; code?: string };
|
|
4
|
+
|
|
5
|
+
export type ShellType = 'zsh' | 'bash' | 'fish' | 'unknown';
|
|
6
|
+
|
|
7
|
+
export interface ShellInfo {
|
|
8
|
+
type: ShellType;
|
|
9
|
+
configPath: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface EspTarget {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
stable: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ConnectionType = 'native-usb' | 'uart-bridge' | 'unknown';
|
|
20
|
+
|
|
21
|
+
export interface SerialDevice {
|
|
22
|
+
port: string;
|
|
23
|
+
vendorId?: string;
|
|
24
|
+
productId?: string;
|
|
25
|
+
manufacturer?: string;
|
|
26
|
+
chip?: string;
|
|
27
|
+
connectionType: ConnectionType;
|
|
28
|
+
espChip?: string; // Actual ESP chip detected via esptool (e.g., "ESP32-S3")
|
|
29
|
+
description?: string; // USB device description (e.g., "USB JTAG/serial debug unit")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IdfStatus {
|
|
33
|
+
installed: boolean;
|
|
34
|
+
path?: string;
|
|
35
|
+
version?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface InstallConfig {
|
|
39
|
+
path: string;
|
|
40
|
+
target: string;
|
|
41
|
+
addToShell: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface InstallResult {
|
|
45
|
+
idfPath: string;
|
|
46
|
+
version: string;
|
|
47
|
+
addedToShell: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface InitConfig {
|
|
51
|
+
name: string;
|
|
52
|
+
directory: string;
|
|
53
|
+
language: 'c' | 'cpp';
|
|
54
|
+
target: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface InitResult {
|
|
58
|
+
projectPath: string;
|
|
59
|
+
files: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BuildConfig {
|
|
63
|
+
projectDir: string;
|
|
64
|
+
target?: string;
|
|
65
|
+
clean?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface FlashConfig {
|
|
69
|
+
projectDir: string;
|
|
70
|
+
port: string;
|
|
71
|
+
baud?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface MonitorConfig {
|
|
75
|
+
port: string;
|
|
76
|
+
baud?: number;
|
|
77
|
+
projectDir?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CleanConfig {
|
|
81
|
+
projectDir: string;
|
|
82
|
+
full?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type EventType =
|
|
86
|
+
| 'progress'
|
|
87
|
+
| 'log'
|
|
88
|
+
| 'stdout'
|
|
89
|
+
| 'stderr'
|
|
90
|
+
| 'complete'
|
|
91
|
+
| 'error';
|
|
92
|
+
|
|
93
|
+
export type LogLevel = 'info' | 'warn' | 'error';
|
|
94
|
+
|
|
95
|
+
export type EventData =
|
|
96
|
+
| { type: 'progress'; message: string; percent?: number }
|
|
97
|
+
| { type: 'log'; level: LogLevel; message: string }
|
|
98
|
+
| { type: 'stdout'; text: string }
|
|
99
|
+
| { type: 'stderr'; text: string }
|
|
100
|
+
| { type: 'complete'; result: unknown }
|
|
101
|
+
| { type: 'error'; message: string; code?: string };
|
|
102
|
+
|
|
103
|
+
export interface CoreEvent {
|
|
104
|
+
type: EventType;
|
|
105
|
+
timestamp: number;
|
|
106
|
+
operationId: string;
|
|
107
|
+
data: EventData;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface TemplateFile {
|
|
111
|
+
path: string;
|
|
112
|
+
content: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ProjectTemplate {
|
|
116
|
+
language: 'c' | 'cpp';
|
|
117
|
+
files: TemplateFile[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type WsClientMessage =
|
|
121
|
+
| { type: 'subscribe'; operationId: string }
|
|
122
|
+
| { type: 'unsubscribe'; operationId: string }
|
|
123
|
+
| { type: 'input'; operationId: string; data: string };
|
|
124
|
+
|
|
125
|
+
export type WsServerMessage =
|
|
126
|
+
| { type: 'event'; operationId: string; event: CoreEvent }
|
|
127
|
+
| { type: 'subscribed'; operationId: string }
|
|
128
|
+
| { type: 'error'; message: string };
|
package/src/index.ts
ADDED
package/src/serve.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createServer } from '@/server';
|
|
3
|
+
|
|
4
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
5
|
+
const server = createServer(port);
|
|
6
|
+
|
|
7
|
+
console.log(`ESP CLI server running on http://localhost:${server.port}`);
|
|
8
|
+
console.log(`WebSocket available at ws://localhost:${server.port}/ws`);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ServerWebSocket } from 'bun';
|
|
2
|
+
import { matchRoute } from '@/server/routes';
|
|
3
|
+
import { emitter } from '@/core/emitter';
|
|
4
|
+
import type { WsClientMessage, WsServerMessage, CoreEvent } from '@/core/types';
|
|
5
|
+
|
|
6
|
+
interface WsData {
|
|
7
|
+
subscriptions: Set<string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const clients = new Set<ServerWebSocket<WsData>>();
|
|
11
|
+
|
|
12
|
+
function broadcast(operationId: string, event: CoreEvent): void {
|
|
13
|
+
const message: WsServerMessage = { type: 'event', operationId, event };
|
|
14
|
+
const payload = JSON.stringify(message);
|
|
15
|
+
|
|
16
|
+
for (const ws of clients) {
|
|
17
|
+
if (ws.data.subscriptions.has(operationId)) {
|
|
18
|
+
ws.send(payload);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
emitter.subscribeAll((event) => {
|
|
24
|
+
broadcast(event.operationId, event);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function corsHeaders(): HeadersInit {
|
|
28
|
+
return {
|
|
29
|
+
'Access-Control-Allow-Origin': '*',
|
|
30
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
31
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function withCors(response: Response): Response {
|
|
36
|
+
const headers = new Headers(response.headers);
|
|
37
|
+
Object.entries(corsHeaders()).forEach(([key, value]) => {
|
|
38
|
+
headers.set(key, value);
|
|
39
|
+
});
|
|
40
|
+
return new Response(response.body, {
|
|
41
|
+
status: response.status,
|
|
42
|
+
statusText: response.statusText,
|
|
43
|
+
headers,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createServer(port: number) {
|
|
48
|
+
return Bun.serve<WsData>({
|
|
49
|
+
port,
|
|
50
|
+
async fetch(req, server) {
|
|
51
|
+
const url = new URL(req.url);
|
|
52
|
+
|
|
53
|
+
// Handle CORS preflight
|
|
54
|
+
if (req.method === 'OPTIONS') {
|
|
55
|
+
return new Response(null, { headers: corsHeaders() });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (url.pathname === '/ws') {
|
|
59
|
+
const upgraded = server.upgrade(req, {
|
|
60
|
+
data: { subscriptions: new Set<string>() },
|
|
61
|
+
});
|
|
62
|
+
if (upgraded) return undefined;
|
|
63
|
+
return new Response('WebSocket upgrade failed', { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const handler = matchRoute(req.method, url.pathname);
|
|
67
|
+
|
|
68
|
+
if (handler) {
|
|
69
|
+
const response = await handler(req);
|
|
70
|
+
return withCors(response);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return withCors(new Response('Not Found', { status: 404 }));
|
|
74
|
+
},
|
|
75
|
+
websocket: {
|
|
76
|
+
open(ws) {
|
|
77
|
+
clients.add(ws);
|
|
78
|
+
},
|
|
79
|
+
close(ws) {
|
|
80
|
+
clients.delete(ws);
|
|
81
|
+
},
|
|
82
|
+
message(ws, message) {
|
|
83
|
+
try {
|
|
84
|
+
const data = JSON.parse(message.toString()) as WsClientMessage;
|
|
85
|
+
|
|
86
|
+
switch (data.type) {
|
|
87
|
+
case 'subscribe':
|
|
88
|
+
ws.data.subscriptions.add(data.operationId);
|
|
89
|
+
ws.send(JSON.stringify({ type: 'subscribed', operationId: data.operationId }));
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
case 'unsubscribe':
|
|
93
|
+
ws.data.subscriptions.delete(data.operationId);
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case 'input':
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }));
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { VERSION, ESP_TARGETS } from '@/core/constants';
|
|
2
|
+
import { getIdfStatus } from '@/core/services/idf';
|
|
3
|
+
import { listDevices } from '@/core/operations/devices';
|
|
4
|
+
import { install } from '@/core/operations/install';
|
|
5
|
+
import { init } from '@/core/operations/init';
|
|
6
|
+
import { build } from '@/core/operations/build';
|
|
7
|
+
import { flash } from '@/core/operations/flash';
|
|
8
|
+
import { startMonitor, stopMonitor } from '@/core/operations/monitor';
|
|
9
|
+
import { clean } from '@/core/operations/clean';
|
|
10
|
+
import { createOperationId } from '@/core/emitter';
|
|
11
|
+
import type {
|
|
12
|
+
HealthResponse,
|
|
13
|
+
TargetsResponse,
|
|
14
|
+
DevicesResponse,
|
|
15
|
+
InstallRequest,
|
|
16
|
+
InitRequest,
|
|
17
|
+
BuildRequest,
|
|
18
|
+
FlashRequest,
|
|
19
|
+
MonitorRequest,
|
|
20
|
+
MonitorStopRequest,
|
|
21
|
+
CleanRequest,
|
|
22
|
+
} from '@/server/schema';
|
|
23
|
+
|
|
24
|
+
type RouteHandler = (req: Request) => Promise<Response>;
|
|
25
|
+
|
|
26
|
+
function json<T>(data: T, status = 200): Response {
|
|
27
|
+
return new Response(JSON.stringify(data), {
|
|
28
|
+
status,
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function error(message: string, status = 400): Response {
|
|
34
|
+
return json({ error: message }, status);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function parseBody<T>(req: Request): Promise<T | null> {
|
|
38
|
+
try {
|
|
39
|
+
return (await req.json()) as T;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const routes: Record<string, RouteHandler> = {
|
|
46
|
+
'GET /api/health': async () => {
|
|
47
|
+
const response: HealthResponse = { status: 'ok', version: VERSION };
|
|
48
|
+
return json(response);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
'GET /api/targets': async () => {
|
|
52
|
+
const response: TargetsResponse = { targets: ESP_TARGETS };
|
|
53
|
+
return json(response);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
'GET /api/devices': async () => {
|
|
57
|
+
const result = await listDevices();
|
|
58
|
+
if (!result.ok) {
|
|
59
|
+
return error(result.error, 500);
|
|
60
|
+
}
|
|
61
|
+
const response: DevicesResponse = { devices: result.data };
|
|
62
|
+
return json(response);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
'GET /api/idf/status': async () => {
|
|
66
|
+
const status = await getIdfStatus();
|
|
67
|
+
return json(status);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
'POST /api/install': async (req) => {
|
|
71
|
+
const body = await parseBody<InstallRequest>(req);
|
|
72
|
+
const operationId = createOperationId();
|
|
73
|
+
|
|
74
|
+
install(
|
|
75
|
+
{
|
|
76
|
+
path: body?.path || '',
|
|
77
|
+
target: body?.target || 'all',
|
|
78
|
+
addToShell: body?.addToShell ?? true,
|
|
79
|
+
},
|
|
80
|
+
operationId
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return json({ operationId });
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
'POST /api/init': async (req) => {
|
|
87
|
+
const body = await parseBody<InitRequest>(req);
|
|
88
|
+
|
|
89
|
+
if (!body?.name || !body.directory || !body.language || !body.target) {
|
|
90
|
+
return error('Missing required fields: name, directory, language, target');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await init(body);
|
|
94
|
+
|
|
95
|
+
if (!result.ok) {
|
|
96
|
+
return json({ ok: false, error: result.error });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return json({ ok: true, projectPath: result.data.projectPath });
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
'POST /api/build': async (req) => {
|
|
103
|
+
const body = await parseBody<BuildRequest>(req);
|
|
104
|
+
|
|
105
|
+
if (!body?.projectDir) {
|
|
106
|
+
return error('Missing required field: projectDir');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const operationId = createOperationId();
|
|
110
|
+
|
|
111
|
+
build(body, operationId);
|
|
112
|
+
|
|
113
|
+
return json({ operationId });
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
'POST /api/flash': async (req) => {
|
|
117
|
+
const body = await parseBody<FlashRequest>(req);
|
|
118
|
+
|
|
119
|
+
if (!body?.projectDir || !body.port) {
|
|
120
|
+
return error('Missing required fields: projectDir, port');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const operationId = createOperationId();
|
|
124
|
+
|
|
125
|
+
flash(body, operationId);
|
|
126
|
+
|
|
127
|
+
return json({ operationId });
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
'POST /api/monitor': async (req) => {
|
|
131
|
+
const body = await parseBody<MonitorRequest>(req);
|
|
132
|
+
|
|
133
|
+
if (!body?.port) {
|
|
134
|
+
return error('Missing required field: port');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const operationId = createOperationId();
|
|
138
|
+
const result = startMonitor(body, operationId);
|
|
139
|
+
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
return error(result.error, 500);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return json({ operationId });
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
'POST /api/monitor/stop': async (req) => {
|
|
148
|
+
const body = await parseBody<MonitorStopRequest>(req);
|
|
149
|
+
|
|
150
|
+
if (!body?.operationId) {
|
|
151
|
+
return error('Missing required field: operationId');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const stopped = stopMonitor(body.operationId);
|
|
155
|
+
|
|
156
|
+
return json({ ok: stopped });
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
'POST /api/clean': async (req) => {
|
|
160
|
+
const body = await parseBody<CleanRequest>(req);
|
|
161
|
+
|
|
162
|
+
if (!body?.projectDir) {
|
|
163
|
+
return error('Missing required field: projectDir');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = await clean(body);
|
|
167
|
+
|
|
168
|
+
return json({ ok: result.ok, error: result.ok ? undefined : result.error });
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export function matchRoute(method: string, path: string): RouteHandler | null {
|
|
173
|
+
const key = `${method} ${path}`;
|
|
174
|
+
return routes[key] || null;
|
|
175
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { EspTarget, SerialDevice, IdfStatus, CoreEvent } from '@/core/types';
|
|
2
|
+
|
|
3
|
+
export interface HealthResponse {
|
|
4
|
+
status: 'ok';
|
|
5
|
+
version: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TargetsResponse {
|
|
9
|
+
targets: EspTarget[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DevicesResponse {
|
|
13
|
+
devices: SerialDevice[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IdfStatusResponse extends IdfStatus {}
|
|
17
|
+
|
|
18
|
+
export interface InstallRequest {
|
|
19
|
+
path?: string;
|
|
20
|
+
target?: string;
|
|
21
|
+
addToShell?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OperationResponse {
|
|
25
|
+
operationId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InitRequest {
|
|
29
|
+
name: string;
|
|
30
|
+
directory: string;
|
|
31
|
+
language: 'c' | 'cpp';
|
|
32
|
+
target: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface InitResponse {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
projectPath?: string;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BuildRequest {
|
|
42
|
+
projectDir: string;
|
|
43
|
+
target?: string;
|
|
44
|
+
clean?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface FlashRequest {
|
|
48
|
+
projectDir: string;
|
|
49
|
+
port: string;
|
|
50
|
+
baud?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MonitorRequest {
|
|
54
|
+
port: string;
|
|
55
|
+
baud?: number;
|
|
56
|
+
projectDir?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MonitorStopRequest {
|
|
60
|
+
operationId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CleanRequest {
|
|
64
|
+
projectDir: string;
|
|
65
|
+
full?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CleanResponse {
|
|
69
|
+
ok: boolean;
|
|
70
|
+
error?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type WsClientMessage =
|
|
74
|
+
| { type: 'subscribe'; operationId: string }
|
|
75
|
+
| { type: 'unsubscribe'; operationId: string }
|
|
76
|
+
| { type: 'input'; operationId: string; data: string };
|
|
77
|
+
|
|
78
|
+
export type WsServerMessage =
|
|
79
|
+
| { type: 'event'; operationId: string; event: CoreEvent }
|
|
80
|
+
| { type: 'subscribed'; operationId: string }
|
|
81
|
+
| { type: 'error'; message: string };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { build } from '@/core/operations/build';
|
|
2
|
+
import { findProjectRoot } from '@/core/services/idf';
|
|
3
|
+
import { emitter, createOperationId } from '@/core/emitter';
|
|
4
|
+
import { logger } from '@/tui/logger';
|
|
5
|
+
|
|
6
|
+
interface BuildOptions {
|
|
7
|
+
target?: string;
|
|
8
|
+
clean?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function buildCommand(options: BuildOptions): Promise<void> {
|
|
12
|
+
const projectDir = await findProjectRoot(process.cwd());
|
|
13
|
+
|
|
14
|
+
if (!projectDir) {
|
|
15
|
+
logger.error('Not in an ESP-IDF project directory');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const opId = createOperationId();
|
|
20
|
+
|
|
21
|
+
emitter.subscribe(opId, (event) => {
|
|
22
|
+
if (event.data.type === 'stdout') {
|
|
23
|
+
logger.output(event.data.text);
|
|
24
|
+
} else if (event.data.type === 'stderr') {
|
|
25
|
+
logger.output(event.data.text);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
logger.step('Building project...');
|
|
30
|
+
|
|
31
|
+
const result = await build(
|
|
32
|
+
{
|
|
33
|
+
projectDir,
|
|
34
|
+
target: options.target,
|
|
35
|
+
clean: options.clean,
|
|
36
|
+
},
|
|
37
|
+
opId
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
logger.newline();
|
|
42
|
+
logger.error(result.error);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
logger.newline();
|
|
47
|
+
logger.success('Build complete');
|
|
48
|
+
}
|