@boundless.network/setup 0.1.3 → 0.1.4

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/dist/app.js CHANGED
@@ -22,12 +22,30 @@ import { LOGO, LOGO_COLUMNS, LOGO_ROWS } from "./ui/logo.js";
22
22
  import { ModelSelect } from "./ui/table.js";
23
23
  import { BAD, BRAND, MUTED, OK, WARN } from "./ui/theme.js";
24
24
  const NAV = "↑↓ navigate enter select";
25
+ const STEPS = new Set(["welcome", "existing", "harnesses", "scope", "store", "model", "review"]);
26
+ const withBack = (hints, canBack) => (canBack ? `${hints} esc back` : hints);
25
27
  function message(error) {
26
28
  return error instanceof Error ? error.message : String(error);
27
29
  }
28
30
  export function App({ onFinish }) {
29
31
  const { exit } = useApp();
30
32
  const [screen, setScreen] = useState({ kind: "boot" });
33
+ const history = useRef([]).current;
34
+ const canBack = history.length > 0;
35
+ const go = (next) => {
36
+ if (STEPS.has(screen.kind)) {
37
+ history.push(screen);
38
+ }
39
+ setScreen(next);
40
+ };
41
+ const back = () => {
42
+ const previous = history.pop();
43
+ if (previous) {
44
+ setScreen(previous);
45
+ }
46
+ };
47
+ const forget = () => history.splice(0);
48
+ const pickModels = () => go(wizard.catalog.length > 0 ? { kind: "model", role: "primary" } : { kind: "catalog" });
31
49
  const wizard = useRef({
32
50
  background: "",
33
51
  backups: [],
@@ -81,12 +99,12 @@ export function App({ onFinish }) {
81
99
  if (role === "primary") {
82
100
  wizard.primary = id;
83
101
  if (hasBackgroundSlot()) {
84
- setScreen({ kind: "model", role: "background" });
102
+ go({ kind: "model", role: "background" });
85
103
  return;
86
104
  }
87
105
  }
88
106
  wizard.background = id;
89
- setScreen({ kind: "review" });
107
+ go({ kind: "review" });
90
108
  };
91
109
  const finish = () => {
92
110
  onFinish(summary());
@@ -98,69 +116,70 @@ export function App({ onFinish }) {
98
116
  case "welcome":
99
117
  return (_jsx(Welcome, { onSelect: (choice) => {
100
118
  if (choice === "continue") {
101
- setScreen(wizard.existing?.valid ? { kind: "existing" } : { kind: "signin" });
119
+ go(wizard.key ? { kind: "harnesses" } : wizard.existing?.valid ? { kind: "existing" } : { kind: "signin" });
102
120
  }
103
121
  else if (choice === "cancel") {
104
122
  exit();
105
123
  }
106
124
  else {
107
- setScreen({ kind: "info", topic: choice });
125
+ go({ kind: "info", topic: choice });
108
126
  }
109
127
  }, wizard: wizard }));
110
128
  case "info":
111
- return _jsx(Info, { onBack: () => setScreen({ kind: "welcome" }), topic: screen.topic });
129
+ return _jsx(Info, { onBack: back, topic: screen.topic });
112
130
  case "existing":
113
- return (_jsx(Existing, { onSelect: (choice) => {
131
+ return (_jsx(Existing, { canBack: canBack, onBack: back, onSelect: (choice) => {
114
132
  if (choice === "reuse") {
115
133
  wizard.key = wizard.existing?.key ?? null;
116
134
  wizard.keyStore = wizard.existing?.source ?? "";
117
- setScreen({ kind: "harnesses" });
135
+ go({ kind: "harnesses" });
118
136
  }
119
137
  else if (choice === "new") {
120
- setScreen({ kind: "signin" });
138
+ go({ kind: "signin" });
121
139
  }
122
140
  else {
123
141
  exit();
124
142
  }
125
143
  }, source: wizard.existing?.source ?? "" }));
126
144
  case "signin":
127
- return (_jsx(SignIn, { config: wizard.config, env: wizard.env, onCancel: () => setScreen({ kind: "welcome" }), onError: (error) => fail(error, { kind: "signin" }), onToken: (token) => setScreen({ kind: "exchange", token }) }));
145
+ return (_jsx(SignIn, { config: wizard.config, env: wizard.env, onCancel: back, onError: (error) => fail(error, { kind: "signin" }), onToken: (token) => setScreen({ kind: "exchange", token }) }));
128
146
  case "exchange":
129
147
  return (_jsx(Exchange, { onDone: (key, name) => {
130
148
  wizard.key = key;
131
149
  wizard.keyName = name;
150
+ forget();
132
151
  setScreen({ kind: "harnesses" });
133
152
  }, onError: (error) => fail(error, error instanceof SetupError && error.code === "setup_recovery_required" ? null : screen), token: screen.token }));
134
153
  case "error":
135
154
  return (_jsx(ErrorScreen, { message: screen.message, onSelect: (choice) => (choice === "retry" && screen.retry ? setScreen(screen.retry) : exit()), retry: screen.retry !== null }));
136
155
  case "harnesses":
137
- return (_jsx(Harnesses, { initial: wizard.harnesses.length > 0 ? wizard.harnesses : [...wizard.installed], installed: wizard.installed, onSubmit: (ids) => {
156
+ return (_jsx(Harnesses, { canBack: canBack, initial: wizard.harnesses.length > 0 ? wizard.harnesses : [...wizard.installed], installed: wizard.installed, onBack: back, onSubmit: (ids) => {
138
157
  wizard.harnesses = ids;
139
- setScreen({ kind: "scope" });
158
+ go({ kind: "scope" });
140
159
  } }));
141
160
  case "scope":
142
- return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Where should this apply?" }), _jsx(Select, { onSelect: (scope) => {
161
+ return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "Where should this apply?" }), _jsx(Select, { initial: wizard.scope, onCancel: back, onSelect: (scope) => {
143
162
  wizard.scope = scope;
144
163
  if (scope === "project") {
145
164
  wizard.store = null;
146
165
  wizard.keyFile = projectKeyFile(wizard.env.cwd);
147
166
  wizard.keyStore = tilde(wizard.keyFile);
148
- setScreen({ kind: "catalog" });
167
+ pickModels();
149
168
  }
150
169
  else {
151
- setScreen({ kind: "store" });
170
+ go({ kind: "store" });
152
171
  }
153
172
  }, options: [
154
173
  { hint: "recommended", label: "My user account", value: "user" },
155
174
  { hint: tilde(wizard.env.cwd), label: "Only this project", value: "project" },
156
175
  ] })] }));
157
176
  case "store":
158
- return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Where should I keep the key?" }), _jsx(Muted, { center: true, children: "Your shell profile will read it from there; the key itself never goes into a profile." }), _jsx(Select, { onSelect: (kind) => {
177
+ return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "Where should I keep the key?" }), _jsx(Muted, { center: true, children: "Your shell profile will read it from there; the key itself never goes into a profile." }), _jsx(Select, { initial: wizard.store?.kind, onCancel: back, onSelect: (kind) => {
159
178
  const store = wizard.stores.find((candidate) => candidate.kind === kind) ?? null;
160
179
  wizard.store = store;
161
180
  wizard.keyFile = store?.path ?? null;
162
181
  wizard.keyStore = store?.label ?? "";
163
- setScreen({ kind: "catalog" });
182
+ pickModels();
164
183
  }, options: wizard.stores.map((store, index) => ({
165
184
  hint: index === 0 ? "recommended" : undefined,
166
185
  label: store.label,
@@ -172,11 +191,14 @@ export function App({ onFinish }) {
172
191
  setScreen({ kind: "model", role: "primary" });
173
192
  }, onError: (error) => fail(error, { kind: "catalog" }), wizard: wizard }));
174
193
  case "model":
175
- return (_jsx(ModelPicker, { catalog: wizard.catalog, onSelect: (id) => chooseModel(screen.role, id), role: screen.role }));
194
+ return (_jsx(ModelPicker, { canBack: canBack, catalog: wizard.catalog, initial: screen.role === "primary" ? wizard.primary : wizard.background, onBack: back, onSelect: (id) => chooseModel(screen.role, id), role: screen.role }));
176
195
  case "review":
177
- return (_jsx(Review, { harnesses: chosen(), input: planInput(), onCancel: () => exit(), onPlans: (plans) => {
196
+ return (_jsx(Review, { harnesses: chosen(), input: planInput(), canBack: canBack, onBack: back, onCancel: () => exit(), onPlans: (plans) => {
178
197
  wizard.plans = plans;
179
- }, onProceed: () => setScreen({ kind: "apply" }), wizard: wizard }));
198
+ }, onProceed: () => {
199
+ forget();
200
+ setScreen({ kind: "apply" });
201
+ }, wizard: wizard }));
180
202
  case "apply":
181
203
  return (_jsx(Apply, { onDone: () => setScreen(wizard.plans.some(({ plan }) => plan.manual.length > 0) ? { kind: "manual" } : { kind: "test" }), onError: (error) => fail(error, { kind: "review" }), wizard: wizard }));
182
204
  case "manual":
@@ -247,8 +269,8 @@ function Info({ onBack, topic }) {
247
269
  });
248
270
  return (_jsxs(Frame, { hints: "enter back", children: [_jsx(Title, { children: topic === "what" ? "What this does" : "Privacy & data" }), topic === "what" ? (_jsxs(_Fragment, { children: [_jsxs(Para, { children: ["1. Opens ", CONSOLE, "/authorize in your browser. You sign in (or sign up) and approve \"Boundless local setup\"."] }), _jsx(Para, { children: "2. Exchanges that approval for one inference key named setup-\u2026 on your account. The secret is shown once, to this program only." }), _jsxs(Para, { children: ["3. Stores the key in your credential store or a private 0600 file, and points your shell at it via ", KEY_ENV, "."] }), _jsx(Para, { children: "4. Backs up and merges each chosen harness's configuration, preserving unrelated providers and settings." }), _jsx(Para, { children: "5. Sends one small request per model to prove it works. That request uses account credits; nothing is purchased." })] })) : (_jsxs(_Fragment, { children: [_jsxs(Para, { children: ["Nothing from this directory is uploaded. The only network calls are to ", CONSOLE, ", its authorization server, and the inference gateway."] }), _jsx(Para, { children: "The sign-in token is used once for the key exchange and then discarded; no refresh token is kept." }), _jsx(Para, { children: "The API key is written only to the store you pick and to harness files that need it, all with owner-only permissions. It never appears in command arguments, shell history, logs, or this screen." }), _jsxs(Para, { children: ["Manage or revoke the key at ", KEYS_URL, "."] })] }))] }));
249
271
  }
250
- function Existing({ onSelect, source }) {
251
- return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "A Boundless key is already configured" }), _jsxs(Muted, { center: true, children: ["Found in ", source, ", and the gateway accepts it."] }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: onSelect, options: [
272
+ function Existing({ canBack, onBack, onSelect, source, }) {
273
+ return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "A Boundless key is already configured" }), _jsxs(Muted, { center: true, children: ["Found in ", source, ", and the gateway accepts it."] }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onCancel: onBack, onSelect: onSelect, options: [
252
274
  { hint: "recommended", label: "Reuse it", value: "reuse" },
253
275
  { label: "Sign in and create a separate key", value: "new" },
254
276
  { label: "Cancel", value: "cancel" },
@@ -294,14 +316,14 @@ function Exchange({ onDone, onError, token }) {
294
316
  function ErrorScreen({ message: text, onSelect, retry }) {
295
317
  return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Something went wrong" }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: BAD, children: text }) }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: onSelect, options: retry ? [{ label: "Try again", value: "retry" }, { label: "Cancel", value: "cancel" }] : [{ label: "Exit", value: "cancel" }] }) })] }));
296
318
  }
297
- function Harnesses({ initial, installed, onSubmit }) {
319
+ function Harnesses({ canBack, initial, installed, onBack, onSubmit, }) {
298
320
  const [empty, setEmpty] = useState(false);
299
321
  const options = HARNESSES.map((harness) => ({
300
322
  hint: installed.has(harness.id) ? "installed" : "not found",
301
323
  label: harness.name,
302
324
  value: harness.id,
303
325
  }));
304
- return (_jsxs(Frame, { hints: "space toggle enter continue", children: [_jsx(Title, { children: "Which harnesses should I configure?" }), _jsx(Muted, { center: true, children: "Installed ones are preselected. Harnesses that are not installed get their configuration written for when they are." }), _jsx(Box, { justifyContent: "center", children: _jsx(MultiSelect, { initial: initial, onSubmit: (ids) => (ids.length === 0 ? setEmpty(true) : onSubmit(ids)), options: options }) }), empty ? (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: WARN, children: "Pick at least one." }) })) : null] }));
326
+ return (_jsxs(Frame, { hints: withBack("space toggle a all/none enter continue", canBack), children: [_jsx(Title, { children: "Which harnesses should I configure?" }), _jsx(Muted, { center: true, children: "Installed ones are preselected. Harnesses that are not installed get their configuration written for when they are." }), _jsx(Box, { justifyContent: "center", children: _jsx(MultiSelect, { initial: initial, onCancel: onBack, onSubmit: (ids) => (ids.length === 0 ? setEmpty(true) : onSubmit(ids)), options: options }) }), empty ? (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: WARN, children: "Pick at least one." }) })) : null] }));
305
327
  }
306
328
  function Catalog({ onDone, onError, wizard }) {
307
329
  useEffect(() => {
@@ -309,10 +331,10 @@ function Catalog({ onDone, onError, wizard }) {
309
331
  }, []);
310
332
  return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: "Reading the model catalog" }) }));
311
333
  }
312
- function ModelPicker({ catalog, onSelect, role, }) {
313
- return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: role === "primary" ? "Which model should be the default?" : "Which model for background work?" }), _jsxs(Muted, { center: true, children: [role === "primary" ? "Every configured harness gets this model." : "Cheap, fast tasks (Claude Code haiku slot, OpenCode small_model, omp tiny).", " Prices are USD per 1M tokens from the live catalog."] }), _jsx(ModelSelect, { models: catalog, onSelect: onSelect, recommended: role === "primary" ? RECOMMENDED_MODEL : RECOMMENDED_BACKGROUND }, role)] }));
334
+ function ModelPicker({ canBack, catalog, initial, onBack, onSelect, role, }) {
335
+ return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: role === "primary" ? "Which model should be the default?" : "Which model for background work?" }), _jsxs(Muted, { center: true, children: [role === "primary" ? "Every configured harness gets this model." : "Cheap, fast tasks (Claude Code haiku slot, OpenCode small_model, omp tiny).", " Prices are USD per 1M tokens from the live catalog."] }), _jsx(ModelSelect, { initial: initial, models: catalog, onBack: onBack, onSelect: onSelect, recommended: role === "primary" ? RECOMMENDED_MODEL : RECOMMENDED_BACKGROUND }, role)] }));
314
336
  }
315
- function Review({ harnesses, input, onCancel, onPlans, onProceed, wizard, }) {
337
+ function Review({ canBack, harnesses, input, onBack, onCancel, onPlans, onProceed, wizard, }) {
316
338
  const [plans, setPlans] = useState(null);
317
339
  useEffect(() => {
318
340
  Promise.all(harnesses.map(async (harness) => ({ harness, plan: await harness.plan(input) }))).then((planned) => {
@@ -329,11 +351,11 @@ function Review({ harnesses, input, onCancel, onPlans, onProceed, wizard, }) {
329
351
  : env.os === "windows"
330
352
  ? "user environment variable"
331
353
  : `exported as ${KEY_ENV} by your shell profile`;
332
- return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Review" }), _jsx(Facts, { facts: [
354
+ return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "Review" }), _jsx(Facts, { facts: [
333
355
  { label: "Key", value: `${wizard.keyStore} · ${exposure}` },
334
356
  { label: "Model", value: wizard.primary },
335
357
  ...(wizard.background !== wizard.primary ? [{ label: "Background", value: wizard.background }] : []),
336
- ] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.map(({ harness, plan }) => (_jsxs(Box, { children: [_jsx(Text, { children: harness.name.padEnd(16) }), _jsx(Text, { color: plan.changes.length > 0 ? BRAND : MUTED, children: plan.changes.length > 0 ? plan.changes.map((change) => tilde(change.path)).join(", ") : "manual steps, shown after apply" })] }, harness.id))) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.flatMap(({ plan }) => plan.notes.slice(0, 1)).map((note) => (_jsx(Text, { color: MUTED, children: note }, note))) }), _jsx(Muted, { children: "Existing files are backed up next to themselves before they change. Credentials are never displayed." }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: (choice) => (choice === "apply" ? onProceed() : onCancel()), options: [
358
+ ] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.map(({ harness, plan }) => (_jsxs(Box, { children: [_jsx(Text, { children: harness.name.padEnd(16) }), _jsx(Text, { color: plan.changes.length > 0 ? BRAND : MUTED, children: plan.changes.length > 0 ? plan.changes.map((change) => tilde(change.path)).join(", ") : "manual steps, shown after apply" })] }, harness.id))) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.flatMap(({ plan }) => plan.notes.slice(0, 1)).map((note) => (_jsx(Text, { color: MUTED, children: note }, note))) }), _jsx(Muted, { children: "Existing files are backed up next to themselves before they change. Credentials are never displayed." }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onCancel: onBack, onSelect: (choice) => (choice === "apply" ? onProceed() : onCancel()), options: [
337
359
  { label: "Apply", value: "apply" },
338
360
  { label: "Cancel", value: "cancel" },
339
361
  ] }) })] }));
