@fui-org/fui-cli 2.2.2 → 2.2.3
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.
|
@@ -72,7 +72,7 @@ ${t}`,e.severity==="block"?"hard":"soft")}function mc(e,t){let n=Qa(t.schema),o=
|
|
|
72
72
|
${u.message}
|
|
73
73
|
the SQL login from ${a} may not have access to this database; pass --borrow-from <project/alias> or --uid <login> --pwd -`)}let l=await Fo(e,n,{alias:t.apiName,connectionName:t.connectionName,dbToken:c,apiDomain:o.apiDomain,sqlServer:r.SQLServer,database:r.DatabaseName,userToken:o.userToken,makeDefault:t.makeDefault,test:!1});return{notes:[...o.note?[o.note]:[],...l.notes,`sql server: ${r.SQLServer} database: ${r.DatabaseName} SQL login from ${a} (credentials not printed)`]}}async function Ac(e,t){let n=await Yn(e),o=await Yr(e.store,n,t),{apiDomain:s,userToken:r}=o.config;return{projectId:n,call:(i,a)=>Jn(s,r,i,a),target:{ModuleID:0,APIName:t??o.config.apiName??o.alias}}}async function Nc(e,t,n){let{projectId:o,call:s,target:r}=await Ac(e,n),a=(await s(t==="system"?"SM_SystemRight_Select":"SM_FunctionRight_Select",r)).data??[];return await Zr(e.store.paths,o,r.APIName,t==="system"?{system:a}:{functions:a}),{apiName:r.APIName,rows:a}}async function Dc(e,t,n){let{projectId:o,call:s,target:r}=await Ac(e,n),i=t.kind==="system"?"SM_SystemRight_Select":"SM_FunctionRight_Select",a;if(t.kind==="system"){if(t.mode==="new"){let d=((await s(i,r)).data??[]).find((p)=>Number(p.SysRight)===t.sysRight);if(d)throw new m(`SystemRight ${t.sysRight} of "${r.APIName}" is already defined ("${String(d.Note??"")}"); edit: fui right system update ${t.sysRight} --note ...; remove: fui right system delete ${t.sysRight}`)}await s("SM_SystemRight_Update",{...r,SysRight:t.sysRight,Note:t.note??null,Delete:t.mode==="delete"?1:0});let u=`SystemRight ${t.sysRight}${t.note&&t.mode!=="delete"?` (${t.note})`:""}`;a=`ok ${t.mode==="new"?"created":t.mode==="update"?"saved":"deleted"} ${u} of "${r.APIName}"`}else await s("SM_FunctionRight_Update",{...r,FunctionID:t.functionId,FunctionCode:t.functionCode,FunctionName:t.functionName??null,Delete:t.mode==="delete"?1:0}),a=t.mode==="new"?`ok created FunctionRight code ${t.functionCode}${t.functionName?` (${t.functionName})`:""} for "${r.APIName}"`:t.mode==="update"?`ok saved FunctionRight ID ${t.functionId}: code ${t.functionCode}${t.functionName?` (${t.functionName})`:""} for "${r.APIName}"`:`ok deleted FunctionRight ID ${t.functionId} and its user assignments`;let c=await s(i,r).catch(()=>null),l=c?.data??[];if(c)await Zr(e.store.paths,o,r.APIName,t.kind==="system"?{system:l}:{functions:l});return{apiName:r.APIName,notes:[a,`current list (${l.length}):`],rows:l}}var $s={"api-domain":{type:"string"},"user-token":{type:"string"},"user-token-env":{type:"string"},"user-token-from":{type:"string"}};async function js(e){let t=f(e.values,"user-token-from"),n=await Ee(f(e.values,"user-token"),f(e.values,"user-token-env"),"--user-token");if(t&&n)throw new m("pass --user-token-from or --user-token-env, not both");let o=t?.split("/")[0];if(t&&(!o||t.split("/").length!==2))throw new m("--user-token-from needs <project/connectionName>");return{apiDomain:f(e.values,"api-domain"),userToken:t?(await Ro(e.store,o,t)).userToken:n}}function Qn(e){b(`${e.join(`
|
|
74
74
|
`)}
|
|
75
|
-
`)}var ah={name:"db user-token",usage:"fui db user-token [--db ref] (--from <project/alias> | --token - | --token-env VAR) no source: list fingerprints",summary:"Save a userToken (Bearer for spAPI_* calls) into one connection; never prints tokens",options:{from:{type:"string"},token:{type:"string"},"token-env":{type:"string"}},async run(e){let t=await Ee(f(e.values,"token"),f(e.values,"token-env"),"--token"),n=f(e.values,"from");if(n&&t)throw new m("pass --from or --token/--token-env, not both");let o=await Ec(await Y(e),{db:f(e.values,"db"),userToken:t,tokenSource:n});Qn(o.notes)}},ch={name:"db token-rebuild",usage:"fui db token-rebuild [--db ref] --uid <login> (--pwd - | --pwd-env VAR)",summary:"Rebuild a connection's dbToken after a UID/PWD change (sqlServer/database from the saved connection), test, save",options:{uid:{type:"string"},pwd:{type:"string"},"pwd-env":{type:"string"}},async run(e){let t=f(e.values,"uid");if(!t)throw new m("missing --uid");let n=await Ee(f(e.values,"pwd"),f(e.values,"pwd-env"),"--pwd");if(!n)throw new m("missing --pwd - or --pwd-env VAR");Qn((await Cc(await Y(e),{db:f(e.values,"db"),uid:t,password:n})).notes)}},lh={name:"db alias list",usage:"fui db alias list [--search text] [--api-domain host] [--user-token-from project/connection | --user-token-env VAR]",summary:"Databases you may manage on the acc system (APIName, SQLServer, DatabaseName); read-only",options:{search:{type:"string"},...$s},async run(e){let t=await Rc(await Y(e),{...await js(e),search:f(e.values,"search")});for(let n of t.notes)P(`# ${n}`);if(t.rows.length>0)b(z(t.rows,e.render))}},uh={name:"db alias new",usage:"fui db alias new <apiName> --module-name N --sql-server S --database D --username U (--password - | --password-env VAR) [--module-id N]",summary:"Register a new database as a tAPI alias on acc (ask the user first; app login, never db_owner)",options:{"module-name":{type:"string"},"sql-server":{type:"string"},database:{type:"string"},username:{type:"string"},password:{type:"string"},"password-env":{type:"string"},"module-id":{type:"string"},...$s},async run(e){let{values:t}=e,n=e.positionals[0],o=f(t,"module-name"),s=f(t,"sql-server"),r=f(t,"database"),i=f(t,"username");if(!n||!o||!s||!r||!i)throw new m("need <apiName>, --module-name, --sql-server, --database and --username");let a=await Ee(f(t,"password"),f(t,"password-env"),"--password");if(!a)throw new m("missing --password - or --password-env VAR");let c=f(t,"module-id"),l=c===void 0?void 0:Number(c);if(l!==void 0&&!Number.isInteger(l))throw new m("--module-id must be an integer");let u=await Tc(await Y(e),{...await js(e),apiName:n,moduleName:o,sqlServer:s,database:r,username:i,password:a,moduleId:l});Qn(u.notes)}},dh={name:"db add-by-name",usage:"fui db add-by-name <apiName> [-p P] [--name connectionName] [--borrow-from <project/alias> | --uid login (--pwd - | --pwd-env VAR)] [--default] [--api-domain host] [--user-token-from project/connection | --user-token-env VAR]",summary:"Build a dbToken: look up SQLServer/Database on acc, use a saved or supplied SQL login, test, save, fetch schema",options:{name:{type:"string"},"borrow-from":{type:"string"},uid:{type:"string"},pwd:{type:"string"},"pwd-env":{type:"string"},default:{type:"boolean"},...$s},async run(e){let t=e.positionals[0];if(!t)throw new m("missing <apiName>; see: fui db alias list");let n=await Pc(await Y(e),{...await js(e),apiName:t,connectionName:f(e.values,"name"),borrowFrom:f(e.values,"borrow-from"),sqlUid:f(e.values,"uid"),sqlPassword:await Ee(f(e.values,"pwd"),f(e.values,"pwd-env"),"--pwd"),makeDefault:e.values.default===!0});Qn(n.notes)}};function _c(e){return e.map((t)=>({sysRight:String(t.SysRight??""),note:String(t.Note??"")}))}function Mc(e){return e.map((t)=>({functionId:String(t.FunctionID??""),code:String(t.FunctionCode??""),name:String(t.FunctionName??""),enabled:t.Enable===!1||t.Enable===0?"no":"yes"}))}var Lc={"api-name":{type:"string"}};function Ss(e,t,n,o){let s=e===void 0?Number.NaN:Number(e);if(!Number.isInteger(s)||!n(s))throw new m(`${t} must be ${o}`);return s}function xc(e,t,n){if(e!==void 0&&e.length>t)throw new m(`${n} is at most ${t} characters`);return e}function Oc(e){return{name:`right ${e} list`,usage:`fui right ${e} list [-p P] [--api-name alias]`,summary:e==="system"?"SystemRight levels (0-9, 9 = Project Admin) of a project on acc; refreshes _db/rights.json":"FunctionRight codes of a project on acc (vueData.user.FunctionRight); refreshes _db/rights.json",options:Lc,async run(t){let{apiName:n,rows:o}=await Nc(await Y(t),e,f(t.values,"api-name"));if(P(`# ${e==="system"?"SystemRight":"FunctionRight"} of "${n}": ${o.length}`),o.length===0){P(`# none defined; create one: fui right ${e} new ...`);return}b(z(e==="system"?_c(o):Mc(o),t.render))}}}function hn(e,t){let n=e==="system"?`fui right system ${t} <0-9>${t==="delete"?"":" [--note text]"} [--api-name alias]`:t==="new"?"fui right function new --code N [--name text] [--api-name alias]":t==="update"?"fui right function update <functionId> --code N [--name text] [--api-name alias]":"fui right function delete <functionId> [--code N] [--api-name alias]",o={system:{new:"Define a SystemRight level that does not exist yet (server pre-check; 9 is always Project Admin)",update:"Change the note of an existing SystemRight level",delete:"DELETE a SystemRight level (ask the user first; the server refuses while users hold it)"},function:{new:"Create a FunctionRight code (the server assigns FunctionID; duplicates rejected)",update:"Edit an existing FunctionRight by its FunctionID",delete:"DELETE a FunctionRight and every user assignment of it (ask the user first)"}};return{name:`right ${e} ${t}`,usage:n,summary:o[e][t],options:{...Lc,note:{type:"string"},code:{type:"string"},name:{type:"string"}},async run(s){let{values:r,positionals:i}=s,a;if(e==="system")a={kind:e,mode:t,sysRight:Ss(i[0],"<level>",(l)=>l>=0&&l<=9,"an integer 0-9"),note:t==="delete"?void 0:xc(f(r,"note"),50,"--note")};else{let l=f(r,"code");a={kind:e,mode:t,functionId:t==="new"?0:Ss(i[0],"<functionId>",(u)=>u>0,"a positive integer"),functionCode:t==="delete"&&l===void 0?0:Ss(l,"--code",()=>!0,"an integer"),functionName:t==="delete"?void 0:xc(f(r,"name"),100,"--name")}}let c=await Dc(await Y(s),a,f(r,"api-name"));Qn(c.notes),b(z(e==="system"?_c(c.rows):Mc(c.rows),s.render))}}}var Fc=[ah,ch,lh,uh,dh,Oc("system"),hn("system","new"),hn("system","update"),hn("system","delete"),Oc("function"),hn("function","new"),hn("function","update"),hn("function","delete")];import{mkdir as jh}from"node:fs/promises";import{execFile as mh,spawn as hh}from"node:child_process";import{realpathSync as fh}from"node:fs";import{join as Uc,sep as St}from"node:path";var le="2.2.
|
|
75
|
+
`)}var ah={name:"db user-token",usage:"fui db user-token [--db ref] (--from <project/alias> | --token - | --token-env VAR) no source: list fingerprints",summary:"Save a userToken (Bearer for spAPI_* calls) into one connection; never prints tokens",options:{from:{type:"string"},token:{type:"string"},"token-env":{type:"string"}},async run(e){let t=await Ee(f(e.values,"token"),f(e.values,"token-env"),"--token"),n=f(e.values,"from");if(n&&t)throw new m("pass --from or --token/--token-env, not both");let o=await Ec(await Y(e),{db:f(e.values,"db"),userToken:t,tokenSource:n});Qn(o.notes)}},ch={name:"db token-rebuild",usage:"fui db token-rebuild [--db ref] --uid <login> (--pwd - | --pwd-env VAR)",summary:"Rebuild a connection's dbToken after a UID/PWD change (sqlServer/database from the saved connection), test, save",options:{uid:{type:"string"},pwd:{type:"string"},"pwd-env":{type:"string"}},async run(e){let t=f(e.values,"uid");if(!t)throw new m("missing --uid");let n=await Ee(f(e.values,"pwd"),f(e.values,"pwd-env"),"--pwd");if(!n)throw new m("missing --pwd - or --pwd-env VAR");Qn((await Cc(await Y(e),{db:f(e.values,"db"),uid:t,password:n})).notes)}},lh={name:"db alias list",usage:"fui db alias list [--search text] [--api-domain host] [--user-token-from project/connection | --user-token-env VAR]",summary:"Databases you may manage on the acc system (APIName, SQLServer, DatabaseName); read-only",options:{search:{type:"string"},...$s},async run(e){let t=await Rc(await Y(e),{...await js(e),search:f(e.values,"search")});for(let n of t.notes)P(`# ${n}`);if(t.rows.length>0)b(z(t.rows,e.render))}},uh={name:"db alias new",usage:"fui db alias new <apiName> --module-name N --sql-server S --database D --username U (--password - | --password-env VAR) [--module-id N]",summary:"Register a new database as a tAPI alias on acc (ask the user first; app login, never db_owner)",options:{"module-name":{type:"string"},"sql-server":{type:"string"},database:{type:"string"},username:{type:"string"},password:{type:"string"},"password-env":{type:"string"},"module-id":{type:"string"},...$s},async run(e){let{values:t}=e,n=e.positionals[0],o=f(t,"module-name"),s=f(t,"sql-server"),r=f(t,"database"),i=f(t,"username");if(!n||!o||!s||!r||!i)throw new m("need <apiName>, --module-name, --sql-server, --database and --username");let a=await Ee(f(t,"password"),f(t,"password-env"),"--password");if(!a)throw new m("missing --password - or --password-env VAR");let c=f(t,"module-id"),l=c===void 0?void 0:Number(c);if(l!==void 0&&!Number.isInteger(l))throw new m("--module-id must be an integer");let u=await Tc(await Y(e),{...await js(e),apiName:n,moduleName:o,sqlServer:s,database:r,username:i,password:a,moduleId:l});Qn(u.notes)}},dh={name:"db add-by-name",usage:"fui db add-by-name <apiName> [-p P] [--name connectionName] [--borrow-from <project/alias> | --uid login (--pwd - | --pwd-env VAR)] [--default] [--api-domain host] [--user-token-from project/connection | --user-token-env VAR]",summary:"Build a dbToken: look up SQLServer/Database on acc, use a saved or supplied SQL login, test, save, fetch schema",options:{name:{type:"string"},"borrow-from":{type:"string"},uid:{type:"string"},pwd:{type:"string"},"pwd-env":{type:"string"},default:{type:"boolean"},...$s},async run(e){let t=e.positionals[0];if(!t)throw new m("missing <apiName>; see: fui db alias list");let n=await Pc(await Y(e),{...await js(e),apiName:t,connectionName:f(e.values,"name"),borrowFrom:f(e.values,"borrow-from"),sqlUid:f(e.values,"uid"),sqlPassword:await Ee(f(e.values,"pwd"),f(e.values,"pwd-env"),"--pwd"),makeDefault:e.values.default===!0});Qn(n.notes)}};function _c(e){return e.map((t)=>({sysRight:String(t.SysRight??""),note:String(t.Note??"")}))}function Mc(e){return e.map((t)=>({functionId:String(t.FunctionID??""),code:String(t.FunctionCode??""),name:String(t.FunctionName??""),enabled:t.Enable===!1||t.Enable===0?"no":"yes"}))}var Lc={"api-name":{type:"string"}};function Ss(e,t,n,o){let s=e===void 0?Number.NaN:Number(e);if(!Number.isInteger(s)||!n(s))throw new m(`${t} must be ${o}`);return s}function xc(e,t,n){if(e!==void 0&&e.length>t)throw new m(`${n} is at most ${t} characters`);return e}function Oc(e){return{name:`right ${e} list`,usage:`fui right ${e} list [-p P] [--api-name alias]`,summary:e==="system"?"SystemRight levels (0-9, 9 = Project Admin) of a project on acc; refreshes _db/rights.json":"FunctionRight codes of a project on acc (vueData.user.FunctionRight); refreshes _db/rights.json",options:Lc,async run(t){let{apiName:n,rows:o}=await Nc(await Y(t),e,f(t.values,"api-name"));if(P(`# ${e==="system"?"SystemRight":"FunctionRight"} of "${n}": ${o.length}`),o.length===0){P(`# none defined; create one: fui right ${e} new ...`);return}b(z(e==="system"?_c(o):Mc(o),t.render))}}}function hn(e,t){let n=e==="system"?`fui right system ${t} <0-9>${t==="delete"?"":" [--note text]"} [--api-name alias]`:t==="new"?"fui right function new --code N [--name text] [--api-name alias]":t==="update"?"fui right function update <functionId> --code N [--name text] [--api-name alias]":"fui right function delete <functionId> [--code N] [--api-name alias]",o={system:{new:"Define a SystemRight level that does not exist yet (server pre-check; 9 is always Project Admin)",update:"Change the note of an existing SystemRight level",delete:"DELETE a SystemRight level (ask the user first; the server refuses while users hold it)"},function:{new:"Create a FunctionRight code (the server assigns FunctionID; duplicates rejected)",update:"Edit an existing FunctionRight by its FunctionID",delete:"DELETE a FunctionRight and every user assignment of it (ask the user first)"}};return{name:`right ${e} ${t}`,usage:n,summary:o[e][t],options:{...Lc,note:{type:"string"},code:{type:"string"},name:{type:"string"}},async run(s){let{values:r,positionals:i}=s,a;if(e==="system")a={kind:e,mode:t,sysRight:Ss(i[0],"<level>",(l)=>l>=0&&l<=9,"an integer 0-9"),note:t==="delete"?void 0:xc(f(r,"note"),50,"--note")};else{let l=f(r,"code");a={kind:e,mode:t,functionId:t==="new"?0:Ss(i[0],"<functionId>",(u)=>u>0,"a positive integer"),functionCode:t==="delete"&&l===void 0?0:Ss(l,"--code",()=>!0,"an integer"),functionName:t==="delete"?void 0:xc(f(r,"name"),100,"--name")}}let c=await Dc(await Y(s),a,f(r,"api-name"));Qn(c.notes),b(z(e==="system"?_c(c.rows):Mc(c.rows),s.render))}}}var Fc=[ah,ch,lh,uh,dh,Oc("system"),hn("system","new"),hn("system","update"),hn("system","delete"),Oc("function"),hn("function","new"),hn("function","update"),hn("function","delete")];import{mkdir as jh}from"node:fs/promises";import{execFile as mh,spawn as hh}from"node:child_process";import{realpathSync as fh}from"node:fs";import{join as Uc,sep as St}from"node:path";var le="2.2.3";var Vo="@fui-org/fui-cli",gh=3600000,wh=5000;function Hc(e=process.env){return Uc(st(e),"..","update.json")}async function fn(e=process.env){try{let t=await tn(Hc(e));if(t&&typeof t==="object"&&!Array.isArray(t))return t}catch{}return{}}async function Xe(e,t=process.env){await L(Hc(t),`${JSON.stringify(e,null,2)}
|
|
76
76
|
`)}var yh=`${St}node_modules${St}@fui-org${St}fui-cli${St}`,bh=[[`${St}install${St}global`,"bun",!1],[`${St}lib`,"npm",!1],[`${St}npm`,"npm",!0]];function Wo(e=gt()){let t;try{t=fh(e)}catch{return null}for(let[n,o,s]of bh){let r=`${n}${yh}`,i=t.indexOf(r);if(i>=0){let a=t.slice(0,s?i+n.length:i);return{dir:t.slice(0,i+r.length-1),manager:o,prefix:a}}}return null}function Bo(e=gt()){return Wo(e)?.dir??null}function qo(e,t){return e==="bun"?`bun add -g ${Vo}@${t}`:`npm install -g ${Vo}@${t}`}function kh(e){return/^[\w@./:=\\-]+$/.test(e)?e:`"${e.replaceAll('"',"")}"`}function Is(e,t,n=process.env,o=process.platform==="win32"){return new Promise((s)=>{mh(e,o?t.map(kh):t,{env:n,shell:o,maxBuffer:16777216,windowsHide:!0},(r,i,a)=>s({ok:!r,output:`${i}${a}`.trim()}))})}function Cs(e=process.env){let t=e.CI?.trim().toLowerCase();return!!t&&!["0","false"].includes(t)}async function Es(e=process.env){let t=e.FUI_AUTO_UPDATE?.trim().toLowerCase();if(t)return!["0","off","false","no"].includes(t);if(Cs(e))return!1;try{return(await wt(e))["auto-update"]!=="off"}catch{return!0}}var vh=/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;async function Rs(e=process.env){let t=(e.FUI_UPDATE_REGISTRY||"https://registry.npmjs.org").replace(/\/+$/,""),n=await fetch(`${t}/${Vo.replace("/","%2F")}/latest`,{signal:AbortSignal.timeout(wh),headers:{accept:"application/json"}});if(!n.ok)throw Error(`registry answered HTTP ${n.status}`);let o=await n.json();if(typeof o.version!=="string")throw Error("registry answer has no version");if(!vh.test(o.version))throw Error("registry answer is not an x.y.z version");return o.version}function Ts(e,t=le){return Yi(e,t)===1}async function Sh(e,t){let n=`${Vo}@${t}`;if(e.manager==="npm")return Is("npm",["install","-g","--prefix",e.prefix,n]);return Is("bun",["add","-g",n],{...process.env,BUN_INSTALL:e.prefix})}async function Ps(e=process.env){let t=await fn(e),n=new Date().toISOString(),o;try{o=await Rs(e)}catch(i){throw await Xe({...t,checkedAt:n,error:i.message},e),i}if(!Ts(o))return await Xe({...t,checkedAt:n,latest:o,error:void 0},e),{latest:o,action:"current"};let s=Wo();if(!s)return await Xe({...t,checkedAt:n,latest:o,error:void 0},e),{latest:o,action:"manual",detail:`${qo("bun",o)} (or ${qo("npm",o)})`};let r=await Sh(s,o);if(!r.ok)return await Xe({...t,checkedAt:n,latest:o,error:r.output.slice(-500)},e),{latest:o,action:"failed",detail:r.output};return await Xe({checkedAt:n,latest:o,updatedFrom:le,updatedTo:o,noticeShown:!1,skillsVersion:t.skillsVersion,skillsAgents:t.skillsAgents},e),await $h(e),{latest:o,action:"installed"}}async function $h(e){let t=Bo();if(!t)return;await Is(process.execPath,[Uc(t,"dist","fui.js"),"skill","install"],{...e,FUI_AUTO_SKILLS:"off",FUI_AUTO_UPDATE:"off"},!1)}async function Vc(e=process.env){if(!Bo()||!await Es(e))return null;let t=await fn(e),n=null;if(t.updatedTo===le&&t.noticeShown===!1)n=`# fui updated ${t.updatedFrom} -> ${le}`,t.noticeShown=!0,await Xe(t,e);let o=t.checkedAt?Date.parse(t.checkedAt):0;if(Date.now()-o<gh)return n;return await Xe({...t,checkedAt:new Date().toISOString()},e),hh(process.execPath,[gt(),"update","--background"],{stdio:"ignore",env:{...e,FUI_AUTO_UPDATE:"off"},detached:!0,windowsHide:!0}).unref(),n}function qc(e){let t=e[0];if(!t)throw new m(`missing <key>; keys: ${ko.join(", ")}`);if(!ko.includes(t))throw new m(`unknown key "${t}"; keys: ${ko.join(", ")}`);return t}async function Ih(){let e=await Es(),t=process.env.FUI_AUTO_UPDATE?.trim(),n=!t&&Cs(),o=(await wt())["auto-update"],s=t||n?"env":o?"config":"default",r=t?"FUI_AUTO_UPDATE":n?"CI":o?st():"built-in (global bun or npm installs only)";return{key:"auto-update",value:e?"on":"off",source:s,from:r}}var Ch={name:"config",usage:"fui config",summary:"Show settings with their effective value and source",async run({workdir:e,render:t}){let{base:n,token:o}=await vo();P(`# config file: ${st()}`),b(z([{key:"workdir",value:e.path,source:e.source,from:e.detail},{key:"api-base",value:n.value??"",source:n.source,from:n.detail},{key:"api-token",value:o.value?"(set)":"",source:o.source,from:o.detail},await Ih()],t))}},Eh={name:"config set",usage:"fui config set workdir <path> | api-base <url> | api-token - | auto-update on|off",summary:"Save a setting to the fui config file (api-token only from stdin)",async run({positionals:e}){let t=qc(e),n=e[1];if(!n)throw new m(`missing <value> for ${t}`);let o;if(t==="workdir")o=Mn(n),await jh(o,{recursive:!0});else if(t==="auto-update"){if(n!=="on"&&n!=="off")throw new m("auto-update takes on or off");o=n}else if(t==="api-token")o=await Ee(n,void 0,"api-token");else o=n.trim().replace(/\/+$/,"");let s=await wt();s[t]=o;let r=await xr(s),i={workdir:"FUI_WORKDIR","api-base":"FUI_API_BASE","api-token":"FUI_API_TOKEN","auto-update":"FUI_AUTO_UPDATE"}[t];if(process.env[i])P(`# note: ${i} is set in this shell and overrides the config file`);b(`set ${t}${t==="api-token"?"":`=${o}`} in ${r}
|
|
77
77
|
`)}},Rh={name:"config unset",usage:"fui config unset <workdir|api-base|api-token|auto-update>",summary:"Remove a setting from the fui config file",async run({positionals:e}){let t=qc(e),n=await wt();delete n[t];let o=await xr(n);b(`unset ${t} in ${o}
|
|
78
78
|
`)}},Wc=[Ch,Eh,Rh];var Th={name:"db list",usage:"fui db list [-p project]",summary:"List connections across the workspace (no secrets)",async run({store:e,values:t,render:n}){let o=f(t,"project"),s=o?[Wn(o)]:await e.listProjects(),r=[];for(let i of s){let a;try{a=await e.load(i)}catch(c){P(`# ${i}: ${c.message}`);continue}for(let[c,l]of Object.entries(a.connections)){let u="invalid",d=l.database??"";try{if(l.dbToken){let p=an(l.dbToken);u=p.engine,d||=p.database}}catch{}r.push({db:`${i}/${c}`,engine:u,default:a.default===c?"*":"",apiDomain:l.apiDomain??"",database:d,userToken:l.userToken?"yes":"no",apiName:l.apiName??c})}}if(r.length===0){P("no connections; discover a managed DB: fui db alias list -p <project>, then fui db add-by-name <apiName> -p <project>; if you already have a dbToken, use fui db add");return}b(z(r,n))}},Ph={name:"db add",usage:`fui db add <apiName> -p <project> (--token - | --token-env VAR | --conn - | --conn-env VAR)
|
package/dist/fui.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{enableCompileCache as a}from"node:module";a?.();var{run:e}=await import("./fui-
|
|
2
|
+
import{enableCompileCache as a}from"node:module";a?.();var{run:e}=await import("./fui-8tyckpke.js");await e();
|
package/package.json
CHANGED
|
@@ -49,7 +49,7 @@ Load: `fui skill ref references/a.md references/b.md` (several at once OK).
|
|
|
49
49
|
|
|
50
50
|
- SP naming, params, URL, response, errors, wiring → `tapi-reference.md`
|
|
51
51
|
- 200 but empty/wrong data, or changing/removing params of an SP a module calls: lenient binding (missing → `NULL`, extra → ignored, never errors) → `tapi-reference.md` §2
|
|
52
|
-
-
|
|
52
|
+
- Remote address in SP: `@sys_RemoteIP varchar(15)` from `REMOTE_ADDR`; HTTP headers (`User-Agent`, `Cookie`, `X-Forwarded-For`, device/browser): `@sys_header_*`, each `-` → `_`, except `Authorization` → `tapi-reference.md` §2
|
|
53
53
|
- SP permission checks (5 patterns, `@sys_SystemRight`, `getSystemRight`) → `tapi-permission-patterns.md`
|
|
54
54
|
- File upload/download/view (`spAPIFILE_`, `fileContent`, `tblFileData`) → `tapi-file-api.md`
|
|
55
55
|
- SQL CLR: httpCall, sendMail, ImageResize, FileReader, RegexMatch… → `sql-clr-functions.md`
|
|
@@ -65,10 +65,11 @@ https://api.domain.vn/app/FunctionName/value1/value2
|
|
|
65
65
|
| `@sys_SystemRight` | int | Tiered right (1=low, 9=high) |
|
|
66
66
|
| `@sys_FunctionRight` | varchar(2000) | Function rights as `[AD][RPT][MGR]` |
|
|
67
67
|
| `@sys_SessionID` | varchar(50) | Auth session ID |
|
|
68
|
+
| `@sys_RemoteIP` | varchar(15) | `Request.ServerVariables("REMOTE_ADDR")`; the address seen by the server |
|
|
68
69
|
| `@sys_RawData` | nvarchar(max) | Full client JSON body |
|
|
69
|
-
| `@sys_header_{HeaderName}` | varchar(n) |
|
|
70
|
+
| `@sys_header_{HeaderName}` | varchar(n) | Request header except `Authorization`; missing header becomes `NULL`. Naming rule below |
|
|
70
71
|
|
|
71
|
-
> **CRITICAL — never put `@sys_
|
|
72
|
+
> **CRITICAL — never put recognized `@sys_*` params in module.json `IN`.** tAPI assigns them from authenticated user data, the HTTP request, or the JSON body; a same-named client field is ignored.
|
|
72
73
|
>
|
|
73
74
|
> ```json
|
|
74
75
|
> // ❌ SAI — không đưa sys_ vào IN
|
|
@@ -78,9 +79,21 @@ https://api.domain.vn/app/FunctionName/value1/value2
|
|
|
78
79
|
> "IN": { "ModuleID": "sModuleID" }
|
|
79
80
|
> ```
|
|
80
81
|
|
|
82
|
+
To read the remote address in an API procedure, declare the parameter with this exact name and type. tAPI reads `REMOTE_ADDR`; do not send it in `IN`. `varchar(15)` fits IPv4 addresses, so check the deployment's IPv6 and proxy behavior before using it for a wider address range:
|
|
83
|
+
|
|
84
|
+
```sql
|
|
85
|
+
CREATE PROCEDURE spAPI_AuditVisit
|
|
86
|
+
@sys_UserID varchar(9),
|
|
87
|
+
@sys_RemoteIP varchar(15)
|
|
88
|
+
AS
|
|
89
|
+
BEGIN
|
|
90
|
+
SELECT @sys_UserID AS UserID, @sys_RemoteIP AS RemoteIP;
|
|
91
|
+
END
|
|
92
|
+
```
|
|
93
|
+
|
|
81
94
|
#### `@sys_header_*` — read HTTP headers: replace each `-` with `_`
|
|
82
95
|
|
|
83
|
-
Works for **
|
|
96
|
+
Works for request headers **except `Authorization`**. The suffix after `@sys_header_` must be nonempty. tAPI lowercases the SQL parameter name, takes the suffix, replaces every `_` with `-`, then looks it up in `Request.Headers`. `-` is invalid in SQL Server param names, so the literal name won't compile:
|
|
84
97
|
|
|
85
98
|
```sql
|
|
86
99
|
-- ❌ SAI — lỗi cú pháp ngay lúc CREATE PROCEDURE, không phải lỗi runtime
|
|
@@ -88,7 +101,7 @@ Works for **any** header. `-` is invalid in SQL Server param names, so the liter
|
|
|
88
101
|
```
|
|
89
102
|
|
|
90
103
|
- **No quoting works**: `@[sys_header_User-Agent]` and `@"sys_header_User-Agent"` are both invalid. Renaming is the **only** way.
|
|
91
|
-
- **Each `-` → one `_
|
|
104
|
+
- **Each `-` → one `_` in the SP declaration.** tAPI reverses every `_` in the suffix to `-`; a header whose actual name contains `_` cannot be represented exactly by this convention.
|
|
92
105
|
|
|
93
106
|
| HTTP header | SP param |
|
|
94
107
|
| ------------------ | ---------------------------------------- |
|
|
@@ -99,26 +112,28 @@ Works for **any** header. `-` is invalid in SQL Server param names, so the liter
|
|
|
99
112
|
| `Cookie` | `@sys_header_Cookie` — no `-`, unchanged |
|
|
100
113
|
|
|
101
114
|
- Reverse (reading an existing SP): drop `@sys_header_`, turn remaining `_` into `-` — `@sys_header_Accept_Language` → `Accept-Language`.
|
|
102
|
-
- Matching is **case-insensitive** (prefix and
|
|
115
|
+
- Matching is **case-insensitive** (prefix and header lookup): `@sys_header_User_Agent`, `@sys_Header_User_Agent`, `@sys_header_USER_AGENT` request the same header. Use `_` for each `-`; tAPI does not insert separators, so `@sys_Header_RequestID` requests `RequestID`, not `Request-ID`.
|
|
116
|
+
- `@sys_header_Authorization` is **not exposed**: the code skips assignment for that header. It does not explicitly set the SQL parameter to `NULL` in this branch; do not rely on its value.
|
|
117
|
+
- A missing header is assigned `DBNull.Value`; exceptions during header lookup are caught without a reported error, so diagnose a missing value against the actual incoming request.
|
|
103
118
|
|
|
104
119
|
```sql
|
|
105
120
|
CREATE PROCEDURE spAPI_AuditLog_Insert
|
|
106
121
|
@Action nvarchar(100),
|
|
107
122
|
@sys_UserID varchar(9),
|
|
123
|
+
@sys_RemoteIP varchar(15),
|
|
108
124
|
@sys_header_User_Agent nvarchar(500),
|
|
109
125
|
@sys_header_X_Forwarded_For varchar(200)
|
|
110
126
|
AS
|
|
111
127
|
BEGIN
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
GETDATE());
|
|
128
|
+
SELECT @Action AS ActionName,
|
|
129
|
+
@sys_UserID AS UserID,
|
|
130
|
+
@sys_RemoteIP AS RemoteIP,
|
|
131
|
+
@sys_header_User_Agent AS UserAgent,
|
|
132
|
+
@sys_header_X_Forwarded_For AS ForwardedFor;
|
|
118
133
|
END
|
|
119
134
|
```
|
|
120
135
|
|
|
121
|
-
- **Still `@sys_*`:** auto-filled, **not** in `IN`, **no default** (`= null` is a hard error; `fui sp deploy` blocks it, see below).
|
|
136
|
+
- **Still recognized `@sys_*`:** auto-filled, **not** in `IN`, **no default** (`= null` is a hard error; `fui sp deploy` blocks it, see below).
|
|
122
137
|
- **Missing header → `NULL`** (not `''`) — normal, e.g. script/server-to-server calls often lack `User-Agent`. Wrap in `ISNULL(...)` at use: comparisons give UNKNOWN (`WHERE @sys_header_User_Agent <> 'bot'` **drops** the row), and `+` concatenation gives `NULL` (blanks the whole log string).
|
|
123
138
|
- **Untrusted (client-supplied, spoofable).** Log/stats only, never authorization; take rights from `@sys_UserID`/`@sys_SystemRight` (from the verified token).
|
|
124
139
|
|