@hupan56/wlkj 2.3.2 → 2.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hupan56/wlkj",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
5
  "bin": {
6
6
  "wlkj": "bin/cli.js"
@@ -140,6 +140,7 @@ def validate_req_ids(prd_paths: List[str], repo_root: Union[str, Path]) -> None:
140
140
  seen = {}
141
141
  errors = []
142
142
  # 全库已有 REQ-ID (用于跨人撞号检测)
143
+ # existing: {(year,n): [filepath, ...]}
143
144
  existing = scan_existing_req_ids(repo_root)
144
145
 
145
146
  for p in prd_paths:
@@ -158,13 +159,17 @@ def validate_req_ids(prd_paths: List[str], repo_root: Union[str, Path]) -> None:
158
159
  )
159
160
  else:
160
161
  seen[key] = p
161
- # 跨人/全库撞号检测: 该号已存在于仓库, 且不是同一个文件 (允许修改自己的)
162
- if key in existing and os.path.basename(existing[key]) != basename:
163
- errors.append(
164
- "撞号: %s 与仓库已有的 %s 都是 REQ-%d-%03d "
165
- "(两人离线各建了同号 PRD? 请改其中一个的编号)"
166
- % (basename, os.path.basename(existing[key]), year, n)
167
- )
162
+ # 跨人/全库撞号检测: 该号已存在于仓库的其他文件
163
+ # existing[key] 是列表; 只要里面有"文件名跟当前不同的"就算撞号
164
+ if key in existing:
165
+ for other_path in existing[key]:
166
+ if os.path.basename(other_path) != basename:
167
+ errors.append(
168
+ "撞号: %s 与仓库已有的 %s 都是 REQ-%d-%03d "
169
+ "(两人离线各建了同号 PRD? 请改其中一个的编号)"
170
+ % (basename, os.path.basename(other_path), year, n)
171
+ )
172
+ break # 一个冲突够了
168
173
  # 超界检测 (NNN 不能超过计数器, 防止跳号)
169
174
  max_n = current_counter(str(year), repo_root)
170
175
  if max_n > 0 and n > max_n:
@@ -253,7 +258,7 @@ def scan_existing_req_ids(repo_root: Optional[Union[str, Path]] = None) -> dict:
253
258
  if d.is_dir():
254
259
  scan_dirs.append(d)
255
260
 
256
- existing = {}
261
+ existing = {} # (year, n) -> [filepath, ...] (一个号可能被多个文件用)
257
262
  for d in scan_dirs:
258
263
  if not d.is_dir():
259
264
  continue
@@ -263,7 +268,7 @@ def scan_existing_req_ids(repo_root: Optional[Union[str, Path]] = None) -> dict:
263
268
  continue
264
269
  parsed = parse_req_id(f.name)
265
270
  if parsed:
266
- existing[parsed] = str(f)
271
+ existing.setdefault(parsed, []).append(str(f))
267
272
  except OSError:
268
273
  continue
269
274
  return existing
