@sellable/install 0.1.39 → 0.1.41

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.
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
3
3
  import {
4
4
  existsSync,
5
5
  mkdirSync,
6
+ readdirSync,
6
7
  readFileSync,
7
8
  rmSync,
8
9
  symlinkSync,
@@ -73,6 +74,7 @@ const C = {
73
74
  grey: useColor ? "\x1b[90m" : "",
74
75
  bold: useColor ? "\x1b[1m" : "",
75
76
  magenta: useColor ? "\x1b[35m" : "",
77
+ brand: useColor ? "\x1b[96m" : "",
76
78
  };
77
79
 
78
80
  let VERBOSE = false;
@@ -105,11 +107,11 @@ function printBanner() {
105
107
  " ╚══════╝ ╚══════╝ ╚══════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚══════╝",
106
108
  ];
107
109
  console.log("");
108
- for (const line of lines) console.log(`${C.magenta}${line}${C.reset}`);
110
+ for (const line of lines) console.log(`${C.brand}${line}${C.reset}`);
109
111
  console.log("");
110
112
  } else {
111
113
  console.log("");
112
- console.log(`${C.bold}${C.magenta}SELLABLE${C.reset}`);
114
+ console.log(`${C.bold}${C.brand}SELLABLE${C.reset}`);
113
115
  console.log("");
114
116
  }
115
117
  }
@@ -1382,14 +1384,48 @@ function verify(opts) {
1382
1384
  }
1383
1385
  }
1384
1386
 
