@turbobase/cli 1.1.21 → 1.1.22
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.
|
@@ -2151,7 +2151,7 @@ ${o.map(gE).join(`
|
|
|
2151
2151
|
`))}async function jy(t,e,r=co){let n=[...new Set(t.map(a=>a.trim()).filter(Boolean))];n.length===0&&v("name at least one skill slug. `turbobase skills list` shows them.");let o=await lo(e,r);if(!await Ji(`Publish ${n.length} skill(s)?`,"Yes \u2014 publish them","their text is injected into every matching turn from now on",e))return;let i;try{i=await ie("publishing",()=>o.skillsPublish(n))}catch(a){oe(a,"skills:publish","publish a skill"),v(`publish failed: ${a instanceof Error?a.message:String(a)}`)}x(g.green(`published ${i.published.length} skill(s) \u2014 live now.`)),i.unknown.length>0&&(B(g.yellow(`no such skill: ${i.unknown.join(", ")}`)),process.exitCode=1),B(g.dim(`release v${i.version.version_no} \xB7 undo with: turbobase skills unpublish ${i.published.join(" ")}`))}async function Ly(t,e,r=co){let n=[...new Set(t.map(a=>a.trim()).filter(Boolean))];n.length===0&&v("name at least one skill slug. `turbobase skills list` shows them.");let o=await lo(e,r);if(!await Ji(`Move ${n.length} skill(s) back to draft?`,"Yes \u2014 unpublish them","they stop being injected immediately; the text and its history are kept",e))return;let i;try{i=await ie("unpublishing",()=>o.skillsUnpublish(n))}catch(a){oe(a,"skills:publish","unpublish a skill"),v(`unpublish failed: ${a instanceof Error?a.message:String(a)}`)}x(g.green(`unpublished ${i.unpublished.length} skill(s) \u2014 retracted now.`)),B(g.dim(`release v${i.version.version_no} \xB7 re-publish with: turbobase skills publish ${i.unpublished.join(" ")}`)),i.unknown.length>0&&(B(g.yellow(`no such skill: ${i.unknown.join(", ")}`)),process.exitCode=1)}async function Dy(t,e,r=co){let n=Number(t);(!Number.isInteger(n)||n<1)&&v(`--version must be a release number (got '${t}'). \`turbobase skills releases\` lists them.`);let o=await lo(e,r);if(await Ji(`Roll the skills back to release v${n}?`,"Yes \u2014 restore that release","content only: a skill an operator has since unpublished stays off",e))try{let i=await ie("rolling back",()=>o.skillsRollback(n));x(g.green(`rolled back to v${i.version.version_no}.`))}catch(i){oe(i,"skills:publish","roll back a skills release"),v(`rollback failed: ${i instanceof Error?i.message:String(i)}`)}}async function My(t,e=co){let r=await lo(t,e),n;try{n=await ie("reading the releases",()=>r.skillsReleases())}catch(o){oe(o,"skills:publish","read the skills releases"),v(`could not read the releases: ${o instanceof Error?o.message:String(o)}`)}if(n.items.length===0){B("no releases yet.");return}for(let o of n.items){let s=o.is_current?g.green("*"):" ",i=o.release_note?` \u2014 ${o.release_note}`:"";x(`${s} v${o.version_no} ${o.skill_count} skill(s) ${g.dim(o.published_at)}${o.published_by?g.dim(` by ${o.published_by}`):""}${g.dim(i)}`)}}async function Uy(t,e,r=co){let n=[...new Set((t??[]).map(l=>l.trim()).filter(Boolean))];n.length===0&&v("name at least one skill to delete. `turbobase skills list` shows the slugs.");let o=await lo(e,r);if(!await Ji(n.length===1?`Delete the skill '${n[0]}'?`:`Delete ${n.length} skills?
|
|
2152
2152
|
${n.join(`
|
|
2153
2153
|
`)}`,"Yes \u2014 delete them","permanent: the rows and their text are removed. Prior releases keep their own copy.",e))return;let i=[],a=[];for(let l of n)try{i.push(await ie(`deleting ${l}`,()=>o.skillsDelete(l)))}catch(u){if(u instanceof pe&&u.status===404){a.push(l);continue}i.length>0&&B(g.dim(`deleted before the error: ${i.map(d=>d.slug).join(", ")}`)),oe(u,"skills:publish","delete a skill"),v(`delete failed on '${l}': ${u instanceof Error?u.message:String(u)}`)}i.length>0&&x(g.green(`deleted ${i.length} skill(s).`));let c=i.filter(l=>l.wasPublished).map(l=>l.slug);c.length>0&&B(g.yellow(`Published until now, so no longer injected: ${c.join(", ")}`)),a.length>0&&(B(g.yellow(`no such skill: ${a.join(", ")}`)),process.exitCode=1)}import{promises as Zy}from"node:fs";import{join as FE,relative as BE,resolve as VE,sep as HE}from"node:path";var KE=new Set(["node_modules","dist","build","coverage",".git",".claude",".turbobase",".next",".svelte-kit",".vercel",".cache"]),GE=new Set([".mcp.json",".env"]);function WE(t){return GE.has(t)||t.startsWith(".env.")&&t!==".env.example"}function JE(t){return Buffer.compare(Buffer.from(t.toString("utf8"),"utf8"),t)===0}function YE(t){return t.includes(0)}async function qy(t){let e=VE(t),r={},n=[],o=[],s=[];async function i(a){let c=await Zy.readdir(a,{withFileTypes:!0});for(let l of c){let u=FE(a,l.name);if(l.isDirectory()){if(KE.has(l.name))continue;await i(u);continue}if(!l.isFile())continue;let d=BE(e,u).split(HE).join("/");if(WE(l.name)){o.push(d);continue}let w=await Zy.readFile(u);if(!JE(w)){n.push(d);continue}YE(w)&&s.push(d),r[d]=w.toString("utf8")}}return await i(e),{files:r,skipped:n,secrets:o,unstorable:s}}import{randomBytes as XE}from"node:crypto";var QE="/_tb/app-assets",eR=16,tR=/^[0-9a-f]{32}$/;function Fy(){return XE(eR).toString("hex")}function rR(t){return tR.test(t)}function By(t){if(!rR(t))throw new Error(`not a build id: ${t}`);return`${QE}/${t}/`}var Vy=7;import Yi from"node:fs/promises";import ls from"node:path";var Ql={esbuild:"0.28.1","isolated-vm":"6.1.2"},eu="@turbobase/isolated-vm-prebuilds";function tu(t,e){return`${t}-${e}`}function ru(t=process.versions){return`abi${t.modules}`}var Hy=["darwin-x64","win32-arm64"];async function Ky(t){let e=t.platform??process.platform,r=t.arch??process.arch,n=tu(e,r),o=ls.join(t.home,"node_modules"),s=ls.join(o,...eu.split("/"),"prebuilds",n),i=ls.join(o,"isolated-vm","prebuilds",n),a;try{a=await Yi.readdir(s)}catch{return[]}let c=[];await Yi.mkdir(i,{recursive:!0});for(let l of a){if(!l.endsWith(".node"))continue;let u=ls.join(i,l);try{await Yi.copyFile(ls.join(s,l),u,Yi.constants?.COPYFILE_EXCL??1),c.push(l)}catch{}}return c}import{homedir as nR}from"node:os";import{join as Gy}from"node:path";import{promises as nu}from"node:fs";function Jy(){let t=process.env.TURBOBASE_HOME??Gy(nR(),".turbobase");return{dir:t,file:Gy(t,"sandboxes.json")}}var oR={s:1e3,m:6e4,h:36e5,d:864e5};function sR(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*oR[e[2]]}function iR(t,e=Date.now()){return new Date(e+sR(t)).toISOString()}function aR(t,e=Date.now()){let r=Date.parse(t);return Number.isNaN(r)?!0:r<=e}function cR(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 lR(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 uR="[REDACTED]",us=class extends Error{constructor(e){super(e),this.name="SandboxDataCopyUnavailableError"}},ou=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 dR(t,e,r){let n={};for(let[o,s]of Object.entries(e)){let i=r[o];if(i===void 0)throw new ou(t,o,"column has no PII classification (unknown = unsafe)");i==="pii"?n[o]=s==null?s:uR:n[o]=s}return n}function pR(t,e,r){return e.map(n=>dR(t,n,r))}var Wy=new Set(["localhost","127.0.0.1","::1","0.0.0.0"]),fR=["oraclecloud.com","us-ashburn-1","amazonaws.com","rds.","supabase.co","turbobase.app","turbobase.io"];function hR(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 su(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 fR)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=hR(t);if(!Wy.has(r))throw new Error(`local-only guard: target host '${r}' is not a recognised local Postgres (${[...Wy].join(", ")}). \`turbobase sandbox create\` is LOCAL-ONLY.`)}function mR(t,e=gR){return`sbx-${t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"tenant"}-${e()}`}function gR(){return Math.floor(Math.random()*16777215).toString(16).padStart(6,"0")}function yR(t){return`turbobase_${t.replace(/[^a-z0-9]+/gi,"_").toLowerCase()}`}function bR(t,e){return`${(e??process.env.TURBOBASE_SANDBOX_BASE_URL??"http://localhost:5173").replace(/\/+$/,"")}/?sandbox=${encodeURIComponent(t)}`}async function iu(){let{file:t}=Jy();try{let e=await nu.readFile(t,"utf8"),r=JSON.parse(e);return Array.isArray(r)?r:[]}catch(e){if(e.code==="ENOENT")return[];throw e}}async function Yy(t){let{dir:e,file:r}=Jy();await nu.mkdir(e,{recursive:!0}),await nu.writeFile(r,`${JSON.stringify(t,null,2)}
|
|
2154
|
-
`,{mode:384})}async function wR(t){let e=await iu();e.push(t),await Yy(e)}async function Xy(t){let e=await iu(),r=e.findIndex(o=>o.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await Yy(e),n??null}async function Xi(){return iu()}function au(t,e=Date.now()){let r=[],n=[];for(let o of t)aR(o.expiresAt,e)?n.push(o):r.push(o);return{active:r,expired:n}}async function Qy(t,e){su(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new us(r);let n=cR(e.sample),o=e.now??Date.now(),s=(e.nameGen??(l=>mR(l)))(e.fromTenant),i=yR(s);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let l of a){let u=lR(l.rows,n),d=pR(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:iR(e.ttl,o),sampleFraction:n,url:bR(s,e.baseUrl)};return await wR(c),c}async function eb(t,e){su(t.target());let n=(await Xi()).find(o=>o.name===e);return n?(await t.dropDatabase(n.database),await Xy(e),n):null}async function cu(t,e=Date.now()){su(t.target());let r=await Xi(),{expired:n}=au(r,e),o=[];for(let s of n){try{await t.dropDatabase(s.database)}catch{continue}await Xy(s.name),o.push(s)}return o}var ds={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","skills:publish","docs:publish","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"}},RZ=Object.keys(ds).filter(t=>ds[t].mintable);var IZ=ds.pat.prefix,_R=Object.entries(ds).map(([t,e])=>[e.prefix,t]),vR=/^[A-Za-z0-9_-]{20,}$/;function xR(t){if(typeof t!="string")return null;for(let[e,r]of _R)if(t.startsWith(e)&&vR.test(t.slice(e.length)))return r;return null}function tb(t){return xR(t)==="pat"}var ps="bundle.json",Qi="turbobase";function nb(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 kR(a)}function kR(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(rb)),tables:[...t.sync.tables].sort(e(r=>r.sourceRecordType))},guardrails:{validationHooks:[...t.guardrails.validationHooks].sort(),fieldRules:[...t.guardrails.fieldRules].sort(e(rb)),conduits:[...t.guardrails.conduits].sort(e(r=>r.conduitId))},schedules:[...t.schedules].sort(e(r=>r.name))}}function rb(t){return`${t.sourceSystem}::${t.recordType}::${t.fieldName}`}function ob(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}")`}}es();var $R=8e3,SR={revoked:"the instance REVOKED this token \u2014 mint a new PAT (Settings \u203A Access tokens) and run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`",expired:"this token has EXPIRED \u2014 mint a new PAT (Settings \u203A Access tokens) and run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`",unknown:"this instance has never seen this token \u2014 it was deleted, or it belongs to a DIFFERENT instance; check the base-url before minting a new PAT",suspended:"the user this token belongs to is SUSPENDED \u2014 an instance administrator has to restore the account; a new token will not help",malformed:"the stored credential is not shaped like a Turbobase PAT (tb_pat_\u2026) \u2014 sign in again",missing:"no token reached the instance \u2014 sign in again",transient:"the instance could not verify the token right now (a storage error, not a decision about the token) \u2014 retry"};function TR(t){return SR[t]??`the instance rejected this token (${t}) \u2014 run \`turbobase auth login\` to store a fresh PAT`}async function ER(t,e,r=fetch){let n;try{n=await r(`${t}/v1/catalog/records`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json"},signal:AbortSignal.timeout($R)})}catch(s){return{state:"unreachable",detail:s instanceof Error?s.message:String(s)}}if(n.status===200||n.status===403)return{state:"accepted"};if(n.status===401){let s=await n.json().catch(()=>null);return{state:"rejected",reason:typeof s?.reason=="string"&&s.reason?s.reason:"unknown"}}return(n.headers.get("content-type")??"").includes("application/json")?{state:"accepted"}:{state:"unreachable",detail:`HTTP ${n.status} \u2014 this host does not serve the /v1 API`}}function sb(t){return t.state==="accepted"?Qe()?g.green("accepted"):"accepted":t.state==="rejected"?Qe()?g.red(`rejected \xB7 ${t.reason}`):`rejected \xB7 ${t.reason}`:Qe()?g.yellow("unreachable"):"unreachable"}async function ib(t={},e=fetch){let r=await kr(),{file:n}=Li();if(r.length===0){x("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),B(g.dim(`no sign-ins stored in ${n}.`));return}let o=await Yt(),s=await Promise.all(r.map(async i=>({baseUrl:i.baseUrl,active:!!o&&i.baseUrl===o.baseUrl,updatedAt:i.updatedAt,check:t.probe===!1?{state:"accepted"}:await ER(i.baseUrl,i.token,e)})));if(t.probe===!1)for(let i of s)x(`${i.active?"* ":" "}${i.baseUrl} (updated ${i.updatedAt})`);else if(Qe())x(Xt(s.map(i=>[i.active?g.green("*"):" ",g.bold(i.baseUrl),sb(i.check),g.dim(i.updatedAt)]),{head:["","instance","token","updated"]}));else for(let i of s)x(`${i.active?"* ":" "}${i.baseUrl} ${sb(i.check)} (updated ${i.updatedAt})`);B(g.dim(`tokens read from ${n}`));for(let i of s)i.check.state==="rejected"?(B(g.red(`${i.baseUrl}: ${TR(i.check.reason)}.`)),B(g.dim(" then re-run `turbobase project init` \u2014 .mcp.json holds its own copy of the token, and until it is rewritten the authoring MCP tools stay absent from your session."))):i.check.state==="unreachable"&&B(g.yellow(`${i.baseUrl}: could not reach the instance (${i.check.detail}).`));o||B("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}async function YL(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 XL(t){let e=await kr();e.length===0&&v("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),ht()||v("this command needs an explicit <base-url> in a non-interactive shell.");try{return await mt(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{v("cancelled.")}}async function QL(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}`}}function eD(t){B(""),B("Sharing is ON. Two things reach the Turbobase product team from this instance:"),B(" \xB7 anonymized pattern cards from your proposals, solutions and INSTANCE-LEARNINGS.md \u2014"),B(" a pattern label, a record class, a fix shape, an outcome. No scripts, values or ids."),B(" \xB7 redacted prose from your projects\u2019 TURBOBASE-FEEDBACK.md \u2014 the findings you write"),B(" about OUR platform, with instance name, user names and ids masked before they leave."),B(`Turn it off: ${t}/admin/settings \u2192 Telemetry \u2192 "Share learnings and platform feedback".`),B("")}async function tD(t,e,r){let n=await Tg({baseUrl:t,token:e}),o=(await kr()).length-1;if(x(`signed in to ${n.baseUrl} \u2014 now the active instance`+(o>0?` (${o} other${o===1?"":"s"} stored; commands default here).`:".")),eD(n.baseUrl),!r)return;let s=await QL(n.baseUrl,e);s.ok||(s.kind==="wrong-host"?(B(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${s.detail}.`),B(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),B(" The token is stored, but commands will fail against this base-url until it points at the API.")):s.kind==="bad-token"?B(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):B(`warning: could not reach ${n.baseUrl} to verify the token (${s.detail}). Stored anyway.`))}async function rD(t){let e=await os(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)&&v(`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)&&v(`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 ye.access(t),"skipped"}catch{}return await ye.mkdir(Uo(t),{recursive:!0}),await ye.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await ye.chmod(t,n),"wrote"}async function X$(t,e){if(Q$(t)!==".gitignore")return Zo(t,e.content,e.executable,e.mode);let r=await ye.readFile(t,"utf8").catch(()=>null);if(r===null)return Zo(t,e.content,e.executable,e.mode);let n=sy(r);return n===null?"skipped":(await ye.writeFile(t,n,"utf8"),"merged")}function $m(t){if(t!==void 0){if(t==="junit"||t==="json")return t;v(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function Hc(t){process.exit(t)}function nD(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 oD(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 sD(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 Am({includeInternal:t=!0}={}){let e=new Wm;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.1.21").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",p=>p.command.parent?"":`${Fl()}
|
|
2154
|
+
`,{mode:384})}async function wR(t){let e=await iu();e.push(t),await Yy(e)}async function Xy(t){let e=await iu(),r=e.findIndex(o=>o.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await Yy(e),n??null}async function Xi(){return iu()}function au(t,e=Date.now()){let r=[],n=[];for(let o of t)aR(o.expiresAt,e)?n.push(o):r.push(o);return{active:r,expired:n}}async function Qy(t,e){su(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new us(r);let n=cR(e.sample),o=e.now??Date.now(),s=(e.nameGen??(l=>mR(l)))(e.fromTenant),i=yR(s);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let l of a){let u=lR(l.rows,n),d=pR(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:iR(e.ttl,o),sampleFraction:n,url:bR(s,e.baseUrl)};return await wR(c),c}async function eb(t,e){su(t.target());let n=(await Xi()).find(o=>o.name===e);return n?(await t.dropDatabase(n.database),await Xy(e),n):null}async function cu(t,e=Date.now()){su(t.target());let r=await Xi(),{expired:n}=au(r,e),o=[];for(let s of n){try{await t.dropDatabase(s.database)}catch{continue}await Xy(s.name),o.push(s)}return o}var ds={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","skills:publish","docs:publish","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"}},RZ=Object.keys(ds).filter(t=>ds[t].mintable);var IZ=ds.pat.prefix,_R=Object.entries(ds).map(([t,e])=>[e.prefix,t]),vR=/^[A-Za-z0-9_-]{20,}$/;function xR(t){if(typeof t!="string")return null;for(let[e,r]of _R)if(t.startsWith(e)&&vR.test(t.slice(e.length)))return r;return null}function tb(t){return xR(t)==="pat"}var ps="bundle.json",Qi="turbobase";function nb(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 kR(a)}function kR(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(rb)),tables:[...t.sync.tables].sort(e(r=>r.sourceRecordType))},guardrails:{validationHooks:[...t.guardrails.validationHooks].sort(),fieldRules:[...t.guardrails.fieldRules].sort(e(rb)),conduits:[...t.guardrails.conduits].sort(e(r=>r.conduitId))},schedules:[...t.schedules].sort(e(r=>r.name))}}function rb(t){return`${t.sourceSystem}::${t.recordType}::${t.fieldName}`}function ob(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}")`}}es();var $R=8e3,SR={revoked:"the instance REVOKED this token \u2014 mint a new PAT (Settings \u203A Access tokens) and run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`",expired:"this token has EXPIRED \u2014 mint a new PAT (Settings \u203A Access tokens) and run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`",unknown:"this instance has never seen this token \u2014 it was deleted, or it belongs to a DIFFERENT instance; check the base-url before minting a new PAT",suspended:"the user this token belongs to is SUSPENDED \u2014 an instance administrator has to restore the account; a new token will not help",malformed:"the stored credential is not shaped like a Turbobase PAT (tb_pat_\u2026) \u2014 sign in again",missing:"no token reached the instance \u2014 sign in again",transient:"the instance could not verify the token right now (a storage error, not a decision about the token) \u2014 retry"};function TR(t){return SR[t]??`the instance rejected this token (${t}) \u2014 run \`turbobase auth login\` to store a fresh PAT`}async function ER(t,e,r=fetch){let n;try{n=await r(`${t}/v1/catalog/records`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json"},signal:AbortSignal.timeout($R)})}catch(s){return{state:"unreachable",detail:s instanceof Error?s.message:String(s)}}if(n.status===200||n.status===403)return{state:"accepted"};if(n.status===401){let s=await n.json().catch(()=>null);return{state:"rejected",reason:typeof s?.reason=="string"&&s.reason?s.reason:"unknown"}}return(n.headers.get("content-type")??"").includes("application/json")?{state:"accepted"}:{state:"unreachable",detail:`HTTP ${n.status} \u2014 this host does not serve the /v1 API`}}function sb(t){return t.state==="accepted"?Qe()?g.green("accepted"):"accepted":t.state==="rejected"?Qe()?g.red(`rejected \xB7 ${t.reason}`):`rejected \xB7 ${t.reason}`:Qe()?g.yellow("unreachable"):"unreachable"}async function ib(t={},e=fetch){let r=await kr(),{file:n}=Li();if(r.length===0){x("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),B(g.dim(`no sign-ins stored in ${n}.`));return}let o=await Yt(),s=await Promise.all(r.map(async i=>({baseUrl:i.baseUrl,active:!!o&&i.baseUrl===o.baseUrl,updatedAt:i.updatedAt,check:t.probe===!1?{state:"accepted"}:await ER(i.baseUrl,i.token,e)})));if(t.probe===!1)for(let i of s)x(`${i.active?"* ":" "}${i.baseUrl} (updated ${i.updatedAt})`);else if(Qe())x(Xt(s.map(i=>[i.active?g.green("*"):" ",g.bold(i.baseUrl),sb(i.check),g.dim(i.updatedAt)]),{head:["","instance","token","updated"]}));else for(let i of s)x(`${i.active?"* ":" "}${i.baseUrl} ${sb(i.check)} (updated ${i.updatedAt})`);B(g.dim(`tokens read from ${n}`));for(let i of s)i.check.state==="rejected"?(B(g.red(`${i.baseUrl}: ${TR(i.check.reason)}.`)),B(g.dim(" then re-run `turbobase project init` \u2014 .mcp.json holds its own copy of the token, and until it is rewritten the authoring MCP tools stay absent from your session."))):i.check.state==="unreachable"&&B(g.yellow(`${i.baseUrl}: could not reach the instance (${i.check.detail}).`));o||B("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}async function YL(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 XL(t){let e=await kr();e.length===0&&v("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),ht()||v("this command needs an explicit <base-url> in a non-interactive shell.");try{return await mt(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{v("cancelled.")}}async function QL(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}`}}function eD(t){B(""),B("Sharing is ON. Two things reach the Turbobase product team from this instance:"),B(" \xB7 anonymized pattern cards from your proposals, solutions and INSTANCE-LEARNINGS.md \u2014"),B(" a pattern label, a record class, a fix shape, an outcome. No scripts, values or ids."),B(" \xB7 redacted prose from your projects\u2019 TURBOBASE-FEEDBACK.md \u2014 the findings you write"),B(" about OUR platform, with instance name, user names and ids masked before they leave."),B(`Turn it off: ${t}/admin/settings \u2192 Telemetry \u2192 "Share learnings and platform feedback".`),B("")}async function tD(t,e,r){let n=await Tg({baseUrl:t,token:e}),o=(await kr()).length-1;if(x(`signed in to ${n.baseUrl} \u2014 now the active instance`+(o>0?` (${o} other${o===1?"":"s"} stored; commands default here).`:".")),eD(n.baseUrl),!r)return;let s=await QL(n.baseUrl,e);s.ok||(s.kind==="wrong-host"?(B(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${s.detail}.`),B(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),B(" The token is stored, but commands will fail against this base-url until it points at the API.")):s.kind==="bad-token"?B(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):B(`warning: could not reach ${n.baseUrl} to verify the token (${s.detail}). Stored anyway.`))}async function rD(t){let e=await os(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)&&v(`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)&&v(`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 ye.access(t),"skipped"}catch{}return await ye.mkdir(Uo(t),{recursive:!0}),await ye.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await ye.chmod(t,n),"wrote"}async function X$(t,e){if(Q$(t)!==".gitignore")return Zo(t,e.content,e.executable,e.mode);let r=await ye.readFile(t,"utf8").catch(()=>null);if(r===null)return Zo(t,e.content,e.executable,e.mode);let n=sy(r);return n===null?"skipped":(await ye.writeFile(t,n,"utf8"),"merged")}function $m(t){if(t!==void 0){if(t==="junit"||t==="json")return t;v(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function Hc(t){process.exit(t)}function nD(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 oD(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 sD(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 Am({includeInternal:t=!0}={}){let e=new Wm;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.1.22").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",p=>p.command.parent?"":`${Fl()}
|
|
2155
2155
|
`),e.hook("preAction",(p,h)=>{ly(e.opts(),h.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 h=Lt(p.baseUrl);p.token||v(`auth login needs --token. Mint a PAT in your Turbobase app (Settings \u203A Access tokens), then:
|
|
2156
2156
|
${is} auth login --base-url ${h} --token tb_pat_\u2026`),tb(p.token)||v("--token does not look like a Turbobase PAT (tb_pat_\u2026)."),await tD(h,p.token,p.verify)}),r.command("status").description("Show every stored sign-in, where its token was read from, and whether the instance still accepts it.").option("--no-probe","List the stored sign-ins without asking each instance about its token.").action(async p=>ib({probe:p.probe})),r.command("use [base-url]").description("Make an already-stored instance the active default (omit the arg to pick interactively).").action(async p=>{let h=p??await XL("Pick the active instance:"),_=await Di(h);if(!_){let S=await kr();v(`no stored sign-in for ${Lt(h)}.`+(S.length?` Stored: ${S.map($=>$.baseUrl).join(", ")}.`:"")+` Run \`turbobase auth login --base-url ${Lt(h)} --token tb_pat_\u2026\`.`)}x(`active instance \u2192 ${_.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 S=await kr();for(let $ of S)await Ol($.baseUrl);x(`removed ${S.length} stored sign-in${S.length===1?"":"s"}.`);return}let h=await Ol(p.baseUrl);h||v(p.baseUrl?`no stored sign-in for ${Lt(p.baseUrl)}.`:"nothing to sign out of (or ambiguous \u2014 pass --base-url / --all).");let _=await Yt();x(`signed out of ${h}.`+(_?` Active instance \u2192 ${_.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:h,token:_}=await re(p.baseUrl),S;try{S=await Ur({baseUrl:h,token:_,recordsOnly:p.recordsOnly})}catch(y){let T=y instanceof Error?y.message:String(y);if(!p.recordsOnly&&/openapi\.json failed \(401/.test(T)){let R=await YL(h,_);if(R)try{S=await Ur({baseUrl:h,token:R,recordsOnly:!1});let C=$e(process.cwd(),p.out);await ye.writeFile(C,S,"utf8"),x(`wrote ${C} (${S.split(`
|
|
2157
2157
|
`).length} lines).`);return}catch{}B("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{S=await Ur({baseUrl:h,token:_,recordsOnly:!0})}catch(C){v(`types generation failed: ${C instanceof Error?C.message:String(C)}`)}}else v(`types generation failed: ${T}`)}let $=$e(process.cwd(),p.out);await ye.writeFile($,S,"utf8"),x(`wrote ${$} (${S.split(`
|
|
@@ -2182,7 +2182,7 @@ samples init: ${R} written, ${C} skipped.`)});let At=e.command("guardrails").des
|
|
|
2182
2182
|
`+g.dim("create one with `turbobase guardrails create <name> --source <s> --record <r>`"));x(Xt($.map(qo),{head:bi})),x(g.dim(" first match wins; lower PRIO is evaluated first."))}),At.command("create <name>").description("Create a rule gating 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("--op <glob>","Raw op glob, when gating something other than a record write.").option("--target <glob>","Row address glob, e.g. 'salesOrder.12345'.").option("--actor <glob>","Only match this caller, e.g. 'agent:*' to gate AI writes but not human ones.").option("--decision <d>","allow | allow_with_audit | require_confirm | deny.","deny").option("--priority <n>","Lower is evaluated first (default 100).",p=>Number(p)).option("--when <expr>","Row predicate, e.g. 'total gt 10000'. Omit to match every row.").option("--reason <text>","Why this rule exists \u2014 shown to a blocked writer and in `list`.").option("--disabled","Create the rule parked, so it takes effect only when activated.").option("--confirm-freeze-all","Required for an unconditional deny across '*'/'*'.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,h)=>{let{baseUrl:_,token:S}=await re(h.baseUrl),$=new X({baseUrl:_,token:S}),y;try{y=await $.guardrailCreate({name:p,source:h.source,record:h.record,decision:h.decision,...h.op?{op:h.op}:{},...h.target?{target:h.target}:{},...h.actor?{actor:h.actor}:{},...Number.isFinite(h.priority)?{priority:Number(h.priority)}:{},...h.when?{when:h.when}:{},...h.reason?{reason:h.reason}:{},...h.disabled?{enabled:!1}:{},...h.confirmFreezeAll?{confirmFreezeAll:!0}:{}})}catch(T){oe(T,"extensions:deploy","create a guardrail"),v(`create guardrail failed: ${T instanceof Error?T.message:String(T)}`)}x(`${g.green("\u2713")} guardrail ${g.bold(y.name??y.id)} created${y.status==="active"?"":g.dim(` (${y.status} \u2014 activate it to take effect)`)}`),x(g.dim(` ${y.decision} ${y.source}/${y.record??y.op}${y.actor?` actor=${y.actor}`:""}${y.when?` where ${y.when}`:""} @prio ${y.priority}`)),x(g.dim(` verify with: turbobase guardrails test --source ${y.source==="*"?"<source>":y.source} --record <record>`))}),At.command("edit <name>").description("Change a rule \u2014 its decision, priority, predicate or reason.").option("--decision <d>","allow | allow_with_audit | require_confirm | deny.").option("--priority <n>","Lower is evaluated first.",p=>Number(p)).option("--when <expr>","New row predicate. Pass '' to match every row.").option("--reason <text>","Why this rule exists.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,h)=>{h.decision===void 0&&h.priority===void 0&&h.when===void 0&&h.reason===void 0&&v("edit: nothing to change \u2014 pass --decision, --priority, --when or --reason.");let{baseUrl:_,token:S}=await re(h.baseUrl),$=new X({baseUrl:_,token:S}),y;try{y=await $.guardrailUpdate(p,{...h.decision!==void 0?{decision:h.decision}:{},...Number.isFinite(h.priority)?{priority:Number(h.priority)}:{},...h.when!==void 0?{when:h.when}:{},...h.reason!==void 0?{reason:h.reason}:{}})}catch(T){oe(T,"extensions:deploy","edit a guardrail"),v(`edit guardrail failed: ${T instanceof Error?T.message:String(T)}`)}x(`${g.green("\u2713")} guardrail ${g.bold(y.name??y.id)} updated`),x(g.dim(` ${y.decision} ${y.source}/${y.record??y.op}${y.when?` where ${y.when}`:""} @prio ${y.priority}`))});for(let[p,h,_]of[["activate","active","activated"],["deactivate","inactive","deactivated"]])At.command(`${p} <name>`).description(h==="active"?"Arm a parked or proposed 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(S,$)=>{let{baseUrl:y,token:T}=await re($.baseUrl),R=new X({baseUrl:y,token:T});try{await R.guardrailUpdate(S,{status:h})}catch(C){oe(C,"extensions:deploy",`${p} a guardrail`),v(`${p} guardrail failed: ${C instanceof Error?C.message:String(C)}`)}x(`${g.green("\u2713")} guardrail ${g.bold(S)} ${_}`),h==="inactive"&&x(g.dim(" writes it was gating now fall through to the other rules."))});At.command("delete <name>").description("Remove a rule. Prefer `deactivate` if you may want it back.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,h)=>{let{baseUrl:_,token:S}=await re(h.baseUrl),$=new X({baseUrl:_,token:S});try{await $.guardrailDelete(p)}catch(y){oe(y,"extensions:deploy","delete a guardrail"),v(`delete guardrail failed: ${y instanceof Error?y.message:String(y)}`)}x(`${g.green("\u2713")} guardrail ${g.bold(p)} deleted`)}),At.command("posture [mode]").description("Show or set the GLOBAL default. 'allow' = deny-list (unmatched writes allowed, carve with deny rules). 'deny' = allow-list (unmatched writes denied, open with allow rules).").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(p,h)=>{let{baseUrl:_,token:S}=await re(h.baseUrl),$=new X({baseUrl:_,token:S}),y=C=>C==="deny"?"deny-by-default (allow-list)":"allow-by-default (deny-list)";if(p===void 0){let C;try{C=await $.guardrailPostureGet()}catch(j){oe(j,"extensions:deploy","read the guardrail posture"),v(`read posture failed: ${j instanceof Error?j.message:String(j)}`)}x(`guardrail posture: ${g.bold(y(C.mode))}`),x(g.dim(C.mode==="deny"?" Unmatched writes are DENIED. Open records with `guardrails create <name> --decision allow`.":" Unmatched writes are allowed-with-audit. Freeze records with `guardrails create <name> --decision deny`."));return}let T=p.trim().toLowerCase();T!=="allow"&&T!=="deny"&&v("posture must be 'allow' or 'deny'.");let R;try{R=await $.guardrailPostureSet(T)}catch(C){oe(C,"extensions:deploy","set the guardrail posture"),v(`set posture failed: ${C instanceof Error?C.message:String(C)}`)}x(`${g.green("\u2713")} guardrail posture set to ${g.bold(y(R.mode))}`),R.mode==="deny"&&x(g.dim(" Every record with no active ALLOW rule is now frozen for write-back. Verify with `guardrails test --source <s> --record <r>`."))}),At.command("test").description("What happens to this write? Runs the real evaluator; writes nothing.").requiredOption("--source <system>","Adapter id the write targets.").requiredOption("--record <type>","Record type the write targets.").option("--actor <actor>","Caller to simulate, e.g. 'agent:ai' or 'user:<id>'.").option("--external-id <id>","Source-side row id, for target-scoped rules.").option("--fields <json>","A candidate row as JSON, so `when` predicates can be decided.").option("--phase <phase>","boundary | cdc | worker | adapter. Defaults to worker (the last gate).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--json","Emit raw JSON.").action(async p=>{let h;if(p.fields)try{h=JSON.parse(p.fields)}catch(R){v(`--fields must be valid JSON: ${R instanceof Error?R.message:String(R)}`)}let{baseUrl:_,token:S}=await re(p.baseUrl),$=new X({baseUrl:_,token:S}),y;try{y=await $.guardrailTest({source:p.source,record:p.record,...p.actor?{actor:p.actor}:{},...p.externalId?{externalId:p.externalId}:{},...h?{fields:h}:{},...p.phase?{phase:p.phase}:{}})}catch(R){oe(R,"extensions:deploy","test guardrails"),v(`guardrails test failed: ${R instanceof Error?R.message:String(R)}`)}if(p.json)return x(JSON.stringify(y,null,2));let T=`${p.source}/${p.record}`;y.degraded?(x(`${g.red("\u2717")} ${T} \u2014 guardrail policy is UNREADABLE; writes fail closed`),x(g.dim(` ${y.reason}`))):y.allowed?x(`${g.green("\u2713")} ${T} \u2014 ${y.decision}`+(y.blockedBy?g.dim(` (rule ${y.blockedBy})`):g.dim(" (no rule matched)"))):(x(`${g.red("\u2717")} ${T} \u2014 ${y.decision} by ${g.bold(y.blockedBy??"?")} at the ${y.phase} gate`),y.message&&x(g.dim(` ${y.message}`))),y.decision==="require_confirm"&&x(g.yellow(" this write is HELD for operator approval, not refused.")),y.allowed||(process.exitCode=1)});let zr=e.command("tables").description("Author custom tables on your instance (create, evolve, inspect)."),Fo=(p,h={})=>{let _=`${g.bold(p.table)} ${g.dim(p.displayName)}`;x(p.exists===!1?`${_} ${g.dim("(new)")}`:_);for(let S of p.fields){let $=[S.required?"required":null].filter(Boolean).join(", "),y=h.showClaimants&&S.claimedBy?.length?` ${g.dim(`\u2190 ${S.claimedBy.join(", ")}`)}`:"";x(` ${S.column.padEnd(28)} ${S.type}${$?` (${$})`:""}${y}`)}h.showClaimants&&p.claimants.length>1&&x(g.dim(` claimed by ${p.claimants.join(", ")}`))};function Gc(p){if(p instanceof pe&&p.status===409){let h=p.body;h?.error==="table_conflict"&&h.message&&(B(h.message),process.exit(1))}v(`tables: ${p instanceof Error?p.message:String(p)}`)}let Im=async p=>{let h=zt($e(process.cwd(),p),"public","turbobase.json"),_=await ye.readFile(h,"utf8").catch(()=>null);if(_===null)return null;let S;try{S=JSON.parse(_)}catch(y){v(`${h} is not valid JSON: ${y instanceof Error?y.message:String(y)}`)}let $=S.tables;return $===void 0?{tables:[],manifestPath:h}:(Array.isArray($)||v(`${h}: "tables" must be an array.`),{tables:$,manifestPath:h})};zr.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:h,token:_}=await re(p.baseUrl),S=new X({baseUrl:h,token:_}),$;try{$=await S.tablesList()}catch(y){oe(y,"extensions:deploy","list tables"),v(`list tables failed: ${y instanceof Error?y.message:String(y)}`)}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(Xt($.map(y=>[y.table,y.displayName,String(y.fields.length),y.status,y.claimedBy.join(", ")||"\u2014"]),{head:["TABLE","NAME","FIELDS","STATUS","CLAIMED BY"]}))}),zr.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,h)=>{let _=await Im(p??".");if(_||v(`no public/turbobase.json under ${$e(process.cwd(),p??".")}.`),_.tables.length===0)return x(`${_.manifestPath} declares no tables \u2014 nothing to plan.`);let{baseUrl:S,token:$}=await re(h.baseUrl),y=new X({baseUrl:S,token:$}),T;try{T=await ie("planning tables",()=>y.tablesPlan({claimedBy:h.as,tables:_.tables}))}catch(R){oe(R,"extensions:deploy","plan tables"),Gc(R)}x(`claiming as ${g.bold(T.claimedBy)}`);for(let R of T.tables)Fo(R,{showClaimants:!0});x(""),x(g.dim("nothing was applied \u2014 run `turbobase tables apply` (or deploy the app) to make it so."))}),zr.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,h)=>{let _=await Im(p??".");if(_||v(`no public/turbobase.json under ${$e(process.cwd(),p??".")}.`),_.tables.length===0)return x(`${_.manifestPath} declares no tables \u2014 nothing to apply.`);let{baseUrl:S,token:$}=await re(h.baseUrl),y=new X({baseUrl:S,token:$}),T;try{T=await ie("applying tables",()=>y.tablesApply({claimedBy:h.as,tables:_.tables}))}catch(R){oe(R,"extensions:deploy","apply tables"),Gc(R)}for(let R of T.tables)Fo(R,{showClaimants:!0});x(`applied ${T.tables.length} table${T.tables.length===1?"":"s"} as ${T.claimedBy}.`)}),zr.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,h)=>[...h??[],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,h)=>{let _=h.field.map(R=>{let[C,j,U]=R.split(":");return(!C||!j)&&v(`--field "${R}" must be name:type[:required] (e.g. title:text:required).`),U!==void 0&&U!=="required"&&v(`--field "${R}": the third part may only be "required".`),{name:C,type:j,...U==="required"?{required:!0}:{}}}),{baseUrl:S,token:$}=await re(h.baseUrl),y=new X({baseUrl:S,token:$}),T;try{T=await ie(`creating cr_${p}`,()=>y.tablesApply({tables:[{slug:p,displayName:h.name??p,fields:_,...h.restrictToOwner?{rowLevelOwner:!0}:{}}]}))}catch(R){oe(R,"extensions:deploy","create a table"),Gc(R)}for(let R of T.tables)Fo(R);x(""),x(g.dim("grant access under Settings \u203A Users, or in the Records page permissions matrix."))}),zr.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,h)=>{h.confirm!==p&&v(`drop refused: --confirm "${h.confirm}" does not match "${p}".`);let{baseUrl:_,token:S}=await re(h.baseUrl),$=new X({baseUrl:_,token:S}),y;try{y=await ie(`dropping cr_${p}`,()=>$.tablesDrop(p,h.confirm))}catch(T){oe(T,"extensions:deploy","drop a table"),v(`drop failed: ${T instanceof Error?T.message:String(T)}`)}if(!y.removed)return x(`no table named "${p}" on this instance \u2014 nothing dropped.`);x(`dropped ${y.table} and every row in it.`)});let Wc=e.command("sandbox").description("Ephemeral local authoring sandboxes.");Wc.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 h=Tm(),_;try{_=await Qy(h,{fromTenant:p.from,ttl:p.ttl,sample:p.sample,baseUrl:p.baseUrl})}catch(S){S instanceof us&&v(S.message),v(`sandbox create failed: ${S instanceof Error?S.message:String(S)}`)}x(`sandbox ${_.name} ready.`),x(` source: ${_.fromTenant} (sampled ${Math.round(_.sampleFraction*100)}%, redacted)`),x(` url: ${_.url}`),x(` expires: ${_.expiresAt}`)}),Wc.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 h=Tm();await cu(h).catch(()=>[]);let _;try{_=await eb(h,p)}catch(S){v(`sandbox destroy failed: ${S instanceof Error?S.message:String(S)}`)}_||v(`no sandbox named '${p}'.`),x(`destroyed sandbox ${_.name} (dropped ${_.database}).`)}),Wc.command("list").description("List active sandboxes with TTL/expiry; reaps any past their TTL.").action(async()=>{let p=Tm(),h=await cu(p).catch(()=>[]),_=await Xi(),{active:S}=au(_);if(S.length===0)x("no active sandboxes.");else{for(let $ of S)x(` ${$.name} from=${$.fromTenant} expires=${$.expiresAt} ${$.url}`);x(`
|
|
2183
2183
|
${S.length} active sandbox(es).`)}h.length>0&&x(`(reaped ${h.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.",Qi).action(async p=>{let{baseUrl:h,token:_}=await re(p.baseUrl),S=new X({baseUrl:h,token:_}),$;try{$=await ie("exporting instance config",()=>S.configExport())}catch(R){R instanceof pe&&(R.status===401||R.status===403)&&v("ext export forbidden \u2014 the stored PAT lacks extensions:deploy. Re-run `turbobase auth login`."),v(`ext export failed: ${R instanceof Error?R.message:String(R)}`)}let y=typeof $.serialized=="string"?$.serialized:`${JSON.stringify($.manifest??$,null,2)}
|
|
2184
2184
|
`,T=$e(process.cwd(),p.out,ps);await ye.mkdir(Uo(T),{recursive:!0}),await ye.writeFile(T,y,"utf8"),x(`wrote ${p.out}/${ps} (${y.split(`
|
|
2185
|
-
`).length} lines).`)});let Om=o.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),eS=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],Cm=()=>process.env.TURBOBASE_HOME?.trim()||zt(JL(),".turbobase"),tS=()=>"1.1.
|
|
2185
|
+
`).length} lines).`)});let Om=o.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),eS=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],Cm=()=>process.env.TURBOBASE_HOME?.trim()||zt(JL(),".turbobase"),tS=()=>"1.1.22",wi=async()=>Promise.all(eS.map(async p=>{try{return await import(p.name),{...p,present:!0}}catch{return{...p,present:!1}}}));Om.command("status").description("Report which optional native packages are installed.").action(async()=>{let p=await wi();for(let h of p)x(`${h.present?Ve.success("installed"):Ve.warn("missing")} ${g.bold(h.name)} \u2014 ${h.needs}`);p.some(h=>!h.present)&&x(`
|
|
2186
2186
|
run \`${is} ext extras install\` to fetch them into ${Cm()}.`)}),Om.command("install").description("Install the optional native packages into $TURBOBASE_HOME (needs npm).").action(async()=>{let p=await wi();if(p.every(C=>C.present)){x(Ve.success("extras already installed \u2014 nothing to do."));return}let h=Cm();await ye.mkdir(h,{recursive:!0});let _=p.filter(C=>!C.present).map(C=>C.name),S=tu(process.platform,process.arch),$=async(C,j)=>{let{code:U,stderr:se}=await yi("npm",["install","--prefix",h,"--no-audit","--no-fund",...C]);U!==0&&v(`extras install failed while installing ${j} (npm exited ${U}).
|
|
2187
2187
|
${se.trim()}
|
|
2188
2188
|
These are native packages \u2014 they need npm and a working toolchain. Everything except \`test\` and \`replay\` works without them.`)},y=_.filter(C=>C!=="isolated-vm");if(y.length>0){let C=y.map(j=>`${j}@${Ql[j]??"latest"}`);x(`installing ${C.join(", ")} \u2192 ${h}/node_modules \u2026`),await $(C,y.join(", "))}if(_.includes("isolated-vm")){let C=`isolated-vm@${Ql["isolated-vm"]}`;if(x(`installing ${C} \u2192 ${h}/node_modules \u2026`),await $(["--ignore-scripts",C],"isolated-vm"),Hy.includes(S)){let U=`${eu}@${tS()}`;x(`fetching ${U} (no upstream binary for ${S}) \u2026`);let{code:se}=await yi("npm",["install","--prefix",h,"--no-audit","--no-fund","--ignore-scripts",U]);se!==0&&x(Ve.warn(`could not fetch ${U} \u2014 falling back to a source build.`))}let j=await Ky({home:h});j.length>0&&x(` ${Ve.success("prebuilt")} ${j.join(", ")} \u2192 isolated-vm/prebuilds/${S}`)}let T=await wi(),R=T.filter(C=>!C.present).map(C=>C.name);if(R.includes("isolated-vm")){x(Ve.warn(`no prebuilt isolated-vm for ${S} / Node ${process.versions.node} (${ru()}) \u2014 building from source. This needs Python and a C++ toolchain, and takes a few minutes.`));let{code:C}=await yi("npm",["rebuild","--prefix",h,"isolated-vm"]);C===0&&(T=await wi(),R=T.filter(j=>!j.present).map(j=>j.name))}R.length>0&&v(`npm succeeded but ${R.join(", ")} still will not load \u2014 no prebuilt binary for ${S} / Node ${process.versions.node} (${ru()}), and the source build did not produce one either. Everything except \`test\` and \`replay\` works without them.`),x(Ve.success("extras installed \u2014 `turbobase ext test` and `turbobase ext replay` are ready."))});let Bn=e.command("release").description("Ship a release to your instance: deploy | status | rollback | list.");return Bn.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 h=p.target==="approved"?"approved":p.target==="auto"?"auto":null;h||v(`--target must be 'auto' or 'approved' (got '${p.target}').`);let _=await lD($e(process.cwd(),p.project??".")),S=Object.keys(_).length;!p.solution&&S===0&&v("release deploy needs --solution <id> (live-fetch) or an SDF project (a src/ dir under --project, default cwd) to upload.");let y=(p.ref?await Em(["rev-parse",p.ref])??p.ref:null)??process.env.GITHUB_SHA??await Em(["rev-parse","HEAD"]),T=p.gitRepo??process.env.GITHUB_REPOSITORY??await Em(["remote","get-url","origin"]),R=p.gitRef??process.env.GITHUB_REF??null,C=p.prUrl??process.env.PR_URL??null,{baseUrl:j,token:U}=await re(p.baseUrl),se=new X({baseUrl:j,token:U}),M;try{M=await se.releaseDeploy({solutionId:p.solution??null,target:h,approvedBy:p.approvedBy??null,files:S>0?_:null,gitRepo:T,gitRef:R,gitSha:y,prUrl:C})}catch(ue){oe(ue,"releases:write","release deploy"),ue instanceof pe&&ue.status===404&&v("solution_not_found \u2014 no solution with that id."),v(`release deploy failed: ${ue instanceof Error?ue.message:String(ue)}`)}x(`release ${M.releaseId} enqueued \u2192 taskId=${M.taskId} (target=${h}`+(S>0?`, uploaded ${S} bundle file(s)`:", live-fetch")+")."),p.follow&&await Gl(()=>se.releaseStatus(M.releaseId),`release ${M.releaseId}`,p.intervalMs)}),Bn.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,h)=>{let{baseUrl:_,token:S}=await re(h.baseUrl),$=new X({baseUrl:_,token:S}),y;try{y=await $.releaseFiles(p)}catch(C){C instanceof pe&&C.status===404&&v(`release ${p} not found.`),C instanceof pe&&C.status===409&&v(`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.`),oe(C,"releases:write","pull a release"),v(`release pull failed: ${C instanceof Error?C.message:String(C)}`)}let T=$e(process.cwd(),h.dir),R=0;for(let[C,j]of[["owned",y.owned],["context",y.context]])for(let[U,se]of Object.entries(j)){let M=zt(T,C,U);await ye.mkdir(Uo(M),{recursive:!0}),await ye.writeFile(M,se,"utf8"),R++}await ye.writeFile(zt(T,".turbobase-release.json"),JSON.stringify({releaseId:y.releaseId,version:y.version,contentDigest:y.contentDigest},null,2),"utf8"),x(`pulled release ${y.releaseId} (${y.status}) \u2192 ${T}
|
package/dist/turbobase.js
CHANGED
|
@@ -2104,7 +2104,7 @@ ${s.map(Jb).join(`
|
|
|
2104
2104
|
`))}async function zu(t,e,n=En){let r=[...new Set(t.map(a=>a.trim()).filter(Boolean))];r.length===0&&b("name at least one skill slug. `turbobase skills list` shows them.");let s=await Pn(e,n);if(!await Ns(`Publish ${r.length} skill(s)?`,"Yes \u2014 publish them","their text is injected into every matching turn from now on",e))return;let i;try{i=await J("publishing",()=>s.skillsPublish(r))}catch(a){G(a,"skills:publish","publish a skill"),b(`publish failed: ${a instanceof Error?a.message:String(a)}`)}w(h.green(`published ${i.published.length} skill(s) \u2014 live now.`)),i.unknown.length>0&&(D(h.yellow(`no such skill: ${i.unknown.join(", ")}`)),process.exitCode=1),D(h.dim(`release v${i.version.version_no} \xB7 undo with: turbobase skills unpublish ${i.published.join(" ")}`))}async function qu(t,e,n=En){let r=[...new Set(t.map(a=>a.trim()).filter(Boolean))];r.length===0&&b("name at least one skill slug. `turbobase skills list` shows them.");let s=await Pn(e,n);if(!await Ns(`Move ${r.length} skill(s) back to draft?`,"Yes \u2014 unpublish them","they stop being injected immediately; the text and its history are kept",e))return;let i;try{i=await J("unpublishing",()=>s.skillsUnpublish(r))}catch(a){G(a,"skills:publish","unpublish a skill"),b(`unpublish failed: ${a instanceof Error?a.message:String(a)}`)}w(h.green(`unpublished ${i.unpublished.length} skill(s) \u2014 retracted now.`)),D(h.dim(`release v${i.version.version_no} \xB7 re-publish with: turbobase skills publish ${i.unpublished.join(" ")}`)),i.unknown.length>0&&(D(h.yellow(`no such skill: ${i.unknown.join(", ")}`)),process.exitCode=1)}async function Mu(t,e,n=En){let r=Number(t);(!Number.isInteger(r)||r<1)&&b(`--version must be a release number (got '${t}'). \`turbobase skills releases\` lists them.`);let s=await Pn(e,n);if(await Ns(`Roll the skills back to release v${r}?`,"Yes \u2014 restore that release","content only: a skill an operator has since unpublished stays off",e))try{let i=await J("rolling back",()=>s.skillsRollback(r));w(h.green(`rolled back to v${i.version.version_no}.`))}catch(i){G(i,"skills:publish","roll back a skills release"),b(`rollback failed: ${i instanceof Error?i.message:String(i)}`)}}async function Bu(t,e=En){let n=await Pn(t,e),r;try{r=await J("reading the releases",()=>n.skillsReleases())}catch(s){G(s,"skills:publish","read the skills releases"),b(`could not read the releases: ${s instanceof Error?s.message:String(s)}`)}if(r.items.length===0){D("no releases yet.");return}for(let s of r.items){let o=s.is_current?h.green("*"):" ",i=s.release_note?` \u2014 ${s.release_note}`:"";w(`${o} v${s.version_no} ${s.skill_count} skill(s) ${h.dim(s.published_at)}${s.published_by?h.dim(` by ${s.published_by}`):""}${h.dim(i)}`)}}async function Fu(t,e,n=En){let r=[...new Set((t??[]).map(p=>p.trim()).filter(Boolean))];r.length===0&&b("name at least one skill to delete. `turbobase skills list` shows the slugs.");let s=await Pn(e,n);if(!await Ns(r.length===1?`Delete the skill '${r[0]}'?`:`Delete ${r.length} skills?
|
|
2105
2105
|
${r.join(`
|
|
2106
2106
|
`)}`,"Yes \u2014 delete them","permanent: the rows and their text are removed. Prior releases keep their own copy.",e))return;let i=[],a=[];for(let p of r)try{i.push(await J(`deleting ${p}`,()=>s.skillsDelete(p)))}catch(u){if(u instanceof X&&u.status===404){a.push(p);continue}i.length>0&&D(h.dim(`deleted before the error: ${i.map(m=>m.slug).join(", ")}`)),G(u,"skills:publish","delete a skill"),b(`delete failed on '${p}': ${u instanceof Error?u.message:String(u)}`)}i.length>0&&w(h.green(`deleted ${i.length} skill(s).`));let l=i.filter(p=>p.wasPublished).map(p=>p.slug);l.length>0&&D(h.yellow(`Published until now, so no longer injected: ${l.join(", ")}`)),a.length>0&&(D(h.yellow(`no such skill: ${a.join(", ")}`)),process.exitCode=1)}import{promises as Hu}from"node:fs";import{join as Sw,relative as _w,resolve as $w,sep as xw}from"node:path";var Rw=new Set(["node_modules","dist","build","coverage",".git",".claude",".turbobase",".next",".svelte-kit",".vercel",".cache"]),Tw=new Set([".mcp.json",".env"]);function Ew(t){return Tw.has(t)||t.startsWith(".env.")&&t!==".env.example"}function Pw(t){return Buffer.compare(Buffer.from(t.toString("utf8"),"utf8"),t)===0}function Aw(t){return t.includes(0)}async function Ku(t){let e=$w(t),n={},r=[],s=[],o=[];async function i(a){let l=await Hu.readdir(a,{withFileTypes:!0});for(let p of l){let u=Sw(a,p.name);if(p.isDirectory()){if(Rw.has(p.name))continue;await i(u);continue}if(!p.isFile())continue;let m=_w(e,u).split(xw).join("/");if(Ew(p.name)){s.push(m);continue}let x=await Hu.readFile(u);if(!Pw(x)){r.push(m);continue}Aw(x)&&o.push(m),n[m]=x.toString("utf8")}}return await i(e),{files:n,skipped:r,secrets:s,unstorable:o}}import{randomBytes as Iw}from"node:crypto";var Cw="/_tb/app-assets",Ow=16,Nw=/^[0-9a-f]{32}$/;function Vu(){return Iw(Ow).toString("hex")}function jw(t){return Nw.test(t)}function Gu(t){if(!jw(t))throw new Error(`not a build id: ${t}`);return`${Cw}/${t}/`}var Wu=7;import js from"node:fs/promises";import br from"node:path";var Ji={esbuild:"0.28.1","isolated-vm":"6.1.2"},Zi="@turbobase/isolated-vm-prebuilds";function Yi(t,e){return`${t}-${e}`}function Qi(t=process.versions){return`abi${t.modules}`}var Ju=["darwin-x64","win32-arm64"];async function Zu(t){let e=t.platform??process.platform,n=t.arch??process.arch,r=Yi(e,n),s=br.join(t.home,"node_modules"),o=br.join(s,...Zi.split("/"),"prebuilds",r),i=br.join(s,"isolated-vm","prebuilds",r),a;try{a=await js.readdir(o)}catch{return[]}let l=[];await js.mkdir(i,{recursive:!0});for(let p of a){if(!p.endsWith(".node"))continue;let u=br.join(i,p);try{await js.copyFile(br.join(o,p),u,js.constants?.COPYFILE_EXCL??1),l.push(p)}catch{}}return l}import{homedir as Lw}from"node:os";import{join as Yu}from"node:path";import{promises as Xi}from"node:fs";function Xu(){let t=process.env.TURBOBASE_HOME??Yu(Lw(),".turbobase");return{dir:t,file:Yu(t,"sandboxes.json")}}var Dw={s:1e3,m:6e4,h:36e5,d:864e5};function Uw(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 n=Number(e[1]);if(!Number.isFinite(n)||n<=0)throw new Error(`invalid --ttl '${t}': duration must be a positive integer.`);return n*Dw[e[2]]}function zw(t,e=Date.now()){return new Date(e+Uw(t)).toISOString()}function qw(t,e=Date.now()){let n=Date.parse(t);return Number.isNaN(n)?!0:n<=e}function Mw(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 n=Number(e[1]);if(!Number.isFinite(n)||n<0||n>100)throw new Error(`invalid --sample '${t}': percent must be between 0 and 100.`);return n/100}function Bw(t,e){if(e>=1)return[...t];if(e<=0||t.length===0)return[];let n=Math.max(1,Math.round(1/e)),r=[];for(let s=0;s<t.length;s+=n)r.push(t[s]);return r}var Fw="[REDACTED]",wr=class extends Error{constructor(e){super(e),this.name="SandboxDataCopyUnavailableError"}},ea=class extends Error{table;column;constructor(e,n,r){super(`Refusing to copy ${e}.${n} into a sandbox: ${r}. 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=n}};function Hw(t,e,n){let r={};for(let[s,o]of Object.entries(e)){let i=n[s];if(i===void 0)throw new ea(t,s,"column has no PII classification (unknown = unsafe)");i==="pii"?r[s]=o==null?o:Fw:r[s]=o}return r}function Kw(t,e,n){return e.map(r=>Hw(t,r,n))}var Qu=new Set(["localhost","127.0.0.1","::1","0.0.0.0"]),Vw=["oraclecloud.com","us-ashburn-1","amazonaws.com","rds.","supabase.co","turbobase.app","turbobase.io"];function Gw(t){let e=t.trim();try{let s=new URL(e);if(s.hostname)return s.hostname.toLowerCase()}catch{}let n=e.replace(/^[a-z]+:\/\//i,""),r=(n.split("/")[0]??n).split("@").pop()??n;return(r.split(":")[0]??r).toLowerCase()}function ta(t){if(!t||!t.trim())throw new Error("local-only guard: empty database target \u2014 refusing to proceed.");let e=t.toLowerCase();for(let r of Vw)if(e.includes(r))throw new Error(`local-only guard: target '${t}' looks like a cloud/prod context (matched '${r}'). \`turbobase sandbox create\` is LOCAL-ONLY and will never touch a cloud database.`);let n=Gw(t);if(!Qu.has(n))throw new Error(`local-only guard: target host '${n}' is not a recognised local Postgres (${[...Qu].join(", ")}). \`turbobase sandbox create\` is LOCAL-ONLY.`)}function Ww(t,e=Jw){return`sbx-${t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"tenant"}-${e()}`}function Jw(){return Math.floor(Math.random()*16777215).toString(16).padStart(6,"0")}function Zw(t){return`turbobase_${t.replace(/[^a-z0-9]+/gi,"_").toLowerCase()}`}function Yw(t,e){return`${(e??process.env.TURBOBASE_SANDBOX_BASE_URL??"http://localhost:5173").replace(/\/+$/,"")}/?sandbox=${encodeURIComponent(t)}`}async function na(){let{file:t}=Xu();try{let e=await Xi.readFile(t,"utf8"),n=JSON.parse(e);return Array.isArray(n)?n:[]}catch(e){if(e.code==="ENOENT")return[];throw e}}async function ep(t){let{dir:e,file:n}=Xu();await Xi.mkdir(e,{recursive:!0}),await Xi.writeFile(n,`${JSON.stringify(t,null,2)}
|
|
2107
|
-
`,{mode:384})}async function Qw(t){let e=await na();e.push(t),await ep(e)}async function tp(t){let e=await na(),n=e.findIndex(s=>s.name===t);if(n===-1)return null;let[r]=e.splice(n,1);return await ep(e),r??null}async function Ls(){return na()}function ra(t,e=Date.now()){let n=[],r=[];for(let s of t)qw(s.expiresAt,e)?r.push(s):n.push(s);return{active:n,expired:r}}async function np(t,e){ta(t.target());let n=t.dataCopyUnavailable?.();if(n)throw new wr(n);let r=Mw(e.sample),s=e.now??Date.now(),o=(e.nameGen??(p=>Ww(p)))(e.fromTenant),i=Zw(o);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let p of a){let u=Bw(p.rows,r),m=Kw(p.table,u,p.classification);m.length>0&&await t.copyRows(i,p.table,m)}let l={name:o,fromTenant:e.fromTenant,database:i,createdAt:new Date(s).toISOString(),expiresAt:zw(e.ttl,s),sampleFraction:r,url:Yw(o,e.baseUrl)};return await Qw(l),l}async function rp(t,e){ta(t.target());let r=(await Ls()).find(s=>s.name===e);return r?(await t.dropDatabase(r.database),await tp(e),r):null}async function sa(t,e=Date.now()){ta(t.target());let n=await Ls(),{expired:r}=ra(n,e),s=[];for(let o of r){try{await t.dropDatabase(o.database)}catch{continue}await tp(o.name),s.push(o)}return s}var vr={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","skills:publish","docs:publish","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"}},pA=Object.keys(vr).filter(t=>vr[t].mintable);var mA=vr.pat.prefix,Xw=Object.entries(vr).map(([t,e])=>[e.prefix,t]),ev=/^[A-Za-z0-9_-]{20,}$/;function tv(t){if(typeof t!="string")return null;for(let[e,n]of Xw)if(t.startsWith(e)&&ev.test(t.slice(e.length)))return n;return null}function sp(t){return tv(t)==="pat"}var kr="bundle.json",Ds="turbobase";function ip(t){let e;try{e=JSON.parse(t)}catch(l){throw new Error(`bundle.json is not valid JSON: ${l instanceof Error?l.message:String(l)}`)}if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("bundle.json must be a JSON object");let n=e,r=typeof n.version=="number"?n.version:0;if(r!==1)throw new Error(`bundle.json version ${r} is unsupported (expected 1)`);let s=l=>Array.isArray(l)?l:[],o=n.sync??{},i=n.guardrails??{},a={version:1,...typeof n.tenant=="string"?{tenant:n.tenant}:{},hooks:s(n.hooks),conduits:s(n.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(n.schedules)};return nv(a)}function nv(t){let e=n=>(r,s)=>n(r)<n(s)?-1:n(r)>n(s)?1:0;return{version:1,...t.tenant?{tenant:t.tenant}:{},hooks:[...t.hooks].sort(e(n=>n.id)),conduits:[...t.conduits].sort(e(n=>n.id)),sync:{fields:[...t.sync.fields].sort(e(op)),tables:[...t.sync.tables].sort(e(n=>n.sourceRecordType))},guardrails:{validationHooks:[...t.guardrails.validationHooks].sort(),fieldRules:[...t.guardrails.fieldRules].sort(e(op)),conduits:[...t.guardrails.conduits].sort(e(n=>n.conduitId))},schedules:[...t.schedules].sort(e(n=>n.name))}}function op(t){return`${t.sourceSystem}::${t.recordType}::${t.fieldName}`}function ap(t,e){let n=(e??"").trim();if(!n)return{ok:!1,reason:"target instance slug is unknown \u2014 cannot confirm a promote"};let r=(t??"").trim();return r?r!==n?{ok:!1,reason:`--confirm "${r}" does not match the target instance "${n}" \u2014 refusing to apply`}:{ok:!0}:{ok:!1,reason:`--confirm <slug> is required for promote (expected "${n}")`}}cr();var rv=8e3,sv={revoked:"the instance REVOKED this token \u2014 mint a new PAT (Settings \u203A Access tokens) and run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`",expired:"this token has EXPIRED \u2014 mint a new PAT (Settings \u203A Access tokens) and run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`",unknown:"this instance has never seen this token \u2014 it was deleted, or it belongs to a DIFFERENT instance; check the base-url before minting a new PAT",suspended:"the user this token belongs to is SUSPENDED \u2014 an instance administrator has to restore the account; a new token will not help",malformed:"the stored credential is not shaped like a Turbobase PAT (tb_pat_\u2026) \u2014 sign in again",missing:"no token reached the instance \u2014 sign in again",transient:"the instance could not verify the token right now (a storage error, not a decision about the token) \u2014 retry"};function ov(t){return sv[t]??`the instance rejected this token (${t}) \u2014 run \`turbobase auth login\` to store a fresh PAT`}async function iv(t,e,n=fetch){let r;try{r=await n(`${t}/v1/catalog/records`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json"},signal:AbortSignal.timeout(rv)})}catch(o){return{state:"unreachable",detail:o instanceof Error?o.message:String(o)}}if(r.status===200||r.status===403)return{state:"accepted"};if(r.status===401){let o=await r.json().catch(()=>null);return{state:"rejected",reason:typeof o?.reason=="string"&&o.reason?o.reason:"unknown"}}return(r.headers.get("content-type")??"").includes("application/json")?{state:"accepted"}:{state:"unreachable",detail:`HTTP ${r.status} \u2014 this host does not serve the /v1 API`}}function lp(t){return t.state==="accepted"?Te()?h.green("accepted"):"accepted":t.state==="rejected"?Te()?h.red(`rejected \xB7 ${t.reason}`):`rejected \xB7 ${t.reason}`:Te()?h.yellow("unreachable"):"unreachable"}async function cp(t={},e=fetch){let n=await Rt(),{file:r}=ks();if(n.length===0){w("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),D(h.dim(`no sign-ins stored in ${r}.`));return}let s=await ut(),o=await Promise.all(n.map(async i=>({baseUrl:i.baseUrl,active:!!s&&i.baseUrl===s.baseUrl,updatedAt:i.updatedAt,check:t.probe===!1?{state:"accepted"}:await iv(i.baseUrl,i.token,e)})));if(t.probe===!1)for(let i of o)w(`${i.active?"* ":" "}${i.baseUrl} (updated ${i.updatedAt})`);else if(Te())w(pt(o.map(i=>[i.active?h.green("*"):" ",h.bold(i.baseUrl),lp(i.check),h.dim(i.updatedAt)]),{head:["","instance","token","updated"]}));else for(let i of o)w(`${i.active?"* ":" "}${i.baseUrl} ${lp(i.check)} (updated ${i.updatedAt})`);D(h.dim(`tokens read from ${r}`));for(let i of o)i.check.state==="rejected"?(D(h.red(`${i.baseUrl}: ${ov(i.check.reason)}.`)),D(h.dim(" then re-run `turbobase project init` \u2014 .mcp.json holds its own copy of the token, and until it is rewritten the authoring MCP tools stay absent from your session."))):i.check.state==="unreachable"&&D(h.yellow(`${i.baseUrl}: could not reach the instance (${i.check.detail}).`));s||D("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}async function $E(t,e){try{let n=await fetch(`${t.replace(/\/+$/,"")}/auth/pat-exchange`,{method:"POST",headers:{Authorization:`Bearer ${e}`}});if(!n.ok)return null;let r=await n.json();return typeof r.token=="string"&&r.token.length>0?r.token:null}catch{return null}}async function xE(t){let e=await Rt();e.length===0&&b("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),ze()||b("this command needs an explicit <base-url> in a non-interactive shell.");try{return await qe(t,e.map(n=>({value:n.baseUrl,label:n.baseUrl,description:`updated ${n.updatedAt}`})))}catch{b("cancelled.")}}async function RE(t,e){let n;try{n=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 r=(n.headers.get("content-type")??"").includes("application/json");return n.status===200||n.status===403?{ok:!0}:n.status===401?{ok:!1,kind:"bad-token",detail:`HTTP ${n.status}`}:r?{ok:!0}:{ok:!1,kind:"wrong-host",detail:`HTTP ${n.status}`}}function TE(t){D(""),D("Sharing is ON. Two things reach the Turbobase product team from this instance:"),D(" \xB7 anonymized pattern cards from your proposals, solutions and INSTANCE-LEARNINGS.md \u2014"),D(" a pattern label, a record class, a fix shape, an outcome. No scripts, values or ids."),D(" \xB7 redacted prose from your projects\u2019 TURBOBASE-FEEDBACK.md \u2014 the findings you write"),D(" about OUR platform, with instance name, user names and ids masked before they leave."),D(`Turn it off: ${t}/admin/settings \u2192 Telemetry \u2192 "Share learnings and platform feedback".`),D("")}async function EE(t,e,n){let r=await Pd({baseUrl:t,token:e}),s=(await Rt()).length-1;if(w(`signed in to ${r.baseUrl} \u2014 now the active instance`+(s>0?` (${s} other${s===1?"":"s"} stored; commands default here).`:".")),TE(r.baseUrl),!n)return;let o=await RE(r.baseUrl,e);o.ok||(o.kind==="wrong-host"?(D(`warning: ${r.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${o.detail}.`),D(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),D(" The token is stored, but commands will fail against this base-url until it points at the API.")):o.kind==="bad-token"?D(`warning: ${r.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):D(`warning: could not reach ${r.baseUrl} to verify the token (${o.detail}). Stored anyway.`))}async function PE(t){let e=await fr(t),n=l=>{let p=new RegExp(`${l}\\s*:\\s*(['"\`])([^'"\`]+)\\1`).exec(e);return p?p[2]:void 0},r=/\bdefineConduit\s*[<(]/.test(e),s=n("id");if(r){let l=n("on"),p=n("recordType"),u=n("source");return(!s||!p||!l||!u)&&b(`could not parse id/on/from.source/from.recordType from ${t} \u2014 is it a defineConduit({...}) file?`),{id:s,recordType:p,event:l,source:u,sourceCode:e,extensionKind:"conduit"}}let o=n("recordType"),i=n("event"),a=n("source")??"standalone";return(!s||!o||!i)&&b(`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 nr(t,e,n,r){try{return await re.access(t),"skipped"}catch{}return await re.mkdir(tr(t),{recursive:!0}),await re.writeFile(t,e,{mode:r??(n?493:420)}),r!==void 0&&await re.chmod(t,r),"wrote"}async function ry(t,e){if(sy(t)!==".gitignore")return nr(t,e.content,e.executable,e.mode);let n=await re.readFile(t,"utf8").catch(()=>null);if(n===null)return nr(t,e.content,e.executable,e.mode);let r=lu(n);return r===null?"skipped":(await re.writeFile(t,r,"utf8"),"merged")}function td(t){if(t!==void 0){if(t==="junit"||t==="json")return t;b(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function li(t){process.exit(t)}function AE(t){return{name:"turbobase ext test",cases:t.map(n=>{if(n.pass)return{name:n.name,status:"passed"};let r=n.error?"error":n.rejection?.code??"mismatch",s=n.error?`error: ${n.error}`:n.observed!==n.expected?`expected ${n.expected}, observed ${n.observed}`+(n.rejection?` (rejected: ${n.rejection.code??"rejected"}${n.rejection.message?` \u2014 ${n.rejection.message}`:""})`:""):`rejected: ${n.rejection?.code??"rejected"}${n.rejection?.message?` \u2014 ${n.rejection.message}`:""}`;return{name:n.name,status:"failed",detail:s,type:r}})}}function IE(t,e){let n=e.warnings.length===0?[{name:`${t} (${e.kind})`,status:"passed"}]:e.warnings.map(r=>({name:`${t}: ${r.rule}`,status:"failed",type:r.rule,detail:`${r.message} \u2192 fix: ${r.fix}`}));return{name:`turbobase ext lint ${t}`,cases:n}}function CE(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 n of t.guards)e.push(n.status==="failed"?{name:`guard: ${n.name}`,status:"failed",type:n.name,detail:n.detail}:{name:`guard: ${n.name}`,status:"passed",detail:n.status==="skipped-offline"?`skipped-offline: ${n.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 ad({includeInternal:t=!0}={}){let e=new _E;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.1.21").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",d=>d.command.parent?"":`${zi()}
|
|
2107
|
+
`,{mode:384})}async function Qw(t){let e=await na();e.push(t),await ep(e)}async function tp(t){let e=await na(),n=e.findIndex(s=>s.name===t);if(n===-1)return null;let[r]=e.splice(n,1);return await ep(e),r??null}async function Ls(){return na()}function ra(t,e=Date.now()){let n=[],r=[];for(let s of t)qw(s.expiresAt,e)?r.push(s):n.push(s);return{active:n,expired:r}}async function np(t,e){ta(t.target());let n=t.dataCopyUnavailable?.();if(n)throw new wr(n);let r=Mw(e.sample),s=e.now??Date.now(),o=(e.nameGen??(p=>Ww(p)))(e.fromTenant),i=Zw(o);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let p of a){let u=Bw(p.rows,r),m=Kw(p.table,u,p.classification);m.length>0&&await t.copyRows(i,p.table,m)}let l={name:o,fromTenant:e.fromTenant,database:i,createdAt:new Date(s).toISOString(),expiresAt:zw(e.ttl,s),sampleFraction:r,url:Yw(o,e.baseUrl)};return await Qw(l),l}async function rp(t,e){ta(t.target());let r=(await Ls()).find(s=>s.name===e);return r?(await t.dropDatabase(r.database),await tp(e),r):null}async function sa(t,e=Date.now()){ta(t.target());let n=await Ls(),{expired:r}=ra(n,e),s=[];for(let o of r){try{await t.dropDatabase(o.database)}catch{continue}await tp(o.name),s.push(o)}return s}var vr={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","skills:publish","docs:publish","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"}},pA=Object.keys(vr).filter(t=>vr[t].mintable);var mA=vr.pat.prefix,Xw=Object.entries(vr).map(([t,e])=>[e.prefix,t]),ev=/^[A-Za-z0-9_-]{20,}$/;function tv(t){if(typeof t!="string")return null;for(let[e,n]of Xw)if(t.startsWith(e)&&ev.test(t.slice(e.length)))return n;return null}function sp(t){return tv(t)==="pat"}var kr="bundle.json",Ds="turbobase";function ip(t){let e;try{e=JSON.parse(t)}catch(l){throw new Error(`bundle.json is not valid JSON: ${l instanceof Error?l.message:String(l)}`)}if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("bundle.json must be a JSON object");let n=e,r=typeof n.version=="number"?n.version:0;if(r!==1)throw new Error(`bundle.json version ${r} is unsupported (expected 1)`);let s=l=>Array.isArray(l)?l:[],o=n.sync??{},i=n.guardrails??{},a={version:1,...typeof n.tenant=="string"?{tenant:n.tenant}:{},hooks:s(n.hooks),conduits:s(n.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(n.schedules)};return nv(a)}function nv(t){let e=n=>(r,s)=>n(r)<n(s)?-1:n(r)>n(s)?1:0;return{version:1,...t.tenant?{tenant:t.tenant}:{},hooks:[...t.hooks].sort(e(n=>n.id)),conduits:[...t.conduits].sort(e(n=>n.id)),sync:{fields:[...t.sync.fields].sort(e(op)),tables:[...t.sync.tables].sort(e(n=>n.sourceRecordType))},guardrails:{validationHooks:[...t.guardrails.validationHooks].sort(),fieldRules:[...t.guardrails.fieldRules].sort(e(op)),conduits:[...t.guardrails.conduits].sort(e(n=>n.conduitId))},schedules:[...t.schedules].sort(e(n=>n.name))}}function op(t){return`${t.sourceSystem}::${t.recordType}::${t.fieldName}`}function ap(t,e){let n=(e??"").trim();if(!n)return{ok:!1,reason:"target instance slug is unknown \u2014 cannot confirm a promote"};let r=(t??"").trim();return r?r!==n?{ok:!1,reason:`--confirm "${r}" does not match the target instance "${n}" \u2014 refusing to apply`}:{ok:!0}:{ok:!1,reason:`--confirm <slug> is required for promote (expected "${n}")`}}cr();var rv=8e3,sv={revoked:"the instance REVOKED this token \u2014 mint a new PAT (Settings \u203A Access tokens) and run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`",expired:"this token has EXPIRED \u2014 mint a new PAT (Settings \u203A Access tokens) and run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`",unknown:"this instance has never seen this token \u2014 it was deleted, or it belongs to a DIFFERENT instance; check the base-url before minting a new PAT",suspended:"the user this token belongs to is SUSPENDED \u2014 an instance administrator has to restore the account; a new token will not help",malformed:"the stored credential is not shaped like a Turbobase PAT (tb_pat_\u2026) \u2014 sign in again",missing:"no token reached the instance \u2014 sign in again",transient:"the instance could not verify the token right now (a storage error, not a decision about the token) \u2014 retry"};function ov(t){return sv[t]??`the instance rejected this token (${t}) \u2014 run \`turbobase auth login\` to store a fresh PAT`}async function iv(t,e,n=fetch){let r;try{r=await n(`${t}/v1/catalog/records`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json"},signal:AbortSignal.timeout(rv)})}catch(o){return{state:"unreachable",detail:o instanceof Error?o.message:String(o)}}if(r.status===200||r.status===403)return{state:"accepted"};if(r.status===401){let o=await r.json().catch(()=>null);return{state:"rejected",reason:typeof o?.reason=="string"&&o.reason?o.reason:"unknown"}}return(r.headers.get("content-type")??"").includes("application/json")?{state:"accepted"}:{state:"unreachable",detail:`HTTP ${r.status} \u2014 this host does not serve the /v1 API`}}function lp(t){return t.state==="accepted"?Te()?h.green("accepted"):"accepted":t.state==="rejected"?Te()?h.red(`rejected \xB7 ${t.reason}`):`rejected \xB7 ${t.reason}`:Te()?h.yellow("unreachable"):"unreachable"}async function cp(t={},e=fetch){let n=await Rt(),{file:r}=ks();if(n.length===0){w("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),D(h.dim(`no sign-ins stored in ${r}.`));return}let s=await ut(),o=await Promise.all(n.map(async i=>({baseUrl:i.baseUrl,active:!!s&&i.baseUrl===s.baseUrl,updatedAt:i.updatedAt,check:t.probe===!1?{state:"accepted"}:await iv(i.baseUrl,i.token,e)})));if(t.probe===!1)for(let i of o)w(`${i.active?"* ":" "}${i.baseUrl} (updated ${i.updatedAt})`);else if(Te())w(pt(o.map(i=>[i.active?h.green("*"):" ",h.bold(i.baseUrl),lp(i.check),h.dim(i.updatedAt)]),{head:["","instance","token","updated"]}));else for(let i of o)w(`${i.active?"* ":" "}${i.baseUrl} ${lp(i.check)} (updated ${i.updatedAt})`);D(h.dim(`tokens read from ${r}`));for(let i of o)i.check.state==="rejected"?(D(h.red(`${i.baseUrl}: ${ov(i.check.reason)}.`)),D(h.dim(" then re-run `turbobase project init` \u2014 .mcp.json holds its own copy of the token, and until it is rewritten the authoring MCP tools stay absent from your session."))):i.check.state==="unreachable"&&D(h.yellow(`${i.baseUrl}: could not reach the instance (${i.check.detail}).`));s||D("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}async function $E(t,e){try{let n=await fetch(`${t.replace(/\/+$/,"")}/auth/pat-exchange`,{method:"POST",headers:{Authorization:`Bearer ${e}`}});if(!n.ok)return null;let r=await n.json();return typeof r.token=="string"&&r.token.length>0?r.token:null}catch{return null}}async function xE(t){let e=await Rt();e.length===0&&b("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),ze()||b("this command needs an explicit <base-url> in a non-interactive shell.");try{return await qe(t,e.map(n=>({value:n.baseUrl,label:n.baseUrl,description:`updated ${n.updatedAt}`})))}catch{b("cancelled.")}}async function RE(t,e){let n;try{n=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 r=(n.headers.get("content-type")??"").includes("application/json");return n.status===200||n.status===403?{ok:!0}:n.status===401?{ok:!1,kind:"bad-token",detail:`HTTP ${n.status}`}:r?{ok:!0}:{ok:!1,kind:"wrong-host",detail:`HTTP ${n.status}`}}function TE(t){D(""),D("Sharing is ON. Two things reach the Turbobase product team from this instance:"),D(" \xB7 anonymized pattern cards from your proposals, solutions and INSTANCE-LEARNINGS.md \u2014"),D(" a pattern label, a record class, a fix shape, an outcome. No scripts, values or ids."),D(" \xB7 redacted prose from your projects\u2019 TURBOBASE-FEEDBACK.md \u2014 the findings you write"),D(" about OUR platform, with instance name, user names and ids masked before they leave."),D(`Turn it off: ${t}/admin/settings \u2192 Telemetry \u2192 "Share learnings and platform feedback".`),D("")}async function EE(t,e,n){let r=await Pd({baseUrl:t,token:e}),s=(await Rt()).length-1;if(w(`signed in to ${r.baseUrl} \u2014 now the active instance`+(s>0?` (${s} other${s===1?"":"s"} stored; commands default here).`:".")),TE(r.baseUrl),!n)return;let o=await RE(r.baseUrl,e);o.ok||(o.kind==="wrong-host"?(D(`warning: ${r.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${o.detail}.`),D(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),D(" The token is stored, but commands will fail against this base-url until it points at the API.")):o.kind==="bad-token"?D(`warning: ${r.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):D(`warning: could not reach ${r.baseUrl} to verify the token (${o.detail}). Stored anyway.`))}async function PE(t){let e=await fr(t),n=l=>{let p=new RegExp(`${l}\\s*:\\s*(['"\`])([^'"\`]+)\\1`).exec(e);return p?p[2]:void 0},r=/\bdefineConduit\s*[<(]/.test(e),s=n("id");if(r){let l=n("on"),p=n("recordType"),u=n("source");return(!s||!p||!l||!u)&&b(`could not parse id/on/from.source/from.recordType from ${t} \u2014 is it a defineConduit({...}) file?`),{id:s,recordType:p,event:l,source:u,sourceCode:e,extensionKind:"conduit"}}let o=n("recordType"),i=n("event"),a=n("source")??"standalone";return(!s||!o||!i)&&b(`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 nr(t,e,n,r){try{return await re.access(t),"skipped"}catch{}return await re.mkdir(tr(t),{recursive:!0}),await re.writeFile(t,e,{mode:r??(n?493:420)}),r!==void 0&&await re.chmod(t,r),"wrote"}async function ry(t,e){if(sy(t)!==".gitignore")return nr(t,e.content,e.executable,e.mode);let n=await re.readFile(t,"utf8").catch(()=>null);if(n===null)return nr(t,e.content,e.executable,e.mode);let r=lu(n);return r===null?"skipped":(await re.writeFile(t,r,"utf8"),"merged")}function td(t){if(t!==void 0){if(t==="junit"||t==="json")return t;b(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function li(t){process.exit(t)}function AE(t){return{name:"turbobase ext test",cases:t.map(n=>{if(n.pass)return{name:n.name,status:"passed"};let r=n.error?"error":n.rejection?.code??"mismatch",s=n.error?`error: ${n.error}`:n.observed!==n.expected?`expected ${n.expected}, observed ${n.observed}`+(n.rejection?` (rejected: ${n.rejection.code??"rejected"}${n.rejection.message?` \u2014 ${n.rejection.message}`:""})`:""):`rejected: ${n.rejection?.code??"rejected"}${n.rejection?.message?` \u2014 ${n.rejection.message}`:""}`;return{name:n.name,status:"failed",detail:s,type:r}})}}function IE(t,e){let n=e.warnings.length===0?[{name:`${t} (${e.kind})`,status:"passed"}]:e.warnings.map(r=>({name:`${t}: ${r.rule}`,status:"failed",type:r.rule,detail:`${r.message} \u2192 fix: ${r.fix}`}));return{name:`turbobase ext lint ${t}`,cases:n}}function CE(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 n of t.guards)e.push(n.status==="failed"?{name:`guard: ${n.name}`,status:"failed",type:n.name,detail:n.detail}:{name:`guard: ${n.name}`,status:"passed",detail:n.status==="skipped-offline"?`skipped-offline: ${n.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 ad({includeInternal:t=!0}={}){let e=new _E;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.1.22").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",d=>d.command.parent?"":`${zi()}
|
|
2108
2108
|
`),e.hook("preAction",(d,f)=>{pu(e.opts(),f.opts())});let n=e.command("auth").description("Sign in to an instance and manage stored credentials."),r=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.");n.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 d=>{let f=Xe(d.baseUrl);d.token||b(`auth login needs --token. Mint a PAT in your Turbobase app (Settings \u203A Access tokens), then:
|
|
2109
2109
|
${mr} auth login --base-url ${f} --token tb_pat_\u2026`),sp(d.token)||b("--token does not look like a Turbobase PAT (tb_pat_\u2026)."),await EE(f,d.token,d.verify)}),n.command("status").description("Show every stored sign-in, where its token was read from, and whether the instance still accepts it.").option("--no-probe","List the stored sign-ins without asking each instance about its token.").action(async d=>cp({probe:d.probe})),n.command("use [base-url]").description("Make an already-stored instance the active default (omit the arg to pick interactively).").action(async d=>{let f=d??await xE("Pick the active instance:"),y=await Ss(f);if(!y){let _=await Rt();b(`no stored sign-in for ${Xe(f)}.`+(_.length?` Stored: ${_.map(S=>S.baseUrl).join(", ")}.`:"")+` Run \`turbobase auth login --base-url ${Xe(f)} --token tb_pat_\u2026\`.`)}w(`active instance \u2192 ${y.baseUrl}.`)}),n.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 d=>{if(d.all){let _=await Rt();for(let S of _)await Ei(S.baseUrl);w(`removed ${_.length} stored sign-in${_.length===1?"":"s"}.`);return}let f=await Ei(d.baseUrl);f||b(d.baseUrl?`no stored sign-in for ${Xe(d.baseUrl)}.`:"nothing to sign out of (or ambiguous \u2014 pass --base-url / --all).");let y=await ut();w(`signed out of ${f}.`+(y?` Active instance \u2192 ${y.baseUrl}.`:""))}),r.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 d=>{let{baseUrl:f,token:y}=await H(d.baseUrl),_;try{_=await Dt({baseUrl:f,token:y,recordsOnly:d.recordsOnly})}catch(g){let $=g instanceof Error?g.message:String(g);if(!d.recordsOnly&&/openapi\.json failed \(401/.test($)){let T=await $E(f,y);if(T)try{_=await Dt({baseUrl:f,token:T,recordsOnly:!1});let A=de(process.cwd(),d.out);await re.writeFile(A,_,"utf8"),w(`wrote ${A} (${_.split(`
|
|
2110
2110
|
`).length} lines).`);return}catch{}D("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 Dt({baseUrl:f,token:y,recordsOnly:!0})}catch(A){b(`types generation failed: ${A instanceof Error?A.message:String(A)}`)}}else b(`types generation failed: ${$}`)}let S=de(process.cwd(),d.out);await re.writeFile(S,_,"utf8"),w(`wrote ${S} (${_.split(`
|
|
@@ -2135,7 +2135,7 @@ samples init: ${T} written, ${A} skipped.`)});let Ze=e.command("guardrails").des
|
|
|
2135
2135
|
`+h.dim("create one with `turbobase guardrails create <name> --source <s> --record <r>`"));w(pt(S.map(sr),{head:fs})),w(h.dim(" first match wins; lower PRIO is evaluated first."))}),Ze.command("create <name>").description("Create a rule gating 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("--op <glob>","Raw op glob, when gating something other than a record write.").option("--target <glob>","Row address glob, e.g. 'salesOrder.12345'.").option("--actor <glob>","Only match this caller, e.g. 'agent:*' to gate AI writes but not human ones.").option("--decision <d>","allow | allow_with_audit | require_confirm | deny.","deny").option("--priority <n>","Lower is evaluated first (default 100).",d=>Number(d)).option("--when <expr>","Row predicate, e.g. 'total gt 10000'. Omit to match every row.").option("--reason <text>","Why this rule exists \u2014 shown to a blocked writer and in `list`.").option("--disabled","Create the rule parked, so it takes effect only when activated.").option("--confirm-freeze-all","Required for an unconditional deny across '*'/'*'.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(d,f)=>{let{baseUrl:y,token:_}=await H(f.baseUrl),S=new B({baseUrl:y,token:_}),g;try{g=await S.guardrailCreate({name:d,source:f.source,record:f.record,decision:f.decision,...f.op?{op:f.op}:{},...f.target?{target:f.target}:{},...f.actor?{actor:f.actor}:{},...Number.isFinite(f.priority)?{priority:Number(f.priority)}:{},...f.when?{when:f.when}:{},...f.reason?{reason:f.reason}:{},...f.disabled?{enabled:!1}:{},...f.confirmFreezeAll?{confirmFreezeAll:!0}:{}})}catch($){G($,"extensions:deploy","create a guardrail"),b(`create guardrail failed: ${$ instanceof Error?$.message:String($)}`)}w(`${h.green("\u2713")} guardrail ${h.bold(g.name??g.id)} created${g.status==="active"?"":h.dim(` (${g.status} \u2014 activate it to take effect)`)}`),w(h.dim(` ${g.decision} ${g.source}/${g.record??g.op}${g.actor?` actor=${g.actor}`:""}${g.when?` where ${g.when}`:""} @prio ${g.priority}`)),w(h.dim(` verify with: turbobase guardrails test --source ${g.source==="*"?"<source>":g.source} --record <record>`))}),Ze.command("edit <name>").description("Change a rule \u2014 its decision, priority, predicate or reason.").option("--decision <d>","allow | allow_with_audit | require_confirm | deny.").option("--priority <n>","Lower is evaluated first.",d=>Number(d)).option("--when <expr>","New row predicate. Pass '' to match every row.").option("--reason <text>","Why this rule exists.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(d,f)=>{f.decision===void 0&&f.priority===void 0&&f.when===void 0&&f.reason===void 0&&b("edit: nothing to change \u2014 pass --decision, --priority, --when or --reason.");let{baseUrl:y,token:_}=await H(f.baseUrl),S=new B({baseUrl:y,token:_}),g;try{g=await S.guardrailUpdate(d,{...f.decision!==void 0?{decision:f.decision}:{},...Number.isFinite(f.priority)?{priority:Number(f.priority)}:{},...f.when!==void 0?{when:f.when}:{},...f.reason!==void 0?{reason:f.reason}:{}})}catch($){G($,"extensions:deploy","edit a guardrail"),b(`edit guardrail failed: ${$ instanceof Error?$.message:String($)}`)}w(`${h.green("\u2713")} guardrail ${h.bold(g.name??g.id)} updated`),w(h.dim(` ${g.decision} ${g.source}/${g.record??g.op}${g.when?` where ${g.when}`:""} @prio ${g.priority}`))});for(let[d,f,y]of[["activate","active","activated"],["deactivate","inactive","deactivated"]])Ze.command(`${d} <name>`).description(f==="active"?"Arm a parked or proposed 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(_,S)=>{let{baseUrl:g,token:$}=await H(S.baseUrl),T=new B({baseUrl:g,token:$});try{await T.guardrailUpdate(_,{status:f})}catch(A){G(A,"extensions:deploy",`${d} a guardrail`),b(`${d} guardrail failed: ${A instanceof Error?A.message:String(A)}`)}w(`${h.green("\u2713")} guardrail ${h.bold(_)} ${y}`),f==="inactive"&&w(h.dim(" writes it was gating now fall through to the other rules."))});Ze.command("delete <name>").description("Remove a rule. Prefer `deactivate` if you may want it back.").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(d,f)=>{let{baseUrl:y,token:_}=await H(f.baseUrl),S=new B({baseUrl:y,token:_});try{await S.guardrailDelete(d)}catch(g){G(g,"extensions:deploy","delete a guardrail"),b(`delete guardrail failed: ${g instanceof Error?g.message:String(g)}`)}w(`${h.green("\u2713")} guardrail ${h.bold(d)} deleted`)}),Ze.command("posture [mode]").description("Show or set the GLOBAL default. 'allow' = deny-list (unmatched writes allowed, carve with deny rules). 'deny' = allow-list (unmatched writes denied, open with allow rules).").option("--base-url <url>","Instance base URL (defaults to the active instance).").action(async(d,f)=>{let{baseUrl:y,token:_}=await H(f.baseUrl),S=new B({baseUrl:y,token:_}),g=A=>A==="deny"?"deny-by-default (allow-list)":"allow-by-default (deny-list)";if(d===void 0){let A;try{A=await S.guardrailPostureGet()}catch(C){G(C,"extensions:deploy","read the guardrail posture"),b(`read posture failed: ${C instanceof Error?C.message:String(C)}`)}w(`guardrail posture: ${h.bold(g(A.mode))}`),w(h.dim(A.mode==="deny"?" Unmatched writes are DENIED. Open records with `guardrails create <name> --decision allow`.":" Unmatched writes are allowed-with-audit. Freeze records with `guardrails create <name> --decision deny`."));return}let $=d.trim().toLowerCase();$!=="allow"&&$!=="deny"&&b("posture must be 'allow' or 'deny'.");let T;try{T=await S.guardrailPostureSet($)}catch(A){G(A,"extensions:deploy","set the guardrail posture"),b(`set posture failed: ${A instanceof Error?A.message:String(A)}`)}w(`${h.green("\u2713")} guardrail posture set to ${h.bold(g(T.mode))}`),T.mode==="deny"&&w(h.dim(" Every record with no active ALLOW rule is now frozen for write-back. Verify with `guardrails test --source <s> --record <r>`."))}),Ze.command("test").description("What happens to this write? Runs the real evaluator; writes nothing.").requiredOption("--source <system>","Adapter id the write targets.").requiredOption("--record <type>","Record type the write targets.").option("--actor <actor>","Caller to simulate, e.g. 'agent:ai' or 'user:<id>'.").option("--external-id <id>","Source-side row id, for target-scoped rules.").option("--fields <json>","A candidate row as JSON, so `when` predicates can be decided.").option("--phase <phase>","boundary | cdc | worker | adapter. Defaults to worker (the last gate).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--json","Emit raw JSON.").action(async d=>{let f;if(d.fields)try{f=JSON.parse(d.fields)}catch(T){b(`--fields must be valid JSON: ${T instanceof Error?T.message:String(T)}`)}let{baseUrl:y,token:_}=await H(d.baseUrl),S=new B({baseUrl:y,token:_}),g;try{g=await S.guardrailTest({source:d.source,record:d.record,...d.actor?{actor:d.actor}:{},...d.externalId?{externalId:d.externalId}:{},...f?{fields:f}:{},...d.phase?{phase:d.phase}:{}})}catch(T){G(T,"extensions:deploy","test guardrails"),b(`guardrails test failed: ${T instanceof Error?T.message:String(T)}`)}if(d.json)return w(JSON.stringify(g,null,2));let $=`${d.source}/${d.record}`;g.degraded?(w(`${h.red("\u2717")} ${$} \u2014 guardrail policy is UNREADABLE; writes fail closed`),w(h.dim(` ${g.reason}`))):g.allowed?w(`${h.green("\u2713")} ${$} \u2014 ${g.decision}`+(g.blockedBy?h.dim(` (rule ${g.blockedBy})`):h.dim(" (no rule matched)"))):(w(`${h.red("\u2717")} ${$} \u2014 ${g.decision} by ${h.bold(g.blockedBy??"?")} at the ${g.phase} gate`),g.message&&w(h.dim(` ${g.message}`))),g.decision==="require_confirm"&&w(h.yellow(" this write is HELD for operator approval, not refused.")),g.allowed||(process.exitCode=1)});let jt=e.command("tables").description("Author custom tables on your instance (create, evolve, inspect)."),or=(d,f={})=>{let y=`${h.bold(d.table)} ${h.dim(d.displayName)}`;w(d.exists===!1?`${y} ${h.dim("(new)")}`:y);for(let _ of d.fields){let S=[_.required?"required":null].filter(Boolean).join(", "),g=f.showClaimants&&_.claimedBy?.length?` ${h.dim(`\u2190 ${_.claimedBy.join(", ")}`)}`:"";w(` ${_.column.padEnd(28)} ${_.type}${S?` (${S})`:""}${g}`)}f.showClaimants&&d.claimants.length>1&&w(h.dim(` claimed by ${d.claimants.join(", ")}`))};function di(d){if(d instanceof X&&d.status===409){let f=d.body;f?.error==="table_conflict"&&f.message&&(D(f.message),process.exit(1))}b(`tables: ${d instanceof Error?d.message:String(d)}`)}let ld=async d=>{let f=Qe(de(process.cwd(),d),"public","turbobase.json"),y=await re.readFile(f,"utf8").catch(()=>null);if(y===null)return null;let _;try{_=JSON.parse(y)}catch(g){b(`${f} is not valid JSON: ${g instanceof Error?g.message:String(g)}`)}let S=_.tables;return S===void 0?{tables:[],manifestPath:f}:(Array.isArray(S)||b(`${f}: "tables" must be an array.`),{tables:S,manifestPath:f})};jt.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 d=>{let{baseUrl:f,token:y}=await H(d.baseUrl),_=new B({baseUrl:f,token:y}),S;try{S=await _.tablesList()}catch(g){G(g,"extensions:deploy","list tables"),b(`list tables failed: ${g instanceof Error?g.message:String(g)}`)}if(d.json)return w(JSON.stringify(S,null,2));if(S.length===0)return w("no custom tables yet \u2014 create one with `turbobase tables create <slug>`.");w(pt(S.map(g=>[g.table,g.displayName,String(g.fields.length),g.status,g.claimedBy.join(", ")||"\u2014"]),{head:["TABLE","NAME","FIELDS","STATUS","CLAIMED BY"]}))}),jt.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(d,f)=>{let y=await ld(d??".");if(y||b(`no public/turbobase.json under ${de(process.cwd(),d??".")}.`),y.tables.length===0)return w(`${y.manifestPath} declares no tables \u2014 nothing to plan.`);let{baseUrl:_,token:S}=await H(f.baseUrl),g=new B({baseUrl:_,token:S}),$;try{$=await J("planning tables",()=>g.tablesPlan({claimedBy:f.as,tables:y.tables}))}catch(T){G(T,"extensions:deploy","plan tables"),di(T)}w(`claiming as ${h.bold($.claimedBy)}`);for(let T of $.tables)or(T,{showClaimants:!0});w(""),w(h.dim("nothing was applied \u2014 run `turbobase tables apply` (or deploy the app) to make it so."))}),jt.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(d,f)=>{let y=await ld(d??".");if(y||b(`no public/turbobase.json under ${de(process.cwd(),d??".")}.`),y.tables.length===0)return w(`${y.manifestPath} declares no tables \u2014 nothing to apply.`);let{baseUrl:_,token:S}=await H(f.baseUrl),g=new B({baseUrl:_,token:S}),$;try{$=await J("applying tables",()=>g.tablesApply({claimedBy:f.as,tables:y.tables}))}catch(T){G(T,"extensions:deploy","apply tables"),di(T)}for(let T of $.tables)or(T,{showClaimants:!0});w(`applied ${$.tables.length} table${$.tables.length===1?"":"s"} as ${$.claimedBy}.`)}),jt.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.",(d,f)=>[...f??[],d],[]).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(d,f)=>{let y=f.field.map(T=>{let[A,C,N]=T.split(":");return(!A||!C)&&b(`--field "${T}" must be name:type[:required] (e.g. title:text:required).`),N!==void 0&&N!=="required"&&b(`--field "${T}": the third part may only be "required".`),{name:A,type:C,...N==="required"?{required:!0}:{}}}),{baseUrl:_,token:S}=await H(f.baseUrl),g=new B({baseUrl:_,token:S}),$;try{$=await J(`creating cr_${d}`,()=>g.tablesApply({tables:[{slug:d,displayName:f.name??d,fields:y,...f.restrictToOwner?{rowLevelOwner:!0}:{}}]}))}catch(T){G(T,"extensions:deploy","create a table"),di(T)}for(let T of $.tables)or(T);w(""),w(h.dim("grant access under Settings \u203A Users, or in the Records page permissions matrix."))}),jt.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(d,f)=>{f.confirm!==d&&b(`drop refused: --confirm "${f.confirm}" does not match "${d}".`);let{baseUrl:y,token:_}=await H(f.baseUrl),S=new B({baseUrl:y,token:_}),g;try{g=await J(`dropping cr_${d}`,()=>S.tablesDrop(d,f.confirm))}catch($){G($,"extensions:deploy","drop a table"),b(`drop failed: ${$ instanceof Error?$.message:String($)}`)}if(!g.removed)return w(`no table named "${d}" on this instance \u2014 nothing dropped.`);w(`dropped ${g.table} and every row in it.`)});let ui=e.command("sandbox").description("Ephemeral local authoring sandboxes.");ui.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 d=>{process.env.TURBOBASE_SANDBOX="1";let f=rd(),y;try{y=await np(f,{fromTenant:d.from,ttl:d.ttl,sample:d.sample,baseUrl:d.baseUrl})}catch(_){_ instanceof wr&&b(_.message),b(`sandbox create failed: ${_ instanceof Error?_.message:String(_)}`)}w(`sandbox ${y.name} ready.`),w(` source: ${y.fromTenant} (sampled ${Math.round(y.sampleFraction*100)}%, redacted)`),w(` url: ${y.url}`),w(` expires: ${y.expiresAt}`)}),ui.command("destroy").argument("<name>","The sandbox name to tear down.").description("Drop a sandbox database and remove it from the registry.").action(async d=>{let f=rd();await sa(f).catch(()=>[]);let y;try{y=await rp(f,d)}catch(_){b(`sandbox destroy failed: ${_ instanceof Error?_.message:String(_)}`)}y||b(`no sandbox named '${d}'.`),w(`destroyed sandbox ${y.name} (dropped ${y.database}).`)}),ui.command("list").description("List active sandboxes with TTL/expiry; reaps any past their TTL.").action(async()=>{let d=rd(),f=await sa(d).catch(()=>[]),y=await Ls(),{active:_}=ra(y);if(_.length===0)w("no active sandboxes.");else{for(let S of _)w(` ${S.name} from=${S.fromTenant} expires=${S.expiresAt} ${S.url}`);w(`
|
|
2136
2136
|
${_.length} active sandbox(es).`)}f.length>0&&w(`(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.",Ds).action(async d=>{let{baseUrl:f,token:y}=await H(d.baseUrl),_=new B({baseUrl:f,token:y}),S;try{S=await J("exporting instance config",()=>_.configExport())}catch(T){T instanceof X&&(T.status===401||T.status===403)&&b("ext export forbidden \u2014 the stored PAT lacks extensions:deploy. Re-run `turbobase auth login`."),b(`ext export failed: ${T instanceof Error?T.message:String(T)}`)}let g=typeof S.serialized=="string"?S.serialized:`${JSON.stringify(S.manifest??S,null,2)}
|
|
2137
2137
|
`,$=de(process.cwd(),d.out,kr);await re.mkdir(tr($),{recursive:!0}),await re.writeFile($,g,"utf8"),w(`wrote ${d.out}/${kr} (${g.split(`
|
|
2138
|
-
`).length} lines).`)});let cd=s.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),oy=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],dd=()=>process.env.TURBOBASE_HOME?.trim()||Qe(SE(),".turbobase"),iy=()=>"1.1.
|
|
2138
|
+
`).length} lines).`)});let cd=s.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),oy=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],dd=()=>process.env.TURBOBASE_HOME?.trim()||Qe(SE(),".turbobase"),iy=()=>"1.1.22",hs=async()=>Promise.all(oy.map(async d=>{try{return await import(d.name),{...d,present:!0}}catch{return{...d,present:!1}}}));cd.command("status").description("Report which optional native packages are installed.").action(async()=>{let d=await hs();for(let f of d)w(`${f.present?ve.success("installed"):ve.warn("missing")} ${h.bold(f.name)} \u2014 ${f.needs}`);d.some(f=>!f.present)&&w(`
|
|
2139
2139
|
run \`${mr} ext extras install\` to fetch them into ${dd()}.`)}),cd.command("install").description("Install the optional native packages into $TURBOBASE_HOME (needs npm).").action(async()=>{let d=await hs();if(d.every(A=>A.present)){w(ve.success("extras already installed \u2014 nothing to do."));return}let f=dd();await re.mkdir(f,{recursive:!0});let y=d.filter(A=>!A.present).map(A=>A.name),_=Yi(process.platform,process.arch),S=async(A,C)=>{let{code:N,stderr:W}=await ps("npm",["install","--prefix",f,"--no-audit","--no-fund",...A]);N!==0&&b(`extras install failed while installing ${C} (npm exited ${N}).
|
|
2140
2140
|
${W.trim()}
|
|
2141
2141
|
These are native packages \u2014 they need npm and a working toolchain. Everything except \`test\` and \`replay\` works without them.`)},g=y.filter(A=>A!=="isolated-vm");if(g.length>0){let A=g.map(C=>`${C}@${Ji[C]??"latest"}`);w(`installing ${A.join(", ")} \u2192 ${f}/node_modules \u2026`),await S(A,g.join(", "))}if(y.includes("isolated-vm")){let A=`isolated-vm@${Ji["isolated-vm"]}`;if(w(`installing ${A} \u2192 ${f}/node_modules \u2026`),await S(["--ignore-scripts",A],"isolated-vm"),Ju.includes(_)){let N=`${Zi}@${iy()}`;w(`fetching ${N} (no upstream binary for ${_}) \u2026`);let{code:W}=await ps("npm",["install","--prefix",f,"--no-audit","--no-fund","--ignore-scripts",N]);W!==0&&w(ve.warn(`could not fetch ${N} \u2014 falling back to a source build.`))}let C=await Zu({home:f});C.length>0&&w(` ${ve.success("prebuilt")} ${C.join(", ")} \u2192 isolated-vm/prebuilds/${_}`)}let $=await hs(),T=$.filter(A=>!A.present).map(A=>A.name);if(T.includes("isolated-vm")){w(ve.warn(`no prebuilt isolated-vm for ${_} / Node ${process.versions.node} (${Qi()}) \u2014 building from source. This needs Python and a C++ toolchain, and takes a few minutes.`));let{code:A}=await ps("npm",["rebuild","--prefix",f,"isolated-vm"]);A===0&&($=await hs(),T=$.filter(C=>!C.present).map(C=>C.name))}T.length>0&&b(`npm succeeded but ${T.join(", ")} still will not load \u2014 no prebuilt binary for ${_} / Node ${process.versions.node} (${Qi()}), and the source build did not produce one either. Everything except \`test\` and \`replay\` works without them.`),w(ve.success("extras installed \u2014 `turbobase ext test` and `turbobase ext replay` are ready."))});let wn=e.command("release").description("Ship a release to your instance: deploy | status | rollback | list.");return wn.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 d=>{let f=d.target==="approved"?"approved":d.target==="auto"?"auto":null;f||b(`--target must be 'auto' or 'approved' (got '${d.target}').`);let y=await LE(de(process.cwd(),d.project??".")),_=Object.keys(y).length;!d.solution&&_===0&&b("release deploy needs --solution <id> (live-fetch) or an SDF project (a src/ dir under --project, default cwd) to upload.");let g=(d.ref?await sd(["rev-parse",d.ref])??d.ref:null)??process.env.GITHUB_SHA??await sd(["rev-parse","HEAD"]),$=d.gitRepo??process.env.GITHUB_REPOSITORY??await sd(["remote","get-url","origin"]),T=d.gitRef??process.env.GITHUB_REF??null,A=d.prUrl??process.env.PR_URL??null,{baseUrl:C,token:N}=await H(d.baseUrl),W=new B({baseUrl:C,token:N}),O;try{O=await W.releaseDeploy({solutionId:d.solution??null,target:f,approvedBy:d.approvedBy??null,files:_>0?y:null,gitRepo:$,gitRef:T,gitSha:g,prUrl:A})}catch(Q){G(Q,"releases:write","release deploy"),Q instanceof X&&Q.status===404&&b("solution_not_found \u2014 no solution with that id."),b(`release deploy failed: ${Q instanceof Error?Q.message:String(Q)}`)}w(`release ${O.releaseId} enqueued \u2192 taskId=${O.taskId} (target=${f}`+(_>0?`, uploaded ${_} bundle file(s)`:", live-fetch")+")."),d.follow&&await Hi(()=>W.releaseStatus(O.releaseId),`release ${O.releaseId}`,d.intervalMs)}),wn.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(d,f)=>{let{baseUrl:y,token:_}=await H(f.baseUrl),S=new B({baseUrl:y,token:_}),g;try{g=await S.releaseFiles(d)}catch(A){A instanceof X&&A.status===404&&b(`release ${d} not found.`),A instanceof X&&A.status===409&&b(`no built project was retained for release ${d}. Files are kept for a FAILED attempt only \u2014 a shipped bundle is recoverable from the account.`),G(A,"releases:write","pull a release"),b(`release pull failed: ${A instanceof Error?A.message:String(A)}`)}let $=de(process.cwd(),f.dir),T=0;for(let[A,C]of[["owned",g.owned],["context",g.context]])for(let[N,W]of Object.entries(C)){let O=Qe($,A,N);await re.mkdir(tr(O),{recursive:!0}),await re.writeFile(O,W,"utf8"),T++}await re.writeFile(Qe($,".turbobase-release.json"),JSON.stringify({releaseId:g.releaseId,version:g.version,contentDigest:g.contentDigest},null,2),"utf8"),w(`pulled release ${g.releaseId} (${g.status}) \u2192 ${$}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turbobase/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.22",
|
|
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",
|