@windsland52/maafw-live 0.0.0-stage → 0.4.0

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.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -2
  3. package/bin/maafw-live.mjs +25 -0
  4. package/docs/daemon-protocol.md +121 -0
  5. package/docs/keyframe-retention-contract-v0.md +225 -0
  6. package/docs/session-unify-v0.md +115 -0
  7. package/lib/client/daemon.js +276 -0
  8. package/lib/commands/annotate.js +179 -0
  9. package/lib/commands/calibrate.js +61 -0
  10. package/lib/commands/color.js +73 -0
  11. package/lib/commands/crop.js +228 -0
  12. package/lib/commands/dispatch.js +121 -0
  13. package/lib/commands/env.js +409 -0
  14. package/lib/commands/index.js +70 -0
  15. package/lib/commands/input.js +257 -0
  16. package/lib/commands/kf.js +167 -0
  17. package/lib/commands/probe.js +70 -0
  18. package/lib/commands/reco.js +440 -0
  19. package/lib/commands/repl.js +1501 -0
  20. package/lib/commands/roi-check.js +273 -0
  21. package/lib/commands/roi-compare.js +388 -0
  22. package/lib/commands/runtime.js +718 -0
  23. package/lib/commands/skill.js +169 -0
  24. package/lib/commands/timing.js +247 -0
  25. package/lib/commands/version.js +66 -0
  26. package/lib/daemon/framed.mjs +3680 -0
  27. package/lib/daemon/reco_child.mjs +287 -0
  28. package/lib/exec.js +96 -0
  29. package/lib/flags.js +44 -0
  30. package/lib/image.js +90 -0
  31. package/lib/index.js +143 -0
  32. package/lib/interface/hotkey.js +142 -0
  33. package/lib/interface/index.js +12 -0
  34. package/lib/interface/jsonc.js +95 -0
  35. package/lib/interface/load.js +397 -0
  36. package/lib/interface/override.js +287 -0
  37. package/lib/interface/plan.js +279 -0
  38. package/lib/option-args.js +29 -0
  39. package/lib/pkg.js +54 -0
  40. package/lib/protocol.js +94 -0
  41. package/lib/runtime/actions.js +225 -0
  42. package/lib/runtime/keyframes.js +73 -0
  43. package/lib/runtime/log-watch.js +160 -0
  44. package/lib/runtime/node-source.js +100 -0
  45. package/lib/runtime/ocr-model.js +45 -0
  46. package/lib/runtime/pi-env.js +34 -0
  47. package/lib/runtime/pretask.js +217 -0
  48. package/lib/runtime/prune.js +28 -0
  49. package/lib/runtime/receipts.js +57 -0
  50. package/lib/runtime/reco-args.js +38 -0
  51. package/lib/runtime/reco-verdict.js +82 -0
  52. package/lib/runtime/roi-compare.js +198 -0
  53. package/lib/runtime/session-forward.js +79 -0
  54. package/lib/runtime/session-ipc.js +471 -0
  55. package/lib/runtime/session.js +135 -0
  56. package/lib/runtime/signal-guard.js +35 -0
  57. package/lib/types/client/daemon.d.ts +86 -0
  58. package/lib/types/commands/annotate.d.ts +3 -0
  59. package/lib/types/commands/calibrate.d.ts +7 -0
  60. package/lib/types/commands/color.d.ts +9 -0
  61. package/lib/types/commands/crop.d.ts +3 -0
  62. package/lib/types/commands/dispatch.d.ts +22 -0
  63. package/lib/types/commands/env.d.ts +21 -0
  64. package/lib/types/commands/index.d.ts +13 -0
  65. package/lib/types/commands/input.d.ts +17 -0
  66. package/lib/types/commands/kf.d.ts +10 -0
  67. package/lib/types/commands/probe.d.ts +15 -0
  68. package/lib/types/commands/reco.d.ts +27 -0
  69. package/lib/types/commands/repl.d.ts +10 -0
  70. package/lib/types/commands/roi-check.d.ts +3 -0
  71. package/lib/types/commands/roi-compare.d.ts +3 -0
  72. package/lib/types/commands/runtime.d.ts +66 -0
  73. package/lib/types/commands/skill.d.ts +17 -0
  74. package/lib/types/commands/timing.d.ts +64 -0
  75. package/lib/types/commands/version.d.ts +3 -0
  76. package/lib/types/exec.d.ts +31 -0
  77. package/lib/types/flags.d.ts +62 -0
  78. package/lib/types/image.d.ts +27 -0
  79. package/lib/types/index.d.ts +1 -0
  80. package/lib/types/interface/hotkey.d.ts +36 -0
  81. package/lib/types/interface/index.d.ts +11 -0
  82. package/lib/types/interface/jsonc.d.ts +9 -0
  83. package/lib/types/interface/load.d.ts +190 -0
  84. package/lib/types/interface/override.d.ts +78 -0
  85. package/lib/types/interface/plan.d.ts +68 -0
  86. package/lib/types/option-args.d.ts +19 -0
  87. package/lib/types/pkg.d.ts +14 -0
  88. package/lib/types/protocol.d.ts +124 -0
  89. package/lib/types/runtime/actions.d.ts +248 -0
  90. package/lib/types/runtime/keyframes.d.ts +43 -0
  91. package/lib/types/runtime/log-watch.d.ts +28 -0
  92. package/lib/types/runtime/node-source.d.ts +25 -0
  93. package/lib/types/runtime/ocr-model.d.ts +8 -0
  94. package/lib/types/runtime/pi-env.d.ts +24 -0
  95. package/lib/types/runtime/pretask.d.ts +53 -0
  96. package/lib/types/runtime/prune.d.ts +4 -0
  97. package/lib/types/runtime/receipts.d.ts +7 -0
  98. package/lib/types/runtime/reco-args.d.ts +18 -0
  99. package/lib/types/runtime/reco-verdict.d.ts +50 -0
  100. package/lib/types/runtime/roi-compare.d.ts +117 -0
  101. package/lib/types/runtime/session-forward.d.ts +10 -0
  102. package/lib/types/runtime/session-ipc.d.ts +117 -0
  103. package/lib/types/runtime/session.d.ts +77 -0
  104. package/lib/types/runtime/signal-guard.d.ts +19 -0
  105. package/lib/types/update-check.d.ts +20 -0
  106. package/lib/types/usage.d.ts +3 -0
  107. package/lib/update-check.js +97 -0
  108. package/lib/usage.js +50 -0
  109. package/package.json +94 -4
  110. package/skills/README.md +51 -0
  111. package/skills/maafw-live/SKILL.md +293 -0
  112. package/skills/maafw-live/references/examples.md +238 -0
  113. package/skills/maafw-live/references/pitfalls.md +211 -0
  114. package/skills/maafw-live/references/workflows.md +164 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 命令分发的唯一实现:argv → CommandResult。
