snoutdata 0.10.3 → 0.10.4
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 +3 -0
- package/dist/snoutdata.mjs +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,6 +122,9 @@ snoutdata teams
|
|
|
122
122
|
snoutdata db url [--ref R] # a connection string
|
|
123
123
|
snoutdata db psql [--ref R] [-- ...] # opens psql, no password typed
|
|
124
124
|
snoutdata db reset-password [--ref R]
|
|
125
|
+
snoutdata db access [--ref R] # who signs in as themselves (Postgres 18)
|
|
126
|
+
snoutdata db access grant EMAIL [--level full|read] # prints their connection string
|
|
127
|
+
snoutdata db access revoke EMAIL|ROLE
|
|
125
128
|
snoutdata db export [--ref R] [--out FILE] # take a dump and download it
|
|
126
129
|
snoutdata db push [--dir migrations] [--dry-run] # run the .sql files, once each
|
|
127
130
|
snoutdata db restore --file DUMP [--force]
|
package/dist/snoutdata.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var cc=Object.defineProperty;var D=(e,t)=>()=>(e&&(t=e(e=0)),t);var Hr=(e,t)=>{for(var n in t)cc(e,n,{get:t[n],enumerable:!0})};function Gr(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]=uc(o);if(lc.has(s)){let l=a??e[++r];if(l===void 0)throw new h(`--${s} needs a value`);n[s]=l;continue}if(a!==void 0)throw new h(`--${s} does not take a value`);if(!dc.has(s))throw new h(`unknown option --${s}`);n[s]=!0}return{command:t,flags:n,json:n.json===!0}}function uc(e){let t=e.replace(/^--?/,""),n=t.indexOf("=");return n===-1?[t,void 0]:[t.slice(0,n),t.slice(n+1)]}function A(e,t){let n=e.flags[t];return typeof n=="string"?n:void 0}function G(e,t){return e.flags[t]===!0}function Ae(e,t){let n=A(e,t);if(n===void 0)return;let r=Number(n);if(!Number.isInteger(r))throw new h(`--${t} must be a whole number, not "${n}"`);return r}var h,lc,dc,te=D(()=>{"use strict";h=class extends Error{},lc=new Set(["name","at","region","ref","file","provider","domain","expires","project","timeout","out","team","dir","days","schema","db-url","data-api-version","postgrest-version","port","entrypoint","memory","concurrency","client-id","site-url","allow","subject","p8","key-id","team-id","topic","environment","email","channel","since","level"]);dc=new Set(["json","window","help","version","force","no-verify-jwt","reset","stdin","quiet","status","no-wait","no-browser","device","sso","env","dry-run","out-of-order","allow-delete","local","no-migrations","history","show-url","check","watch"])});import{chmodSync as mc,existsSync as Kr,mkdirSync as Wr,readFileSync as Jr,rmSync as fc,writeFileSync as zr}from"node:fs";import{homedir as kt}from"node:os";import{dirname as _n,join as Vr,parse as pc,resolve as gc}from"node:path";function xn(e=kt()){return Vr(e,".snoutdata","auth.json")}function vt(e=kt()){let t=process.env.SNOUTDATA_ACCESS_TOKEN;if(t)return{accessToken:t};let n=xn(e);if(!Kr(n))return null;try{let r=JSON.parse(Jr(n,"utf8"));return typeof r.accessToken=="string"?r:null}catch{return null}}function ae(e,t=kt()){let n=xn(t);Wr(_n(n),{recursive:!0}),zr(n,`${JSON.stringify(e,null,2)}
|
|
3
3
|
`,{mode:384}),mc(n,384)}function Yr(e=kt()){fc(xn(e),{force:!0})}function Xr(e,t=Date.now()){return e.expiresAt?e.expiresAt*1e3-6e4<=t:!1}function Zr(e){return Vr(e,".snoutdata","project.json")}function Ve(e){let t=gc(e),n=pc(t).root;for(;;){let r=Zr(t);if(Kr(r))try{let o=JSON.parse(Jr(r,"utf8"));if(typeof o.ref=="string"&&o.ref)return typeof o.local=="string"&&o.local?{ref:o.ref,directory:t,local:o.local}:{ref:o.ref,directory:t}}catch{}if(t===n)return null;t=_n(t)}}function Ye(e,t){let n=Zr(e);return Wr(_n(n),{recursive:!0}),zr(n,`${JSON.stringify(t,null,2)}
|
|
4
|
-
`),n}function Xe(e){let t=e.environment??process.env;return e.flag?e.flag:t.SNOUTDATA_PROJECT?t.SNOUTDATA_PROJECT:Ve(e.cwd??process.cwd())?.ref??null}var we=D(()=>{"use strict"});import{existsSync as kn,readFileSync as Qr}from"node:fs";import{homedir as hc}from"node:os";import{isAbsolute as yc,join as be,resolve as De}from"node:path";function wc(e=process.env,t=process.platform,n=hc()){let r;return t==="win32"?r=e.APPDATA??be(n,"AppData","Roaming"):t==="darwin"?r=be(n,"Library","Application Support"):r=e.XDG_CONFIG_HOME??be(n,".config"),["snoutdata","snoutdata-dev"].map(o=>be(r,o,"localProjects.json"))}function Et(e=wc()){let t=[];for(let n of e)if(kn(n))try{let r=JSON.parse(Qr(n,"utf8"));for(let o of r.projects??[])typeof o.folder=="string"&&o.folder&&!t.some(s=>s.folder===o.folder)&&t.push({name:o.name??null,folder:o.folder,ref:o.ref??null})}catch{}return t}function $t(e){let t=new Map;for(let n of e.split(/\r?\n/)){let r=n.trim();if(!r||r.startsWith("#"))continue;let o=r.indexOf("=");if(o<=0)continue;let s=r.slice(0,o).trim(),a=r.slice(o+1).trim();a.length>=2&&(a[0]==='"'||a[0]==="'")&&a.endsWith(a[0])&&(a=a.slice(1,-1)),t.set(s,a)}return t}function Ze(e){return kn(be(e,"compose.yaml"))&&kn(be(e,".env"))}function Se(e,t=null){let n=De(e);if(!Ze(n))throw new Error(`${n} is not a SnoutData stack folder: it needs compose.yaml and .env (see github.com/snoutdata/snout-stack)`);let r=$t(Qr(be(n,".env"),"utf8")),o=r.get("SNOUT_REF"),s=r.get("POSTGRES_OWNER_PASSWORD");if(!o||!s)throw new Error(`${be(n,".env")} has no ${o?"POSTGRES_OWNER_PASSWORD":"SNOUT_REF"}, so this stack was not set up with \`snout-stack init\``);let a=Number(r.get("API_PORT")??8e3)||8e3,l=Number(r.get("DB_PORT")??5432)||5432;return{folder:n,ref:o,name:t,apiUrl:(r.get("API_EXTERNAL_URL")||`http://127.0.0.1:${a}`).replace(/\/$/,""),apiPort:a,dbPort:l,anonKey:r.get("ANON_KEY")||null,serviceRoleKey:r.get("SERVICE_ROLE_KEY")||null,ownerPassword:s}}function eo(e,t=process.cwd(),n=Et()){if(!e){if(Ze(t))return Se(t);let[i]=n;if(n.length===1&&i)return Se(i.folder,i.name);throw n.length===0?new Error("there is no local project to link: set one up in Studio (Projects, Local), or pass the folder of a stack you made with snout-stack"):new Error(`there are ${n.length} local projects; name one: ${n.map(u=>u.name??u.folder).join(", ")}`)}let r=yc(e)?e:De(t,e);if(Ze(r)){let i=n.find(u=>De(u.folder)===De(r));return Se(r,i?.name??null)}let o=e.toLowerCase(),s=n.filter(i=>i.name?.toLowerCase()===o||i.ref===e),[a]=s;if(s.length===1&&a)return Se(a.folder,a.name);let l=n.map(i=>i.name??i.folder);throw new Error(s.length>1?`more than one local project is called ${e}; link it by its folder`:`no local project called ${e}, and it is not a stack folder${l.length?`. The local projects are ${l.join(", ")}`:""}`)}function to(e){let t=`${e.ref}_owner`;return{ref:e.ref,host:"127.0.0.1",port:e.dbPort,database:e.ref,user:t,password:e.ownerPassword,ssl:"disable",uri:`postgresql://${t}:${encodeURIComponent(e.ownerPassword)}@127.0.0.1:${e.dbPort}/${e.ref}`,anonKey:e.anonKey,serviceRoleKey:e.serviceRoleKey,keysIssuedAt:null,keysExpireAt:null,wakesInstantly:!0,state:"local"}}function no(e,t,n=Et()){if(t&&Ze(t))try{let r=n.find(s=>De(s.folder)===De(t)),o=Se(t,r?.name??null);if(o.ref===e)return o}catch{}for(let r of n)if(r.ref===e&&Ze(r.folder))return Se(r.folder,r.name);return null}var Qe=D(()=>{"use strict"});function O(e,t,n){throw new N(e,t,n)}function At(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===410?"outdated":e>=500?"server":"failed"}function ro(e){let t=e;return!t||typeof t!="object"||t.code!=="ENOENT"||typeof t.path!="string"||t.syscall==="spawn"?null:`no file at ${t.path}`}function oo(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(bc.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 de,N,bc,Q=D(()=>{"use strict";de={ok:0,failed:1,usage:2,"not-signed-in":3,"not-ready":4,forbidden:5,"not-found":6,conflict:7,quota:8,network:9,timeout:10,outdated:11,"tool-missing":127,server:1},N=class extends Error{code;details;constructor(t,n,r){super(n),this.name="CliFailure",this.code=t,this.details=r}get exitCode(){return de[this.code]}};bc=new Set(["ECONNREFUSED","ECONNRESET","ENOTFOUND","EAI_AGAIN","EHOSTUNREACH","ENETUNREACH","EPIPE","CERT_HAS_EXPIRED","UNABLE_TO_VERIFY_LEAF_SIGNATURE","DEPTH_ZERO_SELF_SIGNED_CERT"])});function Pt(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 Sc){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 Ie(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 so(e){vn=e}function _e(){return(vn??Pt({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).canAsk}function Me(){return(vn??Pt({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).reason}var Sc,vn,Ce=D(()=>{"use strict";Sc=["CI","GITHUB_ACTIONS","GITLAB_CI","BUILDKITE","CIRCLECI","TF_BUILD"];vn=null});import{existsSync as _c,readFileSync as xc}from"node:fs";import{homedir as En,hostname as kc}from"node:os";import{join as je}from"node:path";function vc(e,t,n){if(t==="win32"){let r=n.APPDATA??je(En(),"AppData","Roaming");return je(r,e)}return t==="darwin"?je(En(),"Library","Application Support",e):je(n.XDG_CONFIG_HOME??je(En(),".config"),e)}function io(e=process.platform,t=process.env){return t.SNOUTDATA_DESKTOP_CONFIG?[t.SNOUTDATA_DESKTOP_CONFIG]:["snoutdata","snoutdata-dev"].map(n=>je(vc(n,e,t),"mcp.json"))}function Ec(e=process.platform,t=process.env){return io(e,t)[0]}function $c(e=Ec()){if(!_c(e))return null;try{let t=JSON.parse(xc(e,"utf8"));return typeof t.port!="number"||typeof t.token!="string"||!t.token?null:t}catch{return null}}function Ac(e){return e?e.enabled===!1?"server-disabled":e.signInHandoff===!1?"handoff-disabled":null:"no-config"}async function Rt(e){if(e!==void 0)return ao(e);let t="no-config";for(let n of io()){let r=$c(n);if(!r)continue;let o=await ao(r);if(o.available)return o;(o.skip!=="not-running"||t==="no-config")&&(t=o.skip)}return{available:!1,skip:t}}async function ao(e){let t=Ac(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(Pc)});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 co(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??`SnoutData Studio refused (${n.status})`}}function Tt(){return kc()}async function $n(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??"SnoutData Studio refused");return s.result}async function lo(e){return await $n(e,"initialize",{protocolVersion:"2025-06-18",capabilities:{},clientInfo:{name:"snoutdata-cli",version:"0"}},3e3),(await $n(e,"tools/list",{},3e3)).tools??[]}async function uo(e,t,n){return $n(e,"tools/call",{name:t,arguments:n},12e4)}var Pc,Ot=D(()=>{"use strict";Pc=1500});import{basename as Rc}from"node:path";function Dt(e={bun:typeof process.versions.bun=="string",execPath:process.execPath,script:process.argv[1]??""}){let t=Rc(e.execPath.replace(/\\/g,"/")).toLowerCase();if(e.bun&&t.startsWith("snoutdata"))return"binary";let n=e.script.replace(/\\/g,"/");return/\/_npx\//.test(n)?"npx":/\/node_modules\/snoutdata\//.test(n)?"npm":"source"}function Tc(e=Dt(),t=process.platform){switch(e){case"binary":case"npm":return"snoutdata upgrade";case"npx":return"npx snoutdata@latest";case"source":return t==="win32"?ue:et}}function An(e={}){let t=e.kind??Dt(),n=e.platform??process.platform,r=e.minimum?`${e.minimum} or later is required`:"a newer version is required",o=n==="win32"?ue:`${et} (or ${ue})`;return`snoutdata ${V} is installed and ${r}. Run \`${Tc(t,n)}\`, or install again: ${o}`}function fo(e,t){if(e==="dev"||t==="dev")return!1;let n=e.split("-")[0].split(".").map(Number),r=t.split("-")[0].split(".").map(Number);for(let o=0;o<Math.max(n.length,r.length);o+=1){let s=n[o]??0,a=r[o]??0;if(Number.isNaN(s)||Number.isNaN(a))return!1;if(s!==a)return s<a}return!1}var V,Nt,et,ue,tt=D(()=>{"use strict";V="0.10.3",Nt=`snoutdata-cli/${V}`,et="curl -fsSL https://snoutdata.com/install.sh | sh",ue="npm install -g snoutdata@latest"});async function po(e,t){let n=await fetch(`${ie}/functions/v1/${e}`,{method:"POST",headers:{apikey:me,"Content-Type":"application/json","x-client-info":Nt},body:JSON.stringify(t)});Ct(n);let r=await n.json().catch(()=>({}));if(n.status===410){let o=r.error;throw jt(n,typeof o=="string"?o:"This version of the snoutdata CLI is no longer supported.")}return{status:n.status,body:r}}async function It(e={}){let{status:t,body:n}=await po("cloud-device-start",{client:e});return t===429&&O("quota",n.error??"too many pairing attempts from here: wait a few minutes"),t===404&&O("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)&&O("server",n.error??`could not start a pairing (${t})`),n}async function Mt(e,t){let n=t.intervalMs,r=Date.parse(t.expiresAt);for(;;){Date.now()>r&&O("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 po("cloud-device-poll",{secret:e});switch(o===404&&O("not-found","that pairing is not known to the server. Run the command again for a new code."),o===400&&O("usage",s.error??"that is not a pairing secret"),o!==200&&O("server",s.error??`the pairing could not be checked (${o})`),s.status){case"approved":return{token:s.token,name:s.name};case"denied":O("forbidden",s.error??"that pairing was refused in the browser");case"used":O("conflict",s.error??"that pairing has already been completed");case"expired":O("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 Pn=D(()=>{"use strict";W();Q();tt()});function rt(e){nt=e}function ho(e){go=e}function yo(){return nt}function b(e){return wo?`\x1B[2m${e}\x1B[0m`:e}function v(e){return wo?`\x1B[1m${e}\x1B[0m`:e}function g(e,t){if(nt){process.stdout.write(`${JSON.stringify(e,null,2)}
|
|
4
|
+
`),n}function Xe(e){let t=e.environment??process.env;return e.flag?e.flag:t.SNOUTDATA_PROJECT?t.SNOUTDATA_PROJECT:Ve(e.cwd??process.cwd())?.ref??null}var we=D(()=>{"use strict"});import{existsSync as kn,readFileSync as Qr}from"node:fs";import{homedir as hc}from"node:os";import{isAbsolute as yc,join as be,resolve as De}from"node:path";function wc(e=process.env,t=process.platform,n=hc()){let r;return t==="win32"?r=e.APPDATA??be(n,"AppData","Roaming"):t==="darwin"?r=be(n,"Library","Application Support"):r=e.XDG_CONFIG_HOME??be(n,".config"),["snoutdata","snoutdata-dev"].map(o=>be(r,o,"localProjects.json"))}function Et(e=wc()){let t=[];for(let n of e)if(kn(n))try{let r=JSON.parse(Qr(n,"utf8"));for(let o of r.projects??[])typeof o.folder=="string"&&o.folder&&!t.some(s=>s.folder===o.folder)&&t.push({name:o.name??null,folder:o.folder,ref:o.ref??null})}catch{}return t}function $t(e){let t=new Map;for(let n of e.split(/\r?\n/)){let r=n.trim();if(!r||r.startsWith("#"))continue;let o=r.indexOf("=");if(o<=0)continue;let s=r.slice(0,o).trim(),a=r.slice(o+1).trim();a.length>=2&&(a[0]==='"'||a[0]==="'")&&a.endsWith(a[0])&&(a=a.slice(1,-1)),t.set(s,a)}return t}function Ze(e){return kn(be(e,"compose.yaml"))&&kn(be(e,".env"))}function Se(e,t=null){let n=De(e);if(!Ze(n))throw new Error(`${n} is not a SnoutData stack folder: it needs compose.yaml and .env (see github.com/snoutdata/snout-stack)`);let r=$t(Qr(be(n,".env"),"utf8")),o=r.get("SNOUT_REF"),s=r.get("POSTGRES_OWNER_PASSWORD");if(!o||!s)throw new Error(`${be(n,".env")} has no ${o?"POSTGRES_OWNER_PASSWORD":"SNOUT_REF"}, so this stack was not set up with \`snout-stack init\``);let a=Number(r.get("API_PORT")??8e3)||8e3,l=Number(r.get("DB_PORT")??5432)||5432;return{folder:n,ref:o,name:t,apiUrl:(r.get("API_EXTERNAL_URL")||`http://127.0.0.1:${a}`).replace(/\/$/,""),apiPort:a,dbPort:l,anonKey:r.get("ANON_KEY")||null,serviceRoleKey:r.get("SERVICE_ROLE_KEY")||null,ownerPassword:s}}function eo(e,t=process.cwd(),n=Et()){if(!e){if(Ze(t))return Se(t);let[i]=n;if(n.length===1&&i)return Se(i.folder,i.name);throw n.length===0?new Error("there is no local project to link: set one up in Studio (Projects, Local), or pass the folder of a stack you made with snout-stack"):new Error(`there are ${n.length} local projects; name one: ${n.map(u=>u.name??u.folder).join(", ")}`)}let r=yc(e)?e:De(t,e);if(Ze(r)){let i=n.find(u=>De(u.folder)===De(r));return Se(r,i?.name??null)}let o=e.toLowerCase(),s=n.filter(i=>i.name?.toLowerCase()===o||i.ref===e),[a]=s;if(s.length===1&&a)return Se(a.folder,a.name);let l=n.map(i=>i.name??i.folder);throw new Error(s.length>1?`more than one local project is called ${e}; link it by its folder`:`no local project called ${e}, and it is not a stack folder${l.length?`. The local projects are ${l.join(", ")}`:""}`)}function to(e){let t=`${e.ref}_owner`;return{ref:e.ref,host:"127.0.0.1",port:e.dbPort,database:e.ref,user:t,password:e.ownerPassword,ssl:"disable",uri:`postgresql://${t}:${encodeURIComponent(e.ownerPassword)}@127.0.0.1:${e.dbPort}/${e.ref}`,anonKey:e.anonKey,serviceRoleKey:e.serviceRoleKey,keysIssuedAt:null,keysExpireAt:null,wakesInstantly:!0,state:"local"}}function no(e,t,n=Et()){if(t&&Ze(t))try{let r=n.find(s=>De(s.folder)===De(t)),o=Se(t,r?.name??null);if(o.ref===e)return o}catch{}for(let r of n)if(r.ref===e&&Ze(r.folder))return Se(r.folder,r.name);return null}var Qe=D(()=>{"use strict"});function O(e,t,n){throw new N(e,t,n)}function At(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===410?"outdated":e>=500?"server":"failed"}function ro(e){let t=e;return!t||typeof t!="object"||t.code!=="ENOENT"||typeof t.path!="string"||t.syscall==="spawn"?null:`no file at ${t.path}`}function oo(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(bc.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 de,N,bc,Q=D(()=>{"use strict";de={ok:0,failed:1,usage:2,"not-signed-in":3,"not-ready":4,forbidden:5,"not-found":6,conflict:7,quota:8,network:9,timeout:10,outdated:11,"tool-missing":127,server:1},N=class extends Error{code;details;constructor(t,n,r){super(n),this.name="CliFailure",this.code=t,this.details=r}get exitCode(){return de[this.code]}};bc=new Set(["ECONNREFUSED","ECONNRESET","ENOTFOUND","EAI_AGAIN","EHOSTUNREACH","ENETUNREACH","EPIPE","CERT_HAS_EXPIRED","UNABLE_TO_VERIFY_LEAF_SIGNATURE","DEPTH_ZERO_SELF_SIGNED_CERT"])});function Pt(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 Sc){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 Ie(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 so(e){vn=e}function _e(){return(vn??Pt({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).canAsk}function Me(){return(vn??Pt({isTty:!!process.stdin.isTTY,json:!1,env:process.env})).reason}var Sc,vn,Ce=D(()=>{"use strict";Sc=["CI","GITHUB_ACTIONS","GITLAB_CI","BUILDKITE","CIRCLECI","TF_BUILD"];vn=null});import{existsSync as _c,readFileSync as xc}from"node:fs";import{homedir as En,hostname as kc}from"node:os";import{join as je}from"node:path";function vc(e,t,n){if(t==="win32"){let r=n.APPDATA??je(En(),"AppData","Roaming");return je(r,e)}return t==="darwin"?je(En(),"Library","Application Support",e):je(n.XDG_CONFIG_HOME??je(En(),".config"),e)}function io(e=process.platform,t=process.env){return t.SNOUTDATA_DESKTOP_CONFIG?[t.SNOUTDATA_DESKTOP_CONFIG]:["snoutdata","snoutdata-dev"].map(n=>je(vc(n,e,t),"mcp.json"))}function Ec(e=process.platform,t=process.env){return io(e,t)[0]}function $c(e=Ec()){if(!_c(e))return null;try{let t=JSON.parse(xc(e,"utf8"));return typeof t.port!="number"||typeof t.token!="string"||!t.token?null:t}catch{return null}}function Ac(e){return e?e.enabled===!1?"server-disabled":e.signInHandoff===!1?"handoff-disabled":null:"no-config"}async function Rt(e){if(e!==void 0)return ao(e);let t="no-config";for(let n of io()){let r=$c(n);if(!r)continue;let o=await ao(r);if(o.available)return o;(o.skip!=="not-running"||t==="no-config")&&(t=o.skip)}return{available:!1,skip:t}}async function ao(e){let t=Ac(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(Pc)});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 co(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??`SnoutData Studio refused (${n.status})`}}function Tt(){return kc()}async function $n(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??"SnoutData Studio refused");return s.result}async function lo(e){return await $n(e,"initialize",{protocolVersion:"2025-06-18",capabilities:{},clientInfo:{name:"snoutdata-cli",version:"0"}},3e3),(await $n(e,"tools/list",{},3e3)).tools??[]}async function uo(e,t,n){return $n(e,"tools/call",{name:t,arguments:n},12e4)}var Pc,Ot=D(()=>{"use strict";Pc=1500});import{basename as Rc}from"node:path";function Dt(e={bun:typeof process.versions.bun=="string",execPath:process.execPath,script:process.argv[1]??""}){let t=Rc(e.execPath.replace(/\\/g,"/")).toLowerCase();if(e.bun&&t.startsWith("snoutdata"))return"binary";let n=e.script.replace(/\\/g,"/");return/\/_npx\//.test(n)?"npx":/\/node_modules\/snoutdata\//.test(n)?"npm":"source"}function Tc(e=Dt(),t=process.platform){switch(e){case"binary":case"npm":return"snoutdata upgrade";case"npx":return"npx snoutdata@latest";case"source":return t==="win32"?ue:et}}function An(e={}){let t=e.kind??Dt(),n=e.platform??process.platform,r=e.minimum?`${e.minimum} or later is required`:"a newer version is required",o=n==="win32"?ue:`${et} (or ${ue})`;return`snoutdata ${V} is installed and ${r}. Run \`${Tc(t,n)}\`, or install again: ${o}`}function fo(e,t){if(e==="dev"||t==="dev")return!1;let n=e.split("-")[0].split(".").map(Number),r=t.split("-")[0].split(".").map(Number);for(let o=0;o<Math.max(n.length,r.length);o+=1){let s=n[o]??0,a=r[o]??0;if(Number.isNaN(s)||Number.isNaN(a))return!1;if(s!==a)return s<a}return!1}var V,Nt,et,ue,tt=D(()=>{"use strict";V="0.10.4",Nt=`snoutdata-cli/${V}`,et="curl -fsSL https://snoutdata.com/install.sh | sh",ue="npm install -g snoutdata@latest"});async function po(e,t){let n=await fetch(`${ie}/functions/v1/${e}`,{method:"POST",headers:{apikey:me,"Content-Type":"application/json","x-client-info":Nt},body:JSON.stringify(t)});Ct(n);let r=await n.json().catch(()=>({}));if(n.status===410){let o=r.error;throw jt(n,typeof o=="string"?o:"This version of the snoutdata CLI is no longer supported.")}return{status:n.status,body:r}}async function It(e={}){let{status:t,body:n}=await po("cloud-device-start",{client:e});return t===429&&O("quota",n.error??"too many pairing attempts from here: wait a few minutes"),t===404&&O("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)&&O("server",n.error??`could not start a pairing (${t})`),n}async function Mt(e,t){let n=t.intervalMs,r=Date.parse(t.expiresAt);for(;;){Date.now()>r&&O("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 po("cloud-device-poll",{secret:e});switch(o===404&&O("not-found","that pairing is not known to the server. Run the command again for a new code."),o===400&&O("usage",s.error??"that is not a pairing secret"),o!==200&&O("server",s.error??`the pairing could not be checked (${o})`),s.status){case"approved":return{token:s.token,name:s.name};case"denied":O("forbidden",s.error??"that pairing was refused in the browser");case"used":O("conflict",s.error??"that pairing has already been completed");case"expired":O("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 Pn=D(()=>{"use strict";W();Q();tt()});function rt(e){nt=e}function ho(e){go=e}function yo(){return nt}function b(e){return wo?`\x1B[2m${e}\x1B[0m`:e}function v(e){return wo?`\x1B[1m${e}\x1B[0m`:e}function g(e,t){if(nt){process.stdout.write(`${JSON.stringify(e,null,2)}
|
|
5
5
|
`);return}t()}function c(e){!nt&&!go&&process.stderr.write(`${e}
|
|
6
6
|
`)}function xe(e,t,n){process.stderr.write(`${t}
|
|
7
7
|
`),nt&&process.stdout.write(`${JSON.stringify({ok:!1,code:e,error:t,...n},null,2)}
|
|
@@ -561,7 +561,7 @@ SELECT name, from_version, coalesce(to_version, ''), coalesce(error, '') FROM sn
|
|
|
561
561
|
`).at(-1);e?.(`Could not check for a newer database image (${y}); using the one on this machine.`);return}let u=await t(B,["image","inspect","--format","{{.Id}}",r]).catch(()=>null),d=u?.code===0?u.stdout.trim():"";if(!d||d===s)return;let f=await t(B,["tag",r,n]);if(f.code!==0){e?.(`Fetched a newer ${r} but could not tag it as ${n}: ${f.stderr.trim()}`);return}e?.(`Fetched a newer database image (${r}).`)}async function ym(e=fm,t=Mr){if(Nr!=="podman")return`SNOUTPOD_ENGINE=${Nr}: a local project is Podman-only, because a database that works under Docker and not under Podman is a database that works here and not in the cloud`;let n;try{n=await z(B,["--version"])}catch{return`${B} is not installed. macOS: brew install podman, then podman machine init && podman machine set --rootful=false && podman machine start. Windows: winget install RedHat.Podman, then the same three machine commands. Linux: your distribution's podman package.`}if(n.code!==0)return`${B} --version failed: ${n.stderr.trim()||n.stdout.trim()}`;if((await z(B,["image","exists",t])).code!==0)return`the pod image ${t} is not on this machine. ${e}`;let o=await z(B,["machine","inspect","--format","{{.Rootful}}"]);return o.code===0&&o.stdout.trim().startsWith("true")?"the podman machine is ROOTFUL, and the hosts are not: podman machine stop && podman machine set --rootful=false && podman machine start":null}function wm(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 z(B,["machine","ssh",t]).then(n=>{let r=n.stdout.trim().split(`
|
|
562
562
|
`).at(-1);return r==="DATA"?"data":r==="EMPTY"?"empty":"hidden"},()=>"hidden")}function gn(e){let t=pn()==="darwin"||pn()==="win32";return za(z,e,{binary:B,look:t?wm:void 0})}async function mn(e,t=z,n=pn()){let r=j(e).pod;if((await t(B,["volume","exists",r])).code!==0)return null;let o;if(n==="darwin"||n==="win32"){let s=null;for(let a of We){let l=wt(a).name;if((await t(B,["image","exists",l])).code===0){s=l;break}}if(!s)throw new Error(`${r} holds a database and there is no pod image on this machine to read its Postgres version with. Set SNOUTPOD_IMAGE to the image it was made with (${We.map(a=>wt(a).published).join(" or ")}).`);o=await t(B,["run","--rm","--pull=never","--network=none","--userns=host","--user=0","--entrypoint=cat","--volume",`${r}:/data:ro`,s,"/data/PG_VERSION"])}else{let s=await t(B,["volume","inspect","--format","{{.Mountpoint}}",r]),a=s.code===0?s.stdout.trim().replace(/\/+$/,""):"";if(!a)throw new Error(`could not find where ${r} is: ${s.stderr.trim()||"no mountpoint"}`);o=await t(B,["unshare","cat",`${a}/PG_VERSION`])}if(o.code===0)return o.stdout;if(/no such file/i.test(o.stderr))return null;throw new Error(`could not read the Postgres version in ${r}: ${o.stderr.trim()||o.stdout.trim()||`exit ${o.code}`}`)}function Ti(e,t=mn){if(!e||process.env.SNOUTPOD_IMAGE)return Promise.resolve(bt(null));Z(e);let n=t===mn?fn.get(e):void 0;if(n)return n;let r=t(e).then(o=>{try{return bt(o)}catch(s){throw new Error(`${Je(e)}: ${s instanceof Error?s.message:String(s)}`)}});return t===mn&&(fn.set(e,r),r.catch(()=>fn.delete(e))),r}async function Oi(){return{ramFreeMb:4096,ramTotalMb:8192,diskFreeMb:32768,diskTotalMb:65536}}function bm(e){return ki({exec:z,binary:B,network:$e,hasData:gn,capacity:Oi,repository:e})}function Je(e){return j(e).pod}async function Sm(e){if(process.env.SNOUTPOD_LOCAL_ALLOW_EGRESS==="1"){await _m()||await z(B,sn({name:$e}));return}let t=sn({name:$e,allow:[]}),n="";for(let r=0;r<10;r+=1){if(await Di())return;await e?.(),await z(B,["network","rm","--force",$e]).catch(()=>{});let o=await z(B,t);if(o.code===0)return;if(n=`${o.stderr}${o.stdout}`.trim(),/invalid route gateway/i.test(n))throw new Error(`${Ni}: ${n}`);await new Promise(s=>setTimeout(s,500))}throw new Error(`could not create ${$e} with egress blocked${n?`: ${n}`:""}`)}async function _m(){return(await z(B,["network","inspect",$e,"--format","json"])).code===0}async function Di(){let e=await z(B,["network","inspect",$e,"--format","json"]);if(e.code!==0)return!1;try{return Da(JSON.parse(e.stdout)?.[0])}catch{return!1}}function Ii(e){let t=j(e.ref);return`postgres://${t.ownerRole}:${encodeURIComponent(e.ownerPassword)}@127.0.0.1:${e.port}/${t.database}?sslmode=disable`}function Mi(e,t={}){return Z(e.ref),{ref:e.ref,generation:1,image:t.image??Mr,memoryMb:t.memoryMb??Pi,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 yt(e,t){e.onProgress?.(t)}async function xm(e,t,n={}){let r=n.image??(await Ti(t.ref,n.pgVersion)).name,o=Mi(t,{...n,image:r}),s=n.timeoutMs??12e4,a=n.hasData??gn,l=(await e.list()).find(u=>u.ref===t.ref),i=null;if(l?l.status==="stopped"&&(i=await(n.staleImage??Dr)(t.ref,r))?(yt(n,"Starting the database on the newer image. Its data stays as it is."),await e.remove(t.ref),await e.create(o),await(n.dropImage??Ci)(i)):l.status==="stopped"?(yt(n,"Starting the database."),await e.start(t.ref),await e.startServices(t.ref)):(yt(n,"The database is already running."),l.status==="running"&&await(n.staleImage??Dr)(t.ref,r)&&yt(n,"A newer database image is on this machine. To move onto it: snoutdata stop, then snoutdata start.")):(yt(n,await a(t.ref)?"Starting the database over its existing data.":"Creating the database."),await e.create(o)),!await e.waitReady(t.ref,s))throw new Error(await vm(t.ref,s,n.podLog??ji));return await e.applyOwnerPassword(o),await e.createServices(o),Cr(e,t,n)}function Ei(e){return e.trim().replace(/^sha256:/,"")}async function Dr(e,t){let[n,r]=await Promise.all([z(B,["container","inspect","--format","{{.Image}}",Je(e)]).catch(()=>null),z(B,["image","inspect","--format","{{.Id}}",t]).catch(()=>null)]);if(n?.code!==0||r?.code!==0)return null;let o=Ei(n.stdout),s=Ei(r.stdout);return o&&s&&o!==s?o:null}async function Ci(e){let t=await z(B,["image","inspect","--format","{{len .RepoTags}}",e]).catch(()=>null);t?.code===0&&t.stdout.trim()==="0"&&await z(B,["rmi",e]).catch(()=>{})}async function km(e,t){let n=j(e.ref);return z(B,["exec","--interactive","-e","PGPASSWORD",Je(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 ji(e){let t=await z(B,["logs","--tail","20",Je(e)]).catch(()=>null);return t?`${t.stdout}${t.stderr}`:"(no log: the container is not there)"}async function vm(e,t,n){let r=await n(e).catch(()=>"(no log)");return`${Je(e)} never became ready in ${t}ms
|
|
563
563
|
--- pod log ---
|
|
564
|
-
${r}`}async function Em(e,t,n={}){Z(t);let r=(await e.list()).find(o=>o.ref===t);return r&&r.status!=="stopped"&&await e.stop(t,Ka),Cr(e,{ref:t,port:0,ownerPassword:"",adminPassword:""},n)}async function $m(e,t,n){Z(t),await e.remove(t),n&&(await e.removeData(t),fn.delete(t))}async function Cr(e,t,n={}){Z(t.ref);let r=(await e.list()).find(a=>a.ref===t.ref),o=r?.status==="running",s=n.hasData??gn;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?Ii(t):null}}var Nr,B,Ir,We,lm,Ai,Mr,$e,dm,Pi,z,fm,pm,Ri,fn,Ni,Ui=D(()=>{"use strict";Wa();pt();Va();yr();xr();he();Nr=process.env.SNOUTPOD_ENGINE??"podman";B=cm(),Ir=18,We=[17,18],lm="snoutpod/postgres",Ai="ghcr.io/snoutdata/snoutpod-postgres";Mr=bt(null).name,$e=process.env.SNOUTPOD_LOCAL_NETWORK??"snoutpod-local",dm=54322,Pi=512,z=Ja();fm="cd packages/snoutpod && npm run image:build",pm=`${Ai}:${Ir}`;Ri=20*60*1e3;fn=new Map;Ni="this podman cannot blackhole a route, so the local tier cannot isolate its containers"});te();var bn=[{name:"init",summary:"Create or link a project for this folder, and print its DATABASE_URL or write it to .env",flags:["--name","--region","--env","--ref"]},{name:"login",summary:"Sign in through a browser, with --sso through your company's identity provider, or with --device by typing a code into one anywhere",flags:["--email","--provider","--sso","--domain","--no-browser","--device","--timeout"]},{name:"logout",summary:"Forget the stored session",flags:[]},{name:"whoami",summary:"Who this credential belongs to, and whether it is the account login --email asked for",flags:[]},{name:"upgrade",summary:"Install the newest CLI the way this one was installed (binary or npm), checksum verified",flags:["--check"]},{name:"tokens list",summary:"Long-lived access tokens on this account",flags:[]},{name:"tokens create",summary:"Mint an sdt_ token for CI or an agent",flags:["--name","--expires","--project"]},{name:"tokens revoke",summary:"Revoke one by id or prefix",flags:[]},{name:"projects list",summary:"Every project on this account",flags:[]},{name:"projects create",summary:"Make a hosted database, and print its ref",flags:["--name","--region","--team","--no-wait","--timeout","--show-url"]},{name:"projects pause",summary:"Stop a project, and wait until it is paused",flags:["--ref","--no-wait","--timeout"]},{name:"projects resume",summary:"Start a paused project, and wait until it is ready",flags:["--ref","--no-wait","--timeout"]},{name:"projects delete",summary:"Delete a project, and wait until it is gone",flags:["--ref","--no-wait","--timeout"]},{name:"projects show",summary:"One project: state, products (Realtime included), functions, secrets, domains",flags:["--ref"]},{name:"products",summary:"Auth, storage, the data API and push: on or off. Realtime is always on, and listed",flags:["--ref"]},{name:"products enable",summary:"Turn auth, storage, data-api or push on",flags:["--ref"]},{name:"products disable",summary:"Turn auth, storage, data-api or push off",flags:["--ref"]},{name:"push credentials",summary:"Push keys: what is set for APNs, FCM and Web Push (never the keys)",flags:["--ref"]},{name:"push credentials set",summary:"Set the APNs key (a .p8) or the FCM service account, checked before it is stored",flags:["--ref","--p8","--key-id","--team-id","--topic","--environment","--file"]},{name:"push credentials remove",summary:"Remove the APNs or FCM credentials",flags:["--ref"]},{name:"auth",summary:"Auth settings: Google sign-in and redirect addresses",flags:["--ref"]},{name:"auth google",summary:"Sign in with Google, with your own client (secret on stdin)",flags:["--ref","--client-id","--stdin"]},{name:"auth anonymous",summary:"Guest sign-in on or off: signInAnonymously() gives a browser a session with no email or password",flags:["--ref"]},{name:"auth redirects",summary:"The site URL and the addresses a sign-in may return to",flags:["--ref","--site-url","--allow"]},{name:"auth templates",summary:"The five auth emails, ours or your own",flags:["--ref"]},{name:"auth template",summary:"Use your own subject and HTML for one, or reset it",flags:["--ref","--file","--subject"]},{name:"realtime inspect",summary:"The channels open now: each client, its presence, when it was last heard, and the last minute of messages",flags:["--channel","--watch","--ref"]},{name:"realtime logs",summary:"The connection log: connects, joins, leaves and disconnects, with the reason each ended",flags:["--since","--channel","--ref"]},{name:"domains",summary:"Your own domains in front of the project API",flags:["--ref"]},{name:"domains add",summary:"Add one, and print the DNS records to publish",flags:["--ref"]},{name:"domains verify",summary:"Check its records and verify it",flags:["--ref"]},{name:"domains remove",summary:"Stop serving it",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 size",summary:"Its memory and workers, within the plan",flags:["--ref","--memory","--concurrency","--reset"]},{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, or rewind it to a moment into a new project",flags:["--file","--force","--at","--name","--window","--ref"]},{name:"mcp",summary:"Serve these operations as tools over stdio",flags:["--allow-delete"]}];var qr={"--ref":"The project. Defaults to SNOUTDATA_PROJECT, then .snoutdata/project.json here or in a parent","--name":"The project's name","tokens create --name":"What the token is for and where it lives, shown in tokens list","db restore --name":"The name of the new project the restore makes","--region":"Where the project runs. Defaults to the nearest","--env":"Write DATABASE_URL into .env in this folder, and add .env to .gitignore since it holds the password. Without it the URL is printed","--email":"The account to sign in as. The browser offers it, and a different account is reported at once and by whoami","--provider":"google (the default) or github","--sso":"Sign in with your company's identity provider","--domain":"With --sso: your work email or company domain. Only the domain is sent","--no-browser":"Print the sign-in URL instead of opening a browser","--device":"Sign in by typing a code into a browser on any machine: SSH, containers, agents","--timeout":"Seconds to wait before giving up","--check":"Say whether a newer version exists, and install nothing","--expires":"Days until the token stops working. Without it, it works until revoked","--project":"Limit the token to one project ref","--team":"Create it in a team (name or id), shared with its members","--no-wait":"Return as soon as it is asked for, without waiting for it to be true","--show-url":"Also print the connection string, which holds the password. Otherwise db url prints it when wanted","--p8":"The APNs .p8 key file (a path, never the key itself)","--key-id":"The APNs key id","--team-id":"Your Apple developer team id","--topic":"The app's bundle id","--environment":"production or sandbox","--file":"A file to read","push credentials set --file":"The FCM service-account JSON","auth template --file":"The email's HTML body","db restore --file":"The dump to restore (from db export or pg_dump -Fc)","--client-id":"Your Google OAuth client id; the secret is read from stdin with --stdin","--stdin":"Read the secret value from a pipe, so it is not in shell history","--site-url":"Where sign-in emails send people by default","--allow":"Comma-separated addresses a sign-in may return to","--subject":"The email's subject line","--dir":"The folder to read from","db push --dir":"The migrations folder (default ./migrations)","start --dir":"The migrations folder (default ./migrations)","functions deploy --dir":"The function's folder (default ./functions/<name>)","--entrypoint":"The file the runtime starts, when it is not index.ts","--no-verify-jwt":"Let anybody with the URL call it (webhooks). By default a caller needs a project key or a user session","--memory":"Memory per worker, in MB, within the plan","--concurrency":"How many workers it may run at once, within the plan","--reset":"Back to the plan's default size","--force":"Do it although it cannot be undone","--days":"How many days of history","--history":"Print the day-by-day table too","--port":"The port the local database listens on (default 54322)","--no-migrations":"Start the database and apply nothing","--out-of-order":"Apply a new migration that sorts before one already applied","--db-url":"Read a postgres:// database instead of a project","--local":"Read the database snoutdata start is running","--schema":"Comma-separated schemas (default public)","--out":"Write to this file instead of stdout","db export --out":"Where to save the dump","--status":"Show the last export without taking another","--dry-run":"Show what would run, and change nothing","--at":"The moment to restore to (an ISO time), into a NEW project","--window":"How far back a point-in-time restore can go","--allow-delete":"Let the agent's delete_project actually delete","--channel":"Only this channel","--since":"How far back, like 10m, 2h or 1d (default 1h)","--watch":"Keep printing what changes until interrupted","--level":"full (everything the project password can do) or read (reads the project's own tables, never the auth or storage schemas, and writes nothing). Default read"};function Sn(e,t){return qr[`${e} ${t}`]??qr[t]??""}W();Q();q();import{spawn as ol}from"node:child_process";function sl(e){return`snout-${e}`}function Ue(e,t,n){return new Promise((r,o)=>{let s=ol("docker",["compose","-p",sl(e.ref),"--project-directory",e.folder,...t],{cwd:e.folder,stdio:["ignore","pipe","pipe"]}),a="",l="";s.stdout.on("data",i=>{a+=String(i)}),s.stderr.on("data",i=>{if(l+=String(i),n)for(let u of String(i).split(/\r?\n/).filter(Boolean))c(b(` ${u.trim()}`))}),s.on("error",i=>{if(i.code==="ENOENT"){o(new N("tool-missing","docker is not installed, or not on PATH. A local project runs in Docker (Docker Desktop on Windows and macOS)."));return}o(new N("failed",String(i)))}),s.on("close",i=>r({code:i??0,out:a,err:l}))})}async function al(e){let t=await Ue(e,["ps","-a","--format","json"],!1);if(t.code!==0)throw new N("failed",t.err.trim()||"docker compose ps failed. Is Docker running?");let n=t.out.trim();return(n.startsWith("[")?JSON.parse(n):n.split(/\r?\n/).filter(Boolean).map(o=>JSON.parse(o))).map(o=>({service:o.Service??"?",state:o.State==="exited"&&o.ExitCode===0?"done":o.State??"?",health:o.Health??""})).sort((o,s)=>o.service.localeCompare(s.service))}async function To(e){let t=await al(e),n=t.filter(o=>o.state==="running").length,r=t.filter(o=>o.state!=="done").length;return g({ref:e.ref,local:e.folder,api:e.apiUrl,dbPort:e.dbPort,running:n,services:t},()=>{if(c(`${v(e.name??e.ref)} (local, ${e.folder})`),t.length===0){c("Not started. `snoutdata start` brings it up.");return}process.stdout.write(`${F([["SERVICE","STATE","HEALTH"],...t.map(o=>[o.service,o.state,o.health])])}
|
|
564
|
+
${r}`}async function Em(e,t,n={}){Z(t);let r=(await e.list()).find(o=>o.ref===t);return r&&r.status!=="stopped"&&await e.stop(t,Ka),Cr(e,{ref:t,port:0,ownerPassword:"",adminPassword:""},n)}async function $m(e,t,n){Z(t),await e.remove(t),n&&(await e.removeData(t),fn.delete(t))}async function Cr(e,t,n={}){Z(t.ref);let r=(await e.list()).find(a=>a.ref===t.ref),o=r?.status==="running",s=n.hasData??gn;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?Ii(t):null}}var Nr,B,Ir,We,lm,Ai,Mr,$e,dm,Pi,z,fm,pm,Ri,fn,Ni,Ui=D(()=>{"use strict";Wa();pt();Va();yr();xr();he();Nr=process.env.SNOUTPOD_ENGINE??"podman";B=cm(),Ir=18,We=[17,18],lm="snoutpod/postgres",Ai="ghcr.io/snoutdata/snoutpod-postgres";Mr=bt(null).name,$e=process.env.SNOUTPOD_LOCAL_NETWORK??"snoutpod-local",dm=54322,Pi=512,z=Ja();fm="cd packages/snoutpod && npm run image:build",pm=`${Ai}:${Ir}`;Ri=20*60*1e3;fn=new Map;Ni="this podman cannot blackhole a route, so the local tier cannot isolate its containers"});te();var bn=[{name:"init",summary:"Create or link a project for this folder, and print its DATABASE_URL or write it to .env",flags:["--name","--region","--env","--ref"]},{name:"login",summary:"Sign in through a browser, with --sso through your company's identity provider, or with --device by typing a code into one anywhere",flags:["--email","--provider","--sso","--domain","--no-browser","--device","--timeout"]},{name:"logout",summary:"Forget the stored session",flags:[]},{name:"whoami",summary:"Who this credential belongs to, and whether it is the account login --email asked for",flags:[]},{name:"upgrade",summary:"Install the newest CLI the way this one was installed (binary or npm), checksum verified",flags:["--check"]},{name:"tokens list",summary:"Long-lived access tokens on this account",flags:[]},{name:"tokens create",summary:"Mint an sdt_ token for CI or an agent",flags:["--name","--expires","--project"]},{name:"tokens revoke",summary:"Revoke one by id or prefix",flags:[]},{name:"projects list",summary:"Every project on this account",flags:[]},{name:"projects create",summary:"Make a hosted database, and print its ref",flags:["--name","--region","--team","--no-wait","--timeout","--show-url"]},{name:"projects pause",summary:"Stop a project, and wait until it is paused",flags:["--ref","--no-wait","--timeout"]},{name:"projects resume",summary:"Start a paused project, and wait until it is ready",flags:["--ref","--no-wait","--timeout"]},{name:"projects delete",summary:"Delete a project, and wait until it is gone",flags:["--ref","--no-wait","--timeout"]},{name:"projects show",summary:"One project: state, products (Realtime included), functions, secrets, domains",flags:["--ref"]},{name:"products",summary:"Auth, storage, the data API and push: on or off. Realtime is always on, and listed",flags:["--ref"]},{name:"products enable",summary:"Turn auth, storage, data-api or push on",flags:["--ref"]},{name:"products disable",summary:"Turn auth, storage, data-api or push off",flags:["--ref"]},{name:"push credentials",summary:"Push keys: what is set for APNs, FCM and Web Push (never the keys)",flags:["--ref"]},{name:"push credentials set",summary:"Set the APNs key (a .p8) or the FCM service account, checked before it is stored",flags:["--ref","--p8","--key-id","--team-id","--topic","--environment","--file"]},{name:"push credentials remove",summary:"Remove the APNs or FCM credentials",flags:["--ref"]},{name:"auth",summary:"Auth settings: Google sign-in and redirect addresses",flags:["--ref"]},{name:"auth google",summary:"Sign in with Google, with your own client (secret on stdin)",flags:["--ref","--client-id","--stdin"]},{name:"auth anonymous",summary:"Guest sign-in on or off: signInAnonymously() gives a browser a session with no email or password",flags:["--ref"]},{name:"auth redirects",summary:"The site URL and the addresses a sign-in may return to",flags:["--ref","--site-url","--allow"]},{name:"auth templates",summary:"The five auth emails, ours or your own",flags:["--ref"]},{name:"auth template",summary:"Use your own subject and HTML for one, or reset it",flags:["--ref","--file","--subject"]},{name:"realtime inspect",summary:"The channels open now: each client, its presence, when it was last heard, and the last minute of messages",flags:["--channel","--watch","--ref"]},{name:"realtime logs",summary:"The connection log: connects, joins, leaves and disconnects, with the reason each ended",flags:["--since","--channel","--ref"]},{name:"domains",summary:"Your own domains in front of the project API",flags:["--ref"]},{name:"domains add",summary:"Add one, and print the DNS records to publish",flags:["--ref"]},{name:"domains verify",summary:"Check its records and verify it",flags:["--ref"]},{name:"domains remove",summary:"Stop serving it",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 size",summary:"Its memory and workers, within the plan",flags:["--ref","--memory","--concurrency","--reset"]},{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 access",summary:"Who signs in to the database as themselves with OAuth (Postgres 18), at which level, as which role",flags:["--ref"]},{name:"db access grant",summary:"Let a person sign in to the database as themselves, and print their connection string",flags:["--ref","--level"]},{name:"db access revoke",summary:"Take it away, by email or role; a token already issued can work until it expires (up to 1 hour)",flags:["--ref"]},{name:"db export",summary:"Take or download a dump",flags:["--ref","--out","--status","--timeout"]},{name:"db push",summary:"Apply migrations from a folder",flags:["--dir","--dry-run","--out-of-order"]},{name:"db restore",summary:"Put a dump into a project, or rewind it to a moment into a new project",flags:["--file","--force","--at","--name","--window","--ref"]},{name:"mcp",summary:"Serve these operations as tools over stdio",flags:["--allow-delete"]}],qr={"--ref":"The project. Defaults to SNOUTDATA_PROJECT, then .snoutdata/project.json here or in a parent","--name":"The project's name","tokens create --name":"What the token is for and where it lives, shown in tokens list","db restore --name":"The name of the new project the restore makes","--region":"Where the project runs. Defaults to the nearest","--env":"Write DATABASE_URL into .env in this folder, and add .env to .gitignore since it holds the password. Without it the URL is printed","--email":"The account to sign in as. The browser offers it, and a different account is reported at once and by whoami","--provider":"google (the default) or github","--sso":"Sign in with your company's identity provider","--domain":"With --sso: your work email or company domain. Only the domain is sent","--no-browser":"Print the sign-in URL instead of opening a browser","--device":"Sign in by typing a code into a browser on any machine: SSH, containers, agents","--timeout":"Seconds to wait before giving up","--check":"Say whether a newer version exists, and install nothing","--expires":"Days until the token stops working. Without it, it works until revoked","--project":"Limit the token to one project ref","--team":"Create it in a team (name or id), shared with its members","--no-wait":"Return as soon as it is asked for, without waiting for it to be true","--show-url":"Also print the connection string, which holds the password. Otherwise db url prints it when wanted","--p8":"The APNs .p8 key file (a path, never the key itself)","--key-id":"The APNs key id","--team-id":"Your Apple developer team id","--topic":"The app's bundle id","--environment":"production or sandbox","--file":"A file to read","push credentials set --file":"The FCM service-account JSON","auth template --file":"The email's HTML body","db restore --file":"The dump to restore (from db export or pg_dump -Fc)","--client-id":"Your Google OAuth client id; the secret is read from stdin with --stdin","--stdin":"Read the secret value from a pipe, so it is not in shell history","--site-url":"Where sign-in emails send people by default","--allow":"Comma-separated addresses a sign-in may return to","--subject":"The email's subject line","--dir":"The folder to read from","db push --dir":"The migrations folder (default ./migrations)","start --dir":"The migrations folder (default ./migrations)","functions deploy --dir":"The function's folder (default ./functions/<name>)","--entrypoint":"The file the runtime starts, when it is not index.ts","--no-verify-jwt":"Let anybody with the URL call it (webhooks). By default a caller needs a project key or a user session","--memory":"Memory per worker, in MB, within the plan","--concurrency":"How many workers it may run at once, within the plan","--reset":"Back to the plan's default size","--force":"Do it although it cannot be undone","--days":"How many days of history","--history":"Print the day-by-day table too","--port":"The port the local database listens on (default 54322)","--no-migrations":"Start the database and apply nothing","--out-of-order":"Apply a new migration that sorts before one already applied","--db-url":"Read a postgres:// database instead of a project","--local":"Read the database snoutdata start is running","--schema":"Comma-separated schemas (default public)","--out":"Write to this file instead of stdout","db export --out":"Where to save the dump","--status":"Show the last export without taking another","--dry-run":"Show what would run, and change nothing","--at":"The moment to restore to (an ISO time), into a NEW project","--window":"How far back a point-in-time restore can go","--allow-delete":"Let the agent's delete_project actually delete","--channel":"Only this channel","--since":"How far back, like 10m, 2h or 1d (default 1h)","--watch":"Keep printing what changes until interrupted","--level":"full (everything the project password can do) or read (reads the project's own tables, never the auth or storage schemas, and writes nothing). Default read"};function Sn(e,t){return qr[`${e} ${t}`]??qr[t]??""}W();Q();q();import{spawn as ol}from"node:child_process";function sl(e){return`snout-${e}`}function Ue(e,t,n){return new Promise((r,o)=>{let s=ol("docker",["compose","-p",sl(e.ref),"--project-directory",e.folder,...t],{cwd:e.folder,stdio:["ignore","pipe","pipe"]}),a="",l="";s.stdout.on("data",i=>{a+=String(i)}),s.stderr.on("data",i=>{if(l+=String(i),n)for(let u of String(i).split(/\r?\n/).filter(Boolean))c(b(` ${u.trim()}`))}),s.on("error",i=>{if(i.code==="ENOENT"){o(new N("tool-missing","docker is not installed, or not on PATH. A local project runs in Docker (Docker Desktop on Windows and macOS)."));return}o(new N("failed",String(i)))}),s.on("close",i=>r({code:i??0,out:a,err:l}))})}async function al(e){let t=await Ue(e,["ps","-a","--format","json"],!1);if(t.code!==0)throw new N("failed",t.err.trim()||"docker compose ps failed. Is Docker running?");let n=t.out.trim();return(n.startsWith("[")?JSON.parse(n):n.split(/\r?\n/).filter(Boolean).map(o=>JSON.parse(o))).map(o=>({service:o.Service??"?",state:o.State==="exited"&&o.ExitCode===0?"done":o.State??"?",health:o.Health??""})).sort((o,s)=>o.service.localeCompare(s.service))}async function To(e){let t=await al(e),n=t.filter(o=>o.state==="running").length,r=t.filter(o=>o.state!=="done").length;return g({ref:e.ref,local:e.folder,api:e.apiUrl,dbPort:e.dbPort,running:n,services:t},()=>{if(c(`${v(e.name??e.ref)} (local, ${e.folder})`),t.length===0){c("Not started. `snoutdata start` brings it up.");return}process.stdout.write(`${F([["SERVICE","STATE","HEALTH"],...t.map(o=>[o.service,o.state,o.health])])}
|
|
565
565
|
`),c(`${n} of ${r} services running. API ${e.apiUrl}, database 127.0.0.1:${e.dbPort}.`)}),0}async function Oo(e){c(`Starting ${e.name??e.ref} (local)\u2026`);let t=await Ue(e,["up","-d","--wait","--wait-timeout","600"],!0);if(t.code!==0)throw new N("failed",`docker compose up failed: ${t.err.trim().split(/\r?\n/).slice(-3).join(" ")}`);return g({ref:e.ref,api:e.apiUrl,dbPort:e.dbPort,started:!0},()=>{c(`Running. API ${e.apiUrl}, database 127.0.0.1:${e.dbPort}. \`snoutdata db url\` prints the connection string.`)}),0}async function No(e){let t=await Ue(e,["stop"],!0);if(t.code!==0)throw new N("failed",`docker compose stop failed: ${t.err.trim().split(/\r?\n/).slice(-3).join(" ")}`);return g({ref:e.ref,stopped:!0},()=>c(`Stopped ${e.name??e.ref}. Its data is kept; \`snoutdata start\` brings it back.`)),0}function Do(e){g({ref:e.ref,name:e.name,local:e.folder,api:e.apiUrl,dbPort:e.dbPort},()=>{process.stdout.write(`${F([["name",e.name??"(not set up by Studio)"],["ref",e.ref],["where",`local, ${e.folder}`],["api",e.apiUrl],["database",`127.0.0.1:${e.dbPort}`]])}
|
|
566
566
|
`)})}Qe();import{cpSync as Co,existsSync as ct,readdirSync as pl,readFileSync as jo,rmSync as Lo,statSync as gl,writeFileSync as Uo}from"node:fs";import{join as re,resolve as Ht}from"node:path";var il=/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/;function cl(e){return il.test(e)}function Re(e){if(!cl(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 ll=/^[A-Za-z0-9_][A-Za-z0-9._-]*$/;function dl(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!==".."&&ll.test(n))}var Mn=2*1024*1024,Io=200;function Mo(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>Io)throw new Error(`a function may hold ${Io} files and this one has ${e.files.length}`);let n=new Set;for(let a of e.files){if(!dl(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,l)=>a.path<l.path?-1:a.path>l.path?1:0),o={snoutfn:1,entrypoint:t,files:r},s=ul(o);if(s.length>Mn)throw new Error(`this function is ${Math.round(s.length/1024)} KB and the limit is ${Mn/1024} KB. A function is source code; put large files in Storage.`);return o}function ul(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 ml=/^[A-Za-z_][A-Za-z0-9_]{0,63}$/;var fl=["SNOUTDATA_","SNOUT_"];function it(e){if(!ml.test(e))return`${JSON.stringify(e)} is not an environment variable name: letters, digits and underscores, not starting with a digit.`;let t=fl.find(n=>e.startsWith(n));return t?`names beginning with ${t} are set by the platform, so ${e} cannot be one of yours.`:null}var dp=64*1024;Q();te();q();function Fo(e){let t=re(e.folder,"functions");return ct(t)?pl(t).filter(n=>!n.startsWith(".")&&!n.startsWith("_")&&gl(re(t,n)).isDirectory()).sort():[]}async function Gt(e){c(b(" docker compose run --rm functions-deploy"));let t=await Ue(e,["run","--rm","functions-deploy"],!0);if(t.code!==0)throw new N("failed",`functions-deploy failed: ${t.err.trim().split(/\r?\n/).slice(-3).join(" ")}`)}function Bo(e,t,n){let r=re(e.folder,".env"),o=jo(r,"utf8"),s=($t(o).get("FUNCTIONS_NO_VERIFY_JWT")??"").split(",").map(u=>u.trim()).filter(Boolean),a=n?[...new Set([...s,t])]:s.filter(u=>u!==t);if(a.join(",")===s.join(","))return!1;let l=`FUNCTIONS_NO_VERIFY_JWT=${a.join(",")}`,i=/^FUNCTIONS_NO_VERIFY_JWT=.*$/m;return Uo(r,i.test(o)?o.replace(i,l):`${o.replace(/\n?$/,`
|
|
567
567
|
`)}${l}
|
|
@@ -959,6 +959,10 @@ ${o.err.trim()}`),c(" ran seed.sql"),!0}var St="_snoutdata_migrations",Hm=`crea
|
|
|
959
959
|
snoutdata db url [--ref R] print a connection string
|
|
960
960
|
snoutdata db psql [--ref R] [-- ...] open psql, with no password typed
|
|
961
961
|
snoutdata db reset-password [--ref R]
|
|
962
|
+
snoutdata db access [list] [--ref R] who signs in to the database as themselves
|
|
963
|
+
(OAuth, Postgres 18), and as which role
|
|
964
|
+
snoutdata db access grant EMAIL [--level full|read] a teammate, or you; read by default
|
|
965
|
+
snoutdata db access revoke EMAIL|ROLE take it away
|
|
962
966
|
snoutdata db export [--ref R] [--out FILE] take a copy, and download it
|
|
963
967
|
snoutdata db export --status [--ref R] what the last copy is doing
|
|
964
968
|
snoutdata db push [--dir migrations] [--dry-run] run the .sql files, once each
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snoutdata",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "SnoutData Cloud projects from a terminal or an agent. Create a project with a Postgres database, get a DATABASE_URL, run migrations, and serve the same operations to an AI agent over MCP.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SnoutData",
|