@sellable/install 0.1.217 → 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.
package/README.md CHANGED
@@ -35,9 +35,8 @@ Campaign creation, foundation memory, content capture/ideation, and post
35
35
  drafting run inside Claude Code or Codex, where the Sellable MCP tools and
36
36
  approval flows are available.
37
37
 
38
- Install is auth-free by default. The normal path is first-run login: launch a
39
- Sellable workflow in Claude Code or Codex and the agent handles Sellable
40
- sign-in with a browser magic-link handoff.
38
+ Install is auth-free by default. If you do not pass a token, the agent handles
39
+ Sellable sign-in on the first campaign run with a magic-link handoff.
41
40
 
42
41
  The installer uses package stdio MCP by default:
43
42
 
@@ -65,15 +64,14 @@ verification in one path:
65
64
  curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh
66
65
  ```
67
66
 
68
- For scripted fallback after browser login, paste the command shown by Sellable:
67
+ For CI/scripted installs, get a Sellable API token from:
69
68
 
70
- ```bash
71
- sellable auth set <token> --workspace-id <workspace_id>
69
+ ```text
70
+ https://app.sellable.dev/settings
72
71
  ```
73
72
 
74
- For CI/env-only installs, operators can still pass `--token` / `SELLABLE_TOKEN`
75
- plus `--workspace-id` / `SELLABLE_WORKSPACE_ID`. Do not use env vars as the
76
- primary human setup path.
73
+ Then pass it with `--token` / `SELLABLE_TOKEN` plus `--workspace-id` /
74
+ `SELLABLE_WORKSPACE_ID`.
77
75
 
78
76
  Auth is stored once at:
79
77
 
@@ -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
  }
@@ -419,26 +605,13 @@ async function loadAuthIfPresent(opts) {
419
605
  return opts;
420
606
  }
421
607
 
422
- function redactTokensForLog(value) {
423
- if (Array.isArray(value)) {
424
- return value.map((item) => redactTokensForLog(item));
425
- }
426
- if (value && typeof value === "object") {
427
- return Object.fromEntries(
428
- Object.entries(value).map(([key, item]) => [
429
- key,
430
- key.toLowerCase().includes("token") && typeof item === "string"
431
- ? redact(item)
432
- : redactTokensForLog(item),
433
- ])
434
- );
435
- }
436
- return value;
437
- }
438
-
439
608
  function writeJson(path, data, opts) {
440
609
  if (VERBOSE) {
441
- const redacted = JSON.stringify(redactTokensForLog(data), null, 2);
610
+ const redacted = JSON.stringify(
611
+ { ...data, token: redact(data.token) },
612
+ null,
613
+ 2
614
+ );
442
615
  logVerbose(`${C.grey}Writing ${path}: ${redacted}${C.reset}`);
443
616
  }
444
617
  if (opts.dryRun) return;
@@ -455,56 +628,6 @@ function readExisting(path) {
455
628
  }
456
629
  }
457
630
 
458
- function mergeAuthConfig(raw, auth) {
459
- const existing =
460
- raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
461
- const authFields = {
462
- token: auth.token,
463
- activeWorkspaceId: auth.activeWorkspaceId,
464
- apiUrl: auth.apiUrl,
465
- };
466
-
467
- if (existing.activeEnv && existing.environments) {
468
- const activeEnv = existing.activeEnv;
469
- const envConfig = existing.environments[activeEnv];
470
- if (
471
- !envConfig ||
472
- typeof envConfig !== "object" ||
473
- Array.isArray(envConfig)
474
- ) {
475
- throw new Error(
476
- `Unknown active environment '${activeEnv}' in ${authPath()}`
477
- );
478
- }
479
- return {
480
- ...existing,
481
- environments: {
482
- ...existing.environments,
483
- [activeEnv]: {
484
- ...envConfig,
485
- ...authFields,
486
- },
487
- },
488
- };
489
- }
490
-
491
- return {
492
- ...existing,
493
- ...authFields,
494
- };
495
- }
496
-
497
- function writeAuthSetConfig({ token, workspaceId, apiUrl, dryRun }) {
498
- const configPath = authPath();
499
- const raw = readExisting(configPath) || {};
500
- const config = mergeAuthConfig(raw, {
501
- token,
502
- activeWorkspaceId: workspaceId || null,
503
- apiUrl,
504
- });
505
- writeJson(configPath, config, { dryRun });
506
- }
507
-
508
631
  function sellableHostEnvPath() {
509
632
  return join(homedir(), ".local", "sellable", "app-sellable-dev", ".env");
510
633
  }
