snoutdata 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/snoutdata.mjs +436 -40
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ This CLI is built to be driven by a program. The contract, which every command h
|
|
|
43
43
|
| `8` | Over a quota | The plan's limit. Do not retry. |
|
|
44
44
|
| `9` | Network | Retry. |
|
|
45
45
|
| `10` | Timed out | Retry, or raise `--timeout`. |
|
|
46
|
+
| `127` | A tool this command needs is not installed | The message names it. The shell’s own convention, so a CI log reads right. |
|
|
46
47
|
|
|
47
48
|
### Credentials, without a browser
|
|
48
49
|
|
|
@@ -118,6 +119,36 @@ snoutdata db push [--dir migrations] [--dry-run] # run the .sql files, once eac
|
|
|
118
119
|
snoutdata db restore --file DUMP [--force]
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
**Types**
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
snoutdata gen types typescript [--ref R] [--schema public] [--out FILE]
|
|
126
|
+
snoutdata gen types typescript --db-url postgres://... # any Postgres, ours or not
|
|
127
|
+
snoutdata gen types typescript --local # the database `snoutdata start` runs
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Your schema as a TypeScript `Database` type, on stdout. Read out of `pg_catalog`, so an enum
|
|
131
|
+
column says `mood` and a generated column is optional on insert.
|
|
132
|
+
|
|
133
|
+
**Local**
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
snoutdata start [--port 54322] [--dir migrations] [--no-migrations]
|
|
137
|
+
snoutdata stop # the data stays
|
|
138
|
+
snoutdata status # running, how big, on what port
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
A Postgres on your own machine, the same build as a hosted project, with `migrations/*.sql` and
|
|
142
|
+
`seed.sql` applied on the way up, using the same migration ledger `db push` uses, so a folder means
|
|
143
|
+
the same thing here and in the cloud. It publishes on 54322 by default, which is where
|
|
144
|
+
`supabase start` puts its Postgres, so a `.env` written for one works against the other; pass
|
|
145
|
+
`--port` if you run both at once.
|
|
146
|
+
|
|
147
|
+
It needs **Podman** (not Docker, and deliberately with no fallback: a database that works under
|
|
148
|
+
Docker and not under Podman is one that works here and not in the cloud). The database image is
|
|
149
|
+
fetched from `ghcr.io/snoutdata/snoutpod-postgres` the first time you run it, which takes a few
|
|
150
|
+
minutes once and nothing after that. Nothing else in this CLI needs anything installed at all.
|
|
151
|
+
|
|
121
152
|
**Agents**
|
|
122
153
|
|
|
123
154
|
```bash
|
package/dist/snoutdata.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`,{mode:384}),
|
|
4
|
-
`),n}function
|
|
5
|
-
`);return}t()}function
|
|
6
|
-
`)}function
|
|
7
|
-
`),
|
|
8
|
-
`)}function
|
|
9
|
-
`)}function
|
|
10
|
-
`)}function
|
|
2
|
+
var Wo=Object.defineProperty;var E=(e,t)=>()=>(e&&(t=e(e=0)),t);var rn=(e,t)=>{for(var n in t)Wo(e,n,{get:t[n],enumerable:!0})};import{chmodSync as Yo,existsSync as sn,mkdirSync as an,readFileSync as cn,rmSync as Xo,writeFileSync as ln}from"node:fs";import{homedir as Ce}from"node:os";import{dirname as dt,join as un,parse as Zo,resolve as Qo}from"node:path";function ft(e=Ce()){return un(e,".snoutdata","auth.json")}function dn(e=Ce()){let t=process.env.SNOUTDATA_ACCESS_TOKEN;if(t)return{accessToken:t};let n=ft(e);if(!sn(n))return null;try{let r=JSON.parse(cn(n,"utf8"));return typeof r.accessToken=="string"?r:null}catch{return null}}function W(e,t=Ce()){let n=ft(t);an(dt(n),{recursive:!0}),ln(n,`${JSON.stringify(e,null,2)}
|
|
3
|
+
`,{mode:384}),Yo(n,384)}function fn(e=Ce()){Xo(ft(e),{force:!0})}function mn(e,t=Date.now()){return e.expiresAt?e.expiresAt*1e3-6e4<=t:!1}function pn(e){return un(e,".snoutdata","project.json")}function es(e){let t=Qo(e),n=Zo(t).root;for(;;){let r=pn(t);if(sn(r))try{let o=JSON.parse(cn(r,"utf8"));if(typeof o.ref=="string"&&o.ref)return{ref:o.ref,directory:t}}catch{}if(t===n)return null;t=dt(t)}}function je(e,t){let n=pn(e);return an(dt(n),{recursive:!0}),ln(n,`${JSON.stringify(t,null,2)}
|
|
4
|
+
`),n}function Le(e){let t=e.environment??process.env;return e.flag?e.flag:t.SNOUTDATA_PROJECT?t.SNOUTDATA_PROJECT:es(e.cwd??process.cwd())?.ref??null}var de=E(()=>{"use strict"});function Ue(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 ts){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 Fe(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 gn(e){mt=e}function fe(){return(mt??Ue({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).canAsk}function Be(){return(mt??Ue({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).reason}var ts,mt,ke=E(()=>{"use strict";ts=["CI","GITHUB_ACTIONS","GITLAB_CI","BUILDKITE","CIRCLECI","TF_BUILD"];mt=null});import{existsSync as ns,readFileSync as rs}from"node:fs";import{homedir as pt,hostname as os}from"node:os";import{join as me}from"node:path";function ss(e,t,n){if(t==="win32"){let r=n.APPDATA??me(pt(),"AppData","Roaming");return me(r,e)}return t==="darwin"?me(pt(),"Library","Application Support",e):me(n.XDG_CONFIG_HOME??me(pt(),".config"),e)}function yn(e=process.platform,t=process.env){return t.SNOUTDATA_DESKTOP_CONFIG?[t.SNOUTDATA_DESKTOP_CONFIG]:["snoutdata","snoutdata-dev"].map(n=>me(ss(n,e,t),"mcp.json"))}function as(e=process.platform,t=process.env){return yn(e,t)[0]}function is(e=as()){if(!ns(e))return null;try{let t=JSON.parse(rs(e,"utf8"));return typeof t.port!="number"||typeof t.token!="string"||!t.token?null:t}catch{return null}}function cs(e){return e?e.enabled===!1?"server-disabled":e.signInHandoff===!1?"handoff-disabled":null:"no-config"}async function He(e){if(e!==void 0)return hn(e);let t="no-config";for(let n of yn()){let r=is(n);if(!r)continue;let o=await hn(r);if(o.available)return o;(o.skip!=="not-running"||t==="no-config")&&(t=o.skip)}return{available:!1,skip:t}}async function hn(e){let t=cs(e);if(t)return{available:!1,skip:t};let n=e,r;try{let o=await fetch(`http://127.0.0.1:${n.port}/cli/whoami`,{headers:{Authorization:`Bearer ${n.token}`},signal:AbortSignal.timeout(ls)});if(!o.ok)return{available:!1,skip:"not-running"};r=await o.json()}catch{return{available:!1,skip:"not-running"}}return r.signedIn?r.canMintToken?{available:!0,who:r,config:n}:{available:!1,skip:"cannot-mint"}:{available:!1,skip:"signed-out"}}async function wn(e,t){let n=await fetch(`http://127.0.0.1:${e.port}/cli/token`,{method:"POST",headers:{Authorization:`Bearer ${e.token}`,"Content-Type":"application/json"},body:JSON.stringify({client:"snoutdata-cli",command:t}),signal:AbortSignal.timeout(6e4)}),r=await n.json().catch(()=>({}));return n.ok&&r.ok&&r.token?{token:r.token,name:r.name??"SnoutData CLI"}:{declined:!0,reason:r.error??`the app refused (${n.status})`}}function qe(){return os()}async function gt(e,t,n,r){let o=await fetch(`http://127.0.0.1:${e.port}/mcp`,{method:"POST",headers:{Authorization:`Bearer ${e.token}`,"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:Date.now(),method:t,params:n}),signal:AbortSignal.timeout(r)});if(!o.ok)throw new Error(`the app answered ${o.status}`);let s=await o.json();if(s.error)throw new Error(s.error.message??"the app refused");return s.result}async function bn(e){return await gt(e,"initialize",{protocolVersion:"2025-06-18",capabilities:{},clientInfo:{name:"snoutdata-cli",version:"0"}},3e3),(await gt(e,"tools/list",{},3e3)).tools??[]}async function Sn(e,t,n){return gt(e,"tools/call",{name:t,arguments:n},12e4)}var ls,Ke=E(()=>{"use strict";ls=1500});function k(e,t,n){throw new Q(e,t,n)}function xn(e){return e===403?"forbidden":e===404?"not-found":e===409?"conflict":e===402||e===429?"quota":e===503?"not-ready":e===504?"timeout":e>=500?"server":"failed"}function vn(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(us.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 ee,Q,us,ie=E(()=>{"use strict";ee={ok:0,failed:1,usage:2,"not-signed-in":3,"not-ready":4,forbidden:5,"not-found":6,conflict:7,quota:8,network:9,timeout:10,"tool-missing":127,server:1},Q=class extends Error{code;details;constructor(t,n,r){super(n),this.name="CliFailure",this.code=t,this.details=r}get exitCode(){return ee[this.code]}};us=new Set(["ECONNREFUSED","ECONNRESET","ENOTFOUND","EAI_AGAIN","EHOSTUNREACH","ENETUNREACH","EPIPE","CERT_HAS_EXPIRED","UNABLE_TO_VERIFY_LEAF_SIGNATURE","DEPTH_ZERO_SELF_SIGNED_CERT"])});async function Pn(e,t){let n=await fetch(`${V}/functions/v1/${e}`,{method:"POST",headers:{apikey:te,"Content-Type":"application/json"},body:JSON.stringify(t)}),r=await n.json().catch(()=>({}));return{status:n.status,body:r}}async function Je(e={}){let{status:t,body:n}=await Pn("cloud-device-start",{client:e});return t===429&&k("quota",n.error??"too many pairing attempts from here: wait a few minutes"),t===404&&k("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)&&k("server",n.error??`could not start a pairing (${t})`),n}async function We(e,t){let n=t.intervalMs,r=Date.parse(t.expiresAt);for(;;){Date.now()>r&&k("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 Pn("cloud-device-poll",{secret:e});switch(o===404&&k("not-found","that pairing is not known to the server. Run the command again for a new code."),o===400&&k("usage",s.error??"that is not a pairing secret"),o!==200&&k("server",s.error??`the pairing could not be checked (${o})`),s.status){case"approved":return{token:s.token,name:s.name};case"denied":k("forbidden",s.error??"that pairing was refused in the browser");case"used":k("conflict",s.error??"that pairing has already been completed");case"expired":k("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 ht=E(()=>{"use strict";C();ie()});function xe(e){Ve=e}function Tn(e){En=e}function P(e){return _n?`\x1B[2m${e}\x1B[0m`:e}function T(e){return _n?`\x1B[1m${e}\x1B[0m`:e}function w(e,t){if(Ve){process.stdout.write(`${JSON.stringify(e,null,2)}
|
|
5
|
+
`);return}t()}function l(e){!Ve&&!En&&process.stderr.write(`${e}
|
|
6
|
+
`)}function ce(e,t,n){process.stderr.write(`${t}
|
|
7
|
+
`),Ve&&process.stdout.write(`${JSON.stringify({ok:!1,code:e,error:t,...n},null,2)}
|
|
8
|
+
`)}function H(e){process.stderr.write(`${e}
|
|
9
|
+
`)}function q(e){if(e.length===0)return"";let t=e[0].map((n,r)=>Math.max(...e.map(o=>(o[r]??"").length)));return e.map(n=>n.map((r,o)=>r.padEnd(t[o]??0)).join(" ").trimEnd()).join(`
|
|
10
|
+
`)}function G(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[u,d]of o){if(s<u)break;s=Math.round(s/u),a=d}let c=`${s} ${a}${s===1?"":"s"}`;return n?`in ${c}`:`${c} ago`}var Ve,En,_n,U=E(()=>{"use strict";Ve=!1,En=!1;_n=process.stdout.isTTY&&!process.env.NO_COLOR});import{createHash as An,randomBytes as yt}from"node:crypto";import{createServer as $n}from"node:http";import{spawn as fs}from"node:child_process";function Ge(e){return e.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function ze(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.":xs(t??"Something went wrong.");return`<!doctype html><html><head><meta charset="utf-8"><title>SnoutData</title>
|
|
11
11
|
<meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
12
12
|
<body style="font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;background:#050403;color:#e9e4dc;display:grid;place-items:center;min-height:100vh;margin:0">
|
|
13
13
|
<div style="text-align:center;max-width:460px;padding:40px 24px">
|
|
@@ -19,40 +19,416 @@ var tn=Object.defineProperty;var O=(e,t)=>()=>(e&&(t=e(e=0)),t);var nn=(e,t)=>{f
|
|
|
19
19
|
<p style="color:#8f887e;margin:0;font-size:15px;line-height:1.5">${o}</p>
|
|
20
20
|
${e==="ok"?"":'<p style="color:#8f887e;margin:14px 0 0;font-size:14px">Run <code style="background:#1b1917;border:1px solid #262320;border-radius:6px;padding:2px 6px;font-size:13px">snoutdata login</code> to try again.</p>'}
|
|
21
21
|
</div>
|
|
22
|
-
</body></html>`}function
|
|
23
|
-
`),e.noBrowser?
|
|
24
|
-
`),e.noBrowser?
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
</body></html>`}function Rn(){let e=Ge(yt(32)),t=Ge(An("sha256").update(e).digest());return{verifier:e,challenge:t}}function ms(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 On(e){let{command:t,args:n,verbatim:r}=ms(process.platform,e),o=fs(t,n,{stdio:"ignore",detached:!0,windowsVerbatimArguments:r});o.on("error",()=>{}),o.unref()}function ys(e){let t=e??Ge(yt(32));return{raw:t,hashed:An("sha256").update(t).digest("hex")}}function ws(e){return`${ps}?${new URLSearchParams({client_id:Nn,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 bs(e,t){let n=new URL(e,"http://127.0.0.1");if(!n.searchParams.has("code")&&!n.searchParams.has("error"))return{kind:"ignore"};let r=n.searchParams.get("error_description")??n.searchParams.get("error");if(r)return{kind:"error",message:r};let o=n.searchParams.get("code");return o?n.searchParams.get("state")!==t?{kind:"error",message:"the browser came back with a state that did not match"}:{kind:"ok",code:o}:{kind:"error",message:"the browser came back without a code"}}async function Ss(e,t,n){let r=await fetch(gs,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:Nn,client_secret:hs,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 ks(e){let{verifier:t,challenge:n}=Rn(),{raw:r,hashed:o}=ys(),s=Ge(yt(16)),{code:a,redirectUri:c}=await new Promise((m,i)=>{let f="",p=$n((h,g)=>{let S=bs(h.url??"/",s);if(S.kind==="ignore"){g.writeHead(404).end();return}g.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),g.end(S.kind==="ok"?ze("ok"):ze("failed",S.message)),p.close(),S.kind==="ok"?m({code:S.code,redirectUri:f}):i(new Error(S.message))});p.listen(0,"127.0.0.1",()=>{let h=p.address();f=`http://127.0.0.1:${typeof h=="object"&&h?h.port:0}`;let S=ws({redirectUri:f,challenge:n,hashedNonce:o,state:s});process.stdout.write(`${S}
|
|
23
|
+
`),e.noBrowser?l("Open that in a browser, on any machine, and sign in."):(l("Opening that in your browser. If nothing opens, paste it into one."),On(S))}),p.on("error",i),setTimeout(()=>{p.close(),i(new Error("timed out waiting for the browser"))},e.timeoutMs??3e5).unref()}),u=await Ss(a,t,c),d=await fetch(`${V}/auth/v1/token?grant_type=id_token`,{method:"POST",headers:{apikey:te,"Content-Type":"application/json"},body:JSON.stringify({provider:"google",id_token:u,nonce:r})});if(!d.ok)throw new Error(`could not turn the Google sign-in into a session: ${(await d.text()).slice(0,200)}`);return Dn(await d.json())}function Dn(e){let t=e,n={accessToken:t.access_token,refreshToken:t.refresh_token,expiresAt:t.expires_at,email:t.user?.email};return W(n),n}async function Ye(e={}){let t=e.provider??"google";if(t==="google")return ks(e);let{verifier:n,challenge:r}=Rn(),o=await new Promise((a,c)=>{let u=$n((m,i)=>{let f=new URL(m.url??"/","http://127.0.0.1"),p=f.searchParams.get("code"),y=f.searchParams.get("error_description")??f.searchParams.get("error");i.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),i.end(p?ze("ok"):ze("failed",y??"No code came back.")),u.close(),p?a(p):c(new Error(y??"the browser came back without a code"))});u.listen(0,"127.0.0.1",()=>{let m=u.address(),f=`http://127.0.0.1:${typeof m=="object"&&m?m.port:0}/callback`,p=new URL(`${V}/auth/v1/authorize`);p.searchParams.set("provider",t),p.searchParams.set("redirect_to",f),p.searchParams.set("code_challenge",r),p.searchParams.set("code_challenge_method","s256"),p.searchParams.set("prompt","select_account"),process.stdout.write(`${p.toString()}
|
|
24
|
+
`),e.noBrowser?l("Open that in a browser, on any machine, and sign in."):(l("Opening that in your browser. If nothing opens, paste it into one."),On(p.toString()))}),setTimeout(()=>{u.close(),c(new Error("timed out waiting for the browser"))},e.timeoutMs??3e5).unref()}),s=await fetch(`${V}/auth/v1/token?grant_type=pkce`,{method:"POST",headers:{apikey:te,"Content-Type":"application/json"},body:JSON.stringify({auth_code:o,code_verifier:n})});if(!s.ok)throw new Error(`could not exchange the code: ${(await s.text()).slice(0,200)}`);return Dn(await s.json())}function xs(e){return e.replace(/[&<>"]/g,t=>({"&":"&","<":"<",">":">",'"':"""})[t])}var ps,gs,Nn,hs,wt=E(()=>{"use strict";C();de();U();ps="https://accounts.google.com/o/oauth2/v2/auth",gs="https://oauth2.googleapis.com/token",Nn="1064562458607-9fqmghjoqjr7d56b98agkvjt9vt26pdc.apps.googleusercontent.com",hs="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"});import{createInterface as vs}from"node:readline/promises";async function bt(e){let t=vs({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 Ps(){let e=await He();if(!e.available)return(e.skip==="handoff-disabled"||e.skip==="server-disabled")&&l(P("The SnoutData app is running but is not set to hand out sign-ins.")),null;let t=e.who.email??"the signed-in account";if(l(""),l(`The SnoutData app is open here, signed in as ${T(t)}.`),l(P("It can create an access token for this terminal. Your sign-in itself is not handed over,")),l(P("and you can revoke the token later with `snoutdata tokens revoke`.")),!await bt("Use it?"))return null;l("Asking the app. Approve it there.");let n=await wn(e.config,process.argv.slice(2).join(" ").slice(0,60)||"snoutdata");if("declined"in n)return l(`The app did not create a token: ${n.reason}`),null;let r={accessToken:n.token};return W(r),l(`Signed in with "${n.name}".`),r}async function Es(){if(!await bt("Sign in with a browser?"))return null;try{let e=await Ye({});return l(`Signed in${e.email?` as ${e.email}`:""}.`),{accessToken:e.accessToken,refreshToken:e.refreshToken,expiresAt:e.expiresAt}}catch(e){return l(`That did not finish: ${e instanceof Error?e.message:String(e)}`),null}}async function Ts(){if(!await bt("Sign in by typing a code into a browser on another machine?"))return null;let e=await Je({name:qe(),platform:process.platform});l(""),l(` Open ${T(e.verifyUrl)}`),l(` Type ${T(e.code)}`),l(""),l(P("The code is typed rather than in the link on purpose: it is what proves the person")),l(P("at the browser is the person at this terminal. Waiting\u2026"));let t=await We(e.secret,{intervalMs:Math.max(1,e.interval)*1e3,expiresAt:e.expiresAt}),n={accessToken:t.token};return W(n),l(`Signed in${t.name?` with "${t.name}"`:""}.`),n}async function In(){if(!fe())return null;let e=await Ps();if(e)return e;let t=await Es();return t||Ts()}var Mn=E(()=>{"use strict";de();Ke();ht();ke();wt();U()});var he={};rn(he,{ANON_KEY:()=>te,ApiError:()=>z,FUNCTIONS:()=>Cn,NotSignedIn:()=>ne,SUPABASE_URL:()=>V,call:()=>v,connection:()=>K,exportStatus:()=>ge,listProjects:()=>Y,listTeams:()=>Xe,requestExport:()=>ve,waitForReady:()=>Pe,whoami:()=>pe});function _s(){let e=Be();return e?`not signed in, and ${Fe(e)}. Set SNOUTDATA_ACCESS_TOKEN to a token from \`snoutdata tokens create\`.`:"not signed in: run `snoutdata login`, or set SNOUTDATA_ACCESS_TOKEN"}async function As(){let e=dn();if(e||(e=await In()),!e)throw new ne;if(!mn(e)||!e.refreshToken)return e.accessToken;let t=await fetch(`${V}/auth/v1/token?grant_type=refresh_token`,{method:"POST",headers:{apikey:te,"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e.refreshToken})});if(!t.ok)throw new ne;let n=await t.json(),r={accessToken:n.access_token,refreshToken:n.refresh_token,expiresAt:n.expires_at,email:e.email};return W(r),r.accessToken}async function v(e,t={},n="POST"){let r=await As(),o=n==="GET"&&typeof t=="object"&&t?`?${new URLSearchParams(t).toString()}`:"",s=await fetch(`${Cn}/${e}${o}`,{method:n,headers:{Authorization:`Bearer ${r}`,apikey:te,"Content-Type":"application/json"},body:n==="GET"?void 0:JSON.stringify(t)}),a=await s.text(),c;try{c=a?JSON.parse(a):{}}catch{throw new z(s.status,a.slice(0,300)||s.statusText)}if(!s.ok){let u=typeof c=="object"&&c&&"error"in c?String(c.error):`${s.status} ${s.statusText}`;throw s.status===401?new ne(u):new z(s.status,u)}return c}async function pe(){return v("cloud-whoami")}async function Y(){return v("cloud-project-list")}async function K(e){return v("cloud-project-connection",{ref:e})}async function Xe(){return v("cloud-team-list")}async function ve(e){return v("cloud-project-export",{ref:e})}async function ge(e){return v("cloud-project-export",{ref:e},"GET")}async function Pe(e,t={}){let n=Date.now()+(t.timeoutMs??3e5);for(;;){let{projects:r}=await Y(),o=r.find(s=>s.ref===e);if(!o)throw new z(404,`no project ${e}`);if(t.onTick?.(o.state),o.state==="ready")return o;if(Date.now()>=n)throw new z(504,`${e} is still ${o.state} after waiting`);await new Promise(s=>setTimeout(s,2e3))}}var V,te,Cn,z,ne,C=E(()=>{"use strict";de();ke();Mn();V=process.env.SNOUTDATA_SUPABASE_URL??"https://krlhuuvhhgkwfsutvxtb.supabase.co",te=process.env.SNOUTDATA_ANON_KEY??"sb_publishable_B0gbZinAncINGbDGOcP9rg_odcbriG3",Cn=`${V}/functions/v1`,z=class extends Error{status;constructor(t,n){super(n),this.status=t}},ne=class extends Error{constructor(t){super(t??_s())}}});function Ct(e,t){let n=256-256%t.length;for(;;){let r=e(1)[0];if(r<n)return t[r%t.length]}}function vr(e,t,n){let r="";for(;r.length<n;)r+=Ct(e,t);return r}var Mt,xr,Pr=E(()=>{"use strict";Mt="0123456789abcdefghjkmnpqrstvwxyz",xr="abcdefghjkmnpqrstvwxyz"});function _r(e){return Ct(e,Er)+vr(e,Mt,Tr-1)}function Ba(e){return Fa.test(e)}function J(e){if(!Ba(e))throw new Error(`not a project ref: ${JSON.stringify(e)}`);return e}function R(e,t=Ha){return J(e),{ref:e,database:e,ownerRole:`${e}_owner`,pod:`snoutpod-${e}`,host:`${e}.${t}`}}var Er,Tr,Fa,Ha,re=E(()=>{"use strict";Pr();Er=xr,Tr=13,Fa=new RegExp(`^[${Er}][${Mt}]{${Tr-1}}$`);Ha="db.snoutdata.com"});var Ar=E(()=>{"use strict";re()});function Rr(e,t){let n=$r(e,7),r=$r(t,24);return Math.max(2,Math.ceil(n*24/r))}function $r(e,t){return typeof e=="number"&&Number.isFinite(e)&&e>0?e:t}var jt=E(()=>{"use strict"});function Or(e){let t=e.trim();if(!/^\d+$/.test(t))return null;let n=Number(t);return Number.isSafeInteger(n)?n:null}var Lt=E(()=>{"use strict"});var nu,Nr=E(()=>{"use strict";re();nu=7*24});var ou,Dr=E(()=>{"use strict";ou=7*24*60*60*1e3});var qa,Ir=E(()=>{"use strict";Dr();qa=60*60*1e3});var Mr=E(()=>{"use strict"});var Cr=E(()=>{"use strict"});var fu,mu,jr=E(()=>{"use strict";fu=2*1024*1024,mu=64*1024});var Lr,Ur,Ka,Fr=E(()=>{"use strict";Lr="/snoutfn",Ur="/_snoutpod/health",Ka=`// Written by the SnoutData host agent. Do not edit on the host: it is rewritten
|
|
25
|
+
// every time the agent starts, from packages/snoutpod/src/host/functionMain.ts.
|
|
26
|
+
const MOUNT = ${JSON.stringify(Lr)};
|
|
27
|
+
const HEALTH = ${JSON.stringify(Ur)};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Manifests, cached by the file's modification time.
|
|
31
|
+
*
|
|
32
|
+
* The agent writes a manifest by rename, so a reader either sees the whole old one or the
|
|
33
|
+
* whole new one. mtime is what says which \u2014 cheaper than re-reading and re-parsing a file
|
|
34
|
+
* with secrets in it on every invocation, and exact, unlike a TTL.
|
|
35
|
+
*/
|
|
36
|
+
const manifests = new Map();
|
|
37
|
+
|
|
38
|
+
async function manifestFor(ref) {
|
|
39
|
+
const path = MOUNT + '/projects/' + ref + '.json';
|
|
40
|
+
let stat;
|
|
41
|
+
try {
|
|
42
|
+
stat = await Deno.stat(path);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const stamp = stat.mtime ? stat.mtime.getTime() : 0;
|
|
47
|
+
const held = manifests.get(ref);
|
|
48
|
+
if (held && held.stamp === stamp) {
|
|
49
|
+
return held.manifest;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const manifest = JSON.parse(await Deno.readTextFile(path));
|
|
53
|
+
manifests.set(ref, { stamp, manifest });
|
|
54
|
+
return manifest;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('manifest for ' + ref + ' could not be read: ' + (error?.message ?? error));
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function refuse(status, message, hint) {
|
|
62
|
+
// The same JSON shape the front door's refusals use, so a customer sees one error
|
|
63
|
+
// format from this system rather than one per component.
|
|
64
|
+
return new Response(JSON.stringify({ message, hint: hint ?? undefined }), {
|
|
65
|
+
status,
|
|
66
|
+
headers: { 'content-type': 'application/json' }
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The function a path names.
|
|
72
|
+
*
|
|
73
|
+
* The front door strips \`/functions/v1\`, so what arrives is \`/<name>\` or
|
|
74
|
+
* \`/<name>/anything\`. The rule is the same one \`functionNameFromPath\` applies in the
|
|
75
|
+
* control package -- letters, digits, hyphens and underscores, no dot and no slash --
|
|
76
|
+
* because the door decides whether THIS function may be called without a key using that
|
|
77
|
+
* function, and a name that meant two different things in the two places is a way past it.
|
|
78
|
+
*/
|
|
79
|
+
function functionName(pathname) {
|
|
80
|
+
const first = pathname.replace(/^\\/+/, '').split('/')[0] ?? '';
|
|
81
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(first) ? first : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Deno.serve(async (req) => {
|
|
85
|
+
const url = new URL(req.url);
|
|
86
|
+
|
|
87
|
+
// Before anything else, and needing no project: this is what the agent polls and what
|
|
88
|
+
// decides whether the front door believes the runtime is up.
|
|
89
|
+
if (url.pathname === HEALTH) {
|
|
90
|
+
return new Response('ok\\n', { headers: { 'content-type': 'text/plain' } });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Whose request this is, written by the front door from the hostname it resolved.
|
|
94
|
+
// NOT from anything the client sent: a shared server that took the tenant from a
|
|
95
|
+
// client's own header would let anybody address anybody (the Phase B finding).
|
|
96
|
+
const ref = req.headers.get('x-snoutdata-ref');
|
|
97
|
+
if (!ref) {
|
|
98
|
+
return refuse(400, 'This request did not arrive through the front door.', 'Call https://<ref>.api.snoutdata.com/functions/v1/<name>');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const name = functionName(url.pathname);
|
|
102
|
+
if (!name) {
|
|
103
|
+
return refuse(404, 'No function was named in this request.', 'The path is /functions/v1/<name>');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const manifest = await manifestFor(ref);
|
|
107
|
+
const deployed = manifest?.functions?.find((one) => one.name === name);
|
|
108
|
+
if (!deployed) {
|
|
109
|
+
// The same answer for "this project has deployed nothing" and "not that one".
|
|
110
|
+
// A project's function names are not a secret, but neither are they a list to
|
|
111
|
+
// hand out one 404 at a time to somebody walking the namespace.
|
|
112
|
+
return refuse(404, 'There is no function called ' + name + ' in this project.', 'Deploy it with: snoutdata functions deploy ' + name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const servicePath = MOUNT + '/bundles/' + deployed.digest;
|
|
116
|
+
const limits = manifest.limits ?? {};
|
|
117
|
+
try {
|
|
118
|
+
const worker = await EdgeRuntime.userWorkers.create({
|
|
119
|
+
servicePath,
|
|
120
|
+
memoryLimitMb: limits.memoryMb ?? 128,
|
|
121
|
+
workerTimeoutMs: limits.wallMs ?? 10000,
|
|
122
|
+
cpuTimeSoftLimitMs: limits.cpuMs ?? 2000,
|
|
123
|
+
cpuTimeHardLimitMs: limits.hardCpuMs ?? 3500,
|
|
124
|
+
noModuleCache: false,
|
|
125
|
+
importMapPath: null,
|
|
126
|
+
// THE WHOLE OF WHAT THIS WORKER MAY SEE. A user worker does not inherit the
|
|
127
|
+
// main worker's environment (measured), so this list is not a filter over
|
|
128
|
+
// something larger -- it is everything.
|
|
129
|
+
envVars: Object.entries(manifest.env ?? {}),
|
|
130
|
+
// False, so identical requests share a warm isolate. The isolate is still one
|
|
131
|
+
// project's: \`servicePath\` is the digest of THIS project's bundle, and two
|
|
132
|
+
// projects that deployed byte-identical code have different digests because a
|
|
133
|
+
// bundle is stored per project (migration 041).
|
|
134
|
+
forceCreate: false
|
|
135
|
+
});
|
|
136
|
+
return await worker.fetch(req);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const detail = error?.message ?? String(error);
|
|
139
|
+
console.error(ref + '/' + name + ': ' + detail);
|
|
140
|
+
// 500 and the runtime's own words. A function that ran out of time or memory
|
|
141
|
+
// should hear which -- the alternative is the front door's 502, which says only
|
|
142
|
+
// that something upstream did not answer and cannot be acted on.
|
|
143
|
+
return refuse(500, 'This function could not be run.', detail);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
`});function Ut(e,t,n){if(!n.test(t))throw new Error(`${e} is not a value this can route: ${JSON.stringify(t)}`);return t}function tt(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(r!=null)return Wa(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 Wa(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(Ut("the subnet",n,Br),Ut("the sink",r,Ja),!qr(r,n))throw new Error(`the sink ${r} is not inside ${n}, so denied traffic would be routed at the host`);if(e.gateway&&!qr(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",`${Ut("a denied prefix",s,Br)},${r}`);return o.push(e.name),o}function Hr(e){return e.split(".").reduce((t,n)=>t*256+Number(n),0)}function qr(e,t){let[n,r]=t.split("/"),o=Number(r),s=o===0?0:4294967295<<32-o>>>0;return(Hr(e)&s)>>>0===(Hr(n??"")&s)>>>0}function Kr(e){return e?.internal===!0}var Br,Ja,Ft=E(()=>{"use strict";Br=/^((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])$/,Ja=/^((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 Va,nt=E(()=>{"use strict";Va=7*24*60*60});var Jr=E(()=>{"use strict";re();jr();Fr();Ft();nt()});function Vr(e){let t=[];for(let n of e.split(`
|
|
147
|
+
`)){let r=n.trim();if(!r)continue;let[o,s,a,c]=r.split("|");!o||s===void 0||t.push({name:o,active:s==="t",walStatus:a??"",inactiveSeconds:c?Number(c):null})}return t}var Wr,Bt=E(()=>{"use strict";Wr="select slot_name, active, wal_status, coalesce(extract(epoch from (now() - inactive_since))::bigint::text, '') from pg_replication_slots"});var Mu,Cu,Gr=E(()=>{"use strict";re();nt();Mu=6*60*60,Cu=5*60});var zr=E(()=>{"use strict"});var Yr,gd,hd,yd,wd,Xr=E(()=>{"use strict";Ar();jt();Lt();Nr();Ir();Mr();Cr();Jr();Bt();Gr();zr();nt();Yr=120,gd=15*60*1e3,hd=60*60*1e3,yd=60*60*1e3,wd=10*60*1e3});import{execFile as za}from"node:child_process";function Zr(){return(e,t,n={})=>new Promise((r,o)=>{let s=za(e,[...t],{timeout:n.timeoutMs??0,maxBuffer:8388608,env:n.env?{...process.env,...n.env}:process.env},(a,c,u)=>{if(a&&typeof a.code=="string"){o(a);return}let d=a&&typeof a.code=="number"?a.code:0;r({code:d,stdout:String(c),stderr:String(u)})});n.stdin!==void 0&&s.stdin?.end(n.stdin)})}async function $(e,t,n,r){let o=await e(t,n,r);if(o.code!==0){let s=(o.stderr||o.stdout).trim().split(`
|
|
148
|
+
`).slice(-3).join("; ");throw new Error(`${t} ${n[0]??""} failed (${o.code}): ${s}`)}return o.stdout}var rt=E(()=>{"use strict"});import{readdir as Ya,readFile as vd}from"node:fs/promises";async function Qr(e,t,n={}){let r=n.binary??"podman",o=R(t).pod,s=await e(r,["volume","inspect",o,"--format","json"]);if(s.code!==0)return!1;let a=JSON.parse(s.stdout||"[]"),u=(Array.isArray(a)?a[0]:a)?.Mountpoint;return u?await(n.look??Xa)(u)!=="empty":!1}async function Xa(e){try{return(await Ya(e)).length>0?"data":"empty"}catch(t){return t.code==="ENOENT"?"empty":"hidden"}}var eo=E(()=>{"use strict";re();rt()});function to(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}function ro(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=${to("the access key",e.key,Qa)}`),n.push("--env",no)),e.endpoint&&n.push("--env",`SNOUTPOD_S3_ENDPOINT=${to("the endpoint",e.endpoint,Za)}`),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 oo(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");return{[no]:e.secret}}var no,Za,Qa,so=E(()=>{"use strict";no="SNOUTPOD_S3_SECRET",Za=/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:[0-9]{1,5})?$/i,Qa=/^[A-Za-z0-9/+=_.-]{1,256}$/});import{createHash as ei,randomBytes as ti}from"node:crypto";import{access as ni,constants as ri}from"node:fs/promises";import{delimiter as oi,join as si}from"node:path";function Jt(e){if(!ii.test(e))throw new Error(`not a service name: ${JSON.stringify(e)}`);return e}function Re(e){if(!ci.test(e))throw new Error(`not a database role: ${JSON.stringify(e)}`);return e}function li(e,t,n){return`postgres://${Re(t)}:${n}@127.0.0.1:5432/${J(e)}`}function ao(e,t,n,r){let o={...t.env};if(t.dbUriEnv&&t.dbUriRole){let s=n.get(t.dbUriRole);if(s===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]=li(e.ref,t.dbUriRole,s)}return t.smtpSecretEnv&&r&&(o[t.smtpSecretEnv]=r),o}function io(e,t){return`${R(e).pod}-${Jt(t)}`}function ot(e){return`snoutpod-host-${Jt(e)}`}function ui(e){let t=Object.keys(e.env).sort().map(n=>[n,e.env[n]]);return ei("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 di(e,t){let n=[],r=qt(t);for(let o of e??[])if(Jt(o.name),Kt(o.port,`${o.name}: port`),!(o.hostPort===null||o.hostPort===void 0)){Kt(o.hostPort,`${o.name}: hostPort`);for(let s of r)n.push("--publish",`${s}:${o.hostPort}:${o.port}`)}return n}function qt(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 Kt(e,t){if(!Number.isInteger(e)||e<1||e>65535)throw new Error(`${t} is not a port: ${JSON.stringify(e)}`);return e}function lo(e){let t=[];for(let n of Object.keys(e)){if(!fi.test(n))throw new Error(`not an environment variable name: ${JSON.stringify(n)}`);t.push("--env",n)}return t}function mi(e){let t=e.healthPath;if(t==null||t==="")return[];if(!mo.test(t))throw new Error(`${e.name}: not a health path: ${JSON.stringify(t)}`);return e.hostPort===null||e.hostPort===void 0?[]:(Kt(e.hostPort,`${e.name}: hostPort`),["--label",`${Wt}=${e.hostPort}:${t}`])}function pi(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||!mo.test(r)?null:{port:n,path:r}}async function hi(e,t={}){let n=t.path??process.env.PATH??"",r=t.look??(o=>ni(o,ri.X_OK));for(let o of n.split(oi))if(o)try{let s=si(o,e);return await r(s),s}catch{}return null}function yi(){return["\\set ON_ERROR_STOP 0","select 'create extension if not exists pg_prewarm' where exists (select 1 from pg_available_extensions where name = 'pg_prewarm') \\gexec","with candidate as ("," select c.oid, 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(pg_prewarm(oid)), 0) from budgeted","where bytes > 0 and running <= pg_size_bytes(current_setting('shared_buffers')) * 3 / 4;",""].join(`
|
|
149
|
+
`)}function bi(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:uo;return t.push(`SNOUTPOD_MAX_CONNECTIONS=${Math.max(r,uo)}`),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 Si(e){let t=null;return()=>(t===null&&(t=n()),t);async function n(){if(e.runtime===null)return[];let r=e.runtime??gi;return e.runtime!==void 0&&e.runtime!==""?["--runtime",r]:await hi(r,{look:e.lookExecutable})===null?[]:["--runtime",r]}}function go(e){let t=e.binary??"podman",n=e.exec,r=Si(e),o=e.probe??po;async function s(i){try{await n(t,["exec","--interactive",R(i).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",i,"--quiet","--file","-"],{timeoutMs:wi,stdin:yi()})}catch{}}async function a(){let i=await $(n,t,["ps","--all","--filter",`label=${we}`,"--format","json"]),f=i.trim()?JSON.parse(i):[],p=new Map;for(let h of f){let g=h.Labels?.[we],S=h.Labels?.[Ae];if(!g||!S)continue;let O=p.get(g)??[];O.push(S),p.set(g,O)}let y=[];for(let h of f){let g=h.Labels?.[we];if(!g||h.Labels?.[Ae])continue;let S=Number(h.Labels?.[Ht]);y.push({ref:g,status:ki(h.State),generation:Number.isInteger(S)?S:null,hasData:await e.hasData(g),services:p.get(g)??[]})}return y}async function c(i){let f=R(i.ref),p={SNOUTPOD_OWNER_PASSWORD:i.ownerPassword,POSTGRES_PASSWORD:i.adminPassword,...oo(e.repository)},y=[...await r(),"run","--detach","--name",f.pod,"--label",`${we}=${i.ref}`,"--label",`${Ht}=${i.generation}`,"--userns=auto","--memory",`${i.memoryMb}m`,...vi(i.cpus),"--restart","no",...e.network?["--network",e.network]:[],...i.port?qt(e.podBind).flatMap(h=>["--publish",`${h}:${i.port}:5432`]):[],...di(i.services,e.podBind),"--volume",`${f.pod}:/var/lib/postgresql/data:U`,"--env","SNOUTPOD_OWNER_PASSWORD","--env","POSTGRES_PASSWORD","--env",`SNOUTPOD_REF=${i.ref}`,"--env","POSTGRES_USER=snoutpod_admin","--env","POSTGRES_DB=postgres","--env",`SNOUTPOD_S3_BUCKET=${i.bucket}`,"--env",`SNOUTPOD_S3_REGION=${i.region}`,...bi(i.tuning).flatMap(h=>["--env",h]),"--env",`SNOUTPOD_RETENTION_FULL=${Rr(i.backup?.retentionDays,i.backup?.fullIntervalHours)}`,...ro(e.repository),i.image,"snoutpod-postgres"];await $(n,t,y,{env:p})}async function u(i){let f=await $(n,t,["ps","--all","--filter",`label=${we}=${J(i)}`,"--filter",`label=${Ae}`,"--format","json"]),p=f.trim()?JSON.parse(f):[],y=[];for(let h of p){let g=h.Labels?.[Ae];g&&y.push({name:g,container:io(i,g)})}return y}async function d(i){let f=[];for(let S of i.services??[])S.dbUriEnv&&S.dbUriRole&&!f.includes(S.dbUriRole)&&f.push(Re(S.dbUriRole));let p=new Map;if(f.length===0)return p;let y={},h=[],g=[];return f.forEach((S,O)=>{let A=ti(24).toString("base64url"),se=`SNOUTPOD_SERVICE_PASSWORD_${O}`;p.set(S,A),y[se]=A,h.push("--env",se),g.push(`\\getenv service_password_${O} ${se}`),g.push(`\\set service_role_${O} ${S}`),g.push(`alter role :"service_role_${O}" with password :'service_password_${O}';`)}),g.push(""),await $(n,t,["exec","--interactive",...h,R(i.ref).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:y,stdin:g.join(`
|
|
150
|
+
`)}),p}async function m(i){for(let y of await u(i.ref))await $(n,t,["rm","--force","--ignore",y.container]);let f=await d(i),p;(i.services??[]).some(y=>y.smtpSecretEnv)&&(p=await e.smtpSecret?.().catch(()=>{}));for(let y of i.services??[]){let h=io(i.ref,y.name),g=[...await r(),"run","--detach","--name",h,"--label",`${we}=${i.ref}`,"--label",`${Ht}=${i.generation}`,"--label",`${Ae}=${y.name}`,"--network",`container:${R(i.ref).pod}`,"--userns",`container:${R(i.ref).pod}`,"--memory",`${y.memoryMb}m`,"--restart","no",...mi(y),...lo(ao(i,y,f,p)),y.image];await $(n,t,g,{env:ao(i,y,f,p)})}}return{list:a,create:c,createServices:m,async start(i){await $(n,t,["start",R(i).pod])},async startServices(i){for(let f of await u(i))await $(n,t,["start",f.container])},async applyHostServices(i){let f=[],p=await $(n,t,["ps","--all","--filter",`label=${$e}`,"--format","json"]),y=p.trim()?JSON.parse(p):[],h=new Map;for(let S of y){let O=S.Labels?.[$e];O&&h.set(O,{spec:S.Labels?.[co]??"",state:S.State??""})}for(let S of i)try{await g(S)}catch(O){f.push({name:S.name,action:"failed",reason:O instanceof Error?O.message:String(O)})}async function g(S){let O=e.hostServiceLocal?.(S,i),A=O?{...S,...O.env&&Object.keys(O.env).length>0?{env:{...S.env,...O.env}}:{},...O.volumes?{volumes:O.volumes}:{},...O.network?{network:O.network}:{}}:S,se=ot(A.name),nn=ui(A),ae=h.get(A.name);if(h.delete(A.name),ae&&ae.spec===nn){ae.state!=="running"&&(await $(n,t,["start",se]),f.push({name:A.name,action:"started",reason:ae.state}));return}let Jo=ae?"the spec changed":"it was not here";ae&&await $(n,t,["rm","--force","--ignore",se]),await $(n,t,[...await r(),"run","--detach","--name",se,"--label",`${$e}=${A.name}`,"--label",`${co}=${nn}`,...A.healthPath?["--label",`${Wt}=${A.hostPort}:${A.healthPath}`]:[],"--memory",`${A.memoryMb}m`,"--restart","no",...A.network?["--network",A.network]:e.network?["--network",e.network]:[],...(A.volumes??[]).flatMap(lt=>["--volume",lt]),...qt(e.podBind).flatMap(lt=>["--publish",`${lt}:${A.hostPort}:${A.port}`]),...A.adminPort&&A.adminHostPort?["--publish",`127.0.0.1:${A.adminHostPort}:${A.adminPort}`]:[],...lo(A.env),A.image,...A.command??[]],{env:{...A.env}}),f.push({name:A.name,action:ae?"recreated":"created",reason:Jo})}for(let S of h.keys())await $(n,t,["rm","--force","--ignore",ot(S)]),f.push({name:S,action:"removed",reason:"it is no longer on the list"});return f},async hostServiceHealth(i){let f=await $(n,t,["ps","--all","--filter",`label=${$e}`,"--format","json"]),p=(f.trim()?JSON.parse(f):[]).map(h=>h.Labels?.[$e]??"").filter(h=>h!=="");if(p.length===0)return[];let y=Date.now()+i;for(;;){let h=[];for(let g of p)h.push(await fo(n,t,g,ot(g),o));if(h.every(g=>g.ok)||Date.now()>=y)return h;await new Promise(g=>setTimeout(g,500))}},async serviceHealth(i,f){let p=await u(i);if(p.length===0)return[];let y=Date.now()+f;for(;;){let h=[];for(let g of p)h.push(await fo(n,t,g.name,g.container,o));if(h.every(g=>g.ok)||Date.now()>=y)return h;await new Promise(g=>setTimeout(g,500))}},async stop(i,f){for(let p of await u(i))await $(n,t,["stop","--time",String(ai),p.container]);await $(n,t,["stop","--time",String(f),R(i).pod])},async remove(i){for(let f of await u(i))await $(n,t,["rm","--force","--ignore",f.container]);await $(n,t,["rm","--force","--ignore",R(i).pod])},async removeData(i){await $(n,t,["volume","rm","--force",R(i).pod])},async applyOwnerPassword(i){let f=["\\getenv owner_password SNOUTPOD_OWNER_PASSWORD",`\\set owner_role ${i.ref}_owner`,`alter role :"owner_role" with password :'owner_password';`,""].join(`
|
|
151
|
+
`);await $(n,t,["exec","--interactive","--env","SNOUTPOD_OWNER_PASSWORD",R(i.ref).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:{SNOUTPOD_OWNER_PASSWORD:i.ownerPassword},stdin:f})},async applyRolePassword(i,f,p){let y=["\\getenv role_password SNOUTPOD_ROLE_PASSWORD",`\\set target_role ${Re(f)}`,`alter role :"target_role" with password :'role_password';`,""].join(`
|
|
152
|
+
`);await $(n,t,["exec","--interactive","--env","SNOUTPOD_ROLE_PASSWORD",R(i).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{env:{SNOUTPOD_ROLE_PASSWORD:p},stdin:y})},async ensureHostServiceDatabase(i,f,p){let y=ot(i),h=Re(f),g=Re(p),S=A=>["exec",y,"psql","--username",h,"--dbname","postgres","--quiet","-tA","-v","ON_ERROR_STOP=1","-c",A];(await $(n,t,S(`select 1 from pg_database where datname = '${g}'`))).trim()!=="1"&&await $(n,t,S(`create database "${g}"`))},async replicationSlots(i){let f=await $(n,t,["exec",R(i).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",i,"-tA","-F","|","-c",Wr]);return Vr(f)},async dropReplicationSlot(i,f){if(!/^[a-z0-9_]{1,63}$/.test(f))throw new Error(`not a replication slot name: ${JSON.stringify(f)}`);await $(n,t,["exec",R(i).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",i,"-v","ON_ERROR_STOP=1","-c",`select pg_drop_replication_slot('${f}')`])},async renameProject(i,f){f!==i.ref&&await $(n,t,["exec","--interactive",R(i.ref).pod,"psql","--username","snoutpod_admin","--dbname","postgres","--quiet","-v","ON_ERROR_STOP=1","--file","-"],{stdin:xi(f,i.ref)})},async waitReady(i,f){let p=R(i).pod,y=Date.now()+f;for(;;){let h=await n(t,["exec",p,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname","postgres","-tAc","select not pg_is_in_recovery()"]);if(h.code===0&&h.stdout.trim()==="t")return s(i),!0;if(Date.now()>=y)return!1;await new Promise(g=>setTimeout(g,500))}},async capacity(){let i=await a(),f=await e.capacity();return{pods:i.length,...f}},async databaseBytes(i){let f=await n(t,["exec",R(i).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",i,"-tAc","select pg_database_size(current_database())"]);return f.code===0?Or(f.stdout):null},async setReadOnly(i,f){let p=R(i);await $(n,t,["exec",p.pod,"psql","--username","snoutpod_admin","--dbname","postgres","-tAc",`alter database ${p.database} set default_transaction_read_only = ${f?"on":"off"}`])},async archiveState(i){let f=await n(t,["exec",R(i).pod,"psql","--host","127.0.0.1","--username","snoutpod_admin","--dbname",i,"-tA","-F","|","-c",`select coalesce(to_char(last_archived_time, 'YYYY-MM-DD"T"HH24:MI:SS.MSOF'), ''), failed_count from pg_stat_archiver`]);if(f.code!==0)return null;let[p="",y=""]=f.stdout.trim().split(`
|
|
153
|
+
`)[0]?.split("|")??[],h=Number(y);return{lastArchivedAt:p.trim()||null,failures:Number.isInteger(h)&&h>=0?h:0}}}}async function fo(e,t,n,r,o=po){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 i=JSON.parse(s.stdout);a=i[0]?.State,c=i[0]?.Config?.Labels}catch{return{name:n,ok:!1,detail:"podman inspect could not be read"}}let u=a?.Status??"unknown";if(u!=="running")return{name:n,ok:!1,detail:u};let d=pi(c?.[Wt]);if(!d)return{name:n,ok:!0,detail:"running"};let m=await o(d.port,d.path);return m===null?{name:n,ok:!1,detail:"not answering yet"}:{name:n,ok:!0,detail:`answered ${m}`}}function ki(e){switch((e??"").toLowerCase()){case"running":return"running";case"created":case"configured":return"starting";case"exited":case"stopped":return"stopped";default:return"unknown"}}function xi(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(`
|
|
154
|
+
`)}function vi(e){return typeof e!="number"||!Number.isFinite(e)||e<=0?[]:["--cpus",e.toFixed(2)]}var we,Ht,Ae,ai,ii,ci,$e,co,fi,mo,Wt,po,gi,wi,uo,ho=E(()=>{"use strict";re();jt();Bt();rt();so();Lt();we="snoutpod.ref",Ht="snoutpod.generation",Ae="snoutpod.service",ai=10,ii=/^[a-z][a-z0-9-]{0,30}$/;ci=/^[a-z][a-z0-9_]{0,62}$/;$e="snoutpod.host-service",co="snoutpod.host-service-spec";fi=/^[A-Za-z_][A-Za-z0-9_]*$/;mo=/^\/[A-Za-z0-9\/_\-.~%?=&]*$/,Wt="snoutpod.health";po=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()})},gi="crun";wi=3e4,uo=200});var To={};rn(To,{DEFAULT_LOCAL_MEMORY_MB:()=>bo,DEFAULT_LOCAL_PORT:()=>_i,ENGINE:()=>M,ENGINE_NAME:()=>zt,IMAGE:()=>ue,NETWORK:()=>oe,NO_BLACKHOLE_ROUTES:()=>ko,PUBLIC_IMAGE:()=>be,capacity:()=>So,ensureImage:()=>Oi,ensureNetwork:()=>Ci,exec:()=>F,hasData:()=>st,localPsql:()=>Ui,localRuntime:()=>Mi,localSpec:()=>Po,localStatus:()=>Yt,localUri:()=>vo,networkPolicyInPlace:()=>xo,newLocalPassword:()=>$i,newLocalRef:()=>Ai,podLog:()=>Eo,podName:()=>at,podmanReady:()=>Di,removeLocal:()=>Hi,startLocal:()=>Li,stopLocal:()=>Bi});import{randomBytes as wo}from"node:crypto";import{existsSync as yo}from"node:fs";import{platform as Gt}from"node:os";import{delimiter as Pi,join as Ei}from"node:path";function Ti(){if(process.env.SNOUTPOD_ENGINE)return process.env.SNOUTPOD_ENGINE;let e=Gt()==="win32"?["podman.exe","podman"]:["podman"];for(let t of(process.env.PATH??"").split(Pi))if(t&&e.some(n=>yo(Ei(t,n))))return"podman";return yo("/opt/podman/bin/podman")?"/opt/podman/bin/podman":"podman"}function Ai(){return _r(e=>wo(e))}function $i(){return wo(18).toString("base64url")}async function Oi(e,t=F){if(process.env.SNOUTPOD_IMAGE||(await t(M,["image","exists",ue])).code===0)return null;e?.(`Fetching the database image (${be}). This happens once.`);let n=await t(M,["pull",be],{timeoutMs:Ni}).catch(o=>({code:1,stdout:"",stderr:o instanceof Error?o.message:String(o)}));if(n.code!==0)return`could not fetch the database image ${be}: ${n.stderr.trim()||n.stdout.trim()||"the pull failed"}`;let r=await t(M,["tag",be,ue]);return r.code!==0?`fetched ${be} but could not tag it as ${ue}: ${r.stderr.trim()}`:(e?.("Got it."),null)}async function Di(e=Ri){if(zt!=="podman")return`SNOUTPOD_ENGINE=${zt}: 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 t;try{t=await F(M,["--version"])}catch{return`${M} 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(t.code!==0)return`${M} --version failed: ${t.stderr.trim()||t.stdout.trim()}`;if((await F(M,["image","exists",ue])).code!==0)return`the pod image ${ue} is not on this machine. ${e}`;let r=await F(M,["machine","inspect","--format","{{.Rootful}}"]);return r.code===0&&r.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 Ii(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 F(M,["machine","ssh",t]).then(n=>{let r=n.stdout.trim().split(`
|
|
155
|
+
`).at(-1);return r==="DATA"?"data":r==="EMPTY"?"empty":"hidden"},()=>"hidden")}function st(e){let t=Gt()==="darwin"||Gt()==="win32";return Qr(F,e,{binary:M,look:t?Ii:void 0})}async function So(){return{ramFreeMb:4096,ramTotalMb:8192,diskFreeMb:32768,diskTotalMb:65536}}function Mi(e){return go({exec:F,binary:M,network:oe,hasData:st,capacity:So,repository:e})}function at(e){return R(e).pod}async function Ci(e){if(process.env.SNOUTPOD_LOCAL_ALLOW_EGRESS==="1"){await ji()||await F(M,tt({name:oe}));return}let t=tt({name:oe,allow:[]}),n="";for(let r=0;r<10;r+=1){if(await xo())return;await e?.(),await F(M,["network","rm","--force",oe]).catch(()=>{});let o=await F(M,t);if(o.code===0)return;if(n=`${o.stderr}${o.stdout}`.trim(),/invalid route gateway/i.test(n))throw new Error(`${ko}: ${n}`);await new Promise(s=>setTimeout(s,500))}throw new Error(`could not create ${oe} with egress blocked${n?`: ${n}`:""}`)}async function ji(){return(await F(M,["network","inspect",oe,"--format","json"])).code===0}async function xo(){let e=await F(M,["network","inspect",oe,"--format","json"]);if(e.code!==0)return!1;try{return Kr(JSON.parse(e.stdout)?.[0])}catch{return!1}}function vo(e){let t=R(e.ref);return`postgres://${t.ownerRole}:${encodeURIComponent(e.ownerPassword)}@127.0.0.1:${e.port}/${t.database}?sslmode=disable`}function Po(e,t={}){return J(e.ref),{ref:e.ref,generation:1,image:t.image??ue,memoryMb:t.memoryMb??bo,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 Vt(e,t){e.onProgress?.(t)}async function Li(e,t,n={}){let r=Po(t,n),o=n.timeoutMs??12e4,s=n.hasData??st,a=(await e.list()).find(c=>c.ref===t.ref);if(a?a.status==="stopped"?(Vt(n,"Starting the database."),await e.start(t.ref),await e.startServices(t.ref)):Vt(n,"The database is already running."):(Vt(n,await s(t.ref)?"Starting the database over its existing data.":"Creating the database."),await e.create(r)),!await e.waitReady(t.ref,o))throw new Error(await Fi(t.ref,o,n.podLog??Eo));return await e.applyOwnerPassword(r),await e.createServices(r),Yt(e,t,n)}async function Ui(e,t){let n=R(e.ref);return F(M,["exec","--interactive","-e","PGPASSWORD",at(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 Eo(e){let t=await F(M,["logs","--tail","20",at(e)]).catch(()=>null);return t?`${t.stdout}${t.stderr}`:"(no log: the container is not there)"}async function Fi(e,t,n){let r=await n(e).catch(()=>"(no log)");return`${at(e)} never became ready in ${t}ms
|
|
156
|
+
--- pod log ---
|
|
157
|
+
${r}`}async function Bi(e,t,n={}){J(t);let r=(await e.list()).find(o=>o.ref===t);return r&&r.status!=="stopped"&&await e.stop(t,Yr),Yt(e,{ref:t,port:0,ownerPassword:"",adminPassword:""},n)}async function Hi(e,t,n){J(t),await e.remove(t),n&&await e.removeData(t)}async function Yt(e,t,n={}){J(t.ref);let r=(await e.list()).find(a=>a.ref===t.ref),o=r?.status==="running",s=n.hasData??st;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?vo(t):null}}var zt,M,ue,oe,_i,bo,F,Ri,be,Ni,ko,_o=E(()=>{"use strict";Xr();rt();eo();Ft();ho();re();zt=process.env.SNOUTPOD_ENGINE??"podman";M=Ti(),ue=process.env.SNOUTPOD_IMAGE??"snoutpod/postgres:17",oe=process.env.SNOUTPOD_LOCAL_NETWORK??"snoutpod-local",_i=54322,bo=512,F=Zr();Ri="cd packages/snoutpod && npm run image:build",be="ghcr.io/snoutdata/snoutpod-postgres:17";Ni=20*60*1e3;ko="this podman cannot blackhole a route, so the local tier cannot isolate its containers"});var x=class extends Error{},Vo=new Set(["name","region","ref","file","provider","expires","timeout","out","team","dir","days","schema","db-url","data-api-version","postgrest-version","port","entrypoint"]);function on(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]=zo(o);if(Vo.has(s)){let c=a??e[++r];if(c===void 0)throw new x(`--${s} needs a value`);n[s]=c;continue}if(a!==void 0)throw new x(`--${s} does not take a value`);if(!Go.has(s))throw new x(`unknown option --${s}`);n[s]=!0}return{command:t,flags:n,json:n.json===!0}}var Go=new Set(["json","help","version","force","no-verify-jwt","stdin","quiet","status","no-wait","no-browser","device","env","dry-run","out-of-order","allow-delete","local","no-migrations","history"]);function zo(e){let t=e.replace(/^--?/,""),n=t.indexOf("=");return n===-1?[t,void 0]:[t.slice(0,n),t.slice(n+1)]}function N(e,t){let n=e.flags[t];return typeof n=="string"?n:void 0}function B(e,t){return e.flags[t]===!0}function Se(e,t){let n=N(e,t);if(n===void 0)return;let r=Number(n);if(!Number.isInteger(r))throw new x(`--${t} must be a whole number, not "${n}"`);return r}var ut=[{name:"init",summary:"Create or link a project and write a DATABASE_URL",flags:["--name","--region","--env","--ref"]},{name:"login",summary:"Sign in through a browser, or with --device by typing a code into one anywhere",flags:["--provider","--no-browser","--device","--timeout"]},{name:"logout",summary:"Forget the stored session",flags:[]},{name:"whoami",summary:"Who this credential belongs to",flags:[]},{name:"tokens list",summary:"Long-lived access tokens on this account",flags:[]},{name:"tokens create",summary:"Mint an sdt_ token for CI or an agent",flags:["--name","--expires"]},{name:"tokens revoke",summary:"Revoke one by id or prefix",flags:[]},{name:"projects list",summary:"Every project on this account",flags:[]},{name:"projects create",summary:"Make a hosted database",flags:["--name","--region","--team","--no-wait","--timeout"]},{name:"projects pause",summary:"Stop a project",flags:["--ref"]},{name:"projects resume",summary:"Start a paused project",flags:["--ref"]},{name:"projects delete",summary:"Delete a project",flags:["--ref"]},{name:"functions deploy",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 delete",summary:"Remove one",flags:["--ref"]},{name:"secrets set",summary:"Set the environment functions 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 anon and service_role API keys 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 export",summary:"Take or download a dump",flags:["--ref","--out","--status","--timeout"]},{name:"db push",summary:"Apply migrations from a folder",flags:["--dir","--dry-run","--out-of-order"]},{name:"db restore",summary:"Put a dump into a project",flags:["--file","--force"]},{name:"mcp",summary:"Serve these operations as tools over stdio",flags:["--allow-delete"]}];C();ie();ke();de();ht();Ke();U();wt();C();C();de();U();async function jn(){let{projects:e,allowance:t}=await Y();w({projects:e,allowance:t},()=>{if(e.length===0){l('No projects yet. `snoutdata projects create --name "my project"`.');return}process.stdout.write(`${q([["REF","NAME","STATE","REGION","LAST CONNECTION"],...e.map(n=>[n.ref,n.name,n.readOnly?T("read-only"):n.state==="ready"?n.state:T(n.state),n.region,G(n.lastConnectionAt)])])}
|
|
158
|
+
`)})}async function St(e){let t=await v("cloud-project-create",{name:e.name,region:e.region,teamId:e.teamId}),n=t.project.ref;l(`Created ${n}.`);let r=t.project;e.wait&&(l("Waiting for it to start\u2026"),r=await Pe(n,{timeoutMs:e.timeoutMs,onTick:o=>l(P(` ${o}`))})),e.link&&je(e.link,{ref:n,name:r.name}),w({project:r,password:t.password,uri:`postgres://${r.user}:${encodeURIComponent(t.password)}@${r.host}:5432/${r.database}?sslmode=require`},()=>{process.stdout.write(`postgres://${r.user}:${encodeURIComponent(t.password)}@${r.host}:5432/${r.database}?sslmode=require
|
|
159
|
+
`),l(""),l(`Ready. ${T(n)} in ${r.region}.`),l("The password is stored for you: `snoutdata db url` prints this again.")})}async function Ln(e,t){let n=`cloud-project-${e}`,r=await v(n,{ref:t});w(r,()=>{l(r.changed?`${t}: ${e}d.`:`${t} was already ${e==="delete"?"deleted":`${e}d`}.`)})}async function Un(e){let t=await v("cloud-project-reset-password",{ref:e});w(t,()=>{process.stdout.write(`${t.password}
|
|
160
|
+
`),l(`New password for ${t.user}. It applies ${t.appliesIn}.`)})}async function Fn(){let{teams:e}=await Xe();w({teams:e},()=>{if(e.length===0){l("You are not on any teams.");return}process.stdout.write(`${q([["NAME","ID","SHARE"],...e.map(t=>[t.name,t.id,t.mayShare?"yes":t.why??"no"])])}
|
|
161
|
+
`)})}async function Bn(e){let{teams:t}=await Xe(),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}C();U();async function Hn(e){let t=await v("cloud-token-create",{name:e.name,expiresInDays:e.expiresInDays});w(t,()=>{process.stdout.write(`${t.token}
|
|
162
|
+
`),l(""),l(`${T(t.name)} \u2014 ${t.expiresAt?`expires ${t.expiresAt.slice(0,10)}`:"does not expire"}.`),l("This is the only time it is shown: only its hash is stored."),l(P(" export SNOUTDATA_ACCESS_TOKEN=<that token>"))})}async function qn(){let{tokens:e}=await v("cloud-token-list");w({tokens:e},()=>{if(e.length===0){l("No access tokens. `snoutdata tokens create --name ci`.");return}process.stdout.write(`${q([["PREFIX","NAME","STATE","LAST USED","EXPIRES"],...e.map(t=>[t.current?T(t.prefix):t.prefix,t.name,t.revokedAt?"revoked":t.live?"live":"expired",G(t.lastUsedAt),t.expiresAt?t.expiresAt.slice(0,10):"never"])])}
|
|
163
|
+
`)})}async function Kn(e){let t=e.startsWith("sdt_")?{prefix:e}:{id:e},n=await v("cloud-token-revoke",t);w(n,()=>{l(n.changed?`Revoked ${n.token.prefix} (${n.token.name}).`:`${n.token.prefix} was already revoked.`)})}C();U();async function Jn(e){let t=await K(e);w({ref:t.ref,anonKey:t.anonKey,serviceRoleKey:t.serviceRoleKey,issuedAt:t.keysIssuedAt,expiresAt:t.keysExpireAt},()=>{if(!t.anonKey||!t.serviceRoleKey){l(`${e} has no API keys yet. Ask again in a moment.`);return}process.stdout.write(`${Vn(t.anonKey,t.serviceRoleKey)}
|
|
164
|
+
`),Os(t.keysIssuedAt)})}async function Wn(e,t){if(!t.force)throw l(`Rotating the keys for ${e} stops every anon and service_role key already pasted anywhere.`),l("Anything holding one (a deployed app, a CI secret, a teammate) breaks until it is updated."),new x("keys rotate needs --force, because this cannot be undone");let n=await v("cloud-project-rotate-jwt",{ref:e});w(n,()=>{process.stdout.write(`${Vn(n.anonKey,n.serviceRoleKey)}
|
|
165
|
+
`),l(""),l(n.breaks),l(P(" snoutdata keys --json to read them again later"))})}function Vn(e,t){return q([["KEY","VALUE"],["anon",e],["service_role",t]])}function Os(e){l(""),l(`${T("anon")} is the public one: it goes in a browser, and row-level security decides what it reads.`),l(`${T("service_role")} bypasses row-level security. Server side only, never in a client bundle.`),e&&l(P(` issued ${e.slice(0,10)}`))}C();import{readdir as Fs,readFile as Bs,stat as Hs}from"node:fs/promises";import{join as Zn,relative as qs,sep as Ks}from"node:path";U();var Ds=/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/;function Is(e){return Ds.test(e)}function xt(e){if(!Is(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 Ms=/^[A-Za-z0-9][A-Za-z0-9._-]*$/;function Cs(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!==".."&&Ms.test(n))}var kt=2*1024*1024,Gn=200;function zn(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>Gn)throw new Error(`a function may hold ${Gn} files and this one has ${e.files.length}`);let n=new Set;for(let a of e.files){if(!Cs(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=js(o);if(s.length>kt)throw new Error(`this function is ${Math.round(s.length/1024)} KB and the limit is ${kt/1024} KB. A function is source code; put large files in Storage.`);return o}function js(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 Ls=/^[A-Za-z_][A-Za-z0-9_]{0,63}$/,Us=["SUPABASE_URL","SUPABASE_ANON_KEY","SUPABASE_SERVICE_ROLE_KEY","SUPABASE_DB_URL"];function vt(e){return Ls.test(e)?e.startsWith("SNOUT_")?`names beginning with SNOUT_ are set by the platform, so ${e} cannot be one of yours.`:Us.includes(e)?`${e} is set for you from this project's own keys and cannot be overridden.`:null:`${JSON.stringify(e)} is not an environment variable name: letters, digits and underscores, not starting with a digit.`}var sl=64*1024;var Yn=["supabase/functions","functions"];async function Xn(e){try{return(await Hs(e)).isDirectory()}catch{return!1}}async function Js(e,t){if(t){if(!await Xn(t))throw new x(`${t} is not a directory`);return t}for(let n of Yn){let r=Zn(n,e);if(await Xn(r))return r}throw new x(`no folder found for ${e}. Looked in ${Yn.map(n=>`${n}/${e}`).join(" and ")}; use --dir to say where it is.`)}async function Ws(e){let t=[];async function n(r){for(let o of await Fs(r,{withFileTypes:!0})){if(o.name.startsWith(".")||o.name==="node_modules")continue;let s=Zn(r,o.name);if(o.isDirectory()){await n(s);continue}if(!o.isFile())continue;let a=await Bs(s),c=a.toString("utf8");if(Buffer.compare(Buffer.from(c,"utf8"),a)!==0)throw new x(`${s} is not text, and a function bundle is source code. Put binary assets in Storage.`);t.push({path:qs(e,s).split(Ks).join("/"),text:c})}}return await n(e),t}async function Pt(e,t,n){xt(t);let r=await Js(t,n.dir),o=await Ws(r);if(o.length===0)throw new x(`${r} has no files in it`);let s=n.entrypoint??(o.some(u=>u.path==="index.ts")?"index.ts":"");if(!s)throw new x(`${r} has no index.ts, so there is nothing to start. Name the file with --entrypoint.`);let a=zn({entrypoint:s,files:o});return{answer:await v("cloud-project-functions",{ref:e,action:"deploy",name:t,verifyJwt:n.verifyJwt,bundle:a}),directory:r,files:o.length,bytes:JSON.stringify(a).length}}async function Qn(e,t,n){let{answer:r,directory:o,files:s,bytes:a}=await Pt(e,t,n),c=r.functions.find(u=>u.name===t);w(r,()=>{let u=Math.max(1,Math.round(a/1024));l(`${T(t)} deployed from ${o} (${s} file${s===1?"":"s"}, ${u} KB)`),c&&l(` ${c.url}`),n.verifyJwt||(l(""),l(T(" Anybody who knows this URL can run it. No API key is required.")),l(P(" That is what a webhook receiver needs. Check the sender signature inside the function."))),r.limit.maxFunctions!==null&&l(P(` ${r.limit.deployed} of ${r.limit.maxFunctions} functions on this plan`))})}function Et(e){return v("cloud-project-functions",{ref:e})}function Tt(e,t){return xt(t),v("cloud-project-functions",{ref:e,action:"delete",name:t})}async function er(e){let t=await Et(e);w(t,()=>{if(t.functions.length===0){l(`${e} has no functions.`),l(P(" snoutdata functions deploy <name> from supabase/functions/<name>"));return}process.stdout.write(`${q([["NAME","KEY","SIZE","URL"],...t.functions.map(n=>[n.name,n.verifyJwt?"required":T("open"),n.bytes===null?"-":`${Math.max(1,Math.round(n.bytes/1024))} KB`,n.url])])}
|
|
166
|
+
`),t.limit.maxFunctions!==null&&l(P(`${t.limit.deployed} of ${t.limit.maxFunctions} on this plan`))})}async function tr(e,t){let n=await Tt(e,t);w(n,()=>{l(`${t} is gone. It stops answering within a few seconds.`)})}C();U();function Gs(e){let t=e.indexOf("=");return t<=0?null:{name:e.slice(0,t),value:e.slice(t+1)}}async function zs(){let e=[];for await(let t of process.stdin)e.push(Buffer.from(t));return Buffer.concat(e).toString("utf8").replace(/\n$/,"")}async function nr(e,t,n){if(t.length===0)throw new x("secrets set needs NAME=value, or NAME with --stdin");let r=[];if(n.stdin){if(t.length!==1)throw new x("--stdin sets one secret, so it takes one NAME");r.push({name:t[0],value:await zs()})}else for(let s of t){let a=Gs(s);if(!a)throw new x(`${s} is not NAME=value. Use --stdin to set a value from a pipe.`);r.push(a)}for(let s of r){let a=vt(s.name);if(a)throw new x(a)}let o=null;for(let s of r)o=await v("cloud-project-secrets",{ref:e,action:"set",name:s.name,value:s.value});w(o,()=>{for(let s of r)l(`${T(s.name)} set`);l(P(" Functions pick it up within a few seconds. Redeploying is not needed.")),n.stdin||l(P(" A value typed here is in your shell history. In CI, use: \u2026 | snoutdata secrets set NAME --stdin"))})}async function rr(e,t){let n=vt(t);if(n)throw new x(n);let r=await v("cloud-project-secrets",{ref:e,action:"unset",name:t});w(r,()=>{l(`${t} is gone.`)})}async function or(e){let t=await v("cloud-project-secrets",{ref:e});w(t,()=>{if(t.secrets.length===0){l(`${e} has no function secrets.`),l(P(" snoutdata secrets set STRIPE_KEY=sk_..."));return}process.stdout.write(`${q([["NAME","SIZE","SET"],...t.secrets.map(n=>[n.name,n.bytes===null?"-":`${n.bytes} B`,G(n.updatedAt)])])}
|
|
167
|
+
`),l(P("Values are not readable, here or anywhere. Set one again to change it."))})}import{spawn as At}from"node:child_process";import{appendFileSync as Xs,existsSync as lr,readFileSync as Zs,writeFileSync as cr}from"node:fs";import{join as Qs}from"node:path";import{open as ur}from"node:fs/promises";function sr(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 ar(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 ir(e){let t=e.split(`
|
|
168
|
+
`).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}}C();ie();ke();C();U();async function dr(e){try{let{projects:t}=await Y(),n=t.find(r=>r.ref===e);n?.readOnly?H(`${e} is read-only: it is over the storage limit on your plan. Reading works; writing does not.`):n?.readOnlyPending&&H(`${e} is about to become read-only: it has grown past the storage limit on your plan.`)}catch{}}async function $t(e){await dr(e);let t=await K(e);w(t,()=>{process.stdout.write(`${t.uri}
|
|
169
|
+
`)})}async function fr(e,t){if(t.length===0&&!fe()){let o=Be();k("usage",`db psql with no statements opens an interactive session, and ${o?Fe(o):"there is nobody to use it"}. Pass statements after --, or use \`snoutdata db url\` and your own client.`)}await dr(e);let n=await K(e);n.wakesInstantly||l("This project is paused; the connection will wake it, which takes a few seconds.");let r=At("psql",["--host",n.host,"--port",String(n.port),"--username",n.user,"--dbname",n.database,...t],{stdio:"inherit",env:{...process.env,PGPASSWORD:n.password,PGSSLMODE:"require"}});return new Promise((o,s)=>{r.on("error",a=>{if(a.code==="ENOENT"){s(new Q("tool-missing","psql is not installed. `snoutdata db url` prints a connection string for any client."));return}s(new Q("failed",String(a)))}),r.on("close",a=>o(a??0))})}function mr(e,t,n="DATABASE_URL"){let r=Qs(e,".env"),o=`${n}=${t}`;if(!lr(r))return cr(r,`${o}
|
|
170
|
+
`),{path:r,replaced:!1};let s=Zs(r,"utf8"),a=new RegExp(`^${n}=.*$`,"m");return a.test(s)?(cr(r,s.replace(a,o)),{path:r,replaced:!0}):(Xs(r,s.endsWith(`
|
|
34
171
|
`)?`${o}
|
|
35
172
|
`:`
|
|
36
173
|
${o}
|
|
37
|
-
`),{path:r,replaced:!1})}async function
|
|
38
|
-
`),e.urlExpiresAt&&
|
|
174
|
+
`),{path:r,replaced:!1})}async function pr(e,t={}){if(t.statusOnly){let{export:s}=await ge(e);return w({ref:e,export:s},()=>_t(s)),0}let n=await ve(e);if(!n.export.pending&&n.export.url&&!t.out)return w({ref:e,export:n.export},()=>_t(n.export)),0;l("Taking a copy of the database. This runs on the server and can take a few minutes.");let r=await ea(e,t.timeoutMs);if(r.error&&k("failed",`The export did not finish: ${r.error}`,{ref:e}),!t.out)return w({ref:e,export:r},()=>_t(r)),0;r.url||k("failed","The copy was taken, but no download link could be signed. Run this again to get one.",{ref:e});let o=await ta(r.url,t.out);return w({ref:e,path:t.out,bytes:o},()=>{l(`Saved ${X(o)} to ${t.out}.`),l(P("Restore it with: pg_restore --dbname <your database> "+t.out))}),0}async function ea(e,t=30*6e4){let n=Date.now()+t;for(;;){let{export:r}=await ge(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 ta(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 ur(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 _t(e){if(e.error){H(`The last export did not finish: ${e.error}`);return}if(e.pending){l("An export is running. Ask again in a minute, or run `snoutdata db export`, which waits for it.");return}if(!e.completedAt){l("This project has never been exported.");return}l(`A copy taken ${G(e.completedAt)}, ${X(e.bytes??0)}.`),e.url?(process.stdout.write(`${e.url}
|
|
175
|
+
`),e.urlExpiresAt&&l(P(`That link stops working ${G(e.urlExpiresAt)}.`))):l(P("The link has expired. Run the export again to sign a new one; the copy is kept."))}function X(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 gr(e,t){let n=t.file;n||k("usage","db restore needs --file, pointing at a dump. `snoutdata db export` writes one."),lr(n)||k("usage",`no file at ${n}`,{file:n});let r=await ur(n,"r"),o=new Uint8Array(512),s=await r.read(o,0,512,0);await r.close();let a=sr(o.slice(0,s.bytesRead)),c=await K(e);c.wakesInstantly||l("This project is paused; the connection will wake it, which takes a few seconds.");let{projects:u}=await Y(),d=u.find(y=>y.ref===e),m=await na(c);m===null&&k("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 i=ar(a,{existingTables:m,readOnly:d?.readOnly===!0},{force:t.force});i.ok||k("conflict",i.reason,{ref:e,file:n,refusal:i.code}),l(`Restoring ${n} into ${e} with ${i.tool} (${i.format} format).`);let f=await ra(c,i.tool,n),p=ir(f.stderr);return f.code!==0&&p.fatal.length===0&&p.total>0?(w({ref:e,file:n,ok:!0,tool:i.tool,format:i.format,ignoredErrors:p.harmless},()=>{l(`Restored into ${e}.`),l(`${p.harmless} error${p.harmless===1?"":"s"} above ${p.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):(f.code!==0&&k("failed",`${i.tool} exited ${f.code}. Nothing here retried it: a half-finished restore is undone by deleting the project and making another.`,{ref:e,file:n,errors:p.fatal}),w({ref:e,file:n,ok:!0,tool:i.tool,format:i.format},()=>l(`Restored into ${e}.`)),0)}async function na(e){return new Promise(t=>{let n=At("psql",["--host",e.host,"--port",String(e.port),"--username",e.user,"--dbname",e.database,"--no-psqlrc","-tAc","select count(*) from pg_class c join pg_namespace n on n.oid = c.relnamespace where c.relkind = 'r' and n.nspname not in ('pg_catalog', 'information_schema') and n.nspname not like 'pg\\_toast%' and not exists (select 1 from pg_depend d where d.objid = c.oid and d.deptype = 'e')"],{stdio:["ignore","pipe","pipe"],env:{...process.env,PGPASSWORD:e.password,PGSSLMODE:"require"}}),r="";n.stdout.on("data",o=>r+=String(o)),n.on("error",o=>{H(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 ra(e,t,n){let r=["--host",e.host,"--port",String(e.port),"--username",e.user,"--dbname",e.database],o=t==="psql"?[...r,"--no-psqlrc","-v","ON_ERROR_STOP=1","-f",n]:[...r,"--no-owner","--no-privileges",n];return new Promise(s=>{let a=At(t,o,{stdio:["ignore","inherit","pipe"],env:{...process.env,PGPASSWORD:e.password,PGSSLMODE:"require"}}),c="";a.stderr?.on("data",u=>{let d=String(u);c+=d,process.stderr.write(d)}),a.on("error",u=>{H(u.code==="ENOENT"?`${t} is not installed, and this dump needs it.`:String(u)),s({code:127,stderr:c})}),a.on("close",u=>s({code:u??0,stderr:c}))})}C();U();ie();import{spawn as aa}from"node:child_process";import{createHash as ia}from"node:crypto";import{existsSync as ca,readdirSync as la,readFileSync as ua,statSync as da}from"node:fs";import{isAbsolute as fa,join as Rt,relative as ma,resolve as pa}from"node:path";function sa(e){return[...e].sort((t,n)=>t.name<n.name?-1:t.name>n.name?1:0)}function Ze(e,t,n={}){let r=sa(e),o=new Map(r.map(d=>[d.name,d])),s=[];for(let d of t){let m=o.get(d.name);if(!m){s.push({code:"missing",name:d.name,reason:`${d.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}m.checksum!==d.checksum&&s.push({code:"changed",name:d.name,reason:`${d.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(d=>d.name).sort((d,m)=>d<m?-1:d>m?1:0)[t.length-1],c=new Set(t.map(d=>d.name)),u=[];for(let d of r)if(!c.has(d.name)){if(a!==void 0&&d.name<a&&!n.outOfOrder){s.push({code:"out-of-order",name:d.name,reason:`${d.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}u.push(d)}return s.length>0?{ok:!1,problems:s}:{ok:!0,pending:u}}function Ee(e){return!/^[\t ]*--[\t ]*snoutdata:no-transaction[\t ]*$/im.test(e)}function yr(e){let t=ma(process.cwd(),e);return t&&!t.startsWith("..")?t:e}var Te="_snoutdata_migrations",ga=`create table if not exists ${Te} (
|
|
39
176
|
name text primary key,
|
|
40
177
|
checksum text not null,
|
|
41
178
|
applied_at timestamptz not null default now()
|
|
42
|
-
);`;function
|
|
43
|
-
`).map(r=>r.trim()).filter(r=>r.length>0).map(r=>{let o=r.indexOf(" ");return{name:r.slice(0,o),checksum:r.slice(o+1)}})}function
|
|
44
|
-
`);
|
|
45
|
-
${r}`,{ref:e,problems:n.problems})}
|
|
46
|
-
${n.error??""}`.trimEnd(),{ref:e,failed:n.failed})}return
|
|
47
|
-
${
|
|
48
|
-
${
|
|
179
|
+
);`;function ha(e){if(!ca(e)||!da(e).isDirectory())throw new Error(`no migrations folder at ${yr(e)}. Put your .sql files there, or pass --dir.`);return la(e).filter(t=>t.endsWith(".sql")).map(t=>{let n=Rt(e,t),r=ua(n,"utf8");return{name:t,path:n,sql:r,checksum:ia("sha256").update(r,"utf8").digest("hex")}})}function Ot(e,t,n=[]){return new Promise(r=>{let o=aa("psql",["--host",e.host,"--port",String(e.port),"--username",e.user,"--dbname",e.database,"--no-psqlrc","-v","ON_ERROR_STOP=1",...n],{stdio:["pipe","pipe","pipe"],env:{...process.env,PGPASSWORD:e.password,PGSSLMODE:"require"}}),s="",a="";o.stdout.on("data",c=>s+=String(c)),o.stderr.on("data",c=>a+=String(c)),o.on("error",c=>{r({code:127,out:"",err:c.code==="ENOENT"?"psql is not installed, and db push needs it to run your migrations. `snoutdata db url` prints a connection string for any client.":String(c)})}),o.on("close",c=>r({code:c??0,out:s,err:a})),o.stdin.end(t)})}async function ya(e){let t=await Ot(e,ga);if(t.code!==0)throw new Error(t.err.trim()||`could not create ${Te}`);let n=await Ot(e,`select name || E'\\t' || checksum from ${Te};`,["-tA"]);if(n.code!==0)throw new Error(n.err.trim()||`could not read ${Te}`);return n.out.split(`
|
|
180
|
+
`).map(r=>r.trim()).filter(r=>r.length>0).map(r=>{let o=r.indexOf(" ");return{name:r.slice(0,o),checksum:r.slice(o+1)}})}function hr(e){return`'${e.replace(/'/g,"''")}'`}async function wr(e,t={}){let n=await Nt(e,t,r=>l(r));if(!n.ok){if(n.problems){let r=n.problems.map(o=>` ${o.reason}`).join(`
|
|
181
|
+
`);k("conflict",`Refused. This folder and ${e} do not agree:
|
|
182
|
+
${r}`,{ref:e,problems:n.problems})}k("failed",`${n.failed} failed, and nothing after it was tried.
|
|
183
|
+
${n.error??""}`.trimEnd(),{ref:e,failed:n.failed})}return w(n,()=>{if(n.wouldApply){l(`Would apply ${n.wouldApply.length} migration${n.wouldApply.length===1?"":"s"} to ${e}:`);for(let r of n.wouldApply)l(` ${r}`);return}if(n.upToDate){l(`${e} is up to date.`);return}l(`Applied ${n.applied.length} migration${n.applied.length===1?"":"s"} to ${e}.`)}),0}async function Nt(e,t={},n=()=>{}){let r=pa(t.dir===void 0?Rt(process.cwd(),"migrations"):fa(t.dir)?t.dir:Rt(process.cwd(),t.dir)),o=[];function s(p){o.push(p),n(p)}let a=ha(r);if(a.length===0)return s(`No .sql files in ${yr(r)}.`),{ref:e,directory:r,ok:!0,applied:[],upToDate:!0,notes:o};let c=await K(e);c.wakesInstantly||s("This project is paused; the connection will wake it, which takes a few seconds.");let u=await ya(c),d=Ze(a,u,{outOfOrder:t.outOfOrder});if(!d.ok)return{ref:e,directory:r,ok:!1,applied:[],problems:[...d.problems],notes:o};let m=new Map(a.map(p=>[p.name,p])),i=d.pending.map(p=>m.get(p.name));if(i.length===0)return s(`${e} is up to date: ${u.length} migration${u.length===1?"":"s"} already applied.`),{ref:e,directory:r,ok:!0,applied:[],upToDate:!0,notes:o};if(t.dryRun){for(let p of i)Ee(p.sql)||s(`${p.name} would run outside a transaction.`);return{ref:e,directory:r,ok:!0,applied:[],wouldApply:i.map(p=>p.name),notes:o}}let f=[];for(let p of i){let y=`insert into ${Te} (name, checksum) values (${hr(p.name)}, ${hr(p.checksum)});`,h=Ee(p.sql);h||s(`${p.name} runs outside a transaction, so an interruption can leave it half applied.`);let g=h?`begin;
|
|
184
|
+
${p.sql}
|
|
185
|
+
${y}
|
|
49
186
|
commit;
|
|
50
|
-
`:`${
|
|
51
|
-
${
|
|
52
|
-
`,
|
|
53
|
-
`)
|
|
187
|
+
`:`${p.sql}
|
|
188
|
+
${y}
|
|
189
|
+
`,S=await Ot(c,g);if(S.code!==0)return h&&s("It ran inside a transaction, so nothing from it is in the database."),{ref:e,directory:r,ok:!1,applied:f,failed:p.name,error:S.err.trim(),notes:o};f.push(p.name),s(` applied ${p.name}`)}return{ref:e,directory:r,ok:!0,applied:f,notes:o}}C();ie();U();import{spawn as Ra}from"node:child_process";import{writeFileSync as Oa}from"node:fs";import{isAbsolute as Na,resolve as Da}from"node:path";var wa=" ";function b(e){return wa.repeat(e)}function j(e){return JSON.stringify(e)}function Z(e){return/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(e)?e:j(e)}function le(e){return[...e].sort((t,n)=>t.name<n.name?-1:t.name>n.name?1:0)}function ba(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 Sa={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"},ka={sqlserver:{bit:"boolean"},"mysql-aurora":{bit:"number"},mariadb:{bit:"number"},sqlite:{bit:"number"},snowflake:{array:"json"}};function xa(e,t){let n=ka[e]?.[t];return n||(Sa[t]??"unknown")}function Qe(e,t){let{base:n,array:r}=ba(t),o=e.enumRefs.get(n);if(o)return r?`${o}[]`:o;if(n==="array")return"unknown[]";let s=xa(e.driverId,n),a=s==="json"?"Json":s;return r?`${a}[]`:a}function va(e){return e.nullable||e.autoIncrement===!0||e.default!==null&&e.default!==void 0}function Dt(e,t,n,r){let o=Qe(e,t.dataType),s=t.nullable?`${o} | null`:o;return`${b(n)}${Z(t.name)}${r?"?":""}: ${s}`}function Pa(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 Ea(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(`${b(t)}Relationships: []`),n;n.push(`${b(t)}Relationships: [`);for(let o of r)n.push(`${b(t+1)}{`),n.push(`${b(t+2)}foreignKeyName: ${j(o.constraintName)}`),n.push(`${b(t+2)}columns: [${o.columns.map(j).join(", ")}]`),n.push(`${b(t+2)}isOneToOne: ${Pa(e,o)?"true":"false"}`),n.push(`${b(t+2)}referencedRelation: ${j(o.referencedTable)}`),n.push(`${b(t+2)}referencedColumns: [${o.referencedColumns.map(j).join(", ")}]`),n.push(`${b(t+1)}},`);return n.push(`${b(t)}]`),n}function br(e,t,n){let r=le(t.columns),o=[];o.push(`${b(n)}${Z(t.name)}: {`),o.push(`${b(n+1)}Row: {`);for(let s of r)o.push(Dt(e,s,n+2,!1));if(o.push(`${b(n+1)}}`),t.kind!=="view"){o.push(`${b(n+1)}Insert: {`);for(let s of r)o.push(Dt(e,s,n+2,va(s)));o.push(`${b(n+1)}}`),o.push(`${b(n+1)}Update: {`);for(let s of r)o.push(Dt(e,s,n+2,!0));o.push(`${b(n+1)}}`)}return o.push(...Ea(t,n+1)),o.push(`${b(n)}}`),o}function _e(e,t){return[`${b(t)}${e}: {`,`${b(t+1)}[_ in never]: never`,`${b(t)}}`]}function Ta(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}[]`:`${Qe(e,s)}[]`}return r==="record"||r==="trigger"||r==="event_trigger"?"unknown":e.rowRefs.get(r)??Qe(e,n)}function _a(e,t,n){let r=[],o=le((t.parameters??[]).filter(s=>s.name.length>0&&(s.mode??"IN").toUpperCase()!=="OUT").map(s=>({name:s.name,dataType:s.dataType})));if(r.push(`${b(n)}${Z(t.name)}: {`),o.length===0)r.push(`${b(n+1)}Args: never`);else{r.push(`${b(n+1)}Args: {`);for(let s of o)r.push(`${b(n+2)}${Z(s.name)}: ${Qe(e,s.dataType)}`);r.push(`${b(n+1)}}`)}return r.push(`${b(n+1)}Returns: ${Ta(e,t)}`),r.push(`${b(n)}}`),r}function ye(e,t){return e.schema&&e.schema.length>0?e.schema:t}function Sr(e,t={}){let n=t.defaultSchema??"public",r=t.driverId??"postgres",o=t.enums??[],s=new Map;for(let i of o){let f=i.schema&&i.schema.length>0?i.schema:n;s.set(i.name.toLowerCase(),`Database[${j(f)}][${j("Enums")}][${j(i.name)}]`)}let a=new Map;for(let i of e.tables){let f=ye(i,n),p=i.kind==="view"?"Views":"Tables";a.set(i.name.toLowerCase(),`Database[${j(f)}][${j(p)}][${j(i.name)}][${j("Row")}]`)}let c={driverId:r,defaultSchema:n,enumRefs:s,rowRefs:a},u=new Set([n]);for(let i of e.tables)u.add(ye(i,n));for(let i of e.routines??[])u.add(ye(i,n));for(let i of o)u.add(i.schema&&i.schema.length>0?i.schema:n);let d=[...u].sort(),m=[];m.push("export type Json ="),m.push(" | string"),m.push(" | number"),m.push(" | boolean"),m.push(" | null"),m.push(" | { [key: string]: Json | undefined }"),m.push(" | Json[]"),m.push(""),m.push("export type Database = {"),t.postgrestVersion&&(m.push(`${b(1)}__InternalSupabase: {`),m.push(`${b(2)}PostgrestVersion: ${j(t.postgrestVersion)}`),m.push(`${b(1)}}`));for(let i of d){let f=le(e.tables.filter(g=>ye(g,n)===i&&g.kind!=="view")),p=le(e.tables.filter(g=>ye(g,n)===i&&g.kind==="view")),y=le((e.routines??[]).filter(g=>ye(g,n)===i)),h=le(o.filter(g=>(g.schema&&g.schema.length>0?g.schema:n)===i));if(m.push(`${b(1)}${Z(i)}: {`),f.length===0)m.push(..._e("Tables",2));else{m.push(`${b(2)}Tables: {`);for(let g of f)m.push(...br(c,g,3));m.push(`${b(2)}}`)}if(p.length===0)m.push(..._e("Views",2));else{m.push(`${b(2)}Views: {`);for(let g of p)m.push(...br(c,g,3));m.push(`${b(2)}}`)}if(y.length===0)m.push(..._e("Functions",2));else{m.push(`${b(2)}Functions: {`);for(let g of y)m.push(..._a(c,g,3));m.push(`${b(2)}}`)}if(h.length===0)m.push(..._e("Enums",2));else{m.push(`${b(2)}Enums: {`);for(let g of h)m.push(`${b(3)}${Z(g.name)}: ${g.values.map(j).join(" | ")}`);m.push(`${b(2)}}`)}m.push(..._e("CompositeTypes",2)),m.push(`${b(1)}}`)}return m.push("}"),t.helpers!==!1&&(m.push(""),m.push($a(n))),t.constants!==!1&&(m.push(""),m.push(Aa(d,o,n))),`${m.join(`
|
|
190
|
+
`)}
|
|
191
|
+
`}function Aa(e,t,n){let r=["export const Constants = {"];for(let o of e){let s=le(t.filter(a=>(a.schema&&a.schema.length>0?a.schema:n)===o));if(s.length===0){r.push(`${b(1)}${Z(o)}: {`),r.push(`${b(2)}Enums: {},`),r.push(`${b(1)}},`);continue}r.push(`${b(1)}${Z(o)}: {`),r.push(`${b(2)}Enums: {`);for(let a of s)r.push(`${b(3)}${Z(a.name)}: [${a.values.map(j).join(", ")}],`);r.push(`${b(2)}},`),r.push(`${b(1)}},`)}return r.push("} as const"),r.join(`
|
|
192
|
+
`)}function $a(e){return`type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
|
|
193
|
+
|
|
194
|
+
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, ${j(e)}>]
|
|
195
|
+
|
|
196
|
+
export type Tables<
|
|
197
|
+
DefaultSchemaTableNameOrOptions extends
|
|
198
|
+
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
|
199
|
+
| { schema: keyof DatabaseWithoutInternals },
|
|
200
|
+
TableName extends DefaultSchemaTableNameOrOptions extends {
|
|
201
|
+
schema: keyof DatabaseWithoutInternals
|
|
202
|
+
}
|
|
203
|
+
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
|
204
|
+
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
|
205
|
+
: never = never,
|
|
206
|
+
> = DefaultSchemaTableNameOrOptions extends {
|
|
207
|
+
schema: keyof DatabaseWithoutInternals
|
|
208
|
+
}
|
|
209
|
+
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
|
210
|
+
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
|
211
|
+
Row: infer R
|
|
212
|
+
}
|
|
213
|
+
? R
|
|
214
|
+
: never
|
|
215
|
+
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
|
216
|
+
DefaultSchema["Views"])
|
|
217
|
+
? (DefaultSchema["Tables"] &
|
|
218
|
+
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
|
219
|
+
Row: infer R
|
|
220
|
+
}
|
|
221
|
+
? R
|
|
222
|
+
: never
|
|
223
|
+
: never
|
|
224
|
+
|
|
225
|
+
export type TablesInsert<
|
|
226
|
+
DefaultSchemaTableNameOrOptions extends
|
|
227
|
+
| keyof DefaultSchema["Tables"]
|
|
228
|
+
| { schema: keyof DatabaseWithoutInternals },
|
|
229
|
+
TableName extends DefaultSchemaTableNameOrOptions extends {
|
|
230
|
+
schema: keyof DatabaseWithoutInternals
|
|
231
|
+
}
|
|
232
|
+
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
|
233
|
+
: never = never,
|
|
234
|
+
> = DefaultSchemaTableNameOrOptions extends {
|
|
235
|
+
schema: keyof DatabaseWithoutInternals
|
|
236
|
+
}
|
|
237
|
+
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
|
238
|
+
Insert: infer I
|
|
239
|
+
}
|
|
240
|
+
? I
|
|
241
|
+
: never
|
|
242
|
+
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
|
243
|
+
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
|
244
|
+
Insert: infer I
|
|
245
|
+
}
|
|
246
|
+
? I
|
|
247
|
+
: never
|
|
248
|
+
: never
|
|
249
|
+
|
|
250
|
+
export type TablesUpdate<
|
|
251
|
+
DefaultSchemaTableNameOrOptions extends
|
|
252
|
+
| keyof DefaultSchema["Tables"]
|
|
253
|
+
| { schema: keyof DatabaseWithoutInternals },
|
|
254
|
+
TableName extends DefaultSchemaTableNameOrOptions extends {
|
|
255
|
+
schema: keyof DatabaseWithoutInternals
|
|
256
|
+
}
|
|
257
|
+
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
|
258
|
+
: never = never,
|
|
259
|
+
> = DefaultSchemaTableNameOrOptions extends {
|
|
260
|
+
schema: keyof DatabaseWithoutInternals
|
|
261
|
+
}
|
|
262
|
+
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
|
263
|
+
Update: infer U
|
|
264
|
+
}
|
|
265
|
+
? U
|
|
266
|
+
: never
|
|
267
|
+
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
|
268
|
+
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
|
269
|
+
Update: infer U
|
|
270
|
+
}
|
|
271
|
+
? U
|
|
272
|
+
: never
|
|
273
|
+
: never
|
|
274
|
+
|
|
275
|
+
export type Enums<
|
|
276
|
+
DefaultSchemaEnumNameOrOptions extends
|
|
277
|
+
| keyof DefaultSchema["Enums"]
|
|
278
|
+
| { schema: keyof DatabaseWithoutInternals },
|
|
279
|
+
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
|
280
|
+
schema: keyof DatabaseWithoutInternals
|
|
281
|
+
}
|
|
282
|
+
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
|
283
|
+
: never = never,
|
|
284
|
+
> = DefaultSchemaEnumNameOrOptions extends {
|
|
285
|
+
schema: keyof DatabaseWithoutInternals
|
|
286
|
+
}
|
|
287
|
+
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
|
288
|
+
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
|
289
|
+
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
|
290
|
+
: never
|
|
291
|
+
|
|
292
|
+
export type CompositeTypes<
|
|
293
|
+
PublicCompositeTypeNameOrOptions extends
|
|
294
|
+
| keyof DefaultSchema["CompositeTypes"]
|
|
295
|
+
| { schema: keyof DatabaseWithoutInternals },
|
|
296
|
+
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
|
297
|
+
schema: keyof DatabaseWithoutInternals
|
|
298
|
+
}
|
|
299
|
+
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
|
300
|
+
: never = never,
|
|
301
|
+
> = PublicCompositeTypeNameOrOptions extends {
|
|
302
|
+
schema: keyof DatabaseWithoutInternals
|
|
303
|
+
}
|
|
304
|
+
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
|
305
|
+
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
|
306
|
+
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
|
307
|
+
: never`}function Ia(e){return{host:e.host,port:e.port,database:e.database,user:e.user,password:e.password,sslMode:"require",label:e.ref}}function et(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 It(e,t){return new Promise(n=>{let r=Ra("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}}),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 Ma=`
|
|
308
|
+
with wanted as (select unnest($SCHEMAS$::text[]) as nspname)
|
|
309
|
+
select json_build_object(
|
|
310
|
+
'tables', coalesce((
|
|
311
|
+
select json_agg(json_build_object(
|
|
312
|
+
'name', c.relname,
|
|
313
|
+
'schema', n.nspname,
|
|
314
|
+
'kind', case when c.relkind in ('v', 'm') then 'view' else 'table' end,
|
|
315
|
+
'columns', coalesce((
|
|
316
|
+
select json_agg(json_build_object(
|
|
317
|
+
'name', a.attname,
|
|
318
|
+
'dataType', format_type(a.atttypid, a.atttypmod),
|
|
319
|
+
'nullable', not a.attnotnull,
|
|
320
|
+
'isPrimaryKey', false,
|
|
321
|
+
'default', pg_get_expr(d.adbin, d.adrelid),
|
|
322
|
+
'autoIncrement', a.attidentity <> '' or coalesce(pg_get_expr(d.adbin, d.adrelid), '') like 'nextval(%'
|
|
323
|
+
) order by a.attnum)
|
|
324
|
+
from pg_attribute a
|
|
325
|
+
left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
|
|
326
|
+
where a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped
|
|
327
|
+
), '[]'::json),
|
|
328
|
+
'primaryKey', coalesce((
|
|
329
|
+
select json_agg(att.attname order by k.ord)
|
|
330
|
+
from pg_constraint con
|
|
331
|
+
cross join lateral unnest(con.conkey) with ordinality as k(attnum, ord)
|
|
332
|
+
join pg_attribute att on att.attrelid = con.conrelid and att.attnum = k.attnum
|
|
333
|
+
where con.conrelid = c.oid and con.contype = 'p'
|
|
334
|
+
), '[]'::json),
|
|
335
|
+
'foreignKeys', coalesce((
|
|
336
|
+
select json_agg(json_build_object(
|
|
337
|
+
'constraintName', con.conname,
|
|
338
|
+
'columns', (
|
|
339
|
+
select json_agg(att.attname order by k.ord)
|
|
340
|
+
from unnest(con.conkey) with ordinality as k(attnum, ord)
|
|
341
|
+
join pg_attribute att on att.attrelid = con.conrelid and att.attnum = k.attnum
|
|
342
|
+
),
|
|
343
|
+
'referencedTable', rc.relname,
|
|
344
|
+
'referencedColumns', (
|
|
345
|
+
select json_agg(att.attname order by k.ord)
|
|
346
|
+
from unnest(con.confkey) with ordinality as k(attnum, ord)
|
|
347
|
+
join pg_attribute att on att.attrelid = con.confrelid and att.attnum = k.attnum
|
|
348
|
+
)
|
|
349
|
+
) order by con.conname)
|
|
350
|
+
from pg_constraint con
|
|
351
|
+
join pg_class rc on rc.oid = con.confrelid
|
|
352
|
+
where con.conrelid = c.oid and con.contype = 'f'
|
|
353
|
+
), '[]'::json),
|
|
354
|
+
'indexes', coalesce((
|
|
355
|
+
select json_agg(json_build_object(
|
|
356
|
+
'name', ic.relname,
|
|
357
|
+
'unique', i.indisunique,
|
|
358
|
+
'columns', (
|
|
359
|
+
select json_agg(att.attname order by k.ord)
|
|
360
|
+
from unnest(i.indkey::int2[]) with ordinality as k(attnum, ord)
|
|
361
|
+
join pg_attribute att on att.attrelid = i.indrelid and att.attnum = k.attnum
|
|
362
|
+
)
|
|
363
|
+
) order by ic.relname)
|
|
364
|
+
from pg_index i
|
|
365
|
+
join pg_class ic on ic.oid = i.indexrelid
|
|
366
|
+
where i.indrelid = c.oid and not i.indisprimary
|
|
367
|
+
), '[]'::json)
|
|
368
|
+
) order by n.nspname, c.relname)
|
|
369
|
+
from pg_class c
|
|
370
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
371
|
+
where c.relkind in ('r', 'p', 'v', 'm')
|
|
372
|
+
and n.nspname in (select nspname from wanted)
|
|
373
|
+
), '[]'::json),
|
|
374
|
+
'routines', coalesce((
|
|
375
|
+
select json_agg(json_build_object(
|
|
376
|
+
'name', p.proname,
|
|
377
|
+
'schema', n.nspname,
|
|
378
|
+
'kind', case p.prokind when 'p' then 'procedure' else 'function' end,
|
|
379
|
+
'returnType', pg_get_function_result(p.oid),
|
|
380
|
+
'parameters', coalesce((
|
|
381
|
+
select json_agg(json_build_object(
|
|
382
|
+
'name', coalesce(p.proargnames[a.ord], ''),
|
|
383
|
+
'dataType', format_type(a.t, null),
|
|
384
|
+
'mode', case coalesce(p.proargmodes[a.ord], 'i')
|
|
385
|
+
when 'o' then 'OUT' when 'b' then 'INOUT' when 'v' then 'VARIADIC' when 't' then 'OUT' else 'IN' end
|
|
386
|
+
) order by a.ord)
|
|
387
|
+
from unnest(coalesce(p.proallargtypes, p.proargtypes::oid[])) with ordinality as a(t, ord)
|
|
388
|
+
), '[]'::json)
|
|
389
|
+
) order by n.nspname, p.proname)
|
|
390
|
+
from pg_proc p
|
|
391
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
392
|
+
where p.prokind in ('f', 'p')
|
|
393
|
+
and n.nspname in (select nspname from wanted)
|
|
394
|
+
and not exists (
|
|
395
|
+
select 1 from pg_depend dep
|
|
396
|
+
where dep.objid = p.oid and dep.classid = 'pg_proc'::regclass and dep.deptype = 'e'
|
|
397
|
+
)
|
|
398
|
+
), '[]'::json),
|
|
399
|
+
'enums', coalesce((
|
|
400
|
+
select json_agg(json_build_object(
|
|
401
|
+
'name', t.typname,
|
|
402
|
+
'schema', n.nspname,
|
|
403
|
+
'values', (select json_agg(e.enumlabel order by e.enumsortorder) from pg_enum e where e.enumtypid = t.oid)
|
|
404
|
+
) order by n.nspname, t.typname)
|
|
405
|
+
from pg_type t
|
|
406
|
+
join pg_namespace n on n.oid = t.typnamespace
|
|
407
|
+
where t.typtype = 'e' and n.nspname in (select nspname from wanted)
|
|
408
|
+
), '[]'::json)
|
|
409
|
+
)::text
|
|
410
|
+
`;function Ca(e){return`'{${e.map(n=>`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(",").replace(/'/g,"''")}}'`}function ja(e,t,n=0){let r=e.tables.map(a=>{let c=a.primaryKey??[],u=new Set(c),d=a.columns.map(m=>({name:m.name,dataType:m.dataType,nullable:m.nullable,isPrimaryKey:u.has(m.name),default:m.default,autoIncrement:m.autoIncrement}));return{name:a.name,schema:a.schema,kind:a.kind,columns:d,primaryKey:c,foreignKeys:(a.foreignKeys??[]).map(m=>({constraintName:m.constraintName,columns:m.columns??[],referencedTable:m.referencedTable,referencedColumns:m.referencedColumns??[]})),indexes:(a.indexes??[]).filter(m=>Array.isArray(m.columns)&&m.columns.every(i=>typeof i=="string")).map(m=>({name:m.name,unique:m.unique,columns:m.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}}async function La(e){let t=e.schemas&&e.schemas.length>0?[...e.schemas]:["public"],n=e.dbUrl?et(e.dbUrl):Ia(await K(Ua(e))),r=Ma.replace("$SCHEMAS$",Ca(t)),o=await(e.sql?e.sql(r):It(n,r));o.code===127&&k("tool-missing",o.err.trim()),o.code!==0&&k("failed",o.err.trim()||`could not read the schema of ${n.label}`);let s=o.out.trim();s||k("failed",`${n.label} returned nothing for the schema query`);let a;try{a=JSON.parse(s)}catch{k("failed",`could not read the schema of ${n.label}: the answer was not JSON`)}let{schema:c,enums:u}=ja(a,n.database,Date.now()),d=Sr(c,{driverId:"postgres",defaultSchema:e.defaultSchema??t[0]??"public",enums:u,postgrestVersion:e.dataApiVersion}),m=null;return e.out&&(m=Na(e.out)?e.out:Da(process.cwd(),e.out),Oa(m,d,"utf8")),{target:n.label,schemas:t,tables:c.tables.filter(i=>i.kind!=="view").length,views:c.tables.filter(i=>i.kind==="view").length,functions:(c.routines??[]).length,enums:u.length,path:m,types:d}}function Ua(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 kr(e,t){let n=(e??"typescript").toLowerCase();n!=="typescript"&&n!=="ts"&&k("usage",`gen types does not know how to write ${e}. It writes typescript, which is the only language it claims.`);let r=await La(t);return r.path&&l(`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}ie();import{createHash as qi}from"node:crypto";import{existsSync as Xt,mkdirSync as Ki,readFileSync as Zt,readdirSync as Ji,statSync as Wi,writeFileSync as Vi}from"node:fs";import{dirname as Gi,isAbsolute as $o,join as Ne,resolve as Ro}from"node:path";import{pathToFileURL as zi}from"node:url";U();function Yi(e){return/^[a-z][a-z0-9+.-]+:/i.test(e)?e:$o(e)||e.startsWith("./")||e.startsWith("../")?zi(Ro(e)).href:e}function Xi(){let e=process.env.SNOUTDATA_LOCAL_RUNTIME;return e?import(Yi(e)):Promise.resolve().then(()=>(_o(),To))}function Zi(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 Qi="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 De(){let e;try{e=await Xi()}catch(t){k("tool-missing",Zi(t))}return{ready:()=>e.podmanReady(Qi),ensureImage:t=>e.ensureImage(t),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 Oo(e){return Ne(e,".snoutdata","local.json")}function it(e){let t=Oo(e);if(!Xt(t))return null;try{let n=JSON.parse(Zt(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 ec(e,t){let n=Oo(e);return Ki(Gi(n),{recursive:!0}),Vi(n,`${JSON.stringify(t,null,2)}
|
|
411
|
+
`,{encoding:"utf8",mode:384}),n}var tc=54322;async function No(e){let t=await e.ensureImage(r=>l(r));t&&k("tool-missing",t);let n=await e.ready();n&&k("tool-missing",n),await e.prepare()}function Do(e,t,n,r=It){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 nc(e,t={}){let n=t.cwd??process.cwd();await No(e);let r=it(n),o=r??e.mint(t.port??tc),s=r?null:ec(n,o),a=await e.start(o,i=>l(i)),c=e.uri(o);if(t.noMigrations)return{status:a,uri:c,applied:[],seeded:!1,created:s};let u=Do(e,o,et(c)),d=await rc(u,n,t),m=await oc(u,n,d.length>0||!!s);return{status:a,uri:c,applied:d,seeded:m,created:s}}async function rc(e,t,n){let r=Ro(n.dir===void 0?Ne(t,"migrations"):$o(n.dir)?n.dir:Ne(t,n.dir));if(!Xt(r)||!Wi(r).isDirectory())return[];let o=Ji(r).filter(i=>i.endsWith(".sql")).map(i=>{let f=Zt(Ne(r,i),"utf8");return{name:i,sql:f,checksum:qi("sha256").update(f,"utf8").digest("hex")}});if(o.length===0)return[];let s=await e(sc);s.code!==0&&k(s.code===127?"tool-missing":"failed",s.err.trim()||`could not create ${Oe}`);let a=await e(`select name || E'\\t' || checksum from ${Oe};`);a.code!==0&&k("failed",a.err.trim()||`could not read ${Oe}`);let c=a.out.split(`
|
|
412
|
+
`).map(i=>i.trim()).filter(i=>i.length>0).map(i=>{let f=i.indexOf(" ");return{name:i.slice(0,f),checksum:i.slice(f+1)}}),u=Ze(o,c,{outOfOrder:n.outOfOrder});u.ok||k("conflict",`Refused. This folder and the local database do not agree:
|
|
413
|
+
${u.problems.map(i=>` ${i.reason}`).join(`
|
|
414
|
+
`)}`,{problems:u.problems});let d=new Map(o.map(i=>[i.name,i])),m=[];for(let i of u.pending){let f=d.get(i.name),p=`insert into ${Oe} (name, checksum) values (${Ao(f.name)}, ${Ao(f.checksum)});`,y=Ee(f.sql);y||l(`${f.name} runs outside a transaction, so an interruption can leave it half applied.`);let h=y?`begin;
|
|
415
|
+
${f.sql}
|
|
416
|
+
${p}
|
|
417
|
+
commit;
|
|
418
|
+
`:`${f.sql}
|
|
419
|
+
${p}
|
|
420
|
+
`,g=await e(h);g.code!==0&&k("failed",`${f.name} failed, and nothing after it was tried.
|
|
421
|
+
${g.err.trim()}`,{failed:f.name,applied:m}),m.push(f.name),l(` applied ${f.name}`)}return m}async function oc(e,t,n){let r=Ne(t,"seed.sql");if(!n||!Xt(r))return!1;let o=await e(Zt(r,"utf8"));return o.code!==0&&k("failed",`seed.sql failed.
|
|
422
|
+
${o.err.trim()}`),l(" ran seed.sql"),!0}var Oe="_snoutdata_migrations",sc=`create table if not exists ${Oe} (
|
|
423
|
+
name text primary key,
|
|
424
|
+
checksum text not null,
|
|
425
|
+
applied_at timestamptz not null default now()
|
|
426
|
+
);`;function Ao(e){return`'${e.replace(/'/g,"''")}'`}function ac(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 Io(e,t={}){let n=await nc(e,t);return w(n,()=>{n.created&&(l(""),l(`Wrote ${n.created}. It holds a password, so put .snoutdata/ in your .gitignore.`)),l(""),l(` ${T("Database")} running on port ${n.status.port}`),n.applied.length>0&&l(` ${T("Migrations")} ${n.applied.length} applied`),n.seeded&&l(` ${T("Seed")} seed.sql ran`),l(""),process.stdout.write(`${n.uri}
|
|
427
|
+
`),l(P(" snoutdata gen types typescript --local > database.types.ts")),l(P(" snoutdata stop"))}),0}async function Mo(e,t={}){let n=t.cwd??process.cwd(),r=it(n);r||k("not-found","there is no local database for this folder. `snoutdata start` makes one."),await No(e);let o=await e.stop(r.ref);return w(o,()=>l("Stopped. The data is still there; `snoutdata start` brings it back.")),0}async function Co(e,t={}){let n=t.cwd??process.cwd(),r=it(n);if(!r)return w({exists:!1,ref:null,status:"absent"},()=>l("No local database for this folder. `snoutdata start` makes one.")),0;let o=await e.ready();if(o)return w({exists:!1,ref:r.ref,status:"unknown",reason:o},()=>l(o)),0;let s=await e.status(r);return w({...s,uri:s.uri},()=>{l(` ${T("Project")} ${s.ref}`),l(` ${T("State")} ${s.ready?"running":s.status}`),l(` ${T("Port")} ${s.port}`),l(` ${T("Size")} ${ac(s.databaseBytes)}`),s.services.length>0&&l(` ${T("Services")} ${s.services.join(", ")}`),s.uri&&process.stdout.write(`${s.uri}
|
|
428
|
+
`)}),0}async function jo(e,t=process.cwd()){let n=it(t);n||k("not-found","there is no local database for this folder. `snoutdata start` makes one.");let r=e.uri(n);return{dbUrl:r,sql:Do(e,n,et(r))}}C();U();var ic="2024-11-05";function I(e,t=[]){return{type:"object",properties:e,additionalProperties:!1,...t.length?{required:t}:{}}}var D={type:"string"},ct=[{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:I({})},{name:"list_projects",description:"Every hosted Postgres database on this account, with its ref, name, region, state (ready, paused, error), size, and whether it is read-only because it is over its plan's storage limit.",inputSchema:I({})},{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:I({name:D,region:D,teamId:D},["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:I({ref:D},["ref"])},{name:"pause_project",description:"Stop a project. Its data is kept and the next connection wakes it. A production project refuses this.",inputSchema:I({ref:D},["ref"])},{name:"resume_project",description:"Start a paused project without waiting for a connection to wake it.",inputSchema:I({ref:D},["ref"])},{name:"push_migrations",description:"Run the .sql files in a folder against a project, in name order, once each, recording what ran in a _snoutdata_migrations table. Refuses a file that changed after it ran, a file that has gone, and a new file that sorts before one already applied. Use dryRun first to see what would happen. Needs psql on this machine.",inputSchema:I({ref:D,dir:D,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:I({ref:D,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:I({ref:D},["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:I({ref:D},["ref"])},{name:"reset_password",description:"Rotate the project's database password. THE OLD ONE STOPS WORKING IMMEDIATELY, including any DATABASE_URL already written into a file or a running process. Returns the new connection URL. Use it when a credential has leaked, not routinely.",inputSchema:I({ref:D},["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:I({})},{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:I({})},{name:"create_token",description:"Mint a long-lived sdt_ access token, for a CI job or another machine. RETURNS THE TOKEN ONCE AND NEVER AGAIN. It is as capable as this account, so hand it over deliberately. Optionally expires, as an ISO date.",inputSchema:I({name:D,expires:D},["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:I({id:D},["id"])},{name:"deploy_function",description:"Put a folder of TypeScript on the edge as an HTTP endpoint at https://<ref>.api.snoutdata.com/functions/v1/<name>. Reads supabase/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:I({ref:D,name:D,dir:D,entrypoint:D,openToAnyone:{type:"boolean"}},["ref","name"])},{name:"list_functions",description:"The functions deployed to a project, their URLs, sizes, and whether each one needs an API key. Costs nothing.",inputSchema:I({ref:D},["ref"])},{name:"delete_function",description:"Remove one function from a project. It stops answering within a few seconds. It does not touch the database.",inputSchema:I({ref:D,name:D},["ref","name"])},{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:I({ref:D},["ref"])},{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:I({ref:D},["ref"])}];function cc(e){let t=new Set;return e.filter(n=>!t.has(n.name)&&t.add(n.name))}function Ie(e,t){return{jsonrpc:"2.0",id:e,result:t}}function Qt(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 Ie(e,{content:[{type:"text",text:r}],...n?{isError:!0}:{}})}function L(e,t){let n=e[t];return typeof n=="string"&&n.trim().length>0?n.trim():null}async function Lo(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 Ie(r,{protocolVersion:ic,capabilities:{tools:{listChanged:!1}},serverInfo:{name:"snoutdata",version:n.version}});case"ping":return Ie(r,{});case"tools/list":return Ie(r,{tools:cc([...ct,...n.borrowed??[]])});case"tools/call":return lc(r,s,t,n);default:return Qt(r,-32601,`unknown method: ${o}`)}}async function lc(e,t,n,r){let o=typeof t.name=="string"?t.name:"",s=t.arguments??{};if(!(ct.some(d=>d.name===o)||(r.borrowed?.some(d=>d.name===o)??!1)))return Qt(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"],u=L(s,"ref");if(c.includes(o)&&!u)return _(e,`${o} needs a project ref. Call list_projects to see them.`,!0);try{switch(o){case"whoami":return _(e,await n.whoami());case"list_projects":return _(e,await n.listProjects());case"create_project":{let d=L(s,"name");return d?_(e,await n.createProject({name:d,region:L(s,"region")??void 0,teamId:L(s,"teamId")??void 0})):_(e,"create_project needs a name.",!0)}case"get_connection_url":{let d=await n.connection(u);return _(e,{url:d.uri,state:d.state,wakesInstantly:d.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",u));case"resume_project":return _(e,await n.setProjectState("resume",u));case"usage":{let d=typeof s.days=="number"?s.days:void 0;return _(e,await n.usage(u,d))}case"export_status":return _(e,await n.exportStatus(u));case"start_export":return _(e,await n.requestExport(u));case"reset_password":return _(e,await n.resetPassword(u));case"list_teams":return _(e,await n.listTeams());case"deploy_function":{let d=L(s,"ref"),m=L(s,"name");return!d||!m?_(e,"deploy_function needs a project ref and a function name.",!0):_(e,await n.deployFunction(d,m,{dir:L(s,"dir")??void 0,entrypoint:L(s,"entrypoint")??void 0,verifyJwt:s&&typeof s=="object"&&"openToAnyone"in s?s.openToAnyone!==!0:!0}))}case"list_functions":{let d=L(s,"ref");return d?_(e,await n.listFunctions(d)):_(e,"list_functions needs a project ref.",!0)}case"delete_function":{let d=L(s,"ref"),m=L(s,"name");return!d||!m?_(e,"delete_function needs a project ref and a function name.",!0):_(e,await n.deleteFunction(d,m))}case"list_function_secrets":{let d=L(s,"ref");return d?_(e,await n.listFunctionSecrets(d)):_(e,"list_function_secrets needs a project ref.",!0)}case"list_tokens":return _(e,await n.listTokens());case"create_token":{let d=L(s,"name");return d?_(e,await n.createToken(d,L(s,"expires")??void 0)):_(e,"create_token needs a name, so somebody can tell later what it was for.",!0)}case"revoke_token":{let d=L(s,"id");return d?_(e,await n.revokeToken(d)):_(e,"revoke_token needs the id of a token. list_tokens has them.",!0)}case"push_migrations":return _(e,await n.pushMigrations(u,{dir:L(s,"dir")??void 0,dryRun:s.dryRun===!0}));case"delete_project":return r.allowDelete?_(e,await n.setProjectState("delete",u)):_(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(d=>d.name===o)?n.callBorrowed?Ie(e,await n.callBorrowed(o,s)):_(e,`${o} needs the SnoutData app, which is no longer answering.`,!0):Qt(e,-32602,`unknown tool: ${o}`)}}catch(d){return _(e,d instanceof Error?d.message:String(d),!0)}}function Uo(e){let t=e.split(`
|
|
429
|
+
`),n=t.pop()??"";return{messages:t.map(r=>r.trim()).filter(r=>r.length>0),rest:n}}Ke();function uc(){return{whoami:()=>pe(),listProjects:()=>Y(),createProject:async e=>{let t=await v("cloud-project-create",{name:e.name,...e.region?{region:e.region}:{},...e.teamId?{teamId:e.teamId}:{}});try{return await Pe(t.project.ref)}catch{return{...t.project,ready:!1,note:`${t.project.ref} was created and is still starting. It was not ready within the wait. Call list_projects in a few seconds; do not connect to it yet.`}}},usage:(e,t)=>v("cloud-project-usage",{ref:e,...t?{days:t}:{}}),exportStatus:e=>ge(e),requestExport:e=>ve(e),resetPassword:e=>v("cloud-project-reset-password",{ref:e}),listTeams:()=>v("cloud-team-list",{}),deployFunction:(e,t,n)=>Pt(e,t,n).then(r=>r.answer),listFunctions:e=>Et(e),deleteFunction:(e,t)=>Tt(e,t),listFunctionSecrets:e=>v("cloud-project-secrets",{ref:e}),listTokens:()=>v("cloud-token-list",{}),createToken:(e,t)=>v("cloud-token-create",{name:e,...t?{expires:t}:{}}),revokeToken:e=>v("cloud-token-revoke",{id:e}),setProjectState:(e,t)=>v(`cloud-project-${e}`,{ref:t}),connection:e=>K(e),pushMigrations:(e,t)=>Nt(e,{dir:t.dir,dryRun:t.dryRun})}}var Fo="app_";async function dc(){if(process.env.SNOUTDATA_NO_DESKTOP)return{tools:[],config:null};let e=await He();if(!e.available)return{tools:[],config:null};let t=e.config;try{return{tools:(await bn(t)).map(o=>({name:`${Fo}${o.name}`,description:`[SnoutData app, on this machine] ${o.description??o.name}`,inputSchema:o.inputSchema??{type:"object",properties:{}}})),config:t}}catch{return{tools:[],config:null}}}async function Bo(e){xe(!0);let{tools:t,config:n}=await dc(),r=uc();n&&(r.callBorrowed=(u,d)=>Sn(n,u.slice(Fo.length),d));let o={...e,borrowed:t};H(t.length>0?`snoutdata mcp: ${ct.length} cloud tools, and ${t.length} borrowed from the SnoutData app on this machine.`:"snoutdata mcp: cloud tools only. The SnoutData app is not answering here, so its database tools are not on this endpoint.");let s="";process.stdin.setEncoding("utf8");try{let u=await pe();H(`snoutdata mcp: signed in as ${u.email??"this account"}.`)}catch(u){return H(`snoutdata mcp: ${u instanceof Error?u.message:String(u)}`),H("Sign in with `snoutdata login`, or set SNOUTDATA_ACCESS_TOKEN."),3}H(`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(u){a.add(u),u.finally(()=>a.delete(u))}return new Promise(u=>{process.stdin.on("data",d=>{let m=Uo(s+d);s=m.rest;for(let i of m.messages)c(fc(i,r,o))}),process.stdin.on("end",()=>{(async()=>{for(;a.size>0;)await Promise.allSettled([...a]);u(0)})()}),process.stdin.on("error",()=>u(1))})}async function fc(e,t,n){let r;try{r=JSON.parse(e)}catch{H(`snoutdata mcp: ignoring a line that is not JSON: ${e.slice(0,120)}`);return}try{let o=await Lo(r,t,n);o&&process.stdout.write(`${JSON.stringify(o)}
|
|
54
430
|
`)}catch(o){let s=r.id??null;process.stdout.write(`${JSON.stringify({jsonrpc:"2.0",id:s,error:{code:-32603,message:o instanceof Error?o.message:String(o)}})}
|
|
55
|
-
`)}}
|
|
431
|
+
`)}}C();U();function mc(e){return e>=1?"over":e>=.8?"near":"ok"}function Ho(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:mc(n.dbBytes/t.maxStorageBytes)}:null;return{days:e.length,latest:n,peakDbBytes:r,computeSeconds:o,connections:s,storage:a}}function en(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 qo(e,t={}){let n=await v("cloud-project-usage",{ref:e,...t.days?{days:t.days}:{}}),r=Ho(n.days,n.limit);return w({...n,summary:r},()=>{if(l(T(`${n.name} (${n.ref})`)),!r.latest)l("Nothing measured yet. The host samples every fifteen minutes.");else if(r.storage){let o=Math.round(r.storage.fraction*100),s=`${X(r.storage.usedBytes)} of ${X(r.storage.limitBytes)} on ${n.limit?.tier}`;l(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 l(`${X(r.latest.dbBytes)} as of ${r.latest.day}.`);n.readOnly&&r.storage?.state!=="over"&&l("This project is currently read-only."),l(`Over ${r.days} day${r.days===1?"":"s"}: ${en(r.computeSeconds)} of compute, ${r.connections} connection${r.connections===1?"":"s"}.`),r.peakDbBytes!==null&&r.latest&&r.peakDbBytes>r.latest.dbBytes&&l(P(`Peak in that window was ${X(r.peakDbBytes)}.`)),t.history&&n.days.length>0&&(l(""),l(q([["DAY","SIZE","BACKUPS","COMPUTE","CONNS"],...n.days.map(o=>[o.day,o.dbBytes===null?"-":X(o.dbBytes),o.repoBytes===null?"-":X(o.repoBytes),en(o.computeSeconds),String(o.connections)])])),l(P("A dash is a day nothing measured it, which is what a paused project looks like.")))}),0}var Me="0.3.0",Ko=`snoutdata ${Me} \u2014 hosted Postgres, from a terminal or an agent
|
|
56
432
|
|
|
57
433
|
snoutdata init [--name X] [--env] a database for this folder, linked, ready to use
|
|
58
434
|
snoutdata login [--provider github] sign in through a browser
|
|
@@ -72,6 +448,26 @@ ${M}
|
|
|
72
448
|
|
|
73
449
|
snoutdata usage [--ref R] [--days 30] [--history] size, compute, and the plan's limit
|
|
74
450
|
|
|
451
|
+
snoutdata start [--port 54322] [--dir migrations] a Postgres here, migrated and seeded
|
|
452
|
+
snoutdata stop stop it; the data stays
|
|
453
|
+
snoutdata status is it running, how big, on what port
|
|
454
|
+
|
|
455
|
+
snoutdata gen types typescript [--local] [--schema public] [--out FILE]
|
|
456
|
+
the schema as a TypeScript Database type
|
|
457
|
+
|
|
458
|
+
snoutdata functions deploy <name> [--dir D] [--no-verify-jwt]
|
|
459
|
+
from supabase/functions/<name>, onto the edge
|
|
460
|
+
snoutdata functions list [--ref R]
|
|
461
|
+
snoutdata functions delete <name>
|
|
462
|
+
|
|
463
|
+
snoutdata secrets set NAME=value [...] the environment functions run with
|
|
464
|
+
snoutdata secrets set NAME --stdin the value from a pipe, not from history
|
|
465
|
+
snoutdata secrets list names and sizes; values are never readable
|
|
466
|
+
snoutdata secrets unset NAME
|
|
467
|
+
|
|
468
|
+
snoutdata keys [--ref R] the anon and service_role API keys
|
|
469
|
+
snoutdata keys rotate --force [--ref R] new ones, breaking every key already issued
|
|
470
|
+
|
|
75
471
|
snoutdata teams teams you can share a project with
|
|
76
472
|
snoutdata db url [--ref R] print a connection string
|
|
77
473
|
snoutdata db psql [--ref R] [-- ...] open psql, with no password typed
|
|
@@ -98,10 +494,10 @@ A project comes from --ref, then SNOUTDATA_PROJECT, then
|
|
|
98
494
|
.snoutdata/project.json in this folder or a parent. A token comes from
|
|
99
495
|
SNOUTDATA_ACCESS_TOKEN, then ~/.snoutdata/auth.json. The session that login writes lasts
|
|
100
496
|
an hour, so CI wants "tokens create", which does not expire.
|
|
101
|
-
`;async function
|
|
102
|
-
`)),0;if(
|
|
103
|
-
${
|
|
497
|
+
`;async function pc(e){let[t,n,...r]=e.command;if(B(e,"version")||t==="version")return w({version:Me},()=>process.stdout.write(`${Me}
|
|
498
|
+
`)),0;if(B(e,"help")&&t&&t!=="help"){let c=ut.filter(u=>u.name===t||u.name.startsWith(`${t} `));if(c.length>0)return w({commands:c},()=>{for(let u of c){let d=u.flags.length>0?` ${u.flags.join(" ")}`:"";process.stdout.write(` snoutdata ${u.name}${d}
|
|
499
|
+
${u.summary}
|
|
104
500
|
`)}process.stdout.write(`
|
|
105
501
|
Every command also takes --json, --quiet and --help.
|
|
106
|
-
`)}),0}let o=
|
|
107
|
-
`),
|
|
502
|
+
`)}),0}let o=B(e,"help")||t==="help";if(o||!t)return w({version:Me,usage:Ko.trim(),commands:ut},()=>process.stdout.write(Ko)),o?0:ee.usage;let s=()=>{let c=Se(e,"timeout");if(c!==void 0){if(!(c>0))throw new x("--timeout is a number of seconds greater than zero");return c*1e3}},a=()=>{let c=Le({flag:N(e,"ref")});if(!c)throw new x("no project: pass --ref, set SNOUTDATA_PROJECT, or run `snoutdata link --ref <ref>`");return c};switch(t){case"login":{if(B(e,"device")){let u=await Je({name:qe(),platform:process.platform});w({verifyUrl:u.verifyUrl,code:u.code,expiresAt:u.expiresAt},()=>{l(""),l(` Open ${u.verifyUrl}`),l(` Type ${u.code}`),l(""),l(P("Waiting for that to be approved\u2026"))});let d=await We(u.secret,{intervalMs:Math.max(1,u.interval)*1e3,expiresAt:u.expiresAt});return W({accessToken:d.token}),w({ok:!0,name:d.name??null},()=>l("Signed in.")),0}let c=await Ye({provider:N(e,"provider"),noBrowser:B(e,"no-browser")||!fe(),timeoutMs:s()});return w({email:c.email??null},()=>l(`Signed in${c.email?` as ${c.email}`:""}.`)),0}case"logout":{fn();let c=!!process.env.SNOUTDATA_ACCESS_TOKEN;return w({ok:!0,environmentTokenStillSet:c},()=>{l("Signed out."),c&&(l("SNOUTDATA_ACCESS_TOKEN is still set in this shell, so commands will keep working."),l(P(" unset SNOUTDATA_ACCESS_TOKEN")))}),0}case"whoami":{let c=await pe();return w(c,()=>{process.stdout.write(`${c.email??c.id}
|
|
503
|
+
`),c.token&&l(`via access token ${c.token.prefix} (${c.token.name}).`)}),0}case"tokens":switch(n){case void 0:case"list":return await qn(),0;case"create":{let c=N(e,"name")??r[0];if(!c)throw new x("tokens create needs --name (what is it for, and where does it live)");return await Hn({name:c,expiresInDays:Se(e,"expires")}),0}case"revoke":{let c=r[0];if(!c)throw new x("tokens revoke needs the token's id or its sdt_ prefix");return await Kn(c),0}default:throw new x(`unknown command: tokens ${n}`)}case"projects":switch(n){case void 0:case"list":return await jn(),0;case"create":{let c=N(e,"name")??r[0];if(!c)throw new x("projects create needs --name");let u=N(e,"team");return await St({teamId:u?await Bn(u):void 0,name:c,region:N(e,"region"),wait:!B(e,"no-wait"),timeoutMs:s(),link:void 0}),0}case"pause":case"resume":case"delete":return await Ln(n,N(e,"ref")??r[0]??a()),0;default:throw new x(`unknown command: projects ${n}`)}case"functions":switch(n){case"deploy":{let c=r[0];if(!c)throw new x("functions deploy needs a name: snoutdata functions deploy <name>");return await Qn(a(),c,{dir:N(e,"dir"),entrypoint:N(e,"entrypoint"),verifyJwt:!B(e,"no-verify-jwt")}),0}case void 0:case"list":return await er(a()),0;case"delete":case"remove":{let c=r[0];if(!c)throw new x("functions delete needs a name");return await tr(a(),c),0}default:throw new x(`unknown command: functions ${n}`)}case"secrets":switch(n){case"set":return await nr(a(),r,{stdin:B(e,"stdin")}),0;case void 0:case"list":return await or(a()),0;case"unset":case"remove":{let c=r[0];if(!c)throw new x("secrets unset needs a name");return await rr(a(),c),0}default:throw new x(`unknown command: secrets ${n}`)}case"keys":switch(n){case void 0:case"show":case"list":return await Jn(a()),0;case"rotate":return await Wn(a(),{force:B(e,"force")}),0;default:throw new x(`unknown command: keys ${n}`)}case"teams":return await Fn(),0;case"link":{let c=N(e,"ref")??n;if(!c)throw new x("link needs --ref");let u=je(process.cwd(),{ref:c});return w({ref:c,path:u},()=>l(`Linked ${c} (${u}).`)),0}case"db":switch(n){case"url":return await $t(a()),0;case"psql":return fr(a(),r);case"reset-password":return await Un(a()),0;case"push":return wr(a(),{dir:N(e,"dir"),dryRun:!!e.flags["dry-run"],outOfOrder:!!e.flags["out-of-order"]});case"restore":return gr(a(),{file:N(e,"file"),force:!!e.flags.force});case"export":return pr(a(),{timeoutMs:s(),out:N(e,"out"),statusOnly:!!e.flags.status});default:throw new x(n?`unknown command: db ${n}`:"db needs an action: url, psql, reset-password, export, push, restore")}case"gen":{if(n!=="types")throw new x(n?`unknown command: gen ${n}`:"gen needs an action: types");let c=B(e,"local")?await jo(await De()):null,u=c?c.dbUrl:N(e,"db-url");return kr(r[0],{ref:u?void 0:a(),dbUrl:u,schemas:(N(e,"schema")??"public").split(",").map(d=>d.trim()).filter(Boolean),out:N(e,"out"),dataApiVersion:N(e,"data-api-version")??N(e,"postgrest-version"),sql:c?.sql})}case"start":return Io(await De(),{port:Se(e,"port"),dir:N(e,"dir"),noMigrations:B(e,"no-migrations"),outOfOrder:B(e,"out-of-order")});case"stop":return Mo(await De());case"status":return Co(await De());case"usage":return qo(a(),{days:Se(e,"days"),history:!!e.flags.history});case"mcp":return Bo({version:Me,allowDelete:!!e.flags["allow-delete"]});case"init":return gc(e);default:throw new x(`unknown command: ${t}`)}}async function gc(e){let t=Le({flag:N(e,"ref")});if(t)return l(`This folder is already linked to ${t}.`),await $t(t),0;let n=N(e,"name")??hc(process.cwd());l(`Creating a project called "${n}"\u2026`),await St({name:n,region:N(e,"region"),wait:!0,link:process.cwd()});let r=Le({cwd:process.cwd()});if(r&&B(e,"env")){let{connection:o}=await Promise.resolve().then(()=>(C(),he)),s=await o(r),a=mr(process.cwd(),s.uri);l(`${a.replaced?"Updated":"Wrote"} DATABASE_URL in ${a.path}.`)}return 0}function hc(e){let t=e.split(/[\\/]/).filter(Boolean);return t[t.length-1]??"project"}function yc(e){if(e instanceof Q)return ce(e.code,e.message,e.details),e.exitCode;if(e instanceof x)return ce("usage",e.message),ee.usage;if(e instanceof ne)return ce("not-signed-in",e.message),ee["not-signed-in"];if(e instanceof z){let n=xn(e.status);return ce(n,e.message,{status:e.status}),ee[n]}let t=vn(e);return ce(t,e instanceof Error?e.message:String(e)),ee[t]}function tn(e){process.exitCode=e,setTimeout(()=>process.exit(e),2e3).unref()}function wc(e){let t=e.indexOf("--");return(t===-1?e:e.slice(0,t)).includes("--json")}async function bc(){xe(wc(process.argv.slice(2)));let e;try{e=on(process.argv.slice(2))}catch(t){ce("usage",String(t instanceof Error?t.message:t)),tn(ee.usage);return}xe(e.json),Tn(e.flags.quiet===!0),gn(Ue({isTty:!!process.stdin.isTTY,json:e.json,env:process.env}));try{tn(await pc(e))}catch(t){tn(yc(t))}}bc();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snoutdata",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Hosted Postgres from a terminal or an agent. Create a database, get a DATABASE_URL, run migrations, and serve the same operations to an AI agent over MCP.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "SnoutData",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"node": ">=20"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"build": "tsc -p tsconfig.json && node build.mjs",
|
|
38
|
+
"build": "node ../cloud/sync-engine.mjs cli cliControl && tsc -p tsconfig.json && node build.mjs",
|
|
39
39
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
40
40
|
"test": "tsc -p tsconfig.json && node --test \"out/**/*.test.js\"",
|
|
41
41
|
"prepublishOnly": "npm run build"
|