@kvell007/embed-labs-cli 0.1.0-alpha.12 → 0.1.0-alpha.14

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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { createHash } from "node:crypto";
2
+ import { createHash, createHmac, randomBytes } 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, cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
6
6
  import { createRequire } from "node:module";
7
7
  import { homedir, tmpdir } from "node:os";
8
8
  import { basename, delimiter, dirname, join, resolve } from "node:path";
@@ -1351,17 +1351,29 @@ function isApiResponse(value) {
1351
1351
  return isJsonObject(error) && typeof error.code === "string" && typeof error.message === "string";
1352
1352
  }
1353
1353
  async function bridgeGet(path) {
1354
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`);
1354
+ const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1355
+ headers: bridgeHeaders()
1356
+ });
1355
1357
  return await response.json();
1356
1358
  }
1357
1359
  async function bridgePost(path, body) {
1358
1360
  const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1359
1361
  method: "POST",
1360
- headers: { "content-type": "application/json" },
1362
+ headers: bridgeHeaders({ "content-type": "application/json" }),
1361
1363
  body: JSON.stringify(body)
1362
1364
  });
1363
1365
  return await response.json();
1364
1366
  }
1367
+ function bridgeHeaders(base = {}) {
1368
+ const token = process.env.EMBED_BRIDGE_TOKEN?.trim();
1369
+ if (!token) {
1370
+ return base;
1371
+ }
1372
+ return {
1373
+ ...base,
1374
+ authorization: `Bearer ${token}`
1375
+ };
1376
+ }
1365
1377
  async function cloudGet(path) {
1366
1378
  return await cloudRequest("GET", path);
1367
1379
  }
@@ -1374,6 +1386,7 @@ async function cloudDownloadArtifact(artifactId, outputPath) {
1374
1386
  const token = await cloudAuthToken();
1375
1387
  if (token) {
1376
1388
  headers.authorization = `Bearer ${token}`;
1389
+ addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", token);
1377
1390
  }
1378
1391
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
1379
1392
  headers: Object.keys(headers).length > 0 ? headers : undefined
@@ -1409,17 +1422,19 @@ async function cloudDownloadArtifact(artifactId, outputPath) {
1409
1422
  async function cloudRequest(method, path, body) {
1410
1423
  try {
1411
1424
  const headers = {};
1412
- if (body !== undefined) {
1425
+ const bodyText = body === undefined ? "" : JSON.stringify(body);
1426
+ if (bodyText) {
1413
1427
  headers["content-type"] = "application/json";
1414
1428
  }
1415
1429
  const token = await cloudAuthToken();
1416
1430
  if (token) {
1417
1431
  headers.authorization = `Bearer ${token}`;
1432
+ addCloudRequestSignature(headers, method, path, bodyText, token);
1418
1433
  }
1419
1434
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
1420
1435
  method,
1421
1436
  headers: Object.keys(headers).length > 0 ? headers : undefined,
1422
- body: body === undefined ? undefined : JSON.stringify(body)
1437
+ body: body === undefined ? undefined : bodyText
1423
1438
  });
1424
1439
  return await response.json();
1425
1440
  }
@@ -1429,6 +1444,39 @@ async function cloudRequest(method, path, body) {
1429
1444
  });
1430
1445
  }
1431
1446
  }
1447
+ function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token) {
1448
+ if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
1449
+ return;
1450
+ }
1451
+ const timestamp = String(Math.floor(Date.now() / 1000));
1452
+ const nonce = randomBytes(16).toString("hex");
1453
+ const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1454
+ const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1455
+ const canonical = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1456
+ headers["x-embed-key-id"] = keyId;
1457
+ headers["x-embed-timestamp"] = timestamp;
1458
+ headers["x-embed-nonce"] = nonce;
1459
+ headers["x-embed-body-sha256"] = bodySha256;
1460
+ headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1461
+ }
1462
+ function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
1463
+ return [
1464
+ method.toUpperCase(),
1465
+ normalizeCloudPathForSignature(pathWithQuery),
1466
+ timestamp,
1467
+ nonce,
1468
+ bodySha256
1469
+ ].join("\n");
1470
+ }
1471
+ function normalizeCloudPathForSignature(pathWithQuery) {
1472
+ try {
1473
+ const parsed = new URL(pathWithQuery, "http://embed.local");
1474
+ return `${parsed.pathname}${parsed.search}`;
1475
+ }
1476
+ catch {
1477
+ return pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`;
1478
+ }
1479
+ }
1432
1480
  async function pluginList(parsed) {
1433
1481
  const releaseDir = stringFlag(parsed, "release-dir");
1434
1482
  const manifest = releaseDir ? await readPluginReleaseManifest(releaseDir) : undefined;
@@ -1535,6 +1583,7 @@ async function installCodexPlugin(parsed, context) {
1535
1583
  }
1536
1584
  const targetRoot = codexPluginTargetRoot(parsed, context.installingAll);
1537
1585
  const targetPath = join(targetRoot, "embed-labs");
1586
+ const legacyCleanup = await cleanupLegacyCodexPluginRemnants(targetRoot);
1538
1587
  if (await pathExists(targetPath) && !booleanFlag(parsed, "force")) {
1539
1588
  return fail("plugin_already_installed", `Codex plugin already exists at ${targetPath}.`, {
1540
1589
  remediation: "Pass --force to replace it, or pass --codex-target/--target to install into a different directory."
@@ -1552,8 +1601,10 @@ async function installCodexPlugin(parsed, context) {
1552
1601
  command_hint: mcpRegistration.registered
1553
1602
  ? "Codex MCP was registered. Start a new Codex session to reload tools."
1554
1603
  : mcpRegistration.hint,
1604
+ warning: legacyCodexCleanupWarning(legacyCleanup),
1555
1605
  mcp_registered: mcpRegistration.registered,
1556
- mcp_warning: mcpRegistration.warning
1606
+ mcp_warning: mcpRegistration.warning,
1607
+ cleanup: legacyCleanup
1557
1608
  });
1558
1609
  }
1559
1610
  async function installOpenCodePlugin(parsed, context) {
@@ -1562,8 +1613,10 @@ async function installOpenCodePlugin(parsed, context) {
1562
1613
  return source;
1563
1614
  }
1564
1615
  const targetRoot = openCodePluginTargetRoot(parsed, context.installingAll);
1565
- const wrapperPath = join(targetRoot, "plugins", "development-board-toolchain.js");
1566
- if (await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
1616
+ const globalInstall = isGlobalOpenCodeRoot(targetRoot);
1617
+ const wrapperPath = join(targetRoot, "plugins", "embed-labs.js");
1618
+ const legacyCleanup = await cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall);
1619
+ if (!globalInstall && await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
1567
1620
  return fail("plugin_already_installed", `OpenCode plugin wrapper already exists at ${wrapperPath}.`, {
1568
1621
  remediation: "Pass --force to replace it, or pass --opencode-target/--target to install into a different directory."
1569
1622
  });
@@ -1589,15 +1642,25 @@ async function installOpenCodePlugin(parsed, context) {
1589
1642
  });
1590
1643
  }
1591
1644
  await ensureOpenCodeInstallPackageJson(targetRoot);
1592
- await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
1645
+ if (globalInstall) {
1646
+ await rm(wrapperPath, { force: true });
1647
+ legacyCleanup.legacy_removed_config_entries?.push(...await ensureOpenCodeGlobalPluginConfig());
1648
+ }
1649
+ else {
1650
+ await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
1651
+ }
1593
1652
  const duplicateWarning = await openCodeDuplicatePluginWarning(targetRoot);
1653
+ const cleanupWarning = legacyOpenCodeCleanupWarning(legacyCleanup);
1594
1654
  return ok({
1595
1655
  id: "opencode",
1596
1656
  target_path: targetRoot,
1597
1657
  source: source.data.sourceLabel,
1598
1658
  version: source.data.version,
1599
- command_hint: "Start OpenCode from the project containing this .opencode directory.",
1600
- warning: duplicateWarning
1659
+ command_hint: globalInstall
1660
+ ? "Restart OpenCode so the global embed-labs package plugin is reloaded."
1661
+ : "Start OpenCode from the project containing this .opencode directory.",
1662
+ warning: combineWarnings(cleanupWarning, duplicateWarning),
1663
+ cleanup: legacyCleanup
1601
1664
  });
1602
1665
  }
1603
1666
  async function resolveCodexPluginSource(context) {
@@ -1811,7 +1874,251 @@ function openCodePluginTargetRoot(parsed, installingAll) {
1811
1874
  return resolve(target ?? defaultOpenCodeRoot());
1812
1875
  }
1813
1876
  function defaultCodexPluginRoot() {
1814
- return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "plugins");
1877
+ return join(defaultCodexHome(), "plugins");
1878
+ }
1879
+ function defaultCodexHome() {
1880
+ return resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
1881
+ }
1882
+ function codexConfigPath() {
1883
+ return join(defaultCodexHome(), "config.toml");
1884
+ }
1885
+ async function cleanupLegacyCodexPluginRemnants(targetRoot) {
1886
+ const removedPaths = [];
1887
+ const removedConfigTables = [];
1888
+ const warnings = [];
1889
+ const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
1890
+ const legacyPaths = [
1891
+ join(targetRoot, "dbt-agent"),
1892
+ join(targetRoot, "development-board-toolchain"),
1893
+ join(targetRoot, "cache", "embed-labs", "dbt-agent"),
1894
+ join(targetRoot, "cache", "dbt-agent"),
1895
+ join(targetRoot, "cache", "plugins", "dbt-agent")
1896
+ ];
1897
+ legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
1898
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
1899
+ legacyPaths.push(join(defaultCodexHome(), ".tmp", "plugins"), join(defaultCodexHome(), ".tmp", "plugins.sha"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-live-board-ops"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-platform-plugin-maintenance"));
1900
+ }
1901
+ for (const candidate of legacyPaths) {
1902
+ try {
1903
+ if (await pathExists(candidate)) {
1904
+ await rm(candidate, { recursive: true, force: true });
1905
+ removedPaths.push(candidate);
1906
+ }
1907
+ }
1908
+ catch (error) {
1909
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
1910
+ }
1911
+ }
1912
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
1913
+ const configPath = codexConfigPath();
1914
+ try {
1915
+ if (await pathExists(configPath)) {
1916
+ const current = await readFile(configPath, "utf8");
1917
+ const updated = removeLegacyCodexConfigTables(current);
1918
+ if (updated.text !== current) {
1919
+ await writeFile(configPath, updated.text, "utf8");
1920
+ }
1921
+ removedConfigTables.push(...updated.removedTables);
1922
+ }
1923
+ }
1924
+ catch (error) {
1925
+ warnings.push(`Could not update ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
1926
+ }
1927
+ }
1928
+ const removedHistoryEntries = await cleanupLegacyCodexTextState(warnings);
1929
+ return {
1930
+ legacy_removed_paths: Array.from(new Set(removedPaths)),
1931
+ legacy_removed_config_tables: removedConfigTables,
1932
+ legacy_removed_history_entries: removedHistoryEntries,
1933
+ legacy_stopped_processes: stoppedProcesses,
1934
+ ...(warnings.length > 0 ? { warnings } : {})
1935
+ };
1936
+ }
1937
+ async function stopLegacyCodexPluginProcesses(warnings) {
1938
+ if (process.platform === "win32")
1939
+ return 0;
1940
+ try {
1941
+ const ps = await runLocalProcess("ps", ["-axo", "pid=,command="]);
1942
+ if (ps.code !== 0)
1943
+ return 0;
1944
+ let stopped = 0;
1945
+ for (const line of ps.stdout.split("\n")) {
1946
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
1947
+ if (!match)
1948
+ continue;
1949
+ const pid = Number(match[1]);
1950
+ const command = match[2] || "";
1951
+ if (!isLegacyCodexPluginProcess(command))
1952
+ continue;
1953
+ try {
1954
+ process.kill(pid, "SIGTERM");
1955
+ stopped += 1;
1956
+ }
1957
+ catch (error) {
1958
+ warnings.push(`Could not stop legacy Codex plugin process ${pid}: ${error instanceof Error ? error.message : String(error)}`);
1959
+ }
1960
+ }
1961
+ return stopped;
1962
+ }
1963
+ catch (error) {
1964
+ warnings.push(`Could not scan legacy Codex plugin processes: ${error instanceof Error ? error.message : String(error)}`);
1965
+ return 0;
1966
+ }
1967
+ }
1968
+ function isLegacyCodexPluginProcess(command) {
1969
+ const trimmed = command.trim();
1970
+ return /^\/.*\/dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
1971
+ || /^dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed);
1972
+ }
1973
+ async function discoverLegacyCodexCachePaths(targetRoot) {
1974
+ const paths = [];
1975
+ const cacheRoot = join(targetRoot, "cache");
1976
+ try {
1977
+ const marketplaces = await readdir(cacheRoot, { withFileTypes: true });
1978
+ for (const entry of marketplaces) {
1979
+ if (!entry.isDirectory())
1980
+ continue;
1981
+ paths.push(join(cacheRoot, entry.name, "dbt-agent"));
1982
+ paths.push(join(cacheRoot, entry.name, "development-board-toolchain"));
1983
+ }
1984
+ }
1985
+ catch {
1986
+ return paths;
1987
+ }
1988
+ return paths;
1989
+ }
1990
+ async function cleanupLegacyCodexTextState(warnings) {
1991
+ let removed = 0;
1992
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "history.jsonl"), warnings);
1993
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "session_index.jsonl"), warnings);
1994
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "rules", "default.rules"), warnings);
1995
+ return removed;
1996
+ }
1997
+ async function cleanupLegacyCodexTextFile(filePath, warnings) {
1998
+ try {
1999
+ if (!await pathExists(filePath))
2000
+ return 0;
2001
+ const current = await readFile(filePath, "utf8");
2002
+ const lines = current.split("\n");
2003
+ let removed = 0;
2004
+ const kept = lines.filter((line) => {
2005
+ if (!line)
2006
+ return true;
2007
+ if (isLegacyCodexHistoryMention(line)) {
2008
+ removed += 1;
2009
+ return false;
2010
+ }
2011
+ return true;
2012
+ });
2013
+ if (removed > 0) {
2014
+ await writeFile(filePath, kept.join("\n"), "utf8");
2015
+ }
2016
+ return removed;
2017
+ }
2018
+ catch (error) {
2019
+ warnings.push(`Could not clean Codex legacy text state ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2020
+ return 0;
2021
+ }
2022
+ }
2023
+ function isLegacyCodexHistoryMention(line) {
2024
+ return line.includes("plugin://dbt-agent@plugins")
2025
+ || line.includes("plugin://dbt-agent@embed-labs")
2026
+ || /dbt-agent/i.test(line);
2027
+ }
2028
+ function removeLegacyCodexConfigTables(text) {
2029
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2030
+ const output = [];
2031
+ const removedTables = [];
2032
+ let skipping = false;
2033
+ for (const line of lines) {
2034
+ const table = parseTomlTableHeader(line);
2035
+ if (table) {
2036
+ skipping = isLegacyCodexConfigTable(table);
2037
+ if (skipping) {
2038
+ removedTables.push(table);
2039
+ continue;
2040
+ }
2041
+ }
2042
+ if (!skipping) {
2043
+ output.push(line);
2044
+ }
2045
+ }
2046
+ return { text: output.join("").replace(/\n{3,}/g, "\n\n"), removedTables };
2047
+ }
2048
+ function parseTomlTableHeader(line) {
2049
+ const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
2050
+ return match?.[1]?.trim();
2051
+ }
2052
+ function isLegacyCodexConfigTable(table) {
2053
+ return /^plugins\."dbt-agent@[^"]+"$/.test(table)
2054
+ || table === "mcp_servers.dbt-agent"
2055
+ || table.startsWith("mcp_servers.dbt-agent.")
2056
+ || table === 'mcp_servers."dbt-agent"'
2057
+ || table.startsWith('mcp_servers."dbt-agent".');
2058
+ }
2059
+ function legacyCodexCleanupWarning(cleanup) {
2060
+ const parts = [];
2061
+ if (cleanup.legacy_removed_paths.length > 0) {
2062
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} legacy Codex plugin path(s)`);
2063
+ }
2064
+ if (cleanup.legacy_removed_config_tables?.length) {
2065
+ parts.push(`removed ${cleanup.legacy_removed_config_tables.length} legacy Codex config table(s)`);
2066
+ }
2067
+ if (cleanup.legacy_removed_history_entries) {
2068
+ parts.push(`removed ${cleanup.legacy_removed_history_entries} legacy Codex text-state mention(s)`);
2069
+ }
2070
+ if (cleanup.legacy_stopped_processes) {
2071
+ parts.push(`stopped ${cleanup.legacy_stopped_processes} legacy Codex plugin process(es)`);
2072
+ }
2073
+ if (cleanup.warnings?.length) {
2074
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2075
+ }
2076
+ return parts.length > 0 ? `Legacy dbt-agent cleanup: ${parts.join(", ")}.` : undefined;
2077
+ }
2078
+ async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
2079
+ const removedPaths = [];
2080
+ const warnings = [];
2081
+ const legacyPaths = [
2082
+ join(targetRoot, "plugins", "development-board-toolchain.js"),
2083
+ join(targetRoot, "plugins", "dbt-agent.js"),
2084
+ join(targetRoot, "node_modules", "dbt-agent")
2085
+ ];
2086
+ if (globalInstall) {
2087
+ legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
2088
+ }
2089
+ for (const candidate of legacyPaths) {
2090
+ try {
2091
+ if (await pathExists(candidate)) {
2092
+ await rm(candidate, { recursive: true, force: true });
2093
+ removedPaths.push(candidate);
2094
+ }
2095
+ }
2096
+ catch (error) {
2097
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2098
+ }
2099
+ }
2100
+ return {
2101
+ legacy_removed_paths: removedPaths,
2102
+ legacy_removed_config_entries: [],
2103
+ ...(warnings.length > 0 ? { warnings } : {})
2104
+ };
2105
+ }
2106
+ function legacyOpenCodeCleanupWarning(cleanup) {
2107
+ const parts = [];
2108
+ if (cleanup.legacy_removed_paths.length > 0) {
2109
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} legacy OpenCode plugin path(s)`);
2110
+ }
2111
+ if (cleanup.legacy_removed_config_entries?.length) {
2112
+ parts.push(`removed ${cleanup.legacy_removed_config_entries.length} legacy OpenCode config entry(s)`);
2113
+ }
2114
+ if (cleanup.warnings?.length) {
2115
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2116
+ }
2117
+ return parts.length > 0 ? `Legacy OpenCode cleanup: ${parts.join(", ")}.` : undefined;
2118
+ }
2119
+ function combineWarnings(...warnings) {
2120
+ const actual = warnings.filter((warning) => Boolean(warning));
2121
+ return actual.length > 0 ? actual.join(" ") : undefined;
1815
2122
  }
1816
2123
  async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
1817
2124
  const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
@@ -1984,10 +2291,54 @@ async function resolveExecutableOnPath(name) {
1984
2291
  return undefined;
1985
2292
  }
1986
2293
  function defaultOpenCodeRoot() {
1987
- return join(process.cwd(), ".opencode");
2294
+ return globalOpenCodeRoot();
2295
+ }
2296
+ function globalOpenCodeRoot() {
2297
+ return join(homedir(), ".config", "opencode");
2298
+ }
2299
+ function isGlobalOpenCodeRoot(targetRoot) {
2300
+ return resolve(targetRoot) === resolve(globalOpenCodeRoot());
2301
+ }
2302
+ async function ensureOpenCodeGlobalPluginConfig() {
2303
+ const configPath = join(globalOpenCodeRoot(), "opencode.json");
2304
+ let existing = {};
2305
+ try {
2306
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
2307
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2308
+ existing = parsed;
2309
+ }
2310
+ }
2311
+ catch {
2312
+ existing = {};
2313
+ }
2314
+ const configured = Array.isArray(existing.plugin)
2315
+ ? existing.plugin.filter((item) => typeof item === "string")
2316
+ : Array.isArray(existing.plugins)
2317
+ ? existing.plugins.filter((item) => typeof item === "string")
2318
+ : [];
2319
+ const removed = configured.filter(isLegacyOpenCodePluginConfigEntry);
2320
+ const cleaned = configured.filter((item) => !isLegacyOpenCodePluginConfigEntry(item));
2321
+ if (!cleaned.includes("embed-labs")) {
2322
+ cleaned.push("embed-labs");
2323
+ }
2324
+ await writeFile(configPath, `${JSON.stringify({
2325
+ ...existing,
2326
+ plugin: cleaned,
2327
+ plugins: undefined
2328
+ }, null, 2)}\n`, "utf8");
2329
+ return removed;
2330
+ }
2331
+ function isLegacyOpenCodePluginConfigEntry(item) {
2332
+ const normalized = item.replace(/\\/g, "/");
2333
+ return item === "dbt-agent"
2334
+ || item === "development-board-toolchain"
2335
+ || normalized === "./plugins/development-board-toolchain"
2336
+ || normalized === "./plugins/development-board-toolchain.js"
2337
+ || normalized.endsWith("/plugins/development-board-toolchain.js")
2338
+ || normalized.includes("development-board-toolchain");
1988
2339
  }
1989
2340
  async function openCodeDuplicatePluginWarning(targetRoot) {
1990
- const globalRoot = join(homedir(), ".config", "opencode");
2341
+ const globalRoot = globalOpenCodeRoot();
1991
2342
  if (resolve(targetRoot) === resolve(globalRoot))
1992
2343
  return undefined;
1993
2344
  const configPath = join(globalRoot, "opencode.json");