@jentrix/cli 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +523 -51
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -6418,7 +6418,7 @@ var require_compile = __commonJS({
6418
6418
  const schOrFunc = root.refs[ref];
6419
6419
  if (schOrFunc)
6420
6420
  return schOrFunc;
6421
- let _sch = resolve4.call(this, root, ref);
6421
+ let _sch = resolve5.call(this, root, ref);
6422
6422
  if (_sch === void 0) {
6423
6423
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
6424
6424
  const { schemaId } = this.opts;
@@ -6445,7 +6445,7 @@ var require_compile = __commonJS({
6445
6445
  function sameSchemaEnv(s1, s2) {
6446
6446
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
6447
6447
  }
6448
- function resolve4(root, ref) {
6448
+ function resolve5(root, ref) {
6449
6449
  let sch;
6450
6450
  while (typeof (sch = this.refs[ref]) == "string")
6451
6451
  ref = sch;
@@ -7076,7 +7076,7 @@ var require_fast_uri = __commonJS({
7076
7076
  }
7077
7077
  return uri;
7078
7078
  }
7079
- function resolve4(baseURI, relativeURI, options) {
7079
+ function resolve5(baseURI, relativeURI, options) {
7080
7080
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
7081
7081
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
7082
7082
  schemelessOptions.skipEscape = true;
@@ -7334,7 +7334,7 @@ var require_fast_uri = __commonJS({
7334
7334
  var fastUri = {
7335
7335
  SCHEMES,
7336
7336
  normalize,
7337
- resolve: resolve4,
7337
+ resolve: resolve5,
7338
7338
  resolveComponent,
7339
7339
  equal,
7340
7340
  serialize,
@@ -10327,14 +10327,17 @@ var require_dist = __commonJS({
10327
10327
  import { spawn as spawn3 } from "node:child_process";
10328
10328
  import { createInterface } from "node:readline";
10329
10329
  import {
10330
+ appendFileSync,
10330
10331
  closeSync as closeSync3,
10332
+ copyFileSync,
10331
10333
  existsSync as existsSync6,
10334
+ mkdirSync as mkdirSync4,
10332
10335
  openSync as openSync3,
10333
10336
  readFileSync as readFileSync7,
10334
10337
  statSync as statSync3
10335
10338
  } from "node:fs";
10336
10339
  import { homedir } from "node:os";
10337
- import { basename as basename2, join as join6 } from "node:path";
10340
+ import { basename as basename2, delimiter as delimiter2, dirname as dirname4, join as join6, resolve as resolve4 } from "node:path";
10338
10341
  import { fileURLToPath } from "node:url";
10339
10342
 
10340
10343
  // node_modules/.pnpm/commander@14.0.3/node_modules/commander/esm.mjs
@@ -17870,7 +17873,7 @@ var Protocol = class {
17870
17873
  return;
17871
17874
  }
17872
17875
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
17873
- await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
17876
+ await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
17874
17877
  options?.signal?.throwIfAborted();
17875
17878
  }
17876
17879
  } catch (error2) {
@@ -17887,7 +17890,7 @@ var Protocol = class {
17887
17890
  */
17888
17891
  request(request, resultSchema, options) {
17889
17892
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
17890
- return new Promise((resolve4, reject) => {
17893
+ return new Promise((resolve5, reject) => {
17891
17894
  const earlyReject = (error2) => {
17892
17895
  reject(error2);
17893
17896
  };
@@ -17965,7 +17968,7 @@ var Protocol = class {
17965
17968
  if (!parseResult.success) {
17966
17969
  reject(parseResult.error);
17967
17970
  } else {
17968
- resolve4(parseResult.data);
17971
+ resolve5(parseResult.data);
17969
17972
  }
17970
17973
  } catch (error2) {
17971
17974
  reject(error2);
@@ -18226,12 +18229,12 @@ var Protocol = class {
18226
18229
  }
18227
18230
  } catch {
18228
18231
  }
18229
- return new Promise((resolve4, reject) => {
18232
+ return new Promise((resolve5, reject) => {
18230
18233
  if (signal.aborted) {
18231
18234
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
18232
18235
  return;
18233
18236
  }
18234
- const timeoutId = setTimeout(resolve4, interval);
18237
+ const timeoutId = setTimeout(resolve5, interval);
18235
18238
  signal.addEventListener("abort", () => {
18236
18239
  clearTimeout(timeoutId);
18237
18240
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -20959,7 +20962,7 @@ function refreshAccessToken(input) {
20959
20962
 
20960
20963
  // src/client.ts
20961
20964
  var CLI_NAME = "stacks-cli";
20962
- var CLI_VERSION = "0.5.5";
20965
+ var CLI_VERSION = "0.5.7";
20963
20966
  var DEAD_TOKEN_MESSAGE = "authentication failed (HTTP 401): the token is dead or expired \u2014 mint a new PAT at /account/tokens on your Jentrix server and update STACKS_TOKEN (or --token / the config file).";
20964
20967
  var RELOGIN_MESSAGE = "OAuth session expired and could not be refreshed \u2014 run `jentrix login` to sign in again.";
20965
20968
  function originOf2(url2) {
@@ -21095,6 +21098,26 @@ function readOAuthRecord(configPath, reader) {
21095
21098
  }
21096
21099
  }
21097
21100
 
21101
+ // src/exec-target.ts
21102
+ var INTERPRETED = /\.(cmd|bat)$/i;
21103
+ function quoteForCmd(token) {
21104
+ return `"${token.replace(/"/g, '""')}"`;
21105
+ }
21106
+ function execTarget(file, args, platform2 = process.platform, comSpec = process.env.ComSpec) {
21107
+ if (platform2 !== "win32" || !INTERPRETED.test(file)) {
21108
+ return { file, args: [...args], options: {} };
21109
+ }
21110
+ const line = [file, ...args].map(quoteForCmd).join(" ");
21111
+ return {
21112
+ file: comSpec || "cmd.exe",
21113
+ // `/d` skips any AutoRun command the machine has registered; `/s` makes cmd
21114
+ // strip exactly the outer quote pair and take the rest verbatim, which is
21115
+ // the only form that survives a quoted program path.
21116
+ args: ["/d", "/s", "/c", `"${line}"`],
21117
+ options: { windowsVerbatimArguments: true }
21118
+ };
21119
+ }
21120
+
21098
21121
  // src/loopback.ts
21099
21122
  import { createServer } from "node:http";
21100
21123
  import { spawn } from "node:child_process";
@@ -21107,7 +21130,7 @@ function escapeHtml(s) {
21107
21130
  );
21108
21131
  }
21109
21132
  function tryBindPort(port, timeoutMs = CALLBACK_TIMEOUT_MS) {
21110
- return new Promise((resolve4) => {
21133
+ return new Promise((resolve5) => {
21111
21134
  let settleCode = null;
21112
21135
  let rejectCode = null;
21113
21136
  let timer = null;
@@ -21132,9 +21155,9 @@ function tryBindPort(port, timeoutMs = CALLBACK_TIMEOUT_MS) {
21132
21155
  rejectCode?.(new Error("callback had no authorization code"));
21133
21156
  else settleCode?.({ code, state: url2.searchParams.get("state") });
21134
21157
  });
21135
- server.once("error", () => resolve4(null));
21158
+ server.once("error", () => resolve5(null));
21136
21159
  server.listen(port, LOOPBACK_HOST, () => {
21137
- resolve4({
21160
+ resolve5({
21138
21161
  port,
21139
21162
  waitForCode: () => new Promise((res, rej) => {
21140
21163
  settleCode = res;
@@ -21161,17 +21184,17 @@ async function bindLoopback(ports, timeoutMs = CALLBACK_TIMEOUT_MS) {
21161
21184
  return null;
21162
21185
  }
21163
21186
  function openBrowser(url2) {
21164
- return new Promise((resolve4) => {
21187
+ return new Promise((resolve5) => {
21165
21188
  const os = platform();
21166
21189
  const cmd = os === "darwin" ? "open" : os === "win32" ? "cmd" : "xdg-open";
21167
21190
  const args = os === "win32" ? ["/c", "start", "", url2] : [url2];
21168
21191
  try {
21169
21192
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
21170
- child.on("error", () => resolve4());
21193
+ child.on("error", () => resolve5());
21171
21194
  child.unref();
21172
21195
  } catch {
21173
21196
  }
21174
- resolve4();
21197
+ resolve5();
21175
21198
  });
21176
21199
  }
21177
21200
 
@@ -21724,13 +21747,13 @@ function normalizeRemoteRepo(url2) {
21724
21747
  if (trimmed.length < 2) return null;
21725
21748
  return `${trimmed[0]}/${trimmed[1]}`.toLowerCase();
21726
21749
  }
21727
- var defaultGitRunner = (args, cwd) => new Promise((resolve4) => {
21750
+ var defaultGitRunner = (args, cwd) => new Promise((resolve5) => {
21728
21751
  execFile(
21729
21752
  "git",
21730
21753
  args,
21731
21754
  { cwd, timeout: 1e4, maxBuffer: 4 * 1024 * 1024, windowsHide: true },
21732
21755
  (error2, stdout) => {
21733
- resolve4({
21756
+ resolve5({
21734
21757
  code: error2 === null ? 0 : typeof error2.code === "number" ? Number(error2.code) : 1,
21735
21758
  stdout: String(stdout)
21736
21759
  });
@@ -27191,6 +27214,11 @@ function relayFailure(deps2, step, result) {
27191
27214
  );
27192
27215
  return EXIT_CODES.INTERNAL;
27193
27216
  }
27217
+ function persistentPluginRoot(globalNodeModules, ownRoot, exists) {
27218
+ if (!globalNodeModules) return ownRoot;
27219
+ const global = join4(globalNodeModules, "@jentrix", "cli");
27220
+ return exists(global) ? global : ownRoot;
27221
+ }
27194
27222
  function isPluginMarketplaceDir(dir) {
27195
27223
  return existsSync4(join4(dir, ".claude-plugin", "marketplace.json"));
27196
27224
  }
@@ -27468,22 +27496,33 @@ function invokeRunnerProcess(file, args, stdin, timeoutMs = RUNNER_TIMEOUT_MS) {
27468
27496
  if (!isAbsolute(file)) {
27469
27497
  return Promise.reject(new Error("runner executable path must be absolute"));
27470
27498
  }
27499
+ const target = execTarget(file, args);
27471
27500
  return new Promise((resolveInvocation, reject) => {
27472
27501
  const child = execFile2(
27473
- file,
27474
- args,
27502
+ target.file,
27503
+ target.args,
27475
27504
  {
27476
27505
  timeout: timeoutMs,
27477
27506
  maxBuffer: RUNNER_OUTPUT_CAP,
27478
- windowsHide: true
27507
+ windowsHide: true,
27508
+ ...target.options
27479
27509
  },
27480
27510
  (error2, stdout, stderr) => {
27481
27511
  if (error2 && error2.killed) {
27482
27512
  reject(new Error("runner subprocess timed out"));
27483
27513
  return;
27484
27514
  }
27515
+ const raw = error2?.code;
27516
+ if (error2 && typeof raw === "string") {
27517
+ resolveInvocation({
27518
+ code: 1,
27519
+ stdout: String(stdout),
27520
+ stderr: `${String(stderr)}${raw}: ${error2.message}`
27521
+ });
27522
+ return;
27523
+ }
27485
27524
  resolveInvocation({
27486
- code: typeof error2?.code === "number" ? Number(error2.code) : 0,
27525
+ code: typeof raw === "number" ? Number(raw) : 0,
27487
27526
  stdout: String(stdout),
27488
27527
  stderr: String(stderr)
27489
27528
  });
@@ -27804,8 +27843,12 @@ function runRunnerForeground(file, args) {
27804
27843
  if (!isAbsolute(file)) {
27805
27844
  return Promise.reject(new Error("runner executable path must be absolute"));
27806
27845
  }
27846
+ const target = execTarget(file, args);
27807
27847
  return new Promise((resolveRun, reject) => {
27808
- const child = spawn2(file, args, { stdio: "inherit" });
27848
+ const child = spawn2(target.file, target.args, {
27849
+ stdio: "inherit",
27850
+ ...target.options
27851
+ });
27809
27852
  child.once("error", reject);
27810
27853
  child.once("exit", (code, signal) => resolveRun(code ?? (signal ? 1 : 0)));
27811
27854
  });
@@ -27852,6 +27895,344 @@ function registerRunnerCommand(program3, deps2, onExit2) {
27852
27895
  );
27853
27896
  }
27854
27897
 
27898
+ // src/commands/setup.ts
27899
+ var NPM_INSTALL_TIMEOUT_MS2 = 3e5;
27900
+ var SUPERSEDED = ["@jentrix/stacks-cli", "@jentrix/stacks-runner"];
27901
+ var CLI_SPEC = `@jentrix/cli@${CLI_VERSION}`;
27902
+ var RUNNER_SPEC2 = `@jentrix/runner@${CLI_VERSION}`;
27903
+ function say(deps2, text = "") {
27904
+ deps2.writeOut(text);
27905
+ }
27906
+ async function installToolchain(deps2) {
27907
+ const npm = await deps2.resolveNpm();
27908
+ let global = await deps2.globalInstall();
27909
+ if (global.cliDir) {
27910
+ say(
27911
+ deps2,
27912
+ `jentrix ${CLI_VERSION} is already installed (${global.cliDir}).`
27913
+ );
27914
+ say(deps2, " Upgrade it any time with: npm update -g @jentrix/cli");
27915
+ } else {
27916
+ if (!npm) {
27917
+ deps2.writeErr(
27918
+ "NPM_NOT_FOUND: the toolchain installs from npm and `npm` is not on PATH \u2014 install Node.js >= 20 (https://nodejs.org), then retry."
27919
+ );
27920
+ return EXIT_CODES.INVALID_INPUT;
27921
+ }
27922
+ await deps2.invoke(npm, ["rm", "-g", ...SUPERSEDED], void 0, 6e4);
27923
+ say(deps2, `Installing ${CLI_SPEC} + ${RUNNER_SPEC2} (npm -g)\u2026`);
27924
+ const installed = await deps2.invoke(
27925
+ npm,
27926
+ ["install", "-g", CLI_SPEC, RUNNER_SPEC2],
27927
+ void 0,
27928
+ NPM_INSTALL_TIMEOUT_MS2
27929
+ );
27930
+ if (installed.code !== 0) {
27931
+ const detail = (installed.stderr || installed.stdout).trim();
27932
+ deps2.writeErr(
27933
+ `TOOLCHAIN_INSTALL_FAILED: \`npm install -g\` exited ${installed.code}${detail ? `: ${detail}` : ""}`
27934
+ );
27935
+ deps2.writeErr(
27936
+ " On EACCES, fix npm's global prefix (https://docs.npmjs.com/resolving-eacces-permissions-errors); on EEXIST, delete the bin npm names and retry."
27937
+ );
27938
+ return EXIT_CODES.INTERNAL;
27939
+ }
27940
+ global = await deps2.globalInstall();
27941
+ if (!global.cliDir) {
27942
+ deps2.writeErr(
27943
+ "TOOLCHAIN_INSTALL_FAILED: npm reported success but @jentrix/cli is not under `npm root -g` \u2014 check npm's global prefix, then retry."
27944
+ );
27945
+ return EXIT_CODES.INTERNAL;
27946
+ }
27947
+ say(deps2, "Installed the CLI and the session runner.");
27948
+ }
27949
+ if (!global.runnerDir) {
27950
+ if (!npm) {
27951
+ deps2.writeErr(
27952
+ `RUNNER_NOT_INSTALLED: connected sessions need the runner and npm was not found to install it \u2014 npm install -g ${RUNNER_SPEC2}, then retry.`
27953
+ );
27954
+ return EXIT_CODES.INVALID_INPUT;
27955
+ }
27956
+ say(deps2, `Installing ${RUNNER_SPEC2} (connected sessions need it)\u2026`);
27957
+ const runner = await deps2.invoke(
27958
+ npm,
27959
+ ["install", "-g", RUNNER_SPEC2],
27960
+ void 0,
27961
+ NPM_INSTALL_TIMEOUT_MS2
27962
+ );
27963
+ global = await deps2.globalInstall();
27964
+ if (runner.code !== 0 || !global.runnerDir) {
27965
+ deps2.writeErr(
27966
+ `RUNNER_NOT_INSTALLED: \`npm install -g ${RUNNER_SPEC2}\` did not leave the runner under \`npm root -g\` \u2014 install it by hand, then retry.`
27967
+ );
27968
+ return EXIT_CODES.INTERNAL;
27969
+ }
27970
+ }
27971
+ if (global.binDir && !global.binDirOnPath) {
27972
+ say(deps2, "");
27973
+ say(deps2, "note: npm's global bin directory is NOT on your PATH \u2014");
27974
+ say(deps2, ` ${global.binDir}`);
27975
+ say(
27976
+ deps2,
27977
+ " The toolchain is installed, but `jentrix` will not be found in a new"
27978
+ );
27979
+ say(
27980
+ deps2,
27981
+ " terminal until you add that directory to PATH and reopen your shell."
27982
+ );
27983
+ }
27984
+ return EXIT_CODES.OK;
27985
+ }
27986
+ var RUNTIMES = [
27987
+ {
27988
+ provider: "claude",
27989
+ resolve: (deps2) => deps2.resolveClaude(),
27990
+ absent: [
27991
+ "note: the `claude` CLI is not installed \u2014 skipped its plugin.",
27992
+ " Install Claude Code (https://claude.com/claude-code), then run: jentrix plugin install"
27993
+ ]
27994
+ },
27995
+ {
27996
+ provider: "codex",
27997
+ resolve: (deps2) => deps2.resolveCodex(),
27998
+ absent: [
27999
+ "note: the `codex` CLI is not installed \u2014 skipped its plugin and MCP server.",
28000
+ " Install Codex (https://developers.openai.com/codex/cli), then re-run this command."
28001
+ ]
28002
+ }
28003
+ ];
28004
+ async function installRuntimes(deps2, pending) {
28005
+ for (const runtime of RUNTIMES) {
28006
+ if (!await runtime.resolve(deps2)) {
28007
+ for (const line of runtime.absent) say(deps2, line);
28008
+ continue;
28009
+ }
28010
+ if (await deps2.installPlugin(runtime.provider) !== EXIT_CODES.OK) {
28011
+ pending.failedPlugins.push(runtime.provider);
28012
+ }
28013
+ }
28014
+ }
28015
+ async function registerCodexMcp(deps2, codex, pending) {
28016
+ const configPath = deps2.codexConfigPath();
28017
+ const config2 = deps2.readTextFile(configPath);
28018
+ const existingKey = config2 ? /^\[mcp_servers\.jentrix\]/m.test(config2) ? "jentrix" : /^\[mcp_servers\.stacks\]/m.test(config2) ? "stacks" : null : null;
28019
+ if (existingKey) {
28020
+ say(
28021
+ deps2,
28022
+ `note: Codex already has an [mcp_servers.${existingKey}] entry \u2014 left untouched.`
28023
+ );
28024
+ say(
28025
+ deps2,
28026
+ ` It may point at a different deployment; check ${configPath}.`
28027
+ );
28028
+ say(
28029
+ deps2,
28030
+ ` To move it off an exported STACKS_CODEX_TOKEN and onto sign-in:`
28031
+ );
28032
+ say(
28033
+ deps2,
28034
+ ` \`codex mcp remove ${existingKey}\`, then re-run this command.`
28035
+ );
28036
+ return;
28037
+ }
28038
+ const url2 = deps2.signedInUrl();
28039
+ if (!url2) {
28040
+ say(
28041
+ deps2,
28042
+ "note: skipped the Codex MCP entry \u2014 this machine is not signed in yet, so"
28043
+ );
28044
+ say(
28045
+ deps2,
28046
+ " there is no endpoint to register. Run `jentrix login`, then re-run this command."
28047
+ );
28048
+ return;
28049
+ }
28050
+ const oauth = (await deps2.invoke(codex, ["mcp", "login", "--help"])).code === 0;
28051
+ if (config2 !== null) {
28052
+ const stamp = deps2.now().toISOString().replace(/[-:T]/g, "").slice(0, 14);
28053
+ deps2.copyFile(configPath, `${configPath}.bak-jentrix-${stamp}`);
28054
+ }
28055
+ const lines = ["", "# Added by the Jentrix installer."];
28056
+ lines.push(
28057
+ oauth ? "# Authenticated by `codex mcp login jentrix` \u2014 no token to export." : "# Needs STACKS_CODEX_TOKEN in the environment (a personal access token, scopes read + write)."
28058
+ );
28059
+ lines.push("[mcp_servers.jentrix]", `url = "${url2}"`);
28060
+ if (!oauth) lines.push('bearer_token_env_var = "STACKS_CODEX_TOKEN"');
28061
+ deps2.appendTextFile(configPath, `${lines.join("\n")}
28062
+ `);
28063
+ say(deps2, `Codex: registered [mcp_servers.jentrix] \u2192 ${url2}`);
28064
+ if (!oauth) {
28065
+ say(
28066
+ deps2,
28067
+ " (this Codex build has no `codex mcp login`, so the entry reads a token"
28068
+ );
28069
+ say(deps2, " from the environment)");
28070
+ pending.codexTokenUrl = `${url2.replace(/\/api\/mcp$/, "")}/account/tokens`;
28071
+ return;
28072
+ }
28073
+ if (!deps2.isInteractive) {
28074
+ pending.codexLoginUrl = url2;
28075
+ return;
28076
+ }
28077
+ say(
28078
+ deps2,
28079
+ "Signing Codex in \u2014 approve the Jentrix consent screen in your browser\u2026"
28080
+ );
28081
+ if ((await deps2.invoke(codex, ["mcp", "login", "jentrix"])).code !== 0) {
28082
+ pending.codexLoginUrl = url2;
28083
+ }
28084
+ }
28085
+ async function bootstrapRepository(deps2) {
28086
+ const here = deps2.cwd();
28087
+ if (!await deps2.resolveGit()) {
28088
+ say(deps2, "note: `git` is not installed \u2014 skipped the repository step.");
28089
+ say(
28090
+ deps2,
28091
+ " Jentrix aligns sessions by repository identity, so install git (https://git-scm.com/downloads) and re-run."
28092
+ );
28093
+ return;
28094
+ }
28095
+ const inRepo = async () => (await deps2.git(["rev-parse", "--is-inside-work-tree"], here)).code === 0;
28096
+ if (!await inRepo()) {
28097
+ const reply = await deps2.readLine(
28098
+ `Initialize a git repository in ${here} so agent sessions can align here? [y/N] `
28099
+ );
28100
+ if (!/^y/i.test(reply.trim())) return;
28101
+ if ((await deps2.git(["init"], here)).code !== 0) {
28102
+ say(deps2, "note: `git init` failed \u2014 skipped the repository step.");
28103
+ return;
28104
+ }
28105
+ }
28106
+ if ((await deps2.git(["remote", "get-url", "origin"], here)).code === 0)
28107
+ return;
28108
+ const fallback = here.split(/[/\\]/).filter(Boolean).pop().toLowerCase().replace(/ /g, "-");
28109
+ say(
28110
+ deps2,
28111
+ "Jentrix aligns sessions by repository identity (owner/name); the remote doesn't have to exist yet."
28112
+ );
28113
+ say(
28114
+ deps2,
28115
+ `Optional \u2014 leave it blank and this checkout aligns as local/${fallback}.`
28116
+ );
28117
+ const answer = (await deps2.readLine(
28118
+ "GitHub repo for origin (owner/name or full URL, blank to skip): "
28119
+ )).trim();
28120
+ if (!answer) return;
28121
+ const url2 = /:\/\/|^[^/]+@[^:]+:/.test(answer) ? answer : /^[^/]+\/[^/]+$/.test(answer) ? `https://github.com/${answer}.git` : null;
28122
+ if (!url2) {
28123
+ say(deps2, `skipped \u2014 "${answer}" is neither owner/name nor a URL`);
28124
+ return;
28125
+ }
28126
+ if ((await deps2.git(["remote", "add", "origin", url2], here)).code === 0) {
28127
+ say(deps2, `origin \u2192 ${url2}`);
28128
+ }
28129
+ }
28130
+ async function runSetupCommand(flags, deps2) {
28131
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
28132
+ if (Number.isFinite(nodeMajor) && nodeMajor < 20) {
28133
+ deps2.writeErr(
28134
+ `NODE_TOO_OLD: Jentrix requires Node.js >= 20 (running v${process.versions.node}) \u2014 upgrade from https://nodejs.org, then retry.`
28135
+ );
28136
+ return EXIT_CODES.INVALID_INPUT;
28137
+ }
28138
+ const toolchain = await installToolchain(deps2);
28139
+ if (toolchain !== EXIT_CODES.OK) return toolchain;
28140
+ const pending = { failedPlugins: [] };
28141
+ await installRuntimes(deps2, pending);
28142
+ if (!deps2.hasCredential()) {
28143
+ if (deps2.isInteractive) {
28144
+ say(deps2, "");
28145
+ say(deps2, "Connecting this machine to a Jentrix server\u2026");
28146
+ if (await deps2.login({ url: flags.url, local: flags.local }) !== 0) {
28147
+ say(
28148
+ deps2,
28149
+ "note: sign-in did not complete \u2014 run `jentrix login` when you're ready."
28150
+ );
28151
+ }
28152
+ } else {
28153
+ say(
28154
+ deps2,
28155
+ "note: no credential is configured \u2014 run `jentrix login` (interactive) to connect."
28156
+ );
28157
+ }
28158
+ }
28159
+ const codex = await deps2.resolveCodex();
28160
+ if (codex) await registerCodexMcp(deps2, codex, pending);
28161
+ if (flags.git !== false && deps2.isInteractive && deps2.cwd() !== deps2.homeDir()) {
28162
+ await bootstrapRepository(deps2);
28163
+ }
28164
+ say(deps2, "");
28165
+ if (pending.failedPlugins.length) {
28166
+ for (const provider of pending.failedPlugins) {
28167
+ say(
28168
+ deps2,
28169
+ `note: the ${provider} plugin did NOT install \u2014 its commands will be missing.`
28170
+ );
28171
+ say(
28172
+ deps2,
28173
+ ` Run \`jentrix plugin install ${provider}\` on its own to see why.`
28174
+ );
28175
+ }
28176
+ say(deps2, "");
28177
+ }
28178
+ say(deps2, "Done. Verify with: jentrix whoami");
28179
+ if (pending.codexLoginUrl) {
28180
+ say(deps2, "");
28181
+ say(
28182
+ deps2,
28183
+ `One step left for Codex \u2014 approve its access to ${pending.codexLoginUrl}:`
28184
+ );
28185
+ say(
28186
+ deps2,
28187
+ " codex mcp login jentrix (opens the browser; no token to copy)"
28188
+ );
28189
+ }
28190
+ if (pending.codexTokenUrl) {
28191
+ say(deps2, "");
28192
+ say(
28193
+ deps2,
28194
+ "One step left for Codex \u2014 its MCP client carries its own token:"
28195
+ );
28196
+ say(
28197
+ deps2,
28198
+ ` 1. mint a personal access token (scopes read + write) at ${pending.codexTokenUrl}`
28199
+ );
28200
+ say(
28201
+ deps2,
28202
+ " 2. export STACKS_CODEX_TOKEN in the shell you start Codex from"
28203
+ );
28204
+ say(deps2, " 3. restart Codex");
28205
+ say(
28206
+ deps2,
28207
+ " (deliberately not STACKS_TOKEN \u2014 that would also override the CLI's own"
28208
+ );
28209
+ say(deps2, " rotating login, which refreshes itself and needs no export)");
28210
+ }
28211
+ say(deps2, "");
28212
+ say(deps2, "Next: open your agent in the project folder and align \u2014");
28213
+ say(deps2, " Claude Code: /jentrix-align");
28214
+ say(deps2, " Codex: $jentrix-align");
28215
+ say(deps2, " any terminal: jentrix align");
28216
+ say(
28217
+ deps2,
28218
+ "(Bind a folder to a different server any time with: jentrix login --local)"
28219
+ );
28220
+ return pending.failedPlugins.length ? EXIT_CODES.INTERNAL : EXIT_CODES.OK;
28221
+ }
28222
+ function registerSetupCommand(program3, deps2, onExit2) {
28223
+ return program3.command("setup").description(
28224
+ "Install and connect the whole local toolchain \u2014 CLI, session runner, the plugins for whichever agent runtimes are present, browser sign-in, the Codex MCP server, and this folder's repository. The same command on Windows, macOS and Linux. Safe to re-run."
28225
+ ).option(
28226
+ "--url <url>",
28227
+ "MCP endpoint to sign in against (default: the interactive server picker)"
28228
+ ).option(
28229
+ "--local",
28230
+ "bind THIS folder to that server (./.stacks/config.json) instead of the machine-wide config"
28231
+ ).option("--no-git", "never touch the current folder's git repository").action(async (options) => {
28232
+ onExit2(await runSetupCommand(options, deps2));
28233
+ });
28234
+ }
28235
+
27855
28236
  // src/commands/snapshot.ts
27856
28237
  import {
27857
28238
  closeSync as closeSync2,
@@ -28190,12 +28571,12 @@ function surfaceFileMtime() {
28190
28571
  }
28191
28572
  }
28192
28573
  function readStdin() {
28193
- return new Promise((resolve4, reject) => {
28574
+ return new Promise((resolve5, reject) => {
28194
28575
  const chunks = [];
28195
28576
  process.stdin.on("data", (chunk) => chunks.push(chunk));
28196
28577
  process.stdin.on(
28197
28578
  "end",
28198
- () => resolve4(Buffer.concat(chunks).toString("utf8"))
28579
+ () => resolve5(Buffer.concat(chunks).toString("utf8"))
28199
28580
  );
28200
28581
  process.stdin.on("error", reject);
28201
28582
  });
@@ -28206,14 +28587,14 @@ function configFile() {
28206
28587
  return readConfigFile(CONFIG_PATH);
28207
28588
  }
28208
28589
  function readLine(prompt) {
28209
- return new Promise((resolve4) => {
28590
+ return new Promise((resolve5) => {
28210
28591
  const rl = createInterface({
28211
28592
  input: process.stdin,
28212
28593
  output: process.stdout
28213
28594
  });
28214
28595
  rl.question(prompt, (answer) => {
28215
28596
  rl.close();
28216
- resolve4(answer);
28597
+ resolve5(answer);
28217
28598
  });
28218
28599
  });
28219
28600
  }
@@ -28271,7 +28652,7 @@ var deps = {
28271
28652
  `),
28272
28653
  writeErr: (text) => process.stderr.write(`${text}
28273
28654
  `),
28274
- sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms)),
28655
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
28275
28656
  now: () => Date.now()
28276
28657
  };
28277
28658
  var loginDeps = {
@@ -28403,10 +28784,16 @@ var sessionDeps = {
28403
28784
  runSessionHost: (runnerBin, planPath) => runRunnerForeground(runnerBin, ["session-run", "--plan-file", planPath]),
28404
28785
  spawnSessionHostDetached: (runnerBin, planPath, logPath) => {
28405
28786
  const fd = openSync3(logPath, "a", 384);
28787
+ const target = execTarget(runnerBin, [
28788
+ "session-run",
28789
+ "--plan-file",
28790
+ planPath
28791
+ ]);
28406
28792
  try {
28407
- const child = spawn3(runnerBin, ["session-run", "--plan-file", planPath], {
28793
+ const child = spawn3(target.file, target.args, {
28408
28794
  detached: true,
28409
- stdio: ["ignore", fd, fd]
28795
+ stdio: ["ignore", fd, fd],
28796
+ ...target.options
28410
28797
  });
28411
28798
  child.unref();
28412
28799
  return child.pid ?? -1;
@@ -28420,36 +28807,121 @@ var sessionCommand = registerSessionCommand(program2, sessionDeps, onExit);
28420
28807
  registerSnapshotCommand(sessionCommand, sessionDeps, onExit);
28421
28808
  registerAlignCommand(program2, sessionDeps, onExit);
28422
28809
  registerPushCommand(program2, sessionDeps, onExit);
28810
+ var PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
28423
28811
  var PLUGIN_DIR = fileURLToPath(new URL("../claude-plugin", import.meta.url));
28424
28812
  var CODEX_PLUGIN_DIR = fileURLToPath(
28425
28813
  new URL("../codex-plugin", import.meta.url)
28426
28814
  );
28427
- registerPluginCommand(
28428
- program2,
28429
- {
28430
- resolvePluginDir: () => existsSync6(PLUGIN_DIR) && isPluginMarketplaceDir(PLUGIN_DIR) ? PLUGIN_DIR : null,
28431
- resolveCodexPluginDir: () => existsSync6(CODEX_PLUGIN_DIR) && isCodexPluginMarketplaceDir(CODEX_PLUGIN_DIR) ? CODEX_PLUGIN_DIR : null,
28432
- resolveClaude: () => resolveExecutableOnPath(platformExecutableNames("claude"), process.env),
28433
- resolveCodex: () => resolveExecutableOnPath(platformExecutableNames("codex"), process.env),
28434
- resolveRunner: () => resolveRunnerExecutable(process.env),
28435
- resolveNpm: () => resolveExecutableOnPath(platformExecutableNames("npm"), process.env),
28436
- invoke: invokeRunnerProcess,
28437
- writeOut: (text) => process.stdout.write(`${text}
28815
+ var pluginDeps = {
28816
+ resolvePluginDir: () => existsSync6(PLUGIN_DIR) && isPluginMarketplaceDir(PLUGIN_DIR) ? PLUGIN_DIR : null,
28817
+ resolveCodexPluginDir: () => existsSync6(CODEX_PLUGIN_DIR) && isCodexPluginMarketplaceDir(CODEX_PLUGIN_DIR) ? CODEX_PLUGIN_DIR : null,
28818
+ resolveClaude: () => resolveExecutableOnPath(platformExecutableNames("claude"), process.env),
28819
+ resolveCodex: () => resolveExecutableOnPath(platformExecutableNames("codex"), process.env),
28820
+ resolveRunner: () => resolveRunnerExecutable(process.env),
28821
+ resolveNpm: () => resolveExecutableOnPath(platformExecutableNames("npm"), process.env),
28822
+ invoke: invokeRunnerProcess,
28823
+ writeOut: (text) => process.stdout.write(`${text}
28438
28824
  `),
28439
- writeErr: (text) => process.stderr.write(`${text}
28825
+ writeErr: (text) => process.stderr.write(`${text}
28440
28826
  `),
28441
- // AGE-952: the one-liner install ends connected — same in-process login
28442
- // reuse as runner setup (never a `jentrix` subprocess).
28443
- hasCredential: () => {
28827
+ // AGE-952: the one-liner install ends connected — same in-process login
28828
+ // reuse as runner setup (never a `jentrix` subprocess).
28829
+ hasCredential: () => {
28830
+ try {
28831
+ resolveConfig({ env: process.env, file: configFile() });
28832
+ return true;
28833
+ } catch {
28834
+ return false;
28835
+ }
28836
+ },
28837
+ login: () => runLoginCommand({}, loginDeps),
28838
+ isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
28839
+ };
28840
+ registerPluginCommand(program2, pluginDeps, onExit);
28841
+ registerSetupCommand(
28842
+ program2,
28843
+ {
28844
+ ...pluginDeps,
28845
+ // npm's global prefix, read from npm itself — never a PATH probe. Under
28846
+ // `npx @jentrix/cli setup` the _npx cache is prepended to PATH and holds a
28847
+ // `jentrix` shim, so a PATH answer is always yes and always meaningless.
28848
+ globalInstall: async () => {
28849
+ const npm = await resolveExecutableOnPath(
28850
+ platformExecutableNames("npm"),
28851
+ process.env
28852
+ );
28853
+ const ask = async (args) => {
28854
+ if (!npm) return null;
28855
+ const out = (await invokeRunnerProcess(npm, args)).stdout.trim();
28856
+ return out || null;
28857
+ };
28858
+ const root = await ask(["root", "-g"]);
28859
+ const prefix = await ask(["prefix", "-g"]);
28860
+ const binDir = prefix ? process.platform === "win32" ? prefix : join6(prefix, "bin") : null;
28861
+ const dir = (name) => {
28862
+ if (!root) return null;
28863
+ const candidate = join6(root, "@jentrix", name);
28864
+ return existsSync6(join6(candidate, "package.json")) ? candidate : null;
28865
+ };
28866
+ return {
28867
+ cliDir: dir("cli"),
28868
+ runnerDir: dir("runner"),
28869
+ binDir,
28870
+ binDirOnPath: Boolean(
28871
+ binDir && (process.env.PATH ?? "").split(delimiter2).some((entry) => entry && resolve4(entry) === resolve4(binDir))
28872
+ )
28873
+ };
28874
+ },
28875
+ resolveGit: () => resolveExecutableOnPath(platformExecutableNames("git"), process.env),
28876
+ // Register the plugin from whichever copy of the package PERSISTS. Under
28877
+ // `npx @jentrix/cli setup` the running module lives in npm's _npx cache,
28878
+ // and a marketplace registered from there dangles the moment npm prunes
28879
+ // it. `npm root -g` names the copy that stays.
28880
+ installPlugin: async (provider) => {
28881
+ const npm = await resolveExecutableOnPath(
28882
+ platformExecutableNames("npm"),
28883
+ process.env
28884
+ );
28885
+ const root = npm ? (await invokeRunnerProcess(npm, ["root", "-g"])).stdout.trim() || null : null;
28886
+ const packageRoot = persistentPluginRoot(
28887
+ root,
28888
+ PACKAGE_ROOT,
28889
+ (dir) => isPluginMarketplaceDir(join6(dir, "claude-plugin"))
28890
+ );
28891
+ return runPluginInstall(
28892
+ packageRoot === PACKAGE_ROOT ? pluginDeps : {
28893
+ ...pluginDeps,
28894
+ resolvePluginDir: () => join6(packageRoot, "claude-plugin"),
28895
+ resolveCodexPluginDir: () => {
28896
+ const dir = join6(packageRoot, "codex-plugin");
28897
+ return isCodexPluginMarketplaceDir(dir) ? dir : null;
28898
+ }
28899
+ },
28900
+ provider
28901
+ );
28902
+ },
28903
+ // The endpoint a token actually resolves against — the in-process answer
28904
+ // to what install.sh had to ask for with `jentrix whoami --json`.
28905
+ signedInUrl: () => {
28444
28906
  try {
28445
- resolveConfig({ env: process.env, file: configFile() });
28446
- return true;
28907
+ return resolveConfig({ env: process.env, file: configFile() }).url;
28447
28908
  } catch {
28448
- return false;
28909
+ return null;
28449
28910
  }
28450
28911
  },
28451
- login: () => runLoginCommand({}, loginDeps),
28452
- isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
28912
+ login: (flags) => runLoginCommand(flags, loginDeps),
28913
+ codexConfigPath: () => join6(process.env.CODEX_HOME ?? join6(homedir(), ".codex"), "config.toml"),
28914
+ readTextFile: (path) => existsSync6(path) ? readFileSync7(path, "utf8") : null,
28915
+ appendTextFile: (path, text) => {
28916
+ mkdirSync4(dirname4(path), { recursive: true });
28917
+ appendFileSync(path, text);
28918
+ },
28919
+ copyFile: (from, to) => copyFileSync(from, to),
28920
+ now: () => /* @__PURE__ */ new Date(),
28921
+ cwd: () => process.cwd(),
28922
+ homeDir: () => homedir(),
28923
+ git: defaultGitRunner,
28924
+ readLine
28453
28925
  },
28454
28926
  onExit
28455
28927
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jentrix/cli",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Command-line client for the Jentrix MCP surface (jentrix tool <name>, generated noun-verb commands).",
5
5
  "keywords": [
6
6
  "jentrix",