maxsimcli 3.10.3 → 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 (142) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/assets/CHANGELOG.md +26 -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/dashboard/server.js +5 -1
  7. package/dist/assets/templates/agents/AGENTS.md +82 -0
  8. package/dist/assets/templates/commands/maxsim/settings.md +1 -1
  9. package/dist/assets/templates/skills/code-review/SKILL.md +151 -0
  10. package/dist/assets/templates/skills/memory-management/SKILL.md +174 -0
  11. package/dist/assets/templates/skills/simplify/SKILL.md +137 -0
  12. package/dist/assets/templates/skills/using-maxsim/SKILL.md +115 -0
  13. package/dist/assets/templates/templates/config.json +1 -1
  14. package/dist/assets/templates/workflows/add-tests.md +3 -3
  15. package/dist/assets/templates/workflows/complete-milestone.md +1 -1
  16. package/dist/assets/templates/workflows/execute-phase.md +4 -14
  17. package/dist/assets/templates/workflows/execute-plan.md +10 -0
  18. package/dist/assets/templates/workflows/init-existing.md +7 -3
  19. package/dist/assets/templates/workflows/new-milestone.md +4 -0
  20. package/dist/assets/templates/workflows/new-project.md +6 -2
  21. package/dist/assets/templates/workflows/plan-phase.md +2 -2
  22. package/dist/assets/templates/workflows/settings.md +8 -4
  23. package/dist/assets/templates/workflows/verify-work.md +1 -1
  24. package/dist/cli.cjs +818 -599
  25. package/dist/cli.cjs.map +1 -1
  26. package/dist/cli.js +78 -204
  27. package/dist/cli.js.map +1 -1
  28. package/dist/core/commands.d.ts +7 -0
  29. package/dist/core/commands.d.ts.map +1 -1
  30. package/dist/core/commands.js +40 -35
  31. package/dist/core/commands.js.map +1 -1
  32. package/dist/core/core.d.ts +39 -1
  33. package/dist/core/core.d.ts.map +1 -1
  34. package/dist/core/core.js +122 -47
  35. package/dist/core/core.js.map +1 -1
  36. package/dist/core/dashboard-launcher.d.ts +56 -0
  37. package/dist/core/dashboard-launcher.d.ts.map +1 -0
  38. package/dist/core/dashboard-launcher.js +243 -0
  39. package/dist/core/dashboard-launcher.js.map +1 -0
  40. package/dist/core/index.d.ts +4 -2
  41. package/dist/core/index.d.ts.map +1 -1
  42. package/dist/core/index.js +20 -2
  43. package/dist/core/index.js.map +1 -1
  44. package/dist/core/init.d.ts +2 -3
  45. package/dist/core/init.d.ts.map +1 -1
  46. package/dist/core/init.js +33 -52
  47. package/dist/core/init.js.map +1 -1
  48. package/dist/core/milestone.d.ts.map +1 -1
  49. package/dist/core/milestone.js +15 -20
  50. package/dist/core/milestone.js.map +1 -1
  51. package/dist/core/phase.d.ts +33 -0
  52. package/dist/core/phase.d.ts.map +1 -1
  53. package/dist/core/phase.js +282 -225
  54. package/dist/core/phase.js.map +1 -1
  55. package/dist/core/roadmap.d.ts.map +1 -1
  56. package/dist/core/roadmap.js +17 -18
  57. package/dist/core/roadmap.js.map +1 -1
  58. package/dist/core/state.d.ts +5 -0
  59. package/dist/core/state.d.ts.map +1 -1
  60. package/dist/core/state.js +51 -42
  61. package/dist/core/state.js.map +1 -1
  62. package/dist/core/template.d.ts.map +1 -1
  63. package/dist/core/template.js +1 -1
  64. package/dist/core/template.js.map +1 -1
  65. package/dist/core/types.d.ts +4 -4
  66. package/dist/core/types.d.ts.map +1 -1
  67. package/dist/core/types.js +1 -2
  68. package/dist/core/types.js.map +1 -1
  69. package/dist/core/verify.d.ts.map +1 -1
  70. package/dist/core/verify.js +61 -80
  71. package/dist/core/verify.js.map +1 -1
  72. package/dist/install/adapters.d.ts +15 -0
  73. package/dist/install/adapters.d.ts.map +1 -0
  74. package/dist/install/adapters.js +203 -0
  75. package/dist/install/adapters.js.map +1 -0
  76. package/dist/install/copy.d.ts +15 -0
  77. package/dist/install/copy.d.ts.map +1 -0
  78. package/dist/install/copy.js +191 -0
  79. package/dist/install/copy.js.map +1 -0
  80. package/dist/install/dashboard.d.ts +16 -0
  81. package/dist/install/dashboard.d.ts.map +1 -0
  82. package/dist/install/dashboard.js +273 -0
  83. package/dist/install/dashboard.js.map +1 -0
  84. package/dist/install/hooks.d.ts +32 -0
  85. package/dist/install/hooks.d.ts.map +1 -0
  86. package/dist/install/hooks.js +285 -0
  87. package/dist/install/hooks.js.map +1 -0
  88. package/dist/install/index.d.ts +2 -0
  89. package/dist/install/index.d.ts.map +1 -0
  90. package/dist/install/index.js +598 -0
  91. package/dist/install/index.js.map +1 -0
  92. package/dist/install/manifest.d.ts +20 -0
  93. package/dist/install/manifest.d.ts.map +1 -0
  94. package/dist/install/manifest.js +135 -0
  95. package/dist/install/manifest.js.map +1 -0
  96. package/dist/install/patches.d.ts +11 -0
  97. package/dist/install/patches.d.ts.map +1 -0
  98. package/dist/install/patches.js +136 -0
  99. package/dist/install/patches.js.map +1 -0
  100. package/dist/install/shared.d.ts +50 -0
  101. package/dist/install/shared.d.ts.map +1 -0
  102. package/dist/install/shared.js +142 -0
  103. package/dist/install/shared.js.map +1 -0
  104. package/dist/install/uninstall.d.ts +6 -0
  105. package/dist/install/uninstall.d.ts.map +1 -0
  106. package/dist/install/uninstall.js +280 -0
  107. package/dist/install/uninstall.js.map +1 -0
  108. package/dist/install.cjs +782 -705
  109. package/dist/install.cjs.map +1 -1
  110. package/dist/mcp/index.d.ts +12 -0
  111. package/dist/mcp/index.d.ts.map +1 -0
  112. package/dist/mcp/index.js +21 -0
  113. package/dist/mcp/index.js.map +1 -0
  114. package/dist/mcp/phase-tools.d.ts +13 -0
  115. package/dist/mcp/phase-tools.d.ts.map +1 -0
  116. package/dist/mcp/phase-tools.js +164 -0
  117. package/dist/mcp/phase-tools.js.map +1 -0
  118. package/dist/mcp/state-tools.d.ts +13 -0
  119. package/dist/mcp/state-tools.d.ts.map +1 -0
  120. package/dist/mcp/state-tools.js +185 -0
  121. package/dist/mcp/state-tools.js.map +1 -0
  122. package/dist/mcp/todo-tools.d.ts +13 -0
  123. package/dist/mcp/todo-tools.d.ts.map +1 -0
  124. package/dist/mcp/todo-tools.js +143 -0
  125. package/dist/mcp/todo-tools.js.map +1 -0
  126. package/dist/mcp/utils.d.ts +27 -0
  127. package/dist/mcp/utils.d.ts.map +1 -0
  128. package/dist/mcp/utils.js +82 -0
  129. package/dist/mcp/utils.js.map +1 -0
  130. package/dist/mcp-server.cjs +11806 -0
  131. package/dist/mcp-server.cjs.map +1 -0
  132. package/dist/mcp-server.d.cts +2 -0
  133. package/dist/mcp-server.d.ts +12 -0
  134. package/dist/mcp-server.d.ts.map +1 -0
  135. package/dist/mcp-server.js +31 -0
  136. package/dist/mcp-server.js.map +1 -0
  137. package/package.json +5 -3
  138. package/dist/assets/dashboard/client/assets/index-DzJChB-D.css +0 -32
  139. package/dist/install.d.ts +0 -2
  140. package/dist/install.d.ts.map +0 -1
  141. package/dist/install.js +0 -1804
  142. package/dist/install.js.map +0 -1
package/dist/install.cjs CHANGED
@@ -33,9 +33,6 @@ let node_path = require("node:path");
33
33
  node_path = __toESM(node_path);
34
34
  let node_os = require("node:os");
35
35
  node_os = __toESM(node_os);
36
- let node_crypto = require("node:crypto");
37
- node_crypto = __toESM(node_crypto);
38
- let node_child_process = require("node:child_process");
39
36
  let node_process = require("node:process");
40
37
  node_process = __toESM(node_process);
41
38
  let node_tty = require("node:tty");
@@ -46,6 +43,9 @@ let node_util = require("node:util");
46
43
  let node_async_hooks = require("node:async_hooks");
47
44
  let node_readline = require("node:readline");
48
45
  node_readline = __toESM(node_readline);
46
+ let node_crypto = require("node:crypto");
47
+ node_crypto = __toESM(node_crypto);
48
+ let node_child_process = require("node:child_process");
49
49
 
50
50
  //#region ../../node_modules/universalify/index.js
51
51
  var require_universalify = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -8284,103 +8284,9 @@ const codexAdapter = {
8284
8284
  };
8285
8285
 
8286
8286
  //#endregion
8287
- //#region src/install.ts
8287
+ //#region src/install/shared.ts
8288
8288
  const pkg = JSON.parse(node_fs.readFileSync(node_path.resolve(__dirname, "..", "package.json"), "utf-8"));
8289
8289
  const templatesRoot = node_path.resolve(__dirname, "assets", "templates");
8290
- const argv = (0, import_minimist.default)(process.argv.slice(2), {
8291
- boolean: [
8292
- "global",
8293
- "local",
8294
- "opencode",
8295
- "claude",
8296
- "gemini",
8297
- "codex",
8298
- "both",
8299
- "all",
8300
- "uninstall",
8301
- "help",
8302
- "version",
8303
- "force-statusline",
8304
- "network"
8305
- ],
8306
- string: ["config-dir"],
8307
- alias: {
8308
- g: "global",
8309
- l: "local",
8310
- u: "uninstall",
8311
- h: "help",
8312
- c: "config-dir"
8313
- }
8314
- });
8315
- const hasGlobal = !!argv["global"];
8316
- const hasLocal = !!argv["local"];
8317
- const hasOpencode = !!argv["opencode"];
8318
- const hasClaude = !!argv["claude"];
8319
- const hasGemini = !!argv["gemini"];
8320
- const hasCodex = !!argv["codex"];
8321
- const hasBoth = !!argv["both"];
8322
- const hasAll = !!argv["all"];
8323
- const hasUninstall = !!argv["uninstall"];
8324
- let selectedRuntimes = [];
8325
- if (hasAll) selectedRuntimes = [
8326
- "claude",
8327
- "opencode",
8328
- "gemini",
8329
- "codex"
8330
- ];
8331
- else if (hasBoth) selectedRuntimes = ["claude", "opencode"];
8332
- else {
8333
- if (hasOpencode) selectedRuntimes.push("opencode");
8334
- if (hasClaude) selectedRuntimes.push("claude");
8335
- if (hasGemini) selectedRuntimes.push("gemini");
8336
- if (hasCodex) selectedRuntimes.push("codex");
8337
- }
8338
- /**
8339
- * Add a firewall rule to allow inbound traffic on the given port.
8340
- * Handles Windows (netsh), Linux (ufw / iptables), and macOS (no rule needed).
8341
- */
8342
- /** Check whether the current process is running with admin/root privileges. */
8343
- function isElevated() {
8344
- if (process.platform === "win32") try {
8345
- (0, node_child_process.execSync)("net session", { stdio: "pipe" });
8346
- return true;
8347
- } catch {
8348
- return false;
8349
- }
8350
- return process.getuid?.() === 0;
8351
- }
8352
- function applyFirewallRule(port) {
8353
- const platform = process.platform;
8354
- try {
8355
- if (platform === "win32") {
8356
- const cmd = `netsh advfirewall firewall add rule name="MAXSIM Dashboard" dir=in action=allow protocol=TCP localport=${port}`;
8357
- if (isElevated()) {
8358
- (0, node_child_process.execSync)(cmd, { stdio: "pipe" });
8359
- console.log(chalk.green(" ✓ Windows Firewall rule added for port " + port));
8360
- } else {
8361
- console.log(chalk.gray(" Requesting administrator privileges for firewall rule..."));
8362
- (0, node_child_process.execSync)(`powershell -NoProfile -Command "${`Start-Process cmd -ArgumentList '/c ${cmd}' -Verb RunAs -Wait`}"`, { stdio: "pipe" });
8363
- console.log(chalk.green(" ✓ Windows Firewall rule added for port " + port));
8364
- }
8365
- } else if (platform === "linux") {
8366
- const sudoPrefix = isElevated() ? "" : "sudo ";
8367
- try {
8368
- (0, node_child_process.execSync)(`${sudoPrefix}ufw allow ${port}/tcp`, { stdio: "pipe" });
8369
- console.log(chalk.green(" ✓ UFW rule added for port " + port));
8370
- } catch {
8371
- try {
8372
- (0, node_child_process.execSync)(`${sudoPrefix}iptables -A INPUT -p tcp --dport ${port} -j ACCEPT`, { stdio: "pipe" });
8373
- console.log(chalk.green(" ✓ iptables rule added for port " + port));
8374
- } catch {
8375
- console.log(chalk.yellow(` ⚠ Could not add firewall rule automatically. Run: sudo ufw allow ${port}/tcp`));
8376
- }
8377
- }
8378
- } else if (platform === "darwin") console.log(chalk.gray(" macOS: No firewall rule needed (inbound connections are allowed by default)"));
8379
- } catch (err) {
8380
- console.warn(chalk.yellow(` ⚠ Firewall rule failed: ${err.message}`));
8381
- console.warn(chalk.gray(` You may need to manually allow port ${port} through your firewall.`));
8382
- }
8383
- }
8384
8290
  /**
8385
8291
  * Adapter registry keyed by runtime name
8386
8292
  */
