mindsystem-cc 3.3.3 → 3.6.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.
- package/README.md +65 -1
- package/agents/ms-code-simplifier.md +1 -0
- package/agents/ms-flutter-code-quality.md +168 -0
- package/agents/ms-flutter-reviewer.md +210 -0
- package/agents/ms-flutter-simplifier.md +12 -118
- package/bin/install.js +444 -85
- package/commands/ms/audit-milestone.md +209 -0
- package/commands/ms/do-work.md +7 -7
- package/commands/ms/execute-phase.md +5 -5
- package/commands/ms/research-project.md +10 -4
- package/mindsystem/templates/config.json +5 -1
- package/mindsystem/workflows/do-work.md +23 -23
- package/mindsystem/workflows/execute-phase.md +20 -20
- package/mindsystem/workflows/map-codebase.md +12 -6
- package/package.json +3 -2
- package/skills/flutter-code-quality/SKILL.md +142 -0
- package/skills/flutter-code-simplification/SKILL.md +102 -0
- package/skills/flutter-senior-review/AGENTS.md +869 -0
- package/skills/flutter-senior-review/SKILL.md +205 -0
- package/skills/flutter-senior-review/principles/dependencies-data-not-callbacks.md +75 -0
- package/skills/flutter-senior-review/principles/dependencies-provider-tree.md +85 -0
- package/skills/flutter-senior-review/principles/dependencies-temporal-coupling.md +97 -0
- package/skills/flutter-senior-review/principles/pragmatism-consistent-error-handling.md +130 -0
- package/skills/flutter-senior-review/principles/pragmatism-speculative-generality.md +91 -0
- package/skills/flutter-senior-review/principles/state-data-clumps.md +64 -0
- package/skills/flutter-senior-review/principles/state-invalid-states.md +53 -0
- package/skills/flutter-senior-review/principles/state-single-source-of-truth.md +68 -0
- package/skills/flutter-senior-review/principles/state-type-hierarchies.md +75 -0
- package/skills/flutter-senior-review/principles/structure-composition-over-config.md +105 -0
- package/skills/flutter-senior-review/principles/structure-shared-visual-patterns.md +107 -0
- package/skills/flutter-senior-review/principles/structure-wrapper-pattern.md +90 -0
package/bin/install.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const readline = require('readline');
|
|
7
|
+
const crypto = require('crypto');
|
|
7
8
|
|
|
8
9
|
// Colors (using 256-color mode for better terminal compatibility)
|
|
9
10
|
const cyan = '\x1b[38;5;37m'; // Closest to #2FA7A0 in 256-color palette
|
|
@@ -54,6 +55,7 @@ function parseConfigDirArg() {
|
|
|
54
55
|
return null;
|
|
55
56
|
}
|
|
56
57
|
const explicitConfigDir = parseConfigDirArg();
|
|
58
|
+
const hasForce = args.includes('--force') || args.includes('-f');
|
|
57
59
|
const hasHelp = args.includes('--help') || args.includes('-h');
|
|
58
60
|
|
|
59
61
|
console.log(banner);
|
|
@@ -66,6 +68,7 @@ if (hasHelp) {
|
|
|
66
68
|
${cyan}-g, --global${reset} Install globally (to Claude config directory)
|
|
67
69
|
${cyan}-l, --local${reset} Install locally (to ./.claude in current directory)
|
|
68
70
|
${cyan}-c, --config-dir <path>${reset} Specify custom Claude config directory
|
|
71
|
+
${cyan}-f, --force${reset} Overwrite modified files without prompting
|
|
69
72
|
${cyan}-h, --help${reset} Show this help message
|
|
70
73
|
|
|
71
74
|
${yellow}Examples:${reset}
|
|
@@ -99,6 +102,308 @@ function expandTilde(filePath) {
|
|
|
99
102
|
return filePath;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Compute SHA-256 checksum truncated to 16 chars
|
|
107
|
+
*/
|
|
108
|
+
function computeChecksum(content) {
|
|
109
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Read and parse manifest file, return null if missing/corrupted
|
|
114
|
+
*/
|
|
115
|
+
function readManifest(claudeDir) {
|
|
116
|
+
const manifestPath = path.join(claudeDir, 'mindsystem', '.manifest.json');
|
|
117
|
+
try {
|
|
118
|
+
if (!fs.existsSync(manifestPath)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const content = fs.readFileSync(manifestPath, 'utf8');
|
|
122
|
+
return JSON.parse(content);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.log(` ${yellow}⚠${reset} Manifest corrupted, treating as fresh install`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Write manifest JSON file
|
|
131
|
+
*/
|
|
132
|
+
function writeManifest(claudeDir, manifest) {
|
|
133
|
+
const manifestDir = path.join(claudeDir, 'mindsystem');
|
|
134
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
135
|
+
const manifestPath = path.join(manifestDir, '.manifest.json');
|
|
136
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if running in interactive TTY
|
|
141
|
+
*/
|
|
142
|
+
function isInteractive() {
|
|
143
|
+
return process.stdin.isTTY && process.stdout.isTTY;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Recursively collect files with relative paths
|
|
148
|
+
* @param {string} baseDir - The base directory for relative path calculation
|
|
149
|
+
* @param {string} currentDir - The current directory being scanned
|
|
150
|
+
* @param {string} destPrefix - The destination prefix (e.g., 'commands/ms', 'agents')
|
|
151
|
+
* @returns {Array<{relativePath: string, absolutePath: string}>}
|
|
152
|
+
*/
|
|
153
|
+
function collectFiles(baseDir, currentDir, destPrefix) {
|
|
154
|
+
const files = [];
|
|
155
|
+
if (!fs.existsSync(currentDir)) {
|
|
156
|
+
return files;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
162
|
+
const relativeToCurrent = path.relative(baseDir, absolutePath);
|
|
163
|
+
const relativePath = path.join(destPrefix, relativeToCurrent);
|
|
164
|
+
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
files.push(...collectFiles(baseDir, absolutePath, destPrefix));
|
|
167
|
+
} else {
|
|
168
|
+
files.push({ relativePath, absolutePath });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build complete list of files to install from all source directories
|
|
176
|
+
* @param {string} src - The source directory (mindsystem package root)
|
|
177
|
+
* @returns {Array<{relativePath: string, absolutePath: string}>}
|
|
178
|
+
*/
|
|
179
|
+
function buildInstallManifest(src) {
|
|
180
|
+
const files = [];
|
|
181
|
+
|
|
182
|
+
// commands/ms
|
|
183
|
+
const commandsSrc = path.join(src, 'commands', 'ms');
|
|
184
|
+
files.push(...collectFiles(commandsSrc, commandsSrc, 'commands/ms'));
|
|
185
|
+
|
|
186
|
+
// mindsystem
|
|
187
|
+
const mindsystemSrc = path.join(src, 'mindsystem');
|
|
188
|
+
files.push(...collectFiles(mindsystemSrc, mindsystemSrc, 'mindsystem'));
|
|
189
|
+
|
|
190
|
+
// agents
|
|
191
|
+
const agentsSrc = path.join(src, 'agents');
|
|
192
|
+
files.push(...collectFiles(agentsSrc, agentsSrc, 'agents'));
|
|
193
|
+
|
|
194
|
+
// scripts -> mindsystem/scripts
|
|
195
|
+
const scriptsSrc = path.join(src, 'scripts');
|
|
196
|
+
files.push(...collectFiles(scriptsSrc, scriptsSrc, 'mindsystem/scripts'));
|
|
197
|
+
|
|
198
|
+
// skills
|
|
199
|
+
const skillsSrc = path.join(src, 'skills');
|
|
200
|
+
files.push(...collectFiles(skillsSrc, skillsSrc, 'skills'));
|
|
201
|
+
|
|
202
|
+
// CHANGELOG.md -> mindsystem/CHANGELOG.md
|
|
203
|
+
const changelogSrc = path.join(src, 'CHANGELOG.md');
|
|
204
|
+
if (fs.existsSync(changelogSrc)) {
|
|
205
|
+
files.push({ relativePath: 'mindsystem/CHANGELOG.md', absolutePath: changelogSrc });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return files;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Compare manifests to detect orphans and conflicts
|
|
213
|
+
* @param {Object|null} oldManifest - Previous manifest or null for fresh install
|
|
214
|
+
* @param {string} claudeDir - Target directory
|
|
215
|
+
* @param {Array} newFiles - Files to install
|
|
216
|
+
* @param {string} pathPrefix - Path prefix for content replacement
|
|
217
|
+
* @returns {{orphans: string[], conflicts: Array<{relativePath: string, reason: string}>}}
|
|
218
|
+
*/
|
|
219
|
+
function compareManifests(oldManifest, claudeDir, newFiles, pathPrefix) {
|
|
220
|
+
const orphans = [];
|
|
221
|
+
const conflicts = [];
|
|
222
|
+
|
|
223
|
+
// Build set of new file paths
|
|
224
|
+
const newFilePaths = new Set(newFiles.map(f => f.relativePath));
|
|
225
|
+
|
|
226
|
+
// Find orphans (files in old manifest but not in new)
|
|
227
|
+
if (oldManifest && oldManifest.files) {
|
|
228
|
+
for (const oldPath of Object.keys(oldManifest.files)) {
|
|
229
|
+
if (!newFilePaths.has(oldPath)) {
|
|
230
|
+
const fullPath = path.join(claudeDir, oldPath);
|
|
231
|
+
if (fs.existsSync(fullPath)) {
|
|
232
|
+
orphans.push(oldPath);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Find conflicts (installed files modified by user)
|
|
239
|
+
if (oldManifest && oldManifest.files) {
|
|
240
|
+
for (const fileInfo of newFiles) {
|
|
241
|
+
const destPath = path.join(claudeDir, fileInfo.relativePath);
|
|
242
|
+
if (!fs.existsSync(destPath)) {
|
|
243
|
+
continue; // New file, no conflict
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const oldChecksum = oldManifest.files[fileInfo.relativePath];
|
|
247
|
+
if (!oldChecksum) {
|
|
248
|
+
continue; // File not in old manifest, treat as new
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Read current installed content
|
|
252
|
+
const installedContent = fs.readFileSync(destPath, 'utf8');
|
|
253
|
+
const installedChecksum = computeChecksum(installedContent);
|
|
254
|
+
|
|
255
|
+
// If installed file differs from what we last installed, it's been modified
|
|
256
|
+
if (installedChecksum !== oldChecksum) {
|
|
257
|
+
// Read source content with path replacement to compare
|
|
258
|
+
let sourceContent = fs.readFileSync(fileInfo.absolutePath, 'utf8');
|
|
259
|
+
if (fileInfo.absolutePath.endsWith('.md')) {
|
|
260
|
+
sourceContent = sourceContent.replace(/~\/\.claude\//g, pathPrefix);
|
|
261
|
+
}
|
|
262
|
+
const sourceChecksum = computeChecksum(sourceContent);
|
|
263
|
+
|
|
264
|
+
// Only conflict if source is also different (user modified AND we have changes)
|
|
265
|
+
if (sourceChecksum !== installedChecksum) {
|
|
266
|
+
conflicts.push({
|
|
267
|
+
relativePath: fileInfo.relativePath,
|
|
268
|
+
reason: 'locally modified'
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { orphans, conflicts };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Interactive conflict resolution
|
|
280
|
+
* @param {Array} conflicts - List of conflicting files
|
|
281
|
+
* @param {boolean} forceOverwrite - Skip prompts and overwrite all
|
|
282
|
+
* @returns {Promise<{overwrite: Set<string>, keep: Set<string>}>}
|
|
283
|
+
*/
|
|
284
|
+
async function resolveConflicts(conflicts, forceOverwrite) {
|
|
285
|
+
const overwrite = new Set();
|
|
286
|
+
const keep = new Set();
|
|
287
|
+
|
|
288
|
+
if (conflicts.length === 0) {
|
|
289
|
+
return { overwrite, keep };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (forceOverwrite) {
|
|
293
|
+
for (const c of conflicts) {
|
|
294
|
+
overwrite.add(c.relativePath);
|
|
295
|
+
}
|
|
296
|
+
console.log(` ${yellow}⚠${reset} Force overwriting ${conflicts.length} modified file(s)`);
|
|
297
|
+
return { overwrite, keep };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!isInteractive()) {
|
|
301
|
+
for (const c of conflicts) {
|
|
302
|
+
overwrite.add(c.relativePath);
|
|
303
|
+
}
|
|
304
|
+
console.log(` ${yellow}⚠${reset} Non-interactive mode: overwriting ${conflicts.length} modified file(s)`);
|
|
305
|
+
return { overwrite, keep };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const rl = readline.createInterface({
|
|
309
|
+
input: process.stdin,
|
|
310
|
+
output: process.stdout
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
314
|
+
|
|
315
|
+
console.log(`\n ${yellow}${conflicts.length} file(s) have local modifications:${reset}\n`);
|
|
316
|
+
|
|
317
|
+
let overwriteAll = false;
|
|
318
|
+
let keepAll = false;
|
|
319
|
+
|
|
320
|
+
for (const conflict of conflicts) {
|
|
321
|
+
if (overwriteAll) {
|
|
322
|
+
overwrite.add(conflict.relativePath);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (keepAll) {
|
|
326
|
+
keep.add(conflict.relativePath);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log(` ${dim}${conflict.relativePath}${reset}`);
|
|
331
|
+
const answer = await question(` [O]verwrite, [K]eep, [A]ll overwrite, [N]one keep? `);
|
|
332
|
+
|
|
333
|
+
switch (answer.toLowerCase().trim()) {
|
|
334
|
+
case 'o':
|
|
335
|
+
case 'overwrite':
|
|
336
|
+
overwrite.add(conflict.relativePath);
|
|
337
|
+
break;
|
|
338
|
+
case 'k':
|
|
339
|
+
case 'keep':
|
|
340
|
+
keep.add(conflict.relativePath);
|
|
341
|
+
break;
|
|
342
|
+
case 'a':
|
|
343
|
+
case 'all':
|
|
344
|
+
overwriteAll = true;
|
|
345
|
+
overwrite.add(conflict.relativePath);
|
|
346
|
+
break;
|
|
347
|
+
case 'n':
|
|
348
|
+
case 'none':
|
|
349
|
+
keepAll = true;
|
|
350
|
+
keep.add(conflict.relativePath);
|
|
351
|
+
break;
|
|
352
|
+
default:
|
|
353
|
+
// Default to overwrite
|
|
354
|
+
overwrite.add(conflict.relativePath);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
rl.close();
|
|
359
|
+
console.log('');
|
|
360
|
+
return { overwrite, keep };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Remove orphaned files and empty directories
|
|
365
|
+
* @param {string} claudeDir - Target directory
|
|
366
|
+
* @param {string[]} filesToRemove - Relative paths of files to remove
|
|
367
|
+
*/
|
|
368
|
+
function cleanupOrphanedFiles(claudeDir, filesToRemove) {
|
|
369
|
+
if (filesToRemove.length === 0) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const dirsToCheck = new Set();
|
|
374
|
+
|
|
375
|
+
for (const relativePath of filesToRemove) {
|
|
376
|
+
const fullPath = path.join(claudeDir, relativePath);
|
|
377
|
+
try {
|
|
378
|
+
if (fs.existsSync(fullPath)) {
|
|
379
|
+
fs.unlinkSync(fullPath);
|
|
380
|
+
console.log(` ${yellow}✗${reset} Removed ${relativePath}`);
|
|
381
|
+
// Track parent directories for cleanup
|
|
382
|
+
dirsToCheck.add(path.dirname(fullPath));
|
|
383
|
+
}
|
|
384
|
+
} catch (e) {
|
|
385
|
+
console.log(` ${yellow}⚠${reset} Failed to remove ${relativePath}: ${e.message}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Remove empty directories (deepest first)
|
|
390
|
+
const sortedDirs = Array.from(dirsToCheck).sort((a, b) => b.length - a.length);
|
|
391
|
+
for (const dir of sortedDirs) {
|
|
392
|
+
try {
|
|
393
|
+
// Don't remove the claudeDir itself or its immediate children (commands, agents, etc.)
|
|
394
|
+
if (dir === claudeDir || path.dirname(dir) === claudeDir) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const entries = fs.readdirSync(dir);
|
|
398
|
+
if (entries.length === 0) {
|
|
399
|
+
fs.rmdirSync(dir);
|
|
400
|
+
}
|
|
401
|
+
} catch (e) {
|
|
402
|
+
// Ignore errors (directory not empty or doesn't exist)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
102
407
|
/**
|
|
103
408
|
* Recursively copy directory, replacing paths in .md files
|
|
104
409
|
*/
|
|
@@ -144,10 +449,33 @@ function copyDir(srcDir, destDir) {
|
|
|
144
449
|
}
|
|
145
450
|
}
|
|
146
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Install a single file with path replacement
|
|
454
|
+
* @param {string} srcPath - Source file path
|
|
455
|
+
* @param {string} destPath - Destination file path
|
|
456
|
+
* @param {string} pathPrefix - Path prefix for .md file replacement
|
|
457
|
+
*/
|
|
458
|
+
function installFile(srcPath, destPath, pathPrefix) {
|
|
459
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
460
|
+
|
|
461
|
+
if (srcPath.endsWith('.md')) {
|
|
462
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
463
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
464
|
+
fs.writeFileSync(destPath, content);
|
|
465
|
+
} else {
|
|
466
|
+
fs.copyFileSync(srcPath, destPath);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Make shell scripts executable
|
|
470
|
+
if (srcPath.endsWith('.sh')) {
|
|
471
|
+
fs.chmodSync(destPath, '755');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
147
475
|
/**
|
|
148
476
|
* Install to the specified directory
|
|
149
477
|
*/
|
|
150
|
-
function install(isGlobal) {
|
|
478
|
+
async function install(isGlobal) {
|
|
151
479
|
const src = path.join(__dirname, '..');
|
|
152
480
|
// Priority: explicit --config-dir arg > CLAUDE_CONFIG_DIR env var > default ~/.claude
|
|
153
481
|
const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
|
|
@@ -168,85 +496,106 @@ function install(isGlobal) {
|
|
|
168
496
|
|
|
169
497
|
console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
|
|
170
498
|
|
|
171
|
-
//
|
|
172
|
-
const
|
|
173
|
-
|
|
499
|
+
// Phase 1: Read old manifest
|
|
500
|
+
const oldManifest = readManifest(claudeDir);
|
|
501
|
+
|
|
502
|
+
// Phase 2: Build list of files to install
|
|
503
|
+
const filesToInstall = buildInstallManifest(src);
|
|
504
|
+
|
|
505
|
+
// Phase 3: Compare and detect conflicts
|
|
506
|
+
const { orphans, conflicts } = compareManifests(oldManifest, claudeDir, filesToInstall, pathPrefix);
|
|
507
|
+
|
|
508
|
+
// Phase 4: Resolve conflicts interactively
|
|
509
|
+
const { overwrite, keep } = await resolveConflicts(conflicts, hasForce);
|
|
510
|
+
|
|
511
|
+
// Phase 5: Install files (skipping kept files)
|
|
512
|
+
const newManifestFiles = {};
|
|
513
|
+
const categories = {
|
|
514
|
+
'commands/ms': { count: 0, label: 'commands/ms' },
|
|
515
|
+
'mindsystem': { count: 0, label: 'mindsystem' },
|
|
516
|
+
'agents': { count: 0, label: 'agents' },
|
|
517
|
+
'mindsystem/scripts': { count: 0, label: 'scripts' },
|
|
518
|
+
'skills': { count: 0, label: 'skills' }
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
for (const fileInfo of filesToInstall) {
|
|
522
|
+
const destPath = path.join(claudeDir, fileInfo.relativePath);
|
|
523
|
+
|
|
524
|
+
// Skip files user wants to keep
|
|
525
|
+
if (keep.has(fileInfo.relativePath)) {
|
|
526
|
+
// Still need to track in manifest with current checksum
|
|
527
|
+
const installedContent = fs.readFileSync(destPath, 'utf8');
|
|
528
|
+
newManifestFiles[fileInfo.relativePath] = computeChecksum(installedContent);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
174
531
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const msDest = path.join(commandsDir, 'ms');
|
|
178
|
-
copyWithPathReplacement(msSrc, msDest, pathPrefix);
|
|
179
|
-
console.log(` ${green}✓${reset} Installed commands/ms`);
|
|
532
|
+
// Install the file
|
|
533
|
+
installFile(fileInfo.absolutePath, destPath, pathPrefix);
|
|
180
534
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
535
|
+
// Compute checksum of installed content (after path replacement)
|
|
536
|
+
let installedContent;
|
|
537
|
+
if (fileInfo.absolutePath.endsWith('.md')) {
|
|
538
|
+
installedContent = fs.readFileSync(fileInfo.absolutePath, 'utf8');
|
|
539
|
+
installedContent = installedContent.replace(/~\/\.claude\//g, pathPrefix);
|
|
540
|
+
} else {
|
|
541
|
+
installedContent = fs.readFileSync(destPath, 'utf8');
|
|
542
|
+
}
|
|
543
|
+
newManifestFiles[fileInfo.relativePath] = computeChecksum(installedContent);
|
|
186
544
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
545
|
+
// Track category counts
|
|
546
|
+
for (const prefix of Object.keys(categories)) {
|
|
547
|
+
if (fileInfo.relativePath.startsWith(prefix)) {
|
|
548
|
+
categories[prefix].count++;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
193
552
|
}
|
|
194
553
|
|
|
195
|
-
//
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
fs.mkdirSync(scriptsDest, { recursive: true });
|
|
200
|
-
const scriptEntries = fs.readdirSync(scriptsSrc, { withFileTypes: true });
|
|
201
|
-
for (const entry of scriptEntries) {
|
|
202
|
-
const srcPath = path.join(scriptsSrc, entry.name);
|
|
203
|
-
const destPath = path.join(scriptsDest, entry.name);
|
|
204
|
-
if (entry.isDirectory()) {
|
|
205
|
-
// Recursively copy directories (like ms-lookup/)
|
|
206
|
-
copyDir(srcPath, destPath);
|
|
207
|
-
} else {
|
|
208
|
-
fs.copyFileSync(srcPath, destPath);
|
|
209
|
-
// Make shell scripts executable
|
|
210
|
-
if (entry.name.endsWith('.sh')) {
|
|
211
|
-
fs.chmodSync(destPath, '755');
|
|
212
|
-
}
|
|
213
|
-
}
|
|
554
|
+
// Print install summaries
|
|
555
|
+
for (const [prefix, info] of Object.entries(categories)) {
|
|
556
|
+
if (info.count > 0) {
|
|
557
|
+
console.log(` ${green}✓${reset} Installed ${info.label}`);
|
|
214
558
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Phase 6: Write VERSION file
|
|
562
|
+
const versionDest = path.join(claudeDir, 'mindsystem', 'VERSION');
|
|
563
|
+
fs.writeFileSync(versionDest, pkg.version);
|
|
564
|
+
console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
|
|
565
|
+
|
|
566
|
+
// Phase 7: Check Python for ms-lookup
|
|
567
|
+
const msLookupPath = path.join(claudeDir, 'mindsystem', 'scripts', 'ms-lookup');
|
|
568
|
+
if (fs.existsSync(msLookupPath)) {
|
|
569
|
+
try {
|
|
570
|
+
const { execSync } = require('child_process');
|
|
571
|
+
const pyVersion = execSync('python3 --version 2>&1', { encoding: 'utf8' });
|
|
572
|
+
const versionMatch = pyVersion.match(/(\d+)\.(\d+)/);
|
|
573
|
+
if (versionMatch) {
|
|
574
|
+
const [, major, minor] = versionMatch;
|
|
575
|
+
if (parseInt(major) < 3 || (parseInt(major) === 3 && parseInt(minor) < 9)) {
|
|
576
|
+
console.log(` ${yellow}⚠${reset} Python 3.9+ required for ms-lookup (found ${major}.${minor})`);
|
|
577
|
+
} else {
|
|
578
|
+
console.log(` ${green}✓${reset} Installed ms-lookup CLI (Python ${major}.${minor})`);
|
|
231
579
|
}
|
|
232
|
-
} catch (e) {
|
|
233
|
-
console.log(` ${yellow}⚠${reset} Python not found - ms-lookup CLI requires Python 3.9+`);
|
|
234
580
|
}
|
|
581
|
+
} catch (e) {
|
|
582
|
+
console.log(` ${yellow}⚠${reset} Python not found - ms-lookup CLI requires Python 3.9+`);
|
|
235
583
|
}
|
|
236
584
|
}
|
|
237
585
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
fs.copyFileSync(changelogSrc, changelogDest);
|
|
243
|
-
console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
|
|
586
|
+
// Phase 8: Cleanup orphaned files
|
|
587
|
+
if (orphans.length > 0) {
|
|
588
|
+
console.log('');
|
|
589
|
+
cleanupOrphanedFiles(claudeDir, orphans);
|
|
244
590
|
}
|
|
245
591
|
|
|
246
|
-
//
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
592
|
+
// Phase 9: Write new manifest
|
|
593
|
+
const newManifest = {
|
|
594
|
+
version: pkg.version,
|
|
595
|
+
installedAt: new Date().toISOString(),
|
|
596
|
+
files: newManifestFiles
|
|
597
|
+
};
|
|
598
|
+
writeManifest(claudeDir, newManifest);
|
|
250
599
|
|
|
251
600
|
console.log(`
|
|
252
601
|
${green}Done!${reset} Launch Claude Code and run ${cyan}/ms:help${reset}.
|
|
@@ -256,7 +605,7 @@ function install(isGlobal) {
|
|
|
256
605
|
/**
|
|
257
606
|
* Prompt for install location
|
|
258
607
|
*/
|
|
259
|
-
function promptLocation() {
|
|
608
|
+
async function promptLocation() {
|
|
260
609
|
const rl = readline.createInterface({
|
|
261
610
|
input: process.stdin,
|
|
262
611
|
output: process.stdout
|
|
@@ -272,25 +621,35 @@ function promptLocation() {
|
|
|
272
621
|
${cyan}2${reset}) Local ${dim}(./.claude)${reset} - this project only
|
|
273
622
|
`);
|
|
274
623
|
|
|
275
|
-
|
|
276
|
-
rl.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
624
|
+
return new Promise((resolve) => {
|
|
625
|
+
rl.question(` Choice ${dim}[1]${reset}: `, async (answer) => {
|
|
626
|
+
rl.close();
|
|
627
|
+
const choice = answer.trim() || '1';
|
|
628
|
+
const isGlobal = choice !== '2';
|
|
629
|
+
await install(isGlobal);
|
|
630
|
+
resolve();
|
|
631
|
+
});
|
|
280
632
|
});
|
|
281
633
|
}
|
|
282
634
|
|
|
283
635
|
// Main
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
636
|
+
async function main() {
|
|
637
|
+
if (hasGlobal && hasLocal) {
|
|
638
|
+
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
} else if (explicitConfigDir && hasLocal) {
|
|
641
|
+
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
} else if (hasGlobal) {
|
|
644
|
+
await install(true);
|
|
645
|
+
} else if (hasLocal) {
|
|
646
|
+
await install(false);
|
|
647
|
+
} else {
|
|
648
|
+
await promptLocation();
|
|
649
|
+
}
|
|
296
650
|
}
|
|
651
|
+
|
|
652
|
+
main().catch((err) => {
|
|
653
|
+
console.error(` ${yellow}Error: ${err.message}${reset}`);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
});
|