maxsimcli 3.11.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/assets/CHANGELOG.md +19 -0
- package/dist/assets/dashboard/client/assets/index-CxFKStBk.css +32 -0
- package/dist/assets/dashboard/client/assets/{index-CZ8WC97G.js → index-wtQDvXzr.js} +64 -64
- package/dist/assets/dashboard/client/index.html +2 -2
- package/dist/assets/templates/agents/AGENTS.md +82 -0
- package/dist/assets/templates/commands/maxsim/settings.md +1 -1
- package/dist/assets/templates/skills/code-review/SKILL.md +151 -0
- package/dist/assets/templates/skills/memory-management/SKILL.md +174 -0
- package/dist/assets/templates/skills/simplify/SKILL.md +137 -0
- package/dist/assets/templates/skills/using-maxsim/SKILL.md +115 -0
- package/dist/assets/templates/templates/config.json +1 -1
- package/dist/assets/templates/workflows/add-tests.md +3 -3
- package/dist/assets/templates/workflows/complete-milestone.md +1 -1
- package/dist/assets/templates/workflows/execute-phase.md +4 -14
- package/dist/assets/templates/workflows/init-existing.md +7 -3
- package/dist/assets/templates/workflows/new-milestone.md +4 -0
- package/dist/assets/templates/workflows/new-project.md +6 -2
- package/dist/assets/templates/workflows/plan-phase.md +2 -2
- package/dist/assets/templates/workflows/settings.md +8 -4
- package/dist/assets/templates/workflows/verify-work.md +1 -1
- package/dist/cli.cjs +265 -161
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +73 -204
- package/dist/cli.js.map +1 -1
- package/dist/core/commands.d.ts.map +1 -1
- package/dist/core/commands.js +4 -1
- package/dist/core/commands.js.map +1 -1
- package/dist/core/core.d.ts +18 -0
- package/dist/core/core.d.ts.map +1 -1
- package/dist/core/core.js +43 -13
- package/dist/core/core.js.map +1 -1
- package/dist/core/dashboard-launcher.d.ts +56 -0
- package/dist/core/dashboard-launcher.d.ts.map +1 -0
- package/dist/core/dashboard-launcher.js +243 -0
- package/dist/core/dashboard-launcher.js.map +1 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +20 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/init.d.ts +0 -1
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +0 -1
- package/dist/core/init.js.map +1 -1
- package/dist/core/phase.d.ts.map +1 -1
- package/dist/core/phase.js +7 -1
- package/dist/core/phase.js.map +1 -1
- package/dist/core/roadmap.d.ts.map +1 -1
- package/dist/core/roadmap.js +1 -0
- package/dist/core/roadmap.js.map +1 -1
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +7 -5
- package/dist/core/state.js.map +1 -1
- package/dist/core/types.d.ts +1 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +1 -2
- package/dist/core/types.js.map +1 -1
- package/dist/install/adapters.d.ts +15 -0
- package/dist/install/adapters.d.ts.map +1 -0
- package/dist/install/adapters.js +203 -0
- package/dist/install/adapters.js.map +1 -0
- package/dist/install/copy.d.ts +15 -0
- package/dist/install/copy.d.ts.map +1 -0
- package/dist/install/copy.js +191 -0
- package/dist/install/copy.js.map +1 -0
- package/dist/install/dashboard.d.ts +16 -0
- package/dist/install/dashboard.d.ts.map +1 -0
- package/dist/install/dashboard.js +273 -0
- package/dist/install/dashboard.js.map +1 -0
- package/dist/install/hooks.d.ts +32 -0
- package/dist/install/hooks.d.ts.map +1 -0
- package/dist/install/hooks.js +285 -0
- package/dist/install/hooks.js.map +1 -0
- package/dist/install/index.d.ts +2 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +598 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/manifest.d.ts +20 -0
- package/dist/install/manifest.d.ts.map +1 -0
- package/dist/install/manifest.js +135 -0
- package/dist/install/manifest.js.map +1 -0
- package/dist/install/patches.d.ts +11 -0
- package/dist/install/patches.d.ts.map +1 -0
- package/dist/install/patches.js +136 -0
- package/dist/install/patches.js.map +1 -0
- package/dist/install/shared.d.ts +50 -0
- package/dist/install/shared.d.ts.map +1 -0
- package/dist/install/shared.js +142 -0
- package/dist/install/shared.js.map +1 -0
- package/dist/install/uninstall.d.ts +6 -0
- package/dist/install/uninstall.d.ts.map +1 -0
- package/dist/install/uninstall.js +280 -0
- package/dist/install/uninstall.js.map +1 -0
- package/dist/install.cjs +763 -709
- package/dist/install.cjs.map +1 -1
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/dashboard/client/assets/index-DzJChB-D.css +0 -32
- package/dist/install.d.ts +0 -2
- package/dist/install.d.ts.map +0 -1
- package/dist/install.js +0 -1841
- 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
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
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
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
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
|
-
*
|
|
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
|
|
8480
|
-
if (
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
const
|
|
8487
|
-
if (
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
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 (
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
8436
|
+
* Configure OpenCode permissions to allow reading MAXSIM reference docs
|
|
8579
8437
|
*/
|
|
8580
|
-
function
|
|
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);
|
|
@@ -8727,296 +9144,134 @@ function uninstall(isGlobal, runtime = "claude") {
|
|
|
8727
9144
|
if (statusLine && statusLine.command && statusLine.command.includes("maxsim-statusline")) {
|
|
8728
9145
|
delete settings.statusLine;
|
|
8729
9146
|
settingsModified = true;
|
|
8730
|
-
console.log(` ${chalk.green("✓")} Removed MAXSIM statusline from settings`);
|
|
8731
|
-
}
|
|
8732
|
-
const settingsHooks = settings.hooks;
|
|
8733
|
-
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);
|
|
9147
|
+
console.log(` ${chalk.green("✓")} Removed MAXSIM statusline from settings`);
|
|
9148
|
+
}
|
|
9149
|
+
const settingsHooks = settings.hooks;
|
|
9150
|
+
if (settingsHooks && settingsHooks.SessionStart) {
|
|
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 (
|
|
8984
|
-
const
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -9206,35 +9461,7 @@ async function install(isGlobal, runtime = "claude") {
|
|
|
9206
9461
|
node_fs.copyFileSync(mcpSrc, mcpDest);
|
|
9207
9462
|
console.log(` ${chalk.green("✓")} Installed mcp-server.cjs`);
|
|
9208
9463
|
} else console.warn(` ${chalk.yellow("!")} mcp-server.cjs not found — MCP server not installed`);
|
|
9209
|
-
|
|
9210
|
-
const bundledHooksDir = node_path.resolve(__dirname, "assets", "hooks");
|
|
9211
|
-
if (node_fs.existsSync(bundledHooksDir)) hooksSrc = bundledHooksDir;
|
|
9212
|
-
else console.warn(` ${chalk.yellow("!")} bundled hooks not found - hooks will not be installed`);
|
|
9213
|
-
if (hooksSrc) {
|
|
9214
|
-
spinner = ora({
|
|
9215
|
-
text: "Installing hooks...",
|
|
9216
|
-
color: "cyan"
|
|
9217
|
-
}).start();
|
|
9218
|
-
const hooksDest = node_path.join(targetDir, "hooks");
|
|
9219
|
-
node_fs.mkdirSync(hooksDest, { recursive: true });
|
|
9220
|
-
const hookEntries = node_fs.readdirSync(hooksSrc);
|
|
9221
|
-
const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
|
|
9222
|
-
for (const entry of hookEntries) {
|
|
9223
|
-
const srcFile = node_path.join(hooksSrc, entry);
|
|
9224
|
-
if (node_fs.statSync(srcFile).isFile() && entry.endsWith(".cjs") && !entry.includes(".d.")) {
|
|
9225
|
-
const destName = entry.replace(/\.cjs$/, ".js");
|
|
9226
|
-
const destFile = node_path.join(hooksDest, destName);
|
|
9227
|
-
let content = node_fs.readFileSync(srcFile, "utf8");
|
|
9228
|
-
content = content.replace(/'\.claude'/g, configDirReplacement);
|
|
9229
|
-
node_fs.writeFileSync(destFile, content);
|
|
9230
|
-
}
|
|
9231
|
-
}
|
|
9232
|
-
if (verifyInstalled(hooksDest, "hooks")) spinner.succeed(chalk.green("✓") + " Installed hooks (bundled)");
|
|
9233
|
-
else {
|
|
9234
|
-
spinner.fail("Failed to install hooks");
|
|
9235
|
-
failures.push("hooks");
|
|
9236
|
-
}
|
|
9237
|
-
}
|
|
9464
|
+
installHookFiles(targetDir, runtime, isGlobal, failures);
|
|
9238
9465
|
}
|
|
9239
9466
|
const dashboardSrc = node_path.resolve(__dirname, "assets", "dashboard");
|
|
9240
9467
|
if (node_fs.existsSync(dashboardSrc)) {
|
|
@@ -9291,11 +9518,7 @@ async function install(isGlobal, runtime = "claude") {
|
|
|
9291
9518
|
statuslineCommand: null,
|
|
9292
9519
|
runtime
|
|
9293
9520
|
};
|
|
9294
|
-
const settingsPath =
|
|
9295
|
-
const settings = cleanupOrphanedHooks(readSettings(settingsPath));
|
|
9296
|
-
const statuslineCommand = isGlobal ? buildHookCommand(targetDir, "maxsim-statusline.js") : "node " + dirName + "/hooks/maxsim-statusline.js";
|
|
9297
|
-
const updateCheckCommand = isGlobal ? buildHookCommand(targetDir, "maxsim-check-update.js") : "node " + dirName + "/hooks/maxsim-check-update.js";
|
|
9298
|
-
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);
|
|
9299
9522
|
if (isGemini) {
|
|
9300
9523
|
if (!settings.experimental) settings.experimental = {};
|
|
9301
9524
|
const experimental = settings.experimental;
|
|
@@ -9304,26 +9527,6 @@ async function install(isGlobal, runtime = "claude") {
|
|
|
9304
9527
|
console.log(` ${chalk.green("✓")} Enabled experimental agents`);
|
|
9305
9528
|
}
|
|
9306
9529
|
}
|
|
9307
|
-
if (!isOpencode) {
|
|
9308
|
-
if (!settings.hooks) settings.hooks = {};
|
|
9309
|
-
const installHooks = settings.hooks;
|
|
9310
|
-
if (!installHooks.SessionStart) installHooks.SessionStart = [];
|
|
9311
|
-
if (!installHooks.SessionStart.some((entry) => entry.hooks && entry.hooks.some((h) => h.command && h.command.includes("maxsim-check-update")))) {
|
|
9312
|
-
installHooks.SessionStart.push({ hooks: [{
|
|
9313
|
-
type: "command",
|
|
9314
|
-
command: updateCheckCommand
|
|
9315
|
-
}] });
|
|
9316
|
-
console.log(` ${chalk.green("✓")} Configured update check hook`);
|
|
9317
|
-
}
|
|
9318
|
-
if (!installHooks.PostToolUse) installHooks.PostToolUse = [];
|
|
9319
|
-
if (!installHooks.PostToolUse.some((entry) => entry.hooks && entry.hooks.some((h) => h.command && h.command.includes("maxsim-context-monitor")))) {
|
|
9320
|
-
installHooks.PostToolUse.push({ hooks: [{
|
|
9321
|
-
type: "command",
|
|
9322
|
-
command: contextMonitorCommand
|
|
9323
|
-
}] });
|
|
9324
|
-
console.log(` ${chalk.green("✓")} Configured context window monitor hook`);
|
|
9325
|
-
}
|
|
9326
|
-
}
|
|
9327
9530
|
return {
|
|
9328
9531
|
settingsPath,
|
|
9329
9532
|
settings,
|
|
@@ -9332,63 +9535,6 @@ async function install(isGlobal, runtime = "claude") {
|
|
|
9332
9535
|
};
|
|
9333
9536
|
}
|
|
9334
9537
|
/**
|
|
9335
|
-
* Apply statusline config, then print completion message
|
|
9336
|
-
*/
|
|
9337
|
-
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = "claude", isGlobal = true) {
|
|
9338
|
-
const isOpencode = runtime === "opencode";
|
|
9339
|
-
const isCodex = runtime === "codex";
|
|
9340
|
-
if (shouldInstallStatusline && !isOpencode && !isCodex) {
|
|
9341
|
-
settings.statusLine = {
|
|
9342
|
-
type: "command",
|
|
9343
|
-
command: statuslineCommand
|
|
9344
|
-
};
|
|
9345
|
-
console.log(` ${chalk.green("✓")} Configured statusline`);
|
|
9346
|
-
}
|
|
9347
|
-
if (!isCodex && settingsPath && settings) writeSettings(settingsPath, settings);
|
|
9348
|
-
if (isOpencode) configureOpencodePermissions(isGlobal);
|
|
9349
|
-
let program = "Claude Code";
|
|
9350
|
-
if (runtime === "opencode") program = "OpenCode";
|
|
9351
|
-
if (runtime === "gemini") program = "Gemini";
|
|
9352
|
-
if (runtime === "codex") program = "Codex";
|
|
9353
|
-
let command = "/maxsim:help";
|
|
9354
|
-
if (runtime === "opencode") command = "/maxsim-help";
|
|
9355
|
-
if (runtime === "codex") command = "$maxsim-help";
|
|
9356
|
-
console.log(`
|
|
9357
|
-
${chalk.green("Done!")} Launch ${program} and run ${chalk.cyan(command)}.
|
|
9358
|
-
|
|
9359
|
-
${chalk.cyan("Join the community:")} https://discord.gg/5JJgD5svVS
|
|
9360
|
-
`);
|
|
9361
|
-
}
|
|
9362
|
-
/**
|
|
9363
|
-
* Handle statusline configuration — returns true if MAXSIM statusline should be installed
|
|
9364
|
-
*/
|
|
9365
|
-
async function handleStatusline(settings, isInteractive) {
|
|
9366
|
-
if (!(settings.statusLine != null)) return true;
|
|
9367
|
-
if (forceStatusline) return true;
|
|
9368
|
-
if (!isInteractive) {
|
|
9369
|
-
console.log(chalk.yellow("⚠") + " Skipping statusline (already configured)");
|
|
9370
|
-
console.log(" Use " + chalk.cyan("--force-statusline") + " to replace\n");
|
|
9371
|
-
return false;
|
|
9372
|
-
}
|
|
9373
|
-
const statusLine = settings.statusLine;
|
|
9374
|
-
const existingCmd = statusLine.command || statusLine.url || "(custom)";
|
|
9375
|
-
console.log();
|
|
9376
|
-
console.log(chalk.yellow("⚠ Existing statusline detected"));
|
|
9377
|
-
console.log();
|
|
9378
|
-
console.log(" Your current statusline:");
|
|
9379
|
-
console.log(" " + chalk.dim(`command: ${existingCmd}`));
|
|
9380
|
-
console.log();
|
|
9381
|
-
console.log(" MAXSIM includes a statusline showing:");
|
|
9382
|
-
console.log(" • Model name");
|
|
9383
|
-
console.log(" • Current task (from todo list)");
|
|
9384
|
-
console.log(" • Context window usage (color-coded)");
|
|
9385
|
-
console.log();
|
|
9386
|
-
return await dist_default$1({
|
|
9387
|
-
message: "Replace with MAXSIM statusline?",
|
|
9388
|
-
default: false
|
|
9389
|
-
});
|
|
9390
|
-
}
|
|
9391
|
-
/**
|
|
9392
9538
|
* Prompt for runtime selection (multi-select)
|
|
9393
9539
|
*/
|
|
9394
9540
|
async function promptRuntime() {
|
|
@@ -9463,7 +9609,7 @@ async function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
9463
9609
|
const statuslineRuntimes = ["claude", "gemini"];
|
|
9464
9610
|
const primaryStatuslineResult = results.find((r) => statuslineRuntimes.includes(r.runtime));
|
|
9465
9611
|
let shouldInstallStatusline = false;
|
|
9466
|
-
if (primaryStatuslineResult && primaryStatuslineResult.settings) shouldInstallStatusline = await handleStatusline(primaryStatuslineResult.settings, isInteractive);
|
|
9612
|
+
if (primaryStatuslineResult && primaryStatuslineResult.settings) shouldInstallStatusline = await handleStatusline(primaryStatuslineResult.settings, isInteractive, forceStatusline);
|
|
9467
9613
|
let enableAgentTeams = false;
|
|
9468
9614
|
if (isInteractive && runtimes.includes("claude")) enableAgentTeams = await promptAgentTeams();
|
|
9469
9615
|
for (const result of results) {
|
|
@@ -9479,100 +9625,8 @@ async function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
9479
9625
|
const subcommand = argv._[0];
|
|
9480
9626
|
(async () => {
|
|
9481
9627
|
if (subcommand === "dashboard") {
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
const installDir = node_path.join(process.cwd(), ".claude");
|
|
9485
|
-
const installDashDir = node_path.join(installDir, "dashboard");
|
|
9486
|
-
if (node_fs.existsSync(dashboardAssetSrc)) {
|
|
9487
|
-
const nodeModulesDir = node_path.join(installDashDir, "node_modules");
|
|
9488
|
-
const nodeModulesTmp = node_path.join(installDir, "_dashboard_node_modules_tmp");
|
|
9489
|
-
const hadNodeModules = node_fs.existsSync(nodeModulesDir);
|
|
9490
|
-
if (hadNodeModules) node_fs.renameSync(nodeModulesDir, nodeModulesTmp);
|
|
9491
|
-
safeRmDir(installDashDir);
|
|
9492
|
-
node_fs.mkdirSync(installDashDir, { recursive: true });
|
|
9493
|
-
copyDirRecursive(dashboardAssetSrc, installDashDir);
|
|
9494
|
-
if (hadNodeModules && node_fs.existsSync(nodeModulesTmp)) node_fs.renameSync(nodeModulesTmp, nodeModulesDir);
|
|
9495
|
-
const dashConfigPath = node_path.join(installDir, "dashboard.json");
|
|
9496
|
-
if (!node_fs.existsSync(dashConfigPath)) node_fs.writeFileSync(dashConfigPath, JSON.stringify({ projectCwd: process.cwd() }, null, 2) + "\n");
|
|
9497
|
-
}
|
|
9498
|
-
const localDashboard = node_path.join(process.cwd(), ".claude", "dashboard", "server.js");
|
|
9499
|
-
const globalDashboard = node_path.join(node_os.homedir(), ".claude", "dashboard", "server.js");
|
|
9500
|
-
let serverPath = null;
|
|
9501
|
-
if (node_fs.existsSync(localDashboard)) serverPath = localDashboard;
|
|
9502
|
-
else if (node_fs.existsSync(globalDashboard)) serverPath = globalDashboard;
|
|
9503
|
-
if (!serverPath) {
|
|
9504
|
-
console.log(chalk.yellow("\n Dashboard not available.\n"));
|
|
9505
|
-
console.log(" Install MAXSIM first: " + chalk.cyan("npx maxsimcli@latest") + "\n");
|
|
9506
|
-
process.exit(0);
|
|
9507
|
-
}
|
|
9508
|
-
const forceNetwork = !!argv["network"];
|
|
9509
|
-
const dashboardDir = node_path.dirname(serverPath);
|
|
9510
|
-
const dashboardConfigPath = node_path.join(node_path.dirname(dashboardDir), "dashboard.json");
|
|
9511
|
-
let projectCwd = process.cwd();
|
|
9512
|
-
let networkMode = forceNetwork;
|
|
9513
|
-
if (node_fs.existsSync(dashboardConfigPath)) try {
|
|
9514
|
-
const config = JSON.parse(node_fs.readFileSync(dashboardConfigPath, "utf8"));
|
|
9515
|
-
if (config.projectCwd) projectCwd = config.projectCwd;
|
|
9516
|
-
if (!forceNetwork) networkMode = config.networkMode ?? false;
|
|
9517
|
-
} catch {}
|
|
9518
|
-
const dashDirForPty = node_path.dirname(serverPath);
|
|
9519
|
-
const ptyModulePath = node_path.join(dashDirForPty, "node_modules", "node-pty");
|
|
9520
|
-
if (!node_fs.existsSync(ptyModulePath)) {
|
|
9521
|
-
console.log(chalk.gray(" Installing node-pty for terminal support..."));
|
|
9522
|
-
try {
|
|
9523
|
-
const dashPkgPath = node_path.join(dashDirForPty, "package.json");
|
|
9524
|
-
if (!node_fs.existsSync(dashPkgPath)) node_fs.writeFileSync(dashPkgPath, "{\"private\":true}\n");
|
|
9525
|
-
execSyncDash("npm install node-pty --save-optional --no-audit --no-fund --loglevel=error", {
|
|
9526
|
-
cwd: dashDirForPty,
|
|
9527
|
-
stdio: "inherit",
|
|
9528
|
-
timeout: 12e4
|
|
9529
|
-
});
|
|
9530
|
-
} catch {
|
|
9531
|
-
console.warn(chalk.yellow(" node-pty installation failed — terminal will be unavailable."));
|
|
9532
|
-
}
|
|
9533
|
-
}
|
|
9534
|
-
console.log(chalk.blue("Starting dashboard..."));
|
|
9535
|
-
console.log(chalk.gray(` Project: ${projectCwd}`));
|
|
9536
|
-
console.log(chalk.gray(` Server: ${serverPath}`));
|
|
9537
|
-
if (networkMode) console.log(chalk.gray(" Network: enabled (local network access + QR code)"));
|
|
9538
|
-
console.log("");
|
|
9539
|
-
spawnDash(process.execPath, [serverPath], {
|
|
9540
|
-
cwd: dashboardDir,
|
|
9541
|
-
detached: true,
|
|
9542
|
-
stdio: "ignore",
|
|
9543
|
-
env: {
|
|
9544
|
-
...process.env,
|
|
9545
|
-
MAXSIM_PROJECT_CWD: projectCwd,
|
|
9546
|
-
MAXSIM_NETWORK_MODE: networkMode ? "1" : "0",
|
|
9547
|
-
NODE_ENV: "production"
|
|
9548
|
-
}
|
|
9549
|
-
}).unref();
|
|
9550
|
-
const POLL_INTERVAL_MS = 500;
|
|
9551
|
-
const POLL_TIMEOUT_MS = 2e4;
|
|
9552
|
-
const HEALTH_TIMEOUT_MS = 1e3;
|
|
9553
|
-
const DEFAULT_PORT = 3333;
|
|
9554
|
-
const PORT_RANGE_END = 3343;
|
|
9555
|
-
let foundUrl = null;
|
|
9556
|
-
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
9557
|
-
while (Date.now() < deadline) {
|
|
9558
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
9559
|
-
for (let p = DEFAULT_PORT; p <= PORT_RANGE_END; p++) try {
|
|
9560
|
-
const controller = new AbortController();
|
|
9561
|
-
const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
9562
|
-
const res = await fetch(`http://localhost:${p}/api/health`, { signal: controller.signal });
|
|
9563
|
-
clearTimeout(timer);
|
|
9564
|
-
if (res.ok) {
|
|
9565
|
-
if ((await res.json()).status === "ok") {
|
|
9566
|
-
foundUrl = `http://localhost:${p}`;
|
|
9567
|
-
break;
|
|
9568
|
-
}
|
|
9569
|
-
}
|
|
9570
|
-
} catch {}
|
|
9571
|
-
if (foundUrl) break;
|
|
9572
|
-
}
|
|
9573
|
-
if (foundUrl) console.log(chalk.green(` Dashboard ready at ${foundUrl}`));
|
|
9574
|
-
else console.log(chalk.yellow("\n Dashboard did not respond after 20s. The server may still be starting — check http://localhost:3333"));
|
|
9575
|
-
process.exit(0);
|
|
9628
|
+
await runDashboardSubcommand(argv);
|
|
9629
|
+
return;
|
|
9576
9630
|
}
|
|
9577
9631
|
if (hasGlobal && hasLocal) {
|
|
9578
9632
|
console.error(chalk.yellow("Cannot specify both --global and --local"));
|
|
@@ -9586,7 +9640,7 @@ const subcommand = argv._[0];
|
|
|
9586
9640
|
process.exit(1);
|
|
9587
9641
|
}
|
|
9588
9642
|
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ["claude"];
|
|
9589
|
-
for (const runtime of runtimes) uninstall(hasGlobal, runtime);
|
|
9643
|
+
for (const runtime of runtimes) uninstall(hasGlobal, runtime, explicitConfigDir);
|
|
9590
9644
|
} else if (selectedRuntimes.length > 0) if (!hasGlobal && !hasLocal) {
|
|
9591
9645
|
const isGlobal = await promptLocation(selectedRuntimes);
|
|
9592
9646
|
await installAllRuntimes(selectedRuntimes, isGlobal, true);
|