@sellable/install 0.1.216 → 0.1.218

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.
@@ -13,6 +13,7 @@ import {
13
13
  import { homedir } from "node:os";
14
14
  import { dirname, join, relative } from "node:path";
15
15
  import { stdout as output } from "node:process";
16
+ import { createInterface } from "node:readline/promises";
16
17
  import { fileURLToPath } from "node:url";
17
18
  import {
18
19
  REQUIRED_SELLABLE_MCP_TOOLS,
@@ -22,6 +23,7 @@ import {
22
23
  const DEFAULT_API_URL = "https://app.sellable.dev";
23
24
  const DEFAULT_SERVER_PACKAGE =
24
25
  process.env.SELLABLE_MCP_PACKAGE || "@sellable/mcp@latest";
26
+ const CODEX_INSTALL_URL = "https://chatgpt.com/codex/install";
25
27
 
26
28
  function getInstallVersion() {
27
29
  try {
@@ -175,6 +177,8 @@ Options:
175
177
  --hosted-url <url> Hosted MCP URL for --server hosted.
176
178
  --verbose Print every file write and shell command.
177
179
  --dry-run Print actions without writing or running host commands.
180
+ --install-codex-cli Install Codex CLI without prompting if it is missing or blocked.
181
+ --no-install-codex-cli Do not prompt to install Codex CLI; install Desktop config only when possible.
178
182
  --verify-only Verify installed host config where possible.
179
183
  --json Print machine-readable verification JSON.
180
184
  --artifact <path> Write verification JSON to a file.
@@ -247,6 +251,7 @@ function parseArgs(argv) {
247
251
  json: false,
248
252
  artifactPath: process.env.SELLABLE_VERIFY_ARTIFACT || "",
249
253
  verbose: false,
254
+ codexCliInstall: codexCliInstallPreference(),
250
255
  };
251
256
 
252
257
  for (let i = 0; i < argv.length; i += 1) {
@@ -280,6 +285,10 @@ function parseArgs(argv) {
280
285
  opts.hostedUrl = next();
281
286
  } else if (arg === "--dry-run") {
282
287
  opts.dryRun = true;
288
+ } else if (arg === "--install-codex-cli") {
289
+ opts.codexCliInstall = "install";
290
+ } else if (arg === "--no-install-codex-cli") {
291
+ opts.codexCliInstall = "skip";
283
292
  } else if (arg === "--verify-only") {
284
293
  opts.verifyOnly = true;
285
294
  } else if (arg === "--json") {
@@ -303,6 +312,14 @@ function parseArgs(argv) {
303
312
  return opts;
304
313
  }
305
314
 
315
+ function codexCliInstallPreference() {
316
+ const raw = process.env.SELLABLE_INSTALL_CODEX_CLI || "";
317
+ const value = raw.trim().toLowerCase();
318
+ if (["1", "true", "yes", "y", "install"].includes(value)) return "install";
319
+ if (["0", "false", "no", "n", "skip"].includes(value)) return "skip";
320
+ return "ask";
321
+ }
322
+
306
323
  function redact(value) {
307
324
  if (!value) return "";
308
325
  if (value.length <= 10) return "[redacted]";
@@ -360,6 +377,175 @@ function commandExistsDetails(command, platform = process.platform) {
360
377
  };
361
378
  }
362
379
 
380
+ function summarizeSpawnFailure(result) {
381
+ const errorMessage = result.error?.message || "";
382
+ const stderr = (result.stderr || "").trim();
383
+ const stdout = (result.stdout || "").trim();
384
+ return (
385
+ errorMessage ||
386
+ stderr ||
387
+ stdout ||
388
+ (Number.isInteger(result.status) ? `exit ${result.status}` : "unknown failure")
389
+ );
390
+ }
391
+
392
+ function codexCliStatus(opts = {}) {
393
+ const details = commandExistsDetails("codex");
394
+ if (!details.exists) {
395
+ return {
396
+ exists: false,
397
+ usable: false,
398
+ reason: "Codex CLI not found on PATH",
399
+ details,
400
+ };
401
+ }
402
+ if (opts.dryRun) {
403
+ return {
404
+ exists: true,
405
+ usable: true,
406
+ reason: "dry-run",
407
+ details,
408
+ };
409
+ }
410
+
411
+ const result = spawnSync("codex", ["mcp", "--help"], {
412
+ encoding: "utf8",
413
+ stdio: "pipe",
414
+ timeout: 10000,
415
+ });
416
+ if (result.status === 0) {
417
+ return {
418
+ exists: true,
419
+ usable: true,
420
+ reason: "Codex CLI responded to `codex mcp --help`",
421
+ details,
422
+ };
423
+ }
424
+
425
+ return {
426
+ exists: true,
427
+ usable: false,
428
+ reason: `Codex CLI found but not usable: ${summarizeSpawnFailure(result)}`,
429
+ details,
430
+ };
431
+ }
432
+
433
+ function codexDesktopLikelyInstalled(status = null) {
434
+ if (process.env.SELLABLE_INSTALL_ASSUME_CODEX_DESKTOP === "1") return true;
435
+ const home = codexHome();
436
+ const probes = [
437
+ status?.details?.stdout || "",
438
+ process.env.PATH || "",
439
+ ].join("\n");
440
+
441
+ if (/WindowsApps[\\/]+codex\.exe/i.test(probes)) return true;
442
+ if (existsSync(join(home, "config.toml"))) return true;
443
+ if (existsSync(join(home, "plugins", ".plugin-appserver"))) return true;
444
+ if (existsSync(join(home, "plugins", "cache"))) return true;
445
+ if (process.platform === "darwin" && existsSync("/Applications/Codex.app")) {
446
+ return true;
447
+ }
448
+ return false;
449
+ }
450
+
451
+ function isNonInteractiveInstall() {
452
+ return (
453
+ process.env.CI === "true" ||
454
+ process.env.SELLABLE_NON_INTERACTIVE === "1" ||
455
+ process.env.SELLABLE_NON_INTERACTIVE === "true" ||
456
+ process.env.SELLABLE_NON_INTERACTIVE === "yes"
457
+ );
458
+ }
459
+
460
+ async function askYesNo(question, defaultAnswer = false) {
461
+ if (!process.stdin.isTTY || !output.isTTY || isNonInteractiveInstall()) {
462
+ return null;
463
+ }
464
+ const suffix = defaultAnswer ? " [Y/n] " : " [y/N] ";
465
+ const rl = createInterface({
466
+ input: process.stdin,
467
+ output: process.stdout,
468
+ });
469
+ try {
470
+ const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
471
+ if (!answer) return defaultAnswer;
472
+ return ["y", "yes"].includes(answer);
473
+ } finally {
474
+ rl.close();
475
+ }
476
+ }
477
+
478
+ function codexInstallCommand(platform = process.platform) {
479
+ const override = process.env.SELLABLE_CODEX_INSTALL_COMMAND;
480
+ if (override) {
481
+ return isWindowsPlatform(platform)
482
+ ? ["cmd.exe", ["/d", "/s", "/c", override]]
483
+ : ["sh", ["-c", override]];
484
+ }
485
+ if (isWindowsPlatform(platform)) {
486
+ return [
487
+ "powershell.exe",
488
+ [
489
+ "-NoProfile",
490
+ "-ExecutionPolicy",
491
+ "Bypass",
492
+ "-Command",
493
+ `$env:CODEX_NON_INTERACTIVE='1'; irm ${CODEX_INSTALL_URL}.ps1 | iex`,
494
+ ],
495
+ ];
496
+ }
497
+ return [
498
+ "sh",
499
+ ["-c", `curl -fsSL ${CODEX_INSTALL_URL}.sh | CODEX_NON_INTERACTIVE=1 sh`],
500
+ ];
501
+ }
502
+
503
+ function codexManualInstallCommand(platform = process.platform) {
504
+ return isWindowsPlatform(platform)
505
+ ? `$env:CODEX_NON_INTERACTIVE=1; irm ${CODEX_INSTALL_URL}.ps1 | iex`
506
+ : `curl -fsSL ${CODEX_INSTALL_URL}.sh | sh`;
507
+ }
508
+
509
+ async function maybeInstallCodexCli(status, opts) {
510
+ if (opts.dryRun) return false;
511
+ if (opts.codexCliInstall === "skip") {
512
+ logWarn(`${status.reason}. Skipping Codex CLI install by request.`);
513
+ return false;
514
+ }
515
+
516
+ let shouldInstall = opts.codexCliInstall === "install";
517
+ if (!shouldInstall) {
518
+ const answer = await askYesNo(
519
+ `${status.reason}. Install the Codex CLI now?`,
520
+ false
521
+ );
522
+ if (answer === null) {
523
+ logWarn(
524
+ `${status.reason}. Non-interactive shell; skipping Codex CLI install. Re-run with --install-codex-cli to install it.`
525
+ );
526
+ return false;
527
+ }
528
+ shouldInstall = answer;
529
+ }
530
+
531
+ if (!shouldInstall) {
532
+ logWarn(`${status.reason}. Continuing with Codex Desktop plugin setup only.`);
533
+ return false;
534
+ }
535
+
536
+ const [command, args] = codexInstallCommand();
537
+ logStep("Installing Codex CLI using the official Codex installer...");
538
+ try {
539
+ run(command, args, opts);
540
+ return true;
541
+ } catch (err) {
542
+ logWarn(
543
+ `Codex CLI install failed: ${err instanceof Error ? err.message : String(err)}`
544
+ );
545
+ return false;
546
+ }
547
+ }
548
+
363
549
  function shellQuote(value) {
364
550
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
365
551
  }
@@ -2455,6 +2641,27 @@ function writeAuth(opts) {
2455
2641
  return { written: true, reused: false };
2456
2642
  }
2457
2643
 
2644
+ function resolveWindowsNpmCommand() {
2645
+ const explicit =
2646
+ process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
2647
+ process.env.SELLABLE_INSTALL_NPM_COMMAND;
2648
+ if (explicit) return explicit;
2649
+
2650
+ const nodeBin = process.env.SELLABLE_INSTALL_NODE_BIN || "";
2651
+ for (const name of ["npm.cmd", "npm"]) {
2652
+ const candidate = nodeBin ? join(nodeBin, name) : "";
2653
+ if (candidate && existsSync(candidate)) return candidate;
2654
+ }
2655
+
2656
+ if (isWindowsPlatform()) {
2657
+ const details = commandExistsDetails("npm.cmd");
2658
+ const first = details.stdout.split(/\r?\n/).find((line) => line.trim());
2659
+ if (details.exists && first) return first.trim();
2660
+ }
2661
+
2662
+ return packageManagerCommand("win32");
2663
+ }
2664
+
2458
2665
  function installSelfShim(opts) {
2459
2666
  const installDir =
2460
2667
  process.env.SELLABLE_INSTALL_DIR || join(homedir(), ".local", "bin");
@@ -2475,12 +2682,11 @@ exec ${shellQuote(npmCommand)} exec --yes --package ${shellQuote(INSTALL_PACKAGE
2475
2682
  isWindowsPlatform() || process.env.SELLABLE_INSTALL_WRITE_CMD_SHIM === "1";
2476
2683
  if (shouldWriteCmdShim) {
2477
2684
  const cmdPath = `${binPath}.cmd`;
2478
- const npmCmdCommand =
2479
- process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
2480
- process.env.SELLABLE_INSTALL_NPM_COMMAND ||
2481
- packageManagerCommand("win32");
2685
+ const npmCmdCommand = resolveWindowsNpmCommand();
2482
2686
  const cmdPathPrefix = nodeBin ? `set "PATH=${nodeBin};%PATH%"\r\n` : "";
2483
- const cmdShim = `@echo off\r\n${cmdPathPrefix}"${npmCmdCommand}" exec --yes --package ${INSTALL_PACKAGE_SPEC} -- sellable %*\r\n`;
2687
+ const cmdShim =
2688
+ `@echo off\r\n${cmdPathPrefix}` +
2689
+ `call "${npmCmdCommand}" exec --yes --package ${INSTALL_PACKAGE_SPEC} -- sellable %*\r\n`;
2484
2690
  writeFile(cmdPath, cmdShim, opts, 0o755);
2485
2691
  }
2486
2692
  }
@@ -2820,32 +3026,70 @@ function patchClaudeAlwaysLoad(opts) {
2820
3026
  }
2821
3027
  }
2822
3028
 
2823
- function installCodex(opts) {
2824
- if (!opts.dryRun && !commandExists("codex")) {
3029
+ function registerCodexCliMcp(opts) {
3030
+ try {
3031
+ if (opts.server !== "hosted") {
3032
+ run("codex", ["mcp", "remove", "sellable"], {
3033
+ ...opts,
3034
+ dryRun: opts.dryRun,
3035
+ allowFail: true,
3036
+ });
3037
+ }
3038
+ run("codex", codexMcpAddArgs(opts), opts);
3039
+ return true;
3040
+ } catch (err) {
3041
+ logWarn(
3042
+ `Codex CLI MCP registration skipped: ${err instanceof Error ? err.message : String(err)}`
3043
+ );
3044
+ return false;
3045
+ }
3046
+ }
3047
+
3048
+ async function installCodex(opts) {
3049
+ let cliStatus = codexCliStatus(opts);
3050
+ let desktopLikely = codexDesktopLikelyInstalled(cliStatus);
3051
+ let attemptedCliInstall = false;
3052
+
3053
+ if (!opts.dryRun && !cliStatus.usable) {
3054
+ attemptedCliInstall = await maybeInstallCodexCli(cliStatus, opts);
3055
+ if (attemptedCliInstall) {
3056
+ cliStatus = codexCliStatus(opts);
3057
+ desktopLikely = codexDesktopLikelyInstalled(cliStatus) || desktopLikely;
3058
+ if (!cliStatus.usable) {
3059
+ logWarn(
3060
+ `${cliStatus.reason}. Continuing with Codex Desktop plugin setup where possible.`
3061
+ );
3062
+ }
3063
+ }
3064
+ }
3065
+
3066
+ const shouldInstallDesktop =
3067
+ opts.host === "codex" ||
3068
+ cliStatus.usable ||
3069
+ desktopLikely ||
3070
+ attemptedCliInstall;
3071
+ if (!shouldInstallDesktop) {
2825
3072
  const message =
2826
- "Codex CLI not found. Install/login to Codex, then rerun: sellable --host codex";
3073
+ "Codex CLI not found and Codex Desktop was not detected. Install/login to Codex, then rerun: sellable --host codex";
2827
3074
  if (opts.host === "all") {
2828
3075
  logWarn(`Skipping Codex: ${message}`);
2829
3076
  return { installed: false };
2830
3077
  }
2831
3078
  throw new Error(message);
2832
3079
  }
3080
+
2833
3081
  if (!opts.dryRun) {
2834
3082
  mkdirSync(codexHome(), { recursive: true, mode: 0o700 });
2835
3083
  }
2836
- if (opts.server === "hosted") {
2837
- run("codex", codexMcpAddArgs(opts), opts);
2838
- const info = installCodexDesktopPlugin(opts);
2839
- return { installed: true, ...info };
3084
+
3085
+ const cliRegistered = cliStatus.usable ? registerCodexCliMcp(opts) : false;
3086
+ if (!cliRegistered && !opts.dryRun) {
3087
+ logWarn(
3088
+ "Codex CLI MCP registration did not run; writing Codex Desktop plugin and config directly."
3089
+ );
2840
3090
  }
2841
- run("codex", ["mcp", "remove", "sellable"], {
2842
- ...opts,
2843
- dryRun: opts.dryRun,
2844
- allowFail: true,
2845
- });
2846
- run("codex", codexMcpAddArgs(opts), opts);
2847
3091
  const info = installCodexDesktopPlugin(opts);
2848
- return { installed: true, ...info };
3092
+ return { installed: true, cliRegistered, ...info };
2849
3093
  }
2850
3094
 
2851
3095
  function requiredCheck(ok, label) {
@@ -2996,11 +3240,11 @@ async function verify(opts) {
2996
3240
  );
2997
3241
  }
2998
3242
  if (opts.host === "codex" || opts.host === "all") {
2999
- const hasCodexCli = commandExists("codex");
3243
+ const codexStatus = codexCliStatus(opts);
3000
3244
  checks.push(
3001
3245
  warningCheck(
3002
- hasCodexCli,
3003
- hasCodexCli ? "Codex CLI present" : "Codex CLI missing"
3246
+ codexStatus.usable,
3247
+ codexStatus.usable ? "Codex CLI usable" : codexStatus.reason
3004
3248
  )
3005
3249
  );
3006
3250
  const pluginPath = join(
@@ -3358,8 +3602,8 @@ function printNextSteps(installedHosts, authReused) {
3358
3602
  console.log("");
3359
3603
  printInstallAgentBox(
3360
3604
  "Install Codex",
3361
- "npm install -g @openai/codex",
3362
- "https://github.com/openai/codex"
3605
+ codexManualInstallCommand(),
3606
+ "https://developers.openai.com/codex/quickstart"
3363
3607
  );
3364
3608
  console.log("");
3365
3609
  console.log("");
@@ -3805,7 +4049,7 @@ async function main() {
3805
4049
  }
3806
4050
  }
3807
4051
  if (opts.host === "codex" || opts.host === "all") {
3808
- const result = installCodex(opts);
4052
+ const result = await installCodex(opts);
3809
4053
  if (result.installed) {
3810
4054
  installedHosts.push("Codex");
3811
4055
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.216",
3
+ "version": "0.1.218",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {