@secondlayer/cli 21.1.1 → 21.3.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/dist/cli.js CHANGED
@@ -202692,7 +202692,7 @@ var {
202692
202692
  // package.json
202693
202693
  var package_default = {
202694
202694
  name: "@secondlayer/cli",
202695
- version: "21.1.1",
202695
+ version: "21.3.0",
202696
202696
  description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
202697
202697
  repository: {
202698
202698
  type: "git",
@@ -202732,7 +202732,7 @@ var package_default = {
202732
202732
  "@secondlayer/bundler": "^0.7.1",
202733
202733
  "@secondlayer/scaffold": "^1.6.2",
202734
202734
  "@secondlayer/sdk": "^15.1.0",
202735
- "@secondlayer/shared": "^13.1.0",
202735
+ "@secondlayer/shared": "^13.7.0",
202736
202736
  "@secondlayer/stacks": "^6.4.0",
202737
202737
  "@secondlayer/subgraphs": "^7.2.0",
202738
202738
  "@secondlayer/verify": "^0.6.0",
@@ -204926,6 +204926,35 @@ function registerArchiveCommand(program) {
204926
204926
  init_http();
204927
204927
  init_output();
204928
204928
  var PACKS = [10, 25, 50, 100];
204929
+ var DAY_MS = 24 * 60 * 60 * 1000;
204930
+ function rowsUsageDays(daily, days, now) {
204931
+ const byDate = new Map;
204932
+ for (const d of daily) {
204933
+ const day = byDate.get(d.date) ?? {
204934
+ date: d.date,
204935
+ rows: 0,
204936
+ free: 0,
204937
+ paid: 0
204938
+ };
204939
+ day.rows += d.rows;
204940
+ day.free += d.freeRows;
204941
+ day.paid += d.paidRows;
204942
+ byDate.set(d.date, day);
204943
+ }
204944
+ const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
204945
+ return Array.from({ length: days }, (_, i) => {
204946
+ const date = new Date(today - (days - 1 - i) * DAY_MS).toISOString().slice(0, 10);
204947
+ return byDate.get(date) ?? { date, rows: 0, free: 0, paid: 0 };
204948
+ });
204949
+ }
204950
+ function parseDays(raw) {
204951
+ const n = Number(raw);
204952
+ if (!Number.isInteger(n) || n < 1 || n > 30) {
204953
+ error("--days must be a whole number from 1 to 30");
204954
+ process.exit(1);
204955
+ }
204956
+ return n;
204957
+ }
204929
204958
  function parsePack(raw) {
204930
204959
  const n = Number(raw);
204931
204960
  if (!PACKS.includes(n)) {
@@ -204993,10 +205022,49 @@ async function runBalance(opts) {
204993
205022
  }
204994
205023
  });
204995
205024
  }
205025
+ async function runUsage(opts) {
205026
+ const days = parseDays(opts.days);
205027
+ let res;
205028
+ try {
205029
+ res = await httpArchiveOps("/api/billing/rows");
205030
+ } catch (err) {
205031
+ if (err instanceof CliHttpError) {
205032
+ error(err.message);
205033
+ process.exit(1);
205034
+ }
205035
+ throw err;
205036
+ }
205037
+ output({
205038
+ json: opts.json,
205039
+ data: res,
205040
+ human: () => {
205041
+ const n = (v) => v.toLocaleString("en-US");
205042
+ const lines = rowsUsageDays(res.daily, days, new Date);
205043
+ console.log(formatTable(["Date", "Rows", "Free", "Paid"], lines.map((d) => [d.date, n(d.rows), n(d.free), n(d.paid)])));
205044
+ console.log();
205045
+ const { allowance } = res;
205046
+ const pairs = [
205047
+ [
205048
+ "This month",
205049
+ `${n(allowance.usedThisMonth)} rows, ${n(allowance.monthly)} free${allowance.exhaustedOn ? ` (used up ${allowance.exhaustedOn})` : ""}`
205050
+ ]
205051
+ ];
205052
+ for (const source of res.bySource) {
205053
+ const name = source.source === "streams" ? "Streams" : "Index";
205054
+ pairs.push([
205055
+ name,
205056
+ `${n(source.rows)} rows, ${n(source.calls)} calls, 30 days`
205057
+ ]);
205058
+ }
205059
+ console.log(formatKeyValue(pairs));
205060
+ }
205061
+ });
205062
+ }
204996
205063
  function registerCreditsCommand(program) {
204997
205064
  const credits = program.command("credits").description("Buy and check archive credits");
204998
205065
  credits.command("buy").description("Open a one-time Stripe checkout for archive credits").requiredOption("--email <email>", "Receipt email; becomes the account").option("--pack <usd>", `One of ${PACKS.join(", ")}`, "25").option("--json", "Output as JSON").action((opts) => runBuy(opts));
204999
205066
  credits.command("balance").description("Show prepaid archive credit balance").option("--json", "Output as JSON").action((opts) => runBalance(opts));
205067
+ credits.command("usage").description("Rows delivered per day: free vs paid, by source").option("--days <n>", "Days to show (max 30)", "30").option("--json", "Output as JSON").action((opts) => runUsage(opts));
205000
205068
  credits.command("refill").description("Opt-in auto-refill when the balance drops under a threshold").option("--below <usd>", "Trigger when balance is under this USD amount").option("--pack <usd>", `Pack to buy (${PACKS.join(", ")})`, "25").option("--off", "Turn auto-refill off").option("--json", "Output as JSON").action((opts) => runRefill(opts));
205001
205069
  }
205002
205070
  async function runRefill(opts) {
@@ -207733,9 +207801,32 @@ function registerWhoamiCommand(program) {
207733
207801
  // src/commands/login.ts
207734
207802
  init_api_url();
207735
207803
  init_http();
207804
+ import { hostname } from "node:os";
207805
+ import { createInterface } from "node:readline";
207806
+ import { confirm as confirm3, input } from "@inquirer/prompts";
207807
+
207808
+ // src/lib/open-browser.ts
207809
+ import { spawn as spawn3 } from "node:child_process";
207810
+ function browserCommand(url, platform = process.platform) {
207811
+ if (platform === "darwin")
207812
+ return { cmd: "open", args: [url] };
207813
+ if (platform === "win32") {
207814
+ return { cmd: "cmd", args: ["/c", "start", "", url] };
207815
+ }
207816
+ return { cmd: "xdg-open", args: [url] };
207817
+ }
207818
+ function openBrowser(url) {
207819
+ try {
207820
+ const { cmd, args } = browserCommand(url);
207821
+ const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
207822
+ child.on("error", () => {});
207823
+ child.unref();
207824
+ } catch {}
207825
+ }
207826
+
207827
+ // src/commands/login.ts
207736
207828
  init_output();
207737
207829
  init_session();
207738
- import { confirm as confirm3, input } from "@inquirer/prompts";
207739
207830
  function sessionExpiry() {
207740
207831
  return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
207741
207832
  }
@@ -207770,31 +207861,127 @@ async function runTokenLogin(opts) {
207770
207861
  }, target);
207771
207862
  success(`Logged in as ${account.email} (${target})`);
207772
207863
  }
207773
- async function runLoginFlow(options = {}) {
207774
- const target = loginTarget(options);
207775
- if (!options.force) {
207776
- const existing = await readSession(target);
207777
- if (existing) {
207778
- info(`Already logged in as ${existing.email} (${target}).`);
207779
- if (!process.stdin.isTTY) {
207780
- info(dim("Run 'secondlayer logout' first, or re-run with --force to switch accounts."));
207781
- return;
207782
- }
207783
- try {
207784
- const proceed = await confirm3({
207785
- message: "Log in as a different user?",
207786
- default: false
207787
- });
207788
- if (!proceed) {
207789
- info(dim("Run 'secondlayer logout' to sign out."));
207790
- return;
207864
+ async function shouldContinueLogin(target, options) {
207865
+ if (options.force)
207866
+ return true;
207867
+ const existing = await readSession(target);
207868
+ if (!existing)
207869
+ return true;
207870
+ info(`Already logged in as ${existing.email} (${target}).`);
207871
+ const hint = "Run 'secondlayer logout' first, or re-run with --force to switch accounts.";
207872
+ if (!process.stdin.isTTY) {
207873
+ info(dim(hint));
207874
+ return false;
207875
+ }
207876
+ try {
207877
+ const proceed = await confirm3({
207878
+ message: "Log in as a different user?",
207879
+ default: false
207880
+ });
207881
+ if (!proceed) {
207882
+ info(dim("Run 'secondlayer logout' to sign out."));
207883
+ return false;
207884
+ }
207885
+ } catch {
207886
+ info(dim(hint));
207887
+ return false;
207888
+ }
207889
+ return true;
207890
+ }
207891
+ async function pollDeviceToken(start, deps) {
207892
+ const deadline = deps.now() + start.expiresIn * 1000;
207893
+ let failures = 0;
207894
+ while (deps.now() < deadline) {
207895
+ await deps.sleep(start.interval * 1000);
207896
+ try {
207897
+ const res = await deps.post("/api/cli/device/token", { deviceCode: start.deviceCode });
207898
+ return { kind: "approved", key: res.key, account: res.account };
207899
+ } catch (err) {
207900
+ if (err instanceof CliHttpError) {
207901
+ if (err.status === 428) {
207902
+ failures = 0;
207903
+ continue;
207791
207904
  }
207792
- } catch {
207793
- info(dim("Run 'secondlayer logout' first, or re-run with --force to switch accounts."));
207794
- return;
207905
+ if (err.status === 403 && err.body.error === "access_denied") {
207906
+ return { kind: "denied" };
207907
+ }
207908
+ if (err.status === 409 && err.body.error === "key_limit") {
207909
+ return { kind: "key_limit" };
207910
+ }
207911
+ if (err.status === 410)
207912
+ return { kind: "expired" };
207913
+ throw err;
207795
207914
  }
207915
+ if (++failures >= 3)
207916
+ throw err;
207796
207917
  }
207797
207918
  }
207919
+ return { kind: "expired" };
207920
+ }
207921
+ var KEY_LIMIT_MESSAGE = "You have 50 keys. Revoke one under Keys, then run secondlayer login again.";
207922
+ function clientName() {
207923
+ return hostname().trim().slice(0, 64) || "this machine";
207924
+ }
207925
+ async function runDeviceLogin(options = {}) {
207926
+ const target = loginTarget(options);
207927
+ if (!await shouldContinueLogin(target, options))
207928
+ return;
207929
+ let start;
207930
+ try {
207931
+ start = await httpAt(target, "/api/cli/device", {
207932
+ method: "POST",
207933
+ body: { clientName: clientName() }
207934
+ });
207935
+ } catch (err) {
207936
+ error(err instanceof Error ? err.message : String(err));
207937
+ process.exit(1);
207938
+ }
207939
+ info(`First copy your one-time code: ${start.userCode}`);
207940
+ const readline = process.stdin.isTTY ? createInterface({ input: process.stdin }) : null;
207941
+ if (readline) {
207942
+ info("Press Enter to open secondlayer.tools in your browser...");
207943
+ readline.once("line", () => openBrowser(start.verificationUrl));
207944
+ }
207945
+ info(dim(`Or open ${start.verificationUrl}`));
207946
+ info(dim("Waiting for you to approve in the browser..."));
207947
+ let outcome;
207948
+ try {
207949
+ outcome = await pollDeviceToken(start, {
207950
+ post: (path, body) => httpAt(target, path, { method: "POST", body }),
207951
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
207952
+ now: Date.now
207953
+ });
207954
+ } catch (err) {
207955
+ readline?.close();
207956
+ error(err instanceof Error ? err.message : String(err));
207957
+ process.exit(1);
207958
+ }
207959
+ readline?.close();
207960
+ if (outcome.kind === "denied") {
207961
+ error("Cancelled in the browser.");
207962
+ process.exit(1);
207963
+ }
207964
+ if (outcome.kind === "key_limit") {
207965
+ error(KEY_LIMIT_MESSAGE);
207966
+ process.exit(1);
207967
+ }
207968
+ if (outcome.kind === "expired") {
207969
+ error("The code expired. Run secondlayer login again.");
207970
+ process.exit(1);
207971
+ }
207972
+ await writeSession({
207973
+ token: outcome.key,
207974
+ email: outcome.account.email,
207975
+ accountId: outcome.account.id,
207976
+ expiresAt: sessionExpiry()
207977
+ }, target);
207978
+ success(`Logged in as ${outcome.account.email} on ${clientName()}`);
207979
+ info(dim("This machine has its own key. Revoke it under Keys."));
207980
+ }
207981
+ async function runLoginFlow(options = {}) {
207982
+ const target = loginTarget(options);
207983
+ if (!await shouldContinueLogin(target, options))
207984
+ return;
207798
207985
  const email = await input({
207799
207986
  message: "Email",
207800
207987
  validate: (v) => /^.+@.+\..+$/.test(v) ? true : "Invalid email"
@@ -207843,10 +208030,12 @@ async function runLoginFlow(options = {}) {
207843
208030
  }
207844
208031
  }
207845
208032
  function registerLoginCommand(program) {
207846
- program.command("login").description("Log in to Secondlayer (magic-link email)").option("--force", "Skip the already-logged-in check and re-run the flow").option("--with-token", "Read an API key from stdin (non-interactive)").option("--credits", "Log in to the archive credits account used by bootstrap, repair, and credits").addHelpText("after", `
208033
+ program.command("login").description("Log in to Secondlayer through your browser").option("--force", "Skip the already-logged-in check and re-run the flow").option("--email", "Sign in with an emailed 6-digit code instead of a browser").option("--with-token", "Read an API key from stdin (non-interactive)").option("--credits", "Log in to the archive credits account used by bootstrap, repair, and credits").addHelpText("after", `
207847
208034
  Examples:
208035
+ $ secondlayer login
208036
+ $ secondlayer login --email
207848
208037
  $ secondlayer login --credits
207849
- $ echo "$SECONDLAYER_API_KEY" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin({ credits: opts.credits }) : runLoginFlow({ force: opts.force, credits: opts.credits }));
208038
+ $ echo "$SECONDLAYER_API_KEY" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin({ credits: opts.credits }) : opts.email ? runLoginFlow({ force: opts.force, credits: opts.credits }) : runDeviceLogin({ force: opts.force, credits: opts.credits }));
207850
208039
  }
207851
208040
  // src/commands/logout.ts
207852
208041
  init_http();
@@ -208475,5 +208664,5 @@ registerCreditsCommand(program);
208475
208664
  registerArchiveCommand(program);
208476
208665
  program.parse();
208477
208666
 
208478
- //# debugId=078D4A39D2BB214C64756E2164756E21
208667
+ //# debugId=9552E9D4123BD8E564756E2164756E21
208479
208668
  //# sourceMappingURL=cli.js.map