@ubean/devtools 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1319,35 +1319,66 @@ function createTerminalServer() {
1319
1319
  };
1320
1320
  }
1321
1321
  //#endregion
1322
- //#region ../../node_modules/.pnpm/devframe@1.2.3_@devframes+agentic@1.2.3_cac@7.0.1_srvx@1.0.5/node_modules/devframe/dist/define-BLWPsH6y.mjs
1323
- function createDefineWrapperWithContext() {
1324
- return function defineRpcFunctionWithContext(definition) {
1325
- return definition;
1326
- };
1322
+ //#region src/node/rpc/types.ts
1323
+ /**
1324
+ * 身份包装器:校验定义形状,同时**保留 `name` 的字面量类型**。
1325
+ *
1326
+ * 直接用 `satisfies RpcFunctionDefinition` 不行 —— 那个类型至少要一个类型参数(`NAME`),
1327
+ * 不传参就把 `name` 拓宽成 `string`,而 `RpcDefinitionsToFunctionsWithNamespace<'ubean', T>`
1328
+ * 恰恰靠字面量名推导 wire 签名。泛型参数 `const D extends …` 同时满足两件事:约束检查 + 字面量保留。
1329
+ */
1330
+ function defineUbeanRpc(definition) {
1331
+ return definition;
1327
1332
  }
1328
- //#endregion
1329
- //#region ../../node_modules/.pnpm/@vitejs+devtools-kit@0.7.6_@devframes+agentic@1.2.3_@voidzero-dev+vite-plus-core@1.1.0__089b475afebafe0aa497b2df40bc3712/node_modules/@vitejs/devtools-kit/dist/index.js
1330
- const defineRpcFunction = createDefineWrapperWithContext();
1333
+ /**
1334
+ * 本包 RPC 的命名空间。所有裸名注册后成为 `<UBEAN_RPC_NAMESPACE>:<bare-name>`。
1335
+ */
1336
+ const UBEAN_RPC_NAMESPACE = "ubean";
1337
+ /**
1338
+ * `ubean:ai:stream` —— AI 流式响应的 shared state key。
1339
+ *
1340
+ * **不是** scoped key:`ctx.scope('ubean').rpc.sharedState.get('ai:stream')` 会再加一层前缀
1341
+ * (`ubean:ai:stream` 本身已含 `:`,`qualifyName` 原样返回,因此两种写法恰好都落到这个 key)。
1342
+ * 这里显式写全名,避免读者以为它经过 scope。
1343
+ */
1344
+ const UBEAN_AI_STREAM_STATE_KEY = "ubean:ai:stream";
1345
+ /**
1346
+ * anchor dock 的共享 iframe 配置。
1347
+ *
1348
+ * `frameId` / `subTabs` 属于 **`DevframeViewIframe`**(不是 `DevframeDockDefaults`),
1349
+ * 因此不能写在 `DevframeDefinition.dock` 里 —— 它们必须经
1350
+ * `initHub({ devframes: [{ devframe, dock }] })`(或 `ctx.install(def, { dock })`)传入。
1351
+ * 这里集中一处,CLI 与测试都从这里取。
1352
+ *
1353
+ * - `frameId`:所有视图 dock 共用一个 iframe 元素(软导航池)。
1354
+ * - `subTabs`:客户端 host 自动挂 frame-nav 适配器,运行时发现视图清单。
1355
+ *
1356
+ * **不要设 `visibility: 'false'`**:它会隐藏 anchor 自己的 dock 按钮,而 iframe 只在 anchor
1357
+ * 被激活时才挂载 —— 隐藏 anchor 就永远发现不了视图清单,外壳里只剩一个空的 dock 侧栏
1358
+ * (实测 2026-10-11:面板整个打不开)。
1359
+ */
1360
+ const UBEAN_ANCHOR_DOCK = {
1361
+ frameId: "ubean",
1362
+ subTabs: { protocol: "postmessage" }
1363
+ };
1331
1364
  //#endregion
1332
1365
  //#region src/node/rpc/ai.ts
1333
1366
  /**
1334
- * AI RPC functions — `ubean:ai:tools`, `ubean:ai:chat`, and `ubean:ai:chat-stream`.
1367
+ * AI RPC functions — `ai-tools` / `ai-chat` / `ai-chat-stream`(裸名,注册到 `ubean` scope)。
1335
1368
  *
1336
- * `ubean:ai:chat` — non-streaming chat (returns full response).
1337
- * `ubean:ai:chat-stream` — streaming chat; pushes chunks to the
1338
- * `ubean:ai:stream` sharedState key via the `onStreamChunk` callback
1339
- * wired in `createAiServer`. The client subscribes to that sharedState
1340
- * and filters by `requestId`.
1369
+ * - `ai-chat` 非流式(返回完整响应);
1370
+ * - `ai-chat-stream` 流式:分片由 `createAiServer` 里的 `onStreamChunk` 推入
1371
+ * `ubean:ai:stream` shared state,客户端订阅后按 `requestId` 过滤。
1341
1372
  */
1342
1373
  function createAiRpcFunctions(ai) {
1343
1374
  return [
1344
- defineRpcFunction({
1345
- name: "ubean:ai:tools",
1375
+ defineUbeanRpc({
1376
+ name: "ai-tools",
1346
1377
  type: "query",
1347
1378
  setup: () => ({ handler: () => ai.getToolDefinitions() })
1348
1379
  }),
1349
- defineRpcFunction({
1350
- name: "ubean:ai:chat",
1380
+ defineUbeanRpc({
1381
+ name: "ai-chat",
1351
1382
  type: "action",
1352
1383
  setup: () => ({ handler: (params) => ai.chat({
1353
1384
  messages: params.messages,
@@ -1356,8 +1387,8 @@ function createAiRpcFunctions(ai) {
1356
1387
  model: params.model
1357
1388
  }) })
1358
1389
  }),
1359
- defineRpcFunction({
1360
- name: "ubean:ai:chat-stream",
1390
+ defineUbeanRpc({
1391
+ name: "ai-chat-stream",
1361
1392
  type: "action",
1362
1393
  setup: () => ({ handler: (params) => ai.chat({
1363
1394
  messages: params.messages,
@@ -1372,36 +1403,36 @@ function createAiRpcFunctions(ai) {
1372
1403
  //#endregion
1373
1404
  //#region src/node/rpc/crud.ts
1374
1405
  /**
1375
- * CRUD RPC functions — `ubean:crud:create/read/update/delete/restore`.
1406
+ * CRUD RPC functions — `crud-create` / `crud-read` / `crud-update` / `crud-delete` /
1407
+ * `crud-restore`(裸名,注册到 `ubean` scope)。
1376
1408
  *
1377
- * Each function delegates to the existing `createCrudServer` which
1378
- * encapsulates all file-scaffolding, backup, and hook logic. The RPC
1379
- * layer is purely a transport adapter.
1409
+ * 每个都直接委派给 `createCrudServer` —— 它封装了全部文件脚手架、备份与 hook 逻辑,
1410
+ * RPC 层只是传输适配。
1380
1411
  */
1381
1412
  function createCrudRpcFunctions(crud) {
1382
1413
  return [
1383
- defineRpcFunction({
1384
- name: "ubean:crud:create",
1414
+ defineUbeanRpc({
1415
+ name: "crud-create",
1385
1416
  type: "action",
1386
1417
  setup: () => ({ handler: (params) => crud.create(params) })
1387
1418
  }),
1388
- defineRpcFunction({
1389
- name: "ubean:crud:read",
1419
+ defineUbeanRpc({
1420
+ name: "crud-read",
1390
1421
  type: "query",
1391
1422
  setup: () => ({ handler: (params) => crud.read(params) })
1392
1423
  }),
1393
- defineRpcFunction({
1394
- name: "ubean:crud:update",
1424
+ defineUbeanRpc({
1425
+ name: "crud-update",
1395
1426
  type: "action",
1396
1427
  setup: () => ({ handler: (params) => crud.update(params) })
1397
1428
  }),
1398
- defineRpcFunction({
1399
- name: "ubean:crud:delete",
1429
+ defineUbeanRpc({
1430
+ name: "crud-delete",
1400
1431
  type: "action",
1401
1432
  setup: () => ({ handler: (params) => crud.delete(params) })
1402
1433
  }),
1403
- defineRpcFunction({
1404
- name: "ubean:crud:restore",
1434
+ defineUbeanRpc({
1435
+ name: "crud-restore",
1405
1436
  type: "action",
1406
1437
  setup: () => ({ handler: (path) => crud.restore(path) })
1407
1438
  })
@@ -1410,19 +1441,22 @@ function createCrudRpcFunctions(crud) {
1410
1441
  //#endregion
1411
1442
  //#region src/node/rpc/info.ts
1412
1443
  /**
1413
- * Info RPC functions — `ubean:get-info` and `ubean:get-env`.
1444
+ * Info RPC functions — `get-info` 与 `get-env`(裸名,注册到 `ubean` scope)。
1414
1445
  *
1415
- * In the sharedState model, `ubean:get-info` is a fallback for clients
1416
- * that haven't subscribed to the `ubean:info` state yet. Most clients
1417
- * should use `client.sharedState.get('ubean:info')` instead.
1446
+ * sharedState 模型下 `get-info` 只是给**还没订阅** `ubean:info` 的客户端的兜底;正常客户端
1447
+ * 直接 `client.sharedState.get('ubean:info')`。
1448
+ */
1449
+ /**
1450
+ * 返回定义数组(**裸名**)。调用方用
1451
+ * `RpcDefinitionsToFunctionsWithNamespace<'ubean', typeof …>` 推导 wire 签名。
1418
1452
  */
1419
1453
  function createInfoRpcFunctions(opts) {
1420
- return [defineRpcFunction({
1421
- name: "ubean:get-info",
1454
+ return [defineUbeanRpc({
1455
+ name: "get-info",
1422
1456
  type: "query",
1423
1457
  setup: () => ({ handler: () => opts.state.value() })
1424
- }), defineRpcFunction({
1425
- name: "ubean:get-env",
1458
+ }), defineUbeanRpc({
1459
+ name: "get-env",
1426
1460
  type: "query",
1427
1461
  setup: () => ({ handler: () => maskSensitiveEnv(opts.getEnvData()) })
1428
1462
  })];
@@ -1430,15 +1464,14 @@ function createInfoRpcFunctions(opts) {
1430
1464
  //#endregion
1431
1465
  //#region src/node/rpc/playground.ts
1432
1466
  /**
1433
- * Playground RPC function — `ubean:playground:invoke`.
1467
+ * Playground RPC function — `playground-invoke`(裸名,注册到 `ubean` scope)。
1434
1468
  *
1435
- * Forwards an HTTP request to the Hono app in-process (no network
1436
- * round-trip), mirroring the `internalFetch` / `callInternal` pattern
1437
- * used elsewhere in ubean.
1469
+ * 在进程内把 HTTP 请求转给 Hono app(无网络往返),与框架其余处的
1470
+ * `internalFetch` / `callInternal` 同一思路。
1438
1471
  */
1439
1472
  function createPlaygroundRpcFunctions(opts) {
1440
- return [defineRpcFunction({
1441
- name: "ubean:playground:invoke",
1473
+ return [defineUbeanRpc({
1474
+ name: "playground-invoke",
1442
1475
  type: "action",
1443
1476
  setup: () => ({ handler: async (params) => {
1444
1477
  const app = opts.getApp?.();
@@ -1477,35 +1510,36 @@ function createPlaygroundRpcFunctions(opts) {
1477
1510
  //#endregion
1478
1511
  //#region src/node/rpc/terminal.ts
1479
1512
  /**
1480
- * Terminal RPC functions — `ubean:terminal:start/input/resize/poll/kill`.
1513
+ * Terminal RPC functions — `terminal-start` / `terminal-input` / `terminal-resize` /
1514
+ * `terminal-poll` / `terminal-kill`(裸名,注册到 `ubean` scope)。
1481
1515
  *
1482
- * Delegates to `createTerminalServer` which manages shell processes and
1483
- * output buffering. The client polls for output via `ubean:terminal:poll`.
1516
+ * 委派给 `createTerminalServer`(管理 shell 进程与输出缓冲)。客户端通过
1517
+ * `terminal-poll` 拉取输出。
1484
1518
  */
1485
1519
  function createTerminalRpcFunctions(terminal) {
1486
1520
  return [
1487
- defineRpcFunction({
1488
- name: "ubean:terminal:start",
1521
+ defineUbeanRpc({
1522
+ name: "terminal-start",
1489
1523
  type: "action",
1490
1524
  setup: () => ({ handler: (params) => Promise.resolve(terminal.start(params)) })
1491
1525
  }),
1492
- defineRpcFunction({
1493
- name: "ubean:terminal:input",
1526
+ defineUbeanRpc({
1527
+ name: "terminal-input",
1494
1528
  type: "action",
1495
1529
  setup: () => ({ handler: (params) => Promise.resolve(terminal.input(params.sessionId, params.data)) })
1496
1530
  }),
1497
- defineRpcFunction({
1498
- name: "ubean:terminal:resize",
1531
+ defineUbeanRpc({
1532
+ name: "terminal-resize",
1499
1533
  type: "action",
1500
1534
  setup: () => ({ handler: (params) => Promise.resolve(terminal.resize(params.sessionId, params.cols, params.rows)) })
1501
1535
  }),
1502
- defineRpcFunction({
1503
- name: "ubean:terminal:poll",
1536
+ defineUbeanRpc({
1537
+ name: "terminal-poll",
1504
1538
  type: "query",
1505
1539
  setup: () => ({ handler: (params) => Promise.resolve(terminal.poll(params.sessionId)) })
1506
1540
  }),
1507
- defineRpcFunction({
1508
- name: "ubean:terminal:kill",
1541
+ defineUbeanRpc({
1542
+ name: "terminal-kill",
1509
1543
  type: "action",
1510
1544
  setup: () => ({ handler: (params) => Promise.resolve(terminal.kill(params.sessionId)) })
1511
1545
  })
@@ -1514,11 +1548,7 @@ function createTerminalRpcFunctions(terminal) {
1514
1548
  //#endregion
1515
1549
  //#region src/node/rpc/index.ts
1516
1550
  /**
1517
- * Returns an array of `defineRpcFunction` definitions. The return type is
1518
- * intentionally loose (`any[]`) because the DTK `ctx.rpc.register()` accepts
1519
- * definitions parameterized on `ViteDevToolsNodeContext`, and the exact
1520
- * generic instantiation varies per function — the individual functions are
1521
- * already type-checked at their definition site.
1551
+ * 全部 RPC 定义(裸名)。`setup` 里逐个 `ctx.scope('ubean').rpc.register(fn)`。
1522
1552
  */
1523
1553
  function createAllRpcFunctions(deps) {
1524
1554
  return [
@@ -1650,7 +1680,7 @@ function emptyDevToolsInfo(startTime) {
1650
1680
  }
1651
1681
  /**
1652
1682
  * Create or retrieve the `ubean:info` shared state.
1653
- * Called once during `devtools.setup`.
1683
+ * Called once during the devframe's `setup(ctx)`.
1654
1684
  */
1655
1685
  async function initSharedState(ctx, initialValue) {
1656
1686
  return ctx.rpc.sharedState.get(UBEAN_INFO_STATE_KEY, { initialValue });
@@ -1668,163 +1698,63 @@ function refreshSharedState(state, info) {
1668
1698
  //#region src/node/index.ts
1669
1699
  /**
1670
1700
  * Path to the pre-built client SPA. Resolved relative to the built server
1671
- * bundle (`dist/index.mjs`), so this points to `dist/client/` at runtime.
1701
+ * bundle (`dist/index.js`), so this points to `dist/client/` at runtime.
1672
1702
  */
1673
1703
  const CLIENT_DIST = resolve(dirname(fileURLToPath(import.meta.url)), "client");
1674
1704
  /**
1675
- * Ubean DevTools as a Vite plugin.
1705
+ * 构建 `@ubean/devtools` 的 devframe 定义。
1676
1706
  *
1677
- * Registers a dock entry (iframe pointing at the pre-built SPA), hosts the
1678
- * SPA's static assets, and wires up the type-safe RPC + sharedState used by
1679
- * the SPA. This replaces the previous Hono-middleware-based transport with
1680
- * the Vite DevTools Kit (DTK) birpc + shared-state machinery.
1707
+ * 与旧 DTK 接线的区别:
1708
+ * - 不再手动 `ctx.views.hostStatic` / 手写 11 个 dock entry —— hub 的 `ctx.install()`
1709
+ * 负责服务 SPA、注册 anchor dock 与 `__connection.json`;
1710
+ * - 不再有那段 `__connection.json` 转发 hack(hub 的 `mountConnectionMeta` 原生支持);
1711
+ * - RPC 走 `ctx.scope('ubean')`,裸名注册(`get-info` → wire 名 `ubean:get-info`)。
1681
1712
  */
1682
- function ubeanDevtoolsPlugin(options = { getCwd: () => process.cwd() }) {
1713
+ function createUbeanDevframe(options) {
1714
+ const basePath = options.basePath ?? "/_devtools/";
1683
1715
  return {
1684
- name: "ubean:devtools",
1685
- devtools: { async setup(ctx) {
1686
- console.log("[ubean:devtools] devtools.setup hook fired — DTK integration active");
1687
- ctx.views.hostStatic("/_devtools/", CLIENT_DIST);
1688
- const viteServer = ctx.viteServer;
1689
- if (viteServer?.middlewares) {
1690
- const serveConnectionMeta = (req, res) => {
1691
- const targetUrl = `${req.headers["x-forwarded-proto"] || "http"}://${req.headers.host || "localhost:9527"}/__devtools/__connection.json`;
1692
- fetch(targetUrl).then(async (metaRes) => {
1693
- const meta = await metaRes.json();
1694
- if (meta.websocket && typeof meta.websocket === "object") meta.websocket = {
1695
- ...meta.websocket,
1696
- path: "/__devtools/__ws"
1697
- };
1698
- res.setHeader("Content-Type", "application/json");
1699
- res.end(JSON.stringify(meta));
1700
- }).catch(() => {
1701
- res.statusCode = 500;
1702
- res.setHeader("Content-Type", "application/json");
1703
- res.end(JSON.stringify({ error: "Failed to resolve connection meta" }));
1704
- });
1705
- };
1706
- viteServer.middlewares.use("/_devtools/__connection.json", serveConnectionMeta);
1707
- viteServer.middlewares.use("/__connection.json", serveConnectionMeta);
1708
- }
1709
- const SPA_BASE = "/_devtools/index.html";
1710
- const dockEntries = [
1711
- {
1712
- id: "ubean:overview",
1713
- title: "Overview",
1714
- icon: "lucide:layout-dashboard",
1715
- url: `${SPA_BASE}#/overview`,
1716
- order: 100
1717
- },
1718
- {
1719
- id: "ubean:pages",
1720
- title: "Pages",
1721
- icon: "lucide:file-text",
1722
- url: `${SPA_BASE}#/pages`,
1723
- order: 95
1724
- },
1725
- {
1726
- id: "ubean:api",
1727
- title: "API",
1728
- icon: "lucide:send",
1729
- url: `${SPA_BASE}#/api`,
1730
- order: 90
1731
- },
1732
- {
1733
- id: "ubean:middleware",
1734
- title: "Middleware",
1735
- icon: "lucide:layers",
1736
- url: `${SPA_BASE}#/middleware`,
1737
- order: 85
1738
- },
1739
- {
1740
- id: "ubean:crons",
1741
- title: "Crons",
1742
- icon: "lucide:clock",
1743
- url: `${SPA_BASE}#/crons`,
1744
- order: 80
1745
- },
1746
- {
1747
- id: "ubean:config",
1748
- title: "Config",
1749
- icon: "lucide:settings",
1750
- url: `${SPA_BASE}#/config`,
1751
- order: 75
1752
- },
1753
- {
1754
- id: "ubean:env",
1755
- title: "Env",
1756
- icon: "lucide:terminal",
1757
- url: `${SPA_BASE}#/env`,
1758
- order: 70
1759
- },
1760
- {
1761
- id: "ubean:api-docs",
1762
- title: "API Docs",
1763
- icon: "lucide:book-open",
1764
- url: `${SPA_BASE}#/api-docs`,
1765
- order: 65
1766
- },
1767
- {
1768
- id: "ubean:database",
1769
- title: "Database",
1770
- icon: "lucide:database",
1771
- url: `${SPA_BASE}#/database`,
1772
- order: 60
1773
- },
1774
- {
1775
- id: "ubean:terminal",
1776
- title: "Terminal",
1777
- icon: "lucide:square-terminal",
1778
- url: `${SPA_BASE}#/terminal`,
1779
- order: 58
1780
- },
1781
- {
1782
- id: "ubean:ai",
1783
- title: "AI",
1784
- icon: "lucide:sparkles",
1785
- url: `${SPA_BASE}#/ai`,
1786
- order: 55
1787
- }
1788
- ];
1789
- for (const entry of dockEntries) ctx.docks.register({
1790
- id: entry.id,
1791
- title: entry.title,
1792
- icon: entry.icon,
1793
- type: "iframe",
1794
- url: entry.url,
1795
- category: "framework",
1796
- defaultOrder: entry.order
1797
- });
1716
+ id: "ubean",
1717
+ name: "Ubean DevTools",
1718
+ version: "0.6.1",
1719
+ packageName: "@ubean/devtools",
1720
+ homepage: "https://github.com/SoybeanJS/ubean",
1721
+ description: "Ubean 项目的路由 / 页面 / API / 配置 / 终端 / AI 可视化面板。",
1722
+ importMetaUrl: import.meta.url,
1723
+ icon: "https://img.soybeanjs.dev/logo-ubean.svg?v=202608192144",
1724
+ basePath,
1725
+ capabilities: { build: false },
1726
+ clientAssets: CLIENT_DIST,
1727
+ dock: {
1728
+ title: "Ubean",
1729
+ defaultOrder: 100,
1730
+ category: "framework"
1731
+ },
1732
+ setup: async (nodeCtx) => {
1733
+ const ctx = nodeCtx;
1734
+ const cwd = options.getCwd();
1798
1735
  const startTime = Date.now();
1799
- const customTabs = options.getCustomTabs?.() ?? [];
1800
- const envData = snapshotEnv(options.getCwd());
1801
- const initialInfo = options.getScanResult ? buildDevToolsInfo({
1802
- scan: options.getScanResult(),
1803
- configMeta: options.getConfigMeta?.() ?? null,
1804
- customTabs,
1805
- ai: options.ai,
1806
- startTime,
1807
- envData
1808
- }) : emptyDevToolsInfo(startTime);
1809
- for (const tab of customTabs) ctx.docks.register({
1810
- id: `ubean:custom:${tab.id}`,
1811
- title: tab.label,
1812
- icon: tab.icon || "lucide:plugin",
1813
- type: "iframe",
1814
- url: tab.src,
1815
- category: "framework",
1816
- defaultOrder: 50
1817
- });
1818
- const state = await initSharedState(ctx, initialInfo);
1819
- const aiStreamState = await ctx.rpc.sharedState.get("ubean:ai:stream", { initialValue: {
1736
+ const scoped = ctx.scope(UBEAN_RPC_NAMESPACE);
1737
+ const envData = snapshotEnv(cwd);
1738
+ const buildInfo = () => {
1739
+ const scan = options.getScanResult?.() ?? null;
1740
+ return scan ? buildDevToolsInfo({
1741
+ scan,
1742
+ configMeta: options.getConfigMeta?.() ?? null,
1743
+ customTabs: options.getCustomTabs?.() ?? [],
1744
+ ai: options.ai,
1745
+ startTime,
1746
+ envData
1747
+ }) : emptyDevToolsInfo(startTime);
1748
+ };
1749
+ const state = await initSharedState(ctx, buildInfo());
1750
+ const aiStreamState = await ctx.rpc.sharedState.get(UBEAN_AI_STREAM_STATE_KEY, { initialValue: {
1820
1751
  requestId: "",
1821
1752
  text: "",
1822
1753
  done: true
1823
1754
  } });
1824
- const hooks = createDevToolsHooks();
1825
1755
  const crud = createCrudServer({
1826
- cwd: options.getCwd(),
1827
- hooks,
1756
+ cwd,
1757
+ hooks: createDevToolsHooks(),
1828
1758
  scaffoldOps: options.scaffoldOps,
1829
1759
  getEnv: () => envData,
1830
1760
  setEnv: (env) => {
@@ -1846,20 +1776,27 @@ function ubeanDevtoolsPlugin(options = { getCwd: () => process.cwd() }) {
1846
1776
  terminal: createTerminalServer(),
1847
1777
  getApp: options.getApp
1848
1778
  });
1849
- for (const fn of fns) ctx.rpc.register(fn);
1779
+ for (const fn of fns) scoped.rpc.register(fn);
1780
+ for (const tab of options.getCustomTabs?.() ?? []) ctx.docks.register({
1781
+ id: `ubean:custom:${tab.id}`,
1782
+ title: tab.label,
1783
+ icon: tab.icon || "lucide:plugin",
1784
+ type: "iframe",
1785
+ url: tab.src,
1786
+ category: "framework",
1787
+ defaultOrder: 50
1788
+ });
1850
1789
  const refresh = () => {
1851
- const info = options.getScanResult ? buildDevToolsInfo({
1852
- scan: options.getScanResult(),
1853
- configMeta: options.getConfigMeta?.() ?? null,
1854
- customTabs: options.getCustomTabs?.() ?? [],
1855
- ai: options.ai,
1856
- startTime,
1857
- envData
1858
- }) : initialInfo;
1859
- refreshSharedState(state, info);
1790
+ refreshSharedState(state, buildInfo());
1860
1791
  };
1861
1792
  options.registerRefresh?.(refresh);
1862
- } }
1793
+ }
1794
+ };
1795
+ }
1796
+ function ubeanDevtoolsPlugin(options) {
1797
+ return {
1798
+ name: "ubean:devtools",
1799
+ __ubeanDevframe: createUbeanDevframe(options)
1863
1800
  };
1864
1801
  }
1865
1802
  /**
@@ -1915,18 +1852,38 @@ function snapshotEnv(cwd) {
1915
1852
  }
1916
1853
  //#endregion
1917
1854
  //#region src/define-tab.ts
1918
- const customTabs = [];
1855
+ /**
1856
+ * 自定义 tab 注册表 —— **真·进程单例**(挂在 `globalThis` 上)。
1857
+ *
1858
+ * 不能用模块级数组:dev 下同一个进程里常常有第二份 `@ubean/devtools`(CLI 动态 `import()`
1859
+ * 一份、用户的 `vite.config.ts` 又静态引一份;pnpm 也可能装出两份),模块级状态只在同一个模块
1860
+ * 实例内共享 —— 表现是「用户 `defineDevToolsTab()` 注册了 tab,面板里永远看不到」,且**没有任何
1861
+ * 报错**。同样的理由见 `@ubean/vue` 的 matcher 注册表与 `@ubean/islands` 的服务端组件注册表
1862
+ * (AGENTS.md 第 19 条)。
1863
+ *
1864
+ * 同名重复注册会覆盖 —— HMR 重新求值时因此能拿到最新的 tab 定义。
1865
+ */
1866
+ const CUSTOM_TABS_KEY = "__ubean_devtools_custom_tabs__";
1867
+ function getCustomTabsRegistry() {
1868
+ const store = globalThis;
1869
+ const existing = store[CUSTOM_TABS_KEY];
1870
+ if (Array.isArray(existing)) return existing;
1871
+ const created = [];
1872
+ store[CUSTOM_TABS_KEY] = created;
1873
+ return created;
1874
+ }
1919
1875
  function defineDevToolsTab(tab) {
1876
+ const customTabs = getCustomTabsRegistry();
1920
1877
  const exists = customTabs.findIndex((t) => t.id === tab.id);
1921
1878
  if (exists !== -1) customTabs[exists] = tab;
1922
1879
  else customTabs.push(tab);
1923
1880
  return tab;
1924
1881
  }
1925
1882
  function getCustomTabs() {
1926
- return [...customTabs];
1883
+ return [...getCustomTabsRegistry()];
1927
1884
  }
1928
1885
  function clearCustomTabs() {
1929
- customTabs.length = 0;
1886
+ getCustomTabsRegistry().length = 0;
1930
1887
  }
1931
1888
  //#endregion
1932
- export { UBEAN_INFO_STATE_KEY, buildDevToolsInfo, clearCustomTabs, createAiServer, createAllRpcFunctions, createCrudServer, createDevToolsHooks, createTerminalServer, defineDevToolsTab, defineRpcFunction, emptyDevToolsInfo, getCustomTabs, maskSensitiveEnv, ubeanDevtoolsPlugin };
1889
+ export { UBEAN_AI_STREAM_STATE_KEY, UBEAN_ANCHOR_DOCK, UBEAN_INFO_STATE_KEY, UBEAN_RPC_NAMESPACE, buildDevToolsInfo, clearCustomTabs, createAiServer, createAllRpcFunctions, createCrudServer, createDevToolsHooks, createTerminalServer, createUbeanDevframe, defineDevToolsTab, emptyDevToolsInfo, getCustomTabs, maskSensitiveEnv, ubeanDevtoolsPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/devtools",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "DevTools for the ubean framework — server middleware, RPC, and the iframe SPA client",
5
5
  "files": [
6
6
  "dist"
@@ -27,22 +27,23 @@
27
27
  "@codemirror/state": "6.7.6",
28
28
  "@codemirror/theme-one-dark": "6.1.3",
29
29
  "@codemirror/view": "6.43.14",
30
+ "@devframes/hub": "^1.2.5",
31
+ "@devframes/hub-ui": "^1.2.5",
32
+ "@devframes/vite": "^1.2.5",
30
33
  "@soybeanjs/unocss-preset": "^0.2.0",
31
34
  "@types/node": "^26.6.5",
32
35
  "@vean/ui": "^0.50.2",
33
36
  "@vean/unocss": "^0.50.2",
34
- "@vitejs/devtools": "^0.7.6",
35
- "@vitejs/devtools-kit": "^0.7.6",
36
37
  "@vitejs/plugin-vue": "^6.0.10",
37
38
  "@xterm/addon-fit": "^0.11.0",
38
39
  "@xterm/xterm": "^6.0.0",
39
- "devframe": "^1.2.3",
40
+ "devframe": "^1.2.5",
40
41
  "hono": "4.13.13",
41
42
  "typescript": "npm:typescript-native-bridge@latest",
42
- "unocss": "^66.10.5",
43
- "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
44
- "vite-plus": "1.1.0",
45
- "vue": "^3.5.43"
43
+ "unocss": "^66.11.0",
44
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
45
+ "vite-plus": "1.3.0",
46
+ "vue": "^3.5.44"
46
47
  },
47
48
  "peerDependencies": {
48
49
  "hono": "^4.0.0",
@@ -57,8 +58,8 @@
57
58
  }
58
59
  },
59
60
  "optionalDependencies": {
60
- "@ai-sdk/openai-compatible": "3.0.67",
61
- "ai": "7.0.137"
61
+ "@ai-sdk/openai-compatible": "3.0.70",
62
+ "ai": "7.0.142"
62
63
  },
63
64
  "scripts": {
64
65
  "build": "pnpm run build:server && pnpm run build:client",