@botdocs/cli 0.11.0 → 0.12.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.
@@ -21,6 +21,11 @@ interface IngestOptions {
21
21
  pair?: boolean;
22
22
  /** Pre-supplied pairing code (BD-XXXXX). Implies `--pair`. */
23
23
  pairCode?: string;
24
+ /** Suppress the auto-open of https://botdocs.ai/pair when `--pair` is
25
+ * set. Necessary for headless / SSH / CI use where launching a browser
26
+ * is either pointless or actively harmful. Mirrors the same opt-out we
27
+ * want long-term on `botdocs login`. */
28
+ noBrowser?: boolean;
24
29
  }
25
30
  /** Per-file size cap. Any file larger than this is skipped with a warning. */
26
31
  export declare const PER_FILE_BYTE_CAP: number;
@@ -3,7 +3,8 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { render } from 'ink';
6
- import { ApiError, apiFetch } from '../lib/api.js';
6
+ import open from 'open';
7
+ import { ApiError, apiFetch, getApiUrl } from '../lib/api.js';
7
8
  import { loadAuth } from '../lib/config.js';
8
9
  import { IngestSessionClient, PairingClaimError } from '../lib/ingest-session-client.js';
9
10
  import { discoverSkills, ecosystemLabel, formatBytes, STUB_BYTE_THRESHOLD, summaryKey, titleFromContent, } from '../lib/ingest-discover.js';
@@ -949,7 +950,44 @@ async function setupPairingIfRequested(options) {
949
950
  }
950
951
  let code = options.pairCode;
951
952
  if (!code) {
952
- const entered = await readLine(' Enter pairing code from the onboarding page: ');
953
+ // Auto-open the pairing page in the user's default browser unless they
954
+ // opted out via --no-browser. The page mints (or reuses) the BD-XXXXX
955
+ // code server-side, so by the time the browser tab loads, the code is
956
+ // already on screen — no waiting, no second click.
957
+ //
958
+ // We open BEFORE printing the prompt so the order of events feels right
959
+ // ("browser opens, look there, paste here"). If `open()` fails — common
960
+ // in headless / SSH / CI environments — we swallow the error and fall
961
+ // back to the printed URL. The CLI must never become unusable because
962
+ // we couldn't reach a graphical browser.
963
+ const pairUrl = `${getApiUrl()}/pair`;
964
+ const shouldOpen = options.noBrowser !== true;
965
+ let browserOpened = false;
966
+ if (shouldOpen) {
967
+ try {
968
+ await open(pairUrl);
969
+ browserOpened = true;
970
+ }
971
+ catch {
972
+ /* swallow — fall back to the printed URL below */
973
+ }
974
+ }
975
+ console.log('');
976
+ if (browserOpened) {
977
+ console.log(` ✓ Opened ${pairUrl} in your browser.`);
978
+ console.log(' Copy the BD-XXXXX code shown there and paste it here.');
979
+ }
980
+ else {
981
+ console.log(` Get your pairing code at ${pairUrl}`);
982
+ if (!shouldOpen) {
983
+ console.log(' (--no-browser is set; visit the URL above manually.)');
984
+ }
985
+ else {
986
+ console.log(" (Browser open failed — open the URL above manually.)");
987
+ }
988
+ }
989
+ console.log('');
990
+ const entered = await readLine(' Pairing code: ');
953
991
  if (!entered) {
954
992
  console.log(' → Pairing skipped (no code entered).');
955
993
  return null;
@@ -159,7 +159,14 @@ export function IngestDiscoverApp(props) {
159
159
  if (files.length === 0) {
160
160
  return (_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: _jsx(Text, { children: "No skills found in any known location." }) }));
161
161
  }
162
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { bold: true, color: theme.cyan, children: "BotDocs ingest" }), _jsxs(Text, { color: theme.zinc, children: ["Found ", files.length, " skill", files.length === 1 ? '' : 's', " across", ' ', countEcosystems(files), " tool", countEcosystems(files) === 1 ? '' : 's', ":"] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: rows.map((row, i) => {
162
+ // The header summarizes SKILLS, not raw files. A multi-file skill
163
+ // (e.g. a SKILL.md with helper scripts in scripts/) is one row in the
164
+ // list — and should be one tick in the count too. Previously we used
165
+ // `files.length` here, which counted every adjacent file separately
166
+ // and inflated the total ~3× on typical libraries.
167
+ const skillCount = useMemo(() => files.filter(isRootFile).length, [files]);
168
+ const toolCount = useMemo(() => countEcosystems(files), [files]);
169
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { bold: true, color: theme.cyan, children: "BotDocs ingest" }), _jsxs(Text, { color: theme.zinc, children: ["Found ", skillCount, " skill", skillCount === 1 ? '' : 's', " across", ' ', toolCount, " tool", toolCount === 1 ? '' : 's', ":"] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: rows.map((row, i) => {
163
170
  if (row.kind === 'header') {
164
171
  return (_jsx(Box, { marginTop: i === 0 ? 0 : 1, children: _jsxs(Text, { bold: true, color: theme.violet, children: [ecosystemLabel(row.ecosystem), ":"] }) }, `h-${row.ecosystem}`));
165
172
  }
@@ -3,16 +3,17 @@ import { useEffect, useState } from 'react';
3
3
  import { Box, Text, useApp } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
5
  import Gradient from 'ink-gradient';
6
- import BigText from 'ink-big-text';
7
6
  import { theme, BIG_TEXT_MIN_COLS } from './theme.js';
8
- /** Brand wordmark. Falls back to a bold plain "botdocs" on narrow terminals
9
- * where ASCII art would wrap. The `tiny` font keeps the cap height low so the
10
- * screen stays scannable. */
7
+ /** Brand wordmark. A gradient bold "botdocs" on roomy terminals, falling back
8
+ * to a plain bold cyan "botdocs" on narrow ones. (Previously this rendered an
9
+ * ink-big-text ASCII wordmark, but that pulled GPL-3.0 `cfonts` into a package
10
+ * published under MIT — see [G-3]. A styled plain wordmark keeps the brand
11
+ * without the license conflict.) */
11
12
  function BrandMark({ columns }) {
12
13
  if (columns < BIG_TEXT_MIN_COLS) {
13
14
  return (_jsx(Text, { bold: true, color: theme.cyan, children: "botdocs" }));
14
15
  }
15
- return (_jsx(Gradient, { colors: [theme.cyan, theme.violet], children: _jsx(BigText, { text: "botdocs", font: "tiny" }) }));
16
+ return (_jsx(Gradient, { colors: [theme.cyan, theme.violet], children: _jsx(Text, { bold: true, children: "botdocs" }) }));
16
17
  }
17
18
  /** Single source of truth for active-state lines. Returns a label + spinner
18
19
  * pair the renderer can drop into the layout — keeps the state-string mapping
@@ -9,8 +9,7 @@ export declare const theme: {
9
9
  readonly red: "#f87171";
10
10
  readonly zinc: "#71717a";
11
11
  };
12
- /** Width below which we skip the `ink-big-text` ASCII wordmark and fall back
13
- * to a plain bold "botdocs" line — narrower terminals render the big-text
14
- * with awkward wrapping. 60 was chosen empirically: a 50-column wordmark plus
15
- * margins fits comfortably above that. */
12
+ /** Width at/above which the login wordmark renders with the cyan→violet
13
+ * gradient; narrower terminals fall back to a plain bold cyan "botdocs". 60
14
+ * was chosen empirically so the wordmark plus margins fits comfortably. */
16
15
  export declare const BIG_TEXT_MIN_COLS = 60;
@@ -9,8 +9,7 @@ export const theme = {
9
9
  red: '#f87171', // Tailwind red-400 — error
10
10
  zinc: '#71717a', // Tailwind zinc-500 — subtle / hint
11
11
  };
12
- /** Width below which we skip the `ink-big-text` ASCII wordmark and fall back
13
- * to a plain bold "botdocs" line — narrower terminals render the big-text
14
- * with awkward wrapping. 60 was chosen empirically: a 50-column wordmark plus
15
- * margins fits comfortably above that. */
12
+ /** Width at/above which the login wordmark renders with the cyan→violet
13
+ * gradient; narrower terminals fall back to a plain bold cyan "botdocs". 60
14
+ * was chosen empirically so the wordmark plus margins fits comfortably. */
16
15
  export const BIG_TEXT_MIN_COLS = 60;
package/dist/index.js CHANGED
@@ -301,11 +301,18 @@ program
301
301
  .option('--no-ink', 'Disable the interactive TUI; use plain output (zero-arg mode only)')
302
302
  .option('--pair', "Pair with the web onboarding step so it can mirror this ingest live; prompts for a BD-XXXXX code")
303
303
  .option('--pair-code <code>', 'Pre-supply the pairing code (e.g. BD-A4F8K) — skips the interactive prompt; implies --pair')
304
+ .option('--no-browser', 'With --pair: do NOT auto-open https://botdocs.ai/pair (for SSH / CI / headless use)')
304
305
  .action(async (sourcePath, opts) => {
305
- // Commander's --no-ink convention sets opts.ink = false; flip to noInk
306
- // so downstream consumers don't have to handle the inverted boolean.
307
- const { ink, ...rest } = opts;
308
- await ingest(sourcePath, { ...rest, noInk: ink === false, json: program.opts().json });
306
+ // Commander's --no-* convention sets opts.<name> = false; flip both
307
+ // negated flags to their `noX` siblings so downstream consumers don't
308
+ // have to handle the inverted boolean each time.
309
+ const { ink, browser, ...rest } = opts;
310
+ await ingest(sourcePath, {
311
+ ...rest,
312
+ noInk: ink === false,
313
+ noBrowser: browser === false,
314
+ json: program.opts().json,
315
+ });
309
316
  });
310
317
  program
311
318
  .command('compile <path>')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botdocs/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "CLI for BotDocs — author, publish, install, and sync agent skills across Claude, Claude Code, Cursor, Codex, ChatGPT, Windsurf, Copilot, Gemini, Antigravity, and OpenCode.",
5
5
  "keywords": [
6
6
  "botdocs",
@@ -68,7 +68,6 @@
68
68
  "commander": "^14.0.3",
69
69
  "diff": "^9.0.0",
70
70
  "ink": "^5.2.1",
71
- "ink-big-text": "^2.0.0",
72
71
  "ink-gradient": "^3.0.0",
73
72
  "ink-select-input": "^6.2.0",
74
73
  "ink-spinner": "^5.0.0",