@boole-digital/cli 0.2.1 → 0.2.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/index.js +66 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
chmodSync,
|
|
17
17
|
rmSync
|
|
18
18
|
} from "node:fs";
|
|
19
|
-
var API_BASE = (process.env.BOOLE_API_BASE || "https://
|
|
19
|
+
var API_BASE = (process.env.BOOLE_API_BASE || "https://api.cest-finyx.com").replace(/\/+$/, "");
|
|
20
|
+
var WEB_BASE = (process.env.BOOLE_WEB_BASE || process.env.BOOLE_API_BASE || "https://trade.boole.markets").replace(/\/+$/, "");
|
|
20
21
|
var CONFIG_DIR = process.env.BOOLE_HOME || join(homedir(), ".boole");
|
|
21
22
|
var CRED_PATH = join(CONFIG_DIR, "credentials.json");
|
|
22
23
|
var SESSION_PATH = join(CONFIG_DIR, "session.env");
|
|
@@ -145,12 +146,12 @@ function decodePayload(code) {
|
|
|
145
146
|
};
|
|
146
147
|
}
|
|
147
148
|
function successHtml(email, dashboardUrl) {
|
|
148
|
-
const who = email ? `<p>Signed in as <b
|
|
149
|
+
const who = email ? `<p class="who">Signed in as: <b>${email.replace(/[<>&]/g, "")}</b></p>` : "";
|
|
149
150
|
const url = dashboardUrl.replace(/"/g, "");
|
|
150
151
|
return `<!doctype html><meta charset=utf-8><title>Boole</title>
|
|
151
|
-
<style>body{font:16px -apple-system,system-ui,sans-serif;background:#0b0b0b;color:#eaeaea;display:grid;place-items:center;height:100vh;margin:0}div{text-align:center}h1{font-size:22px}p{color:#9a9a9a;margin:
|
|
152
|
-
<div><h1
|
|
153
|
-
<a class="btn" href="${url}">
|
|
152
|
+
<style>body{font:16px -apple-system,system-ui,sans-serif;background:#0b0b0b;color:#eaeaea;display:grid;place-items:center;height:100vh;margin:0}div{text-align:center}h1{font-size:22px;font-weight:600;margin:0 0 14px}p{margin:6px 0;color:#9a9a9a}.who b{color:#eaeaea;font-weight:600}.tag{color:#7a7a7a;margin-top:16px}.btn{display:inline-block;margin-top:22px;padding:10px 22px;border-radius:8px;background:#1c1c22;color:#c8c8d0;text-decoration:none;font-weight:500;border:1px solid #2e2e38}.btn:hover{background:#26262e;color:#eaeaea}</style>
|
|
153
|
+
<div><h1>Authenticated to Boole</h1>${who}<p class="tag">Go build something great</p>
|
|
154
|
+
<a class="btn" href="${url}">Continue to Boole</a></div>`;
|
|
154
155
|
}
|
|
155
156
|
function emailFromCode(code) {
|
|
156
157
|
try {
|
|
@@ -160,7 +161,7 @@ function emailFromCode(code) {
|
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
async function loginPaste() {
|
|
163
|
-
const url = `${
|
|
164
|
+
const url = `${WEB_BASE}/cli-auth?paste=1`;
|
|
164
165
|
info("Opening the Boole login page\u2026");
|
|
165
166
|
log(` ${c.dim("If it does not open, visit:")} ${c.cyan(url)}`);
|
|
166
167
|
openBrowser(url);
|
|
@@ -190,7 +191,7 @@ async function loginLoopback() {
|
|
|
190
191
|
return;
|
|
191
192
|
}
|
|
192
193
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
193
|
-
res.end(successHtml(emailFromCode(code),
|
|
194
|
+
res.end(successHtml(emailFromCode(code), WEB_BASE));
|
|
194
195
|
queueMicrotask(() => resolveCode(code));
|
|
195
196
|
});
|
|
196
197
|
const port = await new Promise((resolve2) => {
|
|
@@ -199,7 +200,7 @@ async function loginLoopback() {
|
|
|
199
200
|
resolve2(typeof addr === "object" && addr ? addr.port : 0);
|
|
200
201
|
});
|
|
201
202
|
});
|
|
202
|
-
const url = `${
|
|
203
|
+
const url = `${WEB_BASE}/cli-auth?port=${port}`;
|
|
203
204
|
info("Opening the Boole login page in your browser\u2026");
|
|
204
205
|
log(` ${c.dim("If it does not open, visit:")} ${c.cyan(url)}`);
|
|
205
206
|
openBrowser(url);
|
|
@@ -283,14 +284,27 @@ var BooleApi = class {
|
|
|
283
284
|
async request(path, init2 = {}) {
|
|
284
285
|
await this.ensureFresh();
|
|
285
286
|
if (!this.creds) throw new AuthError("Not logged in. Run `boole login`.");
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
const TIMEOUT_MS = Number(process.env.BOOLE_TIMEOUT_MS) || 12e3;
|
|
288
|
+
const doFetch = async () => {
|
|
289
|
+
const ac = new AbortController();
|
|
290
|
+
const t = setTimeout(() => ac.abort(), TIMEOUT_MS);
|
|
291
|
+
try {
|
|
292
|
+
return await fetch(`${API_BASE}${path}`, {
|
|
293
|
+
...init2,
|
|
294
|
+
signal: ac.signal,
|
|
295
|
+
headers: {
|
|
296
|
+
"Content-Type": "application/json",
|
|
297
|
+
...init2.headers || {},
|
|
298
|
+
Authorization: `Bearer ${this.creds.access_token}`
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
} catch (e) {
|
|
302
|
+
if (e?.name === "AbortError") throw new Error(`${path}: timed out after ${TIMEOUT_MS / 1e3}s (network or box waking up)`);
|
|
303
|
+
throw e;
|
|
304
|
+
} finally {
|
|
305
|
+
clearTimeout(t);
|
|
292
306
|
}
|
|
293
|
-
}
|
|
307
|
+
};
|
|
294
308
|
let res = await doFetch();
|
|
295
309
|
if (res.status === 401) {
|
|
296
310
|
this.creds.expires_at = 0;
|
|
@@ -333,8 +347,15 @@ var BooleApi = class {
|
|
|
333
347
|
}
|
|
334
348
|
// Returns the plaintext SSH password for a droplet. Field name varies by
|
|
335
349
|
// server version, so probe the likely shapes.
|
|
336
|
-
|
|
337
|
-
|
|
350
|
+
// The ssh-password endpoint is gated by a server-side connect access code
|
|
351
|
+
// (env.CLAUDE_CONNECT_ACCESS_CODE) with brute-force lockout, so we MUST send it
|
|
352
|
+
// in the POST body (the web app does the same). Without it the backend replies
|
|
353
|
+
// "Incorrect access code".
|
|
354
|
+
async getSshPassword(id, accessCode) {
|
|
355
|
+
const body = await this.request(`/api/v1/droplets/${id}/ssh-password`, {
|
|
356
|
+
method: "POST",
|
|
357
|
+
body: JSON.stringify({ accessCode: (accessCode || "").trim() })
|
|
358
|
+
});
|
|
338
359
|
const pw = body?.password ?? body?.ssh_password ?? body?.data?.password ?? body?.data?.ssh_password;
|
|
339
360
|
if (!pw) throw new Error("SSH password not present in API response (check field name in api.ts:getSshPassword).");
|
|
340
361
|
return String(pw);
|
|
@@ -368,7 +389,7 @@ function onboardingNotice() {
|
|
|
368
389
|
warn("No trading computer yet \u2014 and you can\u2019t deploy your first one from the CLI.");
|
|
369
390
|
log(" Finish onboarding in the Boole app first (this is required):");
|
|
370
391
|
log(` 1. Create your account 2. Agree to the terms 3. Set up billing`);
|
|
371
|
-
log(` ${c.cyan(
|
|
392
|
+
log(` ${c.cyan(WEB_BASE)}`);
|
|
372
393
|
log(` Deploy your first trading computer there, then come back and run ${c.cyan("boole connect")}.`);
|
|
373
394
|
log("");
|
|
374
395
|
}
|
|
@@ -389,7 +410,7 @@ function orient() {
|
|
|
389
410
|
log(`${c.bold("Do this next:")} ${c.cyan("boole login")}`);
|
|
390
411
|
} else if (!sess) {
|
|
391
412
|
log(`${c.bold("You are:")} logged in as ${c.bold(creds.user.email || creds.user.id)}, ${c.yellow("no box connected")}.`);
|
|
392
|
-
log(`${c.bold("Do this next:")} ${c.cyan("boole connect")} ${c.dim(`# no trading computer yet? finish onboarding at ${
|
|
413
|
+
log(`${c.bold("Do this next:")} ${c.cyan("boole connect")} ${c.dim(`# no trading computer yet? finish onboarding at ${WEB_BASE}`)}`);
|
|
393
414
|
} else {
|
|
394
415
|
log(`${c.bold("You are:")} connected to ${c.bold(sess.agent || sess.ip)} ${c.green("\u2014 ready")}.`);
|
|
395
416
|
log(c.bold("Do this next:"));
|
|
@@ -445,7 +466,8 @@ function pickAgentId(droplets) {
|
|
|
445
466
|
if (ready.length === 1) return { id: ready[0].id, name: ready[0].name };
|
|
446
467
|
return null;
|
|
447
468
|
}
|
|
448
|
-
async function summary() {
|
|
469
|
+
async function summary(opts = {}) {
|
|
470
|
+
const snapshot = opts.snapshot !== false;
|
|
449
471
|
const creds = loadCredentials();
|
|
450
472
|
if (!creds) {
|
|
451
473
|
warn("Not logged in. Run `boole login`.");
|
|
@@ -483,7 +505,7 @@ async function summary() {
|
|
|
483
505
|
log(c.bold(`Trading computers (${droplets.length})`));
|
|
484
506
|
printAgents(droplets);
|
|
485
507
|
const pick = pickAgentId(droplets);
|
|
486
|
-
if (pick) {
|
|
508
|
+
if (pick && snapshot) {
|
|
487
509
|
log("");
|
|
488
510
|
log(c.bold(`Snapshot \u2014 ${pick.name}`));
|
|
489
511
|
try {
|
|
@@ -493,6 +515,8 @@ async function summary() {
|
|
|
493
515
|
} catch {
|
|
494
516
|
warn("trading computer unreachable (tunnel may be waking up \u2014 retry in a moment)");
|
|
495
517
|
}
|
|
518
|
+
} else if (pick) {
|
|
519
|
+
log(c.dim(` Run ${c.cyan("boole status")} for balances + live snapshot.`));
|
|
496
520
|
}
|
|
497
521
|
operatingBrief();
|
|
498
522
|
}
|
|
@@ -583,7 +607,23 @@ async function connect(opts = {}) {
|
|
|
583
607
|
if (!READY(target)) die(`Trading computer "${target.name}" is not ready yet (status=${target.status}).`);
|
|
584
608
|
if (!target.ip_address) die(`Trading computer "${target.name}" has no IP yet. Try again shortly.`);
|
|
585
609
|
info(`Fetching access for ${c.bold(target.name)}\u2026`);
|
|
586
|
-
|
|
610
|
+
let password;
|
|
611
|
+
try {
|
|
612
|
+
password = await api.getSshPassword(target.id, opts.code || "");
|
|
613
|
+
} catch (e) {
|
|
614
|
+
if (!/access code/i.test(e?.message || "")) throw e;
|
|
615
|
+
let code = (opts.code || "").trim();
|
|
616
|
+
if (!code && process.stdin.isTTY) {
|
|
617
|
+
code = (await prompt("This computer needs a connect access code (from the Boole app):")).trim();
|
|
618
|
+
}
|
|
619
|
+
if (!code) die("This computer needs a connect access code: run `boole connect --code <code>` (from the Boole app).");
|
|
620
|
+
try {
|
|
621
|
+
password = await api.getSshPassword(target.id, code);
|
|
622
|
+
} catch (e2) {
|
|
623
|
+
if (/access code/i.test(e2?.message || "")) die("Connect access code was rejected \u2014 re-run `boole connect --code <code>` with the correct code.");
|
|
624
|
+
throw e2;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
587
627
|
saveSession({ agent: target.name, dropletId: target.id, ip: target.ip_address, password, sshUser: "customer" });
|
|
588
628
|
ok(`Connected to ${c.bold(target.name)} (${target.ip_address}).`);
|
|
589
629
|
log("");
|
|
@@ -711,7 +751,7 @@ function init(opts = {}) {
|
|
|
711
751
|
}
|
|
712
752
|
|
|
713
753
|
// src/index.ts
|
|
714
|
-
var VERSION = "0.2.
|
|
754
|
+
var VERSION = "0.2.4";
|
|
715
755
|
function parse(argv) {
|
|
716
756
|
const _ = [];
|
|
717
757
|
const flags = {};
|
|
@@ -756,7 +796,7 @@ ${c.bold("Operate the box")}
|
|
|
756
796
|
${c.bold("Other")}
|
|
757
797
|
help, --help \xB7 version, --version
|
|
758
798
|
|
|
759
|
-
Env: ${c.dim("BOOLE_API_BASE")} (
|
|
799
|
+
Env: ${c.dim("BOOLE_API_BASE")} (REST API) \xB7 ${c.dim("BOOLE_WEB_BASE")} (login page) \xB7 ${c.dim("BOOLE_HOME")} \xB7 ${c.dim("BOOLE_TIMEOUT_MS")}
|
|
760
800
|
`;
|
|
761
801
|
async function main() {
|
|
762
802
|
const { _, flags } = parse(process.argv.slice(2));
|
|
@@ -776,7 +816,7 @@ async function main() {
|
|
|
776
816
|
switch (cmd) {
|
|
777
817
|
case "login":
|
|
778
818
|
await login({ paste: !!flags.paste });
|
|
779
|
-
await summary();
|
|
819
|
+
await summary({ snapshot: false });
|
|
780
820
|
break;
|
|
781
821
|
case "logout":
|
|
782
822
|
await logout();
|
|
@@ -798,7 +838,7 @@ async function main() {
|
|
|
798
838
|
await provision({ name: str(flags.name), region: str(flags.region), size: str(flags.size) });
|
|
799
839
|
break;
|
|
800
840
|
case "connect":
|
|
801
|
-
await connect({ name: _[1] });
|
|
841
|
+
await connect({ name: _[1], code: str(flags.code) });
|
|
802
842
|
break;
|
|
803
843
|
case "init":
|
|
804
844
|
init({ dir: _[1], force: !!flags.force });
|
package/package.json
CHANGED