@@ -8434,26 +8340,44 @@ function copyDirRecursive(src, dest) {
8434
8340
  function getOpencodeGlobalDir() {
8435
8341
  return opencodeAdapter.getGlobalDir();
8436
8342
  }
8437
- const banner = "\n" + chalk.cyan(figlet.default.textSync("MAXSIM", { font: "ANSI Shadow" }).split("\n").map((line) => " " + line).join("\n")) + "\n\n MAXSIM " + chalk.dim("v" + pkg.version) + "\n A meta-prompting, context engineering and spec-driven\n development system for Claude Code, OpenCode, Gemini, and Codex.\n";
8438
- const explicitConfigDir = argv["config-dir"] || null;
8439
- const hasHelp = !!argv["help"];
8440
- const hasVersion = !!argv["version"];
8441
- const forceStatusline = !!argv["force-statusline"];
8442
- if (hasVersion) {
8443
- console.log(pkg.version);
8444
- process.exit(0);
8343
+ /**
8344
+ * Verify a directory exists and contains files
8345
+ */
8346
+ function verifyInstalled(dirPath, description) {
8347
+ if (!node_fs.existsSync(dirPath)) {
8348
+ console.error(` \u2717 Failed to install ${description}: directory not created`);
8349
+ return false;
8350
+ }
8351
+ try {
8352
+ if (node_fs.readdirSync(dirPath).length === 0) {
8353
+ console.error(` \u2717 Failed to install ${description}: directory is empty`);
8354
+ return false;
8355
+ }
8356
+ } catch (e) {
8357
+ console.error(` \u2717 Failed to install ${description}: ${e.message}`);
8358
+ return false;
8359
+ }
8360
+ return true;
8445
8361
  }
8446
- console.log(banner);
8447
- if (hasHelp) {
8448
- console.log(` ${chalk.yellow("Usage:")} npx maxsimcli [options]\n\n ${chalk.yellow("Options:")}\n ${chalk.cyan("-g, --global")} Install globally (to config directory)\n ${chalk.cyan("-l, --local")} Install locally (to current directory)\n ${chalk.cyan("--claude")} Install for Claude Code only\n ${chalk.cyan("--opencode")} Install for OpenCode only\n ${chalk.cyan("--gemini")} Install for Gemini only\n ${chalk.cyan("--codex")} Install for Codex only\n ${chalk.cyan("--all")} Install for all runtimes\n ${chalk.cyan("-u, --uninstall")} Uninstall MAXSIM (remove all MAXSIM files)\n ${chalk.cyan("-c, --config-dir <path>")} Specify custom config directory\n ${chalk.cyan("-h, --help")} Show this help message\n ${chalk.cyan("--force-statusline")} Replace existing statusline config\n\n ${chalk.yellow("Examples:")}\n ${chalk.dim("# Interactive install (prompts for runtime and location)")}\n npx maxsimcli\n\n ${chalk.dim("# Install for Claude Code globally")}\n npx maxsimcli --claude --global\n\n ${chalk.dim("# Install for Gemini globally")}\n npx maxsimcli --gemini --global\n\n ${chalk.dim("# Install for Codex globally")}\n npx maxsimcli --codex --global\n\n ${chalk.dim("# Install for all runtimes globally")}\n npx maxsimcli --all --global\n\n ${chalk.dim("# Install to custom config directory")}\n npx maxsimcli --codex --global --config-dir ~/.codex-work\n\n ${chalk.dim("# Install to current project only")}\n npx maxsimcli --claude --local\n\n ${chalk.dim("# Uninstall MAXSIM from Codex globally")}\n npx maxsimcli --codex --global --uninstall\n\n ${chalk.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`);
8449
- process.exit(0);
8362
+ /**
8363
+ * Verify a file exists
8364
+ */
8365
+ function verifyFileInstalled(filePath, description) {
8366
+ if (!node_fs.existsSync(filePath)) {
8367
+ console.error(` \u2717 Failed to install ${description}: file not created`);
8368
+ return false;
8369
+ }
8370
+ return true;
8450
8371
  }
8372
+
8373
+ //#endregion
8374
+ //#region src/install/adapters.ts
8451
8375
  const attributionCache = /* @__PURE__ */ new Map();
8452
8376
  /**
8453
8377
  * Get commit attribution setting for a runtime
8454
8378
  * @returns null = remove, undefined = keep default, string = custom
8455
8379
  */
8456
- function getCommitAttribution(runtime) {
8380
+ function getCommitAttribution(runtime, explicitConfigDir) {
8457
8381
  if (attributionCache.has(runtime)) return attributionCache.get(runtime);
8458
8382
  let result;
8459
8383
  if (runtime === "opencode") result = readSettings(node_path.join(getGlobalDir("opencode", null), "opencode.json")).disable_ai_attribution === true ? null : void 0;
@@ -8472,112 +8396,231 @@ function getCommitAttribution(runtime) {
8472
8396
  return result;
8473
8397
  }
8474
8398
  /**
8475
- * Copy commands to a flat structure for OpenCode
8476
- * OpenCode expects: command/maxsim-help.md (invoked as /maxsim-help)
8477
- * Source structure: commands/maxsim/help.md
8399
+ * Parse JSONC (JSON with Comments) by stripping comments and trailing commas.
8478
8400
  */
8479
- function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
8480
- if (!node_fs.existsSync(srcDir)) return;
8481
- if (node_fs.existsSync(destDir)) {
8482
- for (const file of node_fs.readdirSync(destDir)) if (file.startsWith(`${prefix}-`) && file.endsWith(".md")) node_fs.unlinkSync(node_path.join(destDir, file));
8483
- } else node_fs.mkdirSync(destDir, { recursive: true });
8484
- const entries = node_fs.readdirSync(srcDir, { withFileTypes: true });
8485
- for (const entry of entries) {
8486
- const srcPath = node_path.join(srcDir, entry.name);
8487
- if (entry.isDirectory()) copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
8488
- else if (entry.name.endsWith(".md")) {
8489
- const destName = `${prefix}-${entry.name.replace(".md", "")}.md`;
8490
- const destPath = node_path.join(destDir, destName);
8491
- let content = node_fs.readFileSync(srcPath, "utf8");
8492
- const globalClaudeRegex = /~\/\.claude\//g;
8493
- const localClaudeRegex = /\.\/\.claude\//g;
8494
- const opencodeDirRegex = /~\/\.opencode\//g;
8495
- content = content.replace(globalClaudeRegex, pathPrefix);
8496
- content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
8497
- content = content.replace(opencodeDirRegex, pathPrefix);
8498
- content = processAttribution(content, getCommitAttribution(runtime));
8499
- content = convertClaudeToOpencodeFrontmatter(content);
8500
- node_fs.writeFileSync(destPath, content);
8501
- }
8502
- }
8503
- }
8504
- function listCodexSkillNames(skillsDir, prefix = "maxsim-") {
8505
- if (!node_fs.existsSync(skillsDir)) return [];
8506
- return node_fs.readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix)).filter((entry) => node_fs.existsSync(node_path.join(skillsDir, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
8507
- }
8508
- function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
8509
- if (!node_fs.existsSync(srcDir)) return;
8510
- node_fs.mkdirSync(skillsDir, { recursive: true });
8511
- const existing = node_fs.readdirSync(skillsDir, { withFileTypes: true });
8512
- for (const entry of existing) if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) node_fs.rmSync(node_path.join(skillsDir, entry.name), { recursive: true });
8513
- function recurse(currentSrcDir, currentPrefix) {
8514
- const entries = node_fs.readdirSync(currentSrcDir, { withFileTypes: true });
8515
- for (const entry of entries) {
8516
- const srcPath = node_path.join(currentSrcDir, entry.name);
8517
- if (entry.isDirectory()) {
8518
- recurse(srcPath, `${currentPrefix}-${entry.name}`);
8401
+ function parseJsonc(content) {
8402
+ if (content.charCodeAt(0) === 65279) content = content.slice(1);
8403
+ let result = "";
8404
+ let inString = false;
8405
+ let i = 0;
8406
+ while (i < content.length) {
8407
+ const char = content[i];
8408
+ const next = content[i + 1];
8409
+ if (inString) {
8410
+ result += char;
8411
+ if (char === "\\" && i + 1 < content.length) {
8412
+ result += next;
8413
+ i += 2;
8519
8414
  continue;
8520
8415
  }
8521
- if (!entry.name.endsWith(".md")) continue;
8522
- const skillName = `${currentPrefix}-${entry.name.replace(".md", "")}`;
8523
- const skillDir = node_path.join(skillsDir, skillName);
8524
- node_fs.mkdirSync(skillDir, { recursive: true });
8525
- let content = node_fs.readFileSync(srcPath, "utf8");
8526
- const globalClaudeRegex = /~\/\.claude\//g;
8527
- const localClaudeRegex = /\.\/\.claude\//g;
8528
- const codexDirRegex = /~\/\.codex\//g;
8529
- content = content.replace(globalClaudeRegex, pathPrefix);
8530
- content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
8531
- content = content.replace(codexDirRegex, pathPrefix);
8532
- content = processAttribution(content, getCommitAttribution(runtime));
8533
- content = convertClaudeCommandToCodexSkill(content, skillName);
8534
- node_fs.writeFileSync(node_path.join(skillDir, "SKILL.md"), content);
8416
+ if (char === "\"") inString = false;
8417
+ i++;
8418
+ } else if (char === "\"") {
8419
+ inString = true;
8420
+ result += char;
8421
+ i++;
8422
+ } else if (char === "/" && next === "/") while (i < content.length && content[i] !== "\n") i++;
8423
+ else if (char === "/" && next === "*") {
8424
+ i += 2;
8425
+ while (i < content.length - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
8426
+ i += 2;
8427
+ } else {
8428
+ result += char;
8429
+ i++;
8535
8430
  }
8536
8431
  }
8537
- recurse(srcDir, prefix);
8538
- }
8539
- /**
8540
- * Recursively copy directory, replacing paths in .md files
8541
- * Deletes existing destDir first to remove orphaned files from previous versions
8542
- */
8543
- function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false) {
8544
- const isOpencode = runtime === "opencode";
8545
- const isCodex = runtime === "codex";
8546
- const dirName = getDirName(runtime);
8547
- if (node_fs.existsSync(destDir)) node_fs.rmSync(destDir, { recursive: true });
8548
- node_fs.mkdirSync(destDir, { recursive: true });
8549
- const entries = node_fs.readdirSync(srcDir, { withFileTypes: true });
8550
- for (const entry of entries) {
8551
- const srcPath = node_path.join(srcDir, entry.name);
8552
- const destPath = node_path.join(destDir, entry.name);
8553
- if (entry.isDirectory()) copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand);
8554
- else if (entry.name.endsWith(".md")) {
8555
- let content = node_fs.readFileSync(srcPath, "utf8");
8556
- const globalClaudeRegex = /~\/\.claude\//g;
8557
- const localClaudeRegex = /\.\/\.claude\//g;
8558
- content = content.replace(globalClaudeRegex, pathPrefix);
8559
- content = content.replace(localClaudeRegex, `./${dirName}/`);
8560
- content = processAttribution(content, getCommitAttribution(runtime));
8561
- if (isOpencode) {
8562
- content = convertClaudeToOpencodeFrontmatter(content);
8563
- node_fs.writeFileSync(destPath, content);
8564
- } else if (runtime === "gemini") if (isCommand) {
8565
- content = stripSubTags(content);
8566
- const tomlContent = convertClaudeToGeminiToml(content);
8567
- const tomlPath = destPath.replace(/\.md$/, ".toml");
8568
- node_fs.writeFileSync(tomlPath, tomlContent);
8569
- } else node_fs.writeFileSync(destPath, content);
8570
- else if (isCodex) {
8571
- content = convertClaudeToCodexMarkdown(content);
8572
- node_fs.writeFileSync(destPath, content);
8573
- } else node_fs.writeFileSync(destPath, content);
8574
- } else node_fs.copyFileSync(srcPath, destPath);
8575
- }
8432
+ result = result.replace(/,(\s*[}\]])/g, "$1");
8433
+ return JSON.parse(result);
8576
8434
  }
8577
8435
  /**
8578
- * Clean up orphaned files from previous MAXSIM versions
8436
+ * Configure OpenCode permissions to allow reading MAXSIM reference docs
8579
8437
  */
8580
- function cleanupOrphanedFiles(configDir) {
8438
+ function configureOpencodePermissions(isGlobal = true) {
8439
+ const opencodeConfigDir = isGlobal ? getOpencodeGlobalDir() : node_path.join(process.cwd(), ".opencode");
8440
+ const configPath = node_path.join(opencodeConfigDir, "opencode.json");
8441
+ node_fs.mkdirSync(opencodeConfigDir, { recursive: true });
8442
+ let config = {};
8443
+ if (node_fs.existsSync(configPath)) try {
8444
+ config = parseJsonc(node_fs.readFileSync(configPath, "utf8"));
8445
+ } catch (e) {
8446
+ console.log(` ${chalk.yellow("⚠")} Could not parse opencode.json - skipping permission config`);
8447
+ console.log(` ${chalk.dim(`Reason: ${e.message}`)}`);
8448
+ console.log(` ${chalk.dim("Your config was NOT modified. Fix the syntax manually if needed.")}`);
8449
+ return;
8450
+ }
8451
+ if (!config.permission) config.permission = {};
8452
+ const permission = config.permission;
8453
+ const maxsimPath = opencodeConfigDir === node_path.join(node_os.homedir(), ".config", "opencode") ? "~/.config/opencode/maxsim/*" : `${opencodeConfigDir.replace(/\\/g, "/")}/maxsim/*`;
8454
+ let modified = false;
8455
+ if (!permission.read || typeof permission.read !== "object") permission.read = {};
8456
+ if (permission.read[maxsimPath] !== "allow") {
8457
+ permission.read[maxsimPath] = "allow";
8458
+ modified = true;
8459
+ }
8460
+ if (!permission.external_directory || typeof permission.external_directory !== "object") permission.external_directory = {};
8461
+ if (permission.external_directory[maxsimPath] !== "allow") {
8462
+ permission.external_directory[maxsimPath] = "allow";
8463
+ modified = true;
8464
+ }
8465
+ if (!modified) return;
8466
+ node_fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
8467
+ console.log(` ${chalk.green("✓")} Configured read permission for MAXSIM docs`);
8468
+ }
8469
+
8470
+ //#endregion
8471
+ //#region src/install/dashboard.ts
8472
+ /** Check whether the current process is running with admin/root privileges. */
8473
+ function isElevated() {
8474
+ if (process.platform === "win32") try {
8475
+ (0, node_child_process.execSync)("net session", { stdio: "pipe" });
8476
+ return true;
8477
+ } catch {
8478
+ return false;
8479
+ }
8480
+ return process.getuid?.() === 0;
8481
+ }
8482
+ /**
8483
+ * Add a firewall rule to allow inbound traffic on the given port.
8484
+ * Handles Windows (netsh), Linux (ufw / iptables), and macOS (no rule needed).
8485
+ */
8486
+ function applyFirewallRule(port) {
8487
+ const platform = process.platform;
8488
+ try {
8489
+ if (platform === "win32") {
8490
+ const cmd = `netsh advfirewall firewall add rule name="MAXSIM Dashboard" dir=in action=allow protocol=TCP localport=${port}`;
8491
+ if (isElevated()) {
8492
+ (0, node_child_process.execSync)(cmd, { stdio: "pipe" });
8493
+ console.log(chalk.green(" ✓ Windows Firewall rule added for port " + port));
8494
+ } else {
8495
+ console.log(chalk.gray(" Requesting administrator privileges for firewall rule..."));
8496
+ (0, node_child_process.execSync)(`powershell -NoProfile -Command "${`Start-Process cmd -ArgumentList '/c ${cmd}' -Verb RunAs -Wait`}"`, { stdio: "pipe" });
8497
+ console.log(chalk.green(" ✓ Windows Firewall rule added for port " + port));
8498
+ }
8499
+ } else if (platform === "linux") {
8500
+ const sudoPrefix = isElevated() ? "" : "sudo ";
8501
+ try {
8502
+ (0, node_child_process.execSync)(`${sudoPrefix}ufw allow ${port}/tcp`, { stdio: "pipe" });
8503
+ console.log(chalk.green(" ✓ UFW rule added for port " + port));
8504
+ } catch {
8505
+ try {
8506
+ (0, node_child_process.execSync)(`${sudoPrefix}iptables -A INPUT -p tcp --dport ${port} -j ACCEPT`, { stdio: "pipe" });
8507
+ console.log(chalk.green(" ✓ iptables rule added for port " + port));
8508
+ } catch {
8509
+ console.log(chalk.yellow(` \u26a0 Could not add firewall rule automatically. Run: sudo ufw allow ${port}/tcp`));
8510
+ }
8511
+ }
8512
+ } else if (platform === "darwin") console.log(chalk.gray(" macOS: No firewall rule needed (inbound connections are allowed by default)"));
8513
+ } catch (err) {
8514
+ console.warn(chalk.yellow(` \u26a0 Firewall rule failed: ${err.message}`));
8515
+ console.warn(chalk.gray(` You may need to manually allow port ${port} through your firewall.`));
8516
+ }
8517
+ }
8518
+ /**
8519
+ * Handle the `dashboard` subcommand — refresh assets, install node-pty, launch server
8520
+ */
8521
+ async function runDashboardSubcommand(argv) {
8522
+ const { spawn: spawnDash, execSync: execSyncDash } = await import("node:child_process");
8523
+ const dashboardAssetSrc = node_path.resolve(__dirname, "assets", "dashboard");
8524
+ const installDir = node_path.join(process.cwd(), ".claude");
8525
+ const installDashDir = node_path.join(installDir, "dashboard");
8526
+ if (node_fs.existsSync(dashboardAssetSrc)) {
8527
+ const nodeModulesDir = node_path.join(installDashDir, "node_modules");
8528
+ const nodeModulesTmp = node_path.join(installDir, "_dashboard_node_modules_tmp");
8529
+ const hadNodeModules = node_fs.existsSync(nodeModulesDir);
8530
+ if (hadNodeModules) node_fs.renameSync(nodeModulesDir, nodeModulesTmp);
8531
+ safeRmDir(installDashDir);
8532
+ node_fs.mkdirSync(installDashDir, { recursive: true });
8533
+ copyDirRecursive(dashboardAssetSrc, installDashDir);
8534
+ if (hadNodeModules && node_fs.existsSync(nodeModulesTmp)) node_fs.renameSync(nodeModulesTmp, nodeModulesDir);
8535
+ const dashConfigPath = node_path.join(installDir, "dashboard.json");
8536
+ if (!node_fs.existsSync(dashConfigPath)) node_fs.writeFileSync(dashConfigPath, JSON.stringify({ projectCwd: process.cwd() }, null, 2) + "\n");
8537
+ }
8538
+ const localDashboard = node_path.join(process.cwd(), ".claude", "dashboard", "server.js");
8539
+ const globalDashboard = node_path.join(node_os.homedir(), ".claude", "dashboard", "server.js");
8540
+ let serverPath = null;
8541
+ if (node_fs.existsSync(localDashboard)) serverPath = localDashboard;
8542
+ else if (node_fs.existsSync(globalDashboard)) serverPath = globalDashboard;
8543
+ if (!serverPath) {
8544
+ console.log(chalk.yellow("\n Dashboard not available.\n"));
8545
+ console.log(" Install MAXSIM first: " + chalk.cyan("npx maxsimcli@latest") + "\n");
8546
+ process.exit(0);
8547
+ }
8548
+ const forceNetwork = !!argv["network"];
8549
+ const dashboardDir = node_path.dirname(serverPath);
8550
+ const dashboardConfigPath = node_path.join(node_path.dirname(dashboardDir), "dashboard.json");
8551
+ let projectCwd = process.cwd();
8552
+ let networkMode = forceNetwork;
8553
+ if (node_fs.existsSync(dashboardConfigPath)) try {
8554
+ const config = JSON.parse(node_fs.readFileSync(dashboardConfigPath, "utf8"));
8555
+ if (config.projectCwd) projectCwd = config.projectCwd;
8556
+ if (!forceNetwork) networkMode = config.networkMode ?? false;
8557
+ } catch {}
8558
+ const dashDirForPty = node_path.dirname(serverPath);
8559
+ const ptyModulePath = node_path.join(dashDirForPty, "node_modules", "node-pty");
8560
+ if (!node_fs.existsSync(ptyModulePath)) {
8561
+ console.log(chalk.gray(" Installing node-pty for terminal support..."));
8562
+ try {
8563
+ const dashPkgPath = node_path.join(dashDirForPty, "package.json");
8564
+ if (!node_fs.existsSync(dashPkgPath)) node_fs.writeFileSync(dashPkgPath, "{\"private\":true}\n");
8565
+ execSyncDash("npm install node-pty --save-optional --no-audit --no-fund --loglevel=error", {
8566
+ cwd: dashDirForPty,
8567
+ stdio: "inherit",
8568
+ timeout: 12e4
8569
+ });
8570
+ } catch {
8571
+ console.warn(chalk.yellow(" node-pty installation failed — terminal will be unavailable."));
8572
+ }
8573
+ }
8574
+ console.log(chalk.blue("Starting dashboard..."));
8575
+ console.log(chalk.gray(` Project: ${projectCwd}`));
8576
+ console.log(chalk.gray(` Server: ${serverPath}`));
8577
+ if (networkMode) console.log(chalk.gray(" Network: enabled (local network access + QR code)"));
8578
+ console.log("");
8579
+ spawnDash(process.execPath, [serverPath], {
8580
+ cwd: dashboardDir,
8581
+ detached: true,
8582
+ stdio: "ignore",
8583
+ env: {
8584
+ ...process.env,
8585
+ MAXSIM_PROJECT_CWD: projectCwd,
8586
+ MAXSIM_NETWORK_MODE: networkMode ? "1" : "0",
8587
+ NODE_ENV: "production"
8588
+ }
8589
+ }).unref();
8590
+ const POLL_INTERVAL_MS = 500;
8591
+ const POLL_TIMEOUT_MS = 2e4;
8592
+ const HEALTH_TIMEOUT_MS = 1e3;
8593
+ const DEFAULT_PORT = 3333;
8594
+ const PORT_RANGE_END = 3343;
8595
+ let foundUrl = null;
8596
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
8597
+ while (Date.now() < deadline) {
8598
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
8599
+ for (let p = DEFAULT_PORT; p <= PORT_RANGE_END; p++) try {
8600
+ const controller = new AbortController();
8601
+ const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
8602
+ const res = await fetch(`http://localhost:${p}/api/health`, { signal: controller.signal });
8603
+ clearTimeout(timer);
8604
+ if (res.ok) {
8605
+ if ((await res.json()).status === "ok") {
8606
+ foundUrl = `http://localhost:${p}`;
8607
+ break;
8608
+ }
8609
+ }
8610
+ } catch {}
8611
+ if (foundUrl) break;
8612
+ }
8613
+ if (foundUrl) console.log(chalk.green(` Dashboard ready at ${foundUrl}`));
8614
+ else console.log(chalk.yellow("\n Dashboard did not respond after 20s. The server may still be starting — check http://localhost:3333"));
8615
+ process.exit(0);
8616
+ }
8617
+
8618
+ //#endregion
8619
+ //#region src/install/hooks.ts
8620
+ /**
8621
+ * Clean up orphaned files from previous MAXSIM versions
8622
+ */
8623
+ function cleanupOrphanedFiles(configDir) {
8581
8624
  for (const relPath of ["hooks/maxsim-notify.sh", "hooks/statusline.js"]) {
8582
8625
  const fullPath = node_path.join(configDir, relPath);
8583
8626
  if (node_fs.existsSync(fullPath)) {
@@ -8617,12 +8660,386 @@ function cleanupOrphanedHooks(settings) {
8617
8660
  statusLine.command = statusLine.command.replace(/statusline\.js/, "maxsim-statusline.js");
8618
8661
  console.log(` ${chalk.green("✓")} Updated statusline path (statusline.js \u2192 maxsim-statusline.js)`);
8619
8662
  }
8620
- return settings;
8663
+ return settings;
8664
+ }
8665
+ /**
8666
+ * Install hook files and configure settings.json for a runtime
8667
+ */
8668
+ function installHookFiles(targetDir, runtime, isGlobal, failures) {
8669
+ getDirName(runtime);
8670
+ if (runtime === "codex") return;
8671
+ let hooksSrc = null;
8672
+ const bundledHooksDir = node_path.resolve(__dirname, "assets", "hooks");
8673
+ if (node_fs.existsSync(bundledHooksDir)) hooksSrc = bundledHooksDir;
8674
+ else console.warn(` ${chalk.yellow("!")} bundled hooks not found - hooks will not be installed`);
8675
+ if (hooksSrc) {
8676
+ const spinner = ora({
8677
+ text: "Installing hooks...",
8678
+ color: "cyan"
8679
+ }).start();
8680
+ const hooksDest = node_path.join(targetDir, "hooks");
8681
+ node_fs.mkdirSync(hooksDest, { recursive: true });
8682
+ const hookEntries = node_fs.readdirSync(hooksSrc);
8683
+ const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
8684
+ for (const entry of hookEntries) {
8685
+ const srcFile = node_path.join(hooksSrc, entry);
8686
+ if (node_fs.statSync(srcFile).isFile() && entry.endsWith(".cjs") && !entry.includes(".d.")) {
8687
+ const destName = entry.replace(/\.cjs$/, ".js");
8688
+ const destFile = node_path.join(hooksDest, destName);
8689
+ let content = node_fs.readFileSync(srcFile, "utf8");
8690
+ content = content.replace(/'\.claude'/g, configDirReplacement);
8691
+ node_fs.writeFileSync(destFile, content);
8692
+ }
8693
+ }
8694
+ if (verifyInstalled(hooksDest, "hooks")) spinner.succeed(chalk.green("✓") + " Installed hooks (bundled)");
8695
+ else {
8696
+ spinner.fail("Failed to install hooks");
8697
+ failures.push("hooks");
8698
+ }
8699
+ }
8700
+ }
8701
+ /**
8702
+ * Configure hooks and statusline in settings.json
8703
+ */
8704
+ function configureSettingsHooks(targetDir, runtime, isGlobal) {
8705
+ const dirName = getDirName(runtime);
8706
+ const isOpencode = runtime === "opencode";
8707
+ const settingsPath = node_path.join(targetDir, "settings.json");
8708
+ const settings = cleanupOrphanedHooks(readSettings(settingsPath));
8709
+ const statuslineCommand = isGlobal ? buildHookCommand(targetDir, "maxsim-statusline.js") : "node " + dirName + "/hooks/maxsim-statusline.js";
8710
+ const updateCheckCommand = isGlobal ? buildHookCommand(targetDir, "maxsim-check-update.js") : "node " + dirName + "/hooks/maxsim-check-update.js";
8711
+ const contextMonitorCommand = isGlobal ? buildHookCommand(targetDir, "maxsim-context-monitor.js") : "node " + dirName + "/hooks/maxsim-context-monitor.js";
8712
+ if (!isOpencode) {
8713
+ if (!settings.hooks) settings.hooks = {};
8714
+ const installHooks = settings.hooks;
8715
+ if (!installHooks.SessionStart) installHooks.SessionStart = [];
8716
+ if (!installHooks.SessionStart.some((entry) => entry.hooks && entry.hooks.some((h) => h.command && h.command.includes("maxsim-check-update")))) {
8717
+ installHooks.SessionStart.push({ hooks: [{
8718
+ type: "command",
8719
+ command: updateCheckCommand
8720
+ }] });
8721
+ console.log(` ${chalk.green("✓")} Configured update check hook`);
8722
+ }
8723
+ if (!installHooks.PostToolUse) installHooks.PostToolUse = [];
8724
+ if (!installHooks.PostToolUse.some((entry) => entry.hooks && entry.hooks.some((h) => h.command && h.command.includes("maxsim-context-monitor")))) {
8725
+ installHooks.PostToolUse.push({ hooks: [{
8726
+ type: "command",
8727
+ command: contextMonitorCommand
8728
+ }] });
8729
+ console.log(` ${chalk.green("✓")} Configured context window monitor hook`);
8730
+ }
8731
+ }
8732
+ return {
8733
+ settingsPath,
8734
+ settings,
8735
+ statuslineCommand,
8736
+ updateCheckCommand,
8737
+ contextMonitorCommand
8738
+ };
8739
+ }
8740
+ /**
8741
+ * Handle statusline configuration — returns true if MAXSIM statusline should be installed
8742
+ */
8743
+ async function handleStatusline(settings, isInteractive, forceStatusline) {
8744
+ if (!(settings.statusLine != null)) return true;
8745
+ if (forceStatusline) return true;
8746
+ if (!isInteractive) {
8747
+ console.log(chalk.yellow("⚠") + " Skipping statusline (already configured)");
8748
+ console.log(" Use " + chalk.cyan("--force-statusline") + " to replace\n");
8749
+ return false;
8750
+ }
8751
+ const statusLine = settings.statusLine;
8752
+ const existingCmd = statusLine.command || statusLine.url || "(custom)";
8753
+ console.log();
8754
+ console.log(chalk.yellow("⚠ Existing statusline detected"));
8755
+ console.log();
8756
+ console.log(" Your current statusline:");
8757
+ console.log(" " + chalk.dim(`command: ${existingCmd}`));
8758
+ console.log();
8759
+ console.log(" MAXSIM includes a statusline showing:");
8760
+ console.log(" • Model name");
8761
+ console.log(" • Current task (from todo list)");
8762
+ console.log(" • Context window usage (color-coded)");
8763
+ console.log();
8764
+ return await dist_default$1({
8765
+ message: "Replace with MAXSIM statusline?",
8766
+ default: false
8767
+ });
8768
+ }
8769
+ /**
8770
+ * Apply statusline config, then print completion message
8771
+ */
8772
+ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = "claude", isGlobal = true) {
8773
+ const isOpencode = runtime === "opencode";
8774
+ const isCodex = runtime === "codex";
8775
+ if (shouldInstallStatusline && !isOpencode && !isCodex) {
8776
+ settings.statusLine = {
8777
+ type: "command",
8778
+ command: statuslineCommand
8779
+ };
8780
+ console.log(` ${chalk.green("✓")} Configured statusline`);
8781
+ }
8782
+ if (!isCodex && settingsPath && settings) writeSettings(settingsPath, settings);
8783
+ if (isOpencode) configureOpencodePermissions(isGlobal);
8784
+ let program = "Claude Code";
8785
+ if (runtime === "opencode") program = "OpenCode";
8786
+ if (runtime === "gemini") program = "Gemini";
8787
+ if (runtime === "codex") program = "Codex";
8788
+ let command = "/maxsim:help";
8789
+ if (runtime === "opencode") command = "/maxsim-help";
8790
+ if (runtime === "codex") command = "$maxsim-help";
8791
+ console.log(`
8792
+ ${chalk.green("Done!")} Launch ${program} and run ${chalk.cyan(command)}.
8793
+
8794
+ ${chalk.cyan("Join the community:")} https://discord.gg/5JJgD5svVS
8795
+ `);
8796
+ }
8797
+
8798
+ //#endregion
8799
+ //#region src/install/copy.ts
8800
+ /**
8801
+ * Copy commands to a flat structure for OpenCode
8802
+ * OpenCode expects: command/maxsim-help.md (invoked as /maxsim-help)
8803
+ * Source structure: commands/maxsim/help.md
8804
+ */
8805
+ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime, explicitConfigDir) {
8806
+ if (!node_fs.existsSync(srcDir)) return;
8807
+ if (node_fs.existsSync(destDir)) {
8808
+ for (const file of node_fs.readdirSync(destDir)) if (file.startsWith(`${prefix}-`) && file.endsWith(".md")) node_fs.unlinkSync(node_path.join(destDir, file));
8809
+ } else node_fs.mkdirSync(destDir, { recursive: true });
8810
+ const entries = node_fs.readdirSync(srcDir, { withFileTypes: true });
8811
+ for (const entry of entries) {
8812
+ const srcPath = node_path.join(srcDir, entry.name);
8813
+ if (entry.isDirectory()) copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime, explicitConfigDir);
8814
+ else if (entry.name.endsWith(".md")) {
8815
+ const destName = `${prefix}-${entry.name.replace(".md", "")}.md`;
8816
+ const destPath = node_path.join(destDir, destName);
8817
+ let content = node_fs.readFileSync(srcPath, "utf8");
8818
+ const globalClaudeRegex = /~\/\.claude\//g;
8819
+ const localClaudeRegex = /\.\/\.claude\//g;
8820
+ const opencodeDirRegex = /~\/\.opencode\//g;
8821
+ content = content.replace(globalClaudeRegex, pathPrefix);
8822
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
8823
+ content = content.replace(opencodeDirRegex, pathPrefix);
8824
+ content = processAttribution(content, getCommitAttribution(runtime, explicitConfigDir));
8825
+ content = convertClaudeToOpencodeFrontmatter(content);
8826
+ node_fs.writeFileSync(destPath, content);
8827
+ }
8828
+ }
8829
+ }
8830
+ function listCodexSkillNames(skillsDir, prefix = "maxsim-") {
8831
+ if (!node_fs.existsSync(skillsDir)) return [];
8832
+ return node_fs.readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix)).filter((entry) => node_fs.existsSync(node_path.join(skillsDir, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
8833
+ }
8834
+ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtime, explicitConfigDir) {
8835
+ if (!node_fs.existsSync(srcDir)) return;
8836
+ node_fs.mkdirSync(skillsDir, { recursive: true });
8837
+ const existing = node_fs.readdirSync(skillsDir, { withFileTypes: true });
8838
+ for (const entry of existing) if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) node_fs.rmSync(node_path.join(skillsDir, entry.name), { recursive: true });
8839
+ function recurse(currentSrcDir, currentPrefix) {
8840
+ const entries = node_fs.readdirSync(currentSrcDir, { withFileTypes: true });
8841
+ for (const entry of entries) {
8842
+ const srcPath = node_path.join(currentSrcDir, entry.name);
8843
+ if (entry.isDirectory()) {
8844
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
8845
+ continue;
8846
+ }
8847
+ if (!entry.name.endsWith(".md")) continue;
8848
+ const skillName = `${currentPrefix}-${entry.name.replace(".md", "")}`;
8849
+ const skillDir = node_path.join(skillsDir, skillName);
8850
+ node_fs.mkdirSync(skillDir, { recursive: true });
8851
+ let content = node_fs.readFileSync(srcPath, "utf8");
8852
+ const globalClaudeRegex = /~\/\.claude\//g;
8853
+ const localClaudeRegex = /\.\/\.claude\//g;
8854
+ const codexDirRegex = /~\/\.codex\//g;
8855
+ content = content.replace(globalClaudeRegex, pathPrefix);
8856
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
8857
+ content = content.replace(codexDirRegex, pathPrefix);
8858
+ content = processAttribution(content, getCommitAttribution(runtime, explicitConfigDir));
8859
+ content = convertClaudeCommandToCodexSkill(content, skillName);
8860
+ node_fs.writeFileSync(node_path.join(skillDir, "SKILL.md"), content);
8861
+ }
8862
+ }
8863
+ recurse(srcDir, prefix);
8864
+ }
8865
+ /**
8866
+ * Recursively copy directory, replacing paths in .md files
8867
+ * Deletes existing destDir first to remove orphaned files from previous versions
8868
+ */
8869
+ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, explicitConfigDir, isCommand = false) {
8870
+ const isOpencode = runtime === "opencode";
8871
+ const isCodex = runtime === "codex";
8872
+ const dirName = getDirName(runtime);
8873
+ if (node_fs.existsSync(destDir)) node_fs.rmSync(destDir, { recursive: true });
8874
+ node_fs.mkdirSync(destDir, { recursive: true });
8875
+ const entries = node_fs.readdirSync(srcDir, { withFileTypes: true });
8876
+ for (const entry of entries) {
8877
+ const srcPath = node_path.join(srcDir, entry.name);
8878
+ const destPath = node_path.join(destDir, entry.name);
8879
+ if (entry.isDirectory()) copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, explicitConfigDir, isCommand);
8880
+ else if (entry.name.endsWith(".md")) {
8881
+ let content = node_fs.readFileSync(srcPath, "utf8");
8882
+ const globalClaudeRegex = /~\/\.claude\//g;
8883
+ const localClaudeRegex = /\.\/\.claude\//g;
8884
+ content = content.replace(globalClaudeRegex, pathPrefix);
8885
+ content = content.replace(localClaudeRegex, `./${dirName}/`);
8886
+ content = processAttribution(content, getCommitAttribution(runtime, explicitConfigDir));
8887
+ if (isOpencode) {
8888
+ content = convertClaudeToOpencodeFrontmatter(content);
8889
+ node_fs.writeFileSync(destPath, content);
8890
+ } else if (runtime === "gemini") if (isCommand) {
8891
+ content = stripSubTags(content);
8892
+ const tomlContent = convertClaudeToGeminiToml(content);
8893
+ const tomlPath = destPath.replace(/\.md$/, ".toml");
8894
+ node_fs.writeFileSync(tomlPath, tomlContent);
8895
+ } else node_fs.writeFileSync(destPath, content);
8896
+ else if (isCodex) {
8897
+ content = convertClaudeToCodexMarkdown(content);
8898
+ node_fs.writeFileSync(destPath, content);
8899
+ } else node_fs.writeFileSync(destPath, content);
8900
+ } else node_fs.copyFileSync(srcPath, destPath);
8901
+ }
8902
+ }
8903
+
8904
+ //#endregion
8905
+ //#region src/install/manifest.ts
8906
+ const MANIFEST_NAME = "maxsim-file-manifest.json";
8907
+ /**
8908
+ * Compute SHA256 hash of file contents
8909
+ */
8910
+ function fileHash(filePath) {
8911
+ const content = node_fs.readFileSync(filePath);
8912
+ return node_crypto.createHash("sha256").update(content).digest("hex");
8913
+ }
8914
+ /**
8915
+ * Recursively collect all files in dir with their hashes
8916
+ */
8917
+ function generateManifest(dir, baseDir) {
8918
+ if (!baseDir) baseDir = dir;
8919
+ const manifest = {};
8920
+ if (!node_fs.existsSync(dir)) return manifest;
8921
+ const entries = node_fs.readdirSync(dir, { withFileTypes: true });
8922
+ for (const entry of entries) {
8923
+ const fullPath = node_path.join(dir, entry.name);
8924
+ const relPath = node_path.relative(baseDir, fullPath).replace(/\\/g, "/");
8925
+ if (entry.isDirectory()) Object.assign(manifest, generateManifest(fullPath, baseDir));
8926
+ else manifest[relPath] = fileHash(fullPath);
8927
+ }
8928
+ return manifest;
8929
+ }
8930
+ /**
8931
+ * Write file manifest after installation for future modification detection
8932
+ */
8933
+ function writeManifest(configDir, runtime = "claude") {
8934
+ const isOpencode = runtime === "opencode";
8935
+ const isCodex = runtime === "codex";
8936
+ const maxsimDir = node_path.join(configDir, "maxsim");
8937
+ const commandsDir = node_path.join(configDir, "commands", "maxsim");
8938
+ const opencodeCommandDir = node_path.join(configDir, "command");
8939
+ const codexSkillsDir = node_path.join(configDir, "skills");
8940
+ const agentsDir = node_path.join(configDir, "agents");
8941
+ const manifest = {
8942
+ version: pkg.version,
8943
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8944
+ files: {}
8945
+ };
8946
+ const maxsimHashes = generateManifest(maxsimDir);
8947
+ for (const [rel, hash] of Object.entries(maxsimHashes)) manifest.files["maxsim/" + rel] = hash;
8948
+ if (!isOpencode && !isCodex && node_fs.existsSync(commandsDir)) {
8949
+ const cmdHashes = generateManifest(commandsDir);
8950
+ for (const [rel, hash] of Object.entries(cmdHashes)) manifest.files["commands/maxsim/" + rel] = hash;
8951
+ }
8952
+ if (isOpencode && node_fs.existsSync(opencodeCommandDir)) {
8953
+ for (const file of node_fs.readdirSync(opencodeCommandDir)) if (file.startsWith("maxsim-") && file.endsWith(".md")) manifest.files["command/" + file] = fileHash(node_path.join(opencodeCommandDir, file));
8954
+ }
8955
+ if (isCodex && node_fs.existsSync(codexSkillsDir)) for (const skillName of listCodexSkillNames(codexSkillsDir)) {
8956
+ const skillHashes = generateManifest(node_path.join(codexSkillsDir, skillName));
8957
+ for (const [rel, hash] of Object.entries(skillHashes)) manifest.files[`skills/${skillName}/${rel}`] = hash;
8958
+ }
8959
+ if (node_fs.existsSync(agentsDir)) {
8960
+ for (const file of node_fs.readdirSync(agentsDir)) if (file.startsWith("maxsim-") && file.endsWith(".md")) manifest.files["agents/" + file] = fileHash(node_path.join(agentsDir, file));
8961
+ }
8962
+ const skillsManifestDir = node_path.join(agentsDir, "skills");
8963
+ if (node_fs.existsSync(skillsManifestDir)) {
8964
+ const skillHashes = generateManifest(skillsManifestDir);
8965
+ for (const [rel, hash] of Object.entries(skillHashes)) manifest.files["agents/skills/" + rel] = hash;
8966
+ }
8967
+ node_fs.writeFileSync(node_path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
8968
+ return manifest;
8969
+ }
8970
+
8971
+ //#endregion
8972
+ //#region src/install/patches.ts
8973
+ const PATCHES_DIR_NAME = "maxsim-local-patches";
8974
+ /**
8975
+ * Detect user-modified MAXSIM files by comparing against install manifest.
8976
+ */
8977
+ function saveLocalPatches(configDir) {
8978
+ const manifestPath = node_path.join(configDir, MANIFEST_NAME);
8979
+ if (!node_fs.existsSync(manifestPath)) return [];
8980
+ let manifest;
8981
+ try {
8982
+ manifest = JSON.parse(node_fs.readFileSync(manifestPath, "utf8"));
8983
+ } catch {
8984
+ return [];
8985
+ }
8986
+ const patchesDir = node_path.join(configDir, PATCHES_DIR_NAME);
8987
+ const modified = [];
8988
+ for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
8989
+ const fullPath = node_path.join(configDir, relPath);
8990
+ if (!node_fs.existsSync(fullPath)) continue;
8991
+ if (fileHash(fullPath) !== originalHash) {
8992
+ const backupPath = node_path.join(patchesDir, relPath);
8993
+ node_fs.mkdirSync(node_path.dirname(backupPath), { recursive: true });
8994
+ node_fs.copyFileSync(fullPath, backupPath);
8995
+ modified.push(relPath);
8996
+ }
8997
+ }
8998
+ if (modified.length > 0) {
8999
+ const meta = {
9000
+ backed_up_at: (/* @__PURE__ */ new Date()).toISOString(),
9001
+ from_version: manifest.version,
9002
+ files: modified
9003
+ };
9004
+ node_fs.writeFileSync(node_path.join(patchesDir, "backup-meta.json"), JSON.stringify(meta, null, 2));
9005
+ console.log(" " + chalk.yellow("i") + " Found " + modified.length + " locally modified MAXSIM file(s) — backed up to maxsim-local-patches/");
9006
+ for (const f of modified) console.log(" " + chalk.dim(f));
9007
+ }
9008
+ return modified;
9009
+ }
9010
+ /**
9011
+ * After install, report backed-up patches for user to reapply.
9012
+ */
9013
+ function reportLocalPatches(configDir, runtime = "claude") {
9014
+ const patchesDir = node_path.join(configDir, PATCHES_DIR_NAME);
9015
+ const metaPath = node_path.join(patchesDir, "backup-meta.json");
9016
+ if (!node_fs.existsSync(metaPath)) return [];
9017
+ let meta;
9018
+ try {
9019
+ meta = JSON.parse(node_fs.readFileSync(metaPath, "utf8"));
9020
+ } catch {
9021
+ return [];
9022
+ }
9023
+ if (meta.files && meta.files.length > 0) {
9024
+ const reapplyCommand = runtime === "opencode" ? "/maxsim-reapply-patches" : runtime === "codex" ? "$maxsim-reapply-patches" : "/maxsim:reapply-patches";
9025
+ console.log("");
9026
+ console.log(" " + chalk.yellow("Local patches detected") + " (from v" + meta.from_version + "):");
9027
+ for (const f of meta.files) console.log(" " + chalk.cyan(f));
9028
+ console.log("");
9029
+ console.log(" Your modifications are saved in " + chalk.cyan(PATCHES_DIR_NAME + "/"));
9030
+ console.log(" Run " + chalk.cyan(reapplyCommand) + " to merge them into the new version.");
9031
+ console.log(" Or manually compare and merge the files.");
9032
+ console.log("");
9033
+ }
9034
+ return meta.files || [];
8621
9035
  }
9036
+
9037
+ //#endregion
9038
+ //#region src/install/uninstall.ts
8622
9039
  /**
8623
9040
  * Uninstall MAXSIM from the specified directory for a specific runtime
8624
9041
  */
8625
- function uninstall(isGlobal, runtime = "claude") {
9042
+ function uninstall(isGlobal, runtime = "claude", explicitConfigDir = null) {
8626
9043
  const isOpencode = runtime === "opencode";
8627
9044
  const isCodex = runtime === "codex";
8628
9045
  const dirName = getDirName(runtime);
@@ -8731,292 +9148,130 @@ function uninstall(isGlobal, runtime = "claude") {
8731
9148
  }
8732
9149
  const settingsHooks = settings.hooks;
8733
9150
  if (settingsHooks && settingsHooks.SessionStart) {
8734
- const before = settingsHooks.SessionStart.length;
8735
- settingsHooks.SessionStart = settingsHooks.SessionStart.filter((entry) => {
8736
- if (entry.hooks && Array.isArray(entry.hooks)) return !entry.hooks.some((h) => h.command && (h.command.includes("maxsim-check-update") || h.command.includes("maxsim-statusline")));
8737
- return true;
8738
- });
8739
- if (settingsHooks.SessionStart.length < before) {
8740
- settingsModified = true;
8741
- console.log(` ${chalk.green("✓")} Removed MAXSIM hooks from settings`);
8742
- }
8743
- if (settingsHooks.SessionStart.length === 0) delete settingsHooks.SessionStart;
8744
- }
8745
- if (settingsHooks && settingsHooks.PostToolUse) {
8746
- const before = settingsHooks.PostToolUse.length;
8747
- settingsHooks.PostToolUse = settingsHooks.PostToolUse.filter((entry) => {
8748
- if (entry.hooks && Array.isArray(entry.hooks)) return !entry.hooks.some((h) => h.command && h.command.includes("maxsim-context-monitor"));
8749
- return true;
8750
- });
8751
- if (settingsHooks.PostToolUse.length < before) {
8752
- settingsModified = true;
8753
- console.log(` ${chalk.green("✓")} Removed context monitor hook from settings`);
8754
- }
8755
- if (settingsHooks.PostToolUse.length === 0) delete settingsHooks.PostToolUse;
8756
- }
8757
- if (settingsHooks && Object.keys(settingsHooks).length === 0) delete settings.hooks;
8758
- if (settingsModified) {
8759
- writeSettings(settingsPath, settings);
8760
- removedCount++;
8761
- }
8762
- }
8763
- if (isOpencode) {
8764
- const opencodeConfigDir = isGlobal ? getOpencodeGlobalDir() : node_path.join(process.cwd(), ".opencode");
8765
- const configPath = node_path.join(opencodeConfigDir, "opencode.json");
8766
- if (node_fs.existsSync(configPath)) try {
8767
- const config = JSON.parse(node_fs.readFileSync(configPath, "utf8"));
8768
- let modified = false;
8769
- const permission = config.permission;
8770
- if (permission) {
8771
- for (const permType of ["read", "external_directory"]) if (permission[permType]) {
8772
- const keys = Object.keys(permission[permType]);
8773
- for (const key of keys) if (key.includes("maxsim")) {
8774
- delete permission[permType][key];
8775
- modified = true;
8776
- }
8777
- if (Object.keys(permission[permType]).length === 0) delete permission[permType];
8778
- }
8779
- if (Object.keys(permission).length === 0) delete config.permission;
8780
- }
8781
- if (modified) {
8782
- node_fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
8783
- removedCount++;
8784
- console.log(` ${chalk.green("✓")} Removed MAXSIM permissions from opencode.json`);
8785
- }
8786
- } catch {}
8787
- }
8788
- if (removedCount === 0) console.log(` ${chalk.yellow("⚠")} No MAXSIM files found to remove.`);
8789
- console.log(`
8790
- ${chalk.green("Done!")} MAXSIM has been uninstalled from ${runtimeLabel}.
8791
- Your other files and settings have been preserved.
8792
- `);
8793
- }
8794
- /**
8795
- * Parse JSONC (JSON with Comments) by stripping comments and trailing commas.
8796
- */
8797
- function parseJsonc(content) {
8798
- if (content.charCodeAt(0) === 65279) content = content.slice(1);
8799
- let result = "";
8800
- let inString = false;
8801
- let i = 0;
8802
- while (i < content.length) {
8803
- const char = content[i];
8804
- const next = content[i + 1];
8805
- if (inString) {
8806
- result += char;
8807
- if (char === "\\" && i + 1 < content.length) {
8808
- result += next;
8809
- i += 2;
8810
- continue;
8811
- }
8812
- if (char === "\"") inString = false;
8813
- i++;
8814
- } else if (char === "\"") {
8815
- inString = true;
8816
- result += char;
8817
- i++;
8818
- } else if (char === "/" && next === "/") while (i < content.length && content[i] !== "\n") i++;
8819
- else if (char === "/" && next === "*") {
8820
- i += 2;
8821
- while (i < content.length - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
8822
- i += 2;
8823
- } else {
8824
- result += char;
8825
- i++;
8826
- }
8827
- }
8828
- result = result.replace(/,(\s*[}\]])/g, "$1");
8829
- return JSON.parse(result);
8830
- }
8831
- /**
8832
- * Configure OpenCode permissions to allow reading MAXSIM reference docs
8833
- */
8834
- function configureOpencodePermissions(isGlobal = true) {
8835
- const opencodeConfigDir = isGlobal ? getOpencodeGlobalDir() : node_path.join(process.cwd(), ".opencode");
8836
- const configPath = node_path.join(opencodeConfigDir, "opencode.json");
8837
- node_fs.mkdirSync(opencodeConfigDir, { recursive: true });
8838
- let config = {};
8839
- if (node_fs.existsSync(configPath)) try {
8840
- config = parseJsonc(node_fs.readFileSync(configPath, "utf8"));
8841
- } catch (e) {
8842
- console.log(` ${chalk.yellow("⚠")} Could not parse opencode.json - skipping permission config`);
8843
- console.log(` ${chalk.dim(`Reason: ${e.message}`)}`);
8844
- console.log(` ${chalk.dim("Your config was NOT modified. Fix the syntax manually if needed.")}`);
8845
- return;
8846
- }
8847
- if (!config.permission) config.permission = {};
8848
- const permission = config.permission;
8849
- const maxsimPath = opencodeConfigDir === node_path.join(node_os.homedir(), ".config", "opencode") ? "~/.config/opencode/maxsim/*" : `${opencodeConfigDir.replace(/\\/g, "/")}/maxsim/*`;
8850
- let modified = false;
8851
- if (!permission.read || typeof permission.read !== "object") permission.read = {};
8852
- if (permission.read[maxsimPath] !== "allow") {
8853
- permission.read[maxsimPath] = "allow";
8854
- modified = true;
8855
- }
8856
- if (!permission.external_directory || typeof permission.external_directory !== "object") permission.external_directory = {};
8857
- if (permission.external_directory[maxsimPath] !== "allow") {
8858
- permission.external_directory[maxsimPath] = "allow";
8859
- modified = true;
8860
- }
8861
- if (!modified) return;
8862
- node_fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
8863
- console.log(` ${chalk.green("✓")} Configured read permission for MAXSIM docs`);
8864
- }
8865
- /**
8866
- * Verify a directory exists and contains files
8867
- */
8868
- function verifyInstalled(dirPath, description) {
8869
- if (!node_fs.existsSync(dirPath)) {
8870
- console.error(` ${chalk.yellow("✗")} Failed to install ${description}: directory not created`);
8871
- return false;
8872
- }
8873
- try {
8874
- if (node_fs.readdirSync(dirPath).length === 0) {
8875
- console.error(` ${chalk.yellow("✗")} Failed to install ${description}: directory is empty`);
8876
- return false;
8877
- }
8878
- } catch (e) {
8879
- console.error(` ${chalk.yellow("✗")} Failed to install ${description}: ${e.message}`);
8880
- return false;
8881
- }
8882
- return true;
8883
- }
8884
- /**
8885
- * Verify a file exists
8886
- */
8887
- function verifyFileInstalled(filePath, description) {
8888
- if (!node_fs.existsSync(filePath)) {
8889
- console.error(` ${chalk.yellow("✗")} Failed to install ${description}: file not created`);
8890
- return false;
8891
- }
8892
- return true;
8893
- }
8894
- const PATCHES_DIR_NAME = "maxsim-local-patches";
8895
- const MANIFEST_NAME = "maxsim-file-manifest.json";
8896
- /**
8897
- * Compute SHA256 hash of file contents
8898
- */
8899
- function fileHash(filePath) {
8900
- const content = node_fs.readFileSync(filePath);
8901
- return node_crypto.createHash("sha256").update(content).digest("hex");
8902
- }
8903
- /**
8904
- * Recursively collect all files in dir with their hashes
8905
- */
8906
- function generateManifest(dir, baseDir) {
8907
- if (!baseDir) baseDir = dir;
8908
- const manifest = {};
8909
- if (!node_fs.existsSync(dir)) return manifest;
8910
- const entries = node_fs.readdirSync(dir, { withFileTypes: true });
8911
- for (const entry of entries) {
8912
- const fullPath = node_path.join(dir, entry.name);
8913
- const relPath = node_path.relative(baseDir, fullPath).replace(/\\/g, "/");
8914
- if (entry.isDirectory()) Object.assign(manifest, generateManifest(fullPath, baseDir));
8915
- else manifest[relPath] = fileHash(fullPath);
8916
- }
8917
- return manifest;
8918
- }
8919
- /**
8920
- * Write file manifest after installation for future modification detection
8921
- */
8922
- function writeManifest(configDir, runtime = "claude") {
8923
- const isOpencode = runtime === "opencode";
8924
- const isCodex = runtime === "codex";
8925
- const maxsimDir = node_path.join(configDir, "maxsim");
8926
- const commandsDir = node_path.join(configDir, "commands", "maxsim");
8927
- const opencodeCommandDir = node_path.join(configDir, "command");
8928
- const codexSkillsDir = node_path.join(configDir, "skills");
8929
- const agentsDir = node_path.join(configDir, "agents");
8930
- const manifest = {
8931
- version: pkg.version,
8932
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8933
- files: {}
8934
- };
8935
- const maxsimHashes = generateManifest(maxsimDir);
8936
- for (const [rel, hash] of Object.entries(maxsimHashes)) manifest.files["maxsim/" + rel] = hash;
8937
- if (!isOpencode && !isCodex && node_fs.existsSync(commandsDir)) {
8938
- const cmdHashes = generateManifest(commandsDir);
8939
- for (const [rel, hash] of Object.entries(cmdHashes)) manifest.files["commands/maxsim/" + rel] = hash;
8940
- }
8941
- if (isOpencode && node_fs.existsSync(opencodeCommandDir)) {
8942
- for (const file of node_fs.readdirSync(opencodeCommandDir)) if (file.startsWith("maxsim-") && file.endsWith(".md")) manifest.files["command/" + file] = fileHash(node_path.join(opencodeCommandDir, file));
8943
- }
8944
- if (isCodex && node_fs.existsSync(codexSkillsDir)) for (const skillName of listCodexSkillNames(codexSkillsDir)) {
8945
- const skillHashes = generateManifest(node_path.join(codexSkillsDir, skillName));
8946
- for (const [rel, hash] of Object.entries(skillHashes)) manifest.files[`skills/${skillName}/${rel}`] = hash;
8947
- }
8948
- if (node_fs.existsSync(agentsDir)) {
8949
- for (const file of node_fs.readdirSync(agentsDir)) if (file.startsWith("maxsim-") && file.endsWith(".md")) manifest.files["agents/" + file] = fileHash(node_path.join(agentsDir, file));
8950
- }
8951
- const skillsManifestDir = node_path.join(agentsDir, "skills");
8952
- if (node_fs.existsSync(skillsManifestDir)) {
8953
- const skillHashes = generateManifest(skillsManifestDir);
8954
- for (const [rel, hash] of Object.entries(skillHashes)) manifest.files["agents/skills/" + rel] = hash;
8955
- }
8956
- node_fs.writeFileSync(node_path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
8957
- return manifest;
8958
- }
8959
- /**
8960
- * Detect user-modified MAXSIM files by comparing against install manifest.
8961
- */
8962
- function saveLocalPatches(configDir) {
8963
- const manifestPath = node_path.join(configDir, MANIFEST_NAME);
8964
- if (!node_fs.existsSync(manifestPath)) return [];
8965
- let manifest;
8966
- try {
8967
- manifest = JSON.parse(node_fs.readFileSync(manifestPath, "utf8"));
8968
- } catch {
8969
- return [];
8970
- }
8971
- const patchesDir = node_path.join(configDir, PATCHES_DIR_NAME);
8972
- const modified = [];
8973
- for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
8974
- const fullPath = node_path.join(configDir, relPath);
8975
- if (!node_fs.existsSync(fullPath)) continue;
8976
- if (fileHash(fullPath) !== originalHash) {
8977
- const backupPath = node_path.join(patchesDir, relPath);
8978
- node_fs.mkdirSync(node_path.dirname(backupPath), { recursive: true });
8979
- node_fs.copyFileSync(fullPath, backupPath);
8980
- modified.push(relPath);
9151
+ const before = settingsHooks.SessionStart.length;
9152
+ settingsHooks.SessionStart = settingsHooks.SessionStart.filter((entry) => {
9153
+ if (entry.hooks && Array.isArray(entry.hooks)) return !entry.hooks.some((h) => h.command && (h.command.includes("maxsim-check-update") || h.command.includes("maxsim-statusline")));
9154
+ return true;
9155
+ });
9156
+ if (settingsHooks.SessionStart.length < before) {
9157
+ settingsModified = true;
9158
+ console.log(` ${chalk.green("✓")} Removed MAXSIM hooks from settings`);
9159
+ }
9160
+ if (settingsHooks.SessionStart.length === 0) delete settingsHooks.SessionStart;
9161
+ }
9162
+ if (settingsHooks && settingsHooks.PostToolUse) {
9163
+ const before = settingsHooks.PostToolUse.length;
9164
+ settingsHooks.PostToolUse = settingsHooks.PostToolUse.filter((entry) => {
9165
+ if (entry.hooks && Array.isArray(entry.hooks)) return !entry.hooks.some((h) => h.command && h.command.includes("maxsim-context-monitor"));
9166
+ return true;
9167
+ });
9168
+ if (settingsHooks.PostToolUse.length < before) {
9169
+ settingsModified = true;
9170
+ console.log(` ${chalk.green("✓")} Removed context monitor hook from settings`);
9171
+ }
9172
+ if (settingsHooks.PostToolUse.length === 0) delete settingsHooks.PostToolUse;
9173
+ }
9174
+ if (settingsHooks && Object.keys(settingsHooks).length === 0) delete settings.hooks;
9175
+ if (settingsModified) {
9176
+ writeSettings(settingsPath, settings);
9177
+ removedCount++;
8981
9178
  }
8982
9179
  }
8983
- if (modified.length > 0) {
8984
- const meta = {
8985
- backed_up_at: (/* @__PURE__ */ new Date()).toISOString(),
8986
- from_version: manifest.version,
8987
- files: modified
8988
- };
8989
- node_fs.writeFileSync(node_path.join(patchesDir, "backup-meta.json"), JSON.stringify(meta, null, 2));
8990
- console.log(" " + chalk.yellow("i") + " Found " + modified.length + " locally modified MAXSIM file(s) — backed up to maxsim-local-patches/");
8991
- for (const f of modified) console.log(" " + chalk.dim(f));
9180
+ if (isOpencode) {
9181
+ const opencodeConfigDir = isGlobal ? getOpencodeGlobalDir() : node_path.join(process.cwd(), ".opencode");
9182
+ const configPath = node_path.join(opencodeConfigDir, "opencode.json");
9183
+ if (node_fs.existsSync(configPath)) try {
9184
+ const config = JSON.parse(node_fs.readFileSync(configPath, "utf8"));
9185
+ let modified = false;
9186
+ const permission = config.permission;
9187
+ if (permission) {
9188
+ for (const permType of ["read", "external_directory"]) if (permission[permType]) {
9189
+ const keys = Object.keys(permission[permType]);
9190
+ for (const key of keys) if (key.includes("maxsim")) {
9191
+ delete permission[permType][key];
9192
+ modified = true;
9193
+ }
9194
+ if (Object.keys(permission[permType]).length === 0) delete permission[permType];
9195
+ }
9196
+ if (Object.keys(permission).length === 0) delete config.permission;
9197
+ }
9198
+ if (modified) {
9199
+ node_fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
9200
+ removedCount++;
9201
+ console.log(` ${chalk.green("✓")} Removed MAXSIM permissions from opencode.json`);
9202
+ }
9203
+ } catch {}
8992
9204
  }
8993
- return modified;
9205
+ if (removedCount === 0) console.log(` ${chalk.yellow("⚠")} No MAXSIM files found to remove.`);
9206
+ console.log(`
9207
+ ${chalk.green("Done!")} MAXSIM has been uninstalled from ${runtimeLabel}.
9208
+ Your other files and settings have been preserved.
9209
+ `);
8994
9210
  }
8995
- /**
8996
- * After install, report backed-up patches for user to reapply.
8997
- */
8998
- function reportLocalPatches(configDir, runtime = "claude") {
8999
- const patchesDir = node_path.join(configDir, PATCHES_DIR_NAME);
9000
- const metaPath = node_path.join(patchesDir, "backup-meta.json");
9001
- if (!node_fs.existsSync(metaPath)) return [];
9002
- let meta;
9003
- try {
9004
- meta = JSON.parse(node_fs.readFileSync(metaPath, "utf8"));
9005
- } catch {
9006
- return [];
9007
- }
9008
- if (meta.files && meta.files.length > 0) {
9009
- const reapplyCommand = runtime === "opencode" ? "/maxsim-reapply-patches" : runtime === "codex" ? "$maxsim-reapply-patches" : "/maxsim:reapply-patches";
9010
- console.log("");
9011
- console.log(" " + chalk.yellow("Local patches detected") + " (from v" + meta.from_version + "):");
9012
- for (const f of meta.files) console.log(" " + chalk.cyan(f));
9013
- console.log("");
9014
- console.log(" Your modifications are saved in " + chalk.cyan(PATCHES_DIR_NAME + "/"));
9015
- console.log(" Run " + chalk.cyan(reapplyCommand) + " to merge them into the new version.");
9016
- console.log(" Or manually compare and merge the files.");
9017
- console.log("");
9211
+
9212
+ //#endregion
9213
+ //#region src/install/index.ts
9214
+ const argv = (0, import_minimist.default)(process.argv.slice(2), {
9215
+ boolean: [
9216
+ "global",
9217
+ "local",
9218
+ "opencode",
9219
+ "claude",
9220
+ "gemini",
9221
+ "codex",
9222
+ "both",
9223
+ "all",
9224
+ "uninstall",
9225
+ "help",
9226
+ "version",
9227
+ "force-statusline",
9228
+ "network"
9229
+ ],
9230
+ string: ["config-dir"],
9231
+ alias: {
9232
+ g: "global",
9233
+ l: "local",
9234
+ u: "uninstall",
9235
+ h: "help",
9236
+ c: "config-dir"
9018
9237
  }
9019
- return meta.files || [];
9238
+ });
9239
+ const hasGlobal = !!argv["global"];
9240
+ const hasLocal = !!argv["local"];
9241
+ const hasOpencode = !!argv["opencode"];
9242
+ const hasClaude = !!argv["claude"];
9243
+ const hasGemini = !!argv["gemini"];
9244
+ const hasCodex = !!argv["codex"];
9245
+ const hasBoth = !!argv["both"];
9246
+ const hasAll = !!argv["all"];
9247
+ const hasUninstall = !!argv["uninstall"];
9248
+ let selectedRuntimes = [];
9249
+ if (hasAll) selectedRuntimes = [
9250
+ "claude",
9251
+ "opencode",
9252
+ "gemini",
9253
+ "codex"
9254
+ ];
9255
+ else if (hasBoth) selectedRuntimes = ["claude", "opencode"];
9256
+ else {
9257
+ if (hasOpencode) selectedRuntimes.push("opencode");
9258
+ if (hasClaude) selectedRuntimes.push("claude");
9259
+ if (hasGemini) selectedRuntimes.push("gemini");
9260
+ if (hasCodex) selectedRuntimes.push("codex");
9261
+ }
9262
+ const banner = "\n" + chalk.cyan(figlet.default.textSync("MAXSIM", { font: "ANSI Shadow" }).split("\n").map((line) => " " + line).join("\n")) + "\n\n MAXSIM " + chalk.dim("v" + pkg.version) + "\n A meta-prompting, context engineering and spec-driven\n development system for Claude Code, OpenCode, Gemini, and Codex.\n";
9263
+ const explicitConfigDir = argv["config-dir"] || null;
9264
+ const hasHelp = !!argv["help"];
9265
+ const hasVersion = !!argv["version"];
9266
+ const forceStatusline = !!argv["force-statusline"];
9267
+ if (hasVersion) {
9268
+ console.log(pkg.version);
9269
+ process.exit(0);
9270
+ }
9271
+ console.log(banner);
9272
+ if (hasHelp) {
9273
+ console.log(` ${chalk.yellow("Usage:")} npx maxsimcli [options]\n\n ${chalk.yellow("Options:")}\n ${chalk.cyan("-g, --global")} Install globally (to config directory)\n ${chalk.cyan("-l, --local")} Install locally (to current directory)\n ${chalk.cyan("--claude")} Install for Claude Code only\n ${chalk.cyan("--opencode")} Install for OpenCode only\n ${chalk.cyan("--gemini")} Install for Gemini only\n ${chalk.cyan("--codex")} Install for Codex only\n ${chalk.cyan("--all")} Install for all runtimes\n ${chalk.cyan("-u, --uninstall")} Uninstall MAXSIM (remove all MAXSIM files)\n ${chalk.cyan("-c, --config-dir <path>")} Specify custom config directory\n ${chalk.cyan("-h, --help")} Show this help message\n ${chalk.cyan("--force-statusline")} Replace existing statusline config\n\n ${chalk.yellow("Examples:")}\n ${chalk.dim("# Interactive install (prompts for runtime and location)")}\n npx maxsimcli\n\n ${chalk.dim("# Install for Claude Code globally")}\n npx maxsimcli --claude --global\n\n ${chalk.dim("# Install for Gemini globally")}\n npx maxsimcli --gemini --global\n\n ${chalk.dim("# Install for Codex globally")}\n npx maxsimcli --codex --global\n\n ${chalk.dim("# Install for all runtimes globally")}\n npx maxsimcli --all --global\n\n ${chalk.dim("# Install to custom config directory")}\n npx maxsimcli --codex --global --config-dir ~/.codex-work\n\n ${chalk.dim("# Install to current project only")}\n npx maxsimcli --claude --local\n\n ${chalk.dim("# Uninstall MAXSIM from Codex globally")}\n npx maxsimcli --codex --global --uninstall\n\n ${chalk.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`);
9274
+ process.exit(0);
9020
9275
  }
9021
9276
  async function install(isGlobal, runtime = "claude") {
9022
9277
  const isOpencode = runtime === "opencode";
@@ -9042,7 +9297,7 @@ async function install(isGlobal, runtime = "claude") {
9042
9297
  if (isOpencode) {
9043
9298
  const commandDir = node_path.join(targetDir, "command");
9044
9299
  node_fs.mkdirSync(commandDir, { recursive: true });
9045
- copyFlattenedCommands(node_path.join(src, "commands", "maxsim"), commandDir, "maxsim", pathPrefix, runtime);
9300
+ copyFlattenedCommands(node_path.join(src, "commands", "maxsim"), commandDir, "maxsim", pathPrefix, runtime, explicitConfigDir);
9046
9301
  if (verifyInstalled(commandDir, "command/maxsim-*")) {
9047
9302
  const count = node_fs.readdirSync(commandDir).filter((f) => f.startsWith("maxsim-")).length;
9048
9303
  spinner.succeed(chalk.green("✓") + ` Installed ${count} commands to command/`);
@@ -9052,7 +9307,7 @@ async function install(isGlobal, runtime = "claude") {
9052
9307
  }
9053
9308
  } else if (isCodex) {
9054
9309
  const skillsDir = node_path.join(targetDir, "skills");
9055
- copyCommandsAsCodexSkills(node_path.join(src, "commands", "maxsim"), skillsDir, "maxsim", pathPrefix, runtime);
9310
+ copyCommandsAsCodexSkills(node_path.join(src, "commands", "maxsim"), skillsDir, "maxsim", pathPrefix, runtime, explicitConfigDir);
9056
9311
  const installedSkillNames = listCodexSkillNames(skillsDir);
9057
9312
  if (installedSkillNames.length > 0) spinner.succeed(chalk.green("✓") + ` Installed ${installedSkillNames.length} skills to skills/`);
9058
9313
  else {
@@ -9064,7 +9319,7 @@ async function install(isGlobal, runtime = "claude") {
9064
9319
  node_fs.mkdirSync(commandsDir, { recursive: true });
9065
9320
  const maxsimSrc = node_path.join(src, "commands", "maxsim");
9066
9321
  const maxsimDest = node_path.join(commandsDir, "maxsim");
9067
- copyWithPathReplacement(maxsimSrc, maxsimDest, pathPrefix, runtime, true);
9322
+ copyWithPathReplacement(maxsimSrc, maxsimDest, pathPrefix, runtime, explicitConfigDir, true);
9068
9323
  if (verifyInstalled(maxsimDest, "commands/maxsim")) spinner.succeed(chalk.green("✓") + " Installed commands/maxsim");
9069
9324
  else {
9070
9325
  spinner.fail("Failed to install commands/maxsim");
@@ -9085,7 +9340,7 @@ async function install(isGlobal, runtime = "claude") {
9085
9340
  node_fs.mkdirSync(skillDest, { recursive: true });
9086
9341
  for (const subdir of maxsimSubdirs) {
9087
9342
  const subdirSrc = node_path.join(src, subdir);
9088
- if (node_fs.existsSync(subdirSrc)) copyWithPathReplacement(subdirSrc, node_path.join(skillDest, subdir), pathPrefix, runtime);
9343
+ if (node_fs.existsSync(subdirSrc)) copyWithPathReplacement(subdirSrc, node_path.join(skillDest, subdir), pathPrefix, runtime, explicitConfigDir);
9089
9344
  }
9090
9345
  if (verifyInstalled(skillDest, "maxsim")) spinner.succeed(chalk.green("✓") + " Installed maxsim");
9091
9346
  else {
@@ -9107,7 +9362,7 @@ async function install(isGlobal, runtime = "claude") {
9107
9362
  for (const entry of agentEntries) if (entry.isFile() && entry.name.endsWith(".md")) {
9108
9363
  let content = node_fs.readFileSync(node_path.join(agentsSrc, entry.name), "utf8");
9109
9364
  content = content.replace(/~\/\.claude\//g, pathPrefix);
9110
- content = processAttribution(content, getCommitAttribution(runtime));
9365
+ content = processAttribution(content, getCommitAttribution(runtime, explicitConfigDir));
9111
9366
  if (isOpencode) content = convertClaudeToOpencodeFrontmatter(content);
9112
9367
  else if (isGemini) content = convertClaudeToGeminiAgent(content);
9113
9368
  else if (isCodex) content = convertClaudeToCodexMarkdown(content);
@@ -9141,7 +9396,7 @@ async function install(isGlobal, runtime = "claude") {
9141
9396
  if (node_fs.existsSync(skillMd)) {
9142
9397
  let content = node_fs.readFileSync(skillMd, "utf8");
9143
9398
  content = content.replace(/~\/\.claude\//g, pathPrefix);
9144
- content = processAttribution(content, getCommitAttribution(runtime));
9399
+ content = processAttribution(content, getCommitAttribution(runtime, explicitConfigDir));
9145
9400
  node_fs.writeFileSync(skillMd, content);
9146
9401
  }
9147
9402
  }
@@ -9199,35 +9454,14 @@ async function install(isGlobal, runtime = "claude") {
9199
9454
  console.warn(` ${chalk.yellow("!")} cli.cjs not found at ${toolSrc} — maxsim-tools.cjs not installed`);
9200
9455
  failures.push("maxsim-tools.cjs");
9201
9456
  }
9202
- let hooksSrc = null;
9203
- const bundledHooksDir = node_path.resolve(__dirname, "assets", "hooks");
9204
- if (node_fs.existsSync(bundledHooksDir)) hooksSrc = bundledHooksDir;
9205
- else console.warn(` ${chalk.yellow("!")} bundled hooks not found - hooks will not be installed`);
9206
- if (hooksSrc) {
9207
- spinner = ora({
9208
- text: "Installing hooks...",
9209
- color: "cyan"
9210
- }).start();
9211
- const hooksDest = node_path.join(targetDir, "hooks");
9212
- node_fs.mkdirSync(hooksDest, { recursive: true });
9213
- const hookEntries = node_fs.readdirSync(hooksSrc);
9214
- const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
9215
- for (const entry of hookEntries) {
9216
- const srcFile = node_path.join(hooksSrc, entry);
9217
- if (node_fs.statSync(srcFile).isFile() && entry.endsWith(".cjs") && !entry.includes(".d.")) {
9218
- const destName = entry.replace(/\.cjs$/, ".js");
9219
- const destFile = node_path.join(hooksDest, destName);
9220
- let content = node_fs.readFileSync(srcFile, "utf8");
9221
- content = content.replace(/'\.claude'/g, configDirReplacement);
9222
- node_fs.writeFileSync(destFile, content);
9223
- }
9224
- }
9225
- if (verifyInstalled(hooksDest, "hooks")) spinner.succeed(chalk.green("✓") + " Installed hooks (bundled)");
9226
- else {
9227
- spinner.fail("Failed to install hooks");
9228
- failures.push("hooks");
9229
- }
9230
- }
9457
+ const mcpSrc = node_path.resolve(__dirname, "mcp-server.cjs");
9458
+ const mcpDest = node_path.join(binDir, "mcp-server.cjs");
9459
+ if (node_fs.existsSync(mcpSrc)) {
9460
+ node_fs.mkdirSync(binDir, { recursive: true });
9461
+ node_fs.copyFileSync(mcpSrc, mcpDest);
9462
+ console.log(` ${chalk.green("✓")} Installed mcp-server.cjs`);
9463
+ } else console.warn(` ${chalk.yellow("!")} mcp-server.cjs not found — MCP server not installed`);
9464
+ installHookFiles(targetDir, runtime, isGlobal, failures);
9231
9465
  }
9232
9466
  const dashboardSrc = node_path.resolve(__dirname, "assets", "dashboard");
9233
9467
  if (node_fs.existsSync(dashboardSrc)) {
@@ -9255,6 +9489,22 @@ async function install(isGlobal, runtime = "claude") {
9255
9489
  else spinner.succeed(chalk.green("✓") + " Installed dashboard (server.js not found in bundle)");
9256
9490
  if (networkMode) applyFirewallRule(3333);
9257
9491
  }
9492
+ if (!isOpencode && !isCodex && !isGemini) {
9493
+ const mcpJsonPath = isGlobal ? node_path.join(targetDir, "..", ".mcp.json") : node_path.join(process.cwd(), ".mcp.json");
9494
+ let mcpConfig = {};
9495
+ if (node_fs.existsSync(mcpJsonPath)) try {
9496
+ mcpConfig = JSON.parse(node_fs.readFileSync(mcpJsonPath, "utf-8"));
9497
+ } catch {}
9498
+ const mcpServers = mcpConfig.mcpServers ?? {};
9499
+ mcpServers["maxsim"] = {
9500
+ command: "node",
9501
+ args: [".claude/maxsim/bin/mcp-server.cjs"],
9502
+ env: {}
9503
+ };
9504
+ mcpConfig.mcpServers = mcpServers;
9505
+ node_fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
9506
+ console.log(` ${chalk.green("✓")} Configured .mcp.json for MCP server auto-discovery`);
9507
+ }
9258
9508
  if (failures.length > 0) {
9259
9509
  console.error(`\n ${chalk.yellow("Installation incomplete!")} Failed: ${failures.join(", ")}`);
9260
9510
  process.exit(1);
@@ -9268,11 +9518,7 @@ async function install(isGlobal, runtime = "claude") {
9268
9518
  statuslineCommand: null,
9269
9519
  runtime
9270
9520
  };
9271
- const settingsPath = node_path.join(targetDir, "settings.json");
9272
- const settings = cleanupOrphanedHooks(readSettings(settingsPath));
9273
- const statuslineCommand = isGlobal ? buildHookCommand(targetDir, "maxsim-statusline.js") : "node " + dirName + "/hooks/maxsim-statusline.js";
9274
- const updateCheckCommand = isGlobal ? buildHookCommand(targetDir, "maxsim-check-update.js") : "node " + dirName + "/hooks/maxsim-check-update.js";
9275
- const contextMonitorCommand = isGlobal ? buildHookCommand(targetDir, "maxsim-context-monitor.js") : "node " + dirName + "/hooks/maxsim-context-monitor.js";
9521
+ const { settingsPath, settings, statuslineCommand } = configureSettingsHooks(targetDir, runtime, isGlobal);
9276
9522
  if (isGemini) {
9277
9523
  if (!settings.experimental) settings.experimental = {};
9278
9524
  const experimental = settings.experimental;
@@ -9281,26 +9527,6 @@ async function install(isGlobal, runtime = "claude") {
9281
9527
  console.log(` ${chalk.green("✓")} Enabled experimental agents`);
9282
9528
  }
9283
9529
  }
9284
- if (!isOpencode) {
9285
- if (!settings.hooks) settings.hooks = {};
9286
- const installHooks = settings.hooks;
9287
- if (!installHooks.SessionStart) installHooks.SessionStart = [];
9288
- if (!installHooks.SessionStart.some((entry) => entry.hooks && entry.hooks.some((h) => h.command && h.command.includes("maxsim-check-update")))) {
9289
- installHooks.SessionStart.push({ hooks: [{
9290
- type: "command",
9291
- command: updateCheckCommand
9292
- }] });
9293
- console.log(` ${chalk.green("✓")} Configured update check hook`);
9294
- }
9295
- if (!installHooks.PostToolUse) installHooks.PostToolUse = [];
9296
- if (!installHooks.PostToolUse.some((entry) => entry.hooks && entry.hooks.some((h) => h.command && h.command.includes("maxsim-context-monitor")))) {
9297
- installHooks.PostToolUse.push({ hooks: [{
9298
- type: "command",
9299
- command: contextMonitorCommand
9300
- }] });
9301
- console.log(` ${chalk.green("✓")} Configured context window monitor hook`);
9302
- }
9303
- }
9304
9530
  return {
9305
9531
  settingsPath,
9306
9532
  settings,
@@ -9309,63 +9535,6 @@ async function install(isGlobal, runtime = "claude") {
9309
9535
  };
9310
9536
  }
9311
9537
  /**
9312
- * Apply statusline config, then print completion message
9313
- */
9314
- function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = "claude", isGlobal = true) {
9315
- const isOpencode = runtime === "opencode";
9316
- const isCodex = runtime === "codex";
9317
- if (shouldInstallStatusline && !isOpencode && !isCodex) {
9318
- settings.statusLine = {
9319
- type: "command",
9320
- command: statuslineCommand
9321
- };
9322
- console.log(` ${chalk.green("✓")} Configured statusline`);
9323
- }
9324
- if (!isCodex && settingsPath && settings) writeSettings(settingsPath, settings);
9325
- if (isOpencode) configureOpencodePermissions(isGlobal);
9326
- let program = "Claude Code";
9327
- if (runtime === "opencode") program = "OpenCode";
9328
- if (runtime === "gemini") program = "Gemini";
9329
- if (runtime === "codex") program = "Codex";
9330
- let command = "/maxsim:help";
9331
- if (runtime === "opencode") command = "/maxsim-help";
9332
- if (runtime === "codex") command = "$maxsim-help";
9333
- console.log(`
9334
- ${chalk.green("Done!")} Launch ${program} and run ${chalk.cyan(command)}.
9335
-
9336
- ${chalk.cyan("Join the community:")} https://discord.gg/5JJgD5svVS
9337
- `);
9338
- }
9339
- /**
9340
- * Handle statusline configuration — returns true if MAXSIM statusline should be installed
9341
- */
9342
- async function handleStatusline(settings, isInteractive) {
9343
- if (!(settings.statusLine != null)) return true;
9344
- if (forceStatusline) return true;
9345
- if (!isInteractive) {
9346
- console.log(chalk.yellow("⚠") + " Skipping statusline (already configured)");
9347
- console.log(" Use " + chalk.cyan("--force-statusline") + " to replace\n");
9348
- return false;
9349
- }
9350
- const statusLine = settings.statusLine;
9351
- const existingCmd = statusLine.command || statusLine.url || "(custom)";
9352
- console.log();
9353
- console.log(chalk.yellow("⚠ Existing statusline detected"));
9354
- console.log();
9355
- console.log(" Your current statusline:");
9356
- console.log(" " + chalk.dim(`command: ${existingCmd}`));
9357
- console.log();
9358
- console.log(" MAXSIM includes a statusline showing:");
9359
- console.log(" • Model name");
9360
- console.log(" • Current task (from todo list)");
9361
- console.log(" • Context window usage (color-coded)");
9362
- console.log();
9363
- return await dist_default$1({
9364
- message: "Replace with MAXSIM statusline?",
9365
- default: false
9366
- });
9367
- }
9368
- /**
9369
9538
  * Prompt for runtime selection (multi-select)
9370
9539
  */
9371
9540
  async function promptRuntime() {
@@ -9440,7 +9609,7 @@ async function installAllRuntimes(runtimes, isGlobal, isInteractive) {
9440
9609
  const statuslineRuntimes = ["claude", "gemini"];
9441
9610
  const primaryStatuslineResult = results.find((r) => statuslineRuntimes.includes(r.runtime));
9442
9611
  let shouldInstallStatusline = false;
9443
- if (primaryStatuslineResult && primaryStatuslineResult.settings) shouldInstallStatusline = await handleStatusline(primaryStatuslineResult.settings, isInteractive);
9612
+ if (primaryStatuslineResult && primaryStatuslineResult.settings) shouldInstallStatusline = await handleStatusline(primaryStatuslineResult.settings, isInteractive, forceStatusline);
9444
9613
  let enableAgentTeams = false;
9445
9614
  if (isInteractive && runtimes.includes("claude")) enableAgentTeams = await promptAgentTeams();
9446
9615
  for (const result of results) {
@@ -9456,100 +9625,8 @@ async function installAllRuntimes(runtimes, isGlobal, isInteractive) {
9456
9625
  const subcommand = argv._[0];
9457
9626
  (async () => {
9458
9627
  if (subcommand === "dashboard") {
9459
- const { spawn: spawnDash, execSync: execSyncDash } = await import("node:child_process");
9460
- const dashboardAssetSrc = node_path.resolve(__dirname, "assets", "dashboard");
9461
- const installDir = node_path.join(process.cwd(), ".claude");
9462
- const installDashDir = node_path.join(installDir, "dashboard");
9463
- if (node_fs.existsSync(dashboardAssetSrc)) {
9464
- const nodeModulesDir = node_path.join(installDashDir, "node_modules");
9465
- const nodeModulesTmp = node_path.join(installDir, "_dashboard_node_modules_tmp");
9466
- const hadNodeModules = node_fs.existsSync(nodeModulesDir);
9467
- if (hadNodeModules) node_fs.renameSync(nodeModulesDir, nodeModulesTmp);
9468
- safeRmDir(installDashDir);
9469
- node_fs.mkdirSync(installDashDir, { recursive: true });
9470
- copyDirRecursive(dashboardAssetSrc, installDashDir);
9471
- if (hadNodeModules && node_fs.existsSync(nodeModulesTmp)) node_fs.renameSync(nodeModulesTmp, nodeModulesDir);
9472
- const dashConfigPath = node_path.join(installDir, "dashboard.json");
9473
- if (!node_fs.existsSync(dashConfigPath)) node_fs.writeFileSync(dashConfigPath, JSON.stringify({ projectCwd: process.cwd() }, null, 2) + "\n");
9474
- }
9475
- const localDashboard = node_path.join(process.cwd(), ".claude", "dashboard", "server.js");
9476
- const globalDashboard = node_path.join(node_os.homedir(), ".claude", "dashboard", "server.js");
9477
- let serverPath = null;
9478
- if (node_fs.existsSync(localDashboard)) serverPath = localDashboard;
9479
- else if (node_fs.existsSync(globalDashboard)) serverPath = globalDashboard;
9480
- if (!serverPath) {
9481
- console.log(chalk.yellow("\n Dashboard not available.\n"));
9482
- console.log(" Install MAXSIM first: " + chalk.cyan("npx maxsimcli@latest") + "\n");
9483
- process.exit(0);
9484
- }
9485
- const forceNetwork = !!argv["network"];
9486
- const dashboardDir = node_path.dirname(serverPath);
9487
- const dashboardConfigPath = node_path.join(node_path.dirname(dashboardDir), "dashboard.json");
9488
- let projectCwd = process.cwd();
9489
- let networkMode = forceNetwork;
9490
- if (node_fs.existsSync(dashboardConfigPath)) try {
9491
- const config = JSON.parse(node_fs.readFileSync(dashboardConfigPath, "utf8"));
9492
- if (config.projectCwd) projectCwd = config.projectCwd;
9493
- if (!forceNetwork) networkMode = config.networkMode ?? false;
9494
- } catch {}
9495
- const dashDirForPty = node_path.dirname(serverPath);
9496
- const ptyModulePath = node_path.join(dashDirForPty, "node_modules", "node-pty");
9497
- if (!node_fs.existsSync(ptyModulePath)) {
9498
- console.log(chalk.gray(" Installing node-pty for terminal support..."));
9499
- try {
9500
- const dashPkgPath = node_path.join(dashDirForPty, "package.json");
9501
- if (!node_fs.existsSync(dashPkgPath)) node_fs.writeFileSync(dashPkgPath, "{\"private\":true}\n");
9502
- execSyncDash("npm install node-pty --save-optional --no-audit --no-fund --loglevel=error", {
9503
- cwd: dashDirForPty,
9504
- stdio: "inherit",
9505
- timeout: 12e4
9506
- });
9507
- } catch {
9508
- console.warn(chalk.yellow(" node-pty installation failed — terminal will be unavailable."));
9509
- }
9510
- }
9511
- console.log(chalk.blue("Starting dashboard..."));
9512
- console.log(chalk.gray(` Project: ${projectCwd}`));
9513
- console.log(chalk.gray(` Server: ${serverPath}`));
9514
- if (networkMode) console.log(chalk.gray(" Network: enabled (local network access + QR code)"));
9515
- console.log("");
9516
- spawnDash(process.execPath, [serverPath], {
9517
- cwd: dashboardDir,
9518
- detached: true,
9519
- stdio: "ignore",
9520
- env: {
9521
- ...process.env,
9522
- MAXSIM_PROJECT_CWD: projectCwd,
9523
- MAXSIM_NETWORK_MODE: networkMode ? "1" : "0",
9524
- NODE_ENV: "production"
9525
- }
9526
- }).unref();
9527
- const POLL_INTERVAL_MS = 500;
9528
- const POLL_TIMEOUT_MS = 2e4;
9529
- const HEALTH_TIMEOUT_MS = 1e3;
9530
- const DEFAULT_PORT = 3333;
9531
- const PORT_RANGE_END = 3343;
9532
- let foundUrl = null;
9533
- const deadline = Date.now() + POLL_TIMEOUT_MS;
9534
- while (Date.now() < deadline) {
9535
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
9536
- for (let p = DEFAULT_PORT; p <= PORT_RANGE_END; p++) try {
9537
- const controller = new AbortController();
9538
- const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
9539
- const res = await fetch(`http://localhost:${p}/api/health`, { signal: controller.signal });
9540
- clearTimeout(timer);
9541
- if (res.ok) {
9542
- if ((await res.json()).status === "ok") {
9543
- foundUrl = `http://localhost:${p}`;
9544
- break;
9545
- }
9546
- }
9547
- } catch {}
9548
- if (foundUrl) break;
9549
- }
9550
- if (foundUrl) console.log(chalk.green(` Dashboard ready at ${foundUrl}`));
9551
- else console.log(chalk.yellow("\n Dashboard did not respond after 20s. The server may still be starting — check http://localhost:3333"));
9552
- process.exit(0);
9628
+ await runDashboardSubcommand(argv);
9629
+ return;
9553
9630
  }
9554
9631
  if (hasGlobal && hasLocal) {
9555
9632
  console.error(chalk.yellow("Cannot specify both --global and --local"));
@@ -9563,7 +9640,7 @@ const subcommand = argv._[0];
9563
9640
  process.exit(1);
9564
9641
  }
9565
9642
  const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ["claude"];
9566
- for (const runtime of runtimes) uninstall(hasGlobal, runtime);
9643
+ for (const runtime of runtimes) uninstall(hasGlobal, runtime, explicitConfigDir);
9567
9644
  } else if (selectedRuntimes.length > 0) if (!hasGlobal && !hasLocal) {
9568
9645
  const isGlobal = await promptLocation(selectedRuntimes);
9569
9646
  await installAllRuntimes(selectedRuntimes, isGlobal, true);