swytchcode 2.14.0-beta.1 → 2.14.0-beta.3

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.
Files changed (2) hide show
  1. package/bin/swytchcode.js +220 -190
  2. package/package.json +7 -7
package/bin/swytchcode.js CHANGED
@@ -22,6 +22,7 @@ const c = {
22
22
  green: '\x1b[32m',
23
23
  yellow: '\x1b[33m',
24
24
  cyan: '\x1b[36m',
25
+ orange: '\x1b[38;5;214m', // brand orange (matches internal/output banner 256 fallback)
25
26
  white: '\x1b[37m',
26
27
  gray: '\x1b[90m',
27
28
  bgGreen: '\x1b[42m',
@@ -33,7 +34,7 @@ function write(msg) { process.stdout.write(msg); }
33
34
  function writeln(msg = '') { process.stdout.write(msg + '\n'); }
34
35
  function err(msg) { process.stderr.write(msg + '\n'); }
35
36
 
36
- function box(lines, colour = c.cyan) {
37
+ function box(lines, colour = c.orange) {
37
38
  const width = Math.max(...lines.map(l => stripAnsi(l).length)) + 4;
38
39
  const bar = colour + '─'.repeat(width) + c.reset;
39
40
  writeln(bar);
@@ -77,7 +78,7 @@ function menuArrow(prompt, options) {
77
78
  writeln(c.bold + prompt + c.reset);
78
79
  writeln();
79
80
  options.forEach((o, i) => {
80
- writeln(` ${c.cyan}${i + 1}${c.reset} ${stripAnsi(o.label)}`);
81
+ writeln(` ${c.orange}${i + 1}${c.reset} ${stripAnsi(o.label)}`);
81
82
  });
82
83
  writeln();
83
84
  while (true) {
@@ -103,7 +104,7 @@ function menuArrow(prompt, options) {
103
104
  writeln();
104
105
  options.forEach((o, i) => {
105
106
  if (i === selectedIndex) {
106
- writeln(` ${c.cyan}❯${c.reset} ${c.bold}${c.green}${stripAnsi(o.label)}${c.reset}`);
107
+ writeln(` ${c.orange}❯${c.reset} ${c.bold}${c.green}${stripAnsi(o.label)}${c.reset}`);
107
108
  } else {
108
109
  writeln(` ${c.dim}${stripAnsi(o.label)}${c.reset}`);
109
110
  }
@@ -160,16 +161,16 @@ function spinner(msg) {
160
161
  const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
161
162
  let i = 0;
162
163
  const iv = setInterval(() => {
163
- process.stdout.write(`\r${c.cyan}${frames[i++ % frames.length]}${c.reset} ${msg}`);
164
+ process.stdout.write(`\r${c.orange}${frames[i++ % frames.length]}${c.reset} ${msg}`);
164
165
  }, 80);
165
166
  return {
166
167
  stop(doneMsg) {
167
168
  clearInterval(iv);
168
- process.stdout.write(`\r${c.green}›${c.reset} ${doneMsg}\n`);
169
+ process.stdout.write(`\r\x1b[K${c.green}›${c.reset} ${doneMsg}\n`);
169
170
  },
170
171
  fail(failMsg) {
171
172
  clearInterval(iv);
172
- process.stdout.write(`\r${c.red}×${c.reset} ${failMsg}\n`);
173
+ process.stdout.write(`\r\x1b[K${c.red}×${c.reset} ${failMsg}\n`);
173
174
  },
174
175
  };
175
176
  }
@@ -242,9 +243,16 @@ function exitCode(result) {
242
243
  return 1;
243
244
  }
244
245
 
245
- // Explicit override: SWYTCHCODE_BIN=/path/to/binary bypasses platform resolution.
246
+ // Explicit override: SWYTCHCODE_BIN=/path/to/binary bypasses platform resolution
247
+ // and passes straight through to that binary. The one exception is a bare
248
+ // npx-style launch (npm_command=exec / npx, with no args): there we keep the
249
+ // override binary but skip the passthrough so the onboarding still runs (using
250
+ // that binary for its subprocess calls), which makes onboarding testable locally.
246
251
  const explicitBin = (process.env.SWYTCHCODE_BIN || '').trim();
247
- if (explicitBin) {
252
+ const bareNpxLaunch =
253
+ process.argv.length <= 2 &&
254
+ ((process.env.npm_command || '') === 'exec' || (process.env.npm_execpath || '').includes('npx'));
255
+ if (explicitBin && !bareNpxLaunch) {
248
256
  const result = spawnBin(explicitBin, process.argv.slice(2));
249
257
  if (result.error) {
250
258
  err(`swytchcode: failed to launch SWYTCHCODE_BIN="${explicitBin}": ${result.error.message}`);
@@ -253,40 +261,38 @@ if (explicitBin) {
253
261
  process.exit(exitCode(result));
254
262
  }
255
263
 
256
- const PLATFORM_PACKAGES = {
257
- 'darwin-arm64': 'swytchcode-cli-darwin-arm64',
258
- 'darwin-x64': 'swytchcode-cli-darwin-x64',
259
- 'linux-arm64': 'swytchcode-cli-linux-arm64',
260
- 'linux-x64': 'swytchcode-cli-linux-x64',
261
- 'win32-arm64': 'swytchcode-cli-win32-arm64',
262
- 'win32-x64': 'swytchcode-cli-win32-x64',
263
- };
264
-
265
- const platform = process.platform;
266
- const arch = process.arch;
267
- const key = `${platform}-${arch}`;
268
- const pkg = PLATFORM_PACKAGES[key];
269
-
270
- if (!pkg) {
271
- err(
272
- `swytchcode: unsupported platform "${key}".\n` +
273
- `Supported: ${Object.keys(PLATFORM_PACKAGES).join(', ')}\n` +
274
- `Install via curl instead: https://cli.swytchcode.com`
275
- );
276
- process.exit(1);
277
- }
278
-
279
- const isWindows = platform === 'win32';
280
- const binName = isWindows ? 'swytchcode.exe' : 'swytchcode';
281
264
  let binPath;
282
- try {
283
- binPath = require.resolve(`${pkg}/bin/${binName}`);
284
- } catch (_) {
285
- err(
286
- `swytchcode: could not find the binary for "${key}".\n` +
287
- `Try reinstalling: npm install -g swytchcode`
288
- );
289
- process.exit(1);
265
+ if (explicitBin) {
266
+ binPath = explicitBin;
267
+ } else {
268
+ const PLATFORM_PACKAGES = {
269
+ 'darwin-arm64': 'swytchcode-cli-darwin-arm64',
270
+ 'darwin-x64': 'swytchcode-cli-darwin-x64',
271
+ 'linux-arm64': 'swytchcode-cli-linux-arm64',
272
+ 'linux-x64': 'swytchcode-cli-linux-x64',
273
+ 'win32-arm64': 'swytchcode-cli-win32-arm64',
274
+ 'win32-x64': 'swytchcode-cli-win32-x64',
275
+ };
276
+ const key = `${process.platform}-${process.arch}`;
277
+ const pkg = PLATFORM_PACKAGES[key];
278
+ if (!pkg) {
279
+ err(
280
+ `swytchcode: unsupported platform "${key}".\n` +
281
+ `Supported: ${Object.keys(PLATFORM_PACKAGES).join(', ')}\n` +
282
+ `Install via curl instead: https://cli.swytchcode.com`
283
+ );
284
+ process.exit(1);
285
+ }
286
+ const binName = process.platform === 'win32' ? 'swytchcode.exe' : 'swytchcode';
287
+ try {
288
+ binPath = require.resolve(`${pkg}/bin/${binName}`);
289
+ } catch (_) {
290
+ err(
291
+ `swytchcode: could not find the binary for "${key}".\n` +
292
+ `Try reinstalling: npm install -g swytchcode`
293
+ );
294
+ process.exit(1);
295
+ }
290
296
  }
291
297
 
292
298
  // Detect npx
@@ -308,15 +314,15 @@ if (userArgs.length > 0 && /^[\w]+\.[\w.]+$/.test(userArgs[0]) && !userArgs[0].s
308
314
  }
309
315
  async function runOnboardingDirectDemo(canonicalId) {
310
316
  writeln();
311
- writeln(` ${c.yellow}⚡ Running ${c.bold}${canonicalId}${c.reset}${c.yellow} live...${c.reset}`);
317
+ writeln(` ${c.dim}Running ${c.reset}${c.bold}${canonicalId}${c.reset}${c.dim} ...${c.reset}`);
312
318
  writeln();
313
319
  const demoResult = spawnBin(binPath, ['exec', canonicalId], { SWYTCHCODE_ONBOARDING: '1' });
314
320
  writeln();
315
321
  if (demoResult.status === 0) {
316
- writeln(` ${c.dim}That was a real API call through Swytchcode.${c.reset}`);
322
+ writeln(` ${c.dim}Done — ran through Swytchcode.${c.reset}`);
317
323
  writeln();
318
324
  } else {
319
- writeln(` ${c.yellow}⚠ ${c.bold}${canonicalId}${c.reset}${c.yellow} isn't a valid demo tool.${c.reset}`);
325
+ writeln(` ${c.dim}${c.bold}${canonicalId}${c.reset}${c.dim} isn't a demo tool — running a sample instead.${c.reset}`);
320
326
  writeln(` ${c.dim}Let's run a working demo instead.${c.reset}`);
321
327
  writeln();
322
328
  await runDemoSelection();
@@ -351,6 +357,20 @@ function apiGet(urlStr) {
351
357
  });
352
358
  }
353
359
 
360
+ // ─── Welcome card data (mirrors internal/output/banner.go). Declared before the
361
+ // dispatch below because runOnboarding() runs at module-load time and must not
362
+ // hit a temporal-dead-zone on these consts. ─────────────────────────────────
363
+ const CARD_WORDMARK = [
364
+ '███████╗██╗ ██╗██╗ ██╗████████╗ ██████╗██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗',
365
+ '██╔════╝██║ ██║╚██╗ ██╔╝╚══██╔══╝██╔════╝██║ ██║██╔════╝██╔═══██╗██╔══██╗██╔════╝',
366
+ '███████╗██║ █╗ ██║ ╚████╔╝ ██║ ██║ ███████║██║ ██║ ██║██║ ██║█████╗',
367
+ '╚════██║██║███╗██║ ╚██╔╝ ██║ ██║ ██╔══██║██║ ██║ ██║██║ ██║██╔══╝',
368
+ '███████║╚███╔███╔╝ ██║ ██║ ╚██████╗██║ ██║╚██████╗╚██████╔╝██████╔╝███████╗',
369
+ '╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝',
370
+ ];
371
+ const CARD_TAGLINE = 'Your agent calls an API. We make sure it goes through.';
372
+ const CARD_DESCRIPTION = 'Swytchcode sits between your agent and any API — auth, retries, idempotency, and schema validation, handled. Run a command below, or swytchcode --help for the full list.';
373
+
354
374
  // ─── Normal pass-through (not npx, or npx with non-demo args) ───────────────
355
375
  if (!isNpx || (userArgs.length > 0 && !isCanonicalDemoArg && !isNpxBareWordArg)) {
356
376
  const result = spawnBin(binPath, userArgs);
@@ -371,7 +391,7 @@ if (isNpx && isCanonicalDemoArg) {
371
391
  } else if (isNpxBareWordArg) {
372
392
  // Single bare word (e.g. "abcd") — not a valid canonical ID; show message then onboard
373
393
  writeln();
374
- writeln(` ${c.yellow}⚠ ${c.bold}${originalArg}${c.reset}${c.yellow} isn't a valid demo tool — canonical IDs look like ${c.bold}provider.method${c.reset}`);
394
+ writeln(` ${c.dim}${c.bold}${originalArg}${c.reset}${c.dim} isn't a demo tool — IDs look like ${c.reset}${c.bold}provider.method${c.reset}`);
375
395
  writeln(` ${c.dim}Let's run a working demo instead.${c.reset}`);
376
396
  writeln();
377
397
  Promise.resolve()
@@ -422,40 +442,12 @@ async function runDemoSelection() {
422
442
  writeln(` ${c.gray}$ swytchcode demo stripe${c.reset}`);
423
443
  spawnBin(binPath, ['demo', 'stripe'], { SWYTCHCODE_ONBOARDING: '1' });
424
444
  writeln();
425
- writeln(` ${c.dim}That was a real API call. No credentials, no setup.${c.reset}`);
426
- writeln(` ${c.dim}With Swytchcode, every API integration works exactly like this.${c.reset}`);
445
+ writeln(` ${c.dim}Done — ran through Swytchcode's demo account.${c.reset}`);
446
+ writeln(` ${c.dim}Every integration runs the same way.${c.reset}`);
427
447
  }
428
448
  }
429
449
 
430
- // ─── Welcome card (mirrors internal/output/banner.go) ───────────────────────
431
- const CARD_WORDMARK = [
432
- '███████╗██╗ ██╗██╗ ██╗████████╗ ██████╗██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗',
433
- '██╔════╝██║ ██║╚██╗ ██╔╝╚══██╔══╝██╔════╝██║ ██║██╔════╝██╔═══██╗██╔══██╗██╔════╝',
434
- '███████╗██║ █╗ ██║ ╚████╔╝ ██║ ██║ ███████║██║ ██║ ██║██║ ██║█████╗',
435
- '╚════██║██║███╗██║ ╚██╔╝ ██║ ██║ ██╔══██║██║ ██║ ██║██║ ██║██╔══╝',
436
- '███████║╚███╔███╔╝ ██║ ██║ ╚██████╗██║ ██║╚██████╗╚██████╔╝██████╔╝███████╗',
437
- '╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝',
438
- ];
439
- const CARD_MARK = [
440
- ' ▄▄▄█████████▄▄',
441
- ' ▄▄███▀▀▀ ▀▀▀███▄',
442
- ' ▄███▀',
443
- ' ███ ▄████████▀',
444
- ' ██▀ █████████',
445
- ' ██▀ ▄████████▀',
446
- '███ ▄████████▀',
447
- '██ █████████',
448
- '██ ▄██████████████▀',
449
- '██ ▄██████████████▀',
450
- '▀██ ██████▀▄█████▀',
451
- ' ██▄ ▄████▀',
452
- ' ██▄ ▄████▀',
453
- ' ▀██▄ ▄██▀ ▄',
454
- ' ▀▀▀ ▄█▀▀ ▄▄███',
455
- ' ▄▀ ▄▄▄▄▄▄▄▄▄████▀▀',
456
- ' ▀ ▀▀████████▀▀',
457
- ];
458
- const CARD_TAGLINE = 'Your agent calls an API. We make sure it goes through.';
450
+ // ─── Welcome card render helpers (data consts are hoisted above the dispatch) ─
459
451
 
460
452
  function rep(ch, n) { return n > 0 ? ch.repeat(n) : ''; }
461
453
  function vlen(s) { return [...s].length; }
@@ -463,20 +455,38 @@ function padRows(rows) {
463
455
  const w = Math.max(...rows.map(vlen));
464
456
  return { rows: rows.map(r => r + rep(' ', w - vlen(r))), w };
465
457
  }
458
+ // wrapText greedily word-wraps s to the given visible width (in code points).
459
+ function wrapText(s, width) {
460
+ if (width < 1) width = 1;
461
+ const lines = [];
462
+ let cur = '';
463
+ for (const word of s.split(/\s+/).filter(Boolean)) {
464
+ if (cur === '') cur = word;
465
+ else if (vlen(cur) + 1 + vlen(word) <= width) cur += ' ' + word;
466
+ else { lines.push(cur); cur = word; }
467
+ }
468
+ if (cur !== '') lines.push(cur);
469
+ return lines;
470
+ }
471
+ // writeDescRows renders the wrapped description inside a framed box (boxRow closes
472
+ // over innerW), with the "swytchcode --help" hint highlighted.
473
+ function writeDescRows(boxRow, orange, contentW, margin) {
474
+ for (const ln of wrapText(CARD_DESCRIPTION, contentW)) {
475
+ const colored = ln.replace('swytchcode --help', orange('swytchcode --help'));
476
+ writeln(boxRow(rep(' ', margin) + colored, margin + vlen(ln)));
477
+ }
478
+ }
466
479
 
467
- // welcomeCard renders the framed welcome card (logo mark left + wordmark right,
468
- // wordmark vertically centered), stepping down to a plain wordmark or the compact
469
- // banner based on width / UTF-8 / truecolor / TTY / NO_COLOR. Mirrors the Go banner.
480
+ // welcomeCard renders the universal welcome banner: the "swytchcode" figlet
481
+ // wordmark, version, tagline, and wrapped description inside a framed box — no
482
+ // bolt logo. Mirrors wordmarkBanner in internal/output/banner.go so npx
483
+ // onboarding and the installed CLI look identical (single form, no tiering).
470
484
  function welcomeCard(cliVersion) {
471
485
  const isTTY = !!process.stdout.isTTY;
472
- const width = process.stdout.columns || 0;
473
486
  const ct = process.env.COLORTERM || '';
474
487
  const truecolor = ct === 'truecolor' || ct === '24bit';
475
488
  const noColor = !!process.env.NO_COLOR || !!process.env.CI;
476
489
  const useColor = !noColor && isTTY;
477
- const utf8 = process.platform === 'win32'
478
- ? (!!process.env.WT_SESSION || process.env.TERM_PROGRAM === 'vscode' || process.env.ConEmuANSI === 'ON')
479
- : /utf-?8/i.test(process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || '');
480
490
 
481
491
  const orange = s => !useColor ? s : (truecolor ? `\x1b[38;2;245;148;30m${s}\x1b[0m` : `\x1b[38;5;214m${s}\x1b[0m`);
482
492
  const frame = s => !useColor ? s : (truecolor ? `\x1b[38;2;122;78;24m${s}\x1b[0m` : `\x1b[38;5;214m${s}\x1b[0m`);
@@ -484,62 +494,28 @@ function welcomeCard(cliVersion) {
484
494
  const green = s => !useColor ? s : `\x1b[32m${s}\x1b[0m`;
485
495
 
486
496
  const { rows: wm, w: wmW } = padRows(CARD_WORDMARK);
487
- const { rows: mk, w: mkW } = padRows(CARD_MARK);
488
- const gutter = 4, margin = 2;
489
- const innerW = margin + mkW + gutter + wmW + margin;
490
- const cardW = innerW + 2;
491
-
492
- if (!isTTY || noColor) { compactCard(cliVersion, false); return; }
493
- if (!utf8 || width < 87) { compactCard(cliVersion, useColor); return; }
494
- if (!truecolor || width < cardW + 2) { wordmarkCard({ orange, dim, green, wm }); return; }
495
-
497
+ const margin = 2;
498
+ const innerW = margin + wmW + margin;
496
499
  const boxRow = (colored, vis) => frame('│') + colored + rep(' ', Math.max(0, innerW - vis)) + frame('│');
500
+
497
501
  writeln();
498
502
  const title = ' Welcome to Swytchcode ';
499
503
  writeln(frame('┌─' + title + rep('─', innerW - 1 - vlen(title)) + '┐'));
500
504
  writeln(boxRow('', 0));
501
- // Logo (left) + wordmark (right); wordmark vertically centered.
502
- const nRows = Math.max(mk.length, wm.length);
503
- const wmOff = Math.max(0, Math.floor((mk.length - wm.length) / 2));
504
- for (let i = 0; i < nRows; i++) {
505
- const left = i < mk.length ? orange(mk[i]) : rep(' ', mkW);
506
- const wi = i - wmOff;
507
- const right = (wi >= 0 && wi < wm.length) ? orange(wm[wi]) : rep(' ', wmW);
508
- writeln(boxRow(rep(' ', margin) + left + rep(' ', gutter) + right, margin + mkW + gutter + wmW));
509
- }
505
+ for (const row of wm) writeln(boxRow(rep(' ', margin) + orange(row), margin + wmW));
506
+ writeln(boxRow(rep(' ', margin) + dim('v' + cliVersion), margin + 1 + vlen(cliVersion)));
510
507
  writeln(boxRow('', 0));
511
- // Tagline centered across the inner width.
512
508
  const lead = margin + Math.floor((innerW - 2 * margin - vlen(CARD_TAGLINE)) / 2);
513
509
  writeln(boxRow(rep(' ', lead) + dim(CARD_TAGLINE), lead + vlen(CARD_TAGLINE)));
514
510
  writeln(boxRow('', 0));
511
+ writeDescRows(boxRow, orange, innerW - 2 * margin, margin);
512
+ writeln(boxRow('', 0));
515
513
  writeln(frame('└' + rep('─', innerW) + '┘'));
516
514
  cardBelow({ orange, dim, green });
517
515
  }
518
516
 
519
- function compactCard(cliVersion, useColor) {
520
- const orange = s => useColor ? `\x1b[38;5;214m${s}\x1b[0m` : s;
521
- const dim = s => useColor ? `\x1b[2m${s}\x1b[0m` : s;
522
- writeln();
523
- writeln(` ${orange('swytchcode')} ${dim('v' + cliVersion)}`);
524
- writeln(` ${CARD_TAGLINE}`);
525
- writeln();
526
- writeln(` ${dim('docs')} docs.swytchcode.com`);
527
- writeln(` ${dim('chat')} discord.gg/zuSXSv5GWs`);
528
- writeln();
529
- }
530
-
531
- function wordmarkCard({ orange, dim, green, wm }) {
532
- writeln();
533
- for (const row of wm) writeln(` ${orange(row)}`);
534
- writeln(`\n ${dim(CARD_TAGLINE)}`);
535
- cardBelow({ orange, dim, green });
536
- }
537
-
538
517
  // cardBelow prints the description, sign-in line, cwd, and get-started hints.
539
518
  function cardBelow({ orange, dim, green }) {
540
- writeln();
541
- writeln('Swytchcode sits between your agent and any API — auth, retries, idempotency, and schema');
542
- writeln(`validation, handled. Run a command below, or ${orange('swytchcode --help')} for the full list.`);
543
519
  writeln();
544
520
  writeln(`${orange('●')} ${dim('not signed in — run ')}${orange('swytchcode login')}`);
545
521
  const cwd = process.cwd().replace(/\\/g, '/');
@@ -575,55 +551,102 @@ async function runOnboarding() {
575
551
 
576
552
  welcomeCard(cliVersion);
577
553
 
554
+ // Divider between the welcome card/info and the interactive walkthrough,
555
+ // matching the rules drawn between steps below.
556
+ writeln();
557
+ writeln(' ' + '─'.repeat(45));
558
+ writeln();
559
+
578
560
  // ── Step 1: Demo ──────────────────────────────────────────────────────────
579
- writeln(`${c.bold} Step 1 of 4 — See it in action${c.reset}`);
580
- writeln(`${c.dim} Run a live API call right now, no setup required.${c.reset}`);
561
+ writeln(`${c.bold} Step 1 of 4 — Run a live call${c.reset}`);
562
+ writeln(`${c.dim} Run a real API call through Swytchcode.${c.reset}`);
581
563
  writeln();
582
564
  await runDemoSelection();
583
565
  await runOnboardingSteps2to4();
584
566
  }
585
567
 
568
+ // isGloballyInstalled reports whether a real, persistent global `swytchcode`
569
+ // exists on PATH — as opposed to the transient npx cache binary this script is
570
+ // currently running from. Used to skip the "install globally" onboarding step
571
+ // when it's already installed.
572
+ function isGloballyInstalled() {
573
+ try {
574
+ const probe = process.platform === 'win32' ? 'where swytchcode' : 'command -v swytchcode';
575
+ const out = execSync(probe, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
576
+ if (!out) return false;
577
+ // Any resolved path that isn't inside the npx cache means a global install exists.
578
+ return out.split(/\r?\n/).some(line => line && !line.toLowerCase().includes('_npx'));
579
+ } catch (_) {
580
+ return false; // not found on PATH
581
+ }
582
+ }
583
+
584
+ // authStatus reports whether the user is already authenticated, so onboarding
585
+ // can skip the login prompt. Priority: SWYTCHCODE_TOKEN env var, then a
586
+ // SWYTCHCODE_TOKEN in ./.env, then a valid (unexpired) user session in
587
+ // ~/.swytchcode/auth.json.
588
+ function authStatus() {
589
+ if ((process.env.SWYTCHCODE_TOKEN || '').trim()) {
590
+ return { authed: true, label: 'authenticated via SWYTCHCODE_TOKEN' };
591
+ }
592
+ try {
593
+ const envPath = path.join(process.cwd(), '.env');
594
+ if (fs.existsSync(envPath)) {
595
+ const m = fs.readFileSync(envPath, 'utf8').match(/^\s*SWYTCHCODE_TOKEN\s*=\s*["']?([^"'\r\n#]+)/m);
596
+ if (m && m[1].trim()) return { authed: true, label: 'authenticated via .env token' };
597
+ }
598
+ } catch (_) {}
599
+ try {
600
+ const authPath = path.join(require('os').homedir(), '.swytchcode', 'auth.json');
601
+ const s = JSON.parse(fs.readFileSync(authPath, 'utf8'));
602
+ if (s && s.access_token && typeof s.expires_at === 'number' && Date.now() < s.expires_at * 1000) {
603
+ return { authed: true, label: s.email ? ('signed in as ' + s.email) : 'signed in' };
604
+ }
605
+ } catch (_) {}
606
+ return { authed: false, label: '' };
607
+ }
608
+
586
609
  async function runOnboardingSteps2to4() {
587
610
  // ── Step 2: Install ───────────────────────────────────────────────────────
588
611
  writeln();
589
612
  writeln(' ' + '─'.repeat(45));
590
613
  writeln();
591
614
  writeln(`${c.bold} Step 2 of 4 — Install Swytchcode${c.reset}`);
592
- writeln(`${c.dim} Install once, use everywhere. Adds the ${c.reset}${c.cyan}swytchcode${c.reset}${c.dim} command to your terminal.${c.reset}`);
593
- writeln();
594
- writeln(` ${c.dim}Installing into: ${process.cwd()}${c.reset}`);
595
- writeln(` ${c.dim}$ npm install swytchcode${c.reset}`);
596
- writeln();
597
615
 
598
- const installChoice = await menuArrow('Install here?', [
599
- { label: 'Yes, install here', value: 'yes' },
600
- { label: 'Skip for now', value: 'no' },
601
- ]);
616
+ if (isGloballyInstalled()) {
617
+ // Already installed — don't re-offer the install step.
618
+ writeln(` ${c.green}›${c.reset} ${c.dim}swytchcode is already installed globally — skipping.${c.reset}`);
619
+ } else {
620
+ writeln(`${c.dim} Install the ${c.reset}${c.orange}swytchcode${c.reset}${c.dim} command globally.${c.reset}`);
621
+ writeln();
622
+ writeln(` ${c.dim}$ npm install -g swytchcode${c.reset}`);
623
+ writeln();
602
624
 
603
- writeln();
625
+ const installChoice = await menuArrow('Install globally?', [
626
+ { label: 'Yes, install globally', value: 'yes' },
627
+ { label: 'Skip for now', value: 'no' },
628
+ ]);
604
629
 
605
- if (installChoice === 'no') {
606
- writeln(` ${c.dim}Skipped. Install manually with:${c.reset}`);
607
- writeln(` ${c.cyan}npm install swytchcode${c.reset}`);
608
- writeln(` ${c.dim}For global access:${c.reset} ${c.cyan}npm install -g swytchcode${c.reset}`);
609
- } else {
610
- const installSpin = spinner('Running npm install swytchcode ...');
611
- try {
612
- execSync('npm install swytchcode', { stdio: 'pipe' });
613
- installSpin.stop('installed');
614
- } catch (e) {
615
- if (e.signal === 'SIGINT') {
616
- installSpin.stop('Cancelled');
617
- try { process.stdin.setRawMode(false); } catch (_) {}
618
- process.stdout.write('\x1b[?25h');
619
- process.exit(130);
630
+ writeln();
631
+
632
+ if (installChoice === 'no') {
633
+ writeln(` ${c.dim}Skipped. Install manually with:${c.reset}`);
634
+ writeln(` ${c.orange}npm install -g swytchcode${c.reset}`);
635
+ } else {
636
+ const installSpin = spinner('Running npm install -g swytchcode ...');
637
+ try {
638
+ execSync('npm install -g swytchcode', { stdio: 'pipe' });
639
+ installSpin.stop('installed');
640
+ } catch (e) {
641
+ if (e.signal === 'SIGINT') {
642
+ installSpin.stop('Cancelled');
643
+ try { process.stdin.setRawMode(false); } catch (_) {}
644
+ process.stdout.write('\x1b[?25h');
645
+ process.exit(130);
646
+ }
647
+ installSpin.fail('Installation failed.');
620
648
  }
621
- installSpin.fail('Installation failed.');
622
649
  }
623
-
624
- writeln();
625
- writeln(` ${c.dim}To make it available globally, run:${c.reset}`);
626
- writeln(` ${c.bold}${c.green}npm install -g swytchcode${c.reset}`);
627
650
  }
628
651
 
629
652
  // ── Step 3: Account sync ──────────────────────────────────────────────────
@@ -631,41 +654,48 @@ async function runOnboardingSteps2to4() {
631
654
  writeln(' ' + '─'.repeat(45));
632
655
  writeln();
633
656
  writeln(`${c.bold} Step 3 of 4 — Connect your account${c.reset}`);
634
- writeln(`${c.dim} See your usage analytics, execution history, and saved projects.${c.reset}`);
635
- writeln();
636
657
 
637
- const authChoice = await menuArrow('Do you already have an account?', [
638
- { label: 'Login to my account', value: 'login' },
639
- { label: 'Create a new account', value: 'register' },
640
- { label: 'Continue anonymously', value: 'anon' },
641
- ]);
658
+ const auth = authStatus();
659
+ if (auth.authed) {
660
+ // Already authenticated — never re-prompt for login.
661
+ writeln(` ${c.green}›${c.reset} ${c.dim}Already ${auth.label} — skipping.${c.reset}`);
662
+ } else {
663
+ writeln(`${c.dim} See your usage analytics, execution history, and saved projects.${c.reset}`);
664
+ writeln();
642
665
 
643
- writeln();
666
+ const authChoice = await menuArrow('Do you already have an account?', [
667
+ { label: 'Login to my account', value: 'login' },
668
+ { label: 'Create a new account', value: 'register' },
669
+ { label: 'Continue anonymously', value: 'anon' },
670
+ ]);
644
671
 
645
- if (authChoice === 'login') {
646
- writeln(` ${c.dim}Opening browser for login ...${c.reset}`);
647
- writeln();
648
- const result = spawnBin(binPath, ['login']);
649
- if (result.error || result.status !== 0) {
650
- writeln(` ${c.yellow}Login window closed or failed.${c.reset}`);
651
- writeln(` ${c.dim}You can log in later with:${c.reset} ${c.cyan}swytchcode login${c.reset}`);
652
- } else {
653
- writeln(` ${c.green}›${c.reset} logged in`);
654
- }
655
- } else if (authChoice === 'register') {
656
- writeln(` ${c.dim}Opening browser to create your account ...${c.reset}`);
657
672
  writeln();
658
- // login command handles both login and registration via the browser flow
659
- const result = spawnBin(binPath, ['login']);
660
- if (result.error || result.status !== 0) {
661
- writeln(` ${c.yellow}Browser window closed or failed.${c.reset}`);
662
- writeln(` ${c.dim}Create your account at:${c.reset} ${c.cyan}https://swytchcode.com/signup${c.reset}`);
673
+
674
+ if (authChoice === 'login') {
675
+ writeln(` ${c.dim}Opening browser for login ...${c.reset}`);
676
+ writeln();
677
+ const result = spawnBin(binPath, ['login']);
678
+ if (result.error || result.status !== 0) {
679
+ writeln(` ${c.yellow}Login window closed or failed.${c.reset}`);
680
+ writeln(` ${c.dim}You can log in later with:${c.reset} ${c.orange}swytchcode login${c.reset}`);
681
+ } else {
682
+ writeln(` ${c.green}›${c.reset} logged in`);
683
+ }
684
+ } else if (authChoice === 'register') {
685
+ writeln(` ${c.dim}Opening browser to create your account ...${c.reset}`);
686
+ writeln();
687
+ // login command handles both login and registration via the browser flow
688
+ const result = spawnBin(binPath, ['login']);
689
+ if (result.error || result.status !== 0) {
690
+ writeln(` ${c.yellow}Browser window closed or failed.${c.reset}`);
691
+ writeln(` ${c.dim}Create your account at:${c.reset} ${c.orange}https://swytchcode.com/signup${c.reset}`);
692
+ } else {
693
+ writeln(` ${c.green}›${c.reset} account connected`);
694
+ }
663
695
  } else {
664
- writeln(` ${c.green}›${c.reset} account connected`);
696
+ writeln(` ${c.dim}No problem — you can connect your account any time with:${c.reset}`);
697
+ writeln(` ${c.orange}swytchcode login${c.reset}`);
665
698
  }
666
- } else {
667
- writeln(` ${c.dim}No problem — you can connect your account any time with:${c.reset}`);
668
- writeln(` ${c.cyan}swytchcode login${c.reset}`);
669
699
  }
670
700
 
671
701
  // ── Step 4: Next action ───────────────────────────────────────────────────
@@ -701,11 +731,11 @@ async function runOnboardingSteps2to4() {
701
731
  writeln(` ${c.bold}You're all set.${c.reset}`);
702
732
  writeln();
703
733
  writeln(` Quick reference:`);
704
- writeln(` ${c.cyan}swytchcode init${c.reset} ${c.dim}Set up a new project${c.reset}`);
705
- writeln(` ${c.cyan}swytchcode examples${c.reset} ${c.dim}Browse example integrations${c.reset}`);
706
- writeln(` ${c.cyan}swytchcode get <api>${c.reset} ${c.dim}Install an API integration${c.reset}`);
707
- writeln(` ${c.cyan}swytchcode exec <id>${c.reset} ${c.dim}Execute an API method${c.reset}`);
708
- writeln(` ${c.cyan}swytchcode --help${c.reset} ${c.dim}Full command reference${c.reset}`);
734
+ writeln(` ${c.orange}swytchcode init${c.reset} ${c.dim}Set up a new project${c.reset}`);
735
+ writeln(` ${c.orange}swytchcode examples${c.reset} ${c.dim}Browse example integrations${c.reset}`);
736
+ writeln(` ${c.orange}swytchcode get <api>${c.reset} ${c.dim}Install an API integration${c.reset}`);
737
+ writeln(` ${c.orange}swytchcode exec <id>${c.reset} ${c.dim}Execute an API method${c.reset}`);
738
+ writeln(` ${c.orange}swytchcode --help${c.reset} ${c.dim}Full command reference${c.reset}`);
709
739
  }
710
740
 
711
741
  writeln();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swytchcode",
3
- "version": "2.14.0-beta.1",
3
+ "version": "2.14.0-beta.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://gitlab.com/swytchcode/cli.git"
@@ -28,11 +28,11 @@
28
28
  "README.md"
29
29
  ],
30
30
  "optionalDependencies": {
31
- "swytchcode-cli-darwin-arm64": "2.14.0-beta.1",
32
- "swytchcode-cli-darwin-x64": "2.14.0-beta.1",
33
- "swytchcode-cli-linux-arm64": "2.14.0-beta.1",
34
- "swytchcode-cli-linux-x64": "2.14.0-beta.1",
35
- "swytchcode-cli-win32-arm64": "2.14.0-beta.1",
36
- "swytchcode-cli-win32-x64": "2.14.0-beta.1"
31
+ "swytchcode-cli-darwin-arm64": "2.14.0-beta.3",
32
+ "swytchcode-cli-darwin-x64": "2.14.0-beta.3",
33
+ "swytchcode-cli-linux-arm64": "2.14.0-beta.3",
34
+ "swytchcode-cli-linux-x64": "2.14.0-beta.3",
35
+ "swytchcode-cli-win32-arm64": "2.14.0-beta.3",
36
+ "swytchcode-cli-win32-x64": "2.14.0-beta.3"
37
37
  }
38
38
  }