nexus-agents 2.61.0 → 2.63.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/cli.js CHANGED
@@ -16,7 +16,7 @@ import "./chunk-6QU4DJYW.js";
16
16
  import {
17
17
  setupCommandAsync,
18
18
  verifyCommand
19
- } from "./chunk-KNAPTURC.js";
19
+ } from "./chunk-7C32M23X.js";
20
20
  import "./chunk-QGZBCD2A.js";
21
21
  import {
22
22
  AuthHandler,
@@ -142,7 +142,7 @@ import {
142
142
  validateCommand,
143
143
  validateWorkflow,
144
144
  wrapInMarkdownFence
145
- } from "./chunk-TRWIEUI2.js";
145
+ } from "./chunk-4G7MSCIK.js";
146
146
  import "./chunk-ED6VQWNG.js";
147
147
  import {
148
148
  resolveToken
@@ -201,7 +201,7 @@ import {
201
201
  loadConfig,
202
202
  runDoctor,
203
203
  validateNexusEnv
204
- } from "./chunk-NB6IYTMN.js";
204
+ } from "./chunk-FMFQJLMR.js";
205
205
  import {
206
206
  DEFAULTS
207
207
  } from "./chunk-H43PABG4.js";
@@ -17663,6 +17663,20 @@ var PARSE_ARGS_CONFIG = {
17663
17663
  gitignore: {
17664
17664
  type: "boolean",
17665
17665
  default: false
17666
+ },
17667
+ // init --portable --mcp-config flag (#2308)
17668
+ "mcp-config": {
17669
+ type: "boolean",
17670
+ default: false
17671
+ },
17672
+ // init --portable --install / --uninstall flags (#2311)
17673
+ install: {
17674
+ type: "boolean",
17675
+ default: false
17676
+ },
17677
+ uninstall: {
17678
+ type: "boolean",
17679
+ default: false
17666
17680
  }
17667
17681
  },
17668
17682
  allowPositionals: true,
@@ -17721,18 +17735,294 @@ function isValidCommand(value) {
17721
17735
  }
17722
17736
 
17723
17737
  // src/cli-commands-handlers.ts
17724
- import { existsSync as existsSync19 } from "fs";
17738
+ import { existsSync as existsSync22 } from "fs";
17725
17739
 
17726
17740
  // src/cli/init-portable.ts
17727
17741
  import {
17728
- existsSync as existsSync17,
17729
- mkdirSync as mkdirSync3,
17742
+ existsSync as existsSync20,
17743
+ mkdirSync as mkdirSync5,
17730
17744
  readdirSync as readdirSync2,
17731
17745
  statSync as statSync3,
17732
- appendFileSync,
17733
- readFileSync as readFileSync10
17746
+ appendFileSync as appendFileSync2,
17747
+ readFileSync as readFileSync12
17734
17748
  } from "fs";