@@ -27,6 +27,7 @@ export async function keyWorks(gatewayUrl, key) {
27
27
  }
28
28
  }
29
29
  /** Exact USD, never rounded away: trailing zeros trimmed, up to twelve decimals. */
30
+ export const usdPerMillion = (amount) => `$${amount.toFixed(2)}`;
30
31
  export function usd(amount) {
31
32
  return `$${amount.toFixed(12).replace(/0+$/, "").replace(/\.$/, "")}`;
32
33
  }
package/dist/ui/menu.js CHANGED
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { useState } from "react";
4
4
  import { BRAND, MUTED } from "./theme.js";
5
- export function Select({ onCancel, onSelect, options, }) {
6
- const [index, setIndex] = useState(0);
5
+ export function Select({ initial, onCancel, onSelect, options, }) {
6
+ const [index, setIndex] = useState(Math.max(options.findIndex((option) => option.value === initial), 0));
7
7
  const move = (delta) => {
8
8
  let next = index;
9
9
  for (let step = 0; step < options.length; step += 1) {
@@ -46,6 +46,9 @@ export function MultiSelect({ initial, onCancel, onSubmit, options, }) {
46
46
  else if (key.downArrow || input === "j") {
47
47
  setIndex((index + 1) % options.length);
48
48
  }
49
+ else if (input === "a") {
50
+ setChosen(chosen.size === options.length ? new Set() : new Set(options.map((option) => option.value)));
51
+ }
49
52
  else if (input === " ") {
50
53
  const value = options[index]?.value;
51
54
  if (value !== undefined) {
package/dist/ui/table.js CHANGED
@@ -1,21 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { useState } from "react";
4
- import { usd } from "../lib/catalog.js";
4
+ import { usdPerMillion } from "../lib/catalog.js";
5
5
  import { BRAND, MUTED } from "./theme.js";
6
6
  const HEADER = ["ID", "Input /1M", "Output /1M", "Context", ""];
7
7
  const cells = (model, recommended) => [
8
8
  model.id,
9
- usd(model.inputPerMillionTokens),
10
- usd(model.outputPerMillionTokens),
9
+ usdPerMillion(model.inputPerMillionTokens),
10
+ usdPerMillion(model.outputPerMillionTokens),
11
11
  model.contextTokens.toLocaleString("en-US"),
12
12
  model.id === recommended ? "recommended" : "",
13
13
  ];
14
- export function ModelSelect({ models, onSelect, recommended, }) {
15
- const start = Math.max(models.findIndex((model) => model.id === recommended), 0);
14
+ export function ModelSelect({ initial, models, onBack, onSelect, recommended, }) {
15
+ const start = Math.max(models.findIndex((model) => model.id === initial), models.findIndex((model) => model.id === recommended), 0);
16
16
  const [index, setIndex] = useState(start);
17
17
  useInput((input, key) => {
18
- if (key.upArrow || input === "k") {
18
+ if (key.escape) {
19
+ onBack();
20
+ }
21
+ else if (key.upArrow || input === "k") {
19
22
  setIndex((index + models.length - 1) % models.length);
20
23
  }
21
24
  else if (key.downArrow || input === "j") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boundless.network/setup",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Set up Boundless as the inference provider for your coding harness. Run: npx -y @boundless.network/setup@latest",
5
5
  "license": "MIT",
6
6
  "type": "module",