@qomicex/cli 0.1.1 → 0.1.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.
@@ -0,0 +1,30 @@
1
+ // qomicex bump — 递增 manifest.json 版本号(major/minor/patch)。
2
+ import { existsSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { readManifestFile } from "../lib/project.js";
5
+ import { fail, info } from "../lib/io.js";
6
+ const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/;
7
+ export async function bumpCommand(opts) {
8
+ const manifestFile = join(process.cwd(), 'manifest.json');
9
+ if (!existsSync(manifestFile))
10
+ fail('当前目录不是插件项目(缺少 manifest.json)');
11
+ const manifest = readManifestFile(manifestFile);
12
+ const current = String(manifest.version ?? '');
13
+ const next = opts.version ?? (() => {
14
+ const m = SEMVER_RE.exec(current);
15
+ if (!m)
16
+ fail(`当前版本不是合法 semver: ${current}`);
17
+ const [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])];
18
+ if (opts.part === 'major')
19
+ return `${major + 1}.0.0`;
20
+ if (opts.part === 'patch')
21
+ return `${major}.${minor}.${patch + 1}`;
22
+ return `${major}.${minor + 1}.0`;
23
+ })();
24
+ if (!SEMVER_RE.test(next))
25
+ fail(`目标版本不是合法 semver: ${next}`);
26
+ manifest.version = next;
27
+ writeFileSync(manifestFile, JSON.stringify(manifest, null, 2) + '\n');
28
+ info(`✔ manifest.json version: ${current} → ${next}`);
29
+ return next;
30
+ }
@@ -48,21 +48,26 @@ export async function packCommand(opts = {}) {
48
48
  if (!id)
49
49
  fail('manifest.json 缺少 id');
50
50
  let entries = buildPackageEntries(root, manifest);
51
- // 可选签名(仅 signature.json;需要证书链请用 publish)
51
+ // 可选签名(signature.json + signature.cert.json;无商店证书时生成自签证书)
52
52
  if (opts.key) {
53
53
  const keyContent = await readKeyInput(opts.key);
54
54
  const priv = parsePrivateKey(keyContent);
55
- const { derivePublicKey } = await import("../lib/signature.js");
55
+ const { derivePublicKey, makeSelfSignedCert } = await import("../lib/signature.js");
56
56
  const pub = await derivePublicKey(priv);
57
57
  const keyId = `ed25519:${pub.slice(0, 8)}`;
58
58
  const localCert = join(root, 'signature.cert.json');
59
59
  let certJson;
60
- if (existsSync(localCert))
60
+ if (existsSync(localCert)) {
61
61
  certJson = readFileSync(localCert, 'utf8');
62
+ }
63
+ else {
64
+ const name = String(manifest.name ?? '');
65
+ certJson = await makeSelfSignedCert(priv, keyId, name);
66
+ writeFileSync(localCert, certJson);
67
+ warn('未找到本地签名证书,已生成自签证书并写入 signature.cert.json;上传商店需先在开发者中心注册对应公钥');
68
+ }
62
69
  const sig = await signPackage(entries, priv, keyId, certJson);
63
70
  entries = { ...entries, ...sig };
64
- if (!certJson)
65
- warn('缺少本地 signature.cert.json,商店上传仍需证书;建议改用 qomicex publish 走完整签名');
66
71
  }
67
72
  const outDir = resolve(root, opts.outDir ?? 'release');
68
73
  mkdirSync(outDir, { recursive: true });
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
- // qomicex CLI 入口:create / dev / pack / verify / publish
2
+ // qomicex CLI 入口:create / dev / pack / verify / bump / publish
3
3
  import { createCommand } from "./commands/create.js";
4
4
  import { devCommand } from "./commands/dev.js";
5
5
  import { packCommand } from "./commands/pack.js";
6
6
  import { verifyCommand } from "./commands/verify.js";
7
7
  import { publishCommand } from "./commands/publish.js";
8
+ import { bumpCommand } from "./commands/bump.js";
8
9
  import { fail } from "./lib/io.js";
9
- const VERSION = '0.1.0';
10
+ import { createRequire } from 'node:module';
11
+ // 版本号单一来源:package.json(bump 只改一处)
12
+ const VERSION = createRequire(import.meta.url)('../package.json').version;
10
13
  function parseArgs(argv) {
11
14
  const positional = [];
12
15
  const options = {};
@@ -49,6 +52,8 @@ qomicex v${VERSION} — Qomicex 插件生态 CLI
49
52
  构建并打 .qplugin(manifest.json 在 zip 根)
50
53
  qomicex verify [--package <f>]
51
54
  manifest 合法性 + 权限最小化 + 长循环告警 + 签名检查
55
+ qomicex bump <major|minor|patch> [--version <v>]
56
+ 递增 manifest.json 版本号(或 --version 直接指定)
52
57
  qomicex publish [--key <k>] [--slug <s>] [--changelog <c>] [--api <url>] [--org-id <id>] [--package <f>] [--yes]
53
58
  设备流登录 → 注册签名公钥 → 签名 → 上传到商店
54
59
  qomicex --help | -h 显示帮助
@@ -89,6 +94,12 @@ async function main() {
89
94
  case 'verify':
90
95
  await verifyCommand({ package: typeof options['package'] === 'string' ? options['package'] : undefined });
91
96
  break;
97
+ case 'bump':
98
+ await bumpCommand({
99
+ part: positional[0] ?? 'minor',
100
+ version: typeof options['version'] === 'string' ? options['version'] : undefined,
101
+ });
102
+ break;
92
103
  case 'publish':
93
104
  await publishCommand({
94
105
  key: typeof options['key'] === 'string' ? options['key'] : undefined,
@@ -44,6 +44,7 @@ export const METHOD_PERMISSIONS = {
44
44
  registerMethod: 'config:write', callPlugin: 'network:fetch', callWasm: 'wasm:execute', listWasmPlugins: 'wasm:execute',
45
45
  readText: 'filesystem:read', readBytes: 'filesystem:read', writeText: 'filesystem:write', writeBytes: 'filesystem:write', deleteFile: 'filesystem:write', execCommand: 'shell:execute',
46
46
  navigate: 'config:read', showToast: 'ui:toast', getSystemInfo: 'system:info', openUrl: 'system:notification', listPlugins: 'plugin:list',
47
+ getThemeColor: 'config:read', applyThemeOverride: 'config:write', clearThemeOverride: 'config:write',
47
48
  'overlay.create': 'ui:sub_window', 'overlay.show': 'ui:sub_window', 'overlay.hide': 'ui:sub_window',
48
49
  'overlay.destroy': 'ui:sub_window', 'overlay.setHtml': 'ui:sub_window', 'overlay.setPosition': 'ui:sub_window',
49
50
  'download.addTask': 'download:manage', 'download.progress': 'download:manage', 'download.cancel': 'download:manage', 'download.list': 'download:manage', 'download.registerInstall': 'instance:write',
@@ -8,7 +8,7 @@ export const SIGNATURE_FILE = 'signature.json';
8
8
  export const CERT_FILE = 'signature.cert.json';
9
9
  export const ALG = 'Ed25519';
10
10
  /** 商店签名根公钥(raw base64),与 launcher plugin_signature.rs ROOT_PUBLIC_KEY_B64 一致 */
11
- export const STORE_ROOT_PUBLIC_KEY_B64 = 'hNoXOazEkdTRoxBra8ABlCWXhy16S7rM5ZmEDa+GmnE=';
11
+ export const STORE_ROOT_PUBLIC_KEY_B64 = 'sPKcrc6QR5gcOnQMdq21Jo3yqxN7Mbm61OYxZnKuHE0=';
12
12
  /** Ed25519 PKCS#8 DER 固定前缀(前 16 字节),后接 32 字节 seed */
13
13
  const ED25519_PKCS8_PREFIX = Uint8Array.from([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20]);
14
14
  export function bytesToBase64(bytes) {
@@ -98,10 +98,34 @@ async function signBytes(privDer, msg) {
98
98
  const sig = await crypto.subtle.sign({ name: ALG }, key, msg);
99
99
  return bytesToBase64(new Uint8Array(sig));
100
100
  }
101
+ /** 自签开发者证书(开发者私钥签证书体):未注册商店密钥时的离线打包/本地验证用,商店上传需对应公钥已注册。 */
102
+ export async function makeSelfSignedCert(privDer, keyId, developerName = '') {
103
+ const { derivePublicKey } = await import("./signature.js");
104
+ const publicKey = await derivePublicKey(privDer);
105
+ const body = {
106
+ alg: ALG,
107
+ keyId,
108
+ developerId: '',
109
+ developerName,
110
+ publicKey,
111
+ issuedAt: new Date().toISOString(),
112
+ signature: '',
113
+ };
114
+ const certBody = canonicalJson({
115
+ alg: body.alg,
116
+ keyId: body.keyId,
117
+ developerId: body.developerId,
118
+ developerName: body.developerName,
119
+ publicKey: body.publicKey,
120
+ issuedAt: body.issuedAt,
121
+ });
122
+ body.signature = await signBytes(privDer, new TextEncoder().encode(certBody));
123
+ return JSON.stringify(body);
124
+ }
101
125
  /**
102
126
  * 对包内全部条目生成签名文件。
103
127
  * @param certJson 商店根钥签发的开发者证书 JSON(POST /developer/keys 返回值),
104
- * cert 时仅生成 signature.json(pack 场景,publish 会走完整证书链)。
128
+ * 省略时仅生成 signature.json(publish 场景由调用方传证书)。
105
129
  */
106
130
  export async function signPackage(entries, privDer, keyId, certJson) {
107
131
  const payload = await signedPayloadBytes(entries);
package/dist/lib/store.js CHANGED
@@ -65,7 +65,7 @@ export async function fetchMinePlugins(base, token) {
65
65
  }
66
66
  /** 新建插件(store 返回 201 {plugin:{id}}),slug 占用抛 409。 */
67
67
  export async function createPlugin(base, token, input) {
68
- const r = await request(base, '/plugins/', {
68
+ const r = await request(base, '/plugins', {
69
69
  method: 'POST',
70
70
  body: JSON.stringify({
71
71
  slug: input.slug,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qomicex/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,65 +1,65 @@
1
- # Qomicex 插件开发技能包(qomicex-plugin)
2
-
3
- 面向 AI agent(Claude / opencode / Cursor 等)的 Qomicex 启动器插件(`.qplugin`)开发技能包,随 `@qomicex/cli` 分发。本技能把"生成合规插件"所需的全部事实(manifest 校验、权限目录、桥 API、主题 token、签名、调试)收敛到一个目录,避免 AI 臆造字段。
4
-
5
- ## 何时使用
6
-
7
- 用户要求**开发 / 修改 / 审查 / 打包 / 发布** Qomicex 启动器插件时加载本技能。判断依据:涉及 `manifest.json`、`__PLUGIN_API__`、`entry.frontend`、`contributes`、权限声明、`.qplugin` 打包等关键词。
8
-
9
- ## 文件导航(建议全读,勿跳)
10
-
11
- | 文件 | 内容 |
12
- |------|------|
13
- | `manifest-schema.md` | manifest.json 全字段 + layers 语义 + render 默认 iframe + dependencies + contributes |
14
- | `permissions.md` | 权限目录(normal/warning/danger)+ 每个权限对应的桥 API 方法 |
15
- | `plugin-api.md` | 桥 API(`__PLUGIN_API__`)签名速查 |
16
- | `theme.md` | 主题语义 token 三级体系 + `var()` 消费约定 + 主题贡献 |
17
- | `signing.md` | Ed25519 签名流程(生成密钥 → pack --key → publish) |
18
- | `debugging.md` | harness 热重载调试 |
19
- | `rules.md` | AI 生成插件的硬性规则(必须逐条遵守) |
20
-
21
- ## 完整流程(create → dev → verify → pack → publish)
22
-
23
- ```bash
24
- # 1. 生成项目(id 需反向域名格式,如 com.example.demo)
25
- qomicex create com.example.demo
26
-
27
- # 2. 本地调试(仓库内自动进 harness,固定端口 1420;仓库外裸 Vite 默认 5173)
28
- cd com.example.demo && pnpm install && qomicex dev
29
-
30
- # 3. 校验(提交/打包前必跑,0 error 才算通过)
31
- qomicex verify
32
-
33
- # 4. 打包(tsc && vite build → release/<id>-<version>.qplugin)
34
- qomicex pack --version 0.2.0
35
- qomicex pack --key ./dev-key.pem # 附签名(仅 signature.json;完整证书链走 publish)
36
-
37
- # 5. 发布(设备流登录 → 商店签发证书 → 签名 → 上传)
38
- export QOMICEX_SIGN_KEY=<私钥 base64/PEM>
39
- qomicex publish
40
- ```
41
-
42
- ## 常见错误与规避
43
-
44
- | 错误 | 规避 |
45
- |------|------|
46
- | `id` 不含点 / 大写 / 连续双点 | id 用 `^[a-z0-9]+([.-][a-z0-9]+)*$`,3-128 字符,含至少一个点 |
47
- | `version` 非 semver | 严格 `数字.数字.数字`(可带 `-预发布` / `+构建号`) |
48
- | `entry.frontend` 指向非 `.html` | `verify` 警告:frontend 应指向 `.html`(如 `dist/index.html`) |
49
- | 声明了 frontend 但 `layers` 无 `l2`/`l3` | 无法渲染 UI;UI 插件务必声明 `l2`(iframe 沙箱,默认推荐) |
50
- | 权限声明与源码调用不一致 | `verify` 会做权限最小化扫描,声明未用 / 用了未声明**都会报错** |
51
- | 相对 import 不带扩展名 | Vite 强制:`import { x } from './api.ts'`,漏 `.ts/.tsx` 直接构建失败 |
52
- | 插件里 `fetch()` 外部 URL 被 CORS / SSRF 拦 | 用 `proxyFetch` / `proxyFetchStream`(带 SSRF 防护) |
53
- | 沙箱内 `window.open` 被拦 | 用 `callBackend('/system/open-url', { url })` 或 `openUrl` |
54
- | `vite.config.ts` 没设 `base: './'` | 产物 `/assets/...` 会被解析成站点根,插件白屏 |
55
- | 长循环 / 无界 `setInterval` | `verify` 会告警;放 Worker / WASM / 后端 |
56
- | 硬编码 API key 进插件源码 | 无密钥存储;让用户经 `setSettings` 配置,运行时 `getSettings` 读取 |
57
-
58
- ## 事实一致红线
59
-
60
- - 本技能内容与代码事实对齐(`src/plugins/types.ts`、CLI `src/lib/*`、公开文档 `docs/plugins/plugin-api.md`)。字段含义有疑问时**以代码为准**,不确定就标注,不要虚构 API 或字段。
61
- - `packages/plugin-ui`(`@qomicex/plugin-ui`)的组件库使用参见 `tailwind.config.js` 的 `@qomicex/plugin-ui/tailwind-preset`,主题类名(`bg-primary`、`text-muted-foreground` 等)直接消费语义 token。
62
-
63
- ## 起步提示词(给 AI 用)
64
-
65
- > 你是 Qomicex 插件开发工程师。先读 `skills/qomicex-plugin/` 下的 SKILL.md 与各分册(尤其 rules.md),再按流程工作:`qomicex create <id>` 生成项目 → 实现功能 → `qomicex verify` 过 0 error → `qomicex pack` 出包。manifest 字段、权限、API 签名一律以技能包内文档为准,不臆造。
1
+ # Qomicex 插件开发技能包(qomicex-plugin)
2
+
3
+ 面向 AI agent(Claude / opencode / Cursor 等)的 Qomicex 启动器插件(`.qplugin`)开发技能包,随 `@qomicex/cli` 分发。本技能把"生成合规插件"所需的全部事实(manifest 校验、权限目录、桥 API、主题 token、签名、调试)收敛到一个目录,避免 AI 臆造字段。
4
+
5
+ ## 何时使用
6
+
7
+ 用户要求**开发 / 修改 / 审查 / 打包 / 发布** Qomicex 启动器插件时加载本技能。判断依据:涉及 `manifest.json`、`__PLUGIN_API__`、`entry.frontend`、`contributes`、权限声明、`.qplugin` 打包等关键词。
8
+
9
+ ## 文件导航(建议全读,勿跳)
10
+
11
+ | 文件 | 内容 |
12
+ |------|------|
13
+ | `manifest-schema.md` | manifest.json 全字段 + layers 语义 + render 默认 iframe + dependencies + contributes |
14
+ | `permissions.md` | 权限目录(normal/warning/danger)+ 每个权限对应的桥 API 方法 |
15
+ | `plugin-api.md` | 桥 API(`__PLUGIN_API__`)签名速查 |
16
+ | `theme.md` | 主题语义 token 三级体系 + `var()` 消费约定 + 主题贡献 |
17
+ | `signing.md` | Ed25519 签名流程(生成密钥 → pack --key → publish) |
18
+ | `debugging.md` | harness 热重载调试 |
19
+ | `rules.md` | AI 生成插件的硬性规则(必须逐条遵守) |
20
+
21
+ ## 完整流程(create → dev → verify → pack → publish)
22
+
23
+ ```bash
24
+ # 1. 生成项目(id 需反向域名格式,如 com.example.demo)
25
+ qomicex create com.example.demo
26
+
27
+ # 2. 本地调试(仓库内自动进 harness,固定端口 1420;仓库外裸 Vite 默认 5173)
28
+ cd com.example.demo && pnpm install && qomicex dev
29
+
30
+ # 3. 校验(提交/打包前必跑,0 error 才算通过)
31
+ qomicex verify
32
+
33
+ # 4. 打包(tsc && vite build → release/<id>-<version>.qplugin)
34
+ qomicex pack --version 0.2.0
35
+ qomicex pack --key ./dev-key.pem # 附签名(仅 signature.json;完整证书链走 publish)
36
+
37
+ # 5. 发布(设备流登录 → 商店签发证书 → 签名 → 上传)
38
+ export QOMICEX_SIGN_KEY=<私钥 base64/PEM>
39
+ qomicex publish
40
+ ```
41
+
42
+ ## 常见错误与规避
43
+
44
+ | 错误 | 规避 |
45
+ |------|------|
46
+ | `id` 不含点 / 大写 / 连续双点 | id 用 `^[a-z0-9]+([.-][a-z0-9]+)*$`,3-128 字符,含至少一个点 |
47
+ | `version` 非 semver | 严格 `数字.数字.数字`(可带 `-预发布` / `+构建号`) |
48
+ | `entry.frontend` 指向非 `.html` | `verify` 警告:frontend 应指向 `.html`(如 `dist/index.html`) |
49
+ | 声明了 frontend 但 `layers` 无 `l2`/`l3` | 无法渲染 UI;UI 插件务必声明 `l2`(iframe 沙箱,默认推荐) |
50
+ | 权限声明与源码调用不一致 | `verify` 会做权限最小化扫描,声明未用 / 用了未声明**都会报错** |
51
+ | 相对 import 不带扩展名 | Vite 强制:`import { x } from './api.ts'`,漏 `.ts/.tsx` 直接构建失败 |
52
+ | 插件里 `fetch()` 外部 URL 被 CORS / SSRF 拦 | 用 `proxyFetch` / `proxyFetchStream`(带 SSRF 防护) |
53
+ | 沙箱内 `window.open` 被拦 | 用 `callBackend('/system/open-url', { url })` 或 `openUrl` |
54
+ | `vite.config.ts` 没设 `base: './'` | 产物 `/assets/...` 会被解析成站点根,插件白屏 |
55
+ | 长循环 / 无界 `setInterval` | `verify` 会告警;放 Worker / WASM / 后端 |
56
+ | 硬编码 API key 进插件源码 | 无密钥存储;让用户经 `setSettings` 配置,运行时 `getSettings` 读取 |
57
+
58
+ ## 事实一致红线
59
+
60
+ - 本技能内容与代码事实对齐(`src/plugins/types.ts`、CLI `src/lib/*`、公开文档 `docs/plugins/plugin-api.md`)。字段含义有疑问时**以代码为准**,不确定就标注,不要虚构 API 或字段。
61
+ - `packages/plugin-ui`(`@qomicex/plugin-ui`)的组件库使用参见 `tailwind.config.js` 的 `@qomicex/plugin-ui/tailwind-preset`,主题类名(`bg-primary`、`text-muted-foreground` 等)直接消费语义 token。
62
+
63
+ ## 起步提示词(给 AI 用)
64
+
65
+ > 你是 Qomicex 插件开发工程师。先读 `skills/qomicex-plugin/` 下的 SKILL.md 与各分册(尤其 rules.md),再按流程工作:`qomicex create <id>` 生成项目 → 实现功能 → `qomicex verify` 过 0 error → `qomicex pack` 出包。manifest 字段、权限、API 签名一律以技能包内文档为准,不臆造。
@@ -1,55 +1,55 @@
1
- # Harness 热重载调试
2
-
3
- 调试实现源:`packages/qomicex-cli/src/commands/dev.ts` + `scripts/harness/run.mjs`。
4
-
5
- ## qomicex dev 的两条路径
6
-
7
- ```bash
8
- qomicex dev # 默认
9
- qomicex dev --port 3000
10
- ```
11
-
12
- `dev` 命令从当前目录向上查找 `scripts/harness/run.mjs`:
13
-
14
- | 场景 | 行为 |
15
- |------|------|
16
- | **仓库内(harness 模式)** | 检测到 harness → spawn `scripts/harness/run.mjs`,进入完整调试环境。**插件必须位于 `plugins-dev/{id}`**(harness 从该目录定位)。`--port` 在此模式不生效(固定 1420) |
17
- | **仓库外(裸 Vite)** | 回退为 Vite dev server(默认 5173)+ 在项目根写 `.qomicex-dev.json`(dev 源插件配置,供手动注册 dev 源) |
18
-
19
- ## harness 模式做什么
20
-
21
- 不启动 Tauri、不启动 Rust 后端,纯浏览器调试:
22
-
23
- 1. 起 stub mock server(`scripts/harness/stub.mjs`,固定 `:5100`)。
24
- 2. 复用已有 Vite dev(`:1420`);未运行则自动 spawn `pnpm run dev`。
25
- 3. `addInitScript` 注入 **Tauri API mock**(`window.__TAURI_INTERNALS__` 等,跨导航保留)——否则前端在纯浏览器里无法挂载。
26
- 4. 打开插件页 `http://127.0.0.1:1420/plugins/p/{pluginId}`。
27
- 5. `fs.watch` 监听插件 `src/`(含 `index.html`/`theme.css`/`overlay.html`/`vite.config.ts`)→ 变更后重建(`pnpm run build`)→ 整页 reload(iframe 重新挂载)。
28
-
29
- **前置要求**:Playwright + Chromium:
30
-
31
- ```bash
32
- pnpm add -D playwright
33
- pnpm exec playwright install chromium
34
- ```
35
-
36
- ## 手动运行 harness
37
-
38
- ```bash
39
- node scripts/harness/run.mjs <pluginId> [--headed] [--mock file.json] [--build-cmd "pnpm run build"]
40
- # 或仓库根:pnpm run harness -- hello-plugin
41
- ```
42
-
43
- | 参数 | 说明 |
44
- |------|------|
45
- | `--headed` | 有头模式(默认 headless,看不到窗口;调试用 --headed) |
46
- | `--mock file.json` | 自定义 stub 返回(mock 数据) |
47
- | `--build-cmd "..."` | 覆盖重建命令(默认 `pnpm run build`) |
48
-
49
- ## 注意事项
50
-
51
- - **网络请求转发**:前端直连 `:5000` 的 `/api/**` 请求被 route 到 stub `:5100`。stub 返回什么,插件就拿到什么——需要真实后端数据时请先跑 Rust 后端或改 mock。
52
- - **热重载陷阱**:浏览器(Chromium) ≠ WebView2,复合/backdrop-filter 等行为可能有差异,结论需在真实 Tauri 复核。
53
- - **停止**:`Ctrl+C`,会一并清理 stub / Vite。
54
- - **浏览器直开降级**:独立 `pnpm dev`(不经 harness)时 `window.__PLUGIN_API__` 为 `null`,`getApi()` 返回 null,UI 需优雅降级(模板已处理)。
55
- - 只读操作可放心跑;避免在调试页触发写数据 / 启动实例类操作。
1
+ # Harness 热重载调试
2
+
3
+ 调试实现源:`packages/qomicex-cli/src/commands/dev.ts` + `scripts/harness/run.mjs`。
4
+
5
+ ## qomicex dev 的两条路径
6
+
7
+ ```bash
8
+ qomicex dev # 默认
9
+ qomicex dev --port 3000
10
+ ```
11
+
12
+ `dev` 命令从当前目录向上查找 `scripts/harness/run.mjs`:
13
+
14
+ | 场景 | 行为 |
15
+ |------|------|
16
+ | **仓库内(harness 模式)** | 检测到 harness → spawn `scripts/harness/run.mjs`,进入完整调试环境。**插件必须位于 `plugins-dev/{id}`**(harness 从该目录定位)。`--port` 在此模式不生效(固定 1420) |
17
+ | **仓库外(裸 Vite)** | 回退为 Vite dev server(默认 5173)+ 在项目根写 `.qomicex-dev.json`(dev 源插件配置,供手动注册 dev 源) |
18
+
19
+ ## harness 模式做什么
20
+
21
+ 不启动 Tauri、不启动 Rust 后端,纯浏览器调试:
22
+
23
+ 1. 起 stub mock server(`scripts/harness/stub.mjs`,固定 `:5100`)。
24
+ 2. 复用已有 Vite dev(`:1420`);未运行则自动 spawn `pnpm run dev`。
25
+ 3. `addInitScript` 注入 **Tauri API mock**(`window.__TAURI_INTERNALS__` 等,跨导航保留)——否则前端在纯浏览器里无法挂载。
26
+ 4. 打开插件页 `http://127.0.0.1:1420/plugins/p/{pluginId}`。
27
+ 5. `fs.watch` 监听插件 `src/`(含 `index.html`/`theme.css`/`overlay.html`/`vite.config.ts`)→ 变更后重建(`pnpm run build`)→ 整页 reload(iframe 重新挂载)。
28
+
29
+ **前置要求**:Playwright + Chromium:
30
+
31
+ ```bash
32
+ pnpm add -D playwright
33
+ pnpm exec playwright install chromium
34
+ ```
35
+
36
+ ## 手动运行 harness
37
+
38
+ ```bash
39
+ node scripts/harness/run.mjs <pluginId> [--headed] [--mock file.json] [--build-cmd "pnpm run build"]
40
+ # 或仓库根:pnpm run harness -- hello-plugin
41
+ ```
42
+
43
+ | 参数 | 说明 |
44
+ |------|------|
45
+ | `--headed` | 有头模式(默认 headless,看不到窗口;调试用 --headed) |
46
+ | `--mock file.json` | 自定义 stub 返回(mock 数据) |
47
+ | `--build-cmd "..."` | 覆盖重建命令(默认 `pnpm run build`) |
48
+
49
+ ## 注意事项
50
+
51
+ - **网络请求转发**:前端直连 `:5000` 的 `/api/**` 请求被 route 到 stub `:5100`。stub 返回什么,插件就拿到什么——需要真实后端数据时请先跑 Rust 后端或改 mock。
52
+ - **热重载陷阱**:浏览器(Chromium) ≠ WebView2,复合/backdrop-filter 等行为可能有差异,结论需在真实 Tauri 复核。
53
+ - **停止**:`Ctrl+C`,会一并清理 stub / Vite。
54
+ - **浏览器直开降级**:独立 `pnpm dev`(不经 harness)时 `window.__PLUGIN_API__` 为 `null`,`getApi()` 返回 null,UI 需优雅降级(模板已处理)。
55
+ - 只读操作可放心跑;避免在调试页触发写数据 / 启动实例类操作。