@torka/claude-qol 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 (2) hide show
  1. package/install.js +100 -4
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -13,11 +13,17 @@ const colors = {
13
13
  green: '\x1b[32m',
14
14
  yellow: '\x1b[33m',
15
15
  blue: '\x1b[34m',
16
+ cyan: '\x1b[36m',
16
17
  red: '\x1b[31m',
17
18
  reset: '\x1b[0m',
18
19
  bold: '\x1b[1m',
19
20
  };
20
21
 
22
+ // Files that should not be overwritten if user has customized them
23
+ const PROTECTED_FILES = [
24
+ 'auto_approve_safe.rules.json'
25
+ ];
26
+
21
27
  function log(message, color = 'reset') {
22
28
  console.log(`${colors[color]}${message}${colors.reset}`);
23
29
  }
@@ -34,6 +40,18 @@ function logError(message) {
34
40
  log(` ✗ ${message}`, 'red');
35
41
  }
36
42
 
43
+ function logUpdate(message) {
44
+ log(` ↻ ${message}`, 'blue');
45
+ }
46
+
47
+ function logBackup(message) {
48
+ log(` ⤷ ${message}`, 'cyan');
49
+ }
50
+
51
+ function logPreserve(message) {
52
+ log(` ★ ${message}`, 'yellow');
53
+ }
54
+
37
55
  /**
38
56
  * Determine the target .claude directory based on installation context
39
57
  */
@@ -70,6 +88,32 @@ function getTargetBase() {
70
88
  return path.join(process.env.INIT_CWD || process.cwd(), '.claude');
71
89
  }
72
90
 
91
+ /**
92
+ * Ensure entries exist in a .gitignore file (append if missing)
93
+ */
94
+ function ensureGitignoreEntries(gitignorePath, entries, header) {
95
+ let existingContent = '';
96
+ if (fs.existsSync(gitignorePath)) {
97
+ existingContent = fs.readFileSync(gitignorePath, 'utf8');
98
+ }
99
+
100
+ const existingLines = new Set(
101
+ existingContent.split('\n').map(line => line.trim()).filter(Boolean)
102
+ );
103
+
104
+ const missingEntries = entries.filter(entry => !existingLines.has(entry));
105
+
106
+ if (missingEntries.length > 0) {
107
+ const newContent = existingContent.trimEnd() +
108
+ (existingContent ? '\n\n' : '') +
109
+ `# ${header}\n` +
110
+ missingEntries.join('\n') + '\n';
111
+ fs.writeFileSync(gitignorePath, newContent);
112
+ return missingEntries.length;
113
+ }
114
+ return 0;
115
+ }
116
+
73
117
  /**
74
118
  * Recursively copy directory contents
75
119
  */
@@ -93,8 +137,32 @@ function copyDirRecursive(src, dest, stats) {
93
137
  copyDirRecursive(srcPath, destPath, stats);
94
138
  } else {
95
139
  if (fs.existsSync(destPath)) {
96
- stats.skipped.push(destPath);
97
- logSkip(`Skipped (exists): ${path.relative(stats.targetBase, destPath)}`);
140
+ // Check if this is a protected user config file
141
+ if (PROTECTED_FILES.includes(entry.name)) {
142
+ stats.preserved.push(destPath);
143
+ logPreserve(`Preserved (user config): ${path.relative(stats.targetBase, destPath)}`);
144
+ continue;
145
+ }
146
+
147
+ // Check if files are identical (inline comparison)
148
+ const srcContent = fs.readFileSync(srcPath);
149
+ const destContent = fs.readFileSync(destPath);
150
+ const filesAreIdentical = srcContent.equals(destContent);
151
+
152
+ if (filesAreIdentical) {
153
+ stats.unchanged.push(destPath);
154
+ logSkip(`Unchanged: ${path.relative(stats.targetBase, destPath)}`);
155
+ } else {
156
+ // Backup existing file, then replace
157
+ const backupPath = destPath + '.backup';
158
+ fs.copyFileSync(destPath, backupPath);
159
+ stats.backups.push(backupPath);
160
+ logBackup(`Backup: ${path.relative(stats.targetBase, backupPath)}`);
161
+
162
+ fs.copyFileSync(srcPath, destPath);
163
+ stats.updated.push(destPath);
164
+ logUpdate(`Updated: ${path.relative(stats.targetBase, destPath)}`);
165
+ }
98
166
  } else {
99
167
  fs.copyFileSync(srcPath, destPath);
100
168
  stats.copied.push(destPath);
@@ -121,9 +189,34 @@ function install() {
121
189
  fs.mkdirSync(targetBase, { recursive: true });
122
190
  }
123
191
 
192
+ // Ensure gitignore entries for package-installed files
193
+ const gitignorePath = path.join(targetBase, '.gitignore');
194
+ const gitignoreEntries = [
195
+ 'scripts/auto_approve_safe.py',
196
+ 'scripts/auto_approve_safe.rules.json',
197
+ 'scripts/context-monitor.py',
198
+ 'scripts/__pycache__/',
199
+ 'commands/optimize-auto-approve-hook.md',
200
+ 'skills/nash/',
201
+ 'auto_approve_safe.decisions.jsonl',
202
+ 'auto_approve_safe.decisions.archived.jsonl',
203
+ '*.backup',
204
+ ];
205
+ const addedCount = ensureGitignoreEntries(
206
+ gitignorePath,
207
+ gitignoreEntries,
208
+ 'Installed by @torka/claude-qol'
209
+ );
210
+ if (addedCount > 0) {
211
+ log(` Updated .gitignore (added ${addedCount} entries)`, 'green');
212
+ }
213
+
124
214
  const stats = {
125
215
  copied: [],
126
- skipped: [],
216
+ updated: [],
217
+ unchanged: [],
218
+ preserved: [],
219
+ backups: [],
127
220
  targetBase,
128
221
  };
129
222
 
@@ -148,7 +241,10 @@ function install() {
148
241
  // Summary
149
242
  log('\n' + colors.bold + '📊 Installation Summary' + colors.reset);
150
243
  log(` Files copied: ${stats.copied.length}`, 'green');
151
- log(` Files skipped (already exist): ${stats.skipped.length}`, 'yellow');
244
+ log(` Files updated: ${stats.updated.length}`, 'blue');
245
+ log(` Backups created: ${stats.backups.length}`, 'cyan');
246
+ log(` Files unchanged: ${stats.unchanged.length}`, 'yellow');
247
+ log(` Files preserved (user config): ${stats.preserved.length}`, 'yellow');
152
248
 
153
249
  // Post-install instructions
154
250
  log('\n' + colors.bold + '📝 Configuration Required' + colors.reset);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torka/claude-qol",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Claude Code quality-of-life improvements: auto-approve hooks, context monitoring, and status line enhancements",
5
5
  "keywords": [
6
6
  "claude-code",