package/templates/cli.js DELETED
@@ -1,198 +0,0 @@
1
- #!/usr/bin/env node
2
- // wlkj - workflow toolkit
3
- // 参考 Trellis 架构:用户看到的极简,背后 scripts/hooks 精确控制
4
-
5
- const path = require("path");
6
- const fs = require("fs");
7
- const { execSync } = require("child_process");
8
-
9
- const T = path.join(__dirname, "..", "templates");
10
-
11
- // --- helpers ---
12
- function py(script, args = []) {
13
- const p = path.join(process.cwd(), ".qoder", "scripts", script);
14
- if (!fs.existsSync(p)) {
15
- console.log("请先运行: npx wlkj init");
16
- process.exit(1);
17
- }
18
- try {
19
- return execSync(
20
- `python "${p}" ${args.map(a => `"${a}"`).join(" ")}`,
21
- { cwd: process.cwd(), encoding: "utf-8", timeout: 15000 }
22
- );
23
- } catch (e) {
24
- return (e.stdout || e.message);
25
- }
26
- }
27
-
28
- function cp(src, dest) {
29
- const s = path.join(T, src);
30
- const d = path.join(process.cwd(), dest);
31
- if (!fs.existsSync(s)) return;
32
- fs.mkdirSync(path.dirname(d), { recursive: true });
33
- if (!fs.existsSync(d)) { fs.copyFileSync(s, d); return true; }
34
- return false;
35
- }
36
-
37
- function cpDir(src, dest) {
38
- const s = path.join(T, src);
39
- const d = path.join(process.cwd(), dest);
40
- if (!fs.existsSync(s)) return;
41
- fs.mkdirSync(d, { recursive: true });
42
- const items = fs.readdirSync(s);
43
- items.forEach(item => {
44
- const srcPath = path.join(s, item);
45
- const destPath = path.join(d, item);
46
- if (fs.statSync(srcPath).isDirectory()) {
47
- cpDir(path.join(src, item), path.join(dest, item));
48
- } else if (!fs.existsSync(destPath)) {
49
- fs.copyFileSync(srcPath, destPath);
50
- }
51
- });
52
- }
53
-
54
- // --- init ---
55
- function doInit(name) {
56
- const cwd = process.cwd();
57
- console.log(``);
58
- console.log(`wlkj init -> ${cwd}`);
59
-
60
- // 目录结构
61
- const dirs = [
62
- ".qoder/rules", ".qoder/context", ".qoder/scripts/common",
63
- ".qoder/scripts/hooks", ".qoder/skills/prd-generator",
64
- ".qoder/skills/spec-generator", ".qoder/skills/spec-coder",
65
- ".qoder/skills/test-generator", ".qoder/agents", ".qoder/hooks",
66
- ".qoder/tasks", ".qoder/archive", ".qoder/workspace",
67
- ".qoder/.runtime/sessions",
68
- ".codex/hooks", ".codex/agents",
69
- "docs/ai/prd", "docs/ai/specs", "src", "tests"
70
- ];
71
- dirs.forEach(d => fs.mkdirSync(path.join(cwd, d), { recursive: true }));
72
-
73
- // 模板文件(flat list,一目了然)
74
- let copied = 0;
75
- const files = [
76
- // .qoder 核心
77
- ["qoder/config.yaml", ".qoder/config.yaml"],
78
- ["qoder/workflow.md", ".qoder/workflow.md"],
79
- // rules
80
- ["rules/code-style.md", ".qoder/rules/code-style.md"],
81
- ["rules/prd-template.md", ".qoder/rules/prd-template.md"],
82
- ["rules/spec-template.md", ".qoder/rules/spec-template.md"],
83
- ["rules/testing.md", ".qoder/rules/testing.md"],
84
- // context
85
- ["context/architecture.md", ".qoder/context/architecture.md"],
86
- ["context/data-dictionary.md",".qoder/context/data-dictionary.md"],
87
- // skills
88
- ["skills/prd-generator/SKILL.md", ".qoder/skills/prd-generator/SKILL.md"],
89
- ["skills/spec-generator/SKILL.md", ".qoder/skills/spec-generator/SKILL.md"],
90
- ["skills/spec-coder/SKILL.md", ".qoder/skills/spec-coder/SKILL.md"],
91
- ["skills/test-generator/SKILL.md", ".qoder/skills/test-generator/SKILL.md"],
92
- // agents
93
- ["agents/qoder-spec-gen.toml", ".qoder/agents/qoder-spec-gen.toml"],
94
- ["agents/qoder-coder.toml", ".qoder/agents/qoder-coder.toml"],
95
- ["agents/qoder-test-gen.toml", ".qoder/agents/qoder-test-gen.toml"],
96
- // hooks
97
- ["hooks/post-prd-push.py", ".qoder/hooks/post-prd-push.py"],
98
- // scripts
99
- ["scripts/init_developer.py", ".qoder/scripts/init_developer.py"],
100
- ["scripts/task.py", ".qoder/scripts/task.py"],
101
- ["scripts/add_session.py", ".qoder/scripts/add_session.py"],
102
- ["scripts/common/__init__.py", ".qoder/scripts/common/__init__.py"],
103
- ["scripts/common/paths.py", ".qoder/scripts/common/paths.py"],
104
- ["scripts/common/developer.py", ".qoder/scripts/common/developer.py"],
105
- ["scripts/common/active_task.py",".qoder/scripts/common/active_task.py"],
106
- ["scripts/common/task_utils.py", ".qoder/scripts/common/task_utils.py"],
107
- // codex hooks
108
- ["codex/config.toml", ".codex/config.toml"],
109
- ["codex/hooks.json", ".codex/hooks.json"],
110
- ["codex/hooks/inject-pipeline-state.py", ".codex/hooks/inject-pipeline-state.py"],
111
- ["codex/agents/qoder-spec-gen.toml", ".codex/agents/qoder-spec-gen.toml"],
112
- ["codex/agents/qoder-coder.toml", ".codex/agents/qoder-coder.toml"],
113
- ["codex/agents/qoder-test-gen.toml", ".codex/agents/qoder-test-gen.toml"],
114
- // root
115
- ["root/AGENTS.md", "AGENTS.md"],
116
- ];
117
-
118
- files.forEach(([s, d]) => { if (cp(s, d)) copied++; });
119
- console.log(` copied ${copied} files`);
120
-
121
- // git init
122
- if (!fs.existsSync(path.join(cwd, ".git"))) {
123
- try {
124
- execSync("git init", { cwd, stdio: "pipe" });
125
- console.log(" git init done");
126
- } catch (_) {}
127
- }
128
-
129
- // developer
130
- if (name) {
131
- console.log("");
132
- process.stdout.write(py("init_developer.py", [name]));
133
- } else {
134
- console.log("");
135
- console.log(" next: npx wlkj init <your-name>");
136
- }
137
- console.log("");
138
- }
139
-
140
- // --- status ---
141
- function doStatus() {
142
- console.log("");
143
- // developer
144
- const dev = path.join(process.cwd(), ".qoder", ".developer");
145
- if (fs.existsSync(dev)) {
146
- const line = fs.readFileSync(dev, "utf-8").split("\n")[0];
147
- console.log("dev: " + line.split("=")[1]?.trim());
148
- } else {
149
- console.log("dev: -");
150
- }
151
- // task
152
- process.stdout.write(py("task.py", ["current", "--source"]));
153
- // docs
154
- const count = (d) => {
155
- const p = path.join(process.cwd(), d);
156
- return fs.existsSync(p) ? fs.readdirSync(p).filter(f => f.endsWith(".md")).length : 0;
157
- };
158
- console.log(`prd: ${count("docs/ai/prd")} spec: ${count("docs/ai/specs")}`);
159
- console.log("");
160
- }
161
-
162
- // --- main ---
163
- const [,, cmd, ...rest] = process.argv;
164
-
165
- switch (cmd) {
166
- case "init":
167
- doInit(rest[0]);
168
- break;
169
- case "task":
170
- // 映射 -p 到 --priority(CLI 便捷)
171
- const mapped = rest.map(a => a === "-p" ? "--priority" : a);
172
- process.stdout.write(py("task.py", mapped));
173
- break;
174
- case "status":
175
- doStatus();
176
- break;
177
- case "session":
178
- process.stdout.write(py("add_session.py", rest));
179
- break;
180
- default:
181
- console.log("");
182
- console.log("wlkj - workflow toolkit");
183
- console.log("");
184
- console.log(" npx wlkj init [name] init workflow + developer");
185
- console.log(" npx wlkj status show status");
186
- console.log(" npx wlkj task <cmd> task management");
187
- console.log(" npx wlkj session <args> record session log");
188
- console.log("");
189
- console.log("task commands:");
190
- console.log(' create "Title" [-p P0|P1|P2|P3] [--assignee who]');
191
- console.log(" list [--mine] [--status planning|in_progress]");
192
- console.log(" start <name>");
193
- console.log(" finish");
194
- console.log(" current");
195
- console.log(" archive <name>");
196
- console.log(" add-subtask <parent> <child>");
197
- console.log("");
198
- }
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- DEPRECATED: code_index.py used to keep its own (cruder) index builder
5
- whose schema conflicted with git_sync.py's - running it would overwrite
6
- the real indexes. It now delegates to the single implementation.
7
-
8
- Usage:
9
- python code_index.py scan # full index rebuild (= git_sync.py --index-only)
10
- python code_index.py search <kw> # use search_index.py instead
11
- """
12
-
13
- import os
14
- import sys
15
-
16
- # UTF-8 stdio (防御性: stdout 被捕获时不崩溃)
17
- try:
18
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
19
- except (AttributeError, TypeError, OSError, IOError):
20
- try:
21
- sys.stdout.reconfigure(encoding='utf-8')
22
- except Exception:
23
- pass
24
-
25
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
-
27
- if __name__ == '__main__':
28
- cmd = sys.argv[1] if len(sys.argv) > 1 else ''
29
- if cmd == 'scan':
30
- from git_sync import build_full_indexes, FAILURES
31
- build_full_indexes()
32
- sys.exit(1 if FAILURES else 0)
33
- elif cmd == 'search' and len(sys.argv) > 2:
34
- import subprocess
35
- script = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'search_index.py')
36
- sys.exit(subprocess.call([sys.executable, script] + sys.argv[2:]))
37
- else:
38
- print('Usage:')
39
- print(' python code_index.py scan # full index rebuild')
40
- print(' python code_index.py search <kw> # -> search_index.py')
41
- sys.exit(1)
@@ -1,5 +0,0 @@
1
- # notify.py - Simple console notification
2
- class Notifier:
3
- def notify(self, event, title, content):
4
- sep = '=' * 50
5
- print(chr(10).join(['', sep, 'NOTIFY: ' + title, sep, content, sep]))
@@ -1,29 +0,0 @@
1
- # team.py - Team member registration
2
- import os, json, sys
3
- NL = chr(10)
4
- BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5
-
6
- def add_member(name, role):
7
- members_dir = os.path.join(BASE, 'workspace', 'members')
8
- member_dir = os.path.join(members_dir, name)
9
- os.makedirs(os.path.join(member_dir, 'journal'), exist_ok=True)
10
- os.makedirs(os.path.join(member_dir, 'drafts'), exist_ok=True)
11
- os.makedirs(os.path.join(member_dir, 'inbox'), exist_ok=True)
12
- info = {'name': name, 'role': role}
13
- with open(os.path.join(member_dir, 'member.json'), 'w') as f:
14
- json.dump(info, f, indent=2)
15
- print('OK: member ' + name + ' (' + role + ') added')
16
-
17
- def list_members():
18
- md = os.path.join(BASE, 'workspace', 'members')
19
- if not os.path.isdir(md): print('No members'); return
20
- for d in sorted(os.listdir(md)):
21
- info_f = os.path.join(md, d, 'member.json')
22
- if os.path.isfile(info_f):
23
- info = json.load(open(info_f))
24
- print(d + ' [' + info.get('role', '?') + ']')
25
-
26
- if __name__ == '__main__':
27
- if len(sys.argv) < 2: list_members()
28
- elif sys.argv[1] == 'add' and len(sys.argv) >= 4: add_member(sys.argv[2], sys.argv[3])
29
- else: list_members()