@turbobase/cli 1.32.0 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/turbobase-standalone.mjs +15 -10
- package/dist/turbobase.js +15 -10
- package/package.json +1 -1
|
@@ -2564,10 +2564,13 @@ Check them before you trust a result.
|
|
|
2564
2564
|
orders. The catalog marks these (\`sharedTable\`) \u2014 apply the discriminator
|
|
2565
2565
|
(\`entity_type=eq.vendor\`, \`source_system=eq.netsuite\`) or you will label a
|
|
2566
2566
|
vendor with a customer's name.
|
|
2567
|
-
4. **\`created_at\` / \`
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2567
|
+
4. **\`created_at\` / \`synced_at\` / \`last_modified_pg\` are MIRROR timestamps** \u2014
|
|
2568
|
+
when Turbobase wrote the row. After a bulk sync they are identical across the
|
|
2569
|
+
table, so ordering by one gives sync batch order, not business order. Use
|
|
2570
|
+
\`source_modified\`, or the source's own date in \`raw\`, for a business date.
|
|
2571
|
+
\`updated_at\` is the exception: it is when the row last CHANGED \u2014 moved by a
|
|
2572
|
+
source change or by one of your own writes \u2014 so it IS the column to order a
|
|
2573
|
+
"recently touched" list by, and the one \`updated_since\` filters on.
|
|
2571
2574
|
5. **A column can exist and be empty on every row.** \`currency_name\` and
|
|
2572
2575
|
\`subsidiary_name\` are often NULL while \`currency_id\` / \`subsidiary_id\` carry
|
|
2573
2576
|
the data. Check one real row before rendering a column, not just the schema.
|
|
@@ -3646,10 +3649,12 @@ about style.
|
|
|
3646
3649
|
the request SUCCEEDED and returned zero rows; show an error state when it did
|
|
3647
3650
|
not. The starter component does this \u2014 keep the shape.
|
|
3648
3651
|
3. **Never show a mirror timestamp as a business date.** \`created_at\`,
|
|
3649
|
-
\`
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3652
|
+
\`synced_at\` and \`last_modified_pg\` are when Turbobase wrote the row \u2014 after a
|
|
3653
|
+
bulk sync they are identical across the table, so sorting by one gives sync
|
|
3654
|
+
batch order. Use \`source_modified\`, or the source's own date in \`raw\`. A
|
|
3655
|
+
"Date" column that sorts by sync batch is worse than no column. \`updated_at\`
|
|
3656
|
+
is Turbobase's clock too and is still not a business date, but it does say
|
|
3657
|
+
when the row last changed \u2014 label it "Last updated", never "Date".
|
|
3653
3658
|
4. **Check one real row before rendering a column.** A column can exist and be
|
|
3654
3659
|
NULL on every row (\`currency_name\` and \`subsidiary_name\` often are, while
|
|
3655
3660
|
their \`_id\` twins carry the data). A column of blanks reads as broken data.
|
|
@@ -3764,7 +3769,7 @@ ${o.map(RP).join(`
|
|
|
3764
3769
|
`))}async function Qb(t,e,r=wo){let n=[...new Set(t.map(a=>a.trim()).filter(Boolean))];n.length===0&&x("name at least one skill slug. `turbobase skills list` shows them.");let o=await _o(e,r);if(!await ba(`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 se("publishing",()=>o.skillsPublish(n))}catch(a){ne(a,"skills:publish","publish a skill"),x(`publish failed: ${a instanceof Error?a.message:String(a)}`)}_(g.green(`published ${i.published.length} skill(s) \u2014 live now.`)),i.unknown.length>0&&(q(g.yellow(`no such skill: ${i.unknown.join(", ")}`)),process.exitCode=1),q(g.dim(`release v${i.version.version_no} \xB7 undo with: turbobase skills unpublish ${i.published.join(" ")}`))}async function ew(t,e,r=wo){let n=[...new Set(t.map(a=>a.trim()).filter(Boolean))];n.length===0&&x("name at least one skill slug. `turbobase skills list` shows them.");let o=await _o(e,r);if(!await ba(`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 se("unpublishing",()=>o.skillsUnpublish(n))}catch(a){ne(a,"skills:publish","unpublish a skill"),x(`unpublish failed: ${a instanceof Error?a.message:String(a)}`)}_(g.green(`unpublished ${i.unpublished.length} skill(s) \u2014 retracted now.`)),q(g.dim(`release v${i.version.version_no} \xB7 re-publish with: turbobase skills publish ${i.unpublished.join(" ")}`)),i.unknown.length>0&&(q(g.yellow(`no such skill: ${i.unknown.join(", ")}`)),process.exitCode=1)}async function tw(t,e,r=wo){let n=Number(t);(!Number.isInteger(n)||n<1)&&x(`--version must be a release number (got '${t}'). \`turbobase skills releases\` lists them.`);let o=await _o(e,r);if(await ba(`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 se("rolling back",()=>o.skillsRollback(n));_(g.green(`rolled back to v${i.version.version_no}.`))}catch(i){ne(i,"skills:publish","roll back a skills release"),x(`rollback failed: ${i instanceof Error?i.message:String(i)}`)}}async function rw(t,e=wo){let r=await _o(t,e),n;try{n=await se("reading the releases",()=>r.skillsReleases())}catch(o){ne(o,"skills:publish","read the skills releases"),x(`could not read the releases: ${o instanceof Error?o.message:String(o)}`)}if(n.items.length===0){q("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}`:"";_(`${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 nw(t,e,r=wo){let n=[...new Set((t??[]).map(l=>l.trim()).filter(Boolean))];n.length===0&&x("name at least one skill to delete. `turbobase skills list` shows the slugs.");let o=await _o(e,r);if(!await ba(n.length===1?`Delete the skill '${n[0]}'?`:`Delete ${n.length} skills?
|
|
3765
3770
|
${n.join(`
|
|
3766
3771
|
`)}`,"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 se(`deleting ${l}`,()=>o.skillsDelete(l)))}catch(u){if(u instanceof ce&&u.status===404){a.push(l);continue}i.length>0&&q(g.dim(`deleted before the error: ${i.map(p=>p.slug).join(", ")}`)),ne(u,"skills:publish","delete a skill"),x(`delete failed on '${l}': ${u instanceof Error?u.message:String(u)}`)}i.length>0&&_(g.green(`deleted ${i.length} skill(s).`));let c=i.filter(l=>l.wasPublished).map(l=>l.slug);c.length>0&&q(g.yellow(`Published until now, so no longer injected: ${c.join(", ")}`)),a.length>0&&(q(g.yellow(`no such skill: ${a.join(", ")}`)),process.exitCode=1)}import{promises as ow}from"node:fs";import{join as hA,relative as fA,resolve as gA,sep as yA}from"node:path";var bA=new Set(["node_modules","dist","build","coverage",".git",".claude",".turbobase",".next",".svelte-kit",".vercel",".cache"]),wA=new Set([".mcp.json",".env"]);function _A(t){return wA.has(t)||t.startsWith(".env.")&&t!==".env.example"}function vA(t){return Buffer.compare(Buffer.from(t.toString("utf8"),"utf8"),t)===0}function xA(t){return t.includes(0)}async function sw(t){let e=gA(t),r={},n=[],o=[],s=[];async function i(a){let c=await ow.readdir(a,{withFileTypes:!0});for(let l of c){let u=hA(a,l.name);if(bA.has(l.name))continue;if(l.isDirectory()){await i(u);continue}if(!l.isFile())continue;let p=fA(e,u).split(yA).join("/");if(_A(l.name)){o.push(p);continue}let y=await ow.readFile(u);if(!vA(y)){n.push(p);continue}xA(y)&&s.push(p),r[p]=y.toString("utf8")}}return await i(e),{files:r,skipped:n,secrets:o,unstorable:s}}import{randomBytes as kA}from"node:crypto";var SA="/_tb/app-assets",$A=16,TA=/^[0-9a-f]{32}$/;function iw(){return kA($A).toString("hex")}function RA(t){return TA.test(t)}function aw(t){if(!RA(t))throw new Error(`not a build id: ${t}`);return`${SA}/${t}/`}var cw=7;Iu();var Ou=50*1024*1024;import wa from"node:fs/promises";import ys from"node:path";var Nu={esbuild:"0.28.1","isolated-vm":"6.1.2"},zu="@turbobase/isolated-vm-prebuilds";function Lu(t,e){return`${t}-${e}`}function ju(t=process.versions){return`abi${t.modules}`}var lw=["darwin-x64","win32-arm64"];async function uw(t){let e=t.platform??process.platform,r=t.arch??process.arch,n=Lu(e,r),o=ys.join(t.home,"node_modules"),s=ys.join(o,...zu.split("/"),"prebuilds",n),i=ys.join(o,"isolated-vm","prebuilds",n),a;try{a=await wa.readdir(s)}catch{return[]}let c=[];await wa.mkdir(i,{recursive:!0});for(let l of a){if(!l.endsWith(".node"))continue;let u=ys.join(i,l);try{await wa.copyFile(ys.join(s,l),u,wa.constants?.COPYFILE_EXCL??1),c.push(l)}catch{}}return c}import{homedir as EA}from"node:os";import{join as dw}from"node:path";import{promises as Du}from"node:fs";function mw(){let t=process.env.TURBOBASE_HOME??dw(EA(),".turbobase");return{dir:t,file:dw(t,"sandboxes.json")}}var PA={s:1e3,m:6e4,h:36e5,d:864e5};function AA(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*PA[e[2]]}function CA(t,e=Date.now()){return new Date(e+AA(t)).toISOString()}function IA(t,e=Date.now()){let r=Date.parse(t);return Number.isNaN(r)?!0:r<=e}function OA(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 NA(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 zA="[REDACTED]",bs=class extends Error{constructor(e){super(e),this.name="SandboxDataCopyUnavailableError"}},Mu=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 LA(t,e,r){let n={};for(let[o,s]of Object.entries(e)){let i=r[o];if(i===void 0)throw new Mu(t,o,"column has no PII classification (unknown = unsafe)");i==="pii"?n[o]=s==null?s:zA:n[o]=s}return n}function jA(t,e,r){return e.map(n=>LA(t,n,r))}var pw=new Set(["localhost","127.0.0.1","::1","0.0.0.0"]),DA=["oraclecloud.com","us-ashburn-1","amazonaws.com","rds.","supabase.co","turbobase.app","turbobase.io"];function MA(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 Uu(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 DA)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=MA(t);if(!pw.has(r))throw new Error(`local-only guard: target host '${r}' is not a recognised local Postgres (${[...pw].join(", ")}). \`turbobase sandbox create\` is LOCAL-ONLY.`)}function UA(t,e=qA){return`sbx-${t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"tenant"}-${e()}`}function qA(){return Math.floor(Math.random()*16777215).toString(16).padStart(6,"0")}function FA(t){return`turbobase_${t.replace(/[^a-z0-9]+/gi,"_").toLowerCase()}`}function BA(t,e){return`${(e??process.env.TURBOBASE_SANDBOX_BASE_URL??"http://localhost:5173").replace(/\/+$/,"")}/?sandbox=${encodeURIComponent(t)}`}async function qu(){let{file:t}=mw();try{let e=await Du.readFile(t,"utf8"),r=JSON.parse(e);return Array.isArray(r)?r:[]}catch(e){if(e.code==="ENOENT")return[];throw e}}async function hw(t){let{dir:e,file:r}=mw();await Du.mkdir(e,{recursive:!0}),await Du.writeFile(r,`${JSON.stringify(t,null,2)}
|
|
3767
|
-
`,{mode:384})}async function ZA(t){let e=await qu();e.push(t),await hw(e)}async function fw(t){let e=await qu(),r=e.findIndex(o=>o.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await hw(e),n??null}async function _a(){return qu()}function Fu(t,e=Date.now()){let r=[],n=[];for(let o of t)IA(o.expiresAt,e)?n.push(o):r.push(o);return{active:r,expired:n}}async function gw(t,e){Uu(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new bs(r);let n=OA(e.sample),o=e.now??Date.now(),s=(e.nameGen??(l=>UA(l)))(e.fromTenant),i=FA(s);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let l of a){let u=NA(l.rows,n),p=jA(l.table,u,l.classification);p.length>0&&await t.copyRows(i,l.table,p)}let c={name:s,fromTenant:e.fromTenant,database:i,createdAt:new Date(o).toISOString(),expiresAt:CA(e.ttl,o),sampleFraction:n,url:BA(s,e.baseUrl)};return await ZA(c),c}async function yw(t,e){Uu(t.target());let n=(await _a()).find(o=>o.name===e);return n?(await t.dropDatabase(n.database),await fw(e),n):null}async function Bu(t,e=Date.now()){Uu(t.target());let r=await _a(),{expired:n}=Fu(r,e),o=[];for(let s of n){try{await t.dropDatabase(s.database)}catch{continue}await fw(s.name),o.push(s)}return o}var ws={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","skills:publish","docs:publish","mcp:data","mcp:write"],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"}},n2=Object.keys(ws).filter(t=>ws[t].mintable);var i2=ws.pat.prefix,VA=Object.entries(ws).map(([t,e])=>[e.prefix,t]),HA=/^[A-Za-z0-9_-]{20,}$/;function KA(t){if(typeof t!="string")return null;for(let[e,r]of VA)if(t.startsWith(e)&&HA.test(t.slice(e.length)))return r;return null}function bw(t){return KA(t)==="pat"}is();var WA=8e3,GA={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 JA(t){return GA[t]??`the instance rejected this token (${t}) \u2014 run \`turbobase auth login\` to store a fresh PAT`}async function YA(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(WA)})}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 ww(t){return t.state==="accepted"?tt()?g.green("accepted"):"accepted":t.state==="rejected"?tt()?g.red(`rejected \xB7 ${t.reason}`):`rejected \xB7 ${t.reason}`:tt()?g.yellow("unreachable"):"unreachable"}async function _w(t={},e=fetch){let r=await Pr(),{file:n}=Ki();if(r.length===0){_("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),q(g.dim(`no sign-ins stored in ${n}.`));return}let o=await Xt(),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 YA(i.baseUrl,i.token,e)})));if(t.probe===!1)for(let i of s)_(`${i.active?"* ":" "}${i.baseUrl} (updated ${i.updatedAt})`);else if(tt())_(er(s.map(i=>[i.active?g.green("*"):" ",g.bold(i.baseUrl),ww(i.check),g.dim(i.updatedAt)]),{head:["","instance","token","updated"]}));else for(let i of s)_(`${i.active?"* ":" "}${i.baseUrl} ${ww(i.check)} (updated ${i.updatedAt})`);q(g.dim(`tokens read from ${n}`));for(let i of s)i.check.state==="rejected"?(q(g.red(`${i.baseUrl}: ${JA(i.check.reason)}.`)),q(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"&&q(g.yellow(`${i.baseUrl}: could not reach the instance (${i.check.detail}).`));o||q("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}async function TU(t){let e=await Pr();e.length===0&&x("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),ot()||x("this command needs an explicit <base-url> in a non-interactive shell.");try{return await st(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{x("cancelled.")}}async function RU(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 EU(t){q(""),q("Sharing is ON. Two things reach the Turbobase product team from this instance:"),q(" \xB7 anonymized pattern cards from your proposals, solutions and INSTANCE-LEARNINGS.md \u2014"),q(" a pattern label, a record class, a fix shape, an outcome. No scripts, values or ids."),q(" \xB7 redacted prose from your projects\u2019 TURBOBASE-FEEDBACK.md \u2014 the findings you write"),q(" about OUR platform, with instance name, user names and ids masked before they leave."),q(`Turn it off: ${t}/admin/settings \u2192 Telemetry \u2192 "Share learnings and platform feedback".`),q("")}async function PU(t,e,r){let n=await my({baseUrl:t,token:e}),o=(await Pr()).length-1;if(_(`signed in to ${n.baseUrl} \u2014 now the active instance`+(o>0?` (${o} other${o===1?"":"s"} stored; commands default here).`:".")),EU(n.baseUrl),!r)return;let s=await RU(n.baseUrl,e);s.ok||(s.kind==="wrong-host"?(q(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${s.detail}.`),q(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),q(" The token is stored, but commands will fail against this base-url until it points at the API.")):s.kind==="bad-token"?q(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):q(`warning: could not reach ${n.baseUrl} to verify the token (${s.detail}). Stored anyway.`))}async function AU(t){let e=await cu(await us(t),Dr(t)),r=vu(e);return r||x(`${t}: ${xu(e)}`),{...r,sourceCode:e}}async function Qn(t,e,r,n){try{return await be.access(t),"skipped"}catch{}return await be.mkdir(Dr(t),{recursive:!0}),await be.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await be.chmod(t,n),"wrote"}async function bT(t,e){if(_T(t)!==".gitignore")return Qn(t,e.content,e.executable,e.mode);let r=await be.readFile(t,"utf8").catch(()=>null);if(r===null)return Qn(t,e.content,e.executable,e.mode);let n=sb(r);return n===null?"skipped":(await be.writeFile(t,n,"utf8"),"merged")}function sg(t){if(t!==void 0){if(t==="junit"||t==="json")return t;x(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function hl(t){process.exit(t)}function CU(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 IU(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 OU(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 wT(t){return`${(Math.ceil(t/1048576*10)/10).toFixed(1).replace(/\.0$/,"")} MiB`}function dg({includeInternal:t=!0}={}){let e=new Ag;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.32.0").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",d=>d.command.parent?"":`${fu()}
|
|
3772
|
+
`,{mode:384})}async function ZA(t){let e=await qu();e.push(t),await hw(e)}async function fw(t){let e=await qu(),r=e.findIndex(o=>o.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await hw(e),n??null}async function _a(){return qu()}function Fu(t,e=Date.now()){let r=[],n=[];for(let o of t)IA(o.expiresAt,e)?n.push(o):r.push(o);return{active:r,expired:n}}async function gw(t,e){Uu(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new bs(r);let n=OA(e.sample),o=e.now??Date.now(),s=(e.nameGen??(l=>UA(l)))(e.fromTenant),i=FA(s);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let l of a){let u=NA(l.rows,n),p=jA(l.table,u,l.classification);p.length>0&&await t.copyRows(i,l.table,p)}let c={name:s,fromTenant:e.fromTenant,database:i,createdAt:new Date(o).toISOString(),expiresAt:CA(e.ttl,o),sampleFraction:n,url:BA(s,e.baseUrl)};return await ZA(c),c}async function yw(t,e){Uu(t.target());let n=(await _a()).find(o=>o.name===e);return n?(await t.dropDatabase(n.database),await fw(e),n):null}async function Bu(t,e=Date.now()){Uu(t.target());let r=await _a(),{expired:n}=Fu(r,e),o=[];for(let s of n){try{await t.dropDatabase(s.database)}catch{continue}await fw(s.name),o.push(s)}return o}var ws={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","skills:publish","docs:publish","mcp:data","mcp:write"],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"}},n2=Object.keys(ws).filter(t=>ws[t].mintable);var i2=ws.pat.prefix,VA=Object.entries(ws).map(([t,e])=>[e.prefix,t]),HA=/^[A-Za-z0-9_-]{20,}$/;function KA(t){if(typeof t!="string")return null;for(let[e,r]of VA)if(t.startsWith(e)&&HA.test(t.slice(e.length)))return r;return null}function bw(t){return KA(t)==="pat"}is();var WA=8e3,GA={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 JA(t){return GA[t]??`the instance rejected this token (${t}) \u2014 run \`turbobase auth login\` to store a fresh PAT`}async function YA(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(WA)})}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 ww(t){return t.state==="accepted"?tt()?g.green("accepted"):"accepted":t.state==="rejected"?tt()?g.red(`rejected \xB7 ${t.reason}`):`rejected \xB7 ${t.reason}`:tt()?g.yellow("unreachable"):"unreachable"}async function _w(t={},e=fetch){let r=await Pr(),{file:n}=Ki();if(r.length===0){_("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),q(g.dim(`no sign-ins stored in ${n}.`));return}let o=await Xt(),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 YA(i.baseUrl,i.token,e)})));if(t.probe===!1)for(let i of s)_(`${i.active?"* ":" "}${i.baseUrl} (updated ${i.updatedAt})`);else if(tt())_(er(s.map(i=>[i.active?g.green("*"):" ",g.bold(i.baseUrl),ww(i.check),g.dim(i.updatedAt)]),{head:["","instance","token","updated"]}));else for(let i of s)_(`${i.active?"* ":" "}${i.baseUrl} ${ww(i.check)} (updated ${i.updatedAt})`);q(g.dim(`tokens read from ${n}`));for(let i of s)i.check.state==="rejected"?(q(g.red(`${i.baseUrl}: ${JA(i.check.reason)}.`)),q(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"&&q(g.yellow(`${i.baseUrl}: could not reach the instance (${i.check.detail}).`));o||q("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}async function TU(t){let e=await Pr();e.length===0&&x("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),ot()||x("this command needs an explicit <base-url> in a non-interactive shell.");try{return await st(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{x("cancelled.")}}async function RU(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 EU(t){q(""),q("Sharing is ON. Two things reach the Turbobase product team from this instance:"),q(" \xB7 anonymized pattern cards from your proposals, solutions and INSTANCE-LEARNINGS.md \u2014"),q(" a pattern label, a record class, a fix shape, an outcome. No scripts, values or ids."),q(" \xB7 redacted prose from your projects\u2019 TURBOBASE-FEEDBACK.md \u2014 the findings you write"),q(" about OUR platform, with instance name, user names and ids masked before they leave."),q(`Turn it off: ${t}/admin/settings \u2192 Telemetry \u2192 "Share learnings and platform feedback".`),q("")}async function PU(t,e,r){let n=await my({baseUrl:t,token:e}),o=(await Pr()).length-1;if(_(`signed in to ${n.baseUrl} \u2014 now the active instance`+(o>0?` (${o} other${o===1?"":"s"} stored; commands default here).`:".")),EU(n.baseUrl),!r)return;let s=await RU(n.baseUrl,e);s.ok||(s.kind==="wrong-host"?(q(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${s.detail}.`),q(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),q(" The token is stored, but commands will fail against this base-url until it points at the API.")):s.kind==="bad-token"?q(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):q(`warning: could not reach ${n.baseUrl} to verify the token (${s.detail}). Stored anyway.`))}async function AU(t){let e=await cu(await us(t),Dr(t)),r=vu(e);return r||x(`${t}: ${xu(e)}`),{...r,sourceCode:e}}async function Qn(t,e,r,n){try{return await be.access(t),"skipped"}catch{}return await be.mkdir(Dr(t),{recursive:!0}),await be.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await be.chmod(t,n),"wrote"}async function bT(t,e){if(_T(t)!==".gitignore")return Qn(t,e.content,e.executable,e.mode);let r=await be.readFile(t,"utf8").catch(()=>null);if(r===null)return Qn(t,e.content,e.executable,e.mode);let n=sb(r);return n===null?"skipped":(await be.writeFile(t,n,"utf8"),"merged")}function sg(t){if(t!==void 0){if(t==="junit"||t==="json")return t;x(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function hl(t){process.exit(t)}function CU(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 IU(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 OU(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 wT(t){return`${(Math.ceil(t/1048576*10)/10).toFixed(1).replace(/\.0$/,"")} MiB`}function dg({includeInternal:t=!0}={}){let e=new Ag;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.33.0").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",d=>d.command.parent?"":`${fu()}
|
|
3768
3773
|
`),e.hook("preAction",(d,f)=>{bb(e.opts(),f.opts())});let r=e.command("auth").description("Sign in to an instance and manage stored credentials."),n=e.command("project").description("Set up the local project and the files an agent session reads."),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 d=>{let f=Dt(d.baseUrl);d.token||x(`auth login needs --token. Mint a PAT in your Turbobase app (Settings \u203A Access tokens), then:
|
|
3769
3774
|
${hs} auth login --base-url ${f} --token tb_pat_\u2026`),bw(d.token)||x("--token does not look like a Turbobase PAT (tb_pat_\u2026)."),await PU(f,d.token,d.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 d=>_w({probe:d.probe})),r.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 TU("Pick the active instance:"),v=await Wi(f);if(!v){let T=await Pr();x(`no stored sign-in for ${Dt(f)}.`+(T.length?` Stored: ${T.map(k=>k.baseUrl).join(", ")}.`:"")+` Run \`turbobase auth login --base-url ${Dt(f)} --token tb_pat_\u2026\`.`)}_(`active instance \u2192 ${v.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 d=>{if(d.all){let T=await Pr();for(let k of T)await ru(k.baseUrl);_(`removed ${T.length} stored sign-in${T.length===1?"":"s"}.`);return}let f=await ru(d.baseUrl);f||x(d.baseUrl?`no stored sign-in for ${Dt(d.baseUrl)}.`:"nothing to sign out of (or ambiguous \u2014 pass --base-url / --all).");let v=await Xt();_(`signed out of ${f}.`+(v?` Active instance \u2192 ${v.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 d=>{let{baseUrl:f,token:v}=await te(d.baseUrl),T;try{T=await In({baseUrl:f,token:v,recordsOnly:d.recordsOnly})}catch(b){let $=b instanceof Error?b.message:String(b);if(!d.recordsOnly&&/openapi\.json failed \(401/.test($)){q("types-gen: /v1/catalog/openapi.json needs a browser session \u2014 emitting record shapes only. The record shapes are the half apps generate against; for the OpenAPI half, run it from a signed-in browser session.");try{T=await In({baseUrl:f,token:v,recordsOnly:!0})}catch(E){x(`types generation failed: ${E instanceof Error?E.message:String(E)}`)}}else x(`types generation failed: ${$}`)}let k=ge(process.cwd(),d.out);await be.writeFile(k,T,"utf8"),_(`wrote ${k} (${T.split(`
|
|
3770
3775
|
`).length} lines).`)}),vb(n),n.command("grounding").description("Write the instance's grounding brief (source primer + record anatomy + KB retrieval) to a file for agent sessions.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--records <list>","Comma-separated record types to focus, e.g. purchaseOrder,itemReceipt.").option("--query <text>","Retrieval question for the knowledge base (e.g. what the app is about).").option("-o, --out <file>","Output file","INSTANCE.md").action(async d=>{let{baseUrl:f,token:v}=await te(d.baseUrl),T=new URLSearchParams;d.records&&T.set("records",d.records),d.query&&T.set("query",d.query);let k=T.toString(),b=await fetch(`${f}/v1/catalog/grounding${k?`?${k}`:""}`,{headers:{Authorization:`Bearer ${v}`,Accept:"application/json"}});b.ok||x(`grounding fetch failed: GET /v1/catalog/grounding \u2192 ${b.status}`);let E=((await b.json()).markdown??"").trim(),N=ge(process.cwd(),d.out),j=d.records?` records=${d.records}`:"",D=`<!-- GENERATED by \`turbobase project grounding\` against ${f}${j} \u2014 do not edit; re-run to refresh. -->
|
|
@@ -3794,7 +3799,7 @@ samples init: ${E} written, ${N} skipped.`)});let ur=e.command("guardrails").des
|
|
|
3794
3799
|
`+g.dim("create one with `turbobase guardrails create <name> --source <s> --record <r>`"));_(er(k.map($i),{head:gl})),_(g.dim(" first match wins; lower PRIO is evaluated first."))}),ur.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:v,token:T}=await te(f.baseUrl),k=new J({baseUrl:v,token:T}),b;try{b=await k.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($){ne($,"extensions:deploy","create a guardrail"),x(`create guardrail failed: ${$ instanceof Error?$.message:String($)}`)}_(`${g.green("\u2713")} guardrail ${g.bold(b.name??b.id)} created${b.status==="active"?"":g.dim(` (${b.status} \u2014 activate it to take effect)`)}`),_(g.dim(` ${b.decision} ${b.source}/${b.record??b.op}${b.actor?` actor=${b.actor}`:""}${b.when?` where ${b.when}`:""} @prio ${b.priority}`)),_(g.dim(` verify with: turbobase guardrails test --source ${b.source==="*"?"<source>":b.source} --record <record>`))}),ur.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&&x("edit: nothing to change \u2014 pass --decision, --priority, --when or --reason.");let{baseUrl:v,token:T}=await te(f.baseUrl),k=new J({baseUrl:v,token:T}),b;try{b=await k.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($){ne($,"extensions:deploy","edit a guardrail"),x(`edit guardrail failed: ${$ instanceof Error?$.message:String($)}`)}_(`${g.green("\u2713")} guardrail ${g.bold(b.name??b.id)} updated`),_(g.dim(` ${b.decision} ${b.source}/${b.record??b.op}${b.when?` where ${b.when}`:""} @prio ${b.priority}`))});for(let[d,f,v]of[["activate","active","activated"],["deactivate","inactive","deactivated"]])ur.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(T,k)=>{let{baseUrl:b,token:$}=await te(k.baseUrl),E=new J({baseUrl:b,token:$});try{await E.guardrailUpdate(T,{status:f})}catch(N){ne(N,"extensions:deploy",`${d} a guardrail`),x(`${d} guardrail failed: ${N instanceof Error?N.message:String(N)}`)}_(`${g.green("\u2713")} guardrail ${g.bold(T)} ${v}`),f==="inactive"&&_(g.dim(" writes it was gating now fall through to the other rules."))});ur.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:v,token:T}=await te(f.baseUrl),k=new J({baseUrl:v,token:T});try{await k.guardrailDelete(d)}catch(b){ne(b,"extensions:deploy","delete a guardrail"),x(`delete guardrail failed: ${b instanceof Error?b.message:String(b)}`)}_(`${g.green("\u2713")} guardrail ${g.bold(d)} deleted`)}),ur.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:v,token:T}=await te(f.baseUrl),k=new J({baseUrl:v,token:T}),b=N=>N==="deny"?"deny-by-default (allow-list)":"allow-by-default (deny-list)";if(d===void 0){let N;try{N=await k.guardrailPostureGet()}catch(j){ne(j,"extensions:deploy","read the guardrail posture"),x(`read posture failed: ${j instanceof Error?j.message:String(j)}`)}_(`guardrail posture: ${g.bold(b(N.mode))}`),_(g.dim(N.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"&&x("posture must be 'allow' or 'deny'.");let E;try{E=await k.guardrailPostureSet($)}catch(N){ne(N,"extensions:deploy","set the guardrail posture"),x(`set posture failed: ${N instanceof Error?N.message:String(N)}`)}_(`${g.green("\u2713")} guardrail posture set to ${g.bold(b(E.mode))}`),E.mode==="deny"&&_(g.dim(" Every record with no active ALLOW rule is now frozen for write-back. Verify with `guardrails test --source <s> --record <r>`."))}),ur.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("--op <op>","Write op: 'delete' tests <record>.delete; omitted tests <record>.write.").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(E){x(`--fields must be valid JSON: ${E instanceof Error?E.message:String(E)}`)}let{baseUrl:v,token:T}=await te(d.baseUrl),k=new J({baseUrl:v,token:T}),b;try{b=await k.guardrailTest({source:d.source,record:d.record,...d.actor?{actor:d.actor}:{},...d.externalId?{externalId:d.externalId}:{},...f?{fields:f}:{},...d.op?{op:d.op}:{},...d.phase?{phase:d.phase}:{}})}catch(E){ne(E,"extensions:deploy","test guardrails"),x(`guardrails test failed: ${E instanceof Error?E.message:String(E)}`)}if(d.json)return _(JSON.stringify(b,null,2));let $=`${d.source}/${b.op??d.record}`;b.degraded?(_(`${g.red("\u2717")} ${$} \u2014 guardrail policy is UNREADABLE; writes fail closed`),_(g.dim(` ${b.reason}`))):b.allowed?_(`${g.green("\u2713")} ${$} \u2014 ${b.decision}`+(b.blockedBy?g.dim(` (rule ${b.blockedBy})`):g.dim(" (no rule matched)"))):(_(`${g.red("\u2717")} ${$} \u2014 ${b.decision} by ${g.bold(b.blockedBy??"?")} at the ${b.phase} gate`),b.message&&_(g.dim(` ${b.message}`))),b.decision==="require_confirm"&&_(g.yellow(" this write is HELD for operator approval, not refused.")),b.allowed||(process.exitCode=1)});let Jo=e.command("tables").description("Author custom tables on your instance (create, evolve, inspect)."),Ti=(d,f={})=>{let v=`${g.bold(d.table)} ${g.dim(d.displayName)}`;_(d.exists===!1?`${v} ${g.dim("(new)")}`:v);for(let T of d.fields){let k=[T.required?"required":null].filter(Boolean).join(", "),b=f.showClaimants&&T.claimedBy?.length?` ${g.dim(`\u2190 ${T.claimedBy.join(", ")}`)}`:"";_(` ${T.column.padEnd(28)} ${T.type}${k?` (${k})`:""}${b}`)}f.showClaimants&&d.claimants.length>1&&_(g.dim(` claimed by ${d.claimants.join(", ")}`))};function Ri(d){if(d instanceof ce&&d.status===409){let f=d.body;f?.error==="table_conflict"&&f.message&&(q(f.message),process.exit(1))}x(`tables: ${d instanceof Error?d.message:String(d)}`)}let pg=async d=>{let f=It(ge(process.cwd(),d),"public","turbobase.json"),v=await be.readFile(f,"utf8").catch(()=>null);if(v===null)return null;let T;try{T=JSON.parse(v)}catch(b){x(`${f} is not valid JSON: ${b instanceof Error?b.message:String(b)}`)}let k=T.tables;return k===void 0?{tables:[],manifestPath:f}:(Array.isArray(k)||x(`${f}: "tables" must be an array.`),{tables:k,manifestPath:f})};Jo.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:v}=await te(d.baseUrl),T=new J({baseUrl:f,token:v}),k;try{k=await T.tablesList()}catch(b){ne(b,"extensions:deploy","list tables"),x(`list tables failed: ${b instanceof Error?b.message:String(b)}`)}if(d.json)return _(JSON.stringify(k,null,2));if(k.length===0)return _("no custom tables yet \u2014 create one with `turbobase tables create <slug>`.");_(er(k.map(b=>[b.table,b.displayName,String(b.fields.length),b.status,b.claimedBy.join(", ")||"\u2014"]),{head:["TABLE","NAME","FIELDS","STATUS","CLAIMED BY"]}))}),Jo.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 v=await pg(d??".");if(v||x(`no public/turbobase.json under ${ge(process.cwd(),d??".")}.`),v.tables.length===0)return _(`${v.manifestPath} declares no tables \u2014 nothing to plan.`);let{baseUrl:T,token:k}=await te(f.baseUrl),b=new J({baseUrl:T,token:k}),$;try{$=await se("planning tables",()=>b.tablesPlan({claimedBy:f.as,tables:v.tables}))}catch(E){ne(E,"extensions:deploy","plan tables"),Ri(E)}_(`claiming as ${g.bold($.claimedBy)}`);for(let E of $.tables)Ti(E,{showClaimants:!0});_(""),_(g.dim("nothing was applied \u2014 run `turbobase tables apply` (or deploy the app) to make it so."))}),Jo.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 v=await pg(d??".");if(v||x(`no public/turbobase.json under ${ge(process.cwd(),d??".")}.`),v.tables.length===0)return _(`${v.manifestPath} declares no tables \u2014 nothing to apply.`);let{baseUrl:T,token:k}=await te(f.baseUrl),b=new J({baseUrl:T,token:k}),$;try{$=await se("applying tables",()=>b.tablesApply({claimedBy:f.as,tables:v.tables}))}catch(E){ne(E,"extensions:deploy","apply tables"),Ri(E)}for(let E of $.tables)Ti(E,{showClaimants:!0});_(`applied ${$.tables.length} table${$.tables.length===1?"":"s"} as ${$.claimedBy}.`)}),Jo.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 v=f.field.map(E=>{let[N,j,D]=E.split(":");return(!N||!j)&&x(`--field "${E}" must be name:type[:required] (e.g. title:text:required).`),D!==void 0&&D!=="required"&&x(`--field "${E}": the third part may only be "required".`),{name:N,type:j,...D==="required"?{required:!0}:{}}}),{baseUrl:T,token:k}=await te(f.baseUrl),b=new J({baseUrl:T,token:k}),$;try{$=await se(`creating cr_${d}`,()=>b.tablesApply({tables:[{slug:d,displayName:f.name??d,fields:v,...f.restrictToOwner?{rowLevelOwner:!0}:{}}]}))}catch(E){ne(E,"extensions:deploy","create a table"),Ri(E)}for(let E of $.tables)Ti(E);_(""),_(g.dim("grant access under Settings \u203A Users, or in the Records page permissions matrix."))}),Jo.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&&x(`drop refused: --confirm "${f.confirm}" does not match "${d}".`);let{baseUrl:v,token:T}=await te(f.baseUrl),k=new J({baseUrl:v,token:T}),b;try{b=await se(`dropping cr_${d}`,()=>k.tablesDrop(d,f.confirm))}catch($){ne($,"extensions:deploy","drop a table"),x(`drop failed: ${$ instanceof Error?$.message:String($)}`)}if(!b.removed)return _(`no table named "${d}" on this instance \u2014 nothing dropped.`);_(`dropped ${b.table} and every row in it.`)});let yl=e.command("sandbox").description("Ephemeral local authoring sandboxes.");yl.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=ag(),v;try{v=await gw(f,{fromTenant:d.from,ttl:d.ttl,sample:d.sample,baseUrl:d.baseUrl})}catch(T){T instanceof bs&&x(T.message),x(`sandbox create failed: ${T instanceof Error?T.message:String(T)}`)}_(`sandbox ${v.name} ready.`),_(` source: ${v.fromTenant} (sampled ${Math.round(v.sampleFraction*100)}%, redacted)`),_(` url: ${v.url}`),_(` expires: ${v.expiresAt}`)}),yl.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=ag();await Bu(f).catch(()=>[]);let v;try{v=await yw(f,d)}catch(T){x(`sandbox destroy failed: ${T instanceof Error?T.message:String(T)}`)}v||x(`no sandbox named '${d}'.`),_(`destroyed sandbox ${v.name} (dropped ${v.database}).`)}),yl.command("list").description("List active sandboxes with TTL/expiry; reaps any past their TTL.").action(async()=>{let d=ag(),f=await Bu(d).catch(()=>[]),v=await _a(),{active:T}=Fu(v);if(T.length===0)_("no active sandboxes.");else{for(let k of T)_(` ${k.name} from=${k.fromTenant} expires=${k.expiresAt} ${k.url}`);_(`
|
|
3795
3800
|
${T.length} active sandbox(es).`)}f.length>0&&_(`(reaped ${f.length} expired sandbox(es).)`)}),o.command("export").description("Pull the live instance into turbobase/bundle.json (adopt an instance into a repo).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--out <dir>","Directory to write the bundle into.",la).action(async d=>{let{baseUrl:f,token:v}=await te(d.baseUrl),T=new J({baseUrl:f,token:v}),k;try{k=await se("exporting instance config",()=>T.configExport())}catch(E){E instanceof ce&&(E.status===401||E.status===403)&&x("ext export forbidden \u2014 the stored PAT lacks extensions:deploy. Re-run `turbobase auth login`."),x(`ext export failed: ${E instanceof Error?E.message:String(E)}`)}let b=typeof k.serialized=="string"?k.serialized:`${JSON.stringify(k.manifest??k,null,2)}
|
|
3796
3801
|
`,$=ge(process.cwd(),d.out,ps);await be.mkdir(Dr($),{recursive:!0}),await be.writeFile($,b,"utf8"),_(`wrote ${d.out}/${ps} (${b.split(`
|
|
3797
|
-
`).length} lines).`)});let mg=o.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),vT=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],hg=()=>process.env.TURBOBASE_HOME?.trim()||It($U(),".turbobase"),xT=()=>"1.
|
|
3802
|
+
`).length} lines).`)});let mg=o.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),vT=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],hg=()=>process.env.TURBOBASE_HOME?.trim()||It($U(),".turbobase"),xT=()=>"1.33.0",Ei=async()=>Promise.all(vT.map(async d=>{try{return await import(d.name),{...d,present:!0}}catch{return{...d,present:!1}}}));mg.command("status").description("Report which optional native packages are installed.").action(async()=>{let d=await Ei();for(let f of d)_(`${f.present?Oe.success("installed"):Oe.warn("missing")} ${g.bold(f.name)} \u2014 ${f.needs}`);d.some(f=>!f.present)&&_(`
|
|
3798
3803
|
run \`${hs} ext extras install\` to fetch them into ${hg()}.`)}),mg.command("install").description("Install the optional native packages into $TURBOBASE_HOME (needs npm).").action(async()=>{let d=await Ei();if(d.every(N=>N.present)){_(Oe.success("extras already installed \u2014 nothing to do."));return}let f=hg();await be.mkdir(f,{recursive:!0});let v=d.filter(N=>!N.present).map(N=>N.name),T=Lu(process.platform,process.arch),k=async(N,j)=>{let{code:D,stderr:V}=await Si("npm",["install","--prefix",f,"--no-audit","--no-fund",...N]);D!==0&&x(`extras install failed while installing ${j} (npm exited ${D}).
|
|
3799
3804
|
${V.trim()}
|
|
3800
3805
|
These are native packages \u2014 they need npm and a working toolchain. Everything except \`test\` and \`replay\` works without them.`)},b=v.filter(N=>N!=="isolated-vm");if(b.length>0){let N=b.map(j=>`${j}@${Nu[j]??"latest"}`);_(`installing ${N.join(", ")} \u2192 ${f}/node_modules \u2026`),await k(N,b.join(", "))}if(v.includes("isolated-vm")){let N=`isolated-vm@${Nu["isolated-vm"]}`;if(_(`installing ${N} \u2192 ${f}/node_modules \u2026`),await k(["--ignore-scripts",N],"isolated-vm"),lw.includes(T)){let D=`${zu}@${xT()}`;_(`fetching ${D} (no upstream binary for ${T}) \u2026`);let{code:V}=await Si("npm",["install","--prefix",f,"--no-audit","--no-fund","--ignore-scripts",D]);V!==0&&_(Oe.warn(`could not fetch ${D} \u2014 falling back to a source build.`))}let j=await uw({home:f});j.length>0&&_(` ${Oe.success("prebuilt")} ${j.join(", ")} \u2192 isolated-vm/prebuilds/${T}`)}let $=await Ei(),E=$.filter(N=>!N.present).map(N=>N.name);if(E.includes("isolated-vm")){_(Oe.warn(`no prebuilt isolated-vm for ${T} / Node ${process.versions.node} (${ju()}) \u2014 building from source. This needs Python and a C++ toolchain, and takes a few minutes.`));let{code:N}=await Si("npm",["rebuild","--prefix",f,"isolated-vm"]);N===0&&($=await Ei(),E=$.filter(j=>!j.present).map(j=>j.name))}E.length>0&&x(`npm succeeded but ${E.join(", ")} still will not load \u2014 no prebuilt binary for ${T} / Node ${process.versions.node} (${ju()}), and the source build did not produce one either. Everything except \`test\` and \`replay\` works without them.`),_(Oe.success("extras installed \u2014 `turbobase ext test` and `turbobase ext replay` are ready."))});let eo=e.command("release").description("Ship a release to your instance: deploy | status | rollback | list.");return eo.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||x(`--target must be 'auto' or 'approved' (got '${d.target}').`);let v=await MU(ge(process.cwd(),d.project??".")),T=Object.keys(v).length;!d.solution&&T===0&&x("release deploy needs --solution <id> (live-fetch) or an SDF project (a src/ dir under --project, default cwd) to upload.");let b=(d.ref?await cg(["rev-parse",d.ref])??d.ref:null)??process.env.GITHUB_SHA??await cg(["rev-parse","HEAD"]),$=d.gitRepo??process.env.GITHUB_REPOSITORY??await cg(["remote","get-url","origin"]),E=d.gitRef??process.env.GITHUB_REF??null,N=d.prUrl??process.env.PR_URL??null,{baseUrl:j,token:D}=await te(d.baseUrl),V=new J({baseUrl:j,token:D}),L;try{L=await V.releaseDeploy({solutionId:d.solution??null,target:f,approvedBy:d.approvedBy??null,files:T>0?v:null,gitRepo:$,gitRef:E,gitSha:b,prUrl:N})}catch(ee){ne(ee,"releases:write","release deploy"),ee instanceof ce&&ee.status===404&&x("solution_not_found \u2014 no solution with that id."),x(`release deploy failed: ${ee instanceof Error?ee.message:String(ee)}`)}_(`release ${L.releaseId} enqueued \u2192 taskId=${L.taskId} (target=${f}`+(T>0?`, uploaded ${T} bundle file(s)`:", live-fetch")+")."),d.follow&&await Su(()=>V.releaseStatus(L.releaseId),`release ${L.releaseId}`,d.intervalMs)}),eo.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:v,token:T}=await te(f.baseUrl),k=new J({baseUrl:v,token:T}),b;try{b=await k.releaseFiles(d)}catch(N){N instanceof ce&&N.status===404&&x(`release ${d} not found.`),N instanceof ce&&N.status===409&&x(`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.`),ne(N,"releases:write","pull a release"),x(`release pull failed: ${N instanceof Error?N.message:String(N)}`)}let $=ge(process.cwd(),f.dir),E=0;for(let[N,j]of[["owned",b.owned],["context",b.context]])for(let[D,V]of Object.entries(j)){let L=It($,N,D);await be.mkdir(Dr(L),{recursive:!0}),await be.writeFile(L,V,"utf8"),E++}await be.writeFile(It($,".turbobase-release.json"),JSON.stringify({releaseId:b.releaseId,version:b.version,contentDigest:b.contentDigest},null,2),"utf8"),_(`pulled release ${b.releaseId} (${b.status}) \u2192 ${$}
|
package/dist/turbobase.js
CHANGED
|
@@ -2517,10 +2517,13 @@ Check them before you trust a result.
|
|
|
2517
2517
|
orders. The catalog marks these (\`sharedTable\`) \u2014 apply the discriminator
|
|
2518
2518
|
(\`entity_type=eq.vendor\`, \`source_system=eq.netsuite\`) or you will label a
|
|
2519
2519
|
vendor with a customer's name.
|
|
2520
|
-
4. **\`created_at\` / \`
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2520
|
+
4. **\`created_at\` / \`synced_at\` / \`last_modified_pg\` are MIRROR timestamps** \u2014
|
|
2521
|
+
when Turbobase wrote the row. After a bulk sync they are identical across the
|
|
2522
|
+
table, so ordering by one gives sync batch order, not business order. Use
|
|
2523
|
+
\`source_modified\`, or the source's own date in \`raw\`, for a business date.
|
|
2524
|
+
\`updated_at\` is the exception: it is when the row last CHANGED \u2014 moved by a
|
|
2525
|
+
source change or by one of your own writes \u2014 so it IS the column to order a
|
|
2526
|
+
"recently touched" list by, and the one \`updated_since\` filters on.
|
|
2524
2527
|
5. **A column can exist and be empty on every row.** \`currency_name\` and
|
|
2525
2528
|
\`subsidiary_name\` are often NULL while \`currency_id\` / \`subsidiary_id\` carry
|
|
2526
2529
|
the data. Check one real row before rendering a column, not just the schema.
|
|
@@ -3599,10 +3602,12 @@ about style.
|
|
|
3599
3602
|
the request SUCCEEDED and returned zero rows; show an error state when it did
|
|
3600
3603
|
not. The starter component does this \u2014 keep the shape.
|
|
3601
3604
|
3. **Never show a mirror timestamp as a business date.** \`created_at\`,
|
|
3602
|
-
\`
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3605
|
+
\`synced_at\` and \`last_modified_pg\` are when Turbobase wrote the row \u2014 after a
|
|
3606
|
+
bulk sync they are identical across the table, so sorting by one gives sync
|
|
3607
|
+
batch order. Use \`source_modified\`, or the source's own date in \`raw\`. A
|
|
3608
|
+
"Date" column that sorts by sync batch is worse than no column. \`updated_at\`
|
|
3609
|
+
is Turbobase's clock too and is still not a business date, but it does say
|
|
3610
|
+
when the row last changed \u2014 label it "Last updated", never "Date".
|
|
3606
3611
|
4. **Check one real row before rendering a column.** A column can exist and be
|
|
3607
3612
|
NULL on every row (\`currency_name\` and \`subsidiary_name\` often are, while
|
|
3608
3613
|
their \`_id\` twins carry the data). A column of blanks reads as broken data.
|
|
@@ -3717,7 +3722,7 @@ ${o.map(a_).join(`
|
|
|
3717
3722
|
`))}async function nm(t,e,r=Mr){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 qr(e,r);if(!await os(`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 J("publishing",()=>o.skillsPublish(n))}catch(a){G(a,"skills:publish","publish a skill"),v(`publish failed: ${a instanceof Error?a.message:String(a)}`)}b(h.green(`published ${i.published.length} skill(s) \u2014 live now.`)),i.unknown.length>0&&(L(h.yellow(`no such skill: ${i.unknown.join(", ")}`)),process.exitCode=1),L(h.dim(`release v${i.version.version_no} \xB7 undo with: turbobase skills unpublish ${i.published.join(" ")}`))}async function om(t,e,r=Mr){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 qr(e,r);if(!await os(`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 J("unpublishing",()=>o.skillsUnpublish(n))}catch(a){G(a,"skills:publish","unpublish a skill"),v(`unpublish failed: ${a instanceof Error?a.message:String(a)}`)}b(h.green(`unpublished ${i.unpublished.length} skill(s) \u2014 retracted now.`)),L(h.dim(`release v${i.version.version_no} \xB7 re-publish with: turbobase skills publish ${i.unpublished.join(" ")}`)),i.unknown.length>0&&(L(h.yellow(`no such skill: ${i.unknown.join(", ")}`)),process.exitCode=1)}async function sm(t,e,r=Mr){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 qr(e,r);if(await os(`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 J("rolling back",()=>o.skillsRollback(n));b(h.green(`rolled back to v${i.version.version_no}.`))}catch(i){G(i,"skills:publish","roll back a skills release"),v(`rollback failed: ${i instanceof Error?i.message:String(i)}`)}}async function im(t,e=Mr){let r=await qr(t,e),n;try{n=await J("reading the releases",()=>r.skillsReleases())}catch(o){G(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){L("no releases yet.");return}for(let o of n.items){let s=o.is_current?h.green("*"):" ",i=o.release_note?` \u2014 ${o.release_note}`:"";b(`${s} v${o.version_no} ${o.skill_count} skill(s) ${h.dim(o.published_at)}${o.published_by?h.dim(` by ${o.published_by}`):""}${h.dim(i)}`)}}async function am(t,e,r=Mr){let n=[...new Set((t??[]).map(u=>u.trim()).filter(Boolean))];n.length===0&&v("name at least one skill to delete. `turbobase skills list` shows the slugs.");let o=await qr(e,r);if(!await os(n.length===1?`Delete the skill '${n[0]}'?`:`Delete ${n.length} skills?
|
|
3718
3723
|
${n.join(`
|
|
3719
3724
|
`)}`,"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 u of n)try{i.push(await J(`deleting ${u}`,()=>o.skillsDelete(u)))}catch(p){if(p instanceof Z&&p.status===404){a.push(u);continue}i.length>0&&L(h.dim(`deleted before the error: ${i.map(f=>f.slug).join(", ")}`)),G(p,"skills:publish","delete a skill"),v(`delete failed on '${u}': ${p instanceof Error?p.message:String(p)}`)}i.length>0&&b(h.green(`deleted ${i.length} skill(s).`));let l=i.filter(u=>u.wasPublished).map(u=>u.slug);l.length>0&&L(h.yellow(`Published until now, so no longer injected: ${l.join(", ")}`)),a.length>0&&(L(h.yellow(`no such skill: ${a.join(", ")}`)),process.exitCode=1)}import{promises as lm}from"node:fs";import{join as G_,relative as J_,resolve as Y_,sep as X_}from"node:path";var Z_=new Set(["node_modules","dist","build","coverage",".git",".claude",".turbobase",".next",".svelte-kit",".vercel",".cache"]),Q_=new Set([".mcp.json",".env"]);function eS(t){return Q_.has(t)||t.startsWith(".env.")&&t!==".env.example"}function tS(t){return Buffer.compare(Buffer.from(t.toString("utf8"),"utf8"),t)===0}function rS(t){return t.includes(0)}async function cm(t){let e=Y_(t),r={},n=[],o=[],s=[];async function i(a){let l=await lm.readdir(a,{withFileTypes:!0});for(let u of l){let p=G_(a,u.name);if(Z_.has(u.name))continue;if(u.isDirectory()){await i(p);continue}if(!u.isFile())continue;let f=J_(e,p).split(X_).join("/");if(eS(u.name)){o.push(f);continue}let S=await lm.readFile(p);if(!tS(S)){n.push(f);continue}rS(S)&&s.push(f),r[f]=S.toString("utf8")}}return await i(e),{files:r,skipped:n,secrets:o,unstorable:s}}import{randomBytes as nS}from"node:crypto";var oS="/_tb/app-assets",sS=16,iS=/^[0-9a-f]{32}$/;function dm(){return nS(sS).toString("hex")}function aS(t){return iS.test(t)}function um(t){if(!aS(t))throw new Error(`not a build id: ${t}`);return`${oS}/${t}/`}var pm=7;Pa();var Aa=50*1024*1024;import ss from"node:fs/promises";import Rn from"node:path";var Ca={esbuild:"0.28.1","isolated-vm":"6.1.2"},Ia="@turbobase/isolated-vm-prebuilds";function Oa(t,e){return`${t}-${e}`}function Na(t=process.versions){return`abi${t.modules}`}var mm=["darwin-x64","win32-arm64"];async function hm(t){let e=t.platform??process.platform,r=t.arch??process.arch,n=Oa(e,r),o=Rn.join(t.home,"node_modules"),s=Rn.join(o,...Ia.split("/"),"prebuilds",n),i=Rn.join(o,"isolated-vm","prebuilds",n),a;try{a=await ss.readdir(s)}catch{return[]}let l=[];await ss.mkdir(i,{recursive:!0});for(let u of a){if(!u.endsWith(".node"))continue;let p=Rn.join(i,u);try{await ss.copyFile(Rn.join(s,u),p,ss.constants?.COPYFILE_EXCL??1),l.push(u)}catch{}}return l}import{homedir as lS}from"node:os";import{join as fm}from"node:path";import{promises as La}from"node:fs";function ym(){let t=process.env.TURBOBASE_HOME??fm(lS(),".turbobase");return{dir:t,file:fm(t,"sandboxes.json")}}var cS={s:1e3,m:6e4,h:36e5,d:864e5};function dS(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*cS[e[2]]}function uS(t,e=Date.now()){return new Date(e+dS(t)).toISOString()}function pS(t,e=Date.now()){let r=Date.parse(t);return Number.isNaN(r)?!0:r<=e}function mS(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 hS(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 fS="[REDACTED]",En=class extends Error{constructor(e){super(e),this.name="SandboxDataCopyUnavailableError"}},ja=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 gS(t,e,r){let n={};for(let[o,s]of Object.entries(e)){let i=r[o];if(i===void 0)throw new ja(t,o,"column has no PII classification (unknown = unsafe)");i==="pii"?n[o]=s==null?s:fS:n[o]=s}return n}function yS(t,e,r){return e.map(n=>gS(t,n,r))}var gm=new Set(["localhost","127.0.0.1","::1","0.0.0.0"]),bS=["oraclecloud.com","us-ashburn-1","amazonaws.com","rds.","supabase.co","turbobase.app","turbobase.io"];function wS(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 Da(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 bS)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=wS(t);if(!gm.has(r))throw new Error(`local-only guard: target host '${r}' is not a recognised local Postgres (${[...gm].join(", ")}). \`turbobase sandbox create\` is LOCAL-ONLY.`)}function vS(t,e=_S){return`sbx-${t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"tenant"}-${e()}`}function _S(){return Math.floor(Math.random()*16777215).toString(16).padStart(6,"0")}function SS(t){return`turbobase_${t.replace(/[^a-z0-9]+/gi,"_").toLowerCase()}`}function kS(t,e){return`${(e??process.env.TURBOBASE_SANDBOX_BASE_URL??"http://localhost:5173").replace(/\/+$/,"")}/?sandbox=${encodeURIComponent(t)}`}async function Ua(){let{file:t}=ym();try{let e=await La.readFile(t,"utf8"),r=JSON.parse(e);return Array.isArray(r)?r:[]}catch(e){if(e.code==="ENOENT")return[];throw e}}async function bm(t){let{dir:e,file:r}=ym();await La.mkdir(e,{recursive:!0}),await La.writeFile(r,`${JSON.stringify(t,null,2)}
|
|
3720
|
-
`,{mode:384})}async function TS(t){let e=await Ua();e.push(t),await bm(e)}async function wm(t){let e=await Ua(),r=e.findIndex(o=>o.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await bm(e),n??null}async function is(){return Ua()}function Ma(t,e=Date.now()){let r=[],n=[];for(let o of t)pS(o.expiresAt,e)?n.push(o):r.push(o);return{active:r,expired:n}}async function vm(t,e){Da(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new En(r);let n=mS(e.sample),o=e.now??Date.now(),s=(e.nameGen??(u=>vS(u)))(e.fromTenant),i=SS(s);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let u of a){let p=hS(u.rows,n),f=yS(u.table,p,u.classification);f.length>0&&await t.copyRows(i,u.table,f)}let l={name:s,fromTenant:e.fromTenant,database:i,createdAt:new Date(o).toISOString(),expiresAt:uS(e.ttl,o),sampleFraction:n,url:kS(s,e.baseUrl)};return await TS(l),l}async function _m(t,e){Da(t.target());let n=(await is()).find(o=>o.name===e);return n?(await t.dropDatabase(n.database),await wm(e),n):null}async function qa(t,e=Date.now()){Da(t.target());let r=await is(),{expired:n}=Ma(r,e),o=[];for(let s of n){try{await t.dropDatabase(s.database)}catch{continue}await wm(s.name),o.push(s)}return o}var Pn={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","skills:publish","docs:publish","mcp:data","mcp:write"],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"}},BO=Object.keys(Pn).filter(t=>Pn[t].mintable);var KO=Pn.pat.prefix,xS=Object.entries(Pn).map(([t,e])=>[e.prefix,t]),$S=/^[A-Za-z0-9_-]{20,}$/;function RS(t){if(typeof t!="string")return null;for(let[e,r]of xS)if(t.startsWith(e)&&$S.test(t.slice(e.length)))return r;return null}function Sm(t){return RS(t)==="pat"}gn();var ES=8e3,PS={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 AS(t){return PS[t]??`the instance rejected this token (${t}) \u2014 run \`turbobase auth login\` to store a fresh PAT`}async function CS(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(ES)})}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 km(t){return t.state==="accepted"?Pe()?h.green("accepted"):"accepted":t.state==="rejected"?Pe()?h.red(`rejected \xB7 ${t.reason}`):`rejected \xB7 ${t.reason}`:Pe()?h.yellow("unreachable"):"unreachable"}async function Tm(t={},e=fetch){let r=await It(),{file:n}=Oo();if(r.length===0){b("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),L(h.dim(`no sign-ins stored in ${n}.`));return}let o=await pt(),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 CS(i.baseUrl,i.token,e)})));if(t.probe===!1)for(let i of s)b(`${i.active?"* ":" "}${i.baseUrl} (updated ${i.updatedAt})`);else if(Pe())b(ht(s.map(i=>[i.active?h.green("*"):" ",h.bold(i.baseUrl),km(i.check),h.dim(i.updatedAt)]),{head:["","instance","token","updated"]}));else for(let i of s)b(`${i.active?"* ":" "}${i.baseUrl} ${km(i.check)} (updated ${i.updatedAt})`);L(h.dim(`tokens read from ${n}`));for(let i of s)i.check.state==="rejected"?(L(h.red(`${i.baseUrl}: ${AS(i.check.reason)}.`)),L(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"&&L(h.yellow(`${i.baseUrl}: could not reach the instance (${i.check.detail}).`));o||L("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}async function eA(t){let e=await It();e.length===0&&v("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),Ce()||v("this command needs an explicit <base-url> in a non-interactive shell.");try{return await Ie(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{v("cancelled.")}}async function tA(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 rA(t){L(""),L("Sharing is ON. Two things reach the Turbobase product team from this instance:"),L(" \xB7 anonymized pattern cards from your proposals, solutions and INSTANCE-LEARNINGS.md \u2014"),L(" a pattern label, a record class, a fix shape, an outcome. No scripts, values or ids."),L(" \xB7 redacted prose from your projects\u2019 TURBOBASE-FEEDBACK.md \u2014 the findings you write"),L(" about OUR platform, with instance name, user names and ids masked before they leave."),L(`Turn it off: ${t}/admin/settings \u2192 Telemetry \u2192 "Share learnings and platform feedback".`),L("")}async function nA(t,e,r){let n=await yu({baseUrl:t,token:e}),o=(await It()).length-1;if(b(`signed in to ${n.baseUrl} \u2014 now the active instance`+(o>0?` (${o} other${o===1?"":"s"} stored; commands default here).`:".")),rA(n.baseUrl),!r)return;let s=await tA(n.baseUrl,e);s.ok||(s.kind==="wrong-host"?(L(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${s.detail}.`),L(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),L(" The token is stored, but commands will fail against this base-url until it points at the API.")):s.kind==="bad-token"?L(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):L(`warning: could not reach ${n.baseUrl} to verify the token (${s.detail}). Stored anyway.`))}async function oA(t){let e=await sa(await vn(t),qt(t)),r=ba(e);return r||v(`${t}: ${wa(e)}`),{...r,sourceCode:e}}async function Er(t,e,r,n){try{return await se.access(t),"skipped"}catch{}return await se.mkdir(qt(t),{recursive:!0}),await se.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await se.chmod(t,n),"wrote"}async function Tb(t,e){if($b(t)!==".gitignore")return Er(t,e.content,e.executable,e.mode);let r=await se.readFile(t,"utf8").catch(()=>null);if(r===null)return Er(t,e.content,e.executable,e.mode);let n=cp(r);return n===null?"skipped":(await se.writeFile(t,n,"utf8"),"merged")}function Dd(t){if(t!==void 0){if(t==="junit"||t==="json")return t;v(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function Ci(t){process.exit(t)}function sA(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 iA(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 aA(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 xb(t){return`${(Math.ceil(t/1048576*10)/10).toFixed(1).replace(/\.0$/,"")} MiB`}function Fd({includeInternal:t=!0}={}){let e=new QP;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.32.0").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",d=>d.command.parent?"":`${pa()}
|
|
3725
|
+
`,{mode:384})}async function TS(t){let e=await Ua();e.push(t),await bm(e)}async function wm(t){let e=await Ua(),r=e.findIndex(o=>o.name===t);if(r===-1)return null;let[n]=e.splice(r,1);return await bm(e),n??null}async function is(){return Ua()}function Ma(t,e=Date.now()){let r=[],n=[];for(let o of t)pS(o.expiresAt,e)?n.push(o):r.push(o);return{active:r,expired:n}}async function vm(t,e){Da(t.target());let r=t.dataCopyUnavailable?.();if(r)throw new En(r);let n=mS(e.sample),o=e.now??Date.now(),s=(e.nameGen??(u=>vS(u)))(e.fromTenant),i=SS(s);await t.createDatabase(i),await t.replayMigrations(i);let a=await t.readSourceTables(e.fromTenant);for(let u of a){let p=hS(u.rows,n),f=yS(u.table,p,u.classification);f.length>0&&await t.copyRows(i,u.table,f)}let l={name:s,fromTenant:e.fromTenant,database:i,createdAt:new Date(o).toISOString(),expiresAt:uS(e.ttl,o),sampleFraction:n,url:kS(s,e.baseUrl)};return await TS(l),l}async function _m(t,e){Da(t.target());let n=(await is()).find(o=>o.name===e);return n?(await t.dropDatabase(n.database),await wm(e),n):null}async function qa(t,e=Date.now()){Da(t.target());let r=await is(),{expired:n}=Ma(r,e),o=[];for(let s of n){try{await t.dropDatabase(s.database)}catch{continue}await wm(s.name),o.push(s)}return o}var Pn={pat:{prefix:"tb_pat_",defaultScopes:["extensions:deploy"],allowedScopes:["extensions:deploy","catalog:read","mcp:authoring","analyzer:run","releases:write","solutions:write","skills:publish","docs:publish","mcp:data","mcp:write"],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"}},BO=Object.keys(Pn).filter(t=>Pn[t].mintable);var KO=Pn.pat.prefix,xS=Object.entries(Pn).map(([t,e])=>[e.prefix,t]),$S=/^[A-Za-z0-9_-]{20,}$/;function RS(t){if(typeof t!="string")return null;for(let[e,r]of xS)if(t.startsWith(e)&&$S.test(t.slice(e.length)))return r;return null}function Sm(t){return RS(t)==="pat"}gn();var ES=8e3,PS={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 AS(t){return PS[t]??`the instance rejected this token (${t}) \u2014 run \`turbobase auth login\` to store a fresh PAT`}async function CS(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(ES)})}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 km(t){return t.state==="accepted"?Pe()?h.green("accepted"):"accepted":t.state==="rejected"?Pe()?h.red(`rejected \xB7 ${t.reason}`):`rejected \xB7 ${t.reason}`:Pe()?h.yellow("unreachable"):"unreachable"}async function Tm(t={},e=fetch){let r=await It(),{file:n}=Oo();if(r.length===0){b("not signed in. Run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),L(h.dim(`no sign-ins stored in ${n}.`));return}let o=await pt(),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 CS(i.baseUrl,i.token,e)})));if(t.probe===!1)for(let i of s)b(`${i.active?"* ":" "}${i.baseUrl} (updated ${i.updatedAt})`);else if(Pe())b(ht(s.map(i=>[i.active?h.green("*"):" ",h.bold(i.baseUrl),km(i.check),h.dim(i.updatedAt)]),{head:["","instance","token","updated"]}));else for(let i of s)b(`${i.active?"* ":" "}${i.baseUrl} ${km(i.check)} (updated ${i.updatedAt})`);L(h.dim(`tokens read from ${n}`));for(let i of s)i.check.state==="rejected"?(L(h.red(`${i.baseUrl}: ${AS(i.check.reason)}.`)),L(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"&&L(h.yellow(`${i.baseUrl}: could not reach the instance (${i.check.detail}).`));o||L("no active default \u2014 run `turbobase auth use <base-url>` or pass --base-url.")}async function eA(t){let e=await It();e.length===0&&v("no stored sign-ins \u2014 run `turbobase auth login --base-url <instance> --token tb_pat_\u2026`."),Ce()||v("this command needs an explicit <base-url> in a non-interactive shell.");try{return await Ie(t,e.map(r=>({value:r.baseUrl,label:r.baseUrl,description:`updated ${r.updatedAt}`})))}catch{v("cancelled.")}}async function tA(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 rA(t){L(""),L("Sharing is ON. Two things reach the Turbobase product team from this instance:"),L(" \xB7 anonymized pattern cards from your proposals, solutions and INSTANCE-LEARNINGS.md \u2014"),L(" a pattern label, a record class, a fix shape, an outcome. No scripts, values or ids."),L(" \xB7 redacted prose from your projects\u2019 TURBOBASE-FEEDBACK.md \u2014 the findings you write"),L(" about OUR platform, with instance name, user names and ids masked before they leave."),L(`Turn it off: ${t}/admin/settings \u2192 Telemetry \u2192 "Share learnings and platform feedback".`),L("")}async function nA(t,e,r){let n=await yu({baseUrl:t,token:e}),o=(await It()).length-1;if(b(`signed in to ${n.baseUrl} \u2014 now the active instance`+(o>0?` (${o} other${o===1?"":"s"} stored; commands default here).`:".")),rA(n.baseUrl),!r)return;let s=await tA(n.baseUrl,e);s.ok||(s.kind==="wrong-host"?(L(`warning: ${n.baseUrl} did not respond to the Turbobase API (/v1) \u2014 got ${s.detail}.`),L(" In local dev the Turbobase API is on :4100, not the app dev-server port (e.g. :5173)."),L(" The token is stored, but commands will fail against this base-url until it points at the API.")):s.kind==="bad-token"?L(`warning: ${n.baseUrl} rejected the token (401) \u2014 the PAT may be stale, revoked, or for a different instance.`):L(`warning: could not reach ${n.baseUrl} to verify the token (${s.detail}). Stored anyway.`))}async function oA(t){let e=await sa(await vn(t),qt(t)),r=ba(e);return r||v(`${t}: ${wa(e)}`),{...r,sourceCode:e}}async function Er(t,e,r,n){try{return await se.access(t),"skipped"}catch{}return await se.mkdir(qt(t),{recursive:!0}),await se.writeFile(t,e,{mode:n??(r?493:420)}),n!==void 0&&await se.chmod(t,n),"wrote"}async function Tb(t,e){if($b(t)!==".gitignore")return Er(t,e.content,e.executable,e.mode);let r=await se.readFile(t,"utf8").catch(()=>null);if(r===null)return Er(t,e.content,e.executable,e.mode);let n=cp(r);return n===null?"skipped":(await se.writeFile(t,n,"utf8"),"merged")}function Dd(t){if(t!==void 0){if(t==="junit"||t==="json")return t;v(`unknown --reporter '${t}' (expected 'junit' or 'json').`)}}function Ci(t){process.exit(t)}function sA(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 iA(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 aA(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 xb(t){return`${(Math.ceil(t/1048576*10)/10).toFixed(1).replace(/\.0$/,"")} MiB`}function Fd({includeInternal:t=!0}={}){let e=new QP;e.name("turbobase").description("Build apps, author extensions, and drive your Turbobase instance.").version("1.33.0").option("--no-color","Disable ANSI colour + animations (also honoured via NO_COLOR / non-TTY)."),e.addHelpText("beforeAll",d=>d.command.parent?"":`${pa()}
|
|
3721
3726
|
`),e.hook("preAction",(d,m)=>{Sp(e.opts(),m.opts())});let r=e.command("auth").description("Sign in to an instance and manage stored credentials."),n=e.command("project").description("Set up the local project and the files an agent session reads."),o=e.command("ext").description("Author, test, ship, and observe extensions \u2014 hooks and conduits."),s=e.command("kb").description("Write into the instance's knowledge base."),i=e.command("skills").description("Write standing rules into the instance's agent skills.");r.command("login").description("Sign in to an instance by pasting a personal access token (PAT), stored in ~/.turbobase/credentials.").requiredOption("--base-url <url>","Instance base URL, e.g. https://acme.turbobase.app").option("--token <tb_pat_\u2026>","The PAT to store. Mint one in your app under Settings \u203A Access tokens.").option("--no-verify","Skip the post-login connectivity/token probe.").action(async d=>{let m=tt(d.baseUrl);d.token||v(`auth login needs --token. Mint a PAT in your Turbobase app (Settings \u203A Access tokens), then:
|
|
3722
3727
|
${Tn} auth login --base-url ${m} --token tb_pat_\u2026`),Sm(d.token)||v("--token does not look like a Turbobase PAT (tb_pat_\u2026)."),await nA(m,d.token,d.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 d=>Tm({probe:d.probe})),r.command("use [base-url]").description("Make an already-stored instance the active default (omit the arg to pick interactively).").action(async d=>{let m=d??await eA("Pick the active instance:"),w=await No(m);if(!w){let x=await It();v(`no stored sign-in for ${tt(m)}.`+(x.length?` Stored: ${x.map(_=>_.baseUrl).join(", ")}.`:"")+` Run \`turbobase auth login --base-url ${tt(m)} --token tb_pat_\u2026\`.`)}b(`active instance \u2192 ${w.baseUrl}.`)}),r.command("logout").description("Remove a stored sign-in (defaults to the active instance).").option("--base-url <url>","Which instance to remove (defaults to the active/single one).").option("--all","Remove every stored sign-in.").action(async d=>{if(d.all){let x=await It();for(let _ of x)await Qi(_.baseUrl);b(`removed ${x.length} stored sign-in${x.length===1?"":"s"}.`);return}let m=await Qi(d.baseUrl);m||v(d.baseUrl?`no stored sign-in for ${tt(d.baseUrl)}.`:"nothing to sign out of (or ambiguous \u2014 pass --base-url / --all).");let w=await pt();b(`signed out of ${m}.`+(w?` Active instance \u2192 ${w.baseUrl}.`:""))}),n.command("types").description("Generate types.ts from the signed-in instance's catalog.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("-o, --out <file>","Output file","types.ts").option("--records-only","Emit only record-shape interfaces (skip the OpenAPI half).").action(async d=>{let{baseUrl:m,token:w}=await V(d.baseUrl),x;try{x=await pr({baseUrl:m,token:w,recordsOnly:d.recordsOnly})}catch(g){let T=g instanceof Error?g.message:String(g);if(!d.recordsOnly&&/openapi\.json failed \(401/.test(T)){L("types-gen: /v1/catalog/openapi.json needs a browser session \u2014 emitting record shapes only. The record shapes are the half apps generate against; for the OpenAPI half, run it from a signed-in browser session.");try{x=await pr({baseUrl:m,token:w,recordsOnly:!0})}catch(E){v(`types generation failed: ${E instanceof Error?E.message:String(E)}`)}}else v(`types generation failed: ${T}`)}let _=oe(process.cwd(),d.out);await se.writeFile(_,x,"utf8"),b(`wrote ${_} (${x.split(`
|
|
3723
3728
|
`).length} lines).`)}),xp(n),n.command("grounding").description("Write the instance's grounding brief (source primer + record anatomy + KB retrieval) to a file for agent sessions.").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--records <list>","Comma-separated record types to focus, e.g. purchaseOrder,itemReceipt.").option("--query <text>","Retrieval question for the knowledge base (e.g. what the app is about).").option("-o, --out <file>","Output file","INSTANCE.md").action(async d=>{let{baseUrl:m,token:w}=await V(d.baseUrl),x=new URLSearchParams;d.records&&x.set("records",d.records),d.query&&x.set("query",d.query);let _=x.toString(),g=await fetch(`${m}/v1/catalog/grounding${_?`?${_}`:""}`,{headers:{Authorization:`Bearer ${w}`,Accept:"application/json"}});g.ok||v(`grounding fetch failed: GET /v1/catalog/grounding \u2192 ${g.status}`);let E=((await g.json()).markdown??"").trim(),A=oe(process.cwd(),d.out),I=d.records?` records=${d.records}`:"",N=`<!-- GENERATED by \`turbobase project grounding\` against ${m}${I} \u2014 do not edit; re-run to refresh. -->
|
|
@@ -3747,7 +3752,7 @@ samples init: ${E} written, ${A} skipped.`)});let kt=e.command("guardrails").des
|
|
|
3747
3752
|
`+h.dim("create one with `turbobase guardrails create <name> --source <s> --record <r>`"));b(ht(_.map(So),{head:Oi})),b(h.dim(" first match wins; lower PRIO is evaluated first."))}),kt.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,m)=>{let{baseUrl:w,token:x}=await V(m.baseUrl),_=new z({baseUrl:w,token:x}),g;try{g=await _.guardrailCreate({name:d,source:m.source,record:m.record,decision:m.decision,...m.op?{op:m.op}:{},...m.target?{target:m.target}:{},...m.actor?{actor:m.actor}:{},...Number.isFinite(m.priority)?{priority:Number(m.priority)}:{},...m.when?{when:m.when}:{},...m.reason?{reason:m.reason}:{},...m.disabled?{enabled:!1}:{},...m.confirmFreezeAll?{confirmFreezeAll:!0}:{}})}catch(T){G(T,"extensions:deploy","create a guardrail"),v(`create guardrail failed: ${T instanceof Error?T.message:String(T)}`)}b(`${h.green("\u2713")} guardrail ${h.bold(g.name??g.id)} created${g.status==="active"?"":h.dim(` (${g.status} \u2014 activate it to take effect)`)}`),b(h.dim(` ${g.decision} ${g.source}/${g.record??g.op}${g.actor?` actor=${g.actor}`:""}${g.when?` where ${g.when}`:""} @prio ${g.priority}`)),b(h.dim(` verify with: turbobase guardrails test --source ${g.source==="*"?"<source>":g.source} --record <record>`))}),kt.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,m)=>{m.decision===void 0&&m.priority===void 0&&m.when===void 0&&m.reason===void 0&&v("edit: nothing to change \u2014 pass --decision, --priority, --when or --reason.");let{baseUrl:w,token:x}=await V(m.baseUrl),_=new z({baseUrl:w,token:x}),g;try{g=await _.guardrailUpdate(d,{...m.decision!==void 0?{decision:m.decision}:{},...Number.isFinite(m.priority)?{priority:Number(m.priority)}:{},...m.when!==void 0?{when:m.when}:{},...m.reason!==void 0?{reason:m.reason}:{}})}catch(T){G(T,"extensions:deploy","edit a guardrail"),v(`edit guardrail failed: ${T instanceof Error?T.message:String(T)}`)}b(`${h.green("\u2713")} guardrail ${h.bold(g.name??g.id)} updated`),b(h.dim(` ${g.decision} ${g.source}/${g.record??g.op}${g.when?` where ${g.when}`:""} @prio ${g.priority}`))});for(let[d,m,w]of[["activate","active","activated"],["deactivate","inactive","deactivated"]])kt.command(`${d} <name>`).description(m==="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(x,_)=>{let{baseUrl:g,token:T}=await V(_.baseUrl),E=new z({baseUrl:g,token:T});try{await E.guardrailUpdate(x,{status:m})}catch(A){G(A,"extensions:deploy",`${d} a guardrail`),v(`${d} guardrail failed: ${A instanceof Error?A.message:String(A)}`)}b(`${h.green("\u2713")} guardrail ${h.bold(x)} ${w}`),m==="inactive"&&b(h.dim(" writes it was gating now fall through to the other rules."))});kt.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,m)=>{let{baseUrl:w,token:x}=await V(m.baseUrl),_=new z({baseUrl:w,token:x});try{await _.guardrailDelete(d)}catch(g){G(g,"extensions:deploy","delete a guardrail"),v(`delete guardrail failed: ${g instanceof Error?g.message:String(g)}`)}b(`${h.green("\u2713")} guardrail ${h.bold(d)} deleted`)}),kt.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,m)=>{let{baseUrl:w,token:x}=await V(m.baseUrl),_=new z({baseUrl:w,token:x}),g=A=>A==="deny"?"deny-by-default (allow-list)":"allow-by-default (deny-list)";if(d===void 0){let A;try{A=await _.guardrailPostureGet()}catch(I){G(I,"extensions:deploy","read the guardrail posture"),v(`read posture failed: ${I instanceof Error?I.message:String(I)}`)}b(`guardrail posture: ${h.bold(g(A.mode))}`),b(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 T=d.trim().toLowerCase();T!=="allow"&&T!=="deny"&&v("posture must be 'allow' or 'deny'.");let E;try{E=await _.guardrailPostureSet(T)}catch(A){G(A,"extensions:deploy","set the guardrail posture"),v(`set posture failed: ${A instanceof Error?A.message:String(A)}`)}b(`${h.green("\u2713")} guardrail posture set to ${h.bold(g(E.mode))}`),E.mode==="deny"&&b(h.dim(" Every record with no active ALLOW rule is now frozen for write-back. Verify with `guardrails test --source <s> --record <r>`."))}),kt.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("--op <op>","Write op: 'delete' tests <record>.delete; omitted tests <record>.write.").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 m;if(d.fields)try{m=JSON.parse(d.fields)}catch(E){v(`--fields must be valid JSON: ${E instanceof Error?E.message:String(E)}`)}let{baseUrl:w,token:x}=await V(d.baseUrl),_=new z({baseUrl:w,token:x}),g;try{g=await _.guardrailTest({source:d.source,record:d.record,...d.actor?{actor:d.actor}:{},...d.externalId?{externalId:d.externalId}:{},...m?{fields:m}:{},...d.op?{op:d.op}:{},...d.phase?{phase:d.phase}:{}})}catch(E){G(E,"extensions:deploy","test guardrails"),v(`guardrails test failed: ${E instanceof Error?E.message:String(E)}`)}if(d.json)return b(JSON.stringify(g,null,2));let T=`${d.source}/${g.op??d.record}`;g.degraded?(b(`${h.red("\u2717")} ${T} \u2014 guardrail policy is UNREADABLE; writes fail closed`),b(h.dim(` ${g.reason}`))):g.allowed?b(`${h.green("\u2713")} ${T} \u2014 ${g.decision}`+(g.blockedBy?h.dim(` (rule ${g.blockedBy})`):h.dim(" (no rule matched)"))):(b(`${h.red("\u2717")} ${T} \u2014 ${g.decision} by ${h.bold(g.blockedBy??"?")} at the ${g.phase} gate`),g.message&&b(h.dim(` ${g.message}`))),g.decision==="require_confirm"&&b(h.yellow(" this write is HELD for operator approval, not refused.")),g.allowed||(process.exitCode=1)});let mn=e.command("tables").description("Author custom tables on your instance (create, evolve, inspect)."),ko=(d,m={})=>{let w=`${h.bold(d.table)} ${h.dim(d.displayName)}`;b(d.exists===!1?`${w} ${h.dim("(new)")}`:w);for(let x of d.fields){let _=[x.required?"required":null].filter(Boolean).join(", "),g=m.showClaimants&&x.claimedBy?.length?` ${h.dim(`\u2190 ${x.claimedBy.join(", ")}`)}`:"";b(` ${x.column.padEnd(28)} ${x.type}${_?` (${_})`:""}${g}`)}m.showClaimants&&d.claimants.length>1&&b(h.dim(` claimed by ${d.claimants.join(", ")}`))};function To(d){if(d instanceof Z&&d.status===409){let m=d.body;m?.error==="table_conflict"&&m.message&&(L(m.message),process.exit(1))}v(`tables: ${d instanceof Error?d.message:String(d)}`)}let Hd=async d=>{let m=Qe(oe(process.cwd(),d),"public","turbobase.json"),w=await se.readFile(m,"utf8").catch(()=>null);if(w===null)return null;let x;try{x=JSON.parse(w)}catch(g){v(`${m} is not valid JSON: ${g instanceof Error?g.message:String(g)}`)}let _=x.tables;return _===void 0?{tables:[],manifestPath:m}:(Array.isArray(_)||v(`${m}: "tables" must be an array.`),{tables:_,manifestPath:m})};mn.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:m,token:w}=await V(d.baseUrl),x=new z({baseUrl:m,token:w}),_;try{_=await x.tablesList()}catch(g){G(g,"extensions:deploy","list tables"),v(`list tables failed: ${g instanceof Error?g.message:String(g)}`)}if(d.json)return b(JSON.stringify(_,null,2));if(_.length===0)return b("no custom tables yet \u2014 create one with `turbobase tables create <slug>`.");b(ht(_.map(g=>[g.table,g.displayName,String(g.fields.length),g.status,g.claimedBy.join(", ")||"\u2014"]),{head:["TABLE","NAME","FIELDS","STATUS","CLAIMED BY"]}))}),mn.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,m)=>{let w=await Hd(d??".");if(w||v(`no public/turbobase.json under ${oe(process.cwd(),d??".")}.`),w.tables.length===0)return b(`${w.manifestPath} declares no tables \u2014 nothing to plan.`);let{baseUrl:x,token:_}=await V(m.baseUrl),g=new z({baseUrl:x,token:_}),T;try{T=await J("planning tables",()=>g.tablesPlan({claimedBy:m.as,tables:w.tables}))}catch(E){G(E,"extensions:deploy","plan tables"),To(E)}b(`claiming as ${h.bold(T.claimedBy)}`);for(let E of T.tables)ko(E,{showClaimants:!0});b(""),b(h.dim("nothing was applied \u2014 run `turbobase tables apply` (or deploy the app) to make it so."))}),mn.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,m)=>{let w=await Hd(d??".");if(w||v(`no public/turbobase.json under ${oe(process.cwd(),d??".")}.`),w.tables.length===0)return b(`${w.manifestPath} declares no tables \u2014 nothing to apply.`);let{baseUrl:x,token:_}=await V(m.baseUrl),g=new z({baseUrl:x,token:_}),T;try{T=await J("applying tables",()=>g.tablesApply({claimedBy:m.as,tables:w.tables}))}catch(E){G(E,"extensions:deploy","apply tables"),To(E)}for(let E of T.tables)ko(E,{showClaimants:!0});b(`applied ${T.tables.length} table${T.tables.length===1?"":"s"} as ${T.claimedBy}.`)}),mn.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,m)=>[...m??[],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,m)=>{let w=m.field.map(E=>{let[A,I,N]=E.split(":");return(!A||!I)&&v(`--field "${E}" must be name:type[:required] (e.g. title:text:required).`),N!==void 0&&N!=="required"&&v(`--field "${E}": the third part may only be "required".`),{name:A,type:I,...N==="required"?{required:!0}:{}}}),{baseUrl:x,token:_}=await V(m.baseUrl),g=new z({baseUrl:x,token:_}),T;try{T=await J(`creating cr_${d}`,()=>g.tablesApply({tables:[{slug:d,displayName:m.name??d,fields:w,...m.restrictToOwner?{rowLevelOwner:!0}:{}}]}))}catch(E){G(E,"extensions:deploy","create a table"),To(E)}for(let E of T.tables)ko(E);b(""),b(h.dim("grant access under Settings \u203A Users, or in the Records page permissions matrix."))}),mn.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,m)=>{m.confirm!==d&&v(`drop refused: --confirm "${m.confirm}" does not match "${d}".`);let{baseUrl:w,token:x}=await V(m.baseUrl),_=new z({baseUrl:w,token:x}),g;try{g=await J(`dropping cr_${d}`,()=>_.tablesDrop(d,m.confirm))}catch(T){G(T,"extensions:deploy","drop a table"),v(`drop failed: ${T instanceof Error?T.message:String(T)}`)}if(!g.removed)return b(`no table named "${d}" on this instance \u2014 nothing dropped.`);b(`dropped ${g.table} and every row in it.`)});let Ni=e.command("sandbox").description("Ephemeral local authoring sandboxes.");Ni.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 m=Md(),w;try{w=await vm(m,{fromTenant:d.from,ttl:d.ttl,sample:d.sample,baseUrl:d.baseUrl})}catch(x){x instanceof En&&v(x.message),v(`sandbox create failed: ${x instanceof Error?x.message:String(x)}`)}b(`sandbox ${w.name} ready.`),b(` source: ${w.fromTenant} (sampled ${Math.round(w.sampleFraction*100)}%, redacted)`),b(` url: ${w.url}`),b(` expires: ${w.expiresAt}`)}),Ni.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 m=Md();await qa(m).catch(()=>[]);let w;try{w=await _m(m,d)}catch(x){v(`sandbox destroy failed: ${x instanceof Error?x.message:String(x)}`)}w||v(`no sandbox named '${d}'.`),b(`destroyed sandbox ${w.name} (dropped ${w.database}).`)}),Ni.command("list").description("List active sandboxes with TTL/expiry; reaps any past their TTL.").action(async()=>{let d=Md(),m=await qa(d).catch(()=>[]),w=await is(),{active:x}=Ma(w);if(x.length===0)b("no active sandboxes.");else{for(let _ of x)b(` ${_.name} from=${_.fromTenant} expires=${_.expiresAt} ${_.url}`);b(`
|
|
3748
3753
|
${x.length} active sandbox(es).`)}m.length>0&&b(`(reaped ${m.length} expired sandbox(es).)`)}),o.command("export").description("Pull the live instance into turbobase/bundle.json (adopt an instance into a repo).").option("--base-url <url>","Instance base URL (defaults to the active instance).").option("--out <dir>","Directory to write the bundle into.",Jo).action(async d=>{let{baseUrl:m,token:w}=await V(d.baseUrl),x=new z({baseUrl:m,token:w}),_;try{_=await J("exporting instance config",()=>x.configExport())}catch(E){E instanceof Z&&(E.status===401||E.status===403)&&v("ext export forbidden \u2014 the stored PAT lacks extensions:deploy. Re-run `turbobase auth login`."),v(`ext export failed: ${E instanceof Error?E.message:String(E)}`)}let g=typeof _.serialized=="string"?_.serialized:`${JSON.stringify(_.manifest??_,null,2)}
|
|
3749
3754
|
`,T=oe(process.cwd(),d.out,Sn);await se.mkdir(qt(T),{recursive:!0}),await se.writeFile(T,g,"utf8"),b(`wrote ${d.out}/${Sn} (${g.split(`
|
|
3750
|
-
`).length} lines).`)});let Kd=o.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),Rb=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],Vd=()=>process.env.TURBOBASE_HOME?.trim()||Qe(ZP(),".turbobase"),Eb=()=>"1.
|
|
3755
|
+
`).length} lines).`)});let Kd=o.command("extras").description("Manage the optional native packages `ext test` and `ext replay` need."),Rb=[{name:"esbuild",needs:"compiling an extension (`test`, `replay`)"},{name:"isolated-vm",needs:"executing an extension in the sandbox (`test`, `replay`)"}],Vd=()=>process.env.TURBOBASE_HOME?.trim()||Qe(ZP(),".turbobase"),Eb=()=>"1.33.0",xo=async()=>Promise.all(Rb.map(async d=>{try{return await import(d.name),{...d,present:!0}}catch{return{...d,present:!1}}}));Kd.command("status").description("Report which optional native packages are installed.").action(async()=>{let d=await xo();for(let m of d)b(`${m.present?be.success("installed"):be.warn("missing")} ${h.bold(m.name)} \u2014 ${m.needs}`);d.some(m=>!m.present)&&b(`
|
|
3751
3756
|
run \`${Tn} ext extras install\` to fetch them into ${Vd()}.`)}),Kd.command("install").description("Install the optional native packages into $TURBOBASE_HOME (needs npm).").action(async()=>{let d=await xo();if(d.every(A=>A.present)){b(be.success("extras already installed \u2014 nothing to do."));return}let m=Vd();await se.mkdir(m,{recursive:!0});let w=d.filter(A=>!A.present).map(A=>A.name),x=Oa(process.platform,process.arch),_=async(A,I)=>{let{code:N,stderr:U}=await _o("npm",["install","--prefix",m,"--no-audit","--no-fund",...A]);N!==0&&v(`extras install failed while installing ${I} (npm exited ${N}).
|
|
3752
3757
|
${U.trim()}
|
|
3753
3758
|
These are native packages \u2014 they need npm and a working toolchain. Everything except \`test\` and \`replay\` works without them.`)},g=w.filter(A=>A!=="isolated-vm");if(g.length>0){let A=g.map(I=>`${I}@${Ca[I]??"latest"}`);b(`installing ${A.join(", ")} \u2192 ${m}/node_modules \u2026`),await _(A,g.join(", "))}if(w.includes("isolated-vm")){let A=`isolated-vm@${Ca["isolated-vm"]}`;if(b(`installing ${A} \u2192 ${m}/node_modules \u2026`),await _(["--ignore-scripts",A],"isolated-vm"),mm.includes(x)){let N=`${Ia}@${Eb()}`;b(`fetching ${N} (no upstream binary for ${x}) \u2026`);let{code:U}=await _o("npm",["install","--prefix",m,"--no-audit","--no-fund","--ignore-scripts",N]);U!==0&&b(be.warn(`could not fetch ${N} \u2014 falling back to a source build.`))}let I=await hm({home:m});I.length>0&&b(` ${be.success("prebuilt")} ${I.join(", ")} \u2192 isolated-vm/prebuilds/${x}`)}let T=await xo(),E=T.filter(A=>!A.present).map(A=>A.name);if(E.includes("isolated-vm")){b(be.warn(`no prebuilt isolated-vm for ${x} / Node ${process.versions.node} (${Na()}) \u2014 building from source. This needs Python and a C++ toolchain, and takes a few minutes.`));let{code:A}=await _o("npm",["rebuild","--prefix",m,"isolated-vm"]);A===0&&(T=await xo(),E=T.filter(I=>!I.present).map(I=>I.name))}E.length>0&&v(`npm succeeded but ${E.join(", ")} still will not load \u2014 no prebuilt binary for ${x} / Node ${process.versions.node} (${Na()}), and the source build did not produce one either. Everything except \`test\` and \`replay\` works without them.`),b(be.success("extras installed \u2014 `turbobase ext test` and `turbobase ext replay` are ready."))});let Pr=e.command("release").description("Ship a release to your instance: deploy | status | rollback | list.");return Pr.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 m=d.target==="approved"?"approved":d.target==="auto"?"auto":null;m||v(`--target must be 'auto' or 'approved' (got '${d.target}').`);let w=await mA(oe(process.cwd(),d.project??".")),x=Object.keys(w).length;!d.solution&&x===0&&v("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 qd(["rev-parse",d.ref])??d.ref:null)??process.env.GITHUB_SHA??await qd(["rev-parse","HEAD"]),T=d.gitRepo??process.env.GITHUB_REPOSITORY??await qd(["remote","get-url","origin"]),E=d.gitRef??process.env.GITHUB_REF??null,A=d.prUrl??process.env.PR_URL??null,{baseUrl:I,token:N}=await V(d.baseUrl),U=new z({baseUrl:I,token:N}),C;try{C=await U.releaseDeploy({solutionId:d.solution??null,target:m,approvedBy:d.approvedBy??null,files:x>0?w:null,gitRepo:T,gitRef:E,gitSha:g,prUrl:A})}catch(H){G(H,"releases:write","release deploy"),H instanceof Z&&H.status===404&&v("solution_not_found \u2014 no solution with that id."),v(`release deploy failed: ${H instanceof Error?H.message:String(H)}`)}b(`release ${C.releaseId} enqueued \u2192 taskId=${C.taskId} (target=${m}`+(x>0?`, uploaded ${x} bundle file(s)`:", live-fetch")+")."),d.follow&&await _a(()=>U.releaseStatus(C.releaseId),`release ${C.releaseId}`,d.intervalMs)}),Pr.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,m)=>{let{baseUrl:w,token:x}=await V(m.baseUrl),_=new z({baseUrl:w,token:x}),g;try{g=await _.releaseFiles(d)}catch(A){A instanceof Z&&A.status===404&&v(`release ${d} not found.`),A instanceof Z&&A.status===409&&v(`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"),v(`release pull failed: ${A instanceof Error?A.message:String(A)}`)}let T=oe(process.cwd(),m.dir),E=0;for(let[A,I]of[["owned",g.owned],["context",g.context]])for(let[N,U]of Object.entries(I)){let C=Qe(T,A,N);await se.mkdir(qt(C),{recursive:!0}),await se.writeFile(C,U,"utf8"),E++}await se.writeFile(Qe(T,".turbobase-release.json"),JSON.stringify({releaseId:g.releaseId,version:g.version,contentDigest:g.contentDigest},null,2),"utf8"),b(`pulled release ${g.releaseId} (${g.status}) \u2192 ${T}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turbobase/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
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",
|