17735
- import { resolve as resolve11, join as join13, isAbsolute as isAbsolute2 } from "path";
17749
+ import { resolve as resolve11, join as join16, isAbsolute as isAbsolute2 } from "path";
17750
+
17751
+ // src/cli/mcp-config-emitter.ts
17752
+ import { existsSync as existsSync17, readFileSync as readFileSync10, writeFileSync as writeFileSync5, appendFileSync } from "fs";
17753
+ import { join as join13 } from "path";
17754
+ var MCP_CONFIG_FILENAME = ".mcp.json";
17755
+ var NEXUS_SERVER_KEY = "nexus-agents";
17756
+ function buildNexusServerEntry(dataDir, commandPath) {
17757
+ return {
17758
+ command: commandPath ?? "nexus-agents",
17759
+ args: ["--mode=server"],
17760
+ env: { NEXUS_DATA_DIR: dataDir }
17761
+ };
17762
+ }
17763
+ function entriesEqual(a, b) {
17764
+ if (a.command !== b.command) return false;
17765
+ if (a.args.length !== b.args.length) return false;
17766
+ for (let i = 0; i < a.args.length; i++) if (a.args[i] !== b.args[i]) return false;
17767
+ const aEnv = a.env ?? {};
17768
+ const bEnv = b.env ?? {};
17769
+ const aKeys = Object.keys(aEnv);
17770
+ const bKeys = Object.keys(bEnv);
17771
+ if (aKeys.length !== bKeys.length) return false;
17772
+ for (const k of aKeys) if (aEnv[k] !== bEnv[k]) return false;
17773
+ return true;
17774
+ }
17775
+ function loadExistingConfig(path23) {
17776
+ if (!existsSync17(path23)) return { ok: true, value: void 0 };
17777
+ let raw;
17778
+ try {
17779
+ raw = readFileSync10(path23, "utf-8");
17780
+ } catch (e) {
17781
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
17782
+ }
17783
+ try {
17784
+ const parsed = JSON.parse(raw);
17785
+ if (typeof parsed !== "object" || parsed === null) {
17786
+ return { ok: false, error: `${path23}: top-level JSON must be an object` };
17787
+ }
17788
+ return { ok: true, value: parsed };
17789
+ } catch (e) {
17790
+ const msg = e instanceof Error ? e.message : String(e);
17791
+ return { ok: false, error: `${path23}: invalid JSON \u2014 ${msg}` };
17792
+ }
17793
+ }
17794
+ function decideEmission(existing, desired, force) {
17795
+ if (existing === void 0) {
17796
+ return { kind: "write", nextConfig: { mcpServers: { [NEXUS_SERVER_KEY]: desired } } };
17797
+ }
17798
+ const servers = existing.mcpServers ?? {};
17799
+ const current = servers[NEXUS_SERVER_KEY];
17800
+ if (current !== void 0 && entriesEqual(current, desired)) {
17801
+ return { kind: "noop" };
17802
+ }
17803
+ if (current !== void 0 && !force) {
17804
+ return {
17805
+ kind: "refuse",
17806
+ reason: `existing ${NEXUS_SERVER_KEY} entry differs; pass --force to overwrite`
17807
+ };
17808
+ }
17809
+ const nextServers = { ...servers, [NEXUS_SERVER_KEY]: desired };
17810
+ return { kind: "write", nextConfig: { ...existing, mcpServers: nextServers } };
17811
+ }
17812
+ function autoGitignoreMcpConfig(workspaceDir, dryRun) {
17813
+ const gitDir = join13(workspaceDir, ".git");
17814
+ if (!existsSync17(gitDir)) return false;
17815
+ const gitignorePath = join13(workspaceDir, ".gitignore");
17816
+ let existing = "";
17817
+ if (existsSync17(gitignorePath)) {
17818
+ existing = readFileSync10(gitignorePath, "utf-8");
17819
+ const already = existing.split("\n").some((l) => l.trim() === MCP_CONFIG_FILENAME || l.trim() === `/${MCP_CONFIG_FILENAME}`);
17820
+ if (already) return false;
17821
+ }
17822
+ if (!dryRun) {
17823
+ const sep3 = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
17824
+ appendFileSync(gitignorePath, `${sep3}${MCP_CONFIG_FILENAME}
17825
+ `, "utf-8");
17826
+ }
17827
+ return true;
17828
+ }
17829
+ function emitMcpConfig(options) {
17830
+ const mcpConfigPath = join13(options.workspaceDir, MCP_CONFIG_FILENAME);
17831
+ const dryRun = options.dryRun === true;
17832
+ const force = options.force === true;
17833
+ const desired = buildNexusServerEntry(options.dataDir, options.commandPath);
17834
+ const loaded = loadExistingConfig(mcpConfigPath);
17835
+ if (!loaded.ok) return makeFailure(mcpConfigPath, loaded.error);
17836
+ const decision = decideEmission(loaded.value, desired, force);
17837
+ if (decision.kind === "refuse") return makeFailure(mcpConfigPath, decision.reason);
17838
+ if (decision.kind === "noop") {
17839
+ return makeSuccess({
17840
+ mcpConfigPath,
17841
+ written: false,
17842
+ alreadyMatched: true,
17843
+ gitignoreUpdated: false
17844
+ });
17845
+ }
17846
+ if (!dryRun) {
17847
+ writeFileSync5(mcpConfigPath, JSON.stringify(decision.nextConfig, null, 2) + "\n", "utf-8");
17848
+ }
17849
+ const gitignoreUpdated = autoGitignoreMcpConfig(options.workspaceDir, dryRun);
17850
+ return makeSuccess({ mcpConfigPath, written: true, alreadyMatched: false, gitignoreUpdated });
17851
+ }
17852
+ function makeSuccess(opts) {
17853
+ return { success: true, error: null, ...opts };
17854
+ }
17855
+ function makeFailure(mcpConfigPath, error) {
17856
+ return {
17857
+ success: false,
17858
+ mcpConfigPath,
17859
+ written: false,
17860
+ alreadyMatched: false,
17861
+ gitignoreUpdated: false,
17862
+ error
17863
+ };
17864
+ }
17865
+
17866
+ // src/cli/portable-installer.ts
17867
+ import { execFile } from "child_process";
17868
+ import { existsSync as existsSync19, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync7 } from "fs";
17869
+ import { join as join15 } from "path";
17870
+ import { promisify } from "util";
17871
+
17872
+ // src/cli/bin-shim.ts
17873
+ import { existsSync as existsSync18, readFileSync as readFileSync11, writeFileSync as writeFileSync6, chmodSync as chmodSync2, mkdirSync as mkdirSync3 } from "fs";
17874
+ import { join as join14 } from "path";
17875
+ var SHIM_BASENAME = "nexus-agents";
17876
+ var SHIM_MODE = 493;
17877
+ function buildShimContents(cliEntryPath) {
17878
+ const lines = [
17879
+ "#!/usr/bin/env node",
17880
+ "// Generated by `nexus-agents init --portable --install` (#3a).",
17881
+ "// Do not edit by hand \u2014 re-run `init --portable --install` to refresh.",
17882
+ `import('${cliEntryPath}').catch((e) => { console.error(e); process.exit(1); });`,
17883
+ ""
17884
+ ];
17885
+ return lines.join("\n");
17886
+ }
17887
+ function writeBinShim(options) {
17888
+ const shimPath = join14(options.binDir, SHIM_BASENAME);
17889
+ const desired = buildShimContents(options.cliEntryPath);
17890
+ const dryRun = options.dryRun === true;
17891
+ try {
17892
+ if (existsSync18(shimPath)) {
17893
+ const current = readFileSync11(shimPath, "utf-8");
17894
+ if (current === desired) {
17895
+ return { success: true, shimPath, written: false, alreadyMatched: true, error: null };
17896
+ }
17897
+ }
17898
+ if (!dryRun) {
17899
+ if (!existsSync18(options.binDir)) mkdirSync3(options.binDir, { recursive: true });
17900
+ writeFileSync6(shimPath, desired, "utf-8");
17901
+ chmodSync2(shimPath, SHIM_MODE);
17902
+ }
17903
+ return { success: true, shimPath, written: true, alreadyMatched: false, error: null };
17904
+ } catch (error) {
17905
+ const msg = error instanceof Error ? error.message : String(error);
17906
+ return { success: false, shimPath, written: false, alreadyMatched: false, error: msg };
17907
+ }
17908
+ }
17909
+
17910
+ // src/cli/portable-installer.ts
17911
+ var execFileAsync = promisify(execFile);
17912
+ var CLI_SUBDIR = "cli";
17913
+ var BIN_SUBDIR = "bin";
17914
+ var CLI_ENTRY_RELATIVE = "node_modules/nexus-agents/dist/cli.js";
17915
+ var NPM_INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
17916
+ function resolveInstallVersion(override) {
17917
+ const v = override ?? VERSION;
17918
+ if (v === "dev" || v === "" || v.includes(" ")) {
17919
+ return {
17920
+ ok: false,
17921
+ error: `cannot resolve install version: got '${v}'. Portable install requires a published nexus-agents version (build the CLI from a release).`
17922
+ };
17923
+ }
17924
+ return { ok: true, value: v };
17925
+ }
17926
+ function writeInstallManifest(cliDir, version) {
17927
+ const manifest = {
17928
+ name: "nexus-agents-portable-shim",
17929
+ private: true,
17930
+ version: "0.0.0",
17931
+ description: "Local install root for portable nexus-agents (generated; do not commit)",
17932
+ dependencies: { "nexus-agents": version }
17933
+ };
17934
+ writeFileSync7(join15(cliDir, "package.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
17935
+ }
17936
+ function isAlreadyInstalled(cliDir) {
17937
+ return existsSync19(join15(cliDir, "node_modules", "nexus-agents", "package.json"));
17938
+ }
17939
+ async function spawnNpmInstall(cliDir) {
17940
+ await execFileAsync("npm", ["install", "--no-audit", "--no-fund", "--silent"], {
17941
+ cwd: cliDir,
17942
+ timeout: NPM_INSTALL_TIMEOUT_MS
17943
+ });
17944
+ }
17945
+ function cleanupOnFailure(cliDir) {
17946
+ try {
17947
+ rmSync(cliDir, { recursive: true, force: true });
17948
+ } catch {
17949
+ }
17950
+ }
17951
+ async function runNpmStep(ctx) {
17952
+ try {
17953
+ if (!existsSync19(ctx.cliDir)) mkdirSync4(ctx.cliDir, { recursive: true });
17954
+ writeInstallManifest(ctx.cliDir, ctx.version);
17955
+ await spawnNpmInstall(ctx.cliDir);
17956
+ return void 0;
17957
+ } catch (error) {
17958
+ cleanupOnFailure(ctx.cliDir);
17959
+ const msg = error instanceof Error ? error.message : String(error);
17960
+ return { success: false, ...ctx, skipped: false, error: `npm install failed: ${msg}` };
17961
+ }
17962
+ }
17963
+ async function installPortable(options) {
17964
+ const cliDir = join15(options.dataDir, CLI_SUBDIR);
17965
+ const binDir = join15(options.dataDir, BIN_SUBDIR);
17966
+ const versionResolution = resolveInstallVersion(options.version);
17967
+ if (!versionResolution.ok) {
17968
+ return {
17969
+ success: false,
17970
+ version: "",
17971
+ cliDir,
17972
+ binDir,
17973
+ skipped: false,
17974
+ error: versionResolution.error
17975
+ };
17976
+ }
17977
+ const ctx = { cliDir, binDir, version: versionResolution.value };
17978
+ if (isAlreadyInstalled(cliDir) && options.force !== true) {
17979
+ return { success: true, ...ctx, skipped: true, error: null };
17980
+ }
17981
+ if (options.dryRun === true) {
17982
+ return { success: true, ...ctx, skipped: false, error: null };
17983
+ }
17984
+ const npmFailure = await runNpmStep(ctx);
17985
+ if (npmFailure !== void 0) return npmFailure;
17986
+ const shim = writeBinShim({ binDir, cliEntryPath: join15(cliDir, CLI_ENTRY_RELATIVE) });
17987
+ if (!shim.success) {
17988
+ cleanupOnFailure(cliDir);
17989
+ return {
17990
+ success: false,
17991
+ ...ctx,
17992
+ shim,
17993
+ skipped: false,
17994
+ error: `bin shim emission failed: ${shim.error ?? "unknown"}`
17995
+ };
17996
+ }
17997
+ return { success: true, ...ctx, shim, skipped: false, error: null };
17998
+ }
17999
+ function uninstallPortable(options) {
18000
+ const cliDir = join15(options.dataDir, CLI_SUBDIR);
18001
+ const binDir = join15(options.dataDir, BIN_SUBDIR);
18002
+ const removed = [];
18003
+ const notPresent = [];
18004
+ const dryRun = options.dryRun === true;
18005
+ try {
18006
+ for (const dir of [cliDir, binDir]) {
18007
+ if (!existsSync19(dir)) {
18008
+ notPresent.push(dir);
18009
+ continue;
18010
+ }
18011
+ if (!dryRun) rmSync(dir, { recursive: true, force: true });
18012
+ removed.push(dir);
18013
+ }
18014
+ return { success: true, removed, notPresent, error: null };
18015
+ } catch (error) {
18016
+ const msg = error instanceof Error ? error.message : String(error);
18017
+ return { success: false, removed, notPresent, error: msg };
18018
+ }
18019
+ }
18020
+ function findBinShim(dataDir) {
18021
+ const shimPath = join15(dataDir, BIN_SUBDIR, "nexus-agents");
18022
+ return existsSync19(shimPath) ? shimPath : void 0;
18023
+ }
18024
+
18025
+ // src/cli/init-portable.ts
17736
18026
  var DEFAULT_PORTABLE_DIRNAME = ".nexus-agents";
17737
18027
  var RESTRICTED_SUBDIRS = /* @__PURE__ */ new Set(["auth"]);
17738
18028
  function resolveTargetPath(rawPath) {
@@ -17742,53 +18032,53 @@ function resolveTargetPath(rawPath) {
17742
18032
  return isAbsolute2(rawPath) ? rawPath : resolve11(process.cwd(), rawPath);
17743
18033
  }
17744
18034
  function isNonEmpty(dir) {
17745
- if (!existsSync17(dir)) return false;
18035
+ if (!existsSync20(dir)) return false;
17746
18036
  const stat2 = statSync3(dir);
17747
18037
  if (!stat2.isDirectory()) return true;
17748
18038
  return readdirSync2(dir).length > 0;
17749
18039
  }
17750
18040
  function ensureDir(path23, dryRun, created, alreadyExisted, mode) {
17751
- if (existsSync17(path23)) {
18041
+ if (existsSync20(path23)) {
17752
18042
  alreadyExisted.push(path23);
17753
18043
  return;
17754
18044
  }
17755
18045
  if (!dryRun) {
17756
- mkdirSync3(path23, { recursive: true, ...mode !== void 0 ? { mode } : {} });
18046
+ mkdirSync5(path23, { recursive: true, ...mode !== void 0 ? { mode } : {} });
17757
18047
  }
17758
18048
  created.push(path23);
17759
18049
  }
17760
18050
  function maybeUpdateGitignore(workspaceDir, portableDirName, dryRun) {
17761
- const gitDir = join13(workspaceDir, ".git");
17762
- if (!existsSync17(gitDir)) return false;
17763
- const gitignorePath = join13(workspaceDir, ".gitignore");
18051
+ const gitDir = join16(workspaceDir, ".git");
18052
+ if (!existsSync20(gitDir)) return false;
18053
+ const gitignorePath = join16(workspaceDir, ".gitignore");
17764
18054
  const entry = `${portableDirName}/`;
17765
18055
  let existing = "";
17766
- if (existsSync17(gitignorePath)) {
17767
- existing = readFileSync10(gitignorePath, "utf-8");
18056
+ if (existsSync20(gitignorePath)) {
18057
+ existing = readFileSync12(gitignorePath, "utf-8");
17768
18058
  if (existing.split("\n").some((l) => l.trim() === entry || l.trim() === portableDirName)) {
17769
18059
  return false;
17770
18060
  }
17771
18061
  }
17772
18062
  if (!dryRun) {
17773
18063
  const sep3 = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
17774
- appendFileSync(gitignorePath, `${sep3}${entry}
18064
+ appendFileSync2(gitignorePath, `${sep3}${entry}
17775
18065
  `, "utf-8");
17776
18066
  }
17777
18067
  return true;
17778
18068
  }
17779
18069
  function inspectTarget(target) {
17780
- const exists = existsSync17(target);
18070
+ const exists = existsSync20(target);
17781
18071
  if (!exists) return { exists: false, nonEmpty: false, isExistingNexusDir: false };
17782
18072
  const nonEmpty = isNonEmpty(target);
17783
18073
  const stat2 = statSync3(target);
17784
- const isExistingNexusDir = stat2.isDirectory() && existsSync17(join13(target, "audit"));
18074
+ const isExistingNexusDir = stat2.isDirectory() && existsSync20(join16(target, "audit"));
17785
18075
  return { exists, nonEmpty, isExistingNexusDir };
17786
18076
  }
17787
18077
  function createDataLayout(target, dryRun, created, alreadyExisted) {
17788
18078
  ensureDir(target, dryRun, created, alreadyExisted);
17789
18079
  for (const subdir of DATA_SUBDIRECTORIES) {
17790
18080
  const mode = RESTRICTED_SUBDIRS.has(subdir) ? 448 : void 0;
17791
- ensureDir(join13(target, subdir), dryRun, created, alreadyExisted, mode);
18081
+ ensureDir(join16(target, subdir), dryRun, created, alreadyExisted, mode);
17792
18082
  }
17793
18083
  }
17794
18084
  function makeResult(opts) {
@@ -17799,6 +18089,9 @@ function makeResult(opts) {
17799
18089
  alreadyExisted: opts.alreadyExisted,
17800
18090
  skipped: opts.skipped ?? false,
17801
18091
  gitignoreUpdated: opts.gitignoreUpdated ?? false,
18092
+ ...opts.mcpConfig !== void 0 ? { mcpConfig: opts.mcpConfig } : {},
18093
+ ...opts.install !== void 0 ? { install: opts.install } : {},
18094
+ ...opts.uninstall !== void 0 ? { uninstall: opts.uninstall } : {},
17802
18095
  error: opts.error ?? null
17803
18096
  };
17804
18097
  }
@@ -17808,7 +18101,63 @@ function applyGitignoreOption(target, options, dryRun) {
17808
18101
  const portableName = target.slice(workspaceDir.length + 1);
17809
18102
  return maybeUpdateGitignore(workspaceDir, portableName, dryRun);
17810
18103
  }
17811
- function initPortable(options = {}) {
18104
+ function applyMcpConfigOption(target, options, dryRun) {
18105
+ if (options.mcpConfig !== true) return void 0;
18106
+ const workspaceDir = resolve11(target, "..");
18107
+ const shimPath = findBinShim(target);
18108
+ return emitMcpConfig({
18109
+ workspaceDir,
18110
+ dataDir: target,
18111
+ ...shimPath !== void 0 && { commandPath: shimPath },
18112
+ force: options.force === true,
18113
+ dryRun
18114
+ });
18115
+ }
18116
+ async function applyInstallOption(target, options, dryRun) {
18117
+ if (options.install !== true) return void 0;
18118
+ return installPortable({ dataDir: target, force: options.force === true, dryRun });
18119
+ }
18120
+ function buildSuccessResult(base, flags, extras) {
18121
+ const installFailed = extras.install !== void 0 && !extras.install.success;
18122
+ const mcpFailed = extras.mcpConfig !== void 0 && !extras.mcpConfig.success;
18123
+ if (installFailed) {
18124
+ return makeResult({
18125
+ ...base,
18126
+ ...flags,
18127
+ success: false,
18128
+ ...extras,
18129
+ error: extras.install?.error ?? "install failed"
18130
+ });
18131
+ }
18132
+ if (mcpFailed) {
18133
+ return makeResult({
18134
+ ...base,
18135
+ ...flags,
18136
+ success: false,
18137
+ ...extras,
18138
+ error: extras.mcpConfig?.error ?? "mcp-config emission failed"
18139
+ });
18140
+ }
18141
+ return makeResult({ ...base, ...flags, success: true, ...extras });
18142
+ }
18143
+ async function collectExtras(target, options, dryRun) {
18144
+ const extras = {};
18145
+ const install = await applyInstallOption(target, options, dryRun);
18146
+ if (install !== void 0) extras.install = install;
18147
+ const mcpConfig = applyMcpConfigOption(target, options, dryRun);
18148
+ if (mcpConfig !== void 0) extras.mcpConfig = mcpConfig;
18149
+ return extras;
18150
+ }
18151
+ function handleUninstall(target, base, dryRun) {
18152
+ const uninstall = uninstallPortable({ dataDir: target, dryRun });
18153
+ return makeResult({
18154
+ ...base,
18155
+ success: uninstall.success,
18156
+ uninstall,
18157
+ error: uninstall.error
18158
+ });
18159
+ }
18160
+ async function initPortable(options = {}) {
17812
18161
  const created = [];
17813
18162
  const alreadyExisted = [];
17814
18163
  const dryRun = options.dryRun === true;
@@ -17816,10 +18165,12 @@ function initPortable(options = {}) {
17816
18165
  const target = resolveTargetPath(options.path);
17817
18166
  const base = { absolutePath: target, created, alreadyExisted };
17818
18167
  try {
18168
+ if (options.uninstall === true) return handleUninstall(target, base, dryRun);
17819
18169
  const state = inspectTarget(target);
17820
18170
  if (state.isExistingNexusDir && !force) {
17821
18171
  createDataLayout(target, dryRun, created, alreadyExisted);
17822
- return makeResult({ ...base, success: true, skipped: true });
18172
+ const extras2 = await collectExtras(target, options, dryRun);
18173
+ return buildSuccessResult(base, { skipped: true }, extras2);
17823
18174
  }
17824
18175
  if (state.nonEmpty && !state.isExistingNexusDir && !force) {
17825
18176
  const error = `target ${target} already exists and is not empty; pass --force to use anyway`;
@@ -17827,37 +18178,83 @@ function initPortable(options = {}) {
17827
18178
  }
17828
18179
  createDataLayout(target, dryRun, created, alreadyExisted);
17829
18180
  const gitignoreUpdated = applyGitignoreOption(target, options, dryRun);
17830
- return makeResult({ ...base, success: true, gitignoreUpdated });
18181
+ const extras = await collectExtras(target, options, dryRun);
18182
+ return buildSuccessResult(base, { gitignoreUpdated }, extras);
17831
18183
  } catch (error) {
17832
18184
  const msg = error instanceof Error ? error.message : String(error);
17833
18185
  return makeResult({ ...base, success: false, error: msg });
17834
18186
  }
17835
18187
  }
18188
+ function renderMcpConfigLines(mcpConfig) {
18189
+ const lines = [];
18190
+ if (mcpConfig.alreadyMatched) {
18191
+ lines.push(`\u2713 .mcp.json already up to date: ${mcpConfig.mcpConfigPath}`);
18192
+ } else if (mcpConfig.written) {
18193
+ lines.push(`\u2713 Wrote MCP config: ${mcpConfig.mcpConfigPath}`);
18194
+ }
18195
+ if (mcpConfig.gitignoreUpdated) {
18196
+ lines.push(`\u2713 Added .mcp.json to .gitignore (per-machine; do not commit)`);
18197
+ }
18198
+ return lines;
18199
+ }
18200
+ function renderMcpConfigCaveat(mcpConfig) {
18201
+ if (mcpConfig?.written !== true) return [];
18202
+ return [
18203
+ "",
18204
+ "Note: .mcp.json contains an absolute path to your local data dir.",
18205
+ "It is per-machine and should NOT be committed \u2014 collaborators should",
18206
+ "run `nexus-agents init --portable --mcp-config` themselves."
18207
+ ];
18208
+ }
18209
+ function renderInstallLines(install) {
18210
+ if (install.skipped) return [`\u2713 Portable install already present (${install.version})`];
18211
+ return [
18212
+ `\u2713 Installed nexus-agents@${install.version} \u2192 ${install.cliDir}`,
18213
+ `\u2713 Wrote bin shim \u2192 ${install.shim?.shimPath ?? install.binDir + "/nexus-agents"}`
18214
+ ];
18215
+ }
18216
+ function renderUninstallLines(uninstall) {
18217
+ const lines = [];
18218
+ if (uninstall.removed.length === 0 && uninstall.notPresent.length > 0) {
18219
+ lines.push("Nothing to uninstall \u2014 cli/ and bin/ were not present.");
18220
+ }
18221
+ for (const r of uninstall.removed) lines.push(`\u2713 Removed: ${r}`);
18222
+ if (uninstall.removed.length > 0) {
18223
+ lines.push("");
18224
+ lines.push("Note: data subdirs (memory, audit, voting, sessions, \u2026) preserved.");
18225
+ lines.push("To purge data too, remove the parent dir manually.");
18226
+ }
18227
+ return lines;
18228
+ }
17836
18229
  function formatInitPortableMessage(result, dryRun) {
17837
18230
  if (!result.success) {
17838
18231
  return `init --portable failed: ${result.error ?? "unknown error"}
17839
18232
  `;
17840
18233
  }
17841
- const lines = [];
17842
- if (dryRun) {
17843
- lines.push(`(dry-run) would create ${String(result.created.length)} entries under:`);
17844
- lines.push(` ${result.absolutePath}`);
17845
- return lines.join("\n") + "\n";
17846
- }
17847
- if (result.skipped) {
17848
- lines.push(`\u2713 Already initialized: ${result.absolutePath}`);
17849
- } else {
17850
- lines.push(`\u2713 Created: ${result.absolutePath}`);
18234
+ if (result.uninstall !== void 0) {
18235
+ return renderUninstallLines(result.uninstall).join("\n") + "\n";
17851
18236
  }
17852
- if (result.gitignoreUpdated) {
17853
- lines.push(`\u2713 Added entry to .gitignore`);
18237
+ if (dryRun) {
18238
+ const lines2 = [
18239
+ `(dry-run) would create ${String(result.created.length)} entries under:`,
18240
+ ` ${result.absolutePath}`
18241
+ ];
18242
+ return lines2.join("\n") + "\n";
17854
18243
  }
18244
+ const lines = [];
18245
+ lines.push(
18246
+ result.skipped ? `\u2713 Already initialized: ${result.absolutePath}` : `\u2713 Created: ${result.absolutePath}`
18247
+ );
18248
+ if (result.gitignoreUpdated) lines.push(`\u2713 Added entry to .gitignore`);
18249
+ if (result.install !== void 0) lines.push(...renderInstallLines(result.install));
18250
+ if (result.mcpConfig !== void 0) lines.push(...renderMcpConfigLines(result.mcpConfig));
17855
18251
  lines.push("");
17856
18252
  lines.push("Activate by exporting:");
17857
18253
  lines.push(` export NEXUS_DATA_DIR=${result.absolutePath}`);
17858
18254
  lines.push("");
17859
18255
  lines.push("Or one-off:");
17860
18256
  lines.push(` NEXUS_DATA_DIR=${result.absolutePath} nexus-agents <cmd>`);
18257
+ lines.push(...renderMcpConfigCaveat(result.mcpConfig));
17861
18258
  return lines.join("\n") + "\n";
17862
18259
  }
17863
18260
 
@@ -20672,8 +21069,8 @@ function printFirstRunHint() {
20672
21069
  const isTTY = process.stderr.isTTY;
20673
21070
  if (!isTTY) return;
20674
21071
  const dataDir = getNexusDataDir();
20675
- const hasConfig = existsSync19("./nexus-agents.yaml") || existsSync19("./nexus-agents.yml");
20676
- if (existsSync19(dataDir) || hasConfig) return;
21072
+ const hasConfig = existsSync22("./nexus-agents.yaml") || existsSync22("./nexus-agents.yml");
21073
+ if (existsSync22(dataDir) || hasConfig) return;
20677
21074
  process.stderr.write(
20678
21075
  "\n\x1B[36mnexus-agents\x1B[0m: First time? Run \x1B[1mnexus-agents setup\x1B[0m to configure.\n\n"
20679
21076
  );
@@ -20839,19 +21236,29 @@ async function handleDoctorCommand(args) {
20839
21236
  }
20840
21237
  process.exit(exitCode === 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.SERVER_START_FAILED);
20841
21238
  }
20842
- function handleInitCommand(args) {
21239
+ function validateInitFlags(args) {
20843
21240
  if (args.options.portable !== true) {
20844
21241
  process.stderr.write(
20845
- "Usage: nexus-agents init --portable [path] [--force] [--dry-run] [--gitignore]\nBootstraps a workspace-local nexus-agents data directory.\n"
21242
+ "Usage: nexus-agents init --portable [path] [--force] [--dry-run]\n [--gitignore] [--mcp-config]\n [--install | --uninstall]\nBootstraps a workspace-local nexus-agents data directory.\n"
20846
21243
  );
20847
21244
  process.exit(EXIT_CODES.INVALID_ARGS);
20848
21245
  }
21246
+ if (args.options.install === true && args.options.uninstall === true) {
21247
+ process.stderr.write("Error: --install and --uninstall are mutually exclusive.\n");
21248
+ process.exit(EXIT_CODES.INVALID_ARGS);
21249
+ }
21250
+ }
21251
+ async function handleInitCommand(args) {
21252
+ validateInitFlags(args);
20849
21253
  const targetPath = args.positionals[1];
20850
- const result = initPortable({
21254
+ const result = await initPortable({
20851
21255
  ...targetPath !== void 0 && targetPath !== "" ? { path: targetPath } : {},
20852
21256
  force: args.options.force,
20853
21257
  dryRun: args.options.dryRun,
20854
- gitignore: args.options.gitignore ?? false
21258
+ gitignore: args.options.gitignore ?? false,
21259
+ mcpConfig: args.options.mcpConfig ?? false,
21260
+ install: args.options.install ?? false,
21261
+ uninstall: args.options.uninstall ?? false
20855
21262
  });
20856
21263
  process.stdout.write(formatInitPortableMessage(result, args.options.dryRun));
20857
21264
  process.exit(result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.SERVER_START_FAILED);
@@ -22329,7 +22736,7 @@ function handleStatusCommand2(args) {
22329
22736
 
22330
22737
  // src/cli/scenario-command.ts
22331
22738
  import { readdir as readdir4 } from "fs/promises";
22332
- import { join as join15, resolve as resolve14 } from "path";
22739
+ import { join as join18, resolve as resolve14 } from "path";
22333
22740
 
22334
22741
  // src/testing/e2e/scenario-runner.ts
22335
22742
  import { readFile as readFile7 } from "fs/promises";
@@ -22699,7 +23106,7 @@ async function handleRun(args) {
22699
23106
  process.exit(EXIT_CODES.SERVER_START_FAILED);
22700
23107
  }
22701
23108
  const runner = createScenarioRunner();
22702
- const fixturePath = join15(FIXTURES_DIR, `${name}${SCENARIO_SUFFIX}`);
23109
+ const fixturePath = join18(FIXTURES_DIR, `${name}${SCENARIO_SUFFIX}`);
22703
23110
  try {
22704
23111
  const fixture = await runner.loadFixture(fixturePath);
22705
23112
  const result = await runner.run(fixture);
@@ -22962,8 +23369,6 @@ var SYNC_COMMAND_HANDLERS = {
22962
23369
  status: handleStatusCommand2,
22963
23370
  // Issue #1023: Warm-Up Command
22964
23371
  "warm-up": handleWarmUpCommand,
22965
- // #2305: Init Portable Command
22966
- init: handleInitCommand,
22967
23372
  "e2e-eval": handleE2EEvalCommand,
22968
23373
  "routing-ab": handleRoutingABCommand,
22969
23374
  "memory-eval": handleMemoryEvalCommand,
@@ -23003,6 +23408,8 @@ var ASYNC_COMMAND_HANDLERS = {
23003
23408
  hooks: handleHooksCommand,
23004
23409
  setup: handleSetupCommandAsync,
23005
23410
  // Uses async for interactive wizard support (Issue #425)
23411
+ // #2305 / #2308 / #2311: Init Portable Command (async because --install spawns npm)
23412
+ init: handleInitCommand,
23006
23413
  demo: handleDemoCommand,
23007
23414
  // Made async for live CLI execution
23008
23415
  // Issue #526: Newly wired async commands
@@ -23410,7 +23817,17 @@ function buildOptions(values) {
23410
23817
  ...buildSweBenchOptions(values),
23411
23818
  ...buildAtbenchOptions(values),
23412
23819
  ...buildLearningMetricsOptions(values),
23413
- ...buildSetupOptions(values)
23820
+ ...buildSetupOptions(values),
23821
+ ...buildInitOptions(values)
23822
+ };
23823
+ }
23824
+ function buildInitOptions(values) {
23825
+ return {
23826
+ portable: values.portable,
23827
+ gitignore: values.gitignore,
23828
+ mcpConfig: values["mcp-config"],
23829
+ install: values.install,
23830
+ uninstall: values.uninstall
23414
23831
  };
23415
23832
  }
23416
23833
  function parseCliArgs(args = process.argv.slice(2)) {