1385
- function printNextSteps(installedHosts) {
1386
- console.log("");
1387
- printDivider();
1388
- console.log(` ${C.bold}Done.${C.reset}`);
1389
- printDivider();
1387
+ function visibleLen(s) {
1388
+ // Strip ANSI escape sequences for visible-width calculation.
1389
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
1390
+ }
1391
+
1392
+ function printAgentBox(title, terminalCmd, agentCmd) {
1393
+ // Inner width between │ ... │ — i.e. the printable area excluding the borders.
1394
+ const W = 58;
1395
+ const agentName = title.replace("Using ", "").replace("?", "");
1396
+
1397
+ const line = (text) => {
1398
+ const pad = Math.max(0, W - visibleLen(text));
1399
+ return ` │ ${text}${" ".repeat(pad)} │`;
1400
+ };
1401
+ const blank = () => ` │${" ".repeat(W + 2)}│`;
1402
+
1403
+ const titleColored = `${C.bold}${title}${C.reset}`;
1404
+ const titleVis = visibleLen(titleColored);
1405
+ const top = ` ┌─ ${titleColored} ${"─".repeat(Math.max(0, W - titleVis - 1))}┐`;
1406
+ const bot = ` └${"─".repeat(W + 2)}┘`;
1407
+
1408
+ console.log(top);
1409
+ console.log(blank());
1410
+ console.log(line(` ${C.bold}STEP 1${C.reset} In your terminal, run:`));
1411
+ console.log(blank());
1412
+ console.log(line(` ${C.cyan}${terminalCmd}${C.reset}`));
1413
+ console.log(blank());
1414
+ console.log(line(` ${C.bold}STEP 2${C.reset} Then in ${agentName}, run:`));
1415
+ console.log(blank());
1416
+ console.log(line(` ${C.cyan}${agentCmd}${C.reset}`));
1417
+ console.log(blank());
1418
+ console.log(bot);
1419
+ }
1420
+
1421
+ function printNextSteps(installedHosts, authReused) {
1390
1422
  console.log("");
1391
1423
 
1392
1424
  if (installedHosts.length === 0) {
1425
+ printDivider();
1426
+ console.log(` ${C.bold}Installed.${C.reset}`);
1427
+ printDivider();
1428
+ console.log("");
1393
1429
  console.log(
1394
1430
  ` ${C.yellow}!${C.reset} Neither Claude Code nor Codex was found on this machine.`
1395
1431
  );
@@ -1410,50 +1446,207 @@ function printNextSteps(installedHosts) {
1410
1446
  const hasClaude = installedHosts.includes("Claude Code");
1411
1447
  const hasCodex = installedHosts.includes("Codex");
1412
1448
 
1413
- if (hasClaude && hasCodex) {
1414
- console.log(` ${C.bold}Open Claude Code or Codex and run:${C.reset}`);
1415
- console.log("");
1416
- console.log(
1417
- ` ${C.cyan}/sellable:create-campaign${C.reset} ${C.grey}(Claude Code)${C.reset}`
1418
- );
1449
+ // Header
1450
+ if (authReused) {
1451
+ console.log(` ${C.bold}Installed. Welcome back!${C.reset}`);
1452
+ } else {
1453
+ console.log(` ${C.bold}Installed.${C.reset}`);
1454
+ }
1455
+ console.log(
1456
+ ` ${C.green}✓${C.reset} MCP servers registered (${installedHosts.join(" + ")})`
1457
+ );
1458
+ if (hasCodex) {
1459
+ console.log(` ${C.green}✓${C.reset} Codex Desktop plugin installed`);
1460
+ console.log(` ${C.green}✓${C.reset} Skills installed`);
1461
+ }
1462
+ if (authReused) {
1419
1463
  console.log(
1420
- ` ${C.cyan}$sellable:create-campaign${C.reset} ${C.grey}(Codex)${C.reset}`
1464
+ ` ${C.green}✓${C.reset} Existing session detected — no need to sign in again`
1421
1465
  );
1422
- } else if (hasClaude) {
1423
- console.log(` ${C.bold}Open Claude Code and run:${C.reset}`);
1466
+ }
1467
+ console.log("");
1468
+ console.log("");
1469
+ console.log(` ${"═".repeat(63)}`);
1470
+ console.log(` ${C.bold}To launch your first campaign:${C.reset}`);
1471
+ console.log(` ${"═".repeat(63)}`);
1472
+ console.log("");
1473
+ console.log("");
1474
+
1475
+ if (hasClaude) {
1476
+ printAgentBox("Using Claude Code?", "claude", "/sellable:create-campaign");
1424
1477
  console.log("");
1425
- console.log(` ${C.cyan}/sellable:create-campaign${C.reset}`);
1426
- } else {
1427
- console.log(` ${C.bold}Open Codex and run:${C.reset}`);
1428
1478
  console.log("");
1429
- console.log(` ${C.cyan}$sellable:create-campaign${C.reset}`);
1479
+ }
1480
+ if (hasCodex) {
1481
+ printAgentBox("Using Codex?", "codex", "$sellable:create-campaign");
1482
+ console.log("");
1430
1483
  }
1431
1484
 
1432
1485
  console.log("");
1486
+ console.log(` ${"─".repeat(63)}`);
1487
+ console.log(` ${C.grey}Need help?${C.reset}`);
1433
1488
  console.log(
1434
- ` ${C.grey}First time? The agent will sign you up — just answer in chat.${C.reset}`
1435
- );
1436
-
1437
- console.log("");
1438
- printDivider();
1439
- console.log("");
1440
- console.log(` ${C.grey}Need help?${C.reset}`);
1441
- console.log(
1442
- ` Slack: ${C.cyan}https://join.slack.com/t/ditttoai/shared_invite/zt-3wvs86yau-csKZGP3iGXO3oEiAUmtH9A${C.reset}`
1489
+ ` Slack ${C.cyan}https://join.slack.com/t/ditttoai/shared_invite/zt-3wvs86yau-csKZGP3iGXO3oEiAUmtH9A${C.reset}`
1443
1490
  );
1444
- console.log(` Email: ${C.cyan}admin@dittto.ai${C.reset}`);
1491
+ console.log(` Email ${C.cyan}admin@dittto.ai${C.reset}`);
1445
1492
  console.log(
1446
- ` LinkedIn: ${C.cyan}https://www.linkedin.com/in/csreyes92/${C.reset} ${C.grey}(DM Christian directly)${C.reset}`
1493
+ ` LinkedIn ${C.cyan}https://www.linkedin.com/in/csreyes92/${C.reset} ${C.grey}(DM Christian)${C.reset}`
1447
1494
  );
1448
1495
  console.log("");
1449
1496
  const installV = getInstallVersion();
1450
1497
  const mcpV = getMcpVersion();
1451
1498
  console.log(
1452
- ` ${C.grey}@sellable/install v${installV} · @sellable/mcp v${mcpV} · Codex plugin v${CODEX_PLUGIN_VERSION}${C.reset}`
1499
+ ` ${C.grey}@sellable/install v${installV} · @sellable/mcp v${mcpV} · Codex plugin v${CODEX_PLUGIN_VERSION}${C.reset}`
1453
1500
  );
1454
1501
  console.log("");
1455
1502
  }
1456
1503
 
1504
+ function removeTomlSection(content, header) {
1505
+ // Removes a [section] header and its body, up to (but not including) the next [...] header or EOF.
1506
+ // header is the literal string in brackets, e.g. `mcp_servers.sellable` or `plugins."sellable@sellable"`.
1507
+ const pattern = new RegExp(
1508
+ `\\n*\\[${escapeRegExp(header)}\\][^\\n]*\\n[\\s\\S]*?(?=\\n\\[[^\\n]+\\]|$)`
1509
+ );
1510
+ return content.replace(pattern, "");
1511
+ }
1512
+
1513
+ function runUninstall() {
1514
+ console.log("");
1515
+ console.log(` ${C.bold}Uninstalling Sellable…${C.reset}`);
1516
+ console.log("");
1517
+
1518
+ const removed = [];
1519
+ const preserved = [];
1520
+ const skipped = [];
1521
+
1522
+ // 1) Surgical removal from Codex config.toml
1523
+ const codexConfigPath = join(codexHome(), "config.toml");
1524
+ if (existsSync(codexConfigPath)) {
1525
+ try {
1526
+ const before = readFileSync(codexConfigPath, "utf8");
1527
+ const ts = new Date()
1528
+ .toISOString()
1529
+ .replace(/[-:]/g, "")
1530
+ .replace(/\..+/, "");
1531
+ const backupPath = `${codexConfigPath}.bak-sellable-uninstall-${ts}`;
1532
+ writeFileSync(backupPath, before, { mode: 0o600 });
1533
+
1534
+ let after = before;
1535
+ after = removeTomlSection(after, "mcp_servers.sellable");
1536
+ after = removeTomlSection(after, "marketplaces.sellable");
1537
+ after = removeTomlSection(after, 'plugins."sellable@sellable"');
1538
+ after = removeTomlSection(after, 'plugins."sellable@sellable-local"');
1539
+ // Collapse 3+ blank lines that the removals may leave behind.
1540
+ after = after.replace(/\n{3,}/g, "\n\n");
1541
+ if (after !== before) {
1542
+ writeFileSync(codexConfigPath, after, { mode: 0o600 });
1543
+ removed.push(
1544
+ `Codex config (sellable sections removed in ${codexConfigPath})`
1545
+ );
1546
+ removed.push(` backup: ${backupPath}`);
1547
+ } else {
1548
+ skipped.push(`Codex config had no sellable sections`);
1549
+ }
1550
+ } catch (err) {
1551
+ console.log(
1552
+ ` ${C.yellow}!${C.reset} Could not edit Codex config: ${err instanceof Error ? err.message : String(err)}`
1553
+ );
1554
+ }
1555
+ } else {
1556
+ skipped.push(`Codex config not found at ${codexConfigPath}`);
1557
+ }
1558
+
1559
+ // 2) Claude Code MCP removal
1560
+ if (commandExists("claude")) {
1561
+ const r = spawnSync("claude", ["mcp", "remove", "sellable"], {
1562
+ encoding: "utf8",
1563
+ });
1564
+ if (r.status === 0) {
1565
+ removed.push(`Claude Code MCP (sellable)`);
1566
+ } else {
1567
+ skipped.push(`Claude Code MCP (none registered or already removed)`);
1568
+ }
1569
+ } else {
1570
+ skipped.push(`Claude Code CLI not found`);
1571
+ }
1572
+
1573
+ // 3) Surgical removal of Sellable-installed artifacts inside ~/.sellable/
1574
+ const sellableDir = join(homedir(), ".sellable");
1575
+ if (existsSync(sellableDir)) {
1576
+ const tracked = [
1577
+ "config.json",
1578
+ "update-check.json",
1579
+ "codex-marketplace",
1580
+ "create-campaign-v2",
1581
+ ];
1582
+ for (const name of tracked) {
1583
+ const p = join(sellableDir, name);
1584
+ if (existsSync(p)) {
1585
+ try {
1586
+ rmSync(p, { recursive: true, force: true });
1587
+ removed.push(`${p}`);
1588
+ } catch (err) {
1589
+ console.log(
1590
+ ` ${C.yellow}!${C.reset} Could not remove ${p}: ${err instanceof Error ? err.message : String(err)}`
1591
+ );
1592
+ }
1593
+ }
1594
+ }
1595
+ // Inventory anything left — these are user-authored and we leave them.
1596
+ let leftovers = [];
1597
+ try {
1598
+ leftovers = readdirSync(sellableDir);
1599
+ } catch {
1600
+ // ignore
1601
+ }
1602
+ if (leftovers.length > 0) {
1603
+ for (const name of leftovers) {
1604
+ preserved.push(`${join(sellableDir, name)}`);
1605
+ }
1606
+ } else {
1607
+ try {
1608
+ rmSync(sellableDir, { recursive: false, force: false });
1609
+ removed.push(`${sellableDir} (empty)`);
1610
+ } catch {
1611
+ // ignore
1612
+ }
1613
+ }
1614
+ } else {
1615
+ skipped.push(`~/.sellable not present`);
1616
+ }
1617
+
1618
+ // 4) Self-shim
1619
+ const shimPath = join(homedir(), ".local", "bin", "sellable");
1620
+ if (existsSync(shimPath)) {
1621
+ try {
1622
+ rmSync(shimPath, { force: true });
1623
+ removed.push(`${shimPath}`);
1624
+ } catch {
1625
+ // ignore
1626
+ }
1627
+ }
1628
+
1629
+ // Print receipt
1630
+ for (const r of removed) {
1631
+ console.log(` ${C.green}✓${C.reset} Removed ${r}`);
1632
+ }
1633
+ for (const p of preserved) {
1634
+ console.log(
1635
+ ` ${C.cyan}•${C.reset} Preserved ${p} ${C.grey}(user-authored)${C.reset}`
1636
+ );
1637
+ }
1638
+ for (const s of skipped) {
1639
+ console.log(` ${C.grey}·${C.reset} Skipped ${s}`);
1640
+ }
1641
+
1642
+ console.log("");
1643
+ console.log(` ${C.bold}Uninstalled.${C.reset}`);
1644
+ console.log("");
1645
+ console.log(` Reinstall anytime:`);
1646
+ console.log(` ${C.cyan}npx -y @sellable/install@latest${C.reset}`);
1647
+ console.log("");
1648
+ }
1649
+
1457
1650
  async function main() {
1458
1651
  try {
1459
1652
  // Phase 116: auth set <token> subcommand — manual-paste fallback when CLI poll fails.
@@ -1498,6 +1691,10 @@ async function main() {
1498
1691
  console.log(` Run /sellable:create-campaign in your agent to continue.`);
1499
1692
  process.exit(0);
1500
1693
  }
1694
+ if (rawArgs[0] === "uninstall") {
1695
+ runUninstall();
1696
+ process.exit(0);
1697
+ }
1501
1698
  const opts = parseArgs(process.argv.slice(2));
1502
1699
  if (opts.help) {
1503
1700
  console.log(usage());
@@ -1527,19 +1724,12 @@ async function main() {
1527
1724
  }
1528
1725
 
1529
1726
  const installedHosts = [];
1727
+ let authReused = false;
1530
1728
  if (!opts.verifyOnly) {
1531
1729
  await loadAuthIfPresent(opts);
1532
1730
  const authResult = writeAuth(opts);
1731
+ authReused = Boolean(authResult.reused);
1533
1732
  installSelfShim(opts);
1534
- if (authResult.reused) {
1535
- logMilestone("Auth ready");
1536
- } else if (authResult.written) {
1537
- logMilestone(`Authenticated (workspace: ${opts.workspaceId})`);
1538
- } else {
1539
- logMilestone(
1540
- "Sellable infrastructure ready — sign in happens on first /sellable:create-campaign"
1541
- );
1542
- }
1543
1733
 
1544
1734
  if (opts.host === "claude" || opts.host === "all") {
1545
1735
  if (installClaude(opts)) {
@@ -1552,14 +1742,6 @@ async function main() {
1552
1742
  installedHosts.push("Codex");
1553
1743
  }
1554
1744
  }
1555
-
1556
- if (installedHosts.length > 0) {
1557
- logMilestone(`MCP servers registered (${installedHosts.join(" + ")})`);
1558
- if (installedHosts.includes("Codex")) {
1559
- logMilestone("Codex Desktop plugin installed");
1560
- logMilestone("Skills installed");
1561
- }
1562
- }
1563
1745
  }
1564
1746
 
1565
1747
  if (opts.dryRun) {
@@ -1570,7 +1752,7 @@ async function main() {
1570
1752
  }
1571
1753
 
1572
1754
  if (!opts.verifyOnly) {
1573
- printNextSteps(installedHosts);
1755
+ printNextSteps(installedHosts, authReused);
1574
1756
  }
1575
1757
  } catch (error) {
1576
1758
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {