snoutdata 0.18.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/snoutdata.mjs +242 -215
- package/package.json +1 -1
package/dist/snoutdata.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`,{mode:384}),
|
|
4
|
-
`),n}function
|
|
2
|
+
var uf=Object.defineProperty;var D=(e,t)=>()=>(e&&(t=e(e=0)),t);var ii=(e,t)=>{for(var n in t)uf(e,n,{get:t[n],enumerable:!0})};function li(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,a]=ff(o);if(pf.has(s)){let c=a??e[++r];if(c===void 0)throw new g(`--${s} needs a value`);n[s]=c;continue}if(a!==void 0)throw new g(`--${s} does not take a value`);if(!mf.has(s))throw new g(`unknown option --${s}`);n[s]=!0}return{command:t,flags:n,json:n.json===!0}}function ff(e){let t=e.replace(/^--?/,""),n=t.indexOf("=");return n===-1?[t,void 0]:[t.slice(0,n),t.slice(n+1)]}function E(e,t){let n=e.flags[t];return typeof n=="string"?n:void 0}function M(e,t){return e.flags[t]===!0}function Ie(e,t){let n=E(e,t);if(n===void 0)return;let r=Number(n);if(!Number.isInteger(r))throw new g(`--${t} must be a whole number, not "${n}"`);return r}var g,pf,mf,Ae=D(()=>{"use strict";g=class extends Error{},pf=new Set(["name","sort","at","region","ref","file","provider","domain","expires","project","timeout","out","team","dir","days","schema","db-url","data-api-version","postgrest-version","port","entrypoint","memory","concurrency","tail","registry-user","dockerfile","client-id","site-url","allow","subject","p8","key-id","team-id","topic","environment","email","channel","since","level","admin","token","host","dbname","sslmode","peer-host","to","key-type","ranges","seed","nodes","column","keyspace","schemas","min-bytes","interval-ms","range","node","value","add","remove","max-write-pause-ms","drain-timeout-ms","ack-timeout-ms","copy-mb-per-s","set","sample-ms","source","model","base-url","price-in","price-out","access-key-id","auth-header","models","amount","period","by","prompt"]);mf=new Set(["json","window","help","version","force","no-verify-jwt","reset","errors","follow","stdin","clear-registry","quiet","yes","plan","status","no-wait","no-join","queue","no-browser","device","sso","env","dry-run","out-of-order","allow-delete","local","no-migrations","history","show-url","check","watch","soft"])});import{chmodSync as gf,existsSync as ui,mkdirSync as pi,readFileSync as mi,rmSync as hf,writeFileSync as fi}from"node:fs";import{homedir as Bn}from"node:os";import{dirname as Co,join as gi,parse as yf,resolve as wf}from"node:path";function Do(e=Bn()){return gi(e,".snoutdata","auth.json")}function Hn(e=Bn()){let t=process.env.SNOUTDATA_ACCESS_TOKEN;if(t)return{accessToken:t};let n=Do(e);if(!ui(n))return null;try{let r=JSON.parse(mi(n,"utf8"));return typeof r.accessToken=="string"?r:null}catch{return null}}function qe(e,t=Bn()){let n=Do(t);pi(Co(n),{recursive:!0}),fi(n,`${JSON.stringify(e,null,2)}
|
|
3
|
+
`,{mode:384}),gf(n,384)}function hi(e=Bn()){hf(Do(e),{force:!0})}function yi(e,t=Date.now()){return e.expiresAt?e.expiresAt*1e3-6e4<=t:!1}function wi(e){return gi(e,".snoutdata","project.json")}function Dt(e){let t=wf(e),n=yf(t).root;for(;;){let r=wi(t);if(ui(r))try{let o=JSON.parse(mi(r,"utf8"));if(typeof o.ref=="string"&&o.ref)return typeof o.local=="string"&&o.local?{ref:o.ref,directory:t,local:o.local}:{ref:o.ref,directory:t}}catch{}if(t===n)return null;t=Co(t)}}function Mt(e,t){let n=wi(e);return pi(Co(n),{recursive:!0}),fi(n,`${JSON.stringify(t,null,2)}
|
|
4
|
+
`),n}function Kn(e){let t=e.environment??process.env;return e.flag?e.flag:t.SNOUTDATA_PROJECT?t.SNOUTDATA_PROJECT:Dt(e.cwd??process.cwd())?.ref??null}function bi(e){let t=e.environment??process.env,n=Dt(e.cwd??process.cwd())?.ref??null;if(e.flag)return n===e.flag?{kind:"linked",ref:n}:{kind:"link",ref:e.flag,replaces:n};let r=t.SNOUTDATA_PROJECT;return r?n===r?{kind:"linked",ref:n}:{kind:"environment",ref:r}:n?{kind:"linked",ref:n}:{kind:"create"}}var lt=D(()=>{"use strict"});import{existsSync as Mo,readFileSync as Si}from"node:fs";import{homedir as bf}from"node:os";import{isAbsolute as Sf,join as ct,resolve as jt}from"node:path";function _f(e=process.env,t=process.platform,n=bf()){let r;return t==="win32"?r=e.APPDATA??ct(n,"AppData","Roaming"):t==="darwin"?r=ct(n,"Library","Application Support"):r=e.XDG_CONFIG_HOME??ct(n,".config"),["snoutdata","snoutdata-dev"].map(o=>ct(r,o,"localProjects.json"))}function Gn(e=_f()){let t=[];for(let n of e)if(Mo(n))try{let r=JSON.parse(Si(n,"utf8"));for(let o of r.projects??[])typeof o.folder=="string"&&o.folder&&!t.some(s=>s.folder===o.folder)&&t.push({name:o.name??null,folder:o.folder,ref:o.ref??null})}catch{}return t}function qn(e){let t=new Map;for(let n of e.split(/\r?\n/)){let r=n.trim();if(!r||r.startsWith("#"))continue;let o=r.indexOf("=");if(o<=0)continue;let s=r.slice(0,o).trim(),a=r.slice(o+1).trim();a.length>=2&&(a[0]==='"'||a[0]==="'")&&a.endsWith(a[0])&&(a=a.slice(1,-1)),t.set(s,a)}return t}function sn(e){return Mo(ct(e,"compose.yaml"))&&Mo(ct(e,".env"))}function dt(e,t=null){let n=jt(e);if(!sn(n))throw new Error(`${n} is not a SnoutData stack folder: it needs compose.yaml and .env (see github.com/snoutdata/snout-stack)`);let r=qn(Si(ct(n,".env"),"utf8")),o=r.get("SNOUT_REF"),s=r.get("POSTGRES_OWNER_PASSWORD");if(!o||!s)throw new Error(`${ct(n,".env")} has no ${o?"POSTGRES_OWNER_PASSWORD":"SNOUT_REF"}, so this stack was not set up with \`snout-stack init\``);let a=Number(r.get("API_PORT")??8e3)||8e3,c=Number(r.get("DB_PORT")??5432)||5432;return{folder:n,ref:o,name:t,apiUrl:(r.get("API_EXTERNAL_URL")||`http://127.0.0.1:${a}`).replace(/\/$/,""),apiPort:a,dbPort:c,appKey:r.get("APP_KEY")||r.get("ANON_KEY")||null,serverKey:r.get("SERVER_KEY")||r.get("SERVICE_ROLE_KEY")||null,ownerPassword:s}}function _i(e,t=process.cwd(),n=Gn()){if(!e){if(sn(t))return dt(t);let[i]=n;if(n.length===1&&i)return dt(i.folder,i.name);throw n.length===0?new Error("there is no local project to link: set one up in Studio (Projects, Local), or pass the folder of a stack you made with snout-stack"):new Error(`there are ${n.length} local projects; name one: ${n.map(u=>u.name??u.folder).join(", ")}`)}let r=Sf(e)?e:jt(t,e);if(sn(r)){let i=n.find(u=>jt(u.folder)===jt(r));return dt(r,i?.name??null)}let o=e.toLowerCase(),s=n.filter(i=>i.name?.toLowerCase()===o||i.ref===e),[a]=s;if(s.length===1&&a)return dt(a.folder,a.name);let c=n.map(i=>i.name??i.folder);throw new Error(s.length>1?`more than one local project is called ${e}; link it by its folder`:`no local project called ${e}, and it is not a stack folder${c.length?`. The local projects are ${c.join(", ")}`:""}`)}function ki(e){let t=`${e.ref}_owner`;return{ref:e.ref,host:"127.0.0.1",port:e.dbPort,database:e.ref,user:t,password:e.ownerPassword,ssl:"disable",uri:`postgresql://${t}:${encodeURIComponent(e.ownerPassword)}@127.0.0.1:${e.dbPort}/${e.ref}`,appKey:e.appKey,serverKey:e.serverKey,keysIssuedAt:null,keysExpireAt:null,wakesInstantly:!0,state:"local"}}function vi(e,t,n=Gn()){if(t&&sn(t))try{let r=n.find(s=>jt(s.folder)===jt(t)),o=dt(t,r?.name??null);if(o.ref===e)return o}catch{}for(let r of n)if(r.ref===e&&sn(r.folder))return dt(r.folder,r.name);return null}var an=D(()=>{"use strict"});function V(e,t,n){throw new T(e,t,n)}function nt(e,t){return t==="no-capacity"?"no-capacity":e===400&&t==="region"?"usage":e===403?"forbidden":e===404?"not-found":e===409?"conflict":e===402||e===429||t==="provider_billing"?"quota":t==="timeout"?"timeout":t!=null&&kf.has(t)?"server":e===503?"not-ready":e===504?"timeout":e===410?"outdated":e>=500?"server":"failed"}function $i(e){let t=e;return!t||typeof t!="object"||t.code!=="ENOENT"||typeof t.path!="string"||t.syscall==="spawn"?null:`no file at ${t.path}`}function Wn(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(vf.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 tt,T,kf,vf,Se=D(()=>{"use strict";tt={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,outdated:11,"no-capacity":12,"tool-missing":127,server:1},T=class extends Error{code;details;constructor(t,n,r){super(n),this.name="CliFailure",this.code=t,this.details=r}get exitCode(){return tt[this.code]}};kf=new Set(["provider_auth","upstream_error","transport","egress_refused"]);vf=new Set(["ECONNREFUSED","ECONNRESET","ENOTFOUND","EAI_AGAIN","EHOSTUNREACH","ENETUNREACH","EPIPE","CERT_HAS_EXPIRED","UNABLE_TO_VERIFY_LEAF_SIGNATURE","DEPTH_ZERO_SELF_SIGNED_CERT"])});function zn(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 $f){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 Lt(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 Pi(e){jo=e}function ve(){return(jo??zn({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).canAsk}function Ut(){return(jo??zn({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).reason}var $f,jo,Pt=D(()=>{"use strict";$f=["CI","GITHUB_ACTIONS","GITLAB_CI","BUILDKITE","CIRCLECI","TF_BUILD"];jo=null});import{createHmac as Pf,randomBytes as xf,timingSafeEqual as Ef}from"node:crypto";import{existsSync as Af,readFileSync as Rf}from"node:fs";import{homedir as Lo,hostname as Tf}from"node:os";import{join as Ft}from"node:path";function Of(e,t,n){if(t==="win32"){let r=n.APPDATA??Ft(Lo(),"AppData","Roaming");return Ft(r,e)}return t==="darwin"?Ft(Lo(),"Library","Application Support",e):Ft(n.XDG_CONFIG_HOME??Ft(Lo(),".config"),e)}function Ei(e=process.platform,t=process.env){return t.SNOUTDATA_DESKTOP_CONFIG?[t.SNOUTDATA_DESKTOP_CONFIG]:["snoutdata","snoutdata-dev"].map(n=>Ft(Of(n,e,t),"mcp.json"))}function Nf(e=process.platform,t=process.env){return Ei(e,t)[0]}function If(e=Nf()){if(!Af(e))return null;try{let t=JSON.parse(Rf(e,"utf8"));return typeof t.port!="number"||typeof t.token!="string"||!t.token?null:t}catch{return null}}function Cf(e){return e?e.enabled===!1?"server-disabled":e.signInHandoff===!1?"handoff-disabled":null:"no-config"}function Mf(e,t,n){return Pf("sha256",e).update(`${Df}
|
|
5
5
|
${t}
|
|
6
|
-
${n}`).digest("base64url")}function
|
|
7
|
-
`);return}t()}function d(e){!
|
|
8
|
-
`)}function
|
|
9
|
-
`),
|
|
10
|
-
`)}function
|
|
11
|
-
`)}function
|
|
12
|
-
`)}function
|
|
6
|
+
${n}`).digest("base64url")}function jf(e,t){let n=Buffer.from(e),r=Buffer.from(t);return n.length===r.length&&Ef(n,r)}async function Ri(e){let t=e.port;if(typeof t!="number"||!Number.isInteger(t)||t<=0||t>65535||!e.token)return!1;let n=xf(32).toString("base64url");try{let r=await fetch(`http://127.0.0.1:${t}/cli/prove?nonce=${n}`,{redirect:"error",signal:AbortSignal.timeout(Ai)});if(!r.ok)return!1;let o=await r.json();return typeof o.proof=="string"&&jf(o.proof,Mf(e.token,t,n))}catch{return!1}}async function Fo(e,t,n,r=Ri){if(!await r(e).catch(()=>!1))throw new Jn(e.port);return fetch(`http://127.0.0.1:${e.port}${t}`,{...n,redirect:"error",headers:{...n.headers,Authorization:`Bearer ${e.token}`}})}async function Vn(e,t=Ri){if(e!==void 0)return xi(e,t);let n="no-config";for(let r of Ei()){let o=If(r);if(!o)continue;let s=await xi(o,t);if(s.available)return s;(s.skip!=="not-running"&&s.skip!=="not-verified"||n==="no-config")&&(n=s.skip)}return{available:!1,skip:n}}async function xi(e,t){let n=Cf(e);if(n)return{available:!1,skip:n};let r=e,o;try{let s=await Fo(r,"/cli/whoami",{signal:AbortSignal.timeout(Ai)},t);if(!s.ok)return{available:!1,skip:"not-running"};o=await s.json()}catch(s){return s instanceof Jn?{available:!1,skip:"not-verified"}:{available:!1,skip:"not-running"}}return o.signedIn?o.canMintToken?{available:!0,who:o,config:r}:{available:!1,skip:"cannot-mint"}:{available:!1,skip:"signed-out"}}async function Ti(e,t){let n=await Fo(e,"/cli/token",{method:"POST",headers:{"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??`SnoutData Studio refused (${n.status})`}}function Yn(){return Tf()}async function Uo(e,t,n,r){let o=await Fo(e,"/mcp",{method:"POST",headers:{"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??"SnoutData Studio refused");return s.result}async function Oi(e){return await Uo(e,"initialize",{protocolVersion:"2025-06-18",capabilities:{},clientInfo:{name:"snoutdata-cli",version:"0"}},3e3),(await Uo(e,"tools/list",{},3e3)).tools??[]}async function Ni(e,t,n){return Uo(e,"tools/call",{name:t,arguments:n},12e4)}var Ai,Df,Jn,Xn=D(()=>{"use strict";Ai=1500,Df="snoutdata-studio-proof-v1";Jn=class extends Error{constructor(t){super(`the process on 127.0.0.1:${t} did not prove it is SnoutData Studio, so Studio's token was not sent to it`),this.name="PeerNotVerified"}}});function Bo(e){try{return new URL(e).host}catch{return e}}function Qn(e,t=""){return{path:`/v1/fn/${e}${t}`,oldPath:`/functions/v1/${e}${t}`}}function ut(e,t){return{path:`/v1/identity${e}`,oldPath:`/auth/v1${t}`}}function Ff(e){return Zn.has(Bo(e))}function Bf(e,t,n){return t===void 0?e:{...e,[n?Uf:Lf]:t}}async function We(e,t,n={},r=fetch){let o=Bo(e),s=n.headers??{};function a(u){return r(`${e}${u?t.oldPath:t.path}`,{method:n.method??"GET",headers:Bf(s,n.key,u),body:n.body})}if(Zn.has(o))return a(!0);let c=await a(!1);if(c.status!==404)return c;let i=await a(!0);return i.status===404?c:(Zn.add(o),i)}async function Ci(e,t,n,r=fetch){if(!Zn.has(Bo(e)))try{await We(e,ut("/health","/health"),{key:n},r)}catch{}return`${e}${Ff(e)?t.oldPath:t.path}`}var Lf,Uf,Zn,er=D(()=>{"use strict";Lf="Snout-Key",Uf="apikey",Zn=new Set});import{basename as Hf}from"node:path";function nr(e={bun:typeof process.versions.bun=="string",execPath:process.execPath,script:process.argv[1]??""}){let t=Hf(e.execPath.replace(/\\/g,"/")).toLowerCase();if(e.bun&&t.startsWith("snoutdata"))return"binary";let n=e.script.replace(/\\/g,"/");return/\/_npx\//.test(n)?"npx":/\/node_modules\/snoutdata\//.test(n)?"npm":"source"}function Kf(e=nr(),t=process.platform){switch(e){case"binary":case"npm":return"snoutdata upgrade";case"npx":return"npx snoutdata@latest";case"source":return t==="win32"?rt:ln}}function Ho(e={}){let t=e.kind??nr(),n=e.platform??process.platform,r=e.minimum?`${e.minimum} or later is required`:"a newer version is required",o=n==="win32"?rt:`${ln} (or ${rt})`;return`snoutdata ${we} is installed and ${r}. Run \`${Kf(t,n)}\`, or install again: ${o}`}function Di(e,t){if(e==="dev"||t==="dev")return!1;let n=e.split("-")[0].split(".").map(Number),r=t.split("-")[0].split(".").map(Number);for(let o=0;o<Math.max(n.length,r.length);o+=1){let s=n[o]??0,a=r[o]??0;if(Number.isNaN(s)||Number.isNaN(a))return!1;if(s!==a)return s<a}return!1}var we,tr,ln,rt,cn=D(()=>{"use strict";we="0.19.0",tr=`snoutdata-cli/${we}`,ln="curl -fsSL https://snoutdata.com/install.sh | sh",rt="npm install -g snoutdata@latest"});async function Mi(e,t){let n=await We(ze,Qn(e),{method:"POST",key:Je,headers:{"Content-Type":"application/json","x-client-info":tr},body:JSON.stringify(t)});sr(n);let r=await n.json().catch(()=>({}));if(n.status===410){let o=r.error;throw ar(n,typeof o=="string"?o:"This version of the snoutdata CLI is no longer supported.")}return{status:n.status,body:r}}async function rr(e={}){let{status:t,body:n}=await Mi("cloud-device-start",{client:e});return t===429&&V("quota",n.error??"too many pairing attempts from here: wait a few minutes"),t===404&&V("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)&&V("server",n.error??`could not start a pairing (${t})`),n}async function or(e,t){let n=t.intervalMs,r=Date.parse(t.expiresAt);for(;;){Date.now()>r&&V("timeout","that pairing code expired before it was approved. Run the command again for a new one."),await new Promise(a=>setTimeout(a,n));let{status:o,body:s}=await Mi("cloud-device-poll",{secret:e});switch(o===404&&V("not-found","that pairing is not known to the server. Run the command again for a new code."),o===400&&V("usage",s.error??"that is not a pairing secret"),o!==200&&V("server",s.error??`the pairing could not be checked (${o})`),s.status){case"approved":return{token:s.token,name:s.name};case"denied":V("forbidden",s.error??"that pairing was refused in the browser");case"used":V("conflict",s.error??"that pairing has already been completed");case"expired":V("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 Ko=D(()=>{"use strict";me();er();Se();cn()});function un(e){dn=e}function Li(e){ji=e}function ir(){return dn}function b(e){return Ui?`\x1B[2m${e}\x1B[0m`:e}function O(e){return Ui?`\x1B[1m${e}\x1B[0m`:e}function w(e,t){if(dn){process.stdout.write(`${JSON.stringify(e,null,2)}
|
|
7
|
+
`);return}t()}function d(e){!dn&&!ji&&process.stderr.write(`${e}
|
|
8
|
+
`)}function pt(e,t,n){process.stderr.write(`${t}
|
|
9
|
+
`),dn&&process.stdout.write(`${JSON.stringify({ok:!1,code:e,error:t,...n},null,2)}
|
|
10
|
+
`)}function ie(e){process.stderr.write(`${e}
|
|
11
|
+
`)}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(`
|
|
12
|
+
`)}function ot(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,a="second";for(let[i,u]of o){if(s<i)break;s=Math.round(s/i),a=u}let c=`${s} ${a}${s===1?"":"s"}`;return n?`in ${c}`:`${c} ago`}var dn,ji,Ui,ue=D(()=>{"use strict";dn=!1,ji=!1;Ui=process.stdout.isTTY&&!process.env.NO_COLOR});import{createHash as Hi,randomBytes as Go}from"node:crypto";import{createServer as qf}from"node:http";import{createInterface as Wf}from"node:readline/promises";import{spawn as zf}from"node:child_process";function lr(e){return e.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function Fi(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.":dg(t??"Something went wrong.");return`<!doctype html><html><head><meta charset="utf-8"><title>SnoutData</title>
|
|
13
13
|
<meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
14
14
|
<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">
|
|
15
15
|
<div style="text-align:center;max-width:460px;padding:40px 24px">
|
|
@@ -21,12 +21,37 @@ ${n}`).digest("base64url")}function _p(e,t){let n=Buffer.from(e),r=Buffer.from(t
|
|
|
21
21
|
<p style="color:#8f887e;margin:0;font-size:15px;line-height:1.5">${o}</p>
|
|
22
22
|
${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>'}
|
|
23
23
|
</div>
|
|
24
|
-
</body></html>`}function
|
|
25
|
-
`),e.noBrowser?d("Open that in a browser, on any machine, and sign in."):(d("Opening that in your browser. If nothing opens, paste it into one."),Np(i))},i=>{o.close(),n(i)})}),o.on("error",n),setTimeout(()=>{o.close(),n(new Error("timed out waiting for the browser"))},e.timeoutMs??3e5).unref()})}async function Pa(e,t){let n=await Ie(Ce,Je("/session?via=pkce","/token?grant_type=pkce"),{method:"POST",key:je,headers:{"Content-Type":"application/json"},body:JSON.stringify({auth_code:e,code_verifier:t})});if(!n.ok)throw new Error(`could not exchange the code: ${(await n.text()).slice(0,200)}`);return Ea(await n.json())}async function Up(e,t,n){let r=await fetch(Cp,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:Vr(),client_secret:jp(),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 Bp(e){if(!Vr())throw new Error("this build of snoutdata has no Google sign-in (it was built for testing). Sign in with a released snoutdata, then run this one.");let{verifier:t,challenge:n}=Jr(),{raw:r,hashed:o}=Dp(),s=jn(zr(16)),{code:a,redirectUri:l}=await Yr({state:s,timeoutMs:e.timeoutMs,noBrowser:e.noBrowser,authorizeUrl:c=>Mp({redirectUri:c,challenge:n,hashedNonce:o,state:s,loginHint:e.email})}),i=await Up(a,t,l),u=await Ie(Ce,Je("/session?via=id_token","/token?grant_type=id_token"),{method:"POST",key:je,headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:"google",id_token:i,nonce:r})});if(!u.ok)throw new Error(`could not turn the Google sign-in into a session: ${(await u.text()).slice(0,200)}`);return Ea(await u.json())}function Ea(e){let t=e,n={accessToken:t.access_token,refreshToken:t.refresh_token,expiresAt:t.expires_at,email:t.user?.email};return Ne(n),n}function $a(e){let t=e.trim().toLowerCase();if(!t||/\s/.test(t))return null;let n=t.indexOf("@");if(n===0||t.indexOf("@",n+1)!==-1)return null;let r=n===-1?t:t.slice(n+1);if(!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(r))return null;let o=r.slice(r.lastIndexOf(".")+1);return/^[a-z]{2,}$/.test(o)?r:null}function Fp(e){try{let t=JSON.parse(e);for(let n of["error_description","msg","message","error"]){let r=t[n];if(typeof r=="string"&&r)return r}}catch{return""}return""}function Hp(e,t,n){let r=Fp(t);return e===404||/no sso provider/i.test(r)?new P("not-found",`No single sign-on is set up for ${n}. Check the address, or ask whoever administers your team to connect your identity provider.`,{domain:n}):/saml.*(disabled|not enabled)/i.test(r)?new P("failed",`Single sign-on is not available for ${n} yet. Ask whoever administers your team to connect your identity provider.`,{domain:n}):new P(Ue(e),r||`Could not start single sign-on for ${n}.`,{domain:n})}async function Kp(e){let t=await Ie(Ce,Je("/sso/start","/sso"),{method:"POST",key:je,headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:e.domain,redirect_to:e.redirectTo,skip_http_redirect:!0,code_challenge:e.challenge,code_challenge_method:"s256"})}),n=await t.text();if(!t.ok)throw Hp(t.status,n,e.domain);let r;try{r=JSON.parse(n).url}catch{r=void 0}if(typeof r!="string"||!r)throw new P("server",`Single sign-on for ${e.domain} was accepted but came back without somewhere to send the browser.`,{domain:e.domain});return r}async function qp(e){if(e!==void 0){let r=$a(e);if(!r)throw new m(`--domain wants a work email or a company domain, like you@yourcompany.com, not "${e}"`);return r}if(!ye()){let r=wt();throw new m(`login --sso needs --domain <your work email or company domain>${r?`, because ${yt(r)}`:""}`)}let t=Rp({input:process.stdin,output:process.stderr}),n=new Promise((r,o)=>{t.once("close",()=>o(new m("nothing left to read: pass --domain <your work email or company domain>")))});n.catch(()=>{});try{for(let r=0;r<3;r+=1){let o=await Promise.race([t.question("Your work email address: "),n]),s=$a(o);if(s)return s;d("That is not a work email or a company domain. Try something like you@yourcompany.com.")}}finally{t.close()}throw new m("no domain to sign in with: pass --domain <your work email or company domain>")}async function Gp(e){let t=await qp(e.domain),{verifier:n,challenge:r}=Jr();d(`Signing in with the identity provider for ${x(t)}.`);let{code:o}=await Yr({callbackPath:"/callback",timeoutMs:e.timeoutMs,noBrowser:e.noBrowser,authorizeUrl:s=>Kp({domain:t,redirectTo:s,challenge:r})});return Pa(o,n)}async function Dn(e={}){return Xr(e.email,await Wp(e))}function Xr(e,t){if(!e)return t;let n={...t,expectedEmail:e};return Ne(n),t.email&&!Zr(t.email,e)&&Q(`Signed in as ${t.email}, not ${e}. Run \`snoutdata login --email ${e}\` and pick that account.`),n}function Zr(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()}async function Wp(e){if(e.sso)return Gp({...e,domain:e.domain??e.email});let t=e.provider??"google";if(t==="google")return Bp(e);let{verifier:n,challenge:r}=Jr(),{code:o}=await Yr({callbackPath:"/callback",timeoutMs:e.timeoutMs,noBrowser:e.noBrowser,authorizeUrl:async s=>{let a=new URL(await ya(Ce,Je("/provider/start","/authorize"),je));return a.searchParams.set("provider",t),a.searchParams.set("redirect_to",s),a.searchParams.set("code_challenge",r),a.searchParams.set("code_challenge_method","s256"),a.searchParams.set("prompt","select_account"),a.toString()}});return Pa(o,n)}function zp(e){return e.replace(/[&<>"]/g,t=>({"&":"&","<":"<",">":">",'"':"""})[t])}var Ip,Cp,Mn=C(()=>{"use strict";re();En();be();Ge();me();at();te();Ip="https://accounts.google.com/o/oauth2/v2/auth",Cp="https://oauth2.googleapis.com/token"});import{createInterface as Jp}from"node:readline/promises";async function Qr(e){let t=Jp({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 Vp(){let e=await kn();if(!e.available)return(e.skip==="handoff-disabled"||e.skip==="server-disabled")&&d(w("SnoutData Studio is running but is not set to hand out sign-ins.")),null;let t=e.who.email??"the signed-in account";if(d(""),d(`SnoutData Studio is open here, signed in as ${x(t)}.`),d(w("It can create an access token for this terminal. Your sign-in itself is not handed over,")),d(w("and you can revoke the token later with `snoutdata tokens revoke`.")),!await Qr("Use it?"))return null;d("Asking SnoutData Studio. Approve it there.");let n=await ma(e.config,process.argv.slice(2).join(" ").slice(0,60)||"snoutdata");if("declined"in n)return d(`The app did not create a token: ${n.reason}`),null;let r={accessToken:n.token};return Ne(r),d(`Signed in with "${n.name}".`),r}async function Yp(){if(!await Qr("Sign in with a browser?"))return null;try{let e=await Dn({});return d(`Signed in${e.email?` as ${e.email}`:""}.`),{accessToken:e.accessToken,refreshToken:e.refreshToken,expiresAt:e.expiresAt}}catch(e){return d(`That did not finish: ${e instanceof Error?e.message:String(e)}`),null}}async function Xp(){if(!await Qr("Sign in by typing a code into a browser on another machine?"))return null;let e=await Tn({name:vn(),platform:process.platform});d(""),d(` Open ${x(e.verifyUrl)}`),d(` Type ${x(e.code)}`),d(""),d(w("The code is typed rather than in the link on purpose: it is what proves the person")),d(w("at the browser is the person at this terminal. Waiting\u2026"));let t=await On(e.secret,{intervalMs:Math.max(1,e.interval)*1e3,expiresAt:e.expiresAt}),n={accessToken:t.token};return Ne(n),d(`Signed in${t.name?` with "${t.name}"`:""}.`),n}async function Aa(){if(!ye())return null;let e=await Vp();if(e)return e;let t=await Yp();return t||Xp()}var Ra=C(()=>{"use strict";Ge();$n();Wr();at();Mn();te()});var Ae={};qs(Ae,{ACCOUNTS_URL:()=>Ce,APP_KEY:()=>je,ApiError:()=>ge,NotSignedIn:()=>Ye,call:()=>_,connection:()=>ke,errorSentence:()=>Na,exportStatus:()=>_t,isSettled:()=>Oa,listProjects:()=>xe,listTeams:()=>Un,localStack:()=>He,noticeFrom:()=>Nn,outdated:()=>In,requestExport:()=>Ut,unreadableAnswer:()=>Bn,waitForReady:()=>Ft,waitForSettled:()=>Bt,whoami:()=>it,withKeyNames:()=>Ln});function Zp(){let e=wt();return e?`not signed in, and ${yt(e)}. Set SNOUTDATA_ACCESS_TOKEN to a token made at https://dashboard.snoutdata.com/#/tokens, or by \`snoutdata tokens create\` on a machine that is signed in.`:"not signed in: run `snoutdata login`, or set SNOUTDATA_ACCESS_TOKEN"}async function Qp(){let e=gn();if(e||(e=await Aa()),!e)throw new Ye;if(!ea(e)||!e.refreshToken)return e.accessToken;let t=await Ie(Ce,Je("/session?via=renew","/token?grant_type=refresh_token"),{method:"POST",key:je,headers:{"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e.refreshToken})});if(!t.ok)throw new Ye;let n=await t.json(),r={...e,accessToken:n.access_token,refreshToken:n.refresh_token,expiresAt:n.expires_at};return Ne(r),r.accessToken}async function _(e,t={},n="POST"){let r=typeof t=="object"&&t?t.ref:void 0;if(typeof r=="string"){let u=He(r);if(u)throw new P("usage",`${r} is a local project (${u.folder}), and this command is about SnoutData Cloud. Locally: db url, db psql, db push, gen types, keys, status, start, stop, functions, secrets.`,{ref:r,local:u.folder})}let o=await Qp(),s=n==="GET"&&typeof t=="object"&&t?`?${new URLSearchParams(t).toString()}`:"",a=await Ie(Ce,Pn(e,s),{method:n,key:je,headers:{Authorization:`Bearer ${o}`,"Content-Type":"application/json","x-client-info":An},body:n==="GET"?void 0:JSON.stringify(t)});Nn(a);let l=await a.text(),i;try{i=l?JSON.parse(l):{}}catch{throw new ge(a.status,Bn(a.status,a.statusText,l))}if(!a.ok){let u=Na(i)??`${a.status} ${a.statusText}`;if(a.status===401)throw new Ye(u);if(a.status===410)throw In(a,u);let c=typeof i=="object"&&i&&"code"in i&&typeof i.code=="string"?i.code:null;throw new ge(a.status,u,c,i)}return i}function Nn(e){let t=e.headers.get("x-snoutdata-cli-notice");!t||Ta||(Ta=!0,Q(`${t} ${Gr({minimum:e.headers.get("x-snoutdata-cli-minimum")})}`))}function In(e,t){let n=e.headers.get("x-snoutdata-cli-minimum");return new P("outdated",`${t.replace(/\s+$/,"")} ${Gr({minimum:n})}`,{status:410,installed:ue,minimum:n??null})}async function it(){return _("cloud-whoami")}async function xe(){return _("cloud-project-list")}async function ke(e){let t=He(e);return t?sa(t):Ln(await _("cloud-project-connection",{ref:e}))}function Ln(e){let{anonKey:t,serviceRoleKey:n,...r}=e;return{...r,appKey:e.appKey??t??null,serverKey:e.serverKey??n??null}}function He(e){return aa(e,ft(process.cwd())?.local??null)}async function Un(){return _("cloud-team-list")}async function Ut(e){return _("cloud-project-export",{ref:e})}async function _t(e){return _("cloud-project-export",{ref:e},"GET")}async function Bt(e,t,n={}){let r=Date.now()+(n.timeoutMs??3e5);for(;;){let{projects:o}=await xe(),s=o.find(l=>l.ref===e)??null,a=s?.state??"deleted";if(n.onTick?.(a),Oa(t,a))return s;if(Date.now()>=r)throw new ge(504,`${e} is still ${a} after waiting`);await new Promise(l=>setTimeout(l,2e3))}}function Oa(e,t){let n={pause:"paused",resume:"ready",delete:"deleted"}[e];return t===n||t==="error"}async function Ft(e,t={}){let n=Date.now()+(t.timeoutMs??3e5);for(;;){let{projects:r}=await xe(),o=r.find(s=>s.ref===e);if(!o)throw new ge(404,`no project ${e}`);if(t.onTick?.(o.state),o.state==="ready")return o;if(Date.now()>=n)throw new ge(504,`${e} is still ${o.state} after waiting`);await new Promise(s=>setTimeout(s,2e3))}}function Na(e){if(typeof e!="object"||!e)return null;let t=e;return typeof t.error=="string"&&t.error?t.error:typeof t.message=="string"&&t.message?typeof t.hint=="string"&&t.hint?`${t.message} ${t.hint}`:t.message:null}function Bn(e,t,n){return/^\s*</.test(n)||!n.trim()?`SnoutData Cloud answered ${e}${t?` ${t}`:""} with no message. Try again in a minute.`:n.slice(0,300)}var Ce,je,ge,Ye,Ta,re=C(()=>{"use strict";Ge();Ct();me();at();Ra();te();Dt();En();Ce=process.env.SNOUTDATA_ACCOUNTS_URL??"https://accounts.snoutdata.com",je=process.env.SNOUTDATA_APP_KEY??process.env.SNOUTDATA_ANON_KEY??"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzbm91dGRhdGEiLCJyb2xlIjoic25vdXRfYXBwIiwiaWF0IjoxNzkyMDc2NTQyLCJleHAiOjIxMDQ4NzU1MjR9.hWSbcDfbGuyDHHSFLoZzD8KVSg9bWVZzzQ_eftW6TPM",ge=class extends Error{status;serverCode;body;constructor(t,n,r=null,o){super(n),this.status=t,this.serverCode=r,this.body=o}},Ye=class extends Error{constructor(t){super(t??Zp())}};Ta=!1});function es(e,t){let n=256-256%t.length;for(;;){let r=e(1)[0];if(r<n)return t[r%t.length]}}function $l(e,t,n){let r="";for(;r.length<n;)r+=es(e,t);return r}var Qo,vl,xl=C(()=>{"use strict";Qo="0123456789abcdefghjkmnpqrstvwxyz",vl="abcdefghjkmnpqrstvwxyz"});function Al(e){return es(e,Pl)+$l(e,Qo,El-1)}function ny(e){return ty.test(e)}function le(e){if(!ny(e))throw new Error(`not a project ref: ${JSON.stringify(e)}`);return e}function G(e,t=ry){return le(e),{ref:e,database:e,ownerRole:`${e}_owner`,pod:`snoutpod-${e}`,host:`${e}.${t}`}}var Pl,El,ty,ry,Re=C(()=>{"use strict";xl();Pl=vl,El=13,ty=new RegExp(`^[${Pl}][${Qo}]{${El-1}}$`);ry="db.snoutdata.com"});var Rl=C(()=>{"use strict";Re()});function Ol(e,t){let n=Tl(e,7),r=Tl(t,24);return Math.max(2,Math.ceil(n*24/r))}function Tl(e,t){return typeof e=="number"&&Number.isFinite(e)&&e>0?e:t}var ts=C(()=>{"use strict"});function ns(e){let t=e.trim();if(!/^\d+$/.test(t))return null;let n=Number(t);return Number.isSafeInteger(n)?n:null}var rs=C(()=>{"use strict"});var Nl=C(()=>{"use strict"});function jl(e){let n=(e.trim().split(`
|
|
26
|
-
|
|
27
|
-
`)){let
|
|
28
|
-
`)[0]??"").split("|");
|
|
29
|
-
D
|
|
24
|
+
</body></html>`}function qo(){let e=lr(Go(32)),t=lr(Hi("sha256").update(e).digest());return{verifier:e,challenge:t}}function Jf(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 Vf(e){let{command:t,args:n,verbatim:r}=Jf(process.platform,e),o=zf(t,n,{stdio:"ignore",detached:!0,windowsVerbatimArguments:r});o.on("error",()=>{}),o.unref()}function Wo(){return"1064562458607-9fqmghjoqjr7d56b98agkvjt9vt26pdc.apps.googleusercontent.com"}function Zf(){return"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"}function Qf(e){let t=e??lr(Go(32));return{raw:t,hashed:Hi("sha256").update(t).digest("hex")}}function eg(e){return`${Yf}?${new URLSearchParams({...e.loginHint?{login_hint:e.loginHint}:{},client_id:Wo(),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 tg(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?t!==void 0&&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 zo(e){return new Promise((t,n)=>{let r="",o=qf((a,c)=>{let i=tg(a.url??"/",e.state);if(i.kind==="ignore"){c.writeHead(404).end();return}c.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),c.end(i.kind==="ok"?Fi("ok"):Fi("failed",i.message)),o.close(),i.kind==="ok"?t({code:i.code,redirectUri:r}):n(new Error(i.message))});o.listen(0,"127.0.0.1",()=>{let a=o.address();r=`http://127.0.0.1:${typeof a=="object"&&a?a.port:0}${e.callbackPath??""}`,Promise.resolve().then(()=>e.authorizeUrl(r)).then(i=>{process.stdout.write(`${i}
|
|
25
|
+
`),e.noBrowser?d("Open that in a browser, on any machine, and sign in."):(d("Opening that in your browser. If nothing opens, paste it into one."),Vf(i))},i=>{o.close(),n(i)})}),o.on("error",n),setTimeout(()=>{o.close(),n(new Error("timed out waiting for the browser"))},e.timeoutMs??3e5).unref()})}async function Ki(e,t){let n=await We(ze,ut("/session?via=pkce","/token?grant_type=pkce"),{method:"POST",key:Je,headers:{"Content-Type":"application/json"},body:JSON.stringify({auth_code:e,code_verifier:t})});if(!n.ok)throw new Error(`could not exchange the code: ${(await n.text()).slice(0,200)}`);return Gi(await n.json())}async function ng(e,t,n){let r=await fetch(Xf,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:Wo(),client_secret:Zf(),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 rg(e){if(!Wo())throw new Error("this build of snoutdata has no Google sign-in (it was built for testing). Sign in with a released snoutdata, then run this one.");let{verifier:t,challenge:n}=qo(),{raw:r,hashed:o}=Qf(),s=lr(Go(16)),{code:a,redirectUri:c}=await zo({state:s,timeoutMs:e.timeoutMs,noBrowser:e.noBrowser,authorizeUrl:l=>eg({redirectUri:l,challenge:n,hashedNonce:o,state:s,loginHint:e.email})}),i=await ng(a,t,c),u=await We(ze,ut("/session?via=id_token","/token?grant_type=id_token"),{method:"POST",key:Je,headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:"google",id_token:i,nonce:r})});if(!u.ok)throw new Error(`could not turn the Google sign-in into a session: ${(await u.text()).slice(0,200)}`);return Gi(await u.json())}function Gi(e){let t=e,n={accessToken:t.access_token,refreshToken:t.refresh_token,expiresAt:t.expires_at,email:t.user?.email};return qe(n),n}function Bi(e){let t=e.trim().toLowerCase();if(!t||/\s/.test(t))return null;let n=t.indexOf("@");if(n===0||t.indexOf("@",n+1)!==-1)return null;let r=n===-1?t:t.slice(n+1);if(!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(r))return null;let o=r.slice(r.lastIndexOf(".")+1);return/^[a-z]{2,}$/.test(o)?r:null}function og(e){try{let t=JSON.parse(e);for(let n of["error_description","msg","message","error"]){let r=t[n];if(typeof r=="string"&&r)return r}}catch{return""}return""}function sg(e,t,n){let r=og(t);return e===404||/no sso provider/i.test(r)?new T("not-found",`No single sign-on is set up for ${n}. Check the address, or ask whoever administers your team to connect your identity provider.`,{domain:n}):/saml.*(disabled|not enabled)/i.test(r)?new T("failed",`Single sign-on is not available for ${n} yet. Ask whoever administers your team to connect your identity provider.`,{domain:n}):new T(nt(e),r||`Could not start single sign-on for ${n}.`,{domain:n})}async function ag(e){let t=await We(ze,ut("/sso/start","/sso"),{method:"POST",key:Je,headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:e.domain,redirect_to:e.redirectTo,skip_http_redirect:!0,code_challenge:e.challenge,code_challenge_method:"s256"})}),n=await t.text();if(!t.ok)throw sg(t.status,n,e.domain);let r;try{r=JSON.parse(n).url}catch{r=void 0}if(typeof r!="string"||!r)throw new T("server",`Single sign-on for ${e.domain} was accepted but came back without somewhere to send the browser.`,{domain:e.domain});return r}async function ig(e){if(e!==void 0){let r=Bi(e);if(!r)throw new g(`--domain wants a work email or a company domain, like you@yourcompany.com, not "${e}"`);return r}if(!ve()){let r=Ut();throw new g(`login --sso needs --domain <your work email or company domain>${r?`, because ${Lt(r)}`:""}`)}let t=Wf({input:process.stdin,output:process.stderr}),n=new Promise((r,o)=>{t.once("close",()=>o(new g("nothing left to read: pass --domain <your work email or company domain>")))});n.catch(()=>{});try{for(let r=0;r<3;r+=1){let o=await Promise.race([t.question("Your work email address: "),n]),s=Bi(o);if(s)return s;d("That is not a work email or a company domain. Try something like you@yourcompany.com.")}}finally{t.close()}throw new g("no domain to sign in with: pass --domain <your work email or company domain>")}async function lg(e){let t=await ig(e.domain),{verifier:n,challenge:r}=qo();d(`Signing in with the identity provider for ${O(t)}.`);let{code:o}=await zo({callbackPath:"/callback",timeoutMs:e.timeoutMs,noBrowser:e.noBrowser,authorizeUrl:s=>ag({domain:t,redirectTo:s,challenge:r})});return Ki(o,n)}async function cr(e={}){return Jo(e.email,await cg(e))}function Jo(e,t){if(!e)return t;let n={...t,expectedEmail:e};return qe(n),t.email&&!Vo(t.email,e)&&ie(`Signed in as ${t.email}, not ${e}. Run \`snoutdata login --email ${e}\` and pick that account.`),n}function Vo(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()}async function cg(e){if(e.sso)return lg({...e,domain:e.domain??e.email});let t=e.provider??"google";if(t==="google")return rg(e);let{verifier:n,challenge:r}=qo(),{code:o}=await zo({callbackPath:"/callback",timeoutMs:e.timeoutMs,noBrowser:e.noBrowser,authorizeUrl:async s=>{let a=new URL(await Ci(ze,ut("/provider/start","/authorize"),Je));return a.searchParams.set("provider",t),a.searchParams.set("redirect_to",s),a.searchParams.set("code_challenge",r),a.searchParams.set("code_challenge_method","s256"),a.searchParams.set("prompt","select_account"),a.toString()}});return Ki(o,n)}function dg(e){return e.replace(/[&<>"]/g,t=>({"&":"&","<":"<",">":">",'"':"""})[t])}var Yf,Xf,dr=D(()=>{"use strict";me();er();Ae();lt();Se();Pt();ue();Yf="https://accounts.google.com/o/oauth2/v2/auth",Xf="https://oauth2.googleapis.com/token"});import{createInterface as ug}from"node:readline/promises";async function Yo(e){let t=ug({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 pg(){let e=await Vn();if(!e.available)return(e.skip==="handoff-disabled"||e.skip==="server-disabled")&&d(b("SnoutData Studio is running but is not set to hand out sign-ins.")),null;let t=e.who.email??"the signed-in account";if(d(""),d(`SnoutData Studio is open here, signed in as ${O(t)}.`),d(b("It can create an access token for this terminal. Your sign-in itself is not handed over,")),d(b("and you can revoke the token later with `snoutdata tokens revoke`.")),!await Yo("Use it?"))return null;d("Asking SnoutData Studio. Approve it there.");let n=await Ti(e.config,process.argv.slice(2).join(" ").slice(0,60)||"snoutdata");if("declined"in n)return d(`The app did not create a token: ${n.reason}`),null;let r={accessToken:n.token};return qe(r),d(`Signed in with "${n.name}".`),r}async function mg(){if(!await Yo("Sign in with a browser?"))return null;try{let e=await cr({});return d(`Signed in${e.email?` as ${e.email}`:""}.`),{accessToken:e.accessToken,refreshToken:e.refreshToken,expiresAt:e.expiresAt}}catch(e){return d(`That did not finish: ${e instanceof Error?e.message:String(e)}`),null}}async function fg(){if(!await Yo("Sign in by typing a code into a browser on another machine?"))return null;let e=await rr({name:Yn(),platform:process.platform});d(""),d(` Open ${O(e.verifyUrl)}`),d(` Type ${O(e.code)}`),d(""),d(b("The code is typed rather than in the link on purpose: it is what proves the person")),d(b("at the browser is the person at this terminal. Waiting\u2026"));let t=await or(e.secret,{intervalMs:Math.max(1,e.interval)*1e3,expiresAt:e.expiresAt}),n={accessToken:t.token};return qe(n),d(`Signed in${t.name?` with "${t.name}"`:""}.`),n}async function qi(){if(!ve())return null;let e=await pg();if(e)return e;let t=await mg();return t||fg()}var Wi=D(()=>{"use strict";lt();Xn();Ko();Pt();dr();ue()});var Ke={};ii(Ke,{ACCOUNTS_URL:()=>ze,APP_KEY:()=>Je,ApiError:()=>$e,NotSignedIn:()=>mt,call:()=>x,connection:()=>Ce,errorSentence:()=>Yi,exportStatus:()=>Bt,isSettled:()=>Vi,listProjects:()=>je,listTeams:()=>pr,localStack:()=>st,noticeFrom:()=>sr,outdated:()=>ar,requestExport:()=>pn,serverCodeOf:()=>Ji,unreadableAnswer:()=>mr,waitForReady:()=>fn,waitForSettled:()=>mn,whoami:()=>xt,withKeyNames:()=>ur});function gg(){let e=Ut();return e?`not signed in, and ${Lt(e)}. Set SNOUTDATA_ACCESS_TOKEN to a token made at https://dashboard.snoutdata.com/#/tokens, or by \`snoutdata tokens create\` on a machine that is signed in.`:"not signed in: run `snoutdata login`, or set SNOUTDATA_ACCESS_TOKEN"}async function hg(){let e=Hn();if(e||(e=await qi()),!e)throw new mt;if(!yi(e)||!e.refreshToken)return e.accessToken;let t=await We(ze,ut("/session?via=renew","/token?grant_type=refresh_token"),{method:"POST",key:Je,headers:{"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e.refreshToken})});if(!t.ok)throw new mt;let n=await t.json(),r={...e,accessToken:n.access_token,refreshToken:n.refresh_token,expiresAt:n.expires_at};return qe(r),r.accessToken}async function x(e,t={},n="POST"){let r=typeof t=="object"&&t?t.ref:void 0;if(typeof r=="string"){let u=st(r);if(u)throw new T("usage",`${r} is a local project (${u.folder}), and this command is about SnoutData Cloud. Locally: db url, db psql, db push, gen types, keys, status, start, stop, functions, secrets.`,{ref:r,local:u.folder})}let o=await hg(),s=n==="GET"&&typeof t=="object"&&t?`?${new URLSearchParams(t).toString()}`:"",a=await We(ze,Qn(e,s),{method:n,key:Je,headers:{Authorization:`Bearer ${o}`,"Content-Type":"application/json","x-client-info":tr},body:n==="GET"?void 0:JSON.stringify(t)});sr(a);let c=await a.text(),i;try{i=c?JSON.parse(c):{}}catch{throw new $e(a.status,mr(a.status,a.statusText,c))}if(!a.ok){let u=Yi(i)??`${a.status} ${a.statusText}`;throw a.status===401?new mt(u):a.status===410?ar(a,u):new $e(a.status,u,Ji(i),i)}return i}function Ji(e){if(typeof e!="object"||e===null)return null;let{code:t,error:n}=e;return typeof t=="string"?t:typeof n=="object"&&n!==null&&typeof n.code=="string"?n.code:null}function sr(e){let t=e.headers.get("x-snoutdata-cli-notice");!t||zi||(zi=!0,ie(`${t} ${Ho({minimum:e.headers.get("x-snoutdata-cli-minimum")})}`))}function ar(e,t){let n=e.headers.get("x-snoutdata-cli-minimum");return new T("outdated",`${t.replace(/\s+$/,"")} ${Ho({minimum:n})}`,{status:410,installed:we,minimum:n??null})}async function xt(){return x("cloud-whoami")}async function je(){return x("cloud-project-list")}async function Ce(e){let t=st(e);return t?ki(t):ur(await x("cloud-project-connection",{ref:e}))}function ur(e){let{anonKey:t,serviceRoleKey:n,...r}=e;return{...r,appKey:e.appKey??t??null,serverKey:e.serverKey??n??null}}function st(e){return vi(e,Dt(process.cwd())?.local??null)}async function pr(){return x("cloud-team-list")}async function pn(e){return x("cloud-project-export",{ref:e})}async function Bt(e){return x("cloud-project-export",{ref:e},"GET")}async function mn(e,t,n={}){let r=Date.now()+(n.timeoutMs??3e5);for(;;){let{projects:o}=await je(),s=o.find(c=>c.ref===e)??null,a=s?.state??"deleted";if(n.onTick?.(a),Vi(t,a))return s;if(Date.now()>=r)throw new $e(504,`${e} is still ${a} after waiting`);await new Promise(c=>setTimeout(c,2e3))}}function Vi(e,t){let n={pause:"paused",resume:"ready",delete:"deleted"}[e];return t===n||t==="error"}async function fn(e,t={}){let n=Date.now()+(t.timeoutMs??3e5);for(;;){let{projects:r}=await je(),o=r.find(s=>s.ref===e);if(!o)throw new $e(404,`no project ${e}`);if(t.onTick?.(o.state),o.state==="ready")return o;if(Date.now()>=n)throw new $e(504,`${e} is still ${o.state} after waiting`);await new Promise(s=>setTimeout(s,2e3))}}function Yi(e){if(typeof e!="object"||!e)return null;let t=e;return typeof t.error=="string"&&t.error?t.error:typeof t.message=="string"&&t.message?typeof t.hint=="string"&&t.hint?`${t.message} ${t.hint}`:t.message:null}function mr(e,t,n){return/^\s*</.test(n)||!n.trim()?`SnoutData Cloud answered ${e}${t?` ${t}`:""} with no message. Try again in a minute.`:n.slice(0,300)}var ze,Je,$e,mt,zi,me=D(()=>{"use strict";lt();an();Se();Pt();Wi();ue();cn();er();ze=process.env.SNOUTDATA_ACCOUNTS_URL??"https://accounts.snoutdata.com",Je=process.env.SNOUTDATA_APP_KEY??process.env.SNOUTDATA_ANON_KEY??"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzbm91dGRhdGEiLCJyb2xlIjoic25vdXRfYXBwIiwiaWF0IjoxNzkyMDc2NTQyLCJleHAiOjIxMDQ4NzU1MjR9.hWSbcDfbGuyDHHSFLoZzD8KVSg9bWVZzzQ_eftW6TPM",$e=class extends Error{status;serverCode;body;constructor(t,n,r=null,o){super(n),this.status=t,this.serverCode=r,this.body=o}},mt=class extends Error{constructor(t){super(t??gg())}};zi=!1});var Hd,yb,Kd=D(()=>{"use strict";Hd="snoutpod_isolation",yb=`table bridge ${Hd}
|
|
26
|
+
delete table bridge ${Hd}
|
|
27
|
+
`});function Ys(e,t){let n=256-256%t.length;for(;;){let r=e(1)[0];if(r<n)return t[r%t.length]}}function qd(e,t,n){let r="";for(;r.length<n;)r+=Ys(e,t);return r}var Vs,Gd,Wd=D(()=>{"use strict";Vs="0123456789abcdefghjkmnpqrstvwxyz",Gd="abcdefghjkmnpqrstvwxyz"});function Vd(e){return Ys(e,zd)+qd(e,Vs,Jd-1)}function bb(e){return wb.test(e)}function ae(e){if(!bb(e))throw new Error(`not a project ref: ${JSON.stringify(e)}`);return e}function B(e,t=Sb){return ae(e),{ref:e,database:e,ownerRole:`${e}_owner`,pod:`snoutpod-${e}`,host:`${e}.${t}`}}var zd,Jd,wb,Sb,Te=D(()=>{"use strict";Wd();zd=Gd,Jd=13,wb=new RegExp(`^[${zd}][${Vs}]{${Jd-1}}$`);Sb="db.snoutdata.com"});var Yd=D(()=>{"use strict";Te()});function Zd(e,t){let n=Xd(e,7),r=Xd(t,24);return Math.max(2,Math.ceil(n*24/r))}function Xd(e,t){return typeof e=="number"&&Number.isFinite(e)&&e>0?e:t}var Xs=D(()=>{"use strict"});function Zs(e){let t=e.trim();if(!/^\d+$/.test(t))return null;let n=Number(t);return Number.isSafeInteger(n)?n:null}var Qs=D(()=>{"use strict"});var Qd=D(()=>{"use strict"});function tu(e){let n=(e.trim().split(`
|
|
28
|
+
`)[0]??"").split("|");if(n.length!==7||n[0]!=="t"&&n[0]!=="f"||n[5]!=="t"&&n[5]!=="f")return null;let r=Number(n[1]);return!Number.isFinite(r)||r<0?null:{inRecovery:n[0]==="t",checksumFailures:r,syncNames:n[2],streaming:n[3]===""?[]:n[3].split(","),lastArchivedWal:/^[0-9A-F]{24}$/.test(n[4])?n[4]:"",keeperReady:n[5]==="t",certSha256:/^[0-9a-f]{64}$/.test(n[6])?n[6]:""}}function $b(e){if(!_b.test(e))throw new Error(`not a keeper name: ${e}`);return`keeper_${e.replace(/-/g,"_")}`}function nu(e){let t=e.map(r=>`'${$b(r)}'`).join(", "),n=`from pg_replication_slots where slot_name like 'keeper\\_%'${e.length===0?"":` and slot_name not in (${t})`}`;return[`select pg_drop_replication_slot(slot_name) ${n} and not active`,`select count(*) ${n} and active`]}function ru(e){if(e!==""&&!vb.test(e))throw new Error(`not a synchronous_standby_names value: ${e}`);return[e===""?"alter system reset synchronous_standby_names":`alter system set synchronous_standby_names = '${e}'`,"select pg_reload_conf()"]}function xb(e){return typeof e=="string"&&Pb.test(e)}function Eb(e){return e===Gr||xb(e)}function ou(e){if(!Eb(e))throw new Error(`not an IPv4 address or ${Gr}: ${JSON.stringify(e)}`);return`hostssl replication ${Vt} ${e===Gr?Gr:`${e}/32`} scram-sha-256`}function su(e){if(!ta.test(e))throw new Error("not a SCRAM-SHA-256 verifier");return[`do $$ begin if not exists (select 1 from pg_roles where rolname = '${Vt}') then create role ${Vt} with login replication; end if; end $$`,`alter role ${Vt} with login replication noinherit password '${e}'`,`select pg_drop_replication_slot(slot_name) from pg_replication_slots where slot_name = '${ea}' and wal_status = 'lost' and not active`,`select pg_create_physical_replication_slot('${ea}', true) where not exists (select 1 from pg_replication_slots where slot_name = '${ea}')`]}var _b,kb,wt,ta,eu,qr,vb,Vt,ea,Pb,Gr,Wr=D(()=>{"use strict";Te();_b=/^[a-z0-9][a-z0-9-]*$/,kb=20,wt="snoutpod_keeper",ta=/^SCRAM-SHA-256\$[0-9]{4,6}:[A-Za-z0-9+/]+=*\$[A-Za-z0-9+/]+=*:[A-Za-z0-9+/]+=*$/,eu=`select pg_is_in_recovery(), coalesce((select sum(checksum_failures) from pg_stat_database), 0), current_setting('synchronous_standby_names'), coalesce((select string_agg(application_name, ',') from pg_stat_replication where state = 'streaming' and (reply_time is null or reply_time > now() - interval '${kb} seconds')), ''), coalesce((select last_archived_wal from pg_stat_archiver), ''), exists (select 1 from pg_roles where rolname = '${wt}' and rolreplication) and exists (select 1 from pg_hba_file_rules where database = '{replication}' and '${wt}' = any (user_name)) and current_setting('ssl') = 'on', case when current_setting('ssl') = 'on' then encode(sha256(pg_read_binary_file(current_setting('ssl_cert_file'))), 'hex') else '' end`;qr=/^-----BEGIN CERTIFICATE-----\n[A-Za-z0-9+/=\n]{64,12000}-----END CERTIFICATE-----\n$/,vb=/^(?:"[a-z0-9-]+"|(?:ANY|FIRST) [1-9] \("[a-z0-9-]+"(?:, "[a-z0-9-]+")+\))$/;Vt="snoutpod_replica",ea="snoutpod_replica",Pb=/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;Gr="samehost"});function au(e,t){let n=null,r=!1;for(let o of e.split(`
|
|
29
|
+
`)){let s=o.trim();s.startsWith("swap-error")?n=s.slice(10).trim()||"the swap failed":s==="swap-done"&&(r=!0)}return n||(r&&t===0?null:`the swap ended without finishing (exit ${t})`)}var Ab,na,ra,oa=D(()=>{"use strict";Ab='snoutpod-pgbackrest archive-get %f "%p"',na=String.raw`set -u
|
|
30
|
+
A=$1
|
|
31
|
+
B=$2
|
|
32
|
+
fail() { echo "swap-error $*"; exit 3; }
|
|
33
|
+
[ -d "$A" ] || fail "the home's volume has no data directory"
|
|
34
|
+
[ -f "$B/PG_VERSION" ] || fail "the replica's volume holds no cluster"
|
|
35
|
+
[ -f "$B/standby.signal" ] || fail "the replica's data is not a standby's"
|
|
36
|
+
[ ! -e "$A.swap" ] || fail "an earlier swap was left half done: $A.swap exists"
|
|
37
|
+
mv "$A" "$A.swap" || fail "the home's data could not be moved aside"
|
|
38
|
+
if ! mv "$B" "$A"; then
|
|
39
|
+
mv "$A.swap" "$A"
|
|
40
|
+
fail "the replica's data could not be moved into the home's volume"
|
|
41
|
+
fi
|
|
42
|
+
mv "$A.swap" "$B" || fail "the home's old data is at $A.swap, not in the replica's volume"
|
|
43
|
+
C="$A/postgresql.auto.conf"
|
|
44
|
+
kept=""
|
|
45
|
+
if [ -f "$C" ]; then
|
|
46
|
+
kept=$(grep -v -E '^[[:space:]]*(primary_conninfo|primary_slot_name|restore_command|synchronous_standby_names)[[:space:]]*=' "$C")
|
|
47
|
+
fi
|
|
48
|
+
{ [ -n "$kept" ] && printf '%s\n' "$kept"; printf '%s\n' "restore_command = '`+Ab.replace(/"/g,'\\"')+String.raw`'"; } > "$C" || fail "postgresql.auto.conf could not be written"
|
|
49
|
+
echo "swap-done"
|
|
50
|
+
`;ra=["select pg_promote(true, 120)","alter system reset synchronous_standby_names","alter system reset primary_conninfo","alter system reset primary_slot_name","alter system reset restore_command","select pg_reload_conf()"]});function cu(e){return`*:*:*:snoutpod_replica:${e.replace(/[\\:]/g,"\\$&")}
|
|
51
|
+
`}var iu,lu,QP,sa=D(()=>{"use strict";oa();Wr();iu="/tmp/snoutpod/replica.pgpass",lu="/tmp/snoutpod/primary.crt";QP=Buffer.from([0,0,0,8,4,210,22,47])});import{createHash as Rb,createHmac as du,pbkdf2Sync as Tb,randomBytes as nx}from"node:crypto";function uu(e,t){let n=Ob.exec(e.trim());if(!n)return!1;let[,r,o,s,a]=n,c=Number(r);if(!Number.isInteger(c)||c<1||c>1e6)return!1;let i=Tb(t.normalize("NFKC"),Buffer.from(o,"base64"),c,32,"sha256"),u=du("sha256",i).update("Client Key").digest(),l=Rb("sha256").update(u).digest(),m=du("sha256",i).update("Server Key").digest();return l.equals(Buffer.from(s,"base64"))&&m.equals(Buffer.from(a,"base64"))}var Ob,aa=D(()=>{"use strict";Ob=/^SCRAM-SHA-256\$(\d+):([^$]+)\$([^:]+):(.+)$/});function mu(e,t){let n=new Map;for(let[o,s]of t)n.set(s,o);let r=new Map;for(let o of e.split(`
|
|
52
|
+
`)){let[s,a]=o.trim().split(/\s+/),c=s?n.get(s):void 0;c&&(a==="0"||a==="1"?r.set(c,!0):a==="2"&&r.set(c,!1))}return r}var pu,ia=D(()=>{"use strict";pu='for h in "$@"; do ( pg_isready -q -h "$h" -p 5432 -U snoutpod_admin -d postgres -t 3; echo "$h $?" ) & done; wait'});function Ib(e){return/^[0-9A-F]{1,8}\/[0-9A-F]{1,8}$/i.test(e)}function gu(e){let[t,n]=(e.trim().split(`
|
|
53
|
+
`)[0]??"").split("|");return t==="t"&&n!==void 0&&Ib(n)?n:null}var Nb,sx,fu,la=D(()=>{"use strict";Nb="/var/lib/postgresql/data",sx=`set -u
|
|
54
|
+
D=${Nb}
|
|
30
55
|
W="$D/pg_wal"
|
|
31
56
|
fail() { echo "salvage-error $*"; exit 3; }
|
|
32
57
|
[ -d "$W" ] || fail "the volume has no pg_wal"
|
|
@@ -65,22 +90,24 @@ for s in $segs; do
|
|
|
65
90
|
echo "salvage-pushed $s"
|
|
66
91
|
done
|
|
67
92
|
echo "salvage-done"
|
|
68
|
-
`;
|
|
93
|
+
`;fu="select pg_is_in_recovery(), coalesce(pg_last_wal_replay_lsn()::text, '')"});var lx,hu=D(()=>{"use strict";Te();lx=7*24});var dx,yu=D(()=>{"use strict";dx=7*24*60*60*1e3});var Cb,wu=D(()=>{"use strict";yu();Cb=60*60*1e3});var bu=D(()=>{"use strict"});var ca,yx,Su=D(()=>{"use strict";ca="snoutdata_realtime",yx=`do $snoutpod$
|
|
69
94
|
begin
|
|
70
|
-
if exists (select 1 from pg_publication where pubname = '${
|
|
95
|
+
if exists (select 1 from pg_publication where pubname = '${ca}') then
|
|
71
96
|
return;
|
|
72
97
|
end if;
|
|
73
|
-
create publication ${
|
|
74
|
-
execute format('alter publication ${
|
|
98
|
+
create publication ${ca};
|
|
99
|
+
execute format('alter publication ${ca} owner to %I',
|
|
75
100
|
(select pg_get_userbyid(datdba) from pg_database where datname = current_database()));
|
|
76
101
|
end
|
|
77
102
|
$snoutpod$;
|
|
78
|
-
`});var
|
|
79
|
-
`)
|
|
80
|
-
|
|
103
|
+
`});var bx,Sx,_u=D(()=>{"use strict";bx=2*1024*1024,Sx=64*1024});var ku=D(()=>{"use strict"});function xu(e){if(e.length===0)throw new Error("a peer network needs at least one peer");let t=["network","create","--subnet",da,"--gateway",ua,"--disable-dns","--opt","no_default_route=1"];for(let n of[...new Set(e)].sort()){if(zr(An("a peer",n,pa),da))throw new Error(`the peer ${n} is on the peer network's bridge itself, which needs no route`);t.push("--route",`${n}/32,${ua}`)}return t.push(Pe),t}function Eu(e,t){let n;try{let a=JSON.parse(e);n=Array.isArray(a)?a[0]:a}catch{return!1}if(!n||n.internal===!0||n.dns_enabled===!0||!(n.subnets??[]).some(a=>a.subnet===da))return!1;let r=n.options?.no_default_route;if(r!=="1"&&r!=="true")return!1;let o=(n.routes??[]).map(a=>`${a.destination??""},${a.gateway??""}`).sort(),s=[...new Set(t)].sort().map(a=>`${a}/32,${ua}`);return o.length===s.length&&o.every((a,c)=>a===s[c])}function Au(e,t){let n;try{let o=JSON.parse(e);n=(Array.isArray(o)?o[0]:o)?.routes}catch{return[]}let r=new Set((Array.isArray(n)?n:[]).map(o=>String(o.destination??"")));return t.filter(o=>r.has(`${o}/32`))}function An(e,t,n){if(!n.test(t))throw new Error(`${e} is not a value this can route: ${JSON.stringify(t)}`);return t}function Jr(e){let t=["network","create"],n=e.allow,r=e.deny;if(n!=null&&r!=null)throw new Error("a network is policed by an allow-list or by a deny-list, not by both");if((e.peers?.length??0)>0&&r==null)throw new Error("a peer is an exception to a deny-list, so it needs one");if(r!=null)return Db(e,r);if(n==null){if(e.subnet||e.gateway)throw new Error("a subnet or gateway was given with no egress policy, which does nothing");return t.push(e.name),t}if(n.length>0)throw new Error("an egress allow-list is not expressible on this runtime: --internal blocks everything or nothing, and a route-based block does not block (measured on podman 5.8.6 / netavark 1.17.2)");if(e.subnet||e.gateway)throw new Error("a subnet or gateway was given with no allowance to route through it");return t.push("--internal",e.name),t}function Db(e,t){if(t.length===0)throw new Error("an empty deny-list is not a policy: leave it absent to create an ordinary network");let n=e.subnet,r=e.sink;if(!n||!r)throw new Error("a deny-list needs a pinned subnet and a sink address on it");if(An("the subnet",n,vu),An("the sink",r,pa),!zr(r,n))throw new Error(`the sink ${r} is not inside ${n}, so denied traffic would be routed at the host`);if(e.gateway&&!zr(e.gateway,n))throw new Error(`the gateway ${e.gateway} is not inside ${n}`);if(e.gateway&&e.gateway===r)throw new Error("the sink is the gateway, which routes denied traffic at the host rather than nowhere");let o=["network","create","--subnet",n];e.gateway&&o.push("--gateway",e.gateway);for(let s of t)o.push("--route",`${An("a denied prefix",s,vu)},${r}`);for(let s of e.peers??[]){if(!e.gateway)throw new Error("a peer is reached through the gateway, so it needs a pinned gateway");if(zr(An("a peer",s,pa),n))throw new Error(`the peer ${s} is on the bridge itself, which needs no route`);o.push("--route",`${s}/32,${e.gateway}`)}return o.push(e.name),o}function $u(e){return e.split(".").reduce((t,n)=>t*256+Number(n),0)}function zr(e,t){let[n,r]=t.split("/"),o=Number(r),s=o===0?0:4294967295<<32-o>>>0;return($u(e)&s)>>>0===($u(n??"")&s)>>>0}function Ru(e){return e?.internal===!0}var Pu,Pe,da,ua,vu,pa,Vr=D(()=>{"use strict";Pu=["169.254.0.0/16","10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"],Pe="snoutpeer",da="10.96.0.0/24",ua="10.96.0.1";vu=/^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\/(3[0-2]|[12]?[0-9])$/,pa=/^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$/});var Mb,Yr=D(()=>{"use strict";Mb=7*24*60*60});function Fb(e,t){let[n,r]=e.split("/");if(r!=="24"||!n?.endsWith(".0")||!t.startsWith(n.slice(0,-1)))throw new Error(`${t} is not an address of the /24 ${e}`);let o=n.slice(0,-1),s=Number(t.slice(o.length)),a=[];for(let c=32;c>=25;c--){let i=2**(32-c);a.push(`${o}${(s^i)&~(i-1)&255}/${c}`)}return a}var Lb,Ub,jx,Tu=D(()=>{"use strict";Te();_u();ku();Vr();Yr();Lb="10.93.0.0/24",Ub="10.93.0.1",jx=[...Pu,"100.64.0.0/10",...Fb(Lb,Ub)]});var Ou=D(()=>{"use strict"});import{createHash as Bb}from"node:crypto";function St(e){return`${B(e).pod}-${ge}`}function Ze(e){return e.layout==="separated"}function fa(e){return Ze(e)?[]:e.services??[]}function ga(e){if(typeof e!="number"||!Number.isInteger(e)||e<1)throw new Error(`a separated router needs its own pod port (routerPodPort), and has ${JSON.stringify(e??null)}`);let t={postgres:e+Nu*1e3,admin:e+Nu*1e3+eo,passthrough:e+Hb*1e3};if(t.passthrough>65535)throw new Error(`routerPodPort ${e} puts the router's ports past 65535`);return t}function Iu(e,t){let n={...t};return delete n.LEPIS_HOMES,n.LEPIS_HOME=`${B(ae(e)).pod}:5432`,n.LEPIS_PASSTHROUGH_PORT=String(Yt),n}function Cu(e,t){let n=ae(e);return t?["\\getenv router_password SNOUTPOD_ROUTER_PASSWORD",`do $$ begin if not exists (select from pg_roles where rolname = '${Ee}') then create role ${Ee}; end if; end $$;`,`alter role ${Ee} with login noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls connection limit 64 password :'router_password';`,`create schema if not exists ${Ee} authorization snoutpod_admin;`,`create or replace function ${Ee}.verifier(role_name text) returns text`," language sql stable security definer set search_path = pg_catalog","as $f$"," select a.rolpassword from pg_catalog.pg_authid a"," where a.rolname = role_name and a.rolcanlogin and not a.rolsuper and not a.rolreplication"," and (a.rolvaliduntil is null or a.rolvaliduntil > now())"," and a.rolname not like 'snoutpod\\_%' and a.rolname <> 'postgres'","$f$;",`revoke all on schema ${Ee} from public;`,`revoke all on function ${Ee}.verifier(text) from public;`,`grant usage on schema ${Ee} to ${Ee};`,`grant execute on function ${Ee}.verifier(text) to ${Ee};`,`grant connect on database "${n}" to ${Ee};`,""].join(`
|
|
104
|
+
`):[`do $$ begin if exists (select from pg_roles where rolname = '${Ee}') then alter role ${Ee} nologin; end if; end $$;`,""].join(`
|
|
105
|
+
`)}function Rn(e){return`${B(e).pod}-lepis-tls`}function Gb(e){return Bb("sha256").update(`snout-lepis-admin:${e}`).digest("hex")}function ha(e,t){let n={...e},r=n[Xr],o=n[Zr];delete n[Xr],delete n[Zr];for(let[s,a,c]of[[Xr,r,t],[Zr,o,Gb(t)]])if(a!==void 0){if(!Kb.test(a))throw new Error(`${s} names ${JSON.stringify(a)}, which is not a variable name`);n[a]=c}return n}var ge,Xr,Zr,Qr,eo,ma,Nu,Gx,Yt,Hb,bt,Ee,to,Du,Kb,no=D(()=>{"use strict";Te();Ou();ge="lepis",Xr="SNOUTPOD_ADMIN_PASSWORD_ENV",Zr="SNOUTPOD_ADMIN_TOKEN_ENV",Qr=7433,eo=1e3,ma=7432,Nu=4,Gx={LEPIS_HOST:"0.0.0.0",LEPIS_PORT:String(ma),LEPIS_HOME:"127.0.0.1:5432",LEPIS_HOME_SSLMODE:"disable",LEPIS_SERVICE_USER:"snoutpod_admin",[Xr]:"LEPIS_SERVICE_PASSWORD",LEPIS_ADMIN_ADDR:`0.0.0.0:${Qr}`,LEPIS_TLS_CERT:"/tls/cert.pem",LEPIS_TLS_KEY:"/tls/key.pem",[Zr]:"LEPIS_ADMIN_TOKEN",LEPIS_LOG:"info"},Yt=7434,Hb=7,bt="snoutpod.lepis";Ee="snoutpod_router";to="/tls",Du=["set -e","if [ ! -s /tls/cert.pem ] || [ ! -s /tls/key.pem ]; then"," openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 3650 -subj /CN=lepis -addext subjectAltName=DNS:lepis,IP:127.0.0.1 -keyout /tls/key.pem -out /tls/cert.pem 2>/dev/null","fi","chmod 755 /tls","chmod 644 /tls/cert.pem /tls/key.pem"].join(`
|
|
106
|
+
`),Kb=/^[A-Z][A-Z0-9_]{0,63}$/});function ya(e){return Mu.includes(e)}async function wa(e,t){if(!ya(t))throw new Error(`no password is derived for role ${JSON.stringify(t)}: only ${Mu.join(", ")}`);if(e==="")throw new Error("no admin password to derive a role password from");let n=new TextEncoder,r=await crypto.subtle.importKey("raw",n.encode(e),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),o=new Uint8Array(await crypto.subtle.sign("HMAC",r,n.encode(`role:${t}`))),s="";for(let a of o)s+=String.fromCharCode(a);return btoa(s).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}var Mu,ba=D(()=>{"use strict";Mu=["snout_gate","snout_auth_admin","snout_storage_admin","snout_realtime_admin","snout_psyche_admin","snout_push_admin","snoutpod_router"]});var Xx,Zx,ju=D(()=>{"use strict";Te();Yr();Xx=6*60*60,Zx=5*60});var Lu=D(()=>{"use strict"});var Uu=D(()=>{"use strict"});var n0,Fu=D(()=>{"use strict";n0=15*6e4});var i0,Bu=D(()=>{"use strict";Fu();i0=2*1024**3});var Hu=D(()=>{"use strict";Uu();Bu()});function Gu(e){if(!Array.isArray(e))throw new Error("the database access list is not a list");let t=new Set;return e.map(n=>{let r=n,o=r?.role,s=r?.level;if(typeof o!="string"||!Wb.test(o))throw new Error(`not a database access role: ${JSON.stringify(o)}`);if(typeof s!="string"||!qb.includes(s))throw new Error(`not a database access level: ${JSON.stringify(s)}`);if(t.has(o))throw new Error(`${o} is on the database access list twice`);return t.add(o),{role:o,level:s}})}function qu(e,t){return`${e}.nspname !~ '^pg_'
|
|
107
|
+
and ${e}.nspname <> all (array[${Ku.map(n=>`'${n}'`).join(", ")}]::text[])
|
|
81
108
|
and not ${t}.rolsuper
|
|
82
|
-
and ${t}.rolname !~ '${
|
|
83
|
-
`)){let r=/^(created-group|created|applied|dropped-group|dropped|refused|ignored) (\S+)$/.exec(n.trim());r&&t.push({action:r[1],role:r[2]})}return t}function
|
|
109
|
+
and ${t}.rolname !~ '${zb.source}'`}function zu(e){let t=[];for(let n of e.split(`
|
|
110
|
+
`)){let r=/^(created-group|created|applied|dropped-group|dropped|refused|ignored) (\S+)$/.exec(n.trim());r&&t.push({action:r[1],role:r[2]})}return t}function Ju(e,t){return ae(e),JSON.stringify(Gu(t).map(n=>({role:n.role,level:n.level})))}var qb,Xt,Wb,re,Ku,zb,oo,Jb,ro,Vb,Wu,Sa=D(()=>{"use strict";Te();qb=["full","read"],Xt="snout_oauth",Wb=/^oauth_[0-9a-f]{32}$/,re="snout_oauth_read",Ku=["information_schema","auth","storage","realtime","snout_realtime","_realtime","push","graphql","graphql_public","extensions","snout_identity","snout_files","snout_live","snout_graph","snout_ext","cron","net","vault","pgsodium","pgbouncer","snouttime","snouttime_internal"],zb=/^(snout_[a-z]+_admin|snout_gate|authenticator|pgbouncer)$/;oo="SNOUTPOD_DB_ACCESS",Jb=/^[a-z_][a-z0-9_]*$/;for(let e of Ku)if(!Jb.test(e))throw new Error(`not a schema name: ${JSON.stringify(e)}`);ro="snoutpod_oauth_read_new_schema",Vb=`do $home$
|
|
84
111
|
declare
|
|
85
112
|
-- snout_ext in the own layout, extensions in a database not yet renamed (docs/cloud/OWN-LAYOUT.md).
|
|
86
113
|
home text := case
|
|
@@ -89,7 +116,7 @@ declare
|
|
|
89
116
|
end;
|
|
90
117
|
begin
|
|
91
118
|
execute pg_catalog.format('create schema if not exists %I', home);
|
|
92
|
-
execute pg_catalog.replace($fn$create or replace function @home@.${
|
|
119
|
+
execute pg_catalog.replace($fn$create or replace function @home@.${ro}() returns event_trigger
|
|
93
120
|
language plpgsql
|
|
94
121
|
security invoker
|
|
95
122
|
set search_path = pg_catalog, pg_temp
|
|
@@ -98,7 +125,7 @@ declare
|
|
|
98
125
|
created record;
|
|
99
126
|
owner_role text := pg_catalog.current_database() || '_owner';
|
|
100
127
|
begin
|
|
101
|
-
if not exists (select from pg_catalog.pg_roles where rolname = '${
|
|
128
|
+
if not exists (select from pg_catalog.pg_roles where rolname = '${re}') then
|
|
102
129
|
return;
|
|
103
130
|
end if;
|
|
104
131
|
for created in
|
|
@@ -109,29 +136,29 @@ begin
|
|
|
109
136
|
where c.command_tag = 'CREATE SCHEMA'
|
|
110
137
|
and c.classid = 'pg_catalog.pg_namespace'::pg_catalog.regclass
|
|
111
138
|
and not c.in_extension
|
|
112
|
-
and ${
|
|
139
|
+
and ${qu("n","r")}
|
|
113
140
|
loop
|
|
114
141
|
begin
|
|
115
|
-
execute pg_catalog.format('grant usage on schema %I to ${
|
|
116
|
-
execute pg_catalog.format('grant select on all tables in schema %I to ${
|
|
117
|
-
execute pg_catalog.format('grant select on all sequences in schema %I to ${
|
|
118
|
-
execute pg_catalog.format('alter default privileges for role %I in schema %I grant select on tables to ${
|
|
119
|
-
execute pg_catalog.format('alter default privileges for role %I in schema %I grant select on sequences to ${
|
|
142
|
+
execute pg_catalog.format('grant usage on schema %I to ${re}', created.nspname);
|
|
143
|
+
execute pg_catalog.format('grant select on all tables in schema %I to ${re}', created.nspname);
|
|
144
|
+
execute pg_catalog.format('grant select on all sequences in schema %I to ${re}', created.nspname);
|
|
145
|
+
execute pg_catalog.format('alter default privileges for role %I in schema %I grant select on tables to ${re}', owner_role, created.nspname);
|
|
146
|
+
execute pg_catalog.format('alter default privileges for role %I in schema %I grant select on sequences to ${re}', owner_role, created.nspname);
|
|
120
147
|
exception when others then
|
|
121
148
|
raise warning 'snoutpod: read access reaches schema % at the next apply, not now: %', created.nspname, sqlerrm;
|
|
122
149
|
end;
|
|
123
150
|
end loop;
|
|
124
151
|
end
|
|
125
152
|
$snoutpod$$fn$, '@home@', pg_catalog.quote_ident(home));
|
|
126
|
-
if not exists (select from pg_catalog.pg_event_trigger where evtname = '${
|
|
127
|
-
execute pg_catalog.format('create event trigger ${
|
|
153
|
+
if not exists (select from pg_catalog.pg_event_trigger where evtname = '${ro}') then
|
|
154
|
+
execute pg_catalog.format('create event trigger ${ro} on ddl_command_end
|
|
128
155
|
when tag in (''CREATE SCHEMA'')
|
|
129
|
-
execute function %I.${
|
|
156
|
+
execute function %I.${ro}()', home);
|
|
130
157
|
end if;
|
|
131
158
|
end
|
|
132
159
|
$home$;
|
|
133
|
-
`,
|
|
134
|
-
${
|
|
160
|
+
`,Wu=`\\getenv db_access ${oo}
|
|
161
|
+
${Vb}create function pg_temp.snoutpod_member(member_name text, group_name text)
|
|
135
162
|
returns boolean
|
|
136
163
|
language sql
|
|
137
164
|
stable
|
|
@@ -152,7 +179,7 @@ create function pg_temp.snoutpod_customer_schema(schema_oid oid)
|
|
|
152
179
|
set search_path = pg_catalog, pg_temp
|
|
153
180
|
as $snoutpod$
|
|
154
181
|
-- isCustomerSchema in dbAccess.ts, from the same SYSTEM_SCHEMAS and PLATFORM_OWNER.
|
|
155
|
-
select ${
|
|
182
|
+
select ${qu("n","r")}
|
|
156
183
|
from pg_namespace n
|
|
157
184
|
join pg_roles r on r.oid = n.nspowner
|
|
158
185
|
where n.oid = schema_oid
|
|
@@ -187,9 +214,9 @@ begin
|
|
|
187
214
|
end if;
|
|
188
215
|
any_read := exists (select from jsonb_array_elements(wanted) w where w->>'level' = 'read');
|
|
189
216
|
|
|
190
|
-
if not exists (select from pg_roles where rolname = '${
|
|
191
|
-
create role ${
|
|
192
|
-
change := 'created-group'; role_name := '${
|
|
217
|
+
if not exists (select from pg_roles where rolname = '${Xt}') then
|
|
218
|
+
create role ${Xt} nologin;
|
|
219
|
+
change := 'created-group'; role_name := '${Xt}'; return next;
|
|
193
220
|
end if;
|
|
194
221
|
|
|
195
222
|
-- Taken away: every member of the group the list does not name.
|
|
@@ -198,7 +225,7 @@ begin
|
|
|
198
225
|
from pg_auth_members am
|
|
199
226
|
join pg_roles g on g.oid = am.roleid
|
|
200
227
|
join pg_roles m on m.oid = am.member
|
|
201
|
-
where g.rolname = '${
|
|
228
|
+
where g.rolname = '${Xt}'
|
|
202
229
|
order by 1
|
|
203
230
|
loop
|
|
204
231
|
continue when exists (select from jsonb_array_elements(wanted) w where w->>'role' = found_role);
|
|
@@ -217,21 +244,21 @@ begin
|
|
|
217
244
|
|
|
218
245
|
-- What a read person may read: the customer's schemas, and nothing of ours (dbAccess.ts).
|
|
219
246
|
if any_read then
|
|
220
|
-
if not exists (select from pg_roles where rolname = '${
|
|
221
|
-
create role ${
|
|
222
|
-
change := 'created-group'; role_name := '${
|
|
247
|
+
if not exists (select from pg_roles where rolname = '${re}') then
|
|
248
|
+
create role ${re} nologin;
|
|
249
|
+
change := 'created-group'; role_name := '${re}'; return next;
|
|
223
250
|
end if;
|
|
224
251
|
-- Stated whole, as a person's role is, and a member of nothing: whatever it held would reach
|
|
225
252
|
-- every read person.
|
|
226
|
-
alter role ${
|
|
253
|
+
alter role ${re} with nologin nosuperuser nocreatedb nocreaterole noreplication nobypassrls password null;
|
|
227
254
|
for target in
|
|
228
255
|
select g.rolname as of_role, pg_get_userbyid(am.grantor) as grantor
|
|
229
256
|
from pg_auth_members am
|
|
230
257
|
join pg_roles g on g.oid = am.roleid
|
|
231
258
|
join pg_roles m on m.oid = am.member
|
|
232
|
-
where m.rolname = '${
|
|
259
|
+
where m.rolname = '${re}'
|
|
233
260
|
loop
|
|
234
|
-
execute pg_catalog.format('revoke %I from ${
|
|
261
|
+
execute pg_catalog.format('revoke %I from ${re} granted by %I', target.of_role, target.grantor);
|
|
235
262
|
end loop;
|
|
236
263
|
for target in
|
|
237
264
|
select n.nspname, pg_temp.snoutpod_customer_schema(n.oid) as customer
|
|
@@ -240,16 +267,16 @@ begin
|
|
|
240
267
|
order by 1
|
|
241
268
|
loop
|
|
242
269
|
if target.customer then
|
|
243
|
-
execute pg_catalog.format('grant usage on schema %I to ${
|
|
244
|
-
execute pg_catalog.format('grant select on all tables in schema %I to ${
|
|
245
|
-
execute pg_catalog.format('grant select on all sequences in schema %I to ${
|
|
246
|
-
execute pg_catalog.format('alter default privileges for role %I in schema %I grant select on tables to ${
|
|
247
|
-
execute pg_catalog.format('alter default privileges for role %I in schema %I grant select on sequences to ${
|
|
270
|
+
execute pg_catalog.format('grant usage on schema %I to ${re}', target.nspname);
|
|
271
|
+
execute pg_catalog.format('grant select on all tables in schema %I to ${re}', target.nspname);
|
|
272
|
+
execute pg_catalog.format('grant select on all sequences in schema %I to ${re}', target.nspname);
|
|
273
|
+
execute pg_catalog.format('alter default privileges for role %I in schema %I grant select on tables to ${re}', owner_role, target.nspname);
|
|
274
|
+
execute pg_catalog.format('alter default privileges for role %I in schema %I grant select on sequences to ${re}', owner_role, target.nspname);
|
|
248
275
|
else
|
|
249
276
|
-- Ours. Nothing was ever granted here unless the list or an owner changed since.
|
|
250
|
-
execute pg_catalog.format('revoke all on schema %I from ${
|
|
251
|
-
execute pg_catalog.format('revoke all on all tables in schema %I from ${
|
|
252
|
-
execute pg_catalog.format('revoke all on all sequences in schema %I from ${
|
|
277
|
+
execute pg_catalog.format('revoke all on schema %I from ${re}', target.nspname);
|
|
278
|
+
execute pg_catalog.format('revoke all on all tables in schema %I from ${re}', target.nspname);
|
|
279
|
+
execute pg_catalog.format('revoke all on all sequences in schema %I from ${re}', target.nspname);
|
|
253
280
|
end if;
|
|
254
281
|
end loop;
|
|
255
282
|
for target in
|
|
@@ -258,10 +285,10 @@ begin
|
|
|
258
285
|
from pg_default_acl d
|
|
259
286
|
join pg_namespace n on n.oid = d.defaclnamespace
|
|
260
287
|
where not pg_temp.snoutpod_customer_schema(n.oid)
|
|
261
|
-
and exists (select from aclexplode(d.defaclacl) a where a.grantee = '${
|
|
288
|
+
and exists (select from aclexplode(d.defaclacl) a where a.grantee = '${re}'::regrole)
|
|
262
289
|
loop
|
|
263
290
|
continue when target.kind is null;
|
|
264
|
-
execute pg_catalog.format('alter default privileges for role %I in schema %I revoke all on %s from ${
|
|
291
|
+
execute pg_catalog.format('alter default privileges for role %I in schema %I revoke all on %s from ${re}', target.for_role, target.nspname, target.kind);
|
|
265
292
|
end loop;
|
|
266
293
|
end if;
|
|
267
294
|
|
|
@@ -270,7 +297,7 @@ begin
|
|
|
270
297
|
wanted_role := entry->>'role';
|
|
271
298
|
wanted_level := entry->>'level';
|
|
272
299
|
if exists (select from pg_roles where rolname = wanted_role) then
|
|
273
|
-
if not pg_temp.snoutpod_member(wanted_role, '${
|
|
300
|
+
if not pg_temp.snoutpod_member(wanted_role, '${Xt}') then
|
|
274
301
|
-- The name is right and the role is not ours: made by somebody else, who holds
|
|
275
302
|
-- ADMIN on it. Never adopted (see dbAccess.ts).
|
|
276
303
|
change := 'refused'; role_name := wanted_role; return next;
|
|
@@ -278,7 +305,7 @@ begin
|
|
|
278
305
|
end if;
|
|
279
306
|
change := 'applied';
|
|
280
307
|
else
|
|
281
|
-
execute pg_catalog.format('create role %I login in role ${
|
|
308
|
+
execute pg_catalog.format('create role %I login in role ${Xt}', wanted_role);
|
|
282
309
|
change := 'created';
|
|
283
310
|
end if;
|
|
284
311
|
-- No password, ever: the pg_hba line for the group is oauth, and a password could not be
|
|
@@ -291,8 +318,8 @@ begin
|
|
|
291
318
|
execute pg_catalog.format('revoke pg_read_all_data from %I', wanted_role);
|
|
292
319
|
end if;
|
|
293
320
|
if wanted_level = 'full' then
|
|
294
|
-
if pg_temp.snoutpod_member(wanted_role, '${
|
|
295
|
-
execute pg_catalog.format('revoke ${
|
|
321
|
+
if pg_temp.snoutpod_member(wanted_role, '${re}') then
|
|
322
|
+
execute pg_catalog.format('revoke ${re} from %I', wanted_role);
|
|
296
323
|
end if;
|
|
297
324
|
execute pg_catalog.format('alter role %I nobypassrls', wanted_role);
|
|
298
325
|
if not pg_temp.snoutpod_member(wanted_role, owner_role) then
|
|
@@ -307,8 +334,8 @@ begin
|
|
|
307
334
|
execute pg_catalog.format('revoke %I from %I', owner_role, wanted_role);
|
|
308
335
|
perform pg_terminate_backend(pid) from pg_stat_activity where usename = wanted_role;
|
|
309
336
|
end if;
|
|
310
|
-
if not pg_temp.snoutpod_member(wanted_role, '${
|
|
311
|
-
execute pg_catalog.format('grant ${
|
|
337
|
+
if not pg_temp.snoutpod_member(wanted_role, '${re}') then
|
|
338
|
+
execute pg_catalog.format('grant ${re} to %I with inherit true, set false', wanted_role);
|
|
312
339
|
end if;
|
|
313
340
|
execute pg_catalog.format('alter role %I bypassrls', wanted_role);
|
|
314
341
|
execute pg_catalog.format('alter role %I in database %I reset role', wanted_role, db);
|
|
@@ -319,30 +346,30 @@ begin
|
|
|
319
346
|
|
|
320
347
|
-- Nobody reads any more: the group goes, and with it every grant and default it held, so a
|
|
321
348
|
-- project that stops using the level carries nothing of it.
|
|
322
|
-
if not any_read and exists (select from pg_roles where rolname = '${
|
|
349
|
+
if not any_read and exists (select from pg_roles where rolname = '${re}') then
|
|
323
350
|
begin
|
|
324
351
|
for target in
|
|
325
352
|
select m.rolname as member_role, pg_get_userbyid(am.grantor) as grantor
|
|
326
353
|
from pg_auth_members am
|
|
327
354
|
join pg_roles g on g.oid = am.roleid
|
|
328
355
|
join pg_roles m on m.oid = am.member
|
|
329
|
-
where g.rolname = '${
|
|
356
|
+
where g.rolname = '${re}'
|
|
330
357
|
loop
|
|
331
|
-
execute pg_catalog.format('revoke ${
|
|
358
|
+
execute pg_catalog.format('revoke ${re} from %I granted by %I', target.member_role, target.grantor);
|
|
332
359
|
end loop;
|
|
333
|
-
drop owned by ${
|
|
334
|
-
drop role ${
|
|
335
|
-
change := 'dropped-group'; role_name := '${
|
|
360
|
+
drop owned by ${re};
|
|
361
|
+
drop role ${re};
|
|
362
|
+
change := 'dropped-group'; role_name := '${re}'; return next;
|
|
336
363
|
exception when others then
|
|
337
364
|
-- Holding something in another database, which this never grants. Kept, and said.
|
|
338
|
-
raise warning 'snoutpod: could not drop ${
|
|
365
|
+
raise warning 'snoutpod: could not drop ${re}: %', sqlerrm;
|
|
339
366
|
end;
|
|
340
367
|
end if;
|
|
341
368
|
end
|
|
342
369
|
$snoutpod$;
|
|
343
370
|
select change || ' ' || role_name from pg_temp.snoutpod_db_access(:'db_access'::jsonb);
|
|
344
|
-
`});var
|
|
345
|
-
`).slice(-3).join("; ");throw new Error(`${t} ${n[0]??""} failed (${o.code}): ${s}`)}return o.stdout}var
|
|
371
|
+
`});var Vu,SE,_E,kE,vE,$E,Yu=D(()=>{"use strict";Kd();Yd();Xs();Qs();Qd();Wr();sa();aa();ia();la();hu();wu();bu();Su();Tu();no();ba();ju();Lu();Hu();Yr();Sa();Vu=120,SE=2*6e4,_E=15*60*1e3,kE=60*60*1e3,vE=5*60*1e3,$E=10*60*1e3});import{execFile as Yb}from"node:child_process";function Xu(){return(e,t,n={})=>new Promise((r,o)=>{let s=Yb(e,[...t],{timeout:n.timeoutMs??0,maxBuffer:8388608,env:n.env?{...process.env,...n.env}:process.env},(a,c,i)=>{if(a&&typeof a.code=="string"){o(a);return}let u=a&&typeof a.code=="number"?a.code:0;r({code:u,stdout:String(c),stderr:String(i)})});n.stdin!==void 0&&s.stdin?.end(n.stdin)})}async function N(e,t,n,r){let o=await e(t,n,r);if(o.code!==0){let s=(o.stderr||o.stdout).trim().split(`
|
|
372
|
+
`).slice(-3).join("; ");throw new Error(`${t} ${n[0]??""} failed (${o.code}): ${s}`)}return o.stdout}var Tn=D(()=>{"use strict"});import{readdir as Xb,readFile as RE}from"node:fs/promises";async function Zu(e,t,n={}){let r=n.binary??"podman",o=B(t).pod,s=await e(r,["volume","inspect",o,"--format","json"]);if(s.code!==0)return!1;let a=JSON.parse(s.stdout||"[]"),i=(Array.isArray(a)?a[0]:a)?.Mountpoint;return i?await(n.look??Zb)(i)!=="empty":!1}async function Zb(e){try{return(await Xb(e)).length>0?"data":"empty"}catch(t){return t.code==="ENOENT"?"empty":"hidden"}}var Qu=D(()=>{"use strict";Te();Tn()});function ep(e,t,n){if(!n.test(t))throw new Error(`${e} is not a value this can pass to a pod: ${JSON.stringify(t)}`);return t}async function rp(e,t){if(!t)return e??null;let n=await t();return{...e??{},key:n.accessKeyId,secret:n.secretAccessKey,token:n.sessionToken}}function op(e){let t=!!(e?.key&&e?.secret),n=["--env",`SNOUTPOD_S3_KEY_TYPE=${t?"shared":"auto"}`];return e&&(t&&(n.push("--env",`SNOUTPOD_S3_KEY=${ep("the access key",e.key,eS)}`),n.push("--env",tp),e.token&&n.push("--env",np)),e.endpoint&&n.push("--env",`SNOUTPOD_S3_ENDPOINT=${ep("the endpoint",e.endpoint,Qb)}`),e.uriStyle&&n.push("--env",`SNOUTPOD_S3_URI_STYLE=${e.uriStyle==="path"?"path":"host"}`),e.verifyTls===!1&&n.push("--env","SNOUTPOD_S3_VERIFY_TLS=n")),n}function sp(e){if(!e?.key||!e?.secret)return{};if(/[\r\n]/.test(e.secret))throw new Error("the repository secret contains a newline, which would write a second setting");if(e.token&&/[\r\n]/.test(e.token))throw new Error("the session token contains a newline, which would write a second setting");return{[tp]:e.secret,...e.token?{[np]:e.token}:{}}}var tp,np,Qb,eS,ap=D(()=>{"use strict";tp="SNOUTPOD_S3_SECRET",np="SNOUTPOD_S3_TOKEN",Qb=/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:[0-9]{1,5})?$/i,eS=/^[A-Za-z0-9/+=_.-]{1,256}$/});function cp(e){if(!ka.includes(e))throw new Error(`not a role kept out of the query statistics: ${e}`);return`alter role ${e} set pg_stat_statements.track = 'none';`}var _a,ip,lp,ka,dp=D(()=>{"use strict";_a=["snout_gate","authenticator"],ip=`do $snoutpod$
|
|
346
373
|
declare
|
|
347
374
|
wanted text[];
|
|
348
375
|
begin
|
|
@@ -360,8 +387,19 @@ begin
|
|
|
360
387
|
end loop;
|
|
361
388
|
end
|
|
362
389
|
$snoutpod$;
|
|
363
|
-
`,
|
|
364
|
-
|
|
390
|
+
`,lp=`do $snoutpod$
|
|
391
|
+
begin
|
|
392
|
+
if not exists (
|
|
393
|
+
select from pg_catalog.pg_stat_sys_tables
|
|
394
|
+
where relid = 'pg_catalog.pg_proc'::pg_catalog.regclass
|
|
395
|
+
and (last_analyze is not null or last_autoanalyze is not null)
|
|
396
|
+
) then
|
|
397
|
+
analyze pg_catalog.pg_proc, pg_catalog.pg_type, pg_catalog.pg_depend;
|
|
398
|
+
end if;
|
|
399
|
+
end
|
|
400
|
+
$snoutpod$;
|
|
401
|
+
`,ka=["snout_psyche_admin"]});function pp(e){let t=[];for(let n of e.split(`
|
|
402
|
+
`)){let r=n.split($a);if(r.length!==4||!va.includes(r[0]))continue;let[o,s,a,c]=r;t.push(c?{name:o,from:s,error:c}:{name:o,from:s,to:a})}return t}var va,$a,up,mp=D(()=>{"use strict";va=["snouttime"];for(let e of va)if(!/^[a-z][a-z0-9_]{0,62}$/.test(e))throw new Error(`not an extension name: ${JSON.stringify(e)}`);$a="",up=`
|
|
365
403
|
SET search_path = pg_catalog, pg_temp;
|
|
366
404
|
CREATE TEMP TABLE snoutpod_extension_updates (name text, from_version text, to_version text, error text);
|
|
367
405
|
DO $snoutpod$
|
|
@@ -375,7 +413,7 @@ BEGIN
|
|
|
375
413
|
SELECT e.extname, e.extversion
|
|
376
414
|
FROM pg_extension e
|
|
377
415
|
JOIN pg_available_extensions a ON a.name = e.extname
|
|
378
|
-
WHERE e.extname = ANY (ARRAY[${
|
|
416
|
+
WHERE e.extname = ANY (ARRAY[${va.map(e=>`'${e}'`).join(", ")}])
|
|
379
417
|
AND a.default_version IS DISTINCT FROM e.extversion
|
|
380
418
|
LOOP
|
|
381
419
|
BEGIN
|
|
@@ -406,132 +444,119 @@ BEGIN
|
|
|
406
444
|
END
|
|
407
445
|
$snoutpod$;
|
|
408
446
|
SELECT name, from_version, coalesce(to_version, ''), coalesce(error, '') FROM snoutpod_extension_updates ORDER BY name;
|
|
409
|
-
`});function Fd(e){if(typeof e!="string"||!/^https?:\/\//.test(e)||e.length>255)throw new Error(`not an OAuth issuer a pod can be given: ${JSON.stringify(e)}`);if(/[\s"\\\u0000-\u001f\u007f]/.test(e))throw new Error(`an OAuth issuer may not hold spaces, quotes, backslashes or control characters: ${JSON.stringify(e)}`);return e}var Bd,Mx,Lx,Hd=C(()=>{"use strict";nn();ws();Bd="SNOUTPOD_OAUTH_ISSUER",Mx=256*1024,Lx=5*6e4});function Kd(e,t){let n=null,r=!1;for(let o of e.split(`
|
|
410
|
-
`)){let s=o.trim();s.
|
|
411
|
-
A=$1
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
[ -f "$
|
|
416
|
-
|
|
417
|
-
[
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
{ [ -n "$kept" ] && printf '%s\n' "$kept"; printf '%s\n' "restore_command = '`+Ny.replace(/"/g,'\\"')+String.raw`'"; } > "$C" || fail "postgresql.auto.conf could not be written"
|
|
430
|
-
echo "swap-done"
|
|
431
|
-
`;qd=["select pg_promote(true, 120)","alter system reset synchronous_standby_names","alter system reset primary_conninfo","alter system reset primary_slot_name","alter system reset restore_command","select pg_reload_conf()"]});import{createHash as vs,randomBytes as Cy}from"node:crypto";import{access as jy,constants as Dy}from"node:fs/promises";import{delimiter as My,join as Ly}from"node:path";function Uy(e,t){return vs("sha256").update($s(e)).update(e.smtpSecretEnv&&t?vs("sha256").update(t).digest("hex"):"").digest("hex").slice(0,32)}function $s(e){if(Array.isArray(e))return`[${e.map($s).join(",")}]`;if(e!==null&&typeof e=="object"){let t=e;return`{${Object.keys(t).sort().filter(r=>t[r]!==void 0).map(r=>`${JSON.stringify(r)}:${$s(t[r])}`).join(",")}}`}return JSON.stringify(e??null)}function Ps(e){if(!Fy.test(e))throw new Error(`not a service name: ${JSON.stringify(e)}`);return e}function on(e){if(!Hy.test(e))throw new Error(`not a database role: ${JSON.stringify(e)}`);return e}function Ky(e,t,n){return`postgres://${on(t)}:${n}@127.0.0.1:5432/${le(e)}`}function Jd(e,t,n,r){let o=sd(t.env,e.adminPassword);if(t.dbUriEnv&&t.dbUriRole){let s=t.dbUriRole,a=n.get(s);if(a===void 0)throw new Error(`no password was applied for role ${JSON.stringify(t.dbUriRole)}, which ${JSON.stringify(t.name)} needs to reach its database`);o[t.dbUriEnv]=Ky(e.ref,s,a)}return t.smtpSecretEnv&&r&&(o[t.smtpSecretEnv]=r),o}function Vd(e,t){return`${G(e).pod}-${Ps(t)}`}function kr(e){return`snoutpod-host-${Ps(e)}`}function qy(e){let t=Object.keys(e.env).sort().map(n=>[n,e.env[n]]);return vs("sha256").update(JSON.stringify([e.image,e.port,e.hostPort,e.healthPath??null,e.memoryMb,t,e.adminPort??null,e.adminHostPort??null,e.command??null,e.volumes??null,e.network??null])).digest("hex").slice(0,32)}function Gy(e,t){let n=[],r=xs(t);for(let o of e??[])if(Ps(o.name),vr(o.port,`${o.name}: port`),!(o.hostPort===null||o.hostPort===void 0)){vr(o.hostPort,`${o.name}: hostPort`);for(let s of r)n.push("--publish",`${s}:${o.hostPort}:${o.port}`);if(o.name===tn){let s=o.hostPort+rd;vr(s,`${o.name}: admin hostPort`);for(let a of r)n.push("--publish",`${a}:${s}:${nd}`)}}return n}function xs(e){let t=e===void 0?[]:typeof e=="string"?[e]:[...e];return["127.0.0.1",...new Set(t.filter(n=>n!=="127.0.0.1"))]}function vr(e,t){if(!Number.isInteger(e)||e<1||e>65535)throw new Error(`${t} is not a port: ${JSON.stringify(e)}`);return e}function Xd(e){let t=[];for(let n of Object.keys(e)){if(!Wy.test(n))throw new Error(`not an environment variable name: ${JSON.stringify(n)}`);t.push("--env",n)}return t}function zy(e){let t=e.healthPath;if(t==null||t==="")return[];if(!tu.test(t))throw new Error(`${e.name}: not a health path: ${JSON.stringify(t)}`);return e.hostPort===null||e.hostPort===void 0?[]:(vr(e.hostPort,`${e.name}: hostPort`),["--label",`${Es}=${e.hostPort}:${t}`])}function Jy(e){if(!e)return null;let t=e.indexOf(":");if(t<=0)return null;let n=Number(e.slice(0,t)),r=e.slice(t+1);return!Number.isInteger(n)||n<1||n>65535||!tu.test(r)?null:{port:n,path:r}}async function Yy(e,t={}){let n=t.path??process.env.PATH??"",r=t.look??(o=>jy(o,Dy.X_OK));for(let o of n.split(My))if(o)try{let s=Ly(o,e);return await r(s),s}catch{}return null}function Xy(){return["\\set ON_ERROR_STOP 0","set search_path = pg_catalog, pg_temp;","select 'create extension if not exists pg_prewarm schema public' where exists (select 1 from pg_available_extensions where name = 'pg_prewarm') \\gexec","select pg_catalog.format($snoutpod$with candidate as ("," select c.oid, pg_catalog.pg_relation_size(c.oid) as bytes"," from pg_class c join pg_namespace n on n.oid = c.relnamespace"," where c.relkind in ('r', 'i', 'm')"," and c.relpersistence = 'p'"," and n.nspname not in ('pg_catalog', 'information_schema', 'pg_toast')","), budgeted as ("," select oid, bytes,"," sum(bytes) over (order by bytes, oid rows between unbounded preceding and current row) as running"," from candidate",")","select coalesce(sum(%I.pg_prewarm(oid::pg_catalog.regclass, 'buffer'::pg_catalog.text, 'main'::pg_catalog.text, null::pg_catalog.int8, null::pg_catalog.int8)), 0) from budgeted","where bytes > 0 and running <= pg_size_bytes(current_setting('shared_buffers')) * 3 / 4$snoutpod$, n.nspname)","from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_prewarm' \\gexec",""].join(`
|
|
432
|
-
`)}function Qy(e){return/^(\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com)\//.exec(e)?.[1]??null}function ew(e,t){for(let r of e??[]){let o=/^PG_MAJOR=(\d+)$/.exec(r.trim());if(o)return o[1]??null}let n=t.split("@")[0]??"";return/:(\d+)(?:[.-][^/:]*)?$/.exec(n)?.[1]??null}function As(e){let t=(e??"").trim();return/^\d+(?:\.\d+)?$/.test(t)?t:null}function tw(e,t){return t.image===null||t.data===null||t.image===t.data?null:`${e} was not started: its data directory was made by Postgres ${t.data} and it was handed a Postgres ${t.image} image. A data directory starts only on the major that made it. Its data is untouched, and it starts again on a ${t.data} image.`}async function nw(e,t,n,r){return{image:await rw(e,t,r),data:await ow(e,t,n)}}async function rw(e,t,n){let r=null;try{let o=await e(t,["image","inspect","--format","{{json .Config.Env}}",n]);if(o.code===0&&o.stdout.trim()){let s=JSON.parse(o.stdout.trim());r=Array.isArray(s)?s.filter(a=>typeof a=="string"):null}}catch{r=null}return ew(r,n)}async function ow(e,t,n){try{let r=await e(t,["volume","inspect","--format","{{.Mountpoint}}",G(n).pod]),o=r.code===0?r.stdout.trim().replace(/\/+$/,""):"";if(!o)return null;let s=await e(t,["unshare","cat",`${o}/PG_VERSION`]);return s.code===0?As(s.stdout):null}catch{return null}}function sw(e){if(!e)return[];let t=[],n=/^[1-9][0-9]{0,6}(kB|MB|GB)$/;typeof e.sharedBuffers=="string"&&n.test(e.sharedBuffers)&&t.push(`SNOUTPOD_SHARED_BUFFERS=${e.sharedBuffers}`),typeof e.workMem=="string"&&n.test(e.workMem)&&t.push(`SNOUTPOD_WORK_MEM=${e.workMem}`);let r=typeof e.maxConnections=="number"&&Number.isInteger(e.maxConnections)&&e.maxConnections>=1&&e.maxConnections<=1e4?e.maxConnections:Zd;return t.push(`SNOUTPOD_MAX_CONNECTIONS=${Math.max(r,Zd)}`),typeof e.privilegedExtensions=="string"&&/^[a-z][a-z0-9_-]*(,[a-z][a-z0-9_-]*)*$/.test(e.privilegedExtensions)&&t.push(`SNOUTPOD_PRIVILEGED_EXTENSIONS=${e.privilegedExtensions}`),t}function aw(e){let t=null;return()=>(t===null&&(t=n()),t);async function n(){if(e.runtime===null)return[];let r=e.runtime??Vy;return e.runtime!==void 0&&e.runtime!==""?["--runtime",r]:await Yy(r,{look:e.lookExecutable})===null?[]:["--runtime",r]}}function ru(e){let t=e.binary??"podman",n=e.exec,r=aw(e),o=e.probe??nu;async function s(g){try{await n(t,["exec",...he,"--interactive",G(g).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",g,"--quiet","--file","-"],{timeoutMs:Zy,stdin:Xy()})}catch{}}async function a(){let g=await z(n,t,["ps","--all","--filter",`label=${Te}`,"--format","json"]),y=g.trim()?JSON.parse(g):[],b=new Map,T=new Map,A=new Map;for(let E of y){let ne=E.Labels?.[Te],oe=E.Labels?.[ut];if(!ne||!oe)continue;let de=b.get(ne)??[];de.push(oe),b.set(ne,de),eu(E.State)==="stopped"&&T.set(ne,[...T.get(ne)??[],oe]);let M=Number(E.Labels?.[Ss]??0),ae=Number.isInteger(M)?M:0;A.set(ne,Math.max(A.get(ne)??ae,ae))}let q=y.filter(E=>E.Labels?.[Te]&&!E.Labels?.[ut]),F=q.map(E=>eu(E.State)),B=await cw(q,iw,(E,ne)=>F[ne]==="running"?Promise.resolve(!0):e.hasData(E.Labels?.[Te]??""));return q.map((E,ne)=>{let oe=E.Labels?.[Te]??"",de=Number(E.Labels?.[_s]);return{ref:oe,status:F[ne]??"unknown",generation:Number.isInteger(de)?de:null,hasData:B[ne]??!1,services:b.get(oe)??[],servicesStopped:T.get(oe)??[],...A.has(oe)?{servicesGeneration:A.get(oe)}:{}}})}async function l(){let g=await z(n,t,["ps","--all","--filter",`label=${Te}`,"--format","json"]);return(g.trim()?JSON.parse(g):[]).filter(b=>b.Labels?.[Te]&&!b.Labels?.[ut]).length}async function i(g){let y=await(e.majors??((b,T)=>nw(n,t,b,T)))(g.ref,g.image);return tw(g.ref,y)}async function u(g){let y=G(g.ref),b=await i(g);if(b)throw new Error(b);let T=e.credentials,A=await Rd(e.repository,T?()=>T.forProject(g.ref,g.bucket):null),q={SNOUTPOD_OWNER_PASSWORD:g.ownerPassword,POSTGRES_PASSWORD:g.adminPassword,...Od(A)},F=[...await r(),"run","--detach","--name",y.pod,"--label",`${Te}=${g.ref}`,"--label",`${_s}=${g.generation}`,"--userns=auto","--memory",`${g.memoryMb}m`,...uw(g.cpus),"--restart","no",...e.network?["--network",e.network]:[],...g.port?xs(e.podBind).flatMap(B=>["--publish",`${B}:${g.port}:5432`]):[],...Gy(g.services,e.podBind),"--volume",`${y.pod}:/var/lib/postgresql/data:U`,"--env","SNOUTPOD_OWNER_PASSWORD","--env","POSTGRES_PASSWORD","--env",`SNOUTPOD_REF=${g.ref}`,"--env","POSTGRES_USER=snoutpod_admin","--env","POSTGRES_DB=postgres","--env",`SNOUTPOD_S3_BUCKET=${g.bucket}`,"--env",`SNOUTPOD_S3_REGION=${g.region}`,...e.egressDenied?["--env","SNOUTPOD_EGRESS=denied"]:[],...dw(g.oauth),...sw(g.tuning).flatMap(B=>["--env",B]),"--env",`SNOUTPOD_RETENTION_FULL=${Ol(g.backup?.retentionDays,g.backup?.fullIntervalHours)}`,...Td(A),g.image,"snoutpod-postgres"];await z(n,t,F,{env:q})}async function c(g){let y=await z(n,t,["ps","--all","--filter",`label=${Te}=${le(g)}`,"--filter",`label=${ut}`,"--format","json"]),b=y.trim()?JSON.parse(y):[],T=[];for(let A of b){let q=A.Labels?.[ut];if(!q)continue;let F=Number(A.Labels?.[Ss]??0);T.push({name:q,container:Vd(g,q),spec:A.Labels?.[Wd]??"",pod:A.Labels?.[zd]??"",running:A.State==="running",generation:Number.isInteger(F)?F:0})}return T}async function p(g){let y=await n(t,["container","inspect","--format","{{.Id}}",G(g).pod]);return y.code===0?y.stdout.trim():""}async function f(g,y){let b=[];for(let B of y){let E=B.dbUriRole??null;B.dbUriEnv&&E&&!b.includes(E)&&b.push(on(E))}let T=new Map;if(b.length===0)return T;let A={},q=[],F=[];b.some(B=>Id.includes(B))&&F.push(Cd);for(let B of b.filter(E=>gs.includes(E)))F.push(jd(B));return b.forEach((B,E)=>{let ne=Cy(24).toString("base64url"),oe=`SNOUTPOD_SERVICE_PASSWORD_${E}`;T.set(B,ne),A[oe]=ne,q.push("--env",oe),F.push(`\\getenv service_password_${E} ${oe}`),F.push(`\\set service_role_${E} ${B}`),F.push(`alter role :"service_role_${E}" with password :'service_password_${E}';`)}),F.push(""),await z(n,t,["exec",...he,"--interactive",...q,G(g.ref).pod,"psql","--username","snoutpod_admin","--dbname",G(g.ref).database,"--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:A,stdin:F.join(`
|
|
433
|
-
`)}),T}async function k(g){let y=br(g.ref);await z(n,t,["volume","create","--ignore",y]),await z(n,t,[...await r(),"run","--rm","--network","none","--volume",`${y}:${ps}`,"--entrypoint","bash",g.image,"-c",od])}async function v(g){let y=await r();if((await n(t,[...y,"image","exists",g])).code===0)return;let b=await n(t,[...y,"pull","--quiet",g]);if(b.code===0)return;let T=Qy(g);if(!T||!e.registryLogin)throw new Error(`${g} could not be pulled: ${(b.stderr||b.stdout).trim()}`);await e.registryLogin(T),await z(n,t,[...y,"pull","--quiet",g])}async function V(g){let y=g.services??[],b=await c(g.ref),T;y.some(M=>M.smtpSecretEnv)&&(T=await e.smtpSecret?.().catch(()=>{}));let A=y.length>0?await p(g.ref):"",q=new Map(b.map(M=>[M.name,M])),F=new Map(y.map(M=>[M.name,Uy(M,T)])),B=new Set(y.filter(M=>{let ae=q.get(M.name);return!ae||!ae.running||ae.spec!==F.get(M.name)||A===""||ae.pod!==A}).map(M=>M.name)),E=g.servicesGeneration??0;if(B.size===0&&y.length>0&&!b.some(M=>M.generation>=E))for(let M of y)B.add(M.name);let ne=M=>M.dbUriEnv&&M.dbUriRole?M.dbUriRole:null;for(let M=!0;M;){M=!1;let ae=new Set(y.filter(st=>B.has(st.name)).map(ne));for(let st of y){let Ks=ne(st);!B.has(st.name)&&Ks!==null&&ae.has(Ks)&&(B.add(st.name),M=!0)}}for(let M of new Set(y.filter(ae=>B.has(ae.name)).map(ae=>ae.image)))await v(M);for(let M of b)(B.has(M.name)||!F.has(M.name))&&await z(n,t,["rm","--force","--ignore",M.container]);let oe=y.filter(M=>B.has(M.name)),de=await f(g,oe);oe.some(M=>M.name===tn)&&await k(g);for(let M of oe){let ae=Vd(g.ref,M.name),st=[...await r(),"run","--detach","--name",ae,"--label",`${Te}=${g.ref}`,"--label",`${_s}=${g.generation}`,"--label",`${ut}=${M.name}`,"--label",`${Ss}=${g.servicesGeneration??0}`,"--label",`${Wd}=${F.get(M.name)??""}`,"--label",`${zd}=${A}`,"--network",`container:${G(g.ref).pod}`,"--userns",`container:${G(g.ref).pod}`,"--memory",`${M.memoryMb}m`,"--restart","no",...zy(M),...M.name===tn?["--volume",`${br(g.ref)}:${ps}:ro`]:[],...Xd(Jd(g,M,de,T)),M.image];await z(n,t,st,{env:Jd(g,M,de,T)})}}async function j(g,y){if(y%6!==0)return!1;let b=await n(t,["container","inspect","--format","{{.State.Status}}",G(g).pod]);return b.code===0&&/^(exited|stopped|dead)$/.test(b.stdout.trim())}function Y(g){return["exec",...he,G(g).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname","postgres"]}async function R(g){let y=await n(t,[...Y(g),"-tAc",Bl]);return y.code===0?Fl(y.stdout):null}async function ee(g){let y=await n(t,[...Y(g),"-tAc","select pg_is_in_recovery()"]);if(y.code!==0)return null;let b=y.stdout.trim();return b==="t"?!0:b==="f"?!1:null}async function O(g,y){let b=A=>n(t,[...Y(g),"-tAc",A]),T=Date.now()+y;do{if((await b("checkpoint")).code!==0)return!1;let q=await b("select (select timeline_id from pg_control_checkpoint()) = ('x' || substr(pg_walfile_name(pg_current_wal_lsn()), 1, 8))::bit(32)::int");if(q.code===0&&q.stdout.trim()==="t")return!0;await new Promise(F=>setTimeout(F,1e3))}while(Date.now()<T);return!1}async function pe(g){let y=await n(t,[...Y(g),"-tAc",Cl],{timeoutMs:ks});return y.code===0?jl(y.stdout):null}async function W(g,y){let b=new Map;for(let A of y)le(A),b.set(A,G(A).pod);if(b.size===0)return new Map;let T=await n(t,["exec",G(g).pod,"timeout",String(10),"sh","-c",Ll,"sh",...b.values()]);return T.code===0?Ul(T.stdout,b):null}async function ce(g,y){le(y);let b=[];for(let F of[g,y]){let B=G(F).pod,E=await n(t,["container","inspect","--format","{{.State.Status}}",B]);if(E.code===0&&E.stdout.trim()==="running")throw new Error(`${B} is still running, so its data was not moved`);let ne=await n(t,["volume","inspect","--format","{{.Mountpoint}}",B]),oe=ne.code===0?ne.stdout.trim().replace(/\/+$/,""):"";if(!oe)throw new Error(`the volume ${B} could not be found, so nothing was moved`);b.push(oe)}let A=typeof process.getuid!="function"||process.getuid()!==0?await n(t,["unshare","sh","-c",bs,"sh",b[0],b[1]]):await n("sh",["-c",bs,"sh",b[0],b[1]]),q=Kd(A.stdout,A.code);if(q)throw new Error(q)}async function pn(g){return(await n(t,[...Y(g),"-v","ON_ERROR_STOP=1",...qd.flatMap(b=>["-c",b])])).code===0}async function pt(g,y){return(await n(t,[...Y(g),"-v","ON_ERROR_STOP=1",...Ml(y).flatMap(T=>["-c",T])],{timeoutMs:ks})).code===0}async function mn(g){let y=await n(t,[...Y(g),"-tAc","select pg_read_file(current_setting('ssl_cert_file')) where current_setting('ssl') = 'on'"]),b=`${y.stdout.trim()}
|
|
434
|
-
`;return y.code===0&&Dl.test(b)?b:null}async function Ju(g,y){if(!Il.test(y))throw new Error("not a SCRAM-SHA-256 verifier");let b=G(g).pod,T=[...Y(g),"-v","ON_ERROR_STOP=1"];if((await n(t,[...T,"-c",`do $$ begin if not exists (select 1 from pg_roles where rolname = '${tt}') then create role ${tt} with login replication; end if; end $$`,"-c",`alter role ${tt} with login replication noinherit password '${y}'`],{timeoutMs:ks})).code!==0)return!1;let q=await n(t,[...T,"-tAc","show hba_file"]),F=q.stdout.trim();if(q.code!==0||!/^\/[A-Za-z0-9/_.-]+$/.test(F))return!1;let B=`hostssl replication ${tt} all scram-sha-256`;if((await n(t,["exec",b,"sh","-c",'grep -Eq "^host(ssl)?[[:space:]]+replication[[:space:]]+$2[[:space:]]" "$1" || printf "%s\\n" "$3" >> "$1"',"sh",F,tt,B])).code!==0)return!1;if((await n(t,[...T,"-tAc","show ssl"])).stdout.trim()!=="on"){let de=F.replace(/\/[^/]+$/,"");if((await n(t,["exec","--user","root",b,"sh","-c",'[ -s "$1/server.key" ] || { openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj "/CN=$2" -keyout "$1/server.key" -out "$1/server.crt" >/dev/null 2>&1 && chown postgres:postgres "$1/server.key" "$1/server.crt" && chmod 0600 "$1/server.key"; }',"sh",de,b])).code!==0||(await n(t,[...T,"-c","alter system set ssl_cert_file = 'server.crt'","-c","alter system set ssl_key_file = 'server.key'","-c","alter system set ssl = on"])).code!==0)return!1}return(await n(t,[...T,"-tAc","select pg_reload_conf()"])).code===0}return{list:a,inRecovery:ee,replayLsn:R,settleTimeline:O,podHealth:pe,setSyncNames:pt,ensureKeeperRole:Ju,podCertificate:mn,swapHomeData:ce,promoteStandby:pn,nodesAnswer:W,preflight:i,create:u,createServices:V,async start(g){let y=(await c(g).catch(()=>[])).filter(b=>b.running).map(b=>b.container);y.length>0&&await z(n,t,["stop","--time","5",...y]),await z(n,t,["start",G(g).pod])},async startServices(g){let y=(await c(g)).map(b=>b.container);y.length>0&&await z(n,t,["start",...y])},async applyHostServices(g){let y=[],b=await z(n,t,["ps","--all","--filter",`label=${rn}`,"--format","json"]),T=b.trim()?JSON.parse(b):[],A=new Map;for(let F of T){let B=F.Labels?.[rn];B&&A.set(B,{spec:F.Labels?.[Yd]??"",state:F.State??""})}for(let F of g)try{await q(F)}catch(B){y.push({name:F.name,action:"failed",reason:B instanceof Error?B.message:String(B)})}async function q(F){let B=e.hostServiceLocal?.(F,g),E=B?{...F,...B.env&&Object.keys(B.env).length>0?{env:{...F.env,...B.env}}:{},...B.volumes?{volumes:B.volumes}:{},...B.network?{network:B.network}:{}}:F,ne=kr(E.name),oe=qy(E),de=A.get(E.name);if(A.delete(E.name),de&&de.spec===oe){de.state!=="running"&&(await z(n,t,["start",ne]),y.push({name:E.name,action:"started",reason:de.state}));return}let M=de?"the spec changed":"it was not here";await v(E.image),de&&await z(n,t,["rm","--force","--ignore",ne]),await z(n,t,[...await r(),"run","--detach","--name",ne,"--label",`${rn}=${E.name}`,"--label",`${Yd}=${oe}`,...E.healthPath?["--label",`${Es}=${E.hostPort}:${E.healthPath}`]:[],"--memory",`${E.memoryMb}m`,"--restart","no",...E.network?["--network",E.network]:e.network?["--network",e.network]:[],...(E.volumes??[]).flatMap(ae=>["--volume",ae]),...xs(e.podBind).flatMap(ae=>["--publish",`${ae}:${E.hostPort}:${E.port}`]),...E.adminPort&&E.adminHostPort?["--publish",`127.0.0.1:${E.adminHostPort}:${E.adminPort}`]:[],...Xd(E.env),E.image,...E.command??[]],{env:{...E.env}}),y.push({name:E.name,action:de?"recreated":"created",reason:M})}for(let F of A.keys())await z(n,t,["rm","--force","--ignore",kr(F)]),y.push({name:F,action:"removed",reason:"it is no longer on the list"});return y},async hostServiceHealth(g){let y=await z(n,t,["ps","--all","--filter",`label=${rn}`,"--format","json"]),b=(y.trim()?JSON.parse(y):[]).map(A=>A.Labels?.[rn]??"").filter(A=>A!=="");if(b.length===0)return[];let T=Date.now()+g;for(;;){let A=[];for(let q of b)A.push(await Qd(n,t,q,kr(q),o));if(A.every(q=>q.ok)||Date.now()>=T)return A;await new Promise(q=>setTimeout(q,500))}},async serviceHealth(g,y){let b=await c(g);if(b.length===0)return[];let T=Date.now()+y;for(;;){let A=await Promise.all(b.map(q=>Qd(n,t,q.name,q.container,o)));if(A.every(q=>q.ok)||Date.now()>=T)return A;await new Promise(q=>setTimeout(q,500))}},async stop(g,y){for(let b of await c(g))await z(n,t,["stop","--time",String(By),b.container]);await z(n,t,["stop","--time",String(y),G(g).pod])},async remove(g){for(let y of await c(g))await z(n,t,["rm","--force","--ignore",y.container]);await z(n,t,["rm","--force","--ignore","--volumes",G(g).pod])},async removeData(g){await z(n,t,["volume","rm","--force",G(g).pod]);let y=br(g);(await n(t,["volume","exists",y])).code===0&&await z(n,t,["volume","rm","--force",y])},async applyOwnerPassword(g){let y=await n(t,["exec",...he,G(g.ref).pod,"psql","--username","snoutpod_admin","--dbname","postgres","-tAc",`select rolpassword from pg_authid where rolname = '${le(g.ref)}_owner'`]);if(y.code===0&&$d(y.stdout,g.ownerPassword))return;let b=["\\getenv owner_password SNOUTPOD_OWNER_PASSWORD",`\\set owner_role ${g.ref}_owner`,`alter role :"owner_role" with password :'owner_password';`,""].join(`
|
|
435
|
-
`);await z(n,t,["exec",...he,"--interactive","--env","SNOUTPOD_OWNER_PASSWORD",G(g.ref).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:{SNOUTPOD_OWNER_PASSWORD:g.ownerPassword},stdin:b})},async applyRolePassword(g,y,b){let T=["\\getenv role_password SNOUTPOD_ROLE_PASSWORD",`\\set target_role ${on(y)}`,`alter role :"target_role" with password :'role_password';`,""].join(`
|
|
436
|
-
`);await z(n,t,["exec",...he,"--interactive","--env","SNOUTPOD_ROLE_PASSWORD",G(g).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:{SNOUTPOD_ROLE_PASSWORD:b},stdin:T})},async updateExtensions(g){let y=G(g),b=await n(t,["exec",...he,y.pod,"psql","--username","snoutpod_admin","--dbname","postgres","-tAc",`select datname from pg_database where datname = '${y.database}' or (not datistemplate and datname <> 'postgres') order by datname = '${y.database}' desc, datname limit 1`]),T=b.code===0&&/^[a-z][a-z0-9_]{0,62}$/.test(b.stdout.trim())?b.stdout.trim():y.database,A=await z(n,t,["exec",...he,"--interactive",y.pod,"psql","--username","snoutpod_admin","--dbname",T,"--quiet","-tA","-F",ys,"-v","ON_ERROR_STOP=1","--file","-"],{stdin:Md});return Ld(A)},async applyDbAccess(g,y){let b=G(g),T=await z(n,t,["exec",...he,"--interactive","--env",Sr,b.pod,"psql","--username","snoutpod_admin","--dbname",b.database,"--quiet","-tA","-v","ON_ERROR_STOP=1","--file","-"],{env:{[Sr]:yd(g,y)},stdin:gd,timeoutMs:6e4});return hd(T)},async runProjectSql(g,y){let b=G(g);await z(n,t,["exec",...he,"--interactive",b.pod,"psql","--username","snoutpod_admin","--dbname",b.database,"--quiet","-v","ON_ERROR_STOP=1","--file","-"],{stdin:y})},async ensureHostServiceDatabase(g,y,b){let T=kr(g),A=on(y),q=on(b),F=E=>["exec",T,"psql","--username",A,"--dbname","postgres","--quiet","-tA","-v","ON_ERROR_STOP=1","-c",E];(await z(n,t,F(`select 1 from pg_database where datname = '${q}'`))).trim()!=="1"&&await z(n,t,F(`create database "${q}"`))},async renameProject(g,y){y!==g.ref&&await z(n,t,["exec",...he,"--interactive",G(g.ref).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{stdin:lw(y,g.ref)})},async waitReady(g,y){let b=G(g).pod,T=Date.now()+y,A=0;for(;;){let q=await n(t,["exec",...he,b,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname","postgres","-tAc","select not pg_is_in_recovery()"]);if(q.code===0&&q.stdout.trim()==="t")return s(g),!0;if(Date.now()>=T||await j(g,++A))return!1;await new Promise(F=>setTimeout(F,500))}},async waitStandby(g,y){let b=Date.now()+y,T=0;for(;;){if(await ee(g)===!0)return!0;if(Date.now()>=b||await j(g,++T))return!1;await new Promise(A=>setTimeout(A,500))}},async capacity(){let[g,y]=await Promise.all([l(),e.capacity()]);return{pods:g,...y}},async databaseBytes(g){let y=await n(t,["exec",...he,G(g).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",g,"-tAc","select pg_database_size(current_database())"]);return y.code===0?ns(y.stdout):null},async storageBytes(g){let y=await n(t,["exec",...he,G(g).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",g,"-tAc","select (xpath('/row/n/text()', query_to_xml(format('select coalesce(sum((metadata->>''size'')::bigint), 0) as n from %s', coalesce(to_regclass('snout_files.files'), to_regclass('storage.objects'))), false, true, '')))[1]::text"]);return y.code===0?ns(y.stdout):null},async setReadOnly(g,y){let b=G(g);await z(n,t,["exec",...he,b.pod,"psql","--username","snoutpod_admin","--dbname","postgres","-tAc",`alter database ${b.database} set default_transaction_read_only = ${y?"on":"off"}`])},async archiveState(g){let y=await n(t,["exec",...he,G(g).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",g,"-tA","-F","|","-c",`select coalesce(to_char(last_archived_time, 'YYYY-MM-DD"T"HH24:MI:SS.MSOF'), ''), case when last_failed_time > coalesce(last_archived_time, '-infinity') then failed_count else 0 end from pg_stat_archiver`]);if(y.code!==0)return null;let[b="",T=""]=y.stdout.trim().split(`
|
|
437
|
-
`)[0]?.split("|")??[],A=Number(T);return{lastArchivedAt:b.trim()||null,failures:Number.isInteger(A)&&A>=0?A:0}}}}async function Qd(e,t,n,r,o=nu){let s=await e(t,["inspect","--format","json",r]);if(s.code!==0)return{name:n,ok:!1,detail:"no such container"};let a,l;try{let p=JSON.parse(s.stdout);a=p[0]?.State,l=p[0]?.Config?.Labels}catch{return{name:n,ok:!1,detail:"podman inspect could not be read"}}let i=a?.Status??"unknown";if(i!=="running")return{name:n,ok:!1,detail:i};let u=Jy(l?.[Es]);if(!u)return{name:n,ok:!0,detail:"running"};let c=await o(u.port,u.path);return c===null?{name:n,ok:!1,detail:"not answering yet"}:{name:n,ok:!0,detail:`answered ${c}`}}async function cw(e,t,n){let r=new Array(e.length),o=0;async function s(){for(;o<e.length;){let a=o++;r[a]=await n(e[a],a)}}return await Promise.all(Array.from({length:Math.min(Math.max(1,t),e.length)},s)),r}function eu(e){switch((e??"").toLowerCase()){case"running":return"running";case"created":case"configured":return"starting";case"exited":case"stopped":return"stopped";default:return"unknown"}}function lw(e,t){return[`\\set from_db ${e}`,`\\set to_db ${t}`,`\\set from_role ${e}_owner`,`\\set to_role ${t}_owner`,'alter database :"from_db" rename to :"to_db";','alter role :"from_role" rename to :"to_role";',"alter system reset restore_command;","alter system reset recovery_target_time;","alter system reset recovery_target_action;",""].join(`
|
|
438
|
-
`)}function dw(e){if(!e?.issuer)return[];try{return["--env",`${Bd}=${Fd(e.issuer)}`]}catch{return[]}}function uw(e){return typeof e!="number"||!Number.isFinite(e)||e<=0?[]:["--cpus",e.toFixed(2)]}var Te,_s,ut,Ss,Wd,zd,By,Fy,Hy,rn,Yd,Wy,tu,Es,nu,Vy,he,ks,Zy,Zd,iw,ws=C(()=>{"use strict";Re();ts();os();ss();xd();nn();Nd();rs();Dd();Ud();fs();Hd();as();Gd();ms();Te="snoutpod.ref",_s="snoutpod.generation",ut="snoutpod.service",Ss="snoutpod.services-generation",Wd="snoutpod.service-spec",zd="snoutpod.service-pod";By=10,Fy=/^[a-z][a-z0-9-]{0,30}$/;Hy=/^[a-z][a-z0-9_]{0,62}$/;rn="snoutpod.host-service",Yd="snoutpod.host-service-spec";Wy=/^[A-Za-z_][A-Za-z0-9_]*$/;tu=/^\/[A-Za-z0-9\/_\-.~%?=&]*$/,Es="snoutpod.health";nu=async(e,t)=>{let{request:n}=await import("node:http");return new Promise(r=>{let o=n({host:"127.0.0.1",port:e,path:t,method:"GET",timeout:2e3},s=>{s.resume(),r(s.statusCode??null)});o.on("error",()=>r(null)),o.on("timeout",()=>{o.destroy(),r(null)}),o.end()})},Vy="crun";he=["--env","PGOPTIONS=-c search_path=pg_catalog,pg_temp -c synchronous_commit=local"],ks=3e4,Zy=3e4;Zd=200;iw=8});var wu={};qs(wu,{DEFAULT_LOCAL_MEMORY_MB:()=>cu,DEFAULT_LOCAL_PORT:()=>hw,ENGINE:()=>Z,ENGINE_NAME:()=>Rs,IMAGE:()=>Ns,NETWORK:()=>nt,NEW_PROJECT_MAJOR:()=>Os,NO_BLACKHOLE_ROUTES:()=>pu,PUBLIC_IMAGE:()=>_w,PUBLISHED_MAJORS:()=>Rt,capacity:()=>uu,chooseLocalImage:()=>cn,dropIfUnused:()=>hu,ensureImage:()=>Sw,ensureNetwork:()=>Pw,exec:()=>ie,hasData:()=>Er,localImageFor:()=>du,localPsql:()=>Rw,localRuntime:()=>xw,localSpec:()=>gu,localStatus:()=>Is,localUri:()=>fu,networkPolicyInPlace:()=>mu,newLocalPassword:()=>ww,newLocalRef:()=>yw,podLog:()=>yu,podName:()=>Tt,podmanReady:()=>vw,removeLocal:()=>Nw,staleImage:()=>Ts,startLocal:()=>Aw,stopLocal:()=>Ow,volumePgVersion:()=>$r});import{randomBytes as au}from"node:crypto";import{existsSync as ou}from"node:fs";import{platform as Pr}from"node:os";import{delimiter as pw,join as mw}from"node:path";function fw(){if(process.env.SNOUTPOD_ENGINE)return process.env.SNOUTPOD_ENGINE;let e=Pr()==="win32"?["podman.exe","podman"]:["podman"];for(let t of(process.env.PATH??"").split(pw))if(t&&e.some(n=>ou(mw(t,n))))return"podman";return ou("/opt/podman/bin/podman")?"/opt/podman/bin/podman":"podman"}function cn(e,t=process.env.SNOUTPOD_IMAGE){if(t)return{name:t,published:null,major:null};if(e===null)return an(Os);let n=Number(As(e));if(!Rt.includes(n)){let r=e.trim()||"(empty)",o=Rt.map(s=>an(s).published).join(" or ");throw new Error(`its data directory's PG_VERSION says ${r}, and there is an image only for Postgres ${Rt.join(" and ")}. Set SNOUTPOD_IMAGE to an image of that major (${o} for those).`)}return an(n)}function an(e){return{name:`${gw}:${e}`,published:`${iu}:${e}`,major:e}}function yw(){return Al(e=>au(e))}function ww(){return au(18).toString("base64url")}async function Sw(e,t=ie,n=cn(null)){let r=n.published;if(!r)return null;if((await t(Z,["image","exists",n.name])).code===0)return await kw(e,t,n.name,r),null;e?.(`Fetching the database image (${r}). This happens once.`);let o=await t(Z,["pull",r],{timeoutMs:lu}).catch(a=>({code:1,stdout:"",stderr:a instanceof Error?a.message:String(a)}));if(o.code!==0)return`could not fetch the database image ${r}: ${o.stderr.trim()||o.stdout.trim()||"the pull failed"}`;let s=await t(Z,["tag",r,n.name]);return s.code!==0?`fetched ${r} but could not tag it as ${n.name}: ${s.stderr.trim()}`:(e?.("Got it."),null)}async function kw(e,t,n,r){let o=await t(Z,["image","inspect","--format","{{.Id}}",n,r]).catch(()=>null),[s,a]=o?.code===0?o.stdout.trim().split(`
|
|
439
|
-
`):[];if(!s||s!==a)return;let l=setTimeout(()=>e?.(`Fetching a newer database image (${r}).`),3e3),i=await t(Z,["pull",r],{timeoutMs:lu}).catch(f=>({code:1,stdout:"",stderr:f instanceof Error?f.message:String(f)})).finally(()=>clearTimeout(l));if(i.code!==0){let f=(i.stderr.trim()||i.stdout.trim()||"the pull failed").split(`
|
|
440
|
-
`).at(-1);e?.(`Could not check for a newer database image (${f}); using the one on this machine.`);return}let u=await t(Z,["image","inspect","--format","{{.Id}}",r]).catch(()=>null),c=u?.code===0?u.stdout.trim():"";if(!c||c===s)return;let p=await t(Z,["tag",r,n]);if(p.code!==0){e?.(`Fetched a newer ${r} but could not tag it as ${n}: ${p.stderr.trim()}`);return}e?.(`Fetched a newer database image (${r}).`)}async function vw(e=bw,t=Ns){if(Rs!=="podman")return`SNOUTPOD_ENGINE=${Rs}: a local project is Podman-only, because a database that works under Docker and not under Podman is a database that works here and not in the cloud`;let n;try{n=await ie(Z,["--version"])}catch{return`${Z} is not installed. macOS: brew install podman, then podman machine init && podman machine set --rootful=false && podman machine start. Windows: winget install RedHat.Podman, then the same three machine commands. Linux: your distribution's podman package.`}if(n.code!==0)return`${Z} --version failed: ${n.stderr.trim()||n.stdout.trim()}`;if((await ie(Z,["image","exists",t])).code!==0)return`the pod image ${t} is not on this machine. ${e}`;let o=await ie(Z,["machine","inspect","--format","{{.Rootful}}"]);return o.code===0&&o.stdout.trim().startsWith("true")?"the podman machine is ROOTFUL, and the hosts are not: podman machine stop && podman machine set --rootful=false && podman machine start":null}function $w(e){let t=`if out=$(ls -A ${JSON.stringify(e)} 2>/dev/null); then if [ -n "$out" ]; then echo DATA; else echo EMPTY; fi; else echo HIDDEN; fi`;return ie(Z,["machine","ssh",t]).then(n=>{let r=n.stdout.trim().split(`
|
|
441
|
-
`).at(-1);return r==="DATA"?"data":r==="EMPTY"?"empty":"hidden"},()=>"hidden")}function Er(e){let t=Pr()==="darwin"||Pr()==="win32";return Sd(ie,e,{binary:Z,look:t?$w:void 0})}async function $r(e,t=ie,n=Pr()){let r=G(e).pod;if((await t(Z,["volume","exists",r])).code!==0)return null;let o;if(n==="darwin"||n==="win32"){let s=null;for(let a of Rt){let l=an(a).name;if((await t(Z,["image","exists",l])).code===0){s=l;break}}if(!s)throw new Error(`${r} holds a database and there is no pod image on this machine to read its Postgres version with. Set SNOUTPOD_IMAGE to the image it was made with (${Rt.map(a=>an(a).published).join(" or ")}).`);o=await t(Z,["run","--rm","--pull=never","--network=none","--userns=host","--user=0","--entrypoint=cat","--volume",`${r}:/data:ro`,s,"/data/PG_VERSION"])}else{let s=await t(Z,["volume","inspect","--format","{{.Mountpoint}}",r]),a=s.code===0?s.stdout.trim().replace(/\/+$/,""):"";if(!a)throw new Error(`could not find where ${r} is: ${s.stderr.trim()||"no mountpoint"}`);o=await t(Z,["unshare","cat",`${a}/PG_VERSION`])}if(o.code===0)return o.stdout;if(/no such file/i.test(o.stderr))return null;throw new Error(`could not read the Postgres version in ${r}: ${o.stderr.trim()||o.stdout.trim()||`exit ${o.code}`}`)}function du(e,t=$r){if(!e||process.env.SNOUTPOD_IMAGE)return Promise.resolve(cn(null));le(e);let n=t===$r?xr.get(e):void 0;if(n)return n;let r=t(e).then(o=>{try{return cn(o)}catch(s){throw new Error(`${Tt(e)}: ${s instanceof Error?s.message:String(s)}`)}});return t===$r&&(xr.set(e,r),r.catch(()=>xr.delete(e))),r}async function uu(){return{ramFreeMb:4096,ramTotalMb:8192,diskFreeMb:32768,diskTotalMb:65536}}function xw(e){return ru({exec:ie,binary:Z,network:nt,hasData:Er,capacity:uu,repository:e})}function Tt(e){return G(e).pod}async function Pw(e){if(process.env.SNOUTPOD_LOCAL_ALLOW_EGRESS==="1"){await Ew()||await ie(Z,yr({name:nt}));return}let t=yr({name:nt,allow:[]}),n="";for(let r=0;r<10;r+=1){if(await mu())return;await e?.(),await ie(Z,["network","rm","--force",nt]).catch(()=>{});let o=await ie(Z,t);if(o.code===0)return;if(n=`${o.stderr}${o.stdout}`.trim(),/invalid route gateway/i.test(n))throw new Error(`${pu}: ${n}`);await new Promise(s=>setTimeout(s,500))}throw new Error(`could not create ${nt} with egress blocked${n?`: ${n}`:""}`)}async function Ew(){return(await ie(Z,["network","inspect",nt,"--format","json"])).code===0}async function mu(){let e=await ie(Z,["network","inspect",nt,"--format","json"]);if(e.code!==0)return!1;try{return Ql(JSON.parse(e.stdout)?.[0])}catch{return!1}}function fu(e){let t=G(e.ref);return`postgres://${t.ownerRole}:${encodeURIComponent(e.ownerPassword)}@127.0.0.1:${e.port}/${t.database}?sslmode=disable`}function gu(e,t={}){return le(e.ref),{ref:e.ref,generation:1,image:t.image??Ns,memoryMb:t.memoryMb??cu,cpus:null,bucket:"",region:"us-east-1",ownerPassword:e.ownerPassword,adminPassword:e.adminPassword,port:e.port,tuning:t.tuning??null,backup:null,services:t.services??null}}function sn(e,t){e.onProgress?.(t)}async function Aw(e,t,n={}){let r=n.image??(await du(t.ref,n.pgVersion)).name,o=gu(t,{...n,image:r}),s=n.timeoutMs??12e4,a=n.hasData??Er,l=(await e.list()).find(u=>u.ref===t.ref),i=null;if(l?l.status==="stopped"&&(i=await(n.staleImage??Ts)(t.ref,r))?(sn(n,"Starting the database on the newer image. Its data stays as it is."),await e.remove(t.ref),await e.create(o),await(n.dropImage??hu)(i)):l.status==="stopped"?(sn(n,"Starting the database."),await e.start(t.ref),await e.startServices(t.ref)):(sn(n,"The database is already running."),l.status==="running"&&await(n.staleImage??Ts)(t.ref,r)&&sn(n,"A newer database image is on this machine. To move onto it: snoutdata stop, then snoutdata start.")):(sn(n,await a(t.ref)?"Starting the database over its existing data.":"Creating the database."),await e.create(o)),!await e.waitReady(t.ref,s))throw new Error(await Tw(t.ref,s,n.podLog??yu));return await e.applyOwnerPassword(o),await e.createServices(o),Is(e,t,n)}function su(e){return e.trim().replace(/^sha256:/,"")}async function Ts(e,t){let[n,r]=await Promise.all([ie(Z,["container","inspect","--format","{{.Image}}",Tt(e)]).catch(()=>null),ie(Z,["image","inspect","--format","{{.Id}}",t]).catch(()=>null)]);if(n?.code!==0||r?.code!==0)return null;let o=su(n.stdout),s=su(r.stdout);return o&&s&&o!==s?o:null}async function hu(e){let t=await ie(Z,["image","inspect","--format","{{len .RepoTags}}",e]).catch(()=>null);t?.code===0&&t.stdout.trim()==="0"&&await ie(Z,["rmi",e]).catch(()=>{})}async function Rw(e,t){let n=G(e.ref);return ie(Z,["exec","--interactive","-e","PGPASSWORD",Tt(e.ref),"psql","--host","127.0.0.1","--port","5432","--username",n.ownerRole,"--dbname",n.database,"--no-psqlrc","-v","ON_ERROR_STOP=1","-tA","--file","-"],{stdin:t,env:{PGPASSWORD:e.ownerPassword}})}async function yu(e){let t=await ie(Z,["logs","--tail","20",Tt(e)]).catch(()=>null);return t?`${t.stdout}${t.stderr}`:"(no log: the container is not there)"}async function Tw(e,t,n){let r=await n(e).catch(()=>"(no log)");return`${Tt(e)} never became ready in ${t}ms
|
|
447
|
+
`});function gp(e){if(typeof e!="string"||!/^https?:\/\//.test(e)||e.length>255)throw new Error(`not an OAuth issuer a pod can be given: ${JSON.stringify(e)}`);if(/[\s"\\\u0000-\u001f\u007f]/.test(e))throw new Error(`an OAuth issuer may not hold spaces, quotes, backslashes or control characters: ${JSON.stringify(e)}`);return e}var fp,LE,UE,hp=D(()=>{"use strict";Tn();so();fp="SNOUTPOD_OAUTH_ISSUER",LE=256*1024,UE=5*6e4});function Zt(e){return`${B(e).pod}-guard`}function yp(e,t,n){if(!Number.isInteger(t)||t<1||t>65535)throw new Error(`not a port: ${t}`);let r=e.leaseMs??1e4;return{LEPIS_GUARD_WITNESS:e.witness,...e.witnessSslmode?{LEPIS_GUARD_WITNESS_SSLMODE:e.witnessSslmode}:{},LEPIS_GUARD_CLUSTER:e.cluster,LEPIS_GUARD_TOKEN:e.token,LEPIS_GUARD_COPY:e.copy,LEPIS_GUARD_PEER:e.peer,LEPIS_GUARD_PG:`127.0.0.1:${t}`,LEPIS_GUARD_PG_USER:"snoutpod_admin",LEPIS_GUARD_PG_PASSWORD:n,LEPIS_GUARD_STATE:`${Pa}/epoch`,LEPIS_GUARD_STATUS:`${Pa}/status.json`,LEPIS_GUARD_LEASE_MS:String(r),LEPIS_GUARD_TICK_MS:String(Math.max(1,Math.min(1e3,Math.floor(r/5))))}}function wp(e,t,n){return[e.image,e.witness,e.witnessSslmode??"",e.cluster,e.copy,e.peer,e.leaseMs??1e4,t,n].join("|")}function bp(e,t,n){if(ae(e),!n.hostDir.startsWith("/")||n.hostDir.includes(":"))throw new Error(`not a host directory for a volume: ${n.hostDir}`);return["run","--detach","--name",Zt(e),"--label",`${ao}=${e}`,"--label",`${xa}=${n.fingerprint}`,"--network","host","--userns","auto","--volume",`${n.hostDir}:${Pa}:U`,"--memory","64m","--pids-limit","64","--restart","no",...n.envNames.flatMap(r=>["--env",r]),t.image,"guard"]}function Sp(e,t,n,r){let o=2*(r.leaseMs??1e4)/1e3;return n-e<=o+60?!1:t===null||n-t>o}var Pa,ao,xa,_p=D(()=>{"use strict";Te();Pa="/guard",ao="snoutpod.guard",xa="snoutpod.guard-spec"});function $p(e){return ae(e),{anchor:`snoutpod-${e}-copy`,router:`snoutpod-${e}-copy-lepis`,service:t=>`snoutpod-${e}-copy-${Qt(t)}`}}var kp,vp,Pp=D(()=>{"use strict";Te();so();kp="snoutpod.copy",vp="snoutpod.copy-service"});import{createHash as tS}from"node:crypto";function Qe(e){return`${B(ae(e)).pod}-${io}`}function Ap(e){return Ze(e)?(e.services??[]).filter(t=>t.name!==ge):[]}function Tt(e,t){return Ze(e)&&t.name!==ge}function Rp(e){let t=e.stackPodPort;if(typeof t!="number"||!Number.isInteger(t)||t<1||t>65535)throw new Error(`${e.ref}: a services-only pod needs its own pod port (stackPodPort), and has ${JSON.stringify(t??null)}`);return t}function Aa(e,t){if(t.hostPort===null||t.hostPort===void 0)return null;let n=Rp(e);if(typeof e.port!="number"||!Number.isInteger(e.port)||e.port<1)throw new Error(`${e.ref}: ${t.name}'s slot is read off the pod port, and the spec has none`);let r=t.hostPort-e.port;if(r<1e3||r%1e3!==0)throw new Error(`${e.ref}: ${t.name}'s hostPort ${t.hostPort} is on no slot of pod port ${e.port}`);let o=n+r;if(o>65535)throw new Error(`${e.ref}: stackPodPort ${n} puts ${t.name} past 65535`);return o}function Ra(e,t){let n=Aa(e,t),r=Tp(e,t);return n===null?t:{...t,hostPort:n,...r===null?{}:{adminHostPort:r}}}function Tp(e,t){return!t.adminPort||!t.adminHostPort?null:Aa(e,{name:`${t.name} admin`,hostPort:t.adminHostPort})}function Ta(e){return Ap(e).flatMap(t=>{let n=Tp(e,t);return n===null||!t.adminPort?[]:[[n,t.adminPort]]}).sort((t,n)=>t[0]-n[0])}function Oa(e){let t=Ap(e);return t.length>0&&Rp(e),t.flatMap(n=>{let r=Aa(e,n);return r===null?[]:[[r,n.port]]}).sort((n,r)=>n[0]-r[0])}function Op(e,t){let n=Ta(e);return tS("sha256").update(`stack:${t}:`).update(JSON.stringify(Oa(e))).update(n.length===0?"":`admin:${JSON.stringify(n)}`).digest("hex").slice(0,32)}var it,Ea,io,xp,Ep,Np=D(()=>{"use strict";Te();no();it="snoutpod.stack",Ea="snoutpod.stack-spec",io="stack",xp=16,Ep=16});import{createHash as po,randomBytes as rS}from"node:crypto";import{access as oS,constants as sS,mkdir as aS}from"node:fs/promises";import{delimiter as iS,join as Da}from"node:path";function qp(e,t,n,r){return po("sha256").update(Ma(e)).update(e.smtpSecretEnv&&t?po("sha256").update(t).digest("hex"):"").update(n===void 0?"":`router:${n}`).update(r===void 0?"":`@${r}`).digest("hex").slice(0,32)}function lS(e,t){return ga(t),po("sha256").update(`separated:${t}:${Yt}:`).update(qp(e)).digest("hex").slice(0,32)}function Ma(e){if(Array.isArray(e))return`[${e.map(Ma).join(",")}]`;if(e!==null&&typeof e=="object"){let t=e;return`{${Object.keys(t).sort().filter(r=>t[r]!==void 0).map(r=>`${JSON.stringify(r)}:${Ma(t[r])}`).join(",")}}`}return JSON.stringify(e??null)}function Qt(e){if(!cS.test(e))throw new Error(`not a service name: ${JSON.stringify(e)}`);return e}function Nt(e){if(!dS.test(e))throw new Error(`not a database role: ${JSON.stringify(e)}`);return e}function uS(e,t,n,r,o){if(r===void 0)return`postgres://${Nt(t)}:${n}@127.0.0.1:5432/${ae(e)}`;if(o!==void 0&&o!==St(e))throw new Error(`${o} is not ${e}'s router`);return`postgres://${`${Nt(t)}:${n}@${o??"127.0.0.1"}`}:${Ne(r,"router port")}/${ae(e)}?sslmode=disable`}function mo(e,t){if(!t.dbUriEnv||!t.dbUriRole||t.name===ge)return;let n=(e.services??[]).find(r=>r.name===ge);if(n)return _a.includes(t.dbUriRole)?n.port:Ze(e)?Yt:void 0}function ja(e,t){return Ze(e)&&mo(e,t)!==void 0?St(e.ref):void 0}function Dp(e,t,n,r){let o=ha(t.env,e.adminPassword);if(t.dbUriEnv&&t.dbUriRole){let s=t.dbUriRole,a=n.get(s);if(a===void 0)throw new Error(`no password was applied for role ${JSON.stringify(t.dbUriRole)}, which ${JSON.stringify(t.name)} needs to reach its database`);o[t.dbUriEnv]=uS(e.ref,s,a,mo(e,t),ja(e,t))}if(t.smtpSecretEnv&&r&&(o[t.smtpSecretEnv]=r),t.adminPortEnv&&t.adminPort){if(!zp.test(t.adminPortEnv))throw new Error(`${t.name}: not an environment variable name: ${JSON.stringify(t.adminPortEnv)}`);o[t.adminPortEnv]=String(Ne(t.adminPort,`${t.name}: adminPort`))}return o}function Mp(e,t){return`${B(e).pod}-${Qt(t)}`}function lo(e){return`${e}${Wp}`}function jp(e){return typeof e=="string"&&e.endsWith(Wp)}function Ia(e){let t=e.Names?.[0];return t===void 0?void 0:t.replace(/^\//,"")}function Ot(e){return`snoutpod-host-${Qt(e)}`}function pS(e,t){let n=Object.keys(e.env).sort().map(o=>[o,e.env[o]]),r=t&&(t.joins||(t.publishes?.length??0)>0)?[{joins:t.joins??null,publishes:t.publishes??[]}]:[];return po("sha256").update(JSON.stringify([e.image,e.port,e.hostPort,e.healthPath??null,e.memoryMb,n,e.adminPort??null,e.adminHostPort??null,e.command??null,e.volumes??null,e.network??null,...r])).digest("hex").slice(0,32)}function mS(e){return[...e.filter(t=>!t.joins),...e.filter(t=>t.joins)]}function fS(e){return Object.keys(e.secretEnv??{}).length>0||Object.keys(e.files??{}).length>0||!!e.envFrom||!!e.databaseUrl}function gS(e,t){return t.filter(n=>n.joins===e&&n.hostPort>0).map(n=>[n.hostPort,n.port]).sort((n,r)=>n[0]-r[0])}function Ua(e){return!e.adminPort||!e.adminHostPort?null:[Ne(e.adminHostPort,`${e.name}: adminHostPort`),Ne(e.adminPort,`${e.name}: adminPort`)]}function hS(e,t){let n=[],r=tn(t);for(let o of e??[]){if(Qt(o.name),Ne(o.port,`${o.name}: port`),o.hostPort===null||o.hostPort===void 0)continue;Ne(o.hostPort,`${o.name}: hostPort`);for(let a of r)n.push("--publish",`${a}:${o.hostPort}:${o.port}`);let s=Ua(o);if(s&&n.push("--publish",`127.0.0.1:${s[0]}:${s[1]}`),o.name===ge){let a=o.hostPort+eo;Ne(a,`${o.name}: admin hostPort`);for(let c of r)n.push("--publish",`${c}:${a}:${Qr}`)}}return n}function yS(e,t,n){if(ae(e.ref),t.name!==ge)throw new Error(`${t.name} is not the router`);if(!n.network)throw new Error(`${e.ref}: a separated router needs the pod network`);Ne(t.port,`${t.name}: port`);let r=ga(e.routerPodPort),o=([s,a])=>tn(n.bind).flatMap(c=>["--publish",`${c}:${Ne(s,"router hostPort")}:${a}`]);return["run","--detach","--name",St(e.ref),"--label",`${bt}=${e.ref}`,"--label",`${On}=${e.servicesGeneration??0}`,"--label",`${uo}=${n.fingerprint}`,"--network",n.network,"--userns=auto","--memory",`${t.memoryMb}m`,"--pids-limit",String(Yp),"--restart","no",...o([r.postgres,t.port]),...o([r.admin,Qr]),...o([r.passthrough,Yt]),...Jp(t),"--volume",`${Rn(e.ref)}:${to}:ro`,...n.envNames.flatMap(s=>["--env",s]),t.image]}function wS(e,t){if(ae(e.ref),!t.network)throw new Error(`${e.ref}: a services-only pod needs the pod network`);return["run","--detach","--name",Qe(e.ref),"--label",`${it}=${e.ref}`,"--label",`${Ea}=${t.fingerprint}`,"--network",t.network,"--userns=auto","--memory",`${xp}m`,"--pids-limit",String(Ep),"--restart","no","--stop-signal","SIGKILL",...Oa(e).flatMap(([n,r])=>tn(t.bind).flatMap(o=>["--publish",`${o}:${Ne(n,"stack hostPort")}:${Ne(r,"service port")}`])),...Ta(e).flatMap(([n,r])=>["--publish",`127.0.0.1:${Ne(n,"stack adminHostPort")}:${Ne(r,"admin port")}`]),"--entrypoint","sleep",e.image,"infinity"]}function bS(e){let t;try{t=JSON.parse(e.trim()||"null")}catch{return null}if(t===null)return[];if(typeof t!="object"||Array.isArray(t))return null;let n=[];for(let r of Object.values(t))if(Array.isArray(r))for(let o of r){let s=Number(o?.HostPort);Number.isInteger(s)&&s>0&&!n.includes(s)&&n.push(s)}return n}function tn(e){let t=e===void 0?[]:typeof e=="string"?[e]:[...e];return["127.0.0.1",...new Set(t.filter(n=>n!=="127.0.0.1"))]}function Ne(e,t){if(!Number.isInteger(e)||e<1||e>65535)throw new Error(`${t} is not a port: ${JSON.stringify(e)}`);return e}function co(e){let t=[];for(let n of Object.keys(e)){if(!zp.test(n))throw new Error(`not an environment variable name: ${JSON.stringify(n)}`);t.push("--env",n)}return t}function Jp(e,t={}){if(t.adminPublished&&e.adminHealthPath&&Ua(e)){if(!La.test(e.adminHealthPath))throw new Error(`${e.name}: not a health path: ${JSON.stringify(e.adminHealthPath)}`);return["--label",`${fo}=${e.adminHostPort}:${e.adminHealthPath}`]}let n=e.healthPath;if(n==null||n==="")return[];if(!La.test(n))throw new Error(`${e.name}: not a health path: ${JSON.stringify(n)}`);return e.hostPort===null||e.hostPort===void 0?[]:(Ne(e.hostPort,`${e.name}: hostPort`),["--label",`${fo}=${e.hostPort}:${n}`])}function SS(e){if(!e)return null;let t=e.indexOf(":");if(t<=0)return null;let n=Number(e.slice(0,t)),r=e.slice(t+1);return!Number.isInteger(n)||n<1||n>65535||!La.test(r)?null:{port:n,path:r}}async function kS(e,t={}){let n=t.path??process.env.PATH??"",r=t.look??(o=>oS(o,sS.X_OK));for(let o of n.split(iS))if(o)try{let s=Da(o,e);return await r(s),s}catch{}return null}function vS(){return["\\set ON_ERROR_STOP 0","set search_path = pg_catalog, pg_temp;","select 'create extension if not exists pg_prewarm schema public' where exists (select 1 from pg_available_extensions where name = 'pg_prewarm') \\gexec","select pg_catalog.format($snoutpod$with candidate as ("," select c.oid, pg_catalog.pg_relation_size(c.oid) as bytes"," from pg_class c join pg_namespace n on n.oid = c.relnamespace"," where c.relkind in ('r', 'i', 'm')"," and c.relpersistence = 'p'"," and n.nspname not in ('pg_catalog', 'information_schema', 'pg_toast')","), budgeted as ("," select oid, bytes,"," sum(bytes) over (order by bytes, oid rows between unbounded preceding and current row) as running"," from candidate",")","select coalesce(sum(%I.pg_prewarm(oid::pg_catalog.regclass, 'buffer'::pg_catalog.text, 'main'::pg_catalog.text, null::pg_catalog.int8, null::pg_catalog.int8)), 0) from budgeted","where bytes > 0 and running <= pg_size_bytes(current_setting('shared_buffers')) * 3 / 4$snoutpod$, n.nspname)","from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_prewarm' \\gexec",""].join(`
|
|
448
|
+
`)}function xS(e){return/^(\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com)\//.exec(e)?.[1]??null}function ES(e,t){for(let r of e??[]){let o=/^PG_MAJOR=(\d+)$/.exec(r.trim());if(o)return o[1]??null}let n=t.split("@")[0]??"";return/:(\d+)(?:[.-][^/:]*)?$/.exec(n)?.[1]??null}function Fa(e){let t=(e??"").trim();return/^\d+(?:\.\d+)?$/.test(t)?t:null}function AS(e,t){return t.image===null||t.data===null||t.image===t.data?null:`${e} was not started: its data directory was made by Postgres ${t.data} and it was handed a Postgres ${t.image} image. A data directory starts only on the major that made it. Its data is untouched, and it starts again on a ${t.data} image.`}async function RS(e,t,n,r){return{image:await TS(e,t,r),data:await OS(e,t,n)}}async function TS(e,t,n){let r=null;try{let o=await e(t,["image","inspect","--format","{{json .Config.Env}}",n]);if(o.code===0&&o.stdout.trim()){let s=JSON.parse(o.stdout.trim());r=Array.isArray(s)?s.filter(a=>typeof a=="string"):null}}catch{r=null}return ES(r,n)}async function OS(e,t,n){try{let r=await e(t,["volume","inspect","--format","{{.Mountpoint}}",B(n).pod]),o=r.code===0?r.stdout.trim().replace(/\/+$/,""):"";if(!o)return null;let s=await e(t,["unshare","cat",`${o}/PG_VERSION`]);return s.code===0?Fa(s.stdout):null}catch{return null}}function NS(e){return e.filter(t=>!!t.replicaPeer||t.guard!==void 0||t.hostRole==="standby").map(t=>({ref:t.ref,group:t.group??t.ref,container:B(t.ref).pod})).sort((t,n)=>t.ref<n.ref?-1:t.ref>n.ref?1:0)}function IS(e,t,n){if(t.size>1)return`the ${Pe} network already holds more than one project (${[...t].sort().join(", ")}), so nothing more joins it`;let r=t.size===1?[...t][0]:n;return e.group===r?null:`the ${Pe} network holds ${r}'s copies, and another project may not share its bridge`}function Fp(e,t){let n=tn(t);return(e.Ports??[]).some(r=>{let o=r.host_ip??"";return o!==""&&o!=="0.0.0.0"&&o!=="::"&&!n.includes(o)})}function LS(e){if(!e)return[];let t=[],n=/^[1-9][0-9]{0,6}(kB|MB|GB)$/;typeof e.sharedBuffers=="string"&&n.test(e.sharedBuffers)&&t.push(`SNOUTPOD_SHARED_BUFFERS=${e.sharedBuffers}`),typeof e.workMem=="string"&&n.test(e.workMem)&&t.push(`SNOUTPOD_WORK_MEM=${e.workMem}`);let r=typeof e.maxConnections=="number"&&Number.isInteger(e.maxConnections)&&e.maxConnections>=1&&e.maxConnections<=1e4?e.maxConnections:Bp;return t.push(`SNOUTPOD_MAX_CONNECTIONS=${Math.max(r,Bp)}`),typeof e.privilegedExtensions=="string"&&/^[a-z][a-z0-9_-]*(,[a-z][a-z0-9_-]*)*$/.test(e.privilegedExtensions)&&t.push(`SNOUTPOD_PRIVILEGED_EXTENSIONS=${e.privilegedExtensions}`),t}function US(e){let t=null;return()=>(t===null&&(t=n()),t);async function n(){if(e.runtime===null)return[];let r=e.runtime??_S;return e.runtime!==void 0&&e.runtime!==""?["--runtime",r]:await kS(r,{look:e.lookExecutable})===null?[]:["--runtime",r]}}function Xp(e){let t=e.binary??"podman",n=e.exec,r=US(e),o=e.probe??Vp;async function s(p){try{await n(t,["exec",...ke,"--interactive",B(p).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",p,"--quiet","--file","-"],{timeoutMs:PS,stdin:vS()})}catch{}}async function a(){let[p,f,y]=await Promise.all([N(n,t,["ps","--all","--filter",`label=${Oe}`,"--format","json"]),N(n,t,["ps","--all","--filter",`label=${bt}`,"--format","json"]),N(n,t,["ps","--all","--filter",`label=${it}`,"--format","json"])]),S=new Map;for(let H of y.trim()?JSON.parse(y):[]){let Q=h(H);Q&&S.set(Q,(S.get(Q)??!1)||H.State==="running")}let _=[...(p.trim()?JSON.parse(p):[]).flatMap(H=>{let Q=H.Labels?.[Ue]?H.Labels?.[it]:void 0;return Q===void 0?[H]:S.has(Q)?[S.get(Q)?H:{...H,State:"exited"}]:[]}),...(f.trim()?JSON.parse(f):[]).flatMap(H=>{let Q=H.Labels?.[bt];return Q?[{...H,Labels:{...H.Labels,[Oe]:Q,[Ue]:ge}}]:[]})],v=new Map,K=new Map,k=new Map,L=new Map,I=new Set([...S.keys(),...(f.trim()?JSON.parse(f):[]).flatMap(H=>H.Labels?.[bt]??[])]);for(let H of _){let Q=H.Labels?.[Oe],fe=H.Labels?.[Ue];if(!Q||!fe)continue;if(jp(Ia(H))){L.set(Q,[...L.get(Q)??[],fe]);continue}let G=v.get(Q)??[];G.push(fe),v.set(Q,G),Ca(H.State)==="stopped"&&K.set(Q,[...K.get(Q)??[],fe]);let de=Number(H.Labels?.[On]??0),pe=Number.isInteger(de)?de:0;k.set(Q,Math.max(k.get(Q)??pe,pe))}let X=_.filter(H=>H.Labels?.[Oe]&&!H.Labels?.[Ue]),se=X.map(H=>Ca(H.State)),U=await BS(X,FS,(H,Q)=>se[Q]==="running"?Promise.resolve(!0):e.hasData(H.Labels?.[Oe]??""));return X.map((H,Q)=>{let fe=H.Labels?.[Oe]??"",G=Number(H.Labels?.[Na]);return{ref:fe,status:se[Q]??"unknown",generation:Number.isInteger(G)?G:null,hasData:U[Q]??!1,services:v.get(fe)??[],servicesStopped:K.get(fe)??[],...I.has(fe)?{separated:!0}:{},...L.has(fe)?{setAside:L.get(fe)}:{},...Fp(H,e.podBind)?{boundElsewhere:!0}:{},...k.has(fe)?{servicesGeneration:k.get(fe)}:{}}})}async function c(){let p=await N(n,t,["ps","--all","--filter",`label=${Oe}`,"--format","json"]);return(p.trim()?JSON.parse(p):[]).filter(y=>y.Labels?.[Oe]&&!y.Labels?.[Ue]).length}async function i(p){let f=await(e.majors??((y,S)=>RS(n,t,y,S)))(p.ref,p.image);return AS(p.ref,f)}async function u(p){let f=B(p.ref),y=await i(p);if(y)throw new Error(y);let S=e.credentials,_=await rp(e.repository,S?()=>S.forProject(p.ref,p.bucket):null),v={SNOUTPOD_OWNER_PASSWORD:p.ownerPassword,POSTGRES_PASSWORD:p.adminPassword,...sp(_)},K=[...await r(),"run","--detach","--name",f.pod,"--label",`${Oe}=${p.ref}`,"--label",`${Na}=${p.generation}`,"--userns=auto","--memory",`${p.memoryMb}m`,"--pids-limit",String(CS),...GS(p.cpus),"--restart","no",...e.network?["--network",e.network]:[],...p.port?tn(e.podBind).flatMap(k=>["--publish",`${k}:${p.port}:5432`]):[],...hS(fa(p),e.podBind),"--volume",`${f.pod}:/var/lib/postgresql/data:U`,"--env","SNOUTPOD_OWNER_PASSWORD","--env","POSTGRES_PASSWORD","--env",`SNOUTPOD_REF=${p.ref}`,"--env","POSTGRES_USER=snoutpod_admin","--env","POSTGRES_DB=postgres","--env",`SNOUTPOD_S3_BUCKET=${p.bucket}`,"--env",`SNOUTPOD_S3_REGION=${p.region}`,...e.egressDenied?["--env","SNOUTPOD_EGRESS=denied"]:[],...KS(p.oauth),...LS(p.tuning).flatMap(k=>["--env",k]),"--env",`SNOUTPOD_RETENTION_FULL=${Zd(p.backup?.retentionDays,p.backup?.fullIntervalHours)}`,...op(_),p.image,"snoutpod-postgres"];await N(n,t,K,{env:v})}async function l(p){return(await P(p)).services}async function m(p){return(await l(p)).filter(f=>!f.setAside)}function h(p){return p.Labels?.[Ue]?void 0:p.Labels?.[it]}async function P(p){let[f,y,S]=await Promise.all([N(n,t,["ps","--all","--filter",`label=${Oe}=${ae(p)}`,"--filter",`label=${Ue}`,"--format","json"]),N(n,t,["ps","--all","--filter",`label=${bt}=${p}`,"--format","json"]),N(n,t,["ps","--all","--filter",`label=${it}=${p}`,"--format","json"])]),_=f.trim()?JSON.parse(f):[],v=[];for(let k of _){let L=k.Labels?.[Ue];if(!L)continue;let I=Number(k.Labels?.[On]??0),X=jp(Ia(k));v.push({name:L,container:X?Ia(k)??"":Mp(p,L),spec:k.Labels?.[uo]??"",pod:k.Labels?.[Ip]??"",running:k.State==="running",generation:Number.isInteger(I)?I:0,separate:!1,stack:k.Labels?.[it]===p,setAside:X})}for(let k of y.trim()?JSON.parse(y):[]){if(k.Labels?.[bt]!==p)continue;let L=Number(k.Labels?.[On]??0);v.push({name:ge,container:St(p),spec:k.Labels?.[uo]??"",pod:"",running:k.State==="running",generation:Number.isInteger(L)?L:0,separate:!0,stack:!1,setAside:!1})}let K=null;for(let k of S.trim()?JSON.parse(S):[])h(k)===p&&(K={id:k.Id??"",spec:k.Labels?.[Ea]??"",running:k.State==="running"});return{services:v,anchor:K}}async function A(p){let f=await n(t,["container","inspect","--format","{{.Id}}",p]);return f.code===0?f.stdout.trim():""}function ee(p,f){return f?.databaseOnly?{services:p.services.filter(y=>!y.separate&&!y.stack),anchor:null}:p}async function z(p,f){let y=[];for(let I of f){let X=I.dbUriRole??null;I.dbUriEnv&&X&&!y.includes(X)&&y.push(Nt(X))}let S=new Map;if(y.length===0)return S;let _=await e.fixedRolePasswords?.()??new Map,v={},K=[],k=[];y.some(I=>_a.includes(I))&&k.push(ip,lp);for(let I of y.filter(X=>ka.includes(X)))k.push(cp(I));let L=new Map;if(p.derivedServicePasswords===!0)for(let I of y.filter(ya))L.set(I,await wa(p.adminPassword,I));return y.forEach((I,X)=>{let se=_.get(I);if(se!==void 0){S.set(I,se);return}let U=L.get(I)??rS(24).toString("base64url"),H=`SNOUTPOD_SERVICE_PASSWORD_${X}`;S.set(I,U),v[H]=U,K.push("--env",H),k.push(`\\getenv service_password_${X} ${H}`),k.push(`\\set service_role_${X} ${I}`),k.push(`alter role :"service_role_${X}" with password :'service_password_${X}';`)}),k.push(""),await N(n,t,["exec",...ke,"--interactive",...K,B(p.ref).pod,"psql","--username","snoutpod_admin","--dbname",B(p.ref).database,"--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:v,stdin:k.join(`
|
|
449
|
+
`)}),S}async function le(p){let f=Rn(p.ref);await N(n,t,["volume","create","--ignore",f]),await N(n,t,[...await r(),"run","--rm","--network","none","--volume",`${f}:${to}`,"--entrypoint","bash",p.image,"-c",Du])}async function C(p){let f=await r();if((await n(t,[...f,"image","exists",p])).code===0)return;let y=await n(t,[...f,"pull","--quiet",p]);if(y.code===0)return;let S=xS(p);if(!S||!e.registryLogin)throw new Error(`${p} could not be pulled: ${(y.stderr||y.stdout).trim()}`);await e.registryLogin(S),await N(n,t,[...f,"pull","--quiet",p])}async function ce(p){let f=await n(t,["container","inspect","--format","{{json .HostConfig.PortBindings}}",B(p).pod]);return f.code===0?bS(f.stdout):null}async function j(p,f={}){let y=p.services??[],S=Ze(p)?[...y.filter($=>$.name===ge),...y.filter($=>$.name!==ge)]:y,{services:_,anchor:v}=await P(p.ref),K=_.filter($=>!$.setAside),k;S.some($=>$.smtpSecretEnv)&&(k=await e.smtpSecret?.().catch(()=>{}));let L=new Map(K.map($=>[$.name,$])),I=$=>Ze(p)&&$.name===ge,X=S.filter($=>Tt(p,$)),se="";if(X.length>0){if(!S.some($=>$.name===ge))throw new Error(`${p.ref}: a services-only pod reaches its database through the router, and spec.services has no ${ge}`);if(!e.network)throw new Error(`${p.ref}: a services-only pod needs the pod network`);if(X.some($=>$.name===io))throw new Error(`${p.ref}: a service may not be called ${io}, which is the services-only pod's own name`);se=Op(p,e.network)}let U=X.length>0&&v!==null&&v.running&&v.id!==""&&v.spec===se,H=S.some($=>!I($)&&!Tt(p,$))?await A(B(p.ref).pod):"",Q=S.some($=>!I($)&&!Tt(p,$)&&Ua($))?new Set(await ce(p.ref)??[]):new Set,fe=U?v?.id??"":"",G=$=>Tt(p,$)?fe:H,de=new Map(S.map($=>[$.name,I($)?lS($,p.routerPodPort):qp(Tt(p,$)?Ra(p,$):$,k,mo(p,$),ja(p,$))])),pe=new Set(S.filter($=>{let Be=L.get($.name);if(!Be||!Be.running||Be.spec!==de.get($.name))return!0;let He=G($);return!I($)&&(He===""||Be.pod!==He)}).map($=>$.name));if(f.renewPasswords)for(let $ of S)$.dbUriEnv&&$.dbUriRole&&pe.add($.name);let Ao=p.servicesGeneration??0;if(pe.size===0&&S.length>0&&!K.some($=>$.generation>=Ao))for(let $ of S)pe.add($.name);let oi=$=>$.dbUriEnv&&$.dbUriRole?$.dbUriRole:null;for(let $=!0;$;){$=!1;let Be=new Set(S.filter(He=>pe.has(He.name)).map(oi));for(let He of S){let Fn=oi(He);!pe.has(He.name)&&Fn!==null&&Be.has(Fn)&&(pe.add(He.name),$=!0)}}let si=new Set(S.filter($=>pe.has($.name)).map($=>$.image));(X.length>0&&!U||pe.has(ge))&&si.add(p.image);for(let $ of si)await C($);let lf=$=>f.keepSeparated===!0&&($.separate||$.stack)&&!pe.has($.name);for(let $ of K)(pe.has($.name)||!de.has($.name))&&!lf($)&&await N(n,t,["rm","--force","--ignore",$.container]);if(v!==null&&!U&&!(f.keepSeparated===!0&&X.length===0)&&await N(n,t,["rm","--force","--ignore",Qe(p.ref)]),X.length>0&&!U){if(await N(n,t,[...await r(),...wS(p,{network:e.network,bind:e.podBind,fingerprint:se})]),fe=await A(Qe(p.ref)),fe==="")throw new Error(`${p.ref}: the services-only pod was made and its id could not be read`);await f.afterStart?.(Qe(p.ref))}let Ro=S.filter($=>pe.has($.name)),To=await z(p,Ro);Ro.some($=>$.name===ge)&&await le(p);for(let $ of Ro){if(I($)){let ai=ha(Iu(p.ref,$.env),p.adminPassword);await N(n,t,[...await r(),...yS(p,$,{network:e.network,bind:e.podBind,fingerprint:de.get($.name)??"",envNames:co(ai).filter(df=>df!=="--env")})],{env:ai}),await f.afterStart?.(St(p.ref));continue}let Be=Tt(p,$),He=Be?Qe(p.ref):B(p.ref).pod,Fn=Mp(p.ref,$.name),cf=[...await r(),"run","--detach","--name",Fn,"--label",`${Oe}=${p.ref}`,"--label",`${Na}=${p.generation}`,"--label",`${Ue}=${$.name}`,"--label",`${On}=${p.servicesGeneration??0}`,"--label",`${uo}=${de.get($.name)??""}`,"--label",`${Ip}=${G($)}`,...Be?["--label",`${it}=${p.ref}`]:[],"--network",`container:${He}`,"--userns",`container:${He}`,"--memory",`${$.memoryMb}m`,"--pids-limit",String(Yp),"--restart","no",...Jp(Be?Ra(p,$):$,{adminPublished:Be||$.adminHostPort!==null&&$.adminHostPort!==void 0&&Q.has($.adminHostPort)}),...$.name===ge?["--volume",`${Rn(p.ref)}:${to}:ro`]:[],...co(Dp(p,$,To,k)),$.image];pe.has(ge)||await be(p,$,To),await N(n,t,cf,{env:Dp(p,$,To,k)})}}async function be(p,f,y){let S=mo(p,f),_=f.dbUriRole,v=_?y.get(_):void 0;if(S===void 0||S===Yt||!_||v===void 0)return;let K=B(p.ref),k=ja(p,f)??"127.0.0.1",{waitMs:L,retryMs:I}=e.routerLogin??{waitMs:DS,retryMs:MS},X=Date.now()+L;for(let se=1;;se++){let U=await n(t,["exec","--env","PGPASSWORD","--env","PGSSLMODE=disable","--env","PGCONNECT_TIMEOUT=3",K.pod,"psql","--host",k,"--port",String(S),"--username",Nt(_),"--dbname",K.database,"--no-password","--tuples-only","--no-align","--command","select 1"],{env:{PGPASSWORD:v}});if(U.code===0)return;if(Date.now()>=X){let H=`${U.stderr}
|
|
450
|
+
${U.stdout}`.trim().split(`
|
|
451
|
+
`).filter(Boolean).pop()??`exit ${U.code}`;throw new Error(`${p.ref}: the router did not take ${_}'s new password within ${L}ms (${se} tries; last: ${H}), so ${f.name} was not started`)}await new Promise(H=>setTimeout(H,I))}}async function Z(p,f){if(f%6!==0)return!1;let y=await n(t,["container","inspect","--format","{{.State.Status}}",B(p).pod]);return y.code===0&&/^(exited|stopped|dead)$/.test(y.stdout.trim())}function ne(p){return["exec",...ke,B(p).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname","postgres"]}async function Ln(p){let f=await n(t,[...ne(p),"-tAc",fu]);return f.code===0?gu(f.stdout):null}async function $t(p){let f=await n(t,[...ne(p),"-tAc","select pg_is_in_recovery()"]);if(f.code!==0)return null;let y=f.stdout.trim();return y==="t"?!0:y==="f"?!1:null}async function Un(p,f){let y=K=>n(t,[...ne(p),"-tAc",K]),S=async()=>{let K=await y("select case when pg_is_in_recovery() then 'r' when (select timeline_id from pg_control_checkpoint()) = ('x' || substr(pg_walfile_name(pg_current_wal_lsn()), 1, 8))::bit(32)::int then 't' else 'f' end"),k=K.stdout.trim();return K.code===0&&(k==="t"||k==="f"||k==="r")?k:null},_=await S();if(_!=="f")return _==="t";let v=Date.now()+f;do{if((await y("checkpoint")).code!==0)return!1;if(await S()==="t")return!0;await new Promise(k=>setTimeout(k,1e3))}while(Date.now()<v);return!1}async function Km(p){let f=await n(t,[...ne(p),"-tAc",eu],{timeoutMs:_t});return f.code===0?tu(f.stdout):null}async function Gm(p,f){let y=new Map;for(let _ of f)ae(_),y.set(_,B(_).pod);if(y.size===0)return new Map;let S=await n(t,["exec",B(p).pod,"timeout",String(10),"sh","-c",pu,"sh",...y.values()]);return S.code===0?mu(S.stdout,y):null}async function qm(p,f){ae(f);let y=[];for(let K of[p,f]){let k=B(K).pod,L=await n(t,["container","inspect","--format","{{.State.Status}}",k]);if(L.code===0&&L.stdout.trim()==="running")throw new Error(`${k} is still running, so its data was not moved`);let I=await n(t,["volume","inspect","--format","{{.Mountpoint}}",k]),X=I.code===0?I.stdout.trim().replace(/\/+$/,""):"";if(!X)throw new Error(`the volume ${k} could not be found, so nothing was moved`);y.push(X)}let _=typeof process.getuid!="function"||process.getuid()!==0?await n(t,["unshare","sh","-c",na,"sh",y[0],y[1]]):await n("sh",["-c",na,"sh",y[0],y[1]]),v=au(_.stdout,_.code);if(v)throw new Error(v)}async function Wm(p){return(await n(t,[...ne(p),"-v","ON_ERROR_STOP=1",...ra.flatMap(y=>["-c",y])])).code===0}async function zm(p){let f=e.guardRoot;if(!f)return;let y=await N(n,t,["ps","--all","--filter",`label=${ao}`,"--format","json"]),S=new Map((y.trim()?JSON.parse(y):[]).flatMap(v=>{let K=v.Labels?.[ao];return K?[[K,{fingerprint:v.Labels?.[xa]??"",status:Ca(v.State),startedAt:v.StartedAt??0}]]:[]})),_=new Set(p.map(({spec:v})=>v.ref));for(let v of S.keys())_.has(v)||await N(n,t,["rm","--force","--ignore",Zt(v)]);for(let{spec:v,guard:K}of p){if(v.port===null)continue;let k=wp(K,v.port,v.generation),L=S.get(v.ref);if(L?.fingerprint===k){if(L.status!=="running")await N(n,t,["start",Zt(v.ref)]);else{let se=Da(f,v.ref,"status.json"),U=await n(t,["unshare","stat","-c","%Y",se]),H=U.code===0?Number(U.stdout.trim()):null;Sp(L.startedAt,Number.isFinite(H)?H:null,Math.floor(Date.now()/1e3),K)&&await N(n,t,["restart",Zt(v.ref)])}continue}await N(n,t,["rm","--force","--ignore",Zt(v.ref)]);let I=Da(f,v.ref);await aS(I,{recursive:!0,mode:448}),await C(K.image);let X=yp(K,v.port,v.adminPassword);await N(n,t,[...await r(),...bp(v.ref,K,{hostDir:I,fingerprint:k,envNames:co(X).filter(se=>se!=="--env")})],{env:X})}}async function Jm(p,f){return(await n(t,[...ne(p),"-v","ON_ERROR_STOP=1",...ru(f).flatMap(S=>["-c",S])],{timeoutMs:_t})).code===0}async function Vm(){let p=e.network;if(!p)return null;let f=(await N(n,t,["network","inspect",p,"--format","{{range .Subnets}}{{.Subnet}} {{end}}"])).trim().split(" ")[0]??"";if(f==="")return null;let y=await N(n,t,["ps","--format","json"]),_=(y.trim()?JSON.parse(y):[]).map(k=>{let L=k.Labels?.[Oe],I=k.Labels?.[en],X=k.Labels?.[bt];if(X)return{name:St(X),ref:X,service:!1};let se=k.Labels?.[it];if(se&&!k.Labels?.[Ue])return{name:Qe(se),ref:se,service:!1};let U=k.Labels?.[kp],H=k.Labels?.[vp];if(U&&(!H||H===ge)){let Q=$p(U);return{name:H?Q.router:Q.anchor,ref:U,service:!1}}return L&&!k.Labels?.[Ue]?{name:B(L).pod,ref:L,service:!1}:I?{name:Ot(I),ref:I,service:!0}:null}).filter(k=>k!==null);if(_.length===0)return{subnet:f,containers:[]};let v=await N(n,t,["inspect","--format",`{{.Name}} {{with index .NetworkSettings.Networks "${p}"}}{{.IPAddress}}{{end}}`,..._.map(k=>k.name)]),K=new Map(v.split(`
|
|
452
|
+
`).map(k=>k.trim().split(" ")).filter(k=>k.length===2&&k[1]!=="").map(([k,L])=>[k,L]));return{subnet:f,containers:_.flatMap(k=>K.has(k.name)?[{ref:k.ref,ip:K.get(k.name),service:k.service}]:[])}}async function Ym(p,f){if(!e.quotaRequests||!Number.isInteger(f)||f<1)return!1;let{writeFile:y,rename:S}=await import("node:fs/promises"),_=B(p).pod,v=`${e.quotaRequests}/${_}`;return await y(`${e.quotaRequests}/.${_}.tmp`,`${f}
|
|
453
|
+
`),await S(`${e.quotaRequests}/.${_}.tmp`,v),!0}async function ti(p,f,y){if(!Number.isInteger(y)||y<1)return!1;let S=await n(t,[...ne(p),"-tAc",`select setting::bigint * pg_size_bytes(case when unit ~ '^[0-9]' then unit else '1' || unit end) / 1048576 from pg_settings where name = '${f}'`]);return S.code===0&&S.stdout.trim()===String(y)?!0:(await n(t,[...ne(p),"-v","ON_ERROR_STOP=1","-c",`alter system set ${f} = '${y}MB'`,"-c","select pg_reload_conf()"],{timeoutMs:_t})).code===0}function Xm(p,f){return ti(p,"temp_file_limit",f)}function Zm(p,f){return ti(p,"max_wal_size",f)}async function Qm(p){return(await n(t,["unshare","--rootless-netns",$S,"-f","-"],{stdin:p,timeoutMs:_t})).code===0}let Po=null,xo=null,Eo=new Map;async function ef(p,f){let y=p.join(",");if(Po===y)return!0;let S=await n(t,["network","inspect",Pe]);if(S.code===0&&Eu(S.stdout,p))return Po=y,f("network",null),!0;if(S.code===0){let v=(await N(n,t,["ps","--all","--filter",`network=${Pe}`,"--format","{{.Names}}"])).trim();if(v!=="")return f("network",`WARNING: the ${Pe} network does not route exactly to ${p.join(", ")}, and ${v.split(`
|
|
454
|
+
`).join(", ")} is on it, so it is left as it is: remake it once nothing is connected`),!1;await N(n,t,["network","rm",Pe])}let _=await n(t,xu(p));return _.code!==0?(f("network",`WARNING: the ${Pe} network could not be made: ${(_.stderr||_.stdout).trim().split(`
|
|
455
|
+
`)[0]}`),!1):(Po=y,f("network",null),f("made",null),f("made",`made the ${Pe} network, routing only to ${p.join(", ")}`),!0)}async function tf(p){let f=[...new Set(e.peers??[])].sort(),y=NS(p);if(f.length===0||y.length===0)return[];let S=[],_=(G,de)=>{de===null?Eo.delete(G):Eo.get(G)!==de&&(Eo.set(G,de),S.push(de))},v=await N(n,t,["ps","--format","json"]),K=new Set((v.trim()?JSON.parse(v):[]).flatMap(G=>G.Names??[])),k=y.filter(G=>K.has(G.container));if(k.length===0)return S;let L=f.join(",");if(xo?.key!==L){let G=e.network?await n(t,["network","inspect",e.network]):null,de=G?.code===0?Au(G.stdout,f):[];xo={key:L,peers:f.filter(pe=>!de.includes(pe))}}let I=xo.peers;if(I.length===0||!await ef(I,_))return S;let X=await N(n,t,["inspect","--format","{{.Name}} {{range $name, $value := .NetworkSettings.Networks}}{{$name}} {{end}}",...k.map(G=>G.container)]),se=new Set(X.split(`
|
|
456
|
+
`).map(G=>G.trim().split(/\s+/)).filter(G=>G.slice(1).includes(Pe)).map(G=>G[0])),U=k.filter(G=>!se.has(G.container));if(U.length===0)return S;let H=new Map(p.map(G=>[G.ref,G.group??G.ref])),Q=await N(n,t,["ps","--all","--filter",`network=${Pe}`,"--format","json"]),fe=new Set((Q.trim()?JSON.parse(Q):[]).map(G=>G.Labels?.[Oe]).filter(G=>!!G).map(G=>H.get(G)??G));for(let G of U){let de=IS(G,fe,U[0].group);if(de){_(G.container,`${G.container} does not join ${Pe}: ${de}`);continue}let pe=await n(t,["network","connect",Pe,G.container]);if(pe.code!==0){_(G.container,`WARNING: ${G.container} could not join the ${Pe} network, so it has no route to ${I.join(", ")}: `+(pe.stderr||pe.stdout).trim().split(`
|
|
457
|
+
`)[0]);continue}fe.add(G.group),_(G.container,null),S.push(`${G.container} joined the ${Pe} network: its route to ${I.join(", ")} comes from there`)}return S}async function nf(p,f){let y=await n(t,[...ne(p),"-tA","-v","ON_ERROR_STOP=1",...nu(f).flatMap(_=>["-c",_])],{timeoutMs:_t}),S=Number(y.stdout.trim().split(`
|
|
458
|
+
`).pop());return y.code===0&&Number.isInteger(S)?S:null}async function rf(p){let f=await n(t,[...ne(p),"-tAc","select pg_read_file(current_setting('ssl_cert_file')) where current_setting('ssl') = 'on'"]),y=`${f.stdout.trim()}
|
|
459
|
+
`;return f.code===0&&qr.test(y)?y:null}async function of(p,f){if(!ta.test(f))throw new Error("not a SCRAM-SHA-256 verifier");return ni(p,{statements:[`do $$ begin if not exists (select 1 from pg_roles where rolname = '${wt}') then create role ${wt} with login replication; end if; end $$`,`alter role ${wt} with login replication noinherit password '${f}'`],role:wt,line:`hostssl replication ${wt} all scram-sha-256`,replaceLine:!1})}async function sf(p,f,y){return ni(p,{statements:su(f),role:Vt,line:ou(y),replaceLine:!0})}async function ni(p,f){let y=B(p).pod,S=[...ne(p),"-v","ON_ERROR_STOP=1"];if((await n(t,[...S,...f.statements.flatMap(I=>["-c",I])],{timeoutMs:_t})).code!==0)return!1;let v=await n(t,[...S,"-tAc","show hba_file"]),K=v.stdout.trim();return v.code!==0||!/^\/[A-Za-z0-9/_.-]+$/.test(K)||(await n(t,["exec",y,"sh","-c",f.replaceLine?'{ grep -Ev "^host(ssl)?[[:space:]]+replication[[:space:]]+$2[[:space:]]" "$1"; printf "%s\\n" "$3"; } > "$1.next" && cat "$1.next" > "$1" && rm -f "$1.next"':'grep -Eq "^host(ssl)?[[:space:]]+replication[[:space:]]+$2[[:space:]]" "$1" || printf "%s\\n" "$3" >> "$1"',"sh",K,f.role,f.line])).code!==0||!await ri(p,K.replace(/\/[^/]+$/,""))?!1:(await n(t,[...S,"-tAc","select pg_reload_conf()"])).code===0}async function ri(p,f){let y=B(p).pod,S=[...ne(p),"-v","ON_ERROR_STOP=1"],_=await n(t,[...S,"-tAc","show ssl"]);return _.code===0&&_.stdout.trim()!=="on"&&((await n(t,["exec","--user","root",y,"sh","-c",'[ -s "$1/server.key" ] || { openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj "/CN=$2" -keyout "$1/server.key" -out "$1/server.crt" >/dev/null 2>&1 && chown postgres:postgres "$1/server.key" "$1/server.crt" && chmod 0600 "$1/server.key"; }',"sh",f,y])).code!==0||(await n(t,[...S,"-c","alter system set ssl_cert_file = 'server.crt'","-c","alter system set ssl_key_file = 'server.key'","-c","alter system set ssl = on"])).code!==0)?!1:_.code===0}async function af(p,f){if(!qr.test(f.certificate))throw new Error("the primary's certificate is not one PEM certificate");let y=B(p).pod;for(let[S,_]of[[iu,cu(f.password)],[lu,f.certificate]]){let v=await n(t,["exec","--interactive","--user","postgres",y,"sh","-c",'umask 077 && mkdir -p "$(dirname "$1")" && cat > "$1.next" && mv -f "$1.next" "$1"',"sh",S],{stdin:_,timeoutMs:_t});if(v.code!==0)throw new Error(`${S} could not be written in ${y}: ${(v.stderr||v.stdout).trim().slice(0,200)}`)}}return{list:a,ensureImage:C,inRecovery:$t,replayLsn:Ln,settleTimeline:Un,podHealth:Km,setSyncNames:Jm,...e.guardRoot?{syncGuards:zm}:{},dropKeeperSlots:nf,bridgeContainers:Vm,applyPodIsolation:Qm,joinPeerNetwork:tf,requestDiskQuota:Ym,ensureTempFileLimit:Xm,ensureMaxWalSize:Zm,ensureKeeperRole:of,ensureReplicaRole:sf,writeFollowFiles:af,podCertificate:rf,swapHomeData:qm,promoteStandby:Wm,nodesAnswer:Gm,preflight:i,create:u,createServices:j,async start(p){let f=(await l(p).catch(()=>[])).filter(y=>y.running&&!y.separate&&!y.stack).map(y=>y.container);f.length>0&&await N(n,t,["stop","--time","5",...f]),await N(n,t,["start",B(p).pod])},async startServices(p){let{services:f,anchor:y}=await P(p);if(y!==null&&!y.running){let _=f.filter(v=>v.stack&&v.running).map(v=>v.container);_.length>0&&await N(n,t,["stop","--time","5",..._]),await N(n,t,["start",Qe(p)])}let S=f.filter(_=>!_.stack||y!==null).map(_=>_.container);S.length>0&&await N(n,t,["start",...S])},async publishedPorts(p){return ce(p)},async setAsideSidecars(p){let f=(await l(p)).filter(y=>!y.separate&&!y.stack&&!y.setAside);for(let y of f)await N(n,t,["rm","--force","--ignore",lo(y.container)]),await N(n,t,["rename",y.container,lo(y.container)]);return f.map(y=>y.name)},async setAsideStackServices(p){let f=(await l(p)).filter(y=>y.stack&&!y.setAside);for(let y of f)await N(n,t,["rm","--force","--ignore",lo(y.container)]),await N(n,t,["rename",y.container,lo(y.container)]);return f.map(y=>y.name)},async dropSetAside(p){let f=(await l(p)).filter(y=>y.setAside).map(y=>y.container);f.length>0&&await N(n,t,["rm","--force","--ignore",...f])},async routerAnswers(p,f){let y=B(p),S=Date.now()+f;for(;;){if((await n(t,["exec",y.pod,"pg_isready","--host",St(p),"--port",String(ma),"--dbname",y.database,"--timeout","2"])).code===0)return!0;if(Date.now()>=S)return!1;await new Promise(v=>setTimeout(v,500))}},async applyHostServices(p){let f=[],y=await N(n,t,["ps","--all","--filter",`label=${en}`,"--format","json"]),S=y.trim()?JSON.parse(y):[],_=new Map;for(let k of S){let L=k.Labels?.[en];L&&_.set(L,{spec:k.Labels?.[Lp]??"",state:k.State??"",elsewhere:Fp(k,e.podBind),joins:k.Labels?.[Up]||null})}let v=new Set;for(let k of mS(p)){let L=_.get(k.name);_.delete(k.name);try{await K(k,L)}catch(I){f.push({name:k.name,action:"failed",reason:I instanceof Error?I.message:String(I)})}}async function K(k,L){let I=k.joins?Qt(k.joins):null;if(I!==null&&!p.some(de=>de.name===I&&!de.joins))throw new Error(`it joins ${I}, which this host does not run`);let X=e.composeHostService?await e.composeHostService(k):k;if(fS(X))throw new Error("it names host secrets, and this host cannot resolve them");let se=e.hostServiceLocal?.(X,p),U=se?{...X,...se.env&&Object.keys(se.env).length>0?{env:{...X.env,...se.env}}:{},...se.volumes?{volumes:se.volumes}:{},...se.network?{network:se.network}:{}}:X,H=Ot(U.name),Q=I===null?gS(U.name,p):[],fe=pS(U,{joins:I,publishes:Q});if(L&&L.spec===fe&&!(L.state!=="running"&&L.elsewhere)){L.state!=="running"?(await N(n,t,["start",H]),f.push({name:U.name,action:"started",reason:L.state}),v.add(U.name)):I!==null&&v.has(I)&&(await N(n,t,["restart",H]),f.push({name:U.name,action:"started",reason:`${I}, whose network it shares, was started again`}));return}let G=L?L.spec===fe?"published on another instance's address":"the spec changed":"it was not here";if(await C(U.image),L){for(let[de,pe]of _)pe.joins===U.name&&(await N(n,t,["rm","--force","--ignore",Ot(de)]),_.delete(de));await N(n,t,["rm","--force","--ignore",H])}await N(n,t,[...await r(),"run","--detach","--name",H,"--label",`${en}=${U.name}`,"--label",`${Lp}=${fe}`,...I!==null?["--label",`${Up}=${I}`]:[],...U.healthPath&&U.hostPort>0?["--label",`${fo}=${U.hostPort}:${U.healthPath}`]:[],"--memory",`${U.memoryMb}m`,"--pids-limit",String(jS),"--restart","no",...I!==null?["--network",`container:${Ot(I)}`]:U.network?["--network",U.network]:e.network?["--network",e.network]:[],...(U.volumes??[]).flatMap(de=>["--volume",de]),...I===null?[...U.hostPort>0?[[U.hostPort,U.port]]:[],...Q].flatMap(([de,pe])=>tn(e.podBind).flatMap(Ao=>["--publish",`${Ao}:${de}:${pe}`])):[],...U.adminPort&&U.adminHostPort?["--publish",`127.0.0.1:${U.adminHostPort}:${U.adminPort}`]:[],...co(U.env),U.image,...U.command??[]],{env:{...U.env}}),f.push({name:U.name,action:L?"recreated":"created",reason:G}),I===null&&v.add(U.name)}for(let k of[..._.keys()].sort((L,I)=>+!_.get(L)?.joins-+!_.get(I)?.joins))await N(n,t,["rm","--force","--ignore",Ot(k)]),f.push({name:k,action:"removed",reason:"it is no longer on the list"});return f},async hostServiceHealth(p){let f=await N(n,t,["ps","--all","--filter",`label=${en}`,"--format","json"]),y=(f.trim()?JSON.parse(f):[]).map(_=>_.Labels?.[en]??"").filter(_=>_!=="");if(y.length===0)return[];let S=Date.now()+p;for(;;){let _=[];for(let v of y)_.push(await Hp(n,t,v,Ot(v),o));if(_.every(v=>v.ok)||Date.now()>=S)return _;await new Promise(v=>setTimeout(v,500))}},async serviceHealth(p,f){let y=await m(p);if(y.length===0)return[];let S=Date.now()+f;for(;;){let _=await Promise.all(y.map(v=>Hp(n,t,v.name,v.container,o)));if(_.every(v=>v.ok)||Date.now()>=S)return _;await new Promise(v=>setTimeout(v,500))}},async drain(p,f,y){await new Promise(_=>setTimeout(_,f)),await n(t,[...ne(p),"-c","checkpoint"],{timeoutMs:_t});let S=Date.now()+y;for(;;){let _=await n(t,[...ne(p),"-tAc","select count(*) filter (where state <> 'idle') from (select state, case when state = 'idle' then pg_terminate_backend(pid) end from pg_stat_activity where backend_type = 'client backend' and pid <> pg_backend_pid()) s"]),v=_.code===0&&Number(_.stdout.trim())||0;if(v===0||Date.now()>=S)return{stillRunning:v};await new Promise(K=>setTimeout(K,250))}},async stop(p,f,y){let{services:S,anchor:_}=ee(await P(p),y);for(let v of S)await N(n,t,["stop","--time",String(Cp),v.container]);_!==null&&await N(n,t,["stop","--time",String(Cp),Qe(p)]),await N(n,t,["stop","--time",String(f),B(p).pod])},async remove(p,f){let{services:y,anchor:S}=ee(await P(p),f);for(let _ of y)await N(n,t,["rm","--force","--ignore",_.container]);S!==null&&await N(n,t,["rm","--force","--ignore",Qe(p)]),await N(n,t,["rm","--force","--ignore","--volumes",B(p).pod])},async removeData(p){await N(n,t,["volume","rm","--force",B(p).pod]);let f=Rn(p);(await n(t,["volume","exists",f])).code===0&&await N(n,t,["volume","rm","--force",f])},async applyOwnerPassword(p){let f=await n(t,["exec",...ke,B(p.ref).pod,"psql","--username","snoutpod_admin","--dbname","postgres","-tAc",`select rolpassword from pg_authid where rolname = '${ae(p.ref)}_owner'`]);if(f.code===0&&uu(f.stdout,p.ownerPassword))return;let y=["\\getenv owner_password SNOUTPOD_OWNER_PASSWORD",`\\set owner_role ${p.ref}_owner`,`alter role :"owner_role" with password :'owner_password';`,""].join(`
|
|
460
|
+
`);await N(n,t,["exec",...ke,"--interactive","--env","SNOUTPOD_OWNER_PASSWORD",B(p.ref).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:{SNOUTPOD_OWNER_PASSWORD:p.ownerPassword},stdin:y})},async applyRolePassword(p,f,y){let S=["\\getenv role_password SNOUTPOD_ROLE_PASSWORD",`\\set target_role ${Nt(f)}`,`alter role :"target_role" with password :'role_password';`,""].join(`
|
|
461
|
+
`);await N(n,t,["exec",...ke,"--interactive","--env","SNOUTPOD_ROLE_PASSWORD",B(p).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:{SNOUTPOD_ROLE_PASSWORD:y},stdin:S})},async applyRouterLogin(p,f){if(await N(n,t,["exec",...ke,"--interactive","--env","SNOUTPOD_ROUTER_PASSWORD",B(p).pod,"psql","--username","snoutpod_admin","--dbname",ae(p),"--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:{SNOUTPOD_ROUTER_PASSWORD:f??""},stdin:Cu(p,f!==null)}),f!==null){let y=[...ne(p),"-v","ON_ERROR_STOP=1"],S=await n(t,[...y,"-tAc","show data_directory"]),_=S.stdout.trim();if(S.code!==0||!/^\/[A-Za-z0-9/_.-]+$/.test(_)||!await ri(p,_))throw new Error(`${p}: TLS for the copies' routers could not be turned on`);await N(n,t,[...y,"-tAc","select pg_reload_conf()"])}},async updateExtensions(p){let f=B(p),y=await n(t,["exec",...ke,f.pod,"psql","--username","snoutpod_admin","--dbname","postgres","-tAc",`select datname from pg_database where datname = '${f.database}' or (not datistemplate and datname <> 'postgres') order by datname = '${f.database}' desc, datname limit 1`]),S=y.code===0&&/^[a-z][a-z0-9_]{0,62}$/.test(y.stdout.trim())?y.stdout.trim():f.database,_=await N(n,t,["exec",...ke,"--interactive",f.pod,"psql","--username","snoutpod_admin","--dbname",S,"--quiet","-tA","-F",$a,"-v","ON_ERROR_STOP=1","--file","-"],{stdin:up});return pp(_)},async applyDbAccess(p,f){let y=B(p),S=await N(n,t,["exec",...ke,"--interactive","--env",oo,y.pod,"psql","--username","snoutpod_admin","--dbname",y.database,"--quiet","-tA","-v","ON_ERROR_STOP=1","--file","-"],{env:{[oo]:Ju(p,f)},stdin:Wu,timeoutMs:6e4});return zu(S)},async runProjectSql(p,f){let y=B(p);await N(n,t,["exec",...ke,"--interactive",y.pod,"psql","--username","snoutpod_admin","--dbname",y.database,"--quiet","-v","ON_ERROR_STOP=1","--file","-"],{stdin:f})},async ensureHostServiceDatabase(p,f,y){let S=Ot(p),_=Nt(f),v=Nt(y),K=L=>["exec",S,"psql","--username",_,"--dbname","postgres","--quiet","-tA","-v","ON_ERROR_STOP=1","-c",L];(await N(n,t,K(`select 1 from pg_database where datname = '${v}'`))).trim()!=="1"&&await N(n,t,K(`create database "${v}"`))},async renameProject(p,f){f!==p.ref&&await N(n,t,["exec",...ke,"--interactive",B(p.ref).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{stdin:HS(f,p.ref)})},async waitReady(p,f){let y=B(p).pod,S=Date.now()+f,_=0;for(;;){let v=await n(t,["exec",...ke,y,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname","postgres","-tAc","select not pg_is_in_recovery()"]);if(v.code===0&&v.stdout.trim()==="t")return s(p),!0;if(Date.now()>=S||await Z(p,++_))return!1;await new Promise(K=>setTimeout(K,500))}},async waitStandby(p,f){let y=Date.now()+f,S=0;for(;;){if(await $t(p)===!0)return!0;if(Date.now()>=y||await Z(p,++S))return!1;await new Promise(_=>setTimeout(_,500))}},async capacity(){let[p,f]=await Promise.all([c(),e.capacity()]);return{pods:p,...f}},async databaseBytes(p){let f=await n(t,["exec",...ke,B(p).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",p,"-tAc","select pg_database_size(current_database())"]);return f.code===0?Zs(f.stdout):null},async storageBytes(p){let f=await n(t,["exec",...ke,B(p).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",p,"-tAc","select (xpath('/row/n/text()', query_to_xml(format('select coalesce(sum((metadata->>''size'')::bigint), 0) as n from %s', coalesce(to_regclass('snout_files.files'), to_regclass('storage.objects'))), false, true, '')))[1]::text"]);return f.code===0?Zs(f.stdout):null},async setReadOnly(p,f){let y=B(p);await N(n,t,["exec",...ke,y.pod,"psql","--username","snoutpod_admin","--dbname","postgres","-tAc",`alter database ${y.database} set default_transaction_read_only = ${f?"on":"off"}`])},async archiveState(p){let f=await n(t,["exec",...ke,B(p).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",p,"-tA","-F","|","-c",`select coalesce(to_char(last_archived_time, 'YYYY-MM-DD"T"HH24:MI:SS.MSOF'), ''), case when last_failed_time > coalesce(last_archived_time, '-infinity') then failed_count else 0 end from pg_stat_archiver`]);if(f.code!==0)return null;let[y="",S=""]=f.stdout.trim().split(`
|
|
462
|
+
`)[0]?.split("|")??[],_=Number(S);return{lastArchivedAt:y.trim()||null,failures:Number.isInteger(_)&&_>=0?_:0}}}}async function Hp(e,t,n,r,o=Vp){let s=await e(t,["inspect","--format","json",r]);if(s.code!==0)return{name:n,ok:!1,detail:"no such container"};let a,c;try{let m=JSON.parse(s.stdout);a=m[0]?.State,c=m[0]?.Config?.Labels}catch{return{name:n,ok:!1,detail:"podman inspect could not be read"}}let i=a?.Status??"unknown";if(i!=="running")return{name:n,ok:!1,detail:i};let u=SS(c?.[fo]);if(!u)return{name:n,ok:!0,detail:"running"};let l=await o(u.port,u.path);return l===null?{name:n,ok:!1,detail:"not answering yet"}:{name:n,ok:!0,detail:`answered ${l}`}}async function BS(e,t,n){let r=new Array(e.length),o=0;async function s(){for(;o<e.length;){let a=o++;r[a]=await n(e[a],a)}}return await Promise.all(Array.from({length:Math.min(Math.max(1,t),e.length)},s)),r}function Ca(e){switch((e??"").toLowerCase()){case"running":return"running";case"created":case"configured":return"starting";case"exited":case"stopped":return"stopped";default:return"unknown"}}function HS(e,t){return[`\\set from_db ${e}`,`\\set to_db ${t}`,`\\set from_role ${e}_owner`,`\\set to_role ${t}_owner`,'alter database :"from_db" with allow_connections false;',"set snoutpod.rename_from = :'from_db';","do $$","declare"," db text := current_setting('snoutpod.rename_from');"," remaining text;","begin",` for attempt in 1..${Gp} loop`," perform pg_stat_clear_snapshot();"," select string_agg(coalesce(nullif(application_name, ''), backend_type) || ' (pid ' || pid || ')', ', ')"," into remaining from pg_stat_activity where datname = db and pid <> pg_backend_pid();"," if remaining is null then"," return;"," end if;"," perform pg_terminate_backend(pid) from pg_stat_activity where datname = db and pid <> pg_backend_pid();",` perform pg_sleep(${Kp/Gp});`," end loop;",` raise exception 'database % still has sessions after ${Kp}s, not renamed: %', db, remaining;`,"end","$$;",'alter database :"from_db" rename to :"to_db";','alter database :"to_db" with allow_connections true;','alter role :"from_role" rename to :"to_role";',"\\getenv admin_password POSTGRES_PASSWORD","\\if :{?admin_password}","alter role snoutpod_admin with password :'admin_password';","\\endif","alter system reset restore_command;","alter system reset recovery_target_time;","alter system reset recovery_target_action;","checkpoint;",""].join(`
|
|
463
|
+
`)}function KS(e){if(!e?.issuer)return[];try{return["--env",`${fp}=${gp(e.issuer)}`]}catch{return[]}}function GS(e){return typeof e!="number"||!Number.isFinite(e)||e<=0?[]:["--cpus",e.toFixed(2)]}var Oe,Na,Ue,On,uo,Ip,Cp,cS,dS,Wp,en,Lp,Up,zp,La,fo,Vp,_S,$S,ke,_t,PS,Bp,CS,Yp,DS,MS,jS,FS,Kp,Gp,so=D(()=>{"use strict";Te();Xs();Vr();Wr();sa();ia();aa();Tn();ap();Qs();dp();mp();Sa();hp();la();_p();oa();ba();Pp();no();Np();Oe="snoutpod.ref",Na="snoutpod.generation",Ue="snoutpod.service",On="snoutpod.services-generation",uo="snoutpod.service-spec",Ip="snoutpod.service-pod";Cp=10,cS=/^[a-z][a-z0-9-]{0,30}$/;dS=/^[a-z][a-z0-9_]{0,62}$/;Wp="-set-aside";en="snoutpod.host-service",Lp="snoutpod.host-service-spec",Up="snoutpod.host-service-joins";zp=/^[A-Za-z_][A-Za-z0-9_]*$/;La=/^\/[A-Za-z0-9\/_\-.~%?=&]*$/,fo="snoutpod.health";Vp=async(e,t)=>{let{request:n}=await import("node:http");return new Promise(r=>{let o=n({host:"127.0.0.1",port:e,path:t,method:"GET",timeout:2e3},s=>{s.resume(),r(s.statusCode??null)});o.on("error",()=>r(null)),o.on("timeout",()=>{o.destroy(),r(null)}),o.end()})},_S="crun";$S="/usr/sbin/nft",ke=["--env","PGOPTIONS=-c search_path=pg_catalog,pg_temp -c synchronous_commit=local"],_t=3e4,PS=3e4;Bp=200,CS=1024,Yp=512,DS=45e3,MS=1e3,jS=4096;FS=8;Kp=30,Gp=300});var pm={};ii(pm,{DEFAULT_LOCAL_MEMORY_MB:()=>nm,DEFAULT_LOCAL_PORT:()=>VS,ENGINE:()=>oe,ENGINE_NAME:()=>Ba,IMAGE:()=>Ga,NETWORK:()=>kt,NEW_PROJECT_MAJOR:()=>Ka,NO_BLACKHOLE_ROUTES:()=>am,PUBLIC_IMAGE:()=>QS,PUBLISHED_MAJORS:()=>nn,capacity:()=>sm,chooseLocalImage:()=>Cn,dropIfUnused:()=>dm,ensureImage:()=>e_,ensureNetwork:()=>s_,exec:()=>ye,hasData:()=>wo,localImageFor:()=>om,localPsql:()=>l_,localRuntime:()=>o_,localSpec:()=>cm,localStatus:()=>qa,localUri:()=>lm,networkPolicyInPlace:()=>im,newLocalPassword:()=>XS,newLocalRef:()=>YS,podLog:()=>um,podName:()=>rn,podmanReady:()=>n_,removeLocal:()=>u_,staleImage:()=>Ha,startLocal:()=>i_,stopLocal:()=>d_,volumePgVersion:()=>go});import{randomBytes as em}from"node:crypto";import{existsSync as Zp}from"node:fs";import{platform as yo}from"node:os";import{delimiter as qS,join as WS}from"node:path";function zS(){if(process.env.SNOUTPOD_ENGINE)return process.env.SNOUTPOD_ENGINE;let e=yo()==="win32"?["podman.exe","podman"]:["podman"];for(let t of(process.env.PATH??"").split(qS))if(t&&e.some(n=>Zp(WS(t,n))))return"podman";return Zp("/opt/podman/bin/podman")?"/opt/podman/bin/podman":"podman"}function Cn(e,t=process.env.SNOUTPOD_IMAGE){if(t)return{name:t,published:null,major:null};if(e===null)return In(Ka);let n=Number(Fa(e));if(!nn.includes(n)){let r=e.trim()||"(empty)",o=nn.map(s=>In(s).published).join(" or ");throw new Error(`its data directory's PG_VERSION says ${r}, and there is an image only for Postgres ${nn.join(" and ")}. Set SNOUTPOD_IMAGE to an image of that major (${o} for those).`)}return In(n)}function In(e){return{name:`${JS}:${e}`,published:`${tm}:${e}`,major:e}}function YS(){return Vd(e=>em(e))}function XS(){return em(18).toString("base64url")}async function e_(e,t=ye,n=Cn(null)){let r=n.published;if(!r)return null;if((await t(oe,["image","exists",n.name])).code===0)return await t_(e,t,n.name,r),null;e?.(`Fetching the database image (${r}). This happens once.`);let o=await t(oe,["pull",r],{timeoutMs:rm}).catch(a=>({code:1,stdout:"",stderr:a instanceof Error?a.message:String(a)}));if(o.code!==0)return`could not fetch the database image ${r}: ${o.stderr.trim()||o.stdout.trim()||"the pull failed"}`;let s=await t(oe,["tag",r,n.name]);return s.code!==0?`fetched ${r} but could not tag it as ${n.name}: ${s.stderr.trim()}`:(e?.("Got it."),null)}async function t_(e,t,n,r){let o=await t(oe,["image","inspect","--format","{{.Id}}",n,r]).catch(()=>null),[s,a]=o?.code===0?o.stdout.trim().split(`
|
|
464
|
+
`):[];if(!s||s!==a)return;let c=setTimeout(()=>e?.(`Fetching a newer database image (${r}).`),3e3),i=await t(oe,["pull",r],{timeoutMs:rm}).catch(h=>({code:1,stdout:"",stderr:h instanceof Error?h.message:String(h)})).finally(()=>clearTimeout(c));if(i.code!==0){let h=(i.stderr.trim()||i.stdout.trim()||"the pull failed").split(`
|
|
465
|
+
`).at(-1);e?.(`Could not check for a newer database image (${h}); using the one on this machine.`);return}let u=await t(oe,["image","inspect","--format","{{.Id}}",r]).catch(()=>null),l=u?.code===0?u.stdout.trim():"";if(!l||l===s)return;let m=await t(oe,["tag",r,n]);if(m.code!==0){e?.(`Fetched a newer ${r} but could not tag it as ${n}: ${m.stderr.trim()}`);return}e?.(`Fetched a newer database image (${r}).`)}async function n_(e=ZS,t=Ga){if(Ba!=="podman")return`SNOUTPOD_ENGINE=${Ba}: a local project is Podman-only, because a database that works under Docker and not under Podman is a database that works here and not in the cloud`;let n;try{n=await ye(oe,["--version"])}catch{return`${oe} is not installed. macOS: brew install podman, then podman machine init && podman machine set --rootful=false && podman machine start. Windows: winget install RedHat.Podman, then the same three machine commands. Linux: your distribution's podman package.`}if(n.code!==0)return`${oe} --version failed: ${n.stderr.trim()||n.stdout.trim()}`;if((await ye(oe,["image","exists",t])).code!==0)return`the pod image ${t} is not on this machine. ${e}`;let o=await ye(oe,["machine","inspect","--format","{{.Rootful}}"]);return o.code===0&&o.stdout.trim().startsWith("true")?"the podman machine is ROOTFUL, and the hosts are not: podman machine stop && podman machine set --rootful=false && podman machine start":null}function r_(e){let t=`if out=$(ls -A ${JSON.stringify(e)} 2>/dev/null); then if [ -n "$out" ]; then echo DATA; else echo EMPTY; fi; else echo HIDDEN; fi`;return ye(oe,["machine","ssh",t]).then(n=>{let r=n.stdout.trim().split(`
|
|
466
|
+
`).at(-1);return r==="DATA"?"data":r==="EMPTY"?"empty":"hidden"},()=>"hidden")}function wo(e){let t=yo()==="darwin"||yo()==="win32";return Zu(ye,e,{binary:oe,look:t?r_:void 0})}async function go(e,t=ye,n=yo()){let r=B(e).pod;if((await t(oe,["volume","exists",r])).code!==0)return null;let o;if(n==="darwin"||n==="win32"){let s=null;for(let a of nn){let c=In(a).name;if((await t(oe,["image","exists",c])).code===0){s=c;break}}if(!s)throw new Error(`${r} holds a database and there is no pod image on this machine to read its Postgres version with. Set SNOUTPOD_IMAGE to the image it was made with (${nn.map(a=>In(a).published).join(" or ")}).`);o=await t(oe,["run","--rm","--pull=never","--network=none","--userns=host","--user=0","--entrypoint=cat","--volume",`${r}:/data:ro`,s,"/data/PG_VERSION"])}else{let s=await t(oe,["volume","inspect","--format","{{.Mountpoint}}",r]),a=s.code===0?s.stdout.trim().replace(/\/+$/,""):"";if(!a)throw new Error(`could not find where ${r} is: ${s.stderr.trim()||"no mountpoint"}`);o=await t(oe,["unshare","cat",`${a}/PG_VERSION`])}if(o.code===0)return o.stdout;if(/no such file/i.test(o.stderr))return null;throw new Error(`could not read the Postgres version in ${r}: ${o.stderr.trim()||o.stdout.trim()||`exit ${o.code}`}`)}function om(e,t=go){if(!e||process.env.SNOUTPOD_IMAGE)return Promise.resolve(Cn(null));ae(e);let n=t===go?ho.get(e):void 0;if(n)return n;let r=t(e).then(o=>{try{return Cn(o)}catch(s){throw new Error(`${rn(e)}: ${s instanceof Error?s.message:String(s)}`)}});return t===go&&(ho.set(e,r),r.catch(()=>ho.delete(e))),r}async function sm(){return{ramFreeMb:4096,ramTotalMb:8192,diskFreeMb:32768,diskTotalMb:65536}}function o_(e){return Xp({exec:ye,binary:oe,network:kt,hasData:wo,capacity:sm,repository:e})}function rn(e){return B(e).pod}async function s_(e){if(process.env.SNOUTPOD_LOCAL_ALLOW_EGRESS==="1"){await a_()||await ye(oe,Jr({name:kt}));return}let t=Jr({name:kt,allow:[]}),n="";for(let r=0;r<10;r+=1){if(await im())return;await e?.(),await ye(oe,["network","rm","--force",kt]).catch(()=>{});let o=await ye(oe,t);if(o.code===0)return;if(n=`${o.stderr}${o.stdout}`.trim(),/invalid route gateway/i.test(n))throw new Error(`${am}: ${n}`);await new Promise(s=>setTimeout(s,500))}throw new Error(`could not create ${kt} with egress blocked${n?`: ${n}`:""}`)}async function a_(){return(await ye(oe,["network","inspect",kt,"--format","json"])).code===0}async function im(){let e=await ye(oe,["network","inspect",kt,"--format","json"]);if(e.code!==0)return!1;try{return Ru(JSON.parse(e.stdout)?.[0])}catch{return!1}}function lm(e){let t=B(e.ref);return`postgres://${t.ownerRole}:${encodeURIComponent(e.ownerPassword)}@127.0.0.1:${e.port}/${t.database}?sslmode=disable`}function cm(e,t={}){return ae(e.ref),{ref:e.ref,generation:1,image:t.image??Ga,memoryMb:t.memoryMb??nm,cpus:null,bucket:"",region:"us-east-1",ownerPassword:e.ownerPassword,adminPassword:e.adminPassword,port:e.port,tuning:t.tuning??null,backup:null,services:t.services??null}}function Nn(e,t){e.onProgress?.(t)}async function i_(e,t,n={}){let r=n.image??(await om(t.ref,n.pgVersion)).name,o=cm(t,{...n,image:r}),s=n.timeoutMs??12e4,a=n.hasData??wo,c=(await e.list()).find(u=>u.ref===t.ref),i=null;if(c?c.status==="stopped"&&(i=await(n.staleImage??Ha)(t.ref,r))?(Nn(n,"Starting the database on the newer image. Its data stays as it is."),await e.remove(t.ref),await e.create(o),await(n.dropImage??dm)(i)):c.status==="stopped"?(Nn(n,"Starting the database."),await e.start(t.ref),await e.startServices(t.ref)):(Nn(n,"The database is already running."),c.status==="running"&&await(n.staleImage??Ha)(t.ref,r)&&Nn(n,"A newer database image is on this machine. To move onto it: snoutdata stop, then snoutdata start.")):(Nn(n,await a(t.ref)?"Starting the database over its existing data.":"Creating the database."),await e.create(o)),!await e.waitReady(t.ref,s))throw new Error(await c_(t.ref,s,n.podLog??um));return await e.applyOwnerPassword(o),await e.createServices(o),qa(e,t,n)}function Qp(e){return e.trim().replace(/^sha256:/,"")}async function Ha(e,t){let[n,r]=await Promise.all([ye(oe,["container","inspect","--format","{{.Image}}",rn(e)]).catch(()=>null),ye(oe,["image","inspect","--format","{{.Id}}",t]).catch(()=>null)]);if(n?.code!==0||r?.code!==0)return null;let o=Qp(n.stdout),s=Qp(r.stdout);return o&&s&&o!==s?o:null}async function dm(e){let t=await ye(oe,["image","inspect","--format","{{len .RepoTags}}",e]).catch(()=>null);t?.code===0&&t.stdout.trim()==="0"&&await ye(oe,["rmi",e]).catch(()=>{})}async function l_(e,t){let n=B(e.ref);return ye(oe,["exec","--interactive","-e","PGPASSWORD",rn(e.ref),"psql","--host","127.0.0.1","--port","5432","--username",n.ownerRole,"--dbname",n.database,"--no-psqlrc","-v","ON_ERROR_STOP=1","-tA","--file","-"],{stdin:t,env:{PGPASSWORD:e.ownerPassword}})}async function um(e){let t=await ye(oe,["logs","--tail","20",rn(e)]).catch(()=>null);return t?`${t.stdout}${t.stderr}`:"(no log: the container is not there)"}async function c_(e,t,n){let r=await n(e).catch(()=>"(no log)");return`${rn(e)} never became ready in ${t}ms
|
|
442
467
|
--- pod log ---
|
|
443
|
-
${r}`}async function Ow(e,t,n={}){le(t);let r=(await e.list()).find(o=>o.ref===t);return r&&r.status!=="stopped"&&await e.stop(t,wd),Is(e,{ref:t,port:0,ownerPassword:"",adminPassword:""},n)}async function Nw(e,t,n){le(t),await e.remove(t),n&&(await e.removeData(t),xr.delete(t))}async function Is(e,t,n={}){le(t.ref);let r=(await e.list()).find(a=>a.ref===t.ref),o=r?.status==="running",s=n.hasData??Er;return{ref:t.ref,exists:!!r,status:r?.status??"absent",ready:o?await e.waitReady(t.ref,1):!1,hasData:r?.hasData??await s(t.ref),port:t.port,databaseBytes:o?await e.databaseBytes(t.ref):null,services:r?.services??[],uri:t.port>0&&t.ownerPassword?fu(t):null}}var Rs,Z,Os,Rt,gw,iu,Ns,nt,hw,cu,ie,bw,_w,lu,xr,pu,bu=C(()=>{"use strict";bd();nn();kd();ls();ws();Re();Rs=process.env.SNOUTPOD_ENGINE??"podman";Z=fw(),Os=18,Rt=[17,18],gw="snoutpod/postgres",iu="ghcr.io/snoutdata/snoutpod-postgres";Ns=cn(null).name,nt=process.env.SNOUTPOD_LOCAL_NETWORK??"snoutpod-local",hw=54322,cu=512,ie=_d();bw="cd packages/snoutpod && npm run image:build",_w=`${iu}:${Os}`;lu=20*60*1e3;xr=new Map;pu="this podman cannot blackhole a route, so the local tier cannot isolate its containers"});be();var $e=["--admin","--token","--project"],Cr=["--max-write-pause-ms","--drain-timeout-ms","--ack-timeout-ms","--copy-mb-per-s"],mt=[...$e,"--plan","--no-wait","--timeout",...Cr],Se=[...mt,"--yes"],jr=[{name:"init",summary:"Create or link a project for this folder, and print its DATABASE_URL or write it to .env",flags:["--name","--region","--env","--ref"]},{name:"login",summary:"Sign in through a browser, with --sso through your company's identity provider, or with --device by typing a code into one anywhere",flags:["--email","--provider","--sso","--domain","--no-browser","--device","--timeout"]},{name:"logout",summary:"Forget the stored session",flags:[]},{name:"whoami",summary:"Who this credential belongs to, and whether it is the account login --email asked for",flags:[]},{name:"upgrade",summary:"Install the newest CLI the way this one was installed (binary or npm), checksum verified",flags:["--check"]},{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","--project"]},{name:"tokens revoke",args:"<id|sdt_prefix>",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, and print its ref",flags:["--name","--region","--team","--no-wait","--timeout","--show-url"]},{name:"projects pause",summary:"Stop a project, and wait until it is paused",flags:["--ref","--no-wait","--timeout"]},{name:"projects resume",summary:"Start a paused project, and wait until it is ready",flags:["--ref","--no-wait","--timeout"]},{name:"projects delete",summary:"Delete a project, and wait until it is gone",flags:["--ref","--no-wait","--timeout"]},{name:"projects show",summary:"One project: state, products (Realtime included), functions, secrets, domains",flags:["--ref"]},{name:"products",summary:"Auth, storage, the data API and push: on or off. Realtime is always on, and listed",flags:["--ref"]},{name:"products enable",args:"auth|storage|data-api|push",summary:"Turn auth, storage, data-api or push on",flags:["--ref"]},{name:"products disable",args:"auth|storage|data-api|push",summary:"Turn auth, storage, data-api or push off",flags:["--ref"]},{name:"push credentials",summary:"Push keys: what is set for APNs, FCM and Web Push (never the keys)",flags:["--ref"]},{name:"push credentials set",args:"apns|fcm",summary:"Set the APNs key (a .p8) or the FCM service account, checked before it is stored",flags:["--ref","--p8","--key-id","--team-id","--topic","--environment","--file"]},{name:"push credentials remove",args:"apns|fcm",summary:"Remove the APNs or FCM credentials",flags:["--ref"]},{name:"auth",summary:"Auth settings: Google sign-in and redirect addresses",flags:["--ref"]},{name:"auth google",summary:"Sign in with Google, with your own client (secret on stdin)",flags:["--ref","--client-id","--stdin"]},{name:"auth anonymous",args:"on|off",summary:"Guest sign-in on or off: guestSignIn() gives a browser a session with no email or password",flags:["--ref"]},{name:"auth redirects",summary:"The site URL and the addresses a sign-in may return to",flags:["--ref","--site-url","--allow"]},{name:"auth templates",summary:"The five auth emails, ours or your own",flags:["--ref"]},{name:"auth template",args:"KIND",summary:"Use your own subject and HTML for one, or reset it",flags:["--ref","--file","--subject"]},{name:"realtime inspect",summary:"The channels open now: each client, its roster entry, when it was last heard, and the last minute of messages",flags:["--channel","--watch","--ref"]},{name:"realtime logs",summary:"The connection log: connects, joins, leaves and disconnects, with the reason each ended",flags:["--since","--channel","--ref"]},{name:"ai",summary:"The project's own AI gateway: on or off, its models, budgets and the last 30 days' spend",flags:["--ref"]},{name:"ai enable",summary:"Switch the AI gateway on. Its container joins the pod, which restarts the database once",flags:["--ref"]},{name:"ai disable",summary:"Switch it off. Its providers, models, keys and log stay in the project's database",flags:["--ref"]},{name:"ai models",summary:"The models it serves: provider, upstream id, price and whether each is served",flags:["--ref"]},{name:"ai models add",args:"[name]",summary:"Add a model, with its provider and key when the provider is new. The key comes on stdin and the provider proves it first; without --model, print the steps to find the key and the model's id",flags:["--ref","--source","--model","--provider","--base-url","--region","--price-in","--price-out","--access-key-id","--auth-header","--stdin"]},{name:"ai models remove",args:"<name>",summary:"Stop serving a model",flags:["--ref"]},{name:"ai providers",summary:"Where its models run, and whether each one's key is stored (never the key)",flags:["--ref"]},{name:"ai providers remove",args:"<name>",summary:"Remove a provider no model uses",flags:["--ref"]},{name:"ai routes",summary:"Names that stand for several models, and what each tries in which order",flags:["--ref"]},{name:"ai routes set",args:"<name>",summary:"Make or replace a route: its models, tried in turn when one fails for a reason that is not the caller's",flags:["--ref","--models","--sort"]},{name:"ai routes remove",args:"<name>",summary:"Remove a route; its models stay",flags:["--ref"]},{name:"ai keys",summary:"Keys a server calls the gateway with: created, last used, revoked",flags:["--ref"]},{name:"ai keys create",summary:"Make a key for a server, shown once",flags:["--ref","--name","--models","--expires"]},{name:"ai keys revoke",args:"<id>",summary:"Refuse a key from now on",flags:["--ref"]},{name:"ai budgets",summary:"What may be spent, and by whom",flags:["--ref"]},{name:"ai budgets set",args:"project|users",summary:"The project's budget, or every signed-in user's. A browser call needs both, hard",flags:["--ref","--amount","--period","--soft"]},{name:"ai budgets remove",args:"<id>",summary:"Remove a budget",flags:["--ref"]},{name:"ai usage",summary:"Requests, tokens and cost, grouped",flags:["--ref","--by","--days"]},{name:"ai test",summary:"One chat call through the project's door, with a key made for it and revoked after",flags:["--ref","--model","--prompt"]},{name:"domains",summary:"Your own domains in front of the project API",flags:["--ref"]},{name:"domains add",args:"<hostname>",summary:"Add one, and print the DNS records to publish",flags:["--ref"]},{name:"domains verify",args:"<hostname>",summary:"Check its records and verify it",flags:["--ref"]},{name:"domains remove",args:"<hostname>",summary:"Stop serving it",flags:["--ref"]},{name:"functions deploy",args:"<name>",summary:"Put a folder of TypeScript on the edge",flags:["--ref","--dir","--entrypoint","--no-verify-jwt"]},{name:"functions list",summary:"What this project has deployed",flags:["--ref"]},{name:"functions size",args:"<name>",summary:"Its memory and workers, within the plan",flags:["--ref","--memory","--concurrency","--reset"]},{name:"functions logs",args:"[name]",summary:"What your functions printed, the last seven days",flags:["--ref","--errors","--tail","--follow"]},{name:"functions delete",args:"<name>",summary:"Remove one",flags:["--ref"]},{name:"containers deploy",args:"<name> <image> | <name> --dockerfile <dir>",summary:"Run a Docker image of yours all the time, pinned to the digest its tag points at now, or built from a Dockerfile with your Docker",flags:["--ref","--port","--dockerfile","--registry-user","--stdin","--clear-registry","--no-wait"]},{name:"containers list",summary:"Each container: status, image and URL",flags:["--ref"]},{name:"containers logs",args:"[name]",summary:"What your containers printed, the last seven days",flags:["--ref","--errors","--tail","--follow"]},{name:"containers restart",args:"<name>",summary:"Start one again from its image",flags:["--ref"]},{name:"containers delete",args:"<name>",summary:"Stop one and remove it",flags:["--ref"]},{name:"secrets set",summary:"Set the environment functions and containers run with",flags:["--ref","--stdin"]},{name:"secrets list",summary:"The names, never the values",flags:["--ref"]},{name:"secrets unset",summary:"Remove one",flags:["--ref"]},{name:"keys",summary:"The app key and server key for a project",flags:["--ref"]},{name:"keys rotate",summary:"New keys, breaking every one already issued",flags:["--ref","--force"]},{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:"start",summary:"A Postgres on this machine, with your migrations applied",flags:["--port","--dir","--no-migrations","--out-of-order"]},{name:"stop",summary:"Stop the local database. The data stays",flags:[]},{name:"status",summary:"What the local database is doing",flags:[]},{name:"gen types typescript",summary:"The schema as a TypeScript Database type, on stdout",flags:["--ref","--db-url","--local","--schema","--out"]},{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 access",summary:"Who signs in to the database as themselves with OAuth (Postgres 18), at which level, as which role",flags:["--ref"]},{name:"db access grant",summary:"Let a person sign in to the database as themselves, and print their connection string",flags:["--ref","--level"]},{name:"db access revoke",summary:"Take it away, by email or role; a token already issued can work until it expires (up to 1 hour)",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, or rewind it to a moment into a new project",flags:["--file","--force","--at","--name","--window","--ref"]},{name:"cluster status",summary:"A Lepis cluster: its nodes, keyspaces and their ranges (keyspace:lo), tables, routers and unfinished jobs",flags:[...$e]},{name:"cluster ops",summary:"The operations the router offers, and its default settings",flags:[...$e]},{name:"cluster nodes",summary:"The nodes, their state and how many ranges each owns",flags:[...$e]},{name:"cluster enable",summary:"SnoutData Cloud: make a project a cluster. The router joins its pod, which restarts the database once. Each node then uses one of the plan project slots",flags:["--project","--plan","--yes"]},{name:"cluster disable",summary:"SnoutData Cloud: turn a cluster of one node back into a plain project. The data is checked, the cluster is taken apart and the router leaves its pod, which restarts the database once",flags:["--project","--plan","--yes"]},{name:"cluster nodes attach",summary:"SnoutData Cloud: hand a ready node pod made with nodes add --no-join to the cluster, as a standby until a range moves onto it",flags:[...mt]},{name:"cluster nodes add",summary:"Join a Postgres node: checks its version and settings, syncs roles and reference tables. In SnoutData Cloud it makes a node that starts as a copy of the home node, then joins and takes its share of the data on its own (--no-join to attach it by hand); it uses one of the plan project slots",flags:[...mt,"--host","--port","--dbname","--sslmode","--peer-host","--no-join"]},{name:"cluster spread",summary:"SnoutData Cloud: spread a table over the cluster in one go: a keyspace for its key (shared with tables keyed the same way), the table distributed by it, then a rebalance",flags:["--project","--column","--key-type","--plan","--yes"]},{name:"cluster retry",summary:"SnoutData Cloud: take up a stopped step again: a node whose join stopped, or --queue for a stopped spread",flags:["--project","--queue"]},{name:"cluster replica add",args:"<node-ref>",summary:"SnoutData Cloud: give a data node a replica, a copy that follows it and takes its place on its own if the node is lost or corrupt. It takes one of the plan project slots, and the node needs its first backup",flags:["--project","--node","--plan","--yes"]},{name:"cluster replica remove",args:"<node-ref>",summary:"SnoutData Cloud: remove a node's replica and free its project slot. If the node is then lost or corrupt, its share of the data is unavailable until it is restored from backups",flags:["--project","--node","--plan","--yes"]},{name:"cluster alerts",summary:"SnoutData Cloud: what happened to the cluster that you must know about (a node lost or corrupt, a failover, a node without its replica), open ones first, with those resolved in the last day",flags:["--project"]},{name:"cluster alerts seen",args:"<id>",summary:"SnoutData Cloud: mark an alert as seen. It stays open until what it is about is over",flags:["--project"]},{name:"cluster nodes drain",summary:"Move every range off a node",flags:[...Se,"--to"]},{name:"cluster nodes remove",summary:"Detach a drained node",flags:[...Se]},{name:"cluster keyspace create",summary:"A keyspace: the shard key type and its initial ranges",flags:[...mt,"--key-type","--ranges","--seed","--nodes"]},{name:"cluster keyspace drop",summary:"Forget a keyspace no table is distributed by",flags:[...mt]},{name:"cluster table distribute",summary:"Shard a table by a column, moving its rows out from the home node",flags:[...Se,"--column","--keyspace"]},{name:"cluster table reference",summary:"Copy a table to every node",flags:[...Se]},{name:"cluster table global",summary:"Bring a table back to the home node alone",flags:[...Se]},{name:"cluster range split",summary:"One range becomes two, and one half moves",flags:[...Se,"--keyspace","--range","--at","--to"]},{name:"cluster range merge",summary:"Two adjacent ranges become one",flags:[...Se]},{name:"cluster range move",summary:"Give a range to another node",flags:[...Se,"--keyspace","--range","--to"]},{name:"cluster tenant pin",summary:"One key value gets its own range, and optionally its own node",flags:[...Se,"--keyspace","--value","--node"]},{name:"cluster rebalance",summary:"Even out the ranges across the nodes",flags:[...Se,"--keyspace"]},{name:"cluster scale",summary:"Add nodes and rebalance, or drain and remove the emptiest",flags:[...Se,"--add","--remove","--nodes","--dbname","--sslmode"]},{name:"cluster verify",summary:"Row counts and a checksum per range, source against target",flags:[...mt,"--keyspace"]},{name:"cluster cleanup",summary:"Delete the rows a node no longer owns, then VACUUM",flags:[...Se,"--node"]},{name:"cluster dissolve",summary:"A standalone cluster of the home node alone: check the data, take the fences off and forget every keyspace and table, then stop the router yourself",flags:[...Se]},{name:"cluster plan",summary:"The dry run of any operation above: steps, what moves, rows, size, copy time, expected pause",flags:[...$e,...Cr]},{name:"cluster jobs",summary:"The latest jobs",flags:[...$e]},{name:"cluster jobs show",summary:"One job, its plan and its steps",flags:[...$e]},{name:"cluster jobs watch",summary:"Follow a job until it finishes: exit 0 done, 1 failed or cancelled, 10 timed out",flags:[...$e,"--timeout"]},{name:"cluster jobs cancel",summary:"Stop a job: a move before its cutover rolls back, after it finishes its cleanup",flags:[...$e,"--yes"]},{name:"cluster jobs resume",summary:"Run a failed job again from the step that failed, and follow it",flags:[...$e,"--no-wait","--timeout"]},{name:"cluster settings",summary:"The cluster's cutover and advisor settings, or set them",flags:[...$e,...Cr,"--set"]},{name:"cluster advise",summary:"What to split, move or add, from the router's advisor: each recommendation with its reason, its plan and the command that runs it. Runs nothing",flags:[...$e,"--sample-ms"]},{name:"mcp",summary:"Serve these operations as tools over stdio",flags:["--allow-delete"]}],Ws={"--ref":"The project. Defaults to SNOUTDATA_PROJECT, then .snoutdata/project.json here or in a parent","functions logs --errors":"Only what was printed with console.error or console.warn, and the errors that stopped a request","functions logs --tail":"How many of the newest lines to show. Defaults to 100, at most 500","functions logs --follow":"Keep printing new lines as they arrive, about a minute after they are printed","containers deploy --port":"The port the container listens on, also given to it as PORT (default 8080)","containers deploy --dockerfile":"A folder with a Dockerfile (or the Dockerfile): built for linux/arm64 with your Docker and uploaded to the project","containers deploy --registry-user":"The user name for a private registry. The token comes on stdin, with --stdin","containers deploy --stdin":"Read the registry token from a pipe, so it is not in shell history or ps","containers deploy --clear-registry":"Forget the stored registry login, for an image that is public now","containers deploy --no-wait":"Answer once it is recorded, without waiting for the host to start it","containers logs --errors":"Only what was printed to stderr","containers logs --tail":"How many of the newest lines to show. Defaults to 100, at most 500","containers logs --follow":"Keep printing new lines as they arrive, about a minute after they are printed","--name":"The project's name","tokens create --name":"What the token is for and where it lives, shown in tokens list","db restore --name":"The name of the new project the restore makes","--region":"Where the project runs. Defaults to the nearest","--env":"Write DATABASE_URL into .env in this folder, and add .env to .gitignore since it holds the password. Without it the URL is printed","--email":"The account to sign in as. The browser offers it, and a different account is reported at once and by whoami","--provider":"google (the default) or github","--sso":"Sign in with your company's identity provider","--domain":"With --sso: your work email or company domain. Only the domain is sent","--no-browser":"Print the sign-in URL instead of opening a browser","--device":"Sign in by typing a code into a browser on any machine: SSH, containers, agents","--timeout":"Seconds to wait before giving up","--check":"Say whether a newer version exists, and install nothing","--expires":"Days until the token stops working. Without it, it works until revoked","--project":"A SnoutData Cloud project ref: its cluster, through the control plane, as you (instead of --admin)","tokens create --project":"Limit the token to one project ref","--team":"Create it in a team (name or id), shared with its members","--no-wait":"Return as soon as it is asked for, without waiting for it to be true","--show-url":"Also print the connection string, which holds the password. Otherwise db url prints it when wanted","--p8":"The APNs .p8 key file (a path, never the key itself)","--key-id":"The APNs key id","--team-id":"Your Apple developer team id","--topic":"The app's bundle id","--environment":"production or sandbox","--file":"A file to read","push credentials set --file":"The FCM service-account JSON","auth template --file":"The email's HTML body","db restore --file":"The dump to restore (from db export or pg_dump -Fc)","--client-id":"Your Google OAuth client id; the secret is read from stdin with --stdin","--stdin":"Read the secret value from a pipe, so it is not in shell history","--site-url":"Where sign-in emails send people by default","--allow":"Comma-separated addresses a sign-in may return to","--subject":"The email's subject line","--dir":"The folder to read from","db push --dir":"The migrations folder (default ./migrations)","start --dir":"The migrations folder (default ./migrations)","functions deploy --dir":"The function's folder (default ./functions/<name>)","--entrypoint":"The file the runtime starts, when it is not index.ts","--no-verify-jwt":"Let anybody with the URL call it (webhooks). By default a caller needs a project key or a user session","--memory":"Memory per worker, in MB, within the plan","--concurrency":"How many workers it may run at once, within the plan","--reset":"Back to the plan's default size","--force":"Do it although it cannot be undone","--days":"How many days of history","--history":"Print the day-by-day table too","--port":"The port the local database listens on (default 54322)","--no-migrations":"Start the database and apply nothing","--out-of-order":"Apply a new migration that sorts before one already applied","--db-url":"Read a postgres:// database instead of a project","--local":"Read the database snoutdata start is running","--schema":"Comma-separated schemas (default public)","--out":"Write to this file instead of stdout","db export --out":"Where to save the dump","--status":"Show the last export without taking another","--dry-run":"Show what would run, and change nothing","--at":"The moment to restore to (an ISO time), into a NEW project","--window":"How far back a point-in-time restore can go","--allow-delete":"Let the agent's delete_project actually delete","--source":"Where the model runs: bedrock, anthropic, openai, openrouter, ollama or compatible","--model":"The model's id, as the provider names it","ai test --model":"The model to call (default: the first chat model served)","ai models add --provider":"The provider to add it to, or the name of a new one (default: the one already connected for --source)","ai models add --region":"Bedrock: the AWS region the model runs in","ai models add --stdin":"Read the provider's key from a pipe (AWS: the secret access key), so it is in neither the shell history nor ps","--base-url":"The provider's OpenAI-compatible address, usually ending in /v1","--price-in":"Dollars per million input tokens. Both rates, or neither to take the shipped catalogue's","--price-out":"Dollars per million output tokens","--access-key-id":"Bedrock with AWS credentials: the access key id (the secret on stdin)","--auth-header":"A header the server checks for its key, instead of a Bearer token","--models":"Comma-separated models the key may call (default: every one)","ai keys create --name":"What the key is for, shown in ai keys","ai routes set --models":"The models to try, comma-separated, in order","ai routes set --sort":"order (as written, the default), price (the cheapest first), or latency or throughput (the fastest first, as measured)","ai keys create --expires":"Days until the key stops working. Without it, it works until revoked","--amount":"Dollars","--period":"day (the default) or month","--soft":"Warn in the log when it is passed, instead of refusing","--by":"Group by day, model, provider, operation, key, user or group (default model)","ai usage --days":"How many days, up to 366 (default 30)","--prompt":"What to ask (default: a five-word hello)","--channel":"Only this channel","--since":"How far back, like 10m, 2h or 1d (default 1h)","--watch":"Keep printing what changes until interrupted","--admin":"The Lepis router's admin API, like http://127.0.0.1:7432 (LEPIS_ADMIN_ADDR). https for any other machine; NODE_EXTRA_CA_CERTS for a private CA. Defaults to LEPIS_ADMIN_URL","--token":"The router's admin token. Prefer LEPIS_ADMIN_TOKEN, which keeps it out of shell history and ps","--plan":"Show the plan (steps, what moves, rows, size, copy time, expected pause) and change nothing","--yes":"Run it without asking. Without a terminal to ask at, an operation that moves or deletes data needs it","cluster jobs cancel --yes":"Cancel without asking","cluster nodes add --port":"The node's port (default 5432)","--host":"The address the router reaches the node at","--dbname":"The database on the node (default postgres)","--sslmode":"disable, require or verify-full (the default)","--peer-host":"The address the other nodes reach it at, when it differs from the router's","--to":"The node (id or name) that receives it","cluster nodes drain --to":"Comma-separated nodes to move its ranges to. Default: the least loaded","--key-type":"The distribution key's Postgres type: bigint, int, text, uuid, ...","--ranges":"How many equal ranges to start with","--seed":"The hash seed","--nodes":"Comma-separated nodes (ids or names) to place the ranges on","--column":"The distribution key column","--no-join":"SnoutData Cloud: leave the new node for you to attach with cluster nodes attach, instead of joining it on its own","--queue":"The stopped request (a table being spread) rather than a node","--keyspace":"The keyspace","--range":"The range's lower bound, with --keyspace: the spelling for a bound that starts with a dash","cluster range split --at":"Where to split (a hash value). Default: the middle","--node":"A node, by id or name","cluster cleanup --node":"Only this node. Default: every node","cluster replica add --node":"The node pod's ref, as cluster status lists it (or as the first word)","cluster replica remove --node":"The node pod's ref, as cluster status lists it (or as the first word)","--value":"The key value to pin","--add":"Comma-separated nodes to add, each name=host[:port]","--remove":"How many of the emptiest nodes to drain and remove","cluster scale --nodes":"SnoutData Cloud: how many nodes the project should have, the home included","--max-write-pause-ms":"The longest a cutover may pause writes; one that would take longer is postponed","--drain-timeout-ms":"How long a cutover waits for open transactions to finish","--set":"Settings by their API names, name=value,... (the advisor's advice_* thresholds among them)","--sample-ms":"How long to measure write rates over, in ms (default: the advice_sample_ms setting; 0 skips them)","--ack-timeout-ms":"How long a cutover waits for every router to acknowledge it","--copy-mb-per-s":"The copy rate a plan estimates with (chosen, not measured)","--level":"full (everything the project password can do) or read (reads the project's own tables, never the auth or storage schemas, and writes nothing). Default read"};function Dr(e,t){return Ws[`${e} ${t}`]??Ws[t]??""}function zs(e,t){let n=jr.filter(s=>s.name===e||s.name.startsWith(`${e} `));if(!t)return n;let r=`${e} ${t}`,o=n.filter(s=>s.name===r||s.name.startsWith(`${r} `));return o.length>0?o:n}re();me();te();import{spawn as em}from"node:child_process";function tm(e){return`snout-${e}`}function St(e,t,n){return new Promise((r,o)=>{let s=em("docker",["compose","-p",tm(e.ref),"--project-directory",e.folder,...t],{cwd:e.folder,stdio:["ignore","pipe","pipe"]}),a="",l="";s.stdout.on("data",i=>{a+=String(i)}),s.stderr.on("data",i=>{if(l+=String(i),n)for(let u of String(i).split(/\r?\n/).filter(Boolean))d(w(` ${u.trim()}`))}),s.on("error",i=>{if(i.code==="ENOENT"){o(new P("tool-missing","docker is not installed, or not on PATH. A local project runs in Docker (Docker Desktop on Windows and macOS)."));return}o(new P("failed",String(i)))}),s.on("close",i=>r({code:i??0,out:a,err:l}))})}async function nm(e){let t=await St(e,["ps","-a","--format","json"],!1);if(t.code!==0)throw new P("failed",t.err.trim()||"docker compose ps failed. Is Docker running?");let n=t.out.trim();return(n.startsWith("[")?JSON.parse(n):n.split(/\r?\n/).filter(Boolean).map(o=>JSON.parse(o))).map(o=>({service:o.Service??"?",state:o.State==="exited"&&o.ExitCode===0?"done":o.State??"?",health:o.Health??""})).sort((o,s)=>o.service.localeCompare(s.service))}async function Ia(e){let t=await nm(e),n=t.filter(o=>o.state==="running").length,r=t.filter(o=>o.state!=="done").length;return h({ref:e.ref,local:e.folder,api:e.apiUrl,dbPort:e.dbPort,running:n,services:t},()=>{if(d(`${x(e.name??e.ref)} (local, ${e.folder})`),t.length===0){d("Not started. `snoutdata start` brings it up.");return}process.stdout.write(`${L([["SERVICE","STATE","HEALTH"],...t.map(o=>[o.service,o.state,o.health])])}
|
|
444
|
-
`),d(`${n} of ${r} services running. API ${e.apiUrl}, database 127.0.0.1:${e.dbPort}.`)}),0}async function
|
|
445
|
-
`)})}
|
|
446
|
-
`)}${
|
|
447
|
-
`),!0}async function
|
|
448
|
-
`)})}async function
|
|
468
|
+
${r}`}async function d_(e,t,n={}){ae(t);let r=(await e.list()).find(o=>o.ref===t);return r&&r.status!=="stopped"&&await e.stop(t,Vu),qa(e,{ref:t,port:0,ownerPassword:"",adminPassword:""},n)}async function u_(e,t,n){ae(t),await e.remove(t),n&&(await e.removeData(t),ho.delete(t))}async function qa(e,t,n={}){ae(t.ref);let r=(await e.list()).find(a=>a.ref===t.ref),o=r?.status==="running",s=n.hasData??wo;return{ref:t.ref,exists:!!r,status:r?.status??"absent",ready:o?await e.waitReady(t.ref,1):!1,hasData:r?.hasData??await s(t.ref),port:t.port,databaseBytes:o?await e.databaseBytes(t.ref):null,services:r?.services??[],uri:t.port>0&&t.ownerPassword?lm(t):null}}var Ba,oe,Ka,nn,JS,tm,Ga,kt,VS,nm,ye,ZS,QS,rm,ho,am,mm=D(()=>{"use strict";Yu();Tn();Qu();Vr();so();Te();Ba=process.env.SNOUTPOD_ENGINE??"podman";oe=zS(),Ka=18,nn=[17,18],JS="snoutpod/postgres",tm="ghcr.io/snoutdata/snoutpod-postgres";Ga=Cn(null).name,kt=process.env.SNOUTPOD_LOCAL_NETWORK??"snoutpod-local",VS=54322,nm=512,ye=Xu();ZS="cd packages/snoutpod && npm run image:build",QS=`${tm}:${Ka}`;rm=20*60*1e3;ho=new Map;am="this podman cannot blackhole a route, so the local tier cannot isolate its containers"});Ae();var Me=["--admin","--token","--project"],Oo=["--max-write-pause-ms","--drain-timeout-ms","--ack-timeout-ms","--copy-mb-per-s"],Ct=[...Me,"--plan","--no-wait","--timeout",...Oo],Re=[...Ct,"--yes"],No=[{name:"init",summary:"Create or link a project for this folder, and print its DATABASE_URL or write it to .env",flags:["--name","--region","--env","--ref"]},{name:"login",summary:"Sign in through a browser, with --sso through your company's identity provider, or with --device by typing a code into one anywhere",flags:["--email","--provider","--sso","--domain","--no-browser","--device","--timeout"]},{name:"logout",summary:"Forget the stored session",flags:[]},{name:"whoami",summary:"Who this credential belongs to, and whether it is the account login --email asked for",flags:[]},{name:"upgrade",summary:"Install the newest CLI the way this one was installed (binary or npm), checksum verified",flags:["--check"]},{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","--project"]},{name:"tokens revoke",args:"<id|sdt_prefix>",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, and print its ref",flags:["--name","--region","--team","--no-wait","--timeout","--show-url"]},{name:"projects pause",summary:"Stop a project, and wait until it is paused",flags:["--ref","--no-wait","--timeout"]},{name:"projects resume",summary:"Start a paused project, and wait until it is ready",flags:["--ref","--no-wait","--timeout"]},{name:"projects delete",summary:"Delete a project, and wait until it is gone",flags:["--ref","--no-wait","--timeout"]},{name:"projects show",summary:"One project: state, products (Realtime included), functions, secrets, domains",flags:["--ref"]},{name:"products",summary:"Auth, storage, the data API and push: on or off. Realtime is always on, and listed",flags:["--ref"]},{name:"products enable",args:"auth|storage|data-api|push",summary:"Turn auth, storage, data-api or push on",flags:["--ref"]},{name:"products disable",args:"auth|storage|data-api|push",summary:"Turn auth, storage, data-api or push off",flags:["--ref"]},{name:"push credentials",summary:"Push keys: what is set for APNs, FCM and Web Push (never the keys)",flags:["--ref"]},{name:"push credentials set",args:"apns|fcm",summary:"Set the APNs key (a .p8) or the FCM service account, checked before it is stored",flags:["--ref","--p8","--key-id","--team-id","--topic","--environment","--file"]},{name:"push credentials remove",args:"apns|fcm",summary:"Remove the APNs or FCM credentials",flags:["--ref"]},{name:"auth",summary:"Auth settings: Google sign-in and redirect addresses",flags:["--ref"]},{name:"auth google",summary:"Sign in with Google, with your own client (secret on stdin)",flags:["--ref","--client-id","--stdin"]},{name:"auth anonymous",args:"on|off",summary:"Guest sign-in on or off: guestSignIn() gives a browser a session with no email or password",flags:["--ref"]},{name:"auth redirects",summary:"The site URL and the addresses a sign-in may return to",flags:["--ref","--site-url","--allow"]},{name:"auth templates",summary:"The five auth emails, ours or your own",flags:["--ref"]},{name:"auth template",args:"KIND",summary:"Use your own subject and HTML for one, or reset it",flags:["--ref","--file","--subject"]},{name:"realtime inspect",summary:"The channels open now: each client, its roster entry, when it was last heard, and the last minute of messages",flags:["--channel","--watch","--ref"]},{name:"realtime logs",summary:"The connection log: connects, joins, leaves and disconnects, with the reason each ended",flags:["--since","--channel","--ref"]},{name:"ai",summary:"The project's own AI gateway: on or off, its models, budgets and the last 30 days' spend",flags:["--ref"]},{name:"ai enable",summary:"Switch the AI gateway on. Its container joins the pod, which restarts the database once",flags:["--ref"]},{name:"ai disable",summary:"Switch it off. Its providers, models, keys and log stay in the project's database",flags:["--ref"]},{name:"ai models",summary:"The models it serves: provider, upstream id, price and whether each is served",flags:["--ref"]},{name:"ai models add",args:"[name]",summary:"Add a model, with its provider and key when the provider is new. The key comes on stdin and the provider proves it first; without --model, print the steps to find the key and the model's id",flags:["--ref","--source","--model","--provider","--base-url","--region","--price-in","--price-out","--access-key-id","--auth-header","--stdin"]},{name:"ai models remove",args:"<name>",summary:"Stop serving a model",flags:["--ref"]},{name:"ai providers",summary:"Where its models run, and whether each one's key is stored (never the key)",flags:["--ref"]},{name:"ai providers remove",args:"<name>",summary:"Remove a provider no model uses",flags:["--ref"]},{name:"ai routes",summary:"Names that stand for several models, and what each tries in which order",flags:["--ref"]},{name:"ai routes set",args:"<name>",summary:"Make or replace a route: its models, tried in turn when one fails for a reason that is not the caller's",flags:["--ref","--models","--sort"]},{name:"ai routes remove",args:"<name>",summary:"Remove a route; its models stay",flags:["--ref"]},{name:"ai keys",summary:"Keys a server calls the gateway with: created, last used, revoked",flags:["--ref"]},{name:"ai keys create",summary:"Make a key for a server, shown once",flags:["--ref","--name","--models","--expires"]},{name:"ai keys revoke",args:"<id>",summary:"Refuse a key from now on",flags:["--ref"]},{name:"ai budgets",summary:"What may be spent, and by whom",flags:["--ref"]},{name:"ai budgets set",args:"project|users",summary:"The project's budget, or every signed-in user's. A browser call needs both, hard",flags:["--ref","--amount","--period","--soft"]},{name:"ai budgets remove",args:"<id>",summary:"Remove a budget",flags:["--ref"]},{name:"ai usage",summary:"Requests, tokens and cost, grouped",flags:["--ref","--by","--days"]},{name:"ai test",summary:"One chat call through the project's door, with a key made for it and revoked after",flags:["--ref","--model","--prompt"]},{name:"domains",summary:"Your own domains in front of the project API",flags:["--ref"]},{name:"domains add",args:"<hostname>",summary:"Add one, and print the DNS records to publish",flags:["--ref"]},{name:"domains verify",args:"<hostname>",summary:"Check its records and verify it",flags:["--ref"]},{name:"domains remove",args:"<hostname>",summary:"Stop serving it",flags:["--ref"]},{name:"functions deploy",args:"<name>",summary:"Put a folder of TypeScript on the edge",flags:["--ref","--dir","--entrypoint","--no-verify-jwt"]},{name:"functions list",summary:"What this project has deployed",flags:["--ref"]},{name:"functions size",args:"<name>",summary:"Its memory and workers, within the plan",flags:["--ref","--memory","--concurrency","--reset"]},{name:"functions logs",args:"[name]",summary:"What your functions printed, the last seven days",flags:["--ref","--errors","--tail","--follow"]},{name:"functions delete",args:"<name>",summary:"Remove one",flags:["--ref"]},{name:"containers deploy",args:"<name> <image> | <name> --dockerfile <dir>",summary:"Run a Docker image of yours all the time, pinned to the digest its tag points at now, or built from a Dockerfile with your Docker",flags:["--ref","--port","--dockerfile","--registry-user","--stdin","--clear-registry","--no-wait"]},{name:"containers list",summary:"Each container: status, image and URL",flags:["--ref"]},{name:"containers logs",args:"[name]",summary:"What your containers printed, the last seven days",flags:["--ref","--errors","--tail","--follow"]},{name:"containers restart",args:"<name>",summary:"Start one again from its image",flags:["--ref"]},{name:"containers delete",args:"<name>",summary:"Stop one and remove it",flags:["--ref"]},{name:"secrets set",summary:"Set the environment functions and containers run with",flags:["--ref","--stdin"]},{name:"secrets list",summary:"The names, never the values",flags:["--ref"]},{name:"secrets unset",summary:"Remove one",flags:["--ref"]},{name:"keys",summary:"The app key and server key for a project",flags:["--ref"]},{name:"keys rotate",summary:"New keys, breaking every one already issued",flags:["--ref","--force"]},{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:"start",summary:"A Postgres on this machine, with your migrations applied",flags:["--port","--dir","--no-migrations","--out-of-order"]},{name:"stop",summary:"Stop the local database. The data stays",flags:[]},{name:"status",summary:"What the local database is doing",flags:[]},{name:"gen types typescript",summary:"The schema as a TypeScript Database type, on stdout",flags:["--ref","--db-url","--local","--schema","--out"]},{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 access",summary:"Who signs in to the database as themselves with OAuth (Postgres 18), at which level, as which role",flags:["--ref"]},{name:"db access grant",summary:"Let a person sign in to the database as themselves, and print their connection string",flags:["--ref","--level"]},{name:"db access revoke",summary:"Take it away, by email or role; a token already issued can work until it expires (up to 1 hour)",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, or rewind it to a moment into a new project",flags:["--file","--force","--at","--name","--window","--ref"]},{name:"cluster status",summary:"A Lepis cluster: its nodes, keyspaces and their ranges (keyspace:lo), tables, routers and unfinished jobs",flags:[...Me]},{name:"cluster ops",summary:"The operations the router offers, and its default settings",flags:[...Me]},{name:"cluster nodes",summary:"The nodes, their state and how many ranges each owns",flags:[...Me]},{name:"cluster enable",summary:"SnoutData Cloud: make a project a cluster. The router and the services start in pods of their own while the database keeps running. Each node then uses one of the plan project slots",flags:["--project","--plan","--yes"]},{name:"cluster disable",summary:"SnoutData Cloud: turn a cluster of one node back into a plain project. The data is checked, the cluster is taken apart, the router stops and the services move back into the database pod, which restarts the database once",flags:["--project","--plan","--yes"]},{name:"cluster nodes attach",summary:"SnoutData Cloud: hand a ready node pod made with nodes add --no-join to the cluster, as a standby until a range moves onto it",flags:[...Ct]},{name:"cluster nodes add",summary:"Join a Postgres node: checks its version and settings, syncs roles and reference tables. In SnoutData Cloud it makes a node that starts as a copy of the home node, then joins and takes its share of the data on its own (--no-join to attach it by hand); it uses one of the plan project slots",flags:[...Ct,"--host","--port","--dbname","--sslmode","--peer-host","--no-join"]},{name:"cluster spread",summary:"SnoutData Cloud: spread a table over the cluster in one go: a keyspace for its key (shared with tables keyed the same way), the table distributed by it, then a rebalance",flags:["--project","--column","--key-type","--plan","--yes"]},{name:"cluster retry",summary:"SnoutData Cloud: take up a stopped step again: a node whose join stopped, or --queue for a stopped spread",flags:["--project","--queue"]},{name:"cluster replica add",args:"<node-ref>",summary:"SnoutData Cloud: give a data node a replica, a copy that follows it and takes its place on its own if the node is lost or corrupt. It takes one of the plan project slots, and the node needs its first backup",flags:["--project","--node","--plan","--yes"]},{name:"cluster replica remove",args:"<node-ref>",summary:"SnoutData Cloud: remove a node's replica and free its project slot. If the node is then lost or corrupt, its share of the data is unavailable until it is restored from backups",flags:["--project","--node","--plan","--yes"]},{name:"cluster alerts",summary:"SnoutData Cloud: what happened to the cluster that you must know about (a node lost or corrupt, a failover, a node without its replica), open ones first, with those resolved in the last day",flags:["--project"]},{name:"cluster alerts seen",args:"<id>",summary:"SnoutData Cloud: mark an alert as seen. It stays open until what it is about is over",flags:["--project"]},{name:"cluster nodes drain",summary:"Move every range off a node",flags:[...Re,"--to"]},{name:"cluster nodes remove",summary:"Detach a drained node",flags:[...Re]},{name:"cluster keyspace create",summary:"A keyspace: the shard key type and its initial ranges",flags:[...Ct,"--key-type","--ranges","--seed","--nodes"]},{name:"cluster keyspace drop",summary:"Forget a keyspace no table is distributed by",flags:[...Ct]},{name:"cluster table distribute",summary:"Shard a table by a column, moving its rows out from the home node",flags:[...Re,"--column","--keyspace"]},{name:"cluster table reference",summary:"Copy a table to every node",flags:[...Re]},{name:"cluster autopilot",summary:"The autopilot, off unless you set it: say its mode and what it decided last, or set it to off, advise (record what it would run) or auto (run it); auto asks, or takes --yes",flags:["--project","--interval-ms","--yes"]},{name:"cluster table auto",summary:"Choose a distribution key for every table from the schema (the column most tables reference, else the primary key), say why for each, and distribute what it can in one job",flags:[...Re,"--keyspace","--schemas","--min-bytes"]},{name:"cluster table global",summary:"Bring a table back to the home node alone",flags:[...Re]},{name:"cluster range split",summary:"One range becomes two, and one half moves",flags:[...Re,"--keyspace","--range","--at","--to"]},{name:"cluster range merge",summary:"Two adjacent ranges become one",flags:[...Re]},{name:"cluster range move",summary:"Give a range to another node",flags:[...Re,"--keyspace","--range","--to"]},{name:"cluster tenant pin",summary:"One key value gets its own range, and optionally its own node",flags:[...Re,"--keyspace","--value","--node"]},{name:"cluster rebalance",summary:"Even out the ranges across the nodes",flags:[...Re,"--keyspace"]},{name:"cluster scale",summary:"Add nodes and rebalance, or drain and remove the emptiest",flags:[...Re,"--add","--remove","--nodes","--dbname","--sslmode"]},{name:"cluster verify",summary:"Row counts and a checksum per range, source against target",flags:[...Ct,"--keyspace"]},{name:"cluster cleanup",summary:"Delete the rows a node no longer owns, then VACUUM",flags:[...Re,"--node"]},{name:"cluster dissolve",summary:"A standalone cluster of the home node alone: check the data, take the fences off and forget every keyspace and table, then stop the router yourself",flags:[...Re]},{name:"cluster plan",summary:"The dry run of any operation above: steps, what moves, rows, size, copy time, expected pause",flags:[...Me,...Oo]},{name:"cluster jobs",summary:"The latest jobs",flags:[...Me]},{name:"cluster jobs show",summary:"One job, its plan and its steps",flags:[...Me]},{name:"cluster jobs watch",summary:"Follow a job until it finishes: exit 0 done, 1 failed or cancelled, 10 timed out",flags:[...Me,"--timeout"]},{name:"cluster jobs cancel",summary:"Stop a job: a move before its cutover rolls back, after it finishes its cleanup",flags:[...Me,"--yes"]},{name:"cluster jobs resume",summary:"Run a failed job again from the step that failed, and follow it",flags:[...Me,"--no-wait","--timeout"]},{name:"cluster settings",summary:"The cluster's cutover and advisor settings, or set them",flags:[...Me,...Oo,"--set"]},{name:"cluster advise",summary:"What to split, move or add, from the router's advisor: each recommendation with its reason, its plan and the command that runs it. Runs nothing",flags:[...Me,"--sample-ms"]},{name:"mcp",summary:"Serve these operations as tools over stdio",flags:["--allow-delete"]}],ci={"--ref":"The project. Defaults to SNOUTDATA_PROJECT, then .snoutdata/project.json here or in a parent","functions logs --errors":"Only what was printed with console.error or console.warn, and the errors that stopped a request","functions logs --tail":"How many of the newest lines to show. Defaults to 100, at most 500","functions logs --follow":"Keep printing new lines as they arrive, about a minute after they are printed","containers deploy --port":"The port the container listens on, also given to it as PORT (default 8080)","containers deploy --dockerfile":"A folder with a Dockerfile (or the Dockerfile): built for linux/arm64 with your Docker and uploaded to the project","containers deploy --registry-user":"The user name for a private registry. The token comes on stdin, with --stdin","containers deploy --stdin":"Read the registry token from a pipe, so it is not in shell history or ps","containers deploy --clear-registry":"Forget the stored registry login, for an image that is public now","containers deploy --no-wait":"Answer once it is recorded, without waiting for the host to start it","containers logs --errors":"Only what was printed to stderr","containers logs --tail":"How many of the newest lines to show. Defaults to 100, at most 500","containers logs --follow":"Keep printing new lines as they arrive, about a minute after they are printed","--name":"The project's name","tokens create --name":"What the token is for and where it lives, shown in tokens list","db restore --name":"The name of the new project the restore makes","--region":"Where the project runs. Defaults to the nearest","--env":"Write DATABASE_URL into .env in this folder, and add .env to .gitignore since it holds the password. Without it the URL is printed","--email":"The account to sign in as. The browser offers it, and a different account is reported at once and by whoami","--provider":"google (the default) or github","--sso":"Sign in with your company's identity provider","--domain":"With --sso: your work email or company domain. Only the domain is sent","--no-browser":"Print the sign-in URL instead of opening a browser","--device":"Sign in by typing a code into a browser on any machine: SSH, containers, agents","--timeout":"Seconds to wait before giving up","--check":"Say whether a newer version exists, and install nothing","--expires":"Days until the token stops working. Without it, it works until revoked","--project":"A SnoutData Cloud project ref: its cluster, through the control plane, as you (instead of --admin)","tokens create --project":"Limit the token to one project ref","--team":"Create it in a team (name or id), shared with its members","--no-wait":"Return as soon as it is asked for, without waiting for it to be true","--show-url":"Also print the connection string, which holds the password. Otherwise db url prints it when wanted","--p8":"The APNs .p8 key file (a path, never the key itself)","--key-id":"The APNs key id","--team-id":"Your Apple developer team id","--topic":"The app's bundle id","--environment":"production or sandbox","--file":"A file to read","push credentials set --file":"The FCM service-account JSON","auth template --file":"The email's HTML body","db restore --file":"The dump to restore (from db export or pg_dump -Fc)","--client-id":"Your Google OAuth client id; the secret is read from stdin with --stdin","--stdin":"Read the secret value from a pipe, so it is not in shell history","--site-url":"Where sign-in emails send people by default","--allow":"Comma-separated addresses a sign-in may return to","--subject":"The email's subject line","--dir":"The folder to read from","db push --dir":"The migrations folder (default ./migrations)","start --dir":"The migrations folder (default ./migrations)","functions deploy --dir":"The function's folder (default ./functions/<name>)","--entrypoint":"The file the runtime starts, when it is not index.ts","--no-verify-jwt":"Let anybody with the URL call it (webhooks). By default a caller needs a project key or a user session","--memory":"Memory per worker, in MB, within the plan","--concurrency":"How many workers it may run at once, within the plan","--reset":"Back to the plan's default size","--force":"Do it although it cannot be undone","--days":"How many days of history","--history":"Print the day-by-day table too","--port":"The port the local database listens on (default 54322)","--no-migrations":"Start the database and apply nothing","--out-of-order":"Apply a new migration that sorts before one already applied","--db-url":"Read a postgres:// database instead of a project","--local":"Read the database snoutdata start is running","--schema":"Comma-separated schemas (default public)","--out":"Write to this file instead of stdout","db export --out":"Where to save the dump","--status":"Show the last export without taking another","--dry-run":"Show what would run, and change nothing","--at":"The moment to restore to (an ISO time), into a NEW project","--window":"How far back a point-in-time restore can go","--allow-delete":"Let the agent's delete_project actually delete","--source":"Where the model runs: bedrock, anthropic, openai, openrouter, ollama or compatible","--model":"The model's id, as the provider names it","ai test --model":"The model to call (default: the first chat model served)","ai models add --provider":"The provider to add it to, or the name of a new one (default: the one already connected for --source)","ai models add --region":"Bedrock: the AWS region the model runs in","ai models add --stdin":"Read the provider's key from a pipe (AWS: the secret access key), so it is in neither the shell history nor ps","--base-url":"The provider's OpenAI-compatible address, usually ending in /v1","--price-in":"Dollars per million input tokens. Both rates, or neither to take the shipped catalogue's","--price-out":"Dollars per million output tokens","--access-key-id":"Bedrock with AWS credentials: the access key id (the secret on stdin)","--auth-header":"A header the server checks for its key, instead of a Bearer token","--models":"Comma-separated models the key may call (default: every one)","ai keys create --name":"What the key is for, shown in ai keys","ai routes set --models":"The models to try, comma-separated, in order","ai routes set --sort":"order (as written, the default), price (the cheapest first), or latency or throughput (the fastest first, as measured)","ai keys create --expires":"Days until the key stops working. Without it, it works until revoked","--amount":"Dollars","--period":"day (the default) or month","--soft":"Warn in the log when it is passed, instead of refusing","--by":"Group by day, model, provider, operation, key, user or group (default model)","ai usage --days":"How many days, up to 366 (default 30)","--prompt":"What to ask (default: a five-word hello)","--channel":"Only this channel","--since":"How far back, like 10m, 2h or 1d (default 1h)","--watch":"Keep printing what changes until interrupted","--admin":"The Lepis router's admin API, like http://127.0.0.1:7432 (LEPIS_ADMIN_ADDR). https for any other machine; NODE_EXTRA_CA_CERTS for a private CA. Defaults to LEPIS_ADMIN_URL","--token":"The router's admin token. Prefer LEPIS_ADMIN_TOKEN, which keeps it out of shell history and ps","--plan":"Show the plan (steps, what moves, rows, size, copy time, expected pause) and change nothing","--yes":"Run it without asking. Without a terminal to ask at, an operation that moves or deletes data needs it","cluster jobs cancel --yes":"Cancel without asking","cluster nodes add --port":"The node's port (default 5432)","--host":"The address the router reaches the node at","--dbname":"The database on the node (default postgres)","--sslmode":"disable, require or verify-full (the default)","--peer-host":"The address the other nodes reach it at, when it differs from the router's","--to":"The node (id or name) that receives it","cluster nodes drain --to":"Comma-separated nodes to move its ranges to. Default: the least loaded","--key-type":"The distribution key's Postgres type: bigint, int, text, uuid, ...","--ranges":"How many equal ranges to start with","--seed":"The hash seed","--nodes":"Comma-separated nodes (ids or names) to place the ranges on","--column":"The distribution key column","--no-join":"SnoutData Cloud: leave the new node for you to attach with cluster nodes attach, instead of joining it on its own","--queue":"The stopped request (a table being spread) rather than a node","--keyspace":"The keyspace","--interval-ms":"How often the autopilot asks the advisor, in milliseconds (default 60000, at least 5000)","--schemas":"Comma-separated schemas whose tables table auto reads (default public)","--min-bytes":"The size below which table auto leaves a table keyed only by its own primary key unspread (default 16 MiB)","--range":"The range's lower bound, with --keyspace: the spelling for a bound that starts with a dash","cluster range split --at":"Where to split (a hash value). Default: the middle","--node":"A node, by id or name","cluster cleanup --node":"Only this node. Default: every node","cluster replica add --node":"The node pod's ref, as cluster status lists it (or as the first word)","cluster replica remove --node":"The node pod's ref, as cluster status lists it (or as the first word)","--value":"The key value to pin","--add":"Comma-separated nodes to add, each name=host[:port]","--remove":"How many of the emptiest nodes to drain and remove","cluster scale --nodes":"SnoutData Cloud: how many nodes the project should have, the home included","--max-write-pause-ms":"The longest a cutover may pause writes; one that would take longer is postponed","--drain-timeout-ms":"How long a cutover waits for open transactions to finish","--set":"Settings by their API names, name=value,... (the advisor's advice_* thresholds among them)","--sample-ms":"How long to measure write rates over, in ms (default: the advice_sample_ms setting; 0 skips them)","--ack-timeout-ms":"How long a cutover waits for every router to acknowledge it","--copy-mb-per-s":"The copy rate a plan estimates with (chosen, not measured)","--level":"full (everything the project password can do) or read (reads the project's own tables, never the auth or storage schemas, and writes nothing). Default read"};function Io(e,t){return ci[`${e} ${t}`]??ci[t]??""}function di(e,t){let n=No.filter(s=>s.name===e||s.name.startsWith(`${e} `));if(!t)return n;let r=`${e} ${t}`,o=n.filter(s=>s.name===r||s.name.startsWith(`${r} `));return o.length>0?o:n}me();Se();ue();import{spawn as yg}from"node:child_process";function wg(e){return`snout-${e}`}function Ht(e,t,n){return new Promise((r,o)=>{let s=yg("docker",["compose","-p",wg(e.ref),"--project-directory",e.folder,...t],{cwd:e.folder,stdio:["ignore","pipe","pipe"]}),a="",c="";s.stdout.on("data",i=>{a+=String(i)}),s.stderr.on("data",i=>{if(c+=String(i),n)for(let u of String(i).split(/\r?\n/).filter(Boolean))d(b(` ${u.trim()}`))}),s.on("error",i=>{if(i.code==="ENOENT"){o(new T("tool-missing","docker is not installed, or not on PATH. A local project runs in Docker (Docker Desktop on Windows and macOS)."));return}o(new T("failed",String(i)))}),s.on("close",i=>r({code:i??0,out:a,err:c}))})}async function bg(e){let t=await Ht(e,["ps","-a","--format","json"],!1);if(t.code!==0)throw new T("failed",t.err.trim()||"docker compose ps failed. Is Docker running?");let n=t.out.trim();return(n.startsWith("[")?JSON.parse(n):n.split(/\r?\n/).filter(Boolean).map(o=>JSON.parse(o))).map(o=>({service:o.Service??"?",state:o.State==="exited"&&o.ExitCode===0?"done":o.State??"?",health:o.Health??""})).sort((o,s)=>o.service.localeCompare(s.service))}async function Xi(e){let t=await bg(e),n=t.filter(o=>o.state==="running").length,r=t.filter(o=>o.state!=="done").length;return w({ref:e.ref,local:e.folder,api:e.apiUrl,dbPort:e.dbPort,running:n,services:t},()=>{if(d(`${O(e.name??e.ref)} (local, ${e.folder})`),t.length===0){d("Not started. `snoutdata start` brings it up.");return}process.stdout.write(`${W([["SERVICE","STATE","HEALTH"],...t.map(o=>[o.service,o.state,o.health])])}
|
|
469
|
+
`),d(`${n} of ${r} services running. API ${e.apiUrl}, database 127.0.0.1:${e.dbPort}.`)}),0}async function Zi(e){d(`Starting ${e.name??e.ref} (local)\u2026`);let t=await Ht(e,["up","-d","--wait","--wait-timeout","600"],!0);if(t.code!==0)throw new T("failed",`docker compose up failed: ${t.err.trim().split(/\r?\n/).slice(-3).join(" ")}`);return w({ref:e.ref,api:e.apiUrl,dbPort:e.dbPort,started:!0},()=>{d(`Running. API ${e.apiUrl}, database 127.0.0.1:${e.dbPort}. \`snoutdata db url\` prints the connection string.`)}),0}async function Qi(e){let t=await Ht(e,["stop"],!0);if(t.code!==0)throw new T("failed",`docker compose stop failed: ${t.err.trim().split(/\r?\n/).slice(-3).join(" ")}`);return w({ref:e.ref,stopped:!0},()=>d(`Stopped ${e.name??e.ref}. Its data is kept; \`snoutdata start\` brings it back.`)),0}function el(e){w({ref:e.ref,name:e.name,local:e.folder,api:e.apiUrl,dbPort:e.dbPort},()=>{process.stdout.write(`${W([["name",e.name??"(not set up by Studio)"],["ref",e.ref],["where",`local, ${e.folder}`],["api",e.apiUrl],["database",`127.0.0.1:${e.dbPort}`]])}
|
|
470
|
+
`)})}an();import{cpSync as rl,existsSync as hn,readdirSync as Eg,readFileSync as ol,rmSync as sl,statSync as Ag,writeFileSync as al}from"node:fs";import{join as Le,resolve as fr}from"node:path";var Sg=/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/;function _g(e){return Sg.test(e)}function Et(e){if(!_g(e))throw new Error(`not a function name: ${JSON.stringify(e)}. Letters, digits, hyphens and underscores, up to 48 characters, starting with a letter or a digit.`);return e}var kg=/^[A-Za-z0-9_][A-Za-z0-9._-]*$/;function vg(e){if(e.length===0||e.length>255||e.includes("\\")||e.startsWith("/"))return!1;let t=e.split("/");return t.length>8?!1:t.every(n=>n!=="."&&n!==".."&&kg.test(n))}var Xo=2*1024*1024,tl=200;function nl(e){let{entrypoint:t}=e;if(e.files.length===0)throw new Error("there is nothing in this function to deploy");if(e.files.length>tl)throw new Error(`a function may hold ${tl} files and this one has ${e.files.length}`);let n=new Set;for(let a of e.files){if(!vg(a.path))throw new Error(`${JSON.stringify(a.path)} is not a path a function may contain`);if(n.has(a.path))throw new Error(`${a.path} appears twice`);n.add(a.path)}if(!n.has(t))throw new Error(`the entrypoint ${JSON.stringify(t)} is not one of the files`);let r=[...e.files].sort((a,c)=>a.path<c.path?-1:a.path>c.path?1:0),o={snoutfn:1,entrypoint:t,files:r},s=$g(o);if(s.length>Xo)throw new Error(`this function is ${Math.round(s.length/1024)} KB and the limit is ${Xo/1024} KB. A function is source code; put large files in Storage.`);return o}function $g(e){let t=e.files.map(r=>`{"path":${JSON.stringify(r.path)},"text":${JSON.stringify(r.text)}}`).join(","),n=`{"snoutfn":${e.snoutfn},"entrypoint":${JSON.stringify(e.entrypoint)},"files":[${t}]}`;return new TextEncoder().encode(n)}var Pg=/^[A-Za-z_][A-Za-z0-9_]{0,63}$/;var xg=["SNOUTDATA_","SNOUT_"];function gn(e){if(!Pg.test(e))return`${JSON.stringify(e)} is not an environment variable name: letters, digits and underscores, not starting with a digit.`;let t=xg.find(n=>e.startsWith(n));return t?`names beginning with ${t} are set by the platform, so ${e} cannot be one of yours.`:null}var kv=64*1024;Se();Ae();ue();function il(e){let t=Le(e.folder,"functions");return hn(t)?Eg(t).filter(n=>!n.startsWith(".")&&!n.startsWith("_")&&Ag(Le(t,n)).isDirectory()).sort():[]}async function gr(e){d(b(" docker compose run --rm functions-deploy"));let t=await Ht(e,["run","--rm","functions-deploy"],!0);if(t.code!==0)throw new T("failed",`functions-deploy failed: ${t.err.trim().split(/\r?\n/).slice(-3).join(" ")}`)}function ll(e,t,n){let r=Le(e.folder,".env"),o=ol(r,"utf8"),s=(qn(o).get("FUNCTIONS_NO_VERIFY_JWT")??"").split(",").map(u=>u.trim()).filter(Boolean),a=n?[...new Set([...s,t])]:s.filter(u=>u!==t);if(a.join(",")===s.join(","))return!1;let c=`FUNCTIONS_NO_VERIFY_JWT=${a.join(",")}`,i=/^FUNCTIONS_NO_VERIFY_JWT=.*$/m;return al(r,i.test(o)?o.replace(i,c):`${o.replace(/\n?$/,`
|
|
471
|
+
`)}${c}
|
|
472
|
+
`),!0}async function Zo(e,t,n){Et(t);let r=fr(n.dir??Le(process.cwd(),"functions",t));if(!hn(Le(r,"index.ts")))throw new g(`${r} has no index.ts. A local function is a folder with an index.ts in it.`);let o=Le(e.folder,"functions",t);if(fr(o)!==r){sl(o,{recursive:!0,force:!0}),rl(r,o,{recursive:!0});let a=Le(r,"..","_shared");hn(a)&&fr(a)!==fr(Le(e.folder,"functions","_shared"))&&rl(a,Le(e.folder,"functions","_shared"),{recursive:!0})}let s=ll(e,t,!n.verifyJwt);if(await gr(e),s){let a=await Ht(e,["up","-d","--wait","--wait-timeout","600"],!1);if(a.code!==0)throw new T("failed",`docker compose up failed after changing who may call ${t}: ${a.err.trim().split(/\r?\n/).slice(-2).join(" ")}`)}return{ref:e.ref,name:t,url:Qo(e,t),verifyJwt:n.verifyJwt,local:e.folder}}async function cl(e,t,n){let r=await Zo(e,t,n);w(r,()=>{d(`${O(t)} deployed locally: ${r.url}`),n.verifyJwt||d(b(" Callable with no key (FUNCTIONS_NO_VERIFY_JWT), so it must check what calls it itself."))})}function Qo(e,t){return`${e.apiUrl}/functions/v1/${t}`}function hr(e){return{ref:e.ref,local:e.folder,functions:il(e).map(t=>({name:t,url:Qo(e,t)}))}}function dl(e){let t=il(e);w(hr(e),()=>{if(t.length===0){d(`No functions in ${Le(e.folder,"functions")}.`);return}process.stdout.write(`${W([["NAME","URL"],...t.map(n=>[n,Qo(e,n)])])}
|
|
473
|
+
`)})}async function es(e,t){Et(t);let n=Le(e.folder,"functions",t);if(!hn(n))throw new T("not-found",`this local project has no function called ${t}`);return sl(n,{recursive:!0,force:!0}),ll(e,t,!1),await gr(e),{ref:e.ref,name:t,removed:!0}}async function ul(e,t){w(await es(e,t),()=>d(`${t} is gone.`))}function yr(e){return Le(e.folder,"functions",".env")}function ts(e){let t=yr(e);return hn(t)?qn(ol(t,"utf8")):new Map}function pl(e,t){let n=r=>r.includes("'")?JSON.stringify(r):`'${r}'`;al(yr(e),[...t].map(([r,o])=>`${r}=${n(o)}`).join(`
|
|
449
474
|
`)+(t.size?`
|
|
450
|
-
`:""))}async function
|
|
451
|
-
`):`No function secrets in ${
|
|
452
|
-
`)})}function
|
|
453
|
-
`),d(""),d(`Ready. ${
|
|
454
|
-
`),d(`New password for ${t.user}. It applies ${t.appliesIn}.`)})}async function
|
|
455
|
-
`)})}async function
|
|
456
|
-
`),d(""),d(`${
|
|
457
|
-
`)})}async function
|
|
458
|
-
`),
|
|
459
|
-
`),d(""),d(n.breaks),d(
|
|
460
|
-
`),t.limit.maxFunctions!==null&&d(
|
|
461
|
-
`)}),!n.follow)return;let s=o.at(-1)?.at??new Date().toISOString();for(;;){await new Promise(
|
|
462
|
-
`),s=l.at}}async function yi(e,t){let n=await uo(e,t);h(n,()=>{d(n.live?`${t} is gone.`:`${t} is gone. It stops answering within a few seconds.`)})}re();import{spawn as Jm}from"node:child_process";import{createHash as Vm}from"node:crypto";import{createReadStream as Ei}from"node:fs";import{mkdtemp as Ym,rm as Xm,stat as Wn}from"node:fs/promises";import{request as Zm}from"node:http";import{request as Qm}from"node:https";import{tmpdir as ef}from"node:os";import{basename as tf,dirname as nf,join as yo}from"node:path";var Pm="x-snoutdata-upload-bytes",Em="x-snoutdata-upload-sha256",Am="/done",Rm=/^[0-9a-f]{64}$/,Tm=/^sha256:[0-9a-f]{64}$/;function Om(e){return Rm.test(e)}var ve=class extends Error{constructor(n,r){super(`the upload failed (${n})`);this.status=n;this.body=r}};function mo(e){try{let t=JSON.parse(e);return t&&typeof t=="object"?t:{}}catch{return{}}}function wi(e){let t=String(mo(e).digest??"");if(!Tm.test(t))throw new ve(502,JSON.stringify({error:"The upload finished without saying what it stored, so there is nothing to deploy. Build again."}));return t}var Nm=15*6e4,Im=2e3;async function bi(e){let{ticketUrl:t,file:n,transport:r}=e;if(!Om(e.sha256))throw new Error("an upload is declared with its SHA-256, in hex");let o=await r.call("POST",t,{[Pm]:String(e.bytes),[Em]:e.sha256});if(o.status===405){e.step?.("uploading");let c=await r.put(t,n,{});if(c.status!==200)throw new ve(c.status,c.body);return wi(c.body)}if(o.status!==200)throw new ve(o.status,o.body);let s=mo(o.body);if(typeof s.url!="string"||!s.url.startsWith("https://")||!s.headers||typeof s.headers!="object")throw new ve(502,JSON.stringify({error:"The project's host did not say where to put the image. Build again in a moment."}));e.step?.("uploading");let a=await r.put(s.url,n,s.headers);if(a.status!==200){let c=/<Code>([^<]+)<\/Code>/.exec(a.body)?.[1],p=c==="BadDigest"||c==="XAmzContentSHA256Mismatch"?"The image changed or was damaged while it was uploading. Build again.":c==="AccessDenied"||c==="SignatureDoesNotMatch"||c==="RequestTimeTooSkewed"?"The upload link was refused (it lasts 15 minutes, and this computer's clock must be right). Build again.":`The upload to storage failed (${a.status}). Build again in a moment.`;throw new ve(a.status===403?401:502,JSON.stringify({error:p}))}e.step?.("storing");let l=r.now()+(e.waitMs??Nm),i=e.pollMs??Im,u=!1;for(;!u;){let c=await r.call("POST",`${t}${Am}`,{});if(c.status===200||c.status===202)u=!0;else if(c.status===429&&r.now()<l)await r.sleep(i*5);else throw new ve(c.status,c.body)}for(;;){let c=await r.call("GET",t,{});if(c.status!==200)throw new ve(c.status,c.body);let p=mo(c.body);if(p.state==="stored")return wi(c.body);if(p.state==="failed")throw new ve(typeof p.status=="number"?p.status:502,JSON.stringify({error:p.error??"The image could not be stored. Build again in a moment."}));if(r.now()>=l)throw new ve(504,JSON.stringify({error:"The project's host took too long to store the image. Build again in a moment."}));await r.sleep(i)}}te();var Cm=/^[a-z0-9][a-z0-9_-]{0,31}$/;function jm(e){return Cm.test(e)}function Si(e){return jm(e)?null:`${JSON.stringify(e)} is not a container name: lower-case letters, digits, hyphens and underscores, up to 32 characters, starting with a letter or a digit.`}var Dm=/^sha256:[0-9a-f]{64}$/,Mm=/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/,Lm=/^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*$/,Um=/^(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*(?::[0-9]{1,5})?$/;function Bm(e){return Dm.test(e)}function ki(e){let t=e.trim();if(t.length===0||t.length>512||/\s/.test(t))return{error:`${JSON.stringify(e)} is not an image reference.`};let n=t,r=null,o=n.indexOf("@");if(o!==-1&&(r=n.slice(o+1),n=n.slice(0,o),!Bm(r)))return{error:`${JSON.stringify(r)} is not a sha256 digest.`};let s=null,a=n.lastIndexOf("/"),l=n.lastIndexOf(":");if(l>a&&(s=n.slice(l+1),n=n.slice(0,l),!Mm.test(s)))return{error:`${JSON.stringify(s)} is not an image tag.`};let i=n.split("/"),u="docker.io",c=i[0]??"";return i.length>1&&(c.includes(".")||c.includes(":")||c==="localhost")&&(u=c.toLowerCase(),i.shift()),Um.test(u)?((u==="index.docker.io"||u==="registry-1.docker.io")&&(u="docker.io"),u==="docker.io"&&i.length===1&&i.unshift("library"),i.length===0||i.some(p=>!Lm.test(p))?{error:`${JSON.stringify(e)} is not an image name: a repository is lower-case letters, digits and separators.`}:(s===null&&r===null&&(s="latest"),{registry:u,repository:i.join("/"),tag:s,digest:r})):{error:`${JSON.stringify(c)} is not a registry address.`}}var fS=["application/vnd.oci.image.index.v1+json","application/vnd.docker.distribution.manifest.list.v2+json","application/vnd.oci.image.manifest.v1+json","application/vnd.docker.distribution.manifest.v2+json"].join(", ");function fo(e){let t=e.split(".");if(t.length!==4)return null;let n=[];for(let r of t){if(!/^\d{1,3}$/.test(r))return null;let o=Number(r);if(o>255)return null;n.push(o)}return n}function Fm(e){let t=e.replace(/^\[|\]$/g,""),n=t.indexOf("%");if(n!==-1&&(t=t.slice(0,n)),!/^[0-9a-fA-F:.]+$/.test(t)||!t.includes(":"))return null;let r=t.lastIndexOf(":");if(t.slice(r+1).includes(".")){let c=fo(t.slice(r+1));if(!c)return null;t=`${t.slice(0,r+1)}${(c[0]<<8|c[1]).toString(16)}:${(c[2]<<8|c[3]).toString(16)}`}let o=t.split("::");if(o.length>2)return null;let s=c=>c===""?[]:c.split(":"),a=s(o[0]??""),l=o.length===2?s(o[1]??""):[];if([...a,...l].some(c=>!/^[0-9a-fA-F]{1,4}$/.test(c)))return null;let i=a.length+l.length;if(o.length===1?i!==8:i>7)return null;let u=new Array(8-i).fill(0);return[...a.map(c=>parseInt(c,16)),...u,...l.map(c=>parseInt(c,16))]}function Hm(e,t,n){let r=fo(t),o=a=>(a[0]<<24|a[1]<<16|a[2]<<8|a[3])>>>0,s=n===0?0:4294967295<<32-n>>>0;return(o(e)&s)===(o(r)&s)}var Km=[["0.0.0.0",8],["10.0.0.0",8],["100.64.0.0",10],["127.0.0.0",8],["169.254.0.0",16],["172.16.0.0",12],["192.0.0.0",24],["192.0.2.0",24],["192.88.99.0",24],["192.168.0.0",16],["198.18.0.0",15],["198.51.100.0",24],["203.0.113.0",24],["224.0.0.0",4],["240.0.0.0",4]];var gS=1024**3;var _i=16,qm=/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,62}$/,Gm=/^(\d{1,3}(?:\.\d{1,3}){3})(?:\/(\d{1,2}))?$/;function Wm(e,t){let n=e??[],r={error:`${t}: ports are up to ${_i} whole numbers, 1 to 65535.`};if(!Array.isArray(n)||n.length>_i)return r;let o=n.map(s=>typeof s=="string"&&/^\d{1,5}$/.test(s)?Number(s):s);return o.some(s=>typeof s!="number"||!Number.isInteger(s)||s<1||s>65535)?r:[...new Set(o)].sort((s,a)=>s-a)}function zm(e){let t=e.trim().toLowerCase(),n=Gm.exec(t);if(n){let r=fo(n[1]??""),o=n[2]===void 0?32:Number(n[2]);if(!r||o>32)return{error:`${e} is not an IPv4 address or range.`};let s=(r[0]<<24|r[1]<<16|r[2]<<8|r[3])>>>0,a=o===0?0:4294967295<<32-o>>>0,l=(s&a)>>>0,i=[24,16,8,0].map(u=>l>>>u&255);return Km.some(([u,c])=>o>=c&&Hm(i,u,c))?{error:`${e} is not on the public internet, and a container reaches nothing else.`}:o===32?i.join("."):`${i.join(".")}/${o}`}return Fm(t)?{error:`${e} is an IPv6 address. Containers send over IPv4 only; list its IPv4 address or its host name.`}:t.startsWith("*.")?{error:`${e}: a wildcard cannot be looked up. List each host name, or the address range.`}:qm.test(t)?t:/^[a-z0-9-]{1,63}$/.test(t)?{error:`${e} is not a whole host name. Give it with its domain, like api.example.com.`}:{error:`${e} is not a host name, an IPv4 address or an IPv4 range.`}}function vi(e){let t=e.trim(),n=t.lastIndexOf(":"),r=n!==-1&&t.indexOf(":")===n,o=r?t.slice(0,n):t,s=Wm(r?t.slice(n+1).split(",").map(l=>l.trim()):[],o.trim());if("error"in s)return s;let a=zm(o);return typeof a!="string"?a:{destination:a,ports:s}}function go(e){return e.ports.length===0?e.destination:`${e.destination}:${e.ports.join(",")}`}var rf=new Set(["pending","pulling","starting","restarting"]),of=5*6e4;function Vn(e){let t=Si(e);if(t)throw new Error(t)}function wo(e,t,n){Vn(t);let r=ki(n.image);if("error"in r)throw new Error(r.error);return _("cloud-project-containers",{ref:e,action:"deploy",name:t,image:n.image,...n.port===void 0?{}:{port:n.port},...n.registryUser?{registryUser:n.registryUser,registryPassword:n.registryPassword??""}:{},...n.clearCredential?{clearCredential:!0}:{}})}function kt(e){return _("cloud-project-containers",{ref:e})}function bo(e,t){return Vn(t),_("cloud-project-containers",{ref:e,action:"delete",name:t})}function _o(e,t){return Vn(t),_("cloud-project-containers",{ref:e,action:"restart",name:t})}function zn(e,t){return _("cloud-project-containers",{ref:e,action:"logs",...t.name?{name:t.name}:{},...t.errorsOnly?{errorsOnly:!0}:{},...t.since?{since:t.since}:{},limit:t.limit??100})}async function Ai(e,t){let n=Date.now()+of,r=null;for(;Date.now()<n;)if(await new Promise(o=>setTimeout(o,3e3)),r=(await kt(e)).containers.find(o=>o.name===t)??null,!r||!rf.has(r.status))return r;return r}function So(e){if(!e.allowed)return"stopped: not covered by the plan";let t=e.restarts>0?`, ${e.restarts} restart${e.restarts===1?"":"s"}`:"";return`${e.status}${t}`}async function Ri(e,t,n){await Ti(e,t,await wo(e,t,n),n.wait!==!1)}async function Ti(e,t,n,r){let o=n.containers.find(s=>s.name===t)??null;r&&o&&(d(w(`${t}: ${o.image} pinned to ${o.digest.slice(0,19)}\u2026, waiting for the host`)),o=await Ai(e,t)??o),h({...n,containers:o?[o]:n.containers},()=>{if(!o){d(`${x(t)} deployed.`);return}d(`${x(t)} ${So(o)}`),d(` ${o.url}`),o.statusDetail&&d(` ${o.statusDetail}`),d(""),d(w(" Listens on PORT ("+o.port+"). DATABASE_URL and your secrets are in its environment.")),d(x(" Anything written inside the container is lost when it restarts or is redeployed.")),d(w(" Keep what matters in your database.")),n.allowance&&d(w(` ${n.allowance.deployed} of ${n.allowance.maxContainers} containers on this plan, ${n.allowance.memoryMb} MB and ${n.allowance.cpuPercent}% of a core each`))})}var sf=2*1024**3,af=3*6e4;async function cf(e){let t;try{t=await Wn(e)}catch{throw new Error(`${e} does not exist: --dockerfile takes a folder with a Dockerfile in it, or the Dockerfile.`)}if(t.isDirectory()){try{await Wn(yo(e,"Dockerfile"))}catch{throw new Error(`${e} has no Dockerfile in it.`)}return{context:e,file:null}}return{context:nf(e),file:e}}function lf(e){return["buildx","build","--platform","linux/arm64","--provenance=false","--output",`type=oci,dest=${e.archive}`,...e.file?["--file",e.file]:[],e.context]}function ko(e){return e.replace(/\b\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com(?:\/[^\s"',;)]*)?/g,"SnoutData's registry").replace(/\barn:aws[a-z-]*:[^\s"',;)]*/g,"a SnoutData credential").replace(/\b[a-z0-9.-]*\.amazonaws\.com(?:\/[^\s"',;)]*)?/g,"SnoutData Cloud").replace(/\b\d{12}\b/g,"(an account)")}function df(e,t){let n="";try{let r=JSON.parse(t);n=String(r.error??r.message??"")}catch{}return n?ko(n):e===413?"the image is too large to upload: a container's image may be at most 1 GB compressed; a slimmer base (alpine, distroless, -slim) usually fixes it.":`the upload failed (${e}); build again in a moment.`}function uf(e){return new Promise((t,n)=>{let r=Jm("docker",e,{stdio:["ignore",2,2]});r.on("error",o=>n(o)),r.on("close",o=>t(o??1))})}function Oi(e,t,n,r){return new Promise((o,s)=>{let a=new URL(e),l=(a.protocol==="http:"?Zm:Qm)(a,{method:t,headers:n});l.on("response",i=>{let u=[];i.on("data",c=>u.push(c)),i.on("end",()=>o({status:i.statusCode??0,body:Buffer.concat(u).toString("utf8")})),i.on("error",s)}),l.on("error",i=>{s(i)}),r(l)})}async function pf(e,t,n){let r=Object.keys(n).length>0?n:{"content-type":"application/octet-stream","content-length":String((await Wn(t)).size)};return Oi(e,"PUT",r,o=>{let s=Ei(t);s.on("error",a=>o.destroy(a)),s.pipe(o)})}function mf(e){return new Promise((t,n)=>{let r=Vm("sha256");Ei(e).on("error",n).on("data",o=>r.update(o)).on("end",()=>t(r.digest("hex")))})}var ff={call:(e,t,n)=>Oi(t,e,{"content-length":"0",...n},r=>r.end()),put:pf,sleep:e=>new Promise(t=>setTimeout(t,e)),now:()=>Date.now()},Ni={docker:uf,scratch:()=>Ym(yo(ef(),"snoutdata-build-")),remove:e=>Xm(e,{recursive:!0,force:!0}),size:async e=>(await Wn(e)).size,hash:mf,transport:ff,ask:e=>_("cloud-project-containers",e),say:e=>d(w(e)),now:()=>Date.now()};function $i(e){return`${Math.max(1,Math.round(e/1024**2))} MB`}async function xi(e,t,n){let r=await e.ask({ref:t,action:"upload-ticket",name:n});if(typeof r.url!="string"||!r.url.startsWith("https://"))throw new Error("SnoutData Cloud did not hand over an upload link; try again in a moment.");return{url:r.url,expiresAt:String(r.expiresAt??"")}}async function gf(e,t,n,r=Ni){try{if(await r.docker(["version","--format","{{.Server.Version}}"])!==0)throw new Error("docker answered with an error")}catch{throw new Error("deploy --dockerfile builds with your own Docker, and none is running here. Start Docker, or deploy an image from a registry.")}try{let o=await xi(r,e,t),s=await r.scratch();try{let a=yo(s,"image.tar");if(r.say(`${t}: building ${n.file?tf(n.file):n.context} for linux/arm64`),await r.docker(lf({...n,archive:a}))!==0)throw new Error("the build failed; its output is above. If it says the docker driver cannot export OCI, turn on the containerd image store, or run: docker buildx create --use");let l=await r.size(a).catch(()=>0);if(l<=0)throw new Error("docker finished but wrote no image archive, so there is nothing to upload.");if(l>sf)throw new Error(`the image is ${$i(l)}, and a container's image may be at most 1 GB compressed. A slimmer base (alpine, distroless, -slim) usually fixes it.`);Date.parse(o.expiresAt)-r.now()>af||(o=await xi(r,e,t));let i=await r.hash(a);r.say(`${t}: uploading the image (${$i(l)})`);try{return await bi({ticketUrl:o.url,file:a,bytes:l,sha256:i,transport:r.transport,step:u=>{u==="storing"&&r.say(`${t}: storing the image in the project`)}})}catch(u){throw u instanceof ve?new Error(df(u.status,u.body)):u}}finally{await r.remove(s).catch(()=>{})}}catch(o){throw new Error(ko(o instanceof Error?o.message:String(o)))}}async function Ii(e,t,n,r=Ni){Vn(t);let o=await cf(n.path),s=await gf(e,t,o,r),a;try{a=await r.ask({ref:e,action:"deploy",name:t,pushed:s,...n.port===void 0?{}:{port:n.port}})}catch(l){throw new Error(ko(l instanceof Error?l.message:String(l)))}await Ti(e,t,a,n.wait!==!1)}async function Ci(e){let t=await kt(e);h(t,()=>{if(t.containers.length===0){d(`No containers on ${e}.`),t.allowance&&t.allowance.maxContainers===0?d(w(" Containers run on the Plus and Pro plans.")):d(w(" snoutdata containers deploy <name> <image>"));return}d(L([["name","status","image","url"],...t.containers.map(r=>[r.name,So(r),r.image,r.url])]));for(let r of t.containers)r.statusDetail&&r.status!=="running"&&d(w(` ${r.name}: ${r.statusDetail}`));let n=t.allowance?.egress;n&&n.allowanceBytes>0&&d(w(` Sent this month: ${ho(n.usedBytes)} of ${ho(n.allowanceBytes)}. At ${ho(n.allowanceBytes)} the project's containers stop until the 1st.`))})}async function Jn(e,t){if(t===null)return _("cloud-project-containers",{ref:e,action:"egress",clear:!0});let n=[];for(let r of t){let o=vi(r);if("error"in o)throw new Error(o.error);n.push(o)}return _("cloud-project-containers",{ref:e,action:"egress",allow:n})}function hf(e){if(e.allowlist===void 0)return["This control plane does not know about allowlists yet."];if(e.allowlist===null)return[`Containers on ${e.ref} may reach the public internet (within the limits on how much and how fast).`,w(" snoutdata containers egress set <host[:port]> ... to allow only some destinations")];let t=[e.allowlist.allow.length===0?`Containers on ${e.ref} may reach nothing outside the project.`:`Containers on ${e.ref} may reach only:`];for(let n of e.allowlist.allow)t.push(` ${go(n)}${n.ports.length===0?w(" (any port)"):""}`);return t.push(w(` and always: ${e.allowlist.alwaysAllowed.map(go).join(", ")}, and each other`)),t.push(w(" snoutdata containers egress clear to allow the public internet again")),t}async function Yn(e,t){let n=t.clear?await Jn(e,null):t.set?await Jn(e,t.set):await kt(e);h({ref:n.ref,allowlist:n.allowlist??null},()=>{for(let r of hf(n))d(r);(t.set||t.clear)&&d(w(" Running containers follow it within a minute; nothing restarts."))})}function ho(e){return`${Math.round(e/1024**3*10)/10} GB`}async function ji(e,t){await _o(e,t);let n=await Ai(e,t);h({ref:e,containers:n?[n]:[]},()=>{d(n?`${x(t)} ${So(n)}`:`${t} is restarting.`),n?.statusDetail&&d(` ${n.statusDetail}`)})}async function Di(e,t){let n=await bo(e,t);h(n,()=>d(`${t} is gone. It stops within a few seconds.`))}function Pi(e){return`${w(e.at.replace("T"," ").slice(0,19))} ${e.name} ${e.stream==="stderr"?x(e.message):e.message}`}async function Mi(e,t,n){let r=((await zn(e,{name:t,errorsOnly:n.errorsOnly,limit:n.limit})).lines??[]).slice().reverse();if(h({ref:e,lines:r},()=>{if(r.length===0&&!n.follow){d(`Nothing printed${t?` by ${t}`:""} on ${e} in the last seven days.`),d(w(" Lines show up about a minute after a container prints them."));return}for(let s of r)process.stdout.write(`${Pi(s)}
|
|
463
|
-
`)}),!n.follow)return;let o=r.at(-1)?.at??new Date().toISOString();for(;;){await new Promise(a=>setTimeout(a,5e3));let s=((await
|
|
464
|
-
`),o=a.at}}
|
|
465
|
-
`),d(
|
|
466
|
-
`).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}}var
|
|
467
|
-
`)){let u=vf(i);if(!u)continue;let{type:c,schema:p,name:f,owner:k}=u;if(c==="TABLE DATA"||c==="SEQUENCE SET"){let V=/^(schema_migrations|migrations)$/.test(f)&&Xn.has(p);if(/realtime/.test(p)||p==="snout_live"||V||p==="public"&&f==="spatial_ref_sys"){s++;continue}if(p==="cron"){s++,o.add("Scheduled jobs (pg_cron) are not restored: each names the project it came from. Schedule them again with cron.schedule.");continue}if(p==="push"&&f==="credentials"){s++,o.add("Push credentials are not restored: they are sealed for the project they came from. Set them again with `snoutdata push credentials set`.");continue}let j=_f[p];if(j){j==="storage"&&(f==="objects"||f==="files")&&o.add("Storage objects are restored as rows; the files themselves are not part of an export. Upload them again into this project."),n.set(j,[...n.get(j)??[],u]);continue}if(Xn.has(p)){s++;continue}r.push(u.line);continue}if(c==="COMMENT"&&p==="-"&&/^EXTENSION( |$)/.test(f)||(c==="SCHEMA"?Xn.has(f):Xn.has(p))||bf.test(f)||wf.test(k)&&c!=="SCHEMA"){s++;continue}r.push(u.line)}let a=$f(t),l=["auth","storage","push"];return{platformData:l.filter(i=>n.has(i)).map(i=>{let u=n.get(i),c=u.filter(f=>f.type==="TABLE DATA"&&Sf.has(`${f.schema}.${f.name}`)).map(f=>`${f.schema}.${f.name}`),p=u.filter(f=>f.type==="TABLE DATA").map(f=>`${f.schema}.${f.name}`);return{product:i,clear:c,tables:p,lines:xf(u,a)}}),rest:r,products:l.filter(i=>n.has(i)),skipped:s,notes:[...o]}}function xf(e,t){let n=l=>`${l.schema}.${l.name}`,r=new Set(e.map(n)),o=new Set,s=[],a=[...e];for(;a.length>0;){let i=a.find(u=>[...t.get(n(u))??[]].every(c=>!r.has(c)||o.has(c)||c===n(u)))??a[0];o.add(n(i)),s.push(i.line),a=a.filter(u=>u!==i)}return s}re();be();me();te();re();me();var xo=["max_write_pause_ms","drain_timeout_ms","ack_timeout_ms","copy_mb_per_s"],Vi=["advice_sample_ms","advice_min_bytes","advice_split_bytes","advice_skew_pct","advice_hot_pct","advice_min_writes_per_s","advice_pin_pct","advice_node_disk_bytes","advice_disk_pct","advice_conn_pct"];function qt(e){return e==="done"||e==="failed"||e==="cancelled"}function Gt(e,t=process.env){if(e.project!==void 0){if(e.admin!==void 0)throw new P("usage","cluster takes --project (a SnoutData Cloud project) or --admin (a standalone router), not both");return new De(Tf(e.project))}let n=e.admin??Ji(t.LEPIS_ADMIN_URL);if(!n)throw new P("usage","cluster needs a project: --project <ref> for a SnoutData Cloud project, or --admin <url> (or LEPIS_ADMIN_URL) for the admin API of a standalone Lepis router (LEPIS_ADMIN_ADDR)");let r=e.token??Ji(t.LEPIS_ADMIN_TOKEN);if(!r)throw new P("not-signed-in","no admin token: set LEPIS_ADMIN_TOKEN to the router's token, or pass --token");return new $o(Pf(n),r)}function Ji(e){return e===void 0||e===""?void 0:e}function Pf(e){let t=/^[a-z][a-z0-9+.-]*:\/\//i.test(e)?e:`http://${e}`,n;try{n=new URL(t)}catch{throw new P("usage",`--admin: not a URL: ${e}`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new P("usage",`--admin: http or https, not ${n.protocol.replace(":","")}`);if(n.username||n.password)throw new P("usage","--admin: the URL carries no credentials; the token comes from LEPIS_ADMIN_TOKEN or --token");if(n.protocol==="http:"&&!Ef(n.hostname))throw new P("usage",`--admin: ${n.host} is not this machine, and plain http would send the token in the clear. Use https (the router serves it when it has LEPIS_TLS_CERT), or a tunnel to 127.0.0.1`);return n.pathname=n.pathname.replace(/\/+$/,""),n.search="",n.hash="",n}function Ef(e){let t=e.replace(/^\[|\]$/g,"").toLowerCase();return t==="localhost"||t==="::1"||/^127\.\d+\.\d+\.\d+$/.test(t)}var Af=6e4,$o=class{target;#t;#n;constructor(t,n){this.#t=t,this.#n=n,this.target=t.toString().replace(/\/$/,"")}status(){return this.#e("GET","/v1/status")}ops(){return this.#e("GET","/v1/ops")}plan(t){return this.#e("POST","/v1/plan",t)}submit(t){return this.#e("POST","/v1/jobs",t)}async jobs(){return(await this.#e("GET","/v1/jobs")).jobs}job(t){return this.#e("GET",`/v1/jobs/${t}`)}cancel(t){return this.#e("POST",`/v1/jobs/${t}/cancel`)}resume(t){return this.#e("POST",`/v1/jobs/${t}/resume`)}settings(t){return this.#e("POST","/v1/settings",t)}advice(t){return this.#e("GET",t===void 0?"/v1/advice":`/v1/advice?sample_ms=${Math.max(0,Math.floor(t))}`)}async#e(t,n,r){let o=`${this.target}${n}`,s;try{s=await fetch(o,{method:t,headers:{authorization:`Bearer ${this.#n}`,accept:"application/json",...t==="POST"?{"content-type":"application/json"}:{}},...t==="POST"?{body:JSON.stringify(r??{})}:{},signal:AbortSignal.timeout(Af)})}catch(i){let u=bn(i),c=i instanceof Error?i.cause instanceof Error?i.cause.message:i.message:String(i);throw new P(u==="failed"?"network":u,`could not reach the Lepis admin API at ${this.target}: ${c}`)}let a=await s.text(),l;try{l=a===""?{}:JSON.parse(a)}catch{throw new P("server",`the admin API at ${this.target} answered ${s.status} with something that is not JSON`,{status:s.status})}if(!s.ok)throw Yi(s.status,l);return l}},Rf={bad_request:"usage",unauthorized:"not-signed-in",not_found:"not-found",conflict:"conflict",failed:"failed",timeout:"timeout"};function Yi(e,t){let n=t?.error??{},r=n.message??`the admin API answered ${e}`,o={status:e};n.code&&(o.apiCode=n.code),n.kind&&(o.kind=n.kind),n.sqlstate&&(o.sqlstate=n.sqlstate);let s=(n.code?Rf[n.code]:void 0)??(e===401?"not-signed-in":e===400?"usage":Ue(e));return s==="not-signed-in"?new P(s,"the router refused the admin token (LEPIS_ADMIN_TOKEN or --token)",o):new P(s,r,o)}function Tf(e){let t=e.trim();if(!/^[a-z][0-9a-z]{12}$/.test(t))throw new P("usage",`--project: not a project ref: ${e} (13 characters, as projects list prints it)`);return t}var Of={"node.add":"add-node","node.attach":"attach","node.drain":"drain","node.remove":"remove","keyspace.create":"keyspace","table.distribute":"distribute","table.reference":"reference","table.global":"global","range.split":"split","range.merge":"merge","range.move":"move","tenant.pin":"pin",rebalance:"rebalance",scale:"scale",verify:"verify",cleanup:"cleanup","keyspace.drop":"drop-keyspace",dissolve:"disable"},Nf="cloud-project-shards",De=class{target;ref;#t;constructor(t,n=_){this.ref=t,this.target=`project ${t}`,this.#t=n}async status(){let t=await this.#e({action:"status"}),{cluster:n,...r}=t;return{...n??{catalog:!1},cloud:r}}ops(){return this.#e({action:"ops"})}async plan(t){return this.#n(t),this.#e({action:"plan",request:t})}async submit(t){let n=this.#n(t),r={...t};delete r.op;let o=await this.#e({action:n,request:r});return typeof o.job=="number"?{job:o.job,plan:o.plan}:{done:Xe(o),answer:o}}async jobs(){return(await this.#e({action:"jobs"})).jobs}job(t){return this.#e({action:"jobs",id:t})}cancel(t){return this.#e({action:"cancel",id:t})}resume(t){return this.#e({action:"resume",id:t})}settings(t){return this.#e({action:"settings",settings:t})}advice(t){return this.#e({action:"advice",...t===void 0?{}:{sampleMs:Math.max(0,Math.floor(t))}})}enable(t){return this.#e({action:"enable",...t?{plan:!0}:{}})}spread(t,n){return this.#e({action:"spread",request:t,...n?{plan:!0}:{}})}retry(t){return this.#e({action:"retry",...t})}disable(t){return this.#e({action:"disable",...t?{plan:!0}:{}})}replica(t,n,r){return this.#e({action:n?"remove-replica":"add-replica",node:t,...r?{plan:!0}:{}})}async alerts(){return(await this.status()).cloud?.alerts??[]}acknowledge(t){return this.#e({action:"acknowledge",alert:t})}#n(t){let n=Of[t.op];if(!n)throw new P("usage",`${t.op} is not an operation a SnoutData Cloud project takes`);let r=t;if(t.op==="node.add"&&(r.host!==void 0||r.name!==void 0))throw new P("usage","in SnoutData Cloud a node is a pod the project makes: nodes add takes no name or --host. It joins the cluster on its own once it is ready.");if(t.op==="scale"&&(r.add!==void 0||r.remove!==void 0))throw new P("usage","in SnoutData Cloud, scale takes --nodes N: how many nodes the project should have, the home included");return n}async#e(t){try{return await this.#t(Nf,{ref:this.ref,...t})}catch(n){throw If(n)}}};function Xe(e){if(Array.isArray(e.created)){let t=e.created.map(String),n=typeof e.stopped=="string"?` Stopped there: ${e.stopped}`:"",r=t.length===1;return e.joining===!1?`New node${r?"":"s"} ${t.join(", ")}, starting as a copy of the home node. Attach ${r?"it":"each"} once it is ready: snoutdata cluster nodes attach <ref>.${n}`:`New node${r?"":"s"} ${t.join(", ")}, starting as a copy of the home node. ${r?"It joins":"They join"} the cluster and take${r?"s":""} ${r?"its":"their"} share of the data on ${r?"its":"their"} own; follow it with snoutdata cluster status.${n}`}return e.deleted===!0?`The pod of ${String(e.pod)} is being deleted. It owned nothing in the cluster.`:Array.isArray(e.queued)?`${String(e.next??"The table is being spread over the cluster.")} Steps: ${e.queued.map(String).join(", ")}. Follow it with snoutdata cluster status.`:typeof e.joining=="boolean"&&typeof e.node=="string"?e.joining?`${e.node} is joining the cluster again.`:`${e.node} is left as it is.`:e.queue==="retrying"?"The cluster is trying that step again.":typeof e.replica=="string"&&typeof e.node=="string"?`A replica of node ${e.node} is being made (${e.replica}). ${String(e.note??"It protects the node once it is following it.")}`:typeof e.changed=="boolean"&&typeof e.node=="string"&&e.sharded===void 0?e.changed?`The replica of node ${e.node} is being removed, and its project slot freed.`:`node ${e.node} has no replica. Nothing changed.`:e.acknowledged===!0?"Marked as seen. The alert stays open until what it is about is over.":e.unchanged===!0?`The project already has ${String(e.nodes)} nodes. Nothing changed.`:e.sharded===!0?e.changed===!1?"This project is already a cluster.":String(e.note??"This project is a cluster now."):e.sharded===!1?e.changed===!1?"This project is not a cluster.":String(e.note??"This project is not a cluster any more."):typeof e.next=="string"?e.next:"Done."}function If(e){if(!(e instanceof ge))return e;let t=e.body??{};if(typeof t.apiCode=="string"||typeof t.kind=="string")return Yi(e.status,{error:{code:typeof t.apiCode=="string"?t.apiCode:void 0,kind:typeof t.kind=="string"?t.kind:void 0,message:e.message,sqlstate:typeof t.sqlstate=="string"?t.sqlstate:void 0}});let n=t.code==="not-sharded"?"conflict":Ue(e.status);return new P(n,e.message,{status:e.status,...typeof t.code=="string"?{kind:t.code}:{}})}function zt(e){if(Array.isArray(e))return`[${e.map(zt).join(",")}]`;if(e&&typeof e=="object"){let t=e;return`{${Object.keys(t).filter(n=>t[n]!==void 0).sort().map(n=>`${JSON.stringify(n)}:${zt(t[n])}`).join(",")}}`}return JSON.stringify(e)??"null"}var Ao=class{#t=new Map;#n;#e;constructor(t=15*6e4,n=Date.now){this.#n=t,this.#e=n}remember(t,n=""){this.#t.set(`${n}
|
|
468
|
-
${
|
|
469
|
-
${
|
|
470
|
-
${zt(t)}`)}},Cf=new Ao,Ro=["node.add","node.drain","node.remove","keyspace.create","table.distribute","table.reference","table.global","range.split","range.merge","range.move","tenant.pin","rebalance","scale","verify","cleanup","node.attach","keyspace.drop","dissolve"],jf="2024-11-05";function K(e,t=[]){return{type:"object",properties:e,additionalProperties:!1,...t.length?{required:t}:{}}}var I={type:"string"},Xi={type:"object",description:'The operation as the router takes it, e.g. {"op": "range.split", "keyspace": "k", "range": "-3074457345618258603", "at": "18454348402311335", "to": 3}. Range bounds and seeds are 64-bit, so pass them as strings. cluster_advice returns these ready-made, as `operation`. For a SnoutData Cloud project a node is a pod the project makes: {"op": "node.add"} makes one (no host), {"op": "node.attach", "name": "<its ref>"} hands a ready one to the cluster, and {"op": "scale", "nodes": N} is a node count.',properties:{op:{type:"string",enum:[...Ro]}},required:["op"],additionalProperties:!0},Zn={type:"integer",description:"The job id cluster_run returned, or one from cluster_jobs."},Ze={type:"string",description:"A SnoutData Cloud project's ref (from list_projects): its cluster, as the signed-in user. Omit for the standalone router this server was started with (LEPIS_ADMIN_URL)."},Vt=[{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:K({})},{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. Under `local`, the self-hosted projects on this machine (SnoutData Studio's, or linked with `snoutdata link --local`), whose refs work with get_connection_url, push_migrations, get_project and the function tools.",inputSchema:K({})},{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:K({name:I,region:I,teamId:I},["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:K({ref:I},["ref"])},{name:"pause_project",description:"Stop a project, and wait until it is paused. Its data is kept and the next connection wakes it. A production project refuses this.",inputSchema:K({ref:I},["ref"])},{name:"resume_project",description:"Start a paused project without waiting for a connection to wake it, and wait until it is ready.",inputSchema:K({ref:I},["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 nothing installed: the CLI connects to the database itself. A file holding a psql command (\\i, \\set), or a COMMIT, END or ROLLBACK that would end the transaction the file runs in, is refused before anything runs; a file starting with -- snoutdata:no-transaction runs as written, outside a transaction.",inputSchema:K({ref:I,dir:I,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:K({ref:I,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:K({ref:I},["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:K({ref:I},["ref"])},{name:"reset_password",description:"Rotate the project's database password. THE OLD ONE STOPS WORKING usually within fifteen seconds (longer while the host is busy starting other databases), including any DATABASE_URL already written into a file or a running process; connections already open keep working. Returns the new connection URL, which works once the change applies (wait about fifteen seconds before connecting, and retry a refusal for up to two minutes). Use it when a credential has leaked, not routinely.",inputSchema:K({ref:I},["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:K({})},{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:K({})},{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. Without `project` it is as capable as this account, so hand it over deliberately; with a project ref it reaches that one project and nothing else, which is what a CI job for one project should get. Optionally expires, as an ISO date.",inputSchema:K({name:I,expires:I,project:I},["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:K({id:I},["id"])},{name:"deploy_function",description:"Put a folder of TypeScript on the edge as an HTTP endpoint at https://<ref>.api.snoutdata.com/v1/fn/<name>. Reads functions/<name> unless dir says otherwise. Deno, so no build step and no node_modules. Set openToAnyone ONLY for a webhook receiver whose sender cannot send an API key: it makes the URL callable by anybody who knows it, and the function must then check the sender's signature itself.",inputSchema:K({ref:I,name:I,dir:I,entrypoint:I,openToAnyone:{type:"boolean"}},["ref","name"])},{name:"list_functions",description:"The functions deployed to a project, their URLs, bundle sizes, whether each one needs an API key, and each one's memory and concurrency with the plan's limits for both. Costs nothing.",inputSchema:K({ref:I},["ref"])},{name:"size_function",description:"Set one function's memory (MB one worker may use) and concurrency (how many workers it may run at once), within the project's plan. memoryMb x concurrency may not exceed the project's memory; list_functions shows each function's size and the plan's limits (limit.functionMemoryMb, limit.concurrencyMax, limit.podMemoryMb). A size that does not fit is refused with a sentence saying why. Omit one to keep it as it runs now; reset: true goes back to the plan's default. A second worker only helps CPU-bound work: requests that wait on the network share one.",inputSchema:K({ref:I,name:I,memoryMb:{type:"integer"},concurrency:{type:"integer"},reset:{type:"boolean"}},["ref","name"])},{name:"delete_function",description:"Remove one function from a project. It stops answering within a few seconds. It does not touch the database.",inputSchema:K({ref:I,name:I},["ref","name"])},{name:"list_containers",description:"The project's Snout Containers: each one's status (pending, pulling, starting, running, restarting, failed, stopped), why when it is not running, its image and pinned digest, restarts, and its public URL. allowed=false means the plan no longer covers it and it is stopped. Also the plan's allowance. Costs nothing.",inputSchema:K({ref:I},["ref"])},{name:"deploy_container",description:"Run a Docker image all the time beside the project, at https://<ref>.api.snoutdata.com/containers/v1/<name>/ (no API key needed there: the container authenticates its own callers). The image tag is pinned to the digest it points at now and must include linux/arm64. It listens on PORT (port, default 8080) and gets DATABASE_URL and the project's secrets in its environment. STATELESS: anything written inside it is lost on every restart or redeploy, so state belongs in the database. Plus and Pro plans only. A private image needs a person's first deploy with the CLI (no tool takes a registry token); a redeploy reuses the stored login. Returns at once: poll list_containers until the status is running or failed.",inputSchema:K({ref:I,name:I,image:I,port:{type:"integer"}},["ref","name","image"])},{name:"restart_container",description:"Start one container again from its image. Everything written inside it is lost, which is also how to clear a container stuck in a bad state. Poll list_containers to see it come back.",inputSchema:K({ref:I,name:I},["ref","name"])},{name:"delete_container",description:"Stop one container and remove it from the project. It does not touch the database.",inputSchema:K({ref:I,name:I},["ref","name"])},{name:"container_logs",description:"What a project's containers printed to stdout and stderr in the last seven days, newest first: each line's time, container, stream and text. Lines arrive about a minute after they are printed. Use it when list_containers says failed or restarting. name narrows to one container, errorsOnly to stderr, limit is at most 500 (default 100). Costs nothing.",inputSchema:K({ref:I,name:I,errorsOnly:{type:"boolean"},limit:{type:"integer"}},["ref"])},{name:"container_egress",description:"Where a project's containers may send. allowlist null means the public internet (within the limits on how much and how fast); otherwise allowlist.allow is the only destinations they reach, and alwaysAllowed what they reach whatever the list says (the project's own database and API). The same as `snoutdata containers egress`. Costs nothing.",inputSchema:K({ref:I},["ref"])},{name:"set_container_egress",description:'Hold a project\'s containers to an allowlist, or let them reach the public internet again. Give EITHER allow, a list of destinations ("api.example.com:443", "api.example.com" for any port, "140.82.112.0/20:443,8443"; an empty list means nothing outside the project), OR clear: true. The list replaces the one there. Running containers follow it within a minute and nothing restarts, so a container that needs a destination left off stops reaching it. The same as `snoutdata containers egress set|clear`.',inputSchema:K({ref:I,allow:{type:"array",items:I},clear:{type:"boolean"}},["ref"])},{name:"list_function_secrets",description:"The NAMES of the environment variables a project's functions run with, their sizes and when each was last set. Never the values, which are not readable by anything but the runtime. THERE IS NO TOOL THAT SETS ONE: a secret in a tool call is a secret in an audit log, so a person sets it with `snoutdata secrets set`. Use this to check whether one a function needs is there.",inputSchema:K({ref:I},["ref"])},{name:"function_logs",description:"What a project's functions printed with console.log and console.error in the last seven days, newest first: each line's time, function, level ('error' for console.error and console.warn) and text. Lines arrive about a minute after they are printed. Use it after deploying or calling a function to see why it failed. name narrows to one function, errorsOnly to errors, limit is at most 500 (default 100). Costs nothing.",inputSchema:K({ref:I,name:I,errorsOnly:{type:"boolean"},limit:{type:"integer"}},["ref"])},{name:"get_project",description:"Everything about one project in one call: its state, region and size, which products are on (auth, storage, data-api), the names of its deployed functions and function secrets, and its custom domains. Never a password, a key or a download link (export_status has the link to the last export).",inputSchema:K({ref:I},["ref"])},{name:"list_products",description:"Whether a project's auth (user sign-up and sign-in), storage (files) and data API (REST and GraphQL over its tables) are on, and whether the plan allows the data API.",inputSchema:K({ref:I},["ref"])},{name:"set_product",description:"Turn auth, storage or data-api on or off for a project. It starts within about a minute. All three are on every plan, including free.",inputSchema:K({ref:I,product:{type:"string",enum:["auth","storage","data-api"]},enabled:{type:"boolean"}},["ref","product","enabled"])},{name:"list_domains",description:"The custom domains in front of a project's API, whether each is verified, and the DNS records each needs.",inputSchema:K({ref:I},["ref"])},{name:"add_domain",description:"Serve a project's API at the user's own hostname (paid plans). Returns the DNS records the user must publish before verify_domain can succeed.",inputSchema:K({ref:I,hostname:I},["ref","hostname"])},{name:"verify_domain",description:"Check a custom domain's DNS records and verify it once they are published.",inputSchema:K({ref:I,hostname:I},["ref","hostname"])},{name:"remove_domain",description:"Stop serving a project at a custom domain.",inputSchema:K({ref:I,hostname:I},["ref","hostname"])},{name:"restore_window",description:"How far back a point-in-time restore of a project can go, or why it cannot (the plan, or no backup yet). Starts nothing.",inputSchema:K({ref:I},["ref"])},{name:"restore_to_point",description:"Rewind a project to a moment (ISO 8601, inside restore_window) into a NEW project beside it, never over it. Costs a project slot on the plan. The original is untouched.",inputSchema:K({ref:I,at:I,name:I},["ref","at"])},{name:"cluster_status",description:"A Lepis cluster (one Postgres database spread over several nodes): a Cloud project's with project, else the router this server was started against with LEPIS_ADMIN_URL. Its nodes, keyspaces and their ranges (a range is keyspace:lo; bounds are strings), tables, routers, unfinished jobs and settings; for a Cloud project also whether it is a cluster, its plan's node limit, and each node pod with the step that follows (attach, delete-pod, wait). Costs nothing.",inputSchema:K({project:Ze})},{name:"cluster_advice",description:"Ask the cluster's advisor what to split, move or add, from each node's size, connections and write rate and each range's measured share. Each recommendation has a reason, its plan, and an `operation` that cluster_run takes as it is. RUNS NOTHING. sampleMs: how long to measure write rates over (the cluster's setting by default; 0 skips them; at most 30000).",inputSchema:K({project:Ze,sampleMs:{type:"integer"}})},{name:"cluster_plan",description:"The dry run of one cluster operation: its steps, what moves where, rows, bytes, the estimated copy time and the expected write pause. Changes nothing. Planning an operation is what lets cluster_run run it.",inputSchema:K({project:Ze,operation:Xi},["operation"])},{name:"cluster_run",description:"Run one cluster operation as a durable job. IT MOVES DATA, and each cutover pauses writes to what moves for up to the cluster's max_write_pause_ms. Without confirm: true it only returns the plan and runs nothing: show that plan to the user, and when they agree call again with the same operation and confirm: true. confirm runs only an operation planned here (cluster_plan, cluster_advice, or cluster_run without confirm) in the last 15 minutes. Returns the job id; follow it with cluster_watch_job.",inputSchema:K({project:Ze,operation:Xi,confirm:{type:"boolean"}},["operation"])},{name:"cluster_jobs",description:"The cluster's latest jobs, or with id one job with each step's state and phase. Costs nothing.",inputSchema:K({project:Ze,id:Zn})},{name:"cluster_watch_job",description:"Wait for a job to finish, up to timeoutSeconds (default 30, at most 120), and return its state and steps: done, failed (with the error; cluster_resume_job runs it again from the failed step) or cancelled. Running out of time stops the waiting, never the job; call again to keep waiting.",inputSchema:K({project:Ze,id:Zn,timeoutSeconds:{type:"integer"}},["id"])},{name:"cluster_cancel_job",description:"Stop a running job. A move cancelled before its cutover rolls back; one cancelled after it finishes its cleanup.",inputSchema:K({project:Ze,id:Zn},["id"])},{name:"cluster_resume_job",description:"Run a failed job again from the step that failed. It carries on the operation that was already approved and planned; follow it with cluster_watch_job.",inputSchema:K({project:Ze,id:Zn},["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:K({ref:I},["ref"])}],Df=new Set(["whoami","list_projects","get_connection_url","usage","export_status","list_teams","list_tokens","list_functions","list_function_secrets","function_logs","container_egress","get_project","list_products","list_domains","restore_window","cluster_status","cluster_advice","cluster_plan","cluster_jobs","cluster_watch_job"]),Mf=new Set(["delete_project","delete_function","reset_password","revoke_token","remove_domain","cluster_run"]),Zi=["get_connection_url","push_migrations","get_project","list_functions","deploy_function","delete_function","list_function_secrets"];function To(e){if(!e||typeof e!="object")return e;let t=e;if(Array.isArray(t.projects))return{...t,projects:t.projects.map(To)};let n=t.export;if(n&&typeof n=="object"){let{url:r,rolesSql:o,...s}=n;return{...t,export:{...s,hasLink:typeof r=="string"&&r.length>0}}}return e}function Oo(e,t){return t.available?null:t.pitrEnabled?`${e} has no backup to restore from yet.`:`Point-in-time restore is part of the Pro and Business plans, and this project is on ${t.tier??"another plan"}.`}function Lf(e){return{...e,annotations:Df.has(e.name)?{readOnlyHint:!0}:{readOnlyHint:!1,destructiveHint:Mf.has(e.name)}}}function Uf(e){let t=new Set;return e.filter(n=>!t.has(n.name)&&t.add(n.name))}function Jt(e,t){return{jsonrpc:"2.0",id:e,result:t}}function Qn(e,t,n){return{jsonrpc:"2.0",id:e,error:{code:t,message:n}}}function $(e,t,n=!1){let r=typeof t=="string"?t:JSON.stringify(t,null,2);return Jt(e,{content:[{type:"text",text:r}],...n?{isError:!0}:{}})}function Bf(e,t=new Date){let n=Date.parse(e);return!Number.isFinite(n)||n<=t.getTime()?null:Math.ceil((n-t.getTime())/864e5)}function Wt(e){return{op:e.op,cutovers:e.cutovers,moves:(e.moves??[]).map(t=>({from:t.source,to:t.targets,tables:t.tables,estimated_rows:t.estimated_rows,estimated_bytes:t.estimated_bytes,estimated_copy_seconds:t.estimated_copy_seconds,expected_pause_ms:t.expected_pause_ms})),steps:(e.steps??[]).map(t=>t.kind),estimated_rows:e.estimated_rows,estimated_bytes:e.estimated_bytes,estimated_copy_seconds:e.estimated_copy_seconds,expected_pause_ms:e.expected_pause_ms,max_write_pause_ms:e.max_write_pause_ms,warnings:e.warnings??[],...e.assumptions?{assumptions:e.assumptions}:{}}}function Po(e){return{id:e.id,op:e.op,state:e.state,finished:qt(e.state),error:e.error,steps:e.steps.map(t=>({n:t.n+1,kind:t.kind,state:t.state,...typeof t.detail?.phase=="string"?{phase:t.detail.phase}:{}})),...e.state==="failed"?{next:`cluster_resume_job {"id": ${e.id}} runs it again from the failed step.`}:{}}}function Qi(e,t){let n=e.operation;if(!n||typeof n!="object"||Array.isArray(n))return`${t} needs operation: an object like {"op": "range.split", "keyspace": "k", "range": "<lo>"}. cluster_advice returns them ready-made.`;let r=n;if(typeof r.op!="string"||!Ro.includes(r.op))return`${t}: operation.op must be one of ${Ro.join(", ")}.`;for(let o of["range","at","a","b","seed"]){let s=r[o];if(typeof s=="number"&&!Number.isSafeInteger(s))return`${t}: operation.${o} is a 64-bit number and lost its last digits as a JSON number. Pass it as a string, exactly as cluster_status or cluster_advice wrote it.`}return r}function Eo(e){let t=typeof e.id=="string"&&/^\d+$/.test(e.id)?Number(e.id):e.id;return typeof t=="number"&&Number.isSafeInteger(t)&&t>0?t:null}function Ff(e){let t=e instanceof Error?e.message:String(e),n=e?.details?.kind;return typeof n=="string"?{error:t,kind:n}:t}var Hf="The cluster tools need a cluster: pass project (a SnoutData Cloud project's ref), or start this server with LEPIS_ADMIN_URL (a standalone router's admin API, e.g. http://127.0.0.1:7432) and LEPIS_ADMIN_TOKEN set. Tell the user that is what is missing.";async function Kf(e,t,n,r,o){let s,a=typeof n.project=="string"&&n.project.trim()!==""?n.project.trim():void 0;try{if(!r.shards)return $(e,Hf,!0);s=r.shards(a)}catch(u){return $(e,u instanceof Error?u.message:String(u),!0)}let l=o.shardsPlans??Cf,i=a??"";try{switch(t){case"cluster_status":return $(e,await s.status());case"cluster_advice":{let u=typeof n.sampleMs=="number"&&n.sampleMs>=0?Math.min(Math.floor(n.sampleMs),3e4):void 0,c=await s.advice(u);for(let p of c.advice)p.plan&&l.remember(p.request,i);return $(e,{summary:c.summary,advice:c.advice.map(p=>({op:p.op,reason:p.reason,metric:p.metric,operation:p.request,...p.plan?{plan:Wt(p.plan)}:{},...p.needs.length>0?{needs:p.needs,note:`Ask the user for ${p.needs.join(", ")}, then plan it with cluster_plan.`}:{},...p.plan_error?{plan_error:p.plan_error}:{}})),facts:c.facts,assumptions:c.assumptions,...c.advice.length>0?{next:"Nothing has run. Show the user a recommendation and its plan; if they agree, call cluster_run with its operation and confirm: true. Ask for advice again after each one runs."}:{}})}case"cluster_plan":{let u=Qi(n,t);if(typeof u=="string")return $(e,u,!0);let c=await s.plan(u);return l.remember(u,i),$(e,{plan:Wt(c),next:"Nothing has run. To run it, show the user this plan, then call cluster_run with the same operation and confirm: true."})}case"cluster_run":{let u=Qi(n,t);if(typeof u=="string")return $(e,u,!0);if(n.confirm!==!0||!l.shown(u,i)){let p=await s.plan(u);l.remember(u,i);let f=n.confirm===!0;return $(e,{ran:!1,plan:Wt(p),next:f?"NOT RUN: confirm applies to an operation planned here in the last 15 minutes, and this one was not, so it has been planned now instead. Show the user this plan; if they agree, call cluster_run again with the same operation and confirm: true.":"Nothing has run. Show the user this plan; if they agree, call cluster_run again with the same operation and confirm: true."},f)}let c=await s.submit(u);return l.forget(u,i),c.job===void 0?$(e,{ran:!0,done:c.done,answer:c.answer}):$(e,{ran:!0,job:c.job,plan:Wt(c.plan),next:`Follow it with cluster_watch_job {"id": ${c.job}}.`})}case"cluster_jobs":{if(n.id===void 0)return $(e,{jobs:await s.jobs()});let u=Eo(n);if(u===null)return $(e,"cluster_jobs: id is a job number. Call it without id to list them.",!0);let c=await s.job(u);return $(e,{...Po(c),plan:c.plan&&Array.isArray(c.plan.steps)?Wt(c.plan):null})}case"cluster_watch_job":{let u=Eo(n);if(u===null)return $(e,"cluster_watch_job needs id, the job number cluster_run returned.",!0);let c=typeof n.timeoutSeconds=="number"&&n.timeoutSeconds>0?Math.min(n.timeoutSeconds,120):30,p=Date.now()+c*1e3;for(;;){let f=await s.job(u);if(qt(f.state))return $(e,Po(f),f.state!=="done");if(Date.now()>=p)return $(e,{...Po(f),note:`Still ${f.state} after ${c}s. It carries on; call cluster_watch_job again to keep waiting.`});await new Promise(k=>setTimeout(k,o.shardsPollMs??1e3))}}case"cluster_cancel_job":case"cluster_resume_job":{let u=Eo(n);if(u===null)return $(e,`${t} needs id, a job number.`,!0);let c=t==="cluster_cancel_job"?await s.cancel(u):await s.resume(u);return $(e,{...c,next:`Follow it with cluster_watch_job {"id": ${u}}.`})}default:return Qn(e,-32602,`unknown tool: ${t}`)}}catch(u){return $(e,Ff(u),!0)}}function J(e,t){let n=e[t];return typeof n=="string"&&n.trim().length>0?n.trim():null}async function ec(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 Jt(r,{protocolVersion:jf,capabilities:{tools:{listChanged:!1}},serverInfo:{name:"snoutdata",version:n.version}});case"ping":return Jt(r,{});case"tools/list":return Jt(r,{tools:Uf([...Vt.map(Lf),...n.borrowed??[]])});case"tools/call":return qf(r,s,t,n);default:return Qn(r,-32601,`unknown method: ${o}`)}}async function qf(e,t,n,r){let o=typeof t.name=="string"?t.name:"",s=t.arguments??{};if(!(Vt.some(c=>c.name===o)||(r.borrowed?.some(c=>c.name===o)??!1)))return Qn(e,-32602,`unknown tool: ${o}`);let l=["get_connection_url","pause_project","resume_project","delete_project","push_migrations","usage","export_status","start_export","reset_password","get_project","list_products","set_product","list_domains","add_domain","verify_domain","remove_domain","restore_window","restore_to_point"];if(o.startsWith("cluster_")&&Vt.some(c=>c.name===o))return Kf(e,o,s,n,r);let i=J(s,"ref");if(l.includes(o)&&!i)return $(e,`${o} needs a project ref. Call list_projects to see them.`,!0);let u=i&&n.localProject?n.localProject(i):null;if(u&&!Zi.includes(o))return $(e,`${i} is the self-hosted project in ${u.folder}, and ${o} is about SnoutData Cloud. On a local project these tools work: ${Zi.join(", ")}.`,!0);try{switch(o){case"whoami":return $(e,await n.whoami());case"list_projects":return $(e,To(await n.listProjects()));case"create_project":{let c=J(s,"name");return c?$(e,await n.createProject({name:c,region:J(s,"region")??void 0,teamId:J(s,"teamId")??void 0})):$(e,"create_project needs a name.",!0)}case"get_connection_url":{let c=await n.connection(i);return $(e,{url:c.uri,state:c.state,wakesInstantly:c.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 $(e,await n.setProjectState("pause",i));case"resume_project":return $(e,await n.setProjectState("resume",i));case"usage":{let c=typeof s.days=="number"?s.days:void 0;return $(e,await n.usage(i,c))}case"export_status":return $(e,await n.exportStatus(i));case"start_export":return $(e,await n.requestExport(i));case"reset_password":{let c=await n.resetPassword(i),p=await n.connection(i);return $(e,{url:p.uri,appliesIn:c.appliesIn??"within about fifteen seconds",next:"Wait about fifteen seconds before connecting with this URL, and retry a refusal for up to two minutes: until the change applies (usually 5 to 15 seconds, longer while the host is busy) the new password is refused and the old one still works, which does not mean the new one is wrong. Then update DATABASE_URL wherever it is set.",warning:"This URL contains a live database password. Put it in an environment variable, not in a file that is committed."})}case"list_teams":return $(e,await n.listTeams());case"get_project":return $(e,To(await n.getProject(i)));case"list_products":return $(e,await n.getProducts(i));case"set_product":{let c=J(s,"product");return c!=="auth"&&c!=="storage"&&c!=="data-api"?$(e,"set_product needs product: auth, storage or data-api.",!0):typeof s.enabled!="boolean"?$(e,"set_product needs enabled: true or false.",!0):$(e,await n.setProduct(i,c,s.enabled))}case"list_domains":return $(e,await n.listDomains(i));case"add_domain":case"verify_domain":case"remove_domain":{let c=J(s,"hostname");return c?$(e,await n.domainAction(i,o.split("_")[0],c)):$(e,`${o} needs a hostname, e.g. api.example.com.`,!0)}case"restore_window":{let c=await n.restoreWindow(i),p=c&&c.restore?Oo(i,c.restore):null;return $(e,p?{...c,reason:p}:c)}case"restore_to_point":{let c=J(s,"at");return c?$(e,await n.restoreTo(i,c,J(s,"name")??void 0)):$(e,"restore_to_point needs at: an ISO 8601 moment inside restore_window.",!0)}case"deploy_function":{let c=J(s,"ref"),p=J(s,"name");return!c||!p?$(e,"deploy_function needs a project ref and a function name.",!0):$(e,await n.deployFunction(c,p,{dir:J(s,"dir")??void 0,entrypoint:J(s,"entrypoint")??void 0,verifyJwt:s&&typeof s=="object"&&"openToAnyone"in s?s.openToAnyone!==!0:!0}))}case"list_functions":{let c=J(s,"ref");return c?$(e,await n.listFunctions(c)):$(e,"list_functions needs a project ref.",!0)}case"size_function":{let c=J(s,"ref"),p=J(s,"name");if(!c||!p)return $(e,"size_function needs a project ref and a function name.",!0);let f=typeof s.memoryMb=="number"?s.memoryMb:void 0,k=typeof s.concurrency=="number"?s.concurrency:void 0,v=s.reset===!0;return!v&&f===void 0&&k===void 0?$(e,"size_function needs memoryMb, concurrency, or reset: true.",!0):$(e,await n.sizeFunction(c,p,{memoryMb:f,concurrency:k,reset:v}))}case"delete_function":{let c=J(s,"ref"),p=J(s,"name");return!c||!p?$(e,"delete_function needs a project ref and a function name.",!0):$(e,await n.deleteFunction(c,p))}case"list_containers":{let c=J(s,"ref");return c?$(e,await n.listContainers(c)):$(e,"list_containers needs a project ref.",!0)}case"deploy_container":{let c=J(s,"ref"),p=J(s,"name"),f=J(s,"image");if(!c||!p||!f)return $(e,"deploy_container needs a project ref, a container name and an image.",!0);let k=typeof s.port=="number"?s.port:void 0;return $(e,await n.deployContainer(c,p,{image:f,...k===void 0?{}:{port:k}}))}case"restart_container":case"delete_container":{let c=J(s,"ref"),p=J(s,"name");return!c||!p?$(e,`${o} needs a project ref and a container name.`,!0):$(e,o==="restart_container"?await n.restartContainer(c,p):await n.deleteContainer(c,p))}case"container_logs":{let c=J(s,"ref");if(!c)return $(e,"container_logs needs a project ref.",!0);let p=typeof s.limit=="number"?s.limit:void 0;return $(e,await n.containerLogs(c,{name:J(s,"name")??void 0,errorsOnly:s.errorsOnly===!0,limit:p}))}case"container_egress":{let c=J(s,"ref");return c?$(e,await n.containerEgress(c)):$(e,"container_egress needs a project ref.",!0)}case"set_container_egress":{let c=J(s,"ref");if(!c)return $(e,"set_container_egress needs a project ref.",!0);let p=s.clear===!0,f=s.allow;return f!==void 0&&(!Array.isArray(f)||!f.every(k=>typeof k=="string"))?$(e,'set_container_egress takes allow as a list of destinations, such as ["api.example.com:443"].',!0):p===(f!==void 0)?$(e,"set_container_egress takes either allow (a list of destinations; [] for nothing outside the project) or clear: true (the public internet again), and exactly one of them.",!0):$(e,await n.setContainerEgress(c,p?null:f))}case"list_function_secrets":{let c=J(s,"ref");return c?$(e,await n.listFunctionSecrets(c)):$(e,"list_function_secrets needs a project ref.",!0)}case"function_logs":{let c=J(s,"ref");if(!c)return $(e,"function_logs needs a project ref.",!0);let p=typeof s.limit=="number"?s.limit:void 0;return $(e,await n.functionLogs(c,{name:J(s,"name")??void 0,errorsOnly:s.errorsOnly===!0,limit:p}))}case"list_tokens":return $(e,await n.listTokens());case"create_token":{let c=J(s,"name");if(!c)return $(e,"create_token needs a name, so somebody can tell later what it was for.",!0);let p=J(s,"expires"),f=p?Bf(p):void 0;return f===null?$(e,`create_token: \`expires\` must be a date in the future, like 2026-12-31. Got "${p}".`,!0):$(e,await n.createToken(c,f,J(s,"project")??void 0))}case"revoke_token":{let c=J(s,"id");return c?$(e,await n.revokeToken(c)):$(e,"revoke_token needs the id of a token. list_tokens has them.",!0)}case"push_migrations":return $(e,await n.pushMigrations(i,{dir:J(s,"dir")??void 0,dryRun:s.dryRun===!0}));case"delete_project":return r.allowDelete?$(e,await n.setProjectState("delete",i)):$(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(c=>c.name===o)?n.callBorrowed?Jt(e,await n.callBorrowed(o,s)):$(e,`${o} needs SnoutData Studio, which is no longer answering.`,!0):Qn(e,-32602,`unknown tool: ${o}`)}}catch(c){return $(e,c instanceof Error?c.message:String(c),!0)}}function tc(e){let t=e.split(`
|
|
471
|
-
`),n=t.pop()??"";return{messages:t.map(r=>r.trim()).filter(r=>r.length>0),rest:n}}var
|
|
472
|
-
`);let s=
|
|
473
|
-
`)}async function
|
|
474
|
-
`)});return}if(t!=="add"&&t!=="verify"&&t!=="remove")throw new
|
|
475
|
-
`)})}
|
|
475
|
+
`:""))}async function ml(e,t){for(let r of t){let o=gn(r.name);if(o)throw new g(o)}let n=ts(e);for(let r of t)n.set(r.name,r.value);pl(e,n),await gr(e),w({ref:e.ref,set:t.map(r=>r.name)},()=>{for(let r of t)d(`${O(r.name)} set in ${yr(e)}`)})}function wr(e){return{ref:e.ref,local:e.folder,secrets:[...ts(e).keys()].sort().map(t=>({name:t}))}}function fl(e){let t=wr(e),n=t.secrets.map(r=>r.name);w(t,()=>{d(n.length?n.join(`
|
|
476
|
+
`):`No function secrets in ${yr(e)}.`)})}async function gl(e,t){let n=ts(e);if(!n.delete(t))throw new T("not-found",`${t} is not set for this local project`);pl(e,n),await gr(e),w({ref:e.ref,unset:t},()=>d(`${t} unset.`))}Se();Pt();lt();Ko();Xn();ue();dr();me();me();Se();lt();ue();async function hl(){let{projects:e,allowance:t}=await je();w({projects:e,allowance:t},()=>{if(e.length===0){d('No projects yet. `snoutdata projects create --name "my project"`.');return}process.stdout.write(`${W(Rg(e))}
|
|
477
|
+
`)})}function Rg(e){let t=e.some(n=>typeof n.postgresVersion=="number");return[["REF","NAME","STATE","REGION",...t?["POSTGRES"]:[],"LAST CONNECTION"],...e.map(n=>[n.ref,n.name,n.readOnly?O("read-only"):n.state==="ready"?n.state:O(n.state),n.region,...t?[typeof n.postgresVersion=="number"?String(n.postgresVersion):"-"]:[],ot(n.lastConnectionAt)])]}async function ns(e){let t=await x("cloud-project-create",{name:e.name,region:e.region,teamId:e.teamId}),n=t.project.ref;d(`Created ${n}.`);let r=t.project;if(e.wait){d("Waiting for it to start\u2026");let s="";r=await fn(n,{timeoutMs:e.timeoutMs,onTick:a=>{a!==s&&(s=a,d(b(` ${a}`)))}})}e.link&&Mt(e.link,{ref:n,name:r.name});let o=`postgres://${r.user}:${encodeURIComponent(t.password)}@${r.host}:5432/${r.database}?sslmode=require`;w({project:r,ref:n,host:r.host,database:r.database,user:r.user,...e.showUrl?{password:t.password,uri:o}:{connectionString:"snoutdata db url"}},()=>{process.stdout.write(`${e.showUrl?o:n}
|
|
478
|
+
`),d(""),d(`Ready. ${O(n)} in ${r.region}, at ${r.host}.`),d(e.showUrl?"The password is stored for you: `snoutdata db url` prints this again.":"`snoutdata db url` prints the connection string (it holds the password, so it is not shown here).")})}async function yl(e,t,n={}){let r=`cloud-project-${e}`,o=await x(r,{ref:t}),s=e==="delete"?"deleted":`${e}d`;if(n.wait===!1){w({...o,settled:!1},()=>d(`${t}: asked to be ${s}. \`snoutdata projects list\` shows when it is.`));return}let a=o.project?.state??"",c=await mn(t,e,{timeoutMs:n.timeoutMs,onTick:u=>{u!==a&&(a=u,d(b(` ${u}`)))}}),i=c?.state??"deleted";if(i==="error")throw new T("failed",`${t} went to error instead of ${s}${c?.stateDetail?`: ${c.stateDetail}`:""}.`,{ref:t,state:i});w({...o,...c?{project:c}:{},settled:!0},()=>{d(o.changed?`${t}: ${s}.`:`${t} was already ${s}.`)})}async function wl(e){let t=await x("cloud-project-reset-password",{ref:e});w(t,()=>{process.stdout.write(`${t.password}
|
|
479
|
+
`),d(`New password for ${t.user}. It applies ${t.appliesIn}.`)})}async function bl(){let{teams:e}=await pr();w({teams:e},()=>{if(e.length===0){d("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"])])}
|
|
480
|
+
`)})}async function Sl(e){let{teams:t}=await pr(),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}me();ue();async function _l(e){let t=await x("cloud-token-create",{name:e.name,expiresInDays:e.expiresInDays,...e.project?{project:e.project}:{}});w(t,()=>{process.stdout.write(`${t.token}
|
|
481
|
+
`),d(""),d(`${O(t.name)} \u2014 ${t.expiresAt?`expires ${t.expiresAt.slice(0,10)}`:"does not expire"}.`),d(t.project?`Reaches project ${t.project} only.`:"Reaches every project on this account."),d("This is the only time it is shown: only its hash is stored."),d(b(" export SNOUTDATA_ACCESS_TOKEN=<that token>"))})}async function kl(){let{tokens:e}=await x("cloud-token-list");w({tokens:e},()=>{if(e.length===0){d("No access tokens. `snoutdata tokens create --name ci`.");return}process.stdout.write(`${W([["PREFIX","NAME","PROJECT","STATE","LAST USED","EXPIRES"],...e.map(t=>[t.current?O(t.prefix):t.prefix,t.name,t.project??"all",t.revokedAt?"revoked":t.live?"live":"expired",ot(t.lastUsedAt),t.expiresAt?t.expiresAt.slice(0,10):"never"])])}
|
|
482
|
+
`)})}async function vl(e){let t=e.startsWith("sdt_")?{prefix:e}:{id:e},n=await x("cloud-token-revoke",t);w(n,()=>{d(n.changed?`Revoked ${n.token.prefix} (${n.token.name}).`:`${n.token.prefix} was already revoked.`)})}me();Ae();ue();async function $l(e){let t=await Ce(e);w({ref:t.ref,appKey:t.appKey,serverKey:t.serverKey,issuedAt:t.keysIssuedAt,expiresAt:t.keysExpireAt},()=>{if(!t.appKey||!t.serverKey){d(`${e} has no API keys yet. Ask again in a moment.`);return}process.stdout.write(`${xl(t.appKey,t.serverKey)}
|
|
483
|
+
`),Ng(t.keysIssuedAt)})}async function Pl(e,t){if(!t.force)throw d(`Rotating the keys for ${e} stops every app key and server key already pasted anywhere.`),d("Anything holding one (a deployed app, a CI secret, a teammate) breaks until it is updated."),new g("keys rotate needs --force, because this cannot be undone");let n=ur(await x("cloud-project-rotate-jwt",{ref:e}));w(n,()=>{process.stdout.write(`${xl(n.appKey,n.serverKey)}
|
|
484
|
+
`),d(""),d(n.breaks),d(b(" snoutdata keys --json to read them again later"))})}function xl(e,t){return W([["KEY","VALUE"],["app key",e],["server key",t]])}function Ng(e){d(""),d(`The ${O("app key")} is the public one: it goes in a browser, and row-level security decides what it reads.`),d(`The ${O("server key")} bypasses row-level security. Server side only, never in a client bundle.`),e&&d(b(` issued ${e.slice(0,10)}`))}me();Ae();ue();import{readdir as Cg,readFile as Dg,stat as Mg}from"node:fs/promises";import{join as os,relative as jg,sep as Lg}from"node:path";var El=["functions"];async function rs(e){try{return(await Mg(e)).isDirectory()}catch{return!1}}async function Ug(e,t){if(t){if(!await rs(t))throw new g(`${t} is not a directory`);return t}for(let n of El){let r=os(n,e);if(await rs(r))return r}throw new g(`no folder found for ${e} at ${El[0]}/${e}; use --dir to say where it is.`)}async function Al(e){let t=[];async function n(r){for(let o of await Cg(r,{withFileTypes:!0})){if(o.name.startsWith(".")||o.name==="node_modules")continue;let s=os(r,o.name);if(o.isDirectory()){await n(s);continue}if(!o.isFile())continue;let a=await Dg(s),c=a.toString("utf8");if(Buffer.compare(Buffer.from(c,"utf8"),a)!==0)throw new g(`${s} is not text, and a function bundle is source code. Put binary assets in Storage.`);t.push({path:jg(e,s).split(Lg).join("/"),text:c})}}return await n(e),t}async function Fg(e,t,n){let r=await Al(t);if(r.length===0)throw new g(`${t} has no files in it`);let o=n??(r.some(c=>c.path==="index.ts")?"index.ts":"");if(!o)throw new g(`${t} has no index.ts, so there is nothing to start. Name the file with --entrypoint.`);let s=os(t,"..","_shared");if(!await rs(s))return{entrypoint:o,files:r};let a=(await Al(s)).map(c=>({path:`_shared/${c.path}`,text:c.text}));return{entrypoint:`${e}/${o}`,files:[...r.map(c=>({path:`${e}/${c.path}`,text:c.text})),...a]}}async function ss(e,t,n){Et(t);let r=await Ug(t,n.dir),{entrypoint:o,files:s}=await Fg(t,r,n.entrypoint),a=nl({entrypoint:o,files:s});return{answer:await x("cloud-project-functions",{ref:e,action:"deploy",name:t,verifyJwt:n.verifyJwt,bundle:a}),directory:r,files:s.length,bytes:JSON.stringify(a).length}}async function Tl(e,t,n){let{answer:r,directory:o,files:s,bytes:a}=await ss(e,t,n),c=r.functions.find(i=>i.name===t);w(r,()=>{let i=Math.max(1,Math.round(a/1024));d(`${O(t)} deployed from ${o} (${s} file${s===1?"":"s"}, ${i} KB)`),c&&d(` ${c.url}`),n.verifyJwt||(d(""),d(O(" Anybody who knows this URL can run it. No API key is required.")),d(b(" That is what a webhook receiver needs. Check the sender signature inside the function."))),r.limit.maxFunctions!==null&&d(b(` ${r.limit.deployed} of ${r.limit.maxFunctions} functions on this plan`))})}function as(e){return x("cloud-project-functions",{ref:e})}function is(e,t){return Et(t),x("cloud-project-functions",{ref:e,action:"delete",name:t})}function ls(e,t,n){if(Et(t),!n.reset&&n.memoryMb===void 0&&n.concurrency===void 0)throw new g("functions size needs --memory MB, --concurrency N, or --reset");return x("cloud-project-functions",{ref:e,action:"size",name:t,...n.reset?{reset:!0}:{memoryMb:n.memoryMb??null,concurrency:n.concurrency??null}})}function Bg(e,t){if(e.memoryMb==null||e.concurrency==null)return null;let n=e.memoryMb*e.concurrency,r=t.podMemoryMb?` of ${t.podMemoryMb} MB`:"";return`${e.memoryMb} MB x ${e.concurrency} worker${e.concurrency===1?"":"s"} = ${n} MB${r}`}function Ol(e){return e.functionMemoryMb==null||e.concurrencyMax==null||e.podMemoryMb==null?null:`${e.tier??"this"} plan: up to ${e.functionMemoryMb} MB a worker and ${e.concurrencyMax} workers a function, memory x workers up to ${e.podMemoryMb} MB`}async function Nl(e,t,n){let r=await ls(e,t,n),o=r.functions.find(s=>s.name===t);w(r,()=>{let s=o?Bg(o,r.limit):null;d(`${O(t)} ${n.reset?"is back to the plan default":"is resized"}${s?`: ${s}`:""}`),d(b(" The next request runs at it; workers already running finish what they hold."));let a=Ol(r.limit);a&&d(b(` ${a}`))})}async function Il(e){let t=await as(e);w(t,()=>{if(t.functions.length===0){d(`${e} has no functions.`),d(b(" snoutdata functions deploy <name> from functions/<name>"));return}process.stdout.write(`${W([["NAME","KEY","SIZE","MEMORY","WORKERS","URL"],...t.functions.map(r=>[r.name,r.verifyJwt?"required":O("open"),r.bytes===null?"-":`${Math.max(1,Math.round(r.bytes/1024))} KB`,r.memoryMb==null?"-":`${r.memoryMb} MB`,r.concurrency==null?"-":String(r.concurrency),r.url])])}
|
|
485
|
+
`),t.limit.maxFunctions!==null&&d(b(`${t.limit.deployed} of ${t.limit.maxFunctions} on this plan`));let n=Ol(t.limit);n&&d(b(`${n}. Change one with: snoutdata functions size <name> --memory MB --concurrency N`))})}function Rl(e){let t=e.dropped_before>0?b(`(${e.dropped_before} earlier line(s) were printed too fast to keep) `):"",n=e.cut_bytes?b(` (cut from ${e.cut_bytes} bytes)`):"",r=e.level==="error"?O("error "):"";return`${b(e.at)} ${e.name} ${r}${t}${e.message}${n}`}async function Cl(e,t,n){let r=a=>x("cloud-project-functions",{ref:e,action:"logs",...t?{name:t}:{},...n.errorsOnly?{errorsOnly:!0}:{},...a?{since:a}:{},limit:n.limit??100}),o=((await r()).lines??[]).slice().reverse();if(w({ref:e,lines:o},()=>{if(o.length===0&&!n.follow){d(`Nothing printed${t?` by ${t}`:""} on ${e} in the last seven days.`),d(b(" Lines show up about a minute after a function prints them."));return}for(let a of o)process.stdout.write(`${Rl(a)}
|
|
486
|
+
`)}),!n.follow)return;let s=o.at(-1)?.at??new Date().toISOString();for(;;){await new Promise(c=>setTimeout(c,5e3));let a=((await r(s)).lines??[]).slice().reverse();for(let c of a)process.stdout.write(`${Rl(c)}
|
|
487
|
+
`),s=c.at}}async function Dl(e,t){let n=await is(e,t);w(n,()=>{d(n.live?`${t} is gone.`:`${t} is gone. It stops answering within a few seconds.`)})}me();import{spawn as uh}from"node:child_process";import{createHash as ph}from"node:crypto";import{createReadStream as ql}from"node:fs";import{mkdtemp as mh,rm as fh,stat as br}from"node:fs/promises";import{request as gh}from"node:http";import{request as hh}from"node:https";import{tmpdir as yh}from"node:os";import{basename as wh,dirname as bh,join as ms}from"node:path";var Kg="x-snoutdata-upload-bytes",Gg="x-snoutdata-upload-sha256",qg="/done",Wg=/^[0-9a-f]{64}$/,zg=/^sha256:[0-9a-f]{64}$/;function Jg(e){return Wg.test(e)}var De=class extends Error{constructor(n,r){super(`the upload failed (${n})`);this.status=n;this.body=r}};function cs(e){try{let t=JSON.parse(e);return t&&typeof t=="object"?t:{}}catch{return{}}}function Ml(e){let t=String(cs(e).digest??"");if(!zg.test(t))throw new De(502,JSON.stringify({error:"The upload finished without saying what it stored, so there is nothing to deploy. Build again."}));return t}var Vg=15*6e4,Yg=2e3;async function jl(e){let{ticketUrl:t,file:n,transport:r}=e;if(!Jg(e.sha256))throw new Error("an upload is declared with its SHA-256, in hex");let o=await r.call("POST",t,{[Kg]:String(e.bytes),[Gg]:e.sha256});if(o.status===405){e.step?.("uploading");let l=await r.put(t,n,{});if(l.status!==200)throw new De(l.status,l.body);return Ml(l.body)}if(o.status!==200)throw new De(o.status,o.body);let s=cs(o.body);if(typeof s.url!="string"||!s.url.startsWith("https://")||!s.headers||typeof s.headers!="object")throw new De(502,JSON.stringify({error:"The project's host did not say where to put the image. Build again in a moment."}));e.step?.("uploading");let a=await r.put(s.url,n,s.headers);if(a.status!==200){let l=/<Code>([^<]+)<\/Code>/.exec(a.body)?.[1],m=l==="BadDigest"||l==="XAmzContentSHA256Mismatch"?"The image changed or was damaged while it was uploading. Build again.":l==="AccessDenied"||l==="SignatureDoesNotMatch"||l==="RequestTimeTooSkewed"?"The upload link was refused (it lasts 15 minutes, and this computer's clock must be right). Build again.":`The upload to storage failed (${a.status}). Build again in a moment.`;throw new De(a.status===403?401:502,JSON.stringify({error:m}))}e.step?.("storing");let c=r.now()+(e.waitMs??Vg),i=e.pollMs??Yg,u=!1;for(;!u;){let l=await r.call("POST",`${t}${qg}`,{});if(l.status===200||l.status===202)u=!0;else if(l.status===429&&r.now()<c)await r.sleep(i*5);else throw new De(l.status,l.body)}for(;;){let l=await r.call("GET",t,{});if(l.status!==200)throw new De(l.status,l.body);let m=cs(l.body);if(m.state==="stored")return Ml(l.body);if(m.state==="failed")throw new De(typeof m.status=="number"?m.status:502,JSON.stringify({error:m.error??"The image could not be stored. Build again in a moment."}));if(r.now()>=c)throw new De(504,JSON.stringify({error:"The project's host took too long to store the image. Build again in a moment."}));await r.sleep(i)}}ue();var Xg=/^[a-z0-9][a-z0-9_-]{0,31}$/;function Zg(e){return Xg.test(e)}function Ul(e){return Zg(e)?null:`${JSON.stringify(e)} is not a container name: lower-case letters, digits, hyphens and underscores, up to 32 characters, starting with a letter or a digit.`}var Qg=/^sha256:[0-9a-f]{64}$/,eh=/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/,th=/^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*$/,nh=/^(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*(?::[0-9]{1,5})?$/;function rh(e){return Qg.test(e)}function Fl(e){let t=e.trim();if(t.length===0||t.length>512||/\s/.test(t))return{error:`${JSON.stringify(e)} is not an image reference.`};let n=t,r=null,o=n.indexOf("@");if(o!==-1&&(r=n.slice(o+1),n=n.slice(0,o),!rh(r)))return{error:`${JSON.stringify(r)} is not a sha256 digest.`};let s=null,a=n.lastIndexOf("/"),c=n.lastIndexOf(":");if(c>a&&(s=n.slice(c+1),n=n.slice(0,c),!eh.test(s)))return{error:`${JSON.stringify(s)} is not an image tag.`};let i=n.split("/"),u="docker.io",l=i[0]??"";return i.length>1&&(l.includes(".")||l.includes(":")||l==="localhost")&&(u=l.toLowerCase(),i.shift()),nh.test(u)?((u==="index.docker.io"||u==="registry-1.docker.io")&&(u="docker.io"),u==="docker.io"&&i.length===1&&i.unshift("library"),i.length===0||i.some(m=>!th.test(m))?{error:`${JSON.stringify(e)} is not an image name: a repository is lower-case letters, digits and separators.`}:(s===null&&r===null&&(s="latest"),{registry:u,repository:i.join("/"),tag:s,digest:r})):{error:`${JSON.stringify(l)} is not a registry address.`}}var Vv=["application/vnd.oci.image.index.v1+json","application/vnd.docker.distribution.manifest.list.v2+json","application/vnd.oci.image.manifest.v1+json","application/vnd.docker.distribution.manifest.v2+json"].join(", ");function ds(e){let t=e.split(".");if(t.length!==4)return null;let n=[];for(let r of t){if(!/^\d{1,3}$/.test(r))return null;let o=Number(r);if(o>255)return null;n.push(o)}return n}function oh(e){let t=e.replace(/^\[|\]$/g,""),n=t.indexOf("%");if(n!==-1&&(t=t.slice(0,n)),!/^[0-9a-fA-F:.]+$/.test(t)||!t.includes(":"))return null;let r=t.lastIndexOf(":");if(t.slice(r+1).includes(".")){let l=ds(t.slice(r+1));if(!l)return null;t=`${t.slice(0,r+1)}${(l[0]<<8|l[1]).toString(16)}:${(l[2]<<8|l[3]).toString(16)}`}let o=t.split("::");if(o.length>2)return null;let s=l=>l===""?[]:l.split(":"),a=s(o[0]??""),c=o.length===2?s(o[1]??""):[];if([...a,...c].some(l=>!/^[0-9a-fA-F]{1,4}$/.test(l)))return null;let i=a.length+c.length;if(o.length===1?i!==8:i>7)return null;let u=new Array(8-i).fill(0);return[...a.map(l=>parseInt(l,16)),...u,...c.map(l=>parseInt(l,16))]}function sh(e,t,n){let r=ds(t),o=a=>(a[0]<<24|a[1]<<16|a[2]<<8|a[3])>>>0,s=n===0?0:4294967295<<32-n>>>0;return(o(e)&s)===(o(r)&s)}var ah=[["0.0.0.0",8],["10.0.0.0",8],["100.64.0.0",10],["127.0.0.0",8],["169.254.0.0",16],["172.16.0.0",12],["192.0.0.0",24],["192.0.2.0",24],["192.88.99.0",24],["192.168.0.0",16],["198.18.0.0",15],["198.51.100.0",24],["203.0.113.0",24],["224.0.0.0",4],["240.0.0.0",4]];var Yv=1024**3;var Ll=16,ih=/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,62}$/,lh=/^(\d{1,3}(?:\.\d{1,3}){3})(?:\/(\d{1,2}))?$/;function ch(e,t){let n=e??[],r={error:`${t}: ports are up to ${Ll} whole numbers, 1 to 65535.`};if(!Array.isArray(n)||n.length>Ll)return r;let o=n.map(s=>typeof s=="string"&&/^\d{1,5}$/.test(s)?Number(s):s);return o.some(s=>typeof s!="number"||!Number.isInteger(s)||s<1||s>65535)?r:[...new Set(o)].sort((s,a)=>s-a)}function dh(e){let t=e.trim().toLowerCase(),n=lh.exec(t);if(n){let r=ds(n[1]??""),o=n[2]===void 0?32:Number(n[2]);if(!r||o>32)return{error:`${e} is not an IPv4 address or range.`};let s=(r[0]<<24|r[1]<<16|r[2]<<8|r[3])>>>0,a=o===0?0:4294967295<<32-o>>>0,c=(s&a)>>>0,i=[24,16,8,0].map(u=>c>>>u&255);return ah.some(([u,l])=>o>=l&&sh(i,u,l))?{error:`${e} is not on the public internet, and a container reaches nothing else.`}:o===32?i.join("."):`${i.join(".")}/${o}`}return oh(t)?{error:`${e} is an IPv6 address. Containers send over IPv4 only; list its IPv4 address or its host name.`}:t.startsWith("*.")?{error:`${e}: a wildcard cannot be looked up. List each host name, or the address range.`}:ih.test(t)?t:/^[a-z0-9-]{1,63}$/.test(t)?{error:`${e} is not a whole host name. Give it with its domain, like api.example.com.`}:{error:`${e} is not a host name, an IPv4 address or an IPv4 range.`}}function Bl(e){let t=e.trim(),n=t.lastIndexOf(":"),r=n!==-1&&t.indexOf(":")===n,o=r?t.slice(0,n):t,s=ch(r?t.slice(n+1).split(",").map(c=>c.trim()):[],o.trim());if("error"in s)return s;let a=dh(o);return typeof a!="string"?a:{destination:a,ports:s}}function us(e){return e.ports.length===0?e.destination:`${e.destination}:${e.ports.join(",")}`}var Sh=new Set(["pending","pulling","starting","restarting"]),_h=5*6e4;function kr(e){let t=Ul(e);if(t)throw new Error(t)}function fs(e,t,n){kr(t);let r=Fl(n.image);if("error"in r)throw new Error(r.error);return x("cloud-project-containers",{ref:e,action:"deploy",name:t,image:n.image,...n.port===void 0?{}:{port:n.port},...n.registryUser?{registryUser:n.registryUser,registryPassword:n.registryPassword??""}:{},...n.clearCredential?{clearCredential:!0}:{}})}function Kt(e){return x("cloud-project-containers",{ref:e})}function gs(e,t){return kr(t),x("cloud-project-containers",{ref:e,action:"delete",name:t})}function hs(e,t){return kr(t),x("cloud-project-containers",{ref:e,action:"restart",name:t})}function Sr(e,t){return x("cloud-project-containers",{ref:e,action:"logs",...t.name?{name:t.name}:{},...t.errorsOnly?{errorsOnly:!0}:{},...t.since?{since:t.since}:{},limit:t.limit??100})}async function Wl(e,t){let n=Date.now()+_h,r=null;for(;Date.now()<n;)if(await new Promise(o=>setTimeout(o,3e3)),r=(await Kt(e)).containers.find(o=>o.name===t)??null,!r||!Sh.has(r.status))return r;return r}function ys(e){if(!e.allowed)return"stopped: not covered by the plan";let t=e.restarts>0?`, ${e.restarts} restart${e.restarts===1?"":"s"}`:"";return`${e.status}${t}`}async function zl(e,t,n){await Jl(e,t,await fs(e,t,n),n.wait!==!1)}async function Jl(e,t,n,r){let o=n.containers.find(s=>s.name===t)??null;r&&o&&(d(b(`${t}: ${o.image} pinned to ${o.digest.slice(0,19)}\u2026, waiting for the host`)),o=await Wl(e,t)??o),w({...n,containers:o?[o]:n.containers},()=>{if(!o){d(`${O(t)} deployed.`);return}d(`${O(t)} ${ys(o)}`),d(` ${o.url}`),o.statusDetail&&d(` ${o.statusDetail}`),d(""),d(b(" Listens on PORT ("+o.port+"). DATABASE_URL and your secrets are in its environment.")),d(O(" Anything written inside the container is lost when it restarts or is redeployed.")),d(b(" Keep what matters in your database.")),n.allowance&&d(b(` ${n.allowance.deployed} of ${n.allowance.maxContainers} containers on this plan, ${n.allowance.memoryMb} MB and ${n.allowance.cpuPercent}% of a core each`))})}var kh=2*1024**3,vh=3*6e4;async function $h(e){let t;try{t=await br(e)}catch{throw new Error(`${e} does not exist: --dockerfile takes a folder with a Dockerfile in it, or the Dockerfile.`)}if(t.isDirectory()){try{await br(ms(e,"Dockerfile"))}catch{throw new Error(`${e} has no Dockerfile in it.`)}return{context:e,file:null}}return{context:bh(e),file:e}}function Ph(e){return["buildx","build","--platform","linux/arm64","--provenance=false","--output",`type=oci,dest=${e.archive}`,...e.file?["--file",e.file]:[],e.context]}function ws(e){return e.replace(/\b\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com(?:\/[^\s"',;)]*)?/g,"SnoutData's registry").replace(/\barn:aws[a-z-]*:[^\s"',;)]*/g,"a SnoutData credential").replace(/\b[a-z0-9.-]*\.amazonaws\.com(?:\/[^\s"',;)]*)?/g,"SnoutData Cloud").replace(/\b\d{12}\b/g,"(an account)")}function xh(e,t){let n="";try{let r=JSON.parse(t);n=String(r.error??r.message??"")}catch{}return n?ws(n):e===413?"the image is too large to upload: a container's image may be at most 1 GB compressed; a slimmer base (alpine, distroless, -slim) usually fixes it.":`the upload failed (${e}); build again in a moment.`}function Eh(e){return new Promise((t,n)=>{let r=uh("docker",e,{stdio:["ignore",2,2]});r.on("error",o=>n(o)),r.on("close",o=>t(o??1))})}function Vl(e,t,n,r){return new Promise((o,s)=>{let a=new URL(e),c=(a.protocol==="http:"?gh:hh)(a,{method:t,headers:n});c.on("response",i=>{let u=[];i.on("data",l=>u.push(l)),i.on("end",()=>o({status:i.statusCode??0,body:Buffer.concat(u).toString("utf8")})),i.on("error",s)}),c.on("error",i=>{s(i)}),r(c)})}async function Ah(e,t,n){let r=Object.keys(n).length>0?n:{"content-type":"application/octet-stream","content-length":String((await br(t)).size)};return Vl(e,"PUT",r,o=>{let s=ql(t);s.on("error",a=>o.destroy(a)),s.pipe(o)})}function Rh(e){return new Promise((t,n)=>{let r=ph("sha256");ql(e).on("error",n).on("data",o=>r.update(o)).on("end",()=>t(r.digest("hex")))})}var Th={call:(e,t,n)=>Vl(t,e,{"content-length":"0",...n},r=>r.end()),put:Ah,sleep:e=>new Promise(t=>setTimeout(t,e)),now:()=>Date.now()},Yl={docker:Eh,scratch:()=>mh(ms(yh(),"snoutdata-build-")),remove:e=>fh(e,{recursive:!0,force:!0}),size:async e=>(await br(e)).size,hash:Rh,transport:Th,ask:e=>x("cloud-project-containers",e),say:e=>d(b(e)),now:()=>Date.now()};function Hl(e){return`${Math.max(1,Math.round(e/1024**2))} MB`}async function Kl(e,t,n){let r=await e.ask({ref:t,action:"upload-ticket",name:n});if(typeof r.url!="string"||!r.url.startsWith("https://"))throw new Error("SnoutData Cloud did not hand over an upload link; try again in a moment.");return{url:r.url,expiresAt:String(r.expiresAt??"")}}async function Oh(e,t,n,r=Yl){try{if(await r.docker(["version","--format","{{.Server.Version}}"])!==0)throw new Error("docker answered with an error")}catch{throw new Error("deploy --dockerfile builds with your own Docker, and none is running here. Start Docker, or deploy an image from a registry.")}try{let o=await Kl(r,e,t),s=await r.scratch();try{let a=ms(s,"image.tar");if(r.say(`${t}: building ${n.file?wh(n.file):n.context} for linux/arm64`),await r.docker(Ph({...n,archive:a}))!==0)throw new Error("the build failed; its output is above. If it says the docker driver cannot export OCI, turn on the containerd image store, or run: docker buildx create --use");let c=await r.size(a).catch(()=>0);if(c<=0)throw new Error("docker finished but wrote no image archive, so there is nothing to upload.");if(c>kh)throw new Error(`the image is ${Hl(c)}, and a container's image may be at most 1 GB compressed. A slimmer base (alpine, distroless, -slim) usually fixes it.`);Date.parse(o.expiresAt)-r.now()>vh||(o=await Kl(r,e,t));let i=await r.hash(a);r.say(`${t}: uploading the image (${Hl(c)})`);try{return await jl({ticketUrl:o.url,file:a,bytes:c,sha256:i,transport:r.transport,step:u=>{u==="storing"&&r.say(`${t}: storing the image in the project`)}})}catch(u){throw u instanceof De?new Error(xh(u.status,u.body)):u}}finally{await r.remove(s).catch(()=>{})}}catch(o){throw new Error(ws(o instanceof Error?o.message:String(o)))}}async function Xl(e,t,n,r=Yl){kr(t);let o=await $h(n.path),s=await Oh(e,t,o,r),a;try{a=await r.ask({ref:e,action:"deploy",name:t,pushed:s,...n.port===void 0?{}:{port:n.port}})}catch(c){throw new Error(ws(c instanceof Error?c.message:String(c)))}await Jl(e,t,a,n.wait!==!1)}async function Zl(e){let t=await Kt(e);w(t,()=>{if(t.containers.length===0){d(`No containers on ${e}.`),t.allowance&&t.allowance.maxContainers===0?d(b(" Containers run on the Plus and Pro plans.")):d(b(" snoutdata containers deploy <name> <image>"));return}d(W([["name","status","image","url"],...t.containers.map(r=>[r.name,ys(r),r.image,r.url])]));for(let r of t.containers)r.statusDetail&&r.status!=="running"&&d(b(` ${r.name}: ${r.statusDetail}`));let n=t.allowance?.egress;n&&n.allowanceBytes>0&&d(b(` Sent this month: ${ps(n.usedBytes)} of ${ps(n.allowanceBytes)}. At ${ps(n.allowanceBytes)} the project's containers stop until the 1st.`))})}async function _r(e,t){if(t===null)return x("cloud-project-containers",{ref:e,action:"egress",clear:!0});let n=[];for(let r of t){let o=Bl(r);if("error"in o)throw new Error(o.error);n.push(o)}return x("cloud-project-containers",{ref:e,action:"egress",allow:n})}function Nh(e){if(e.allowlist===void 0)return["This control plane does not know about allowlists yet."];if(e.allowlist===null)return[`Containers on ${e.ref} may reach the public internet (within the limits on how much and how fast).`,b(" snoutdata containers egress set <host[:port]> ... to allow only some destinations")];let t=[e.allowlist.allow.length===0?`Containers on ${e.ref} may reach nothing outside the project.`:`Containers on ${e.ref} may reach only:`];for(let n of e.allowlist.allow)t.push(` ${us(n)}${n.ports.length===0?b(" (any port)"):""}`);return t.push(b(` and always: ${e.allowlist.alwaysAllowed.map(us).join(", ")}, and each other`)),t.push(b(" snoutdata containers egress clear to allow the public internet again")),t}async function vr(e,t){let n=t.clear?await _r(e,null):t.set?await _r(e,t.set):await Kt(e);w({ref:n.ref,allowlist:n.allowlist??null},()=>{for(let r of Nh(n))d(r);(t.set||t.clear)&&d(b(" Running containers follow it within a minute; nothing restarts."))})}function ps(e){return`${Math.round(e/1024**3*10)/10} GB`}async function Ql(e,t){await hs(e,t);let n=await Wl(e,t);w({ref:e,containers:n?[n]:[]},()=>{d(n?`${O(t)} ${ys(n)}`:`${t} is restarting.`),n?.statusDetail&&d(` ${n.statusDetail}`)})}async function ec(e,t){let n=await gs(e,t);w(n,()=>d(`${t} is gone. It stops within a few seconds.`))}function Gl(e){return`${b(e.at.replace("T"," ").slice(0,19))} ${e.name} ${e.stream==="stderr"?O(e.message):e.message}`}async function tc(e,t,n){let r=((await Sr(e,{name:t,errorsOnly:n.errorsOnly,limit:n.limit})).lines??[]).slice().reverse();if(w({ref:e,lines:r},()=>{if(r.length===0&&!n.follow){d(`Nothing printed${t?` by ${t}`:""} on ${e} in the last seven days.`),d(b(" Lines show up about a minute after a container prints them."));return}for(let s of r)process.stdout.write(`${Gl(s)}
|
|
488
|
+
`)}),!n.follow)return;let o=r.at(-1)?.at??new Date().toISOString();for(;;){await new Promise(a=>setTimeout(a,5e3));let s=((await Sr(e,{name:t,errorsOnly:n.errorsOnly,limit:n.limit,since:o})).lines??[]).slice().reverse();for(let a of s)process.stdout.write(`${Gl(a)}
|
|
489
|
+
`),o=a.at}}me();Ae();ue();function rc(e){let t=e.indexOf("=");return t<=0?null:{name:e.slice(0,t),value:e.slice(t+1)}}async function Gt(){let e=[];for await(let t of process.stdin)e.push(Buffer.from(t));return Buffer.concat(e).toString("utf8").replace(/\n$/,"")}async function oc(e,t){if(e.length===0)throw new g("secrets set needs NAME=value, or NAME with --stdin");if(t.stdin){if(e.length!==1)throw new g("--stdin sets one secret, so it takes one NAME");return[{name:e[0],value:await Gt()}]}return e.map(n=>{let r=rc(n);if(!r)throw new g(`${n} is not NAME=value. Use --stdin to set a value from a pipe.`);return r})}async function sc(e,t,n){if(t.length===0)throw new g("secrets set needs NAME=value, or NAME with --stdin");let r=[];if(n.stdin){if(t.length!==1)throw new g("--stdin sets one secret, so it takes one NAME");r.push({name:t[0],value:await Gt()})}else for(let s of t){let a=rc(s);if(!a)throw new g(`${s} is not NAME=value. Use --stdin to set a value from a pipe.`);r.push(a)}for(let s of r){let a=gn(s.name);if(a)throw new g(a)}let o=null;for(let s of r)o=await x("cloud-project-secrets",{ref:e,action:"set",name:s.name,value:s.value});w(o,()=>{for(let s of r)d(`${O(s.name)} set`);d(b(" Functions pick it up within a few seconds. Redeploying is not needed.")),n.stdin||d(b(" A value typed here is in your shell history. In CI, use: \u2026 | snoutdata secrets set NAME --stdin"))})}async function ac(e,t){let n=gn(t);if(n)throw new g(n);let r=await x("cloud-project-secrets",{ref:e,action:"unset",name:t});w(r,()=>{d(`${t} is gone.`)})}async function ic(e){let t=await x("cloud-project-secrets",{ref:e});w(t,()=>{if(t.secrets.length===0){d(`${e} has no function secrets.`),d(b(" snoutdata secrets set STRIPE_KEY=sk_..."));return}process.stdout.write(`${W([["NAME","SIZE","SET"],...t.secrets.map(n=>[n.name,n.bytes===null?"-":`${n.bytes} B`,ot(n.updatedAt)])])}
|
|
490
|
+
`),d(b("Values are not readable, here or anywhere. Set one again to change it."))})}import{spawn as Pn}from"node:child_process";import{appendFileSync as Oc,existsSync as Tr,readFileSync as Nc,statSync as wy,unlinkSync as by,writeFileSync as Wt}from"node:fs";import{dirname as Sy,join as Or}from"node:path";import{tmpdir as _y}from"node:os";import{open as Ic}from"node:fs/promises";function lc(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 cc(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 dc(e){let t=e.split(`
|
|
491
|
+
`).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}}var bs={auth:"snout_auth_admin",storage:"snout_storage_admin",push:"snout_push_admin"},Ch=/^(snoutpod_admin|snout_[a-z]+_admin)$/,$r=new Set(["snout_identity","snout_files","push","snout_live","snout_realtime","_realtime","graphql","snout_graph","snout_ext","net","cron","pgbouncer","vault","pgsodium","auth","storage","realtime","graphql_public","extensions"]),Dh=/^(pgrst_ddl_watch|pgrst_drop_watch|snoutpod_[a-z_]+|snoutdata_realtime|snout_realtime_messages)(\(|$)/,Mh={snout_identity:"auth",snout_files:"storage",auth:"auth",storage:"storage",push:"push"},jh=new Set(["push.settings"]),Lh=["TABLE DATA","SEQUENCE SET","SEQUENCE OWNED BY","FK CONSTRAINT","CHECK CONSTRAINT","DEFAULT ACL","EVENT TRIGGER","ROW SECURITY","INDEX ATTACH","TABLE ATTACH","MATERIALIZED VIEW DATA","MATERIALIZED VIEW","PUBLICATION TABLES IN SCHEMA","PUBLICATION TABLE","FOREIGN TABLE","FOREIGN DATA WRAPPER","FOREIGN SERVER","USER MAPPING","TEXT SEARCH CONFIGURATION","TEXT SEARCH DICTIONARY","TEXT SEARCH PARSER","TEXT SEARCH TEMPLATE","LARGE OBJECT","PROCEDURAL LANGUAGE","OPERATOR CLASS","OPERATOR FAMILY","ACCESS METHOD","EXTENDED STATISTICS","DATABASE PROPERTIES"];function Uh(e){let t=/^\d+; \d+ \d+ (.+)$/.exec(e.trim());if(!t)return null;let n=t[1],r=Lh.find(i=>n.startsWith(`${i} `))??n.split(" ")[0],o=n.slice(r.length).trim().split(" "),s=o[0]??"-",a=o.length>2?o[o.length-1]:"",c=o.slice(1,o.length>2?-1:void 0).join(" ");return{line:e.trim(),type:r,schema:s,name:c,owner:a}}function Fh(e){let t=new Map,n=o=>o.replace(/"/g,""),r=/ALTER TABLE (?:ONLY )?([\w."$]+)\s+ADD CONSTRAINT [^;]*?FOREIGN KEY \([^)]*\) REFERENCES ([\w."$]+)\(/g;for(let o of e.matchAll(r)){let s=n(o[1]),a=n(o[2]);if(s!==a){let c=t.get(s)??new Set;c.add(a),t.set(s,c)}}return t}function uc(e,t){let n=new Map,r=[],o=new Set,s=0;for(let i of e.split(`
|
|
492
|
+
`)){let u=Uh(i);if(!u)continue;let{type:l,schema:m,name:h,owner:P}=u;if(l==="TABLE DATA"||l==="SEQUENCE SET"){let ee=/^(schema_migrations|migrations)$/.test(h)&&$r.has(m);if(/realtime/.test(m)||m==="snout_live"||ee||m==="public"&&h==="spatial_ref_sys"){s++;continue}if(m==="cron"){s++,o.add("Scheduled jobs (pg_cron) are not restored: each names the project it came from. Schedule them again with cron.schedule.");continue}if(m==="push"&&h==="credentials"){s++,o.add("Push credentials are not restored: they are sealed for the project they came from. Set them again with `snoutdata push credentials set`.");continue}let z=Mh[m];if(z){z==="storage"&&(h==="objects"||h==="files")&&o.add("Storage objects are restored as rows; the files themselves are not part of an export. Upload them again into this project."),n.set(z,[...n.get(z)??[],u]);continue}if($r.has(m)){s++;continue}r.push(u.line);continue}if(l==="COMMENT"&&m==="-"&&/^EXTENSION( |$)/.test(h)||(l==="SCHEMA"?$r.has(h):$r.has(m))||Dh.test(h)||Ch.test(P)&&l!=="SCHEMA"){s++;continue}r.push(u.line)}let a=Fh(t),c=["auth","storage","push"];return{platformData:c.filter(i=>n.has(i)).map(i=>{let u=n.get(i),l=u.filter(h=>h.type==="TABLE DATA"&&jh.has(`${h.schema}.${h.name}`)).map(h=>`${h.schema}.${h.name}`),m=u.filter(h=>h.type==="TABLE DATA").map(h=>`${h.schema}.${h.name}`);return{product:i,clear:l,tables:m,lines:Bh(u,a)}}),rest:r,products:c.filter(i=>n.has(i)),skipped:s,notes:[...o]}}function Bh(e,t){let n=c=>`${c.schema}.${c.name}`,r=new Set(e.map(n)),o=new Set,s=[],a=[...e];for(;a.length>0;){let i=a.find(u=>[...t.get(n(u))??[]].every(l=>!r.has(l)||o.has(l)||l===n(u)))??a[0];o.add(n(i)),s.push(i.line),a=a.filter(u=>u!==i)}return s}me();Ae();Se();ue();me();Se();var _s=["max_write_pause_ms","drain_timeout_ms","ack_timeout_ms","copy_mb_per_s"],mc=["advice_sample_ms","advice_min_bytes","advice_split_bytes","advice_skew_pct","advice_hot_pct","advice_min_writes_per_s","advice_pin_pct","advice_node_disk_bytes","advice_disk_pct","advice_conn_pct"],fc=["off","advise","auto"];function yn(e){return e==="done"||e==="failed"||e==="cancelled"}function wn(e,t=process.env){if(e.project!==void 0){if(e.admin!==void 0)throw new T("usage","cluster takes --project (a SnoutData Cloud project) or --admin (a standalone router), not both");return new Ve(Wh(e.project))}let n=e.admin??pc(t.LEPIS_ADMIN_URL);if(!n)throw new T("usage","cluster needs a project: --project <ref> for a SnoutData Cloud project, or --admin <url> (or LEPIS_ADMIN_URL) for the admin API of a standalone Lepis router (LEPIS_ADMIN_ADDR)");let r=e.token??pc(t.LEPIS_ADMIN_TOKEN);if(!r)throw new T("not-signed-in","no admin token: set LEPIS_ADMIN_TOKEN to the router's token, or pass --token");return new Ss(Hh(n),r)}function pc(e){return e===void 0||e===""?void 0:e}function Hh(e){let t=/^[a-z][a-z0-9+.-]*:\/\//i.test(e)?e:`http://${e}`,n;try{n=new URL(t)}catch{throw new T("usage",`--admin: not a URL: ${e}`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new T("usage",`--admin: http or https, not ${n.protocol.replace(":","")}`);if(n.username||n.password)throw new T("usage","--admin: the URL carries no credentials; the token comes from LEPIS_ADMIN_TOKEN or --token");if(n.protocol==="http:"&&!Kh(n.hostname))throw new T("usage",`--admin: ${n.host} is not this machine, and plain http would send the token in the clear. Use https (the router serves it when it has LEPIS_TLS_CERT), or a tunnel to 127.0.0.1`);return n.pathname=n.pathname.replace(/\/+$/,""),n.search="",n.hash="",n}function Kh(e){let t=e.replace(/^\[|\]$/g,"").toLowerCase();return t==="localhost"||t==="::1"||/^127\.\d+\.\d+\.\d+$/.test(t)}var Gh=6e4,Ss=class{target;#t;#n;constructor(t,n){this.#t=t,this.#n=n,this.target=t.toString().replace(/\/$/,"")}status(){return this.#e("GET","/v1/status")}ops(){return this.#e("GET","/v1/ops")}plan(t){return this.#e("POST","/v1/plan",t)}submit(t){return this.#e("POST","/v1/jobs",t)}async jobs(){return(await this.#e("GET","/v1/jobs")).jobs}job(t){return this.#e("GET",`/v1/jobs/${t}`)}cancel(t){return this.#e("POST",`/v1/jobs/${t}/cancel`)}resume(t){return this.#e("POST",`/v1/jobs/${t}/resume`)}settings(t){return this.#e("POST","/v1/settings",t)}advice(t){return this.#e("GET",t===void 0?"/v1/advice":`/v1/advice?sample_ms=${Math.max(0,Math.floor(t))}`)}autopilot(){return this.#e("GET","/v1/autopilot")}setAutopilot(t){return this.#e("POST","/v1/settings",t)}async#e(t,n,r){let o=`${this.target}${n}`,s;try{s=await fetch(o,{method:t,headers:{authorization:`Bearer ${this.#n}`,accept:"application/json",...t==="POST"?{"content-type":"application/json"}:{}},...t==="POST"?{body:JSON.stringify(r??{})}:{},signal:AbortSignal.timeout(Gh)})}catch(i){let u=Wn(i),l=i instanceof Error?i.cause instanceof Error?i.cause.message:i.message:String(i);throw new T(u==="failed"?"network":u,`could not reach the Lepis admin API at ${this.target}: ${l}`)}let a=await s.text(),c;try{c=a===""?{}:JSON.parse(a)}catch{throw new T("server",`the admin API at ${this.target} answered ${s.status} with something that is not JSON`,{status:s.status})}if(!s.ok)throw gc(s.status,c);return c}},qh={bad_request:"usage",unauthorized:"not-signed-in",not_found:"not-found",conflict:"conflict",failed:"failed",timeout:"timeout"};function gc(e,t){let n=t?.error??{},r=n.message??`the admin API answered ${e}`,o={status:e};n.code&&(o.apiCode=n.code),n.kind&&(o.kind=n.kind),n.sqlstate&&(o.sqlstate=n.sqlstate);let s=(n.code?qh[n.code]:void 0)??(e===401?"not-signed-in":e===400?"usage":nt(e));return s==="not-signed-in"?new T(s,"the router refused the admin token (LEPIS_ADMIN_TOKEN or --token)",o):new T(s,r,o)}function Wh(e){let t=e.trim();if(!/^[a-z][0-9a-z]{12}$/.test(t))throw new T("usage",`--project: not a project ref: ${e} (13 characters, as projects list prints it)`);return t}var zh={"node.add":"add-node","node.attach":"attach","node.drain":"drain","node.remove":"remove","keyspace.create":"keyspace","table.distribute":"distribute","table.reference":"reference","table.global":"global","table.auto":"auto","range.split":"split","range.merge":"merge","range.move":"move","tenant.pin":"pin",rebalance:"rebalance",scale:"scale",verify:"verify",cleanup:"cleanup","keyspace.drop":"drop-keyspace",dissolve:"disable"},Jh="cloud-project-shards",Ve=class{target;ref;#t;constructor(t,n=x){this.ref=t,this.target=`project ${t}`,this.#t=n}async status(){let t=await this.#e({action:"status"}),{cluster:n,...r}=t;return{...n??{catalog:!1},cloud:r}}ops(){return this.#e({action:"ops"})}async plan(t){return this.#n(t),this.#e({action:"plan",request:t})}async submit(t){let n=this.#n(t),r={...t};delete r.op;let o=await this.#e({action:n,request:r});return typeof o.job=="number"?{job:o.job,plan:o.plan}:{done:ft(o),answer:o}}async jobs(){return(await this.#e({action:"jobs"})).jobs}job(t){return this.#e({action:"jobs",id:t})}cancel(t){return this.#e({action:"cancel",id:t})}resume(t){return this.#e({action:"resume",id:t})}settings(t){return this.#e({action:"settings",settings:t})}advice(t){return this.#e({action:"advice",...t===void 0?{}:{sampleMs:Math.max(0,Math.floor(t))}})}autopilot(){return this.#e({action:"autopilot"})}setAutopilot(t){return this.#e({action:"settings",settings:t})}enable(t){return this.#e({action:"enable",...t?{plan:!0}:{}})}spread(t,n){return this.#e({action:"spread",request:t,...n?{plan:!0}:{}})}retry(t){return this.#e({action:"retry",...t})}disable(t){return this.#e({action:"disable",...t?{plan:!0}:{}})}replica(t,n,r){return this.#e({action:n?"remove-replica":"add-replica",node:t,...r?{plan:!0}:{}})}async alerts(){return(await this.status()).cloud?.alerts??[]}acknowledge(t){return this.#e({action:"acknowledge",alert:t})}#n(t){let n=zh[t.op];if(!n)throw new T("usage",`${t.op} is not an operation a SnoutData Cloud project takes`);let r=t;if(t.op==="node.add"&&(r.host!==void 0||r.name!==void 0))throw new T("usage","in SnoutData Cloud a node is a pod the project makes: nodes add takes no name or --host. It joins the cluster on its own once it is ready.");if(t.op==="scale"&&(r.add!==void 0||r.remove!==void 0))throw new T("usage","in SnoutData Cloud, scale takes --nodes N: how many nodes the project should have, the home included");return n}async#e(t){try{return await this.#t(Jh,{ref:this.ref,...t})}catch(n){throw Vh(n)}}};function ft(e){if(Array.isArray(e.created)){let t=e.created.map(String),n=typeof e.stopped=="string"?` Stopped there: ${e.stopped}`:"",r=t.length===1;return e.joining===!1?`New node${r?"":"s"} ${t.join(", ")}, starting as a copy of the home node. Attach ${r?"it":"each"} once it is ready: snoutdata cluster nodes attach <ref>.${n}`:`New node${r?"":"s"} ${t.join(", ")}, starting as a copy of the home node. ${r?"It joins":"They join"} the cluster and take${r?"s":""} ${r?"its":"their"} share of the data on ${r?"its":"their"} own; follow it with snoutdata cluster status.${n}`}return e.deleted===!0?`The pod of ${String(e.pod)} is being deleted. It owned nothing in the cluster.`:Array.isArray(e.queued)?`${String(e.next??"The table is being spread over the cluster.")} Steps: ${e.queued.map(String).join(", ")}. Follow it with snoutdata cluster status.`:typeof e.joining=="boolean"&&typeof e.node=="string"?e.joining?`${e.node} is joining the cluster again.`:`${e.node} is left as it is.`:e.queue==="retrying"?"The cluster is trying that step again.":typeof e.replica=="string"&&typeof e.node=="string"?`A replica of node ${e.node} is being made (${e.replica}). ${String(e.note??"It protects the node once it is following it.")}`:typeof e.changed=="boolean"&&typeof e.node=="string"&&e.sharded===void 0?e.changed?`The replica of node ${e.node} is being removed, and its project slot freed.`:`node ${e.node} has no replica. Nothing changed.`:e.acknowledged===!0?"Marked as seen. The alert stays open until what it is about is over.":e.unchanged===!0?`The project already has ${String(e.nodes)} nodes. Nothing changed.`:e.sharded===!0?e.changed===!1?"This project is already a cluster.":String(e.note??"This project is a cluster now."):e.sharded===!1?e.changed===!1?"This project is not a cluster.":String(e.note??"This project is not a cluster any more."):typeof e.next=="string"?e.next:"Done."}function Vh(e){if(!(e instanceof $e))return e;let t=e.body??{};if(typeof t.apiCode=="string"||typeof t.kind=="string")return gc(e.status,{error:{code:typeof t.apiCode=="string"?t.apiCode:void 0,kind:typeof t.kind=="string"?t.kind:void 0,message:e.message,sqlstate:typeof t.sqlstate=="string"?t.sqlstate:void 0}});let n=t.code==="not-sharded"?"conflict":nt(e.status);return new T(n,e.message,{status:e.status,...typeof t.code=="string"?{kind:t.code}:{}})}function Sn(e){if(Array.isArray(e))return`[${e.map(Sn).join(",")}]`;if(e&&typeof e=="object"){let t=e;return`{${Object.keys(t).filter(n=>t[n]!==void 0).sort().map(n=>`${JSON.stringify(n)}:${Sn(t[n])}`).join(",")}}`}return JSON.stringify(e)??"null"}var $s=class{#t=new Map;#n;#e;constructor(t=15*6e4,n=Date.now){this.#n=t,this.#e=n}remember(t,n=""){this.#t.set(`${n}
|
|
493
|
+
${Sn(t)}`,this.#e())}shown(t,n=""){let r=this.#t.get(`${n}
|
|
494
|
+
${Sn(t)}`);return r!==void 0&&this.#e()-r<=this.#n}forget(t,n=""){this.#t.delete(`${n}
|
|
495
|
+
${Sn(t)}`)}},Yh=new $s,Ps=["node.add","node.drain","node.remove","keyspace.create","table.distribute","table.reference","table.global","table.auto","range.split","range.merge","range.move","tenant.pin","rebalance","scale","verify","cleanup","node.attach","keyspace.drop","dissolve"],Xh="2024-11-05";function Y(e,t=[]){return{type:"object",properties:e,additionalProperties:!1,...t.length?{required:t}:{}}}var F={type:"string"},hc={type:"object",description:'The operation as the router takes it, e.g. {"op": "range.split", "keyspace": "k", "range": "-3074457345618258603", "at": "18454348402311335", "to": 3}. Range bounds and seeds are 64-bit, so pass them as strings. cluster_advice returns these ready-made, as `operation`. For a SnoutData Cloud project a node is a pod the project makes: {"op": "node.add"} makes one (no host), {"op": "node.attach", "name": "<its ref>"} hands a ready one to the cluster, and {"op": "scale", "nodes": N} is a node count.',properties:{op:{type:"string",enum:[...Ps]}},required:["op"],additionalProperties:!0},Pr={type:"integer",description:"The job id cluster_run returned, or one from cluster_jobs."},gt={type:"string",description:"A SnoutData Cloud project's ref (from list_projects): its cluster, as the signed-in user. Omit for the standalone router this server was started with (LEPIS_ADMIN_URL)."},kn=[{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:Y({})},{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. Under `local`, the self-hosted projects on this machine (SnoutData Studio's, or linked with `snoutdata link --local`), whose refs work with get_connection_url, push_migrations, get_project and the function tools.",inputSchema:Y({})},{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:Y({name:F,region:F,teamId:F},["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:Y({ref:F},["ref"])},{name:"pause_project",description:"Stop a project, and wait until it is paused. Its data is kept and the next connection wakes it. A production project refuses this.",inputSchema:Y({ref:F},["ref"])},{name:"resume_project",description:"Start a paused project without waiting for a connection to wake it, and wait until it is ready.",inputSchema:Y({ref:F},["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 nothing installed: the CLI connects to the database itself. A file holding a psql command (\\i, \\set), or a COMMIT, END or ROLLBACK that would end the transaction the file runs in, is refused before anything runs; a file starting with -- snoutdata:no-transaction runs as written, outside a transaction.",inputSchema:Y({ref:F,dir:F,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:Y({ref:F,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:Y({ref:F},["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:Y({ref:F},["ref"])},{name:"reset_password",description:"Rotate the project's database password. THE OLD ONE STOPS WORKING usually within fifteen seconds (longer while the host is busy starting other databases), including any DATABASE_URL already written into a file or a running process; connections already open keep working. Returns the new connection URL, which works once the change applies (wait about fifteen seconds before connecting, and retry a refusal for up to two minutes). Use it when a credential has leaked, not routinely.",inputSchema:Y({ref:F},["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:Y({})},{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:Y({})},{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. Without `project` it is as capable as this account, so hand it over deliberately; with a project ref it reaches that one project and nothing else, which is what a CI job for one project should get. Optionally expires, as an ISO date.",inputSchema:Y({name:F,expires:F,project:F},["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:Y({id:F},["id"])},{name:"deploy_function",description:"Put a folder of TypeScript on the edge as an HTTP endpoint at https://<ref>.api.snoutdata.com/v1/fn/<name>. Reads functions/<name> unless dir says otherwise. Deno, so no build step and no node_modules. Set openToAnyone ONLY for a webhook receiver whose sender cannot send an API key: it makes the URL callable by anybody who knows it, and the function must then check the sender's signature itself.",inputSchema:Y({ref:F,name:F,dir:F,entrypoint:F,openToAnyone:{type:"boolean"}},["ref","name"])},{name:"list_functions",description:"The functions deployed to a project, their URLs, bundle sizes, whether each one needs an API key, and each one's memory and concurrency with the plan's limits for both. Costs nothing.",inputSchema:Y({ref:F},["ref"])},{name:"size_function",description:"Set one function's memory (MB one worker may use) and concurrency (how many workers it may run at once), within the project's plan. memoryMb x concurrency may not exceed the project's memory; list_functions shows each function's size and the plan's limits (limit.functionMemoryMb, limit.concurrencyMax, limit.podMemoryMb). A size that does not fit is refused with a sentence saying why. Omit one to keep it as it runs now; reset: true goes back to the plan's default. A second worker only helps CPU-bound work: requests that wait on the network share one.",inputSchema:Y({ref:F,name:F,memoryMb:{type:"integer"},concurrency:{type:"integer"},reset:{type:"boolean"}},["ref","name"])},{name:"delete_function",description:"Remove one function from a project. It stops answering within a few seconds. It does not touch the database.",inputSchema:Y({ref:F,name:F},["ref","name"])},{name:"list_containers",description:"The project's Snout Containers: each one's status (pending, pulling, starting, running, restarting, failed, stopped), why when it is not running, its image and pinned digest, restarts, and its public URL. allowed=false means the plan no longer covers it and it is stopped. Also the plan's allowance. Costs nothing.",inputSchema:Y({ref:F},["ref"])},{name:"deploy_container",description:"Run a Docker image all the time beside the project, at https://<ref>.api.snoutdata.com/containers/v1/<name>/ (no API key needed there: the container authenticates its own callers). The image tag is pinned to the digest it points at now and must include linux/arm64. It listens on PORT (port, default 8080) and gets DATABASE_URL and the project's secrets in its environment. STATELESS: anything written inside it is lost on every restart or redeploy, so state belongs in the database. Plus and Pro plans only. A private image needs a person's first deploy with the CLI (no tool takes a registry token); a redeploy reuses the stored login. Returns at once: poll list_containers until the status is running or failed.",inputSchema:Y({ref:F,name:F,image:F,port:{type:"integer"}},["ref","name","image"])},{name:"restart_container",description:"Start one container again from its image. Everything written inside it is lost, which is also how to clear a container stuck in a bad state. Poll list_containers to see it come back.",inputSchema:Y({ref:F,name:F},["ref","name"])},{name:"delete_container",description:"Stop one container and remove it from the project. It does not touch the database.",inputSchema:Y({ref:F,name:F},["ref","name"])},{name:"container_logs",description:"What a project's containers printed to stdout and stderr in the last seven days, newest first: each line's time, container, stream and text. Lines arrive about a minute after they are printed. Use it when list_containers says failed or restarting. name narrows to one container, errorsOnly to stderr, limit is at most 500 (default 100). Costs nothing.",inputSchema:Y({ref:F,name:F,errorsOnly:{type:"boolean"},limit:{type:"integer"}},["ref"])},{name:"container_egress",description:"Where a project's containers may send. allowlist null means the public internet (within the limits on how much and how fast); otherwise allowlist.allow is the only destinations they reach, and alwaysAllowed what they reach whatever the list says (the project's own database and API). The same as `snoutdata containers egress`. Costs nothing.",inputSchema:Y({ref:F},["ref"])},{name:"set_container_egress",description:'Hold a project\'s containers to an allowlist, or let them reach the public internet again. Give EITHER allow, a list of destinations ("api.example.com:443", "api.example.com" for any port, "140.82.112.0/20:443,8443"; an empty list means nothing outside the project), OR clear: true. The list replaces the one there. Running containers follow it within a minute and nothing restarts, so a container that needs a destination left off stops reaching it. The same as `snoutdata containers egress set|clear`.',inputSchema:Y({ref:F,allow:{type:"array",items:F},clear:{type:"boolean"}},["ref"])},{name:"list_function_secrets",description:"The NAMES of the environment variables a project's functions run with, their sizes and when each was last set. Never the values, which are not readable by anything but the runtime. THERE IS NO TOOL THAT SETS ONE: a secret in a tool call is a secret in an audit log, so a person sets it with `snoutdata secrets set`. Use this to check whether one a function needs is there.",inputSchema:Y({ref:F},["ref"])},{name:"function_logs",description:"What a project's functions printed with console.log and console.error in the last seven days, newest first: each line's time, function, level ('error' for console.error and console.warn) and text. Lines arrive about a minute after they are printed. Use it after deploying or calling a function to see why it failed. name narrows to one function, errorsOnly to errors, limit is at most 500 (default 100). Costs nothing.",inputSchema:Y({ref:F,name:F,errorsOnly:{type:"boolean"},limit:{type:"integer"}},["ref"])},{name:"get_project",description:"Everything about one project in one call: its state, region and size, which products are on (auth, storage, data-api), the names of its deployed functions and function secrets, and its custom domains. Never a password, a key or a download link (export_status has the link to the last export).",inputSchema:Y({ref:F},["ref"])},{name:"list_products",description:"Whether a project's auth (user sign-up and sign-in), storage (files) and data API (REST and GraphQL over its tables) are on, and whether the plan allows the data API.",inputSchema:Y({ref:F},["ref"])},{name:"set_product",description:"Turn auth, storage or data-api on or off for a project. It starts within about a minute. All three are on every plan, including free.",inputSchema:Y({ref:F,product:{type:"string",enum:["auth","storage","data-api"]},enabled:{type:"boolean"}},["ref","product","enabled"])},{name:"list_domains",description:"The custom domains in front of a project's API, whether each is verified, and the DNS records each needs.",inputSchema:Y({ref:F},["ref"])},{name:"add_domain",description:"Serve a project's API at the user's own hostname (paid plans). Returns the DNS records the user must publish before verify_domain can succeed.",inputSchema:Y({ref:F,hostname:F},["ref","hostname"])},{name:"verify_domain",description:"Check a custom domain's DNS records and verify it once they are published.",inputSchema:Y({ref:F,hostname:F},["ref","hostname"])},{name:"remove_domain",description:"Stop serving a project at a custom domain.",inputSchema:Y({ref:F,hostname:F},["ref","hostname"])},{name:"restore_window",description:"How far back a point-in-time restore of a project can go, or why it cannot (the plan, or no backup yet). Starts nothing.",inputSchema:Y({ref:F},["ref"])},{name:"restore_to_point",description:"Rewind a project to a moment (ISO 8601, inside restore_window) into a NEW project beside it, never over it. Costs a project slot on the plan. The original is untouched.",inputSchema:Y({ref:F,at:F,name:F},["ref","at"])},{name:"cluster_status",description:"A Lepis cluster (one Postgres database spread over several nodes): a Cloud project's with project, else the router this server was started against with LEPIS_ADMIN_URL. Its nodes, keyspaces and their ranges (a range is keyspace:lo; bounds are strings), tables, routers, unfinished jobs and settings; for a Cloud project also whether it is a cluster, its plan's node limit, and each node pod with the step that follows (attach, delete-pod, wait). Costs nothing.",inputSchema:Y({project:gt})},{name:"cluster_advice",description:"Ask the cluster's advisor what to split, move or add, from each node's size, connections and write rate and each range's measured share. Each recommendation has a reason, its plan, and an `operation` that cluster_run takes as it is. RUNS NOTHING. sampleMs: how long to measure write rates over (the cluster's setting by default; 0 skips them; at most 30000).",inputSchema:Y({project:gt,sampleMs:{type:"integer"}})},{name:"cluster_plan",description:"The dry run of one cluster operation: its steps, what moves where, rows, bytes, the estimated copy time and the expected write pause. Changes nothing. Planning an operation is what lets cluster_run run it.",inputSchema:Y({project:gt,operation:hc},["operation"])},{name:"cluster_run",description:"Run one cluster operation as a durable job. IT MOVES DATA, and each cutover pauses writes to what moves for up to the cluster's max_write_pause_ms. Without confirm: true it only returns the plan and runs nothing: show that plan to the user, and when they agree call again with the same operation and confirm: true. confirm runs only an operation planned here (cluster_plan, cluster_advice, or cluster_run without confirm) in the last 15 minutes. Returns the job id; follow it with cluster_watch_job.",inputSchema:Y({project:gt,operation:hc,confirm:{type:"boolean"}},["operation"])},{name:"cluster_jobs",description:"The cluster's latest jobs, or with id one job with each step's state and phase. Costs nothing.",inputSchema:Y({project:gt,id:Pr})},{name:"cluster_watch_job",description:"Wait for a job to finish, up to timeoutSeconds (default 30, at most 120), and return its state and steps: done, failed (with the error; cluster_resume_job runs it again from the failed step) or cancelled. Running out of time stops the waiting, never the job; call again to keep waiting.",inputSchema:Y({project:gt,id:Pr,timeoutSeconds:{type:"integer"}},["id"])},{name:"cluster_cancel_job",description:"Stop a running job. A move cancelled before its cutover rolls back; one cancelled after it finishes its cleanup.",inputSchema:Y({project:gt,id:Pr},["id"])},{name:"cluster_resume_job",description:"Run a failed job again from the step that failed. It carries on the operation that was already approved and planned; follow it with cluster_watch_job.",inputSchema:Y({project:gt,id:Pr},["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:Y({ref:F},["ref"])}],Zh=new Set(["whoami","list_projects","get_connection_url","usage","export_status","list_teams","list_tokens","list_functions","list_function_secrets","function_logs","container_egress","get_project","list_products","list_domains","restore_window","cluster_status","cluster_advice","cluster_plan","cluster_jobs","cluster_watch_job"]),Qh=new Set(["delete_project","delete_function","reset_password","revoke_token","remove_domain","cluster_run"]),yc=["get_connection_url","push_migrations","get_project","list_functions","deploy_function","delete_function","list_function_secrets"];function xs(e){if(!e||typeof e!="object")return e;let t=e;if(Array.isArray(t.projects))return{...t,projects:t.projects.map(xs)};let n=t.export;if(n&&typeof n=="object"){let{url:r,rolesSql:o,...s}=n;return{...t,export:{...s,hasLink:typeof r=="string"&&r.length>0}}}return e}function Es(e,t){return t.available?null:t.pitrEnabled?`${e} has no backup to restore from yet.`:`Point-in-time restore is part of the Pro and Business plans, and this project is on ${t.tier??"another plan"}.`}function ey(e){return{...e,annotations:Zh.has(e.name)?{readOnlyHint:!0}:{readOnlyHint:!1,destructiveHint:Qh.has(e.name)}}}function ty(e){let t=new Set;return e.filter(n=>!t.has(n.name)&&t.add(n.name))}function _n(e,t){return{jsonrpc:"2.0",id:e,result:t}}function xr(e,t,n){return{jsonrpc:"2.0",id:e,error:{code:t,message:n}}}function R(e,t,n=!1){let r=typeof t=="string"?t:JSON.stringify(t,null,2);return _n(e,{content:[{type:"text",text:r}],...n?{isError:!0}:{}})}function ny(e,t=new Date){let n=Date.parse(e);return!Number.isFinite(n)||n<=t.getTime()?null:Math.ceil((n-t.getTime())/864e5)}function bn(e){return{op:e.op,cutovers:e.cutovers,moves:(e.moves??[]).map(t=>({from:t.source,to:t.targets,tables:t.tables,estimated_rows:t.estimated_rows,estimated_bytes:t.estimated_bytes,estimated_copy_seconds:t.estimated_copy_seconds,expected_pause_ms:t.expected_pause_ms})),steps:(e.steps??[]).map(t=>t.kind),estimated_rows:e.estimated_rows,estimated_bytes:e.estimated_bytes,estimated_copy_seconds:e.estimated_copy_seconds,expected_pause_ms:e.expected_pause_ms,max_write_pause_ms:e.max_write_pause_ms,warnings:e.warnings??[],...e.assumptions?{assumptions:e.assumptions}:{}}}function ks(e){return{id:e.id,op:e.op,state:e.state,finished:yn(e.state),error:e.error,steps:e.steps.map(t=>({n:t.n+1,kind:t.kind,state:t.state,...typeof t.detail?.phase=="string"?{phase:t.detail.phase}:{}})),...e.state==="failed"?{next:`cluster_resume_job {"id": ${e.id}} runs it again from the failed step.`}:{}}}function wc(e,t){let n=e.operation;if(!n||typeof n!="object"||Array.isArray(n))return`${t} needs operation: an object like {"op": "range.split", "keyspace": "k", "range": "<lo>"}. cluster_advice returns them ready-made.`;let r=n;if(typeof r.op!="string"||!Ps.includes(r.op))return`${t}: operation.op must be one of ${Ps.join(", ")}.`;for(let o of["range","at","a","b","seed"]){let s=r[o];if(typeof s=="number"&&!Number.isSafeInteger(s))return`${t}: operation.${o} is a 64-bit number and lost its last digits as a JSON number. Pass it as a string, exactly as cluster_status or cluster_advice wrote it.`}return r}function vs(e){let t=typeof e.id=="string"&&/^\d+$/.test(e.id)?Number(e.id):e.id;return typeof t=="number"&&Number.isSafeInteger(t)&&t>0?t:null}function ry(e){let t=e instanceof Error?e.message:String(e),n=e?.details?.kind;return typeof n=="string"?{error:t,kind:n}:t}var oy="The cluster tools need a cluster: pass project (a SnoutData Cloud project's ref), or start this server with LEPIS_ADMIN_URL (a standalone router's admin API, e.g. http://127.0.0.1:7432) and LEPIS_ADMIN_TOKEN set. Tell the user that is what is missing.";async function sy(e,t,n,r,o){let s,a=typeof n.project=="string"&&n.project.trim()!==""?n.project.trim():void 0;try{if(!r.shards)return R(e,oy,!0);s=r.shards(a)}catch(u){return R(e,u instanceof Error?u.message:String(u),!0)}let c=o.shardsPlans??Yh,i=a??"";try{switch(t){case"cluster_status":return R(e,await s.status());case"cluster_advice":{let u=typeof n.sampleMs=="number"&&n.sampleMs>=0?Math.min(Math.floor(n.sampleMs),3e4):void 0,l=await s.advice(u);for(let m of l.advice)m.plan&&c.remember(m.request,i);return R(e,{summary:l.summary,advice:l.advice.map(m=>({op:m.op,reason:m.reason,metric:m.metric,operation:m.request,...m.plan?{plan:bn(m.plan)}:{},...m.needs.length>0?{needs:m.needs,note:`Ask the user for ${m.needs.join(", ")}, then plan it with cluster_plan.`}:{},...m.plan_error?{plan_error:m.plan_error}:{}})),facts:l.facts,assumptions:l.assumptions,...l.advice.length>0?{next:"Nothing has run. Show the user a recommendation and its plan; if they agree, call cluster_run with its operation and confirm: true. Ask for advice again after each one runs."}:{}})}case"cluster_plan":{let u=wc(n,t);if(typeof u=="string")return R(e,u,!0);let l=await s.plan(u);return c.remember(u,i),R(e,{plan:bn(l),next:"Nothing has run. To run it, show the user this plan, then call cluster_run with the same operation and confirm: true."})}case"cluster_run":{let u=wc(n,t);if(typeof u=="string")return R(e,u,!0);if(n.confirm!==!0||!c.shown(u,i)){let m=await s.plan(u);c.remember(u,i);let h=n.confirm===!0;return R(e,{ran:!1,plan:bn(m),next:h?"NOT RUN: confirm applies to an operation planned here in the last 15 minutes, and this one was not, so it has been planned now instead. Show the user this plan; if they agree, call cluster_run again with the same operation and confirm: true.":"Nothing has run. Show the user this plan; if they agree, call cluster_run again with the same operation and confirm: true."},h)}let l=await s.submit(u);return c.forget(u,i),l.job===void 0?R(e,{ran:!0,done:l.done,answer:l.answer}):R(e,{ran:!0,job:l.job,plan:bn(l.plan),next:`Follow it with cluster_watch_job {"id": ${l.job}}.`})}case"cluster_jobs":{if(n.id===void 0)return R(e,{jobs:await s.jobs()});let u=vs(n);if(u===null)return R(e,"cluster_jobs: id is a job number. Call it without id to list them.",!0);let l=await s.job(u);return R(e,{...ks(l),plan:l.plan&&Array.isArray(l.plan.steps)?bn(l.plan):null})}case"cluster_watch_job":{let u=vs(n);if(u===null)return R(e,"cluster_watch_job needs id, the job number cluster_run returned.",!0);let l=typeof n.timeoutSeconds=="number"&&n.timeoutSeconds>0?Math.min(n.timeoutSeconds,120):30,m=Date.now()+l*1e3;for(;;){let h=await s.job(u);if(yn(h.state))return R(e,ks(h),h.state!=="done");if(Date.now()>=m)return R(e,{...ks(h),note:`Still ${h.state} after ${l}s. It carries on; call cluster_watch_job again to keep waiting.`});await new Promise(P=>setTimeout(P,o.shardsPollMs??1e3))}}case"cluster_cancel_job":case"cluster_resume_job":{let u=vs(n);if(u===null)return R(e,`${t} needs id, a job number.`,!0);let l=t==="cluster_cancel_job"?await s.cancel(u):await s.resume(u);return R(e,{...l,next:`Follow it with cluster_watch_job {"id": ${u}}.`})}default:return xr(e,-32602,`unknown tool: ${t}`)}}catch(u){return R(e,ry(u),!0)}}function te(e,t){let n=e[t];return typeof n=="string"&&n.trim().length>0?n.trim():null}async function bc(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 _n(r,{protocolVersion:Xh,capabilities:{tools:{listChanged:!1}},serverInfo:{name:"snoutdata",version:n.version}});case"ping":return _n(r,{});case"tools/list":return _n(r,{tools:ty([...kn.map(ey),...n.borrowed??[]])});case"tools/call":return ay(r,s,t,n);default:return xr(r,-32601,`unknown method: ${o}`)}}async function ay(e,t,n,r){let o=typeof t.name=="string"?t.name:"",s=t.arguments??{};if(!(kn.some(l=>l.name===o)||(r.borrowed?.some(l=>l.name===o)??!1)))return xr(e,-32602,`unknown tool: ${o}`);let c=["get_connection_url","pause_project","resume_project","delete_project","push_migrations","usage","export_status","start_export","reset_password","get_project","list_products","set_product","list_domains","add_domain","verify_domain","remove_domain","restore_window","restore_to_point"];if(o.startsWith("cluster_")&&kn.some(l=>l.name===o))return sy(e,o,s,n,r);let i=te(s,"ref");if(c.includes(o)&&!i)return R(e,`${o} needs a project ref. Call list_projects to see them.`,!0);let u=i&&n.localProject?n.localProject(i):null;if(u&&!yc.includes(o))return R(e,`${i} is the self-hosted project in ${u.folder}, and ${o} is about SnoutData Cloud. On a local project these tools work: ${yc.join(", ")}.`,!0);try{switch(o){case"whoami":return R(e,await n.whoami());case"list_projects":return R(e,xs(await n.listProjects()));case"create_project":{let l=te(s,"name");return l?R(e,await n.createProject({name:l,region:te(s,"region")??void 0,teamId:te(s,"teamId")??void 0})):R(e,"create_project needs a name.",!0)}case"get_connection_url":{let l=await n.connection(i);return R(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 R(e,await n.setProjectState("pause",i));case"resume_project":return R(e,await n.setProjectState("resume",i));case"usage":{let l=typeof s.days=="number"?s.days:void 0;return R(e,await n.usage(i,l))}case"export_status":return R(e,await n.exportStatus(i));case"start_export":return R(e,await n.requestExport(i));case"reset_password":{let l=await n.resetPassword(i),m=await n.connection(i);return R(e,{url:m.uri,appliesIn:l.appliesIn??"within about fifteen seconds",next:"Wait about fifteen seconds before connecting with this URL, and retry a refusal for up to two minutes: until the change applies (usually 5 to 15 seconds, longer while the host is busy) the new password is refused and the old one still works, which does not mean the new one is wrong. Then update DATABASE_URL wherever it is set.",warning:"This URL contains a live database password. Put it in an environment variable, not in a file that is committed."})}case"list_teams":return R(e,await n.listTeams());case"get_project":return R(e,xs(await n.getProject(i)));case"list_products":return R(e,await n.getProducts(i));case"set_product":{let l=te(s,"product");return l!=="auth"&&l!=="storage"&&l!=="data-api"?R(e,"set_product needs product: auth, storage or data-api.",!0):typeof s.enabled!="boolean"?R(e,"set_product needs enabled: true or false.",!0):R(e,await n.setProduct(i,l,s.enabled))}case"list_domains":return R(e,await n.listDomains(i));case"add_domain":case"verify_domain":case"remove_domain":{let l=te(s,"hostname");return l?R(e,await n.domainAction(i,o.split("_")[0],l)):R(e,`${o} needs a hostname, e.g. api.example.com.`,!0)}case"restore_window":{let l=await n.restoreWindow(i),m=l&&l.restore?Es(i,l.restore):null;return R(e,m?{...l,reason:m}:l)}case"restore_to_point":{let l=te(s,"at");return l?R(e,await n.restoreTo(i,l,te(s,"name")??void 0)):R(e,"restore_to_point needs at: an ISO 8601 moment inside restore_window.",!0)}case"deploy_function":{let l=te(s,"ref"),m=te(s,"name");return!l||!m?R(e,"deploy_function needs a project ref and a function name.",!0):R(e,await n.deployFunction(l,m,{dir:te(s,"dir")??void 0,entrypoint:te(s,"entrypoint")??void 0,verifyJwt:s&&typeof s=="object"&&"openToAnyone"in s?s.openToAnyone!==!0:!0}))}case"list_functions":{let l=te(s,"ref");return l?R(e,await n.listFunctions(l)):R(e,"list_functions needs a project ref.",!0)}case"size_function":{let l=te(s,"ref"),m=te(s,"name");if(!l||!m)return R(e,"size_function needs a project ref and a function name.",!0);let h=typeof s.memoryMb=="number"?s.memoryMb:void 0,P=typeof s.concurrency=="number"?s.concurrency:void 0,A=s.reset===!0;return!A&&h===void 0&&P===void 0?R(e,"size_function needs memoryMb, concurrency, or reset: true.",!0):R(e,await n.sizeFunction(l,m,{memoryMb:h,concurrency:P,reset:A}))}case"delete_function":{let l=te(s,"ref"),m=te(s,"name");return!l||!m?R(e,"delete_function needs a project ref and a function name.",!0):R(e,await n.deleteFunction(l,m))}case"list_containers":{let l=te(s,"ref");return l?R(e,await n.listContainers(l)):R(e,"list_containers needs a project ref.",!0)}case"deploy_container":{let l=te(s,"ref"),m=te(s,"name"),h=te(s,"image");if(!l||!m||!h)return R(e,"deploy_container needs a project ref, a container name and an image.",!0);let P=typeof s.port=="number"?s.port:void 0;return R(e,await n.deployContainer(l,m,{image:h,...P===void 0?{}:{port:P}}))}case"restart_container":case"delete_container":{let l=te(s,"ref"),m=te(s,"name");return!l||!m?R(e,`${o} needs a project ref and a container name.`,!0):R(e,o==="restart_container"?await n.restartContainer(l,m):await n.deleteContainer(l,m))}case"container_logs":{let l=te(s,"ref");if(!l)return R(e,"container_logs needs a project ref.",!0);let m=typeof s.limit=="number"?s.limit:void 0;return R(e,await n.containerLogs(l,{name:te(s,"name")??void 0,errorsOnly:s.errorsOnly===!0,limit:m}))}case"container_egress":{let l=te(s,"ref");return l?R(e,await n.containerEgress(l)):R(e,"container_egress needs a project ref.",!0)}case"set_container_egress":{let l=te(s,"ref");if(!l)return R(e,"set_container_egress needs a project ref.",!0);let m=s.clear===!0,h=s.allow;return h!==void 0&&(!Array.isArray(h)||!h.every(P=>typeof P=="string"))?R(e,'set_container_egress takes allow as a list of destinations, such as ["api.example.com:443"].',!0):m===(h!==void 0)?R(e,"set_container_egress takes either allow (a list of destinations; [] for nothing outside the project) or clear: true (the public internet again), and exactly one of them.",!0):R(e,await n.setContainerEgress(l,m?null:h))}case"list_function_secrets":{let l=te(s,"ref");return l?R(e,await n.listFunctionSecrets(l)):R(e,"list_function_secrets needs a project ref.",!0)}case"function_logs":{let l=te(s,"ref");if(!l)return R(e,"function_logs needs a project ref.",!0);let m=typeof s.limit=="number"?s.limit:void 0;return R(e,await n.functionLogs(l,{name:te(s,"name")??void 0,errorsOnly:s.errorsOnly===!0,limit:m}))}case"list_tokens":return R(e,await n.listTokens());case"create_token":{let l=te(s,"name");if(!l)return R(e,"create_token needs a name, so somebody can tell later what it was for.",!0);let m=te(s,"expires"),h=m?ny(m):void 0;return h===null?R(e,`create_token: \`expires\` must be a date in the future, like 2026-12-31. Got "${m}".`,!0):R(e,await n.createToken(l,h,te(s,"project")??void 0))}case"revoke_token":{let l=te(s,"id");return l?R(e,await n.revokeToken(l)):R(e,"revoke_token needs the id of a token. list_tokens has them.",!0)}case"push_migrations":return R(e,await n.pushMigrations(i,{dir:te(s,"dir")??void 0,dryRun:s.dryRun===!0}));case"delete_project":return r.allowDelete?R(e,await n.setProjectState("delete",i)):R(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?_n(e,await n.callBorrowed(o,s)):R(e,`${o} needs SnoutData Studio, which is no longer answering.`,!0):xr(e,-32602,`unknown tool: ${o}`)}}catch(l){return R(e,l instanceof Error?l.message:String(l),!0)}}function Sc(e){let t=e.split(`
|
|
496
|
+
`),n=t.pop()??"";return{messages:t.map(r=>r.trim()).filter(r=>r.length>0),rest:n}}var Er=["auth","storage","data-api","push"];function At(e){return{error:e instanceof Error?e.message:String(e)}}async function qt(e){let[t,n,r,o]=await Promise.all([x("cloud-project-auth",{ref:e,action:"status"}).then(s=>({enabled:s.enabled===!0,google:s.google?.enabled===!0,saml:s.saml?.enabled===!0}),At),x("cloud-project-storage",{ref:e}).then(s=>({enabled:s.storage?.enabled===!0,pending:s.storage?.pending===!0,bytes:s.storage?.bytes??null,files:s.storage?.files??null}),At),x("cloud-project-data-api",{ref:e}).then(s=>({enabled:s.dataApi?.enabled===!0,allowedOnPlan:s.dataApi?.allowed===!0}),At),x("cloud-project-push",{ref:e}).then(s=>({enabled:s.push?.enabled===!0,scheduling:s.push?.scheduling===!0}),At)]);return{ref:e,auth:t,storage:n,dataApi:r,push:o,realtime:iy}}var iy={enabled:!0,broadcast:!0,presence:!0,tableChanges:"Plus and Pro plans"};function kc(e){let t=(e??"").toLowerCase().replace("_","-").replace(/^dataapi$/,"data-api");if(!Er.includes(t))throw new g(`say which product: ${Er.join(", ")}`);return t}async function As(e,t,n){if(t==="auth"){let r=await x("cloud-project-auth",{ref:e,action:n?"enable":"disable"});return{ref:e,product:t,enabled:n,note:r.note??(n?"Auth starts within a minute.":"Auth stops. Your users stay in the auth schema.")}}return t==="push"?(await x("cloud-project-push",{ref:e,enable:n}),{ref:e,product:t,enabled:n,note:n?"Push starts within a minute. It runs beside the database, so the database restarts once. Set keys with: snoutdata push credentials":"Push is off. Devices, the log and the keys stay in the push schema."}):(await x(t==="storage"?"cloud-project-storage":"cloud-project-data-api",{ref:e,enable:n}),{ref:e,product:t,enabled:n,note:n?`${t} starts within a minute, when the host picks up the change.`:`${t} is off.`})}async function vc(e){let t=await qt(e);w(t,()=>{let n=(c,i,u="")=>[c,i&&typeof i=="object"&&"error"in i?`could not read: ${i.error}`:i.enabled?O("on"):"off",u],r=t.storage,o=t.dataApi;process.stdout.write(`${W([["PRODUCT","STATE",""],n("auth",t.auth),n("storage",r,"pending"in r&&r.pending?"waiting for the host":""),n("data-api",o,"allowedOnPlan"in o&&!o.allowedOnPlan?"not on this plan":""),n("push",t.push,"scheduling"in t.push&&t.push.enabled&&!t.push.scheduling?"scheduled sends on paid plans":""),n("realtime",t.realtime,"always on: signals, roster; table changes on Plus and Pro")])}
|
|
497
|
+
`);let s=c=>!!(c&&typeof c=="object"&&"enabled"in c&&!c.enabled),a=["auth","storage","data-api","push"].find(c=>s(c==="data-api"?o:t[c]));a&&d(b(` snoutdata products enable ${a} --ref ${e}`))})}async function $c(e,t,n){let r=await As(e,t,n);w(r,()=>d(r.note))}async function Ar(e){let t=await x("cloud-project-domain",{ref:e,action:"list"});return{ref:e,domains:t.domains??[]}}async function Rs(e,t,n){if(!n)throw new g(`domains ${t} needs a hostname, e.g. api.example.com`);let r=await x("cloud-project-domain",{ref:e,action:t,hostname:n});return{ref:e,domain:r.domain??null,next:r.next??null}}function _c(e,t){let n=[["TYPE","NAME","VALUE"]];e.record&&n.push([e.record.type,e.record.name,e.record.value]),e.certificateRecord&&n.push([e.certificateRecord.type,e.certificateRecord.name,e.certificateRecord.value]),n.push(["CNAME",e.hostname,`${t}.api.snoutdata.com`]),process.stdout.write(`${W(n)}
|
|
498
|
+
`)}async function Pc(e,t,n){if(t===void 0||t==="list"){let o=await Ar(e);w(o,()=>{if(o.domains.length===0){d(`No domains on ${e}. \`snoutdata domains add api.example.com\`.`);return}process.stdout.write(`${W([["HOSTNAME","VERIFIED","CERTIFICATE"],...o.domains.map(s=>[s.hostname,s.verified?"yes":O("no"),s.certificate?.state??""])])}
|
|
499
|
+
`)});return}if(t!=="add"&&t!=="verify"&&t!=="remove")throw new g(`unknown command: domains ${t}`);let r=await Rs(e,t,n??"");t==="verify"&&r.domain&&!r.domain.verified&&(ir()||_c(r.domain,e),V("not-ready",r.domain.verificationError??`${n} is not verified yet. Publish the records above, then run this again.`,{ref:e,domain:r.domain})),w(r,()=>{if(t==="remove"){d(`${n} removed.`);return}r.domain&&(d(r.domain.verified?`${n} is verified.`:r.domain.verificationError??`${n} is not verified yet. Publish these records, then \`snoutdata domains verify ${n}\`:`),r.domain.verified||_c(r.domain,e)),r.next&&d(r.next)})}async function Ts(e){return x("cloud-project-restore",{ref:e},"GET")}async function Os(e,t,n){if(Number.isNaN(Date.parse(t)))throw new g(`--at needs a moment, e.g. 2026-09-20T14:30:00Z (got "${t}")`);return x("cloud-project-restore",{ref:e,at:new Date(t).toISOString(),...n?{name:n}:{}})}async function xc(e){let t=await Ts(e);w(t,()=>{let n=t.restore,r=Es(e,n);if(r){d(r);return}d(`${e} can be restored to any moment from ${n.earliest} to ${n.latest}.`),d(b(` snoutdata db restore --at ${n.latest} --ref ${e}`))})}async function Ec(e,t,n){let r=await Os(e,t,n);w(r,()=>{d(`Restoring ${e} as it was at ${r.targetAt} into a new project, ${O(r.project.name)} (${r.project.ref}).`),d(b(" snoutdata projects list to see when it is ready"))})}async function Ns(e){let{projects:t}=await je(),n=t.find(c=>c.ref===e);if(!n)throw new g(`no project ${e} on this account. \`snoutdata projects list\`.`);let[r,o,s,a]=await Promise.all([qt(e),x("cloud-project-functions",{ref:e}).then(c=>(c.functions??[]).map(i=>i.name),At),x("cloud-project-secrets",{ref:e}).then(c=>(c.secrets??[]).map(i=>i.name),At),Ar(e).then(c=>c.domains.map(i=>({hostname:i.hostname,verified:i.verified})),At)]);return{...n,products:r,functions:o,secretNames:s,domains:a}}async function Ac(e){let t=await Ns(e);w(t,()=>{let n=t,r=s=>s&&typeof s=="object"&&"enabled"in s?s.enabled?"on":"off":"?",o=s=>Array.isArray(s)?s.length?s.map(a=>typeof a=="string"?a:a.hostname).join(", "):"none":"could not read";process.stdout.write(`${W([["name",n.name],["ref",n.ref],["state",n.state],["region",n.region],...typeof n.postgresVersion=="number"?[["postgres",String(n.postgresVersion)]]:[],["auth / storage / data-api / push",`${r(n.products.auth)} / ${r(n.products.storage)} / ${r(n.products.dataApi)} / ${r(n.products.push)}`],["realtime","on (signals and the roster; table changes on Plus and Pro)"],["functions",o(n.functions)],["secrets",o(n.secretNames)],["domains",o(n.domains)]])}
|
|
500
|
+
`)})}me();Se();Pt();me();ue();import{existsSync as ly,mkdirSync as cy,readFileSync as dy,renameSync as uy,writeFileSync as py}from"node:fs";import{homedir as Is}from"node:os";import{dirname as my,join as fy}from"node:path";import{rootCertificates as gy}from"node:tls";var hy="SNOUTDATA_DB_SSLMODE";function yy(e=Is()){let t=fy(e,".snoutdata","ca-roots.pem"),n=`${gy.join(`
|
|
476
501
|
`)}
|
|
477
|
-
`,r=null;try{r=
|
|
478
|
-
`)})}async function
|
|
479
|
-
`,{mode:384}),{path:r,replaced:!1,ignored:s};process.platform!=="win32"&&
|
|
502
|
+
`,r=null;try{r=ly(t)?dy(t,"utf8"):null}catch{r=null}if(r!==n){cy(my(t),{recursive:!0,mode:448});let o=`${t}.${process.pid}.tmp`;py(o,n,{mode:420}),uy(o,t)}return t}function vn(e,t=process.env,n=Is()){if(e==="disable")return{mode:"disable"};let r=t[hy]?.trim();return r?{mode:r}:{mode:"verify-full",rootCert:t.PGSSLROOTCERT||yy(n)}}function $n(e,t=process.env,n=Is()){let{mode:r,rootCert:o}=vn(e,t,n);return o?{PGSSLMODE:r,PGSSLROOTCERT:o}:{PGSSLMODE:r}}async function Cc(e){try{let{projects:t}=await je(),n=t.find(r=>r.ref===e);n?.readOnly?ie(`${e} is read-only: it is over the storage limit on your plan. Reading works; writing does not.`):n?.readOnlyPending&&ie(`${e} is about to become read-only: it has grown past the storage limit on your plan.`)}catch{}}async function Ds(e){await Cc(e);let t=await Ce(e);w(t,()=>{process.stdout.write(`${t.uri}
|
|
503
|
+
`)})}async function Dc(e,t){if(t.length===0&&!ve()){let o=Ut();V("usage",`db psql with no statements opens an interactive session, and ${o?Lt(o):"there is nobody to use it"}. Pass statements after --, or use \`snoutdata db url\` and your own client.`)}await Cc(e);let n=await Ce(e);n.wakesInstantly||d("This project is paused; the connection will wake it, which takes a few seconds.");let r=Pn("psql",["--host",n.host,"--port",String(n.port),"--username",n.user,"--dbname",n.database,...t],{stdio:"inherit",env:{...process.env,PGPASSWORD:n.password,...$n(n.ssl)}});return new Promise((o,s)=>{r.on("error",a=>{if(a.code==="ENOENT"){s(new T("tool-missing","psql is not installed. `snoutdata db url` prints a connection string for any client."));return}s(new T("failed",String(a)))}),r.on("close",a=>o(a??0))})}function Mc(e,t,n="DATABASE_URL"){let r=Or(e,".env"),o=`${n}=${t}`,s=ky(e);if(!Tr(r))return Wt(r,`${o}
|
|
504
|
+
`,{mode:384}),{path:r,replaced:!1,ignored:s};process.platform!=="win32"&&wy(r).mode&63&&ie(`${r} can be read by other users on this machine, and it holds a database password. \`chmod 600 ${r}\` closes it.`);let a=Nc(r,"utf8"),c=new RegExp(`^${n}=.*$`,"m");return c.test(a)?(Wt(r,a.replace(c,o)),{path:r,replaced:!0,ignored:s}):(Oc(r,a.endsWith(`
|
|
480
505
|
`)?`${o}
|
|
481
506
|
`:`
|
|
482
507
|
${o}
|
|
483
|
-
`),{path:r,replaced:!1,ignored:s})}function
|
|
508
|
+
`),{path:r,replaced:!1,ignored:s})}function ky(e){let t=e;for(;!Tr(Or(t,".git"));){let s=Sy(t);if(s===t)return!1;t=s}let n=Or(e,".gitignore"),r=Tr(n)?Nc(n,"utf8"):"";return r.split(/\r?\n/).some(s=>/^\/?(\.env|\.env\*|\*\.env|\.env\.\*)$/.test(s.trim()))?!1:(Oc(n,r===""||r.endsWith(`
|
|
484
509
|
`)?`.env
|
|
485
510
|
`:`
|
|
486
511
|
.env
|
|
487
|
-
`),!0)}async function
|
|
512
|
+
`),!0)}async function jc(e,t={}){if(t.statusOnly){let{export:a}=await Bt(e);return w({ref:e,export:a},()=>Cs(a)),0}let n=await pn(e);if(!n.export.pending&&n.export.url&&!t.out)return w({ref:e,export:n.export},()=>Cs(n.export)),0;d("Taking a copy of the database. This runs on the server and can take a few minutes.");let r=await $y(e,t.timeoutMs);if(r.error&&V("failed",`The export did not finish: ${r.error}`,{ref:e}),!t.out)return w({ref:e,export:r},()=>Cs(r)),0;r.url||V("failed","The copy was taken, but no download link could be signed. Run this again to get one.",{ref:e});let o=await Py(r.url,t.out),s=r.rolesSql?vy(t.out):null;return s&&r.rolesSql&&Wt(s,r.rolesSql.endsWith(`
|
|
488
513
|
`)?r.rolesSql:`${r.rolesSql}
|
|
489
|
-
`,"utf8"),
|
|
490
|
-
`),e.urlExpiresAt&&d(
|
|
491
|
-
${
|
|
492
|
-
`)}`,{ref:e,file:n,refusal:"products-off",products:
|
|
493
|
-
`).map(
|
|
494
|
-
${i.map(
|
|
514
|
+
`,"utf8"),w({ref:e,path:t.out,bytes:o,rolesPath:s},()=>{d(`Saved ${at(o)} to ${t.out}.`),d(b(`Into a SnoutData project: snoutdata db restore --file ${t.out} --ref <ref>`)),s?(d(`Saved the roles it needs to ${s}.`),d(b("Into any other Postgres, in this order:")),d(b(` psql --dbname <your database> -f ${s}`)),d(b(` pg_restore --dbname <your database> ${t.out}`)),d(b("The roles are created without passwords. Set them yourself if anything signs in as one."))):(d(b("Restore it with: pg_restore --no-owner --no-privileges --dbname <your database> "+t.out)),d(b("Owners and grants are skipped: this export carries no roles file.")))}),0}function vy(e){return e.replace(/\.[^./\\]*$/,"")+".roles.sql"}async function $y(e,t=30*6e4){let n=Date.now()+t;for(;;){let{export:r}=await Bt(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 Py(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 Ic(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 Cs(e){if(e.error){ie(`The last export did not finish: ${e.error}`);return}if(e.pending){d("An export is running. Ask again in a minute, or run `snoutdata db export`, which waits for it.");return}if(!e.completedAt){d("This project has never been exported.");return}d(`A copy taken ${ot(e.completedAt)}, ${at(e.bytes??0)}.`),e.url?(process.stdout.write(`${e.url}
|
|
515
|
+
`),e.urlExpiresAt&&d(b(`That link stops working ${ot(e.urlExpiresAt)}.`))):d(b("The link has expired. Run the export again to sign a new one; the copy is kept."))}function at(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 Lc(e,t){let n=t.file;n||V("usage","db restore needs --file, pointing at a dump. `snoutdata db export` writes one."),Tr(n)||V("usage",`no file at ${n}`,{file:n});let r=await Ic(n,"r"),o=new Uint8Array(512),s=await r.read(o,0,512,0);await r.close();let a=lc(o.slice(0,s.bytesRead)),c=await Ce(e);c.wakesInstantly||d("This project is paused; the connection will wake it, which takes a few seconds.");let{projects:i}=await je(),u=i.find(ee=>ee.ref===e),l=await xy(c);l===null&&V("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=cc(a,{existingTables:l,readOnly:u?.readOnly===!0},{force:t.force});m.ok||V("conflict",m.reason,{ref:e,file:n,refusal:m.code});let h=null;if(m.tool==="pg_restore"){let ee=await Tc("pg_restore",["--list",n]),z=await Tc("pg_restore",["--section=post-data","--file=-",n]);if((ee.code!==0||z.code!==0)&&V("failed",`pg_restore could not read ${n}: ${(ee.stderr||z.stderr).trim().slice(0,300)}`,{ref:e,file:n}),h=uc(ee.stdout,z.stdout),h.products.length>0){let le=await qt(e),C=j=>!!(j&&typeof j=="object"&&"enabled"in j&&j.enabled),ce=h.products.filter(j=>!C(le[j]));ce.length>0&&V("conflict",`this dump holds ${ce.length>1?`${ce.slice(0,-1).join(", ")} and ${ce[ce.length-1]}`:ce[0]} data, and ${e} has ${ce.length===1?"it":"them"} off. Their tables are the platform's, so the product has to be on to take the rows. Switch ${ce.length===1?"it":"them"} on, wait a minute, and run this again:
|
|
516
|
+
${ce.map(j=>` snoutdata products enable ${j} --ref ${e}`).join(`
|
|
517
|
+
`)}`,{ref:e,file:n,refusal:"products-off",products:ce})}}if(h&&h.platformData.length>0){let ee=h.platformData.flatMap(({tables:C})=>C),z=await Ey(c,ee);z===null&&V("failed",`could not check ${e} for the tables this dump loads into.`,{ref:e});let le=h.platformData.filter(({tables:C})=>C.some(ce=>z.includes(ce))).map(({product:C})=>C);le.length>0&&V("conflict",`${le.join(", ")} on ${e} ${le.length===1?"has":"have"} not finished setting up ${le.length===1?"its":"their"} tables yet. Run this again in a minute.`,{ref:e,file:n,refusal:"products-starting",products:le})}d(`Restoring ${n} into ${e} with ${m.tool} (${m.format} format).`);let P=h?await Ay(c,n,h):await Rr(c,m.tool,n),A=dc(P.stderr);for(let ee of h?.notes??[])d(b(ee));return P.code!==0&&A.fatal.length===0&&A.total>0?(w({ref:e,file:n,ok:!0,tool:m.tool,format:m.format,ignoredErrors:A.harmless},()=>{d(`Restored into ${e}.`),d(`${A.harmless} error${A.harmless===1?"":"s"} above ${A.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):(P.code!==0&&V("failed",`${m.tool} exited ${P.code}. Nothing here retried it: a half-finished restore is undone by deleting the project and making another.`,{ref:e,file:n,errors:A.fatal}),w({ref:e,file:n,ok:!0,tool:m.tool,format:m.format},()=>d(`Restored into ${e}.`)),0)}async function xy(e){return new Promise(t=>{let n=Pn("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') and pg_get_userbyid(c.relowner) !~ '^(snoutpod_admin|snout_[a-z]+_admin)$'"],{stdio:["ignore","pipe","pipe"],env:{...process.env,PGPASSWORD:e.password,...$n(e.ssl)}}),r="";n.stdout.on("data",o=>r+=String(o)),n.on("error",o=>{ie(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 Ey(e,t){if(t.length===0)return Promise.resolve([]);let n=t.map(r=>`'${r.replace(/'/g,"''")}'`).join(", ");return new Promise(r=>{let o=Pn("psql",["--host",e.host,"--port",String(e.port),"--username",e.user,"--dbname",e.database,"--no-psqlrc","-tAc",`select t from unnest(array[${n}]) t where to_regclass(t) is null`],{stdio:["ignore","pipe","pipe"],env:{...process.env,PGPASSWORD:e.password,...$n(e.ssl)}}),s="";o.stdout.on("data",a=>s+=String(a)),o.on("error",()=>r(null)),o.on("close",a=>r(a===0?s.split(`
|
|
518
|
+
`).map(c=>c.trim()).filter(Boolean):null))})}function Tc(e,t){return new Promise(n=>{let r=Pn(e,t,{stdio:["ignore","pipe","pipe"]}),o="",s="";r.stdout.on("data",a=>o+=String(a)),r.stderr.on("data",a=>s+=String(a)),r.on("error",a=>{ie(a.code==="ENOENT"?`${e} is not installed, and this dump needs it.`:String(a)),n({code:127,stdout:o,stderr:s})}),r.on("close",a=>n({code:a??0,stdout:o,stderr:s}))})}async function Ay(e,t,n){let r=Or(_y(),`snoutdata-restore-${process.pid}-${Date.now()}`),o=0,s="";try{for(let{product:c,clear:i,lines:u}of n.platformData){if(i.length>0){Wt(`${r}.sql`,`set role ${bs[c]};
|
|
519
|
+
${i.map(h=>`delete from ${h};`).join(`
|
|
495
520
|
`)}
|
|
496
|
-
`,"utf8");let
|
|
521
|
+
`,"utf8");let m=await Rr(e,"psql",`${r}.sql`,["--quiet"]);o=o||m.code,s+=m.stderr}Wt(`${r}.platform`,`${u.join(`
|
|
497
522
|
`)}
|
|
498
|
-
`,"utf8");let
|
|
523
|
+
`,"utf8");let l=await Rr(e,"pg_restore",t,["--data-only",`--role=${bs[c]}`,`--use-list=${r}.platform`]);o=o||l.code,s+=l.stderr}Wt(`${r}.rest`,`${n.rest.join(`
|
|
499
524
|
`)}
|
|
500
|
-
`,"utf8");let a=await
|
|
501
|
-
`);let t=e.access.find(n=>n.you);t?.connection&&(d(""),d("You sign in with (psql 18; a browser approves it):"),d(
|
|
502
|
-
`),d("");let t=e.outcome==="unchanged"?"already has":"now has";d(`${e.email} ${t} ${
|
|
525
|
+
`,"utf8");let a=await Rr(e,"pg_restore",t,[`--use-list=${r}.rest`]);return{code:o||a.code,stderr:s+a.stderr}}finally{for(let a of[".platform",".rest",".sql"])try{by(`${r}${a}`)}catch{}}}function Rr(e,t,n,r=[]){let o=["--host",e.host,"--port",String(e.port),"--username",e.user,"--dbname",e.database],s=t==="psql"?[...o,"--no-psqlrc","-v","ON_ERROR_STOP=1",...r,"-f",n]:[...o,"--no-owner","--no-privileges",...r,n];return new Promise(a=>{let c=Pn(t,s,{stdio:["ignore","inherit","pipe"],env:{...process.env,PGPASSWORD:e.password,...$n(e.ssl)}}),i="";c.stderr?.on("data",u=>{let l=String(u);i+=l,process.stderr.write(l)}),c.on("error",u=>{ie(u.code==="ENOENT"?`${t} is not installed, and this dump needs it.`:String(u)),a({code:127,stderr:i})}),c.on("close",u=>a({code:u??0,stderr:i}))})}me();Ae();ue();function Ty(e,t){let[n="list",r,...o]=e;if(o.length>0)throw new g(`db access ${n} takes one argument, not ${1+o.length}`);switch(n){case"list":if(r!==void 0)throw new g("db access list takes no argument");if(t!==void 0)throw new g("--level is for db access grant");return{action:"list"};case"grant":{if(!r||!r.includes("@"))throw new g("db access grant needs an email address: db access grant you@example.com [--level full|read]");let s=t??"read";if(s!=="full"&&s!=="read")throw new g(`--level is full or read, not ${JSON.stringify(s)}`);return{action:"grant",email:r,level:s}}case"revoke":if(!r)throw new g("db access revoke needs an email address or a role name");if(t!==void 0)throw new g("--level is for db access grant");return{action:"revoke",who:r};default:throw new g(`unknown command: db access ${n}. db access list|grant|revoke`)}}async function Uc(e,t,n){let r=Ty(t,n.level);if(r.action==="list"){let s=await x("cloud-project-db-access",{ref:e,action:"list"});w(s,()=>Oy(s));return}if(r.action==="grant"){let s=await x("cloud-project-db-access",{ref:e,action:"grant",email:r.email,level:r.level});w(s,()=>Ny(s));return}let o=await x("cloud-project-db-access",{ref:e,action:"revoke",who:r.who});w(o,()=>Cy(o))}function Oy(e){if(e.note&&d(e.note),e.access.length===0){d(`Nobody signs in to ${e.ref}'s database as themselves yet. \`snoutdata db access grant you@example.com --level full\`.`);return}process.stdout.write(`${W([["EMAIL","LEVEL","ROLE"],...e.access.map(n=>[n.you?O(n.email):n.email,n.level,n.role])])}
|
|
526
|
+
`);let t=e.access.find(n=>n.you);t?.connection&&(d(""),d("You sign in with (psql 18; a browser approves it):"),d(b(` psql "${t.connection.keywords}"`)))}function Ny(e){e.connection&&process.stdout.write(`${e.connection.keywords}
|
|
527
|
+
`),d("");let t=e.outcome==="unchanged"?"already has":"now has";d(`${e.email} ${t} ${O(e.level)} access to ${e.ref}'s database, as ${e.role}${e.means?`: ${e.means}`:""}.`),e.outcome!=="unchanged"&&d(e.appliesIn),d("They sign in with psql 18 (libpq 18 with OAuth support) and approve the code it prints in a browser:"),e.connection&&d(b(` psql "${e.connection.keywords}"`))}function Iy(e){return[...e.revoked.map(t=>`Took away ${t.email}'s ${t.level} access to ${e.ref}'s database (${t.role}).`),e.note]}function Cy(e){let t=Iy(e);for(let n of t.slice(0,-1))d(n);ie(t[t.length-1])}me();Ae();ue();var Dy=6e4;async function Ms(e,t={}){return x("cloud-project-realtime",{ref:e,...t.channel?{channel:t.channel}:{},...t.since!==void 0?{since:t.since}:{}})}function My(e,t=Date.now()){let n=/^(\d+)\s*(s|m|h|d)$/i.exec(e.trim());if(n){let o={s:1e3,m:6e4,h:36e5,d:864e5}[n[2].toLowerCase()];return t-Number(n[1])*o}let r=Date.parse(e);if(Number.isFinite(r))return r;throw new g(`--since wants a duration like 10m, 2h or 1d, or a time like 2026-10-03T14:00, not "${e}"`)}function jy(e){let t=new Date(e),n=r=>String(r).padStart(2,"0");return`${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`}function Fc(e){if(e===null)return"-";let t=Math.round(e/1e3);if(t<60)return`${t}s ago`;let n=Math.round(t/60);return n<60?`${n}m ago`:`${Math.round(n/60)}h ago`}var Ly={connect:"connected",connect_refused:"refused at connect",join:"joined",join_refused:"join refused",leave:"left",channel_closed:"channel closed by the server",disconnect:"disconnected",handler_failed:"handler failed"};function Bc(e){let t=[e.presence_key?e.presence_key:null,e.socket!==void 0?`socket ${e.socket}`:null].filter(Boolean).join(", "),n=[jy(e.at),Ly[e.kind]??e.kind];return e.channel&&n.push(e.channel),t&&n.push(`(${t})`),e.reason?`${n.join(" ")}: ${e.reason}`:n.join(" ")}function Uy(e){let t=Object.fromEntries(Object.entries(e).filter(([n])=>!n.startsWith("phx_ref")));return JSON.stringify(t)}function Fy(e){let t=e.inspect;if(d(O(`Realtime for ${e.ref}`)),d(`${t.connections} connection${t.connections===1?"":"s"}, ${t.connected_users} on a channel. The project's limit is ${t.limits.max_events_per_second} messages a second, counted once per message sent, however many receive it.`),t.channels.length===0){d("No channels are open.");return}for(let n of t.channels){let r=n.messages;if(d(""),d(O(`${n.name}${n.private?" (private)":""}`)+` ${n.clients.length} client${n.clients.length===1?"":"s"}`),d(b(` last ${r.window_seconds}s: ${r.broadcasts_received} signal${r.broadcasts_received===1?"":"s"} sent, ${r.broadcasts_delivered} delivered, busiest second ${r.peak_broadcasts_per_second}; ${r.presence_diffs_delivered} roster update${r.presence_diffs_delivered===1?"":"s"} delivered`)),n.clients.length>0){let s=n.clients.map(a=>[String(a.socket),a.presence_key,Fc(Date.now()-a.joined_at),Fc(a.last_seen_ms_ago)+(a.last_seen_ms_ago!==null&&a.last_seen_ms_ago>Dy?" (silent: likely gone)":"")]);d(W([["SOCKET","ROSTER KEY","JOINED","LAST HEARD"],...s]).split(`
|
|
503
528
|
`).map(a=>` ${a}`).join(`
|
|
504
|
-
`))}let o=Object.keys(n.presence);if(o.length>0){d(" roster:");for(let s of o)for(let a of n.presence[s].metas)d(` ${s} ${
|
|
505
|
-
`);if(s.events.length>0){r=Math.max(...s.events.map(
|
|
506
|
-
`))}}}async function
|
|
507
|
-
`);r.logCapacity!==null&&r.events.length>=r.logCapacity&&d(w(`The log keeps the newest ${r.logCapacity} events; older ones have gone.`))}),0}re();be();me();te();var sr=[{id:"bedrock",name:"Amazon Bedrock",mark:"AB",blurb:"Claude, Llama, Mistral, Qwen and more, in your own AWS account.",kind:"bedrock",suggestedName:"bedrock",region:!0,auth:"bedrock",connect:{console:{label:"AWS IAM console",href:"https://console.aws.amazon.com/iam/home#/users"},steps:["In IAM, make a user for the AI gateway (or reuse one) with a policy allowing bedrock:InvokeModel, bedrock:InvokeModelWithResponseStream and bedrock:Converse on the models you will serve.","Under the user's Security credentials, choose Create access key, for an application running outside AWS, and copy the access key ID and the secret. The secret is shown once.","Or use a Bedrock API key instead: Bedrock console, API keys, Generate.","Pick the region the models are in. Inference profiles starting with us. or global. route across regions from there."]},model:{link:{label:"Bedrock model catalog",href:"https://console.aws.amazon.com/bedrock/home#/model-catalog"},steps:["Open the Bedrock model catalog in the region you picked and choose the model.","If it asks for access, request it under Model access first; Anthropic models ask for a short use-case form once.","Copy its inference profile ID (it starts with us., eu., apac. or global.). A model with no inference profile is called by its model ID."],example:"us.anthropic.claude-sonnet-4-5-20250929-v1:0"},price:{link:{label:"Bedrock pricing",href:"https://aws.amazon.com/bedrock/pricing/"},steps:["Find the model on Bedrock's pricing page under on-demand. AWS quotes per 1,000 tokens for some models: multiply by 1,000 for the per-million rate asked here.",'Leave the prices blank if the shipped catalogue already knows the model; the model list shows "from the catalogue" when it does.']},caching:!0},{id:"anthropic",name:"Anthropic",mark:"An",blurb:"Claude, straight from Anthropic's API.",kind:"anthropic",suggestedName:"anthropic",baseUrl:"https://api.anthropic.com/v1",auth:"api_key",connect:{console:{label:"Anthropic Console, API keys",href:"https://console.anthropic.com/settings/keys"},steps:["In the Anthropic Console, open Settings, API keys, and choose Create key.","Name it after this gateway and copy it. It starts with sk-ant- and is shown once.","Make sure the organisation has credit or a billing plan, or every call is refused."]},model:{link:{label:"Claude models",href:"https://docs.anthropic.com/en/docs/about-claude/models/overview"},steps:["Open Anthropic's models overview and copy the model's API name, not its display name."],example:"claude-sonnet-4-5"},price:{link:{label:"Anthropic API pricing",href:"https://www.anthropic.com/pricing#api"},steps:["Copy the input and output rates per million tokens. Cache reads and cache writes have their own rates on the same page.","Leave the prices blank if the shipped catalogue already knows the model."]},caching:!0},{id:"openai",name:"OpenAI",mark:"OA",blurb:"GPT models, straight from OpenAI's API.",kind:"openai",suggestedName:"openai",baseUrl:"https://api.openai.com/v1",auth:"api_key",connect:{console:{label:"OpenAI API keys",href:"https://platform.openai.com/api-keys"},steps:["On the OpenAI platform, open API keys and choose Create new secret key.","Give it a project and the permission to call models, and copy it. It starts with sk- and is shown once.","Check the project has billing set up; a key without it is refused on the first call."]},model:{link:{label:"OpenAI models",href:"https://platform.openai.com/docs/models"},steps:["Open OpenAI's models page, choose the model and copy its id from the top of its page."],example:"gpt-5-mini"},price:{link:{label:"OpenAI API pricing",href:"https://openai.com/api/pricing/"},steps:['Copy the input and output rates per million tokens; "cached input" is the cache read rate. OpenAI caches by itself, so there is no cache write rate.',"Leave the prices blank if the shipped catalogue already knows the model."]},caching:!1},{id:"openrouter",name:"OpenRouter",mark:"OR",blurb:"Hundreds of models from many providers behind one key.",kind:"openai",suggestedName:"openrouter",baseUrl:"https://openrouter.ai/api/v1",auth:"api_key",connect:{console:{label:"OpenRouter keys",href:"https://openrouter.ai/settings/keys"},steps:["On OpenRouter, open Settings, Keys, and choose Create Key. A credit limit on the key is a good second fence.","Copy it. It starts with sk-or- and is shown once.","Add credits to the account, or every paid model is refused."]},model:{link:{label:"OpenRouter models",href:"https://openrouter.ai/models"},steps:["Find the model on OpenRouter and copy its id from under its name: the provider, a slash and the model."],example:"anthropic/claude-sonnet-4.5"},price:{link:{label:"OpenRouter models",href:"https://openrouter.ai/models"},steps:["Each model's page shows its input and output price per million tokens. Copy those; OpenRouter adds no fee on top of them for a key you pay for with credits."]},caching:!0},{id:"ollama",name:"Ollama",mark:"Ol",blurb:"A model on a machine of your own, such as the home server.",kind:"openai",suggestedName:"home-ollama",editableUrl:!0,urlExample:"https://home-ai.snoutdata.com/v1",auth:"optional",connect:{console:{label:"Ollama documentation",href:"https://github.com/ollama/ollama/blob/main/docs/openai.md"},steps:["The address must reach Ollama from this gateway, so not localhost: a public HTTPS name, such as one through a Cloudflare tunnel, ending in /v1.","Never leave it open: put a check in front of it (a Cloudflare rule that blocks any request without a secret header) and give that header's name and secret here.","Check it from your own machine first: a request to the address plus /models with the header should list the models, and one without it should be refused."]},model:{steps:["On the Ollama machine, run ollama pull with the model first, if it is not there yet.","Run ollama list and copy the NAME column exactly, tag included."],example:"snout-fim:1.0"},price:{steps:["Your machine charges nothing per token, so the price is what you choose to count against a person's budget: a small rate, such as $0.05 in and $0.10 out, keeps a runaway caller inside it.","Do not leave the price blank: the catalogue does not know your models, and a model nothing prices is refused to every signed-in user."]},caching:!1},{id:"compatible",name:"Any OpenAI-compatible server",mark:"{}",blurb:"vLLM, LM Studio, Together, Groq, DeepSeek and the rest.",kind:"openai",suggestedName:"my-provider",editableUrl:!0,urlExample:"https://api.example.com/v1",auth:"optional",connect:{steps:["Find the server's OpenAI-compatible address in its documentation; it usually ends in /v1.","Make an API key in its console and copy it. A server that checks a header of its own instead of a Bearer token takes the header's name too."]},model:{steps:["Copy the model id from the provider's model list, or from what the address plus /models returns."],example:"deepseek-chat"},price:{steps:["Copy the input and output rates per million tokens from the provider's pricing page.","For a server of your own, choose a small rate so budgets still count it; a model nothing prices is refused to every signed-in user."]},caching:!1}];function xc(e,t){if(t.kind!==e.kind)return!1;if(e.kind!=="openai")return!0;let n=t.base_url.replace(/\/+$/,"");return e.baseUrl?n===e.baseUrl:!sr.some(r=>r.baseUrl&&r.kind==="openai"&&r.baseUrl===n)}function Pc(e){return(e.trim().split("/").pop()??"").replace(/^(us|eu|apac|global)\.(anthropic|meta|mistral|amazon|cohere|qwen|deepseek|openai)\./,"").replace(/-v\d+:\d+$/,"").replace(/:/g,"-").toLowerCase()}function Ec(e,t,n){let o=e.auth==="bedrock"&&n.aws===!0?n.accessKeyId?.trim()&&n.secretAccessKey?.trim()?{access_key_id:n.accessKeyId.trim(),secret_access_key:n.secretAccessKey.trim()}:null:n.apiKey?.trim()?{api_key:n.apiKey.trim()}:null;if(e.auth!=="optional"&&!o)return{problem:"Paste the key from the first steps; Psyche checks it with the provider before keeping it."};if(e.kind!=="bedrock"&&!n.baseUrl?.trim())return{problem:"Give the address."};if(e.kind==="bedrock"&&!n.region?.trim())return{problem:"Give the region the model runs in."};let s={kind:e.kind};return e.kind==="bedrock"?s.region=n.region.trim():(s.base_url=n.baseUrl.trim(),n.authHeader?.trim()&&(s.auth_header=n.authHeader.trim())),o&&(s.credential=t),{provider:s,credential:o}}function Pg(e){return e===void 0||e.trim()===""?void 0:Number(e)}function Ac(e){if(!e.upstream.trim())return{problem:"Give the model's id, as the provider names it."};let[t,n,r,o,s]=[e.input,e.output,e.cacheRead,e.cacheWrite,e.maxOutput].map(Pg);if([t,n,r,o].some(l=>l!==void 0&&(!Number.isFinite(l)||l<0)))return{problem:"Prices are numbers, zero or more."};if(t===void 0!=(n===void 0))return{problem:"Give both the input and the output rate, or neither to take the catalogue's."};let a={provider:e.provider,upstream:e.upstream.trim()};return e.caching!==void 0&&(a.prompt_caching=e.caching),e.enabled!==void 0&&(a.enabled=e.enabled),e.access&&(a.access=e.access),t!==void 0&&n!==void 0&&(a.price={input:t,output:n},r!==void 0&&(a.price.cache_read=r),o!==void 0&&(a.price.cache_write=o)),s!==void 0&&Number.isFinite(s)&&s>0&&(a.max_output_tokens=Math.round(s)),{model:a}}var ir=class{send;constructor(t){this.send=t}providers(){return this.list("admin/providers")}putProvider(t,n){return this.call("PUT",`admin/providers/${qe(t)}`,n)}deleteProvider(t){return this.none("DELETE",`admin/providers/${qe(t)}`)}models(){return this.list("admin/models")}putModel(t,n){return this.call("PUT",`admin/models/${ar(t)}`,n)}deleteModel(t){return this.call("DELETE",`admin/models/${ar(t)}`)}routes(){return this.list("admin/routes")}putRoute(t,n){return this.call("PUT",`admin/routes/${ar(t)}`,n)}deleteRoute(t){return this.none("DELETE",`admin/routes/${ar(t)}`)}health(){return this.list("admin/health")}credentials(){return this.list("admin/credentials")}putCredential(t,n){return this.call("PUT",`admin/credentials/${qe(t)}`,n)}deleteCredential(t){return this.none("DELETE",`admin/credentials/${qe(t)}`)}keys(){return this.list("admin/keys")}createKey(t){return this.call("POST","admin/keys",t)}revokeKey(t){return this.none("DELETE",`admin/keys/${qe(t)}`)}budgets(){return this.list("admin/budgets")}createBudget(t){return this.call("POST","admin/budgets",t)}editBudget(t,n){return this.call("PATCH",`admin/budgets/${qe(t)}`,n)}deleteBudget(t){return this.none("DELETE",`admin/budgets/${qe(t)}`)}limits(){return this.list("admin/limits")}createLimit(t){return this.call("POST","admin/limits",t)}deleteLimit(t){return this.none("DELETE",`admin/limits/${qe(t)}`)}groups(){return this.list("admin/groups")}usage(t={}){return this.call("GET",`admin/usage${Rc(t)}`)}requests(t={}){return this.call("GET",`admin/requests${Rc(t)}`)}async list(t){return(await this.send("GET",t))?.data??[]}async call(t,n,r){return await this.send(t,n,r)}async none(t,n){await this.send(t,n)}};function qe(e){return encodeURIComponent(e)}function ar(e){return e.split("/").map(qe).join("/")}function Rc(e){let t=Object.entries(e).filter(([,n])=>n!=null&&n!=="").map(([n,r])=>`${encodeURIComponent(n)}=${encodeURIComponent(String(r))}`);return t.length?`?${t.join("&")}`:""}function Qt(e){if(e==null||e==="")return null;let t=Number(e);return Number.isFinite(t)?t:null}async function lr(e,t){return _("cloud-project-ai",t===void 0?{ref:e}:{ref:e,enable:t})}function Eg(e){if(e instanceof ge){let t=e.body?.error;if(t&&typeof t=="object"&&typeof t.message=="string")return new ge(e.status,t.message,typeof t.code=="string"?t.code:null,e.body)}return e}function Ag(e){return async(t,n,r)=>{try{return await _("cloud-project-ai",{ref:e,admin:{method:t,path:n,...r===void 0?{}:{body:r}}})}catch(o){throw Eg(o)}}}function fe(e){return new ir(Ag(e))}function Rg(e){return e instanceof ge&&(e.status===502||e.status===404&&e.serverCode!=="ai-off")}function Fo(e){return Number.isInteger(e)?`$${e}`:Math.abs(e*100-Math.round(e*100))<1e-9?`$${e.toFixed(2)}`:`$${e}`}function Oc(e){return e?`${Fo(e.input)} in, ${Fo(e.output)} out`:"no price"}function Nc(e){return e.every_user?"every user":e.user_id?`user ${e.user_id}`:e.key_id?`key ${e.key_id}`:e.group_id?`group ${e.group_id}`:"the project"}function Ho(e){let t=e.period==="rolling"?`per ${e.days??"?"} days`:e.period==="since"?`since ${e.starts_on??"?"}`:`a ${e.period}`,n=Qt(e.amount);return`${n===null?`$${e.amount}`:Fo(n)} ${t}, ${e.hard?"hard":"soft"}`}function Tg(e){let t=Qt(e);return t===null?"-":`$${t.toFixed(t!==0&&t<.01?4:2)}`}function cr(e){return e===null?"never":new Date(e*1e3).toISOString().slice(0,16).replace("T"," ")}function Ic(e,t=Date.now()){return new Date(t-(e-1)*864e5).toISOString().slice(0,10)}function Og(e,t){return`${e} ${t}${e===1?"":"s"}`}async function Cc(e){let t=await lr(e);if(!t.ai.enabled){h({ref:e,enabled:!1,endpoint:t.ai.endpoint},()=>{d(`AI is off for ${e}.`),d(w(` snoutdata ai enable --ref ${e} ${t.ai.restartsDatabase}`))});return}let n=fe(e),[r,o,s]=await Promise.allSettled([n.models(),n.budgets(),n.usage({by:"day",from:Ic(30)})]),a=[r,o,s].find(f=>f.status==="rejected");if(a&&Rg(a.reason)){h({ref:e,enabled:!0,endpoint:t.ai.endpoint,serving:!1},()=>{d(`AI is on for ${e}, and its gateway is still starting. Try again in a minute.`)});return}if(a)throw a.reason;let l=r.value,i=o.value,u=s.value.data,c=u.reduce((f,k)=>f+(Qt(k.cost)??0),0),p=u.reduce((f,k)=>f+k.requests,0);h({ref:e,enabled:!0,endpoint:t.ai.endpoint,serving:!0,models:l,budgets:i,last30Days:{requests:p,cost:c}},()=>{d(`${x(`AI for ${e}`)}: on, at ${t.ai.endpoint}`),d(""),l.length===0?(d("No models yet. Add one:"),d(w(" snoutdata ai models add --source openai --model gpt-4o-mini --stdin < key.txt"))):d(Dc(l)),d(""),d(i.length===0?"No budgets.":i.map(f=>`Budget: ${Nc(f)}, ${Ho(f)}`).join(`
|
|
508
|
-
`)),d(`Last 30 days: ${
|
|
509
|
-
`),d(""),d(`${
|
|
510
|
-
`),d(
|
|
511
|
-
`),e.enabled||d(
|
|
512
|
-
`)}
|
|
529
|
+
`))}let o=Object.keys(n.presence);if(o.length>0){d(" roster:");for(let s of o)for(let a of n.presence[s].metas)d(` ${s} ${Uy(a)}`)}}}async function Hc(e,t={}){if(t.watch&&ir())throw new g("--watch is for a person; with --json, poll `realtime inspect --json` or `realtime logs --since 1m --json`");let n=await Ms(e,{channel:t.channel});if(w(n.inspect,()=>Fy(n)),!t.watch)return 0;d(""),d(b("Watching. New events appear below as they happen; Ctrl+C to stop."));let r=n.at+1,o=t.intervalMs??2e3;for(;;){await new Promise(a=>setTimeout(a,o));let s=await Ms(e,{channel:t.channel,since:r});for(let a of s.events)process.stdout.write(`${Bc(a)}
|
|
530
|
+
`);if(s.events.length>0){r=Math.max(...s.events.map(c=>c.at))+1;let a=s.inspect.channels.map(c=>`${c.name}: ${Object.keys(c.presence).join(", ")||"nobody tracked"}`).join("; ");process.stdout.write(b(` now ${a||"no channels open"}
|
|
531
|
+
`))}}}async function Kc(e,t={}){let n=t.since?My(t.since):0,r=await Ms(e,{channel:t.channel,since:n});return w({ref:r.ref,events:r.events,capacity:r.logCapacity},()=>{if(r.events.length===0){d(t.since||t.channel?"Nothing in the log for that.":"Nothing in the log yet. It fills as clients connect, and starts again when the project's host restarts.");return}for(let o of r.events)process.stdout.write(`${Bc(o)}
|
|
532
|
+
`);r.logCapacity!==null&&r.events.length>=r.logCapacity&&d(b(`The log keeps the newest ${r.logCapacity} events; older ones have gone.`))}),0}me();Ae();Se();ue();var Nr=[{id:"bedrock",name:"Amazon Bedrock",mark:"AB",blurb:"Claude, Llama, Mistral, Qwen and more, in your own AWS account.",kind:"bedrock",suggestedName:"bedrock",region:!0,auth:"bedrock",connect:{console:{label:"AWS IAM console",href:"https://console.aws.amazon.com/iam/home#/users"},steps:["In IAM, make a user for the AI gateway (or reuse one) with a policy allowing bedrock:InvokeModel, bedrock:InvokeModelWithResponseStream and bedrock:Converse on the models you will serve.","Under the user's Security credentials, choose Create access key, for an application running outside AWS, and copy the access key ID and the secret. The secret is shown once.","Or use a Bedrock API key instead: Bedrock console, API keys, Generate.","Pick the region the models are in. Inference profiles starting with us. or global. route across regions from there."]},model:{link:{label:"Bedrock model catalog",href:"https://console.aws.amazon.com/bedrock/home#/model-catalog"},steps:["Open the Bedrock model catalog in the region you picked and choose the model.","If it asks for access, request it under Model access first; Anthropic models ask for a short use-case form once.","Copy its inference profile ID (it starts with us., eu., apac. or global.). A model with no inference profile is called by its model ID."],example:"us.anthropic.claude-sonnet-4-5-20250929-v1:0"},price:{link:{label:"Bedrock pricing",href:"https://aws.amazon.com/bedrock/pricing/"},steps:["Find the model on Bedrock's pricing page under on-demand. AWS quotes per 1,000 tokens for some models: multiply by 1,000 for the per-million rate asked here.",'Leave the prices blank if the shipped catalogue already knows the model; the model list shows "from the catalogue" when it does.']},caching:!0},{id:"anthropic",name:"Anthropic",mark:"An",blurb:"Claude, straight from Anthropic's API.",kind:"anthropic",suggestedName:"anthropic",baseUrl:"https://api.anthropic.com/v1",auth:"api_key",connect:{console:{label:"Anthropic Console, API keys",href:"https://console.anthropic.com/settings/keys"},steps:["In the Anthropic Console, open Settings, API keys, and choose Create key.","Name it after this gateway and copy it. It starts with sk-ant- and is shown once.","Make sure the organisation has credit or a billing plan, or every call is refused."]},model:{link:{label:"Claude models",href:"https://docs.anthropic.com/en/docs/about-claude/models/overview"},steps:["Open Anthropic's models overview and copy the model's API name, not its display name."],example:"claude-sonnet-4-5"},price:{link:{label:"Anthropic API pricing",href:"https://www.anthropic.com/pricing#api"},steps:["Copy the input and output rates per million tokens. Cache reads and cache writes have their own rates on the same page.","Leave the prices blank if the shipped catalogue already knows the model."]},caching:!0},{id:"openai",name:"OpenAI",mark:"OA",blurb:"GPT models, straight from OpenAI's API.",kind:"openai",suggestedName:"openai",baseUrl:"https://api.openai.com/v1",auth:"api_key",connect:{console:{label:"OpenAI API keys",href:"https://platform.openai.com/api-keys"},steps:["On the OpenAI platform, open API keys and choose Create new secret key.","Give it a project and the permission to call models, and copy it. It starts with sk- and is shown once.","Check the project has billing set up; a key without it is refused on the first call."]},model:{link:{label:"OpenAI models",href:"https://platform.openai.com/docs/models"},steps:["Open OpenAI's models page, choose the model and copy its id from the top of its page."],example:"gpt-5-mini"},price:{link:{label:"OpenAI API pricing",href:"https://openai.com/api/pricing/"},steps:['Copy the input and output rates per million tokens; "cached input" is the cache read rate. OpenAI caches by itself, so there is no cache write rate.',"Leave the prices blank if the shipped catalogue already knows the model."]},caching:!1},{id:"openrouter",name:"OpenRouter",mark:"OR",blurb:"Hundreds of models from many providers behind one key.",kind:"openai",suggestedName:"openrouter",baseUrl:"https://openrouter.ai/api/v1",auth:"api_key",connect:{console:{label:"OpenRouter keys",href:"https://openrouter.ai/settings/keys"},steps:["On OpenRouter, open Settings, Keys, and choose Create Key. A credit limit on the key is a good second fence.","Copy it. It starts with sk-or- and is shown once.","Add credits to the account, or every paid model is refused."]},model:{link:{label:"OpenRouter models",href:"https://openrouter.ai/models"},steps:["Find the model on OpenRouter and copy its id from under its name: the provider, a slash and the model."],example:"anthropic/claude-sonnet-4.5"},price:{link:{label:"OpenRouter models",href:"https://openrouter.ai/models"},steps:["Each model's page shows its input and output price per million tokens. Copy those; OpenRouter adds no fee on top of them for a key you pay for with credits."]},caching:!0},{id:"ollama",name:"Ollama",mark:"Ol",blurb:"A model on a machine of your own, such as the home server.",kind:"openai",suggestedName:"home-ollama",editableUrl:!0,urlExample:"https://home-ai.snoutdata.com/v1",auth:"optional",connect:{console:{label:"Ollama documentation",href:"https://github.com/ollama/ollama/blob/main/docs/openai.md"},steps:["The address must reach Ollama from this gateway, so not localhost: a public HTTPS name, such as one through a Cloudflare tunnel, ending in /v1.","Never leave it open: put a check in front of it (a Cloudflare rule that blocks any request without a secret header) and give that header's name and secret here.","Check it from your own machine first: a request to the address plus /models with the header should list the models, and one without it should be refused."]},model:{steps:["On the Ollama machine, run ollama pull with the model first, if it is not there yet.","Run ollama list and copy the NAME column exactly, tag included."],example:"snout-fim:1.0"},price:{steps:["Your machine charges nothing per token, so the price is what you choose to count against a person's budget: a small rate, such as $0.05 in and $0.10 out, keeps a runaway caller inside it.","Do not leave the price blank: the catalogue does not know your models, and a model nothing prices is refused to every signed-in user."]},caching:!1},{id:"compatible",name:"Any OpenAI-compatible server",mark:"{}",blurb:"vLLM, LM Studio, Together, Groq, DeepSeek and the rest.",kind:"openai",suggestedName:"my-provider",editableUrl:!0,urlExample:"https://api.example.com/v1",auth:"optional",connect:{steps:["Find the server's OpenAI-compatible address in its documentation; it usually ends in /v1.","Make an API key in its console and copy it. A server that checks a header of its own instead of a Bearer token takes the header's name too."]},model:{steps:["Copy the model id from the provider's model list, or from what the address plus /models returns."],example:"deepseek-chat"},price:{steps:["Copy the input and output rates per million tokens from the provider's pricing page.","For a server of your own, choose a small rate so budgets still count it; a model nothing prices is refused to every signed-in user."]},caching:!1}];function Gc(e,t){if(t.kind!==e.kind)return!1;if(e.kind!=="openai")return!0;let n=t.base_url.replace(/\/+$/,"");return e.baseUrl?n===e.baseUrl:!Nr.some(r=>r.baseUrl&&r.kind==="openai"&&r.baseUrl===n)}function qc(e){return(e.trim().split("/").pop()??"").replace(/^(us|eu|apac|global)\.(anthropic|meta|mistral|amazon|cohere|qwen|deepseek|openai)\./,"").replace(/-v\d+:\d+$/,"").replace(/:/g,"-").toLowerCase()}function Wc(e,t,n){let o=e.auth==="bedrock"&&n.aws===!0?n.accessKeyId?.trim()&&n.secretAccessKey?.trim()?{access_key_id:n.accessKeyId.trim(),secret_access_key:n.secretAccessKey.trim()}:null:n.apiKey?.trim()?{api_key:n.apiKey.trim()}:null;if(e.auth!=="optional"&&!o)return{problem:"Paste the key from the first steps; Psyche checks it with the provider before keeping it."};if(e.kind!=="bedrock"&&!n.baseUrl?.trim())return{problem:"Give the address."};if(e.kind==="bedrock"&&!n.region?.trim())return{problem:"Give the region the model runs in."};let s={kind:e.kind};return e.kind==="bedrock"?s.region=n.region.trim():(s.base_url=n.baseUrl.trim(),n.authHeader?.trim()&&(s.auth_header=n.authHeader.trim())),o&&(s.credential=t),{provider:s,credential:o}}function Hy(e){return e===void 0||e.trim()===""?void 0:Number(e)}function zc(e){if(!e.upstream.trim())return{problem:"Give the model's id, as the provider names it."};let[t,n,r,o,s]=[e.input,e.output,e.cacheRead,e.cacheWrite,e.maxOutput].map(Hy);if([t,n,r,o].some(c=>c!==void 0&&(!Number.isFinite(c)||c<0)))return{problem:"Prices are numbers, zero or more."};if(t===void 0!=(n===void 0))return{problem:"Give both the input and the output rate, or neither to take the catalogue's."};let a={provider:e.provider,upstream:e.upstream.trim()};return e.caching!==void 0&&(a.prompt_caching=e.caching),e.enabled!==void 0&&(a.enabled=e.enabled),e.access&&(a.access=e.access),t!==void 0&&n!==void 0&&(a.price={input:t,output:n},r!==void 0&&(a.price.cache_read=r),o!==void 0&&(a.price.cache_write=o)),s!==void 0&&Number.isFinite(s)&&s>0&&(a.max_output_tokens=Math.round(s)),{model:a}}var Cr=class{send;constructor(t){this.send=t}providers(){return this.list("admin/providers")}putProvider(t,n){return this.call("PUT",`admin/providers/${Ye(t)}`,n)}deleteProvider(t){return this.none("DELETE",`admin/providers/${Ye(t)}`)}upstreamModels(t){return typeof t=="string"?this.list(`admin/providers/${Ye(t)}/upstream-models`):this.call("POST","admin/upstream-models",t).then(n=>n?.data??[])}models(){return this.list("admin/models")}putModel(t,n){return this.call("PUT",`admin/models/${Ir(t)}`,n)}deleteModel(t){return this.call("DELETE",`admin/models/${Ir(t)}`)}routes(){return this.list("admin/routes")}putRoute(t,n){return this.call("PUT",`admin/routes/${Ir(t)}`,n)}deleteRoute(t){return this.none("DELETE",`admin/routes/${Ir(t)}`)}health(){return this.list("admin/health")}credentials(){return this.list("admin/credentials")}putCredential(t,n){return this.call("PUT",`admin/credentials/${Ye(t)}`,n)}deleteCredential(t){return this.none("DELETE",`admin/credentials/${Ye(t)}`)}keys(){return this.list("admin/keys")}createKey(t){return this.call("POST","admin/keys",t)}revokeKey(t){return this.none("DELETE",`admin/keys/${Ye(t)}`)}budgets(){return this.list("admin/budgets")}createBudget(t){return this.call("POST","admin/budgets",t)}editBudget(t,n){return this.call("PATCH",`admin/budgets/${Ye(t)}`,n)}deleteBudget(t){return this.none("DELETE",`admin/budgets/${Ye(t)}`)}limits(){return this.list("admin/limits")}createLimit(t){return this.call("POST","admin/limits",t)}deleteLimit(t){return this.none("DELETE",`admin/limits/${Ye(t)}`)}groups(){return this.list("admin/groups")}usage(t={}){return this.call("GET",`admin/usage${Jc(t)}`)}requests(t={}){return this.call("GET",`admin/requests${Jc(t)}`)}async list(t){return(await this.send("GET",t))?.data??[]}async call(t,n,r){return await this.send(t,n,r)}async none(t,n){await this.send(t,n)}};function Ye(e){return encodeURIComponent(e)}function Ir(e){return e.split("/").map(Ye).join("/")}function Jc(e){let t=Object.entries(e).filter(([,n])=>n!=null&&n!=="").map(([n,r])=>`${encodeURIComponent(n)}=${encodeURIComponent(String(r))}`);return t.length?`?${t.join("&")}`:""}function xn(e){if(e==null||e==="")return null;let t=Number(e);return Number.isFinite(t)?t:null}async function Mr(e,t){return x("cloud-project-ai",t===void 0?{ref:e}:{ref:e,enable:t})}function Ky(e){if(e instanceof $e){let t=e.body?.error;if(t&&typeof t=="object"&&typeof t.message=="string")return new $e(e.status,t.message,typeof t.code=="string"?t.code:null,e.body)}return e}function Gy(e){return async(t,n,r)=>{try{return await x("cloud-project-ai",{ref:e,admin:{method:t,path:n,...r===void 0?{}:{body:r}}})}catch(o){throw Ky(o)}}}function _e(e){return new Cr(Gy(e))}function qy(e){return e instanceof $e&&(e.status===502||e.status===404&&e.serverCode!=="ai-off")}function js(e){return Number.isInteger(e)?`$${e}`:Math.abs(e*100-Math.round(e*100))<1e-9?`$${e.toFixed(2)}`:`$${e}`}function Yc(e){return e?`${js(e.input)} in, ${js(e.output)} out`:"no price"}function Xc(e){return e.every_user?"every user":e.user_id?`user ${e.user_id}`:e.key_id?`key ${e.key_id}`:e.group_id?`group ${e.group_id}`:"the project"}function Ls(e){let t=e.period==="rolling"?`per ${e.days??"?"} days`:e.period==="since"?`since ${e.starts_on??"?"}`:`a ${e.period}`,n=xn(e.amount);return`${n===null?`$${e.amount}`:js(n)} ${t}, ${e.hard?"hard":"soft"}`}function Wy(e){let t=xn(e);return t===null?"-":`$${t.toFixed(t!==0&&t<.01?4:2)}`}function Dr(e){return e===null?"never":new Date(e*1e3).toISOString().slice(0,16).replace("T"," ")}function Zc(e,t=Date.now()){return new Date(t-(e-1)*864e5).toISOString().slice(0,10)}function zy(e,t){return`${e} ${t}${e===1?"":"s"}`}async function Qc(e){let t=await Mr(e);if(!t.ai.enabled){w({ref:e,enabled:!1,endpoint:t.ai.endpoint},()=>{d(`AI is off for ${e}.`),d(b(` snoutdata ai enable --ref ${e} ${t.ai.restartsDatabase}`))});return}let n=_e(e),[r,o,s]=await Promise.allSettled([n.models(),n.budgets(),n.usage({by:"day",from:Zc(30)})]),a=[r,o,s].find(h=>h.status==="rejected");if(a&&qy(a.reason)){w({ref:e,enabled:!0,endpoint:t.ai.endpoint,serving:!1},()=>{d(`AI is on for ${e}, and its gateway is still starting. Try again in a minute.`)});return}if(a)throw a.reason;let c=r.value,i=o.value,u=s.value.data,l=u.reduce((h,P)=>h+(xn(P.cost)??0),0),m=u.reduce((h,P)=>h+P.requests,0);w({ref:e,enabled:!0,endpoint:t.ai.endpoint,serving:!0,models:c,budgets:i,last30Days:{requests:m,cost:l}},()=>{d(`${O(`AI for ${e}`)}: on, at ${t.ai.endpoint}`),d(""),c.length===0?(d("No models yet. Add one:"),d(b(" snoutdata ai models add --source openai --model gpt-4o-mini --stdin < key.txt"))):d(td(c)),d(""),d(i.length===0?"No budgets.":i.map(h=>`Budget: ${Xc(h)}, ${Ls(h)}`).join(`
|
|
533
|
+
`)),d(`Last 30 days: ${zy(m,"request")}, $${l.toFixed(2)}.`)})}async function ed(e,t){let n=await Mr(e,t);w(n,()=>{t?(d(`AI is on for ${e}. ${n.ai.restartsDatabase} The gateway answers at ${n.ai.endpoint} within a minute or two.`),d(b(" next: snoutdata ai models add --source openai --model gpt-4o-mini --stdin < key.txt"))):d(`AI is off for ${e}. Its providers, models, keys and log stay in the project's database, for when it is switched on again.`)})}function td(e){return W([["MODEL","PROVIDER","UPSTREAM","PRICE PER M TOKENS","SERVED"],...e.map(t=>[t.name,t.provider,t.upstream,Yc(t.price)+(t.price_from==="catalogue"?" (catalogue)":""),t.served?"yes":`no: ${t.problem??(t.enabled?"not served":"disabled")}`])])}async function nd(e){let t=await _e(e).models();w(t,()=>{d(t.length===0?`No models on ${e}'s gateway yet.`:td(t))})}async function rd(e){let t=await _e(e).providers();w(t,()=>{if(t.length===0){d(`No providers on ${e}'s gateway yet. \`snoutdata ai models add\` makes one with its first model.`);return}d(W([["PROVIDER","KIND","WHERE","KEY","MODELS"],...t.map(n=>[n.name,n.kind,n.kind==="bedrock"?n.region??"":n.base_url,Jy(n),n.models.join(", ")||"-"])]));for(let n of t.filter(r=>r.problem))ie(`${n.name}: ${n.problem}`)})}function Jy(e){return e.key==="environment"?"from the environment":e.key==="none"||e.credential===null?"none":e.credential_stored?"stored":"missing"}async function od(e,t){if(!t)throw new g("ai providers remove needs the provider's name (see `snoutdata ai providers`)");await _e(e).deleteProvider(t),w({ref:e,removed:t},()=>d(`Removed the provider ${t}.`))}function Vy(e){let t=Nr.map(r=>r.id).join("|");if(!e)throw new g(`ai models add needs --source ${t}: where the model runs`);let n=Nr.find(r=>r.id===e);if(!n)throw new g(`--source is one of ${t}, not "${e}"`);return n}function Yy(e){d(O(`Adding a model from ${e.name}`));let t=[["The key",e.connect],["The model's id",e.model],["Its price",e.price]];for(let[n,r]of t){d(""),d(O(n));for(let s of r.steps)d(` - ${s}`);let o=r.console??r.link;o&&d(b(` ${o.label}: ${o.href}`))}d("")}function Xy(e,t,n){if(n)return{name:n,existing:t.find(o=>o.name===n)??null};let r=t.filter(o=>Gc(e,o));if(r.length===1)return{name:r[0].name,existing:r[0]};if(r.length>1)throw new g(`${e.name} is connected more than once (${r.map(o=>o.name).join(", ")}): say which with --provider`);return{name:e.suggestedName,existing:t.find(o=>o.name===e.suggestedName)??null}}async function sd(e,t,n){let r=Vy(n.source),o=n.upstream?.trim();if(!o)throw Yy(r),new g(`Give the model's id as ${r.name} names it, with --model (for example ${r.model.example})`);let s=t?.trim()||qc(o),a=_e(e),[c,i]=await Promise.all([a.providers(),a.models()]);if(i.some(C=>C.name===s))throw new g(`${e} already has a model called ${s}. Name this one differently, or remove that one first.`);let{name:u,existing:l}=Xy(r,c,n.provider);if(l&&l.kind!==r.kind)throw new g(`The provider ${u} speaks ${l.kind}, not ${r.name}: choose another --provider name`);if(l?.origin==="file"&&n.stdin)throw new g(`The provider ${u} comes from the gateway's providers file, so its key is set there, not here`);let m=n.stdin?await Gt():void 0,h=r.auth==="bedrock"&&n.accessKeyId!==void 0,P=null;if(!l){if(r.auth!=="optional"&&!m)throw new g(h?"Pipe the AWS secret access key in with --stdin; --access-key-id is its id":`${r.name} needs a key: pipe it in with --stdin (snoutdata ai models add ... --stdin < key.txt), so it is in neither the shell history nor ps`);let C=Wc(r,u,{baseUrl:n.baseUrl??r.baseUrl,region:n.region,apiKey:h?void 0:m,aws:h,accessKeyId:n.accessKeyId,secretAccessKey:h?m:void 0,authHeader:n.authHeader});if("problem"in C)throw new g(r.kind==="bedrock"&&!n.region?"Give the region the model runs in, with --region":n.baseUrl===void 0&&r.kind!=="bedrock"?"Give the provider's address, with --base-url":C.problem);P=C}let A=zc({provider:u,upstream:o,input:n.priceIn,output:n.priceOut,caching:r.caching,enabled:!0,access:"everyone"});if("problem"in A)throw new g(A.problem.replace("input and the output rate","--price-in and --price-out"));let ee=!1,z=!1,le;try{if(P&&(await a.putProvider(u,P.provider),ee=!0),le=await a.putModel(s,A.model),z=!0,P?.credential)await a.putCredential(u,P.credential);else if(l&&m){if(!l.credential)throw new g(`The provider ${u} takes no key`);await a.putCredential(l.credential,h?{access_key_id:n.accessKeyId,secret_access_key:m}:{api_key:m})}}catch(C){throw z&&await a.deleteModel(s).catch(()=>{}),ee&&await a.deleteProvider(u).catch(()=>{}),C}le=(await a.models()).find(C=>C.name===s)??le,w(le,()=>{d(`Added ${O(s)} (${o} on ${u}${ee?", a new provider":""}), at ${Yc(le.price)}${le.price_from==="catalogue"?" from the shipped catalogue":""}.`),le.price||ie("Nothing prices this model, so signed-in users are refused it. Give it a price with --price-in and --price-out."),!le.served&&le.problem&&ie(`It is not served yet: ${le.problem}`),d(b(` snoutdata ai test --model ${s}`))})}async function ad(e,t){if(!t)throw new g("ai models remove needs the model's name (see `snoutdata ai models`)");let n=await _e(e).deleteModel(t);w(n,()=>d(n.disabled?`${t} comes from the gateway's providers file, so it was disabled rather than removed.`:`Removed ${t}.`))}function Zy(e){return(e??"").split(",").map(t=>t.trim()).filter(t=>t!=="")}function Qy(e){return W([["ROUTE","TRIES, IN ORDER","SORT","SKIPS"],...e.map(t=>[t.name,t.tried.join(", ")||"-",t.sort,t.skipped.join(", ")||"-"])])}async function id(e){let t=await _e(e).routes();w(t,()=>{if(t.length===0){d(`No routes on ${e}'s gateway. A route is one name for several models, each tried when the one before fails:`),d(b(" snoutdata ai routes set NAME --models first,second [--sort price]"));return}d(Qy(t))})}async function ld(e,t,n){if(!t)throw new g("ai routes set needs the route's name: snoutdata ai routes set NAME --models first,second");let r=Zy(n.models);if(r.length===0)throw new g("ai routes set needs --models, the models to try in order, comma-separated (see `snoutdata ai models`)");let o=n.sort??"order";if(o!=="order"&&o!=="price"&&o!=="latency"&&o!=="throughput")throw new g(`--sort is order (as written), price (the cheapest first), latency or throughput (the fastest first, as measured), not ${o}`);let s=_e(e);await s.putRoute(t,{targets:r,sort:o});let a=(await s.routes()).find(c=>c.name===t);w(a??{name:t,targets:r,sort:o},()=>{let c=a?.tried??r;d(`${O(t)} tries ${c.join(", then ")}${o==="price"?", the cheapest first":o==="latency"||o==="throughput"?`, the fastest first by ${o} once measured`:""}.`),a&&a.skipped.length>0&&ie(`skipped, not served: ${a.skipped.join(", ")}`),d(b(` Call it as the model "${t}", or: snoutdata ai test --model ${t}`))})}async function cd(e,t){if(!t)throw new g("ai routes remove needs the route's name (see `snoutdata ai routes`)");await _e(e).deleteRoute(t),w({name:t,removed:!0},()=>d(`Removed the route ${t}. Its models are untouched.`))}function ew(e,t=Date.now()/1e3){return e.revoked_at!==null?"revoked":e.expires_at!==null&&e.expires_at<=t?"expired":e.expires_at!==null?`until ${Dr(e.expires_at).slice(0,10)}`:"active"}async function dd(e){let t=await _e(e).keys();w(t,()=>{if(t.length===0){d(`No keys on ${e}'s gateway. \`snoutdata ai keys create --name my-server\` makes one for a server.`);return}d(W([["NAME","ID","KEY","MODELS","CREATED","LAST USED","STATE"],...t.map(n=>[n.name,n.id,n.hint,n.models?.join(", ")??"all",Dr(n.created_at),n.last_used_at!==null?Dr(n.last_used_at):n.last_used_on??"never",ew(n)])]))})}async function ud(e,t){if(!t.name)throw new g("ai keys create needs --name: what the key is for, shown in ai keys");if(t.expiresDays!==void 0&&t.expiresDays<=0)throw new g("--expires is a number of days, 1 or more");let n=t.models?.split(",").map(s=>s.trim()).filter(Boolean),r=await _e(e).createKey({name:t.name,...n&&n.length>0?{models:n}:{},...t.expiresDays!==void 0?{expires_in:t.expiresDays*86400}:{}}),o=await Mr(e);w({...r,endpoint:o.ai.endpoint},()=>{process.stdout.write(`${r.key}
|
|
534
|
+
`),d(""),d(`${O(r.name)}: ${r.models?`for ${r.models.join(", ")}`:"for every model"}, ${r.expires_at?`until ${Dr(r.expires_at)}`:"until revoked"}.`),d("This is the only time it is shown: only its hash is kept. It is a server's key; a browser calls with the app key and a signed-in user instead."),d(b(` OPENAI_BASE_URL=${o.ai.endpoint} OPENAI_API_KEY=<that key>`))})}async function pd(e,t){if(!t)throw new g("ai keys revoke needs the key's id (see `snoutdata ai keys`)");await _e(e).revokeKey(t),w({ref:e,revoked:t},()=>d(`Revoked ${t}. A call with it is refused from now on.`))}async function md(e){let t=await _e(e).budgets();w(t,()=>{if(t.length===0){d(`No budgets on ${e}'s gateway. A browser call needs two hard ones:`),d(b(" snoutdata ai budgets set project --amount 5")),d(b(" snoutdata ai budgets set users --amount 0.50"));return}d(W([["ID","FOR","BUDGET"],...t.map(n=>[n.id,Xc(n),Ls(n)])]))})}function tw(e){if(e==="project"||e==="users")return e;throw new g("ai budgets set takes project (the whole project) or users (each signed-in user)")}function nw(e,t,n){return e.find(r=>r.period===n&&!r.key_id&&!r.user_id&&!r.group_id&&(t==="users"?r.every_user:!r.every_user))}async function fd(e,t,n){let r=tw(t),o=Number(n.amount);if(n.amount===void 0||!Number.isFinite(o)||o<0)throw new g("ai budgets set needs --amount, in dollars, zero or more");let s=n.period??"day";if(s!=="day"&&s!=="month")throw new g("--period is day or month");let a=_e(e),c=nw(await a.budgets(),r,s),i=c?await a.editBudget(c.id,{amount:o,hard:!n.soft}):await a.createBudget({name:r==="users"?"every user":"project",amount:o,period:s,hard:!n.soft,...r==="users"?{every_user:!0}:{}});w(i,()=>d(`${c?"Changed":"Set"}: ${r==="users"?"every user":"the project"}, ${Ls(i)}.`))}async function gd(e,t){if(!t)throw new g("ai budgets remove needs the budget's id (see `snoutdata ai budgets`)");await _e(e).deleteBudget(t),w({ref:e,removed:t},()=>d(`Removed the budget ${t}.`))}var Vc=["day","model","provider","operation","key","user","group"];async function hd(e,t){let n=t.by??"model";if(!Vc.includes(n))throw new g(`--by is one of ${Vc.join("|")}`);let r=t.days??30;if(r<1||r>366)throw new g("--days is 1 to 366");let o=await _e(e).usage({by:n,from:Zc(r)});w(o,()=>{if(o.data.length===0){d(`No calls to ${e}'s gateway from ${o.from} to ${o.to}.`);return}let s=o.data.reduce((a,c)=>a+(xn(c.cost)??0),0);d(W([[n.toUpperCase(),"REQUESTS","FAILED","TOKENS IN","TOKENS OUT","COST"],...o.data.map(a=>[String(a[n]??"-"),String(a.requests),String(a.failed),String(a.prompt_tokens),String(a.completion_tokens),Wy(a.cost)+(a.unpriced>0?` (+${a.unpriced} unpriced)`:"")])])),d(`${o.from} to ${o.to}: $${s.toFixed(2)}.`)})}async function yd(e,t){let n=await Mr(e);if(!n.ai.enabled)throw new T("not-found",`AI is off for ${e}. Switch it on first: snoutdata ai enable.`);let r=_e(e),s=(await r.models()).filter(P=>P.served&&P.embeddings===null),a=t.model??s[0]?.name;if(!a)throw new T("not-found",`${e}'s gateway serves no chat model yet. Add one: snoutdata ai models add.`);let c=await r.createKey({name:"snoutdata ai test",models:[a],expires_in:600}),i=Date.now(),u=0,l={};try{let P=await fetch(`${n.ai.endpoint}/chat/completions`,{method:"POST",headers:{authorization:`Bearer ${c.key}`,"content-type":"application/json"},body:JSON.stringify({model:a,max_tokens:64,messages:[{role:"user",content:t.prompt??"Say hello in five words."}]})});u=P.status;let A=await P.text();try{l=A?JSON.parse(A):{}}catch{l={error:{message:mr(P.status,P.statusText,A)}}}}finally{await r.revokeKey(c.id).catch(()=>{})}let m=Date.now()-i;if(u<200||u>=300){let P=l.error?.message??`the gateway answered ${u}`;throw new T(nt(u,l.error?.code??null),`${a} through ${e}'s gateway: ${P}`,{model:a,status:u,gatewayCode:l.error?.code??null})}let h=l.choices?.[0]?.message?.content??"";return w({ref:e,model:a,reply:h,usage:l.usage??null,ms:m},()=>{process.stdout.write(`${h}
|
|
535
|
+
`),d(b(`${a}, ${m} ms, ${l.usage?.prompt_tokens??"?"} tokens in, ${l.usage?.completion_tokens??"?"} out, through ${n.ai.endpoint}`))}),0}me();Ae();ue();import{readFile as ow}from"node:fs/promises";var sw="api.snoutdata.com";function Sd(e,t){return{ref:e,enabled:t.enabled===!0,siteUrl:t.settings?.site_url??null,redirects:t.settings?.uri_allow_list??[],google:{enabled:t.google?.enabled===!0,clientId:t.google?.clientId??null,clientSecretSet:t.google?.clientSecretSet===!0,ready:t.google?.ready===!0},googleCallback:`https://${e}.${sw}/auth/v1/callback`,anonymous:{enabled:t.anonymous?.enabled===!0,ready:t.anonymous?.ready===!0}}}async function aw(e){return Sd(e,await x("cloud-project-auth",{ref:e,action:"status"}))}async function jr(e,t){return Sd(e,await x("cloud-project-auth",{ref:e,action:"settings",settings:t}))}function Us(e){let t=e.google,n="off";t.ready?n=O("on"):t.enabled&&(n=`switched on, not offered yet: needs ${[!t.clientId&&"a client id",!t.clientSecretSet&&"a client secret"].filter(Boolean).join(" and ")}`),process.stdout.write(`${W([["auth",e.enabled?O("on"):"off"],["site url",e.siteUrl??b("not set")],["redirects",e.redirects.length?e.redirects.join(", "):b("none")],["google",n],["google client id",t.clientId??b("not set")],["google callback",e.googleCallback],["guests",e.anonymous.ready?O("on"):e.anonymous.enabled?"switched on, not applied yet":"off"]])}
|
|
536
|
+
`),e.enabled||d(b(` snoutdata products enable auth --ref ${e.ref}`))}async function iw(){let e=[];for await(let t of process.stdin)e.push(Buffer.from(t));return Buffer.concat(e).toString("utf8").replace(/\r?\n$/,"")}async function _d(e,t,n,r){if(t===void 0||t==="show"){let o=await aw(e);w(o,()=>Us(o));return}if(t==="google"){if(n[0]==="off"){let a=await jr(e,{googleEnabled:!1});w(a,()=>d("Google sign-in is off. Your auth service restarts within about a minute."));return}if(n.length>0)throw new g('auth google takes --client-id and --stdin, or "off"');let o={googleEnabled:!0};if(r.clientId&&(o.googleClientId=r.clientId),r.stdin){let a=await iw();if(!a)throw new g("--stdin read nothing. Pipe the Google client secret in.");o.googleClientSecret=a}let s=await jr(e,o);w(s,()=>{s.google.ready?d("Google sign-in is on. Your auth service restarts within about a minute."):Us(s),d(b(` Add ${s.googleCallback} to your Google client's Authorized redirect URIs.`))});return}if(t==="anonymous"||t==="guests"){let o=n[0];if(o!=="on"&&o!=="off")throw new g('auth anonymous takes "on" or "off"');let s=await jr(e,{anonymousEnabled:o==="on"});w(s,()=>{o==="off"?d("Guest sign-in is off. Guests who already have a session keep it until it ends. Your auth service restarts within about a minute."):s.enabled?d("Guest sign-in is on: guestSignIn() gives a browser a session with no email or password. Your auth service restarts within about a minute."):(d("Guest sign-in is switched on, and takes effect when auth is."),d(b(` snoutdata products enable auth --ref ${s.ref}`)))});return}if(t==="redirects"){let o={};if(r.siteUrl!==void 0&&(o.siteUrl=r.siteUrl||null),r.allow!==void 0&&(o.uriAllowList=r.allow.split(",").map(a=>a.trim()).filter(Boolean)),Object.keys(o).length===0)throw new g("auth redirects needs --site-url and/or --allow URL,URL (an empty --allow= clears the list)");let s=await jr(e,o);w(s,()=>Us(s));return}if(t==="templates"){let o=await x("cloud-project-auth",{ref:e,action:"templates"});w(o,()=>bd(o.templates));return}if(t==="template"){let o=n[0];if(!o||!wd.includes(o))throw new g(`auth template needs a kind: ${wd.join(", ")}`);let s;if(n[1]==="reset")s={kind:o,reset:!0};else{if(!r.file)throw new g(`auth template ${o} needs --file body.html (and --subject), or "reset"`);let c=await ow(r.file,"utf8"),i=await x("cloud-project-auth",{ref:e,action:"templates"}),u=r.subject??i.templates.find(l=>l.kind===o)?.subject;s={kind:o,subject:u,body:c}}let a=await x("cloud-project-auth",{ref:e,action:"template",template:s});w(a,()=>{bd(a.templates),d(b(" Your auth service restarts with it within about a minute."))});return}throw new g(`unknown command: auth ${t}. auth [google [off] | anonymous on|off | redirects | templates | template KIND]`)}var wd=["confirmation","recovery","magic_link","invite","email_change"];function bd(e){process.stdout.write(`${W([["KIND","SOURCE","SUBJECT"],...e.map(t=>[t.kind,t.custom?O("yours"):"ours",t.subject])])}
|
|
537
|
+
`)}me();ue();Se();import{createHash as Ew}from"node:crypto";import{existsSync as Aw,readdirSync as Rw,readFileSync as Tw,statSync as Ow}from"node:fs";import{isAbsolute as Nw,join as Gs,relative as Iw,resolve as Cw}from"node:path";function lw(e){return[...e].sort((t,n)=>t.name<n.name?-1:t.name>n.name?1:0)}function Lr(e,t,n={}){let r=lw(e),o=new Map(r.map(u=>[u.name,u])),s=[];for(let u of t){let l=o.get(u.name);if(!l){s.push({code:"missing",name:u.name,reason:`${u.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}l.checksum!==u.checksum&&s.push({code:"changed",name:u.name,reason:`${u.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 a=t.map(u=>u.name).sort((u,l)=>u<l?-1:u>l?1:0)[t.length-1],c=new Set(t.map(u=>u.name)),i=[];for(let u of r)if(!c.has(u.name)){if(a!==void 0&&u.name<a&&!n.outOfOrder){s.push({code:"out-of-order",name:u.name,reason:`${u.name} sorts before ${a}, 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}i.push(u)}return s.length>0?{ok:!1,problems:s}:{ok:!0,pending:i}}function zt(e){return!/^[\t ]*--[\t ]*snoutdata:no-transaction[\t ]*$/im.test(e)}import{createHash as Fs,createHmac as Ur,pbkdf2Sync as cw,randomBytes as dw,timingSafeEqual as uw}from"node:crypto";import{readFileSync as pw}from"node:fs";import{homedir as mw}from"node:os";import{connect as fw,isIP as gw}from"node:net";import{connect as hw}from"node:tls";var yt=class extends Error{severity;code;detail;hint;position;where;constructor(t){super(t.get("M")??"the server reported an error"),this.name="PgServerError",this.severity=t.get("V")??t.get("S")??"ERROR",this.code=t.get("C")??"";let n=t.get("D"),r=t.get("H"),o=Number(t.get("P")),s=t.get("W");n&&(this.detail=n),r&&(this.hint=r),Number.isInteger(o)&&o>0&&(this.position=o),s&&(this.where=s)}},he=class extends Error{constructor(t){super(t),this.name="PgConnectError"}},yw=196608,ww=80877103;function kd(e,t){let n=Buffer.alloc(5);return n.write(e,0,"latin1"),n.writeInt32BE(t.length+4,1),Buffer.concat([n,t])}function ht(e){return Buffer.concat([Buffer.from(e,"utf8"),Buffer.from([0])])}function bw(e,t=0){let n=[],r=t;for(;r<e.length;){let o=e.indexOf(0,r);if(o<0||o===r)break;n.push(e.toString("utf8",r,o)),r=o+1}return n}function vd(e){let t=new Map,n=0;for(;n<e.length&&e[n]!==0;){let r=String.fromCharCode(e[n]),o=e.indexOf(0,n+1);if(o<0)break;t.set(r,e.toString("utf8",n+1,o)),n=o+1}return t}var Bs=class{constructor(t,n=dw(18).toString("base64")){this.password=t;this.nonce=n,this.firstBare=`n=,r=${this.nonce}`}nonce;firstBare;serverSignature=null;first(){return`n,,${this.firstBare}`}final(t){let n=new Map(t.split(",").map(A=>[A.slice(0,1),A.slice(2)])),r=n.get("r"),o=n.get("s"),s=Number(n.get("i"));if(!r||!r.startsWith(this.nonce)||r.length<=this.nonce.length)throw new he("the server answered the password exchange with a nonce that is not ours");if(!o||!Number.isInteger(s)||s<1)throw new he("the server answered the password exchange with no salt or iteration count");let a=cw(this.password.normalize("NFKC"),Buffer.from(o,"base64"),s,32,"sha256"),c=Ur("sha256",a).update("Client Key").digest(),i=Fs("sha256").update(c).digest(),u=`c=biws,r=${r}`,l=`${this.firstBare},${t},${u}`,m=Ur("sha256",i).update(l).digest(),h=Buffer.alloc(c.length);for(let A=0;A<h.length;A++)h[A]=c[A]^m[A];let P=Ur("sha256",a).update("Server Key").digest();return this.serverSignature=Ur("sha256",P).update(l).digest(),`${u},p=${h.toString("base64")}`}verify(t){let n=/(?:^|,)e=([^,]*)/.exec(t);if(n)throw new he(`the server refused the password exchange: ${n[1]}`);let r=/(?:^|,)v=([^,]*)/.exec(t),o=r?Buffer.from(r[1],"base64"):Buffer.alloc(0);if(!this.serverSignature||o.length!==this.serverSignature.length||!uw(o,this.serverSignature))throw new he("the server could not prove it knows the password, so this is not the database it claims to be")}},Hs=class{buffer=Buffer.alloc(0);queue=[];waiting=null;failure=null;constructor(t){t.on("data",n=>this.take(n)),t.on("error",n=>this.fail(new he(n.message))),t.on("close",()=>this.fail(new he("the server closed the connection")))}take(t){for(this.buffer=this.buffer.length===0?t:Buffer.concat([this.buffer,t]);this.buffer.length>=5;){let n=this.buffer.readInt32BE(1);if(n<4){this.fail(new he("the server sent a message this client cannot read"));return}if(this.buffer.length<n+1)break;let r={type:String.fromCharCode(this.buffer[0]),body:this.buffer.subarray(5,n+1)};if(this.buffer=this.buffer.subarray(n+1),this.waiting){let{resolve:o}=this.waiting;this.waiting=null,o(r)}else this.queue.push(r)}}fail(t){if(!this.failure&&(this.failure=t,this.waiting)){let{reject:n}=this.waiting;this.waiting=null,n(t)}}next(){let t=this.queue.shift();return t?Promise.resolve(t):this.failure?Promise.reject(this.failure):new Promise((n,r)=>{this.waiting={resolve:n,reject:r}})}};function Sw(e){return new Promise((t,n)=>{let r=c=>{if(a(),c.length!==1){n(new he("the server sent more than its answer to the TLS request, so the connection was closed"));return}t(String.fromCharCode(c[0]))},o=c=>{a(),n(new he(c.message))},s=()=>{a(),n(new he("the server closed the connection before TLS"))},a=()=>{e.off("data",r),e.off("error",o),e.off("close",s)};e.on("data",r),e.on("error",o),e.on("close",s)})}function _w(e,t){return new Promise((n,r)=>{let o=fw({host:e,port:t}),s=a=>r(new he(`could not connect to ${e}:${t}: ${a.message}`));o.once("connect",()=>{o.off("error",s),n(o)}),o.once("error",s)})}async function kw(e,t,n,r){let o=await _w(e.host,e.port);if(r(o),t==="disable")return o;let s=Buffer.alloc(8);if(s.writeInt32BE(8,0),s.writeInt32BE(ww,4),o.write(s),await Sw(o)!=="S"){if(t==="allow"||t==="prefer")return o;throw new he(`${e.host} does not accept TLS, and this connection requires it (sslmode ${t})`)}let c=t==="verify-ca"||t==="verify-full";return new Promise((i,u)=>{let l=hw({socket:o,host:e.host,...gw(e.host)===0?{servername:e.host}:{},rejectUnauthorized:c,...c&&n?{ca:pw(n,"utf8")}:{},...t==="verify-ca"?{checkServerIdentity:()=>{}}:{}});r(l),l.once("secureConnect",()=>{l.off("error",m),i(l)});let m=h=>u(new he(`TLS to ${e.host} failed: ${h.message}`));l.once("error",m)})}var Fr=class e{constructor(t,n){this.socket=t;this.reader=n}static async open(t,n={}){let r=n.env??process.env,o=vn(t.ssl,r,n.home??mw()),s=o.rootCert??(r.PGSSLROOTCERT||void 0),a=o.mode==="require"&&s?"verify-ca":o.mode,c=n.connectTimeoutMs??12e4,i=[],u=()=>{for(let h of i)h.destroy()},l,m=new Promise((h,P)=>{l=setTimeout(()=>{u(),P(new he(`${t.host}:${t.port} did not answer within ${Math.round(c/1e3)} seconds`))},c)});try{return await Promise.race([(async()=>{let h=await kw(t,a,s,A=>i.push(A)),P=new e(h,new Hs(h));return await P.startup(t),P})(),m])}catch(h){throw u(),h}finally{clearTimeout(l)}}send(t,n){this.socket.write(kd(t,n))}async startup(t){let n=Buffer.concat([...[["user",t.user],["database",t.database],["application_name","snoutdata"],["client_encoding","UTF8"]].flatMap(([s,a])=>[ht(s),ht(a)]),Buffer.from([0])]),r=Buffer.alloc(8);r.writeInt32BE(n.length+8,0),r.writeInt32BE(yw,4),this.socket.write(Buffer.concat([r,n]));let o=null;for(;;){let s=await this.reader.next();switch(s.type){case"R":{let a=s.body.readInt32BE(0);if(a===0)break;if(a===3)this.send("p",ht(t.password));else if(a===5){let c=Fs("md5").update(t.password+t.user).digest("hex"),i=Fs("md5").update(Buffer.concat([Buffer.from(c),s.body.subarray(4,8)])).digest("hex");this.send("p",ht(`md5${i}`))}else if(a===10){let c=bw(s.body,4);if(!c.includes("SCRAM-SHA-256"))throw new he(`the server asked for ${c.join(", ")||"a password exchange"}, which this client does not speak`);o=new Bs(t.password);let i=Buffer.from(o.first(),"utf8"),u=Buffer.alloc(4);u.writeInt32BE(i.length,0),this.send("p",Buffer.concat([ht("SCRAM-SHA-256"),u,i]))}else if(a===11&&o)this.send("p",Buffer.from(o.final(s.body.toString("utf8",4)),"utf8"));else if(a===12&&o)o.verify(s.body.toString("utf8",4));else throw new he(`the server asked for an authentication method (${a}) this client does not speak`);break}case"E":throw new yt(vd(s.body));case"Z":return;default:break}}}async query(t,n){this.send("Q",ht(t));let r=[],o=null,s=null;for(;;){let a=await this.reader.next();switch(a.type){case"T":r=[];break;case"D":{let c=a.body.readInt16BE(0),i=[],u=2;for(let l=0;l<c;l++){let m=a.body.readInt32BE(u);u+=4,m<0?i.push(null):(i.push(a.body.toString("utf8",u,u+m)),u+=m)}r.push(i);break}case"C":o=a.body.toString("utf8",0,Math.max(0,a.body.indexOf(0)));break;case"E":s??=new yt(vd(a.body));break;case"G":n===void 0?this.send("f",ht("db push has no rows for this COPY: the lines after it, ending in \\., are what it reads")):(n.length>0&&this.send("d",Buffer.from(n,"utf8")),this.send("c",Buffer.alloc(0)));break;case"W":this.send("f",ht("COPY BOTH is not something a migration can do"));break;case"Z":if(s)throw s;return{rows:r,command:o};default:break}}}close(){this.socket.destroyed||this.socket.end(kd("X",Buffer.alloc(0)))}};function $d(e,t,n=1){let r=[`${e.severity}: ${e.message}`];if(e.position!==void 0){let o=[...t].slice(0,e.position-1),s=o.filter(u=>u===`
|
|
513
538
|
`).length,a=t.split(`
|
|
514
|
-
`)[s]??"",
|
|
515
|
-
`)+1),i=`LINE ${n+s}: `;r.push(`${i}${a.replace(/\r$/,"")}`,`${" ".repeat(i.length+
|
|
516
|
-
`)}var
|
|
517
|
-
`,o),
|
|
518
|
-
`,
|
|
519
|
-
`)}
|
|
520
|
-
`||
|
|
521
|
-
`,o));continue}if(
|
|
522
|
-
`,o);n.push({line:s,text:e.slice(o,
|
|
539
|
+
`)[s]??"",c=o.length-(o.lastIndexOf(`
|
|
540
|
+
`)+1),i=`LINE ${n+s}: `;r.push(`${i}${a.replace(/\r$/,"")}`,`${" ".repeat(i.length+c)}^`)}return e.detail&&r.push(`DETAIL: ${e.detail}`),e.hint&&r.push(`HINT: ${e.hint}`),e.where&&r.push(`CONTEXT: ${e.where}`),r.join(`
|
|
541
|
+
`)}var vw=/[A-Za-z0-9_$\u0080-]/,$w=/[A-Za-z_\u0080-]/,Pw=/^\$(?:[A-Za-z_\u0080-][A-Za-z0-9_\u0080-]*)?\$/;function xw(e){let t=Ks(e);return/^\s*copy\b/i.test(t)&&/\bfrom\s+stdin\b/i.test(t)}function Pd(e){let t=Ks(e).trim();if(/^rollback\s+(?:(?:work|transaction)\s+)?to\b/i.test(t))return null;let n=/^(commit|end|rollback|abort|prepare\s+transaction)\b/i.exec(t);return n?n[1].toUpperCase().replace(/\s+/," "):null}function Ks(e){return e.replace(/\/\*[\s\S]*?\*\//g," ").replace(/--[^\n]*/g," ")}function xd(e){let t=[],n=[],r=e.length,o=0,s=1,a=-1,c=1,i=0,u=0,l=[],m=0,h=()=>{a<0&&(a=o,c=s)},P=()=>{a=-1,i=0,u=0,l=[],m=0},A=j=>{let be=Math.min(r,o+j);for(let Z=o;Z<be;Z++)e.charCodeAt(Z)===10&&s++;o=be},ee=j=>A(j<0?r-o:j-o),z=null,le=()=>{let j=a>=0?e.slice(a,o).trim():"";if(Ks(j).replace(/;/g,"").trim()!=="")if(!xw(j))t.push({sql:j,line:c});else{let be=e.indexOf(`
|
|
542
|
+
`,o),Z=[];if(be>=0){let ne=be+1,Ln=s+1;for(;ne<r;){let $t=e.indexOf(`
|
|
543
|
+
`,ne),Un=e.slice(ne,$t<0?r:$t).replace(/\r$/,"");if(ne=$t<0?r:$t+1,Ln++,Un==="\\.")break;Z.push(`${Un}
|
|
544
|
+
`)}z={from:be+1,to:ne,line:Ln}}t.push({sql:j,line:c,copyData:Z.join("")})}P()};for(;o<r;){if(z&&o>=z.from){o=z.to,s=z.line,z=null;continue}let j=e[o],be=e[o+1];if(j===`
|
|
545
|
+
`||j===" "||j===" "||j==="\r"||j==="\f"||j==="\v"){A(1);continue}if(j==="-"&&be==="-"){ee(e.indexOf(`
|
|
546
|
+
`,o));continue}if(j==="/"&&be==="*"){h();let Z=0;for(;o<r;)if(e[o]==="/"&&e[o+1]==="*")Z++,A(2);else if(e[o]==="*"&&e[o+1]==="/"){if(Z--,A(2),Z===0)break}else A(1);continue}if(j==="'"){h(),C(!1);continue}if(j==='"'){for(h(),A(1);;){let Z=e.indexOf('"',o);if(Z<0){ee(-1);break}if(ee(Z+1),e[o]==='"'){A(1);continue}break}continue}if(j==="$"){h();let Z=Pw.exec(e.slice(o,o+64))?.[0];if(Z){A(Z.length);let ne=e.indexOf(Z,o);ee(ne<0?-1:ne+Z.length)}else A(1);continue}if(j==="\\"){let Z=e.indexOf(`
|
|
547
|
+
`,o);n.push({line:s,text:e.slice(o,Z<0?r:Z).replace(/\r$/,"").trimEnd()}),ee(Z);continue}if(j==="("){h(),i++,A(1);continue}if(j===")"){h(),i>0&&i--,A(1);continue}if(j===";"){h(),A(1),i===0&&u===0&&le();continue}if($w.test(j)){h();let Z=o+1;for(;Z<r&&vw.test(e[Z]);)Z++;let ne=e.slice(o,Z);if(A(Z-o),e[o]==="'"){C(ne==="e"||ne==="E");continue}ce(ne.toLowerCase());continue}if(j>="0"&&j<="9"){h();let Z=o+1;for(;Z<r&&/[0-9A-Za-z_.]/.test(e[Z]);)Z++;A(Z-o);continue}h(),A(1)}return le(),{statements:t,metaCommands:n};function C(j){for(A(1);o<r;){let be=e[o];if(j&&be==="\\"){A(2);continue}if(be==="'"){if(A(1),e[o]==="'"){A(1);continue}return}A(1)}}function ce(j){m<4&&["create","function","procedure","or","replace"].includes(j)&&(l[m]=j[0]),m++,!(!(l[0]==="c"&&(l[1]==="f"||l[1]==="p"||l[1]==="o"&&l[2]==="r"&&(l[3]==="f"||l[3]==="p")))||i!==0)&&(j==="begin"?u++:j==="case"?u>=1&&u++:j==="end"&&u>0&&u--)}}function Ad(e){let t=Iw(process.cwd(),e);return t&&!t.startsWith("..")?t:e}var Br="_snoutdata_migrations",Dw=`create table if not exists ${Br} (
|
|
523
548
|
name text primary key,
|
|
524
549
|
checksum text not null,
|
|
525
550
|
applied_at timestamptz not null default now()
|
|
526
|
-
);`;function
|
|
527
|
-
`);n.problems.every(o=>o.code==="psql-command"||o.code==="ends-transaction")&&
|
|
528
|
-
${r}`,{ref:e,problems:n.problems}),
|
|
529
|
-
${r}`,{ref:e,problems:n.problems})}
|
|
530
|
-
${n.error??""}`.trimEnd(),{ref:e,failed:n.failed})}return
|
|
531
|
-
`),(!n||!r)&&d(
|
|
551
|
+
);`;function Mw(e){if(!Aw(e)||!Ow(e).isDirectory())throw new Error(`no migrations folder at ${Ad(e)}. Put your .sql files there, or pass --dir.`);return Rw(e).filter(t=>t.endsWith(".sql")).map(t=>{let n=Gs(e,t),r=Tw(n,"utf8");return{name:t,path:n,sql:r,checksum:Ew("sha256").update(r,"utf8").digest("hex")}})}var jw={connection:e=>Ce(e),open:e=>Fr.open(e)};function Rd(e,t){throw e instanceof he?new T("network",`Could not reach ${t}'s database: ${e.message}.`):e instanceof yt?new T("failed",`${t}'s database refused the connection: ${e.message}`):e}async function Td(e,t,n){try{return await e.open(t)}catch(r){return Rd(r,n)}}async function Lw(e,t,n){let r=await Td(e,t,n);try{await r.query(Dw);let{rows:o}=await r.query(`select name, checksum from ${Br};`);return o.map(([s,a])=>({name:s??"",checksum:a??""}))}catch(o){if(o instanceof yt)throw new T("failed",`Could not read ${Br} on ${n}: ${o.message}`);return Rd(o,n)}finally{r.close()}}async function Uw(e,t,n){for(let r of t)try{await e.query(r.sql,r.copyData)}catch(o){if(o instanceof yt)return`${r.line>0?`${n}:${r.line}: `:""}${$d(o,r.sql,r.line>0?r.line:1)}`;if(o instanceof he)return`the connection was lost: ${o.message}`;throw o}return null}function Ed(e){return`'${e.replace(/'/g,"''")}'`}async function Od(e,t={}){let n=await qs(e,t,r=>d(r));if(!n.ok){if(n.problems){let r=n.problems.map(o=>` ${o.reason}`).join(`
|
|
552
|
+
`);n.problems.every(o=>o.code==="psql-command"||o.code==="ends-transaction")&&V("failed",`Refused, and nothing ran:
|
|
553
|
+
${r}`,{ref:e,problems:n.problems}),V("conflict",`Refused. This folder and ${e} do not agree:
|
|
554
|
+
${r}`,{ref:e,problems:n.problems})}V("failed",`${n.failed} failed, and nothing after it was tried.
|
|
555
|
+
${n.error??""}`.trimEnd(),{ref:e,failed:n.failed})}return w(n,()=>{if(n.wouldApply){d(`Would apply ${n.wouldApply.length} migration${n.wouldApply.length===1?"":"s"} to ${e}:`);for(let r of n.wouldApply)d(` ${r}`);return}if(n.upToDate){d(`${e} is up to date.`);return}d(`Applied ${n.applied.length} migration${n.applied.length===1?"":"s"} to ${e}.`)}),0}async function qs(e,t={},n=()=>{},r=jw){let o=Cw(t.dir===void 0?Gs(process.cwd(),"migrations"):Nw(t.dir)?t.dir:Gs(process.cwd(),t.dir)),s=[];function a(C){s.push(C),n(C)}let c=Mw(o);if(c.length===0)return a(`No .sql files in ${Ad(o)}.`),{ref:e,directory:o,ok:!0,applied:[],upToDate:!0,notes:s};let i=await r.connection(e),u={host:i.host,port:i.port,database:i.database,user:i.user,password:i.password,ssl:i.ssl};i.wakesInstantly||a("This project is paused; the connection will wake it, which takes a few seconds.");let l=await Lw(r,u,e),m=Lr(c,l,{outOfOrder:t.outOfOrder});if(!m.ok)return{ref:e,directory:o,ok:!1,applied:[],problems:[...m.problems],notes:s};let h=new Map(c.map(C=>[C.name,C])),P=m.pending.map(C=>h.get(C.name)),A=new Map(P.map(C=>[C.name,xd(C.sql)])),ee=P.flatMap(C=>A.get(C.name).metaCommands.map(ce=>({code:"psql-command",name:C.name,reason:`${C.name} line ${ce.line} is a psql command (${ce.text.length>60?`${ce.text.slice(0,57)}...`:ce.text}), which only psql runs. db push sends SQL to the database itself, so put the SQL it stands for in the file instead.`}))),z=P.filter(C=>zt(C.sql)).flatMap(C=>A.get(C.name).statements.flatMap(ce=>{let j=Pd(ce.sql);return j?[{code:"ends-transaction",name:C.name,reason:`${C.name} line ${ce.line} ends the transaction itself (${j}). db push already runs each file in one transaction and records it there, so a ${j} would keep what ran before it if a later statement failed. Take it out, or put -- snoutdata:no-transaction at the top of the file to run it exactly as written.`}]:[]}));if(ee.length>0||z.length>0)return{ref:e,directory:o,ok:!1,applied:[],problems:[...ee,...z],notes:s};if(P.length===0)return a(`${e} is up to date: ${l.length} migration${l.length===1?"":"s"} already applied.`),{ref:e,directory:o,ok:!0,applied:[],upToDate:!0,notes:s};if(t.dryRun){for(let C of P)zt(C.sql)||a(`${C.name} would run outside a transaction.`);return{ref:e,directory:o,ok:!0,applied:[],wouldApply:P.map(C=>C.name),notes:s}}let le=[];for(let C of P){let ce=`insert into ${Br} (name, checksum) values (${Ed(C.name)}, ${Ed(C.checksum)});`,j=zt(C.sql);j||a(`${C.name} runs outside a transaction, so an interruption can leave it half applied.`);let be=[...j?[{sql:"begin;",line:0}]:[],...A.get(C.name).statements,{sql:ce,line:0},...j?[{sql:"commit;",line:0}]:[]],Z=await Td(r,u,e),ne;try{ne=await Uw(Z,be,C.name)}finally{Z.close()}if(ne!==null)return j&&(a("It ran inside a transaction, so nothing from it is in the database."),/cannot run inside a transaction block/i.test(ne)&&a("Put -- snoutdata:no-transaction at the top of the file to run it outside one.")),{ref:e,directory:o,ok:!1,applied:le,failed:C.name,error:ne,notes:s};le.push(C.name),a(` applied ${C.name}`)}return{ref:e,directory:o,ok:!0,applied:le,notes:s}}me();Ae();ue();import{readFile as Nd}from"node:fs/promises";function Ws(e){if(e==="apns"||e==="fcm")return e;throw new g("say which: apns (an Apple .p8) or fcm (a Firebase service account)")}async function Fw(e){let t=await x("cloud-project-push",{ref:e,credentials:{list:!0}});return{ref:e,credentials:t??{}}}async function Id(e){if(!e.p8||!e.keyId||!e.teamId||!e.topic)throw new g("push credentials set apns needs --p8 AuthKey.p8 --key-id ID --team-id ID --topic com.example.app");if(e.environment!==void 0&&e.environment!=="production"&&e.environment!=="sandbox")throw new g("--environment is production or sandbox; leave it out for a key that serves both");return{topic:e.topic,keys:[{p8:await Nd(e.p8,"utf8"),key_id:e.keyId,team_id:e.teamId,environment:e.environment??null}]}}async function Cd(e){if(!e)throw new g("push credentials set fcm needs --file service-account.json");return{service_account:await Nd(e,"utf8")}}async function Bw(e,t,n){let r=await x("cloud-project-push",{ref:e,credentials:{set:{kind:t,body:n}}});return{ref:e,kind:t,summary:r}}async function Hw(e,t){return await x("cloud-project-push",{ref:e,credentials:{remove:t}}),{ref:e,kind:t,removed:!0}}async function Dd(e){let t=await Fw(e);w(t,()=>{let{apns:n,fcm:r,vapid:o}=t.credentials;process.stdout.write(`${W([["TRANSPORT","SET",""],["web push",o?"yes":"starting",o?.public_key?`public key ${o.public_key.slice(0,16)}\u2026`:""],["apns",n?"yes":"no",n?`${n.topic}, key ${n.keys?.map(s=>s.key_id).join(", ")}`:""],["fcm",r?"yes":"no",r?`${r.project_id} as ${r.client_email}`:""]])}
|
|
556
|
+
`),(!n||!r)&&d(b(` snoutdata push credentials set ${n?"fcm --file service-account.json":"apns --p8 AuthKey.p8 --key-id ID --team-id ID --topic com.example.app"} --ref ${e}`))})}async function Md(e,t,n){let r=await Bw(e,t,n);w(r,()=>d(`${t==="apns"?"The Apple key signed":"Google accepted the service account"}, and it is stored in the project's own database.`))}async function jd(e,t){let n=await Hw(e,t);w(n,()=>d(`${t} credentials removed. Devices on it stop receiving until new ones are set.`))}me();Se();ue();import{spawn as nb}from"node:child_process";import{writeFileSync as rb}from"node:fs";import{isAbsolute as ob,resolve as sb}from"node:path";var Gw=" ";function J(e){return Gw.repeat(e)}function xe(e){return JSON.stringify(e)}function Xe(e){return/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(e)?e:xe(e)}function Rt(e){return[...e].sort((t,n)=>t.name<n.name?-1:t.name>n.name?1:0)}function qw(e){let t=(e??"").trim(),n=!1;for(;t.endsWith("[]");)n=!0,t=t.slice(0,-2).trim();t.startsWith("_")&&t.length>1&&(n=!0,t=t.slice(1));let r=t.indexOf("(");return r>0&&(t=t.slice(0,r)),{base:t.trim().toLowerCase(),array:n}}var Ww={smallint:"number",int2:"number",integer:"number",int:"number",int4:"number",bigint:"number",int8:"number",decimal:"number",numeric:"number",number:"number",real:"number",float:"number",float4:"number",float8:"number","double precision":"number",double:"number",binary_float:"number",binary_double:"number",smallserial:"number",serial:"number",bigserial:"number",oid:"number",smallmoney:"number",money:"number",bool:"boolean",boolean:"boolean",json:"json",jsonb:"json",variant:"json",object:"json",char:"string",character:"string","character varying":"string",varchar:"string",varchar2:"string",nvarchar:"string",nvarchar2:"string",nchar:"string",text:"string",ntext:"string",citext:"string",clob:"string",nclob:"string",longtext:"string",mediumtext:"string",tinytext:"string",uuid:"string",uniqueidentifier:"string",date:"string",time:"string","time without time zone":"string","time with time zone":"string",timetz:"string",timestamp:"string","timestamp without time zone":"string","timestamp with time zone":"string",timestamptz:"string",datetime:"string",datetime2:"string",datetimeoffset:"string",smalldatetime:"string",timestamp_ntz:"string",timestamp_ltz:"string",timestamp_tz:"string",interval:"string",year:"string",bytea:"string",blob:"string",longblob:"string",mediumblob:"string",tinyblob:"string",binary:"string",varbinary:"string",raw:"string",xml:"string",inet:"string",cidr:"string",macaddr:"string",macaddr8:"string",tsvector:"string",tsquery:"string",name:"string",bit:"string",varbit:"string",tinyint:"number",enum:"string",set:"string"},zw={sqlserver:{bit:"boolean"},"mysql-aurora":{bit:"number"},mariadb:{bit:"number"},sqlite:{bit:"number"},snowflake:{array:"json"}};function Jw(e,t){let n=zw[e]?.[t];return n||(Ww[t]??"unknown")}function Hr(e,t){let{base:n,array:r}=qw(t),o=e.enumRefs.get(n);if(o)return r?`${o}[]`:o;if(n==="array")return"unknown[]";let s=Jw(e.driverId,n),a=s==="json"?"Json":s;return r?`${a}[]`:a}function Vw(e){return e.nullable||e.autoIncrement===!0||e.default!==null&&e.default!==void 0}function zs(e,t,n,r,o=!1){if(o&&t.generatedAlways===!0)return`${J(n)}${Xe(t.name)}?: never`;let s=Hr(e,t.dataType),a=t.nullable?`${s} | null`:s;return`${J(n)}${Xe(t.name)}${r?"?":""}: ${a}`}function Yw(e,t){let n=[...t.columns].sort(),r=o=>{let s=[...o].sort();return s.length===n.length&&s.every((a,c)=>a===n[c])};return e.primaryKey.length>0&&r(e.primaryKey)?!0:(e.indexes??[]).some(o=>o.unique&&r(o.columns))}function Xw(e,t){let n=[],r=[...e.foreignKeys].sort((o,s)=>o.constraintName<s.constraintName?-1:o.constraintName>s.constraintName?1:0);if(r.length===0)return n.push(`${J(t)}Relationships: []`),n;n.push(`${J(t)}Relationships: [`);for(let o of r)n.push(`${J(t+1)}{`),n.push(`${J(t+2)}foreignKeyName: ${xe(o.constraintName)}`),n.push(`${J(t+2)}columns: [${o.columns.map(xe).join(", ")}]`),n.push(`${J(t+2)}isOneToOne: ${Yw(e,o)?"true":"false"}`),n.push(`${J(t+2)}referencedRelation: ${xe(o.referencedTable)}`),n.push(`${J(t+2)}referencedColumns: [${o.referencedColumns.map(xe).join(", ")}]`),n.push(`${J(t+1)}},`);return n.push(`${J(t)}]`),n}function Ld(e,t,n){let r=Rt(t.columns),o=[];o.push(`${J(n)}${Xe(t.name)}: {`),o.push(`${J(n+1)}Row: {`);for(let s of r)o.push(zs(e,s,n+2,!1));if(o.push(`${J(n+1)}}`),t.kind!=="view"){o.push(`${J(n+1)}Insert: {`);for(let s of r)o.push(zs(e,s,n+2,Vw(s),!0));o.push(`${J(n+1)}}`),o.push(`${J(n+1)}Update: {`);for(let s of r)o.push(zs(e,s,n+2,!0,!0));o.push(`${J(n+1)}}`)}return o.push(...Xw(t,n+1)),o.push(`${J(n)}}`),o}function En(e,t){return[`${J(t)}${e}: {`,`${J(t+1)}[_ in never]: never`,`${J(t)}}`]}function Zw(e,t){if(t.kind==="procedure"||!t.returnType)return"undefined";let n=t.returnType.trim(),r=n.toLowerCase();if(r==="void"||r==="")return"undefined";if(r.startsWith("table("))return"Record<string, unknown>[]";if(r.startsWith("setof ")){let s=n.slice(6).trim();if(s.toLowerCase()==="record")return"Record<string, unknown>[]";let a=e.rowRefs.get(s.toLowerCase());return a?`${a}[]`:`${Hr(e,s)}[]`}return r==="record"||r==="trigger"||r==="event_trigger"?"unknown":e.rowRefs.get(r)??Hr(e,n)}function Qw(e,t,n){let r=[],o=Rt((t.parameters??[]).filter(s=>s.name.length>0&&(s.mode??"IN").toUpperCase()!=="OUT").map(s=>({name:s.name,dataType:s.dataType})));if(r.push(`${J(n)}${Xe(t.name)}: {`),o.length===0)r.push(`${J(n+1)}Args: never`);else{r.push(`${J(n+1)}Args: {`);for(let s of o)r.push(`${J(n+2)}${Xe(s.name)}: ${Hr(e,s.dataType)}`);r.push(`${J(n+1)}}`)}return r.push(`${J(n+1)}Returns: ${Zw(e,t)}`),r.push(`${J(n)}}`),r}function Jt(e,t){return e.schema&&e.schema.length>0?e.schema:t}function Ud(e,t={}){let n=t.defaultSchema??"public",r=t.driverId??"postgres",o=t.enums??[],s=new Map;for(let m of o){let h=m.schema&&m.schema.length>0?m.schema:n;s.set(m.name.toLowerCase(),`Database[${xe(h)}][${xe("Enums")}][${xe(m.name)}]`)}let a=new Map;for(let m of e.tables){let h=Jt(m,n),P=m.kind==="view"?"Views":"Tables";a.set(m.name.toLowerCase(),`Database[${xe(h)}][${xe(P)}][${xe(m.name)}][${xe("Row")}]`)}let c={driverId:r,defaultSchema:n,enumRefs:s,rowRefs:a},i=new Set([n]);for(let m of e.tables)i.add(Jt(m,n));for(let m of e.routines??[])i.add(Jt(m,n));for(let m of o)i.add(m.schema&&m.schema.length>0?m.schema:n);let u=[...i].sort(),l=[];l.push("export type Json ="),l.push(" | string"),l.push(" | number"),l.push(" | boolean"),l.push(" | null"),l.push(" | { [key: string]: Json | undefined }"),l.push(" | Json[]"),l.push(""),l.push("export type Database = {");for(let m of u){let h=Rt(e.tables.filter(z=>Jt(z,n)===m&&z.kind!=="view")),P=Rt(e.tables.filter(z=>Jt(z,n)===m&&z.kind==="view")),A=Rt((e.routines??[]).filter(z=>Jt(z,n)===m)),ee=Rt(o.filter(z=>(z.schema&&z.schema.length>0?z.schema:n)===m));if(l.push(`${J(1)}${Xe(m)}: {`),h.length===0)l.push(...En("Tables",2));else{l.push(`${J(2)}Tables: {`);for(let z of h)l.push(...Ld(c,z,3));l.push(`${J(2)}}`)}if(P.length===0)l.push(...En("Views",2));else{l.push(`${J(2)}Views: {`);for(let z of P)l.push(...Ld(c,z,3));l.push(`${J(2)}}`)}if(A.length===0)l.push(...En("Functions",2));else{l.push(`${J(2)}Functions: {`);for(let z of A)l.push(...Qw(c,z,3));l.push(`${J(2)}}`)}if(ee.length===0)l.push(...En("Enums",2));else{l.push(`${J(2)}Enums: {`);for(let z of ee)l.push(`${J(3)}${Xe(z.name)}: ${z.values.map(xe).join(" | ")}`);l.push(`${J(2)}}`)}l.push(...En("CompositeTypes",2)),l.push(`${J(1)}}`)}return l.push("}"),t.helpers!==!1&&(l.push(""),l.push(tb(n))),t.constants!==!1&&(l.push(""),l.push(eb(u,o,n))),`${l.join(`
|
|
532
557
|
`)}
|
|
533
|
-
`}function
|
|
534
|
-
`)}function
|
|
558
|
+
`}function eb(e,t,n){let r=["export const Constants = {"];for(let o of e){let s=Rt(t.filter(a=>(a.schema&&a.schema.length>0?a.schema:n)===o));if(s.length===0){r.push(`${J(1)}${Xe(o)}: {`),r.push(`${J(2)}Enums: {},`),r.push(`${J(1)}},`);continue}r.push(`${J(1)}${Xe(o)}: {`),r.push(`${J(2)}Enums: {`);for(let a of s)r.push(`${J(3)}${Xe(a.name)}: [${a.values.map(xe).join(", ")}],`);r.push(`${J(2)}},`),r.push(`${J(1)}},`)}return r.push("} as const"),r.join(`
|
|
559
|
+
`)}function tb(e){return`type DefaultSchemaName = Extract<keyof Database, ${xe(e)}>
|
|
535
560
|
|
|
536
561
|
type InSchema = { schema: keyof Database }
|
|
537
562
|
|
|
@@ -568,7 +593,7 @@ export type Enums<
|
|
|
568
593
|
export type CompositeTypes<
|
|
569
594
|
Choice extends keyof Database[DefaultSchemaName]["CompositeTypes"] | InSchema,
|
|
570
595
|
Name extends Choice extends InSchema ? keyof Database[Choice["schema"]]["CompositeTypes"] : never = never,
|
|
571
|
-
> = Lookup<Database[SchemaOf<Choice>]["CompositeTypes"], NameOf<Choice, Name>>`}function
|
|
596
|
+
> = Lookup<Database[SchemaOf<Choice>]["CompositeTypes"], NameOf<Choice, Name>>`}function ab(e){let t=vn(e.ssl);return{host:e.host,port:e.port,database:e.database,user:e.user,password:e.password,sslMode:t.mode,...t.rootCert?{sslRootCert:t.rootCert}:{},label:e.ref}}function Kr(e){let t;try{t=new URL(e)}catch{throw new Error(`--db-url is not a URL: ${e}`)}if(t.protocol!=="postgres:"&&t.protocol!=="postgresql:")throw new Error(`--db-url must be a postgres:// URL, not ${t.protocol}//`);let n=decodeURIComponent(t.pathname.replace(/^\//,""));if(!n)throw new Error("--db-url has no database name in its path");let r=t.searchParams.get("sslmode")??"prefer";return{host:t.hostname||"127.0.0.1",port:t.port?Number(t.port):5432,database:n,user:decodeURIComponent(t.username)||"postgres",password:decodeURIComponent(t.password),sslMode:r,label:`${t.hostname||"127.0.0.1"}/${n}`}}function Js(e,t){return new Promise(n=>{let r=nb("psql",["--host",e.host,"--port",String(e.port),"--username",e.user,"--dbname",e.database,"--no-psqlrc","-v","ON_ERROR_STOP=1","-tA"],{stdio:["pipe","pipe","pipe"],env:{...process.env,PGPASSWORD:e.password,PGSSLMODE:e.sslMode,...e.sslRootCert?{PGSSLROOTCERT:e.sslRootCert}:{}}}),o="",s="";r.stdout.on("data",a=>o+=String(a)),r.stderr.on("data",a=>s+=String(a)),r.on("error",a=>{n({code:127,out:"",err:a.code==="ENOENT"?"psql is not installed, and gen types needs it to read the schema. `snoutdata db url` prints a connection string for any client.":String(a)})}),r.on("close",a=>n({code:a??0,out:o,err:s})),r.stdin.end(t)})}var ib=`
|
|
572
597
|
with wanted as (select unnest($SCHEMAS$::text[]) as nspname)
|
|
573
598
|
select json_build_object(
|
|
574
599
|
'tables', coalesce((
|
|
@@ -672,40 +697,40 @@ select json_build_object(
|
|
|
672
697
|
where t.typtype = 'e' and n.nspname in (select nspname from wanted)
|
|
673
698
|
), '[]'::json)
|
|
674
699
|
)::text
|
|
675
|
-
`;function
|
|
700
|
+
`;function lb(e){return`'{${e.map(n=>`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(",").replace(/'/g,"''")}}'`}function cb(e,t,n=0){let r=e.tables.map(a=>{let c=a.primaryKey??[],i=new Set(c),u=a.columns.map(l=>({name:l.name,dataType:l.dataType,nullable:l.nullable,isPrimaryKey:i.has(l.name),default:l.default,autoIncrement:l.autoIncrement,...l.generatedAlways===!0?{generatedAlways:!0}:{}}));return{name:a.name,schema:a.schema,kind:a.kind,columns:u,primaryKey:c,foreignKeys:(a.foreignKeys??[]).map(l=>({constraintName:l.constraintName,columns:l.columns??[],referencedTable:l.referencedTable,referencedColumns:l.referencedColumns??[]})),indexes:(a.indexes??[]).filter(l=>Array.isArray(l.columns)&&l.columns.every(m=>typeof m=="string")).map(l=>({name:l.name,unique:l.unique,columns:l.columns??[]}))}}),o=e.routines.map(a=>({name:a.name,schema:a.schema,kind:a.kind,returnType:a.returnType??void 0,parameters:(a.parameters??[]).map(c=>({name:c.name,dataType:c.dataType,mode:c.mode}))})),s=e.enums.map(a=>({name:a.name,schema:a.schema,values:a.values??[]}));return{schema:{database:t,tables:r,routines:o,fetchedAt:n},enums:s}}var db=`select exists (
|
|
676
701
|
select from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
677
702
|
where n.nspname = 'lepis' and c.relname = 'relation'
|
|
678
|
-
)::text`,
|
|
703
|
+
)::text`,ub=`select coalesce(json_agg(json_build_object(
|
|
679
704
|
'schema', schema_name, 'table', table_name, 'kind', kind, 'keyColumn', key_column
|
|
680
|
-
) order by schema_name, table_name), '[]'::json)::text from lepis.relation`;function
|
|
681
|
-
`,{encoding:"utf8",mode:384}),n}var
|
|
682
|
-
`).map(
|
|
683
|
-
${i.problems.map(
|
|
684
|
-
`)}`,{problems:i.problems});let u=new Map(o.map(
|
|
685
|
-
${
|
|
686
|
-
${
|
|
705
|
+
) order by schema_name, table_name), '[]'::json)::text from lepis.relation`;function pb(e){let t=[];for(let n of e.split(/\r?\n/)){let r=n.trim();if(r)try{t.push(JSON.parse(r))}catch{return null}}return t.length===0?null:mb(t)}function mb(e){let t=(n,r)=>{let o=new Set,s=[];for(let a of n)for(let c of a??[]){let i=r(c);o.has(i)||(o.add(i),s.push(c))}return s};return{tables:t(e.map(n=>n.tables),n=>`${n.schema}.${n.name}`),routines:t(e.map(n=>n.routines),n=>`${n.schema}.${n.name}(${(n.parameters??[]).map(r=>`${r.mode} ${r.dataType}`).join(",")})`),enums:t(e.map(n=>n.enums),n=>`${n.schema}.${n.name}`)}}async function fb(e,t){let n=await e(db);if(n.code!==0||n.out.trim()!=="true")return[];let r=await e(ub);if(r.code!==0)return[];try{let o=JSON.parse(r.out.trim()),s=new Set(t);return o.filter(a=>s.has(a.schema)).map(a=>({schema:a.schema,table:a.table,kind:a.kind,keyColumn:a.keyColumn??null}))}catch{return[]}}async function gb(e){let t=e.schemas&&e.schemas.length>0?[...e.schemas]:["public"],n=e.dbUrl?Kr(e.dbUrl):ab(await Ce(hb(e))),r=!1,o=async A=>{if(e.sql)return e.sql(A);if(r&&e.ref)return Fd(e.ref,A);let ee=await Js(n,A);return ee.code===127&&!e.dbUrl&&e.ref?(r=!0,Fd(e.ref,A)):ee},s=ib.replace("$SCHEMAS$",lb(t)),a=await o(s);a.code===127&&V("tool-missing",a.err.trim()),a.code!==0&&V("failed",a.err.trim()||`could not read the schema of ${n.label}`);let c=a.out.trim();c||V("failed",`${n.label} returned nothing for the schema query`);let i=pb(c);i||V("failed",`could not read the schema of ${n.label}: the answer was not JSON`);let u=await fb(o,t),{schema:l,enums:m}=cb(i,n.database,Date.now()),h=Ud(l,{driverId:"postgres",defaultSchema:e.defaultSchema??t[0]??"public",enums:m}),P=null;return e.out&&(P=ob(e.out)?e.out:sb(process.cwd(),e.out),rb(P,h,"utf8")),{target:n.label,schemas:t,tables:l.tables.filter(A=>A.kind!=="view").length,views:l.tables.filter(A=>A.kind==="view").length,functions:(l.routines??[]).length,enums:m.length,path:P,distribution:u,types:h}}async function Fd(e,t,n=x){try{let r=await n("cloud-project-sql",{ref:e,sql:t});if(r.error)return{code:1,out:"",err:r.error};let o=Object.values(r.rows?.[0]??{})[0];return{code:0,out:typeof o=="string"?o:o==null?"":JSON.stringify(o),err:""}}catch(r){return{code:1,out:"",err:r instanceof Error?r.message:String(r)}}}function hb(e){if(!e.ref)throw new Error("no project: pass --ref, --db-url, set SNOUTDATA_PROJECT, or run `snoutdata link --ref <ref>`");return e.ref}async function Bd(e,t){let n=(e??"typescript").toLowerCase();n!=="typescript"&&n!=="ts"&&V("usage",`gen types does not know how to write ${e}. It writes typescript, which is the only language it claims.`);let r=await gb(t);return r.path&&d(`Wrote ${r.tables} table${r.tables===1?"":"s"}, ${r.views} view${r.views===1?"":"s"}, ${r.functions} function${r.functions===1?"":"s"} and ${r.enums} enum${r.enums===1?"":"s"} to ${r.path}.`),w(r,()=>{r.path||process.stdout.write(r.types)}),0}Se();import{createHash as p_}from"node:crypto";import{existsSync as Wa,mkdirSync as m_,readFileSync as za,readdirSync as f_,statSync as g_,writeFileSync as h_}from"node:fs";import{dirname as y_,isAbsolute as ym,join as Mn,resolve as wm}from"node:path";import{pathToFileURL as w_}from"node:url";ue();function b_(e){return/^[a-z][a-z0-9+.-]+:/i.test(e)?e:ym(e)||e.startsWith("./")||e.startsWith("../")?w_(wm(e)).href:e}function S_(){let e=process.env.SNOUTDATA_LOCAL_RUNTIME;return e?import(b_(e)):Promise.resolve().then(()=>(mm(),pm))}function __(e){let t=e instanceof Error?e.message:String(e),n=process.env.SNOUTDATA_LOCAL_RUNTIME;return n?`SNOUTDATA_LOCAL_RUNTIME points at ${n}, and it could not be loaded: ${t}`:`this build of the CLI has no local pod runtime in it, so \`snoutdata start\` cannot run. That is a packaging fault rather than anything you did: please report it. (${t})`}var fm="It is fetched from ghcr.io/snoutdata/snoutpod-postgres on the first `snoutdata start`, so this usually means that registry could not be reached from here.";async function gm(e,t){try{return await e.localImageFor(t)}catch(n){return n instanceof Error?n.message:String(n)}}async function jn(){let e;try{e=await S_()}catch(t){V("tool-missing",__(t))}return{ready:async t=>{let n=await gm(e,t);return typeof n=="string"?await e.podmanReady(fm)??n:e.podmanReady(fm,n.name)},ensureImage:async(t,n)=>{let r=await gm(e,t);return typeof r=="string"?r:e.ensureImage(n,void 0,r)},prepare:()=>e.ensureNetwork(),start:(t,n)=>e.startLocal(e.localRuntime(),t,{onProgress:n}),stop:t=>e.stopLocal(e.localRuntime(),t),status:t=>e.localStatus(e.localRuntime(),t),remove:(t,n)=>e.removeLocal(e.localRuntime(),t,n),uri:t=>e.localUri(t),psql:async(t,n)=>{let r=await e.localPsql(t,n);return{code:r.code,out:r.stdout,err:r.stderr}},mint:t=>({ref:e.newLocalRef(),port:t,ownerPassword:e.newLocalPassword(),adminPassword:e.newLocalPassword()})}}function bm(e){return Mn(e,".snoutdata","local.json")}function bo(e){let t=bm(e);if(!Wa(t))return null;try{let n=JSON.parse(za(t,"utf8"));return!n.ref||!n.port||!n.ownerPassword||!n.adminPassword?null:{ref:n.ref,port:n.port,ownerPassword:n.ownerPassword,adminPassword:n.adminPassword}}catch{return null}}function k_(e,t){let n=bm(e);return m_(y_(n),{recursive:!0}),h_(n,`${JSON.stringify(t,null,2)}
|
|
706
|
+
`,{encoding:"utf8",mode:384}),n}var v_=54322;async function Sm(e,t){let n=await e.ensureImage(t,o=>d(o));n&&V("tool-missing",n);let r=await e.ready(t);r&&V("tool-missing",r),await e.prepare()}function _m(e,t,n,r=Js){let o=!1;return async s=>{if(o)return e.psql(t,s);let a=await r(n,s);return a.code!==127?a:(o=!0,e.psql(t,s))}}async function $_(e,t={}){let n=t.cwd??process.cwd(),r=bo(n);await Sm(e,r?.ref??null);let o=r??e.mint(t.port??v_),s=r?null:k_(n,o),a=await e.start(o,m=>d(m)),c=e.uri(o);if(t.noMigrations)return{status:a,uri:c,applied:[],seeded:!1,created:s};let i=_m(e,o,Kr(c)),u=await P_(i,n,t),l=await x_(i,n,u.length>0||!!s);return{status:a,uri:c,applied:u,seeded:l,created:s}}async function P_(e,t,n){let r=wm(n.dir===void 0?Mn(t,"migrations"):ym(n.dir)?n.dir:Mn(t,n.dir));if(!Wa(r)||!g_(r).isDirectory())return[];let o=f_(r).filter(m=>m.endsWith(".sql")).map(m=>{let h=za(Mn(r,m),"utf8");return{name:m,sql:h,checksum:p_("sha256").update(h,"utf8").digest("hex")}});if(o.length===0)return[];let s=await e(E_);s.code!==0&&V(s.code===127?"tool-missing":"failed",s.err.trim()||`could not create ${Dn}`);let a=await e(`select name || E'\\t' || checksum from ${Dn};`);a.code!==0&&V("failed",a.err.trim()||`could not read ${Dn}`);let c=a.out.split(`
|
|
707
|
+
`).map(m=>m.trim()).filter(m=>m.length>0).map(m=>{let h=m.indexOf(" ");return{name:m.slice(0,h),checksum:m.slice(h+1)}}),i=Lr(o,c,{outOfOrder:n.outOfOrder});i.ok||V("conflict",`Refused. This folder and the local database do not agree:
|
|
708
|
+
${i.problems.map(m=>` ${m.reason}`).join(`
|
|
709
|
+
`)}`,{problems:i.problems});let u=new Map(o.map(m=>[m.name,m])),l=[];for(let m of i.pending){let h=u.get(m.name),P=`insert into ${Dn} (name, checksum) values (${hm(h.name)}, ${hm(h.checksum)});`,A=zt(h.sql);A||d(`${h.name} runs outside a transaction, so an interruption can leave it half applied.`);let ee=A?`begin;
|
|
710
|
+
${h.sql}
|
|
711
|
+
${P}
|
|
687
712
|
commit;
|
|
688
|
-
`:`${
|
|
689
|
-
${
|
|
690
|
-
`,
|
|
691
|
-
${
|
|
692
|
-
${o.err.trim()}`),d(" ran seed.sql"),!0}var
|
|
713
|
+
`:`${h.sql}
|
|
714
|
+
${P}
|
|
715
|
+
`,z=await e(ee);z.code!==0&&V("failed",`${h.name} failed, and nothing after it was tried.
|
|
716
|
+
${z.err.trim()}`,{failed:h.name,applied:l}),l.push(h.name),d(` applied ${h.name}`)}return l}async function x_(e,t,n){let r=Mn(t,"seed.sql");if(!n||!Wa(r))return!1;let o=await e(za(r,"utf8"));return o.code!==0&&V("failed",`seed.sql failed.
|
|
717
|
+
${o.err.trim()}`),d(" ran seed.sql"),!0}var Dn="_snoutdata_migrations",E_=`create table if not exists ${Dn} (
|
|
693
718
|
name text primary key,
|
|
694
719
|
checksum text not null,
|
|
695
720
|
applied_at timestamptz not null default now()
|
|
696
|
-
);`;function
|
|
697
|
-
`),d(
|
|
698
|
-
`)}),0}async function
|
|
721
|
+
);`;function hm(e){return`'${e.replace(/'/g,"''")}'`}function A_(e){if(e===null)return"unknown";let t=["B","kB","MB","GB"],n=e,r=0;for(;n>=1024&&r<t.length-1;)n/=1024,r+=1;return`${n<10&&r>0?n.toFixed(1):Math.round(n)} ${t[r]}`}async function km(e,t={}){let n=await $_(e,t);return w(n,()=>{n.created&&(d(""),d(`Wrote ${n.created}. It holds a password, so put .snoutdata/ in your .gitignore.`)),d(""),d(` ${O("Database")} running on port ${n.status.port}`),n.applied.length>0&&d(` ${O("Migrations")} ${n.applied.length} applied`),n.seeded&&d(` ${O("Seed")} seed.sql ran`),d(""),process.stdout.write(`${n.uri}
|
|
722
|
+
`),d(b(" snoutdata gen types typescript --local > database.types.ts")),d(b(" snoutdata stop"))}),0}async function vm(e,t={}){let n=t.cwd??process.cwd(),r=bo(n);r||V("not-found","there is no local database for this folder. `snoutdata start` makes one."),await Sm(e,r.ref);let o=await e.stop(r.ref);return w(o,()=>d("Stopped. The data is still there; `snoutdata start` brings it back.")),0}async function $m(e,t={}){let n=t.cwd??process.cwd(),r=bo(n);if(!r)return w({exists:!1,ref:null,status:"absent"},()=>d("No local database for this folder. `snoutdata start` makes one.")),0;let o=await e.ready(r.ref);if(o)return w({exists:!1,ref:r.ref,status:"unknown",reason:o},()=>d(o)),0;let s=await e.status(r);return w({...s,uri:s.uri},()=>{d(` ${O("Project")} ${s.ref}`),d(` ${O("State")} ${s.ready?"running":s.status}`),d(` ${O("Port")} ${s.port}`),d(` ${O("Size")} ${A_(s.databaseBytes)}`),s.services.length>0&&d(` ${O("Services")} ${s.services.join(", ")}`),s.uri&&process.stdout.write(`${s.uri}
|
|
723
|
+
`)}),0}async function Pm(e,t=process.cwd()){let n=bo(t);n||V("not-found","there is no local database for this folder. `snoutdata start` makes one.");let r=e.uri(n);return{dbUrl:r,sql:_m(e,n,Kr(r))}}me();ue();Xn();an();lt();import{resolve as xm}from"node:path";function R_(){let e=n=>st(n),t;return{shards:n=>{if(n!==void 0)return wn({project:n});if(!process.env.LEPIS_ADMIN_URL)throw new Error("The cluster tools need a cluster: pass project (a SnoutData Cloud project's ref, from list_projects), or start snoutdata mcp with LEPIS_ADMIN_URL (a standalone router's admin API, e.g. http://127.0.0.1:7432) and LEPIS_ADMIN_TOKEN set.");return t??=wn({}),t},whoami:()=>xt(),listProjects:async()=>({...await je(),local:T_()}),localProject:n=>e(n),createProject:async n=>{let r=await x("cloud-project-create",{name:n.name,...n.region?{region:n.region}:{},...n.teamId?{teamId:n.teamId}:{}});try{return await fn(r.project.ref)}catch{return{...r.project,ready:!1,note:`${r.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:(n,r)=>x("cloud-project-usage",{ref:n,...r?{days:r}:{}}),exportStatus:n=>Bt(n),requestExport:n=>pn(n),resetPassword:n=>x("cloud-project-reset-password",{ref:n}),listTeams:()=>x("cloud-team-list",{}),deployFunction:(n,r,o)=>{let s=e(n);return s?Zo(s,r,o):ss(n,r,o).then(a=>a.answer)},listFunctions:async n=>{let r=e(n);return r?hr(r):as(n)},deleteFunction:(n,r)=>{let o=e(n);return o?es(o,r):is(n,r)},sizeFunction:(n,r,o)=>ls(n,r,o),listFunctionSecrets:async n=>{let r=e(n);return r?wr(r):x("cloud-project-secrets",{ref:n})},functionLogs:(n,r)=>x("cloud-project-functions",{ref:n,action:"logs",...r.name?{name:r.name}:{},...r.errorsOnly?{errorsOnly:!0}:{},...r.limit?{limit:r.limit}:{}}),listContainers:n=>Kt(n),deployContainer:(n,r,o)=>fs(n,r,o),restartContainer:(n,r)=>hs(n,r),deleteContainer:(n,r)=>gs(n,r),containerLogs:(n,r)=>Sr(n,r),containerEgress:async n=>{let r=await Kt(n);return{ref:r.ref,allowlist:r.allowlist??null}},setContainerEgress:async(n,r)=>{let o=await _r(n,r);return{ref:o.ref,allowlist:o.allowlist??null,note:"Running containers follow it within a minute; nothing restarts."}},listTokens:()=>x("cloud-token-list",{}),createToken:(n,r,o)=>x("cloud-token-create",{name:n,...r?{expiresInDays:r}:{},...o?{project:o}:{}}),revokeToken:n=>x("cloud-token-revoke",{id:n}),getProject:async n=>{let r=e(n);return r?{ref:r.ref,name:r.name,local:r.folder,api:r.apiUrl,database:`127.0.0.1:${r.dbPort}`,functions:hr(r).functions.map(o=>o.name),secretNames:wr(r).secrets.map(o=>o.name)}:Ns(n)},getProducts:n=>qt(n),setProduct:(n,r,o)=>As(n,r,o),listDomains:n=>Ar(n),domainAction:(n,r,o)=>Rs(n,r,o),restoreWindow:n=>Ts(n),restoreTo:(n,r,o)=>Os(n,r,o),setProjectState:async(n,r)=>{let o=await x(`cloud-project-${n}`,{ref:r});try{let s=await mn(r,n,{timeoutMs:12e4});return{changed:o.changed,state:s?.state??"deleted",...s?{project:s}:{}}}catch{return{...o,settled:!1,note:`${r} has not finished yet. Call list_projects in a few seconds.`}}},connection:n=>Ce(n),pushMigrations:(n,r)=>qs(n,{dir:r.dir,dryRun:r.dryRun})}}function T_(){let e=Gn().filter(n=>n.ref!==null).map(n=>({ref:n.ref,name:n.name,folder:n.folder})),t=Dt(process.cwd())?.local;if(t&&!e.some(n=>xm(n.folder)===xm(t)))try{let n=dt(t);e.push({ref:n.ref,name:n.name,folder:n.folder})}catch{}return e}var Em="app_";async function O_(){if(process.env.SNOUTDATA_NO_DESKTOP)return{tools:[],config:null};let e=await Vn();if(!e.available)return{tools:[],config:null};let t=e.config;try{return{tools:(await Oi(t)).map(o=>({name:`${Em}${o.name}`,description:`[SnoutData Studio, on this machine] ${o.description??o.name}`,inputSchema:o.inputSchema??{type:"object",properties:{}}})),config:t}}catch{return{tools:[],config:null}}}async function Am(e){un(!0);let{tools:t,config:n}=await O_(),r=R_();n&&(r.callBorrowed=(i,u)=>Ni(n,i.slice(Em.length),u));let o={...e,borrowed:t};ie(t.length>0?`snoutdata mcp: ${kn.length} cloud tools, and ${t.length} borrowed from SnoutData Studio on this machine.`:"snoutdata mcp: cloud tools only. SnoutData Studio is not answering here, so its database tools are not on this endpoint.");let s="";process.stdin.setEncoding("utf8");try{let i=await xt();ie(`snoutdata mcp: signed in as ${i.email??"this account"}.`)}catch(i){if(ie(`snoutdata mcp: ${i instanceof Error?i.message:String(i)}`),!process.env.LEPIS_ADMIN_URL)return ie("Sign in with `snoutdata login`, or set SNOUTDATA_ACCESS_TOKEN."),3;ie(`snoutdata mcp: not signed in, so only the cluster tools will work (router ${process.env.LEPIS_ADMIN_URL}).`)}ie(`snoutdata mcp: ready${e.allowDelete?" (delete_project is ON)":""}. Speaking MCP on stdin/stdout; stop it with Ctrl-C.`);let a=new Set;function c(i){a.add(i),i.finally(()=>a.delete(i))}return new Promise(i=>{process.stdin.on("data",u=>{let l=Sc(s+u);s=l.rest;for(let m of l.messages)c(N_(m,r,o))}),process.stdin.on("end",()=>{(async()=>{for(;a.size>0;)await Promise.allSettled([...a]);i(0)})()}),process.stdin.on("error",()=>i(1))})}async function N_(e,t,n){let r;try{r=JSON.parse(e)}catch{ie(`snoutdata mcp: ignoring a line that is not JSON: ${e.slice(0,120)}`);return}try{let o=await bc(r,t,n);o&&process.stdout.write(`${JSON.stringify(o)}
|
|
699
724
|
`)}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)}})}
|
|
700
|
-
`)}}Ct();re();te();function eb(e){return e>=1?"over":e>=.8?"near":"ok"}function ju(e,t){let n=null,r=null,o=0,s=0;for(let l of e)o+=Number.isFinite(l.computeSeconds)?l.computeSeconds:0,s+=Number.isFinite(l.connections)?l.connections:0,!(l.dbBytes===null||!Number.isFinite(l.dbBytes))&&(r=r===null?l.dbBytes:Math.max(r,l.dbBytes),(!n||l.day>=n.day)&&(n={day:l.day,dbBytes:l.dbBytes,repoBytes:l.repoBytes}));let a=n&&t&&t.maxStorageBytes>0?{limitBytes:t.maxStorageBytes,usedBytes:n.dbBytes,fraction:n.dbBytes/t.maxStorageBytes,state:eb(n.dbBytes/t.maxStorageBytes)}:null;return{days:e.length,latest:n,peakDbBytes:r,computeSeconds:o,connections:s,storage:a}}function Ds(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 Du(e,t={}){let n=await _("cloud-project-usage",{ref:e,...t.days?{days:t.days}:{}}),r=ju(n.days,n.limit);return h({...n,summary:r},()=>{if(d(x(`${n.name} (${n.ref})`)),!r.latest)d("Nothing measured yet. The host samples every fifteen minutes.");else if(r.storage){let o=Math.round(r.storage.fraction*100),s=`${Ke(r.storage.usedBytes)} of ${Ke(r.storage.limitBytes)} on ${n.limit?.tier}`;d(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 d(`${Ke(r.latest.dbBytes)} as of ${r.latest.day}.`);n.readOnly&&r.storage?.state!=="over"&&d("This project is currently read-only."),d(`Over ${r.days} day${r.days===1?"":"s"}: ${Ds(r.computeSeconds)} of compute, ${r.connections} connection${r.connections===1?"":"s"}.`),r.peakDbBytes!==null&&r.latest&&r.peakDbBytes>r.latest.dbBytes&&d(w(`Peak in that window was ${Ke(r.peakDbBytes)}.`)),t.history&&n.days.length>0&&(d(""),d(L([["DAY","SIZE","BACKUPS","COMPUTE","CONNS"],...n.days.map(o=>[o.day,o.dbBytes===null?"-":Ke(o.dbBytes),o.repoBytes===null?"-":Ke(o.repoBytes),Ds(o.computeSeconds),String(o.connections)])])),d(w("A dash is a day nothing measured it, which is what a paused project looks like.")))}),0}be();me();at();te();import{createInterface as tb}from"node:readline/promises";var nb=new Set(["node.drain","node.remove","table.distribute","table.reference","table.global","range.split","range.merge","range.move","tenant.pin","rebalance","scale","cleanup","dissolve"]),Ms={max_write_pause_ms:"max-write-pause-ms",drain_timeout_ms:"drain-timeout-ms",ack_timeout_ms:"ack-timeout-ms",copy_mb_per_s:"copy-mb-per-s"},rb="nodes add|drain|remove, keyspace create|drop, table distribute|reference|global, range split|merge|move, tenant pin, rebalance, scale, verify, cleanup, dissolve";function Mu(e,t,n=!1){let[r,o,...s]=e,a=c=>S(t,c),l=(c,p)=>{if(c===void 0||c==="")throw new m(p);return c},i,u=`${r??""}${o!==void 0&&ob(r)?` ${o}`:""}`;if(n&&(u==="nodes add"||u==="node add")){if(s.length>0||a("host")!==void 0)throw new m("in SnoutData Cloud a node is a pod the project makes: cluster nodes add --project <ref>, with no name or --host. It joins the cluster on its own once it is ready.");return{body:N(t,"no-join")?{op:"node.add",join:!1}:{op:"node.add"},destructive:!1}}if(u==="nodes attach"||u==="node attach"){if(!n)throw new m("nodes attach is for a SnoutData Cloud project (--project): a standalone node joins with cluster nodes add <name> --host H");return{body:{op:"node.attach",name:l(s[0]??a("node"),"nodes attach needs the node pod's ref, as cluster status lists it")},destructive:!1}}if(n&&u==="scale"){if(a("add")!==void 0||a("remove")!==void 0)throw new m("in SnoutData Cloud, scale takes --nodes N: how many nodes the project should have, the home included");let c=ot(t,"nodes","nodes");if(!c.nodes)throw new m("scale needs --nodes N: how many nodes the project should have, the home included");i={op:"scale",...c};for(let[p,f]of Object.entries(Ms))Object.assign(i,ot(t,f,p));return{body:i,destructive:!0}}switch(u){case"nodes add":case"node add":i={op:"node.add",name:l(s[0]??a("name"),"nodes add needs a name: cluster nodes add <name> --host H [--port 5432] [--dbname D] [--sslmode M]"),host:l(a("host"),"nodes add needs --host, the address the router reaches the node at"),...ot(t,"port","port"),...rt(t,"dbname","dbname"),...rt(t,"sslmode","sslmode"),...rt(t,"peer-host","peer_host")};break;case"nodes drain":case"node drain":i={op:"node.drain",node:l(s[0]??a("node"),"nodes drain needs a node: cluster nodes drain <id|name> [--to a,b]"),...a("to")?{to:Or(a("to"))}:{}};break;case"nodes remove":case"node remove":i={op:"node.remove",node:l(s[0]??a("node"),"nodes remove needs a node: cluster nodes remove <id|name>")};break;case"keyspace create":i={op:"keyspace.create",name:l(s[0]??a("name"),"keyspace create needs a name: cluster keyspace create <name> --key-type bigint [--ranges N] [--nodes a,b]"),key_type:l(a("key-type"),"keyspace create needs --key-type, the distribution key's type (bigint, int, text, uuid, ...)"),...ot(t,"ranges","ranges"),...a("seed")!==void 0?{seed:sb(a("seed"))}:{},...a("nodes")?{nodes:Or(a("nodes"))}:{}};break;case"keyspace drop":i={op:"keyspace.drop",name:l(s[0]??a("name"),"keyspace drop needs a name: cluster keyspace drop <name> (a keyspace no table is distributed by)")};break;case"dissolve":if(n)throw new m("in SnoutData Cloud, turn the cluster off with cluster disable --project <ref>: it dissolves the cluster and then stops the router");i={op:"dissolve"};break;case"table distribute":i={op:"table.distribute",table:l(s[0],"table distribute needs a table: cluster table distribute <schema.table> --column C --keyspace K"),column:l(a("column"),"table distribute needs --column, the distribution key column"),keyspace:l(a("keyspace"),"table distribute needs --keyspace")};break;case"table reference":case"table global":i={op:`table.${o}`,table:l(s[0],`table ${o} needs a table: cluster table ${o} <schema.table>`)};break;case"range split":{let c=Rr(s[0],t);i={op:"range.split",keyspace:c.keyspace,range:c.lo,...a("at")!==void 0?{at:Ls(a("at"),"--at")}:{},...a("to")?{to:a("to")}:{}};break}case"range move":{let c=Rr(s[0],t);i={op:"range.move",keyspace:c.keyspace,range:c.lo,to:l(a("to"),"range move needs --to, the node that will own it")};break}case"range merge":{if(s.length!==2)throw new m("range merge needs two adjacent ranges: cluster range merge <keyspace:lo> <keyspace:lo>");let c=Rr(s[0],t),p=Rr(s[1],t);if(c.keyspace!==p.keyspace)throw new m(`range merge: ${c.keyspace} and ${p.keyspace} are different keyspaces`);i={op:"range.merge",keyspace:c.keyspace,a:c.lo,b:p.lo};break}case"tenant pin":{let c=a("keyspace")??s[0],p=a("value")??(a("keyspace")?s[0]:s[1]);i={op:"tenant.pin",keyspace:l(c,"tenant pin needs a keyspace: cluster tenant pin <keyspace> <value> [--node N]"),value:l(p,"tenant pin needs the key value to pin (--value V for one that starts with a dash)"),...a("node")?{node:a("node")}:{}};break}case"rebalance":i={op:"rebalance",...rt(t,"keyspace","keyspace")};break;case"scale":{let c=a("add")?Or(a("add")).map(f=>ab(f,t)):[],p=ot(t,"remove","remove");if(c.length===0&&!p.remove)throw new m("scale needs --add name=host[:port],... or --remove N (or both)");i={op:"scale",...c.length>0?{add:c}:{},...p};break}case"verify":i={op:"verify",...rt(t,"keyspace","keyspace")};break;case"cleanup":i={op:"cleanup",...rt(t,"node","node")};break;default:throw new m(`unknown operation: cluster ${e.join(" ")}. One of ${rb}`)}for(let[c,p]of Object.entries(Ms)){let f=ot(t,p,c);Object.assign(i,f)}return{body:i,destructive:nb.has(i.op)}}function ob(e){return e==="nodes"||e==="node"||e==="keyspace"||e==="table"||e==="range"||e==="tenant"}function Or(e){return e.split(",").map(t=>t.trim()).filter(Boolean)}function rt(e,t,n){let r=S(e,t);return r===void 0?{}:{[n]:r}}function ot(e,t,n){let r=S(e,t);if(r===void 0)return{};let o=Number(r);if(!/^\d+$/.test(r)||!Number.isSafeInteger(o))throw new m(`--${t} must be a whole number, not "${r}"`);return{[n]:o}}function Ls(e,t){let n=e.trim();if(!/^-?\d+$/.test(n)||BigInt(n)<-(2n**63n)||BigInt(n)>=2n**63n)throw new m(`${t} must be a 64-bit whole number, not "${e}"`);return BigInt(n).toString()}function sb(e){let t=e.trim();if(!/^\d+$/.test(t)||BigInt(t)>=2n**64n)throw new m(`--seed must be an unsigned 64-bit whole number, not "${e}"`);return BigInt(t).toString()}function Rr(e,t){let n=S(t,"keyspace"),r=S(t,"range");if(e!==void 0&&e.includes(":")){let s=e.lastIndexOf(":"),a=e.slice(0,s);if(!a)throw new m(`a range is keyspace:lo, as cluster status prints it, not "${e}"`);return{keyspace:a,lo:Ls(e.slice(s+1),`the range's lower bound in "${e}"`)}}let o=r??e;if(!n||o===void 0)throw new m("name the range as keyspace:lo (as cluster status prints it), or with --keyspace K --range LO");return{keyspace:n,lo:Ls(o,"--range")}}function ab(e,t){let n=/^([^=]+)=([^:]+)(?::(\d+))?$/.exec(e);if(!n)throw new m(`scale --add takes name=host[:port], not "${e}"`);return{name:n[1],host:n[2],...n[3]?{port:Number(n[3])}:{},...rt(t,"dbname","dbname"),...rt(t,"sslmode","sslmode")}}function Tr(e,t){if(e===void 0||!/^\d+$/.test(e))throw new m(`jobs ${t} needs a job id: cluster jobs ${t} <id>`);return Number(e)}async function Bu(e,t={}){let n=e.command.slice(1),[r,o,s]=n,a=S(e,"project")??S(e,"ref"),l=a!==void 0,i=()=>t.client??Gt({project:a,admin:S(e,"admin"),token:S(e,"token")});switch(r){case"enable":{if(!l)throw new m("cluster enable is for a SnoutData Cloud project: cluster enable --project <ref>. A standalone router is a cluster from the moment it runs");let c=i();if(!(c instanceof De))throw new m("cluster enable needs --project <ref>");if(N(e,"plan")){let f=await c.enable(!0);return h(f,()=>Ee(f,process.stdout)),0}if(!N(e,"yes")){if(!t.confirm&&!ye())throw new m("making it a cluster restarts the database once, and nothing was asked: pass --yes, or --plan to see what it does");if(Ee(await c.enable(!0),process.stderr),!await(t.confirm??Nt)(`Make a cluster of ${c.target}?`))throw new P("failed","Not turned on. Nothing was changed.")}let p=await c.enable(!1);return h(p,()=>d(Xe(p))),0}case"disable":{if(!l)throw new m("cluster disable is for a SnoutData Cloud project: cluster disable --project <ref>. A standalone cluster is taken apart with cluster dissolve, and then by stopping the router");let c=i();if(!(c instanceof De))throw new m("cluster disable needs --project <ref>");if(N(e,"plan")){let f=await c.disable(!0);return h(f,()=>Ee(f,process.stdout)),0}if(!N(e,"yes")){if(!t.confirm&&!ye())throw new m("turning the cluster off restarts the database once, and nothing was asked: pass --yes, or --plan to see what it does");if(Ee(await c.disable(!0),process.stderr),!await(t.confirm??Nt)(`Turn the cluster of ${c.target} off?`))throw new P("failed","Not turned off. Nothing was changed.")}let p=await c.disable(!1);return h(p,()=>d(Xe(p))),0}case"spread":{if(!l)throw new m("cluster spread is for a SnoutData Cloud project (--project). On a standalone router: cluster keyspace create, then cluster table distribute and cluster rebalance");let c=i();if(!(c instanceof De))throw new m("cluster spread needs --project <ref>");let p={table:o??Ot(S(e,"table"),"cluster spread needs the table: cluster spread <schema.table> --column C --key-type T"),column:Ot(S(e,"column"),"cluster spread needs --column, the column to spread the rows by"),key_type:Ot(S(e,"key-type"),"cluster spread needs --key-type, the column's type (bigint, uuid, text, ...)")};if(N(e,"plan")){let k=await c.spread(p,!0);return h(k,()=>Ee(k,process.stdout)),0}if(!N(e,"yes")){if(!t.confirm&&!ye())throw new m("spreading a table moves its rows, and nothing was asked: pass --yes, or --plan to see what it does");if(Ee(await c.spread(p,!0),process.stderr),!await(t.confirm??Nt)(`Spread ${p.table} over ${c.target}?`))throw new P("failed","Not spread. Nothing was changed.")}let f=await c.spread(p,!1);return h(f,()=>d(Xe(f))),0}case"retry":{if(!l)throw new m("cluster retry is for a SnoutData Cloud project (--project): a stopped job on a standalone router is resumed with cluster jobs resume <id>");let c=i();if(!(c instanceof De))throw new m("cluster retry needs --project <ref>");let p=N(e,"queue")?await c.retry({queue:!0}):await c.retry({node:Ot(o??S(e,"node"),"cluster retry needs the node pod whose join stopped, or --queue")});return h(p,()=>d(Xe(p))),0}case"replica":{if(!l)throw new m("cluster replica is for a SnoutData Cloud project (--project): cluster replica add|remove <node-ref> --project <ref>");if(o!=="add"&&o!=="remove")throw new m("cluster replica add|remove <node-ref> --project <ref>: the node pod, as cluster status lists it");let c=i();if(!(c instanceof De))throw new m("cluster replica needs --project <ref>");let p=o==="remove",f=Ot(s??S(e,"node"),`cluster replica ${o} needs the node pod's ref, as cluster status lists it`);if(N(e,"plan")){let v=await c.replica(f,p,!0);return h(v,()=>Ee(v,process.stdout)),0}if(!N(e,"yes")){if(!t.confirm&&!ye())throw new m(p?"removing a replica leaves the node without one, and nothing was asked: pass --yes, or --plan to see what it does":"a replica takes one project slot, and nothing was asked: pass --yes, or --plan to see what it does");if(Ee(await c.replica(f,p,!0),process.stderr),!await(t.confirm??Nt)(p?`Remove the replica of node ${f}?`:`Add a replica to node ${f}?`))throw new P("failed","Nothing was changed.")}let k=await c.replica(f,p,!1);return h(k,()=>d(Xe(k))),0}case"alerts":{if(!l)throw new m("cluster alerts is for a SnoutData Cloud project (--project <ref>)");let c=i();if(!(c instanceof De))throw new m("cluster alerts needs --project <ref>");if(o===void 0||o==="list"){let p=await c.alerts();return h({alerts:p},()=>Hu(p,process.stdout,!0,c.ref)),0}if(o==="seen"||o==="acknowledge"){let p=Ot(s,`cluster alerts ${o} needs the alert's id, as cluster alerts lists it`);if(!/^[0-9a-f-]{36}$/i.test(p))throw new m(`not an alert id: ${p} (as cluster alerts lists it)`);let f=await c.acknowledge(p.toLowerCase());return h(f,()=>d(Xe(f))),0}throw new m(`unknown command: cluster alerts ${o}. alerts [list], or alerts seen <id>`)}case void 0:case"status":{let c=await i().status();return h(c,()=>mb(c)),0}case"ops":{let c=await i().ops();return h(c,()=>{process.stdout.write(`${c.ops.join(`
|
|
725
|
+
`)}}an();me();ue();function I_(e){return e>=1?"over":e>=.8?"near":"ok"}function Rm(e,t){let n=null,r=null,o=0,s=0;for(let c of e)o+=Number.isFinite(c.computeSeconds)?c.computeSeconds:0,s+=Number.isFinite(c.connections)?c.connections:0,!(c.dbBytes===null||!Number.isFinite(c.dbBytes))&&(r=r===null?c.dbBytes:Math.max(r,c.dbBytes),(!n||c.day>=n.day)&&(n={day:c.day,dbBytes:c.dbBytes,repoBytes:c.repoBytes}));let a=n&&t&&t.maxStorageBytes>0?{limitBytes:t.maxStorageBytes,usedBytes:n.dbBytes,fraction:n.dbBytes/t.maxStorageBytes,state:I_(n.dbBytes/t.maxStorageBytes)}:null;return{days:e.length,latest:n,peakDbBytes:r,computeSeconds:o,connections:s,storage:a}}function Ja(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 Tm(e,t={}){let n=await x("cloud-project-usage",{ref:e,...t.days?{days:t.days}:{}}),r=Rm(n.days,n.limit);return w({...n,summary:r},()=>{if(d(O(`${n.name} (${n.ref})`)),!r.latest)d("Nothing measured yet. The host samples every fifteen minutes.");else if(r.storage){let o=Math.round(r.storage.fraction*100),s=`${at(r.storage.usedBytes)} of ${at(r.storage.limitBytes)} on ${n.limit?.tier}`;d(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 d(`${at(r.latest.dbBytes)} as of ${r.latest.day}.`);n.readOnly&&r.storage?.state!=="over"&&d("This project is currently read-only."),d(`Over ${r.days} day${r.days===1?"":"s"}: ${Ja(r.computeSeconds)} of compute, ${r.connections} connection${r.connections===1?"":"s"}.`),r.peakDbBytes!==null&&r.latest&&r.peakDbBytes>r.latest.dbBytes&&d(b(`Peak in that window was ${at(r.peakDbBytes)}.`)),t.history&&n.days.length>0&&(d(""),d(W([["DAY","SIZE","BACKUPS","COMPUTE","CONNS"],...n.days.map(o=>[o.day,o.dbBytes===null?"-":at(o.dbBytes),o.repoBytes===null?"-":at(o.repoBytes),Ja(o.computeSeconds),String(o.connections)])])),d(b("A dash is a day nothing measured it, which is what a paused project looks like.")))}),0}Ae();Se();Pt();ue();import{createInterface as C_}from"node:readline/promises";var D_=new Set(["node.drain","node.remove","table.distribute","table.reference","table.global","table.auto","range.split","range.merge","range.move","tenant.pin","rebalance","scale","cleanup","dissolve"]),Va={max_write_pause_ms:"max-write-pause-ms",drain_timeout_ms:"drain-timeout-ms",ack_timeout_ms:"ack-timeout-ms",copy_mb_per_s:"copy-mb-per-s"},M_="nodes add|drain|remove, keyspace create|drop, table distribute|reference|global|auto, range split|merge|move, tenant pin, rebalance, scale, verify, cleanup, dissolve";function Om(e,t,n=!1){let[r,o,...s]=e,a=l=>E(t,l),c=(l,m)=>{if(l===void 0||l==="")throw new g(m);return l},i,u=`${r??""}${o!==void 0&&j_(r)?` ${o}`:""}`;if(n&&(u==="nodes add"||u==="node add")){if(s.length>0||a("host")!==void 0)throw new g("in SnoutData Cloud a node is a pod the project makes: cluster nodes add --project <ref>, with no name or --host. It joins the cluster on its own once it is ready.");return{body:M(t,"no-join")?{op:"node.add",join:!1}:{op:"node.add"},destructive:!1}}if(u==="nodes attach"||u==="node attach"){if(!n)throw new g("nodes attach is for a SnoutData Cloud project (--project): a standalone node joins with cluster nodes add <name> --host H");return{body:{op:"node.attach",name:c(s[0]??a("node"),"nodes attach needs the node pod's ref, as cluster status lists it")},destructive:!1}}if(n&&u==="scale"){if(a("add")!==void 0||a("remove")!==void 0)throw new g("in SnoutData Cloud, scale takes --nodes N: how many nodes the project should have, the home included");let l=et(t,"nodes","nodes");if(!l.nodes)throw new g("scale needs --nodes N: how many nodes the project should have, the home included");i={op:"scale",...l};for(let[m,h]of Object.entries(Va))Object.assign(i,et(t,h,m));return{body:i,destructive:!0}}switch(u){case"nodes add":case"node add":i={op:"node.add",name:c(s[0]??a("name"),"nodes add needs a name: cluster nodes add <name> --host H [--port 5432] [--dbname D] [--sslmode M]"),host:c(a("host"),"nodes add needs --host, the address the router reaches the node at"),...et(t,"port","port"),...vt(t,"dbname","dbname"),...vt(t,"sslmode","sslmode"),...vt(t,"peer-host","peer_host")};break;case"nodes drain":case"node drain":i={op:"node.drain",node:c(s[0]??a("node"),"nodes drain needs a node: cluster nodes drain <id|name> [--to a,b]"),...a("to")?{to:ko(a("to"))}:{}};break;case"nodes remove":case"node remove":i={op:"node.remove",node:c(s[0]??a("node"),"nodes remove needs a node: cluster nodes remove <id|name>")};break;case"keyspace create":i={op:"keyspace.create",name:c(s[0]??a("name"),"keyspace create needs a name: cluster keyspace create <name> --key-type bigint [--ranges N] [--nodes a,b]"),key_type:c(a("key-type"),"keyspace create needs --key-type, the distribution key's type (bigint, int, text, uuid, ...)"),...et(t,"ranges","ranges"),...a("seed")!==void 0?{seed:L_(a("seed"))}:{},...a("nodes")?{nodes:ko(a("nodes"))}:{}};break;case"keyspace drop":i={op:"keyspace.drop",name:c(s[0]??a("name"),"keyspace drop needs a name: cluster keyspace drop <name> (a keyspace no table is distributed by)")};break;case"dissolve":if(n)throw new g("in SnoutData Cloud, turn the cluster off with cluster disable --project <ref>: it dissolves the cluster and then stops the router");i={op:"dissolve"};break;case"table distribute":i={op:"table.distribute",table:c(s[0],"table distribute needs a table: cluster table distribute <schema.table> --column C --keyspace K"),column:c(a("column"),"table distribute needs --column, the distribution key column"),keyspace:c(a("keyspace"),"table distribute needs --keyspace")};break;case"table auto":{let l=a("schemas");i={op:"table.auto",keyspace:c(a("keyspace"),"table auto needs --keyspace, the keyspace every table it distributes goes in"),...l!==void 0?{schemas:l.split(",").map(m=>m.trim()).filter(m=>m!=="")}:{},...et(t,"min-bytes","min_bytes")};break}case"table reference":case"table global":i={op:`table.${o}`,table:c(s[0],`table ${o} needs a table: cluster table ${o} <schema.table>`)};break;case"range split":{let l=So(s[0],t);i={op:"range.split",keyspace:l.keyspace,range:l.lo,...a("at")!==void 0?{at:Ya(a("at"),"--at")}:{},...a("to")?{to:a("to")}:{}};break}case"range move":{let l=So(s[0],t);i={op:"range.move",keyspace:l.keyspace,range:l.lo,to:c(a("to"),"range move needs --to, the node that will own it")};break}case"range merge":{if(s.length!==2)throw new g("range merge needs two adjacent ranges: cluster range merge <keyspace:lo> <keyspace:lo>");let l=So(s[0],t),m=So(s[1],t);if(l.keyspace!==m.keyspace)throw new g(`range merge: ${l.keyspace} and ${m.keyspace} are different keyspaces`);i={op:"range.merge",keyspace:l.keyspace,a:l.lo,b:m.lo};break}case"tenant pin":{let l=a("keyspace")??s[0],m=a("value")??(a("keyspace")?s[0]:s[1]);i={op:"tenant.pin",keyspace:c(l,"tenant pin needs a keyspace: cluster tenant pin <keyspace> <value> [--node N]"),value:c(m,"tenant pin needs the key value to pin (--value V for one that starts with a dash)"),...a("node")?{node:a("node")}:{}};break}case"rebalance":i={op:"rebalance",...vt(t,"keyspace","keyspace")};break;case"scale":{let l=a("add")?ko(a("add")).map(h=>U_(h,t)):[],m=et(t,"remove","remove");if(l.length===0&&!m.remove)throw new g("scale needs --add name=host[:port],... or --remove N (or both)");i={op:"scale",...l.length>0?{add:l}:{},...m};break}case"verify":i={op:"verify",...vt(t,"keyspace","keyspace")};break;case"cleanup":i={op:"cleanup",...vt(t,"node","node")};break;default:throw new g(`unknown operation: cluster ${e.join(" ")}. One of ${M_}`)}for(let[l,m]of Object.entries(Va)){let h=et(t,m,l);Object.assign(i,h)}return{body:i,destructive:D_.has(i.op)}}function j_(e){return e==="nodes"||e==="node"||e==="keyspace"||e==="table"||e==="range"||e==="tenant"}function ko(e){return e.split(",").map(t=>t.trim()).filter(Boolean)}function vt(e,t,n){let r=E(e,t);return r===void 0?{}:{[n]:r}}function et(e,t,n){let r=E(e,t);if(r===void 0)return{};let o=Number(r);if(!/^\d+$/.test(r)||!Number.isSafeInteger(o))throw new g(`--${t} must be a whole number, not "${r}"`);return{[n]:o}}function Ya(e,t){let n=e.trim();if(!/^-?\d+$/.test(n)||BigInt(n)<-(2n**63n)||BigInt(n)>=2n**63n)throw new g(`${t} must be a 64-bit whole number, not "${e}"`);return BigInt(n).toString()}function L_(e){let t=e.trim();if(!/^\d+$/.test(t)||BigInt(t)>=2n**64n)throw new g(`--seed must be an unsigned 64-bit whole number, not "${e}"`);return BigInt(t).toString()}function So(e,t){let n=E(t,"keyspace"),r=E(t,"range");if(e!==void 0&&e.includes(":")){let s=e.lastIndexOf(":"),a=e.slice(0,s);if(!a)throw new g(`a range is keyspace:lo, as cluster status prints it, not "${e}"`);return{keyspace:a,lo:Ya(e.slice(s+1),`the range's lower bound in "${e}"`)}}let o=r??e;if(!n||o===void 0)throw new g("name the range as keyspace:lo (as cluster status prints it), or with --keyspace K --range LO");return{keyspace:n,lo:Ya(o,"--range")}}function U_(e,t){let n=/^([^=]+)=([^:]+)(?::(\d+))?$/.exec(e);if(!n)throw new g(`scale --add takes name=host[:port], not "${e}"`);return{name:n[1],host:n[2],...n[3]?{port:Number(n[3])}:{},...vt(t,"dbname","dbname"),...vt(t,"sslmode","sslmode")}}function _o(e,t){if(e===void 0||!/^\d+$/.test(e))throw new g(`jobs ${t} needs a job id: cluster jobs ${t} <id>`);return Number(e)}async function Cm(e,t={}){let n=e.command.slice(1),[r,o,s]=n,a=E(e,"project")??E(e,"ref"),c=a!==void 0,i=()=>t.client??wn({project:a,admin:E(e,"admin"),token:E(e,"token")});switch(r){case"enable":{if(!c)throw new g("cluster enable is for a SnoutData Cloud project: cluster enable --project <ref>. A standalone router is a cluster from the moment it runs");let l=i();if(!(l instanceof Ve))throw new g("cluster enable needs --project <ref>");if(M(e,"plan")){let h=await l.enable(!0);return w(h,()=>Fe(h,process.stdout)),0}if(!M(e,"yes")){if(!t.confirm&&!ve())throw new g("making it a cluster moves the project's services into pods of their own, and nothing was asked: pass --yes, or --plan to see what it does");if(Fe(await l.enable(!0),process.stderr),!await(t.confirm??It)(`Make a cluster of ${l.target}?`))throw new T("failed","Not turned on. Nothing was changed.")}let m=await l.enable(!1);return w(m,()=>d(ft(m))),0}case"disable":{if(!c)throw new g("cluster disable is for a SnoutData Cloud project: cluster disable --project <ref>. A standalone cluster is taken apart with cluster dissolve, and then by stopping the router");let l=i();if(!(l instanceof Ve))throw new g("cluster disable needs --project <ref>");if(M(e,"plan")){let h=await l.disable(!0);return w(h,()=>Fe(h,process.stdout)),0}if(!M(e,"yes")){if(!t.confirm&&!ve())throw new g("turning the cluster off restarts the database once, and nothing was asked: pass --yes, or --plan to see what it does");if(Fe(await l.disable(!0),process.stderr),!await(t.confirm??It)(`Turn the cluster of ${l.target} off?`))throw new T("failed","Not turned off. Nothing was changed.")}let m=await l.disable(!1);return w(m,()=>d(ft(m))),0}case"spread":{if(!c)throw new g("cluster spread is for a SnoutData Cloud project (--project). On a standalone router: cluster keyspace create, then cluster table distribute and cluster rebalance");let l=i();if(!(l instanceof Ve))throw new g("cluster spread needs --project <ref>");let m={table:o??on(E(e,"table"),"cluster spread needs the table: cluster spread <schema.table> --column C --key-type T"),column:on(E(e,"column"),"cluster spread needs --column, the column to spread the rows by"),key_type:on(E(e,"key-type"),"cluster spread needs --key-type, the column's type (bigint, uuid, text, ...)")};if(M(e,"plan")){let P=await l.spread(m,!0);return w(P,()=>Fe(P,process.stdout)),0}if(!M(e,"yes")){if(!t.confirm&&!ve())throw new g("spreading a table moves its rows, and nothing was asked: pass --yes, or --plan to see what it does");if(Fe(await l.spread(m,!0),process.stderr),!await(t.confirm??It)(`Spread ${m.table} over ${l.target}?`))throw new T("failed","Not spread. Nothing was changed.")}let h=await l.spread(m,!1);return w(h,()=>d(ft(h))),0}case"retry":{if(!c)throw new g("cluster retry is for a SnoutData Cloud project (--project): a stopped job on a standalone router is resumed with cluster jobs resume <id>");let l=i();if(!(l instanceof Ve))throw new g("cluster retry needs --project <ref>");let m=M(e,"queue")?await l.retry({queue:!0}):await l.retry({node:on(o??E(e,"node"),"cluster retry needs the node pod whose join stopped, or --queue")});return w(m,()=>d(ft(m))),0}case"replica":{if(!c)throw new g("cluster replica is for a SnoutData Cloud project (--project): cluster replica add|remove <node-ref> --project <ref>");if(o!=="add"&&o!=="remove")throw new g("cluster replica add|remove <node-ref> --project <ref>: the node pod, as cluster status lists it");let l=i();if(!(l instanceof Ve))throw new g("cluster replica needs --project <ref>");let m=o==="remove",h=on(s??E(e,"node"),`cluster replica ${o} needs the node pod's ref, as cluster status lists it`);if(M(e,"plan")){let A=await l.replica(h,m,!0);return w(A,()=>Fe(A,process.stdout)),0}if(!M(e,"yes")){if(!t.confirm&&!ve())throw new g(m?"removing a replica leaves the node without one, and nothing was asked: pass --yes, or --plan to see what it does":"a replica takes one project slot, and nothing was asked: pass --yes, or --plan to see what it does");if(Fe(await l.replica(h,m,!0),process.stderr),!await(t.confirm??It)(m?`Remove the replica of node ${h}?`:`Add a replica to node ${h}?`))throw new T("failed","Nothing was changed.")}let P=await l.replica(h,m,!1);return w(P,()=>d(ft(P))),0}case"alerts":{if(!c)throw new g("cluster alerts is for a SnoutData Cloud project (--project <ref>)");let l=i();if(!(l instanceof Ve))throw new g("cluster alerts needs --project <ref>");if(o===void 0||o==="list"){let m=await l.alerts();return w({alerts:m},()=>jm(m,process.stdout,!0,l.ref)),0}if(o==="seen"||o==="acknowledge"){let m=on(s,`cluster alerts ${o} needs the alert's id, as cluster alerts lists it`);if(!/^[0-9a-f-]{36}$/i.test(m))throw new g(`not an alert id: ${m} (as cluster alerts lists it)`);let h=await l.acknowledge(m.toLowerCase());return w(h,()=>d(ft(h))),0}throw new g(`unknown command: cluster alerts ${o}. alerts [list], or alerts seen <id>`)}case void 0:case"status":{let l=await i().status();return w(l,()=>J_(l)),0}case"ops":{let l=await i().ops();return w(l,()=>{process.stdout.write(`${l.ops.join(`
|
|
701
726
|
`)}
|
|
702
|
-
`),d(
|
|
703
|
-
`),d(`Job ${
|
|
704
|
-
`)),0}let o=await t().settings(n);return
|
|
705
|
-
`)),0;let t=e.state==="failed"?`failed: ${e.error??"no reason recorded"}. Resume it from the failed step: snoutdata cluster jobs resume ${e.id}`:"was cancelled";throw new
|
|
706
|
-
`)}function Fs(e){let t=["B","kB","MB","GB","TB"],n=e,r=0;for(;n>=1e3&&r<t.length-1;)n/=1e3,r+=1;return`${r===0?n:n.toFixed(1)} ${t[r]}`}function Ee(e,t){D(t,`${x(e.op)}: ${e.steps.length} step${e.steps.length===1?"":"s"}, ${e.cutovers} cutover${e.cutovers===1?"":"s"}`),e.moves.length>0&&D(t,L([[" FROM","TO","TABLES","ROWS","SIZE","COPY","PAUSE"],...e.moves.map(n=>[` node ${n.source}`,n.targets.map(r=>`node ${r}`).join(", ")||"-",n.tables.join(", ")||"-",`~${n.estimated_rows.toLocaleString("en-US")}`,Fs(n.estimated_bytes),`~${n.estimated_copy_seconds}s`,`~${n.expected_pause_ms}ms`])])),D(t,` total: ~${e.estimated_rows.toLocaleString("en-US")} rows, ${Fs(e.estimated_bytes)}, ~${e.estimated_copy_seconds}s to copy, expected write pause ~${e.expected_pause_ms}ms (ceiling ${e.max_write_pause_ms}ms)`),D(t,` steps: ${e.steps.map(n=>n.kind).join(", ")||"none"}`);for(let n of e.warnings)D(t,` warning: ${n}`);e.assumptions&&D(t,w(` ${e.assumptions}`))}function Oe(e){let t=String(e);return/^[\w.:@+-]+$/.test(t)?t:`'${t.replace(/'/g,"'\\''")}'`}function ub(e){let t=e,n="snoutdata cluster";switch(e.op){case"range.split":return`${n} range split ${Oe(`${t.keyspace}:${t.range}`)}${t.at!==void 0?` --at ${Oe(t.at)}`:""}${t.to!==void 0?` --to ${Oe(t.to)}`:""}`;case"range.move":return`${n} range move ${Oe(`${t.keyspace}:${t.range}`)} --to ${Oe(t.to)}`;case"tenant.pin":return`${n} tenant pin ${Oe(t.keyspace)} --value ${Oe(t.value)}${t.node!==void 0?` --node ${Oe(t.node)}`:""}`;case"node.add":return`${n} nodes add ${Oe(t.name)} --host ${t.host?Oe(t.host):"<its address>"}, then ${n} rebalance`;case"rebalance":return`${n} rebalance${t.keyspace?` --keyspace ${Oe(t.keyspace)}`:""}`;default:return null}}function pb(e,t){D(t,e.summary),e.advice.forEach((n,r)=>{D(t),D(t,`${x(`${r+1}. ${n.op}`)} (${n.metric})`),D(t,` ${n.reason}`);let o=ub(n.request);if(D(t,` run: ${o??JSON.stringify(n.request)}`),n.plan){let s=n.plan;D(t,` plan: ${s.cutovers} cutover${s.cutovers===1?"":"s"}, ~${s.estimated_rows.toLocaleString("en-US")} rows, ${Fs(s.estimated_bytes)}, ~${s.estimated_copy_seconds}s to copy, expected write pause ~${s.expected_pause_ms}ms`);for(let a of s.warnings??[])D(t,` warning: ${a}`)}else n.needs.length>0?D(t,` needs: ${n.needs.join(", ")}, so it has no plan yet`):n.plan_error&&D(t,` no plan: ${n.plan_error.message} (${n.plan_error.kind})`)}),e.assumptions&&(D(t),D(t,w(e.assumptions)))}function Lu(e,t){return e.nodes?.find(n=>n.id===t)?.name??`node ${t}`}function Fu(e){let t=e.nodes??[];if(t.length===0){d("No nodes in the catalog.");return}D(process.stdout,L([["ID","NAME","ADDRESS","STATE","HOME","RANGES","POSTGRES"],...t.map(n=>[String(n.id),n.name,`${n.host}:${n.port}/${n.dbname}`,n.state,n.home?"yes":"",String(n.ranges.length),n.server_version_num?String(Math.floor(n.server_version_num/1e4)):"?"])]))}function mb(e){let t=process.stdout;if(e.cloud&&(wb(e.cloud,t),!e.cloud.sharded))return;if(!e.catalog){D(t,"The home node has no Lepis catalog yet (schema lepis).");return}D(t,`${x("Cluster")} at epoch ${e.epoch}`),D(t),Fu(e);for(let s of e.keyspaces??[]){D(t),D(t,`${x(`Keyspace ${s.name}`)} (${s.key_type}, ${s.ranges.length} range${s.ranges.length===1?"":"s"})`),D(t,L([[" RANGE","HI","NODE"],...s.ranges.map(a=>[` ${s.name}:${a.lo}`,a.hi,Lu(e,a.node)])]));for(let a of s.pins)D(t,` pinned: ${a.value} on ${Lu(e,a.node)}`)}let n=e.tables??[];n.length>0&&(D(t),D(t,L([["TABLE","KIND","KEYSPACE","COLUMN"],...n.map(s=>[s.table,s.kind,s.keyspace??"",s.column??""])])));let r=e.routers??[];r.length>0&&(D(t),D(t,`Routers: ${r.map(s=>`${s.id} (epoch ${s.epoch}${s.live?"":", not heard from"}${s.current?"":", behind"})`).join("; ")}`));let o=e.jobs??[];o.length>0&&D(t,`Unfinished jobs: ${o.map(s=>`${s.id} ${s.op} ${s.state}`).join("; ")}`),e.settings&&d(w(`Settings: ${Object.entries(e.settings).map(([s,a])=>`${s}=${a}`).join(", ")}`))}function fb(e,t){if(t.joining==="failed"&&t.next!=="delete-pod")return`stopped joining: ${t.joinError??"see the audit log"}. Retry: snoutdata cluster retry ${t.ref} --project ${e.ref}`;if(t.replicaOf)return`replica of node ${t.replicaOf} (${t.state})`;switch(t.next){case"attach":return`ready: snoutdata cluster nodes attach ${t.ref} --project ${e.ref}`;case"delete-pod":return`let go by the cluster: snoutdata cluster nodes remove ${t.ref} --project ${e.ref} deletes its pod`;case"wait":return`starting as a copy of the home node (${t.state})`;case"joining":return t.joining==="spreading"?"joining: taking its share of the data":t.joining==="joining"?"joining the cluster":`joining: starting as a copy of the home node (${t.state})`;default:return t.node?`node ${t.node.id}, ${t.node.state}`:t.state}}function gb(e){if(e.replicaOf)return`replica of node ${e.replicaOf}`;switch(e.failover){case"fence":return"failing over: stopping the node so its replica can take its place";case"promote":return"failing over: its replica is taking its place";case"failed":return`failover stopped: ${e.failoverError??"see cluster alerts"}`;case"held":return"failover held: its replica was behind and its last writes could not be copied to it, so it was not promoted (see cluster alerts)"}return e.desiredState==="deleted"||e.replicaOf===void 0?"":e.replica?`protected by replica ${e.replica}`:e.wantsReplica?"no replica yet: one is being made":"no replica"}function Uu(e){return e.severity.toUpperCase()}function Hu(e,t,n,r="<ref>"){let o=e.filter(a=>a.resolvedAt===null),s=e.filter(a=>a.resolvedAt!==null);if(!n){for(let a of o)D(t,`${x(Uu(a))} ${a.message}${a.acknowledged?w(" (seen)"):""}`);o.length>0&&D(t,w(` See them all, and mark one as seen: snoutdata cluster alerts --project ${r}`));return}if(e.length===0){d("No alerts. Nothing has happened to this cluster that needs you.");return}for(let a of o)D(t,`${x(Uu(a))} ${a.message}`),D(t,w(` ${a.id}, opened ${a.openedAt.slice(0,16).replace("T"," ")}${a.acknowledged?", seen":""}`));if(s.length>0){o.length>0&&D(t),D(t,"Resolved in the last day:");for(let a of s)D(t,` ${a.message}${a.resolution?` ${w(a.resolution)}`:""}`)}}function hb(e){return e.projectsUsed===null||e.maxProjects===null?0:Math.max(0,e.maxProjects-e.projectsUsed)}function yb(e){return e.projectsUsed===null||e.maxProjects===null?"the plan could not be read":`${e.projectsUsed} of ${e.maxProjects} project slots used on the ${e.tier??"current"} plan; each node uses one`}function wb(e,t){let n=yb(e);if(!e.sharded){D(t,`${x(e.ref)} is not a cluster (${n}).`),hb(e)>0&&D(t,`Make it a cluster: snoutdata cluster enable --project ${e.ref} (the database restarts once).`);return}Hu(e.alerts??[],t,!1,e.ref);let r=e.pods.filter(l=>l.desiredState!=="deleted"),o=1+r.filter(l=>!l.replicaOf).length,s=r.length-(o-1);if(D(t,`${x(e.ref)}: ${o} node${o===1?"":"s"}${s>0?` and ${s} replica${s===1?"":"s"}`:""}, ${n}`),e.pods.length>0){let l=e.pods.some(i=>i.replicaOf!==void 0);D(t,L([[" POD","STATE","NEXT",...l?["REPLICA"]:[]],...e.pods.map(i=>[` ${i.ref}`,i.state,fb(e,i),...l?[gb(i)]:[]])]))}e.router==="starting"?D(t,"The router is starting beside the database, which takes about twenty seconds."):e.router==="paused"&&D(t,"The project is paused; the router wakes with it on the next connection."),e.routerError&&D(t,`The router is not answering: ${e.routerError}`);let a=e.automation;a&&a.queueError?D(t,`Stopped: ${a.queueError}. Retry: snoutdata cluster retry --queue --project ${e.ref}`):a&&(a.queue.length>0||a.queueJob!==null)&&D(t,`Working through a request${a.queueJob!==null?` (job ${a.queueJob})`:""}${a.queue.length>0?`; then ${a.queue.join(", ")}`:""}.`),D(t)}function bb(e){if(e.length===0){d("No jobs yet.");return}D(process.stdout,L([["ID","OPERATION","STATE","STEPS","CREATED","ERROR"],...e.map(t=>[String(t.id),t.op,t.state,`${t.steps_done}/${t.steps}`,t.created_at.slice(0,19).replace("T"," "),t.error??""])]))}function _b(e){let t=process.stdout;D(t,`${x(`Job ${e.id}`)} ${e.op}: ${e.state}${e.error?` (${e.error})`:""}`),D(t,L([[" STEP","KIND","STATE","PHASE"],...e.steps.map(n=>[` ${n.n+1}`,n.kind,n.state,typeof n.detail?.phase=="string"?n.detail.phase:""])])),e.plan&&Array.isArray(e.plan.steps)&&(D(t),Ee(e.plan,t))}me();te();Dt();import{spawn as Sb}from"node:child_process";import{createHash as kb}from"node:crypto";import{chmodSync as vb,renameSync as Nr,rmSync as Ku,writeFileSync as $b}from"node:fs";import{dirname as xb,join as Pb}from"node:path";var Ir=process.env.SNOUTDATA_NPM_REGISTRY??"https://registry.npmjs.org",Eb="https://github.com/snoutdata/studio/releases/download";function Ab(e,t){let n=t==="x64"||t==="amd64"?"x64":t==="arm64"?"arm64":null;return n?e==="linux"||e==="darwin"?`snoutdata-${e}-${n}`:e==="win32"&&n==="x64"?"snoutdata-windows-x64.exe":null:null}function Rb(e,t){for(let n of e.split(/\r?\n/)){let r=/^([0-9a-f]{64})\s+\*?(.+)$/i.exec(n.trim());if(r&&r[2]===t)return r[1].toLowerCase()}return null}async function Tb(){let e;try{e=await fetch(`${Ir}/snoutdata/latest`,{headers:{accept:"application/json"}})}catch(n){throw new P("network",`could not reach ${Ir} to find the newest version: ${n instanceof Error?n.message:String(n)}`)}if(!e.ok)throw new P("server",`${Ir} answered ${e.status} when asked for the newest version`);let t=await e.json();if(typeof t.version!="string")throw new P("server",`${Ir} did not say which version is newest`);return t.version}async function qu(e){let t;try{t=await fetch(e)}catch(n){throw new P("network",`could not download ${e}: ${n instanceof Error?n.message:String(n)}`)}if(!t.ok)throw new P("failed",`download failed (${t.status}): ${e}`);return Buffer.from(await t.arrayBuffer())}function Ob(e,t){let n=Pb(xb(e),`.snoutdata-upgrade-${process.pid}`);$b(n,t,{mode:493});try{if(process.platform==="win32"){let r=`${e}.old`;Ku(r,{force:!0}),Nr(e,r);try{Nr(n,e)}catch(o){throw Nr(r,e),o}}else vb(n,493),Nr(n,e)}catch(r){Ku(n,{force:!0});let o=r.code;throw o==="EACCES"||o==="EPERM"?new P("forbidden",`${e} is not writable by this user. Run the installer instead: ${process.platform==="win32"?Be:jt}`):r}}function Nb(){return new Promise((e,t)=>{let n=Sb(process.platform==="win32"?"npm.cmd":"npm",["install","-g","snoutdata@latest"],{stdio:["ignore","inherit","inherit"],shell:process.platform==="win32"});n.on("error",r=>t(new P("tool-missing",`npm could not be run (${r.message}). Run it yourself: ${Be}`))),n.on("exit",r=>e(r??1))})}async function Gu(e={}){let t=e.kind??Rn(),n=await Tb(),r=wa(ue,n),o={installed:ue,latest:n,upToDate:!r,install:t};if(!r||e.check)return h({...o,upgraded:!1},()=>{d(r?`snoutdata ${ue} is installed; ${n} is the newest. \`snoutdata upgrade\` installs it.`:`snoutdata ${ue} is the newest version.`)}),0;switch(t){case"npx":return h({...o,upgraded:!1},()=>{d(`This is a copy npx fetched (${ue}). Run \`npx snoutdata@latest\` to use ${n}; there is nothing installed to upgrade.`)}),0;case"source":throw new P("usage",`this snoutdata (${ue}) is running from source, so there is nothing to replace. Install the published one: ${process.platform==="win32"?Be:jt}`);case"npm":{d(`Upgrading snoutdata ${ue} to ${n} with npm\u2026`);let s=await Nb();if(s!==0)throw new P("failed",`npm exited ${s}. Run it yourself: ${Be}`);return h({...o,upgraded:!0},()=>d(`Upgraded to ${n}.`)),0}case"binary":{let s=Ab(process.platform,process.arch);if(!s)throw new P("failed",`no snoutdata binary is built for ${process.platform}-${process.arch}. Use npm: ${Be}`);d(`Downloading snoutdata ${n} for ${process.platform}-${process.arch}\u2026`);let a=`${Eb}/cli-v${n}`,[l,i]=await Promise.all([qu(`${a}/${s}`),qu(`${a}/SHA256SUMS`)]),u=Rb(i.toString("utf8"),s);if(!u)throw new P("failed",`${s} is not listed in the release's SHA256SUMS. Refusing to install.`);let c=kb("sha256").update(l).digest("hex");if(c!==u)throw new P("failed",`checksum mismatch for ${s}. Refusing to install.
|
|
727
|
+
`),d(b(`Default settings: ${Object.entries(l.settings).map(([m,h])=>`${m}=${h}`).join(", ")}`)),l.refusals&&l.refusals.length>0&&d(b(`Refusal kinds: ${l.refusals.map(m=>`${m.kind} (${m.status})`).join(", ")}`))}),0}case"settings":return H_(e,i);case"autopilot":return G_(o,e,i,t);case"advise":case"advice":{let l=et(e,"sample-ms","sampleMs").sampleMs,m=await i().advice(l);return w(m,()=>z_(m,process.stdout)),0}case"jobs":return q_(o,s,e,i(),t);case"nodes":case"node":if(o===void 0||o==="list"){let l=await i().status();return w({nodes:l.nodes??[]},()=>Mm(l)),0}break;case"plan":{let l=Om(n.slice(1),e,c),m=await i().plan(l.body);return w(m,()=>Fe(m,process.stdout)),0}}let u=Om(n,e,c);if(M(e,"plan")){let l=await i().plan(u.body);return w(l,()=>Fe(l,process.stdout)),0}return F_(u,e,i,t)}async function F_(e,t,n,r){let o=M(t,"yes"),s=e.body.op;if(e.destructive&&!o&&!r.confirm&&!ve())throw new g(`${s} moves or deletes data, and nothing was asked: pass --yes to run it, or --plan to see what it would do`);let a=n();if(e.destructive&&!o){let u=await a.plan(e.body);if(Fe(u,process.stderr),!await(r.confirm??It)(`Run ${s} on ${a.target}?`))throw new T("failed","Not run. Nothing was changed.")}let c=await a.submit(e.body);if(c.job===void 0)return w(c.answer,()=>d(c.done)),0;if(M(t,"no-wait"))return w({job:c.job,state:"pending",plan:c.plan},()=>{process.stdout.write(`${c.job}
|
|
728
|
+
`),d(`Job ${c.job} (${s}) is queued. Follow it: snoutdata cluster jobs watch ${c.job}`)}),0;d(`Job ${c.job} (${s}) is queued; following it. Ctrl+C stops following, not the job.`);let i=await Xa(a,c.job,r);return Za(i)}function on(e,t){if(e===void 0||e==="")throw new g(t);return e}async function It(e){let t=C_({input:process.stdin,output:process.stderr});try{let n=(await t.question(`${e} [y/N] `)).trim().toLowerCase();return n==="y"||n==="yes"}finally{t.close()}}function B_(e){let t=[..._s,...mc],n={};for(let r of ko(e)){let o=r.indexOf("="),s=o<0?r:r.slice(0,o).trim(),a=o<0?"":r.slice(o+1).trim();if(!t.includes(s))throw new g(`--set: unknown setting "${s}". One of ${t.join(", ")}`);let c=Number(a);if(!/^\d+$/.test(a)||!Number.isSafeInteger(c))throw new g(`--set ${s} must be a whole number, not "${a}"`);n[s]=c}return n}async function H_(e,t){let n={};for(let s of _s)Object.assign(n,et(e,Va[s],s));let r=E(e,"set");if(r!==void 0&&Object.assign(n,B_(r)),Object.keys(n).length===0){let s=await t().status(),a={...s.settings??{},...s.advice_settings??{}};return w({settings:a},()=>process.stdout.write(`${W(Object.entries(a).map(([c,i])=>[c,String(i)]))}
|
|
729
|
+
`)),0}let o=await t().settings(n);return w(o,()=>d(`Set ${Object.entries(o.settings).map(([s,a])=>`${s}=${a}`).join(", ")}.`)),0}function Dm(e,t){let n=`every ${Math.round(t/1e3)} s`;switch(e){case"advise":return`Autopilot: advise. ${n[0].toUpperCase()}${n.slice(1)} it records what it would run, and runs nothing.`;case"auto":return`Autopilot: auto. ${n[0].toUpperCase()}${n.slice(1)}, when no job is running, it runs the advisor's recommendation as a job of its own, never in a keyspace you placed by hand.`;default:return"Autopilot: off. It runs nothing on its own. Turn it on with cluster autopilot advise, or auto."}}function K_(e){let t=[Dm(e.mode,e.interval_ms)],n=e.last?.decision;if(e.last&&n&&n.kind!=="off"){let r={propose:"it would run",submit:"it ran",nothing:"nothing to run",busy:"it waited for a job",held:"it is waiting for you"},o=n.request?.op?` ${n.request.op}${n.request.keyspace?` in ${String(n.request.keyspace)}`:""}`:"",s=n.job!==void 0?` (job ${n.job})`:"";t.push(`Last decided ${e.last.at.slice(0,19).replace("T"," ")}: ${r[n.kind]??n.kind}${o}${s}${n.why?`. ${n.why}`:""}${n.error?`. It could not: ${n.error}`:""}`)}return t}async function G_(e,t,n,r){let o=et(t,"interval-ms","autopilot_interval_ms");if(e===void 0||e==="status"){if(Object.keys(o).length>0){let i=await n().setAutopilot(o);return w(i,()=>d(`Set autopilot_interval_ms=${o.autopilot_interval_ms}.`)),0}let c=await n().autopilot();return w(c,()=>{for(let i of K_(c))d(i)}),0}if(!fc.includes(e))throw new g(`cluster autopilot [off|advise|auto] [--interval-ms N], not "${e}"`);let s=e;if(s==="auto"&&!M(t,"yes")){if(!r.confirm&&!ve())throw new g("autopilot auto lets the router move data on its own, and nothing was asked: pass --yes to turn it on");if(!await(r.confirm??It)("Let the router move data on its own (autopilot auto)?"))throw new T("failed","Not changed. The autopilot is as it was.")}let a=await n().setAutopilot({autopilot:s,...o});return w(a,()=>d(Dm(s,o.autopilot_interval_ms??6e4))),0}async function q_(e,t,n,r,o){switch(e){case void 0:case"list":{let s=await r.jobs();return w({jobs:s},()=>ek(s)),0}case"show":{let s=await r.job(_o(t,"show"));return w(s,()=>tk(s)),0}case"watch":{let s=await Xa(r,_o(t,"watch"),o);return Za(s)}case"cancel":{let s=_o(t,"cancel");if(!M(n,"yes")){if(!o.confirm&&!ve())throw new g(`cancelling job ${s} stops a running operation, and nothing was asked: pass --yes`);if(!await(o.confirm??It)(`Cancel job ${s} on ${r.target}?`))throw new T("failed","Not cancelled. Nothing was changed.")}let a=await r.cancel(s);return w(a,()=>d(`Job ${s}: ${a.state}.`)),0}case"resume":{let s=_o(t,"resume"),a=await r.resume(s);return M(n,"no-wait")?(w(a,()=>d(`Job ${s}: ${a.state}. Follow it: snoutdata cluster jobs watch ${s}`)),0):Za(await Xa(r,s,o))}default:throw new g(`unknown command: cluster jobs ${e}. jobs [list|show|watch|cancel|resume] <id>`)}}async function Xa(e,t,n={}){let r=n.pollMs??1e3,o=n.timeoutMs===void 0?1/0:Date.now()+n.timeoutMs,s=new Map;for(;;){let a=await e.job(t);for(let c of a.steps){let i=typeof c.detail?.phase=="string"?c.detail.phase:"",u=`${c.state}${i?` ${i}`:""}`;s.get(c.n)!==u&&(s.set(c.n,u),c.state!=="pending"&&d(b(` step ${c.n+1}/${a.steps.length} ${c.kind}: ${u}`)))}if(yn(a.state))return a;if(Date.now()>=o)throw new T("timeout",`job ${t} is still ${a.state}; it carries on without this command. Follow it again: snoutdata cluster jobs watch ${t}`,{job:a});await new Promise(c=>setTimeout(c,r))}}function Za(e){if(e.state==="done")return w(e,()=>process.stdout.write(`Job ${e.id} (${e.op}): done, ${e.steps.length} step${e.steps.length===1?"":"s"}.
|
|
730
|
+
`)),0;let t=e.state==="failed"?`failed: ${e.error??"no reason recorded"}. Resume it from the failed step: snoutdata cluster jobs resume ${e.id}`:"was cancelled";throw new T("failed",`job ${e.id} (${e.op}) ${t}`,{job:e})}function q(e,t=""){e.write(`${t}
|
|
731
|
+
`)}function Qa(e){let t=["B","kB","MB","GB","TB"],n=e,r=0;for(;n>=1e3&&r<t.length-1;)n/=1e3,r+=1;return`${r===0?n:n.toFixed(1)} ${t[r]}`}function Fe(e,t){if(q(t,`${O(e.op)}: ${e.steps.length} step${e.steps.length===1?"":"s"}, ${e.cutovers} cutover${e.cutovers===1?"":"s"}`),e.moves.length>0&&q(t,W([[" FROM","TO","TABLES","ROWS","SIZE","COPY","PAUSE"],...e.moves.map(n=>[` node ${n.source}`,n.targets.map(r=>`node ${r}`).join(", ")||"-",n.tables.join(", ")||"-",`~${n.estimated_rows.toLocaleString("en-US")}`,Qa(n.estimated_bytes),`~${n.estimated_copy_seconds}s`,`~${n.expected_pause_ms}ms`])])),q(t,` total: ~${e.estimated_rows.toLocaleString("en-US")} rows, ${Qa(e.estimated_bytes)}, ~${e.estimated_copy_seconds}s to copy, expected write pause ~${e.expected_pause_ms}ms (ceiling ${e.max_write_pause_ms}ms)`),e.choices&&e.choices.length>0){let n={shard:"shard by",reference:"copy to every node",leave:"leave on the home node"};q(t,W([[" TABLE","CHOICE","WHY"],...e.choices.map(r=>[` ${r.table}`,r.choice==="shard"?`${n.shard} ${r.column}`:n[r.choice],r.why])]))}q(t,` steps: ${e.steps.map(n=>n.kind).join(", ")||"none"}`);for(let n of e.warnings)q(t,` warning: ${n}`);e.assumptions&&q(t,b(` ${e.assumptions}`))}function Ge(e){let t=String(e);return/^[\w.:@+-]+$/.test(t)?t:`'${t.replace(/'/g,"'\\''")}'`}function W_(e){let t=e,n="snoutdata cluster";switch(e.op){case"range.split":return`${n} range split ${Ge(`${t.keyspace}:${t.range}`)}${t.at!==void 0?` --at ${Ge(t.at)}`:""}${t.to!==void 0?` --to ${Ge(t.to)}`:""}`;case"range.move":return`${n} range move ${Ge(`${t.keyspace}:${t.range}`)} --to ${Ge(t.to)}`;case"tenant.pin":return`${n} tenant pin ${Ge(t.keyspace)} --value ${Ge(t.value)}${t.node!==void 0?` --node ${Ge(t.node)}`:""}`;case"node.add":return`${n} nodes add ${Ge(t.name)} --host ${t.host?Ge(t.host):"<its address>"}, then ${n} rebalance`;case"rebalance":return`${n} rebalance${t.keyspace?` --keyspace ${Ge(t.keyspace)}`:""}`;default:return null}}function z_(e,t){q(t,e.summary),e.advice.forEach((n,r)=>{q(t),q(t,`${O(`${r+1}. ${n.op}`)} (${n.metric})`),q(t,` ${n.reason}`);let o=W_(n.request);if(q(t,` run: ${o??JSON.stringify(n.request)}`),n.plan){let s=n.plan;q(t,` plan: ${s.cutovers} cutover${s.cutovers===1?"":"s"}, ~${s.estimated_rows.toLocaleString("en-US")} rows, ${Qa(s.estimated_bytes)}, ~${s.estimated_copy_seconds}s to copy, expected write pause ~${s.expected_pause_ms}ms`);for(let a of s.warnings??[])q(t,` warning: ${a}`)}else n.needs.length>0?q(t,` needs: ${n.needs.join(", ")}, so it has no plan yet`):n.plan_error&&q(t,` no plan: ${n.plan_error.message} (${n.plan_error.kind})`)}),e.assumptions&&(q(t),q(t,b(e.assumptions)))}function Nm(e,t){return e.nodes?.find(n=>n.id===t)?.name??`node ${t}`}function Mm(e){let t=e.nodes??[];if(t.length===0){d("No nodes in the catalog.");return}q(process.stdout,W([["ID","NAME","ADDRESS","STATE","HOME","RANGES","POSTGRES"],...t.map(n=>[String(n.id),n.name,`${n.host}:${n.port}/${n.dbname}`,n.state,n.home?"yes":"",String(n.ranges.length),n.server_version_num?String(Math.floor(n.server_version_num/1e4)):"?"])]))}function J_(e){let t=process.stdout;if(e.cloud&&(Q_(e.cloud,t),!e.cloud.sharded))return;if(!e.catalog){q(t,"The home node has no Lepis catalog yet (schema lepis).");return}q(t,`${O("Cluster")} at epoch ${e.epoch}`),q(t),Mm(e);for(let s of e.keyspaces??[]){q(t),q(t,`${O(`Keyspace ${s.name}`)} (${s.key_type}, ${s.ranges.length} range${s.ranges.length===1?"":"s"})`),q(t,W([[" RANGE","HI","NODE"],...s.ranges.map(a=>[` ${s.name}:${a.lo}`,a.hi,Nm(e,a.node)])]));for(let a of s.pins)q(t,` pinned: ${a.value} on ${Nm(e,a.node)}`)}let n=e.tables??[];n.length>0&&(q(t),q(t,W([["TABLE","KIND","KEYSPACE","COLUMN"],...n.map(s=>[s.table,s.kind,s.keyspace??"",s.column??""])])));let r=e.routers??[];r.length>0&&(q(t),q(t,`Routers: ${r.map(s=>`${s.id} (epoch ${s.epoch}${s.live?"":", not heard from"}${s.current?"":", behind"})`).join("; ")}`));let o=e.jobs??[];o.length>0&&q(t,`Unfinished jobs: ${o.map(s=>`${s.id} ${s.op} ${s.state}`).join("; ")}`),e.settings&&d(b(`Settings: ${Object.entries(e.settings).map(([s,a])=>`${s}=${a}`).join(", ")}`))}function V_(e,t){if(t.joining==="failed"&&t.next!=="delete-pod")return`stopped joining: ${t.joinError??"see the audit log"}. Retry: snoutdata cluster retry ${t.ref} --project ${e.ref}`;if(t.replicaOf)return`replica of node ${t.replicaOf} (${t.state})`;switch(t.next){case"attach":return`ready: snoutdata cluster nodes attach ${t.ref} --project ${e.ref}`;case"delete-pod":return`let go by the cluster: snoutdata cluster nodes remove ${t.ref} --project ${e.ref} deletes its pod`;case"wait":return`starting as a copy of the home node (${t.state})`;case"joining":return t.joining==="spreading"?"joining: taking its share of the data":t.joining==="joining"?"joining the cluster":`joining: starting as a copy of the home node (${t.state})`;default:return t.node?`node ${t.node.id}, ${t.node.state}`:t.state}}function Y_(e){if(e.replicaOf)return`replica of node ${e.replicaOf}`;switch(e.failover){case"fence":return"failing over: stopping the node so its replica can take its place";case"promote":return"failing over: its replica is taking its place";case"failed":return`failover stopped: ${e.failoverError??"see cluster alerts"}`;case"held":return"failover held: its replica was behind and its last writes could not be copied to it, so it was not promoted (see cluster alerts)"}return e.desiredState==="deleted"||e.replicaOf===void 0?"":e.replica?`protected by replica ${e.replica}`:e.wantsReplica?"no replica yet: one is being made":"no replica"}function Im(e){return e.severity.toUpperCase()}function jm(e,t,n,r="<ref>"){let o=e.filter(a=>a.resolvedAt===null),s=e.filter(a=>a.resolvedAt!==null);if(!n){for(let a of o)q(t,`${O(Im(a))} ${a.message}${a.acknowledged?b(" (seen)"):""}`);o.length>0&&q(t,b(` See them all, and mark one as seen: snoutdata cluster alerts --project ${r}`));return}if(e.length===0){d("No alerts. Nothing has happened to this cluster that needs you.");return}for(let a of o)q(t,`${O(Im(a))} ${a.message}`),q(t,b(` ${a.id}, opened ${a.openedAt.slice(0,16).replace("T"," ")}${a.acknowledged?", seen":""}`));if(s.length>0){o.length>0&&q(t),q(t,"Resolved in the last day:");for(let a of s)q(t,` ${a.message}${a.resolution?` ${b(a.resolution)}`:""}`)}}function X_(e){return e.projectsUsed===null||e.maxProjects===null?0:Math.max(0,e.maxProjects-e.projectsUsed)}function Z_(e){return e.projectsUsed===null||e.maxProjects===null?"the plan could not be read":`${e.projectsUsed} of ${e.maxProjects} project slots used on the ${e.tier??"current"} plan; each node uses one`}function Q_(e,t){let n=Z_(e);if(!e.sharded){q(t,`${O(e.ref)} is not a cluster (${n}).`),X_(e)>0&&q(t,`Make it a cluster: snoutdata cluster enable --project ${e.ref} (the database keeps running).`);return}jm(e.alerts??[],t,!1,e.ref);let r=e.pods.filter(c=>c.desiredState!=="deleted"),o=1+r.filter(c=>!c.replicaOf).length,s=r.length-(o-1);if(q(t,`${O(e.ref)}: ${o} node${o===1?"":"s"}${s>0?` and ${s} replica${s===1?"":"s"}`:""}, ${n}`),e.pods.length>0){let c=e.pods.some(i=>i.replicaOf!==void 0);q(t,W([[" POD","STATE","NEXT",...c?["REPLICA"]:[]],...e.pods.map(i=>[` ${i.ref}`,i.state,V_(e,i),...c?[Y_(i)]:[]])]))}e.router==="starting"?q(t,"The router is starting beside the database, which takes about twenty seconds."):e.router==="paused"&&q(t,"The project is paused; the router wakes with it on the next connection."),e.routerError&&q(t,`The router is not answering: ${e.routerError}`);let a=e.automation;a&&a.queueError?q(t,`Stopped: ${a.queueError}. Retry: snoutdata cluster retry --queue --project ${e.ref}`):a&&(a.queue.length>0||a.queueJob!==null)&&q(t,`Working through a request${a.queueJob!==null?` (job ${a.queueJob})`:""}${a.queue.length>0?`; then ${a.queue.join(", ")}`:""}.`),q(t)}function ek(e){if(e.length===0){d("No jobs yet.");return}q(process.stdout,W([["ID","OPERATION","STATE","STEPS","BY","CREATED","ERROR"],...e.map(t=>[String(t.id),t.op,t.state,`${t.steps_done}/${t.steps}`,t.actor==="lepis"?"autopilot":"you",t.created_at.slice(0,19).replace("T"," "),t.error??""])]))}function tk(e){let t=process.stdout;q(t,`${O(`Job ${e.id}`)} ${e.op}: ${e.state}${e.error?` (${e.error})`:""}`),q(t,W([[" STEP","KIND","STATE","PHASE"],...e.steps.map(n=>[` ${n.n+1}`,n.kind,n.state,typeof n.detail?.phase=="string"?n.detail.phase:""])])),e.plan&&Array.isArray(e.plan.steps)&&(q(t),Fe(e.plan,t))}Se();ue();cn();import{spawn as nk}from"node:child_process";import{createHash as rk}from"node:crypto";import{chmodSync as ok,renameSync as vo,rmSync as Lm,writeFileSync as sk}from"node:fs";import{dirname as ak,join as ik}from"node:path";var $o=process.env.SNOUTDATA_NPM_REGISTRY??"https://registry.npmjs.org",lk="https://github.com/snoutdata/studio/releases/download";function ck(e,t){let n=t==="x64"||t==="amd64"?"x64":t==="arm64"?"arm64":null;return n?e==="linux"||e==="darwin"?`snoutdata-${e}-${n}`:e==="win32"&&n==="x64"?"snoutdata-windows-x64.exe":null:null}function dk(e,t){for(let n of e.split(/\r?\n/)){let r=/^([0-9a-f]{64})\s+\*?(.+)$/i.exec(n.trim());if(r&&r[2]===t)return r[1].toLowerCase()}return null}async function uk(){let e;try{e=await fetch(`${$o}/snoutdata/latest`,{headers:{accept:"application/json"}})}catch(n){throw new T("network",`could not reach ${$o} to find the newest version: ${n instanceof Error?n.message:String(n)}`)}if(!e.ok)throw new T("server",`${$o} answered ${e.status} when asked for the newest version`);let t=await e.json();if(typeof t.version!="string")throw new T("server",`${$o} did not say which version is newest`);return t.version}async function Um(e){let t;try{t=await fetch(e)}catch(n){throw new T("network",`could not download ${e}: ${n instanceof Error?n.message:String(n)}`)}if(!t.ok)throw new T("failed",`download failed (${t.status}): ${e}`);return Buffer.from(await t.arrayBuffer())}function pk(e,t){let n=ik(ak(e),`.snoutdata-upgrade-${process.pid}`);sk(n,t,{mode:493});try{if(process.platform==="win32"){let r=`${e}.old`;Lm(r,{force:!0}),vo(e,r);try{vo(n,e)}catch(o){throw vo(r,e),o}}else ok(n,493),vo(n,e)}catch(r){Lm(n,{force:!0});let o=r.code;throw o==="EACCES"||o==="EPERM"?new T("forbidden",`${e} is not writable by this user. Run the installer instead: ${process.platform==="win32"?rt:ln}`):r}}function mk(){return new Promise((e,t)=>{let n=nk(process.platform==="win32"?"npm.cmd":"npm",["install","-g","snoutdata@latest"],{stdio:["ignore","inherit","inherit"],shell:process.platform==="win32"});n.on("error",r=>t(new T("tool-missing",`npm could not be run (${r.message}). Run it yourself: ${rt}`))),n.on("exit",r=>e(r??1))})}async function Fm(e={}){let t=e.kind??nr(),n=await uk(),r=Di(we,n),o={installed:we,latest:n,upToDate:!r,install:t};if(!r||e.check)return w({...o,upgraded:!1},()=>{d(r?`snoutdata ${we} is installed; ${n} is the newest. \`snoutdata upgrade\` installs it.`:`snoutdata ${we} is the newest version.`)}),0;switch(t){case"npx":return w({...o,upgraded:!1},()=>{d(`This is a copy npx fetched (${we}). Run \`npx snoutdata@latest\` to use ${n}; there is nothing installed to upgrade.`)}),0;case"source":throw new T("usage",`this snoutdata (${we}) is running from source, so there is nothing to replace. Install the published one: ${process.platform==="win32"?rt:ln}`);case"npm":{d(`Upgrading snoutdata ${we} to ${n} with npm\u2026`);let s=await mk();if(s!==0)throw new T("failed",`npm exited ${s}. Run it yourself: ${rt}`);return w({...o,upgraded:!0},()=>d(`Upgraded to ${n}.`)),0}case"binary":{let s=ck(process.platform,process.arch);if(!s)throw new T("failed",`no snoutdata binary is built for ${process.platform}-${process.arch}. Use npm: ${rt}`);d(`Downloading snoutdata ${n} for ${process.platform}-${process.arch}\u2026`);let a=`${lk}/cli-v${n}`,[c,i]=await Promise.all([Um(`${a}/${s}`),Um(`${a}/SHA256SUMS`)]),u=dk(i.toString("utf8"),s);if(!u)throw new T("failed",`${s} is not listed in the release's SHA256SUMS. Refusing to install.`);let l=rk("sha256").update(c).digest("hex");if(l!==u)throw new T("failed",`checksum mismatch for ${s}. Refusing to install.
|
|
707
732
|
expected ${u}
|
|
708
|
-
actual ${
|
|
733
|
+
actual ${l}`);return pk(process.execPath,c),w({...o,upgraded:!0,path:process.execPath},()=>d(`Upgraded ${process.execPath} to ${n}.`)),0}}}dr();lt();cn();var Bm=`snoutdata ${we} \u2014 hosted Postgres, from a terminal or an agent
|
|
709
734
|
|
|
710
735
|
snoutdata init [--name X] [--env] a database for this folder, linked, ready to use;
|
|
711
736
|
--env writes DATABASE_URL into ./.env (and adds
|
|
@@ -835,7 +860,7 @@ ${o.err.trim()}`),d(" ran seed.sql"),!0}var ln="_snoutdata_migrations",Jw=`crea
|
|
|
835
860
|
snoutdata cluster --project REF ... the same, for a SnoutData Cloud project, as you; there
|
|
836
861
|
enable turns it on, nodes add makes a node pod, nodes
|
|
837
862
|
attach REF hands a ready one over, scale --nodes N
|
|
838
|
-
snoutdata cluster nodes [add|drain|remove] | keyspace create | table distribute|reference|global
|
|
863
|
+
snoutdata cluster nodes [add|drain|remove] | keyspace create | table distribute|reference|global|auto
|
|
839
864
|
range split|merge|move | tenant pin | rebalance | scale | verify | cleanup
|
|
840
865
|
each with --plan for the dry run (size, copy time,
|
|
841
866
|
expected pause); one that moves or deletes data asks,
|
|
@@ -844,6 +869,8 @@ ${o.err.trim()}`),d(" ran seed.sql"),!0}var ln="_snoutdata_migrations",Jw=`crea
|
|
|
844
869
|
place on its own if it is lost or corrupt; one
|
|
845
870
|
project slot each
|
|
846
871
|
snoutdata cluster alerts [seen <id>] --project REF what happened to the cluster that needs you
|
|
872
|
+
snoutdata cluster autopilot [off|advise|auto] the autopilot, off unless set: advise
|
|
873
|
+
records what it would run, auto runs it (asks first)
|
|
847
874
|
snoutdata cluster plan <operation> the same dry run
|
|
848
875
|
snoutdata cluster jobs [show|watch|cancel|resume <id>] the durable job log
|
|
849
876
|
snoutdata cluster settings [--max-write-pause-ms N ...] [--set advice_min_bytes=N,...]
|
|
@@ -879,11 +906,11 @@ that re-signs Postgres traffic.
|
|
|
879
906
|
Docs: https://docs.snoutdata.com/developers/cli
|
|
880
907
|
Examples: https://github.com/snoutdata/snoutdata (a star helps other people find it)
|
|
881
908
|
Source: https://github.com/snoutdata/snout-cli
|
|
882
|
-
`,
|
|
883
|
-
`)),0;if(
|
|
909
|
+
`,fk=["yes","token"];async function gk(e){let[t,n,...r]=e.command;if(M(e,"version")||t==="version")return w({version:we},()=>process.stdout.write(`${we}
|
|
910
|
+
`)),0;if(M(e,"help")&&t&&t!=="help"){let i=di(t,n);if(i.length>0)return w({commands:i.map(u=>({...u,flagHelp:Object.fromEntries(u.flags.map(l=>[l,Io(u.name,l)]))}))},()=>{for(let u of i){let l="args"in u&&u.args?` ${u.args}`:"",m=u.flags.length>0?` ${u.flags.join(" ")}`:"";process.stdout.write(` snoutdata ${u.name}${l}${m}
|
|
884
911
|
${u.summary}
|
|
885
|
-
`);let
|
|
912
|
+
`);let h=Math.max(0,...u.flags.map(P=>P.length));for(let P of u.flags)process.stdout.write(` ${P.padEnd(h)} ${Io(u.name,P)}
|
|
886
913
|
`)}process.stdout.write(`
|
|
887
914
|
Every command also takes --json, --quiet and --help.
|
|
888
|
-
`)}),0}let o=
|
|
889
|
-
`),i.token&&d(`via access token ${i.token.prefix} (${i.token.name})${i.token.project?`, limited to project ${i.token.project}`:""}.`)}),0}case"tokens":switch(n){case void 0:case"list":return await oi(),0;case"create":{let i=S(e,"name")??r[0];if(!i)throw new m("tokens create needs --name (what is it for, and where does it live)");return await ri({name:i,expiresInDays:_e(e,"expires"),project:S(e,"project")}),0}case"revoke":{let i=r[0];if(!i)throw new m("tokens revoke needs the token's id or its sdt_ prefix");return await si(i),0}default:throw new m(`unknown command: tokens ${n}`)}case"projects":switch(n){case void 0:case"list":return await Za(),0;case"create":{let i=S(e,"name")??r[0];if(!i)throw new m("projects create needs --name");let u=S(e,"team");return await so({teamId:u?await ni(u):void 0,name:i,region:S(e,"region"),wait:!N(e,"no-wait"),timeoutMs:s(),link:void 0,showUrl:N(e,"show-url")}),0}case"pause":case"resume":case"delete":return await Qa(n,S(e,"ref")??r[0]??a(),{wait:!N(e,"no-wait"),timeoutMs:s()}),0;case"show":{let i=S(e,"ref")??r[0]??a(),u=He(i);u?Da(u):await lc(i)}return 0;default:throw new m(`unknown command: projects ${n}`)}case"functions":{let i=l();if(i){if(n==="deploy"){if(!r[0])throw new m("functions deploy needs a name: snoutdata functions deploy <name>");return await Ga(i,r[0],{dir:S(e,"dir"),verifyJwt:!N(e,"no-verify-jwt")}),0}if(n===void 0||n==="list")return Wa(i),0;if((n==="delete"||n==="remove")&&r[0])return await za(i,r[0]),0}switch(n){case"deploy":{let u=r[0];if(!u)throw new m("functions deploy needs a name: snoutdata functions deploy <name>");return await pi(a(),u,{dir:S(e,"dir"),entrypoint:S(e,"entrypoint"),verifyJwt:!N(e,"no-verify-jwt")}),0}case void 0:case"list":return await gi(a()),0;case"size":{let u=r[0];if(!u)throw new m("functions size needs a name: snoutdata functions size <name> --memory MB --concurrency N");return await fi(a(),u,{memoryMb:_e(e,"memory"),concurrency:_e(e,"concurrency"),reset:N(e,"reset")}),0}case"logs":return await hi(a(),r[0],{errorsOnly:N(e,"errors"),limit:_e(e,"tail"),follow:N(e,"follow")}),0;case"delete":case"remove":{let u=r[0];if(!u)throw new m("functions delete needs a name");return await yi(a(),u),0}default:throw new m(`unknown command: functions ${n}`)}}case"containers":switch(n){case"deploy":{let i=r[0],u=r[1],c=S(e,"dockerfile");if(i&&c){if(u)throw new m("containers deploy takes an image or --dockerfile, not both");if(S(e,"registry-user")||N(e,"clear-registry"))throw new m("an image built with --dockerfile is uploaded to the project and needs no registry login");return await Ii(a(),i,{path:c,port:_e(e,"port"),wait:!N(e,"no-wait")}),0}if(!i||!u)throw new m("containers deploy needs a name and an image: snoutdata containers deploy <name> <image>, or <name> --dockerfile <dir>");let p=S(e,"registry-user");if(p&&!N(e,"stdin"))throw new m('a registry login takes its token on stdin: echo "$TOKEN" | snoutdata containers deploy <name> <image> --registry-user U --stdin');return await Ri(a(),i,{image:u,port:_e(e,"port"),registryUser:p,registryPassword:p?await vt():void 0,clearCredential:N(e,"clear-registry"),wait:!N(e,"no-wait")}),0}case void 0:case"list":return await Ci(a()),0;case"logs":return await Mi(a(),r[0],{errorsOnly:N(e,"errors"),limit:_e(e,"tail"),follow:N(e,"follow")}),0;case"restart":{if(!r[0])throw new m("containers restart needs a name");return await ji(a(),r[0]),0}case"delete":case"remove":{if(!r[0])throw new m("containers delete needs a name");return await Di(a(),r[0]),0}case"egress":{let i=r[0];if(i===void 0||i==="list"||i==="show")return await Yn(a(),{}),0;if(i==="clear")return await Yn(a(),{clear:!0}),0;if(i==="set"){let u=r.slice(1);if(u.length===0&&!N(e,"none"))throw new m("containers egress set needs at least one destination (api.example.com:443), or --none for nothing outside the project");if(u.length>0&&N(e,"none"))throw new m("containers egress set takes destinations or --none, not both");return await Yn(a(),{set:u}),0}throw new m(`unknown command: containers egress ${i}; it is egress, egress set or egress clear`)}default:throw new m(`unknown command: containers ${n}`)}case"secrets":{let i=l();if(i){if(n==="set")return await Va(i,await Bi(r,{stdin:N(e,"stdin")})),0;if(n===void 0||n==="list")return Ya(i),0;if((n==="unset"||n==="remove")&&r[0])return await Xa(i,r[0]),0}switch(n){case"set":return await Fi(a(),r,{stdin:N(e,"stdin")}),0;case void 0:case"list":return await Ki(a()),0;case"unset":case"remove":{let u=r[0];if(!u)throw new m("secrets unset needs a name");return await Hi(a(),u),0}default:throw new m(`unknown command: secrets ${n}`)}}case"keys":switch(n){case void 0:case"show":case"list":return await ai(a()),0;case"rotate":return await ii(a(),{force:N(e,"force")}),0;default:throw new m(`unknown command: keys ${n}`)}case"products":{if(n===void 0||n==="list")return await oc(a()),0;if(n==="enable"||n==="disable")return await sc(a(),rc(r[0]),n==="enable"),0;throw new m(`unknown command: products ${n}. products [enable|disable ${er.join("|")}]`)}case"push":{if(n!=="credentials")throw new m("push credentials [set apns|fcm | remove apns|fcm]");let i=r[0];if(i===void 0||i==="list")return await hl(a()),0;if(i==="set"){let u=Yo(r[1]),c=u==="apns"?await fl({p8:S(e,"p8"),keyId:S(e,"key-id"),teamId:S(e,"team-id"),topic:S(e,"topic"),environment:S(e,"environment")}):await gl(S(e,"file"));return await yl(a(),u,c),0}if(i==="remove"||i==="unset")return await wl(a(),Yo(r[1])),0;throw new m(`unknown command: push credentials ${i}`)}case"auth":return await nl(a(),n,r,{clientId:S(e,"client-id"),stdin:N(e,"stdin"),siteUrl:S(e,"site-url"),allow:S(e,"allow"),file:S(e,"file"),subject:S(e,"subject")}),0;case"realtime":{if(n===void 0||n==="inspect")return vc(a(),{channel:S(e,"channel"),watch:N(e,"watch")});if(n==="logs")return $c(a(),{channel:S(e,"channel"),since:S(e,"since")});throw new m(`unknown command: realtime ${n}. realtime inspect|logs`)}case"ai":{let i=r[0];switch(n){case void 0:case"status":return await Cc(a()),0;case"enable":case"disable":return await jc(a(),n==="enable"),0;case"models":if(i===void 0||i==="list")return await Mc(a()),0;if(i==="add")return await Bc(a(),r[1],{source:S(e,"source"),upstream:S(e,"model"),provider:S(e,"provider"),baseUrl:S(e,"base-url"),region:S(e,"region"),priceIn:S(e,"price-in"),priceOut:S(e,"price-out"),accessKeyId:S(e,"access-key-id"),authHeader:S(e,"auth-header"),stdin:N(e,"stdin")}),0;if(i==="remove"||i==="delete")return await Fc(a(),r[1]),0;throw new m(`unknown command: ai models ${i}. ai models [add|remove]`);case"providers":if(i===void 0||i==="list")return await Lc(a()),0;if(i==="remove"||i==="delete")return await Uc(a(),r[1]),0;throw new m(`unknown command: ai providers ${i}. ai providers [remove NAME]`);case"routes":if(i===void 0||i==="list")return await Hc(a()),0;if(i==="set")return await Kc(a(),r[1],{models:S(e,"models"),sort:S(e,"sort")}),0;if(i==="remove"||i==="delete")return await qc(a(),r[1]),0;throw new m(`unknown command: ai routes ${i}. ai routes [set NAME --models a,b | remove NAME]`);case"keys":if(i===void 0||i==="list")return await Gc(a()),0;if(i==="create")return await Wc(a(),{name:S(e,"name"),models:S(e,"models"),expiresDays:_e(e,"expires")}),0;if(i==="revoke")return await zc(a(),r[1]),0;throw new m(`unknown command: ai keys ${i}. ai keys [create|revoke]`);case"budgets":if(i===void 0||i==="list")return await Jc(a()),0;if(i==="set")return await Vc(a(),r[1],{amount:S(e,"amount"),period:S(e,"period"),soft:N(e,"soft")}),0;if(i==="remove"||i==="delete")return await Yc(a(),r[1]),0;throw new m(`unknown command: ai budgets ${i}. ai budgets [set|remove]`);case"usage":return await Xc(a(),{days:_e(e,"days"),by:S(e,"by")}),0;case"test":return Zc(a(),{model:S(e,"model"),prompt:S(e,"prompt")});default:throw new m(`unknown command: ai ${n}. ai [enable|disable|models|providers|keys|budgets|usage|test]`)}}case"domains":return await ac(a(),n,r[0]),0;case"teams":return await ti(),0;case"link":{if(N(e,"local")){let c;try{c=oa(n)}catch(f){throw new P("not-found",f instanceof Error?f.message:String(f))}let p=gt(process.cwd(),{ref:c.ref,...c.name?{name:c.name}:{},local:c.folder});return h({ref:c.ref,local:c.folder,api:c.apiUrl,path:p},()=>{d(`Linked ${c.name??c.ref}, the local project in ${c.folder} (${p}).`),d(`Its API is ${c.apiUrl}; its database is on 127.0.0.1:${c.dbPort}.`)}),0}let i=S(e,"ref")??n;if(!i)throw new m("link needs --ref");let u=gt(process.cwd(),{ref:i});return h({ref:i,path:u},()=>d(`Linked ${i} (${u}).`)),0}case"db":switch(n){case"url":return await Uo(a()),0;case"psql":return hc(a(),r);case"reset-password":return await ei(a()),0;case"access":return await _c(a(),r,{level:S(e,"level")}),0;case"push":return pl(a(),{dir:S(e,"dir"),dryRun:!!e.flags["dry-run"],outOfOrder:!!e.flags["out-of-order"]});case"restore":return N(e,"window")?(await ic(a()),0):S(e,"at")?(await cc(a(),S(e,"at"),S(e,"name")),0):bc(a(),{file:S(e,"file"),force:!!e.flags.force});case"export":return wc(a(),{timeoutMs:s(),out:S(e,"out"),statusOnly:!!e.flags.status});default:throw new m(n?`unknown command: db ${n}`:"db needs an action: url, psql, reset-password, access, export, push, restore")}case"gen":{if(n!=="types")throw new m(n?`unknown command: gen ${n}`:"gen needs an action: types");let i=N(e,"local")?await Ou(await un()):null,u=i?i.dbUrl:S(e,"db-url");return kl(r[0],{ref:u?void 0:a(),dbUrl:u,schemas:(S(e,"schema")??"public").split(",").map(c=>c.trim()).filter(Boolean),out:S(e,"out"),sql:i?.sql})}case"start":{let i=l();if(i)return Ca(i)}return Au(await un(),{port:_e(e,"port"),dir:S(e,"dir"),noMigrations:N(e,"no-migrations"),outOfOrder:N(e,"out-of-order")});case"stop":{let i=l();return i?ja(i):Ru(await un())}case"status":{let i=l();return i?Ia(i):Tu(await un())}case"usage":return Du(a(),{days:_e(e,"days"),history:!!e.flags.history});case"mcp":return Cu({version:ue,allowDelete:!!e.flags["allow-delete"]});case"init":return jb(e);case"upgrade":return Gu({check:N(e,"check")});default:throw new m(`unknown command: ${t}`)}}async function jb(e){let t=na({flag:S(e,"ref")});if(t.kind!=="create"){let o=t.ref;if(t.kind==="linked"?d(`This folder is already linked to ${o}.`):t.kind==="environment"&&d(`Using ${o}, from SNOUTDATA_PROJECT. This folder is not linked to it.`),N(e,"env")?await zu(o):await Uo(o),t.kind==="link"){let s=He(o),a=gt(process.cwd(),s?{ref:o,...s.name?{name:s.name}:{},local:s.folder}:{ref:o});d(`Linked this folder to ${o}${t.replaces?`, in place of ${t.replaces}`:""} (${a}).`)}return 0}let n=S(e,"name")??Db(process.cwd());d(`Creating a project called "${n}"\u2026`),await so({name:n,region:S(e,"region"),wait:!0,link:process.cwd(),showUrl:!N(e,"env")});let r=hn({cwd:process.cwd()});return r&&N(e,"env")&&await zu(r),0}async function zu(e){let{connection:t}=await Promise.resolve().then(()=>(re(),Ae)),n=await t(e),r=yc(process.cwd(),n.uri);d(`${r.replaced?"Updated":"Wrote"} DATABASE_URL in ${r.path}.`),r.ignored&&d("Added .env to .gitignore, since it holds the password.")}function Db(e){let t=e.split(/[\\/]/).filter(Boolean);return t[t.length-1]??"project"}function Mb(e){if(e instanceof P)return Ve(e.code,e.message,e.details),e.exitCode;if(e instanceof m)return Ve("usage",e.message),Le.usage;if(e instanceof Ye)return Ve("not-signed-in",e.message),Le["not-signed-in"];if(e instanceof ge){let r=Ue(e.status,e.serverCode);return Ve(r,e.message,{status:e.status,...e.serverCode?{serverCode:e.serverCode}:{}}),Le[r]}let t=ia(e);if(t)return Ve("usage",t),Le.usage;let n=bn(e);return Ve(n,e instanceof Error?e.message:String(e)),Le[n]}function Hs(e){process.exitCode=e,setTimeout(()=>process.exit(e),2e3).unref()}function Lb(e){let t=e.indexOf("--");return(t===-1?e:e.slice(0,t)).includes("--json")}async function Ub(){Lt(Lb(process.argv.slice(2)));let e;try{e=Gs(process.argv.slice(2))}catch(t){Ve("usage",String(t instanceof Error?t.message:t)),Hs(Le.usage);return}Lt(e.json),Sa(e.flags.quiet===!0),ca(_n({isTty:!!process.stdin.isTTY,json:e.json,env:process.env}));try{Hs(await Cb(e))}catch(t){Hs(Mb(t))}}Ub();
|
|
915
|
+
`)}),0}let o=M(e,"help")||t==="help";if(o||!t)return w({version:we,usage:Bm.trim(),commands:No},()=>process.stdout.write(Bm)),o?0:tt.usage;let s=()=>{let i=Ie(e,"timeout");if(i!==void 0){if(!(i>0))throw new g("--timeout is a number of seconds greater than zero");return i*1e3}},a=()=>{let i=Kn({flag:E(e,"ref")});if(!i)throw new g("no project: pass --ref, set SNOUTDATA_PROJECT, or run `snoutdata link --ref <ref>`");return i},c=()=>{let i=Kn({flag:E(e,"ref")});return i?st(i):null};for(let i of fk)if(e.flags[i]!==void 0&&t!=="cluster")throw new g(`--${i} is only for snoutdata cluster`);switch(t){case"cluster":return Cm(e,{timeoutMs:s()});case"login":{if(M(e,"device")){let m=await rr({name:Yn(),platform:process.platform});w({verifyUrl:m.verifyUrl,code:m.code,expiresAt:m.expiresAt},()=>{d(""),d(` Open ${m.verifyUrl}`),d(` Type ${m.code}`),d(""),d(b("Waiting for that to be approved\u2026"))});let h=await or(m.secret,{intervalMs:Math.max(1,m.interval)*1e3,expiresAt:m.expiresAt});qe({accessToken:h.token});let P=E(e,"email"),A=P?(await xt()).email??void 0:void 0;return P&&Jo(P,{accessToken:h.token,email:A}),w({ok:!0,name:h.name??null,email:A??null},()=>d(`Signed in${A?` as ${A}`:""}.`)),0}let i=E(e,"domain"),u=M(e,"sso")||i!==void 0;if(u&&E(e,"provider"))throw new g("--sso and --provider are two different doors: pass one or the other");let l=await cr({provider:E(e,"provider"),sso:u,domain:i,noBrowser:M(e,"no-browser")||!ve(),timeoutMs:s(),email:E(e,"email")});return w({email:l.email??null},()=>d(`Signed in${l.email?` as ${l.email}`:""}.`)),0}case"logout":{hi();let i=!!process.env.SNOUTDATA_ACCESS_TOKEN;return w({ok:!0,environmentTokenStillSet:i},()=>{d("Signed out."),i&&(d("SNOUTDATA_ACCESS_TOKEN is still set in this shell, so commands will keep working."),d(b(" unset SNOUTDATA_ACCESS_TOKEN")))}),0}case"whoami":{let i=await xt(),u=Hn()?.expectedEmail,l=!!(u&&i.email&&!Vo(u,i.email));return l&&ie(`This session is ${i.email}, and login asked for ${u}. Run "snoutdata login --email ${u}" to sign in as that account.`),w({...i,...u?{expectedEmail:u,matchesExpected:!l}:{}},()=>{process.stdout.write(`${i.email??i.id}
|
|
916
|
+
`),i.token&&d(`via access token ${i.token.prefix} (${i.token.name})${i.token.project?`, limited to project ${i.token.project}`:""}.`)}),0}case"tokens":switch(n){case void 0:case"list":return await kl(),0;case"create":{let i=E(e,"name")??r[0];if(!i)throw new g("tokens create needs --name (what is it for, and where does it live)");return await _l({name:i,expiresInDays:Ie(e,"expires"),project:E(e,"project")}),0}case"revoke":{let i=r[0];if(!i)throw new g("tokens revoke needs the token's id or its sdt_ prefix");return await vl(i),0}default:throw new g(`unknown command: tokens ${n}`)}case"projects":switch(n){case void 0:case"list":return await hl(),0;case"create":{let i=E(e,"name")??r[0];if(!i)throw new g("projects create needs --name");let u=E(e,"team");return await ns({teamId:u?await Sl(u):void 0,name:i,region:E(e,"region"),wait:!M(e,"no-wait"),timeoutMs:s(),link:void 0,showUrl:M(e,"show-url")}),0}case"pause":case"resume":case"delete":return await yl(n,E(e,"ref")??r[0]??a(),{wait:!M(e,"no-wait"),timeoutMs:s()}),0;case"show":{let i=E(e,"ref")??r[0]??a(),u=st(i);u?el(u):await Ac(i)}return 0;default:throw new g(`unknown command: projects ${n}`)}case"functions":{let i=c();if(i){if(n==="deploy"){if(!r[0])throw new g("functions deploy needs a name: snoutdata functions deploy <name>");return await cl(i,r[0],{dir:E(e,"dir"),verifyJwt:!M(e,"no-verify-jwt")}),0}if(n===void 0||n==="list")return dl(i),0;if((n==="delete"||n==="remove")&&r[0])return await ul(i,r[0]),0}switch(n){case"deploy":{let u=r[0];if(!u)throw new g("functions deploy needs a name: snoutdata functions deploy <name>");return await Tl(a(),u,{dir:E(e,"dir"),entrypoint:E(e,"entrypoint"),verifyJwt:!M(e,"no-verify-jwt")}),0}case void 0:case"list":return await Il(a()),0;case"size":{let u=r[0];if(!u)throw new g("functions size needs a name: snoutdata functions size <name> --memory MB --concurrency N");return await Nl(a(),u,{memoryMb:Ie(e,"memory"),concurrency:Ie(e,"concurrency"),reset:M(e,"reset")}),0}case"logs":return await Cl(a(),r[0],{errorsOnly:M(e,"errors"),limit:Ie(e,"tail"),follow:M(e,"follow")}),0;case"delete":case"remove":{let u=r[0];if(!u)throw new g("functions delete needs a name");return await Dl(a(),u),0}default:throw new g(`unknown command: functions ${n}`)}}case"containers":switch(n){case"deploy":{let i=r[0],u=r[1],l=E(e,"dockerfile");if(i&&l){if(u)throw new g("containers deploy takes an image or --dockerfile, not both");if(E(e,"registry-user")||M(e,"clear-registry"))throw new g("an image built with --dockerfile is uploaded to the project and needs no registry login");return await Xl(a(),i,{path:l,port:Ie(e,"port"),wait:!M(e,"no-wait")}),0}if(!i||!u)throw new g("containers deploy needs a name and an image: snoutdata containers deploy <name> <image>, or <name> --dockerfile <dir>");let m=E(e,"registry-user");if(m&&!M(e,"stdin"))throw new g('a registry login takes its token on stdin: echo "$TOKEN" | snoutdata containers deploy <name> <image> --registry-user U --stdin');return await zl(a(),i,{image:u,port:Ie(e,"port"),registryUser:m,registryPassword:m?await Gt():void 0,clearCredential:M(e,"clear-registry"),wait:!M(e,"no-wait")}),0}case void 0:case"list":return await Zl(a()),0;case"logs":return await tc(a(),r[0],{errorsOnly:M(e,"errors"),limit:Ie(e,"tail"),follow:M(e,"follow")}),0;case"restart":{if(!r[0])throw new g("containers restart needs a name");return await Ql(a(),r[0]),0}case"delete":case"remove":{if(!r[0])throw new g("containers delete needs a name");return await ec(a(),r[0]),0}case"egress":{let i=r[0];if(i===void 0||i==="list"||i==="show")return await vr(a(),{}),0;if(i==="clear")return await vr(a(),{clear:!0}),0;if(i==="set"){let u=r.slice(1);if(u.length===0&&!M(e,"none"))throw new g("containers egress set needs at least one destination (api.example.com:443), or --none for nothing outside the project");if(u.length>0&&M(e,"none"))throw new g("containers egress set takes destinations or --none, not both");return await vr(a(),{set:u}),0}throw new g(`unknown command: containers egress ${i}; it is egress, egress set or egress clear`)}default:throw new g(`unknown command: containers ${n}`)}case"secrets":{let i=c();if(i){if(n==="set")return await ml(i,await oc(r,{stdin:M(e,"stdin")})),0;if(n===void 0||n==="list")return fl(i),0;if((n==="unset"||n==="remove")&&r[0])return await gl(i,r[0]),0}switch(n){case"set":return await sc(a(),r,{stdin:M(e,"stdin")}),0;case void 0:case"list":return await ic(a()),0;case"unset":case"remove":{let u=r[0];if(!u)throw new g("secrets unset needs a name");return await ac(a(),u),0}default:throw new g(`unknown command: secrets ${n}`)}}case"keys":switch(n){case void 0:case"show":case"list":return await $l(a()),0;case"rotate":return await Pl(a(),{force:M(e,"force")}),0;default:throw new g(`unknown command: keys ${n}`)}case"products":{if(n===void 0||n==="list")return await vc(a()),0;if(n==="enable"||n==="disable")return await $c(a(),kc(r[0]),n==="enable"),0;throw new g(`unknown command: products ${n}. products [enable|disable ${Er.join("|")}]`)}case"push":{if(n!=="credentials")throw new g("push credentials [set apns|fcm | remove apns|fcm]");let i=r[0];if(i===void 0||i==="list")return await Dd(a()),0;if(i==="set"){let u=Ws(r[1]),l=u==="apns"?await Id({p8:E(e,"p8"),keyId:E(e,"key-id"),teamId:E(e,"team-id"),topic:E(e,"topic"),environment:E(e,"environment")}):await Cd(E(e,"file"));return await Md(a(),u,l),0}if(i==="remove"||i==="unset")return await jd(a(),Ws(r[1])),0;throw new g(`unknown command: push credentials ${i}`)}case"auth":return await _d(a(),n,r,{clientId:E(e,"client-id"),stdin:M(e,"stdin"),siteUrl:E(e,"site-url"),allow:E(e,"allow"),file:E(e,"file"),subject:E(e,"subject")}),0;case"realtime":{if(n===void 0||n==="inspect")return Hc(a(),{channel:E(e,"channel"),watch:M(e,"watch")});if(n==="logs")return Kc(a(),{channel:E(e,"channel"),since:E(e,"since")});throw new g(`unknown command: realtime ${n}. realtime inspect|logs`)}case"ai":{let i=r[0];switch(n){case void 0:case"status":return await Qc(a()),0;case"enable":case"disable":return await ed(a(),n==="enable"),0;case"models":if(i===void 0||i==="list")return await nd(a()),0;if(i==="add")return await sd(a(),r[1],{source:E(e,"source"),upstream:E(e,"model"),provider:E(e,"provider"),baseUrl:E(e,"base-url"),region:E(e,"region"),priceIn:E(e,"price-in"),priceOut:E(e,"price-out"),accessKeyId:E(e,"access-key-id"),authHeader:E(e,"auth-header"),stdin:M(e,"stdin")}),0;if(i==="remove"||i==="delete")return await ad(a(),r[1]),0;throw new g(`unknown command: ai models ${i}. ai models [add|remove]`);case"providers":if(i===void 0||i==="list")return await rd(a()),0;if(i==="remove"||i==="delete")return await od(a(),r[1]),0;throw new g(`unknown command: ai providers ${i}. ai providers [remove NAME]`);case"routes":if(i===void 0||i==="list")return await id(a()),0;if(i==="set")return await ld(a(),r[1],{models:E(e,"models"),sort:E(e,"sort")}),0;if(i==="remove"||i==="delete")return await cd(a(),r[1]),0;throw new g(`unknown command: ai routes ${i}. ai routes [set NAME --models a,b | remove NAME]`);case"keys":if(i===void 0||i==="list")return await dd(a()),0;if(i==="create")return await ud(a(),{name:E(e,"name"),models:E(e,"models"),expiresDays:Ie(e,"expires")}),0;if(i==="revoke")return await pd(a(),r[1]),0;throw new g(`unknown command: ai keys ${i}. ai keys [create|revoke]`);case"budgets":if(i===void 0||i==="list")return await md(a()),0;if(i==="set")return await fd(a(),r[1],{amount:E(e,"amount"),period:E(e,"period"),soft:M(e,"soft")}),0;if(i==="remove"||i==="delete")return await gd(a(),r[1]),0;throw new g(`unknown command: ai budgets ${i}. ai budgets [set|remove]`);case"usage":return await hd(a(),{days:Ie(e,"days"),by:E(e,"by")}),0;case"test":return yd(a(),{model:E(e,"model"),prompt:E(e,"prompt")});default:throw new g(`unknown command: ai ${n}. ai [enable|disable|models|providers|keys|budgets|usage|test]`)}}case"domains":return await Pc(a(),n,r[0]),0;case"teams":return await bl(),0;case"link":{if(M(e,"local")){let l;try{l=_i(n)}catch(h){throw new T("not-found",h instanceof Error?h.message:String(h))}let m=Mt(process.cwd(),{ref:l.ref,...l.name?{name:l.name}:{},local:l.folder});return w({ref:l.ref,local:l.folder,api:l.apiUrl,path:m},()=>{d(`Linked ${l.name??l.ref}, the local project in ${l.folder} (${m}).`),d(`Its API is ${l.apiUrl}; its database is on 127.0.0.1:${l.dbPort}.`)}),0}let i=E(e,"ref")??n;if(!i)throw new g("link needs --ref");let u=Mt(process.cwd(),{ref:i});return w({ref:i,path:u},()=>d(`Linked ${i} (${u}).`)),0}case"db":switch(n){case"url":return await Ds(a()),0;case"psql":return Dc(a(),r);case"reset-password":return await wl(a()),0;case"access":return await Uc(a(),r,{level:E(e,"level")}),0;case"push":return Od(a(),{dir:E(e,"dir"),dryRun:!!e.flags["dry-run"],outOfOrder:!!e.flags["out-of-order"]});case"restore":return M(e,"window")?(await xc(a()),0):E(e,"at")?(await Ec(a(),E(e,"at"),E(e,"name")),0):Lc(a(),{file:E(e,"file"),force:!!e.flags.force});case"export":return jc(a(),{timeoutMs:s(),out:E(e,"out"),statusOnly:!!e.flags.status});default:throw new g(n?`unknown command: db ${n}`:"db needs an action: url, psql, reset-password, access, export, push, restore")}case"gen":{if(n!=="types")throw new g(n?`unknown command: gen ${n}`:"gen needs an action: types");let i=M(e,"local")?await Pm(await jn()):null,u=i?i.dbUrl:E(e,"db-url");return Bd(r[0],{ref:u?void 0:a(),dbUrl:u,schemas:(E(e,"schema")??"public").split(",").map(l=>l.trim()).filter(Boolean),out:E(e,"out"),sql:i?.sql})}case"start":{let i=c();if(i)return Zi(i)}return km(await jn(),{port:Ie(e,"port"),dir:E(e,"dir"),noMigrations:M(e,"no-migrations"),outOfOrder:M(e,"out-of-order")});case"stop":{let i=c();return i?Qi(i):vm(await jn())}case"status":{let i=c();return i?Xi(i):$m(await jn())}case"usage":return Tm(a(),{days:Ie(e,"days"),history:!!e.flags.history});case"mcp":return Am({version:we,allowDelete:!!e.flags["allow-delete"]});case"init":return hk(e);case"upgrade":return Fm({check:M(e,"check")});default:throw new g(`unknown command: ${t}`)}}async function hk(e){let t=bi({flag:E(e,"ref")});if(t.kind!=="create"){let o=t.ref;if(t.kind==="linked"?d(`This folder is already linked to ${o}.`):t.kind==="environment"&&d(`Using ${o}, from SNOUTDATA_PROJECT. This folder is not linked to it.`),M(e,"env")?await Hm(o):await Ds(o),t.kind==="link"){let s=st(o),a=Mt(process.cwd(),s?{ref:o,...s.name?{name:s.name}:{},local:s.folder}:{ref:o});d(`Linked this folder to ${o}${t.replaces?`, in place of ${t.replaces}`:""} (${a}).`)}return 0}let n=E(e,"name")??yk(process.cwd());d(`Creating a project called "${n}"\u2026`),await ns({name:n,region:E(e,"region"),wait:!0,link:process.cwd(),showUrl:!M(e,"env")});let r=Kn({cwd:process.cwd()});return r&&M(e,"env")&&await Hm(r),0}async function Hm(e){let{connection:t}=await Promise.resolve().then(()=>(me(),Ke)),n=await t(e),r=Mc(process.cwd(),n.uri);d(`${r.replaced?"Updated":"Wrote"} DATABASE_URL in ${r.path}.`),r.ignored&&d("Added .env to .gitignore, since it holds the password.")}function yk(e){let t=e.split(/[\\/]/).filter(Boolean);return t[t.length-1]??"project"}function wk(e){if(e instanceof T)return pt(e.code,e.message,e.details),e.exitCode;if(e instanceof g)return pt("usage",e.message),tt.usage;if(e instanceof mt)return pt("not-signed-in",e.message),tt["not-signed-in"];if(e instanceof $e){let r=nt(e.status,e.serverCode);return pt(r,e.message,{status:e.status,...e.serverCode?{serverCode:e.serverCode}:{}}),tt[r]}let t=$i(e);if(t)return pt("usage",t),tt.usage;let n=Wn(e);return pt(n,e instanceof Error?e.message:String(e)),tt[n]}function ei(e){process.exitCode=e,setTimeout(()=>process.exit(e),2e3).unref()}function bk(e){let t=e.indexOf("--");return(t===-1?e:e.slice(0,t)).includes("--json")}async function Sk(){un(bk(process.argv.slice(2)));let e;try{e=li(process.argv.slice(2))}catch(t){pt("usage",String(t instanceof Error?t.message:t)),ei(tt.usage);return}un(e.json),Li(e.flags.quiet===!0),Pi(zn({isTty:!!process.stdin.isTTY,json:e.json,env:process.env}));try{ei(await gk(e))}catch(t){ei(wk(t))}}Sk();
|