@zentao-hub/cli 0.1.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -0
  3. package/README.zh-CN.md +192 -0
  4. package/dist/agents.d.ts +62 -0
  5. package/dist/agents.js +202 -0
  6. package/dist/agents.js.map +1 -0
  7. package/dist/commands/init.d.ts +9 -0
  8. package/dist/commands/init.js +94 -0
  9. package/dist/commands/init.js.map +1 -0
  10. package/dist/commands/install-claude-md.d.ts +12 -0
  11. package/dist/commands/install-claude-md.js +25 -0
  12. package/dist/commands/install-claude-md.js.map +1 -0
  13. package/dist/commands/install-commands.d.ts +8 -0
  14. package/dist/commands/install-commands.js +27 -0
  15. package/dist/commands/install-commands.js.map +1 -0
  16. package/dist/commands/install-hooks.d.ts +7 -0
  17. package/dist/commands/install-hooks.js +31 -0
  18. package/dist/commands/install-hooks.js.map +1 -0
  19. package/dist/commands/install-mcp.d.ts +20 -0
  20. package/dist/commands/install-mcp.js +203 -0
  21. package/dist/commands/install-mcp.js.map +1 -0
  22. package/dist/commands/login.d.ts +17 -0
  23. package/dist/commands/login.js +110 -0
  24. package/dist/commands/login.js.map +1 -0
  25. package/dist/commands/logout.d.ts +7 -0
  26. package/dist/commands/logout.js +59 -0
  27. package/dist/commands/logout.js.map +1 -0
  28. package/dist/commands/profiles.d.ts +5 -0
  29. package/dist/commands/profiles.js +26 -0
  30. package/dist/commands/profiles.js.map +1 -0
  31. package/dist/commands/register.d.ts +12 -0
  32. package/dist/commands/register.js +26 -0
  33. package/dist/commands/register.js.map +1 -0
  34. package/dist/commands/repos.d.ts +8 -0
  35. package/dist/commands/repos.js +28 -0
  36. package/dist/commands/repos.js.map +1 -0
  37. package/dist/commands/uninstall-claude-md.d.ts +6 -0
  38. package/dist/commands/uninstall-claude-md.js +13 -0
  39. package/dist/commands/uninstall-claude-md.js.map +1 -0
  40. package/dist/commands/uninstall-commands.d.ts +7 -0
  41. package/dist/commands/uninstall-commands.js +21 -0
  42. package/dist/commands/uninstall-commands.js.map +1 -0
  43. package/dist/commands/uninstall-hooks.d.ts +6 -0
  44. package/dist/commands/uninstall-hooks.js +36 -0
  45. package/dist/commands/uninstall-hooks.js.map +1 -0
  46. package/dist/commands/uninstall-mcp.d.ts +8 -0
  47. package/dist/commands/uninstall-mcp.js +99 -0
  48. package/dist/commands/uninstall-mcp.js.map +1 -0
  49. package/dist/commands/uninstall.d.ts +14 -0
  50. package/dist/commands/uninstall.js +28 -0
  51. package/dist/commands/uninstall.js.map +1 -0
  52. package/dist/commands/use.d.ts +6 -0
  53. package/dist/commands/use.js +20 -0
  54. package/dist/commands/use.js.map +1 -0
  55. package/dist/commands/whoami.d.ts +6 -0
  56. package/dist/commands/whoami.js +48 -0
  57. package/dist/commands/whoami.js.map +1 -0
  58. package/dist/commands/workspace.d.ts +8 -0
  59. package/dist/commands/workspace.js +28 -0
  60. package/dist/commands/workspace.js.map +1 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +496 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/paths.d.ts +16 -0
  65. package/dist/paths.js +36 -0
  66. package/dist/paths.js.map +1 -0
  67. package/dist/prompt.d.ts +7 -0
  68. package/dist/prompt.js +43 -0
  69. package/dist/prompt.js.map +1 -0
  70. package/dist/registry.d.ts +16 -0
  71. package/dist/registry.js +42 -0
  72. package/dist/registry.js.map +1 -0
  73. package/dist/util.d.ts +27 -0
  74. package/dist/util.js +70 -0
  75. package/dist/util.js.map +1 -0
  76. package/package.json +57 -0
  77. package/templates/.claude/commands/bug-analyze.md +46 -0
  78. package/templates/.claude/commands/fix-bug.md +193 -0
  79. package/templates/.githooks/commit-msg +47 -0
  80. package/templates/CLAUDE.md +86 -0
  81. package/templates/repos.yaml.example +26 -0
@@ -0,0 +1,42 @@
1
+ import fs from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import yaml from "js-yaml";
5
+ import { resolveWorkspace } from "./paths.js";
6
+ export function registryPath(profile, workspace) {
7
+ return path.join(resolveWorkspace(profile, workspace), "repos.yaml");
8
+ }
9
+ export async function loadRegistry(profile, workspace) {
10
+ const file = registryPath(profile, workspace);
11
+ if (!existsSync(file))
12
+ return { products: {} };
13
+ const text = await fs.readFile(file, "utf8");
14
+ const parsed = (yaml.load(text) ?? {});
15
+ return { products: parsed.products ?? {} };
16
+ }
17
+ export async function saveRegistry(reg, profile, workspace) {
18
+ const file = registryPath(profile, workspace);
19
+ await fs.mkdir(path.dirname(file), { recursive: true });
20
+ const text = yaml.dump(reg, { sortKeys: false, lineWidth: 120 });
21
+ await fs.writeFile(file, text, "utf8");
22
+ }
23
+ export function listEntries(reg, productId) {
24
+ return reg.products[String(productId)] ?? [];
25
+ }
26
+ export function addEntry(reg, productId, entry) {
27
+ const key = String(productId);
28
+ const entries = reg.products[key] ?? [];
29
+ const normalized = {
30
+ path: path.resolve(entry.path),
31
+ ...(entry.name ? { name: entry.name } : {}),
32
+ ...(entry.tags && entry.tags.length ? { tags: entry.tags } : {}),
33
+ };
34
+ if (entries.some((e) => path.resolve(e.path) === normalized.path)) {
35
+ reg.products[key] = entries;
36
+ return { added: false, entries };
37
+ }
38
+ const next = [...entries, normalized];
39
+ reg.products[key] = next;
40
+ return { added: true, entries: next };
41
+ }
42
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
package/dist/util.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ export declare function copyFile(src: string, dest: string, opts?: {
2
+ overwrite?: boolean;
3
+ }): Promise<"copied" | "skipped">;
4
+ /**
5
+ * Delete a file if it exists. Returns "removed" / "missing" so callers can
6
+ * print symmetric output to install-style commands.
7
+ */
8
+ export declare function removeFile(filePath: string): Promise<"removed" | "missing">;
9
+ /**
10
+ * Remove a directory if it exists and is empty. Used by uninstall-* commands
11
+ * to tidy up empty `.githooks/` once the last hook is gone, without ever
12
+ * touching directories that still hold user files.
13
+ */
14
+ export declare function removeIfEmpty(dirPath: string): Promise<boolean>;
15
+ export declare function ensureExecutable(filePath: string): Promise<void>;
16
+ export declare function isGitRepo(repoPath: string): boolean;
17
+ export declare function git(repoPath: string, args: string[]): {
18
+ status: number;
19
+ stdout: string;
20
+ stderr: string;
21
+ };
22
+ export declare class CliError extends Error {
23
+ readonly exitCode: number;
24
+ constructor(message: string, exitCode?: number);
25
+ }
26
+ export declare function info(msg: string): void;
27
+ export declare function warn(msg: string): void;
package/dist/util.js ADDED
@@ -0,0 +1,70 @@
1
+ import fs from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ export async function copyFile(src, dest, opts = {}) {
6
+ await fs.mkdir(path.dirname(dest), { recursive: true });
7
+ if (existsSync(dest) && !opts.overwrite)
8
+ return "skipped";
9
+ await fs.copyFile(src, dest);
10
+ return "copied";
11
+ }
12
+ /**
13
+ * Delete a file if it exists. Returns "removed" / "missing" so callers can
14
+ * print symmetric output to install-style commands.
15
+ */
16
+ export async function removeFile(filePath) {
17
+ if (!existsSync(filePath))
18
+ return "missing";
19
+ await fs.unlink(filePath);
20
+ return "removed";
21
+ }
22
+ /**
23
+ * Remove a directory if it exists and is empty. Used by uninstall-* commands
24
+ * to tidy up empty `.githooks/` once the last hook is gone, without ever
25
+ * touching directories that still hold user files.
26
+ */
27
+ export async function removeIfEmpty(dirPath) {
28
+ if (!existsSync(dirPath))
29
+ return false;
30
+ try {
31
+ await fs.rmdir(dirPath);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ export async function ensureExecutable(filePath) {
39
+ try {
40
+ await fs.chmod(filePath, 0o755);
41
+ }
42
+ catch {
43
+ // best-effort; on Windows chmod is a no-op
44
+ }
45
+ }
46
+ export function isGitRepo(repoPath) {
47
+ const r = spawnSync("git", ["-C", repoPath, "rev-parse", "--git-dir"], {
48
+ encoding: "utf8",
49
+ });
50
+ return r.status === 0;
51
+ }
52
+ export function git(repoPath, args) {
53
+ const r = spawnSync("git", ["-C", repoPath, ...args], { encoding: "utf8" });
54
+ return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
55
+ }
56
+ export class CliError extends Error {
57
+ exitCode;
58
+ constructor(message, exitCode = 1) {
59
+ super(message);
60
+ this.exitCode = exitCode;
61
+ this.name = "CliError";
62
+ }
63
+ }
64
+ export function info(msg) {
65
+ process.stdout.write(`${msg}\n`);
66
+ }
67
+ export function warn(msg) {
68
+ process.stderr.write(`[warn] ${msg}\n`);
69
+ }
70
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@zentao-hub/cli",
3
+ "version": "0.1.0",
4
+ "description": "Helper CLI for @zentao-hub/mcp — registers the MCP server, installs slash commands / prompts (Claude Code, GitHub Copilot, Codex CLI), installs a commit-msg hook, and manages the bug hub + repos.yaml registry.",
5
+ "license": "MIT",
6
+ "author": "Deven Liu <iliudonghui@gmail.com>",
7
+ "keywords": [
8
+ "zentao",
9
+ "禅道",
10
+ "mcp",
11
+ "claude",
12
+ "claude-code",
13
+ "github-copilot",
14
+ "copilot-cli",
15
+ "codex",
16
+ "cli",
17
+ "slash-command",
18
+ "commit-msg-hook"
19
+ ],
20
+ "homepage": "https://github.com/devenliu/zentao-hub/tree/main/packages/cli#readme",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/devenliu/zentao-hub.git",
24
+ "directory": "packages/cli"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/devenliu/zentao-hub/issues"
28
+ },
29
+ "type": "module",
30
+ "bin": {
31
+ "zentao-hub": "dist/index.js"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "templates",
36
+ "README.md"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "js-yaml": "^4.1.0",
46
+ "@zentao-hub/sdk": "0.1.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/js-yaml": "^4.0.9",
50
+ "@types/node": "^20.14.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -b",
54
+ "dev": "tsc -b --watch",
55
+ "clean": "tsc -b --clean && rm -rf dist tsconfig.tsbuildinfo"
56
+ }
57
+ }
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: Pull and analyze the context of a single ZenTao bug (no worktree, no changes to any repo). Useful for sizing / scheduling / reading screenshots.
3
+ argument-hint: <bugId>
4
+ ---
5
+
6
+ # ZenTao bug analysis (read-only)
7
+
8
+ Runs only Phase 1 of /fix-bug.
9
+
10
+ ## Variables
11
+
12
+ - `$WS` = `zentao-hub workspace ${ZENTAO_PROFILE:+--profile "$ZENTAO_PROFILE"} --print-path` — the CLI handles the per-profile layout. **Only pass `--profile` when `$ZENTAO_PROFILE` is set**; when it's unset we deliberately omit the flag so the CLI falls back to the `"default"` field of `~/.zentao-hub/credentials.json` (otherwise the literal string `default` would override whatever profile the user configured as default).
13
+ Fallback when `zentao-hub` isn't on `$PATH`: `${ZENTAO_HUB:-$HOME/.zentao-hub}/<profile>`, where `<profile>` is `$ZENTAO_PROFILE` if set, else the `"default"` field of `credentials.json` (`python3 -c 'import json,os; print(json.load(open(os.path.expanduser("~/.zentao-hub/credentials.json")))["default"])' 2>/dev/null`), else literal `default`.
14
+ - `$BUG_ID` = `$1` (required)
15
+ - `$BUG_DIR` = `$WS/bugs/$BUG_ID`
16
+
17
+ ## Steps
18
+
19
+ ### 1. Prep
20
+ - `mkdir -p $WS/bugs`
21
+
22
+ ### 2. Pull context into `$BUG_DIR`
23
+ - `mkdir -p $BUG_DIR/attachments`
24
+ - `mcp__zentao__get_bug(bugId=$BUG_ID)` → write `$BUG_DIR/raw.json`, and produce a `description.md` summary (title / priority / severity / reporter / assignedTo / reproduction steps). For HTML→markdown rules, see [section 1.3 in fix-bug.md](fix-bug.md#bugsteps-html--markdown-conversion-rules).
25
+ - `mcp__zentao__get_bug_history(bugId=$BUG_ID)` → write `history.md`
26
+ - `mcp__zentao__download_bug_attachments(bugId=$BUG_ID, destDir="$BUG_DIR/attachments", includeInline=true)`. If `errors[]` is non-empty, tell the user.
27
+
28
+ ### 3. Analyze
29
+ Read every local file (use Read directly on screenshots as images). Produce:
30
+
31
+ - **Symptom**: what the user sees / repro conditions / blast radius
32
+ - **Likely root cause** (at the business-logic level)
33
+ - **Code leads**: keywords / file names / function names that future-you should grep for when actually fixing
34
+ - **Unknowns / things to clarify**: list them explicitly
35
+ - **Difficulty estimate**: a rough S / M / L plus a guess at which repos / modules will need to change
36
+
37
+ Write the analysis to `$BUG_DIR/analysis.md` and print a summary to the user.
38
+
39
+ ### 4. Wrap up
40
+ - No worktree creation, no repository changes
41
+ - Do NOT call `mcp__zentao__resolve_bug`
42
+ - Keep `$BUG_DIR` around — when you later run `/fix-bug $productId $BUG_ID` it will be reused (avoids re-downloading)
43
+
44
+ ## When to upgrade to /fix-bug
45
+
46
+ If you decide to fix it: run `/fix-bug <productId> $BUG_ID`. Phase 1 detects that `$BUG_DIR` already exists, skips the download, and continues from the analysis step.
@@ -0,0 +1,193 @@
1
+ ---
2
+ description: Pull a ZenTao bug, analyze in the workspace, confirm with the user, pick repositories, then open a worktree per repo to fix and squash-merge.
3
+ argument-hint: [productId] [bugId?]
4
+ ---
5
+
6
+ # ZenTao bug-fix flow (workspace + multi-repo + worktree)
7
+
8
+ Run in the three phases below. At every ⏸ point **stop and wait for user confirmation**. Announce every git / file write command before running it.
9
+
10
+ ## Variables
11
+
12
+ Resolve before running:
13
+
14
+ - `$WS` = `zentao-hub workspace ${ZENTAO_PROFILE:+--profile "$ZENTAO_PROFILE"} --print-path` — the CLI handles the per-profile layout. **Only pass `--profile` when `$ZENTAO_PROFILE` is set** (check with `printenv ZENTAO_PROFILE`); when it's unset we deliberately omit the flag so the CLI falls back to the `"default"` field of `~/.zentao-hub/credentials.json` (otherwise the literal string `default` would override whatever profile the user configured as default).
15
+ Fallback when `zentao-hub` isn't on `$PATH`: `${ZENTAO_HUB:-$HOME/.zentao-hub}/<profile>`, where `<profile>` is `$ZENTAO_PROFILE` if set, else the `"default"` field of `credentials.json` (`python3 -c 'import json,os; print(json.load(open(os.path.expanduser("~/.zentao-hub/credentials.json")))["default"])' 2>/dev/null`), else literal `default`.
16
+ - `$BUG_ID` = chosen bug id
17
+ - `$BUG_DIR` = `$WS/bugs/$BUG_ID`
18
+ - `$REGISTRY` = `$WS/repos.yaml`
19
+ - For each chosen repository:
20
+ - `$REPO` = repository root (absolute path)
21
+ - `$WORKTREE` = `$(dirname $REPO)/$(basename $REPO)-bug-$BUG_ID`
22
+ - `$BRANCH` = `fix/bug-$BUG_ID`
23
+ - `$MAIN` = main branch of that repo (detect via `git -C $REPO symbolic-ref --short refs/remotes/origin/HEAD`; fall back to `main` / `master` / `trunk`)
24
+
25
+ Always pass `-C <repo|worktree>` to git so the non-persistent Bash cwd never causes confusion.
26
+
27
+ ---
28
+
29
+ ## Phase 1 — Bug context (does not depend on any repository)
30
+
31
+ ### 1.1 Prepare the workspace
32
+ - `mkdir -p $WS/bugs`
33
+ - Make sure `$WS` exists; create if missing.
34
+
35
+ ### 1.2 Pick a bug
36
+ - If `$2` is not provided: `mcp__zentao__list_my_bugs(productId=$1, status=active, limit=20)`, display the top 10 sorted by `pri` ascending, and let the user pick.
37
+ - If `$2` is provided: `$BUG_ID = $2`.
38
+
39
+ ### 1.3 Pull context into `$BUG_DIR` (idempotent)
40
+
41
+ If `$BUG_DIR/raw.json` already exists (e.g. a previous `/bug-analyze` ran), tell the user and ask ⏸: reuse, or re-download? Default: reuse and skip to 1.4.
42
+
43
+ Otherwise:
44
+ - `mkdir -p $BUG_DIR/attachments`
45
+ - `mcp__zentao__get_bug(bugId=$BUG_ID)` → write `$BUG_DIR/raw.json`; also produce `$BUG_DIR/description.md` containing: title, priority, severity, reporter, assignedTo, plus a "Reproduction steps" section converted from `bug.steps` HTML using the rules below.
46
+ - `mcp__zentao__get_bug_history(bugId=$BUG_ID)` → write `$BUG_DIR/history.md` (comments and status changes ordered by time).
47
+ - `mcp__zentao__download_bug_attachments(bugId=$BUG_ID, destDir="$BUG_DIR/attachments", includeInline=true)`. Report any failed files honestly to the user.
48
+
49
+ #### `bug.steps` HTML → markdown conversion rules
50
+
51
+ Apply these rules so the result is reliably readable:
52
+
53
+ - `<img src=".../file-read-{id}.{ext}" ...>` → `![](attachments/inline/inline-N-<basename>)` (path aligned with how download_bug_attachments persists files; N follows in-text order)
54
+ - `<br>` / `<br/>` → newline `\n`
55
+ - `</p>`, `</div>`, `</li>` → newline `\n`
56
+ - `<li ...>` → start-of-line `- ` (unordered list item)
57
+ - `<li>` inside `<ol>` → start-of-line `1. `, etc. (ordered list item)
58
+ - All other HTML tags → strip
59
+ - HTML entities → decoded: `&nbsp;` → space, `&amp;` → `&`, `&lt;` → `<`, `&gt;` → `>`, `&quot;` → `"`
60
+ - 3+ consecutive newlines → collapse to 2
61
+ - Trim leading / trailing whitespace
62
+
63
+ After this, the result should read as clean markdown with no residual tags.
64
+
65
+ ### 1.4 Analyze ⏸
66
+ Read `description.md` + `history.md` + every attachment (Read screenshots directly as images). **Note: at this point you may not know yet which repository to change.** Focus the analysis on the bug itself:
67
+
68
+ - **Symptom**: what the user sees / repro conditions / blast radius
69
+ - **Likely root cause** (code-agnostic): business-logic step, data / config / API
70
+ - **Code leads**: keywords / function names / file-name hints, to help match repositories in Phase 2
71
+ - **Unknowns**: list them explicitly
72
+
73
+ Write the analysis to `$BUG_DIR/analysis.md`.
74
+
75
+ ⏸ **Ask the user**: does the analysis look right? Anything to add?
76
+
77
+ ---
78
+
79
+ ## Phase 2 — Pick repositories
80
+
81
+ ### 2.1 Look up the registry
82
+ - If `$REGISTRY` doesn't exist: either `cp $WS/repos.yaml.example $REGISTRY`, or create a minimal skeleton `{products: {}}` (the user may not have copied the example — be tolerant).
83
+ - Parse the yaml with `python3 -c` (no extra deps):
84
+ ```bash
85
+ python3 -c "
86
+ import yaml, sys
87
+ with open('$REGISTRY') as f: data = yaml.safe_load(f) or {}
88
+ entries = (data.get('products') or {}).get($1) or []
89
+ for e in entries: print(e.get('name', ''), e.get('path', ''), ','.join(e.get('tags', [])))
90
+ "
91
+ ```
92
+ - List the candidate repositories for this productId (name / path / tags).
93
+
94
+ ### 2.2 Let the user pick ⏸
95
+ - If the registry has candidates → list them and let the user pick (**multi-select** — cross-repo bugs are common, e.g. frontend + backend).
96
+ - No candidates / wants to add a new one → prompt for an absolute path.
97
+ - For each selected path:
98
+ - Verify it's a git repo: `git -C <path> rev-parse --git-dir`
99
+ - Verify the working tree is clean: `git -C <path> status --porcelain` should be empty
100
+ - If either fails, stop and let the user clean up.
101
+ - Write any newly-entered repos back to `$REGISTRY`:
102
+ ```bash
103
+ python3 << 'PYEOF'
104
+ import yaml
105
+ with open('$REGISTRY') as f: data = yaml.safe_load(f) or {}
106
+ data.setdefault('products', {}).setdefault($1, [])
107
+ # Append entries, dedup by path
108
+ existing = {e['path'] for e in data['products'][$1]}
109
+ for new_path in [...]: # user-supplied
110
+ if new_path not in existing:
111
+ data['products'][$1].append({'path': new_path})
112
+ with open('$REGISTRY', 'w') as f: yaml.safe_dump(data, f, allow_unicode=True, sort_keys=False)
113
+ PYEOF
114
+ ```
115
+
116
+ ⏸ **Confirm the repository list** before moving to Phase 3.
117
+
118
+ ---
119
+
120
+ ## Phase 3 — Worktree + fix (loop per repository)
121
+
122
+ For **each** chosen `$REPO`:
123
+
124
+ ### 3.1 Create the worktree
125
+ - `git -C $REPO fetch origin` (skip if there is no origin)
126
+ - Detect `$MAIN` and tell the user. If `$MAIN` is behind origin, suggest fast-forwarding first.
127
+ - Check whether `$BRANCH` already exists; if so, stop and ask: reuse, delete and recreate, or rename.
128
+ - `git -C $REPO worktree add -b $BRANCH $WORKTREE $MAIN`
129
+
130
+ ### 3.2 Fix plan ⏸
131
+ Based on `$BUG_DIR/analysis.md` + the code in this repo (use Grep / Read), produce a plan specific to **this repository**:
132
+
133
+ - File list to change + the approach
134
+ - Blast radius / test coverage / risk
135
+ - Cross-repo dependencies (e.g. frontend depends on a backend release first)
136
+
137
+ ⏸ **Ask the user**: does the plan look right?
138
+
139
+ ### 3.3 Apply changes (cwd = `$WORKTREE`)
140
+ - Edit / Write the changes
141
+ - Run tests and type checks (use the project's own commands)
142
+ - After tests pass: `git -C $WORKTREE add -A && git -C $WORKTREE commit -m "<wip msg>"`. Multiple commits are fine.
143
+
144
+ ### 3.4 Squash merge ⏸
145
+ - `git -C $REPO checkout $MAIN`
146
+ - `git -C $REPO merge --squash $BRANCH`
147
+ - On conflict, stop and let the user resolve manually; **do not auto-reset**.
148
+ - Draft a commit message, **show it to the user, and only after confirmation submit via heredoc**:
149
+
150
+ ```
151
+ fix: #$BUG_ID <original bug title>
152
+
153
+ <one-line root cause>
154
+ <one-line fix>
155
+
156
+ Closes ZenTao bug $BUG_ID
157
+ ```
158
+
159
+ - After committing, record the commit hash and repo name for wrap-up.
160
+
161
+ ---
162
+
163
+ ## Phase 4 — Wrap-up suggestions (**suggest only, do not auto-run**)
164
+
165
+ Summarize the commit hash for every repository:
166
+
167
+ ```
168
+ - venus-frontend: abc1234
169
+ - venus-backend: def5678
170
+ ```
171
+
172
+ Based on user preference, optionally:
173
+
174
+ - Push each repo: `git -C $REPO push origin $MAIN`
175
+ - Mark the bug resolved in ZenTao:
176
+ ```
177
+ mcp__zentao__resolve_bug(
178
+ bugId=$BUG_ID,
179
+ resolution="fixed",
180
+ comment="Multi-repo fix:\n- venus-frontend: abc1234\n- venus-backend: def5678",
181
+ resolvedBuild="...",
182
+ )
183
+ ```
184
+ - Remove each worktree: `git -C $REPO worktree remove $WORKTREE`
185
+ - Delete the fix branch: `git -C $REPO branch -D $BRANCH`
186
+ - Leave the bug context in `$BUG_DIR` (kept by default for future traceability); remove with `rm -rf $BUG_DIR` if desired.
187
+
188
+ ## Failure / cancel paths
189
+
190
+ - Phase 1/2, user decides not to fix: optionally clean up an empty `$BUG_DIR`; touch no repositories.
191
+ - Phase 3, tests cannot be made to pass in one repo: stop at that worktree, hand it back to the user; do not roll back the other repos already completed.
192
+ - pre-commit hook fails: fix the underlying problem per the hook's message, then commit again; **do not** `--amend`, do not `--no-verify`.
193
+ - squash merge conflict: stop, ask the user to resolve in the main checkout manually; once they say "continue", proceed with the commit step.
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ # Block AI signatures and validate the Conventional Commits header.
3
+ # Enable with: git config core.hooksPath .githooks
4
+ # (zentao install-hooks does this automatically)
5
+
6
+ msg_file="$1"
7
+ msg=$(cat "$msg_file")
8
+
9
+ # 1) Block AI signatures / auto-generated markers
10
+ if echo "$msg" | grep -qiE "(generated with claude|co-authored-by:[[:space:]]*claude|co-authored-by:[[:space:]]*.*noreply@anthropic|🤖|generated by ai|claude assisted|ai assisted)"; then
11
+ echo "ERROR: commit message contains an AI signature, blocked."
12
+ echo
13
+ echo "Matched fragment:"
14
+ echo "$msg" | grep -iE "(generated with claude|co-authored-by|🤖|AI|claude)" | head -3
15
+ echo
16
+ echo "Remove it and re-commit."
17
+ exit 1
18
+ fi
19
+
20
+ # 2) Conventional Commits header: <type>(<scope>)?: <subject>
21
+ first_line=$(echo "$msg" | head -n 1)
22
+
23
+ # Let Merge / Revert pass through
24
+ if echo "$first_line" | grep -qE "^(Merge|Revert)"; then
25
+ exit 0
26
+ fi
27
+
28
+ if ! echo "$first_line" | grep -qE "^(feat|fix|docs|style|refactor|test|chore|perf|build|ci)(\([a-z0-9_-]+\))?!?: .+"; then
29
+ echo "ERROR: commit header does not match Conventional Commits."
30
+ echo
31
+ echo "Current: $first_line"
32
+ echo
33
+ echo "Expected: <type>(<scope>): <subject>"
34
+ echo "Allowed types: feat, fix, docs, style, refactor, test, chore, perf, build, ci"
35
+ echo "Example: fix(auth): handle expired login token"
36
+ exit 1
37
+ fi
38
+
39
+ # 3) Optional: require every commit to carry a ZenTao bug reference [ZT#<id>].
40
+ # Disabled by default; uncomment to enforce.
41
+ # if ! echo "$msg" | grep -qE "\[ZT#[0-9]+\]"; then
42
+ # echo "ERROR: commit message is missing the ZenTao bug reference [ZT#<id>]"
43
+ # echo " Suggested placement: end of subject or in the body, e.g. fix(auth): handle expired login token [ZT#12345]"
44
+ # exit 1
45
+ # fi
46
+
47
+ exit 0
@@ -0,0 +1,86 @@
1
+ # Project AI collaboration rules
2
+
3
+ > Copied here by `zentao-hub install claude-md`, sitting next to `package.json` / `pyproject.toml` / etc.
4
+ > Claude Code reads this file on startup as the collaboration contract for this repository.
5
+ > Before relying on it, please fill in the "Code style" section with the conventions specific to this repo.
6
+
7
+ ## Bug-fixing workflow
8
+
9
+ This project fixes ZenTao bugs through the slash commands from [zentao-hub](https://github.com/devenliu/zentao-hub) (installed globally, or via a local path). **Do not commit / push manually on the main branch.**
10
+
11
+ Entry points:
12
+
13
+ - `/bug-analyze <bugId>` — analyze only, no fix. Bug context lands in `<workspace>/bugs/<id>/`, where the workspace is the per-profile dir under `$ZENTAO_HUB` (or `~/.zentao-hub`).
14
+ - `/fix-bug <productId> [bugId]` — full flow. **Automatically creates a worktree**, edits inside the worktree, then squash-merges back to the main branch.
15
+
16
+ See the [@zentao-hub/cli README](https://github.com/devenliu/zentao-hub/blob/main/packages/cli/README.md) for the full design.
17
+
18
+ ## Commit conventions (enforced by `.githooks/commit-msg`)
19
+
20
+ Follow [Conventional Commits](https://www.conventionalcommits.org/):
21
+
22
+ ```
23
+ <type>(<scope>): <subject>
24
+ ```
25
+
26
+ - **type**: `feat` / `fix` / `docs` / `style` / `refactor` / `test` / `chore` / `perf` / `build` / `ci`
27
+ - **scope**: module name (optional)
28
+ - **subject**: imperative mood, no trailing period, kept short
29
+
30
+ When fixing a bug, append a ZenTao reference in the body so it stays traceable:
31
+
32
+ ```
33
+ fix(auth): handle expired login token
34
+
35
+ Root cause: the interceptor did not handle 401.
36
+ Fix: add refresh + retry in the response interceptor.
37
+
38
+ Closes ZenTao bug 12345
39
+ ```
40
+
41
+ > If the team wants every commit to carry a `[ZT#<id>]` marker, uncomment section 3 of `.githooks/commit-msg`.
42
+
43
+ ## Hard prohibitions (the hook will block these)
44
+
45
+ - Do NOT add AI signatures to commit messages — `Co-Authored-By: Claude`, `🤖 Generated with`, or any equivalent.
46
+ - Do NOT append phrases like "Generated by AI", "Claude-assisted", "AI-assisted".
47
+ - Do NOT use `--no-verify` to bypass the hook (unless explicitly authorized with a clear reason).
48
+ - Do NOT use `--amend` on commits that have already been pushed.
49
+
50
+ ## Test and commit cadence
51
+
52
+ - Code changes must pass this repository's standard test / type-check commands (see "Test commands" below) before commit.
53
+ - One commit, one clear goal — split into multiple commits if multiple goals are in play.
54
+ - If a pre-commit hook fails: **fix the underlying problem and re-commit**, do not use `--no-verify` and do not `--amend`.
55
+
56
+ ## Test commands
57
+
58
+ > Fill in based on this repository. Examples:
59
+
60
+ ```bash
61
+ # Node.js
62
+ npm test
63
+ npm run typecheck
64
+
65
+ # Python
66
+ pytest
67
+ mypy .
68
+
69
+ # Go
70
+ go test ./...
71
+
72
+ # Rust
73
+ cargo test
74
+ ```
75
+
76
+ ## Code style
77
+
78
+ > Fill in based on this repository. Typical contents:
79
+ >
80
+ > - Location and conventions for ESLint / Prettier (or equivalent)
81
+ > - Naming conventions (camelCase / snake_case / package structure)
82
+ > - Test requirements (coverage thresholds, required test types)
83
+ > - Module boundaries and dependency-injection conventions
84
+ > - i18n and error-handling style
85
+
86
+ (Delete the `>` quote block and replace with the real rules for this repo.)
@@ -0,0 +1,26 @@
1
+ # zentao-hub repository registry — example
2
+ #
3
+ # Real file path: <HUB>/repos.yaml (gitignored).
4
+ # Auto-created on the first /fix-bug run if missing, then appended as the user adds entries.
5
+ # You can also edit it by hand — the structure is intentionally simple.
6
+ #
7
+ # Top level is productId (numeric) → list of repositories backing that product.
8
+ # Per-entry fields:
9
+ # path (required) — absolute path to the repository root
10
+ # name (optional) — friendly name for the list UI; defaults to basename(path)
11
+ # tags (optional) — array of tags for matching bug content (frontend / backend / db / ...)
12
+
13
+ products:
14
+ # Example: Venus2.0 (productId comes from list_products)
15
+ 2:
16
+ - name: venus-frontend
17
+ path: /Users/you/projects/venus-fe
18
+ tags: [frontend, ui, electron]
19
+ - name: venus-backend
20
+ path: /Users/you/projects/venus-be
21
+ tags: [backend, api, python]
22
+
23
+ # Example: Guochuang
24
+ 20:
25
+ - path: /Users/you/projects/guochuang
26
+ tags: [fullstack]