chief-clancy 0.2.0 → 0.3.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 (143) hide show
  1. package/README.md +13 -24
  2. package/dist/installer/file-ops/file-ops.d.ts +32 -0
  3. package/dist/installer/file-ops/file-ops.d.ts.map +1 -0
  4. package/dist/installer/file-ops/file-ops.js +58 -0
  5. package/dist/installer/file-ops/file-ops.js.map +1 -0
  6. package/dist/installer/hook-installer/hook-installer.d.ts +29 -0
  7. package/dist/installer/hook-installer/hook-installer.d.ts.map +1 -0
  8. package/dist/installer/hook-installer/hook-installer.js +96 -0
  9. package/dist/installer/hook-installer/hook-installer.js.map +1 -0
  10. package/dist/installer/install.d.ts +3 -0
  11. package/dist/installer/install.d.ts.map +1 -0
  12. package/dist/installer/install.js +227 -0
  13. package/dist/installer/install.js.map +1 -0
  14. package/dist/installer/manifest/manifest.d.ts +41 -0
  15. package/dist/installer/manifest/manifest.d.ts.map +1 -0
  16. package/dist/installer/manifest/manifest.js +97 -0
  17. package/dist/installer/manifest/manifest.js.map +1 -0
  18. package/dist/installer/prompts/prompts.d.ts +33 -0
  19. package/dist/installer/prompts/prompts.d.ts.map +1 -0
  20. package/dist/installer/prompts/prompts.js +55 -0
  21. package/dist/installer/prompts/prompts.js.map +1 -0
  22. package/dist/schemas/env.d.ts +75 -0
  23. package/dist/schemas/env.d.ts.map +1 -0
  24. package/dist/schemas/env.js +40 -0
  25. package/dist/schemas/env.js.map +1 -0
  26. package/dist/schemas/github.d.ts +27 -0
  27. package/dist/schemas/github.d.ts.map +1 -0
  28. package/dist/schemas/github.js +17 -0
  29. package/dist/schemas/github.js.map +1 -0
  30. package/dist/schemas/index.d.ts +9 -0
  31. package/dist/schemas/index.d.ts.map +1 -0
  32. package/dist/schemas/index.js +5 -0
  33. package/dist/schemas/index.js.map +1 -0
  34. package/dist/schemas/jira.d.ts +37 -0
  35. package/dist/schemas/jira.d.ts.map +1 -0
  36. package/dist/schemas/jira.js +37 -0
  37. package/dist/schemas/jira.js.map +1 -0
  38. package/dist/schemas/linear.d.ts +67 -0
  39. package/dist/schemas/linear.d.ts.map +1 -0
  40. package/dist/schemas/linear.js +50 -0
  41. package/dist/schemas/linear.js.map +1 -0
  42. package/dist/scripts/afk/afk.d.ts +21 -0
  43. package/dist/scripts/afk/afk.d.ts.map +1 -0
  44. package/dist/scripts/afk/afk.js +116 -0
  45. package/dist/scripts/afk/afk.js.map +1 -0
  46. package/dist/scripts/board/github/github.d.ts +56 -0
  47. package/dist/scripts/board/github/github.d.ts.map +1 -0
  48. package/dist/scripts/board/github/github.js +142 -0
  49. package/dist/scripts/board/github/github.js.map +1 -0
  50. package/dist/scripts/board/jira/jira.d.ts +90 -0
  51. package/dist/scripts/board/jira/jira.d.ts.map +1 -0
  52. package/dist/scripts/board/jira/jira.js +251 -0
  53. package/dist/scripts/board/jira/jira.js.map +1 -0
  54. package/dist/scripts/board/linear/linear.d.ts +85 -0
  55. package/dist/scripts/board/linear/linear.d.ts.map +1 -0
  56. package/dist/scripts/board/linear/linear.js +209 -0
  57. package/dist/scripts/board/linear/linear.js.map +1 -0
  58. package/dist/scripts/once/once.d.ts +12 -0
  59. package/dist/scripts/once/once.d.ts.map +1 -0
  60. package/dist/scripts/once/once.js +323 -0
  61. package/dist/scripts/once/once.js.map +1 -0
  62. package/dist/scripts/shared/branch/branch.d.ts +50 -0
  63. package/dist/scripts/shared/branch/branch.d.ts.map +1 -0
  64. package/dist/scripts/shared/branch/branch.js +61 -0
  65. package/dist/scripts/shared/branch/branch.js.map +1 -0
  66. package/dist/scripts/shared/claude-cli/claude-cli.d.ts +17 -0
  67. package/dist/scripts/shared/claude-cli/claude-cli.d.ts.map +1 -0
  68. package/dist/scripts/shared/claude-cli/claude-cli.js +35 -0
  69. package/dist/scripts/shared/claude-cli/claude-cli.js.map +1 -0
  70. package/dist/scripts/shared/env-parser/env-parser.d.ts +30 -0
  71. package/dist/scripts/shared/env-parser/env-parser.d.ts.map +1 -0
  72. package/dist/scripts/shared/env-parser/env-parser.js +64 -0
  73. package/dist/scripts/shared/env-parser/env-parser.js.map +1 -0
  74. package/dist/scripts/shared/env-schema/env-schema.d.ts +27 -0
  75. package/dist/scripts/shared/env-schema/env-schema.d.ts.map +1 -0
  76. package/dist/scripts/shared/env-schema/env-schema.js +46 -0
  77. package/dist/scripts/shared/env-schema/env-schema.js.map +1 -0
  78. package/dist/scripts/shared/git-ops/git-ops.d.ts +52 -0
  79. package/dist/scripts/shared/git-ops/git-ops.d.ts.map +1 -0
  80. package/dist/scripts/shared/git-ops/git-ops.js +107 -0
  81. package/dist/scripts/shared/git-ops/git-ops.js.map +1 -0
  82. package/dist/scripts/shared/http/http.d.ts +52 -0
  83. package/dist/scripts/shared/http/http.d.ts.map +1 -0
  84. package/dist/scripts/shared/http/http.js +74 -0
  85. package/dist/scripts/shared/http/http.js.map +1 -0
  86. package/dist/scripts/shared/notify/notify.d.ts +46 -0
  87. package/dist/scripts/shared/notify/notify.d.ts.map +1 -0
  88. package/dist/scripts/shared/notify/notify.js +88 -0
  89. package/dist/scripts/shared/notify/notify.js.map +1 -0
  90. package/dist/scripts/shared/preflight/preflight.d.ts +40 -0
  91. package/dist/scripts/shared/preflight/preflight.d.ts.map +1 -0
  92. package/dist/scripts/shared/preflight/preflight.js +84 -0
  93. package/dist/scripts/shared/preflight/preflight.js.map +1 -0
  94. package/dist/scripts/shared/progress/progress.d.ts +25 -0
  95. package/dist/scripts/shared/progress/progress.d.ts.map +1 -0
  96. package/dist/scripts/shared/progress/progress.js +46 -0
  97. package/dist/scripts/shared/progress/progress.js.map +1 -0
  98. package/dist/scripts/shared/prompt/prompt.d.ts +38 -0
  99. package/dist/scripts/shared/prompt/prompt.d.ts.map +1 -0
  100. package/dist/scripts/shared/prompt/prompt.js +77 -0
  101. package/dist/scripts/shared/prompt/prompt.js.map +1 -0
  102. package/dist/types/board.d.ts +13 -0
  103. package/dist/types/board.d.ts.map +1 -0
  104. package/dist/types/board.js +5 -0
  105. package/dist/types/board.js.map +1 -0
  106. package/dist/types/index.d.ts +3 -0
  107. package/dist/types/index.d.ts.map +1 -0
  108. package/dist/types/index.js +2 -0
  109. package/dist/types/index.js.map +1 -0
  110. package/dist/utils/ansi/ansi.d.ts +55 -0
  111. package/dist/utils/ansi/ansi.d.ts.map +1 -0
  112. package/dist/utils/ansi/ansi.js +55 -0
  113. package/dist/utils/ansi/ansi.js.map +1 -0
  114. package/dist/utils/index.d.ts +3 -0
  115. package/dist/utils/index.d.ts.map +1 -0
  116. package/dist/utils/index.js +3 -0
  117. package/dist/utils/index.js.map +1 -0
  118. package/dist/utils/parse-json/parse-json.d.ts +20 -0
  119. package/dist/utils/parse-json/parse-json.d.ts.map +1 -0
  120. package/dist/utils/parse-json/parse-json.js +27 -0
  121. package/dist/utils/parse-json/parse-json.js.map +1 -0
  122. package/hooks/clancy-check-update.js +2 -2
  123. package/hooks/clancy-credential-guard.js +8 -1
  124. package/package.json +52 -8
  125. package/registry/boards.json +3 -6
  126. package/src/templates/CLAUDE.md +1 -1
  127. package/src/workflows/doctor.md +32 -23
  128. package/src/workflows/init.md +88 -19
  129. package/src/workflows/logs.md +13 -6
  130. package/src/workflows/map-codebase.md +17 -16
  131. package/src/workflows/once.md +22 -12
  132. package/src/workflows/review.md +40 -27
  133. package/src/workflows/run.md +20 -12
  134. package/src/workflows/scaffold.md +12 -1023
  135. package/src/workflows/settings.md +9 -6
  136. package/src/workflows/status.md +17 -8
  137. package/src/workflows/uninstall.md +11 -6
  138. package/src/workflows/update.md +13 -11
  139. package/bin/install.js +0 -362
  140. package/src/templates/scripts/clancy-afk.sh +0 -111
  141. package/src/templates/scripts/clancy-once-github.sh +0 -249
  142. package/src/templates/scripts/clancy-once-linear.sh +0 -320
  143. package/src/templates/scripts/clancy-once.sh +0 -322
