@boundless.network/setup 0.1.0
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 +42 -0
- package/dist/app.js +471 -0
- package/dist/cli.js +37 -0
- package/dist/harnesses/claude-code.js +55 -0
- package/dist/harnesses/codex.js +61 -0
- package/dist/harnesses/gui.js +43 -0
- package/dist/harnesses/hermes.js +46 -0
- package/dist/harnesses/index.js +7 -0
- package/dist/harnesses/omp.js +41 -0
- package/dist/harnesses/opencode.js +66 -0
- package/dist/harnesses/types.js +1 -0
- package/dist/lib/browser.js +21 -0
- package/dist/lib/catalog.js +29 -0
- package/dist/lib/config.js +42 -0
- package/dist/lib/env.js +35 -0
- package/dist/lib/exchange.js +55 -0
- package/dist/lib/files.js +116 -0
- package/dist/lib/oauth.js +89 -0
- package/dist/lib/smoke.js +36 -0
- package/dist/lib/store.js +157 -0
- package/dist/lib/test.js +56 -0
- package/dist/summary.js +14 -0
- package/dist/ui/facts.js +19 -0
- package/dist/ui/frame.js +21 -0
- package/dist/ui/menu.js +74 -0
- package/dist/ui/spinner.js +13 -0
- package/dist/ui/table.js +17 -0
- package/dist/ui/text-input.js +22 -0
- package/dist/ui/theme.js +5 -0
- package/dist/version.js +3 -0
- package/package.json +43 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { KEY_ENV } from "./config.js";
|
|
6
|
+
export async function runSmoke(smoke, key) {
|
|
7
|
+
const cwd = await mkdtemp(join(tmpdir(), "boundless-setup-"));
|
|
8
|
+
try {
|
|
9
|
+
return await new Promise((resolve) => {
|
|
10
|
+
const child = spawn(smoke.command, smoke.args, {
|
|
11
|
+
cwd,
|
|
12
|
+
env: { ...process.env, [KEY_ENV]: key },
|
|
13
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
14
|
+
});
|
|
15
|
+
let output = "";
|
|
16
|
+
const collect = (chunk) => {
|
|
17
|
+
output += chunk.toString();
|
|
18
|
+
};
|
|
19
|
+
child.stdout.on("data", collect);
|
|
20
|
+
child.stderr.on("data", collect);
|
|
21
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 180_000);
|
|
22
|
+
child.once("error", (error) => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
resolve({ kind: "failed", output: error.message });
|
|
25
|
+
});
|
|
26
|
+
child.once("close", () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
const trimmed = output.trim().slice(-400);
|
|
29
|
+
resolve({ kind: smoke.expect.test(output) ? "ok" : "failed", output: trimmed });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await rm(cwd, { force: true, recursive: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { KEY_ENV } from "./config.js";
|
|
4
|
+
import { ensureIgnored, exists, gitTracked, home, readText, run, tilde, which, writeKeepingMode, writePrivate } from "./files.js";
|
|
5
|
+
const SERVICE = "boundless";
|
|
6
|
+
function stdinCommand(command, args, input) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(command, args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
9
|
+
let stderr = "";
|
|
10
|
+
child.stderr.on("data", (chunk) => {
|
|
11
|
+
stderr += chunk.toString();
|
|
12
|
+
});
|
|
13
|
+
child.once("error", reject);
|
|
14
|
+
child.once("close", (code) => (code === 0 ? resolve() : reject(new Error(stderr.trim() || `${command} exited ${code}`))));
|
|
15
|
+
child.stdin.end(input);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function userKeyFile(env) {
|
|
19
|
+
return env.os === "windows"
|
|
20
|
+
? join(process.env.LOCALAPPDATA ?? home("AppData", "Local"), "Boundless", "api-key")
|
|
21
|
+
: home(".config", "boundless", "api-key");
|
|
22
|
+
}
|
|
23
|
+
export const projectKeyFile = (cwd) => join(cwd, ".boundless", "api-key");
|
|
24
|
+
export async function availableStores(env) {
|
|
25
|
+
const stores = [];
|
|
26
|
+
if (env.os === "macos" && (await which("security"))) {
|
|
27
|
+
stores.push({ kind: "keychain", label: "macOS Keychain", path: null });
|
|
28
|
+
}
|
|
29
|
+
if (env.os === "linux" && (await which("secret-tool"))) {
|
|
30
|
+
stores.push({ kind: "secret-service", label: "Secret Service (secret-tool)", path: null });
|
|
31
|
+
}
|
|
32
|
+
stores.push({ kind: "file", label: `Private file ${tilde(userKeyFile(env))}`, path: userKeyFile(env) });
|
|
33
|
+
return stores;
|
|
34
|
+
}
|
|
35
|
+
export async function saveKey(store, key) {
|
|
36
|
+
switch (store.kind) {
|
|
37
|
+
case "keychain":
|
|
38
|
+
// `security -i` reads commands from stdin, so the secret never appears in process arguments.
|
|
39
|
+
await stdinCommand("security", ["-i"], `add-generic-password -a ${KEY_ENV} -s ${SERVICE} -U -w "${key}"\n`);
|
|
40
|
+
return;
|
|
41
|
+
case "secret-service":
|
|
42
|
+
await stdinCommand("secret-tool", ["store", "--label=Boundless API key", "service", SERVICE, "account", KEY_ENV], key);
|
|
43
|
+
return;
|
|
44
|
+
case "file":
|
|
45
|
+
await writePrivate(store.path, `${key}\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** A shell expression that yields the key at shell start, so no profile ever contains it. */
|
|
50
|
+
export function readExpression(store) {
|
|
51
|
+
const quoted = tilde(store.path ?? "").replace("~", "$HOME");
|
|
52
|
+
switch (store.kind) {
|
|
53
|
+
case "keychain":
|
|
54
|
+
return `security find-generic-password -a ${KEY_ENV} -s ${SERVICE} -w 2>/dev/null`;
|
|
55
|
+
case "secret-service":
|
|
56
|
+
return `secret-tool lookup service ${SERVICE} account ${KEY_ENV} 2>/dev/null`;
|
|
57
|
+
case "file":
|
|
58
|
+
return `cat "${quoted}" 2>/dev/null`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function readStoredKey(store) {
|
|
62
|
+
try {
|
|
63
|
+
switch (store.kind) {
|
|
64
|
+
case "keychain": {
|
|
65
|
+
const { stdout } = await run("security", ["find-generic-password", "-a", KEY_ENV, "-s", SERVICE, "-w"]);
|
|
66
|
+
return stdout.trim() || null;
|
|
67
|
+
}
|
|
68
|
+
case "secret-service": {
|
|
69
|
+
const { stdout } = await run("secret-tool", ["lookup", "service", SERVICE, "account", KEY_ENV]);
|
|
70
|
+
return stdout.trim() || null;
|
|
71
|
+
}
|
|
72
|
+
case "file": {
|
|
73
|
+
const text = await readText(store.path);
|
|
74
|
+
return text?.trim() || null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function findExistingKey(env) {
|
|
83
|
+
const fromEnv = process.env[KEY_ENV]?.trim();
|
|
84
|
+
if (fromEnv) {
|
|
85
|
+
return { key: fromEnv, source: `${KEY_ENV} in your environment` };
|
|
86
|
+
}
|
|
87
|
+
for (const store of await availableStores(env)) {
|
|
88
|
+
const key = await readStoredKey(store);
|
|
89
|
+
if (key) {
|
|
90
|
+
return { key, source: store.label };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const project = projectKeyFile(env.cwd);
|
|
94
|
+
const text = await readText(project);
|
|
95
|
+
if (text?.trim()) {
|
|
96
|
+
return { key: text.trim(), source: tilde(project) };
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const MARK_START = "# >>> boundless setup >>>";
|
|
101
|
+
const MARK_END = "# <<< boundless setup <<<";
|
|
102
|
+
export function profilePath(env) {
|
|
103
|
+
switch (env.shell) {
|
|
104
|
+
case "zsh":
|
|
105
|
+
return home(".zshrc");
|
|
106
|
+
case "bash":
|
|
107
|
+
return env.os === "macos" ? home(".bash_profile") : home(".bashrc");
|
|
108
|
+
case "fish":
|
|
109
|
+
return home(".config", "fish", "conf.d", "boundless.fish");
|
|
110
|
+
case "powershell":
|
|
111
|
+
return null;
|
|
112
|
+
case "other":
|
|
113
|
+
return home(".profile");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function exposeInShell(env, store) {
|
|
117
|
+
const changed = [];
|
|
118
|
+
if (env.os === "windows") {
|
|
119
|
+
// A user-scoped environment variable read from the private file, so the key never appears in a command line.
|
|
120
|
+
const path = store.path;
|
|
121
|
+
await run("powershell", [
|
|
122
|
+
"-NoProfile",
|
|
123
|
+
"-Command",
|
|
124
|
+
`[Environment]::SetEnvironmentVariable('${KEY_ENV}', (Get-Content -Raw -LiteralPath '${path.replaceAll("'", "''")}').Trim(), 'User')`,
|
|
125
|
+
]);
|
|
126
|
+
changed.push("User environment variable BOUNDLESS_API_KEY");
|
|
127
|
+
return changed;
|
|
128
|
+
}
|
|
129
|
+
const path = profilePath(env);
|
|
130
|
+
if (!path) {
|
|
131
|
+
return changed;
|
|
132
|
+
}
|
|
133
|
+
const line = env.shell === "fish"
|
|
134
|
+
? `set -gx ${KEY_ENV} (${readExpression(store)})`
|
|
135
|
+
: `export ${KEY_ENV}="$(${readExpression(store)})"`;
|
|
136
|
+
const block = `${MARK_START}\n${line}\n${MARK_END}\n`;
|
|
137
|
+
const current = (await readText(path)) ?? "";
|
|
138
|
+
const start = current.indexOf(MARK_START);
|
|
139
|
+
const end = current.indexOf(MARK_END);
|
|
140
|
+
const next = start >= 0 && end > start
|
|
141
|
+
? `${current.slice(0, start)}${block}${current.slice(end + MARK_END.length).replace(/^\n/, "")}`
|
|
142
|
+
: `${current}${current === "" ? "" : current.endsWith("\n") ? "\n" : "\n\n"}${block}`;
|
|
143
|
+
if (next !== current) {
|
|
144
|
+
await writeKeepingMode(path, next);
|
|
145
|
+
changed.push(tilde(path));
|
|
146
|
+
}
|
|
147
|
+
return changed;
|
|
148
|
+
}
|
|
149
|
+
export async function saveProjectKey(cwd, key) {
|
|
150
|
+
const path = projectKeyFile(cwd);
|
|
151
|
+
if (await gitTracked(".boundless/api-key", cwd)) {
|
|
152
|
+
throw new Error(`${tilde(path)} is tracked by git; untrack it before storing a key there.`);
|
|
153
|
+
}
|
|
154
|
+
await writePrivate(path, `${key}\n`);
|
|
155
|
+
const ignored = (await exists(join(cwd, ".git"))) ? await ensureIgnored(cwd, ".boundless/") : false;
|
|
156
|
+
return { ignored, path };
|
|
157
|
+
}
|
package/dist/lib/test.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { usd } from "./catalog.js";
|
|
2
|
+
import { request } from "./config.js";
|
|
3
|
+
const PROMPT = "Reply with exactly: ok";
|
|
4
|
+
function classify(status, body) {
|
|
5
|
+
if (status === 401 || status === 403) {
|
|
6
|
+
return { message: "The key was rejected (authentication).", reason: "auth" };
|
|
7
|
+
}
|
|
8
|
+
if (status === 402 || /budget|credit|balance/i.test(body)) {
|
|
9
|
+
return { message: "The account has no credit for this request.", reason: "credit" };
|
|
10
|
+
}
|
|
11
|
+
if (status === 429) {
|
|
12
|
+
return { message: "Rate limited; try again in a minute.", reason: "rate_limit" };
|
|
13
|
+
}
|
|
14
|
+
if (status === 404 || status === 400) {
|
|
15
|
+
return { message: `The model or request was refused (HTTP ${status}): ${body.slice(0, 160)}`, reason: "model" };
|
|
16
|
+
}
|
|
17
|
+
return { message: `HTTP ${status}: ${body.slice(0, 160)}`, reason: "config" };
|
|
18
|
+
}
|
|
19
|
+
function cost(model, inputTokens, outputTokens) {
|
|
20
|
+
if (!model) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return usd((inputTokens * model.inputPerMillionTokens + outputTokens * model.outputPerMillionTokens) / 1_000_000);
|
|
24
|
+
}
|
|
25
|
+
export async function testRequest(gatewayUrl, key, wire, model, catalog) {
|
|
26
|
+
const url = wire === "anthropic" ? `${gatewayUrl}/v1/messages` : `${gatewayUrl}/v1/chat/completions`;
|
|
27
|
+
const headers = wire === "anthropic"
|
|
28
|
+
? { "anthropic-version": "2023-06-01", "content-type": "application/json", "x-api-key": key }
|
|
29
|
+
: { authorization: `Bearer ${key}`, "content-type": "application/json" };
|
|
30
|
+
try {
|
|
31
|
+
const response = await request(url, {
|
|
32
|
+
body: JSON.stringify({ max_tokens: 1024, messages: [{ content: PROMPT, role: "user" }], model }),
|
|
33
|
+
headers,
|
|
34
|
+
method: "POST",
|
|
35
|
+
signal: AbortSignal.timeout(120_000),
|
|
36
|
+
});
|
|
37
|
+
const text = await response.text();
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
return { kind: "failed", model, wire, ...classify(response.status, text) };
|
|
40
|
+
}
|
|
41
|
+
const body = JSON.parse(text);
|
|
42
|
+
const reply = wire === "anthropic"
|
|
43
|
+
? (body.content ?? []).filter((block) => block.type === "text").map((block) => block.text ?? "").join("")
|
|
44
|
+
: (body.choices?.[0]?.message?.content ?? "");
|
|
45
|
+
const inputTokens = body.usage?.prompt_tokens ?? body.usage?.input_tokens ?? 0;
|
|
46
|
+
const outputTokens = body.usage?.completion_tokens ?? body.usage?.output_tokens ?? 0;
|
|
47
|
+
if (reply.trim() === "") {
|
|
48
|
+
return { kind: "failed", message: "The model returned an empty reply.", model, reason: "model", wire };
|
|
49
|
+
}
|
|
50
|
+
const entry = catalog.find((candidate) => candidate.id === model);
|
|
51
|
+
return { cost: cost(entry, inputTokens, outputTokens), inputTokens, kind: "ok", model, outputTokens, reply: reply.trim(), wire };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return { kind: "failed", message: error.message, model, reason: "config", wire };
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/summary.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function succeeded(summary) {
|
|
2
|
+
return summary.tests.every((test) => test.kind === "ok") && summary.smokes.every((smoke) => smoke.result.kind === "ok");
|
|
3
|
+
}
|
|
4
|
+
export function headline(summary) {
|
|
5
|
+
if (summary.tests.length === 0) {
|
|
6
|
+
return "Boundless is configured, but nothing was tested.";
|
|
7
|
+
}
|
|
8
|
+
if (succeeded(summary)) {
|
|
9
|
+
return summary.smokes.length > 0
|
|
10
|
+
? "Success — Boundless is configured and tested"
|
|
11
|
+
: "Success — Boundless is configured and the API test passed";
|
|
12
|
+
}
|
|
13
|
+
return "Boundless is configured, but a test failed";
|
|
14
|
+
}
|
package/dist/ui/facts.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { BAD, MUTED, OK, WARN } from "./theme.js";
|
|
4
|
+
export function Facts({ facts }) {
|
|
5
|
+
const width = Math.max(...facts.map((fact) => fact.label.length));
|
|
6
|
+
return (_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Box, { flexDirection: "column", children: facts.map((fact) => (_jsxs(Box, { children: [_jsxs(Box, { flexShrink: 0, width: width + 3, children: [_jsxs(Text, { color: MUTED, children: [fact.label.padStart(width), " "] }), _jsx(Mark, { state: fact.state ?? "ok" })] }), _jsx(Box, { flexShrink: 1, children: _jsx(Text, { wrap: "wrap", children: fact.value }) })] }, fact.label))) }) }));
|
|
7
|
+
}
|
|
8
|
+
function Mark({ state }) {
|
|
9
|
+
switch (state) {
|
|
10
|
+
case "ok":
|
|
11
|
+
return _jsx(Text, { color: OK, children: "\u2714 " });
|
|
12
|
+
case "warn":
|
|
13
|
+
return _jsx(Text, { color: WARN, children: "! " });
|
|
14
|
+
case "bad":
|
|
15
|
+
return _jsx(Text, { color: BAD, children: "\u2718 " });
|
|
16
|
+
case "none":
|
|
17
|
+
return _jsx(Text, { color: MUTED, children: "\u00B7 " });
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/ui/frame.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
|
+
import { VERSION } from "../version.js";
|
|
4
|
+
import { BRAND, MUTED } from "./theme.js";
|
|
5
|
+
export function Frame({ children, hints }) {
|
|
6
|
+
const { columns, rows } = useWindowSize();
|
|
7
|
+
const width = Math.min(Math.max(columns - 4, 40), 88);
|
|
8
|
+
const title = ` Boundless Setup v${VERSION}`;
|
|
9
|
+
const right = "boundless.md ";
|
|
10
|
+
const pad = Math.max(columns - title.length - right.length, 1);
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, width: columns, children: [_jsxs(Text, { backgroundColor: BRAND, color: "black", children: [title, " ".repeat(pad), right] }), _jsx(Box, { alignItems: "center", flexDirection: "column", flexGrow: 1, justifyContent: "center", children: _jsx(Box, { flexDirection: "column", width: width, children: children }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: MUTED, children: hints }) })] }));
|
|
12
|
+
}
|
|
13
|
+
export function Title({ children }) {
|
|
14
|
+
return (_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: BRAND, children: children }) }));
|
|
15
|
+
}
|
|
16
|
+
export function Muted({ children, center }) {
|
|
17
|
+
return (_jsx(Box, { justifyContent: center ? "center" : "flex-start", marginBottom: 1, children: _jsx(Text, { color: MUTED, children: children }) }));
|
|
18
|
+
}
|
|
19
|
+
export function Para({ children }) {
|
|
20
|
+
return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: children }) }));
|
|
21
|
+
}
|
package/dist/ui/menu.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { BRAND, MUTED } from "./theme.js";
|
|
5
|
+
export function Select({ onCancel, onSelect, options, }) {
|
|
6
|
+
const [index, setIndex] = useState(0);
|
|
7
|
+
const move = (delta) => {
|
|
8
|
+
let next = index;
|
|
9
|
+
for (let step = 0; step < options.length; step += 1) {
|
|
10
|
+
next = (next + delta + options.length) % options.length;
|
|
11
|
+
if (!options[next]?.disabled) {
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
setIndex(next);
|
|
16
|
+
};
|
|
17
|
+
useInput((input, key) => {
|
|
18
|
+
if (key.upArrow || input === "k") {
|
|
19
|
+
move(-1);
|
|
20
|
+
}
|
|
21
|
+
else if (key.downArrow || input === "j") {
|
|
22
|
+
move(1);
|
|
23
|
+
}
|
|
24
|
+
else if (key.return) {
|
|
25
|
+
const chosen = options[index];
|
|
26
|
+
if (chosen && !chosen.disabled) {
|
|
27
|
+
onSelect(chosen.value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (key.escape && onCancel) {
|
|
31
|
+
onCancel();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return (_jsx(Box, { alignItems: "flex-start", flexDirection: "column", children: options.map((option, position) => {
|
|
35
|
+
const active = position === index;
|
|
36
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: BRAND, children: active ? "▸ " : " " }), _jsx(Text, { bold: active, color: option.disabled ? MUTED : active ? BRAND : undefined, dimColor: option.disabled, children: option.label }), option.hint ? _jsxs(Text, { color: MUTED, children: [" ", option.hint] }) : null] }, option.value));
|
|
37
|
+
}) }));
|
|
38
|
+
}
|
|
39
|
+
export function MultiSelect({ initial, onCancel, onSubmit, options, }) {
|
|
40
|
+
const [index, setIndex] = useState(0);
|
|
41
|
+
const [chosen, setChosen] = useState(new Set(initial));
|
|
42
|
+
useInput((input, key) => {
|
|
43
|
+
if (key.upArrow || input === "k") {
|
|
44
|
+
setIndex((index + options.length - 1) % options.length);
|
|
45
|
+
}
|
|
46
|
+
else if (key.downArrow || input === "j") {
|
|
47
|
+
setIndex((index + 1) % options.length);
|
|
48
|
+
}
|
|
49
|
+
else if (input === " ") {
|
|
50
|
+
const value = options[index]?.value;
|
|
51
|
+
if (value !== undefined) {
|
|
52
|
+
const next = new Set(chosen);
|
|
53
|
+
if (next.has(value)) {
|
|
54
|
+
next.delete(value);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
next.add(value);
|
|
58
|
+
}
|
|
59
|
+
setChosen(next);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (key.return) {
|
|
63
|
+
onSubmit(options.map((option) => option.value).filter((value) => chosen.has(value)));
|
|
64
|
+
}
|
|
65
|
+
else if (key.escape && onCancel) {
|
|
66
|
+
onCancel();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return (_jsx(Box, { alignItems: "flex-start", flexDirection: "column", children: options.map((option, position) => {
|
|
70
|
+
const active = position === index;
|
|
71
|
+
const on = chosen.has(option.value);
|
|
72
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: BRAND, children: active ? "▸ " : " " }), _jsx(Text, { color: on ? BRAND : MUTED, children: on ? "◉ " : "○ " }), _jsx(Text, { bold: active, color: active ? BRAND : undefined, children: option.label }), option.hint ? _jsxs(Text, { color: MUTED, children: [" ", option.hint] }) : null] }, option.value));
|
|
73
|
+
}) }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { BRAND } from "./theme.js";
|
|
5
|
+
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
6
|
+
export function Spinner({ label }) {
|
|
7
|
+
const [frame, setFrame] = useState(0);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const timer = setInterval(() => setFrame((current) => (current + 1) % FRAMES.length), 80);
|
|
10
|
+
return () => clearInterval(timer);
|
|
11
|
+
}, []);
|
|
12
|
+
return (_jsxs(Box, { justifyContent: "center", marginBottom: 1, children: [_jsxs(Text, { color: BRAND, children: [FRAMES[frame], " "] }), _jsx(Text, { children: label })] }));
|
|
13
|
+
}
|
package/dist/ui/table.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { usd } from "../lib/catalog.js";
|
|
4
|
+
import { MUTED } from "./theme.js";
|
|
5
|
+
const columns = (model) => [
|
|
6
|
+
model.id,
|
|
7
|
+
usd(model.inputPerMillionTokens),
|
|
8
|
+
usd(model.outputPerMillionTokens),
|
|
9
|
+
model.contextTokens.toLocaleString("en-US"),
|
|
10
|
+
];
|
|
11
|
+
export function ModelTable({ models }) {
|
|
12
|
+
const header = ["ID", "Input /1M", "Output /1M", "Context"];
|
|
13
|
+
const rows = models.map(columns);
|
|
14
|
+
const widths = header.map((cell, column) => Math.max(cell.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
15
|
+
const line = (cells) => cells.map((cell, column) => (column === 0 ? cell.padEnd(widths[column] ?? 0) : cell.padStart(widths[column] ?? 0))).join(" ");
|
|
16
|
+
return (_jsxs(Box, { alignItems: "center", flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: MUTED, children: line(header) }), rows.map((row) => (_jsx(Text, { children: line(row) }, row[0])))] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { BRAND, MUTED } from "./theme.js";
|
|
5
|
+
export function TextInput({ onCancel, onSubmit, placeholder, }) {
|
|
6
|
+
const [value, setValue] = useState("");
|
|
7
|
+
useInput((input, key) => {
|
|
8
|
+
if (key.return) {
|
|
9
|
+
onSubmit(value.trim());
|
|
10
|
+
}
|
|
11
|
+
else if (key.escape) {
|
|
12
|
+
onCancel();
|
|
13
|
+
}
|
|
14
|
+
else if (key.backspace || key.delete) {
|
|
15
|
+
setValue(value.slice(0, -1));
|
|
16
|
+
}
|
|
17
|
+
else if (!(key.ctrl || key.meta) && input) {
|
|
18
|
+
setValue(value + input);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: BRAND, children: "\u25B8 " }), value ? _jsx(Text, { children: value }) : _jsx(Text, { color: MUTED, children: placeholder }), _jsx(Text, { color: BRAND, children: "\u258F" })] }));
|
|
22
|
+
}
|
package/dist/ui/theme.js
ADDED
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boundless.network/setup",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Set up Boundless as the inference provider for your coding harness. Run: npx -y @boundless.network/setup@latest",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"boundless-setup": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
|
|
18
|
+
"start": "node dist/cli.js",
|
|
19
|
+
"test": "tsc -p tsconfig.test.json && node --test \".test-dist/**/*.test.js\"",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"ink": "^7.1.1",
|
|
25
|
+
"react": "^19.3.0",
|
|
26
|
+
"smol-toml": "^1.9.0",
|
|
27
|
+
"yaml": "^2.9.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^24.0.0",
|
|
31
|
+
"@types/react": "^19.3.0",
|
|
32
|
+
"typescript": "^5.9.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/boundless-network/inference-serving.git",
|
|
40
|
+
"directory": "setup"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://boundless.md"
|
|
43
|
+
}
|