@hupan56/wlkj 2.2.0 → 2.2.2
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 +262 -26
- package/package.json +23 -7
- package/templates/qoder/scripts/install_qoderwork.py +32 -7
package/bin/cli.js
CHANGED
|
@@ -7,6 +7,23 @@ const { execSync } = require("child_process");
|
|
|
7
7
|
|
|
8
8
|
const T = path.join(__dirname, "..", "templates");
|
|
9
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
|
+
|
|
10
27
|
function py(script, args = []) {
|
|
11
28
|
const p = path.join(process.cwd(), ".qoder", "scripts", script);
|
|
12
29
|
if (!fs.existsSync(p)) { console.log("请先运行: npx wlkj init"); process.exit(1); }
|
|
@@ -48,10 +65,16 @@ function doInit(name) {
|
|
|
48
65
|
|
|
49
66
|
// === 1. 拷贝完整引擎 (镜像 .qoder/ 结构) ===
|
|
50
67
|
// 源: templates/qoder/* 目标: .qoder/*
|
|
68
|
+
// 注意: init 用 "update" 模式的保护逻辑更安全 —— 新装时目标文件不存在,
|
|
69
|
+
// 保护逻辑不触发; 重跑 init 时 config.yaml/settings.json 不会被覆盖。
|
|
51
70
|
const qoderSrc = path.join(T, "qoder");
|
|
52
71
|
let copied = 0;
|
|
53
72
|
if (fs.existsSync(qoderSrc)) {
|
|
54
|
-
|
|
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
|
+
}
|
|
55
78
|
}
|
|
56
79
|
console.log(` 引擎: ${copied} 个文件 (${hasExisting ? "更新" : "新建"})`);
|
|
57
80
|
|
|
@@ -86,7 +109,8 @@ function doInit(name) {
|
|
|
86
109
|
const baseGitignore = [
|
|
87
110
|
"# Source code repos (cloned by git_sync.py)", "data/code/",
|
|
88
111
|
"", "# AI pipeline runtime", ".qoder/.developer", ".qoder/.current-task",
|
|
89
|
-
".qoder/.
|
|
112
|
+
".qoder/.engine-version", ".qoder/.runtime/", "",
|
|
113
|
+
"# Personal learning", ".qoder/learning/feedback.jsonl",
|
|
90
114
|
"", "# Machine-local", "data/index/.last-sync", "data/index/.prd-collected.json",
|
|
91
115
|
"data/index/.index-meta.json", "data/index/.sync-lock", "data/index/.file-keys.json",
|
|
92
116
|
"data/index/.inverted-cache.json", "data/index/*.corrupt",
|
|
@@ -113,8 +137,11 @@ function doInit(name) {
|
|
|
113
137
|
console.log(` setup.py 未找到, 跳过自动初始化`);
|
|
114
138
|
}
|
|
115
139
|
|
|
140
|
+
// === 7. 写版本戳 ===
|
|
141
|
+
writeEngineVersion(cwd);
|
|
142
|
+
|
|
116
143
|
console.log(`\n${"=".repeat(50)}`);
|
|
117
|
-
console.log(`
|
|
144
|
+
console.log(` 安装完成! (v${PKG_VERSION})`);
|
|
118
145
|
console.log(`${"=".repeat(50)}`);
|
|
119
146
|
console.log(`\n 现在可以开始了:`);
|
|
120
147
|
console.log(` 在 Qoder 里说 "写个 XX 的需求"`);
|
|
@@ -123,31 +150,98 @@ function doInit(name) {
|
|
|
123
150
|
console.log(` 环境问题? python .qoder/scripts/init_doctor.py --fix\n`);
|
|
124
151
|
}
|
|
125
152
|
|
|
126
|
-
// 递归拷贝目录,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 };
|
|
130
158
|
fs.mkdirSync(dst, { recursive: true });
|
|
131
159
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
132
160
|
if (entry.name === "__pycache__" || entry.name.endsWith(".pyc")) continue;
|
|
133
161
|
const s = path.join(src, entry.name);
|
|
134
162
|
const d = path.join(dst, entry.name);
|
|
135
163
|
if (entry.isDirectory()) {
|
|
136
|
-
|
|
164
|
+
const r = copyDirRecursive(s, d, mode);
|
|
165
|
+
copied += r.copied; protectedN += r.protectedN; skipped += r.skipped;
|
|
137
166
|
} else {
|
|
138
|
-
// 不覆盖已存在的 (增量更新, 保留用户改动)
|
|
139
|
-
// 但 .py 和 .md / config.yaml / rules 始终更新 (引擎文件)
|
|
140
167
|
const isEngine = entry.name.endsWith(".py") || entry.name.endsWith(".md") ||
|
|
141
168
|
entry.name.endsWith(".yaml") || entry.name.endsWith(".yml") ||
|
|
142
169
|
entry.name.endsWith(".toml") || entry.name.endsWith(".json") ||
|
|
143
170
|
entry.name.endsWith(".html") || entry.name.endsWith(".txt");
|
|
144
|
-
|
|
145
|
-
|
|
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++;
|
|
146
198
|
}
|
|
147
|
-
count++;
|
|
148
199
|
}
|
|
149
200
|
}
|
|
150
|
-
return
|
|
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
|
+
}
|
|
151
245
|
}
|
|
152
246
|
|
|
153
247
|
function doStatus() {
|
|
@@ -165,43 +259,127 @@ function doStatus() {
|
|
|
165
259
|
const n = fs.readdirSync(wsSpecs).filter(f => f.endsWith(".md")).length;
|
|
166
260
|
console.log(`PRDs: ${n}`);
|
|
167
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
|
+
}
|
|
168
330
|
console.log("");
|
|
169
331
|
}
|
|
170
332
|
|
|
171
333
|
function doHelp() {
|
|
172
334
|
console.log("");
|
|
173
|
-
console.log(
|
|
335
|
+
console.log(`wlkj - AI 产品研发工作流 (v${PKG_VERSION})`);
|
|
174
336
|
console.log("");
|
|
175
337
|
console.log("=== 环境安装 (什么都没装时先跑这个) ===");
|
|
176
338
|
console.log(" npx @hupan56/wlkj install-env 检测+自动装 Node/Python/git");
|
|
177
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");
|
|
178
342
|
console.log("");
|
|
179
343
|
console.log("=== 一键安装 (装好环境后) ===");
|
|
180
344
|
console.log(" npx @hupan56/wlkj init [你的名字] 安装完整引擎 + 自动初始化");
|
|
181
345
|
console.log("");
|
|
346
|
+
console.log("=== 升级 (已装用户, 新版本发布后) ===");
|
|
347
|
+
console.log(" npx @hupan56/wlkj update 刷新引擎, 保护你的配置");
|
|
348
|
+
console.log("");
|
|
182
349
|
console.log("=== 安装后怎么用 ===");
|
|
183
350
|
console.log(" 在 Qoder (IDE/Quest/QoderWork) 里:");
|
|
184
351
|
console.log(" 说中文: '写个 XX 的需求' '查一下 XX 代码' '建个任务'");
|
|
185
352
|
console.log(" 或斜杠: /wl-prd /wl-search /wl-task /wl-status /wl-report");
|
|
186
353
|
console.log("");
|
|
187
354
|
console.log("=== 状态 ===");
|
|
188
|
-
console.log(" npx wlkj status 查看当前状态");
|
|
355
|
+
console.log(" npx @hupan56/wlkj status 查看当前状态 + 引擎版本");
|
|
189
356
|
console.log("");
|
|
190
357
|
console.log("=== 环境修复 ===");
|
|
191
358
|
console.log(" python .qoder/scripts/init_doctor.py --fix 自动修复环境");
|
|
192
359
|
console.log(" python .qoder/scripts/setup.py 重新初始化");
|
|
193
360
|
console.log("");
|
|
194
361
|
console.log("=== Git (中文命令) ===");
|
|
195
|
-
console.log(" npx wlkj 提交PRD 提交 PRD");
|
|
196
|
-
console.log(" npx wlkj 提交任务 提交任务");
|
|
197
|
-
console.log(" npx wlkj 提交 全部提交并推送");
|
|
198
|
-
console.log(" npx wlkj 拉取最新 / 同步 git pull");
|
|
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");
|
|
199
366
|
console.log("");
|
|
200
367
|
}
|
|
201
368
|
|
|
202
|
-
function doInstallEnv(checkOnly = false) {
|
|
369
|
+
function doInstallEnv(checkOnly = false, location = null) {
|
|
203
370
|
const { execSync } = require("child_process");
|
|
371
|
+
const isWin = process.platform === "win32";
|
|
372
|
+
const isMac = process.platform === "darwin";
|
|
373
|
+
|
|
374
|
+
// 解析 --location(CLI 入口已提取,这里兜底)
|
|
204
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
|
+
}
|
|
205
383
|
|
|
206
384
|
// 检测函数
|
|
207
385
|
function check(cmd) {
|
|
@@ -212,15 +390,25 @@ function doInstallEnv(checkOnly = false) {
|
|
|
212
390
|
try { execSync(`where ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; }
|
|
213
391
|
catch { try { execSync(`which ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; } catch { return false; } }
|
|
214
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
|
+
}
|
|
215
403
|
function install(name, wingetId, brewPkg) {
|
|
216
404
|
if (checkOnly) return false;
|
|
217
|
-
const isWin = process.platform === "win32";
|
|
218
|
-
const isMac = process.platform === "darwin";
|
|
219
405
|
try {
|
|
220
406
|
if (isWin && has("winget")) {
|
|
221
407
|
console.log(` 用 winget 装 ${name}...`);
|
|
222
|
-
|
|
223
|
-
|
|
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 });
|
|
224
412
|
return true;
|
|
225
413
|
} else if (isMac && has("brew")) {
|
|
226
414
|
console.log(` 用 brew 装 ${name}...`);
|
|
@@ -270,6 +458,20 @@ function doInstallEnv(checkOnly = false) {
|
|
|
270
458
|
else { need.push("git (手动: https://git-scm.com, 可选)"); }
|
|
271
459
|
}
|
|
272
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
|
+
|
|
273
475
|
console.log("\n=== 结果 ===");
|
|
274
476
|
if (need.length === 0) {
|
|
275
477
|
console.log(" 环境就绪! 下一步: npx @hupan56/wlkj init");
|
|
@@ -280,12 +482,46 @@ function doInstallEnv(checkOnly = false) {
|
|
|
280
482
|
}
|
|
281
483
|
}
|
|
282
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
|
+
|
|
283
507
|
const [,, cmd, ...rest] = process.argv;
|
|
284
508
|
const mapped = rest.map(a => a === "-p" ? "--priority" : a);
|
|
285
509
|
|
|
286
510
|
switch (cmd) {
|
|
287
511
|
case "init": doInit(rest[0]); break;
|
|
288
|
-
case "
|
|
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;
|
|
289
525
|
case "task": process.stdout.write(py("task.py", mapped)); break;
|
|
290
526
|
case "status": doStatus(); break;
|
|
291
527
|
case "session": process.stdout.write(py("add_session.py", rest)); break;
|
package/package.json
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hupan56/wlkj",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
|
|
5
|
-
"bin": {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
"bin": {
|
|
6
|
+
"wlkj": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"templates/"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"workflow",
|
|
14
|
+
"ai",
|
|
15
|
+
"prd",
|
|
16
|
+
"pipeline",
|
|
17
|
+
"qoder",
|
|
18
|
+
"product",
|
|
19
|
+
"team"
|
|
20
|
+
],
|
|
8
21
|
"license": "MIT",
|
|
9
|
-
"publishConfig": {
|
|
10
|
-
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=16"
|
|
27
|
+
}
|
|
11
28
|
}
|
|
12
|
-
|
|
@@ -88,6 +88,16 @@ def find_source_skills() -> list:
|
|
|
88
88
|
return result
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def file_equal(a: Path, b: Path) -> bool:
|
|
92
|
+
"""快速判断两文件内容是否相同(先比大小再比内容,避免无谓全读)。"""
|
|
93
|
+
try:
|
|
94
|
+
if a.stat().st_size != b.stat().st_size:
|
|
95
|
+
return False
|
|
96
|
+
return a.read_bytes() == b.read_bytes()
|
|
97
|
+
except OSError:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
91
101
|
def create_junction(link: Path, target: Path) -> bool:
|
|
92
102
|
"""用 mklink /J 创建 junction。返回是否成功。"""
|
|
93
103
|
# 用 errors="replace" 防止 Windows GBK 输出(如"为...创建的联接")触发 UnicodeDecodeError
|
|
@@ -197,6 +207,8 @@ def main():
|
|
|
197
207
|
help="仅检查状态,不改动")
|
|
198
208
|
parser.add_argument("--copy", action="store_true",
|
|
199
209
|
help="用拷贝代替 junction(非 Windows 或不想软链时用)")
|
|
210
|
+
parser.add_argument("--force-commands", action="store_true",
|
|
211
|
+
help="强制覆盖已存在的 command 文件(升级时用;默认已存在则跳过)")
|
|
200
212
|
args = parser.parse_args()
|
|
201
213
|
|
|
202
214
|
print("=" * 56)
|
|
@@ -295,7 +307,7 @@ def main():
|
|
|
295
307
|
# 同时安装 commands (让 QoderWork 用户级也能看到 /wl-* 命令)
|
|
296
308
|
if action in ("install", "check") and SOURCE_COMMANDS_DIR.is_dir():
|
|
297
309
|
print("\n--- Commands (/wl-* 斜杠命令) ---")
|
|
298
|
-
cmd_count = {"ok": 0, "new": 0}
|
|
310
|
+
cmd_count = {"ok": 0, "new": 0, "upd": 0}
|
|
299
311
|
for cmd_file in sorted(SOURCE_COMMANDS_DIR.glob("wl-*.md")):
|
|
300
312
|
name = cmd_file.name # e.g. wl-prd.md
|
|
301
313
|
target = QODERWORK_COMMANDS_DIR / name
|
|
@@ -305,18 +317,31 @@ def main():
|
|
|
305
317
|
else:
|
|
306
318
|
print(f" [MISS] {name}")
|
|
307
319
|
else: # install
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
320
|
+
need_copy = False
|
|
321
|
+
if not target.exists():
|
|
322
|
+
need_copy = True
|
|
323
|
+
elif args.force_commands:
|
|
324
|
+
# 强制刷新(升级场景):仅当内容不同才覆盖,避免无谓写
|
|
325
|
+
import shutil
|
|
326
|
+
if not file_equal(cmd_file, target):
|
|
327
|
+
need_copy = True
|
|
328
|
+
if need_copy:
|
|
311
329
|
try:
|
|
312
330
|
QODERWORK_COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
|
|
313
|
-
# commands 是单文件, 用拷贝 (不像 skills 是目录用 junction)
|
|
314
331
|
import shutil
|
|
315
332
|
shutil.copy2(str(cmd_file), str(target))
|
|
316
|
-
|
|
333
|
+
if target.exists() and args.force_commands:
|
|
334
|
+
cmd_count["upd"] += 1
|
|
335
|
+
else:
|
|
336
|
+
cmd_count["new"] += 1
|
|
317
337
|
except OSError as e:
|
|
318
338
|
print(f" [ERR] {name}: {e}")
|
|
319
|
-
|
|
339
|
+
else:
|
|
340
|
+
cmd_count["ok"] += 1
|
|
341
|
+
msg = f" commands: {cmd_count['new']} 新建 / {cmd_count['ok']} 已存在"
|
|
342
|
+
if cmd_count["upd"]:
|
|
343
|
+
msg += f" / {cmd_count['upd']} 刷新"
|
|
344
|
+
print(msg)
|
|
320
345
|
|
|
321
346
|
if action == "install" and counts["err"] == 0:
|
|
322
347
|
print("\n✓ 安装完成。重启 QoderWork(或新建对话)后技能生效。")
|