snoutdata 0.1.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/dist/snoutdata.mjs +49 -2757
  2. package/package.json +1 -1
@@ -1,2477 +1,58 @@
1
1
  #!/usr/bin/env node
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
- };
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
-
12
- // src/config.ts
13
- import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
14
- import { homedir } from "node:os";
15
- import { dirname, join, parse, resolve } from "node:path";
16
- function authPath(home = homedir()) {
17
- return join(home, ".snoutdata", "auth.json");
18
- }
19
- function readAuth(home = homedir()) {
20
- const fromEnvironment = process.env.SNOUTDATA_ACCESS_TOKEN;
21
- if (fromEnvironment) {
22
- return { accessToken: fromEnvironment };
23
- }
24
- const path = authPath(home);
25
- if (!existsSync(path)) {
26
- return null;
27
- }
28
- try {
29
- const parsed = JSON.parse(readFileSync(path, "utf8"));
30
- return typeof parsed.accessToken === "string" ? parsed : null;
31
- } catch {
32
- return null;
33
- }
34
- }
35
- function writeAuth(auth, home = homedir()) {
36
- const path = authPath(home);
37
- mkdirSync(dirname(path), { recursive: true });
38
- writeFileSync(path, `${JSON.stringify(auth, null, 2)}
39
- `, { mode: 384 });
40
- chmodSync(path, 384);
41
- }
42
- function clearAuth(home = homedir()) {
43
- rmSync(authPath(home), { force: true });
44
- }
45
- function isExpired(auth, now = Date.now()) {
46
- if (!auth.expiresAt) {
47
- return false;
48
- }
49
- return auth.expiresAt * 1e3 - 6e4 <= now;
50
- }
51
- function linkPath(directory) {
52
- return join(directory, ".snoutdata", "project.json");
53
- }
54
- function findLink(from) {
55
- let directory = resolve(from);
56
- const root = parse(directory).root;
57
- for (; ; ) {
58
- const path = linkPath(directory);
59
- if (existsSync(path)) {
60
- try {
61
- const parsed = JSON.parse(readFileSync(path, "utf8"));
62
- if (typeof parsed.ref === "string" && parsed.ref) {
63
- return { ref: parsed.ref, directory };
64
- }
65
- } catch {
66
- }
67
- }
68
- if (directory === root) {
69
- return null;
70
- }
71
- directory = dirname(directory);
72
- }
73
- }
74
- function writeLink(directory, project) {
75
- const path = linkPath(directory);
76
- mkdirSync(dirname(path), { recursive: true });
77
- writeFileSync(path, `${JSON.stringify(project, null, 2)}
78
- `);
79
- return path;
80
- }
81
- function resolveRef(options) {
82
- const environment = options.environment ?? process.env;
83
- if (options.flag) {
84
- return options.flag;
85
- }
86
- if (environment.SNOUTDATA_PROJECT) {
87
- return environment.SNOUTDATA_PROJECT;
88
- }
89
- return findLink(options.cwd ?? process.cwd())?.ref ?? null;
90
- }
91
- var init_config = __esm({
92
- "src/config.ts"() {
93
- "use strict";
94
- }
95
- });
96
-
97
- // src/interactive.ts
98
- function interactiveState(input) {
99
- const optOut = input.env.SNOUTDATA_NO_INTERACTIVE;
100
- if (optOut !== void 0 && optOut !== "" && optOut !== "0" && optOut !== "false") {
101
- return { canAsk: false, reason: "opted-out" };
102
- }
103
- if (input.json) {
104
- return { canAsk: false, reason: "json" };
105
- }
106
- for (const name of CI_VARIABLES) {
107
- const value = input.env[name];
108
- if (value !== void 0 && value !== "" && value !== "0" && value !== "false") {
109
- return { canAsk: false, reason: "ci" };
110
- }
111
- }
112
- if (!input.isTty) {
113
- return { canAsk: false, reason: "not-a-tty" };
114
- }
115
- return { canAsk: true };
116
- }
117
- function explainNoHuman(reason) {
118
- switch (reason) {
119
- case "json":
120
- return "running with --json, so nothing was asked";
121
- case "opted-out":
122
- return "SNOUTDATA_NO_INTERACTIVE is set, so nothing was asked";
123
- case "ci":
124
- return "this looks like CI, so nothing was asked";
125
- case "not-a-tty":
126
- return "there is no terminal to ask at";
127
- }
128
- }
129
- function setInteractive(state) {
130
- override = state;
131
- }
132
- function canAsk() {
133
- return (override ?? interactiveState({ isTty: Boolean(process.stdin.isTTY), json: false, env: process.env })).canAsk;
134
- }
135
- function noHumanReason() {
136
- return (override ?? interactiveState({ isTty: Boolean(process.stdin.isTTY), json: false, env: process.env })).reason;
137
- }
138
- var CI_VARIABLES, override;
139
- var init_interactive = __esm({
140
- "src/interactive.ts"() {
141
- "use strict";
142
- CI_VARIABLES = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI", "TF_BUILD"];
143
- override = null;
144
- }
145
- });
146
-
147
- // src/desktop.ts
148
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
149
- import { homedir as homedir2, hostname } from "node:os";
150
- import { join as join2 } from "node:path";
151
- function userDataDir(app, platform, env) {
152
- if (platform === "win32") {
153
- const appData = env.APPDATA ?? join2(homedir2(), "AppData", "Roaming");
154
- return join2(appData, app);
155
- }
156
- if (platform === "darwin") {
157
- return join2(homedir2(), "Library", "Application Support", app);
158
- }
159
- return join2(env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"), app);
160
- }
161
- function configPaths(platform = process.platform, env = process.env) {
162
- if (env.SNOUTDATA_DESKTOP_CONFIG) {
163
- return [env.SNOUTDATA_DESKTOP_CONFIG];
164
- }
165
- return ["snoutdata", "snoutdata-dev"].map((app) => join2(userDataDir(app, platform, env), "mcp.json"));
166
- }
167
- function configPath(platform = process.platform, env = process.env) {
168
- return configPaths(platform, env)[0];
169
- }
170
- function readDesktopConfig(path = configPath()) {
171
- if (!existsSync2(path)) {
172
- return null;
173
- }
174
- try {
175
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
176
- if (typeof parsed.port !== "number" || typeof parsed.token !== "string" || !parsed.token) {
177
- return null;
178
- }
179
- return parsed;
180
- } catch {
181
- return null;
182
- }
183
- }
184
- function offerFromConfig(config) {
185
- if (!config) {
186
- return "no-config";
187
- }
188
- if (config.enabled === false) {
189
- return "server-disabled";
190
- }
191
- if (config.signInHandoff === false) {
192
- return "handoff-disabled";
193
- }
194
- return null;
195
- }
196
- async function look(config) {
197
- if (config !== void 0) {
198
- return lookAt(config);
199
- }
200
- let lastSkip = "no-config";
201
- for (const path of configPaths()) {
202
- const found = readDesktopConfig(path);
203
- if (!found) {
204
- continue;
205
- }
206
- const result = await lookAt(found);
207
- if (result.available) {
208
- return result;
209
- }
210
- if (result.skip !== "not-running") {
211
- lastSkip = result.skip;
212
- } else if (lastSkip === "no-config") {
213
- lastSkip = result.skip;
214
- }
215
- }
216
- return { available: false, skip: lastSkip };
217
- }
218
- async function lookAt(config) {
219
- const skip = offerFromConfig(config);
220
- if (skip) {
221
- return { available: false, skip };
222
- }
223
- const settled = config;
224
- let who;
225
- try {
226
- const response = await fetch(`http://127.0.0.1:${settled.port}/cli/whoami`, {
227
- headers: { Authorization: `Bearer ${settled.token}` },
228
- signal: AbortSignal.timeout(DIAL_MS)
229
- });
230
- if (!response.ok) {
231
- return { available: false, skip: "not-running" };
232
- }
233
- who = await response.json();
234
- } catch {
235
- return { available: false, skip: "not-running" };
236
- }
237
- if (!who.signedIn) {
238
- return { available: false, skip: "signed-out" };
239
- }
240
- if (!who.canMintToken) {
241
- return { available: false, skip: "cannot-mint" };
242
- }
243
- return { available: true, who, config: settled };
244
- }
245
- async function mint(config, command) {
246
- const response = await fetch(`http://127.0.0.1:${config.port}/cli/token`, {
247
- method: "POST",
248
- headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" },
249
- body: JSON.stringify({ client: "snoutdata-cli", command }),
250
- // Longer than the dial: there is a person reading a card on the other end of this.
251
- signal: AbortSignal.timeout(6e4)
252
- });
253
- const body = await response.json().catch(() => ({}));
254
- if (response.ok && body.ok && body.token) {
255
- return { token: body.token, name: body.name ?? "SnoutData CLI" };
256
- }
257
- return { declined: true, reason: body.error ?? `the app refused (${response.status})` };
258
- }
259
- function thisMachine() {
260
- return hostname();
261
- }
262
- async function rpc(config, method, params, timeoutMs) {
263
- const response = await fetch(`http://127.0.0.1:${config.port}/mcp`, {
264
- method: "POST",
265
- headers: {
266
- Authorization: `Bearer ${config.token}`,
267
- "Content-Type": "application/json",
268
- Accept: "application/json"
269
- },
270
- body: JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method, params }),
271
- signal: AbortSignal.timeout(timeoutMs)
272
- });
273
- if (!response.ok) {
274
- throw new Error(`the app answered ${response.status}`);
275
- }
276
- const body = await response.json();
277
- if (body.error) {
278
- throw new Error(body.error.message ?? "the app refused");
279
- }
280
- return body.result;
281
- }
282
- async function borrowTools(config) {
283
- await rpc(config, "initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "snoutdata-cli", version: "0" } }, 3e3);
284
- const listed = await rpc(config, "tools/list", {}, 3e3);
285
- return listed.tools ?? [];
286
- }
287
- async function callBorrowed(config, name, args) {
288
- return rpc(config, "tools/call", { name, arguments: args }, 12e4);
289
- }
290
- var DIAL_MS;
291
- var init_desktop = __esm({
292
- "src/desktop.ts"() {
293
- "use strict";
294
- DIAL_MS = 1500;
295
- }
296
- });
297
-
298
- // src/failure.ts
299
- function fail(code, message, details) {
300
- throw new CliFailure(code, message, details);
301
- }
302
- function codeForStatus(status) {
303
- if (status === 403) {
304
- return "forbidden";
305
- }
306
- if (status === 404) {
307
- return "not-found";
308
- }
309
- if (status === 409) {
310
- return "conflict";
311
- }
312
- if (status === 402 || status === 429) {
313
- return "quota";
314
- }
315
- if (status === 503) {
316
- return "not-ready";
317
- }
318
- if (status === 504) {
319
- return "timeout";
320
- }
321
- if (status >= 500) {
322
- return "server";
323
- }
324
- return "failed";
325
- }
326
- function codeForThrown(error) {
327
- const causes = [error];
328
- let cursor = error;
329
- for (let depth = 0; depth < 4 && cursor instanceof Error && cursor.cause; depth += 1) {
330
- cursor = cursor.cause;
331
- causes.push(cursor);
332
- }
333
- for (const one of causes) {
334
- const code = typeof one === "object" && one && "code" in one ? String(one.code) : "";
335
- if (NETWORK_ERRNO.has(code)) {
336
- return "network";
337
- }
338
- if (code === "ABORT_ERR" || code === "ETIMEDOUT") {
339
- return "timeout";
340
- }
341
- }
342
- if (error instanceof Error && /fetch failed|network|socket hang up/i.test(error.message)) {
343
- return "network";
344
- }
345
- return "failed";
346
- }
347
- var EXIT, CliFailure, NETWORK_ERRNO;
348
- var init_failure = __esm({
349
- "src/failure.ts"() {
350
- "use strict";
351
- EXIT = {
352
- ok: 0,
353
- failed: 1,
354
- usage: 2,
355
- "not-signed-in": 3,
356
- "not-ready": 4,
357
- forbidden: 5,
358
- "not-found": 6,
359
- conflict: 7,
360
- quota: 8,
361
- network: 9,
362
- timeout: 10,
363
- // 127 is the shell's own convention for "command not found", and somebody reading a CI
364
- // log knows it on sight. Worth more than a number of ours in sequence.
365
- "tool-missing": 127,
366
- server: 1
367
- };
368
- CliFailure = class extends Error {
369
- code;
370
- /** Anything structured worth handing back: a list of refusals, a ref, a limit. */
371
- details;
372
- constructor(code, message, details) {
373
- super(message);
374
- this.name = "CliFailure";
375
- this.code = code;
376
- this.details = details;
377
- }
378
- get exitCode() {
379
- return EXIT[this.code];
380
- }
381
- };
382
- NETWORK_ERRNO = /* @__PURE__ */ new Set([
383
- "ECONNREFUSED",
384
- "ECONNRESET",
385
- "ENOTFOUND",
386
- "EAI_AGAIN",
387
- "EHOSTUNREACH",
388
- "ENETUNREACH",
389
- "EPIPE",
390
- "CERT_HAS_EXPIRED",
391
- "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
392
- "DEPTH_ZERO_SELF_SIGNED_CERT"
393
- ]);
394
- }
395
- });
396
-
397
- // src/device.ts
398
- async function post(fn, body) {
399
- const response = await fetch(`${SUPABASE_URL}/functions/v1/${fn}`, {
400
- method: "POST",
401
- headers: { apikey: ANON_KEY, "Content-Type": "application/json" },
402
- body: JSON.stringify(body)
403
- });
404
- const parsed = await response.json().catch(() => ({}));
405
- return { status: response.status, body: parsed };
406
- }
407
- async function start(client = {}) {
408
- const { status, body } = await post("cloud-device-start", { client });
409
- if (status === 429) {
410
- fail("quota", body.error ?? "too many pairing attempts from here: wait a few minutes");
411
- }
412
- if (status === 404) {
413
- fail(
414
- "not-found",
415
- "this SnoutData does not have the pairing flow (cloud-device-start is not deployed). Use `snoutdata login` to sign in with a browser, or set SNOUTDATA_ACCESS_TOKEN."
416
- );
417
- }
418
- if (status !== 200 || !body.code || !body.secret) {
419
- fail("server", body.error ?? `could not start a pairing (${status})`);
420
- }
421
- return body;
422
- }
423
- async function waitForApproval(secret, options) {
424
- let intervalMs = options.intervalMs;
425
- const deadline = Date.parse(options.expiresAt);
426
- for (; ; ) {
427
- if (Date.now() > deadline) {
428
- fail("timeout", "that pairing code expired before it was approved. Run the command again for a new one.");
429
- }
430
- await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
431
- const { status, body } = await post("cloud-device-poll", { secret });
432
- if (status === 404) {
433
- fail("not-found", "that pairing is not known to the server. Run the command again for a new code.");
434
- }
435
- if (status === 400) {
436
- fail("usage", body.error ?? "that is not a pairing secret");
437
- }
438
- if (status !== 200) {
439
- fail("server", body.error ?? `the pairing could not be checked (${status})`);
440
- }
441
- switch (body.status) {
442
- case "approved":
443
- return { token: body.token, name: body.name };
444
- case "denied":
445
- fail("forbidden", body.error ?? "that pairing was refused in the browser");
446
- // falls through, unreachable: `fail` never returns
447
- case "used":
448
- fail("conflict", body.error ?? "that pairing has already been completed");
449
- case "expired":
450
- fail("timeout", body.error ?? "that pairing code expired before it was approved");
451
- case "slow-down":
452
- intervalMs = Math.max(intervalMs * 2, (body.interval ?? 2) * 1e3);
453
- break;
454
- default:
455
- if (body.interval) {
456
- intervalMs = Math.max(intervalMs, body.interval * 1e3);
457
- }
458
- break;
459
- }
460
- options.onWait?.(Math.max(0, Math.round((deadline - Date.now()) / 1e3)));
461
- }
462
- }
463
- var init_device = __esm({
464
- "src/device.ts"() {
465
- "use strict";
466
- init_api();
467
- init_failure();
468
- }
469
- });
470
-
471
- // src/output.ts
472
- function setJsonMode(on) {
473
- jsonMode = on;
474
- }
475
- function setQuiet(on) {
476
- quiet = on;
477
- }
478
- function dim(text) {
479
- return colour ? `\x1B[2m${text}\x1B[0m` : text;
480
- }
481
- function bold(text) {
482
- return colour ? `\x1B[1m${text}\x1B[0m` : text;
483
- }
484
- function emit(value, human) {
485
- if (jsonMode) {
486
- process.stdout.write(`${JSON.stringify(value, null, 2)}
487
- `);
488
- return;
489
- }
490
- human();
491
- }
492
- function say(message) {
493
- if (!jsonMode && !quiet) {
494
- process.stderr.write(`${message}
495
- `);
496
- }
497
- }
498
- function emitFailure(code, message, details) {
499
- process.stderr.write(`${message}
500
- `);
501
- if (jsonMode) {
502
- process.stdout.write(`${JSON.stringify({ ok: false, code, error: message, ...details }, null, 2)}
503
- `);
504
- }
505
- }
506
- function warn(message) {
507
- process.stderr.write(`${message}
508
- `);
509
- }
510
- function table(rows) {
511
- if (rows.length === 0) {
512
- return "";
513
- }
514
- const widths = rows[0].map(
515
- (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length))
516
- );
517
- return rows.map((row) => row.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" ").trimEnd()).join("\n");
518
- }
519
- function relative(iso) {
520
- if (!iso) {
521
- return "never";
522
- }
523
- const delta = Math.round((Date.now() - Date.parse(iso)) / 1e3);
524
- const ahead = delta < 0;
525
- const seconds = Math.abs(delta);
526
- if (seconds < 60) {
527
- return ahead ? "in a moment" : "just now";
528
- }
529
- const units = [
530
- [60, "minute"],
531
- [60, "hour"],
532
- [24, "day"],
533
- [30, "month"]
534
- ];
535
- let value = seconds;
536
- let name = "second";
537
- for (const [size, unit] of units) {
538
- if (value < size) {
539
- break;
540
- }
541
- value = Math.round(value / size);
542
- name = unit;
543
- }
544
- const plural = `${value} ${name}${value === 1 ? "" : "s"}`;
545
- return ahead ? `in ${plural}` : `${plural} ago`;
546
- }
547
- var jsonMode, quiet, colour;
548
- var init_output = __esm({
549
- "src/output.ts"() {
550
- "use strict";
551
- jsonMode = false;
552
- quiet = false;
553
- colour = process.stdout.isTTY && !process.env.NO_COLOR;
554
- }
555
- });
556
-
557
- // src/commands/login.ts
558
- import { createHash, randomBytes } from "node:crypto";
559
- import { createServer } from "node:http";
560
- import { spawn } from "node:child_process";
561
- function base64url(input) {
562
- return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
563
- }
564
- function page(kind, detail) {
565
- const accent = kind === "ok" ? "#e0b256" : "#e0765a";
566
- const heading = kind === "ok" ? "Signed in" : "Sign-in failed";
567
- const body = kind === "ok" ? "You can close this tab and go back to your terminal." : escapeHtml(detail ?? "Something went wrong.");
568
- const hint = kind === "ok" ? "" : '<p style="color:#8f887e;margin:14px 0 0;font-size:14px">Run <code style="background:#1b1917;border:1px solid #262320;border-radius:6px;padding:2px 6px;font-size:13px">snoutdata login</code> to try again.</p>';
569
- return `<!doctype html><html><head><meta charset="utf-8"><title>SnoutData</title>
2
+ var tn=Object.defineProperty;var _=(e,t)=>()=>(e&&(t=e(e=0)),t);var nn=(e,t)=>{for(var n in t)tn(e,n,{get:t[n],enumerable:!0})};import{chmodSync as an,existsSync as Ke,mkdirSync as ze,readFileSync as Ye,rmSync as cn,writeFileSync as Ve}from"node:fs";import{homedir as le}from"node:os";import{dirname as $e,join as Xe,parse as ln,resolve as dn}from"node:path";function Pe(e=le()){return Xe(e,".snoutdata","auth.json")}function Ze(e=le()){let t=process.env.SNOUTDATA_ACCESS_TOKEN;if(t)return{accessToken:t};let n=Pe(e);if(!Ke(n))return null;try{let r=JSON.parse(Ye(n,"utf8"));return typeof r.accessToken=="string"?r:null}catch{return null}}function j(e,t=le()){let n=Pe(t);ze($e(n),{recursive:!0}),Ve(n,`${JSON.stringify(e,null,2)}
3
+ `,{mode:384}),an(n,384)}function Qe(e=le()){cn(Pe(e),{force:!0})}function et(e,t=Date.now()){return e.expiresAt?e.expiresAt*1e3-6e4<=t:!1}function tt(e){return Xe(e,".snoutdata","project.json")}function un(e){let t=dn(e),n=ln(t).root;for(;;){let r=tt(t);if(Ke(r))try{let o=JSON.parse(Ye(r,"utf8"));if(typeof o.ref=="string"&&o.ref)return{ref:o.ref,directory:t}}catch{}if(t===n)return null;t=$e(t)}}function de(e,t){let n=tt(e);return ze($e(n),{recursive:!0}),Ve(n,`${JSON.stringify(t,null,2)}
4
+ `),n}function ue(e){let t=e.environment??process.env;return e.flag?e.flag:t.SNOUTDATA_PROJECT?t.SNOUTDATA_PROJECT:un(e.cwd??process.cwd())?.ref??null}var z=_(()=>{"use strict"});function pe(e){let t=e.env.SNOUTDATA_NO_INTERACTIVE;if(t!==void 0&&t!==""&&t!=="0"&&t!=="false")return{canAsk:!1,reason:"opted-out"};if(e.json)return{canAsk:!1,reason:"json"};for(let n of pn){let r=e.env[n];if(r!==void 0&&r!==""&&r!=="0"&&r!=="false")return{canAsk:!1,reason:"ci"}}return e.isTty?{canAsk:!0}:{canAsk:!1,reason:"not-a-tty"}}function me(e){switch(e){case"json":return"running with --json, so nothing was asked";case"opted-out":return"SNOUTDATA_NO_INTERACTIVE is set, so nothing was asked";case"ci":return"this looks like CI, so nothing was asked";case"not-a-tty":return"there is no terminal to ask at"}}function nt(e){Ee=e}function Y(){return(Ee??pe({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).canAsk}function fe(){return(Ee??pe({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).reason}var pn,Ee,ee=_(()=>{"use strict";pn=["CI","GITHUB_ACTIONS","GITLAB_CI","BUILDKITE","CIRCLECI","TF_BUILD"];Ee=null});import{existsSync as mn,readFileSync as fn}from"node:fs";import{homedir as Oe,hostname as gn}from"node:os";import{join as V}from"node:path";function hn(e,t,n){if(t==="win32"){let r=n.APPDATA??V(Oe(),"AppData","Roaming");return V(r,e)}return t==="darwin"?V(Oe(),"Library","Application Support",e):V(n.XDG_CONFIG_HOME??V(Oe(),".config"),e)}function ot(e=process.platform,t=process.env){return t.SNOUTDATA_DESKTOP_CONFIG?[t.SNOUTDATA_DESKTOP_CONFIG]:["snoutdata","snoutdata-dev"].map(n=>V(hn(n,e,t),"mcp.json"))}function wn(e=process.platform,t=process.env){return ot(e,t)[0]}function yn(e=wn()){if(!mn(e))return null;try{let t=JSON.parse(fn(e,"utf8"));return typeof t.port!="number"||typeof t.token!="string"||!t.token?null:t}catch{return null}}function kn(e){return e?e.enabled===!1?"server-disabled":e.signInHandoff===!1?"handoff-disabled":null:"no-config"}async function ge(e){if(e!==void 0)return rt(e);let t="no-config";for(let n of ot()){let r=yn(n);if(!r)continue;let o=await rt(r);if(o.available)return o;(o.skip!=="not-running"||t==="no-config")&&(t=o.skip)}return{available:!1,skip:t}}async function rt(e){let t=kn(e);if(t)return{available:!1,skip:t};let n=e,r;try{let o=await fetch(`http://127.0.0.1:${n.port}/cli/whoami`,{headers:{Authorization:`Bearer ${n.token}`},signal:AbortSignal.timeout(bn)});if(!o.ok)return{available:!1,skip:"not-running"};r=await o.json()}catch{return{available:!1,skip:"not-running"}}return r.signedIn?r.canMintToken?{available:!0,who:r,config:n}:{available:!1,skip:"cannot-mint"}:{available:!1,skip:"signed-out"}}async function st(e,t){let n=await fetch(`http://127.0.0.1:${e.port}/cli/token`,{method:"POST",headers:{Authorization:`Bearer ${e.token}`,"Content-Type":"application/json"},body:JSON.stringify({client:"snoutdata-cli",command:t}),signal:AbortSignal.timeout(6e4)}),r=await n.json().catch(()=>({}));return n.ok&&r.ok&&r.token?{token:r.token,name:r.name??"SnoutData CLI"}:{declined:!0,reason:r.error??`the app refused (${n.status})`}}function he(){return gn()}async function _e(e,t,n,r){let o=await fetch(`http://127.0.0.1:${e.port}/mcp`,{method:"POST",headers:{Authorization:`Bearer ${e.token}`,"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:Date.now(),method:t,params:n}),signal:AbortSignal.timeout(r)});if(!o.ok)throw new Error(`the app answered ${o.status}`);let s=await o.json();if(s.error)throw new Error(s.error.message??"the app refused");return s.result}async function at(e){return await _e(e,"initialize",{protocolVersion:"2025-06-18",capabilities:{},clientInfo:{name:"snoutdata-cli",version:"0"}},3e3),(await _e(e,"tools/list",{},3e3)).tools??[]}async function it(e,t,n){return _e(e,"tools/call",{name:t,arguments:n},12e4)}var bn,we=_(()=>{"use strict";bn=1500});function g(e,t,n){throw new B(e,t,n)}function lt(e){return e===403?"forbidden":e===404?"not-found":e===409?"conflict":e===402||e===429?"quota":e===503?"not-ready":e===504?"timeout":e>=500?"server":"failed"}function dt(e){let t=[e],n=e;for(let r=0;r<4&&n instanceof Error&&n.cause;r+=1)n=n.cause,t.push(n);for(let r of t){let o=typeof r=="object"&&r&&"code"in r?String(r.code):"";if(xn.has(o))return"network";if(o==="ABORT_ERR"||o==="ETIMEDOUT")return"timeout"}return e instanceof Error&&/fetch failed|network|socket hang up/i.test(e.message)?"network":"failed"}var L,B,xn,te=_(()=>{"use strict";L={ok:0,failed:1,usage:2,"not-signed-in":3,"not-ready":4,forbidden:5,"not-found":6,conflict:7,quota:8,network:9,timeout:10,"tool-missing":127,server:1},B=class extends Error{code;details;constructor(t,n,r){super(n),this.name="CliFailure",this.code=t,this.details=r}get exitCode(){return L[this.code]}};xn=new Set(["ECONNREFUSED","ECONNRESET","ENOTFOUND","EAI_AGAIN","EHOSTUNREACH","ENETUNREACH","EPIPE","CERT_HAS_EXPIRED","UNABLE_TO_VERIFY_LEAF_SIGNATURE","DEPTH_ZERO_SELF_SIGNED_CERT"])});async function ut(e,t){let n=await fetch(`${R}/functions/v1/${e}`,{method:"POST",headers:{apikey:F,"Content-Type":"application/json"},body:JSON.stringify(t)}),r=await n.json().catch(()=>({}));return{status:n.status,body:r}}async function ye(e={}){let{status:t,body:n}=await ut("cloud-device-start",{client:e});return t===429&&g("quota",n.error??"too many pairing attempts from here: wait a few minutes"),t===404&&g("not-found","this SnoutData does not have the pairing flow (cloud-device-start is not deployed). Use `snoutdata login` to sign in with a browser, or set SNOUTDATA_ACCESS_TOKEN."),(t!==200||!n.code||!n.secret)&&g("server",n.error??`could not start a pairing (${t})`),n}async function ke(e,t){let n=t.intervalMs,r=Date.parse(t.expiresAt);for(;;){Date.now()>r&&g("timeout","that pairing code expired before it was approved. Run the command again for a new one."),await new Promise(d=>setTimeout(d,n));let{status:o,body:s}=await ut("cloud-device-poll",{secret:e});switch(o===404&&g("not-found","that pairing is not known to the server. Run the command again for a new code."),o===400&&g("usage",s.error??"that is not a pairing secret"),o!==200&&g("server",s.error??`the pairing could not be checked (${o})`),s.status){case"approved":return{token:s.token,name:s.name};case"denied":g("forbidden",s.error??"that pairing was refused in the browser");case"used":g("conflict",s.error??"that pairing has already been completed");case"expired":g("timeout",s.error??"that pairing code expired before it was approved");case"slow-down":n=Math.max(n*2,(s.interval??2)*1e3);break;default:s.interval&&(n=Math.max(n,s.interval*1e3));break}t.onWait?.(Math.max(0,Math.round((r-Date.now())/1e3)))}}var je=_(()=>{"use strict";T();te()});function ne(e){be=e}function mt(e){pt=e}function y(e){return ft?`\x1B[2m${e}\x1B[0m`:e}function A(e){return ft?`\x1B[1m${e}\x1B[0m`:e}function p(e,t){if(be){process.stdout.write(`${JSON.stringify(e,null,2)}
5
+ `);return}t()}function i(e){!be&&!pt&&process.stderr.write(`${e}
6
+ `)}function G(e,t,n){process.stderr.write(`${t}
7
+ `),be&&process.stdout.write(`${JSON.stringify({ok:!1,code:e,error:t,...n},null,2)}
8
+ `)}function $(e){process.stderr.write(`${e}
9
+ `)}function W(e){if(e.length===0)return"";let t=e[0].map((n,r)=>Math.max(...e.map(o=>(o[r]??"").length)));return e.map(n=>n.map((r,o)=>r.padEnd(t[o]??0)).join(" ").trimEnd()).join(`
10
+ `)}function H(e){if(!e)return"never";let t=Math.round((Date.now()-Date.parse(e))/1e3),n=t<0,r=Math.abs(t);if(r<60)return n?"in a moment":"just now";let o=[[60,"minute"],[60,"hour"],[24,"day"],[30,"month"]],s=r,d="second";for(let[c,l]of o){if(s<c)break;s=Math.round(s/c),d=l}let a=`${s} ${d}${s===1?"":"s"}`;return n?`in ${a}`:`${a} ago`}var be,pt,ft,E=_(()=>{"use strict";be=!1,pt=!1;ft=process.stdout.isTTY&&!process.env.NO_COLOR});import{createHash as gt,randomBytes as Re}from"node:crypto";import{createServer as ht}from"node:http";import{spawn as vn}from"node:child_process";function xe(e){return e.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function Se(e,t){let n=e==="ok"?"#e0b256":"#e0765a",r=e==="ok"?"Signed in":"Sign-in failed",o=e==="ok"?"You can close this tab and go back to your terminal.":Nn(t??"Something went wrong.");return`<!doctype html><html><head><meta charset="utf-8"><title>SnoutData</title>
570
11
  <meta name="viewport" content="width=device-width,initial-scale=1"></head>
571
12
  <body style="font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;background:#050403;color:#e9e4dc;display:grid;place-items:center;min-height:100vh;margin:0">
572
13
  <div style="text-align:center;max-width:460px;padding:40px 24px">
573
14
  <div style="width:44px;height:44px;margin:0 auto 30px;border-radius:12px;background:#191714;border:1px solid #262320;display:grid;place-items:center">
574
- <div style="width:14px;height:14px;border-radius:50%;background:${accent}"></div>
15
+ <div style="width:14px;height:14px;border-radius:50%;background:${n}"></div>
575
16
  </div>
576
17
  <div style="font-size:12px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:#8f887e;margin-bottom:22px">SnoutData</div>
577
- <h1 style="margin:0 0 12px;font-size:30px;font-weight:600;letter-spacing:-.01em">${heading}</h1>
578
- <p style="color:#8f887e;margin:0;font-size:15px;line-height:1.5">${body}</p>
579
- ${hint}
18
+ <h1 style="margin:0 0 12px;font-size:30px;font-weight:600;letter-spacing:-.01em">${r}</h1>
19
+ <p style="color:#8f887e;margin:0;font-size:15px;line-height:1.5">${o}</p>
20
+ ${e==="ok"?"":'<p style="color:#8f887e;margin:14px 0 0;font-size:14px">Run <code style="background:#1b1917;border:1px solid #262320;border-radius:6px;padding:2px 6px;font-size:13px">snoutdata login</code> to try again.</p>'}
580
21
  </div>
581
- </body></html>`;
582
- }
583
- function pkcePair() {
584
- const verifier = base64url(randomBytes(32));
585
- const challenge = base64url(createHash("sha256").update(verifier).digest());
586
- return { verifier, challenge };
587
- }
588
- function openCommand(platform, url2) {
589
- if (platform === "darwin") {
590
- return { command: "open", args: [url2], verbatim: false };
591
- }
592
- if (platform === "win32") {
593
- return { command: "cmd.exe", args: ["/c", "start", '""', `"${url2}"`], verbatim: true };
594
- }
595
- return { command: "xdg-open", args: [url2], verbatim: false };
596
- }
597
- function open(url2) {
598
- const { command, args, verbatim } = openCommand(process.platform, url2);
599
- const child = spawn(command, args, {
600
- stdio: "ignore",
601
- detached: true,
602
- windowsVerbatimArguments: verbatim
603
- });
604
- child.on("error", () => {
605
- });
606
- child.unref();
607
- }
608
- function nonceForGoogle(raw) {
609
- const value = raw ?? base64url(randomBytes(32));
610
- return { raw: value, hashed: createHash("sha256").update(value).digest("hex") };
611
- }
612
- function googleAuthorizeUrl(parts) {
613
- return `${GOOGLE_AUTH_ENDPOINT}?${new URLSearchParams({
614
- client_id: GOOGLE_CLIENT_ID,
615
- redirect_uri: parts.redirectUri,
616
- response_type: "code",
617
- scope: "openid email profile",
618
- code_challenge: parts.challenge,
619
- code_challenge_method: "S256",
620
- nonce: parts.hashedNonce,
621
- state: parts.state,
622
- // ALWAYS ask which account.
623
- //
624
- // Without this, Google reuses whatever account the browser is already signed into
625
- // and never says so: you type `snoutdata login`, a page flashes, and you are signed
626
- // in as somebody else. On a shared machine, or for anybody with a work account and a
627
- // personal one, that is silently the wrong identity, and the CLI is the surface where
628
- // it is hardest to notice, because there is no avatar in the corner to check.
629
- prompt: "select_account"
630
- }).toString()}`;
631
- }
632
- function readRedirect(requestUrl, expectedState) {
633
- const url2 = new URL(requestUrl, "http://127.0.0.1");
634
- if (!url2.searchParams.has("code") && !url2.searchParams.has("error")) {
635
- return { kind: "ignore" };
636
- }
637
- const error = url2.searchParams.get("error_description") ?? url2.searchParams.get("error");
638
- if (error) {
639
- return { kind: "error", message: error };
640
- }
641
- const code = url2.searchParams.get("code");
642
- if (!code) {
643
- return { kind: "error", message: "the browser came back without a code" };
644
- }
645
- if (url2.searchParams.get("state") !== expectedState) {
646
- return { kind: "error", message: "the browser came back with a state that did not match" };
647
- }
648
- return { kind: "ok", code };
649
- }
650
- async function exchangeCodeForIdToken(code, verifier, redirectUri) {
651
- const response = await fetch(GOOGLE_TOKEN_ENDPOINT, {
652
- method: "POST",
653
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
654
- body: new URLSearchParams({
655
- client_id: GOOGLE_CLIENT_ID,
656
- client_secret: GOOGLE_CLIENT_SECRET,
657
- code,
658
- code_verifier: verifier,
659
- grant_type: "authorization_code",
660
- redirect_uri: redirectUri
661
- })
662
- });
663
- const body = await response.json().catch(() => ({}));
664
- if (!response.ok || !body.id_token) {
665
- throw new Error(
666
- `Google would not exchange the code: ${body.error_description ?? body.error ?? `HTTP ${response.status}`}`
667
- );
668
- }
669
- return body.id_token;
670
- }
671
- async function loginWithGoogle(options) {
672
- const { verifier, challenge } = pkcePair();
673
- const { raw: rawNonce, hashed: hashedNonce } = nonceForGoogle();
674
- const state = base64url(randomBytes(16));
675
- const { code, redirectUri } = await new Promise(
676
- (resolve3, reject) => {
677
- let redirect = "";
678
- const server = createServer((request, response2) => {
679
- const outcome = readRedirect(request.url ?? "/", state);
680
- if (outcome.kind === "ignore") {
681
- response2.writeHead(404).end();
682
- return;
683
- }
684
- response2.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
685
- response2.end(
686
- outcome.kind === "ok" ? page("ok") : page("failed", outcome.message)
687
- );
688
- server.close();
689
- if (outcome.kind === "ok") {
690
- resolve3({ code: outcome.code, redirectUri: redirect });
691
- } else {
692
- reject(new Error(outcome.message));
693
- }
694
- });
695
- server.listen(0, "127.0.0.1", () => {
696
- const address = server.address();
697
- const port = typeof address === "object" && address ? address.port : 0;
698
- redirect = `http://127.0.0.1:${port}`;
699
- const authorize = googleAuthorizeUrl({
700
- redirectUri: redirect,
701
- challenge,
702
- hashedNonce,
703
- state
704
- });
705
- process.stdout.write(`${authorize}
706
- `);
707
- if (options.noBrowser) {
708
- say("Open that in a browser, on any machine, and sign in.");
709
- } else {
710
- say("Opening that in your browser. If nothing opens, paste it into one.");
711
- open(authorize);
712
- }
713
- });
714
- server.on("error", reject);
715
- const timer = setTimeout(() => {
716
- server.close();
717
- reject(new Error("timed out waiting for the browser"));
718
- }, options.timeoutMs ?? 3e5);
719
- timer.unref();
720
- }
721
- );
722
- const idToken = await exchangeCodeForIdToken(code, verifier, redirectUri);
723
- const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=id_token`, {
724
- method: "POST",
725
- headers: { apikey: ANON_KEY, "Content-Type": "application/json" },
726
- body: JSON.stringify({ provider: "google", id_token: idToken, nonce: rawNonce })
727
- });
728
- if (!response.ok) {
729
- throw new Error(
730
- `could not turn the Google sign-in into a session: ${(await response.text()).slice(0, 200)}`
731
- );
732
- }
733
- return storeSession(await response.json());
734
- }
735
- function storeSession(data) {
736
- const body = data;
737
- const result = {
738
- accessToken: body.access_token,
739
- refreshToken: body.refresh_token,
740
- expiresAt: body.expires_at,
741
- email: body.user?.email
742
- };
743
- writeAuth(result);
744
- return result;
745
- }
746
- async function login(options = {}) {
747
- const provider = options.provider ?? "google";
748
- if (provider === "google") {
749
- return loginWithGoogle(options);
750
- }
751
- const { verifier, challenge } = pkcePair();
752
- const code = await new Promise((resolve3, reject) => {
753
- const server = createServer((request, response2) => {
754
- const url2 = new URL(request.url ?? "/", "http://127.0.0.1");
755
- const received = url2.searchParams.get("code");
756
- const error = url2.searchParams.get("error_description") ?? url2.searchParams.get("error");
757
- response2.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
758
- response2.end(received ? page("ok") : page("failed", error ?? "No code came back."));
759
- server.close();
760
- if (received) {
761
- resolve3(received);
762
- } else {
763
- reject(new Error(error ?? "the browser came back without a code"));
764
- }
765
- });
766
- server.listen(0, "127.0.0.1", () => {
767
- const address = server.address();
768
- const port = typeof address === "object" && address ? address.port : 0;
769
- const redirect = `http://127.0.0.1:${port}/callback`;
770
- const authorize = new URL(`${SUPABASE_URL}/auth/v1/authorize`);
771
- authorize.searchParams.set("provider", provider);
772
- authorize.searchParams.set("redirect_to", redirect);
773
- authorize.searchParams.set("code_challenge", challenge);
774
- authorize.searchParams.set("code_challenge_method", "s256");
775
- authorize.searchParams.set("prompt", "select_account");
776
- process.stdout.write(`${authorize.toString()}
777
- `);
778
- if (options.noBrowser) {
779
- say("Open that in a browser, on any machine, and sign in.");
780
- } else {
781
- say("Opening that in your browser. If nothing opens, paste it into one.");
782
- open(authorize.toString());
783
- }
784
- });
785
- const timer = setTimeout(() => {
786
- server.close();
787
- reject(new Error("timed out waiting for the browser"));
788
- }, options.timeoutMs ?? 3e5);
789
- timer.unref();
790
- });
791
- const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=pkce`, {
792
- method: "POST",
793
- headers: { apikey: ANON_KEY, "Content-Type": "application/json" },
794
- body: JSON.stringify({ auth_code: code, code_verifier: verifier })
795
- });
796
- if (!response.ok) {
797
- throw new Error(`could not exchange the code: ${(await response.text()).slice(0, 200)}`);
798
- }
799
- return storeSession(await response.json());
800
- }
801
- function escapeHtml(text) {
802
- return text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
803
- }
804
- var GOOGLE_AUTH_ENDPOINT, GOOGLE_TOKEN_ENDPOINT, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET;
805
- var init_login = __esm({
806
- "src/commands/login.ts"() {
807
- "use strict";
808
- init_api();
809
- init_config();
810
- init_output();
811
- GOOGLE_AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
812
- GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
813
- GOOGLE_CLIENT_ID = "1064562458607-9fqmghjoqjr7d56b98agkvjt9vt26pdc.apps.googleusercontent.com";
814
- GOOGLE_CLIENT_SECRET = "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";
815
- }
816
- });
817
-
818
- // src/ladder.ts
819
- import { createInterface } from "node:readline/promises";
820
- async function confirm(question) {
821
- const rl = createInterface({ input: process.stdin, output: process.stderr });
822
- try {
823
- const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
824
- return answer === "" || answer === "y" || answer === "yes";
825
- } finally {
826
- rl.close();
827
- }
828
- }
829
- async function fromDesktop() {
830
- const look2 = await look();
831
- if (!look2.available) {
832
- if (look2.skip === "handoff-disabled" || look2.skip === "server-disabled") {
833
- say(dim("The SnoutData app is running but is not set to hand out sign-ins."));
834
- }
835
- return null;
836
- }
837
- const who = look2.who.email ?? "the signed-in account";
838
- say("");
839
- say(`The SnoutData app is open here, signed in as ${bold(who)}.`);
840
- say(dim("It can create an access token for this terminal. Your sign-in itself is not handed over,"));
841
- say(dim("and you can revoke the token later with `snoutdata tokens revoke`."));
842
- if (!await confirm("Use it?")) {
843
- return null;
844
- }
845
- say("Asking the app. Approve it there.");
846
- const minted = await mint(look2.config, process.argv.slice(2).join(" ").slice(0, 60) || "snoutdata");
847
- if ("declined" in minted) {
848
- say(`The app did not create a token: ${minted.reason}`);
849
- return null;
850
- }
851
- const auth = { accessToken: minted.token };
852
- writeAuth(auth);
853
- say(`Signed in with "${minted.name}".`);
854
- return auth;
855
- }
856
- async function fromBrowser() {
857
- if (!await confirm("Sign in with a browser?")) {
858
- return null;
859
- }
860
- try {
861
- const result = await login({});
862
- say(`Signed in${result.email ? ` as ${result.email}` : ""}.`);
863
- return { accessToken: result.accessToken, refreshToken: result.refreshToken, expiresAt: result.expiresAt };
864
- } catch (error) {
865
- say(`That did not finish: ${error instanceof Error ? error.message : String(error)}`);
866
- return null;
867
- }
868
- }
869
- async function fromDeviceCode() {
870
- if (!await confirm("Sign in by typing a code into a browser on another machine?")) {
871
- return null;
872
- }
873
- const started = await start({ name: thisMachine(), platform: process.platform });
874
- say("");
875
- say(` Open ${bold(started.verifyUrl)}`);
876
- say(` Type ${bold(started.code)}`);
877
- say("");
878
- say(dim("The code is typed rather than in the link on purpose: it is what proves the person"));
879
- say(dim("at the browser is the person at this terminal. Waiting\u2026"));
880
- const approved = await waitForApproval(started.secret, {
881
- intervalMs: Math.max(1, started.interval) * 1e3,
882
- expiresAt: started.expiresAt
883
- });
884
- const auth = { accessToken: approved.token };
885
- writeAuth(auth);
886
- say(`Signed in${approved.name ? ` with "${approved.name}"` : ""}.`);
887
- return auth;
888
- }
889
- async function climb() {
890
- if (!canAsk()) {
891
- return null;
892
- }
893
- const fromApp = await fromDesktop();
894
- if (fromApp) {
895
- return fromApp;
896
- }
897
- const browser = await fromBrowser();
898
- if (browser) {
899
- return browser;
900
- }
901
- return fromDeviceCode();
902
- }
903
- var init_ladder = __esm({
904
- "src/ladder.ts"() {
905
- "use strict";
906
- init_config();
907
- init_desktop();
908
- init_device();
909
- init_interactive();
910
- init_login();
911
- init_output();
912
- }
913
- });
914
-
915
- // src/api.ts
916
- var api_exports = {};
917
- __export(api_exports, {
918
- ANON_KEY: () => ANON_KEY,
919
- ApiError: () => ApiError,
920
- FUNCTIONS: () => FUNCTIONS,
921
- NotSignedIn: () => NotSignedIn,
922
- SUPABASE_URL: () => SUPABASE_URL,
923
- call: () => call,
924
- connection: () => connection,
925
- exportStatus: () => exportStatus,
926
- listProjects: () => listProjects,
927
- listTeams: () => listTeams,
928
- requestExport: () => requestExport,
929
- waitForReady: () => waitForReady,
930
- whoami: () => whoami
931
- });
932
- function notSignedInAdvice() {
933
- const reason = noHumanReason();
934
- if (reason) {
935
- return `not signed in, and ${explainNoHuman(reason)}. Set SNOUTDATA_ACCESS_TOKEN to a token from \`snoutdata tokens create\`.`;
936
- }
937
- return "not signed in: run `snoutdata login`, or set SNOUTDATA_ACCESS_TOKEN";
938
- }
939
- async function currentToken() {
940
- let auth = readAuth();
941
- if (!auth) {
942
- auth = await climb();
943
- }
944
- if (!auth) {
945
- throw new NotSignedIn();
946
- }
947
- if (!isExpired(auth) || !auth.refreshToken) {
948
- return auth.accessToken;
949
- }
950
- const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
951
- method: "POST",
952
- headers: { apikey: ANON_KEY, "Content-Type": "application/json" },
953
- body: JSON.stringify({ refresh_token: auth.refreshToken })
954
- });
955
- if (!response.ok) {
956
- throw new NotSignedIn();
957
- }
958
- const data = await response.json();
959
- const next = {
960
- accessToken: data.access_token,
961
- refreshToken: data.refresh_token,
962
- expiresAt: data.expires_at,
963
- email: auth.email
964
- };
965
- writeAuth(next);
966
- return next.accessToken;
967
- }
968
- async function call(fn, body = {}, method = "POST") {
969
- const token = await currentToken();
970
- const query = method === "GET" && typeof body === "object" && body ? `?${new URLSearchParams(body).toString()}` : "";
971
- const response = await fetch(`${FUNCTIONS}/${fn}${query}`, {
972
- method,
973
- headers: {
974
- Authorization: `Bearer ${token}`,
975
- apikey: ANON_KEY,
976
- "Content-Type": "application/json"
977
- },
978
- body: method === "GET" ? void 0 : JSON.stringify(body)
979
- });
980
- const text = await response.text();
981
- let parsed;
982
- try {
983
- parsed = text ? JSON.parse(text) : {};
984
- } catch {
985
- throw new ApiError(response.status, text.slice(0, 300) || response.statusText);
986
- }
987
- if (!response.ok) {
988
- const message = typeof parsed === "object" && parsed && "error" in parsed ? String(parsed.error) : `${response.status} ${response.statusText}`;
989
- if (response.status === 401) {
990
- throw new NotSignedIn(message);
991
- }
992
- throw new ApiError(response.status, message);
993
- }
994
- return parsed;
995
- }
996
- async function whoami() {
997
- return call("cloud-whoami");
998
- }
999
- async function listProjects() {
1000
- return call("cloud-project-list");
1001
- }
1002
- async function connection(ref) {
1003
- return call("cloud-project-connection", { ref });
1004
- }
1005
- async function listTeams() {
1006
- return call("cloud-team-list");
1007
- }
1008
- async function requestExport(ref) {
1009
- return call("cloud-project-export", { ref });
1010
- }
1011
- async function exportStatus(ref) {
1012
- return call("cloud-project-export", { ref }, "GET");
1013
- }
1014
- async function waitForReady(ref, options = {}) {
1015
- const deadline = Date.now() + (options.timeoutMs ?? 3e5);
1016
- for (; ; ) {
1017
- const { projects } = await listProjects();
1018
- const project = projects.find((candidate) => candidate.ref === ref);
1019
- if (!project) {
1020
- throw new ApiError(404, `no project ${ref}`);
1021
- }
1022
- options.onTick?.(project.state);
1023
- if (project.state === "ready") {
1024
- return project;
1025
- }
1026
- if (Date.now() >= deadline) {
1027
- throw new ApiError(504, `${ref} is still ${project.state} after waiting`);
1028
- }
1029
- await new Promise((resolve3) => setTimeout(resolve3, 2e3));
1030
- }
1031
- }
1032
- var SUPABASE_URL, ANON_KEY, FUNCTIONS, ApiError, NotSignedIn;
1033
- var init_api = __esm({
1034
- "src/api.ts"() {
1035
- "use strict";
1036
- init_config();
1037
- init_interactive();
1038
- init_ladder();
1039
- SUPABASE_URL = process.env.SNOUTDATA_SUPABASE_URL ?? "https://krlhuuvhhgkwfsutvxtb.supabase.co";
1040
- ANON_KEY = process.env.SNOUTDATA_ANON_KEY ?? "sb_publishable_B0gbZinAncINGbDGOcP9rg_odcbriG3";
1041
- FUNCTIONS = `${SUPABASE_URL}/functions/v1`;
1042
- ApiError = class extends Error {
1043
- status;
1044
- constructor(status, message) {
1045
- super(message);
1046
- this.status = status;
1047
- }
1048
- };
1049
- NotSignedIn = class extends Error {
1050
- constructor(message) {
1051
- super(message ?? notSignedInAdvice());
1052
- }
1053
- };
1054
- }
1055
- });
1056
-
1057
- // src/args.ts
1058
- var UsageError = class extends Error {
1059
- };
1060
- var VALUED = /* @__PURE__ */ new Set([
1061
- "name",
1062
- "region",
1063
- "ref",
1064
- "file",
1065
- "provider",
1066
- "expires",
1067
- // `--timeout`: seconds to wait before giving up on anything that blocks. Every waiting
1068
- // path had a hardcoded ceiling (300s to create, 1800s to export) and no way to shorten
1069
- // it, which is fine for a person watching and useless inside a job with its own budget.
1070
- "timeout",
1071
- "out",
1072
- "team",
1073
- // `db push --dir`: which folder holds the migrations. A value rather than a positional
1074
- // so the command reads the same whether or not it is given.
1075
- "dir",
1076
- // `usage --days`: how much history to ask for.
1077
- "days"
1078
- ]);
1079
- function parseArgs(argv) {
1080
- const command = [];
1081
- const flags = {};
1082
- for (let i = 0; i < argv.length; i++) {
1083
- const arg = argv[i];
1084
- if (!arg.startsWith("-")) {
1085
- command.push(arg);
1086
- continue;
1087
- }
1088
- if (arg === "--") {
1089
- command.push(...argv.slice(i + 1));
1090
- break;
1091
- }
1092
- const [name, inline] = splitFlag(arg);
1093
- if (VALUED.has(name)) {
1094
- const value = inline ?? argv[++i];
1095
- if (value === void 0) {
1096
- throw new UsageError(`--${name} needs a value`);
1097
- }
1098
- flags[name] = value;
1099
- continue;
1100
- }
1101
- if (inline !== void 0) {
1102
- throw new UsageError(`--${name} does not take a value`);
1103
- }
1104
- if (!KNOWN_BOOLEANS.has(name)) {
1105
- throw new UsageError(`unknown option --${name}`);
1106
- }
1107
- flags[name] = true;
1108
- }
1109
- return { command, flags, json: flags.json === true };
1110
- }
1111
- var KNOWN_BOOLEANS = /* @__PURE__ */ new Set([
1112
- "json",
1113
- "help",
1114
- "version",
1115
- "force",
1116
- // `--quiet`: drop the commentary, keep the answer and keep the errors. Wired 2026-09-06;
1117
- // it parsed and did nothing before.
1118
- "quiet",
1119
- // `--yes` is deliberately NOT here. Nothing in this CLI prompts, so there is nothing for
1120
- // it to answer, and accepting it would tell a script author it had confirmed something.
1121
- // The command that needs a confirmation says which flag it wants (`db restore --force`).
1122
- // `db export --status`: look at the last copy without asking for another one. The
1123
- // distinction is worth a flag rather than an inference, because the other branch runs a
1124
- // pg_dump against somebody's production database.
1125
- "status",
1126
- "no-wait",
1127
- // `login --no-browser`: print the URL and wait, rather than trying to spawn anything.
1128
- // The case the loopback flow could never serve on its own: SSH, a container, WSL.
1129
- "no-browser",
1130
- // `login --device`: a code typed into a browser on ANY machine, with no callback to
1131
- // this one at all. --no-browser still needs the loopback redirect to come back here;
1132
- // this does not, which is what makes it work over SSH and inside a container.
1133
- "device",
1134
- "env",
1135
- // `db push`: find out, and change nothing. Its own flag rather than the absence of a
1136
- // --yes, because the safe thing should be the easy thing to type.
1137
- "dry-run",
1138
- // `db push`: run a new migration that sorts BEFORE one already applied. Refused
1139
- // otherwise, because it gives this database a history no fresh one will have.
1140
- "out-of-order",
1141
- // `mcp`: let the agent's delete_project actually delete. Off by default, and the tool
1142
- // is listed either way so the agent can say what it wanted rather than invent.
1143
- "allow-delete",
1144
- // `usage`: print the day-by-day table under the headline, which most callers do not
1145
- // want and --json always carries anyway.
1146
- "history"
1147
- ]);
1148
- function splitFlag(arg) {
1149
- const bare = arg.replace(/^--?/, "");
1150
- const equals = bare.indexOf("=");
1151
- if (equals === -1) {
1152
- return [bare, void 0];
1153
- }
1154
- return [bare.slice(0, equals), bare.slice(equals + 1)];
1155
- }
1156
- function flagString(args, name) {
1157
- const value = args.flags[name];
1158
- return typeof value === "string" ? value : void 0;
1159
- }
1160
- function flagBoolean(args, name) {
1161
- return args.flags[name] === true;
1162
- }
1163
- function flagNumber(args, name) {
1164
- const value = flagString(args, name);
1165
- if (value === void 0) {
1166
- return void 0;
1167
- }
1168
- const parsed = Number(value);
1169
- if (!Number.isInteger(parsed)) {
1170
- throw new UsageError(`--${name} must be a whole number, not "${value}"`);
1171
- }
1172
- return parsed;
1173
- }
1174
-
1175
- // src/main.ts
1176
- init_api();
1177
- init_failure();
1178
- init_interactive();
1179
- init_config();
1180
- init_device();
1181
- init_desktop();
1182
- init_output();
1183
- init_login();
1184
-
1185
- // src/commands/projects.ts
1186
- init_api();
1187
- init_api();
1188
- init_config();
1189
- init_output();
1190
- async function list() {
1191
- const { projects, allowance } = await listProjects();
1192
- emit({ projects, allowance }, () => {
1193
- if (projects.length === 0) {
1194
- say('No projects yet. `snoutdata projects create --name "my project"`.');
1195
- return;
1196
- }
1197
- process.stdout.write(
1198
- `${table([
1199
- ["REF", "NAME", "STATE", "REGION", "LAST CONNECTION"],
1200
- ...projects.map((p) => [
1201
- p.ref,
1202
- p.name,
1203
- // A project that is ready and refusing writes is NOT "ready" as far as
1204
- // somebody reading this is concerned: it is the state they need to know
1205
- // about, and showing `ready` beside a database that rejects every INSERT
1206
- // is the listing lying to them.
1207
- p.readOnly ? bold("read-only") : p.state === "ready" ? p.state : bold(p.state),
1208
- p.region,
1209
- relative(p.lastConnectionAt)
1210
- ])
1211
- ])}
1212
- `
1213
- );
1214
- });
1215
- }
1216
- async function create(options) {
1217
- const created = await call("cloud-project-create", {
1218
- name: options.name,
1219
- region: options.region,
1220
- teamId: options.teamId
1221
- });
1222
- const ref = created.project.ref;
1223
- say(`Created ${ref}.`);
1224
- let project = created.project;
1225
- if (options.wait) {
1226
- say("Waiting for it to start\u2026");
1227
- project = await waitForReady(ref, {
1228
- timeoutMs: options.timeoutMs,
1229
- onTick: (state) => say(dim(` ${state}`))
1230
- });
1231
- }
1232
- if (options.link) {
1233
- writeLink(options.link, { ref, name: project.name });
1234
- }
1235
- emit(
1236
- {
1237
- project,
1238
- // The one time a create's password is shown. `cloud-project-connection` gives
1239
- // it back to the owner any time, so this is convenience rather than a
1240
- // last chance — and saying so is better than implying otherwise.
1241
- password: created.password,
1242
- uri: `postgres://${project.user}:${encodeURIComponent(created.password)}@${project.host}:5432/${project.database}?sslmode=require`
1243
- },
1244
- () => {
1245
- process.stdout.write(
1246
- `postgres://${project.user}:${encodeURIComponent(created.password)}@${project.host}:5432/${project.database}?sslmode=require
1247
- `
1248
- );
1249
- say("");
1250
- say(`Ready. ${bold(ref)} in ${project.region}.`);
1251
- say("The password is stored for you: `snoutdata db url` prints this again.");
1252
- }
1253
- );
1254
- }
1255
- async function action(verb, ref) {
1256
- const fn = `cloud-project-${verb}`;
1257
- const result = await call(fn, { ref });
1258
- emit(result, () => {
1259
- say(
1260
- result.changed ? `${ref}: ${verb}d.` : `${ref} was already ${verb === "delete" ? "deleted" : `${verb}d`}.`
1261
- );
1262
- });
1263
- }
1264
- async function resetPassword(ref) {
1265
- const result = await call(
1266
- "cloud-project-reset-password",
1267
- { ref }
1268
- );
1269
- emit(result, () => {
1270
- process.stdout.write(`${result.password}
1271
- `);
1272
- say(`New password for ${result.user}. It applies ${result.appliesIn}.`);
1273
- });
1274
- }
1275
- async function listTeamsCommand() {
1276
- const { teams } = await listTeams();
1277
- emit({ teams }, () => {
1278
- if (teams.length === 0) {
1279
- say("You are not on any teams.");
1280
- return;
1281
- }
1282
- process.stdout.write(
1283
- `${table([
1284
- ["NAME", "ID", "SHARE"],
1285
- ...teams.map((team) => [
1286
- team.name,
1287
- team.id,
1288
- team.mayShare ? "yes" : team.why ?? "no"
1289
- ])
1290
- ])}
1291
- `
1292
- );
1293
- });
1294
- }
1295
- async function resolveTeam(wanted) {
1296
- const { teams } = await listTeams();
1297
- const byId = teams.find((team2) => team2.id === wanted);
1298
- const matches = byId ? [byId] : teams.filter((team2) => team2.name.toLowerCase() === wanted.toLowerCase());
1299
- if (matches.length === 0) {
1300
- throw new Error(
1301
- teams.length === 0 ? `You are not on any teams, so there is no "${wanted}" to share with.` : `No team called "${wanted}". You are on: ${teams.map((t) => t.name).join(", ")}.`
1302
- );
1303
- }
1304
- if (matches.length > 1) {
1305
- throw new Error(
1306
- `More than one team is called "${wanted}". Use the id: ${matches.map((t) => t.id).join(", ")}.`
1307
- );
1308
- }
1309
- const team = matches[0];
1310
- if (!team.mayShare) {
1311
- throw new Error(`${team.name}: ${team.why ?? "you cannot share a project into this team"}.`);
1312
- }
1313
- return team.id;
1314
- }
1315
-
1316
- // src/commands/tokens.ts
1317
- init_api();
1318
- init_output();
1319
- async function create2(options) {
1320
- const created = await call("cloud-token-create", { name: options.name, expiresInDays: options.expiresInDays });
1321
- emit(created, () => {
1322
- process.stdout.write(`${created.token}
1323
- `);
1324
- say("");
1325
- say(`${bold(created.name)} \u2014 ${created.expiresAt ? `expires ${created.expiresAt.slice(0, 10)}` : "does not expire"}.`);
1326
- say("This is the only time it is shown: only its hash is stored.");
1327
- say(dim(" export SNOUTDATA_ACCESS_TOKEN=<that token>"));
1328
- });
1329
- }
1330
- async function list2() {
1331
- const { tokens } = await call("cloud-token-list");
1332
- emit({ tokens }, () => {
1333
- if (tokens.length === 0) {
1334
- say("No access tokens. `snoutdata tokens create --name ci`.");
1335
- return;
1336
- }
1337
- process.stdout.write(
1338
- `${table([
1339
- ["PREFIX", "NAME", "STATE", "LAST USED", "EXPIRES"],
1340
- ...tokens.map((t) => [
1341
- t.current ? bold(t.prefix) : t.prefix,
1342
- t.name,
1343
- t.revokedAt ? "revoked" : t.live ? "live" : "expired",
1344
- relative(t.lastUsedAt),
1345
- t.expiresAt ? t.expiresAt.slice(0, 10) : "never"
1346
- ])
1347
- ])}
1348
- `
1349
- );
1350
- });
1351
- }
1352
- async function revoke(wanted) {
1353
- const body = wanted.startsWith("sdt_") ? { prefix: wanted } : { id: wanted };
1354
- const result = await call("cloud-token-revoke", body);
1355
- emit(result, () => {
1356
- say(
1357
- result.changed ? `Revoked ${result.token.prefix} (${result.token.name}).` : `${result.token.prefix} was already revoked.`
1358
- );
1359
- });
1360
- }
1361
-
1362
- // src/commands/db.ts
1363
- import { spawn as spawn2 } from "node:child_process";
1364
- import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
1365
- import { join as join3 } from "node:path";
1366
- import { open as open2 } from "node:fs/promises";
1367
-
1368
- // src/restore.ts
1369
- function dumpFormat(head) {
1370
- const ascii = (from, length) => String.fromCharCode(...Array.from(head.slice(from, from + length)));
1371
- if (ascii(0, 5) === "PGDMP") {
1372
- return "custom";
1373
- }
1374
- if (head.length >= 262 && ascii(257, 5) === "ustar") {
1375
- return "tar";
1376
- }
1377
- const printable = Array.from(head.slice(0, 256)).every(
1378
- (byte) => byte === 9 || byte === 10 || byte === 13 || byte >= 32 && byte < 127 || byte >= 128
1379
- );
1380
- return printable && head.length > 0 ? "plain" : "unknown";
1381
- }
1382
- function restoreVerdict(format, target, options = {}) {
1383
- if (target.readOnly) {
1384
- return {
1385
- ok: false,
1386
- code: "read-only",
1387
- reason: "this project is over its plan's storage limit, so it refuses writes. A restore would fail part way. Delete some rows or change plan first."
1388
- };
1389
- }
1390
- if (format === "unknown") {
1391
- return {
1392
- ok: false,
1393
- code: "unknown-format",
1394
- reason: "that file is not a pg_dump archive and does not look like SQL. `snoutdata db export` writes the custom format; psql reads plain .sql."
1395
- };
1396
- }
1397
- if (!options.force && target.existingTables > 0) {
1398
- return {
1399
- ok: false,
1400
- code: "not-empty",
1401
- reason: `this database already has ${target.existingTables} table${target.existingTables === 1 ? "" : "s"}. Restoring into it would leave some objects created and some not. Check --ref is the project you meant, or pass --force.`
1402
- };
1403
- }
1404
- return { ok: true, tool: format === "plain" ? "psql" : "pg_restore", format };
1405
- }
1406
- function classifyRestoreErrors(stderr) {
1407
- const lines = stderr.split("\n").filter((line) => /^pg_restore: error:/.test(line.trim()));
1408
- const fatal = lines.filter((line) => !/must be owner of extension/.test(line));
1409
- return { total: lines.length, harmless: lines.length - fatal.length, fatal };
1410
- }
1411
-
1412
- // src/commands/db.ts
1413
- init_api();
1414
- init_failure();
1415
- init_interactive();
1416
- init_api();
1417
- init_output();
1418
- async function warnIfReadOnly(ref) {
1419
- try {
1420
- const { projects } = await listProjects();
1421
- const project = projects.find((candidate) => candidate.ref === ref);
1422
- if (project?.readOnly) {
1423
- warn(`${ref} is read-only: it is over the storage limit on your plan. Reading works; writing does not.`);
1424
- } else if (project?.readOnlyPending) {
1425
- warn(`${ref} is about to become read-only: it has grown past the storage limit on your plan.`);
1426
- }
1427
- } catch {
1428
- }
1429
- }
1430
- async function url(ref) {
1431
- await warnIfReadOnly(ref);
1432
- const details = await connection(ref);
1433
- emit(details, () => {
1434
- process.stdout.write(`${details.uri}
1435
- `);
1436
- });
1437
- }
1438
- async function psql(ref, rest) {
1439
- if (rest.length === 0 && !canAsk()) {
1440
- const why = noHumanReason();
1441
- fail(
1442
- "usage",
1443
- `db psql with no statements opens an interactive session, and ${why ? explainNoHuman(why) : "there is nobody to use it"}. Pass statements after --, or use \`snoutdata db url\` and your own client.`
1444
- );
1445
- }
1446
- await warnIfReadOnly(ref);
1447
- const details = await connection(ref);
1448
- if (!details.wakesInstantly) {
1449
- say("This project is paused; the connection will wake it, which takes a few seconds.");
1450
- }
1451
- const child = spawn2(
1452
- "psql",
1453
- [
1454
- "--host",
1455
- details.host,
1456
- "--port",
1457
- String(details.port),
1458
- "--username",
1459
- details.user,
1460
- "--dbname",
1461
- details.database,
1462
- ...rest
1463
- ],
1464
- {
1465
- stdio: "inherit",
1466
- env: { ...process.env, PGPASSWORD: details.password, PGSSLMODE: "require" }
1467
- }
1468
- );
1469
- return new Promise((resolve3, reject) => {
1470
- child.on("error", (error) => {
1471
- if (error.code === "ENOENT") {
1472
- reject(new CliFailure("tool-missing", "psql is not installed. `snoutdata db url` prints a connection string for any client."));
1473
- return;
1474
- }
1475
- reject(new CliFailure("failed", String(error)));
1476
- });
1477
- child.on("close", (code) => resolve3(code ?? 0));
1478
- });
1479
- }
1480
- function writeEnv(directory, value, key = "DATABASE_URL") {
1481
- const path = join3(directory, ".env");
1482
- const line = `${key}=${value}`;
1483
- if (!existsSync3(path)) {
1484
- writeFileSync2(path, `${line}
1485
- `);
1486
- return { path, replaced: false };
1487
- }
1488
- const existing = readFileSync3(path, "utf8");
1489
- const pattern = new RegExp(`^${key}=.*$`, "m");
1490
- if (pattern.test(existing)) {
1491
- writeFileSync2(path, existing.replace(pattern, line));
1492
- return { path, replaced: true };
1493
- }
1494
- appendFileSync(path, existing.endsWith("\n") ? `${line}
1495
- ` : `
1496
- ${line}
1497
- `);
1498
- return { path, replaced: false };
1499
- }
1500
- async function exportDatabase(ref, options = {}) {
1501
- if (options.statusOnly) {
1502
- const { export: current } = await exportStatus(ref);
1503
- emit({ ref, export: current }, () => describeExport(current));
1504
- return 0;
1505
- }
1506
- const asked = await requestExport(ref);
1507
- if (!asked.export.pending && asked.export.url && !options.out) {
1508
- emit({ ref, export: asked.export }, () => describeExport(asked.export));
1509
- return 0;
1510
- }
1511
- say("Taking a copy of the database. This runs on the server and can take a few minutes.");
1512
- const finished = await waitForExport(ref, options.timeoutMs);
1513
- if (finished.error) {
1514
- fail("failed", `The export did not finish: ${finished.error}`, { ref });
1515
- }
1516
- if (!options.out) {
1517
- emit({ ref, export: finished }, () => describeExport(finished));
1518
- return 0;
1519
- }
1520
- if (!finished.url) {
1521
- fail("failed", "The copy was taken, but no download link could be signed. Run this again to get one.", { ref });
1522
- }
1523
- const written = await download(finished.url, options.out);
1524
- emit({ ref, path: options.out, bytes: written }, () => {
1525
- say(`Saved ${describeBytes(written)} to ${options.out}.`);
1526
- say(dim("Restore it with: pg_restore --dbname <your database> " + options.out));
1527
- });
1528
- return 0;
1529
- }
1530
- async function waitForExport(ref, timeoutMs = 30 * 6e4) {
1531
- const deadline = Date.now() + timeoutMs;
1532
- for (; ; ) {
1533
- const { export: current } = await exportStatus(ref);
1534
- if (!current.pending) {
1535
- return current;
1536
- }
1537
- if (Date.now() >= deadline) {
1538
- throw new Error(`the export of ${ref} was still running after waiting`);
1539
- }
1540
- await new Promise((resolve3) => setTimeout(resolve3, 3e3));
1541
- }
1542
- }
1543
- async function download(url2, path) {
1544
- const response = await fetch(url2);
1545
- if (!response.ok || !response.body) {
1546
- throw new Error(`the download failed: ${response.status} ${response.statusText}`);
1547
- }
1548
- const handle2 = await open2(path, "w");
1549
- let written = 0;
1550
- try {
1551
- for await (const chunk of response.body) {
1552
- await handle2.write(chunk);
1553
- written += chunk.byteLength;
1554
- }
1555
- } finally {
1556
- await handle2.close();
1557
- }
1558
- return written;
1559
- }
1560
- function describeExport(current) {
1561
- if (current.error) {
1562
- warn(`The last export did not finish: ${current.error}`);
1563
- return;
1564
- }
1565
- if (current.pending) {
1566
- say("An export is running. Ask again in a minute, or run `snoutdata db export`, which waits for it.");
1567
- return;
1568
- }
1569
- if (!current.completedAt) {
1570
- say("This project has never been exported.");
1571
- return;
1572
- }
1573
- say(`A copy taken ${relative(current.completedAt)}, ${describeBytes(current.bytes ?? 0)}.`);
1574
- if (current.url) {
1575
- process.stdout.write(`${current.url}
1576
- `);
1577
- if (current.urlExpiresAt) {
1578
- say(dim(`That link stops working ${relative(current.urlExpiresAt)}.`));
1579
- }
1580
- } else {
1581
- say(dim("The link has expired. Run the export again to sign a new one; the copy is kept."));
1582
- }
1583
- }
1584
- function describeBytes(value) {
1585
- if (value < 1024) {
1586
- return `${value} B`;
1587
- }
1588
- const units = ["KB", "MB", "GB", "TB"];
1589
- let size = value / 1024;
1590
- let unit = 0;
1591
- while (size >= 1024 && unit < units.length - 1) {
1592
- size /= 1024;
1593
- unit += 1;
1594
- }
1595
- return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;
1596
- }
1597
- async function restoreDatabase(ref, options) {
1598
- const path = options.file;
1599
- if (!path) {
1600
- fail("usage", "db restore needs --file, pointing at a dump. `snoutdata db export` writes one.");
1601
- }
1602
- if (!existsSync3(path)) {
1603
- fail("usage", `no file at ${path}`, { file: path });
1604
- }
1605
- const handle2 = await open2(path, "r");
1606
- const head = new Uint8Array(512);
1607
- const read = await handle2.read(head, 0, 512, 0);
1608
- await handle2.close();
1609
- const format = dumpFormat(head.slice(0, read.bytesRead));
1610
- const details = await connection(ref);
1611
- if (!details.wakesInstantly) {
1612
- say("This project is paused; the connection will wake it, which takes a few seconds.");
1613
- }
1614
- const { projects } = await listProjects();
1615
- const project = projects.find((candidate) => candidate.ref === ref);
1616
- const counted = await countTables(details);
1617
- if (counted === null) {
1618
- fail("tool-missing", "psql is not installed, so the tables in this project could not be counted. Install the Postgres client tools, or restore with your own client using `snoutdata db url`.");
1619
- }
1620
- const verdict = restoreVerdict(format, { existingTables: counted, readOnly: project?.readOnly === true }, { force: options.force });
1621
- if (!verdict.ok) {
1622
- fail("conflict", verdict.reason, { ref, file: path, refusal: verdict.code });
1623
- }
1624
- say(`Restoring ${path} into ${ref} with ${verdict.tool} (${verdict.format} format).`);
1625
- const run2 = await runRestore(details, verdict.tool, path);
1626
- const errors = classifyRestoreErrors(run2.stderr);
1627
- if (run2.code !== 0 && errors.fatal.length === 0 && errors.total > 0) {
1628
- emit({ ref, file: path, ok: true, tool: verdict.tool, format: verdict.format, ignoredErrors: errors.harmless }, () => {
1629
- say(`Restored into ${ref}.`);
1630
- say(
1631
- `${errors.harmless} error${errors.harmless === 1 ? "" : "s"} above ${errors.harmless === 1 ? "is" : "are"} expected and nothing is missing: a dump carries comments on the extensions this image installs, and your role does not own them.`
1632
- );
1633
- });
1634
- return 0;
1635
- }
1636
- if (run2.code !== 0) {
1637
- fail(
1638
- "failed",
1639
- `${verdict.tool} exited ${run2.code}. Nothing here retried it: a half-finished restore is undone by deleting the project and making another.`,
1640
- { ref, file: path, errors: errors.fatal }
1641
- );
1642
- }
1643
- emit({ ref, file: path, ok: true, tool: verdict.tool, format: verdict.format }, () => say(`Restored into ${ref}.`));
1644
- return 0;
1645
- }
1646
- async function countTables(details) {
1647
- return new Promise((resolve3) => {
1648
- const child = spawn2(
1649
- "psql",
1650
- [
1651
- "--host",
1652
- details.host,
1653
- "--port",
1654
- String(details.port),
1655
- "--username",
1656
- details.user,
1657
- "--dbname",
1658
- details.database,
1659
- "--no-psqlrc",
1660
- "-tAc",
1661
- // ORDINARY tables the USER made, and neither of those words is decoration.
1662
- // `information_schema.tables` was the first version of this and it counted
1663
- // VIEWS: a fresh hosted project carries pg_stat_statements, whose two views
1664
- // made every brand-new database look occupied, so the guard refused exactly
1665
- // the restore it exists to allow. Found by driving it, 2026-09-06.
1666
- //
1667
- // `deptype = 'e'` is the general form of the same fix: anything an extension
1668
- // created belongs to the extension and not to the user, so pgvector's tables
1669
- // do not count either.
1670
- "select count(*) from pg_class c join pg_namespace n on n.oid = c.relnamespace where c.relkind = 'r' and n.nspname not in ('pg_catalog', 'information_schema') and n.nspname not like 'pg\\_toast%' and not exists (select 1 from pg_depend d where d.objid = c.oid and d.deptype = 'e')"
1671
- ],
1672
- { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, PGPASSWORD: details.password, PGSSLMODE: "require" } }
1673
- );
1674
- let out = "";
1675
- child.stdout.on("data", (chunk) => out += String(chunk));
1676
- child.on("error", (error) => {
1677
- warn(
1678
- error.code === "ENOENT" ? "psql is not installed, and db restore needs it (and pg_restore for an archive)." : String(error)
1679
- );
1680
- resolve3(null);
1681
- });
1682
- child.on("close", (code) => resolve3(code === 0 ? Number(out.trim()) || 0 : null));
1683
- });
1684
- }
1685
- function runRestore(details, tool, path) {
1686
- const shared = ["--host", details.host, "--port", String(details.port), "--username", details.user, "--dbname", details.database];
1687
- const args = tool === "psql" ? [...shared, "--no-psqlrc", "-v", "ON_ERROR_STOP=1", "-f", path] : [...shared, "--no-owner", "--no-privileges", path];
1688
- return new Promise((resolve3) => {
1689
- const child = spawn2(tool, args, {
1690
- stdio: ["ignore", "inherit", "pipe"],
1691
- env: { ...process.env, PGPASSWORD: details.password, PGSSLMODE: "require" }
1692
- });
1693
- let stderr = "";
1694
- child.stderr?.on("data", (chunk) => {
1695
- const text = String(chunk);
1696
- stderr += text;
1697
- process.stderr.write(text);
1698
- });
1699
- child.on("error", (error) => {
1700
- warn(
1701
- error.code === "ENOENT" ? `${tool} is not installed, and this dump needs it.` : String(error)
1702
- );
1703
- resolve3({ code: 127, stderr });
1704
- });
1705
- child.on("close", (code) => resolve3({ code: code ?? 0, stderr }));
1706
- });
1707
- }
1708
-
1709
- // src/commands/push.ts
1710
- init_api();
1711
- init_output();
1712
- init_failure();
1713
- import { spawn as spawn3 } from "node:child_process";
1714
- import { createHash as createHash2 } from "node:crypto";
1715
- import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
1716
- import { isAbsolute, join as join4, relative as relativePath, resolve as resolve2 } from "node:path";
1717
-
1718
- // src/migrations.ts
1719
- function migrationOrder(files) {
1720
- return [...files].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1721
- }
1722
- function planMigrations(files, applied, options = {}) {
1723
- const ordered = migrationOrder(files);
1724
- const onDisk = new Map(ordered.map((file) => [file.name, file]));
1725
- const problems = [];
1726
- for (const done2 of applied) {
1727
- const file = onDisk.get(done2.name);
1728
- if (!file) {
1729
- problems.push({
1730
- code: "missing",
1731
- name: done2.name,
1732
- reason: `${done2.name} has already run against this database and is no longer in the folder. If it was renamed, the old name is still recorded and the new one looks new, so its statements would run a second time.`
1733
- });
1734
- continue;
1735
- }
1736
- if (file.checksum !== done2.checksum) {
1737
- problems.push({
1738
- code: "changed",
1739
- name: done2.name,
1740
- reason: `${done2.name} has changed since it ran. The database and this folder no longer agree about what happened to the schema, and running anything cannot fix that. Put the change in a new migration instead.`
1741
- });
1742
- }
1743
- }
1744
- const highWater = applied.map((one) => one.name).sort((a, b) => a < b ? -1 : a > b ? 1 : 0)[applied.length - 1];
1745
- const done = new Set(applied.map((one) => one.name));
1746
- const pending = [];
1747
- for (const file of ordered) {
1748
- if (done.has(file.name)) {
1749
- continue;
1750
- }
1751
- if (highWater !== void 0 && file.name < highWater && !options.outOfOrder) {
1752
- problems.push({
1753
- code: "out-of-order",
1754
- name: file.name,
1755
- reason: `${file.name} sorts before ${highWater}, which has already run. Applying it now gives this database a history no fresh database will have. Pass --out-of-order if that is what you mean.`
1756
- });
1757
- continue;
1758
- }
1759
- pending.push(file);
1760
- }
1761
- if (problems.length > 0) {
1762
- return { ok: false, problems };
1763
- }
1764
- return { ok: true, pending };
1765
- }
1766
- function wantsTransaction(sql) {
1767
- return !/^[\t ]*--[\t ]*snoutdata:no-transaction[\t ]*$/im.test(sql);
1768
- }
1769
-
1770
- // src/commands/push.ts
1771
- function shown(path) {
1772
- const near = relativePath(process.cwd(), path);
1773
- return near && !near.startsWith("..") ? near : path;
1774
- }
1775
- var LEDGER = "_snoutdata_migrations";
1776
- var LEDGER_DDL = `create table if not exists ${LEDGER} (
22
+ </body></html>`}function wt(){let e=xe(Re(32)),t=xe(gt("sha256").update(e).digest());return{verifier:e,challenge:t}}function Tn(e,t){return e==="darwin"?{command:"open",args:[t],verbatim:!1}:e==="win32"?{command:"cmd.exe",args:["/c","start",'""',`"${t}"`],verbatim:!0}:{command:"xdg-open",args:[t],verbatim:!1}}function yt(e){let{command:t,args:n,verbatim:r}=Tn(process.platform,e),o=vn(t,n,{stdio:"ignore",detached:!0,windowsVerbatimArguments:r});o.on("error",()=>{}),o.unref()}function En(e){let t=e??xe(Re(32));return{raw:t,hashed:gt("sha256").update(t).digest("hex")}}function On(e){return`${An}?${new URLSearchParams({client_id:kt,redirect_uri:e.redirectUri,response_type:"code",scope:"openid email profile",code_challenge:e.challenge,code_challenge_method:"S256",nonce:e.hashedNonce,state:e.state,prompt:"select_account"}).toString()}`}function _n(e,t){let n=new URL(e,"http://127.0.0.1");if(!n.searchParams.has("code")&&!n.searchParams.has("error"))return{kind:"ignore"};let r=n.searchParams.get("error_description")??n.searchParams.get("error");if(r)return{kind:"error",message:r};let o=n.searchParams.get("code");return o?n.searchParams.get("state")!==t?{kind:"error",message:"the browser came back with a state that did not match"}:{kind:"ok",code:o}:{kind:"error",message:"the browser came back without a code"}}async function jn(e,t,n){let r=await fetch($n,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:kt,client_secret:Pn,code:e,code_verifier:t,grant_type:"authorization_code",redirect_uri:n})}),o=await r.json().catch(()=>({}));if(!r.ok||!o.id_token)throw new Error(`Google would not exchange the code: ${o.error_description??o.error??`HTTP ${r.status}`}`);return o.id_token}async function Rn(e){let{verifier:t,challenge:n}=wt(),{raw:r,hashed:o}=En(),s=xe(Re(16)),{code:d,redirectUri:a}=await new Promise((w,m)=>{let k="",u=ht((O,K)=>{let P=_n(O.url??"/",s);if(P.kind==="ignore"){K.writeHead(404).end();return}K.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),K.end(P.kind==="ok"?Se("ok"):Se("failed",P.message)),u.close(),P.kind==="ok"?w({code:P.code,redirectUri:k}):m(new Error(P.message))});u.listen(0,"127.0.0.1",()=>{let O=u.address();k=`http://127.0.0.1:${typeof O=="object"&&O?O.port:0}`;let P=On({redirectUri:k,challenge:n,hashedNonce:o,state:s});process.stdout.write(`${P}
23
+ `),e.noBrowser?i("Open that in a browser, on any machine, and sign in."):(i("Opening that in your browser. If nothing opens, paste it into one."),yt(P))}),u.on("error",m),setTimeout(()=>{u.close(),m(new Error("timed out waiting for the browser"))},e.timeoutMs??3e5).unref()}),c=await jn(d,t,a),l=await fetch(`${R}/auth/v1/token?grant_type=id_token`,{method:"POST",headers:{apikey:F,"Content-Type":"application/json"},body:JSON.stringify({provider:"google",id_token:c,nonce:r})});if(!l.ok)throw new Error(`could not turn the Google sign-in into a session: ${(await l.text()).slice(0,200)}`);return bt(await l.json())}function bt(e){let t=e,n={accessToken:t.access_token,refreshToken:t.refresh_token,expiresAt:t.expires_at,email:t.user?.email};return j(n),n}async function ve(e={}){let t=e.provider??"google";if(t==="google")return Rn(e);let{verifier:n,challenge:r}=wt(),o=await new Promise((d,a)=>{let c=ht((w,m)=>{let k=new URL(w.url??"/","http://127.0.0.1"),u=k.searchParams.get("code"),M=k.searchParams.get("error_description")??k.searchParams.get("error");m.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),m.end(u?Se("ok"):Se("failed",M??"No code came back.")),c.close(),u?d(u):a(new Error(M??"the browser came back without a code"))});c.listen(0,"127.0.0.1",()=>{let w=c.address(),k=`http://127.0.0.1:${typeof w=="object"&&w?w.port:0}/callback`,u=new URL(`${R}/auth/v1/authorize`);u.searchParams.set("provider",t),u.searchParams.set("redirect_to",k),u.searchParams.set("code_challenge",r),u.searchParams.set("code_challenge_method","s256"),u.searchParams.set("prompt","select_account"),process.stdout.write(`${u.toString()}
24
+ `),e.noBrowser?i("Open that in a browser, on any machine, and sign in."):(i("Opening that in your browser. If nothing opens, paste it into one."),yt(u.toString()))}),setTimeout(()=>{c.close(),a(new Error("timed out waiting for the browser"))},e.timeoutMs??3e5).unref()}),s=await fetch(`${R}/auth/v1/token?grant_type=pkce`,{method:"POST",headers:{apikey:F,"Content-Type":"application/json"},body:JSON.stringify({auth_code:o,code_verifier:n})});if(!s.ok)throw new Error(`could not exchange the code: ${(await s.text()).slice(0,200)}`);return bt(await s.json())}function Nn(e){return e.replace(/[&<>"]/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[t])}var An,$n,kt,Pn,Ne=_(()=>{"use strict";T();z();E();An="https://accounts.google.com/o/oauth2/v2/auth",$n="https://oauth2.googleapis.com/token",kt="1064562458607-9fqmghjoqjr7d56b98agkvjt9vt26pdc.apps.googleusercontent.com",Pn="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"});import{createInterface as Dn}from"node:readline/promises";async function De(e){let t=Dn({input:process.stdin,output:process.stderr});try{let n=(await t.question(`${e} [Y/n] `)).trim().toLowerCase();return n===""||n==="y"||n==="yes"}finally{t.close()}}async function Cn(){let e=await ge();if(!e.available)return(e.skip==="handoff-disabled"||e.skip==="server-disabled")&&i(y("The SnoutData app is running but is not set to hand out sign-ins.")),null;let t=e.who.email??"the signed-in account";if(i(""),i(`The SnoutData app is open here, signed in as ${A(t)}.`),i(y("It can create an access token for this terminal. Your sign-in itself is not handed over,")),i(y("and you can revoke the token later with `snoutdata tokens revoke`.")),!await De("Use it?"))return null;i("Asking the app. Approve it there.");let n=await st(e.config,process.argv.slice(2).join(" ").slice(0,60)||"snoutdata");if("declined"in n)return i(`The app did not create a token: ${n.reason}`),null;let r={accessToken:n.token};return j(r),i(`Signed in with "${n.name}".`),r}async function In(){if(!await De("Sign in with a browser?"))return null;try{let e=await ve({});return i(`Signed in${e.email?` as ${e.email}`:""}.`),{accessToken:e.accessToken,refreshToken:e.refreshToken,expiresAt:e.expiresAt}}catch(e){return i(`That did not finish: ${e instanceof Error?e.message:String(e)}`),null}}async function Mn(){if(!await De("Sign in by typing a code into a browser on another machine?"))return null;let e=await ye({name:he(),platform:process.platform});i(""),i(` Open ${A(e.verifyUrl)}`),i(` Type ${A(e.code)}`),i(""),i(y("The code is typed rather than in the link on purpose: it is what proves the person")),i(y("at the browser is the person at this terminal. Waiting\u2026"));let t=await ke(e.secret,{intervalMs:Math.max(1,e.interval)*1e3,expiresAt:e.expiresAt}),n={accessToken:t.token};return j(n),i(`Signed in${t.name?` with "${t.name}"`:""}.`),n}async function xt(){if(!Y())return null;let e=await Cn();if(e)return e;let t=await In();return t||Mn()}var St=_(()=>{"use strict";z();we();je();ee();Ne();E()});var Q={};nn(Q,{ANON_KEY:()=>F,ApiError:()=>N,FUNCTIONS:()=>vt,NotSignedIn:()=>q,SUPABASE_URL:()=>R,call:()=>f,connection:()=>C,exportStatus:()=>Z,listProjects:()=>D,listTeams:()=>Te,requestExport:()=>re,waitForReady:()=>oe,whoami:()=>X});function Un(){let e=fe();return e?`not signed in, and ${me(e)}. Set SNOUTDATA_ACCESS_TOKEN to a token from \`snoutdata tokens create\`.`:"not signed in: run `snoutdata login`, or set SNOUTDATA_ACCESS_TOKEN"}async function Bn(){let e=Ze();if(e||(e=await xt()),!e)throw new q;if(!et(e)||!e.refreshToken)return e.accessToken;let t=await fetch(`${R}/auth/v1/token?grant_type=refresh_token`,{method:"POST",headers:{apikey:F,"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e.refreshToken})});if(!t.ok)throw new q;let n=await t.json(),r={accessToken:n.access_token,refreshToken:n.refresh_token,expiresAt:n.expires_at,email:e.email};return j(r),r.accessToken}async function f(e,t={},n="POST"){let r=await Bn(),o=n==="GET"&&typeof t=="object"&&t?`?${new URLSearchParams(t).toString()}`:"",s=await fetch(`${vt}/${e}${o}`,{method:n,headers:{Authorization:`Bearer ${r}`,apikey:F,"Content-Type":"application/json"},body:n==="GET"?void 0:JSON.stringify(t)}),d=await s.text(),a;try{a=d?JSON.parse(d):{}}catch{throw new N(s.status,d.slice(0,300)||s.statusText)}if(!s.ok){let c=typeof a=="object"&&a&&"error"in a?String(a.error):`${s.status} ${s.statusText}`;throw s.status===401?new q(c):new N(s.status,c)}return a}async function X(){return f("cloud-whoami")}async function D(){return f("cloud-project-list")}async function C(e){return f("cloud-project-connection",{ref:e})}async function Te(){return f("cloud-team-list")}async function re(e){return f("cloud-project-export",{ref:e})}async function Z(e){return f("cloud-project-export",{ref:e},"GET")}async function oe(e,t={}){let n=Date.now()+(t.timeoutMs??3e5);for(;;){let{projects:r}=await D(),o=r.find(s=>s.ref===e);if(!o)throw new N(404,`no project ${e}`);if(t.onTick?.(o.state),o.state==="ready")return o;if(Date.now()>=n)throw new N(504,`${e} is still ${o.state} after waiting`);await new Promise(s=>setTimeout(s,2e3))}}var R,F,vt,N,q,T=_(()=>{"use strict";z();ee();St();R=process.env.SNOUTDATA_SUPABASE_URL??"https://krlhuuvhhgkwfsutvxtb.supabase.co",F=process.env.SNOUTDATA_ANON_KEY??"sb_publishable_B0gbZinAncINGbDGOcP9rg_odcbriG3",vt=`${R}/functions/v1`,N=class extends Error{status;constructor(t,n){super(n),this.status=t}},q=class extends Error{constructor(t){super(t??Un())}}});var b=class extends Error{},rn=new Set(["name","region","ref","file","provider","expires","timeout","out","team","dir","days"]);function He(e){let t=[],n={};for(let r=0;r<e.length;r++){let o=e[r];if(!o.startsWith("-")){t.push(o);continue}if(o==="--"){t.push(...e.slice(r+1));break}let[s,d]=sn(o);if(rn.has(s)){let a=d??e[++r];if(a===void 0)throw new b(`--${s} needs a value`);n[s]=a;continue}if(d!==void 0)throw new b(`--${s} does not take a value`);if(!on.has(s))throw new b(`unknown option --${s}`);n[s]=!0}return{command:t,flags:n,json:n.json===!0}}var on=new Set(["json","help","version","force","quiet","status","no-wait","no-browser","device","env","dry-run","out-of-order","allow-delete","history"]);function sn(e){let t=e.replace(/^--?/,""),n=t.indexOf("=");return n===-1?[t,void 0]:[t.slice(0,n),t.slice(n+1)]}function x(e,t){let n=e.flags[t];return typeof n=="string"?n:void 0}function U(e,t){return e.flags[t]===!0}function ce(e,t){let n=x(e,t);if(n===void 0)return;let r=Number(n);if(!Number.isInteger(r))throw new b(`--${t} must be a whole number, not "${n}"`);return r}T();te();ee();z();je();we();E();Ne();T();T();z();E();async function Tt(){let{projects:e,allowance:t}=await D();p({projects:e,allowance:t},()=>{if(e.length===0){i('No projects yet. `snoutdata projects create --name "my project"`.');return}process.stdout.write(`${W([["REF","NAME","STATE","REGION","LAST CONNECTION"],...e.map(n=>[n.ref,n.name,n.readOnly?A("read-only"):n.state==="ready"?n.state:A(n.state),n.region,H(n.lastConnectionAt)])])}
25
+ `)})}async function Ce(e){let t=await f("cloud-project-create",{name:e.name,region:e.region,teamId:e.teamId}),n=t.project.ref;i(`Created ${n}.`);let r=t.project;e.wait&&(i("Waiting for it to start\u2026"),r=await oe(n,{timeoutMs:e.timeoutMs,onTick:o=>i(y(` ${o}`))})),e.link&&de(e.link,{ref:n,name:r.name}),p({project:r,password:t.password,uri:`postgres://${r.user}:${encodeURIComponent(t.password)}@${r.host}:5432/${r.database}?sslmode=require`},()=>{process.stdout.write(`postgres://${r.user}:${encodeURIComponent(t.password)}@${r.host}:5432/${r.database}?sslmode=require
26
+ `),i(""),i(`Ready. ${A(n)} in ${r.region}.`),i("The password is stored for you: `snoutdata db url` prints this again.")})}async function At(e,t){let n=`cloud-project-${e}`,r=await f(n,{ref:t});p(r,()=>{i(r.changed?`${t}: ${e}d.`:`${t} was already ${e==="delete"?"deleted":`${e}d`}.`)})}async function $t(e){let t=await f("cloud-project-reset-password",{ref:e});p(t,()=>{process.stdout.write(`${t.password}
27
+ `),i(`New password for ${t.user}. It applies ${t.appliesIn}.`)})}async function Pt(){let{teams:e}=await Te();p({teams:e},()=>{if(e.length===0){i("You are not on any teams.");return}process.stdout.write(`${W([["NAME","ID","SHARE"],...e.map(t=>[t.name,t.id,t.mayShare?"yes":t.why??"no"])])}
28
+ `)})}async function Et(e){let{teams:t}=await Te(),n=t.find(s=>s.id===e),r=n?[n]:t.filter(s=>s.name.toLowerCase()===e.toLowerCase());if(r.length===0)throw new Error(t.length===0?`You are not on any teams, so there is no "${e}" to share with.`:`No team called "${e}". You are on: ${t.map(s=>s.name).join(", ")}.`);if(r.length>1)throw new Error(`More than one team is called "${e}". Use the id: ${r.map(s=>s.id).join(", ")}.`);let o=r[0];if(!o.mayShare)throw new Error(`${o.name}: ${o.why??"you cannot share a project into this team"}.`);return o.id}T();E();async function Ot(e){let t=await f("cloud-token-create",{name:e.name,expiresInDays:e.expiresInDays});p(t,()=>{process.stdout.write(`${t.token}
29
+ `),i(""),i(`${A(t.name)} \u2014 ${t.expiresAt?`expires ${t.expiresAt.slice(0,10)}`:"does not expire"}.`),i("This is the only time it is shown: only its hash is stored."),i(y(" export SNOUTDATA_ACCESS_TOKEN=<that token>"))})}async function _t(){let{tokens:e}=await f("cloud-token-list");p({tokens:e},()=>{if(e.length===0){i("No access tokens. `snoutdata tokens create --name ci`.");return}process.stdout.write(`${W([["PREFIX","NAME","STATE","LAST USED","EXPIRES"],...e.map(t=>[t.current?A(t.prefix):t.prefix,t.name,t.revokedAt?"revoked":t.live?"live":"expired",H(t.lastUsedAt),t.expiresAt?t.expiresAt.slice(0,10):"never"])])}
30
+ `)})}async function jt(e){let t=e.startsWith("sdt_")?{prefix:e}:{id:e},n=await f("cloud-token-revoke",t);p(n,()=>{i(n.changed?`Revoked ${n.token.prefix} (${n.token.name}).`:`${n.token.prefix} was already revoked.`)})}import{spawn as Me}from"node:child_process";import{appendFileSync as qn,existsSync as It,readFileSync as Jn,writeFileSync as Ct}from"node:fs";import{join as Gn}from"node:path";import{open as Mt}from"node:fs/promises";function Rt(e){let t=(r,o)=>String.fromCharCode(...Array.from(e.slice(r,r+o)));return t(0,5)==="PGDMP"?"custom":e.length>=262&&t(257,5)==="ustar"?"tar":Array.from(e.slice(0,256)).every(r=>r===9||r===10||r===13||r>=32&&r<127||r>=128)&&e.length>0?"plain":"unknown"}function Nt(e,t,n={}){return t.readOnly?{ok:!1,code:"read-only",reason:"this project is over its plan's storage limit, so it refuses writes. A restore would fail part way. Delete some rows or change plan first."}:e==="unknown"?{ok:!1,code:"unknown-format",reason:"that file is not a pg_dump archive and does not look like SQL. `snoutdata db export` writes the custom format; psql reads plain .sql."}:!n.force&&t.existingTables>0?{ok:!1,code:"not-empty",reason:`this database already has ${t.existingTables} table${t.existingTables===1?"":"s"}. Restoring into it would leave some objects created and some not. Check --ref is the project you meant, or pass --force.`}:{ok:!0,tool:e==="plain"?"psql":"pg_restore",format:e}}function Dt(e){let t=e.split(`
31
+ `).filter(r=>/^pg_restore: error:/.test(r.trim())),n=t.filter(r=>!/must be owner of extension/.test(r));return{total:t.length,harmless:t.length-n.length,fatal:n}}T();te();ee();T();E();async function Ut(e){try{let{projects:t}=await D(),n=t.find(r=>r.ref===e);n?.readOnly?$(`${e} is read-only: it is over the storage limit on your plan. Reading works; writing does not.`):n?.readOnlyPending&&$(`${e} is about to become read-only: it has grown past the storage limit on your plan.`)}catch{}}async function Ue(e){await Ut(e);let t=await C(e);p(t,()=>{process.stdout.write(`${t.uri}
32
+ `)})}async function Bt(e,t){if(t.length===0&&!Y()){let o=fe();g("usage",`db psql with no statements opens an interactive session, and ${o?me(o):"there is nobody to use it"}. Pass statements after --, or use \`snoutdata db url\` and your own client.`)}await Ut(e);let n=await C(e);n.wakesInstantly||i("This project is paused; the connection will wake it, which takes a few seconds.");let r=Me("psql",["--host",n.host,"--port",String(n.port),"--username",n.user,"--dbname",n.database,...t],{stdio:"inherit",env:{...process.env,PGPASSWORD:n.password,PGSSLMODE:"require"}});return new Promise((o,s)=>{r.on("error",d=>{if(d.code==="ENOENT"){s(new B("tool-missing","psql is not installed. `snoutdata db url` prints a connection string for any client."));return}s(new B("failed",String(d)))}),r.on("close",d=>o(d??0))})}function Lt(e,t,n="DATABASE_URL"){let r=Gn(e,".env"),o=`${n}=${t}`;if(!It(r))return Ct(r,`${o}
33
+ `),{path:r,replaced:!1};let s=Jn(r,"utf8"),d=new RegExp(`^${n}=.*$`,"m");return d.test(s)?(Ct(r,s.replace(d,o)),{path:r,replaced:!0}):(qn(r,s.endsWith(`
34
+ `)?`${o}
35
+ `:`
36
+ ${o}
37
+ `),{path:r,replaced:!1})}async function Ft(e,t={}){if(t.statusOnly){let{export:s}=await Z(e);return p({ref:e,export:s},()=>Ie(s)),0}let n=await re(e);if(!n.export.pending&&n.export.url&&!t.out)return p({ref:e,export:n.export},()=>Ie(n.export)),0;i("Taking a copy of the database. This runs on the server and can take a few minutes.");let r=await Wn(e,t.timeoutMs);if(r.error&&g("failed",`The export did not finish: ${r.error}`,{ref:e}),!t.out)return p({ref:e,export:r},()=>Ie(r)),0;r.url||g("failed","The copy was taken, but no download link could be signed. Run this again to get one.",{ref:e});let o=await Hn(r.url,t.out);return p({ref:e,path:t.out,bytes:o},()=>{i(`Saved ${I(o)} to ${t.out}.`),i(y("Restore it with: pg_restore --dbname <your database> "+t.out))}),0}async function Wn(e,t=30*6e4){let n=Date.now()+t;for(;;){let{export:r}=await Z(e);if(!r.pending)return r;if(Date.now()>=n)throw new Error(`the export of ${e} was still running after waiting`);await new Promise(o=>setTimeout(o,3e3))}}async function Hn(e,t){let n=await fetch(e);if(!n.ok||!n.body)throw new Error(`the download failed: ${n.status} ${n.statusText}`);let r=await Mt(t,"w"),o=0;try{for await(let s of n.body)await r.write(s),o+=s.byteLength}finally{await r.close()}return o}function Ie(e){if(e.error){$(`The last export did not finish: ${e.error}`);return}if(e.pending){i("An export is running. Ask again in a minute, or run `snoutdata db export`, which waits for it.");return}if(!e.completedAt){i("This project has never been exported.");return}i(`A copy taken ${H(e.completedAt)}, ${I(e.bytes??0)}.`),e.url?(process.stdout.write(`${e.url}
38
+ `),e.urlExpiresAt&&i(y(`That link stops working ${H(e.urlExpiresAt)}.`))):i(y("The link has expired. Run the export again to sign a new one; the copy is kept."))}function I(e){if(e<1024)return`${e} B`;let t=["KB","MB","GB","TB"],n=e/1024,r=0;for(;n>=1024&&r<t.length-1;)n/=1024,r+=1;return`${n.toFixed(n>=10?0:1)} ${t[r]}`}async function qt(e,t){let n=t.file;n||g("usage","db restore needs --file, pointing at a dump. `snoutdata db export` writes one."),It(n)||g("usage",`no file at ${n}`,{file:n});let r=await Mt(n,"r"),o=new Uint8Array(512),s=await r.read(o,0,512,0);await r.close();let d=Rt(o.slice(0,s.bytesRead)),a=await C(e);a.wakesInstantly||i("This project is paused; the connection will wake it, which takes a few seconds.");let{projects:c}=await D(),l=c.find(M=>M.ref===e),w=await Kn(a);w===null&&g("tool-missing","psql is not installed, so the tables in this project could not be counted. Install the Postgres client tools, or restore with your own client using `snoutdata db url`.");let m=Nt(d,{existingTables:w,readOnly:l?.readOnly===!0},{force:t.force});m.ok||g("conflict",m.reason,{ref:e,file:n,refusal:m.code}),i(`Restoring ${n} into ${e} with ${m.tool} (${m.format} format).`);let k=await zn(a,m.tool,n),u=Dt(k.stderr);return k.code!==0&&u.fatal.length===0&&u.total>0?(p({ref:e,file:n,ok:!0,tool:m.tool,format:m.format,ignoredErrors:u.harmless},()=>{i(`Restored into ${e}.`),i(`${u.harmless} error${u.harmless===1?"":"s"} above ${u.harmless===1?"is":"are"} expected and nothing is missing: a dump carries comments on the extensions this image installs, and your role does not own them.`)}),0):(k.code!==0&&g("failed",`${m.tool} exited ${k.code}. Nothing here retried it: a half-finished restore is undone by deleting the project and making another.`,{ref:e,file:n,errors:u.fatal}),p({ref:e,file:n,ok:!0,tool:m.tool,format:m.format},()=>i(`Restored into ${e}.`)),0)}async function Kn(e){return new Promise(t=>{let n=Me("psql",["--host",e.host,"--port",String(e.port),"--username",e.user,"--dbname",e.database,"--no-psqlrc","-tAc","select count(*) from pg_class c join pg_namespace n on n.oid = c.relnamespace where c.relkind = 'r' and n.nspname not in ('pg_catalog', 'information_schema') and n.nspname not like 'pg\\_toast%' and not exists (select 1 from pg_depend d where d.objid = c.oid and d.deptype = 'e')"],{stdio:["ignore","pipe","pipe"],env:{...process.env,PGPASSWORD:e.password,PGSSLMODE:"require"}}),r="";n.stdout.on("data",o=>r+=String(o)),n.on("error",o=>{$(o.code==="ENOENT"?"psql is not installed, and db restore needs it (and pg_restore for an archive).":String(o)),t(null)}),n.on("close",o=>t(o===0?Number(r.trim())||0:null))})}function zn(e,t,n){let r=["--host",e.host,"--port",String(e.port),"--username",e.user,"--dbname",e.database],o=t==="psql"?[...r,"--no-psqlrc","-v","ON_ERROR_STOP=1","-f",n]:[...r,"--no-owner","--no-privileges",n];return new Promise(s=>{let d=Me(t,o,{stdio:["ignore","inherit","pipe"],env:{...process.env,PGPASSWORD:e.password,PGSSLMODE:"require"}}),a="";d.stderr?.on("data",c=>{let l=String(c);a+=l,process.stderr.write(l)}),d.on("error",c=>{$(c.code==="ENOENT"?`${t} is not installed, and this dump needs it.`:String(c)),s({code:127,stderr:a})}),d.on("close",c=>s({code:c??0,stderr:a}))})}T();E();te();import{spawn as Xn}from"node:child_process";import{createHash as Zn}from"node:crypto";import{existsSync as Qn,readdirSync as er,readFileSync as tr,statSync as nr}from"node:fs";import{isAbsolute as rr,join as Le,relative as or,resolve as sr}from"node:path";function Vn(e){return[...e].sort((t,n)=>t.name<n.name?-1:t.name>n.name?1:0)}function Jt(e,t,n={}){let r=Vn(e),o=new Map(r.map(l=>[l.name,l])),s=[];for(let l of t){let w=o.get(l.name);if(!w){s.push({code:"missing",name:l.name,reason:`${l.name} has already run against this database and is no longer in the folder. If it was renamed, the old name is still recorded and the new one looks new, so its statements would run a second time.`});continue}w.checksum!==l.checksum&&s.push({code:"changed",name:l.name,reason:`${l.name} has changed since it ran. The database and this folder no longer agree about what happened to the schema, and running anything cannot fix that. Put the change in a new migration instead.`})}let d=t.map(l=>l.name).sort((l,w)=>l<w?-1:l>w?1:0)[t.length-1],a=new Set(t.map(l=>l.name)),c=[];for(let l of r)if(!a.has(l.name)){if(d!==void 0&&l.name<d&&!n.outOfOrder){s.push({code:"out-of-order",name:l.name,reason:`${l.name} sorts before ${d}, which has already run. Applying it now gives this database a history no fresh database will have. Pass --out-of-order if that is what you mean.`});continue}c.push(l)}return s.length>0?{ok:!1,problems:s}:{ok:!0,pending:c}}function Be(e){return!/^[\t ]*--[\t ]*snoutdata:no-transaction[\t ]*$/im.test(e)}function Wt(e){let t=or(process.cwd(),e);return t&&!t.startsWith("..")?t:e}var se="_snoutdata_migrations",ar=`create table if not exists ${se} (
1777
39
  name text primary key,
1778
40
  checksum text not null,
1779
41
  applied_at timestamptz not null default now()
1780
- );`;
1781
- function readFolder(directory) {
1782
- if (!existsSync4(directory) || !statSync(directory).isDirectory()) {
1783
- throw new Error(
1784
- `no migrations folder at ${shown(directory)}. Put your .sql files there, or pass --dir.`
1785
- );
1786
- }
1787
- return readdirSync(directory).filter((name) => name.endsWith(".sql")).map((name) => {
1788
- const path = join4(directory, name);
1789
- const sql = readFileSync4(path, "utf8");
1790
- return {
1791
- name,
1792
- path,
1793
- sql,
1794
- checksum: createHash2("sha256").update(sql, "utf8").digest("hex")
1795
- };
1796
- });
1797
- }
1798
- function runSql(details, sql, extra = []) {
1799
- return new Promise((done) => {
1800
- const child = spawn3(
1801
- "psql",
1802
- [
1803
- "--host",
1804
- details.host,
1805
- "--port",
1806
- String(details.port),
1807
- "--username",
1808
- details.user,
1809
- "--dbname",
1810
- details.database,
1811
- "--no-psqlrc",
1812
- "-v",
1813
- "ON_ERROR_STOP=1",
1814
- ...extra
1815
- ],
1816
- { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, PGPASSWORD: details.password, PGSSLMODE: "require" } }
1817
- );
1818
- let out = "";
1819
- let err = "";
1820
- child.stdout.on("data", (chunk) => out += String(chunk));
1821
- child.stderr.on("data", (chunk) => err += String(chunk));
1822
- child.on("error", (error) => {
1823
- done({
1824
- code: 127,
1825
- out: "",
1826
- err: error.code === "ENOENT" ? "psql is not installed, and db push needs it to run your migrations. `snoutdata db url` prints a connection string for any client." : String(error)
1827
- });
1828
- });
1829
- child.on("close", (code) => done({ code: code ?? 0, out, err }));
1830
- child.stdin.end(sql);
1831
- });
1832
- }
1833
- async function readLedger(details) {
1834
- const created = await runSql(details, LEDGER_DDL);
1835
- if (created.code !== 0) {
1836
- throw new Error(created.err.trim() || `could not create ${LEDGER}`);
1837
- }
1838
- const rows = await runSql(details, `select name || E'\\t' || checksum from ${LEDGER};`, ["-tA"]);
1839
- if (rows.code !== 0) {
1840
- throw new Error(rows.err.trim() || `could not read ${LEDGER}`);
1841
- }
1842
- return rows.out.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
1843
- const tab = line.indexOf(" ");
1844
- return { name: line.slice(0, tab), checksum: line.slice(tab + 1) };
1845
- });
1846
- }
1847
- function literal(value) {
1848
- return `'${value.replace(/'/g, "''")}'`;
1849
- }
1850
- async function push(ref, options = {}) {
1851
- const result = await runPush(ref, options, (message) => say(message));
1852
- if (!result.ok) {
1853
- if (result.problems) {
1854
- const lines = result.problems.map((problem) => ` ${problem.reason}`).join("\n");
1855
- fail("conflict", `Refused. This folder and ${ref} do not agree:
1856
- ${lines}`, {
1857
- ref,
1858
- problems: result.problems
1859
- });
1860
- }
1861
- fail("failed", `${result.failed} failed, and nothing after it was tried.
1862
- ${result.error ?? ""}`.trimEnd(), {
1863
- ref,
1864
- failed: result.failed
1865
- });
1866
- }
1867
- emit(result, () => {
1868
- if (result.wouldApply) {
1869
- say(`Would apply ${result.wouldApply.length} migration${result.wouldApply.length === 1 ? "" : "s"} to ${ref}:`);
1870
- for (const name of result.wouldApply) {
1871
- say(` ${name}`);
1872
- }
1873
- return;
1874
- }
1875
- if (result.upToDate) {
1876
- say(`${ref} is up to date.`);
1877
- return;
1878
- }
1879
- say(`Applied ${result.applied.length} migration${result.applied.length === 1 ? "" : "s"} to ${ref}.`);
1880
- });
1881
- return 0;
1882
- }
1883
- async function runPush(ref, options = {}, note = () => {
1884
- }) {
1885
- const directory = resolve2(
1886
- options.dir === void 0 ? join4(process.cwd(), "migrations") : isAbsolute(options.dir) ? options.dir : join4(process.cwd(), options.dir)
1887
- );
1888
- const notes = [];
1889
- function record(message) {
1890
- notes.push(message);
1891
- note(message);
1892
- }
1893
- const files = readFolder(directory);
1894
- if (files.length === 0) {
1895
- record(`No .sql files in ${shown(directory)}.`);
1896
- return { ref, directory, ok: true, applied: [], upToDate: true, notes };
1897
- }
1898
- const details = await connection(ref);
1899
- if (!details.wakesInstantly) {
1900
- record("This project is paused; the connection will wake it, which takes a few seconds.");
1901
- }
1902
- const already = await readLedger(details);
1903
- const plan = planMigrations(files, already, { outOfOrder: options.outOfOrder });
1904
- if (!plan.ok) {
1905
- return { ref, directory, ok: false, applied: [], problems: [...plan.problems], notes };
1906
- }
1907
- const byName = new Map(files.map((file) => [file.name, file]));
1908
- const pending = plan.pending.map((one) => byName.get(one.name));
1909
- if (pending.length === 0) {
1910
- record(`${ref} is up to date: ${already.length} migration${already.length === 1 ? "" : "s"} already applied.`);
1911
- return { ref, directory, ok: true, applied: [], upToDate: true, notes };
1912
- }
1913
- if (options.dryRun) {
1914
- for (const one of pending) {
1915
- if (!wantsTransaction(one.sql)) {
1916
- record(`${one.name} would run outside a transaction.`);
1917
- }
1918
- }
1919
- return { ref, directory, ok: true, applied: [], wouldApply: pending.map((one) => one.name), notes };
1920
- }
1921
- const done = [];
1922
- for (const migration of pending) {
1923
- const ledgerRow = `insert into ${LEDGER} (name, checksum) values (${literal(migration.name)}, ${literal(migration.checksum)});`;
1924
- const wrapped = wantsTransaction(migration.sql);
1925
- if (!wrapped) {
1926
- record(`${migration.name} runs outside a transaction, so an interruption can leave it half applied.`);
1927
- }
1928
- const sql = wrapped ? `begin;
1929
- ${migration.sql}
1930
- ${ledgerRow}
42
+ );`;function ir(e){if(!Qn(e)||!nr(e).isDirectory())throw new Error(`no migrations folder at ${Wt(e)}. Put your .sql files there, or pass --dir.`);return er(e).filter(t=>t.endsWith(".sql")).map(t=>{let n=Le(e,t),r=tr(n,"utf8");return{name:t,path:n,sql:r,checksum:Zn("sha256").update(r,"utf8").digest("hex")}})}function Fe(e,t,n=[]){return new Promise(r=>{let o=Xn("psql",["--host",e.host,"--port",String(e.port),"--username",e.user,"--dbname",e.database,"--no-psqlrc","-v","ON_ERROR_STOP=1",...n],{stdio:["pipe","pipe","pipe"],env:{...process.env,PGPASSWORD:e.password,PGSSLMODE:"require"}}),s="",d="";o.stdout.on("data",a=>s+=String(a)),o.stderr.on("data",a=>d+=String(a)),o.on("error",a=>{r({code:127,out:"",err:a.code==="ENOENT"?"psql is not installed, and db push needs it to run your migrations. `snoutdata db url` prints a connection string for any client.":String(a)})}),o.on("close",a=>r({code:a??0,out:s,err:d})),o.stdin.end(t)})}async function cr(e){let t=await Fe(e,ar);if(t.code!==0)throw new Error(t.err.trim()||`could not create ${se}`);let n=await Fe(e,`select name || E'\\t' || checksum from ${se};`,["-tA"]);if(n.code!==0)throw new Error(n.err.trim()||`could not read ${se}`);return n.out.split(`
43
+ `).map(r=>r.trim()).filter(r=>r.length>0).map(r=>{let o=r.indexOf(" ");return{name:r.slice(0,o),checksum:r.slice(o+1)}})}function Gt(e){return`'${e.replace(/'/g,"''")}'`}async function Ht(e,t={}){let n=await qe(e,t,r=>i(r));if(!n.ok){if(n.problems){let r=n.problems.map(o=>` ${o.reason}`).join(`
44
+ `);g("conflict",`Refused. This folder and ${e} do not agree:
45
+ ${r}`,{ref:e,problems:n.problems})}g("failed",`${n.failed} failed, and nothing after it was tried.
46
+ ${n.error??""}`.trimEnd(),{ref:e,failed:n.failed})}return p(n,()=>{if(n.wouldApply){i(`Would apply ${n.wouldApply.length} migration${n.wouldApply.length===1?"":"s"} to ${e}:`);for(let r of n.wouldApply)i(` ${r}`);return}if(n.upToDate){i(`${e} is up to date.`);return}i(`Applied ${n.applied.length} migration${n.applied.length===1?"":"s"} to ${e}.`)}),0}async function qe(e,t={},n=()=>{}){let r=sr(t.dir===void 0?Le(process.cwd(),"migrations"):rr(t.dir)?t.dir:Le(process.cwd(),t.dir)),o=[];function s(u){o.push(u),n(u)}let d=ir(r);if(d.length===0)return s(`No .sql files in ${Wt(r)}.`),{ref:e,directory:r,ok:!0,applied:[],upToDate:!0,notes:o};let a=await C(e);a.wakesInstantly||s("This project is paused; the connection will wake it, which takes a few seconds.");let c=await cr(a),l=Jt(d,c,{outOfOrder:t.outOfOrder});if(!l.ok)return{ref:e,directory:r,ok:!1,applied:[],problems:[...l.problems],notes:o};let w=new Map(d.map(u=>[u.name,u])),m=l.pending.map(u=>w.get(u.name));if(m.length===0)return s(`${e} is up to date: ${c.length} migration${c.length===1?"":"s"} already applied.`),{ref:e,directory:r,ok:!0,applied:[],upToDate:!0,notes:o};if(t.dryRun){for(let u of m)Be(u.sql)||s(`${u.name} would run outside a transaction.`);return{ref:e,directory:r,ok:!0,applied:[],wouldApply:m.map(u=>u.name),notes:o}}let k=[];for(let u of m){let M=`insert into ${se} (name, checksum) values (${Gt(u.name)}, ${Gt(u.checksum)});`,O=Be(u.sql);O||s(`${u.name} runs outside a transaction, so an interruption can leave it half applied.`);let K=O?`begin;
47
+ ${u.sql}
48
+ ${M}
1931
49
  commit;
1932
- ` : `${migration.sql}
1933
- ${ledgerRow}
1934
- `;
1935
- const result = await runSql(details, sql);
1936
- if (result.code !== 0) {
1937
- if (wrapped) {
1938
- record("It ran inside a transaction, so nothing from it is in the database.");
1939
- }
1940
- return {
1941
- ref,
1942
- directory,
1943
- ok: false,
1944
- applied: done,
1945
- failed: migration.name,
1946
- error: result.err.trim(),
1947
- notes
1948
- };
1949
- }
1950
- done.push(migration.name);
1951
- record(` applied ${migration.name}`);
1952
- }
1953
- return { ref, directory, ok: true, applied: done, notes };
1954
- }
1955
-
1956
- // src/commands/mcp.ts
1957
- init_api();
1958
- init_output();
1959
-
1960
- // src/mcp.ts
1961
- var PROTOCOL_VERSION = "2024-11-05";
1962
- function object(properties, required = []) {
1963
- return { type: "object", properties, additionalProperties: false, ...required.length ? { required } : {} };
1964
- }
1965
- var STRING = { type: "string" };
1966
- var TOOLS = [
1967
- {
1968
- name: "whoami",
1969
- description: "Who this CLI is signed in as, and how (a session or a long-lived access token). Use it to check a credential before doing anything else.",
1970
- inputSchema: object({})
1971
- },
1972
- {
1973
- name: "list_projects",
1974
- description: "Every hosted Postgres database on this account, with its ref, name, region, state (ready, paused, error), size, and whether it is read-only because it is over its plan's storage limit.",
1975
- inputSchema: object({})
1976
- },
1977
- {
1978
- name: "create_project",
1979
- description: "Make a new hosted Postgres database and wait for it to be ready. Costs a project against the plan allowance; a free plan includes two. Returns its ref, which every other tool takes.",
1980
- inputSchema: object({ name: STRING, region: STRING, teamId: STRING }, ["name"])
1981
- },
1982
- {
1983
- name: "get_connection_url",
1984
- description: "The postgres:// URL for a project, ready to put in DATABASE_URL. IT CONTAINS A LIVE PASSWORD, so treat it as a credential and do not print it into a log or a file that is committed. Connecting to a paused project wakes it, which takes a few seconds.",
1985
- inputSchema: object({ ref: STRING }, ["ref"])
1986
- },
1987
- {
1988
- name: "pause_project",
1989
- description: "Stop a project. Its data is kept and the next connection wakes it. A production project refuses this.",
1990
- inputSchema: object({ ref: STRING }, ["ref"])
1991
- },
1992
- {
1993
- name: "resume_project",
1994
- description: "Start a paused project without waiting for a connection to wake it.",
1995
- inputSchema: object({ ref: STRING }, ["ref"])
1996
- },
1997
- {
1998
- name: "push_migrations",
1999
- description: "Run the .sql files in a folder against a project, in name order, once each, recording what ran in a _snoutdata_migrations table. Refuses a file that changed after it ran, a file that has gone, and a new file that sorts before one already applied. Use dryRun first to see what would happen. Needs psql on this machine.",
2000
- inputSchema: object({ ref: STRING, dir: STRING, dryRun: { type: "boolean" } }, ["ref"])
2001
- },
2002
- {
2003
- name: "usage",
2004
- description: "How much storage and compute a project has used against the limit on this plan, and whether it is near it. READ THIS BEFORE a migration or a bulk insert: a project over its storage limit is made read-only, and `list_projects` will say readOnly afterwards without ever saying how much headroom was left. Takes an optional number of days of history (default 30, maximum 365).",
2005
- inputSchema: object({ ref: STRING, days: { type: "number" } }, ["ref"])
2006
- },
2007
- {
2008
- name: "export_status",
2009
- description: "Whether a copy of this database is being taken, when the last one finished, and a download link if there is a live one. Costs nothing and starts nothing.",
2010
- inputSchema: object({ ref: STRING }, ["ref"])
2011
- },
2012
- {
2013
- name: "start_export",
2014
- description: "Take a copy of the whole database. Runs a pg_dump on the server and can take minutes, so this STARTS it and returns; poll export_status for the link. Worth doing before a migration you are unsure of. It does not block the database.",
2015
- inputSchema: object({ ref: STRING }, ["ref"])
2016
- },
2017
- {
2018
- name: "reset_password",
2019
- description: "Rotate the project's database password. THE OLD ONE STOPS WORKING IMMEDIATELY, including any DATABASE_URL already written into a file or a running process. Returns the new connection URL. Use it when a credential has leaked, not routinely.",
2020
- inputSchema: object({ ref: STRING }, ["ref"])
2021
- },
2022
- {
2023
- name: "list_teams",
2024
- description: "Teams this account belongs to, with the id create_project takes as teamId. Without this there is no way to discover that id.",
2025
- inputSchema: object({})
2026
- },
2027
- {
2028
- name: "list_tokens",
2029
- description: "The long-lived sdt_ access tokens on this account: id, name, prefix, when each was made and last used. Never the token itself, which is shown once when it is made.",
2030
- inputSchema: object({})
2031
- },
2032
- {
2033
- name: "create_token",
2034
- description: "Mint a long-lived sdt_ access token, for a CI job or another machine. RETURNS THE TOKEN ONCE AND NEVER AGAIN. It is as capable as this account, so hand it over deliberately. Optionally expires, as an ISO date.",
2035
- inputSchema: object({ name: STRING, expires: STRING }, ["name"])
2036
- },
2037
- {
2038
- name: "revoke_token",
2039
- description: "Revoke an access token by its id. Anything using it stops working at once. Safe to do to a token you think has leaked.",
2040
- inputSchema: object({ id: STRING }, ["id"])
2041
- },
2042
- {
2043
- name: "delete_project",
2044
- description: "Delete a project. THIS DESTROYS A DATABASE. Off unless the person who started this server passed --allow-delete, in which case it still only marks the project deleted and its data is erasable for a grace period.",
2045
- inputSchema: object({ ref: STRING }, ["ref"])
2046
- }
2047
- ];
2048
- function uniqueByName(tools) {
2049
- const seen = /* @__PURE__ */ new Set();
2050
- return tools.filter((tool) => !seen.has(tool.name) && seen.add(tool.name));
2051
- }
2052
- function ok(id, result) {
2053
- return { jsonrpc: "2.0", id, result };
2054
- }
2055
- function fail2(id, code, message) {
2056
- return { jsonrpc: "2.0", id, error: { code, message } };
2057
- }
2058
- function said(id, value, isError = false) {
2059
- const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
2060
- return ok(id, { content: [{ type: "text", text }], ...isError ? { isError: true } : {} });
2061
- }
2062
- function stringArg(params, name) {
2063
- const value = params[name];
2064
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
2065
- }
2066
- async function handle(request, operations2, options) {
2067
- const id = request.id ?? null;
2068
- const method = request.method ?? "";
2069
- const params = request.params ?? {};
2070
- if (request.id === void 0 || request.id === null) {
2071
- return null;
2072
- }
2073
- switch (method) {
2074
- case "initialize":
2075
- return ok(id, {
2076
- protocolVersion: PROTOCOL_VERSION,
2077
- capabilities: { tools: { listChanged: false } },
2078
- serverInfo: { name: "snoutdata", version: options.version }
2079
- });
2080
- case "ping":
2081
- return ok(id, {});
2082
- case "tools/list":
2083
- return ok(id, { tools: uniqueByName([...TOOLS, ...options.borrowed ?? []]) });
2084
- case "tools/call":
2085
- return callTool(id, params, operations2, options);
2086
- default:
2087
- return fail2(id, -32601, `unknown method: ${method}`);
2088
- }
2089
- }
2090
- async function callTool(id, params, operations2, options) {
2091
- const name = typeof params.name === "string" ? params.name : "";
2092
- const args = params.arguments ?? {};
2093
- const known = TOOLS.some((tool) => tool.name === name) || (options.borrowed?.some((tool) => tool.name === name) ?? false);
2094
- if (!known) {
2095
- return fail2(id, -32602, `unknown tool: ${name}`);
2096
- }
2097
- const needsRef = [
2098
- "get_connection_url",
2099
- "pause_project",
2100
- "resume_project",
2101
- "delete_project",
2102
- "push_migrations",
2103
- "usage",
2104
- "export_status",
2105
- "start_export",
2106
- "reset_password"
2107
- ];
2108
- const ref = stringArg(args, "ref");
2109
- if (needsRef.includes(name) && !ref) {
2110
- return said(id, `${name} needs a project ref. Call list_projects to see them.`, true);
2111
- }
2112
- try {
2113
- switch (name) {
2114
- case "whoami":
2115
- return said(id, await operations2.whoami());
2116
- case "list_projects":
2117
- return said(id, await operations2.listProjects());
2118
- case "create_project": {
2119
- const projectName = stringArg(args, "name");
2120
- if (!projectName) {
2121
- return said(id, "create_project needs a name.", true);
2122
- }
2123
- return said(
2124
- id,
2125
- await operations2.createProject({
2126
- name: projectName,
2127
- region: stringArg(args, "region") ?? void 0,
2128
- teamId: stringArg(args, "teamId") ?? void 0
2129
- })
2130
- );
2131
- }
2132
- case "get_connection_url": {
2133
- const details = await operations2.connection(ref);
2134
- return said(id, {
2135
- url: details.uri,
2136
- state: details.state,
2137
- wakesInstantly: details.wakesInstantly,
2138
- warning: "This URL contains a live database password. Put it in an environment variable, not in a file that is committed."
2139
- });
2140
- }
2141
- case "pause_project":
2142
- return said(id, await operations2.setProjectState("pause", ref));
2143
- case "resume_project":
2144
- return said(id, await operations2.setProjectState("resume", ref));
2145
- case "usage": {
2146
- const days = typeof args.days === "number" ? args.days : void 0;
2147
- return said(id, await operations2.usage(ref, days));
2148
- }
2149
- case "export_status":
2150
- return said(id, await operations2.exportStatus(ref));
2151
- case "start_export":
2152
- return said(id, await operations2.requestExport(ref));
2153
- case "reset_password":
2154
- return said(id, await operations2.resetPassword(ref));
2155
- case "list_teams":
2156
- return said(id, await operations2.listTeams());
2157
- case "list_tokens":
2158
- return said(id, await operations2.listTokens());
2159
- case "create_token": {
2160
- const tokenName = stringArg(args, "name");
2161
- if (!tokenName) {
2162
- return said(id, "create_token needs a name, so somebody can tell later what it was for.", true);
2163
- }
2164
- return said(id, await operations2.createToken(tokenName, stringArg(args, "expires") ?? void 0));
2165
- }
2166
- case "revoke_token": {
2167
- const tokenId = stringArg(args, "id");
2168
- if (!tokenId) {
2169
- return said(id, "revoke_token needs the id of a token. list_tokens has them.", true);
2170
- }
2171
- return said(id, await operations2.revokeToken(tokenId));
2172
- }
2173
- case "push_migrations":
2174
- return said(
2175
- id,
2176
- await operations2.pushMigrations(ref, {
2177
- dir: stringArg(args, "dir") ?? void 0,
2178
- dryRun: args.dryRun === true
2179
- })
2180
- );
2181
- case "delete_project": {
2182
- if (!options.allowDelete) {
2183
- return said(
2184
- id,
2185
- "delete_project is off. The person running this server started it without --allow-delete, so deleting a database is theirs to do. Tell them that is what you wanted.",
2186
- true
2187
- );
2188
- }
2189
- return said(id, await operations2.setProjectState("delete", ref));
2190
- }
2191
- default: {
2192
- if (options.borrowed?.some((tool) => tool.name === name)) {
2193
- if (!operations2.callBorrowed) {
2194
- return said(id, `${name} needs the SnoutData app, which is no longer answering.`, true);
2195
- }
2196
- return ok(id, await operations2.callBorrowed(name, args));
2197
- }
2198
- return fail2(id, -32602, `unknown tool: ${name}`);
2199
- }
2200
- }
2201
- } catch (error) {
2202
- return said(id, error instanceof Error ? error.message : String(error), true);
2203
- }
2204
- }
2205
- function takeMessages(buffer) {
2206
- const parts = buffer.split("\n");
2207
- const rest = parts.pop() ?? "";
2208
- return { messages: parts.map((line) => line.trim()).filter((line) => line.length > 0), rest };
2209
- }
2210
-
2211
- // src/commands/mcp.ts
2212
- init_desktop();
2213
- function operations() {
2214
- return {
2215
- whoami: () => whoami(),
2216
- listProjects: () => listProjects(),
2217
- createProject: async (input) => {
2218
- const created = await call("cloud-project-create", {
2219
- name: input.name,
2220
- ...input.region ? { region: input.region } : {},
2221
- ...input.teamId ? { teamId: input.teamId } : {}
2222
- });
2223
- try {
2224
- return await waitForReady(created.project.ref);
2225
- } catch {
2226
- return {
2227
- ...created.project,
2228
- ready: false,
2229
- note: `${created.project.ref} was created and is still starting. It was not ready within the wait. Call list_projects in a few seconds; do not connect to it yet.`
2230
- };
2231
- }
2232
- },
2233
- usage: (ref, days) => call("cloud-project-usage", { ref, ...days ? { days } : {} }),
2234
- exportStatus: (ref) => exportStatus(ref),
2235
- requestExport: (ref) => requestExport(ref),
2236
- resetPassword: (ref) => call("cloud-project-reset-password", { ref }),
2237
- listTeams: () => call("cloud-team-list", {}),
2238
- listTokens: () => call("cloud-token-list", {}),
2239
- createToken: (name, expires) => call("cloud-token-create", { name, ...expires ? { expires } : {} }),
2240
- revokeToken: (id) => call("cloud-token-revoke", { id }),
2241
- setProjectState: (verb, ref) => call(`cloud-project-${verb}`, { ref }),
2242
- connection: (ref) => connection(ref),
2243
- // The result-returning half, never the printing one: stdout is the JSON-RPC wire
2244
- // here, and a command that writes to it corrupts the protocol.
2245
- pushMigrations: (ref, options) => runPush(ref, { dir: options.dir, dryRun: options.dryRun })
2246
- };
2247
- }
2248
- var BORROWED = "app_";
2249
- async function borrow() {
2250
- if (process.env.SNOUTDATA_NO_DESKTOP) {
2251
- return { tools: [], config: null };
2252
- }
2253
- const found = await look();
2254
- if (!found.available) {
2255
- return { tools: [], config: null };
2256
- }
2257
- const config = found.config;
2258
- try {
2259
- const borrowed = await borrowTools(config);
2260
- const tools = borrowed.map((tool) => ({
2261
- // Prefixed so provenance is visible in the name itself. An agent choosing
2262
- // between `list_projects` (your account, in the cloud) and `app_list_connections`
2263
- // (the databases configured in the app on this machine) should not have to infer
2264
- // which is which from a description.
2265
- name: `${BORROWED}${tool.name}`,
2266
- description: `[SnoutData app, on this machine] ${tool.description ?? tool.name}`,
2267
- inputSchema: tool.inputSchema ?? { type: "object", properties: {} }
2268
- }));
2269
- return { tools, config };
2270
- } catch {
2271
- return { tools: [], config: null };
2272
- }
2273
- }
2274
- async function serve(options) {
2275
- setJsonMode(true);
2276
- const { tools: borrowed, config: appConfig } = await borrow();
2277
- const ops = operations();
2278
- if (appConfig) {
2279
- ops.callBorrowed = (name, args) => callBorrowed(appConfig, name.slice(BORROWED.length), args);
2280
- }
2281
- const served = { ...options, borrowed };
2282
- warn(
2283
- borrowed.length > 0 ? `snoutdata mcp: ${TOOLS.length} cloud tools, and ${borrowed.length} borrowed from the SnoutData app on this machine.` : "snoutdata mcp: cloud tools only. The SnoutData app is not answering here, so its database tools are not on this endpoint."
2284
- );
2285
- let rest = "";
2286
- process.stdin.setEncoding("utf8");
2287
- try {
2288
- const who = await whoami();
2289
- warn(`snoutdata mcp: signed in as ${who.email ?? "this account"}.`);
2290
- } catch (error) {
2291
- warn(`snoutdata mcp: ${error instanceof Error ? error.message : String(error)}`);
2292
- warn("Sign in with `snoutdata login`, or set SNOUTDATA_ACCESS_TOKEN.");
2293
- return 3;
2294
- }
2295
- warn(`snoutdata mcp: ready${options.allowDelete ? " (delete_project is ON)" : ""}. Speaking MCP on stdin/stdout; stop it with Ctrl-C.`);
2296
- const inFlight = /* @__PURE__ */ new Set();
2297
- function track(work) {
2298
- inFlight.add(work);
2299
- void work.finally(() => inFlight.delete(work));
2300
- }
2301
- return new Promise((resolve3) => {
2302
- process.stdin.on("data", (chunk) => {
2303
- const taken = takeMessages(rest + chunk);
2304
- rest = taken.rest;
2305
- for (const line of taken.messages) {
2306
- track(respond(line, ops, served));
2307
- }
2308
- });
2309
- process.stdin.on("end", () => {
2310
- void (async () => {
2311
- while (inFlight.size > 0) {
2312
- await Promise.allSettled([...inFlight]);
2313
- }
2314
- resolve3(0);
2315
- })();
2316
- });
2317
- process.stdin.on("error", () => resolve3(1));
2318
- });
2319
- }
2320
- async function respond(line, ops, options) {
2321
- let request;
2322
- try {
2323
- request = JSON.parse(line);
2324
- } catch {
2325
- warn(`snoutdata mcp: ignoring a line that is not JSON: ${line.slice(0, 120)}`);
2326
- return;
2327
- }
2328
- try {
2329
- const answer = await handle(request, ops, options);
2330
- if (answer) {
2331
- process.stdout.write(`${JSON.stringify(answer)}
2332
- `);
2333
- }
2334
- } catch (error) {
2335
- const id = request.id ?? null;
2336
- process.stdout.write(
2337
- `${JSON.stringify({
2338
- jsonrpc: "2.0",
2339
- id,
2340
- error: { code: -32603, message: error instanceof Error ? error.message : String(error) }
2341
- })}
2342
- `
2343
- );
2344
- }
2345
- }
2346
-
2347
- // src/commands/usage.ts
2348
- init_api();
2349
- init_output();
2350
-
2351
- // src/usage.ts
2352
- var NEAR_FRACTION = 0.8;
2353
- function storageState(fraction) {
2354
- if (fraction >= 1) {
2355
- return "over";
2356
- }
2357
- return fraction >= NEAR_FRACTION ? "near" : "ok";
2358
- }
2359
- function summariseUsage(days, limit) {
2360
- let latest = null;
2361
- let peak = null;
2362
- let computeSeconds = 0;
2363
- let connections = 0;
2364
- for (const day of days) {
2365
- computeSeconds += Number.isFinite(day.computeSeconds) ? day.computeSeconds : 0;
2366
- connections += Number.isFinite(day.connections) ? day.connections : 0;
2367
- if (day.dbBytes === null || !Number.isFinite(day.dbBytes)) {
2368
- continue;
2369
- }
2370
- peak = peak === null ? day.dbBytes : Math.max(peak, day.dbBytes);
2371
- if (!latest || day.day >= latest.day) {
2372
- latest = { day: day.day, dbBytes: day.dbBytes, repoBytes: day.repoBytes };
2373
- }
2374
- }
2375
- const storage = latest && limit && limit.maxStorageBytes > 0 ? {
2376
- limitBytes: limit.maxStorageBytes,
2377
- usedBytes: latest.dbBytes,
2378
- fraction: latest.dbBytes / limit.maxStorageBytes,
2379
- state: storageState(latest.dbBytes / limit.maxStorageBytes)
2380
- } : null;
2381
- return { days: days.length, latest, peakDbBytes: peak, computeSeconds, connections, storage };
2382
- }
2383
- function describeCompute(seconds) {
2384
- if (!Number.isFinite(seconds) || seconds <= 0) {
2385
- return "none";
2386
- }
2387
- const day = Math.floor(seconds / 86400);
2388
- const hour = Math.floor(seconds % 86400 / 3600);
2389
- const minute = Math.floor(seconds % 3600 / 60);
2390
- if (day > 0) {
2391
- return hour > 0 ? `${day}d ${hour}h` : `${day}d`;
2392
- }
2393
- if (hour > 0) {
2394
- return minute > 0 ? `${hour}h ${minute}m` : `${hour}h`;
2395
- }
2396
- return minute > 0 ? `${minute}m` : `${Math.round(seconds)}s`;
2397
- }
2398
-
2399
- // src/commands/usage.ts
2400
- async function usage(ref, options = {}) {
2401
- const answer = await call("cloud-project-usage", {
2402
- ref,
2403
- ...options.days ? { days: options.days } : {}
2404
- });
2405
- const summary = summariseUsage(answer.days, answer.limit);
2406
- emit({ ...answer, summary }, () => {
2407
- say(bold(`${answer.name} (${answer.ref})`));
2408
- if (!summary.latest) {
2409
- say("Nothing measured yet. The host samples every fifteen minutes.");
2410
- } else if (summary.storage) {
2411
- const percent = Math.round(summary.storage.fraction * 100);
2412
- const of = `${describeBytes(summary.storage.usedBytes)} of ${describeBytes(summary.storage.limitBytes)} on ${answer.limit?.tier}`;
2413
- say(
2414
- summary.storage.state === "over" ? `${of} (${percent}%). Over the limit, so writes are refused until it shrinks or the plan changes.` : summary.storage.state === "near" ? `${of} (${percent}%). Close to the limit: at 100% the database goes read-only.` : `${of} (${percent}%).`
2415
- );
2416
- } else {
2417
- say(`${describeBytes(summary.latest.dbBytes)} as of ${summary.latest.day}.`);
2418
- }
2419
- if (answer.readOnly && summary.storage?.state !== "over") {
2420
- say("This project is currently read-only.");
2421
- }
2422
- say(
2423
- `Over ${summary.days} day${summary.days === 1 ? "" : "s"}: ${describeCompute(summary.computeSeconds)} of compute, ${summary.connections} connection${summary.connections === 1 ? "" : "s"}.`
2424
- );
2425
- if (summary.peakDbBytes !== null && summary.latest && summary.peakDbBytes > summary.latest.dbBytes) {
2426
- say(dim(`Peak in that window was ${describeBytes(summary.peakDbBytes)}.`));
2427
- }
2428
- if (options.history && answer.days.length > 0) {
2429
- say("");
2430
- say(
2431
- table([
2432
- ["DAY", "SIZE", "BACKUPS", "COMPUTE", "CONNS"],
2433
- ...answer.days.map((one) => [
2434
- one.day,
2435
- one.dbBytes === null ? "-" : describeBytes(one.dbBytes),
2436
- one.repoBytes === null ? "-" : describeBytes(one.repoBytes),
2437
- describeCompute(one.computeSeconds),
2438
- String(one.connections)
2439
- ])
2440
- ])
2441
- );
2442
- say(dim("A dash is a day nothing measured it, which is what a paused project looks like."));
2443
- }
2444
- });
2445
- return 0;
2446
- }
2447
-
2448
- // src/main.ts
2449
- var VERSION = "0.1.0";
2450
- var COMMANDS = [
2451
- { name: "init", summary: "Create or link a project and write a DATABASE_URL", flags: ["--name", "--region", "--env", "--ref"] },
2452
- { name: "login", summary: "Sign in through a browser, or with --device by typing a code into one anywhere", flags: ["--provider", "--no-browser", "--device", "--timeout"] },
2453
- { name: "logout", summary: "Forget the stored session", flags: [] },
2454
- { name: "whoami", summary: "Who this credential belongs to", flags: [] },
2455
- { name: "tokens list", summary: "Long-lived access tokens on this account", flags: [] },
2456
- { name: "tokens create", summary: "Mint an sdt_ token for CI or an agent", flags: ["--name", "--expires"] },
2457
- { name: "tokens revoke", summary: "Revoke one by id or prefix", flags: [] },
2458
- { name: "projects list", summary: "Every project on this account", flags: [] },
2459
- { name: "projects create", summary: "Make a hosted database", flags: ["--name", "--region", "--team", "--no-wait", "--timeout"] },
2460
- { name: "projects pause", summary: "Stop a project", flags: ["--ref"] },
2461
- { name: "projects resume", summary: "Start a paused project", flags: ["--ref"] },
2462
- { name: "projects delete", summary: "Delete a project", flags: ["--ref"] },
2463
- { name: "teams", summary: "Teams this account belongs to", flags: [] },
2464
- { name: "link", summary: "Point this folder at a project", flags: ["--ref"] },
2465
- { name: "usage", summary: "Size and compute against the plan limit", flags: ["--ref", "--days", "--history"] },
2466
- { name: "db url", summary: "A connection string", flags: ["--ref"] },
2467
- { name: "db psql", summary: "Open psql, or run statements after --", flags: ["--ref"] },
2468
- { name: "db reset-password", summary: "Rotate the project password", flags: ["--ref"] },
2469
- { name: "db export", summary: "Take or download a dump", flags: ["--ref", "--out", "--status", "--timeout"] },
2470
- { name: "db push", summary: "Apply migrations from a folder", flags: ["--dir", "--dry-run", "--out-of-order"] },
2471
- { name: "db restore", summary: "Put a dump into a project", flags: ["--file", "--force"] },
2472
- { name: "mcp", summary: "Serve these operations as tools over stdio", flags: ["--allow-delete"] }
2473
- ];
2474
- var USAGE = `snoutdata ${VERSION} \u2014 hosted Postgres, from a terminal or an agent
50
+ `:`${u.sql}
51
+ ${M}
52
+ `,P=await Fe(a,K);if(P.code!==0)return O&&s("It ran inside a transaction, so nothing from it is in the database."),{ref:e,directory:r,ok:!1,applied:k,failed:u.name,error:P.err.trim(),notes:o};k.push(u.name),s(` applied ${u.name}`)}return{ref:e,directory:r,ok:!0,applied:k,notes:o}}T();E();var lr="2024-11-05";function S(e,t=[]){return{type:"object",properties:e,additionalProperties:!1,...t.length?{required:t}:{}}}var v={type:"string"},Ae=[{name:"whoami",description:"Who this CLI is signed in as, and how (a session or a long-lived access token). Use it to check a credential before doing anything else.",inputSchema:S({})},{name:"list_projects",description:"Every hosted Postgres database on this account, with its ref, name, region, state (ready, paused, error), size, and whether it is read-only because it is over its plan's storage limit.",inputSchema:S({})},{name:"create_project",description:"Make a new hosted Postgres database and wait for it to be ready. Costs a project against the plan allowance; a free plan includes two. Returns its ref, which every other tool takes.",inputSchema:S({name:v,region:v,teamId:v},["name"])},{name:"get_connection_url",description:"The postgres:// URL for a project, ready to put in DATABASE_URL. IT CONTAINS A LIVE PASSWORD, so treat it as a credential and do not print it into a log or a file that is committed. Connecting to a paused project wakes it, which takes a few seconds.",inputSchema:S({ref:v},["ref"])},{name:"pause_project",description:"Stop a project. Its data is kept and the next connection wakes it. A production project refuses this.",inputSchema:S({ref:v},["ref"])},{name:"resume_project",description:"Start a paused project without waiting for a connection to wake it.",inputSchema:S({ref:v},["ref"])},{name:"push_migrations",description:"Run the .sql files in a folder against a project, in name order, once each, recording what ran in a _snoutdata_migrations table. Refuses a file that changed after it ran, a file that has gone, and a new file that sorts before one already applied. Use dryRun first to see what would happen. Needs psql on this machine.",inputSchema:S({ref:v,dir:v,dryRun:{type:"boolean"}},["ref"])},{name:"usage",description:"How much storage and compute a project has used against the limit on this plan, and whether it is near it. READ THIS BEFORE a migration or a bulk insert: a project over its storage limit is made read-only, and `list_projects` will say readOnly afterwards without ever saying how much headroom was left. Takes an optional number of days of history (default 30, maximum 365).",inputSchema:S({ref:v,days:{type:"number"}},["ref"])},{name:"export_status",description:"Whether a copy of this database is being taken, when the last one finished, and a download link if there is a live one. Costs nothing and starts nothing.",inputSchema:S({ref:v},["ref"])},{name:"start_export",description:"Take a copy of the whole database. Runs a pg_dump on the server and can take minutes, so this STARTS it and returns; poll export_status for the link. Worth doing before a migration you are unsure of. It does not block the database.",inputSchema:S({ref:v},["ref"])},{name:"reset_password",description:"Rotate the project's database password. THE OLD ONE STOPS WORKING IMMEDIATELY, including any DATABASE_URL already written into a file or a running process. Returns the new connection URL. Use it when a credential has leaked, not routinely.",inputSchema:S({ref:v},["ref"])},{name:"list_teams",description:"Teams this account belongs to, with the id create_project takes as teamId. Without this there is no way to discover that id.",inputSchema:S({})},{name:"list_tokens",description:"The long-lived sdt_ access tokens on this account: id, name, prefix, when each was made and last used. Never the token itself, which is shown once when it is made.",inputSchema:S({})},{name:"create_token",description:"Mint a long-lived sdt_ access token, for a CI job or another machine. RETURNS THE TOKEN ONCE AND NEVER AGAIN. It is as capable as this account, so hand it over deliberately. Optionally expires, as an ISO date.",inputSchema:S({name:v,expires:v},["name"])},{name:"revoke_token",description:"Revoke an access token by its id. Anything using it stops working at once. Safe to do to a token you think has leaked.",inputSchema:S({id:v},["id"])},{name:"delete_project",description:"Delete a project. THIS DESTROYS A DATABASE. Off unless the person who started this server passed --allow-delete, in which case it still only marks the project deleted and its data is erasable for a grace period.",inputSchema:S({ref:v},["ref"])}];function dr(e){let t=new Set;return e.filter(n=>!t.has(n.name)&&t.add(n.name))}function ae(e,t){return{jsonrpc:"2.0",id:e,result:t}}function Je(e,t,n){return{jsonrpc:"2.0",id:e,error:{code:t,message:n}}}function h(e,t,n=!1){let r=typeof t=="string"?t:JSON.stringify(t,null,2);return ae(e,{content:[{type:"text",text:r}],...n?{isError:!0}:{}})}function J(e,t){let n=e[t];return typeof n=="string"&&n.trim().length>0?n.trim():null}async function Kt(e,t,n){let r=e.id??null,o=e.method??"",s=e.params??{};if(e.id===void 0||e.id===null)return null;switch(o){case"initialize":return ae(r,{protocolVersion:lr,capabilities:{tools:{listChanged:!1}},serverInfo:{name:"snoutdata",version:n.version}});case"ping":return ae(r,{});case"tools/list":return ae(r,{tools:dr([...Ae,...n.borrowed??[]])});case"tools/call":return ur(r,s,t,n);default:return Je(r,-32601,`unknown method: ${o}`)}}async function ur(e,t,n,r){let o=typeof t.name=="string"?t.name:"",s=t.arguments??{};if(!(Ae.some(l=>l.name===o)||(r.borrowed?.some(l=>l.name===o)??!1)))return Je(e,-32602,`unknown tool: ${o}`);let a=["get_connection_url","pause_project","resume_project","delete_project","push_migrations","usage","export_status","start_export","reset_password"],c=J(s,"ref");if(a.includes(o)&&!c)return h(e,`${o} needs a project ref. Call list_projects to see them.`,!0);try{switch(o){case"whoami":return h(e,await n.whoami());case"list_projects":return h(e,await n.listProjects());case"create_project":{let l=J(s,"name");return l?h(e,await n.createProject({name:l,region:J(s,"region")??void 0,teamId:J(s,"teamId")??void 0})):h(e,"create_project needs a name.",!0)}case"get_connection_url":{let l=await n.connection(c);return h(e,{url:l.uri,state:l.state,wakesInstantly:l.wakesInstantly,warning:"This URL contains a live database password. Put it in an environment variable, not in a file that is committed."})}case"pause_project":return h(e,await n.setProjectState("pause",c));case"resume_project":return h(e,await n.setProjectState("resume",c));case"usage":{let l=typeof s.days=="number"?s.days:void 0;return h(e,await n.usage(c,l))}case"export_status":return h(e,await n.exportStatus(c));case"start_export":return h(e,await n.requestExport(c));case"reset_password":return h(e,await n.resetPassword(c));case"list_teams":return h(e,await n.listTeams());case"list_tokens":return h(e,await n.listTokens());case"create_token":{let l=J(s,"name");return l?h(e,await n.createToken(l,J(s,"expires")??void 0)):h(e,"create_token needs a name, so somebody can tell later what it was for.",!0)}case"revoke_token":{let l=J(s,"id");return l?h(e,await n.revokeToken(l)):h(e,"revoke_token needs the id of a token. list_tokens has them.",!0)}case"push_migrations":return h(e,await n.pushMigrations(c,{dir:J(s,"dir")??void 0,dryRun:s.dryRun===!0}));case"delete_project":return r.allowDelete?h(e,await n.setProjectState("delete",c)):h(e,"delete_project is off. The person running this server started it without --allow-delete, so deleting a database is theirs to do. Tell them that is what you wanted.",!0);default:return r.borrowed?.some(l=>l.name===o)?n.callBorrowed?ae(e,await n.callBorrowed(o,s)):h(e,`${o} needs the SnoutData app, which is no longer answering.`,!0):Je(e,-32602,`unknown tool: ${o}`)}}catch(l){return h(e,l instanceof Error?l.message:String(l),!0)}}function zt(e){let t=e.split(`
53
+ `),n=t.pop()??"";return{messages:t.map(r=>r.trim()).filter(r=>r.length>0),rest:n}}we();function pr(){return{whoami:()=>X(),listProjects:()=>D(),createProject:async e=>{let t=await f("cloud-project-create",{name:e.name,...e.region?{region:e.region}:{},...e.teamId?{teamId:e.teamId}:{}});try{return await oe(t.project.ref)}catch{return{...t.project,ready:!1,note:`${t.project.ref} was created and is still starting. It was not ready within the wait. Call list_projects in a few seconds; do not connect to it yet.`}}},usage:(e,t)=>f("cloud-project-usage",{ref:e,...t?{days:t}:{}}),exportStatus:e=>Z(e),requestExport:e=>re(e),resetPassword:e=>f("cloud-project-reset-password",{ref:e}),listTeams:()=>f("cloud-team-list",{}),listTokens:()=>f("cloud-token-list",{}),createToken:(e,t)=>f("cloud-token-create",{name:e,...t?{expires:t}:{}}),revokeToken:e=>f("cloud-token-revoke",{id:e}),setProjectState:(e,t)=>f(`cloud-project-${e}`,{ref:t}),connection:e=>C(e),pushMigrations:(e,t)=>qe(e,{dir:t.dir,dryRun:t.dryRun})}}var Yt="app_";async function mr(){if(process.env.SNOUTDATA_NO_DESKTOP)return{tools:[],config:null};let e=await ge();if(!e.available)return{tools:[],config:null};let t=e.config;try{return{tools:(await at(t)).map(o=>({name:`${Yt}${o.name}`,description:`[SnoutData app, on this machine] ${o.description??o.name}`,inputSchema:o.inputSchema??{type:"object",properties:{}}})),config:t}}catch{return{tools:[],config:null}}}async function Vt(e){ne(!0);let{tools:t,config:n}=await mr(),r=pr();n&&(r.callBorrowed=(c,l)=>it(n,c.slice(Yt.length),l));let o={...e,borrowed:t};$(t.length>0?`snoutdata mcp: ${Ae.length} cloud tools, and ${t.length} borrowed from the SnoutData app on this machine.`:"snoutdata mcp: cloud tools only. The SnoutData app is not answering here, so its database tools are not on this endpoint.");let s="";process.stdin.setEncoding("utf8");try{let c=await X();$(`snoutdata mcp: signed in as ${c.email??"this account"}.`)}catch(c){return $(`snoutdata mcp: ${c instanceof Error?c.message:String(c)}`),$("Sign in with `snoutdata login`, or set SNOUTDATA_ACCESS_TOKEN."),3}$(`snoutdata mcp: ready${e.allowDelete?" (delete_project is ON)":""}. Speaking MCP on stdin/stdout; stop it with Ctrl-C.`);let d=new Set;function a(c){d.add(c),c.finally(()=>d.delete(c))}return new Promise(c=>{process.stdin.on("data",l=>{let w=zt(s+l);s=w.rest;for(let m of w.messages)a(fr(m,r,o))}),process.stdin.on("end",()=>{(async()=>{for(;d.size>0;)await Promise.allSettled([...d]);c(0)})()}),process.stdin.on("error",()=>c(1))})}async function fr(e,t,n){let r;try{r=JSON.parse(e)}catch{$(`snoutdata mcp: ignoring a line that is not JSON: ${e.slice(0,120)}`);return}try{let o=await Kt(r,t,n);o&&process.stdout.write(`${JSON.stringify(o)}
54
+ `)}catch(o){let s=r.id??null;process.stdout.write(`${JSON.stringify({jsonrpc:"2.0",id:s,error:{code:-32603,message:o instanceof Error?o.message:String(o)}})}
55
+ `)}}T();E();function gr(e){return e>=1?"over":e>=.8?"near":"ok"}function Xt(e,t){let n=null,r=null,o=0,s=0;for(let a of e)o+=Number.isFinite(a.computeSeconds)?a.computeSeconds:0,s+=Number.isFinite(a.connections)?a.connections:0,!(a.dbBytes===null||!Number.isFinite(a.dbBytes))&&(r=r===null?a.dbBytes:Math.max(r,a.dbBytes),(!n||a.day>=n.day)&&(n={day:a.day,dbBytes:a.dbBytes,repoBytes:a.repoBytes}));let d=n&&t&&t.maxStorageBytes>0?{limitBytes:t.maxStorageBytes,usedBytes:n.dbBytes,fraction:n.dbBytes/t.maxStorageBytes,state:gr(n.dbBytes/t.maxStorageBytes)}:null;return{days:e.length,latest:n,peakDbBytes:r,computeSeconds:o,connections:s,storage:d}}function Ge(e){if(!Number.isFinite(e)||e<=0)return"none";let t=Math.floor(e/86400),n=Math.floor(e%86400/3600),r=Math.floor(e%3600/60);return t>0?n>0?`${t}d ${n}h`:`${t}d`:n>0?r>0?`${n}h ${r}m`:`${n}h`:r>0?`${r}m`:`${Math.round(e)}s`}async function Zt(e,t={}){let n=await f("cloud-project-usage",{ref:e,...t.days?{days:t.days}:{}}),r=Xt(n.days,n.limit);return p({...n,summary:r},()=>{if(i(A(`${n.name} (${n.ref})`)),!r.latest)i("Nothing measured yet. The host samples every fifteen minutes.");else if(r.storage){let o=Math.round(r.storage.fraction*100),s=`${I(r.storage.usedBytes)} of ${I(r.storage.limitBytes)} on ${n.limit?.tier}`;i(r.storage.state==="over"?`${s} (${o}%). Over the limit, so writes are refused until it shrinks or the plan changes.`:r.storage.state==="near"?`${s} (${o}%). Close to the limit: at 100% the database goes read-only.`:`${s} (${o}%).`)}else i(`${I(r.latest.dbBytes)} as of ${r.latest.day}.`);n.readOnly&&r.storage?.state!=="over"&&i("This project is currently read-only."),i(`Over ${r.days} day${r.days===1?"":"s"}: ${Ge(r.computeSeconds)} of compute, ${r.connections} connection${r.connections===1?"":"s"}.`),r.peakDbBytes!==null&&r.latest&&r.peakDbBytes>r.latest.dbBytes&&i(y(`Peak in that window was ${I(r.peakDbBytes)}.`)),t.history&&n.days.length>0&&(i(""),i(W([["DAY","SIZE","BACKUPS","COMPUTE","CONNS"],...n.days.map(o=>[o.day,o.dbBytes===null?"-":I(o.dbBytes),o.repoBytes===null?"-":I(o.repoBytes),Ge(o.computeSeconds),String(o.connections)])])),i(y("A dash is a day nothing measured it, which is what a paused project looks like.")))}),0}var ie="0.1.0",Qt=[{name:"init",summary:"Create or link a project and write a DATABASE_URL",flags:["--name","--region","--env","--ref"]},{name:"login",summary:"Sign in through a browser, or with --device by typing a code into one anywhere",flags:["--provider","--no-browser","--device","--timeout"]},{name:"logout",summary:"Forget the stored session",flags:[]},{name:"whoami",summary:"Who this credential belongs to",flags:[]},{name:"tokens list",summary:"Long-lived access tokens on this account",flags:[]},{name:"tokens create",summary:"Mint an sdt_ token for CI or an agent",flags:["--name","--expires"]},{name:"tokens revoke",summary:"Revoke one by id or prefix",flags:[]},{name:"projects list",summary:"Every project on this account",flags:[]},{name:"projects create",summary:"Make a hosted database",flags:["--name","--region","--team","--no-wait","--timeout"]},{name:"projects pause",summary:"Stop a project",flags:["--ref"]},{name:"projects resume",summary:"Start a paused project",flags:["--ref"]},{name:"projects delete",summary:"Delete a project",flags:["--ref"]},{name:"teams",summary:"Teams this account belongs to",flags:[]},{name:"link",summary:"Point this folder at a project",flags:["--ref"]},{name:"usage",summary:"Size and compute against the plan limit",flags:["--ref","--days","--history"]},{name:"db url",summary:"A connection string",flags:["--ref"]},{name:"db psql",summary:"Open psql, or run statements after --",flags:["--ref"]},{name:"db reset-password",summary:"Rotate the project password",flags:["--ref"]},{name:"db export",summary:"Take or download a dump",flags:["--ref","--out","--status","--timeout"]},{name:"db push",summary:"Apply migrations from a folder",flags:["--dir","--dry-run","--out-of-order"]},{name:"db restore",summary:"Put a dump into a project",flags:["--file","--force"]},{name:"mcp",summary:"Serve these operations as tools over stdio",flags:["--allow-delete"]}],en=`snoutdata ${ie} \u2014 hosted Postgres, from a terminal or an agent
2475
56
 
2476
57
  snoutdata init [--name X] [--env] a database for this folder, linked, ready to use
2477
58
  snoutdata login [--provider github] sign in through a browser
@@ -2517,299 +98,10 @@ A project comes from --ref, then SNOUTDATA_PROJECT, then
2517
98
  .snoutdata/project.json in this folder or a parent. A token comes from
2518
99
  SNOUTDATA_ACCESS_TOKEN, then ~/.snoutdata/auth.json. The session that login writes lasts
2519
100
  an hour, so CI wants "tokens create", which does not expire.
2520
- `;
2521
- async function run(args) {
2522
- const [group, action2, ...rest] = args.command;
2523
- if (flagBoolean(args, "version") || group === "version") {
2524
- emit({ version: VERSION }, () => process.stdout.write(`${VERSION}
2525
- `));
2526
- return 0;
2527
- }
2528
- if (flagBoolean(args, "help") && group && group !== "help") {
2529
- const matching = COMMANDS.filter((one) => one.name === group || one.name.startsWith(`${group} `));
2530
- if (matching.length > 0) {
2531
- emit({ commands: matching }, () => {
2532
- for (const one of matching) {
2533
- const flags = one.flags.length > 0 ? ` ${one.flags.join(" ")}` : "";
2534
- process.stdout.write(` snoutdata ${one.name}${flags}
2535
- ${one.summary}
2536
- `);
2537
- }
2538
- process.stdout.write(`
101
+ `;async function hr(e){let[t,n,...r]=e.command;if(U(e,"version")||t==="version")return p({version:ie},()=>process.stdout.write(`${ie}
102
+ `)),0;if(U(e,"help")&&t&&t!=="help"){let a=Qt.filter(c=>c.name===t||c.name.startsWith(`${t} `));if(a.length>0)return p({commands:a},()=>{for(let c of a){let l=c.flags.length>0?` ${c.flags.join(" ")}`:"";process.stdout.write(` snoutdata ${c.name}${l}
103
+ ${c.summary}
104
+ `)}process.stdout.write(`
2539
105
  Every command also takes --json, --quiet and --help.
2540
- `);
2541
- });
2542
- return 0;
2543
- }
2544
- }
2545
- const wantsHelp = flagBoolean(args, "help") || group === "help";
2546
- if (wantsHelp || !group) {
2547
- emit({ version: VERSION, usage: USAGE.trim(), commands: COMMANDS }, () => process.stdout.write(USAGE));
2548
- return wantsHelp ? 0 : EXIT.usage;
2549
- }
2550
- const timeoutMs = () => {
2551
- const seconds = flagNumber(args, "timeout");
2552
- if (seconds === void 0) {
2553
- return void 0;
2554
- }
2555
- if (!(seconds > 0)) {
2556
- throw new UsageError("--timeout is a number of seconds greater than zero");
2557
- }
2558
- return seconds * 1e3;
2559
- };
2560
- const ref = () => {
2561
- const found = resolveRef({ flag: flagString(args, "ref") });
2562
- if (!found) {
2563
- throw new UsageError(
2564
- "no project: pass --ref, set SNOUTDATA_PROJECT, or run `snoutdata link --ref <ref>`"
2565
- );
2566
- }
2567
- return found;
2568
- };
2569
- switch (group) {
2570
- case "login": {
2571
- if (flagBoolean(args, "device")) {
2572
- const started = await start({ name: thisMachine(), platform: process.platform });
2573
- emit({ verifyUrl: started.verifyUrl, code: started.code, expiresAt: started.expiresAt }, () => {
2574
- say("");
2575
- say(` Open ${started.verifyUrl}`);
2576
- say(` Type ${started.code}`);
2577
- say("");
2578
- say(dim("Waiting for that to be approved\u2026"));
2579
- });
2580
- const approved = await waitForApproval(started.secret, {
2581
- intervalMs: Math.max(1, started.interval) * 1e3,
2582
- expiresAt: started.expiresAt
2583
- });
2584
- writeAuth({ accessToken: approved.token });
2585
- emit({ ok: true, name: approved.name ?? null }, () => say("Signed in."));
2586
- return 0;
2587
- }
2588
- const result = await login({
2589
- provider: flagString(args, "provider"),
2590
- noBrowser: flagBoolean(args, "no-browser") || !canAsk(),
2591
- timeoutMs: timeoutMs()
2592
- });
2593
- emit({ email: result.email ?? null }, () => say(`Signed in${result.email ? ` as ${result.email}` : ""}.`));
2594
- return 0;
2595
- }
2596
- case "logout": {
2597
- clearAuth();
2598
- const stillSet = Boolean(process.env.SNOUTDATA_ACCESS_TOKEN);
2599
- emit({ ok: true, environmentTokenStillSet: stillSet }, () => {
2600
- say("Signed out.");
2601
- if (stillSet) {
2602
- say("SNOUTDATA_ACCESS_TOKEN is still set in this shell, so commands will keep working.");
2603
- say(dim(" unset SNOUTDATA_ACCESS_TOKEN"));
2604
- }
2605
- });
2606
- return 0;
2607
- }
2608
- case "whoami": {
2609
- const me = await whoami();
2610
- emit(me, () => {
2611
- process.stdout.write(`${me.email ?? me.id}
2612
- `);
2613
- if (me.token) {
2614
- say(`via access token ${me.token.prefix} (${me.token.name}).`);
2615
- }
2616
- });
2617
- return 0;
2618
- }
2619
- case "tokens": {
2620
- switch (action2) {
2621
- case void 0:
2622
- case "list":
2623
- await list2();
2624
- return 0;
2625
- case "create": {
2626
- const name = flagString(args, "name") ?? rest[0];
2627
- if (!name) {
2628
- throw new UsageError("tokens create needs --name (what is it for, and where does it live)");
2629
- }
2630
- await create2({ name, expiresInDays: flagNumber(args, "expires") });
2631
- return 0;
2632
- }
2633
- case "revoke": {
2634
- const wanted = rest[0];
2635
- if (!wanted) {
2636
- throw new UsageError("tokens revoke needs the token's id or its sdt_ prefix");
2637
- }
2638
- await revoke(wanted);
2639
- return 0;
2640
- }
2641
- default:
2642
- throw new UsageError(`unknown command: tokens ${action2}`);
2643
- }
2644
- }
2645
- case "projects": {
2646
- switch (action2) {
2647
- case void 0:
2648
- case "list":
2649
- await list();
2650
- return 0;
2651
- case "create": {
2652
- const name = flagString(args, "name") ?? rest[0];
2653
- if (!name) {
2654
- throw new UsageError("projects create needs --name");
2655
- }
2656
- const teamFlag = flagString(args, "team");
2657
- await create({
2658
- teamId: teamFlag ? await resolveTeam(teamFlag) : void 0,
2659
- name,
2660
- region: flagString(args, "region"),
2661
- wait: !flagBoolean(args, "no-wait"),
2662
- timeoutMs: timeoutMs(),
2663
- link: void 0
2664
- });
2665
- return 0;
2666
- }
2667
- case "pause":
2668
- case "resume":
2669
- case "delete":
2670
- await action(action2, flagString(args, "ref") ?? rest[0] ?? ref());
2671
- return 0;
2672
- default:
2673
- throw new UsageError(`unknown command: projects ${action2}`);
2674
- }
2675
- }
2676
- case "teams": {
2677
- await listTeamsCommand();
2678
- return 0;
2679
- }
2680
- case "link": {
2681
- const target = flagString(args, "ref") ?? action2;
2682
- if (!target) {
2683
- throw new UsageError("link needs --ref");
2684
- }
2685
- const path = writeLink(process.cwd(), { ref: target });
2686
- emit({ ref: target, path }, () => say(`Linked ${target} (${path}).`));
2687
- return 0;
2688
- }
2689
- case "db": {
2690
- switch (action2) {
2691
- case "url":
2692
- await url(ref());
2693
- return 0;
2694
- case "psql":
2695
- return psql(ref(), rest);
2696
- case "reset-password":
2697
- await resetPassword(ref());
2698
- return 0;
2699
- case "push":
2700
- return push(ref(), {
2701
- dir: flagString(args, "dir"),
2702
- dryRun: Boolean(args.flags["dry-run"]),
2703
- outOfOrder: Boolean(args.flags["out-of-order"])
2704
- });
2705
- case "restore":
2706
- return restoreDatabase(ref(), {
2707
- file: flagString(args, "file"),
2708
- force: Boolean(args.flags["force"])
2709
- });
2710
- case "export":
2711
- return exportDatabase(ref(), {
2712
- timeoutMs: timeoutMs(),
2713
- out: flagString(args, "out"),
2714
- // `--status` looks without asking. Worth its own flag rather than being
2715
- // inferred, because the difference between the two is whether a
2716
- // `pg_dump` runs against somebody's production database.
2717
- statusOnly: Boolean(args.flags["status"])
2718
- });
2719
- default:
2720
- throw new UsageError(
2721
- action2 ? `unknown command: db ${action2}` : "db needs an action: url, psql, reset-password, export, push, restore"
2722
- );
2723
- }
2724
- }
2725
- case "usage":
2726
- return usage(ref(), {
2727
- days: flagNumber(args, "days"),
2728
- history: Boolean(args.flags["history"])
2729
- });
2730
- case "mcp":
2731
- return serve({ version: VERSION, allowDelete: Boolean(args.flags["allow-delete"]) });
2732
- case "init":
2733
- return init(args);
2734
- default:
2735
- throw new UsageError(`unknown command: ${group}`);
2736
- }
2737
- }
2738
- async function init(args) {
2739
- const existing = resolveRef({ flag: flagString(args, "ref") });
2740
- if (existing) {
2741
- say(`This folder is already linked to ${existing}.`);
2742
- await url(existing);
2743
- return 0;
2744
- }
2745
- const name = flagString(args, "name") ?? basenameOf(process.cwd());
2746
- say(`Creating a project called "${name}"\u2026`);
2747
- await create({
2748
- name,
2749
- region: flagString(args, "region"),
2750
- wait: true,
2751
- link: process.cwd()
2752
- });
2753
- const ref = resolveRef({ cwd: process.cwd() });
2754
- if (ref && flagBoolean(args, "env")) {
2755
- const { connection: connection2 } = await Promise.resolve().then(() => (init_api(), api_exports));
2756
- const details = await connection2(ref);
2757
- const written = writeEnv(process.cwd(), details.uri);
2758
- say(`${written.replaced ? "Updated" : "Wrote"} DATABASE_URL in ${written.path}.`);
2759
- }
2760
- return 0;
2761
- }
2762
- function basenameOf(directory) {
2763
- const parts = directory.split(/[\\/]/).filter(Boolean);
2764
- return parts[parts.length - 1] ?? "project";
2765
- }
2766
- function report(error) {
2767
- if (error instanceof CliFailure) {
2768
- emitFailure(error.code, error.message, error.details);
2769
- return error.exitCode;
2770
- }
2771
- if (error instanceof UsageError) {
2772
- emitFailure("usage", error.message);
2773
- return EXIT.usage;
2774
- }
2775
- if (error instanceof NotSignedIn) {
2776
- emitFailure("not-signed-in", error.message);
2777
- return EXIT["not-signed-in"];
2778
- }
2779
- if (error instanceof ApiError) {
2780
- const code2 = codeForStatus(error.status);
2781
- emitFailure(code2, error.message, { status: error.status });
2782
- return EXIT[code2];
2783
- }
2784
- const code = codeForThrown(error);
2785
- emitFailure(code, error instanceof Error ? error.message : String(error));
2786
- return EXIT[code];
2787
- }
2788
- function exitWith(code) {
2789
- process.exitCode = code;
2790
- setTimeout(() => process.exit(code), 2e3).unref();
2791
- }
2792
- function wantsJson(argv) {
2793
- const end = argv.indexOf("--");
2794
- return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
2795
- }
2796
- async function main() {
2797
- setJsonMode(wantsJson(process.argv.slice(2)));
2798
- let args;
2799
- try {
2800
- args = parseArgs(process.argv.slice(2));
2801
- } catch (error) {
2802
- emitFailure("usage", String(error instanceof Error ? error.message : error));
2803
- exitWith(EXIT.usage);
2804
- return;
2805
- }
2806
- setJsonMode(args.json);
2807
- setQuiet(args.flags.quiet === true);
2808
- setInteractive(interactiveState({ isTty: Boolean(process.stdin.isTTY), json: args.json, env: process.env }));
2809
- try {
2810
- exitWith(await run(args));
2811
- } catch (error) {
2812
- exitWith(report(error));
2813
- }
2814
- }
2815
- void main();
106
+ `)}),0}let o=U(e,"help")||t==="help";if(o||!t)return p({version:ie,usage:en.trim(),commands:Qt},()=>process.stdout.write(en)),o?0:L.usage;let s=()=>{let a=ce(e,"timeout");if(a!==void 0){if(!(a>0))throw new b("--timeout is a number of seconds greater than zero");return a*1e3}},d=()=>{let a=ue({flag:x(e,"ref")});if(!a)throw new b("no project: pass --ref, set SNOUTDATA_PROJECT, or run `snoutdata link --ref <ref>`");return a};switch(t){case"login":{if(U(e,"device")){let c=await ye({name:he(),platform:process.platform});p({verifyUrl:c.verifyUrl,code:c.code,expiresAt:c.expiresAt},()=>{i(""),i(` Open ${c.verifyUrl}`),i(` Type ${c.code}`),i(""),i(y("Waiting for that to be approved\u2026"))});let l=await ke(c.secret,{intervalMs:Math.max(1,c.interval)*1e3,expiresAt:c.expiresAt});return j({accessToken:l.token}),p({ok:!0,name:l.name??null},()=>i("Signed in.")),0}let a=await ve({provider:x(e,"provider"),noBrowser:U(e,"no-browser")||!Y(),timeoutMs:s()});return p({email:a.email??null},()=>i(`Signed in${a.email?` as ${a.email}`:""}.`)),0}case"logout":{Qe();let a=!!process.env.SNOUTDATA_ACCESS_TOKEN;return p({ok:!0,environmentTokenStillSet:a},()=>{i("Signed out."),a&&(i("SNOUTDATA_ACCESS_TOKEN is still set in this shell, so commands will keep working."),i(y(" unset SNOUTDATA_ACCESS_TOKEN")))}),0}case"whoami":{let a=await X();return p(a,()=>{process.stdout.write(`${a.email??a.id}
107
+ `),a.token&&i(`via access token ${a.token.prefix} (${a.token.name}).`)}),0}case"tokens":switch(n){case void 0:case"list":return await _t(),0;case"create":{let a=x(e,"name")??r[0];if(!a)throw new b("tokens create needs --name (what is it for, and where does it live)");return await Ot({name:a,expiresInDays:ce(e,"expires")}),0}case"revoke":{let a=r[0];if(!a)throw new b("tokens revoke needs the token's id or its sdt_ prefix");return await jt(a),0}default:throw new b(`unknown command: tokens ${n}`)}case"projects":switch(n){case void 0:case"list":return await Tt(),0;case"create":{let a=x(e,"name")??r[0];if(!a)throw new b("projects create needs --name");let c=x(e,"team");return await Ce({teamId:c?await Et(c):void 0,name:a,region:x(e,"region"),wait:!U(e,"no-wait"),timeoutMs:s(),link:void 0}),0}case"pause":case"resume":case"delete":return await At(n,x(e,"ref")??r[0]??d()),0;default:throw new b(`unknown command: projects ${n}`)}case"teams":return await Pt(),0;case"link":{let a=x(e,"ref")??n;if(!a)throw new b("link needs --ref");let c=de(process.cwd(),{ref:a});return p({ref:a,path:c},()=>i(`Linked ${a} (${c}).`)),0}case"db":switch(n){case"url":return await Ue(d()),0;case"psql":return Bt(d(),r);case"reset-password":return await $t(d()),0;case"push":return Ht(d(),{dir:x(e,"dir"),dryRun:!!e.flags["dry-run"],outOfOrder:!!e.flags["out-of-order"]});case"restore":return qt(d(),{file:x(e,"file"),force:!!e.flags.force});case"export":return Ft(d(),{timeoutMs:s(),out:x(e,"out"),statusOnly:!!e.flags.status});default:throw new b(n?`unknown command: db ${n}`:"db needs an action: url, psql, reset-password, export, push, restore")}case"usage":return Zt(d(),{days:ce(e,"days"),history:!!e.flags.history});case"mcp":return Vt({version:ie,allowDelete:!!e.flags["allow-delete"]});case"init":return wr(e);default:throw new b(`unknown command: ${t}`)}}async function wr(e){let t=ue({flag:x(e,"ref")});if(t)return i(`This folder is already linked to ${t}.`),await Ue(t),0;let n=x(e,"name")??yr(process.cwd());i(`Creating a project called "${n}"\u2026`),await Ce({name:n,region:x(e,"region"),wait:!0,link:process.cwd()});let r=ue({cwd:process.cwd()});if(r&&U(e,"env")){let{connection:o}=await Promise.resolve().then(()=>(T(),Q)),s=await o(r),d=Lt(process.cwd(),s.uri);i(`${d.replaced?"Updated":"Wrote"} DATABASE_URL in ${d.path}.`)}return 0}function yr(e){let t=e.split(/[\\/]/).filter(Boolean);return t[t.length-1]??"project"}function kr(e){if(e instanceof B)return G(e.code,e.message,e.details),e.exitCode;if(e instanceof b)return G("usage",e.message),L.usage;if(e instanceof q)return G("not-signed-in",e.message),L["not-signed-in"];if(e instanceof N){let n=lt(e.status);return G(n,e.message,{status:e.status}),L[n]}let t=dt(e);return G(t,e instanceof Error?e.message:String(e)),L[t]}function We(e){process.exitCode=e,setTimeout(()=>process.exit(e),2e3).unref()}function br(e){let t=e.indexOf("--");return(t===-1?e:e.slice(0,t)).includes("--json")}async function xr(){ne(br(process.argv.slice(2)));let e;try{e=He(process.argv.slice(2))}catch(t){G("usage",String(t instanceof Error?t.message:t)),We(L.usage);return}ne(e.json),mt(e.flags.quiet===!0),nt(pe({isTty:!!process.stdin.isTTY,json:e.json,env:process.env}));try{We(await hr(e))}catch(t){We(kr(t))}}xr();