chief-clancy 0.2.0-beta.1 → 0.2.0-beta.3
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/README.md +10 -2
- package/bin/install.js +113 -0
- package/hooks/clancy-credential-guard.js +113 -0
- package/package.json +1 -1
- package/src/workflows/settings.md +41 -0
- package/src/workflows/uninstall.md +49 -10
- package/src/workflows/update.md +158 -23
package/README.md
CHANGED
|
@@ -192,7 +192,7 @@ Clancy also merges a section into your `CLAUDE.md` (or creates one) that tells C
|
|
|
192
192
|
|
|
193
193
|
## Optional enhancements
|
|
194
194
|
|
|
195
|
-
Set during `/clancy:init` advanced setup, or by editing `.clancy/.env` directly.
|
|
195
|
+
Set during `/clancy:init` advanced setup, or by editing `.clancy/.env` directly. Use `/clancy:settings` → "Save as defaults" to save non-credential settings to `~/.clancy/defaults.json` — new projects created with `/clancy:init` will inherit them automatically.
|
|
196
196
|
|
|
197
197
|
### Figma MCP
|
|
198
198
|
|
|
@@ -336,6 +336,12 @@ Your board tokens and API keys live in `.clancy/.env`. Although Claude doesn't n
|
|
|
336
336
|
|
|
337
337
|
This prevents Claude from reading these files regardless of what commands run. Clancy automatically adds `.clancy/.env` to `.gitignore` during init, but the deny list is an additional layer.
|
|
338
338
|
|
|
339
|
+
### Credential guard
|
|
340
|
+
|
|
341
|
+
Clancy installs a `PreToolUse` hook (`clancy-credential-guard.js`) that scans every Write, Edit, and MultiEdit operation for credential patterns — API keys, tokens, passwords, private keys, and connection strings. If a match is found, the operation is blocked with a message telling Claude to move the credential to `.clancy/.env` instead. Files that are expected to contain credentials (`.clancy/.env`, `.env.example`, etc.) are exempt.
|
|
342
|
+
|
|
343
|
+
This is best-effort — it won't catch every possible credential format, but it prevents the most common accidental leaks.
|
|
344
|
+
|
|
339
345
|
### Token scopes
|
|
340
346
|
|
|
341
347
|
Use the minimum permissions each integration requires:
|
|
@@ -418,6 +424,8 @@ lsof -ti:5173 | xargs kill -9 # replace 5173 with your PLAYWRIGHT_DEV_PORT
|
|
|
418
424
|
|
|
419
425
|
Or directly: `npx chief-clancy@latest`
|
|
420
426
|
|
|
427
|
+
The update workflow shows what's changed (changelog diff) and asks for confirmation before overwriting. If you've customised any command or workflow files, they're automatically backed up to `.claude/clancy/local-patches/` before the update — check there to reapply your changes afterwards.
|
|
428
|
+
|
|
421
429
|
---
|
|
422
430
|
|
|
423
431
|
**Uninstalling?**
|
|
@@ -426,7 +434,7 @@ Or directly: `npx chief-clancy@latest`
|
|
|
426
434
|
/clancy:uninstall
|
|
427
435
|
```
|
|
428
436
|
|
|
429
|
-
Removes slash commands from your chosen location. Optionally removes `.clancy/` (credentials and docs).
|
|
437
|
+
Removes slash commands from your chosen location. Cleans up the `<!-- clancy:start -->` / `<!-- clancy:end -->` block from `CLAUDE.md` (or deletes it entirely if Clancy created it) and removes the `.clancy/.env` entry from `.gitignore`. Optionally removes `.clancy/` (credentials and docs).
|
|
430
438
|
|
|
431
439
|
---
|
|
432
440
|
|
package/bin/install.js
CHANGED
|
@@ -49,6 +49,13 @@ async function choose(question, options, defaultChoice = 1) {
|
|
|
49
49
|
return raw.trim() || String(defaultChoice);
|
|
50
50
|
}
|
|
51
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
|
+
|
|
52
59
|
function copyDir(src, dest) {
|
|
53
60
|
// Use lstatSync (not statSync) to detect symlinks — statSync follows them and misreports
|
|
54
61
|
if (fs.existsSync(dest)) {
|
|
@@ -65,6 +72,73 @@ function copyDir(src, dest) {
|
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
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
|
+
|
|
68
142
|
async function main() {
|
|
69
143
|
console.log('');
|
|
70
144
|
console.log(blue(' ██████╗██╗ █████╗ ███╗ ██╗ ██████╗██╗ ██╗'));
|
|
@@ -116,14 +190,42 @@ async function main() {
|
|
|
116
190
|
console.log(dim(` Installing to: ${dest}`));
|
|
117
191
|
|
|
118
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
|
+
|
|
119
198
|
if (fs.existsSync(dest) || fs.existsSync(workflowsDest)) {
|
|
120
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
|
+
|
|
121
217
|
const overwrite = await ask(blue(` Commands already exist at ${dest}. Overwrite? [y/N] `));
|
|
122
218
|
if (!overwrite.trim().toLowerCase().startsWith('y')) {
|
|
123
219
|
console.log('\n Aborted. No files changed.');
|
|
124
220
|
rl.close();
|
|
125
221
|
process.exit(0);
|
|
126
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
|
+
}
|
|
127
229
|
}
|
|
128
230
|
|
|
129
231
|
copyDir(COMMANDS_SRC, dest);
|
|
@@ -150,6 +252,14 @@ async function main() {
|
|
|
150
252
|
// Write VERSION file so /clancy:doctor and /clancy:update can read the installed version
|
|
151
253
|
fs.writeFileSync(path.join(dest, 'VERSION'), PKG.version);
|
|
152
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
|
+
|
|
153
263
|
// Install hooks and register them in Claude settings.json
|
|
154
264
|
const claudeConfigDir = dest === GLOBAL_DEST
|
|
155
265
|
? path.join(homeDir, '.claude')
|
|
@@ -161,6 +271,7 @@ async function main() {
|
|
|
161
271
|
'clancy-check-update.js',
|
|
162
272
|
'clancy-statusline.js',
|
|
163
273
|
'clancy-context-monitor.js',
|
|
274
|
+
'clancy-credential-guard.js',
|
|
164
275
|
];
|
|
165
276
|
|
|
166
277
|
try {
|
|
@@ -196,9 +307,11 @@ async function main() {
|
|
|
196
307
|
const updateScript = path.join(hooksInstallDir, 'clancy-check-update.js');
|
|
197
308
|
const statuslineScript = path.join(hooksInstallDir, 'clancy-statusline.js');
|
|
198
309
|
const monitorScript = path.join(hooksInstallDir, 'clancy-context-monitor.js');
|
|
310
|
+
const guardScript = path.join(hooksInstallDir, 'clancy-credential-guard.js');
|
|
199
311
|
|
|
200
312
|
registerHook('SessionStart', `node ${updateScript}`);
|
|
201
313
|
registerHook('PostToolUse', `node ${monitorScript}`);
|
|
314
|
+
registerHook('PreToolUse', `node ${guardScript}`);
|
|
202
315
|
|
|
203
316
|
// Statusline: registered as top-level key, not inside hooks
|
|
204
317
|
if (!settings.statusLine) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Clancy Credential Guard — PreToolUse hook.
|
|
3
|
+
// Scans file content being written or edited for credential patterns
|
|
4
|
+
// (API keys, tokens, passwords, private keys) and blocks the operation
|
|
5
|
+
// if a match is found. Best-effort — never fails the tool call on error.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const CREDENTIAL_PATTERNS = [
|
|
10
|
+
// Generic API keys and tokens
|
|
11
|
+
{ name: 'Generic API key', pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']?[A-Za-z0-9\-_.]{20,}["']?/i },
|
|
12
|
+
{ name: 'Generic secret', pattern: /(?:secret|private[_-]?key)\s*[:=]\s*["']?[A-Za-z0-9\-_.]{20,}["']?/i },
|
|
13
|
+
{ name: 'Generic token', pattern: /(?:auth[_-]?token|access[_-]?token|bearer)\s*[:=]\s*["']?[A-Za-z0-9\-_.]{20,}["']?/i },
|
|
14
|
+
{ name: 'Generic password', pattern: /(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']{8,}["']?/i },
|
|
15
|
+
|
|
16
|
+
// AWS
|
|
17
|
+
{ name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/ },
|
|
18
|
+
{ name: 'AWS Secret Key', pattern: /(?:aws_secret_access_key|aws_secret)\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?/i },
|
|
19
|
+
|
|
20
|
+
// GitHub
|
|
21
|
+
{ name: 'GitHub PAT (classic)', pattern: /ghp_[A-Za-z0-9]{36}/ },
|
|
22
|
+
{ name: 'GitHub PAT (fine-grained)', pattern: /github_pat_[A-Za-z0-9_]{82}/ },
|
|
23
|
+
{ name: 'GitHub OAuth token', pattern: /gho_[A-Za-z0-9]{36}/ },
|
|
24
|
+
|
|
25
|
+
// Slack
|
|
26
|
+
{ name: 'Slack token', pattern: /xox[bpors]-[0-9]{10,}-[A-Za-z0-9-]+/ },
|
|
27
|
+
|
|
28
|
+
// Stripe
|
|
29
|
+
{ name: 'Stripe key', pattern: /(?:sk|pk)_(?:live|test)_[A-Za-z0-9]{24,}/ },
|
|
30
|
+
|
|
31
|
+
// Private keys
|
|
32
|
+
{ name: 'Private key', pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/ },
|
|
33
|
+
|
|
34
|
+
// Jira/Atlassian API tokens (base64-like, 24+ chars after the prefix)
|
|
35
|
+
{ name: 'Atlassian API token', pattern: /(?:jira_api_token|atlassian[_-]?token)\s*[:=]\s*["']?[A-Za-z0-9+/=]{24,}["']?/i },
|
|
36
|
+
|
|
37
|
+
// Linear API key
|
|
38
|
+
{ name: 'Linear API key', pattern: /lin_api_[A-Za-z0-9]{40,}/ },
|
|
39
|
+
|
|
40
|
+
// Generic connection strings
|
|
41
|
+
{ name: 'Database connection string', pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^\s"']+:[^\s"']+@/i },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Files that are expected to contain credentials — skip them
|
|
45
|
+
const ALLOWED_PATHS = [
|
|
46
|
+
'.clancy/.env',
|
|
47
|
+
'.env.local',
|
|
48
|
+
'.env.example',
|
|
49
|
+
'.env.development',
|
|
50
|
+
'.env.test',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function isAllowedPath(filePath) {
|
|
54
|
+
if (!filePath) return false;
|
|
55
|
+
return ALLOWED_PATHS.some(allowed => filePath.endsWith(allowed));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function scanForCredentials(content) {
|
|
59
|
+
if (!content || typeof content !== 'string') return [];
|
|
60
|
+
const matches = [];
|
|
61
|
+
for (const { name, pattern } of CREDENTIAL_PATTERNS) {
|
|
62
|
+
if (pattern.test(content)) {
|
|
63
|
+
matches.push(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return matches;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const input = JSON.parse(process.argv[2] || '{}');
|
|
71
|
+
const toolName = input.tool_name || '';
|
|
72
|
+
const toolInput = input.tool_input || {};
|
|
73
|
+
|
|
74
|
+
// Only check file-writing tools
|
|
75
|
+
if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
|
|
76
|
+
// Pass through — not a file write
|
|
77
|
+
console.log(JSON.stringify({ decision: 'approve' }));
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const filePath = toolInput.file_path || '';
|
|
82
|
+
|
|
83
|
+
// Skip files that are expected to contain credentials
|
|
84
|
+
if (isAllowedPath(filePath)) {
|
|
85
|
+
console.log(JSON.stringify({ decision: 'approve' }));
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Collect content to scan based on tool type
|
|
90
|
+
let contentToScan = '';
|
|
91
|
+
if (toolName === 'Write') {
|
|
92
|
+
contentToScan = toolInput.content || '';
|
|
93
|
+
} else if (toolName === 'Edit') {
|
|
94
|
+
contentToScan = toolInput.new_string || '';
|
|
95
|
+
} else if (toolName === 'MultiEdit') {
|
|
96
|
+
const edits = toolInput.edits || [];
|
|
97
|
+
contentToScan = edits.map(e => e.new_string || '').join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const found = scanForCredentials(contentToScan);
|
|
101
|
+
|
|
102
|
+
if (found.length > 0) {
|
|
103
|
+
console.log(JSON.stringify({
|
|
104
|
+
decision: 'block',
|
|
105
|
+
reason: `Credential guard: blocked writing to ${filePath}. Detected: ${found.join(', ')}. Move credentials to .clancy/.env instead.`,
|
|
106
|
+
}));
|
|
107
|
+
} else {
|
|
108
|
+
console.log(JSON.stringify({ decision: 'approve' }));
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Best-effort — never block on error
|
|
112
|
+
console.log(JSON.stringify({ decision: 'approve' }));
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -408,9 +408,50 @@ When updating a value:
|
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|
|
411
|
+
### Save as global defaults
|
|
412
|
+
|
|
413
|
+
At the bottom of the settings menu (before Exit), show:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
[{N}] Save as defaults save current settings for all future projects
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
When selected:
|
|
420
|
+
|
|
421
|
+
1. Read the current `.clancy/.env` and extract only the non-credential, non-board-specific settings:
|
|
422
|
+
- `MAX_ITERATIONS`
|
|
423
|
+
- `CLANCY_MODEL`
|
|
424
|
+
- `CLANCY_BASE_BRANCH`
|
|
425
|
+
- `PLAYWRIGHT_ENABLED`
|
|
426
|
+
- `PLAYWRIGHT_STARTUP_WAIT`
|
|
427
|
+
|
|
428
|
+
2. Write these to `~/.clancy/defaults.json`:
|
|
429
|
+
```json
|
|
430
|
+
{
|
|
431
|
+
"MAX_ITERATIONS": "5",
|
|
432
|
+
"CLANCY_MODEL": "claude-sonnet-4-6",
|
|
433
|
+
"CLANCY_BASE_BRANCH": "main"
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
3. Print: `✓ Defaults saved to ~/.clancy/defaults.json — new projects will inherit these settings.`
|
|
438
|
+
|
|
439
|
+
4. Loop back to the settings menu.
|
|
440
|
+
|
|
441
|
+
**Never save credentials, board-specific settings (status filter, sprint, label), or webhook URLs to global defaults.**
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Step 6 — Load global defaults during init
|
|
446
|
+
|
|
447
|
+
When `/clancy:init` creates `.clancy/.env`, check if `~/.clancy/defaults.json` exists. If so, pre-populate the `.env` with those values instead of the built-in defaults. The user's answers during init still take priority — defaults are only used for settings that init doesn't ask about (max iterations, model, etc.).
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
411
451
|
## Notes
|
|
412
452
|
|
|
413
453
|
- All changes are written to `.clancy/.env` immediately after confirmation
|
|
414
454
|
- Switching boards verifies credentials before making any changes — nothing is written if verification fails
|
|
415
455
|
- `/clancy:init` remains available for a full re-setup (re-scaffolds scripts and docs)
|
|
416
456
|
- This command never restarts any servers or triggers any ticket processing
|
|
457
|
+
- Global defaults (`~/.clancy/defaults.json`) are optional — if the file doesn't exist, built-in defaults are used
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
Remove Clancy's slash commands from the local project, globally, or both. Optionally remove the `.clancy/` project folder (which includes `.clancy/.env`).
|
|
5
|
+
Remove Clancy's slash commands from the local project, globally, or both. Optionally remove the `.clancy/` project folder (which includes `.clancy/.env`). Clean up CLAUDE.md and .gitignore changes made during init.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -50,14 +50,15 @@ Print: `✓ Clancy commands removed from [location].`
|
|
|
50
50
|
### Step 2b — Remove hooks
|
|
51
51
|
|
|
52
52
|
For each location being removed, delete these hook files if they exist:
|
|
53
|
-
- Project-local: `.claude/hooks/clancy-check-update.js`, `.claude/hooks/clancy-statusline.js`, `.claude/hooks/clancy-context-monitor.js`
|
|
54
|
-
- Global: `~/.claude/hooks/clancy-check-update.js`, `~/.claude/hooks/clancy-statusline.js`, `~/.claude/hooks/clancy-context-monitor.js`
|
|
53
|
+
- Project-local: `.claude/hooks/clancy-check-update.js`, `.claude/hooks/clancy-statusline.js`, `.claude/hooks/clancy-context-monitor.js`, `.claude/hooks/clancy-credential-guard.js`
|
|
54
|
+
- Global: `~/.claude/hooks/clancy-check-update.js`, `~/.claude/hooks/clancy-statusline.js`, `~/.claude/hooks/clancy-context-monitor.js`, `~/.claude/hooks/clancy-credential-guard.js`
|
|
55
55
|
|
|
56
56
|
Then remove the Clancy hook registrations from the corresponding `settings.json` (`.claude/settings.json` for local, `~/.claude/settings.json` for global):
|
|
57
57
|
- Remove any entry in `hooks.SessionStart` whose `command` contains `clancy-check-update`
|
|
58
58
|
- Remove any entry in `hooks.PostToolUse` whose `command` contains `clancy-context-monitor`
|
|
59
|
+
- Remove any entry in `hooks.PreToolUse` whose `command` contains `clancy-credential-guard`
|
|
59
60
|
- Remove the `statusLine` key if its `command` value contains `clancy-statusline`
|
|
60
|
-
- If removing an entry leaves a `hooks.SessionStart` or `hooks.
|
|
61
|
+
- If removing an entry leaves a `hooks.SessionStart`, `hooks.PostToolUse`, or `hooks.PreToolUse` array empty, remove the key entirely
|
|
61
62
|
|
|
62
63
|
Also remove the update check cache if it exists: `~/.claude/cache/clancy-update-check.json`
|
|
63
64
|
|
|
@@ -65,7 +66,46 @@ If `settings.json` does not exist or cannot be parsed, skip silently — do not
|
|
|
65
66
|
|
|
66
67
|
---
|
|
67
68
|
|
|
68
|
-
## Step 3 —
|
|
69
|
+
## Step 3 — Clean up CLAUDE.md
|
|
70
|
+
|
|
71
|
+
Check whether `CLAUDE.md` exists in the current project directory.
|
|
72
|
+
|
|
73
|
+
If it does, check for Clancy markers (`<!-- clancy:start -->` and `<!-- clancy:end -->`):
|
|
74
|
+
|
|
75
|
+
**If markers found:**
|
|
76
|
+
|
|
77
|
+
Read the full file content. Determine whether Clancy created the file or appended to an existing one:
|
|
78
|
+
|
|
79
|
+
- **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
|
+
- **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
|
+
|
|
82
|
+
Print `✓ CLAUDE.md cleaned up.` (or `✓ CLAUDE.md removed.` if deleted).
|
|
83
|
+
|
|
84
|
+
**If no markers found:** skip — Clancy didn't modify this file.
|
|
85
|
+
|
|
86
|
+
**If CLAUDE.md does not exist:** skip.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Step 4 — Clean up .gitignore
|
|
91
|
+
|
|
92
|
+
Check whether `.gitignore` exists in the current project directory.
|
|
93
|
+
|
|
94
|
+
If it does, check whether it contains the Clancy entries (`# Clancy credentials` and/or `.clancy/.env`):
|
|
95
|
+
|
|
96
|
+
**If found:** remove the `# Clancy credentials` comment line and the `.clancy/.env` line. Also remove any blank line immediately before or after the removed block to avoid leaving double blank lines. Write the cleaned file back.
|
|
97
|
+
|
|
98
|
+
If the file is now empty (or contains only whitespace) after removal, delete it entirely — Clancy created it during init.
|
|
99
|
+
|
|
100
|
+
Print `✓ .gitignore cleaned up.` (or `✓ .gitignore removed.` if deleted).
|
|
101
|
+
|
|
102
|
+
**If not found:** skip — Clancy didn't modify this file.
|
|
103
|
+
|
|
104
|
+
**If .gitignore does not exist:** skip.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Step 5 — Offer to remove .clancy/ (if present)
|
|
69
109
|
|
|
70
110
|
Check whether `.clancy/` exists in the current project directory.
|
|
71
111
|
|
|
@@ -83,7 +123,7 @@ If `.clancy/` does not exist, skip this step entirely.
|
|
|
83
123
|
|
|
84
124
|
---
|
|
85
125
|
|
|
86
|
-
## Step
|
|
126
|
+
## Step 6 — Final message
|
|
87
127
|
|
|
88
128
|
```
|
|
89
129
|
Clancy uninstalled. To reinstall: npx chief-clancy
|
|
@@ -93,7 +133,6 @@ Clancy uninstalled. To reinstall: npx chief-clancy
|
|
|
93
133
|
|
|
94
134
|
## Hard constraints
|
|
95
135
|
|
|
96
|
-
- **Never touch any `.env` at the project root** — Clancy's credentials live in `.clancy/.env` and are only removed as part of `.clancy/` in Step
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
- If the user says no to commands removal in Step 2, skip Step 3 entirely and stop
|
|
136
|
+
- **Never touch any `.env` at the project root** — Clancy's credentials live in `.clancy/.env` and are only removed as part of `.clancy/` in Step 5
|
|
137
|
+
- Steps 1–2 (commands removal), Steps 3–4 (CLAUDE.md and .gitignore cleanup), and Step 5 (`.clancy/` removal) are always asked separately — never bundle them into one confirmation
|
|
138
|
+
- If the user says no to commands removal in Step 2, skip all remaining steps and stop
|
package/src/workflows/update.md
CHANGED
|
@@ -2,66 +2,201 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Check for Clancy updates via npm, display changelog for versions between installed and latest, obtain user confirmation, and execute clean installation.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## Step 1 —
|
|
9
|
+
## Step 1 — Detect installed version
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Determine whether Clancy is installed locally or globally by checking both locations:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
LOCAL_VERSION_FILE="./.claude/commands/clancy/VERSION"
|
|
15
|
+
GLOBAL_VERSION_FILE="$HOME/.claude/commands/clancy/VERSION"
|
|
16
|
+
|
|
17
|
+
if [ -f "$LOCAL_VERSION_FILE" ] && grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+' "$LOCAL_VERSION_FILE"; then
|
|
18
|
+
INSTALLED=$(cat "$LOCAL_VERSION_FILE")
|
|
19
|
+
INSTALL_TYPE="LOCAL"
|
|
20
|
+
elif [ -f "$GLOBAL_VERSION_FILE" ] && grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+' "$GLOBAL_VERSION_FILE"; then
|
|
21
|
+
INSTALLED=$(cat "$GLOBAL_VERSION_FILE")
|
|
22
|
+
INSTALL_TYPE="GLOBAL"
|
|
23
|
+
else
|
|
24
|
+
INSTALLED="unknown"
|
|
25
|
+
INSTALL_TYPE="UNKNOWN"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
echo "$INSTALLED"
|
|
29
|
+
echo "$INSTALL_TYPE"
|
|
15
30
|
```
|
|
16
31
|
|
|
32
|
+
Parse output:
|
|
33
|
+
- First line = installed version (or "unknown")
|
|
34
|
+
- Second line = install type (LOCAL, GLOBAL, or UNKNOWN)
|
|
35
|
+
|
|
36
|
+
**If version is unknown:**
|
|
37
|
+
```
|
|
38
|
+
## Clancy Update
|
|
39
|
+
|
|
40
|
+
**Installed version:** Unknown
|
|
41
|
+
|
|
42
|
+
Your installation doesn't include version tracking.
|
|
43
|
+
|
|
44
|
+
Running fresh install...
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Proceed to Step 4 (treat as version 0.0.0 for comparison).
|
|
48
|
+
|
|
17
49
|
---
|
|
18
50
|
|
|
19
|
-
## Step 2 —
|
|
51
|
+
## Step 2 — Check latest version
|
|
52
|
+
|
|
53
|
+
Check npm for the latest published version:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm view chief-clancy version 2>/dev/null
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**If npm check fails:**
|
|
60
|
+
```
|
|
61
|
+
Couldn't check for updates (offline or npm unavailable).
|
|
62
|
+
|
|
63
|
+
To update manually: `npx chief-clancy@latest`
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Exit.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Step 3 — Compare versions and confirm
|
|
71
|
+
|
|
72
|
+
Compare installed vs latest:
|
|
73
|
+
|
|
74
|
+
**If installed == latest:**
|
|
75
|
+
```
|
|
76
|
+
## Clancy Update
|
|
77
|
+
|
|
78
|
+
**Installed:** X.Y.Z
|
|
79
|
+
**Latest:** X.Y.Z
|
|
80
|
+
|
|
81
|
+
You're already on the latest version.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Exit.
|
|
85
|
+
|
|
86
|
+
**If update available**, fetch the changelog from GitHub and show what's new BEFORE updating:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
curl -s https://raw.githubusercontent.com/Pushedskydiver/clancy/main/CHANGELOG.md
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Extract only the entries between the installed version and the latest version. Display:
|
|
20
93
|
|
|
21
94
|
```
|
|
22
|
-
|
|
95
|
+
## Clancy Update Available
|
|
96
|
+
|
|
97
|
+
**Installed:** {installed}
|
|
98
|
+
**Latest:** {latest}
|
|
99
|
+
|
|
100
|
+
### What's New
|
|
101
|
+
────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
{relevant CHANGELOG entries between installed and latest}
|
|
104
|
+
|
|
105
|
+
────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
⚠️ **Note:** The update performs a clean install of Clancy command folders:
|
|
108
|
+
- `.claude/commands/clancy/` will be replaced
|
|
109
|
+
- `.claude/clancy/workflows/` will be replaced
|
|
110
|
+
|
|
111
|
+
If you've modified any Clancy files directly, they'll be automatically backed up
|
|
112
|
+
to `.claude/clancy/local-patches/` before overwriting.
|
|
113
|
+
|
|
114
|
+
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 ✓
|
|
23
119
|
```
|
|
24
120
|
|
|
25
|
-
|
|
121
|
+
Ask the user: **"Proceed with update?"** with options:
|
|
122
|
+
- "Yes, update now"
|
|
123
|
+
- "No, cancel"
|
|
124
|
+
|
|
125
|
+
**If user cancels:** Exit.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Step 4 — Run the update
|
|
130
|
+
|
|
131
|
+
Run the installer using the detected install type:
|
|
132
|
+
|
|
26
133
|
```bash
|
|
27
|
-
npx chief-clancy@latest
|
|
134
|
+
npx -y chief-clancy@latest
|
|
28
135
|
```
|
|
29
136
|
|
|
30
|
-
|
|
137
|
+
The installer auto-detects whether to install globally or locally based on the existing install.
|
|
31
138
|
|
|
32
|
-
|
|
33
|
-
- `.clancy/clancy-once.sh` or `.clancy/clancy-afk.sh` — shell scripts
|
|
34
|
-
- `.clancy/docs/` codebase documentation
|
|
35
|
-
- `.clancy/progress.txt` progress log
|
|
36
|
-
- `.clancy/.env` credentials
|
|
139
|
+
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
|
|
141
|
+
- `.clancy/docs/` — codebase documentation
|
|
142
|
+
- `.clancy/progress.txt` — progress log
|
|
143
|
+
- `.clancy/.env` — credentials
|
|
37
144
|
- `CLAUDE.md`
|
|
38
145
|
|
|
39
|
-
**To update the shell scripts
|
|
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.
|
|
40
147
|
|
|
41
148
|
---
|
|
42
149
|
|
|
43
|
-
## Step
|
|
150
|
+
## Step 5 — Clear update cache and confirm
|
|
44
151
|
|
|
45
|
-
|
|
152
|
+
Clear the update check cache so the statusline indicator disappears:
|
|
46
153
|
|
|
154
|
+
```bash
|
|
155
|
+
rm -f "$HOME/.claude/cache/clancy-update-check.json"
|
|
156
|
+
rm -f "./.claude/cache/clancy-update-check.json"
|
|
47
157
|
```
|
|
48
|
-
Updated Clancy from v{old} to v{new}.
|
|
49
158
|
|
|
50
|
-
|
|
51
|
-
|
|
159
|
+
Display completion message:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
163
|
+
║ Clancy Updated: v{old} → v{new} ║
|
|
164
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
165
|
+
|
|
166
|
+
⚠️ Restart Claude Code to pick up the new commands.
|
|
52
167
|
|
|
53
168
|
View full changelog: github.com/Pushedskydiver/clancy/blob/main/CHANGELOG.md
|
|
54
169
|
```
|
|
55
170
|
|
|
56
|
-
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Step 6 — Check for local patches
|
|
174
|
+
|
|
175
|
+
After the update completes, check if the installer backed up any locally modified files:
|
|
176
|
+
|
|
177
|
+
Check for `.claude/clancy/local-patches/backup-meta.json` (local install) or `~/.claude/clancy/local-patches/backup-meta.json` (global install).
|
|
178
|
+
|
|
179
|
+
**If patches were found:**
|
|
180
|
+
|
|
57
181
|
```
|
|
58
|
-
|
|
182
|
+
Local patches were backed up before the update.
|
|
183
|
+
Your modified files are in .claude/clancy/local-patches/
|
|
184
|
+
|
|
185
|
+
To review what changed:
|
|
186
|
+
Compare each file in local-patches/ against its counterpart in
|
|
187
|
+
.claude/commands/clancy/ or .claude/clancy/workflows/ and manually
|
|
188
|
+
reapply any customisations you want to keep.
|
|
189
|
+
|
|
190
|
+
Backed up files:
|
|
191
|
+
{list from backup-meta.json}
|
|
59
192
|
```
|
|
60
193
|
|
|
194
|
+
**If no patches:** Continue normally (no message needed).
|
|
195
|
+
|
|
61
196
|
---
|
|
62
197
|
|
|
63
198
|
## Notes
|
|
64
199
|
|
|
65
200
|
- If the user installed globally, the update applies globally
|
|
66
201
|
- If the user installed locally, the update applies locally
|
|
67
|
-
- After updating,
|
|
202
|
+
- After updating, restart Claude Code for new commands to take effect
|