maxsimcli 3.11.0 → 3.12.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 (102) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/assets/CHANGELOG.md +19 -0
  3. package/dist/assets/dashboard/client/assets/index-CxFKStBk.css +32 -0
  4. package/dist/assets/dashboard/client/assets/{index-CZ8WC97G.js → index-wtQDvXzr.js} +64 -64
  5. package/dist/assets/dashboard/client/index.html +2 -2
  6. package/dist/assets/templates/agents/AGENTS.md +82 -0
  7. package/dist/assets/templates/commands/maxsim/settings.md +1 -1
  8. package/dist/assets/templates/skills/code-review/SKILL.md +151 -0
  9. package/dist/assets/templates/skills/memory-management/SKILL.md +174 -0
  10. package/dist/assets/templates/skills/simplify/SKILL.md +137 -0
  11. package/dist/assets/templates/skills/using-maxsim/SKILL.md +115 -0
  12. package/dist/assets/templates/templates/config.json +1 -1
  13. package/dist/assets/templates/workflows/add-tests.md +3 -3
  14. package/dist/assets/templates/workflows/complete-milestone.md +1 -1
  15. package/dist/assets/templates/workflows/execute-phase.md +4 -14
  16. package/dist/assets/templates/workflows/init-existing.md +7 -3
  17. package/dist/assets/templates/workflows/new-milestone.md +4 -0
  18. package/dist/assets/templates/workflows/new-project.md +6 -2
  19. package/dist/assets/templates/workflows/plan-phase.md +2 -2
  20. package/dist/assets/templates/workflows/settings.md +8 -4
  21. package/dist/assets/templates/workflows/verify-work.md +1 -1
  22. package/dist/cli.cjs +265 -161
  23. package/dist/cli.cjs.map +1 -1
  24. package/dist/cli.js +73 -204
  25. package/dist/cli.js.map +1 -1
  26. package/dist/core/commands.d.ts.map +1 -1
  27. package/dist/core/commands.js +4 -1
  28. package/dist/core/commands.js.map +1 -1
  29. package/dist/core/core.d.ts +18 -0
  30. package/dist/core/core.d.ts.map +1 -1
  31. package/dist/core/core.js +43 -13
  32. package/dist/core/core.js.map +1 -1
  33. package/dist/core/dashboard-launcher.d.ts +56 -0
  34. package/dist/core/dashboard-launcher.d.ts.map +1 -0
  35. package/dist/core/dashboard-launcher.js +243 -0
  36. package/dist/core/dashboard-launcher.js.map +1 -0
  37. package/dist/core/index.d.ts +3 -1
  38. package/dist/core/index.d.ts.map +1 -1
  39. package/dist/core/index.js +20 -2
  40. package/dist/core/index.js.map +1 -1
  41. package/dist/core/init.d.ts +0 -1
  42. package/dist/core/init.d.ts.map +1 -1
  43. package/dist/core/init.js +0 -1
  44. package/dist/core/init.js.map +1 -1
  45. package/dist/core/phase.d.ts.map +1 -1
  46. package/dist/core/phase.js +7 -1
  47. package/dist/core/phase.js.map +1 -1
  48. package/dist/core/roadmap.d.ts.map +1 -1
  49. package/dist/core/roadmap.js +1 -0
  50. package/dist/core/roadmap.js.map +1 -1
  51. package/dist/core/state.d.ts.map +1 -1
  52. package/dist/core/state.js +7 -5
  53. package/dist/core/state.js.map +1 -1
  54. package/dist/core/types.d.ts +1 -2
  55. package/dist/core/types.d.ts.map +1 -1
  56. package/dist/core/types.js +1 -2
  57. package/dist/core/types.js.map +1 -1
  58. package/dist/install/adapters.d.ts +15 -0
  59. package/dist/install/adapters.d.ts.map +1 -0
  60. package/dist/install/adapters.js +203 -0
  61. package/dist/install/adapters.js.map +1 -0
  62. package/dist/install/copy.d.ts +15 -0
  63. package/dist/install/copy.d.ts.map +1 -0
  64. package/dist/install/copy.js +191 -0
  65. package/dist/install/copy.js.map +1 -0
  66. package/dist/install/dashboard.d.ts +16 -0
  67. package/dist/install/dashboard.d.ts.map +1 -0
  68. package/dist/install/dashboard.js +273 -0
  69. package/dist/install/dashboard.js.map +1 -0
  70. package/dist/install/hooks.d.ts +32 -0
  71. package/dist/install/hooks.d.ts.map +1 -0
  72. package/dist/install/hooks.js +285 -0
  73. package/dist/install/hooks.js.map +1 -0
  74. package/dist/install/index.d.ts +2 -0
  75. package/dist/install/index.d.ts.map +1 -0
  76. package/dist/install/index.js +598 -0
  77. package/dist/install/index.js.map +1 -0
  78. package/dist/install/manifest.d.ts +20 -0
  79. package/dist/install/manifest.d.ts.map +1 -0
  80. package/dist/install/manifest.js +135 -0
  81. package/dist/install/manifest.js.map +1 -0
  82. package/dist/install/patches.d.ts +11 -0
  83. package/dist/install/patches.d.ts.map +1 -0
  84. package/dist/install/patches.js +136 -0
  85. package/dist/install/patches.js.map +1 -0
  86. package/dist/install/shared.d.ts +50 -0
  87. package/dist/install/shared.d.ts.map +1 -0
  88. package/dist/install/shared.js +142 -0
  89. package/dist/install/shared.js.map +1 -0
  90. package/dist/install/uninstall.d.ts +6 -0
  91. package/dist/install/uninstall.d.ts.map +1 -0
  92. package/dist/install/uninstall.js +280 -0
  93. package/dist/install/uninstall.js.map +1 -0
  94. package/dist/install.cjs +763 -709
  95. package/dist/install.cjs.map +1 -1
  96. package/dist/mcp-server.cjs.map +1 -1
  97. package/package.json +1 -1
  98. package/dist/assets/dashboard/client/assets/index-DzJChB-D.css +0 -32
  99. package/dist/install.d.ts +0 -2
  100. package/dist/install.d.ts.map +0 -1
  101. package/dist/install.js +0 -1841
  102. package/dist/install.js.map +0 -1
package/dist/install.js DELETED
@@ -1,1841 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- const fs = __importStar(require("node:fs"));
40
- const path = __importStar(require("node:path"));
41
- const os = __importStar(require("node:os"));
42
- const crypto = __importStar(require("node:crypto"));
43
- const node_child_process_1 = require("node:child_process");
44
- const fs_extra_1 = __importDefault(require("fs-extra"));
45
- const chalk_1 = __importDefault(require("chalk"));
46
- const figlet_1 = __importDefault(require("figlet"));
47
- const ora_1 = __importDefault(require("ora"));
48
- const prompts_1 = require("@inquirer/prompts");
49
- const minimist_1 = __importDefault(require("minimist"));
50
- const index_js_1 = require("./adapters/index.js");
51
- // Get version from package.json — read at runtime so semantic-release's version bump
52
- // is reflected without needing to rebuild dist/install.cjs after the version bump.
53
- const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8'));
54
- // Resolve template asset root — bundled into dist/assets/templates at publish time
55
- const templatesRoot = path.resolve(__dirname, 'assets', 'templates');
56
- // Parse args
57
- const args = process.argv.slice(2);
58
- const argv = (0, minimist_1.default)(args, {
59
- boolean: ['global', 'local', 'opencode', 'claude', 'gemini', 'codex', 'both', 'all', 'uninstall', 'help', 'version', 'force-statusline', 'network'],
60
- string: ['config-dir'],
61
- alias: { g: 'global', l: 'local', u: 'uninstall', h: 'help', c: 'config-dir' },
62
- });
63
- const hasGlobal = !!argv['global'];
64
- const hasLocal = !!argv['local'];
65
- const hasOpencode = !!argv['opencode'];
66
- const hasClaude = !!argv['claude'];
67
- const hasGemini = !!argv['gemini'];
68
- const hasCodex = !!argv['codex'];
69
- const hasBoth = !!argv['both']; // Legacy flag, keeps working
70
- const hasAll = !!argv['all'];
71
- const hasUninstall = !!argv['uninstall'];
72
- // Runtime selection - can be set by flags or interactive prompt
73
- let selectedRuntimes = [];
74
- if (hasAll) {
75
- selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex'];
76
- }
77
- else if (hasBoth) {
78
- selectedRuntimes = ['claude', 'opencode'];
79
- }
80
- else {
81
- if (hasOpencode)
82
- selectedRuntimes.push('opencode');
83
- if (hasClaude)
84
- selectedRuntimes.push('claude');
85
- if (hasGemini)
86
- selectedRuntimes.push('gemini');
87
- if (hasCodex)
88
- selectedRuntimes.push('codex');
89
- }
90
- /**
91
- * Add a firewall rule to allow inbound traffic on the given port.
92
- * Handles Windows (netsh), Linux (ufw / iptables), and macOS (no rule needed).
93
- */
94
- /** Check whether the current process is running with admin/root privileges. */
95
- function isElevated() {
96
- if (process.platform === 'win32') {
97
- try {
98
- (0, node_child_process_1.execSync)('net session', { stdio: 'pipe' });
99
- return true;
100
- }
101
- catch {
102
- return false;
103
- }
104
- }
105
- // Linux / macOS: check if uid is 0
106
- return process.getuid?.() === 0;
107
- }
108
- function applyFirewallRule(port) {
109
- const platform = process.platform;
110
- try {
111
- if (platform === 'win32') {
112
- const cmd = `netsh advfirewall firewall add rule name="MAXSIM Dashboard" dir=in action=allow protocol=TCP localport=${port}`;
113
- if (isElevated()) {
114
- (0, node_child_process_1.execSync)(cmd, { stdio: 'pipe' });
115
- console.log(chalk_1.default.green(' ✓ Windows Firewall rule added for port ' + port));
116
- }
117
- else {
118
- // Trigger UAC elevation via PowerShell — this opens the Windows UAC dialog
119
- console.log(chalk_1.default.gray(' Requesting administrator privileges for firewall rule...'));
120
- const psCmd = `Start-Process cmd -ArgumentList '/c ${cmd}' -Verb RunAs -Wait`;
121
- (0, node_child_process_1.execSync)(`powershell -NoProfile -Command "${psCmd}"`, { stdio: 'pipe' });
122
- console.log(chalk_1.default.green(' ✓ Windows Firewall rule added for port ' + port));
123
- }
124
- }
125
- else if (platform === 'linux') {
126
- const sudoPrefix = isElevated() ? '' : 'sudo ';
127
- try {
128
- (0, node_child_process_1.execSync)(`${sudoPrefix}ufw allow ${port}/tcp`, { stdio: 'pipe' });
129
- console.log(chalk_1.default.green(' ✓ UFW rule added for port ' + port));
130
- }
131
- catch {
132
- try {
133
- (0, node_child_process_1.execSync)(`${sudoPrefix}iptables -A INPUT -p tcp --dport ${port} -j ACCEPT`, { stdio: 'pipe' });
134
- console.log(chalk_1.default.green(' ✓ iptables rule added for port ' + port));
135
- }
136
- catch {
137
- console.log(chalk_1.default.yellow(` ⚠ Could not add firewall rule automatically. Run: sudo ufw allow ${port}/tcp`));
138
- }
139
- }
140
- }
141
- else if (platform === 'darwin') {
142
- // macOS does not block inbound connections by default — no rule needed
143
- console.log(chalk_1.default.gray(' macOS: No firewall rule needed (inbound connections are allowed by default)'));
144
- }
145
- }
146
- catch (err) {
147
- console.warn(chalk_1.default.yellow(` ⚠ Firewall rule failed: ${err.message}`));
148
- console.warn(chalk_1.default.gray(` You may need to manually allow port ${port} through your firewall.`));
149
- }
150
- }
151
- /**
152
- * Walk up from cwd to find the MAXSIM monorepo root (has packages/dashboard/src/server.ts)
153
- */
154
- function findMonorepoRoot(startDir) {
155
- let dir = startDir;
156
- for (let i = 0; i < 10; i++) {
157
- if (fs.existsSync(path.join(dir, 'packages', 'dashboard', 'src', 'server.ts'))) {
158
- return dir;
159
- }
160
- const parent = path.dirname(dir);
161
- if (parent === dir)
162
- break;
163
- dir = parent;
164
- }
165
- return null;
166
- }
167
- /**
168
- * Adapter registry keyed by runtime name
169
- */
170
- const adapterMap = {
171
- claude: index_js_1.claudeAdapter,
172
- opencode: index_js_1.opencodeAdapter,
173
- gemini: index_js_1.geminiAdapter,
174
- codex: index_js_1.codexAdapter,
175
- };
176
- /**
177
- * Get adapter for a runtime
178
- */
179
- function getAdapter(runtime) {
180
- return adapterMap[runtime];
181
- }
182
- /**
183
- * Get the global config directory for a runtime, using adapter
184
- */
185
- function getGlobalDir(runtime, explicitDir = null) {
186
- return getAdapter(runtime).getGlobalDir(explicitDir);
187
- }
188
- /**
189
- * Get the config directory path relative to home for hook templating
190
- */
191
- function getConfigDirFromHome(runtime, isGlobal) {
192
- return getAdapter(runtime).getConfigDirFromHome(isGlobal);
193
- }
194
- /**
195
- * Get the local directory name for a runtime
196
- */
197
- function getDirName(runtime) {
198
- return getAdapter(runtime).dirName;
199
- }
200
- /**
201
- * Recursively remove a directory, handling Windows read-only file attributes.
202
- * fs-extra handles cross-platform edge cases (EPERM on Windows, symlinks, etc.)
203
- */
204
- function safeRmDir(dirPath) {
205
- fs_extra_1.default.removeSync(dirPath);
206
- }
207
- /**
208
- * Recursively copy a directory (dereferences symlinks)
209
- */
210
- function copyDirRecursive(src, dest) {
211
- fs_extra_1.default.copySync(src, dest, { dereference: true });
212
- }
213
- /**
214
- * Get the global config directory for OpenCode (for JSONC permissions)
215
- * OpenCode follows XDG Base Directory spec
216
- */
217
- function getOpencodeGlobalDir() {
218
- return index_js_1.opencodeAdapter.getGlobalDir();
219
- }
220
- const banner = '\n' +
221
- chalk_1.default.cyan(figlet_1.default.textSync('MAXSIM', { font: 'ANSI Shadow' })
222
- .split('\n')
223
- .map((line) => ' ' + line)
224
- .join('\n')) +
225
- '\n' +
226
- '\n' +
227
- ' MAXSIM ' +
228
- chalk_1.default.dim('v' + pkg.version) +
229
- '\n' +
230
- ' A meta-prompting, context engineering and spec-driven\n' +
231
- ' development system for Claude Code, OpenCode, Gemini, and Codex.\n';
232
- // Parse --config-dir argument (minimist handles --config-dir VALUE and --config-dir=VALUE)
233
- const explicitConfigDir = argv['config-dir'] || null;
234
- const hasHelp = !!argv['help'];
235
- const hasVersion = !!argv['version'];
236
- const forceStatusline = !!argv['force-statusline'];
237
- // Show version if requested (before banner for clean output)
238
- if (hasVersion) {
239
- console.log(pkg.version);
240
- process.exit(0);
241
- }
242
- console.log(banner);
243
- // Show help if requested
244
- if (hasHelp) {
245
- console.log(` ${chalk_1.default.yellow('Usage:')} npx maxsimcli [options]\n\n ${chalk_1.default.yellow('Options:')}\n ${chalk_1.default.cyan('-g, --global')} Install globally (to config directory)\n ${chalk_1.default.cyan('-l, --local')} Install locally (to current directory)\n ${chalk_1.default.cyan('--claude')} Install for Claude Code only\n ${chalk_1.default.cyan('--opencode')} Install for OpenCode only\n ${chalk_1.default.cyan('--gemini')} Install for Gemini only\n ${chalk_1.default.cyan('--codex')} Install for Codex only\n ${chalk_1.default.cyan('--all')} Install for all runtimes\n ${chalk_1.default.cyan('-u, --uninstall')} Uninstall MAXSIM (remove all MAXSIM files)\n ${chalk_1.default.cyan('-c, --config-dir <path>')} Specify custom config directory\n ${chalk_1.default.cyan('-h, --help')} Show this help message\n ${chalk_1.default.cyan('--force-statusline')} Replace existing statusline config\n\n ${chalk_1.default.yellow('Examples:')}\n ${chalk_1.default.dim('# Interactive install (prompts for runtime and location)')}\n npx maxsimcli\n\n ${chalk_1.default.dim('# Install for Claude Code globally')}\n npx maxsimcli --claude --global\n\n ${chalk_1.default.dim('# Install for Gemini globally')}\n npx maxsimcli --gemini --global\n\n ${chalk_1.default.dim('# Install for Codex globally')}\n npx maxsimcli --codex --global\n\n ${chalk_1.default.dim('# Install for all runtimes globally')}\n npx maxsimcli --all --global\n\n ${chalk_1.default.dim('# Install to custom config directory')}\n npx maxsimcli --codex --global --config-dir ~/.codex-work\n\n ${chalk_1.default.dim('# Install to current project only')}\n npx maxsimcli --claude --local\n\n ${chalk_1.default.dim('# Uninstall MAXSIM from Codex globally')}\n npx maxsimcli --codex --global --uninstall\n\n ${chalk_1.default.yellow('Notes:')}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME environment variables.\n`);
246
- process.exit(0);
247
- }
248
- // Cache for attribution settings (populated once per runtime during install)
249
- const attributionCache = new Map();
250
- /**
251
- * Get commit attribution setting for a runtime
252
- * @returns null = remove, undefined = keep default, string = custom
253
- */
254
- function getCommitAttribution(runtime) {
255
- if (attributionCache.has(runtime)) {
256
- return attributionCache.get(runtime);
257
- }
258
- let result;
259
- if (runtime === 'opencode') {
260
- const config = (0, index_js_1.readSettings)(path.join(getGlobalDir('opencode', null), 'opencode.json'));
261
- result =
262
- config.disable_ai_attribution === true
263
- ? null
264
- : undefined;
265
- }
266
- else if (runtime === 'gemini') {
267
- const settings = (0, index_js_1.readSettings)(path.join(getGlobalDir('gemini', explicitConfigDir), 'settings.json'));
268
- const attr = settings.attribution;
269
- if (!attr || attr.commit === undefined) {
270
- result = undefined;
271
- }
272
- else if (attr.commit === '') {
273
- result = null;
274
- }
275
- else {
276
- result = attr.commit;
277
- }
278
- }
279
- else if (runtime === 'claude') {
280
- const settings = (0, index_js_1.readSettings)(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json'));
281
- const attr = settings.attribution;
282
- if (!attr || attr.commit === undefined) {
283
- result = undefined;
284
- }
285
- else if (attr.commit === '') {
286
- result = null;
287
- }
288
- else {
289
- result = attr.commit;
290
- }
291
- }
292
- else {
293
- result = undefined;
294
- }
295
- attributionCache.set(runtime, result);
296
- return result;
297
- }
298
- /**
299
- * Copy commands to a flat structure for OpenCode
300
- * OpenCode expects: command/maxsim-help.md (invoked as /maxsim-help)
301
- * Source structure: commands/maxsim/help.md
302
- */
303
- function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
304
- if (!fs.existsSync(srcDir)) {
305
- return;
306
- }
307
- if (fs.existsSync(destDir)) {
308
- for (const file of fs.readdirSync(destDir)) {
309
- if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
310
- fs.unlinkSync(path.join(destDir, file));
311
- }
312
- }
313
- }
314
- else {
315
- fs.mkdirSync(destDir, { recursive: true });
316
- }
317
- const entries = fs.readdirSync(srcDir, { withFileTypes: true });
318
- for (const entry of entries) {
319
- const srcPath = path.join(srcDir, entry.name);
320
- if (entry.isDirectory()) {
321
- copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
322
- }
323
- else if (entry.name.endsWith('.md')) {
324
- const baseName = entry.name.replace('.md', '');
325
- const destName = `${prefix}-${baseName}.md`;
326
- const destPath = path.join(destDir, destName);
327
- let content = fs.readFileSync(srcPath, 'utf8');
328
- const globalClaudeRegex = /~\/\.claude\//g;
329
- const localClaudeRegex = /\.\/\.claude\//g;
330
- const opencodeDirRegex = /~\/\.opencode\//g;
331
- content = content.replace(globalClaudeRegex, pathPrefix);
332
- content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
333
- content = content.replace(opencodeDirRegex, pathPrefix);
334
- content = (0, index_js_1.processAttribution)(content, getCommitAttribution(runtime));
335
- content = (0, index_js_1.convertClaudeToOpencodeFrontmatter)(content);
336
- fs.writeFileSync(destPath, content);
337
- }
338
- }
339
- }
340
- function listCodexSkillNames(skillsDir, prefix = 'maxsim-') {
341
- if (!fs.existsSync(skillsDir))
342
- return [];
343
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
344
- return entries
345
- .filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix))
346
- .filter((entry) => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
347
- .map((entry) => entry.name)
348
- .sort();
349
- }
350
- function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
351
- if (!fs.existsSync(srcDir)) {
352
- return;
353
- }
354
- fs.mkdirSync(skillsDir, { recursive: true });
355
- const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
356
- for (const entry of existing) {
357
- if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
358
- fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
359
- }
360
- }
361
- function recurse(currentSrcDir, currentPrefix) {
362
- const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
363
- for (const entry of entries) {
364
- const srcPath = path.join(currentSrcDir, entry.name);
365
- if (entry.isDirectory()) {
366
- recurse(srcPath, `${currentPrefix}-${entry.name}`);
367
- continue;
368
- }
369
- if (!entry.name.endsWith('.md')) {
370
- continue;
371
- }
372
- const baseName = entry.name.replace('.md', '');
373
- const skillName = `${currentPrefix}-${baseName}`;
374
- const skillDir = path.join(skillsDir, skillName);
375
- fs.mkdirSync(skillDir, { recursive: true });
376
- let content = fs.readFileSync(srcPath, 'utf8');
377
- const globalClaudeRegex = /~\/\.claude\//g;
378
- const localClaudeRegex = /\.\/\.claude\//g;
379
- const codexDirRegex = /~\/\.codex\//g;
380
- content = content.replace(globalClaudeRegex, pathPrefix);
381
- content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
382
- content = content.replace(codexDirRegex, pathPrefix);
383
- content = (0, index_js_1.processAttribution)(content, getCommitAttribution(runtime));
384
- content = (0, index_js_1.convertClaudeCommandToCodexSkill)(content, skillName);
385
- fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
386
- }
387
- }
388
- recurse(srcDir, prefix);
389
- }
390
- /**
391
- * Recursively copy directory, replacing paths in .md files
392
- * Deletes existing destDir first to remove orphaned files from previous versions
393
- */
394
- function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false) {
395
- const isOpencode = runtime === 'opencode';
396
- const isCodex = runtime === 'codex';
397
- const dirName = getDirName(runtime);
398
- if (fs.existsSync(destDir)) {
399
- fs.rmSync(destDir, { recursive: true });
400
- }
401
- fs.mkdirSync(destDir, { recursive: true });
402
- const entries = fs.readdirSync(srcDir, { withFileTypes: true });
403
- for (const entry of entries) {
404
- const srcPath = path.join(srcDir, entry.name);
405
- const destPath = path.join(destDir, entry.name);
406
- if (entry.isDirectory()) {
407
- copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand);
408
- }
409
- else if (entry.name.endsWith('.md')) {
410
- let content = fs.readFileSync(srcPath, 'utf8');
411
- const globalClaudeRegex = /~\/\.claude\//g;
412
- const localClaudeRegex = /\.\/\.claude\//g;
413
- content = content.replace(globalClaudeRegex, pathPrefix);
414
- content = content.replace(localClaudeRegex, `./${dirName}/`);
415
- content = (0, index_js_1.processAttribution)(content, getCommitAttribution(runtime));
416
- if (isOpencode) {
417
- content = (0, index_js_1.convertClaudeToOpencodeFrontmatter)(content);
418
- fs.writeFileSync(destPath, content);
419
- }
420
- else if (runtime === 'gemini') {
421
- if (isCommand) {
422
- content = (0, index_js_1.stripSubTags)(content);
423
- const tomlContent = (0, index_js_1.convertClaudeToGeminiToml)(content);
424
- const tomlPath = destPath.replace(/\.md$/, '.toml');
425
- fs.writeFileSync(tomlPath, tomlContent);
426
- }
427
- else {
428
- fs.writeFileSync(destPath, content);
429
- }
430
- }
431
- else if (isCodex) {
432
- content = (0, index_js_1.convertClaudeToCodexMarkdown)(content);
433
- fs.writeFileSync(destPath, content);
434
- }
435
- else {
436
- fs.writeFileSync(destPath, content);
437
- }
438
- }
439
- else {
440
- fs.copyFileSync(srcPath, destPath);
441
- }
442
- }
443
- }
444
- /**
445
- * Clean up orphaned files from previous MAXSIM versions
446
- */
447
- function cleanupOrphanedFiles(configDir) {
448
- const orphanedFiles = [
449
- 'hooks/maxsim-notify.sh',
450
- 'hooks/statusline.js',
451
- ];
452
- for (const relPath of orphanedFiles) {
453
- const fullPath = path.join(configDir, relPath);
454
- if (fs.existsSync(fullPath)) {
455
- fs.unlinkSync(fullPath);
456
- console.log(` ${chalk_1.default.green('\u2713')} Removed orphaned ${relPath}`);
457
- }
458
- }
459
- }
460
- /**
461
- * Clean up orphaned hook registrations from settings.json
462
- */
463
- function cleanupOrphanedHooks(settings) {
464
- const orphanedHookPatterns = [
465
- 'maxsim-notify.sh',
466
- 'hooks/statusline.js',
467
- 'maxsim-intel-index.js',
468
- 'maxsim-intel-session.js',
469
- 'maxsim-intel-prune.js',
470
- ];
471
- let cleanedHooks = false;
472
- const hooks = settings.hooks;
473
- if (hooks) {
474
- for (const eventType of Object.keys(hooks)) {
475
- const hookEntries = hooks[eventType];
476
- if (Array.isArray(hookEntries)) {
477
- const filtered = hookEntries.filter((entry) => {
478
- if (entry.hooks && Array.isArray(entry.hooks)) {
479
- const hasOrphaned = entry.hooks.some((h) => h.command &&
480
- orphanedHookPatterns.some((pattern) => h.command.includes(pattern)));
481
- if (hasOrphaned) {
482
- cleanedHooks = true;
483
- return false;
484
- }
485
- }
486
- return true;
487
- });
488
- hooks[eventType] = filtered;
489
- }
490
- }
491
- }
492
- if (cleanedHooks) {
493
- console.log(` ${chalk_1.default.green('\u2713')} Removed orphaned hook registrations`);
494
- }
495
- const statusLine = settings.statusLine;
496
- if (statusLine &&
497
- statusLine.command &&
498
- statusLine.command.includes('statusline.js') &&
499
- !statusLine.command.includes('maxsim-statusline.js')) {
500
- statusLine.command = statusLine.command.replace(/statusline\.js/, 'maxsim-statusline.js');
501
- console.log(` ${chalk_1.default.green('\u2713')} Updated statusline path (statusline.js \u2192 maxsim-statusline.js)`);
502
- }
503
- return settings;
504
- }
505
- /**
506
- * Uninstall MAXSIM from the specified directory for a specific runtime
507
- */
508
- function uninstall(isGlobal, runtime = 'claude') {
509
- const isOpencode = runtime === 'opencode';
510
- const isCodex = runtime === 'codex';
511
- const dirName = getDirName(runtime);
512
- const targetDir = isGlobal
513
- ? getGlobalDir(runtime, explicitConfigDir)
514
- : path.join(process.cwd(), dirName);
515
- const locationLabel = isGlobal
516
- ? targetDir.replace(os.homedir(), '~')
517
- : targetDir.replace(process.cwd(), '.');
518
- let runtimeLabel = 'Claude Code';
519
- if (runtime === 'opencode')
520
- runtimeLabel = 'OpenCode';
521
- if (runtime === 'gemini')
522
- runtimeLabel = 'Gemini';
523
- if (runtime === 'codex')
524
- runtimeLabel = 'Codex';
525
- console.log(` Uninstalling MAXSIM from ${chalk_1.default.cyan(runtimeLabel)} at ${chalk_1.default.cyan(locationLabel)}\n`);
526
- if (!fs.existsSync(targetDir)) {
527
- console.log(` ${chalk_1.default.yellow('\u26a0')} Directory does not exist: ${locationLabel}`);
528
- console.log(` Nothing to uninstall.\n`);
529
- return;
530
- }
531
- let removedCount = 0;
532
- // 1. Remove MAXSIM commands/skills
533
- if (isOpencode) {
534
- const commandDir = path.join(targetDir, 'command');
535
- if (fs.existsSync(commandDir)) {
536
- const files = fs.readdirSync(commandDir);
537
- for (const file of files) {
538
- if (file.startsWith('maxsim-') && file.endsWith('.md')) {
539
- fs.unlinkSync(path.join(commandDir, file));
540
- removedCount++;
541
- }
542
- }
543
- console.log(` ${chalk_1.default.green('\u2713')} Removed MAXSIM commands from command/`);
544
- }
545
- }
546
- else if (isCodex) {
547
- const skillsDir = path.join(targetDir, 'skills');
548
- if (fs.existsSync(skillsDir)) {
549
- let skillCount = 0;
550
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
551
- for (const entry of entries) {
552
- if (entry.isDirectory() && entry.name.startsWith('maxsim-')) {
553
- fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
554
- skillCount++;
555
- }
556
- }
557
- if (skillCount > 0) {
558
- removedCount++;
559
- console.log(` ${chalk_1.default.green('\u2713')} Removed ${skillCount} Codex skills`);
560
- }
561
- }
562
- }
563
- else {
564
- const maxsimCommandsDir = path.join(targetDir, 'commands', 'maxsim');
565
- if (fs.existsSync(maxsimCommandsDir)) {
566
- fs.rmSync(maxsimCommandsDir, { recursive: true });
567
- removedCount++;
568
- console.log(` ${chalk_1.default.green('\u2713')} Removed commands/maxsim/`);
569
- }
570
- }
571
- // 2. Remove maxsim directory
572
- const maxsimDir = path.join(targetDir, 'maxsim');
573
- if (fs.existsSync(maxsimDir)) {
574
- fs.rmSync(maxsimDir, { recursive: true });
575
- removedCount++;
576
- console.log(` ${chalk_1.default.green('\u2713')} Removed maxsim/`);
577
- }
578
- // 3. Remove MAXSIM agents
579
- const agentsDir = path.join(targetDir, 'agents');
580
- if (fs.existsSync(agentsDir)) {
581
- const files = fs.readdirSync(agentsDir);
582
- let agentCount = 0;
583
- for (const file of files) {
584
- if (file.startsWith('maxsim-') && file.endsWith('.md')) {
585
- fs.unlinkSync(path.join(agentsDir, file));
586
- agentCount++;
587
- }
588
- }
589
- if (agentCount > 0) {
590
- removedCount++;
591
- console.log(` ${chalk_1.default.green('\u2713')} Removed ${agentCount} MAXSIM agents`);
592
- }
593
- }
594
- // 4. Remove MAXSIM hooks
595
- const hooksDir = path.join(targetDir, 'hooks');
596
- if (fs.existsSync(hooksDir)) {
597
- const maxsimHooks = [
598
- 'maxsim-statusline.js',
599
- 'maxsim-check-update.js',
600
- 'maxsim-check-update.sh',
601
- 'maxsim-context-monitor.js',
602
- ];
603
- let hookCount = 0;
604
- for (const hook of maxsimHooks) {
605
- const hookPath = path.join(hooksDir, hook);
606
- if (fs.existsSync(hookPath)) {
607
- fs.unlinkSync(hookPath);
608
- hookCount++;
609
- }
610
- }
611
- if (hookCount > 0) {
612
- removedCount++;
613
- console.log(` ${chalk_1.default.green('\u2713')} Removed ${hookCount} MAXSIM hooks`);
614
- }
615
- }
616
- // 5. Remove MAXSIM package.json (CommonJS mode marker)
617
- const pkgJsonPath = path.join(targetDir, 'package.json');
618
- if (fs.existsSync(pkgJsonPath)) {
619
- try {
620
- const content = fs.readFileSync(pkgJsonPath, 'utf8').trim();
621
- if (content === '{"type":"commonjs"}') {
622
- fs.unlinkSync(pkgJsonPath);
623
- removedCount++;
624
- console.log(` ${chalk_1.default.green('\u2713')} Removed MAXSIM package.json`);
625
- }
626
- }
627
- catch {
628
- // Ignore read errors
629
- }
630
- }
631
- // 6. Clean up settings.json
632
- const settingsPath = path.join(targetDir, 'settings.json');
633
- if (fs.existsSync(settingsPath)) {
634
- const settings = (0, index_js_1.readSettings)(settingsPath);
635
- let settingsModified = false;
636
- const statusLine = settings.statusLine;
637
- if (statusLine &&
638
- statusLine.command &&
639
- statusLine.command.includes('maxsim-statusline')) {
640
- delete settings.statusLine;
641
- settingsModified = true;
642
- console.log(` ${chalk_1.default.green('\u2713')} Removed MAXSIM statusline from settings`);
643
- }
644
- const settingsHooks = settings.hooks;
645
- if (settingsHooks && settingsHooks.SessionStart) {
646
- const before = settingsHooks.SessionStart.length;
647
- settingsHooks.SessionStart = settingsHooks.SessionStart.filter((entry) => {
648
- if (entry.hooks && Array.isArray(entry.hooks)) {
649
- const hasMaxsimHook = entry.hooks.some((h) => h.command &&
650
- (h.command.includes('maxsim-check-update') ||
651
- h.command.includes('maxsim-statusline')));
652
- return !hasMaxsimHook;
653
- }
654
- return true;
655
- });
656
- if (settingsHooks.SessionStart.length < before) {
657
- settingsModified = true;
658
- console.log(` ${chalk_1.default.green('\u2713')} Removed MAXSIM hooks from settings`);
659
- }
660
- if (settingsHooks.SessionStart.length === 0) {
661
- delete settingsHooks.SessionStart;
662
- }
663
- }
664
- if (settingsHooks && settingsHooks.PostToolUse) {
665
- const before = settingsHooks.PostToolUse.length;
666
- settingsHooks.PostToolUse = settingsHooks.PostToolUse.filter((entry) => {
667
- if (entry.hooks && Array.isArray(entry.hooks)) {
668
- const hasMaxsimHook = entry.hooks.some((h) => h.command &&
669
- h.command.includes('maxsim-context-monitor'));
670
- return !hasMaxsimHook;
671
- }
672
- return true;
673
- });
674
- if (settingsHooks.PostToolUse.length < before) {
675
- settingsModified = true;
676
- console.log(` ${chalk_1.default.green('\u2713')} Removed context monitor hook from settings`);
677
- }
678
- if (settingsHooks.PostToolUse.length === 0) {
679
- delete settingsHooks.PostToolUse;
680
- }
681
- }
682
- if (settingsHooks && Object.keys(settingsHooks).length === 0) {
683
- delete settings.hooks;
684
- }
685
- if (settingsModified) {
686
- (0, index_js_1.writeSettings)(settingsPath, settings);
687
- removedCount++;
688
- }
689
- }
690
- // 7. For OpenCode, clean up permissions from opencode.json
691
- if (isOpencode) {
692
- const opencodeConfigDir = isGlobal
693
- ? getOpencodeGlobalDir()
694
- : path.join(process.cwd(), '.opencode');
695
- const configPath = path.join(opencodeConfigDir, 'opencode.json');
696
- if (fs.existsSync(configPath)) {
697
- try {
698
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
699
- let modified = false;
700
- const permission = config.permission;
701
- if (permission) {
702
- for (const permType of ['read', 'external_directory']) {
703
- if (permission[permType]) {
704
- const keys = Object.keys(permission[permType]);
705
- for (const key of keys) {
706
- if (key.includes('maxsim')) {
707
- delete permission[permType][key];
708
- modified = true;
709
- }
710
- }
711
- if (Object.keys(permission[permType]).length === 0) {
712
- delete permission[permType];
713
- }
714
- }
715
- }
716
- if (Object.keys(permission).length === 0) {
717
- delete config.permission;
718
- }
719
- }
720
- if (modified) {
721
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
722
- removedCount++;
723
- console.log(` ${chalk_1.default.green('\u2713')} Removed MAXSIM permissions from opencode.json`);
724
- }
725
- }
726
- catch {
727
- // Ignore JSON parse errors
728
- }
729
- }
730
- }
731
- if (removedCount === 0) {
732
- console.log(` ${chalk_1.default.yellow('\u26a0')} No MAXSIM files found to remove.`);
733
- }
734
- console.log(`
735
- ${chalk_1.default.green('Done!')} MAXSIM has been uninstalled from ${runtimeLabel}.
736
- Your other files and settings have been preserved.
737
- `);
738
- }
739
- /**
740
- * Parse JSONC (JSON with Comments) by stripping comments and trailing commas.
741
- */
742
- function parseJsonc(content) {
743
- if (content.charCodeAt(0) === 0xfeff) {
744
- content = content.slice(1);
745
- }
746
- let result = '';
747
- let inString = false;
748
- let i = 0;
749
- while (i < content.length) {
750
- const char = content[i];
751
- const next = content[i + 1];
752
- if (inString) {
753
- result += char;
754
- if (char === '\\' && i + 1 < content.length) {
755
- result += next;
756
- i += 2;
757
- continue;
758
- }
759
- if (char === '"') {
760
- inString = false;
761
- }
762
- i++;
763
- }
764
- else {
765
- if (char === '"') {
766
- inString = true;
767
- result += char;
768
- i++;
769
- }
770
- else if (char === '/' && next === '/') {
771
- while (i < content.length && content[i] !== '\n') {
772
- i++;
773
- }
774
- }
775
- else if (char === '/' && next === '*') {
776
- i += 2;
777
- while (i < content.length - 1 &&
778
- !(content[i] === '*' && content[i + 1] === '/')) {
779
- i++;
780
- }
781
- i += 2;
782
- }
783
- else {
784
- result += char;
785
- i++;
786
- }
787
- }
788
- }
789
- result = result.replace(/,(\s*[}\]])/g, '$1');
790
- return JSON.parse(result);
791
- }
792
- /**
793
- * Configure OpenCode permissions to allow reading MAXSIM reference docs
794
- */
795
- function configureOpencodePermissions(isGlobal = true) {
796
- const opencodeConfigDir = isGlobal
797
- ? getOpencodeGlobalDir()
798
- : path.join(process.cwd(), '.opencode');
799
- const configPath = path.join(opencodeConfigDir, 'opencode.json');
800
- fs.mkdirSync(opencodeConfigDir, { recursive: true });
801
- let config = {};
802
- if (fs.existsSync(configPath)) {
803
- try {
804
- const content = fs.readFileSync(configPath, 'utf8');
805
- config = parseJsonc(content);
806
- }
807
- catch (e) {
808
- console.log(` ${chalk_1.default.yellow('\u26a0')} Could not parse opencode.json - skipping permission config`);
809
- console.log(` ${chalk_1.default.dim(`Reason: ${e.message}`)}`);
810
- console.log(` ${chalk_1.default.dim('Your config was NOT modified. Fix the syntax manually if needed.')}`);
811
- return;
812
- }
813
- }
814
- if (!config.permission) {
815
- config.permission = {};
816
- }
817
- const permission = config.permission;
818
- const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
819
- const maxsimPath = opencodeConfigDir === defaultConfigDir
820
- ? '~/.config/opencode/maxsim/*'
821
- : `${opencodeConfigDir.replace(/\\/g, '/')}/maxsim/*`;
822
- let modified = false;
823
- if (!permission.read || typeof permission.read !== 'object') {
824
- permission.read = {};
825
- }
826
- if (permission.read[maxsimPath] !== 'allow') {
827
- permission.read[maxsimPath] = 'allow';
828
- modified = true;
829
- }
830
- if (!permission.external_directory ||
831
- typeof permission.external_directory !== 'object') {
832
- permission.external_directory = {};
833
- }
834
- if (permission.external_directory[maxsimPath] !== 'allow') {
835
- permission.external_directory[maxsimPath] = 'allow';
836
- modified = true;
837
- }
838
- if (!modified) {
839
- return;
840
- }
841
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
842
- console.log(` ${chalk_1.default.green('\u2713')} Configured read permission for MAXSIM docs`);
843
- }
844
- /**
845
- * Verify a directory exists and contains files
846
- */
847
- function verifyInstalled(dirPath, description) {
848
- if (!fs.existsSync(dirPath)) {
849
- console.error(` ${chalk_1.default.yellow('\u2717')} Failed to install ${description}: directory not created`);
850
- return false;
851
- }
852
- try {
853
- const entries = fs.readdirSync(dirPath);
854
- if (entries.length === 0) {
855
- console.error(` ${chalk_1.default.yellow('\u2717')} Failed to install ${description}: directory is empty`);
856
- return false;
857
- }
858
- }
859
- catch (e) {
860
- console.error(` ${chalk_1.default.yellow('\u2717')} Failed to install ${description}: ${e.message}`);
861
- return false;
862
- }
863
- return true;
864
- }
865
- /**
866
- * Verify a file exists
867
- */
868
- function verifyFileInstalled(filePath, description) {
869
- if (!fs.existsSync(filePath)) {
870
- console.error(` ${chalk_1.default.yellow('\u2717')} Failed to install ${description}: file not created`);
871
- return false;
872
- }
873
- return true;
874
- }
875
- // ──────────────────────────────────────────────────────
876
- // Local Patch Persistence
877
- // ──────────────────────────────────────────────────────
878
- const PATCHES_DIR_NAME = 'maxsim-local-patches';
879
- const MANIFEST_NAME = 'maxsim-file-manifest.json';
880
- /**
881
- * Compute SHA256 hash of file contents
882
- */
883
- function fileHash(filePath) {
884
- const content = fs.readFileSync(filePath);
885
- return crypto.createHash('sha256').update(content).digest('hex');
886
- }
887
- /**
888
- * Recursively collect all files in dir with their hashes
889
- */
890
- function generateManifest(dir, baseDir) {
891
- if (!baseDir)
892
- baseDir = dir;
893
- const manifest = {};
894
- if (!fs.existsSync(dir))
895
- return manifest;
896
- const entries = fs.readdirSync(dir, { withFileTypes: true });
897
- for (const entry of entries) {
898
- const fullPath = path.join(dir, entry.name);
899
- const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
900
- if (entry.isDirectory()) {
901
- Object.assign(manifest, generateManifest(fullPath, baseDir));
902
- }
903
- else {
904
- manifest[relPath] = fileHash(fullPath);
905
- }
906
- }
907
- return manifest;
908
- }
909
- /**
910
- * Write file manifest after installation for future modification detection
911
- */
912
- function writeManifest(configDir, runtime = 'claude') {
913
- const isOpencode = runtime === 'opencode';
914
- const isCodex = runtime === 'codex';
915
- const maxsimDir = path.join(configDir, 'maxsim');
916
- const commandsDir = path.join(configDir, 'commands', 'maxsim');
917
- const opencodeCommandDir = path.join(configDir, 'command');
918
- const codexSkillsDir = path.join(configDir, 'skills');
919
- const agentsDir = path.join(configDir, 'agents');
920
- const manifest = {
921
- version: pkg.version,
922
- timestamp: new Date().toISOString(),
923
- files: {},
924
- };
925
- const maxsimHashes = generateManifest(maxsimDir);
926
- for (const [rel, hash] of Object.entries(maxsimHashes)) {
927
- manifest.files['maxsim/' + rel] = hash;
928
- }
929
- if (!isOpencode && !isCodex && fs.existsSync(commandsDir)) {
930
- const cmdHashes = generateManifest(commandsDir);
931
- for (const [rel, hash] of Object.entries(cmdHashes)) {
932
- manifest.files['commands/maxsim/' + rel] = hash;
933
- }
934
- }
935
- if (isOpencode && fs.existsSync(opencodeCommandDir)) {
936
- for (const file of fs.readdirSync(opencodeCommandDir)) {
937
- if (file.startsWith('maxsim-') && file.endsWith('.md')) {
938
- manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
939
- }
940
- }
941
- }
942
- if (isCodex && fs.existsSync(codexSkillsDir)) {
943
- for (const skillName of listCodexSkillNames(codexSkillsDir)) {
944
- const skillRoot = path.join(codexSkillsDir, skillName);
945
- const skillHashes = generateManifest(skillRoot);
946
- for (const [rel, hash] of Object.entries(skillHashes)) {
947
- manifest.files[`skills/${skillName}/${rel}`] = hash;
948
- }
949
- }
950
- }
951
- if (fs.existsSync(agentsDir)) {
952
- for (const file of fs.readdirSync(agentsDir)) {
953
- if (file.startsWith('maxsim-') && file.endsWith('.md')) {
954
- manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file));
955
- }
956
- }
957
- }
958
- // Include skills in manifest (agents/skills/<skill-name>/*)
959
- const skillsManifestDir = path.join(agentsDir, 'skills');
960
- if (fs.existsSync(skillsManifestDir)) {
961
- const skillHashes = generateManifest(skillsManifestDir);
962
- for (const [rel, hash] of Object.entries(skillHashes)) {
963
- manifest.files['agents/skills/' + rel] = hash;
964
- }
965
- }
966
- fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
967
- return manifest;
968
- }
969
- /**
970
- * Detect user-modified MAXSIM files by comparing against install manifest.
971
- */
972
- function saveLocalPatches(configDir) {
973
- const manifestPath = path.join(configDir, MANIFEST_NAME);
974
- if (!fs.existsSync(manifestPath))
975
- return [];
976
- let manifest;
977
- try {
978
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
979
- }
980
- catch {
981
- return [];
982
- }
983
- const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
984
- const modified = [];
985
- for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
986
- const fullPath = path.join(configDir, relPath);
987
- if (!fs.existsSync(fullPath))
988
- continue;
989
- const currentHash = fileHash(fullPath);
990
- if (currentHash !== originalHash) {
991
- const backupPath = path.join(patchesDir, relPath);
992
- fs.mkdirSync(path.dirname(backupPath), { recursive: true });
993
- fs.copyFileSync(fullPath, backupPath);
994
- modified.push(relPath);
995
- }
996
- }
997
- if (modified.length > 0) {
998
- const meta = {
999
- backed_up_at: new Date().toISOString(),
1000
- from_version: manifest.version,
1001
- files: modified,
1002
- };
1003
- fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify(meta, null, 2));
1004
- console.log(' ' +
1005
- chalk_1.default.yellow('i') +
1006
- ' Found ' +
1007
- modified.length +
1008
- ' locally modified MAXSIM file(s) \u2014 backed up to ' +
1009
- PATCHES_DIR_NAME +
1010
- '/');
1011
- for (const f of modified) {
1012
- console.log(' ' + chalk_1.default.dim(f));
1013
- }
1014
- }
1015
- return modified;
1016
- }
1017
- /**
1018
- * After install, report backed-up patches for user to reapply.
1019
- */
1020
- function reportLocalPatches(configDir, runtime = 'claude') {
1021
- const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
1022
- const metaPath = path.join(patchesDir, 'backup-meta.json');
1023
- if (!fs.existsSync(metaPath))
1024
- return [];
1025
- let meta;
1026
- try {
1027
- meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
1028
- }
1029
- catch {
1030
- return [];
1031
- }
1032
- if (meta.files && meta.files.length > 0) {
1033
- const reapplyCommand = runtime === 'opencode'
1034
- ? '/maxsim-reapply-patches'
1035
- : runtime === 'codex'
1036
- ? '$maxsim-reapply-patches'
1037
- : '/maxsim:reapply-patches';
1038
- console.log('');
1039
- console.log(' ' +
1040
- chalk_1.default.yellow('Local patches detected') +
1041
- ' (from v' +
1042
- meta.from_version +
1043
- '):');
1044
- for (const f of meta.files) {
1045
- console.log(' ' + chalk_1.default.cyan(f));
1046
- }
1047
- console.log('');
1048
- console.log(' Your modifications are saved in ' +
1049
- chalk_1.default.cyan(PATCHES_DIR_NAME + '/'));
1050
- console.log(' Run ' +
1051
- chalk_1.default.cyan(reapplyCommand) +
1052
- ' to merge them into the new version.');
1053
- console.log(' Or manually compare and merge the files.');
1054
- console.log('');
1055
- }
1056
- return meta.files || [];
1057
- }
1058
- async function install(isGlobal, runtime = 'claude') {
1059
- const isOpencode = runtime === 'opencode';
1060
- const isGemini = runtime === 'gemini';
1061
- const isCodex = runtime === 'codex';
1062
- const dirName = getDirName(runtime);
1063
- const src = templatesRoot;
1064
- const targetDir = isGlobal
1065
- ? getGlobalDir(runtime, explicitConfigDir)
1066
- : path.join(process.cwd(), dirName);
1067
- const locationLabel = isGlobal
1068
- ? targetDir.replace(os.homedir(), '~')
1069
- : targetDir.replace(process.cwd(), '.');
1070
- const pathPrefix = isGlobal
1071
- ? `${targetDir.replace(/\\/g, '/')}/`
1072
- : `./${dirName}/`;
1073
- let runtimeLabel = 'Claude Code';
1074
- if (isOpencode)
1075
- runtimeLabel = 'OpenCode';
1076
- if (isGemini)
1077
- runtimeLabel = 'Gemini';
1078
- if (isCodex)
1079
- runtimeLabel = 'Codex';
1080
- console.log(` Installing for ${chalk_1.default.cyan(runtimeLabel)} to ${chalk_1.default.cyan(locationLabel)}\n`);
1081
- const failures = [];
1082
- // Save any locally modified MAXSIM files before they get wiped
1083
- saveLocalPatches(targetDir);
1084
- // Clean up orphaned files from previous versions
1085
- cleanupOrphanedFiles(targetDir);
1086
- // OpenCode uses command/ (flat), Codex uses skills/, Claude/Gemini use commands/maxsim/
1087
- let spinner = (0, ora_1.default)({ text: 'Installing commands...', color: 'cyan' }).start();
1088
- if (isOpencode) {
1089
- const commandDir = path.join(targetDir, 'command');
1090
- fs.mkdirSync(commandDir, { recursive: true });
1091
- const maxsimSrc = path.join(src, 'commands', 'maxsim');
1092
- copyFlattenedCommands(maxsimSrc, commandDir, 'maxsim', pathPrefix, runtime);
1093
- if (verifyInstalled(commandDir, 'command/maxsim-*')) {
1094
- const count = fs
1095
- .readdirSync(commandDir)
1096
- .filter((f) => f.startsWith('maxsim-')).length;
1097
- spinner.succeed(chalk_1.default.green('✓') + ` Installed ${count} commands to command/`);
1098
- }
1099
- else {
1100
- spinner.fail('Failed to install commands');
1101
- failures.push('command/maxsim-*');
1102
- }
1103
- }
1104
- else if (isCodex) {
1105
- const skillsDir = path.join(targetDir, 'skills');
1106
- const maxsimSrc = path.join(src, 'commands', 'maxsim');
1107
- copyCommandsAsCodexSkills(maxsimSrc, skillsDir, 'maxsim', pathPrefix, runtime);
1108
- const installedSkillNames = listCodexSkillNames(skillsDir);
1109
- if (installedSkillNames.length > 0) {
1110
- spinner.succeed(chalk_1.default.green('✓') + ` Installed ${installedSkillNames.length} skills to skills/`);
1111
- }
1112
- else {
1113
- spinner.fail('Failed to install skills');
1114
- failures.push('skills/maxsim-*');
1115
- }
1116
- }
1117
- else {
1118
- const commandsDir = path.join(targetDir, 'commands');
1119
- fs.mkdirSync(commandsDir, { recursive: true });
1120
- const maxsimSrc = path.join(src, 'commands', 'maxsim');
1121
- const maxsimDest = path.join(commandsDir, 'maxsim');
1122
- copyWithPathReplacement(maxsimSrc, maxsimDest, pathPrefix, runtime, true);
1123
- if (verifyInstalled(maxsimDest, 'commands/maxsim')) {
1124
- spinner.succeed(chalk_1.default.green('✓') + ' Installed commands/maxsim');
1125
- }
1126
- else {
1127
- spinner.fail('Failed to install commands/maxsim');
1128
- failures.push('commands/maxsim');
1129
- }
1130
- }
1131
- // Copy maxsim directory content (workflows, templates, references) with path replacement
1132
- // Templates package layout: workflows/, templates/, references/ at root
1133
- // Install target: maxsim/workflows/, maxsim/templates/, maxsim/references/
1134
- spinner = (0, ora_1.default)({ text: 'Installing workflows and templates...', color: 'cyan' }).start();
1135
- const skillDest = path.join(targetDir, 'maxsim');
1136
- const maxsimSubdirs = ['workflows', 'templates', 'references'];
1137
- if (fs.existsSync(skillDest)) {
1138
- fs.rmSync(skillDest, { recursive: true });
1139
- }
1140
- fs.mkdirSync(skillDest, { recursive: true });
1141
- for (const subdir of maxsimSubdirs) {
1142
- const subdirSrc = path.join(src, subdir);
1143
- if (fs.existsSync(subdirSrc)) {
1144
- const subdirDest = path.join(skillDest, subdir);
1145
- copyWithPathReplacement(subdirSrc, subdirDest, pathPrefix, runtime);
1146
- }
1147
- }
1148
- if (verifyInstalled(skillDest, 'maxsim')) {
1149
- spinner.succeed(chalk_1.default.green('✓') + ' Installed maxsim');
1150
- }
1151
- else {
1152
- spinner.fail('Failed to install maxsim');
1153
- failures.push('maxsim');
1154
- }
1155
- // Copy agents to agents directory
1156
- const agentsSrc = path.join(src, 'agents');
1157
- if (fs.existsSync(agentsSrc)) {
1158
- spinner = (0, ora_1.default)({ text: 'Installing agents...', color: 'cyan' }).start();
1159
- const agentsDest = path.join(targetDir, 'agents');
1160
- fs.mkdirSync(agentsDest, { recursive: true });
1161
- // Remove old MAXSIM agents before copying new ones
1162
- if (fs.existsSync(agentsDest)) {
1163
- for (const file of fs.readdirSync(agentsDest)) {
1164
- if (file.startsWith('maxsim-') && file.endsWith('.md')) {
1165
- fs.unlinkSync(path.join(agentsDest, file));
1166
- }
1167
- }
1168
- }
1169
- const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
1170
- for (const entry of agentEntries) {
1171
- if (entry.isFile() && entry.name.endsWith('.md')) {
1172
- let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
1173
- const dirRegex = /~\/\.claude\//g;
1174
- content = content.replace(dirRegex, pathPrefix);
1175
- content = (0, index_js_1.processAttribution)(content, getCommitAttribution(runtime));
1176
- if (isOpencode) {
1177
- content = (0, index_js_1.convertClaudeToOpencodeFrontmatter)(content);
1178
- }
1179
- else if (isGemini) {
1180
- content = (0, index_js_1.convertClaudeToGeminiAgent)(content);
1181
- }
1182
- else if (isCodex) {
1183
- content = (0, index_js_1.convertClaudeToCodexMarkdown)(content);
1184
- }
1185
- fs.writeFileSync(path.join(agentsDest, entry.name), content);
1186
- }
1187
- }
1188
- if (verifyInstalled(agentsDest, 'agents')) {
1189
- spinner.succeed(chalk_1.default.green('✓') + ' Installed agents');
1190
- }
1191
- else {
1192
- spinner.fail('Failed to install agents');
1193
- failures.push('agents');
1194
- }
1195
- }
1196
- // Copy skills to agents/skills/ directory
1197
- const skillsSrc = path.join(src, 'skills');
1198
- if (fs.existsSync(skillsSrc)) {
1199
- spinner = (0, ora_1.default)({ text: 'Installing skills...', color: 'cyan' }).start();
1200
- const skillsDest = path.join(targetDir, 'agents', 'skills');
1201
- // Remove old MAXSIM built-in skills before copying new ones (preserve user custom skills)
1202
- if (fs.existsSync(skillsDest)) {
1203
- const builtInSkills = ['tdd', 'systematic-debugging', 'verification-before-completion'];
1204
- for (const skill of builtInSkills) {
1205
- const skillDir = path.join(skillsDest, skill);
1206
- if (fs.existsSync(skillDir)) {
1207
- fs.rmSync(skillDir, { recursive: true });
1208
- }
1209
- }
1210
- }
1211
- // Copy skills directory recursively
1212
- fs_extra_1.default.copySync(skillsSrc, skillsDest, { overwrite: true });
1213
- // Process path prefixes in skill files (replace ~/.claude/ with runtime-specific path)
1214
- const skillEntries = fs.readdirSync(skillsDest, { withFileTypes: true });
1215
- for (const entry of skillEntries) {
1216
- if (entry.isDirectory()) {
1217
- const skillMd = path.join(skillsDest, entry.name, 'SKILL.md');
1218
- if (fs.existsSync(skillMd)) {
1219
- let content = fs.readFileSync(skillMd, 'utf8');
1220
- const dirRegex = /~\/\.claude\//g;
1221
- content = content.replace(dirRegex, pathPrefix);
1222
- content = (0, index_js_1.processAttribution)(content, getCommitAttribution(runtime));
1223
- fs.writeFileSync(skillMd, content);
1224
- }
1225
- }
1226
- }
1227
- const installedSkillDirs = fs.readdirSync(skillsDest, { withFileTypes: true })
1228
- .filter(e => e.isDirectory()).length;
1229
- if (installedSkillDirs > 0) {
1230
- spinner.succeed(chalk_1.default.green('\u2713') + ` Installed ${installedSkillDirs} skills to agents/skills/`);
1231
- }
1232
- else {
1233
- spinner.fail('Failed to install skills');
1234
- failures.push('agents/skills');
1235
- }
1236
- }
1237
- // Copy CHANGELOG.md (lives at repo root, one level above templates package)
1238
- const changelogSrc = path.join(src, '..', 'CHANGELOG.md');
1239
- const changelogDest = path.join(targetDir, 'maxsim', 'CHANGELOG.md');
1240
- if (fs.existsSync(changelogSrc)) {
1241
- spinner = (0, ora_1.default)({ text: 'Installing CHANGELOG.md...', color: 'cyan' }).start();
1242
- fs.copyFileSync(changelogSrc, changelogDest);
1243
- if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
1244
- spinner.succeed(chalk_1.default.green('✓') + ' Installed CHANGELOG.md');
1245
- }
1246
- else {
1247
- spinner.fail('Failed to install CHANGELOG.md');
1248
- failures.push('CHANGELOG.md');
1249
- }
1250
- }
1251
- // Copy CLAUDE.md (global MAXSIM context for Claude Code)
1252
- const claudeMdSrc = path.join(src, 'CLAUDE.md');
1253
- const claudeMdDest = path.join(targetDir, 'CLAUDE.md');
1254
- if (fs.existsSync(claudeMdSrc)) {
1255
- spinner = (0, ora_1.default)({ text: 'Installing CLAUDE.md...', color: 'cyan' }).start();
1256
- fs.copyFileSync(claudeMdSrc, claudeMdDest);
1257
- if (verifyFileInstalled(claudeMdDest, 'CLAUDE.md')) {
1258
- spinner.succeed(chalk_1.default.green('✓') + ' Installed CLAUDE.md');
1259
- }
1260
- else {
1261
- spinner.fail('Failed to install CLAUDE.md');
1262
- failures.push('CLAUDE.md');
1263
- }
1264
- }
1265
- // Write VERSION file
1266
- const versionDest = path.join(targetDir, 'maxsim', 'VERSION');
1267
- fs.writeFileSync(versionDest, pkg.version);
1268
- if (verifyFileInstalled(versionDest, 'VERSION')) {
1269
- console.log(` ${chalk_1.default.green('\u2713')} Wrote VERSION (${pkg.version})`);
1270
- }
1271
- else {
1272
- failures.push('VERSION');
1273
- }
1274
- if (!isCodex) {
1275
- // Write package.json to force CommonJS mode for MAXSIM scripts
1276
- const pkgJsonDest = path.join(targetDir, 'package.json');
1277
- fs.writeFileSync(pkgJsonDest, '{"type":"commonjs"}\n');
1278
- console.log(` ${chalk_1.default.green('\u2713')} Wrote package.json (CommonJS mode)`);
1279
- // Install maxsim-tools.cjs binary — workflows call `node ~/.claude/maxsim/bin/maxsim-tools.cjs`
1280
- const toolSrc = path.resolve(__dirname, 'cli.cjs');
1281
- const binDir = path.join(targetDir, 'maxsim', 'bin');
1282
- const toolDest = path.join(binDir, 'maxsim-tools.cjs');
1283
- if (fs.existsSync(toolSrc)) {
1284
- fs.mkdirSync(binDir, { recursive: true });
1285
- fs.copyFileSync(toolSrc, toolDest);
1286
- console.log(` ${chalk_1.default.green('\u2713')} Installed maxsim-tools.cjs`);
1287
- }
1288
- else {
1289
- console.warn(` ${chalk_1.default.yellow('!')} cli.cjs not found at ${toolSrc} — maxsim-tools.cjs not installed`);
1290
- failures.push('maxsim-tools.cjs');
1291
- }
1292
- // Install mcp-server.cjs — MCP server binary for Claude Code integration
1293
- const mcpSrc = path.resolve(__dirname, 'mcp-server.cjs');
1294
- const mcpDest = path.join(binDir, 'mcp-server.cjs');
1295
- if (fs.existsSync(mcpSrc)) {
1296
- fs.mkdirSync(binDir, { recursive: true });
1297
- fs.copyFileSync(mcpSrc, mcpDest);
1298
- console.log(` ${chalk_1.default.green('\u2713')} Installed mcp-server.cjs`);
1299
- }
1300
- else {
1301
- console.warn(` ${chalk_1.default.yellow('!')} mcp-server.cjs not found — MCP server not installed`);
1302
- // Don't add to failures — MCP is optional, install should not fail
1303
- }
1304
- // Copy hooks from bundled assets directory (copied from @maxsim/hooks/dist at build time)
1305
- let hooksSrc = null;
1306
- const bundledHooksDir = path.resolve(__dirname, 'assets', 'hooks');
1307
- if (fs.existsSync(bundledHooksDir)) {
1308
- hooksSrc = bundledHooksDir;
1309
- }
1310
- else {
1311
- console.warn(` ${chalk_1.default.yellow('!')} bundled hooks not found - hooks will not be installed`);
1312
- }
1313
- if (hooksSrc) {
1314
- spinner = (0, ora_1.default)({ text: 'Installing hooks...', color: 'cyan' }).start();
1315
- const hooksDest = path.join(targetDir, 'hooks');
1316
- fs.mkdirSync(hooksDest, { recursive: true });
1317
- const hookEntries = fs.readdirSync(hooksSrc);
1318
- const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
1319
- for (const entry of hookEntries) {
1320
- const srcFile = path.join(hooksSrc, entry);
1321
- if (fs.statSync(srcFile).isFile() && entry.endsWith('.cjs') && !entry.includes('.d.')) {
1322
- const destName = entry.replace(/\.cjs$/, '.js');
1323
- const destFile = path.join(hooksDest, destName);
1324
- let content = fs.readFileSync(srcFile, 'utf8');
1325
- content = content.replace(/'\.claude'/g, configDirReplacement);
1326
- fs.writeFileSync(destFile, content);
1327
- }
1328
- }
1329
- if (verifyInstalled(hooksDest, 'hooks')) {
1330
- spinner.succeed(chalk_1.default.green('✓') + ' Installed hooks (bundled)');
1331
- }
1332
- else {
1333
- spinner.fail('Failed to install hooks');
1334
- failures.push('hooks');
1335
- }
1336
- }
1337
- }
1338
- // Copy dashboard Vite+Express build (if bundled in dist/assets/dashboard/)
1339
- // The dashboard now ships as: server.js (tsdown-bundled Express) + client/ (Vite static)
1340
- // No node_modules/ needed at destination — all server deps are bundled inline.
1341
- const dashboardSrc = path.resolve(__dirname, 'assets', 'dashboard');
1342
- if (fs.existsSync(dashboardSrc)) {
1343
- // Ask whether to expose the dashboard on the local network (before spinner)
1344
- let networkMode = false;
1345
- try {
1346
- networkMode = await (0, prompts_1.confirm)({
1347
- message: 'Allow dashboard to be accessible on your local network? (adds firewall rule, enables QR code)',
1348
- default: false,
1349
- });
1350
- }
1351
- catch {
1352
- // Non-interactive terminal — default to false
1353
- }
1354
- spinner = (0, ora_1.default)({ text: 'Installing dashboard...', color: 'cyan' }).start();
1355
- const dashboardDest = path.join(targetDir, 'dashboard');
1356
- // Clean existing dashboard to prevent stale files from old installs
1357
- safeRmDir(dashboardDest);
1358
- copyDirRecursive(dashboardSrc, dashboardDest);
1359
- // Write dashboard.json NEXT TO dashboard/ dir (survives overwrites on upgrade)
1360
- const dashboardConfigDest = path.join(targetDir, 'dashboard.json');
1361
- const projectCwd = isGlobal ? targetDir : process.cwd();
1362
- fs.writeFileSync(dashboardConfigDest, JSON.stringify({ projectCwd, networkMode }, null, 2) + '\n');
1363
- if (fs.existsSync(path.join(dashboardDest, 'server.js'))) {
1364
- spinner.succeed(chalk_1.default.green('✓') + ' Installed dashboard');
1365
- }
1366
- else {
1367
- spinner.succeed(chalk_1.default.green('✓') + ' Installed dashboard (server.js not found in bundle)');
1368
- }
1369
- if (networkMode) {
1370
- applyFirewallRule(3333);
1371
- }
1372
- }
1373
- // Write .mcp.json for Claude Code MCP server auto-discovery
1374
- if (!isOpencode && !isCodex && !isGemini) {
1375
- const mcpJsonPath = isGlobal
1376
- ? path.join(targetDir, '..', '.mcp.json')
1377
- : path.join(process.cwd(), '.mcp.json');
1378
- let mcpConfig = {};
1379
- // Preserve existing .mcp.json if it exists (user may have other MCP servers)
1380
- if (fs.existsSync(mcpJsonPath)) {
1381
- try {
1382
- mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
1383
- }
1384
- catch {
1385
- // Corrupted file — start fresh
1386
- }
1387
- }
1388
- const mcpServers = mcpConfig.mcpServers ?? {};
1389
- mcpServers['maxsim'] = {
1390
- command: 'node',
1391
- args: ['.claude/maxsim/bin/mcp-server.cjs'],
1392
- env: {},
1393
- };
1394
- mcpConfig.mcpServers = mcpServers;
1395
- fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf-8');
1396
- console.log(` ${chalk_1.default.green('\u2713')} Configured .mcp.json for MCP server auto-discovery`);
1397
- }
1398
- if (failures.length > 0) {
1399
- console.error(`\n ${chalk_1.default.yellow('Installation incomplete!')} Failed: ${failures.join(', ')}`);
1400
- process.exit(1);
1401
- }
1402
- // Write file manifest for future modification detection
1403
- writeManifest(targetDir, runtime);
1404
- console.log(` ${chalk_1.default.green('\u2713')} Wrote file manifest (${MANIFEST_NAME})`);
1405
- // Report any backed-up local patches
1406
- reportLocalPatches(targetDir, runtime);
1407
- if (isCodex) {
1408
- return {
1409
- settingsPath: null,
1410
- settings: null,
1411
- statuslineCommand: null,
1412
- runtime,
1413
- };
1414
- }
1415
- // Configure statusline and hooks in settings.json
1416
- const settingsPath = path.join(targetDir, 'settings.json');
1417
- const settings = cleanupOrphanedHooks((0, index_js_1.readSettings)(settingsPath));
1418
- const statuslineCommand = isGlobal
1419
- ? (0, index_js_1.buildHookCommand)(targetDir, 'maxsim-statusline.js')
1420
- : 'node ' + dirName + '/hooks/maxsim-statusline.js';
1421
- const updateCheckCommand = isGlobal
1422
- ? (0, index_js_1.buildHookCommand)(targetDir, 'maxsim-check-update.js')
1423
- : 'node ' + dirName + '/hooks/maxsim-check-update.js';
1424
- const contextMonitorCommand = isGlobal
1425
- ? (0, index_js_1.buildHookCommand)(targetDir, 'maxsim-context-monitor.js')
1426
- : 'node ' + dirName + '/hooks/maxsim-context-monitor.js';
1427
- // Enable experimental agents for Gemini CLI
1428
- if (isGemini) {
1429
- if (!settings.experimental) {
1430
- settings.experimental = {};
1431
- }
1432
- const experimental = settings.experimental;
1433
- if (!experimental.enableAgents) {
1434
- experimental.enableAgents = true;
1435
- console.log(` ${chalk_1.default.green('\u2713')} Enabled experimental agents`);
1436
- }
1437
- }
1438
- // Configure SessionStart hook for update checking (skip for opencode)
1439
- if (!isOpencode) {
1440
- if (!settings.hooks) {
1441
- settings.hooks = {};
1442
- }
1443
- const installHooks = settings.hooks;
1444
- if (!installHooks.SessionStart) {
1445
- installHooks.SessionStart = [];
1446
- }
1447
- const hasMaxsimUpdateHook = installHooks.SessionStart.some((entry) => entry.hooks &&
1448
- entry.hooks.some((h) => h.command && h.command.includes('maxsim-check-update')));
1449
- if (!hasMaxsimUpdateHook) {
1450
- installHooks.SessionStart.push({
1451
- hooks: [
1452
- {
1453
- type: 'command',
1454
- command: updateCheckCommand,
1455
- },
1456
- ],
1457
- });
1458
- console.log(` ${chalk_1.default.green('\u2713')} Configured update check hook`);
1459
- }
1460
- // Configure PostToolUse hook for context window monitoring
1461
- if (!installHooks.PostToolUse) {
1462
- installHooks.PostToolUse = [];
1463
- }
1464
- const hasContextMonitorHook = installHooks.PostToolUse.some((entry) => entry.hooks &&
1465
- entry.hooks.some((h) => h.command && h.command.includes('maxsim-context-monitor')));
1466
- if (!hasContextMonitorHook) {
1467
- installHooks.PostToolUse.push({
1468
- hooks: [
1469
- {
1470
- type: 'command',
1471
- command: contextMonitorCommand,
1472
- },
1473
- ],
1474
- });
1475
- console.log(` ${chalk_1.default.green('\u2713')} Configured context window monitor hook`);
1476
- }
1477
- }
1478
- return { settingsPath, settings, statuslineCommand, runtime };
1479
- }
1480
- /**
1481
- * Apply statusline config, then print completion message
1482
- */
1483
- function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true) {
1484
- const isOpencode = runtime === 'opencode';
1485
- const isCodex = runtime === 'codex';
1486
- if (shouldInstallStatusline && !isOpencode && !isCodex) {
1487
- settings.statusLine = {
1488
- type: 'command',
1489
- command: statuslineCommand,
1490
- };
1491
- console.log(` ${chalk_1.default.green('\u2713')} Configured statusline`);
1492
- }
1493
- if (!isCodex && settingsPath && settings) {
1494
- (0, index_js_1.writeSettings)(settingsPath, settings);
1495
- }
1496
- if (isOpencode) {
1497
- configureOpencodePermissions(isGlobal);
1498
- }
1499
- let program = 'Claude Code';
1500
- if (runtime === 'opencode')
1501
- program = 'OpenCode';
1502
- if (runtime === 'gemini')
1503
- program = 'Gemini';
1504
- if (runtime === 'codex')
1505
- program = 'Codex';
1506
- let command = '/maxsim:help';
1507
- if (runtime === 'opencode')
1508
- command = '/maxsim-help';
1509
- if (runtime === 'codex')
1510
- command = '$maxsim-help';
1511
- console.log(`
1512
- ${chalk_1.default.green('Done!')} Launch ${program} and run ${chalk_1.default.cyan(command)}.
1513
-
1514
- ${chalk_1.default.cyan('Join the community:')} https://discord.gg/5JJgD5svVS
1515
- `);
1516
- }
1517
- /**
1518
- * Handle statusline configuration — returns true if MAXSIM statusline should be installed
1519
- */
1520
- async function handleStatusline(settings, isInteractive) {
1521
- const hasExisting = settings.statusLine != null;
1522
- if (!hasExisting)
1523
- return true;
1524
- if (forceStatusline)
1525
- return true;
1526
- if (!isInteractive) {
1527
- console.log(chalk_1.default.yellow('⚠') + ' Skipping statusline (already configured)');
1528
- console.log(' Use ' + chalk_1.default.cyan('--force-statusline') + ' to replace\n');
1529
- return false;
1530
- }
1531
- const statusLine = settings.statusLine;
1532
- const existingCmd = statusLine.command || statusLine.url || '(custom)';
1533
- console.log();
1534
- console.log(chalk_1.default.yellow('⚠ Existing statusline detected'));
1535
- console.log();
1536
- console.log(' Your current statusline:');
1537
- console.log(' ' + chalk_1.default.dim(`command: ${existingCmd}`));
1538
- console.log();
1539
- console.log(' MAXSIM includes a statusline showing:');
1540
- console.log(' • Model name');
1541
- console.log(' • Current task (from todo list)');
1542
- console.log(' • Context window usage (color-coded)');
1543
- console.log();
1544
- const shouldReplace = await (0, prompts_1.confirm)({
1545
- message: 'Replace with MAXSIM statusline?',
1546
- default: false,
1547
- });
1548
- return shouldReplace;
1549
- }
1550
- /**
1551
- * Prompt for runtime selection (multi-select)
1552
- */
1553
- async function promptRuntime() {
1554
- const selected = await (0, prompts_1.checkbox)({
1555
- message: 'Which runtime(s) would you like to install for?',
1556
- choices: [
1557
- { name: 'Claude Code ' + chalk_1.default.dim('(~/.claude)'), value: 'claude', checked: true },
1558
- { name: 'OpenCode ' + chalk_1.default.dim('(~/.config/opencode)') + ' — open source, free models', value: 'opencode' },
1559
- { name: 'Gemini ' + chalk_1.default.dim('(~/.gemini)'), value: 'gemini' },
1560
- { name: 'Codex ' + chalk_1.default.dim('(~/.codex)'), value: 'codex' },
1561
- ],
1562
- validate: (choices) => choices.length > 0 || 'Please select at least one runtime',
1563
- });
1564
- return selected;
1565
- }
1566
- /**
1567
- * Prompt for install location
1568
- */
1569
- async function promptLocation(runtimes) {
1570
- if (!process.stdin.isTTY) {
1571
- console.log(chalk_1.default.yellow('Non-interactive terminal detected, defaulting to global install') + '\n');
1572
- return true; // isGlobal
1573
- }
1574
- const pathExamples = runtimes
1575
- .map((r) => getGlobalDir(r, explicitConfigDir).replace(os.homedir(), '~'))
1576
- .join(', ');
1577
- const localExamples = runtimes.map((r) => `./${getDirName(r)}`).join(', ');
1578
- const choice = await (0, prompts_1.select)({
1579
- message: 'Where would you like to install?',
1580
- choices: [
1581
- {
1582
- name: 'Global ' + chalk_1.default.dim(`(${pathExamples})`) + ' — available in all projects',
1583
- value: 'global',
1584
- },
1585
- {
1586
- name: 'Local ' + chalk_1.default.dim(`(${localExamples})`) + ' — this project only',
1587
- value: 'local',
1588
- },
1589
- ],
1590
- });
1591
- return choice === 'global';
1592
- }
1593
- /**
1594
- * Prompt whether to enable Agent Teams (Claude only, experimental feature)
1595
- */
1596
- async function promptAgentTeams() {
1597
- console.log();
1598
- console.log(chalk_1.default.cyan(' Agent Teams') + chalk_1.default.dim(' (experimental)'));
1599
- console.log(chalk_1.default.dim(' Coordinate multiple Claude Code instances working in parallel.'));
1600
- console.log(chalk_1.default.dim(' Enables CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS in settings.json.'));
1601
- console.log();
1602
- return (0, prompts_1.confirm)({
1603
- message: 'Enable Agent Teams?',
1604
- default: false,
1605
- });
1606
- }
1607
- /**
1608
- * Install MAXSIM for all selected runtimes
1609
- */
1610
- async function installAllRuntimes(runtimes, isGlobal, isInteractive) {
1611
- const results = [];
1612
- for (const runtime of runtimes) {
1613
- const result = await install(isGlobal, runtime);
1614
- results.push(result);
1615
- }
1616
- const statuslineRuntimes = ['claude', 'gemini'];
1617
- const primaryStatuslineResult = results.find((r) => statuslineRuntimes.includes(r.runtime));
1618
- let shouldInstallStatusline = false;
1619
- if (primaryStatuslineResult && primaryStatuslineResult.settings) {
1620
- shouldInstallStatusline = await handleStatusline(primaryStatuslineResult.settings, isInteractive);
1621
- }
1622
- // Prompt for Agent Teams if Claude is in the selected runtimes
1623
- let enableAgentTeams = false;
1624
- if (isInteractive && runtimes.includes('claude')) {
1625
- enableAgentTeams = await promptAgentTeams();
1626
- }
1627
- for (const result of results) {
1628
- const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline;
1629
- // Apply Agent Teams setting for Claude
1630
- if (result.runtime === 'claude' && enableAgentTeams && result.settings) {
1631
- const env = result.settings.env ?? {};
1632
- env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] = '1';
1633
- result.settings.env = env;
1634
- }
1635
- finishInstall(result.settingsPath, result.settings, result.statuslineCommand, useStatusline, result.runtime, isGlobal);
1636
- }
1637
- }
1638
- // Main logic
1639
- // Subcommand routing — intercept before install flow
1640
- const subcommand = argv._[0];
1641
- (async () => {
1642
- // Dashboard subcommand
1643
- if (subcommand === 'dashboard') {
1644
- const { spawn: spawnDash, execSync: execSyncDash } = await import('node:child_process');
1645
- // Always refresh dashboard from bundled assets before launching.
1646
- // This ensures users get the latest version (fixes broken ESM builds, etc.)
1647
- const dashboardAssetSrc = path.resolve(__dirname, 'assets', 'dashboard');
1648
- const installDir = path.join(process.cwd(), '.claude');
1649
- const installDashDir = path.join(installDir, 'dashboard');
1650
- if (fs.existsSync(dashboardAssetSrc)) {
1651
- // Preserve node_modules (contains native addons like node-pty) across refreshes
1652
- const nodeModulesDir = path.join(installDashDir, 'node_modules');
1653
- const nodeModulesTmp = path.join(installDir, '_dashboard_node_modules_tmp');
1654
- const hadNodeModules = fs.existsSync(nodeModulesDir);
1655
- if (hadNodeModules) {
1656
- fs.renameSync(nodeModulesDir, nodeModulesTmp);
1657
- }
1658
- // Clean existing dashboard dir to prevent stale files from old installs
1659
- safeRmDir(installDashDir);
1660
- fs.mkdirSync(installDashDir, { recursive: true });
1661
- // Dashboard is now Vite+Express: server.js (self-contained) + client/ (static)
1662
- // No node_modules/ hoisting needed — all deps are bundled into server.js by tsdown.
1663
- copyDirRecursive(dashboardAssetSrc, installDashDir);
1664
- // Restore node_modules if it was preserved
1665
- if (hadNodeModules && fs.existsSync(nodeModulesTmp)) {
1666
- fs.renameSync(nodeModulesTmp, nodeModulesDir);
1667
- }
1668
- // Write/update dashboard.json
1669
- const dashConfigPath = path.join(installDir, 'dashboard.json');
1670
- if (!fs.existsSync(dashConfigPath)) {
1671
- fs.writeFileSync(dashConfigPath, JSON.stringify({ projectCwd: process.cwd() }, null, 2) + '\n');
1672
- }
1673
- }
1674
- // Resolve server path: local project first, then global
1675
- const localDashboard = path.join(process.cwd(), '.claude', 'dashboard', 'server.js');
1676
- const globalDashboard = path.join(os.homedir(), '.claude', 'dashboard', 'server.js');
1677
- let serverPath = null;
1678
- if (fs.existsSync(localDashboard)) {
1679
- serverPath = localDashboard;
1680
- }
1681
- else if (fs.existsSync(globalDashboard)) {
1682
- serverPath = globalDashboard;
1683
- }
1684
- if (!serverPath) {
1685
- console.log(chalk_1.default.yellow('\n Dashboard not available.\n'));
1686
- console.log(' Install MAXSIM first: ' + chalk_1.default.cyan('npx maxsimcli@latest') + '\n');
1687
- process.exit(0);
1688
- }
1689
- // --network flag overrides stored config (lets users enable network mode ad-hoc)
1690
- const forceNetwork = !!argv['network'];
1691
- // Read projectCwd from dashboard.json (one level up from dashboard/ dir)
1692
- const dashboardDir = path.dirname(serverPath);
1693
- const dashboardConfigPath = path.join(path.dirname(dashboardDir), 'dashboard.json');
1694
- let projectCwd = process.cwd();
1695
- let networkMode = forceNetwork;
1696
- if (fs.existsSync(dashboardConfigPath)) {
1697
- try {
1698
- const config = JSON.parse(fs.readFileSync(dashboardConfigPath, 'utf8'));
1699
- if (config.projectCwd) {
1700
- projectCwd = config.projectCwd;
1701
- }
1702
- if (!forceNetwork) {
1703
- networkMode = config.networkMode ?? false;
1704
- }
1705
- }
1706
- catch {
1707
- // Use default cwd
1708
- }
1709
- }
1710
- // node-pty is a native addon that cannot be bundled — auto-install if missing
1711
- const dashDirForPty = path.dirname(serverPath);
1712
- const ptyModulePath = path.join(dashDirForPty, 'node_modules', 'node-pty');
1713
- if (!fs.existsSync(ptyModulePath)) {
1714
- console.log(chalk_1.default.gray(' Installing node-pty for terminal support...'));
1715
- try {
1716
- // Ensure a package.json exists so npm install works in the dashboard dir
1717
- const dashPkgPath = path.join(dashDirForPty, 'package.json');
1718
- if (!fs.existsSync(dashPkgPath)) {
1719
- fs.writeFileSync(dashPkgPath, '{"private":true}\n');
1720
- }
1721
- execSyncDash('npm install node-pty --save-optional --no-audit --no-fund --loglevel=error', {
1722
- cwd: dashDirForPty,
1723
- stdio: 'inherit',
1724
- timeout: 120_000,
1725
- });
1726
- }
1727
- catch {
1728
- console.warn(chalk_1.default.yellow(' node-pty installation failed — terminal will be unavailable.'));
1729
- }
1730
- }
1731
- console.log(chalk_1.default.blue('Starting dashboard...'));
1732
- console.log(chalk_1.default.gray(` Project: ${projectCwd}`));
1733
- console.log(chalk_1.default.gray(` Server: ${serverPath}`));
1734
- if (networkMode) {
1735
- console.log(chalk_1.default.gray(' Network: enabled (local network access + QR code)'));
1736
- }
1737
- console.log('');
1738
- // Use stdio: 'ignore' (fully detached) — a piped stderr causes the server to crash on
1739
- // Windows when the read-end is closed after the parent reads the ready message (EPIPE).
1740
- const child = spawnDash(process.execPath, [serverPath], {
1741
- cwd: dashboardDir,
1742
- detached: true,
1743
- stdio: 'ignore',
1744
- env: {
1745
- ...process.env,
1746
- MAXSIM_PROJECT_CWD: projectCwd,
1747
- MAXSIM_NETWORK_MODE: networkMode ? '1' : '0',
1748
- NODE_ENV: 'production',
1749
- },
1750
- });
1751
- child.unref();
1752
- // Poll /api/health until the server is ready (or 20s timeout).
1753
- // Health polling avoids any pipe between parent and child, so the server
1754
- // process stays alive after the parent exits.
1755
- const POLL_INTERVAL_MS = 500;
1756
- const POLL_TIMEOUT_MS = 20000;
1757
- const HEALTH_TIMEOUT_MS = 1000;
1758
- const DEFAULT_PORT = 3333;
1759
- const PORT_RANGE_END = 3343;
1760
- let foundUrl = null;
1761
- const deadline = Date.now() + POLL_TIMEOUT_MS;
1762
- while (Date.now() < deadline) {
1763
- await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
1764
- for (let p = DEFAULT_PORT; p <= PORT_RANGE_END; p++) {
1765
- try {
1766
- const controller = new AbortController();
1767
- const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
1768
- const res = await fetch(`http://localhost:${p}/api/health`, { signal: controller.signal });
1769
- clearTimeout(timer);
1770
- if (res.ok) {
1771
- const data = await res.json();
1772
- if (data.status === 'ok') {
1773
- foundUrl = `http://localhost:${p}`;
1774
- break;
1775
- }
1776
- }
1777
- }
1778
- catch { /* not ready yet */ }
1779
- }
1780
- if (foundUrl)
1781
- break;
1782
- }
1783
- if (foundUrl) {
1784
- console.log(chalk_1.default.green(` Dashboard ready at ${foundUrl}`));
1785
- }
1786
- else {
1787
- console.log(chalk_1.default.yellow('\n Dashboard did not respond after 20s. The server may still be starting — check http://localhost:3333'));
1788
- }
1789
- process.exit(0);
1790
- }
1791
- if (hasGlobal && hasLocal) {
1792
- console.error(chalk_1.default.yellow('Cannot specify both --global and --local'));
1793
- process.exit(1);
1794
- }
1795
- else if (explicitConfigDir && hasLocal) {
1796
- console.error(chalk_1.default.yellow('Cannot use --config-dir with --local'));
1797
- process.exit(1);
1798
- }
1799
- else if (hasUninstall) {
1800
- if (!hasGlobal && !hasLocal) {
1801
- console.error(chalk_1.default.yellow('--uninstall requires --global or --local'));
1802
- process.exit(1);
1803
- }
1804
- const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
1805
- for (const runtime of runtimes) {
1806
- uninstall(hasGlobal, runtime);
1807
- }
1808
- }
1809
- else if (selectedRuntimes.length > 0) {
1810
- if (!hasGlobal && !hasLocal) {
1811
- const isGlobal = await promptLocation(selectedRuntimes);
1812
- await installAllRuntimes(selectedRuntimes, isGlobal, true);
1813
- }
1814
- else {
1815
- await installAllRuntimes(selectedRuntimes, hasGlobal, false);
1816
- }
1817
- }
1818
- else if (hasGlobal || hasLocal) {
1819
- await installAllRuntimes(['claude'], hasGlobal, false);
1820
- }
1821
- else {
1822
- if (!process.stdin.isTTY) {
1823
- console.log(chalk_1.default.yellow('Non-interactive terminal detected, defaulting to Claude Code global install') + '\n');
1824
- await installAllRuntimes(['claude'], true, false);
1825
- }
1826
- else {
1827
- const runtimes = await promptRuntime();
1828
- const isGlobal = await promptLocation(runtimes);
1829
- await installAllRuntimes(runtimes, isGlobal, true);
1830
- }
1831
- }
1832
- })().catch((err) => {
1833
- if (err instanceof Error && err.message.includes('User force closed')) {
1834
- // User pressed Ctrl+C during an @inquirer/prompts prompt — exit cleanly
1835
- console.log('\n' + chalk_1.default.yellow('Installation cancelled') + '\n');
1836
- process.exit(0);
1837
- }
1838
- console.error(chalk_1.default.red('Unexpected error:'), err);
1839
- process.exit(1);
1840
- });
1841
- //# sourceMappingURL=install.js.map