@aiti-ai/aiti-msg-nav 0.0.0-stage → 0.1.7

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,52 @@
1
+ /**
2
+ * deploy 脚本共享「部署状态 commit」助手(pi-web-ext 各扩展 tools/deploy.mjs 统一用)。
3
+ *
4
+ * 背景(2026-08-30 排障,session 0cc6ad47 op264):重复部署同内容时
5
+ * `git add -A && git commit` 以 "nothing to commit" exit 1,而 stdio:"ignore" 吞掉
6
+ * stderr,输出「提交失败(忽略):Command failed: …」假报警(msg-smart 实证);
7
+ * 另有 --allow-empty 族每次部署都造空 commit 噪音。本助手统一三态语义:
8
+ * committed → 有暂存变更,已提交
9
+ * clean → 无变更,跳过(不算失败)
10
+ * failed → git 真失败(带 stderr 首行,不阻断部署——部署本体永远优先)
11
+ *
12
+ * 用法:
13
+ * import { deployCommit } from "../../_shared/deploy-commit.mjs";
14
+ * deployCommit({ repo: extRepo, name: "aiti-msg-smart", paths: ["aiti-msg-smart"],
15
+ * log: (m) => console.log("[deploy] " + m) });
16
+ *
17
+ * 红线:本助手永不 throw、不触碰部署产物路径之外的任何东西;只对 <repo> 做
18
+ * add/diff/commit(默认 --no-verify,extRepo 无 hooks,防未来误挂)。
19
+ */
20
+ import { execFileSync } from "node:child_process";
21
+
22
+ export function deployCommit({ repo, name, paths = null, extraMsg = "", ident = false, log = console.log }) {
23
+ const git = (args) =>
24
+ execFileSync("git", args, { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
25
+ const firstLine = (s) => String(s || "").trim().split("\n")[0];
26
+ try {
27
+ // 暂存:默认只收本扩展子目录(与 add <ext> 行为对齐;避免把别的扩展的
28
+ // 未提交改动卷进本扩展的部署 commit);paths=null 时退回 add -A 全仓。
29
+ git(["add", ...(paths && paths.length ? ["--", ...paths] : ["-A"])]);
30
+ let dirty;
31
+ try {
32
+ git(["diff", "--cached", "--quiet"]);
33
+ dirty = false; // exit 0 = 暂存区与 HEAD 一致
34
+ } catch (e) {
35
+ if (e.status !== 1) throw e; // 1 = 有变更;其他才是真错误
36
+ dirty = true;
37
+ }
38
+ if (!dirty) {
39
+ log("扩展仓库无变化(跳过提交)");
40
+ return { status: "clean" };
41
+ }
42
+ const stamp = new Date().toISOString().replace("T", " ").slice(0, 19);
43
+ const msg = `[deploy] ${name} · ${stamp}${extraMsg ? " · " + extraMsg : ""}`;
44
+ git([...(ident ? ["-c", "user.name=deploy", "-c", "user.email=deploy@local"] : []), "commit", "--no-verify", "-m", msg]);
45
+ log("扩展仓库已提交:" + msg);
46
+ return { status: "committed", msg };
47
+ } catch (e) {
48
+ const reason = firstLine(e.stderr) || firstLine(e.message) || "git error";
49
+ log("提交失败(忽略,不影响部署):" + reason);
50
+ return { status: "failed", reason };
51
+ }
52
+ }
package/dev-pack.mjs ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * pi-web-ext · dev-pack 共享实现(方案 §3.2 / M5:扩展 dev-pack——src + 构建工具 + AGENTS.md 直达部署目录)
3
+ *
4
+ * 目标:用户在 AI 窗对 AI 说「帮我改一下 xxx 扩展」时,AI 能在**部署目录**直接:
5
+ * 读 AGENTS.md → 改 src/ → node tools/build.mjs → curl reload → 浏览器生效,不等回开发机。
6
+ *
7
+ * 各扩展 tools/deploy.mjs 末尾统一追加一段(见本文件同级各 deploy.mjs 模式):
8
+ * const { packDevPack } = await import("../../_shared/dev-pack.mjs");
9
+ * packDevPack({ extRoot: <扩展源码根>, log: console.log });
10
+ *
11
+ * packDevPack 向每个已存在的部署目录(~/.pi/web{,-dev}/extensions/<extId>)写入:
12
+ * AGENTS.md 源码目录有则拷,无则由模板生成(§3.4)
13
+ * src/ client/src/**、根级服务端源码 *.ts/*.cjs/*.mjs、lib/(跳 vendor)、server/、sched/、worker-src/
14
+ * tools/build.mjs 现场构建入口(扩展自带则拷真身,否则生成 esbuild 默认版)
15
+ * tools/deploy.mjs 部署脚本本身(大改后回源码仓重部署的入口)
16
+ * package.json 含 aitiConfigs 声明与 build 脚本
17
+ * vite.config.ts 有则拷
18
+ *
19
+ * 幂等:重复部署整目录覆盖;允许失败:任何异常只 log 不影响 deploy 退出码(调用方 try/catch)。
20
+ */
21
+ "use strict";
22
+ import { homedir } from "node:os";
23
+ import { join, resolve, dirname, basename, sep as pathSep } from "node:path";
24
+ import {
25
+ existsSync, readdirSync, statSync, mkdirSync, cpSync, rmSync, writeFileSync, readFileSync,
26
+ } from "node:fs";
27
+
28
+ // 根级服务端源码文件:拷进 src/(运行时平铺文件在部署根不动,宿主 loader 路径不变)
29
+ const ROOT_SRC_EXTS = new Set([".ts", ".tsx", ".cjs", ".mjs"]);
30
+ const ROOT_SRC_SKIP = new Set(["dist", "node_modules"]);
31
+ // 源码子目录:整目录拷(vendor 类大资产跳过——部署目录本就有运行时副本)
32
+ const SRC_DIRS = ["client/src", "server", "lib", "sched", "worker-src"];
33
+ const DIR_SKIP = new Set(["vendor", "__pycache__", "node_modules"]);
34
+
35
+ function copyTree(src, dest, skip) {
36
+ mkdirSync(dest, { recursive: true });
37
+ for (const en of readdirSync(src, { withFileTypes: true })) {
38
+ if (skip && skip.has(en.name)) continue;
39
+ const s = join(src, en.name), d = join(dest, en.name);
40
+ if (en.isDirectory()) copyTree(s, d, skip);
41
+ else if (en.isFile()) cpSync(s, d);
42
+ }
43
+ }
44
+
45
+ /** AGENTS.md 默认模板(§3.4;扩展自带 AGENTS.md 则不走到这里)。供 CLI 批量生成源码目录版复用。 */
46
+ export function agentsMdTemplate(extId) {
47
+ return `# ${extId} 扩展(部署目录直改指南)
48
+
49
+ ## 结构
50
+ - 运行时文件平铺在本目录根(index.ts / core.cjs …,宿主 loader 按相对路径加载——**禁手改运行时平铺文件**,改 src/ 再 build)
51
+ - \`src/\` 源码(client/src + 服务端 ts/cjs)· \`dist/\` 构建产物(禁手改)· \`tools/\` 构建/部署脚本
52
+ - npm 包形态的扩展:本目录 package.json 含 version 与 aitiConfigs 声明
53
+
54
+ ## 小改现场流程(改文案/样式/小逻辑)
55
+ 1. 改 \`src/\` 下对应源码
56
+ 2. \`node tools/build.mjs\`(client 有构建产物的扩展;纯服务端改动跳过)
57
+ 3. \`curl -X POST http://127.0.0.1:3001/api/extensions/reload\`(dev;prod 3002;外部经 nginx 带 basePath:/dev、/mac、/mdev、/win、/wdev → \`/xxx/api/extensions/reload\`)
58
+ 4. 浏览器刷新页面验证
59
+
60
+ ## 大改回源码仓
61
+ - 源码仓:\`extensions/${extId}/\`(git 管理,deploy 同步回本目录)
62
+ - 改完在源码仓跑 \`node tools/deploy.mjs\`(或仓库根 \`node scripts/deploy-all.mjs --only ${extId}\`)
63
+
64
+ ## 数据目录(数据分层:config / cache)
65
+ - \`ctx.configDir\` 根级 = 配置与关键小状态(aiti-configs 备份范围;小、珍贵、人可读)
66
+ - \`ctx.cacheDir\`(= \`<configDir>/cache/\`)= 可删缓存:db/venv/日志/大文件;rm -rf 后扩展下次访问自动重建
67
+ - \`ctx.dataDir\` 已移除(访问即抛)——新代码禁用
68
+ - 本扩展白名单(永不为 cache 的配置文件)见 package.json \`aitiConfigs.configFiles\`
69
+
70
+ ## 红线
71
+ - 禁轮询(setInterval 拉取);需要等待用事件/回调/SSE(桥的 SSE、aiti-event-bus topics)
72
+ - 禁重启生产服务(pi-web 3002 等 restart 须用户明确同意)
73
+ - 禁手改 dist/ 与运行时平铺文件;跨扩展通信走 ctx.services(禁 globalThis.__ext 直穿)
74
+ - 服务端:SQLite 一律 WAL + 分批小事务(批间 setImmediate 让步);禁 execFileSync 调 >1s 外部程序;>8MB 文件禁同步读
75
+ `;
76
+ }
77
+
78
+ /** 默认 tools/build.mjs(esbuild 单包零配置;扩展自带 tools/build.mjs 则不走到这里)。 */
79
+ function buildMjsTemplate(extId, extRoot, clientOut, entryRel) {
80
+ const repoHint = "/home/pi/workspace/pi-work"; // 现场构建探测源码仓 node_modules 用(三机各不同,缺失时走 npm exec)
81
+ return `/**
82
+ * ${extId} · 现场构建入口(dev-pack 默认模板,esbuild 单包零配置)
83
+ * 用法:node tools/build.mjs
84
+ * 本文件由 dev-pack 生成;复杂构建(多入口/插件链)请参照 src/vite.config.ts 改写本文件或回源码仓构建。
85
+ */
86
+ "use strict";
87
+ import { existsSync, readdirSync, constants as fsConstants, accessSync } from "node:fs";
88
+ import { join, resolve, dirname, basename, sep as pathSep } from "node:path";
89
+ import { fileURLToPath } from "node:url";
90
+ import { spawnSync } from "node:child_process";
91
+
92
+ const here = dirname(fileURLToPath(import.meta.url));
93
+ const extRoot = resolve(here, "..");
94
+ const clientOut = ${JSON.stringify(clientOut || "dist/client-bundle.mjs")};
95
+ const entryRel = ${JSON.stringify(entryRel || "")};
96
+
97
+ // esbuild 解析:部署目录 node_modules → 源码仓 node_modules(本机开发场景)→ PATH 上的 esbuild → npm exec 现拉。
98
+ // 注意:node_modules/esbuild/bin/esbuild 是原生二进制(ELF/Mach-O),必须直接执行(不能当 JS 跑)。
99
+ function findEsbuild() {
100
+ const home = (process.env.HOME || process.env.USERPROFILE || "").split(pathSep).join("/");
101
+ const candidates = [
102
+ extRoot + "/node_modules/esbuild/bin/esbuild",
103
+ home + "/workspace/pi-work/node_modules/esbuild/bin/esbuild",
104
+ "D:/workspace/pi-work/node_modules/esbuild/bin/esbuild", // 155 win(源码在 D 盘)
105
+ ];
106
+ for (const c of candidates) {
107
+ if (!existsSync(c)) continue;
108
+ try { accessSync(c, fsConstants.X_OK); return [c]; } catch (e) { /* 无执行位 → 下一个来源 */ }
109
+ }
110
+ const r = spawnSync("esbuild", ["--version"], { encoding: "utf8" });
111
+ if (r.status === 0) return ["esbuild"];
112
+ return ["npm", "exec", "--yes", "esbuild", "--"];
113
+ }
114
+
115
+ function pickEntry() {
116
+ if (entryRel) return entryRel;
117
+ const srcDir = join(extRoot, "src", "client", "src");
118
+ if (!existsSync(srcDir)) throw new Error("找不到 client 源码目录 src/client/src");
119
+ for (const name of ["entry.tsx", "entry.ts", "entry.jsx", "entry.js", "index.tsx", "index.ts"]) {
120
+ if (existsSync(join(srcDir, name))) return "client/src/" + name;
121
+ }
122
+ const first = readdirSync(srcDir).find((f) => /\\.(tsx?|jsx?)$/.test(f));
123
+ if (!first) throw new Error("client/src 下没有可构建入口");
124
+ return "client/src/" + first;
125
+ }
126
+
127
+ // 服务端源码同步:src/ 根级文件(index.ts/core.cjs/package.json/vite.config.ts 等)→ 部署根
128
+ // (服务端没有构建产物——jiti 直接加载部署根文件;不 sync 则「改 src → reload」永远不生效)
129
+ import { readdirSync as _rd, copyFileSync as _cp, statSync as _st } from "node:fs";
130
+ const srcRoot = join(extRoot, "src");
131
+ if (existsSync(srcRoot)) {
132
+ for (const f of _rd(srcRoot)) {
133
+ const fp = join(srcRoot, f);
134
+ if (!_st(fp).isFile()) continue;
135
+ _cp(fp, join(extRoot, f));
136
+ }
137
+ console.log("[build] 服务端源码已从 src/ 同步到部署根");
138
+ }
139
+
140
+ const entry = pickEntry();
141
+ const args = [join(extRoot, "src", entry), "--bundle", "--format=esm",
142
+ "--outfile=" + join(extRoot, clientOut), "--jsx=automatic"];
143
+ // pi-web host-shim 委托(react → 本地 shim;jsx-runtime 同理)——与各扩展 vite.config 同款
144
+ const shim = join(extRoot, "src", "client", "src", "host-shim.ts");
145
+ const jsxRuntime = ["jsx-runtime.ts", "jsx-runtime.tsx", "jsx-dev-runtime.ts"].map((f) => join(extRoot, "src", "client", "src", f)).find(existsSync);
146
+ if (existsSync(shim)) args.push("--alias:react=" + shim);
147
+ if (jsxRuntime) args.push("--alias:react/jsx-runtime=" + jsxRuntime);
148
+ args.push("--log-level=info");
149
+
150
+ const [cmd, ...pre] = findEsbuild();
151
+ console.log("[build] esbuild:", cmd, ...pre);
152
+ const r = spawnSync(cmd, [...pre, ...args], { stdio: "inherit", cwd: extRoot });
153
+ if (r.status !== 0) { console.error("[build] 失败——可参照 src/vite.config.ts 调整本文件后重试,或回源码仓构建"); process.exit(r.status ?? 1); }
154
+ console.log("[build] ✓ " + clientOut + "(reload 后浏览器刷新生效)");
155
+ `;
156
+ }
157
+
158
+ /**
159
+ * 主入口:向已存在的部署目录写 dev-pack。
160
+ * @param {object} opts
161
+ * @param {string} opts.extRoot 扩展源码根(pi-web-ext/<extId>)
162
+ * @param {string[]} [opts.targets] 显式部署目录;缺省 = ~/.pi/web{,-dev}/extensions/<extId>(存在的才写)
163
+ * @param {(m: string) => void} [opts.log]
164
+ */
165
+ export function packDevPack(opts) {
166
+ const extRoot = resolve(opts.extRoot);
167
+ const extId = basename(extRoot);
168
+ const log = opts.log || (() => {});
169
+ const targets = (opts.targets || [
170
+ join(homedir(), ".pi", "web", "extensions", extId),
171
+ join(homedir(), ".pi", "web-dev", "extensions", extId),
172
+ ]).filter((t) => existsSync(t));
173
+ if (!targets.length) return;
174
+
175
+ // 1) 打包源码到临时区(staging)→ 逐目标 cpSync(多部署目录零重复扫描)
176
+ const stage = join(extRoot, "dist", ".dev-pack-staging");
177
+ rmSync(stage, { recursive: true, force: true });
178
+ mkdirSync(stage, { recursive: true });
179
+ try {
180
+ // src/:根级源码文件 + 源码子目录
181
+ const srcDir = join(stage, "src");
182
+ mkdirSync(srcDir, { recursive: true });
183
+ for (const en of readdirSync(extRoot, { withFileTypes: true })) {
184
+ if (en.isDirectory()) continue;
185
+ if (ROOT_SRC_SKIP.has(en.name)) continue;
186
+ if (!ROOT_SRC_EXTS.has(en.name.slice(en.name.lastIndexOf(".") || 0)) && en.name !== "package.json") continue;
187
+ cpSync(join(extRoot, en.name), join(srcDir, en.name));
188
+ }
189
+ for (const d of SRC_DIRS) {
190
+ const s = join(extRoot, d);
191
+ if (!existsSync(s)) continue;
192
+ copyTree(s, join(srcDir, ...d.split("/")), DIR_SKIP);
193
+ }
194
+ for (const f of ["vite.config.ts", "package.json"]) {
195
+ if (existsSync(join(extRoot, f))) cpSync(join(extRoot, f), join(srcDir, f));
196
+ }
197
+ // tools/build.mjs:扩展自带拷真身,否则生成默认 esbuild 版
198
+ mkdirSync(join(stage, "tools"), { recursive: true });
199
+ if (existsSync(join(extRoot, "tools", "build.mjs"))) {
200
+ cpSync(join(extRoot, "tools", "build.mjs"), join(stage, "tools", "build.mjs"));
201
+ } else {
202
+ let clientOut = "";
203
+ try {
204
+ const pkg = JSON.parse(readFileSync(join(extRoot, "package.json"), "utf8"));
205
+ if (pkg.client && String(pkg.client).startsWith("dist/")) clientOut = pkg.client;
206
+ } catch (e) {}
207
+ if (!clientOut) {
208
+ // 回退:client 产物名常声明在 entry 的 client: "dist/<id>-client.mjs"(index.ts)——从 staging 的 src 里解析
209
+ try {
210
+ for (const f of readdirSync(srcDir)) {
211
+ if (!/\.tsx?$/.test(f)) continue;
212
+ const m = readFileSync(join(srcDir, f), "utf8").match(/client\s*:\s*["'`](dist\/[\w.-]+\.mjs)["'`]/);
213
+ if (m) { clientOut = m[1]; break; }
214
+ }
215
+ } catch (e) {}
216
+ }
217
+ writeFileSync(join(stage, "tools", "build.mjs"), buildMjsTemplate(extId, extRoot, clientOut, ""));
218
+ }
219
+ // tools/deploy.mjs 本身
220
+ if (existsSync(join(extRoot, "tools", "deploy.mjs"))) {
221
+ cpSync(join(extRoot, "tools", "deploy.mjs"), join(stage, "tools", "deploy.mjs"));
222
+ }
223
+ // AGENTS.md:自带优先,否则模板生成
224
+ if (existsSync(join(extRoot, "AGENTS.md"))) cpSync(join(extRoot, "AGENTS.md"), join(stage, "AGENTS.md"));
225
+ else writeFileSync(join(stage, "AGENTS.md"), agentsMdTemplate(extId));
226
+
227
+ // 1.5) _shared 依赖:部署 src 里的源码可能相对引用 ../../../_shared/*(源仓出仓路径)——
228
+ // 部署根放一份 _shared(小体量、纯源码),让现场 build 的模块解析与源仓一致
229
+ const sharedSrc = resolve(extRoot, "..", "_shared");
230
+ if (existsSync(sharedSrc)) {
231
+ const sharedStage = join(stage, "_shared");
232
+ copyTree(sharedSrc, sharedStage, DIR_SKIP);
233
+ }
234
+
235
+ // 2) 落位(staging → 部署目录;package.json/vite.config.ts 双放:根级按 §3.2 结构,src 内供现场阅读)
236
+ for (const t of targets) {
237
+ cpSync(stage, t, { recursive: true });
238
+ for (const f of ["package.json", "vite.config.ts"]) {
239
+ if (existsSync(join(stage, f))) cpSync(join(stage, f), join(t, f));
240
+ }
241
+ }
242
+ log(`dev-pack → ${targets.map((t) => t.replace(homedir(), "~")).join(", ")}(AGENTS.md + src/ + tools/)`);
243
+ } finally {
244
+ rmSync(stage, { recursive: true, force: true });
245
+ }
246
+ }