@fui-org/fui-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/dist/fui.js +37 -34
- package/package.json +2 -2
- package/skills/fui/SKILL.md +11 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ v1 covers the database workflow:
|
|
|
9
9
|
- schema
|
|
10
10
|
- stored procedures
|
|
11
11
|
|
|
12
|
-
It shares the Fui MCP workspace
|
|
12
|
+
It shares the Fui MCP workspace, so existing connections work immediately.
|
|
13
13
|
|
|
14
14
|
Design: [docs/specs/2026-09-28-fui-cli-v1-design.md](docs/specs/2026-09-28-fui-cli-v1-design.md)
|
|
15
15
|
|
|
@@ -45,12 +45,15 @@ bun link
|
|
|
45
45
|
|
|
46
46
|
### Release
|
|
47
47
|
|
|
48
|
+
npm never accepts the same version twice, so bump before every publish (`patch` for fixes, `minor` for features):
|
|
49
|
+
|
|
48
50
|
```bash
|
|
49
51
|
bun test && bun run typecheck && bun run lint && bun run format:check
|
|
50
|
-
bun
|
|
52
|
+
bun pm version patch --no-git-tag-version
|
|
53
|
+
bun publish --otp <code>
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
`bun publish` packs `dist/fui.js`, `skills/` and `README.md`, and needs npm access to the `@fui-org` scope.
|
|
56
|
+
`bun publish` packs `dist/fui.js`, `skills/` and `README.md`, and needs npm access to the `@fui-org` scope and 2FA on the account.
|
|
54
57
|
|
|
55
58
|
## Use with an AI agent
|
|
56
59
|
|
|
@@ -70,6 +73,22 @@ fui sp get spAPI_Student_Select
|
|
|
70
73
|
fui sp deploy spAPI_Student_Select
|
|
71
74
|
```
|
|
72
75
|
|
|
76
|
+
## Workspace location
|
|
77
|
+
|
|
78
|
+
`fui` picks the workspace folder in this order:
|
|
79
|
+
|
|
80
|
+
1. `--workdir <dir>`
|
|
81
|
+
2. `FUI_MCP_WORKDIR` env var
|
|
82
|
+
3. `fui config set workdir <dir>`, saved in `~/.config/fui/config.json` (respects `XDG_CONFIG_HOME`)
|
|
83
|
+
4. `FUI_MCP_WORKDIR` of the Fui MCP server in `~/.claude.json` (project scope first, then user scope; respects `CLAUDE_CONFIG_DIR`)
|
|
84
|
+
5. `~/.fui-mcp/workspaces`
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
fui config # effective value and where it came from
|
|
88
|
+
fui config set workdir ~/work/fui
|
|
89
|
+
fui config unset workdir
|
|
90
|
+
```
|
|
91
|
+
|
|
73
92
|
## Connections
|
|
74
93
|
|
|
75
94
|
A dbToken is Base64 of a connection string. The engine is detected from the token:
|
package/dist/fui.js
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
import{parseArgs as
|
|
3
|
+
import{parseArgs as hn}from"util";import{homedir as ut}from"os";import{join as A,resolve as lt}from"path";var C={Ok:0,Runtime:1,Usage:2,Guard:3};class a extends Error{name="UsageError"}class P extends Error{name="GuardError"}function we(e){if(e instanceof a)return C.Usage;if(e instanceof P)return C.Guard;return C.Runtime}var q=["workdir"];function L(e){return e.HOME||ut()}function _(e,t=Bun.env,n=process.cwd()){let o=e.trim();if(o==="~")return L(t);if(o.startsWith("~/"))return A(L(t),o.slice(2));return lt(n,o)}function j(e=Bun.env){let t=e.XDG_CONFIG_HOME||A(L(e),".config");return A(t,"fui","config.json")}async function B(e=Bun.env){let t=j(e),n=Bun.file(t);if(!await n.exists())return{};try{let o=await n.json();if(o&&typeof o==="object"&&!Array.isArray(o))return o}catch{}throw new a(`${t} is not a JSON object; fix or delete it`)}async function X(e,t=Bun.env){let n=j(t);return await Bun.write(n,`${JSON.stringify(e,null,2)}
|
|
4
|
+
`),n}function dt(e){return e.CLAUDE_CONFIG_DIR?A(e.CLAUDE_CONFIG_DIR,".claude.json"):A(L(e),".claude.json")}function pt(e,t){return[t.command,...Array.isArray(t.args)?t.args:[]].join(" ").includes("fui-mcp")||e.toLowerCase().includes("fui")}function ye(e){if(!e||typeof e!=="object")return;for(let[t,n]of Object.entries(e)){let o=n?.env?.FUI_MCP_WORKDIR;if(typeof o==="string"&&o.trim()&&pt(t,n))return o}return}async function mt(e=Bun.env,t=process.cwd()){let n=dt(e),o=Bun.file(n);if(!await o.exists())return null;let r;try{r=await o.json()}catch{return null}let s=ye(r.projects?.[t]?.mcpServers)??ye(r.mcpServers);return s?{path:_(s,e,t),file:n}:null}var ft=A(".fui-mcp","workspaces");async function be(e,t=Bun.env,n=process.cwd()){if(e)return{path:_(e,t,n),source:"flag",detail:"--workdir"};if(t.FUI_MCP_WORKDIR)return{path:_(t.FUI_MCP_WORKDIR,t,n),source:"env",detail:"FUI_MCP_WORKDIR"};let o=await B(t);if(typeof o.workdir==="string"&&o.workdir)return{path:_(o.workdir,t,n),source:"config",detail:j(t)};let r=await mt(t,n);if(r)return{path:r.path,source:"mcp",detail:r.file};return{path:A(L(t),ft),source:"default",detail:"built-in"}}function z(e){return new TextEncoder().encode(e).toBase64()}function Z(e){try{return new TextDecoder("utf-8",{fatal:!0}).decode(Uint8Array.fromBase64(e.trim()))}catch{throw new a("token is not valid Base64 UTF-8")}}function Ee(e,t,n,o){return z(`Server=${e}; Database=${t}; UID=${n}; PWD=${o}; Encrypt=True; TrustServerCertificate=True;`)}function Te(e){let t=new Map;for(let n of e.split(";")){let o=n.indexOf("=");if(o<=0)continue;t.set(n.slice(0,o).trim().toLowerCase(),n.slice(o+1).trim())}return t}function gt(e){let t=e.trim();if(/^postgres(?:ql)?:\/\//i.test(t))return"postgres";if(/^(?:mysql|mariadb):\/\//i.test(t))return"mysql";if(/^(?:sqlite|file):/i.test(t))return"sqlite";let n=Te(t);if(n.has("server")||n.has("data source"))return"mssql";throw new a("unsupported connection: expected postgres://, mysql://, mariadb://, sqlite:/file: URL or MSSQL Server=...; string")}function v(e){let t=Z(e);return{engine:gt(t),plain:t}}function W(e){let{engine:t,plain:n}=v(e);if(t==="mssql"){let r=Te(n);return{engine:t,server:r.get("server")??r.get("data source")??"",database:r.get("database")??r.get("initial catalog")??"",user:r.get("uid")??r.get("user id")??""}}if(t==="sqlite")return{engine:t,server:"",database:n.replace(/^(?:sqlite|file):(?:\/\/)?/i,""),user:""};let o=new URL(n);return{engine:t,server:o.port?`${o.hostname}:${o.port}`:o.hostname,database:decodeURIComponent(o.pathname.replace(/^\//,"")),user:decodeURIComponent(o.username)}}import{mkdir as ht,writeFile as wt}from"fs/promises";import{basename as Ce,dirname as Ae,extname as Re,isAbsolute as yt,join as Se,relative as bt}from"path";var ke=50;function Et(e=new Date){let t=(n)=>String(n).padStart(2,"0");return`${e.getFullYear()}${t(e.getMonth()+1)}${t(e.getDate())}_${t(e.getHours())}${t(e.getMinutes())}${t(e.getSeconds())}`}function Tt(e,t,n){let o=bt(e.root,t),s=o!==""&&!o.startsWith("..")&&!yt(o)?o:Se("_external",Ce(t)),i=Re(s),c=Ce(s,i),l=n?`_${n}`:"";return Se(e.historyRoot(),Ae(s),`${c}_${Et()}${l}${i}`)}async function Ct(e,t){let n=Re(e),o=e.slice(0,e.length-n.length);for(let s=1;s<=ke;s++){let i=s===1?e:`${o}_${s}${n}`;try{return await wt(i,t,{encoding:"utf-8",flag:"wx"}),i}catch(c){if(c.code!=="EEXIST")throw c}}let r=`${o}_${ke}${n}`;return await Bun.write(r,t),r}async function R(e,t,n={}){let o=n.content;if(o===void 0){let s=Bun.file(t);if(!await s.exists())return null;o=await s.text()}let r=Tt(e,t,n.suffix);return await ht(Ae(r),{recursive:!0}),Ct(r,o)}var St=/^[A-Za-z0-9_-]+$/,kt=/^[A-Za-z0-9_.-]+$/;function At(e){if(!St.test(e))throw new a(`invalid alias "${e}": use letters, digits, "_" or "-"`);return e}function H(e){if(!kt.test(e)||e.startsWith("_")||e.startsWith("."))throw new a(`invalid project id "${e}"`);return e}function xe(e){let t=e.split("/");if(t.length===1)return{alias:t[0]};if(t.length===2&&t[0]&&t[1])return{project:t[0],alias:t[1]};throw new a(`invalid --db "${e}": use <alias> or <project>/<alias>`)}function O(e,t){let n=t.toLowerCase();return Object.keys(e.connections).find((o)=>o.toLowerCase()===n)??null}class ee{paths;constructor(e){this.paths=e}async load(e){let t=this.paths.connectionsFile(e),n=Bun.file(t);if(!await n.exists())return{connections:{}};let o=await n.text();if(!o.trim())return{connections:{}};let r;try{r=JSON.parse(o)}catch(i){throw Error(`${t} is not valid JSON (${i.message}); fix it by hand before writing`)}if(!r||typeof r!=="object"||Array.isArray(r))throw Error(`${t} must be an object { default, connections }`);let s=r;if(s.connections===void 0)return{...s,connections:{}};if(!s.connections||typeof s.connections!=="object"||Array.isArray(s.connections))throw Error(`${t}: "connections" must be an object of alias -> config`);return s}async write(e,t){let n=this.paths.connectionsFile(e);await R(this.paths,n),await Bun.write(n,JSON.stringify(t,null,2))}async save(e,t,n,o){let r=await this.load(e),s=O(r,t)??At(t);if(r.connections[s]=n,o||!r.default||!O(r,r.default))r.default=s;return await this.write(e,r),s}async setDefault(e,t){let n=await this.load(e),o=O(n,t);if(!o)throw new a(this.menu(e,n,`no connection "${t}" in project "${e}"`));return n.default=o,await this.write(e,n),o}async listProjects(){let e=[],t=new Bun.Glob("*/_db/_connections.json");try{for await(let n of t.scan({cwd:this.paths.root,onlyFiles:!0})){let o=n.split(/[\\/]/)[0];if(!o.startsWith("_"))e.push(o)}}catch(n){if(n.code!=="ENOENT")throw n}return e.sort()}async resolveProject(e){let t=e.db?xe(e.db):void 0,n=t?.project??e.project??Bun.env.FUI_PROJECT;if(n)return H(n);let o=await this.listProjects();if(t){let s=[];for(let i of o)if(O(await this.load(i),t.alias))s.push(i);if(s.length===1)return s[0];if(s.length===0)throw new a(`no project has connection "${t.alias}"; run: fui db list`);throw new a(`connection "${t.alias}" exists in several projects; use one of:
|
|
4
5
|
`+s.map((i)=>` --db ${i}/${t.alias}`).join(`
|
|
5
6
|
`))}let r=[];for(let s of o)if(Object.keys((await this.load(s)).connections).length>0)r.push(s);if(r.length===1)return r[0];if(r.length===0)throw new a("no DB connections in workspace; run: fui db add <alias> -p <project> --token -");throw new a(`several projects have connections; pass -p <project> or --db <project>/<alias>:
|
|
6
7
|
`+r.map((s)=>` -p ${s}`).join(`
|
|
7
|
-
`))}async resolve(e){let t=await this.resolveProject(e),n=await this.load(t);if(Object.keys(n.connections).length===0)throw new a(`project "${t}" has no DB connections; run: fui db add <alias> -p ${t}`);let r=await this.pickAlias(t,n,e),s=n.connections[r];if(!s.dbToken)throw new a(`connection ${t}/${r} has no dbToken; re-add it`);let{engine:i}=
|
|
8
|
-
`)}}import{
|
|
8
|
+
`))}async resolve(e){let t=await this.resolveProject(e),n=await this.load(t);if(Object.keys(n.connections).length===0)throw new a(`project "${t}" has no DB connections; run: fui db add <alias> -p ${t}`);let r=await this.pickAlias(t,n,e),s=n.connections[r];if(!s.dbToken)throw new a(`connection ${t}/${r} has no dbToken; re-add it`);let{engine:i}=v(s.dbToken);if(i==="mssql"&&!s.apiDomain)throw new a(`connection ${t}/${r} is MSSQL and needs apiDomain; re-add with --api-domain`);return{projectId:t,alias:r,config:s,engine:i,dbToken:s.dbToken}}async pickAlias(e,t,n){let o=Object.keys(t.connections);if(n.db){let{alias:s}=xe(n.db),i=O(t,s);if(!i)throw new a(this.menu(e,t,`no connection "${s}" in project "${e}"`));return i}if(o.length===1)return o[0];if(n.objectName){let s=n.objectName.toLowerCase(),i=[];for(let c of o)if((await this.knownObjectNames(e,c)).has(s))i.push(c);if(i.length===1)return i[0];throw new a(this.menu(e,t,i.length===0?`cannot infer connection for "${n.objectName}"`:`"${n.objectName}" exists in several connections`))}if(n.write)throw new a(this.menu(e,t,`project "${e}" has several connections; writes need --db`));let r=t.default?O(t,t.default):null;if(r)return r;throw new a(this.menu(e,t,`project "${e}" has several connections and no default`))}async knownObjectNames(e,t){let n=new Set,o=Bun.file(this.paths.schemaFile(e,t));if(await o.exists())try{let r=await o.json();for(let s of[r.tables,r.procedures].flatMap((i)=>Array.isArray(i)?i:[])){let i=typeof s==="string"?/^CREATE\s+(?:TABLE|VIEW|PROCEDURE|FUNCTION)\s+(\S+)/i.exec(s):null;if(i)n.add(i[1].toLowerCase())}}catch{}for(let r of await this.localSqlNames(e,t))n.add(r.toLowerCase());return n}async localSqlNames(e,t){let n=[];try{for await(let o of new Bun.Glob("*.sql").scan({cwd:this.paths.connDir(e,t)}))n.push(o.slice(0,-4))}catch(o){if(o.code!=="ENOENT")throw o}return n.sort()}menu(e,t,n){let o=Object.keys(t.connections).map((r)=>` --db ${e}/${r}${t.default===r?" (default)":""}`);return[n,...o].join(`
|
|
9
|
+
`)}}import{join as x}from"path";class te{root;constructor(e){this.root=e}projectDir(e){return x(this.root,e)}dbDir(e){return x(this.projectDir(e),"_db")}connectionsFile(e){return x(this.dbDir(e),"_connections.json")}connDir(e,t){return x(this.dbDir(e),t)}schemaFile(e,t){return x(this.connDir(e,t),"schema.json")}spFile(e,t,n){return x(this.connDir(e,t),`${n}.sql`)}historyRoot(){return x(this.root,"_history")}}var Ne={project:{type:"string",short:"p"},db:{type:"string"},workdir:{type:"string"},json:{type:"boolean"},help:{type:"boolean",short:"h"}};async function Pe(e,t){let n=await be(u(e,"workdir")),o=new te(n.path);return{paths:o,workdir:n,store:new ee(o),values:e,positionals:t,json:e.json===!0}}function u(e,t){let n=e[t];return typeof n==="string"?n:void 0}function _e(e){return e?.code==="EPIPE"}process.stdout.on("error",(e)=>{if(_e(e))process.exit(0);throw e});function f(e){try{process.stdout.write(e)}catch(t){if(_e(t))process.exit(0);throw t}}function h(e){process.stderr.write(e.endsWith(`
|
|
9
10
|
`)?e:`${e}
|
|
10
|
-
`)}async function
|
|
11
|
+
`)}async function Oe(){return Bun.stdin.text()}async function ne(e,t){if(t&&e)throw new a("pass SQL as an argument or with -f, not both");let n;if(t){let o=Bun.file(t);if(!await o.exists())throw new a(`file not found: ${t}`);n=await o.text()}else if(e==="-")n=await Oe();else if(e!==void 0)n=e;else throw new a('missing SQL: pass "<sql>", -f <file>, or - for stdin');if(!n.trim())throw new a("SQL is empty");return n}async function I(e,t,n){if(e!==void 0&&t!==void 0)throw new a(`pass ${n} or ${n}-env, not both`);if(t!==void 0){let r=Bun.env[t];if(!r)throw new a(`env var ${t} is empty or unset`);return r.trim()}if(e===void 0)return;if(e!=="-")throw new a(`${n} takes "-" (stdin) only; use ${n}-env VAR for env vars`);let o=(await Oe()).trim();if(!o)throw new a(`${n}: stdin is empty`);return o}import{mkdir as Pt}from"fs/promises";function Rt(e){if(e===null||e===void 0)return"NULL";if(e instanceof Date)return e.toISOString();if(e instanceof Uint8Array)return`0x${e.toHex()}`;return(typeof e==="object"?JSON.stringify(e):String(e)).replace(/\\/g,"\\\\").replace(/\t/g,"\\t").replace(/\r/g,"\\r").replace(/\n/g,"\\n")}function $e(e){let t=[...new Set(e.flatMap((n)=>Object.keys(n)))];return[t,...e.map((n)=>t.map((o)=>Rt(n[o])))]}function xt(e){if(e.length===0)return"";return`${$e(e).map((t)=>t.join("\t")).join(`
|
|
11
12
|
`)}
|
|
12
|
-
`}function
|
|
13
|
+
`}function Nt(e){if(e.length===0)return"";let t=$e(e),n=t[0].map((r,s)=>Math.max(...t.map((i)=>Bun.stringWidth(i[s]))));return`${t.map((r)=>r.map((s,i)=>i===r.length-1?s:s+" ".repeat(n[i]-Bun.stringWidth(s))).join(" ")).join(`
|
|
13
14
|
`)}
|
|
14
|
-
`}function
|
|
15
|
-
`}function
|
|
16
|
-
|
|
17
|
-
`)}},
|
|
18
|
-
|
|
19
|
-
`)}},
|
|
20
|
-
`)
|
|
15
|
+
`}function re(e){return`${JSON.stringify(e,(t,n)=>typeof n==="bigint"?n.toString():n)}
|
|
16
|
+
`}function T(e,t,n=process.stdout.isTTY===!0){if(t)return re(e);return n?Nt(e):xt(e)}function Ie(e,t){let n=t>0&&e.length>t;return{rows:n?e.slice(0,t):e,total:e.length,truncated:n}}function Le(e){let t=e[0];if(!t)throw new a(`missing <key>; keys: ${q.join(", ")}`);if(!q.includes(t))throw new a(`unknown key "${t}"; keys: ${q.join(", ")}`);return t}var _t={name:"config",usage:"fui config",summary:"Show settings with their effective value and source",async run({workdir:e,json:t}){h(`# config file: ${j()}`),f(T([{key:"workdir",value:e.path,source:e.source,from:e.detail}],t))}},Ot={name:"config set",usage:"fui config set workdir <path>",summary:"Save a setting to the fui config file",async run({positionals:e}){let t=Le(e),n=e[1];if(!n)throw new a(`missing <value> for ${t}`);let o=_(n);await Pt(o,{recursive:!0});let r=await B();r[t]=o;let s=await X(r);if(Bun.env.FUI_MCP_WORKDIR)h("# note: FUI_MCP_WORKDIR is set in this shell and overrides the config file");f(`set ${t}=${o} in ${s}
|
|
17
|
+
`)}},$t={name:"config unset",usage:"fui config unset workdir",summary:"Remove a setting from the fui config file",async run({positionals:e}){let t=Le(e),n=await B();delete n[t];let o=await X(n);f(`unset ${t} in ${o}
|
|
18
|
+
`)}},je=[_t,Ot,$t];var{SQL:It}=globalThis.Bun;class oe{engine;sql;constructor(e,t){this.engine=e;this.sql=new It(t)}async query(e){return[...await this.sql.unsafe(e)]}async exec(e){return{count:(await this.sql.unsafe(e)).count??null}}close(){return this.sql.close()}}function se(e){return e.trim().replace(/^https?:\/\//i,"").replace(/\/.*$/,"").toLowerCase()}function D(e){let t=se(e);return`${/^(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/.test(t)?"http":"https"}://${t}`}function Lt(e){try{let t=JSON.parse(e),n=t.Message??t.message??t.error??t.ExceptionMessage;if(typeof n==="string"&&n)return n.slice(0,400)}catch{}return e.slice(0,400)}async function ve(e,t,n,o){let r;try{r=await fetch(`${e}${t}`,{method:"POST",headers:{"Content-Type":"application/json",authorization:`Bearer ${n}`},body:JSON.stringify({sqlcmd:o}),signal:AbortSignal.timeout(30000)})}catch(i){if(i instanceof Error&&i.name==="TimeoutError")throw Error("tAPI timed out after 30s; narrow the query with TOP/WHERE");throw i}let s=await r.text();if(!r.ok)throw Error(`tAPI HTTP ${r.status}: ${Lt(s)||r.statusText}`);try{return JSON.parse(s)}catch{throw Error(`tAPI returned non-JSON: ${s.slice(0,400)}`)}}class ie{origin;dbToken;engine="mssql";constructor(e,t){this.origin=e;this.dbToken=t}async query(e){return(await ve(this.origin,"/help/_sql_execute",this.dbToken,e)).data??[]}exec(e){return ve(this.origin,"/help/_sql_execute_NonQuery",this.dbToken,e)}async close(){}}async function ae(e,t,n,o,r){let s=`${e}/help/${t}/${n?"auth/":""}${o}`,i={};if(!n&&r)i.authorization=`Bearer ${r}`;let c=await fetch(s,{headers:i,signal:AbortSignal.timeout(15000)});return{url:s,status:c.status,ok:c.ok,text:await c.text()}}function jt(e){if(e.engine==="mssql")return new ie(D(e.config.apiDomain),e.dbToken);return new oe(e.engine,Z(e.dbToken))}async function E(e,t){let n=jt(e);try{return await t(n)}finally{await n.close()}}var vt={name:"db list",usage:"fui db list [-p project]",summary:"List connections across the workspace (no secrets)",async run({store:e,values:t,json:n}){let o=u(t,"project"),r=o?[H(o)]:await e.listProjects(),s=[];for(let i of r){let c=await e.load(i);for(let[l,d]of Object.entries(c.connections)){let g="invalid";try{if(d.dbToken)g=v(d.dbToken).engine}catch{}s.push({db:`${i}/${l}`,engine:g,default:c.default===l?"*":"",apiDomain:d.apiDomain??"",database:d.database??"",userToken:d.userToken?"yes":"no"})}}if(s.length===0){h("no connections; add one: fui db add <alias> -p <project> --token -");return}f(T(s,n))}},Dt={name:"db add",usage:`fui db add <alias> -p <project> (--token - | --token-env VAR | --conn - | --conn-env VAR)
|
|
19
|
+
[--api-domain host] [--user-token-env VAR] [--default] [--no-test]`,summary:"Add or replace a connection; tests it with SELECT 1",options:{token:{type:"string"},"token-env":{type:"string"},conn:{type:"string"},"conn-env":{type:"string"},"api-domain":{type:"string"},"user-token-env":{type:"string"},default:{type:"boolean"},"no-test":{type:"boolean"}},async run({store:e,values:t,positionals:n}){let o=n[0];if(!o)throw new a("missing <alias>");let r=u(t,"project");if(!r)throw new a("missing -p <project>");let s=H(r),i=await I(u(t,"token"),u(t,"token-env"),"--token"),c=await I(u(t,"conn"),u(t,"conn-env"),"--conn");if(i===void 0===(c===void 0))throw new a("pass exactly one of --token/--token-env or --conn/--conn-env");let l=i??z(c),d=W(l),g=(await e.load(s)).connections,p=Object.entries(g).find(([k])=>k.toLowerCase()===o.toLowerCase())?.[1],m=u(t,"api-domain")??p?.apiDomain;if(d.engine==="mssql"&&!m)throw new a("MSSQL connections need --api-domain <host>");let w={...p,dbToken:l};if(m)w.apiDomain=se(m);w.sqlServer=d.server,w.database=d.database;let y=await I(void 0,u(t,"user-token-env"),"--user-token");if(y)w.userToken=y;if(t["no-test"]!==!0){let k=Bun.nanoseconds();await E({projectId:s,alias:o,config:w,engine:d.engine,dbToken:l},(M)=>M.query("SELECT 1 AS ok")),h(`test ok ${Math.round((Bun.nanoseconds()-k)/1e6)}ms`)}let N=await e.save(s,o,w,t.default===!0);f(`saved ${s}/${N} ${d.engine} ${d.database}
|
|
20
|
+
`)}},Ft={name:"db default",usage:"fui db default <project>/<alias>",summary:"Set the project default connection",async run({store:e,values:t,positionals:n}){let o=n[0]??u(t,"db");if(!o)throw new a("missing <project>/<alias>");let r=await e.resolveProject({db:o,project:u(t,"project")}),s=o.split("/").pop(),i=await e.setDefault(r,s);f(`default ${r}/${i}
|
|
21
|
+
`)}},Ut={name:"db test",usage:"fui db test [--db ref]",summary:"Run SELECT 1 and report latency",async run({store:e,values:t}){let n=await e.resolve({db:u(t,"db"),project:u(t,"project")}),o=Bun.nanoseconds();await E(n,(s)=>s.query("SELECT 1 AS ok"));let r=Math.round((Bun.nanoseconds()-o)/1e6);f(`ok ${n.projectId}/${n.alias} ${n.engine} ${r}ms
|
|
22
|
+
`)}},De=[vt,Dt,Ft,Ut];function S(e,t={}){let n=t.strings!==!1,o=e.split(""),r=0;while(r<e.length)if(e[r]==="-"&&e[r+1]==="-")while(r<e.length&&e[r]!==`
|
|
23
|
+
`)o[r++]=" ";else if(e[r]==="/"&&e[r+1]==="*"){let s=e.indexOf("*/",r+2),i=s===-1?e.length:s+2;while(r<i)o[r++]=" "}else if(e[r]==="'"){if(n)o[r]=" ";r++;while(r<e.length){if(e[r]==="'"){if(n)o[r]=" ";if(r++,e[r]==="'"){if(n)o[r]=" ";r++;continue}break}if(n)o[r]=" ";r++}}else r++;return o.join("")}function ce(e){return S(e,{strings:!1})}function ue(e){let t=S(e),n=[],o=0;for(let r of t.matchAll(/;|^[ \t]*GO[ \t]*$/gim))n.push({start:o,end:r.index}),o=r.index+r[0].length;return n.push({start:o,end:e.length}),n.filter((r)=>r.end>r.start)}function le(e,t=","){let n=[],o=0,r="";for(let s of e){if(s==="(")o++;else if(s===")")o--;if(s===t&&o===0)n.push(r),r="";else r+=s}if(r.trim())n.push(r);return n}var Mt=/\bALTER\s+TABLE\s+(?:(?:\[[^\]]*\]|\w+)\s*\.\s*)*(?:\[[^\]]*\]|\w+)\s+(?:WITH\s+(?:CHECK|NOCHECK)\s+)?(\w+)/i,qt=new Set(["ADD","ALTER","DROP"]);function Bt(e){let t=ce(e),n=null;for(let o of ue(e)){let r=t.slice(o.start,o.end),s=e.slice(o.start,o.end).trim();if(/\bDROP\s+TABLE\b/i.test(r))return{severity:"block",reason:"DROP TABLE",statement:s};if(!/\bALTER\s+TABLE\b/i.test(r))continue;if(/\bDROP\s+COLUMN\b/i.test(r))return{severity:"block",reason:"ALTER TABLE ... DROP COLUMN",statement:s};if(/\bSWITCH\b/i.test(r))return{severity:"block",reason:"ALTER TABLE ... SWITCH",statement:s};let i=Mt.exec(r)?.[1]?.toUpperCase();if(!i||!qt.has(i))return{severity:"block",reason:`ALTER TABLE ... ${i??"(unknown action)"}`,statement:s};if(!n&&/\bALTER\s+COLUMN\b/i.test(r))n={severity:"confirm",reason:"ALTER TABLE ... ALTER COLUMN (narrowing numeric/date precision loses data silently)",statement:s}}return n}function Fe(e,t){let o=(t==="DELETE"?/\bDELETE\s+(?:TOP\s*\([^)]*\)\s*)?(?:FROM\s+)?(#{1,2}\w+|@\w+|\w+)/i:/\bUPDATE\s+(?:TOP\s*\([^)]*\)\s*)?(#{1,2}\w+|@\w+|\w+)/i).exec(e)?.[1];return Boolean(o&&(o.startsWith("#")||o.startsWith("@")))}function Wt(e){let t=ce(e);for(let{start:n,end:o}of ue(e)){let r=t.slice(n,o);if(/\bWHERE\b/i.test(r))continue;let s=e.slice(n,o).trim();if(/\bDELETE\b/i.test(r)&&!Fe(r,"DELETE"))return{severity:"block",reason:"DELETE without WHERE",statement:s};if(/\bUPDATE\b/i.test(r)&&/\bSET\b/i.test(r)&&!Fe(r,"UPDATE"))return{severity:"block",reason:"UPDATE without WHERE",statement:s}}return null}function de(e){let t=S(e),n=[];if(/\bINSERT\b/i.test(t))n.push("INSERT");if(/\bUPDATE\b/i.test(t)&&/\bSET\b/i.test(t))n.push("UPDATE");if(/\bDELETE\b/i.test(t))n.push("DELETE");if(/\bMERGE\b/i.test(t))n.push("MERGE");if(/\bTRUNCATE\b/i.test(t))n.push("TRUNCATE");return n}function pe(e,t){let n=Bt(e);if(n?.severity==="block")return n;let o=Wt(e);if(o)return o;if(n&&!t)return n;let r=de(e);if(r.length>0&&!t)return{severity:"confirm",reason:`writes real data (${r.join(", ")})`,statement:e.trim()};return null}var Ht=200,Ue=400;function Me(e){let t=e.statement.length>Ue?`${e.statement.slice(0,Ue)}...`:e.statement,n=e.severity==="block"?"blocked: no flag unlocks this; the user must run it by hand":"needs --confirm-write; pass it only after the user approved this write";return new P(`${e.reason}
|
|
21
24
|
${n}
|
|
22
25
|
---
|
|
23
|
-
${t}`)}function
|
|
24
|
-
`)}}
|
|
26
|
+
${t}`)}function Gt(e){if(e===void 0)return Ht;let t=Number(e);if(!Number.isInteger(t)||t<0)throw new a("--limit must be an integer >= 0 (0 = no limit)");return t}var Kt={name:"query",usage:'fui query ("<sql>" | -f file | -) [--db ref] [--limit 200] [--confirm-write] [--json]',summary:"Run SQL that returns rows; prints TSV",options:{file:{type:"string",short:"f"},limit:{type:"string"},"confirm-write":{type:"boolean"}},async run({store:e,values:t,positionals:n,json:o}){let r=await ne(n[0],u(t,"file")),s=Gt(u(t,"limit")),i=t["confirm-write"]===!0,c=pe(r,i);if(c)throw Me(c);let l=await e.resolve({db:u(t,"db"),project:u(t,"project"),write:de(r).length>0}),d=await E(l,(p)=>p.query(r)),g=Ie(d,s);f(T(g.rows,o)),h(g.truncated?`# rows: ${g.rows.length} of ${g.total} (truncated; use --limit or TOP/WHERE)`:`# rows: ${g.total}`)}},Jt={name:"exec",usage:'fui exec ("<sql>" | -f file | -) [--db ref] [--confirm-write]',summary:"Run DDL/DML that returns no rows",options:{file:{type:"string",short:"f"},"confirm-write":{type:"boolean"}},async run({store:e,values:t,positionals:n,json:o}){let r=await ne(n[0],u(t,"file")),s=pe(r,t["confirm-write"]===!0);if(s)throw Me(s);let i=await e.resolve({db:u(t,"db"),project:u(t,"project"),write:!0}),c=await E(i,(d)=>d.exec(r)),l=`${i.projectId}/${i.alias}`;f(o?re({db:l,result:c}):`ok ${l} ${JSON.stringify(c)}
|
|
27
|
+
`)}},qe=[Kt,Jt];var Be=`
|
|
25
28
|
SELECT
|
|
26
29
|
o.type AS ObjectTypeCode, o.name AS ObjectName, c.name AS ColumnName, t.name AS DataType,
|
|
27
30
|
c.max_length, c.precision, c.scale, c.is_nullable, c.is_identity,
|
|
@@ -45,7 +48,7 @@ WHERE o.is_ms_shipped = 0 AND o.type IN ('U','V') AND s.name NOT IN ('sys','INFO
|
|
|
45
48
|
AND o.name NOT LIKE 'sys%' AND o.name NOT LIKE 'dt[_]%'
|
|
46
49
|
AND o.name NOT LIKE 'queue[_]%' AND o.name NOT LIKE 'filestream[_]%' AND o.name NOT LIKE 'cdc.%'
|
|
47
50
|
ORDER BY ObjectTypeCode, ObjectName
|
|
48
|
-
`.trim(),
|
|
51
|
+
`.trim(),We=`
|
|
49
52
|
SELECT
|
|
50
53
|
o.type AS ObjectTypeCode, o.name AS ObjectName,
|
|
51
54
|
p.name AS ParameterName, pt.name AS ParameterType,
|
|
@@ -60,31 +63,31 @@ WHERE o.is_ms_shipped = 0 AND o.type IN ('P','FN','TF','IF') AND s.name NOT IN (
|
|
|
60
63
|
AND o.name NOT IN ('fn_diagramobjects','sp_alterdiagram','sp_creatediagram','sp_dropdiagram',
|
|
61
64
|
'sp_helpdiagramdefinition','sp_helpdiagrams','sp_renamediagram','sp_upgraddiagrams')
|
|
62
65
|
ORDER BY ObjectTypeCode, ObjectName
|
|
63
|
-
`.trim(),
|
|
66
|
+
`.trim(),Qt=new Set(["nvarchar","nchar","ntext"]);function He(e,t,n,o){let r=e.toLowerCase();if(t===-1)return`${e}(max)`;if(r==="decimal"||r==="numeric")return o!==void 0?`${e}(${n},${o})`:`${e}(${n})`;let s=Qt.has(r)?t/2:t;return s>0?`${e}(${s})`:e}function Vt(e){let t=[String(e.ColumnName),He(String(e.DataType??""),Number(e.max_length??0),Number(e.precision??0),Number(e.scale??0)),e.is_nullable?"NULL":"NOT NULL"];if(e.IsPrimaryKey)t.push("PRIMARY KEY");if(e.is_identity)t.push("IDENTITY");if(e.ForeignKeyName)t.push(`REFERENCES ${String(e.RefTable??"")}(${String(e.RefColumn??"")})`);return t.join(" ")}function Yt(e,t){if(t)return"Function";if(/^spAPIFILE_/i.test(e))return"API File";if(/^spAPI_/i.test(e))return"API";return"Procedure"}function Ge(e,t){let n=new Map;for(let r of e){let s=String(r.ObjectName??"");if(!s)continue;let i=n.get(s);if(!i)i={keyword:String(r.ObjectTypeCode??"").trim()==="V"?"VIEW":"TABLE",columns:[]},n.set(s,i);if(r.ColumnName)i.columns.push(Vt(r))}let o=new Map;for(let r of t){let s=String(r.ObjectName??""),i=String(r.ObjectTypeCode??"").trim();if(!s||!["P","FN","TF","IF"].includes(i))continue;let c=o.get(s);if(!c)c={keyword:i==="P"?"PROCEDURE":"FUNCTION",params:[]},o.set(s,c);if(r.ParameterName){let l=He(String(r.ParameterType??""),Number(r.ParameterMaxLength??0),Number(r.ParameterPrecision??0));c.params.push(`@${String(r.ParameterName).replace(/^@/,"")} ${l}`)}}return{fetchedAt:new Date().toISOString(),tables:[...n].map(([r,s])=>`CREATE ${s.keyword} ${r} (${s.columns.join(",")});`),procedures:[...o].map(([r,s])=>`CREATE ${s.keyword} ${r} (${s.params.join(",")});`)}}function Ke(e){if(!e||typeof e!=="object")return!1;let t=e,n=(o)=>Array.isArray(o)&&o.every((r)=>typeof r==="string");return n(t.tables)&&n(t.procedures)}function G(e){return/^CREATE\s+(?:TABLE|VIEW|PROCEDURE|FUNCTION)\s+(\S+)/i.exec(e)?.[1]??null}function K(e){let t=/^CREATE\s+(TABLE|VIEW|PROCEDURE|FUNCTION)\s+(\S+)/i.exec(e);if(!t)return"Unknown";let n=t[1].toUpperCase();if(n==="TABLE")return"Table";if(n==="VIEW")return"View";return Yt(t[2],n==="FUNCTION")}function me(e){let t=e.match(/^(spAPIFILE_UPLOAD_|spAPIFILE_|spAPI_)(AUTH_)?(.+)$/i);if(!t)return null;return{prefix:t[1],isAuth:Boolean(t[2]),fnName:t[3]}}function Je(e){let t=S(e).match(/\b(?:CREATE|ALTER)\s+(?:PROCEDURE|PROC|FUNCTION)\s+(?:(?:\[[^\]]+\]|\w+)\s*\.\s*){0,2}(?:\[([^\]]+)\]|(\w+))/i);if(!t)return null;return(t[1]??t[2])||null}function Qe(e){return/\b(?:CREATE|ALTER)\s+FUNCTION\b/i.test(S(e))}function Ve(e,t){let n={sql:e,changed:!1,from:null,to:null};if(t===null)return n;let o=[...S(e).matchAll(/\b(CREATE|ALTER)(\s+)(?:PROCEDURE|PROC|FUNCTION)\b/gi)];if(o.length!==1)return n;let r=o[0],s=r[1].toUpperCase(),i=t?"ALTER":"CREATE";if(s===i)return n;return{sql:e.slice(0,r.index)+i+e.slice(r.index+r[1].length),changed:!0,from:s,to:i}}function Ye(e){let t=S(e),n=t.match(/(?:CREATE|ALTER)\s+(?:PROCEDURE|PROC|FUNCTION)\s+(?:(?:\[[^\]]+\]|\w+)\s*\.\s*){0,2}(?:\[[^\]]+\]|\w+)\s*\(?/i);if(!n||n.index===void 0)return[];let o=n.index+n[0].length,r=n[0].endsWith("(")?1:0,s=t.length,i=/\bAS\b/gi;i.lastIndex=o;for(let p=i.exec(t);p;p=i.exec(t)){let m=t.slice(o,p.index);if((m.match(/\(/g)??[]).length+r===(m.match(/\)/g)??[]).length){s=p.index;break}}let c=(p)=>p.replace(/^\(/,"").replace(/\)\s*$/,""),l=le(c(t.slice(o,s))),d=le(c(e.slice(o,s))),g=[];return l.forEach((p,m)=>{if(/^\s*@\w+[\s\S]*=/.test(p))g.push((d[m]??p).trim())}),g}function F(e){return`N'${e.replace(/'/g,"''")}'`}function J(e){return`GRANT EXECUTE ON [dbo].[${e}] TO [public] AS [dbo]`}function Xe(e){return`SELECT granted = COUNT(*) FROM sys.database_permissions p JOIN sys.database_principals u ON u.principal_id = p.grantee_principal_id WHERE p.class = 1 AND p.major_id = OBJECT_ID(${F(e)}) AND p.permission_name = 'EXECUTE' AND p.state IN ('G','W') AND u.name = 'public'`}var Xt=/^[A-Za-z0-9_]+$/,ze=50;function U(e,t){if(e.engine!=="mssql")throw new a(`${t} supports MSSQL connections only; ${e.alias} is ${e.engine}`)}async function zt(e,t){let n=new Map;for(let o=0;o<t.length;o+=ze){let r=t.slice(o,o+ze),s=await e.query(`SELECT o.name AS name, ISNULL(OBJECT_DEFINITION(o.object_id), '') AS sqlText FROM sys.objects o WHERE o.name IN (${r.map(F).join(",")})`);for(let i of s){let c=String(i.sqlText??"");if(i.name&&c.trim())n.set(String(i.name).toLowerCase(),c)}}return n}async function Ze(e,t){U(t,"schema pull");let{projectId:n,alias:o}=t;return E(t,async(r)=>{let[s,i]=await Promise.all([r.query(Be),r.query(We)]),c=Ge(s,i);await Bun.write(e.paths.schemaFile(n,o),JSON.stringify(c,null,2));let l=(await e.localSqlNames(n,o)).filter((m)=>Xt.test(m)),d=await zt(r,l),g=[],p=[];for(let m of l){let w=d.get(m.toLowerCase());if(w===void 0){p.push(`${m}.sql`);continue}let y=e.paths.spFile(n,o,m);if(await Bun.file(y).text()===w)continue;await R(e.paths,y),await Bun.write(y,w),g.push(`${m}.sql`)}return{schema:c,refreshed:g,localOnly:p}})}async function fe(e,t){let n=Bun.file(e.paths.schemaFile(t.projectId,t.alias));if(await n.exists()){try{let o=await n.json();if(Ke(o))return o}catch{}h("# schema cache unreadable; pulling")}else h("# no schema cache; pulling");return(await Ze(e,t)).schema}function Zt(e){let t=new Map;for(let n of[...e.tables,...e.procedures]){let o=K(n);t.set(o,(t.get(o)??0)+1)}return[...t].map(([n,o])=>`${n}=${o}`).join(" ")}var en={name:"schema",usage:"fui schema [--db ref] [--filter text] [--tables | --procs]",summary:"Print compact DDL (one line per object) from the local cache",options:{filter:{type:"string"},tables:{type:"boolean"},procs:{type:"boolean"}},async run({store:e,values:t}){let n=await e.resolve({db:u(t,"db"),project:u(t,"project")});U(n,"schema");let o=await fe(e,n),r=u(t,"filter")?.toLowerCase(),s=t.tables===!0,c=[...t.procs===!0?[]:o.tables,...s?[]:o.procedures].filter((l)=>!r||(G(l)??"").toLowerCase().includes(r));if(h(`# ${n.projectId}/${n.alias} fetchedAt=${o.fetchedAt} objects=${c.length}`),c.length>0)f(`${c.join(`
|
|
64
67
|
`)}
|
|
65
|
-
`)}},
|
|
68
|
+
`)}},tn={name:"schema pull",usage:"fui schema pull [--db ref]",summary:"Refresh schema.json and local .sql bodies from the server (MSSQL)",async run({store:e,values:t}){let n=await e.resolve({db:u(t,"db"),project:u(t,"project")}),o=await Ze(e,n),r=[`ok ${n.projectId}/${n.alias} ${Zt(o.schema)}`];if(o.refreshed.length>0)r.push(`refreshed (old copy in _history): ${o.refreshed.join(" ")}`);if(o.localOnly.length>0)r.push(`local only, not on server: ${o.localOnly.join(" ")}`);f(`${r.join(`
|
|
66
69
|
`)}
|
|
67
|
-
`)}},
|
|
68
|
-
`)}},
|
|
69
|
-
`)}},
|
|
70
|
-
`)}},
|
|
71
|
-
`+
|
|
72
|
-
`))}let p=`${o}/${r}`,m=[];await E(n,async(
|
|
70
|
+
`)}},et=[en,tn];import{homedir as rn}from"os";import{join as tt}from"path";var ge="---\nname: fui\ndescription: Use the `fui` CLI to work with FUI (low-code platform) databases \u2014 list DB connections, run SQL, read schema, fetch/edit/deploy stored procedures (tAPI spAPI_*). Use instead of the Fui MCP db_* tools whenever the `fui` command is available.\n---\n\n# fui CLI\n\n`fui` is the shell replacement for the Fui MCP `db_*` tools. Run it with your shell tool. It shares the Fui MCP workspace, so connections added in either tool work in both. `fui config` shows which workspace folder is in use and why.\n\n## Output contract\n\n- stdout carries data only. Query results are TSV with a header row; `NULL` means SQL null; tab, newline and backslash inside cells are escaped as `\\t`, `\\n`, `\\\\`.\n- stderr carries `# ...` notes (row counts, truncation) and `error: ...` / `guard: ...` messages.\n- Add `--json` for compact single-line JSON instead of TSV.\n- Exit codes: `0` ok, `1` runtime error (network/SQL), `2` usage error (read the message; it lists valid choices), `3` blocked by a safety guard.\n\n## Pick a connection\n\n```bash\nfui db list # db, engine, default, apiDomain, database, userToken\n```\n\n- `--db <alias>` or `--db <project>/<alias>`. `-p <project>` or env `FUI_PROJECT` also selects the project.\n- With no `--db`: the only connection, else the project default. `sp` commands infer the connection from the object name.\n- Writes (`exec`, write SQL in `query`) never fall back to the default when a project has several connections. Pass `--db`.\n- Engines come from the token. `mssql` goes through tAPI over HTTPS. `postgres`, `mysql` and `sqlite` connect directly with Bun.SQL. `schema` and `sp` commands are MSSQL-only.\n\n## Read data\n\n```bash\nfui query \"SELECT TOP 20 * FROM tblStudent WHERE ClassID = 5\" --db lhu-docs/docs\nfui query -f report.sql --limit 0 # --limit 0 = no row cap (default 200)\necho \"SELECT 1 AS x\" | fui query -\n```\n\nNarrow with `TOP`/`WHERE` before raising `--limit`. Every row costs tokens.\n\n## Structure (MSSQL)\n\n```bash\nfui schema --filter student # one compact DDL line per table/view/proc\nfui schema --procs --filter spAPI_ # only procedures/functions\nfui schema pull # refresh cache + local .sql bodies from server\nfui sp list --filter Student # name, kind (API, API File, Procedure, Function, local-only), local\n```\n\nPrefer `fui schema` over querying `sys.*` yourself. The cache pulls automatically the first time.\n\n## Change stored procedures (MSSQL)\n\n1. `fui sp get spAPI_Student_Select` saves the live body to `_db/<alias>/<name>.sql` and prints the path.\n For a new SP: `fui sp path spAPI_New --db <ref>` prints where to create the file.\n2. Edit that file with your file tools. `CREATE` or `ALTER` both work; deploy fixes the verb.\n3. `fui sp deploy spAPI_Student_Select` does all of this:\n - archives the live body to `_history/` as the rollback copy\n - runs the file\n - grants `EXECUTE` to `[public]` for new procedures and verifies the grant\n - clears the tAPI parameter cache for `spAPI_*` names\n4. If the output says the grant failed, do **not** redeploy. Run the printed `fui exec \"GRANT ...\"` line.\n\n`fui sp help spAPI_X` shows the tAPI parameter list and clears its cache without redeploying.\n\n## Write data (needs the user)\n\n```bash\nfui exec \"UPDATE tblX SET Status = 1 WHERE ID = 42\" --db lhu-docs/docs --confirm-write\n```\n\n- Any `INSERT`/`UPDATE`/`DELETE`/`MERGE`/`TRUNCATE` or `ALTER COLUMN` exits `3` until `--confirm-write` is added.\n- Add `--confirm-write` **only after the user approved that exact write in chat**. Never add it on your own to get past the guard.\n- These are always blocked, with no flag to unlock them: `DROP TABLE`, `ALTER TABLE ... DROP COLUMN`/`SWITCH`, and `DELETE`/`UPDATE` without `WHERE`. Show the SQL to the user to run by hand.\n- Never run a write or call a live API SP \"to test\" it. Read the SP body instead.\n\n## FUI conventions that matter here\n\n- tAPI exposes `spAPI_<Fn>` as `/<alias>/<Fn>`, `spAPI_AUTH_<Fn>` as `/<alias>/auth/<Fn>`, and `spAPIFILE_*` for file endpoints. The alias is the connection name.\n- tAPI SP parameters must not have default values (`@x int = 0`). Deploy blocks them with exit `3`.\n- Signal errors in SPs with `RAISERROR`. Callers read `Message`.\n- Every new procedure needs `GRANT EXECUTE ... TO [public]`. Deploy handles it; raw `fui exec CREATE PROCEDURE` does not.\n\n## Connections and tokens\n\n```bash\nprintf %s \"$TOKEN\" | fui db add docs -p lhu-docs --token - --api-domain tapi.lhu.edu.vn\nfui db add local -p demo --conn-env PG_URL # postgres://user:pass@host/db\nfui db add cache -p demo --conn-env SQLITE_URL # sqlite:///abs/path/app.db\nfui db default lhu-docs/docs\nfui db test --db lhu-docs/docs\nfui token decode --db lhu-docs/docs # engine, server, database, user (no password)\nfui token encode --server S --database D --uid U --pwd-env PWD # Base64 MSSQL dbToken\n```\n\n- A dbToken is Base64 of a connection string.\n- Secrets are accepted only from stdin (`-`) or `--*-env VAR`. Never echo, log or paste token values.\n- `db add` tests with `SELECT 1` before saving (`--no-test` skips the test). The previous `_connections.json` is archived to `_history/`.\n\n## Workspace location\n\nThe workspace is picked in this order: `--workdir` flag, then `FUI_MCP_WORKDIR` env, then `fui config set workdir`, then the Fui MCP workdir in `~/.claude.json`, then `~/.fui-mcp/workspaces`.\n\n```bash\nfui config # effective workdir and its source\nfui config set workdir ~/work/fui # persist a custom location\nfui config unset workdir\n```\n\n## Help\n\n`fui --help` lists commands. `fui <command> --help` shows usage for one command.\n";var on={name:"skill",usage:"fui skill",summary:"Print the agent skill (how to use this CLI)",async run(){f(ge)}},sn={name:"skill install",usage:"fui skill install [--dir <dir>]",summary:"Write SKILL.md to ~/.claude/skills/fui/ (or --dir)",options:{dir:{type:"string"}},async run({values:e}){let t=u(e,"dir")??tt(rn(),".claude","skills","fui"),n=tt(t,"SKILL.md");await Bun.write(n,ge),f(`${n}
|
|
71
|
+
`)}},nt=[on,sn];var an=/^[A-Za-z0-9_]+$/,rt=2000;function Q(e){let t=e[0];if(!t)throw new a("missing <name>");if(!an.test(t))throw new a(`invalid object name "${t}": letters, digits and "_" only`);return t}async function V({store:e,values:t},n){let o=await e.resolve({db:u(t,"db"),project:u(t,"project"),objectName:n});return U(o,"sp"),o}async function ot(e,t){let n=await e.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(${F(t)})), '') AS sqlText`);return String(n[0]?.sqlText??"")}async function cn(e,t){try{let n=await e.query(Xe(t)),o=Number(n[0]?.granted??Number.NaN);return Number.isFinite(o)?o>0:null}catch{return null}}var un={name:"sp list",usage:"fui sp list [--db ref] [--filter text]",summary:"List procedures/functions from the schema cache plus local-only .sql files",options:{filter:{type:"string"}},async run(e){let{store:t,values:n,json:o}=e,r=await t.resolve({db:u(n,"db"),project:u(n,"project")});U(r,"sp list");let s=await fe(t,r),i=new Set((await t.localSqlNames(r.projectId,r.alias)).map((p)=>p.toLowerCase())),c=u(n,"filter")?.toLowerCase(),l=[],d=new Set;for(let p of s.procedures){let m=G(p);if(!m)continue;d.add(m.toLowerCase()),l.push({name:m,kind:K(p),local:i.has(m.toLowerCase())?"yes":"no"})}for(let p of await t.localSqlNames(r.projectId,r.alias))if(!d.has(p.toLowerCase()))l.push({name:p,kind:"local-only",local:"yes"});let g=l.filter((p)=>!c||p.name.toLowerCase().includes(c));h(`# ${r.projectId}/${r.alias} count=${g.length}`),f(T(g,o))}},ln={name:"sp get",usage:"fui sp get <name> [--db ref] [--print]",summary:"Fetch a definition from the server into _db/<alias>/<name>.sql",options:{print:{type:"boolean"}},async run(e){let t=Q(e.positionals),n=await V(e,t),o=await E(n,(i)=>ot(i,t));if(!o.trim())throw Error(`"${t}" not found on ${n.projectId}/${n.alias}`);let r=e.store.paths.spFile(n.projectId,n.alias,t),s=Bun.file(r);if(await s.exists()&&await s.text()!==o)h(`# old local copy archived: ${await R(e.store.paths,r)}`);await Bun.write(r,o),f(e.values.print===!0?o:`${r}
|
|
72
|
+
`)}},dn={name:"sp path",usage:"fui sp path <name> [--db ref]",summary:"Print the local .sql path to edit (file may not exist yet)",async run(e){let t=Q(e.positionals),n=await V(e,t);f(`${e.store.paths.spFile(n.projectId,n.alias,t)}
|
|
73
|
+
`)}},pn={name:"sp deploy",usage:"fui sp deploy <name> [--db ref]",summary:"Deploy the local .sql: archive live body, align CREATE/ALTER, grant, clear tAPI cache",async run(e){let t=Q(e.positionals),n=await V(e,t),{projectId:o,alias:r}=n,s=e.store.paths,i=s.spFile(o,r,t),c=Bun.file(i);if(!await c.exists())throw new a(`file not found: ${i}; run: fui sp get ${t} or create it`);let l=await c.text();if(!l.trim())throw new a(`file is empty: ${i}`);let d=Je(l)??t,g=me(d);if(g){let w=Ye(l);if(w.length>0)throw new P(`tAPI SP parameters cannot have default values; remove "= ..." then deploy again:
|
|
74
|
+
`+w.map((y)=>` ${y}`).join(`
|
|
75
|
+
`))}let p=`${o}/${r}`,m=[];await E(n,async(w)=>{let y=null,N="";try{N=(await ot(w,d)).trim(),y=N.length>0}catch{}if(N){let b=await R(s,i,{content:N});m.push(`rollback copy: ${b}`)}let k=Ve(l,y);await w.exec(k.sql),m.unshift(`ok deployed ${d} to ${p}${k.changed?` (ran as ${k.to}, file unchanged)`:""}`);let M=Qe(l),he=y===null?/\bCREATE\s+(?:PROC|PROCEDURE)\b/i.test(l):!y;if(he&&!M)try{await w.exec(J(d)),m.push("grant EXECUTE to [public]: applied")}catch(b){m.push(`grant FAILED (${b.message}); do not redeploy, run: fui exec "${J(d)}" --db ${p}`)}if(!M){let b=await cn(w,d);if(b===!1)m.push(`[public] lacks EXECUTE; tAPI calls will fail. run: fui exec "${J(d)}" --db ${p}`);else if(b===!0&&!he)m.push("[public] has EXECUTE")}if(g)try{let b=await ae(D(n.config.apiDomain),r,g.isAuth,g.fnName,n.config.userToken);m.push(b.ok?`tAPI cache cleared (HTTP ${b.status})`:`tAPI /help HTTP ${b.status}; ignored`)}catch(b){m.push(`tAPI /help failed (${b.message}); ignored`)}}),f(`${m.join(`
|
|
73
76
|
`)}
|
|
74
|
-
`)}},
|
|
75
|
-
`),!r.ok)process.exitCode=1}},
|
|
76
|
-
`)}},
|
|
77
|
-
exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked`;function
|
|
77
|
+
`)}},mn={name:"sp help",usage:"fui sp help <name> [--db ref]",summary:"Call tAPI /help for an spAPI_* SP: shows params and clears the tAPI cache",async run(e){let t=Q(e.positionals),n=me(t);if(!n)throw new a(`"${t}" is not a tAPI SP (spAPI_*, spAPIFILE_*)`);let o=await V(e,t),r=await ae(D(o.config.apiDomain),o.alias,n.isAuth,n.fnName,o.config.userToken),s=r.text.length>rt?`${r.text.slice(0,rt)}...`:r.text;if(h(`# GET ${r.url} -> HTTP ${r.status}`),f(`${s}
|
|
78
|
+
`),!r.ok)process.exitCode=1}},st=[un,ln,dn,pn,mn];var fn={name:"token decode",usage:"fui token decode [--db ref]",summary:"Show engine, server, database and user of a stored token (password never shown)",async run({store:e,values:t,json:n}){let o=await e.resolve({db:u(t,"db"),project:u(t,"project")}),r={db:`${o.projectId}/${o.alias}`,...W(o.dbToken)};f(T([r],n))}},gn={name:"token encode",usage:"fui token encode --server S --database D --uid U --pwd-env VAR",summary:"Print a Fui MSSQL dbToken (Base64) built like Fui MCP",options:{server:{type:"string"},database:{type:"string"},uid:{type:"string"},"pwd-env":{type:"string"}},async run({values:e}){let t=u(e,"server"),n=u(e,"database"),o=u(e,"uid"),r=await I(void 0,u(e,"pwd-env"),"--pwd");if(!t||!n||!o||!r)throw new a("need --server, --database, --uid and --pwd-env");f(`${Ee(t,n,o,r)}
|
|
79
|
+
`)}},it=[fn,gn];var Y=[...De,...it,...qe,...et,...st,...nt,...je],ct=`global: -p/--project <id> --db <alias|project/alias> --workdir <dir> --json -h/--help
|
|
80
|
+
exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked`;function at(){let e=Math.max(...Y.map((n)=>n.name.length));return`fui: FUI platform CLI (DB)
|
|
78
81
|
|
|
79
82
|
commands:
|
|
80
|
-
${
|
|
83
|
+
${Y.map((n)=>` ${n.name.padEnd(e)} ${n.summary}`).join(`
|
|
81
84
|
`)}
|
|
82
85
|
|
|
83
|
-
${
|
|
86
|
+
${ct}
|
|
84
87
|
run "fui skill" for the agent guide
|
|
85
|
-
`}function
|
|
88
|
+
`}function wn(e){return`${e.usage}
|
|
86
89
|
${e.summary}
|
|
87
|
-
${
|
|
88
|
-
`}function
|
|
89
|
-
`),
|
|
90
|
-
usage: ${n.usage}`)}if(r.values.help===!0)return f(
|
|
90
|
+
${ct}
|
|
91
|
+
`}function yn(e){let[t,n]=e;if(!t||t.startsWith("-"))return null;if(n&&!n.startsWith("-")){let r=Y.find((s)=>s.name===`${t} ${n}`);if(r)return{command:r,rest:e.slice(2)}}let o=Y.find((r)=>r.name===t);return o?{command:o,rest:e.slice(1)}:null}async function bn(e){let t=yn(e);if(!t){let r=e[0];if(!r||r==="-h"||r==="--help"||r==="help")return f(at()),C.Ok;return h(`unknown command: ${e.slice(0,2).join(" ")}
|
|
92
|
+
`),h(at()),C.Usage}let{command:n,rest:o}=t;try{let r;try{r=hn({args:o,options:{...Ne,...n.options},allowPositionals:!0,strict:!0})}catch(s){throw new a(`${s.message}
|
|
93
|
+
usage: ${n.usage}`)}if(r.values.help===!0)return f(wn(n)),C.Ok;return await n.run(await Pe(r.values,r.positionals)),typeof process.exitCode==="number"?process.exitCode:C.Ok}catch(r){let s=we(r),i=s===C.Guard?"guard":"error";return h(`${i}: ${r.message}`),s}}if(import.meta.main)process.exitCode=await bn(Bun.argv.slice(2));export{bn as main};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fui-org/fui-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Token-lean CLI for the FUI low-code platform, successor to Fui MCP",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -42,4 +42,4 @@
|
|
|
42
42
|
"engines": {
|
|
43
43
|
"bun": ">=1.4.0"
|
|
44
44
|
}
|
|
45
|
-
}
|
|
45
|
+
}
|
package/skills/fui/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Use the `fui` CLI to work with FUI (low-code platform) databases
|
|
|
5
5
|
|
|
6
6
|
# fui CLI
|
|
7
7
|
|
|
8
|
-
`fui` is the shell replacement for the Fui MCP `db_*` tools. Run it with your shell tool. It shares the Fui MCP workspace
|
|
8
|
+
`fui` is the shell replacement for the Fui MCP `db_*` tools. Run it with your shell tool. It shares the Fui MCP workspace, so connections added in either tool work in both. `fui config` shows which workspace folder is in use and why.
|
|
9
9
|
|
|
10
10
|
## Output contract
|
|
11
11
|
|
|
@@ -94,6 +94,16 @@ fui token encode --server S --database D --uid U --pwd-env PWD # Base64 MSSQL
|
|
|
94
94
|
- Secrets are accepted only from stdin (`-`) or `--*-env VAR`. Never echo, log or paste token values.
|
|
95
95
|
- `db add` tests with `SELECT 1` before saving (`--no-test` skips the test). The previous `_connections.json` is archived to `_history/`.
|
|
96
96
|
|
|
97
|
+
## Workspace location
|
|
98
|
+
|
|
99
|
+
The workspace is picked in this order: `--workdir` flag, then `FUI_MCP_WORKDIR` env, then `fui config set workdir`, then the Fui MCP workdir in `~/.claude.json`, then `~/.fui-mcp/workspaces`.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
fui config # effective workdir and its source
|
|
103
|
+
fui config set workdir ~/work/fui # persist a custom location
|
|
104
|
+
fui config unset workdir
|
|
105
|
+
```
|
|
106
|
+
|
|
97
107
|
## Help
|
|
98
108
|
|
|
99
109
|
`fui --help` lists commands. `fui <command> --help` shows usage for one command.
|