@sellable/install 0.1.28 → 0.1.31

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.
@@ -5,12 +5,12 @@ import {
5
5
  mkdirSync,
6
6
  readFileSync,
7
7
  rmSync,
8
+ symlinkSync,
8
9
  writeFileSync,
9
10
  } from "node:fs";
10
11
  import { homedir } from "node:os";
11
- import { dirname, join } from "node:path";
12
- import { stdin as input, stdout as output } from "node:process";
13
- import { createInterface } from "node:readline/promises";
12
+ import { dirname, join, relative } from "node:path";
13
+ import { stdout as output } from "node:process";
14
14
 
15
15
  const DEFAULT_API_URL = "https://app.sellable.dev";
16
16
  const DEFAULT_SERVER_PACKAGE =
@@ -40,6 +40,62 @@ const CODEX_PLUGIN_COMPAT_VERSIONS = [
40
40
  const INSTALL_PACKAGE_SPEC =
41
41
  process.env.SELLABLE_INSTALL_PACKAGE_SPEC || "@sellable/install@latest";
42
42
 
43
+ const useColor = Boolean(output.isTTY) && process.env.NO_COLOR === undefined;
44
+ const C = {
45
+ reset: useColor ? "\x1b[0m" : "",
46
+ green: useColor ? "\x1b[32m" : "",
47
+ cyan: useColor ? "\x1b[36m" : "",
48
+ yellow: useColor ? "\x1b[33m" : "",
49
+ grey: useColor ? "\x1b[90m" : "",
50
+ bold: useColor ? "\x1b[1m" : "",
51
+ magenta: useColor ? "\x1b[35m" : "",
52
+ };
53
+
54
+ let VERBOSE = false;
55
+
56
+ function logVerbose(line) {
57
+ if (VERBOSE) console.log(line);
58
+ }
59
+
60
+ function logMilestone(line) {
61
+ console.log(` ${C.green}✓${C.reset} ${line}`);
62
+ }
63
+
64
+ function logWarn(line) {
65
+ console.log(` ${C.yellow}!${C.reset} ${line}`);
66
+ }
67
+
68
+ function logStep(line) {
69
+ console.log(` ${C.grey}${line}${C.reset}`);
70
+ }
71
+
72
+ function printBanner() {
73
+ const cols = output.columns || 80;
74
+ if (output.isTTY && cols >= 70) {
75
+ const lines = [
76
+ " ███████╗ ███████╗ ██╗ ██╗ █████╗ ██████╗ ██╗ ███████╗",
77
+ " ██╔════╝ ██╔════╝ ██║ ██║ ██╔══██╗ ██╔══██╗ ██║ ██╔════╝",
78
+ " ███████╗ █████╗ ██║ ██║ ███████║ ██████╔╝ ██║ █████╗ ",
79
+ " ╚════██║ ██╔══╝ ██║ ██║ ██╔══██║ ██╔══██╗ ██║ ██╔══╝ ",
80
+ " ███████║ ███████╗ ███████╗ ███████╗██║ ██║ ██████╔╝ ███████╗ ███████╗",
81
+ " ╚══════╝ ╚══════╝ ╚══════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚══════╝",
82
+ ];
83
+ console.log("");
84
+ for (const line of lines) console.log(`${C.magenta}${line}${C.reset}`);
85
+ console.log("");
86
+ } else {
87
+ console.log("");
88
+ console.log(`${C.bold}${C.magenta}SELLABLE${C.reset}`);
89
+ console.log("");
90
+ }
91
+ }
92
+
93
+ function printDivider() {
94
+ console.log(
95
+ `${C.cyan}────────────────────────────────────────────────────────────${C.reset}`
96
+ );
97
+ }
98
+
43
99
  function usage() {
44
100
  return `Sellable agent installer
45
101
 
@@ -56,14 +112,20 @@ Options:
56
112
  --mcp-package <pkg> MCP server package. Default: ${DEFAULT_SERVER_PACKAGE}
57
113
  --local-command <cmd> Local MCP command for --server local.
58
114
  --hosted-url <url> Hosted MCP URL for --server hosted.
115
+ --verbose Print every file write and shell command.
59
116
  --dry-run Print actions without writing or running host commands.
60
117
  --verify-only Verify installed host config where possible.
61
118
  --help Show help.
62
119
 
63
120
  Auth:
64
- Sign up or log in at ${DEFAULT_API_URL}/settings, create an API token, then
65
- pass it with --token or SELLABLE_TOKEN. In an interactive terminal, the
66
- installer will prompt for missing auth values.
121
+ Install is auth-free by default. Sign in happens on the first run of
122
+ /sellable:create-campaign in Claude Code or Codex, where the agent walks
123
+ you through signup or sign-in and stores credentials in
124
+ ~/.sellable/config.json.
125
+
126
+ For CI/scripted installs, pass --token + --workspace-id or set
127
+ SELLABLE_TOKEN + SELLABLE_WORKSPACE_ID and the installer will write the
128
+ config file directly without prompting.
67
129
  `;
68
130
  }
69
131
 
@@ -79,6 +141,7 @@ function parseArgs(argv) {
79
141
  hostedUrl: process.env.SELLABLE_MCP_HOSTED_URL || "",
80
142
  dryRun: false,
81
143
  verifyOnly: false,
144
+ verbose: false,
82
145
  };
83
146
 
84
147
  for (let i = 0; i < argv.length; i += 1) {
@@ -114,6 +177,8 @@ function parseArgs(argv) {
114
177
  opts.dryRun = true;
115
178
  } else if (arg === "--verify-only") {
116
179
  opts.verifyOnly = true;
180
+ } else if (arg === "--verbose") {
181
+ opts.verbose = true;
117
182
  } else {
118
183
  throw new Error(`Unknown option: ${arg}`);
119
184
  }
@@ -137,13 +202,13 @@ function redact(value) {
137
202
 
138
203
  function run(command, args, opts = {}) {
139
204
  const rendered = [command, ...args].join(" ");
140
- console.log(
141
- `+ ${rendered.replace(opts.token || "__NO_TOKEN__", "[redacted-token]")}`
205
+ logVerbose(
206
+ `${C.grey}+ ${rendered.replace(opts.token || "__NO_TOKEN__", "[redacted-token]")}${C.reset}`
142
207
  );
143
208
  if (opts.dryRun) return { status: 0, stdout: "", stderr: "" };
144
209
  const result = spawnSync(command, args, {
145
210
  encoding: "utf8",
146
- stdio: "pipe",
211
+ stdio: VERBOSE ? "inherit" : "pipe",
147
212
  });
148
213
  if (result.status !== 0 && !opts.allowFail) {
149
214
  const stderr = (result.stderr || "").trim();
@@ -159,10 +224,6 @@ function commandExists(command) {
159
224
  return result.status === 0;
160
225
  }
161
226
 
162
- function isInteractive() {
163
- return Boolean(input.isTTY && output.isTTY);
164
- }
165
-
166
227
  function authPath() {
167
228
  return join(homedir(), ".sellable", "config.json");
168
229
  }
@@ -190,7 +251,7 @@ function readStoredAuth() {
190
251
  return raw ? normalizeStoredAuth(raw) : null;
191
252
  }
192
253
 
193
- async function promptForMissingAuth(opts) {
254
+ async function loadAuthIfPresent(opts) {
194
255
  if (opts.token && opts.workspaceId) return opts;
195
256
 
196
257
  const stored = readStoredAuth();
@@ -199,58 +260,19 @@ async function promptForMissingAuth(opts) {
199
260
  opts.workspaceId ||= stored.workspaceId;
200
261
  opts.apiUrl = opts.apiUrl || stored.apiUrl || DEFAULT_API_URL;
201
262
  opts.authFromExistingConfig = true;
202
- console.log(`Using existing Sellable auth config: ${authPath()}`);
203
- return opts;
204
- }
205
-
206
- if (!isInteractive()) {
207
- throw new Error(
208
- [
209
- "Missing Sellable token/workspace id.",
210
- `Create a token at ${opts.apiUrl}/settings, then rerun:`,
211
- ` npx -y ${INSTALL_PACKAGE_SPEC} --host all --token <token> --workspace-id <workspace_id>`,
212
- "",
213
- "You can also use SELLABLE_TOKEN and SELLABLE_WORKSPACE_ID.",
214
- ].join("\n")
215
- );
216
- }
217
-
218
- console.log("");
219
- console.log(
220
- "Sellable needs one API token to connect Claude/Codex to your workspace."
221
- );
222
- console.log(
223
- `Open ${opts.apiUrl}/settings, create a token, then paste the values below.`
224
- );
225
- console.log(`Auth will be stored once at ${authPath()}.`);
226
- console.log("");
227
-
228
- const rl = createInterface({ input, output });
229
- try {
230
- if (!opts.token) {
231
- opts.token = (await rl.question("Sellable API token: ")).trim();
232
- }
233
- if (!opts.workspaceId) {
234
- opts.workspaceId = (await rl.question("Sellable workspace id: ")).trim();
235
- }
236
- } finally {
237
- rl.close();
238
- }
239
-
240
- if (!opts.token || !opts.workspaceId) {
241
- throw new Error("Sellable token and workspace id are both required.");
242
263
  }
243
-
244
264
  return opts;
245
265
  }
246
266
 
247
267
  function writeJson(path, data, opts) {
248
- const redacted = JSON.stringify(
249
- { ...data, token: redact(data.token) },
250
- null,
251
- 2
252
- );
253
- console.log(`Writing ${path}: ${redacted}`);
268
+ if (VERBOSE) {
269
+ const redacted = JSON.stringify(
270
+ { ...data, token: redact(data.token) },
271
+ null,
272
+ 2
273
+ );
274
+ logVerbose(`${C.grey}Writing ${path}: ${redacted}${C.reset}`);
275
+ }
254
276
  if (opts.dryRun) return;
255
277
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
256
278
  writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
@@ -317,12 +339,25 @@ function upsertTomlBoolean(content, tableName, key, value) {
317
339
  }
318
340
 
319
341
  function writeFile(path, content, opts, mode = 0o644) {
320
- console.log(`Writing ${path}`);
342
+ logVerbose(`${C.grey}Writing ${path}${C.reset}`);
321
343
  if (opts.dryRun) return;
322
344
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
323
345
  writeFileSync(path, content, { mode });
324
346
  }
325
347
 
348
+ function ensureSymlink(target, linkPath, opts) {
349
+ logVerbose(`${C.grey}Linking ${linkPath} -> ${target}${C.reset}`);
350
+ if (opts.dryRun) return;
351
+ mkdirSync(dirname(linkPath), { recursive: true, mode: 0o700 });
352
+ try {
353
+ rmSync(linkPath, { recursive: true, force: true });
354
+ } catch {
355
+ // ignore
356
+ }
357
+ const rel = relative(dirname(linkPath), target);
358
+ symlinkSync(rel, linkPath, "dir");
359
+ }
360
+
326
361
  function codexPluginManifest(opts) {
327
362
  return {
328
363
  name: "sellable",
@@ -1062,13 +1097,7 @@ function installCodexDesktopPlugin(opts) {
1062
1097
  );
1063
1098
  const pluginRoot = join(marketplaceRoot, "plugins", "sellable");
1064
1099
  const cacheRoot = join(home, "plugins", "cache", "sellable", "sellable");
1065
- const pluginCache = join(cacheRoot, CODEX_PLUGIN_VERSION);
1066
- const pluginCacheVersions = [
1067
- CODEX_PLUGIN_VERSION,
1068
- ...CODEX_PLUGIN_COMPAT_VERSIONS.filter(
1069
- (version) => version !== CODEX_PLUGIN_VERSION
1070
- ),
1071
- ];
1100
+ const canonicalCachePath = join(cacheRoot, CODEX_PLUGIN_VERSION);
1072
1101
 
1073
1102
  const marketplace = {
1074
1103
  name: "sellable",
@@ -1110,23 +1139,24 @@ function installCodexDesktopPlugin(opts) {
1110
1139
  if (!opts.dryRun) {
1111
1140
  rmSync(cacheRoot, { recursive: true, force: true });
1112
1141
  }
1113
- for (const version of pluginCacheVersions) {
1114
- const cachePath = join(cacheRoot, version);
1115
- writeFile(
1116
- join(cachePath, ".codex-plugin", "plugin.json"),
1117
- `${JSON.stringify(
1118
- { ...manifest, version: CODEX_PLUGIN_VERSION },
1119
- null,
1120
- 2
1121
- )}\n`,
1122
- opts
1123
- );
1124
- writeFile(
1125
- join(cachePath, ".mcp.json"),
1126
- `${JSON.stringify(mcp, null, 2)}\n`,
1127
- opts
1128
- );
1129
- writeCodexPluginSkills(cachePath, opts);
1142
+
1143
+ // Write canonical version once
1144
+ writeFile(
1145
+ join(canonicalCachePath, ".codex-plugin", "plugin.json"),
1146
+ `${JSON.stringify({ ...manifest, version: CODEX_PLUGIN_VERSION }, null, 2)}\n`,
1147
+ opts
1148
+ );
1149
+ writeFile(
1150
+ join(canonicalCachePath, ".mcp.json"),
1151
+ `${JSON.stringify(mcp, null, 2)}\n`,
1152
+ opts
1153
+ );
1154
+ writeCodexPluginSkills(canonicalCachePath, opts);
1155
+
1156
+ // Symlink older compat versions to canonical (one ln per version, no file copy)
1157
+ for (const version of CODEX_PLUGIN_COMPAT_VERSIONS) {
1158
+ if (version === CODEX_PLUGIN_VERSION) continue;
1159
+ ensureSymlink(canonicalCachePath, join(cacheRoot, version), opts);
1130
1160
  }
1131
1161
 
1132
1162
  if (!opts.dryRun) {
@@ -1162,28 +1192,31 @@ enabled = false`
1162
1192
  );
1163
1193
  writeFileSync(configPath, `${content.trimEnd()}\n`, { mode: 0o600 });
1164
1194
  } else {
1165
- console.log(`+ upsert [marketplaces.sellable] in ${configPath}`);
1166
- console.log(`+ enable [plugins."sellable@sellable"] in ${configPath}`);
1167
- console.log(
1168
- `+ disable stale [plugins."sellable@sellable-local"] in ${configPath}`
1195
+ logVerbose(
1196
+ `${C.grey}+ upsert [marketplaces.sellable] in ${configPath}${C.reset}`
1169
1197
  );
1170
- console.log(
1171
- `+ enable [features].default_mode_request_user_input in ${configPath}`
1198
+ logVerbose(
1199
+ `${C.grey}+ enable [plugins."sellable@sellable"] in ${configPath}${C.reset}`
1200
+ );
1201
+ logVerbose(
1202
+ `${C.grey}+ enable [features].default_mode_request_user_input in ${configPath}${C.reset}`
1172
1203
  );
1173
1204
  }
1174
1205
 
1175
- console.log("Codex Desktop plugin installed:");
1176
- console.log(`- marketplace: ${marketplaceRoot}`);
1177
- console.log(`- plugin: sellable@sellable`);
1178
- console.log(`- cache: ${pluginCache}`);
1179
- console.log(`- compat: ${CODEX_PLUGIN_COMPAT_VERSIONS.join(", ")}`);
1206
+ return {
1207
+ marketplaceRoot,
1208
+ canonicalCachePath,
1209
+ compatCount: CODEX_PLUGIN_COMPAT_VERSIONS.length,
1210
+ };
1180
1211
  }
1181
1212
 
1182
1213
  function writeAuth(opts) {
1183
1214
  if (!opts.token || !opts.workspaceId) {
1184
- throw new Error(
1185
- `Missing Sellable token/workspace id. Create a token at ${opts.apiUrl}/settings, then rerun with --token and --workspace-id or SELLABLE_TOKEN and SELLABLE_WORKSPACE_ID.`
1186
- );
1215
+ return { written: false, reused: false };
1216
+ }
1217
+
1218
+ if (opts.authFromExistingConfig) {
1219
+ return { written: false, reused: true };
1187
1220
  }
1188
1221
 
1189
1222
  const config = {
@@ -1191,16 +1224,8 @@ function writeAuth(opts) {
1191
1224
  activeWorkspaceId: opts.workspaceId,
1192
1225
  apiUrl: opts.apiUrl,
1193
1226
  };
1194
-
1195
- if (opts.authFromExistingConfig) {
1196
- console.log(
1197
- `Leaving existing Sellable auth config unchanged: ${authPath()}`
1198
- );
1199
- return;
1200
- }
1201
-
1202
- const sellablePath = authPath();
1203
- writeJson(sellablePath, config, opts);
1227
+ writeJson(authPath(), config, opts);
1228
+ return { written: true, reused: false };
1204
1229
  }
1205
1230
 
1206
1231
  function installSelfShim(opts) {
@@ -1234,7 +1259,7 @@ function installClaude(opts) {
1234
1259
  const message =
1235
1260
  "Claude CLI not found. Install/login to Claude Code, then rerun: sellable --host claude";
1236
1261
  if (opts.host === "all") {
1237
- console.log(`Skipping Claude Code: ${message}`);
1262
+ logWarn(`Skipping Claude Code: ${message}`);
1238
1263
  return false;
1239
1264
  }
1240
1265
  throw new Error(message);
@@ -1266,15 +1291,15 @@ function installCodex(opts) {
1266
1291
  const message =
1267
1292
  "Codex CLI not found. Install/login to Codex, then rerun: sellable --host codex";
1268
1293
  if (opts.host === "all") {
1269
- console.log(`Skipping Codex: ${message}`);
1270
- return false;
1294
+ logWarn(`Skipping Codex: ${message}`);
1295
+ return { installed: false };
1271
1296
  }
1272
1297
  throw new Error(message);
1273
1298
  }
1274
1299
  if (opts.server === "hosted") {
1275
1300
  run("codex", ["mcp", "add", "sellable", "--url", opts.hostedUrl], opts);
1276
- installCodexDesktopPlugin(opts);
1277
- return true;
1301
+ const info = installCodexDesktopPlugin(opts);
1302
+ return { installed: true, ...info };
1278
1303
  }
1279
1304
  const [command, args] = mcpCommand(opts);
1280
1305
  run("codex", ["mcp", "remove", "sellable"], {
@@ -1283,27 +1308,35 @@ function installCodex(opts) {
1283
1308
  allowFail: true,
1284
1309
  });
1285
1310
  run("codex", ["mcp", "add", "sellable", "--", command, ...args], opts);
1286
- installCodexDesktopPlugin(opts);
1287
- return true;
1311
+ const info = installCodexDesktopPlugin(opts);
1312
+ return { installed: true, ...info };
1288
1313
  }
1289
1314
 
1290
1315
  function verify(opts) {
1291
1316
  const stored = readStoredAuth();
1292
- if (!stored?.token || !stored?.workspaceId) {
1293
- throw new Error(
1294
- `Sellable auth config missing or incomplete: ${authPath()}`
1295
- );
1317
+ const checks = [];
1318
+ if (stored?.token && stored?.workspaceId) {
1319
+ checks.push({ ok: true, label: `Auth config: ${authPath()}` });
1320
+ } else {
1321
+ checks.push({
1322
+ ok: true,
1323
+ label: `Auth: not yet signed in — sign in on first run of /sellable:create-campaign`,
1324
+ });
1296
1325
  }
1297
- console.log(`Sellable auth config present: ${authPath()}`);
1326
+
1298
1327
  if (opts.host === "claude" || opts.host === "all") {
1299
- console.log(
1300
- commandExists("claude") ? "Claude CLI present" : "Claude CLI missing"
1301
- );
1328
+ checks.push({
1329
+ ok: commandExists("claude"),
1330
+ label: commandExists("claude")
1331
+ ? "Claude CLI present"
1332
+ : "Claude CLI missing",
1333
+ });
1302
1334
  }
1303
1335
  if (opts.host === "codex" || opts.host === "all") {
1304
- console.log(
1305
- commandExists("codex") ? "Codex CLI present" : "Codex CLI missing"
1306
- );
1336
+ checks.push({
1337
+ ok: commandExists("codex"),
1338
+ label: commandExists("codex") ? "Codex CLI present" : "Codex CLI missing",
1339
+ });
1307
1340
  const pluginPath = join(
1308
1341
  codexHome(),
1309
1342
  "plugins",
@@ -1325,45 +1358,114 @@ function verify(opts) {
1325
1358
  "sellable-create-campaign",
1326
1359
  "SKILL.md"
1327
1360
  );
1328
- console.log(
1329
- existsSync(pluginPath)
1361
+ checks.push({
1362
+ ok: existsSync(pluginPath),
1363
+ label: existsSync(pluginPath)
1330
1364
  ? "Codex Desktop plugin present"
1331
- : "Codex Desktop plugin missing"
1332
- );
1333
- console.log(
1334
- existsSync(skillPath)
1365
+ : "Codex Desktop plugin missing",
1366
+ });
1367
+ checks.push({
1368
+ ok: existsSync(skillPath),
1369
+ label: existsSync(skillPath)
1335
1370
  ? "Codex skill bundle present"
1336
- : "Codex skill bundle missing"
1337
- );
1371
+ : "Codex skill bundle missing",
1372
+ });
1338
1373
  const configPath = join(codexHome(), "config.toml");
1339
1374
  const configContent = existsSync(configPath)
1340
1375
  ? readFileSync(configPath, "utf8")
1341
1376
  : "";
1342
- console.log(
1343
- configContent.includes("default_mode_request_user_input = true")
1344
- ? "Codex Default-mode request_user_input enabled"
1345
- : "Codex Default-mode request_user_input missing"
1377
+ const hasFlag = configContent.includes(
1378
+ "default_mode_request_user_input = true"
1346
1379
  );
1380
+ checks.push({
1381
+ ok: hasFlag,
1382
+ label: hasFlag
1383
+ ? "Codex Default-mode request_user_input enabled"
1384
+ : "Codex Default-mode request_user_input missing",
1385
+ });
1386
+ }
1387
+
1388
+ for (const c of checks) {
1389
+ if (c.ok) logMilestone(c.label);
1390
+ else logWarn(c.label);
1347
1391
  }
1348
1392
  }
1349
1393
 
1350
1394
  function printNextSteps(installedHosts) {
1351
1395
  console.log("");
1352
- console.log("Next steps:");
1353
- if (installedHosts.length > 0) {
1396
+ printDivider();
1397
+ console.log(
1398
+ ` ${C.bold}You're set. Let's launch your first campaign.${C.reset}`
1399
+ );
1400
+ printDivider();
1401
+ console.log("");
1402
+
1403
+ if (installedHosts.length === 0) {
1354
1404
  console.log(
1355
- `1. Fully quit and reopen ${installedHosts.join(" and ")} so MCP tools reload.`
1405
+ ` ${C.yellow}!${C.reset} Neither Claude Code nor Codex was found on this machine.`
1356
1406
  );
1357
- console.log("2. Start a new thread and choose Sellable Create Campaign.");
1407
+ console.log("");
1408
+ console.log(` Install one of them, then rerun this command:`);
1409
+ console.log(` ${C.cyan}npx -y @sellable/install@latest${C.reset}`);
1410
+ console.log("");
1358
1411
  console.log(
1359
- "3. If tools do not appear, run: sellable --verify-only --host all"
1412
+ ` ${C.grey}Claude Code: https://docs.anthropic.com/en/docs/claude-code${C.reset}`
1360
1413
  );
1361
- } else {
1362
1414
  console.log(
1363
- "1. Install Claude Code or Codex, then rerun this installer for that host."
1415
+ ` ${C.grey}Codex: https://github.com/openai/codex${C.reset}`
1364
1416
  );
1365
- console.log("2. Verify auth later with: sellable --verify-only --host all");
1417
+ console.log("");
1418
+ return;
1366
1419
  }
1420
+
1421
+ const useClaude = installedHosts.includes("Claude Code");
1422
+ const primary = useClaude
1423
+ ? {
1424
+ label: "Claude Code",
1425
+ cmd: "claude",
1426
+ slash: "/sellable:create-campaign",
1427
+ }
1428
+ : { label: "Codex", cmd: "codex", slash: "/sellable:create-campaign" };
1429
+ const secondary =
1430
+ useClaude && installedHosts.includes("Codex")
1431
+ ? { label: "Codex", cmd: "codex", slash: "/sellable:create-campaign" }
1432
+ : null;
1433
+
1434
+ console.log(
1435
+ ` ${C.bold}Try it now${C.reset} ${C.grey}(in a new terminal)${C.reset}`
1436
+ );
1437
+ console.log("");
1438
+ console.log(` ${C.grey}1.${C.reset} Open ${primary.label}:`);
1439
+ console.log("");
1440
+ console.log(` ${C.cyan}${primary.cmd}${C.reset}`);
1441
+ console.log("");
1442
+ console.log(` ${C.grey}2.${C.reset} Type this and hit enter:`);
1443
+ console.log("");
1444
+ console.log(` ${C.cyan}${primary.slash}${C.reset}`);
1445
+ console.log("");
1446
+ console.log(
1447
+ ` ${C.grey}First time? Sellable signs you up automatically — just answer in chat.${C.reset}`
1448
+ );
1449
+
1450
+ if (secondary) {
1451
+ console.log("");
1452
+ console.log(
1453
+ ` ${C.grey}Prefer ${secondary.label}? Run ${C.reset}${C.cyan}${secondary.cmd}${C.reset}${C.grey} and type ${C.reset}${C.cyan}${secondary.slash}${C.reset}${C.grey}.${C.reset}`
1454
+ );
1455
+ }
1456
+
1457
+ console.log("");
1458
+ printDivider();
1459
+ console.log("");
1460
+ console.log(
1461
+ ` Verify install: ${C.cyan}sellable --verify-only --host all${C.reset}`
1462
+ );
1463
+ console.log(` Need help?`);
1464
+ console.log(
1465
+ ` Slack: ${C.cyan}https://join.slack.com/t/ditttoai/shared_invite/zt-3wvs86yau-csKZGP3iGXO3oEiAUmtH9A${C.reset}`
1466
+ );
1467
+ console.log(` Email: ${C.cyan}admin@dittto.ai${C.reset}`);
1468
+ console.log("");
1367
1469
  }
1368
1470
 
1369
1471
  async function main() {
@@ -1374,38 +1476,84 @@ async function main() {
1374
1476
  return;
1375
1477
  }
1376
1478
 
1377
- console.log("Sellable installer");
1378
- console.log(`- host: ${opts.host}`);
1379
- console.log(`- server: ${opts.server}`);
1380
- if (opts.server === "package") {
1381
- console.log(`- mcp package: ${opts.mcpPackage}`);
1479
+ VERBOSE = Boolean(opts.verbose);
1480
+
1481
+ if (!opts.verifyOnly) {
1482
+ printBanner();
1483
+ console.log(
1484
+ ` ${C.bold}Connecting Sellable to Claude Code and Codex…${C.reset}`
1485
+ );
1486
+ console.log("");
1487
+ } else {
1488
+ printBanner();
1489
+ console.log(` ${C.bold}Verifying Sellable install…${C.reset}`);
1490
+ console.log("");
1491
+ }
1492
+
1493
+ if (VERBOSE) {
1494
+ logStep(`host: ${opts.host}`);
1495
+ logStep(`server: ${opts.server}`);
1496
+ if (opts.server === "package") logStep(`mcp package: ${opts.mcpPackage}`);
1497
+ logStep(`api: ${opts.apiUrl}`);
1498
+ logStep(`token: ${opts.token ? redact(opts.token) : "(missing)"}`);
1382
1499
  }
1383
- console.log(`- api: ${opts.apiUrl}`);
1384
- console.log(`- token: ${opts.token ? redact(opts.token) : "(missing)"}`);
1385
1500
 
1386
1501
  const installedHosts = [];
1387
1502
  if (!opts.verifyOnly) {
1388
- await promptForMissingAuth(opts);
1389
- writeAuth(opts);
1503
+ await loadAuthIfPresent(opts);
1504
+ const authResult = writeAuth(opts);
1390
1505
  installSelfShim(opts);
1506
+ if (authResult.reused) {
1507
+ logMilestone(`Using existing auth config: ${authPath()}`);
1508
+ } else if (authResult.written) {
1509
+ logMilestone(`Authenticated (workspace: ${opts.workspaceId})`);
1510
+ } else {
1511
+ logMilestone(
1512
+ "Sellable infrastructure ready — sign in happens on first /sellable:create-campaign"
1513
+ );
1514
+ }
1515
+
1391
1516
  if (opts.host === "claude" || opts.host === "all") {
1392
- if (installClaude(opts)) installedHosts.push("Claude Code");
1517
+ if (installClaude(opts)) {
1518
+ installedHosts.push("Claude Code");
1519
+ logMilestone("Claude Code MCP server registered");
1520
+ }
1393
1521
  }
1394
1522
  if (opts.host === "codex" || opts.host === "all") {
1395
- if (installCodex(opts)) installedHosts.push("Codex");
1523
+ const result = installCodex(opts);
1524
+ if (result.installed) {
1525
+ installedHosts.push("Codex");
1526
+ logMilestone("Codex MCP server registered");
1527
+ logMilestone(
1528
+ `Codex Desktop plugin installed (sellable@sellable v${CODEX_PLUGIN_VERSION})`
1529
+ );
1530
+ logMilestone(
1531
+ "Skill bundle: create-campaign, engage, interview, workflow-sequences"
1532
+ );
1533
+ if (result.compatCount) {
1534
+ logMilestone(
1535
+ `Backwards-compat cache linked (${result.compatCount} versions → symlinked, no copy)`
1536
+ );
1537
+ }
1538
+ logMilestone("Codex Default-mode request_user_input enabled");
1539
+ }
1396
1540
  }
1397
1541
  }
1542
+
1398
1543
  if (opts.dryRun) {
1399
- console.log(
1400
- "Dry run complete; verification skipped because no files were written."
1401
- );
1402
- } else {
1544
+ console.log("");
1545
+ logStep("Dry run complete; no files were written.");
1546
+ } else if (opts.verifyOnly) {
1403
1547
  verify(opts);
1404
1548
  }
1405
- console.log("Sellable install complete.");
1406
- if (!opts.verifyOnly && !opts.dryRun) printNextSteps(installedHosts);
1549
+
1550
+ if (!opts.verifyOnly) {
1551
+ printNextSteps(installedHosts);
1552
+ }
1407
1553
  } catch (error) {
1408
- console.error(error instanceof Error ? error.message : String(error));
1554
+ console.error(
1555
+ `${C.yellow}✗${C.reset} ${error instanceof Error ? error.message : String(error)}`
1556
+ );
1409
1557
  process.exitCode = 1;
1410
1558
  }
1411
1559
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.28",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {