@kvell007/embed-labs-cli 0.1.0-alpha.3 → 0.1.0-alpha.30
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/README.md +44 -3
- package/dist/index.js +2022 -81
- package/dist/index.js.map +1 -1
- package/dist/local-toolchain.d.ts +198 -1
- package/dist/local-toolchain.js +980 -22
- package/dist/local-toolchain.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, createHmac, generateKeyPairSync, randomBytes, sign as signCrypto } from "node:crypto";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { access, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { arch, hostname, homedir, platform, tmpdir } from "node:os";
|
|
8
8
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { startServer } from "@embed-labs/local-bridge";
|
|
11
10
|
import { composeBootLogoPackage } from "./image-compose.js";
|
|
12
|
-
import { buildTaishanPiQtSmoke, compileTaishanPiSingleFile, validateLocalToolchain } from "./local-toolchain.js";
|
|
11
|
+
import { buildTaishanPiQtSmoke, compileTaishanPiSingleFile, currentLocalToolchain, installLocalToolchain, latestLocalToolchain, listLocalToolchainEnvironments, validateLocalToolchain } from "./local-toolchain.js";
|
|
13
12
|
import { fail, ok } from "@embed-labs/protocol";
|
|
14
13
|
const require = createRequire(import.meta.url);
|
|
15
14
|
const CLI_MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
16
15
|
const SOURCE_CHECKOUT_ROOT = resolve(CLI_MODULE_DIR, "..", "..", "..");
|
|
17
16
|
const qrcodeTerminal = require("qrcode-terminal");
|
|
18
17
|
const DEFAULT_BRIDGE_URL = process.env.EMBED_BRIDGE_URL ?? "http://127.0.0.1:18083";
|
|
19
|
-
const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "
|
|
18
|
+
const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "https://api.embedboard.com";
|
|
20
19
|
const DEFAULT_AUTH_FILE = process.env.EMBED_AUTH_FILE ?? ".embed-labs/auth.json";
|
|
20
|
+
const DEFAULT_DEVICE_FILE = process.env.EMBED_DEVICE_FILE ?? join(dirname(DEFAULT_AUTH_FILE), "device.json");
|
|
21
|
+
const DEFAULT_DASHBOARD_URL = process.env.EMBED_DASHBOARD_URL ?? "https://api.embedboard.com/dashboard";
|
|
22
|
+
const EMBED_CLIENT_NAME = "embedlabs-cli";
|
|
23
|
+
const EMBED_CLIENT_VERSION = process.env.EMBED_CLIENT_VERSION ?? "0.1.0";
|
|
21
24
|
const DEFAULT_AGENT_ARTIFACT_DIR = process.env.EMBED_AGENT_ARTIFACT_DIR ?? ".embed-labs/artifacts";
|
|
25
|
+
const TOOL_INTEGRITY_RELOGIN_MESSAGE = "工具完整性校验失败,请在当前电脑重新执行 embedlabs auth login --token <key>";
|
|
22
26
|
const DOCTOR_USAGE = "Usage: embed doctor [--json]";
|
|
23
27
|
const DOCTOR_HTTP_TIMEOUT_MS = 8000;
|
|
24
28
|
const MIN_NODE_MAJOR = 20;
|
|
@@ -27,7 +31,19 @@ const DEVICE_PROBE_USAGE = "Usage: embed device probe --host <host> --ports 22,1
|
|
|
27
31
|
const QUERY_USAGE = "Usage: embed query <natural language request> [--account <account_id>] [--qr] [--json]";
|
|
28
32
|
const DEFAULT_PLUGIN_RELEASE_URL = process.env.EMBED_PLUGIN_RELEASE_URL?.trim() || "https://api.embedboard.com/plugin-releases/agent-plugins/latest";
|
|
29
33
|
const PLUGIN_LIST_USAGE = "Usage: embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]";
|
|
34
|
+
const CODEX_PLUGIN_NAME = "embed-labs";
|
|
35
|
+
const CODEX_MARKETPLACE_NAME = "embed-labs";
|
|
36
|
+
const LEGACY_CODEX_PLUGIN_NAMES = [
|
|
37
|
+
"dbt-agent",
|
|
38
|
+
"Dbt Agent",
|
|
39
|
+
"development-board-toolchain",
|
|
40
|
+
"development-board-toolchain-dev",
|
|
41
|
+
"deve"
|
|
42
|
+
];
|
|
43
|
+
const LEGACY_CODEX_MARKETPLACE_NAMES = new Set(["embed-labs-plugins", "plugins", "Plugins", "deve"]);
|
|
30
44
|
const PLUGIN_INSTALL_USAGE = "Usage: embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]";
|
|
45
|
+
const PLUGIN_UPDATE_CHECK_USAGE = "Usage: embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
|
|
46
|
+
const PLUGIN_UPDATE_USAGE = "Usage: embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
|
|
31
47
|
const CLOUD_TASK_ARTIFACTS_USAGE = "Usage: embed cloud task artifacts <task_id> [--json]";
|
|
32
48
|
const CLOUD_TASK_EVIDENCE_USAGE = "Usage: embed cloud task evidence <task_id> [--json]";
|
|
33
49
|
const ARTIFACT_STATUS_USAGE = "Usage: embed artifact status <artifact_id> [--json]";
|
|
@@ -81,9 +97,17 @@ const BUILD_IMAGE_BOOT_LOGO_USAGE = "Usage: embed build image boot-logo --logo <
|
|
|
81
97
|
const IMAGE_BOOT_LOGO_COMPOSE_USAGE = "Usage: embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]";
|
|
82
98
|
const BUILD_IMAGE_DTB_USAGE = "Usage: embed build image dtb --dtb <local.dtb|local.dts> [--input-format auto|dtb|dts] [--account <account_id>] [--project <project_id>] [--board taishanpi] [--variant 1M-RK3566] [--output <package.json>] [--json]";
|
|
83
99
|
const IMAGE_DTB_COMPOSE_USAGE = "Usage: embed image dtb compose --package <dtb-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]";
|
|
84
|
-
const
|
|
100
|
+
const LOCAL_TOOLCHAIN_LIST_USAGE = "Usage: embed local toolchain list [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]";
|
|
101
|
+
const LOCAL_TOOLCHAIN_LATEST_USAGE = "Usage: embed local toolchain latest [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--json]";
|
|
102
|
+
const LOCAL_TOOLCHAIN_CURRENT_USAGE = "Usage: embed local toolchain current [--install-root <path>] [--json]";
|
|
103
|
+
const LOCAL_TOOLCHAIN_INSTALL_USAGE = "Usage: embed local toolchain install [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--mode minimal|compile|qt|full|images] [--force] [--json]\nDefault source: the production download channel at download.embedboard.com.";
|
|
104
|
+
const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--release-root <path>] [--mode minimal|compile|qt|full|images] [--json]";
|
|
85
105
|
const LOCAL_COMPILE_TAISHANPI_USAGE = "Usage: embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]";
|
|
86
106
|
const LOCAL_BUILD_QT_SMOKE_USAGE = "Usage: embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]";
|
|
107
|
+
const AUTH_DEVICE_STATUS_USAGE = "Usage: embed auth device status [--json]";
|
|
108
|
+
const AUTH_DEVICE_LIST_USAGE = "Usage: embed auth device list [--json]";
|
|
109
|
+
const AUTH_DEVICE_REVOKE_USAGE = "Usage: embed auth device revoke <device_id> [--json]";
|
|
110
|
+
const AUTH_DEVICE_RENAME_USAGE = "Usage: embed auth device rename <device_id> --label <name> [--json]";
|
|
87
111
|
const BOARD_REGISTRY_LIST_USAGE = "Usage: embed board registry list [--json]";
|
|
88
112
|
const BOARD_REGISTRY_SHOW_USAGE = "Usage: embed board registry show <template_id> [--json]";
|
|
89
113
|
const BOARD_METHODS_USAGE = "Usage: embed board methods <template_id> [--json]";
|
|
@@ -93,8 +117,10 @@ const MODEL_LIST_USAGE = "Usage: embed model list [--json]";
|
|
|
93
117
|
const MODEL_DEFAULT_USAGE = "Usage: embed model default [--json]";
|
|
94
118
|
const SERVICE_MODES_USAGE = "Usage: embed service modes [--json]";
|
|
95
119
|
const AGENT_RUN_USAGE = "Usage: embed agent run --prompt <request> [--account <account_id>] [--workspace <workspace_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--max-tool-calls 6] [--host <ip>] [--ports 22,15301] [--artifact <local_file>|--artifact-id <artifact_id>|--artifact-task <task_id>] [--artifact-output <path>] [--remote-path <path>] [--run] [--approve] [--json]";
|
|
120
|
+
let cachedLocalHardwareFingerprint;
|
|
96
121
|
const TOOL_LIST_USAGE = "Usage: embed tool list [--json]";
|
|
97
122
|
const TOOL_CALL_USAGE = "Usage: embed tool call <capability_id> [--input-json '<json>'] [--approve] [--json]";
|
|
123
|
+
const MCP_TOOL_EVENT_USAGE = "Usage: embed mcp log --tool <tool_name> [--client codex|opencode] [--mode local_ai|server_ai] [--local-device-id <id>] [--server-model-used true|false] [--success true|false] [--request-id <id>] [--duration-ms <ms>] [--input-summary <text>] [--output-summary <text>] [--json]";
|
|
98
124
|
const BOARD_DEPLOY_TAISHANPI_USAGE = "Usage: embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--user root] [--remote-path /userdata/embed-labs/apps/app] [--run] [--timeout 30] [--json]";
|
|
99
125
|
const CLOUD_TASK_EVENT_APPEND_USAGE = "Usage: embed cloud task event append <task_id> [--state <state>] [--progress-stage <stage>|--stage <stage>] [--progress-text <text>|--message <text>] [--progress-percent 0-100] [--severity info|warning|error] [--type <event_type>] [--artifact-json '<json>'] [--evidence-json '<json>'] [--json]";
|
|
100
126
|
const TASK_STATES = new Set([
|
|
@@ -144,11 +170,7 @@ async function main(argv) {
|
|
|
144
170
|
return output(parsed, await bridgePost("/v1/board/taishanpi/deploy", request), renderBoardDeployResult);
|
|
145
171
|
}
|
|
146
172
|
if (area === "bridge" && action === "start") {
|
|
147
|
-
|
|
148
|
-
host: stringFlag(parsed, "host"),
|
|
149
|
-
port: numberFlag(parsed, "port")
|
|
150
|
-
});
|
|
151
|
-
return await waitForever();
|
|
173
|
+
return await runBridgeStart(parsed);
|
|
152
174
|
}
|
|
153
175
|
if (area === "bridge" && action === "status") {
|
|
154
176
|
return output(parsed, await bridgeGet("/healthz"), renderBridgeStatus);
|
|
@@ -276,7 +298,18 @@ async function main(argv) {
|
|
|
276
298
|
const result = await pluginInstall(parsed);
|
|
277
299
|
return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
|
|
278
300
|
}
|
|
279
|
-
|
|
301
|
+
if (action === "update") {
|
|
302
|
+
if (parsed.command[2] === "check") {
|
|
303
|
+
const result = await pluginUpdateCheck(parsed);
|
|
304
|
+
return output(parsed, result, renderPluginUpdateCheck, result.ok ? 0 : 2);
|
|
305
|
+
}
|
|
306
|
+
if (["codex", "opencode", "all"].includes(parsed.command[2] ?? "")) {
|
|
307
|
+
const result = await pluginUpdate(parsed);
|
|
308
|
+
return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
|
|
309
|
+
}
|
|
310
|
+
return output(parsed, fail("invalid_args", [PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
|
|
311
|
+
}
|
|
312
|
+
return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE, PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
|
|
280
313
|
}
|
|
281
314
|
if (area === "auth" && action === "login") {
|
|
282
315
|
const result = await authLogin(parsed);
|
|
@@ -285,6 +318,31 @@ async function main(argv) {
|
|
|
285
318
|
if (area === "auth" && action === "status") {
|
|
286
319
|
return output(parsed, ok(await authStatus()), renderAuthStatus);
|
|
287
320
|
}
|
|
321
|
+
if (area === "auth" && action === "device") {
|
|
322
|
+
const deviceAction = parsed.command[2] ?? "status";
|
|
323
|
+
if (deviceAction === "status") {
|
|
324
|
+
const result = await authDeviceStatus(parsed);
|
|
325
|
+
return output(parsed, result, renderAuthDeviceStatus, result.ok ? 0 : 2);
|
|
326
|
+
}
|
|
327
|
+
if (deviceAction === "list") {
|
|
328
|
+
const result = await authDeviceList(parsed);
|
|
329
|
+
return output(parsed, result, renderAuthDeviceList, result.ok ? 0 : 2);
|
|
330
|
+
}
|
|
331
|
+
if (deviceAction === "revoke") {
|
|
332
|
+
const result = await authDeviceRevoke(parsed);
|
|
333
|
+
return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
|
|
334
|
+
}
|
|
335
|
+
if (deviceAction === "rename") {
|
|
336
|
+
const result = await authDeviceRename(parsed);
|
|
337
|
+
return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
|
|
338
|
+
}
|
|
339
|
+
return output(parsed, fail("invalid_args", [
|
|
340
|
+
AUTH_DEVICE_STATUS_USAGE,
|
|
341
|
+
AUTH_DEVICE_LIST_USAGE,
|
|
342
|
+
AUTH_DEVICE_REVOKE_USAGE,
|
|
343
|
+
AUTH_DEVICE_RENAME_USAGE
|
|
344
|
+
].join("\n")), undefined, 2);
|
|
345
|
+
}
|
|
288
346
|
if (area === "auth" && action === "logout") {
|
|
289
347
|
await rm(DEFAULT_AUTH_FILE, { force: true });
|
|
290
348
|
return output(parsed, ok(await authStatus()), renderAuthStatus);
|
|
@@ -364,6 +422,16 @@ async function main(argv) {
|
|
|
364
422
|
USAGE_EVENTS_USAGE
|
|
365
423
|
].join("\n")), undefined, 2);
|
|
366
424
|
}
|
|
425
|
+
if (area === "mcp") {
|
|
426
|
+
if (action === "log" || action === "tool-event") {
|
|
427
|
+
const body = mcpToolEventBody(parsed);
|
|
428
|
+
if (typeof body === "string") {
|
|
429
|
+
return output(parsed, fail("invalid_args", body), undefined, 2);
|
|
430
|
+
}
|
|
431
|
+
return output(parsed, await cloudPost("/v1/mcp/tool-events", body), renderMcpToolEvent);
|
|
432
|
+
}
|
|
433
|
+
return output(parsed, fail("invalid_args", MCP_TOOL_EVENT_USAGE), undefined, 2);
|
|
434
|
+
}
|
|
367
435
|
if (area === "billing") {
|
|
368
436
|
if (action === "statement") {
|
|
369
437
|
const request = billingStatementRequest(parsed);
|
|
@@ -503,12 +571,40 @@ async function main(argv) {
|
|
|
503
571
|
return output(parsed, fail("invalid_args", [IMAGE_BOOT_LOGO_COMPOSE_USAGE, IMAGE_DTB_COMPOSE_USAGE].join("\n")), undefined, 2);
|
|
504
572
|
}
|
|
505
573
|
if (area === "local") {
|
|
574
|
+
if (action === "toolchain" && parsed.command[2] === "list") {
|
|
575
|
+
const request = localToolchainListRequest(parsed);
|
|
576
|
+
if (typeof request === "string") {
|
|
577
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
578
|
+
}
|
|
579
|
+
return output(parsed, ok(await listLocalToolchainEnvironments(request)), renderLocalToolchainList);
|
|
580
|
+
}
|
|
581
|
+
if (action === "toolchain" && parsed.command[2] === "latest") {
|
|
582
|
+
const request = localToolchainLatestRequest(parsed);
|
|
583
|
+
if (typeof request === "string") {
|
|
584
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
585
|
+
}
|
|
586
|
+
return output(parsed, ok(await latestLocalToolchain(request)), renderLocalToolchainLatest);
|
|
587
|
+
}
|
|
588
|
+
if (action === "toolchain" && parsed.command[2] === "current") {
|
|
589
|
+
const request = localToolchainCurrentRequest(parsed);
|
|
590
|
+
if (typeof request === "string") {
|
|
591
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
592
|
+
}
|
|
593
|
+
return output(parsed, ok(await currentLocalToolchain(request.installRoot)), renderLocalToolchainCurrent);
|
|
594
|
+
}
|
|
595
|
+
if (action === "toolchain" && parsed.command[2] === "install") {
|
|
596
|
+
const request = localToolchainInstallRequest(parsed);
|
|
597
|
+
if (typeof request === "string") {
|
|
598
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
599
|
+
}
|
|
600
|
+
return output(parsed, ok(await installLocalToolchain(request)), renderLocalToolchainInstall);
|
|
601
|
+
}
|
|
506
602
|
if (action === "toolchain" && parsed.command[2] === "validate") {
|
|
507
603
|
const request = localToolchainValidateRequest(parsed);
|
|
508
604
|
if (typeof request === "string") {
|
|
509
605
|
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
510
606
|
}
|
|
511
|
-
return output(parsed, ok(await validateLocalToolchain(request
|
|
607
|
+
return output(parsed, ok(await validateLocalToolchain(request)), renderLocalToolchainValidation);
|
|
512
608
|
}
|
|
513
609
|
if (action === "compile" && parsed.command[2] === "taishanpi") {
|
|
514
610
|
const request = localCompileTaishanPiRequest(parsed, await authStatus());
|
|
@@ -525,6 +621,10 @@ async function main(argv) {
|
|
|
525
621
|
return output(parsed, ok(await buildTaishanPiQtSmoke(request)), renderLocalCompileResult);
|
|
526
622
|
}
|
|
527
623
|
return output(parsed, fail("invalid_args", [
|
|
624
|
+
LOCAL_TOOLCHAIN_LIST_USAGE,
|
|
625
|
+
LOCAL_TOOLCHAIN_LATEST_USAGE,
|
|
626
|
+
LOCAL_TOOLCHAIN_CURRENT_USAGE,
|
|
627
|
+
LOCAL_TOOLCHAIN_INSTALL_USAGE,
|
|
528
628
|
LOCAL_TOOLCHAIN_VALIDATE_USAGE,
|
|
529
629
|
LOCAL_COMPILE_TAISHANPI_USAGE,
|
|
530
630
|
LOCAL_BUILD_QT_SMOKE_USAGE
|
|
@@ -1099,11 +1199,11 @@ async function doctor() {
|
|
|
1099
1199
|
const bridgeHealth = await apiDoctorCheck("bridge_health", "Local Bridge health", `${bridgeBaseUrl}/healthz`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderHealthSummary, healthStatus);
|
|
1100
1200
|
checks.push(bridgeHealth);
|
|
1101
1201
|
if (isUsableDoctorCheck(bridgeHealth)) {
|
|
1102
|
-
checks.push(await apiDoctorCheck("device_scan", "Device
|
|
1202
|
+
checks.push(await apiDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
|
|
1103
1203
|
checks.push(await apiDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDebugToolScanSummary, warningIfWarnings));
|
|
1104
1204
|
}
|
|
1105
1205
|
else {
|
|
1106
|
-
checks.push(dependentDoctorCheck("device_scan", "Device
|
|
1206
|
+
checks.push(dependentDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "Device inventory requires a reachable Local Bridge."));
|
|
1107
1207
|
checks.push(dependentDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "Debug tool scan requires a reachable Local Bridge."));
|
|
1108
1208
|
}
|
|
1109
1209
|
checks.push(await apiDoctorCheck("cloud_api_health", "Cloud API health", `${cloudBaseUrl}/healthz`, "cloud_api_unreachable", `Cloud API is unreachable at ${cloudBaseUrl}.`, renderHealthSummary, healthStatus));
|
|
@@ -1162,7 +1262,8 @@ function authDoctorCheck(status) {
|
|
|
1162
1262
|
: {
|
|
1163
1263
|
code: "auth_not_ready",
|
|
1164
1264
|
message: "No CLI auth token is configured.",
|
|
1165
|
-
remediation:
|
|
1265
|
+
remediation: cloudAuthSetupRemediation(),
|
|
1266
|
+
details: cloudAuthSetupDetails()
|
|
1166
1267
|
}
|
|
1167
1268
|
};
|
|
1168
1269
|
}
|
|
@@ -1295,7 +1396,7 @@ function warningIfWarnings(data) {
|
|
|
1295
1396
|
}
|
|
1296
1397
|
function renderDeviceScanSummary(result) {
|
|
1297
1398
|
const warningText = result.warnings?.length ? ` ${result.warnings.length} warning(s).` : "";
|
|
1298
|
-
return `Device
|
|
1399
|
+
return `Device inventory snapshot: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
|
|
1299
1400
|
}
|
|
1300
1401
|
function renderDebugToolScanSummary(result) {
|
|
1301
1402
|
const available = result.tools.filter((tool) => tool.available).length;
|
|
@@ -1313,16 +1414,142 @@ function isApiResponse(value) {
|
|
|
1313
1414
|
return isJsonObject(error) && typeof error.code === "string" && typeof error.message === "string";
|
|
1314
1415
|
}
|
|
1315
1416
|
async function bridgeGet(path) {
|
|
1316
|
-
|
|
1317
|
-
return await response.json();
|
|
1417
|
+
return await bridgeRequest("GET", path);
|
|
1318
1418
|
}
|
|
1319
1419
|
async function bridgePost(path, body) {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1420
|
+
return await bridgeRequest("POST", path, body);
|
|
1421
|
+
}
|
|
1422
|
+
async function bridgeRequest(method, path, body) {
|
|
1423
|
+
const bodyText = body === undefined ? "" : JSON.stringify(body);
|
|
1424
|
+
const makeRequest = async () => {
|
|
1425
|
+
const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
|
|
1426
|
+
method,
|
|
1427
|
+
headers: bridgeHeaders(method, path, method === "POST" ? bodyText : "", method === "POST" ? { "content-type": "application/json" } : {}),
|
|
1428
|
+
body: method === "POST" ? bodyText : undefined
|
|
1429
|
+
});
|
|
1430
|
+
return await response.json();
|
|
1431
|
+
};
|
|
1432
|
+
try {
|
|
1433
|
+
return await makeRequest();
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
if (!isBridgeConnectionFailure(error)) {
|
|
1437
|
+
throw error;
|
|
1438
|
+
}
|
|
1439
|
+
const started = await ensureBridgeStartedForRequest();
|
|
1440
|
+
if (!started.ok) {
|
|
1441
|
+
return started;
|
|
1442
|
+
}
|
|
1443
|
+
return await makeRequest();
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function bridgeHeaders(method, path, bodyText, base = {}) {
|
|
1447
|
+
const token = process.env.EMBED_BRIDGE_TOKEN?.trim();
|
|
1448
|
+
if (!token) {
|
|
1449
|
+
return base;
|
|
1450
|
+
}
|
|
1451
|
+
const headers = {
|
|
1452
|
+
...base,
|
|
1453
|
+
authorization: `Bearer ${token}`
|
|
1454
|
+
};
|
|
1455
|
+
addBridgeRequestSignature(headers, method, path, bodyText, token);
|
|
1456
|
+
return headers;
|
|
1457
|
+
}
|
|
1458
|
+
function addBridgeRequestSignature(headers, method, pathWithQuery, bodyText, token) {
|
|
1459
|
+
if (process.env.EMBED_BRIDGE_SIGNING === "0") {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
1463
|
+
const nonce = randomBytes(16).toString("hex");
|
|
1464
|
+
const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
|
|
1465
|
+
const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
1466
|
+
const canonical = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
|
|
1467
|
+
headers["x-embed-key-id"] = keyId;
|
|
1468
|
+
headers["x-embed-timestamp"] = timestamp;
|
|
1469
|
+
headers["x-embed-nonce"] = nonce;
|
|
1470
|
+
headers["x-embed-body-sha256"] = bodySha256;
|
|
1471
|
+
headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
|
|
1472
|
+
}
|
|
1473
|
+
function isBridgeConnectionFailure(error) {
|
|
1474
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1475
|
+
return message.includes("fetch failed") ||
|
|
1476
|
+
message.includes("ECONNREFUSED") ||
|
|
1477
|
+
message.includes("ECONNRESET") ||
|
|
1478
|
+
message.includes("UND_ERR_SOCKET");
|
|
1479
|
+
}
|
|
1480
|
+
async function ensureBridgeStartedForRequest() {
|
|
1481
|
+
if (process.env.EMBED_BRIDGE_AUTO_START === "0") {
|
|
1482
|
+
return fail("bridge_unavailable", `embed-local-bridge is not running at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1483
|
+
remediation: `Start it with: embed bridge start`
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
let bridgeURL;
|
|
1487
|
+
try {
|
|
1488
|
+
bridgeURL = new URL(DEFAULT_BRIDGE_URL);
|
|
1489
|
+
}
|
|
1490
|
+
catch {
|
|
1491
|
+
return fail("bridge_url_invalid", `EMBED_BRIDGE_URL is not a valid URL: ${DEFAULT_BRIDGE_URL}`);
|
|
1492
|
+
}
|
|
1493
|
+
if (!isLocalBridgeURL(bridgeURL)) {
|
|
1494
|
+
return fail("bridge_unavailable", `embed-local-bridge is not reachable at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1495
|
+
remediation: `Start the bridge for that host, or set EMBED_BRIDGE_URL to a local bridge URL.`
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
const launcher = await resolveBridgeLauncher();
|
|
1499
|
+
const host = bridgeURL.hostname === "::1" ? "::1" : bridgeURL.hostname || "127.0.0.1";
|
|
1500
|
+
const port = bridgeURL.port || "18083";
|
|
1501
|
+
const env = {
|
|
1502
|
+
...process.env,
|
|
1503
|
+
EMBED_BRIDGE_HOST: host,
|
|
1504
|
+
EMBED_BRIDGE_PORT: port
|
|
1505
|
+
};
|
|
1506
|
+
const child = spawn(launcher.command, [...launcher.args, "--host", host, "--port", port], {
|
|
1507
|
+
cwd: process.cwd(),
|
|
1508
|
+
detached: true,
|
|
1509
|
+
stdio: "ignore",
|
|
1510
|
+
env
|
|
1511
|
+
});
|
|
1512
|
+
child.unref();
|
|
1513
|
+
const ready = await waitForBridgeHealth(bridgeURL, 8000);
|
|
1514
|
+
if (!ready.ok) {
|
|
1515
|
+
return ready;
|
|
1516
|
+
}
|
|
1517
|
+
return ok({
|
|
1518
|
+
started: true,
|
|
1519
|
+
bridge_url: DEFAULT_BRIDGE_URL,
|
|
1520
|
+
command: launcher.command
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
function isLocalBridgeURL(url) {
|
|
1524
|
+
const host = url.hostname.toLowerCase();
|
|
1525
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1526
|
+
}
|
|
1527
|
+
async function waitForBridgeHealth(bridgeURL, timeoutMs) {
|
|
1528
|
+
const deadline = Date.now() + timeoutMs;
|
|
1529
|
+
let lastError = "";
|
|
1530
|
+
while (Date.now() < deadline) {
|
|
1531
|
+
try {
|
|
1532
|
+
const response = await fetch(new URL("/healthz", bridgeURL), {
|
|
1533
|
+
headers: bridgeHeaders("GET", "/healthz", "")
|
|
1534
|
+
});
|
|
1535
|
+
const parsed = await response.json();
|
|
1536
|
+
if (parsed.ok) {
|
|
1537
|
+
return parsed;
|
|
1538
|
+
}
|
|
1539
|
+
lastError = parsed.error?.message ?? `HTTP ${response.status}`;
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
1543
|
+
}
|
|
1544
|
+
await delay(100);
|
|
1545
|
+
}
|
|
1546
|
+
return fail("bridge_start_failed", `embed-local-bridge did not become healthy at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1547
|
+
remediation: `Run embed bridge start in a separate terminal and retry.`,
|
|
1548
|
+
details: { last_error: lastError }
|
|
1324
1549
|
});
|
|
1325
|
-
|
|
1550
|
+
}
|
|
1551
|
+
function delay(ms) {
|
|
1552
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
1326
1553
|
}
|
|
1327
1554
|
async function cloudGet(path) {
|
|
1328
1555
|
return await cloudRequest("GET", path);
|
|
@@ -1333,16 +1560,23 @@ async function cloudPost(path, body) {
|
|
|
1333
1560
|
async function cloudDownloadArtifact(artifactId, outputPath) {
|
|
1334
1561
|
try {
|
|
1335
1562
|
const headers = {};
|
|
1336
|
-
const
|
|
1337
|
-
if (token) {
|
|
1338
|
-
|
|
1563
|
+
const auth = await cloudAuthConfig();
|
|
1564
|
+
if (auth.token) {
|
|
1565
|
+
if (auth.device) {
|
|
1566
|
+
const integrity = await validateLocalDeviceIntegrity(auth.device);
|
|
1567
|
+
if (!integrity.ok) {
|
|
1568
|
+
return integrity;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
1572
|
+
addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", auth.token, auth.device);
|
|
1339
1573
|
}
|
|
1340
1574
|
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
|
|
1341
1575
|
headers: Object.keys(headers).length > 0 ? headers : undefined
|
|
1342
1576
|
});
|
|
1343
1577
|
if (!response.ok) {
|
|
1344
1578
|
const parsed = await parseErrorResponse(response);
|
|
1345
|
-
return parsed
|
|
1579
|
+
return parsed ? enrichCloudAuthFailure(parsed, Boolean(auth.token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
|
|
1346
1580
|
}
|
|
1347
1581
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
1348
1582
|
const expectedSha256 = response.headers.get("x-embed-artifact-sha256")?.trim();
|
|
@@ -1371,19 +1605,28 @@ async function cloudDownloadArtifact(artifactId, outputPath) {
|
|
|
1371
1605
|
async function cloudRequest(method, path, body) {
|
|
1372
1606
|
try {
|
|
1373
1607
|
const headers = {};
|
|
1374
|
-
|
|
1608
|
+
const bodyText = body === undefined ? "" : JSON.stringify(body);
|
|
1609
|
+
if (bodyText) {
|
|
1375
1610
|
headers["content-type"] = "application/json";
|
|
1376
1611
|
}
|
|
1377
|
-
const
|
|
1378
|
-
if (token) {
|
|
1379
|
-
|
|
1612
|
+
const auth = await cloudAuthConfig();
|
|
1613
|
+
if (auth.token) {
|
|
1614
|
+
if (auth.device) {
|
|
1615
|
+
const integrity = await validateLocalDeviceIntegrity(auth.device);
|
|
1616
|
+
if (!integrity.ok) {
|
|
1617
|
+
return integrity;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
1621
|
+
addCloudRequestSignature(headers, method, path, bodyText, auth.token, auth.device);
|
|
1380
1622
|
}
|
|
1381
1623
|
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
|
|
1382
1624
|
method,
|
|
1383
1625
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
1384
|
-
body: body === undefined ? undefined :
|
|
1626
|
+
body: body === undefined ? undefined : bodyText
|
|
1385
1627
|
});
|
|
1386
|
-
|
|
1628
|
+
const parsed = await response.json();
|
|
1629
|
+
return enrichCloudAuthFailure(parsed, Boolean(auth.token));
|
|
1387
1630
|
}
|
|
1388
1631
|
catch (error) {
|
|
1389
1632
|
return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
|
|
@@ -1391,6 +1634,129 @@ async function cloudRequest(method, path, body) {
|
|
|
1391
1634
|
});
|
|
1392
1635
|
}
|
|
1393
1636
|
}
|
|
1637
|
+
async function validateLocalDeviceIntegrity(device) {
|
|
1638
|
+
const current = await localHardwareFingerprint();
|
|
1639
|
+
if (current.fingerprint_hash === device.fingerprint_hash) {
|
|
1640
|
+
return ok(undefined);
|
|
1641
|
+
}
|
|
1642
|
+
return fail("tool_integrity_check_failed", TOOL_INTEGRITY_RELOGIN_MESSAGE, {
|
|
1643
|
+
remediation: [
|
|
1644
|
+
"当前 Embed Labs CLI/插件配置绑定的电脑与本机硬件唯一码不一致。",
|
|
1645
|
+
TOOL_INTEGRITY_RELOGIN_MESSAGE,
|
|
1646
|
+
"如果账号设备数量已达上限,请先在原电脑或用户后台撤销旧设备。"
|
|
1647
|
+
].join("\n"),
|
|
1648
|
+
details: {
|
|
1649
|
+
expected_fingerprint_hash: device.fingerprint_hash,
|
|
1650
|
+
current_fingerprint_hash: current.fingerprint_hash,
|
|
1651
|
+
platform: current.platform,
|
|
1652
|
+
arch: current.arch,
|
|
1653
|
+
fingerprint_source: current.source
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
function enrichCloudAuthFailure(response, hadToken) {
|
|
1658
|
+
if (response.ok) {
|
|
1659
|
+
return response;
|
|
1660
|
+
}
|
|
1661
|
+
if (response.error.code.startsWith("device_") || response.error.code.startsWith("request_signature_")) {
|
|
1662
|
+
return fail(response.error.code, response.error.message, {
|
|
1663
|
+
remediation: [
|
|
1664
|
+
"This computer is not fully registered for the configured Embed Labs API Token.",
|
|
1665
|
+
"Run: embedlabs auth login --token <your_token>",
|
|
1666
|
+
"Then verify with: embedlabs auth device status",
|
|
1667
|
+
"If the account already has too many devices, revoke one with: embedlabs auth device revoke <device_id>"
|
|
1668
|
+
].join("\n"),
|
|
1669
|
+
details: response.error.details
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
if (response.error.code !== "unauthorized") {
|
|
1673
|
+
return response;
|
|
1674
|
+
}
|
|
1675
|
+
if (!hadToken) {
|
|
1676
|
+
return fail("auth_token_missing", "Embed Labs API Token is not configured. Register or sign in, create an API Token, then configure it locally before using cloud and plugin services.", {
|
|
1677
|
+
remediation: cloudAuthSetupRemediation(),
|
|
1678
|
+
details: cloudAuthSetupDetails()
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
return fail("auth_token_rejected", "The configured Embed Labs API Token was rejected by the server. Recreate or copy a fresh token from the dashboard and sign in again.", {
|
|
1682
|
+
remediation: cloudAuthSetupRemediation(),
|
|
1683
|
+
details: cloudAuthSetupDetails()
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
function cloudAuthSetupRemediation() {
|
|
1687
|
+
return [
|
|
1688
|
+
`1. Open ${DEFAULT_DASHBOARD_URL} and register or sign in.`,
|
|
1689
|
+
"2. Create or copy your Embed Labs API Token from the user dashboard.",
|
|
1690
|
+
"3. Run: embedlabs auth login --token <your_token>",
|
|
1691
|
+
"4. For automation, set: EMBED_API_TOKEN=<your_token>",
|
|
1692
|
+
"5. Verify with: embedlabs auth status"
|
|
1693
|
+
].join("\n");
|
|
1694
|
+
}
|
|
1695
|
+
function cloudAuthSetupDetails() {
|
|
1696
|
+
return {
|
|
1697
|
+
dashboard_url: DEFAULT_DASHBOARD_URL,
|
|
1698
|
+
login_command: "embedlabs auth login --token <your_token>",
|
|
1699
|
+
env_var: "EMBED_API_TOKEN",
|
|
1700
|
+
auth_status_command: "embedlabs auth status",
|
|
1701
|
+
auth_file: DEFAULT_AUTH_FILE
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token, device) {
|
|
1705
|
+
if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
1709
|
+
const nonce = randomBytes(16).toString("hex");
|
|
1710
|
+
const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
|
|
1711
|
+
const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
1712
|
+
const canonical = device
|
|
1713
|
+
? cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, EMBED_CLIENT_NAME, EMBED_CLIENT_VERSION)
|
|
1714
|
+
: cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
|
|
1715
|
+
headers["x-embed-key-id"] = keyId;
|
|
1716
|
+
headers["x-embed-timestamp"] = timestamp;
|
|
1717
|
+
headers["x-embed-nonce"] = nonce;
|
|
1718
|
+
headers["x-embed-body-sha256"] = bodySha256;
|
|
1719
|
+
if (device) {
|
|
1720
|
+
headers["x-embed-signature-version"] = "v2";
|
|
1721
|
+
headers["x-embed-device-id"] = device.device_id;
|
|
1722
|
+
headers["x-embed-device-fingerprint-sha256"] = device.fingerprint_hash;
|
|
1723
|
+
headers["x-embed-client-name"] = EMBED_CLIENT_NAME;
|
|
1724
|
+
headers["x-embed-client-version"] = EMBED_CLIENT_VERSION;
|
|
1725
|
+
headers["x-embed-device-signature"] = signCrypto(null, Buffer.from(canonical), device.private_key_pem).toString("base64url");
|
|
1726
|
+
}
|
|
1727
|
+
headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
|
|
1728
|
+
}
|
|
1729
|
+
function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
|
|
1730
|
+
return [
|
|
1731
|
+
method.toUpperCase(),
|
|
1732
|
+
normalizeCloudPathForSignature(pathWithQuery),
|
|
1733
|
+
timestamp,
|
|
1734
|
+
nonce,
|
|
1735
|
+
bodySha256
|
|
1736
|
+
].join("\n");
|
|
1737
|
+
}
|
|
1738
|
+
function cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, clientName, clientVersion) {
|
|
1739
|
+
return [
|
|
1740
|
+
method.toUpperCase(),
|
|
1741
|
+
normalizeCloudPathForSignature(pathWithQuery),
|
|
1742
|
+
timestamp,
|
|
1743
|
+
nonce,
|
|
1744
|
+
bodySha256,
|
|
1745
|
+
device.device_id,
|
|
1746
|
+
device.fingerprint_hash,
|
|
1747
|
+
clientName,
|
|
1748
|
+
clientVersion
|
|
1749
|
+
].join("\n");
|
|
1750
|
+
}
|
|
1751
|
+
function normalizeCloudPathForSignature(pathWithQuery) {
|
|
1752
|
+
try {
|
|
1753
|
+
const parsed = new URL(pathWithQuery, "http://embed.local");
|
|
1754
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
1755
|
+
}
|
|
1756
|
+
catch {
|
|
1757
|
+
return pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1394
1760
|
async function pluginList(parsed) {
|
|
1395
1761
|
const releaseDir = stringFlag(parsed, "release-dir");
|
|
1396
1762
|
const manifest = releaseDir ? await readPluginReleaseManifest(releaseDir) : undefined;
|
|
@@ -1490,13 +1856,112 @@ async function pluginInstall(parsed) {
|
|
|
1490
1856
|
await rm(tempDir, { recursive: true, force: true });
|
|
1491
1857
|
}
|
|
1492
1858
|
}
|
|
1859
|
+
async function pluginUpdateCheck(parsed) {
|
|
1860
|
+
const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
|
|
1861
|
+
if (unknownFlag) {
|
|
1862
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
|
|
1863
|
+
}
|
|
1864
|
+
const unexpected = parsed.command.slice(3);
|
|
1865
|
+
if (unexpected.length > 0) {
|
|
1866
|
+
return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
|
|
1867
|
+
}
|
|
1868
|
+
const remoteManifest = await fetchRemotePluginManifest(parsed);
|
|
1869
|
+
if (!remoteManifest.ok) {
|
|
1870
|
+
return remoteManifest;
|
|
1871
|
+
}
|
|
1872
|
+
const manifest = remoteManifest.data;
|
|
1873
|
+
const codexPackage = manifest.packages?.find((item) => item.id === "codex-embed-labs");
|
|
1874
|
+
const opencodePackage = manifest.packages?.find((item) => item.id === "opencode-embed-labs");
|
|
1875
|
+
const codexTarget = join(codexPluginTargetRoot(parsed, true), CODEX_PLUGIN_NAME);
|
|
1876
|
+
const openCodeTarget = openCodePluginTargetRoot(parsed, true);
|
|
1877
|
+
return ok({
|
|
1878
|
+
release_url: pluginReleaseBaseUrl(parsed),
|
|
1879
|
+
latest_version: manifest.version,
|
|
1880
|
+
release_notes: normalizedReleaseNotes(manifest.release_notes),
|
|
1881
|
+
plugins: [
|
|
1882
|
+
await pluginUpdateItem({
|
|
1883
|
+
id: "codex",
|
|
1884
|
+
displayName: "Embed Labs Codex plugin",
|
|
1885
|
+
targetPath: codexTarget,
|
|
1886
|
+
installedVersion: await installedCodexPluginVersion(codexTarget),
|
|
1887
|
+
latestVersion: codexPackage?.version ?? manifest.version,
|
|
1888
|
+
releaseFile: codexPackage?.file,
|
|
1889
|
+
updateCommand: "embedlabs plugin update codex"
|
|
1890
|
+
}),
|
|
1891
|
+
await pluginUpdateItem({
|
|
1892
|
+
id: "opencode",
|
|
1893
|
+
displayName: "Embed Labs OpenCode plugin",
|
|
1894
|
+
targetPath: openCodeTarget,
|
|
1895
|
+
installedVersion: await installedOpenCodePluginVersion(openCodeTarget),
|
|
1896
|
+
latestVersion: opencodePackage?.version ?? manifest.version,
|
|
1897
|
+
releaseFile: opencodePackage?.file,
|
|
1898
|
+
updateCommand: "embedlabs plugin update opencode"
|
|
1899
|
+
})
|
|
1900
|
+
]
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
function normalizedReleaseNotes(notes) {
|
|
1904
|
+
if (!Array.isArray(notes)) {
|
|
1905
|
+
return [];
|
|
1906
|
+
}
|
|
1907
|
+
return notes
|
|
1908
|
+
.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
1909
|
+
.map((item) => item.trim());
|
|
1910
|
+
}
|
|
1911
|
+
async function pluginUpdate(parsed) {
|
|
1912
|
+
const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
|
|
1913
|
+
if (unknownFlag) {
|
|
1914
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_USAGE}`);
|
|
1915
|
+
}
|
|
1916
|
+
const target = parsed.command[2];
|
|
1917
|
+
if (!target || !["codex", "opencode", "all"].includes(target)) {
|
|
1918
|
+
return fail("invalid_args", PLUGIN_UPDATE_USAGE);
|
|
1919
|
+
}
|
|
1920
|
+
const unexpected = parsed.command.slice(3);
|
|
1921
|
+
if (unexpected.length > 0) {
|
|
1922
|
+
return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_USAGE}`);
|
|
1923
|
+
}
|
|
1924
|
+
const installParsed = {
|
|
1925
|
+
...parsed,
|
|
1926
|
+
command: ["plugin", "install", target],
|
|
1927
|
+
flags: { ...parsed.flags, force: true }
|
|
1928
|
+
};
|
|
1929
|
+
return await pluginInstall(installParsed);
|
|
1930
|
+
}
|
|
1931
|
+
async function pluginUpdateItem(input) {
|
|
1932
|
+
const installed = !!input.installedVersion;
|
|
1933
|
+
const updateAvailable = !!input.latestVersion && input.installedVersion !== input.latestVersion;
|
|
1934
|
+
const notes = [];
|
|
1935
|
+
if (!installed) {
|
|
1936
|
+
notes.push("Plugin is not installed in the selected target.");
|
|
1937
|
+
}
|
|
1938
|
+
else if (updateAvailable) {
|
|
1939
|
+
notes.push("A newer plugin release is available. Run the update command, then restart Codex/OpenCode.");
|
|
1940
|
+
}
|
|
1941
|
+
else {
|
|
1942
|
+
notes.push("Plugin is up to date for the selected release channel.");
|
|
1943
|
+
}
|
|
1944
|
+
return {
|
|
1945
|
+
id: input.id,
|
|
1946
|
+
display_name: input.displayName,
|
|
1947
|
+
installed,
|
|
1948
|
+
installed_version: input.installedVersion,
|
|
1949
|
+
latest_version: input.latestVersion,
|
|
1950
|
+
update_available: updateAvailable,
|
|
1951
|
+
target_path: input.targetPath,
|
|
1952
|
+
release_file: input.releaseFile,
|
|
1953
|
+
update_command: input.updateCommand,
|
|
1954
|
+
notes
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1493
1957
|
async function installCodexPlugin(parsed, context) {
|
|
1494
1958
|
const source = await resolveCodexPluginSource(context);
|
|
1495
1959
|
if (!source.ok) {
|
|
1496
1960
|
return source;
|
|
1497
1961
|
}
|
|
1498
1962
|
const targetRoot = codexPluginTargetRoot(parsed, context.installingAll);
|
|
1499
|
-
const targetPath = join(targetRoot,
|
|
1963
|
+
const targetPath = join(targetRoot, CODEX_PLUGIN_NAME);
|
|
1964
|
+
const legacyCleanup = await cleanupLegacyCodexPluginRemnants(targetRoot);
|
|
1500
1965
|
if (await pathExists(targetPath) && !booleanFlag(parsed, "force")) {
|
|
1501
1966
|
return fail("plugin_already_installed", `Codex plugin already exists at ${targetPath}.`, {
|
|
1502
1967
|
remediation: "Pass --force to replace it, or pass --codex-target/--target to install into a different directory."
|
|
@@ -1506,16 +1971,24 @@ async function installCodexPlugin(parsed, context) {
|
|
|
1506
1971
|
await mkdir(targetRoot, { recursive: true });
|
|
1507
1972
|
await cp(source.data.sourcePath, targetPath, { recursive: true });
|
|
1508
1973
|
const mcpRegistration = await maybeRegisterCodexMcp(parsed, targetRoot, targetPath);
|
|
1974
|
+
const marketplaceRegistration = await maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath);
|
|
1509
1975
|
return ok({
|
|
1510
1976
|
id: "codex",
|
|
1511
1977
|
target_path: targetPath,
|
|
1512
1978
|
source: source.data.sourceLabel,
|
|
1513
1979
|
version: source.data.version,
|
|
1514
1980
|
command_hint: mcpRegistration.registered
|
|
1515
|
-
?
|
|
1981
|
+
? (marketplaceRegistration.registered
|
|
1982
|
+
? "Codex MCP and plugin marketplace entry were registered. Fully restart Codex to reload @Embed Labs."
|
|
1983
|
+
: "Codex MCP was registered. Start a new Codex session to reload tools.")
|
|
1516
1984
|
: mcpRegistration.hint,
|
|
1985
|
+
warning: legacyCodexCleanupWarning(legacyCleanup),
|
|
1517
1986
|
mcp_registered: mcpRegistration.registered,
|
|
1518
|
-
mcp_warning: mcpRegistration.warning
|
|
1987
|
+
mcp_warning: mcpRegistration.warning,
|
|
1988
|
+
marketplace_registered: marketplaceRegistration.registered,
|
|
1989
|
+
marketplace_path: marketplaceRegistration.marketplacePath,
|
|
1990
|
+
marketplace_warning: marketplaceRegistration.warning,
|
|
1991
|
+
cleanup: legacyCleanup
|
|
1519
1992
|
});
|
|
1520
1993
|
}
|
|
1521
1994
|
async function installOpenCodePlugin(parsed, context) {
|
|
@@ -1524,8 +1997,10 @@ async function installOpenCodePlugin(parsed, context) {
|
|
|
1524
1997
|
return source;
|
|
1525
1998
|
}
|
|
1526
1999
|
const targetRoot = openCodePluginTargetRoot(parsed, context.installingAll);
|
|
1527
|
-
const
|
|
1528
|
-
|
|
2000
|
+
const globalInstall = isGlobalOpenCodeRoot(targetRoot);
|
|
2001
|
+
const wrapperPath = join(targetRoot, "plugins", "embed-labs.js");
|
|
2002
|
+
const legacyCleanup = await cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall);
|
|
2003
|
+
if (!globalInstall && await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
|
|
1529
2004
|
return fail("plugin_already_installed", `OpenCode plugin wrapper already exists at ${wrapperPath}.`, {
|
|
1530
2005
|
remediation: "Pass --force to replace it, or pass --opencode-target/--target to install into a different directory."
|
|
1531
2006
|
});
|
|
@@ -1551,15 +2026,25 @@ async function installOpenCodePlugin(parsed, context) {
|
|
|
1551
2026
|
});
|
|
1552
2027
|
}
|
|
1553
2028
|
await ensureOpenCodeInstallPackageJson(targetRoot);
|
|
1554
|
-
|
|
2029
|
+
if (globalInstall) {
|
|
2030
|
+
await rm(wrapperPath, { force: true });
|
|
2031
|
+
legacyCleanup.legacy_removed_config_entries?.push(...await ensureOpenCodeGlobalPluginConfig());
|
|
2032
|
+
}
|
|
2033
|
+
else {
|
|
2034
|
+
await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
|
|
2035
|
+
}
|
|
1555
2036
|
const duplicateWarning = await openCodeDuplicatePluginWarning(targetRoot);
|
|
2037
|
+
const cleanupWarning = legacyOpenCodeCleanupWarning(legacyCleanup);
|
|
1556
2038
|
return ok({
|
|
1557
2039
|
id: "opencode",
|
|
1558
2040
|
target_path: targetRoot,
|
|
1559
2041
|
source: source.data.sourceLabel,
|
|
1560
2042
|
version: source.data.version,
|
|
1561
|
-
command_hint:
|
|
1562
|
-
|
|
2043
|
+
command_hint: globalInstall
|
|
2044
|
+
? "Restart OpenCode so the global embed-labs package plugin is reloaded."
|
|
2045
|
+
: "Start OpenCode from the project containing this .opencode directory.",
|
|
2046
|
+
warning: combineWarnings(cleanupWarning, duplicateWarning),
|
|
2047
|
+
cleanup: legacyCleanup
|
|
1563
2048
|
});
|
|
1564
2049
|
}
|
|
1565
2050
|
async function resolveCodexPluginSource(context) {
|
|
@@ -1773,7 +2258,458 @@ function openCodePluginTargetRoot(parsed, installingAll) {
|
|
|
1773
2258
|
return resolve(target ?? defaultOpenCodeRoot());
|
|
1774
2259
|
}
|
|
1775
2260
|
function defaultCodexPluginRoot() {
|
|
1776
|
-
return join(
|
|
2261
|
+
return join(defaultCodexHome(), "plugins");
|
|
2262
|
+
}
|
|
2263
|
+
function defaultCodexHome() {
|
|
2264
|
+
return resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
2265
|
+
}
|
|
2266
|
+
function codexConfigPath() {
|
|
2267
|
+
return join(defaultCodexHome(), "config.toml");
|
|
2268
|
+
}
|
|
2269
|
+
async function cleanupLegacyCodexPluginRemnants(targetRoot) {
|
|
2270
|
+
const removedPaths = [];
|
|
2271
|
+
const removedConfigTables = [];
|
|
2272
|
+
const warnings = [];
|
|
2273
|
+
const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
|
|
2274
|
+
const legacyPaths = [
|
|
2275
|
+
join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME)
|
|
2276
|
+
];
|
|
2277
|
+
for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
|
|
2278
|
+
legacyPaths.push(join(targetRoot, "cache", marketplaceName, CODEX_PLUGIN_NAME));
|
|
2279
|
+
}
|
|
2280
|
+
for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
|
|
2281
|
+
legacyPaths.push(join(targetRoot, pluginName));
|
|
2282
|
+
legacyPaths.push(join(targetRoot, "cache", pluginName));
|
|
2283
|
+
for (const marketplaceName of [CODEX_MARKETPLACE_NAME, ...LEGACY_CODEX_MARKETPLACE_NAMES]) {
|
|
2284
|
+
legacyPaths.push(join(targetRoot, "cache", marketplaceName, pluginName));
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
|
|
2288
|
+
if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
|
|
2289
|
+
legacyPaths.push(join(defaultCodexHome(), ".tmp", "plugins"), join(defaultCodexHome(), ".tmp", "plugins.sha"), join(defaultCodexHome(), "ambient-suggestions"), join(defaultCodexHome(), "skills", "dbt-agent"), join(defaultCodexHome(), "skills", "development-board-toolchain-dev"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-live-board-ops"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-platform-plugin-maintenance"));
|
|
2290
|
+
legacyPaths.push(...legacyCodexLocalMarketplacePaths(), ...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...await discoverLegacyCodexProjectMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
|
|
2291
|
+
}
|
|
2292
|
+
for (const candidate of legacyPaths) {
|
|
2293
|
+
try {
|
|
2294
|
+
if (await pathExists(candidate)) {
|
|
2295
|
+
await rm(candidate, { recursive: true, force: true });
|
|
2296
|
+
removedPaths.push(candidate);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
catch (error) {
|
|
2300
|
+
warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
|
|
2304
|
+
const configPath = codexConfigPath();
|
|
2305
|
+
try {
|
|
2306
|
+
if (await pathExists(configPath)) {
|
|
2307
|
+
const current = await readFile(configPath, "utf8");
|
|
2308
|
+
const updated = removeLegacyCodexConfigTables(current);
|
|
2309
|
+
if (updated.text !== current) {
|
|
2310
|
+
await writeFile(configPath, updated.text, "utf8");
|
|
2311
|
+
}
|
|
2312
|
+
removedConfigTables.push(...updated.removedTables);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
catch (error) {
|
|
2316
|
+
warnings.push(`Could not update ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
const removedHistoryEntries = await cleanupLegacyCodexTextState(warnings);
|
|
2320
|
+
return {
|
|
2321
|
+
legacy_removed_paths: Array.from(new Set(removedPaths)),
|
|
2322
|
+
legacy_removed_config_tables: removedConfigTables,
|
|
2323
|
+
legacy_removed_history_entries: removedHistoryEntries,
|
|
2324
|
+
legacy_stopped_processes: stoppedProcesses,
|
|
2325
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
async function stopLegacyCodexPluginProcesses(warnings) {
|
|
2329
|
+
if (process.platform === "win32")
|
|
2330
|
+
return 0;
|
|
2331
|
+
try {
|
|
2332
|
+
const ps = await runLocalProcess("ps", ["-axo", "pid=,command="]);
|
|
2333
|
+
if (ps.code !== 0)
|
|
2334
|
+
return 0;
|
|
2335
|
+
let stopped = 0;
|
|
2336
|
+
for (const line of ps.stdout.split("\n")) {
|
|
2337
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
2338
|
+
if (!match)
|
|
2339
|
+
continue;
|
|
2340
|
+
const pid = Number(match[1]);
|
|
2341
|
+
const command = match[2] || "";
|
|
2342
|
+
if (!isLegacyCodexPluginProcess(command))
|
|
2343
|
+
continue;
|
|
2344
|
+
try {
|
|
2345
|
+
process.kill(pid, "SIGTERM");
|
|
2346
|
+
stopped += 1;
|
|
2347
|
+
}
|
|
2348
|
+
catch (error) {
|
|
2349
|
+
warnings.push(`Could not stop legacy Codex plugin process ${pid}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
return stopped;
|
|
2353
|
+
}
|
|
2354
|
+
catch (error) {
|
|
2355
|
+
warnings.push(`Could not scan legacy Codex plugin processes: ${error instanceof Error ? error.message : String(error)}`);
|
|
2356
|
+
return 0;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
function isLegacyCodexPluginProcess(command) {
|
|
2360
|
+
const trimmed = command.trim();
|
|
2361
|
+
return /^\/.*\/dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
|
|
2362
|
+
|| /^dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
|
|
2363
|
+
|| legacyDevelopmentBoardRuntimeProcessPatterns().some((pattern) => pattern.test(trimmed));
|
|
2364
|
+
}
|
|
2365
|
+
function legacyDevelopmentBoardRuntimeProcessPatterns() {
|
|
2366
|
+
const home = escapeRegExp(homedir());
|
|
2367
|
+
return [
|
|
2368
|
+
new RegExp(`^${home}/Library/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
|
|
2369
|
+
new RegExp(`^${home}/Library/Application Support/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
|
|
2370
|
+
new RegExp(`^${home}/Library/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
|
|
2371
|
+
new RegExp(`^${home}/Library/Application Support/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
|
|
2372
|
+
new RegExp(`^${home}/.*?/DBT-Agent\\.app/Contents/MacOS/DBT-Agent(?:\\s|$)`)
|
|
2373
|
+
];
|
|
2374
|
+
}
|
|
2375
|
+
function legacyDevelopmentBoardRuntimePluginPaths() {
|
|
2376
|
+
return [
|
|
2377
|
+
join(homedir(), "Library", "development-board-toolchain", "runtime", "editor_plugins"),
|
|
2378
|
+
join(homedir(), "Library", "development-board-toolchain", "runtime", "opencode_plugin"),
|
|
2379
|
+
join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "editor_plugins"),
|
|
2380
|
+
join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "opencode_plugin")
|
|
2381
|
+
];
|
|
2382
|
+
}
|
|
2383
|
+
function legacyCodexLocalMarketplacePaths() {
|
|
2384
|
+
return Array.from(LEGACY_CODEX_MARKETPLACE_NAMES)
|
|
2385
|
+
.filter((name) => name !== CODEX_MARKETPLACE_NAME)
|
|
2386
|
+
.map((name) => join(defaultCodexHome(), "local-marketplaces", name));
|
|
2387
|
+
}
|
|
2388
|
+
async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
|
|
2389
|
+
const paths = [];
|
|
2390
|
+
const pluginRoot = join(homedir(), ".agents", "plugins");
|
|
2391
|
+
try {
|
|
2392
|
+
const entries = await readdir(pluginRoot, { withFileTypes: true });
|
|
2393
|
+
for (const entry of entries) {
|
|
2394
|
+
if (!entry.isFile() || !entry.name.startsWith("marketplace.json"))
|
|
2395
|
+
continue;
|
|
2396
|
+
const filePath = join(pluginRoot, entry.name);
|
|
2397
|
+
try {
|
|
2398
|
+
const current = await readFile(filePath, "utf8");
|
|
2399
|
+
if (isLegacyHomeAgentsMarketplace(current)) {
|
|
2400
|
+
paths.push(filePath);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
catch (error) {
|
|
2404
|
+
warnings.push(`Could not inspect legacy Codex home marketplace ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
catch {
|
|
2409
|
+
return paths;
|
|
2410
|
+
}
|
|
2411
|
+
return paths;
|
|
2412
|
+
}
|
|
2413
|
+
function isLegacyHomeAgentsMarketplace(text) {
|
|
2414
|
+
return marketplaceTextHasLegacyCodexPlugin(text);
|
|
2415
|
+
}
|
|
2416
|
+
async function discoverLegacyCodexProjectMarketplacePaths(warnings) {
|
|
2417
|
+
const configPath = codexConfigPath();
|
|
2418
|
+
let text = "";
|
|
2419
|
+
try {
|
|
2420
|
+
text = await readFile(configPath, "utf8");
|
|
2421
|
+
}
|
|
2422
|
+
catch {
|
|
2423
|
+
return [];
|
|
2424
|
+
}
|
|
2425
|
+
const paths = new Set();
|
|
2426
|
+
for (const projectPath of legacyCodexProjectPathsFromConfig(text)) {
|
|
2427
|
+
for (const candidate of [
|
|
2428
|
+
join(projectPath, ".agents", "plugins", "marketplace.json"),
|
|
2429
|
+
join(projectPath, "platform_plugin", ".agents", "plugins", "marketplace.json"),
|
|
2430
|
+
join(projectPath, "platform_plugins", "codex_plugin", ".agents", "plugins", "marketplace.json")
|
|
2431
|
+
]) {
|
|
2432
|
+
try {
|
|
2433
|
+
if (!await pathExists(candidate))
|
|
2434
|
+
continue;
|
|
2435
|
+
const current = await readFile(candidate, "utf8");
|
|
2436
|
+
if (marketplaceTextHasLegacyCodexPlugin(current)) {
|
|
2437
|
+
paths.add(candidate);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
catch (error) {
|
|
2441
|
+
warnings.push(`Could not inspect legacy Codex project marketplace ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
return Array.from(paths);
|
|
2446
|
+
}
|
|
2447
|
+
function legacyCodexProjectPathsFromConfig(text) {
|
|
2448
|
+
const paths = [];
|
|
2449
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
2450
|
+
for (const line of lines) {
|
|
2451
|
+
const table = parseTomlTableHeader(line);
|
|
2452
|
+
const match = table ? /^projects\."([^"]+)"$/.exec(table) : undefined;
|
|
2453
|
+
if (match?.[1] && /DBT-Agent-Project|development-board-toolchain|dbt-agent/i.test(match[1])) {
|
|
2454
|
+
paths.push(match[1].replace(/\\"/g, '"'));
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return paths;
|
|
2458
|
+
}
|
|
2459
|
+
function marketplaceTextHasLegacyCodexPlugin(text) {
|
|
2460
|
+
try {
|
|
2461
|
+
const parsed = JSON.parse(text);
|
|
2462
|
+
const marketplaceName = typeof parsed.name === "string" ? parsed.name : "";
|
|
2463
|
+
const marketplaceDisplayName = typeof parsed.interface?.displayName === "string" ? parsed.interface.displayName : "";
|
|
2464
|
+
const marketplaceLooksLegacy = legacyTextHasCodexPluginResidue(marketplaceName)
|
|
2465
|
+
|| legacyTextHasCodexPluginResidue(marketplaceDisplayName)
|
|
2466
|
+
|| LEGACY_CODEX_MARKETPLACE_NAMES.has(marketplaceName);
|
|
2467
|
+
return (parsed.plugins ?? []).some((plugin) => {
|
|
2468
|
+
const values = [
|
|
2469
|
+
plugin.name,
|
|
2470
|
+
plugin.category,
|
|
2471
|
+
plugin.source?.path,
|
|
2472
|
+
plugin.interface?.displayName
|
|
2473
|
+
].filter((value) => typeof value === "string");
|
|
2474
|
+
return values.some(legacyTextHasCodexPluginResidue) || marketplaceLooksLegacy && values.some((value) => /embed-labs|dbt|development-board/i.test(value));
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
catch {
|
|
2478
|
+
return legacyTextHasCodexPluginResidue(text);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
async function discoverLegacyCodexCachePaths(targetRoot) {
|
|
2482
|
+
const paths = [];
|
|
2483
|
+
const cacheRoot = join(targetRoot, "cache");
|
|
2484
|
+
try {
|
|
2485
|
+
const marketplaces = await readdir(cacheRoot, { withFileTypes: true });
|
|
2486
|
+
for (const entry of marketplaces) {
|
|
2487
|
+
if (!entry.isDirectory())
|
|
2488
|
+
continue;
|
|
2489
|
+
if (LEGACY_CODEX_MARKETPLACE_NAMES.has(entry.name)) {
|
|
2490
|
+
paths.push(join(cacheRoot, entry.name, CODEX_PLUGIN_NAME));
|
|
2491
|
+
}
|
|
2492
|
+
for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
|
|
2493
|
+
paths.push(join(cacheRoot, entry.name, pluginName));
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
catch {
|
|
2498
|
+
return paths;
|
|
2499
|
+
}
|
|
2500
|
+
return paths;
|
|
2501
|
+
}
|
|
2502
|
+
async function cleanupLegacyCodexTextState(warnings) {
|
|
2503
|
+
let removed = 0;
|
|
2504
|
+
removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "history.jsonl"), warnings);
|
|
2505
|
+
removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "session_index.jsonl"), warnings);
|
|
2506
|
+
removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "rules", "default.rules"), warnings);
|
|
2507
|
+
return removed;
|
|
2508
|
+
}
|
|
2509
|
+
async function cleanupLegacyCodexTextFile(filePath, warnings) {
|
|
2510
|
+
try {
|
|
2511
|
+
if (!await pathExists(filePath))
|
|
2512
|
+
return 0;
|
|
2513
|
+
const current = await readFile(filePath, "utf8");
|
|
2514
|
+
const lines = current.split("\n");
|
|
2515
|
+
let removed = 0;
|
|
2516
|
+
const kept = lines.filter((line) => {
|
|
2517
|
+
if (!line)
|
|
2518
|
+
return true;
|
|
2519
|
+
if (isLegacyCodexHistoryMention(line)) {
|
|
2520
|
+
removed += 1;
|
|
2521
|
+
return false;
|
|
2522
|
+
}
|
|
2523
|
+
return true;
|
|
2524
|
+
});
|
|
2525
|
+
if (removed > 0) {
|
|
2526
|
+
await writeFile(filePath, kept.join("\n"), "utf8");
|
|
2527
|
+
}
|
|
2528
|
+
return removed;
|
|
2529
|
+
}
|
|
2530
|
+
catch (error) {
|
|
2531
|
+
warnings.push(`Could not clean Codex legacy text state ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2532
|
+
return 0;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
function isLegacyCodexHistoryMention(line) {
|
|
2536
|
+
return line.includes("plugin://dbt-agent@plugins")
|
|
2537
|
+
|| line.includes("plugin://dbt-agent@embed-labs")
|
|
2538
|
+
|| line.includes("development-board-toolchain-dev")
|
|
2539
|
+
|| line.includes("development-board-toolchain")
|
|
2540
|
+
|| /plugin:\/\/Dbt Agent@/i.test(line)
|
|
2541
|
+
|| /plugin:\/\/deve@/i.test(line)
|
|
2542
|
+
|| /dbt-agent/i.test(line);
|
|
2543
|
+
}
|
|
2544
|
+
function removeLegacyCodexConfigTables(text) {
|
|
2545
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
2546
|
+
const output = [];
|
|
2547
|
+
const removedTables = [];
|
|
2548
|
+
let skipping = false;
|
|
2549
|
+
for (const line of lines) {
|
|
2550
|
+
const table = parseTomlTableHeader(line);
|
|
2551
|
+
if (table) {
|
|
2552
|
+
skipping = isLegacyCodexConfigTable(table);
|
|
2553
|
+
if (skipping) {
|
|
2554
|
+
removedTables.push(table);
|
|
2555
|
+
continue;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
if (!skipping) {
|
|
2559
|
+
output.push(line);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return { text: output.join("").replace(/\n{3,}/g, "\n\n"), removedTables };
|
|
2563
|
+
}
|
|
2564
|
+
function parseTomlTableHeader(line) {
|
|
2565
|
+
const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
|
|
2566
|
+
return match?.[1]?.trim();
|
|
2567
|
+
}
|
|
2568
|
+
function isLegacyCodexConfigTable(table) {
|
|
2569
|
+
return /^plugins\."dbt-agent@[^"]+"$/.test(table)
|
|
2570
|
+
|| /^plugins\."Dbt Agent@[^"]+"$/i.test(table)
|
|
2571
|
+
|| /^plugins\."deve@[^"]+"$/i.test(table)
|
|
2572
|
+
|| isLegacyEmbedLabsCodexMarketplaceConfigTable(table)
|
|
2573
|
+
|| table === "mcp_servers.dbt-agent"
|
|
2574
|
+
|| table.startsWith("mcp_servers.dbt-agent.")
|
|
2575
|
+
|| table === 'mcp_servers."dbt-agent"'
|
|
2576
|
+
|| table.startsWith('mcp_servers."dbt-agent".')
|
|
2577
|
+
|| table === "mcp_servers.deve"
|
|
2578
|
+
|| table.startsWith("mcp_servers.deve.")
|
|
2579
|
+
|| /^projects\."[^"]*\/DBT-Agent-Project(?:\/[^"]*)?"$/.test(table);
|
|
2580
|
+
}
|
|
2581
|
+
function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
|
|
2582
|
+
for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
|
|
2583
|
+
if (table === `marketplaces.${marketplaceName}`) {
|
|
2584
|
+
return true;
|
|
2585
|
+
}
|
|
2586
|
+
if (table === `plugins."${CODEX_PLUGIN_NAME}@${marketplaceName}"`) {
|
|
2587
|
+
return true;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
return false;
|
|
2591
|
+
}
|
|
2592
|
+
function legacyTextHasCodexPluginResidue(value) {
|
|
2593
|
+
return /dbt-agent|Dbt Agent|development-board-toolchain|development-board-toolchain-dev/i.test(value)
|
|
2594
|
+
|| value === "deve"
|
|
2595
|
+
|| value.replace(/\\/g, "/").includes("/plugins/deve")
|
|
2596
|
+
|| value.includes("plugin://deve@");
|
|
2597
|
+
}
|
|
2598
|
+
function legacyCodexCleanupWarning(cleanup) {
|
|
2599
|
+
const parts = [];
|
|
2600
|
+
if (cleanup.legacy_removed_paths.length > 0) {
|
|
2601
|
+
parts.push(`removed ${cleanup.legacy_removed_paths.length} stale/legacy Codex plugin path(s)`);
|
|
2602
|
+
}
|
|
2603
|
+
if (cleanup.legacy_removed_config_tables?.length) {
|
|
2604
|
+
parts.push(`removed ${cleanup.legacy_removed_config_tables.length} legacy Codex config table(s)`);
|
|
2605
|
+
}
|
|
2606
|
+
if (cleanup.legacy_removed_history_entries) {
|
|
2607
|
+
parts.push(`removed ${cleanup.legacy_removed_history_entries} legacy Codex text-state mention(s)`);
|
|
2608
|
+
}
|
|
2609
|
+
if (cleanup.legacy_stopped_processes) {
|
|
2610
|
+
parts.push(`stopped ${cleanup.legacy_stopped_processes} legacy Codex plugin process(es)`);
|
|
2611
|
+
}
|
|
2612
|
+
if (cleanup.warnings?.length) {
|
|
2613
|
+
parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
|
|
2614
|
+
}
|
|
2615
|
+
return parts.length > 0 ? `Codex plugin cleanup: ${parts.join(", ")}.` : undefined;
|
|
2616
|
+
}
|
|
2617
|
+
async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
|
|
2618
|
+
const removedPaths = [];
|
|
2619
|
+
const warnings = [];
|
|
2620
|
+
const legacyPaths = [
|
|
2621
|
+
join(targetRoot, "plugins", "development-board-toolchain.js"),
|
|
2622
|
+
join(targetRoot, "plugins", "development-board-toolchain-dev.js"),
|
|
2623
|
+
join(targetRoot, "plugins", "dbt-agent.js"),
|
|
2624
|
+
join(targetRoot, "plugins", "Dbt Agent.js"),
|
|
2625
|
+
join(targetRoot, "plugins", "deve.js"),
|
|
2626
|
+
join(targetRoot, "plugins", "deve"),
|
|
2627
|
+
join(targetRoot, "node_modules", "development-board-toolchain"),
|
|
2628
|
+
join(targetRoot, "node_modules", "development-board-toolchain-dev"),
|
|
2629
|
+
join(targetRoot, "node_modules", "dbt-agent")
|
|
2630
|
+
];
|
|
2631
|
+
if (globalInstall) {
|
|
2632
|
+
legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
|
|
2633
|
+
legacyPaths.push(...await discoverLegacyOpenCodeBackupPaths(targetRoot, warnings));
|
|
2634
|
+
legacyPaths.push(...await discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings));
|
|
2635
|
+
}
|
|
2636
|
+
for (const candidate of legacyPaths) {
|
|
2637
|
+
try {
|
|
2638
|
+
if (await pathExists(candidate)) {
|
|
2639
|
+
await rm(candidate, { recursive: true, force: true });
|
|
2640
|
+
removedPaths.push(candidate);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
catch (error) {
|
|
2644
|
+
warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
return {
|
|
2648
|
+
legacy_removed_paths: removedPaths,
|
|
2649
|
+
legacy_removed_config_entries: [],
|
|
2650
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
|
|
2654
|
+
const paths = [];
|
|
2655
|
+
try {
|
|
2656
|
+
const entries = await readdir(targetRoot, { withFileTypes: true });
|
|
2657
|
+
for (const entry of entries) {
|
|
2658
|
+
if (!entry.isFile() || !entry.name.includes(".bak"))
|
|
2659
|
+
continue;
|
|
2660
|
+
const filePath = join(targetRoot, entry.name);
|
|
2661
|
+
try {
|
|
2662
|
+
const current = await readFile(filePath, "utf8");
|
|
2663
|
+
if (legacyTextHasCodexPluginResidue(current)) {
|
|
2664
|
+
paths.push(filePath);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
catch (error) {
|
|
2668
|
+
warnings.push(`Could not inspect legacy OpenCode backup ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
catch {
|
|
2673
|
+
return paths;
|
|
2674
|
+
}
|
|
2675
|
+
return paths;
|
|
2676
|
+
}
|
|
2677
|
+
async function discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings) {
|
|
2678
|
+
const paths = [];
|
|
2679
|
+
const cacheRoot = join(targetRoot, ".embed-labs", "plugin-cache");
|
|
2680
|
+
try {
|
|
2681
|
+
const entries = await readdir(cacheRoot, { withFileTypes: true });
|
|
2682
|
+
for (const entry of entries) {
|
|
2683
|
+
if (!entry.isFile())
|
|
2684
|
+
continue;
|
|
2685
|
+
if (/^(embed-labs|embed-labs-opencode-plugin)-\d+\.\d+\.\d+\.tgz$/.test(entry.name)) {
|
|
2686
|
+
paths.push(join(cacheRoot, entry.name));
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
catch (error) {
|
|
2691
|
+
if (error.code !== "ENOENT") {
|
|
2692
|
+
warnings.push(`Could not inspect OpenCode plugin cache ${cacheRoot}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
return paths;
|
|
2696
|
+
}
|
|
2697
|
+
function legacyOpenCodeCleanupWarning(cleanup) {
|
|
2698
|
+
const parts = [];
|
|
2699
|
+
if (cleanup.legacy_removed_paths.length > 0) {
|
|
2700
|
+
parts.push(`removed ${cleanup.legacy_removed_paths.length} legacy OpenCode plugin path(s)`);
|
|
2701
|
+
}
|
|
2702
|
+
if (cleanup.legacy_removed_config_entries?.length) {
|
|
2703
|
+
parts.push(`removed ${cleanup.legacy_removed_config_entries.length} legacy OpenCode config entry(s)`);
|
|
2704
|
+
}
|
|
2705
|
+
if (cleanup.warnings?.length) {
|
|
2706
|
+
parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
|
|
2707
|
+
}
|
|
2708
|
+
return parts.length > 0 ? `Legacy OpenCode cleanup: ${parts.join(", ")}.` : undefined;
|
|
2709
|
+
}
|
|
2710
|
+
function combineWarnings(...warnings) {
|
|
2711
|
+
const actual = warnings.filter((warning) => Boolean(warning));
|
|
2712
|
+
return actual.length > 0 ? actual.join(" ") : undefined;
|
|
1777
2713
|
}
|
|
1778
2714
|
async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
|
|
1779
2715
|
const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
|
|
@@ -1834,6 +2770,115 @@ async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
|
|
|
1834
2770
|
const warning = await upsertCodexMcpRuntimeConfig(bridgePath);
|
|
1835
2771
|
return warning ? { registered: true, warning } : { registered: true };
|
|
1836
2772
|
}
|
|
2773
|
+
async function maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath) {
|
|
2774
|
+
const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
|
|
2775
|
+
if (explicitTarget && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
|
|
2776
|
+
return {
|
|
2777
|
+
registered: false,
|
|
2778
|
+
warning: "Codex plugin marketplace entry was not registered because a custom target was used. Set EMBED_CODEX_MARKETPLACE_REGISTER=1 to register it anyway."
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
if (resolve(targetRoot) !== resolve(defaultCodexPluginRoot()) && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
|
|
2782
|
+
return {
|
|
2783
|
+
registered: false,
|
|
2784
|
+
warning: "Codex plugin marketplace entry was not registered because the install target is not the default Codex plugin root."
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
const marketplacePath = defaultCodexLocalMarketplaceRoot();
|
|
2788
|
+
const marketplacePluginPath = join(marketplacePath, "plugins", CODEX_PLUGIN_NAME);
|
|
2789
|
+
try {
|
|
2790
|
+
if (!await pathExists(join(targetPath, ".codex-plugin", "plugin.json"))) {
|
|
2791
|
+
return {
|
|
2792
|
+
registered: false,
|
|
2793
|
+
warning: `Codex plugin manifest was not found at ${join(targetPath, ".codex-plugin", "plugin.json")}.`
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
await rm(marketplacePluginPath, { recursive: true, force: true });
|
|
2797
|
+
await mkdir(dirname(marketplacePluginPath), { recursive: true });
|
|
2798
|
+
await cp(targetPath, marketplacePluginPath, { recursive: true });
|
|
2799
|
+
await refreshCodexPluginCache(targetPath);
|
|
2800
|
+
await writeCodexLocalMarketplaceManifest(marketplacePath);
|
|
2801
|
+
const warning = await upsertCodexPluginMarketplaceConfig(marketplacePath);
|
|
2802
|
+
return warning ? { registered: true, marketplacePath, warning } : { registered: true, marketplacePath };
|
|
2803
|
+
}
|
|
2804
|
+
catch (error) {
|
|
2805
|
+
return {
|
|
2806
|
+
registered: false,
|
|
2807
|
+
marketplacePath,
|
|
2808
|
+
warning: `Could not register Codex plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
function defaultCodexLocalMarketplaceRoot() {
|
|
2813
|
+
return join(defaultCodexHome(), "local-marketplaces", CODEX_PLUGIN_NAME);
|
|
2814
|
+
}
|
|
2815
|
+
async function refreshCodexPluginCache(targetPath) {
|
|
2816
|
+
const manifestPath = join(targetPath, ".codex-plugin", "plugin.json");
|
|
2817
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
2818
|
+
const version = typeof manifest.version === "string" && manifest.version.trim()
|
|
2819
|
+
? manifest.version.trim()
|
|
2820
|
+
: "local";
|
|
2821
|
+
const cachePluginRoot = join(defaultCodexPluginRoot(), "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME);
|
|
2822
|
+
const cacheVersionPath = join(cachePluginRoot, version);
|
|
2823
|
+
await rm(cachePluginRoot, { recursive: true, force: true });
|
|
2824
|
+
await mkdir(dirname(cacheVersionPath), { recursive: true });
|
|
2825
|
+
await cp(targetPath, cacheVersionPath, { recursive: true });
|
|
2826
|
+
}
|
|
2827
|
+
async function writeCodexLocalMarketplaceManifest(marketplacePath) {
|
|
2828
|
+
const manifestPath = join(marketplacePath, ".agents", "plugins", "marketplace.json");
|
|
2829
|
+
const manifest = {
|
|
2830
|
+
name: CODEX_MARKETPLACE_NAME,
|
|
2831
|
+
interface: {
|
|
2832
|
+
displayName: "Embed Labs"
|
|
2833
|
+
},
|
|
2834
|
+
plugins: [
|
|
2835
|
+
{
|
|
2836
|
+
name: CODEX_PLUGIN_NAME,
|
|
2837
|
+
source: {
|
|
2838
|
+
source: "local",
|
|
2839
|
+
path: `./plugins/${CODEX_PLUGIN_NAME}`
|
|
2840
|
+
},
|
|
2841
|
+
policy: {
|
|
2842
|
+
installation: "AVAILABLE",
|
|
2843
|
+
authentication: "ON_USE"
|
|
2844
|
+
},
|
|
2845
|
+
category: "Developer Tools"
|
|
2846
|
+
}
|
|
2847
|
+
]
|
|
2848
|
+
};
|
|
2849
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
2850
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
2851
|
+
}
|
|
2852
|
+
async function upsertCodexPluginMarketplaceConfig(marketplacePath) {
|
|
2853
|
+
const configPath = codexConfigPath();
|
|
2854
|
+
try {
|
|
2855
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
2856
|
+
let text = "";
|
|
2857
|
+
try {
|
|
2858
|
+
text = await readFile(configPath, "utf8");
|
|
2859
|
+
}
|
|
2860
|
+
catch {
|
|
2861
|
+
text = "";
|
|
2862
|
+
}
|
|
2863
|
+
const original = text;
|
|
2864
|
+
const cleaned = removeLegacyCodexConfigTables(text).text;
|
|
2865
|
+
let updated = upsertTomlTableKeys(cleaned, `marketplaces.${CODEX_MARKETPLACE_NAME}`, {
|
|
2866
|
+
source_type: tomlString("local"),
|
|
2867
|
+
source: tomlString(marketplacePath),
|
|
2868
|
+
last_updated: tomlString(new Date().toISOString().replace(/\.\d{3}Z$/, "Z"))
|
|
2869
|
+
});
|
|
2870
|
+
updated = upsertTomlTableKeys(updated, `plugins."${CODEX_PLUGIN_NAME}@${CODEX_MARKETPLACE_NAME}"`, {
|
|
2871
|
+
enabled: "true"
|
|
2872
|
+
});
|
|
2873
|
+
if (updated !== original) {
|
|
2874
|
+
await writeFile(configPath, updated, "utf8");
|
|
2875
|
+
}
|
|
2876
|
+
return undefined;
|
|
2877
|
+
}
|
|
2878
|
+
catch (error) {
|
|
2879
|
+
return `${configPath} could not be updated with the Embed Labs plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
1837
2882
|
function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embedCliBin) {
|
|
1838
2883
|
try {
|
|
1839
2884
|
const parsed = JSON.parse(stdout);
|
|
@@ -1922,34 +2967,183 @@ function pluginMcpCloudApiUrl(parsed) {
|
|
|
1922
2967
|
return DEFAULT_CLOUD_API_URL.replace(/\/+$/, "");
|
|
1923
2968
|
}
|
|
1924
2969
|
}
|
|
1925
|
-
async function resolveExecutableOnPath(name) {
|
|
1926
|
-
if (name.includes("/")) {
|
|
2970
|
+
async function resolveExecutableOnPath(name) {
|
|
2971
|
+
if (name.includes("/")) {
|
|
2972
|
+
try {
|
|
2973
|
+
await access(name, constants.X_OK);
|
|
2974
|
+
return name;
|
|
2975
|
+
}
|
|
2976
|
+
catch {
|
|
2977
|
+
return undefined;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const pathEntries = (process.env.PATH || "").split(delimiter).filter(Boolean);
|
|
2981
|
+
for (const entry of pathEntries) {
|
|
2982
|
+
const candidate = join(entry, name);
|
|
2983
|
+
try {
|
|
2984
|
+
await access(candidate, constants.X_OK);
|
|
2985
|
+
return candidate;
|
|
2986
|
+
}
|
|
2987
|
+
catch {
|
|
2988
|
+
// Keep searching PATH.
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
return undefined;
|
|
2992
|
+
}
|
|
2993
|
+
async function runBridgeStart(parsed) {
|
|
2994
|
+
const host = stringFlag(parsed, "host");
|
|
2995
|
+
const port = numberFlag(parsed, "port");
|
|
2996
|
+
const bridge = await resolveBridgeLauncher();
|
|
2997
|
+
const args = [...bridge.args];
|
|
2998
|
+
if (host) {
|
|
2999
|
+
args.push("--host", host);
|
|
3000
|
+
}
|
|
3001
|
+
if (port !== undefined) {
|
|
3002
|
+
args.push("--port", String(port));
|
|
3003
|
+
}
|
|
3004
|
+
const env = {
|
|
3005
|
+
...process.env,
|
|
3006
|
+
...(host ? { EMBED_BRIDGE_HOST: host } : {}),
|
|
3007
|
+
...(port !== undefined ? { EMBED_BRIDGE_PORT: String(port) } : {})
|
|
3008
|
+
};
|
|
3009
|
+
const child = spawn(bridge.command, args, {
|
|
3010
|
+
stdio: "inherit",
|
|
3011
|
+
env
|
|
3012
|
+
});
|
|
3013
|
+
const forwardSignal = (signal) => {
|
|
3014
|
+
if (!child.killed) {
|
|
3015
|
+
child.kill(signal);
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
process.once("SIGINT", forwardSignal);
|
|
3019
|
+
process.once("SIGTERM", forwardSignal);
|
|
3020
|
+
return await new Promise((resolveCode) => {
|
|
3021
|
+
child.on("error", (error) => {
|
|
3022
|
+
process.off("SIGINT", forwardSignal);
|
|
3023
|
+
process.off("SIGTERM", forwardSignal);
|
|
3024
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3025
|
+
resolveCode(1);
|
|
3026
|
+
});
|
|
3027
|
+
child.on("close", (code, signal) => {
|
|
3028
|
+
process.off("SIGINT", forwardSignal);
|
|
3029
|
+
process.off("SIGTERM", forwardSignal);
|
|
3030
|
+
if (signal === "SIGINT" || signal === "SIGTERM") {
|
|
3031
|
+
resolveCode(0);
|
|
3032
|
+
}
|
|
3033
|
+
else {
|
|
3034
|
+
resolveCode(code ?? 0);
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
async function resolveBridgeLauncher() {
|
|
3040
|
+
const explicitBinary = process.env.EMBED_LOCAL_BRIDGE_BINARY?.trim();
|
|
3041
|
+
if (explicitBinary) {
|
|
1927
3042
|
try {
|
|
1928
|
-
await access(
|
|
1929
|
-
return
|
|
3043
|
+
await access(explicitBinary, constants.X_OK);
|
|
3044
|
+
return { command: explicitBinary, args: [] };
|
|
1930
3045
|
}
|
|
1931
3046
|
catch {
|
|
1932
|
-
|
|
3047
|
+
// Fall through so the package launcher can print its clearer repair message.
|
|
1933
3048
|
}
|
|
1934
3049
|
}
|
|
1935
|
-
const
|
|
1936
|
-
|
|
1937
|
-
|
|
3050
|
+
const pathBinary = await resolveExecutableOnPath(process.platform === "win32" ? "embed-local-bridge.cmd" : "embed-local-bridge");
|
|
3051
|
+
if (pathBinary) {
|
|
3052
|
+
return { command: pathBinary, args: [] };
|
|
3053
|
+
}
|
|
3054
|
+
const packageLauncher = await resolveBridgePackageLauncher();
|
|
3055
|
+
if (packageLauncher) {
|
|
3056
|
+
return { command: process.execPath, args: [packageLauncher] };
|
|
3057
|
+
}
|
|
3058
|
+
return {
|
|
3059
|
+
command: process.execPath,
|
|
3060
|
+
args: [resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js")]
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
async function resolveBridgePackageLauncher() {
|
|
3064
|
+
const candidates = [];
|
|
3065
|
+
try {
|
|
3066
|
+
const packageJson = require.resolve("@embed-labs/local-bridge/package.json");
|
|
3067
|
+
candidates.push(join(dirname(packageJson), "dist", "index.js"));
|
|
3068
|
+
}
|
|
3069
|
+
catch {
|
|
3070
|
+
// Source checkout fallback below.
|
|
3071
|
+
}
|
|
3072
|
+
candidates.push(resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js"));
|
|
3073
|
+
for (const candidate of candidates) {
|
|
1938
3074
|
try {
|
|
1939
|
-
await access(candidate, constants.
|
|
3075
|
+
await access(candidate, constants.R_OK);
|
|
1940
3076
|
return candidate;
|
|
1941
3077
|
}
|
|
1942
3078
|
catch {
|
|
1943
|
-
// Keep
|
|
3079
|
+
// Keep looking.
|
|
1944
3080
|
}
|
|
1945
3081
|
}
|
|
1946
3082
|
return undefined;
|
|
1947
3083
|
}
|
|
1948
3084
|
function defaultOpenCodeRoot() {
|
|
1949
|
-
return
|
|
3085
|
+
return globalOpenCodeRoot();
|
|
3086
|
+
}
|
|
3087
|
+
function globalOpenCodeRoot() {
|
|
3088
|
+
return join(homedir(), ".config", "opencode");
|
|
3089
|
+
}
|
|
3090
|
+
function isGlobalOpenCodeRoot(targetRoot) {
|
|
3091
|
+
return resolve(targetRoot) === resolve(globalOpenCodeRoot());
|
|
3092
|
+
}
|
|
3093
|
+
async function ensureOpenCodeGlobalPluginConfig() {
|
|
3094
|
+
const configPath = join(globalOpenCodeRoot(), "opencode.json");
|
|
3095
|
+
let existing = {};
|
|
3096
|
+
try {
|
|
3097
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
3098
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3099
|
+
existing = parsed;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
catch {
|
|
3103
|
+
existing = {};
|
|
3104
|
+
}
|
|
3105
|
+
const configured = Array.isArray(existing.plugin)
|
|
3106
|
+
? existing.plugin.filter((item) => typeof item === "string")
|
|
3107
|
+
: Array.isArray(existing.plugins)
|
|
3108
|
+
? existing.plugins.filter((item) => typeof item === "string")
|
|
3109
|
+
: [];
|
|
3110
|
+
const removed = configured.filter(isLegacyOpenCodePluginConfigEntry);
|
|
3111
|
+
const cleaned = configured.filter((item) => !isLegacyOpenCodePluginConfigEntry(item));
|
|
3112
|
+
if (!cleaned.includes("embed-labs")) {
|
|
3113
|
+
cleaned.push("embed-labs");
|
|
3114
|
+
}
|
|
3115
|
+
await writeFile(configPath, `${JSON.stringify({
|
|
3116
|
+
...existing,
|
|
3117
|
+
plugin: cleaned,
|
|
3118
|
+
plugins: undefined
|
|
3119
|
+
}, null, 2)}\n`, "utf8");
|
|
3120
|
+
return removed;
|
|
3121
|
+
}
|
|
3122
|
+
function isLegacyOpenCodePluginConfigEntry(item) {
|
|
3123
|
+
const normalized = item.trim().replace(/\\/g, "/");
|
|
3124
|
+
const pathOnly = normalized.split(/[?#]/, 1)[0] || normalized;
|
|
3125
|
+
return normalized === "dbt-agent"
|
|
3126
|
+
|| normalized === "Dbt Agent"
|
|
3127
|
+
|| normalized === "deve"
|
|
3128
|
+
|| normalized === "development-board-toolchain"
|
|
3129
|
+
|| normalized === "development-board-toolchain-dev"
|
|
3130
|
+
|| normalized === "./plugins/deve"
|
|
3131
|
+
|| normalized === "./plugins/deve.js"
|
|
3132
|
+
|| /(?:^|\/)plugins\/deve(?:\.js)?$/.test(pathOnly)
|
|
3133
|
+
|| /(?:^|\/)plugins\/dbt-agent(?:\.js)?$/.test(pathOnly)
|
|
3134
|
+
|| normalized === "./plugins/development-board-toolchain"
|
|
3135
|
+
|| normalized === "./plugins/development-board-toolchain.js"
|
|
3136
|
+
|| normalized === "./plugins/development-board-toolchain-dev"
|
|
3137
|
+
|| normalized === "./plugins/development-board-toolchain-dev.js"
|
|
3138
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain")
|
|
3139
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain.js")
|
|
3140
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain-dev")
|
|
3141
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain-dev.js")
|
|
3142
|
+
|| normalized.includes("dbt-agent")
|
|
3143
|
+
|| normalized.includes("development-board-toolchain");
|
|
1950
3144
|
}
|
|
1951
3145
|
async function openCodeDuplicatePluginWarning(targetRoot) {
|
|
1952
|
-
const globalRoot =
|
|
3146
|
+
const globalRoot = globalOpenCodeRoot();
|
|
1953
3147
|
if (resolve(targetRoot) === resolve(globalRoot))
|
|
1954
3148
|
return undefined;
|
|
1955
3149
|
const configPath = join(globalRoot, "opencode.json");
|
|
@@ -1981,6 +3175,21 @@ async function localPluginVersion(kind) {
|
|
|
1981
3175
|
return undefined;
|
|
1982
3176
|
}
|
|
1983
3177
|
}
|
|
3178
|
+
async function installedCodexPluginVersion(pluginPath) {
|
|
3179
|
+
return await readPackageVersion(join(pluginPath, ".codex-plugin", "plugin.json"));
|
|
3180
|
+
}
|
|
3181
|
+
async function installedOpenCodePluginVersion(targetRoot) {
|
|
3182
|
+
return await readPackageVersion(join(targetRoot, "node_modules", "embed-labs", "package.json"));
|
|
3183
|
+
}
|
|
3184
|
+
async function readPackageVersion(filePath) {
|
|
3185
|
+
try {
|
|
3186
|
+
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
3187
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : undefined;
|
|
3188
|
+
}
|
|
3189
|
+
catch {
|
|
3190
|
+
return undefined;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
1984
3193
|
async function localPluginSourcesAvailable() {
|
|
1985
3194
|
return await pathExists(sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", ".codex-plugin", "plugin.json"))
|
|
1986
3195
|
&& await pathExists(sourceCheckoutPath("platform_plugins", "opencode_plugin", "package.json"));
|
|
@@ -2039,19 +3248,65 @@ async function parseErrorResponse(response) {
|
|
|
2039
3248
|
return undefined;
|
|
2040
3249
|
}
|
|
2041
3250
|
async function cloudAuthToken() {
|
|
3251
|
+
return (await cloudAuthConfig()).token;
|
|
3252
|
+
}
|
|
3253
|
+
async function cloudAuthConfig() {
|
|
2042
3254
|
const envToken = process.env.EMBED_API_TOKEN?.trim();
|
|
2043
3255
|
if (envToken) {
|
|
2044
|
-
|
|
3256
|
+
const fileConfig = await readLocalAuthFile();
|
|
3257
|
+
return {
|
|
3258
|
+
...fileConfig,
|
|
3259
|
+
token: envToken,
|
|
3260
|
+
profile: process.env.EMBED_AUTH_PROFILE ?? fileConfig.profile ?? "default",
|
|
3261
|
+
source: "env"
|
|
3262
|
+
};
|
|
2045
3263
|
}
|
|
3264
|
+
const fileConfig = await readLocalAuthFile();
|
|
3265
|
+
return {
|
|
3266
|
+
...fileConfig,
|
|
3267
|
+
token: fileConfig.token?.trim() || undefined,
|
|
3268
|
+
profile: fileConfig.profile ?? "default",
|
|
3269
|
+
source: fileConfig.token ? "file" : undefined
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
async function readLocalAuthFile() {
|
|
2046
3273
|
try {
|
|
2047
3274
|
const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
|
|
2048
|
-
|
|
2049
|
-
return fileToken || undefined;
|
|
3275
|
+
return normalizeLocalAuthFile(parsed);
|
|
2050
3276
|
}
|
|
2051
3277
|
catch {
|
|
2052
|
-
return
|
|
3278
|
+
return {};
|
|
2053
3279
|
}
|
|
2054
3280
|
}
|
|
3281
|
+
function normalizeLocalAuthFile(parsed) {
|
|
3282
|
+
const device = isJsonObject(parsed.device) ? parsed.device : undefined;
|
|
3283
|
+
const normalizedDevice = device && typeof device.device_id === "string" && typeof device.fingerprint_hash === "string" && typeof device.private_key_pem === "string"
|
|
3284
|
+
? {
|
|
3285
|
+
device_id: device.device_id,
|
|
3286
|
+
fingerprint_hash: device.fingerprint_hash,
|
|
3287
|
+
private_key_pem: device.private_key_pem,
|
|
3288
|
+
public_key_pem: typeof device.public_key_pem === "string" ? device.public_key_pem : undefined,
|
|
3289
|
+
label: typeof device.label === "string" ? device.label : undefined,
|
|
3290
|
+
platform: typeof device.platform === "string" ? device.platform : undefined,
|
|
3291
|
+
arch: typeof device.arch === "string" ? device.arch : undefined,
|
|
3292
|
+
hostname_hash: typeof device.hostname_hash === "string" ? device.hostname_hash : undefined,
|
|
3293
|
+
registered_at: typeof device.registered_at === "string" ? device.registered_at : undefined
|
|
3294
|
+
}
|
|
3295
|
+
: undefined;
|
|
3296
|
+
return {
|
|
3297
|
+
profile: typeof parsed.profile === "string" ? parsed.profile : undefined,
|
|
3298
|
+
token: typeof parsed.token === "string" ? parsed.token.trim() : undefined,
|
|
3299
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined,
|
|
3300
|
+
account_id: typeof parsed.account_id === "string" ? parsed.account_id : undefined,
|
|
3301
|
+
api_key_id: typeof parsed.api_key_id === "string" ? parsed.api_key_id : undefined,
|
|
3302
|
+
device: normalizedDevice
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
async function writeLocalAuthFile(config) {
|
|
3306
|
+
await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
|
|
3307
|
+
await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
3308
|
+
await chmod(DEFAULT_AUTH_FILE, 0o600).catch(() => undefined);
|
|
3309
|
+
}
|
|
2055
3310
|
function serviceBaseUrl(url) {
|
|
2056
3311
|
return url.replace(/\/+$/, "");
|
|
2057
3312
|
}
|
|
@@ -3510,30 +4765,272 @@ async function authLogin(parsed) {
|
|
|
3510
4765
|
return fail("invalid_args", "Usage: embed auth login --token <token> [--profile default] [--json]");
|
|
3511
4766
|
}
|
|
3512
4767
|
const updatedAt = new Date().toISOString();
|
|
3513
|
-
|
|
3514
|
-
await
|
|
3515
|
-
|
|
4768
|
+
const current = await readLocalAuthFile();
|
|
4769
|
+
const localDevice = await buildLocalDeviceAuth(parsed, current.device);
|
|
4770
|
+
const registration = await registerLocalDevice(token.trim(), localDevice.registration);
|
|
4771
|
+
if (!registration.ok) {
|
|
4772
|
+
return fail(registration.error.code, registration.error.message, {
|
|
4773
|
+
remediation: registration.error.remediation,
|
|
4774
|
+
details: registration.error.details
|
|
4775
|
+
});
|
|
4776
|
+
}
|
|
4777
|
+
const device = {
|
|
4778
|
+
device_id: registration.data.device.device_id,
|
|
4779
|
+
fingerprint_hash: localDevice.device.fingerprint_hash,
|
|
4780
|
+
private_key_pem: localDevice.device.private_key_pem,
|
|
4781
|
+
public_key_pem: localDevice.device.public_key_pem,
|
|
4782
|
+
label: registration.data.device.label ?? localDevice.device.label,
|
|
4783
|
+
platform: registration.data.device.platform ?? localDevice.device.platform,
|
|
4784
|
+
arch: registration.data.device.arch ?? localDevice.device.arch,
|
|
4785
|
+
hostname_hash: registration.data.device.hostname_hash ?? localDevice.device.hostname_hash,
|
|
4786
|
+
registered_at: registration.data.device.first_seen_at
|
|
4787
|
+
};
|
|
4788
|
+
await writeLocalAuthFile({
|
|
4789
|
+
profile,
|
|
4790
|
+
token: token.trim(),
|
|
4791
|
+
updated_at: updatedAt,
|
|
4792
|
+
account_id: registration.data.device.account_id,
|
|
4793
|
+
api_key_id: registration.data.device.api_key_id,
|
|
4794
|
+
device
|
|
4795
|
+
});
|
|
4796
|
+
return ok({
|
|
4797
|
+
authenticated: true,
|
|
4798
|
+
profile,
|
|
4799
|
+
source: "file",
|
|
4800
|
+
updated_at: updatedAt,
|
|
4801
|
+
account_id: registration.data.device.account_id,
|
|
4802
|
+
api_key_id: registration.data.device.api_key_id,
|
|
4803
|
+
device_id: device.device_id,
|
|
4804
|
+
device_fingerprint_hash: device.fingerprint_hash,
|
|
4805
|
+
device_label: device.label,
|
|
4806
|
+
device_registered_at: device.registered_at,
|
|
4807
|
+
device_private_key_configured: true
|
|
4808
|
+
});
|
|
3516
4809
|
}
|
|
3517
4810
|
async function authStatus() {
|
|
3518
|
-
|
|
4811
|
+
const envToken = process.env.EMBED_API_TOKEN?.trim();
|
|
4812
|
+
const file = await readLocalAuthFile();
|
|
4813
|
+
const deviceIntegrity = file.device
|
|
4814
|
+
? (await validateLocalDeviceIntegrity(file.device)).ok ? "ok" : "failed"
|
|
4815
|
+
: "unbound";
|
|
4816
|
+
if (envToken) {
|
|
3519
4817
|
return {
|
|
3520
4818
|
authenticated: true,
|
|
3521
4819
|
profile: process.env.EMBED_AUTH_PROFILE ?? "default",
|
|
3522
|
-
source: "env"
|
|
4820
|
+
source: "env",
|
|
4821
|
+
account_id: file.account_id,
|
|
4822
|
+
api_key_id: file.api_key_id,
|
|
4823
|
+
device_id: file.device?.device_id,
|
|
4824
|
+
device_fingerprint_hash: file.device?.fingerprint_hash,
|
|
4825
|
+
device_label: file.device?.label,
|
|
4826
|
+
device_registered_at: file.device?.registered_at,
|
|
4827
|
+
device_private_key_configured: Boolean(file.device?.private_key_pem),
|
|
4828
|
+
device_integrity: deviceIntegrity
|
|
3523
4829
|
};
|
|
3524
4830
|
}
|
|
4831
|
+
return {
|
|
4832
|
+
authenticated: Boolean(file.token?.trim()),
|
|
4833
|
+
profile: file.profile ?? "default",
|
|
4834
|
+
source: file.token ? "file" : undefined,
|
|
4835
|
+
updated_at: file.updated_at,
|
|
4836
|
+
account_id: file.account_id,
|
|
4837
|
+
api_key_id: file.api_key_id,
|
|
4838
|
+
device_id: file.device?.device_id,
|
|
4839
|
+
device_fingerprint_hash: file.device?.fingerprint_hash,
|
|
4840
|
+
device_label: file.device?.label,
|
|
4841
|
+
device_registered_at: file.device?.registered_at,
|
|
4842
|
+
device_private_key_configured: Boolean(file.device?.private_key_pem),
|
|
4843
|
+
device_integrity: deviceIntegrity
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4846
|
+
async function authDeviceStatus(parsed) {
|
|
4847
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
4848
|
+
const unexpected = parsed.command.slice(3);
|
|
4849
|
+
if (unknownFlag || unexpected.length > 0) {
|
|
4850
|
+
return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_STATUS_USAGE}` : AUTH_DEVICE_STATUS_USAGE);
|
|
4851
|
+
}
|
|
4852
|
+
const local = await authStatus();
|
|
4853
|
+
const remote = local.authenticated ? await cloudGet("/v1/me/devices") : undefined;
|
|
4854
|
+
if (remote && !remote.ok) {
|
|
4855
|
+
return ok({ local });
|
|
4856
|
+
}
|
|
4857
|
+
return ok({ local, remote: remote?.data });
|
|
4858
|
+
}
|
|
4859
|
+
async function authDeviceList(parsed) {
|
|
4860
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
4861
|
+
const unexpected = parsed.command.slice(3);
|
|
4862
|
+
if (unknownFlag || unexpected.length > 0) {
|
|
4863
|
+
return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_LIST_USAGE}` : AUTH_DEVICE_LIST_USAGE);
|
|
4864
|
+
}
|
|
4865
|
+
return await cloudGet("/v1/me/devices");
|
|
4866
|
+
}
|
|
4867
|
+
async function authDeviceRevoke(parsed) {
|
|
4868
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
4869
|
+
if (unknownFlag) {
|
|
4870
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_REVOKE_USAGE}`);
|
|
4871
|
+
}
|
|
4872
|
+
const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_REVOKE_USAGE);
|
|
4873
|
+
if (!id.ok) {
|
|
4874
|
+
return fail("invalid_args", id.error);
|
|
4875
|
+
}
|
|
4876
|
+
return await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}/revoke`, {});
|
|
4877
|
+
}
|
|
4878
|
+
async function authDeviceRename(parsed) {
|
|
4879
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "label"]);
|
|
4880
|
+
if (unknownFlag) {
|
|
4881
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_RENAME_USAGE}`);
|
|
4882
|
+
}
|
|
4883
|
+
const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_RENAME_USAGE);
|
|
4884
|
+
if (!id.ok) {
|
|
4885
|
+
return fail("invalid_args", id.error);
|
|
4886
|
+
}
|
|
4887
|
+
const label = stringFlag(parsed, "label");
|
|
4888
|
+
if (!label?.trim()) {
|
|
4889
|
+
return fail("invalid_args", AUTH_DEVICE_RENAME_USAGE);
|
|
4890
|
+
}
|
|
4891
|
+
const updated = await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}`, { label: label.trim() });
|
|
4892
|
+
if (updated.ok) {
|
|
4893
|
+
const auth = await readLocalAuthFile();
|
|
4894
|
+
if (auth.device?.device_id === updated.data.device_id) {
|
|
4895
|
+
await writeLocalAuthFile({ ...auth, device: { ...auth.device, label: updated.data.label } });
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
return updated;
|
|
4899
|
+
}
|
|
4900
|
+
async function buildLocalDeviceAuth(parsed, existing) {
|
|
4901
|
+
const fingerprint = await localHardwareFingerprint();
|
|
4902
|
+
const keyPair = existing?.fingerprint_hash === fingerprint.fingerprint_hash && existing.private_key_pem && existing.public_key_pem
|
|
4903
|
+
? { privateKeyPem: existing.private_key_pem, publicKeyPem: existing.public_key_pem }
|
|
4904
|
+
: generateLocalDeviceKeyPair();
|
|
4905
|
+
const label = stringFlag(parsed, "label")?.trim()
|
|
4906
|
+
|| existing?.label
|
|
4907
|
+
|| `${fingerprint.platform} ${fingerprint.arch}`;
|
|
4908
|
+
const device = {
|
|
4909
|
+
device_id: existing?.fingerprint_hash === fingerprint.fingerprint_hash ? existing.device_id : "",
|
|
4910
|
+
fingerprint_hash: fingerprint.fingerprint_hash,
|
|
4911
|
+
private_key_pem: keyPair.privateKeyPem,
|
|
4912
|
+
public_key_pem: keyPair.publicKeyPem,
|
|
4913
|
+
label,
|
|
4914
|
+
platform: fingerprint.platform,
|
|
4915
|
+
arch: fingerprint.arch,
|
|
4916
|
+
hostname_hash: fingerprint.hostname_hash,
|
|
4917
|
+
registered_at: existing?.registered_at
|
|
4918
|
+
};
|
|
4919
|
+
return {
|
|
4920
|
+
device,
|
|
4921
|
+
registration: {
|
|
4922
|
+
fingerprint_hash: fingerprint.fingerprint_hash,
|
|
4923
|
+
public_key: keyPair.publicKeyPem,
|
|
4924
|
+
label,
|
|
4925
|
+
platform: fingerprint.platform,
|
|
4926
|
+
arch: fingerprint.arch,
|
|
4927
|
+
hostname_hash: fingerprint.hostname_hash,
|
|
4928
|
+
client_name: EMBED_CLIENT_NAME,
|
|
4929
|
+
client_version: EMBED_CLIENT_VERSION,
|
|
4930
|
+
metadata: {
|
|
4931
|
+
fingerprint_version: "v1",
|
|
4932
|
+
fingerprint_source: fingerprint.source
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
};
|
|
4936
|
+
}
|
|
4937
|
+
function generateLocalDeviceKeyPair() {
|
|
4938
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
4939
|
+
return {
|
|
4940
|
+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
4941
|
+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString()
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
async function registerLocalDevice(token, body) {
|
|
3525
4945
|
try {
|
|
3526
|
-
const
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
source: "file",
|
|
3531
|
-
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined
|
|
4946
|
+
const bodyText = JSON.stringify(body);
|
|
4947
|
+
const headers = {
|
|
4948
|
+
"content-type": "application/json",
|
|
4949
|
+
authorization: `Bearer ${token}`
|
|
3532
4950
|
};
|
|
4951
|
+
addCloudRequestSignature(headers, "POST", "/v1/me/devices/register", bodyText, token);
|
|
4952
|
+
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/me/devices/register`, {
|
|
4953
|
+
method: "POST",
|
|
4954
|
+
headers,
|
|
4955
|
+
body: bodyText
|
|
4956
|
+
});
|
|
4957
|
+
const parsed = await response.json();
|
|
4958
|
+
return enrichCloudAuthFailure(parsed, true);
|
|
4959
|
+
}
|
|
4960
|
+
catch (error) {
|
|
4961
|
+
return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
|
|
4962
|
+
remediation: `Check that embed cloud-api is running at ${DEFAULT_CLOUD_API_URL}. Start it with: npm run cloud-api`
|
|
4963
|
+
});
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
async function localHardwareFingerprint() {
|
|
4967
|
+
cachedLocalHardwareFingerprint ??= localHardwareFingerprintUncached();
|
|
4968
|
+
return await cachedLocalHardwareFingerprint;
|
|
4969
|
+
}
|
|
4970
|
+
async function localHardwareFingerprintUncached() {
|
|
4971
|
+
const platformName = platform();
|
|
4972
|
+
const archName = arch();
|
|
4973
|
+
const raw = await localHardwareId(platformName);
|
|
4974
|
+
const fingerprintHash = createHash("sha256")
|
|
4975
|
+
.update(`embed-labs:device:v1:${platformName}:${raw.value}`)
|
|
4976
|
+
.digest("hex");
|
|
4977
|
+
const hostnameHash = createHash("sha256")
|
|
4978
|
+
.update(`embed-labs:hostname:v1:${hostname()}`)
|
|
4979
|
+
.digest("hex");
|
|
4980
|
+
return {
|
|
4981
|
+
fingerprint_hash: fingerprintHash,
|
|
4982
|
+
platform: platformName,
|
|
4983
|
+
arch: archName,
|
|
4984
|
+
hostname_hash: hostnameHash,
|
|
4985
|
+
source: raw.source
|
|
4986
|
+
};
|
|
4987
|
+
}
|
|
4988
|
+
async function localHardwareId(platformName) {
|
|
4989
|
+
if (platformName === "darwin") {
|
|
4990
|
+
const result = await runLocalProcess("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
|
|
4991
|
+
const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(result.stdout);
|
|
4992
|
+
if (match?.[1]) {
|
|
4993
|
+
return { value: match[1], source: "macos_ioplatformuuid" };
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
if (platformName === "win32") {
|
|
4997
|
+
const result = await runLocalProcess("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
|
|
4998
|
+
const match = /MachineGuid\s+REG_\w+\s+([^\r\n]+)/.exec(result.stdout);
|
|
4999
|
+
if (match?.[1]?.trim()) {
|
|
5000
|
+
return { value: match[1].trim(), source: "windows_machineguid" };
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
if (platformName === "linux") {
|
|
5004
|
+
for (const pathValue of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
5005
|
+
try {
|
|
5006
|
+
const value = (await readFile(pathValue, "utf8")).trim();
|
|
5007
|
+
if (value) {
|
|
5008
|
+
return { value, source: `linux:${pathValue}` };
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
catch {
|
|
5012
|
+
// Try the next stable machine id location.
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
const generated = await localGeneratedInstallId();
|
|
5017
|
+
return { value: generated, source: "generated_install_id" };
|
|
5018
|
+
}
|
|
5019
|
+
async function localGeneratedInstallId() {
|
|
5020
|
+
try {
|
|
5021
|
+
const parsed = JSON.parse(await readFile(DEFAULT_DEVICE_FILE, "utf8"));
|
|
5022
|
+
if (typeof parsed.generated_install_id === "string" && parsed.generated_install_id.trim()) {
|
|
5023
|
+
return parsed.generated_install_id.trim();
|
|
5024
|
+
}
|
|
3533
5025
|
}
|
|
3534
5026
|
catch {
|
|
3535
|
-
|
|
5027
|
+
// Fall through and create a local-only fallback id.
|
|
3536
5028
|
}
|
|
5029
|
+
const generated = `install_${randomBytes(24).toString("base64url")}`;
|
|
5030
|
+
await mkdir(dirname(DEFAULT_DEVICE_FILE), { recursive: true });
|
|
5031
|
+
await writeFile(DEFAULT_DEVICE_FILE, `${JSON.stringify({ generated_install_id: generated, created_at: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
5032
|
+
await chmod(DEFAULT_DEVICE_FILE, 0o600).catch(() => undefined);
|
|
5033
|
+
return generated;
|
|
3537
5034
|
}
|
|
3538
5035
|
function accountCreateBody(parsed) {
|
|
3539
5036
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "email", "display-name"]);
|
|
@@ -3689,6 +5186,80 @@ function usageRecordBody(parsed) {
|
|
|
3689
5186
|
created_at: createdAtResult.value
|
|
3690
5187
|
});
|
|
3691
5188
|
}
|
|
5189
|
+
function mcpToolEventBody(parsed) {
|
|
5190
|
+
const unknownFlag = firstUnknownFlag(parsed, [
|
|
5191
|
+
"json",
|
|
5192
|
+
"account",
|
|
5193
|
+
"account-id",
|
|
5194
|
+
"tool",
|
|
5195
|
+
"client",
|
|
5196
|
+
"mode",
|
|
5197
|
+
"server-model-used",
|
|
5198
|
+
"success",
|
|
5199
|
+
"local-device-id",
|
|
5200
|
+
"local_device_id",
|
|
5201
|
+
"request-id",
|
|
5202
|
+
"duration-ms",
|
|
5203
|
+
"input-summary",
|
|
5204
|
+
"output-summary"
|
|
5205
|
+
]);
|
|
5206
|
+
if (unknownFlag) {
|
|
5207
|
+
return `Unknown flag --${unknownFlag}. ${MCP_TOOL_EVENT_USAGE}`;
|
|
5208
|
+
}
|
|
5209
|
+
const extra = parsed.command.slice(2);
|
|
5210
|
+
if (extra.length > 0) {
|
|
5211
|
+
return `Unexpected argument: ${extra[0]}. ${MCP_TOOL_EVENT_USAGE}`;
|
|
5212
|
+
}
|
|
5213
|
+
const toolResult = optionalTrimmedStringFlag(parsed, "tool");
|
|
5214
|
+
if (toolResult.error)
|
|
5215
|
+
return toolResult.error;
|
|
5216
|
+
if (!toolResult.value)
|
|
5217
|
+
return MCP_TOOL_EVENT_USAGE;
|
|
5218
|
+
const accountResult = optionalTrimmedStringAliasFlag(parsed, ["account", "account-id"], "account or account-id");
|
|
5219
|
+
if (accountResult.error)
|
|
5220
|
+
return accountResult.error;
|
|
5221
|
+
const clientResult = optionalTrimmedStringFlag(parsed, "client");
|
|
5222
|
+
if (clientResult.error)
|
|
5223
|
+
return clientResult.error;
|
|
5224
|
+
const modeResult = optionalTrimmedStringFlag(parsed, "mode");
|
|
5225
|
+
if (modeResult.error)
|
|
5226
|
+
return modeResult.error;
|
|
5227
|
+
const localDeviceResult = optionalTrimmedStringAliasFlag(parsed, ["local-device-id", "local_device_id"], "local-device-id");
|
|
5228
|
+
if (localDeviceResult.error)
|
|
5229
|
+
return localDeviceResult.error;
|
|
5230
|
+
const requestIdResult = optionalTrimmedStringFlag(parsed, "request-id");
|
|
5231
|
+
if (requestIdResult.error)
|
|
5232
|
+
return requestIdResult.error;
|
|
5233
|
+
const inputSummaryResult = optionalTrimmedStringFlag(parsed, "input-summary");
|
|
5234
|
+
if (inputSummaryResult.error)
|
|
5235
|
+
return inputSummaryResult.error;
|
|
5236
|
+
const outputSummaryResult = optionalTrimmedStringFlag(parsed, "output-summary");
|
|
5237
|
+
if (outputSummaryResult.error)
|
|
5238
|
+
return outputSummaryResult.error;
|
|
5239
|
+
const durationResult = optionalNonNegativeIntegerFlag(parsed, "duration-ms");
|
|
5240
|
+
if (durationResult.error)
|
|
5241
|
+
return durationResult.error;
|
|
5242
|
+
const serverModelUsed = optionalBooleanFlag(parsed, "server-model-used");
|
|
5243
|
+
if (typeof serverModelUsed === "string")
|
|
5244
|
+
return serverModelUsed;
|
|
5245
|
+
const success = optionalBooleanFlag(parsed, "success");
|
|
5246
|
+
if (typeof success === "string")
|
|
5247
|
+
return success;
|
|
5248
|
+
return compactBody({
|
|
5249
|
+
account_id: accountResult.value,
|
|
5250
|
+
tool_name: toolResult.value,
|
|
5251
|
+
client: clientResult.value,
|
|
5252
|
+
mode: modeResult.value,
|
|
5253
|
+
local_device_id: localDeviceResult.value,
|
|
5254
|
+
server_model_used: serverModelUsed,
|
|
5255
|
+
success,
|
|
5256
|
+
request_id: requestIdResult.value,
|
|
5257
|
+
duration_ms: durationResult.value,
|
|
5258
|
+
input_summary: inputSummaryResult.value,
|
|
5259
|
+
output_summary: outputSummaryResult.value,
|
|
5260
|
+
metadata: localDeviceResult.value ? { local_device_id: localDeviceResult.value } : undefined
|
|
5261
|
+
});
|
|
5262
|
+
}
|
|
3692
5263
|
function usageSummaryRequest(parsed) {
|
|
3693
5264
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "account", "account-id", "api-key-id", "from", "to"]);
|
|
3694
5265
|
if (unknownFlag) {
|
|
@@ -4024,8 +5595,109 @@ function billingSnapshotListRequest(parsed) {
|
|
|
4024
5595
|
}
|
|
4025
5596
|
return { path: `/v1/accounts/${encodeURIComponent(accountResult.value)}/billing/snapshots` };
|
|
4026
5597
|
}
|
|
5598
|
+
function localToolchainListRequest(parsed) {
|
|
5599
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "install-root"]);
|
|
5600
|
+
if (unknownFlag) {
|
|
5601
|
+
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_LIST_USAGE}`;
|
|
5602
|
+
}
|
|
5603
|
+
const extra = parsed.command.slice(3);
|
|
5604
|
+
if (extra.length > 0) {
|
|
5605
|
+
return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_LIST_USAGE}`;
|
|
5606
|
+
}
|
|
5607
|
+
const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
|
|
5608
|
+
if (board.error)
|
|
5609
|
+
return board.error;
|
|
5610
|
+
const channel = optionalTrimmedStringFlag(parsed, "channel");
|
|
5611
|
+
if (channel.error)
|
|
5612
|
+
return channel.error;
|
|
5613
|
+
const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
|
|
5614
|
+
if (metadataRoot.error)
|
|
5615
|
+
return metadataRoot.error;
|
|
5616
|
+
const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
|
|
5617
|
+
if (installRoot.error)
|
|
5618
|
+
return installRoot.error;
|
|
5619
|
+
return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value, installRoot: installRoot.value };
|
|
5620
|
+
}
|
|
5621
|
+
function localToolchainLatestRequest(parsed) {
|
|
5622
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root"]);
|
|
5623
|
+
if (unknownFlag) {
|
|
5624
|
+
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_LATEST_USAGE}`;
|
|
5625
|
+
}
|
|
5626
|
+
const extra = parsed.command.slice(3);
|
|
5627
|
+
if (extra.length > 0) {
|
|
5628
|
+
return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_LATEST_USAGE}`;
|
|
5629
|
+
}
|
|
5630
|
+
const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
|
|
5631
|
+
if (board.error)
|
|
5632
|
+
return board.error;
|
|
5633
|
+
const channel = optionalTrimmedStringFlag(parsed, "channel");
|
|
5634
|
+
if (channel.error)
|
|
5635
|
+
return channel.error;
|
|
5636
|
+
const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
|
|
5637
|
+
if (metadataRoot.error)
|
|
5638
|
+
return metadataRoot.error;
|
|
5639
|
+
return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value };
|
|
5640
|
+
}
|
|
5641
|
+
function localToolchainCurrentRequest(parsed) {
|
|
5642
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "install-root"]);
|
|
5643
|
+
if (unknownFlag) {
|
|
5644
|
+
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_CURRENT_USAGE}`;
|
|
5645
|
+
}
|
|
5646
|
+
const extra = parsed.command.slice(3);
|
|
5647
|
+
if (extra.length > 0) {
|
|
5648
|
+
return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_CURRENT_USAGE}`;
|
|
5649
|
+
}
|
|
5650
|
+
const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
|
|
5651
|
+
if (installRoot.error)
|
|
5652
|
+
return installRoot.error;
|
|
5653
|
+
return { installRoot: installRoot.value };
|
|
5654
|
+
}
|
|
5655
|
+
function localToolchainInstallRequest(parsed) {
|
|
5656
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "mode", "force"]);
|
|
5657
|
+
if (unknownFlag) {
|
|
5658
|
+
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_INSTALL_USAGE}`;
|
|
5659
|
+
}
|
|
5660
|
+
const extra = parsed.command.slice(3);
|
|
5661
|
+
if (extra.length > 0) {
|
|
5662
|
+
return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_INSTALL_USAGE}`;
|
|
5663
|
+
}
|
|
5664
|
+
const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
|
|
5665
|
+
if (board.error)
|
|
5666
|
+
return board.error;
|
|
5667
|
+
const channel = optionalTrimmedStringFlag(parsed, "channel");
|
|
5668
|
+
if (channel.error)
|
|
5669
|
+
return channel.error;
|
|
5670
|
+
const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
|
|
5671
|
+
if (metadataRoot.error)
|
|
5672
|
+
return metadataRoot.error;
|
|
5673
|
+
const sourceUrl = optionalTrimmedStringFlag(parsed, "source-url");
|
|
5674
|
+
if (sourceUrl.error)
|
|
5675
|
+
return sourceUrl.error;
|
|
5676
|
+
const sourceReleaseRoot = optionalTrimmedStringFlag(parsed, "source-release-root");
|
|
5677
|
+
if (sourceReleaseRoot.error)
|
|
5678
|
+
return sourceReleaseRoot.error;
|
|
5679
|
+
if (sourceUrl.value && sourceReleaseRoot.value) {
|
|
5680
|
+
return "Use only one of --source-url or --source-release-root.";
|
|
5681
|
+
}
|
|
5682
|
+
const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
|
|
5683
|
+
if (installRoot.error)
|
|
5684
|
+
return installRoot.error;
|
|
5685
|
+
const mode = optionalTrimmedStringFlag(parsed, "mode");
|
|
5686
|
+
if (mode.error)
|
|
5687
|
+
return mode.error;
|
|
5688
|
+
return {
|
|
5689
|
+
boardId: board.value,
|
|
5690
|
+
channel: channel.value,
|
|
5691
|
+
metadataRoot: metadataRoot.value,
|
|
5692
|
+
sourceUrl: sourceUrl.value,
|
|
5693
|
+
sourceReleaseRoot: sourceReleaseRoot.value,
|
|
5694
|
+
installRoot: installRoot.value,
|
|
5695
|
+
mode: mode.value,
|
|
5696
|
+
force: booleanFlag(parsed, "force")
|
|
5697
|
+
};
|
|
5698
|
+
}
|
|
4027
5699
|
function localToolchainValidateRequest(parsed) {
|
|
4028
|
-
const unknownFlag = firstUnknownFlag(parsed, ["json", "release-root"]);
|
|
5700
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "release-root", "mode"]);
|
|
4029
5701
|
if (unknownFlag) {
|
|
4030
5702
|
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_VALIDATE_USAGE}`;
|
|
4031
5703
|
}
|
|
@@ -4037,7 +5709,11 @@ function localToolchainValidateRequest(parsed) {
|
|
|
4037
5709
|
if (releaseRoot.error) {
|
|
4038
5710
|
return releaseRoot.error;
|
|
4039
5711
|
}
|
|
4040
|
-
|
|
5712
|
+
const mode = optionalTrimmedStringFlag(parsed, "mode");
|
|
5713
|
+
if (mode.error) {
|
|
5714
|
+
return mode.error;
|
|
5715
|
+
}
|
|
5716
|
+
return { releaseRoot: releaseRoot.value, mode: mode.value };
|
|
4041
5717
|
}
|
|
4042
5718
|
function localCompileTaishanPiRequest(parsed, auth) {
|
|
4043
5719
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "source", "output", "release-root", "account", "account-id"]);
|
|
@@ -4122,7 +5798,9 @@ function localToolchainAuthContext(auth, accountId) {
|
|
|
4122
5798
|
authenticated: auth.authenticated,
|
|
4123
5799
|
profile: auth.profile,
|
|
4124
5800
|
source: auth.source,
|
|
4125
|
-
account_id: accountId
|
|
5801
|
+
account_id: accountId,
|
|
5802
|
+
api_key_id: auth.api_key_id,
|
|
5803
|
+
device_id: auth.device_id
|
|
4126
5804
|
};
|
|
4127
5805
|
}
|
|
4128
5806
|
function usageEventsRequest(parsed) {
|
|
@@ -4532,6 +6210,35 @@ function renderPluginList(result) {
|
|
|
4532
6210
|
`install="${plugin.install_command}"`
|
|
4533
6211
|
].filter(Boolean).join(" ")).join("\n");
|
|
4534
6212
|
}
|
|
6213
|
+
function renderPluginUpdateCheck(result) {
|
|
6214
|
+
const lines = [
|
|
6215
|
+
`release_url=${result.release_url}`,
|
|
6216
|
+
result.latest_version ? `latest_version=${result.latest_version}` : ""
|
|
6217
|
+
].filter(Boolean);
|
|
6218
|
+
if (result.release_notes.length > 0) {
|
|
6219
|
+
lines.push("release_notes:");
|
|
6220
|
+
for (const note of result.release_notes) {
|
|
6221
|
+
lines.push(` - ${note}`);
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
for (const plugin of result.plugins) {
|
|
6225
|
+
lines.push("");
|
|
6226
|
+
lines.push(`${plugin.display_name} (${plugin.id})`);
|
|
6227
|
+
lines.push(` installed=${plugin.installed}`);
|
|
6228
|
+
lines.push(` installed_version=${plugin.installed_version ?? "none"}`);
|
|
6229
|
+
lines.push(` latest_version=${plugin.latest_version ?? "unknown"}`);
|
|
6230
|
+
lines.push(` update_available=${plugin.update_available}`);
|
|
6231
|
+
lines.push(` target=${plugin.target_path}`);
|
|
6232
|
+
lines.push(` update_command=${plugin.update_command}`);
|
|
6233
|
+
if (plugin.release_file) {
|
|
6234
|
+
lines.push(` release_file=${plugin.release_file}`);
|
|
6235
|
+
}
|
|
6236
|
+
for (const note of plugin.notes) {
|
|
6237
|
+
lines.push(` note=${note}`);
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
return lines.join("\n");
|
|
6241
|
+
}
|
|
4535
6242
|
function renderPluginInstall(result) {
|
|
4536
6243
|
const lines = ["Installed plugins:"];
|
|
4537
6244
|
for (const item of result.installed) {
|
|
@@ -4553,6 +6260,15 @@ function renderPluginInstall(result) {
|
|
|
4553
6260
|
if (item.mcp_warning) {
|
|
4554
6261
|
lines.push(` warning=${item.mcp_warning}`);
|
|
4555
6262
|
}
|
|
6263
|
+
if (item.marketplace_registered !== undefined) {
|
|
6264
|
+
lines.push(` codex_marketplace_registered=${item.marketplace_registered}`);
|
|
6265
|
+
}
|
|
6266
|
+
if (item.marketplace_path) {
|
|
6267
|
+
lines.push(` codex_marketplace=${item.marketplace_path}`);
|
|
6268
|
+
}
|
|
6269
|
+
if (item.marketplace_warning) {
|
|
6270
|
+
lines.push(` warning=${item.marketplace_warning}`);
|
|
6271
|
+
}
|
|
4556
6272
|
}
|
|
4557
6273
|
return lines.join("\n");
|
|
4558
6274
|
}
|
|
@@ -4584,9 +6300,53 @@ function renderAgentRunResult(result) {
|
|
|
4584
6300
|
return lines.join("\n");
|
|
4585
6301
|
}
|
|
4586
6302
|
function renderAuthStatus(status) {
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
6303
|
+
if (!status.authenticated) {
|
|
6304
|
+
return `Not authenticated profile=${status.profile}`;
|
|
6305
|
+
}
|
|
6306
|
+
return [
|
|
6307
|
+
`Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`,
|
|
6308
|
+
status.account_id ? `account=${status.account_id}` : "",
|
|
6309
|
+
status.api_key_id ? `api_key=${status.api_key_id}` : "",
|
|
6310
|
+
status.device_id ? `device=${status.device_id}` : "device=not_registered",
|
|
6311
|
+
status.device_label ? `device_label=${status.device_label}` : "",
|
|
6312
|
+
status.device_integrity ? `device_integrity=${status.device_integrity}` : "",
|
|
6313
|
+
status.device_private_key_configured === false ? "device_private_key=missing" : ""
|
|
6314
|
+
].filter(Boolean).join("\n");
|
|
6315
|
+
}
|
|
6316
|
+
function renderAuthDeviceStatus(status) {
|
|
6317
|
+
const lines = [renderAuthStatus(status.local)];
|
|
6318
|
+
if (status.remote) {
|
|
6319
|
+
const activeCount = status.remote.devices.filter((device) => device.status === "active").length;
|
|
6320
|
+
lines.push(`remote_devices=${activeCount}/${status.remote.device_limit}`);
|
|
6321
|
+
const localDevice = status.local.device_id
|
|
6322
|
+
? status.remote.devices.find((device) => device.device_id === status.local.device_id)
|
|
6323
|
+
: undefined;
|
|
6324
|
+
if (localDevice) {
|
|
6325
|
+
lines.push(`remote_current=${renderAuthDevice(localDevice)}`);
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
return lines.join("\n");
|
|
6329
|
+
}
|
|
6330
|
+
function renderAuthDeviceList(result) {
|
|
6331
|
+
if (result.devices.length === 0) {
|
|
6332
|
+
return `No registered devices. device_limit=${result.device_limit}`;
|
|
6333
|
+
}
|
|
6334
|
+
return [
|
|
6335
|
+
`device_limit=${result.device_limit}`,
|
|
6336
|
+
...result.devices.map(renderAuthDevice)
|
|
6337
|
+
].join("\n");
|
|
6338
|
+
}
|
|
6339
|
+
function renderAuthDevice(device) {
|
|
6340
|
+
return [
|
|
6341
|
+
`${device.device_id} account=${device.account_id}`,
|
|
6342
|
+
device.api_key_id ? `api_key=${device.api_key_id}` : "",
|
|
6343
|
+
`status=${device.status}`,
|
|
6344
|
+
device.label ? `label=${device.label}` : "",
|
|
6345
|
+
device.platform ? `platform=${device.platform}` : "",
|
|
6346
|
+
device.arch ? `arch=${device.arch}` : "",
|
|
6347
|
+
`last_seen_at=${device.last_seen_at}`,
|
|
6348
|
+
device.revoked_at ? `revoked_at=${device.revoked_at}` : ""
|
|
6349
|
+
].filter(Boolean).join(" ");
|
|
4590
6350
|
}
|
|
4591
6351
|
function renderAccount(account) {
|
|
4592
6352
|
return [
|
|
@@ -4636,6 +6396,20 @@ function renderUsageRecord(record) {
|
|
|
4636
6396
|
`created_at=${record.created_at}`
|
|
4637
6397
|
].filter(Boolean).join(" ");
|
|
4638
6398
|
}
|
|
6399
|
+
function renderMcpToolEvent(event) {
|
|
6400
|
+
return [
|
|
6401
|
+
`${event.event_id} tool=${event.tool_name}`,
|
|
6402
|
+
event.account_id ? `account=${event.account_id}` : "",
|
|
6403
|
+
event.api_key_id ? `api_key=${event.api_key_id}` : "",
|
|
6404
|
+
`client=${event.client}`,
|
|
6405
|
+
`mode=${event.mode}`,
|
|
6406
|
+
`server_model_used=${event.server_model_used}`,
|
|
6407
|
+
`success=${event.success}`,
|
|
6408
|
+
event.request_id ? `request=${event.request_id}` : "",
|
|
6409
|
+
event.duration_ms !== undefined ? `duration_ms=${event.duration_ms}` : "",
|
|
6410
|
+
`created_at=${event.created_at}`
|
|
6411
|
+
].filter(Boolean).join(" ");
|
|
6412
|
+
}
|
|
4639
6413
|
function renderUsageSummary(summary) {
|
|
4640
6414
|
const lines = [
|
|
4641
6415
|
summary.account_id ? `account=${summary.account_id}` : "",
|
|
@@ -5159,10 +6933,131 @@ function renderBuildWorkspaceSourcePatch(result) {
|
|
|
5159
6933
|
}
|
|
5160
6934
|
return lines.join("\n");
|
|
5161
6935
|
}
|
|
6936
|
+
function renderLocalToolchainList(result) {
|
|
6937
|
+
const lines = [
|
|
6938
|
+
`Local development environments: ${result.environments.length}`,
|
|
6939
|
+
`host=${result.host}`,
|
|
6940
|
+
`channel=${result.channel}`,
|
|
6941
|
+
result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in",
|
|
6942
|
+
`install_root=${result.install_root}`,
|
|
6943
|
+
`registry=${result.registry_path}`
|
|
6944
|
+
];
|
|
6945
|
+
for (const environment of result.environments) {
|
|
6946
|
+
lines.push("");
|
|
6947
|
+
lines.push(`${environment.display_name} (${environment.board_id})`);
|
|
6948
|
+
lines.push(` status=${environment.status}`);
|
|
6949
|
+
lines.push(` latest=${environment.latest.version}`);
|
|
6950
|
+
if (environment.installed) {
|
|
6951
|
+
lines.push(` installed=${environment.installed.version ?? "unknown"} mode=${environment.installed.mode ?? "unknown"}`);
|
|
6952
|
+
if (environment.installed.release_root) {
|
|
6953
|
+
lines.push(` release_root=${environment.installed.release_root}`);
|
|
6954
|
+
}
|
|
6955
|
+
}
|
|
6956
|
+
lines.push(` install_modes=${environment.install_modes.join(",")}`);
|
|
6957
|
+
lines.push(` install_command=${environment.install_command}`);
|
|
6958
|
+
if (environment.status === "update_available") {
|
|
6959
|
+
lines.push(` update_command=${environment.update_command}`);
|
|
6960
|
+
}
|
|
6961
|
+
if (environment.components?.length) {
|
|
6962
|
+
lines.push(` components=${environment.components.map((component) => `${component.id}@${component.version}`).join(", ")}`);
|
|
6963
|
+
}
|
|
6964
|
+
if (environment.notes.length > 0) {
|
|
6965
|
+
for (const note of environment.notes) {
|
|
6966
|
+
lines.push(` note=${note}`);
|
|
6967
|
+
}
|
|
6968
|
+
}
|
|
6969
|
+
}
|
|
6970
|
+
return lines.join("\n");
|
|
6971
|
+
}
|
|
6972
|
+
function renderLocalToolchainLatest(result) {
|
|
6973
|
+
const lines = [
|
|
6974
|
+
`board=${result.board_id}`,
|
|
6975
|
+
`channel=${result.channel}`,
|
|
6976
|
+
`version=${result.version}`,
|
|
6977
|
+
`host=${result.host}`,
|
|
6978
|
+
result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in",
|
|
6979
|
+
result.download?.source_url ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
|
|
6980
|
+
result.download?.archive ? `archive_sha256=${result.download.archive.sha256}` : "",
|
|
6981
|
+
result.download?.archive ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
|
|
6982
|
+
result.download?.components?.length ? `components=${result.download.components.length}` : "",
|
|
6983
|
+
result.download?.default_mode ? `default_mode=${result.download.default_mode}` : "",
|
|
6984
|
+
result.download_error ? `download_error=${result.download_error}` : ""
|
|
6985
|
+
].filter(Boolean);
|
|
6986
|
+
if (result.download?.components?.length) {
|
|
6987
|
+
lines.push("download_components:");
|
|
6988
|
+
for (const component of result.download.components) {
|
|
6989
|
+
lines.push(` ${component.id}@${component.version} modes=${component.install_modes?.join(",") || "all"} bytes=${component.archive.size_bytes}`);
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
if (result.packages.length > 0) {
|
|
6993
|
+
lines.push("packages:");
|
|
6994
|
+
for (const pkg of result.packages) {
|
|
6995
|
+
lines.push(` ${pkg.id}@${pkg.version}`);
|
|
6996
|
+
}
|
|
6997
|
+
}
|
|
6998
|
+
return lines.join("\n");
|
|
6999
|
+
}
|
|
7000
|
+
function renderLocalToolchainCurrent(result) {
|
|
7001
|
+
if (!result.installed) {
|
|
7002
|
+
return [
|
|
7003
|
+
"No local toolchain installed.",
|
|
7004
|
+
`board=${result.board_id}`,
|
|
7005
|
+
`install_root=${result.install_root}`,
|
|
7006
|
+
`registry=${result.registry_path}`
|
|
7007
|
+
].join("\n");
|
|
7008
|
+
}
|
|
7009
|
+
return [
|
|
7010
|
+
"Local toolchain installed.",
|
|
7011
|
+
`board=${result.board_id}`,
|
|
7012
|
+
result.version ? `version=${result.version}` : "",
|
|
7013
|
+
result.channel ? `channel=${result.channel}` : "",
|
|
7014
|
+
result.mode ? `mode=${result.mode}` : "",
|
|
7015
|
+
result.release_root ? `release_root=${result.release_root}` : "",
|
|
7016
|
+
`install_root=${result.install_root}`,
|
|
7017
|
+
`registry=${result.registry_path}`
|
|
7018
|
+
].filter(Boolean).join("\n");
|
|
7019
|
+
}
|
|
7020
|
+
function renderLocalToolchainInstall(result) {
|
|
7021
|
+
const lines = [
|
|
7022
|
+
"Local toolchain installed.",
|
|
7023
|
+
`board=${result.board_id}`,
|
|
7024
|
+
`version=${result.version}`,
|
|
7025
|
+
`channel=${result.channel}`,
|
|
7026
|
+
`host=${result.host}`,
|
|
7027
|
+
`mode=${result.mode}`,
|
|
7028
|
+
`install_root=${result.install_root}`,
|
|
7029
|
+
`release_root=${result.release_root}`,
|
|
7030
|
+
`registry=${result.registry_path}`,
|
|
7031
|
+
`source=${result.source.kind}:${result.source.value}`,
|
|
7032
|
+
result.source.downloaded_path ? `downloaded=${result.source.downloaded_path}` : "",
|
|
7033
|
+
result.source.components?.length ? `components=${result.source.components.length}` : "",
|
|
7034
|
+
`validation=${result.validation.ok ? "ok" : "failed"}`
|
|
7035
|
+
].filter(Boolean);
|
|
7036
|
+
if (result.source.components?.length) {
|
|
7037
|
+
lines.push("installed_components:");
|
|
7038
|
+
for (const component of result.source.components) {
|
|
7039
|
+
lines.push(` ${component.id}@${component.version} ${component.mirror_kind || ""} bytes=${component.size_bytes}`);
|
|
7040
|
+
}
|
|
7041
|
+
}
|
|
7042
|
+
if (result.installed_paths.length > 0) {
|
|
7043
|
+
lines.push("installed_paths:");
|
|
7044
|
+
for (const installedPath of result.installed_paths) {
|
|
7045
|
+
lines.push(` ${installedPath}`);
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
7048
|
+
if (result.packages.length > 0) {
|
|
7049
|
+
lines.push("packages:");
|
|
7050
|
+
for (const pkg of result.packages) {
|
|
7051
|
+
lines.push(` ${pkg.id}@${pkg.version}`);
|
|
7052
|
+
}
|
|
7053
|
+
}
|
|
7054
|
+
return lines.join("\n");
|
|
7055
|
+
}
|
|
5162
7056
|
function renderLocalToolchainValidation(result) {
|
|
5163
7057
|
const lines = [
|
|
5164
7058
|
result.ok ? "Local toolchain ready." : "Local toolchain not ready.",
|
|
5165
7059
|
`board=${result.board_id}`,
|
|
7060
|
+
`mode=${result.mode}`,
|
|
5166
7061
|
`host=${result.host.platform}/${result.host.arch}`,
|
|
5167
7062
|
`release_root=${result.release_root}`
|
|
5168
7063
|
];
|
|
@@ -5617,6 +7512,22 @@ function stringFlag(parsed, name) {
|
|
|
5617
7512
|
function booleanFlag(parsed, name) {
|
|
5618
7513
|
return parsed.flags[name] === true;
|
|
5619
7514
|
}
|
|
7515
|
+
function optionalBooleanFlag(parsed, name) {
|
|
7516
|
+
const values = flagValues(parsed, name);
|
|
7517
|
+
if (values.length === 0)
|
|
7518
|
+
return undefined;
|
|
7519
|
+
const value = values[values.length - 1];
|
|
7520
|
+
if (value === true)
|
|
7521
|
+
return true;
|
|
7522
|
+
if (typeof value !== "string")
|
|
7523
|
+
return `--${name} must be true or false.`;
|
|
7524
|
+
const normalized = value.trim().toLowerCase();
|
|
7525
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized))
|
|
7526
|
+
return true;
|
|
7527
|
+
if (["0", "false", "no", "n", "off"].includes(normalized))
|
|
7528
|
+
return false;
|
|
7529
|
+
return `--${name} must be true or false.`;
|
|
7530
|
+
}
|
|
5620
7531
|
function switchFlag(parsed, name) {
|
|
5621
7532
|
const values = flagValues(parsed, name);
|
|
5622
7533
|
for (const value of values) {
|
|
@@ -5915,12 +7826,16 @@ Main workflow:
|
|
|
5915
7826
|
3. Inspect server model routing:
|
|
5916
7827
|
embed plugin install codex
|
|
5917
7828
|
embed plugin install opencode
|
|
7829
|
+
embed plugin update check
|
|
5918
7830
|
embed service modes
|
|
5919
7831
|
embed model list
|
|
5920
7832
|
embed model default
|
|
5921
7833
|
4. Run a natural-language local tool loop:
|
|
5922
7834
|
embed agent run --prompt "验证开发板状态"
|
|
5923
7835
|
5. Validate or use the local TaishanPi toolchain:
|
|
7836
|
+
embed local toolchain list
|
|
7837
|
+
embed local toolchain latest
|
|
7838
|
+
embed local toolchain install
|
|
5924
7839
|
embed local toolchain validate
|
|
5925
7840
|
embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
|
|
5926
7841
|
embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
|
|
@@ -5963,9 +7878,14 @@ Local hardware:
|
|
|
5963
7878
|
embed tool list
|
|
5964
7879
|
embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22,15301]}'
|
|
5965
7880
|
embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}'
|
|
7881
|
+
embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve
|
|
5966
7882
|
embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}'
|
|
5967
7883
|
embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}'
|
|
5968
7884
|
embed device list
|
|
7885
|
+
embed local toolchain list
|
|
7886
|
+
embed local toolchain latest
|
|
7887
|
+
embed local toolchain current
|
|
7888
|
+
embed local toolchain install
|
|
5969
7889
|
embed local toolchain validate
|
|
5970
7890
|
embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
|
|
5971
7891
|
embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
|
|
@@ -5979,7 +7899,7 @@ Help:
|
|
|
5979
7899
|
|
|
5980
7900
|
Environment:
|
|
5981
7901
|
EMBED_BRIDGE_URL=http://127.0.0.1:18083
|
|
5982
|
-
EMBED_CLOUD_API_URL=
|
|
7902
|
+
EMBED_CLOUD_API_URL=https://api.embedboard.com
|
|
5983
7903
|
EMBED_API_TOKEN=<token>
|
|
5984
7904
|
CODEX_HOME=~/.codex
|
|
5985
7905
|
|
|
@@ -6034,6 +7954,8 @@ Install local AI client plugins explicitly:
|
|
|
6034
7954
|
embed plugin list
|
|
6035
7955
|
embed plugin install codex
|
|
6036
7956
|
embed plugin install opencode
|
|
7957
|
+
embed plugin update check
|
|
7958
|
+
embed plugin update all
|
|
6037
7959
|
|
|
6038
7960
|
Cloud build path:
|
|
6039
7961
|
|
|
@@ -6112,6 +8034,8 @@ Usage:
|
|
|
6112
8034
|
embed auth logout [--json]
|
|
6113
8035
|
embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]
|
|
6114
8036
|
embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]
|
|
8037
|
+
embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
|
|
8038
|
+
embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
|
|
6115
8039
|
embed service modes [--json]
|
|
6116
8040
|
embed model list [--json]
|
|
6117
8041
|
embed model default [--json]
|
|
@@ -6167,7 +8091,12 @@ Usage:
|
|
|
6167
8091
|
embed build image generate --workspace <workspace_id> --prompt <request> [--account <account_id>] [--image-profile <profile_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--execution-mode cloud_worker|dry_run] [--worker-pool <pool>] [--json]
|
|
6168
8092
|
embed build image boot-logo --logo <local_image> [--account <account_id>] [--project <project_id>] [--board taishanpi] [--variant 1M-RK3566] [--kernel-logo <local_image>] [--rotate -90] [--scale 100] [--output <package.json>] [--json]
|
|
6169
8093
|
embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]
|
|
6170
|
-
embed local toolchain
|
|
8094
|
+
embed local toolchain list [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
|
|
8095
|
+
embed local toolchain latest [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--json]
|
|
8096
|
+
embed local toolchain current [--install-root <path>] [--json]
|
|
8097
|
+
embed local toolchain install [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--mode minimal|compile|qt|full|images] [--force] [--json]
|
|
8098
|
+
Defaults to the production download channel at download.embedboard.com.
|
|
8099
|
+
embed local toolchain validate [--release-root <path>] [--mode minimal|compile|qt|full|images] [--json]
|
|
6171
8100
|
embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]
|
|
6172
8101
|
embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]
|
|
6173
8102
|
embed debug tools [--json]
|
|
@@ -6178,6 +8107,18 @@ Usage:
|
|
|
6178
8107
|
embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
|
|
6179
8108
|
embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
|
|
6180
8109
|
embed tool call qml.runtime.start --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
|
|
8110
|
+
embed tool call rp2350.monitor.capabilities [--json]
|
|
8111
|
+
embed tool call rp2350.monitor.status [--json]
|
|
8112
|
+
embed tool call rp2350.monitor.gpio.read --input-json '{"pins":[16,17]}' --approve [--json]
|
|
8113
|
+
embed tool call rp2350.monitor.gpio.write --input-json '{"pin":16,"level":true}' --approve [--json]
|
|
8114
|
+
embed tool call rp2350.monitor.uart.write --input-json '{"baud":115200,"text":"hello","line_ending":"lf"}' --approve [--json]
|
|
8115
|
+
embed tool call rp2350.monitor.i2c.transfer --input-json '{"address":"0x50","write":"00","read_len":4}' --approve [--json]
|
|
8116
|
+
embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve [--json]
|
|
8117
|
+
embed tool call rp2350.monitor.logic.capture --input-json '{"pin_base":16,"pin_count":4,"sample_rate":1000000,"samples":4096}' --approve [--json]
|
|
8118
|
+
embed tool call rp2350.monitor.logic.decode --input-json '{"input_path":".embed-labs/rp2350-monitor/captures/logic.jsonl","decoder":"summary"}' [--json]
|
|
8119
|
+
embed tool call rp2350.monitor.wifi.manage --input-json '{"action":"scan"}' --approve [--json]
|
|
8120
|
+
embed tool call rp2350.monitor.probe.debug --input-json '{"action":"status"}' --approve [--json]
|
|
8121
|
+
embed tool call rp2350.monitor.operation --input-json '{"action":"logic.stop","params":{}}' --approve [--json]
|
|
6181
8122
|
embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
|
|
6182
8123
|
embed board deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
|
|
6183
8124
|
embed device list [--json]
|
|
@@ -6211,7 +8152,7 @@ Usage:
|
|
|
6211
8152
|
|
|
6212
8153
|
Environment:
|
|
6213
8154
|
EMBED_BRIDGE_URL=http://127.0.0.1:18083
|
|
6214
|
-
EMBED_CLOUD_API_URL=
|
|
8155
|
+
EMBED_CLOUD_API_URL=https://api.embedboard.com
|
|
6215
8156
|
EMBED_API_TOKEN=<token>
|
|
6216
8157
|
CODEX_HOME=~/.codex
|
|
6217
8158
|
`);
|