@@ -2518,6 +2641,27 @@ function writeAuth(opts) {
2518
2641
  return { written: true, reused: false };
2519
2642
  }
2520
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
+
2521
2665
  function installSelfShim(opts) {
2522
2666
  const installDir =
2523
2667
  process.env.SELLABLE_INSTALL_DIR || join(homedir(), ".local", "bin");
@@ -2538,12 +2682,11 @@ exec ${shellQuote(npmCommand)} exec --yes --package ${shellQuote(INSTALL_PACKAGE
2538
2682
  isWindowsPlatform() || process.env.SELLABLE_INSTALL_WRITE_CMD_SHIM === "1";
2539
2683
  if (shouldWriteCmdShim) {
2540
2684
  const cmdPath = `${binPath}.cmd`;
2541
- const npmCmdCommand =
2542
- process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
2543
- process.env.SELLABLE_INSTALL_NPM_COMMAND ||
2544
- packageManagerCommand("win32");
2685
+ const npmCmdCommand = resolveWindowsNpmCommand();
2545
2686
  const cmdPathPrefix = nodeBin ? `set "PATH=${nodeBin};%PATH%"\r\n` : "";
2546
- 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`;
2547
2690
  writeFile(cmdPath, cmdShim, opts, 0o755);
2548
2691
  }
2549
2692
  }
@@ -2883,32 +3026,70 @@ function patchClaudeAlwaysLoad(opts) {
2883
3026
  }
2884
3027
  }
2885
3028
 
2886
- function installCodex(opts) {
2887
- 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) {
2888
3072
  const message =
2889
- "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";
2890
3074
  if (opts.host === "all") {
2891
3075
  logWarn(`Skipping Codex: ${message}`);
2892
3076
  return { installed: false };
2893
3077
  }
2894
3078
  throw new Error(message);
2895
3079
  }
3080
+
2896
3081
  if (!opts.dryRun) {
2897
3082
  mkdirSync(codexHome(), { recursive: true, mode: 0o700 });
2898
3083
  }
2899
- if (opts.server === "hosted") {
2900
- run("codex", codexMcpAddArgs(opts), opts);
2901
- const info = installCodexDesktopPlugin(opts);
2902
- 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
+ );
2903
3090
  }
2904
- run("codex", ["mcp", "remove", "sellable"], {
2905
- ...opts,
2906
- dryRun: opts.dryRun,
2907
- allowFail: true,
2908
- });
2909
- run("codex", codexMcpAddArgs(opts), opts);
2910
3091
  const info = installCodexDesktopPlugin(opts);
2911
- return { installed: true, ...info };
3092
+ return { installed: true, cliRegistered, ...info };
2912
3093
  }
2913
3094
 
2914
3095
  function requiredCheck(ok, label) {
@@ -3059,11 +3240,11 @@ async function verify(opts) {
3059
3240
  );
3060
3241
  }
3061
3242
  if (opts.host === "codex" || opts.host === "all") {
3062
- const hasCodexCli = commandExists("codex");
3243
+ const codexStatus = codexCliStatus(opts);
3063
3244
  checks.push(
3064
3245
  warningCheck(
3065
- hasCodexCli,
3066
- hasCodexCli ? "Codex CLI present" : "Codex CLI missing"
3246
+ codexStatus.usable,
3247
+ codexStatus.usable ? "Codex CLI usable" : codexStatus.reason
3067
3248
  )
3068
3249
  );
3069
3250
  const pluginPath = join(
@@ -3421,8 +3602,8 @@ function printNextSteps(installedHosts, authReused) {
3421
3602
  console.log("");
3422
3603
  printInstallAgentBox(
3423
3604
  "Install Codex",
3424
- "npm install -g @openai/codex",
3425
- "https://github.com/openai/codex"
3605
+ codexManualInstallCommand(),
3606
+ "https://developers.openai.com/codex/quickstart"
3426
3607
  );
3427
3608
  console.log("");
3428
3609
  console.log("");
@@ -3728,31 +3909,18 @@ async function main() {
3728
3909
  }
3729
3910
  const token = rawArgs[2];
3730
3911
  const authSetFlags = rawArgs.slice(3);
3731
- let dryRun = false;
3732
- let workspaceId = "";
3733
- for (let i = 0; i < authSetFlags.length; i += 1) {
3734
- const flag = authSetFlags[i];
3735
- if (flag === "--dry-run") {
3736
- dryRun = true;
3737
- continue;
3738
- }
3739
- if (flag === "--workspace-id") {
3740
- const value = authSetFlags[i + 1];
3741
- if (!value || value.startsWith("--")) {
3742
- console.error("Missing value for --workspace-id");
3743
- process.exit(2);
3744
- }
3745
- workspaceId = value;
3746
- i += 1;
3747
- continue;
3748
- }
3749
- console.error(`Unknown auth set option: ${flag}`);
3912
+ const dryRun = authSetFlags.includes("--dry-run");
3913
+ const unknownAuthSetFlag = authSetFlags.find(
3914
+ (arg) => arg !== "--dry-run"
3915
+ );
3916
+ if (unknownAuthSetFlag) {
3917
+ console.error(`Unknown auth set option: ${unknownAuthSetFlag}`);
3750
3918
  process.exit(2);
3751
3919
  }
3752
3920
  if (!token) {
3753
3921
  console.error(
3754
- "Usage: sellable auth set <token> [--workspace-id <id>]\n" +
3755
- "Use this only when the Sellable browser login page shows a manual fallback command."
3922
+ "Usage: sellable auth set <token>\n" +
3923
+ "Get the token from the Sellable browser confirm page (after clicking the magic link)."
3756
3924
  );
3757
3925
  process.exit(2);
3758
3926
  }
@@ -3764,22 +3932,23 @@ async function main() {
3764
3932
  );
3765
3933
  process.exit(2);
3766
3934
  }
3767
- // Bypass writeAuth() because auth-set is a first-run browser fallback:
3768
- // the workspace id is optional for legacy fallback pages, and existing
3769
- // config metadata must be preserved.
3935
+ // Bypass writeAuth() its workspaceId guard early-returns on missing
3936
+ // workspaceId, but on the auth-set path we don't have one yet (next MCP
3937
+ // call hydrates it). Write the same shape directly.
3770
3938
  // Skip installSelfShim() — by definition the user has the shim already
3771
3939
  // (they invoked `sellable auth set` from it).
3772
3940
  const apiUrl = process.env.SELLABLE_API_URL || DEFAULT_API_URL;
3773
- writeAuthSetConfig({ token, workspaceId, apiUrl, dryRun });
3941
+ writeJson(
3942
+ authPath(),
3943
+ { token, activeWorkspaceId: null, apiUrl },
3944
+ { dryRun }
3945
+ );
3774
3946
  if (dryRun) {
3775
3947
  console.log(`Dry run: token would be saved to ${authPath()}`);
3776
3948
  } else {
3777
3949
  console.log(`✓ Token saved to ${authPath()}`);
3778
3950
  }
3779
3951
  console.log(` apiUrl: ${apiUrl}`);
3780
- if (workspaceId) {
3781
- console.log(` activeWorkspaceId: ${workspaceId}`);
3782
- }
3783
3952
  console.log(` Continue in your agent:`);
3784
3953
  console.log(` Claude Code: /sellable:create-campaign`);
3785
3954
  console.log(` Claude Code: /sellable:foundation`);
@@ -3880,7 +4049,7 @@ async function main() {
3880
4049
  }
3881
4050
  }
3882
4051
  if (opts.host === "codex" || opts.host === "all") {
3883
- const result = installCodex(opts);
4052
+ const result = await installCodex(opts);
3884
4053
  if (result.installed) {
3885
4054
  installedHosts.push("Codex");
3886
4055
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.217",
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": {
@@ -893,11 +893,7 @@ updates.
893
893
 
894
894
  1. Call `mcp__sellable__get_auth_status({})`.
895
895
  2. If auth is not OK with `error.type === "config"` or `error.type === "auth"`,
896
- the user has not signed in yet. Run first-run login through the FTUX
897
- magic-link handoff. If a browser page or tool guidance gives the user a
898
- manual fallback, it must be
899
- `sellable auth set <token> --workspace-id <workspace_id>`. Do not instruct
900
- the user to hand-edit JSON auth config.
896
+ the user has not signed in yet. Run the FTUX magic-link handoff:
901
897
 
902
898
  a. Say to the user verbatim:
903
899