@@ -34,7 +34,10 @@ Source `.clancy/.env` silently. Detect which board is configured:
34
34
  Show all current values. Board-specific settings only appear when that board is configured.
35
35
 
36
36
  ```
37
- Clancy settings .clancy/.env
37
+ 🚨 Clancy — Settings
38
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39
+
40
+ "Welcome to headquarters." — .clancy/.env
38
41
 
39
42
  General
40
43
  [1] Max iterations {MAX_ITERATIONS:-5} tickets per /clancy:run session
@@ -72,7 +75,7 @@ Number each option sequentially. Show only the board-specific section that match
72
75
 
73
76
  ## Step 4 — Handle each selection
74
77
 
75
- After the user picks a number, handle it as below. After saving, print `✓ Saved.` and loop back to Step 3 to show the updated menu.
78
+ After the user picks a number, handle it as below. After saving, print `✅ Saved.` and loop back to Step 3 to show the updated menu.
76
79
 
77
80
  ---
78
81
 
@@ -344,7 +347,7 @@ Verify the new credentials before making any changes — same checks as the init
344
347
 
345
348
  ```
346
349
  Verifying...
347
- Connected — {board-specific confirmation, e.g. "PROJ reachable" / "acme/my-app found" / "Linear authenticated"}
350
+ Connected — {board-specific confirmation, e.g. "PROJ reachable" / "acme/my-app found" / "Linear authenticated"}
348
351
  ```
349
352
 
350
353
  If verification fails, tell the user clearly and offer:
@@ -379,12 +382,12 @@ If no: print `Cancelled. No changes made.` and loop back to the menu.
379
382
  - Linear: `LINEAR_API_KEY`, `LINEAR_TEAM_ID`
380
383
  2. Write the new board credentials to `.clancy/.env`
381
384
  3. If switching to Jira: also ask the status filter question (same as init Q3) and write `CLANCY_JQL_STATUS` to `.clancy/.env`
382
- 4. Write the correct `clancy-once.sh` variant for the new board to `.clancy/clancy-once.sh` — same script content as init Step 4 uses (Jira `clancy-once.sh`, GitHub `clancy-once-github.sh`, Linear → `clancy-once-linear.sh`). Make it executable: `chmod +x .clancy/clancy-once.sh`
385
+ 4. No script replacement needed the JS shims are board-agnostic (board detection happens at runtime from `.clancy/.env`)
383
386
 
384
387
  Print:
385
388
 
386
389
  ```