3
+ *
4
+ * 为什么单列一个模块:一次性 CLI(index.ts)与命名会话常驻进程(repl --session-serve)
5
+ * 必须把 argv 走**同一条**解析→闸门→run 路径——会话里再写一份 switch 就必然漂移
6
+ *(设计审查 P0-3:REPL 的 connect project 选不了 controller、run 不吃 --option,就是这么来的)。
7
+ *
8
+ * 与入口(index.ts main)的分工:这里只做"argv 落到哪个命令、给它什么 ctx、拿回什么结果";
9
+ * 顶层 help/version/建议、--session 转发、emit 输出是入口的事,不在这里。
10
+ */
11
+ import { parseArgs } from 'node:util';
12
+ import { statSync } from 'node:fs';
13
+ import { GLOBAL_OPTIONS, readGlobalFlags } from '../flags.js';
14
+ import { EXIT, fail } from '../protocol.js';
15
+ /* 命令注册表延迟 import:commands/index 会把全部命令模块求值一遍(含 repl.ts——
16
+ * 而 repl.ts 又 import 本模块)。顶层静态 import 就是"谁先进来谁炸"的循环
17
+ * (Cannot access before initialization)。函数体里第一次调用时才加载注册表,
18
+ * 此时模块图早已稳定;同一条 import 只付一次。 */
19
+ let registryPromise = null;
20
+ function registry() {
21
+ registryPromise ??= import('./index.js');
22
+ return registryPromise;
23
+ }
24
+ function message(e) {
25
+ return e instanceof Error ? e.message : String(e);
26
+ }
27
+ /**
28
+ * 分发一条 argv(含命令名,即 `argv[0]` 是命令名)。
29
+ * 永不抛异常:BAD_ARGUMENTS / UNKNOWN_COMMAND / SESSION_REQUIRED / UNEXPECTED 都收敛成
30
+ * CommandResult——会话常驻进程不能因为一条坏命令挂掉。
31
+ */
32
+ export async function dispatchArgv(argv, env) {
33
+ const { findCommand, suggestCommand } = await registry();
34
+ const head = argv[0] ?? '';
35
+ const cmd = findCommand(head);
36
+ if (!cmd) {
37
+ const suggestion = suggestCommand(head);
38
+ return {
39
+ cmd: null,
40
+ flags: readGlobalFlags({}, env.cwd),
41
+ result: {
42
+ exitCode: EXIT.USAGE,
43
+ error: {
44
+ code: 'UNKNOWN_COMMAND',
45
+ message: `未知命令:${head}`,
46
+ ...(suggestion ? { hint: `是否想执行 ${suggestion}?` } : {}),
47
+ },
48
+ },
49
+ };
50
+ }
51
+ const options = { ...GLOBAL_OPTIONS, ...(cmd.options ?? {}) };
52
+ let parsed;
53
+ try {
54
+ parsed = parseArgs({
55
+ args: argv.slice(1),
56
+ options,
57
+ allowPositionals: true,
58
+ strict: true,
59
+ });
60
+ }
61
+ catch (e) {
62
+ return { cmd, flags: readGlobalFlags({}, env.cwd), result: fail('BAD_ARGUMENTS', message(e), `用法:${cmd.usage}`, EXIT.USAGE) };
63
+ }
64
+ const flags = readGlobalFlags(parsed.values, env.cwd);
65
+ /* 预留旗标没有消费者:传了就退 2 而不是静默吞掉——"以为在试跑其实已经落盘"
66
+ * 是 agent 最坏的失败模式。`--limit`/`--verbose` 例外:events 自己消费同名列;
67
+ * verbose 已接 describeSession 的 note 行门控。 */
68
+ const unimpl = [];
69
+ if (flags.dryRun)
70
+ unimpl.push('--dry-run');
71
+ if (parsed.values.limit !== undefined && !(cmd.options && 'limit' in cmd.options))
72
+ unimpl.push('--limit');
73
+ if (unimpl.length > 0) {
74
+ return {
75
+ cmd, flags,
76
+ result: fail('FLAG_NOT_IMPLEMENTED', `未实现的旗标:${unimpl.join(' ')}(接受但不生效是最坏的失败模式,所以直接报错)`, '要"只校验不落盘"用 validate;要截断输出用命令自己的分页参数', EXIT.USAGE),
77
+ };
78
+ }
79
+ if (flags.help) {
80
+ /* usage.ts 也 import commands/index(顶层 usage 列表):同一条环,同样延迟到
81
+ * 真用到才求值(只有 --help 才需要)。 */
82
+ const { commandUsage } = await import('../usage.js');
83
+ return { cmd, flags, result: { exitCode: EXIT.OK, human: commandUsage(cmd) } };
84
+ }
85
+ const ctx = {
86
+ positionals: parsed.positionals,
87
+ values: parsed.values,
88
+ flags,
89
+ cwd: env.cwd,
90
+ ...(env.client ? { client: env.client } : {}),
91
+ };
92
+ /* requiresSession 闸门:一次性形态(env.client 为空)下跑的是刚起的空 daemon,
93
+ * 这类命令只能给出假信号(stop 停空 daemon 回 ok、frame get 恒 NO_FRAME)——
94
+ * 退 2 + 指路,比"看起来成功了"安全得多。会话内(env.client 已注入)天然放行。 */
95
+ const needsSession = !env.client &&
96
+ (typeof cmd.requiresSession === 'function' ? cmd.requiresSession(ctx) : cmd.requiresSession === true);
97
+ if (needsSession) {
98
+ return {
99
+ cmd, flags,
100
+ result: fail('SESSION_REQUIRED', cmd.name + ' 需要会话态(一次性命令的 daemon 用完即关,这条命令依赖的会话内状态不存在)', '先起会话:maafw-live repl --session <name> --project <dir>,然后 ' + cmd.name + ' --session <name>(或直接在 REPL 里用)', EXIT.USAGE),
101
+ };
102
+ }
103
+ try {
104
+ return { cmd, flags, result: await cmd.run(ctx) };
105
+ }
106
+ catch (e) {
107
+ return {
108
+ cmd, flags,
109
+ result: fail('UNEXPECTED', message(e), '这是未预期异常,请把 --json 输出一并反馈', EXIT.FAIL),
110
+ };
111
+ }
112
+ }
113
+ export function isDirectory(p) {
114
+ try {
115
+ return statSync(p).isDirectory();
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1,409 @@
1
+ /**
2
+ * `maafw-live env` —— 环境与能力探针。
3
+ *
4
+ * 这是整个 CLI 的前置命令:技能在动手前先问它「现在有什么」,而不是各自写一遍检测。
5
+ * 因此它有一条硬要求:**它本身永远不能因为环境残缺而失败**。缺 adb、缺 python、
6
+ * 缺框架源码都是「探测结果」,不是错误。
7
+ */
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { createRequire } from 'node:module';
10
+ import { dirname, join, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { EXIT } from '../protocol.js';
13
+ import { firstLine, run } from '../exec.js';
14
+ import { loadInterface, resolveResourcePaths } from '../interface/index.js';
15
+ /** 向上找 Maa 项目根:interface.json / maa-project.json 是可靠边界 */
16
+ function findProjectRoot(start) {
17
+ let dir = resolve(start);
18
+ for (let i = 0; i < 12; i++) {
19
+ if (existsSync(join(dir, 'interface.json')) || existsSync(join(dir, 'maa-project.json')))
20
+ return dir;
21
+ const parent = dirname(dir);
22
+ if (parent === dir)
23
+ return null;
24
+ dir = parent;
25
+ }
26
+ return null;
27
+ }
28
+ /** 从 interface.json 的 resource 声明推导 pipeline 目录;找不到时退回常见目录 */
29
+ function pipelineDirs(root) {
30
+ const out = new Set();
31
+ for (const p of resolveResourcePaths(loadInterface(root)).paths) {
32
+ const dir = join(p, 'pipeline');
33
+ if (existsSync(dir))
34
+ out.add(dir);
35
+ }
36
+ for (const f of ['resource/base/pipeline', 'resource/pipeline', 'pipeline']) {
37
+ const dir = join(root, f);
38
+ if (existsSync(dir))
39
+ out.add(dir);
40
+ }
41
+ return [...out];
42
+ }
43
+ /** 解析 @maaxyz/maa-node。它可能没导出 package.json,故从入口向上找包根 */
44
+ function resolveMaaNode(from) {
45
+ try {
46
+ const req = createRequire(join(from, '__maa_cli_probe__.js'));
47
+ const entry = req.resolve('@maaxyz/maa-node');
48
+ let dir = dirname(entry);
49
+ for (let i = 0; i < 6; i++) {
50
+ const pkgPath = join(dir, 'package.json');
51
+ if (existsSync(pkgPath)) {
52
+ const j = JSON.parse(readFileSync(pkgPath, 'utf8'));
53
+ return { version: j.version ?? 'unknown', path: dir };
54
+ }
55
+ const parent = dirname(dir);
56
+ if (parent === dir)
57
+ break;
58
+ dir = parent;
59
+ }
60
+ }
61
+ catch {
62
+ /* 未安装 */
63
+ }
64
+ return null;
65
+ }
66
+ /** 本包自身所在目录:把运行时绑定当依赖解析,而不是指望调用方目录里恰好有它 */
67
+ function selfDir() {
68
+ return dirname(fileURLToPath(import.meta.url));
69
+ }
70
+ /**
71
+ * 精确版本判定:`5.14.2` / `=5.14.2` / `==5.14.2` / `v5.14.2`,以及 PEP 440 的
72
+ * 预发布/后发布后缀(`5.15.0a1`、`5.15.0b1`、`5.15.0rc1`、`5.15.0.dev1`、`5.15.0.post1`)
73
+ * 才算「就是这个版本」。
74
+ *
75
+ * 后缀不改变「唯一确定」这一点:Python 绑定发布的正是 `MaaFW==5.15.0b1` 这种写法,
76
+ * 判成范围就等于对真正要防的那个不一致视而不见。
77
+ *
78
+ * `^5.14.2`、`>=5.14` 这类范围要 semver 解析才能判等,本工具不替 npm/pip 做这件事:
79
+ * 判错了比不判更坏,调用方会据此去改依赖。
80
+ */
81
+ function exactVersion(spec) {
82
+ const m = /^(?:==|=)?\s*v?(\d+\.\d+\.\d+(?:[-+.][0-9A-Za-z.-]+|(?:a|b|c|rc|alpha|beta|pre|preview|post|dev)\d*)?)$/i.exec(spec.trim());
83
+ return m ? m[1] : null;
84
+ }
85
+ /**
86
+ * qualifier 归一化:同一个 pre-release 在两个生态里写法不同,比较前先落到同一形态。
87
+ * `-beta.1` / `beta.1` / `beta1` / `b1` → `b1`;`alpha` → `a`;`rc` / `pre` / `preview` → `rc`;
88
+ * `.dev1` → `dev1`;`.post1` → `post1`;没有后缀 → 空串(正式版)。
89
+ * 认不出的后缀原样保留:宁可把它判成不一致,也不要凭猜测把两个不相干的后缀当同一个。
90
+ */
91
+ export function normalizeQualifier(qualifier) {
92
+ const body = qualifier.trim().toLowerCase().replace(/^[-+_.]/, '');
93
+ if (body === '')
94
+ return '';
95
+ const m = /^(alpha|beta|pre|preview|rc|a|b|c|post|dev)\.?(\d*)$/.exec(body);
96
+ if (!m)
97
+ return body;
98
+ const kind = m[1];
99
+ const num = m[2] ?? '';
100
+ const canon = kind === 'alpha' ? 'a'
101
+ : kind === 'beta' ? 'b'
102
+ : kind === 'pre' || kind === 'preview' ? 'rc'
103
+ : kind;
104
+ return canon + num;
105
+ }
106
+ /**
107
+ * 把精确版本拆成 release 线与**归一化后的** qualifier:`5.15.0b1` → `5.15.0` + `b1`,
108
+ * `5.15.0-beta.1` 同样落到 `5.15.0` + `b1`。
109
+ *
110
+ * 对账要分别看两段:release 线不同是换了个版本;release 相同而 qualifier 不同是**同一条线上的
111
+ * 另一份构建**(正式版 vs 预发布、b1 vs rc1 都是)。只比字符串会把两种拼写当不一致;
112
+ * 只比 release 又会把正式版与预发布报成一致——两头都错。
113
+ */
114
+ export function splitRelease(version) {
115
+ const m = /^(\d+\.\d+\.\d+)(.*)$/.exec(version.trim());
116
+ return m ? { release: m[1], qualifier: normalizeQualifier(m[2] ?? '') } : null;
117
+ }
118
+ function pinFromPackageJson(root) {
119
+ const file = join(root, 'package.json');
120
+ if (!existsSync(file))
121
+ return null;
122
+ try {
123
+ const j = JSON.parse(readFileSync(file, 'utf8'));
124
+ for (const field of ['dependencies', 'devDependencies']) {
125
+ const group = j[field];
126
+ if (!group || typeof group !== 'object')
127
+ continue;
128
+ const v = group['@maaxyz/maa-node'];
129
+ if (typeof v === 'string' && v.trim()) {
130
+ return { source: 'package.json', declared: v.trim(), exact: exactVersion(v) };
131
+ }
132
+ }
133
+ }
134
+ catch {
135
+ /* package.json 坏了就当这个源没有 pin:探针不因环境残缺而失败 */
136
+ }
137
+ return null;
138
+ }
139
+ function pinFromRequirements(root) {
140
+ const file = join(root, 'requirements.txt');
141
+ if (!existsSync(file))
142
+ return null;
143
+ let text;
144
+ try {
145
+ text = readFileSync(file, 'utf8');
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ for (const raw of text.split(/\r?\n/)) {
151
+ /* 注释与 environment marker 都是环境噪音,只看「包名 + 版本声明」这一截。
152
+ PyPI 上的绑定包名是 `MaaFW`,老项目写成 `maa`,两种都是同一个包,大小写不敏感;
153
+ 负向前瞻保证只认独立的包名:maa-evidence-kit、maa_x、maafw2 是别的包。 */
154
+ const body = (raw.split('#')[0] ?? '').split(';')[0].trim();
155
+ const m = /^(?:maafw|maa)(?![\w.-])(?:\[[^\]]*\])?\s*(===|==|>=|<=|~=|!=|>|<|=)?\s*([^\s;#]*)$/i.exec(body);
156
+ if (!m)
157
+ continue;
158
+ return { source: 'requirements.txt', declared: body, exact: exactVersion((m[1] ?? '') + (m[2] ?? '')) };
159
+ }
160
+ return null;
161
+ }
162
+ function readMaaNodePin(root) {
163
+ const pkg = pinFromPackageJson(root);
164
+ const req = pinFromRequirements(root);
165
+ /* 两处都读:范围声明回答不了「跑的是不是同一版」这个等式,只能回答「落没落在区间里」,
166
+ 精确声明才能。所以按 package.json 精确 → requirements.txt 精确 → package.json 范围 →
167
+ requirements.txt 范围取第一个;见到 package.json 的任何声明就收工,会把另一处的精确 pin
168
+ 顶掉,而不一致正是靠那个精确 pin 才发现的。 */
169
+ for (const candidate of [pkg?.exact ? pkg : null, req?.exact ? req : null, pkg, req]) {
170
+ if (candidate)
171
+ return candidate;
172
+ }
173
+ /* 两个声明源都拿不到才记 uv.lock:它只说明「求解过」,不产生 warning */
174
+ return existsSync(join(root, 'uv.lock')) ? { source: 'uv.lock', declared: '', exact: null } : null;
175
+ }
176
+ /** Windows 的路径大小写不敏感:同一份依赖不该因为盘符/目录大小写而被报成两份 */
177
+ function samePath(a, b) {
178
+ const na = resolve(a);
179
+ const nb = resolve(b);
180
+ return process.platform === 'win32' ? na.toLowerCase() === nb.toLowerCase() : na === nb;
181
+ }
182
+ /**
183
+ * 取 checkout 的 schema 快照版本。
184
+ *
185
+ * checkout 的版本快照用 git describe 取;取不到就是空串——探针不因环境残缺而失败。
186
+ */
187
+ async function checkoutVersion(checkout, gitBin) {
188
+ const r = await run(gitBin, ['-C', checkout, 'describe', '--tags'], { timeout: 5000 });
189
+ return r.ok ? firstLine(r.stdout) : '';
190
+ }
191
+ function mark(status) {
192
+ return status === 'ok' ? '[ ok ]' : status === 'warn' ? '[warn]' : '[ -- ]';
193
+ }
194
+ function pad(s, n) {
195
+ return s.length >= n ? s : s + ' '.repeat(n - s.length);
196
+ }
197
+ export const envCommand = {
198
+ name: 'env',
199
+ summary: '探测环境与能力:项目、运行时绑定、外部工具(框架源码对账见 --checkout)',
200
+ usage: 'maafw-live env [--checkout <MaaFramework 源码目录>] [--git <bin>] [--deep]',
201
+ options: {
202
+ checkout: { type: 'string' },
203
+ maafw: { type: 'string' }, // 兼容别名:等价于 --checkout
204
+ git: { type: 'string' },
205
+ deep: { type: 'boolean' },
206
+ },
207
+ async run(ctx) {
208
+ const cwd = ctx.cwd;
209
+ /* 框架源码只服务于「版本对账」这一件事,只有框架开发与静态校验才需要它——写 Maa 应用的
210
+ 人不该被要求 clone MaaFramework,所以不传就完全不出现这一项。--maafw 保留为兼容别名。 */
211
+ const maafw = String(ctx.values.checkout ?? ctx.values.maafw ?? process.env.MAAFW_CHECKOUT ?? process.env.MAAFW_DIR ?? '');
212
+ const gitBin = String(ctx.values.git ?? 'git');
213
+ const deep = ctx.values.deep === true;
214
+ const projectRoot = findProjectRoot(cwd);
215
+ const dirs = projectRoot ? pipelineDirs(projectRoot) : [];
216
+ /* 运行时绑定是本包自己的依赖:self 才是 CLI 真正会 require 到的那份。
217
+ cwd 的副本只用来对照「项目可能 pin 了别的版本」,它不决定本 CLI 加载什么。 */
218
+ const self = resolveMaaNode(selfDir());
219
+ const cwdCopy = resolveMaaNode(cwd);
220
+ const [checkoutVer, gitV, adbV, pyV] = await Promise.all([
221
+ existsSync(maafw) ? checkoutVersion(maafw, gitBin) : Promise.resolve(''),
222
+ run(gitBin, ['--version'], { timeout: 5000 }),
223
+ run('adb', ['version'], { timeout: 5000 }),
224
+ run('python', ['--version'], { timeout: 5000 }),
225
+ ]);
226
+ const items = [];
227
+ items.push({
228
+ key: 'runtime',
229
+ status: 'ok',
230
+ detail: `node ${process.version} · ${process.platform} ${process.arch}`,
231
+ });
232
+ items.push({
233
+ key: 'cwd',
234
+ status: 'ok',
235
+ detail: cwd,
236
+ });
237
+ items.push({
238
+ key: 'project',
239
+ status: projectRoot ? 'ok' : 'warn',
240
+ detail: projectRoot
241
+ ? `${projectRoot}${dirs.length ? ' (pipeline: ' + dirs.join(', ') + ')' : ' (未发现 pipeline 目录)'}`
242
+ : '未找到 interface.json / maa-project.json(当前目录不在 Maa 项目内)',
243
+ });
244
+ items.push({
245
+ key: 'maa-node',
246
+ status: self ? 'ok' : 'missing',
247
+ detail: self ? `${self.version} (${self.path})` : '未安装(运行时命令不可用)',
248
+ });
249
+ /* 并列而不是覆盖:全局安装时 cwd 的那份不会被加载,报成同一个版本会误导排查 */
250
+ if (cwdCopy && (!self || !samePath(self.path, cwdCopy.path))) {
251
+ items.push({
252
+ key: 'maa-node(cwd)',
253
+ status: 'warn',
254
+ detail: `${cwdCopy.version} (${cwdCopy.path}) — 项目目录里的副本,本 CLI 不加载它`,
255
+ });
256
+ }
257
+ /* 项目 pin 只与 self 对账:比的是「正在运行的那份」,不是项目里躺着的那份 */
258
+ const pin = projectRoot ? readMaaNodePin(projectRoot) : null;
259
+ const pinWarnings = [];
260
+ if (pin && pin.source !== 'uv.lock') {
261
+ if (pin.exact === null) {
262
+ items.push({
263
+ key: 'maa-node pin',
264
+ status: 'ok',
265
+ detail: `${pin.declared}(${pin.source})—— 范围声明,不比对`,
266
+ });
267
+ }
268
+ else if (!self) {
269
+ items.push({
270
+ key: 'maa-node pin',
271
+ status: 'warn',
272
+ detail: `${pin.declared}(${pin.source})—— CLI 实际加载的 maa-node 未安装,无法比对`,
273
+ });
274
+ }
275
+ else {
276
+ /* 两段都比、且 qualifier 先归一化:`5.15.0b1` 与 `5.15.0-beta.1` 是同一份构建的两种
277
+ 生态写法(不算不一致);而正式版 `5.14.2` 与预发布 `5.14.2b1` 是同一条线上的两份
278
+ 不同构建(要报)。只比字符串会把前者误报、只比 release 会把后者漏报。 */
279
+ const pinV = splitRelease(pin.exact);
280
+ const runV = splitRelease(self.version);
281
+ const mismatch = () => {
282
+ const detail = `项目 pin ${pin.declared}(${pin.source}),CLI 实际加载 ${self.version}`;
283
+ items.push({ key: 'maa-node pin', status: 'warn', detail });
284
+ pinWarnings.push(`maa-node 版本不一致:${detail}`);
285
+ };
286
+ if (!pinV || !runV) {
287
+ /* 有一边不是 x.y.z 形态(自定义构建号之类):没有 release 线可比,只能退回整串 */
288
+ if (pin.exact === self.version) {
289
+ items.push({
290
+ key: 'maa-node pin',
291
+ status: 'ok',
292
+ detail: `${pin.exact}(${pin.source})与 CLI 实际加载一致`,
293
+ });
294
+ }
295
+ else {
296
+ mismatch();
297
+ }
298
+ }
299
+ else if (pinV.release !== runV.release) {
300
+ mismatch();
301
+ }
302
+ else if (pinV.qualifier !== runV.qualifier) {
303
+ /* 同 release 线但 qualifier 不同 = 同一条线上的另一份构建(正式版 vs b1、b1 vs rc1),
304
+ * 仍然是"跑的不是 pin 的那份",要报。归一化只抹平两个生态的拼写(b1 / beta.1)。 */
305
+ mismatch();
306
+ }
307
+ else if (pin.exact === self.version) {
308
+ items.push({
309
+ key: 'maa-node pin',
310
+ status: 'ok',
311
+ detail: `${pin.exact}(${pin.source})与 CLI 实际加载一致`,
312
+ });
313
+ }
314
+ else {
315
+ items.push({
316
+ key: 'maa-node pin',
317
+ status: 'ok',
318
+ detail: `同版本 ${pinV.release}${pinV.qualifier ? '-' + pinV.qualifier : ''}:` +
319
+ `声明 ${pin.exact} / 实际 ${self.version}(两种拼写指向同一份构建)`,
320
+ });
321
+ }
322
+ }
323
+ }
324
+ /* 显式给了源码目录才出现:应用开发者的机器上不该看到一个「缺一项」 */
325
+ if (maafw)
326
+ items.push({
327
+ key: 'framework',
328
+ status: !existsSync(maafw) ? 'missing' : checkoutVer ? 'ok' : 'warn',
329
+ /* 进到这里 maafw 必然非空,"未提供源码目录"那一支是死的(曾经留着) */
330
+ detail: !existsSync(maafw)
331
+ ? `checkout 不存在:${maafw}`
332
+ : checkoutVer
333
+ ? `${checkoutVer} (${maafw})`
334
+ : `存在但 git describe 失败:${maafw}`,
335
+ });
336
+ items.push({
337
+ key: 'git',
338
+ status: gitV.ok ? 'ok' : 'warn',
339
+ detail: gitV.ok ? firstLine(gitV.stdout || gitV.stderr) : '不可用(仅版本对账与迁移脚本需要)',
340
+ });
341
+ // adb 与 python 是「按需」依赖:只有设备类命令和 v5 迁移脚本需要,
342
+ // 缺失不影响其余功能,因此记 warn 而不是 missing(不拉高退出码)。
343
+ items.push({
344
+ key: 'adb',
345
+ status: adbV.ok ? 'ok' : 'warn',
346
+ detail: adbV.ok ? firstLine(adbV.stdout || adbV.stderr) : '不可用(设备类命令需要它)',
347
+ });
348
+ items.push({
349
+ key: 'python',
350
+ status: pyV.ok ? 'ok' : 'warn',
351
+ detail: pyV.ok
352
+ ? firstLine(pyV.stdout || pyV.stderr)
353
+ : '不可用(migrate_pipeline_v5.py 需要它)',
354
+ });
355
+ if (deep) {
356
+ const npm = await run('npm', ['--version'], { timeout: 8000 });
357
+ items.push({
358
+ key: 'npm',
359
+ status: npm.ok ? 'ok' : 'missing',
360
+ detail: npm.ok ? `npm ${firstLine(npm.stdout)}` : '不可用',
361
+ });
362
+ for (const pkg of ['maa-evidence-kit', 'create-maa-project']) {
363
+ const r = await run('npm', ['ls', '-g', '--depth=0', pkg], { timeout: 15000 });
364
+ const installed = r.ok && r.stdout.includes(pkg);
365
+ items.push({
366
+ key: pkg,
367
+ status: installed ? 'ok' : 'warn',
368
+ detail: installed ? '已全局安装' : '未全局安装',
369
+ });
370
+ }
371
+ }
372
+ const width = Math.max(...items.map((i) => i.key.length));
373
+ const human = [
374
+ 'maafw-live env',
375
+ ...items.map((i) => ` ${mark(i.status)} ${pad(i.key, width)} ${i.detail}`),
376
+ ];
377
+ const missing = items.filter((i) => i.status === 'missing');
378
+ const warnings = missing.map((i) => `${i.key}: ${i.detail}`);
379
+ warnings.push(...pinWarnings);
380
+ return {
381
+ exitCode: missing.length > 0 ? EXIT.ENV : EXIT.OK,
382
+ root: projectRoot,
383
+ human,
384
+ warnings,
385
+ data: {
386
+ runtime: { node: process.version, platform: process.platform, arch: process.arch },
387
+ cwd,
388
+ project: { root: projectRoot, pipelineDirs: dirs },
389
+ maaNode: self,
390
+ /** cwd 那份副本,仅作对照:它不是 CLI 实际加载的那份 */
391
+ maaNodeCwd: cwdCopy,
392
+ /** 项目声明的 pin:只用 package.json / requirements.txt 下结论,uv.lock 仅记来源 */
393
+ maaNodePin: pin,
394
+ framework: maafw
395
+ ? {
396
+ checkout: { path: maafw, found: existsSync(maafw), version: checkoutVer },
397
+ native: self?.version ?? null,
398
+ }
399
+ : null,
400
+ tools: {
401
+ git: { available: gitV.ok, version: gitV.ok ? firstLine(gitV.stdout || gitV.stderr) : '' },
402
+ adb: { available: adbV.ok, version: adbV.ok ? firstLine(adbV.stdout || adbV.stderr) : '' },
403
+ python: { available: pyV.ok, version: pyV.ok ? firstLine(pyV.stdout || pyV.stderr) : '' },
404
+ },
405
+ },
406
+ };
407
+ },
408
+ };
409
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1,70 @@
1
+ import { ANNOTATE_COMMANDS } from './annotate.js';
2
+ import { CALIBRATE_COMMANDS } from './calibrate.js';
3
+ import { COLOR_COMMANDS } from './color.js';
4
+ import { CROP_COMMANDS } from './crop.js';
5
+ import { envCommand } from './env.js';
6
+ import { INPUT_COMMANDS } from './input.js';
7
+ import { KF_COMMANDS } from './kf.js';
8
+ import { screencapProbeCommand } from './probe.js';
9
+ import { RECO_COMMANDS } from './reco.js';
10
+ import { replCommand } from './repl.js';
11
+ import { ROI_CHECK_COMMANDS } from './roi-check.js';
12
+ import { ROI_COMPARE_COMMANDS } from './roi-compare.js';
13
+ import { RUNTIME_COMMANDS } from './runtime.js';
14
+ import { skillCommand } from './skill.js';
15
+ import { TIMING_COMMANDS } from './timing.js';
16
+ import { versionCommand } from './version.js';
17
+ /** 展示顺序:探针 → 运行时/设备 → 输入 → 识别 → 验真 → 探色 → 裁剪 → SoM → 校准 → 关键帧 → timing → 会话 → skill 自检。 */
18
+ export const COMMANDS = [
19
+ envCommand,
20
+ versionCommand,
21
+ ...RUNTIME_COMMANDS,
22
+ screencapProbeCommand,
23
+ ...INPUT_COMMANDS,
24
+ ...RECO_COMMANDS,
25
+ ...ROI_COMPARE_COMMANDS,
26
+ ...ROI_CHECK_COMMANDS,
27
+ ...COLOR_COMMANDS,
28
+ ...CROP_COMMANDS,
29
+ ...ANNOTATE_COMMANDS,
30
+ ...CALIBRATE_COMMANDS,
31
+ ...KF_COMMANDS,
32
+ ...TIMING_COMMANDS,
33
+ replCommand,
34
+ skillCommand,
35
+ ];
36
+ export function findCommand(name) {
37
+ return COMMANDS.find((c) => c.name === name);
38
+ }
39
+ /** 未知命令时给出「最接近的名字」建议(编辑距离),避免用户猜 */
40
+ export function suggestCommand(name, maxDistance = 3) {
41
+ let best;
42
+ let bestDist = maxDistance + 1;
43
+ for (const c of COMMANDS) {
44
+ const d = distance(name, c.name);
45
+ if (d < bestDist) {
46
+ bestDist = d;
47
+ best = c.name;
48
+ }
49
+ }
50
+ return bestDist <= maxDistance ? best : undefined;
51
+ }
52
+ function distance(a, b) {
53
+ const m = a.length;
54
+ const n = b.length;
55
+ if (m === 0)
56
+ return n;
57
+ if (n === 0)
58
+ return m;
59
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
60
+ for (let i = 1; i <= m; i++) {
61
+ const cur = [i];
62
+ for (let j = 1; j <= n; j++) {
63
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
64
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
65
+ }
66
+ prev = cur;
67
+ }
68
+ return prev[n];
69
+ }
70
+ //# sourceMappingURL=index.js.map