sella-cli 0.5.0 → 0.6.1

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
@@ -1,21 +1,25 @@
1
1
  # sella-cli
2
2
 
3
- Terminal onboarding for [Sella](https://sella.network) — the marketplace where agents buy datasets,
3
+ Terminal onboarding for [Sella](https://sellag.vercel.app) — the marketplace where agents buy datasets,
4
4
  models, and machine-payable APIs.
5
5
 
6
6
  ```bash
7
- npx sella-cli init
7
+ npx sella-cli
8
8
  ```
9
9
 
10
- `init` detects your agent clients (Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, Cline),
11
- installs the Sella MCP server into each, and walks you through pairing. Installed users get the
12
- `sella` binary.
10
+ That single command is the whole onboarding: with no stored credentials and a human at the
11
+ terminal, the guided wizard starts by itself (`init` is the explicit spelling). It previews the
12
+ marketplace, detects your agent clients (Claude Code, Claude Desktop, Cursor, Windsurf, VS Code,
13
+ Cline), installs the Sella MCP server into each, pairs the machine (arrow-key choice of setup code
14
+ or email + 6-digit code), wires your key into the client configs, and finishes with a live
15
+ `doctor` verification. Installed users get the `sella` binary; `npm install` also prints a
16
+ one-line pointer after install (silent in CI and non-TTY runs — installs are never interrupted).
13
17
 
14
18
  ## Commands
15
19
 
16
20
  | Command | What it does |
17
21
  |---|---|
18
- | `sella init` | Detect clients → install the Sella MCP server → pair → verify |
22
+ | `sella init` | Guided wizard: detect clients → install the Sella MCP server → pair → verify (auto-runs on bare `sella` when unpaired) |
19
23
  | `sella sandbox <query>` | Try Sella with **no signup** — search the live marketplace (rate-limited) |
20
24
  | `sella pair` | Pair this machine only (`--setup-code <code>` / `SELLA_SETUP_CODE`, or `--email` + OTP) |
21
25
  | `sella clients` | List detected clients (`--install` to write configs, `--client a,b` to filter, `--dry-run`) |
@@ -34,7 +38,8 @@ your wallets, just without the label.
34
38
  ## Contract (every command)
35
39
 
36
40
  - `--json` → exactly one JSON document on stdout; errors on stderr; meaningful exit codes.
37
- - `--yes` / env vars for every prompt — agents run this CLI too; nothing may hang on a TTY.
41
+ - `--yes` / env vars for every prompt — agents run this CLI too; nothing may hang on a TTY (the
42
+ wizard renders as plain sequential lines off-TTY and never auto-starts in `--yes`/`--json` runs).
38
43
  - `NO_COLOR` / `--no-color` honored; no spinner-only feedback; color never carries meaning alone.
39
44
  - Config writes are idempotent, preserve other servers, and leave a one-time `.sella-backup`.
40
45
  - `SELLA_MCP_URL` overrides the endpoint (self-hosted / staging).
package/dist/fund.js CHANGED
@@ -2,9 +2,9 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { fetchChainRegistry, chainCapability } from './chains.js';
4
4
  function platformOrigin(mcpUrl) {
5
- return mcpUrl.replace(/\/api\/mcp\/?$/, '') || 'https://sella.network';
5
+ return mcpUrl.replace(/\/api\/mcp\/?$/, '') || 'https://sellag.vercel.app';
6
6
  }
7
- export function getFundingInfo(env, mcpUrl = 'https://sella.network/api/mcp') {
7
+ export function getFundingInfo(env, mcpUrl = 'https://sellag.vercel.app/api/mcp') {
8
8
  const fundingUrl = `${platformOrigin(mcpUrl)}/dashboard/funding`;
9
9
  const configPath = path.join(env.home, '.sella-agentwallet', 'config.json');
10
10
  let config = null;
package/dist/index.js CHANGED
@@ -10,8 +10,9 @@ import { sandboxSearch } from './api.js';
10
10
  import { getFundingInfo, annotateFunding } from './fund.js';
11
11
  import { capabilityLabel } from './chains.js';
12
12
  import { defaultIo, Printer } from './output.js';
13
- const DEFAULT_MCP_URL = 'https://sella.network/api/mcp';
14
- const VERSION = '0.5.0';
13
+ import { Ui } from './ui.js';
14
+ const DEFAULT_MCP_URL = 'https://sellag.vercel.app/api/mcp';
15
+ const VERSION = '0.6.1';
15
16
  function parseFlags(argv) {
16
17
  const flags = {
17
18
  json: false, yes: false, noColor: false, dryRun: false, noKeychain: false,
@@ -53,7 +54,8 @@ const HELP = `sella — the Sella onboarding CLI
53
54
  Usage: sella <command> [options]
54
55
 
55
56
  Commands:
56
- init Install the Sella MCP server into your agent clients, pair, done
57
+ init Guided onboarding: install the Sella MCP server, pair, verify
58
+ (also runs when you invoke \`sella\` with no command and no credentials yet)
57
59
  pair Pair this machine only (setup code or email + OTP)
58
60
  sandbox Try Sella with no signup — search the live marketplace (rate-limited)
59
61
  clients List detected agent clients (--install to write configs)
@@ -87,7 +89,16 @@ function selectClientIds(flags, detected) {
87
89
  }
88
90
  return detected.filter((c) => c.installed).map((c) => c.id);
89
91
  }
90
- function installAll(ctx, flags, printer, apiKey) {
92
+ function uiEmitter(ui, printer) {
93
+ return {
94
+ info: (t) => ui.step(t),
95
+ ok: (t) => ui.ok(t),
96
+ warn: (t) => ui.warn(t),
97
+ // Errors always reach stderr too (screen readers, log capture, --json pipelines).
98
+ error: (t) => printer.error(t),
99
+ };
100
+ }
101
+ function installAll(ctx, flags, emit, apiKey) {
91
102
  const detected = detectClients(ctx.env);
92
103
  const targets = selectClientIds(flags, detected);
93
104
  const results = [];
@@ -96,13 +107,13 @@ function installAll(ctx, flags, printer, apiKey) {
96
107
  try {
97
108
  const result = installSellaIntoClient(id, { mcpUrl: ctx.mcpUrl, env: ctx.env, dryRun: flags.dryRun, apiKey });
98
109
  results.push({ ...result });
99
- printer.ok(`${id}: ${flags.dryRun ? `would be ${result.action}` : result.action} → ${result.configPath}`);
110
+ emit.ok(`${id}: ${flags.dryRun ? `would be ${result.action}` : result.action} → ${result.configPath}`);
100
111
  }
101
112
  catch (err) {
102
113
  failed = true;
103
114
  const message = err instanceof Error ? err.message : String(err);
104
115
  results.push({ id, error: message });
105
- printer.error(`${id}: ${message}`);
116
+ emit.error(`${id}: ${message}`);
106
117
  }
107
118
  }
108
119
  return { results, failed, targets };
@@ -120,26 +131,26 @@ function makeAsk(ctx) {
120
131
  }
121
132
  };
122
133
  }
123
- async function runPair(ctx, flags, printer) {
134
+ async function runPair(ctx, flags, emit, extra = {}) {
124
135
  const outcome = await pair({
125
136
  mcpUrl: ctx.mcpUrl,
126
137
  env: ctx.env,
127
138
  interactive: ctx.io.isTTY && !flags.yes && !flags.json,
128
- setupCode: flags.setupCode || ctx.io.env.SELLA_SETUP_CODE,
129
- email: flags.email,
139
+ setupCode: extra.setupCode ?? (flags.setupCode || ctx.io.env.SELLA_SETUP_CODE),
140
+ email: extra.email ?? flags.email,
130
141
  allowKeychain: !flags.noKeychain && !ctx.io.env.SELLA_NO_KEYCHAIN,
131
- ask: makeAsk(ctx),
142
+ ask: extra.ask ?? makeAsk(ctx),
132
143
  });
133
144
  if (!outcome.ok) {
134
- printer.error(outcome.error);
145
+ emit.error(outcome.error);
135
146
  return { code: outcome.exitCode, summary: { paired: false, error: outcome.error } };
136
147
  }
137
- printer.ok(`Paired via ${outcome.method} — status: ${outcome.payload.status || 'ok'}`);
148
+ emit.ok(`Paired via ${outcome.method} — status: ${outcome.payload.status || 'ok'}`);
138
149
  for (const file of outcome.save.files)
139
- printer.ok(`wrote ${file}`);
140
- printer.info(outcome.save.custodyNote);
150
+ emit.ok(`wrote ${file}`);
151
+ emit.info(outcome.save.custodyNote);
141
152
  if (outcome.payload.agentWallet?.status === 'unavailable') {
142
- printer.warn(`AgentWallet not provisioned (${outcome.payload.agentWallet.reason || 'backend unavailable'}) — MCP access still works; re-run \`sella pair\` later.`);
153
+ emit.warn(`AgentWallet not provisioned (${outcome.payload.agentWallet.reason || 'backend unavailable'}) — MCP access still works; re-run \`sella pair\` later.`);
143
154
  }
144
155
  return {
145
156
  code: 0,
@@ -154,43 +165,129 @@ async function runPair(ctx, flags, printer) {
154
165
  },
155
166
  };
156
167
  }
157
- async function previewSandbox(ctx, flags, printer) {
158
- if (flags.json)
159
- return; // cosmetic; skip in machine mode
168
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
169
+ /**
170
+ * Wizard input collection: honors --setup-code/--email/SELLA_SETUP_CODE first (identical to the
171
+ * headless path), and only prompts when a human is present.
172
+ */
173
+ async function collectPairChoice(ui, flags, ctx) {
174
+ const preset = flags.setupCode || ctx.io.env.SELLA_SETUP_CODE;
175
+ if (preset || flags.email || !ui.interactive)
176
+ return { setupCode: preset, email: flags.email };
177
+ const method = await ui.select('How do you want to pair this machine?', [
178
+ { value: 'setup-code', label: 'Setup code', hint: 'paste one from the Sella dashboard' },
179
+ { value: 'email-otp', label: 'Email + code', hint: 'we email you a 6-digit code' },
180
+ ]);
181
+ if (method === 'setup-code') {
182
+ const code = await ui.text('Paste your setup code', {
183
+ placeholder: 'SELLA-XXXX-XXXX-XXXX-XXXX',
184
+ validate: (v) => (v ? undefined : 'A setup code is required — mint one on the dashboard (Connect your agent).'),
185
+ });
186
+ return { setupCode: code };
187
+ }
188
+ const email = await ui.text('Email for your Sella account', {
189
+ validate: (v) => (EMAIL_RE.test(v) ? undefined : 'Enter a valid email address.'),
190
+ });
191
+ return { email };
192
+ }
193
+ /** The one prompt pair() asks itself (the emailed OTP) — styled when the wizard is active. */
194
+ function wizardAsk(ui, ctx) {
195
+ if (!ui.interactive)
196
+ return makeAsk(ctx);
197
+ return async (question) => {
198
+ if (/6-digit/i.test(question)) {
199
+ return ui.text('6-digit code from your inbox', {
200
+ validate: (v) => (/^\d{6}$/.test(v) ? undefined : 'Enter exactly the 6 digits from the email.'),
201
+ });
202
+ }
203
+ return ui.text(question.replace(/:\s*$/, ''));
204
+ };
205
+ }
206
+ async function cmdInit(ctx, flags, printer) {
207
+ const ui = new Ui(ctx.io, { json: flags.json, noColor: flags.noColor, yes: flags.yes });
208
+ const emit = ui.pretty ? uiEmitter(ui, printer) : printer;
209
+ const origin = ctx.mcpUrl.replace(/\/api\/mcp\/?$/, '') || 'https://sellag.vercel.app';
210
+ let host = ctx.mcpUrl;
211
+ try {
212
+ host = new URL(ctx.mcpUrl).host;
213
+ }
214
+ catch {
215
+ /* keep full URL */
216
+ }
217
+ ui.intro('Sella — connect your agents', `v${VERSION} · ${host}`);
218
+ // A taste of the marketplace (free sandbox, best-effort — never blocks onboarding).
219
+ const taste = ui.spinner('Peeking at the marketplace (free, no signup)…');
160
220
  try {
161
221
  const result = await sandboxSearch(ctx.mcpUrl, 'data');
162
- const n = result.datasets.length + result.apis.length;
163
- if (n === 0)
164
- return;
165
- printer.info('Here’s a taste of what your agent can buy on Sella (no signup needed):');
166
- for (const d of result.datasets.slice(0, 2))
167
- printer.info(` dataset ${d.title || d.id}`);
168
- for (const a of result.apis.slice(0, 2))
169
- printer.info(` api ${a.name || ''}`);
170
- printer.info('');
222
+ const picks = [
223
+ ...result.datasets.slice(0, 2).map((d) => `dataset ${d.title || d.id}`),
224
+ ...result.apis.slice(0, 2).map((a) => `api ${a.name || ''}`),
225
+ ];
226
+ taste.succeed(picks.length ? 'A taste of what your agent can buy here:' : 'Marketplace preview is empty right now.');
227
+ for (const p of picks)
228
+ ui.detail(p);
171
229
  }
172
230
  catch {
173
- /* best-effort — never blocks onboarding */
231
+ taste.succeed('Marketplace preview skipped.');
174
232
  }
175
- }
176
- async function cmdInit(ctx, flags, printer) {
177
- printer.info('Sella init 1/3 install, 2/3 pair, 3/3 verify.\n');
178
- await previewSandbox(ctx, flags, printer);
179
- const install = installAll(ctx, flags, printer);
180
- if (install.targets.length === 0) {
181
- printer.error('No agent clients found (and none specified via --client). Nothing to install into.');
233
+ ui.bar();
234
+ // Step 1/3 install
235
+ const detected = detectClients(ctx.env);
236
+ const targets = selectClientIds(flags, detected);
237
+ if (targets.length === 0) {
238
+ emit.error('No agent clients found (and none specified via --client). Nothing to install into.');
182
239
  printer.jsonOut({ installed: [], paired: false, error: 'no_clients' });
240
+ ui.outro('Nothing to do.');
183
241
  return 2;
184
242
  }
185
- printer.info('\nPairing this machine…');
186
- const paired = await runPair(ctx, flags, printer);
243
+ ui.step(`Step 1/3 · Install the Sella MCP server (${targets.join(', ')})`);
244
+ const install = installAll(ctx, flags, emit);
245
+ ui.bar();
246
+ // Step 2/3 — pair
247
+ ui.step('Step 2/3 · Pair this machine with your Sella account');
248
+ const choice = await collectPairChoice(ui, flags, ctx);
249
+ if (choice.email)
250
+ ui.step(`Requesting a verification code for ${choice.email}…`);
251
+ const paired = await runPair(ctx, flags, emit, { ...choice, ask: wizardAsk(ui, ctx) });
187
252
  let reinstalled = [];
188
253
  if (paired.code === 0 && paired.apiKey && !flags.dryRun) {
189
- reinstalled = installAll(ctx, flags, printer, paired.apiKey).results;
190
- printer.info('\nDone. Your agent clients are connected to Sella — fund your wallet to enable paid calls:');
191
- printer.info(' https://sella.network/dashboard/funding');
254
+ ui.bar();
255
+ ui.step('Wiring your API key into the client configs');
256
+ reinstalled = installAll(ctx, flags, emit, paired.apiKey).results;
257
+ }
258
+ // Step 3/3 — verify (report-only: exit codes stay install/pair-driven so automation is stable)
259
+ let verifySummary;
260
+ if (paired.code === 0 && !flags.dryRun) {
261
+ ui.bar();
262
+ const sp = ui.spinner('Step 3/3 · Verifying the install (sella doctor)…');
263
+ try {
264
+ const verify = await runDoctor({ mcpUrl: ctx.mcpUrl, env: ctx.env });
265
+ const passed = verify.checks.filter((c) => c.ok).length;
266
+ sp.succeed(`Step 3/3 · Verified — ${passed}/${verify.checks.length} doctor checks passed`);
267
+ for (const check of verify.checks.filter((c) => !c.ok)) {
268
+ ui.warn(`${check.label} — ${check.detail}`);
269
+ if (check.fix)
270
+ ui.detail(`fix: ${check.fix}`);
271
+ }
272
+ verifySummary = { ok: verify.ok, passed, total: verify.checks.length };
273
+ }
274
+ catch (err) {
275
+ sp.fail('Step 3/3 · Verify could not run (network?) — try `sella doctor` later.');
276
+ verifySummary = { ok: false, error: err instanceof Error ? err.message : String(err) };
277
+ }
192
278
  }
193
- printer.jsonOut({ installed: install.results, pairing: paired.summary, reinstalled });
279
+ if (paired.code === 0) {
280
+ ui.note('Sella is connected', [
281
+ `Fund your agent wallet: ${origin}/dashboard/funding`,
282
+ 'Try it free: sella sandbox "web search"',
283
+ 'Health check: sella doctor',
284
+ ]);
285
+ ui.outro(flags.dryRun ? 'Dry run complete — nothing was written.' : 'Done — your agents can now buy on Sella.');
286
+ }
287
+ else {
288
+ ui.outro('Pairing incomplete — fix the issue above, then re-run `sella init`.');
289
+ }
290
+ printer.jsonOut({ installed: install.results, pairing: paired.summary, reinstalled, verify: verifySummary });
194
291
  return install.failed ? 1 : paired.code;
195
292
  }
196
293
  export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
@@ -202,7 +299,19 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
202
299
  io.stdout(VERSION);
203
300
  return 0;
204
301
  }
205
- if (flags.help || !command) {
302
+ if (flags.help) {
303
+ io.stdout(HELP);
304
+ return 0;
305
+ }
306
+ if (!command) {
307
+ // Bare `sella` / `npx sella-cli`: a human at a TTY with no credentials yet gets the
308
+ // onboarding wizard directly (that IS the task); everyone else gets help. Never in
309
+ // --json/--yes runs — automation must not fall into an interactive flow.
310
+ const interactive = io.isTTY && Boolean(process.stdin.isTTY) && !flags.json && !flags.yes;
311
+ if (interactive && !loadStoredKey(ctx.env)) {
312
+ printer.info('No Sella credentials on this machine yet — starting onboarding (Ctrl+C to abort, `sella --help` for commands).');
313
+ return await cmdInit(ctx, flags, printer);
314
+ }
206
315
  io.stdout(HELP);
207
316
  return 0;
208
317
  }
@@ -230,7 +339,21 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
230
339
  return failed ? 1 : 0;
231
340
  }
232
341
  case 'pair': {
233
- const paired = await runPair(ctx, flags, printer);
342
+ const ui = new Ui(ctx.io, { json: flags.json, noColor: flags.noColor, yes: flags.yes });
343
+ const emit = ui.pretty ? uiEmitter(ui, printer) : printer;
344
+ let host = ctx.mcpUrl;
345
+ try {
346
+ host = new URL(ctx.mcpUrl).host;
347
+ }
348
+ catch {
349
+ /* keep full URL */
350
+ }
351
+ ui.intro('Sella — pair this machine', `v${VERSION} · ${host}`);
352
+ const choice = await collectPairChoice(ui, flags, ctx);
353
+ if (choice.email)
354
+ ui.step(`Requesting a verification code for ${choice.email}…`);
355
+ const paired = await runPair(ctx, flags, emit, { ...choice, ask: wizardAsk(ui, ctx) });
356
+ ui.outro(paired.code === 0 ? 'Paired — try `sella doctor` next.' : 'Pairing failed — see above.');
234
357
  printer.jsonOut(paired.summary || {});
235
358
  return paired.code;
236
359
  }
@@ -319,7 +442,7 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
319
442
  }
320
443
  case 'publish': {
321
444
  const sub = flags.positional[1];
322
- const origin = ctx.mcpUrl.replace(/\/api\/mcp\/?$/, '') || 'https://sella.network';
445
+ const origin = ctx.mcpUrl.replace(/\/api\/mcp\/?$/, '') || 'https://sellag.vercel.app';
323
446
  if (sub === 'init') {
324
447
  const csv = flags.positional.slice(2).find((s) => !s.startsWith('--'));
325
448
  if (!csv) {
package/dist/pairing.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { claimSetupCode, completeEmailOtp, startEmailOtp } from './api.js';
2
2
  import { saveCredentials } from './credentials.js';
3
3
  const NO_TTY_HELP = 'Non-interactive run without credentials. Pass --setup-code <SELLA-…>, set SELLA_SETUP_CODE, ' +
4
- 'or run interactively. Mint a code at https://sella.network/onboarding (Connect your agent).';
4
+ 'or run interactively. Mint a code at https://sellag.vercel.app/onboarding (Connect your agent).';
5
5
  export async function pair(opts) {
6
6
  const finish = (payload, method) => {
7
7
  if (!payload.ok || !payload.apiKey) {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * npm postinstall pointer (CLI sprint C7). Constraints that shape this file:
3
+ * - npm ≥7 runs dependency lifecycle scripts with piped, usually-hidden output, and CI runs
4
+ * must NEVER gain an interactive step — so we do not auto-launch the wizard here.
5
+ * - Instead the wizard auto-starts on the first bare `sella` / `npx sella-cli` run with no
6
+ * stored credentials (see index.ts). This script only prints a one-glance pointer when a
7
+ * human terminal will actually show it.
8
+ * - It must never fail or block an install: everything is wrapped, and we always exit 0.
9
+ */
10
+ function main() {
11
+ const env = process.env;
12
+ const ci = Boolean(env.CI || env.CONTINUOUS_INTEGRATION || env.GITHUB_ACTIONS || env.BUILD_NUMBER || env.TEAMCITY_VERSION);
13
+ if (ci || !process.stdout.isTTY)
14
+ return;
15
+ const globalInstall = env.npm_config_global === 'true';
16
+ const cmd = globalInstall ? 'sella' : 'npx sella';
17
+ const color = !env.NO_COLOR;
18
+ const dim = (s) => (color ? `\x1b[90m${s}\x1b[0m` : s);
19
+ const bold = (s) => (color ? `\x1b[1m${s}\x1b[0m` : s);
20
+ process.stdout.write([
21
+ '',
22
+ ` ${bold('sella-cli installed.')}`,
23
+ ` Finish onboarding (installs the MCP server into your agent clients, pairs, verifies):`,
24
+ '',
25
+ ` ${bold(cmd)}`,
26
+ '',
27
+ ` ${dim('Docs: https://sellag.vercel.app · Try without an account: `' + cmd + ' sandbox "web search"`')}`,
28
+ '',
29
+ ].join('\n'));
30
+ }
31
+ try {
32
+ main();
33
+ }
34
+ catch {
35
+ /* a postinstall pointer must never break an install */
36
+ }
37
+ process.exit(0);
38
+ export {};
package/dist/ui.js ADDED
@@ -0,0 +1,310 @@
1
+ import * as readline from 'node:readline/promises';
2
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
3
+ let cursorHooked = false;
4
+ function ensureCursorRestoredOnExit() {
5
+ if (cursorHooked)
6
+ return;
7
+ cursorHooked = true;
8
+ process.on('exit', () => {
9
+ if (process.stdout.isTTY)
10
+ process.stdout.write('\x1b[?25h');
11
+ });
12
+ }
13
+ export class Ui {
14
+ io;
15
+ /** Glyph/ANSI rendering is worth doing (real TTY, not machine output). */
16
+ pretty;
17
+ /** Prompts are allowed (pretty or plain — but a human is present). */
18
+ interactive;
19
+ color;
20
+ quiet;
21
+ constructor(io, opts) {
22
+ this.io = io;
23
+ this.quiet = opts.json;
24
+ this.pretty = io.isTTY && Boolean(process.stdout.isTTY) && !opts.json;
25
+ this.color = this.pretty && !opts.noColor && !io.env.NO_COLOR;
26
+ this.interactive = io.isTTY && Boolean(process.stdin.isTTY) && !opts.json && !opts.yes;
27
+ }
28
+ // ── paint helpers ──────────────────────────────────────────────────────────
29
+ paint(code, text) {
30
+ return this.color ? `\x1b[${code}m${text}\x1b[0m` : text;
31
+ }
32
+ dim(text) {
33
+ return this.paint('90', text);
34
+ }
35
+ accent(text) {
36
+ return this.paint('36', text);
37
+ }
38
+ width() {
39
+ return Math.max(40, Math.min(process.stdout.columns || 80, 100));
40
+ }
41
+ out(text) {
42
+ if (!this.quiet)
43
+ this.io.stdout(text);
44
+ }
45
+ // ── static rendering ───────────────────────────────────────────────────────
46
+ intro(title, meta) {
47
+ if (this.quiet)
48
+ return;
49
+ if (!this.pretty) {
50
+ this.out(`${title}${meta ? ` (${meta})` : ''}`);
51
+ return;
52
+ }
53
+ this.out('');
54
+ this.out(`${this.dim('┌')} ${this.paint('1', title)}${meta ? ` ${this.dim(meta)}` : ''}`);
55
+ this.bar();
56
+ }
57
+ outro(text) {
58
+ if (this.quiet)
59
+ return;
60
+ if (!this.pretty) {
61
+ this.out(text);
62
+ return;
63
+ }
64
+ this.out(`${this.dim('└')} ${text}`);
65
+ this.out('');
66
+ }
67
+ bar() {
68
+ if (!this.quiet && this.pretty)
69
+ this.out(this.dim('│'));
70
+ }
71
+ step(text) {
72
+ if (this.quiet)
73
+ return;
74
+ this.out(this.pretty ? `${this.dim('◇')} ${text}` : text);
75
+ }
76
+ ok(text) {
77
+ if (this.quiet)
78
+ return;
79
+ this.out(this.pretty ? `${this.paint('32', '◇')} ${text}` : `ok ${text}`);
80
+ }
81
+ warn(text) {
82
+ if (this.quiet)
83
+ return;
84
+ this.out(this.pretty ? `${this.paint('33', '▲')} ${text}` : `warn ${text}`);
85
+ }
86
+ fail(text) {
87
+ if (this.quiet)
88
+ return;
89
+ this.out(this.pretty ? `${this.paint('31', '✕')} ${text}` : `error ${text}`);
90
+ }
91
+ /** Indented supporting line under a step. */
92
+ detail(text) {
93
+ if (this.quiet)
94
+ return;
95
+ this.out(this.pretty ? `${this.dim('│')} ${text}` : ` ${text}`);
96
+ }
97
+ /** Boxed panel for the final summary / next steps. */
98
+ note(title, lines) {
99
+ if (this.quiet)
100
+ return;
101
+ if (!this.pretty) {
102
+ this.out('');
103
+ this.out(title);
104
+ for (const line of lines)
105
+ this.out(` ${line}`);
106
+ return;
107
+ }
108
+ const inner = this.width() - 6;
109
+ const wrapped = [];
110
+ for (const line of lines) {
111
+ if (line.length <= inner) {
112
+ wrapped.push(line);
113
+ continue;
114
+ }
115
+ let rest = line;
116
+ while (rest.length > inner) {
117
+ const cut = rest.lastIndexOf(' ', inner);
118
+ const at = cut > inner / 2 ? cut : inner;
119
+ wrapped.push(rest.slice(0, at));
120
+ rest = rest.slice(at).trimStart();
121
+ }
122
+ if (rest)
123
+ wrapped.push(rest);
124
+ }
125
+ const w = Math.min(this.width() - 2, Math.max(title.length, ...wrapped.map((l) => l.length), 20) + 4);
126
+ this.bar();
127
+ this.out(`${this.dim('╭' + '─'.repeat(w) + '╮')}`);
128
+ // Pad from the raw string length, then paint — ANSI codes must not count toward width.
129
+ const pad = (s, painted = s) => `${this.dim('│')} ${painted}${' '.repeat(Math.max(0, w - s.length - 2))}${this.dim('│')}`;
130
+ this.out(pad(title, this.paint('1', title)));
131
+ for (const line of wrapped)
132
+ this.out(pad(line));
133
+ this.out(`${this.dim('╰' + '─'.repeat(w) + '╯')}`);
134
+ }
135
+ // ── spinner ────────────────────────────────────────────────────────────────
136
+ spinner(label) {
137
+ if (this.quiet) {
138
+ return { update: () => { }, succeed: () => { }, fail: () => { } };
139
+ }
140
+ if (!this.pretty) {
141
+ this.out(`… ${label}`);
142
+ return {
143
+ update: () => { },
144
+ succeed: (text) => this.ok(text || label),
145
+ fail: (text) => this.fail(text || label),
146
+ };
147
+ }
148
+ ensureCursorRestoredOnExit();
149
+ let current = label;
150
+ let i = 0;
151
+ process.stdout.write('\x1b[?25l');
152
+ const render = () => {
153
+ const frame = this.accent(FRAMES[i % FRAMES.length]);
154
+ process.stdout.write(`\r\x1b[K${frame} ${current}`);
155
+ i += 1;
156
+ };
157
+ render();
158
+ const timer = setInterval(render, 80);
159
+ const finish = (glyphCode, glyph, text) => {
160
+ clearInterval(timer);
161
+ process.stdout.write(`\r\x1b[K`);
162
+ process.stdout.write('\x1b[?25h');
163
+ this.out(`${this.paint(glyphCode, glyph)} ${text || current}`);
164
+ };
165
+ return {
166
+ update: (text) => {
167
+ current = text;
168
+ },
169
+ succeed: (text) => finish('32', '◇', text),
170
+ fail: (text) => finish('31', '✕', text),
171
+ };
172
+ }
173
+ // ── prompts ────────────────────────────────────────────────────────────────
174
+ async plainQuestion(prompt) {
175
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
176
+ try {
177
+ return await rl.question(prompt);
178
+ }
179
+ finally {
180
+ rl.close();
181
+ }
182
+ }
183
+ async select(label, options, initial = 0) {
184
+ if (!this.interactive)
185
+ throw new Error(`Cannot prompt for "${label}" in a non-interactive run.`);
186
+ if (!this.pretty) {
187
+ this.out(label);
188
+ options.forEach((o, idx) => this.out(` ${idx + 1}) ${o.label}${o.hint ? ` — ${o.hint}` : ''}`));
189
+ for (;;) {
190
+ const answer = (await this.plainQuestion(`Choose 1-${options.length} (${initial + 1}): `)).trim();
191
+ if (!answer)
192
+ return options[initial].value;
193
+ const n = Number(answer);
194
+ if (Number.isInteger(n) && n >= 1 && n <= options.length)
195
+ return options[n - 1].value;
196
+ }
197
+ }
198
+ ensureCursorRestoredOnExit();
199
+ let index = initial;
200
+ const height = options.length + 2;
201
+ const render = (first) => {
202
+ if (!first)
203
+ process.stdout.write(`\x1b[${height}A`);
204
+ process.stdout.write('\x1b[J');
205
+ process.stdout.write(`${this.accent('◆')} ${this.paint('1', label)}\n`);
206
+ options.forEach((o, idx) => {
207
+ const active = idx === index;
208
+ const dot = active ? this.accent('●') : this.dim('○');
209
+ const text = active ? o.label : this.dim(o.label);
210
+ const hint = o.hint && active ? ` ${this.dim(o.hint)}` : '';
211
+ process.stdout.write(`${this.dim('│')} ${dot} ${text}${hint}\n`);
212
+ });
213
+ process.stdout.write(`${this.dim('└')} ${this.dim('↑↓ move · enter select')}\n`);
214
+ };
215
+ process.stdout.write('\x1b[?25l');
216
+ render(true);
217
+ const value = await new Promise((resolve) => {
218
+ const stdin = process.stdin;
219
+ stdin.setRawMode?.(true);
220
+ stdin.resume();
221
+ const onData = (chunk) => {
222
+ const key = chunk.toString('utf8');
223
+ if (key === '\x03') {
224
+ cleanup();
225
+ process.stdout.write('\x1b[?25h\n');
226
+ process.exit(130);
227
+ }
228
+ if (key === '\x1b[A' || key === 'k') {
229
+ index = (index - 1 + options.length) % options.length;
230
+ render(false);
231
+ }
232
+ else if (key === '\x1b[B' || key === 'j') {
233
+ index = (index + 1) % options.length;
234
+ render(false);
235
+ }
236
+ else if (key === '\r' || key === '\n') {
237
+ cleanup();
238
+ resolve(options[index].value);
239
+ }
240
+ };
241
+ const cleanup = () => {
242
+ stdin.off('data', onData);
243
+ stdin.setRawMode?.(false);
244
+ stdin.pause();
245
+ };
246
+ stdin.on('data', onData);
247
+ });
248
+ // Collapse the block to one settled line.
249
+ process.stdout.write(`\x1b[${height}A\x1b[J`);
250
+ process.stdout.write('\x1b[?25h');
251
+ const chosen = options.find((o) => o.value === value);
252
+ this.out(`${this.paint('32', '◇')} ${label} ${this.dim('·')} ${chosen.label}`);
253
+ return value;
254
+ }
255
+ async text(label, opts = {}) {
256
+ if (!this.interactive)
257
+ throw new Error(`Cannot prompt for "${label}" in a non-interactive run.`);
258
+ if (!this.pretty) {
259
+ for (;;) {
260
+ const answer = (await this.plainQuestion(`${label}${opts.placeholder ? ` (${opts.placeholder})` : ''}: `)).trim();
261
+ const problem = opts.validate?.(answer);
262
+ if (!problem)
263
+ return answer;
264
+ this.io.stderr(`error ${problem}`);
265
+ }
266
+ }
267
+ for (;;) {
268
+ process.stdout.write(`${this.accent('◆')} ${this.paint('1', label)}\n`);
269
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
270
+ let answer;
271
+ try {
272
+ answer = (await rl.question(`${this.dim('│')} ${this.dim('›')} `)).trim();
273
+ }
274
+ finally {
275
+ rl.close();
276
+ }
277
+ const problem = opts.validate?.(answer);
278
+ const promptVisibleLength = 5 + answer.length;
279
+ const lines = 1 + Math.max(1, Math.ceil(promptVisibleLength / (process.stdout.columns || 80)));
280
+ process.stdout.write(`\x1b[${lines}A\x1b[J`);
281
+ if (problem) {
282
+ this.out(`${this.paint('31', '✕')} ${label} ${this.dim('·')} ${problem}`);
283
+ continue;
284
+ }
285
+ this.out(`${this.paint('32', '◇')} ${label} ${this.dim('·')} ${opts.redactAs || answer || this.dim('(empty)')}`);
286
+ return answer;
287
+ }
288
+ }
289
+ async confirm(label, initial = true) {
290
+ if (!this.interactive)
291
+ return initial;
292
+ const suffix = initial ? 'Y/n' : 'y/N';
293
+ if (!this.pretty) {
294
+ const answer = (await this.plainQuestion(`${label} [${suffix}]: `)).trim().toLowerCase();
295
+ if (!answer)
296
+ return initial;
297
+ return answer.startsWith('y');
298
+ }
299
+ const value = await this.select(label, initial
300
+ ? [
301
+ { value: 'yes', label: 'Yes' },
302
+ { value: 'no', label: 'No' },
303
+ ]
304
+ : [
305
+ { value: 'no', label: 'No' },
306
+ { value: 'yes', label: 'Yes' },
307
+ ]);
308
+ return value === 'yes';
309
+ }
310
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "sella-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Sella onboarding CLI — install Sella into your agent clients, pair, verify, fund, publish. (`npx sella-cli init`)",
5
5
  "license": "MIT",
6
- "homepage": "https://sella.network",
6
+ "homepage": "https://sellag.vercel.app",
7
7
  "repository": { "type": "git", "url": "https://github.com/010100100100011101010100/ogsella", "directory": "cli" },
8
8
  "type": "module",
9
9
  "bin": { "sella": "./dist/index.js" },
@@ -12,7 +12,8 @@
12
12
  "engines": { "node": ">=18" },
13
13
  "scripts": {
14
14
  "build": "tsc -p tsconfig.json",
15
- "prepublishOnly": "npm run build"
15
+ "prepublishOnly": "npm run build",
16
+ "postinstall": "node dist/postinstall.js || exit 0"
16
17
  },
17
18
  "devDependencies": {
18
19
  "typescript": "^5"