@hupan56/wlkj 2.2.3 → 2.2.5

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/bin/cli.js CHANGED
@@ -1,533 +1,533 @@
1
- #!/usr/bin/env node
2
- // wlkj - workflow toolkit
3
-
4
- const path = require("path");
5
- const fs = require("fs");
6
- const { execSync } = require("child_process");
7
-
8
- const T = path.join(__dirname, "..", "templates");
9
-
10
- // 当前 npm 包版本(从 package.json 读,不硬编码)
11
- const PKG_VERSION = JSON.parse(
12
- fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
13
- ).version;
14
-
15
- // 升级时受保护的用户数据文件(绝不覆盖)
16
- // 这些文件含团队/机器特定配置,覆盖 = 数据丢失
17
- const PROTECTED_FILES = new Set([
18
- "config.yaml", // 团队 git 仓库 URL、平台映射
19
- "settings.json", // 本地权限/hook 配置
20
- ]);
21
-
22
- // 本地状态文件(升级时也绝不碰,但不算"受保护配置",不提示合并)
23
- const LOCAL_STATE_FILES = new Set([
24
- ".developer", ".current-task", ".engine-version",
25
- ]);
26
-
27
- function py(script, args = []) {
28
- const p = path.join(process.cwd(), ".qoder", "scripts", script);
29
- if (!fs.existsSync(p)) { console.log("请先运行: npx wlkj init"); process.exit(1); }
30
- try {
31
- return execSync(`python "${p}" ${args.map(a => `"${a}"`).join(" ")}`, { cwd: process.cwd(), encoding: "utf-8", timeout: 15000 });
32
- } catch (e) { return (e.stdout || e.message); }
33
- }
34
-
35
- function cp(src, dest) {
36
- const s = path.join(T, src), d = path.join(process.cwd(), dest);
37
- if (!fs.existsSync(s)) return;
38
- fs.mkdirSync(path.dirname(d), { recursive: true });
39
- if (!fs.existsSync(d)) { fs.copyFileSync(s, d); return true; }
40
- return false;
41
- }
42
-
43
- function gitOp(op) {
44
- const cwd = process.cwd();
45
- const cmds = {
46
- "提交": 'git add . && git commit -m "update [ai-assisted]" && git push',
47
- "推送": "git push",
48
- "拉取最新": "git pull",
49
- "同步": "git pull",
50
- "查看状态": "git status",
51
- "提交PRD": 'git add workspace/specs/prd/ && git commit -m "docs(ai): PRD update [ai-generated]" && git push',
52
- "提交Spec": 'git add workspace/specs/ && git commit -m "docs(ai): Spec update [ai-generated]" && git push',
53
- "提交任务": 'git add workspace/tasks/ && git commit -m "chore: task update [ai-assisted]" && git push',
54
- };
55
- const cmd = cmds[op];
56
- if (!cmd) { console.log("可用: 提交 推送 拉取最新 同步 查看状态 提交PRD 提交Spec 提交任务"); return; }
57
- try { console.log(execSync(cmd, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" }) || `${op} done`); }
58
- catch (e) { console.log(e.stdout || e.message); }
59
- }
60
-
61
- function doInit(name) {
62
- const cwd = process.cwd();
63
- const hasExisting = fs.existsSync(path.join(cwd, ".qoder", "scripts", "setup.py"));
64
- console.log(`\nwlkj init -> ${cwd}${hasExisting ? " (已存在, 增量更新)" : ""}\n`);
65
-
66
- // === 1. 拷贝完整引擎 (镜像 .qoder/ 结构) ===
67
- // 源: templates/qoder/* 目标: .qoder/*
68
- // 注意: init 用 "update" 模式的保护逻辑更安全 —— 新装时目标文件不存在,
69
- // 保护逻辑不触发; 重跑 init 时 config.yaml/settings.json 不会被覆盖。
70
- const qoderSrc = path.join(T, "qoder");
71
- let copied = 0;
72
- if (fs.existsSync(qoderSrc)) {
73
- const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update");
74
- copied = r.copied;
75
- if (r.protectedN > 0) {
76
- console.log(` [保护] ${r.protectedN} 个配置文件未覆盖(新版存为 .new)`);
77
- }
78
- }
79
- console.log(` 引擎: ${copied} 个文件 (${hasExisting ? "更新" : "新建"})`);
80
-
81
- // === 2. 根文件 (AGENTS.md, 新手指南.md) ===
82
- ["root/AGENTS.md", "root/新手指南.md"].forEach(f => {
83
- const src = path.join(T, f);
84
- if (fs.existsSync(src)) {
85
- const dstName = path.basename(f);
86
- const dst = path.join(cwd, dstName);
87
- if (!fs.existsSync(dst)) {
88
- fs.copyFileSync(src, dst);
89
- }
90
- }
91
- });
92
-
93
- // === 3. workspace 目录结构 ===
94
- ["workspace/specs/prd", "workspace/tasks", "workspace/constitution",
95
- "workspace/members", "data/docs/prd", "data/index", "data/code"].forEach(d => {
96
- fs.mkdirSync(path.join(cwd, d), { recursive: true });
97
- });
98
- console.log(` workspace 目录就绪`);
99
-
100
- // === 4. git init (若没有) ===
101
- if (!fs.existsSync(path.join(cwd, ".git"))) {
102
- try { execSync("git init", { cwd, stdio: "pipe" }); console.log(` git init done`); }
103
- catch (_) { /* git 可能没装, 不阻塞 */ }
104
- }
105
-
106
- // === 5. .gitignore (若没有) ===
107
- const gitignorePath = path.join(cwd, ".gitignore");
108
- if (!fs.existsSync(gitignorePath)) {
109
- const baseGitignore = [
110
- "# Source code repos (cloned by git_sync.py)", "data/code/",
111
- "", "# AI pipeline runtime", ".qoder/.developer", ".qoder/.current-task",
112
- ".qoder/.engine-version", ".qoder/.runtime/", "",
113
- "# Personal learning", ".qoder/learning/feedback.jsonl",
114
- "", "# Machine-local", "data/index/.last-sync", "data/index/.prd-collected.json",
115
- "data/index/.index-meta.json", "data/index/.sync-lock", "data/index/.file-keys.json",
116
- "data/index/.inverted-cache.json", "data/index/*.corrupt",
117
- "", "# Identity (secret)", "workspace/members/*/.signing_key", "",
118
- "# Python", "__pycache__/", "*.pyc", "*.lock", "",
119
- ].join("\n");
120
- fs.writeFileSync(gitignorePath, baseGitignore, "utf-8");
121
- }
122
-
123
- // === 6. 自动跑 setup.py (名字探测/git/索引/cron/QoderWork) ===
124
- console.log(`\n--- 自动初始化 ---`);
125
- const setupPath = path.join(cwd, ".qoder", "scripts", "setup.py");
126
- if (fs.existsSync(setupPath)) {
127
- const setupArgs = [name, "pm", "--skip-cron", "--skip-qoderwork"].filter(Boolean);
128
- try {
129
- const cmd = `python "${setupPath}" ${setupArgs.map(a => `"${a}"`).join(" ")}`;
130
- console.log(` 运行: setup.py ${setupArgs.join(" ")}`);
131
- execSync(cmd, { cwd, stdio: "inherit", timeout: 300000 });
132
- } catch (e) {
133
- console.log(` setup 部分失败 (不阻塞): ${(e.message || "").slice(0, 100)}`);
134
- console.log(` 可手动重跑: python .qoder/scripts/setup.py`);
135
- }
136
- } else {
137
- console.log(` setup.py 未找到, 跳过自动初始化`);
138
- }
139
-
140
- // === 7. 写版本戳 ===
141
- writeEngineVersion(cwd);
142
-
143
- console.log(`\n${"=".repeat(50)}`);
144
- console.log(` 安装完成! (v${PKG_VERSION})`);
145
- console.log(`${"=".repeat(50)}`);
146
- console.log(`\n 现在可以开始了:`);
147
- console.log(` 在 Qoder 里说 "写个 XX 的需求"`);
148
- console.log(` 或输 / 看命令列表 (/wl-prd /wl-search /wl-task)`);
149
- console.log(`\n 看不到命令? 新建对话 / 重启 QoderWork`);
150
- console.log(` 环境问题? python .qoder/scripts/init_doctor.py --fix\n`);
151
- }
152
-
153
- // 递归拷贝目录, 返回 {copied, protected, skipped}
154
- // mode: "init" (新装, 全拷) | "update" (升级, 保护用户数据)
155
- function copyDirRecursive(src, dst, mode = "init") {
156
- let copied = 0, protectedN = 0, skipped = 0;
157
- if (!fs.existsSync(src)) return { copied, protectedN, skipped };
158
- fs.mkdirSync(dst, { recursive: true });
159
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
160
- if (entry.name === "__pycache__" || entry.name.endsWith(".pyc")) continue;
161
- const s = path.join(src, entry.name);
162
- const d = path.join(dst, entry.name);
163
- if (entry.isDirectory()) {
164
- const r = copyDirRecursive(s, d, mode);
165
- copied += r.copied; protectedN += r.protectedN; skipped += r.skipped;
166
- } else {
167
- const isEngine = entry.name.endsWith(".py") || entry.name.endsWith(".md") ||
168
- entry.name.endsWith(".yaml") || entry.name.endsWith(".yml") ||
169
- entry.name.endsWith(".toml") || entry.name.endsWith(".json") ||
170
- entry.name.endsWith(".html") || entry.name.endsWith(".txt");
171
-
172
- if (mode === "update") {
173
- // 升级模式: 受保护文件绝不覆盖
174
- if (PROTECTED_FILES.has(entry.name) && fs.existsSync(d)) {
175
- // 新版内容存为 .new 让用户手动合并(仅当内容不同)
176
- if (!filesEqual(s, d)) {
177
- fs.copyFileSync(s, d + ".new");
178
- protectedN++;
179
- } else {
180
- skipped++;
181
- }
182
- continue;
183
- }
184
- // 引擎文件: 无条件刷新(纯引擎, 不含用户数据)
185
- // 非引擎文件: 已存在跳过, 不存在才建
186
- if (isEngine || !fs.existsSync(d)) {
187
- fs.copyFileSync(s, d);
188
- copied++;
189
- } else {
190
- skipped++;
191
- }
192
- } else {
193
- // init 模式: 引擎文件全覆盖, 非引擎已存在跳过 (保留用户改动)
194
- if (isEngine || !fs.existsSync(d)) {
195
- fs.copyFileSync(s, d);
196
- }
197
- copied++;
198
- }
199
- }
200
- }
201
- return { copied, protectedN, skipped };
202
- }
203
-
204
- // 两文件内容是否相同(先比大小再比内容)
205
- function filesEqual(a, b) {
206
- try {
207
- const sa = fs.statSync(a), sb = fs.statSync(b);
208
- if (sa.size !== sb.size) return false;
209
- return fs.readFileSync(a).equals(fs.readFileSync(b));
210
- } catch { return false; }
211
- }
212
-
213
- // 版本戳: .qoder/.engine-version 存当前引擎版本
214
- function writeEngineVersion(cwd) {
215
- try {
216
- const p = path.join(cwd, ".qoder", ".engine-version");
217
- fs.mkdirSync(path.dirname(p), { recursive: true });
218
- fs.writeFileSync(p, PKG_VERSION + "\n", "utf-8");
219
- } catch { /* 不阻塞 */ }
220
- }
221
-
222
- function readEngineVersion(cwd) {
223
- try {
224
- const p = path.join(cwd, ".qoder", ".engine-version");
225
- if (fs.existsSync(p)) return fs.readFileSync(p, "utf-8").trim();
226
- } catch { /* */ }
227
- return null;
228
- }
229
-
230
- // 跑 install_qoderwork.py(升级时强制刷新 commands)
231
- function runQoderWorkInstall(cwd, forceCommands) {
232
- const script = path.join(cwd, ".qoder", "scripts", "install_qoderwork.py");
233
- if (!fs.existsSync(script)) {
234
- console.log(" [跳过] install_qoderwork.py 不存在");
235
- return;
236
- }
237
- const args = ["python", `"${script}"`];
238
- if (forceCommands) args.push("--force-commands");
239
- try {
240
- console.log(`\n--- 刷新 QoderWork ---`);
241
- execSync(args.join(" "), { cwd, stdio: "inherit", timeout: 60000 });
242
- } catch (e) {
243
- console.log(` QoderWork 刷新失败 (不阻塞): ${(e.message || "").slice(0, 80)}`);
244
- }
245
- }
246
-
247
- function doStatus() {
248
- console.log("");
249
- const dev = path.join(process.cwd(), ".qoder", ".developer");
250
- if (fs.existsSync(dev)) {
251
- const line = fs.readFileSync(dev, "utf-8").split("\n")[0];
252
- console.log("dev: " + line.split("=")[1]?.trim());
253
- } else { console.log("dev: - (run: npx wlkj init <name>)"); }
254
-
255
- process.stdout.write(py("task.py", ["current", "--source"]));
256
-
257
- const wsSpecs = path.join(process.cwd(), "workspace", "specs", "prd");
258
- if (fs.existsSync(wsSpecs)) {
259
- const n = fs.readdirSync(wsSpecs).filter(f => f.endsWith(".md")).length;
260
- console.log(`PRDs: ${n}`);
261
- }
262
- const ver = readEngineVersion(process.cwd());
263
- console.log(`engine: ${ver || "未知"} (最新: v${PKG_VERSION})${ver === PKG_VERSION ? " [最新]" : ver ? " [可升级: npx @hupan56/wlkj update]" : ""}`);
264
- console.log("");
265
- }
266
-
267
- function doUpdate() {
268
- const cwd = process.cwd();
269
- const oldVer = readEngineVersion(cwd);
270
- const qoderSrc = path.join(T, "qoder");
271
-
272
- // 前置检查: 必须已装过
273
- if (!fs.existsSync(path.join(cwd, ".qoder", "scripts"))) {
274
- console.log("\n 未检测到已安装的引擎。首次安装请用:");
275
- console.log(" npx @hupan56/wlkj init [你的名字]\n");
276
- return;
277
- }
278
-
279
- console.log(`\nwlkj update${oldVer ? " " + oldVer : ""} -> v${PKG_VERSION}\n`);
280
-
281
- // === 1. 刷新引擎文件(保护 config.yaml / settings.json)===
282
- let copied = 0, protectedN = 0, skipped = 0;
283
- if (fs.existsSync(qoderSrc)) {
284
- const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update");
285
- copied = r.copied; protectedN = r.protectedN; skipped = r.skipped;
286
- }
287
- console.log(` 引擎刷新: ${copied} 个文件${skipped ? ` / ${skipped} 无变化` : ""}`);
288
-
289
- // 报告受保护文件
290
- if (protectedN > 0) {
291
- console.log(` [保护] ${protectedN} 个配置文件已保留你的本地设置:`);
292
- console.log(` 新版模板存为 .new 文件, 如需合并新结构请手动对比:`);
293
- // 列出实际生成的 .new 文件
294
- try {
295
- for (const f of fs.readdirSync(path.join(cwd, ".qoder"))) {
296
- if (f.endsWith(".new")) {
297
- console.log(` .qoder/${f}`);
298
- }
299
- }
300
- } catch { /* */ }
301
- } else {
302
- console.log(` config.yaml / settings.json: 已保护(未改动)`);
303
- }
304
-
305
- // === 2. 根文件(文档类, 安全覆盖)===
306
- ["root/AGENTS.md", "root/新手指南.md"].forEach(f => {
307
- const src = path.join(T, f);
308
- if (fs.existsSync(src)) {
309
- fs.copyFileSync(src, path.join(cwd, path.basename(f)));
310
- }
311
- });
312
- console.log(` 根文档: 已更新`);
313
-
314
- // === 3. 刷新 QoderWork commands(强制覆盖已存在的)===
315
- runQoderWorkInstall(cwd, true);
316
-
317
- // === 4. 写版本戳 ===
318
- writeEngineVersion(cwd);
319
-
320
- console.log(`\n${"=".repeat(50)}`);
321
- console.log(` 升级完成! (v${PKG_VERSION})`);
322
- console.log(`${"=".repeat(50)}`);
323
- console.log(`\n 生效方式:`);
324
- console.log(` Qoder IDE / Quest: 新建对话即可`);
325
- console.log(` QoderWork: 重启应用或新建对话`);
326
- if (protectedN > 0) {
327
- console.log(`\n ⚠ 有配置文件新版有变化, 生成 .new 文件:`);
328
- console.log(` 确认无需合并后可直接删除, 或手动合并后删除`);
329
- }
330
- console.log("");
331
- }
332
-
333
- function doHelp() {
334
- console.log("");
335
- console.log(`wlkj - AI 产品研发工作流 (v${PKG_VERSION})`);
336
- console.log("");
337
- console.log("=== 环境安装 (什么都没装时先跑这个) ===");
338
- console.log(" npx @hupan56/wlkj install-env 检测+自动装 Node/Python/git");
339
- console.log(" npx @hupan56/wlkj install-env --check 只检测不装");
340
- console.log(" npx @hupan56/wlkj install-env --location D:\\wldev 指定安装目录");
341
- console.log(" npx @hupan56/wlkj add-path D:\\wldev\\nodejs 把目录加到系统 PATH");
342
- console.log("");
343
- console.log("=== 一键安装 (装好环境后) ===");
344
- console.log(" npx @hupan56/wlkj init [你的名字] 安装完整引擎 + 自动初始化");
345
- console.log("");
346
- console.log("=== 升级 (已装用户, 新版本发布后) ===");
347
- console.log(" npx @hupan56/wlkj update 刷新引擎, 保护你的配置");
348
- console.log("");
349
- console.log("=== 安装后怎么用 ===");
350
- console.log(" 在 Qoder (IDE/Quest/QoderWork) 里:");
351
- console.log(" 说中文: '写个 XX 的需求' '查一下 XX 代码' '建个任务'");
352
- console.log(" 或斜杠: /wl-prd /wl-search /wl-task /wl-status /wl-report");
353
- console.log("");
354
- console.log("=== 状态 ===");
355
- console.log(" npx @hupan56/wlkj status 查看当前状态 + 引擎版本");
356
- console.log("");
357
- console.log("=== 环境修复 ===");
358
- console.log(" python .qoder/scripts/init_doctor.py --fix 自动修复环境");
359
- console.log(" python .qoder/scripts/setup.py 重新初始化");
360
- console.log("");
361
- console.log("=== Git (中文命令) ===");
362
- console.log(" npx @hupan56/wlkj 提交PRD 提交 PRD");
363
- console.log(" npx @hupan56/wlkj 提交任务 提交任务");
364
- console.log(" npx @hupan56/wlkj 提交 全部提交并推送");
365
- console.log(" npx @hupan56/wlkj 拉取最新 / 同步 git pull");
366
- console.log("");
367
- }
368
-
369
- function doInstallEnv(checkOnly = false, location = null) {
370
- const { execSync } = require("child_process");
371
- const isWin = process.platform === "win32";
372
- const isMac = process.platform === "darwin";
373
-
374
- // 解析 --location(CLI 入口已提取,这里兜底)
375
- console.log("\n=== 环境检测 ===\n");
376
- if (location) {
377
- console.log(` 安装位置: ${location}${isWin ? "" : " (仅 Windows winget 支持指定目录)"}`);
378
- // Windows: 预建目录
379
- if (isWin) {
380
- try { fs.mkdirSync(location, { recursive: true }); } catch { /* */ }
381
- }
382
- }
383
-
384
- // 检测函数
385
- function check(cmd) {
386
- try { return execSync(`${cmd} --version`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim().split("\n")[0]; }
387
- catch { return null; }
388
- }
389
- function has(cmd) {
390
- try { execSync(`where ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; }
391
- catch { try { execSync(`which ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; } catch { return false; } }
392
- }
393
- // Windows: 把目录加到用户 PATH(幂等,已存在不重复加)
394
- function addToPathWin(dir) {
395
- if (!isWin) return false;
396
- try {
397
- // 用 setx 会截断超长 PATH,改用 PowerShell 读 User PATH 追加
398
- const ps = `powershell -NoProfile -Command "$p=[Environment]::GetEnvironmentVariable('Path','User'); if($p -notlike '*{dir}*'){[Environment]::SetEnvironmentVariable('Path',$p.TrimEnd(';')+';{dir}','User')}"`.replace(/{dir}/g, dir.replace(/\\/g, "\\\\"));
399
- execSync(ps, { stdio: "pipe", timeout: 15000 });
400
- return true;
401
- } catch (e) { console.log(` [WARN] 加 PATH 失败(可手动加): ${dir}`); return false; }
402
- }
403
- function install(name, wingetId, brewPkg) {
404
- if (checkOnly) return false;
405
- try {
406
- if (isWin && has("winget")) {
407
- console.log(` 用 winget 装 ${name}...`);
408
- // winget --location 不一定被安装包尊重,但先传上
409
- let cmd = `winget install --id ${wingetId} -e --source winget --accept-source-agreements --accept-package-agreements`;
410
- if (location) cmd += ` --location "${location}"`;
411
- execSync(cmd, { stdio: "inherit", timeout: 300000 });
412
- return true;
413
- } else if (isMac && has("brew")) {
414
- console.log(` 用 brew 装 ${name}...`);
415
- execSync(`brew install ${brewPkg}`, { stdio: "inherit", timeout: 300000 });
416
- return true;
417
- } else if (!isWin && !isMac) {
418
- // Linux: 尝试 apt/dnf/yum
419
- for (const [mgr, pkg] of [["apt", brewPkg], ["dnf", brewPkg]]) {
420
- if (has(mgr)) {
421
- console.log(` 用 ${mgr} 装 ${name}...`);
422
- execSync(`sudo ${mgr} install -y ${pkg}`, { stdio: "inherit", timeout: 300000 });
423
- return true;
424
- }
425
- }
426
- }
427
- } catch (e) { console.log(` 自动安装失败: ${(e.message || "").slice(0, 80)}`); }
428
- return false;
429
- }
430
-
431
- const nodeVer = check("node");
432
- const pyVer = check("python") || check("python3");
433
- const gitVer = check("git");
434
-
435
- console.log(` Node.js: ${nodeVer ? "[OK] " + nodeVer : "[缺失] (npx 命令需要)"}`);
436
- console.log(` Python: ${pyVer ? "[OK] " + pyVer : "[缺失] (引擎需要)"}`);
437
- console.log(` git: ${gitVer ? "[OK] " + gitVer : "[缺失] (可选, 核心功能不依赖)"}`);
438
-
439
- if (checkOnly) {
440
- console.log("\n仅检测。去掉 --check 自动安装。");
441
- return;
442
- }
443
-
444
- const need = [];
445
- if (!nodeVer) {
446
- console.log("");
447
- if (nodeVer || install("Node.js", "OpenJS.NodeJS.LTS", "node")) { console.log(` [OK] Node.js 已装`); }
448
- else { need.push("Node.js (手动: https://nodejs.org)"); }
449
- }
450
- if (!pyVer) {
451
- console.log("");
452
- if (install("Python", "Python.Python.3.12", "python@3.12")) { console.log(` [OK] Python 已装`); }
453
- else { need.push("Python (手动: https://python.org, 勾 Add to PATH)"); }
454
- }
455
- if (!gitVer) {
456
- console.log("");
457
- if (install("git", "Git.Git", "git")) { console.log(` [OK] git 已装`); }
458
- else { need.push("git (手动: https://git-scm.com, 可选)"); }
459
- }
460
-
461
- // --location 模式: 提示用户验证安装位置 + 手动加 PATH
462
- // (winget 的 --location 对 Node/Python 不一定生效,需用户确认)
463
- if (location && isWin) {
464
- console.log(`\n--- 安装位置确认 ---`);
465
- console.log(` 你指定了: ${location}`);
466
- console.log(` 注意: winget 的 --location 对 Node.js/Python 不一定生效`);
467
- console.log(` (取决于安装包是否支持自定义路径)`);
468
- console.log(` 请检查实际装到哪了:`);
469
- console.log(` 重新打开终端后跑: node --version python --version git --version`);
470
- console.log(` 如果某软件没进 PATH, 手动把它加进去:`);
471
- console.log(` 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → Path → 新建`);
472
- console.log(` 或用命令: npx @hupan56/wlkj add-path "D:\\wldev\\nodejs"`);
473
- }
474
-
475
- console.log("\n=== 结果 ===");
476
- if (need.length === 0) {
477
- console.log(" 环境就绪! 下一步: npx @hupan56/wlkj init");
478
- console.log(" (新装的可能需要重新打开终端)");
479
- } else {
480
- console.log(" 以下需手动安装:");
481
- need.forEach(n => console.log(" - " + n));
482
- }
483
- }
484
-
485
- // add-path: 把目录加到 Windows 用户 PATH(幂等)
486
- function doAddPath(dir) {
487
- if (!dir) { console.log("用法: npx @hupan56/wlkj add-path <目录>"); return; }
488
- if (process.platform !== "win32") {
489
- console.log("add-path 仅支持 Windows。Mac/Linux 请手动加到 ~/.zshrc 或 ~/.bashrc");
490
- return;
491
- }
492
- const abs = path.resolve(dir);
493
- if (!fs.existsSync(abs)) {
494
- console.log(`[警告] 目录不存在: ${abs}(仍会加到 PATH)`);
495
- }
496
- try {
497
- const ps = `powershell -NoProfile -Command "$p=[Environment]::GetEnvironmentVariable('Path','User'); if($p -notlike '*${abs.replace(/\\/g,"\\\\")}*'){[Environment]::SetEnvironmentVariable('Path',$p.TrimEnd(';')+';${abs.replace(/\\/g,"\\\\")}','User'); Write-Output 'ADDED'} else { Write-Output 'EXISTS' }"`;
498
- const out = execSync(ps, { encoding: "utf-8", timeout: 15000 }).trim();
499
- console.log(` ${out === "ADDED" ? "已加入" : "已存在(无需重复加)"}: ${abs}`);
500
- console.log(` ⚠ 需要重新打开终端才生效`);
501
- } catch (e) {
502
- console.log(` 失败: ${(e.message || "").slice(0, 100)}`);
503
- console.log(` 手动加: 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → Path → 新建 → ${abs}`);
504
- }
505
- }
506
-
507
- const [,, cmd, ...rest] = process.argv;
508
- const mapped = rest.map(a => a === "-p" ? "--priority" : a);
509
-
510
- switch (cmd) {
511
- case "init": doInit(rest[0]); break;
512
- case "update": case "upgrade": doUpdate(); break;
513
- case "install-env": {
514
- const checkOnly = rest.includes("--check");
515
- // 提取 --location <dir> 或 --location=<dir>
516
- let loc = null;
517
- const li = rest.indexOf("--location");
518
- if (li !== -1 && rest[li + 1]) loc = rest[li + 1];
519
- const eq = rest.find(a => a.startsWith("--location="));
520
- if (eq) loc = eq.slice("--location=".length);
521
- doInstallEnv(checkOnly, loc);
522
- break;
523
- }
524
- case "add-path": doAddPath(rest[0]); break;
525
- case "task": process.stdout.write(py("task.py", mapped)); break;
526
- case "status": doStatus(); break;
527
- case "session": process.stdout.write(py("add_session.py", rest)); break;
528
- case "help": case "--help": case "-h": doHelp(); break;
529
- case "提交": case "推送": case "拉取最新": case "同步":
530
- case "查看状态": case "提交PRD": case "提交Spec": case "提交任务":
531
- gitOp(cmd); break;
532
- default: doHelp();
1
+ #!/usr/bin/env node
2
+ // wlkj - workflow toolkit
3
+
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const { execSync } = require("child_process");
7
+
8
+ const T = path.join(__dirname, "..", "templates");
9
+
10
+ // 当前 npm 包版本(从 package.json 读,不硬编码)
11
+ const PKG_VERSION = JSON.parse(
12
+ fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
13
+ ).version;
14
+
15
+ // 升级时受保护的用户数据文件(绝不覆盖)
16
+ // 这些文件含团队/机器特定配置,覆盖 = 数据丢失
17
+ const PROTECTED_FILES = new Set([
18
+ "config.yaml", // 团队 git 仓库 URL、平台映射
19
+ "settings.json", // 本地权限/hook 配置
20
+ ]);
21
+
22
+ // 本地状态文件(升级时也绝不碰,但不算"受保护配置",不提示合并)
23
+ const LOCAL_STATE_FILES = new Set([
24
+ ".developer", ".current-task", ".engine-version",
25
+ ]);
26
+
27
+ function py(script, args = []) {
28
+ const p = path.join(process.cwd(), ".qoder", "scripts", script);
29
+ if (!fs.existsSync(p)) { console.log("请先运行: npx wlkj init"); process.exit(1); }
30
+ try {
31
+ return execSync(`python "${p}" ${args.map(a => `"${a}"`).join(" ")}`, { cwd: process.cwd(), encoding: "utf-8", timeout: 15000 });
32
+ } catch (e) { return (e.stdout || e.message); }
33
+ }
34
+
35
+ function cp(src, dest) {
36
+ const s = path.join(T, src), d = path.join(process.cwd(), dest);
37
+ if (!fs.existsSync(s)) return;
38
+ fs.mkdirSync(path.dirname(d), { recursive: true });
39
+ if (!fs.existsSync(d)) { fs.copyFileSync(s, d); return true; }
40
+ return false;
41
+ }
42
+
43
+ function gitOp(op) {
44
+ const cwd = process.cwd();
45
+ const cmds = {
46
+ "提交": 'git add . && git commit -m "update [ai-assisted]" && git push',
47
+ "推送": "git push",
48
+ "拉取最新": "git pull",
49
+ "同步": "git pull",
50
+ "查看状态": "git status",
51
+ "提交PRD": 'git add workspace/specs/prd/ && git commit -m "docs(ai): PRD update [ai-generated]" && git push',
52
+ "提交Spec": 'git add workspace/specs/ && git commit -m "docs(ai): Spec update [ai-generated]" && git push',
53
+ "提交任务": 'git add workspace/tasks/ && git commit -m "chore: task update [ai-assisted]" && git push',
54
+ };
55
+ const cmd = cmds[op];
56
+ if (!cmd) { console.log("可用: 提交 推送 拉取最新 同步 查看状态 提交PRD 提交Spec 提交任务"); return; }
57
+ try { console.log(execSync(cmd, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" }) || `${op} done`); }
58
+ catch (e) { console.log(e.stdout || e.message); }
59
+ }
60
+
61
+ function doInit(name) {
62
+ const cwd = process.cwd();
63
+ const hasExisting = fs.existsSync(path.join(cwd, ".qoder", "scripts", "setup.py"));
64
+ console.log(`\nwlkj init -> ${cwd}${hasExisting ? " (已存在, 增量更新)" : ""}\n`);
65
+
66
+ // === 1. 拷贝完整引擎 (镜像 .qoder/ 结构) ===
67
+ // 源: templates/qoder/* 目标: .qoder/*
68
+ // 注意: init 用 "update" 模式的保护逻辑更安全 —— 新装时目标文件不存在,
69
+ // 保护逻辑不触发; 重跑 init 时 config.yaml/settings.json 不会被覆盖。
70
+ const qoderSrc = path.join(T, "qoder");
71
+ let copied = 0;
72
+ if (fs.existsSync(qoderSrc)) {
73
+ const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update");
74
+ copied = r.copied;
75
+ if (r.protectedN > 0) {
76
+ console.log(` [保护] ${r.protectedN} 个配置文件未覆盖(新版存为 .new)`);
77
+ }
78
+ }
79
+ console.log(` 引擎: ${copied} 个文件 (${hasExisting ? "更新" : "新建"})`);
80
+
81
+ // === 2. 根文件 (AGENTS.md, 新手指南.md) ===
82
+ ["root/AGENTS.md", "root/新手指南.md"].forEach(f => {
83
+ const src = path.join(T, f);
84
+ if (fs.existsSync(src)) {
85
+ const dstName = path.basename(f);
86
+ const dst = path.join(cwd, dstName);
87
+ if (!fs.existsSync(dst)) {
88
+ fs.copyFileSync(src, dst);
89
+ }
90
+ }
91
+ });
92
+
93
+ // === 3. workspace 目录结构 ===
94
+ ["workspace/specs/prd", "workspace/tasks", "workspace/constitution",
95
+ "workspace/members", "data/docs/prd", "data/index", "data/code"].forEach(d => {
96
+ fs.mkdirSync(path.join(cwd, d), { recursive: true });
97
+ });
98
+ console.log(` workspace 目录就绪`);
99
+
100
+ // === 4. git init (若没有) ===
101
+ if (!fs.existsSync(path.join(cwd, ".git"))) {
102
+ try { execSync("git init", { cwd, stdio: "pipe" }); console.log(` git init done`); }
103
+ catch (_) { /* git 可能没装, 不阻塞 */ }
104
+ }
105
+
106
+ // === 5. .gitignore (若没有) ===
107
+ const gitignorePath = path.join(cwd, ".gitignore");
108
+ if (!fs.existsSync(gitignorePath)) {
109
+ const baseGitignore = [
110
+ "# Source code repos (cloned by git_sync.py)", "data/code/",
111
+ "", "# AI pipeline runtime", ".qoder/.developer", ".qoder/.current-task",
112
+ ".qoder/.engine-version", ".qoder/.runtime/", "",
113
+ "# Personal learning", ".qoder/learning/feedback.jsonl",
114
+ "", "# Machine-local", "data/index/.last-sync", "data/index/.prd-collected.json",
115
+ "data/index/.index-meta.json", "data/index/.sync-lock", "data/index/.file-keys.json",
116
+ "data/index/.inverted-cache.json", "data/index/*.corrupt",
117
+ "", "# Identity (secret)", "workspace/members/*/.signing_key", "",
118
+ "# Python", "__pycache__/", "*.pyc", "*.lock", "",
119
+ ].join("\n");
120
+ fs.writeFileSync(gitignorePath, baseGitignore, "utf-8");
121
+ }
122
+
123
+ // === 6. 自动跑 setup.py (名字探测/git/索引/cron/QoderWork) ===
124
+ console.log(`\n--- 自动初始化 ---`);
125
+ const setupPath = path.join(cwd, ".qoder", "scripts", "setup.py");
126
+ if (fs.existsSync(setupPath)) {
127
+ const setupArgs = [name, "pm", "--skip-cron", "--skip-qoderwork"].filter(Boolean);
128
+ try {
129
+ const cmd = `python "${setupPath}" ${setupArgs.map(a => `"${a}"`).join(" ")}`;
130
+ console.log(` 运行: setup.py ${setupArgs.join(" ")}`);
131
+ execSync(cmd, { cwd, stdio: "inherit", timeout: 300000 });
132
+ } catch (e) {
133
+ console.log(` setup 部分失败 (不阻塞): ${(e.message || "").slice(0, 100)}`);
134
+ console.log(` 可手动重跑: python .qoder/scripts/setup.py`);
135
+ }
136
+ } else {
137
+ console.log(` setup.py 未找到, 跳过自动初始化`);
138
+ }
139
+
140
+ // === 7. 写版本戳 ===
141
+ writeEngineVersion(cwd);
142
+
143
+ console.log(`\n${"=".repeat(50)}`);
144
+ console.log(` 安装完成! (v${PKG_VERSION})`);
145
+ console.log(`${"=".repeat(50)}`);
146
+ console.log(`\n 现在可以开始了:`);
147
+ console.log(` 在 Qoder 里说 "写个 XX 的需求"`);
148
+ console.log(` 或输 / 看命令列表 (/wl-prd /wl-search /wl-task)`);
149
+ console.log(`\n 看不到命令? 新建对话 / 重启 QoderWork`);
150
+ console.log(` 环境问题? python .qoder/scripts/init_doctor.py --fix\n`);
151
+ }
152
+
153
+ // 递归拷贝目录, 返回 {copied, protected, skipped}
154
+ // mode: "init" (新装, 全拷) | "update" (升级, 保护用户数据)
155
+ function copyDirRecursive(src, dst, mode = "init") {
156
+ let copied = 0, protectedN = 0, skipped = 0;
157
+ if (!fs.existsSync(src)) return { copied, protectedN, skipped };
158
+ fs.mkdirSync(dst, { recursive: true });
159
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
160
+ if (entry.name === "__pycache__" || entry.name.endsWith(".pyc")) continue;
161
+ const s = path.join(src, entry.name);
162
+ const d = path.join(dst, entry.name);
163
+ if (entry.isDirectory()) {
164
+ const r = copyDirRecursive(s, d, mode);
165
+ copied += r.copied; protectedN += r.protectedN; skipped += r.skipped;
166
+ } else {
167
+ const isEngine = entry.name.endsWith(".py") || entry.name.endsWith(".md") ||
168
+ entry.name.endsWith(".yaml") || entry.name.endsWith(".yml") ||
169
+ entry.name.endsWith(".toml") || entry.name.endsWith(".json") ||
170
+ entry.name.endsWith(".html") || entry.name.endsWith(".txt");
171
+
172
+ if (mode === "update") {
173
+ // 升级模式: 受保护文件绝不覆盖
174
+ if (PROTECTED_FILES.has(entry.name) && fs.existsSync(d)) {
175
+ // 新版内容存为 .new 让用户手动合并(仅当内容不同)
176
+ if (!filesEqual(s, d)) {
177
+ fs.copyFileSync(s, d + ".new");
178
+ protectedN++;
179
+ } else {
180
+ skipped++;
181
+ }
182
+ continue;
183
+ }
184
+ // 引擎文件: 无条件刷新(纯引擎, 不含用户数据)
185
+ // 非引擎文件: 已存在跳过, 不存在才建
186
+ if (isEngine || !fs.existsSync(d)) {
187
+ fs.copyFileSync(s, d);
188
+ copied++;
189
+ } else {
190
+ skipped++;
191
+ }
192
+ } else {
193
+ // init 模式: 引擎文件全覆盖, 非引擎已存在跳过 (保留用户改动)
194
+ if (isEngine || !fs.existsSync(d)) {
195
+ fs.copyFileSync(s, d);
196
+ }
197
+ copied++;
198
+ }
199
+ }
200
+ }
201
+ return { copied, protectedN, skipped };
202
+ }
203
+
204
+ // 两文件内容是否相同(先比大小再比内容)
205
+ function filesEqual(a, b) {
206
+ try {
207
+ const sa = fs.statSync(a), sb = fs.statSync(b);
208
+ if (sa.size !== sb.size) return false;
209
+ return fs.readFileSync(a).equals(fs.readFileSync(b));
210
+ } catch { return false; }
211
+ }
212
+
213
+ // 版本戳: .qoder/.engine-version 存当前引擎版本
214
+ function writeEngineVersion(cwd) {
215
+ try {
216
+ const p = path.join(cwd, ".qoder", ".engine-version");
217
+ fs.mkdirSync(path.dirname(p), { recursive: true });
218
+ fs.writeFileSync(p, PKG_VERSION + "\n", "utf-8");
219
+ } catch { /* 不阻塞 */ }
220
+ }
221
+
222
+ function readEngineVersion(cwd) {
223
+ try {
224
+ const p = path.join(cwd, ".qoder", ".engine-version");
225
+ if (fs.existsSync(p)) return fs.readFileSync(p, "utf-8").trim();
226
+ } catch { /* */ }
227
+ return null;
228
+ }
229
+
230
+ // 跑 install_qoderwork.py(升级时强制刷新 commands)
231
+ function runQoderWorkInstall(cwd, forceCommands) {
232
+ const script = path.join(cwd, ".qoder", "scripts", "install_qoderwork.py");
233
+ if (!fs.existsSync(script)) {
234
+ console.log(" [跳过] install_qoderwork.py 不存在");
235
+ return;
236
+ }
237
+ const args = ["python", `"${script}"`];
238
+ if (forceCommands) args.push("--force-commands");
239
+ try {
240
+ console.log(`\n--- 刷新 QoderWork ---`);
241
+ execSync(args.join(" "), { cwd, stdio: "inherit", timeout: 60000 });
242
+ } catch (e) {
243
+ console.log(` QoderWork 刷新失败 (不阻塞): ${(e.message || "").slice(0, 80)}`);
244
+ }
245
+ }
246
+
247
+ function doStatus() {
248
+ console.log("");
249
+ const dev = path.join(process.cwd(), ".qoder", ".developer");
250
+ if (fs.existsSync(dev)) {
251
+ const line = fs.readFileSync(dev, "utf-8").split("\n")[0];
252
+ console.log("dev: " + line.split("=")[1]?.trim());
253
+ } else { console.log("dev: - (run: npx wlkj init <name>)"); }
254
+
255
+ process.stdout.write(py("task.py", ["current", "--source"]));
256
+
257
+ const wsSpecs = path.join(process.cwd(), "workspace", "specs", "prd");
258
+ if (fs.existsSync(wsSpecs)) {
259
+ const n = fs.readdirSync(wsSpecs).filter(f => f.endsWith(".md")).length;
260
+ console.log(`PRDs: ${n}`);
261
+ }
262
+ const ver = readEngineVersion(process.cwd());
263
+ console.log(`engine: ${ver || "未知"} (最新: v${PKG_VERSION})${ver === PKG_VERSION ? " [最新]" : ver ? " [可升级: npx @hupan56/wlkj update]" : ""}`);
264
+ console.log("");
265
+ }
266
+
267
+ function doUpdate() {
268
+ const cwd = process.cwd();
269
+ const oldVer = readEngineVersion(cwd);
270
+ const qoderSrc = path.join(T, "qoder");
271
+
272
+ // 前置检查: 必须已装过
273
+ if (!fs.existsSync(path.join(cwd, ".qoder", "scripts"))) {
274
+ console.log("\n 未检测到已安装的引擎。首次安装请用:");
275
+ console.log(" npx @hupan56/wlkj init [你的名字]\n");
276
+ return;
277
+ }
278
+
279
+ console.log(`\nwlkj update${oldVer ? " " + oldVer : ""} -> v${PKG_VERSION}\n`);
280
+
281
+ // === 1. 刷新引擎文件(保护 config.yaml / settings.json)===
282
+ let copied = 0, protectedN = 0, skipped = 0;
283
+ if (fs.existsSync(qoderSrc)) {
284
+ const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update");
285
+ copied = r.copied; protectedN = r.protectedN; skipped = r.skipped;
286
+ }
287
+ console.log(` 引擎刷新: ${copied} 个文件${skipped ? ` / ${skipped} 无变化` : ""}`);
288
+
289
+ // 报告受保护文件
290
+ if (protectedN > 0) {
291
+ console.log(` [保护] ${protectedN} 个配置文件已保留你的本地设置:`);
292
+ console.log(` 新版模板存为 .new 文件, 如需合并新结构请手动对比:`);
293
+ // 列出实际生成的 .new 文件
294
+ try {
295
+ for (const f of fs.readdirSync(path.join(cwd, ".qoder"))) {
296
+ if (f.endsWith(".new")) {
297
+ console.log(` .qoder/${f}`);
298
+ }
299
+ }
300
+ } catch { /* */ }
301
+ } else {
302
+ console.log(` config.yaml / settings.json: 已保护(未改动)`);
303
+ }
304
+
305
+ // === 2. 根文件(文档类, 安全覆盖)===
306
+ ["root/AGENTS.md", "root/新手指南.md"].forEach(f => {
307
+ const src = path.join(T, f);
308
+ if (fs.existsSync(src)) {
309
+ fs.copyFileSync(src, path.join(cwd, path.basename(f)));
310
+ }
311
+ });
312
+ console.log(` 根文档: 已更新`);
313
+
314
+ // === 3. 刷新 QoderWork commands(强制覆盖已存在的)===
315
+ runQoderWorkInstall(cwd, true);
316
+
317
+ // === 4. 写版本戳 ===
318
+ writeEngineVersion(cwd);
319
+
320
+ console.log(`\n${"=".repeat(50)}`);
321
+ console.log(` 升级完成! (v${PKG_VERSION})`);
322
+ console.log(`${"=".repeat(50)}`);
323
+ console.log(`\n 生效方式:`);
324
+ console.log(` Qoder IDE / Quest: 新建对话即可`);
325
+ console.log(` QoderWork: 重启应用或新建对话`);
326
+ if (protectedN > 0) {
327
+ console.log(`\n ⚠ 有配置文件新版有变化, 生成 .new 文件:`);
328
+ console.log(` 确认无需合并后可直接删除, 或手动合并后删除`);
329
+ }
330
+ console.log("");
331
+ }
332
+
333
+ function doHelp() {
334
+ console.log("");
335
+ console.log(`wlkj - AI 产品研发工作流 (v${PKG_VERSION})`);
336
+ console.log("");
337
+ console.log("=== 环境安装 (什么都没装时先跑这个) ===");
338
+ console.log(" npx @hupan56/wlkj install-env 检测+自动装 Node/Python/git");
339
+ console.log(" npx @hupan56/wlkj install-env --check 只检测不装");
340
+ console.log(" npx @hupan56/wlkj install-env --location D:\\wldev 指定安装目录");
341
+ console.log(" npx @hupan56/wlkj add-path D:\\wldev\\nodejs 把目录加到系统 PATH");
342
+ console.log("");
343
+ console.log("=== 一键安装 (装好环境后) ===");
344
+ console.log(" npx @hupan56/wlkj init [你的名字] 安装完整引擎 + 自动初始化");
345
+ console.log("");
346
+ console.log("=== 升级 (已装用户, 新版本发布后) ===");
347
+ console.log(" npx @hupan56/wlkj update 刷新引擎, 保护你的配置");
348
+ console.log("");
349
+ console.log("=== 安装后怎么用 ===");
350
+ console.log(" 在 Qoder (IDE/Quest/QoderWork) 里:");
351
+ console.log(" 说中文: '写个 XX 的需求' '查一下 XX 代码' '建个任务'");
352
+ console.log(" 或斜杠: /wl-prd /wl-search /wl-task /wl-status /wl-report");
353
+ console.log("");
354
+ console.log("=== 状态 ===");
355
+ console.log(" npx @hupan56/wlkj status 查看当前状态 + 引擎版本");
356
+ console.log("");
357
+ console.log("=== 环境修复 ===");
358
+ console.log(" python .qoder/scripts/init_doctor.py --fix 自动修复环境");
359
+ console.log(" python .qoder/scripts/setup.py 重新初始化");
360
+ console.log("");
361
+ console.log("=== Git (中文命令) ===");
362
+ console.log(" npx @hupan56/wlkj 提交PRD 提交 PRD");
363
+ console.log(" npx @hupan56/wlkj 提交任务 提交任务");
364
+ console.log(" npx @hupan56/wlkj 提交 全部提交并推送");
365
+ console.log(" npx @hupan56/wlkj 拉取最新 / 同步 git pull");
366
+ console.log("");
367
+ }
368
+
369
+ function doInstallEnv(checkOnly = false, location = null) {
370
+ const { execSync } = require("child_process");
371
+ const isWin = process.platform === "win32";
372
+ const isMac = process.platform === "darwin";
373
+
374
+ // 解析 --location(CLI 入口已提取,这里兜底)
375
+ console.log("\n=== 环境检测 ===\n");
376
+ if (location) {
377
+ console.log(` 安装位置: ${location}${isWin ? "" : " (仅 Windows winget 支持指定目录)"}`);
378
+ // Windows: 预建目录
379
+ if (isWin) {
380
+ try { fs.mkdirSync(location, { recursive: true }); } catch { /* */ }
381
+ }
382
+ }
383
+
384
+ // 检测函数
385
+ function check(cmd) {
386
+ try { return execSync(`${cmd} --version`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim().split("\n")[0]; }
387
+ catch { return null; }
388
+ }
389
+ function has(cmd) {
390
+ try { execSync(`where ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; }
391
+ catch { try { execSync(`which ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; } catch { return false; } }
392
+ }
393
+ // Windows: 把目录加到用户 PATH(幂等,已存在不重复加)
394
+ function addToPathWin(dir) {
395
+ if (!isWin) return false;
396
+ try {
397
+ // 用 setx 会截断超长 PATH,改用 PowerShell 读 User PATH 追加
398
+ const ps = `powershell -NoProfile -Command "$p=[Environment]::GetEnvironmentVariable('Path','User'); if($p -notlike '*{dir}*'){[Environment]::SetEnvironmentVariable('Path',$p.TrimEnd(';')+';{dir}','User')}"`.replace(/{dir}/g, dir.replace(/\\/g, "\\\\"));
399
+ execSync(ps, { stdio: "pipe", timeout: 15000 });
400
+ return true;
401
+ } catch (e) { console.log(` [WARN] 加 PATH 失败(可手动加): ${dir}`); return false; }
402
+ }
403
+ function install(name, wingetId, brewPkg) {
404
+ if (checkOnly) return false;
405
+ try {
406
+ if (isWin && has("winget")) {
407
+ console.log(` 用 winget 装 ${name}...`);
408
+ // winget --location 不一定被安装包尊重,但先传上
409
+ let cmd = `winget install --id ${wingetId} -e --source winget --accept-source-agreements --accept-package-agreements`;
410
+ if (location) cmd += ` --location "${location}"`;
411
+ execSync(cmd, { stdio: "inherit", timeout: 300000 });
412
+ return true;
413
+ } else if (isMac && has("brew")) {
414
+ console.log(` 用 brew 装 ${name}...`);
415
+ execSync(`brew install ${brewPkg}`, { stdio: "inherit", timeout: 300000 });
416
+ return true;
417
+ } else if (!isWin && !isMac) {
418
+ // Linux: 尝试 apt/dnf/yum
419
+ for (const [mgr, pkg] of [["apt", brewPkg], ["dnf", brewPkg]]) {
420
+ if (has(mgr)) {
421
+ console.log(` 用 ${mgr} 装 ${name}...`);
422
+ execSync(`sudo ${mgr} install -y ${pkg}`, { stdio: "inherit", timeout: 300000 });
423
+ return true;
424
+ }
425
+ }
426
+ }
427
+ } catch (e) { console.log(` 自动安装失败: ${(e.message || "").slice(0, 80)}`); }
428
+ return false;
429
+ }
430
+
431
+ const nodeVer = check("node");
432
+ const pyVer = check("python") || check("python3");
433
+ const gitVer = check("git");
434
+
435
+ console.log(` Node.js: ${nodeVer ? "[OK] " + nodeVer : "[缺失] (npx 命令需要)"}`);
436
+ console.log(` Python: ${pyVer ? "[OK] " + pyVer : "[缺失] (引擎需要)"}`);
437
+ console.log(` git: ${gitVer ? "[OK] " + gitVer : "[缺失] (可选, 核心功能不依赖)"}`);
438
+
439
+ if (checkOnly) {
440
+ console.log("\n仅检测。去掉 --check 自动安装。");
441
+ return;
442
+ }
443
+
444
+ const need = [];
445
+ if (!nodeVer) {
446
+ console.log("");
447
+ if (nodeVer || install("Node.js", "OpenJS.NodeJS.LTS", "node")) { console.log(` [OK] Node.js 已装`); }
448
+ else { need.push("Node.js (手动: https://nodejs.org)"); }
449
+ }
450
+ if (!pyVer) {
451
+ console.log("");
452
+ if (install("Python", "Python.Python.3.12", "python@3.12")) { console.log(` [OK] Python 已装`); }
453
+ else { need.push("Python (手动: https://python.org, 勾 Add to PATH)"); }
454
+ }
455
+ if (!gitVer) {
456
+ console.log("");
457
+ if (install("git", "Git.Git", "git")) { console.log(` [OK] git 已装`); }
458
+ else { need.push("git (手动: https://git-scm.com, 可选)"); }
459
+ }
460
+
461
+ // --location 模式: 提示用户验证安装位置 + 手动加 PATH
462
+ // (winget 的 --location 对 Node/Python 不一定生效,需用户确认)
463
+ if (location && isWin) {
464
+ console.log(`\n--- 安装位置确认 ---`);
465
+ console.log(` 你指定了: ${location}`);
466
+ console.log(` 注意: winget 的 --location 对 Node.js/Python 不一定生效`);
467
+ console.log(` (取决于安装包是否支持自定义路径)`);
468
+ console.log(` 请检查实际装到哪了:`);
469
+ console.log(` 重新打开终端后跑: node --version python --version git --version`);
470
+ console.log(` 如果某软件没进 PATH, 手动把它加进去:`);
471
+ console.log(` 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → Path → 新建`);
472
+ console.log(` 或用命令: npx @hupan56/wlkj add-path "D:\\wldev\\nodejs"`);
473
+ }
474
+
475
+ console.log("\n=== 结果 ===");
476
+ if (need.length === 0) {
477
+ console.log(" 环境就绪! 下一步: npx @hupan56/wlkj init");
478
+ console.log(" (新装的可能需要重新打开终端)");
479
+ } else {
480
+ console.log(" 以下需手动安装:");
481
+ need.forEach(n => console.log(" - " + n));
482
+ }
483
+ }
484
+
485
+ // add-path: 把目录加到 Windows 用户 PATH(幂等)
486
+ function doAddPath(dir) {
487
+ if (!dir) { console.log("用法: npx @hupan56/wlkj add-path <目录>"); return; }
488
+ if (process.platform !== "win32") {
489
+ console.log("add-path 仅支持 Windows。Mac/Linux 请手动加到 ~/.zshrc 或 ~/.bashrc");
490
+ return;
491
+ }
492
+ const abs = path.resolve(dir);
493
+ if (!fs.existsSync(abs)) {
494
+ console.log(`[警告] 目录不存在: ${abs}(仍会加到 PATH)`);
495
+ }
496
+ try {
497
+ const ps = `powershell -NoProfile -Command "$p=[Environment]::GetEnvironmentVariable('Path','User'); if($p -notlike '*${abs.replace(/\\/g,"\\\\")}*'){[Environment]::SetEnvironmentVariable('Path',$p.TrimEnd(';')+';${abs.replace(/\\/g,"\\\\")}','User'); Write-Output 'ADDED'} else { Write-Output 'EXISTS' }"`;
498
+ const out = execSync(ps, { encoding: "utf-8", timeout: 15000 }).trim();
499
+ console.log(` ${out === "ADDED" ? "已加入" : "已存在(无需重复加)"}: ${abs}`);
500
+ console.log(` ⚠ 需要重新打开终端才生效`);
501
+ } catch (e) {
502
+ console.log(` 失败: ${(e.message || "").slice(0, 100)}`);
503
+ console.log(` 手动加: 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → Path → 新建 → ${abs}`);
504
+ }
505
+ }
506
+
507
+ const [,, cmd, ...rest] = process.argv;
508
+ const mapped = rest.map(a => a === "-p" ? "--priority" : a);
509
+
510
+ switch (cmd) {
511
+ case "init": doInit(rest[0]); break;
512
+ case "update": case "upgrade": doUpdate(); break;
513
+ case "install-env": {
514
+ const checkOnly = rest.includes("--check");
515
+ // 提取 --location <dir> 或 --location=<dir>
516
+ let loc = null;
517
+ const li = rest.indexOf("--location");
518
+ if (li !== -1 && rest[li + 1]) loc = rest[li + 1];
519
+ const eq = rest.find(a => a.startsWith("--location="));
520
+ if (eq) loc = eq.slice("--location=".length);
521
+ doInstallEnv(checkOnly, loc);
522
+ break;
523
+ }
524
+ case "add-path": doAddPath(rest[0]); break;
525
+ case "task": process.stdout.write(py("task.py", mapped)); break;
526
+ case "status": doStatus(); break;
527
+ case "session": process.stdout.write(py("add_session.py", rest)); break;
528
+ case "help": case "--help": case "-h": doHelp(); break;
529
+ case "提交": case "推送": case "拉取最新": case "同步":
530
+ case "查看状态": case "提交PRD": case "提交Spec": case "提交任务":
531
+ gitOp(cmd); break;
532
+ default: doHelp();
533
533
  }