@aiti-ai/aiti-cmd 0.0.0-stage → 0.1.9
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.
Potentially problematic release.
This version of @aiti-ai/aiti-cmd might be problematic. Click here for more details.
- package/AGENTS.md +28 -0
- package/LICENSE +21 -0
- package/audit.ts +255 -0
- package/check-client.mjs +273 -0
- package/client/src/index.js +2216 -0
- package/deploy-commit.mjs +52 -0
- package/dev-pack.mjs +246 -0
- package/ext-icons-deploy.mjs +24 -0
- package/icon.svg +1 -0
- package/icons/terminal.svg +1 -0
- package/index.ts +544 -0
- package/package.json +34 -4
- package/pi-commands.ts +286 -0
- package/recognize.ts +508 -0
- package/reconcile-deps.mjs +41 -0
- package/reload-instances.mjs +60 -0
- package/runner.ts +594 -0
- package/skills/command-create/SKILL.md +47 -0
- package/skills/command-fill/SKILL.md +46 -0
- package/skills/command-fix/SKILL.md +49 -0
- package/skills/command-run/SKILL.md +41 -0
- package/skills/index.json +6 -0
- package/store.ts +228 -0
- package/tools/build-client.mjs +52 -0
- package/tools/deploy.mjs +120 -0
- package/tools/exec-integration.test.mjs +222 -0
- package/tools/recognize.test.mjs +163 -0
- package/README.md +0 -3
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiti-cmd 执行链集成方案(docs/aiti-cmd-exec-integration-plan.md)新增逻辑单测:
|
|
3
|
+
* - M0 audit:sourceDefaulted 透传、summarizeBySource 汇总(数量/最近/失败率)
|
|
4
|
+
* - M0+M2 index.ts api:/run source 收口(未知→unknown、缺省→manual+sourceDefaulted)、
|
|
5
|
+
* /shell 后端建会话、/history?groupBy=source
|
|
6
|
+
* - M1 services 适配层:run 缺省 source=service、显式 source 保留、input/resize/kill 解包纯数据
|
|
7
|
+
*
|
|
8
|
+
* index.ts/audit.ts 是 TS 且相对 import "./x.js"——node 原生无法解析,
|
|
9
|
+
* 用 esbuild(aiti-cmd 自身构建链)bundle 到临时 mjs 再 import(同 recognize.test.mjs)。
|
|
10
|
+
*/
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import test, { after } from "node:test";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
|
+
import { rmSync, mkdtempSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join, dirname } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { build } from "esbuild";
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const srcIndex = join(here, "..", "index.ts");
|
|
22
|
+
|
|
23
|
+
/** esbuild bundle index.ts → 临时 mjs(external:node-pty 保持运行时解析)。 */
|
|
24
|
+
let modPromise = null;
|
|
25
|
+
function loadExt() {
|
|
26
|
+
if (modPromise) return modPromise;
|
|
27
|
+
modPromise = (async () => {
|
|
28
|
+
const out = join(here, ".tmp-exec-integration.mjs");
|
|
29
|
+
await build({
|
|
30
|
+
entryPoints: [srcIndex],
|
|
31
|
+
bundle: true,
|
|
32
|
+
format: "esm",
|
|
33
|
+
platform: "node",
|
|
34
|
+
target: "node22",
|
|
35
|
+
outfile: out,
|
|
36
|
+
external: ["node-pty"],
|
|
37
|
+
logLevel: "silent",
|
|
38
|
+
});
|
|
39
|
+
return import(pathToFileURL(out).href);
|
|
40
|
+
})();
|
|
41
|
+
return modPromise;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 假扩展 ctx:services.provide 捕获实现;configDir/cacheDir 用一次性临时目录。 */
|
|
45
|
+
function makeCtx() {
|
|
46
|
+
const provided = {};
|
|
47
|
+
const root = mkdtempSync(join(tmpdir(), "aiti-cmd-test-"));
|
|
48
|
+
return {
|
|
49
|
+
provided,
|
|
50
|
+
ctx: {
|
|
51
|
+
id: "aiti-cmd",
|
|
52
|
+
dir: join(here, ".."),
|
|
53
|
+
configDir: join(root, "config"),
|
|
54
|
+
cacheDir: join(root, "cache"),
|
|
55
|
+
core: {},
|
|
56
|
+
services: {
|
|
57
|
+
provide: (key, impl) => { provided[key] = impl; },
|
|
58
|
+
get: (key) => provided[key],
|
|
59
|
+
},
|
|
60
|
+
features: { get: () => undefined },
|
|
61
|
+
},
|
|
62
|
+
root,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 调扩展 api 并解析 JSON(query 解析进 ctx.searchParams,同宿主行为)。 */
|
|
67
|
+
async function api(ext, method, path, body) {
|
|
68
|
+
const url = new URL("http://t" + path);
|
|
69
|
+
const req = new Request(url.href, {
|
|
70
|
+
method,
|
|
71
|
+
...(body !== undefined ? { body: JSON.stringify(body), headers: { "content-type": "application/json" } } : {}),
|
|
72
|
+
});
|
|
73
|
+
const res = await ext.default.server.api(req, { path: url.pathname.split("/").filter(Boolean), searchParams: url.searchParams });
|
|
74
|
+
return { status: res.status, body: await res.json() };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test("M1: startup 注册 aiti-cmd.runner(方法集对齐 runner)", async () => {
|
|
78
|
+
const { provided, ctx, root } = makeCtx();
|
|
79
|
+
const ext = await loadExt();
|
|
80
|
+
await ext.default.server.startup(ctx);
|
|
81
|
+
const svc = provided["aiti-cmd.runner"];
|
|
82
|
+
assert.ok(svc, "services 未注册");
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
Object.keys(svc).sort(),
|
|
85
|
+
["buffered", "diag", "getInfo", "input", "kill", "list", "resize", "run", "sessions"].sort(),
|
|
86
|
+
);
|
|
87
|
+
rmSync(root, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("M1: services run 缺省 source=service、显式 source 保留、unwrap 纯数据", async () => {
|
|
91
|
+
const { provided, ctx, root } = makeCtx();
|
|
92
|
+
const ext = await loadExt();
|
|
93
|
+
await ext.default.server.startup(ctx);
|
|
94
|
+
const svc = provided["aiti-cmd.runner"];
|
|
95
|
+
|
|
96
|
+
const r1 = svc.run({ command: "echo t1", name: "t-nosrc" });
|
|
97
|
+
assert.equal(r1.ok, true);
|
|
98
|
+
assert.equal(typeof r1.sessionId, "string");
|
|
99
|
+
assert.equal(r1.queued, false, "run 应返回纯数据(含 queued 字段)");
|
|
100
|
+
assert.equal(svc.list().find((s) => s.id === r1.sessionId).source, "service", "缺省 source 应为 service");
|
|
101
|
+
|
|
102
|
+
const r2 = svc.run({ command: "sleep 30", source: "asr-selfheal", ttlMs: 60000 });
|
|
103
|
+
assert.equal(svc.list().find((s) => s.id === r2.sessionId).source, "asr-selfheal", "显式 source 应保留");
|
|
104
|
+
|
|
105
|
+
const inp = await svc.input(r1.sessionId, "");
|
|
106
|
+
assert.ok(!(inp instanceof Response), "input 应解包为纯数据");
|
|
107
|
+
assert.equal(inp.ok, true);
|
|
108
|
+
const rs = await svc.resize(r1.sessionId, 100, 30);
|
|
109
|
+
assert.ok(!(rs instanceof Response), "resize 应解包为纯数据");
|
|
110
|
+
assert.equal(rs.cols, 100);
|
|
111
|
+
const kl = await svc.kill(r2.sessionId);
|
|
112
|
+
assert.ok(!(kl instanceof Response), "kill 应解包为纯数据");
|
|
113
|
+
assert.equal(kl.ok, true);
|
|
114
|
+
// durationMs 是存活计时(两次调用间隔内必然走表,负载下 1ms 抖动即 deepEqual 失败)——剥离后比同源性
|
|
115
|
+
const stripTiming = (rows) => (rows ?? []).map(({ durationMs, ...rest }) => rest);
|
|
116
|
+
assert.deepEqual(stripTiming(svc.sessions()), stripTiming(svc.list()), "sessions 应与 list 同源");
|
|
117
|
+
|
|
118
|
+
// 清场
|
|
119
|
+
await svc.kill(r1.sessionId);
|
|
120
|
+
ext.default.server.shutdown();
|
|
121
|
+
rmSync(root, { recursive: true, force: true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/** 轮询直到 cond 为真(审计在会话异步退出时落盘,需等待)或超时。 */
|
|
125
|
+
async function waitFor(cond, ms = 4000) {
|
|
126
|
+
const t0 = Date.now();
|
|
127
|
+
while (Date.now() - t0 < ms) {
|
|
128
|
+
if (await cond()) return true;
|
|
129
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test("M0+M2: /run source 收口、/shell 后端建会话、/history groupBy", async () => {
|
|
135
|
+
const { provided, ctx, root } = makeCtx();
|
|
136
|
+
const ext = await loadExt();
|
|
137
|
+
await ext.default.server.startup(ctx);
|
|
138
|
+
const svc = provided["aiti-cmd.runner"];
|
|
139
|
+
|
|
140
|
+
// 缺 source → 兜底 manual + 审计 sourceDefaulted
|
|
141
|
+
const a = await api(ext, "POST", "/run", { command: "echo plan-a" });
|
|
142
|
+
assert.equal(a.body.ok, true);
|
|
143
|
+
// 未知 source → 归 unknown
|
|
144
|
+
const b = await api(ext, "POST", "/run", { command: "echo plan-b", source: "no-such-src" });
|
|
145
|
+
assert.equal(b.body.ok, true);
|
|
146
|
+
// /shell → 后端建会话(命令不落前端)
|
|
147
|
+
const sh = await api(ext, "POST", "/shell", { name: "t-shell" });
|
|
148
|
+
assert.equal(sh.body.ok, true);
|
|
149
|
+
assert.ok(sh.body.sessionId, "shell 应返回 sessionId");
|
|
150
|
+
assert.equal(sh.body.name, "t-shell");
|
|
151
|
+
const info = await api(ext, "GET", "/sessions/" + sh.body.sessionId);
|
|
152
|
+
assert.equal(info.body.cmd, "$SHELL", "shell 会话命令应由后端构造");
|
|
153
|
+
// 危险命令仍需确认(协议不变)
|
|
154
|
+
const danger = await api(ext, "POST", "/run", { command: "rm -rf /tmp/x-nonexistent", source: "favorite" });
|
|
155
|
+
assert.equal(danger.status, 403);
|
|
156
|
+
assert.equal(danger.body.needConfirm, true);
|
|
157
|
+
assert.ok(danger.body.confirmToken);
|
|
158
|
+
|
|
159
|
+
// groupBy=source 汇总(等会话异步退出 → 审计落盘)
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
try { return api(ext, "GET", "/history?cmd=plan-b").then((r) => r.body.records.length > 0); } catch { return false; }
|
|
162
|
+
});
|
|
163
|
+
const g = await api(ext, "GET", "/history?groupBy=source");
|
|
164
|
+
assert.equal(g.body.groupBy, "source");
|
|
165
|
+
const bySrc = Object.fromEntries(g.body.groups.map((x) => [x.source, x]));
|
|
166
|
+
assert.ok(bySrc.unknown, "未知 source 应记为 unknown");
|
|
167
|
+
// /shell 会话归因(list 含 source;交互 shell 未退出无审计记录,退出后才入 groupBy)
|
|
168
|
+
const listed = (await api(ext, "GET", "/sessions")).body.sessions.find((s) => s.id === sh.body.sessionId);
|
|
169
|
+
assert.equal(listed.source, "shell", "shell 会话 source 应为 shell");
|
|
170
|
+
assert.equal(listed.cmd, "$SHELL");
|
|
171
|
+
// 缺 source 的记录:source=manual + sourceDefaulted 标记(逐条查)
|
|
172
|
+
const recs = await api(ext, "GET", "/history?cmd=plan-a");
|
|
173
|
+
assert.equal(recs.body.records[0].source, "manual");
|
|
174
|
+
assert.equal(recs.body.records[0].sourceDefaulted, true);
|
|
175
|
+
const recB = await api(ext, "GET", "/history?cmd=plan-b");
|
|
176
|
+
assert.equal(recB.body.records[0].source, "unknown");
|
|
177
|
+
assert.notEqual(recB.body.records[0].sourceDefaulted, true);
|
|
178
|
+
|
|
179
|
+
// 清场(先杀会话等全部退出落盘,再 shutdown,最后删目录——防 audit 迟到写 ENOENT 噪音)
|
|
180
|
+
for (const s of svc.list()) if (s.status !== "exit") svc.kill(s.id);
|
|
181
|
+
await waitFor(() => svc.list().every((s) => s.status === "exit"));
|
|
182
|
+
ext.default.server.shutdown();
|
|
183
|
+
rmSync(root, { recursive: true, force: true });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("M0: audit.summarizeBySource 数量/失败率/排序", async () => {
|
|
187
|
+
const { provided, ctx, root } = makeCtx();
|
|
188
|
+
const ext = await loadExt();
|
|
189
|
+
await ext.default.server.startup(ctx);
|
|
190
|
+
const svc = provided["aiti-cmd.runner"];
|
|
191
|
+
// 制造:2 条成功 manual + 1 条被信号杀的 shell(kill = exit 128+15 → 失败)
|
|
192
|
+
await api(ext, "POST", "/run", { command: "echo s1" });
|
|
193
|
+
await api(ext, "POST", "/run", { command: "echo s2" });
|
|
194
|
+
const sh = await svc.run({ command: "sleep 30", source: "shell" });
|
|
195
|
+
await svc.kill(sh.sessionId);
|
|
196
|
+
// 审计在会话异步退出时落盘——固定 300ms 在全量套件负载下不够(组缺失 → .count 读 undefined),条件等待
|
|
197
|
+
assert.ok(
|
|
198
|
+
await waitFor(async () => {
|
|
199
|
+
const gg = await api(ext, "GET", "/history?groupBy=source");
|
|
200
|
+
const bs = Object.fromEntries(gg.body.groups.map((x) => [x.source, x]));
|
|
201
|
+
return bs.manual?.count >= 2 && !!bs.shell;
|
|
202
|
+
}),
|
|
203
|
+
"审计落盘超时(manual×2 + shell 未入汇总)",
|
|
204
|
+
);
|
|
205
|
+
const g = await api(ext, "GET", "/history?groupBy=source");
|
|
206
|
+
const bySrc = Object.fromEntries(g.body.groups.map((x) => [x.source, x]));
|
|
207
|
+
assert.equal(bySrc.manual.count, 2);
|
|
208
|
+
assert.equal(bySrc.manual.failRate, 0);
|
|
209
|
+
assert.ok(bySrc.shell.count >= 1, "shell 记录应入汇总");
|
|
210
|
+
assert.ok(bySrc.shell.failCount >= 1, "信号杀(128+n)应计失败");
|
|
211
|
+
assert.ok(bySrc.shell.lastTs > 0);
|
|
212
|
+
// 排序:count 降序
|
|
213
|
+
const counts = g.body.groups.map((x) => x.count);
|
|
214
|
+
assert.deepEqual(counts, [...counts].sort((x, y) => y - x));
|
|
215
|
+
|
|
216
|
+
for (const s of svc.list()) if (s.status !== "exit") svc.kill(s.id);
|
|
217
|
+
ext.default.server.shutdown();
|
|
218
|
+
rmSync(root, { recursive: true, force: true });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// node-pty/ConPTY 句柄会钉住事件循环(会话常驻为产品语义)——全部用例结束后强制收口
|
|
222
|
+
after(() => { setTimeout(() => process.exit(process.exitCode || 0), 300); });
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiti-cmd recognize.ts 测试(isCommandContext 等纯函数)。
|
|
3
|
+
* recognize.ts 是 TS 且 import "./store.js"(映射 store.ts)——node 原生无法解析,
|
|
4
|
+
* 用 esbuild(aiti-cmd 自身构建链)bundle 到内存再 import。
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { existsSync, rmSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join, dirname, resolve as pathResolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { build } from "esbuild";
|
|
14
|
+
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const src = join(here, "..", "recognize.ts");
|
|
17
|
+
|
|
18
|
+
/** esbuild bundle recognize.ts → 临时 mjs,import 后返回模块。 */
|
|
19
|
+
let modPromise = null;
|
|
20
|
+
function loadRecognize() {
|
|
21
|
+
if (modPromise) return modPromise;
|
|
22
|
+
modPromise = (async () => {
|
|
23
|
+
// 产物放 aiti-cmd/tools/ 下:createRequire(import.meta.url) 向上解析 node_modules
|
|
24
|
+
// (command-exists/shell-quote 保持运行时 require)
|
|
25
|
+
const out = join(here, ".tmp-recognize.mjs");
|
|
26
|
+
await build({
|
|
27
|
+
entryPoints: [src],
|
|
28
|
+
bundle: true,
|
|
29
|
+
format: "esm",
|
|
30
|
+
platform: "node",
|
|
31
|
+
outfile: out,
|
|
32
|
+
logLevel: "silent",
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
return await import(pathToFileURL(out).href + `?t=${Date.now()}`);
|
|
36
|
+
} finally {
|
|
37
|
+
if (existsSync(out)) rmSync(out, { force: true });
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
return modPromise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test("isCommandContext: 文本开头 → true(命令语境)", async () => {
|
|
44
|
+
const { isCommandContext } = await loadRecognize();
|
|
45
|
+
assert.equal(isCommandContext("file", 0), true);
|
|
46
|
+
assert.equal(isCommandContext(" file", 2), true); // 行首前有空白
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("isCommandContext: 行首(前换行)→ true", async () => {
|
|
50
|
+
const { isCommandContext } = await loadRecognize();
|
|
51
|
+
assert.equal(isCommandContext("npm install\nfile", 12), true);
|
|
52
|
+
assert.equal(isCommandContext("a\nb\nfile", 4), true); // f 在 idx 4
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("isCommandContext: $ 提示符 → true;$ 变量引用 → false", async () => {
|
|
56
|
+
const { isCommandContext } = await loadRecognize();
|
|
57
|
+
assert.equal(isCommandContext("$ file", 2), true);
|
|
58
|
+
assert.equal(isCommandContext(" $ file", 4), true);
|
|
59
|
+
// "echo $file" 中 $ 前有命令词 → 变量引用,非命令语境
|
|
60
|
+
assert.equal(isCommandContext("echo $file", 7), false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("isCommandContext: 命令链边界 ; & | → true", async () => {
|
|
64
|
+
const { isCommandContext } = await loadRecognize();
|
|
65
|
+
assert.equal(isCommandContext("a; file", 3), true);
|
|
66
|
+
assert.equal(isCommandContext("a && file", 5), true);
|
|
67
|
+
assert.equal(isCommandContext("a | file", 3), true);
|
|
68
|
+
assert.equal(isCommandContext("a & file", 3), true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("isCommandContext: 叙述语境(中文/普通词后)→ false", async () => {
|
|
72
|
+
const { isCommandContext } = await loadRecognize();
|
|
73
|
+
assert.equal(isCommandContext("请先 file 一下", 3), false); // 中文叙述
|
|
74
|
+
assert.equal(isCommandContext("then file", 5), false); // 英文叙述
|
|
75
|
+
assert.equal(isCommandContext("<button> + file", 12), false); // 页面元素信息里的词
|
|
76
|
+
assert.equal(isCommandContext("说 file 吧", 2), false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("splitCdPrefix: cd xxx && … → workdir/action", async () => {
|
|
80
|
+
const { splitCdPrefix } = await loadRecognize();
|
|
81
|
+
assert.deepEqual(splitCdPrefix("cd pi-web-ext/aiti-helper && node tools/deploy.mjs"), { workdir: "pi-web-ext/aiti-helper", action: "node tools/deploy.mjs" });
|
|
82
|
+
assert.deepEqual(splitCdPrefix("cd /tmp; ls"), { workdir: "/tmp", action: "ls" });
|
|
83
|
+
assert.deepEqual(splitCdPrefix("cd ~/proj | pwd"), { workdir: "~/proj", action: "pwd" });
|
|
84
|
+
assert.deepEqual(splitCdPrefix("cd \"/my dir\" && ls"), { workdir: "/my dir", action: "ls" });
|
|
85
|
+
// 纯 cd 无后续动作 → 不提取
|
|
86
|
+
assert.deepEqual(splitCdPrefix("cd /tmp"), {});
|
|
87
|
+
assert.deepEqual(splitCdPrefix("cd ."), {});
|
|
88
|
+
// 非 cd 前缀 → 不提取
|
|
89
|
+
assert.deepEqual(splitCdPrefix("npm install"), {});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("resolveWorkdir: 结合会话 cwd 解析为绝对路径", async () => {
|
|
93
|
+
const { resolveWorkdir } = await loadRecognize();
|
|
94
|
+
// 相对路径 + cwd → 绝对
|
|
95
|
+
assert.equal(pathResolve(resolveWorkdir("pi-web-ext/aiti-helper", "/home/pi/workspace/pi-work")), pathResolve("/home/pi/workspace/pi-work/pi-web-ext/aiti-helper"));
|
|
96
|
+
// 含 ../ 规范化
|
|
97
|
+
assert.equal(pathResolve(resolveWorkdir("../pi-web", "/home/pi/workspace/pi-work")), pathResolve("/home/pi/workspace/pi-work/../pi-web"));
|
|
98
|
+
// 绝对路径原样
|
|
99
|
+
assert.equal(pathResolve(resolveWorkdir("/tmp", "/home/pi/workspace/pi-work")), pathResolve("/tmp"));
|
|
100
|
+
// ~ 展开
|
|
101
|
+
assert.equal(pathResolve(resolveWorkdir("~/proj", "/home/pi/workspace/pi-work")), pathResolve(homedir() + "/proj"));
|
|
102
|
+
assert.equal(pathResolve(resolveWorkdir("~", "/home/pi/workspace/pi-work")), pathResolve(homedir()));
|
|
103
|
+
// 无 cwd(行内 code 场景)→ 保持相对,不臆造基准
|
|
104
|
+
assert.equal(resolveWorkdir("pi-web-ext/aiti-helper", undefined), "pi-web-ext/aiti-helper");
|
|
105
|
+
// 空/undefined
|
|
106
|
+
assert.equal(resolveWorkdir(undefined, "/x"), undefined);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("checkCommand: cd 相对路径 → workdir 结合 cwd 解析", async () => {
|
|
110
|
+
const { checkCommand } = await loadRecognize();
|
|
111
|
+
const r = await checkCommand("cd pi-web-ext/aiti-helper && node tools/deploy.mjs", "/home/pi/workspace/pi-work");
|
|
112
|
+
assert.equal(pathResolve(r.workdir), pathResolve("/home/pi/workspace/pi-work/pi-web-ext/aiti-helper"));
|
|
113
|
+
assert.equal(r.action, "node tools/deploy.mjs");
|
|
114
|
+
// 绝对路径 cd 不受 cwd 影响
|
|
115
|
+
const r2 = await checkCommand("cd /tmp && ls", "/home/pi/workspace/pi-work");
|
|
116
|
+
assert.equal(pathResolve(r2.workdir), pathResolve("/tmp"));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("chainCommandWords: 命令链各段首命令", async () => {
|
|
120
|
+
const { chainCommandWords } = await loadRecognize();
|
|
121
|
+
assert.deepEqual(chainCommandWords("cd x && npm i"), ["cd", "npm"]);
|
|
122
|
+
assert.deepEqual(chainCommandWords("npm install && npm run build"), ["npm"]);
|
|
123
|
+
assert.deepEqual(chainCommandWords("git add . | grep src"), ["git", "grep"]);
|
|
124
|
+
assert.deepEqual(chainCommandWords("cd /tmp"), ["cd"]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("captureChain: && 连接符后继续吞(词库命令)", async () => {
|
|
128
|
+
const { extractCommandsFromText } = await loadRecognize();
|
|
129
|
+
const fs = await import("node:fs");
|
|
130
|
+
const os = await import("node:os");
|
|
131
|
+
const path = await import("node:path");
|
|
132
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "aiti-cmd-test-"));
|
|
133
|
+
try {
|
|
134
|
+
const r = await extractCommandsFromText(tmp, "cd pi-web-ext/aiti-helper && node tools/deploy.mjs");
|
|
135
|
+
const chain = r.find((c) => c.cmd.includes("&&"));
|
|
136
|
+
assert.ok(chain, "应识别出含 && 的完整命令链,实际:" + JSON.stringify(r.map((c) => c.cmd)));
|
|
137
|
+
assert.equal(chain.cmd, "cd pi-web-ext/aiti-helper && node tools/deploy.mjs");
|
|
138
|
+
assert.equal(chain.workdir, "pi-web-ext/aiti-helper");
|
|
139
|
+
assert.equal(chain.action, "node tools/deploy.mjs");
|
|
140
|
+
} finally {
|
|
141
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("captureChain: 叙述语境不误吞(中文/英文功能词停)", async () => {
|
|
146
|
+
const { extractCommandsFromText } = await loadRecognize();
|
|
147
|
+
const fs = await import("node:fs");
|
|
148
|
+
const os = await import("node:os");
|
|
149
|
+
const path = await import("node:path");
|
|
150
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "aiti-cmd-test-"));
|
|
151
|
+
try {
|
|
152
|
+
// 中文叙述:"pnpm install 然后运行" → 只认 pnpm install,不吞中文
|
|
153
|
+
const r1 = await extractCommandsFromText(tmp, "先 pnpm install 然后运行测试");
|
|
154
|
+
assert.equal(r1.length, 1);
|
|
155
|
+
assert.equal(r1[0].cmd, "pnpm install");
|
|
156
|
+
// 英文功能词后不吞:"npm install and start" → and 非命令
|
|
157
|
+
const r2 = await extractCommandsFromText(tmp, "npm install and start");
|
|
158
|
+
assert.equal(r2.length, 1);
|
|
159
|
+
assert.equal(r2[0].cmd, "npm install");
|
|
160
|
+
} finally {
|
|
161
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
package/README.md
DELETED