@series-a/cli 1.3.0 → 1.3.2

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/lib/api.mjs DELETED
@@ -1,84 +0,0 @@
1
- import { loadConfig } from "./config.mjs";
2
-
3
- const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
4
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
5
-
6
- const VERBOSE = process.argv.includes("--verbose");
7
-
8
- /**
9
- * Call an internal Setu edge function.
10
- *
11
- * Hardened for unattended/bulk use: every request has a timeout, transient
12
- * failures (network drops, 429s, 5xx) are retried with exponential backoff,
13
- * and a `Retry-After` header is honoured when the server sends one.
14
- */
15
- export async function call(fn, body = {}, overrides = {}) {
16
- const cfg = { ...loadConfig(), ...overrides };
17
- if (!cfg.key) {
18
- throw new Error("Not signed in. Run `setu login` first (or set SETU_CLI_API_KEY).");
19
- }
20
-
21
- const timeoutMs = Number(overrides.timeoutMs || process.env.SETU_TIMEOUT_MS || 240_000);
22
- const maxAttempts = Number(overrides.retries ?? process.env.SETU_RETRIES ?? 3);
23
- let lastError;
24
-
25
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
26
- const controller = new AbortController();
27
- const timer = setTimeout(() => controller.abort(), timeoutMs);
28
- try {
29
- const res = await fetch(`${cfg.api}/${fn}`, {
30
- method: "POST",
31
- headers: {
32
- "Content-Type": "application/json",
33
- "x-setu-cli-key": cfg.key,
34
- "x-setu-operator": cfg.operator || "cli",
35
- "x-setu-request-id": crypto.randomUUID(),
36
- },
37
- body: JSON.stringify(body),
38
- signal: controller.signal,
39
- });
40
- clearTimeout(timer);
41
-
42
- const text = await res.text();
43
- let data;
44
- try { data = text ? JSON.parse(text) : {}; } catch { data = { error: text }; }
45
-
46
- if (res.ok) return data;
47
-
48
- const err = new Error(data.message || data.error || `${fn} failed (${res.status})`);
49
- err.status = res.status;
50
- err.data = data;
51
-
52
- if (RETRYABLE_STATUS.has(res.status) && attempt < maxAttempts) {
53
- const retryAfter = Number(res.headers.get("retry-after")) * 1000;
54
- const wait = Number.isFinite(retryAfter) && retryAfter > 0
55
- ? retryAfter
56
- : Math.min(30_000, 1000 * 2 ** (attempt - 1)) + Math.random() * 500;
57
- if (VERBOSE) console.error(` retry ${attempt}/${maxAttempts - 1} on ${fn} (${res.status}) in ${Math.round(wait)}ms`);
58
- await sleep(wait);
59
- lastError = err;
60
- continue;
61
- }
62
- throw err;
63
- } catch (e) {
64
- clearTimeout(timer);
65
- if (e.status && !RETRYABLE_STATUS.has(e.status)) throw e;
66
-
67
- const aborted = e.name === "AbortError";
68
- const message = aborted
69
- ? `${fn} timed out after ${Math.round(timeoutMs / 1000)}s`
70
- : e.message;
71
-
72
- if (attempt < maxAttempts) {
73
- const wait = Math.min(30_000, 1000 * 2 ** (attempt - 1)) + Math.random() * 500;
74
- if (VERBOSE) console.error(` retry ${attempt}/${maxAttempts - 1} on ${fn} (${message}) in ${Math.round(wait)}ms`);
75
- await sleep(wait);
76
- lastError = Object.assign(new Error(message), { status: e.status, data: e.data });
77
- continue;
78
- }
79
- throw Object.assign(new Error(message), { status: e.status, data: e.data });
80
- }
81
- }
82
-
83
- throw lastError ?? new Error(`${fn} failed`);
84
- }
package/lib/config.mjs DELETED
@@ -1,42 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join, dirname } from "node:path";
3
- import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync, rmSync } from "node:fs";
4
-
5
- const DEFAULT_API =
6
- process.env.SETU_API_URL || "https://vaanfxluxietcicoikcg.supabase.co/functions/v1";
7
-
8
- function configPath() {
9
- const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
10
- return join(base, "setu", "config.json");
11
- }
12
-
13
- export function loadConfig() {
14
- const path = configPath();
15
- if (!existsSync(path)) return { api: DEFAULT_API, key: process.env.SETU_CLI_API_KEY || "", operator: "" };
16
- try {
17
- const raw = JSON.parse(readFileSync(path, "utf8"));
18
- return {
19
- api: process.env.SETU_API_URL || raw.api || DEFAULT_API,
20
- key: process.env.SETU_CLI_API_KEY || raw.key || "",
21
- operator: raw.operator || "",
22
- };
23
- } catch {
24
- return { api: DEFAULT_API, key: process.env.SETU_CLI_API_KEY || "", operator: "" };
25
- }
26
- }
27
-
28
- export function saveConfig(cfg) {
29
- const path = configPath();
30
- mkdirSync(dirname(path), { recursive: true });
31
- writeFileSync(path, JSON.stringify(cfg, null, 2));
32
- try { chmodSync(path, 0o600); } catch { /* windows */ }
33
- return path;
34
- }
35
-
36
- export function clearConfig() {
37
- const path = configPath();
38
- try { rmSync(path, { force: true }); } catch { /* ignore */ }
39
- return path;
40
- }
41
-
42
- export { DEFAULT_API, configPath };
package/lib/ui.mjs DELETED
@@ -1,68 +0,0 @@
1
- import { createInterface } from "node:readline/promises";
2
- import { stdin, stdout } from "node:process";
3
-
4
- const useColor = stdout.isTTY && !process.env.NO_COLOR;
5
- const wrap = (code) => (s) => (useColor ? `\u001b[${code}m${s}\u001b[0m` : String(s));
6
-
7
- export const c = {
8
- bold: wrap("1"),
9
- dim: wrap("2"),
10
- blue: wrap("36"),
11
- green: wrap("32"),
12
- yellow: wrap("33"),
13
- red: wrap("31"),
14
- };
15
-
16
- export function log(...args) { console.log(...args); }
17
- export function ok(msg) { log(`${c.green("✔")} ${msg}`); }
18
- export function warn(msg) { log(`${c.yellow("!")} ${msg}`); }
19
- export function fail(msg) { console.error(`${c.red("✖")} ${msg}`); }
20
-
21
- export function heading(msg) { log(`\n${c.bold(msg)}`); }
22
-
23
- export function table(rows) {
24
- for (const [k, v] of rows) {
25
- log(` ${c.dim(String(k).padEnd(18))} ${v ?? c.dim("—")}`);
26
- }
27
- }
28
-
29
- export async function ask(question, { silent = false } = {}) {
30
- const rl = createInterface({ input: stdin, output: stdout, terminal: true });
31
- if (!silent) {
32
- const answer = await rl.question(question);
33
- rl.close();
34
- return answer.trim();
35
- }
36
- // Hidden input for secrets.
37
- const onData = (char) => {
38
- if (["\n", "\r", "\u0004"].includes(char.toString())) return;
39
- stdout.write("\u001b[2K\u001b[200D" + question + "*".repeat(rl.line.length));
40
- };
41
- stdin.on("data", onData);
42
- const answer = await rl.question(question);
43
- stdin.off("data", onData);
44
- rl.close();
45
- stdout.write("\n");
46
- return answer.trim();
47
- }
48
-
49
- export async function confirm(question) {
50
- const a = await ask(`${question} ${c.dim("(y/N)")} `);
51
- return /^y(es)?$/i.test(a);
52
- }
53
-
54
- export function spinner(label) {
55
- if (!stdout.isTTY) { log(`${label}…`); return { stop: () => {} }; }
56
- const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
57
- let i = 0;
58
- const timer = setInterval(() => {
59
- stdout.write(`\r${c.blue(frames[i++ % frames.length])} ${label} `);
60
- }, 80);
61
- return {
62
- stop: (final) => {
63
- clearInterval(timer);
64
- stdout.write("\r\u001b[2K");
65
- if (final) log(final);
66
- },
67
- };
68
- }