@turbobase/cli 1.1.13 → 1.1.15

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.
@@ -1759,7 +1759,7 @@ ${r}`}var n0=JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",comman
1759
1759
  `))}catch(c){if(c instanceof _e&&c.status===409){Ee(T.red("turbobase: the pending queue changed while you were reading the plan \u2014 nothing was written.")),Ee(`re-run \`${Wo} kb push\` to review the current plan.`),process.exitCode=2;return}qg(c),Ee(T.red(`turbobase: ${c instanceof Error?c.message:String(c)}`)),process.exitCode=2}}Xn();function v0(t){return t.diff??!!t.dryRun}function x0(t,e){let r=[];return t.forEach((n,o)=>{let s=n.action==="new_rule"?T.green("new rule"):T.yellow("amend rule"),i=n.targetPublished?` ${T.yellow("[currently live]")}`:"";r.push(`${o+1}. ${s} ${T.bold(n.slug)}${i}`),r.push(` ${n.name}`);for(let a of n.entries)r.push(` ${T.dim("\xB7")} ${T.dim(a.app_slug)} ${a.title}`);if(e.diff){r.push("");for(let a of n.bodyMarkdown.split(`
1760
1760
  `))r.push(` ${T.dim("|")} ${a}`)}r.push("")}),r}function $0(t){let e=t.reduce((o,s)=>o+s.entries.length,0),r=t.filter(o=>o.targetPublished).length,n=[`${e} finding(s) \u2192 ${t.length} rule(s)`];return r>0&&n.push(T.yellow(`${r} amend a rule that is currently live`)),n.join(" \xB7 ")}async function Vg(t,e=r=>new te(r)){t.limit!==void 0&&(!Number.isFinite(t.limit)||t.limit<1)&&k("--limit must be a positive integer.");let r=await oe(t.baseUrl),n=e(r),o={...t.app?{app:t.app}:{},...t.limit!==void 0?{limit:t.limit}:{}},s;try{s=await Se("reading the pending rules",()=>n.rulesPending(o))}catch(a){Ae(a,"extensions:deploy","read the pending rules"),k(`could not read the pending queue: ${a instanceof Error?a.message:String(a)}`)}if(s.items.length===0){Ee(s.pendingTotal===0?"nothing pending \u2014 no findings are waiting under `## Agent rules`.":`nothing matched \u2014 ${s.pendingTotal} pending rule finding(s) exist but not under these filters.`);return}if(S(x0(s.items,{diff:v0(t)}).join(`
1761
1761
  `)),S($0(s.items)),t.dryRun){Ee(T.dim("dry run \u2014 nothing was written."));return}let i=s.items.reduce((a,c)=>a+c.entries.length,0);if(!t.yes){if(!_r()){Ee(T.red("turbobase: refusing to write without confirmation on a non-interactive terminal. Re-run with --yes (or --dry-run to review the plan).")),process.exitCode=1;return}if(await vr(`Push ${i} finding(s) as ${s.items.length} draft rule(s)?`,[{value:"yes",label:"Yes \u2014 write them as draft rules",description:"they land DISABLED; an admin publishes them before anything is injected"},{value:"no",label:"No \u2014 leave the queue untouched"}],{defaultIndex:1}).catch(()=>"no")!=="yes"){Ee("aborted \u2014 nothing was written."),process.exitCode=1;return}}try{await Se("writing the rules",()=>n.rulesPush(s.planToken,o))}catch(a){a instanceof _e&&a.status===409&&k("the pending queue changed while you were reading the plan \u2014 nothing was written. Re-run to see the new plan."),k(`push failed: ${a instanceof Error?a.message:String(a)}`)}S(T.green(`wrote ${s.items.length} draft rule(s).`)),Ee(T.dim("They are DRAFT and DISABLED \u2014 nothing is injected yet. An admin reviews and publishes them under Settings \u203A Skills."))}import{promises as Hg}from"node:fs";import{join as k0,relative as S0,resolve as T0,sep as E0}from"node:path";var R0=new Set(["node_modules","dist","build","coverage",".git",".claude",".turbobase",".next",".svelte-kit",".vercel",".cache"]),P0=new Set([".mcp.json",".env"]);function A0(t){return P0.has(t)||t.startsWith(".env.")&&t!==".env.example"}function I0(t){return Buffer.compare(Buffer.from(t.toString("utf8"),"utf8"),t)===0}function O0(t){return t.includes(0)}async function Kg(t){let e=T0(t),r={},n=[],o=[],s=[];async function i(a){let c=await Hg.readdir(a,{withFileTypes:!0});for(let l of c){let u=k0(a,l.name);if(l.isDirectory()){if(R0.has(l.name))continue;await i(u);continue}if(!l.isFile())continue;let d=S0(e,u).split(E0).join("/");if(A0(l.name)){o.push(d);continue}let b=await Hg.readFile(u);if(!I0(b)){n.push(d);continue}O0(b)&&s.push(d),r[d]=b.toString("utf8")}}return await i(e),{files:r,skipped:n,secrets:o,unstorable:s}}import{randomBytes as C0}from"node:crypto";var z0="/_tb/app-assets",N0=16,j0=/^[0-9a-f]{32}$/;function Gg(){return C0(N0).toString("hex")}function L0(t){return j0.test(t)}function Wg(t){if(!L0(t))throw new Error(`not a build id: ${t}`);return`${z0}/${t}/`}var Jg=7;import Ci from"node:fs/promises";import Yo from"node:path";var Il={esbuild:"0.28.1","isolated-vm":"6.1.2"},Ol="@turbobase/isolated-vm-prebuilds";function Cl(t,e){return`${t}-${e}`}function zl(t=process.versions){return`abi${t.modules}`}var Yg=["darwin-x64","win32-arm64"];async function Xg(t){let e=t.platform??process.platform,r=t.arch??process.arch,n=Cl(e,r),o=Yo.join(t.home,"node_modules"),s=Yo.join(o,...Ol.split("/"),"prebuilds",n),i=Yo.join(o,"isolated-vm","prebuilds",n),a;try{a=await Ci.readdir(s)}catch{return[]}let c=[];await Ci.mkdir(i,{recursive:!0});for(let l of a){if(!l.endsWith(".node"))continue;let u=Yo.join(i,l);try{await Ci.copyFile(Yo.join(s,l),u,Ci.constants?.COPYFILE_EXCL??1),c.push(l)}catch{}}return c}import{homedir as M0}from"node:os";import{join as Qg}from"node:path";import{promises as Nl}from"node:fs";function ty(){let t=process.env.TURBOBASE_HOME??Qg(M0(),".turbobase");return{dir:t,file:Qg(t,"sandboxes.json")}}var D0={s:1e3,m:6e4,h:36e5,d:864e5};function U0(t){let e=/^\s*(\d+)\s*([smhd])\s*$/.exec(t);if(!e)throw new Error(`invalid --ttl '${t}': expected <number><unit> where unit is s|m|h|d (e.g. 72h, 30m, 7d).`);let r=Number(e[1]);if(!Number.isFinite(r)||r<=0)throw new Error(`invalid --ttl '${t}': duration must be a positive integer.`);return r*D0[e[2]]}function Z0(t,e=Date.now()){return new Date(e+U0(t)).toISOString()}function q0(t,e=Date.now()){let r=Date.parse(t);return Number.isNaN(r)?!0:r<=e}function F0(t){let e=/^\s*(\d+(?:\.\d+)?)\s*%?\s*$/.exec(t);if(!e)throw new Error(`invalid --sample '${t}': expected a percent like 2% (or a bare number).`);let r=Number(e[1]);if(!Number.isFinite(r)||r<0||r>100)throw new Error(`invalid --sample '${t}': percent must be between 0 and 100.`);return r/100}function B0(t,e){if(e>=1)return[...t];if(e<=0||t.length===0)return[];let r=Math.max(1,Math.round(1/e)),n=[];for(let o=0;o<t.length;o+=r)n.push(t[o]);return n}var V0="[REDACTED]",Xo=class extends Error{constructor(e){super(e),this.name="SandboxDataCopyUnavailableError"}},jl=class extends Error{table;column;constructor(e,r,n){super(`Refusing to copy ${e}.${r} into a sandbox: ${n}. A sandbox must never hold un-redacted PII \u2014 classify the column 'safe' (non-PII) or let redaction mask it, then retry.`),this.name="UnredactedPiiError",this.table=e,this.column=r}};function H0(t,e,r){let n={};for(let[o,s]of Object.entries(e)){let i=r[o];if(i===void 0)throw new jl(t,o,"column has no PII classification (unknown = unsafe)");i==="pii"?n[o]=s==null?s:V0:n[o]=s}return n}function K0(t,e,r){return e.map(n=>H0(t,n,r))}var ey=new Set(["localhost","127.0.0.1","::1","0.0.0.0"]),G0=["oraclecloud.com","us-ashburn-1","amazonaws.com","rds.","supabase.co","turbobase.app","turbobase.io"];function W0(t){let e=t.trim();try{let o=new URL(e);if(o.hostname)return o.hostname.toLowerCase()}catch{}let r=e.replace(/^[a-z]+:\/\//i,""),n=(r.split("/")[0]??r).split("@").pop()??r;return(n.split(":")[0]??n).toLowerCase()}function Ll(t){if(!t||!t.trim())throw new Error("local-only guard: empty database target \u2014 refusing to proceed.");let e=t.toLowerCase();for(let n of G0)if(e.includes(n))throw new Error(`local-only guard: target '${t}' looks like a cloud/prod context (matched '${n}'). \`turbobase sandbox create\` is LOCAL-ONLY and will never touch a cloud database.`);let r=W0(t);if(!ey.has(r))throw new Error(`local-only guard: target host '${r}' is not a recognised local Postgres (${[...ey].join(", ")}). \`turbobase sandbox create\` is LOCAL-ONLY.`)}function J0(t,e=Y0){return`sbx-${t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"tenant"}-${e()}`}function Y0(){return Math.floor(Math.random()*16777215).toString(16).padStart(6,"0")}function X0(t){return`turbobase_${t.replace(/[^a-z0-9]+/gi,"_").toLowerCase()}`}function Q0(t,e){return`${(e??process.env.TURBOBASE_SANDBOX_BASE_URL??"http://localhost:5173").replace(/\/+$/,"")}/?sandbox=${encodeURIComponent(t)}`}async function Ml(){let{file:t}=ty();try{let e=await Nl.readFile(t,"utf8"),r=JSON.parse(e);return Array.isArray(r)?r:[]}catch(e){if(e.code==="ENOENT")return[];throw e}}async function ry(t){let{dir:e,file:r}=ty();await Nl.mkdir(e,{recursive:!0}),await Nl.writeFile(r,`${JSON.stringify(t,null,2)}
1762
- `,{mode:384})}async function eE(t){let e=await Ml();e.push(t),await ry(e)}async function ny(t){let e=await Ml(),r=e.findIndex(o=>o.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await ry(e),n??null}async function zi(){return Ml()}function Dl(t,e=Date.now()){let r=[],n=[];for(let o of t)q0(o.expiresAt,e)?n.push(o):r.push(o);return{active:r,expired:n}}async function oy(t,e){Ll(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new Xo(r);let n=F0(e.sample),o=e.now??Date.now(),s=(e.nameGen??(l=>J0(l)))(e.fromTenant),i=X0(s);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let l of a){let u=B0(l.rows,n),d=K0(l.table,u,l.classification);d.length>0&&await t.copyRows(i,l.table,d)}let c={name:s,fromTenant:e.fromTenant,database:i,createdAt:new Date(o).toISOString(),expiresAt:Z0(e.ttl,o),sampleFraction:n,url:Q0(s,e.baseUrl)};return await eE(c),c}async function sy(t,e){Ll(t.target());let n=(await zi()).find(o=>o.name===e);return n?(await t.dropDatabase(n.database),await ny(e),n):null}async function Ul(t,e=Date.now()){Ll(t.target());let r=await zi(),{expired:n}=Dl(r,e),o=[];for(let s of n){try{await t.dropDatabase(s.database)}catch{continue}await ny(s.name),o.push(s)}return o}var Qo={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","data:read"],defaultTtlDays:90,mintable:!0,label:"Personal access token"},catalog:{prefix:"tb_cat_",defaultScopes:["catalog:read"],allowedScopes:["catalog:read"],defaultTtlDays:null,mintable:!0,label:"AI / catalog token"}},DD=Object.keys(Qo).filter(t=>Qo[t].mintable);var qD=Qo.pat.prefix,tE=Object.entries(Qo).map(([t,e])=>[e.prefix,t]),rE=/^[A-Za-z0-9_-]{20,}$/;function nE(t){if(typeof t!="string")return null;for(let[e,r]of tE)if(t.startsWith(e)&&rE.test(t.slice(e.length)))return r;return null}function iy(t){return nE(t)==="pat"}var es="bundle.json",Ni="turbobase";function cy(t){let e;try{e=JSON.parse(t)}catch(c){throw new Error(`bundle.json is not valid JSON: ${c instanceof Error?c.message:String(c)}`)}if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("bundle.json must be a JSON object");let r=e,n=typeof r.version=="number"?r.version:0;if(n!==1)throw new Error(`bundle.json version ${n} is unsupported (expected 1)`);let o=c=>Array.isArray(c)?c:[],s=r.sync??{},i=r.guardrails??{},a={version:1,...typeof r.tenant=="string"?{tenant:r.tenant}:{},hooks:o(r.hooks),conduits:o(r.conduits),sync:{fields:o(s.fields),tables:o(s.tables)},guardrails:{validationHooks:o(i.validationHooks),fieldRules:o(i.fieldRules),conduits:o(i.conduits)},schedules:o(r.schedules)};return oE(a)}function oE(t){let e=r=>(n,o)=>r(n)<r(o)?-1:r(n)>r(o)?1:0;return{version:1,...t.tenant?{tenant:t.tenant}:{},hooks:[...t.hooks].sort(e(r=>r.id)),conduits:[...t.conduits].sort(e(r=>r.id)),sync:{fields:[...t.sync.fields].sort(e(ay)),tables:[...t.sync.tables].sort(e(r=>r.sourceRecordType))},guardrails:{validationHooks:[...t.guardrails.validationHooks].sort(),fieldRules:[...t.guardrails.fieldRules].sort(e(ay)),conduits:[...t.guardrails.conduits].sort(e(r=>r.conduitId))},schedules:[...t.schedules].sort(e(r=>r.name))}}function ay(t){return`${t.sourceSystem}::${t.recordType}::${t.fieldName}`}function ly(t,e){let r=(e??"").trim();if(!r)return{ok:!1,reason:"target instance slug is unknown \u2014 cannot confirm a promote"};let n=(t??"").trim();return n?n!==r?{ok:!1,reason:`--confirm "${n}" does not match the target instance "${r}" \u2014 refusing to apply`}:{ok:!0}:{ok:!1,reason:`--confirm <slug> is required for promote (expected "${r}")`}}async function Ej(t,e){try{let r=await fetch(`${t.replace(/\/+$/,"")}/auth/pat-exchange`,{method:"POST",headers:{Authorization:`Bearer ${e}`}});if(!r.ok)return null;let n=await r.json();return typeof n.token=="string"&&n.token.length>0?n.token:null}catch{return null}}async function Rj(t){let e=await Nr();e.length===0&&k("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),_r()||k("this command needs an explicit <base-url> in a non-interactive shell.");try{return await vr(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{k("cancelled.")}}async function Pj(t,e){let r;try{r=await fetch(`${t}/v1/catalog/records`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json"},signal:AbortSignal.timeout(8e3)})}catch(o){return{ok:!1,kind:"unreachable",detail:o instanceof Error?o.message:String(o)}}let n=(r.headers.get("content-type")??"").includes("application/json");return r.status===200||r.status===403?{ok:!0}:r.status===401?{ok:!1,kind:"bad-token",detail:`HTTP ${r.status}`}:n?{ok:!0}:{ok:!1,kind:"wrong-host",detail:`HTTP ${r.status}`}}async function Aj(t,e,r){let n=await og({baseUrl:t,token:e}),o=(await Nr()).length-1;if(S(`signed in to ${n.baseUrl} \u2014 now the active instance`+(o>0?` (${o} other${o===1?"":"s"} stored; commands default here).`:".")),!r)return;let s=await Pj(n.baseUrl,e);s.ok||(s.kind==="wrong-host"?(Ee(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${s.detail}.`),Ee(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),Ee(" The token is stored, but commands will fail against this base-url until it points at the API.")):s.kind==="bad-token"?Ee(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):Ee(`warning: could not reach ${n.baseUrl} to verify the token (${s.detail}). Stored anyway.`))}async function Ij(t){let e=await Ko(t),r=c=>{let l=new RegExp(`${c}\\s*:\\s*(['"\`])([^'"\`]+)\\1`).exec(e);return l?l[2]:void 0},n=/\bdefineConduit\s*[<(]/.test(e),o=r("id");if(n){let c=r("on"),l=r("recordType"),u=r("source");return(!o||!l||!c||!u)&&k(`could not parse id/on/from.source/from.recordType from ${t} \u2014 is it a defineConduit({...}) file?`),{id:o,recordType:l,event:c,source:u,sourceCode:e,extensionKind:"conduit"}}let s=r("recordType"),i=r("event"),a=r("source")??"standalone";return(!o||!s||!i)&&k(`could not parse id/recordType/event from ${t} \u2014 is it a defineHook({...}) file?`),{id:o,recordType:s,event:i,source:a,sourceCode:e,extensionKind:"hook"}}async function zo(t,e,r,n){try{return await me.access(t),"skipped"}catch{}return await me.mkdir(Co(t),{recursive:!0}),await me.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await me.chmod(t,n),"wrote"}async function tk(t,e){if(rk(t)!==".gitignore")return zo(t,e.content,e.executable,e.mode);let r=await me.readFile(t,"utf8").catch(()=>null);if(r===null)return zo(t,e.content,e.executable,e.mode);let n=Lg(r);return n===null?"skipped":(await me.writeFile(t,n,"utf8"),"merged")}function rm(t){if(t!==void 0){if(t==="junit"||t==="json")return t;k(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function Oj(t){return{name:"turbobase ext test",cases:t.map(r=>{if(r.pass)return{name:r.name,status:"passed"};let n=r.error?"error":r.rejection?.code??"mismatch",o=r.error?`error: ${r.error}`:r.observed!==r.expected?`expected ${r.expected}, observed ${r.observed}`+(r.rejection?` (rejected: ${r.rejection.code??"rejected"}${r.rejection.message?` \u2014 ${r.rejection.message}`:""})`:""):`rejected: ${r.rejection?.code??"rejected"}${r.rejection?.message?` \u2014 ${r.rejection.message}`:""}`;return{name:r.name,status:"failed",detail:o,type:n}})}}function Cj(t,e){let r=e.warnings.length===0?[{name:`${t} (${e.kind})`,status:"passed"}]:e.warnings.map(n=>({name:`${t}: ${n.rule}`,status:"failed",type:n.rule,detail:`${n.message} \u2192 fix: ${n.fix}`}));return{name:`turbobase ext lint ${t}`,cases:r}}function zj(t){let e=[];if(t.error)return e.push({name:`replay ${t.fixtureName}`,status:"failed",type:"error",detail:t.error}),{name:`turbobase ext replay ${t.fixtureName}`,cases:e};for(let r of t.guards)e.push(r.status==="failed"?{name:`guard: ${r.name}`,status:"failed",type:r.name,detail:r.detail}:{name:`guard: ${r.name}`,status:"passed",detail:r.status==="skipped-offline"?`skipped-offline: ${r.detail}`:void 0});return t.whenPassed&&t.applyBranch&&e.push({name:`apply-branch: ${t.applyBranch}`,status:"passed",detail:`${t.masterAction??""} (${t.wouldWriteBindings.length} binding(s) would be written)`.trim()}),{name:`turbobase ext replay ${t.fixtureName}`,cases:e}}function cm({includeInternal:t=!0}={}){let e=new Tm;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.1.13").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",p=>p.command.parent?"":`${$l()}
1762
+ `,{mode:384})}async function eE(t){let e=await Ml();e.push(t),await ry(e)}async function ny(t){let e=await Ml(),r=e.findIndex(o=>o.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await ry(e),n??null}async function zi(){return Ml()}function Dl(t,e=Date.now()){let r=[],n=[];for(let o of t)q0(o.expiresAt,e)?n.push(o):r.push(o);return{active:r,expired:n}}async function oy(t,e){Ll(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new Xo(r);let n=F0(e.sample),o=e.now??Date.now(),s=(e.nameGen??(l=>J0(l)))(e.fromTenant),i=X0(s);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let l of a){let u=B0(l.rows,n),d=K0(l.table,u,l.classification);d.length>0&&await t.copyRows(i,l.table,d)}let c={name:s,fromTenant:e.fromTenant,database:i,createdAt:new Date(o).toISOString(),expiresAt:Z0(e.ttl,o),sampleFraction:n,url:Q0(s,e.baseUrl)};return await eE(c),c}async function sy(t,e){Ll(t.target());let n=(await zi()).find(o=>o.name===e);return n?(await t.dropDatabase(n.database),await ny(e),n):null}async function Ul(t,e=Date.now()){Ll(t.target());let r=await zi(),{expired:n}=Dl(r,e),o=[];for(let s of n){try{await t.dropDatabase(s.database)}catch{continue}await ny(s.name),o.push(s)}return o}var Qo={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","data:read"],defaultTtlDays:90,mintable:!0,label:"Personal access token"},catalog:{prefix:"tb_cat_",defaultScopes:["catalog:read"],allowedScopes:["catalog:read"],defaultTtlDays:null,mintable:!0,label:"AI / catalog token"}},DD=Object.keys(Qo).filter(t=>Qo[t].mintable);var qD=Qo.pat.prefix,tE=Object.entries(Qo).map(([t,e])=>[e.prefix,t]),rE=/^[A-Za-z0-9_-]{20,}$/;function nE(t){if(typeof t!="string")return null;for(let[e,r]of tE)if(t.startsWith(e)&&rE.test(t.slice(e.length)))return r;return null}function iy(t){return nE(t)==="pat"}var es="bundle.json",Ni="turbobase";function cy(t){let e;try{e=JSON.parse(t)}catch(c){throw new Error(`bundle.json is not valid JSON: ${c instanceof Error?c.message:String(c)}`)}if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("bundle.json must be a JSON object");let r=e,n=typeof r.version=="number"?r.version:0;if(n!==1)throw new Error(`bundle.json version ${n} is unsupported (expected 1)`);let o=c=>Array.isArray(c)?c:[],s=r.sync??{},i=r.guardrails??{},a={version:1,...typeof r.tenant=="string"?{tenant:r.tenant}:{},hooks:o(r.hooks),conduits:o(r.conduits),sync:{fields:o(s.fields),tables:o(s.tables)},guardrails:{validationHooks:o(i.validationHooks),fieldRules:o(i.fieldRules),conduits:o(i.conduits)},schedules:o(r.schedules)};return oE(a)}function oE(t){let e=r=>(n,o)=>r(n)<r(o)?-1:r(n)>r(o)?1:0;return{version:1,...t.tenant?{tenant:t.tenant}:{},hooks:[...t.hooks].sort(e(r=>r.id)),conduits:[...t.conduits].sort(e(r=>r.id)),sync:{fields:[...t.sync.fields].sort(e(ay)),tables:[...t.sync.tables].sort(e(r=>r.sourceRecordType))},guardrails:{validationHooks:[...t.guardrails.validationHooks].sort(),fieldRules:[...t.guardrails.fieldRules].sort(e(ay)),conduits:[...t.guardrails.conduits].sort(e(r=>r.conduitId))},schedules:[...t.schedules].sort(e(r=>r.name))}}function ay(t){return`${t.sourceSystem}::${t.recordType}::${t.fieldName}`}function ly(t,e){let r=(e??"").trim();if(!r)return{ok:!1,reason:"target instance slug is unknown \u2014 cannot confirm a promote"};let n=(t??"").trim();return n?n!==r?{ok:!1,reason:`--confirm "${n}" does not match the target instance "${r}" \u2014 refusing to apply`}:{ok:!0}:{ok:!1,reason:`--confirm <slug> is required for promote (expected "${r}")`}}async function Ej(t,e){try{let r=await fetch(`${t.replace(/\/+$/,"")}/auth/pat-exchange`,{method:"POST",headers:{Authorization:`Bearer ${e}`}});if(!r.ok)return null;let n=await r.json();return typeof n.token=="string"&&n.token.length>0?n.token:null}catch{return null}}async function Rj(t){let e=await Nr();e.length===0&&k("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),_r()||k("this command needs an explicit <base-url> in a non-interactive shell.");try{return await vr(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{k("cancelled.")}}async function Pj(t,e){let r;try{r=await fetch(`${t}/v1/catalog/records`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json"},signal:AbortSignal.timeout(8e3)})}catch(o){return{ok:!1,kind:"unreachable",detail:o instanceof Error?o.message:String(o)}}let n=(r.headers.get("content-type")??"").includes("application/json");return r.status===200||r.status===403?{ok:!0}:r.status===401?{ok:!1,kind:"bad-token",detail:`HTTP ${r.status}`}:n?{ok:!0}:{ok:!1,kind:"wrong-host",detail:`HTTP ${r.status}`}}async function Aj(t,e,r){let n=await og({baseUrl:t,token:e}),o=(await Nr()).length-1;if(S(`signed in to ${n.baseUrl} \u2014 now the active instance`+(o>0?` (${o} other${o===1?"":"s"} stored; commands default here).`:".")),!r)return;let s=await Pj(n.baseUrl,e);s.ok||(s.kind==="wrong-host"?(Ee(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${s.detail}.`),Ee(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),Ee(" The token is stored, but commands will fail against this base-url until it points at the API.")):s.kind==="bad-token"?Ee(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):Ee(`warning: could not reach ${n.baseUrl} to verify the token (${s.detail}). Stored anyway.`))}async function Ij(t){let e=await Ko(t),r=c=>{let l=new RegExp(`${c}\\s*:\\s*(['"\`])([^'"\`]+)\\1`).exec(e);return l?l[2]:void 0},n=/\bdefineConduit\s*[<(]/.test(e),o=r("id");if(n){let c=r("on"),l=r("recordType"),u=r("source");return(!o||!l||!c||!u)&&k(`could not parse id/on/from.source/from.recordType from ${t} \u2014 is it a defineConduit({...}) file?`),{id:o,recordType:l,event:c,source:u,sourceCode:e,extensionKind:"conduit"}}let s=r("recordType"),i=r("event"),a=r("source")??"standalone";return(!o||!s||!i)&&k(`could not parse id/recordType/event from ${t} \u2014 is it a defineHook({...}) file?`),{id:o,recordType:s,event:i,source:a,sourceCode:e,extensionKind:"hook"}}async function zo(t,e,r,n){try{return await me.access(t),"skipped"}catch{}return await me.mkdir(Co(t),{recursive:!0}),await me.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await me.chmod(t,n),"wrote"}async function tk(t,e){if(rk(t)!==".gitignore")return zo(t,e.content,e.executable,e.mode);let r=await me.readFile(t,"utf8").catch(()=>null);if(r===null)return zo(t,e.content,e.executable,e.mode);let n=Lg(r);return n===null?"skipped":(await me.writeFile(t,n,"utf8"),"merged")}function rm(t){if(t!==void 0){if(t==="junit"||t==="json")return t;k(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function Oj(t){return{name:"turbobase ext test",cases:t.map(r=>{if(r.pass)return{name:r.name,status:"passed"};let n=r.error?"error":r.rejection?.code??"mismatch",o=r.error?`error: ${r.error}`:r.observed!==r.expected?`expected ${r.expected}, observed ${r.observed}`+(r.rejection?` (rejected: ${r.rejection.code??"rejected"}${r.rejection.message?` \u2014 ${r.rejection.message}`:""})`:""):`rejected: ${r.rejection?.code??"rejected"}${r.rejection?.message?` \u2014 ${r.rejection.message}`:""}`;return{name:r.name,status:"failed",detail:o,type:n}})}}function Cj(t,e){let r=e.warnings.length===0?[{name:`${t} (${e.kind})`,status:"passed"}]:e.warnings.map(n=>({name:`${t}: ${n.rule}`,status:"failed",type:n.rule,detail:`${n.message} \u2192 fix: ${n.fix}`}));return{name:`turbobase ext lint ${t}`,cases:r}}function zj(t){let e=[];if(t.error)return e.push({name:`replay ${t.fixtureName}`,status:"failed",type:"error",detail:t.error}),{name:`turbobase ext replay ${t.fixtureName}`,cases:e};for(let r of t.guards)e.push(r.status==="failed"?{name:`guard: ${r.name}`,status:"failed",type:r.name,detail:r.detail}:{name:`guard: ${r.name}`,status:"passed",detail:r.status==="skipped-offline"?`skipped-offline: ${r.detail}`:void 0});return t.whenPassed&&t.applyBranch&&e.push({name:`apply-branch: ${t.applyBranch}`,status:"passed",detail:`${t.masterAction??""} (${t.wouldWriteBindings.length} binding(s) would be written)`.trim()}),{name:`turbobase ext replay ${t.fixtureName}`,cases:e}}function cm({includeInternal:t=!0}={}){let e=new Tm;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.1.15").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",p=>p.command.parent?"":`${$l()}
1763
1763
  `),e.hook("preAction",(p,m)=>{Dg(e.opts(),m.opts())});let r=e.command("auth").description("Sign in to an instance and manage stored credentials."),n=e.command("project").description("Set up the local project and the files an agent session reads."),o=e.command("ext").description("Author, test, ship, and observe extensions \u2014 hooks and conduits."),s=e.command("kb").description("Write into the instance's knowledge base."),i=e.command("skills").description("Write standing rules into the instance's agent skills.");r.command("login").description("Sign in to an instance by pasting a personal access token (PAT), stored in ~/.turbobase/credentials.").requiredOption("--base-url <url>","Instance base URL, e.g. https://acme.turbobase.app").option("--token <tb_pat_\u2026>","The PAT to store. Mint one in your app under Settings \u203A Access tokens.").option("--no-verify","Skip the post-login connectivity/token probe.").action(async p=>{let m=Ot(p.baseUrl);p.token||k(`auth login needs --token. Mint a PAT in your Turbobase app (Settings \u203A Access tokens), then:
1764
1764
  ${Wo} auth login --base-url ${m} --token tb_pat_\u2026`),iy(p.token)||k("--token does not look like a Turbobase PAT (tb_pat_\u2026)."),await Aj(m,p.token,p.verify)}),r.command("status").description("Show the active instance (the no-`--base-url` default) and every stored sign-in.").action(async()=>{let p=await Nr();if(p.length===0){S("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`.");return}let m=await or(),w=_=>!!m&&_.baseUrl===m.baseUrl;if(wt())S(wn(p.map(_=>[w(_)?T.green("*"):" ",T.bold(_.baseUrl),T.dim(_.updatedAt)]),{head:["","instance","updated"]}));else for(let _ of p)S(`${w(_)?"* ":" "}${_.baseUrl} (updated ${_.updatedAt})`);m||Ee("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}),r.command("use [base-url]").description("Make an already-stored instance the active default (omit the arg to pick interactively).").action(async p=>{let m=p??await Rj("Pick the active instance:"),w=await ki(m);if(!w){let _=await Nr();k(`no stored sign-in for ${Ot(m)}.`+(_.length?` Stored: ${_.map($=>$.baseUrl).join(", ")}.`:"")+` Run \`turbobase auth login --base-url ${Ot(m)} --token tb_pat_\u2026\`.`)}S(`active instance \u2192 ${w.baseUrl}.`)}),r.command("logout").description("Remove a stored sign-in (defaults to the active instance).").option("--base-url <url>","Which instance to remove (defaults to the active/single one).").option("--all","Remove every stored sign-in.").action(async p=>{if(p.all){let _=await Nr();for(let $ of _)await pl($.baseUrl);S(`removed ${_.length} stored sign-in${_.length===1?"":"s"}.`);return}let m=await pl(p.baseUrl);m||k(p.baseUrl?`no stored sign-in for ${Ot(p.baseUrl)}.`:"nothing to sign out of (or ambiguous \u2014 pass --base-url / --all).");let w=await or();S(`signed out of ${m}.`+(w?` Active instance \u2192 ${w.baseUrl}.`:""))}),n.command("types").description("Generate types.ts from the signed-in instance's catalog.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("-o, --out <file>","Output file","types.ts").option("--records-only","Emit only record-shape interfaces (skip the OpenAPI half).").action(async p=>{let{baseUrl:m,token:w}=await oe(p.baseUrl),_;try{_=await zr({baseUrl:m,token:w,recordsOnly:p.recordsOnly})}catch(g){let x=g instanceof Error?g.message:String(g);if(!p.recordsOnly&&/openapi\.json failed \(401/.test(x)){let A=await Ej(m,w);if(A)try{_=await zr({baseUrl:m,token:A,recordsOnly:!1});let C=$e(process.cwd(),p.out);await me.writeFile(C,_,"utf8"),S(`wrote ${C} (${_.split(`
1765
1765
  `).length} lines).`);return}catch{}Ee("types-gen: /v1/catalog/openapi.json needs a session \u2014 emitting record shapes only. To get the OpenAPI half headlessly, mint a PAT with the `data:read` scope (Settings \u203A Tokens); the CLI then exchanges it for a short-lived session automatically.");try{_=await zr({baseUrl:m,token:w,recordsOnly:!0})}catch(C){k(`types generation failed: ${C instanceof Error?C.message:String(C)}`)}}else k(`types generation failed: ${x}`)}let $=$e(process.cwd(),p.out);await me.writeFile($,_,"utf8"),S(`wrote ${$} (${_.split(`
@@ -1790,7 +1790,7 @@ samples init: ${A} written, ${C} skipped.`)});let er=e.command("guardrails").des
1790
1790
  `+T.dim("create one with `turbobase guardrails create <name> --source <s> --when '\u2026'`"));S(wn($.map(tn),{head:["NAME","STATE","SOURCE","RECORD","WHEN","NOTE"]}))}),er.command("create <name>").description("Create a DENY rule. Blocks PG\u2192source writes that match it.").option("--source <system>","Adapter id to scope to, or '*' for every source.","*").option("--record <type>","Record type to scope to, or '*' for every record.","*").option("--when <expr>","Condition, e.g. 'recordType not_in customer,invoice' or 'record.amount gt 10000'. Omit to deny unconditionally.").option("--message <text>","What a blocked writer is told.").option("--note <text>","Why this rule exists \u2014 for whoever reads `list` later.").option("--disabled","Create the rule parked, so it takes effect only when enabled.").option("--confirm-freeze-all","Required for an unconditional '*'/'*' rule.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,m)=>{let{baseUrl:w,token:_}=await oe(m.baseUrl),$=new te({baseUrl:w,token:_}),g;try{g=await $.guardrailCreate({name:p,source:m.source,record:m.record,...m.when?{when:m.when}:{},...m.message?{message:m.message}:{},...m.note?{note:m.note}:{},...m.disabled?{enabled:!1}:{},...m.confirmFreezeAll?{confirmFreezeAll:!0}:{}})}catch(x){Ae(x,"extensions:deploy","create a guardrail"),k(`create guardrail failed: ${x instanceof Error?x.message:String(x)}`)}S(`${T.green("\u2713")} guardrail ${T.bold(g.name)} created${g.enabled?"":T.dim(" (parked \u2014 enable it to take effect)")}`),S(T.dim(` denies ${g.source}/${g.record}${g.when?` where ${g.when}`:" (always)"}`)),S(T.dim(` verify with: turbobase guardrails test --source ${g.source==="*"?"<source>":g.source} --record <record>`))}),er.command("edit <name>").description("Change a rule \u2014 most often widening its exception set as records are proven.").option("--when <expr>","New condition. Pass '' to make it unconditional.").option("--message <text>","What a blocked writer is told.").option("--note <text>","Why this rule exists.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,m)=>{m.when===void 0&&m.message===void 0&&m.note===void 0&&k("edit: nothing to change \u2014 pass --when, --message or --note.");let{baseUrl:w,token:_}=await oe(m.baseUrl),$=new te({baseUrl:w,token:_}),g;try{g=await $.guardrailUpdate(p,{...m.when!==void 0?{when:m.when}:{},...m.message!==void 0?{message:m.message}:{},...m.note!==void 0?{note:m.note}:{}})}catch(x){Ae(x,"extensions:deploy","edit a guardrail"),k(`edit guardrail failed: ${x instanceof Error?x.message:String(x)}`)}S(`${T.green("\u2713")} guardrail ${T.bold(g.name)} updated`),S(T.dim(` denies ${g.source}/${g.record}${g.when?` where ${g.when}`:" (always)"}`))});for(let[p,m,w]of[["enable",!0,"enabled"],["disable",!1,"disabled"]])er.command(`${p} <name>`).description(m?"Arm a parked rule.":"Park a rule without deleting it \u2014 the reversible move mid-rollout.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(_,$)=>{let{baseUrl:g,token:x}=await oe($.baseUrl),A=new te({baseUrl:g,token:x});try{await A.guardrailUpdate(_,{enabled:m})}catch(C){Ae(C,"extensions:deploy",`${p} a guardrail`),k(`${p} guardrail failed: ${C instanceof Error?C.message:String(C)}`)}S(`${T.green("\u2713")} guardrail ${T.bold(_)} ${w}`),m||S(T.dim(" writes it was blocking are now permitted (subject to the other rules)."))});er.command("delete <name>").description("Remove a rule. This OPENS a write path \u2014 prefer `disable` if unsure.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,m)=>{let{baseUrl:w,token:_}=await oe(m.baseUrl),$=new te({baseUrl:w,token:_});try{await $.guardrailDelete(p)}catch(g){Ae(g,"extensions:deploy","delete a guardrail"),k(`delete guardrail failed: ${g instanceof Error?g.message:String(g)}`)}S(`${T.green("\u2713")} guardrail ${T.bold(p)} deleted`)}),er.command("test").description("Would a write be blocked? Runs the real evaluator; writes nothing.").requiredOption("--source <system>","Adapter id the write targets.").requiredOption("--record <type>","Record type the write targets.").option("--origin <origin>","Write origin to simulate ('user', 'agent').").option("--fields <json>","A candidate row as JSON, so `record.<column>` conditions can be decided.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--json","Emit raw JSON.").action(async p=>{let m;if(p.fields)try{m=JSON.parse(p.fields)}catch(x){k(`--fields must be valid JSON: ${x instanceof Error?x.message:String(x)}`)}let{baseUrl:w,token:_}=await oe(p.baseUrl),$=new te({baseUrl:w,token:_}),g;try{g=await $.guardrailTest({source:p.source,record:p.record,...p.origin?{origin:p.origin}:{},...m?{fields:m}:{}})}catch(x){Ae(x,"extensions:deploy","test guardrails"),k(`guardrails test failed: ${x instanceof Error?x.message:String(x)}`)}if(p.json)return S(JSON.stringify(g,null,2));g.allowed?S(`${T.green("\u2713")} ${p.source}/${p.record} \u2014 no guardrail blocks this write`):(S(`${T.red("\u2717")} ${p.source}/${p.record} \u2014 blocked by ${T.bold(g.blockedBy??"?")}`),g.message&&S(T.dim(` ${g.message}`))),g.undecided.length>0&&(S(T.yellow(` ${g.undecided.length} rule(s) need a row to decide: ${g.undecided.join(", ")}`)),S(T.dim(` re-run with --fields '{"\u2026":"\u2026"}' to evaluate them.`))),g.allowed||(process.exitCode=1)});let fr=e.command("tables").description("Author custom tables on your instance (create, evolve, inspect)."),Dn=(p,m={})=>{let w=`${T.bold(p.table)} ${T.dim(p.displayName)}`;S(p.exists===!1?`${w} ${T.dim("(new)")}`:w);for(let _ of p.fields){let $=[_.required?"required":null].filter(Boolean).join(", "),g=m.showClaimants&&_.claimedBy?.length?` ${T.dim(`\u2190 ${_.claimedBy.join(", ")}`)}`:"";S(` ${_.column.padEnd(28)} ${_.type}${$?` (${$})`:""}${g}`)}m.showClaimants&&p.claimants.length>1&&S(T.dim(` claimed by ${p.claimants.join(", ")}`))};function Un(p){if(p instanceof _e&&p.status===409){let m=p.body;m?.error==="table_conflict"&&m.message&&(Ee(m.message),process.exit(1))}k(`tables: ${p instanceof Error?p.message:String(p)}`)}let si=async p=>{let m=Rt($e(process.cwd(),p),"public","turbobase.json"),w=await me.readFile(m,"utf8").catch(()=>null);if(w===null)return null;let _;try{_=JSON.parse(w)}catch(g){k(`${m} is not valid JSON: ${g instanceof Error?g.message:String(g)}`)}let $=_.tables;return $===void 0?{tables:[],manifestPath:m}:(Array.isArray($)||k(`${m}: "tables" must be an array.`),{tables:$,manifestPath:m})};fr.command("list").description("List the custom tables on your instance and who claims each one.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--json","Emit raw JSON instead of a table.").action(async p=>{let{baseUrl:m,token:w}=await oe(p.baseUrl),_=new te({baseUrl:m,token:w}),$;try{$=await _.tablesList()}catch(g){Ae(g,"extensions:deploy","list tables"),k(`list tables failed: ${g instanceof Error?g.message:String(g)}`)}if(p.json)return S(JSON.stringify($,null,2));if($.length===0)return S("no custom tables yet \u2014 create one with `turbobase tables create <slug>`.");S(wn($.map(g=>[g.table,g.displayName,String(g.fields.length),g.status,g.claimedBy.join(", ")||"\u2014"]),{head:["TABLE","NAME","FIELDS","STATUS","CLAIMED BY"]}))}),fr.command("plan").description("Preview what this project's declared tables would do. Writes nothing; exits 1 on a conflict.").argument("[dir]","Project directory holding public/turbobase.json (default ./).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--as <claimant>","Claim as `cli` or `app:<slug>` (default: the app slug in .turbobase, else cli).").action(async(p,m)=>{let w=await si(p??".");if(w||k(`no public/turbobase.json under ${$e(process.cwd(),p??".")}.`),w.tables.length===0)return S(`${w.manifestPath} declares no tables \u2014 nothing to plan.`);let{baseUrl:_,token:$}=await oe(m.baseUrl),g=new te({baseUrl:_,token:$}),x;try{x=await Se("planning tables",()=>g.tablesPlan({claimedBy:m.as,tables:w.tables}))}catch(A){Ae(A,"extensions:deploy","plan tables"),Un(A)}S(`claiming as ${T.bold(x.claimedBy)}`);for(let A of x.tables)Dn(A,{showClaimants:!0});S(""),S(T.dim("nothing was applied \u2014 run `turbobase tables apply` (or deploy the app) to make it so."))}),fr.command("apply").description("Apply this project's declared tables to the instance.").argument("[dir]","Project directory holding public/turbobase.json (default ./).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--as <claimant>","Claim as `cli` or `app:<slug>` (default: cli).").action(async(p,m)=>{let w=await si(p??".");if(w||k(`no public/turbobase.json under ${$e(process.cwd(),p??".")}.`),w.tables.length===0)return S(`${w.manifestPath} declares no tables \u2014 nothing to apply.`);let{baseUrl:_,token:$}=await oe(m.baseUrl),g=new te({baseUrl:_,token:$}),x;try{x=await Se("applying tables",()=>g.tablesApply({claimedBy:m.as,tables:w.tables}))}catch(A){Ae(A,"extensions:deploy","apply tables"),Un(A)}for(let A of x.tables)Dn(A,{showClaimants:!0});S(`applied ${x.tables.length} table${x.tables.length===1?"":"s"} as ${x.claimedBy}.`)}),fr.command("create").description('Create a single table without a manifest (the CLI twin of "New custom table").').argument("<slug>","Table slug; the physical table is cr_<slug>.").requiredOption("-f, --field <name:type>","Field as name:type[:required] \u2014 text|number|date|bool|select. Repeatable.",(p,m)=>[...m??[],p],[]).option("--name <displayName>","Human label for the table (defaults to the slug).").option("--restrict-to-owner","Non-admins see only rows they created.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,m)=>{let w=m.field.map(A=>{let[C,L,D]=A.split(":");return(!C||!L)&&k(`--field "${A}" must be name:type[:required] (e.g. title:text:required).`),D!==void 0&&D!=="required"&&k(`--field "${A}": the third part may only be "required".`),{name:C,type:L,...D==="required"?{required:!0}:{}}}),{baseUrl:_,token:$}=await oe(m.baseUrl),g=new te({baseUrl:_,token:$}),x;try{x=await Se(`creating cr_${p}`,()=>g.tablesApply({tables:[{slug:p,displayName:m.name??p,fields:w,...m.restrictToOwner?{rowLevelOwner:!0}:{}}]}))}catch(A){Ae(A,"extensions:deploy","create a table"),Un(A)}for(let A of x.tables)Dn(A);S(""),S(T.dim("grant access under Settings \u203A Users, or in the Records page permissions matrix."))}),fr.command("drop").description("Drop a table and everything in it. Irreversible.").argument("<slug>","Table slug (without the cr_ prefix).").requiredOption("--confirm <slug>","Type the EXACT slug to confirm (fail-closed).").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,m)=>{m.confirm!==p&&k(`drop refused: --confirm "${m.confirm}" does not match "${p}".`);let{baseUrl:w,token:_}=await oe(m.baseUrl),$=new te({baseUrl:w,token:_}),g;try{g=await Se(`dropping cr_${p}`,()=>$.tablesDrop(p,m.confirm))}catch(x){Ae(x,"extensions:deploy","drop a table"),k(`drop failed: ${x instanceof Error?x.message:String(x)}`)}if(!g.removed)return S(`no table named "${p}" on this instance \u2014 nothing dropped.`);S(`dropped ${g.table} and every row in it.`)});let Pc=e.command("sandbox").description("Ephemeral local authoring sandboxes.");Pc.command("create").description("Stand up a fresh local sandbox seeded with a sampled+redacted slice of an instance.").requiredOption("--from <instance>","Source instance to clone the data slice from.").option("--ttl <duration>","Time-to-live before reaping (e.g. 72h, 30m, 7d).","72h").option("--sample <pct>","Percent of each table to copy (e.g. 2%).","2%").option("--base-url <url>","Local base URL the sandbox is reachable at.").action(async p=>{process.env.TURBOBASE_SANDBOX="1";let m=om(),w;try{w=await oy(m,{fromTenant:p.from,ttl:p.ttl,sample:p.sample,baseUrl:p.baseUrl})}catch(_){_ instanceof Xo&&k(_.message),k(`sandbox create failed: ${_ instanceof Error?_.message:String(_)}`)}S(`sandbox ${w.name} ready.`),S(` source: ${w.fromTenant} (sampled ${Math.round(w.sampleFraction*100)}%, redacted)`),S(` url: ${w.url}`),S(` expires: ${w.expiresAt}`)}),Pc.command("destroy").argument("<name>","The sandbox name to tear down.").description("Drop a sandbox database and remove it from the registry.").action(async p=>{let m=om();await Ul(m).catch(()=>[]);let w;try{w=await sy(m,p)}catch(_){k(`sandbox destroy failed: ${_ instanceof Error?_.message:String(_)}`)}w||k(`no sandbox named '${p}'.`),S(`destroyed sandbox ${w.name} (dropped ${w.database}).`)}),Pc.command("list").description("List active sandboxes with TTL/expiry; reaps any past their TTL.").action(async()=>{let p=om(),m=await Ul(p).catch(()=>[]),w=await zi(),{active:_}=Dl(w);if(_.length===0)S("no active sandboxes.");else{for(let $ of _)S(` ${$.name} from=${$.fromTenant} expires=${$.expiresAt} ${$.url}`);S(`
1791
1791
  ${_.length} active sandbox(es).`)}m.length>0&&S(`(reaped ${m.length} expired sandbox(es).)`)}),o.command("export").description("Snapshot the live instance into turbobase/bundle.json.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--out <dir>","Directory to write the bundle into.",Ni).action(async p=>{let{baseUrl:m,token:w}=await oe(p.baseUrl),_=new te({baseUrl:m,token:w}),$;try{$=await Se("exporting instance config",()=>_.configExport())}catch(A){A instanceof _e&&(A.status===401||A.status===403)&&k("ext export forbidden \u2014 the stored PAT lacks extensions:deploy. Re-run `turbobase auth login`."),k(`ext export failed: ${A instanceof Error?A.message:String(A)}`)}let g=typeof $.serialized=="string"?$.serialized:`${JSON.stringify($.manifest??$,null,2)}
1792
1792
  `,x=$e(process.cwd(),p.out,es);await me.mkdir(Co(x),{recursive:!0}),await me.writeFile(x,g,"utf8"),S(`wrote ${p.out}/${es} (${g.split(`
1793
- `).length} lines).`)});let lm=o.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),nk=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],um=()=>process.env.TURBOBASE_HOME?.trim()||Rt(Tj(),".turbobase"),ok=()=>"1.1.13",ii=async()=>Promise.all(nk.map(async p=>{try{return await import(p.name),{...p,present:!0}}catch{return{...p,present:!1}}}));lm.command("status").description("Report which optional native packages are installed.").action(async()=>{let p=await ii();for(let m of p)S(`${m.present?lt.success("installed"):lt.warn("missing")} ${T.bold(m.name)} \u2014 ${m.needs}`);p.some(m=>!m.present)&&S(`
1793
+ `).length} lines).`)});let lm=o.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),nk=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],um=()=>process.env.TURBOBASE_HOME?.trim()||Rt(Tj(),".turbobase"),ok=()=>"1.1.15",ii=async()=>Promise.all(nk.map(async p=>{try{return await import(p.name),{...p,present:!0}}catch{return{...p,present:!1}}}));lm.command("status").description("Report which optional native packages are installed.").action(async()=>{let p=await ii();for(let m of p)S(`${m.present?lt.success("installed"):lt.warn("missing")} ${T.bold(m.name)} \u2014 ${m.needs}`);p.some(m=>!m.present)&&S(`
1794
1794
  run \`${Wo} ext extras install\` to fetch them into ${um()}.`)}),lm.command("install").description("Install the optional native packages into $TURBOBASE_HOME (needs npm).").action(async()=>{let p=await ii();if(p.every(C=>C.present)){S(lt.success("extras already installed \u2014 nothing to do."));return}let m=um();await me.mkdir(m,{recursive:!0});let w=p.filter(C=>!C.present).map(C=>C.name),_=Cl(process.platform,process.arch),$=async(C,L)=>{let{code:D,stderr:ne}=await oi("npm",["install","--prefix",m,"--no-audit","--no-fund",...C]);D!==0&&k(`extras install failed while installing ${L} (npm exited ${D}).
1795
1795
  ${ne.trim()}
1796
1796
  These are native packages \u2014 they need npm and a working toolchain. Everything except \`test\` and \`replay\` works without them.`)},g=w.filter(C=>C!=="isolated-vm");if(g.length>0){let C=g.map(L=>`${L}@${Il[L]??"latest"}`);S(`installing ${C.join(", ")} \u2192 ${m}/node_modules \u2026`),await $(C,g.join(", "))}if(w.includes("isolated-vm")){let C=`isolated-vm@${Il["isolated-vm"]}`;if(S(`installing ${C} \u2192 ${m}/node_modules \u2026`),await $(["--ignore-scripts",C],"isolated-vm"),Yg.includes(_)){let D=`${Ol}@${ok()}`;S(`fetching ${D} (no upstream binary for ${_}) \u2026`);let{code:ne}=await oi("npm",["install","--prefix",m,"--no-audit","--no-fund","--ignore-scripts",D]);ne!==0&&S(lt.warn(`could not fetch ${D} \u2014 falling back to a source build.`))}let L=await Xg({home:m});L.length>0&&S(` ${lt.success("prebuilt")} ${L.join(", ")} \u2192 isolated-vm/prebuilds/${_}`)}let x=await ii(),A=x.filter(C=>!C.present).map(C=>C.name);if(A.includes("isolated-vm")){S(lt.warn(`no prebuilt isolated-vm for ${_} / Node ${process.versions.node} (${zl()}) \u2014 building from source. This needs Python and a C++ toolchain, and takes a few minutes.`));let{code:C}=await oi("npm",["rebuild","--prefix",m,"isolated-vm"]);C===0&&(x=await ii(),A=x.filter(L=>!L.present).map(L=>L.name))}A.length>0&&k(`npm succeeded but ${A.join(", ")} still will not load \u2014 no prebuilt binary for ${_} / Node ${process.versions.node} (${zl()}), and the source build did not produce one either. Everything except \`test\` and \`replay\` works without them.`),S(lt.success("extras installed \u2014 `turbobase ext test` and `turbobase ext replay` are ready."))});let Zn=e.command("release").description("Ship a release to your instance: deploy | status | rollback | list.");return Zn.command("deploy").description("Enqueue a server-side release deploy. UPLOADS the SDF project src/ from the checkout (the producer swap) when present; else a solution-only live-fetch deploy. An approved target requires --approved-by.").requiredOption("--target <mode>","Deploy posture: auto (apply now) | approved (needs a recorded approval).").option("--solution <id>","The solution id the release delivers (omit for an upload-only deploy).").option("--project <dir>","SDF project dir whose src/ is uploaded (default: cwd).").option("--git-repo <repo>","Git provenance LINK: source repo (default $GITHUB_REPOSITORY or `git remote get-url origin`).").option("--git-ref <ref>","Git provenance LINK: ref/branch/tag (default $GITHUB_REF).").option("--ref <committish>","Git provenance: the ref/branch/tag/sha the upload was built from; resolved to a commit sha via `git rev-parse` (default $GITHUB_SHA or HEAD).").option("--pr-url <url>","Git provenance LINK: the PR this deploy came from (default $PR_URL).").option("--approved-by <email>","Record the approval (REQUIRED for --target approved).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--follow","Poll the release deploy to completion (exit non-zero on failure).").option("--interval-ms <ms>","Poll interval for --follow (default 2000).").action(async p=>{let m=p.target==="approved"?"approved":p.target==="auto"?"auto":null;m||k(`--target must be 'auto' or 'approved' (got '${p.target}').`);let w=await Mj($e(process.cwd(),p.project??".")),_=Object.keys(w).length;!p.solution&&_===0&&k("release deploy needs --solution <id> (live-fetch) or an SDF project (a src/ dir under --project, default cwd) to upload.");let g=(p.ref?await sm(["rev-parse",p.ref])??p.ref:null)??process.env.GITHUB_SHA??await sm(["rev-parse","HEAD"]),x=p.gitRepo??process.env.GITHUB_REPOSITORY??await sm(["remote","get-url","origin"]),A=p.gitRef??process.env.GITHUB_REF??null,C=p.prUrl??process.env.PR_URL??null,{baseUrl:L,token:D}=await oe(p.baseUrl),ne=new te({baseUrl:L,token:D}),U;try{U=await ne.releaseDeploy({solutionId:p.solution??null,target:m,approvedBy:p.approvedBy??null,files:_>0?w:null,gitRepo:x,gitRef:A,gitSha:g,prUrl:C})}catch(fe){Ae(fe,"releases:write","release deploy"),fe instanceof _e&&fe.status===404&&k("solution_not_found \u2014 no solution with that id."),k(`release deploy failed: ${fe instanceof Error?fe.message:String(fe)}`)}S(`release ${U.releaseId} enqueued \u2192 taskId=${U.taskId} (target=${m}`+(_>0?`, uploaded ${_} bundle file(s)`:", live-fetch")+")."),p.follow&&await Pl(()=>ne.releaseStatus(U.releaseId),`release ${U.releaseId}`,p.intervalMs)}),Zn.command("pull").argument("<id>","The release id").description("Write the SDF project a FAILED release built into <dir>: owned/ (yours, editable) and context/ (the rest of the bundle, read-only). Edit owned/, then `release push`.").requiredOption("--dir <path>","Directory to write the project into (created if absent).").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,m)=>{let{baseUrl:w,token:_}=await oe(m.baseUrl),$=new te({baseUrl:w,token:_}),g;try{g=await $.releaseFiles(p)}catch(C){C instanceof _e&&C.status===404&&k(`release ${p} not found.`),C instanceof _e&&C.status===409&&k(`no built project was retained for release ${p}. Files are kept for a FAILED attempt only \u2014 a shipped bundle is recoverable from the account.`),Ae(C,"releases:write","pull a release"),k(`release pull failed: ${C instanceof Error?C.message:String(C)}`)}let x=$e(process.cwd(),m.dir),A=0;for(let[C,L]of[["owned",g.owned],["context",g.context]])for(let[D,ne]of Object.entries(L)){let U=Rt(x,C,D);await me.mkdir(Co(U),{recursive:!0}),await me.writeFile(U,ne,"utf8"),A++}await me.writeFile(Rt(x,".turbobase-release.json"),JSON.stringify({releaseId:g.releaseId,version:g.version,contentDigest:g.contentDigest},null,2),"utf8"),S(`pulled release ${g.releaseId} (${g.status}) \u2192 ${x}
package/dist/turbobase.js CHANGED
@@ -1712,7 +1712,7 @@ ${r}`}var zy=JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",comman
1712
1712
  `))}catch(c){if(c instanceof ie&&c.status===409){pe(T.red("turbobase: the pending queue changed while you were reading the plan \u2014 nothing was written.")),pe(`re-run \`${nn} kb push\` to review the current plan.`),process.exitCode=2;return}Kd(c),pe(T.red(`turbobase: ${c instanceof Error?c.message:String(c)}`)),process.exitCode=2}}yr();function eb(t){return t.diff??!!t.dryRun}function tb(t,e){let r=[];return t.forEach((n,s)=>{let o=n.action==="new_rule"?T.green("new rule"):T.yellow("amend rule"),i=n.targetPublished?` ${T.yellow("[currently live]")}`:"";r.push(`${s+1}. ${o} ${T.bold(n.slug)}${i}`),r.push(` ${n.name}`);for(let a of n.entries)r.push(` ${T.dim("\xB7")} ${T.dim(a.app_slug)} ${a.title}`);if(e.diff){r.push("");for(let a of n.bodyMarkdown.split(`
1713
1713
  `))r.push(` ${T.dim("|")} ${a}`)}r.push("")}),r}function rb(t){let e=t.reduce((s,o)=>s+o.entries.length,0),r=t.filter(s=>s.targetPublished).length,n=[`${e} finding(s) \u2192 ${t.length} rule(s)`];return r>0&&n.push(T.yellow(`${r} amend a rule that is currently live`)),n.join(" \xB7 ")}async function Wd(t,e=r=>new H(r)){t.limit!==void 0&&(!Number.isFinite(t.limit)||t.limit<1)&&$("--limit must be a positive integer.");let r=await G(t.baseUrl),n=e(r),s={...t.app?{app:t.app}:{},...t.limit!==void 0?{limit:t.limit}:{}},o;try{o=await ue("reading the pending rules",()=>n.rulesPending(s))}catch(a){he(a,"extensions:deploy","read the pending rules"),$(`could not read the pending queue: ${a instanceof Error?a.message:String(a)}`)}if(o.items.length===0){pe(o.pendingTotal===0?"nothing pending \u2014 no findings are waiting under `## Agent rules`.":`nothing matched \u2014 ${o.pendingTotal} pending rule finding(s) exist but not under these filters.`);return}if(x(tb(o.items,{diff:eb(t)}).join(`
1714
1714
  `)),x(rb(o.items)),t.dryRun){pe(T.dim("dry run \u2014 nothing was written."));return}let i=o.items.reduce((a,c)=>a+c.entries.length,0);if(!t.yes){if(!kt()){pe(T.red("turbobase: refusing to write without confirmation on a non-interactive terminal. Re-run with --yes (or --dry-run to review the plan).")),process.exitCode=1;return}if(await $t(`Push ${i} finding(s) as ${o.items.length} draft rule(s)?`,[{value:"yes",label:"Yes \u2014 write them as draft rules",description:"they land DISABLED; an admin publishes them before anything is injected"},{value:"no",label:"No \u2014 leave the queue untouched"}],{defaultIndex:1}).catch(()=>"no")!=="yes"){pe("aborted \u2014 nothing was written."),process.exitCode=1;return}}try{await ue("writing the rules",()=>n.rulesPush(o.planToken,s))}catch(a){a instanceof ie&&a.status===409&&$("the pending queue changed while you were reading the plan \u2014 nothing was written. Re-run to see the new plan."),$(`push failed: ${a instanceof Error?a.message:String(a)}`)}x(T.green(`wrote ${o.items.length} draft rule(s).`)),pe(T.dim("They are DRAFT and DISABLED \u2014 nothing is injected yet. An admin reviews and publishes them under Settings \u203A Skills."))}import{promises as Jd}from"node:fs";import{join as nb,relative as sb,resolve as ob,sep as ib}from"node:path";var ab=new Set(["node_modules","dist","build","coverage",".git",".claude",".turbobase",".next",".svelte-kit",".vercel",".cache"]),lb=new Set([".mcp.json",".env"]);function cb(t){return lb.has(t)||t.startsWith(".env.")&&t!==".env.example"}function db(t){return Buffer.compare(Buffer.from(t.toString("utf8"),"utf8"),t)===0}function ub(t){return t.includes(0)}async function Zd(t){let e=ob(t),r={},n=[],s=[],o=[];async function i(a){let c=await Jd.readdir(a,{withFileTypes:!0});for(let d of c){let u=nb(a,d.name);if(d.isDirectory()){if(ab.has(d.name))continue;await i(u);continue}if(!d.isFile())continue;let h=sb(e,u).split(ib).join("/");if(cb(d.name)){s.push(h);continue}let S=await Jd.readFile(u);if(!db(S)){n.push(h);continue}ub(S)&&o.push(h),r[h]=S.toString("utf8")}}return await i(e),{files:r,skipped:n,secrets:s,unstorable:o}}import{randomBytes as pb}from"node:crypto";var fb="/_tb/app-assets",hb=16,mb=/^[0-9a-f]{32}$/;function Yd(){return pb(hb).toString("hex")}function gb(t){return mb.test(t)}function Qd(t){if(!gb(t))throw new Error(`not a build id: ${t}`);return`${fb}/${t}/`}var Xd=7;import ys from"node:fs/promises";import on from"node:path";var Ri={esbuild:"0.28.1","isolated-vm":"6.1.2"},Ei="@turbobase/isolated-vm-prebuilds";function Pi(t,e){return`${t}-${e}`}function Ai(t=process.versions){return`abi${t.modules}`}var eu=["darwin-x64","win32-arm64"];async function tu(t){let e=t.platform??process.platform,r=t.arch??process.arch,n=Pi(e,r),s=on.join(t.home,"node_modules"),o=on.join(s,...Ei.split("/"),"prebuilds",n),i=on.join(s,"isolated-vm","prebuilds",n),a;try{a=await ys.readdir(o)}catch{return[]}let c=[];await ys.mkdir(i,{recursive:!0});for(let d of a){if(!d.endsWith(".node"))continue;let u=on.join(i,d);try{await ys.copyFile(on.join(o,d),u,ys.constants?.COPYFILE_EXCL??1),c.push(d)}catch{}}return c}import{homedir as yb}from"node:os";import{join as ru}from"node:path";import{promises as Ii}from"node:fs";function su(){let t=process.env.TURBOBASE_HOME??ru(yb(),".turbobase");return{dir:t,file:ru(t,"sandboxes.json")}}var bb={s:1e3,m:6e4,h:36e5,d:864e5};function wb(t){let e=/^\s*(\d+)\s*([smhd])\s*$/.exec(t);if(!e)throw new Error(`invalid --ttl '${t}': expected <number><unit> where unit is s|m|h|d (e.g. 72h, 30m, 7d).`);let r=Number(e[1]);if(!Number.isFinite(r)||r<=0)throw new Error(`invalid --ttl '${t}': duration must be a positive integer.`);return r*bb[e[2]]}function vb(t,e=Date.now()){return new Date(e+wb(t)).toISOString()}function Sb(t,e=Date.now()){let r=Date.parse(t);return Number.isNaN(r)?!0:r<=e}function _b(t){let e=/^\s*(\d+(?:\.\d+)?)\s*%?\s*$/.exec(t);if(!e)throw new Error(`invalid --sample '${t}': expected a percent like 2% (or a bare number).`);let r=Number(e[1]);if(!Number.isFinite(r)||r<0||r>100)throw new Error(`invalid --sample '${t}': percent must be between 0 and 100.`);return r/100}function kb(t,e){if(e>=1)return[...t];if(e<=0||t.length===0)return[];let r=Math.max(1,Math.round(1/e)),n=[];for(let s=0;s<t.length;s+=r)n.push(t[s]);return n}var $b="[REDACTED]",an=class extends Error{constructor(e){super(e),this.name="SandboxDataCopyUnavailableError"}},Ci=class extends Error{table;column;constructor(e,r,n){super(`Refusing to copy ${e}.${r} into a sandbox: ${n}. A sandbox must never hold un-redacted PII \u2014 classify the column 'safe' (non-PII) or let redaction mask it, then retry.`),this.name="UnredactedPiiError",this.table=e,this.column=r}};function xb(t,e,r){let n={};for(let[s,o]of Object.entries(e)){let i=r[s];if(i===void 0)throw new Ci(t,s,"column has no PII classification (unknown = unsafe)");i==="pii"?n[s]=o==null?o:$b:n[s]=o}return n}function Tb(t,e,r){return e.map(n=>xb(t,n,r))}var nu=new Set(["localhost","127.0.0.1","::1","0.0.0.0"]),Rb=["oraclecloud.com","us-ashburn-1","amazonaws.com","rds.","supabase.co","turbobase.app","turbobase.io"];function Eb(t){let e=t.trim();try{let s=new URL(e);if(s.hostname)return s.hostname.toLowerCase()}catch{}let r=e.replace(/^[a-z]+:\/\//i,""),n=(r.split("/")[0]??r).split("@").pop()??r;return(n.split(":")[0]??n).toLowerCase()}function Oi(t){if(!t||!t.trim())throw new Error("local-only guard: empty database target \u2014 refusing to proceed.");let e=t.toLowerCase();for(let n of Rb)if(e.includes(n))throw new Error(`local-only guard: target '${t}' looks like a cloud/prod context (matched '${n}'). \`turbobase sandbox create\` is LOCAL-ONLY and will never touch a cloud database.`);let r=Eb(t);if(!nu.has(r))throw new Error(`local-only guard: target host '${r}' is not a recognised local Postgres (${[...nu].join(", ")}). \`turbobase sandbox create\` is LOCAL-ONLY.`)}function Pb(t,e=Ab){return`sbx-${t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"tenant"}-${e()}`}function Ab(){return Math.floor(Math.random()*16777215).toString(16).padStart(6,"0")}function Ib(t){return`turbobase_${t.replace(/[^a-z0-9]+/gi,"_").toLowerCase()}`}function Cb(t,e){return`${(e??process.env.TURBOBASE_SANDBOX_BASE_URL??"http://localhost:5173").replace(/\/+$/,"")}/?sandbox=${encodeURIComponent(t)}`}async function Ni(){let{file:t}=su();try{let e=await Ii.readFile(t,"utf8"),r=JSON.parse(e);return Array.isArray(r)?r:[]}catch(e){if(e.code==="ENOENT")return[];throw e}}async function ou(t){let{dir:e,file:r}=su();await Ii.mkdir(e,{recursive:!0}),await Ii.writeFile(r,`${JSON.stringify(t,null,2)}
1715
- `,{mode:384})}async function Ob(t){let e=await Ni();e.push(t),await ou(e)}async function iu(t){let e=await Ni(),r=e.findIndex(s=>s.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await ou(e),n??null}async function bs(){return Ni()}function ji(t,e=Date.now()){let r=[],n=[];for(let s of t)Sb(s.expiresAt,e)?n.push(s):r.push(s);return{active:r,expired:n}}async function au(t,e){Oi(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new an(r);let n=_b(e.sample),s=e.now??Date.now(),o=(e.nameGen??(d=>Pb(d)))(e.fromTenant),i=Ib(o);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let d of a){let u=kb(d.rows,n),h=Tb(d.table,u,d.classification);h.length>0&&await t.copyRows(i,d.table,h)}let c={name:o,fromTenant:e.fromTenant,database:i,createdAt:new Date(s).toISOString(),expiresAt:vb(e.ttl,s),sampleFraction:n,url:Cb(o,e.baseUrl)};return await Ob(c),c}async function lu(t,e){Oi(t.target());let n=(await bs()).find(s=>s.name===e);return n?(await t.dropDatabase(n.database),await iu(e),n):null}async function zi(t,e=Date.now()){Oi(t.target());let r=await bs(),{expired:n}=ji(r,e),s=[];for(let o of n){try{await t.dropDatabase(o.database)}catch{continue}await iu(o.name),s.push(o)}return s}var ln={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","data:read"],defaultTtlDays:90,mintable:!0,label:"Personal access token"},catalog:{prefix:"tb_cat_",defaultScopes:["catalog:read"],allowedScopes:["catalog:read"],defaultTtlDays:null,mintable:!0,label:"AI / catalog token"}},kE=Object.keys(ln).filter(t=>ln[t].mintable);var TE=ln.pat.prefix,Nb=Object.entries(ln).map(([t,e])=>[e.prefix,t]),jb=/^[A-Za-z0-9_-]{20,}$/;function zb(t){if(typeof t!="string")return null;for(let[e,r]of Nb)if(t.startsWith(e)&&jb.test(t.slice(e.length)))return r;return null}function cu(t){return zb(t)==="pat"}var cn="bundle.json",ws="turbobase";function uu(t){let e;try{e=JSON.parse(t)}catch(c){throw new Error(`bundle.json is not valid JSON: ${c instanceof Error?c.message:String(c)}`)}if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("bundle.json must be a JSON object");let r=e,n=typeof r.version=="number"?r.version:0;if(n!==1)throw new Error(`bundle.json version ${n} is unsupported (expected 1)`);let s=c=>Array.isArray(c)?c:[],o=r.sync??{},i=r.guardrails??{},a={version:1,...typeof r.tenant=="string"?{tenant:r.tenant}:{},hooks:s(r.hooks),conduits:s(r.conduits),sync:{fields:s(o.fields),tables:s(o.tables)},guardrails:{validationHooks:s(i.validationHooks),fieldRules:s(i.fieldRules),conduits:s(i.conduits)},schedules:s(r.schedules)};return Lb(a)}function Lb(t){let e=r=>(n,s)=>r(n)<r(s)?-1:r(n)>r(s)?1:0;return{version:1,...t.tenant?{tenant:t.tenant}:{},hooks:[...t.hooks].sort(e(r=>r.id)),conduits:[...t.conduits].sort(e(r=>r.id)),sync:{fields:[...t.sync.fields].sort(e(du)),tables:[...t.sync.tables].sort(e(r=>r.sourceRecordType))},guardrails:{validationHooks:[...t.guardrails.validationHooks].sort(),fieldRules:[...t.guardrails.fieldRules].sort(e(du)),conduits:[...t.guardrails.conduits].sort(e(r=>r.conduitId))},schedules:[...t.schedules].sort(e(r=>r.name))}}function du(t){return`${t.sourceSystem}::${t.recordType}::${t.fieldName}`}function pu(t,e){let r=(e??"").trim();if(!r)return{ok:!1,reason:"target instance slug is unknown \u2014 cannot confirm a promote"};let n=(t??"").trim();return n?n!==r?{ok:!1,reason:`--confirm "${n}" does not match the target instance "${r}" \u2014 refusing to apply`}:{ok:!0}:{ok:!1,reason:`--confirm <slug> is required for promote (expected "${r}")`}}async function e0(t,e){try{let r=await fetch(`${t.replace(/\/+$/,"")}/auth/pat-exchange`,{method:"POST",headers:{Authorization:`Bearer ${e}`}});if(!r.ok)return null;let n=await r.json();return typeof n.token=="string"&&n.token.length>0?n.token:null}catch{return null}}async function t0(t){let e=await Ct();e.length===0&&$("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),kt()||$("this command needs an explicit <base-url> in a non-interactive shell.");try{return await $t(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{$("cancelled.")}}async function r0(t,e){let r;try{r=await fetch(`${t}/v1/catalog/records`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json"},signal:AbortSignal.timeout(8e3)})}catch(s){return{ok:!1,kind:"unreachable",detail:s instanceof Error?s.message:String(s)}}let n=(r.headers.get("content-type")??"").includes("application/json");return r.status===200||r.status===403?{ok:!0}:r.status===401?{ok:!1,kind:"bad-token",detail:`HTTP ${r.status}`}:n?{ok:!0}:{ok:!1,kind:"wrong-host",detail:`HTTP ${r.status}`}}async function n0(t,e,r){let n=await ad({baseUrl:t,token:e}),s=(await Ct()).length-1;if(x(`signed in to ${n.baseUrl} \u2014 now the active instance`+(s>0?` (${s} other${s===1?"":"s"} stored; commands default here).`:".")),!r)return;let o=await r0(n.baseUrl,e);o.ok||(o.kind==="wrong-host"?(pe(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${o.detail}.`),pe(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),pe(" The token is stored, but commands will fail against this base-url until it points at the API.")):o.kind==="bad-token"?pe(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):pe(`warning: could not reach ${n.baseUrl} to verify the token (${o.detail}). Stored anyway.`))}async function s0(t){let e=await tn(t),r=c=>{let d=new RegExp(`${c}\\s*:\\s*(['"\`])([^'"\`]+)\\1`).exec(e);return d?d[2]:void 0},n=/\bdefineConduit\s*[<(]/.test(e),s=r("id");if(n){let c=r("on"),d=r("recordType"),u=r("source");return(!s||!d||!c||!u)&&$(`could not parse id/on/from.source/from.recordType from ${t} \u2014 is it a defineConduit({...}) file?`),{id:s,recordType:d,event:c,source:u,sourceCode:e,extensionKind:"conduit"}}let o=r("recordType"),i=r("event"),a=r("source")??"standalone";return(!s||!o||!i)&&$(`could not parse id/recordType/event from ${t} \u2014 is it a defineHook({...}) file?`),{id:s,recordType:o,event:i,source:a,sourceCode:e,extensionKind:"hook"}}async function Wr(t,e,r,n){try{return await te.access(t),"skipped"}catch{}return await te.mkdir(Gr(t),{recursive:!0}),await te.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await te.chmod(t,n),"wrote"}async function ig(t,e){if(ag(t)!==".gitignore")return Wr(t,e.content,e.executable,e.mode);let r=await te.readFile(t,"utf8").catch(()=>null);if(r===null)return Wr(t,e.content,e.executable,e.mode);let n=qd(r);return n===null?"skipped":(await te.writeFile(t,n,"utf8"),"merged")}function Oc(t){if(t!==void 0){if(t==="junit"||t==="json")return t;$(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function o0(t){return{name:"turbobase ext test",cases:t.map(r=>{if(r.pass)return{name:r.name,status:"passed"};let n=r.error?"error":r.rejection?.code??"mismatch",s=r.error?`error: ${r.error}`:r.observed!==r.expected?`expected ${r.expected}, observed ${r.observed}`+(r.rejection?` (rejected: ${r.rejection.code??"rejected"}${r.rejection.message?` \u2014 ${r.rejection.message}`:""})`:""):`rejected: ${r.rejection?.code??"rejected"}${r.rejection?.message?` \u2014 ${r.rejection.message}`:""}`;return{name:r.name,status:"failed",detail:s,type:n}})}}function i0(t,e){let r=e.warnings.length===0?[{name:`${t} (${e.kind})`,status:"passed"}]:e.warnings.map(n=>({name:`${t}: ${n.rule}`,status:"failed",type:n.rule,detail:`${n.message} \u2192 fix: ${n.fix}`}));return{name:`turbobase ext lint ${t}`,cases:r}}function a0(t){let e=[];if(t.error)return e.push({name:`replay ${t.fixtureName}`,status:"failed",type:"error",detail:t.error}),{name:`turbobase ext replay ${t.fixtureName}`,cases:e};for(let r of t.guards)e.push(r.status==="failed"?{name:`guard: ${r.name}`,status:"failed",type:r.name,detail:r.detail}:{name:`guard: ${r.name}`,status:"passed",detail:r.status==="skipped-offline"?`skipped-offline: ${r.detail}`:void 0});return t.whenPassed&&t.applyBranch&&e.push({name:`apply-branch: ${t.applyBranch}`,status:"passed",detail:`${t.masterAction??""} (${t.wouldWriteBindings.length} binding(s) would be written)`.trim()}),{name:`turbobase ext replay ${t.fixtureName}`,cases:e}}function Dc({includeInternal:t=!0}={}){let e=new XT;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.1.13").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",p=>p.command.parent?"":`${wi()}
1715
+ `,{mode:384})}async function Ob(t){let e=await Ni();e.push(t),await ou(e)}async function iu(t){let e=await Ni(),r=e.findIndex(s=>s.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await ou(e),n??null}async function bs(){return Ni()}function ji(t,e=Date.now()){let r=[],n=[];for(let s of t)Sb(s.expiresAt,e)?n.push(s):r.push(s);return{active:r,expired:n}}async function au(t,e){Oi(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new an(r);let n=_b(e.sample),s=e.now??Date.now(),o=(e.nameGen??(d=>Pb(d)))(e.fromTenant),i=Ib(o);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let d of a){let u=kb(d.rows,n),h=Tb(d.table,u,d.classification);h.length>0&&await t.copyRows(i,d.table,h)}let c={name:o,fromTenant:e.fromTenant,database:i,createdAt:new Date(s).toISOString(),expiresAt:vb(e.ttl,s),sampleFraction:n,url:Cb(o,e.baseUrl)};return await Ob(c),c}async function lu(t,e){Oi(t.target());let n=(await bs()).find(s=>s.name===e);return n?(await t.dropDatabase(n.database),await iu(e),n):null}async function zi(t,e=Date.now()){Oi(t.target());let r=await bs(),{expired:n}=ji(r,e),s=[];for(let o of n){try{await t.dropDatabase(o.database)}catch{continue}await iu(o.name),s.push(o)}return s}var ln={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","data:read"],defaultTtlDays:90,mintable:!0,label:"Personal access token"},catalog:{prefix:"tb_cat_",defaultScopes:["catalog:read"],allowedScopes:["catalog:read"],defaultTtlDays:null,mintable:!0,label:"AI / catalog token"}},kE=Object.keys(ln).filter(t=>ln[t].mintable);var TE=ln.pat.prefix,Nb=Object.entries(ln).map(([t,e])=>[e.prefix,t]),jb=/^[A-Za-z0-9_-]{20,}$/;function zb(t){if(typeof t!="string")return null;for(let[e,r]of Nb)if(t.startsWith(e)&&jb.test(t.slice(e.length)))return r;return null}function cu(t){return zb(t)==="pat"}var cn="bundle.json",ws="turbobase";function uu(t){let e;try{e=JSON.parse(t)}catch(c){throw new Error(`bundle.json is not valid JSON: ${c instanceof Error?c.message:String(c)}`)}if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("bundle.json must be a JSON object");let r=e,n=typeof r.version=="number"?r.version:0;if(n!==1)throw new Error(`bundle.json version ${n} is unsupported (expected 1)`);let s=c=>Array.isArray(c)?c:[],o=r.sync??{},i=r.guardrails??{},a={version:1,...typeof r.tenant=="string"?{tenant:r.tenant}:{},hooks:s(r.hooks),conduits:s(r.conduits),sync:{fields:s(o.fields),tables:s(o.tables)},guardrails:{validationHooks:s(i.validationHooks),fieldRules:s(i.fieldRules),conduits:s(i.conduits)},schedules:s(r.schedules)};return Lb(a)}function Lb(t){let e=r=>(n,s)=>r(n)<r(s)?-1:r(n)>r(s)?1:0;return{version:1,...t.tenant?{tenant:t.tenant}:{},hooks:[...t.hooks].sort(e(r=>r.id)),conduits:[...t.conduits].sort(e(r=>r.id)),sync:{fields:[...t.sync.fields].sort(e(du)),tables:[...t.sync.tables].sort(e(r=>r.sourceRecordType))},guardrails:{validationHooks:[...t.guardrails.validationHooks].sort(),fieldRules:[...t.guardrails.fieldRules].sort(e(du)),conduits:[...t.guardrails.conduits].sort(e(r=>r.conduitId))},schedules:[...t.schedules].sort(e(r=>r.name))}}function du(t){return`${t.sourceSystem}::${t.recordType}::${t.fieldName}`}function pu(t,e){let r=(e??"").trim();if(!r)return{ok:!1,reason:"target instance slug is unknown \u2014 cannot confirm a promote"};let n=(t??"").trim();return n?n!==r?{ok:!1,reason:`--confirm "${n}" does not match the target instance "${r}" \u2014 refusing to apply`}:{ok:!0}:{ok:!1,reason:`--confirm <slug> is required for promote (expected "${r}")`}}async function e0(t,e){try{let r=await fetch(`${t.replace(/\/+$/,"")}/auth/pat-exchange`,{method:"POST",headers:{Authorization:`Bearer ${e}`}});if(!r.ok)return null;let n=await r.json();return typeof n.token=="string"&&n.token.length>0?n.token:null}catch{return null}}async function t0(t){let e=await Ct();e.length===0&&$("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),kt()||$("this command needs an explicit <base-url> in a non-interactive shell.");try{return await $t(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{$("cancelled.")}}async function r0(t,e){let r;try{r=await fetch(`${t}/v1/catalog/records`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json"},signal:AbortSignal.timeout(8e3)})}catch(s){return{ok:!1,kind:"unreachable",detail:s instanceof Error?s.message:String(s)}}let n=(r.headers.get("content-type")??"").includes("application/json");return r.status===200||r.status===403?{ok:!0}:r.status===401?{ok:!1,kind:"bad-token",detail:`HTTP ${r.status}`}:n?{ok:!0}:{ok:!1,kind:"wrong-host",detail:`HTTP ${r.status}`}}async function n0(t,e,r){let n=await ad({baseUrl:t,token:e}),s=(await Ct()).length-1;if(x(`signed in to ${n.baseUrl} \u2014 now the active instance`+(s>0?` (${s} other${s===1?"":"s"} stored; commands default here).`:".")),!r)return;let o=await r0(n.baseUrl,e);o.ok||(o.kind==="wrong-host"?(pe(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${o.detail}.`),pe(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),pe(" The token is stored, but commands will fail against this base-url until it points at the API.")):o.kind==="bad-token"?pe(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):pe(`warning: could not reach ${n.baseUrl} to verify the token (${o.detail}). Stored anyway.`))}async function s0(t){let e=await tn(t),r=c=>{let d=new RegExp(`${c}\\s*:\\s*(['"\`])([^'"\`]+)\\1`).exec(e);return d?d[2]:void 0},n=/\bdefineConduit\s*[<(]/.test(e),s=r("id");if(n){let c=r("on"),d=r("recordType"),u=r("source");return(!s||!d||!c||!u)&&$(`could not parse id/on/from.source/from.recordType from ${t} \u2014 is it a defineConduit({...}) file?`),{id:s,recordType:d,event:c,source:u,sourceCode:e,extensionKind:"conduit"}}let o=r("recordType"),i=r("event"),a=r("source")??"standalone";return(!s||!o||!i)&&$(`could not parse id/recordType/event from ${t} \u2014 is it a defineHook({...}) file?`),{id:s,recordType:o,event:i,source:a,sourceCode:e,extensionKind:"hook"}}async function Wr(t,e,r,n){try{return await te.access(t),"skipped"}catch{}return await te.mkdir(Gr(t),{recursive:!0}),await te.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await te.chmod(t,n),"wrote"}async function ig(t,e){if(ag(t)!==".gitignore")return Wr(t,e.content,e.executable,e.mode);let r=await te.readFile(t,"utf8").catch(()=>null);if(r===null)return Wr(t,e.content,e.executable,e.mode);let n=qd(r);return n===null?"skipped":(await te.writeFile(t,n,"utf8"),"merged")}function Oc(t){if(t!==void 0){if(t==="junit"||t==="json")return t;$(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function o0(t){return{name:"turbobase ext test",cases:t.map(r=>{if(r.pass)return{name:r.name,status:"passed"};let n=r.error?"error":r.rejection?.code??"mismatch",s=r.error?`error: ${r.error}`:r.observed!==r.expected?`expected ${r.expected}, observed ${r.observed}`+(r.rejection?` (rejected: ${r.rejection.code??"rejected"}${r.rejection.message?` \u2014 ${r.rejection.message}`:""})`:""):`rejected: ${r.rejection?.code??"rejected"}${r.rejection?.message?` \u2014 ${r.rejection.message}`:""}`;return{name:r.name,status:"failed",detail:s,type:n}})}}function i0(t,e){let r=e.warnings.length===0?[{name:`${t} (${e.kind})`,status:"passed"}]:e.warnings.map(n=>({name:`${t}: ${n.rule}`,status:"failed",type:n.rule,detail:`${n.message} \u2192 fix: ${n.fix}`}));return{name:`turbobase ext lint ${t}`,cases:r}}function a0(t){let e=[];if(t.error)return e.push({name:`replay ${t.fixtureName}`,status:"failed",type:"error",detail:t.error}),{name:`turbobase ext replay ${t.fixtureName}`,cases:e};for(let r of t.guards)e.push(r.status==="failed"?{name:`guard: ${r.name}`,status:"failed",type:r.name,detail:r.detail}:{name:`guard: ${r.name}`,status:"passed",detail:r.status==="skipped-offline"?`skipped-offline: ${r.detail}`:void 0});return t.whenPassed&&t.applyBranch&&e.push({name:`apply-branch: ${t.applyBranch}`,status:"passed",detail:`${t.masterAction??""} (${t.wouldWriteBindings.length} binding(s) would be written)`.trim()}),{name:`turbobase ext replay ${t.fixtureName}`,cases:e}}function Dc({includeInternal:t=!0}={}){let e=new XT;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.1.15").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",p=>p.command.parent?"":`${wi()}
1716
1716
  `),e.hook("preAction",(p,f)=>{Bd(e.opts(),f.opts())});let r=e.command("auth").description("Sign in to an instance and manage stored credentials."),n=e.command("project").description("Set up the local project and the files an agent session reads."),s=e.command("ext").description("Author, test, ship, and observe extensions \u2014 hooks and conduits."),o=e.command("kb").description("Write into the instance's knowledge base."),i=e.command("skills").description("Write standing rules into the instance's agent skills.");r.command("login").description("Sign in to an instance by pasting a personal access token (PAT), stored in ~/.turbobase/credentials.").requiredOption("--base-url <url>","Instance base URL, e.g. https://acme.turbobase.app").option("--token <tb_pat_\u2026>","The PAT to store. Mint one in your app under Settings \u203A Access tokens.").option("--no-verify","Skip the post-login connectivity/token probe.").action(async p=>{let f=We(p.baseUrl);p.token||$(`auth login needs --token. Mint a PAT in your Turbobase app (Settings \u203A Access tokens), then:
1717
1717
  ${nn} auth login --base-url ${f} --token tb_pat_\u2026`),cu(p.token)||$("--token does not look like a Turbobase PAT (tb_pat_\u2026)."),await n0(f,p.token,p.verify)}),r.command("status").description("Show the active instance (the no-`--base-url` default) and every stored sign-in.").action(async()=>{let p=await Ct();if(p.length===0){x("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`.");return}let f=await ht(),g=y=>!!f&&y.baseUrl===f.baseUrl;if(qe())x(Qt(p.map(y=>[g(y)?T.green("*"):" ",T.bold(y.baseUrl),T.dim(y.updatedAt)]),{head:["","instance","updated"]}));else for(let y of p)x(`${g(y)?"* ":" "}${y.baseUrl} (updated ${y.updatedAt})`);f||pe("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}),r.command("use [base-url]").description("Make an already-stored instance the active default (omit the arg to pick interactively).").action(async p=>{let f=p??await t0("Pick the active instance:"),g=await ls(f);if(!g){let y=await Ct();$(`no stored sign-in for ${We(f)}.`+(y.length?` Stored: ${y.map(_=>_.baseUrl).join(", ")}.`:"")+` Run \`turbobase auth login --base-url ${We(f)} --token tb_pat_\u2026\`.`)}x(`active instance \u2192 ${g.baseUrl}.`)}),r.command("logout").description("Remove a stored sign-in (defaults to the active instance).").option("--base-url <url>","Which instance to remove (defaults to the active/single one).").option("--all","Remove every stored sign-in.").action(async p=>{if(p.all){let y=await Ct();for(let _ of y)await li(_.baseUrl);x(`removed ${y.length} stored sign-in${y.length===1?"":"s"}.`);return}let f=await li(p.baseUrl);f||$(p.baseUrl?`no stored sign-in for ${We(p.baseUrl)}.`:"nothing to sign out of (or ambiguous \u2014 pass --base-url / --all).");let g=await ht();x(`signed out of ${f}.`+(g?` Active instance \u2192 ${g.baseUrl}.`:""))}),n.command("types").description("Generate types.ts from the signed-in instance's catalog.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("-o, --out <file>","Output file","types.ts").option("--records-only","Emit only record-shape interfaces (skip the OpenAPI half).").action(async p=>{let{baseUrl:f,token:g}=await G(p.baseUrl),y;try{y=await It({baseUrl:f,token:g,recordsOnly:p.recordsOnly})}catch(m){let v=m instanceof Error?m.message:String(m);if(!p.recordsOnly&&/openapi\.json failed \(401/.test(v)){let P=await e0(f,g);if(P)try{y=await It({baseUrl:f,token:P,recordsOnly:!1});let A=ce(process.cwd(),p.out);await te.writeFile(A,y,"utf8"),x(`wrote ${A} (${y.split(`
1718
1718
  `).length} lines).`);return}catch{}pe("types-gen: /v1/catalog/openapi.json needs a session \u2014 emitting record shapes only. To get the OpenAPI half headlessly, mint a PAT with the `data:read` scope (Settings \u203A Tokens); the CLI then exchanges it for a short-lived session automatically.");try{y=await It({baseUrl:f,token:g,recordsOnly:!0})}catch(A){$(`types generation failed: ${A instanceof Error?A.message:String(A)}`)}}else $(`types generation failed: ${v}`)}let _=ce(process.cwd(),p.out);await te.writeFile(_,y,"utf8"),x(`wrote ${_} (${y.split(`
@@ -1743,7 +1743,7 @@ samples init: ${P} written, ${A} skipped.`)});let ft=e.command("guardrails").des
1743
1743
  `+T.dim("create one with `turbobase guardrails create <name> --source <s> --when '\u2026'`"));x(Qt(_.map(Zt),{head:["NAME","STATE","SOURCE","RECORD","WHEN","NOTE"]}))}),ft.command("create <name>").description("Create a DENY rule. Blocks PG\u2192source writes that match it.").option("--source <system>","Adapter id to scope to, or '*' for every source.","*").option("--record <type>","Record type to scope to, or '*' for every record.","*").option("--when <expr>","Condition, e.g. 'recordType not_in customer,invoice' or 'record.amount gt 10000'. Omit to deny unconditionally.").option("--message <text>","What a blocked writer is told.").option("--note <text>","Why this rule exists \u2014 for whoever reads `list` later.").option("--disabled","Create the rule parked, so it takes effect only when enabled.").option("--confirm-freeze-all","Required for an unconditional '*'/'*' rule.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,f)=>{let{baseUrl:g,token:y}=await G(f.baseUrl),_=new H({baseUrl:g,token:y}),m;try{m=await _.guardrailCreate({name:p,source:f.source,record:f.record,...f.when?{when:f.when}:{},...f.message?{message:f.message}:{},...f.note?{note:f.note}:{},...f.disabled?{enabled:!1}:{},...f.confirmFreezeAll?{confirmFreezeAll:!0}:{}})}catch(v){he(v,"extensions:deploy","create a guardrail"),$(`create guardrail failed: ${v instanceof Error?v.message:String(v)}`)}x(`${T.green("\u2713")} guardrail ${T.bold(m.name)} created${m.enabled?"":T.dim(" (parked \u2014 enable it to take effect)")}`),x(T.dim(` denies ${m.source}/${m.record}${m.when?` where ${m.when}`:" (always)"}`)),x(T.dim(` verify with: turbobase guardrails test --source ${m.source==="*"?"<source>":m.source} --record <record>`))}),ft.command("edit <name>").description("Change a rule \u2014 most often widening its exception set as records are proven.").option("--when <expr>","New condition. Pass '' to make it unconditional.").option("--message <text>","What a blocked writer is told.").option("--note <text>","Why this rule exists.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,f)=>{f.when===void 0&&f.message===void 0&&f.note===void 0&&$("edit: nothing to change \u2014 pass --when, --message or --note.");let{baseUrl:g,token:y}=await G(f.baseUrl),_=new H({baseUrl:g,token:y}),m;try{m=await _.guardrailUpdate(p,{...f.when!==void 0?{when:f.when}:{},...f.message!==void 0?{message:f.message}:{},...f.note!==void 0?{note:f.note}:{}})}catch(v){he(v,"extensions:deploy","edit a guardrail"),$(`edit guardrail failed: ${v instanceof Error?v.message:String(v)}`)}x(`${T.green("\u2713")} guardrail ${T.bold(m.name)} updated`),x(T.dim(` denies ${m.source}/${m.record}${m.when?` where ${m.when}`:" (always)"}`))});for(let[p,f,g]of[["enable",!0,"enabled"],["disable",!1,"disabled"]])ft.command(`${p} <name>`).description(f?"Arm a parked rule.":"Park a rule without deleting it \u2014 the reversible move mid-rollout.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(y,_)=>{let{baseUrl:m,token:v}=await G(_.baseUrl),P=new H({baseUrl:m,token:v});try{await P.guardrailUpdate(y,{enabled:f})}catch(A){he(A,"extensions:deploy",`${p} a guardrail`),$(`${p} guardrail failed: ${A instanceof Error?A.message:String(A)}`)}x(`${T.green("\u2713")} guardrail ${T.bold(y)} ${g}`),f||x(T.dim(" writes it was blocking are now permitted (subject to the other rules)."))});ft.command("delete <name>").description("Remove a rule. This OPENS a write path \u2014 prefer `disable` if unsure.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,f)=>{let{baseUrl:g,token:y}=await G(f.baseUrl),_=new H({baseUrl:g,token:y});try{await _.guardrailDelete(p)}catch(m){he(m,"extensions:deploy","delete a guardrail"),$(`delete guardrail failed: ${m instanceof Error?m.message:String(m)}`)}x(`${T.green("\u2713")} guardrail ${T.bold(p)} deleted`)}),ft.command("test").description("Would a write be blocked? Runs the real evaluator; writes nothing.").requiredOption("--source <system>","Adapter id the write targets.").requiredOption("--record <type>","Record type the write targets.").option("--origin <origin>","Write origin to simulate ('user', 'agent').").option("--fields <json>","A candidate row as JSON, so `record.<column>` conditions can be decided.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--json","Emit raw JSON.").action(async p=>{let f;if(p.fields)try{f=JSON.parse(p.fields)}catch(v){$(`--fields must be valid JSON: ${v instanceof Error?v.message:String(v)}`)}let{baseUrl:g,token:y}=await G(p.baseUrl),_=new H({baseUrl:g,token:y}),m;try{m=await _.guardrailTest({source:p.source,record:p.record,...p.origin?{origin:p.origin}:{},...f?{fields:f}:{}})}catch(v){he(v,"extensions:deploy","test guardrails"),$(`guardrails test failed: ${v instanceof Error?v.message:String(v)}`)}if(p.json)return x(JSON.stringify(m,null,2));m.allowed?x(`${T.green("\u2713")} ${p.source}/${p.record} \u2014 no guardrail blocks this write`):(x(`${T.red("\u2717")} ${p.source}/${p.record} \u2014 blocked by ${T.bold(m.blockedBy??"?")}`),m.message&&x(T.dim(` ${m.message}`))),m.undecided.length>0&&(x(T.yellow(` ${m.undecided.length} rule(s) need a row to decide: ${m.undecided.join(", ")}`)),x(T.dim(` re-run with --fields '{"\u2026":"\u2026"}' to evaluate them.`))),m.allowed||(process.exitCode=1)});let St=e.command("tables").description("Author custom tables on your instance (create, evolve, inspect)."),fr=(p,f={})=>{let g=`${T.bold(p.table)} ${T.dim(p.displayName)}`;x(p.exists===!1?`${g} ${T.dim("(new)")}`:g);for(let y of p.fields){let _=[y.required?"required":null].filter(Boolean).join(", "),m=f.showClaimants&&y.claimedBy?.length?` ${T.dim(`\u2190 ${y.claimedBy.join(", ")}`)}`:"";x(` ${y.column.padEnd(28)} ${y.type}${_?` (${_})`:""}${m}`)}f.showClaimants&&p.claimants.length>1&&x(T.dim(` claimed by ${p.claimants.join(", ")}`))};function hr(p){if(p instanceof ie&&p.status===409){let f=p.body;f?.error==="table_conflict"&&f.message&&(pe(f.message),process.exit(1))}$(`tables: ${p instanceof Error?p.message:String(p)}`)}let es=async p=>{let f=Ve(ce(process.cwd(),p),"public","turbobase.json"),g=await te.readFile(f,"utf8").catch(()=>null);if(g===null)return null;let y;try{y=JSON.parse(g)}catch(m){$(`${f} is not valid JSON: ${m instanceof Error?m.message:String(m)}`)}let _=y.tables;return _===void 0?{tables:[],manifestPath:f}:(Array.isArray(_)||$(`${f}: "tables" must be an array.`),{tables:_,manifestPath:f})};St.command("list").description("List the custom tables on your instance and who claims each one.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--json","Emit raw JSON instead of a table.").action(async p=>{let{baseUrl:f,token:g}=await G(p.baseUrl),y=new H({baseUrl:f,token:g}),_;try{_=await y.tablesList()}catch(m){he(m,"extensions:deploy","list tables"),$(`list tables failed: ${m instanceof Error?m.message:String(m)}`)}if(p.json)return x(JSON.stringify(_,null,2));if(_.length===0)return x("no custom tables yet \u2014 create one with `turbobase tables create <slug>`.");x(Qt(_.map(m=>[m.table,m.displayName,String(m.fields.length),m.status,m.claimedBy.join(", ")||"\u2014"]),{head:["TABLE","NAME","FIELDS","STATUS","CLAIMED BY"]}))}),St.command("plan").description("Preview what this project's declared tables would do. Writes nothing; exits 1 on a conflict.").argument("[dir]","Project directory holding public/turbobase.json (default ./).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--as <claimant>","Claim as `cli` or `app:<slug>` (default: the app slug in .turbobase, else cli).").action(async(p,f)=>{let g=await es(p??".");if(g||$(`no public/turbobase.json under ${ce(process.cwd(),p??".")}.`),g.tables.length===0)return x(`${g.manifestPath} declares no tables \u2014 nothing to plan.`);let{baseUrl:y,token:_}=await G(f.baseUrl),m=new H({baseUrl:y,token:_}),v;try{v=await ue("planning tables",()=>m.tablesPlan({claimedBy:f.as,tables:g.tables}))}catch(P){he(P,"extensions:deploy","plan tables"),hr(P)}x(`claiming as ${T.bold(v.claimedBy)}`);for(let P of v.tables)fr(P,{showClaimants:!0});x(""),x(T.dim("nothing was applied \u2014 run `turbobase tables apply` (or deploy the app) to make it so."))}),St.command("apply").description("Apply this project's declared tables to the instance.").argument("[dir]","Project directory holding public/turbobase.json (default ./).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--as <claimant>","Claim as `cli` or `app:<slug>` (default: cli).").action(async(p,f)=>{let g=await es(p??".");if(g||$(`no public/turbobase.json under ${ce(process.cwd(),p??".")}.`),g.tables.length===0)return x(`${g.manifestPath} declares no tables \u2014 nothing to apply.`);let{baseUrl:y,token:_}=await G(f.baseUrl),m=new H({baseUrl:y,token:_}),v;try{v=await ue("applying tables",()=>m.tablesApply({claimedBy:f.as,tables:g.tables}))}catch(P){he(P,"extensions:deploy","apply tables"),hr(P)}for(let P of v.tables)fr(P,{showClaimants:!0});x(`applied ${v.tables.length} table${v.tables.length===1?"":"s"} as ${v.claimedBy}.`)}),St.command("create").description('Create a single table without a manifest (the CLI twin of "New custom table").').argument("<slug>","Table slug; the physical table is cr_<slug>.").requiredOption("-f, --field <name:type>","Field as name:type[:required] \u2014 text|number|date|bool|select. Repeatable.",(p,f)=>[...f??[],p],[]).option("--name <displayName>","Human label for the table (defaults to the slug).").option("--restrict-to-owner","Non-admins see only rows they created.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,f)=>{let g=f.field.map(P=>{let[A,C,O]=P.split(":");return(!A||!C)&&$(`--field "${P}" must be name:type[:required] (e.g. title:text:required).`),O!==void 0&&O!=="required"&&$(`--field "${P}": the third part may only be "required".`),{name:A,type:C,...O==="required"?{required:!0}:{}}}),{baseUrl:y,token:_}=await G(f.baseUrl),m=new H({baseUrl:y,token:_}),v;try{v=await ue(`creating cr_${p}`,()=>m.tablesApply({tables:[{slug:p,displayName:f.name??p,fields:g,...f.restrictToOwner?{rowLevelOwner:!0}:{}}]}))}catch(P){he(P,"extensions:deploy","create a table"),hr(P)}for(let P of v.tables)fr(P);x(""),x(T.dim("grant access under Settings \u203A Users, or in the Records page permissions matrix."))}),St.command("drop").description("Drop a table and everything in it. Irreversible.").argument("<slug>","Table slug (without the cr_ prefix).").requiredOption("--confirm <slug>","Type the EXACT slug to confirm (fail-closed).").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,f)=>{f.confirm!==p&&$(`drop refused: --confirm "${f.confirm}" does not match "${p}".`);let{baseUrl:g,token:y}=await G(f.baseUrl),_=new H({baseUrl:g,token:y}),m;try{m=await ue(`dropping cr_${p}`,()=>_.tablesDrop(p,f.confirm))}catch(v){he(v,"extensions:deploy","drop a table"),$(`drop failed: ${v instanceof Error?v.message:String(v)}`)}if(!m.removed)return x(`no table named "${p}" on this instance \u2014 nothing dropped.`);x(`dropped ${m.table} and every row in it.`)});let Ko=e.command("sandbox").description("Ephemeral local authoring sandboxes.");Ko.command("create").description("Stand up a fresh local sandbox seeded with a sampled+redacted slice of an instance.").requiredOption("--from <instance>","Source instance to clone the data slice from.").option("--ttl <duration>","Time-to-live before reaping (e.g. 72h, 30m, 7d).","72h").option("--sample <pct>","Percent of each table to copy (e.g. 2%).","2%").option("--base-url <url>","Local base URL the sandbox is reachable at.").action(async p=>{process.env.TURBOBASE_SANDBOX="1";let f=jc(),g;try{g=await au(f,{fromTenant:p.from,ttl:p.ttl,sample:p.sample,baseUrl:p.baseUrl})}catch(y){y instanceof an&&$(y.message),$(`sandbox create failed: ${y instanceof Error?y.message:String(y)}`)}x(`sandbox ${g.name} ready.`),x(` source: ${g.fromTenant} (sampled ${Math.round(g.sampleFraction*100)}%, redacted)`),x(` url: ${g.url}`),x(` expires: ${g.expiresAt}`)}),Ko.command("destroy").argument("<name>","The sandbox name to tear down.").description("Drop a sandbox database and remove it from the registry.").action(async p=>{let f=jc();await zi(f).catch(()=>[]);let g;try{g=await lu(f,p)}catch(y){$(`sandbox destroy failed: ${y instanceof Error?y.message:String(y)}`)}g||$(`no sandbox named '${p}'.`),x(`destroyed sandbox ${g.name} (dropped ${g.database}).`)}),Ko.command("list").description("List active sandboxes with TTL/expiry; reaps any past their TTL.").action(async()=>{let p=jc(),f=await zi(p).catch(()=>[]),g=await bs(),{active:y}=ji(g);if(y.length===0)x("no active sandboxes.");else{for(let _ of y)x(` ${_.name} from=${_.fromTenant} expires=${_.expiresAt} ${_.url}`);x(`
1744
1744
  ${y.length} active sandbox(es).`)}f.length>0&&x(`(reaped ${f.length} expired sandbox(es).)`)}),s.command("export").description("Snapshot the live instance into turbobase/bundle.json.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--out <dir>","Directory to write the bundle into.",ws).action(async p=>{let{baseUrl:f,token:g}=await G(p.baseUrl),y=new H({baseUrl:f,token:g}),_;try{_=await ue("exporting instance config",()=>y.configExport())}catch(P){P instanceof ie&&(P.status===401||P.status===403)&&$("ext export forbidden \u2014 the stored PAT lacks extensions:deploy. Re-run `turbobase auth login`."),$(`ext export failed: ${P instanceof Error?P.message:String(P)}`)}let m=typeof _.serialized=="string"?_.serialized:`${JSON.stringify(_.manifest??_,null,2)}
1745
1745
  `,v=ce(process.cwd(),p.out,cn);await te.mkdir(Gr(v),{recursive:!0}),await te.writeFile(v,m,"utf8"),x(`wrote ${p.out}/${cn} (${m.split(`
1746
- `).length} lines).`)});let qc=s.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),lg=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],Mc=()=>process.env.TURBOBASE_HOME?.trim()||Ve(QT(),".turbobase"),cg=()=>"1.1.13",ts=async()=>Promise.all(lg.map(async p=>{try{return await import(p.name),{...p,present:!0}}catch{return{...p,present:!1}}}));qc.command("status").description("Report which optional native packages are installed.").action(async()=>{let p=await ts();for(let f of p)x(`${f.present?Ne.success("installed"):Ne.warn("missing")} ${T.bold(f.name)} \u2014 ${f.needs}`);p.some(f=>!f.present)&&x(`
1746
+ `).length} lines).`)});let qc=s.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),lg=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],Mc=()=>process.env.TURBOBASE_HOME?.trim()||Ve(QT(),".turbobase"),cg=()=>"1.1.15",ts=async()=>Promise.all(lg.map(async p=>{try{return await import(p.name),{...p,present:!0}}catch{return{...p,present:!1}}}));qc.command("status").description("Report which optional native packages are installed.").action(async()=>{let p=await ts();for(let f of p)x(`${f.present?Ne.success("installed"):Ne.warn("missing")} ${T.bold(f.name)} \u2014 ${f.needs}`);p.some(f=>!f.present)&&x(`
1747
1747
  run \`${nn} ext extras install\` to fetch them into ${Mc()}.`)}),qc.command("install").description("Install the optional native packages into $TURBOBASE_HOME (needs npm).").action(async()=>{let p=await ts();if(p.every(A=>A.present)){x(Ne.success("extras already installed \u2014 nothing to do."));return}let f=Mc();await te.mkdir(f,{recursive:!0});let g=p.filter(A=>!A.present).map(A=>A.name),y=Pi(process.platform,process.arch),_=async(A,C)=>{let{code:O,stderr:V}=await Xn("npm",["install","--prefix",f,"--no-audit","--no-fund",...A]);O!==0&&$(`extras install failed while installing ${C} (npm exited ${O}).
1748
1748
  ${V.trim()}
1749
1749
  These are native packages \u2014 they need npm and a working toolchain. Everything except \`test\` and \`replay\` works without them.`)},m=g.filter(A=>A!=="isolated-vm");if(m.length>0){let A=m.map(C=>`${C}@${Ri[C]??"latest"}`);x(`installing ${A.join(", ")} \u2192 ${f}/node_modules \u2026`),await _(A,m.join(", "))}if(g.includes("isolated-vm")){let A=`isolated-vm@${Ri["isolated-vm"]}`;if(x(`installing ${A} \u2192 ${f}/node_modules \u2026`),await _(["--ignore-scripts",A],"isolated-vm"),eu.includes(y)){let O=`${Ei}@${cg()}`;x(`fetching ${O} (no upstream binary for ${y}) \u2026`);let{code:V}=await Xn("npm",["install","--prefix",f,"--no-audit","--no-fund","--ignore-scripts",O]);V!==0&&x(Ne.warn(`could not fetch ${O} \u2014 falling back to a source build.`))}let C=await tu({home:f});C.length>0&&x(` ${Ne.success("prebuilt")} ${C.join(", ")} \u2192 isolated-vm/prebuilds/${y}`)}let v=await ts(),P=v.filter(A=>!A.present).map(A=>A.name);if(P.includes("isolated-vm")){x(Ne.warn(`no prebuilt isolated-vm for ${y} / Node ${process.versions.node} (${Ai()}) \u2014 building from source. This needs Python and a C++ toolchain, and takes a few minutes.`));let{code:A}=await Xn("npm",["rebuild","--prefix",f,"isolated-vm"]);A===0&&(v=await ts(),P=v.filter(C=>!C.present).map(C=>C.name))}P.length>0&&$(`npm succeeded but ${P.join(", ")} still will not load \u2014 no prebuilt binary for ${y} / Node ${process.versions.node} (${Ai()}), and the source build did not produce one either. Everything except \`test\` and \`replay\` works without them.`),x(Ne.success("extras installed \u2014 `turbobase ext test` and `turbobase ext replay` are ready."))});let mr=e.command("release").description("Ship a release to your instance: deploy | status | rollback | list.");return mr.command("deploy").description("Enqueue a server-side release deploy. UPLOADS the SDF project src/ from the checkout (the producer swap) when present; else a solution-only live-fetch deploy. An approved target requires --approved-by.").requiredOption("--target <mode>","Deploy posture: auto (apply now) | approved (needs a recorded approval).").option("--solution <id>","The solution id the release delivers (omit for an upload-only deploy).").option("--project <dir>","SDF project dir whose src/ is uploaded (default: cwd).").option("--git-repo <repo>","Git provenance LINK: source repo (default $GITHUB_REPOSITORY or `git remote get-url origin`).").option("--git-ref <ref>","Git provenance LINK: ref/branch/tag (default $GITHUB_REF).").option("--ref <committish>","Git provenance: the ref/branch/tag/sha the upload was built from; resolved to a commit sha via `git rev-parse` (default $GITHUB_SHA or HEAD).").option("--pr-url <url>","Git provenance LINK: the PR this deploy came from (default $PR_URL).").option("--approved-by <email>","Record the approval (REQUIRED for --target approved).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--follow","Poll the release deploy to completion (exit non-zero on failure).").option("--interval-ms <ms>","Poll interval for --follow (default 2000).").action(async p=>{let f=p.target==="approved"?"approved":p.target==="auto"?"auto":null;f||$(`--target must be 'auto' or 'approved' (got '${p.target}').`);let g=await u0(ce(process.cwd(),p.project??".")),y=Object.keys(g).length;!p.solution&&y===0&&$("release deploy needs --solution <id> (live-fetch) or an SDF project (a src/ dir under --project, default cwd) to upload.");let m=(p.ref?await zc(["rev-parse",p.ref])??p.ref:null)??process.env.GITHUB_SHA??await zc(["rev-parse","HEAD"]),v=p.gitRepo??process.env.GITHUB_REPOSITORY??await zc(["remote","get-url","origin"]),P=p.gitRef??process.env.GITHUB_REF??null,A=p.prUrl??process.env.PR_URL??null,{baseUrl:C,token:O}=await G(p.baseUrl),V=new H({baseUrl:C,token:O}),N;try{N=await V.releaseDeploy({solutionId:p.solution??null,target:f,approvedBy:p.approvedBy??null,files:y>0?g:null,gitRepo:v,gitRef:P,gitSha:m,prUrl:A})}catch(ee){he(ee,"releases:write","release deploy"),ee instanceof ie&&ee.status===404&&$("solution_not_found \u2014 no solution with that id."),$(`release deploy failed: ${ee instanceof Error?ee.message:String(ee)}`)}x(`release ${N.releaseId} enqueued \u2192 taskId=${N.taskId} (target=${f}`+(y>0?`, uploaded ${y} bundle file(s)`:", live-fetch")+")."),p.follow&&await xi(()=>V.releaseStatus(N.releaseId),`release ${N.releaseId}`,p.intervalMs)}),mr.command("pull").argument("<id>","The release id").description("Write the SDF project a FAILED release built into <dir>: owned/ (yours, editable) and context/ (the rest of the bundle, read-only). Edit owned/, then `release push`.").requiredOption("--dir <path>","Directory to write the project into (created if absent).").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,f)=>{let{baseUrl:g,token:y}=await G(f.baseUrl),_=new H({baseUrl:g,token:y}),m;try{m=await _.releaseFiles(p)}catch(A){A instanceof ie&&A.status===404&&$(`release ${p} not found.`),A instanceof ie&&A.status===409&&$(`no built project was retained for release ${p}. Files are kept for a FAILED attempt only \u2014 a shipped bundle is recoverable from the account.`),he(A,"releases:write","pull a release"),$(`release pull failed: ${A instanceof Error?A.message:String(A)}`)}let v=ce(process.cwd(),f.dir),P=0;for(let[A,C]of[["owned",m.owned],["context",m.context]])for(let[O,V]of Object.entries(C)){let N=Ve(v,A,O);await te.mkdir(Gr(N),{recursive:!0}),await te.writeFile(N,V,"utf8"),P++}await te.writeFile(Ve(v,".turbobase-release.json"),JSON.stringify({releaseId:m.releaseId,version:m.version,contentDigest:m.contentDigest},null,2),"utf8"),x(`pulled release ${m.releaseId} (${m.status}) \u2192 ${v}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turbobase/cli",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "Author, test, and deploy Turbobase extensions (hooks + conduits) against a tenant over HTTP + a personal access token.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",