387
- Switched to {new board}.
390
+ Switched to {new board}. "New beat, same Chief."
388
391
  ```
389
392
 
390
393
  Then loop back to the main settings menu.
@@ -434,7 +437,7 @@ When selected:
434
437
  }
435
438
  ```
436
439
 
437
- 3. Print: `✓ Defaults saved to ~/.clancy/defaults.json — new projects will inherit these settings.`
440
+ 3. Print: `✅ Defaults saved to ~/.clancy/defaults.json — new projects will inherit these settings.`
438
441
 
439
442
  4. Loop back to the settings menu.
440
443
 
@@ -23,7 +23,7 @@ Detect board from `.clancy/.env`:
23
23
 
24
24
  **Jira:**
25
25
 
26
- Build the JQL string first using the same clauses as `clancy-once.sh`:
26
+ Build the JQL string first using the same clauses as the once-runner:
27
27
  - Sprint clause: include `AND sprint in openSprints()` if `CLANCY_JQL_SPRINT` is set
28
28
  - Label clause: include `AND labels = "$CLANCY_LABEL"` if `CLANCY_LABEL` is set
29
29
  - `CLANCY_JQL_STATUS` defaults to `To Do` if not set
@@ -76,7 +76,10 @@ query {
76
76
 
77
77
  If tickets found, display:
78
78
  ```
79
- Next up for Clancy:
79
+ 🚨 Clancy Status
80
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
81
+
82
+ Next up:
80
83
 
81
84
  1. [{TICKET-KEY}] {Summary}
82
85
  Epic: {epic key} — {epic title}
@@ -90,19 +93,25 @@ Next up for Clancy:
90
93
  Epic: {epic key} — {epic title}
91
94
  Status: {status}
92
95
 
93
- Run /clancy:once to pick up the first ticket.
94
- Run /clancy:run to process all tickets in the queue.
96
+ "Let me check the dispatch..." — Run /clancy:once to pick up #1, or /clancy:run to process the queue.
95
97
  ```
96
98
 
97
99
  If no tickets found:
98
100
  ```
99
- No tickets found in the current queue. Check your board or run /clancy:init
100
- to verify your configuration.
101
+ 🚨 Clancy Status
102
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
103
+
104
+ No tickets found in the current queue.
105
+
106
+ "Quiet. Too quiet." — Check your board or run /clancy:init to verify your config.
101
107
  ```
102
108
 
103
109
  If API call fails, show the error clearly:
104
110
  ```
105
- Board API error: {error message}
111
+ 🚨 Clancy Status
112
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
113
+
114
+ ❌ Board API error: {error message}
106
115
 
107
116
  Tips:
108
117
  - Check your credentials in .clancy/.env
@@ -117,4 +126,4 @@ Tips:
117
126
  - Show up to 3 tickets. If only 1 or 2 are available, show those.
118
127
  - Omit "Epic:" line if no epic/parent data is present for that ticket.
119
128
  - This command is strictly read-only. No git ops, no file writes, no Claude invocation for analysis.
120
- - The query used here must be identical to the one used by `clancy-once.sh` — what status shows is exactly what run would pick up.
129
+ - The query used here must be identical to the one used by the once-runner — what status shows is exactly what run would pick up.
@@ -29,6 +29,9 @@ Check both locations silently. Each install has two parts — commands and workf
29
29
  Show exactly this message, filling in the detected location:
30
30
 
31
31
  ```
32
+ 🚨 Clancy — Uninstall
33
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34
+
32
35
  This will remove Clancy's slash commands from [location].
33
36
  Your .clancy/ folder will not be touched.
34
37
  Continue? (yes / no)
@@ -45,7 +48,7 @@ Delete both the commands directory and the workflows directory for the chosen lo
45
48
  - Project-local: `.claude/commands/clancy/` and `.claude/clancy/`
46
49
  - Global: `~/.claude/commands/clancy/` and `~/.claude/clancy/`
47
50
 
48
- Print: `✓ Clancy commands removed from [location].`
51
+ Print: `✅ Clancy commands removed from [location].`
49
52
 
50
53
  ### Step 2b — Remove hooks
51
54
 
@@ -79,7 +82,7 @@ Read the full file content. Determine whether Clancy created the file or appende
79
82
  - **Clancy created it** (the file contains only whitespace outside the markers — no meaningful content before `<!-- clancy:start -->` or after `<!-- clancy:end -->`): delete the entire file.
80
83
  - **Clancy appended to an existing file** (there is meaningful content outside the markers): remove everything from `<!-- clancy:start -->` through `<!-- clancy:end -->` (inclusive), plus any blank lines immediately before the start marker that were added as spacing. Write the cleaned file back.
81
84
 
82
- Print `✓ CLAUDE.md cleaned up.` (or `✓ CLAUDE.md removed.` if deleted).
85
+ Print `✅ CLAUDE.md cleaned up.` (or `✅ CLAUDE.md removed.` if deleted).
83
86
 
84
87
  **If no markers found:** skip — Clancy didn't modify this file.
85
88
 
@@ -97,7 +100,7 @@ If it does, check whether it contains the Clancy entries (`# Clancy credentials`
97
100
 
98
101
  If the file is now empty (or contains only whitespace) after removal, delete it entirely — Clancy created it during init.
99
102
 
100
- Print `✓ .gitignore cleaned up.` (or `✓ .gitignore removed.` if deleted).
103
+ Print `✅ .gitignore cleaned up.` (or `✅ .gitignore removed.` if deleted).
101
104
 
102
105
  **If not found:** skip — Clancy didn't modify this file.
103
106
 
@@ -116,8 +119,8 @@ If it does, ask separately:
116
119
  Remove it too? This cannot be undone. (yes / no)
117
120
  ```
118
121
 
119
- - `no` → print " .clancy/ kept — your docs and progress log are safe."
120
- - `yes` → delete `.clancy/` and print " .clancy/ removed."
122
+ - `no` → print " .clancy/ kept — your docs and progress log are safe."
123
+ - `yes` → delete `.clancy/` and print " .clancy/ removed."
121
124
 
122
125
  If `.clancy/` does not exist, skip this step entirely.
123
126
 
@@ -126,7 +129,9 @@ If `.clancy/` does not exist, skip this step entirely.
126
129
  ## Step 6 — Final message
127
130
 
128
131
  ```
129
- Clancy uninstalled. To reinstall: npx chief-clancy
132
+ Clancy uninstalled.
133
+
134
+ "You have the right to remain silent... goodbye, Clancy." — To reinstall: npx chief-clancy
130
135
  ```
131
136
 
132
137
  ---
@@ -73,12 +73,13 @@ Compare installed vs latest:
73
73
 
74
74
  **If installed == latest:**
75
75
  ```
76
- ## Clancy Update
76
+ 🚨 Clancy Update
77
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77
78
 
78
79
  **Installed:** X.Y.Z
79
80
  **Latest:** X.Y.Z
80
81
 
81
- You're already on the latest version.
82
+ You're already on the latest version. "Nothing to see here, folks."
82
83
  ```
83
84
 
84
85
  Exit.
@@ -92,7 +93,8 @@ curl -s https://raw.githubusercontent.com/Pushedskydiver/clancy/main/CHANGELOG.m
92
93
  Extract only the entries between the installed version and the latest version. Display:
93
94
 
94
95
  ```
95
- ## Clancy Update Available
96
+ 🚨 Clancy Update
97
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
96
98
 
97
99
  **Installed:** {installed}
98
100
  **Latest:** {latest}
@@ -112,10 +114,10 @@ If you've modified any Clancy files directly, they'll be automatically backed up
112
114
  to `.claude/clancy/local-patches/` before overwriting.
113
115
 
114
116
  Your project files are preserved:
115
- - `.clancy/` project folder (scripts, docs, .env, progress log)
116
- - `CLAUDE.md`
117
- - Custom commands not in `commands/clancy/`
118
- - Custom hooks
117
+ - `.clancy/` project folder (scripts, docs, .env, progress log)
118
+ - `CLAUDE.md`
119
+ - Custom commands not in `commands/clancy/`
120
+ - Custom hooks
119
121
  ```
120
122
 
121
123
  Ask the user: **"Proceed with update?"** with options:
@@ -137,13 +139,13 @@ npx -y chief-clancy@latest
137
139
  The installer auto-detects whether to install globally or locally based on the existing install.
138
140
 
139
141
  This only touches `.claude/commands/clancy/` and `.claude/clancy/workflows/`. It never modifies:
140
- - `.clancy/clancy-once.sh` or `.clancy/clancy-afk.sh` — shell scripts
142
+ - `.clancy/clancy-once.js` or `.clancy/clancy-afk.js` — JS shims (these import from the installed chief-clancy package, so updating the package automatically updates the behavior)
141
143
  - `.clancy/docs/` — codebase documentation
142
144
  - `.clancy/progress.txt` — progress log
143
145
  - `.clancy/.env` — credentials
144
146
  - `CLAUDE.md`
145
147
 
146
- **To update the shell scripts**, re-run `/clancy:init` — it will detect the existing setup and re-scaffold the scripts without asking for credentials again.
148
+ **To re-scaffold the JS shims**, re-run `/clancy:init` — it will detect the existing setup and re-create `.clancy/clancy-once.js` and `.clancy/clancy-afk.js` without asking for credentials again.
147
149
 
148
150
  ---
149
151
 
@@ -160,10 +162,10 @@ Display completion message:
160
162
 
161
163
  ```
162
164
  ╔═══════════════════════════════════════════════════════════╗
163
- ║ Clancy Updated: v{old} → v{new}
165
+ Clancy Updated: v{old} → v{new}
164
166
  ╚═══════════════════════════════════════════════════════════╝
165
167
 
166
- ⚠️ Restart Claude Code to pick up the new commands.
168
+ "New badge, same Chief." — Restart Claude Code to pick up the new commands.
167
169
 
168
170
  View full changelog: github.com/Pushedskydiver/clancy/blob/main/CHANGELOG.md
169
171
  ```
package/bin/install.js DELETED
@@ -1,362 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const readline = require('readline');
7
-
8
- const PKG = require('../package.json');
9
- const COMMANDS_SRC = path.join(__dirname, '..', 'src', 'commands');
10
- const WORKFLOWS_SRC = path.join(__dirname, '..', 'src', 'workflows');
11
- const HOOKS_SRC = path.join(__dirname, '..', 'hooks');
12
-
13
- const homeDir = process.env.HOME || process.env.USERPROFILE;
14
- if (!homeDir) {
15
- process.stderr.write('\x1b[31m\n Error: HOME or USERPROFILE environment variable is not set.\x1b[0m\n');
16
- process.exit(1);
17
- }
18
-
19
- const GLOBAL_DEST = path.join(homeDir, '.claude', 'commands', 'clancy');
20
- const LOCAL_DEST = path.join(process.cwd(), '.claude', 'commands', 'clancy');
21
-
22
- // Workflows live outside commands/ so Claude Code doesn't expose them as slash commands
23
- const GLOBAL_WORKFLOWS_DEST = path.join(homeDir, '.claude', 'clancy', 'workflows');
24
- const LOCAL_WORKFLOWS_DEST = path.join(process.cwd(), '.claude', 'clancy', 'workflows');
25
-
26
- // ANSI helpers
27
- const dim = s => `\x1b[2m${s}\x1b[0m`;
28
- const bold = s => `\x1b[1m${s}\x1b[0m`;
29
- const blue = s => `\x1b[1;34m${s}\x1b[0m`;
30
- const cyan = s => `\x1b[36m${s}\x1b[0m`;
31
- const green = s => `\x1b[32m${s}\x1b[0m`;
32
- const red = s => `\x1b[31m${s}\x1b[0m`;
33
-
34
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
35
- // Ensure readline is always closed — process.exit() doesn't trigger finally blocks
36
- process.on('exit', () => rl.close());
37
-
38
- function ask(label) {
39
- return new Promise(resolve => rl.question(label, resolve));
40
- }
41
-
42
- async function choose(question, options, defaultChoice = 1) {
43
- console.log('');
44
- console.log(blue(question));
45
- console.log('');
46
- options.forEach((opt, i) => console.log(` ${i + 1}) ${opt}`));
47
- console.log('');
48
- const raw = await ask(cyan(`Choice [${defaultChoice}]: `));
49
- return raw.trim() || String(defaultChoice);
50
- }
51
-
52
- const crypto = require('crypto');
53
-
54
- function fileHash(filePath) {
55
- const content = fs.readFileSync(filePath);
56
- return crypto.createHash('sha256').digest('hex', content);
57
- }
58
-
59
- function copyDir(src, dest) {
60
- // Use lstatSync (not statSync) to detect symlinks — statSync follows them and misreports
61
- if (fs.existsSync(dest)) {
62
- const stat = fs.lstatSync(dest);
63
- if (stat.isSymbolicLink()) {
64
- throw new Error(`${dest} is a symlink. Remove it first before installing.`);
65
- }
66
- }
67
- fs.mkdirSync(dest, { recursive: true });
68
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
69
- const s = path.join(src, entry.name);
70
- const d = path.join(dest, entry.name);
71
- entry.isDirectory() ? copyDir(s, d) : fs.copyFileSync(s, d);
72
- }
73
- }
74
-
75
- /**
76
- * Build a manifest of installed files with SHA-256 hashes.
77
- * Format: { "relative/path.md": "<sha256>", ... }
78
- */
79
- function buildManifest(baseDir) {
80
- const manifest = {};
81
- function walk(dir, prefix) {
82
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
83
- const full = path.join(dir, entry.name);
84
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
85
- if (entry.isDirectory()) {
86
- walk(full, rel);
87
- } else {
88
- const content = fs.readFileSync(full);
89
- manifest[rel] = crypto.createHash('sha256').update(content).digest('hex');
90
- }
91
- }
92
- }
93
- walk(baseDir, '');
94
- return manifest;
95
- }
96
-
97
- /**
98
- * Detect files modified by the user since last install by comparing
99
- * current file hashes against the stored manifest. Returns array of
100
- * { rel, absPath } for modified files.
101
- */
102
- function detectModifiedFiles(baseDir, manifestPath) {
103
- if (!fs.existsSync(manifestPath)) return [];
104
- let manifest;
105
- try {
106
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
107
- } catch { return []; }
108
-
109
- const modified = [];
110
- for (const [rel, hash] of Object.entries(manifest)) {
111
- const absPath = path.join(baseDir, rel);
112
- if (!fs.existsSync(absPath)) continue;
113
- const content = fs.readFileSync(absPath);
114
- const currentHash = crypto.createHash('sha256').update(content).digest('hex');
115
- if (currentHash !== hash) {
116
- modified.push({ rel, absPath });
117
- }
118
- }
119
- return modified;
120
- }
121
-
122
- /**
123
- * Back up modified files to a patches directory alongside the install.
124
- * Returns the backup directory path if any files were backed up.
125
- */
126
- function backupModifiedFiles(modified, patchesDir) {
127
- if (modified.length === 0) return null;
128
- fs.mkdirSync(patchesDir, { recursive: true });
129
- for (const { rel, absPath } of modified) {
130
- const backupPath = path.join(patchesDir, rel);
131
- fs.mkdirSync(path.dirname(backupPath), { recursive: true });
132
- fs.copyFileSync(absPath, backupPath);
133
- }
134
- // Write metadata so /clancy:update workflow knows what was backed up
135
- fs.writeFileSync(
136
- path.join(patchesDir, 'backup-meta.json'),
137
- JSON.stringify({ backed_up: modified.map(m => m.rel), date: new Date().toISOString() }, null, 2)
138
- );
139
- return patchesDir;
140
- }
141
-
142
- async function main() {
143
- console.log('');
144
- console.log(blue(' ██████╗██╗ █████╗ ███╗ ██╗ ██████╗██╗ ██╗'));
145
- console.log(blue(' ██╔════╝██║ ██╔══██╗████╗ ██║██╔════╝╚██╗ ██╔╝'));
146
- console.log(blue(' ██║ ██║ ███████║██╔██╗ ██║██║ ╚████╔╝ '));
147
- console.log(blue(' ██║ ██║ ██╔══██║██║╚██╗██║██║ ╚██╔╝ '));
148
- console.log(blue(' ╚██████╗███████╗██║ ██║██║ ╚████║╚██████╗ ██║ '));
149
- console.log(blue(' ╚═════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ '));
150
- console.log('');
151
- console.log(' ' + bold(`v${PKG.version}`) + dim(' Autonomous, board-driven development for Claude Code.'));
152
- console.log(dim(' Named after Chief Clancy Wiggum. Built on the Ralph technique by Geoffrey Huntley.'));
153
-
154
- const installChoice = await choose(
155
- 'Where would you like to install?',
156
- [
157
- `Global ${dim('(~/.claude)')} — available in all projects`,
158
- `Local ${dim('(./.claude)')} — this project only`,
159
- ]
160
- );
161
-
162
- let dest, workflowsDest;
163
- if (installChoice === '1' || installChoice.toLowerCase() === 'global') {
164
- dest = GLOBAL_DEST;
165
- workflowsDest = GLOBAL_WORKFLOWS_DEST;
166
- } else if (installChoice === '2' || installChoice.toLowerCase() === 'local') {
167
- dest = LOCAL_DEST;
168
- workflowsDest = LOCAL_WORKFLOWS_DEST;
169
- } else {
170
- console.log(red('\n Invalid choice. Run npx chief-clancy again and enter 1 or 2.'));
171
- rl.close();
172
- process.exit(1);
173
- }
174
-
175
- // Validate source directories — guards against corrupted npm package
176
- if (!fs.existsSync(COMMANDS_SRC)) {
177
- console.error(red(`\n Error: Source not found: ${COMMANDS_SRC}`));
178
- console.error(red(' The npm package may be corrupted. Try: npm cache clean --force'));
179
- rl.close();
180
- process.exit(1);
181
- }
182
- if (!fs.existsSync(WORKFLOWS_SRC)) {
183
- console.error(red(`\n Error: Source not found: ${WORKFLOWS_SRC}`));
184
- console.error(red(' The npm package may be corrupted. Try: npm cache clean --force'));
185
- rl.close();
186
- process.exit(1);
187
- }
188
-
189
- console.log('');
190
- console.log(dim(` Installing to: ${dest}`));
191
-
192
- try {
193
- // Determine manifest and patches paths (sibling to commands dir)
194
- const claudeDir = path.dirname(path.dirname(dest)); // .claude/ (parent of commands/)
195
- const manifestPath = path.join(claudeDir, 'clancy', 'manifest.json');
196
- const patchesDir = path.join(claudeDir, 'clancy', 'local-patches');
197
-
198
- if (fs.existsSync(dest) || fs.existsSync(workflowsDest)) {
199
- console.log('');
200
-
201
- // Detect user-modified files before overwriting
202
- const modified = detectModifiedFiles(dest, manifestPath);
203
- const modifiedWorkflows = detectModifiedFiles(workflowsDest, manifestPath.replace('manifest.json', 'workflows-manifest.json'));
204
- const allModified = [...modified, ...modifiedWorkflows];
205
-
206
- if (allModified.length > 0) {
207
- console.log(blue(' Modified files detected:'));
208
- for (const { rel } of allModified) {
209
- console.log(` ${dim('•')} ${rel}`);
210
- }
211
- console.log('');
212
- console.log(dim(' These will be backed up to .claude/clancy/local-patches/'));
213
- console.log(dim(' before overwriting. You can reapply them after the update.'));
214
- console.log('');
215
- }
216
-
217
- const overwrite = await ask(blue(` Commands already exist at ${dest}. Overwrite? [y/N] `));
218
- if (!overwrite.trim().toLowerCase().startsWith('y')) {
219
- console.log('\n Aborted. No files changed.');
220
- rl.close();
221
- process.exit(0);
222
- }
223
-
224
- // Back up modified files before overwriting
225
- if (allModified.length > 0) {
226
- backupModifiedFiles(allModified, patchesDir);
227
- console.log(green(`\n ✓ ${allModified.length} modified file(s) backed up to local-patches/`));
228
- }
229
- }
230
-
231
- copyDir(COMMANDS_SRC, dest);
232
- copyDir(WORKFLOWS_SRC, workflowsDest);
233
-
234
- // For global installs, @-file references in command files resolve relative to the
235
- // project root — not ~/.claude/ — so the workflow files won't be found at runtime.
236
- // Fix: inline the workflow content directly into the installed command files.
237
- if (dest === GLOBAL_DEST) {
238
- const WORKFLOW_REF = /^@\.claude\/clancy\/workflows\/(.+\.md)$/m;
239
- for (const file of fs.readdirSync(dest)) {
240
- if (!file.endsWith('.md')) continue;
241
- const cmdPath = path.join(dest, file);
242
- const content = fs.readFileSync(cmdPath, 'utf8');
243
- const match = content.match(WORKFLOW_REF);
244
- if (!match) continue;
245
- const workflowFile = path.join(workflowsDest, match[1]);
246
- if (!fs.existsSync(workflowFile)) continue;
247
- const workflowContent = fs.readFileSync(workflowFile, 'utf8');
248
- fs.writeFileSync(cmdPath, content.replace(match[0], workflowContent));
249
- }
250
- }
251
-
252
- // Write VERSION file so /clancy:doctor and /clancy:update can read the installed version
253
- fs.writeFileSync(path.join(dest, 'VERSION'), PKG.version);
254
-
255
- // Write manifests so future updates can detect user-modified files
256
- fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
257
- fs.writeFileSync(manifestPath, JSON.stringify(buildManifest(dest), null, 2));
258
- fs.writeFileSync(
259
- manifestPath.replace('manifest.json', 'workflows-manifest.json'),
260
- JSON.stringify(buildManifest(workflowsDest), null, 2)
261
- );
262
-
263
- // Install hooks and register them in Claude settings.json
264
- const claudeConfigDir = dest === GLOBAL_DEST
265
- ? path.join(homeDir, '.claude')
266
- : path.join(process.cwd(), '.claude');
267
- const hooksInstallDir = path.join(claudeConfigDir, 'hooks');
268
- const settingsFile = path.join(claudeConfigDir, 'settings.json');
269
-
270
- const hookFiles = [
271
- 'clancy-check-update.js',
272
- 'clancy-statusline.js',
273
- 'clancy-context-monitor.js',
274
- 'clancy-credential-guard.js',
275
- ];
276
-
277
- try {
278
- fs.mkdirSync(hooksInstallDir, { recursive: true });
279
- for (const f of hookFiles) {
280
- fs.copyFileSync(path.join(HOOKS_SRC, f), path.join(hooksInstallDir, f));
281
- }
282
- // Force CommonJS resolution for hook files — projects with "type":"module"
283
- // in their package.json would otherwise treat .js files as ESM, breaking require().
284
- fs.writeFileSync(
285
- path.join(hooksInstallDir, 'package.json'),
286
- JSON.stringify({ type: 'commonjs' }, null, 2) + '\n'
287
- );
288
-
289
- // Merge hooks into settings.json without clobbering existing config
290
- let settings = {};
291
- if (fs.existsSync(settingsFile)) {
292
- try { settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); } catch {}
293
- }
294
- if (!settings.hooks) settings.hooks = {};
295
-
296
- // Helper: add a hook command to an event array if not already present
297
- function registerHook(event, command) {
298
- if (!settings.hooks[event]) settings.hooks[event] = [];
299
- const already = settings.hooks[event].some(
300
- h => h.hooks && h.hooks.some(hh => hh.command === command)
301
- );
302
- if (!already) {
303
- settings.hooks[event].push({ hooks: [{ type: 'command', command }] });
304
- }
305
- }
306
-
307
- const updateScript = path.join(hooksInstallDir, 'clancy-check-update.js');
308
- const statuslineScript = path.join(hooksInstallDir, 'clancy-statusline.js');
309
- const monitorScript = path.join(hooksInstallDir, 'clancy-context-monitor.js');
310
- const guardScript = path.join(hooksInstallDir, 'clancy-credential-guard.js');
311
-
312
- registerHook('SessionStart', `node ${updateScript}`);
313
- registerHook('PostToolUse', `node ${monitorScript}`);
314
- registerHook('PreToolUse', `node ${guardScript}`);
315
-
316
- // Statusline: registered as top-level key, not inside hooks
317
- if (!settings.statusLine) {
318
- settings.statusLine = { type: 'command', command: `node ${statuslineScript}` };
319
- }
320
-
321
- fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
322
- } catch {
323
- // Hook registration is best-effort — don't fail the install over it
324
- }
325
-
326
- console.log('');
327
- console.log(green(' ✓ Clancy installed successfully.'));
328
- console.log('');
329
- console.log(' Next steps:');
330
- console.log(dim(' 1. Open a project in Claude Code'));
331
- console.log(` 2. Run: ${cyan('/clancy:init')}`);
332
- console.log('');
333
- console.log(' Commands available:');
334
- console.log('');
335
- const cmds = [
336
- ['/clancy:init', 'Set up Clancy in your project'],
337
- ['/clancy:map-codebase', 'Scan codebase with 5 parallel agents'],
338
- ['/clancy:run', 'Run Clancy in loop mode'],
339
- ['/clancy:once', 'Pick up one ticket and stop'],
340
- ['/clancy:dry-run', 'Preview next ticket without making changes'],
341
- ['/clancy:status', 'Show next tickets without running'],
342
- ['/clancy:review', 'Score next ticket and get recommendations'],
343
- ['/clancy:logs', 'Display progress log'],
344
- ['/clancy:settings', 'View and change configuration'],
345
- ['/clancy:doctor', 'Diagnose your setup'],
346
- ['/clancy:update', 'Update Clancy to latest version'],
347
- ['/clancy:help', 'Show all commands'],
348
- ];
349
- for (const [cmd, desc] of cmds) {
350
- console.log(` ${cyan(cmd.padEnd(22))} ${dim(desc)}`);
351
- }
352
- console.log('');
353
- } catch (err) {
354
- console.error(red(`\n Install failed: ${err.message}`));
355
- rl.close();
356
- process.exit(1);
357
- }
358
-
359
- rl.close();
360
- }
361
-
362
- main();