@fui-org/fui-cli 2.1.0 → 2.1.2

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.
@@ -1,22 +1,22 @@
1
- import{parseArgs as gw}from"node:util";import{chmod as jd}from"node:fs/promises";import{homedir as Id}from"node:os";import{join as An,resolve as Cd}from"node:path";var Oe={Ok:0,Runtime:1,Usage:2,Guard:3};class m extends Error{name="UsageError"}class Se extends Error{severity;logKind;name="GuardError";constructor(e,t="hard",n="guard"){super(e);this.severity=t;this.logKind=n}}class Xe extends Error{report;name="ReportedFailure";constructor(e,t){super(e);this.report=t}}function Si(e){if(e instanceof m)return Oe.Usage;if(e instanceof Se)return Oe.Guard;return Oe.Runtime}import{createHash as fd}from"node:crypto";import{mkdir as gd,readdir as wd,readFile as yd,stat as ji,writeFile as bd}from"node:fs/promises";import{dirname as kd,join as vd}from"node:path";function ht(){return process.argv[1]??""}var Ii=new TextDecoder("utf-8");async function ft(e){return Ii.decode(await yd(e))}async function Yt(e){return JSON.parse(await ft(e))}async function ot(e){try{return(await ji(e)).isFile()}catch{return!1}}async function wr(e){try{return(await ji(e)).isDirectory()}catch{return!1}}async function F(e,t){await gd(kd(e),{recursive:!0}),await bd(e,t)}async function*yr(e,t){if(!await wr(t))return;yield*ho(t,"",e.split("/"))}function $d(e){let t="";for(let n of e)if(n==="*")t+=".*";else if(n==="?")t+=".";else t+=/[.+^${}()|[\]\\]/.test(n)?`\\${n}`:n;return new RegExp(`^${t}$`)}async function*ho(e,t,n){let[o,...s]=n;if(o===void 0)return;let r;try{r=await wd(vd(e,t),{withFileTypes:!0})}catch{return}if(o==="**"){yield*ho(e,t,s);for(let a of r)if(a.isDirectory())yield*ho(e,t?`${t}/${a.name}`:a.name,n);return}let i=$d(o);for(let a of r){if(!i.test(a.name))continue;let c=t?`${t}/${a.name}`:a.name;if(s.length===0){if(a.isFile())yield c}else if(a.isDirectory())yield*ho(e,c,s)}}function Qt(e,t){return fd(e).update(t).digest("hex")}async function Pn(){let e=[];for await(let t of process.stdin)e.push(typeof t==="string"?Buffer.from(t):t);return Ii.decode(Buffer.concat(e))}var Sd=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g");function br(e){let t=0;for(let n of e.replace(Sd,"")){if(/\p{Mn}|\p{Me}|\p{Cf}/u.test(n))continue;let o=n.codePointAt(0),s=o>=4352&&o<=4447||o>=11904&&o<=42191||o>=44032&&o<=55203||o>=63744&&o<=64255||o>=65072&&o<=65103||o>=65280&&o<=65376||o>=65504&&o<=65510||o>=127744&&o<=129791||o>=131072&&o<=262141;t+=s?2:1}return t}function Ci(e,t){let n=(r)=>{let[i="",a]=r.replace(/^v/,"").split("-",2);return{parts:i.split(".").map((c)=>Number.parseInt(c,10)||0),pre:a}},o=n(e),s=n(t);for(let r=0;r<3;r++){let i=(o.parts[r]??0)-(s.parts[r]??0);if(i!==0)return Math.sign(i)}if(o.pre===s.pre)return 0;if(o.pre===void 0)return 1;if(s.pre===void 0)return-1;return o.pre<s.pre?-1:1}var go=["workdir","api-base","api-token","auto-update"];function fo(e){return e.HOME||Id()}function Nn(e,t=process.env,n=process.cwd()){let o=e.trim();if(o==="~")return fo(t);if(o.startsWith("~/"))return An(fo(t),o.slice(2));return Cd(n,o)}function rt(e=process.env){let t=e.XDG_CONFIG_HOME||An(fo(e),".config");return An(t,"fui","config.json")}async function gt(e=process.env){let t=rt(e);if(!await ot(t))return{};try{let n=await Yt(t);if(n&&typeof n==="object"&&!Array.isArray(n))return n}catch{}throw new m(`${t} is not a JSON object; fix or delete it`)}async function kr(e,t=process.env){let n=rt(t);return await F(n,`${JSON.stringify(e,null,2)}
2
- `),await jd(n,384),n}var vr=An(".fui","workspaces");async function Ti(e,t=process.env,n=process.cwd()){if(e)return{path:Nn(e,t,n),source:"flag",detail:"--workdir"};if(t.FUI_WORKDIR)return{path:Nn(t.FUI_WORKDIR,t,n),source:"env",detail:"FUI_WORKDIR"};let o=await gt(t);if(typeof o.workdir==="string"&&o.workdir)return{path:Nn(o.workdir,t,n),source:"config",detail:rt(t)};return{path:An(fo(t),vr),source:"default",detail:"built-in"}}async function Ri(e,t,n){let o=n[e];if(o)return{value:o,source:"env",detail:e};let r=(await gt(n))[t];if(typeof r==="string"&&r)return{value:r,source:"config",detail:rt(n)};return{value:void 0,source:"missing",detail:`set ${e} or run: fui config set ${t} ...`}}async function wo(e=process.env){let[t,n]=await Promise.all([Ri("FUI_API_BASE","api-base",e),Ri("FUI_API_TOKEN","api-token",e)]);return{base:t,token:n}}function Ei(e){return e?.code==="EPIPE"}process.stdout.on("error",(e)=>{if(Ei(e))process.exit(0);throw e});function k(e){try{process.stdout.write(e)}catch(t){if(Ei(t))process.exit(0);throw t}}function T(e){process.stderr.write(e.endsWith(`
1
+ import{parseArgs as Sw}from"node:util";import{chmod as Ad}from"node:fs/promises";import{homedir as Nd}from"node:os";import{join as Nn,resolve as Dd}from"node:path";var Oe={Ok:0,Runtime:1,Usage:2,Guard:3};class h extends Error{name="UsageError"}class Se extends Error{severity;logKind;name="GuardError";constructor(e,t="hard",n="guard"){super(e);this.severity=t;this.logKind=n}}class Xe extends Error{report;name="ReportedFailure";constructor(e,t){super(e);this.report=t}}function Ti(e){if(e instanceof h)return Oe.Usage;if(e instanceof Se)return Oe.Guard;return Oe.Runtime}import{createHash as $d}from"node:crypto";import{mkdir as Sd,readdir as jd,readFile as Id,stat as Pi,writeFile as Cd}from"node:fs/promises";import{dirname as Rd,join as Td}from"node:path";function ht(){return process.argv[1]??""}var Ei=new TextDecoder("utf-8");async function ft(e){return Ei.decode(await Id(e))}async function Yt(e){return JSON.parse(await ft(e))}async function ot(e){try{return(await Pi(e)).isFile()}catch{return!1}}async function br(e){try{return(await Pi(e)).isDirectory()}catch{return!1}}async function L(e,t){await Sd(Rd(e),{recursive:!0}),await Cd(e,t)}async function*kr(e,t){if(!await br(t))return;yield*fo(t,"",e.split("/"))}function Pd(e){let t="";for(let n of e)if(n==="*")t+=".*";else if(n==="?")t+=".";else t+=/[.+^${}()|[\]\\]/.test(n)?`\\${n}`:n;return new RegExp(`^${t}$`)}async function*fo(e,t,n){let[o,...s]=n;if(o===void 0)return;let r;try{r=await jd(Td(e,t),{withFileTypes:!0})}catch{return}if(o==="**"){yield*fo(e,t,s);for(let a of r)if(a.isDirectory())yield*fo(e,t?`${t}/${a.name}`:a.name,n);return}let i=Pd(o);for(let a of r){if(!i.test(a.name))continue;let c=t?`${t}/${a.name}`:a.name;if(s.length===0){if(a.isFile())yield c}else if(a.isDirectory())yield*fo(e,c,s)}}function Qt(e,t){return $d(e).update(t).digest("hex")}async function En(){let e=[];for await(let t of process.stdin)e.push(typeof t==="string"?Buffer.from(t):t);return Ei.decode(Buffer.concat(e))}var Ed=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g");function vr(e){let t=0;for(let n of e.replace(Ed,"")){if(/\p{Mn}|\p{Me}|\p{Cf}/u.test(n))continue;let o=n.codePointAt(0),s=o>=4352&&o<=4447||o>=11904&&o<=42191||o>=44032&&o<=55203||o>=63744&&o<=64255||o>=65072&&o<=65103||o>=65280&&o<=65376||o>=65504&&o<=65510||o>=127744&&o<=129791||o>=131072&&o<=262141;t+=s?2:1}return t}function Ai(e,t){let n=(r)=>{let[i="",a]=r.replace(/^v/,"").split("-",2);return{parts:i.split(".").map((c)=>Number.parseInt(c,10)||0),pre:a}},o=n(e),s=n(t);for(let r=0;r<3;r++){let i=(o.parts[r]??0)-(s.parts[r]??0);if(i!==0)return Math.sign(i)}if(o.pre===s.pre)return 0;if(o.pre===void 0)return 1;if(s.pre===void 0)return-1;return o.pre<s.pre?-1:1}var wo=["workdir","api-base","api-token","auto-update"];function go(e){return e.HOME||Nd()}function An(e,t=process.env,n=process.cwd()){let o=e.trim();if(o==="~")return go(t);if(o.startsWith("~/"))return Nn(go(t),o.slice(2));return Dd(n,o)}function rt(e=process.env){let t=e.XDG_CONFIG_HOME||Nn(go(e),".config");return Nn(t,"fui","config.json")}async function gt(e=process.env){let t=rt(e);if(!await ot(t))return{};try{let n=await Yt(t);if(n&&typeof n==="object"&&!Array.isArray(n))return n}catch{}throw new h(`${t} is not a JSON object; fix or delete it`)}async function $r(e,t=process.env){let n=rt(t);return await L(n,`${JSON.stringify(e,null,2)}
2
+ `),await Ad(n,384),n}var Sr=Nn(".fui","workspaces");async function Di(e,t=process.env,n=process.cwd()){if(e)return{path:An(e,t,n),source:"flag",detail:"--workdir"};if(t.FUI_WORKDIR)return{path:An(t.FUI_WORKDIR,t,n),source:"env",detail:"FUI_WORKDIR"};let o=await gt(t);if(typeof o.workdir==="string"&&o.workdir)return{path:An(o.workdir,t,n),source:"config",detail:rt(t)};return{path:Nn(go(t),Sr),source:"default",detail:"built-in"}}async function Ni(e,t,n){let o=n[e];if(o)return{value:o,source:"env",detail:e};let r=(await gt(n))[t];if(typeof r==="string"&&r)return{value:r,source:"config",detail:rt(n)};return{value:void 0,source:"missing",detail:`set ${e} or run: fui config set ${t} ...`}}async function yo(e=process.env){let[t,n]=await Promise.all([Ni("FUI_API_BASE","api-base",e),Ni("FUI_API_TOKEN","api-token",e)]);return{base:t,token:n}}function xi(e){return e?.code==="EPIPE"}process.stdout.on("error",(e)=>{if(xi(e))process.exit(0);throw e});function b(e){try{process.stdout.write(e)}catch(t){if(xi(t))process.exit(0);throw t}}function T(e){process.stderr.write(e.endsWith(`
3
3
  `)?e:`${e}
4
- `)}async function yo(){return Pn()}async function Dn(e,t){if(t&&e)throw new m("pass SQL as an argument or with -f, not both");let n;if(t){if(!await ot(t))throw new m(`file not found: ${t}`);n=await ft(t)}else if(e==="-")n=await yo();else if(e!==void 0)n=e;else throw new m('missing SQL: pass "<sql>", -f <file>, or - for stdin');if(!n.trim())throw new m("SQL is empty");return n}async function Te(e,t,n){if(e!==void 0&&t!==void 0)throw new m(`pass ${n} or ${n}-env, not both`);if(t!==void 0){let s=process.env[t];if(!s)throw new m(`env var ${t} is empty or unset`);return s.trim()}if(e===void 0)return;if(e!=="-")throw new m(`${n} takes "-" (stdin) only; use ${n}-env VAR for env vars`);let o=(await yo()).trim();if(!o)throw new m(`${n}: stdin is empty`);return o}var Pi=60000,Ni=400;function Ai(e){return e.length<=Ni?e:`${e.slice(0,Ni)}…`}async function Rd(e){let t;try{t=(await e.text()).trim()}catch{return""}if(!t)return"";try{let n=JSON.parse(t);for(let o of["Message","message","error","ExceptionMessage"]){let s=n?.[o];if(typeof s==="string"&&s.trim())return` — ${Ai(s.trim())}`}}catch{}return` — ${Ai(t)}`}function st(e){return e.data||[]}class Di{base;token;constructor(e,t){this.base=e;this.token=t}async request(e,t){let n;try{n=await fetch(`${this.base}${e}`,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json; charset=UTF-8",Authorization:`Bearer ${this.token}`},body:t!==void 0?JSON.stringify(t):"{}",signal:AbortSignal.timeout(Pi)})}catch(o){let s=o?.name;if(s==="TimeoutError"||s==="AbortError")throw Error(`FUI API timeout: ${e} did not answer within ${Pi/1000}s`);throw Error(`FUI API network error: ${e} — ${o?.message??String(o)}`)}if(!n.ok)throw Error(`FUI API error: ${n.status} ${n.statusText}${await Rd(n)}`);return await n.json()}async listProjects(){return st(await this.request("/fp/FUI_Dev_ProjectList"))}editProject(e){return this.request("/fp/FUI_Dev_ProjectEdit",{jsonObj:JSON.stringify(e)})}getProjectJson(e){return this.request(`/fp/obj/FUI_Dev_ProjectJson/${e}`,{})}async listProjectUserRights(e){return st(await this.request("/fp/FUI_Dev_UserRightList",{PID:e,MID:""}))}async searchUsers(e,t){return st(await this.request("/fp/FUI_Dev_UserSearch",{textsearch:e,useridlike:t}))}updateProjectUserRight(e,t,n){return this.request("/fp/FUI_Dev_UserRight_Update",{PID:e,MID:"",UserRight:t,UserList:n})}async getRuntimeModuleVersion(e,t,n){return st(await this.request("/fp/FUI_Runtime_ModuleVersionSelect",{domain:e,url1_ID:t,url2_ID:n}))}async listModules(e){return st(await this.request("/fp/FUI_Dev_modulelist",{pid:e}))}editModule(e,t){return this.request("/fp/FUI_Dev_ModuleEdit",{ProjectID:e,ModuleObj:JSON.stringify(t)})}deleteModule(e,t){return this.request("/fp/FUI_Dev_moduleDelete",{ProjectID:e,ModuleID:t})}async getModuleContent(e,t){let o=(await this.request(`/fp/obj/FUI_Dev_moduledata/module-load/${e}/${t}`,{})).data;if(!o.mHeader&&!o.mBody)T(`# warning: module-load ${e}/${t} returned empty mHeader and mBody`);return o}getModuleUI(e,t){return this.request(`/fp/obj/FUI_Dev_moduledata/module-ui/${e}/${t}`,{})}getModuleHtml(e,t){return this.request(`/fp/obj/FUI_Dev_moduledata/module-html/${e}/${t}`,{})}async getModuleScript(e,t){return(await this.request(`/fp/obj/FUI_Dev_moduledata/module-code/${e}/${t}`,{})).data?.FileContent||""}async getModuleCSS(e,t){return(await this.request(`/fp/obj/FUI_Dev_moduledata/module-css/${e}/${t}`,{})).data?.FileContent||""}updateModule(e,t,n){return this.request(`/fp/obj/FUI_Dev_editorupdate/module-save/${e}/${t}`,n)}updateModuleUI(e,t,n){return this.request(`/fp/obj/FUI_Dev_editorupdate/module-ui/${e}/${t}`,{FileContent:n})}updateModuleScript(e,t,n){return this.request(`/fp/obj/FUI_Dev_editorupdate/module-code/${e}/${t}`,{FileContent:n})}updateModuleCSS(e,t,n){return this.request(`/fp/obj/FUI_Dev_editorupdate/module-css/${e}/${t}`,{FileContent:n})}updateProjectData(e,t){return this.request(`/fp/obj/FUI_Dev_editorupdate/project/${e}/-`,{FileContent:JSON.stringify(t)})}async getModuleComponents(e,t){return st(await this.request("/fp/FUI_Dev_ComponentSelect",{ProjectID:e,...t&&{ModuleID:t}}))}updateComponent(e){return this.request("/fp/FUI_Dev_ComponentUpdate",e)}async getModuleDependencies(e,t){return st(await this.request("/fp/FUI_Dev_File/select",{ProjectID:e,ModuleID:t}))}async listFrameworkImportFiles(e){return st(await this.request("/fp/FUI_Dev_File/select",{frameWork:e}))}async listImportFiles(e,t){return st(await this.request("/fp/FUI_Dev_File/select",{...e&&{ProjectID:e},...t&&{ModuleID:t}}))}getFileContent(e){return this.request(`/fp/obj/FUI_Dev_File/getcontent/${e}`,{})}uploadFileContent(e,t){return this.request(`/fp/FUI_Dev_File/uploadcontent/${e}`,{FileContent:t})}insertImportFile(e){return this.request("/fp/FUI_Dev_File/insert",e)}updateImportFile(e,t){return this.request(`/fp/FUI_Dev_File/update/${e}`,t)}deleteImportFile(e){return this.request(`/fp/FUI_Dev_File/delete/${e}`,{})}}async function bo(){let{base:e,token:t}=await wo();if(!e.value||!t.value){let n=[!e.value&&"FUI_API_BASE",!t.value&&"FUI_API_TOKEN"].filter(Boolean).join(", ");throw new m(`missing ${n}; set the env var, or run fui config set api-base <url> / fui config set api-token -`)}return new Di(e.value,t.value)}import{mkdir as Pd,rename as Nd}from"node:fs/promises";import{homedir as Ad}from"node:os";import{join as $r}from"node:path";import{readFile as Td}from"node:fs/promises";var Ed=new TextDecoder("utf-8",{ignoreBOM:!0});async function G(e){return Ed.decode(await Td(e))}async function he(e){try{return await G(e)}catch{return null}}function xn(e){return e?.code}var Dd=/^[A-Za-z0-9_-]+$/;function xd(e=process.env){return e.FUI_SESSION_DIR||$r(e.HOME||Ad(),".fui","session")}function Sr(e=process.env){let t=e.FUI_SESSION||"default";if(!Dd.test(t))throw new m(`invalid FUI_SESSION "${t}": letters, digits, "_" or "-"`);return $r(xd(e),`cli-${t}.json`)}class On{file;constructor(e=Sr()){this.file=e}async read(){let e=await he(this.file);if(!e)return{};try{let t=JSON.parse(e);return t&&typeof t==="object"&&!Array.isArray(t)?t:{}}catch{return{}}}async write(e){await Pd($r(this.file,".."),{recursive:!0});let t=`${this.file}.${process.pid}.tmp`;await F(t,JSON.stringify(e,null,2)),await Nd(t,this.file)}async update(e){let t=await this.read();return e(t),await this.write(t),t}async getTarget(){return(await this.read()).target??null}async setTarget(e){let t={projectId:e.projectId,moduleId:e.moduleId,moduleDir:e.moduleDir,workspaceRoot:e.workspaceRoot,...e.target!==void 0?{target:e.target}:{}};await this.update((n)=>{n.target=t})}async clearTarget(){await this.update((e)=>{delete e.target})}async createApproval(e){let t={...e,token:crypto.randomUUID(),createdAt:new Date().toISOString()};return await this.update((n)=>{n.approval=t}),t}async getApproval(){return(await this.read()).approval??null}async clearApproval(){await this.update((e)=>{delete e.approval})}async markSkillLoaded(){await this.update((e)=>{e.skillLoadedAt=new Date().toISOString()})}async wasSkillLoaded(){return Boolean((await this.read()).skillLoadedAt)}async markSpVerified(e){await this.update((t)=>{let n=new Set(t.spVerified??[]);for(let o of e)n.add(o.toLowerCase());t.spVerified=[...n]})}async getVerifiedSpNames(){return new Set((await this.read()).spVerified??[])}}function Zt(e=process.env){return["1","true","yes"].includes((e.FUI_STRICT||"").trim().toLowerCase())}import{readdir as Od}from"node:fs/promises";import{isAbsolute as _d,join as Ir,relative as Md,sep as Fd}from"node:path";var Ld=/^[A-Za-z0-9._ -]+$/,jr=20;function it(e,t){if(!e||!Ld.test(e)||/^\.+$/.test(e))throw new m(`invalid ${t} ${JSON.stringify(e)}: letters, digits, ".", "_", "-" and spaces only (used as a workspace folder name)`);return e}function Ud(e,t){let n=Md(e,t);if(!n||n.startsWith("..")||_d(n))return null;let[o,s,r]=n.split(Fd);if(!o||o.startsWith("_")||o.startsWith("."))return null;if(s==="modules"&&r)return{projectId:o,moduleId:r,moduleDir:Ir(e,o,"modules",r)};return{projectId:o}}async function xi(e,t){try{return(await Od(e,{withFileTypes:!0})).filter((o)=>o.isDirectory()&&!(t&&(o.name.startsWith("_")||o.name.startsWith(".")))).map((o)=>o.name).sort()}catch{return[]}}function Oi(e){return e.length>jr?[...e.slice(0,jr),` ... and ${e.length-jr} more`]:e}class Cr{paths;session;cwd;constructor(e,t,n=process.cwd()){this.paths=e;this.session=t;this.cwd=n}cwdTarget(){return Ud(this.paths.root,this.cwd)}async contextProjectId(){return this.cwdTarget()?.projectId??(await this.session.getTarget())?.projectId??void 0}async resolveProject(e){if(e)return{projectId:it(e,"projectId"),source:"explicit"};let t=this.cwdTarget();if(t)return{projectId:t.projectId,source:"cwd"};let n=await this.session.getTarget();if(n?.projectId)return{projectId:n.projectId,source:"session"};throw new m(await this.noTargetMenu("project"))}async resolveProjectModule(e,t){if(e&&t)return it(e,"projectId"),it(t,"moduleId"),{projectId:e,moduleId:t,moduleDir:Ir(this.paths.root,e,"modules",t),source:"explicit"};if(e||t)throw new m("module commands need both -p <project> and -m <module>");let n=this.cwdTarget();if(n?.moduleId&&n.moduleDir)return{projectId:n.projectId,moduleId:n.moduleId,moduleDir:n.moduleDir,source:"cwd"};let o=await this.session.getTarget();if(o?.projectId&&o.moduleId&&o.moduleDir)return{projectId:o.projectId,moduleId:o.moduleId,moduleDir:o.moduleDir,source:"session"};throw new m(await this.noTargetMenu("module"))}async resolveModuleDir(e,t,n){if(e)return e;return(await this.resolveProjectModule(t,n)).moduleDir}async noTargetMenu(e){let t=await xi(this.paths.root,!0),n=e==="project"?"no project: pass -p <project>, cd into a workspace project, or run: fui use <project>/<module>":"no module: pass -p <project> -m <module>, cd into a module folder, or run: fui use <project>/<module>";if(t.length===0)return`${n}
5
- workspace ${this.paths.root} has no projects; run: fui project list`;if(e==="module"&&t.length===1){let o=await xi(Ir(this.paths.root,t[0],"modules"),!1);if(o.length>0)return[n,...Oi(o.map((s)=>` fui use ${t[0]}/${s}`))].join(`
6
- `)}return[n,...Oi(t.map((o)=>` -p ${o}`))].join(`
7
- `)}}function Le(e){return e.trim().replace(/^https?:\/\//i,"").replace(/\/.*$/,"").toLowerCase()}function Ee(e){let t=Le(e);return`${/^(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/.test(t)?"http":"https"}://${t}`}function Hd(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 _i(e,t,n,o){let s;try{s=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 r=await s.text();if(!s.ok)throw Error(`tAPI HTTP ${s.status}: ${Hd(r)||s.statusText}`);try{return JSON.parse(r)}catch{throw Error(`tAPI returned non-JSON: ${r.slice(0,400)}`)}}class at{origin;dbToken;engine="mssql";constructor(e,t){this.origin=e;this.dbToken=t}async query(e){return(await _i(this.origin,"/help/_sql_execute",this.dbToken,e)).data??[]}exec(e){return _i(this.origin,"/help/_sql_execute_NonQuery",this.dbToken,e)}async close(){}}async function Rr(e,t,n,o,s){let r=`${e}/help/${t}/${n?"auth/":""}${o}`,i={};if(!n&&s)i.authorization=`Bearer ${s}`;let a=await fetch(r,{headers:i,signal:AbortSignal.timeout(15000)});return{url:r,status:a.status,ok:a.ok,text:await a.text()}}function Tr(e){return Buffer.from(e,"utf8").toString("base64")}function _n(e){try{let t=e.replace(/\s+/g,"");if(!/^[A-Za-z0-9+/]*={0,2}$/.test(t)||t.length%4===1)throw Error("not base64");return new TextDecoder("utf-8",{fatal:!0}).decode(Buffer.from(t,"base64"))}catch{throw new m("token is not valid Base64 UTF-8")}}function en(e,t,n,o){return Tr(`Server=${e}; Database=${t}; UID=${n}; PWD=${o}; Encrypt=True; TrustServerCertificate=True;`)}function Mi(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 Vd(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=Mi(t);if(n.has("server")||n.has("data source"))return"mssql";throw new m("unsupported connection: expected postgres://, mysql://, mariadb://, sqlite:/file: URL or MSSQL Server=...; string")}function Mn(e){let t=_n(e);return{engine:Vd(t),plain:t}}function tn(e){let{engine:t,plain:n}=Mn(e);if(t==="mssql"){let s=Mi(n);return{engine:t,server:s.get("server")??s.get("data source")??"",database:s.get("database")??s.get("initial catalog")??"",user:s.get("uid")??s.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 Wd,readdir as qd,writeFile as Bd}from"node:fs/promises";import{basename as Fi,dirname as Ui,extname as Hi,isAbsolute as Jd,join as Er,relative as Vi,sep as Kd}from"node:path";var Li=50;function ko(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 Gd(e,t,n={}){let o=Vi(e.root,t),r=o!==""&&!o.startsWith("..")&&!Jd(o)?o:Er("_external",Fi(t)),i=Hi(r),a=Fi(r,i),c=n.stamp??ko(),l=n.suffix?`_${n.suffix}`:"";return Er(e.historyRoot(),Ui(r),`${a}_${c}${l}${i}`)}async function zd(e,t){let n=Hi(e),o=e.slice(0,e.length-n.length);for(let r=1;r<=Li;r++){let i=r===1?e:`${o}_${r}${n}`;try{return await Bd(i,t,{encoding:"utf-8",flag:"wx"}),i}catch(a){if(xn(a)!=="EEXIST")throw a}}let s=`${o}_${Li}${n}`;return await F(s,t),s}async function fe(e,t,n={}){let o=n.content;if(o===void 0)try{o=await G(t)}catch(r){let i=xn(r);if(i==="ENOENT"||i==="ENOTDIR")return null;throw r}let s=Gd(e,t,n);return await Wd(Ui(s),{recursive:!0}),zd(s,o)}function Nt(e,t){let n=Vi(e.root,t);return(n.startsWith("..")?t:n).split(Kd).join("/")}async function vo(e,t,n={}){let o=n.stamp??ko(),s=new Set(n.skipDirNames??["_preview"]),r=[];async function i(a){let c;try{c=await qd(a,{withFileTypes:!0})}catch{return}for(let l of c){let u=Er(a,l.name);if(l.isDirectory()){if(!s.has(l.name))await i(u)}else if(l.isFile()){let d=await fe(e,u,{suffix:n.suffix,stamp:o});if(d)r.push(d)}}}return await i(t),r}var At=2,Xd=/^[A-Za-z0-9_-]+$/,Yd=/^[A-Za-z0-9._-]+$/,Bi=/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i,Qd=/^[A-Za-z0-9_.-]+$/;function yt(e){if(!Xd.test(e))throw new m(`invalid alias "${e}": use letters, digits, "_" or "-"`);return e}function Wi(e){if(!Yd.test(e)||e==="."||e===".."||/[. ]$/.test(e)||Bi.test(e))throw new m(`invalid connection name "${e}": use letters, digits, ".", "_" or "-"`);return e}function Fn(e){let t=e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"_").replace(/^[._-]+|[._-]+$/g,"").slice(0,64)||"db";return Bi.test(t)?`db_${t}`:t}function qi(e,t,n,o){let s=new Set(Object.keys(e.connections).map((d)=>d.toLowerCase())),r=Fn(t);if(!s.has(r))return r;let i=(n?Le(n):"").split(".").filter(Boolean),a=Fn(["api","tapi"].includes(i[0]??"")?i[1]??i[0]??"zone":i[0]??"zone"),c=`${r}_${a}`;if(!s.has(c))return c;let l=`${c}_${Fn(o)}`;if(!s.has(l))return l;let u=2;while(s.has(`${l}_${u}`))u++;return`${l}_${u}`}function Dt(e){let{sqlServer:t,database:n}=e;if((!t||!n)&&e.dbToken)try{let o=tn(e.dbToken);t||=o.server,n||=o.database}catch{}return t&&n?{sqlServer:t,database:n}:null}function Pr(e,t,n){let o=(s)=>s.trim().toLowerCase();return Object.entries(e.connections).find(([,s])=>{let r=Dt(s);return r&&o(r.sqlServer)===o(t)&&o(r.database)===o(n)})?.[0]??null}function So(e){if(!Qd.test(e)||e.startsWith("_")||e.startsWith("."))throw new m(`invalid project id "${e}"`);return e}function $o(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 m(`invalid --db "${e}": use <connectionName> or <project>/<connectionName>`)}function wt(e,t){let n=t.toLowerCase(),o=Object.keys(e.connections).find((r)=>r.toLowerCase()===n);if(o)return o;let s=Object.entries(e.connections).filter(([,r])=>(r.apiName??"").toLowerCase()===n).map(([r])=>r);return s.length===1?s[0]:null}function Ji(e,t,n){let o=t.toLowerCase(),s=Object.keys(e.connections).find((i)=>i.toLowerCase()===o);if(s)return s;let r=Object.entries(e.connections).filter(([,i])=>(i.apiName??"").toLowerCase()===o);if(r.length===1)return r[0][0];if(r.length>1&&n){let i=r.filter(([,a])=>a.apiDomain&&Le(a.apiDomain)===n);if(i.length===1)return i[0][0]}return null}class ct{paths;constructor(e){this.paths=e}async readRaw(e){let t=this.paths.connectionsFile(e);if(!await ot(t))return null;let n=await G(t);if(!n.trim())return null;let o;try{o=JSON.parse(n)}catch(s){throw Error(`${t} is not valid JSON (${s.message}); fix it by hand before writing`)}if(!o||typeof o!=="object"||Array.isArray(o))throw Error(`${t} must be an object { version, default, connections }`);return o}connectionsOf(e,t){let n=t.connections??{};if(!n||typeof n!=="object"||Array.isArray(n))throw Error(`${e}: "connections" must be an object of connectionName -> config`);for(let[o,s]of Object.entries(n)){if(!s||typeof s!=="object"||Array.isArray(s))throw Error(`${e}: connection "${o}" must be an object`);Wi(o)}return n}async load(e){let t=await this.readRaw(e);if(!t)return{connections:{},version:At};let n=this.paths.connectionsFile(e);if(t.version!==At)throw new m(t.version===void 0?`${n} is the old v1 format; convert it once: fui db migrate -p ${e}`:`${n}: unsupported version ${String(t.version)}; fui reads version ${At}`);let o={};for(let[s,r]of Object.entries(this.connectionsOf(n,t))){if(typeof r.apiName!=="string"||!r.apiName)throw Error(`${n}: connection "${s}" has no apiName`);o[s]={...r,apiName:yt(r.apiName)}}return{...t,version:At,connections:o}}async migrate(e){let t=await this.readRaw(e);if(!t)return"none";if(t.version===At)return"current";let n=this.paths.connectionsFile(e);if(t.version!==void 0)throw new m(`${n}: unsupported version ${String(t.version)}; fui reads version ${At}`);let o={};for(let[a,c]of Object.entries(this.connectionsOf(n,t))){let l=c;o[a]={...l,apiName:yt(l.apiName??a),...Dt(l)}}let{version:s,connections:r,...i}=t;return await this.write(e,{version:At,...i,connections:o}),"migrated"}async write(e,t){let n=this.paths.connectionsFile(e);await fe(this.paths,n),await F(n,JSON.stringify(t,null,2))}async save(e,t){let n=await this.load(e),o=yt(t.apiName),s=Dt(t.config),r=t.connectionName?Wi(Fn(t.connectionName)):null,i=(c)=>Object.keys(n.connections).find((l)=>l.toLowerCase()===c.toLowerCase()),a;if(s){let c=Pr(n,s.sqlServer,s.database);if(c&&r&&r.toLowerCase()!==c.toLowerCase())throw new m(`${s.sqlServer}/${s.database} is already saved as connection "${c}"; folders are not renamed, use --db ${e}/${c}`);if(a=c??r??qi(n,o,t.config.apiDomain,s.database),!c&&i(a))throw new m(`connection name "${i(a)}" is taken by another database; pass --name, e.g. --name ${qi(n,o,t.config.apiDomain,s.database)}`)}else{let c=r??Fn(o);a=i(c)??c}if(n.connections[a]={...t.config,apiName:o,...s},t.makeDefault||!n.default||!wt(n,n.default))n.default=a;return await this.write(e,n),a}async update(e,t,n){let o=await this.load(e),s=o.connections[t];if(!s)throw new m(this.menu(e,o,`no connection "${t}" in project "${e}"`));let{apiName:r,...i}=n;o.connections[t]={...i,apiName:s.apiName,...Dt(n)},await this.write(e,o)}async setDefault(e,t){let n=await this.load(e),o=wt(n,t);if(!o)throw new m(this.menu(e,n,`no connection "${t}" in project "${e}"`));return n.default=o,await this.write(e,n),o}async listProjects(){let e=[];try{for await(let t of yr("*/_db/_connections.json",this.paths.root)){let n=t.split(/[\\/]/)[0];if(!n.startsWith("_"))e.push(n)}}catch(t){if(t.code!=="ENOENT")throw t}return e.sort()}async resolveProject(e){let t=e.db?$o(e.db):void 0,n=t?.project??e.project??process.env.FUI_PROJECT??e.contextProject;if(n)return So(n);let o=await this.listProjects();if(t){let r=[];for(let i of o)if(wt(await this.load(i),t.alias))r.push(i);if(r.length===1)return r[0];if(r.length===0)throw new m(`no project has connection "${t.alias}"; run: fui db list`);throw new m(`connection "${t.alias}" exists in several projects; use one of:
4
+ `)}async function bo(){return En()}async function Dn(e,t){if(t&&e)throw new h("pass SQL as an argument or with -f, not both");let n;if(t){if(!await ot(t))throw new h(`file not found: ${t}`);n=await ft(t)}else if(e==="-")n=await bo();else if(e!==void 0)n=e;else throw new h('missing SQL: pass "<sql>", -f <file>, or - for stdin');if(!n.trim())throw new h("SQL is empty");return n}async function Te(e,t,n){if(e!==void 0&&t!==void 0)throw new h(`pass ${n} or ${n}-env, not both`);if(t!==void 0){let s=process.env[t];if(!s)throw new h(`env var ${t} is empty or unset`);return s.trim()}if(e===void 0)return;if(e!=="-")throw new h(`${n} takes "-" (stdin) only; use ${n}-env VAR for env vars`);let o=(await bo()).trim();if(!o)throw new h(`${n}: stdin is empty`);return o}var Oi=60000,_i=400;function Mi(e){return e.length<=_i?e:`${e.slice(0,_i)}…`}async function xd(e){let t;try{t=(await e.text()).trim()}catch{return""}if(!t)return"";try{let n=JSON.parse(t);for(let o of["Message","message","error","ExceptionMessage"]){let s=n?.[o];if(typeof s==="string"&&s.trim())return` — ${Mi(s.trim())}`}}catch{}return` — ${Mi(t)}`}function st(e){return e.data||[]}class Li{base;token;constructor(e,t){this.base=e;this.token=t}async request(e,t){let n;try{n=await fetch(`${this.base}${e}`,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json; charset=UTF-8",Authorization:`Bearer ${this.token}`},body:t!==void 0?JSON.stringify(t):"{}",signal:AbortSignal.timeout(Oi)})}catch(o){let s=o?.name;if(s==="TimeoutError"||s==="AbortError")throw Error(`FUI API timeout: ${e} did not answer within ${Oi/1000}s`);throw Error(`FUI API network error: ${e} — ${o?.message??String(o)}`)}if(!n.ok)throw Error(`FUI API error: ${n.status} ${n.statusText}${await xd(n)}`);return await n.json()}async listProjects(){return st(await this.request("/fp/FUI_Dev_ProjectList"))}editProject(e){return this.request("/fp/FUI_Dev_ProjectEdit",{jsonObj:JSON.stringify(e)})}getProjectJson(e){return this.request(`/fp/obj/FUI_Dev_ProjectJson/${e}`,{})}async listProjectUserRights(e){return st(await this.request("/fp/FUI_Dev_UserRightList",{PID:e,MID:""}))}async searchUsers(e,t){return st(await this.request("/fp/FUI_Dev_UserSearch",{textsearch:e,useridlike:t}))}updateProjectUserRight(e,t,n){return this.request("/fp/FUI_Dev_UserRight_Update",{PID:e,MID:"",UserRight:t,UserList:n})}async getRuntimeModuleVersion(e,t,n){return st(await this.request("/fp/FUI_Runtime_ModuleVersionSelect",{domain:e,url1_ID:t,url2_ID:n}))}async listModules(e){return st(await this.request("/fp/FUI_Dev_modulelist",{pid:e}))}editModule(e,t){return this.request("/fp/FUI_Dev_ModuleEdit",{ProjectID:e,ModuleObj:JSON.stringify(t)})}deleteModule(e,t){return this.request("/fp/FUI_Dev_moduleDelete",{ProjectID:e,ModuleID:t})}async getModuleContent(e,t){let o=(await this.request(`/fp/obj/FUI_Dev_moduledata/module-load/${e}/${t}`,{})).data;if(!o.mHeader&&!o.mBody)T(`# warning: module-load ${e}/${t} returned empty mHeader and mBody`);return o}getModuleUI(e,t){return this.request(`/fp/obj/FUI_Dev_moduledata/module-ui/${e}/${t}`,{})}getModuleHtml(e,t){return this.request(`/fp/obj/FUI_Dev_moduledata/module-html/${e}/${t}`,{})}async getModuleScript(e,t){return(await this.request(`/fp/obj/FUI_Dev_moduledata/module-code/${e}/${t}`,{})).data?.FileContent||""}async getModuleCSS(e,t){return(await this.request(`/fp/obj/FUI_Dev_moduledata/module-css/${e}/${t}`,{})).data?.FileContent||""}updateModule(e,t,n){return this.request(`/fp/obj/FUI_Dev_editorupdate/module-save/${e}/${t}`,n)}updateModuleUI(e,t,n){return this.request(`/fp/obj/FUI_Dev_editorupdate/module-ui/${e}/${t}`,{FileContent:n})}updateModuleScript(e,t,n){return this.request(`/fp/obj/FUI_Dev_editorupdate/module-code/${e}/${t}`,{FileContent:n})}updateModuleCSS(e,t,n){return this.request(`/fp/obj/FUI_Dev_editorupdate/module-css/${e}/${t}`,{FileContent:n})}updateProjectData(e,t){return this.request(`/fp/obj/FUI_Dev_editorupdate/project/${e}/-`,{FileContent:JSON.stringify(t)})}async getModuleComponents(e,t){return st(await this.request("/fp/FUI_Dev_ComponentSelect",{ProjectID:e,...t&&{ModuleID:t}}))}updateComponent(e){return this.request("/fp/FUI_Dev_ComponentUpdate",e)}async getModuleDependencies(e,t){return st(await this.request("/fp/FUI_Dev_File/select",{ProjectID:e,ModuleID:t}))}async listFrameworkImportFiles(e){return st(await this.request("/fp/FUI_Dev_File/select",{frameWork:e}))}async listImportFiles(e,t){return st(await this.request("/fp/FUI_Dev_File/select",{...e&&{ProjectID:e},...t&&{ModuleID:t}}))}getFileContent(e){return this.request(`/fp/obj/FUI_Dev_File/getcontent/${e}`,{})}uploadFileContent(e,t){return this.request(`/fp/FUI_Dev_File/uploadcontent/${e}`,{FileContent:t})}insertImportFile(e){return this.request("/fp/FUI_Dev_File/insert",e)}updateImportFile(e,t){return this.request(`/fp/FUI_Dev_File/update/${e}`,t)}deleteImportFile(e){return this.request(`/fp/FUI_Dev_File/delete/${e}`,{})}}async function ko(){let{base:e,token:t}=await yo();if(!e.value||!t.value){let n=[!e.value&&"FUI_API_BASE",!t.value&&"FUI_API_TOKEN"].filter(Boolean).join(", ");throw new h(`missing ${n}; set the env var, or run fui config set api-base <url> / fui config set api-token -`)}return new Li(e.value,t.value)}import{mkdir as Md,rename as Ld}from"node:fs/promises";import{homedir as Fd}from"node:os";import{join as jr}from"node:path";import{readFile as Od}from"node:fs/promises";var _d=new TextDecoder("utf-8",{ignoreBOM:!0});async function G(e){return _d.decode(await Od(e))}async function he(e){try{return await G(e)}catch{return null}}function xn(e){return e?.code}var Ud=/^[A-Za-z0-9_-]+$/;function Hd(e=process.env){return e.FUI_SESSION_DIR||jr(e.HOME||Fd(),".fui","session")}function Ir(e=process.env){let t=e.FUI_SESSION||"default";if(!Ud.test(t))throw new h(`invalid FUI_SESSION "${t}": letters, digits, "_" or "-"`);return jr(Hd(e),`cli-${t}.json`)}class On{file;constructor(e=Ir()){this.file=e}async read(){let e=await he(this.file);if(!e)return{};try{let t=JSON.parse(e);return t&&typeof t==="object"&&!Array.isArray(t)?t:{}}catch{return{}}}async write(e){await Md(jr(this.file,".."),{recursive:!0});let t=`${this.file}.${process.pid}.tmp`;await L(t,JSON.stringify(e,null,2)),await Ld(t,this.file)}async update(e){let t=await this.read();return e(t),await this.write(t),t}async getTarget(){return(await this.read()).target??null}async setTarget(e){let t={projectId:e.projectId,moduleId:e.moduleId,moduleDir:e.moduleDir,workspaceRoot:e.workspaceRoot,...e.target!==void 0?{target:e.target}:{}};await this.update((n)=>{n.target=t})}async clearTarget(){await this.update((e)=>{delete e.target})}async createApproval(e){let t={...e,token:crypto.randomUUID(),createdAt:new Date().toISOString()};return await this.update((n)=>{n.approval=t}),t}async getApproval(){return(await this.read()).approval??null}async clearApproval(){await this.update((e)=>{delete e.approval})}async markSkillLoaded(){await this.update((e)=>{e.skillLoadedAt=new Date().toISOString()})}async wasSkillLoaded(){return Boolean((await this.read()).skillLoadedAt)}async markSpVerified(e){await this.update((t)=>{let n=new Set(t.spVerified??[]);for(let o of e)n.add(o.toLowerCase());t.spVerified=[...n]})}async getVerifiedSpNames(){return new Set((await this.read()).spVerified??[])}}function Zt(e=process.env){return["1","true","yes"].includes((e.FUI_STRICT||"").trim().toLowerCase())}import{readdir as Vd}from"node:fs/promises";import{isAbsolute as Wd,join as Rr,relative as qd,sep as Bd}from"node:path";var Jd=/^[A-Za-z0-9._ -]+$/,Cr=20;function it(e,t){if(!e||!Jd.test(e)||/^\.+$/.test(e))throw new h(`invalid ${t} ${JSON.stringify(e)}: letters, digits, ".", "_", "-" and spaces only (used as a workspace folder name)`);return e}function Kd(e,t){let n=qd(e,t);if(!n||n.startsWith("..")||Wd(n))return null;let[o,s,r]=n.split(Bd);if(!o||o.startsWith("_")||o.startsWith("."))return null;if(s==="modules"&&r)return{projectId:o,moduleId:r,moduleDir:Rr(e,o,"modules",r)};return{projectId:o}}async function Fi(e,t){try{return(await Vd(e,{withFileTypes:!0})).filter((o)=>o.isDirectory()&&!(t&&(o.name.startsWith("_")||o.name.startsWith(".")))).map((o)=>o.name).sort()}catch{return[]}}function Ui(e){return e.length>Cr?[...e.slice(0,Cr),` ... and ${e.length-Cr} more`]:e}class Tr{paths;session;cwd;constructor(e,t,n=process.cwd()){this.paths=e;this.session=t;this.cwd=n}cwdTarget(){return Kd(this.paths.root,this.cwd)}async contextProjectId(){return this.cwdTarget()?.projectId??(await this.session.getTarget())?.projectId??void 0}async resolveProject(e){if(e)return{projectId:it(e,"projectId"),source:"explicit"};let t=this.cwdTarget();if(t)return{projectId:t.projectId,source:"cwd"};let n=await this.session.getTarget();if(n?.projectId)return{projectId:n.projectId,source:"session"};throw new h(await this.noTargetMenu("project"))}async resolveProjectModule(e,t){if(e&&t)return it(e,"projectId"),it(t,"moduleId"),{projectId:e,moduleId:t,moduleDir:Rr(this.paths.root,e,"modules",t),source:"explicit"};if(e||t)throw new h("module commands need both -p <project> and -m <module>");let n=this.cwdTarget();if(n?.moduleId&&n.moduleDir)return{projectId:n.projectId,moduleId:n.moduleId,moduleDir:n.moduleDir,source:"cwd"};let o=await this.session.getTarget();if(o?.projectId&&o.moduleId&&o.moduleDir)return{projectId:o.projectId,moduleId:o.moduleId,moduleDir:o.moduleDir,source:"session"};throw new h(await this.noTargetMenu("module"))}async resolveModuleDir(e,t,n){if(e)return e;return(await this.resolveProjectModule(t,n)).moduleDir}async noTargetMenu(e){let t=await Fi(this.paths.root,!0),n=e==="project"?"no project: pass -p <project>, cd into a workspace project, or run: fui use <project>/<module>":"no module: pass -p <project> -m <module>, cd into a module folder, or run: fui use <project>/<module>";if(t.length===0)return`${n}
5
+ workspace ${this.paths.root} has no projects; run: fui project list`;if(e==="module"&&t.length===1){let o=await Fi(Rr(this.paths.root,t[0],"modules"),!1);if(o.length>0)return[n,...Ui(o.map((s)=>` fui use ${t[0]}/${s}`))].join(`
6
+ `)}return[n,...Ui(t.map((o)=>` -p ${o}`))].join(`
7
+ `)}}function Fe(e){return e.trim().replace(/^https?:\/\//i,"").replace(/\/.*$/,"").toLowerCase()}function Pe(e){let t=Fe(e);return`${/^(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/.test(t)?"http":"https"}://${t}`}function Gd(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 Hi(e,t,n,o){let s;try{s=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 r=await s.text();if(!s.ok)throw Error(`tAPI HTTP ${s.status}: ${Gd(r)||s.statusText}`);try{return JSON.parse(r)}catch{throw Error(`tAPI returned non-JSON: ${r.slice(0,400)}`)}}class at{origin;dbToken;engine="mssql";constructor(e,t){this.origin=e;this.dbToken=t}async query(e){return(await Hi(this.origin,"/help/_sql_execute",this.dbToken,e)).data??[]}exec(e){return Hi(this.origin,"/help/_sql_execute_NonQuery",this.dbToken,e)}async close(){}}async function Pr(e,t,n,o,s){let r=`${e}/help/${t}/${n?"auth/":""}${o}`,i={};if(!n&&s)i.authorization=`Bearer ${s}`;let a=await fetch(r,{headers:i,signal:AbortSignal.timeout(15000)});return{url:r,status:a.status,ok:a.ok,text:await a.text()}}function Er(e){return Buffer.from(e,"utf8").toString("base64")}function _n(e){try{let t=e.replace(/\s+/g,"");if(!/^[A-Za-z0-9+/]*={0,2}$/.test(t)||t.length%4===1)throw Error("not base64");return new TextDecoder("utf-8",{fatal:!0}).decode(Buffer.from(t,"base64"))}catch{throw new h("token is not valid Base64 UTF-8")}}function en(e,t,n,o){return Er(`Server=${e}; Database=${t}; UID=${n}; PWD=${o}; Encrypt=True; TrustServerCertificate=True;`)}function Vi(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 zd(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=Vi(t);if(n.has("server")||n.has("data source"))return"mssql";throw new h("unsupported connection: expected postgres://, mysql://, mariadb://, sqlite:/file: URL or MSSQL Server=...; string")}function Mn(e){let t=_n(e);return{engine:zd(t),plain:t}}function tn(e){let{engine:t,plain:n}=Mn(e);if(t==="mssql"){let s=Vi(n);return{engine:t,server:s.get("server")??s.get("data source")??"",database:s.get("database")??s.get("initial catalog")??"",user:s.get("uid")??s.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 Xd,readdir as Yd,writeFile as Qd}from"node:fs/promises";import{basename as Wi,dirname as Bi,extname as Ji,isAbsolute as Zd,join as Ar,relative as Ki,sep as ep}from"node:path";var qi=50;function vo(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 tp(e,t,n={}){let o=Ki(e.root,t),r=o!==""&&!o.startsWith("..")&&!Zd(o)?o:Ar("_external",Wi(t)),i=Ji(r),a=Wi(r,i),c=n.stamp??vo(),l=n.suffix?`_${n.suffix}`:"";return Ar(e.historyRoot(),Bi(r),`${a}_${c}${l}${i}`)}async function np(e,t){let n=Ji(e),o=e.slice(0,e.length-n.length);for(let r=1;r<=qi;r++){let i=r===1?e:`${o}_${r}${n}`;try{return await Qd(i,t,{encoding:"utf-8",flag:"wx"}),i}catch(a){if(xn(a)!=="EEXIST")throw a}}let s=`${o}_${qi}${n}`;return await L(s,t),s}async function fe(e,t,n={}){let o=n.content;if(o===void 0)try{o=await G(t)}catch(r){let i=xn(r);if(i==="ENOENT"||i==="ENOTDIR")return null;throw r}let s=tp(e,t,n);return await Xd(Bi(s),{recursive:!0}),np(s,o)}function At(e,t){let n=Ki(e.root,t);return(n.startsWith("..")?t:n).split(ep).join("/")}async function $o(e,t,n={}){let o=n.stamp??vo(),s=new Set(n.skipDirNames??["_preview"]),r=[];async function i(a){let c;try{c=await Yd(a,{withFileTypes:!0})}catch{return}for(let l of c){let u=Ar(a,l.name);if(l.isDirectory()){if(!s.has(l.name))await i(u)}else if(l.isFile()){let d=await fe(e,u,{suffix:n.suffix,stamp:o});if(d)r.push(d)}}}return await i(t),r}var Nt=2,op=/^[A-Za-z0-9_-]+$/,rp=/^[A-Za-z0-9._-]+$/,Xi=/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i,sp=/^[A-Za-z0-9_.-]+$/;function yt(e){if(!op.test(e))throw new h(`invalid alias "${e}": use letters, digits, "_" or "-"`);return e}function Gi(e){if(!rp.test(e)||e==="."||e===".."||/[. ]$/.test(e)||Xi.test(e))throw new h(`invalid connection name "${e}": use letters, digits, ".", "_" or "-"`);return e}function Ln(e){let t=e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"_").replace(/^[._-]+|[._-]+$/g,"").slice(0,64)||"db";return Xi.test(t)?`db_${t}`:t}function zi(e,t,n,o){let s=new Set(Object.keys(e.connections).map((d)=>d.toLowerCase())),r=Ln(t);if(!s.has(r))return r;let i=(n?Fe(n):"").split(".").filter(Boolean),a=Ln(["api","tapi"].includes(i[0]??"")?i[1]??i[0]??"zone":i[0]??"zone"),c=`${r}_${a}`;if(!s.has(c))return c;let l=`${c}_${Ln(o)}`;if(!s.has(l))return l;let u=2;while(s.has(`${l}_${u}`))u++;return`${l}_${u}`}function Dt(e){let{sqlServer:t,database:n}=e;if((!t||!n)&&e.dbToken)try{let o=tn(e.dbToken);t||=o.server,n||=o.database}catch{}return t&&n?{sqlServer:t,database:n}:null}function Nr(e,t,n){let o=(s)=>s.trim().toLowerCase();return Object.entries(e.connections).find(([,s])=>{let r=Dt(s);return r&&o(r.sqlServer)===o(t)&&o(r.database)===o(n)})?.[0]??null}function jo(e){if(!sp.test(e)||e.startsWith("_")||e.startsWith("."))throw new h(`invalid project id "${e}"`);return e}function So(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 h(`invalid --db "${e}": use <connectionName> or <project>/<connectionName>`)}function wt(e,t){let n=t.toLowerCase(),o=Object.keys(e.connections).find((r)=>r.toLowerCase()===n);if(o)return o;let s=Object.entries(e.connections).filter(([,r])=>(r.apiName??"").toLowerCase()===n).map(([r])=>r);return s.length===1?s[0]:null}function Yi(e,t,n){let o=t.toLowerCase(),s=Object.keys(e.connections).find((i)=>i.toLowerCase()===o);if(s)return s;let r=Object.entries(e.connections).filter(([,i])=>(i.apiName??"").toLowerCase()===o);if(r.length===1)return r[0][0];if(r.length>1&&n){let i=r.filter(([,a])=>a.apiDomain&&Fe(a.apiDomain)===n);if(i.length===1)return i[0][0]}return null}class ct{paths;constructor(e){this.paths=e}async readRaw(e){let t=this.paths.connectionsFile(e);if(!await ot(t))return null;let n=await G(t);if(!n.trim())return null;let o;try{o=JSON.parse(n)}catch(s){throw Error(`${t} is not valid JSON (${s.message}); fix it by hand before writing`)}if(!o||typeof o!=="object"||Array.isArray(o))throw Error(`${t} must be an object { version, default, connections }`);return o}connectionsOf(e,t){let n=t.connections??{};if(!n||typeof n!=="object"||Array.isArray(n))throw Error(`${e}: "connections" must be an object of connectionName -> config`);for(let[o,s]of Object.entries(n)){if(!s||typeof s!=="object"||Array.isArray(s))throw Error(`${e}: connection "${o}" must be an object`);Gi(o)}return n}async load(e){let t=await this.readRaw(e);if(!t)return{connections:{},version:Nt};let n=this.paths.connectionsFile(e);if(t.version!==Nt)throw new h(t.version===void 0?`${n} is the old v1 format; convert it once: fui db migrate -p ${e}`:`${n}: unsupported version ${String(t.version)}; fui reads version ${Nt}`);let o={};for(let[s,r]of Object.entries(this.connectionsOf(n,t))){if(typeof r.apiName!=="string"||!r.apiName)throw Error(`${n}: connection "${s}" has no apiName`);o[s]={...r,apiName:yt(r.apiName)}}return{...t,version:Nt,connections:o}}async migrate(e){let t=await this.readRaw(e);if(!t)return"none";if(t.version===Nt)return"current";let n=this.paths.connectionsFile(e);if(t.version!==void 0)throw new h(`${n}: unsupported version ${String(t.version)}; fui reads version ${Nt}`);let o={};for(let[a,c]of Object.entries(this.connectionsOf(n,t))){let l=c;o[a]={...l,apiName:yt(l.apiName??a),...Dt(l)}}let{version:s,connections:r,...i}=t;return await this.write(e,{version:Nt,...i,connections:o}),"migrated"}async write(e,t){let n=this.paths.connectionsFile(e);await fe(this.paths,n),await L(n,JSON.stringify(t,null,2))}async save(e,t){let n=await this.load(e),o=yt(t.apiName),s=Dt(t.config),r=t.connectionName?Gi(Ln(t.connectionName)):null,i=(c)=>Object.keys(n.connections).find((l)=>l.toLowerCase()===c.toLowerCase()),a;if(s){let c=Nr(n,s.sqlServer,s.database);if(c&&r&&r.toLowerCase()!==c.toLowerCase())throw new h(`${s.sqlServer}/${s.database} is already saved as connection "${c}"; folders are not renamed, use --db ${e}/${c}`);if(a=c??r??zi(n,o,t.config.apiDomain,s.database),!c&&i(a))throw new h(`connection name "${i(a)}" is taken by another database; pass --name, e.g. --name ${zi(n,o,t.config.apiDomain,s.database)}`)}else{let c=r??Ln(o);a=i(c)??c}if(n.connections[a]={...t.config,apiName:o,...s},t.makeDefault||!n.default||!wt(n,n.default))n.default=a;return await this.write(e,n),a}async update(e,t,n){let o=await this.load(e),s=o.connections[t];if(!s)throw new h(this.menu(e,o,`no connection "${t}" in project "${e}"`));let{apiName:r,...i}=n;o.connections[t]={...i,apiName:s.apiName,...Dt(n)},await this.write(e,o)}async setDefault(e,t){let n=await this.load(e),o=wt(n,t);if(!o)throw new h(this.menu(e,n,`no connection "${t}" in project "${e}"`));return n.default=o,await this.write(e,n),o}async listProjects(){let e=[];try{for await(let t of kr("*/_db/_connections.json",this.paths.root)){let n=t.split(/[\\/]/)[0];if(!n.startsWith("_"))e.push(n)}}catch(t){if(t.code!=="ENOENT")throw t}return e.sort()}async resolveProject(e){let t=e.db?So(e.db):void 0,n=t?.project??e.project??process.env.FUI_PROJECT??e.contextProject;if(n)return jo(n);let o=await this.listProjects();if(t){let r=[];for(let i of o)if(wt(await this.load(i),t.alias))r.push(i);if(r.length===1)return r[0];if(r.length===0)throw new h(`no project has connection "${t.alias}"; run: fui db list`);throw new h(`connection "${t.alias}" exists in several projects; use one of:
8
8
  `+r.map((i)=>` --db ${i}/${t.alias}`).join(`
9
- `))}let s=[];for(let r of o)if(Object.keys((await this.load(r)).connections).length>0)s.push(r);if(s.length===1)return s[0];if(s.length===0)throw new m("no DB connections in workspace; run: fui db add <apiName> -p <project> --token -");throw new m(`several projects have connections; pass -p <project> or --db <project>/<connectionName>:
9
+ `))}let s=[];for(let r of o)if(Object.keys((await this.load(r)).connections).length>0)s.push(r);if(s.length===1)return s[0];if(s.length===0)throw new h("no DB connections in workspace; run: fui db add <apiName> -p <project> --token -");throw new h(`several projects have connections; pass -p <project> or --db <project>/<connectionName>:
10
10
  `+s.map((r)=>` -p ${r}`).join(`
11
- `))}async resolve(e){let t=await this.resolveProject(e),n=await this.load(t);if(Object.keys(n.connections).length===0)throw new m(`project "${t}" has no DB connections; run: fui db add <apiName> -p ${t}`);let{alias:s,note:r}=await this.pickAlias(t,n,e),i=n.connections[s];if(!i.dbToken)throw new m(`connection ${t}/${s} has no dbToken; re-add it`);let{engine:a}=Mn(i.dbToken);if(a==="mssql"&&!i.apiDomain)throw new m(`connection ${t}/${s} is MSSQL and needs apiDomain; re-add with --api-domain`);return{projectId:t,alias:s,config:i,engine:a,dbToken:i.dbToken,note:r}}async pickAlias(e,t,n){let o=Object.keys(t.connections);if(n.db){let{alias:r}=$o(n.db),i=wt(t,r);if(!i)throw new m(this.menu(e,t,`no connection "${r}" in project "${e}"`));return{alias:i,note:null}}if(o.length===1)return{alias:o[0],note:null};if(n.objectName){let r=Array.isArray(n.objectName)?n.objectName:[n.objectName],i=r[0],a=r.map((l)=>l.toLowerCase()),c=[];for(let l of o){let u=await this.knownObjectNames(e,l);if(a.some((d)=>u.has(d)))c.push(l)}if(c.length===1)return{alias:c[0],note:`connection "${c[0]}" (inferred from "${i}")`};throw new m(this.menu(e,t,c.length===0?`cannot infer the connection for "${i}": it is in no connection of project "${e}"`:`"${i}" exists in several connections (${c.join(", ")}); pick one`))}if(n.write)throw new m(this.menu(e,t,`project "${e}" has ${o.length} connections; this command needs --db`));let s=t.default?wt(t,t.default):null;if(s)return{alias:s,note:`using default connection "${s}" (${o.length} connections)`};throw new m(this.menu(e,t,`project "${e}" has several connections and no default`))}async knownObjectNames(e,t){let n=new Set,o=this.paths.schemaFile(e,t);if(await ot(o))try{let s=await Yt(o);for(let r of[s.tables,s.procedures].flatMap((i)=>Array.isArray(i)?i:[])){let i=typeof r==="string"?/^CREATE\s+(?:TABLE|VIEW|PROCEDURE|FUNCTION)\s+(\S+)/i.exec(r):null;if(i)n.add(i[1].toLowerCase())}}catch{}for(let s of await this.localSqlNames(e,t))n.add(s.toLowerCase());return n}async localSqlNames(e,t){let n=[];try{for await(let o of yr("*.sql",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.entries(t.connections).map(([s,r])=>{let i=Dt(r),a=[`apiName ${r.apiName??s}`,...i?[`${i.sqlServer}/${i.database}`]:[]];return` --db ${e}/${s} (${a.join(", ")})${t.default===s?" (default)":""}`});return[n,...o].join(`
12
- `)}}import{join as xt}from"node:path";class Ln{root;constructor(e){this.root=e}projectDir(e){return xt(this.root,e)}dbDir(e){return xt(this.projectDir(e),"_db")}connectionsFile(e){return xt(this.dbDir(e),"_connections.json")}connDir(e,t){return xt(this.dbDir(e),t)}schemaFile(e,t){return xt(this.connDir(e,t),"schema.json")}spFile(e,t,n){return xt(this.connDir(e,t),`${n}.sql`)}historyRoot(){return xt(this.root,"_history")}}var Ki={project:{type:"string",short:"p"},db:{type:"string"},workdir:{type:"string"},json:{type:"boolean"},"no-header":{type:"boolean"},filter:{type:"string"},where:{type:"string",multiple:!0},fields:{type:"string"},help:{type:"boolean",short:"h"}};async function Gi(e,t){let n=await Ti(f(e,"workdir")),o=new Ln(n.path),s=e.json===!0,r={json:s,header:e["no-header"]!==!0,filter:f(e,"filter"),where:Zd(e.where),fields:f(e,"fields")?.split(",").map((c)=>c.trim()).filter(Boolean)},i=new On,a=new Cr(o,i);return{paths:o,session:i,targets:a,workdir:n,store:new ct(o),values:e,positionals:t,json:s,render:r}}function Zd(e){if(e===void 0)return;return(Array.isArray(e)?e:[e]).map((t)=>{let n=String(t),o=n.indexOf("=");if(o<=0)throw new m(`--where expects column=value, got "${n}"`);return[n.slice(0,o),n.slice(o+1)]})}function f(e,t){let n=e[t];return typeof n==="string"?n:void 0}async function X(e){return{store:e.store,session:e.session,project:f(e.values,"project"),contextProject:await e.targets.contextProjectId()}}function _(e){let t;return{paths:e.paths,session:e.session,targets:e.targets,api:()=>t??=bo()}}function ep(e){if(e===null||e===void 0)return"NULL";if(e instanceof Date)return e.toISOString();if(e instanceof Uint8Array)return`0x${Buffer.from(e).toString("hex")}`;return(typeof e==="object"?JSON.stringify(e):String(e)).replace(/\\/g,"\\\\").replace(/\t/g,"\\t").replace(/\r/g,"\\r").replace(/\n/g,"\\n")}function Xi(e,t){let n=[...new Set(e.flatMap((s)=>Object.keys(s)))],o=e.map((s)=>n.map((r)=>ep(s[r])));return t?[n,...o]:o}function tp(e,t=!0){if(e.length===0)return"";return`${Xi(e,t).map((n)=>n.join("\t")).join(`
11
+ `))}async resolve(e){let t=await this.resolveProject(e),n=await this.load(t);if(Object.keys(n.connections).length===0)throw new h(`project "${t}" has no DB connections; run: fui db add <apiName> -p ${t}`);let{alias:s,note:r}=await this.pickAlias(t,n,e),i=n.connections[s];if(!i.dbToken)throw new h(`connection ${t}/${s} has no dbToken; re-add it`);let{engine:a}=Mn(i.dbToken);if(a==="mssql"&&!i.apiDomain)throw new h(`connection ${t}/${s} is MSSQL and needs apiDomain; re-add with --api-domain`);return{projectId:t,alias:s,config:i,engine:a,dbToken:i.dbToken,note:r}}async pickAlias(e,t,n){let o=Object.keys(t.connections);if(n.db){let{alias:r}=So(n.db),i=wt(t,r);if(!i)throw new h(this.menu(e,t,`no connection "${r}" in project "${e}"`));return{alias:i,note:null}}if(o.length===1)return{alias:o[0],note:null};if(n.objectName){let r=Array.isArray(n.objectName)?n.objectName:[n.objectName],i=r[0],a=r.map((l)=>l.toLowerCase()),c=[];for(let l of o){let u=await this.knownObjectNames(e,l);if(a.some((d)=>u.has(d)))c.push(l)}if(c.length===1)return{alias:c[0],note:`connection "${c[0]}" (inferred from "${i}")`};throw new h(this.menu(e,t,c.length===0?`cannot infer the connection for "${i}": it is in no connection of project "${e}"`:`"${i}" exists in several connections (${c.join(", ")}); pick one`))}if(n.write)throw new h(this.menu(e,t,`project "${e}" has ${o.length} connections; this command needs --db`));let s=t.default?wt(t,t.default):null;if(s)return{alias:s,note:`using default connection "${s}" (${o.length} connections)`};throw new h(this.menu(e,t,`project "${e}" has several connections and no default`))}async knownObjectNames(e,t){let n=new Set,o=this.paths.schemaFile(e,t);if(await ot(o))try{let s=await Yt(o);for(let r of[s.tables,s.procedures].flatMap((i)=>Array.isArray(i)?i:[])){let i=typeof r==="string"?/^CREATE\s+(?:TABLE|VIEW|PROCEDURE|FUNCTION)\s+(\S+)/i.exec(r):null;if(i)n.add(i[1].toLowerCase())}}catch{}for(let s of await this.localSqlNames(e,t))n.add(s.toLowerCase());return n}async localSqlNames(e,t){let n=[];try{for await(let o of kr("*.sql",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.entries(t.connections).map(([s,r])=>{let i=Dt(r),a=[`apiName ${r.apiName??s}`,...i?[`${i.sqlServer}/${i.database}`]:[]];return` --db ${e}/${s} (${a.join(", ")})${t.default===s?" (default)":""}`});return[n,...o].join(`
12
+ `)}}import{join as xt}from"node:path";class Fn{root;constructor(e){this.root=e}projectDir(e){return xt(this.root,e)}dbDir(e){return xt(this.projectDir(e),"_db")}connectionsFile(e){return xt(this.dbDir(e),"_connections.json")}connDir(e,t){return xt(this.dbDir(e),t)}schemaFile(e,t){return xt(this.connDir(e,t),"schema.json")}spFile(e,t,n){return xt(this.connDir(e,t),`${n}.sql`)}historyRoot(){return xt(this.root,"_history")}}var Qi={project:{type:"string",short:"p"},db:{type:"string"},workdir:{type:"string"},json:{type:"boolean"},"no-header":{type:"boolean"},filter:{type:"string"},where:{type:"string",multiple:!0},fields:{type:"string"},help:{type:"boolean",short:"h"}};async function Zi(e,t){let n=await Di(f(e,"workdir")),o=new Fn(n.path),s=e.json===!0,r={json:s,header:e["no-header"]!==!0,filter:f(e,"filter"),where:ip(e.where),fields:f(e,"fields")?.split(",").map((c)=>c.trim()).filter(Boolean)},i=new On,a=new Tr(o,i);return{paths:o,session:i,targets:a,workdir:n,store:new ct(o),values:e,positionals:t,json:s,render:r}}function ip(e){if(e===void 0)return;return(Array.isArray(e)?e:[e]).map((t)=>{let n=String(t),o=n.indexOf("=");if(o<=0)throw new h(`--where expects column=value, got "${n}"`);return[n.slice(0,o),n.slice(o+1)]})}function f(e,t){let n=e[t];return typeof n==="string"?n:void 0}async function X(e){return{store:e.store,session:e.session,project:f(e.values,"project"),contextProject:await e.targets.contextProjectId()}}function _(e){let t;return{paths:e.paths,session:e.session,targets:e.targets,api:()=>t??=ko()}}function ap(e){if(e===null||e===void 0)return"NULL";if(e instanceof Date)return e.toISOString();if(e instanceof Uint8Array)return`0x${Buffer.from(e).toString("hex")}`;return(typeof e==="object"?JSON.stringify(e):String(e)).replace(/\\/g,"\\\\").replace(/\t/g,"\\t").replace(/\r/g,"\\r").replace(/\n/g,"\\n")}function ta(e,t){let n=[...new Set(e.flatMap((s)=>Object.keys(s)))],o=e.map((s)=>n.map((r)=>ap(s[r])));return t?[n,...o]:o}function cp(e,t=!0){if(e.length===0)return"";return`${ta(e,t).map((n)=>n.join("\t")).join(`
13
13
  `)}
14
- `}function np(e,t=!0){if(e.length===0)return"";let n=Xi(e,t),o=n[0].map((r,i)=>Math.max(...n.map((a)=>br(a[i]))));return`${n.map((r)=>r.map((i,a)=>a===r.length-1?i:i+" ".repeat(o[a]-br(i))).join(" ")).join(`
14
+ `}function lp(e,t=!0){if(e.length===0)return"";let n=ta(e,t),o=n[0].map((r,i)=>Math.max(...n.map((a)=>vr(a[i]))));return`${n.map((r)=>r.map((i,a)=>a===r.length-1?i:i+" ".repeat(o[a]-vr(i))).join(" ")).join(`
15
15
  `)}
16
- `}function Nr(e){return`${JSON.stringify(e,(t,n)=>typeof n==="bigint"?n.toString():n)}
17
- `}function zi(e){return e===null||e===void 0?"":typeof e==="object"?JSON.stringify(e):String(e)}function op(e,{filter:t,where:n,fields:o}){let s=[...new Set(e.flatMap((a)=>Object.keys(a)))],r=(a)=>{let c=s.find((l)=>l.toLowerCase()===a.toLowerCase());if(!c&&e.length>0)throw new m(`unknown column "${a}"; columns: ${s.join(", ")}`);return c},i=e;if(t){let a=t.toLowerCase();i=i.filter((c)=>Object.values(c).some((l)=>zi(l).toLowerCase().includes(a)))}for(let[a,c]of n??[]){let l=r(a);if(l)i=i.filter((u)=>zi(u[l]).toLowerCase()===c.toLowerCase())}if(o?.length){let a=o.map(r).filter((c)=>Boolean(c));i=i.map((c)=>Object.fromEntries(a.map((l)=>[l,c[l]])))}return i}function z(e,t){let{json:n,header:o,isTty:s=process.stdout.isTTY===!0}=t,r=op(e,t);if(n)return Nr(r);return s?np(r,o):tp(r,o)}function Yi(e,t){let n=t>0&&e.length>t;return{rows:n?e.slice(0,t):e,total:e.length,truncated:n}}import{readdir as rp}from"node:fs/promises";import{join as ea}from"node:path";var sp=20000;async function Un(e,t,n,o){if(!t)throw new m('no userToken: run fui db user-token first (a developer token with access to the permission database, alias "acc")');let s=await fetch(`${Ee(e)}/acc/${n}`,{method:"POST",headers:{"Content-Type":"application/json",authorization:`Bearer ${t}`},body:JSON.stringify(o),signal:AbortSignal.timeout(sp)}),r=await s.json().catch(()=>null);if(!s.ok||r&&r.Message&&!r.data)throw Error(r?.Message??`HTTP ${s.status}: ${s.statusText}`);return r??{}}async function ta(e){try{return(await rp(e,{withFileTypes:!0})).filter((t)=>t.isDirectory()&&!t.name.startsWith("_")).map((t)=>t.name)}catch{return[]}}async function Ar(e){let t=[];for(let n of await ta(e.paths.root)){let o=await e.load(n);for(let[s,r]of Object.entries(o.connections))if(r?.userToken)t.push({projectId:n,alias:s,userToken:r.userToken,apiDomain:r.apiDomain?Le(r.apiDomain):""})}return t}function Dr(e){let t=new Map;for(let n of e){let o=t.get(n.userToken)??[];o.push({projectId:n.projectId,alias:n.alias,apiDomain:n.apiDomain??""}),t.set(n.userToken,o)}return[...t.entries()].map(([n,o])=>({token:n,sources:o}))}function Qi(e){return e.map((t)=>`${t.projectId}/${t.alias}`).join(", ")}function na(e){return e.map((t)=>`${t.projectId}/${t.alias} (${t.apiDomain||"?"})`).join(", ")}function oa(e){return Qt("sha256",e).slice(-8)}function ra(e,t,n){let o=t.split("/").filter(Boolean);if(o.length===2)return{projectId:o[0],alias:o[1]};if(o.length===1)return{projectId:e,alias:o[0]};throw new m(`${n} must be "<project>/<alias>" or "<alias>" (a connection of this project); got "${t}"`)}async function sa(e,t,n){let{projectId:o,alias:s}=ra(t,n,"--from"),r=await e.load(o),i=wt(r,s);if(!i)throw new m(`no connection "${s}" in project "${o}"; see: fui db list -p ${o}`);let a=r.connections[i].userToken;if(!a)throw new m(`connection ${o}/${i} has no userToken to borrow; set one there first: fui db user-token -p ${o} --db ${i} ...`);return{userToken:a,source:`${o}/${i}`}}async function ia(e,t){let n=await he(ea(e.projectDir(t),"project.json")),o;try{o=n===null?null:JSON.parse(n)}catch{return null}if(!o||typeof o!=="object")return null;let s=o,i=(s.data&&typeof s.data==="object"&&!Array.isArray(s.data)?s.data:s).apiDomain;if(typeof i!=="string"||!i.trim())return null;return Le(i)}async function jo(e,t,n){let o=await e.load(t);return Ji(o,n,await ia(e.paths,t))}async function xr(e,t,n){if(n){let o=await jo(e,t,n);if(o)return e.resolve({project:t,db:o})}return e.resolve({project:t})}async function Io(e,t,n){let o=n.apiDomain?Le(n.apiDomain):void 0;if(o&&n.userToken)return{apiDomain:o,userToken:n.userToken,note:null};try{let{alias:i,config:a}=await xr(e,t,n.apiName),c=o??(a.apiDomain?Le(a.apiDomain):a.apiDomain),l=n.userToken??a.userToken;if(c&&l)return{apiDomain:c,userToken:l,note:`using apiDomain + userToken of connection "${i}" (project ${t})`}}catch{}let s=o??await ia(e.paths,t);if(!s)throw new m(`cannot tell the apiDomain of the acc system for project "${t}"; pass --api-domain <host> (and a userToken if the project has no connection), or run: fui project sync -p ${t}`);if(n.userToken)return{apiDomain:s,userToken:n.userToken,note:null};let r=Dr((await Ar(e)).filter((i)=>i.apiDomain===s));if(r.length===1){let i=r[0];return{apiDomain:s,userToken:i.token,note:`project "${t}" has no connection; borrowing the userToken of ${Qi(i.sources)} (same apiDomain ${s})`}}if(r.length===0)throw new m(`no userToken for apiDomain "${s}" in the workspace; pass --user-token-env VAR, or run fui db user-token on a project connected to that domain`);throw new m([`${r.length} different userTokens share apiDomain "${s}"; not picking one (a wrong token lists another system's data that still looks valid)`,"sources (one line per distinct token):",...r.map((i)=>` ${Qi(i.sources)}`),"pass --user-token-env VAR with the token of the source you mean"].join(`
18
- `))}async function Hn(e){return(await Un(e.apiDomain,e.userToken,"SM_Modules_ManagerSelect",{})).data??[]}function Or(e){return e.map((t)=>({apiName:String(t.APIName??""),sqlServer:String(t.SQLServer??""),database:String(t.DatabaseName??""),enable:t.Enable===!1?"no":"yes",moduleName:String(t.ModuleName??"")}))}function Zi(e){let t;try{t=_n(e)}catch{return null}let n=/(?:^|;)\s*UID\s*=\s*([^;]*)/i.exec(t)?.[1]?.trim(),o=/(?:^|;)\s*PWD\s*=\s*([^;]*)/i.exec(t)?.[1]?.trim();if(!n||!o)return null;return{uid:n,password:o}}async function aa(e,t,n,o){let s=async(a,c)=>{let l=await e.load(a),u=wt(l,c);if(!u)throw new m(`no connection "${c}" in project "${a}"; see: fui db list -p ${a}`);let d=Zi(l.connections[u].dbToken??"");if(!d)throw new m(`the dbToken of ${a}/${u} has no UID/PWD to borrow (not a standard connection string); use fui db add with your own token`);return{creds:d,source:`${a}/${u}`}};if(n){let{projectId:a,alias:c}=ra(t,n,"--borrow-from");return s(a,c)}let r=[];for(let a of await ta(e.paths.root)){let c=await e.load(a);for(let[l,u]of Object.entries(c.connections)){if(u.apiDomain!==o||!u.dbToken)continue;let d=Zi(u.dbToken);if(d)r.push({source:`${a}/${l}`,creds:d})}}let i=new Map;for(let a of r){let c=JSON.stringify([a.creds.uid,a.creds.password]);if(!i.has(c))i.set(c,a)}if(i.size===1)return[...i.values()][0];if(i.size===0)throw new m(`no connection in the workspace uses apiDomain "${o}" to borrow a SQL login from; use fui db add <apiName> --api-domain ${o} --token -`);throw new m([`${i.size} different SQL logins on apiDomain "${o}"; not picking one (each database group has its own login)`,"sources:",...[...i.values()].map((a)=>` --borrow-from ${a.source}`)].join(`
19
- `))}async function ca(e,t){let n,o;try{let i=await jo(e,t,"acc");if(!i)throw Error("no acc alias");let a=await e.resolve({project:t,db:i});n=Ee(a.config.apiDomain),o=a.dbToken}catch{throw new m(`cannot compute a new ModuleID: project "${t}" has no connection to the permission database (alias "acc"). ModuleID must be MAX(ModuleID)+1 over ALL of acc.tblModules. connect it: fui db add-by-name acc, or pass --module-id <n>`)}let s=await new at(n,o).query("SELECT ISNULL(MAX(ModuleID), 0) + 1 AS NextID FROM tblModules"),r=Number(s[0]?.NextID);if(!Number.isInteger(r)||r<=0)throw Error(`could not read MAX(ModuleID)+1 from acc.tblModules (got ${JSON.stringify(s[0])})`);return r}async function _r(e,t,n,o){try{let s=ea(e.dbDir(t),"rights.json"),r={};try{r=JSON.parse(await he(s)??"")}catch{}let i=Object.keys(r).find((c)=>c.toLowerCase()===n.toLowerCase())??n,a=r[i]??{};r[i]={...a,...o.system?{systemRights:o.system,systemFetchedAt:new Date().toISOString()}:{},...o.functions?{functionRights:o.functions,functionFetchedAt:new Date().toISOString()}:{}},await F(s,JSON.stringify(r,null,2))}catch{}}import{mkdir as Bp,rename as Jp,unlink as Kp}from"node:fs/promises";import{dirname as Gp}from"node:path";class Mr{engine;url;db;constructor(e,t){this.engine=e;this.url=t}async open(){if(this.engine!=="sqlite")throw Error(`${this.engine} connections are not supported; fui connects to MSSQL through tAPI and to SQLite`);return this.db??=new(await import("node:sqlite")).DatabaseSync(this.url.replace(/^(?:sqlite|file):(?:\/\/)?/i,"")),this.db}async query(e){return(await this.open()).prepare(e).all().map((t)=>({...t}))}async exec(e){let t=await this.open(),n=()=>Number(t.prepare("SELECT total_changes() AS n").all()[0]?.n??0),o=n();return t.exec(e),{count:n()-o}}async close(){this.db?.close()}}function Fr(e){if(e.engine==="mssql")return new at(Ee(e.config.apiDomain),e.dbToken);return new Mr(e.engine,_n(e.dbToken))}async function je(e,t){let n=Fr(e);try{return await t(n)}finally{await n.close()}}import{mkdir as Cp,readdir as Rp,unlink as Tp}from"node:fs/promises";import{dirname as Ep,join as Pp}from"node:path";var ua=`
16
+ `}function Dr(e){return`${JSON.stringify(e,(t,n)=>typeof n==="bigint"?n.toString():n)}
17
+ `}function ea(e){return e===null||e===void 0?"":typeof e==="object"?JSON.stringify(e):String(e)}function up(e,{filter:t,where:n,fields:o}){let s=[...new Set(e.flatMap((a)=>Object.keys(a)))],r=(a)=>{let c=s.find((l)=>l.toLowerCase()===a.toLowerCase());if(!c&&e.length>0)throw new h(`unknown column "${a}"; columns: ${s.join(", ")}`);return c},i=e;if(t){let a=t.toLowerCase();i=i.filter((c)=>Object.values(c).some((l)=>ea(l).toLowerCase().includes(a)))}for(let[a,c]of n??[]){let l=r(a);if(l)i=i.filter((u)=>ea(u[l]).toLowerCase()===c.toLowerCase())}if(o?.length){let a=o.map(r).filter((c)=>Boolean(c));i=i.map((c)=>Object.fromEntries(a.map((l)=>[l,c[l]])))}return i}function z(e,t){let{json:n,header:o,isTty:s=process.stdout.isTTY===!0}=t,r=up(e,t);if(n)return Dr(r);return s?lp(r,o):cp(r,o)}function na(e,t){let n=t>0&&e.length>t;return{rows:n?e.slice(0,t):e,total:e.length,truncated:n}}import{readdir as dp}from"node:fs/promises";import{join as sa}from"node:path";var pp=20000;async function Un(e,t,n,o){if(!t)throw new h('no userToken: run fui db user-token first (a developer token with access to the permission database, alias "acc")');let s=await fetch(`${Pe(e)}/acc/${n}`,{method:"POST",headers:{"Content-Type":"application/json",authorization:`Bearer ${t}`},body:JSON.stringify(o),signal:AbortSignal.timeout(pp)}),r=await s.json().catch(()=>null);if(!s.ok||r&&r.Message&&!r.data)throw Error(r?.Message??`HTTP ${s.status}: ${s.statusText}`);return r??{}}async function ia(e){try{return(await dp(e,{withFileTypes:!0})).filter((t)=>t.isDirectory()&&!t.name.startsWith("_")).map((t)=>t.name)}catch{return[]}}async function xr(e){let t=[];for(let n of await ia(e.paths.root)){let o=await e.load(n);for(let[s,r]of Object.entries(o.connections))if(r?.userToken)t.push({projectId:n,alias:s,userToken:r.userToken,apiDomain:r.apiDomain?Fe(r.apiDomain):""})}return t}function Or(e){let t=new Map;for(let n of e){let o=t.get(n.userToken)??[];o.push({projectId:n.projectId,alias:n.alias,apiDomain:n.apiDomain??""}),t.set(n.userToken,o)}return[...t.entries()].map(([n,o])=>({token:n,sources:o}))}function oa(e){return e.map((t)=>`${t.projectId}/${t.alias}`).join(", ")}function aa(e){return e.map((t)=>`${t.projectId}/${t.alias} (${t.apiDomain||"?"})`).join(", ")}function ca(e){return Qt("sha256",e).slice(-8)}function la(e,t,n){let o=t.split("/").filter(Boolean);if(o.length===2)return{projectId:o[0],alias:o[1]};if(o.length===1)return{projectId:e,alias:o[0]};throw new h(`${n} must be "<project>/<alias>" or "<alias>" (a connection of this project); got "${t}"`)}async function ua(e,t,n){let{projectId:o,alias:s}=la(t,n,"--from"),r=await e.load(o),i=wt(r,s);if(!i)throw new h(`no connection "${s}" in project "${o}"; see: fui db list -p ${o}`);let a=r.connections[i].userToken;if(!a)throw new h(`connection ${o}/${i} has no userToken to borrow; set one there first: fui db user-token -p ${o} --db ${i} ...`);return{userToken:a,source:`${o}/${i}`}}async function da(e,t){let n=await he(sa(e.projectDir(t),"project.json")),o;try{o=n===null?null:JSON.parse(n)}catch{return null}if(!o||typeof o!=="object")return null;let s=o,i=(s.data&&typeof s.data==="object"&&!Array.isArray(s.data)?s.data:s).apiDomain;if(typeof i!=="string"||!i.trim())return null;return Fe(i)}async function Io(e,t,n){let o=await e.load(t);return Yi(o,n,await da(e.paths,t))}async function _r(e,t,n){if(n){let o=await Io(e,t,n);if(o)return e.resolve({project:t,db:o})}return e.resolve({project:t})}async function Co(e,t,n){let o=n.apiDomain?Fe(n.apiDomain):void 0;if(o&&n.userToken)return{apiDomain:o,userToken:n.userToken,note:null};try{let{alias:i,config:a}=await _r(e,t,n.apiName),c=o??(a.apiDomain?Fe(a.apiDomain):a.apiDomain),l=n.userToken??a.userToken;if(c&&l)return{apiDomain:c,userToken:l,note:`using apiDomain + userToken of connection "${i}" (project ${t})`}}catch{}let s=o??await da(e.paths,t);if(!s)throw new h(`cannot tell the apiDomain of the acc system for project "${t}"; pass --api-domain <host> (and a userToken if the project has no connection), or run: fui project sync -p ${t}`);if(n.userToken)return{apiDomain:s,userToken:n.userToken,note:null};let r=Or((await xr(e)).filter((i)=>i.apiDomain===s));if(r.length===1){let i=r[0];return{apiDomain:s,userToken:i.token,note:`project "${t}" has no connection; borrowing the userToken of ${oa(i.sources)} (same apiDomain ${s})`}}if(r.length===0)throw new h(`no userToken for apiDomain "${s}" in the workspace; pass --user-token-env VAR, or run fui db user-token on a project connected to that domain`);throw new h([`${r.length} different userTokens share apiDomain "${s}"; not picking one (a wrong token lists another system's data that still looks valid)`,"sources (one line per distinct token):",...r.map((i)=>` ${oa(i.sources)}`),"pass --user-token-env VAR with the token of the source you mean"].join(`
18
+ `))}async function Hn(e){return(await Un(e.apiDomain,e.userToken,"SM_Modules_ManagerSelect",{})).data??[]}function Mr(e){return e.map((t)=>({apiName:String(t.APIName??""),sqlServer:String(t.SQLServer??""),database:String(t.DatabaseName??""),enable:t.Enable===!1?"no":"yes",moduleName:String(t.ModuleName??"")}))}function ra(e){let t;try{t=_n(e)}catch{return null}let n=/(?:^|;)\s*UID\s*=\s*([^;]*)/i.exec(t)?.[1]?.trim(),o=/(?:^|;)\s*PWD\s*=\s*([^;]*)/i.exec(t)?.[1]?.trim();if(!n||!o)return null;return{uid:n,password:o}}async function pa(e,t,n,o){let s=async(a,c)=>{let l=await e.load(a),u=wt(l,c);if(!u)throw new h(`no connection "${c}" in project "${a}"; see: fui db list -p ${a}`);let d=ra(l.connections[u].dbToken??"");if(!d)throw new h(`the dbToken of ${a}/${u} has no UID/PWD to borrow (not a standard connection string); use fui db add with your own token`);return{creds:d,source:`${a}/${u}`}};if(n){let{projectId:a,alias:c}=la(t,n,"--borrow-from");return s(a,c)}let r=[];for(let a of await ia(e.paths.root)){let c=await e.load(a);for(let[l,u]of Object.entries(c.connections)){if(u.apiDomain!==o||!u.dbToken)continue;let d=ra(u.dbToken);if(d)r.push({source:`${a}/${l}`,creds:d})}}let i=new Map;for(let a of r){let c=JSON.stringify([a.creds.uid,a.creds.password]);if(!i.has(c))i.set(c,a)}if(i.size===1)return[...i.values()][0];if(i.size===0)throw new h(`no connection in the workspace uses apiDomain "${o}" to borrow a SQL login from; use fui db add <apiName> --api-domain ${o} --token -`);throw new h([`${i.size} different SQL logins on apiDomain "${o}"; not picking one (each database group has its own login)`,"sources:",...[...i.values()].map((a)=>` --borrow-from ${a.source}`)].join(`
19
+ `))}async function ma(e,t){let n,o;try{let i=await Io(e,t,"acc");if(!i)throw Error("no acc alias");let a=await e.resolve({project:t,db:i});n=Pe(a.config.apiDomain),o=a.dbToken}catch{throw new h(`cannot compute a new ModuleID: project "${t}" has no connection to the permission database (alias "acc"). ModuleID must be MAX(ModuleID)+1 over ALL of acc.tblModules. connect it: fui db add-by-name acc, or pass --module-id <n>`)}let s=await new at(n,o).query("SELECT ISNULL(MAX(ModuleID), 0) + 1 AS NextID FROM tblModules"),r=Number(s[0]?.NextID);if(!Number.isInteger(r)||r<=0)throw Error(`could not read MAX(ModuleID)+1 from acc.tblModules (got ${JSON.stringify(s[0])})`);return r}async function Lr(e,t,n,o){try{let s=sa(e.dbDir(t),"rights.json"),r={};try{r=JSON.parse(await he(s)??"")}catch{}let i=Object.keys(r).find((c)=>c.toLowerCase()===n.toLowerCase())??n,a=r[i]??{};r[i]={...a,...o.system?{systemRights:o.system,systemFetchedAt:new Date().toISOString()}:{},...o.functions?{functionRights:o.functions,functionFetchedAt:new Date().toISOString()}:{}},await L(s,JSON.stringify(r,null,2))}catch{}}import{mkdir as Qp,rename as Zp,unlink as em}from"node:fs/promises";import{dirname as tm}from"node:path";class Fr{engine;url;db;constructor(e,t){this.engine=e;this.url=t}async open(){if(this.engine!=="sqlite")throw Error(`${this.engine} connections are not supported; fui connects to MSSQL through tAPI and to SQLite`);return this.db??=new(await import("node:sqlite")).DatabaseSync(this.url.replace(/^(?:sqlite|file):(?:\/\/)?/i,"")),this.db}async query(e){return(await this.open()).prepare(e).all().map((t)=>({...t}))}async exec(e){let t=await this.open(),n=()=>Number(t.prepare("SELECT total_changes() AS n").all()[0]?.n??0),o=n();return t.exec(e),{count:n()-o}}async close(){this.db?.close()}}function Ur(e){if(e.engine==="mssql")return new at(Pe(e.config.apiDomain),e.dbToken);return new Fr(e.engine,_n(e.dbToken))}async function je(e,t){let n=Ur(e);try{return await t(n)}finally{await n.close()}}import{mkdir as Dp,readdir as xp,unlink as Op}from"node:fs/promises";import{dirname as _p,join as Mp}from"node:path";var fa=`
20
20
  SELECT
21
21
  o.type AS ObjectTypeCode, o.name AS ObjectName, c.name AS ColumnName, t.name AS DataType,
22
22
  c.max_length, c.precision, c.scale, c.is_nullable, c.is_identity,
@@ -40,7 +40,7 @@ WHERE o.is_ms_shipped = 0 AND o.type IN ('U','V') AND s.name NOT IN ('sys','INFO
40
40
  AND o.name NOT LIKE 'sys%' AND o.name NOT LIKE 'dt[_]%'
41
41
  AND o.name NOT LIKE 'queue[_]%' AND o.name NOT LIKE 'filestream[_]%' AND o.name NOT LIKE 'cdc.%'
42
42
  ORDER BY ObjectTypeCode, ObjectName
43
- `.trim(),da=`
43
+ `.trim(),ga=`
44
44
  SELECT
45
45
  o.type AS ObjectTypeCode, o.name AS ObjectName,
46
46
  p.name AS ParameterName, pt.name AS ParameterType,
@@ -55,94 +55,94 @@ WHERE o.is_ms_shipped = 0 AND o.type IN ('P','FN','TF','IF') AND s.name NOT IN (
55
55
  AND o.name NOT IN ('fn_diagramobjects','sp_alterdiagram','sp_creatediagram','sp_dropdiagram',
56
56
  'sp_helpdiagramdefinition','sp_helpdiagrams','sp_renamediagram','sp_upgraddiagrams')
57
57
  ORDER BY ObjectTypeCode, ObjectName
58
- `.trim(),ip=new Set(["nvarchar","nchar","ntext"]);function pa(e,t,n,o){let s=e.toLowerCase();if(t===-1)return`${e}(max)`;if(s==="decimal"||s==="numeric")return o!==void 0?`${e}(${n},${o})`:`${e}(${n})`;let r=ip.has(s)?t/2:t;return r>0?`${e}(${r})`:e}function ap(e){let t=[String(e.ColumnName),pa(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 Lr(e,t){if(t)return"Function";if(/^spAPIFILE_/i.test(e))return"API File";if(/^spAPI_/i.test(e))return"API";return"Procedure"}function ma(e,t){let n=new Map;for(let s of e){let r=String(s.ObjectName??"");if(!r)continue;let i=n.get(r);if(!i)i={keyword:String(s.ObjectTypeCode??"").trim()==="V"?"VIEW":"TABLE",columns:[]},n.set(r,i);if(s.ColumnName)i.columns.push(ap(s))}let o=new Map;for(let s of t){let r=String(s.ObjectName??""),i=String(s.ObjectTypeCode??"").trim();if(!r||!["P","FN","TF","IF"].includes(i))continue;let a=o.get(r);if(!a)a={keyword:i==="P"?"PROCEDURE":"FUNCTION",params:[]},o.set(r,a);if(s.ParameterName){let c=pa(String(s.ParameterType??""),Number(s.ParameterMaxLength??0),Number(s.ParameterPrecision??0));a.params.push(`@${String(s.ParameterName).replace(/^@/,"")} ${c}`)}}return{fetchedAt:new Date().toISOString(),tables:[...n].map(([s,r])=>`CREATE ${r.keyword} ${s} (${r.columns.join(",")});`),procedures:[...o].map(([s,r])=>`CREATE ${r.keyword} ${s} (${r.params.join(",")});`)}}function Ot(e){if(!e||typeof e!=="object")return!1;let t=e,n=(o)=>Array.isArray(o)&&o.every((s)=>typeof s==="string");return n(t.tables)&&n(t.procedures)}function ha(e){return/^CREATE\s+(?:TABLE|VIEW|PROCEDURE|FUNCTION)\s+(\S+)/i.exec(e)?.[1]??null}var Ur=["Table","View","API","API File","Procedure","Function"];function Hr(e){if(/^CREATE TABLE/i.test(e))return"Table";if(/^CREATE VIEW/i.test(e))return"View";return null}function Vr(e){let t=e.match(/^CREATE (PROCEDURE|FUNCTION) (\S+)/i);return t?Lr(t[2],t[1].toUpperCase()==="FUNCTION"):null}var cp=/^(\S+)\s+(\S+)\s+(NULL|NOT NULL)(\s+PRIMARY KEY)?(\s+IDENTITY)?(?:\s+REFERENCES\s+(\S+)\((\S+)\))?$/,la=/^@(\S+)\s+(\S+)$/;function fa(e){let t=[],n=0,o="";for(let s of e){if(s==="(")n++;else if(s===")")n--;if(s===","&&n===0)t.push(o),o="";else o+=s}if(o.trim())t.push(o);return t}function Wr(e){let t=e.match(/^CREATE (TABLE|VIEW) (\S+) \((.*)\);?$/s),n=t?fa(t[3]):null;if(t&&n.every((s)=>cp.test(s.trim())))return{name:t[2],objectType:t[1]==="VIEW"?"View":"Table",count:n.length};let o=ha(e);if(!o)throw Error(`Cannot parse table DDL: ${e.slice(0,120)}`);return{name:o,objectType:Hr(e)??"Table",count:0,opaque:!0}}function Co(e){let t=e.match(/^CREATE (PROCEDURE|FUNCTION) (\S+) \((.*)\);?$/s),n=t?fa(t[3]):null;if(t&&n.every((r)=>la.test(r.trim())))return{name:t[2],objectType:Lr(t[2],t[1]==="FUNCTION"),count:n.length,parameters:n.map((r)=>la.exec(r.trim())[1])};let o=ha(e);if(!o)throw Error(`Cannot parse proc DDL: ${e.slice(0,120)}`);let s=/^CREATE\s+FUNCTION/i.test(e);return{name:o,objectType:Vr(e)??Lr(o,s),count:0,parameters:[],opaque:!0}}import{mkdir as fp,stat as gp}from"node:fs/promises";import{basename as wp,dirname as yp,join as Br,resolve as ya,sep as bp}from"node:path";import{mkdir as lp,readdir as up}from"node:fs/promises";import{join as nn,sep as dp}from"node:path";var on="_publishState.json";function ke(e){return e.split(dp).join("/")}function _t(e){return`sha1:${Qt("sha1",e.replace(/\r\n/g,`
59
- `))}`}async function Ro(e){try{let t=JSON.parse(await G(nn(e,on)));return t&&typeof t==="object"&&!Array.isArray(t)?t:{}}catch{return{}}}async function ga(e,t){await lp(e,{recursive:!0}),await F(nn(e,on),JSON.stringify(t,null,2))}async function Pe(e,t){if(t.length===0)return;let n=await Ro(e),o=!1;for(let s of t){let r=ke(s);try{n[r]=_t(await G(nn(e,r))),o=!0}catch{if(r in n)delete n[r],o=!0}}if(o)await ga(e,n)}async function wa(e,t){if(t.length===0)return;let n=await Ro(e),o=!1;for(let s of t){let r=ke(s);if(r in n)delete n[r],o=!0}if(o)await ga(e,n)}var pp=["module.json","header.html","body.html","script.js","style.css","project.json"];async function mp(e){try{return(await up(nn(e,"components"),{withFileTypes:!0})).filter((n)=>n.isFile()&&n.name.toLowerCase().endsWith(".vue")).map((n)=>ke(nn("components",n.name)))}catch{return[]}}async function Vn(e){let t=await Ro(e),n=[];for(let o of[...pp,...await mp(e)]){let s;try{s=await G(nn(e,o))}catch{continue}let r=t[o];if(r===void 0)n.push({relPath:o,reason:"new"});else if(r!==_t(s))n.push({relPath:o,reason:"changed"})}return n}async function rn(e,t,n){let o=(await Ro(e))[ke(t)];return o!==void 0&&_t(n)===o}var hp={"module.json":"fui module publish json","header.html":"fui module publish html","body.html":"fui module publish html","script.js":"fui module publish script","style.css":"fui module publish css"};function To(e){if(e.startsWith("components/"))return"fui component update";return hp[e]??"fui module publish json"}function qr(e){if(e.length===0)return null;return`unpublished local files: ${e.map((n)=>`${n.relPath} (${To(n.relPath)})`).join(", ")}`}var kp=172800000,vp=new Set(["_moduleInfo.json","_projectInfo.json","_modules.json","_components.json","_imports.json",on]);function ba(e){return vp.has(wp(e))}function $p(e,t){return e.replace(/\r\n/g,`
58
+ `.trim(),mp=new Set(["nvarchar","nchar","ntext"]);function wa(e,t,n,o){let s=e.toLowerCase();if(t===-1)return`${e}(max)`;if(s==="decimal"||s==="numeric")return o!==void 0?`${e}(${n},${o})`:`${e}(${n})`;let r=mp.has(s)?t/2:t;return r>0?`${e}(${r})`:e}function hp(e){let t=[String(e.ColumnName),wa(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 Hr(e,t){if(t)return"Function";if(/^spAPIFILE_/i.test(e))return"API File";if(/^spAPI_/i.test(e))return"API";return"Procedure"}function ya(e,t){let n=new Map;for(let s of e){let r=String(s.ObjectName??"");if(!r)continue;let i=n.get(r);if(!i)i={keyword:String(s.ObjectTypeCode??"").trim()==="V"?"VIEW":"TABLE",columns:[]},n.set(r,i);if(s.ColumnName)i.columns.push(hp(s))}let o=new Map;for(let s of t){let r=String(s.ObjectName??""),i=String(s.ObjectTypeCode??"").trim();if(!r||!["P","FN","TF","IF"].includes(i))continue;let a=o.get(r);if(!a)a={keyword:i==="P"?"PROCEDURE":"FUNCTION",params:[]},o.set(r,a);if(s.ParameterName){let c=wa(String(s.ParameterType??""),Number(s.ParameterMaxLength??0),Number(s.ParameterPrecision??0));a.params.push(`@${String(s.ParameterName).replace(/^@/,"")} ${c}`)}}return{fetchedAt:new Date().toISOString(),tables:[...n].map(([s,r])=>`CREATE ${r.keyword} ${s} (${r.columns.join(",")});`),procedures:[...o].map(([s,r])=>`CREATE ${r.keyword} ${s} (${r.params.join(",")});`)}}function Ot(e){if(!e||typeof e!=="object")return!1;let t=e,n=(o)=>Array.isArray(o)&&o.every((s)=>typeof s==="string");return n(t.tables)&&n(t.procedures)}function ba(e){return/^CREATE\s+(?:TABLE|VIEW|PROCEDURE|FUNCTION)\s+(\S+)/i.exec(e)?.[1]??null}var Vr=["Table","View","API","API File","Procedure","Function"];function Wr(e){if(/^CREATE TABLE/i.test(e))return"Table";if(/^CREATE VIEW/i.test(e))return"View";return null}function qr(e){let t=e.match(/^CREATE (PROCEDURE|FUNCTION) (\S+)/i);return t?Hr(t[2],t[1].toUpperCase()==="FUNCTION"):null}var fp=/^(\S+)\s+(\S+)\s+(NULL|NOT NULL)(\s+PRIMARY KEY)?(\s+IDENTITY)?(?:\s+REFERENCES\s+(\S+)\((\S+)\))?$/,ha=/^@(\S+)\s+(\S+)$/;function ka(e){let t=[],n=0,o="";for(let s of e){if(s==="(")n++;else if(s===")")n--;if(s===","&&n===0)t.push(o),o="";else o+=s}if(o.trim())t.push(o);return t}function Br(e){let t=e.match(/^CREATE (TABLE|VIEW) (\S+) \((.*)\);?$/s),n=t?ka(t[3]):null;if(t&&n.every((s)=>fp.test(s.trim())))return{name:t[2],objectType:t[1]==="VIEW"?"View":"Table",count:n.length};let o=ba(e);if(!o)throw Error(`Cannot parse table DDL: ${e.slice(0,120)}`);return{name:o,objectType:Wr(e)??"Table",count:0,opaque:!0}}function Ro(e){let t=e.match(/^CREATE (PROCEDURE|FUNCTION) (\S+) \((.*)\);?$/s),n=t?ka(t[3]):null;if(t&&n.every((r)=>ha.test(r.trim())))return{name:t[2],objectType:Hr(t[2],t[1]==="FUNCTION"),count:n.length,parameters:n.map((r)=>ha.exec(r.trim())[1])};let o=ba(e);if(!o)throw Error(`Cannot parse proc DDL: ${e.slice(0,120)}`);let s=/^CREATE\s+FUNCTION/i.test(e);return{name:o,objectType:qr(e)??Hr(o,s),count:0,parameters:[],opaque:!0}}import{mkdir as $p,stat as Sp}from"node:fs/promises";import{basename as jp,dirname as Ip,join as Gr,resolve as Sa,sep as Cp}from"node:path";import{mkdir as gp,readdir as wp}from"node:fs/promises";import{join as nn,sep as yp}from"node:path";var on="_publishState.json";function ke(e){return e.split(yp).join("/")}function _t(e){return`sha1:${Qt("sha1",e.replace(/\r\n/g,`
59
+ `))}`}async function To(e){try{let t=JSON.parse(await G(nn(e,on)));return t&&typeof t==="object"&&!Array.isArray(t)?t:{}}catch{return{}}}async function va(e,t){await gp(e,{recursive:!0}),await L(nn(e,on),JSON.stringify(t,null,2))}async function Ee(e,t){if(t.length===0)return;let n=await To(e),o=!1;for(let s of t){let r=ke(s);try{n[r]=_t(await G(nn(e,r))),o=!0}catch{if(r in n)delete n[r],o=!0}}if(o)await va(e,n)}async function $a(e,t){if(t.length===0)return;let n=await To(e),o=!1;for(let s of t){let r=ke(s);if(r in n)delete n[r],o=!0}if(o)await va(e,n)}var bp=["module.json","header.html","body.html","script.js","style.css","project.json"];async function kp(e){try{return(await wp(nn(e,"components"),{withFileTypes:!0})).filter((n)=>n.isFile()&&n.name.toLowerCase().endsWith(".vue")).map((n)=>ke(nn("components",n.name)))}catch{return[]}}async function Jr(e){return[...bp,...await kp(e)]}async function Vn(e){let t=await To(e),n=[];for(let o of await Jr(e)){let s;try{s=await G(nn(e,o))}catch{continue}let r=t[o];if(r===void 0)n.push({relPath:o,reason:"new"});else if(r!==_t(s))n.push({relPath:o,reason:"changed"})}return n}async function rn(e,t,n){let o=(await To(e))[ke(t)];return o!==void 0&&_t(n)===o}var vp={"module.json":"fui module publish json","header.html":"fui module publish html","body.html":"fui module publish html","script.js":"fui module publish script","style.css":"fui module publish css"};function Po(e){if(e.startsWith("components/"))return"fui component update";return vp[e]??"fui module publish json"}function Kr(e){if(e.length===0)return null;return`unpublished local files: ${e.map((n)=>`${n.relPath} (${Po(n.relPath)})`).join(", ")}`}var Rp=172800000,Tp=new Set(["_moduleInfo.json","_projectInfo.json","_modules.json","_components.json","_imports.json",on]);function Eo(e){return Tp.has(jp(e))}function Pp(e,t){return e.replace(/\r\n/g,`
60
60
  `)===t.replace(/\r\n/g,`
61
- `)}function Sp(e,t,n){let o=ya(e),s=ya(t);if(s!==o&&!s.startsWith(o+bp))throw Error(`server file path escapes the target folder: ${JSON.stringify(n)} -> ${s} (must be inside ${o}); nothing written`)}async function Ne(e,t,n){let o=[],s=[],r=[],i=ko(),a=[];for(let c of Object.keys(n)){let l=ke(c);Sp(t,Br(t,l),l)}for(let[c,l]of Object.entries(n)){let u=ke(c),d=Br(t,u),p=await he(d);if(p!==null&&$p(p,l))s.push(u);else if(p!==null&&!ba(u)){let h=await fe(e,d,{stamp:i});if(h)r.push({relPath:u,historyPath:Nt(e,h)})}if(await fp(yp(d),{recursive:!0}),await F(d,l),o.push(d),!ba(u))a.push(u)}if(a.length>0)await Pe(t,a).catch(()=>{return});return{written:o,unchanged:s,archived:r}}function Jr(e){let t=e.written.length-e.unchanged.length,n=[`wrote ${e.written.length}`,`changed ${t}`,`unchanged ${e.unchanged.length}`];if(e.archived.length>0)n.push(`archived ${e.archived.length}`);return n.join(", ")}var jp=8;function Kr(e){if(e.length===0)return[];let t=e.slice(0,jp).map((o)=>o.relPath),n=e.length-t.length;return[`archived ${e.length} local file(s) that differed from the server: ${t.join(", ")}${n>0?` (+${n} more)`:""}`,`restore by copying back from ${e[0].historyPath}`]}async function Ip(e,t){try{return(await gp(Br(e,t))).mtime}catch{return null}}async function Mt(e,t,n,o,s=Date.now()){let r=await Ip(e,o);if(!r)return null;let i=s-r.getTime();if(i<kp)return null;let a=Math.floor(i/3600000),c=a>=48?`${Math.floor(a/24)} days`:`${a} hours`;return`warning: ${t} last synced ${c} ago; the server may have changed. run: ${n}`}var Np=/^[A-Za-z0-9_]+$/,ka=50;class Eo extends Error{name="SchemaFormatError"}async function Ap(e){try{return JSON.parse(await G(e))}catch{return null}}async function Gr(e,t,n){return Ap(e.schemaFile(t,n))}async function zr(e,t,n){let o=await Gr(e,t,n);if(!o)return null;if(!Ot(o))throw new Eo(`_db/${n}/schema.json of project "${t}" is not in the current compact format`);try{return{fetchedAt:o.fetchedAt,tables:o.tables.map(Wr),procedures:o.procedures.map(Co)}}catch(s){throw new Eo(`_db/${n}/schema.json of project "${t}": ${s.message}`)}}async function va(e){try{return(await Rp(e)).filter((t)=>t.toLowerCase().endsWith(".sql"))}catch{return null}}async function Dp(e,t){try{let n=await G(t);return await fe(e,t,{content:n,suffix:"deleted"}),await Tp(t),!0}catch{return!1}}async function xp(e,t,n,o,s,r){let i=[],a=[],c=await va(e.connDir(t,n));if(!c)return{removed:i,localOnly:a};let l=(p)=>new Set([...p].map((h)=>h.toLowerCase())),u=l(o),d=l(s);for(let p of c){let h=p.slice(0,-4).toLowerCase();if(d.has(h))continue;if(u.has(h)||r){if(await Dp(e,Pp(e.connDir(t,n),p)))i.push(p)}else a.push(p)}return{removed:i,localOnly:a}}async function Op(e,t,n,o){let s=await va(e.connDir(n,o));if(!s||s.length===0)return{sync:null,skipped:[],missing:[]};let r=[],i=[];for(let d of s){let p=d.slice(0,-4);if(Np.test(p))r.push(p);else i.push(d)}let a=new Map;for(let d=0;d<r.length;d+=ka){let p=r.slice(d,d+ka).map((w)=>`N'${w}'`).join(","),h=await t.query(`SELECT o.name AS name, ISNULL(OBJECT_DEFINITION(o.object_id), '') AS sqlText
62
- FROM sys.objects o WHERE o.name IN (${p})`);for(let w of h){let y=String(w.name??""),v=String(w.sqlText??"");if(y&&v.trim())a.set(y.toLowerCase(),v)}}let c={},l=[],u=new Set(i);for(let d of s){if(u.has(d))continue;let p=a.get(d.slice(0,-4).toLowerCase());if(p===void 0)l.push(d);else c[`${o}/${d}`]=p}if(Object.keys(c).length===0)return{sync:null,skipped:i,missing:l};return{sync:await Ne(e,e.dbDir(n),c),skipped:i,missing:l}}async function Po(e,t,n,o,s=!1){let r=null;try{r=await zr(e,n,o)}catch{}let i=new Set((r?.procedures??[]).map((w)=>w.name)),[a,c]=await Promise.all([t.query(ua),t.query(da)]),l=ma(a,c),u=e.schemaFile(n,o);await Cp(Ep(u),{recursive:!0}),await F(u,JSON.stringify(l,null,2));let d=new Set(l.procedures.map((w)=>Co(w).name)),p=await xp(e,n,o,i,d,s),h=await Op(e,t,n,o);return{schemaFile:u,schema:l,refresh:h.sync,skipped:h.skipped,missingOnServer:h.missing,removed:p.removed,localOnly:p.localOnly}}async function No(e,t,n,o){try{return await zr(e,t,n)}catch(s){if(!(s instanceof Eo))throw s;return await o(),zr(e,t,n)}}async function $a(e,t,n,o){let s=await Gr(e,t,n);if(!s)return null;if(Ot(s))return s;await o();let r=await Gr(e,t,n);if(!Ot(r))throw Error("refetched the schema but it is still not in the compact format");return r}function Sa(e){let t={Table:0,View:0,API:0,"API File":0,Procedure:0,Function:0};for(let n of e.tables)t[Wr(n).objectType]+=1;for(let n of e.procedures)t[Co(n).objectType]+=1;return t}var _p=/token|passw|pwd|secret|api[-_]?key|credential|authorization/i,ja=[/\b(?:PWD|Password)\s*=\s*[^;]+/i,/^Bearer\s+\S+/i,/:\/\/[^/\s:@]+:[^@\s]+@/];function Ia(e){return`<redacted ${e.length} chars>`}function Ca(e){if(ja.some((t)=>t.test(e)))return!0;if(e.length<40||!/^[A-Za-z0-9+/=]+$/.test(e))return!1;try{return ja.some((t)=>t.test(atob(e)))}catch{return!1}}function sn(e){if(Array.isArray(e))return e.map((t)=>sn(t));if(e&&typeof e==="object")return Object.fromEntries(Object.entries(e).map(([t,n])=>[t,typeof n==="string"&&(_p.test(t)||Ca(n))?Ia(n):sn(n)]));if(typeof e==="string"&&Ca(e))return Ia(e);return e}function Ue(e,t={}){let n=t.strings!==!1,o=e.split(""),s=0;while(s<e.length)if(e[s]==="-"&&e[s+1]==="-")while(s<e.length&&e[s]!==`
63
- `)o[s++]=" ";else if(e[s]==="/"&&e[s+1]==="*"){let r=e.indexOf("*/",s+2),i=r===-1?e.length:r+2;while(s<i)o[s++]=" "}else if(e[s]==="'"){if(n)o[s]=" ";s++;while(s<e.length){if(e[s]==="'"){if(n)o[s]=" ";if(s++,e[s]==="'"){if(n)o[s]=" ";s++;continue}break}if(n)o[s]=" ";s++}}else s++;return o.join("")}function Ft(e){return e.replace(/--[^\n]*/g," ").replace(/\/\*[\s\S]*?\*\//g," ").replace(/N?'(?:[^']|'')*'/gi,"''")}function Xr(e){return Ue(e,{strings:!1})}function Yr(e){let t=Ue(e),n=[],o=0;for(let s of t.matchAll(/;|^[ \t]*GO[ \t]*$/gim))n.push({start:o,end:s.index}),o=s.index+s[0].length;return n.push({start:o,end:e.length}),n.filter((s)=>s.end>s.start)}function Wn(e,t=","){let n=[],o=0,s="";for(let r of e){if(r==="(")o++;else if(r===")")o--;if(r===t&&o===0)n.push(s),s="";else s+=r}if(s.trim())n.push(s);return n}function Lt(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 Qr(e){let t=Ue(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 Ra(e){return/\b(?:CREATE|ALTER)\s+FUNCTION\b/i.test(Ue(e))}function Ta(e,t){let n={sql:e,changed:!1,from:null,to:null};if(t===null)return n;let o=[...Ue(e).matchAll(/\b(CREATE|ALTER)(\s+)(?:PROCEDURE|PROC|FUNCTION)\b/gi)];if(o.length!==1)return n;let s=o[0],r=s[1].toUpperCase(),i=t?"ALTER":"CREATE";if(r===i)return n;return{sql:e.slice(0,s.index)+i+e.slice(s.index+s[1].length),changed:!0,from:r,to:i}}function Zr(e){let t=Ue(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,s=n[0].endsWith("(")?1:0,r=t.length,i=/\bAS\b/gi;i.lastIndex=o;for(let d=i.exec(t);d;d=i.exec(t)){let p=t.slice(o,d.index);if((p.match(/\(/g)??[]).length+s===(p.match(/\)/g)??[]).length){r=d.index;break}}let a=(d)=>d.replace(/^\(/,"").replace(/\)\s*$/,""),c=Wn(a(t.slice(o,r))),l=Wn(a(e.slice(o,r))),u=[];return c.forEach((d,p)=>{if(/^\s*@\w+[\s\S]*=/.test(d))u.push((l[p]??d).trim())}),u}function Mp(e){return`N'${e.replace(/'/g,"''")}'`}function qn(e){return`GRANT EXECUTE ON [dbo].[${e}] TO [public] AS [dbo]`}function Ea(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(${Mp(e)}) AND p.permission_name = 'EXECUTE' AND p.state IN ('G','W') AND u.name = 'public'`}var Fp=/\bALTER\s+TABLE\s+(?:(?:\[[^\]]*\]|\w+)\s*\.\s*)*(?:\[[^\]]*\]|\w+)\s+(?:WITH\s+(?:CHECK|NOCHECK)\s+)?(\w+)/i,Lp=new Set(["ADD","ALTER","DROP"]);function Up(e){let t=Xr(e),n=null;for(let o of Yr(e)){let s=t.slice(o.start,o.end),r=e.slice(o.start,o.end).trim();if(/\bDROP\s+TABLE\b/i.test(s))return{severity:"block",reason:"DROP TABLE",statement:r};if(!/\bALTER\s+TABLE\b/i.test(s))continue;if(/\bDROP\s+COLUMN\b/i.test(s))return{severity:"block",reason:"ALTER TABLE ... DROP COLUMN",statement:r};if(/\bSWITCH\b/i.test(s))return{severity:"block",reason:"ALTER TABLE ... SWITCH",statement:r};let i=Fp.exec(s)?.[1]?.toUpperCase();if(!i||!Lp.has(i))return{severity:"block",reason:`ALTER TABLE ... ${i??"(unknown action)"}`,statement:r};if(!n&&/\bALTER\s+COLUMN\b/i.test(s))n={severity:"confirm",reason:"ALTER TABLE ... ALTER COLUMN (narrowing numeric/date precision loses data silently)",statement:r}}return n}function Pa(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 Hp(e){let t=Xr(e);for(let{start:n,end:o}of Yr(e)){let s=t.slice(n,o);if(/\bWHERE\b/i.test(s))continue;let r=e.slice(n,o).trim();if(/\bDELETE\b/i.test(s)&&!Pa(s,"DELETE"))return{severity:"block",reason:"DELETE without WHERE",statement:r};if(/\bUPDATE\b/i.test(s)&&/\bSET\b/i.test(s)&&!Pa(s,"UPDATE"))return{severity:"block",reason:"UPDATE without WHERE",statement:r}}return null}function Na(e,t){if(/\bINSERT\b/i.test(e))t.add("INSERT");if(/\bUPDATE\b/i.test(e)&&/\bSET\b/i.test(e))t.add("UPDATE");if(/\bDELETE\b/i.test(e))t.add("DELETE");if(/\bMERGE\b/i.test(e))t.add("MERGE");if(/\bTRUNCATE\b/i.test(e))t.add("TRUNCATE")}var Vp=["INSERT","UPDATE","DELETE","MERGE","TRUNCATE"];function an(e){let t=new Set;return Na(Ft(e),t),Na(Ue(e),t),Vp.filter((n)=>t.has(n))}function es(e,t,n){let o=Up(e);if(o&&(o.severity==="block"||!t))return o;if(n==="nonquery"){let r=Hp(e);if(r)return r}let s=an(e);if(s.length>0&&!t)return{severity:"confirm",reason:`writes real data (${s.join(", ")})`,statement:e.trim()};return null}function Wp(e){let t=e.trim(),n=t.match(/\bAS\s+\[?(\w+)\]?\s*$/i);if(n)return n[1];let o=t.match(/^(.+?)\s+\[?(\w+)\]?$/);if(o){let i=o[1].trim();if(!/[+\-*/,(]$/.test(i)&&!/^(AND|OR|ON|WHEN|THEN|ELSE)$/i.test(i))return o[2]}let s=t.match(/\.\s*\[?(\w+)\]?\s*$/);if(s)return s[1];let r=t.match(/^\[?(\w+)\]?$/);if(r)return r[1];return t.length>30?`${t.slice(0,30)}…`:t}function ts(e){let t=[],n=0,o=e.length;for(let s=0;s<o;s++){let r=e[s];if(r==="("){n++;continue}if(r===")"){n--;continue}if(n!==0)continue;if(r!=="S"&&r!=="s")continue;if(s>0&&/[A-Za-z0-9_]/.test(e[s-1]))continue;if(!/^SELECT\b/i.test(e.slice(s,s+7)))continue;let i=0,a=o;for(let y=s+6;y<o;y++){let v=e[y];if(v==="("){i++;continue}if(v===")"){i--;continue}if(i!==0)continue;if(v===";"){a=y;break}if(/[A-Za-z]/.test(v)&&(y===0||!/[A-Za-z0-9_]/.test(e[y-1]))){let N=e.slice(y).match(/^[A-Za-z]+/)?.[0]?.toUpperCase();if(N&&["INSERT","UPDATE","DELETE","MERGE","SELECT"].includes(N)){a=y;break}}}let c=e.slice(s,a),l=/\bINTO\s+[#@[\w]/i.test(c),u=0,d=-1;for(let y=6;y<c.length;y++){let v=c[y];if(v==="("){u++;continue}if(v===")"){u--;continue}if(u!==0)continue;if(/^FROM\b/i.test(c.slice(y))&&(y===0||!/[A-Za-z0-9_]/.test(c[y-1]))){d=y;break}}let p=(d>=0?c.slice(6,d):c.slice(6)).trim();p=p.replace(/^(TOP\s*\(?\s*\d+\s*\)?\s*(PERCENT)?\s*)/i,"").replace(/^DISTINCT\s+/i,"").trim();let h=/(^|,)\s*\*\s*($|,)/.test(p)||p==="*",w=null;if(!h&&p)w=Wn(p,",").map(Wp).filter(Boolean);t.push({columns:w,hasStar:h,isInto:l})}return t}function Aa(e){return/RAISERROR\s*\([^)]*\)[^;]*;?\s*(\r?\n)?\s*RETURN\b/i.test(e)}function ns(e){let t=e.toLowerCase();if(/(^|_)id$/.test(t)||t==="id")return 1;if(/date|time/.test(t))return"2026-01-01";if(/(count|qty|quantity|amount|price|total|sum|so[_ ]?luong)/.test(t))return 0;if(/(code|ma[_ ]?)/.test(t))return"C001";return`Sample ${e}`}function Da(e){let t=ts(Ft(e)).filter((o)=>!o.isInto)[0];if(!t||t.hasStar||!t.columns||t.columns.length===0)return{data:[]};let n={};for(let o of t.columns)n[o]=ns(o);return{data:[n,{...n}]}}function qp(e){let t=Ft(e),n=[];if(/\b(?:EXEC|EXECUTE)\s*\(/i.test(t)||/\bsp_executesql\b/i.test(t))n.push("dynamic SQL (EXEC(...)/sp_executesql)");if(/\b(?:EXEC|EXECUTE)\s+(?:@\w+\s*=\s*)?[A-Za-z_#[]/i.test(t))n.push("calls another SP (EXEC <proc>)");if(/\bINTO\s+(?!#|@)[A-Za-z_[]/i.test(t)&&/\bSELECT\b/i.test(t)&&!/\bINSERT\b/i.test(t))n.push("SELECT ... INTO a real table");return n}function xa(e){if(e===null)return"unknown";if(an(e).length>0)return"unsafe-write";if(qp(e).length>0)return"opaque";return"safe-read"}var zp=/^[A-Za-z0-9_]+$/,Oa=200,_a=2000;function bt(e){if(!zp.test(e))throw new m(`invalid object name "${e}": letters, digits and "_" only (it becomes _db/<alias>/<name>.sql)`);return e}function Ie(e,t){return{project:e.project,contextProject:e.contextProject,...t}}function lt(e,t){if(e.engine!=="mssql")throw new m(`${t} supports MSSQL connections only; ${e.alias} is ${e.engine}`)}function Ma(e){let t=e.statement.length>400?`${e.statement.slice(0,400)}...`: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 Se(`${e.reason}
61
+ `)}function Ep(e,t,n){let o=Sa(e),s=Sa(t);if(s!==o&&!s.startsWith(o+Cp))throw Error(`server file path escapes the target folder: ${JSON.stringify(n)} -> ${s} (must be inside ${o}); nothing written`)}async function Ae(e,t,n){let o=[],s=[],r=[],i=vo(),a=[];for(let c of Object.keys(n)){let l=ke(c);Ep(t,Gr(t,l),l)}for(let[c,l]of Object.entries(n)){let u=ke(c),d=Gr(t,u),p=await he(d);if(p!==null&&Pp(p,l))s.push(u);else if(p!==null&&!Eo(u)){let m=await fe(e,d,{stamp:i});if(m)r.push({relPath:u,historyPath:At(e,m)})}if(await $p(Ip(d),{recursive:!0}),await L(d,l),o.push(d),!Eo(u))a.push(u)}if(a.length>0)await Ee(t,a).catch(()=>{return});return{written:o,unchanged:s,archived:r}}function zr(e){let t=e.written.length-e.unchanged.length,n=[`wrote ${e.written.length}`,`changed ${t}`,`unchanged ${e.unchanged.length}`];if(e.archived.length>0)n.push(`archived ${e.archived.length}`);return n.join(", ")}var Ap=8;function Xr(e){if(e.length===0)return[];let t=e.slice(0,Ap).map((o)=>o.relPath),n=e.length-t.length;return[`archived ${e.length} local file(s) that differed from the server: ${t.join(", ")}${n>0?` (+${n} more)`:""}`,`restore by copying back from ${e[0].historyPath}`]}async function Np(e,t){try{return(await Sp(Gr(e,t))).mtime}catch{return null}}async function Mt(e,t,n,o,s=Date.now()){let r=await Np(e,o);if(!r)return null;let i=s-r.getTime();if(i<Rp)return null;let a=Math.floor(i/3600000),c=a>=48?`${Math.floor(a/24)} days`:`${a} hours`;return`warning: ${t} last synced ${c} ago; the server may have changed. run: ${n}`}var Lp=/^[A-Za-z0-9_]+$/,ja=50;class Ao extends Error{name="SchemaFormatError"}async function Fp(e){try{return JSON.parse(await G(e))}catch{return null}}async function Yr(e,t,n){return Fp(e.schemaFile(t,n))}async function Qr(e,t,n){let o=await Yr(e,t,n);if(!o)return null;if(!Ot(o))throw new Ao(`_db/${n}/schema.json of project "${t}" is not in the current compact format`);try{return{fetchedAt:o.fetchedAt,tables:o.tables.map(Br),procedures:o.procedures.map(Ro)}}catch(s){throw new Ao(`_db/${n}/schema.json of project "${t}": ${s.message}`)}}async function Ia(e){try{return(await xp(e)).filter((t)=>t.toLowerCase().endsWith(".sql"))}catch{return null}}async function Up(e,t){try{let n=await G(t);return await fe(e,t,{content:n,suffix:"deleted"}),await Op(t),!0}catch{return!1}}async function Hp(e,t,n,o,s,r){let i=[],a=[],c=await Ia(e.connDir(t,n));if(!c)return{removed:i,localOnly:a};let l=(p)=>new Set([...p].map((m)=>m.toLowerCase())),u=l(o),d=l(s);for(let p of c){let m=p.slice(0,-4).toLowerCase();if(d.has(m))continue;if(u.has(m)||r){if(await Up(e,Mp(e.connDir(t,n),p)))i.push(p)}else a.push(p)}return{removed:i,localOnly:a}}async function Vp(e,t,n,o){let s=await Ia(e.connDir(n,o));if(!s||s.length===0)return{sync:null,skipped:[],missing:[]};let r=[],i=[];for(let d of s){let p=d.slice(0,-4);if(Lp.test(p))r.push(p);else i.push(d)}let a=new Map;for(let d=0;d<r.length;d+=ja){let p=r.slice(d,d+ja).map((w)=>`N'${w}'`).join(","),m=await t.query(`SELECT o.name AS name, ISNULL(OBJECT_DEFINITION(o.object_id), '') AS sqlText
62
+ FROM sys.objects o WHERE o.name IN (${p})`);for(let w of m){let y=String(w.name??""),v=String(w.sqlText??"");if(y&&v.trim())a.set(y.toLowerCase(),v)}}let c={},l=[],u=new Set(i);for(let d of s){if(u.has(d))continue;let p=a.get(d.slice(0,-4).toLowerCase());if(p===void 0)l.push(d);else c[`${o}/${d}`]=p}if(Object.keys(c).length===0)return{sync:null,skipped:i,missing:l};return{sync:await Ae(e,e.dbDir(n),c),skipped:i,missing:l}}async function No(e,t,n,o,s=!1){let r=null;try{r=await Qr(e,n,o)}catch{}let i=new Set((r?.procedures??[]).map((w)=>w.name)),[a,c]=await Promise.all([t.query(fa),t.query(ga)]),l=ya(a,c),u=e.schemaFile(n,o);await Dp(_p(u),{recursive:!0}),await L(u,JSON.stringify(l,null,2));let d=new Set(l.procedures.map((w)=>Ro(w).name)),p=await Hp(e,n,o,i,d,s),m=await Vp(e,t,n,o);return{schemaFile:u,schema:l,refresh:m.sync,skipped:m.skipped,missingOnServer:m.missing,removed:p.removed,localOnly:p.localOnly}}async function Do(e,t,n,o){try{return await Qr(e,t,n)}catch(s){if(!(s instanceof Ao))throw s;return await o(),Qr(e,t,n)}}async function Ca(e,t,n,o){let s=await Yr(e,t,n);if(!s)return null;if(Ot(s))return s;await o();let r=await Yr(e,t,n);if(!Ot(r))throw Error("refetched the schema but it is still not in the compact format");return r}function Ra(e){let t={Table:0,View:0,API:0,"API File":0,Procedure:0,Function:0};for(let n of e.tables)t[Br(n).objectType]+=1;for(let n of e.procedures)t[Ro(n).objectType]+=1;return t}var Wp=/token|passw|pwd|secret|api[-_]?key|credential|authorization/i,Ta=[/\b(?:PWD|Password)\s*=\s*[^;]+/i,/^Bearer\s+\S+/i,/:\/\/[^/\s:@]+:[^@\s]+@/];function Pa(e){return`<redacted ${e.length} chars>`}function Ea(e){if(Ta.some((t)=>t.test(e)))return!0;if(e.length<40||!/^[A-Za-z0-9+/=]+$/.test(e))return!1;try{return Ta.some((t)=>t.test(atob(e)))}catch{return!1}}function sn(e){if(Array.isArray(e))return e.map((t)=>sn(t));if(e&&typeof e==="object")return Object.fromEntries(Object.entries(e).map(([t,n])=>[t,typeof n==="string"&&(Wp.test(t)||Ea(n))?Pa(n):sn(n)]));if(typeof e==="string"&&Ea(e))return Pa(e);return e}function Ue(e,t={}){let n=t.strings!==!1,o=e.split(""),s=0;while(s<e.length)if(e[s]==="-"&&e[s+1]==="-")while(s<e.length&&e[s]!==`
63
+ `)o[s++]=" ";else if(e[s]==="/"&&e[s+1]==="*"){let r=e.indexOf("*/",s+2),i=r===-1?e.length:r+2;while(s<i)o[s++]=" "}else if(e[s]==="'"){if(n)o[s]=" ";s++;while(s<e.length){if(e[s]==="'"){if(n)o[s]=" ";if(s++,e[s]==="'"){if(n)o[s]=" ";s++;continue}break}if(n)o[s]=" ";s++}}else s++;return o.join("")}function Lt(e){return e.replace(/--[^\n]*/g," ").replace(/\/\*[\s\S]*?\*\//g," ").replace(/N?'(?:[^']|'')*'/gi,"''")}function Zr(e){return Ue(e,{strings:!1})}function es(e){let t=Ue(e),n=[],o=0;for(let s of t.matchAll(/;|^[ \t]*GO[ \t]*$/gim))n.push({start:o,end:s.index}),o=s.index+s[0].length;return n.push({start:o,end:e.length}),n.filter((s)=>s.end>s.start)}function Wn(e,t=","){let n=[],o=0,s="";for(let r of e){if(r==="(")o++;else if(r===")")o--;if(r===t&&o===0)n.push(s),s="";else s+=r}if(s.trim())n.push(s);return n}function Ft(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 ts(e){let t=Ue(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 Aa(e){return/\b(?:CREATE|ALTER)\s+FUNCTION\b/i.test(Ue(e))}function Na(e,t){let n={sql:e,changed:!1,from:null,to:null};if(t===null)return n;let o=[...Ue(e).matchAll(/\b(CREATE|ALTER)(\s+)(?:PROCEDURE|PROC|FUNCTION)\b/gi)];if(o.length!==1)return n;let s=o[0],r=s[1].toUpperCase(),i=t?"ALTER":"CREATE";if(r===i)return n;return{sql:e.slice(0,s.index)+i+e.slice(s.index+s[1].length),changed:!0,from:r,to:i}}function ns(e){let t=Ue(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,s=n[0].endsWith("(")?1:0,r=t.length,i=/\bAS\b/gi;i.lastIndex=o;for(let d=i.exec(t);d;d=i.exec(t)){let p=t.slice(o,d.index);if((p.match(/\(/g)??[]).length+s===(p.match(/\)/g)??[]).length){r=d.index;break}}let a=(d)=>d.replace(/^\(/,"").replace(/\)\s*$/,""),c=Wn(a(t.slice(o,r))),l=Wn(a(e.slice(o,r))),u=[];return c.forEach((d,p)=>{if(/^\s*@\w+[\s\S]*=/.test(d))u.push((l[p]??d).trim())}),u}function qp(e){return`N'${e.replace(/'/g,"''")}'`}function qn(e){return`GRANT EXECUTE ON [dbo].[${e}] TO [public] AS [dbo]`}function Da(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(${qp(e)}) AND p.permission_name = 'EXECUTE' AND p.state IN ('G','W') AND u.name = 'public'`}var Bp=/\bALTER\s+TABLE\s+(?:(?:\[[^\]]*\]|\w+)\s*\.\s*)*(?:\[[^\]]*\]|\w+)\s+(?:WITH\s+(?:CHECK|NOCHECK)\s+)?(\w+)/i,Jp=new Set(["ADD","ALTER","DROP"]);function Kp(e){let t=Zr(e),n=null;for(let o of es(e)){let s=t.slice(o.start,o.end),r=e.slice(o.start,o.end).trim();if(/\bDROP\s+TABLE\b/i.test(s))return{severity:"block",reason:"DROP TABLE",statement:r};if(!/\bALTER\s+TABLE\b/i.test(s))continue;if(/\bDROP\s+COLUMN\b/i.test(s))return{severity:"block",reason:"ALTER TABLE ... DROP COLUMN",statement:r};if(/\bSWITCH\b/i.test(s))return{severity:"block",reason:"ALTER TABLE ... SWITCH",statement:r};let i=Bp.exec(s)?.[1]?.toUpperCase();if(!i||!Jp.has(i))return{severity:"block",reason:`ALTER TABLE ... ${i??"(unknown action)"}`,statement:r};if(!n&&/\bALTER\s+COLUMN\b/i.test(s))n={severity:"confirm",reason:"ALTER TABLE ... ALTER COLUMN (narrowing numeric/date precision loses data silently)",statement:r}}return n}function xa(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 Gp(e){let t=Zr(e);for(let{start:n,end:o}of es(e)){let s=t.slice(n,o);if(/\bWHERE\b/i.test(s))continue;let r=e.slice(n,o).trim();if(/\bDELETE\b/i.test(s)&&!xa(s,"DELETE"))return{severity:"block",reason:"DELETE without WHERE",statement:r};if(/\bUPDATE\b/i.test(s)&&/\bSET\b/i.test(s)&&!xa(s,"UPDATE"))return{severity:"block",reason:"UPDATE without WHERE",statement:r}}return null}function Oa(e,t){if(/\bINSERT\b/i.test(e))t.add("INSERT");if(/\bUPDATE\b/i.test(e)&&/\bSET\b/i.test(e))t.add("UPDATE");if(/\bDELETE\b/i.test(e))t.add("DELETE");if(/\bMERGE\b/i.test(e))t.add("MERGE");if(/\bTRUNCATE\b/i.test(e))t.add("TRUNCATE")}var zp=["INSERT","UPDATE","DELETE","MERGE","TRUNCATE"];function an(e){let t=new Set;return Oa(Lt(e),t),Oa(Ue(e),t),zp.filter((n)=>t.has(n))}function os(e,t,n){let o=Kp(e);if(o&&(o.severity==="block"||!t))return o;if(n==="nonquery"){let r=Gp(e);if(r)return r}let s=an(e);if(s.length>0&&!t)return{severity:"confirm",reason:`writes real data (${s.join(", ")})`,statement:e.trim()};return null}function Xp(e){let t=e.trim(),n=t.match(/\bAS\s+\[?(\w+)\]?\s*$/i);if(n)return n[1];let o=t.match(/^(.+?)\s+\[?(\w+)\]?$/);if(o){let i=o[1].trim();if(!/[+\-*/,(]$/.test(i)&&!/^(AND|OR|ON|WHEN|THEN|ELSE)$/i.test(i))return o[2]}let s=t.match(/\.\s*\[?(\w+)\]?\s*$/);if(s)return s[1];let r=t.match(/^\[?(\w+)\]?$/);if(r)return r[1];return t.length>30?`${t.slice(0,30)}…`:t}function rs(e){let t=[],n=0,o=e.length;for(let s=0;s<o;s++){let r=e[s];if(r==="("){n++;continue}if(r===")"){n--;continue}if(n!==0)continue;if(r!=="S"&&r!=="s")continue;if(s>0&&/[A-Za-z0-9_]/.test(e[s-1]))continue;if(!/^SELECT\b/i.test(e.slice(s,s+7)))continue;let i=0,a=o;for(let y=s+6;y<o;y++){let v=e[y];if(v==="("){i++;continue}if(v===")"){i--;continue}if(i!==0)continue;if(v===";"){a=y;break}if(/[A-Za-z]/.test(v)&&(y===0||!/[A-Za-z0-9_]/.test(e[y-1]))){let A=e.slice(y).match(/^[A-Za-z]+/)?.[0]?.toUpperCase();if(A&&["INSERT","UPDATE","DELETE","MERGE","SELECT"].includes(A)){a=y;break}}}let c=e.slice(s,a),l=/\bINTO\s+[#@[\w]/i.test(c),u=0,d=-1;for(let y=6;y<c.length;y++){let v=c[y];if(v==="("){u++;continue}if(v===")"){u--;continue}if(u!==0)continue;if(/^FROM\b/i.test(c.slice(y))&&(y===0||!/[A-Za-z0-9_]/.test(c[y-1]))){d=y;break}}let p=(d>=0?c.slice(6,d):c.slice(6)).trim();p=p.replace(/^(TOP\s*\(?\s*\d+\s*\)?\s*(PERCENT)?\s*)/i,"").replace(/^DISTINCT\s+/i,"").trim();let m=/(^|,)\s*\*\s*($|,)/.test(p)||p==="*",w=null;if(!m&&p)w=Wn(p,",").map(Xp).filter(Boolean);t.push({columns:w,hasStar:m,isInto:l})}return t}function _a(e){return/RAISERROR\s*\([^)]*\)[^;]*;?\s*(\r?\n)?\s*RETURN\b/i.test(e)}function ss(e){let t=e.toLowerCase();if(/(^|_)id$/.test(t)||t==="id")return 1;if(/date|time/.test(t))return"2026-01-01";if(/(count|qty|quantity|amount|price|total|sum|so[_ ]?luong)/.test(t))return 0;if(/(code|ma[_ ]?)/.test(t))return"C001";return`Sample ${e}`}function Ma(e){let t=rs(Lt(e)).filter((o)=>!o.isInto)[0];if(!t||t.hasStar||!t.columns||t.columns.length===0)return{data:[]};let n={};for(let o of t.columns)n[o]=ss(o);return{data:[n,{...n}]}}function Yp(e){let t=Lt(e),n=[];if(/\b(?:EXEC|EXECUTE)\s*\(/i.test(t)||/\bsp_executesql\b/i.test(t))n.push("dynamic SQL (EXEC(...)/sp_executesql)");if(/\b(?:EXEC|EXECUTE)\s+(?:@\w+\s*=\s*)?[A-Za-z_#[]/i.test(t))n.push("calls another SP (EXEC <proc>)");if(/\bINTO\s+(?!#|@)[A-Za-z_[]/i.test(t)&&/\bSELECT\b/i.test(t)&&!/\bINSERT\b/i.test(t))n.push("SELECT ... INTO a real table");return n}function La(e){if(e===null)return"unknown";if(an(e).length>0)return"unsafe-write";if(Yp(e).length>0)return"opaque";return"safe-read"}var nm=/^[A-Za-z0-9_]+$/,Fa=200,Ua=2000;function bt(e){if(!nm.test(e))throw new h(`invalid object name "${e}": letters, digits and "_" only (it becomes _db/<alias>/<name>.sql)`);return e}function Ie(e,t){return{project:e.project,contextProject:e.contextProject,...t}}function lt(e,t){if(e.engine!=="mssql")throw new h(`${t} supports MSSQL connections only; ${e.alias} is ${e.engine}`)}function Ha(e){let t=e.statement.length>400?`${e.statement.slice(0,400)}...`: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 Se(`${e.reason}
64
64
  ${n}
65
65
  ---
66
- ${t}`,e.severity==="block"?"hard":"soft")}function Fa(e,t){let n=Sa(t.schema),o=[`tables ${n.Table} views ${n.View} api ${n.API} api-file ${n["API File"]} procedures ${n.Procedure} functions ${n.Function}`],s=t.refresh;if(s)o.push(`sql bodies in _db/${e}/: wrote ${s.written.length}, unchanged ${s.unchanged.length}, archived ${s.archived.length}`);if(t.skipped.length>0)o.push(`not refreshed (file name is not a SQL identifier): ${t.skipped.join(" ")}`);if(t.removed.length>0)o.push(`archived to _history (dropped on server): ${t.removed.join(" ")}`);if(t.localOnly.length>0)o.push(`local only, not on server: ${t.localOnly.join(" ")}; undeployed draft -> keep or deploy; stale -> fui schema pull --db ${e} --prune-local-only`);return o}async function Ao(e,t,n){yt(n.alias);let{engine:o}=Mn(n.dbToken);if(o==="mssql"&&!n.apiDomain)throw new m("MSSQL connections need --api-domain <host>");let s=n.apiDomain?Le(n.apiDomain):void 0,r={projectId:t,alias:n.alias,config:{apiDomain:s,dbToken:n.dbToken},engine:o,dbToken:n.dbToken,note:null};if(n.test)await je(r,(y)=>y.query("SELECT 1 AS ok"));let i={...s?{apiDomain:s}:{},...n.sqlServer?{sqlServer:n.sqlServer}:{},...n.database?{database:n.database}:{},dbToken:n.dbToken},a=await e.store.load(t),c=Dt(i),l=c?Pr(a,c.sqlServer,c.database):null,u=n.userToken??(l?a.connections[l].userToken:void 0);if(u)i.userToken=u;let d=await e.store.save(t,{apiName:n.alias,config:i,makeDefault:n.makeDefault,connectionName:n.connectionName}),p=(await e.store.load(t)).connections[d],w=[`saved ${t}/${d} ${o}${o==="mssql"&&!u?" (no userToken; tAPI calls need it: fui db add ... --user-token-env VAR)":""}`];if(c)w.push(`apiName: ${p.apiName} database: ${c.sqlServer}/${c.database}`);if(o==="mssql"){let y={...r,alias:d,config:p},v=await je(y,(N)=>Po(e.store.paths,N,t,d));w.push(...Fa(d,v))}return{notes:w}}async function La(e,t,n){let o=await e.store.resolve(Ie(e,{db:t}));lt(o,"schema pull");let s=await je(o,(r)=>Po(e.store.paths,r,o.projectId,o.alias,n));return{notes:[...o.note?[o.note]:[],`ok ${o.projectId}/${o.alias} schema refreshed`,...Fa(o.alias,s)]}}function Do(e,t){return()=>je(t,(n)=>Po(e.store.paths,n,t.projectId,t.alias))}async function Ua(e,t){let n=await e.store.resolve(Ie(e,{db:t.db}));lt(n,"schema");let{projectId:o,alias:s}=n,r=await $a(e.store.paths,o,s,Do(e,n));if(!r)throw new m(`no local schema for ${o}/${s}; run: fui schema pull --db ${o}/${s}`);let i=t.search?.toLowerCase(),a=(y)=>!i||y.toLowerCase().includes(i),c=t.objectType,l=!c||c==="Table"||c==="View",u=!c||c!=="Table"&&c!=="View",d=l?r.tables.filter((y)=>(!c||Hr(y)===c)&&a(y)):[],p=u?r.procedures.filter((y)=>(!c||Vr(y)===c)&&a(y)):[],h=await Mt(e.store.paths.connDir(o,s),`schema of ${o}/${s}`,`fui schema pull --db ${o}/${s}`,"schema.json"),w=[...d,...p];return{notes:[...n.note?[n.note]:[],...h?[h]:[],`${o}/${s} fetchedAt=${r.fetchedAt} tables/views=${d.length} procs/functions=${p.length}`],data:w.length>0?`${w.join(`
66
+ ${t}`,e.severity==="block"?"hard":"soft")}function Va(e,t){let n=Ra(t.schema),o=[`tables ${n.Table} views ${n.View} api ${n.API} api-file ${n["API File"]} procedures ${n.Procedure} functions ${n.Function}`],s=t.refresh;if(s)o.push(`sql bodies in _db/${e}/: wrote ${s.written.length}, unchanged ${s.unchanged.length}, archived ${s.archived.length}`);if(t.skipped.length>0)o.push(`not refreshed (file name is not a SQL identifier): ${t.skipped.join(" ")}`);if(t.removed.length>0)o.push(`archived to _history (dropped on server): ${t.removed.join(" ")}`);if(t.localOnly.length>0)o.push(`local only, not on server: ${t.localOnly.join(" ")}; undeployed draft -> keep or deploy; stale -> fui schema pull --db ${e} --prune-local-only`);return o}async function xo(e,t,n){yt(n.alias);let{engine:o}=Mn(n.dbToken);if(o==="mssql"&&!n.apiDomain)throw new h("MSSQL connections need --api-domain <host>");let s=n.apiDomain?Fe(n.apiDomain):void 0,r={projectId:t,alias:n.alias,config:{apiDomain:s,dbToken:n.dbToken},engine:o,dbToken:n.dbToken,note:null};if(n.test)await je(r,(y)=>y.query("SELECT 1 AS ok"));let i={...s?{apiDomain:s}:{},...n.sqlServer?{sqlServer:n.sqlServer}:{},...n.database?{database:n.database}:{},dbToken:n.dbToken},a=await e.store.load(t),c=Dt(i),l=c?Nr(a,c.sqlServer,c.database):null,u=n.userToken??(l?a.connections[l].userToken:void 0);if(u)i.userToken=u;let d=await e.store.save(t,{apiName:n.alias,config:i,makeDefault:n.makeDefault,connectionName:n.connectionName}),p=(await e.store.load(t)).connections[d],w=[`saved ${t}/${d} ${o}${o==="mssql"&&!u?" (no userToken; tAPI calls need it: fui db add ... --user-token-env VAR)":""}`];if(c)w.push(`apiName: ${p.apiName} database: ${c.sqlServer}/${c.database}`);if(o==="mssql"){let y={...r,alias:d,config:p},v=await je(y,(A)=>No(e.store.paths,A,t,d));w.push(...Va(d,v))}return{notes:w}}async function Wa(e,t,n){let o=await e.store.resolve(Ie(e,{db:t}));lt(o,"schema pull");let s=await je(o,(r)=>No(e.store.paths,r,o.projectId,o.alias,n));return{notes:[...o.note?[o.note]:[],`ok ${o.projectId}/${o.alias} schema refreshed`,...Va(o.alias,s)]}}function Oo(e,t){return()=>je(t,(n)=>No(e.store.paths,n,t.projectId,t.alias))}async function qa(e,t){let n=await e.store.resolve(Ie(e,{db:t.db}));lt(n,"schema");let{projectId:o,alias:s}=n,r=await Ca(e.store.paths,o,s,Oo(e,n));if(!r)throw new h(`no local schema for ${o}/${s}; run: fui schema pull --db ${o}/${s}`);let i=t.search?.toLowerCase(),a=(y)=>!i||y.toLowerCase().includes(i),c=t.objectType,l=!c||c==="Table"||c==="View",u=!c||c!=="Table"&&c!=="View",d=l?r.tables.filter((y)=>(!c||Wr(y)===c)&&a(y)):[],p=u?r.procedures.filter((y)=>(!c||qr(y)===c)&&a(y)):[],m=await Mt(e.store.paths.connDir(o,s),`schema of ${o}/${s}`,`fui schema pull --db ${o}/${s}`,"schema.json"),w=[...d,...p];return{notes:[...n.note?[n.note]:[],...m?[m]:[],`${o}/${s} fetchedAt=${r.fetchedAt} tables/views=${d.length} procs/functions=${p.length}`],data:w.length>0?`${w.join(`
67
67
  `)}
68
- `:""}}async function Ha(e,t){let n=await e.store.resolveProject(Ie(e,{db:t.db})),o=await e.store.load(n);if(Object.keys(o.connections).length===0)throw new m(`project "${n}" has no DB connections; run: fui db add <apiName> -p ${n}`);let s=t.db?[(await e.store.resolve(Ie(e,{db:t.db}))).alias]:Object.keys(o.connections),r=t.search?.toLowerCase(),i=(u)=>!r||u.toLowerCase().includes(r),a=t.objectType,c=[],l=[];for(let u of s){let d;try{let h=await e.store.resolve({project:n,db:u});d=await No(e.store.paths,n,u,Do(e,h))}catch(h){l.push(`${u}: schema unreadable: ${h.message}`);continue}if(!d){l.push(`${u}: no local schema; run: fui schema pull --db ${n}/${u}`);continue}l.push(`${u} fetchedAt=${d.fetchedAt}`);let p=`${n}/${u}`;if(!a||a==="Table"||a==="View"){for(let h of d.tables)if((!a||h.objectType===a)&&i(h.name))c.push({db:p,name:h.name,kind:h.objectType,count:h.count})}if(!a||a!=="Table"&&a!=="View"){for(let h of d.procedures)if((!a||h.objectType===a)&&i(h.name))c.push({db:p,name:h.name,kind:h.objectType,count:h.count})}}return{rows:c,notes:l}}var os=(e)=>e.replace(/'/g,"''");async function Va(e,t,n){bt(t);let o=await e.store.resolve(Ie(e,{db:n,objectName:t}));lt(o,"sp get");let s=await je(o,(l)=>l.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(N'${os(t)}')),'') AS sqlText`)),r=String(s[0]?.sqlText??"");if(!r.trim())throw Error(`object "${t}" not found or has no definition in the database`);let i=e.store.paths,a=await Ne(i,i.dbDir(o.projectId),{[`${o.alias}/${t}.sql`]:r}),c=o.note?[o.note]:[];for(let l of a.archived)c.push(`old local copy archived: ${l.historyPath}`);return{path:i.spFile(o.projectId,o.alias,t),body:r,sync:a,notes:c}}async function Wa(e,t,n,o){bt(t);let s=await e.store.resolve(Ie(e,{db:o,objectName:t})),r=e.store.paths.spFile(s.projectId,s.alias,t);return await Bp(Gp(r),{recursive:!0}),await F(r,n),{path:r,alias:s.alias,notes:s.note?[s.note]:[]}}function Xp(e){return/(CREATE\s+PROCEDURE|CREATE\s+PROC|CREATE\s+FUNCTION)/im.test(e)}async function rs(e,t){try{let n=await e.query(Ea(t)),o=Number(n[0]?.granted??0);return Number.isFinite(o)?o>0:null}catch{return null}}async function Yp(e){return await e.wasSkillLoaded()?[]:["warning: skill not loaded this session; read the fui-skill references (tapi-reference, db-workflow), then run: fui skill loaded"]}async function Qp(e,t){if(!Zt()||await e.wasSkillLoaded())return;throw new Se(`[FUI_STRICT] ${t} blocked: skill not loaded this session; read tapi-reference.md and db-workflow.md, run fui skill loaded, then retry`,"hard","error")}async function qa(e,t,n){await Qp(e.session,"sp deploy"),bt(t);let o=await e.store.resolve(Ie(e,{db:n,objectName:t}));lt(o,"sp deploy");let{projectId:s,alias:r}=o,i=e.store.paths,a=i.spFile(s,r,t),c;try{c=await G(a)}catch{throw new m(`file not found: ${a}; run fui sp save or fui sp get first`)}if(!c.trim())throw new m(`file is empty: ${a}`);let l=Qr(c)??t,u=Lt(l);if(u){let h=Zr(c);if(h.length>0)throw new Se(`tAPI does not support parameter default values; remove "= ..." and save before deploying:
69
- `+h.map((w)=>` - ${w}`).join(`
70
- `),"hard","error")}let d=`${s}/${r}`,p=[];return await je(o,async(h)=>{let w=null,y="";try{let b=await h.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(N'${os(l)}')), '') AS def`);y=String(b[0]?.def??"").trim(),w=y.length>0}catch{}let v="";if(y)try{v=`rollback copy: ${await fe(i,a,{content:y})}`}catch(b){throw Error(`could not save the rollback copy (${b.message}); not deploying`)}let N=Ta(c,w);if(await h.exec(N.sql),o.note)p.push(o.note);if(p.push(`ok deployed ${t} to ${d}`,`file: ${a}`),N.changed)p.push(`ran as ${N.to} because "${l}" ${N.to==="ALTER"?"exists":"does not exist"}; file unchanged`);if(v)p.push(v);let I=w===null?Xp(c):!w,g=Ra(c);if(I&&!g){let b=qn(l);try{await h.exec(b),p.push(`GRANT EXECUTE ON [dbo].[${l}] TO [public]: applied`)}catch(R){p.push(`deploy OK but grant FAILED (${R.message}); do not redeploy, run: fui exec "${b}" --db ${d}`)}}if(!g){let b=await rs(h,l);if(b===!1)p.push(`[public] lacks EXECUTE on "${l}"; tAPI calls will fail. run: fui exec "${qn(l)}" --db ${d}`);else if(b===!0&&!I)p.push(`[public] has EXECUTE on "${l}"`)}if(u)try{let b=await Rr(Ee(o.config.apiDomain),o.config.apiName??r,u.isAuth,u.fnName,o.config.userToken);p.push(b.ok?`tAPI cache cleared (GET ${b.url} -> HTTP ${b.status})`:`tAPI /help HTTP ${b.status}; ignored, deploy done. only if the endpoint still takes old params: fui sp help ${l}`)}catch(b){p.push(`tAPI /help failed (${b.message}); ignored, deploy done`)}}),{notes:[...await Yp(e.session),...p]}}async function Ba(e,t,n,o){let s=[t,`spAPI_${t}`,`spAPI_AUTH_${t}`,`spAPIFILE_${t}`,`spAPIFILE_UPLOAD_${t}`],r=await e.store.resolve(Ie(e,{db:n,objectName:s}));lt(r,"sp help");let i=null;try{i=await No(e.store.paths,r.projectId,r.alias,Do(e,r))}catch{}let a=t,c=i?.procedures.find((h)=>s.some((w)=>w.toLowerCase()===h.name.toLowerCase()));if(c)a=c.name;let l=Lt(a);if(!l)throw new m(`"${a}" is not an API SP (spAPI_/spAPIFILE_ prefix); only those have a /help route. `+(i?"check the name: fui sp list --type API":"no local schema; run: fui schema pull"));let u=r.note?[r.note]:[],d;try{d=await Rr(Ee(r.config.apiDomain),r.config.apiName??r.alias,l.isAuth,l.fnName,r.config.userToken)}catch(h){return u.push(`/help for "${a}" failed (${h.message}); ignored, does not affect the deployed SP`),{notes:u}}if(u.push(`GET ${d.url} -> HTTP ${d.status}`),!d.ok)return u.push("/help unavailable for this function; ignored. it only shows params and clears the tAPI cache. do not try other URL shapes"),{notes:u};u.push("tAPI cache cleared for this function");let p;try{let h=JSON.stringify(sn(JSON.parse(d.text)));p=o>0&&h.length>o?`${h.slice(0,o)}…(truncated)`:h}catch{p=o>0?d.text.slice(0,o):d.text}return{notes:u,data:`${p}
71
- `}}async function Ja(e,t,n){let o=es(t,n.confirmWrite,"query");if(o)throw Ma(o);let s=await e.store.resolve(Ie(e,{db:n.db,write:!n.db&&an(t).length>0}));return{rows:await je(s,(i)=>i.query(t)),connection:s}}function Zp(e){if(!/\b(?:CREATE|ALTER)\s+(?:PROCEDURE|PROC)\b/i.test(Ue(e)))return null;return Qr(e)}async function Ka(e,t,n){let o=es(t,n.confirmWrite,"nonquery");if(o)throw Ma(o);let s=await e.store.resolve(Ie(e,{db:n.db,write:!n.db})),r=Fr(s),i=[];try{let a=await r.exec(t),c=s.engine==="mssql"?Zp(t):null;if(c){let l=`${s.projectId}/${s.alias}`,u=await rs(r,c);if(u===!1)i.push(`[public] lacks EXECUTE on "${c}"; tAPI calls will fail. run: fui exec "${qn(c)}" --db ${l} (fui sp save + sp deploy grants automatically)`);else if(u===!0)i.push(`[public] has EXECUTE on "${c}"`);if(Lt(c))i.push(`"${c}" is an API SP: the tAPI param cache is not cleared here; run: fui sp help ${c}`)}return{result:a,connection:s,notes:i}}finally{await r.close()}}async function Ga(e,t){let{name:n}=t,o=[n,`spAPI_${n}`,`spAPI_AUTH_${n}`,`spAPIFILE_${n}`,`spAPIFILE_UPLOAD_${n}`],s=await e.store.resolve(Ie(e,{db:t.db,objectName:o}));lt(s,"sp verify");let{projectId:r,alias:i,config:a}=s,c=e.store.paths,l=null;try{l=await No(c,r,i,Do(e,s))}catch{}let u=n,d=l?.procedures.find((V)=>o.some((de)=>de.toLowerCase()===V.name.toLowerCase()));if(d)u=d.name;let p=Lt(u);if(!p)throw new m(`"${u}" is not an API SP (spAPI_/spAPIFILE_ prefix); plain SPs have no HTTP endpoint. `+(l?"check the name: fui sp list --type API":"no local schema; run: fui schema pull"));let{prefix:h,isAuth:w,fnName:y}=p,v=/^spAPIFILE_UPLOAD_/i.test(h),N=/^spAPIFILE_/i.test(h)&&!v,I=await em(e,s,u);if(!I)throw Error(`cannot read the real body of "${u}": no _db/${i}/${u}.sql and the live OBJECT_DEFINITION lookup failed (no permission or no such object). sp verify never guesses from the name; fetch it first: fui sp get ${u} --db ${r}/${i}`);await e.session.markSpVerified([u,y]);let g=I.source==="local"?await Mt(c.connDir(r,i),`_db/${i}/${u}.sql`,`fui schema pull --db ${r}/${i}`,"schema.json"):null,b=I.source==="local"?`_db/${i}/${u}.sql`:"OBJECT_DEFINITION (live definition read, no call)",R=v?"API File Upload":N?"API File Get":"API",C=N||v?"GET/POST":"POST",S=[...s.note?[s.note]:[],...g?[g]:[],`verify ${u} (from ${b})`,`kind: ${R}${w?" (AUTH_: public, no Bearer)":""}`,`url (reference only, not called; copy verbatim): ${C} https://${a.apiDomain}/${a.apiName??i}/${w?"auth/":""}${y}`,"","## params"],D=0;if(d){let V=[];for(let ge of d.parameters??[]){let Be=ge.replace(/^@/,"");if(/^sys_/i.test(Be))continue;if(/^url\d+_/i.test(Be)){D++;continue}V.push(Be)}if(S.push(`route params (@url*_): ${D}`,`body params: ${V.length?V.join(", "):"(none)"}`),t.params){let ge=new Set(Object.keys(t.params).map((Ce)=>Ce.toLowerCase())),Be=V.filter((Ce)=>!ge.has(Ce.toLowerCase())),tt=Object.keys(t.params).filter((Ce)=>!/^@?sys_/i.test(Ce)&&!V.some((Et)=>Et.toLowerCase()===Ce.replace(/^@/,"").toLowerCase()));if(Be.length)S.push(` warning: missing from --params: ${Be.join(", ")}`);if(tt.length)S.push(` warning: not declared by the SP: ${tt.join(", ")}`)}let de=(t.urlParams??[]).length;if(t.urlParams&&de!==D)S.push(` warning: the SP declares ${D} route params, --url-param gave ${de}`)}else S.push(`warning: "${u}" is not in the local schema; params not checked. just deployed? run: fui schema pull --db ${r}/${i}`);if(t.checkGrant){let V=await je(s,(de)=>rs(de,u));S.push(V===!0?`[public] has EXECUTE on "${u}"`:V===!1?`[public] LACKS EXECUTE on "${u}"; the endpoint fails with a permission error. fix: fui exec "${qn(u)}" --db ${r}/${i}`:"warning: could not read sys.database_permissions; EXECUTE grant unknown")}let x=Zr(I.sql);if(x.length)S.push(`warning: tAPI does not support default values; sp deploy will block: ${x.join("; ")}`);S.push("","## response shape (static, best effort)");let A=ts(Ft(I.sql)),E=A.filter((V)=>!V.isInto),te=A.length-E.length;if(te>0)S.push(`(${te} SELECT ... INTO #temp/@table not returned to the client; not counted)`);if(E.length===0)S.push("warning: no result-returning SELECT; the SP may only write (check any OUT binding in module.json)");else if(E.length===1){let V=E[0];if(S.push("1 SELECT: data is an array (one resultset)"),V.hasStar)S.push("warning: SELECT *: columns unknown from the body; check fui schema --search <table>");else if(V.columns)S.push(`columns: ${V.columns.join(", ")}`)}else S.push(`${E.length} result SELECTs: data may be array-of-arrays (data[0], data[1], ...)`),E.forEach((V,de)=>{S.push(` data[${de}]: ${V.hasStar?"SELECT * (columns unknown)":V.columns?V.columns.join(", "):"(unknown)"}`)});S.push(Aa(I.sql)?"error contract { Message }: RAISERROR(...) + RETURN present":"warning: no clear RAISERROR(...) + RETURN; error branches must RAISERROR level 16 then RETURN"),S.push("","## read/write");let ve=an(I.sql);if(ve.length===0)S.push("read API: no INSERT/UPDATE+SET/DELETE/MERGE in the body");else S.push(`WRITE API: ${ve.join(", ")}`,"never call it to test; check the body: WHERE on the key/route param, multi-step writes in a TRANSACTION, RAISERROR when the row is missing",`test the UI flow with fake data: fui module simulate --mocks '{"${y}":{"data":[...]}}'`);S.push("","## apiMocks snippet (fake data)");let $e=E[0];if($e&&!$e.hasStar&&$e.columns?.length){let V={};for(let de of $e.columns)V[de]=ns(de);S.push(JSON.stringify({apiMocks:{[y]:{data:[V,V]}}},null,2))}else S.push("(not generated: SELECT * or unknown columns; write apiMocks from fui schema --search <table>)");return{notes:S}}async function em(e,t,n){try{let o=await G(e.store.paths.spFile(t.projectId,t.alias,n));if(o.trim())return{sql:o,source:"local"}}catch{}try{let o=await je(t,(r)=>r.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(N'${os(n)}')),'') AS sqlText`)),s=String(o[0]?.sqlText??"");if(s.trim())return{sql:s,source:"live"}}catch{}return null}async function za(e,t,n,o){bt(t);let s=await e.store.resolve(Ie(e,{db:o,objectName:t}));lt(s,"sp delete");let{projectId:r,alias:i}=s,a=n==="Function"?`DROP FUNCTION [dbo].[${t}]`:`DROP PROCEDURE [dbo].[${t}]`;await je(s,(u)=>u.exec(a));let c=[...s.note?[s.note]:[],`ok dropped ${n} [dbo].[${t}] on ${r}/${i}`],l=e.store.paths.spFile(r,i,t);try{let u=await G(l),d=await fe(e.store.paths,l,{content:u,suffix:"deleted"});await Kp(l),c.push(`local file archived: ${d?Nt(e.store.paths,d):""} (removed from _db/${i}/)`)}catch{}return{notes:c}}async function Xa(e,t,n,o){bt(t),bt(n);let s=await e.store.resolve(Ie(e,{db:o,objectName:t}));lt(s,"sp rename");let{projectId:r,alias:i}=s;await je(s,(c)=>c.exec(`EXEC sp_rename '[dbo].[${t}]', '${n.replace(/'/g,"''")}'`));let a=[...s.note?[s.note]:[],`ok renamed [dbo].[${t}] -> ${n} on ${r}/${i}`,`refresh the local schema: fui schema pull --db ${r}/${i}`];try{await Jp(e.store.paths.spFile(r,i,t),e.store.paths.spFile(r,i,n)),a.push(`local file renamed: _db/${i}/${t}.sql -> _db/${i}/${n}.sql`)}catch{}return{notes:a}}async function Ya(e,t){let n=await e.store.resolve(Ie(e,{db:t.db,write:!t.db}));lt(n,"db token-rebuild");let{projectId:o,alias:s,config:r}=n;if(!r.sqlServer||!r.database)throw new m(`connection ${o}/${s} has no sqlServer/database saved; re-add it: fui db add ${s} -p ${o} --sql-server <host> --database <name> ...`);let i=en(r.sqlServer,r.database,t.uid,t.password);return await new at(Ee(r.apiDomain),i).query("SELECT 1 AS ok"),await e.store.update(o,s,{...r,dbToken:i}),{notes:["ok dbToken rebuilt and tested",`connection: ${o}/${s}`,`sql server: ${r.sqlServer}`,`database: ${r.database}`,`uid: ${t.uid}`,`config: ${e.store.paths.connectionsFile(o)}`]}}async function Bn(e,t){let n=t?$o(t).project:void 0;return e.store.resolveProject({project:e.project??n,contextProject:e.contextProject})}async function Qa(e,t){let n=await Bn(e,t.db),o=e.store.paths.connectionsFile(n);if(t.tokenSource){let{userToken:r,source:i}=await sa(e.store,n,t.tokenSource),a=await e.store.resolve({project:n,db:t.db});return await e.store.update(n,a.alias,{...a.config,userToken:r}),{notes:[...a.note?[a.note]:[],`ok borrowed the userToken of ${i} into ${n}/${a.alias}`,`config: ${o}`]}}if(!t.userToken){let r=await Ar(e.store);if(r.length===0)return{notes:["no userToken in any project of this workspace; pass one: fui db user-token --token -"]};let i=Dr(r);return{notes:[`${i.length} distinct userToken(s) across ${r.length} connection(s); pick one (same apiDomain preferred):`,...i.map((a)=>`fingerprint ${oa(a.token)} sources: ${na(a.sources)}`),"then: fui db user-token --from <project/alias> (the value is copied internally, never printed)"]}}let s=await e.store.resolve({project:n,db:t.db});return await e.store.update(n,s.alias,{...s.config,userToken:t.userToken}),{notes:[...s.note?[s.note]:[],`ok userToken saved for ${n}/${s.alias}`,`config: ${o}`]}}async function Za(e,t){let n=await Bn(e),o=await Io(e.store,n,t),s=await Hn(o),r=t.search?.trim().toLowerCase(),i=r?s.filter((c)=>[c.APIName,c.DatabaseName,c.ModuleName].some((l)=>String(l??"").toLowerCase().includes(r))):s,a=o.note?[o.note]:[];if(i.length===0)a.push(r?`no alias matches "${t.search}" among the ${s.length} databases you may manage; drop --search to see all`:`empty: this token manages no database on ${o.apiDomain}`);else a.push(`${i.length}/${s.length} databases you may manage on ${o.apiDomain}; connect one: fui db add-by-name ${String(i[0].APIName??"<alias>")}`);return{rows:Or(i),notes:a,total:s.length}}async function ec(e,t){yt(t.apiName);let n=await Bn(e),o=await Io(e.store,n,{...t}),s=(await Hn(o)).find((l)=>String(l.APIName??"").toLowerCase()===t.apiName.toLowerCase());if(s)throw new m(`alias "${t.apiName}" already exists on ${o.apiDomain} (ModuleID ${s.ModuleID}, database ${s.DatabaseName}); db alias new only registers NEW aliases. see: fui db alias list --search ${t.apiName}`);let r=t.moduleId??await ca(e.store,n);await Un(o.apiDomain,o.userToken,"SM_Modules_Update",{ModuleID:r,ModuleName:t.moduleName,APIName:t.apiName,Enable:1,UpdateConnection:0}),await Un(o.apiDomain,o.userToken,"SM_Modules_Update",{ModuleID:r,SQLServer:t.sqlServer,DatabaseName:t.database,Username:t.username,Password:t.password,UpdateConnection:1});let a=(await Hn(o).catch(()=>null))?.find((l)=>String(l.APIName??"").toLowerCase()===t.apiName.toLowerCase()),c=a?Or([a])[0]:null;return{notes:[...o.note?[o.note]:[],`ok registered alias "${t.apiName}" (ModuleID ${r}) on ${o.apiDomain}`,c?`post-check: ${c.apiName} ${c.sqlServer} ${c.database} enable=${c.enable} ${c.moduleName}`:`warning: post-check: alias not in the managed list yet; check: fui db alias list --search ${t.apiName}`,`login stored: ${t.username} (password not printed)`,`connect it: fui db add-by-name ${t.apiName}`]}}async function tc(e,t){let n=await Bn(e);yt(t.apiName);let o=await Io(e.store,n,t),s=(await Hn(o)).filter((u)=>String(u.APIName??"").toLowerCase()===t.apiName.toLowerCase());if(s.length===0)throw new m(`alias "${t.apiName}" not found on ${o.apiDomain}: it does not exist, or your account may not manage it (the list is scoped to your rights). see: fui db alias list`);if(s.length>1)throw new m(`alias "${t.apiName}" matches ${s.length} acc records; not picking one. use fui db add with an explicit token`);let r=s[0];if(!r.SQLServer||!r.DatabaseName)throw new m(`the acc record of "${t.apiName}" has no SQLServer/DatabaseName; use fui db add with your own token`);let{creds:i,source:a}=await aa(e.store,n,t.borrowFrom,o.apiDomain),c=en(r.SQLServer,r.DatabaseName,i.uid,i.password);try{await new at(Ee(o.apiDomain),c).query("SELECT 1 AS ok")}catch(u){throw Error(`test connection to ${r.SQLServer}/${r.DatabaseName} failed; nothing saved.
68
+ `:""}}async function Ba(e,t){let n=await e.store.resolveProject(Ie(e,{db:t.db})),o=await e.store.load(n);if(Object.keys(o.connections).length===0)throw new h(`project "${n}" has no DB connections; run: fui db add <apiName> -p ${n}`);let s=t.db?[(await e.store.resolve(Ie(e,{db:t.db}))).alias]:Object.keys(o.connections),r=t.search?.toLowerCase(),i=(u)=>!r||u.toLowerCase().includes(r),a=t.objectType,c=[],l=[];for(let u of s){let d;try{let m=await e.store.resolve({project:n,db:u});d=await Do(e.store.paths,n,u,Oo(e,m))}catch(m){l.push(`${u}: schema unreadable: ${m.message}`);continue}if(!d){l.push(`${u}: no local schema; run: fui schema pull --db ${n}/${u}`);continue}l.push(`${u} fetchedAt=${d.fetchedAt}`);let p=`${n}/${u}`;if(!a||a==="Table"||a==="View"){for(let m of d.tables)if((!a||m.objectType===a)&&i(m.name))c.push({db:p,name:m.name,kind:m.objectType,count:m.count})}if(!a||a!=="Table"&&a!=="View"){for(let m of d.procedures)if((!a||m.objectType===a)&&i(m.name))c.push({db:p,name:m.name,kind:m.objectType,count:m.count})}}return{rows:c,notes:l}}var is=(e)=>e.replace(/'/g,"''");async function Ja(e,t,n){bt(t);let o=await e.store.resolve(Ie(e,{db:n,objectName:t}));lt(o,"sp get");let s=await je(o,(l)=>l.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(N'${is(t)}')),'') AS sqlText`)),r=String(s[0]?.sqlText??"");if(!r.trim())throw Error(`object "${t}" not found or has no definition in the database`);let i=e.store.paths,a=await Ae(i,i.dbDir(o.projectId),{[`${o.alias}/${t}.sql`]:r}),c=o.note?[o.note]:[];for(let l of a.archived)c.push(`old local copy archived: ${l.historyPath}`);return{path:i.spFile(o.projectId,o.alias,t),body:r,sync:a,notes:c}}async function Ka(e,t,n,o){bt(t);let s=await e.store.resolve(Ie(e,{db:o,objectName:t})),r=e.store.paths.spFile(s.projectId,s.alias,t);return await Qp(tm(r),{recursive:!0}),await L(r,n),{path:r,alias:s.alias,notes:s.note?[s.note]:[]}}function om(e){return/(CREATE\s+PROCEDURE|CREATE\s+PROC|CREATE\s+FUNCTION)/im.test(e)}async function as(e,t){try{let n=await e.query(Da(t)),o=Number(n[0]?.granted??0);return Number.isFinite(o)?o>0:null}catch{return null}}async function rm(e){return await e.wasSkillLoaded()?[]:["warning: skill not loaded this session; read the fui-skill references (tapi-reference, db-workflow), then run: fui skill loaded"]}async function sm(e,t){if(!Zt()||await e.wasSkillLoaded())return;throw new Se(`[FUI_STRICT] ${t} blocked: skill not loaded this session; read tapi-reference.md and db-workflow.md, run fui skill loaded, then retry`,"hard","error")}async function Ga(e,t,n){await sm(e.session,"sp deploy"),bt(t);let o=await e.store.resolve(Ie(e,{db:n,objectName:t}));lt(o,"sp deploy");let{projectId:s,alias:r}=o,i=e.store.paths,a=i.spFile(s,r,t),c;try{c=await G(a)}catch{throw new h(`file not found: ${a}; run fui sp save or fui sp get first`)}if(!c.trim())throw new h(`file is empty: ${a}`);let l=ts(c)??t,u=Ft(l);if(u){let m=ns(c);if(m.length>0)throw new Se(`tAPI does not support parameter default values; remove "= ..." and save before deploying:
69
+ `+m.map((w)=>` - ${w}`).join(`
70
+ `),"hard","error")}let d=`${s}/${r}`,p=[];return await je(o,async(m)=>{let w=null,y="";try{let k=await m.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(N'${is(l)}')), '') AS def`);y=String(k[0]?.def??"").trim(),w=y.length>0}catch{}let v="";if(y)try{v=`rollback copy: ${await fe(i,a,{content:y})}`}catch(k){throw Error(`could not save the rollback copy (${k.message}); not deploying`)}let A=Na(c,w);if(await m.exec(A.sql),o.note)p.push(o.note);if(p.push(`ok deployed ${t} to ${d}`,`file: ${a}`),A.changed)p.push(`ran as ${A.to} because "${l}" ${A.to==="ALTER"?"exists":"does not exist"}; file unchanged`);if(v)p.push(v);let I=w===null?om(c):!w,g=Aa(c);if(I&&!g){let k=qn(l);try{await m.exec(k),p.push(`GRANT EXECUTE ON [dbo].[${l}] TO [public]: applied`)}catch(R){p.push(`deploy OK but grant FAILED (${R.message}); do not redeploy, run: fui exec "${k}" --db ${d}`)}}if(!g){let k=await as(m,l);if(k===!1)p.push(`[public] lacks EXECUTE on "${l}"; tAPI calls will fail. run: fui exec "${qn(l)}" --db ${d}`);else if(k===!0&&!I)p.push(`[public] has EXECUTE on "${l}"`)}if(u)try{let k=await Pr(Pe(o.config.apiDomain),o.config.apiName??r,u.isAuth,u.fnName,o.config.userToken);p.push(k.ok?`tAPI cache cleared (GET ${k.url} -> HTTP ${k.status})`:`tAPI /help HTTP ${k.status}; ignored, deploy done. only if the endpoint still takes old params: fui sp help ${l}`)}catch(k){p.push(`tAPI /help failed (${k.message}); ignored, deploy done`)}}),{notes:[...await rm(e.session),...p]}}async function za(e,t,n,o){let s=[t,`spAPI_${t}`,`spAPI_AUTH_${t}`,`spAPIFILE_${t}`,`spAPIFILE_UPLOAD_${t}`],r=await e.store.resolve(Ie(e,{db:n,objectName:s}));lt(r,"sp help");let i=null;try{i=await Do(e.store.paths,r.projectId,r.alias,Oo(e,r))}catch{}let a=t,c=i?.procedures.find((m)=>s.some((w)=>w.toLowerCase()===m.name.toLowerCase()));if(c)a=c.name;let l=Ft(a);if(!l)throw new h(`"${a}" is not an API SP (spAPI_/spAPIFILE_ prefix); only those have a /help route. `+(i?"check the name: fui sp list --type API":"no local schema; run: fui schema pull"));let u=r.note?[r.note]:[],d;try{d=await Pr(Pe(r.config.apiDomain),r.config.apiName??r.alias,l.isAuth,l.fnName,r.config.userToken)}catch(m){return u.push(`/help for "${a}" failed (${m.message}); ignored, does not affect the deployed SP`),{notes:u}}if(u.push(`GET ${d.url} -> HTTP ${d.status}`),!d.ok)return u.push("/help unavailable for this function; ignored. it only shows params and clears the tAPI cache. do not try other URL shapes"),{notes:u};u.push("tAPI cache cleared for this function");let p;try{let m=JSON.stringify(sn(JSON.parse(d.text)));p=o>0&&m.length>o?`${m.slice(0,o)}…(truncated)`:m}catch{p=o>0?d.text.slice(0,o):d.text}return{notes:u,data:`${p}
71
+ `}}async function Xa(e,t,n){let o=os(t,n.confirmWrite,"query");if(o)throw Ha(o);let s=await e.store.resolve(Ie(e,{db:n.db,write:!n.db&&an(t).length>0}));return{rows:await je(s,(i)=>i.query(t)),connection:s}}function im(e){if(!/\b(?:CREATE|ALTER)\s+(?:PROCEDURE|PROC)\b/i.test(Ue(e)))return null;return ts(e)}async function Ya(e,t,n){let o=os(t,n.confirmWrite,"nonquery");if(o)throw Ha(o);let s=await e.store.resolve(Ie(e,{db:n.db,write:!n.db})),r=Ur(s),i=[];try{let a=await r.exec(t),c=s.engine==="mssql"?im(t):null;if(c){let l=`${s.projectId}/${s.alias}`,u=await as(r,c);if(u===!1)i.push(`[public] lacks EXECUTE on "${c}"; tAPI calls will fail. run: fui exec "${qn(c)}" --db ${l} (fui sp save + sp deploy grants automatically)`);else if(u===!0)i.push(`[public] has EXECUTE on "${c}"`);if(Ft(c))i.push(`"${c}" is an API SP: the tAPI param cache is not cleared here; run: fui sp help ${c}`)}return{result:a,connection:s,notes:i}}finally{await r.close()}}async function Qa(e,t){let{name:n}=t,o=[n,`spAPI_${n}`,`spAPI_AUTH_${n}`,`spAPIFILE_${n}`,`spAPIFILE_UPLOAD_${n}`],s=await e.store.resolve(Ie(e,{db:t.db,objectName:o}));lt(s,"sp verify");let{projectId:r,alias:i,config:a}=s,c=e.store.paths,l=null;try{l=await Do(c,r,i,Oo(e,s))}catch{}let u=n,d=l?.procedures.find((V)=>o.some((de)=>de.toLowerCase()===V.name.toLowerCase()));if(d)u=d.name;let p=Ft(u);if(!p)throw new h(`"${u}" is not an API SP (spAPI_/spAPIFILE_ prefix); plain SPs have no HTTP endpoint. `+(l?"check the name: fui sp list --type API":"no local schema; run: fui schema pull"));let{prefix:m,isAuth:w,fnName:y}=p,v=/^spAPIFILE_UPLOAD_/i.test(m),A=/^spAPIFILE_/i.test(m)&&!v,I=await am(e,s,u);if(!I)throw Error(`cannot read the real body of "${u}": no _db/${i}/${u}.sql and the live OBJECT_DEFINITION lookup failed (no permission or no such object). sp verify never guesses from the name; fetch it first: fui sp get ${u} --db ${r}/${i}`);await e.session.markSpVerified([u,y]);let g=I.source==="local"?await Mt(c.connDir(r,i),`_db/${i}/${u}.sql`,`fui schema pull --db ${r}/${i}`,"schema.json"):null,k=I.source==="local"?`_db/${i}/${u}.sql`:"OBJECT_DEFINITION (live definition read, no call)",R=v?"API File Upload":A?"API File Get":"API",C=A||v?"GET/POST":"POST",S=[...s.note?[s.note]:[],...g?[g]:[],`verify ${u} (from ${k})`,`kind: ${R}${w?" (AUTH_: public, no Bearer)":""}`,`url (reference only, not called; copy verbatim): ${C} https://${a.apiDomain}/${a.apiName??i}/${w?"auth/":""}${y}`,"","## params"],D=0;if(d){let V=[];for(let ge of d.parameters??[]){let Be=ge.replace(/^@/,"");if(/^sys_/i.test(Be))continue;if(/^url\d+_/i.test(Be)){D++;continue}V.push(Be)}if(S.push(`route params (@url*_): ${D}`,`body params: ${V.length?V.join(", "):"(none)"}`),t.params){let ge=new Set(Object.keys(t.params).map((Ce)=>Ce.toLowerCase())),Be=V.filter((Ce)=>!ge.has(Ce.toLowerCase())),tt=Object.keys(t.params).filter((Ce)=>!/^@?sys_/i.test(Ce)&&!V.some((Pt)=>Pt.toLowerCase()===Ce.replace(/^@/,"").toLowerCase()));if(Be.length)S.push(` warning: missing from --params: ${Be.join(", ")}`);if(tt.length)S.push(` warning: not declared by the SP: ${tt.join(", ")}`)}let de=(t.urlParams??[]).length;if(t.urlParams&&de!==D)S.push(` warning: the SP declares ${D} route params, --url-param gave ${de}`)}else S.push(`warning: "${u}" is not in the local schema; params not checked. just deployed? run: fui schema pull --db ${r}/${i}`);if(t.checkGrant){let V=await je(s,(de)=>as(de,u));S.push(V===!0?`[public] has EXECUTE on "${u}"`:V===!1?`[public] LACKS EXECUTE on "${u}"; the endpoint fails with a permission error. fix: fui exec "${qn(u)}" --db ${r}/${i}`:"warning: could not read sys.database_permissions; EXECUTE grant unknown")}let x=ns(I.sql);if(x.length)S.push(`warning: tAPI does not support default values; sp deploy will block: ${x.join("; ")}`);S.push("","## response shape (static, best effort)");let N=rs(Lt(I.sql)),P=N.filter((V)=>!V.isInto),te=N.length-P.length;if(te>0)S.push(`(${te} SELECT ... INTO #temp/@table not returned to the client; not counted)`);if(P.length===0)S.push("warning: no result-returning SELECT; the SP may only write (check any OUT binding in module.json)");else if(P.length===1){let V=P[0];if(S.push("1 SELECT: data is an array (one resultset)"),V.hasStar)S.push("warning: SELECT *: columns unknown from the body; check fui schema --search <table>");else if(V.columns)S.push(`columns: ${V.columns.join(", ")}`)}else S.push(`${P.length} result SELECTs: data may be array-of-arrays (data[0], data[1], ...)`),P.forEach((V,de)=>{S.push(` data[${de}]: ${V.hasStar?"SELECT * (columns unknown)":V.columns?V.columns.join(", "):"(unknown)"}`)});S.push(_a(I.sql)?"error contract { Message }: RAISERROR(...) + RETURN present":"warning: no clear RAISERROR(...) + RETURN; error branches must RAISERROR level 16 then RETURN"),S.push("","## read/write");let ve=an(I.sql);if(ve.length===0)S.push("read API: no INSERT/UPDATE+SET/DELETE/MERGE in the body");else S.push(`WRITE API: ${ve.join(", ")}`,"never call it to test; check the body: WHERE on the key/route param, multi-step writes in a TRANSACTION, RAISERROR when the row is missing",`test the UI flow with fake data: fui module simulate --mocks '{"${y}":{"data":[...]}}'`);S.push("","## apiMocks snippet (fake data)");let $e=P[0];if($e&&!$e.hasStar&&$e.columns?.length){let V={};for(let de of $e.columns)V[de]=ss(de);S.push(JSON.stringify({apiMocks:{[y]:{data:[V,V]}}},null,2))}else S.push("(not generated: SELECT * or unknown columns; write apiMocks from fui schema --search <table>)");return{notes:S}}async function am(e,t,n){try{let o=await G(e.store.paths.spFile(t.projectId,t.alias,n));if(o.trim())return{sql:o,source:"local"}}catch{}try{let o=await je(t,(r)=>r.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(N'${is(n)}')),'') AS sqlText`)),s=String(o[0]?.sqlText??"");if(s.trim())return{sql:s,source:"live"}}catch{}return null}async function Za(e,t,n,o){bt(t);let s=await e.store.resolve(Ie(e,{db:o,objectName:t}));lt(s,"sp delete");let{projectId:r,alias:i}=s,a=n==="Function"?`DROP FUNCTION [dbo].[${t}]`:`DROP PROCEDURE [dbo].[${t}]`;await je(s,(u)=>u.exec(a));let c=[...s.note?[s.note]:[],`ok dropped ${n} [dbo].[${t}] on ${r}/${i}`],l=e.store.paths.spFile(r,i,t);try{let u=await G(l),d=await fe(e.store.paths,l,{content:u,suffix:"deleted"});await em(l),c.push(`local file archived: ${d?At(e.store.paths,d):""} (removed from _db/${i}/)`)}catch{}return{notes:c}}async function ec(e,t,n,o){bt(t),bt(n);let s=await e.store.resolve(Ie(e,{db:o,objectName:t}));lt(s,"sp rename");let{projectId:r,alias:i}=s;await je(s,(c)=>c.exec(`EXEC sp_rename '[dbo].[${t}]', '${n.replace(/'/g,"''")}'`));let a=[...s.note?[s.note]:[],`ok renamed [dbo].[${t}] -> ${n} on ${r}/${i}`,`refresh the local schema: fui schema pull --db ${r}/${i}`];try{await Zp(e.store.paths.spFile(r,i,t),e.store.paths.spFile(r,i,n)),a.push(`local file renamed: _db/${i}/${t}.sql -> _db/${i}/${n}.sql`)}catch{}return{notes:a}}async function tc(e,t){let n=await e.store.resolve(Ie(e,{db:t.db,write:!t.db}));lt(n,"db token-rebuild");let{projectId:o,alias:s,config:r}=n;if(!r.sqlServer||!r.database)throw new h(`connection ${o}/${s} has no sqlServer/database saved; re-add it: fui db add ${s} -p ${o} --sql-server <host> --database <name> ...`);let i=en(r.sqlServer,r.database,t.uid,t.password);return await new at(Pe(r.apiDomain),i).query("SELECT 1 AS ok"),await e.store.update(o,s,{...r,dbToken:i}),{notes:["ok dbToken rebuilt and tested",`connection: ${o}/${s}`,`sql server: ${r.sqlServer}`,`database: ${r.database}`,`uid: ${t.uid}`,`config: ${e.store.paths.connectionsFile(o)}`]}}async function Bn(e,t){let n=t?So(t).project:void 0;return e.store.resolveProject({project:e.project??n,contextProject:e.contextProject})}async function nc(e,t){let n=await Bn(e,t.db),o=e.store.paths.connectionsFile(n);if(t.tokenSource){let{userToken:r,source:i}=await ua(e.store,n,t.tokenSource),a=await e.store.resolve({project:n,db:t.db});return await e.store.update(n,a.alias,{...a.config,userToken:r}),{notes:[...a.note?[a.note]:[],`ok borrowed the userToken of ${i} into ${n}/${a.alias}`,`config: ${o}`]}}if(!t.userToken){let r=await xr(e.store);if(r.length===0)return{notes:["no userToken in any project of this workspace; pass one: fui db user-token --token -"]};let i=Or(r);return{notes:[`${i.length} distinct userToken(s) across ${r.length} connection(s); pick one (same apiDomain preferred):`,...i.map((a)=>`fingerprint ${ca(a.token)} sources: ${aa(a.sources)}`),"then: fui db user-token --from <project/alias> (the value is copied internally, never printed)"]}}let s=await e.store.resolve({project:n,db:t.db});return await e.store.update(n,s.alias,{...s.config,userToken:t.userToken}),{notes:[...s.note?[s.note]:[],`ok userToken saved for ${n}/${s.alias}`,`config: ${o}`]}}async function oc(e,t){let n=await Bn(e),o=await Co(e.store,n,t),s=await Hn(o),r=t.search?.trim().toLowerCase(),i=r?s.filter((c)=>[c.APIName,c.DatabaseName,c.ModuleName].some((l)=>String(l??"").toLowerCase().includes(r))):s,a=o.note?[o.note]:[];if(i.length===0)a.push(r?`no alias matches "${t.search}" among the ${s.length} databases you may manage; drop --search to see all`:`empty: this token manages no database on ${o.apiDomain}`);else a.push(`${i.length}/${s.length} databases you may manage on ${o.apiDomain}; connect one: fui db add-by-name ${String(i[0].APIName??"<alias>")}`);return{rows:Mr(i),notes:a,total:s.length}}async function rc(e,t){yt(t.apiName);let n=await Bn(e),o=await Co(e.store,n,{...t}),s=(await Hn(o)).find((l)=>String(l.APIName??"").toLowerCase()===t.apiName.toLowerCase());if(s)throw new h(`alias "${t.apiName}" already exists on ${o.apiDomain} (ModuleID ${s.ModuleID}, database ${s.DatabaseName}); db alias new only registers NEW aliases. see: fui db alias list --search ${t.apiName}`);let r=t.moduleId??await ma(e.store,n);await Un(o.apiDomain,o.userToken,"SM_Modules_Update",{ModuleID:r,ModuleName:t.moduleName,APIName:t.apiName,Enable:1,UpdateConnection:0}),await Un(o.apiDomain,o.userToken,"SM_Modules_Update",{ModuleID:r,SQLServer:t.sqlServer,DatabaseName:t.database,Username:t.username,Password:t.password,UpdateConnection:1});let a=(await Hn(o).catch(()=>null))?.find((l)=>String(l.APIName??"").toLowerCase()===t.apiName.toLowerCase()),c=a?Mr([a])[0]:null;return{notes:[...o.note?[o.note]:[],`ok registered alias "${t.apiName}" (ModuleID ${r}) on ${o.apiDomain}`,c?`post-check: ${c.apiName} ${c.sqlServer} ${c.database} enable=${c.enable} ${c.moduleName}`:`warning: post-check: alias not in the managed list yet; check: fui db alias list --search ${t.apiName}`,`login stored: ${t.username} (password not printed)`,`connect it: fui db add-by-name ${t.apiName}`]}}async function sc(e,t){let n=await Bn(e);yt(t.apiName);let o=await Co(e.store,n,t),s=(await Hn(o)).filter((u)=>String(u.APIName??"").toLowerCase()===t.apiName.toLowerCase());if(s.length===0)throw new h(`alias "${t.apiName}" not found on ${o.apiDomain}: it does not exist, or your account may not manage it (the list is scoped to your rights). see: fui db alias list`);if(s.length>1)throw new h(`alias "${t.apiName}" matches ${s.length} acc records; not picking one. use fui db add with an explicit token`);let r=s[0];if(!r.SQLServer||!r.DatabaseName)throw new h(`the acc record of "${t.apiName}" has no SQLServer/DatabaseName; use fui db add with your own token`);let{creds:i,source:a}=await pa(e.store,n,t.borrowFrom,o.apiDomain),c=en(r.SQLServer,r.DatabaseName,i.uid,i.password);try{await new at(Pe(o.apiDomain),c).query("SELECT 1 AS ok")}catch(u){throw Error(`test connection to ${r.SQLServer}/${r.DatabaseName} failed; nothing saved.
72
72
  ${u.message}
73
- likely the login borrowed from ${a} has no access to this database (other group); pass --borrow-from <project/alias> or use fui db add`)}let l=await Ao(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} login borrowed from ${a} (not printed)`]}}async function nc(e,t){let n=await Bn(e),o=await xr(e.store,n,t),{apiDomain:s,userToken:r}=o.config;return{projectId:n,call:(i,a)=>Un(s,r,i,a),target:{ModuleID:0,APIName:t??o.config.apiName??o.alias}}}async function oc(e,t,n){let{projectId:o,call:s,target:r}=await nc(e,n),a=(await s(t==="system"?"SM_SystemRight_Select":"SM_FunctionRight_Select",r)).data??[];return await _r(e.store.paths,o,r.APIName,t==="system"?{system:a}:{functions:a}),{apiName:r.APIName,rows:a}}async function rc(e,t,n){let{projectId:o,call:s,target:r}=await nc(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 _r(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 is={"api-domain":{type:"string"},"user-token":{type:"string"},"user-token-env":{type:"string"}};async function as(e){return{apiDomain:f(e.values,"api-domain"),userToken:await Te(f(e.values,"user-token"),f(e.values,"user-token-env"),"--user-token")}}function Jn(e){k(`${e.join(`
73
+ likely the login borrowed from ${a} has no access to this database (other group); pass --borrow-from <project/alias> or use fui db add`)}let l=await xo(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} login borrowed from ${a} (not printed)`]}}async function ic(e,t){let n=await Bn(e),o=await _r(e.store,n,t),{apiDomain:s,userToken:r}=o.config;return{projectId:n,call:(i,a)=>Un(s,r,i,a),target:{ModuleID:0,APIName:t??o.config.apiName??o.alias}}}async function ac(e,t,n){let{projectId:o,call:s,target:r}=await ic(e,n),a=(await s(t==="system"?"SM_SystemRight_Select":"SM_FunctionRight_Select",r)).data??[];return await Lr(e.store.paths,o,r.APIName,t==="system"?{system:a}:{functions:a}),{apiName:r.APIName,rows:a}}async function cc(e,t,n){let{projectId:o,call:s,target:r}=await ic(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 h(`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 Lr(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 ls={"api-domain":{type:"string"},"user-token":{type:"string"},"user-token-env":{type:"string"}};async function us(e){return{apiDomain:f(e.values,"api-domain"),userToken:await Te(f(e.values,"user-token"),f(e.values,"user-token-env"),"--user-token")}}function Jn(e){b(`${e.join(`
74
74
  `)}
75
- `)}var tm={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 Te(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 Qa(await X(e),{db:f(e.values,"db"),userToken:t,tokenSource:n});Jn(o.notes)}},nm={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 Te(f(e.values,"pwd"),f(e.values,"pwd-env"),"--pwd");if(!n)throw new m("missing --pwd - or --pwd-env VAR");Jn((await Ya(await X(e),{db:f(e.values,"db"),uid:t,password:n})).notes)}},om={name:"db alias list",usage:"fui db alias list [--search text] [--api-domain host] [--user-token-env VAR]",summary:"Databases you may manage on the acc system (APIName, SQLServer, DatabaseName); read-only",options:{search:{type:"string"},...is},async run(e){let t=await Za(await X(e),{...await as(e),search:f(e.values,"search")});for(let n of t.notes)T(`# ${n}`);if(t.rows.length>0)k(z(t.rows,e.render))}},rm={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"},...is},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 Te(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 ec(await X(e),{...await as(e),apiName:n,moduleName:o,sqlServer:s,database:r,username:i,password:a,moduleId:l});Jn(u.notes)}},sm={name:"db add-by-name",usage:"fui db add-by-name <apiName> [-p P] [--name connectionName] [--borrow-from <project/alias>] [--default] [--api-domain host] [--user-token-env VAR]",summary:"Connect by alias: look up SQLServer/Database on acc, borrow a SQL login, test, save, fetch schema",options:{name:{type:"string"},"borrow-from":{type:"string"},default:{type:"boolean"},...is},async run(e){let t=e.positionals[0];if(!t)throw new m("missing <apiName>; see: fui db alias list");let n=await tc(await X(e),{...await as(e),apiName:t,connectionName:f(e.values,"name"),borrowFrom:f(e.values,"borrow-from"),makeDefault:e.values.default===!0});Jn(n.notes)}};function ac(e){return e.map((t)=>({sysRight:String(t.SysRight??""),note:String(t.Note??"")}))}function cc(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 sc(e,t,n){if(e!==void 0&&e.length>t)throw new m(`${n} is at most ${t} characters`);return e}function ic(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 oc(await X(t),e,f(t.values,"api-name"));if(T(`# ${e==="system"?"SystemRight":"FunctionRight"} of "${n}": ${o.length}`),o.length===0){T(`# none defined; create one: fui right ${e} new ...`);return}k(z(e==="system"?ac(o):cc(o),t.render))}}}function cn(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:sc(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:sc(f(r,"name"),100,"--name")}}let c=await rc(await X(s),a,f(r,"api-name"));Jn(c.notes),k(z(e==="system"?ac(c.rows):cc(c.rows),s.render))}}}var uc=[tm,nm,om,rm,sm,ic("system"),cn("system","new"),cn("system","update"),cn("system","delete"),ic("function"),cn("function","new"),cn("function","update"),cn("function","delete")];import{mkdir as gm}from"node:fs/promises";import{execFile as am,spawn as cm}from"node:child_process";import{realpathSync as lm}from"node:fs";import{join as dc,sep as kt}from"node:path";var le="2.1.0";var xo="@fui-org/fui-cli",um=3600000,dm=5000;function pc(e=process.env){return dc(rt(e),"..","update.json")}async function Kn(e=process.env){try{let t=await Yt(pc(e));if(t&&typeof t==="object"&&!Array.isArray(t))return t}catch{}return{}}async function ut(e,t=process.env){await F(pc(t),`${JSON.stringify(e,null,2)}
76
- `)}var pm=`${kt}node_modules${kt}@fui-org${kt}fui-cli${kt}`,mm=[[`${kt}install${kt}global`,"bun",!1],[`${kt}lib`,"npm",!1],[`${kt}npm`,"npm",!0]];function _o(e=ht()){let t;try{t=lm(e)}catch{return null}for(let[n,o,s]of mm){let r=`${n}${pm}`,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 Mo(e=ht()){return _o(e)?.dir??null}function Oo(e,t){return e==="bun"?`bun add -g ${xo}@${t}`:`npm install -g ${xo}@${t}`}function cs(e,t,n=process.env){return new Promise((o)=>{am(e,t,{env:n,shell:process.platform==="win32",maxBuffer:16777216,windowsHide:!0},(s,r,i)=>o({ok:!s,output:`${r}${i}`.trim()}))})}async function ls(e=process.env){let t=e.FUI_AUTO_UPDATE?.trim().toLowerCase();if(t)return!["0","off","false","no"].includes(t);try{return(await gt(e))["auto-update"]!=="off"}catch{return!0}}async function us(e=process.env){let t=(e.FUI_UPDATE_REGISTRY||"https://registry.npmjs.org").replace(/\/+$/,""),n=await fetch(`${t}/${xo.replace("/","%2F")}/latest`,{signal:AbortSignal.timeout(dm),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");return o.version}function ds(e,t=le){return Ci(e,t)===1}async function hm(e,t){let n=`${xo}@${t}`;if(e.manager==="npm")return cs("npm",["install","-g","--prefix",e.prefix,n]);return cs("bun",["add","-g",n],{...process.env,BUN_INSTALL:e.prefix})}async function ps(e=process.env){let t=await Kn(e),n=new Date().toISOString(),o;try{o=await us(e)}catch(i){throw await ut({...t,checkedAt:n,error:i.message},e),i}if(!ds(o))return await ut({...t,checkedAt:n,latest:o,error:void 0},e),{latest:o,action:"current"};let s=_o();if(!s)return await ut({...t,checkedAt:n,latest:o,error:void 0},e),{latest:o,action:"manual",detail:`${Oo("bun",o)} (or ${Oo("npm",o)})`};let r=await hm(s,o);if(!r.ok)return await ut({...t,checkedAt:n,latest:o,error:r.output.slice(-500)},e),{latest:o,action:"failed",detail:r.output};return await ut({checkedAt:n,latest:o,updatedFrom:le,updatedTo:o,noticeShown:!1,skillsVersion:t.skillsVersion,skillsAgents:t.skillsAgents},e),await fm(e),{latest:o,action:"installed"}}async function fm(e){let t=Mo();if(!t)return;await cs(process.execPath,[dc(t,"dist","fui.js"),"skill","install"],{...e,FUI_AUTO_SKILLS:"off",FUI_AUTO_UPDATE:"off"})}async function mc(e=process.env){if(!Mo()||!await ls(e))return null;let t=await Kn(e),n=null;if(t.updatedTo===le&&t.noticeShown===!1)n=`# fui updated ${t.updatedFrom} -> ${le}`,t.noticeShown=!0,await ut(t,e);let o=t.checkedAt?Date.parse(t.checkedAt):0;if(Date.now()-o<um)return n;return await ut({...t,checkedAt:new Date().toISOString()},e),cm(process.execPath,[ht(),"update","--background"],{stdio:"ignore",env:{...e,FUI_AUTO_UPDATE:"off"},detached:!0,windowsHide:!0}).unref(),n}function hc(e){let t=e[0];if(!t)throw new m(`missing <key>; keys: ${go.join(", ")}`);if(!go.includes(t))throw new m(`unknown key "${t}"; keys: ${go.join(", ")}`);return t}async function wm(){let e=await ls(),t=process.env.FUI_AUTO_UPDATE?.trim(),n=(await gt())["auto-update"],o=t?"env":n?"config":"default",s=t?"FUI_AUTO_UPDATE":n?rt():"built-in (global bun or npm installs only)";return{key:"auto-update",value:e?"on":"off",source:o,from:s}}var ym={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 wo();T(`# config file: ${rt()}`),k(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 wm()],t))}},bm={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=hc(e),n=e[1];if(!n)throw new m(`missing <value> for ${t}`);let o;if(t==="workdir")o=Nn(n),await gm(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 Te(n,void 0,"api-token");else o=n.trim().replace(/\/+$/,"");let s=await gt();s[t]=o;let r=await kr(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])T(`# note: ${i} is set in this shell and overrides the config file`);k(`set ${t}${t==="api-token"?"":`=${o}`} in ${r}
77
- `)}},km={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=hc(e),n=await gt();delete n[t];let o=await kr(n);k(`unset ${t} in ${o}
78
- `)}},fc=[ym,bm,km];var vm={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?[So(o)]:await e.listProjects(),r=[];for(let i of s){let a;try{a=await e.load(i)}catch(c){T(`# ${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=tn(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){T("no connections; add one: fui db add <apiName> -p <project> --token -");return}k(z(r,n))}},$m={name:"db add",usage:`fui db add <apiName> -p <project> (--token - | --token-env VAR | --conn - | --conn-env VAR)
75
+ `)}var cm={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 Te(f(e.values,"token"),f(e.values,"token-env"),"--token"),n=f(e.values,"from");if(n&&t)throw new h("pass --from or --token/--token-env, not both");let o=await nc(await X(e),{db:f(e.values,"db"),userToken:t,tokenSource:n});Jn(o.notes)}},lm={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 h("missing --uid");let n=await Te(f(e.values,"pwd"),f(e.values,"pwd-env"),"--pwd");if(!n)throw new h("missing --pwd - or --pwd-env VAR");Jn((await tc(await X(e),{db:f(e.values,"db"),uid:t,password:n})).notes)}},um={name:"db alias list",usage:"fui db alias list [--search text] [--api-domain host] [--user-token-env VAR]",summary:"Databases you may manage on the acc system (APIName, SQLServer, DatabaseName); read-only",options:{search:{type:"string"},...ls},async run(e){let t=await oc(await X(e),{...await us(e),search:f(e.values,"search")});for(let n of t.notes)T(`# ${n}`);if(t.rows.length>0)b(z(t.rows,e.render))}},dm={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"},...ls},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 h("need <apiName>, --module-name, --sql-server, --database and --username");let a=await Te(f(t,"password"),f(t,"password-env"),"--password");if(!a)throw new h("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 h("--module-id must be an integer");let u=await rc(await X(e),{...await us(e),apiName:n,moduleName:o,sqlServer:s,database:r,username:i,password:a,moduleId:l});Jn(u.notes)}},pm={name:"db add-by-name",usage:"fui db add-by-name <apiName> [-p P] [--name connectionName] [--borrow-from <project/alias>] [--default] [--api-domain host] [--user-token-env VAR]",summary:"Connect by alias: look up SQLServer/Database on acc, borrow a SQL login, test, save, fetch schema",options:{name:{type:"string"},"borrow-from":{type:"string"},default:{type:"boolean"},...ls},async run(e){let t=e.positionals[0];if(!t)throw new h("missing <apiName>; see: fui db alias list");let n=await sc(await X(e),{...await us(e),apiName:t,connectionName:f(e.values,"name"),borrowFrom:f(e.values,"borrow-from"),makeDefault:e.values.default===!0});Jn(n.notes)}};function dc(e){return e.map((t)=>({sysRight:String(t.SysRight??""),note:String(t.Note??"")}))}function pc(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 mc={"api-name":{type:"string"}};function cs(e,t,n,o){let s=e===void 0?Number.NaN:Number(e);if(!Number.isInteger(s)||!n(s))throw new h(`${t} must be ${o}`);return s}function lc(e,t,n){if(e!==void 0&&e.length>t)throw new h(`${n} is at most ${t} characters`);return e}function uc(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:mc,async run(t){let{apiName:n,rows:o}=await ac(await X(t),e,f(t.values,"api-name"));if(T(`# ${e==="system"?"SystemRight":"FunctionRight"} of "${n}": ${o.length}`),o.length===0){T(`# none defined; create one: fui right ${e} new ...`);return}b(z(e==="system"?dc(o):pc(o),t.render))}}}function cn(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:{...mc,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:cs(i[0],"<level>",(l)=>l>=0&&l<=9,"an integer 0-9"),note:t==="delete"?void 0:lc(f(r,"note"),50,"--note")};else{let l=f(r,"code");a={kind:e,mode:t,functionId:t==="new"?0:cs(i[0],"<functionId>",(u)=>u>0,"a positive integer"),functionCode:t==="delete"&&l===void 0?0:cs(l,"--code",()=>!0,"an integer"),functionName:t==="delete"?void 0:lc(f(r,"name"),100,"--name")}}let c=await cc(await X(s),a,f(r,"api-name"));Jn(c.notes),b(z(e==="system"?dc(c.rows):pc(c.rows),s.render))}}}var hc=[cm,lm,um,dm,pm,uc("system"),cn("system","new"),cn("system","update"),cn("system","delete"),uc("function"),cn("function","new"),cn("function","update"),cn("function","delete")];import{mkdir as Sm}from"node:fs/promises";import{execFile as hm,spawn as fm}from"node:child_process";import{realpathSync as gm}from"node:fs";import{join as fc,sep as kt}from"node:path";var le="2.1.2";var _o="@fui-org/fui-cli",wm=3600000,ym=5000;function gc(e=process.env){return fc(rt(e),"..","update.json")}async function Kn(e=process.env){try{let t=await Yt(gc(e));if(t&&typeof t==="object"&&!Array.isArray(t))return t}catch{}return{}}async function ut(e,t=process.env){await L(gc(t),`${JSON.stringify(e,null,2)}
76
+ `)}var bm=`${kt}node_modules${kt}@fui-org${kt}fui-cli${kt}`,km=[[`${kt}install${kt}global`,"bun",!1],[`${kt}lib`,"npm",!1],[`${kt}npm`,"npm",!0]];function Lo(e=ht()){let t;try{t=gm(e)}catch{return null}for(let[n,o,s]of km){let r=`${n}${bm}`,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 Fo(e=ht()){return Lo(e)?.dir??null}function Mo(e,t){return e==="bun"?`bun add -g ${_o}@${t}`:`npm install -g ${_o}@${t}`}function ds(e,t,n=process.env){return new Promise((o)=>{hm(e,t,{env:n,shell:process.platform==="win32",maxBuffer:16777216,windowsHide:!0},(s,r,i)=>o({ok:!s,output:`${r}${i}`.trim()}))})}async function ps(e=process.env){let t=e.FUI_AUTO_UPDATE?.trim().toLowerCase();if(t)return!["0","off","false","no"].includes(t);try{return(await gt(e))["auto-update"]!=="off"}catch{return!0}}async function ms(e=process.env){let t=(e.FUI_UPDATE_REGISTRY||"https://registry.npmjs.org").replace(/\/+$/,""),n=await fetch(`${t}/${_o.replace("/","%2F")}/latest`,{signal:AbortSignal.timeout(ym),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");return o.version}function hs(e,t=le){return Ai(e,t)===1}async function vm(e,t){let n=`${_o}@${t}`;if(e.manager==="npm")return ds("npm",["install","-g","--prefix",e.prefix,n]);return ds("bun",["add","-g",n],{...process.env,BUN_INSTALL:e.prefix})}async function fs(e=process.env){let t=await Kn(e),n=new Date().toISOString(),o;try{o=await ms(e)}catch(i){throw await ut({...t,checkedAt:n,error:i.message},e),i}if(!hs(o))return await ut({...t,checkedAt:n,latest:o,error:void 0},e),{latest:o,action:"current"};let s=Lo();if(!s)return await ut({...t,checkedAt:n,latest:o,error:void 0},e),{latest:o,action:"manual",detail:`${Mo("bun",o)} (or ${Mo("npm",o)})`};let r=await vm(s,o);if(!r.ok)return await ut({...t,checkedAt:n,latest:o,error:r.output.slice(-500)},e),{latest:o,action:"failed",detail:r.output};return await ut({checkedAt:n,latest:o,updatedFrom:le,updatedTo:o,noticeShown:!1,skillsVersion:t.skillsVersion,skillsAgents:t.skillsAgents},e),await $m(e),{latest:o,action:"installed"}}async function $m(e){let t=Fo();if(!t)return;await ds(process.execPath,[fc(t,"dist","fui.js"),"skill","install"],{...e,FUI_AUTO_SKILLS:"off",FUI_AUTO_UPDATE:"off"})}async function wc(e=process.env){if(!Fo()||!await ps(e))return null;let t=await Kn(e),n=null;if(t.updatedTo===le&&t.noticeShown===!1)n=`# fui updated ${t.updatedFrom} -> ${le}`,t.noticeShown=!0,await ut(t,e);let o=t.checkedAt?Date.parse(t.checkedAt):0;if(Date.now()-o<wm)return n;return await ut({...t,checkedAt:new Date().toISOString()},e),fm(process.execPath,[ht(),"update","--background"],{stdio:"ignore",env:{...e,FUI_AUTO_UPDATE:"off"},detached:!0,windowsHide:!0}).unref(),n}function yc(e){let t=e[0];if(!t)throw new h(`missing <key>; keys: ${wo.join(", ")}`);if(!wo.includes(t))throw new h(`unknown key "${t}"; keys: ${wo.join(", ")}`);return t}async function jm(){let e=await ps(),t=process.env.FUI_AUTO_UPDATE?.trim(),n=(await gt())["auto-update"],o=t?"env":n?"config":"default",s=t?"FUI_AUTO_UPDATE":n?rt():"built-in (global bun or npm installs only)";return{key:"auto-update",value:e?"on":"off",source:o,from:s}}var Im={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 yo();T(`# config file: ${rt()}`),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 jm()],t))}},Cm={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=yc(e),n=e[1];if(!n)throw new h(`missing <value> for ${t}`);let o;if(t==="workdir")o=An(n),await Sm(o,{recursive:!0});else if(t==="auto-update"){if(n!=="on"&&n!=="off")throw new h("auto-update takes on or off");o=n}else if(t==="api-token")o=await Te(n,void 0,"api-token");else o=n.trim().replace(/\/+$/,"");let s=await gt();s[t]=o;let r=await $r(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])T(`# note: ${i} is set in this shell and overrides the config file`);b(`set ${t}${t==="api-token"?"":`=${o}`} in ${r}
77
+ `)}},Rm={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=yc(e),n=await gt();delete n[t];let o=await $r(n);b(`unset ${t} in ${o}
78
+ `)}},bc=[Im,Cm,Rm];var Tm={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?[jo(o)]:await e.listProjects(),r=[];for(let i of s){let a;try{a=await e.load(i)}catch(c){T(`# ${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=tn(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){T("no connections; add one: fui db add <apiName> -p <project> --token -");return}b(z(r,n))}},Pm={name:"db add",usage:`fui db add <apiName> -p <project> (--token - | --token-env VAR | --conn - | --conn-env VAR)
79
79
  [--name connectionName] [--api-domain host] [--sql-server host] [--database name] [--user-token-env VAR] [--default] [--no-test]
80
80
  <apiName> is the tAPI route alias (/{apiName}/Fn in module.json); --name is the local connection and _db/ folder,
81
- suggested from apiName + zone when omitted. The same server + database re-added keeps its name.`,summary:"Add or replace a connection: test, save, and (MSSQL) fetch the schema",options:{name:{type:"string"},token:{type:"string"},"token-env":{type:"string"},conn:{type:"string"},"conn-env":{type:"string"},"api-domain":{type:"string"},"sql-server":{type:"string"},database:{type:"string"},"user-token-env":{type:"string"},default:{type:"boolean"},"no-test":{type:"boolean"}},async run(e){let{values:t,positionals:n,targets:o}=e,s=n[0];if(!s)throw new m("missing <apiName>");let r=(await o.resolveProject(f(t,"project"))).projectId,i=await Te(f(t,"token"),f(t,"token-env"),"--token"),a=await Te(f(t,"conn"),f(t,"conn-env"),"--conn");if(i===void 0===(a===void 0))throw new m("pass exactly one of --token/--token-env or --conn/--conn-env");let c=await Ao(await X(e),r,{alias:s,connectionName:f(t,"name"),dbToken:i??Tr(a),apiDomain:f(t,"api-domain"),sqlServer:f(t,"sql-server"),database:f(t,"database"),userToken:await Te(void 0,f(t,"user-token-env"),"--user-token"),makeDefault:t.default===!0,test:t["no-test"]!==!0});k(`${c.notes.join(`
81
+ suggested from apiName + zone when omitted. The same server + database re-added keeps its name.`,summary:"Add or replace a connection: test, save, and (MSSQL) fetch the schema",options:{name:{type:"string"},token:{type:"string"},"token-env":{type:"string"},conn:{type:"string"},"conn-env":{type:"string"},"api-domain":{type:"string"},"sql-server":{type:"string"},database:{type:"string"},"user-token-env":{type:"string"},default:{type:"boolean"},"no-test":{type:"boolean"}},async run(e){let{values:t,positionals:n,targets:o}=e,s=n[0];if(!s)throw new h("missing <apiName>");let r=(await o.resolveProject(f(t,"project"))).projectId,i=await Te(f(t,"token"),f(t,"token-env"),"--token"),a=await Te(f(t,"conn"),f(t,"conn-env"),"--conn");if(i===void 0===(a===void 0))throw new h("pass exactly one of --token/--token-env or --conn/--conn-env");let c=await xo(await X(e),r,{alias:s,connectionName:f(t,"name"),dbToken:i??Er(a),apiDomain:f(t,"api-domain"),sqlServer:f(t,"sql-server"),database:f(t,"database"),userToken:await Te(void 0,f(t,"user-token-env"),"--user-token"),makeDefault:t.default===!0,test:t["no-test"]!==!0});b(`${c.notes.join(`
82
82
  `)}
83
- `)}},Sm={name:"db default",usage:"fui db default <project>/<connectionName>",summary:"Set the project default connection",async run(e){let t=e.positionals[0]??f(e.values,"db");if(!t)throw new m("missing <project>/<connectionName>");let n=await X(e),o=await e.store.resolveProject({db:t,project:n.project,contextProject:n.contextProject}),s=await e.store.setDefault(o,t.split("/").pop());k(`default ${o}/${s}
84
- `)}},jm={name:"db test",usage:"fui db test [--db ref]",summary:"Run SELECT 1 and report latency",async run(e){let t=await X(e),n=await e.store.resolve({db:f(e.values,"db"),project:t.project,contextProject:t.contextProject}),o=performance.now();await je(n,(r)=>r.query("SELECT 1 AS ok"));let s=Math.round(performance.now()-o);k(`ok ${n.projectId}/${n.alias} ${n.engine} ${s}ms
85
- `)}},Im={name:"db migrate",usage:"fui db migrate [-p project]",summary:"Convert v1 _db/_connections.json files to v2 (the old file goes to _history/)",async run({store:e,values:t}){let n=f(t,"project"),o=n?[So(n)]:await e.listProjects();if(o.length===0){T("no _db/_connections.json in the workspace");return}let s=0;for(let r of o)try{let i=await e.migrate(r);k(`${r} ${i==="migrated"?"migrated to v2":i==="current"?"already v2":"no connections file"}
86
- `)}catch(i){s++,k(`${r} error: ${i.message}
87
- `)}if(s>0)throw new m(`${s} project(s) not migrated; fix the files above and run again`)}},gc=[vm,$m,Im,Sm,jm];import{readdir as Dm}from"node:fs/promises";import{join as hs}from"node:path";import{readdir as Cm}from"node:fs/promises";import{basename as Rm,dirname as Tm,extname as Em,isAbsolute as Pm,join as Lo,relative as Fo,resolve as Nm,sep as Am}from"node:path";function Uo(e,t,n=process.cwd()){let o=Nm(n,t),s=Fo(e.root,o);if(!s||s.startsWith("..")||Pm(s))return null;let r=s.split(Am),i=r[0];if(i.startsWith("_")||r.length<2)return null;if(r[1]==="modules"&&r.length>=4){let c=r[2],l=Lo(e.root,i,"modules",c);return{abs:o,baseDir:l,rel:ke(Fo(l,o)),projectId:i,moduleId:c}}let a=Lo(e.root,i);return{abs:o,baseDir:a,rel:ke(Fo(a,o)),projectId:i}}var wc=(e)=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");async function ms(e,t){let n=Fo(e.root,t),o=Em(n),s=Rm(n,o),r=Lo(e.historyRoot(),Tm(n)),i=new RegExp(`^${wc(s)}_(\\d{8}_\\d{6})((?:_[A-Za-z0-9-]+)*)${wc(o)}$`),a;try{a=await Cm(r)}catch{return[]}return a.map((c)=>{let l=i.exec(c);return l?{stamp:l[1],suffix:l[2].replace(/^_/,""),path:Lo(r,c)}:null}).filter((c)=>c!==null).sort((c,l)=>l.stamp.localeCompare(c.stamp)||l.path.localeCompare(c.path))}function fs(e,t){let n=Uo(e.paths,t);if(!n)throw new m(`${t} is not inside a project of workspace ${e.paths.root}`);return n}var xm={name:"file where",usage:"fui file where [-p project -m module]",summary:"Show the workspace, resolved project/module, their folders and where each came from",options:{module:{type:"string",short:"m"}},async run(e){let{paths:t,workdir:n,session:o,targets:s,values:r,render:i}=e,a=[{key:"workspace",value:t.root,source:n.source},{key:"session",value:o.file,source:"FUI_SESSION"}];try{let c=await s.resolveProjectModule(f(r,"project"),f(r,"module"));a.push({key:"project",value:c.projectId,source:c.source},{key:"module",value:c.moduleId,source:c.source},{key:"module-dir",value:c.moduleDir,source:c.source})}catch{try{let c=await s.resolveProject(f(r,"project"));a.push({key:"project",value:c.projectId,source:c.source})}catch{a.push({key:"project",value:"",source:"none"})}}k(z(a,i))}};async function Om(e){try{return(await Dm(hs(e,"modules"),{withFileTypes:!0})).filter((n)=>n.isDirectory()).map((n)=>hs(e,"modules",n.name))}catch{return[]}}var _m={name:"file status",usage:"fui file status [-p project -m module] [--all]",summary:"Local files not matching the last publish/sync (ledger); --all = every checked-out module of the project",options:{module:{type:"string",short:"m"},all:{type:"boolean"}},async run(e){let{values:t,targets:n,paths:o,render:s}=e,r=[];if(t.all===!0){let{projectId:a}=await n.resolveProject(f(t,"project"));r.push(o.projectDir(a),...await Om(o.projectDir(a)))}else{let a=await n.resolveProjectModule(f(t,"project"),f(t,"module"));r.push(a.moduleDir)}let i=[];for(let a of r)for(let c of await Vn(a))i.push({dir:a,path:c.relPath,reason:c.reason,push:To(c.relPath)});T(`# ${i.length} unpublished file(s)`),k(z(i,s))}},Mm={name:"file history",usage:"fui file history <path>",summary:"List archived versions of a workspace file (newest first)",async run(e){let t=e.positionals[0];if(!t)throw new m("missing <path>");let n=fs(e,t),o=await ms(e.paths,n.abs);T(`# ${o.length} version(s) of ${n.rel}`),k(z(o.map((s)=>({...s,path:Nt(e.paths,s.path)})),e.render))}},Fm={name:"file restore",usage:"fui file restore <path> [--at YYYYMMDD_HHMMSS]",summary:"Copy an archived version back (current file archived first); local only, ledger untouched",options:{at:{type:"string"}},async run(e){let t=e.positionals[0];if(!t)throw new m("missing <path>");let n=fs(e,t),o=await ms(e.paths,n.abs),s=f(e.values,"at"),r=s?o.find((c)=>c.stamp===s):o[0];if(!r)throw new m(s?`no version ${s} of ${n.rel}; run: fui file history ${t}`:`no archived versions of ${n.rel}`);let i=await he(n.abs),a=i===null?null:await fe(e.paths,n.abs,{content:i});if(await F(n.abs,await G(r.path)),a)T(`# previous content archived: ${Nt(e.paths,a)}`);k(`restored ${n.rel} from ${r.stamp}; push it with: ${To(n.rel)}
88
- `)}},Lm={name:"file ledger",usage:"fui file ledger <path...> [--mark | --forget]",summary:"Show, mark or forget publish-ledger entries (_publishState.json)",options:{mark:{type:"boolean"},forget:{type:"boolean"}},async run(e){let{positionals:t,values:n,render:o}=e;if(t.length===0)throw new m("missing <path>");if(n.mark===!0&&n.forget===!0)throw new m("pass --mark or --forget, not both");let s=t.map((a)=>fs(e,a)),r=Map.groupBy(s,(a)=>a.baseDir);for(let[a,c]of r){let l=c.map((u)=>u.rel);if(n.mark===!0)await Pe(a,l);if(n.forget===!0)await wa(a,l)}let i=[];for(let[a,c]of r){let l={};try{l=JSON.parse(await G(hs(a,on)))}catch{}for(let u of c){let d=await he(u.abs),p=l[u.rel]??"",h=d===null?"missing":!p?"new":p===_t(d)?"published":"changed";i.push({path:u.rel,state:h,ledger:p,dir:a})}}k(z(i,o))}},yc=[xm,_m,Mm,Fm,Lm];import{rename as Ym}from"node:fs/promises";import{basename as Qm,dirname as Rs,join as He}from"node:path";function ln(e){return e==="text/css"||e==="text/javascript"}function Um(e){return(e||"").trim().replace(/[<>:"/\\|?*\x00-\x1F]/g,"-").replace(/\s+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"import-file"}function Gn(e,t,n){let o=n.startsWith(".")?n:`.${n}`;return`${Um(e)}-${t}${o}`}function vt(e){return e.map((t)=>({name:t.FileName,type:t.FileType,contentType:t.ContentType,fileID:t.fileID,scope:t.Scope,sort:t.FileSort??null,...ln(t.ContentType)?{filePath:`imports/${Gn(t.FileName,t.fileID,t.FileType)}`}:{}}))}function un(e,t,n=[],o="",s="",r=[]){let i={},a={ModuleName:e.ModuleName,data:[],watch:{},controls:[],set:{}},c=t.moduleUI;if(c&&c.trim())try{a=JSON.parse(c)}catch(l){throw Error(`module.json of module "${e.ModuleID}" from the server is not valid JSON: ${l.message}; nothing written, local copy kept. ${c.length} chars, starts with ${JSON.stringify(c.slice(0,200))}`)}i["module.json"]=JSON.stringify(a,null,2),i["script.js"]=o??"",i["style.css"]=s??"",i["header.html"]=t.mHeader??"",i["body.html"]=t.mBody??"",i["imports/_imports.json"]=JSON.stringify(vt(r.filter((l)=>l.Scope!=="project")),null,2),i["components/_components.json"]=JSON.stringify(n.map((l)=>({comID:l.comID,comName:l.comName})),null,2);for(let l of n)i[`components/${l.comName}.vue`]=l.comCode;return i}function gs(e,t,n,o,s){let r={};r["_projectInfo.json"]=JSON.stringify(e,null,2),r["project.json"]=JSON.stringify(t,null,2),r["modules/_modules.json"]=JSON.stringify(n,null,2),r["imports/_imports.json"]=JSON.stringify(vt(s),null,2),r["components/_components.json"]=JSON.stringify(o.map((i)=>({comID:i.comID,comName:i.comName})),null,2);for(let i of o)r[`components/${i.comName}.vue`]=i.comCode;return r}function bc(e,t){let n=[`${e}/`],o=new Set;for(let s of Object.keys(t).sort()){let r=s.split("/");for(let i=0;i<r.length-1;i++){let a=r.slice(0,i+1).join("/");if(!o.has(a))o.add(a),n.push(`${" ".repeat(i+1)}${r[i]}/`)}n.push(`${" ".repeat(r.length)}${r[r.length-1]}`)}return n.join(`
89
- `)}import{mkdir as dn,readdir as Ut,rm as Hm,stat as Vm,unlink as ws}from"node:fs/promises";import{basename as kc,dirname as Ho,join as ie}from"node:path";function Ht(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}async function L(e,t=""){try{return await G(e)}catch{return t}}async function ys(e,t){try{return await G(e)}catch(n){if(xn(n)==="ENOENT")throw new m(`local ${t} does not exist (${e}); run fui module get first, or check -p/-m. Nothing published (an empty publish would wipe the server copy)`);throw Error(`cannot read ${t} at ${e}: ${n.message}`)}}async function H(e,t){try{return JSON.parse(await G(e))}catch{return t}}async function St(e){try{return(await Vm(e)).isDirectory()}catch{return!1}}async function _e(e){try{return(await Ut(e,{withFileTypes:!0})).filter((n)=>n.isFile()&&n.name.endsWith(".vue")).map((n)=>n.name)}catch{return[]}}async function zn(e,t){let n=[];for(let[o,s]of Object.entries(t)){let r=ie(e,o);await dn(Ho(r),{recursive:!0}),await F(r,s),n.push(r)}return n}async function vc(e,t,n){let o=[],s;try{s=await Ut(t,{withFileTypes:!0})}catch{return o}for(let r of s){if(!r.isFile()||!r.name.endsWith(".vue")||n.has(r.name))continue;let i=ie(t,r.name);await fe(e,i,{suffix:"deleted"}),await ws(i),o.push(i)}return o}function $c(e){return new Set(Object.keys(e).filter((t)=>t.startsWith("components/")&&t.endsWith(".vue")).map((t)=>kc(t)))}async function bs(e,t,n){return await dn(t,{recursive:!0}),await vc(e,ie(t,"components"),$c(n)),Ne(e,t,n)}async function Sc(e,t,n,o,s){await dn(t,{recursive:!0});let r={"_moduleInfo.json":JSON.stringify({...n,HTMLOnly:o.HTMLOnly,Framework:o.Framework,mTitle:o.mTitle},null,2),...s},i=[];for(let l of["header.html","body.html"])if(!r[l])try{let u=await G(ie(t,l));if(u.trim())i.push(`${l}: server returned empty; local content (${u.length} chars) was overwritten. check the server SP if unexpected`)}catch{}let a=await vc(e,ie(t,"components"),$c(r)),c=await Ne(e,t,r);return{moduleDir:t,removedFiles:a,warnings:i,sync:c}}async function jc(e,t,n){let o=await H(ie(t,"components","_components.json"),[]),s=[];if(!Array.isArray(o))return s;for(let r of o)if(r?.comName&&!n.has(r.comName)){let i=ie(t,"components",`${r.comName}.vue`);try{await fe(e,i,{suffix:"deleted"}),await ws(i),s.push(`${r.comName}.vue`)}catch{}}return s}async function Xn(e,t,n){let o=ie(t,"imports"),s=[],r;try{r=await Ut(o,{withFileTypes:!0})}catch{return s}let i=new Set([...n].map((a)=>kc(a)));for(let a of r){if(!a.isFile()||a.name==="_imports.json"||i.has(a.name))continue;let c=ie(o,a.name);try{await fe(e,c,{suffix:"deleted"}),await ws(c),s.push(`imports/${a.name}`)}catch{}}return s}async function Ic(e,t,n){let o=ie(t,"modules"),s=[],r;try{r=await Ut(o,{withFileTypes:!0})}catch{return s}for(let i of r){if(!i.isDirectory()||n.has(i.name))continue;let a=ie(o,i.name);await vo(e,a,{suffix:"deleted"}),await Hm(a,{recursive:!0,force:!0}),s.push(i.name)}return s}function ks(e){if(!e||typeof e!=="object")return!1;let t=e;return typeof t.ModuleID==="string"&&t.mDomain!==void 0&&typeof t.ProjectID==="string"}function vs(e,t){return ie(e.projectDir(t),"modules","_modules.json")}async function Cc(e,t){let n=await H(vs(e,t),[]);return Array.isArray(n)?n.filter(ks):[]}async function Yn(e,t,n){let o=e.projectDir(t);await zn(o,{"modules/_modules.json":JSON.stringify(n,null,2)});for(let s of n){let r=ie(o,"modules",s.ModuleID,"_moduleInfo.json"),i=await H(r,null);if(!i)continue;await F(r,JSON.stringify({...i,...s},null,2))}return n.length}async function Vo(e,t,n,o){let s=vs(e,t),r=await H(s,[]),i=Array.isArray(r)?r.filter(ks):[],a=o??n.ModuleID,{mTitle:c,...l}=n,u=i.findIndex((d)=>d.ModuleID===a||n.id&&d.id===n.id);if(u>=0)i[u]={...i[u],...l};else i.push(l);await dn(Ho(s),{recursive:!0}),await F(s,JSON.stringify(i,null,2))}async function Rc(e,t,n){let o=vs(e,t),s=await H(o,[]),r=Array.isArray(s)?s.filter(ks):[],i=r.findIndex((a)=>a.ModuleID===n);if(i<0)return!1;return r.splice(i,1),await dn(Ho(o),{recursive:!0}),await F(o,JSON.stringify(r,null,2)),!0}async function Tc(e){try{return(await Ut(e.root,{withFileTypes:!0})).filter((n)=>n.isDirectory()&&!n.name.startsWith("_")).map((n)=>n.name)}catch{return[]}}async function Ec(e){let t=[],n;try{n=await Ut(e.root,{withFileTypes:!0})}catch{return t}for(let o of n){if(!o.isDirectory()||o.name.startsWith("_"))continue;let s=ie(e.root,o.name);t.push({baseDir:s,projectId:o.name});try{for(let r of await Ut(ie(s,"modules"),{withFileTypes:!0}))if(r.isDirectory())t.push({baseDir:ie(s,"modules",r.name),projectId:o.name,moduleId:r.name})}catch{}}return t}async function $s(e,t){let n=[];for(let o of await Ec(e)){let s=await H(ie(o.baseDir,"imports/_imports.json"),[]),r=Array.isArray(s)?s.find((i)=>i?.fileID===t):void 0;if(r)n.push({...o,entry:r})}return n}async function Ss(e,t,n){let o=[];for(let{baseDir:s}of await Ec(e)){let r=await H(ie(s,"imports/_imports.json"),[]),i=Array.isArray(r)?r.find((c)=>c?.fileID===t&&c?.filePath):void 0;if(!i?.filePath)continue;let a=ie(s,i.filePath);try{await dn(Ho(a),{recursive:!0}),await F(a,n),o.push(a)}catch{}}return o}var Wm=20,qm=50;function js(e,t){let n=t==="module"?qm:Wm;if(e===void 0)return{effectiveSort:n,note:`fileSort not given; using ${n} (${t} imports must load after the framework)`};if(e<n)return{effectiveSort:n,note:`fileSort ${e} raised to ${n}: lower values load before the framework (~1-10) and can break the page`};return{effectiveSort:e,note:""}}function jt(e){return JSON.stringify(e??{},null,2)}function $t(e){return e.replace(/\r\n/g,`
90
- `).trim()}var Bm=["module.json","header.html","body.html","script.js","style.css"];async function Wo(e){let t=[];for(let o of Bm)t.push(`${o}
91
- ${await L(ie(e,o))}`);let n=ie(e,"components");for(let o of(await _e(n)).slice().sort())t.push(`components/${o}
92
- ${await L(ie(n,o))}`);return _t(t.join(`
83
+ `)}},Em={name:"db default",usage:"fui db default <project>/<connectionName>",summary:"Set the project default connection",async run(e){let t=e.positionals[0]??f(e.values,"db");if(!t)throw new h("missing <project>/<connectionName>");let n=await X(e),o=await e.store.resolveProject({db:t,project:n.project,contextProject:n.contextProject}),s=await e.store.setDefault(o,t.split("/").pop());b(`default ${o}/${s}
84
+ `)}},Am={name:"db test",usage:"fui db test [--db ref]",summary:"Run SELECT 1 and report latency",async run(e){let t=await X(e),n=await e.store.resolve({db:f(e.values,"db"),project:t.project,contextProject:t.contextProject}),o=performance.now();await je(n,(r)=>r.query("SELECT 1 AS ok"));let s=Math.round(performance.now()-o);b(`ok ${n.projectId}/${n.alias} ${n.engine} ${s}ms
85
+ `)}},Nm={name:"db migrate",usage:"fui db migrate [-p project]",summary:"Convert v1 _db/_connections.json files to v2 (the old file goes to _history/)",async run({store:e,values:t}){let n=f(t,"project"),o=n?[jo(n)]:await e.listProjects();if(o.length===0){T("no _db/_connections.json in the workspace");return}let s=0;for(let r of o)try{let i=await e.migrate(r);b(`${r} ${i==="migrated"?"migrated to v2":i==="current"?"already v2":"no connections file"}
86
+ `)}catch(i){s++,b(`${r} error: ${i.message}
87
+ `)}if(s>0)throw new h(`${s} project(s) not migrated; fix the files above and run again`)}},kc=[Tm,Pm,Nm,Em,Am];import{readdir as Um}from"node:fs/promises";import{join as ws}from"node:path";import{readdir as Dm}from"node:fs/promises";import{basename as xm,dirname as Om,extname as _m,isAbsolute as Mm,join as Ho,relative as Uo,resolve as Lm,sep as Fm}from"node:path";function Vo(e,t,n=process.cwd()){let o=Lm(n,t),s=Uo(e.root,o);if(!s||s.startsWith("..")||Mm(s))return null;let r=s.split(Fm),i=r[0];if(i.startsWith("_")||r.length<2)return null;if(r[1]==="modules"&&r.length>=4){let c=r[2],l=Ho(e.root,i,"modules",c);return{abs:o,baseDir:l,rel:ke(Uo(l,o)),projectId:i,moduleId:c}}let a=Ho(e.root,i);return{abs:o,baseDir:a,rel:ke(Uo(a,o)),projectId:i}}var vc=(e)=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");async function gs(e,t){let n=Uo(e.root,t),o=_m(n),s=xm(n,o),r=Ho(e.historyRoot(),Om(n)),i=new RegExp(`^${vc(s)}_(\\d{8}_\\d{6})((?:_[A-Za-z0-9-]+)*)${vc(o)}$`),a;try{a=await Dm(r)}catch{return[]}return a.map((c)=>{let l=i.exec(c);return l?{stamp:l[1],suffix:l[2].replace(/^_/,""),path:Ho(r,c)}:null}).filter((c)=>c!==null).sort((c,l)=>l.stamp.localeCompare(c.stamp)||l.path.localeCompare(c.path))}function ys(e,t){let n=Vo(e.paths,t);if(!n)throw new h(`${t} is not inside a project of workspace ${e.paths.root}`);return n}var Hm={name:"file where",usage:"fui file where [-p project -m module]",summary:"Show the workspace, resolved project/module, their folders and where each came from",options:{module:{type:"string",short:"m"}},async run(e){let{paths:t,workdir:n,session:o,targets:s,values:r,render:i}=e,a=[{key:"workspace",value:t.root,source:n.source},{key:"session",value:o.file,source:"FUI_SESSION"}];try{let c=await s.resolveProjectModule(f(r,"project"),f(r,"module"));a.push({key:"project",value:c.projectId,source:c.source},{key:"module",value:c.moduleId,source:c.source},{key:"module-dir",value:c.moduleDir,source:c.source})}catch{try{let c=await s.resolveProject(f(r,"project"));a.push({key:"project",value:c.projectId,source:c.source})}catch{a.push({key:"project",value:"",source:"none"})}}b(z(a,i))}};async function Vm(e){try{return(await Um(ws(e,"modules"),{withFileTypes:!0})).filter((n)=>n.isDirectory()).map((n)=>ws(e,"modules",n.name))}catch{return[]}}var Wm={name:"file status",usage:"fui file status [-p project -m module] [--all]",summary:"Local files not matching the last publish/sync (ledger); --all = every checked-out module of the project",options:{module:{type:"string",short:"m"},all:{type:"boolean"}},async run(e){let{values:t,targets:n,paths:o,render:s}=e,r=[];if(t.all===!0){let{projectId:a}=await n.resolveProject(f(t,"project"));r.push(o.projectDir(a),...await Vm(o.projectDir(a)))}else{let a=await n.resolveProjectModule(f(t,"project"),f(t,"module"));r.push(a.moduleDir)}let i=[];for(let a of r)for(let c of await Vn(a))i.push({dir:a,path:c.relPath,reason:c.reason,push:Po(c.relPath)});T(`# ${i.length} unpublished file(s)`),b(z(i,s))}},qm={name:"file history",usage:"fui file history <path>",summary:"List archived versions of a workspace file (newest first)",async run(e){let t=e.positionals[0];if(!t)throw new h("missing <path>");let n=ys(e,t),o=await gs(e.paths,n.abs);T(`# ${o.length} version(s) of ${n.rel}`),b(z(o.map((s)=>({...s,path:At(e.paths,s.path)})),e.render))}},Bm={name:"file restore",usage:"fui file restore <path> [--at YYYYMMDD_HHMMSS]",summary:"Copy an archived version back (current file archived first); local only, ledger untouched",options:{at:{type:"string"}},async run(e){let t=e.positionals[0];if(!t)throw new h("missing <path>");let n=ys(e,t),o=await gs(e.paths,n.abs),s=f(e.values,"at"),r=s?o.find((c)=>c.stamp===s):o[0];if(!r)throw new h(s?`no version ${s} of ${n.rel}; run: fui file history ${t}`:`no archived versions of ${n.rel}`);let i=await he(n.abs),a=i===null?null:await fe(e.paths,n.abs,{content:i});if(await L(n.abs,await G(r.path)),a)T(`# previous content archived: ${At(e.paths,a)}`);b(`restored ${n.rel} from ${r.stamp}; push it with: ${Po(n.rel)}
88
+ `)}},Jm={name:"file ledger",usage:"fui file ledger <path...> [--mark | --forget]",summary:"Show, mark or forget publish-ledger entries (_publishState.json)",options:{mark:{type:"boolean"},forget:{type:"boolean"}},async run(e){let{positionals:t,values:n,render:o}=e;if(t.length===0)throw new h("missing <path>");if(n.mark===!0&&n.forget===!0)throw new h("pass --mark or --forget, not both");let s=t.map((a)=>ys(e,a)),r=Map.groupBy(s,(a)=>a.baseDir);for(let[a,c]of r){let l=c.map((u)=>u.rel);if(n.mark===!0)await Ee(a,l);if(n.forget===!0)await $a(a,l)}let i=[];for(let[a,c]of r){let l={};try{l=JSON.parse(await G(ws(a,on)))}catch{}for(let u of c){let d=await he(u.abs),p=l[u.rel]??"",m=d===null?"missing":!p?"new":p===_t(d)?"published":"changed";i.push({path:u.rel,state:m,ledger:p,dir:a})}}b(z(i,o))}},$c=[Hm,Wm,qm,Bm,Jm];import{rename as rh}from"node:fs/promises";import{basename as sh,dirname as Es,join as He}from"node:path";function ln(e){return e==="text/css"||e==="text/javascript"}function Km(e){return(e||"").trim().replace(/[<>:"/\\|?*\x00-\x1F]/g,"-").replace(/\s+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"import-file"}function Gn(e,t,n){let o=n.startsWith(".")?n:`.${n}`;return`${Km(e)}-${t}${o}`}function vt(e){return e.map((t)=>({name:t.FileName,type:t.FileType,contentType:t.ContentType,fileID:t.fileID,scope:t.Scope,sort:t.FileSort??null,...ln(t.ContentType)?{filePath:`imports/${Gn(t.FileName,t.fileID,t.FileType)}`}:{}}))}function un(e,t,n=[],o="",s="",r=[]){let i={},a={ModuleName:e.ModuleName,data:[],watch:{},controls:[],set:{}},c=t.moduleUI;if(c&&c.trim())try{a=JSON.parse(c)}catch(l){throw Error(`module.json of module "${e.ModuleID}" from the server is not valid JSON: ${l.message}; nothing written, local copy kept. ${c.length} chars, starts with ${JSON.stringify(c.slice(0,200))}`)}i["module.json"]=JSON.stringify(a,null,2),i["script.js"]=o??"",i["style.css"]=s??"",i["header.html"]=t.mHeader??"",i["body.html"]=t.mBody??"",i["imports/_imports.json"]=JSON.stringify(vt(r.filter((l)=>l.Scope!=="project")),null,2),i["components/_components.json"]=JSON.stringify(n.map((l)=>({comID:l.comID,comName:l.comName})),null,2);for(let l of n)i[`components/${l.comName}.vue`]=l.comCode;return i}function bs(e,t,n,o,s){let r={};r["_projectInfo.json"]=JSON.stringify(e,null,2),r["project.json"]=JSON.stringify(t,null,2),r["modules/_modules.json"]=JSON.stringify(n,null,2),r["imports/_imports.json"]=JSON.stringify(vt(s),null,2),r["components/_components.json"]=JSON.stringify(o.map((i)=>({comID:i.comID,comName:i.comName})),null,2);for(let i of o)r[`components/${i.comName}.vue`]=i.comCode;return r}function Sc(e,t){let n=[`${e}/`],o=new Set;for(let s of Object.keys(t).sort()){let r=s.split("/");for(let i=0;i<r.length-1;i++){let a=r.slice(0,i+1).join("/");if(!o.has(a))o.add(a),n.push(`${" ".repeat(i+1)}${r[i]}/`)}n.push(`${" ".repeat(r.length)}${r[r.length-1]}`)}return n.join(`
89
+ `)}import{mkdir as dn,readdir as Ut,rm as Gm,stat as zm,unlink as ks}from"node:fs/promises";import{basename as jc,dirname as Wo,join as ie}from"node:path";function Ht(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}async function F(e,t=""){try{return await G(e)}catch{return t}}async function vs(e,t){try{return await G(e)}catch(n){if(xn(n)==="ENOENT")throw new h(`local ${t} does not exist (${e}); run fui module get first, or check -p/-m. Nothing published (an empty publish would wipe the server copy)`);throw Error(`cannot read ${t} at ${e}: ${n.message}`)}}async function H(e,t){try{return JSON.parse(await G(e))}catch{return t}}async function St(e){try{return(await zm(e)).isDirectory()}catch{return!1}}async function _e(e){try{return(await Ut(e,{withFileTypes:!0})).filter((n)=>n.isFile()&&n.name.endsWith(".vue")).map((n)=>n.name)}catch{return[]}}async function zn(e,t){let n=[];for(let[o,s]of Object.entries(t)){let r=ie(e,o);await dn(Wo(r),{recursive:!0}),await L(r,s),n.push(r)}return n}async function Ic(e,t,n){let o=[],s;try{s=await Ut(t,{withFileTypes:!0})}catch{return o}for(let r of s){if(!r.isFile()||!r.name.endsWith(".vue")||n.has(r.name))continue;let i=ie(t,r.name);await fe(e,i,{suffix:"deleted"}),await ks(i),o.push(i)}return o}function Cc(e){return new Set(Object.keys(e).filter((t)=>t.startsWith("components/")&&t.endsWith(".vue")).map((t)=>jc(t)))}async function $s(e,t,n){return await dn(t,{recursive:!0}),await Ic(e,ie(t,"components"),Cc(n)),Ae(e,t,n)}async function Rc(e,t,n,o,s){await dn(t,{recursive:!0});let r={"_moduleInfo.json":JSON.stringify({...n,HTMLOnly:o.HTMLOnly,Framework:o.Framework,mTitle:o.mTitle},null,2),...s},i=[];for(let l of["header.html","body.html"])if(!r[l])try{let u=await G(ie(t,l));if(u.trim())i.push(`${l}: server returned empty; local content (${u.length} chars) was overwritten. check the server SP if unexpected`)}catch{}let a=await Ic(e,ie(t,"components"),Cc(r)),c=await Ae(e,t,r);return{moduleDir:t,removedFiles:a,warnings:i,sync:c}}async function Tc(e,t,n){let o=await H(ie(t,"components","_components.json"),[]),s=[];if(!Array.isArray(o))return s;for(let r of o)if(r?.comName&&!n.has(r.comName)){let i=ie(t,"components",`${r.comName}.vue`);try{await fe(e,i,{suffix:"deleted"}),await ks(i),s.push(`${r.comName}.vue`)}catch{}}return s}async function Xn(e,t,n){let o=ie(t,"imports"),s=[],r;try{r=await Ut(o,{withFileTypes:!0})}catch{return s}let i=new Set([...n].map((a)=>jc(a)));for(let a of r){if(!a.isFile()||a.name==="_imports.json"||i.has(a.name))continue;let c=ie(o,a.name);try{await fe(e,c,{suffix:"deleted"}),await ks(c),s.push(`imports/${a.name}`)}catch{}}return s}async function Pc(e,t,n){let o=ie(t,"modules"),s=[],r;try{r=await Ut(o,{withFileTypes:!0})}catch{return s}for(let i of r){if(!i.isDirectory()||n.has(i.name))continue;let a=ie(o,i.name);await $o(e,a,{suffix:"deleted"}),await Gm(a,{recursive:!0,force:!0}),s.push(i.name)}return s}function Ss(e){if(!e||typeof e!=="object")return!1;let t=e;return typeof t.ModuleID==="string"&&t.mDomain!==void 0&&typeof t.ProjectID==="string"}function js(e,t){return ie(e.projectDir(t),"modules","_modules.json")}async function Ec(e,t){let n=await H(js(e,t),[]);return Array.isArray(n)?n.filter(Ss):[]}async function Yn(e,t,n){let o=e.projectDir(t);await zn(o,{"modules/_modules.json":JSON.stringify(n,null,2)});for(let s of n){let r=ie(o,"modules",s.ModuleID,"_moduleInfo.json"),i=await H(r,null);if(!i)continue;await L(r,JSON.stringify({...i,...s},null,2))}return n.length}async function qo(e,t,n,o){let s=js(e,t),r=await H(s,[]),i=Array.isArray(r)?r.filter(Ss):[],a=o??n.ModuleID,{mTitle:c,...l}=n,u=i.findIndex((d)=>d.ModuleID===a||n.id&&d.id===n.id);if(u>=0)i[u]={...i[u],...l};else i.push(l);await dn(Wo(s),{recursive:!0}),await L(s,JSON.stringify(i,null,2))}async function Ac(e,t,n){let o=js(e,t),s=await H(o,[]),r=Array.isArray(s)?s.filter(Ss):[],i=r.findIndex((a)=>a.ModuleID===n);if(i<0)return!1;return r.splice(i,1),await dn(Wo(o),{recursive:!0}),await L(o,JSON.stringify(r,null,2)),!0}async function Nc(e){try{return(await Ut(e.root,{withFileTypes:!0})).filter((n)=>n.isDirectory()&&!n.name.startsWith("_")).map((n)=>n.name)}catch{return[]}}async function Dc(e){let t=[],n;try{n=await Ut(e.root,{withFileTypes:!0})}catch{return t}for(let o of n){if(!o.isDirectory()||o.name.startsWith("_"))continue;let s=ie(e.root,o.name);t.push({baseDir:s,projectId:o.name});try{for(let r of await Ut(ie(s,"modules"),{withFileTypes:!0}))if(r.isDirectory())t.push({baseDir:ie(s,"modules",r.name),projectId:o.name,moduleId:r.name})}catch{}}return t}async function Is(e,t){let n=[];for(let o of await Dc(e)){let s=await H(ie(o.baseDir,"imports/_imports.json"),[]),r=Array.isArray(s)?s.find((i)=>i?.fileID===t):void 0;if(r)n.push({...o,entry:r})}return n}async function Cs(e,t,n){let o=[];for(let{baseDir:s}of await Dc(e)){let r=await H(ie(s,"imports/_imports.json"),[]),i=Array.isArray(r)?r.find((c)=>c?.fileID===t&&c?.filePath):void 0;if(!i?.filePath)continue;let a=ie(s,i.filePath);try{await dn(Wo(a),{recursive:!0}),await L(a,n),o.push(a)}catch{}}return o}var Xm=20,Ym=50;function Rs(e,t){let n=t==="module"?Ym:Xm;if(e===void 0)return{effectiveSort:n,note:`fileSort not given; using ${n} (${t} imports must load after the framework)`};if(e<n)return{effectiveSort:n,note:`fileSort ${e} raised to ${n}: lower values load before the framework (~1-10) and can break the page`};return{effectiveSort:e,note:""}}function jt(e){return JSON.stringify(e??{},null,2)}function $t(e){return e.replace(/\r\n/g,`
90
+ `).trim()}var Qm=["module.json","header.html","body.html","script.js","style.css"];async function Bo(e){let t=[];for(let o of Qm)t.push(`${o}
91
+ ${await F(ie(e,o))}`);let n=ie(e,"components");for(let o of(await _e(n)).slice().sort())t.push(`components/${o}
92
+ ${await F(ie(n,o))}`);return _t(t.join(`
93
93
  \x00
94
- `))}function Pc(e,t,n){let o=e.trim(),s=t.trim(),r=n,i=o.match(/^<head[^>]*>([\s\S]*)<\/head>\s*$/i);if(i)o=i[1].trim();let a=o.match(/<title[^>]*>([\s\S]*?)<\/title>/i);if(a)r=a[1].replace(/\s+/g," ").trim(),o=o.replace(/<title[^>]*>[\s\S]*?<\/title>\s*/i,"").trim();let c=s.match(/^<body[^>]*>([\s\S]*)<\/body>\s*$/i);if(c)s=c[1].trim();return{pageHeader:o,pageBody:s,mTitle:r}}function Nc(e,t){let n=[],o=new Map(e.map((r)=>[r.comName,r.comCode])),s=new Set(Object.keys(t));for(let[r,i]of Object.entries(t)){let a=o.get(r);if(a===void 0)n.push(`added component ${r}`);else if($t(a)!==$t(i))n.push(`updated component ${r}`)}for(let r of e)if(!s.has(r.comName))n.push(`deleted component ${r.comName}`);return n}function Is(e){if(!Ht(e))throw new m("project.json must be a JSON object at the top level");if(Ht(e.data))return{config:e.data,wasEnveloped:!0};return{config:e,wasEnveloped:!1}}function qo(e){let t=0,n=0,o=new Set;try{let s=e.moduleUI?JSON.parse(e.moduleUI):{};t=(s.controls||[]).length,n=(s.data||[]).length;for(let r of s.controls||[])for(let i of r.rows||[])for(let a of i.cols||[])if(a.el)o.add(a.el)}catch{}return[`framework ${e.Framework}, htmlOnly ${e.HTMLOnly?"yes":"no"}, controls ${t}, data keys ${n}, referenced components ${o.size}`]}import{join as ae}from"node:path";function It(e){if(!e)throw Error("component source is empty");let t=/<template(\s[^>]*)?>/i.exec(e);if(!t)throw Error("no <template> in the source");let n=t.index+t[0].length,o=e.toLowerCase(),s=Km(e,o,n);if(s===-1)throw Error("no </template> closing the template block");let r=o.lastIndexOf("</script");if(r===-1)throw Error("no <script> in the source");let i=/<script(\s[^>]*)?>/gi;i.lastIndex=s;let a=i.exec(e);if(!a||a.index>=r)throw Error("no <script> in the source");let c=e.substring(n,s).trim(),l=Gm(e.substring(a.index+a[0].length,r).trim()),u=Jm(e,o,r);return{template:c,script:l,style:u}}function Jm(e,t,n){let o=/<style(\s[^>]*)?>/i.exec(e.slice(n));if(!o)return"";let s=n+o.index+o[0].length,r=t.lastIndexOf("</style");if(r===-1||r<s)return"";return e.substring(s,r).trim()}function Km(e,t,n){let o=/<!--|<\/?([a-zA-Z][a-zA-Z0-9-]*)/g;o.lastIndex=n;let s=1,r=e.length;for(let i=o.exec(e);i!==null;i=o.exec(e)){if(i[0]==="<!--"){let d=e.indexOf("-->",o.lastIndex);if(d===-1)return-1;o.lastIndex=d+3;continue}let a=e.charAt(i.index+1)==="/",c=i[1].toLowerCase(),l=o.lastIndex,u=!1;while(l<r){let d=e.charAt(l);if(d==='"'||d==="'"){let p=e.indexOf(d,l+1);if(p===-1)return-1;l=p+1;continue}if(d===">"){u=e.charAt(l-1)==="/",l++;break}l++}if(o.lastIndex=l,c==="template"){if(a){if(s--,s===0)return i.index}else if(!u)s++;continue}if(!a&&(c==="script"||c==="style"||c==="textarea"||c==="title")){let d=t.indexOf(`</${c}`,l);if(d===-1)return-1;let p=e.indexOf(">",d);if(p===-1)return-1;o.lastIndex=p+1}}return-1}function Gm(e){let t=e.match(/export\s+default\s*\{/);if(!t)throw Error('no "export default {" in the script');let n=e.lastIndexOf("}");if(n<=t.index)throw Error('no "}" closing "export default {"');return e.substring(t.index+t[0].length,n).trim()}function pn(e){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}function Ac(e){let t=(e??"").trim();if(!t)return{};let n=t.startsWith("//")?`https:${t}`:t;if(/^https?:\/\//i.test(n))try{let s=new URL(n);return{host:s.host,path:s.pathname||"/"}}catch{}let o=t.split(/[?#]/)[0];if(!o)return{};return{path:o.startsWith("/")?o:`/${o}`}}function zm(e){let t=e.trim();if(/^(https?:)?\/\//i.test(t)){let{host:o,path:s}=Ac(t);return o?{host:o,path:s}:null}let n=t.split("/")[0];if(n&&n.includes(".")){let{host:o,path:s}=Ac(`//${t}`);return o?{host:o,path:s}:null}return null}function Dc(e){let n=e.trim().replace(/^https?:\/\/[^/]+/i,"").split(/[?#]/,1)[0].replace(/^\/+|\/+$/g,"").split("/").filter(Boolean);if(n.length<2)throw new m(`target "${e}" must look like /projectId/moduleId`);return{projectHint:n[0],moduleHint:n.slice(1).join("/")}}function Xm(e){let t=[e.ModuleID,e.ModuleName,e.mDomain,e.url].filter((n)=>Boolean(n));return[...new Set(t.flatMap((n)=>[n.toLowerCase(),pn(n),n.replace(/^\/+|\/+$/g,"").toLowerCase()]))]}function Cs(e,t){let n=t.trim().toLowerCase().replace(/^\/+|\/+$/g,""),o=pn(t);return e.filter((s)=>{let r=Xm(s);return r.includes(n)||r.includes(o)})}function mn(e,t,n){if(n?.refreshed)return new m(`module "${t}" does not exist in project "${e}" (the module list was just refreshed from the server). check the name: fui module list -p ${e}, or create it: fui module new`);let o=n?.refreshError?` refreshing the list also failed: ${n.refreshError}.`:"";return new m(`module "${t}" is not in the local cache (${e}/modules/_modules.json).${o} run: fui module list -p ${e}`)}class Qn{paths;api;constructor(e,t){this.paths=e;this.api=t}baseDir(e,t){return t?ae(this.paths.projectDir(e),"modules",t):this.paths.projectDir(e)}async lookupModuleInfo(e,t){let n=t(await Cc(this.paths,e));if(n)return{found:n,refreshed:!1};let o;try{o=await this.api.listModules(e),await Yn(this.paths,e,o)}catch(s){return{found:null,refreshed:!1,refreshError:s.message}}return{found:t(o),refreshed:!0}}resolveModuleInfoFromCache(e,t){return this.lookupModuleInfo(e,(n)=>n.find((o)=>o.ModuleID===t)??null)}async refreshProjectInfoCache(e){let t=this.paths.projectDir(e);if(!await St(t))return null;let n=(await this.api.listProjects()).find((o)=>o.ProjectID===e);if(!n)return null;return await zn(t,{"_projectInfo.json":JSON.stringify({...n,fetchedAt:new Date().toISOString()},null,2)}),e}async projectInfoRefreshNote(e){if(typeof e!=="string"||!e.trim())return null;try{return await this.refreshProjectInfoCache(e)?`refreshed ${e}/_projectInfo.json`:null}catch{return`could not refresh ${e}/_projectInfo.json; run fui project sync -p ${e} if needed`}}async resolveProjectIdFromHint(e){let t=e.trim().toLowerCase(),n=pn(e),o=(await Tc(this.paths)).filter((a)=>a.toLowerCase()===t||pn(a)===n);if(o.length>1)throw new m(`project hint "${e}" matched several local project folders: ${o.join(", ")}`);if(o.length===1)return o[0];let s;try{s=await this.api.listProjects()}catch{return e}let r=s.filter((a)=>pn(a.ProjectName)===n||a.ProjectName.trim().toLowerCase()===t);if(r.length===1)return r[0].ProjectID;if(r.length>1)throw new m(`project hint "${e}" matched several projects by name: ${r.map((a)=>`${a.ProjectName} (${a.ProjectID})`).join(", ")}`);let i=s.filter((a)=>a.ProjectName.toLowerCase().includes(t)||pn(a.ProjectName).includes(n)).slice(0,10);if(i.length>0)throw new m(`no project matches "${e}" exactly; did you mean: ${i.map((a)=>`${a.ProjectName} (${a.ProjectID})`).join(", ")}`);return e}async resolveModuleTargetByUrl(e,t){let n=(t??"").split("/").filter(Boolean),s=(await this.api.getRuntimeModuleVersion(e,n[0]??"",n[1]??""))[0];if(!s||s.Status!==1||!s.ProjectID||!s.ModuleID)throw new m(`cannot resolve a module from domain "${e}"${t?` path "${t}"`:""} via FUI_Runtime_ModuleVersionSelect${s?` (Status=${s.Status})`:" (no result)"}; check the URL or look it up: fui project list / fui module list`);let r=await this.resolveModuleInfoFromCache(s.ProjectID,s.ModuleID);if(!r.found)throw mn(s.ProjectID,s.ModuleID,r);return{projectId:s.ProjectID,moduleId:s.ModuleID,moduleInfo:r.found,matchedBy:"runtime_lookup"}}async resolveProjectAndModuleHint(e){let t=zm(e);if(t)try{let o=await this.resolveModuleTargetByUrl(t.host,t.path);return{projectId:o.projectId,moduleHint:o.moduleId,matchedBy:o.matchedBy,moduleInfo:o.moduleInfo}}catch(o){try{let s=Dc(e);return{projectId:await this.resolveProjectIdFromHint(s.projectHint),moduleHint:s.moduleHint,matchedBy:"target_path"}}catch(s){throw new m(`cannot resolve target "${e}": as URL/domain: ${o.message}; as /projectId/moduleId: ${s.message}`)}}let n=Dc(e);return{projectId:await this.resolveProjectIdFromHint(n.projectHint),moduleHint:n.moduleHint,matchedBy:"target_path"}}async resolveModuleTarget(e,t,n){if(t&&n){let r=await this.resolveModuleInfoFromCache(t,n);if(!r.found)throw mn(t,n,r);return{projectId:t,moduleId:n,moduleInfo:r.found,matchedBy:"explicit_ids"}}if(!e)throw new m("give a target (/project/module or a URL) or both -p and -m");let o=await this.resolveProjectAndModuleHint(e);if(o.moduleInfo)return{projectId:o.projectId,moduleId:o.moduleInfo.ModuleID,moduleInfo:o.moduleInfo,matchedBy:o.matchedBy};let s=await this.lookupModuleInfo(o.projectId,(r)=>{let i=Cs(r,o.moduleHint);if(i.length>1)throw new m(`target "${e}" matched several modules in project ${o.projectId}: ${i.map((a)=>a.ModuleID).join(", ")}`);return i[0]??null});if(!s.found)throw mn(o.projectId,o.moduleHint,s);return{projectId:o.projectId,moduleId:s.found.ModuleID,moduleInfo:s.found,matchedBy:"target_path"}}async fetchModuleBundle(e){let{ProjectID:t,ModuleID:n}=e,[o,s,r,i,a]=await Promise.all([this.api.getModuleContent(t,n),this.api.getModuleComponents(t,n),this.api.getModuleScript(t,n),this.api.getModuleCSS(t,n),this.api.getModuleDependencies(t,n)]);return{moduleInfo:e,content:o,components:s,script:r,css:i,dependencies:a}}async fetchProjectBundle(e){let[t,n,o,s,r]=await Promise.all([this.api.listProjects(),this.api.listModules(e),this.api.getProjectJson(e),this.api.getModuleComponents(e),this.api.listImportFiles(e)]);return{projectInfo:{...t.find((c)=>c.ProjectID===e)??{ProjectID:e,ProjectName:e},fetchedAt:new Date().toISOString()},modules:n,projectJson:o,projectComponents:s,projectImports:r}}async downloadImportContents(e){let t={},n=[];for(let o of e){if(!ln(o.ContentType))continue;try{let r=(await this.api.getFileContent(o.fileID))?.data?.FileContent??"";t[`imports/${Gn(o.FileName,o.fileID,o.FileType)}`]=r}catch(s){n.push(`${o.FileName} (fileID ${o.fileID}): ${s?.message??String(s)}`)}}return{files:t,failures:n}}async uploadImportContents(e){let t=await H(ae(e,"imports/_imports.json"),[]);for(let n of t){if(!n.fileID||!n.filePath||!ln(n.contentType??""))continue;let o=await L(ae(e,n.filePath));if(o)await this.api.uploadFileContent(n.fileID,o)}}async syncComponentsLocal(e,t){let n=await this.api.getModuleComponents(e,t),o=n.map((i)=>({comID:i.comID,comName:i.comName})),s=this.baseDir(e,t),r=await jc(this.paths,s,new Set(o.map((i)=>i.comName)));return await zn(s,{"components/_components.json":JSON.stringify(o,null,2)}),{count:n.length,removed:r}}async syncImportsLocal(e,t){let n=await this.api.listImportFiles(e,t),o=t?n.filter((c)=>c.Scope!=="project"):n,s=this.baseDir(e,t),r=vt(o),i=new Set(r.map((c)=>c.filePath).filter((c)=>!!c)),a=await Xn(this.paths,s,i);return await zn(s,{"imports/_imports.json":JSON.stringify(r,null,2)}),{list:n,count:o.length,removed:a}}async publishStagedModuleDir(e){let t=await H(ae(e,"_moduleInfo.json"),null);if(!t)throw new m(`missing _moduleInfo.json in ${e}; run fui module get first`);let n=await L(ae(e,"module.json")),o;try{o=JSON.parse(n)}catch(g){throw new m(`module.json has invalid JSON syntax: ${g.message}; fix it before publishing`)}let s=await L(ae(e,"header.html")),r=await L(ae(e,"body.html")),i=await L(ae(e,"script.js")),a=await L(ae(e,"style.css")),{pageHeader:c,pageBody:l,mTitle:u}=Pc(s,r,t.mTitle??""),{ProjectID:d,ModuleID:p}=t;if(await this.api.updateModule(d,p,{moduleUI:o,title:u,pageHeader:c,pageBody:l}),i.trim())await this.api.updateModuleScript(d,p,i);if(a.trim())await this.api.updateModuleCSS(d,p,a);let h=ae(e,"components"),w=await H(ae(h,"_components.json"),[]),y=new Map(w.map((g)=>[g.comName,g.comID])),v=await _e(h),N=new Set(v.map((g)=>g.replace(/\.vue$/i,"")));for(let g of v){let b=g.replace(/\.vue$/i,""),{template:R,script:C,style:S}=It(await L(ae(h,g)));await this.api.updateComponent({ProjectID:d,ModuleID:p,ComID:y.get(b),ComName:b,ComTemplate:R,ComScript:C,ComCSS:S})}for(let g of w)if(!N.has(g.comName))await this.api.updateComponent({ProjectID:d,ModuleID:p,ComID:g.comID,ComName:"",ComTemplate:"",ComScript:""});await this.syncComponentsLocal(d,p),await this.uploadImportContents(e);let I=["module.json","header.html","body.html"];if(i.trim())I.push("script.js");if(a.trim())I.push("style.css");for(let g of v)I.push(ke(ae("components",g)));return await Pe(e,I).catch(()=>{return}),t}async previewStagedModuleDir(e){let t=await H(ae(e,"_moduleInfo.json"),null);if(!t)throw new m(`missing _moduleInfo.json in ${e}; run fui module get first`);let n=await this.fetchModuleBundle(t),o=un(n.moduleInfo,n.content,n.components,n.script,n.css,n.dependencies),s;try{s=JSON.parse(await L(ae(e,"module.json")))}catch(p){throw new m(`module.json has invalid JSON syntax: ${p.message}; fix it before previewing`)}let r=await L(ae(e,"header.html")),i=await L(ae(e,"body.html")),a=await L(ae(e,"script.js")),c=await L(ae(e,"style.css")),l=await H(ae(e,"imports/_imports.json"),[]),u={};for(let p of await _e(ae(e,"components")))u[p.replace(/\.vue$/i,"")]=await L(ae(e,"components",p));let d=[];if(jt(s)!==jt(JSON.parse(o["module.json"])))d.push("module.json will be updated");for(let p of["header.html","body.html","script.js","style.css"]){let h={"header.html":r,"body.html":i,"script.js":a,"style.css":c}[p];if($t(h)!==$t(o[p]||""))d.push(`${p} will be updated`)}if(jt(l)!==jt(JSON.parse(o["imports/_imports.json"]||"[]")))d.push("imports/_imports.json differs locally (metadata only, not sent)");return d.push(...Nc(n.components,u)),{moduleInfo:t,changes:d,remoteSummary:qo(n.content)}}}async function ce(e){return new Qn(e.paths,await e.api())}function xc(e){if(e.length===0)return[];return[`${e.length} import content(s) NOT downloaded; their local copies are stale even though imports/_imports.json is fresh. do not edit and publish them:`,...e.map((t)=>` - ${t}`),"re-run the sync, or fetch one: fui import get <fileId>"]}async function Vt(e,t){let n=await e.wasSkillLoaded();if(!n&&Zt())throw new Se(`[FUI_STRICT] ${t} blocked: skill not loaded this session; read the fui-skill references, run fui skill loaded, then retry`,"hard","error");return n?[]:["warning: skill not loaded this session; read fui-skill (fui skill get / references), then run: fui skill loaded"]}async function Oc(e,t){let n=await(await ce(e)).resolveModuleTarget(t);return{notes:[],data:`project ${n.projectId}
94
+ `))}function xc(e,t,n){let o=e.trim(),s=t.trim(),r=n,i=o.match(/^<head[^>]*>([\s\S]*)<\/head>\s*$/i);if(i)o=i[1].trim();let a=o.match(/<title[^>]*>([\s\S]*?)<\/title>/i);if(a)r=a[1].replace(/\s+/g," ").trim(),o=o.replace(/<title[^>]*>[\s\S]*?<\/title>\s*/i,"").trim();let c=s.match(/^<body[^>]*>([\s\S]*)<\/body>\s*$/i);if(c)s=c[1].trim();return{pageHeader:o,pageBody:s,mTitle:r}}function Oc(e,t){let n=[],o=new Map(e.map((r)=>[r.comName,r.comCode])),s=new Set(Object.keys(t));for(let[r,i]of Object.entries(t)){let a=o.get(r);if(a===void 0)n.push(`added component ${r}`);else if($t(a)!==$t(i))n.push(`updated component ${r}`)}for(let r of e)if(!s.has(r.comName))n.push(`deleted component ${r.comName}`);return n}function Ts(e){if(!Ht(e))throw new h("project.json must be a JSON object at the top level");if(Ht(e.data))return{config:e.data,wasEnveloped:!0};return{config:e,wasEnveloped:!1}}function Jo(e){let t=0,n=0,o=new Set;try{let s=e.moduleUI?JSON.parse(e.moduleUI):{};t=(s.controls||[]).length,n=(s.data||[]).length;for(let r of s.controls||[])for(let i of r.rows||[])for(let a of i.cols||[])if(a.el)o.add(a.el)}catch{}return[`framework ${e.Framework}, htmlOnly ${e.HTMLOnly?"yes":"no"}, controls ${t}, data keys ${n}, referenced components ${o.size}`]}import{join as ae}from"node:path";function It(e){if(!e)throw Error("component source is empty");let t=/<template(\s[^>]*)?>/i.exec(e);if(!t)throw Error("no <template> in the source");let n=t.index+t[0].length,o=e.toLowerCase(),s=eh(e,o,n);if(s===-1)throw Error("no </template> closing the template block");let r=o.lastIndexOf("</script");if(r===-1)throw Error("no <script> in the source");let i=/<script(\s[^>]*)?>/gi;i.lastIndex=s;let a=i.exec(e);if(!a||a.index>=r)throw Error("no <script> in the source");let c=e.substring(n,s).trim(),l=th(e.substring(a.index+a[0].length,r).trim()),u=Zm(e,o,r);return{template:c,script:l,style:u}}function Zm(e,t,n){let o=/<style(\s[^>]*)?>/i.exec(e.slice(n));if(!o)return"";let s=n+o.index+o[0].length,r=t.lastIndexOf("</style");if(r===-1||r<s)return"";return e.substring(s,r).trim()}function eh(e,t,n){let o=/<!--|<\/?([a-zA-Z][a-zA-Z0-9-]*)/g;o.lastIndex=n;let s=1,r=e.length;for(let i=o.exec(e);i!==null;i=o.exec(e)){if(i[0]==="<!--"){let d=e.indexOf("-->",o.lastIndex);if(d===-1)return-1;o.lastIndex=d+3;continue}let a=e.charAt(i.index+1)==="/",c=i[1].toLowerCase(),l=o.lastIndex,u=!1;while(l<r){let d=e.charAt(l);if(d==='"'||d==="'"){let p=e.indexOf(d,l+1);if(p===-1)return-1;l=p+1;continue}if(d===">"){u=e.charAt(l-1)==="/",l++;break}l++}if(o.lastIndex=l,c==="template"){if(a){if(s--,s===0)return i.index}else if(!u)s++;continue}if(!a&&(c==="script"||c==="style"||c==="textarea"||c==="title")){let d=t.indexOf(`</${c}`,l);if(d===-1)return-1;let p=e.indexOf(">",d);if(p===-1)return-1;o.lastIndex=p+1}}return-1}function th(e){let t=e.match(/export\s+default\s*\{/);if(!t)throw Error('no "export default {" in the script');let n=e.lastIndexOf("}");if(n<=t.index)throw Error('no "}" closing "export default {"');return e.substring(t.index+t[0].length,n).trim()}function pn(e){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}function _c(e){let t=(e??"").trim();if(!t)return{};let n=t.startsWith("//")?`https:${t}`:t;if(/^https?:\/\//i.test(n))try{let s=new URL(n);return{host:s.host,path:s.pathname||"/"}}catch{}let o=t.split(/[?#]/)[0];if(!o)return{};return{path:o.startsWith("/")?o:`/${o}`}}function nh(e){let t=e.trim();if(/^(https?:)?\/\//i.test(t)){let{host:o,path:s}=_c(t);return o?{host:o,path:s}:null}let n=t.split("/")[0];if(n&&n.includes(".")){let{host:o,path:s}=_c(`//${t}`);return o?{host:o,path:s}:null}return null}function Mc(e){let n=e.trim().replace(/^https?:\/\/[^/]+/i,"").split(/[?#]/,1)[0].replace(/^\/+|\/+$/g,"").split("/").filter(Boolean);if(n.length<2)throw new h(`target "${e}" must look like /projectId/moduleId`);return{projectHint:n[0],moduleHint:n.slice(1).join("/")}}function oh(e){let t=[e.ModuleID,e.ModuleName,e.mDomain,e.url].filter((n)=>Boolean(n));return[...new Set(t.flatMap((n)=>[n.toLowerCase(),pn(n),n.replace(/^\/+|\/+$/g,"").toLowerCase()]))]}function Ps(e,t){let n=t.trim().toLowerCase().replace(/^\/+|\/+$/g,""),o=pn(t);return e.filter((s)=>{let r=oh(s);return r.includes(n)||r.includes(o)})}function mn(e,t,n){if(n?.refreshed)return new h(`module "${t}" does not exist in project "${e}" (the module list was just refreshed from the server). check the name: fui module list -p ${e}, or create it: fui module new`);let o=n?.refreshError?` refreshing the list also failed: ${n.refreshError}.`:"";return new h(`module "${t}" is not in the local cache (${e}/modules/_modules.json).${o} run: fui module list -p ${e}`)}class Qn{paths;api;constructor(e,t){this.paths=e;this.api=t}baseDir(e,t){return t?ae(this.paths.projectDir(e),"modules",t):this.paths.projectDir(e)}async lookupModuleInfo(e,t){let n=t(await Ec(this.paths,e));if(n)return{found:n,refreshed:!1};let o;try{o=await this.api.listModules(e),await Yn(this.paths,e,o)}catch(s){return{found:null,refreshed:!1,refreshError:s.message}}return{found:t(o),refreshed:!0}}resolveModuleInfoFromCache(e,t){return this.lookupModuleInfo(e,(n)=>n.find((o)=>o.ModuleID===t)??null)}async refreshProjectInfoCache(e){let t=this.paths.projectDir(e);if(!await St(t))return null;let n=(await this.api.listProjects()).find((o)=>o.ProjectID===e);if(!n)return null;return await zn(t,{"_projectInfo.json":JSON.stringify({...n,fetchedAt:new Date().toISOString()},null,2)}),e}async projectInfoRefreshNote(e){if(typeof e!=="string"||!e.trim())return null;try{return await this.refreshProjectInfoCache(e)?`refreshed ${e}/_projectInfo.json`:null}catch{return`could not refresh ${e}/_projectInfo.json; run fui project sync -p ${e} if needed`}}async resolveProjectIdFromHint(e){let t=e.trim().toLowerCase(),n=pn(e),o=(await Nc(this.paths)).filter((a)=>a.toLowerCase()===t||pn(a)===n);if(o.length>1)throw new h(`project hint "${e}" matched several local project folders: ${o.join(", ")}`);if(o.length===1)return o[0];let s;try{s=await this.api.listProjects()}catch{return e}let r=s.filter((a)=>pn(a.ProjectName)===n||a.ProjectName.trim().toLowerCase()===t);if(r.length===1)return r[0].ProjectID;if(r.length>1)throw new h(`project hint "${e}" matched several projects by name: ${r.map((a)=>`${a.ProjectName} (${a.ProjectID})`).join(", ")}`);let i=s.filter((a)=>a.ProjectName.toLowerCase().includes(t)||pn(a.ProjectName).includes(n)).slice(0,10);if(i.length>0)throw new h(`no project matches "${e}" exactly; did you mean: ${i.map((a)=>`${a.ProjectName} (${a.ProjectID})`).join(", ")}`);return e}async resolveModuleTargetByUrl(e,t){let n=(t??"").split("/").filter(Boolean),s=(await this.api.getRuntimeModuleVersion(e,n[0]??"",n[1]??""))[0];if(!s||s.Status!==1||!s.ProjectID||!s.ModuleID)throw new h(`cannot resolve a module from domain "${e}"${t?` path "${t}"`:""} via FUI_Runtime_ModuleVersionSelect${s?` (Status=${s.Status})`:" (no result)"}; check the URL or look it up: fui project list / fui module list`);let r=await this.resolveModuleInfoFromCache(s.ProjectID,s.ModuleID);if(!r.found)throw mn(s.ProjectID,s.ModuleID,r);return{projectId:s.ProjectID,moduleId:s.ModuleID,moduleInfo:r.found,matchedBy:"runtime_lookup"}}async resolveProjectAndModuleHint(e){let t=nh(e);if(t)try{let o=await this.resolveModuleTargetByUrl(t.host,t.path);return{projectId:o.projectId,moduleHint:o.moduleId,matchedBy:o.matchedBy,moduleInfo:o.moduleInfo}}catch(o){try{let s=Mc(e);return{projectId:await this.resolveProjectIdFromHint(s.projectHint),moduleHint:s.moduleHint,matchedBy:"target_path"}}catch(s){throw new h(`cannot resolve target "${e}": as URL/domain: ${o.message}; as /projectId/moduleId: ${s.message}`)}}let n=Mc(e);return{projectId:await this.resolveProjectIdFromHint(n.projectHint),moduleHint:n.moduleHint,matchedBy:"target_path"}}async resolveModuleTarget(e,t,n){if(t&&n){let r=await this.resolveModuleInfoFromCache(t,n);if(!r.found)throw mn(t,n,r);return{projectId:t,moduleId:n,moduleInfo:r.found,matchedBy:"explicit_ids"}}if(!e)throw new h("give a target (/project/module or a URL) or both -p and -m");let o=await this.resolveProjectAndModuleHint(e);if(o.moduleInfo)return{projectId:o.projectId,moduleId:o.moduleInfo.ModuleID,moduleInfo:o.moduleInfo,matchedBy:o.matchedBy};let s=await this.lookupModuleInfo(o.projectId,(r)=>{let i=Ps(r,o.moduleHint);if(i.length>1)throw new h(`target "${e}" matched several modules in project ${o.projectId}: ${i.map((a)=>a.ModuleID).join(", ")}`);return i[0]??null});if(!s.found)throw mn(o.projectId,o.moduleHint,s);return{projectId:o.projectId,moduleId:s.found.ModuleID,moduleInfo:s.found,matchedBy:"target_path"}}async fetchModuleBundle(e){let{ProjectID:t,ModuleID:n}=e,[o,s,r,i,a]=await Promise.all([this.api.getModuleContent(t,n),this.api.getModuleComponents(t,n),this.api.getModuleScript(t,n),this.api.getModuleCSS(t,n),this.api.getModuleDependencies(t,n)]);return{moduleInfo:e,content:o,components:s,script:r,css:i,dependencies:a}}async fetchProjectBundle(e){let[t,n,o,s,r]=await Promise.all([this.api.listProjects(),this.api.listModules(e),this.api.getProjectJson(e),this.api.getModuleComponents(e),this.api.listImportFiles(e)]);return{projectInfo:{...t.find((c)=>c.ProjectID===e)??{ProjectID:e,ProjectName:e},fetchedAt:new Date().toISOString()},modules:n,projectJson:o,projectComponents:s,projectImports:r}}async downloadImportContents(e){let t={},n=[];for(let o of e){if(!ln(o.ContentType))continue;try{let r=(await this.api.getFileContent(o.fileID))?.data?.FileContent??"";t[`imports/${Gn(o.FileName,o.fileID,o.FileType)}`]=r}catch(s){n.push(`${o.FileName} (fileID ${o.fileID}): ${s?.message??String(s)}`)}}return{files:t,failures:n}}async uploadImportContents(e){let t=await H(ae(e,"imports/_imports.json"),[]);for(let n of t){if(!n.fileID||!n.filePath||!ln(n.contentType??""))continue;let o=await F(ae(e,n.filePath));if(o)await this.api.uploadFileContent(n.fileID,o)}}async syncComponentsLocal(e,t){let n=await this.api.getModuleComponents(e,t),o=n.map((i)=>({comID:i.comID,comName:i.comName})),s=this.baseDir(e,t),r=await Tc(this.paths,s,new Set(o.map((i)=>i.comName)));return await zn(s,{"components/_components.json":JSON.stringify(o,null,2)}),{count:n.length,removed:r}}async syncImportsLocal(e,t){let n=await this.api.listImportFiles(e,t),o=t?n.filter((c)=>c.Scope!=="project"):n,s=this.baseDir(e,t),r=vt(o),i=new Set(r.map((c)=>c.filePath).filter((c)=>!!c)),a=await Xn(this.paths,s,i);return await zn(s,{"imports/_imports.json":JSON.stringify(r,null,2)}),{list:n,count:o.length,removed:a}}async publishStagedModuleDir(e){let t=await H(ae(e,"_moduleInfo.json"),null);if(!t)throw new h(`missing _moduleInfo.json in ${e}; run fui module get first`);let n=await F(ae(e,"module.json")),o;try{o=JSON.parse(n)}catch(g){throw new h(`module.json has invalid JSON syntax: ${g.message}; fix it before publishing`)}let s=await F(ae(e,"header.html")),r=await F(ae(e,"body.html")),i=await F(ae(e,"script.js")),a=await F(ae(e,"style.css")),{pageHeader:c,pageBody:l,mTitle:u}=xc(s,r,t.mTitle??""),{ProjectID:d,ModuleID:p}=t;if(await this.api.updateModule(d,p,{moduleUI:o,title:u,pageHeader:c,pageBody:l}),i.trim())await this.api.updateModuleScript(d,p,i);if(a.trim())await this.api.updateModuleCSS(d,p,a);let m=ae(e,"components"),w=await H(ae(m,"_components.json"),[]),y=new Map(w.map((g)=>[g.comName,g.comID])),v=await _e(m),A=new Set(v.map((g)=>g.replace(/\.vue$/i,"")));for(let g of v){let k=g.replace(/\.vue$/i,""),{template:R,script:C,style:S}=It(await F(ae(m,g)));await this.api.updateComponent({ProjectID:d,ModuleID:p,ComID:y.get(k),ComName:k,ComTemplate:R,ComScript:C,ComCSS:S})}for(let g of w)if(!A.has(g.comName))await this.api.updateComponent({ProjectID:d,ModuleID:p,ComID:g.comID,ComName:"",ComTemplate:"",ComScript:""});await this.syncComponentsLocal(d,p),await this.uploadImportContents(e);let I=["module.json","header.html","body.html"];if(i.trim())I.push("script.js");if(a.trim())I.push("style.css");for(let g of v)I.push(ke(ae("components",g)));return await Ee(e,I).catch(()=>{return}),t}async previewStagedModuleDir(e){let t=await H(ae(e,"_moduleInfo.json"),null);if(!t)throw new h(`missing _moduleInfo.json in ${e}; run fui module get first`);let n=await this.fetchModuleBundle(t),o=un(n.moduleInfo,n.content,n.components,n.script,n.css,n.dependencies),s;try{s=JSON.parse(await F(ae(e,"module.json")))}catch(p){throw new h(`module.json has invalid JSON syntax: ${p.message}; fix it before previewing`)}let r=await F(ae(e,"header.html")),i=await F(ae(e,"body.html")),a=await F(ae(e,"script.js")),c=await F(ae(e,"style.css")),l=await H(ae(e,"imports/_imports.json"),[]),u={};for(let p of await _e(ae(e,"components")))u[p.replace(/\.vue$/i,"")]=await F(ae(e,"components",p));let d=[];if(jt(s)!==jt(JSON.parse(o["module.json"])))d.push("module.json will be updated");for(let p of["header.html","body.html","script.js","style.css"]){let m={"header.html":r,"body.html":i,"script.js":a,"style.css":c}[p];if($t(m)!==$t(o[p]||""))d.push(`${p} will be updated`)}if(jt(l)!==jt(JSON.parse(o["imports/_imports.json"]||"[]")))d.push("imports/_imports.json differs locally (metadata only, not sent)");return d.push(...Oc(n.components,u)),{moduleInfo:t,changes:d,remoteSummary:Jo(n.content)}}}async function ce(e){return new Qn(e.paths,await e.api())}function Lc(e){if(e.length===0)return[];return[`${e.length} import content(s) NOT downloaded; their local copies are stale even though imports/_imports.json is fresh. do not edit and publish them:`,...e.map((t)=>` - ${t}`),"re-run the sync, or fetch one: fui import get <fileId>"]}async function Vt(e,t){let n=await e.wasSkillLoaded();if(!n&&Zt())throw new Se(`[FUI_STRICT] ${t} blocked: skill not loaded this session; read the fui-skill references, run fui skill loaded, then retry`,"hard","error");return n?[]:["warning: skill not loaded this session; read fui-skill (fui skill get / references), then run: fui skill loaded"]}async function Fc(e,t){let n=await(await ce(e)).resolveModuleTarget(t);return{notes:[],data:`project ${n.projectId}
95
95
  module ${n.moduleId}
96
96
  matchedBy ${n.matchedBy}
97
97
  moduleDir ${e.paths.projectDir(n.projectId)}/modules/${n.moduleId}
98
- `}}async function _c(e,t){let{projectId:n,moduleId:o}=t,s=t.moduleDir,r="module_dir";if(!s){if(!t.target&&!(n&&o))throw new m("give a target (/project/module or URL), -p and -m, or --module-dir");let a=await(await ce(e)).resolveModuleTarget(t.target,n,o);n=a.projectId,o=a.moduleId,s=He(e.paths.projectDir(n),"modules",o),r=a.matchedBy}else{let a=await H(He(s,"_moduleInfo.json"),null);if(!a)throw new m(`missing _moduleInfo.json in ${s}`);n=a.ProjectID,o=a.ModuleID}let i=t.moduleDir?Rs(Rs(Rs(s))):e.paths.root;return await e.session.setTarget({projectId:n??"",moduleId:o??"",moduleDir:s,workspaceRoot:i,target:t.target}),{notes:[`active ${n}/${o} (${r}) -> ${s}`,`session ${e.session.file}`]}}async function Mc(e){return(await e.api()).listProjects()}async function Fc(e,t){let n=await ce(e),o=e.paths.projectDir(t),s=await n.fetchProjectBundle(t),r=gs(s.projectInfo,s.projectJson,s.modules,s.projectComponents,s.projectImports),{"modules/_modules.json":i,...a}=r,c=new Set(vt(s.projectImports).map((y)=>y.filePath).filter((y)=>!!y)),l=await Xn(e.paths,o,c),u=await n.downloadImportContents(s.projectImports),d=await bs(e.paths,o,{...a,...u.files}),p=await Ic(e.paths,o,new Set(s.modules.map((y)=>y.ModuleID)));await Yn(e.paths,t,s.modules);let h=!1,w=await e.session.getTarget();if(w?.projectId===t&&w.moduleId&&p.includes(w.moduleId))await e.session.clearTarget(),h=!0;return{notes:[`ok synced project ${t} -> ${o}`,`modules ${s.modules.length}, project components ${s.projectComponents.length}, project imports ${s.projectImports.length} (contents ${Object.keys(u.files).length})`,`files: ${Jr(d)}`,...l.length>0?[`removed orphaned import files: ${l.join(", ")}`]:[],...p.length>0?[`removed orphaned module dirs (archived): ${p.join(", ")}`]:[],...h?["session target cleared: it pointed to a removed module; run fui use / fui module get again"]:[],...Kr(d.archived),...xc(u.failures)]}}async function Lc(e,t){let n=await(await e.api()).listModules(t),o;try{await Yn(e.paths,t,n),o="cache updated: modules/_modules.json (+ _moduleInfo.json of checked-out modules)"}catch(s){o=`warning: could not write the local cache: ${s.message}`}return{rows:n,notes:[o]}}async function Uc(e,t){let n=await ce(e),o=(await n.resolveModuleInfoFromCache(t.projectId,t.moduleId)).found;if(o)throw new m(`module "${t.projectId}/${t.moduleId}" already exists ("${o.ModuleName}"); edit it: fui module update -p ${t.projectId} -m ${t.moduleId}, or pick another id`);let s={...t.extra,Framework:t.framework??"V2",mDomain:t.mDomain??"",ModuleID:t.moduleId,ModuleName:t.moduleName,HTMLOnly:t.htmlOnly??!1,dialogModel:"new"},i=(await n.api.editModule(t.projectId,s)).data??[],a=i.find((l)=>l.ModuleID===t.moduleId)??i[0],c=[`ok created module ${a?.id??`${t.projectId}/${t.moduleId}`} url ${a?.url??`/${t.projectId}/${t.moduleId}`} framework ${s.Framework}`];if(a)try{await Vo(e.paths,t.projectId,a),c.push("modules/_modules.json updated")}catch{c.push("warning: module created but modules/_modules.json update failed")}return c.push(`next: fui module get -p ${t.projectId} -m ${t.moduleId}`),{notes:c}}async function Hc(e,t){let n=await ce(e),{projectId:o,moduleId:s,newModuleId:r}=t,i=He(e.paths.projectDir(o),"modules",s),a=await H(He(i,"_moduleInfo.json"),null),c=a?null:await n.resolveModuleInfoFromCache(o,s),l=a??c?.found;if(!l)throw mn(o,s,c??void 0);let u=[];if(t.framework&&t.framework!==l.Framework)u.push(`warning: framework ${l.Framework} -> ${t.framework} switches the CDN set (V2/V3); re-check header/body/script and components`);if(r&&r!==s)u.push(`warning: ModuleID "${s}" -> "${r}" changes the module URL; links and menus to the old URL break`);let{mTitle:d,...p}=l,h={...p,...t.extra,Framework:t.framework??l.Framework,mDomain:t.mDomain??l.mDomain??"",ModuleID:r??s,ModuleName:t.moduleName??l.ModuleName,HTMLOnly:t.htmlOnly??l.HTMLOnly,dialogModel:"edit"},y=((await n.api.editModule(o,h)).data??[]).find((I)=>I.ModuleID===h.ModuleID),v={...l,Framework:h.Framework,mDomain:h.mDomain,ModuleID:h.ModuleID,ModuleName:h.ModuleName,HTMLOnly:h.HTMLOnly};if(r&&r!==s)v.id=`${o}/${r}`,v.url=`/${o}/${r}`;if(y)v={...v,...y};await Vo(e.paths,o,v,s);let N=["modules/_modules.json updated"];if(a){let I=i;if(r&&r!==s){let b=He(e.paths.projectDir(o),"modules",r);if(await St(b))u.push(`warning: cannot rename the local folder: ${b} exists; _moduleInfo.json updated in place`);else await Ym(i,b),I=b,N.push(`local folder renamed: modules/${s} -> modules/${r}`)}await F(He(I,"_moduleInfo.json"),JSON.stringify({...a,...v},null,2)),N.push("_moduleInfo.json updated");let g=await e.session.getTarget();if(g&&g.projectId===o&&g.moduleId===s)await e.session.setTarget({projectId:o,moduleId:v.ModuleID,moduleDir:I,workspaceRoot:g.workspaceRoot,target:g.target}),N.push("session target updated")}return{notes:[`ok updated ${v.id} url ${v.url} name ${v.ModuleName} framework ${v.Framework} htmlOnly ${v.HTMLOnly}`,...N,...u]}}async function Vc(e,t,n){await(await e.api()).deleteModule(t,n);let o=[`ok deleted module ${n} from project ${t}`];try{o.push(await Rc(e.paths,t,n)?"entry removed from modules/_modules.json":"no entry in modules/_modules.json (already absent)")}catch(i){o.push(`warning: could not remove the cache entry: ${i.message}`)}let s=He(e.paths.projectDir(t),"modules",n);if(await St(s)){await vo(e.paths,s,{suffix:"deleted"});let{rm:i}=await import("node:fs/promises");await i(s,{recursive:!0,force:!0}),o.push(`local folder removed (archived to _history first): modules/${n}`)}let r=await e.session.getTarget();if(r&&r.projectId===t&&r.moduleId===n)await e.session.clearTarget(),o.push("session target cleared (it pointed to the deleted module)");return{notes:o}}async function Wc(e,t){let n=await ce(e),o,s,r,i;if(t.projectId&&t.moduleId)o=t.projectId,s=t.moduleId,r="explicit_ids";else if(t.target){let E=await n.resolveProjectAndModuleHint(t.target);o=E.projectId,s=E.moduleInfo?.ModuleID??E.moduleHint,r=E.matchedBy,i=E.moduleInfo}else{let E=await e.targets.resolveProjectModule();o=E.projectId,s=E.moduleId,r=E.source==="cwd"?"cwd":"active_session"}let a=e.paths.projectDir(o),c=await H(He(a,"_projectInfo.json"),null)!==null,l=[];if(!c){let E=await n.fetchProjectBundle(o);await bs(e.paths,a,gs(E.projectInfo,E.projectJson,E.modules,E.projectComponents,E.projectImports));let te=await n.downloadImportContents(E.projectImports);if(l.push(...te.failures),Object.keys(te.files).length>0)await Ne(e.paths,a,te.files)}let u=i??null;if(!u){let E=await n.lookupModuleInfo(o,(te)=>{let ve=te.find((V)=>V.ModuleID===s);if(ve||r!=="target_path")return ve??null;let $e=Cs(te,s);if($e.length>1)throw new m(`target "${t.target}" matched several modules in project ${o}: ${$e.map((V)=>V.ModuleID).join(", ")}`);return $e[0]??null});if(u=E.found,!u)throw mn(o,s,E)}let{content:d,components:p,script:h,css:w,dependencies:y}=await n.fetchModuleBundle(u),v=un(u,d,p,h,w,y),N=He(e.paths.projectDir(u.ProjectID),"modules",u.ModuleID),I=new Set(vt(y.filter((E)=>E.Scope!=="project")).map((E)=>E.filePath).filter((E)=>!!E)),g=await Xn(e.paths,N,I),[b,R]=await Promise.all([n.downloadImportContents(y.filter((E)=>E.Scope==="module")),n.downloadImportContents(c?y.filter((E)=>E.Scope==="project"):[])]);l.push(...b.failures,...R.failures);let C=await Sc(e.paths,N,u,d,{...v,...b.files}),S=Object.keys(R.files).length>0?await Ne(e.paths,a,R.files):null;await Vo(e.paths,o,{...u,Framework:d.Framework,HTMLOnly:d.HTMLOnly}),await e.session.setTarget({projectId:u.ProjectID,moduleId:u.ModuleID,moduleDir:C.moduleDir,workspaceRoot:e.paths.root,target:t.target});let D=[...C.removedFiles.map((E)=>Qm(E)),...g],x=[...C.sync.archived,...S?.archived??[]],A=c?await Mt(a,"project data (project.json, components, imports)",`fui project sync -p ${u.ProjectID}`,"_projectInfo.json"):null;return{notes:[`ok fetched ${u.ProjectID}/${u.ModuleID} (${r}) -> ${C.moduleDir}`,`files: ${Jr(C.sync)}`,...D.length>0?[`removed orphaned local files: ${D.join(", ")}`]:[],...Kr(x),c?"project data already local (not fetched); refresh: fui project sync":"project data bootstrapped (project.json, components, imports, modules/_modules.json)",...A?[A]:[],...C.warnings.map((E)=>`warning: ${E}`),...xc(l),...qo(d),"now active for fui module preview / publish / validate"],data:`${bc(u.ModuleID,{"_moduleInfo.json":"{}",...v})}
99
- `}}async function qc(e,t,n,o){let s=await e.api(),r=o==="ui"?await s.getModuleUI(t,n):await s.getModuleHtml(t,n),i=r.data,a=He(e.paths.projectDir(t),"modules",n),c=[];if(o==="ui"&&i?.moduleUI!==void 0&&i.moduleUI!==null&&await St(a)){let l;if(typeof i.moduleUI==="string")try{l=JSON.stringify(JSON.parse(i.moduleUI),null,2)}catch{l=i.moduleUI}else l=JSON.stringify(i.moduleUI,null,2);await Ne(e.paths,a,{"module.json":l}),c.push(`module.json written to ${a}`)}if(o==="html"&&typeof i?.mBody==="string"&&await St(a))await Ne(e.paths,a,{"body.html":i.mBody}),c.push(`body.html written to ${a}`);return{notes:c,data:`${JSON.stringify(r)}
100
- `}}async function hn(e,t,n){let o=t,s=n;if(!o){let a=e.targets.cwdTarget()??await e.session.getTarget();if(!a?.projectId)throw new m("no project: pass -p <project> (and -m <module> for module scope), or cd into a module");o=a.projectId,s=n||a.moduleId||void 0}let r=s?He(e.paths.projectDir(o),"modules",s):e.paths.projectDir(o);return{projectId:o,moduleId:s,baseDir:r}}async function Bc(e,t,n){let o=await(await e.api()).getModuleComponents(t.projectId,t.moduleId),s=o.find((a)=>a.comName===n),r=t.moduleId?`module ${t.moduleId}`:`project ${t.projectId} (global)`;if(!s)throw new m(`component "${n}" not found in ${r}; available: ${o.map((a)=>a.comName).join(", ")}`);let i=[`comID ${s.comID} scope ${t.moduleId?`module:${t.moduleId}`:"project (global)"}`];if(await St(t.baseDir))await Ne(e.paths,t.baseDir,{[`components/${n}.vue`]:s.comCode}),i.push(`${n}.vue written to ${t.baseDir}/components/`);return{notes:i,data:s.comCode}}function fn(e){if(e===void 0)return;let t=Array.isArray(e)?e:[e],n={};for(let o of t){let s=String(o),r=s.indexOf("=");if(r<=0)throw new m(`--set expects key=value, got "${s}"`);let i=s.slice(r+1);try{n[s.slice(0,r)]=JSON.parse(i)}catch{n[s.slice(0,r)]=i}}return n}async function Ts(e){let t=typeof e.data==="string"?e.data:void 0,n=typeof e["data-file"]==="string"?e["data-file"]:void 0;if(t===void 0===(n===void 0))throw new m("pass exactly one of --data '<json>' | --data - | --data-file <path>");let o=n!==void 0?await ft(n):t==="-"?await Pn():t,s;try{s=JSON.parse(o)}catch(r){throw new m(`--data is not valid JSON: ${r.message}`)}if(!s||typeof s!=="object"||Array.isArray(s))throw new m("--data must be a JSON object");return s}async function Zn(e,t,n){let o=typeof e[t]==="string"?e[t]:void 0,s=typeof e[`${t}-file`]==="string"?e[`${t}-file`]:void 0;if(o===void 0&&s===void 0)return;if(o!==void 0&&s!==void 0)throw new m(`pass --${t} or --${t}-file, not both`);let r=s!==void 0?await ft(s):o==="-"?await Pn():o,i;try{i=JSON.parse(r)}catch(c){throw new m(`--${t} is not valid JSON: ${c.message}`)}if(!(n==="array"?Array.isArray(i):!!i&&typeof i==="object"&&!Array.isArray(i)))throw new m(`--${t} must be a JSON ${n}`);return i}function Bo(...e){return Object.fromEntries(e.flatMap((t)=>[[t,{type:"string"}],[`${t}-file`,{type:"string"}]]))}var gn={module:{type:"string",short:"m"}};function Jo(e){for(let t of e.notes)T(`# ${t}`);if(e.data)k(e.data)}function eo(e){if(k(`${e.notes.join(`
98
+ `}}async function Uc(e,t){let{projectId:n,moduleId:o}=t,s=t.moduleDir,r="module_dir";if(!s){if(!t.target&&!(n&&o))throw new h("give a target (/project/module or URL), -p and -m, or --module-dir");let a=await(await ce(e)).resolveModuleTarget(t.target,n,o);n=a.projectId,o=a.moduleId,s=He(e.paths.projectDir(n),"modules",o),r=a.matchedBy}else{let a=await H(He(s,"_moduleInfo.json"),null);if(!a)throw new h(`missing _moduleInfo.json in ${s}`);n=a.ProjectID,o=a.ModuleID}let i=t.moduleDir?Es(Es(Es(s))):e.paths.root;return await e.session.setTarget({projectId:n??"",moduleId:o??"",moduleDir:s,workspaceRoot:i,target:t.target}),{notes:[`active ${n}/${o} (${r}) -> ${s}`,`session ${e.session.file}`]}}async function Hc(e){return(await e.api()).listProjects()}async function Vc(e,t){let n=await ce(e),o=e.paths.projectDir(t),s=await n.fetchProjectBundle(t),r=bs(s.projectInfo,s.projectJson,s.modules,s.projectComponents,s.projectImports),{"modules/_modules.json":i,...a}=r,c=new Set(vt(s.projectImports).map((y)=>y.filePath).filter((y)=>!!y)),l=await Xn(e.paths,o,c),u=await n.downloadImportContents(s.projectImports),d=await $s(e.paths,o,{...a,...u.files}),p=await Pc(e.paths,o,new Set(s.modules.map((y)=>y.ModuleID)));await Yn(e.paths,t,s.modules);let m=!1,w=await e.session.getTarget();if(w?.projectId===t&&w.moduleId&&p.includes(w.moduleId))await e.session.clearTarget(),m=!0;return{notes:[`ok synced project ${t} -> ${o}`,`modules ${s.modules.length}, project components ${s.projectComponents.length}, project imports ${s.projectImports.length} (contents ${Object.keys(u.files).length})`,`files: ${zr(d)}`,...l.length>0?[`removed orphaned import files: ${l.join(", ")}`]:[],...p.length>0?[`removed orphaned module dirs (archived): ${p.join(", ")}`]:[],...m?["session target cleared: it pointed to a removed module; run fui use / fui module get again"]:[],...Xr(d.archived),...Lc(u.failures)]}}async function Wc(e,t){let n=await(await e.api()).listModules(t),o;try{await Yn(e.paths,t,n),o="cache updated: modules/_modules.json (+ _moduleInfo.json of checked-out modules)"}catch(s){o=`warning: could not write the local cache: ${s.message}`}return{rows:n,notes:[o]}}async function qc(e,t){let n=await ce(e),o=(await n.resolveModuleInfoFromCache(t.projectId,t.moduleId)).found;if(o)throw new h(`module "${t.projectId}/${t.moduleId}" already exists ("${o.ModuleName}"); edit it: fui module update -p ${t.projectId} -m ${t.moduleId}, or pick another id`);let s={...t.extra,Framework:t.framework??"V2",mDomain:t.mDomain??"",ModuleID:t.moduleId,ModuleName:t.moduleName,HTMLOnly:t.htmlOnly??!1,dialogModel:"new"},i=(await n.api.editModule(t.projectId,s)).data??[],a=i.find((l)=>l.ModuleID===t.moduleId)??i[0],c=[`ok created module ${a?.id??`${t.projectId}/${t.moduleId}`} url ${a?.url??`/${t.projectId}/${t.moduleId}`} framework ${s.Framework}`];if(a)try{await qo(e.paths,t.projectId,a),c.push("modules/_modules.json updated")}catch{c.push("warning: module created but modules/_modules.json update failed")}return c.push(`next: fui module get -p ${t.projectId} -m ${t.moduleId}`),{notes:c}}async function Bc(e,t){let n=await ce(e),{projectId:o,moduleId:s,newModuleId:r}=t,i=He(e.paths.projectDir(o),"modules",s),a=await H(He(i,"_moduleInfo.json"),null),c=a?null:await n.resolveModuleInfoFromCache(o,s),l=a??c?.found;if(!l)throw mn(o,s,c??void 0);let u=[];if(t.framework&&t.framework!==l.Framework)u.push(`warning: framework ${l.Framework} -> ${t.framework} switches the CDN set (V2/V3); re-check header/body/script and components`);if(r&&r!==s)u.push(`warning: ModuleID "${s}" -> "${r}" changes the module URL; links and menus to the old URL break`);let{mTitle:d,...p}=l,m={...p,...t.extra,Framework:t.framework??l.Framework,mDomain:t.mDomain??l.mDomain??"",ModuleID:r??s,ModuleName:t.moduleName??l.ModuleName,HTMLOnly:t.htmlOnly??l.HTMLOnly,dialogModel:"edit"},y=((await n.api.editModule(o,m)).data??[]).find((I)=>I.ModuleID===m.ModuleID),v={...l,Framework:m.Framework,mDomain:m.mDomain,ModuleID:m.ModuleID,ModuleName:m.ModuleName,HTMLOnly:m.HTMLOnly};if(r&&r!==s)v.id=`${o}/${r}`,v.url=`/${o}/${r}`;if(y)v={...v,...y};await qo(e.paths,o,v,s);let A=["modules/_modules.json updated"];if(a){let I=i;if(r&&r!==s){let k=He(e.paths.projectDir(o),"modules",r);if(await St(k))u.push(`warning: cannot rename the local folder: ${k} exists; _moduleInfo.json updated in place`);else await rh(i,k),I=k,A.push(`local folder renamed: modules/${s} -> modules/${r}`)}await L(He(I,"_moduleInfo.json"),JSON.stringify({...a,...v},null,2)),A.push("_moduleInfo.json updated");let g=await e.session.getTarget();if(g&&g.projectId===o&&g.moduleId===s)await e.session.setTarget({projectId:o,moduleId:v.ModuleID,moduleDir:I,workspaceRoot:g.workspaceRoot,target:g.target}),A.push("session target updated")}return{notes:[`ok updated ${v.id} url ${v.url} name ${v.ModuleName} framework ${v.Framework} htmlOnly ${v.HTMLOnly}`,...A,...u]}}async function Jc(e,t,n){await(await e.api()).deleteModule(t,n);let o=[`ok deleted module ${n} from project ${t}`];try{o.push(await Ac(e.paths,t,n)?"entry removed from modules/_modules.json":"no entry in modules/_modules.json (already absent)")}catch(i){o.push(`warning: could not remove the cache entry: ${i.message}`)}let s=He(e.paths.projectDir(t),"modules",n);if(await St(s)){await $o(e.paths,s,{suffix:"deleted"});let{rm:i}=await import("node:fs/promises");await i(s,{recursive:!0,force:!0}),o.push(`local folder removed (archived to _history first): modules/${n}`)}let r=await e.session.getTarget();if(r&&r.projectId===t&&r.moduleId===n)await e.session.clearTarget(),o.push("session target cleared (it pointed to the deleted module)");return{notes:o}}async function Kc(e,t){let n=await ce(e),o,s,r,i;if(t.projectId&&t.moduleId)o=t.projectId,s=t.moduleId,r="explicit_ids";else if(t.target){let P=await n.resolveProjectAndModuleHint(t.target);o=P.projectId,s=P.moduleInfo?.ModuleID??P.moduleHint,r=P.matchedBy,i=P.moduleInfo}else{let P=await e.targets.resolveProjectModule();o=P.projectId,s=P.moduleId,r=P.source==="cwd"?"cwd":"active_session"}let a=e.paths.projectDir(o),c=await H(He(a,"_projectInfo.json"),null)!==null,l=[];if(!c){let P=await n.fetchProjectBundle(o);await $s(e.paths,a,bs(P.projectInfo,P.projectJson,P.modules,P.projectComponents,P.projectImports));let te=await n.downloadImportContents(P.projectImports);if(l.push(...te.failures),Object.keys(te.files).length>0)await Ae(e.paths,a,te.files)}let u=i??null;if(!u){let P=await n.lookupModuleInfo(o,(te)=>{let ve=te.find((V)=>V.ModuleID===s);if(ve||r!=="target_path")return ve??null;let $e=Ps(te,s);if($e.length>1)throw new h(`target "${t.target}" matched several modules in project ${o}: ${$e.map((V)=>V.ModuleID).join(", ")}`);return $e[0]??null});if(u=P.found,!u)throw mn(o,s,P)}let{content:d,components:p,script:m,css:w,dependencies:y}=await n.fetchModuleBundle(u),v=un(u,d,p,m,w,y),A=He(e.paths.projectDir(u.ProjectID),"modules",u.ModuleID),I=new Set(vt(y.filter((P)=>P.Scope!=="project")).map((P)=>P.filePath).filter((P)=>!!P)),g=await Xn(e.paths,A,I),[k,R]=await Promise.all([n.downloadImportContents(y.filter((P)=>P.Scope==="module")),n.downloadImportContents(c?y.filter((P)=>P.Scope==="project"):[])]);l.push(...k.failures,...R.failures);let C=await Rc(e.paths,A,u,d,{...v,...k.files}),S=Object.keys(R.files).length>0?await Ae(e.paths,a,R.files):null;await qo(e.paths,o,{...u,Framework:d.Framework,HTMLOnly:d.HTMLOnly}),await e.session.setTarget({projectId:u.ProjectID,moduleId:u.ModuleID,moduleDir:C.moduleDir,workspaceRoot:e.paths.root,target:t.target});let D=[...C.removedFiles.map((P)=>sh(P)),...g],x=[...C.sync.archived,...S?.archived??[]],N=c?await Mt(a,"project data (project.json, components, imports)",`fui project sync -p ${u.ProjectID}`,"_projectInfo.json"):null;return{notes:[`ok fetched ${u.ProjectID}/${u.ModuleID} (${r}) -> ${C.moduleDir}`,`files: ${zr(C.sync)}`,...D.length>0?[`removed orphaned local files: ${D.join(", ")}`]:[],...Xr(x),c?"project data already local (not fetched); refresh: fui project sync":"project data bootstrapped (project.json, components, imports, modules/_modules.json)",...N?[N]:[],...C.warnings.map((P)=>`warning: ${P}`),...Lc(l),...Jo(d),"now active for fui module preview / publish / validate"],data:`${Sc(u.ModuleID,{"_moduleInfo.json":"{}",...v})}
99
+ `}}async function Gc(e,t,n,o){let s=await e.api(),r=o==="ui"?await s.getModuleUI(t,n):await s.getModuleHtml(t,n),i=r.data,a=He(e.paths.projectDir(t),"modules",n),c=[];if(o==="ui"&&i?.moduleUI!==void 0&&i.moduleUI!==null&&await St(a)){let l;if(typeof i.moduleUI==="string")try{l=JSON.stringify(JSON.parse(i.moduleUI),null,2)}catch{l=i.moduleUI}else l=JSON.stringify(i.moduleUI,null,2);await Ae(e.paths,a,{"module.json":l}),c.push(`module.json written to ${a}`)}if(o==="html"&&typeof i?.mBody==="string"&&await St(a))await Ae(e.paths,a,{"body.html":i.mBody}),c.push(`body.html written to ${a}`);return{notes:c,data:`${JSON.stringify(r)}
100
+ `}}async function hn(e,t,n){let o=t,s=n;if(!o){let a=e.targets.cwdTarget()??await e.session.getTarget();if(!a?.projectId)throw new h("no project: pass -p <project> (and -m <module> for module scope), or cd into a module");o=a.projectId,s=n||a.moduleId||void 0}let r=s?He(e.paths.projectDir(o),"modules",s):e.paths.projectDir(o);return{projectId:o,moduleId:s,baseDir:r}}async function zc(e,t,n){let o=await(await e.api()).getModuleComponents(t.projectId,t.moduleId),s=o.find((a)=>a.comName===n),r=t.moduleId?`module ${t.moduleId}`:`project ${t.projectId} (global)`;if(!s)throw new h(`component "${n}" not found in ${r}; available: ${o.map((a)=>a.comName).join(", ")}`);let i=[`comID ${s.comID} scope ${t.moduleId?`module:${t.moduleId}`:"project (global)"}`];if(await St(t.baseDir))await Ae(e.paths,t.baseDir,{[`components/${n}.vue`]:s.comCode}),i.push(`${n}.vue written to ${t.baseDir}/components/`);return{notes:i,data:s.comCode}}function fn(e){if(e===void 0)return;let t=Array.isArray(e)?e:[e],n={};for(let o of t){let s=String(o),r=s.indexOf("=");if(r<=0)throw new h(`--set expects key=value, got "${s}"`);let i=s.slice(r+1);try{n[s.slice(0,r)]=JSON.parse(i)}catch{n[s.slice(0,r)]=i}}return n}async function As(e){let t=typeof e.data==="string"?e.data:void 0,n=typeof e["data-file"]==="string"?e["data-file"]:void 0;if(t===void 0===(n===void 0))throw new h("pass exactly one of --data '<json>' | --data - | --data-file <path>");let o=n!==void 0?await ft(n):t==="-"?await En():t,s;try{s=JSON.parse(o)}catch(r){throw new h(`--data is not valid JSON: ${r.message}`)}if(!s||typeof s!=="object"||Array.isArray(s))throw new h("--data must be a JSON object");return s}async function Zn(e,t,n){let o=typeof e[t]==="string"?e[t]:void 0,s=typeof e[`${t}-file`]==="string"?e[`${t}-file`]:void 0;if(o===void 0&&s===void 0)return;if(o!==void 0&&s!==void 0)throw new h(`pass --${t} or --${t}-file, not both`);let r=s!==void 0?await ft(s):o==="-"?await En():o,i;try{i=JSON.parse(r)}catch(c){throw new h(`--${t} is not valid JSON: ${c.message}`)}if(!(n==="array"?Array.isArray(i):!!i&&typeof i==="object"&&!Array.isArray(i)))throw new h(`--${t} must be a JSON ${n}`);return i}function Ko(...e){return Object.fromEntries(e.flatMap((t)=>[[t,{type:"string"}],[`${t}-file`,{type:"string"}]]))}var gn={module:{type:"string",short:"m"}};function Go(e){for(let t of e.notes)T(`# ${t}`);if(e.data)b(e.data)}function eo(e){if(b(`${e.notes.join(`
101
101
  `)}
102
- `),e.data)k(e.data)}async function Es(e){return(await e.targets.resolveProject(f(e.values,"project"))).projectId}async function Kc(e){return e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module"))}function Zm(e,t){if(e===void 0)return;if(e==="true")return!0;if(e==="false")return!1;throw new m(`${t} takes true or false`)}function Gc(e){if(e===void 0)return;let t=e.toUpperCase();if(t!=="V2"&&t!=="V3")throw new m("--framework must be V2 or V3");return t}var eh={name:"use",usage:"fui use [<target> | -p P -m M | --module-dir D] | fui use --clear",summary:"Show, set or clear the session target (target: /project/module, a live URL, or a display name)",options:{...gn,"module-dir":{type:"string"},clear:{type:"boolean"}},async run(e){let{values:t,positionals:n,session:o,render:s}=e;if(t.clear===!0){await o.clearTarget(),k(`session target cleared
103
- `);return}let r=n[0],i=f(t,"project"),a=f(t,"module"),c=f(t,"module-dir");if(!r&&!i&&!a&&!c){let l=await o.getTarget();if(!l){T("# no session target; set one: fui use <project>/<module>");return}k(z([{...l,session:o.file}],s));return}if(i&&!a||!i&&a)throw new m("pass both -p and -m");eo(await _c(_(e),{target:r,projectId:i,moduleId:a,moduleDir:c}))}},th={name:"resolve",usage:"fui resolve <target>",summary:"Resolve /project/module, a live module URL or a display name to ids (no session change)",async run(e){let t=e.positionals[0];if(!t)throw new m("missing <target>");Jo(await Oc(_(e),t))}},nh={name:"project list",usage:"fui project list",summary:"List projects you can access",async run(e){let t=(await Mc(_(e))).map((n)=>({group:n.GroupName||"Khac",id:n.ProjectID,name:n.ProjectName,framework:n.Framework??"",domain:n.pDomain??""}));k(z(t,e.render))}},oh={name:"project sync",usage:"fui project sync [-p project]",summary:"Re-fetch project data (project.json, components, imports, module list); archives what changed or vanished",async run(e){eo(await Fc(_(e),await Es(e)))}},rh={name:"module list",usage:"fui module list [-p project]",summary:"List modules from the server and refresh the local module cache",async run(e){let{rows:t,notes:n}=await Lc(_(e),await Es(e));for(let o of n)T(`# ${o}`);k(z(t.map((o)=>({id:o.ModuleID,name:o.ModuleName,framework:o.Framework??"",domain:o.mDomain??"",htmlOnly:o.HTMLOnly?"yes":"no",updated:o.UpdateTime??"",by:o.UpdateUser??""})),e.render))}},sh={name:"module new",usage:"fui module new <moduleId> -p <project> --name <name> [--framework V2|V3] [--html-only] [--domain d] [--set key=json ...]",summary:"Create an empty module on the server (then fui module get)",options:{name:{type:"string"},framework:{type:"string"},"html-only":{type:"boolean"},domain:{type:"string"},set:{type:"string",multiple:!0}},async run(e){let t=e.positionals[0],n=f(e.values,"name");if(!t||!n)throw new m("need <moduleId> and --name");eo(await Uc(_(e),{projectId:await Es(e),moduleId:t,moduleName:n,framework:Gc(f(e.values,"framework")),htmlOnly:e.values["html-only"]===!0?!0:void 0,mDomain:f(e.values,"domain"),extra:fn(e.values.set)}))}},ih={name:"module update",usage:"fui module update [-p P -m M] [--name n] [--framework V2|V3] [--html-only true|false] [--domain d] [--new-id id] [--set key=json ...]",summary:"Edit metadata of an existing module (name, id, framework, domain, HTMLOnly)",options:{...gn,name:{type:"string"},framework:{type:"string"},"html-only":{type:"string"},domain:{type:"string"},"new-id":{type:"string"},set:{type:"string",multiple:!0}},async run(e){let{projectId:t,moduleId:n}=await Kc(e);eo(await Hc(_(e),{projectId:t,moduleId:n,moduleName:f(e.values,"name"),framework:Gc(f(e.values,"framework")),htmlOnly:Zm(f(e.values,"html-only"),"--html-only"),mDomain:f(e.values,"domain"),newModuleId:f(e.values,"new-id"),extra:fn(e.values.set)}))}},ah={name:"module delete",usage:"fui module delete -p <project> -m <module>",summary:"Delete a module on the server and archive its local folder (ask the user first; ids must be explicit)",options:gn,async run(e){let t=f(e.values,"project"),n=f(e.values,"module");if(!t||!n)throw new m("module delete needs explicit -p <project> -m <module>");eo(await Vc(_(e),t,n))}},ch={name:"module get",usage:"fui module get [<target> | -p P -m M]",summary:"Check out a module into the workspace (archives differing local files) and make it the session target",options:gn,async run(e){let t=f(e.values,"project"),n=f(e.values,"module");if(t&&!n||!t&&n)throw new m("pass both -p and -m, or a target");Jo(await Wc(_(e),{target:e.positionals[0],projectId:t,moduleId:n}))}};function Jc(e){return{name:`module get-${e}`,usage:`fui module get-${e} [-p P -m M]`,summary:`Fetch only the module ${e==="ui"?"UI JSON (writes module.json":"HTML body (writes body.html"} if checked out)`,options:gn,async run(t){let{projectId:n,moduleId:o}=await Kc(t);Jo(await qc(_(t),n,o,e))}}}var lh={name:"component get",usage:"fui component get <name> [-p P [-m M]]",summary:"Print a component's .vue source (writes it locally when the scope folder exists); -p without -m = project scope",options:gn,async run(e){let t=e.positionals[0];if(!t)throw new m("missing <name>");let n=_(e),o=await hn(n,f(e.values,"project"),f(e.values,"module"));Jo(await Bc(n,o,t))}},zc=[eh,th,nh,oh,rh,sh,ih,ah,ch,Jc("ui"),Jc("html"),lh];import{resolve as Uf}from"node:path";import{join as pt}from"node:path";import dt from"node:path";import{join as Xc}from"node:path";async function Wt(e,t){let n={functionRights:null,systemRights:null},o;try{o=JSON.parse(await G(Xc(e,"_db","rights.json")))}catch{return n}let s=Object.keys(o??{});if(s.length===0)return n;let r=(l)=>s.find((u)=>u.toLowerCase()===l.toLowerCase())??null,i;if(t)i=r(t);else if(s.length===1)i=s[0];else{let l=await H(Xc(e,"_db","_connections.json"),null),u=l?.default?l.connections?.[l.default]?.apiName??null:null;i=u?r(u):null}if(!i)return n;let a=o[i],c=(l,u)=>Array.isArray(l)?new Set(l.map((d)=>d&&typeof d==="object"?String(d[u]??""):"").filter(Boolean)):null;return{functionRights:c(a?.functionRights,"FunctionCode"),systemRights:c(a?.systemRights,"SysRight")}}function qt(e){return Qc("FunctionRight",e)}function Yc(e){return Qc("SystemRight",e)}function to(e,t){if(e.length===0)return null;if(t===null)return`Chưa có cache _db/rights.json — KHÔNG kiểm được ${e.length} mã FunctionRight (${e.join(", ")}) `+"có thật hay không. Preview cấp sẵn đúng những mã này nên control vẫn hiện ở ĐÂY; user thật chỉ thấy nếu mã đã "+"được định nghĩa. Chạy right_function_list (một lần cho mỗi project) rồi render lại.";let n=e.filter((o)=>!t.has(o));if(n.length===0)return null;return`\uD83D\uDEA8 FunctionRight CHƯA ĐƯỢC ĐỊNH NGHĨA trên hệ thống acc: ${n.join(", ")} — preview cấp sẵn nên control vẫn `+"hiện ở ĐÂY, nhưng chạy thật thì rightTest() luôn trả false → control/menu biến mất mà không có lỗi JS nào. "+'Tạo mã: right_function_new({ functionCode: …, functionName: "…" }); hoặc right_function_list nếu cache cũ.'}function Qc(e,t){let n=new Set,o=new RegExp(`${e}['"]?\\s*:\\s*\\[([^\\]]*)\\]`,"g"),s=new RegExp(`${e}\\s*\\.\\s*includes\\(\\s*['"]([^'"]+)['"]`,"g");for(let r of t){if(!r)continue;let i;o.lastIndex=0;while(i=o.exec(r))for(let a of i[1].split(",")){let c=a.trim().replace(/^['"]|['"]$/g,"");if(c)n.add(c)}s.lastIndex=0;while(i=s.exec(r))n.add(i[1])}return Array.from(n).sort()}var uh=new Set(["f-table","f-dialog","f-window","f-button","f-date","f-time","f-time-counter","f-search","f-menu","f-radiobox","f-file-upload","f-image-update","f-excel-reader","f-qrcode","f-qrcode-reader","f-editor","f-editor-dialog","f-echart","f-chart","f-sheet","f-pdfmake","f-label","f-box","f-header","f-title","f-slider","fp-profile","f-dynamic-component","f-table-item-component"]),Zc=new Set(["t-html","t-label","t-num","t-time","t-boolean","t-check","t-text","t-select","t-combobox","t-menu","t-link","t-button"]),dh={"f-pdfmake":["pdfmake.min.js","print.min.js","vfs_fonts.js"],"f-file-upload":["plupload.full.min.js"],"f-image-update":["index.umd.js","style.css"],"f-qrcode":["qrcode.min.js"],"f-qrcode-reader":["qrcode-reader.min.js"],"f-echart":["echarts.min.js","fechart.js"],"f-sheet":["ag-grid-community.min.js","fsheet.js"],"f-editor":["ckeditor.js"],"f-editor-dialog":["ckeditor.js"]};var wn=new Set(["API","CALL","IF","FUN","EXE","MESS","MESSBOX","ARRAY","CONFIRM","IN","OUT","THEN","ELSE","CALLBACK","ERROROUT","ERRORCALLBACK","CANCEL","METHOD","HEADER","COL"]),el=new Set(["item","index","v_old","user","vueData","p_router","p_routers","v_Set","moment"]);function j(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}function As(e){if(Array.isArray(e))return e.length>0&&e.every((t)=>j(t)||typeof t==="string")&&e.some((t)=>j(t)&&Object.keys(t).some((n)=>wn.has(n)));if(!j(e))return!1;return Object.keys(e).some((t)=>wn.has(t))}function Ye(e){let t=e.trim();if(t.startsWith("vueData."))t=t.slice(8);let n=t.match(/^[A-Za-z_$][\w$]*/);return n?n[0]:t}function no(e){return!!e&&!e.includes(" ")&&!e.includes("{{")&&!e.includes("(")&&!e.startsWith("`")&&!e.startsWith("#")&&!e.startsWith("!")&&/^[A-Za-z_$]/.test(e.replace(/^vueData\./,""))}function ph(e){let t=e.trim();if(!t)return!0;if(/^-?\d+(\.\d+)?$/.test(t)||t==="true"||t==="false"||t==="null")return!0;if(/^'[^']*'$/.test(t)||/^"[^"]*"$/.test(t))return!0;return/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(t)}function mh(e){let t=e.split(""),n=null;for(let o=0;o<e.length;o++){let s=e[o];if(n){if(s==="\\"){if(t[o]=" ",o+1<e.length)t[o+1]=" ",o++;continue}if(s===n){n=null;continue}t[o]=" "}else if(s==="'"||s==='"'||s==="`")n=s}return t.join("")}var hh=[{re:/\bfunction\s*\(/,what:'khai báo "function(...)"'},{re:/\bfunction\s+[A-Za-z_$]/,what:'khai báo "function ten(...)"'},{re:/\breturn\b/,what:'lệnh "return"'},{re:/\bvar\s+[A-Za-z_$]/,what:'khai báo "var"'},{re:/\blet\s+[A-Za-z_$]/,what:'khai báo "let"'},{re:/\bconst\s+[A-Za-z_$]/,what:'khai báo "const"'}];function fh(e,t,n,o){if(typeof t!=="string")return;if(!(e.startsWith(":")&&!e.startsWith("::")||e.startsWith("v-bind:")))return;let r=mh(t),i=[];for(let{re:a,what:c}of hh)if(a.test(r))i.push(c);if(r.replace(/;\s*$/,"").includes(";"))i.push('dấu ";" nối nhiều câu lệnh');if(i.length===0)return;o.push(`attr "${e}" (tại ${n}) chứa ${i.join(" + ")} — giá trị binding đi thẳng vào template compiler của Vue, `+"chỉ nhận MỘT BIỂU THỨC thuần (object/array literal, ternary, chuỗi .map()/.filter() arrow, truy cập thuộc tính). "+"Statement ở đây làm compile template hỏng → TRANG TRẮNG mà không có JS error hay [Vue warn] nào. "+'Cách sửa: đưa logic vào một hàm trong script.js (function buildX(){ … vueData.x = …; }), gọi qua "EXE": "buildX()" '+`trong CALLBACK của action nạp dữ liệu, rồi "${e}" chỉ trỏ tới biến vueData đó (vd "${e}": "x"). `+"Chi tiết: references/advanced-techniques.md.")}function Me(e,t,n="$",o=!1){if(Array.isArray(e))e.forEach((s,r)=>Me(s,t,`${n}[${r}]`,o));else if(j(e)){t(e,n,o);for(let[s,r]of Object.entries(e))Me(r,t,`${n}.${s}`,o||s.startsWith("#"))}}var gh="(chuẩn mặc định fui/DESIGN.md — chỉ giữ nguyên nếu user yêu cầu rõ kiểu khác)",wh=new Set([400,550,600,650,800]),yh=/(xóa|xoá|\bdelete\b|\bremove\b)/i,bh=/(delete|remove|xoa)/i,kh=new Set(["v-text-field","v-textarea","v-select","v-combobox","v-autocomplete","f-date","f-time","f-search"]),tl=new Set(["9"]);function vh(e,t,n){let o=JSON.stringify(e),s=qt([o]),r=Yc([o]).filter((l)=>!tl.has(l)),i=s.length+r.length;if(i===0)return 0;let a=t.definedFunctionRights??null,c=t.definedSystemRights??null;if(a===null&&c===null)return n.push(`Module gác quyền bằng ${i} mã literal (${[...s,...r].join(", ")}) nhưng chưa có cache _db/rights.json `+"để đối chiếu — KHÔNG kiểm được mã nào có thật. Mã không tồn tại làm rightTest() luôn trả false: "+"control/menu biến mất im lặng, không lỗi JS. Chạy right_function_list + right_system_list (một lần cho mỗi project) rồi validate lại."),i;if(a&&s.length>0){let l=a,u=s.filter((d)=>!l.has(d));for(let d of u)n.push(`FunctionRight "${d}" chưa được định nghĩa cho project (_db/rights.json có ${l.size} mã`+`${l.size?`: ${Array.from(l).slice(0,15).join(", ")}${l.size>15?", …":""}`:""}). `+"rightTest() sẽ luôn trả false → control/menu gác bằng mã này biến mất mà không báo lỗi. "+`Tạo mã: right_function_new({ functionCode: ${/^\d+$/.test(d)?d:`"${d}"`}, functionName: "…" }); `+"hoặc chạy right_function_list nếu mã đã có trên server mà cache cũ.")}if(c&&r.length>0){let l=c,u=r.filter((d)=>!l.has(d));for(let d of u)n.push(`SystemRight ${d} chưa được định nghĩa cho project (_db/rights.json có: `+`${l.size?Array.from(l).sort().join(", "):"(chưa mức nào)"}${tl.size?" — mức 9 luôn hợp lệ":""}). `+"Không ai được cấp mức này thì control gác bằng nó biến mất im lặng. "+`Định nghĩa: right_system_new({ sysRight: ${d}, note: "…" }); hoặc chạy right_system_list nếu cache cũ.`)}return i}function $h(e,t){let n=(a)=>t.push(`[design] ${a} ${gh}`),o=(a)=>typeof a==="string"?a:"";Me(e,(a,c)=>{let l=a.API;if(typeof l==="string"&&bh.test(l)&&a.CONFIRM===void 0)n(`Action gọi API "${l}" (tại ${c}) có vẻ là thao tác XÓA nhưng thiếu CONFIRM — mọi action xóa phải hỏi xác nhận.`)});let s=new Set;Me(e,(a,c)=>{if(s.has(a))return;let l=j(a.attr)?a.attr:a;if(l!==a)s.add(l);let u=o(l.label)||o(l.text)||(typeof a.innerHTML==="string"?a.innerHTML:""),d=o(l["icon-text"]);if(!yh.test(u)&&d!=="mdi-delete")return;let p=o(l.color)||o(l[":color"]);if(/\b(error|red)\b/i.test(p)&&!p.includes("?"))n(`Nút/mục xóa (tại ${c}) đang màu "${p}" — nút xóa dùng màu "warning" (cam), không dùng error/đỏ.`)});let r=(a,c,l,u)=>{if(Array.isArray(a)){a.forEach((I,g)=>r(I,`${c}[${g}]`,l,u));return}if(!j(a))return;let d=o(a.el),p=o(a.prop),h=j(a.attr)?a.attr:{},w=l||d==="v-dialog"||d==="f-dialog"||d==="hidden-container"||p.includes("hidden-container"),y=u||d==="v-card-actions";if(d==="v-toolbar"&&["color",":color","dark",":dark"].some((I)=>(I in h)))n(`v-toolbar (tại ${c}) có color/dark — toolbar tiêu đề phải "dense flat" cùng màu nền với thân dialog, không tô màu/dark riêng.`);if(d==="v-dialog"||d==="f-dialog"){let I=h.width??h[":width"],g=typeof I==="number"?I:typeof I==="string"&&/^\d+$/.test(I.trim())?parseInt(I,10):NaN;if(!Number.isNaN(g)&&!wh.has(g))n(`Dialog (tại ${c}) width=${g} — bộ width chuẩn: 400 / 550 / 600 / 650 / 800.`)}if(w&&d==="v-card"&&["elevation",":elevation","outlined",":outlined"].some((I)=>(I in h)))n(`v-card trong dialog (tại ${c}) có elevation/outlined — bỏ đi: v-dialog đã tự có bóng đổ, thêm nữa là bóng chồng bóng. v-card chỉ cần class "pa-0" (triệt đệm để toolbar tiêu đề sát mép).`);let v=o(h.class),N=v.includes("flex-md-grow-0")&&v.includes("ml-2");if(N)n(`Control (tại ${c}) mang class "flex-md-grow-0 ml-2" — dấu hiệu pattern label-div rời đã loại bỏ; form dùng floating label ("label" trong attr của field).`);if(w&&!N&&kh.has(d)&&!("label"in h)&&!(":label"in h)&&!("placeholder"in h))n(`${d} trong dialog (tại ${c}) thiếu "label" — form dialog dùng floating label.`);if(d==="v-snackbar"||d==="v-alert")n(`${d} (tại ${c}) — phản hồi/thông báo dùng hệ thống có sẵn của FUI: MESS (toast) / MESSBOX (hộp thoại) trong action, CONFIRM để xác nhận — không tự dựng ${d} trong controls.`);if(d==="f-table"||d==="f-sheet"){let I=h[":height"]??h.height;if(I===void 0)n(`${d} (tại ${c}) không có ":height" — bảng sẽ tự giãn theo số dòng và làm cả TRANG cuộn. Bảng data chính nên đặt \`":height": -16\` (fill tới đáy màn hình, chừa 16px — component tự đo vị trí thật, không cần tính tay); trong dialog dùng số dương (px cố định, vd 360). Bảng ngắn/lookup chủ ý tự giãn thì bỏ qua.`);else if(typeof I==="string"&&/\$\s*\(\s*window\s*\)\s*\.\s*height\s*\(\s*\)/.test(I))n(`${d} (tại ${c}) tính ":height" thủ công theo viewport (\`${I}\`) — không cần nữa: ${d} tự đo vị trí thật của bảng sau layout và tự cập nhật khi resize. Thay bằng \`":height": -16\` (số âm = khoảng chừa ở đáy).`)}if(y&&(d==="f-button"||d==="v-btn")){let I=o(h.label)||(typeof a.innerHTML==="string"?a.innerHTML:"");if(/^\s*(đóng|dong|close)\s*$/i.test(I))n(`Nút "${I.trim()}" trong v-card-actions (tại ${c}) — dialog chuẩn chỉ dùng icon close trên toolbar, không thêm nút Đóng ở actions.`)}for(let[I,g]of Object.entries(a))r(g,`${c}.${I}`,w,y)};(Array.isArray(e.controls)?e.controls:[]).forEach((a,c)=>r(a,`$.controls[${c}]`,!1,!1))}var Sh=12,Ps=25,ol=/\b(shrink|flex-grow-0)\b/;function rl(e){return typeof e==="string"?e:j(e)?[e.class,e[":class"]].filter((t)=>typeof t==="string").join(" "):""}function jh(e){return e.w===void 0||e.w===null||e.w===""||e.w===0}function Ih(e,t){Me(e.controls,(n,o)=>{if(!Array.isArray(n.cols))return;Ch(n.cols,o,t),n.cols.forEach((s,r)=>{if(!j(s))return;let i=s,a=i.w,c=typeof a==="number"?a:typeof a==="string"&&/^\d+$/.test(a.trim())?parseInt(a.trim(),10):NaN;if(Number.isNaN(c)||c<=Sh)return;let l=`${o}.cols[${r}]${typeof i.el==="string"?` (el: ${i.el})`:""}`;if(c<Ps){t.push(`${l} có "w": ${c} — vùng chết 13–24: V2 hiểu là ${c}px, còn V3 sinh class "v-col-${c}" mà Vuetify 3 không định nghĩa (chỉ có .v-col-1 … .v-col-12) nên cột KHÔNG có bề rộng nào, rơi về chia đều. Dùng 1–12 (chia cột) hoặc >= ${Ps} (px, kèm \`"col": { "class": "shrink" }\`).`);return}let u=i.col;if(ol.test(rl(u)))return;t.push(j(u)&&"shrink"in u?`${l} có "w": ${c} (px) kèm \`"col": { "shrink": ... }\` — dạng thuộc tính trần là prop của v-flex (Vuetify 2); Vuetify 3 không có prop đó nên nó trơ và cột vẫn giãn hết chỗ trống. Viết \`"col": { "class": "shrink" }\` để đúng ở cả hai framework.`:`${l} có "w": ${c} — số >= ${Ps} nghĩa là bề rộng cố định px, nhưng cột grid mặc định flex-grow:1 nên flex-basis ${c}px KHÔNG có tác dụng: cột vẫn giãn hết chỗ trống (đo thật: w=300 ra 687px ở V2, 738px ở V3). Thêm \`"col": { "class": "shrink" }\`.`)})},"$.controls")}function Ch(e,t,n){let o=(s)=>j(s)&&s.el==="v-spacer";if(!e.some(o))return;e.forEach((s,r)=>{if(!j(s))return;let i=s;if(typeof i.el!=="string"||!i.el)return;let a=`${t}.cols[${r}] (el: ${i.el})`,c=i.col,l=ol.test(rl(c));if(i.el==="v-spacer"){if(l)n.push(`${a} — \`v-spacer\` mà có \`shrink\` thì co lại còn 0px (đo thật: 24px ở V3, 0px ở V2), không đẩy được nhóm sau sang phải nữa và cả hàng dồn về trái. Bỏ \`shrink\` khỏi riêng cột này.`);return}if(!jh(i)||l)return;n.push(j(c)&&"shrink"in c?`${a} nằm cùng hàng với \`v-spacer\`, \`w\` rỗng và chỉ có \`"col": { "shrink": ... }\` — dạng thuộc tính trần là prop của v-flex (Vuetify 2); Vuetify 3 không có prop đó nên cột vẫn giãn và chiếm một phần đều. Viết \`"col": "shrink"\` (hoặc \`{ "class": "shrink" }\`).`:`${a} nằm cùng hàng với \`v-spacer\` nhưng thiếu \`shrink\` — \`w\` rỗng KHÔNG có nghĩa "tự co theo nội dung": runtime bọc phần bề rộng trong \`if (c.w)\` nên cột này không có class grid lẫn flex-basis, còn trơ \`flex-grow: 1\` và được CHIA PHẦN TRỐNG y như \`v-spacer\`. Hệ quả: control bị kéo dãn, hở khoảng giữa các nút, nhóm cuối hàng không sát mép phải. Chuẩn cho hàng nhiều control: MỌI cột \`"col": "shrink"\`, trừ đúng \`v-spacer\`.`)})}var nl=["API","CALL","EXE","FUN","IF","MESS","MESSBOX"],Rh=new Set(["IN","OUT","HEADER","METHOD","CALLBACK","ERROROUT","ERRORCALLBACK","THEN","ELSE","CONFIRM","CANCEL","ARRAY","COL","MAP"]);function Th(e,t,n){for(let[o,s]of Object.entries(e)){if(o==="set")continue;Me(s,(r,i)=>{let a=Object.keys(r),c=nl.filter((h)=>a.includes(h));if(c.length===0)return;let l=c[0],u=new Set([l]);if(l==="API"&&c.includes("MESS"))u.add("MESS");let d=c.filter((h)=>!u.has(h));if(d.length>0){t.push(`${i} gộp ${c.length} lệnh trong MỘT object \`{}\` (${c.join(", ")}) — chỉ \`${l}\` chạy, còn ${d.map((h)=>`\`${h}\``).join(", ")} bị bỏ IM LẶNG (không JS error, không [Vue warn]). \`vueAction\` dò key lệnh theo thứ tự cố định API → CALL → EXE → FUN → IF → MESS → MESSBOX và \`return\` ngay ở nhánh đầu tiên khớp, nên thứ tự viết không có tác dụng. Nhiều lệnh có thứ tự trước sau thì phải là MẢNG, mỗi lệnh một object: \`[{ "${c[0]}": … }, { "${c[1]}": … }]\`.`);return}let p=a.filter((h)=>!u.has(h)&&!Rh.has(h)&&!nl.includes(h));if(p.length>0)n.push(`${i} có lệnh \`${l}\` nằm chung object với key gán dữ liệu ${p.map((h)=>`\`${h}\``).join(", ")} — fragment gán chạy ở nhánh CUỐI của \`vueAction\`, nhánh đó chỉ tới lượt khi KHÔNG key lệnh nào khớp, nên các key này không bao giờ được gán. Tách thành mảng: \`[{ ${p.map((h)=>`"${h}": …`).join(", ")} }, { "${l}": … }]\`.`)},`$.${o}`)}}function Eh(e,t){let n=[],o=[];if(t.schemaWarning)o.push(t.schemaWarning);if(!j(e))return{errors:["module.json gốc phải là một object với các khối data/watch/controls/set."],warnings:o,stats:Ns()};let s=e,r=new Set(["data","watch","controls","set"]);for(let g of Object.keys(s))if(!r.has(g))o.push(`Khối cấp 1 không chuẩn: "${g}" — module.json chỉ có data/watch/controls/set (xem module-json-anatomy.md).`);if(s.data!==void 0&&!Array.isArray(s.data))n.push(`"data" phải là MẢNG (array), hiện là ${typeof s.data}.`);if(s.watch!==void 0&&!j(s.watch))n.push(`"watch" phải là OBJECT, hiện là ${Array.isArray(s.watch)?"array":typeof s.watch}.`);if(s.controls!==void 0&&!Array.isArray(s.controls))n.push(`"controls" phải là MẢNG container, hiện là ${typeof s.controls}.`);if(s.set!==void 0&&!j(s.set))n.push(`"set" phải là OBJECT, hiện là ${Array.isArray(s.set)?"array":typeof s.set}.`);let i=new Set(el),a=new Set,c=Array.isArray(s.data)?s.data:[];for(let g of c){if(!j(g)){o.push(`data[] chứa phần tử không phải object: ${JSON.stringify(g)?.slice(0,60)}`);continue}for(let[b,R]of Object.entries(g)){if(wn.has(b))continue;if(b.startsWith("#"))continue;if(As(R))a.add(b);else i.add(Ye(b))}}let l=0;Me(s,(g,b,R)=>{if(R)return;for(let C of["OUT","ERROROUT"]){let S=g[C];if(typeof S==="string"){if(no(S))i.add(Ye(S));else if(S.trim())l++}else if(j(S))for(let D of Object.keys(S))i.add(Ye(D))}});let u=new Map,d=new Map;Me(s,(g,b,R)=>{let C=g.CALL;if(typeof C==="string"){if(no(C))(R?d:u).set(Ye(C),b);else if(C.trim()&&!R)l++}let S=g.API;if(typeof S==="string"&&S.trim()&&!S.startsWith("/")&&!no(S))l++;for(let[D,x]of Object.entries(g)){if(typeof x!=="string")continue;let A=R||D.startsWith("#")?d:u;for(let E of x.matchAll(/\bCALL\(\s*([A-Za-z_$][\w$.]*)\s*\)/g))A.set(Ye(E[1]),b)}});for(let[g,b]of u)if(!a.has(g))n.push(`CALL "${g}" (tại ${b}) không khớp named action nào trong data[]. Named actions hiện có: ${[...a].join(", ")||"(không có)"}.`);let p=0,h=0,w=Array.isArray(s.controls)?s.controls:[];for(let g=0;g<w.length;g++){let b=w[g];if(p++,!j(b)||!Array.isArray(b.rows)){n.push(`controls[${g}] phải là container có "rows" (grid bắt buộc: container > rows > cols).`);continue}for(let R=0;R<b.rows.length;R++){let C=b.rows[R];if(!j(C)||!Array.isArray(C.cols)){n.push(`controls[${g}].rows[${R}] phải có "cols" (mảng Control Object).`);continue}for(let S=0;S<C.cols.length;S++){let D=C.cols[S];if(h++,!j(D))continue;if(D.w===void 0&&D.el!==void 0)o.push(`controls[${g}].rows[${R}].cols[${S}] (el: ${String(D.el)}) thiếu "w" — bắt buộc cho item trong cols.`)}}}let y=new Set;if(Me(s,(g,b)=>{if("children"in g&&(("el"in g)||("rows"in g)||("cols"in g)))n.push(`"children" không được hỗ trợ (tại ${b}) — dùng "innerHTML" (mảng Control Object) để lồng.`);let R=g.el;if(typeof R==="string"&&R){if(y.add(R),(R.startsWith("f-")||R.startsWith("fp-"))&&!uh.has(R)&&!t.localComponents.has(R))o.push(`Component "${R}" (tại ${b}) không có trong registry FUI (component-quickref.md) và không thấy trong components local — kiểm tra chính tả hoặc chạy component_list.`);else if(R.startsWith("t-")&&!Zc.has(R))o.push(`Cell component "${R}" (tại ${b}) không có trong danh sách t-* hợp lệ: ${[...Zc].join(", ")}.`);else if(R.startsWith("uc-")&&!t.localComponents.has(R))o.push(`Custom component "${R}" (tại ${b}) chưa thấy file .vue/local metadata — tạo component trước hoặc chạy component_list.`)}let C=g.attr;if(j(C))for(let[S,D]of Object.entries(C)){if(S.startsWith("@"))n.push(`attr "${S}" (tại ${b}) — module.json KHÔNG hỗ trợ shorthand "@"; dùng "v-on:${S.slice(1)}".`);else if(S.startsWith("#"))n.push(`attr "${S}" (tại ${b}) — module.json KHÔNG hỗ trợ slot shorthand "#"; dùng "v-slot:${S.slice(1)}".`);else if((S==="style"||S===":style")&&typeof D==="string"){if(D.split(";").map((A)=>A.trim()).filter(Boolean).filter((A)=>!/^(max-|min-)?width\s*:/i.test(A)).filter((A)=>!(g.el==="v-card"&&/^padding\s*:\s*0(px)?$/i.test(A))).length>0)o.push(`attr style "${D}" (tại ${b}) — module thực tế gần như không dùng style ngoài width (controls-styling-vocabulary.md). Cân nhắc bỏ.`)}else if((S.startsWith(":")||S.startsWith("v-bind:"))&&typeof D==="string"&&!ph(D))l++;else if(S==="v-model"&&typeof D==="string"){if(no(D)){let x=Ye(D);if(!i.has(x)&&!a.has(x))o.push(`v-model "${D}" (tại ${b}) — biến gốc "${x}" chưa khai báo trong data[]. Khai báo state trước để reactive hoạt động đúng.`)}else if(D.trim())l++}fh(S,D,b,n)}}),j(s.watch)){let g=(b,R)=>{for(let C of Object.keys(b)){if(C==="deep-watch"){if(j(b[C]))g(b[C],`${R}.deep-watch`);continue}let S=Ye(C);if(!i.has(S))o.push(`watch "${C}" (${R}) — biến "${S}" chưa khai báo trong data[], watcher sẽ không bao giờ chạy.`)}};g(s.watch,"watch")}for(let g of y){let b=dh[g];if(!b)continue;let R=b.filter((C)=>!t.importsText.includes(C.toLowerCase()));if(R.length>0)n.push(`"${g}" cần import: ${R.join(", ")} — chưa thấy trong _imports.json (module lẫn project). Dùng file_import_list/file_import_new (xem component-quickref.md).`)}let v=0,N=new Set;if(t.schemaProcs){let g=new Set;for(let b of t.schemaProcs.values())for(let R of b)g.add(R);if(Me(s,(b,R)=>{let C=b.API;if(typeof C!=="string"||!C||C.includes("{{"))return;v++;let S=null,D=null;if(C.startsWith("/")){let A=C.replace(/^\/+/,"").split("/").filter(Boolean);if(A.length<2)return;if(D=t.schemaProcs.get(A[0].toLowerCase())??null,!D){N.add(A[0]);return}S=A[1]==="auth"?A[2]??null:A[1]}else if(no(C))S=C,D=g;if(!S||S.includes("`")||!D)return;if(![`spAPI_${S}`,`spAPI_AUTH_${S}`,`spAPIFILE_${S}`,`spAPIFILE_UPLOAD_${S}`,S].some((A)=>D.has(A)))o.push(`API "${C}" (tại ${R}) — không tìm thấy spAPI_${S} trong schema local. Deploy SP trước hoặc chạy db_schema_get (bỏ qua nếu API thuộc backend khác).`)}),N.size>0){let b=[...N].sort();o.push(`${b.length} alias được gọi nhưng chưa có kết nối local — KHÔNG đối chiếu được tên SP: ${b.join(", ")}. `+`Kết nối từng cái bằng db_connect({ apiName: "${b[0]}", apiDomain: "…", dbToken: "…" }) để bước kiểm này chạy (bỏ qua nếu alias thuộc backend của nhóm khác).`)}}let I=vh(s,t,o);return $h(s,o),Ih(s,o),Th(s,n,o),{errors:n,warnings:o,stats:{stateKeys:i.size-el.size,namedActions:a.size,containers:p,controls:h,callRefs:u.size,apiRefs:v,crossWindowCalls:d.size,dynamicRefs:l,rightRefs:I}}}function Ns(){return{stateKeys:0,namedActions:0,containers:0,controls:0,callRefs:0,apiRefs:0,crossWindowCalls:0,dynamicRefs:0,rightRefs:0}}function Ph(e){return e.match(/^CREATE (?:PROCEDURE|FUNCTION)\s+(\S+)\s*\(/i)?.[1]??null}async function Nh(e){let t=dt.dirname(dt.dirname(e)),n=new Set;for(let c of[dt.join(e,"components"),dt.join(t,"components")]){for(let u of await _e(c))n.add(u.replace(/\.vue$/i,""));let l=await H(dt.join(c,"_components.json"),[]);for(let u of l)if(u?.comName)n.add(u.comName)}let o=[];for(let c of[e,t])o.push(await L(dt.join(c,"imports","_imports.json")));let s=await H(dt.join(t,"_db","_connections.json"),null),r=new Map,i=[];for(let[c,l]of Object.entries(s?.connections??{})){let u=l.apiName;if(!u)continue;let d=c,p=await H(dt.join(t,"_db",d,"schema.json"),null),h=Array.isArray(p?.procedures)?p.procedures:null;if(h===null)continue;if(!h.every((v)=>typeof v==="string")){i.push(d);continue}let w=u.toLowerCase(),y=r.get(w)??new Set;for(let v of h.map(Ph).filter((N)=>N!==null))y.add(v);r.set(w,y)}let a=await Wt(t);return{localComponents:n,importsText:o.join(`
104
- `).toLowerCase(),schemaProcs:r.size>0?r:null,schemaWarning:i.length>0?`_db/${i.join("/schema.json, _db/")}/schema.json sai định dạng (bản cache cũ) — bỏ qua bước đối chiếu tên API với stored procedure ở ${i.length>1?"các kết nối này":"kết nối này"}. Chạy db_schema_get để lấy bản mới.`:null,definedFunctionRights:a.functionRights,definedSystemRights:a.systemRights}}async function Ko(e){let t=await L(dt.join(e,"module.json"));if(!t.trim())return{errors:[`module.json rỗng hoặc không tồn tại trong ${e}.`],warnings:[],stats:Ns()};let n;try{n=JSON.parse(t)}catch(s){return{errors:[`module.json có lỗi JSON syntax: ${s.message}`],warnings:[],stats:Ns()}}let o=await Nh(e);return Eh(n,o)}function Go(e){let t=[];if(e.errors.length===0&&e.warnings.length===0)t.push("✅ module.json hợp lệ — không phát hiện lỗi ngữ nghĩa.");else{if(e.errors.length>0)t.push(`❌ ${e.errors.length} lỗi (sẽ hỏng khi chạy):`),t.push(...e.errors.map((o)=>` - ${o}`));if(e.warnings.length>0){if(t.length)t.push("");t.push(`⚠️ ${e.warnings.length} cảnh báo (nên xem lại):`),t.push(...e.warnings.map((o)=>` - ${o}`))}}let n=e.stats;if(t.push(""),n.dynamicRefs>0)t.push(`ℹ️ ${n.dynamicRefs} biểu thức động (\`:attr\` phức tạp, CALL/API/v-model/OUT dạng \`{{…}}\`/gọi hàm/template string) KHÔNG kiểm tĩnh được — biến bên trong chúng không được đối chiếu với data[]. "Hợp lệ" ở đây KHÔNG có nghĩa là chạy đúng: dùng module_simulate để phủ phần này.`);return t.push(`Thống kê: ${n.stateKeys} state · ${n.namedActions} named action · ${n.containers} container / ${n.controls} control · ${n.callRefs} CALL ref${n.apiRefs?` · ${n.apiRefs} API ref đối chiếu schema`:""}${n.crossWindowCalls?` · ${n.crossWindowCalls} CALL xuyên cửa sổ (thuộc module khác, không kiểm được từ đây)`:""}${n.rightRefs?` · ${n.rightRefs} mã quyền đối chiếu _db/rights.json`:""}`),t.join(`
105
- `)}function ee(e,t){let n=e.replace(/\s+/g," ").trim();return n.length>t?n.slice(0,t)+"…":n}function oo(e){if(e===null)return"null";if(Array.isArray(e))return e.length===0?"[]":`[${e.length} items]`;if(j(e)){let t=Object.keys(e);return t.length===0?"{}":`{${ee(t.join(","),40)}}`}if(typeof e==="string")return`"${ee(e,30)}"`;return String(e)}function Ae(e,t=0){if(t>3)return"…";if(typeof e==="string")return ee(e,70);if(Array.isArray(e))return e.map((o)=>Ae(o,t+1)).join(" ; ");if(!j(e))return ee(JSON.stringify(e)??"",60);let n=[];if(e.CONFIRM!==void 0)n.push(`CONFIRM "${ee(String(e.CONFIRM),40)}"`);if(e.IF!==void 0)n.push(`IF(${ee(String(e.IF),50)})`);if(e.API!==void 0)n.push(`API ${ee(String(e.API),60)}`);if(e.METHOD!==void 0)n.push(`[${String(e.METHOD)}]`);if(e.IN!==void 0)n.push(`IN{${j(e.IN)?ee(Object.keys(e.IN).join(","),50):ee(String(e.IN),40)}}`);if(e.OUT!==void 0)n.push(`OUT→${j(e.OUT)?Object.keys(e.OUT).join(","):String(e.OUT)}`);if(e.ARRAY!==void 0)n.push(`ARRAY ${ee(String(e.ARRAY),30)}${e.COL!==void 0?` COL ${ee(String(e.COL),30)}`:""}`);if(e.CALL!==void 0)n.push(`CALL ${ee(String(e.CALL),40)}`);if(e.FUN!==void 0)n.push(`FUN ${ee(String(e.FUN),40)}`);if(e.EXE!==void 0)n.push(`EXE ${ee(String(e.EXE),70)}`);if(e.MESS!==void 0)n.push(`MESS "${ee(String(e.MESS),40)}"`);if(e.MESSBOX!==void 0)n.push(`MESSBOX "${ee(String(e.MESSBOX),40)}"`);if(e.THEN!==void 0)n.push(`THEN[${Ae(e.THEN,t+1)}]`);if(e.ELSE!==void 0)n.push(`ELSE[${Ae(e.ELSE,t+1)}]`);if(e.CALLBACK!==void 0)n.push(`CALLBACK[${Ae(e.CALLBACK,t+1)}]`);if(e.ERROROUT!==void 0)n.push(`ERROROUT→${String(e.ERROROUT)}`);if(e.ERRORCALLBACK!==void 0)n.push(`ERRORCALLBACK[${Ae(e.ERRORCALLBACK,t+1)}]`);if(e.CANCEL!==void 0)n.push(`CANCEL[${Ae(e.CANCEL,t+1)}]`);return n.join(" ")||ee(JSON.stringify(e)??"",60)}var Ah=["v-model","v-if","v-show","v-for","label",":label","placeholder",":items",":data",":headers",":config",":option",":options","value",":value","readonly","disabled",":disabled","multiple","type"];function Dh(e){let t=[],n=e.w!==void 0?`w=${String(e.w)}`:"w=?",o=typeof e.el==="string"&&e.el?e.el:e.rows?"(container)":"(text)";t.push(`${n} ${o}`);let s=j(e.attr)?e.attr:{};for(let r of Ah){let i=s[r];if(i===void 0)continue;t.push(`${r}=${typeof i==="string"?ee(i,45):oo(i)}`)}for(let[r,i]of Object.entries(s)){if(!r.startsWith("v-on:"))continue;let a=r.slice(5);t.push(`${a}→${typeof i==="string"?ee(i,60):Ae(i)}`)}for(let r of["innerText","html","text"])if(typeof e[r]==="string"&&e[r])t.push(`"${ee(String(e[r]),35)}"`);return t.join(" ")}function sl(e,t,n,o){let s=o?"└─":"├─",r=t+(o?" ":"│ ");if(!j(e)){n.push(`${t}${s} ${ee(JSON.stringify(e)??String(e),60)}`);return}n.push(`${t}${s} ${Dh(e)}`);let i=e.innerHTML;if(Array.isArray(i))i.forEach((a,c)=>sl(a,r,n,c===i.length-1));if(Array.isArray(e.rows))il(e.rows,r,n)}function il(e,t,n){e.forEach((o,s)=>{if(!j(o)||!Array.isArray(o.cols)){n.push(`${t}row ${s+1}: ❌ thiếu "cols"`);return}let r=o.cols,a=r.map((u)=>j(u)?u.w:void 0).filter((u)=>typeof u==="number"),c="";if(r.length>1&&a.length===r.length){let u=a.reduce((d,p)=>d+p,0);c=` (Σw=${u}${u>12?" ⚠ vượt 12":""})`}let l=[];for(let u of["v-if","v-show","prop"])if(typeof o[u]==="string")l.push(`${u}=${ee(String(o[u]),40)}`);n.push(`${t}row ${s+1}${l.length?` [${l.join(" ")}]`:""}${c}`),r.forEach((u,d)=>sl(u,t+" ",n,d===r.length-1))})}function al(e){if(!j(e))return"❌ module.json gốc không phải object.";let t=e,n=[],o=new Map,s=new Map,r=[],i=Array.isArray(t.data)?t.data:[];for(let d of i){if(!j(d))continue;let p=Object.keys(d);if(p.length>0&&p.every((h)=>wn.has(h))){r.push(d);continue}for(let[h,w]of Object.entries(d)){if(wn.has(h)){r.push({[h]:w});continue}if(h.startsWith("#"))continue;if(As(w))s.set(h,w);else o.set(Ye(h),w)}}let a=JSON.stringify({c:t.controls,w:t.watch,s:t.set,a:[...s.values()],u:r})??"",c=(d)=>new RegExp(`\\b${d.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`).test(a);n.push("═══ LAYOUT (controls) ═══");let l=Array.isArray(t.controls)?t.controls:[];if(l.length===0)n.push("(không có controls)");let u=[];if(l.forEach((d,p)=>{if(!j(d)){n.push(`[container ${p+1}] ❌ không phải object`);return}let h=typeof d.el==="string"?d.el:"",w=h==="hidden-container",y=j(d.attr)?d.attr:{},v=[];if(h)v.push(`el=${h}`);if(typeof d.prop==="string")v.push(`prop="${ee(d.prop,40)}"`);for(let N of["v-if","v-show","v-model"]){let I=d[N]??y[N];if(typeof I==="string")v.push(`${N}=${ee(I,40)}`)}if(n.push(`[container ${p+1}${w?" — DIALOG":""}] ${v.join(" ")||"(mặc định)"}`),w){let N=d["v-model"]??y["v-model"];if(typeof N==="string")u.push({vModel:Ye(N),where:`container ${p+1}`});else u.push({vModel:"",where:`container ${p+1}`})}if(Array.isArray(d.rows))il(d.rows," ",n);else n.push(' ❌ thiếu "rows"')}),n.push("","═══ STATE (data) ═══"),o.size===0)n.push("(không có state)");for(let[d,p]of o){let h=c(d);n.push(`${d} = ${oo(p)}${h?"":" ⚠ không thấy dùng ở controls/watch/actions"}`)}if(n.push("","═══ ACTIONS ═══"),s.size===0&&r.length===0)n.push("(không có action)");for(let[d,p]of s){let h=JSON.stringify({c:t.controls,w:t.watch,s:t.set,u:r,a:[...s.entries()].filter(([y])=>y!==d).map(([,y])=>y)})??"",w=new RegExp(`\\b${d.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`).test(h);n.push(`${d}: ${Ae(p)}${w?"":" ⚠ không thấy nơi gọi (CALL/watch/v-on)"}`)}if(r.forEach((d,p)=>{n.push(`(auto-startup ${p+1}): ${Ae(d)}`)}),j(t.watch)&&Object.keys(t.watch).length>0){n.push("","═══ WATCH ═══");let d=(p,h)=>{for(let[w,y]of Object.entries(p)){if(w==="deep-watch"&&j(y)){d(y,"deep ");continue}n.push(`${h}${w} → ${Ae(y)}`)}};d(t.watch,"")}if(u.length>0){n.push("","═══ DIALOGS ═══");let d=JSON.stringify(t)??"";for(let p of u){if(!p.vModel){n.push(`${p.where}: ⚠ hidden-container không có v-model — không điều khiển mở/đóng được`);continue}let h=p.vModel.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),w=new RegExp(`\\b${h}\\b\\s*=\\s*true`).test(d);n.push(`${p.where}: v-model=${p.vModel}${w?` — có lệnh mở (${p.vModel} = true)`:` ⚠ không thấy lệnh mở "${p.vModel} = true" ở đâu`}`)}}if(j(t.set)&&Object.keys(t.set).length>0){n.push("","═══ SET (override) ═══");for(let[d,p]of Object.entries(t.set))n.push(`${d} = ${typeof p==="string"?ee(p,60):oo(p)}`)}return n.push("",`Thống kê: ${o.size} state · ${s.size} named action · ${r.length} auto-startup · ${l.length} container · ${u.length} dialog`),n.join(`
106
- `)}var xh=[[/cũ (\d+) ngày/g,"$1 days old"],[/cũ (\d+) giờ/g,"$1 h old"],[/\(control trong f-table — dùng row (\d+) của /g,"(control in f-table; using row $1 of "],[/(✘ STEP \d+) lỗi: /g,"$1 error: "]],Oh={project_sync:"fui project sync",module_get:"fui module get",file_import_list:"fui import list"},_h={'khai báo "function(...)"':'"function(...)" declaration','khai báo "function ten(...)"':'"function name(...)" declaration','lệnh "return"':'"return" statement','khai báo "var"':'"var" declaration','khai báo "let"':'"let" declaration','khai báo "const"':'"const" declaration','dấu ";" nối nhiều câu lệnh':'";" joining statements','" (tại ':'" (at ',") chứa ":") contains "," — giá trị binding đi thẳng vào template compiler của Vue, ":" — Vue compiles a binding as ONE expression ","chỉ nhận MỘT BIỂU THỨC thuần (object/array literal, ternary, chuỗi .map()/.filter() arrow, truy cập thuộc tính). ":"(literal, ternary, .map()/.filter() arrow, property access). ","Statement ở đây làm compile template hỏng → TRANG TRẮNG mà không có JS error hay [Vue warn] nào. ":"A statement breaks the template: BLANK PAGE with no JS error or [Vue warn]. ",'Cách sửa: đưa logic vào một hàm trong script.js (function buildX(){ … vueData.x = …; }), gọi qua "EXE": "buildX()" ':'Fix: move the logic into a script.js function (function buildX(){ … vueData.x = …; }), call it with "EXE": "buildX()" ','trong CALLBACK của action nạp dữ liệu, rồi "':'in the load action CALLBACK, and point "','" chỉ trỏ tới biến vueData đó (vd "':'" at that vueData var (e.g. "',"Chi tiết: references/advanced-techniques.md.":"See: fui skill ref references/advanced-techniques.md.","(chuẩn mặc định fui/DESIGN.md — chỉ giữ nguyên nếu user yêu cầu rõ kiểu khác)":"(fui/DESIGN.md default; keep only if the user asked for another style)","Module gác quyền bằng ":"Module gates on "," mã literal (":" literal right codes (",") nhưng chưa có cache _db/rights.json ":") but _db/rights.json is not cached, ","để đối chiếu — KHÔNG kiểm được mã nào có thật. Mã không tồn tại làm rightTest() luôn trả false: ":"so none can be checked. An unknown code makes rightTest() always false: ","control/menu biến mất im lặng, không lỗi JS. Chạy right_function_list + right_system_list (một lần cho mỗi project) rồi validate lại.":"the control/menu silently disappears. Run fui right function list and fui right system list (once per project), then validate again.",'" chưa được định nghĩa cho project (_db/rights.json có ':'" is not defined for the project (_db/rights.json has '," mã: ":" codes: "," mã). ":" codes). ","rightTest() sẽ luôn trả false → control/menu gác bằng mã này biến mất mà không báo lỗi. ":"rightTest() is always false, so controls gated by it silently disappear. ","Tạo mã: right_function_new({ functionCode: ":"Create: fui right function new --code ",', functionName: "…" }); hoặc chạy right_function_list nếu mã đã có trên server mà cache cũ.':' --name "…", or fui right function list if the cache is stale.',"hoặc chạy right_function_list nếu mã đã có trên server mà cache cũ.":"or fui right function list if the cache is stale."," chưa được định nghĩa cho project (_db/rights.json có: ":" is not defined for the project (_db/rights.json has: ","(chưa mức nào)":"(none)"," — mức 9 luôn hợp lệ":"; 9 is always valid","Không ai được cấp mức này thì control gác bằng nó biến mất im lặng. ":"Nobody holds this level, so gated controls silently disappear. ","Định nghĩa: right_system_new({ sysRight: ":"Define: fui right system new ",', note: "…" }); hoặc chạy right_system_list nếu cache cũ.':' --note "…", or fui right system list if the cache is stale.','Action gọi API "':'Action calling API "',") có vẻ là thao tác XÓA nhưng thiếu CONFIRM — mọi action xóa phải hỏi xác nhận.":") looks like a DELETE without CONFIRM; every delete action must confirm.","Nút/mục xóa (tại ":"Delete button/item (at ",') đang màu "':') is color "','" — nút xóa dùng màu "warning" (cam), không dùng error/đỏ.':'"; delete buttons use "warning" (orange), not error/red.',"v-toolbar (tại ":"v-toolbar (at ",') có color/dark — toolbar tiêu đề phải "dense flat" cùng màu nền với thân dialog, không tô màu/dark riêng.':') has color/dark; a title toolbar must be "dense flat" on the dialog background, no own color/dark.',"Dialog (tại ":"Dialog (at "," — bộ width chuẩn: 400 / 550 / 600 / 650 / 800.":"; standard widths: 400 / 550 / 600 / 650 / 800.","v-card trong dialog (tại ":"v-card in dialog (at ",') có elevation/outlined — bỏ đi: v-dialog đã tự có bóng đổ, thêm nữa là bóng chồng bóng. v-card chỉ cần class "pa-0" (triệt đệm để toolbar tiêu đề sát mép).':') has elevation/outlined; remove it (v-dialog already casts a shadow). v-card needs only class "pa-0".',"Control (tại ":"Control (at ",') mang class "flex-md-grow-0 ml-2" — dấu hiệu pattern label-div rời đã loại bỏ; form dùng floating label ("label" trong attr của field).':') has class "flex-md-grow-0 ml-2", the retired separate label-div pattern; forms use floating labels ("label" in the field attr).'," trong dialog (tại ":" in dialog (at ",') thiếu "label" — form dialog dùng floating label.':') lacks "label"; dialog forms use floating labels.'," (tại ":" (at ",") — phản hồi/thông báo dùng hệ thống có sẵn của FUI: MESS (toast) / MESSBOX (hộp thoại) trong action, CONFIRM để xác nhận — không tự dựng ":"); use FUI feedback instead: MESS (toast) / MESSBOX (dialog) in actions, CONFIRM to confirm; do not build "," trong controls.":" in controls.",') không có ":height" — bảng sẽ tự giãn theo số dòng và làm cả TRANG cuộn. Bảng data chính nên đặt `":height": -16` (fill tới đáy màn hình, chừa 16px — component tự đo vị trí thật, không cần tính tay); trong dialog dùng số dương (px cố định, vd 360). Bảng ngắn/lookup chủ ý tự giãn thì bỏ qua.':') has no ":height", so it grows with its rows and scrolls the whole PAGE. Main data table: `":height": -16` (fills to the screen bottom, self-measured); in a dialog a positive px (e.g. 360). Skip for short lookup tables.',') tính ":height" thủ công theo viewport (`':') computes ":height" from the viewport (`',"`) — không cần nữa: ":"`); not needed: ",' tự đo vị trí thật của bảng sau layout và tự cập nhật khi resize. Thay bằng `":height": -16` (số âm = khoảng chừa ở đáy).':' measures itself after layout and on resize. Use `":height": -16` (negative = bottom gap).','Nút "':'Button "','" trong v-card-actions (tại ':'" in v-card-actions (at ',") — dialog chuẩn chỉ dùng icon close trên toolbar, không thêm nút Đóng ở actions.":"); standard dialogs close via the toolbar icon only, no Close button in actions.",' có "w": ':' has "w": '," — vùng chết 13–24: V2 hiểu là ":" — dead zone 13–24: V2 reads ",'px, còn V3 sinh class "v-col-':'px, V3 emits class "v-col-','" mà Vuetify 3 không định nghĩa (chỉ có .v-col-1 … .v-col-12) nên cột KHÔNG có bề rộng nào, rơi về chia đều. Dùng 1–12 (chia cột) hoặc >= ':'", which Vuetify 3 lacks (only .v-col-1 … .v-col-12), so the column gets NO width. Use 1–12 (grid) or >= ',' (px, kèm `"col": { "class": "shrink" }`).':' (px, with `"col": { "class": "shrink" }`).',' (px) kèm `"col": { "shrink": ... }` — dạng thuộc tính trần là prop của v-flex (Vuetify 2); Vuetify 3 không có prop đó nên nó trơ và cột vẫn giãn hết chỗ trống. Viết `"col": { "class": "shrink" }` để đúng ở cả hai framework.':' (px) with `"col": { "shrink": ... }`: a bare v-flex prop (Vuetify 2), inert in Vuetify 3, so the column still grows. Write `"col": { "class": "shrink" }` for both.'," — số >= ":" — >= "," nghĩa là bề rộng cố định px, nhưng cột grid mặc định flex-grow:1 nên flex-basis ":" means fixed px, but grid columns default to flex-grow:1, so flex-basis ",'px KHÔNG có tác dụng: cột vẫn giãn hết chỗ trống (đo thật: w=300 ra 687px ở V2, 738px ở V3). Thêm `"col": { "class": "shrink" }`.':'px has NO effect and the column still grows (measured: w=300 gives 687px V2, 738px V3). Add `"col": { "class": "shrink" }`.'," — `v-spacer` mà có `shrink` thì co lại còn 0px (đo thật: 24px ở V3, 0px ở V2), không đẩy được nhóm sau sang phải nữa và cả hàng dồn về trái. Bỏ `shrink` khỏi riêng cột này.":" — `v-spacer` with `shrink` collapses to 0px and no longer pushes the next group right. Remove `shrink` from this column.",' nằm cùng hàng với `v-spacer`, `w` rỗng và chỉ có `"col": { "shrink": ... }` — dạng thuộc tính trần là prop của v-flex (Vuetify 2); Vuetify 3 không có prop đó nên cột vẫn giãn và chiếm một phần đều. Viết `"col": "shrink"` (hoặc `{ "class": "shrink" }`).':' shares a row with `v-spacer`, has empty `w` and only `"col": { "shrink": ... }`: a bare v-flex prop (Vuetify 2), inert in Vuetify 3, so the column grows. Write `"col": "shrink"` (or `{ "class": "shrink" }`).',' nằm cùng hàng với `v-spacer` nhưng thiếu `shrink` — `w` rỗng KHÔNG có nghĩa "tự co theo nội dung": runtime bọc phần bề rộng trong `if (c.w)` nên cột này không có class grid lẫn flex-basis, còn trơ `flex-grow: 1` và được CHIA PHẦN TRỐNG y như `v-spacer`. Hệ quả: control bị kéo dãn, hở khoảng giữa các nút, nhóm cuối hàng không sát mép phải. Chuẩn cho hàng nhiều control: MỌI cột `"col": "shrink"`, trừ đúng `v-spacer`.':' shares a row with `v-spacer` but lacks `shrink`: empty `w` is NOT fit-content, the column keeps `flex-grow: 1` and SPLITS free space like `v-spacer` (stretched controls, gaps, last group off the right edge). In multi-control rows EVERY column is `"col": "shrink"` except `v-spacer`.'," gộp ":" puts "," lệnh trong MỘT object `{}` (":" commands in ONE object `{}` (",") — chỉ `":"); only `","` chạy, còn ":"` runs, ",' bị bỏ IM LẶNG (không JS error, không [Vue warn]). `vueAction` dò key lệnh theo thứ tự cố định API → CALL → EXE → FUN → IF → MESS → MESSBOX và `return` ngay ở nhánh đầu tiên khớp, nên thứ tự viết không có tác dụng. Nhiều lệnh có thứ tự trước sau thì phải là MẢNG, mỗi lệnh một object: `[{ "':' are SILENTLY dropped: `vueAction` checks API → CALL → EXE → FUN → IF → MESS → MESSBOX and runs only the first match. Ordered commands need an ARRAY, one per object: `[{ "'," có lệnh `":" has command `","` nằm chung object với key gán dữ liệu ":"` in the same object as data keys "," — fragment gán chạy ở nhánh CUỐI của `vueAction`, nhánh đó chỉ tới lượt khi KHÔNG key lệnh nào khớp, nên các key này không bao giờ được gán. Tách thành mảng: `[{ ":" — `vueAction` assigns only when NO command key matches, so these keys are never set. Split into an array: `[{ ","module.json gốc phải là một object với các khối data/watch/controls/set.":"module.json root must be an object with data/watch/controls/set.",'Khối cấp 1 không chuẩn: "':'Unknown top-level key: "','" — module.json chỉ có data/watch/controls/set (xem module-json-anatomy.md).':'"; module.json has only data/watch/controls/set (see module-json-anatomy.md).','"data" phải là MẢNG (array), hiện là ':'"data" must be an array, got ','"watch" phải là OBJECT, hiện là ':'"watch" must be an object, got ','"controls" phải là MẢNG container, hiện là ':'"controls" must be an array of containers, got ','"set" phải là OBJECT, hiện là ':'"set" must be an object, got ',"data[] chứa phần tử không phải object: ":"data[] has a non-object item: ",") không khớp named action nào trong data[]. Named actions hiện có: ":") matches no named action in data[]. Named actions: ","(không có)":"(none)",'] phải là container có "rows" (grid bắt buộc: container > rows > cols).':'] must be a container with "rows" (container > rows > cols).','] phải có "cols" (mảng Control Object).':'] needs "cols" (array of Control Objects).',') thiếu "w" — bắt buộc cho item trong cols.':') lacks "w" (required in cols).','"children" không được hỗ trợ (tại ':'"children" is not supported (at ',') — dùng "innerHTML" (mảng Control Object) để lồng.':'); nest with "innerHTML" (array of Control Objects).',") không có trong registry FUI (component-quickref.md) và không thấy trong components local — kiểm tra chính tả hoặc chạy component_list.":") is neither in the FUI registry (component-quickref.md) nor a local component; check spelling or run fui component list.",") không có trong danh sách t-* hợp lệ: ":") is not a valid t-* cell: ",") chưa thấy file .vue/local metadata — tạo component trước hoặc chạy component_list.":") has no .vue/local metadata; create it first or run fui component list.",') — module.json KHÔNG hỗ trợ shorthand "@"; dùng "v-on:':'); module.json has no "@" shorthand; use "v-on:',') — module.json KHÔNG hỗ trợ slot shorthand "#"; dùng "v-slot:':'); module.json has no "#" slot shorthand; use "v-slot:',") — module thực tế gần như không dùng style ngoài width (controls-styling-vocabulary.md). Cân nhắc bỏ.":"); real modules rarely style beyond width (controls-styling-vocabulary.md). Consider removing it.",') — biến gốc "':'); root var "','" chưa khai báo trong data[]. Khai báo state trước để reactive hoạt động đúng.':'" is not declared in data[]; declare it so reactivity works.',') — biến "':') — var "','" chưa khai báo trong data[], watcher sẽ không bao giờ chạy.':'" is not declared in data[]; the watcher never runs.','" cần import: ':'" needs import: '," — chưa thấy trong _imports.json (module lẫn project). Dùng file_import_list/file_import_new (xem component-quickref.md).":", missing from _imports.json (module and project). Use fui import list / fui import new (see component-quickref.md).",") — không tìm thấy spAPI_":"); no spAPI_"," trong schema local. Deploy SP trước hoặc chạy db_schema_get (bỏ qua nếu API thuộc backend khác).":" in the local schema. Deploy the SP or run fui schema pull (ignore if another backend)."," alias được gọi nhưng chưa có kết nối local — KHÔNG đối chiếu được tên SP: ":" called alias(es) have no local connection, so SP names are NOT checked: ",'Kết nối từng cái bằng db_connect({ apiName: "':"Connect each: fui db add ",'", apiDomain: "…", dbToken: "…" }) để bước kiểm này chạy (bỏ qua nếu alias thuộc backend của nhóm khác).':" -p <project> --token - --api-domain … (ignore if another team's backend).","/schema.json sai định dạng (bản cache cũ) — bỏ qua bước đối chiếu tên API với stored procedure ở ":"/schema.json has an old format; API names were not checked against SPs for ","các kết nối này":"these connections","kết nối này":"this connection",". Chạy db_schema_get để lấy bản mới.":". Run fui schema pull.","module.json rỗng hoặc không tồn tại trong ":"module.json is empty or missing in ","module.json có lỗi JSON syntax: ":"module.json JSON syntax error: ","✅ module.json hợp lệ — không phát hiện lỗi ngữ nghĩa.":"✅ module.json valid: no semantic errors."," lỗi (sẽ hỏng khi chạy):":" error(s) (break at runtime):"," cảnh báo (nên xem lại):":" warning(s) (review):",' biểu thức động (`:attr` phức tạp, CALL/API/v-model/OUT dạng `{{…}}`/gọi hàm/template string) KHÔNG kiểm tĩnh được — biến bên trong chúng không được đối chiếu với data[]. "Hợp lệ" ở đây KHÔNG có nghĩa là chạy đúng: dùng module_simulate để phủ phần này.':' dynamic expression(s) (complex `:attr`, `{{…}}` CALL/API/v-model/OUT, calls, template strings) are NOT checked statically. "Valid" does NOT mean it runs: use fui module simulate.',"Thống kê: ":"Stats: "," API ref đối chiếu schema":" API ref checked against schema"," CALL xuyên cửa sổ (thuộc module khác, không kiểm được từ đây)":" cross-window CALL (other module, not checked)"," mã quyền đối chiếu _db/rights.json":" right code checked against _db/rights.json","Chưa có cache _db/rights.json — KHÔNG kiểm được ":"_db/rights.json not cached: cannot check whether "," mã FunctionRight (":" FunctionRight code(s) (","có thật hay không. Preview cấp sẵn đúng những mã này nên control vẫn hiện ở ĐÂY; user thật chỉ thấy nếu mã đã ":"exist. The preview grants them, so controls show HERE; real users see them only if the codes are ","được định nghĩa. Chạy right_function_list (một lần cho mỗi project) rồi render lại.":"defined. Run fui right function list (once per project), then render again.","\uD83D\uDEA8 FunctionRight CHƯA ĐƯỢC ĐỊNH NGHĨA trên hệ thống acc: ":"\uD83D\uDEA8 FunctionRight NOT DEFINED in acc: "," — preview cấp sẵn nên control vẫn ":" — the preview grants them, so controls ","hiện ở ĐÂY, nhưng chạy thật thì rightTest() luôn trả false → control/menu biến mất mà không có lỗi JS nào. ":"show HERE, but live rightTest() is false and they silently disappear. ",'Tạo mã: right_function_new({ functionCode: …, functionName: "…" }); hoặc right_function_list nếu cache cũ.':'Create: fui right function new --code … --name "…", or fui right function list if the cache is stale.',"\uD83D\uDEA8 CẤU TRÚC MOCK CHƯA ĐƯỢC KIỂM CHỨNG — nghi ngờ tự suy diễn thay vì đọc từ SP thật:":"\uD83D\uDEA8 UNVERIFIED MOCK SHAPE: likely guessed instead of read from the real SP:",'"] khớp tên SP CÓ THẬT "':'"] matches REAL SP "','" trong schema local, nhưng SP đó chưa từng được db_sp_verify đọc thân trong phiên này.':'", whose body fui sp verify has not read in this session.',' → Chạy db_sp_verify({ name: "':" → Run fui sp verify ",'" }) để đọc thân SP thật rồi COPY NGUYÊN VĂN snippet apiMocks nó sinh sẵn — không tự đoán tên cột/shape cho SP đã tồn tại, kết quả mô phỏng sẽ lệch với thực tế.':" and copy its apiMocks snippet VERBATIM; do not guess columns/shape of an existing SP.",") — gọi tAPI thật thành công":"): live tAPI call OK",") gọi thật thất bại (":") live call failed (",") — rơi xuống cache/tự sinh":"); fell back to cache/generated",") có lệnh ghi — KHÔNG gọi thật, dùng cache/tự sinh":") writes: NOT called live, cache/generated used",") có đoạn đọc tĩnh không nhìn xuyên qua được (SQL động / gọi SP khác) — KHÔNG gọi thật, dùng cache/tự sinh":") has opaque reads (dynamic SQL / nested SP): NOT called live, cache/generated used",") chưa có thân SP local để phân loại an toàn — KHÔNG gọi thật, dùng cache/tự sinh":") has no local body to classify: NOT called live, cache/generated used",") đã đạt trần ":") hit the cap of "," lượt gọi thật/lượt simulate — dùng cache/tự sinh":" live calls per run; cache/generated used",") — dùng _testdata cache (nguồn: ":"): _testdata cache (source: ",", lưu lúc ":", saved ",") — tự sinh mock từ cấu trúc SELECT, đã lưu cache":"): mock generated from the SELECT shape, cached","… (trace bị cắt — quá dài)":"… (trace truncated)",") — stub, không mô phỏng UI thật":"): stub, UI not simulated"," chưa hỗ trợ trong simulator — trả undefined":" unsupported in the simulator: undefined"," — stub (sub-window không mô phỏng)":": stub (sub-window not simulated)","(giả lập user đóng dialog → chạy onclose)":"(simulated dialog close → onclose)",'" → OK (mặc định simulator)':'" → OK (simulator default)','" → HỦY':'" → CANCEL','đọc cross-window "':'cross-window read "','" — không mô phỏng được, gán undefined':'": not simulated, undefined'," (không mô phỏng)":" (not simulated)",'" undefined — CALL trỏ tới action không tồn tại?':'" undefined: CALL to a missing action?',"Lỗi khi chạy action: ":"Action error: ",'" không tồn tại (simulator chỉ có các hàm FUI chuẩn)':'" does not exist (simulator has only standard FUI functions)','" lỗi eval: ':'" eval error: '," → coi như false":" → treated as false",'(mock khớp "':'(mock matched "','không có mock cho API "':'no mock for API "','" — trả mặc định {data: []}':'": default {data: []}'," (mock lỗi)":" (error mock)",'" lỗi không được bắt (không có ERROROUT) → hiện ERROR DIALOG mặc định: ':'" error not handled (no ERROROUT) → default ERROR DIALOG: ',"← (không có OUT)":"← (no OUT)"," thay đổi → ":" changed → ","watch cascade vượt ":"watch cascade over "," vòng — nguy cơ VÒNG LẶP VÔ HẠN giữa các watch/action":" rounds: possible INFINITE LOOP between watch/actions","MỞ \uD83D\uDD32":"OPEN \uD83D\uDD32","ĐÓNG ▪":"CLOSED ▪","Lỗi khi init data[]: ":"data[] init error: ",'không tìm thấy control có label "':'no control labeled "','" kèm v-on:click/:action':'" with v-on:click/:action',"(khớp ":"(matched "," control — dùng #":" controls; using #",'control nằm trong f-table nhưng "':'control is in an f-table but "','" rỗng — item = {} (truyền "item" trong event để chỉ định)':'" is empty: item = {} (pass "item" in the event)',"exe lỗi: ":"exe error: "," không hợp lệ — cần một trong: set / click / call / exe / assert":" invalid: needs one of set / click / call / exe / assert","SystemRight=9 (mặc định, quyền cao nhất)":"SystemRight=9 (default, highest)","] (mặc định — quét được từ module.json)":"] (default: scanned from module.json)","FunctionRight=[] (mặc định — không quét được mã nào trong module.json)":"FunctionRight=[] (default: no codes found in module.json)",". rightTest()/v-if theo FunctionRight khác mã trên sẽ trả false — truyền tham số `user` để test vai trò hạn chế hơn.":". rightTest()/v-if on other FunctionRight codes is false; pass --user to test a narrower role.","═══ KẾT QUẢ ═══":"═══ RESULT ═══","── Cảnh báo ──":"── Warnings ──","(stub đã dùng — kết quả các hàm này KHÔNG phản ánh runtime thật: ":"(stubs used; their results do NOT reflect the real runtime: ","── STATE CUỐI ──":"── FINAL STATE ──"," assert đạt":" assert passed"," · dialog đang mở: ":" · open dialogs: ","\n\n── Live/Cache data (SP chưa có mock, đã tự resolve) ──\n":`
102
+ `),e.data)b(e.data)}async function Ns(e){return(await e.targets.resolveProject(f(e.values,"project"))).projectId}async function Yc(e){return e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module"))}function ih(e,t){if(e===void 0)return;if(e==="true")return!0;if(e==="false")return!1;throw new h(`${t} takes true or false`)}function Qc(e){if(e===void 0)return;let t=e.toUpperCase();if(t!=="V2"&&t!=="V3")throw new h("--framework must be V2 or V3");return t}var ah={name:"use",usage:"fui use [<target> | -p P -m M | --module-dir D] | fui use --clear",summary:"Show, set or clear the session target (target: /project/module, a live URL, or a display name)",options:{...gn,"module-dir":{type:"string"},clear:{type:"boolean"}},async run(e){let{values:t,positionals:n,session:o,render:s}=e;if(t.clear===!0){await o.clearTarget(),b(`session target cleared
103
+ `);return}let r=n[0],i=f(t,"project"),a=f(t,"module"),c=f(t,"module-dir");if(!r&&!i&&!a&&!c){let l=await o.getTarget();if(!l){T("# no session target; set one: fui use <project>/<module>");return}b(z([{...l,session:o.file}],s));return}if(i&&!a||!i&&a)throw new h("pass both -p and -m");eo(await Uc(_(e),{target:r,projectId:i,moduleId:a,moduleDir:c}))}},ch={name:"resolve",usage:"fui resolve <target>",summary:"Resolve /project/module, a live module URL or a display name to ids (no session change)",async run(e){let t=e.positionals[0];if(!t)throw new h("missing <target>");Go(await Fc(_(e),t))}},lh={name:"project list",usage:"fui project list",summary:"List projects you can access",async run(e){let t=(await Hc(_(e))).map((n)=>({group:n.GroupName||"Khac",id:n.ProjectID,name:n.ProjectName,framework:n.Framework??"",domain:n.pDomain??""}));b(z(t,e.render))}},uh={name:"project sync",usage:"fui project sync [-p project]",summary:"Re-fetch project data (project.json, components, imports, module list); archives what changed or vanished",async run(e){eo(await Vc(_(e),await Ns(e)))}},dh={name:"module list",usage:"fui module list [-p project]",summary:"List modules from the server and refresh the local module cache",async run(e){let{rows:t,notes:n}=await Wc(_(e),await Ns(e));for(let o of n)T(`# ${o}`);b(z(t.map((o)=>({id:o.ModuleID,name:o.ModuleName,framework:o.Framework??"",domain:o.mDomain??"",htmlOnly:o.HTMLOnly?"yes":"no",updated:o.UpdateTime??"",by:o.UpdateUser??""})),e.render))}},ph={name:"module new",usage:"fui module new <moduleId> -p <project> --name <name> [--framework V2|V3] [--html-only] [--domain d] [--set key=json ...]",summary:"Create an empty module on the server (then fui module get)",options:{name:{type:"string"},framework:{type:"string"},"html-only":{type:"boolean"},domain:{type:"string"},set:{type:"string",multiple:!0}},async run(e){let t=e.positionals[0],n=f(e.values,"name");if(!t||!n)throw new h("need <moduleId> and --name");eo(await qc(_(e),{projectId:await Ns(e),moduleId:t,moduleName:n,framework:Qc(f(e.values,"framework")),htmlOnly:e.values["html-only"]===!0?!0:void 0,mDomain:f(e.values,"domain"),extra:fn(e.values.set)}))}},mh={name:"module update",usage:"fui module update [-p P -m M] [--name n] [--framework V2|V3] [--html-only true|false] [--domain d] [--new-id id] [--set key=json ...]",summary:"Edit metadata of an existing module (name, id, framework, domain, HTMLOnly)",options:{...gn,name:{type:"string"},framework:{type:"string"},"html-only":{type:"string"},domain:{type:"string"},"new-id":{type:"string"},set:{type:"string",multiple:!0}},async run(e){let{projectId:t,moduleId:n}=await Yc(e);eo(await Bc(_(e),{projectId:t,moduleId:n,moduleName:f(e.values,"name"),framework:Qc(f(e.values,"framework")),htmlOnly:ih(f(e.values,"html-only"),"--html-only"),mDomain:f(e.values,"domain"),newModuleId:f(e.values,"new-id"),extra:fn(e.values.set)}))}},hh={name:"module delete",usage:"fui module delete -p <project> -m <module>",summary:"Delete a module on the server and archive its local folder (ask the user first; ids must be explicit)",options:gn,async run(e){let t=f(e.values,"project"),n=f(e.values,"module");if(!t||!n)throw new h("module delete needs explicit -p <project> -m <module>");eo(await Jc(_(e),t,n))}},fh={name:"module get",usage:"fui module get [<target> | -p P -m M]",summary:"Check out a module into the workspace (archives differing local files) and make it the session target",options:gn,async run(e){let t=f(e.values,"project"),n=f(e.values,"module");if(t&&!n||!t&&n)throw new h("pass both -p and -m, or a target");Go(await Kc(_(e),{target:e.positionals[0],projectId:t,moduleId:n}))}};function Xc(e){return{name:`module get-${e}`,usage:`fui module get-${e} [-p P -m M]`,summary:`Fetch only the module ${e==="ui"?"UI JSON (writes module.json":"HTML body (writes body.html"} if checked out)`,options:gn,async run(t){let{projectId:n,moduleId:o}=await Yc(t);Go(await Gc(_(t),n,o,e))}}}var gh={name:"component get",usage:"fui component get <name> [-p P [-m M]]",summary:"Print a component's .vue source (writes it locally when the scope folder exists); -p without -m = project scope",options:gn,async run(e){let t=e.positionals[0];if(!t)throw new h("missing <name>");let n=_(e),o=await hn(n,f(e.values,"project"),f(e.values,"module"));Go(await zc(n,o,t))}},Zc=[ah,ch,lh,uh,dh,ph,mh,hh,fh,Xc("ui"),Xc("html"),gh];import{resolve as Kf}from"node:path";import{join as pt}from"node:path";import dt from"node:path";import{join as el}from"node:path";async function Wt(e,t){let n={functionRights:null,systemRights:null},o;try{o=JSON.parse(await G(el(e,"_db","rights.json")))}catch{return n}let s=Object.keys(o??{});if(s.length===0)return n;let r=(l)=>s.find((u)=>u.toLowerCase()===l.toLowerCase())??null,i;if(t)i=r(t);else if(s.length===1)i=s[0];else{let l=await H(el(e,"_db","_connections.json"),null),u=l?.default?l.connections?.[l.default]?.apiName??null:null;i=u?r(u):null}if(!i)return n;let a=o[i],c=(l,u)=>Array.isArray(l)?new Set(l.map((d)=>d&&typeof d==="object"?String(d[u]??""):"").filter(Boolean)):null;return{functionRights:c(a?.functionRights,"FunctionCode"),systemRights:c(a?.systemRights,"SysRight")}}function qt(e){return nl("FunctionRight",e)}function tl(e){return nl("SystemRight",e)}function to(e,t){if(e.length===0)return null;if(t===null)return`Chưa có cache _db/rights.json — KHÔNG kiểm được ${e.length} mã FunctionRight (${e.join(", ")}) `+"có thật hay không. Preview cấp sẵn đúng những mã này nên control vẫn hiện ở ĐÂY; user thật chỉ thấy nếu mã đã "+"được định nghĩa. Chạy right_function_list (một lần cho mỗi project) rồi render lại.";let n=e.filter((o)=>!t.has(o));if(n.length===0)return null;return`\uD83D\uDEA8 FunctionRight CHƯA ĐƯỢC ĐỊNH NGHĨA trên hệ thống acc: ${n.join(", ")} — preview cấp sẵn nên control vẫn `+"hiện ở ĐÂY, nhưng chạy thật thì rightTest() luôn trả false → control/menu biến mất mà không có lỗi JS nào. "+'Tạo mã: right_function_new({ functionCode: …, functionName: "…" }); hoặc right_function_list nếu cache cũ.'}function nl(e,t){let n=new Set,o=new RegExp(`${e}['"]?\\s*:\\s*\\[([^\\]]*)\\]`,"g"),s=new RegExp(`${e}\\s*\\.\\s*includes\\(\\s*['"]([^'"]+)['"]`,"g");for(let r of t){if(!r)continue;let i;o.lastIndex=0;while(i=o.exec(r))for(let a of i[1].split(",")){let c=a.trim().replace(/^['"]|['"]$/g,"");if(c)n.add(c)}s.lastIndex=0;while(i=s.exec(r))n.add(i[1])}return Array.from(n).sort()}var wh=new Set(["f-table","f-dialog","f-window","f-button","f-date","f-time","f-time-counter","f-search","f-menu","f-radiobox","f-file-upload","f-image-update","f-excel-reader","f-qrcode","f-qrcode-reader","f-editor","f-editor-dialog","f-echart","f-chart","f-sheet","f-pdfmake","f-label","f-box","f-header","f-title","f-slider","fp-profile","f-dynamic-component","f-table-item-component"]),ol=new Set(["t-html","t-label","t-num","t-time","t-boolean","t-check","t-text","t-select","t-combobox","t-menu","t-link","t-button"]),yh={"f-pdfmake":["pdfmake.min.js","print.min.js","vfs_fonts.js"],"f-file-upload":["plupload.full.min.js"],"f-image-update":["index.umd.js","style.css"],"f-qrcode":["qrcode.min.js"],"f-qrcode-reader":["qrcode-reader.min.js"],"f-echart":["echarts.min.js","fechart.js"],"f-sheet":["ag-grid-community.min.js","fsheet.js"],"f-editor":["ckeditor.js"],"f-editor-dialog":["ckeditor.js"]};var wn=new Set(["API","CALL","IF","FUN","EXE","MESS","MESSBOX","ARRAY","CONFIRM","IN","OUT","THEN","ELSE","CALLBACK","ERROROUT","ERRORCALLBACK","CANCEL","METHOD","HEADER","COL"]),rl=new Set(["item","index","v_old","user","vueData","p_router","p_routers","v_Set","moment"]);function j(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}function Os(e){if(Array.isArray(e))return e.length>0&&e.every((t)=>j(t)||typeof t==="string")&&e.some((t)=>j(t)&&Object.keys(t).some((n)=>wn.has(n)));if(!j(e))return!1;return Object.keys(e).some((t)=>wn.has(t))}function Ye(e){let t=e.trim();if(t.startsWith("vueData."))t=t.slice(8);let n=t.match(/^[A-Za-z_$][\w$]*/);return n?n[0]:t}function no(e){return!!e&&!e.includes(" ")&&!e.includes("{{")&&!e.includes("(")&&!e.startsWith("`")&&!e.startsWith("#")&&!e.startsWith("!")&&/^[A-Za-z_$]/.test(e.replace(/^vueData\./,""))}function bh(e){let t=e.trim();if(!t)return!0;if(/^-?\d+(\.\d+)?$/.test(t)||t==="true"||t==="false"||t==="null")return!0;if(/^'[^']*'$/.test(t)||/^"[^"]*"$/.test(t))return!0;return/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(t)}function kh(e){let t=e.split(""),n=null;for(let o=0;o<e.length;o++){let s=e[o];if(n){if(s==="\\"){if(t[o]=" ",o+1<e.length)t[o+1]=" ",o++;continue}if(s===n){n=null;continue}t[o]=" "}else if(s==="'"||s==='"'||s==="`")n=s}return t.join("")}var vh=[{re:/\bfunction\s*\(/,what:'khai báo "function(...)"'},{re:/\bfunction\s+[A-Za-z_$]/,what:'khai báo "function ten(...)"'},{re:/\breturn\b/,what:'lệnh "return"'},{re:/\bvar\s+[A-Za-z_$]/,what:'khai báo "var"'},{re:/\blet\s+[A-Za-z_$]/,what:'khai báo "let"'},{re:/\bconst\s+[A-Za-z_$]/,what:'khai báo "const"'}];function $h(e,t,n,o){if(typeof t!=="string")return;if(!(e.startsWith(":")&&!e.startsWith("::")||e.startsWith("v-bind:")))return;let r=kh(t),i=[];for(let{re:a,what:c}of vh)if(a.test(r))i.push(c);if(r.replace(/;\s*$/,"").includes(";"))i.push('dấu ";" nối nhiều câu lệnh');if(i.length===0)return;o.push(`attr "${e}" (tại ${n}) chứa ${i.join(" + ")} — giá trị binding đi thẳng vào template compiler của Vue, `+"chỉ nhận MỘT BIỂU THỨC thuần (object/array literal, ternary, chuỗi .map()/.filter() arrow, truy cập thuộc tính). "+"Statement ở đây làm compile template hỏng → TRANG TRẮNG mà không có JS error hay [Vue warn] nào. "+'Cách sửa: đưa logic vào một hàm trong script.js (function buildX(){ … vueData.x = …; }), gọi qua "EXE": "buildX()" '+`trong CALLBACK của action nạp dữ liệu, rồi "${e}" chỉ trỏ tới biến vueData đó (vd "${e}": "x"). `+"Chi tiết: references/advanced-techniques.md.")}function Me(e,t,n="$",o=!1){if(Array.isArray(e))e.forEach((s,r)=>Me(s,t,`${n}[${r}]`,o));else if(j(e)){t(e,n,o);for(let[s,r]of Object.entries(e))Me(r,t,`${n}.${s}`,o||s.startsWith("#"))}}var Sh="(chuẩn mặc định fui/DESIGN.md — chỉ giữ nguyên nếu user yêu cầu rõ kiểu khác)",jh=new Set([400,550,600,650,800]),Ih=/(xóa|xoá|\bdelete\b|\bremove\b)/i,Ch=/(delete|remove|xoa)/i,Rh=new Set(["v-text-field","v-textarea","v-select","v-combobox","v-autocomplete","f-date","f-time","f-search"]),sl=new Set(["9"]);function Th(e,t,n){let o=JSON.stringify(e),s=qt([o]),r=tl([o]).filter((l)=>!sl.has(l)),i=s.length+r.length;if(i===0)return 0;let a=t.definedFunctionRights??null,c=t.definedSystemRights??null;if(a===null&&c===null)return n.push(`Module gác quyền bằng ${i} mã literal (${[...s,...r].join(", ")}) nhưng chưa có cache _db/rights.json `+"để đối chiếu — KHÔNG kiểm được mã nào có thật. Mã không tồn tại làm rightTest() luôn trả false: "+"control/menu biến mất im lặng, không lỗi JS. Chạy right_function_list + right_system_list (một lần cho mỗi project) rồi validate lại."),i;if(a&&s.length>0){let l=a,u=s.filter((d)=>!l.has(d));for(let d of u)n.push(`FunctionRight "${d}" chưa được định nghĩa cho project (_db/rights.json có ${l.size} mã`+`${l.size?`: ${Array.from(l).slice(0,15).join(", ")}${l.size>15?", …":""}`:""}). `+"rightTest() sẽ luôn trả false → control/menu gác bằng mã này biến mất mà không báo lỗi. "+`Tạo mã: right_function_new({ functionCode: ${/^\d+$/.test(d)?d:`"${d}"`}, functionName: "…" }); `+"hoặc chạy right_function_list nếu mã đã có trên server mà cache cũ.")}if(c&&r.length>0){let l=c,u=r.filter((d)=>!l.has(d));for(let d of u)n.push(`SystemRight ${d} chưa được định nghĩa cho project (_db/rights.json có: `+`${l.size?Array.from(l).sort().join(", "):"(chưa mức nào)"}${sl.size?" — mức 9 luôn hợp lệ":""}). `+"Không ai được cấp mức này thì control gác bằng nó biến mất im lặng. "+`Định nghĩa: right_system_new({ sysRight: ${d}, note: "…" }); hoặc chạy right_system_list nếu cache cũ.`)}return i}function Ph(e,t){let n=(a)=>t.push(`[design] ${a} ${Sh}`),o=(a)=>typeof a==="string"?a:"";Me(e,(a,c)=>{let l=a.API;if(typeof l==="string"&&Ch.test(l)&&a.CONFIRM===void 0)n(`Action gọi API "${l}" (tại ${c}) có vẻ là thao tác XÓA nhưng thiếu CONFIRM — mọi action xóa phải hỏi xác nhận.`)});let s=new Set;Me(e,(a,c)=>{if(s.has(a))return;let l=j(a.attr)?a.attr:a;if(l!==a)s.add(l);let u=o(l.label)||o(l.text)||(typeof a.innerHTML==="string"?a.innerHTML:""),d=o(l["icon-text"]);if(!Ih.test(u)&&d!=="mdi-delete")return;let p=o(l.color)||o(l[":color"]);if(/\b(error|red)\b/i.test(p)&&!p.includes("?"))n(`Nút/mục xóa (tại ${c}) đang màu "${p}" — nút xóa dùng màu "warning" (cam), không dùng error/đỏ.`)});let r=(a,c,l,u)=>{if(Array.isArray(a)){a.forEach((I,g)=>r(I,`${c}[${g}]`,l,u));return}if(!j(a))return;let d=o(a.el),p=o(a.prop),m=j(a.attr)?a.attr:{},w=l||d==="v-dialog"||d==="f-dialog"||d==="hidden-container"||p.includes("hidden-container"),y=u||d==="v-card-actions";if(d==="v-toolbar"&&["color",":color","dark",":dark"].some((I)=>(I in m)))n(`v-toolbar (tại ${c}) có color/dark — toolbar tiêu đề phải "dense flat" cùng màu nền với thân dialog, không tô màu/dark riêng.`);if(d==="v-dialog"||d==="f-dialog"){let I=m.width??m[":width"],g=typeof I==="number"?I:typeof I==="string"&&/^\d+$/.test(I.trim())?parseInt(I,10):NaN;if(!Number.isNaN(g)&&!jh.has(g))n(`Dialog (tại ${c}) width=${g} — bộ width chuẩn: 400 / 550 / 600 / 650 / 800.`)}if(w&&d==="v-card"&&["elevation",":elevation","outlined",":outlined"].some((I)=>(I in m)))n(`v-card trong dialog (tại ${c}) có elevation/outlined — bỏ đi: v-dialog đã tự có bóng đổ, thêm nữa là bóng chồng bóng. v-card chỉ cần class "pa-0" (triệt đệm để toolbar tiêu đề sát mép).`);let v=o(m.class),A=v.includes("flex-md-grow-0")&&v.includes("ml-2");if(A)n(`Control (tại ${c}) mang class "flex-md-grow-0 ml-2" — dấu hiệu pattern label-div rời đã loại bỏ; form dùng floating label ("label" trong attr của field).`);if(w&&!A&&Rh.has(d)&&!("label"in m)&&!(":label"in m)&&!("placeholder"in m))n(`${d} trong dialog (tại ${c}) thiếu "label" — form dialog dùng floating label.`);if(d==="v-snackbar"||d==="v-alert")n(`${d} (tại ${c}) — phản hồi/thông báo dùng hệ thống có sẵn của FUI: MESS (toast) / MESSBOX (hộp thoại) trong action, CONFIRM để xác nhận — không tự dựng ${d} trong controls.`);if(d==="f-table"||d==="f-sheet"){let I=m[":height"]??m.height;if(I===void 0)n(`${d} (tại ${c}) không có ":height" — bảng sẽ tự giãn theo số dòng và làm cả TRANG cuộn. Bảng data chính nên đặt \`":height": -16\` (fill tới đáy màn hình, chừa 16px — component tự đo vị trí thật, không cần tính tay); trong dialog dùng số dương (px cố định, vd 360). Bảng ngắn/lookup chủ ý tự giãn thì bỏ qua.`);else if(typeof I==="string"&&/\$\s*\(\s*window\s*\)\s*\.\s*height\s*\(\s*\)/.test(I))n(`${d} (tại ${c}) tính ":height" thủ công theo viewport (\`${I}\`) — không cần nữa: ${d} tự đo vị trí thật của bảng sau layout và tự cập nhật khi resize. Thay bằng \`":height": -16\` (số âm = khoảng chừa ở đáy).`)}if(y&&(d==="f-button"||d==="v-btn")){let I=o(m.label)||(typeof a.innerHTML==="string"?a.innerHTML:"");if(/^\s*(đóng|dong|close)\s*$/i.test(I))n(`Nút "${I.trim()}" trong v-card-actions (tại ${c}) — dialog chuẩn chỉ dùng icon close trên toolbar, không thêm nút Đóng ở actions.`)}for(let[I,g]of Object.entries(a))r(g,`${c}.${I}`,w,y)};(Array.isArray(e.controls)?e.controls:[]).forEach((a,c)=>r(a,`$.controls[${c}]`,!1,!1))}var Eh=12,Ds=25,al=/\b(shrink|flex-grow-0)\b/;function cl(e){return typeof e==="string"?e:j(e)?[e.class,e[":class"]].filter((t)=>typeof t==="string").join(" "):""}function Ah(e){return e.w===void 0||e.w===null||e.w===""||e.w===0}function Nh(e,t){Me(e.controls,(n,o)=>{if(!Array.isArray(n.cols))return;Dh(n.cols,o,t),n.cols.forEach((s,r)=>{if(!j(s))return;let i=s,a=i.w,c=typeof a==="number"?a:typeof a==="string"&&/^\d+$/.test(a.trim())?parseInt(a.trim(),10):NaN;if(Number.isNaN(c)||c<=Eh)return;let l=`${o}.cols[${r}]${typeof i.el==="string"?` (el: ${i.el})`:""}`;if(c<Ds){t.push(`${l} có "w": ${c} — vùng chết 13–24: V2 hiểu là ${c}px, còn V3 sinh class "v-col-${c}" mà Vuetify 3 không định nghĩa (chỉ có .v-col-1 … .v-col-12) nên cột KHÔNG có bề rộng nào, rơi về chia đều. Dùng 1–12 (chia cột) hoặc >= ${Ds} (px, kèm \`"col": { "class": "shrink" }\`).`);return}let u=i.col;if(al.test(cl(u)))return;t.push(j(u)&&"shrink"in u?`${l} có "w": ${c} (px) kèm \`"col": { "shrink": ... }\` — dạng thuộc tính trần là prop của v-flex (Vuetify 2); Vuetify 3 không có prop đó nên nó trơ và cột vẫn giãn hết chỗ trống. Viết \`"col": { "class": "shrink" }\` để đúng ở cả hai framework.`:`${l} có "w": ${c} — số >= ${Ds} nghĩa là bề rộng cố định px, nhưng cột grid mặc định flex-grow:1 nên flex-basis ${c}px KHÔNG có tác dụng: cột vẫn giãn hết chỗ trống (đo thật: w=300 ra 687px ở V2, 738px ở V3). Thêm \`"col": { "class": "shrink" }\`.`)})},"$.controls")}function Dh(e,t,n){let o=(s)=>j(s)&&s.el==="v-spacer";if(!e.some(o))return;e.forEach((s,r)=>{if(!j(s))return;let i=s;if(typeof i.el!=="string"||!i.el)return;let a=`${t}.cols[${r}] (el: ${i.el})`,c=i.col,l=al.test(cl(c));if(i.el==="v-spacer"){if(l)n.push(`${a} — \`v-spacer\` mà có \`shrink\` thì co lại còn 0px (đo thật: 24px ở V3, 0px ở V2), không đẩy được nhóm sau sang phải nữa và cả hàng dồn về trái. Bỏ \`shrink\` khỏi riêng cột này.`);return}if(!Ah(i)||l)return;n.push(j(c)&&"shrink"in c?`${a} nằm cùng hàng với \`v-spacer\`, \`w\` rỗng và chỉ có \`"col": { "shrink": ... }\` — dạng thuộc tính trần là prop của v-flex (Vuetify 2); Vuetify 3 không có prop đó nên cột vẫn giãn và chiếm một phần đều. Viết \`"col": "shrink"\` (hoặc \`{ "class": "shrink" }\`).`:`${a} nằm cùng hàng với \`v-spacer\` nhưng thiếu \`shrink\` — \`w\` rỗng KHÔNG có nghĩa "tự co theo nội dung": runtime bọc phần bề rộng trong \`if (c.w)\` nên cột này không có class grid lẫn flex-basis, còn trơ \`flex-grow: 1\` và được CHIA PHẦN TRỐNG y như \`v-spacer\`. Hệ quả: control bị kéo dãn, hở khoảng giữa các nút, nhóm cuối hàng không sát mép phải. Chuẩn cho hàng nhiều control: MỌI cột \`"col": "shrink"\`, trừ đúng \`v-spacer\`.`)})}var il=["API","CALL","EXE","FUN","IF","MESS","MESSBOX"],xh=new Set(["IN","OUT","HEADER","METHOD","CALLBACK","ERROROUT","ERRORCALLBACK","THEN","ELSE","CONFIRM","CANCEL","ARRAY","COL","MAP"]);function Oh(e,t,n){for(let[o,s]of Object.entries(e)){if(o==="set")continue;Me(s,(r,i)=>{let a=Object.keys(r),c=il.filter((m)=>a.includes(m));if(c.length===0)return;let l=c[0],u=new Set([l]);if(l==="API"&&c.includes("MESS"))u.add("MESS");let d=c.filter((m)=>!u.has(m));if(d.length>0){t.push(`${i} gộp ${c.length} lệnh trong MỘT object \`{}\` (${c.join(", ")}) — chỉ \`${l}\` chạy, còn ${d.map((m)=>`\`${m}\``).join(", ")} bị bỏ IM LẶNG (không JS error, không [Vue warn]). \`vueAction\` dò key lệnh theo thứ tự cố định API → CALL → EXE → FUN → IF → MESS → MESSBOX và \`return\` ngay ở nhánh đầu tiên khớp, nên thứ tự viết không có tác dụng. Nhiều lệnh có thứ tự trước sau thì phải là MẢNG, mỗi lệnh một object: \`[{ "${c[0]}": … }, { "${c[1]}": … }]\`.`);return}let p=a.filter((m)=>!u.has(m)&&!xh.has(m)&&!il.includes(m));if(p.length>0)n.push(`${i} có lệnh \`${l}\` nằm chung object với key gán dữ liệu ${p.map((m)=>`\`${m}\``).join(", ")} — fragment gán chạy ở nhánh CUỐI của \`vueAction\`, nhánh đó chỉ tới lượt khi KHÔNG key lệnh nào khớp, nên các key này không bao giờ được gán. Tách thành mảng: \`[{ ${p.map((m)=>`"${m}": …`).join(", ")} }, { "${l}": … }]\`.`)},`$.${o}`)}}function _h(e,t){let n=[],o=[];if(t.schemaWarning)o.push(t.schemaWarning);if(!j(e))return{errors:["module.json gốc phải là một object với các khối data/watch/controls/set."],warnings:o,stats:xs()};let s=e,r=new Set(["data","watch","controls","set"]);for(let g of Object.keys(s))if(!r.has(g))o.push(`Khối cấp 1 không chuẩn: "${g}" — module.json chỉ có data/watch/controls/set (xem module-json-anatomy.md).`);if(s.data!==void 0&&!Array.isArray(s.data))n.push(`"data" phải là MẢNG (array), hiện là ${typeof s.data}.`);if(s.watch!==void 0&&!j(s.watch))n.push(`"watch" phải là OBJECT, hiện là ${Array.isArray(s.watch)?"array":typeof s.watch}.`);if(s.controls!==void 0&&!Array.isArray(s.controls))n.push(`"controls" phải là MẢNG container, hiện là ${typeof s.controls}.`);if(s.set!==void 0&&!j(s.set))n.push(`"set" phải là OBJECT, hiện là ${Array.isArray(s.set)?"array":typeof s.set}.`);let i=new Set(rl),a=new Set,c=Array.isArray(s.data)?s.data:[];for(let g of c){if(!j(g)){o.push(`data[] chứa phần tử không phải object: ${JSON.stringify(g)?.slice(0,60)}`);continue}for(let[k,R]of Object.entries(g)){if(wn.has(k))continue;if(k.startsWith("#"))continue;if(Os(R))a.add(k);else i.add(Ye(k))}}let l=0;Me(s,(g,k,R)=>{if(R)return;for(let C of["OUT","ERROROUT"]){let S=g[C];if(typeof S==="string"){if(no(S))i.add(Ye(S));else if(S.trim())l++}else if(j(S))for(let D of Object.keys(S))i.add(Ye(D))}});let u=new Map,d=new Map;Me(s,(g,k,R)=>{let C=g.CALL;if(typeof C==="string"){if(no(C))(R?d:u).set(Ye(C),k);else if(C.trim()&&!R)l++}let S=g.API;if(typeof S==="string"&&S.trim()&&!S.startsWith("/")&&!no(S))l++;for(let[D,x]of Object.entries(g)){if(typeof x!=="string")continue;let N=R||D.startsWith("#")?d:u;for(let P of x.matchAll(/\bCALL\(\s*([A-Za-z_$][\w$.]*)\s*\)/g))N.set(Ye(P[1]),k)}});for(let[g,k]of u)if(!a.has(g))n.push(`CALL "${g}" (tại ${k}) không khớp named action nào trong data[]. Named actions hiện có: ${[...a].join(", ")||"(không có)"}.`);let p=0,m=0,w=Array.isArray(s.controls)?s.controls:[];for(let g=0;g<w.length;g++){let k=w[g];if(p++,!j(k)||!Array.isArray(k.rows)){n.push(`controls[${g}] phải là container có "rows" (grid bắt buộc: container > rows > cols).`);continue}for(let R=0;R<k.rows.length;R++){let C=k.rows[R];if(!j(C)||!Array.isArray(C.cols)){n.push(`controls[${g}].rows[${R}] phải có "cols" (mảng Control Object).`);continue}for(let S=0;S<C.cols.length;S++){let D=C.cols[S];if(m++,!j(D))continue;if(D.w===void 0&&D.el!==void 0)o.push(`controls[${g}].rows[${R}].cols[${S}] (el: ${String(D.el)}) thiếu "w" — bắt buộc cho item trong cols.`)}}}let y=new Set;if(Me(s,(g,k)=>{if("children"in g&&(("el"in g)||("rows"in g)||("cols"in g)))n.push(`"children" không được hỗ trợ (tại ${k}) — dùng "innerHTML" (mảng Control Object) để lồng.`);let R=g.el;if(typeof R==="string"&&R){if(y.add(R),(R.startsWith("f-")||R.startsWith("fp-"))&&!wh.has(R)&&!t.localComponents.has(R))o.push(`Component "${R}" (tại ${k}) không có trong registry FUI (component-quickref.md) và không thấy trong components local — kiểm tra chính tả hoặc chạy component_list.`);else if(R.startsWith("t-")&&!ol.has(R))o.push(`Cell component "${R}" (tại ${k}) không có trong danh sách t-* hợp lệ: ${[...ol].join(", ")}.`);else if(R.startsWith("uc-")&&!t.localComponents.has(R))o.push(`Custom component "${R}" (tại ${k}) chưa thấy file .vue/local metadata — tạo component trước hoặc chạy component_list.`)}let C=g.attr;if(j(C))for(let[S,D]of Object.entries(C)){if(S.startsWith("@"))n.push(`attr "${S}" (tại ${k}) — module.json KHÔNG hỗ trợ shorthand "@"; dùng "v-on:${S.slice(1)}".`);else if(S.startsWith("#"))n.push(`attr "${S}" (tại ${k}) — module.json KHÔNG hỗ trợ slot shorthand "#"; dùng "v-slot:${S.slice(1)}".`);else if((S==="style"||S===":style")&&typeof D==="string"){if(D.split(";").map((N)=>N.trim()).filter(Boolean).filter((N)=>!/^(max-|min-)?width\s*:/i.test(N)).filter((N)=>!(g.el==="v-card"&&/^padding\s*:\s*0(px)?$/i.test(N))).length>0)o.push(`attr style "${D}" (tại ${k}) — module thực tế gần như không dùng style ngoài width (controls-styling-vocabulary.md). Cân nhắc bỏ.`)}else if((S.startsWith(":")||S.startsWith("v-bind:"))&&typeof D==="string"&&!bh(D))l++;else if(S==="v-model"&&typeof D==="string"){if(no(D)){let x=Ye(D);if(!i.has(x)&&!a.has(x))o.push(`v-model "${D}" (tại ${k}) — biến gốc "${x}" chưa khai báo trong data[]. Khai báo state trước để reactive hoạt động đúng.`)}else if(D.trim())l++}$h(S,D,k,n)}}),j(s.watch)){let g=(k,R)=>{for(let C of Object.keys(k)){if(C==="deep-watch"){if(j(k[C]))g(k[C],`${R}.deep-watch`);continue}let S=Ye(C);if(!i.has(S))o.push(`watch "${C}" (${R}) — biến "${S}" chưa khai báo trong data[], watcher sẽ không bao giờ chạy.`)}};g(s.watch,"watch")}for(let g of y){let k=yh[g];if(!k)continue;let R=k.filter((C)=>!t.importsText.includes(C.toLowerCase()));if(R.length>0)n.push(`"${g}" cần import: ${R.join(", ")} — chưa thấy trong _imports.json (module lẫn project). Dùng file_import_list/file_import_new (xem component-quickref.md).`)}let v=0,A=new Set;if(t.schemaProcs){let g=new Set;for(let k of t.schemaProcs.values())for(let R of k)g.add(R);if(Me(s,(k,R)=>{let C=k.API;if(typeof C!=="string"||!C||C.includes("{{"))return;v++;let S=null,D=null;if(C.startsWith("/")){let N=C.replace(/^\/+/,"").split("/").filter(Boolean);if(N.length<2)return;if(D=t.schemaProcs.get(N[0].toLowerCase())??null,!D){A.add(N[0]);return}S=N[1]==="auth"?N[2]??null:N[1]}else if(no(C))S=C,D=g;if(!S||S.includes("`")||!D)return;if(![`spAPI_${S}`,`spAPI_AUTH_${S}`,`spAPIFILE_${S}`,`spAPIFILE_UPLOAD_${S}`,S].some((N)=>D.has(N)))o.push(`API "${C}" (tại ${R}) — không tìm thấy spAPI_${S} trong schema local. Deploy SP trước hoặc chạy db_schema_get (bỏ qua nếu API thuộc backend khác).`)}),A.size>0){let k=[...A].sort();o.push(`${k.length} alias được gọi nhưng chưa có kết nối local — KHÔNG đối chiếu được tên SP: ${k.join(", ")}. `+`Kết nối từng cái bằng db_connect({ apiName: "${k[0]}", apiDomain: "…", dbToken: "…" }) để bước kiểm này chạy (bỏ qua nếu alias thuộc backend của nhóm khác).`)}}let I=Th(s,t,o);return Ph(s,o),Nh(s,o),Oh(s,n,o),{errors:n,warnings:o,stats:{stateKeys:i.size-rl.size,namedActions:a.size,containers:p,controls:m,callRefs:u.size,apiRefs:v,crossWindowCalls:d.size,dynamicRefs:l,rightRefs:I}}}function xs(){return{stateKeys:0,namedActions:0,containers:0,controls:0,callRefs:0,apiRefs:0,crossWindowCalls:0,dynamicRefs:0,rightRefs:0}}function Mh(e){return e.match(/^CREATE (?:PROCEDURE|FUNCTION)\s+(\S+)\s*\(/i)?.[1]??null}async function Lh(e){let t=dt.dirname(dt.dirname(e)),n=new Set;for(let c of[dt.join(e,"components"),dt.join(t,"components")]){for(let u of await _e(c))n.add(u.replace(/\.vue$/i,""));let l=await H(dt.join(c,"_components.json"),[]);for(let u of l)if(u?.comName)n.add(u.comName)}let o=[];for(let c of[e,t])o.push(await F(dt.join(c,"imports","_imports.json")));let s=await H(dt.join(t,"_db","_connections.json"),null),r=new Map,i=[];for(let[c,l]of Object.entries(s?.connections??{})){let u=l.apiName;if(!u)continue;let d=c,p=await H(dt.join(t,"_db",d,"schema.json"),null),m=Array.isArray(p?.procedures)?p.procedures:null;if(m===null)continue;if(!m.every((v)=>typeof v==="string")){i.push(d);continue}let w=u.toLowerCase(),y=r.get(w)??new Set;for(let v of m.map(Mh).filter((A)=>A!==null))y.add(v);r.set(w,y)}let a=await Wt(t);return{localComponents:n,importsText:o.join(`
104
+ `).toLowerCase(),schemaProcs:r.size>0?r:null,schemaWarning:i.length>0?`_db/${i.join("/schema.json, _db/")}/schema.json sai định dạng (bản cache cũ) — bỏ qua bước đối chiếu tên API với stored procedure ở ${i.length>1?"các kết nối này":"kết nối này"}. Chạy db_schema_get để lấy bản mới.`:null,definedFunctionRights:a.functionRights,definedSystemRights:a.systemRights}}async function zo(e){let t=await F(dt.join(e,"module.json"));if(!t.trim())return{errors:[`module.json rỗng hoặc không tồn tại trong ${e}.`],warnings:[],stats:xs()};let n;try{n=JSON.parse(t)}catch(s){return{errors:[`module.json có lỗi JSON syntax: ${s.message}`],warnings:[],stats:xs()}}let o=await Lh(e);return _h(n,o)}function Xo(e){let t=[];if(e.errors.length===0&&e.warnings.length===0)t.push("✅ module.json hợp lệ — không phát hiện lỗi ngữ nghĩa.");else{if(e.errors.length>0)t.push(`❌ ${e.errors.length} lỗi (sẽ hỏng khi chạy):`),t.push(...e.errors.map((o)=>` - ${o}`));if(e.warnings.length>0){if(t.length)t.push("");t.push(`⚠️ ${e.warnings.length} cảnh báo (nên xem lại):`),t.push(...e.warnings.map((o)=>` - ${o}`))}}let n=e.stats;if(t.push(""),n.dynamicRefs>0)t.push(`ℹ️ ${n.dynamicRefs} biểu thức động (\`:attr\` phức tạp, CALL/API/v-model/OUT dạng \`{{…}}\`/gọi hàm/template string) KHÔNG kiểm tĩnh được — biến bên trong chúng không được đối chiếu với data[]. "Hợp lệ" ở đây KHÔNG có nghĩa là chạy đúng: dùng module_simulate để phủ phần này.`);return t.push(`Thống kê: ${n.stateKeys} state · ${n.namedActions} named action · ${n.containers} container / ${n.controls} control · ${n.callRefs} CALL ref${n.apiRefs?` · ${n.apiRefs} API ref đối chiếu schema`:""}${n.crossWindowCalls?` · ${n.crossWindowCalls} CALL xuyên cửa sổ (thuộc module khác, không kiểm được từ đây)`:""}${n.rightRefs?` · ${n.rightRefs} mã quyền đối chiếu _db/rights.json`:""}`),t.join(`
105
+ `)}function ee(e,t){let n=e.replace(/\s+/g," ").trim();return n.length>t?n.slice(0,t)+"…":n}function oo(e){if(e===null)return"null";if(Array.isArray(e))return e.length===0?"[]":`[${e.length} items]`;if(j(e)){let t=Object.keys(e);return t.length===0?"{}":`{${ee(t.join(","),40)}}`}if(typeof e==="string")return`"${ee(e,30)}"`;return String(e)}function Ne(e,t=0){if(t>3)return"…";if(typeof e==="string")return ee(e,70);if(Array.isArray(e))return e.map((o)=>Ne(o,t+1)).join(" ; ");if(!j(e))return ee(JSON.stringify(e)??"",60);let n=[];if(e.CONFIRM!==void 0)n.push(`CONFIRM "${ee(String(e.CONFIRM),40)}"`);if(e.IF!==void 0)n.push(`IF(${ee(String(e.IF),50)})`);if(e.API!==void 0)n.push(`API ${ee(String(e.API),60)}`);if(e.METHOD!==void 0)n.push(`[${String(e.METHOD)}]`);if(e.IN!==void 0)n.push(`IN{${j(e.IN)?ee(Object.keys(e.IN).join(","),50):ee(String(e.IN),40)}}`);if(e.OUT!==void 0)n.push(`OUT→${j(e.OUT)?Object.keys(e.OUT).join(","):String(e.OUT)}`);if(e.ARRAY!==void 0)n.push(`ARRAY ${ee(String(e.ARRAY),30)}${e.COL!==void 0?` COL ${ee(String(e.COL),30)}`:""}`);if(e.CALL!==void 0)n.push(`CALL ${ee(String(e.CALL),40)}`);if(e.FUN!==void 0)n.push(`FUN ${ee(String(e.FUN),40)}`);if(e.EXE!==void 0)n.push(`EXE ${ee(String(e.EXE),70)}`);if(e.MESS!==void 0)n.push(`MESS "${ee(String(e.MESS),40)}"`);if(e.MESSBOX!==void 0)n.push(`MESSBOX "${ee(String(e.MESSBOX),40)}"`);if(e.THEN!==void 0)n.push(`THEN[${Ne(e.THEN,t+1)}]`);if(e.ELSE!==void 0)n.push(`ELSE[${Ne(e.ELSE,t+1)}]`);if(e.CALLBACK!==void 0)n.push(`CALLBACK[${Ne(e.CALLBACK,t+1)}]`);if(e.ERROROUT!==void 0)n.push(`ERROROUT→${String(e.ERROROUT)}`);if(e.ERRORCALLBACK!==void 0)n.push(`ERRORCALLBACK[${Ne(e.ERRORCALLBACK,t+1)}]`);if(e.CANCEL!==void 0)n.push(`CANCEL[${Ne(e.CANCEL,t+1)}]`);return n.join(" ")||ee(JSON.stringify(e)??"",60)}var Fh=["v-model","v-if","v-show","v-for","label",":label","placeholder",":items",":data",":headers",":config",":option",":options","value",":value","readonly","disabled",":disabled","multiple","type"];function Uh(e){let t=[],n=e.w!==void 0?`w=${String(e.w)}`:"w=?",o=typeof e.el==="string"&&e.el?e.el:e.rows?"(container)":"(text)";t.push(`${n} ${o}`);let s=j(e.attr)?e.attr:{};for(let r of Fh){let i=s[r];if(i===void 0)continue;t.push(`${r}=${typeof i==="string"?ee(i,45):oo(i)}`)}for(let[r,i]of Object.entries(s)){if(!r.startsWith("v-on:"))continue;let a=r.slice(5);t.push(`${a}→${typeof i==="string"?ee(i,60):Ne(i)}`)}for(let r of["innerText","html","text"])if(typeof e[r]==="string"&&e[r])t.push(`"${ee(String(e[r]),35)}"`);return t.join(" ")}function ll(e,t,n,o){let s=o?"└─":"├─",r=t+(o?" ":"│ ");if(!j(e)){n.push(`${t}${s} ${ee(JSON.stringify(e)??String(e),60)}`);return}n.push(`${t}${s} ${Uh(e)}`);let i=e.innerHTML;if(Array.isArray(i))i.forEach((a,c)=>ll(a,r,n,c===i.length-1));if(Array.isArray(e.rows))ul(e.rows,r,n)}function ul(e,t,n){e.forEach((o,s)=>{if(!j(o)||!Array.isArray(o.cols)){n.push(`${t}row ${s+1}: ❌ thiếu "cols"`);return}let r=o.cols,a=r.map((u)=>j(u)?u.w:void 0).filter((u)=>typeof u==="number"),c="";if(r.length>1&&a.length===r.length){let u=a.reduce((d,p)=>d+p,0);c=` (Σw=${u}${u>12?" ⚠ vượt 12":""})`}let l=[];for(let u of["v-if","v-show","prop"])if(typeof o[u]==="string")l.push(`${u}=${ee(String(o[u]),40)}`);n.push(`${t}row ${s+1}${l.length?` [${l.join(" ")}]`:""}${c}`),r.forEach((u,d)=>ll(u,t+" ",n,d===r.length-1))})}function dl(e){if(!j(e))return"❌ module.json gốc không phải object.";let t=e,n=[],o=new Map,s=new Map,r=[],i=Array.isArray(t.data)?t.data:[];for(let d of i){if(!j(d))continue;let p=Object.keys(d);if(p.length>0&&p.every((m)=>wn.has(m))){r.push(d);continue}for(let[m,w]of Object.entries(d)){if(wn.has(m)){r.push({[m]:w});continue}if(m.startsWith("#"))continue;if(Os(w))s.set(m,w);else o.set(Ye(m),w)}}let a=JSON.stringify({c:t.controls,w:t.watch,s:t.set,a:[...s.values()],u:r})??"",c=(d)=>new RegExp(`\\b${d.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`).test(a);n.push("═══ LAYOUT (controls) ═══");let l=Array.isArray(t.controls)?t.controls:[];if(l.length===0)n.push("(không có controls)");let u=[];if(l.forEach((d,p)=>{if(!j(d)){n.push(`[container ${p+1}] ❌ không phải object`);return}let m=typeof d.el==="string"?d.el:"",w=m==="hidden-container",y=j(d.attr)?d.attr:{},v=[];if(m)v.push(`el=${m}`);if(typeof d.prop==="string")v.push(`prop="${ee(d.prop,40)}"`);for(let A of["v-if","v-show","v-model"]){let I=d[A]??y[A];if(typeof I==="string")v.push(`${A}=${ee(I,40)}`)}if(n.push(`[container ${p+1}${w?" — DIALOG":""}] ${v.join(" ")||"(mặc định)"}`),w){let A=d["v-model"]??y["v-model"];if(typeof A==="string")u.push({vModel:Ye(A),where:`container ${p+1}`});else u.push({vModel:"",where:`container ${p+1}`})}if(Array.isArray(d.rows))ul(d.rows," ",n);else n.push(' ❌ thiếu "rows"')}),n.push("","═══ STATE (data) ═══"),o.size===0)n.push("(không có state)");for(let[d,p]of o){let m=c(d);n.push(`${d} = ${oo(p)}${m?"":" ⚠ không thấy dùng ở controls/watch/actions"}`)}if(n.push("","═══ ACTIONS ═══"),s.size===0&&r.length===0)n.push("(không có action)");for(let[d,p]of s){let m=JSON.stringify({c:t.controls,w:t.watch,s:t.set,u:r,a:[...s.entries()].filter(([y])=>y!==d).map(([,y])=>y)})??"",w=new RegExp(`\\b${d.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`).test(m);n.push(`${d}: ${Ne(p)}${w?"":" ⚠ không thấy nơi gọi (CALL/watch/v-on)"}`)}if(r.forEach((d,p)=>{n.push(`(auto-startup ${p+1}): ${Ne(d)}`)}),j(t.watch)&&Object.keys(t.watch).length>0){n.push("","═══ WATCH ═══");let d=(p,m)=>{for(let[w,y]of Object.entries(p)){if(w==="deep-watch"&&j(y)){d(y,"deep ");continue}n.push(`${m}${w} → ${Ne(y)}`)}};d(t.watch,"")}if(u.length>0){n.push("","═══ DIALOGS ═══");let d=JSON.stringify(t)??"";for(let p of u){if(!p.vModel){n.push(`${p.where}: ⚠ hidden-container không có v-model — không điều khiển mở/đóng được`);continue}let m=p.vModel.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),w=new RegExp(`\\b${m}\\b\\s*=\\s*true`).test(d);n.push(`${p.where}: v-model=${p.vModel}${w?` — có lệnh mở (${p.vModel} = true)`:` ⚠ không thấy lệnh mở "${p.vModel} = true" ở đâu`}`)}}if(j(t.set)&&Object.keys(t.set).length>0){n.push("","═══ SET (override) ═══");for(let[d,p]of Object.entries(t.set))n.push(`${d} = ${typeof p==="string"?ee(p,60):oo(p)}`)}return n.push("",`Thống kê: ${o.size} state · ${s.size} named action · ${r.length} auto-startup · ${l.length} container · ${u.length} dialog`),n.join(`
106
+ `)}var Hh=[[/cũ (\d+) ngày/g,"$1 days old"],[/cũ (\d+) giờ/g,"$1 h old"],[/\(control trong f-table — dùng row (\d+) của /g,"(control in f-table; using row $1 of "],[/(✘ STEP \d+) lỗi: /g,"$1 error: "]],Vh={project_sync:"fui project sync",module_get:"fui module get",file_import_list:"fui import list"},Wh={'khai báo "function(...)"':'"function(...)" declaration','khai báo "function ten(...)"':'"function name(...)" declaration','lệnh "return"':'"return" statement','khai báo "var"':'"var" declaration','khai báo "let"':'"let" declaration','khai báo "const"':'"const" declaration','dấu ";" nối nhiều câu lệnh':'";" joining statements','" (tại ':'" (at ',") chứa ":") contains "," — giá trị binding đi thẳng vào template compiler của Vue, ":" — Vue compiles a binding as ONE expression ","chỉ nhận MỘT BIỂU THỨC thuần (object/array literal, ternary, chuỗi .map()/.filter() arrow, truy cập thuộc tính). ":"(literal, ternary, .map()/.filter() arrow, property access). ","Statement ở đây làm compile template hỏng → TRANG TRẮNG mà không có JS error hay [Vue warn] nào. ":"A statement breaks the template: BLANK PAGE with no JS error or [Vue warn]. ",'Cách sửa: đưa logic vào một hàm trong script.js (function buildX(){ … vueData.x = …; }), gọi qua "EXE": "buildX()" ':'Fix: move the logic into a script.js function (function buildX(){ … vueData.x = …; }), call it with "EXE": "buildX()" ','trong CALLBACK của action nạp dữ liệu, rồi "':'in the load action CALLBACK, and point "','" chỉ trỏ tới biến vueData đó (vd "':'" at that vueData var (e.g. "',"Chi tiết: references/advanced-techniques.md.":"See: fui skill ref references/advanced-techniques.md.","(chuẩn mặc định fui/DESIGN.md — chỉ giữ nguyên nếu user yêu cầu rõ kiểu khác)":"(fui/DESIGN.md default; keep only if the user asked for another style)","Module gác quyền bằng ":"Module gates on "," mã literal (":" literal right codes (",") nhưng chưa có cache _db/rights.json ":") but _db/rights.json is not cached, ","để đối chiếu — KHÔNG kiểm được mã nào có thật. Mã không tồn tại làm rightTest() luôn trả false: ":"so none can be checked. An unknown code makes rightTest() always false: ","control/menu biến mất im lặng, không lỗi JS. Chạy right_function_list + right_system_list (một lần cho mỗi project) rồi validate lại.":"the control/menu silently disappears. Run fui right function list and fui right system list (once per project), then validate again.",'" chưa được định nghĩa cho project (_db/rights.json có ':'" is not defined for the project (_db/rights.json has '," mã: ":" codes: "," mã). ":" codes). ","rightTest() sẽ luôn trả false → control/menu gác bằng mã này biến mất mà không báo lỗi. ":"rightTest() is always false, so controls gated by it silently disappear. ","Tạo mã: right_function_new({ functionCode: ":"Create: fui right function new --code ",', functionName: "…" }); hoặc chạy right_function_list nếu mã đã có trên server mà cache cũ.':' --name "…", or fui right function list if the cache is stale.',"hoặc chạy right_function_list nếu mã đã có trên server mà cache cũ.":"or fui right function list if the cache is stale."," chưa được định nghĩa cho project (_db/rights.json có: ":" is not defined for the project (_db/rights.json has: ","(chưa mức nào)":"(none)"," — mức 9 luôn hợp lệ":"; 9 is always valid","Không ai được cấp mức này thì control gác bằng nó biến mất im lặng. ":"Nobody holds this level, so gated controls silently disappear. ","Định nghĩa: right_system_new({ sysRight: ":"Define: fui right system new ",', note: "…" }); hoặc chạy right_system_list nếu cache cũ.':' --note "…", or fui right system list if the cache is stale.','Action gọi API "':'Action calling API "',") có vẻ là thao tác XÓA nhưng thiếu CONFIRM — mọi action xóa phải hỏi xác nhận.":") looks like a DELETE without CONFIRM; every delete action must confirm.","Nút/mục xóa (tại ":"Delete button/item (at ",') đang màu "':') is color "','" — nút xóa dùng màu "warning" (cam), không dùng error/đỏ.':'"; delete buttons use "warning" (orange), not error/red.',"v-toolbar (tại ":"v-toolbar (at ",') có color/dark — toolbar tiêu đề phải "dense flat" cùng màu nền với thân dialog, không tô màu/dark riêng.':') has color/dark; a title toolbar must be "dense flat" on the dialog background, no own color/dark.',"Dialog (tại ":"Dialog (at "," — bộ width chuẩn: 400 / 550 / 600 / 650 / 800.":"; standard widths: 400 / 550 / 600 / 650 / 800.","v-card trong dialog (tại ":"v-card in dialog (at ",') có elevation/outlined — bỏ đi: v-dialog đã tự có bóng đổ, thêm nữa là bóng chồng bóng. v-card chỉ cần class "pa-0" (triệt đệm để toolbar tiêu đề sát mép).':') has elevation/outlined; remove it (v-dialog already casts a shadow). v-card needs only class "pa-0".',"Control (tại ":"Control (at ",') mang class "flex-md-grow-0 ml-2" — dấu hiệu pattern label-div rời đã loại bỏ; form dùng floating label ("label" trong attr của field).':') has class "flex-md-grow-0 ml-2", the retired separate label-div pattern; forms use floating labels ("label" in the field attr).'," trong dialog (tại ":" in dialog (at ",') thiếu "label" — form dialog dùng floating label.':') lacks "label"; dialog forms use floating labels.'," (tại ":" (at ",") — phản hồi/thông báo dùng hệ thống có sẵn của FUI: MESS (toast) / MESSBOX (hộp thoại) trong action, CONFIRM để xác nhận — không tự dựng ":"); use FUI feedback instead: MESS (toast) / MESSBOX (dialog) in actions, CONFIRM to confirm; do not build "," trong controls.":" in controls.",') không có ":height" — bảng sẽ tự giãn theo số dòng và làm cả TRANG cuộn. Bảng data chính nên đặt `":height": -16` (fill tới đáy màn hình, chừa 16px — component tự đo vị trí thật, không cần tính tay); trong dialog dùng số dương (px cố định, vd 360). Bảng ngắn/lookup chủ ý tự giãn thì bỏ qua.':') has no ":height", so it grows with its rows and scrolls the whole PAGE. Main data table: `":height": -16` (fills to the screen bottom, self-measured); in a dialog a positive px (e.g. 360). Skip for short lookup tables.',') tính ":height" thủ công theo viewport (`':') computes ":height" from the viewport (`',"`) — không cần nữa: ":"`); not needed: ",' tự đo vị trí thật của bảng sau layout và tự cập nhật khi resize. Thay bằng `":height": -16` (số âm = khoảng chừa ở đáy).':' measures itself after layout and on resize. Use `":height": -16` (negative = bottom gap).','Nút "':'Button "','" trong v-card-actions (tại ':'" in v-card-actions (at ',") — dialog chuẩn chỉ dùng icon close trên toolbar, không thêm nút Đóng ở actions.":"); standard dialogs close via the toolbar icon only, no Close button in actions.",' có "w": ':' has "w": '," — vùng chết 13–24: V2 hiểu là ":" — dead zone 13–24: V2 reads ",'px, còn V3 sinh class "v-col-':'px, V3 emits class "v-col-','" mà Vuetify 3 không định nghĩa (chỉ có .v-col-1 … .v-col-12) nên cột KHÔNG có bề rộng nào, rơi về chia đều. Dùng 1–12 (chia cột) hoặc >= ':'", which Vuetify 3 lacks (only .v-col-1 … .v-col-12), so the column gets NO width. Use 1–12 (grid) or >= ',' (px, kèm `"col": { "class": "shrink" }`).':' (px, with `"col": { "class": "shrink" }`).',' (px) kèm `"col": { "shrink": ... }` — dạng thuộc tính trần là prop của v-flex (Vuetify 2); Vuetify 3 không có prop đó nên nó trơ và cột vẫn giãn hết chỗ trống. Viết `"col": { "class": "shrink" }` để đúng ở cả hai framework.':' (px) with `"col": { "shrink": ... }`: a bare v-flex prop (Vuetify 2), inert in Vuetify 3, so the column still grows. Write `"col": { "class": "shrink" }` for both.'," — số >= ":" — >= "," nghĩa là bề rộng cố định px, nhưng cột grid mặc định flex-grow:1 nên flex-basis ":" means fixed px, but grid columns default to flex-grow:1, so flex-basis ",'px KHÔNG có tác dụng: cột vẫn giãn hết chỗ trống (đo thật: w=300 ra 687px ở V2, 738px ở V3). Thêm `"col": { "class": "shrink" }`.':'px has NO effect and the column still grows (measured: w=300 gives 687px V2, 738px V3). Add `"col": { "class": "shrink" }`.'," — `v-spacer` mà có `shrink` thì co lại còn 0px (đo thật: 24px ở V3, 0px ở V2), không đẩy được nhóm sau sang phải nữa và cả hàng dồn về trái. Bỏ `shrink` khỏi riêng cột này.":" — `v-spacer` with `shrink` collapses to 0px and no longer pushes the next group right. Remove `shrink` from this column.",' nằm cùng hàng với `v-spacer`, `w` rỗng và chỉ có `"col": { "shrink": ... }` — dạng thuộc tính trần là prop của v-flex (Vuetify 2); Vuetify 3 không có prop đó nên cột vẫn giãn và chiếm một phần đều. Viết `"col": "shrink"` (hoặc `{ "class": "shrink" }`).':' shares a row with `v-spacer`, has empty `w` and only `"col": { "shrink": ... }`: a bare v-flex prop (Vuetify 2), inert in Vuetify 3, so the column grows. Write `"col": "shrink"` (or `{ "class": "shrink" }`).',' nằm cùng hàng với `v-spacer` nhưng thiếu `shrink` — `w` rỗng KHÔNG có nghĩa "tự co theo nội dung": runtime bọc phần bề rộng trong `if (c.w)` nên cột này không có class grid lẫn flex-basis, còn trơ `flex-grow: 1` và được CHIA PHẦN TRỐNG y như `v-spacer`. Hệ quả: control bị kéo dãn, hở khoảng giữa các nút, nhóm cuối hàng không sát mép phải. Chuẩn cho hàng nhiều control: MỌI cột `"col": "shrink"`, trừ đúng `v-spacer`.':' shares a row with `v-spacer` but lacks `shrink`: empty `w` is NOT fit-content, the column keeps `flex-grow: 1` and SPLITS free space like `v-spacer` (stretched controls, gaps, last group off the right edge). In multi-control rows EVERY column is `"col": "shrink"` except `v-spacer`.'," gộp ":" puts "," lệnh trong MỘT object `{}` (":" commands in ONE object `{}` (",") — chỉ `":"); only `","` chạy, còn ":"` runs, ",' bị bỏ IM LẶNG (không JS error, không [Vue warn]). `vueAction` dò key lệnh theo thứ tự cố định API → CALL → EXE → FUN → IF → MESS → MESSBOX và `return` ngay ở nhánh đầu tiên khớp, nên thứ tự viết không có tác dụng. Nhiều lệnh có thứ tự trước sau thì phải là MẢNG, mỗi lệnh một object: `[{ "':' are SILENTLY dropped: `vueAction` checks API → CALL → EXE → FUN → IF → MESS → MESSBOX and runs only the first match. Ordered commands need an ARRAY, one per object: `[{ "'," có lệnh `":" has command `","` nằm chung object với key gán dữ liệu ":"` in the same object as data keys "," — fragment gán chạy ở nhánh CUỐI của `vueAction`, nhánh đó chỉ tới lượt khi KHÔNG key lệnh nào khớp, nên các key này không bao giờ được gán. Tách thành mảng: `[{ ":" — `vueAction` assigns only when NO command key matches, so these keys are never set. Split into an array: `[{ ","module.json gốc phải là một object với các khối data/watch/controls/set.":"module.json root must be an object with data/watch/controls/set.",'Khối cấp 1 không chuẩn: "':'Unknown top-level key: "','" — module.json chỉ có data/watch/controls/set (xem module-json-anatomy.md).':'"; module.json has only data/watch/controls/set (see module-json-anatomy.md).','"data" phải là MẢNG (array), hiện là ':'"data" must be an array, got ','"watch" phải là OBJECT, hiện là ':'"watch" must be an object, got ','"controls" phải là MẢNG container, hiện là ':'"controls" must be an array of containers, got ','"set" phải là OBJECT, hiện là ':'"set" must be an object, got ',"data[] chứa phần tử không phải object: ":"data[] has a non-object item: ",") không khớp named action nào trong data[]. Named actions hiện có: ":") matches no named action in data[]. Named actions: ","(không có)":"(none)",'] phải là container có "rows" (grid bắt buộc: container > rows > cols).':'] must be a container with "rows" (container > rows > cols).','] phải có "cols" (mảng Control Object).':'] needs "cols" (array of Control Objects).',') thiếu "w" — bắt buộc cho item trong cols.':') lacks "w" (required in cols).','"children" không được hỗ trợ (tại ':'"children" is not supported (at ',') — dùng "innerHTML" (mảng Control Object) để lồng.':'); nest with "innerHTML" (array of Control Objects).',") không có trong registry FUI (component-quickref.md) và không thấy trong components local — kiểm tra chính tả hoặc chạy component_list.":") is neither in the FUI registry (component-quickref.md) nor a local component; check spelling or run fui component list.",") không có trong danh sách t-* hợp lệ: ":") is not a valid t-* cell: ",") chưa thấy file .vue/local metadata — tạo component trước hoặc chạy component_list.":") has no .vue/local metadata; create it first or run fui component list.",') — module.json KHÔNG hỗ trợ shorthand "@"; dùng "v-on:':'); module.json has no "@" shorthand; use "v-on:',') — module.json KHÔNG hỗ trợ slot shorthand "#"; dùng "v-slot:':'); module.json has no "#" slot shorthand; use "v-slot:',") — module thực tế gần như không dùng style ngoài width (controls-styling-vocabulary.md). Cân nhắc bỏ.":"); real modules rarely style beyond width (controls-styling-vocabulary.md). Consider removing it.",') — biến gốc "':'); root var "','" chưa khai báo trong data[]. Khai báo state trước để reactive hoạt động đúng.':'" is not declared in data[]; declare it so reactivity works.',') — biến "':') — var "','" chưa khai báo trong data[], watcher sẽ không bao giờ chạy.':'" is not declared in data[]; the watcher never runs.','" cần import: ':'" needs import: '," — chưa thấy trong _imports.json (module lẫn project). Dùng file_import_list/file_import_new (xem component-quickref.md).":", missing from _imports.json (module and project). Use fui import list / fui import new (see component-quickref.md).",") — không tìm thấy spAPI_":"); no spAPI_"," trong schema local. Deploy SP trước hoặc chạy db_schema_get (bỏ qua nếu API thuộc backend khác).":" in the local schema. Deploy the SP or run fui schema pull (ignore if another backend)."," alias được gọi nhưng chưa có kết nối local — KHÔNG đối chiếu được tên SP: ":" called alias(es) have no local connection, so SP names are NOT checked: ",'Kết nối từng cái bằng db_connect({ apiName: "':"Connect each: fui db add ",'", apiDomain: "…", dbToken: "…" }) để bước kiểm này chạy (bỏ qua nếu alias thuộc backend của nhóm khác).':" -p <project> --token - --api-domain … (ignore if another team's backend).","/schema.json sai định dạng (bản cache cũ) — bỏ qua bước đối chiếu tên API với stored procedure ở ":"/schema.json has an old format; API names were not checked against SPs for ","các kết nối này":"these connections","kết nối này":"this connection",". Chạy db_schema_get để lấy bản mới.":". Run fui schema pull.","module.json rỗng hoặc không tồn tại trong ":"module.json is empty or missing in ","module.json có lỗi JSON syntax: ":"module.json JSON syntax error: ","✅ module.json hợp lệ — không phát hiện lỗi ngữ nghĩa.":"✅ module.json valid: no semantic errors."," lỗi (sẽ hỏng khi chạy):":" error(s) (break at runtime):"," cảnh báo (nên xem lại):":" warning(s) (review):",' biểu thức động (`:attr` phức tạp, CALL/API/v-model/OUT dạng `{{…}}`/gọi hàm/template string) KHÔNG kiểm tĩnh được — biến bên trong chúng không được đối chiếu với data[]. "Hợp lệ" ở đây KHÔNG có nghĩa là chạy đúng: dùng module_simulate để phủ phần này.':' dynamic expression(s) (complex `:attr`, `{{…}}` CALL/API/v-model/OUT, calls, template strings) are NOT checked statically. "Valid" does NOT mean it runs: use fui module simulate.',"Thống kê: ":"Stats: "," API ref đối chiếu schema":" API ref checked against schema"," CALL xuyên cửa sổ (thuộc module khác, không kiểm được từ đây)":" cross-window CALL (other module, not checked)"," mã quyền đối chiếu _db/rights.json":" right code checked against _db/rights.json","Chưa có cache _db/rights.json — KHÔNG kiểm được ":"_db/rights.json not cached: cannot check whether "," mã FunctionRight (":" FunctionRight code(s) (","có thật hay không. Preview cấp sẵn đúng những mã này nên control vẫn hiện ở ĐÂY; user thật chỉ thấy nếu mã đã ":"exist. The preview grants them, so controls show HERE; real users see them only if the codes are ","được định nghĩa. Chạy right_function_list (một lần cho mỗi project) rồi render lại.":"defined. Run fui right function list (once per project), then render again.","\uD83D\uDEA8 FunctionRight CHƯA ĐƯỢC ĐỊNH NGHĨA trên hệ thống acc: ":"\uD83D\uDEA8 FunctionRight NOT DEFINED in acc: "," — preview cấp sẵn nên control vẫn ":" — the preview grants them, so controls ","hiện ở ĐÂY, nhưng chạy thật thì rightTest() luôn trả false → control/menu biến mất mà không có lỗi JS nào. ":"show HERE, but live rightTest() is false and they silently disappear. ",'Tạo mã: right_function_new({ functionCode: …, functionName: "…" }); hoặc right_function_list nếu cache cũ.':'Create: fui right function new --code … --name "…", or fui right function list if the cache is stale.',"\uD83D\uDEA8 CẤU TRÚC MOCK CHƯA ĐƯỢC KIỂM CHỨNG — nghi ngờ tự suy diễn thay vì đọc từ SP thật:":"\uD83D\uDEA8 UNVERIFIED MOCK SHAPE: likely guessed instead of read from the real SP:",'"] khớp tên SP CÓ THẬT "':'"] matches REAL SP "','" trong schema local, nhưng SP đó chưa từng được db_sp_verify đọc thân trong phiên này.':'", whose body fui sp verify has not read in this session.',' → Chạy db_sp_verify({ name: "':" → Run fui sp verify ",'" }) để đọc thân SP thật rồi COPY NGUYÊN VĂN snippet apiMocks nó sinh sẵn — không tự đoán tên cột/shape cho SP đã tồn tại, kết quả mô phỏng sẽ lệch với thực tế.':" and copy its apiMocks snippet VERBATIM; do not guess columns/shape of an existing SP.",") — gọi tAPI thật thành công":"): live tAPI call OK",") gọi thật thất bại (":") live call failed (",") — rơi xuống cache/tự sinh":"); fell back to cache/generated",") có lệnh ghi — KHÔNG gọi thật, dùng cache/tự sinh":") writes: NOT called live, cache/generated used",") có đoạn đọc tĩnh không nhìn xuyên qua được (SQL động / gọi SP khác) — KHÔNG gọi thật, dùng cache/tự sinh":") has opaque reads (dynamic SQL / nested SP): NOT called live, cache/generated used",") chưa có thân SP local để phân loại an toàn — KHÔNG gọi thật, dùng cache/tự sinh":") has no local body to classify: NOT called live, cache/generated used",") đã đạt trần ":") hit the cap of "," lượt gọi thật/lượt simulate — dùng cache/tự sinh":" live calls per run; cache/generated used",") — dùng _testdata cache (nguồn: ":"): _testdata cache (source: ",", lưu lúc ":", saved ",") — tự sinh mock từ cấu trúc SELECT, đã lưu cache":"): mock generated from the SELECT shape, cached","… (trace bị cắt — quá dài)":"… (trace truncated)",") — stub, không mô phỏng UI thật":"): stub, UI not simulated"," chưa hỗ trợ trong simulator — trả undefined":" unsupported in the simulator: undefined"," — stub (sub-window không mô phỏng)":": stub (sub-window not simulated)","(giả lập user đóng dialog → chạy onclose)":"(simulated dialog close → onclose)",'" → OK (mặc định simulator)':'" → OK (simulator default)','" → HỦY':'" → CANCEL','đọc cross-window "':'cross-window read "','" — không mô phỏng được, gán undefined':'": not simulated, undefined'," (không mô phỏng)":" (not simulated)",'" undefined — CALL trỏ tới action không tồn tại?':'" undefined: CALL to a missing action?',"Lỗi khi chạy action: ":"Action error: ",'" không tồn tại (simulator chỉ có các hàm FUI chuẩn)':'" does not exist (simulator has only standard FUI functions)','" lỗi eval: ':'" eval error: '," → coi như false":" → treated as false",'(mock khớp "':'(mock matched "','không có mock cho API "':'no mock for API "','" — trả mặc định {data: []}':'": default {data: []}'," (mock lỗi)":" (error mock)",'" lỗi không được bắt (không có ERROROUT) → hiện ERROR DIALOG mặc định: ':'" error not handled (no ERROROUT) → default ERROR DIALOG: ',"← (không có OUT)":"← (no OUT)"," thay đổi → ":" changed → ","watch cascade vượt ":"watch cascade over "," vòng — nguy cơ VÒNG LẶP VÔ HẠN giữa các watch/action":" rounds: possible INFINITE LOOP between watch/actions","MỞ \uD83D\uDD32":"OPEN \uD83D\uDD32","ĐÓNG ▪":"CLOSED ▪","Lỗi khi init data[]: ":"data[] init error: ",'không tìm thấy control có label "':'no control labeled "','" kèm v-on:click/:action':'" with v-on:click/:action',"(khớp ":"(matched "," control — dùng #":" controls; using #",'control nằm trong f-table nhưng "':'control is in an f-table but "','" rỗng — item = {} (truyền "item" trong event để chỉ định)':'" is empty: item = {} (pass "item" in the event)',"exe lỗi: ":"exe error: "," không hợp lệ — cần một trong: set / click / call / exe / assert":" invalid: needs one of set / click / call / exe / assert","SystemRight=9 (mặc định, quyền cao nhất)":"SystemRight=9 (default, highest)","] (mặc định — quét được từ module.json)":"] (default: scanned from module.json)","FunctionRight=[] (mặc định — không quét được mã nào trong module.json)":"FunctionRight=[] (default: no codes found in module.json)",". rightTest()/v-if theo FunctionRight khác mã trên sẽ trả false — truyền tham số `user` để test vai trò hạn chế hơn.":". rightTest()/v-if on other FunctionRight codes is false; pass --user to test a narrower role.","═══ KẾT QUẢ ═══":"═══ RESULT ═══","── Cảnh báo ──":"── Warnings ──","(stub đã dùng — kết quả các hàm này KHÔNG phản ánh runtime thật: ":"(stubs used; their results do NOT reflect the real runtime: ","── STATE CUỐI ──":"── FINAL STATE ──"," assert đạt":" assert passed"," · dialog đang mở: ":" · open dialogs: ","\n\n── Live/Cache data (SP chưa có mock, đã tự resolve) ──\n":`
107
107
 
108
108
  ── Live/Cache data (SPs without mocks, auto-resolved) ──
109
109
  `,"Phản hồi không phải JSON":"Response is not JSON",': ❌ thiếu "cols"':': ❌ missing "cols"'," ⚠ vượt 12":" ⚠ over 12","❌ module.json gốc không phải object.":"❌ module.json root is not an object.","(không có controls)":"(no controls)","] ❌ không phải object":"] ❌ not an object","(mặc định)":"(default)",' ❌ thiếu "rows"':' ❌ missing "rows"',"(không có state)":"(no state)"," ⚠ không thấy dùng ở controls/watch/actions":" ⚠ unused in controls/watch/actions","(không có action)":"(no actions)"," ⚠ không thấy nơi gọi (CALL/watch/v-on)":" ⚠ never called (CALL/watch/v-on)",": ⚠ hidden-container không có v-model — không điều khiển mở/đóng được":": ⚠ hidden-container without v-model: cannot open/close"," — có lệnh mở (":" — opened by (",' ⚠ không thấy lệnh mở "':' ⚠ no opener "',' = true" ở đâu':' = true" found'," file, tải lúc ":" files, fetched ","API trả danh mục rỗng":"API returned an empty list","cache CŨ (":"STALE cache (","Không gọi được API danh mục framework ":"Framework list API failed for ",") — dùng cache cũ trên đĩa.":"); using the stale disk cache.","danh sách dự phòng hardcode (":"hardcoded fallback list (",") và chưa có cache — ":") and no cache: ","dùng danh sách hardcode trong renderAssets.ts. Danh sách này có thể đã lệch với server.":"using the hardcoded list, which may differ from the server.","projectdata.js tải về từ ":"projectdata.js from ",' không chứa "$projectData =" (':' lacks "$projectData =" (',"không dùng, quay về dựng $projectData + component project từ workspace local.":"ignored; building $projectData + project components from the local workspace.","Không tải được ":"Cannot fetch "," — quay về dựng từ workspace local.":"; building from the local workspace.","StyleCSS cấp project tải về từ ":"Project StyleCSS from "," là HTML chứ không phải CSS — bỏ qua.":" is HTML, not CSS; skipped.","StyleCSS project rỗng (":"Project StyleCSS empty (",") — project chưa có component nào khai style.":"): no project component declares a style."," — CSS component cấp project sẽ dựng từ local.":"; project component CSS comes from local.",'.vue" không có khuôn `export default { … }` — server biên dịch bằng cách ':'.vue" lacks `export default { … }`; the server ',"nối thẳng phần <script> vào `{template:`…`, …}` nên bản trên server có thể lỗi cú pháp. ":"splices <script> into `{template:`…`, …}`, so the server build may have a syntax error. ","renderUI đã bọc an toàn để chạy tiếp, nhưng nên sửa .vue về đúng khuôn.":"The render wraps it to continue; fix the .vue shape."," (bundle server không theo khuôn ComponentBuild)":" (server bundle not in ComponentBuild shape)"," (chỉ đối chiếu được template, không bóc được thân script)":" (template checked only; script body not extractable)",'": FUI không nhận cú pháp @/# trong JSON — đổi thành "v-on:"/"v-slot:". Đã bỏ qua.':'": FUI does not accept @/# in JSON; use "v-on:"/"v-slot:". Skipped.',"Không xác định được projectId. Truyền projectId hoặc chạy session_set trước.":"No project: pass -p or run fui use first.","Không tìm thấy ":"Not found: ",".vue trong ":".vue in "," Có sẵn: ":" Available: "," Thư mục components rỗng.":" The components folder is empty."," Nếu đây là component cấp module, truyền thêm moduleId.":" For a module-level component, also pass -m."," lần render, vượt giới hạn ":" renders, over the limit of ","Mỗi lần render là một phiên browser thật — chia nhỏ thành nhiều lần gọi.":"Each render is a real browser session; split into several calls.","Nguồn: ":"Source: ","(không tên)":"(unnamed)"," lần render":" renders","Ảnh: ":"Screenshots: ","Không gian riêng: chỉ component này + CSS của chính nó + theme/import cấp project.":"Isolated: this component + its own CSS + project theme/imports.","style.css của module KHÔNG nạp — component phải tự đứng được. Tầng project dựng từ workspace":"Module style.css is NOT loaded; the component must stand alone. The project layer comes from the workspace,","local nên bản chưa push vẫn hiện; chạy `project_sync` nếu nghi file local đã cũ.":"so unpushed edits show; run `fui project sync` if local files look stale.","⚠ Ghi chú dựng harness:":"⚠ Harness notes:","file môi trường thiếu":"missing env files","text nghi vấn":"suspect text","ảnh vỡ":"broken images","canvas chết":"dead canvas","text dính/chồng lấn":"text collisions","assert hỏng":"failed asserts","app render RỖNG (trước có nội dung) ↑":"app EMPTY (had content before) ↑","app đã có nội dung ↓":"app has content now ↓","tràn ngang (trước không) ↑":"horizontal overflow (none before) ↑","hết tràn ngang ↓":"no horizontal overflow ↓","Δ so với lượt trước (":"Δ vs previous run (","): KHÔNG chỉ số nào đổi — thay đổi vừa rồi không chạm tới lỗi đang có. Đổi hướng chẩn đoán, đừng render lại cùng một cách.":"): NO metric changed; the last edit missed the failure. Change the diagnosis, do not render again the same way.","): không đổi — cả hai lượt đều sạch.":"): unchanged; both runs clean.","\uD83D\uDED1 ĐANG GIẬM CHÂN — ":"\uD83D\uDED1 STUCK: "," lượt render hỏng liên tiếp mà KHÔNG chỉ số nào đổi.":" failed renders in a row with NO metric change."," Render lại lần nữa theo cùng hướng sẽ cho đúng kết quả này. Chọn MỘT trong ba:":" Rendering again the same way gives the same result. Pick ONE:"," 1. Chạy tầng rẻ hơn được nêu ở dòng ↪ trên — nó chỉ thẳng vào chỗ hỏng.":" 1. Run the cheaper check on the ↪ line above; it points at the fault."," 1. Lùi về tầng tĩnh: module_validate (lỗi quy tắc) → module_outline (cấu trúc) → module_simulate không renderUI (action/watch).":" 1. Step back: fui module validate (rules) → fui module outline (structure) → fui module simulate without --render (actions/watch)."," 2. Đọc lại screenshot + khối lỗi ở cuối report bằng mắt, đừng chỉ đọc con số.":" 2. Look at the screenshot and the error block at the end, not only the numbers."," 3. HỎI NGƯỜI DÙNG. Ba lượt không nhúc nhích thường nghĩa là giả định ban đầu sai, không phải sửa chưa đủ.":" 3. ASK THE USER. Three stuck runs usually mean a wrong assumption, not an incomplete fix.","═══ CHẨN ĐOÁN ═══":"═══ DIAGNOSIS ═══","[MÔI TRƯỜNG] ":"[ENV] "," file import/asset KHÔNG nạp được → sửa phần này TRƯỚC (project_sync / module_get / file_import_*). Lỗi ở hai nhóm dưới có thể chỉ là hệ quả.":" import/asset file(s) NOT loaded; fix this FIRST (fui project sync / fui module get / fui import). Errors below may only be consequences.","[MÔI TRƯỜNG] ✓ mọi import/asset nạp đủ.":"[ENV] ✓ all imports/assets loaded."," lúc init)":" at init)"," assert hỏng":" failed asserts"," → lỗi nằm trong module.json / script.js / component.":" → fault is in module.json / script.js / component.","[MODULE] ✓ không JS error, không Vue warn.":"[MODULE] ✓ no JS error, no Vue warn.","app render RỖNG":"app EMPTY","tràn ngang viewport":"horizontal viewport overflow"," text nghi vấn (undefined/NaN)":" suspect text (undefined/NaN)"," ảnh vỡ":" broken images"," chỗ text dính/chồng lấn":" text collisions","[HIỂN THỊ] ":"[VIEW] "," → xem screenshot; đây là nhóm CHỈ renderUI thấy được.":" → check the screenshot; only --render sees these.","[HIỂN THỊ] ✓ DOM audit sạch.":"[VIEW] ✓ DOM audit clean.","`… is not defined` = một định danh trần không có trên window. Ca thường gặp nhất: `EXE`/`FUN` gọi hàm chưa viết trong script.js — hoặc viết rồi mà quên `module_publish_script` (module_publish_json KHÔNG đẩy script.js). Soát tên hàm trong script.js trước khi đi tìm chỗ khác.":"`… is not defined` = a bare name missing on window. Usually `EXE`/`FUN` calls a function not in script.js, or not pushed with `fui module publish script` (publish json does NOT push script.js). Check script.js names first.","Trang trắng mà KHÔNG có JS error/Vue warn là dấu hiệu kinh điển của statement trong một `:attr` (Vue nuốt lỗi compile template) — chạy module_validate, nó bắt tĩnh được ca này.":"A blank page with NO JS error/Vue warn usually means a statement in a `:attr` (Vue swallows the template error); fui module validate catches it.",'[Vue warn] kiểu "property is not defined" = biến dùng trong template chưa khai trong data[] — module_validate bắt được (v-model/watch/state), không cần browser.':'[Vue warn] "property is not defined" = a template var missing from data[]; fui module validate catches it (v-model/watch/state).','Text "undefined"/"NaN" thường là OUT/mapData trỏ sai khoá chứ không phải lỗi hiển thị — module_simulate (engine vm) chỉ ra chỗ gán, chạy nhanh hơn nhiều.':'Text "undefined"/"NaN" usually means OUT/mapData uses a wrong key; fui module simulate (no --render) shows the assignment, much faster.',"không rõ tuổi":"unknown age","cũ dưới 1 giờ":"under 1 h old","offline: chưa có bản cache nào cho URL này":"offline: no cached copy of this URL","offline — dùng cache ":"offline: cache ","nội dung ĐỔI so với bản cache ":"content CHANGED vs cache ","không revalidate được (":"revalidate failed (",") — dùng cache ":"); cache ","_moduleInfo.json không có url kèm host — origin lấy từ ":"_moduleInfo.json has no url with a host; origin from ","domain asset mặc định":"default asset domain","Chạy `module_get` để có url thật nếu render sai domain.":"Run `fui module get` for the real url if the domain is wrong.","Project/module phục vụ trên nhiều domain (":"Project/module served on several domains (",') — lần render này chạy ở "':'); this render uses "',"v_Set.domainSetting của domain khác KHÔNG được áp dụng; muốn thử domain khác thì render module có url thuộc domain đó.":"v_Set.domainSetting of other domains is NOT applied; to test another domain, render a module whose url is on it.","(trống)":"(empty)",'" không phải .js/.css — không nạp vào trang render':'" is not .js/.css; not loaded'," import cấp ":" import(s), scope "," không có FileSort trong _imports.json (":", lack FileSort in _imports.json (",") → tạm xếp cuối ":") → sorted last ","). Thứ tự nạp có thể khác production; chạy `":"). Load order may differ from production; run `","` để lấy FileSort thật.":"` for the real FileSort.",': file nội bộ "':': inline file "','" thiếu/rỗng và không có fileID để lấy bản server → KHÔNG nạp được. ':'" missing/empty and no fileID to fetch it → NOT loaded. ',"Chạy `":"Run `","`. Lỗi JS/hiển thị bên dưới có thể chỉ là hệ quả của việc này.":"`. JS/display errors below may only be consequences.",": chưa có bản local → dùng bản trên server. Chạy `":": no local copy → server copy used. Run `","` nếu cần sửa file này.":"` to edit it.",": không có file nội bộ và tên không phải URL/đường dẫn tuyệt đối → không xác định được nguồn để nạp. ":": no inline file and the name is not a URL/absolute path → nothing to load. ","Nếu đây là file tự tạo, chạy `":"If you created it, run `"," — sửa trước khi renderUI.":"; fix it before --render.","module.json rỗng — render shell với module trống":"module.json empty: rendering an empty shell","(bỏ qua — HTMLOnly)":"(skipped: HTMLOnly)",": KHÔNG nhúng (HTMLOnly=true — server cũng không nhúng)":": NOT embedded (HTMLOnly=true, same as the server)",": danh mục từ ":": list from ","HTMLOnly=true → render KHÔNG nhúng framework ":"HTMLOnly=true → no framework ",", KHÔNG nhúng projectdata/StyleCSS cấp project ":", no project projectdata/StyleCSS ","và KHÔNG dựng #fastProjectAPP — giống hệt trang production. Thư viện nào module cần (Vue, jQuery, Vuetify, ":"and no #fastProjectAPP, exactly like production. Libraries the module needs (Vue, jQuery, Vuetify, ","lodash…) phải do CHÍNH module khai làm import; thiếu là trắng trang thật, không phải lỗi của render.":"lodash…) must be module imports; a missing one blanks the real page too.","Tầng project dựng từ workspace LOCAL (chế độ preview cấp component) — không dùng bundle server. ":"Project layer built from the LOCAL workspace (component preview), not the server bundle. ","Chạy `project_sync` nếu nghi file project trong workspace đã cũ.":"Run `fui project sync` if workspace project files look stale."," (đã sửa local, chưa push)":" (edited locally, not pushed)"," (chưa có trên server)":" (not on the server)"," (<style> đã sửa local, chưa push)":" (<style> edited locally, not pushed)","⛔ Component cấp project lệch với bản trên server: ":"⛔ Project components differ from the server: ","renderUI dựng trang bằng bundle project TẢI TỪ SERVER (_aset/.../projectdata) nên bản local chưa push ":"The render uses the project bundle FROM THE SERVER (_aset/.../projectdata), so unpushed local edits ","không xuất hiện — nếu vẫn render, tấm ảnh bạn nhận được là BẢN CŨ, không phải bản vừa sửa.\n\n":`do not show; a render now shows the OLD version.
110
110
 
111
111
  `,"• Nếu bản local là bản bạn vừa sửa → `component_preview` để kiểm chứng riêng component (chạy được bằng ":"• Local is your edit → `fui component preview` checks the component alone (uses ","file local, không cần push), rồi `component_update` từng component, rồi render lại.\n":"local files), then `fui component update` each one, then render again.\n","• Nếu bản trên server mới hơn (ai đó sửa trên web IDE) → `project_sync` để lấy về, ĐỪNG push đè.":"• Server is newer (edited in the web IDE) → `fui project sync`; do NOT overwrite it.","Không đối chiếu được component cấp project: ":"Cannot compare project components: ","Không chặn render, nhưng nếu giao diện khác mong đợi thì đây là chỗ đáng nghi đầu tiên.":"Render continues; if the UI looks wrong, suspect this first.","StyleCSS cấp project tải về RỖNG nhưng component local có khai <style>: ":"Project StyleCSS is EMPTY but local components declare <style>: ","Hoặc các component đó chưa push (`component_update`), hoặc route StyleCSS đang hỏng — ":"Either they are not pushed (`fui component update`) or the StyleCSS route is broken; ","dù nguyên nhân nào thì style cấp project trong lần render này cũng đang thiếu.":"either way project styles are missing in this render.","Không bóc được `var $projectData` khỏi bundle projectdata tải từ server — apiDomain/userInfo ":"Cannot extract `var $projectData` from the server projectdata bundle; apiDomain/userInfo ","trong lần render này suy ra từ project.json local, có thể lệch với bản trang thật đang chạy.":"come from local project.json and may differ from the live page.",'project.json local có apiDomain "':'Local project.json apiDomain "','" nhưng bản trên server là "':'" but the server has "',"render dùng bản server (giống trang thật). Chạy `project_sync` để đồng bộ file local.":"render uses the server value (like the live page). Run `fui project sync`.","(không tầng nào khai)":"(not set anywhere)",'Không tầng nào ($projectGroupSetting → $projectData → module.json set → domainSetting["':'No layer ($projectGroupSetting → $projectData → module.json set → domainSetting["','"]) khai apiDomain cho "':'"]) sets apiDomain for "','" — API sẽ gọi bằng đường dẫn tương đối và apiMocks khớp theo URL ':'"; APIs use relative paths, so URL-matched mocks ',"gần như chắc chắn trượt. Chạy `project_sync` nếu dữ liệu project đã cũ.":"almost surely miss. Run `fui project sync` if project data is stale.","] (mặc định — quét được từ module.json/project)":"] (default: scanned from module.json/project)","FunctionRight=[] (mặc định — không quét được mã nào)":"FunctionRight=[] (default: no codes found)",". rightTest()/v-if theo FunctionRight khác mã trên sẽ ẩn — truyền tham số `user` để test vai trò hạn chế hơn.":". rightTest()/v-if on other FunctionRight codes hides; pass --user to test a narrower role.","projectdata/StyleCSS: KHÔNG nhúng (HTMLOnly=true — server cũng không nhúng)":"projectdata/StyleCSS: NOT embedded (HTMLOnly=true, same as the server)","projectdata/StyleCSS: dựng từ workspace local (chế độ preview cấp component — bản local chưa push vẫn hiện)":"projectdata/StyleCSS: from the local workspace (component preview; unpushed edits show)","projectdata/StyleCSS: dựng từ workspace local (không tải được bản server)":"projectdata/StyleCSS: from the local workspace (server copy unavailable)","gọi tAPI thật (SP ":"live tAPI call (SP ",", nguồn: ":", source: ","tự sinh mock (SP ":"generated mock (SP ","), đã lưu cache":"), cached","Không launch được Chromium: ":"Cannot launch Chromium: ","\n→ Chạy một lần: npx playwright install chromium":`
112
- → Run once: bunx playwright install chromium`,'runtime điều hướng ra "':'runtime navigated to "','" — đã chặn (thường do login gate / redirect trong action)':'"; blocked (usually a login gate / redirect in an action)','" tải thất bại (':'" failed to load ('," — trả 404 (offline + chưa cache?)":"; answered 404 (offline, not cached?)"," → user preview (userInfo của project, auto-mock)":" → preview user (project userInfo, auto-mock)",'" — trả {data: []}':'": {data: []}'," → {data:[]} (không có mock)":" → {data:[]} (no mock)","module mở WebSocket tới backend thật — đã chặn (renderUI chỉ cho phép gọi thật qua tAPI SP đã xác nhận an toàn, không qua WebSocket)":"module opened a WebSocket to the real backend; blocked (only verified-safe tAPI SPs run live)","Vue chưa mount sau 15s — trang chưa init xong (xem JS error / asset hỏng / API treo bên dưới)":"Vue not mounted after 15s; page init unfinished (see JS errors / broken assets / hung APIs below)"," CẬP NHẬT":" UPDATED"," tải mới":" fetched"," gọi thật":" live"," tự sinh":" generated"," (API không-mock: ":" (unmocked API: ","trang load xong sau ":"page loaded in ",': không tìm thấy phần tử có text "':': no element with text "','" trên DOM':'" in the DOM'," phần tử — dùng #":" elements; using #",'CONFIRM → bấm "':'CONFIRM → click "',"CONFIRM → bấm nút cuối (không nhận diện được label)":"CONFIRM → click the last button (label not recognized)","CONFIRM → bấm nút đầu (không nhận diện được label)":"CONFIRM → click the first button (label not recognized)","global CALL không tồn tại trong runtime này":"global CALL missing in this runtime","chồng lấn":"overlap","dính sát (gap ":"touching (gap ","canvas trong ":"canvas in ","✘ <body> không render được phần tử nào (HTMLOnly — body.html LÀ cả trang)":"✘ <body> rendered nothing (HTMLOnly: body.html IS the page)","✘ #fastProjectAPP rỗng — controls không render":"✘ #fastProjectAPP empty: controls not rendered"," (CÓ file toàn cục nạp thiếu — xem mục MÔI TRƯỜNG THIẾU FILE trước khi nghi module.json)":" (global files FAILED to load; see MISSING ENV FILES before suspecting module.json)"," (kiểm tra pageerror / module.json)":" (check pageerror / module.json)","⚠ trang tràn ngang viewport ":"⚠ page overflows the viewport ","px — phần tử tràn: ":"px; overflowing: ","(không xác định)":"(unknown)","⚠ text nghi vấn hiển thị trên trang: ":"⚠ suspect text on page: "," (binding thiếu data?)":" (binding missing data?)","⚠ ảnh vỡ (load fail nhưng đang chiếm chỗ): ":"⚠ broken images (failed but taking space): ","⚠ canvas chart 0-size — chart init khi container chưa có kích thước: ":"⚠ 0-size chart canvas (init before the container had a size): ","⚠ text dính/chồng lấn (thẩm mỹ — PHẢI sửa, không được bỏ qua): ":"⚠ text collisions (visual; MUST fix): ","RENDER (browser thật): ":"RENDER (real browser): "," · HTMLOnly=true (KHÔNG nhúng framework/projectdata — như production)":" · HTMLOnly=true (no framework/projectdata, like production)","Origin trang: ":"Page origin: ","Import (framework ∪ project ∪ module, sort theo FileSort/FileName): ":"Imports (framework ∪ project ∪ module, by FileSort/FileName): "," nạp được":" loaded"," KHÔNG nạp được":" NOT loaded","── ⛔ MÔI TRƯỜNG THIẾU FILE (đọc TRƯỚC khi sửa module) ──":"── ⛔ MISSING ENV FILES (read BEFORE fixing the module) ──","Các file dưới đây không nạp được vào trang test. Hàm/biến/style toàn cục do chúng":"These files did not load. Their global functions/vars/styles","định nghĩa sẽ không tồn tại → lỗi JS và lỗi hiển thị bên dưới CÓ THỂ chỉ là hệ quả,":"are missing, so JS and display errors below MAY only be consequences,","không phải lỗi của module. Khắc phục phần này trước rồi render lại.":"not module faults. Fix this first, then render again.","── ⛔ ASSET CŨ, KHÔNG XÁC MINH ĐƯỢC (đọc TRƯỚC khi kết luận về platform) ──":"── ⛔ STALE, UNVERIFIED ASSETS (read BEFORE blaming the platform) ──","Không hỏi được server xem các file dưới đây còn đúng không, nên trang test đang chạy":"The server could not confirm these files, so the page ran",'bằng bản trên đĩa. Mọi kết luận kiểu "thư viện X có bug" rút ra từ lượt render này':'on disk copies. Any "library X has a bug" conclusion from this render',"đều KHÔNG dùng được — bản thật trên CDN có thể đã khác. Nối mạng rồi render lại,":"is NOT valid; the CDN copy may differ. Go online and render again,","hoặc ép tải mới bằng refreshAssets: true.":"or force a fetch with --refresh-assets.","── ℹ️ ASSET PLATFORM ĐÃ ĐỔI so với lượt render trước ──":"── ℹ️ PLATFORM ASSETS CHANGED since the previous render ──","Hành vi đổi mà module.json không đụng tới có thể đến từ đây, không phải từ module.":"Behavior changes not caused by module.json may come from here.","── STATE CUỐI (vueData) ──":"── FINAL STATE (vueData) ──"," (đính kèm bên dưới)":" (attached below)"," (quá lớn để inline — dùng Read tool để xem)":" (too large to inline; open it with the Read tool)","Dialog đang mở — chụp riêng: ":"Open dialog screenshot: "," API call (tất cả mock) · ":" API call (all mocked) · "," lúc init) · ":" at init) · "," · dialog đang mở":" · open dialog"},Mh=Object.entries(_h).sort(([e],[t])=>t.length-e.length);function Ve(e){let t=e;for(let[n,o]of xh)t=t.replace(n,o);for(let[n,o]of Mh)t=t.replaceAll(n,o);return t.replace(/`(project_sync|module_get|file_import_list)`/g,(n,o)=>`\`${Oh[o]}\``)}async function bn(e){let t=qr(await Vn(e));return t?[t]:[]}async function cl(e,t){let n=await Ko(e);if(n.errors.length===0&&n.warnings.length===0)return[];let o=Ve(Go(n));if(n.errors.length>0&&Zt())throw new Se(`[FUI_STRICT] ${t} blocked: module.json has semantic errors; fix them and retry
112
+ → Run once: bunx playwright install chromium`,'runtime điều hướng ra "':'runtime navigated to "','" — đã chặn (thường do login gate / redirect trong action)':'"; blocked (usually a login gate / redirect in an action)','" tải thất bại (':'" failed to load ('," — trả 404 (offline + chưa cache?)":"; answered 404 (offline, not cached?)"," → user preview (userInfo của project, auto-mock)":" → preview user (project userInfo, auto-mock)",'" — trả {data: []}':'": {data: []}'," → {data:[]} (không có mock)":" → {data:[]} (no mock)","module mở WebSocket tới backend thật — đã chặn (renderUI chỉ cho phép gọi thật qua tAPI SP đã xác nhận an toàn, không qua WebSocket)":"module opened a WebSocket to the real backend; blocked (only verified-safe tAPI SPs run live)","Vue chưa mount sau 15s — trang chưa init xong (xem JS error / asset hỏng / API treo bên dưới)":"Vue not mounted after 15s; page init unfinished (see JS errors / broken assets / hung APIs below)"," CẬP NHẬT":" UPDATED"," tải mới":" fetched"," gọi thật":" live"," tự sinh":" generated"," (API không-mock: ":" (unmocked API: ","trang load xong sau ":"page loaded in ",': không tìm thấy phần tử có text "':': no element with text "','" trên DOM':'" in the DOM'," phần tử — dùng #":" elements; using #",'CONFIRM → bấm "':'CONFIRM → click "',"CONFIRM → bấm nút cuối (không nhận diện được label)":"CONFIRM → click the last button (label not recognized)","CONFIRM → bấm nút đầu (không nhận diện được label)":"CONFIRM → click the first button (label not recognized)","global CALL không tồn tại trong runtime này":"global CALL missing in this runtime","chồng lấn":"overlap","dính sát (gap ":"touching (gap ","canvas trong ":"canvas in ","✘ <body> không render được phần tử nào (HTMLOnly — body.html LÀ cả trang)":"✘ <body> rendered nothing (HTMLOnly: body.html IS the page)","✘ #fastProjectAPP rỗng — controls không render":"✘ #fastProjectAPP empty: controls not rendered"," (CÓ file toàn cục nạp thiếu — xem mục MÔI TRƯỜNG THIẾU FILE trước khi nghi module.json)":" (global files FAILED to load; see MISSING ENV FILES before suspecting module.json)"," (kiểm tra pageerror / module.json)":" (check pageerror / module.json)","⚠ trang tràn ngang viewport ":"⚠ page overflows the viewport ","px — phần tử tràn: ":"px; overflowing: ","(không xác định)":"(unknown)","⚠ text nghi vấn hiển thị trên trang: ":"⚠ suspect text on page: "," (binding thiếu data?)":" (binding missing data?)","⚠ ảnh vỡ (load fail nhưng đang chiếm chỗ): ":"⚠ broken images (failed but taking space): ","⚠ canvas chart 0-size — chart init khi container chưa có kích thước: ":"⚠ 0-size chart canvas (init before the container had a size): ","⚠ text dính/chồng lấn (thẩm mỹ — PHẢI sửa, không được bỏ qua): ":"⚠ text collisions (visual; MUST fix): ","RENDER (browser thật): ":"RENDER (real browser): "," · HTMLOnly=true (KHÔNG nhúng framework/projectdata — như production)":" · HTMLOnly=true (no framework/projectdata, like production)","Origin trang: ":"Page origin: ","Import (framework ∪ project ∪ module, sort theo FileSort/FileName): ":"Imports (framework ∪ project ∪ module, by FileSort/FileName): "," nạp được":" loaded"," KHÔNG nạp được":" NOT loaded","── ⛔ MÔI TRƯỜNG THIẾU FILE (đọc TRƯỚC khi sửa module) ──":"── ⛔ MISSING ENV FILES (read BEFORE fixing the module) ──","Các file dưới đây không nạp được vào trang test. Hàm/biến/style toàn cục do chúng":"These files did not load. Their global functions/vars/styles","định nghĩa sẽ không tồn tại → lỗi JS và lỗi hiển thị bên dưới CÓ THỂ chỉ là hệ quả,":"are missing, so JS and display errors below MAY only be consequences,","không phải lỗi của module. Khắc phục phần này trước rồi render lại.":"not module faults. Fix this first, then render again.","── ⛔ ASSET CŨ, KHÔNG XÁC MINH ĐƯỢC (đọc TRƯỚC khi kết luận về platform) ──":"── ⛔ STALE, UNVERIFIED ASSETS (read BEFORE blaming the platform) ──","Không hỏi được server xem các file dưới đây còn đúng không, nên trang test đang chạy":"The server could not confirm these files, so the page ran",'bằng bản trên đĩa. Mọi kết luận kiểu "thư viện X có bug" rút ra từ lượt render này':'on disk copies. Any "library X has a bug" conclusion from this render',"đều KHÔNG dùng được — bản thật trên CDN có thể đã khác. Nối mạng rồi render lại,":"is NOT valid; the CDN copy may differ. Go online and render again,","hoặc ép tải mới bằng refreshAssets: true.":"or force a fetch with --refresh-assets.","── ℹ️ ASSET PLATFORM ĐÃ ĐỔI so với lượt render trước ──":"── ℹ️ PLATFORM ASSETS CHANGED since the previous render ──","Hành vi đổi mà module.json không đụng tới có thể đến từ đây, không phải từ module.":"Behavior changes not caused by module.json may come from here.","── STATE CUỐI (vueData) ──":"── FINAL STATE (vueData) ──"," (đính kèm bên dưới)":" (attached below)"," (quá lớn để inline — dùng Read tool để xem)":" (too large to inline; open it with the Read tool)","Dialog đang mở — chụp riêng: ":"Open dialog screenshot: "," API call (tất cả mock) · ":" API call (all mocked) · "," lúc init) · ":" at init) · "," · dialog đang mở":" · open dialog"},qh=Object.entries(Wh).sort(([e],[t])=>t.length-e.length);function Ve(e){let t=e;for(let[n,o]of Hh)t=t.replace(n,o);for(let[n,o]of qh)t=t.replaceAll(n,o);return t.replace(/`(project_sync|module_get|file_import_list)`/g,(n,o)=>`\`${Vh[o]}\``)}async function bn(e){let t=Kr(await Vn(e));return t?[t]:[]}async function pl(e,t){let n=await zo(e);if(n.errors.length===0&&n.warnings.length===0)return[];let o=Ve(Xo(n));if(n.errors.length>0&&Zt())throw new Se(`[FUI_STRICT] ${t} blocked: module.json has semantic errors; fix them and retry
113
113
 
114
- ${o}`,"hard","error");return["--- module validate ---",o]}async function Bt(e){let t=await H(pt(e,"_moduleInfo.json"),null),n=t?.ProjectID&&t?.ModuleID?`fui module get -p ${t.ProjectID} -m ${t.ModuleID}`:"fui module get";return Mt(e,"local module",n,"_moduleInfo.json")}async function Ds(e){let t=await Bt(e),n=await Ko(e);return{notes:[...t?[t]:[],...await bn(e)],data:`${Ve(Go(n))}
115
- `,errorCount:n.errors.length}}async function ll(e,t){let o=await(await ce(e)).previewStagedModuleDir(t),s=await e.session.createApproval({projectId:o.moduleInfo.ProjectID,moduleId:o.moduleInfo.ModuleID,moduleDir:t,contentHash:await Wo(t)}),r=await Bt(t),i=await Ko(t);return{token:s.token,notes:[...r?[r]:[],`preview of ${o.moduleInfo.ProjectID}/${o.moduleInfo.ModuleID} (${t})`,`remote: ${o.remoteSummary.join("; ")}`,"changes that would be pushed:",...o.changes.length===0?[" none"]:o.changes.map((a)=>` ${a}`),"--- module validate ---",Ve(Go(i)),`approval token: ${s.token}`,"ask the user; only after explicit approval run: fui module publish html --approval <token>"]}}async function ul(e,t,n){let o=await Vt(e.session,"module publish html"),s=(c)=>new Se(c,"hard","error");if(!n)throw s("missing --approval: run fui module preview, show it to the user, and publish only after explicit approval");let r=await e.session.getApproval();if(!r)throw s("no pending publish approval; run fui module preview first");if(r.token!==n)throw s("invalid approval token; run fui module preview again and use the latest token after approval");if(r.moduleDir!==t)throw s(`approval token is for ${r.moduleDir}, not ${t}`);if(await Wo(t)!==r.contentHash)throw s("files changed since the preview (module.json, header.html, body.html, script.js, style.css or components/*.vue); run fui module preview again and get a fresh approval");let i=await cl(t,"module publish html"),a=await(await ce(e)).publishStagedModuleDir(t);return await e.session.clearApproval(),{notes:[...o,`ok published ${a.ProjectID}/${a.ModuleID} (${t})`,...i,...await bn(t)]}}async function Xo(e,t){let n=await Vt(e.session,"module publish json"),o=await L(pt(t.moduleDir,"module.json"));if(!o.trim())throw new m("local module.json is empty or missing");try{JSON.parse(o)}catch(r){throw new m(`module.json has invalid JSON syntax: ${r.message}`)}if(await rn(t.moduleDir,"module.json",o))return{notes:[...n,`skip module.json unchanged since the last publish/sync of ${t.projectId}/${t.moduleId}`]};let s=await cl(t.moduleDir,"module publish json");return await(await e.api()).updateModuleUI(t.projectId,t.moduleId,o),await Pe(t.moduleDir,["module.json"]).catch(()=>{return}),{notes:[...n,`ok published module.json to ${t.projectId}/${t.moduleId}`,...s,...await bn(t.moduleDir)]}}async function Yo(e,t,n){let o=`module publish ${n==="script.js"?"script":"css"}`,s=await Vt(e.session,o),r=await ys(pt(t.moduleDir,n),n);if(await rn(t.moduleDir,n,r))return{notes:[...s,`skip ${n} unchanged since the last publish/sync of ${t.projectId}/${t.moduleId}`]};let i=await e.api();if(n==="script.js")await i.updateModuleScript(t.projectId,t.moduleId,r);else await i.updateModuleCSS(t.projectId,t.moduleId,r);return await Pe(t.moduleDir,[n]).catch(()=>{return}),{notes:[...s,`ok published ${n} to ${t.projectId}/${t.moduleId}`,...await bn(t.moduleDir)]}}function yn(e){return e.moduleId?`module (${e.moduleId})`:"project (global, loaded in all modules)"}async function dl(e,t){let n=await L(pt(e.baseDir,"components",`${t}.vue`));if(!n.trim())throw new m(`${t}.vue not found in ${e.baseDir}/components/${e.moduleId?"":"; for module scope pass -m <module>"}`);return n}async function zo(e){return H(pt(e,"components","_components.json"),[])}async function pl(e,t){let{count:n,removed:o}=await(await ce(e)).syncComponentsLocal(t.projectId,t.moduleId);return{notes:[`${n} component(s) in ${t.moduleId?`module ${t.moduleId}`:`project ${t.projectId}`}; _components.json refreshed`,...o.length>0?[`removed orphaned local files: ${o.join(", ")}`]:[]]}}async function Qo(e,t,n){let o=await Vt(e.session,"component new"),s=await dl(t,n),r=await ce(e);await r.syncComponentsLocal(t.projectId,t.moduleId);let i=(await zo(t.baseDir)).find((h)=>h.comName===n);if(i)throw new m(`component "${n}" already exists in ${yn(t)} (ComID ${i.comID}); overwrite it: fui component update ${n}, or pick another name`);let{template:a,script:c,style:l}=It(s);await r.api.updateComponent({ProjectID:t.projectId,ModuleID:t.moduleId,ComName:n,ComTemplate:a,ComScript:c,ComCSS:l});let{count:u,removed:d}=await r.syncComponentsLocal(t.projectId,t.moduleId),p=(await zo(t.baseDir)).find((h)=>h.comName===n);return await Pe(t.baseDir,[ke(pt("components",`${n}.vue`))]).catch(()=>{return}),{notes:[...o,`ok created component ${n} in ${yn(t)} (ComID ${p?.comID??"not visible yet; run fui component list"})`,`${u} component(s) synced${d.length>0?`; removed orphaned: ${d.join(", ")}`:""}`,...await bn(t.baseDir)]}}async function Zo(e,t,n){let o=await Vt(e.session,"component update"),s=await dl(t,n),r=(await zo(t.baseDir)).find((h)=>h.comName===n)?.comID;if(r===void 0)throw new m(`component "${n}" is not in _components.json of ${yn(t)}, so there is no ComID to overwrite. new component: fui component new ${n}; stale cache: fui component list, then retry`);let i=ke(pt("components",`${n}.vue`));if(await rn(t.baseDir,i,s))return{notes:[...o,`skip ${n}.vue unchanged since the last publish/sync`]};let{template:a,script:c,style:l}=It(s),u=await ce(e);await u.api.updateComponent({ProjectID:t.projectId,ModuleID:t.moduleId,ComID:r,ComName:n,ComTemplate:a,ComScript:c,ComCSS:l});let{count:d,removed:p}=await u.syncComponentsLocal(t.projectId,t.moduleId);return await Pe(t.baseDir,[i]).catch(()=>{return}),{notes:[...o,`ok updated component ${n} in ${yn(t)} (ComID ${r})`,`${d} component(s) synced${p.length>0?`; removed orphaned: ${p.join(", ")}`:""}`,...await bn(t.baseDir)]}}async function ml(e,t,n){let o=(await zo(t.baseDir)).find((a)=>a.comName===n);if(!o)throw new m(`component "${n}" not in the local _components.json of ${yn(t)}; run fui component list first`);let s=await ce(e);await s.api.updateComponent({ProjectID:t.projectId,...t.moduleId?{ModuleID:t.moduleId}:{},ComID:o.comID,ComName:"",ComTemplate:"",ComScript:""});let{count:r,removed:i}=await s.syncComponentsLocal(t.projectId,t.moduleId);return{notes:[`ok deleted component ${n} from ${yn(t)}; ${r} remain`,...i.length>0?[`local files removed (archived): ${i.join(", ")}`]:[]]}}async function hl(e,t){let{id:n,...o}=t,s=await ce(e),r=await s.api.editProject(o),i=await s.projectInfoRefreshNote(gl(o,r));return{notes:["ok project created",...n!==void 0?[`warning: projectData had id="${String(n)}"; removed to create a new project (use fui project update to edit)`]:[],...i?[i]:[]],data:`${JSON.stringify(r)}
116
- `}}async function fl(e,t){let n=t.id;if(n===void 0||n===null||n==="")throw new m("projectData.id is required to update (set it to the ProjectID); to create a project use fui project new");let o=await ce(e),s=await o.api.editProject(t),r=await o.projectInfoRefreshNote(gl(t,s));return{notes:["ok project updated",...r?[r]:[]],data:`${JSON.stringify(s)}
117
- `}}function gl(e,t){let n=Ht(e)?e.id??e.ProjectID:void 0;if(typeof n==="string"&&n.trim())return n;let o=Ht(t)?t.data:void 0,s=Array.isArray(o)?o[0]:o,r=Ht(s)?s.ProjectID??s.id:void 0;return typeof r==="string"&&r.trim()?r:null}async function er(e,t){let n=await Vt(e.session,"project publish-config"),o=e.paths.projectDir(t),s=await L(pt(o,"project.json"));if(!s.trim())throw new m(`cannot read ${t}/project.json (missing or empty); run fui project sync -p ${t}, edit it, then publish`);let r;try{r=JSON.parse(s)}catch(l){throw new m(`${t}/project.json has invalid JSON: ${l.message}; nothing published`)}let{config:i,wasEnveloped:a}=Is(r);if(await rn(o,"project.json",s))return{notes:[...n,`skip ${t}/project.json unchanged since the last publish/sync`]};let c=await(await e.api()).updateProjectData(t,i);return await Pe(o,["project.json"]).catch(()=>{return}),{notes:[...n,`ok published ${t}/project.json (${Object.keys(i).length} keys)`,...a?["the file is an API envelope { data: {...} }; the data layer was sent, the file is unchanged"]:[]],data:`${JSON.stringify(c)}
118
- `}}async function Fh(e,t){let n=await L(pt(e,"module.json"));if(!n.trim())throw new m(`module.json is empty or missing in ${e}`);try{return JSON.parse(n)}catch(o){throw new m(`module.json has a JSON syntax error: ${o.message}; fix it, ${t} needs valid JSON`)}}async function xs(e){let t=await Fh(e,"outline"),n=await Bt(e);return{notes:n?[n]:[],data:`Outline: ${e}
114
+ ${o}`,"hard","error");return["--- module validate ---",o]}async function Bt(e){let t=await H(pt(e,"_moduleInfo.json"),null),n=t?.ProjectID&&t?.ModuleID?`fui module get -p ${t.ProjectID} -m ${t.ModuleID}`:"fui module get";return Mt(e,"local module",n,"_moduleInfo.json")}async function _s(e){let t=await Bt(e),n=await zo(e);return{notes:[...t?[t]:[],...await bn(e)],data:`${Ve(Xo(n))}
115
+ `,errorCount:n.errors.length}}async function ml(e,t){let o=await(await ce(e)).previewStagedModuleDir(t),s=await e.session.createApproval({projectId:o.moduleInfo.ProjectID,moduleId:o.moduleInfo.ModuleID,moduleDir:t,contentHash:await Bo(t)}),r=await Bt(t),i=await zo(t);return{token:s.token,notes:[...r?[r]:[],`preview of ${o.moduleInfo.ProjectID}/${o.moduleInfo.ModuleID} (${t})`,`remote: ${o.remoteSummary.join("; ")}`,"changes that would be pushed:",...o.changes.length===0?[" none"]:o.changes.map((a)=>` ${a}`),"--- module validate ---",Ve(Xo(i)),`approval token: ${s.token}`,"ask the user; only after explicit approval run: fui module publish html --approval <token>"]}}async function hl(e,t,n){let o=await Vt(e.session,"module publish html"),s=(c)=>new Se(c,"hard","error");if(!n)throw s("missing --approval: run fui module preview, show it to the user, and publish only after explicit approval");let r=await e.session.getApproval();if(!r)throw s("no pending publish approval; run fui module preview first");if(r.token!==n)throw s("invalid approval token; run fui module preview again and use the latest token after approval");if(r.moduleDir!==t)throw s(`approval token is for ${r.moduleDir}, not ${t}`);if(await Bo(t)!==r.contentHash)throw s("files changed since the preview (module.json, header.html, body.html, script.js, style.css or components/*.vue); run fui module preview again and get a fresh approval");let i=await pl(t,"module publish html"),a=await(await ce(e)).publishStagedModuleDir(t);return await e.session.clearApproval(),{notes:[...o,`ok published ${a.ProjectID}/${a.ModuleID} (${t})`,...i,...await bn(t)]}}async function Qo(e,t){let n=await Vt(e.session,"module publish json"),o=await F(pt(t.moduleDir,"module.json"));if(!o.trim())throw new h("local module.json is empty or missing");try{JSON.parse(o)}catch(r){throw new h(`module.json has invalid JSON syntax: ${r.message}`)}if(await rn(t.moduleDir,"module.json",o))return{notes:[...n,`skip module.json unchanged since the last publish/sync of ${t.projectId}/${t.moduleId}`]};let s=await pl(t.moduleDir,"module publish json");return await(await e.api()).updateModuleUI(t.projectId,t.moduleId,o),await Ee(t.moduleDir,["module.json"]).catch(()=>{return}),{notes:[...n,`ok published module.json to ${t.projectId}/${t.moduleId}`,...s,...await bn(t.moduleDir)]}}async function Zo(e,t,n){let o=`module publish ${n==="script.js"?"script":"css"}`,s=await Vt(e.session,o),r=await vs(pt(t.moduleDir,n),n);if(await rn(t.moduleDir,n,r))return{notes:[...s,`skip ${n} unchanged since the last publish/sync of ${t.projectId}/${t.moduleId}`]};let i=await e.api();if(n==="script.js")await i.updateModuleScript(t.projectId,t.moduleId,r);else await i.updateModuleCSS(t.projectId,t.moduleId,r);return await Ee(t.moduleDir,[n]).catch(()=>{return}),{notes:[...s,`ok published ${n} to ${t.projectId}/${t.moduleId}`,...await bn(t.moduleDir)]}}function yn(e){return e.moduleId?`module (${e.moduleId})`:"project (global, loaded in all modules)"}async function fl(e,t){let n=await F(pt(e.baseDir,"components",`${t}.vue`));if(!n.trim())throw new h(`${t}.vue not found in ${e.baseDir}/components/${e.moduleId?"":"; for module scope pass -m <module>"}`);return n}async function Yo(e){return H(pt(e,"components","_components.json"),[])}async function gl(e,t){let{count:n,removed:o}=await(await ce(e)).syncComponentsLocal(t.projectId,t.moduleId);return{notes:[`${n} component(s) in ${t.moduleId?`module ${t.moduleId}`:`project ${t.projectId}`}; _components.json refreshed`,...o.length>0?[`removed orphaned local files: ${o.join(", ")}`]:[]]}}async function er(e,t,n){let o=await Vt(e.session,"component new"),s=await fl(t,n),r=await ce(e);await r.syncComponentsLocal(t.projectId,t.moduleId);let i=(await Yo(t.baseDir)).find((m)=>m.comName===n);if(i)throw new h(`component "${n}" already exists in ${yn(t)} (ComID ${i.comID}); overwrite it: fui component update ${n}, or pick another name`);let{template:a,script:c,style:l}=It(s);await r.api.updateComponent({ProjectID:t.projectId,ModuleID:t.moduleId,ComName:n,ComTemplate:a,ComScript:c,ComCSS:l});let{count:u,removed:d}=await r.syncComponentsLocal(t.projectId,t.moduleId),p=(await Yo(t.baseDir)).find((m)=>m.comName===n);return await Ee(t.baseDir,[ke(pt("components",`${n}.vue`))]).catch(()=>{return}),{notes:[...o,`ok created component ${n} in ${yn(t)} (ComID ${p?.comID??"not visible yet; run fui component list"})`,`${u} component(s) synced${d.length>0?`; removed orphaned: ${d.join(", ")}`:""}`,...await bn(t.baseDir)]}}async function tr(e,t,n){let o=await Vt(e.session,"component update"),s=await fl(t,n),r=(await Yo(t.baseDir)).find((m)=>m.comName===n)?.comID;if(r===void 0)throw new h(`component "${n}" is not in _components.json of ${yn(t)}, so there is no ComID to overwrite. new component: fui component new ${n}; stale cache: fui component list, then retry`);let i=ke(pt("components",`${n}.vue`));if(await rn(t.baseDir,i,s))return{notes:[...o,`skip ${n}.vue unchanged since the last publish/sync`]};let{template:a,script:c,style:l}=It(s),u=await ce(e);await u.api.updateComponent({ProjectID:t.projectId,ModuleID:t.moduleId,ComID:r,ComName:n,ComTemplate:a,ComScript:c,ComCSS:l});let{count:d,removed:p}=await u.syncComponentsLocal(t.projectId,t.moduleId);return await Ee(t.baseDir,[i]).catch(()=>{return}),{notes:[...o,`ok updated component ${n} in ${yn(t)} (ComID ${r})`,`${d} component(s) synced${p.length>0?`; removed orphaned: ${p.join(", ")}`:""}`,...await bn(t.baseDir)]}}async function wl(e,t,n){let o=(await Yo(t.baseDir)).find((a)=>a.comName===n);if(!o)throw new h(`component "${n}" not in the local _components.json of ${yn(t)}; run fui component list first`);let s=await ce(e);await s.api.updateComponent({ProjectID:t.projectId,...t.moduleId?{ModuleID:t.moduleId}:{},ComID:o.comID,ComName:"",ComTemplate:"",ComScript:""});let{count:r,removed:i}=await s.syncComponentsLocal(t.projectId,t.moduleId);return{notes:[`ok deleted component ${n} from ${yn(t)}; ${r} remain`,...i.length>0?[`local files removed (archived): ${i.join(", ")}`]:[]]}}async function yl(e,t){let{id:n,...o}=t,s=await ce(e),r=await s.api.editProject(o),i=await s.projectInfoRefreshNote(kl(o,r));return{notes:["ok project created",...n!==void 0?[`warning: projectData had id="${String(n)}"; removed to create a new project (use fui project update to edit)`]:[],...i?[i]:[]],data:`${JSON.stringify(r)}
116
+ `}}async function bl(e,t){let n=t.id;if(n===void 0||n===null||n==="")throw new h("projectData.id is required to update (set it to the ProjectID); to create a project use fui project new");let o=await ce(e),s=await o.api.editProject(t),r=await o.projectInfoRefreshNote(kl(t,s));return{notes:["ok project updated",...r?[r]:[]],data:`${JSON.stringify(s)}
117
+ `}}function kl(e,t){let n=Ht(e)?e.id??e.ProjectID:void 0;if(typeof n==="string"&&n.trim())return n;let o=Ht(t)?t.data:void 0,s=Array.isArray(o)?o[0]:o,r=Ht(s)?s.ProjectID??s.id:void 0;return typeof r==="string"&&r.trim()?r:null}async function nr(e,t){let n=await Vt(e.session,"project publish-config"),o=e.paths.projectDir(t),s=await F(pt(o,"project.json"));if(!s.trim())throw new h(`cannot read ${t}/project.json (missing or empty); run fui project sync -p ${t}, edit it, then publish`);let r;try{r=JSON.parse(s)}catch(l){throw new h(`${t}/project.json has invalid JSON: ${l.message}; nothing published`)}let{config:i,wasEnveloped:a}=Ts(r);if(await rn(o,"project.json",s))return{notes:[...n,`skip ${t}/project.json unchanged since the last publish/sync`]};let c=await(await e.api()).updateProjectData(t,i);return await Ee(o,["project.json"]).catch(()=>{return}),{notes:[...n,`ok published ${t}/project.json (${Object.keys(i).length} keys)`,...a?["the file is an API envelope { data: {...} }; the data layer was sent, the file is unchanged"]:[]],data:`${JSON.stringify(c)}
118
+ `}}async function Bh(e,t){let n=await F(pt(e,"module.json"));if(!n.trim())throw new h(`module.json is empty or missing in ${e}`);try{return JSON.parse(n)}catch(o){throw new h(`module.json has a JSON syntax error: ${o.message}; fix it, ${t} needs valid JSON`)}}async function Ms(e){let t=await Bh(e,"outline"),n=await Bt(e);return{notes:n?[n]:[],data:`Outline: ${e}
119
119
 
120
- ${Ve(al(t))}
121
- `}}var Lh=["✘","✔","❌","⚠","❗","\uD83D\uDEA8","Stats:","Screenshot:","screenshot:","Full-page:"],Uh=["Simulate:","Render UI:","live ","skip ","cache ","gen "];function Os(e){let t=e.split(`
122
- `).filter((o)=>o.trim()),n=t.filter((o)=>Lh.some((s)=>o.includes(s))||Uh.some((s)=>o.startsWith(s)));return{text:`${n.join(`
120
+ ${Ve(dl(t))}
121
+ `}}var Jh=["✘","✔","❌","⚠","❗","\uD83D\uDEA8","Stats:","Screenshot:","screenshot:","Full-page:"],Kh=["Simulate:","Render UI:","live ","skip ","cache ","gen "];function Ls(e){let t=e.split(`
122
+ `).filter((o)=>o.trim()),n=t.filter((o)=>Jh.some((s)=>o.includes(s))||Kh.some((s)=>o.startsWith(s)));return{text:`${n.join(`
123
123
  `)}
124
- `,kept:n.length,total:t.length}}import{join as Hh}from"node:path";function wl(e,t){let n=Sr(),o=Qt("sha1",`${t}
125
- ${e}`).slice(0,16);return Hh(n.replace(/\.json$/,""),"last-input",`${o}.json`)}async function _s(e,t){let n=await he(wl(e,t));if(!n)return null;try{let o=JSON.parse(n);return o&&typeof o==="object"&&!Array.isArray(o)?o.flags??null:null}catch{return null}}async function yl(e,t,n){await F(wl(e,t),JSON.stringify({moduleDir:e,savedAt:new Date().toISOString(),flags:n},null,2))}function Ms(e,t,n){let o={...e},s=[];for(let r of n){if(r.some((i)=>e[i]!==void 0))continue;for(let i of r)if(t[i]!==void 0)o[i]=t[i],s.push(i)}return{merged:o,reused:s}}import{basename as Ff}from"node:path";import{join as Vh}from"node:path";var Jt=8,tr=5000;function Wh(e){let t=e.split(/[?#]/)[0],n=t.split("/").filter(Boolean);return(n.length?n[n.length-1]:t).toLowerCase()}function kl(e,t,n){let o=new Map;for(let r of t){o.set(r.toLowerCase(),r);let i=Lt(r);if(i)o.set(i.fnName.toLowerCase(),r)}let s=[];for(let r of e){let i=o.get(Wh(r));if(i&&!n.has(i.toLowerCase()))s.push({mockKey:r,spName:i})}return s}function ro(e){let t=e.match(/^\/([A-Za-z0-9_-]+)\/(auth\/)?([A-Za-z0-9_]+)/);if(!t)return null;return{alias:t[1],isAuth:Boolean(t[2]),fnName:t[3]}}async function Fs(e){try{return JSON.parse(await G(e))}catch{return null}}function bl(e){if(!Ot(e))return[];let t=[];for(let n of e.procedures){let o=/^CREATE\s+(?:PROCEDURE|FUNCTION)\s+(\S+)/i.exec(n);if(o)t.push(o[1])}return t}function kn(e,t){let n=e.paths,o=(r,i,a)=>Vh(n.projectDir(r),"_testdata",i,`${a}.json`),s=async(r,i,a)=>{let c=await he(n.spFile(r,i,a));return c!==null&&c.trim()?c:null};return{async loadAllSchemaProcNames(r){let i=new Set;for(let a of Object.keys((await e.load(r)).connections))for(let c of bl(await Fs(n.schemaFile(r,a))))i.add(c);return[...i]},getVerifiedSpNames:()=>t.getVerifiedSpNames(),async getConnectionAlias(r,i){return jo(e,r,i)},async resolveApiSpName(r,i,a){let c=await Fs(n.schemaFile(r,i));if(!Ot(c))return null;let l=bl(c),u=[a,`spAPI_${a}`,`spAPI_AUTH_${a}`,`spAPIFILE_${a}`,`spAPIFILE_UPLOAD_${a}`];for(let d of u){let p=l.find((h)=>h.toLowerCase()===d.toLowerCase());if(p)return p}return null},async classifySpSafety(r,i,a){return xa(await s(r,i,a))},async callLiveApiSp(r,i,a,c,l="GET",u,d=tr){try{let{config:p}=await e.resolve({project:r,db:i}),h=`${Ee(p.apiDomain)}/${p.apiName??i}/${a?"auth/":""}${c}`,w={"content-type":"application/json"};if(!a&&p.userToken)w.authorization=`Bearer ${p.userToken}`;let y=await fetch(h,{method:l,headers:w,body:u!==void 0?JSON.stringify(u):void 0,signal:AbortSignal.timeout(d)}),v=await y.text(),N;try{N=JSON.parse(v)}catch{return{ok:!1,status:y.status,error:"Phản hồi không phải JSON"}}if(!y.ok)return{ok:!1,status:y.status,data:N,error:`HTTP ${y.status}`};return{ok:!0,status:y.status,data:N}}catch(p){return{ok:!1,error:p instanceof Error?p.message:String(p)}}},async readTestData(r,i,a){return await Fs(o(r,i,a))},async writeTestData(r,i,a,c,l){try{let u={source:c,savedAt:new Date().toISOString(),data:l};await F(o(r,i,a),JSON.stringify(u,null,2))}catch{}},async generateMockForLocalSp(r,i,a){let c=await s(r,i,a);return c===null?null:Da(c)}}}import{promises as Qe}from"node:fs";import ue from"node:path";import{homedir as qh}from"node:os";import{join as Us}from"node:path";var Ls=null;function Hs(e){Ls={...e,live:kn(e.store,e.session)}}function We(){if(!Ls){let e=process.env.FUI_WORKDIR||Us(qh(),vr),t=new On;Hs({store:new ct(new Ln(e)),session:t,api:bo,activeTarget:()=>t.getTarget()})}return Ls}var vn=()=>We().store.paths.root,$n=(e)=>Us(We().store.paths.root,it(e,"projectId")),Vs=(e,t)=>Us($n(e),"modules",it(t,"moduleId")),vl=()=>We().activeTarget();async function $l(e){return await(await We().api()).listFrameworkImportFiles(e)}async function Sl(e,t){let n=await We().store.resolve({project:e,db:t.db});return{alias:n.alias,config:{...n.config,apiName:n.alias},note:n.note}}var jl=(...e)=>We().live.getConnectionAlias(...e),Il=(...e)=>We().live.resolveApiSpName(...e),Cl=(...e)=>We().live.classifySpSafety(...e),Rl=(...e)=>We().live.readTestData(...e),Ws=(...e)=>We().live.writeTestData(...e),Tl=(...e)=>We().live.generateMockForLocalSp(...e);import{promises as mt}from"node:fs";import{createHash as ql}from"node:crypto";import{createRequire as ef}from"node:module";import J from"node:path";import{pathToFileURL as tf}from"node:url";import{promises as qs}from"node:fs";import Nl from"node:path";var Bh=86400000,El={V2:{styles:["https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900","/include/icons/mdi/materialdesignicons.min.css","/include/vuetify.min.css","/include/toast/vue-toast-notification.css","/include/jquery-confirm.min.css","/projectdefaultstyle.css"],scripts:["/include/jquery-3.6.0.min.js","/include/jquery-confirm.min.js","/include/lodash.4.17.min.js","/include/moment/moment-vi.min.js","/include/numeral.min.js","/include/vue@2.js","/include/vuetify.min.js","/include/toast/vue-toast-notification.js","/defaultfunction.js","/componentTable.js","/component.js","/fastproject.js"]},V3:{styles:["https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900","/include/icons/mdi/materialdesignicons.min.css","/Framework/V3/vuetify@3.7.min.css","/Framework/V3/vuetify-labs@3.7.min.css","/Framework/V3/vue-toast-notification@3.1.3.theme-bootstrap.css","/include/jquery-confirm.min.css","/Framework/V3/projectdefaultstyle.css"],scripts:["/include/jquery-3.6.0.min.js","/include/jquery-confirm.min.js","/include/lodash.4.17.min.js","/include/numeral.min.js","/Framework/V3/dayjs-vi.min.js","/Framework/V3/vue.global.min.3.5.js","/Framework/V3/vuetify@3.7.min.js","/Framework/V3/vuetify-labs@3.7.min.js","/Framework/V3/vue-toast-notification@3.1.3.js","/Framework/V3/defaultfunction-3.0.js","/Framework/V3/fastproject-3.0.js","/Framework/V3/component-3.0.js","/Framework/V3/componentTable-3.0.js"]}};function Jh(e){return Nl.join(vn(),"_cache","framework",`${e}.json`)}function Kh(e,t){let n=El[e]??El.V3,o=(s,r)=>s.map((i,a)=>({fileID:"",ProjectID:"",ModuleID:"",FileType:r,ContentType:"link",EditContent:0,FileName:/^https?:\/\//i.test(i)?i:`${t}${i}`,FileSort:a,CreateTime:"",CreateUser:"",UpdateTime:null,UpdateUser:null,Scope:"framework"}));return[...o(n.styles,".css"),...o(n.scripts,".js")]}async function Al(e,t){let n=Jh(e),o;try{o=JSON.parse(await qs.readFile(n,"utf-8"))}catch{}let s=o?.fetchedAt?Date.now()-Date.parse(o.fetchedAt):Number.POSITIVE_INFINITY;if(Array.isArray(o?.data)&&o.data.length>0&&s<Bh)return{deps:o.data,source:`cache (${o.data.length} file, tải lúc ${o.fetchedAt})`};try{let r=await $l(e);if(r.length===0)throw Error("API trả danh mục rỗng");try{await qs.mkdir(Nl.dirname(n),{recursive:!0}),await qs.writeFile(n,JSON.stringify({fetchedAt:new Date().toISOString(),data:r},null,2),"utf-8")}catch{}return{deps:r,source:`API /fp/FUI_Dev_File/select {frameWork:"${e}"} (${r.length} file)`}}catch(r){if(Array.isArray(o?.data)&&o.data.length>0)return{deps:o.data,source:`cache CŨ (${o.data.length} file, tải lúc ${o.fetchedAt})`,warning:`Không gọi được API danh mục framework ${e} (${r.message}) — dùng cache cũ trên đĩa.`};let i=Kh(e,t);return{deps:i,source:`danh sách dự phòng hardcode (${i.length} file)`,warning:`Không gọi được API danh mục framework ${e} (${r.message}) và chưa có cache — `+"dùng danh sách hardcode trong renderAssets.ts. Danh sách này có thể đã lệch với server."}}}function Gh(e){if(e===".css")return"css";if(e===".js")return"js";return}function Dl(e){return[...e].sort((t,n)=>t.sort-n.sort||(t.name<n.name?-1:t.name>n.name?1:0))}function xl(e,t){let n=Gh(e.FileType);if(!n)return;let o=(e.FileName??"").trim();if(!o)return;return{label:`[framework] ${o}`,scope:"framework",kind:n,sort:typeof e.FileSort==="number"?e.FileSort:0,name:o,fileID:e.fileID,contentType:e.ContentType,domain:t.replace(/^https?:\/\//,""),fixHint:"file_import_list"}}function zh(){return`render-${Date.now().toString(36)}`}async function Pl(e){try{let t=await fetch(e,{headers:{Accept:"*/*","Cache-Control":"no-cache"}});if(!t.ok)return{ok:!1,reason:`HTTP ${t.status} ${t.statusText}`};return{ok:!0,text:await t.text()}}catch(t){return{ok:!1,reason:t.message}}}async function Ol(e,t){let n=t.replace(/^https?:\/\//,"").replace(/\/+$/,""),o=zh(),s=`https://${n}/_aset/fp/auth/projectdata/${e}@${n.replace(/\//g,"")}/${o}.js`,r=`https://${n}/_aset/fp/auth/StyleCSS/${e}/${o}.css`,i=[],a=[],[c,l]=await Promise.all([Pl(s),Pl(r)]),u;if(c.ok)if(/\$projectData\s*=/.test(c.text))u=c.text,i.push(`projectdata.js ← ${s} (${c.text.length} bytes)`);else a.push(`projectdata.js tải về từ ${s} không chứa "$projectData =" (${c.text.length} bytes) — `+"không dùng, quay về dựng $projectData + component project từ workspace local.");else a.push(`Không tải được ${s}: ${c.reason} — quay về dựng từ workspace local.`);let d;if(l.ok)if(/<html|<!DOCTYPE/i.test(l.text.slice(0,200)))a.push(`StyleCSS cấp project tải về từ ${r} là HTML chứ không phải CSS — bỏ qua.`);else if(l.text.trim())d=l.text,i.push(`StyleCSS project ← ${r} (${l.text.length} bytes)`);else i.push(`StyleCSS project rỗng (${r}) — project chưa có component nào khai style.`);else a.push(`Không tải được ${r}: ${l.reason} — CSS component cấp project sẽ dựng từ local.`);return{js:u,css:d,jsUrl:s,cssUrl:r,notes:i,problems:a}}function Bs(e,t){let n=[],o=new RegExp(`<${t}(\\s[^>]*)?>`,"gi"),s=new RegExp(`</${t}>`,"gi"),r=0;while(r<e.length){o.lastIndex=r;let i=o.exec(e);if(!i)break;let a=1,c=o.lastIndex;while(a>0){s.lastIndex=c;let l=s.exec(e);if(!l){r=e.length;break}o.lastIndex=c;let u=o.exec(e);if(u&&u.index<l.index){a+=1,c=o.lastIndex;continue}if(a-=1,c=s.lastIndex,a===0)n.push(e.slice(i.index+i[0].length,l.index)),r=s.lastIndex}}return n}function Xh(e){return e.replace(/[\r\n\t]+/g," ").replace(/\\/g,"\\\\").replace(/`/g,"\\`").replace(/\$\{/g,"\\${")}function Yh(e){let t=/export\s+default\s*\{([\s\S]*)\}\s*;?\s*$/.exec(e.trim());return t?t[1].trim():void 0}function _l(e,t){let n=Bs(t,"template")[0]??"",o=Bs(t,"script")[0]??"",s=Bs(t,"style").map((c)=>c.trim()).filter(Boolean).join(`
126
- `);if(!n&&!o&&t.includes("Vue.component("))return{compiled:{name:e,script:t,css:s}};let r=Yh(o),i=Xh(n);if(r!==void 0)return{compiled:{name:e,script:`Vue.component('${e}',{template:\`${i}\`,${r}})`,css:s}};let a=/module\.exports\s*=/.test(o)?o.replace(/module\.exports\s*=/,"return "):o.trim()?o:"return {};";return{compiled:{name:e,script:`;(() => {
124
+ `,kept:n.length,total:t.length}}import{join as Gh}from"node:path";function vl(e,t){let n=Ir(),o=Qt("sha1",`${t}
125
+ ${e}`).slice(0,16);return Gh(n.replace(/\.json$/,""),"last-input",`${o}.json`)}async function Fs(e,t){let n=await he(vl(e,t));if(!n)return null;try{let o=JSON.parse(n);return o&&typeof o==="object"&&!Array.isArray(o)?o.flags??null:null}catch{return null}}async function $l(e,t,n){await L(vl(e,t),JSON.stringify({moduleDir:e,savedAt:new Date().toISOString(),flags:n},null,2))}function Us(e,t,n){let o={...e},s=[];for(let r of n){if(r.some((i)=>e[i]!==void 0))continue;for(let i of r)if(t[i]!==void 0)o[i]=t[i],s.push(i)}return{merged:o,reused:s}}import{basename as Bf}from"node:path";import{join as zh}from"node:path";var Jt=8,or=5000;function Xh(e){let t=e.split(/[?#]/)[0],n=t.split("/").filter(Boolean);return(n.length?n[n.length-1]:t).toLowerCase()}function jl(e,t,n){let o=new Map;for(let r of t){o.set(r.toLowerCase(),r);let i=Ft(r);if(i)o.set(i.fnName.toLowerCase(),r)}let s=[];for(let r of e){let i=o.get(Xh(r));if(i&&!n.has(i.toLowerCase()))s.push({mockKey:r,spName:i})}return s}function ro(e){let t=e.match(/^\/([A-Za-z0-9_-]+)\/(auth\/)?([A-Za-z0-9_]+)/);if(!t)return null;return{alias:t[1],isAuth:Boolean(t[2]),fnName:t[3]}}async function Hs(e){try{return JSON.parse(await G(e))}catch{return null}}function Sl(e){if(!Ot(e))return[];let t=[];for(let n of e.procedures){let o=/^CREATE\s+(?:PROCEDURE|FUNCTION)\s+(\S+)/i.exec(n);if(o)t.push(o[1])}return t}function kn(e,t){let n=e.paths,o=(r,i,a)=>zh(n.projectDir(r),"_testdata",i,`${a}.json`),s=async(r,i,a)=>{let c=await he(n.spFile(r,i,a));return c!==null&&c.trim()?c:null};return{async loadAllSchemaProcNames(r){let i=new Set;for(let a of Object.keys((await e.load(r)).connections))for(let c of Sl(await Hs(n.schemaFile(r,a))))i.add(c);return[...i]},getVerifiedSpNames:()=>t.getVerifiedSpNames(),async getConnectionAlias(r,i){return Io(e,r,i)},async resolveApiSpName(r,i,a){let c=await Hs(n.schemaFile(r,i));if(!Ot(c))return null;let l=Sl(c),u=[a,`spAPI_${a}`,`spAPI_AUTH_${a}`,`spAPIFILE_${a}`,`spAPIFILE_UPLOAD_${a}`];for(let d of u){let p=l.find((m)=>m.toLowerCase()===d.toLowerCase());if(p)return p}return null},async classifySpSafety(r,i,a){return La(await s(r,i,a))},async callLiveApiSp(r,i,a,c,l="GET",u,d=or){try{let{config:p}=await e.resolve({project:r,db:i}),m=`${Pe(p.apiDomain)}/${p.apiName??i}/${a?"auth/":""}${c}`,w={"content-type":"application/json"};if(!a&&p.userToken)w.authorization=`Bearer ${p.userToken}`;let y=await fetch(m,{method:l,headers:w,body:u!==void 0?JSON.stringify(u):void 0,signal:AbortSignal.timeout(d)}),v=await y.text(),A;try{A=JSON.parse(v)}catch{return{ok:!1,status:y.status,error:"Phản hồi không phải JSON"}}if(!y.ok)return{ok:!1,status:y.status,data:A,error:`HTTP ${y.status}`};return{ok:!0,status:y.status,data:A}}catch(p){return{ok:!1,error:p instanceof Error?p.message:String(p)}}},async readTestData(r,i,a){return await Hs(o(r,i,a))},async writeTestData(r,i,a,c,l){try{let u={source:c,savedAt:new Date().toISOString(),data:l};await L(o(r,i,a),JSON.stringify(u,null,2))}catch{}},async generateMockForLocalSp(r,i,a){let c=await s(r,i,a);return c===null?null:Ma(c)}}}import{promises as Qe}from"node:fs";import ue from"node:path";import{homedir as Yh}from"node:os";import{join as Ws}from"node:path";var Vs=null;function qs(e){Vs={...e,live:kn(e.store,e.session)}}function We(){if(!Vs){let e=process.env.FUI_WORKDIR||Ws(Yh(),Sr),t=new On;qs({store:new ct(new Fn(e)),session:t,api:ko,activeTarget:()=>t.getTarget()})}return Vs}var vn=()=>We().store.paths.root,$n=(e)=>Ws(We().store.paths.root,it(e,"projectId")),Bs=(e,t)=>Ws($n(e),"modules",it(t,"moduleId")),Il=()=>We().activeTarget();async function Cl(e){return await(await We().api()).listFrameworkImportFiles(e)}async function Rl(e,t){let n=await We().store.resolve({project:e,db:t.db});return{alias:n.alias,config:{...n.config,apiName:n.alias},note:n.note}}var Tl=(...e)=>We().live.getConnectionAlias(...e),Pl=(...e)=>We().live.resolveApiSpName(...e),El=(...e)=>We().live.classifySpSafety(...e),Al=(...e)=>We().live.readTestData(...e),Js=(...e)=>We().live.writeTestData(...e),Nl=(...e)=>We().live.generateMockForLocalSp(...e);import{promises as mt}from"node:fs";import{createHash as Gl}from"node:crypto";import{createRequire as cf}from"node:module";import J from"node:path";import{pathToFileURL as lf}from"node:url";import{promises as Ks}from"node:fs";import Ol from"node:path";var Qh=86400000,Dl={V2:{styles:["https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900","/include/icons/mdi/materialdesignicons.min.css","/include/vuetify.min.css","/include/toast/vue-toast-notification.css","/include/jquery-confirm.min.css","/projectdefaultstyle.css"],scripts:["/include/jquery-3.6.0.min.js","/include/jquery-confirm.min.js","/include/lodash.4.17.min.js","/include/moment/moment-vi.min.js","/include/numeral.min.js","/include/vue@2.js","/include/vuetify.min.js","/include/toast/vue-toast-notification.js","/defaultfunction.js","/componentTable.js","/component.js","/fastproject.js"]},V3:{styles:["https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900","/include/icons/mdi/materialdesignicons.min.css","/Framework/V3/vuetify@3.7.min.css","/Framework/V3/vuetify-labs@3.7.min.css","/Framework/V3/vue-toast-notification@3.1.3.theme-bootstrap.css","/include/jquery-confirm.min.css","/Framework/V3/projectdefaultstyle.css"],scripts:["/include/jquery-3.6.0.min.js","/include/jquery-confirm.min.js","/include/lodash.4.17.min.js","/include/numeral.min.js","/Framework/V3/dayjs-vi.min.js","/Framework/V3/vue.global.min.3.5.js","/Framework/V3/vuetify@3.7.min.js","/Framework/V3/vuetify-labs@3.7.min.js","/Framework/V3/vue-toast-notification@3.1.3.js","/Framework/V3/defaultfunction-3.0.js","/Framework/V3/fastproject-3.0.js","/Framework/V3/component-3.0.js","/Framework/V3/componentTable-3.0.js"]}};function Zh(e){return Ol.join(vn(),"_cache","framework",`${e}.json`)}function ef(e,t){let n=Dl[e]??Dl.V3,o=(s,r)=>s.map((i,a)=>({fileID:"",ProjectID:"",ModuleID:"",FileType:r,ContentType:"link",EditContent:0,FileName:/^https?:\/\//i.test(i)?i:`${t}${i}`,FileSort:a,CreateTime:"",CreateUser:"",UpdateTime:null,UpdateUser:null,Scope:"framework"}));return[...o(n.styles,".css"),...o(n.scripts,".js")]}async function _l(e,t){let n=Zh(e),o;try{o=JSON.parse(await Ks.readFile(n,"utf-8"))}catch{}let s=o?.fetchedAt?Date.now()-Date.parse(o.fetchedAt):Number.POSITIVE_INFINITY;if(Array.isArray(o?.data)&&o.data.length>0&&s<Qh)return{deps:o.data,source:`cache (${o.data.length} file, tải lúc ${o.fetchedAt})`};try{let r=await Cl(e);if(r.length===0)throw Error("API trả danh mục rỗng");try{await Ks.mkdir(Ol.dirname(n),{recursive:!0}),await Ks.writeFile(n,JSON.stringify({fetchedAt:new Date().toISOString(),data:r},null,2),"utf-8")}catch{}return{deps:r,source:`API /fp/FUI_Dev_File/select {frameWork:"${e}"} (${r.length} file)`}}catch(r){if(Array.isArray(o?.data)&&o.data.length>0)return{deps:o.data,source:`cache CŨ (${o.data.length} file, tải lúc ${o.fetchedAt})`,warning:`Không gọi được API danh mục framework ${e} (${r.message}) — dùng cache cũ trên đĩa.`};let i=ef(e,t);return{deps:i,source:`danh sách dự phòng hardcode (${i.length} file)`,warning:`Không gọi được API danh mục framework ${e} (${r.message}) và chưa có cache — `+"dùng danh sách hardcode trong renderAssets.ts. Danh sách này có thể đã lệch với server."}}}function tf(e){if(e===".css")return"css";if(e===".js")return"js";return}function Ml(e){return[...e].sort((t,n)=>t.sort-n.sort||(t.name<n.name?-1:t.name>n.name?1:0))}function Ll(e,t){let n=tf(e.FileType);if(!n)return;let o=(e.FileName??"").trim();if(!o)return;return{label:`[framework] ${o}`,scope:"framework",kind:n,sort:typeof e.FileSort==="number"?e.FileSort:0,name:o,fileID:e.fileID,contentType:e.ContentType,domain:t.replace(/^https?:\/\//,""),fixHint:"file_import_list"}}function nf(){return`render-${Date.now().toString(36)}`}async function xl(e){try{let t=await fetch(e,{headers:{Accept:"*/*","Cache-Control":"no-cache"}});if(!t.ok)return{ok:!1,reason:`HTTP ${t.status} ${t.statusText}`};return{ok:!0,text:await t.text()}}catch(t){return{ok:!1,reason:t.message}}}async function Fl(e,t){let n=t.replace(/^https?:\/\//,"").replace(/\/+$/,""),o=nf(),s=`https://${n}/_aset/fp/auth/projectdata/${e}@${n.replace(/\//g,"")}/${o}.js`,r=`https://${n}/_aset/fp/auth/StyleCSS/${e}/${o}.css`,i=[],a=[],[c,l]=await Promise.all([xl(s),xl(r)]),u;if(c.ok)if(/\$projectData\s*=/.test(c.text))u=c.text,i.push(`projectdata.js ← ${s} (${c.text.length} bytes)`);else a.push(`projectdata.js tải về từ ${s} không chứa "$projectData =" (${c.text.length} bytes) — `+"không dùng, quay về dựng $projectData + component project từ workspace local.");else a.push(`Không tải được ${s}: ${c.reason} — quay về dựng từ workspace local.`);let d;if(l.ok)if(/<html|<!DOCTYPE/i.test(l.text.slice(0,200)))a.push(`StyleCSS cấp project tải về từ ${r} là HTML chứ không phải CSS — bỏ qua.`);else if(l.text.trim())d=l.text,i.push(`StyleCSS project ← ${r} (${l.text.length} bytes)`);else i.push(`StyleCSS project rỗng (${r}) — project chưa có component nào khai style.`);else a.push(`Không tải được ${r}: ${l.reason} — CSS component cấp project sẽ dựng từ local.`);return{js:u,css:d,jsUrl:s,cssUrl:r,notes:i,problems:a}}function Gs(e,t){let n=[],o=new RegExp(`<${t}(\\s[^>]*)?>`,"gi"),s=new RegExp(`</${t}>`,"gi"),r=0;while(r<e.length){o.lastIndex=r;let i=o.exec(e);if(!i)break;let a=1,c=o.lastIndex;while(a>0){s.lastIndex=c;let l=s.exec(e);if(!l){r=e.length;break}o.lastIndex=c;let u=o.exec(e);if(u&&u.index<l.index){a+=1,c=o.lastIndex;continue}if(a-=1,c=s.lastIndex,a===0)n.push(e.slice(i.index+i[0].length,l.index)),r=s.lastIndex}}return n}function of(e){return e.replace(/[\r\n\t]+/g," ").replace(/\\/g,"\\\\").replace(/`/g,"\\`").replace(/\$\{/g,"\\${")}function rf(e){let t=/export\s+default\s*\{([\s\S]*)\}\s*;?\s*$/.exec(e.trim());return t?t[1].trim():void 0}function Ul(e,t){let n=Gs(t,"template")[0]??"",o=Gs(t,"script")[0]??"",s=Gs(t,"style").map((c)=>c.trim()).filter(Boolean).join(`
126
+ `);if(!n&&!o&&t.includes("Vue.component("))return{compiled:{name:e,script:t,css:s}};let r=rf(o),i=of(n);if(r!==void 0)return{compiled:{name:e,script:`Vue.component('${e}',{template:\`${i}\`,${r}})`,css:s}};let a=/module\.exports\s*=/.test(o)?o.replace(/module\.exports\s*=/,"return "):o.trim()?o:"return {};";return{compiled:{name:e,script:`;(() => {
127
127
  const component = (() => {
128
128
  ${a}
129
129
  })() || {};
130
130
  component.template = \`${i}\`;
131
131
  Vue.component(${JSON.stringify(e)}, component);
132
- })();`,css:s},warning:`component "${e}.vue" không có khuôn \`export default { … }\` — server biên dịch bằng cách `+"nối thẳng phần <script> vào `{template:`…`, …}` nên bản trên server có thể lỗi cú pháp. "+"renderUI đã bọc an toàn để chạy tiếp, nhưng nên sửa .vue về đúng khuôn."}}function Sn(e){return e.replace(/\s+/g," ").trim()}function Qh(e){let t="";for(let n=0;n<e.length;n+=1){let o=e[n];if(o==="\\"&&n+1<e.length){let s=e[n+1];if(s==="\\"||s==="`"||s==="$"){t+=s,n+=1;continue}}t+=o}return t}function Zh(e){let t=new Map,n=/Vue\.component\(\s*(['"])([^'"]+)\1\s*,\s*\{\s*template\s*:\s*`/g,o=[],s;while((s=n.exec(e))!==null)o.push({name:s[2],templateStart:n.lastIndex,matchStart:s.index});for(let r=0;r<o.length;r+=1){let{name:i,templateStart:a}=o[r],c=a,l=-1;while(c<e.length){let w=e[c];if(w==="\\"){c+=2;continue}if(w==="`"){l=c;break}c+=1}if(l===-1)continue;let u=Qh(e.slice(a,l)),d=r+1<o.length?o[r+1].matchStart:e.length,p,h=e.slice(l+1,d).trim();if(h.startsWith(",")){let w=h.replace(/[;\s]+$/,"");if(w.endsWith("})"))p=w.slice(1,w.length-2).trim()}t.set(i,{template:u,body:p})}return t}function Ml(e,t,n){let o={missing:[],changed:[],changedCss:[],unverifiable:[]},s=Zh(t),r=Sn(n);for(let i of e){if(i.parseError){o.unverifiable.push(`${i.name} (${i.parseError})`);continue}if(!new RegExp(`Vue\\.component\\(\\s*(['"])${i.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\1`).test(t)){o.missing.push(i.name);continue}let c=s.get(i.name);if(!c){o.unverifiable.push(`${i.name} (bundle server không theo khuôn ComponentBuild)`);continue}if(Sn(i.template??"")!==Sn(c.template)){o.changed.push(i.name);continue}if(c.body===void 0)o.unverifiable.push(`${i.name} (chỉ đối chiếu được template, không bóc được thân script)`);else if(Sn(i.script??"")!==Sn(c.body)){o.changed.push(i.name);continue}let l=Sn(i.css??"");if(l&&!r.includes(l))o.changedCss.push(i.name)}return o}function Js(e){if(e.length===0)return"";return`${e.map((t)=>t.script).join(`;
132
+ })();`,css:s},warning:`component "${e}.vue" không có khuôn \`export default { … }\` — server biên dịch bằng cách `+"nối thẳng phần <script> vào `{template:`…`, …}` nên bản trên server có thể lỗi cú pháp. "+"renderUI đã bọc an toàn để chạy tiếp, nhưng nên sửa .vue về đúng khuôn."}}function Sn(e){return e.replace(/\s+/g," ").trim()}function sf(e){let t="";for(let n=0;n<e.length;n+=1){let o=e[n];if(o==="\\"&&n+1<e.length){let s=e[n+1];if(s==="\\"||s==="`"||s==="$"){t+=s,n+=1;continue}}t+=o}return t}function af(e){let t=new Map,n=/Vue\.component\(\s*(['"])([^'"]+)\1\s*,\s*\{\s*template\s*:\s*`/g,o=[],s;while((s=n.exec(e))!==null)o.push({name:s[2],templateStart:n.lastIndex,matchStart:s.index});for(let r=0;r<o.length;r+=1){let{name:i,templateStart:a}=o[r],c=a,l=-1;while(c<e.length){let w=e[c];if(w==="\\"){c+=2;continue}if(w==="`"){l=c;break}c+=1}if(l===-1)continue;let u=sf(e.slice(a,l)),d=r+1<o.length?o[r+1].matchStart:e.length,p,m=e.slice(l+1,d).trim();if(m.startsWith(",")){let w=m.replace(/[;\s]+$/,"");if(w.endsWith("})"))p=w.slice(1,w.length-2).trim()}t.set(i,{template:u,body:p})}return t}function Hl(e,t,n){let o={missing:[],changed:[],changedCss:[],unverifiable:[]},s=af(t),r=Sn(n);for(let i of e){if(i.parseError){o.unverifiable.push(`${i.name} (${i.parseError})`);continue}if(!new RegExp(`Vue\\.component\\(\\s*(['"])${i.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\1`).test(t)){o.missing.push(i.name);continue}let c=s.get(i.name);if(!c){o.unverifiable.push(`${i.name} (bundle server không theo khuôn ComponentBuild)`);continue}if(Sn(i.template??"")!==Sn(c.template)){o.changed.push(i.name);continue}if(c.body===void 0)o.unverifiable.push(`${i.name} (chỉ đối chiếu được template, không bóc được thân script)`);else if(Sn(i.script??"")!==Sn(c.body)){o.changed.push(i.name);continue}let l=Sn(i.css??"");if(l&&!r.includes(l))o.changedCss.push(i.name)}return o}function zs(e){if(e.length===0)return"";return`${e.map((t)=>t.script).join(`;
133
133
  `)};
134
- `}function Ks(e){let t=e.map((n)=>n.css).filter((n)=>n.trim());if(t.length===0)return"";return`${t.join(`
134
+ `}function Xs(e){let t=e.map((n)=>n.css).filter((n)=>n.trim());if(t.length===0)return"";return`${t.join(`
135
135
  `)}
136
- `}var or="https://fui.vn",nf=45000,of=700,Gs=4000,Fl=1200000,rf=new Set(["script","stylesheet","image","font","media","manifest","texttrack"]),sf=new Set([".js",".css",".map",".woff",".woff2",".ttf",".otf",".eot",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp"]),Ll={project:90000,module:95000},af={desktop:{viewport:{width:1366,height:768}},tablet:{viewport:{width:768,height:1024},hasTouch:!0,userAgent:"Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"},mobile:{viewport:{width:390,height:844},isMobile:!0,hasTouch:!0,userAgent:"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"}},cf=3;function Bl(e,t){return J.join(e,`last-vitals-${t}.json`)}async function lf(e,t){try{let n=await mt.readFile(Bl(e,t),"utf8"),o=JSON.parse(n);return typeof o?.jsErrors==="number"?o:null}catch{return null}}function Jl(e,t){let n=[],o=(s,r,i)=>{if(r!==i)n.push(`${s} ${r}→${i}${i<r?" ↓":" ↑"}`)};if(o("JS error",e.jsErrors,t.jsErrors),o("init error",e.initErrors,t.initErrors),o("Vue warn",e.vueWarns,t.vueWarns),o("file môi trường thiếu",e.envMissing,t.envMissing),o("text nghi vấn",e.suspectTexts,t.suspectTexts),o("ảnh vỡ",e.brokenImages,t.brokenImages),o("canvas chết",e.deadCanvas,t.deadCanvas),o("text dính/chồng lấn",e.textCollisions,t.textCollisions),o("assert hỏng",e.assertFail,t.assertFail),e.appEmpty!==t.appEmpty)n.push(t.appEmpty?"app render RỖNG (trước có nội dung) ↑":"app đã có nội dung ↓");if(e.overflowX!==t.overflowX)n.push(t.overflowX?"tràn ngang (trước không) ↑":"hết tràn ngang ↓");return n}function uf(e,t){let n=Jl(e,t),o=e.ts.replace("T"," ").slice(0,19);if(n.length>0)return`Δ so với lượt trước (${o}): ${n.join(" · ")}`;return Ys(t)?`Δ so với lượt trước (${o}): KHÔNG chỉ số nào đổi — thay đổi vừa rồi không chạm tới lỗi đang có. Đổi hướng chẩn đoán, đừng render lại cùng một cách.`:`Δ so với lượt trước (${o}): không đổi — cả hai lượt đều sạch.`}function Ul(e,t){let n=new Set;for(let o of e){let s=String(o).split(`
137
- `)[0].trim();if(s)n.add(s.length>200?s.slice(0,200)+"…":s);if(n.size>=t)break}return[...n]}function df(e,t){return["",`\uD83D\uDED1 ĐANG GIẬM CHÂN — ${e} lượt render hỏng liên tiếp mà KHÔNG chỉ số nào đổi.`," Render lại lần nữa theo cùng hướng sẽ cho đúng kết quả này. Chọn MỘT trong ba:",t.length>0?" 1. Chạy tầng rẻ hơn được nêu ở dòng ↪ trên — nó chỉ thẳng vào chỗ hỏng.":" 1. Lùi về tầng tĩnh: module_validate (lỗi quy tắc) → module_outline (cấu trúc) → module_simulate không renderUI (action/watch)."," 2. Đọc lại screenshot + khối lỗi ở cuối report bằng mắt, đừng chỉ đọc con số."," 3. HỎI NGƯỜI DÙNG. Ba lượt không nhúc nhích thường nghĩa là giả định ban đầu sai, không phải sửa chưa đủ."]}function Ys(e){return e.jsErrors>0||e.vueWarns>0||e.envMissing>0||e.appEmpty||e.overflowX||e.suspectTexts>0||e.brokenImages>0||e.deadCanvas>0||e.textCollisions>0||e.assertFail>0}function pf(e,t,n,o){let s=["═══ CHẨN ĐOÁN ═══"];s.push(e.envMissing>0?`[MÔI TRƯỜNG] ${e.envMissing} file import/asset KHÔNG nạp được → sửa phần này TRƯỚC (project_sync / module_get / file_import_*). Lỗi ở hai nhóm dưới có thể chỉ là hệ quả.`:"[MÔI TRƯỜNG] ✓ mọi import/asset nạp đủ.");let r=[];if(e.jsErrors>0)r.push(`${e.jsErrors} JS error${e.initErrors?` (${e.initErrors} lúc init)`:""}`);if(e.vueWarns>0)r.push(`${e.vueWarns} [Vue warn]`);if(e.assertFail>0)r.push(`${e.assertFail} assert hỏng`);s.push(r.length>0?`[MODULE] ${r.join(" · ")} → lỗi nằm trong module.json / script.js / component.`:"[MODULE] ✓ không JS error, không Vue warn.");for(let a of Ul(n,3))s.push(` ✘ ${a}`);for(let a of Ul(o,2))s.push(` ⚠ ${a}`);let i=[];if(e.appEmpty)i.push("app render RỖNG");if(e.overflowX)i.push("tràn ngang viewport");if(e.suspectTexts>0)i.push(`${e.suspectTexts} text nghi vấn (undefined/NaN)`);if(e.brokenImages>0)i.push(`${e.brokenImages} ảnh vỡ`);if(e.deadCanvas>0)i.push(`${e.deadCanvas} canvas chart 0-size`);if(e.textCollisions>0)i.push(`${e.textCollisions} chỗ text dính/chồng lấn`);s.push(i.length>0?`[HIỂN THỊ] ${i.join(" · ")} → xem screenshot; đây là nhóm CHỈ renderUI thấy được.`:"[HIỂN THỊ] ✓ DOM audit sạch.");for(let a of t)s.push(` ↪ ${a}`);return s}function mf(e,t,n){let o=[];if(n.some((s)=>/\bis not defined\b/.test(s)))o.push("`… is not defined` = một định danh trần không có trên window. Ca thường gặp nhất: `EXE`/`FUN` gọi hàm chưa viết trong script.js — hoặc viết rồi mà quên `module_publish_script` (module_publish_json KHÔNG đẩy script.js). Soát tên hàm trong script.js trước khi đi tìm chỗ khác.");if(e.appEmpty&&e.jsErrors===0&&e.vueWarns===0&&e.envMissing===0)o.push("Trang trắng mà KHÔNG có JS error/Vue warn là dấu hiệu kinh điển của statement trong một `:attr` (Vue nuốt lỗi compile template) — chạy module_validate, nó bắt tĩnh được ca này.");if(t.some((s)=>/is not defined|Property or method/i.test(s)))o.push('[Vue warn] kiểu "property is not defined" = biến dùng trong template chưa khai trong data[] — module_validate bắt được (v-model/watch/state), không cần browser.');if(e.suspectTexts>0&&e.jsErrors===0)o.push('Text "undefined"/"NaN" thường là OUT/mapData trỏ sai khoá chứ không phải lỗi hiển thị — module_simulate (engine vm) chỉ ra chỗ gán, chạy nhanh hơn nhiều.');return o}async function hf(){for(let e of["playwright","playwright-core"])for(let t of[void 0,process.cwd()])try{return await(t?import(tf(ef(J.join(t,"noop.js")).resolve(e)).href):import(e))}catch{}throw Error(`rendering needs Playwright (optional, not installed). Install once:
136
+ `}var sr="https://fui.vn",uf=45000,df=700,Ys=4000,Vl=1200000,pf=new Set(["script","stylesheet","image","font","media","manifest","texttrack"]),mf=new Set([".js",".css",".map",".woff",".woff2",".ttf",".otf",".eot",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp"]),Wl={project:90000,module:95000},hf={desktop:{viewport:{width:1366,height:768}},tablet:{viewport:{width:768,height:1024},hasTouch:!0,userAgent:"Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"},mobile:{viewport:{width:390,height:844},isMobile:!0,hasTouch:!0,userAgent:"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"}},ff=3;function zl(e,t){return J.join(e,`last-vitals-${t}.json`)}async function gf(e,t){try{let n=await mt.readFile(zl(e,t),"utf8"),o=JSON.parse(n);return typeof o?.jsErrors==="number"?o:null}catch{return null}}function Xl(e,t){let n=[],o=(s,r,i)=>{if(r!==i)n.push(`${s} ${r}→${i}${i<r?" ↓":" ↑"}`)};if(o("JS error",e.jsErrors,t.jsErrors),o("init error",e.initErrors,t.initErrors),o("Vue warn",e.vueWarns,t.vueWarns),o("file môi trường thiếu",e.envMissing,t.envMissing),o("text nghi vấn",e.suspectTexts,t.suspectTexts),o("ảnh vỡ",e.brokenImages,t.brokenImages),o("canvas chết",e.deadCanvas,t.deadCanvas),o("text dính/chồng lấn",e.textCollisions,t.textCollisions),o("assert hỏng",e.assertFail,t.assertFail),e.appEmpty!==t.appEmpty)n.push(t.appEmpty?"app render RỖNG (trước có nội dung) ↑":"app đã có nội dung ↓");if(e.overflowX!==t.overflowX)n.push(t.overflowX?"tràn ngang (trước không) ↑":"hết tràn ngang ↓");return n}function wf(e,t){let n=Xl(e,t),o=e.ts.replace("T"," ").slice(0,19);if(n.length>0)return`Δ so với lượt trước (${o}): ${n.join(" · ")}`;return ei(t)?`Δ so với lượt trước (${o}): KHÔNG chỉ số nào đổi — thay đổi vừa rồi không chạm tới lỗi đang có. Đổi hướng chẩn đoán, đừng render lại cùng một cách.`:`Δ so với lượt trước (${o}): không đổi — cả hai lượt đều sạch.`}function ql(e,t){let n=new Set;for(let o of e){let s=String(o).split(`
137
+ `)[0].trim();if(s)n.add(s.length>200?s.slice(0,200)+"…":s);if(n.size>=t)break}return[...n]}function yf(e,t){return["",`\uD83D\uDED1 ĐANG GIẬM CHÂN — ${e} lượt render hỏng liên tiếp mà KHÔNG chỉ số nào đổi.`," Render lại lần nữa theo cùng hướng sẽ cho đúng kết quả này. Chọn MỘT trong ba:",t.length>0?" 1. Chạy tầng rẻ hơn được nêu ở dòng ↪ trên — nó chỉ thẳng vào chỗ hỏng.":" 1. Lùi về tầng tĩnh: module_validate (lỗi quy tắc) → module_outline (cấu trúc) → module_simulate không renderUI (action/watch)."," 2. Đọc lại screenshot + khối lỗi ở cuối report bằng mắt, đừng chỉ đọc con số."," 3. HỎI NGƯỜI DÙNG. Ba lượt không nhúc nhích thường nghĩa là giả định ban đầu sai, không phải sửa chưa đủ."]}function ei(e){return e.jsErrors>0||e.vueWarns>0||e.envMissing>0||e.appEmpty||e.overflowX||e.suspectTexts>0||e.brokenImages>0||e.deadCanvas>0||e.textCollisions>0||e.assertFail>0}function bf(e,t,n,o){let s=["═══ CHẨN ĐOÁN ═══"];s.push(e.envMissing>0?`[MÔI TRƯỜNG] ${e.envMissing} file import/asset KHÔNG nạp được → sửa phần này TRƯỚC (project_sync / module_get / file_import_*). Lỗi ở hai nhóm dưới có thể chỉ là hệ quả.`:"[MÔI TRƯỜNG] ✓ mọi import/asset nạp đủ.");let r=[];if(e.jsErrors>0)r.push(`${e.jsErrors} JS error${e.initErrors?` (${e.initErrors} lúc init)`:""}`);if(e.vueWarns>0)r.push(`${e.vueWarns} [Vue warn]`);if(e.assertFail>0)r.push(`${e.assertFail} assert hỏng`);s.push(r.length>0?`[MODULE] ${r.join(" · ")} → lỗi nằm trong module.json / script.js / component.`:"[MODULE] ✓ không JS error, không Vue warn.");for(let a of ql(n,3))s.push(` ✘ ${a}`);for(let a of ql(o,2))s.push(` ⚠ ${a}`);let i=[];if(e.appEmpty)i.push("app render RỖNG");if(e.overflowX)i.push("tràn ngang viewport");if(e.suspectTexts>0)i.push(`${e.suspectTexts} text nghi vấn (undefined/NaN)`);if(e.brokenImages>0)i.push(`${e.brokenImages} ảnh vỡ`);if(e.deadCanvas>0)i.push(`${e.deadCanvas} canvas chart 0-size`);if(e.textCollisions>0)i.push(`${e.textCollisions} chỗ text dính/chồng lấn`);s.push(i.length>0?`[HIỂN THỊ] ${i.join(" · ")} → xem screenshot; đây là nhóm CHỈ renderUI thấy được.`:"[HIỂN THỊ] ✓ DOM audit sạch.");for(let a of t)s.push(` ↪ ${a}`);return s}function kf(e,t,n){let o=[];if(n.some((s)=>/\bis not defined\b/.test(s)))o.push("`… is not defined` = một định danh trần không có trên window. Ca thường gặp nhất: `EXE`/`FUN` gọi hàm chưa viết trong script.js — hoặc viết rồi mà quên `module_publish_script` (module_publish_json KHÔNG đẩy script.js). Soát tên hàm trong script.js trước khi đi tìm chỗ khác.");if(e.appEmpty&&e.jsErrors===0&&e.vueWarns===0&&e.envMissing===0)o.push("Trang trắng mà KHÔNG có JS error/Vue warn là dấu hiệu kinh điển của statement trong một `:attr` (Vue nuốt lỗi compile template) — chạy module_validate, nó bắt tĩnh được ca này.");if(t.some((s)=>/is not defined|Property or method/i.test(s)))o.push('[Vue warn] kiểu "property is not defined" = biến dùng trong template chưa khai trong data[] — module_validate bắt được (v-model/watch/state), không cần browser.');if(e.suspectTexts>0&&e.jsErrors===0)o.push('Text "undefined"/"NaN" thường là OUT/mapData trỏ sai khoá chứ không phải lỗi hiển thị — module_simulate (engine vm) chỉ ra chỗ gán, chạy nhanh hơn nhiều.');return o}async function vf(){for(let e of["playwright","playwright-core"])for(let t of[void 0,process.cwd()])try{return await(t?import(lf(cf(J.join(t,"noop.js")).resolve(e)).href):import(e))}catch{}throw Error(`rendering needs Playwright (optional, not installed). Install once:
138
138
  bun add -g playwright
139
139
  bunx playwright install chromium
140
- (module simulate without --render works without Playwright.)`)}var ff=60000,zs=new Map;function Qs(){return J.join(vn(),"_cache","cdn")}function Zs(e){return ql("sha1").update(e).digest("hex")}function Kl(e){return ql("sha256").update(e).digest("hex")}async function gf(e){let t=Zs(e),n=Qs();try{let o=JSON.parse(await mt.readFile(J.join(n,`${t}.meta.json`),"utf-8"));return{body:await mt.readFile(J.join(n,`${t}.bin`)),contentType:o.contentType??Gl(e),meta:o}}catch{return}}async function wf(e,t,n,o){try{let s=Zs(e),r=Qs();await mt.mkdir(r,{recursive:!0}),await mt.writeFile(J.join(r,`${s}.bin`),t);let i={url:e,contentType:n,cachedAt:new Date().toISOString(),etag:o.etag,lastModified:o.lastModified,sha256:Kl(t)};await mt.writeFile(J.join(r,`${s}.meta.json`),JSON.stringify(i),"utf-8")}catch{}}async function yf(e,t){try{let n=Zs(e);await mt.writeFile(J.join(Qs(),`${n}.meta.json`),JSON.stringify({...t,cachedAt:new Date().toISOString()}),"utf-8")}catch{}}function Xs(e){if(!e)return"không rõ tuổi";let t=Date.now()-Date.parse(e);if(!Number.isFinite(t))return"không rõ tuổi";let n=Math.floor(t/86400000);if(n>=1)return`cũ ${n} ngày`;let o=Math.floor(t/3600000);return o>=1?`cũ ${o} giờ`:"cũ dưới 1 giờ"}async function bf(e,t){let n=t.refresh?void 0:await gf(e);if(t.offline){if(!n)throw Error("offline: chưa có bản cache nào cho URL này");return{body:n.body,contentType:n.contentType,origin:"stale",note:`offline — dùng cache ${Xs(n.meta.cachedAt)}`}}if(n&&!t.refresh){let r=zs.get(e);if(r!==void 0&&Date.now()-r<ff)return{body:n.body,contentType:n.contentType,origin:"memo"}}let o={},s=Boolean(n?.meta.etag||n?.meta.lastModified);if(n&&s&&!t.refresh){if(n.meta.etag)o["If-None-Match"]=n.meta.etag;if(n.meta.lastModified)o["If-Modified-Since"]=n.meta.lastModified}try{let r=await fetch(e,{headers:o,signal:AbortSignal.timeout(20000)});if(r.status===304&&n)return zs.set(e,Date.now()),await yf(e,n.meta),{body:n.body,contentType:n.contentType,origin:"revalidated"};if(!r.ok)throw Error(`HTTP ${r.status}`);let i=Buffer.from(await r.arrayBuffer()),a=r.headers.get("content-type")??Gl(e),c=r.headers.get("etag")??void 0,l=r.headers.get("last-modified")??void 0;await wf(e,i,a,{etag:c,lastModified:l}),zs.set(e,Date.now());let u=Boolean(n?.meta.sha256&&n.meta.sha256!==Kl(i));return{body:i,contentType:a,origin:n?"updated":"fetched",changed:u,note:u?`nội dung ĐỔI so với bản cache ${Xs(n?.meta.cachedAt)}`:void 0}}catch(r){if(n)return{body:n.body,contentType:n.contentType,origin:"stale",note:`không revalidate được (${r.message}) — dùng cache ${Xs(n.meta.cachedAt)}`};throw r}}function Gl(e){switch(J.extname(new URL(e).pathname).toLowerCase()){case".js":return"application/javascript; charset=utf-8";case".css":return"text/css; charset=utf-8";case".svg":return"image/svg+xml";case".png":return"image/png";case".jpg":case".jpeg":return"image/jpeg";case".woff":return"font/woff";case".woff2":return"font/woff2";case".ttf":return"font/ttf";default:return"application/octet-stream"}}function jn(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function Ct(e,t){return e.replace(new RegExp(`</(?=${t})`,"gi"),"<\\/")}function rr(e,t){return JSON.stringify(e,null,t).replace(/</g,"\\u003c")}function kf(e,t){if(!e)return;let n=e.trim();if(!n)return;if(/^https?:\/\//i.test(n))return n;if(n.startsWith("//"))return`https:${n}`;if(n.startsWith("/"))return`https://${t}${n}`;return}function Hl(e){if(typeof e!=="string")return[];let t=e.trim();if(!t)return[];if(t.startsWith("["))try{let n=JSON.parse(t);if(Array.isArray(n))return n.filter((o)=>typeof o==="string").map((o)=>o.trim()).filter(Boolean)}catch{}return[t]}function vf(e){let t=(e??"").trim();if(!t)return{};let n=t.startsWith("//")?`https:${t}`:t;if(/^https?:\/\//i.test(n))try{let s=new URL(n);return{host:s.host,path:s.pathname||"/"}}catch{}let o=t.split(/[?#]/)[0];if(!o)return{};return{path:o.startsWith("/")?o:`/${o}`}}function $f(e,t,n){let o=[],s=vf(e.url),r=Hl(e.mDomain),i=Hl(t?.pDomain),a=s.host;if(!a)a=r[0]??i[0]??n,o.push("_moduleInfo.json không có url kèm host — origin lấy từ "+`${r.length?"mDomain":i.length?"pDomain":"domain asset mặc định"}: ${a}. `+"Chạy `module_get` để có url thật nếu render sai domain.");let c=[...new Set([...s.host?[s.host]:[],...r,...i])];if(c.length>1)o.push(`Project/module phục vụ trên nhiều domain (${c.join(", ")}) — lần render này chạy ở "${a}". `+"v_Set.domainSetting của domain khác KHÔNG được áp dụng; muốn thử domain khác thì render module có url thuộc domain đó.");let l=s.path&&s.path!=="/"?s.path:`/${e.ModuleID}`;return{host:a,basePath:l,notes:o}}function Vl(e,t){if(!e)return null;let o=new RegExp(`var\\s+\\$${t}\\s*=\\s*\\{`).exec(e);if(!o)return null;let s=e.indexOf("{",o.index),r=0,i=null;for(let a=s;a<e.length;a+=1){let c=e[a];if(i){if(c==="\\"){a+=1;continue}if(c===i)i=null;continue}if(c==='"'||c==="'"){i=c;continue}if(c==="{")r+=1;else if(c==="}"){if(r-=1,r===0)try{return JSON.parse(e.slice(s,a+1))}catch{return null}}}return null}function Sf(e,t){let n={},o={},s=(i,a)=>{if(!j(i))return;for(let[c,l]of Object.entries(i))n[c]=l,o[c]=a};s(e.groupSetting,"$projectGroupSetting"),s(e.projectData,"$projectData"),s(e.moduleSet,"module.json set");let r=n.domainSetting;if(j(r))s(r[t],`domainSetting[${t}]`),delete n.domainSetting,delete o.domainSetting;return{settings:n,origins:o}}async function Wl(e){let t=await _e(e),n=[],o=[];for(let s of t){let r=await L(J.join(e,s));if(!r.trim())continue;let{compiled:i,warning:a}=_l(s.replace(/\.vue$/,""),r);if(n.push(i),a)o.push(a)}return{components:n,warnings:o}}async function jf(e){let t=await _e(e),n=[];for(let o of t){let s=o.replace(/\.vue$/i,""),r=await L(J.join(e,o));if(!r.trim())continue;try{let{template:i,script:a,style:c}=It(r);n.push({name:s,template:i,script:a,css:c})}catch(i){n.push({name:s,parseError:i.message})}}return n}async function If(e,t,n){let o=[],s=[],r=[],i={project:[],module:[]},a=[{scope:"project",items:await H(J.join(t,"imports","_imports.json"),[]),baseDir:t,fixHint:"project_sync"},{scope:"module",items:await H(J.join(e,"imports","_imports.json"),[]),baseDir:e,fixHint:"module_get"}];for(let{scope:c,items:l,baseDir:u,fixHint:d}of a){if(!Array.isArray(l))continue;for(let p of l){if(!p||!p.name)continue;let h=`[${c}] ${p.name}`,w=p.type===".css"?"css":p.type===".js"?"js":void 0;if(!w){s.push(`import ${h}: type "${p.type??"(trống)"}" không phải .js/.css — không nạp vào trang render`);continue}if(typeof p.sort!=="number")i[c].push(p.name);o.push({label:h,scope:c,kind:w,sort:typeof p.sort==="number"?p.sort:Ll[c],name:p.name,fileID:p.fileID,contentType:p.contentType,filePath:p.filePath,baseDir:u,domain:n,fixHint:d})}}for(let c of["project","module"]){let l=i[c];if(l.length===0)continue;r.push(`${l.length} import cấp ${c} không có FileSort trong _imports.json (${l.join(", ")}) → tạm xếp cuối `+`(${Ll[c]}). Thứ tự nạp có thể khác production; chạy \`${c==="project"?"project_sync":"module_get"}\` để lấy FileSort thật.`)}return{entries:o,problems:s,notes:r}}async function Cf(e){let t=[],n=[],o=[],s=[],r=[],i=new Set;for(let a of Dl(e)){let c=a.fileID?`id:${a.fileID}`:`name:${a.scope}:${a.name}`;if(i.has(c))continue;if(a.filePath&&a.baseDir){let p=await L(J.join(a.baseDir,a.filePath));if(p.trim()){i.add(c);let h=jn(a.label);if(a.kind==="css")t.push(`<style data-fui-import="${h}">
140
+ (module simulate without --render works without Playwright.)`)}var $f=60000,Qs=new Map;function ti(){return J.join(vn(),"_cache","cdn")}function ni(e){return Gl("sha1").update(e).digest("hex")}function Yl(e){return Gl("sha256").update(e).digest("hex")}async function Sf(e){let t=ni(e),n=ti();try{let o=JSON.parse(await mt.readFile(J.join(n,`${t}.meta.json`),"utf-8"));return{body:await mt.readFile(J.join(n,`${t}.bin`)),contentType:o.contentType??Ql(e),meta:o}}catch{return}}async function jf(e,t,n,o){try{let s=ni(e),r=ti();await mt.mkdir(r,{recursive:!0}),await mt.writeFile(J.join(r,`${s}.bin`),t);let i={url:e,contentType:n,cachedAt:new Date().toISOString(),etag:o.etag,lastModified:o.lastModified,sha256:Yl(t)};await mt.writeFile(J.join(r,`${s}.meta.json`),JSON.stringify(i),"utf-8")}catch{}}async function If(e,t){try{let n=ni(e);await mt.writeFile(J.join(ti(),`${n}.meta.json`),JSON.stringify({...t,cachedAt:new Date().toISOString()}),"utf-8")}catch{}}function Zs(e){if(!e)return"không rõ tuổi";let t=Date.now()-Date.parse(e);if(!Number.isFinite(t))return"không rõ tuổi";let n=Math.floor(t/86400000);if(n>=1)return`cũ ${n} ngày`;let o=Math.floor(t/3600000);return o>=1?`cũ ${o} giờ`:"cũ dưới 1 giờ"}async function Cf(e,t){let n=t.refresh?void 0:await Sf(e);if(t.offline){if(!n)throw Error("offline: chưa có bản cache nào cho URL này");return{body:n.body,contentType:n.contentType,origin:"stale",note:`offline — dùng cache ${Zs(n.meta.cachedAt)}`}}if(n&&!t.refresh){let r=Qs.get(e);if(r!==void 0&&Date.now()-r<$f)return{body:n.body,contentType:n.contentType,origin:"memo"}}let o={},s=Boolean(n?.meta.etag||n?.meta.lastModified);if(n&&s&&!t.refresh){if(n.meta.etag)o["If-None-Match"]=n.meta.etag;if(n.meta.lastModified)o["If-Modified-Since"]=n.meta.lastModified}try{let r=await fetch(e,{headers:o,signal:AbortSignal.timeout(20000)});if(r.status===304&&n)return Qs.set(e,Date.now()),await If(e,n.meta),{body:n.body,contentType:n.contentType,origin:"revalidated"};if(!r.ok)throw Error(`HTTP ${r.status}`);let i=Buffer.from(await r.arrayBuffer()),a=r.headers.get("content-type")??Ql(e),c=r.headers.get("etag")??void 0,l=r.headers.get("last-modified")??void 0;await jf(e,i,a,{etag:c,lastModified:l}),Qs.set(e,Date.now());let u=Boolean(n?.meta.sha256&&n.meta.sha256!==Yl(i));return{body:i,contentType:a,origin:n?"updated":"fetched",changed:u,note:u?`nội dung ĐỔI so với bản cache ${Zs(n?.meta.cachedAt)}`:void 0}}catch(r){if(n)return{body:n.body,contentType:n.contentType,origin:"stale",note:`không revalidate được (${r.message}) — dùng cache ${Zs(n.meta.cachedAt)}`};throw r}}function Ql(e){switch(J.extname(new URL(e).pathname).toLowerCase()){case".js":return"application/javascript; charset=utf-8";case".css":return"text/css; charset=utf-8";case".svg":return"image/svg+xml";case".png":return"image/png";case".jpg":case".jpeg":return"image/jpeg";case".woff":return"font/woff";case".woff2":return"font/woff2";case".ttf":return"font/ttf";default:return"application/octet-stream"}}function jn(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function Ct(e,t){return e.replace(new RegExp(`</(?=${t})`,"gi"),"<\\/")}function ir(e,t){return JSON.stringify(e,null,t).replace(/</g,"\\u003c")}function Rf(e,t){if(!e)return;let n=e.trim();if(!n)return;if(/^https?:\/\//i.test(n))return n;if(n.startsWith("//"))return`https:${n}`;if(n.startsWith("/"))return`https://${t}${n}`;return}function Bl(e){if(typeof e!=="string")return[];let t=e.trim();if(!t)return[];if(t.startsWith("["))try{let n=JSON.parse(t);if(Array.isArray(n))return n.filter((o)=>typeof o==="string").map((o)=>o.trim()).filter(Boolean)}catch{}return[t]}function Tf(e){let t=(e??"").trim();if(!t)return{};let n=t.startsWith("//")?`https:${t}`:t;if(/^https?:\/\//i.test(n))try{let s=new URL(n);return{host:s.host,path:s.pathname||"/"}}catch{}let o=t.split(/[?#]/)[0];if(!o)return{};return{path:o.startsWith("/")?o:`/${o}`}}function Pf(e,t,n){let o=[],s=Tf(e.url),r=Bl(e.mDomain),i=Bl(t?.pDomain),a=s.host;if(!a)a=r[0]??i[0]??n,o.push("_moduleInfo.json không có url kèm host — origin lấy từ "+`${r.length?"mDomain":i.length?"pDomain":"domain asset mặc định"}: ${a}. `+"Chạy `module_get` để có url thật nếu render sai domain.");let c=[...new Set([...s.host?[s.host]:[],...r,...i])];if(c.length>1)o.push(`Project/module phục vụ trên nhiều domain (${c.join(", ")}) — lần render này chạy ở "${a}". `+"v_Set.domainSetting của domain khác KHÔNG được áp dụng; muốn thử domain khác thì render module có url thuộc domain đó.");let l=s.path&&s.path!=="/"?s.path:`/${e.ModuleID}`;return{host:a,basePath:l,notes:o}}function Jl(e,t){if(!e)return null;let o=new RegExp(`var\\s+\\$${t}\\s*=\\s*\\{`).exec(e);if(!o)return null;let s=e.indexOf("{",o.index),r=0,i=null;for(let a=s;a<e.length;a+=1){let c=e[a];if(i){if(c==="\\"){a+=1;continue}if(c===i)i=null;continue}if(c==='"'||c==="'"){i=c;continue}if(c==="{")r+=1;else if(c==="}"){if(r-=1,r===0)try{return JSON.parse(e.slice(s,a+1))}catch{return null}}}return null}function Ef(e,t){let n={},o={},s=(i,a)=>{if(!j(i))return;for(let[c,l]of Object.entries(i))n[c]=l,o[c]=a};s(e.groupSetting,"$projectGroupSetting"),s(e.projectData,"$projectData"),s(e.moduleSet,"module.json set");let r=n.domainSetting;if(j(r))s(r[t],`domainSetting[${t}]`),delete n.domainSetting,delete o.domainSetting;return{settings:n,origins:o}}async function Kl(e){let t=await _e(e),n=[],o=[];for(let s of t){let r=await F(J.join(e,s));if(!r.trim())continue;let{compiled:i,warning:a}=Ul(s.replace(/\.vue$/,""),r);if(n.push(i),a)o.push(a)}return{components:n,warnings:o}}async function Af(e){let t=await _e(e),n=[];for(let o of t){let s=o.replace(/\.vue$/i,""),r=await F(J.join(e,o));if(!r.trim())continue;try{let{template:i,script:a,style:c}=It(r);n.push({name:s,template:i,script:a,css:c})}catch(i){n.push({name:s,parseError:i.message})}}return n}async function Nf(e,t,n){let o=[],s=[],r=[],i={project:[],module:[]},a=[{scope:"project",items:await H(J.join(t,"imports","_imports.json"),[]),baseDir:t,fixHint:"project_sync"},{scope:"module",items:await H(J.join(e,"imports","_imports.json"),[]),baseDir:e,fixHint:"module_get"}];for(let{scope:c,items:l,baseDir:u,fixHint:d}of a){if(!Array.isArray(l))continue;for(let p of l){if(!p||!p.name)continue;let m=`[${c}] ${p.name}`,w=p.type===".css"?"css":p.type===".js"?"js":void 0;if(!w){s.push(`import ${m}: type "${p.type??"(trống)"}" không phải .js/.css — không nạp vào trang render`);continue}if(typeof p.sort!=="number")i[c].push(p.name);o.push({label:m,scope:c,kind:w,sort:typeof p.sort==="number"?p.sort:Wl[c],name:p.name,fileID:p.fileID,contentType:p.contentType,filePath:p.filePath,baseDir:u,domain:n,fixHint:d})}}for(let c of["project","module"]){let l=i[c];if(l.length===0)continue;r.push(`${l.length} import cấp ${c} không có FileSort trong _imports.json (${l.join(", ")}) → tạm xếp cuối `+`(${Wl[c]}). Thứ tự nạp có thể khác production; chạy \`${c==="project"?"project_sync":"module_get"}\` để lấy FileSort thật.`)}return{entries:o,problems:s,notes:r}}async function Df(e){let t=[],n=[],o=[],s=[],r=[],i=new Set;for(let a of Ml(e)){let c=a.fileID?`id:${a.fileID}`:`name:${a.scope}:${a.name}`;if(i.has(c))continue;if(a.filePath&&a.baseDir){let p=await F(J.join(a.baseDir,a.filePath));if(p.trim()){i.add(c);let m=jn(a.label);if(a.kind==="css")t.push(`<style data-fui-import="${m}">
141
141
  ${Ct(p,"style")}
142
- </style>`);else n.push(`<script data-fui-import="${h}">
142
+ </style>`);else n.push(`<script data-fui-import="${m}">
143
143
  ${Ct(p,"script")}
144
- </script>`);s.push(`${a.sort}. ${a.label} — inline ${p.length} bytes`);continue}if(!a.fileID){o.push(`import ${a.label}: file nội bộ "${a.filePath}" thiếu/rỗng và không có fileID để lấy bản server → KHÔNG nạp được. `+`Chạy \`${a.fixHint}\`. Lỗi JS/hiển thị bên dưới có thể chỉ là hệ quả của việc này.`);continue}r.push(`${a.label}: chưa có bản local → dùng bản trên server. Chạy \`${a.fixHint}\` nếu cần sửa file này.`)}let u=!a.filePath&&(!a.contentType||a.contentType==="link"||a.contentType==="comp-link")?a.name:a.fileID?`//${a.domain}/_aset/fp/auth/moduleImportFile/${a.fileID}${a.kind==="css"?".css":".js"}`:void 0,d=kf(u,a.domain);if(!d){o.push(`import ${a.label}: không có file nội bộ và tên không phải URL/đường dẫn tuyệt đối → không xác định được nguồn để nạp. `+`Nếu đây là file tự tạo, chạy \`${a.fixHint}\`.`);continue}if(i.has(`url:${d}`))continue;if(i.add(c),i.add(`url:${d}`),a.kind==="css")t.push(`<link href="${jn(d)}" rel="stylesheet" type="text/css" />`);else n.push(`<script src="${jn(d)}" type="text/javascript"></script>`);s.push(`${a.sort}. ${a.label} → ${d}`)}return{styles:t,scripts:n,problems:o,loaded:s,serverFallbackNotes:r}}function Rf(e,t,n){return`(() => {
145
- const previewVueData = ${rr(n,2)};
144
+ </script>`);s.push(`${a.sort}. ${a.label} — inline ${p.length} bytes`);continue}if(!a.fileID){o.push(`import ${a.label}: file nội bộ "${a.filePath}" thiếu/rỗng và không có fileID để lấy bản server → KHÔNG nạp được. `+`Chạy \`${a.fixHint}\`. Lỗi JS/hiển thị bên dưới có thể chỉ là hệ quả của việc này.`);continue}r.push(`${a.label}: chưa có bản local → dùng bản trên server. Chạy \`${a.fixHint}\` nếu cần sửa file này.`)}let u=!a.filePath&&(!a.contentType||a.contentType==="link"||a.contentType==="comp-link")?a.name:a.fileID?`//${a.domain}/_aset/fp/auth/moduleImportFile/${a.fileID}${a.kind==="css"?".css":".js"}`:void 0,d=Rf(u,a.domain);if(!d){o.push(`import ${a.label}: không có file nội bộ và tên không phải URL/đường dẫn tuyệt đối → không xác định được nguồn để nạp. `+`Nếu đây là file tự tạo, chạy \`${a.fixHint}\`.`);continue}if(i.has(`url:${d}`))continue;if(i.add(c),i.add(`url:${d}`),a.kind==="css")t.push(`<link href="${jn(d)}" rel="stylesheet" type="text/css" />`);else n.push(`<script src="${jn(d)}" type="text/javascript"></script>`);s.push(`${a.sort}. ${a.label} → ${d}`)}return{styles:t,scripts:n,problems:o,loaded:s,serverFallbackNotes:r}}function xf(e,t,n){return`(() => {
145
+ const previewVueData = ${ir(n,2)};
146
146
  const basePath = ${JSON.stringify(t)};
147
147
  const moduleId = ${JSON.stringify(e.ModuleID)};
148
148
 
@@ -181,20 +181,20 @@ ${Ct(p,"script")}
181
181
  if (window.vueData && Object.keys(window.vueData).length) {
182
182
  syncRouteState(window.vueData);
183
183
  }
184
- })();`}async function Tf(e,t){let n=[],o=await H(J.join(e,"_moduleInfo.json"),{ProjectID:"render-project",ModuleID:J.basename(e)}),s=$n(o.ProjectID),r=await H(J.join(s,"_projectInfo.json"),null),i=await H(J.join(s,"project.json"),null),a=(o.Framework??"V3").toUpperCase()==="V2"?"V2":"V3",c=or.replace(/^https:\/\//,""),l=$f(o,r,c),u=l.host,d=`https://${u}`;n.push(...l.notes);let p=await L(J.join(e,"module.json")),h={ModuleName:o.ModuleName??o.ModuleID,data:[],watch:{},controls:[],set:{}};if(p.trim())try{h=JSON.parse(p)}catch(oe){throw Error(`module.json có lỗi JSON syntax: ${oe.message} — sửa trước khi renderUI.`)}else n.push("module.json rỗng — render shell với module trống");let w=await L(J.join(e,"header.html")),y=await L(J.join(e,"body.html")),v=await L(J.join(e,"script.js")),N=await L(J.join(e,"style.css")),I=o.HTMLOnly===!0,g=I?{deps:[],source:"(bỏ qua — HTMLOnly)",warning:""}:await Al(a,or);if(g.warning)n.push(g.warning);let b=g.deps.map((oe)=>xl(oe,or)).filter((oe)=>Boolean(oe)),R=await If(e,s,u),C=await Cf([...b,...R.entries]);C.problems.unshift(...R.problems),n.push(...R.notes);let S=I?`Framework ${a}: KHÔNG nhúng (HTMLOnly=true — server cũng không nhúng)`:`Framework ${a}: danh mục từ ${g.source}`;n.push(...C.serverFallbackNotes);let D=await Wl(J.join(s,"components"));n.push(...D.warnings);let x=I?{js:"",css:"",jsUrl:"",notes:[],problems:[]}:await Ol(o.ProjectID,u),A=Boolean(x.js)&&!t.forceLocalProjectBundle;if(I)n.push("HTMLOnly=true → render KHÔNG nhúng framework "+a+", KHÔNG nhúng projectdata/StyleCSS cấp project "+"và KHÔNG dựng #fastProjectAPP — giống hệt trang production. Thư viện nào module cần (Vue, jQuery, Vuetify, "+"lodash…) phải do CHÍNH module khai làm import; thiếu là trắng trang thật, không phải lỗi của render.");else if(t.forceLocalProjectBundle)n.push("Tầng project dựng từ workspace LOCAL (chế độ preview cấp component) — không dùng bundle server. "+"Chạy `project_sync` nếu nghi file project trong workspace đã cũ.");else if(!A)n.push(...x.problems);else{let oe=await jf(J.join(s,"components")),De=Ml(oe,x.js,x.css??""),Xt=[...De.changed.map((pe)=>`${pe} (đã sửa local, chưa push)`),...De.missing.map((pe)=>`${pe} (chưa có trên server)`),...De.changedCss.filter((pe)=>!De.changed.includes(pe)).map((pe)=>`${pe} (<style> đã sửa local, chưa push)`)];if(Xt.length>0)throw Error(`⛔ Component cấp project lệch với bản trên server: ${Xt.join(", ")}.
184
+ })();`}async function Of(e,t){let n=[],o=await H(J.join(e,"_moduleInfo.json"),{ProjectID:"render-project",ModuleID:J.basename(e)}),s=$n(o.ProjectID),r=await H(J.join(s,"_projectInfo.json"),null),i=await H(J.join(s,"project.json"),null),a=(o.Framework??"V3").toUpperCase()==="V2"?"V2":"V3",c=sr.replace(/^https:\/\//,""),l=Pf(o,r,c),u=l.host,d=`https://${u}`;n.push(...l.notes);let p=await F(J.join(e,"module.json")),m={ModuleName:o.ModuleName??o.ModuleID,data:[],watch:{},controls:[],set:{}};if(p.trim())try{m=JSON.parse(p)}catch(oe){throw Error(`module.json có lỗi JSON syntax: ${oe.message} — sửa trước khi renderUI.`)}else n.push("module.json rỗng — render shell với module trống");let w=await F(J.join(e,"header.html")),y=await F(J.join(e,"body.html")),v=await F(J.join(e,"script.js")),A=await F(J.join(e,"style.css")),I=o.HTMLOnly===!0,g=I?{deps:[],source:"(bỏ qua — HTMLOnly)",warning:""}:await _l(a,sr);if(g.warning)n.push(g.warning);let k=g.deps.map((oe)=>Ll(oe,sr)).filter((oe)=>Boolean(oe)),R=await Nf(e,s,u),C=await Df([...k,...R.entries]);C.problems.unshift(...R.problems),n.push(...R.notes);let S=I?`Framework ${a}: KHÔNG nhúng (HTMLOnly=true — server cũng không nhúng)`:`Framework ${a}: danh mục từ ${g.source}`;n.push(...C.serverFallbackNotes);let D=await Kl(J.join(s,"components"));n.push(...D.warnings);let x=I?{js:"",css:"",jsUrl:"",notes:[],problems:[]}:await Fl(o.ProjectID,u),N=Boolean(x.js)&&!t.forceLocalProjectBundle;if(I)n.push("HTMLOnly=true → render KHÔNG nhúng framework "+a+", KHÔNG nhúng projectdata/StyleCSS cấp project "+"và KHÔNG dựng #fastProjectAPP — giống hệt trang production. Thư viện nào module cần (Vue, jQuery, Vuetify, "+"lodash…) phải do CHÍNH module khai làm import; thiếu là trắng trang thật, không phải lỗi của render.");else if(t.forceLocalProjectBundle)n.push("Tầng project dựng từ workspace LOCAL (chế độ preview cấp component) — không dùng bundle server. "+"Chạy `project_sync` nếu nghi file project trong workspace đã cũ.");else if(!N)n.push(...x.problems);else{let oe=await Af(J.join(s,"components")),De=Hl(oe,x.js,x.css??""),Xt=[...De.changed.map((pe)=>`${pe} (đã sửa local, chưa push)`),...De.missing.map((pe)=>`${pe} (chưa có trên server)`),...De.changedCss.filter((pe)=>!De.changed.includes(pe)).map((pe)=>`${pe} (<style> đã sửa local, chưa push)`)];if(Xt.length>0)throw Error(`⛔ Component cấp project lệch với bản trên server: ${Xt.join(", ")}.
185
185
 
186
186
  `+"renderUI dựng trang bằng bundle project TẢI TỪ SERVER (_aset/.../projectdata) nên bản local chưa push "+`không xuất hiện — nếu vẫn render, tấm ảnh bạn nhận được là BẢN CŨ, không phải bản vừa sửa.
187
187
 
188
- `+"• Nếu bản local là bản bạn vừa sửa → `component_preview` để kiểm chứng riêng component (chạy được bằng "+"file local, không cần push), rồi `component_update` từng component, rồi render lại.\n"+"• Nếu bản trên server mới hơn (ai đó sửa trên web IDE) → `project_sync` để lấy về, ĐỪNG push đè.");if(De.unverifiable.length>0)n.push(`Không đối chiếu được component cấp project: ${De.unverifiable.join(", ")}. `+"Không chặn render, nhưng nếu giao diện khác mong đợi thì đây là chỗ đáng nghi đầu tiên.");let Re=D.components.filter((pe)=>pe.css.trim()).map((pe)=>pe.name);if(Re.length>0&&!(x.css??"").trim())n.push(`StyleCSS cấp project tải về RỖNG nhưng component local có khai <style>: ${Re.join(", ")}. `+"Hoặc các component đó chưa push (`component_update`), hoặc route StyleCSS đang hỏng — "+"dù nguyên nhân nào thì style cấp project trong lần render này cũng đang thiếu.")}let E=await Wl(J.join(e,"components"));n.push(...E.warnings);let te=j(i?.data)?i.data:i??{},ve={...te,ProjectName:te.ProjectName??r?.ProjectName??o.ProjectID,title:te.title??o.mTitle??o.ModuleName??o.ModuleID},$e=Vl(x.js,"projectGroupSetting"),V=Vl(x.js,"projectData");if(A&&!V)n.push("Không bóc được `var $projectData` khỏi bundle projectdata tải từ server — apiDomain/userInfo "+"trong lần render này suy ra từ project.json local, có thể lệch với bản trang thật đang chạy.");let de=String(te.apiDomain??""),ge=typeof V?.apiDomain==="string"?V.apiDomain:"";if(V&&de&&ge&&de!==ge)n.push(`project.json local có apiDomain "${de}" nhưng bản trên server là "${ge}" — `+"render dùng bản server (giống trang thật). Chạy `project_sync` để đồng bộ file local.");let Be=j(h.set)?h.set:null,tt=Sf({groupSetting:$e,projectData:V??te,moduleSet:Be},u),Ce=String(tt.settings.apiDomain??""),Et=tt.origins.apiDomain??"(không tầng nào khai)",Je=(()=>{let oe=typeof tt.settings.userInfo==="string"?tt.settings.userInfo.trim():"";if(!oe)return"";return/^https?:\/\//i.test(oe)?oe:`${Ce}${oe}`})();if(!Ce)n.push(`Không tầng nào ($projectGroupSetting → $projectData → module.json set → domainSetting["${u}"]) khai apiDomain cho "${o.ProjectID}" — API sẽ gọi bằng đường dẫn tương đối và apiMocks khớp theo URL `+"gần như chắc chắn trượt. Chạy `project_sync` nếu dữ liệu project đã cũ.");let nt={},Pt=l.basePath,Y=t.user??{},co=!("SystemRight"in Y),Rn=!("FunctionRight"in Y),zt=qt([p,JSON.stringify(te),w,y,v]),lo={user:{UserID:"render-preview",FirstName:"Render",LastName:"Preview",Email:"render@preview.local",SystemRight:9,FunctionRight:zt,...Y},...t.vueData??{}},uo=Rn?to(zt,(await Wt(s)).functionRights):null,po=(()=>{let oe=[];if(co)oe.push("SystemRight=9 (mặc định, quyền cao nhất)");if(Rn)oe.push(zt.length?`FunctionRight=[${zt.join(", ")}] (mặc định — quét được từ module.json/project)`:"FunctionRight=[] (mặc định — không quét được mã nào)");return oe.length?`user preview: ${oe.join(" · ")}. rightTest()/v-if theo FunctionRight khác mã trên sẽ ẩn — truyền tham số \`user\` để test vai trò hạn chế hơn.`:null})(),Ke=jn(o.mTitle||o.ModuleName||o.ModuleID),ne=I?"":A?x.css??"":Ks(D.components),Fe=[N,Ks(E.components)].filter((oe)=>oe.trim()).join(`
189
- `),Tn=I?"":A?`<script data-fui-projectdata="${jn(x.jsUrl)}">
188
+ `+"• Nếu bản local là bản bạn vừa sửa → `component_preview` để kiểm chứng riêng component (chạy được bằng "+"file local, không cần push), rồi `component_update` từng component, rồi render lại.\n"+"• Nếu bản trên server mới hơn (ai đó sửa trên web IDE) → `project_sync` để lấy về, ĐỪNG push đè.");if(De.unverifiable.length>0)n.push(`Không đối chiếu được component cấp project: ${De.unverifiable.join(", ")}. `+"Không chặn render, nhưng nếu giao diện khác mong đợi thì đây là chỗ đáng nghi đầu tiên.");let Re=D.components.filter((pe)=>pe.css.trim()).map((pe)=>pe.name);if(Re.length>0&&!(x.css??"").trim())n.push(`StyleCSS cấp project tải về RỖNG nhưng component local có khai <style>: ${Re.join(", ")}. `+"Hoặc các component đó chưa push (`component_update`), hoặc route StyleCSS đang hỏng — "+"dù nguyên nhân nào thì style cấp project trong lần render này cũng đang thiếu.")}let P=await Kl(J.join(e,"components"));n.push(...P.warnings);let te=j(i?.data)?i.data:i??{},ve={...te,ProjectName:te.ProjectName??r?.ProjectName??o.ProjectID,title:te.title??o.mTitle??o.ModuleName??o.ModuleID},$e=Jl(x.js,"projectGroupSetting"),V=Jl(x.js,"projectData");if(N&&!V)n.push("Không bóc được `var $projectData` khỏi bundle projectdata tải từ server — apiDomain/userInfo "+"trong lần render này suy ra từ project.json local, có thể lệch với bản trang thật đang chạy.");let de=String(te.apiDomain??""),ge=typeof V?.apiDomain==="string"?V.apiDomain:"";if(V&&de&&ge&&de!==ge)n.push(`project.json local có apiDomain "${de}" nhưng bản trên server là "${ge}" — `+"render dùng bản server (giống trang thật). Chạy `project_sync` để đồng bộ file local.");let Be=j(m.set)?m.set:null,tt=Ef({groupSetting:$e,projectData:V??te,moduleSet:Be},u),Ce=String(tt.settings.apiDomain??""),Pt=tt.origins.apiDomain??"(không tầng nào khai)",Je=(()=>{let oe=typeof tt.settings.userInfo==="string"?tt.settings.userInfo.trim():"";if(!oe)return"";return/^https?:\/\//i.test(oe)?oe:`${Ce}${oe}`})();if(!Ce)n.push(`Không tầng nào ($projectGroupSetting → $projectData → module.json set → domainSetting["${u}"]) khai apiDomain cho "${o.ProjectID}" — API sẽ gọi bằng đường dẫn tương đối và apiMocks khớp theo URL `+"gần như chắc chắn trượt. Chạy `project_sync` nếu dữ liệu project đã cũ.");let nt={},Et=l.basePath,Y=t.user??{},lo=!("SystemRight"in Y),Rn=!("FunctionRight"in Y),zt=qt([p,JSON.stringify(te),w,y,v]),uo={user:{UserID:"render-preview",FirstName:"Render",LastName:"Preview",Email:"render@preview.local",SystemRight:9,FunctionRight:zt,...Y},...t.vueData??{}},po=Rn?to(zt,(await Wt(s)).functionRights):null,mo=(()=>{let oe=[];if(lo)oe.push("SystemRight=9 (mặc định, quyền cao nhất)");if(Rn)oe.push(zt.length?`FunctionRight=[${zt.join(", ")}] (mặc định — quét được từ module.json/project)`:"FunctionRight=[] (mặc định — không quét được mã nào)");return oe.length?`user preview: ${oe.join(" · ")}. rightTest()/v-if theo FunctionRight khác mã trên sẽ ẩn — truyền tham số \`user\` để test vai trò hạn chế hơn.`:null})(),Ke=jn(o.mTitle||o.ModuleName||o.ModuleID),ne=I?"":N?x.css??"":Xs(D.components),Le=[A,Xs(P.components)].filter((oe)=>oe.trim()).join(`
189
+ `),Tn=I?"":N?`<script data-fui-projectdata="${jn(x.jsUrl)}">
190
190
  ${Ct(x.js??"","script")}
191
191
  </script>`:`<script data-fui-projectdata="local">
192
- var $projectData = ${rr(ve,2)};
193
- ${Ct(Js(D.components),"script")}
194
- </script>`,mo=`<script data-fui-moduledata="local">
195
- var $moduleUI = ${rr(h,2)};
192
+ var $projectData = ${ir(ve,2)};
193
+ ${Ct(zs(D.components),"script")}
194
+ </script>`,ho=`<script data-fui-moduledata="local">
195
+ var $moduleUI = ${ir(m,2)};
196
196
  ${Ct(v,"script")}
197
- ${Ct(Js(E.components),"script")}
197
+ ${Ct(zs(P.components),"script")}
198
198
  </script>`;return{html:`<!DOCTYPE html>
199
199
  <html>
200
200
  <head>
@@ -207,8 +207,8 @@ ${Ct(Js(E.components),"script")}
207
207
  ${ne.trim()?`<style data-fui-project-css>
208
208
  ${Ct(ne,"style")}
209
209
  </style>`:""}
210
- ${Fe.trim()?`<style data-fui-module-css>
211
- ${Ct(Fe,"style")}
210
+ ${Le.trim()?`<style data-fui-module-css>
211
+ ${Ct(Le,"style")}
212
212
  </style>`:""}
213
213
  <script>
214
214
  var urlReferrer = '';
@@ -217,7 +217,7 @@ ${Ct(Fe,"style")}
217
217
  var Framework = ${JSON.stringify(a)};
218
218
  </script>
219
219
  <script>
220
- ${Rf(o,Pt,lo)}
220
+ ${xf(o,Et,uo)}
221
221
  </script>
222
222
  </head>
223
223
  <body>
@@ -230,27 +230,27 @@ ${Rf(o,Pt,lo)}
230
230
  ${C.scripts.join(`
231
231
  `)}
232
232
  ${I?"":`<script>
233
- var $projectGroupSetting = ${rr(nt,2)};
233
+ var $projectGroupSetting = ${ir(nt,2)};
234
234
  </script>`}
235
235
  ${Tn}
236
- ${mo}
236
+ ${ho}
237
237
  </body>
238
- </html>`,moduleInfo:o,basePath:Pt,origin:d,apiDomain:Ce,apiDomainOrigin:Et,userInfoUrl:Je,previewUser:lo.user,importsLoaded:C.loaded,importProblems:C.problems,frameworkNote:S,htmlOnly:I,projectBundleNotes:I?["projectdata/StyleCSS: KHÔNG nhúng (HTMLOnly=true — server cũng không nhúng)"]:A?x.notes:[...t.forceLocalProjectBundle?[]:x.notes,t.forceLocalProjectBundle?"projectdata/StyleCSS: dựng từ workspace local (chế độ preview cấp component — bản local chưa push vẫn hiện)":"projectdata/StyleCSS: dựng từ workspace local (không tải được bản server)"],rightDefaultNote:po,undefinedRightsNote:uo,warnings:n}}function Ef(e,t,n){let o=[t,n];for(let s of o){let r=Object.keys(e).find((i)=>i===s);if(r??=Object.keys(e).find((i)=>s.endsWith(i)||i.endsWith(s)),r??=Object.keys(e).find((i)=>s.includes(i)||i.includes(s)),r!==void 0)return{key:r,value:e[r]}}return}var nr={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS","Access-Control-Allow-Headers":"*"};function Pf(e){if(!e)return null;try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return null}}async function Nf(e,t,n,o,s,r,i,a){if(!o||n!==o)return null;let c=ro(s);if(!c)return null;let l=await jl(t,c.alias);if(!l)return null;let u=await Il(t,l,c.fnName);if(!u)return null;let d=`${l}:${u}`,p=r.get(d);if(p===void 0)p=await Cl(t,l,u),r.set(d,p);if(p==="safe-read"&&i.count<Jt){i.count++;try{let y;if(!c.isAuth){let{config:N}=await Sl(t,{db:l});if(N.userToken)y={...await e.request().allHeaders(),authorization:`Bearer ${N.userToken}`}}let v=await e.fetch({timeout:tr,headers:y});if(v.ok()){let N=await v.json().catch(()=>null);if(N!==null)return await Ws(t,l,u,"live",N),a.live++,{payload:N,note:`gọi tAPI thật (SP ${u}, ${l})`}}}catch{}}let h=await Rl(t,l,u);if(h)return a.cache++,{payload:h.data,note:`_testdata cache (SP ${u}, ${l}, nguồn: ${h.source})`};let w=await Tl(t,l,u);if(w)return await Ws(t,l,u,"generated",w),a.generated++,{payload:w,note:`tự sinh mock (SP ${u}, ${l}), đã lưu cache`};return null}async function sr(e,t){let{html:n,moduleInfo:o,basePath:s,origin:r,apiDomain:i,apiDomainOrigin:a,userInfoUrl:c,previewUser:l,importsLoaded:u,importProblems:d,frameworkNote:p,htmlOnly:h,projectBundleNotes:w,rightDefaultNote:y,undefinedRightsNote:v,warnings:N}=await Tf(e,t),I=await hf(),g=[],b=new Set(N),R=new Set,C=[],S=[],D=[],x=[],A={revalidated:0,updated:0,fetched:0,memo:0,stale:0},E=new Set,te=new Set,ve=!1,$e=new Map,V={count:0},de=Pf(i),ge={live:0,cache:0,generated:0},Be=t.urlParams&&Object.keys(t.urlParams).length?"?"+new URLSearchParams(t.urlParams).toString():"",tt=`${r}${s}${Be}`,Ce=(()=>{try{return c?new URL(c).pathname:""}catch{return""}})(),Et;try{Et=await I.chromium.launch({headless:!0})}catch(Je){let nt=Je.message;if(/executable doesn't exist|Executable doesn't exist|browserType.launch/i.test(nt))throw Error(`Không launch được Chromium: ${nt}
239
- → Chạy một lần: npx playwright install chromium`);throw Je}try{let Je=t.device??"desktop",nt=af[Je],Pt=t.viewport??nt.viewport,Y=await Et.newPage({viewport:Pt,isMobile:nt.isMobile,hasTouch:nt.hasTouch,userAgent:nt.userAgent,deviceScaleFactor:1}),co=Math.min(Math.max(t.settleMs??of,100),5000);Y.on("pageerror",(P)=>{C.push(String(P?.message??P))}),Y.on("console",(P)=>{let W=P.type(),q=P.text();if(W==="error"){if(!/Failed to load resource/i.test(q))C.push(q)}else if(W==="warning"&&/\[Vue warn\]|\[Vuetify\]/i.test(q))S.push(q)}),await Y.route("**/*",async(P)=>{let W=P.request(),q=W.url(),me=W.method(),O=W.resourceType();if(W.isNavigationRequest()){if(q.startsWith(r))await P.fulfill({status:200,contentType:"text/html; charset=utf-8",body:n});else b.add(`runtime điều hướng ra "${q}" — đã chặn (thường do login gate / redirect trong action)`),await P.fulfill({status:200,contentType:"text/html; charset=utf-8",body:`<html><body>[fui-render] navigation blocked: ${jn(q)}</body></html>`});return}let M;try{M=new URL(q)}catch{await P.fulfill({status:400,contentType:"text/plain",body:""});return}if(me==="OPTIONS"){await P.fulfill({status:204,headers:nr,body:""});return}if(M.pathname==="/favicon.ico"){await P.fulfill({status:204,body:""});return}let we=J.extname(M.pathname).toLowerCase();if(rf.has(O)||me==="GET"&&sf.has(we)){try{let K=await bf(q,{refresh:t.refreshAssets,offline:t.offline});if(A[K.origin]++,K.origin==="stale")E.add(`${O} "${q}" — ${K.note}`);else if(K.changed)te.add(`${q} — ${K.note}`);await P.fulfill({status:200,contentType:K.contentType,body:K.body,headers:nr})}catch(K){let be=`${O} "${q}" tải thất bại (${K.message})`;if(O==="script"||O==="stylesheet")R.add(be);else b.add(`${be} — trả 404 (offline + chưa cache?)`);await P.fulfill({status:404,contentType:"text/plain",body:""})}return}D.push(`${me} ${q}`);let ye=Ef(t.apiMocks,q,M.pathname),Z;if(ye)Z=ye.value,g.push(`API ${me} ${M.pathname} → mock "${ye.key}"`);else if(Ce&&(q===c||M.pathname===Ce))Z=l,g.push(`API ${me} ${M.pathname} → user preview (userInfo của project, auto-mock)`);else{let K=await Nf(P,o.ProjectID,M.hostname,de,M.pathname,$e,V,ge);if(K)Z=K.payload,g.push(`API ${me} ${M.pathname} → ${K.note}`);else Z={data:[]},b.add(`không có mock cho API "${q}" — trả {data: []}`),g.push(`API ${me} ${M.pathname} → {data:[]} (không có mock)`)}if(j(Z)&&Z.__error!==void 0){let K=j(Z.__error)?Z.__error:{};await P.fulfill({status:Number(K.status??500),contentType:"application/json; charset=utf-8",headers:nr,body:JSON.stringify(K.responseJSON??{Message:"Simulated error"})});return}await P.fulfill({status:200,contentType:"application/json; charset=utf-8",headers:nr,body:JSON.stringify(Z)})});let Rn=Y;if(typeof Rn.routeWebSocket==="function")await Rn.routeWebSocket("**/*",(P)=>{b.add("module mở WebSocket tới backend thật — đã chặn (renderUI chỉ cho phép gọi thật qua tAPI SP đã xác nhận an toàn, không qua WebSocket)"),P.close()});g.push("── INIT ──");let zt=Date.now();if(await Y.goto(tt,{waitUntil:"networkidle",timeout:nf}),!h)try{await Y.waitForFunction("!!window.vm || !!document.querySelector('#mainContainer, #fastProjectAPP .v-application')",void 0,{timeout:15000})}catch{b.add("Vue chưa mount sau 15s — trang chưa init xong (xem JS error / asset hỏng / API treo bên dưới)")}await Y.waitForTimeout(co);let lo=[A.revalidated?`${A.revalidated} revalidate 304`:"",A.updated?`${A.updated} CẬP NHẬT`:"",A.fetched?`${A.fetched} tải mới`:"",A.memo?`${A.memo} memo`:"",A.stale?`${A.stale} STALE`:""].filter(Boolean).join(" + ")||"0",uo=[ge.live?`${ge.live} gọi thật`:"",ge.cache?`${ge.cache} cache`:"",ge.generated?`${ge.generated} tự sinh`:""].filter(Boolean).join(" + ");g.push(`trang load xong sau ${((Date.now()-zt)/1000).toFixed(1)}s · asset: ${lo} · ${D.length} API call${uo?` (API không-mock: ${uo})`:""}`);let po=C.length,Ke=J.join(e,"_preview");await mt.mkdir(Ke,{recursive:!0});for(let P=0;P<t.scenario.length;P++){let W=t.scenario[P],q=P+1,me=C.length;try{if(j(W.set))g.push("",`── STEP ${q}: set ${Object.keys(W.set).join(", ")} ──`),await Y.evaluate((O)=>{let M=window,we=(Z,K,be)=>{if(M.Vue&&typeof M.Vue.set==="function")M.Vue.set(Z,K,be);else Z[K]=be},ye=(Z,K,be)=>{let xe=String(K).match(/[^.[\]']+/g)||[K],B=Z;for(let Q=0;Q<xe.length-1;Q++){if(B[xe[Q]]==null)we(B,xe[Q],/^\d+$/.test(xe[Q+1])?[]:{});B=B[xe[Q]]}we(B,xe[xe.length-1],be)};for(let[Z,K]of O)ye(M.vueData,Z,K)},Object.entries(W.set));else if(typeof W.click==="string"){g.push("",`── STEP ${q}: click "${W.click}" ──`);let O=W.click,M=typeof W.nth==="number"?W.nth-1:0,we=Y.locator(".v-btn, button, [role='button'], .v-list-item, .v-tab, a",{hasText:O}),ye=await we.count();if(ye===0)we=Y.locator(`text=${O}`),ye=await we.count();if(ye===0)b.add(`STEP ${q}: không tìm thấy phần tử có text "${O}" trên DOM`),ve=!0;else{if(ye>1)g.push(`(khớp ${ye} phần tử — dùng #${M+1})`);await we.nth(Math.min(M,ye-1)).click({timeout:Gs}),await Y.waitForTimeout(350);let Z=Y.locator(".jconfirm .jconfirm-buttons button"),K=await Z.count();if(K>0){let be=W.cancelConfirm===!0,xe=!1;for(let B=0;B<K;B++){let Q=(await Z.nth(B).innerText()).trim().toLowerCase(),re=/hủy|huỷ|cancel|không|no|đóng/.test(Q);if(be===re){g.push(`CONFIRM → bấm "${Q}"`),await Z.nth(B).click({timeout:Gs}),xe=!0;break}}if(!xe)g.push(`CONFIRM → bấm nút ${be?"cuối":"đầu"} (không nhận diện được label)`),await Z.nth(be?K-1:0).click({timeout:Gs})}}}else if(typeof W.call==="string")g.push("",`── STEP ${q}: call ${W.call} ──`),await Y.evaluate((O)=>{let M=window;if(typeof M.CALL!=="function")throw Error("global CALL không tồn tại trong runtime này");let we=M.vueData&&M.vueData[O]!==void 0?M.vueData[O]:O;M.CALL(we)},W.call);else if(typeof W.exe==="string")g.push("",`── STEP ${q}: exe ──`),await Y.evaluate((O)=>{Function(O)()},W.exe);else if(typeof W.assert==="string"){let O=typeof W.label==="string"?W.label:W.assert,M=!1;try{M=await Y.evaluate((we)=>{try{return!!Function("with(window.vueData||{}){return ("+we+")}")()}catch{return!1}},W.assert)}catch{M=!1}x.push({label:O,ok:M}),g.push("",`── STEP ${q}: assert ${M?"✔":"✘"} ${O} ──`);continue}else{b.add(`event ${q} không hợp lệ — cần một trong: set / click / call / exe / assert`);continue}await Y.waitForTimeout(co);for(let O=me;O<C.length;O++)g.push(`✘ pageerror: ${C[O]}`);if(t.screenshotSteps){let O=J.join(Ke,`render-step-${q}.png`);await Y.screenshot({path:O}),g.push(`screenshot: ${O}`)}}catch(O){ve=!0,g.push(`✘ STEP ${q} lỗi: ${O.message.split(`
240
- `)[0]}`)}}let ne=await Y.evaluate(()=>{let P=document.querySelector("#fastProjectAPP"),W=P?P.children.length:Array.from(document.body.children).filter((B)=>!/^(SCRIPT|STYLE|LINK|TEMPLATE|NOSCRIPT)$/.test(B.tagName)).length,q=window.innerWidth,me=document.documentElement.scrollWidth>q+4,O=[],M=(B)=>B&&typeof B.className==="string"?B.className.trim().split(/\s+/).slice(0,2).join("."):"";if(me)for(let B of document.body.querySelectorAll("*")){let Q=B.getBoundingClientRect();if(Q.width>0&&Q.right>q+4&&O.length<5){let re=M(B);O.push(B.tagName.toLowerCase()+(re?"."+re:"")+" (right="+Math.round(Q.right)+"px)")}}let we=document.body.innerText||"",ye=[];for(let B of["undefined","NaN","[object Object]"])if(we.includes(B))ye.push(B);let Z=[];for(let B of document.querySelectorAll("img"))if(B.complete&&B.naturalWidth===0&&B.getBoundingClientRect().width>0&&Z.length<5)Z.push((B.getAttribute("src")||"(no src)").slice(0,120));let K=[];for(let B of document.querySelectorAll("canvas")){let Q=B.getBoundingClientRect();if((Q.width===0||Q.height===0)&&K.length<5){let re=B.parentElement,se=M(re);K.push("canvas trong "+(re?re.tagName.toLowerCase():"?")+(se?"."+se:"")+" ("+Math.round(Q.width)+"×"+Math.round(Q.height)+")")}}let be=[];{let B=(re)=>{let se="";for(let Ge of re.childNodes)if(Ge.nodeType===Node.TEXT_NODE)se+=Ge.textContent||"";return se.trim()},Q=[];for(let re of document.body.querySelectorAll("*")){if(Q.length>=600)break;let se=B(re);if(!se)continue;let Ge=re.getBoundingClientRect();if(Ge.width<=0||Ge.height<=0)continue;Q.push({el:re,rect:Ge,text:se.slice(0,24)})}Q.sort((re,se)=>re.rect.top-se.rect.top||re.rect.left-se.rect.left);e:for(let re=0;re<Q.length;re++){let se=Q[re];for(let Ge=re+1;Ge<Q.length;Ge++){let ze=Q[Ge];if(ze.rect.top>se.rect.bottom+4)break;if(se.el.contains(ze.el)||ze.el.contains(se.el))continue;let pd=se.rect.top+se.rect.height/2,md=ze.rect.top+ze.rect.height/2;if(Math.abs(pd-md)>Math.min(se.rect.height,ze.rect.height)*0.6)continue;let gr=se.rect.left<=ze.rect.left?ze.rect.left-se.rect.right:se.rect.left-ze.rect.right;if(gr<2){let hd=gr<0?"chồng lấn":`dính sát (gap ${Math.round(gr)}px)`;if(be.push(`${hd}: "${se.text}" ↔ "${ze.text}"`),be.length>=6)break e}}}}let xe=!!document.querySelector(".v-dialog--active, .v-overlay--active");return{appChildCount:W,overflowX:me,overflowers:O,suspectTexts:ye,brokenImages:Z,deadCanvas:K,textCollisions:be,dialogOpen:xe}}),Fe=[];if(ne.appChildCount===0)Fe.push((h?"✘ <body> không render được phần tử nào (HTMLOnly — body.html LÀ cả trang)":"✘ #fastProjectAPP rỗng — controls không render")+(d.length>0||R.size>0?" (CÓ file toàn cục nạp thiếu — xem mục MÔI TRƯỜNG THIẾU FILE trước khi nghi module.json)":" (kiểm tra pageerror / module.json)"));if(ne.overflowX)Fe.push(`⚠ trang tràn ngang viewport ${Pt.width}px — phần tử tràn: ${ne.overflowers.join(", ")||"(không xác định)"}`);if(ne.suspectTexts.length>0)Fe.push(`⚠ text nghi vấn hiển thị trên trang: ${ne.suspectTexts.map((P)=>`"${P}"`).join(", ")} (binding thiếu data?)`);if(ne.brokenImages.length>0)Fe.push(`⚠ ảnh vỡ (load fail nhưng đang chiếm chỗ): ${ne.brokenImages.join(", ")}`);if(ne.deadCanvas.length>0)Fe.push(`⚠ canvas chart 0-size — chart init khi container chưa có kích thước: ${ne.deadCanvas.join(", ")}`);if(ne.textCollisions.length>0)Fe.push(`⚠ text dính/chồng lấn (thẩm mỹ — PHẢI sửa, không được bỏ qua): ${ne.textCollisions.join("; ")}`);let Tn=J.join(Ke,"render.png"),mo=await Y.screenshot({path:Tn}),En,oe="image/png";if(mo.length<=Fl)En=mo.toString("base64");else try{let P=await Y.screenshot({type:"jpeg",quality:60});if(P.length<=Fl)En=P.toString("base64"),oe="image/jpeg"}catch{}try{await Y.screenshot({path:J.join(Ke,"render-full.png"),fullPage:!0})}catch{}let De;if(ne.dialogOpen)try{De=J.join(Ke,"render-dialog.jpg"),await Y.locator(".v-dialog--active, .v-overlay--active").first().screenshot({path:De,type:"jpeg",quality:60,timeout:3000})}catch{De=void 0}let Xt=[];try{Xt=await Y.evaluate(()=>{let P=new Set(["p_domain","p_routers","p_params","p_urlReferrer","p_router","p_path","p_render","p_module_id","v_Loading","v_LoadingOverlay","v_containerOn","user","v_Set"]),W=[],q=window.vueData||{};for(let me of Object.keys(q)){if(P.has(me)||W.length>=40)continue;let O=q[me];if(O&&typeof O==="object"&&!Array.isArray(O)&&(O.API!==void 0||O.CALL!==void 0||O.EXE!==void 0||O.FUN!==void 0||O.IF!==void 0))continue;let M;try{M=JSON.stringify(O)}catch{M=String(O)}if(M===void 0)M="undefined";if(M.length>120)M=M.slice(0,120)+"…";W.push(me+" = "+M)}return W})}catch{}let Re={ts:new Date().toISOString(),jsErrors:C.length,initErrors:po,vueWarns:S.length,envMissing:d.length+R.size,appEmpty:ne.appChildCount===0,overflowX:ne.overflowX,suspectTexts:ne.suspectTexts.length,brokenImages:ne.brokenImages.length,deadCanvas:ne.deadCanvas.length,textCollisions:ne.textCollisions.length,assertFail:x.filter((P)=>!P.ok).length},pe=await lf(Ke,Je),ud=pe!==null&&Ys(Re)&&Jl(pe,Re).length===0;Re.failStreak=!Ys(Re)?0:ud?(pe?.failStreak??1)+1:1,await mt.writeFile(Bl(Ke,Je),JSON.stringify(Re,null,1),"utf8").catch(()=>{return});let ki=[...new Set(C)],vi=[...S],$i=mf(Re,vi,ki),U=[];if(U.push(t.reportLabel?`RENDER (browser thật): ${t.reportLabel}`:`RENDER (browser thật): ${e}`),U.push(...pf(Re,$i,ki,vi)),pe)U.push(uf(pe,Re));if((Re.failStreak??0)>=cf)U.push(...df(Re.failStreak,$i));U.push(""),U.push(`Framework ${o.Framework??"V3"}${h?" · HTMLOnly=true (KHÔNG nhúng framework/projectdata — như production)":""} · viewport ${Pt.width}×${Pt.height}${Je!=="desktop"?` (${Je})`:""} · CDN ${or}`),U.push(`Origin trang: ${r} · apiDomain: ${i||"(trống)"} [${a}]${c?` · userInfo auto-mock: ${c}`:""}`),U.push(p);for(let P of w)U.push(` ✓ ${P}`);U.push(`Import (framework ∪ project ∪ module, sort theo FileSort/FileName): ${u.length} nạp được${d.length?`, ${d.length} KHÔNG nạp được`:""}`);for(let P of u)U.push(` ✓ ${P}`);if(y)U.push(`ℹ ${y}`);if(v)U.push(v);U.push(""),U.push(...g),U.push(""),U.push("═══ KẾT QUẢ ═══");for(let P of x)U.push(`${P.ok?"✔":"✘"} assert: ${P.label}`);if(d.length>0||R.size>0){U.push("","── ⛔ MÔI TRƯỜNG THIẾU FILE (đọc TRƯỚC khi sửa module) ──","Các file dưới đây không nạp được vào trang test. Hàm/biến/style toàn cục do chúng","định nghĩa sẽ không tồn tại → lỗi JS và lỗi hiển thị bên dưới CÓ THỂ chỉ là hệ quả,","không phải lỗi của module. Khắc phục phần này trước rồi render lại.");for(let P of d)U.push(`⛔ ${P}`);for(let P of R)U.push(`⛔ ${P}`)}if(E.size>0){U.push("","── ⛔ ASSET CŨ, KHÔNG XÁC MINH ĐƯỢC (đọc TRƯỚC khi kết luận về platform) ──","Không hỏi được server xem các file dưới đây còn đúng không, nên trang test đang chạy",'bằng bản trên đĩa. Mọi kết luận kiểu "thư viện X có bug" rút ra từ lượt render này',"đều KHÔNG dùng được — bản thật trên CDN có thể đã khác. Nối mạng rồi render lại,","hoặc ép tải mới bằng refreshAssets: true.");for(let P of E)U.push(`⛔ ${P}`)}if(te.size>0){U.push("","── ℹ️ ASSET PLATFORM ĐÃ ĐỔI so với lượt render trước ──","Hành vi đổi mà module.json không đụng tới có thể đến từ đây, không phải từ module.");for(let P of te)U.push(`ℹ️ ${P}`)}if(C.length>0){U.push("","── JS/Vue errors ──");for(let P of[...new Set(C)].slice(0,20))U.push(`✘ ${P.split(`
241
- `)[0].slice(0,300)}`)}if(S.length>0){U.push("","── [Vue warn] ──");for(let P of[...new Set(S)].slice(0,15))U.push(`⚠ ${P.split(`
242
- `)[0].slice(0,300)}`)}if(Fe.length>0)U.push("","── DOM AUDIT ──",...Fe);if(b.size>0){U.push("","── Cảnh báo ──");for(let P of b)U.push(`⚠ ${P}`)}if(Xt.length>0)U.push("","── STATE CUỐI (vueData) ──",...Xt);if(U.push(""),U.push(`Screenshot: ${Tn}${En?" (đính kèm bên dưới)":" (quá lớn để inline — dùng Read tool để xem)"}`),U.push(`Full-page: ${J.join(Ke,"render-full.png")}`),De)U.push(`Dialog đang mở — chụp riêng: ${De}`);U.push(""),U.push(`Thống kê: ${D.length} API call (tất cả mock) · ${C.length} JS error (${po} lúc init) · ${S.length} Vue warn · ${x.filter((P)=>P.ok).length}/${x.length} assert đạt${ne.dialogOpen?" · dialog đang mở":""}`);let dd=x.some((P)=>!P.ok)||ve||C.length>0||ne.appChildCount===0||d.length>0||R.size>0||E.size>0&&!t.offline;return{report:U.join(`
243
- `),hasFailures:dd,screenshotPath:Tn,inlineScreenshotBase64:En,inlineScreenshotMime:oe}}finally{await Et.close().catch(()=>{return})}}var Af="_componentPreview",zl=8;function Df(e){return e.normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/[^a-zA-Z0-9]+/g,"-").replace(/^-+|-+$/g,"").toLowerCase()||"case"}function xf(e){let t={},n={},o=[];for(let[s,r]of Object.entries(e)){if(s.startsWith("@")||s.startsWith("#")){o.push(`prop "${s}": FUI không nhận cú pháp @/# trong JSON — đổi thành "v-on:"/"v-slot:". Đã bỏ qua.`);continue}if(s.startsWith(":")||s.startsWith("v-")){t[s]=r;continue}if(typeof r==="string"){t[s]=r;continue}if(typeof r==="boolean"||typeof r==="number"){t[`:${s}`]=JSON.stringify(r);continue}let i=`cp_${s.replace(/[^a-zA-Z0-9_]/g,"_")}`;n[i]=r,t[`:${s}`]=i}return{attr:t,hoisted:n,notes:o}}function Of(e,t,n){let{attr:o,hoisted:s,notes:r}=xf(n.props??{}),i={...n.vueData??{},...s},a={w:"12",el:e,attr:o};if(n.slot!==void 0)a.innerHTML=n.slot;return{moduleJson:{ModuleName:`preview: ${e} — ${t}`,data:Object.keys(i).length>0?[i]:[],watch:{},controls:[{prop:"fluid pa-0",rows:[{prop:"",cols:[a]}]}],set:{}},notes:r}}async function Xl(e,t){try{await Qe.access(e)}catch{return!1}return await Qe.cp(e,t,{recursive:!0}),!0}async function _f(e,t){let n=$n(e),o=[];if(t){let i=await H(ue.join(Vs(e,t),"_moduleInfo.json"),null);o.push(i?.Framework)}let s=await H(ue.join(n,"_projectInfo.json"),null);if(o.push(s?.Framework),!o.some(Boolean))try{let i=await Qe.readdir(ue.join(n,"modules"),{withFileTypes:!0});for(let a of i){if(!a.isDirectory())continue;let c=await H(ue.join(n,"modules",a.name,"_moduleInfo.json"),null);if(c?.Framework){o.push(c.Framework);break}}}catch{}let r=o.find(Boolean);return String(r??"V3").toUpperCase()==="V2"?"V2":"V3"}async function Yl(e){let t=await vl(),n=e.projectId||t?.projectId;if(!n)throw Error("Không xác định được projectId. Truyền projectId hoặc chạy session_set trước.");let o=e.moduleId||(!e.projectId?t?.moduleId:void 0),s=$n(n),r=o?Vs(n,o):s,i=ue.join(r,"components"),a=o?`module (${o})`:"project (global)",c=ue.join(i,`${e.componentName}.vue`);if(!(await L(c)).trim()){let I=await _e(i);throw Error(`Không tìm thấy ${e.componentName}.vue trong ${i}.`+(I.length?` Có sẵn: ${I.join(", ")}.`:" Thư mục components rỗng.")+(o?"":" Nếu đây là component cấp module, truyền thêm moduleId."))}let u=e.cases&&e.cases.length>0?e.cases:[{name:"default"}],d=e.devices&&e.devices.length>0?e.devices:["desktop","mobile"];if(u.length*d.length>zl)throw Error(`${u.length} case × ${d.length} device = ${u.length*d.length} lần render, vượt giới hạn ${zl}. `+"Mỗi lần render là một phiên browser thật — chia nhỏ thành nhiều lần gọi.");let p=await _f(n,o),h=ue.join(vn(),Af,it(n,"projectId"),it(e.componentName,"componentName")),w=ue.join(h,"_preview");await Qe.mkdir(w,{recursive:!0});let y=[],v=new Set;for(let[I,g]of u.entries()){let b=g.name?.trim()||`case-${I+1}`,R=Df(b),C=ue.join(h,R);await Qe.rm(C,{recursive:!0,force:!0}),await Qe.mkdir(C,{recursive:!0}),await Qe.writeFile(ue.join(C,"_moduleInfo.json"),JSON.stringify({ProjectID:n,ModuleID:`_cpreview_${e.componentName}`,ModuleName:`preview ${e.componentName}`,Framework:p},null,2),"utf-8");let{moduleJson:S,notes:D}=Of(e.componentName,b,g);for(let x of D)v.add(`[${b}] ${x}`);if(await Qe.writeFile(ue.join(C,"module.json"),JSON.stringify(S,null,2),"utf-8"),await Xl(i,ue.join(C,"components")),o)await Xl(ue.join(r,"imports"),ue.join(C,"imports"));for(let x of d){let A=`${e.componentName} · ${b} · ${x}`,E=await sr(C,{scenario:g.scenario??[],apiMocks:g.apiMocks??{},vueData:void 0,user:e.user,device:x,viewport:e.viewport,settleMs:e.settleMs,screenshotSteps:e.screenshotSteps,forceLocalProjectBundle:!0,reportLabel:A}),te=E.screenshotPath;if(E.screenshotPath){let ve=ue.join(w,`${R}-${x}.png`);try{await Qe.rename(E.screenshotPath,ve),te=ve;let $e=ue.join(ue.dirname(E.screenshotPath),"render-full.png");await Qe.rename($e,ue.join(w,`${R}-${x}-full.png`)).catch(()=>{})}catch{}}y.push({caseName:b,device:x,report:E.report,hasFailures:E.hasFailures,screenshotPath:te,inlineScreenshotBase64:E.inlineScreenshotBase64,inlineScreenshotMime:E.inlineScreenshotMime})}}let N=[`PREVIEW COMPONENT: ${e.componentName}`,`Scope: ${a} · Project: ${n} · Framework ${p}`,`Nguồn: ${c}`,`Cases: ${u.map((I)=>I.name?.trim()||"(không tên)").join(", ")} × Devices: ${d.join(", ")} = ${y.length} lần render`,`Ảnh: ${w}`,"","Không gian riêng: chỉ component này + CSS của chính nó + theme/import cấp project.","style.css của module KHÔNG nạp — component phải tự đứng được. Tầng project dựng từ workspace","local nên bản chưa push vẫn hiện; chạy `project_sync` nếu nghi file local đã cũ."];if(v.size>0){N.push("","⚠ Ghi chú dựng harness:");for(let I of v)N.push(` • ${I}`)}return{header:N.join(`
244
- `),runs:y,hasFailures:y.some((I)=>I.hasFailures)}}import ei from"node:vm";var Ql=500,Zl=15,eu=500;function Ze(e){if(typeof e==="string")return`"${e.length>40?e.slice(0,40)+"…":e}"`;return oo(e)}function ti(e){return e.replace(/^`+|`+$/g,"")}function tu(e){let t=e.split(/[\\/]+/).filter(Boolean),n=t.lastIndexOf("modules");return n>0?t[n-1]:null}async function ir(e,t,n){let{loadAllSchemaProcNames:o,getVerifiedSpNames:s}=e;if(!t||Object.keys(t).length===0)return null;let r=tu(n);if(!r)return null;let i=[];try{i=await o(r)}catch{return null}if(i.length===0)return null;let a=await s(),c=kl(Object.keys(t),i,a);if(c.length===0)return null;return["\uD83D\uDEA8 CẤU TRÚC MOCK CHƯA ĐƯỢC KIỂM CHỨNG — nghi ngờ tự suy diễn thay vì đọc từ SP thật:",...c.map((l)=>` - apiMocks["${l.mockKey}"] khớp tên SP CÓ THẬT "${l.spName}" trong schema local, nhưng SP đó chưa từng được db_sp_verify đọc thân trong phiên này.`),` → Chạy db_sp_verify({ name: "${c[0].spName}" }) để đọc thân SP thật rồi COPY NGUYÊN VĂN snippet apiMocks nó sinh sẵn — không tự đoán tên cột/shape cho SP đã tồn tại, kết quả mô phỏng sẽ lệch với thực tế.`].join(`
245
- `)}function Mf(e){let t=[],n=new Set;return Me(e,(o,s,r)=>{if(r)return;let i=o.API;if(typeof i!=="string"||!i||i.includes("`")||i.includes("{{"))return;let a=o.IN;if(a!==void 0&&!(j(a)&&Object.keys(a).length===0))return;if(n.has(i))return;n.add(i),t.push({url:i,method:typeof o.METHOD==="string"?o.METHOD:"POST"})}),t}async function nu(e,t,n,o){let{getConnectionAlias:s,resolveApiSpName:r,writeTestData:i,classifySpSafety:a,callLiveApiSp:c,readTestData:l,generateMockForLocalSp:u}=e,d={...o},p=[],h=tu(n);if(!h)return{mocks:d,trace:p};let w=Mf(t);if(w.length===0)return{mocks:d,trace:p};let y=0;for(let{url:v,method:N}of w){let I=ro(v);if(!I)continue;let g=await s(h,I.alias);if(!g)continue;let b=await r(h,g,I.fnName);if(!b)continue;let R=Object.keys(o).find((A)=>A===v);if(R??=Object.keys(o).find((A)=>v.endsWith(A)||A.endsWith(v)),R??=Object.keys(o).find((A)=>v.includes(A)||A.includes(v)),R!==void 0){await i(h,g,b,"user",o[R]);continue}let C=await a(h,g,b),S=!1;if(C==="safe-read"&&y<Jt){y++;let A=await c(h,g,I.isAuth,I.fnName,N);if(A.ok)d[v]=A.data,p.push(`live ${v} → SP ${b} (${g}) — gọi tAPI thật thành công`),await i(h,g,b,"live",A.data),S=!0;else p.push(`⚠ live ${v} → SP ${b} (${g}) gọi thật thất bại (${A.error??`HTTP ${A.status}`}) — rơi xuống cache/tự sinh`)}else if(C==="unsafe-write")p.push(`skip ${v} → SP ${b} (${g}) có lệnh ghi — KHÔNG gọi thật, dùng cache/tự sinh`);else if(C==="opaque")p.push(`skip ${v} → SP ${b} (${g}) có đoạn đọc tĩnh không nhìn xuyên qua được (SQL động / gọi SP khác) — KHÔNG gọi thật, dùng cache/tự sinh`);else if(C==="unknown")p.push(`skip ${v} → SP ${b} (${g}) chưa có thân SP local để phân loại an toàn — KHÔNG gọi thật, dùng cache/tự sinh`);else if(y>=Jt)p.push(`skip ${v} → SP ${b} (${g}) đã đạt trần ${Jt} lượt gọi thật/lượt simulate — dùng cache/tự sinh`);if(S)continue;let D=await l(h,g,b);if(D){d[v]=D.data,p.push(`cache ${v} → SP ${b} (${g}) — dùng _testdata cache (nguồn: ${D.source}, lưu lúc ${D.savedAt})`);continue}let x=await u(h,g,b);if(x)d[v]=x,p.push(`gen ${v} → SP ${b} (${g}) — tự sinh mock từ cấu trúc SELECT, đã lưu cache`),await i(h,g,b,"generated",x)}return{mocks:d,trace:p}}class ni{vueData;moduleJson;opts;sandbox;ctx;trace=[];warnings=new Set;depth=0;apiCallCount=0;toastCount=0;watchTriggerCount=0;assertResults=[];dialogs=[];watchSnapshots=new Map;stubUsed=new Set;scannedFunctionRightCodes;systemRightDefaulted;functionRightDefaulted;constructor(e,t){this.moduleJson=e,this.opts=t,this.systemRightDefaulted=!("SystemRight"in(t.user??{})),this.functionRightDefaulted=!("FunctionRight"in(t.user??{})),this.scannedFunctionRightCodes=qt([JSON.stringify(e)]),this.vueData={p_domain:"sim.local",p_routers:[],p_params:"",p_urlReferrer:"",p_router:"/sim/module",v_Loading:!1,v_LoadingOverlay:[],v_containerOn:!0,user:{UserID:null,UserName:null,LastName:null,FirstName:null,Sex:null,Phone:null,Email:null,DepartmentID:null,GroupID:null,SystemRight:9,FunctionRight:this.scannedFunctionRightCodes,LastLogin:null,...t.user??{}},v_Set:{title:null,apiDomain:"",login:!1,menu:[],userInfo:""}},this.sandbox=this.buildSandbox(),this.ctx=ei.createContext(this.sandbox)}log(e){if(this.trace.length===Ql)this.trace.push("… (trace bị cắt — quá dài)");if(this.trace.length>Ql)return;this.trace.push(" ".repeat(Math.min(this.depth,6))+e)}warn(e){this.warnings.add(e),this.log(`⚠ ${e}`)}rightTest(e){if(!j(e))return!0;let t=this.vueData.user??{};for(let[n,o]of Object.entries(e)){let s=Array.isArray(o)?o:[o],r=t[n],i=Array.isArray(r)?r:[r];if(!s.some((a)=>i.includes(a)))return!1}return!0}buildSandbox(){let e=this,t=(r,i)=>(...a)=>(e.stubUsed.add(r),e.log(`FUN ${r}(${a.map((c)=>Ze(c)).join(", ")}) — stub, không mô phỏng UI thật`),i),n=(r)=>{e.stubUsed.add("moment");let i=r!==void 0&&r!==null?new Date(r):new Date,a=(u)=>String(u).padStart(2,"0"),c={},l=(u)=>{let d=`${i.getFullYear()}-${a(i.getMonth()+1)}-${a(i.getDate())}`;if(!u)return`${d}T${a(i.getHours())}:${a(i.getMinutes())}:${a(i.getSeconds())}`;return u.replace(/YYYY/g,String(i.getFullYear())).replace(/MM/g,a(i.getMonth()+1)).replace(/DD/g,a(i.getDate())).replace(/HH/g,a(i.getHours())).replace(/mm/g,a(i.getMinutes())).replace(/ss/g,a(i.getSeconds()))};return Object.assign(c,{format:l,add:()=>c,subtract:()=>c,startOf:()=>c,endOf:()=>c,valueOf:()=>i.getTime(),toDate:()=>i,toString:()=>l(),year:()=>i.getFullYear(),month:()=>i.getMonth(),date:()=>i.getDate(),daysInMonth:()=>new Date(i.getFullYear(),i.getMonth()+1,0).getDate(),locale:()=>c,isValid:()=>!isNaN(i.getTime())}),c},o={forEach:(r,i)=>{if(Array.isArray(r))r.forEach((a,c)=>i(a,c));else if(j(r))Object.entries(r).forEach(([a,c])=>i(c,a));return r},map:(r,i)=>{let a=Array.isArray(r)?r:j(r)?Object.values(r):[];if(typeof i==="string")return a.map((c)=>c?.[i]);return a.map(i)},filter:(r,i)=>Array.isArray(r)?r.filter(i):[],find:(r,i)=>Array.isArray(r)?r.find(i):void 0,findIndex:(r,i)=>Array.isArray(r)?r.findIndex(i):-1,includes:(r,i)=>Array.isArray(r)||typeof r==="string"?r.includes(i):!1,cloneDeep:(r)=>r===void 0?void 0:JSON.parse(JSON.stringify(r)),isArray:Array.isArray,isString:(r)=>typeof r==="string",isNumber:(r)=>typeof r==="number",isPlainObject:j,isEmpty:(r)=>r==null||typeof r==="string"&&r===""||Array.isArray(r)&&r.length===0||j(r)&&Object.keys(r).length===0,isUndefined:(r)=>r===void 0,keys:(r)=>j(r)?Object.keys(r):[],values:(r)=>j(r)?Object.values(r):[],uniq:(r)=>[...new Set(r)],uniqBy:(r,i)=>{let a=new Set;return(r??[]).filter((c)=>{let l=c?.[i];if(a.has(l))return!1;return a.add(l),!0})},sumBy:(r,i)=>(r??[]).reduce((a,c)=>a+Number(typeof i==="string"?c?.[i]??0:i(c)),0),maxBy:(r,i)=>(r??[]).reduce((a,c)=>a===void 0||Number(c?.[i])>Number(a?.[i])?c:a,void 0),minBy:(r,i)=>(r??[]).reduce((a,c)=>a===void 0||Number(c?.[i])<Number(a?.[i])?c:a,void 0),orderBy:(r,i,a)=>{let c=Array.isArray(i)?i:[i],l=Array.isArray(a)?a:[a??"asc"];return[...r??[]].sort((u,d)=>{for(let p=0;p<c.length;p++){let h=u?.[c[p]],w=d?.[c[p]];if(h===w)continue;let y=h>w?1:-1;return(l[p]??"asc")==="desc"?-y:y}return 0})},sortBy:(r,i)=>o.orderBy(r,i),groupBy:(r,i)=>{let a={};return(r??[]).forEach((c)=>{let l=String(c?.[i]);(a[l]??=[]).push(c)}),a},without:(r,...i)=>(r??[]).filter((a)=>!i.includes(a)),intersection:(r,i)=>(r??[]).filter((a)=>(i??[]).includes(a)),castArray:(r)=>Array.isArray(r)?r:[r],trim:(r,i)=>{if(typeof r!=="string")return r;if(!i)return r.trim();let a=i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return r.replace(new RegExp(`^[${a}]+|[${a}]+$`,"g"),"")},replace:(r,i,a)=>String(r).replace(i,a),split:(r,i,a)=>String(r).split(i,a),startsWith:(r,i)=>String(r).startsWith(i),endsWith:(r,i)=>String(r).endsWith(i),concat:(...r)=>[].concat(...r),take:(r,i)=>(r??[]).slice(0,i),last:(r)=>(r??[])[(r??[]).length-1],head:(r)=>(r??[])[0],first:(r)=>(r??[])[0],round:(r,i=0)=>Math.round(r*10**i)/10**i,assignIn:Object.assign,assign:Object.assign,at:(r,i)=>[e.getPath(r,i)],property:(r)=>(i)=>e.getPath(i,r)},s=new Proxy(o,{get(r,i){if(i in r)return r[i];return(...a)=>{e.warn(`_.${i} chưa hỗ trợ trong simulator — trả undefined`);return}}});return{vueData:this.vueData,console:{log:(...r)=>e.log(`console.log: ${r.map((i)=>typeof i==="string"?i:Ze(i)).join(" ")}`),error:(...r)=>e.log(`console.error: ${r.map((i)=>typeof i==="string"?i:Ze(i)).join(" ")}`),warn:(...r)=>e.log(`console.warn: ${r.map((i)=>typeof i==="string"?i:Ze(i)).join(" ")}`)},JSON,Math,Date,Array,Object,String,Number,Boolean,RegExp,parseInt,parseFloat,isNaN,isFinite,encodeURIComponent,decodeURIComponent,encodeURI,decodeURI,moment:n,_:s,CALL:(r,i)=>e.runAction(r,i),groupBy:(r,i,a)=>{let c=Array.isArray(a)?a:[a],l={};return(r??[]).forEach((u)=>{let d=String(u?.[i]);(l[d]??=[]).push(u)}),Object.entries(l).map(([u,d])=>{let p={[i]:u,count:d.length};for(let h of c)if(h)p[h]=d.reduce((w,y)=>w+Number(y?.[h]??0),0);return p})},buildHeader:(r,i)=>{let a=[];if(i)(Array.isArray(r)?r:[]).forEach((c)=>a.push({text:c[i.text],value:c[i.value],divider:!0}));else{let c=Array.isArray(r)?r[0]:j(r)&&Array.isArray(r.data)?r.data[0]:void 0;if(j(c))for(let[l,u]of Object.entries(c))a.push(typeof u==="number"?{el:"t-num",text:l,value:l,divider:!0}:{text:l,value:l,divider:!0})}return a},findInArray:(r)=>(r?.src??[]).find((i)=>i?.[r.key]===r.value),openWindow:(r)=>{e.stubUsed.add("openWindow"),e.log(`FUN openWindow id=${String(r?.id??"?")} url=${String(r?.url??"?")} — stub (sub-window không mô phỏng)`)},showMessage:(r)=>{if(e.log(`MESSBOX/showMessage "${String(r?.title??"")}"${r?.message?` — ${String(r.message)}`:""}`),r?.onclose)e.log("(giả lập user đóng dialog → chạy onclose)"),e.runAction(r.onclose)},confirm:(r)=>{if(e.log(`CONFIRM "${String(r?.title??"")}" → OK (mặc định simulator)`),typeof r?.action==="function")r.action()},ajaxCALL:t("ajaxCALL"),jsonToExcel:t("jsonToExcel"),printPDF:t("printPDF"),copyToClipboard:t("copyToClipboard"),redirect:t("redirect"),reload:t("reload"),logout:t("logout"),webSocket_Send:t("webSocket_Send"),webSocketJoinGroup:t("webSocketJoinGroup"),appCommand:t("appCommand"),pushRouter:t("pushRouter"),capacityText:(r)=>`${r}`,generateID:()=>`sim${Math.floor(Math.random()*1e6)}`,rightTest:(r)=>e.rightTest(r),fillData:t("fillData"),chartDataBuild:t("chartDataBuild"),fixURL:(r)=>r,alert:t("alert")}}evalExpr(e,t){return this.sandbox.__scope__=t,this.sandbox.__ret__=void 0,ei.runInContext(`with(__scope__) { __ret__ = ${e} }`,this.ctx,{timeout:eu}),this.sandbox.__ret__}evalJs(e,t){this.sandbox.__scope__=t,ei.runInContext(`with(__scope__) { ${e} }`,this.ctx,{timeout:eu})}bindDataToString(e,t){if(typeof e!=="string")return e;return e.replace(/{{([\s\S]+?)}}/g,(n,o)=>{let s=this.evalExpr(o,t);return s===void 0||s===null?"":String(s)})}getPath(e,t){let n=String(t).match(/[^.[\]]+/g)??[],o=e;for(let s of n){if(o==null)return;o=o[s]}return o}getVueData(e,t){if(typeof e==="string"){if(e==="")return"";let n=e.replace("vueData.","").trim();if(n[0]==="`")return ti(this.bindDataToString(n,t));let o=n;if(/^{{.*}}$/gm.test(o))o=o.replace(/^{{|}}$/gm,"");try{return this.evalExpr(o,t)}catch{if(/({{.*}}|[ ])/gm.test(n))try{return ti(this.bindDataToString(n,t))}catch{return}return}}else if(j(e)&&e.ARRAY&&e.COL){let n=this.getPath(t,String(e.ARRAY));return Array.isArray(n)?n.map((o)=>this.getPath(o,String(e.COL))):[]}return e}setValue(e,t,n,o,s){if(Array.isArray(t)){if(!j(e)&&!Array.isArray(e))return;let r=e,i=s??0,a=t[i];if(typeof a==="string"&&a[a.length-1]==="]")if(a=a.replaceAll("]",""),/^[0-9]*$/gm.test(a))a=parseInt(a,10);else if(a[0]==="'")a=a.replaceAll("'","");else a=this.getVueData(a,o??this.vueData);if(t.length-1===i){r[a]=n;return}if(typeof r[a]>"u"){let c=t[i+1];r[a]=c[c.length-1]==="]"?[]:{}}this.setValue(r[a],t,n,o,i+1)}else if(typeof t==="string")if(t[0]==="`"||t[0]==="'")this.setValue(e,[t.replaceAll("'","").replaceAll("`","")],n,o,0);else this.setValue(e,t.match(/[^.[]+/g)??[t],n,o,0)}mapData(e,t,n,o){if(typeof e==="string"){this.setValue(t,e,n);return}if(!j(e))return;for(let[s,r]of Object.entries(e)){if(o)n={...this.vueData,...o};let i;try{i=this.bindDataToString(s,this.vueData)}catch{i=s}if(typeof r==="string"&&r[0]==="#")this.warn(`đọc cross-window "${r}" — không mô phỏng được, gán undefined`),this.setValue(t,i,void 0);else if(i[0]==="#")this.log(`→ ghi cross-window ${i} = ${Ze(this.getVueData(r,n))} (không mô phỏng)`);else this.setValue(t,i,this.getVueData(r,n),n)}}defProp(e){if(typeof e==="string"){if(typeof this.vueData[e]>"u")this.vueData[e]=[]}else if(j(e)){for(let t of Object.keys(e))if(typeof this.vueData[t]>"u")this.vueData[t]=void 0}}runAction(e,t){if(!e)return;let n={...this.vueData,...t??{}};if(typeof e==="string"){let s=this.getVueData(e,n);if(!s){this.warn(`Action "${e}" undefined — CALL trỏ tới action không tồn tại?`);return}e=s}if(!e)return;let o=Array.isArray(e)?e:[e];for(let s of o)if(j(s)&&s.CONFIRM){let r;try{r=this.bindDataToString(String(s.CONFIRM),n)}catch{r=String(s.CONFIRM)}let i=t?.__cancelConfirm===!0;if(this.log(`CONFIRM "${r}" → ${i?"HỦY":"OK"}`),i){if(s.CANCEL)this.vueActionSafe(s.CANCEL,t)}else{let a=JSON.parse(JSON.stringify(s));delete a.CONFIRM,delete a.CANCEL,this.vueActionSafe(a,t)}}else this.vueActionSafe(s,t)}vueActionSafe(e,t){this.depth++;try{this.vueAction(e,t)}catch(n){this.warn(`Lỗi khi chạy action: ${n.message} — action: ${Ae(e)}`)}finally{this.depth--}}vueAction(e,t){if(!e)return;if(typeof e==="string"){this.log(`EXE ${e}`),this.evalJs(e,{...t??{},...this.vueData});return}if(!j(e))return;let n=JSON.parse(JSON.stringify(e)),o={...this.vueData,...t??{}};if(n.IN!==void 0)if(typeof n.IN==="string")this.mapData({IN:n.IN},n,o);else{let r={};this.mapData(n.IN,r,o),n.IN=r}if(n.API!==void 0){try{n.API=this.bindDataToString(String(n.API),o)}catch{}if(n.HEADER!==void 0)if(typeof n.HEADER==="string")this.mapData({HEADER:n.HEADER},n,o);else this.mapData(n.HEADER,n.HEADER,o);this.defProp(n.OUT),this.callAPI(n,t);return}if(n.CALL!==void 0){this.log(`CALL ${String(n.CALL)}`),this.runAction(n.CALL,t);return}if(n.EXE!==void 0){this.log(`EXE ${String(n.EXE).replace(/\s+/g," ").slice(0,100)}`),this.evalJs(String(n.EXE),{...t??{},...this.vueData});return}if(n.FUN!==void 0){let r;try{r=this.bindDataToString(String(n.FUN),o)}catch{r=String(n.FUN)}let i=this.sandbox[r];if(typeof i==="function"){let a=i(n.IN);if(n.OUT!==void 0)this.mapData(n.OUT,this.vueData,a);if(n.CALLBACK!==void 0)this.runAction(n.CALLBACK,t)}else this.warn(`FUN "${r}" không tồn tại (simulator chỉ có các hàm FUI chuẩn)`);return}if(n.IF!==void 0){let r=!1;try{r=String(this.evalExpr(String(n.IF),{...t??{},...this.vueData}))==="true"}catch(a){this.warn(`IF "${String(n.IF)}" lỗi eval: ${a.message} → coi như false`)}this.log(`IF(${String(n.IF).slice(0,60)}) → ${r?"THEN":"ELSE"}`);let i=r?n.THEN:n.ELSE;if(typeof i==="string")this.log(`EXE ${i.slice(0,80)}`),this.evalJs(i,{...t??{},...this.vueData});else if(i!==void 0)this.runAction(i,t);return}if(n.MESS!==void 0){let r;try{r=this.bindDataToString(String(n.MESS),o)}catch{r=String(n.MESS)}this.toastCount++,this.log(`MESS \uD83D\uDD14 "${r}"`);return}if(n.MESSBOX!==void 0){let r;try{r=this.bindDataToString(String(n.MESSBOX),o)}catch{r=String(n.MESSBOX)}this.log(`MESSBOX "${r}"`);return}let s=[];for(let r of Object.keys(n))s.push(r);if(this.mapData(n,this.vueData,this.vueData,t),s.length>0)this.log(`SET ${s.map((r)=>`${r} = ${Ze(r[0]==="#"?"(cross-window)":this.getPath(this.vueData,r))}`).join(" · ")}`)}callAPI(e,t){let n=String(e.API),o=e.METHOD?String(e.METHOD):"POST";this.apiCallCount++,this.log(`API ${o} ${n} IN=${e.IN!==void 0?JSON.stringify(e.IN)?.slice(0,200):"{}"}`);let s=this.opts.apiMocks,r=Object.keys(s).find((a)=>a===n);r??=Object.keys(s).find((a)=>n.endsWith(a)||a.endsWith(n)),r??=Object.keys(s).find((a)=>n.includes(a)||a.includes(n));let i;if(r!==void 0)i=JSON.parse(JSON.stringify(s[r])),this.log(`(mock khớp "${r}")`);else i={data:[]},this.warn(`không có mock cho API "${n}" — trả mặc định {data: []}`);if(j(i)&&i.__error!==void 0){let a=j(i.__error)?i.__error:{},c={status:a.status??500,responseJSON:a.responseJSON??{Message:"Simulated error"}};if(this.log(`← HTTP ${String(c.status)} (mock lỗi)`),e.ERROROUT!==void 0)this.mapData(e.ERROROUT,this.vueData,c);if(e.ERRORCALLBACK!==void 0)this.runAction(e.ERRORCALLBACK,t);if(e.ERROROUT!==void 0)return;let l=c.responseJSON;this.warn(`API "${n}" lỗi không được bắt (không có ERROROUT) → hiện ERROR DIALOG mặc định: ${typeof l==="string"?l:j(l)?String(l.Message??""):""}`);return}if(e.OUT!==void 0){let a=i;if(typeof e.OUT==="string"&&j(i)&&Object.keys(i).length===1&&i.data)a=i.data;this.mapData(e.OUT,this.vueData,a);let c=typeof e.OUT==="string"?`${e.OUT} = ${Ze(this.vueData[e.OUT])}`:Object.keys(e.OUT).map((l)=>`${l} = ${Ze(this.getPath(this.vueData,l))}`).join(" · ");this.log(`← OUT ${c}`)}else this.log("← (không có OUT)");if(e.MESS!==void 0){let a;try{a=this.bindDataToString(String(e.MESS),this.vueData)}catch{a=String(e.MESS)}this.toastCount++,this.log(`MESS \uD83D\uDD14 "${a}"`)}if(e.CALLBACK!==void 0)this.runAction(e.CALLBACK,t)}initWatches(){let e=j(this.moduleJson.watch)?this.moduleJson.watch:{};for(let o of Object.keys(e)){if(o==="deep-watch")continue;this.setValue(this.vueData,o,this.getVueData(o,this.vueData))}let t=(o,s)=>{let r=this.getPath(this.vueData,o);this.watchSnapshots.set(`${s?"D:":"S:"}${o}`,{deep:s,snap:s?JSON.stringify(r):r})},n=e["deep-watch"];if(j(n))for(let o of Object.keys(n))t(o,!0);for(let o of Object.keys(e)){if(o==="deep-watch")continue;t(o,!1)}}checkWatches(){let e=j(this.moduleJson.watch)?this.moduleJson.watch:{},t=j(e["deep-watch"])?e["deep-watch"]:{};for(let n=0;n<Zl;n++){let o=[];for(let[s,r]of this.watchSnapshots){let i=s.slice(2),a=this.getPath(this.vueData,i);if(r.deep){let c=JSON.stringify(a);if(c!==r.snap)o.push({key:i,handler:t[i],oldVal:r.snap}),r.snap=c}else if(!Object.is(a,r.snap))o.push({key:i,handler:e[i],oldVal:r.snap}),r.snap=a}if(o.length===0)return;for(let s of o){this.watchTriggerCount++,this.log(`WATCH ${s.key} thay đổi → ${Ae(s.handler)}`);let r={};this.setValue(r,s.key,s.oldVal),this.depth++;try{this.runAction(s.handler,{v_old:r})}finally{this.depth--}}}this.warn(`watch cascade vượt ${Zl} vòng — nguy cơ VÒNG LẶP VÔ HẠN giữa các watch/action`)}initDialogs(){(Array.isArray(this.moduleJson.controls)?this.moduleJson.controls:[]).forEach((t,n)=>{if(!j(t)||t.el!=="hidden-container")return;let o=j(t.attr)?t.attr:{},s=t["v-model"]??o["v-model"];if(typeof s==="string"){let r=s.replace("vueData.","").split(/[.[]/)[0];this.dialogs.push({vModel:r,where:`container ${n+1}`,open:Boolean(this.getPath(this.vueData,r))})}})}checkDialogs(){for(let e of this.dialogs){let t=Boolean(this.getPath(this.vueData,e.vModel));if(t!==e.open)e.open=t,this.log(`DIALOG ${e.vModel} (${e.where}): ${t?"MỞ \uD83D\uDD32":"ĐÓNG ▪"}`)}}declareControlVars(){let e=/^[a-zA-Z0-9$_\-.[\]]*$/,t=(n)=>{if(Array.isArray(n)){n.forEach(t);return}if(!j(n))return;let o=j(n.attr)?n.attr:{};for(let[s,r]of Object.entries(o)){if(typeof r!=="string")continue;if(s==="v-model"){if(!r.includes("vueData.")&&this.getVueData(r,this.vueData)===void 0)this.mapData({[r]:void 0},this.vueData,{})}else if(s[0]===":"&&e.test(r)){if(!r.includes("vueData.")&&this.getVueData(r,this.vueData)===void 0)this.mapData({[r]:s===":items"?[]:void 0},this.vueData,{})}}if(Array.isArray(n.innerHTML))n.innerHTML.forEach(t);if(Array.isArray(n.rows))n.rows.forEach(t);if(Array.isArray(n.cols))n.cols.forEach(t)};t(this.moduleJson.controls)}collectClickables(){let e=[],t=(n,o)=>{if(Array.isArray(n)){n.forEach((a)=>t(a,o));return}if(!j(n))return;let s=j(n.attr)?n.attr:{},r=s["v-on:click"]??s[":action"]??s.action;if(r!==void 0){let a=[];for(let c of["label",":label","innerText","text"]){let l=s[c]??n[c];if(typeof l==="string"&&l)a.push(ti(l))}if(typeof n.innerHTML==="string"&&n.innerHTML)a.push(n.innerHTML.replace(/<[^>]*>/g,"").trim());e.push({labels:a,handler:r,el:typeof n.el==="string"?n.el:"?",tableItemsExpr:o})}let i=o;if(n.el==="f-table"){let a=s[":items"];if(typeof a==="string")i=a}if(Array.isArray(n.innerHTML))n.innerHTML.forEach((a)=>t(a,i));if(Array.isArray(n.rows))n.rows.forEach((a)=>t(a,i));if(Array.isArray(n.cols))n.cols.forEach((a)=>t(a,i))};return t(this.moduleJson.controls),e}simulate(e){if(this.log("── INIT ──"),j(this.moduleJson.set))Object.assign(this.vueData.v_Set,this.moduleJson.set);if(this.opts.urlParams)for(let[n,o]of Object.entries(this.opts.urlParams))this.vueData[n]=o;try{this.runAction(this.moduleJson.data)}catch(n){this.warn(`Lỗi khi init data[]: ${n.message}`)}this.declareControlVars(),this.initWatches(),this.initDialogs(),this.checkDialogs();let t=this.collectClickables();return e.forEach((n,o)=>{let s=o+1;if(j(n.set)){this.log(""),this.log(`── STEP ${s}: set ${Object.entries(n.set).map(([r,i])=>`${r}=${Ze(i)}`).join(", ")} ──`);for(let[r,i]of Object.entries(n.set))this.setValue(this.vueData,r,i)}else if(typeof n.click==="string"){this.log(""),this.log(`── STEP ${s}: click "${n.click}" ──`);let r=n.click.toLowerCase(),i=t.filter((d)=>d.labels.some((p)=>p.toLowerCase()===r)),a=i.length>0?i:t.filter((d)=>d.labels.some((p)=>p.toLowerCase().includes(r)));if(a.length===0){this.warn(`không tìm thấy control có label "${n.click}" kèm v-on:click/:action`);return}let c=typeof n.nth==="number"?n.nth-1:0;if(a.length>1)this.log(`(khớp ${a.length} control — dùng #${c+1})`);let l=a[Math.min(c,a.length-1)],u;if(l.tableItemsExpr!==void 0||n.item!==void 0||n.index!==void 0){let d=n.item,p=typeof n.index==="number"?n.index:0;if(d===void 0&&l.tableItemsExpr){let h=this.getVueData(l.tableItemsExpr,this.vueData);if(Array.isArray(h)&&h.length>0)d=h[Math.min(p,h.length-1)],this.log(`(control trong f-table — dùng row ${p} của ${l.tableItemsExpr})`);else d={},this.warn(`control nằm trong f-table nhưng "${l.tableItemsExpr??""}" rỗng — item = {} (truyền "item" trong event để chỉ định)`)}u={$row:d,item:d,index:p}}if(n.cancelConfirm===!0)u={...u??{},__cancelConfirm:!0};this.runAction(l.handler,u)}else if(typeof n.call==="string"){this.log(""),this.log(`── STEP ${s}: call ${n.call} ──`);let r;if(n.item!==void 0||n.index!==void 0)r={$row:n.item,item:n.item,index:typeof n.index==="number"?n.index:0};if(n.cancelConfirm===!0)r={...r??{},__cancelConfirm:!0};this.runAction(n.call,r)}else if(typeof n.exe==="string"){this.log(""),this.log(`── STEP ${s}: exe ──`),this.log(`EXE ${n.exe.slice(0,100)}`);try{this.evalJs(n.exe,{...this.vueData})}catch(r){this.warn(`exe lỗi: ${r.message}`)}}else if(typeof n.assert==="string"){let r=typeof n.label==="string"?n.label:n.assert,i=!1;try{i=Boolean(this.evalExpr(n.assert,{...this.vueData}))}catch(a){this.warn(`assert "${n.assert}" lỗi eval: ${a.message}`)}this.assertResults.push({label:r,ok:i}),this.log(""),this.log(`── STEP ${s}: assert ${i?"✔":"✘"} ${r} ──`);return}else{this.warn(`event ${s} không hợp lệ — cần một trong: set / click / call / exe / assert`);return}this.checkWatches(),this.checkDialogs()}),{report:this.buildReport(),hasFailures:this.assertResults.some((n)=>!n.ok)}}buildRightDefaultNote(){let e=[];if(this.systemRightDefaulted)e.push("SystemRight=9 (mặc định, quyền cao nhất)");if(this.functionRightDefaulted)e.push(this.scannedFunctionRightCodes.length?`FunctionRight=[${this.scannedFunctionRightCodes.join(", ")}] (mặc định — quét được từ module.json)`:"FunctionRight=[] (mặc định — không quét được mã nào trong module.json)");if(e.length===0)return null;return`user preview: ${e.join(" · ")}. rightTest()/v-if theo FunctionRight khác mã trên sẽ trả false — truyền tham số \`user\` để test vai trò hạn chế hơn.`}buildReport(){let e=[...this.trace,""];e.push("═══ KẾT QUẢ ═══");let t=this.buildRightDefaultNote();if(t)e.push(`ℹ ${t}`);if(this.functionRightDefaulted){let s=to(this.scannedFunctionRightCodes,this.opts.definedFunctionRights??null);if(s)e.push(s)}for(let s of this.assertResults)e.push(`${s.ok?"✔":"✘"} assert: ${s.label}`);if(this.warnings.size>0){e.push("","── Cảnh báo ──");for(let s of this.warnings)e.push(`⚠ ${s}`)}if(this.stubUsed.size>0)e.push("",`(stub đã dùng — kết quả các hàm này KHÔNG phản ánh runtime thật: ${[...this.stubUsed].join(", ")})`);e.push("","── STATE CUỐI ──");let n=new Set(["p_domain","p_routers","p_params","p_urlReferrer","p_router","v_Loading","v_LoadingOverlay","v_containerOn","user","v_Set"]);for(let[s,r]of Object.entries(this.vueData)){if(n.has(s))continue;if(j(r)&&(r.API!==void 0||r.CALL!==void 0||r.EXE!==void 0||r.FUN!==void 0||r.IF!==void 0))continue;e.push(`${s} = ${Ze(r)}`)}let o=this.dialogs.filter((s)=>s.open).map((s)=>s.vModel);return e.push("",`Thống kê: ${this.apiCallCount} API call · ${this.toastCount} toast · ${this.watchTriggerCount} watch trigger · ${this.assertResults.filter((s)=>s.ok).length}/${this.assertResults.length} assert đạt${o.length>0?` · dialog đang mở: ${o.join(", ")}`:""}`),e.join(`
246
- `)}}function ou(e){let t=new ct(e.paths);return Hs({store:t,session:e.session,api:e.api,activeTarget:async()=>{let n=e.targets.cwdTarget();if(n)return{projectId:n.projectId,moduleId:n.moduleId??"",moduleDir:n.moduleDir??""};return e.session.getTarget()}}),t}async function ru(e){try{return await e()}catch(t){if(t instanceof Error)t.message=Ve(t.message);throw t}}async function su(e,t,n){let o=ou(e),s=await Bt(t),r=await ir(kn(o,e.session),n.apiMocks,t),i=`${s?`${s}
238
+ </html>`,moduleInfo:o,basePath:Et,origin:d,apiDomain:Ce,apiDomainOrigin:Pt,userInfoUrl:Je,previewUser:uo.user,importsLoaded:C.loaded,importProblems:C.problems,frameworkNote:S,htmlOnly:I,projectBundleNotes:I?["projectdata/StyleCSS: KHÔNG nhúng (HTMLOnly=true — server cũng không nhúng)"]:N?x.notes:[...t.forceLocalProjectBundle?[]:x.notes,t.forceLocalProjectBundle?"projectdata/StyleCSS: dựng từ workspace local (chế độ preview cấp component — bản local chưa push vẫn hiện)":"projectdata/StyleCSS: dựng từ workspace local (không tải được bản server)"],rightDefaultNote:mo,undefinedRightsNote:po,warnings:n}}function _f(e,t,n){let o=[t,n];for(let s of o){let r=Object.keys(e).find((i)=>i===s);if(r??=Object.keys(e).find((i)=>s.endsWith(i)||i.endsWith(s)),r??=Object.keys(e).find((i)=>s.includes(i)||i.includes(s)),r!==void 0)return{key:r,value:e[r]}}return}var rr={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS","Access-Control-Allow-Headers":"*"};function Mf(e){if(!e)return null;try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return null}}async function Lf(e,t,n,o,s,r,i,a){if(!o||n!==o)return null;let c=ro(s);if(!c)return null;let l=await Tl(t,c.alias);if(!l)return null;let u=await Pl(t,l,c.fnName);if(!u)return null;let d=`${l}:${u}`,p=r.get(d);if(p===void 0)p=await El(t,l,u),r.set(d,p);if(p==="safe-read"&&i.count<Jt){i.count++;try{let y;if(!c.isAuth){let{config:A}=await Rl(t,{db:l});if(A.userToken)y={...await e.request().allHeaders(),authorization:`Bearer ${A.userToken}`}}let v=await e.fetch({timeout:or,headers:y});if(v.ok()){let A=await v.json().catch(()=>null);if(A!==null)return await Js(t,l,u,"live",A),a.live++,{payload:A,note:`gọi tAPI thật (SP ${u}, ${l})`}}}catch{}}let m=await Al(t,l,u);if(m)return a.cache++,{payload:m.data,note:`_testdata cache (SP ${u}, ${l}, nguồn: ${m.source})`};let w=await Nl(t,l,u);if(w)return await Js(t,l,u,"generated",w),a.generated++,{payload:w,note:`tự sinh mock (SP ${u}, ${l}), đã lưu cache`};return null}async function ar(e,t){let{html:n,moduleInfo:o,basePath:s,origin:r,apiDomain:i,apiDomainOrigin:a,userInfoUrl:c,previewUser:l,importsLoaded:u,importProblems:d,frameworkNote:p,htmlOnly:m,projectBundleNotes:w,rightDefaultNote:y,undefinedRightsNote:v,warnings:A}=await Of(e,t),I=await vf(),g=[],k=new Set(A),R=new Set,C=[],S=[],D=[],x=[],N={revalidated:0,updated:0,fetched:0,memo:0,stale:0},P=new Set,te=new Set,ve=!1,$e=new Map,V={count:0},de=Mf(i),ge={live:0,cache:0,generated:0},Be=t.urlParams&&Object.keys(t.urlParams).length?"?"+new URLSearchParams(t.urlParams).toString():"",tt=`${r}${s}${Be}`,Ce=(()=>{try{return c?new URL(c).pathname:""}catch{return""}})(),Pt;try{Pt=await I.chromium.launch({headless:!0})}catch(Je){let nt=Je.message;if(/executable doesn't exist|Executable doesn't exist|browserType.launch/i.test(nt))throw Error(`Không launch được Chromium: ${nt}
239
+ → Chạy một lần: npx playwright install chromium`);throw Je}try{let Je=t.device??"desktop",nt=hf[Je],Et=t.viewport??nt.viewport,Y=await Pt.newPage({viewport:Et,isMobile:nt.isMobile,hasTouch:nt.hasTouch,userAgent:nt.userAgent,deviceScaleFactor:1}),lo=Math.min(Math.max(t.settleMs??df,100),5000);Y.on("pageerror",(E)=>{C.push(String(E?.message??E))}),Y.on("console",(E)=>{let W=E.type(),q=E.text();if(W==="error"){if(!/Failed to load resource/i.test(q))C.push(q)}else if(W==="warning"&&/\[Vue warn\]|\[Vuetify\]/i.test(q))S.push(q)}),await Y.route("**/*",async(E)=>{let W=E.request(),q=W.url(),me=W.method(),O=W.resourceType();if(W.isNavigationRequest()){if(q.startsWith(r))await E.fulfill({status:200,contentType:"text/html; charset=utf-8",body:n});else k.add(`runtime điều hướng ra "${q}" — đã chặn (thường do login gate / redirect trong action)`),await E.fulfill({status:200,contentType:"text/html; charset=utf-8",body:`<html><body>[fui-render] navigation blocked: ${jn(q)}</body></html>`});return}let M;try{M=new URL(q)}catch{await E.fulfill({status:400,contentType:"text/plain",body:""});return}if(me==="OPTIONS"){await E.fulfill({status:204,headers:rr,body:""});return}if(M.pathname==="/favicon.ico"){await E.fulfill({status:204,body:""});return}let we=J.extname(M.pathname).toLowerCase();if(pf.has(O)||me==="GET"&&mf.has(we)){try{let K=await Cf(q,{refresh:t.refreshAssets,offline:t.offline});if(N[K.origin]++,K.origin==="stale")P.add(`${O} "${q}" — ${K.note}`);else if(K.changed)te.add(`${q} — ${K.note}`);await E.fulfill({status:200,contentType:K.contentType,body:K.body,headers:rr})}catch(K){let be=`${O} "${q}" tải thất bại (${K.message})`;if(O==="script"||O==="stylesheet")R.add(be);else k.add(`${be} — trả 404 (offline + chưa cache?)`);await E.fulfill({status:404,contentType:"text/plain",body:""})}return}D.push(`${me} ${q}`);let ye=_f(t.apiMocks,q,M.pathname),Z;if(ye)Z=ye.value,g.push(`API ${me} ${M.pathname} → mock "${ye.key}"`);else if(Ce&&(q===c||M.pathname===Ce))Z=l,g.push(`API ${me} ${M.pathname} → user preview (userInfo của project, auto-mock)`);else{let K=await Lf(E,o.ProjectID,M.hostname,de,M.pathname,$e,V,ge);if(K)Z=K.payload,g.push(`API ${me} ${M.pathname} → ${K.note}`);else Z={data:[]},k.add(`không có mock cho API "${q}" — trả {data: []}`),g.push(`API ${me} ${M.pathname} → {data:[]} (không có mock)`)}if(j(Z)&&Z.__error!==void 0){let K=j(Z.__error)?Z.__error:{};await E.fulfill({status:Number(K.status??500),contentType:"application/json; charset=utf-8",headers:rr,body:JSON.stringify(K.responseJSON??{Message:"Simulated error"})});return}await E.fulfill({status:200,contentType:"application/json; charset=utf-8",headers:rr,body:JSON.stringify(Z)})});let Rn=Y;if(typeof Rn.routeWebSocket==="function")await Rn.routeWebSocket("**/*",(E)=>{k.add("module mở WebSocket tới backend thật — đã chặn (renderUI chỉ cho phép gọi thật qua tAPI SP đã xác nhận an toàn, không qua WebSocket)"),E.close()});g.push("── INIT ──");let zt=Date.now();if(await Y.goto(tt,{waitUntil:"networkidle",timeout:uf}),!m)try{await Y.waitForFunction("!!window.vm || !!document.querySelector('#mainContainer, #fastProjectAPP .v-application')",void 0,{timeout:15000})}catch{k.add("Vue chưa mount sau 15s — trang chưa init xong (xem JS error / asset hỏng / API treo bên dưới)")}await Y.waitForTimeout(lo);let uo=[N.revalidated?`${N.revalidated} revalidate 304`:"",N.updated?`${N.updated} CẬP NHẬT`:"",N.fetched?`${N.fetched} tải mới`:"",N.memo?`${N.memo} memo`:"",N.stale?`${N.stale} STALE`:""].filter(Boolean).join(" + ")||"0",po=[ge.live?`${ge.live} gọi thật`:"",ge.cache?`${ge.cache} cache`:"",ge.generated?`${ge.generated} tự sinh`:""].filter(Boolean).join(" + ");g.push(`trang load xong sau ${((Date.now()-zt)/1000).toFixed(1)}s · asset: ${uo} · ${D.length} API call${po?` (API không-mock: ${po})`:""}`);let mo=C.length,Ke=J.join(e,"_preview");await mt.mkdir(Ke,{recursive:!0});for(let E=0;E<t.scenario.length;E++){let W=t.scenario[E],q=E+1,me=C.length;try{if(j(W.set))g.push("",`── STEP ${q}: set ${Object.keys(W.set).join(", ")} ──`),await Y.evaluate((O)=>{let M=window,we=(Z,K,be)=>{if(M.Vue&&typeof M.Vue.set==="function")M.Vue.set(Z,K,be);else Z[K]=be},ye=(Z,K,be)=>{let xe=String(K).match(/[^.[\]']+/g)||[K],B=Z;for(let Q=0;Q<xe.length-1;Q++){if(B[xe[Q]]==null)we(B,xe[Q],/^\d+$/.test(xe[Q+1])?[]:{});B=B[xe[Q]]}we(B,xe[xe.length-1],be)};for(let[Z,K]of O)ye(M.vueData,Z,K)},Object.entries(W.set));else if(typeof W.click==="string"){g.push("",`── STEP ${q}: click "${W.click}" ──`);let O=W.click,M=typeof W.nth==="number"?W.nth-1:0,we=Y.locator(".v-btn, button, [role='button'], .v-list-item, .v-tab, a",{hasText:O}),ye=await we.count();if(ye===0)we=Y.locator(`text=${O}`),ye=await we.count();if(ye===0)k.add(`STEP ${q}: không tìm thấy phần tử có text "${O}" trên DOM`),ve=!0;else{if(ye>1)g.push(`(khớp ${ye} phần tử — dùng #${M+1})`);await we.nth(Math.min(M,ye-1)).click({timeout:Ys}),await Y.waitForTimeout(350);let Z=Y.locator(".jconfirm .jconfirm-buttons button"),K=await Z.count();if(K>0){let be=W.cancelConfirm===!0,xe=!1;for(let B=0;B<K;B++){let Q=(await Z.nth(B).innerText()).trim().toLowerCase(),re=/hủy|huỷ|cancel|không|no|đóng/.test(Q);if(be===re){g.push(`CONFIRM → bấm "${Q}"`),await Z.nth(B).click({timeout:Ys}),xe=!0;break}}if(!xe)g.push(`CONFIRM → bấm nút ${be?"cuối":"đầu"} (không nhận diện được label)`),await Z.nth(be?K-1:0).click({timeout:Ys})}}}else if(typeof W.call==="string")g.push("",`── STEP ${q}: call ${W.call} ──`),await Y.evaluate((O)=>{let M=window;if(typeof M.CALL!=="function")throw Error("global CALL không tồn tại trong runtime này");let we=M.vueData&&M.vueData[O]!==void 0?M.vueData[O]:O;M.CALL(we)},W.call);else if(typeof W.exe==="string")g.push("",`── STEP ${q}: exe ──`),await Y.evaluate((O)=>{Function(O)()},W.exe);else if(typeof W.assert==="string"){let O=typeof W.label==="string"?W.label:W.assert,M=!1;try{M=await Y.evaluate((we)=>{try{return!!Function("with(window.vueData||{}){return ("+we+")}")()}catch{return!1}},W.assert)}catch{M=!1}x.push({label:O,ok:M}),g.push("",`── STEP ${q}: assert ${M?"✔":"✘"} ${O} ──`);continue}else{k.add(`event ${q} không hợp lệ — cần một trong: set / click / call / exe / assert`);continue}await Y.waitForTimeout(lo);for(let O=me;O<C.length;O++)g.push(`✘ pageerror: ${C[O]}`);if(t.screenshotSteps){let O=J.join(Ke,`render-step-${q}.png`);await Y.screenshot({path:O}),g.push(`screenshot: ${O}`)}}catch(O){ve=!0,g.push(`✘ STEP ${q} lỗi: ${O.message.split(`
240
+ `)[0]}`)}}let ne=await Y.evaluate(()=>{let E=document.querySelector("#fastProjectAPP"),W=E?E.children.length:Array.from(document.body.children).filter((B)=>!/^(SCRIPT|STYLE|LINK|TEMPLATE|NOSCRIPT)$/.test(B.tagName)).length,q=window.innerWidth,me=document.documentElement.scrollWidth>q+4,O=[],M=(B)=>B&&typeof B.className==="string"?B.className.trim().split(/\s+/).slice(0,2).join("."):"";if(me)for(let B of document.body.querySelectorAll("*")){let Q=B.getBoundingClientRect();if(Q.width>0&&Q.right>q+4&&O.length<5){let re=M(B);O.push(B.tagName.toLowerCase()+(re?"."+re:"")+" (right="+Math.round(Q.right)+"px)")}}let we=document.body.innerText||"",ye=[];for(let B of["undefined","NaN","[object Object]"])if(we.includes(B))ye.push(B);let Z=[];for(let B of document.querySelectorAll("img"))if(B.complete&&B.naturalWidth===0&&B.getBoundingClientRect().width>0&&Z.length<5)Z.push((B.getAttribute("src")||"(no src)").slice(0,120));let K=[];for(let B of document.querySelectorAll("canvas")){let Q=B.getBoundingClientRect();if((Q.width===0||Q.height===0)&&K.length<5){let re=B.parentElement,se=M(re);K.push("canvas trong "+(re?re.tagName.toLowerCase():"?")+(se?"."+se:"")+" ("+Math.round(Q.width)+"×"+Math.round(Q.height)+")")}}let be=[];{let B=(re)=>{let se="";for(let Ge of re.childNodes)if(Ge.nodeType===Node.TEXT_NODE)se+=Ge.textContent||"";return se.trim()},Q=[];for(let re of document.body.querySelectorAll("*")){if(Q.length>=600)break;let se=B(re);if(!se)continue;let Ge=re.getBoundingClientRect();if(Ge.width<=0||Ge.height<=0)continue;Q.push({el:re,rect:Ge,text:se.slice(0,24)})}Q.sort((re,se)=>re.rect.top-se.rect.top||re.rect.left-se.rect.left);e:for(let re=0;re<Q.length;re++){let se=Q[re];for(let Ge=re+1;Ge<Q.length;Ge++){let ze=Q[Ge];if(ze.rect.top>se.rect.bottom+4)break;if(se.el.contains(ze.el)||ze.el.contains(se.el))continue;let bd=se.rect.top+se.rect.height/2,kd=ze.rect.top+ze.rect.height/2;if(Math.abs(bd-kd)>Math.min(se.rect.height,ze.rect.height)*0.6)continue;let yr=se.rect.left<=ze.rect.left?ze.rect.left-se.rect.right:se.rect.left-ze.rect.right;if(yr<2){let vd=yr<0?"chồng lấn":`dính sát (gap ${Math.round(yr)}px)`;if(be.push(`${vd}: "${se.text}" ↔ "${ze.text}"`),be.length>=6)break e}}}}let xe=!!document.querySelector(".v-dialog--active, .v-overlay--active");return{appChildCount:W,overflowX:me,overflowers:O,suspectTexts:ye,brokenImages:Z,deadCanvas:K,textCollisions:be,dialogOpen:xe}}),Le=[];if(ne.appChildCount===0)Le.push((m?"✘ <body> không render được phần tử nào (HTMLOnly — body.html LÀ cả trang)":"✘ #fastProjectAPP rỗng — controls không render")+(d.length>0||R.size>0?" (CÓ file toàn cục nạp thiếu — xem mục MÔI TRƯỜNG THIẾU FILE trước khi nghi module.json)":" (kiểm tra pageerror / module.json)"));if(ne.overflowX)Le.push(`⚠ trang tràn ngang viewport ${Et.width}px — phần tử tràn: ${ne.overflowers.join(", ")||"(không xác định)"}`);if(ne.suspectTexts.length>0)Le.push(`⚠ text nghi vấn hiển thị trên trang: ${ne.suspectTexts.map((E)=>`"${E}"`).join(", ")} (binding thiếu data?)`);if(ne.brokenImages.length>0)Le.push(`⚠ ảnh vỡ (load fail nhưng đang chiếm chỗ): ${ne.brokenImages.join(", ")}`);if(ne.deadCanvas.length>0)Le.push(`⚠ canvas chart 0-size — chart init khi container chưa có kích thước: ${ne.deadCanvas.join(", ")}`);if(ne.textCollisions.length>0)Le.push(`⚠ text dính/chồng lấn (thẩm mỹ — PHẢI sửa, không được bỏ qua): ${ne.textCollisions.join("; ")}`);let Tn=J.join(Ke,"render.png"),ho=await Y.screenshot({path:Tn}),Pn,oe="image/png";if(ho.length<=Vl)Pn=ho.toString("base64");else try{let E=await Y.screenshot({type:"jpeg",quality:60});if(E.length<=Vl)Pn=E.toString("base64"),oe="image/jpeg"}catch{}try{await Y.screenshot({path:J.join(Ke,"render-full.png"),fullPage:!0})}catch{}let De;if(ne.dialogOpen)try{De=J.join(Ke,"render-dialog.jpg"),await Y.locator(".v-dialog--active, .v-overlay--active").first().screenshot({path:De,type:"jpeg",quality:60,timeout:3000})}catch{De=void 0}let Xt=[];try{Xt=await Y.evaluate(()=>{let E=new Set(["p_domain","p_routers","p_params","p_urlReferrer","p_router","p_path","p_render","p_module_id","v_Loading","v_LoadingOverlay","v_containerOn","user","v_Set"]),W=[],q=window.vueData||{};for(let me of Object.keys(q)){if(E.has(me)||W.length>=40)continue;let O=q[me];if(O&&typeof O==="object"&&!Array.isArray(O)&&(O.API!==void 0||O.CALL!==void 0||O.EXE!==void 0||O.FUN!==void 0||O.IF!==void 0))continue;let M;try{M=JSON.stringify(O)}catch{M=String(O)}if(M===void 0)M="undefined";if(M.length>120)M=M.slice(0,120)+"…";W.push(me+" = "+M)}return W})}catch{}let Re={ts:new Date().toISOString(),jsErrors:C.length,initErrors:mo,vueWarns:S.length,envMissing:d.length+R.size,appEmpty:ne.appChildCount===0,overflowX:ne.overflowX,suspectTexts:ne.suspectTexts.length,brokenImages:ne.brokenImages.length,deadCanvas:ne.deadCanvas.length,textCollisions:ne.textCollisions.length,assertFail:x.filter((E)=>!E.ok).length},pe=await gf(Ke,Je),wd=pe!==null&&ei(Re)&&Xl(pe,Re).length===0;Re.failStreak=!ei(Re)?0:wd?(pe?.failStreak??1)+1:1,await mt.writeFile(zl(Ke,Je),JSON.stringify(Re,null,1),"utf8").catch(()=>{return});let Ii=[...new Set(C)],Ci=[...S],Ri=kf(Re,Ci,Ii),U=[];if(U.push(t.reportLabel?`RENDER (browser thật): ${t.reportLabel}`:`RENDER (browser thật): ${e}`),U.push(...bf(Re,Ri,Ii,Ci)),pe)U.push(wf(pe,Re));if((Re.failStreak??0)>=ff)U.push(...yf(Re.failStreak,Ri));U.push(""),U.push(`Framework ${o.Framework??"V3"}${m?" · HTMLOnly=true (KHÔNG nhúng framework/projectdata — như production)":""} · viewport ${Et.width}×${Et.height}${Je!=="desktop"?` (${Je})`:""} · CDN ${sr}`),U.push(`Origin trang: ${r} · apiDomain: ${i||"(trống)"} [${a}]${c?` · userInfo auto-mock: ${c}`:""}`),U.push(p);for(let E of w)U.push(` ✓ ${E}`);U.push(`Import (framework ∪ project ∪ module, sort theo FileSort/FileName): ${u.length} nạp được${d.length?`, ${d.length} KHÔNG nạp được`:""}`);for(let E of u)U.push(` ✓ ${E}`);if(y)U.push(`ℹ ${y}`);if(v)U.push(v);U.push(""),U.push(...g),U.push(""),U.push("═══ KẾT QUẢ ═══");for(let E of x)U.push(`${E.ok?"✔":"✘"} assert: ${E.label}`);if(d.length>0||R.size>0){U.push("","── ⛔ MÔI TRƯỜNG THIẾU FILE (đọc TRƯỚC khi sửa module) ──","Các file dưới đây không nạp được vào trang test. Hàm/biến/style toàn cục do chúng","định nghĩa sẽ không tồn tại → lỗi JS và lỗi hiển thị bên dưới CÓ THỂ chỉ là hệ quả,","không phải lỗi của module. Khắc phục phần này trước rồi render lại.");for(let E of d)U.push(`⛔ ${E}`);for(let E of R)U.push(`⛔ ${E}`)}if(P.size>0){U.push("","── ⛔ ASSET CŨ, KHÔNG XÁC MINH ĐƯỢC (đọc TRƯỚC khi kết luận về platform) ──","Không hỏi được server xem các file dưới đây còn đúng không, nên trang test đang chạy",'bằng bản trên đĩa. Mọi kết luận kiểu "thư viện X có bug" rút ra từ lượt render này',"đều KHÔNG dùng được — bản thật trên CDN có thể đã khác. Nối mạng rồi render lại,","hoặc ép tải mới bằng refreshAssets: true.");for(let E of P)U.push(`⛔ ${E}`)}if(te.size>0){U.push("","── ℹ️ ASSET PLATFORM ĐÃ ĐỔI so với lượt render trước ──","Hành vi đổi mà module.json không đụng tới có thể đến từ đây, không phải từ module.");for(let E of te)U.push(`ℹ️ ${E}`)}if(C.length>0){U.push("","── JS/Vue errors ──");for(let E of[...new Set(C)].slice(0,20))U.push(`✘ ${E.split(`
241
+ `)[0].slice(0,300)}`)}if(S.length>0){U.push("","── [Vue warn] ──");for(let E of[...new Set(S)].slice(0,15))U.push(`⚠ ${E.split(`
242
+ `)[0].slice(0,300)}`)}if(Le.length>0)U.push("","── DOM AUDIT ──",...Le);if(k.size>0){U.push("","── Cảnh báo ──");for(let E of k)U.push(`⚠ ${E}`)}if(Xt.length>0)U.push("","── STATE CUỐI (vueData) ──",...Xt);if(U.push(""),U.push(`Screenshot: ${Tn}${Pn?" (đính kèm bên dưới)":" (quá lớn để inline — dùng Read tool để xem)"}`),U.push(`Full-page: ${J.join(Ke,"render-full.png")}`),De)U.push(`Dialog đang mở — chụp riêng: ${De}`);U.push(""),U.push(`Thống kê: ${D.length} API call (tất cả mock) · ${C.length} JS error (${mo} lúc init) · ${S.length} Vue warn · ${x.filter((E)=>E.ok).length}/${x.length} assert đạt${ne.dialogOpen?" · dialog đang mở":""}`);let yd=x.some((E)=>!E.ok)||ve||C.length>0||ne.appChildCount===0||d.length>0||R.size>0||P.size>0&&!t.offline;return{report:U.join(`
243
+ `),hasFailures:yd,screenshotPath:Tn,inlineScreenshotBase64:Pn,inlineScreenshotMime:oe}}finally{await Pt.close().catch(()=>{return})}}var Ff="_componentPreview",Zl=8;function Uf(e){return e.normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/[^a-zA-Z0-9]+/g,"-").replace(/^-+|-+$/g,"").toLowerCase()||"case"}function Hf(e){let t={},n={},o=[];for(let[s,r]of Object.entries(e)){if(s.startsWith("@")||s.startsWith("#")){o.push(`prop "${s}": FUI không nhận cú pháp @/# trong JSON — đổi thành "v-on:"/"v-slot:". Đã bỏ qua.`);continue}if(s.startsWith(":")||s.startsWith("v-")){t[s]=r;continue}if(typeof r==="string"){t[s]=r;continue}if(typeof r==="boolean"||typeof r==="number"){t[`:${s}`]=JSON.stringify(r);continue}let i=`cp_${s.replace(/[^a-zA-Z0-9_]/g,"_")}`;n[i]=r,t[`:${s}`]=i}return{attr:t,hoisted:n,notes:o}}function Vf(e,t,n){let{attr:o,hoisted:s,notes:r}=Hf(n.props??{}),i={...n.vueData??{},...s},a={w:"12",el:e,attr:o};if(n.slot!==void 0)a.innerHTML=n.slot;return{moduleJson:{ModuleName:`preview: ${e} — ${t}`,data:Object.keys(i).length>0?[i]:[],watch:{},controls:[{prop:"fluid pa-0",rows:[{prop:"",cols:[a]}]}],set:{}},notes:r}}async function eu(e,t){try{await Qe.access(e)}catch{return!1}return await Qe.cp(e,t,{recursive:!0}),!0}async function Wf(e,t){let n=$n(e),o=[];if(t){let i=await H(ue.join(Bs(e,t),"_moduleInfo.json"),null);o.push(i?.Framework)}let s=await H(ue.join(n,"_projectInfo.json"),null);if(o.push(s?.Framework),!o.some(Boolean))try{let i=await Qe.readdir(ue.join(n,"modules"),{withFileTypes:!0});for(let a of i){if(!a.isDirectory())continue;let c=await H(ue.join(n,"modules",a.name,"_moduleInfo.json"),null);if(c?.Framework){o.push(c.Framework);break}}}catch{}let r=o.find(Boolean);return String(r??"V3").toUpperCase()==="V2"?"V2":"V3"}async function tu(e){let t=await Il(),n=e.projectId||t?.projectId;if(!n)throw Error("Không xác định được projectId. Truyền projectId hoặc chạy session_set trước.");let o=e.moduleId||(!e.projectId?t?.moduleId:void 0),s=$n(n),r=o?Bs(n,o):s,i=ue.join(r,"components"),a=o?`module (${o})`:"project (global)",c=ue.join(i,`${e.componentName}.vue`);if(!(await F(c)).trim()){let I=await _e(i);throw Error(`Không tìm thấy ${e.componentName}.vue trong ${i}.`+(I.length?` Có sẵn: ${I.join(", ")}.`:" Thư mục components rỗng.")+(o?"":" Nếu đây là component cấp module, truyền thêm moduleId."))}let u=e.cases&&e.cases.length>0?e.cases:[{name:"default"}],d=e.devices&&e.devices.length>0?e.devices:["desktop","mobile"];if(u.length*d.length>Zl)throw Error(`${u.length} case × ${d.length} device = ${u.length*d.length} lần render, vượt giới hạn ${Zl}. `+"Mỗi lần render là một phiên browser thật — chia nhỏ thành nhiều lần gọi.");let p=await Wf(n,o),m=ue.join(vn(),Ff,it(n,"projectId"),it(e.componentName,"componentName")),w=ue.join(m,"_preview");await Qe.mkdir(w,{recursive:!0});let y=[],v=new Set;for(let[I,g]of u.entries()){let k=g.name?.trim()||`case-${I+1}`,R=Uf(k),C=ue.join(m,R);await Qe.rm(C,{recursive:!0,force:!0}),await Qe.mkdir(C,{recursive:!0}),await Qe.writeFile(ue.join(C,"_moduleInfo.json"),JSON.stringify({ProjectID:n,ModuleID:`_cpreview_${e.componentName}`,ModuleName:`preview ${e.componentName}`,Framework:p},null,2),"utf-8");let{moduleJson:S,notes:D}=Vf(e.componentName,k,g);for(let x of D)v.add(`[${k}] ${x}`);if(await Qe.writeFile(ue.join(C,"module.json"),JSON.stringify(S,null,2),"utf-8"),await eu(i,ue.join(C,"components")),o)await eu(ue.join(r,"imports"),ue.join(C,"imports"));for(let x of d){let N=`${e.componentName} · ${k} · ${x}`,P=await ar(C,{scenario:g.scenario??[],apiMocks:g.apiMocks??{},vueData:void 0,user:e.user,device:x,viewport:e.viewport,settleMs:e.settleMs,screenshotSteps:e.screenshotSteps,forceLocalProjectBundle:!0,reportLabel:N}),te=P.screenshotPath;if(P.screenshotPath){let ve=ue.join(w,`${R}-${x}.png`);try{await Qe.rename(P.screenshotPath,ve),te=ve;let $e=ue.join(ue.dirname(P.screenshotPath),"render-full.png");await Qe.rename($e,ue.join(w,`${R}-${x}-full.png`)).catch(()=>{})}catch{}}y.push({caseName:k,device:x,report:P.report,hasFailures:P.hasFailures,screenshotPath:te,inlineScreenshotBase64:P.inlineScreenshotBase64,inlineScreenshotMime:P.inlineScreenshotMime})}}let A=[`PREVIEW COMPONENT: ${e.componentName}`,`Scope: ${a} · Project: ${n} · Framework ${p}`,`Nguồn: ${c}`,`Cases: ${u.map((I)=>I.name?.trim()||"(không tên)").join(", ")} × Devices: ${d.join(", ")} = ${y.length} lần render`,`Ảnh: ${w}`,"","Không gian riêng: chỉ component này + CSS của chính nó + theme/import cấp project.","style.css của module KHÔNG nạp — component phải tự đứng được. Tầng project dựng từ workspace","local nên bản chưa push vẫn hiện; chạy `project_sync` nếu nghi file local đã cũ."];if(v.size>0){A.push("","⚠ Ghi chú dựng harness:");for(let I of v)A.push(` • ${I}`)}return{header:A.join(`
244
+ `),runs:y,hasFailures:y.some((I)=>I.hasFailures)}}import oi from"node:vm";var nu=500,ou=15,ru=500;function Ze(e){if(typeof e==="string")return`"${e.length>40?e.slice(0,40)+"…":e}"`;return oo(e)}function ri(e){return e.replace(/^`+|`+$/g,"")}function su(e){let t=e.split(/[\\/]+/).filter(Boolean),n=t.lastIndexOf("modules");return n>0?t[n-1]:null}async function cr(e,t,n){let{loadAllSchemaProcNames:o,getVerifiedSpNames:s}=e;if(!t||Object.keys(t).length===0)return null;let r=su(n);if(!r)return null;let i=[];try{i=await o(r)}catch{return null}if(i.length===0)return null;let a=await s(),c=jl(Object.keys(t),i,a);if(c.length===0)return null;return["\uD83D\uDEA8 CẤU TRÚC MOCK CHƯA ĐƯỢC KIỂM CHỨNG — nghi ngờ tự suy diễn thay vì đọc từ SP thật:",...c.map((l)=>` - apiMocks["${l.mockKey}"] khớp tên SP CÓ THẬT "${l.spName}" trong schema local, nhưng SP đó chưa từng được db_sp_verify đọc thân trong phiên này.`),` → Chạy db_sp_verify({ name: "${c[0].spName}" }) để đọc thân SP thật rồi COPY NGUYÊN VĂN snippet apiMocks nó sinh sẵn — không tự đoán tên cột/shape cho SP đã tồn tại, kết quả mô phỏng sẽ lệch với thực tế.`].join(`
245
+ `)}function qf(e){let t=[],n=new Set;return Me(e,(o,s,r)=>{if(r)return;let i=o.API;if(typeof i!=="string"||!i||i.includes("`")||i.includes("{{"))return;let a=o.IN;if(a!==void 0&&!(j(a)&&Object.keys(a).length===0))return;if(n.has(i))return;n.add(i),t.push({url:i,method:typeof o.METHOD==="string"?o.METHOD:"POST"})}),t}async function iu(e,t,n,o){let{getConnectionAlias:s,resolveApiSpName:r,writeTestData:i,classifySpSafety:a,callLiveApiSp:c,readTestData:l,generateMockForLocalSp:u}=e,d={...o},p=[],m=su(n);if(!m)return{mocks:d,trace:p};let w=qf(t);if(w.length===0)return{mocks:d,trace:p};let y=0;for(let{url:v,method:A}of w){let I=ro(v);if(!I)continue;let g=await s(m,I.alias);if(!g)continue;let k=await r(m,g,I.fnName);if(!k)continue;let R=Object.keys(o).find((N)=>N===v);if(R??=Object.keys(o).find((N)=>v.endsWith(N)||N.endsWith(v)),R??=Object.keys(o).find((N)=>v.includes(N)||N.includes(v)),R!==void 0){await i(m,g,k,"user",o[R]);continue}let C=await a(m,g,k),S=!1;if(C==="safe-read"&&y<Jt){y++;let N=await c(m,g,I.isAuth,I.fnName,A);if(N.ok)d[v]=N.data,p.push(`live ${v} → SP ${k} (${g}) — gọi tAPI thật thành công`),await i(m,g,k,"live",N.data),S=!0;else p.push(`⚠ live ${v} → SP ${k} (${g}) gọi thật thất bại (${N.error??`HTTP ${N.status}`}) — rơi xuống cache/tự sinh`)}else if(C==="unsafe-write")p.push(`skip ${v} → SP ${k} (${g}) có lệnh ghi — KHÔNG gọi thật, dùng cache/tự sinh`);else if(C==="opaque")p.push(`skip ${v} → SP ${k} (${g}) có đoạn đọc tĩnh không nhìn xuyên qua được (SQL động / gọi SP khác) — KHÔNG gọi thật, dùng cache/tự sinh`);else if(C==="unknown")p.push(`skip ${v} → SP ${k} (${g}) chưa có thân SP local để phân loại an toàn — KHÔNG gọi thật, dùng cache/tự sinh`);else if(y>=Jt)p.push(`skip ${v} → SP ${k} (${g}) đã đạt trần ${Jt} lượt gọi thật/lượt simulate — dùng cache/tự sinh`);if(S)continue;let D=await l(m,g,k);if(D){d[v]=D.data,p.push(`cache ${v} → SP ${k} (${g}) — dùng _testdata cache (nguồn: ${D.source}, lưu lúc ${D.savedAt})`);continue}let x=await u(m,g,k);if(x)d[v]=x,p.push(`gen ${v} → SP ${k} (${g}) — tự sinh mock từ cấu trúc SELECT, đã lưu cache`),await i(m,g,k,"generated",x)}return{mocks:d,trace:p}}class si{vueData;moduleJson;opts;sandbox;ctx;trace=[];warnings=new Set;depth=0;apiCallCount=0;toastCount=0;watchTriggerCount=0;assertResults=[];dialogs=[];watchSnapshots=new Map;stubUsed=new Set;scannedFunctionRightCodes;systemRightDefaulted;functionRightDefaulted;constructor(e,t){this.moduleJson=e,this.opts=t,this.systemRightDefaulted=!("SystemRight"in(t.user??{})),this.functionRightDefaulted=!("FunctionRight"in(t.user??{})),this.scannedFunctionRightCodes=qt([JSON.stringify(e)]),this.vueData={p_domain:"sim.local",p_routers:[],p_params:"",p_urlReferrer:"",p_router:"/sim/module",v_Loading:!1,v_LoadingOverlay:[],v_containerOn:!0,user:{UserID:null,UserName:null,LastName:null,FirstName:null,Sex:null,Phone:null,Email:null,DepartmentID:null,GroupID:null,SystemRight:9,FunctionRight:this.scannedFunctionRightCodes,LastLogin:null,...t.user??{}},v_Set:{title:null,apiDomain:"",login:!1,menu:[],userInfo:""}},this.sandbox=this.buildSandbox(),this.ctx=oi.createContext(this.sandbox)}log(e){if(this.trace.length===nu)this.trace.push("… (trace bị cắt — quá dài)");if(this.trace.length>nu)return;this.trace.push(" ".repeat(Math.min(this.depth,6))+e)}warn(e){this.warnings.add(e),this.log(`⚠ ${e}`)}rightTest(e){if(!j(e))return!0;let t=this.vueData.user??{};for(let[n,o]of Object.entries(e)){let s=Array.isArray(o)?o:[o],r=t[n],i=Array.isArray(r)?r:[r];if(!s.some((a)=>i.includes(a)))return!1}return!0}buildSandbox(){let e=this,t=(r,i)=>(...a)=>(e.stubUsed.add(r),e.log(`FUN ${r}(${a.map((c)=>Ze(c)).join(", ")}) — stub, không mô phỏng UI thật`),i),n=(r)=>{e.stubUsed.add("moment");let i=r!==void 0&&r!==null?new Date(r):new Date,a=(u)=>String(u).padStart(2,"0"),c={},l=(u)=>{let d=`${i.getFullYear()}-${a(i.getMonth()+1)}-${a(i.getDate())}`;if(!u)return`${d}T${a(i.getHours())}:${a(i.getMinutes())}:${a(i.getSeconds())}`;return u.replace(/YYYY/g,String(i.getFullYear())).replace(/MM/g,a(i.getMonth()+1)).replace(/DD/g,a(i.getDate())).replace(/HH/g,a(i.getHours())).replace(/mm/g,a(i.getMinutes())).replace(/ss/g,a(i.getSeconds()))};return Object.assign(c,{format:l,add:()=>c,subtract:()=>c,startOf:()=>c,endOf:()=>c,valueOf:()=>i.getTime(),toDate:()=>i,toString:()=>l(),year:()=>i.getFullYear(),month:()=>i.getMonth(),date:()=>i.getDate(),daysInMonth:()=>new Date(i.getFullYear(),i.getMonth()+1,0).getDate(),locale:()=>c,isValid:()=>!isNaN(i.getTime())}),c},o={forEach:(r,i)=>{if(Array.isArray(r))r.forEach((a,c)=>i(a,c));else if(j(r))Object.entries(r).forEach(([a,c])=>i(c,a));return r},map:(r,i)=>{let a=Array.isArray(r)?r:j(r)?Object.values(r):[];if(typeof i==="string")return a.map((c)=>c?.[i]);return a.map(i)},filter:(r,i)=>Array.isArray(r)?r.filter(i):[],find:(r,i)=>Array.isArray(r)?r.find(i):void 0,findIndex:(r,i)=>Array.isArray(r)?r.findIndex(i):-1,includes:(r,i)=>Array.isArray(r)||typeof r==="string"?r.includes(i):!1,cloneDeep:(r)=>r===void 0?void 0:JSON.parse(JSON.stringify(r)),isArray:Array.isArray,isString:(r)=>typeof r==="string",isNumber:(r)=>typeof r==="number",isPlainObject:j,isEmpty:(r)=>r==null||typeof r==="string"&&r===""||Array.isArray(r)&&r.length===0||j(r)&&Object.keys(r).length===0,isUndefined:(r)=>r===void 0,keys:(r)=>j(r)?Object.keys(r):[],values:(r)=>j(r)?Object.values(r):[],uniq:(r)=>[...new Set(r)],uniqBy:(r,i)=>{let a=new Set;return(r??[]).filter((c)=>{let l=c?.[i];if(a.has(l))return!1;return a.add(l),!0})},sumBy:(r,i)=>(r??[]).reduce((a,c)=>a+Number(typeof i==="string"?c?.[i]??0:i(c)),0),maxBy:(r,i)=>(r??[]).reduce((a,c)=>a===void 0||Number(c?.[i])>Number(a?.[i])?c:a,void 0),minBy:(r,i)=>(r??[]).reduce((a,c)=>a===void 0||Number(c?.[i])<Number(a?.[i])?c:a,void 0),orderBy:(r,i,a)=>{let c=Array.isArray(i)?i:[i],l=Array.isArray(a)?a:[a??"asc"];return[...r??[]].sort((u,d)=>{for(let p=0;p<c.length;p++){let m=u?.[c[p]],w=d?.[c[p]];if(m===w)continue;let y=m>w?1:-1;return(l[p]??"asc")==="desc"?-y:y}return 0})},sortBy:(r,i)=>o.orderBy(r,i),groupBy:(r,i)=>{let a={};return(r??[]).forEach((c)=>{let l=String(c?.[i]);(a[l]??=[]).push(c)}),a},without:(r,...i)=>(r??[]).filter((a)=>!i.includes(a)),intersection:(r,i)=>(r??[]).filter((a)=>(i??[]).includes(a)),castArray:(r)=>Array.isArray(r)?r:[r],trim:(r,i)=>{if(typeof r!=="string")return r;if(!i)return r.trim();let a=i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return r.replace(new RegExp(`^[${a}]+|[${a}]+$`,"g"),"")},replace:(r,i,a)=>String(r).replace(i,a),split:(r,i,a)=>String(r).split(i,a),startsWith:(r,i)=>String(r).startsWith(i),endsWith:(r,i)=>String(r).endsWith(i),concat:(...r)=>[].concat(...r),take:(r,i)=>(r??[]).slice(0,i),last:(r)=>(r??[])[(r??[]).length-1],head:(r)=>(r??[])[0],first:(r)=>(r??[])[0],round:(r,i=0)=>Math.round(r*10**i)/10**i,assignIn:Object.assign,assign:Object.assign,at:(r,i)=>[e.getPath(r,i)],property:(r)=>(i)=>e.getPath(i,r)},s=new Proxy(o,{get(r,i){if(i in r)return r[i];return(...a)=>{e.warn(`_.${i} chưa hỗ trợ trong simulator — trả undefined`);return}}});return{vueData:this.vueData,console:{log:(...r)=>e.log(`console.log: ${r.map((i)=>typeof i==="string"?i:Ze(i)).join(" ")}`),error:(...r)=>e.log(`console.error: ${r.map((i)=>typeof i==="string"?i:Ze(i)).join(" ")}`),warn:(...r)=>e.log(`console.warn: ${r.map((i)=>typeof i==="string"?i:Ze(i)).join(" ")}`)},JSON,Math,Date,Array,Object,String,Number,Boolean,RegExp,parseInt,parseFloat,isNaN,isFinite,encodeURIComponent,decodeURIComponent,encodeURI,decodeURI,moment:n,_:s,CALL:(r,i)=>e.runAction(r,i),groupBy:(r,i,a)=>{let c=Array.isArray(a)?a:[a],l={};return(r??[]).forEach((u)=>{let d=String(u?.[i]);(l[d]??=[]).push(u)}),Object.entries(l).map(([u,d])=>{let p={[i]:u,count:d.length};for(let m of c)if(m)p[m]=d.reduce((w,y)=>w+Number(y?.[m]??0),0);return p})},buildHeader:(r,i)=>{let a=[];if(i)(Array.isArray(r)?r:[]).forEach((c)=>a.push({text:c[i.text],value:c[i.value],divider:!0}));else{let c=Array.isArray(r)?r[0]:j(r)&&Array.isArray(r.data)?r.data[0]:void 0;if(j(c))for(let[l,u]of Object.entries(c))a.push(typeof u==="number"?{el:"t-num",text:l,value:l,divider:!0}:{text:l,value:l,divider:!0})}return a},findInArray:(r)=>(r?.src??[]).find((i)=>i?.[r.key]===r.value),openWindow:(r)=>{e.stubUsed.add("openWindow"),e.log(`FUN openWindow id=${String(r?.id??"?")} url=${String(r?.url??"?")} — stub (sub-window không mô phỏng)`)},showMessage:(r)=>{if(e.log(`MESSBOX/showMessage "${String(r?.title??"")}"${r?.message?` — ${String(r.message)}`:""}`),r?.onclose)e.log("(giả lập user đóng dialog → chạy onclose)"),e.runAction(r.onclose)},confirm:(r)=>{if(e.log(`CONFIRM "${String(r?.title??"")}" → OK (mặc định simulator)`),typeof r?.action==="function")r.action()},ajaxCALL:t("ajaxCALL"),jsonToExcel:t("jsonToExcel"),printPDF:t("printPDF"),copyToClipboard:t("copyToClipboard"),redirect:t("redirect"),reload:t("reload"),logout:t("logout"),webSocket_Send:t("webSocket_Send"),webSocketJoinGroup:t("webSocketJoinGroup"),appCommand:t("appCommand"),pushRouter:t("pushRouter"),capacityText:(r)=>`${r}`,generateID:()=>`sim${Math.floor(Math.random()*1e6)}`,rightTest:(r)=>e.rightTest(r),fillData:t("fillData"),chartDataBuild:t("chartDataBuild"),fixURL:(r)=>r,alert:t("alert")}}evalExpr(e,t){return this.sandbox.__scope__=t,this.sandbox.__ret__=void 0,oi.runInContext(`with(__scope__) { __ret__ = ${e} }`,this.ctx,{timeout:ru}),this.sandbox.__ret__}evalJs(e,t){this.sandbox.__scope__=t,oi.runInContext(`with(__scope__) { ${e} }`,this.ctx,{timeout:ru})}bindDataToString(e,t){if(typeof e!=="string")return e;return e.replace(/{{([\s\S]+?)}}/g,(n,o)=>{let s=this.evalExpr(o,t);return s===void 0||s===null?"":String(s)})}getPath(e,t){let n=String(t).match(/[^.[\]]+/g)??[],o=e;for(let s of n){if(o==null)return;o=o[s]}return o}getVueData(e,t){if(typeof e==="string"){if(e==="")return"";let n=e.replace("vueData.","").trim();if(n[0]==="`")return ri(this.bindDataToString(n,t));let o=n;if(/^{{.*}}$/gm.test(o))o=o.replace(/^{{|}}$/gm,"");try{return this.evalExpr(o,t)}catch{if(/({{.*}}|[ ])/gm.test(n))try{return ri(this.bindDataToString(n,t))}catch{return}return}}else if(j(e)&&e.ARRAY&&e.COL){let n=this.getPath(t,String(e.ARRAY));return Array.isArray(n)?n.map((o)=>this.getPath(o,String(e.COL))):[]}return e}setValue(e,t,n,o,s){if(Array.isArray(t)){if(!j(e)&&!Array.isArray(e))return;let r=e,i=s??0,a=t[i];if(typeof a==="string"&&a[a.length-1]==="]")if(a=a.replaceAll("]",""),/^[0-9]*$/gm.test(a))a=parseInt(a,10);else if(a[0]==="'")a=a.replaceAll("'","");else a=this.getVueData(a,o??this.vueData);if(t.length-1===i){r[a]=n;return}if(typeof r[a]>"u"){let c=t[i+1];r[a]=c[c.length-1]==="]"?[]:{}}this.setValue(r[a],t,n,o,i+1)}else if(typeof t==="string")if(t[0]==="`"||t[0]==="'")this.setValue(e,[t.replaceAll("'","").replaceAll("`","")],n,o,0);else this.setValue(e,t.match(/[^.[]+/g)??[t],n,o,0)}mapData(e,t,n,o){if(typeof e==="string"){this.setValue(t,e,n);return}if(!j(e))return;for(let[s,r]of Object.entries(e)){if(o)n={...this.vueData,...o};let i;try{i=this.bindDataToString(s,this.vueData)}catch{i=s}if(typeof r==="string"&&r[0]==="#")this.warn(`đọc cross-window "${r}" — không mô phỏng được, gán undefined`),this.setValue(t,i,void 0);else if(i[0]==="#")this.log(`→ ghi cross-window ${i} = ${Ze(this.getVueData(r,n))} (không mô phỏng)`);else this.setValue(t,i,this.getVueData(r,n),n)}}defProp(e){if(typeof e==="string"){if(typeof this.vueData[e]>"u")this.vueData[e]=[]}else if(j(e)){for(let t of Object.keys(e))if(typeof this.vueData[t]>"u")this.vueData[t]=void 0}}runAction(e,t){if(!e)return;let n={...this.vueData,...t??{}};if(typeof e==="string"){let s=this.getVueData(e,n);if(!s){this.warn(`Action "${e}" undefined — CALL trỏ tới action không tồn tại?`);return}e=s}if(!e)return;let o=Array.isArray(e)?e:[e];for(let s of o)if(j(s)&&s.CONFIRM){let r;try{r=this.bindDataToString(String(s.CONFIRM),n)}catch{r=String(s.CONFIRM)}let i=t?.__cancelConfirm===!0;if(this.log(`CONFIRM "${r}" → ${i?"HỦY":"OK"}`),i){if(s.CANCEL)this.vueActionSafe(s.CANCEL,t)}else{let a=JSON.parse(JSON.stringify(s));delete a.CONFIRM,delete a.CANCEL,this.vueActionSafe(a,t)}}else this.vueActionSafe(s,t)}vueActionSafe(e,t){this.depth++;try{this.vueAction(e,t)}catch(n){this.warn(`Lỗi khi chạy action: ${n.message} — action: ${Ne(e)}`)}finally{this.depth--}}vueAction(e,t){if(!e)return;if(typeof e==="string"){this.log(`EXE ${e}`),this.evalJs(e,{...t??{},...this.vueData});return}if(!j(e))return;let n=JSON.parse(JSON.stringify(e)),o={...this.vueData,...t??{}};if(n.IN!==void 0)if(typeof n.IN==="string")this.mapData({IN:n.IN},n,o);else{let r={};this.mapData(n.IN,r,o),n.IN=r}if(n.API!==void 0){try{n.API=this.bindDataToString(String(n.API),o)}catch{}if(n.HEADER!==void 0)if(typeof n.HEADER==="string")this.mapData({HEADER:n.HEADER},n,o);else this.mapData(n.HEADER,n.HEADER,o);this.defProp(n.OUT),this.callAPI(n,t);return}if(n.CALL!==void 0){this.log(`CALL ${String(n.CALL)}`),this.runAction(n.CALL,t);return}if(n.EXE!==void 0){this.log(`EXE ${String(n.EXE).replace(/\s+/g," ").slice(0,100)}`),this.evalJs(String(n.EXE),{...t??{},...this.vueData});return}if(n.FUN!==void 0){let r;try{r=this.bindDataToString(String(n.FUN),o)}catch{r=String(n.FUN)}let i=this.sandbox[r];if(typeof i==="function"){let a=i(n.IN);if(n.OUT!==void 0)this.mapData(n.OUT,this.vueData,a);if(n.CALLBACK!==void 0)this.runAction(n.CALLBACK,t)}else this.warn(`FUN "${r}" không tồn tại (simulator chỉ có các hàm FUI chuẩn)`);return}if(n.IF!==void 0){let r=!1;try{r=String(this.evalExpr(String(n.IF),{...t??{},...this.vueData}))==="true"}catch(a){this.warn(`IF "${String(n.IF)}" lỗi eval: ${a.message} → coi như false`)}this.log(`IF(${String(n.IF).slice(0,60)}) → ${r?"THEN":"ELSE"}`);let i=r?n.THEN:n.ELSE;if(typeof i==="string")this.log(`EXE ${i.slice(0,80)}`),this.evalJs(i,{...t??{},...this.vueData});else if(i!==void 0)this.runAction(i,t);return}if(n.MESS!==void 0){let r;try{r=this.bindDataToString(String(n.MESS),o)}catch{r=String(n.MESS)}this.toastCount++,this.log(`MESS \uD83D\uDD14 "${r}"`);return}if(n.MESSBOX!==void 0){let r;try{r=this.bindDataToString(String(n.MESSBOX),o)}catch{r=String(n.MESSBOX)}this.log(`MESSBOX "${r}"`);return}let s=[];for(let r of Object.keys(n))s.push(r);if(this.mapData(n,this.vueData,this.vueData,t),s.length>0)this.log(`SET ${s.map((r)=>`${r} = ${Ze(r[0]==="#"?"(cross-window)":this.getPath(this.vueData,r))}`).join(" · ")}`)}callAPI(e,t){let n=String(e.API),o=e.METHOD?String(e.METHOD):"POST";this.apiCallCount++,this.log(`API ${o} ${n} IN=${e.IN!==void 0?JSON.stringify(e.IN)?.slice(0,200):"{}"}`);let s=this.opts.apiMocks,r=Object.keys(s).find((a)=>a===n);r??=Object.keys(s).find((a)=>n.endsWith(a)||a.endsWith(n)),r??=Object.keys(s).find((a)=>n.includes(a)||a.includes(n));let i;if(r!==void 0)i=JSON.parse(JSON.stringify(s[r])),this.log(`(mock khớp "${r}")`);else i={data:[]},this.warn(`không có mock cho API "${n}" — trả mặc định {data: []}`);if(j(i)&&i.__error!==void 0){let a=j(i.__error)?i.__error:{},c={status:a.status??500,responseJSON:a.responseJSON??{Message:"Simulated error"}};if(this.log(`← HTTP ${String(c.status)} (mock lỗi)`),e.ERROROUT!==void 0)this.mapData(e.ERROROUT,this.vueData,c);if(e.ERRORCALLBACK!==void 0)this.runAction(e.ERRORCALLBACK,t);if(e.ERROROUT!==void 0)return;let l=c.responseJSON;this.warn(`API "${n}" lỗi không được bắt (không có ERROROUT) → hiện ERROR DIALOG mặc định: ${typeof l==="string"?l:j(l)?String(l.Message??""):""}`);return}if(e.OUT!==void 0){let a=i;if(typeof e.OUT==="string"&&j(i)&&Object.keys(i).length===1&&i.data)a=i.data;this.mapData(e.OUT,this.vueData,a);let c=typeof e.OUT==="string"?`${e.OUT} = ${Ze(this.vueData[e.OUT])}`:Object.keys(e.OUT).map((l)=>`${l} = ${Ze(this.getPath(this.vueData,l))}`).join(" · ");this.log(`← OUT ${c}`)}else this.log("← (không có OUT)");if(e.MESS!==void 0){let a;try{a=this.bindDataToString(String(e.MESS),this.vueData)}catch{a=String(e.MESS)}this.toastCount++,this.log(`MESS \uD83D\uDD14 "${a}"`)}if(e.CALLBACK!==void 0)this.runAction(e.CALLBACK,t)}initWatches(){let e=j(this.moduleJson.watch)?this.moduleJson.watch:{};for(let o of Object.keys(e)){if(o==="deep-watch")continue;this.setValue(this.vueData,o,this.getVueData(o,this.vueData))}let t=(o,s)=>{let r=this.getPath(this.vueData,o);this.watchSnapshots.set(`${s?"D:":"S:"}${o}`,{deep:s,snap:s?JSON.stringify(r):r})},n=e["deep-watch"];if(j(n))for(let o of Object.keys(n))t(o,!0);for(let o of Object.keys(e)){if(o==="deep-watch")continue;t(o,!1)}}checkWatches(){let e=j(this.moduleJson.watch)?this.moduleJson.watch:{},t=j(e["deep-watch"])?e["deep-watch"]:{};for(let n=0;n<ou;n++){let o=[];for(let[s,r]of this.watchSnapshots){let i=s.slice(2),a=this.getPath(this.vueData,i);if(r.deep){let c=JSON.stringify(a);if(c!==r.snap)o.push({key:i,handler:t[i],oldVal:r.snap}),r.snap=c}else if(!Object.is(a,r.snap))o.push({key:i,handler:e[i],oldVal:r.snap}),r.snap=a}if(o.length===0)return;for(let s of o){this.watchTriggerCount++,this.log(`WATCH ${s.key} thay đổi → ${Ne(s.handler)}`);let r={};this.setValue(r,s.key,s.oldVal),this.depth++;try{this.runAction(s.handler,{v_old:r})}finally{this.depth--}}}this.warn(`watch cascade vượt ${ou} vòng — nguy cơ VÒNG LẶP VÔ HẠN giữa các watch/action`)}initDialogs(){(Array.isArray(this.moduleJson.controls)?this.moduleJson.controls:[]).forEach((t,n)=>{if(!j(t)||t.el!=="hidden-container")return;let o=j(t.attr)?t.attr:{},s=t["v-model"]??o["v-model"];if(typeof s==="string"){let r=s.replace("vueData.","").split(/[.[]/)[0];this.dialogs.push({vModel:r,where:`container ${n+1}`,open:Boolean(this.getPath(this.vueData,r))})}})}checkDialogs(){for(let e of this.dialogs){let t=Boolean(this.getPath(this.vueData,e.vModel));if(t!==e.open)e.open=t,this.log(`DIALOG ${e.vModel} (${e.where}): ${t?"MỞ \uD83D\uDD32":"ĐÓNG ▪"}`)}}declareControlVars(){let e=/^[a-zA-Z0-9$_\-.[\]]*$/,t=(n)=>{if(Array.isArray(n)){n.forEach(t);return}if(!j(n))return;let o=j(n.attr)?n.attr:{};for(let[s,r]of Object.entries(o)){if(typeof r!=="string")continue;if(s==="v-model"){if(!r.includes("vueData.")&&this.getVueData(r,this.vueData)===void 0)this.mapData({[r]:void 0},this.vueData,{})}else if(s[0]===":"&&e.test(r)){if(!r.includes("vueData.")&&this.getVueData(r,this.vueData)===void 0)this.mapData({[r]:s===":items"?[]:void 0},this.vueData,{})}}if(Array.isArray(n.innerHTML))n.innerHTML.forEach(t);if(Array.isArray(n.rows))n.rows.forEach(t);if(Array.isArray(n.cols))n.cols.forEach(t)};t(this.moduleJson.controls)}collectClickables(){let e=[],t=(n,o)=>{if(Array.isArray(n)){n.forEach((a)=>t(a,o));return}if(!j(n))return;let s=j(n.attr)?n.attr:{},r=s["v-on:click"]??s[":action"]??s.action;if(r!==void 0){let a=[];for(let c of["label",":label","innerText","text"]){let l=s[c]??n[c];if(typeof l==="string"&&l)a.push(ri(l))}if(typeof n.innerHTML==="string"&&n.innerHTML)a.push(n.innerHTML.replace(/<[^>]*>/g,"").trim());e.push({labels:a,handler:r,el:typeof n.el==="string"?n.el:"?",tableItemsExpr:o})}let i=o;if(n.el==="f-table"){let a=s[":items"];if(typeof a==="string")i=a}if(Array.isArray(n.innerHTML))n.innerHTML.forEach((a)=>t(a,i));if(Array.isArray(n.rows))n.rows.forEach((a)=>t(a,i));if(Array.isArray(n.cols))n.cols.forEach((a)=>t(a,i))};return t(this.moduleJson.controls),e}simulate(e){if(this.log("── INIT ──"),j(this.moduleJson.set))Object.assign(this.vueData.v_Set,this.moduleJson.set);if(this.opts.urlParams)for(let[n,o]of Object.entries(this.opts.urlParams))this.vueData[n]=o;try{this.runAction(this.moduleJson.data)}catch(n){this.warn(`Lỗi khi init data[]: ${n.message}`)}this.declareControlVars(),this.initWatches(),this.initDialogs(),this.checkDialogs();let t=this.collectClickables();return e.forEach((n,o)=>{let s=o+1;if(j(n.set)){this.log(""),this.log(`── STEP ${s}: set ${Object.entries(n.set).map(([r,i])=>`${r}=${Ze(i)}`).join(", ")} ──`);for(let[r,i]of Object.entries(n.set))this.setValue(this.vueData,r,i)}else if(typeof n.click==="string"){this.log(""),this.log(`── STEP ${s}: click "${n.click}" ──`);let r=n.click.toLowerCase(),i=t.filter((d)=>d.labels.some((p)=>p.toLowerCase()===r)),a=i.length>0?i:t.filter((d)=>d.labels.some((p)=>p.toLowerCase().includes(r)));if(a.length===0){this.warn(`không tìm thấy control có label "${n.click}" kèm v-on:click/:action`);return}let c=typeof n.nth==="number"?n.nth-1:0;if(a.length>1)this.log(`(khớp ${a.length} control — dùng #${c+1})`);let l=a[Math.min(c,a.length-1)],u;if(l.tableItemsExpr!==void 0||n.item!==void 0||n.index!==void 0){let d=n.item,p=typeof n.index==="number"?n.index:0;if(d===void 0&&l.tableItemsExpr){let m=this.getVueData(l.tableItemsExpr,this.vueData);if(Array.isArray(m)&&m.length>0)d=m[Math.min(p,m.length-1)],this.log(`(control trong f-table — dùng row ${p} của ${l.tableItemsExpr})`);else d={},this.warn(`control nằm trong f-table nhưng "${l.tableItemsExpr??""}" rỗng — item = {} (truyền "item" trong event để chỉ định)`)}u={$row:d,item:d,index:p}}if(n.cancelConfirm===!0)u={...u??{},__cancelConfirm:!0};this.runAction(l.handler,u)}else if(typeof n.call==="string"){this.log(""),this.log(`── STEP ${s}: call ${n.call} ──`);let r;if(n.item!==void 0||n.index!==void 0)r={$row:n.item,item:n.item,index:typeof n.index==="number"?n.index:0};if(n.cancelConfirm===!0)r={...r??{},__cancelConfirm:!0};this.runAction(n.call,r)}else if(typeof n.exe==="string"){this.log(""),this.log(`── STEP ${s}: exe ──`),this.log(`EXE ${n.exe.slice(0,100)}`);try{this.evalJs(n.exe,{...this.vueData})}catch(r){this.warn(`exe lỗi: ${r.message}`)}}else if(typeof n.assert==="string"){let r=typeof n.label==="string"?n.label:n.assert,i=!1;try{i=Boolean(this.evalExpr(n.assert,{...this.vueData}))}catch(a){this.warn(`assert "${n.assert}" lỗi eval: ${a.message}`)}this.assertResults.push({label:r,ok:i}),this.log(""),this.log(`── STEP ${s}: assert ${i?"✔":"✘"} ${r} ──`);return}else{this.warn(`event ${s} không hợp lệ — cần một trong: set / click / call / exe / assert`);return}this.checkWatches(),this.checkDialogs()}),{report:this.buildReport(),hasFailures:this.assertResults.some((n)=>!n.ok)}}buildRightDefaultNote(){let e=[];if(this.systemRightDefaulted)e.push("SystemRight=9 (mặc định, quyền cao nhất)");if(this.functionRightDefaulted)e.push(this.scannedFunctionRightCodes.length?`FunctionRight=[${this.scannedFunctionRightCodes.join(", ")}] (mặc định — quét được từ module.json)`:"FunctionRight=[] (mặc định — không quét được mã nào trong module.json)");if(e.length===0)return null;return`user preview: ${e.join(" · ")}. rightTest()/v-if theo FunctionRight khác mã trên sẽ trả false — truyền tham số \`user\` để test vai trò hạn chế hơn.`}buildReport(){let e=[...this.trace,""];e.push("═══ KẾT QUẢ ═══");let t=this.buildRightDefaultNote();if(t)e.push(`ℹ ${t}`);if(this.functionRightDefaulted){let s=to(this.scannedFunctionRightCodes,this.opts.definedFunctionRights??null);if(s)e.push(s)}for(let s of this.assertResults)e.push(`${s.ok?"✔":"✘"} assert: ${s.label}`);if(this.warnings.size>0){e.push("","── Cảnh báo ──");for(let s of this.warnings)e.push(`⚠ ${s}`)}if(this.stubUsed.size>0)e.push("",`(stub đã dùng — kết quả các hàm này KHÔNG phản ánh runtime thật: ${[...this.stubUsed].join(", ")})`);e.push("","── STATE CUỐI ──");let n=new Set(["p_domain","p_routers","p_params","p_urlReferrer","p_router","v_Loading","v_LoadingOverlay","v_containerOn","user","v_Set"]);for(let[s,r]of Object.entries(this.vueData)){if(n.has(s))continue;if(j(r)&&(r.API!==void 0||r.CALL!==void 0||r.EXE!==void 0||r.FUN!==void 0||r.IF!==void 0))continue;e.push(`${s} = ${Ze(r)}`)}let o=this.dialogs.filter((s)=>s.open).map((s)=>s.vModel);return e.push("",`Thống kê: ${this.apiCallCount} API call · ${this.toastCount} toast · ${this.watchTriggerCount} watch trigger · ${this.assertResults.filter((s)=>s.ok).length}/${this.assertResults.length} assert đạt${o.length>0?` · dialog đang mở: ${o.join(", ")}`:""}`),e.join(`
246
+ `)}}function au(e){let t=new ct(e.paths);return qs({store:t,session:e.session,api:e.api,activeTarget:async()=>{let n=e.targets.cwdTarget();if(n)return{projectId:n.projectId,moduleId:n.moduleId??"",moduleDir:n.moduleDir??""};return e.session.getTarget()}}),t}async function cu(e){try{return await e()}catch(t){if(t instanceof Error)t.message=Ve(t.message);throw t}}async function lu(e,t,n){let o=au(e),s=await Bt(t),r=await cr(kn(o,e.session),n.apiMocks,t),i=`${s?`${s}
247
247
 
248
248
  `:""}${r?`${r}
249
249
 
250
- `:""}`,a=await ru(()=>sr(t,n));return{text:Ve(`${i}Render UI: ${t}
250
+ `:""}`,a=await cu(()=>ar(t,n));return{text:Ve(`${i}Render UI: ${t}
251
251
 
252
252
  ${a.report}
253
- `),hasFailures:a.hasFailures,screenshots:a.screenshotPath?[a.screenshotPath]:[]}}async function iu(e,t){ou(e);let n=await ru(()=>Yl(t)),o=n.runs.map((r)=>`${r.hasFailures?"✘":"✔"} ${r.caseName} · ${r.device}${r.screenshotPath?` → ${Ff(r.screenshotPath)}`:""}`).join(`
253
+ `),hasFailures:a.hasFailures,screenshots:a.screenshotPath?[a.screenshotPath]:[]}}async function uu(e,t){au(e);let n=await cu(()=>tu(t)),o=n.runs.map((r)=>`${r.hasFailures?"✘":"✔"} ${r.caseName} · ${r.device}${r.screenshotPath?` → ${Bf(r.screenshotPath)}`:""}`).join(`
254
254
  `),s=[`${n.header}
255
255
 
256
256
  ── SUMMARY ──
@@ -258,122 +258,124 @@ ${o}`];for(let r of n.runs)if(s.push(`
258
258
  ══ ${r.caseName} · ${r.device} ══
259
259
  ${r.report}`),r.screenshotPath)s.push(`screenshot: ${r.screenshotPath}`);return{text:Ve(`${s.join(`
260
260
  `)}
261
- `),hasFailures:n.hasFailures,screenshots:n.runs.flatMap((r)=>r.screenshotPath?[r.screenshotPath]:[])}}import{dirname as au,join as Lf}from"node:path";async function oi(e,t,n){let o=kn(new ct(e.paths),e.session),s=await Bt(t),r=await ir(o,n.apiMocks,t),i=`${s?`${s}
261
+ `),hasFailures:n.hasFailures,screenshots:n.runs.flatMap((r)=>r.screenshotPath?[r.screenshotPath]:[])}}import{dirname as du,join as Jf}from"node:path";async function ii(e,t,n){let o=kn(new ct(e.paths),e.session),s=await Bt(t),r=await cr(o,n.apiMocks,t),i=`${s?`${s}
262
262
 
263
263
  `:""}${r?`${r}
264
264
 
265
- `:""}`,a=await L(Lf(t,"module.json"));if(!a.trim())throw new m(`module.json is empty or missing in ${t}`);let c;try{c=JSON.parse(a)}catch(v){throw new m(`module.json has a JSON syntax error: ${v.message}; fix it, simulate needs valid JSON`)}if(!j(c))throw new m("module.json root is not an object");let l=await Wt(au(au(t))),{mocks:u,trace:d}=await nu(o,c,t,n.apiMocks??{}),p=new ni(c,{apiMocks:u,user:n.user,urlParams:n.urlParams,definedFunctionRights:l.functionRights}),{report:h,hasFailures:w}=p.simulate(n.scenario??[]),y=d.length>0?`
265
+ `:""}`,a=await F(Jf(t,"module.json"));if(!a.trim())throw new h(`module.json is empty or missing in ${t}`);let c;try{c=JSON.parse(a)}catch(v){throw new h(`module.json has a JSON syntax error: ${v.message}; fix it, simulate needs valid JSON`)}if(!j(c))throw new h("module.json root is not an object");let l=await Wt(du(du(t))),{mocks:u,trace:d}=await iu(o,c,t,n.apiMocks??{}),p=new si(c,{apiMocks:u,user:n.user,urlParams:n.urlParams,definedFunctionRights:l.functionRights}),{report:m,hasFailures:w}=p.simulate(n.scenario??[]),y=d.length>0?`
266
266
 
267
267
  ── Live/Cache data (SPs without mocks, auto-resolved) ──
268
268
  ${d.join(`
269
269
  `)}`:"";return{text:Ve(`${i}Simulate: ${t}
270
270
 
271
- ${h}${y}
272
- `),hasFailures:w}}var et={module:{type:"string",short:"m"}},du={data:{type:"string"},"data-file":{type:"string"}};function Rt(e){if(k(`${e.notes.join(`
271
+ ${m}${y}
272
+ `),hasFailures:w}}var et={module:{type:"string",short:"m"}},fu={data:{type:"string"},"data-file":{type:"string"}};function Rt(e){if(b(`${e.notes.join(`
273
273
  `)}
274
- `),e.data)k(e.data)}async function pu(e){return e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module"))}async function In(e){return e.targets.resolveModuleDir(f(e.values,"module-dir"),f(e.values,"project"),f(e.values,"module"))}var Hf={name:"module validate",usage:"fui module validate [-p P -m M | --module-dir D]",summary:"Static semantic check of module.json (no network); fix every error before publishing",options:{...et,"module-dir":{type:"string"}},async run(e){let t=await In(e),n=await Ds(t);for(let o of n.notes)T(`# ${o}`);if(k(n.data??""),n.errorCount>0)throw new Xe(`module.json has ${n.errorCount} error(s)`,[...n.notes,`Validate: ${t}`,"",n.data??""].join(`
275
- `))}},Vf={name:"module outline",usage:"fui module outline [-p P -m M | --module-dir D]",summary:"Text layout tree of module.json: grid widths, bindings, actions, dialogs, unused state (no network)",options:{...et,"module-dir":{type:"string"}},async run(e){let t=await xs(await In(e));for(let n of t.notes)T(`# ${n}`);k(t.data??"")}},mu={device:{type:"string"},viewport:{type:"string"},"settle-ms":{type:"string"},"screenshot-steps":{type:"boolean"}},cu=["desktop","tablet","mobile"];function hu(e){let n=(typeof e.device==="string"?e.device:void 0)?.split(",").map((r)=>r.trim());for(let r of n??[])if(!cu.includes(r))throw new m(`--device takes ${cu.join(", ")}`);let o;if(typeof e.viewport==="string"){let r=/^(\d+)x(\d+)$/i.exec(e.viewport);if(!r)throw new m("--viewport takes WIDTHxHEIGHT, e.g. 1366x768");o={width:Number(r[1]),height:Number(r[2])}}let s;if(typeof e["settle-ms"]==="string"){if(s=Number(e["settle-ms"]),!Number.isFinite(s)||s<0)throw new m("--settle-ms takes a number of milliseconds")}return{devices:n,viewport:o,settleMs:s,screenshotSteps:e["screenshot-steps"]===!0?!0:void 0}}var lr=[["scenario","array"],["mocks","object"],["user","object"],["url-params","object"],["vue-data","object"]],fu=[...lr.map(([e])=>[e,`${e}-file`]),["render"],["device"],["viewport"],["settle-ms"],["screenshot-steps"],["offline"],["brief"]];function Wf(e,t){let n={};for(let o of fu.flat()){let s=e[o];if(typeof s!=="string"&&typeof s!=="boolean")continue;n[o]=s==="-"?JSON.stringify(t[o]):typeof s==="string"&&o.endsWith("-file")?Uf(s):s}return n}function lu(e,t){if(!t){k(e);return}let n=Os(e);T(`# brief: ${n.kept} of ${n.total} lines (failures, warnings, asserts, stats, screenshots); full report: rerun without --brief`),k(n.text)}var qf={name:"module simulate",usage:`fui module simulate [-p P -m M | --module-dir D] [--scenario '[{"click":"Save"}]'] [--mocks '{"Fn":{"data":[]}}']
274
+ `),e.data)b(e.data)}async function gu(e){return e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module"))}async function In(e){return e.targets.resolveModuleDir(f(e.values,"module-dir"),f(e.values,"project"),f(e.values,"module"))}var Gf={name:"module validate",usage:"fui module validate [-p P -m M | --module-dir D]",summary:"Static semantic check of module.json (no network); fix every error before publishing",options:{...et,"module-dir":{type:"string"}},async run(e){let t=await In(e),n=await _s(t);for(let o of n.notes)T(`# ${o}`);if(b(n.data??""),n.errorCount>0)throw new Xe(`module.json has ${n.errorCount} error(s)`,[...n.notes,`Validate: ${t}`,"",n.data??""].join(`
275
+ `))}},zf={name:"module outline",usage:"fui module outline [-p P -m M | --module-dir D]",summary:"Text layout tree of module.json: grid widths, bindings, actions, dialogs, unused state (no network)",options:{...et,"module-dir":{type:"string"}},async run(e){let t=await Ms(await In(e));for(let n of t.notes)T(`# ${n}`);b(t.data??"")}},wu={device:{type:"string"},viewport:{type:"string"},"settle-ms":{type:"string"},"screenshot-steps":{type:"boolean"}},pu=["desktop","tablet","mobile"];function yu(e){let n=(typeof e.device==="string"?e.device:void 0)?.split(",").map((r)=>r.trim());for(let r of n??[])if(!pu.includes(r))throw new h(`--device takes ${pu.join(", ")}`);let o;if(typeof e.viewport==="string"){let r=/^(\d+)x(\d+)$/i.exec(e.viewport);if(!r)throw new h("--viewport takes WIDTHxHEIGHT, e.g. 1366x768");o={width:Number(r[1]),height:Number(r[2])}}let s;if(typeof e["settle-ms"]==="string"){if(s=Number(e["settle-ms"]),!Number.isFinite(s)||s<0)throw new h("--settle-ms takes a number of milliseconds")}return{devices:n,viewport:o,settleMs:s,screenshotSteps:e["screenshot-steps"]===!0?!0:void 0}}var dr=[["scenario","array"],["mocks","object"],["user","object"],["url-params","object"],["vue-data","object"]],bu=[...dr.map(([e])=>[e,`${e}-file`]),["render"],["device"],["viewport"],["settle-ms"],["screenshot-steps"],["offline"],["brief"]];function Xf(e,t){let n={};for(let o of bu.flat()){let s=e[o];if(typeof s!=="string"&&typeof s!=="boolean")continue;n[o]=s==="-"?JSON.stringify(t[o]):typeof s==="string"&&o.endsWith("-file")?Kf(s):s}return n}function mu(e,t){if(!t){b(e);return}let n=Ls(e);T(`# brief: ${n.kept} of ${n.total} lines (failures, warnings, asserts, stats, screenshots); full report: rerun without --brief`),b(n.text)}var Yf={name:"module simulate",usage:`fui module simulate [-p P -m M | --module-dir D] [--scenario '[{"click":"Save"}]'] [--mocks '{"Fn":{"data":[]}}']
276
276
  [--user '{"SystemRight":3}'] [--url-params '{"id":"1"}'] (each also as --<name>-file path)
277
277
  real browser: --render [--device desktop|tablet|mobile] [--viewport 1366x768] [--vue-data {...}] [--settle-ms N]
278
278
  [--screenshot-steps] [--refresh-assets | --offline] (needs Playwright)
279
279
  --last: reuse this module's last input (files are re-read); flags given now replace their saved value
280
- --brief: print only failures, warnings, asserts, stats and screenshot paths`,summary:"Run module.json in the simulated FUI runtime (or --render: a real browser); fails when an assert or the page fails",options:{...et,"module-dir":{type:"string"},...Bo("scenario","mocks","user","url-params","vue-data"),...mu,render:{type:"boolean"},"refresh-assets":{type:"boolean"},offline:{type:"boolean"},last:{type:"boolean"},brief:{type:"boolean"}},async run(e){let t=await In(e),n=e.values;if(n.last===!0){let a=await _s(t,"module simulate");if(!a)throw new m("no saved input for this module yet; run fui module simulate with flags first");let c=Ms(n,a,fu);n=c.merged,T(`# reused last input: ${c.reused.join(", ")||"none (every input given now)"}`)}let o={};for(let[a,c]of lr)o[a]=await Zn(n,a,c);await yl(t,"module simulate",Wf(n,o));let s={scenario:o.scenario,apiMocks:o.mocks,user:o.user,urlParams:o["url-params"]};if(n.render!==!0){let a=["vue-data","vue-data-file","device","viewport","settle-ms","screenshot-steps"];for(let l of[...a,"refresh-assets","offline"])if(n[l]!==void 0)throw new m(`--${l} needs --render`);let c=await oi(_(e),t,s);if(lu(c.text,n.brief===!0),c.hasFailures)throw new Xe("simulate: an assert failed",c.text);return}let r=hu(n);if((r.devices?.length??0)>1)throw new m("module render takes one --device; component preview takes several");let i=await su(_(e),t,{...s,scenario:s.scenario??[],apiMocks:s.apiMocks??{},vueData:o["vue-data"],device:r.devices?.[0],viewport:r.viewport,settleMs:r.settleMs,screenshotSteps:r.screenshotSteps,refreshAssets:n["refresh-assets"]===!0,offline:n.offline===!0});lu(i.text,n.brief===!0);for(let a of i.screenshots)if(!i.text.includes(a))k(`screenshot: ${a}
281
- `);if(i.hasFailures)throw new Xe("render: the page has failures",i.text)}};function Bf(e){return e.split(`
280
+ --brief: print only failures, warnings, asserts, stats and screenshot paths`,summary:"Run module.json in the simulated FUI runtime (or --render: a real browser); fails when an assert or the page fails",options:{...et,"module-dir":{type:"string"},...Ko("scenario","mocks","user","url-params","vue-data"),...wu,render:{type:"boolean"},"refresh-assets":{type:"boolean"},offline:{type:"boolean"},last:{type:"boolean"},brief:{type:"boolean"}},async run(e){let t=await In(e),n=e.values;if(n.last===!0){let a=await Fs(t,"module simulate");if(!a)throw new h("no saved input for this module yet; run fui module simulate with flags first");let c=Us(n,a,bu);n=c.merged,T(`# reused last input: ${c.reused.join(", ")||"none (every input given now)"}`)}let o={};for(let[a,c]of dr)o[a]=await Zn(n,a,c);await $l(t,"module simulate",Xf(n,o));let s={scenario:o.scenario,apiMocks:o.mocks,user:o.user,urlParams:o["url-params"]};if(n.render!==!0){let a=["vue-data","vue-data-file","device","viewport","settle-ms","screenshot-steps"];for(let l of[...a,"refresh-assets","offline"])if(n[l]!==void 0)throw new h(`--${l} needs --render`);let c=await ii(_(e),t,s);if(mu(c.text,n.brief===!0),c.hasFailures)throw new Xe("simulate: an assert failed",c.text);return}let r=yu(n);if((r.devices?.length??0)>1)throw new h("module render takes one --device; component preview takes several");let i=await lu(_(e),t,{...s,scenario:s.scenario??[],apiMocks:s.apiMocks??{},vueData:o["vue-data"],device:r.devices?.[0],viewport:r.viewport,settleMs:r.settleMs,screenshotSteps:r.screenshotSteps,refreshAssets:n["refresh-assets"]===!0,offline:n.offline===!0});mu(i.text,n.brief===!0);for(let a of i.screenshots)if(!i.text.includes(a))b(`screenshot: ${a}
281
+ `);if(i.hasFailures)throw new Xe("render: the page has failures",i.text)}};function Qf(e){return e.split(`
282
282
  `).filter((t)=>/[⚠❌]|^Stats:/.test(t)).join(`
283
- `)}var Jf={name:"module check",usage:`fui module check [-p P -m M | --module-dir D] [--scenario [...]] [--mocks {...}] [--user {...}] [--url-params {...}] [--last] [--full]
283
+ `)}var Zf={name:"module check",usage:`fui module check [-p P -m M | --module-dir D] [--scenario [...]] [--mocks {...}] [--user {...}] [--url-params {...}] [--last] [--full]
284
284
  validate, then outline and simulate in one step; stops after validate when it finds errors
285
- prints problems, asserts and stats only (--full: every report); --last reuses module simulate's saved input`,summary:"validate + outline + simulate in one step, problems and stats only; fails when any part fails",options:{...et,"module-dir":{type:"string"},...Bo("scenario","mocks","user","url-params"),last:{type:"boolean"},full:{type:"boolean"}},async run(e){let t=await In(e),n=e.values.full===!0,o=e.values;if(o.last===!0){let c=await _s(t,"module simulate");if(!c)throw new m("no saved input for this module yet; run fui module simulate with flags first");let l=lr.filter(([d])=>d!=="vue-data").map(([d])=>[d,`${d}-file`]),u=Ms(o,c,l);o=u.merged,T(`# reused last input: ${u.reused.join(", ")||"none (every input given now)"}`)}let s=await Ds(t);for(let c of s.notes)T(`# ${c}`);if(k(`── validate ──
285
+ prints problems, asserts and stats only (--full: every report); --last reuses module simulate's saved input`,summary:"validate + outline + simulate in one step, problems and stats only; fails when any part fails",options:{...et,"module-dir":{type:"string"},...Ko("scenario","mocks","user","url-params"),last:{type:"boolean"},full:{type:"boolean"}},async run(e){let t=await In(e),n=e.values.full===!0,o=e.values;if(o.last===!0){let c=await Fs(t,"module simulate");if(!c)throw new h("no saved input for this module yet; run fui module simulate with flags first");let l=dr.filter(([d])=>d!=="vue-data").map(([d])=>[d,`${d}-file`]),u=Us(o,c,l);o=u.merged,T(`# reused last input: ${u.reused.join(", ")||"none (every input given now)"}`)}let s=await _s(t);for(let c of s.notes)T(`# ${c}`);if(b(`── validate ──
286
286
  ${s.data??""}`),s.errorCount>0)throw new Xe(`module.json has ${s.errorCount} error(s); fix them, then run fui module check again`,[`Validate: ${t}`,"",s.data??""].join(`
287
- `));let r=await xs(t);k(`
287
+ `));let r=await Ms(t);b(`
288
288
  ── outline ──
289
- ${n?r.data??"":`${Bf(r.data??"")}
290
- `}`);let i={};for(let[c,l]of lr)if(c!=="vue-data")i[c]=await Zn(o,c,l);let a=await oi(_(e),t,{scenario:i.scenario,apiMocks:i.mocks,user:i.user,urlParams:i["url-params"]});if(k(`
289
+ ${n?r.data??"":`${Qf(r.data??"")}
290
+ `}`);let i={};for(let[c,l]of dr)if(c!=="vue-data")i[c]=await Zn(o,c,l);let a=await ii(_(e),t,{scenario:i.scenario,apiMocks:i.mocks,user:i.user,urlParams:i["url-params"]});if(b(`
291
291
  ── simulate ──
292
- ${n?a.text:Os(a.text).text}`),a.hasFailures)throw new Xe("simulate: an assert failed",a.text)}},Kf={name:"component preview",usage:`fui component preview <uc-name> [-p P] [-m M] [--cases '[{"name":"empty","props":{"items":[]}}]'] [--device desktop,mobile]
293
- [--viewport 1366x420] [--user {...}] [--settle-ms N] [--screenshot-steps] (needs Playwright)`,summary:"Render one local .vue alone in a real browser per case x device: JS errors, Vue warns, DOM audit, screenshots",options:{...et,...Bo("cases","user"),...mu},async run(e){let{values:t}=e,n=e.positionals[0];if(!n)throw new m("missing <uc-name> (file name without .vue)");let o=hu(t),s=await iu(_(e),{componentName:n,projectId:f(t,"project"),moduleId:f(t,"module"),cases:await Zn(t,"cases","array"),devices:o.devices,viewport:o.viewport,user:await Zn(t,"user","object"),settleMs:o.settleMs,screenshotSteps:o.screenshotSteps});if(k(s.text),s.hasFailures)throw new Xe("component preview: a run has failures",s.text)}},Gf={name:"module preview",usage:"fui module preview [-p P -m M | --module-dir D]",summary:"Diff local vs server, run the validator and issue an approval token for publish html",options:{...et,"module-dir":{type:"string"}},async run(e){Rt(await ll(_(e),await In(e)))}},zf={name:"module publish html",usage:"fui module publish html --approval <token> [-p P -m M | --module-dir D]",summary:"Publish the whole staged module (needs the token from fui module preview, after user approval)",options:{...et,"module-dir":{type:"string"},approval:{type:"string"}},async run(e){Rt(await ul(_(e),await In(e),f(e.values,"approval")))}},Xf={name:"module publish json",usage:"fui module publish json [-p P -m M]",summary:"Push local module.json (skips when unchanged since the last publish/sync)",options:et,async run(e){Rt(await Xo(_(e),await pu(e)))}};function uu(e){let t=e==="script"?"script.js":"style.css";return{name:`module publish ${e}`,usage:`fui module publish ${e} [-p P -m M]`,summary:`Push local ${t} (an empty file clears it on the server; a missing file is refused)`,options:et,async run(n){Rt(await Yo(_(n),await pu(n),t))}}}function ar(e,t,n){return{name:`component ${e}`,usage:`fui component ${e}${e==="list"?"":" <name>"} [-p P [-m M]]`,summary:n,options:et,async run(o){let s=o.positionals[0];if(e!=="list"&&!s)throw new m("missing <name>");Rt(await t(o,s))}}}async function cr(e){return hn(_(e),f(e.values,"project"),f(e.values,"module"))}var Yf=[ar("list",async(e)=>pl(_(e),await cr(e)),"Refresh _components.json from the server (archives .vue files the server dropped)"),ar("new",async(e,t)=>Qo(_(e),await cr(e),t),"Create a component on the server from local components/<name>.vue"),ar("update",async(e,t)=>Zo(_(e),await cr(e),t),"Overwrite an existing component from local components/<name>.vue (skips when unchanged)"),ar("delete",async(e,t)=>ml(_(e),await cr(e),t),"Delete a component on the server (ask the user first); archives the local .vue")],Qf={name:"project new",usage:"fui project new (--data '<json>' | --data - | --data-file f)",summary:"Create a project (needs SystemRight>=3 on fp; server reads ProjectID, ProjectName, GroupName, Framework, pDomain, Router)",options:du,async run(e){Rt(await hl(_(e),await Ts(e.values)))}},Zf={name:"project update",usage:"fui project update (--data '<json with id>' | --data - | --data-file f)",summary:"Edit project metadata; projectData.id is required",options:du,async run(e){Rt(await fl(_(e),await Ts(e.values)))}},eg={name:"project publish-config",usage:"fui project publish-config [-p project]",summary:"Push local <project>/project.json (menu, routes, settings); skips when unchanged",async run(e){let{projectId:t}=await e.targets.resolveProject(f(e.values,"project"));Rt(await er(_(e),t))}},gu=[Hf,Vf,qf,Jf,Gf,zf,Xf,uu("script"),uu("css"),...Yf,Kf,Qf,Zf,eg];import{rename as tg}from"node:fs/promises";import{join as wu}from"node:path";async function bu(e,t,n){let o=await ce(e);if(!t)return{rows:await o.api.listImportFiles(void 0,n),notes:["no project given: read-only query, local workspace not synced"]};let s=await o.syncImportsLocal(t,n);return{rows:s.list,notes:[`_imports.json synced for ${n?`module ${n}`:`project ${t}`} (${s.count} file(s), metadata only; content: fui import get <fileId>)`,...s.removed.length>0?[`removed orphaned local files: ${s.removed.join(", ")}`]:[]]}}async function ku(e,t){return(await e.api()).listFrameworkImportFiles(t)}async function vu(e,t){let o=(await(await e.api()).getFileContent(t))?.data?.FileContent??"",s=await Ss(e.paths,t,o);return{notes:s.length>0?s.map((r)=>`written: ${r}`):["no local _imports.json declares this fileID yet; content printed only. materialize it: fui import list -p <project> [-m <module>]"],data:o}}async function $u(e,t,n){let o=await(await e.api()).uploadFileContent(t,n),s=await Ss(e.paths,t,n);return{notes:[`ok uploaded content of import ${t}`,...s.map((r)=>`local copy synced: ${r}`)],data:`${JSON.stringify(o)}
294
- `}}async function Su(e,t){let n=await ce(e),{effectiveSort:o,note:s}=js(t.fileSort,t.moduleId?"module":"project"),r=await n.api.insertImportFile({...t.extra,ProjectID:t.projectId,ModuleID:t.moduleId,FileType:t.fileType,ContentType:t.contentType,FileName:t.fileName,FileSort:o}),i=["ok import added",...s?[s]:[]];if(t.projectId)try{let{count:a}=await n.syncImportsLocal(t.projectId,t.moduleId);i.push(`_imports.json synced for ${t.moduleId?`module ${t.moduleId}`:`project ${t.projectId}`} (${a} file(s))`)}catch{i.push("warning: import added but the _imports.json sync failed")}return{notes:i,data:`${JSON.stringify(r)}
295
- `}}async function ju(e,t){let n=await ce(e),o=await $s(e.paths,t.fileId),s=t.fileSort,r="";if(t.fileSort!==void 0&&o.length>0){let u=js(t.fileSort,o.some((d)=>d.moduleId)?"module":"project");s=u.effectiveSort,r=u.note}let i=await n.api.updateImportFile(t.fileId,{...t.extra,FileType:t.fileType,ContentType:t.contentType,FileName:t.fileName,FileSort:s}),a=ln(t.contentType)?`imports/${Gn(t.fileName,t.fileId,t.fileType)}`:void 0,c=[],l=new Set;for(let u of o){let d=u.entry.filePath;if(d&&a&&a!==d)try{await tg(wu(u.baseDir,d),wu(u.baseDir,a)),c.push(`local file moved: ${d} -> ${a}`)}catch{}let p=`${u.projectId}::${u.moduleId??""}`;if(l.has(p))continue;l.add(p);let h=u.moduleId?`module ${u.moduleId}`:`project ${u.projectId}`;try{let{removed:w}=await n.syncImportsLocal(u.projectId,u.moduleId);c.push(`${h}: _imports.json synced${w.length>0?`, removed orphaned ${w.join(", ")}`:""}`)}catch{c.push(`${h}: sync failed`)}}return{notes:["ok import updated",...r?[r]:[],...c],data:`${JSON.stringify(i)}
296
- `}}async function Iu(e,t){let n=await ce(e),o=await $s(e.paths,t),s=await n.api.deleteImportFile(t),r=[],i=new Set;for(let a of o){let c=`${a.projectId}::${a.moduleId??""}`;if(i.has(c))continue;i.add(c);let l=a.moduleId?`module ${a.moduleId}`:`project ${a.projectId}`;try{let{removed:u}=await n.syncImportsLocal(a.projectId,a.moduleId);r.push(`${l}: _imports.json synced${u.length>0?`, removed ${u.join(", ")}`:""}`)}catch{r.push(`${l}: sync failed`)}}return{notes:[`ok import ${t} deleted`,...r],data:`${JSON.stringify(s)}
297
- `}}var ng={1:"View only",2:"Develop",3:"Admin"};function so(e){let t=typeof e==="number"?e:Number(e);if(!Number.isFinite(t))return String(e??"");let n=ng[t];return n?`${t} (${n})`:String(t)}function yu(e,t){let n=t.trim().toLowerCase();return e.find((o)=>String(o.UserID??"").trim().toLowerCase()===n)}function ur(e,t){return e.map((n)=>({userId:String(n.UserID??"?"),username:n.UserName||"—",fullName:n.FullName||"—",...t?{right:so(n.UserRight)}:{}}))}async function Cu(e,t,n){return(await e.api()).searchUsers(t,n??"%")}async function Ru(e,t){return(await e.api()).listProjectUserRights(t)}async function Tu(e,t,n){let o=await e.api(),s=`project ${t.projectId}`,r=t.userId.trim();if(!r)throw new m("empty userId; find the real UserID: fui user search <text>");let i=yu(await o.listProjectUserRights(t.projectId),r);if(n==="new"&&i)throw new m(`user "${r}" already has right ${so(i.UserRight)} on ${s}; change it: fui user set ${r} --right N`);if(n!=="new"&&!i)throw new m(`user "${r}" has no right on ${s}; ${n==="update"?`grant one: fui user add ${r} --right N`:"nothing to revoke; see: fui user list"}`);let a=n==="delete"?0:t.userRight;await o.updateProjectUserRight(t.projectId,a,JSON.stringify([r]));let c=await o.listProjectUserRights(t.projectId),l=yu(c,r),u=n==="delete"?`ok revoked the right of "${r}" on ${s}`:`ok set right ${so(a)} for "${r}" on ${s}`,d=n==="delete"?l?`warning: post-check: the user is still listed with right ${so(l.UserRight)}; the server did not remove it`:"post-check: user no longer listed":l?`post-check: ${r} has ${so(l.UserRight)}`:"warning: post-check: user not found in the list after the write; check: fui user list";return{notes:[u,d,`current rights on ${s} (${c.length}):`],rows:c}}var Pu={module:{type:"string",short:"m"}},Nu={type:{type:"string"},"content-type":{type:"string"},name:{type:"string"},sort:{type:"string"},set:{type:"string",multiple:!0}};function dr(e){if(k(`${e.notes.join(`
292
+ ${n?a.text:Ls(a.text).text}`),a.hasFailures)throw new Xe("simulate: an assert failed",a.text)}},eg={name:"component preview",usage:`fui component preview <uc-name> [-p P] [-m M] [--cases '[{"name":"empty","props":{"items":[]}}]'] [--device desktop,mobile]
293
+ [--viewport 1366x420] [--user {...}] [--settle-ms N] [--screenshot-steps] (needs Playwright)`,summary:"Render one local .vue alone in a real browser per case x device: JS errors, Vue warns, DOM audit, screenshots",options:{...et,...Ko("cases","user"),...wu},async run(e){let{values:t}=e,n=e.positionals[0];if(!n)throw new h("missing <uc-name> (file name without .vue)");let o=yu(t),s=await uu(_(e),{componentName:n,projectId:f(t,"project"),moduleId:f(t,"module"),cases:await Zn(t,"cases","array"),devices:o.devices,viewport:o.viewport,user:await Zn(t,"user","object"),settleMs:o.settleMs,screenshotSteps:o.screenshotSteps});if(b(s.text),s.hasFailures)throw new Xe("component preview: a run has failures",s.text)}},tg={name:"module preview",usage:"fui module preview [-p P -m M | --module-dir D]",summary:"Diff local vs server, run the validator and issue an approval token for publish html",options:{...et,"module-dir":{type:"string"}},async run(e){Rt(await ml(_(e),await In(e)))}},ng={name:"module publish html",usage:"fui module publish html --approval <token> [-p P -m M | --module-dir D]",summary:"Publish the whole staged module (needs the token from fui module preview, after user approval)",options:{...et,"module-dir":{type:"string"},approval:{type:"string"}},async run(e){Rt(await hl(_(e),await In(e),f(e.values,"approval")))}},og={name:"module publish json",usage:"fui module publish json [-p P -m M]",summary:"Push local module.json (skips when unchanged since the last publish/sync)",options:et,async run(e){Rt(await Qo(_(e),await gu(e)))}};function hu(e){let t=e==="script"?"script.js":"style.css";return{name:`module publish ${e}`,usage:`fui module publish ${e} [-p P -m M]`,summary:`Push local ${t} (an empty file clears it on the server; a missing file is refused)`,options:et,async run(n){Rt(await Zo(_(n),await gu(n),t))}}}function lr(e,t,n){return{name:`component ${e}`,usage:`fui component ${e}${e==="list"?"":" <name>"} [-p P [-m M]]`,summary:n,options:et,async run(o){let s=o.positionals[0];if(e!=="list"&&!s)throw new h("missing <name>");Rt(await t(o,s))}}}async function ur(e){return hn(_(e),f(e.values,"project"),f(e.values,"module"))}var rg=[lr("list",async(e)=>gl(_(e),await ur(e)),"Refresh _components.json from the server (archives .vue files the server dropped)"),lr("new",async(e,t)=>er(_(e),await ur(e),t),"Create a component on the server from local components/<name>.vue"),lr("update",async(e,t)=>tr(_(e),await ur(e),t),"Overwrite an existing component from local components/<name>.vue (skips when unchanged)"),lr("delete",async(e,t)=>wl(_(e),await ur(e),t),"Delete a component on the server (ask the user first); archives the local .vue")],sg={name:"project new",usage:"fui project new (--data '<json>' | --data - | --data-file f)",summary:"Create a project (needs SystemRight>=3 on fp; server reads ProjectID, ProjectName, GroupName, Framework, pDomain, Router)",options:fu,async run(e){Rt(await yl(_(e),await As(e.values)))}},ig={name:"project update",usage:"fui project update (--data '<json with id>' | --data - | --data-file f)",summary:"Edit project metadata; projectData.id is required",options:fu,async run(e){Rt(await bl(_(e),await As(e.values)))}},ag={name:"project publish-config",usage:"fui project publish-config [-p project]",summary:"Push local <project>/project.json (menu, routes, settings); skips when unchanged",async run(e){let{projectId:t}=await e.targets.resolveProject(f(e.values,"project"));Rt(await nr(_(e),t))}},ku=[Gf,zf,Yf,Zf,tg,ng,og,hu("script"),hu("css"),...rg,eg,sg,ig,ag];import{rename as cg}from"node:fs/promises";import{join as vu}from"node:path";async function Su(e,t,n){let o=await ce(e);if(!t)return{rows:await o.api.listImportFiles(void 0,n),notes:["no project given: read-only query, local workspace not synced"]};let s=await o.syncImportsLocal(t,n);return{rows:s.list,notes:[`_imports.json synced for ${n?`module ${n}`:`project ${t}`} (${s.count} file(s), metadata only; content: fui import get <fileId>)`,...s.removed.length>0?[`removed orphaned local files: ${s.removed.join(", ")}`]:[]]}}async function ju(e,t){return(await e.api()).listFrameworkImportFiles(t)}async function Iu(e,t){let o=(await(await e.api()).getFileContent(t))?.data?.FileContent??"",s=await Cs(e.paths,t,o);return{notes:s.length>0?s.map((r)=>`written: ${r}`):["no local _imports.json declares this fileID yet; content printed only. materialize it: fui import list -p <project> [-m <module>]"],data:o}}async function Cu(e,t,n){let o=await(await e.api()).uploadFileContent(t,n),s=await Cs(e.paths,t,n);return{notes:[`ok uploaded content of import ${t}`,...s.map((r)=>`local copy synced: ${r}`)],data:`${JSON.stringify(o)}
294
+ `}}async function Ru(e,t){let n=await ce(e),{effectiveSort:o,note:s}=Rs(t.fileSort,t.moduleId?"module":"project"),r=await n.api.insertImportFile({...t.extra,ProjectID:t.projectId,ModuleID:t.moduleId,FileType:t.fileType,ContentType:t.contentType,FileName:t.fileName,FileSort:o}),i=["ok import added",...s?[s]:[]];if(t.projectId)try{let{count:a}=await n.syncImportsLocal(t.projectId,t.moduleId);i.push(`_imports.json synced for ${t.moduleId?`module ${t.moduleId}`:`project ${t.projectId}`} (${a} file(s))`)}catch{i.push("warning: import added but the _imports.json sync failed")}return{notes:i,data:`${JSON.stringify(r)}
295
+ `}}async function Tu(e,t){let n=await ce(e),o=await Is(e.paths,t.fileId),s=t.fileSort,r="";if(t.fileSort!==void 0&&o.length>0){let u=Rs(t.fileSort,o.some((d)=>d.moduleId)?"module":"project");s=u.effectiveSort,r=u.note}let i=await n.api.updateImportFile(t.fileId,{...t.extra,FileType:t.fileType,ContentType:t.contentType,FileName:t.fileName,FileSort:s}),a=ln(t.contentType)?`imports/${Gn(t.fileName,t.fileId,t.fileType)}`:void 0,c=[],l=new Set;for(let u of o){let d=u.entry.filePath;if(d&&a&&a!==d)try{await cg(vu(u.baseDir,d),vu(u.baseDir,a)),c.push(`local file moved: ${d} -> ${a}`)}catch{}let p=`${u.projectId}::${u.moduleId??""}`;if(l.has(p))continue;l.add(p);let m=u.moduleId?`module ${u.moduleId}`:`project ${u.projectId}`;try{let{removed:w}=await n.syncImportsLocal(u.projectId,u.moduleId);c.push(`${m}: _imports.json synced${w.length>0?`, removed orphaned ${w.join(", ")}`:""}`)}catch{c.push(`${m}: sync failed`)}}return{notes:["ok import updated",...r?[r]:[],...c],data:`${JSON.stringify(i)}
296
+ `}}async function Pu(e,t){let n=await ce(e),o=await Is(e.paths,t),s=await n.api.deleteImportFile(t),r=[],i=new Set;for(let a of o){let c=`${a.projectId}::${a.moduleId??""}`;if(i.has(c))continue;i.add(c);let l=a.moduleId?`module ${a.moduleId}`:`project ${a.projectId}`;try{let{removed:u}=await n.syncImportsLocal(a.projectId,a.moduleId);r.push(`${l}: _imports.json synced${u.length>0?`, removed ${u.join(", ")}`:""}`)}catch{r.push(`${l}: sync failed`)}}return{notes:[`ok import ${t} deleted`,...r],data:`${JSON.stringify(s)}
297
+ `}}var lg={1:"View only",2:"Develop",3:"Admin"};function so(e){let t=typeof e==="number"?e:Number(e);if(!Number.isFinite(t))return String(e??"");let n=lg[t];return n?`${t} (${n})`:String(t)}function $u(e,t){let n=t.trim().toLowerCase();return e.find((o)=>String(o.UserID??"").trim().toLowerCase()===n)}function pr(e,t){return e.map((n)=>({userId:String(n.UserID??"?"),username:n.UserName||"—",fullName:n.FullName||"—",...t?{right:so(n.UserRight)}:{}}))}async function Eu(e,t,n){return(await e.api()).searchUsers(t,n??"%")}async function Au(e,t){return(await e.api()).listProjectUserRights(t)}async function Nu(e,t,n){let o=await e.api(),s=`project ${t.projectId}`,r=t.userId.trim();if(!r)throw new h("empty userId; find the real UserID: fui user search <text>");let i=$u(await o.listProjectUserRights(t.projectId),r);if(n==="new"&&i)throw new h(`user "${r}" already has right ${so(i.UserRight)} on ${s}; change it: fui user set ${r} --right N`);if(n!=="new"&&!i)throw new h(`user "${r}" has no right on ${s}; ${n==="update"?`grant one: fui user add ${r} --right N`:"nothing to revoke; see: fui user list"}`);let a=n==="delete"?0:t.userRight;await o.updateProjectUserRight(t.projectId,a,JSON.stringify([r]));let c=await o.listProjectUserRights(t.projectId),l=$u(c,r),u=n==="delete"?`ok revoked the right of "${r}" on ${s}`:`ok set right ${so(a)} for "${r}" on ${s}`,d=n==="delete"?l?`warning: post-check: the user is still listed with right ${so(l.UserRight)}; the server did not remove it`:"post-check: user no longer listed":l?`post-check: ${r} has ${so(l.UserRight)}`:"warning: post-check: user not found in the list after the write; check: fui user list";return{notes:[u,d,`current rights on ${s} (${c.length}):`],rows:c}}var xu={module:{type:"string",short:"m"}},Ou={type:{type:"string"},"content-type":{type:"string"},name:{type:"string"},sort:{type:"string"},set:{type:"string",multiple:!0}};function mr(e){if(b(`${e.notes.join(`
298
298
  `)}
299
- `),e.data)k(e.data)}function Au(e){if(e===void 0)return;let t=Number(e);if(!Number.isFinite(t))throw new m("--sort must be a number");return t}function pr(e){let t=e.positionals[0];if(!t)throw new m("missing <fileId>; list them: fui import list -p <project> [-m <module>]");return t}function Eu(e){return e.map((t)=>({fileId:String(t.fileID??""),name:String(t.FileName??""),type:String(t.FileType??""),contentType:String(t.ContentType??""),scope:String(t.Scope??""),sort:String(t.FileSort??"")}))}var og={name:"import list",usage:"fui import list [-p P [-m M]] | fui import list --framework V2|V3",summary:"List import files (with -p it also syncs local _imports.json); --framework lists framework imports",options:{...Pu,framework:{type:"string"}},async run(e){let t=_(e),n=f(e.values,"framework");if(n){k(z(Eu(await ku(t,n.toUpperCase())),e.render));return}let{rows:o,notes:s}=await bu(t,f(e.values,"project"),f(e.values,"module"));for(let r of s)T(`# ${r}`);k(z(Eu(o),e.render))}},rg={name:"import get",usage:"fui import get <fileId> [-o file]",summary:"Print an inline import's source and write it to every local imports/ location declaring it",options:{out:{type:"string",short:"o"}},async run(e){let t=await vu(_(e),pr(e));for(let o of t.notes)T(`# ${o}`);let n=f(e.values,"out");if(n)await F(n,t.data??""),k(`${n}
300
- `);else k(t.data??"")}},sg={name:"import upload",usage:"fui import upload <fileId> (-f file | -)",summary:"Overwrite an existing inline import's source (text/javascript or text/css) and sync local copies",options:{file:{type:"string",short:"f"}},async run(e){let t=pr(e),n=f(e.values,"file"),o=e.positionals[1];if(n===void 0===(o!=="-"))throw new m("pass the content with -f <file> or - (stdin)");let s=n?await ft(n):await yo();dr(await $u(_(e),t,s))}},ig={name:"import new",usage:"fui import new -p P [-m M] --type js|css|... --content-type link|comp-link|text/javascript|text/css --name <name|url> [--sort N]",summary:"Add an import (CDN link or inline code); sort is raised to 20 (project) / 50 (module) when lower",options:{...Pu,...Nu},async run(e){let{values:t}=e,n=f(t,"type"),o=f(t,"content-type"),s=f(t,"name");if(!n||!o||!s)throw new m("need --type, --content-type and --name");dr(await Su(_(e),{projectId:f(t,"project"),moduleId:f(t,"module"),fileType:n,contentType:o,fileName:s,fileSort:Au(f(t,"sort")),extra:fn(t.set)}))}},ag={name:"import update",usage:"fui import update <fileId> --type T --content-type C --name N [--sort N]",summary:"Edit import metadata (not its source); renames the local content file when the name changes",options:Nu,async run(e){let{values:t}=e,n=f(t,"type"),o=f(t,"content-type"),s=f(t,"name");if(!n||!o||!s)throw new m("need --type, --content-type and --name (the server replaces all three)");dr(await ju(_(e),{fileId:pr(e),fileType:n,contentType:o,fileName:s,fileSort:Au(f(t,"sort")),extra:fn(t.set)}))}},cg={name:"import delete",usage:"fui import delete <fileId>",summary:"Delete an import on the server (ask the user first) and clean local _imports.json copies",async run(e){dr(await Iu(_(e),pr(e)))}};async function Du(e){return(await e.targets.resolveProject(f(e.values,"project"))).projectId}function lg(e){if(e!=="1"&&e!=="2"&&e!=="3")throw new m("--right must be 1 (View only), 2 (Develop) or 3 (Admin)");return Number(e)}var ug={name:"user search",usage:"fui user search <text> [--id-like pattern]",summary:"Find a developer account; the only way to get the exact UserID for user add/set/remove",options:{"id-like":{type:"string"}},async run(e){let t=e.positionals[0];if(!t)throw new m("missing <text>");let n=await Cu(_(e),t,f(e.values,"id-like"));if(n.length===0){T(`# no user matches "${t}"; the user may not exist, or --id-like filtered it out (default % = no filter)`);return}T("# grant: fui user add <UserID> --right 3 (1=View only, 2=Develop, 3=Admin)"),k(z(ur(n,!1),e.render))}},dg={name:"user list",usage:"fui user list [-p project]",summary:"Who holds an IDE right on the project (project-wide; not the acc SystemRight)",async run(e){let t=await Du(e),n=await Ru(_(e),t);if(n.length===0){T(`# nobody has a right on project ${t}; find a user: fui user search <text>`);return}k(z(ur(n,!0),e.render))}};function ri(e,t,n){return{name:`user ${e}`,usage:`fui user ${e} <UserID>${t==="delete"?"":" --right 1|2|3"} [-p project]`,summary:n,options:t==="delete"?{}:{right:{type:"string"}},async run(o){let s=o.positionals[0];if(s===void 0)throw new m("missing <UserID>; find it: fui user search <text>");let r=await Tu(_(o),{projectId:await Du(o),userId:s,userRight:t==="delete"?void 0:lg(f(o.values,"right"))},t);k(`${r.notes.join(`
299
+ `),e.data)b(e.data)}function _u(e){if(e===void 0)return;let t=Number(e);if(!Number.isFinite(t))throw new h("--sort must be a number");return t}function hr(e){let t=e.positionals[0];if(!t)throw new h("missing <fileId>; list them: fui import list -p <project> [-m <module>]");return t}function Du(e){return e.map((t)=>({fileId:String(t.fileID??""),name:String(t.FileName??""),type:String(t.FileType??""),contentType:String(t.ContentType??""),scope:String(t.Scope??""),sort:String(t.FileSort??"")}))}var ug={name:"import list",usage:"fui import list [-p P [-m M]] | fui import list --framework V2|V3",summary:"List import files (with -p it also syncs local _imports.json); --framework lists framework imports",options:{...xu,framework:{type:"string"}},async run(e){let t=_(e),n=f(e.values,"framework");if(n){b(z(Du(await ju(t,n.toUpperCase())),e.render));return}let{rows:o,notes:s}=await Su(t,f(e.values,"project"),f(e.values,"module"));for(let r of s)T(`# ${r}`);b(z(Du(o),e.render))}},dg={name:"import get",usage:"fui import get <fileId> [-o file]",summary:"Print an inline import's source and write it to every local imports/ location declaring it",options:{out:{type:"string",short:"o"}},async run(e){let t=await Iu(_(e),hr(e));for(let o of t.notes)T(`# ${o}`);let n=f(e.values,"out");if(n)await L(n,t.data??""),b(`${n}
300
+ `);else b(t.data??"")}},pg={name:"import upload",usage:"fui import upload <fileId> (-f file | -)",summary:"Overwrite an existing inline import's source (text/javascript or text/css) and sync local copies",options:{file:{type:"string",short:"f"}},async run(e){let t=hr(e),n=f(e.values,"file"),o=e.positionals[1];if(n===void 0===(o!=="-"))throw new h("pass the content with -f <file> or - (stdin)");let s=n?await ft(n):await bo();mr(await Cu(_(e),t,s))}},mg={name:"import new",usage:"fui import new -p P [-m M] --type js|css|... --content-type link|comp-link|text/javascript|text/css --name <name|url> [--sort N]",summary:"Add an import (CDN link or inline code); sort is raised to 20 (project) / 50 (module) when lower",options:{...xu,...Ou},async run(e){let{values:t}=e,n=f(t,"type"),o=f(t,"content-type"),s=f(t,"name");if(!n||!o||!s)throw new h("need --type, --content-type and --name");mr(await Ru(_(e),{projectId:f(t,"project"),moduleId:f(t,"module"),fileType:n,contentType:o,fileName:s,fileSort:_u(f(t,"sort")),extra:fn(t.set)}))}},hg={name:"import update",usage:"fui import update <fileId> --type T --content-type C --name N [--sort N]",summary:"Edit import metadata (not its source); renames the local content file when the name changes",options:Ou,async run(e){let{values:t}=e,n=f(t,"type"),o=f(t,"content-type"),s=f(t,"name");if(!n||!o||!s)throw new h("need --type, --content-type and --name (the server replaces all three)");mr(await Tu(_(e),{fileId:hr(e),fileType:n,contentType:o,fileName:s,fileSort:_u(f(t,"sort")),extra:fn(t.set)}))}},fg={name:"import delete",usage:"fui import delete <fileId>",summary:"Delete an import on the server (ask the user first) and clean local _imports.json copies",async run(e){mr(await Pu(_(e),hr(e)))}};async function Mu(e){return(await e.targets.resolveProject(f(e.values,"project"))).projectId}function gg(e){if(e!=="1"&&e!=="2"&&e!=="3")throw new h("--right must be 1 (View only), 2 (Develop) or 3 (Admin)");return Number(e)}var wg={name:"user search",usage:"fui user search <text> [--id-like pattern]",summary:"Find a developer account; the only way to get the exact UserID for user add/set/remove",options:{"id-like":{type:"string"}},async run(e){let t=e.positionals[0];if(!t)throw new h("missing <text>");let n=await Eu(_(e),t,f(e.values,"id-like"));if(n.length===0){T(`# no user matches "${t}"; the user may not exist, or --id-like filtered it out (default % = no filter)`);return}T("# grant: fui user add <UserID> --right 3 (1=View only, 2=Develop, 3=Admin)"),b(z(pr(n,!1),e.render))}},yg={name:"user list",usage:"fui user list [-p project]",summary:"Who holds an IDE right on the project (project-wide; not the acc SystemRight)",async run(e){let t=await Mu(e),n=await Au(_(e),t);if(n.length===0){T(`# nobody has a right on project ${t}; find a user: fui user search <text>`);return}b(z(pr(n,!0),e.render))}};function ai(e,t,n){return{name:`user ${e}`,usage:`fui user ${e} <UserID>${t==="delete"?"":" --right 1|2|3"} [-p project]`,summary:n,options:t==="delete"?{}:{right:{type:"string"}},async run(o){let s=o.positionals[0];if(s===void 0)throw new h("missing <UserID>; find it: fui user search <text>");let r=await Nu(_(o),{projectId:await Mu(o),userId:s,userRight:t==="delete"?void 0:gg(f(o.values,"right"))},t);b(`${r.notes.join(`
301
301
  `)}
302
- `),k(z(ur(r.rows,!0),o.render))}}}var xu=[og,rg,sg,ig,ag,cg,ug,dg,ri("add","new","Grant a right to a user who has none (exact UserID from fui user search)"),ri("set","update","Change the right level of a user who already has one"),ri("remove","delete","Revoke a user's right (ask the user first)")];import{mkdir as pg,readFile as mg,stat as hg,writeFile as fg}from"node:fs/promises";import{join as si}from"node:path";var mr={component:"https://fui.vn/component.js","component-table":"https://fui.vn/componentTable.js","default-function":"https://fui.vn/defaultfunction.js","fast-project":"https://fui.vn/fastproject.js","default-style":"https://fui.vn/projectdefaultstyle.css"},gg=86400000;async function Ou(e){try{return(await hg(e)).mtimeMs}catch{return null}}var wg={name:"library",usage:`fui library [${Object.keys(mr).join("|")}] [--refresh] [--path]`,summary:"FUI framework sources from fui.vn (component, table, default functions, fast project, style), cached a day",options:{refresh:{type:"boolean"},path:{type:"boolean"}},async run({positionals:e,values:t,paths:n}){let o=si(n.root,"_cache","library"),s=e[0];if(!s){let c=await Promise.all(Object.entries(mr).map(async([l,u])=>{let d=si(o,u.slice(u.lastIndexOf("/")+1)),p=await Ou(d);return`${l} ${u} ${p?new Date(p).toISOString():"not cached"}`}));k(`name url cached
302
+ `),b(z(pr(r.rows,!0),o.render))}}}var Lu=[ug,dg,pg,mg,hg,fg,wg,yg,ai("add","new","Grant a right to a user who has none (exact UserID from fui user search)"),ai("set","update","Change the right level of a user who already has one"),ai("remove","delete","Revoke a user's right (ask the user first)")];import{mkdir as bg,readFile as kg,stat as vg,writeFile as $g}from"node:fs/promises";import{join as ci}from"node:path";var fr={component:"https://fui.vn/component.js","component-table":"https://fui.vn/componentTable.js","default-function":"https://fui.vn/defaultfunction.js","fast-project":"https://fui.vn/fastproject.js","default-style":"https://fui.vn/projectdefaultstyle.css"},Sg=86400000;async function Fu(e){try{return(await vg(e)).mtimeMs}catch{return null}}var jg={name:"library",usage:`fui library [${Object.keys(fr).join("|")}] [--refresh] [--path]`,summary:"FUI framework sources from fui.vn (component, table, default functions, fast project, style), cached a day",options:{refresh:{type:"boolean"},path:{type:"boolean"}},async run({positionals:e,values:t,paths:n}){let o=ci(n.root,"_cache","library"),s=e[0];if(!s){let c=await Promise.all(Object.entries(fr).map(async([l,u])=>{let d=ci(o,u.slice(u.lastIndexOf("/")+1)),p=await Fu(d);return`${l} ${u} ${p?new Date(p).toISOString():"not cached"}`}));b(`name url cached
303
303
  ${c.join(`
304
304
  `)}
305
- `);return}let r=mr[s];if(!r)throw new m(`unknown library "${s}"; choose ${Object.keys(mr).join(", ")}`);let i=si(o,r.slice(r.lastIndexOf("/")+1)),a=await Ou(i);if(t.refresh===!0||a===null||Date.now()-a>gg){let c=await fetch(r,{signal:AbortSignal.timeout(30000)});if(!c.ok)throw Error(`${r} answered HTTP ${c.status}`);await pg(o,{recursive:!0}),await fg(i,await c.text()),T(`# fetched ${r}`)}else T(`# cached ${new Date(a).toISOString()}; --refresh to fetch again`);k(t.path===!0?`${i}
306
- `:await mg(i,"utf8"))}},_u=[wg];import{rm as Hu}from"node:fs/promises";import{appendFile as yg,mkdir as bg,rename as kg,stat as vg}from"node:fs/promises";import{dirname as $g,join as Sg}from"node:path";var jg=1048576,Mu=["❌","⛔","✘","⚠️"];function io(e){return Sg(e,"_logs","tool-errors.jsonl")}function Ig(e,t){let n=e.split(`
307
- `).map((i)=>i.trim()).filter(Boolean),o=[],s=new Set,r=0;for(let i of Mu)for(let a of n){if(!a.startsWith(i)||s.has(a))continue;let c=a.length>200?`${a.slice(0,200)}…`:a;if(r+c.length+1>t)return o;s.add(a),o.push(c),r+=c.length+1}return o}function ii(e,t,n){if(e.length<=t+n)return e;let o=e.slice(t,e.length-n),s=Ig(o,Math.round((t+n)/2)),r=s.length>0?[`…[cắt ${o.length} ký tự — giữ lại ${s.length} dòng lý do hỏng]…`,...s]:[`…[cắt ${o.length} ký tự]…`];return`${e.slice(0,t)}
305
+ `);return}let r=fr[s];if(!r)throw new h(`unknown library "${s}"; choose ${Object.keys(fr).join(", ")}`);let i=ci(o,r.slice(r.lastIndexOf("/")+1)),a=await Fu(i);if(t.refresh===!0||a===null||Date.now()-a>Sg){let c=await fetch(r,{signal:AbortSignal.timeout(30000)});if(!c.ok)throw Error(`${r} answered HTTP ${c.status}`);await bg(o,{recursive:!0}),await $g(i,await c.text()),T(`# fetched ${r}`)}else T(`# cached ${new Date(a).toISOString()}; --refresh to fetch again`);b(t.path===!0?`${i}
306
+ `:await kg(i,"utf8"))}},Uu=[jg];import{rm as Bu}from"node:fs/promises";import{appendFile as Ig,mkdir as Cg,rename as Rg,stat as Tg}from"node:fs/promises";import{dirname as Pg,join as Eg}from"node:path";var Ag=1048576,Hu=["❌","⛔","✘","⚠️"];function io(e){return Eg(e,"_logs","tool-errors.jsonl")}function Ng(e,t){let n=e.split(`
307
+ `).map((i)=>i.trim()).filter(Boolean),o=[],s=new Set,r=0;for(let i of Hu)for(let a of n){if(!a.startsWith(i)||s.has(a))continue;let c=a.length>200?`${a.slice(0,200)}…`:a;if(r+c.length+1>t)return o;s.add(a),o.push(c),r+=c.length+1}return o}function li(e,t,n){if(e.length<=t+n)return e;let o=e.slice(t,e.length-n),s=Ng(o,Math.round((t+n)/2)),r=s.length>0?[`…[cắt ${o.length} ký tự — giữ lại ${s.length} dòng lý do hỏng]…`,...s]:[`…[cắt ${o.length} ký tự]…`];return`${e.slice(0,t)}
308
308
  ${r.join(`
309
309
  `)}
310
- ${e.slice(-n)}`}async function Fu(e,t,n,o){try{let s=io(e);await bg($g(s),{recursive:!0});try{if((await vg(s)).size>jg)await kg(s,s.replace(/\.jsonl$/,".1.jsonl")).catch(()=>{return})}catch{}let r={ts:new Date().toISOString(),pid:process.pid,tool:t,message:ii(n,400,600),...o&&o!=="error"?{kind:o}:{}};await yg(s,`${JSON.stringify(r)}
311
- `,"utf-8")}catch{}}async function Lu(e){let t="";try{t=await G(io(e))}catch{}let n=[];for(let o of t.split(`
312
- `)){if(!o.trim())continue;try{n.push(JSON.parse(o))}catch{}}return n}function Cg(e){let t=e.split(`
313
- `).map((o)=>o.trim()).filter(Boolean),n=Mu.map((o)=>t.find((s)=>s.startsWith(o))).find(Boolean);if(!n)return"(bản ghi không chứa dòng lý do hỏng — xem ví dụ bên dưới)";return n.replace(/^[❌⛔✘⚠️]+️?\s*/,"").replace(/'[^'\s]*'|"[^"\s]*"|`[^`\s]*`/g,"'…'").replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f-]{12,}\b/gi,"…").replace(/[A-Za-z]:[\\/][^\s,)]+|\/[^\s,)]*\/[^\s,)]+/g,"…").replace(/\b\d+\b/g,"N").replace(/\s+/g," ").trim().slice(0,140)}function Uu(e){return{errors:e.filter((t)=>t.kind!=="guard"),guards:e.filter((t)=>t.kind==="guard")}}function ai(e){let t=new Map;for(let n of e){let o=Cg(n.message),s=`${n.tool} ${o}`,r=t.get(s);if(r)r.count+=1,r.lastTs=n.ts,r.sample=n.message;else t.set(s,{tool:n.tool,signature:o,count:1,firstTs:n.ts,lastTs:n.ts,sample:n.message})}return[...t.values()].sort((n,o)=>o.count-n.count||o.lastTs.localeCompare(n.lastTs))}function Vu(e,t,n,o,s){if(e===void 0)return s;let r=Number(e);if(!Number.isInteger(r)||r<n||r>o)throw new m(`${t} must be an integer ${n}-${o}`);return r}var Rg={name:"log read",usage:"fui log read [--tool mcp_tool_name] [--limit N] [--summary] [--sample-chars N]",summary:"Tool-error log grouped by command and error signature, most frequent first",options:{tool:{type:"string"},limit:{type:"string"},summary:{type:"boolean"},"sample-chars":{type:"string"}},async run({paths:e,values:t}){let n=f(t,"tool"),o=Vu(f(t,"limit"),"--limit",0,Number.MAX_SAFE_INTEGER,15),s=Vu(f(t,"sample-chars"),"--sample-chars",200,1e5,900),r=io(e.root),i=await Lu(e.root);if(n)i=i.filter((h)=>h.tool===n);let{errors:a,guards:c}=Uu(i);if(a.length===0&&c.length===0){k(`no errors logged${n?` for ${n}`:""}
310
+ ${e.slice(-n)}`}async function Vu(e,t,n,o){try{let s=io(e);await Cg(Pg(s),{recursive:!0});try{if((await Tg(s)).size>Ag)await Rg(s,s.replace(/\.jsonl$/,".1.jsonl")).catch(()=>{return})}catch{}let r={ts:new Date().toISOString(),pid:process.pid,tool:t,message:li(n,400,600),...o&&o!=="error"?{kind:o}:{}};await Ig(s,`${JSON.stringify(r)}
311
+ `,"utf-8")}catch{}}async function Wu(e){let t="";try{t=await G(io(e))}catch{}let n=[];for(let o of t.split(`
312
+ `)){if(!o.trim())continue;try{n.push(JSON.parse(o))}catch{}}return n}function Dg(e){let t=e.split(`
313
+ `).map((o)=>o.trim()).filter(Boolean),n=Hu.map((o)=>t.find((s)=>s.startsWith(o))).find(Boolean);if(!n)return"(bản ghi không chứa dòng lý do hỏng — xem ví dụ bên dưới)";return n.replace(/^[❌⛔✘⚠️]+️?\s*/,"").replace(/'[^'\s]*'|"[^"\s]*"|`[^`\s]*`/g,"'…'").replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f-]{12,}\b/gi,"…").replace(/[A-Za-z]:[\\/][^\s,)]+|\/[^\s,)]*\/[^\s,)]+/g,"…").replace(/\b\d+\b/g,"N").replace(/\s+/g," ").trim().slice(0,140)}function qu(e){return{errors:e.filter((t)=>t.kind!=="guard"),guards:e.filter((t)=>t.kind==="guard")}}function ui(e){let t=new Map;for(let n of e){let o=Dg(n.message),s=`${n.tool} ${o}`,r=t.get(s);if(r)r.count+=1,r.lastTs=n.ts,r.sample=n.message;else t.set(s,{tool:n.tool,signature:o,count:1,firstTs:n.ts,lastTs:n.ts,sample:n.message})}return[...t.values()].sort((n,o)=>o.count-n.count||o.lastTs.localeCompare(n.lastTs))}function Ju(e,t,n,o,s){if(e===void 0)return s;let r=Number(e);if(!Number.isInteger(r)||r<n||r>o)throw new h(`${t} must be an integer ${n}-${o}`);return r}var xg={name:"log read",usage:"fui log read [--tool mcp_tool_name] [--limit N] [--summary] [--sample-chars N]",summary:"Tool-error log grouped by command and error signature, most frequent first",options:{tool:{type:"string"},limit:{type:"string"},summary:{type:"boolean"},"sample-chars":{type:"string"}},async run({paths:e,values:t}){let n=f(t,"tool"),o=Ju(f(t,"limit"),"--limit",0,Number.MAX_SAFE_INTEGER,15),s=Ju(f(t,"sample-chars"),"--sample-chars",200,1e5,900),r=io(e.root),i=await Wu(e.root);if(n)i=i.filter((m)=>m.tool===n);let{errors:a,guards:c}=qu(i);if(a.length===0&&c.length===0){b(`no errors logged${n?` for ${n}`:""}
314
314
  log file: ${r}
315
- `);return}let l=new Map;for(let h of a)l.set(h.tool,(l.get(h.tool)??0)+1);let u=ai(a),d=o===0?u:u.slice(0,o),p=[`tool error log: ${a.length} failures, ${u.length} groups${n?` (tool ${n})`:""}`,`file: ${r}`,"","failures per tool (most first: where the skill is weakest):",...[...l.entries()].sort((h,w)=>w[1]-h[1]).map(([h,w])=>` ${String(w).padStart(4)}x ${h}`),"",`recurring groups (${d.length}/${u.length}, most first):`];if(d.forEach((h,w)=>{if(p.push("",`[${w+1}] ${h.count}x ${h.tool} first ${h.firstTs} last ${h.lastTs}`,` ${h.signature}`),t.summary!==!0){p.push(" -- latest occurrence --");for(let y of ii(h.sample,Math.round(s*0.4),Math.round(s*0.6)).split(`
316
- `))p.push(` ${y}`)}}),u.length>d.length)p.push("",`${u.length-d.length} more groups; raise --limit (0 = all)`);if(c.length>0){p.push("",`guards that blocked by design: ${c.length} (not skill gaps; not counted above):`);for(let h of ai(c).slice(0,5))p.push(` ${String(h.count).padStart(4)}x ${h.tool} ${h.signature}`)}k(`${p.join(`
315
+ `);return}let l=new Map;for(let m of a)l.set(m.tool,(l.get(m.tool)??0)+1);let u=ui(a),d=o===0?u:u.slice(0,o),p=[`tool error log: ${a.length} failures, ${u.length} groups${n?` (tool ${n})`:""}`,`file: ${r}`,"","failures per tool (most first: where the skill is weakest):",...[...l.entries()].sort((m,w)=>w[1]-m[1]).map(([m,w])=>` ${String(w).padStart(4)}x ${m}`),"",`recurring groups (${d.length}/${u.length}, most first):`];if(d.forEach((m,w)=>{if(p.push("",`[${w+1}] ${m.count}x ${m.tool} first ${m.firstTs} last ${m.lastTs}`,` ${m.signature}`),t.summary!==!0){p.push(" -- latest occurrence --");for(let y of li(m.sample,Math.round(s*0.4),Math.round(s*0.6)).split(`
316
+ `))p.push(` ${y}`)}}),u.length>d.length)p.push("",`${u.length-d.length} more groups; raise --limit (0 = all)`);if(c.length>0){p.push("",`guards that blocked by design: ${c.length} (not skill gaps; not counted above):`);for(let m of ui(c).slice(0,5))p.push(` ${String(m.count).padStart(4)}x ${m.tool} ${m.signature}`)}b(`${p.join(`
317
317
  `)}
318
- `)}},Tg={name:"log clear",usage:"fui log clear",summary:"Delete the shared tool-error log (after its patterns were folded back into the skill)",async run({paths:e}){let t=io(e.root);await Hu(t,{force:!0}),await Hu(t.replace(/\.jsonl$/,".1.jsonl"),{force:!0}),k(`cleared ${t}
319
- `)}},Wu=[Rg,Tg];import{join as Eg}from"node:path";var Pg={module:{type:"string",short:"m"}},ci=["load","ui","html","script","css","components","imports"],li=["info","config","modules","components","imports"];async function qu(e,t){let n=typeof t==="string"?t:`${JSON.stringify(t,null,e.json?0:2)}
320
- `,o=f(e.values,"out");if(o)await F(o,n),k(`${o}
321
- `);else k(n)}var Ng={name:"remote module",usage:`fui remote module [-p P -m M] --part ${ci.join("|")} [-o file]`,summary:"Print one part of a module as the server has it (script/css/html raw; others JSON); no local writes",options:{...Pg,part:{type:"string"},out:{type:"string",short:"o"}},async run(e){let t=f(e.values,"part");if(!t||!ci.includes(t))throw new m(`--part must be one of: ${ci.join(", ")}`);let{projectId:n,moduleId:o}=await e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module")),s=await _(e).api(),r=await{load:()=>s.getModuleContent(n,o),ui:async()=>(await s.getModuleUI(n,o)).data,html:async()=>String((await s.getModuleHtml(n,o)).data?.mBody??""),script:()=>s.getModuleScript(n,o),css:()=>s.getModuleCSS(n,o),components:()=>s.getModuleComponents(n,o),imports:()=>s.getModuleDependencies(n,o)}[t]();await qu(e,r)}},Ag={name:"remote project",usage:`fui remote project [-p P] --part ${li.join("|")} [-o file]`,summary:"Print one part of a project as the server has it; no local writes",options:{part:{type:"string"},out:{type:"string",short:"o"}},async run(e){let t=f(e.values,"part");if(!t||!li.includes(t))throw new m(`--part must be one of: ${li.join(", ")}`);let{projectId:n}=await e.targets.resolveProject(f(e.values,"project")),o=await _(e).api(),s=await{info:async()=>(await o.listProjects()).find((r)=>r.ProjectID===n)??null,config:()=>o.getProjectJson(n),modules:()=>o.listModules(n),components:()=>o.getModuleComponents(n),imports:()=>o.listImportFiles(n)}[t]();await qu(e,s)}};function Bu(e,t){let n=Uo(e.paths,t);if(!n)throw new m(`${t} is not inside a project of workspace ${e.paths.root}`);return n}var Dg={name:"remote diff",usage:"fui remote diff <path...>",summary:"Compare local module files with the server copy (module preview logic, per file); no writes",async run(e){if(e.positionals.length===0)throw new m("missing <path>");let t=e.positionals.map((r)=>Bu(e,r)),n=new Qn(e.paths,await _(e).api()),o=new Map,s=[];for(let r of t){if(!r.moduleId){s.push(`${r.rel} unsupported only module files can be diffed (use fui remote project --part config)`);continue}let i=`${r.projectId}/${r.moduleId}`,a=o.get(i);if(!a){let d=(await n.resolveModuleInfoFromCache(r.projectId,r.moduleId)).found;if(!d)throw new m(`module ${i} unknown; run fui module list -p ${r.projectId}`);let p=await n.fetchModuleBundle(d);a=un(d,p.content,p.components,p.script,p.css,p.dependencies),o.set(i,a)}let c=await L(r.abs,"\x00missing"),l=a[r.rel],u;if(c==="\x00missing")u=l===void 0?"absent":"only-server";else if(l===void 0)u="only-local";else if(r.rel.endsWith(".json")){let d=!1;try{d=jt(JSON.parse(c))===jt(JSON.parse(l))}catch{}u=d?"same":"differs"}else u=$t(c)===$t(l)?"same":"differs";s.push(`${r.rel} ${u} ${Eg(r.projectId,"modules",r.moduleId)}`)}k(`${s.join(`
318
+ `)}},Og={name:"log clear",usage:"fui log clear",summary:"Delete the shared tool-error log (after its patterns were folded back into the skill)",async run({paths:e}){let t=io(e.root);await Bu(t,{force:!0}),await Bu(t.replace(/\.jsonl$/,".1.jsonl"),{force:!0}),b(`cleared ${t}
319
+ `)}},Ku=[xg,Og];import{join as mi}from"node:path";function ao(e){let t=e.replace(/\r\n/g,`
320
+ `).split(`
321
+ `);if(t.at(-1)==="")t.pop();return t}function _g(e,t,n,o,s){let r=s-n,i=new Uint32Array(r+1),a=new Uint32Array(r+1);for(let c=n;c<o;c++){for(let l=1;l<=r;l++)a[l]=e[c]===t[n+l-1]?i[l-1]+1:Math.max(i[l],a[l-1]);[i,a]=[a,i]}return i[r]}function Gu(e,t){let n=ao(e),o=ao(t),s=0;while(s<n.length&&s<o.length&&n[s]===o[s])s++;let r=n.length,i=o.length;while(r>s&&i>s&&n[r-1]===o[i-1])r--,i--;let a=r-s,c=i-s,l=a*c>25000000?0:_g(n,o,s,r,i);return{added:c-l,removed:a-l}}var Xu={module:{type:"string",short:"m"}},di=["load","ui","html","script","css","components","imports"],pi=["info","config","modules","components","imports"];async function Yu(e,t){let n=typeof t==="string"?t:`${JSON.stringify(t,null,e.json?0:2)}
322
+ `,o=f(e.values,"out");if(o)await L(o,n),b(`${o}
323
+ `);else b(n)}var Mg={name:"remote module",usage:`fui remote module [-p P -m M] --part ${di.join("|")} [-o file]`,summary:"Print one part of a module as the server has it (script/css/html raw; others JSON); no local writes",options:{...Xu,part:{type:"string"},out:{type:"string",short:"o"}},async run(e){let t=f(e.values,"part");if(!t||!di.includes(t))throw new h(`--part must be one of: ${di.join(", ")}`);let{projectId:n,moduleId:o}=await e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module")),s=await _(e).api(),r=await{load:()=>s.getModuleContent(n,o),ui:async()=>(await s.getModuleUI(n,o)).data,html:async()=>String((await s.getModuleHtml(n,o)).data?.mBody??""),script:()=>s.getModuleScript(n,o),css:()=>s.getModuleCSS(n,o),components:()=>s.getModuleComponents(n,o),imports:()=>s.getModuleDependencies(n,o)}[t]();await Yu(e,r)}},Lg={name:"remote project",usage:`fui remote project [-p P] --part ${pi.join("|")} [-o file]`,summary:"Print one part of a project as the server has it; no local writes",options:{part:{type:"string"},out:{type:"string",short:"o"}},async run(e){let t=f(e.values,"part");if(!t||!pi.includes(t))throw new h(`--part must be one of: ${pi.join(", ")}`);let{projectId:n}=await e.targets.resolveProject(f(e.values,"project")),o=await _(e).api(),s=await{info:async()=>(await o.listProjects()).find((r)=>r.ProjectID===n)??null,config:()=>o.getProjectJson(n),modules:()=>o.listModules(n),components:()=>o.getModuleComponents(n),imports:()=>o.listImportFiles(n)}[t]();await Yu(e,s)}};function Qu(e,t){let n=Vo(e.paths,t);if(!n)throw new h(`${t} is not inside a project of workspace ${e.paths.root}`);return n}var hi="\x00missing";function zu(e,t,n){if(t===hi)return n===void 0?{state:"absent",added:0,removed:0}:{state:"only-server",added:0,removed:ao(n).length};if(n===void 0)return{state:"only-local",added:ao(t).length,removed:0};let o;if(e.endsWith(".json")){o=!1;try{o=jt(JSON.parse(t))===jt(JSON.parse(n))}catch{}}else o=$t(t)===$t(n);return o?{state:"same",added:0,removed:0}:{state:"differs",...Gu(n,t)}}async function Fg(e){let{projectId:t,moduleId:n,moduleDir:o}=await e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module"));return(await Jr(o)).filter((s)=>s!=="project.json").map((s)=>({abs:mi(o,s),baseDir:o,rel:s,projectId:t,moduleId:n}))}var Ug={name:"remote diff",usage:"fui remote diff [path...] [--stat]",summary:"Compare local module files with the server copy (module preview logic, per file); no path = the target module; --stat adds +/- lines; no writes",options:{...Xu,stat:{type:"boolean"}},async run(e){let t=e.positionals.length===0,n=t?await Fg(e):e.positionals.map((u)=>Qu(e,u)),o=new Qn(e.paths,await _(e).api()),s=new Map,r=[],i=[];for(let u of n){if(!u.moduleId){i.push(u.rel);continue}let d=`${u.projectId}/${u.moduleId}`,p=s.get(d);if(!p){let w=(await o.resolveModuleInfoFromCache(u.projectId,u.moduleId)).found;if(!w)throw new h(`module ${d} unknown; run fui module list -p ${u.projectId}`);let y=await o.fetchModuleBundle(w);p=un(w,y.content,y.components,y.script,y.css,y.dependencies),s.set(d,p)}let m=mi(u.projectId,"modules",u.moduleId);r.push({path:u.rel,module:m,...zu(u.rel,await F(u.abs,hi),p[u.rel])})}if(t){let u=n[0],d=u?s.get(`${u.projectId}/${u.moduleId}`):void 0,p=new Set(n.map((m)=>m.rel));for(let[m,w]of Object.entries(d??{})){if(p.has(m)||Eo(m)||!u)continue;r.push({path:m,module:mi(u.projectId,"modules",u.moduleId),...zu(m,hi,w)})}}if(e.values.stat!==!0){let u=[...i.map((d)=>`${d} unsupported only module files can be diffed (use fui remote project --part config)`),...r.map((d)=>`${d.path} ${d.state} ${d.module}`)];b(`${u.join(`
322
324
  `)}
323
- `)}},xg={name:"file push",usage:"fui file push <path...> [--new]",summary:"Push local files through their guarded publisher (module.json, script.js, style.css, components/*.vue, project.json)",options:{new:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new m("missing <path>");let t=_(e),n=0;for(let o of e.positionals){let s=Bu(e,o),r;if(!s.moduleId&&s.rel==="project.json")r=await er(t,s.projectId);else if(s.rel.startsWith("components/")&&s.rel.endsWith(".vue")&&!s.rel.slice(11).includes("/")){let i=await hn(t,s.projectId,s.moduleId),a=s.rel.slice(11,-4);r=e.values.new===!0?await Qo(t,i,a):await Zo(t,i,a)}else if(s.moduleId&&(s.rel==="module.json"||s.rel==="script.js"||s.rel==="style.css")){let i={projectId:s.projectId,moduleId:s.moduleId,moduleDir:s.baseDir};r=s.rel==="module.json"?await Xo(t,i):await Yo(t,i,s.rel)}else if(s.rel==="header.html"||s.rel==="body.html"){T(`# ${o}: header/body publish only as a whole module: fui module preview, then fui module publish html --approval <token>`),n=Math.max(n,2);continue}else{T(`# ${o}: no publisher for this file`),n=Math.max(n,2);continue}k(`${r.notes.join(`
325
+ `);return}let a=r.filter((u)=>u.state!=="same"&&u.state!=="absent"),c=a.reduce((u,d)=>u+d.added,0),l=a.reduce((u,d)=>u+d.removed,0);T(`# ${a.length} file(s) differ from the server: +${c} -${l}`);for(let u of i)T(`# ${u}: only module files can be diffed`);b(z(a.map(({path:u,state:d,added:p,removed:m})=>({path:u,state:d,added:p,removed:m})),e.render))}},Hg={name:"file push",usage:"fui file push <path...> [--new]",summary:"Push local files through their guarded publisher (module.json, script.js, style.css, components/*.vue, project.json)",options:{new:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new h("missing <path>");let t=_(e),n=0;for(let o of e.positionals){let s=Qu(e,o),r;if(!s.moduleId&&s.rel==="project.json")r=await nr(t,s.projectId);else if(s.rel.startsWith("components/")&&s.rel.endsWith(".vue")&&!s.rel.slice(11).includes("/")){let i=await hn(t,s.projectId,s.moduleId),a=s.rel.slice(11,-4);r=e.values.new===!0?await er(t,i,a):await tr(t,i,a)}else if(s.moduleId&&(s.rel==="module.json"||s.rel==="script.js"||s.rel==="style.css")){let i={projectId:s.projectId,moduleId:s.moduleId,moduleDir:s.baseDir};r=s.rel==="module.json"?await Qo(t,i):await Zo(t,i,s.rel)}else if(s.rel==="header.html"||s.rel==="body.html"){T(`# ${o}: header/body publish only as a whole module: fui module preview, then fui module publish html --approval <token>`),n=Math.max(n,2);continue}else{T(`# ${o}: no publisher for this file`),n=Math.max(n,2);continue}b(`${r.notes.join(`
324
326
  `)}
325
- `)}return n}},Ju=[Ng,Ag,Dg,xg];function Og(e){if(e===void 0)return Oa;let t=Number(e);if(!Number.isInteger(t)||t<0)throw new m("--limit must be an integer >= 0 (0 = no limit)");return t}var _g={name:"query",usage:'fui query ("<sql>" | -f file | -) [--db ref] [--limit 200] [--confirm-write] [--json]',summary:"Run SQL that returns rows (TSV); writes need --confirm-write",options:{file:{type:"string",short:"f"},limit:{type:"string"},"confirm-write":{type:"boolean"}},async run(e){let{values:t,positionals:n,render:o}=e,s=await Dn(n[0],f(t,"file")),r=Og(f(t,"limit")),{rows:i,connection:a}=await Ja(await X(e),s,{db:f(t,"db"),confirmWrite:t["confirm-write"]===!0});if(a.note)T(`# ${a.note}`);let c=Yi(i,r);k(z(c.rows,o)),T(c.truncated?`# rows: ${c.rows.length} of ${c.total} (truncated; use --limit 0 or TOP/WHERE)`:`# rows: ${c.total}`)}},Mg={name:"exec",usage:'fui exec ("<sql>" | -f file | -) [--db ref] [--confirm-write]',summary:"Run DDL/DML that returns no rows; several connections need --db",options:{file:{type:"string",short:"f"},"confirm-write":{type:"boolean"}},async run(e){let{values:t,positionals:n,json:o}=e,s=await Dn(n[0],f(t,"file")),{result:r,connection:i,notes:a}=await Ka(await X(e),s,{db:f(t,"db"),confirmWrite:t["confirm-write"]===!0}),c=`${i.projectId}/${i.alias}`;if(i.note)T(`# ${i.note}`);let l=sn(r);k(o?Nr({db:c,result:l}):`ok ${c} ${JSON.stringify(l)}
326
- `);for(let u of a)T(`# ${u}`)}},Ku=[_g,Mg];function ui(e){if(e===void 0)return;let t=Ur.find((n)=>n.toLowerCase()===e.toLowerCase());if(!t)throw new m(`--type must be one of: ${Ur.join(", ")}`);return t}var Fg={name:"schema",usage:'fui schema [--db ref] [--type Table|View|API|"API File"|Procedure|Function] [--search text]',summary:"Print compact DDL (one line per object) from the local cache; --search matches the whole line",options:{type:{type:"string"},search:{type:"string"}},async run(e){let t=await Ua(await X(e),{db:f(e.values,"db"),objectType:ui(f(e.values,"type")),search:f(e.values,"search")});for(let n of t.notes)T(`# ${n}`);k(t.data??"")}},Lg={name:"schema pull",usage:"fui schema pull [--db ref] [--prune-local-only]",summary:"Refresh schema.json and local .sql bodies; archive files of objects dropped on the server",options:{"prune-local-only":{type:"boolean"}},async run(e){let t=await La(await X(e),f(e.values,"db"),e.values["prune-local-only"]===!0);k(`${t.notes.join(`
327
+ `)}return n}},Zu=[Mg,Lg,Ug,Hg];function Vg(e){if(e===void 0)return Fa;let t=Number(e);if(!Number.isInteger(t)||t<0)throw new h("--limit must be an integer >= 0 (0 = no limit)");return t}var Wg={name:"query",usage:'fui query ("<sql>" | -f file | -) [--db ref] [--limit 200] [--confirm-write] [--json]',summary:"Run SQL that returns rows (TSV); writes need --confirm-write",options:{file:{type:"string",short:"f"},limit:{type:"string"},"confirm-write":{type:"boolean"}},async run(e){let{values:t,positionals:n,render:o}=e,s=await Dn(n[0],f(t,"file")),r=Vg(f(t,"limit")),{rows:i,connection:a}=await Xa(await X(e),s,{db:f(t,"db"),confirmWrite:t["confirm-write"]===!0});if(a.note)T(`# ${a.note}`);let c=na(i,r);b(z(c.rows,o)),T(c.truncated?`# rows: ${c.rows.length} of ${c.total} (truncated; use --limit 0 or TOP/WHERE)`:`# rows: ${c.total}`)}},qg={name:"exec",usage:'fui exec ("<sql>" | -f file | -) [--db ref] [--confirm-write]',summary:"Run DDL/DML that returns no rows; several connections need --db",options:{file:{type:"string",short:"f"},"confirm-write":{type:"boolean"}},async run(e){let{values:t,positionals:n,json:o}=e,s=await Dn(n[0],f(t,"file")),{result:r,connection:i,notes:a}=await Ya(await X(e),s,{db:f(t,"db"),confirmWrite:t["confirm-write"]===!0}),c=`${i.projectId}/${i.alias}`;if(i.note)T(`# ${i.note}`);let l=sn(r);b(o?Dr({db:c,result:l}):`ok ${c} ${JSON.stringify(l)}
328
+ `);for(let u of a)T(`# ${u}`)}},ed=[Wg,qg];function fi(e){if(e===void 0)return;let t=Vr.find((n)=>n.toLowerCase()===e.toLowerCase());if(!t)throw new h(`--type must be one of: ${Vr.join(", ")}`);return t}var Bg={name:"schema",usage:'fui schema [--db ref] [--type Table|View|API|"API File"|Procedure|Function] [--search text]',summary:"Print compact DDL (one line per object) from the local cache; --search matches the whole line",options:{type:{type:"string"},search:{type:"string"}},async run(e){let t=await qa(await X(e),{db:f(e.values,"db"),objectType:fi(f(e.values,"type")),search:f(e.values,"search")});for(let n of t.notes)T(`# ${n}`);b(t.data??"")}},Jg={name:"schema pull",usage:"fui schema pull [--db ref] [--prune-local-only]",summary:"Refresh schema.json and local .sql bodies; archive files of objects dropped on the server",options:{"prune-local-only":{type:"boolean"}},async run(e){let t=await Wa(await X(e),f(e.values,"db"),e.values["prune-local-only"]===!0);b(`${t.notes.join(`
327
329
  `)}
328
- `)}},Gu=[Fg,Lg];import{join as zg}from"node:path";var hr="---\nname: fui\ndescription: Use the `fui` CLI for all FUI (low-code platform) work — projects, modules (get, validate, simulate, preview, publish), components, imports, project users, rights, DB connections, SQL, schema, stored procedures (tAPI spAPI_*), framework library, workspace files, publish ledger and history. Run it in the shell.\n---\n\n# fui CLI\n\n`fui` is the command-line tool for the FUI low-code platform. Run it with your shell tool.\n\n## Before you start\n\n- FUI has its own conventions (module.json action engine, tAPI SP naming, `f-*` components). They differ from standard Vue/Vuetify/REST, so never rely on training-data assumptions.\n- Before creating, editing, reviewing or deploying any module, component or stored procedure: run `fui skill get` (SKILL.md and the `references/INDEX.md` router). For module or UI work also load `fui skill ref references/controls-patterns.md references/module-structure.md`; pick other files from the router the same way. The same files are in the `fui-skill` skill.\n- Strict mode (`FUI_STRICT=1`) blocks publishing and deploys until the skill was read this session (`fui skill get`, `fui skill ref` or `fui skill loaded`) and while module.json has validation errors.\n- First use on a machine: `fui config set api-base <url>` and `printf %s \"$TOKEN\" | fui config set api-token -` (ask the user for both). `fui config` shows what is set.\n\n## Working rules\n\n- **Build order:** UI first with fake data in `data[]` plus a visual check, then design and deploy the SP contract, then replace the fake data with real API bindings (IN/OUT/CALLBACK).\n- **Check cheapest first:** `module check` (validate, outline and simulate in one step) → `component preview` → `module simulate --render`. Read every screenshot you get and fix what you see in the same turn.\n- **Never call a real API/SP/DML \"to test\".** Verifying an SP means reading its body (`fui sp verify`). Data edits the user asks for still run, gated by `--confirm-write`.\n- **Create and edit are separate commands** (`new` / `update`); nothing upserts. Editing needs the record id.\n- **Publishing is per file:** edit the local file, then push it. Pushing module.json does not carry script.js along (`module publish json` / `script` / `css` / `html`, `component new` / `update`, `import upload`, `project publish-config`).\n- **Local files are a snapshot;** other people publish to the same project. `module get`, `project sync`, `schema pull` and `sp get` overwrite every file in their scope without asking. Whatever they replace is copied to `<workspace>/_history/` first (`fui file history <path>`, `fui file restore <path>`).\n\n## Output contract\n\n- stdout is data only: TSV with a header when piped, an aligned table in a terminal, `--json` for one-line JSON, `--no-header` to drop the header.\n- stderr carries `# ...` notes and `error:` / `guard:` lines.\n- Exit codes:\n - `0`: ok, including \"skipped: unchanged\".\n - `1`: runtime error (network or SQL).\n - `2`: usage error. Read the message; it lists the valid choices.\n - `3`: blocked by a guard, or waiting for `--confirm-write`.\n\n## Targets\n\n- **Project and module**, first match wins:\n 1. `-p <project> -m <module>`. Passing only one of them is an error.\n 2. The current directory, if it is inside `<workspace>/<project>/modules/<module>/`.\n 3. The session target.\n- **Session:** one per `FUI_SESSION` (default `default`). Parallel agents should each set their own `FUI_SESSION`.\n- **Connection:**\n - `--db <connectionName>` or `--db <project>/<connectionName>`. A unique apiName is accepted too.\n - connectionName is the local key and `_db/<connectionName>/` folder (e.g. `docs`, `docs_lhbs`); apiName is the tAPI route alias and may repeat across zones. `fui db list` shows both.\n - With no `--db`: the only connection, else one inferred from the object name (`sp` commands), else the default.\n - Writes never use the default when a project has several connections.\n- `fui file where` prints what resolved and why.\n- `fui config` shows the workspace and API settings with their sources: flag, env (`FUI_WORKDIR`, `FUI_API_BASE`, `FUI_API_TOKEN`), then `fui config set`. The default workspace is `~/.fui/workspaces`.\n- A global install updates itself in the background, at most once an hour. A line starting `# fui updated` means a new version is running; the skills for Claude Code, Codex and Antigravity refresh on their own. `fui --version` prints the version.\n\n## Modules\n\n1. `fui module get /project/module` (also a live URL or display name).\n - It archives local files that differ, deletes `.vue` files the server no longer has, and makes the module the session target.\n2. Edit the files in `<workspace>/<project>/modules/<module>/`. Running `fui` from inside that folder targets it.\n3. Check, cheapest first. `fui module check [--scenario ...] [--mocks ...] [--last]` runs the first three in one step and prints only problems, asserts and stats (`--full` for every report); it stops after validate when there are errors:\n - `fui module validate`: fix every error (exit 1 while any remain); it only reads local files.\n - `fui module outline`: the layout tree (grid widths, bindings, actions, dialogs) to confirm the structure is what you meant.\n - `fui module simulate --scenario '[{\"click\":\"Save\"},{\"assert\":\"vueData.list.length > 0\"}]'`: runs the module in a simulated runtime; exit 1 when an assert fails. `--mocks` wins over live data. An unmocked static API may call the real tAPI when its local `.sql` body reads as read-only (max 8 calls, cached in `_testdata/`). For an existing SP take mocks from `fui sp verify`, never invent them. Keep scenario and mocks in files (`--scenario-file`, `--mocks-file`); `fui module simulate --last` reruns the module's last input (render mode, viewport, files) and re-reads those files. When the scenario changes a lot, pass it inline with `--last --scenario '[...]'`: one command, one step. Add `--brief` (remembered by `--last`) to print only failures, warnings, asserts, stats and screenshot paths.\n - `fui component preview <uc-name> --cases '[{\"name\":\"empty\",\"props\":{\"items\":[]}}]'`: one local `.vue` alone in a real browser; do it before wiring a component in and before publishing a project-scope one.\n - `fui module simulate --render [--device mobile]`: the real page in headless Chromium, last and only for problems that need pixels. Project-level assets come from the server, so publish project files first. It prints screenshot paths: open and read them, then fix what you see. Needs Playwright once, installed with the same package manager as fui: `npm install -g playwright && npx playwright install chromium` (or `bun add -g playwright && bunx playwright install chromium`).\n4. Publish:\n - **Whole module, including header/body:** `fui module preview` → show the changes to the user → after approval `fui module publish html --approval <token>`.\n - The token breaks if any payload file changes after the preview.\n - **One file:** `fui module publish json|script|css`, `fui component update <name>`, or `fui file push <path...>`, which routes each path to the right publisher.\n - Unchanged files are skipped via the publish ledger.\n5. `fui file status` lists what is still unpublished.\n\n`fui remote module --part ui|html|script|css|load|components|imports` and `fui remote project --part config|modules|…` show the server copy without touching the workspace. `fui remote diff <path...>` compares local files with the server.\n\nAsk the user before `module delete`, `component delete`, `import delete` and `user remove`.\n\n## Components\n\n- A component is one `uc-<name>.vue` file (Vue 2 options API). FUI takes the name from the file name and registers every `uc-*.vue` globally, so leave out `name:` and `components: {}` in `export default`.\n- Use Vuetify components where they fit and validate props.\n- Check it alone with `fui component preview <uc-name> --cases [...]` before wiring it into a module, then `fui component new|update <name>`.\n\n## Framework library\n\n`fui library` lists the FUI framework sources on fui.vn (`component`, `component-table`, `default-function`, `fast-project`, `default-style`). `fui library default-function --path` caches one for a day and prints its path, so you can `grep` it instead of reading it whole.\n\n## 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 # default cap 200 rows; 0 = all\necho \"SELECT 1 AS x\" | fui query -\n```\n\nNarrow with `TOP`/`WHERE` first. Every row you read costs tokens.\n\n## Structure (MSSQL)\n\n```bash\nfui schema --search studentid # compact DDL lines; --search matches the whole line (columns, params)\nfui schema --type API --search Student # Table View API \"API File\" Procedure Function\nfui schema pull # refresh schema.json + every local .sql body\nfui sp list --search Student # columns: db name kind count local\n```\n\n- With no cache, `schema` fails and tells you to run `fui schema pull`. It never pulls on its own.\n- A `# warning: ... last synced N days ago` note means pull before trusting the names.\n- `schema pull` archives and deletes local `.sql` files whose objects were dropped on the server.\n- It only lists drafts that exist in neither schema. `--prune-local-only` archives those too, but only once the user confirms they are stale.\n\n## Stored procedures (MSSQL)\n\n1. `fui sp get <name>` saves the live body to `_db/<alias>/<name>.sql`. A differing local copy is archived first.\n - For a new SP: `fui sp path <name> --db <ref>`, or `fui sp save <name> -f file.sql --db <ref>`.\n2. Edit the file. `CREATE` versus `ALTER` does not matter.\n3. `fui sp deploy <name>`:\n - saves the live body as a rollback copy\n - fixes the verb\n - grants `EXECUTE` to `[public]` on new procedures and checks it\n - clears the tAPI cache\n4. If the grant failed, do **not** redeploy. Run the printed `fui exec \"GRANT ...\"`.\n\nBefore writing `apiMocks` for an SP that exists, run `fui sp verify <name>`. It reads the real body (never calls the endpoint) and prints the params, the response shape, the error contract, read/write and a mock snippet. `sp delete` and `sp rename` change the server: ask the user first.\n\n## Writes need the user\n\n- Writes to real data exit `3` until you add `--confirm-write`. That covers `INSERT`, `UPDATE`, `DELETE`, `MERGE` and `TRUNCATE` in `query` or `exec`, and `ALTER TABLE ... ALTER COLUMN`.\n- Add `--confirm-write` **only after the user approved that exact write in chat.**\n- Always blocked, with no flag to unlock them. Show the SQL to the user to run by hand:\n - `DROP TABLE`\n - `ALTER TABLE ... DROP COLUMN` or `SWITCH`\n - `DELETE`/`UPDATE` without `WHERE` in `exec`\n- Never write or call a live API SP \"to test\" it. Read its body.\n\n## Workspace files (local, no network)\n\n```bash\nfui file where # workspace, project, module, dirs, and why\nfui file status [--all] # files changed since the last publish/sync, with the command to push each\nfui file history <path> # archived versions in _history, newest first\nfui file restore <path> [--at YYYYMMDD_HHMMSS] # the current file is archived first\nfui file ledger <path...> [--mark|--forget]\n```\n\n## Compose with shell\n\nFilter and loop in the shell. Piped output costs no tokens. Every table also takes `--filter text`, `--where col=value` and `--fields a,b` (applied after the request, so nothing else changes).\n\n```bash\nfui sp list --no-header --type API | cut -f2 # API SP names\nfui sp list --no-header | awk -F'\\t' '$5==\"yes\"{print $2}' # SPs with a local copy\nfui schema --type Procedure | grep -i tblStudent | cut -d' ' -f3 # procedures mentioning a word\nfui sp list --no-header --search Student | cut -f2 | xargs -n1 fui sp get\nfui query \"SELECT TOP 5 ID, Name FROM tblX\" --json | jq -r '.[].Name'\n```\n\n- Check exit codes. An exit `3` inside a loop means ask the user; do not retry.\n- Never put `--confirm-write` in a loop or `xargs`.\n\n## Connections and secrets\n\n```bash\nprintf %s \"$TOKEN\" | fui db add docs -p lhu-docs --token - --api-domain tapi.lhu.edu.vn --user-token-env UT\nfui db add local -p demo --conn-env SQLITE_URL # sqlite:// connects directly (MSSQL goes through tAPI)\nfui db default lhu-docs/docs\nfui db test --db lhu-docs/docs\nfui token decode --db lhu-docs/docs # never shows the password\n```\n\nSecrets are accepted only from stdin (`-`) or `--*-env VAR`. Never echo or paste token values.\n\n- Never `cat`, `jq` or otherwise print `_db/_connections.json` or `~/.config/fui/config.json`. Use `fui db list`, `fui token decode` or `fui config`; they never print secrets.\n- No token at hand? `fui db user-token` lists fingerprints of the tokens already in the workspace; copy one with `--from <project/alias>`. The value never appears.\n- Know the alias but not the connection string? `fui db alias list`, then `fui db add-by-name <apiName>`: it borrows the SQL login of a connection on the same apiDomain.\n- `db add` / `db add-by-name` take `--name <connectionName>`; omitted, it is suggested from apiName, then apiName_zone. The same server + database re-added keeps its name, and folders are never renamed.\n- `right *` and `db alias new` write to the shared acc system. Ask the user first, especially for `delete`.\n- `_db/_connections.json` must be version 2. An old v1 file stops every DB command with a hint; convert it once with `fui db migrate` (the old file goes to `_history/`).\n\n## FUI conventions\n\n- tAPI routes:\n - `spAPI_<Fn>` → `/<apiName>/<Fn>`\n - `spAPI_AUTH_<Fn>` → `/<apiName>/auth/<Fn>`\n - `spAPIFILE_*` → file endpoints\n- tAPI parameters must not have default values (`@x int = 0`). Deploy blocks them.\n- Every new procedure needs `GRANT EXECUTE ... TO [public]`. `sp deploy` does it; raw `exec` only reports it.\n\n`fui --help` lists commands. `fui <command> --help` shows usage for one command.\n";import{readdir as Hg}from"node:fs/promises";import{dirname as Vg,join as Kt}from"node:path";import{fileURLToPath as Wg}from"node:url";var fr="fui-skill",di="fui-target";async function zu(){let e=Vg(Wg(import.meta.url));for(let t of[Kt(e,"..","..","..","skills"),Kt(e,"..","skills")])if(await ot(Kt(t,fr,"SKILL.md")))return t;return Kt(e,"..","..","..","skills")}async function Tt(){return Kt(await zu(),fr)}async function Xu(){return Kt(await zu(),di)}async function pi(){let e=Kt(await Tt(),"design-md");return(await Hg(e,{withFileTypes:!0})).filter((n)=>n.isDirectory()).map((n)=>n.name).sort()}import{isAbsolute as qg,join as mi,relative as Bg}from"node:path";var Yu="fui skill ref references/controls-patterns.md references/module-structure.md";async function Qu(e){return he(mi(await Tt(),e))}async function hi(e){let t=await Tt();return(await Promise.all(e.map(async(o)=>{let s=Bg(t,mi(t,o)),r=s.startsWith("..")||qg(s)?"invalid path (outside the skill folder)":await Qu(o)??`not found at ${mi(t,o)}`;return`
330
+ `)}},td=[Bg,Jg];import{join as nw}from"node:path";var gr="---\nname: fui\ndescription: Use the `fui` CLI for all FUI (low-code platform) work — projects, modules (get, validate, simulate, preview, publish), components, imports, project users, rights, DB connections, SQL, schema, stored procedures (tAPI spAPI_*), framework library, workspace files, publish ledger and history. Run it in the shell.\n---\n\n# fui CLI\n\n`fui` is the command-line tool for the FUI low-code platform. Run it with your shell tool.\n\n## Before you start\n\n- FUI has its own conventions (module.json action engine, tAPI SP naming, `f-*` components). They differ from standard Vue/Vuetify/REST, so never rely on training-data assumptions.\n- Before creating, editing, reviewing or deploying any module, component or stored procedure: run `fui skill get` (SKILL.md and the `references/INDEX.md` router). For module or UI work also load `fui skill ref references/controls-patterns.md references/module-structure.md`; pick other files from the router the same way. The same files are in the `fui-skill` skill.\n- Strict mode (`FUI_STRICT=1`) blocks publishing and deploys until the skill was read this session (`fui skill get`, `fui skill ref` or `fui skill loaded`) and while module.json has validation errors.\n- First use on a machine: `fui config set api-base <url>` and `printf %s \"$TOKEN\" | fui config set api-token -` (ask the user for both). `fui config` shows what is set.\n\n## Working rules\n\n- **Build order:** UI first with fake data in `data[]` plus a visual check, then design and deploy the SP contract, then replace the fake data with real API bindings (IN/OUT/CALLBACK).\n- **Check cheapest first:** `module check` (validate, outline and simulate in one step) → `component preview` → `module simulate --render`. Read every screenshot you get and fix what you see in the same turn.\n- **Never call a real API/SP/DML \"to test\".** Verifying an SP means reading its body (`fui sp verify`). Data edits the user asks for still run, gated by `--confirm-write`.\n- **Create and edit are separate commands** (`new` / `update`); nothing upserts. Editing needs the record id.\n- **Publishing is per file:** edit the local file, then push it. Pushing module.json does not carry script.js along (`module publish json` / `script` / `css` / `html`, `component new` / `update`, `import upload`, `project publish-config`).\n- **Local files are a snapshot;** other people publish to the same project. `module get`, `project sync`, `schema pull` and `sp get` overwrite every file in their scope without asking. Whatever they replace is copied to `<workspace>/_history/` first (`fui file history <path>`, `fui file restore <path>`).\n\n## Output contract\n\n- stdout is data only: TSV with a header when piped, an aligned table in a terminal, `--json` for one-line JSON, `--no-header` to drop the header.\n- stderr carries `# ...` notes and `error:` / `guard:` lines.\n- Exit codes:\n - `0`: ok, including \"skipped: unchanged\".\n - `1`: runtime error (network or SQL).\n - `2`: usage error. Read the message; it lists the valid choices.\n - `3`: blocked by a guard, or waiting for `--confirm-write`.\n\n## Targets\n\n- **Project and module**, first match wins:\n 1. `-p <project> -m <module>`. Passing only one of them is an error.\n 2. The current directory, if it is inside `<workspace>/<project>/modules/<module>/`.\n 3. The session target.\n- **Session:** one per `FUI_SESSION` (default `default`). Parallel agents should each set their own `FUI_SESSION`.\n- **Connection:**\n - `--db <connectionName>` or `--db <project>/<connectionName>`. A unique apiName is accepted too.\n - connectionName is the local key and `_db/<connectionName>/` folder (e.g. `docs`, `docs_lhbs`); apiName is the tAPI route alias and may repeat across zones. `fui db list` shows both.\n - With no `--db`: the only connection, else one inferred from the object name (`sp` commands), else the default.\n - Writes never use the default when a project has several connections.\n- `fui file where` prints what resolved and why.\n- `fui config` shows the workspace and API settings with their sources: flag, env (`FUI_WORKDIR`, `FUI_API_BASE`, `FUI_API_TOKEN`), then `fui config set`. The default workspace is `~/.fui/workspaces`.\n- A global install updates itself in the background, at most once an hour. A line starting `# fui updated` means a new version is running; the skills for Claude Code, Codex and Antigravity refresh on their own. `fui --version` prints the version.\n\n## Modules\n\n1. `fui module get /project/module` (also a live URL or display name).\n - It archives local files that differ, deletes `.vue` files the server no longer has, and makes the module the session target.\n2. Edit the files in `<workspace>/<project>/modules/<module>/`. Running `fui` from inside that folder targets it.\n3. Check, cheapest first. `fui module check [--scenario ...] [--mocks ...] [--last]` runs the first three in one step and prints only problems, asserts and stats (`--full` for every report); it stops after validate when there are errors:\n - `fui module validate`: fix every error (exit 1 while any remain); it only reads local files.\n - `fui module outline`: the layout tree (grid widths, bindings, actions, dialogs) to confirm the structure is what you meant.\n - `fui module simulate --scenario '[{\"click\":\"Save\"},{\"assert\":\"vueData.list.length > 0\"}]'`: runs the module in a simulated runtime; exit 1 when an assert fails. `--mocks` wins over live data. An unmocked static API may call the real tAPI when its local `.sql` body reads as read-only (max 8 calls, cached in `_testdata/`). For an existing SP take mocks from `fui sp verify`, never invent them. Keep scenario and mocks in files (`--scenario-file`, `--mocks-file`); `fui module simulate --last` reruns the module's last input (render mode, viewport, files) and re-reads those files. When the scenario changes a lot, pass it inline with `--last --scenario '[...]'`: one command, one step. Add `--brief` (remembered by `--last`) to print only failures, warnings, asserts, stats and screenshot paths.\n - `fui component preview <uc-name> --cases '[{\"name\":\"empty\",\"props\":{\"items\":[]}}]'`: one local `.vue` alone in a real browser; do it before wiring a component in and before publishing a project-scope one.\n - `fui module simulate --render [--device mobile]`: the real page in headless Chromium, last and only for problems that need pixels. Project-level assets come from the server, so publish project files first. It prints screenshot paths: open and read them, then fix what you see. Needs Playwright once, installed with the same package manager as fui: `npm install -g playwright && npx playwright install chromium` (or `bun add -g playwright && bunx playwright install chromium`).\n4. Publish:\n - **Whole module, including header/body:** `fui module preview` → show the changes to the user → after approval `fui module publish html --approval <token>`.\n - The token breaks if any payload file changes after the preview.\n - **One file:** `fui module publish json|script|css`, `fui component update <name>`, or `fui file push <path...>`, which routes each path to the right publisher.\n - Unchanged files are skipped via the publish ledger.\n5. `fui file status` lists what is still unpublished.\n\n`fui remote module --part ui|html|script|css|load|components|imports` and `fui remote project --part config|modules|…` show the server copy without touching the workspace. `fui remote diff [path...]` compares local files with the server (no path: the whole target module); `--stat` adds the +/- line counts.\n\nAsk the user before `module delete`, `component delete`, `import delete` and `user remove`.\n\n## Components\n\n- A component is one `uc-<name>.vue` file (Vue 2 options API). FUI takes the name from the file name and registers every `uc-*.vue` globally, so leave out `name:` and `components: {}` in `export default`.\n- Use Vuetify components where they fit and validate props.\n- Check it alone with `fui component preview <uc-name> --cases [...]` before wiring it into a module, then `fui component new|update <name>`.\n\n## Framework library\n\n`fui library` lists the FUI framework sources on fui.vn (`component`, `component-table`, `default-function`, `fast-project`, `default-style`). `fui library default-function --path` caches one for a day and prints its path, so you can `grep` it instead of reading it whole.\n\n## 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 # default cap 200 rows; 0 = all\necho \"SELECT 1 AS x\" | fui query -\n```\n\nNarrow with `TOP`/`WHERE` first. Every row you read costs tokens.\n\n## Structure (MSSQL)\n\n```bash\nfui schema --search studentid # compact DDL lines; --search matches the whole line (columns, params)\nfui schema --type API --search Student # Table View API \"API File\" Procedure Function\nfui schema pull # refresh schema.json + every local .sql body\nfui sp list --search Student # columns: db name kind count local\n```\n\n- With no cache, `schema` fails and tells you to run `fui schema pull`. It never pulls on its own.\n- A `# warning: ... last synced N days ago` note means pull before trusting the names.\n- `schema pull` archives and deletes local `.sql` files whose objects were dropped on the server.\n- It only lists drafts that exist in neither schema. `--prune-local-only` archives those too, but only once the user confirms they are stale.\n\n## Stored procedures (MSSQL)\n\n1. `fui sp get <name>` saves the live body to `_db/<alias>/<name>.sql`. A differing local copy is archived first.\n - For a new SP: `fui sp path <name> --db <ref>`, or `fui sp save <name> -f file.sql --db <ref>`.\n2. Edit the file. `CREATE` versus `ALTER` does not matter.\n3. `fui sp deploy <name>`:\n - saves the live body as a rollback copy\n - fixes the verb\n - grants `EXECUTE` to `[public]` on new procedures and checks it\n - clears the tAPI cache\n4. If the grant failed, do **not** redeploy. Run the printed `fui exec \"GRANT ...\"`.\n\nBefore writing `apiMocks` for an SP that exists, run `fui sp verify <name>`. It reads the real body (never calls the endpoint) and prints the params, the response shape, the error contract, read/write and a mock snippet. `sp delete` and `sp rename` change the server: ask the user first.\n\n## Writes need the user\n\n- Writes to real data exit `3` until you add `--confirm-write`. That covers `INSERT`, `UPDATE`, `DELETE`, `MERGE` and `TRUNCATE` in `query` or `exec`, and `ALTER TABLE ... ALTER COLUMN`.\n- Add `--confirm-write` **only after the user approved that exact write in chat.**\n- Always blocked, with no flag to unlock them. Show the SQL to the user to run by hand:\n - `DROP TABLE`\n - `ALTER TABLE ... DROP COLUMN` or `SWITCH`\n - `DELETE`/`UPDATE` without `WHERE` in `exec`\n- Never write or call a live API SP \"to test\" it. Read its body.\n\n## Workspace files (local, no network)\n\n```bash\nfui file where # workspace, project, module, dirs, and why\nfui file status [--all] # files changed since the last publish/sync, with the command to push each\nfui file history <path> # archived versions in _history, newest first\nfui file restore <path> [--at YYYYMMDD_HHMMSS] # the current file is archived first\nfui file ledger <path...> [--mark|--forget]\n```\n\n## Compose with shell\n\nFilter and loop in the shell. Piped output costs no tokens. Every table also takes `--filter text`, `--where col=value` and `--fields a,b` (applied after the request, so nothing else changes).\n\n```bash\nfui sp list --no-header --type API | cut -f2 # API SP names\nfui sp list --no-header | awk -F'\\t' '$5==\"yes\"{print $2}' # SPs with a local copy\nfui schema --type Procedure | grep -i tblStudent | cut -d' ' -f3 # procedures mentioning a word\nfui sp list --no-header --search Student | cut -f2 | xargs -n1 fui sp get\nfui query \"SELECT TOP 5 ID, Name FROM tblX\" --json | jq -r '.[].Name'\n```\n\n- Check exit codes. An exit `3` inside a loop means ask the user; do not retry.\n- Never put `--confirm-write` in a loop or `xargs`.\n\n## Connections and secrets\n\n```bash\nprintf %s \"$TOKEN\" | fui db add docs -p lhu-docs --token - --api-domain tapi.lhu.edu.vn --user-token-env UT\nfui db add local -p demo --conn-env SQLITE_URL # sqlite:// connects directly (MSSQL goes through tAPI)\nfui db default lhu-docs/docs\nfui db test --db lhu-docs/docs\nfui token decode --db lhu-docs/docs # never shows the password\n```\n\nSecrets are accepted only from stdin (`-`) or `--*-env VAR`. Never echo or paste token values.\n\n- Never `cat`, `jq` or otherwise print `_db/_connections.json` or `~/.config/fui/config.json`. Use `fui db list`, `fui token decode` or `fui config`; they never print secrets.\n- No token at hand? `fui db user-token` lists fingerprints of the tokens already in the workspace; copy one with `--from <project/alias>`. The value never appears.\n- Know the alias but not the connection string? `fui db alias list`, then `fui db add-by-name <apiName>`: it borrows the SQL login of a connection on the same apiDomain.\n- `db add` / `db add-by-name` take `--name <connectionName>`; omitted, it is suggested from apiName, then apiName_zone. The same server + database re-added keeps its name, and folders are never renamed.\n- `right *` and `db alias new` write to the shared acc system. Ask the user first, especially for `delete`.\n- `_db/_connections.json` must be version 2. An old v1 file stops every DB command with a hint; convert it once with `fui db migrate` (the old file goes to `_history/`).\n\n## FUI conventions\n\n- tAPI routes:\n - `spAPI_<Fn>` → `/<apiName>/<Fn>`\n - `spAPI_AUTH_<Fn>` → `/<apiName>/auth/<Fn>`\n - `spAPIFILE_*` → file endpoints\n- tAPI parameters must not have default values (`@x int = 0`). Deploy blocks them.\n- Every new procedure needs `GRANT EXECUTE ... TO [public]`. `sp deploy` does it; raw `exec` only reports it.\n\n`fui --help` lists commands. `fui <command> --help` shows usage for one command.\n";import{readdir as Gg}from"node:fs/promises";import{dirname as zg,join as Kt}from"node:path";import{fileURLToPath as Xg}from"node:url";var wr="fui-skill",gi="fui-target";async function nd(){let e=zg(Xg(import.meta.url));for(let t of[Kt(e,"..","..","..","skills"),Kt(e,"..","skills")])if(await ot(Kt(t,wr,"SKILL.md")))return t;return Kt(e,"..","..","..","skills")}async function Tt(){return Kt(await nd(),wr)}async function od(){return Kt(await nd(),gi)}async function wi(){let e=Kt(await Tt(),"design-md");return(await Gg(e,{withFileTypes:!0})).filter((n)=>n.isDirectory()).map((n)=>n.name).sort()}import{isAbsolute as Yg,join as yi,relative as Qg}from"node:path";var rd="fui skill ref references/controls-patterns.md references/module-structure.md";async function sd(e){return he(yi(await Tt(),e))}async function bi(e){let t=await Tt();return(await Promise.all(e.map(async(o)=>{let s=Qg(t,yi(t,o)),r=s.startsWith("..")||Yg(s)?"invalid path (outside the skill folder)":await sd(o)??`not found at ${yi(t,o)}`;return`
329
331
 
330
332
  ---
331
333
  <!-- ${o} -->
332
334
 
333
- ${r}`}))).join("")}async function Zu(){return`${await Qu("SKILL.md")??""}${await hi(["references/INDEX.md"])}`}import{cp as ed,mkdir as td,rm as nd}from"node:fs/promises";import{homedir as Jg}from"node:os";import{join as qe}from"node:path";var ao=["claude","codex","antigravity"];function Cn(e){return e.HOME||Jg()}function Kg(e,t=process.env){if(e==="claude")return qe(t.CLAUDE_CONFIG_DIR||qe(Cn(t),".claude"),"skills");if(e==="codex")return qe(Cn(t),".agents","skills");return qe(Cn(t),".gemini","antigravity","skills")}async function fi(e){try{return await wr(e)}catch{return!1}}async function gi(e=process.env){let t=[];if(await fi(e.CLAUDE_CONFIG_DIR||qe(Cn(e),".claude")))t.push("claude");if(await fi(e.CODEX_HOME||qe(Cn(e),".codex")))t.push("codex");if(await fi(qe(Cn(e),".gemini","antigravity")))t.push("antigravity");return t}async function wi(e){await F(qe(e,"fui","SKILL.md"),hr);let t=qe(e,fr);return await nd(t,{recursive:!0,force:!0}),await td(e,{recursive:!0}),await ed(await Tt(),t,{recursive:!0}),[qe(e,"fui"),t]}async function Gg(e){let t=qe(e,di);return await nd(t,{recursive:!0,force:!0}),await td(e,{recursive:!0}),await ed(await Xu(),t,{recursive:!0}),t}async function yi(e,t=process.env){let n=[];for(let i of e){let a=Kg(i,t);if(n.push(...await wi(a)),i==="claude")n.push(await Gg(a))}let o=await Kn(t),s=o.skillsVersion===le?o.skillsAgents??[]:[],r=ao.filter((i)=>e.includes(i)||s.includes(i));return await ut({...o,skillsVersion:le,skillsAgents:r},t),n}async function od(e=process.env,t=ht()){if(!Mo(t))return null;let n=e.FUI_AUTO_SKILLS?.trim().toLowerCase();if(n&&["0","off","false","no"].includes(n))return null;let o=await Kn(e),s=o.skillsVersion===le?o.skillsAgents??[]:[],r=(await gi(e)).filter((i)=>!s.includes(i));if(r.length===0)return null;return await yi(r,e),`# fui ${le} skills installed for ${r.join(", ")}`}var Xg={name:"skill",usage:"fui skill",summary:"Print the fui CLI agent guide",async run(){k(hr)}},Yg={name:"skill get",usage:"fui skill get",summary:"Print the FUI domain skill (SKILL.md + references/INDEX.md); marks it loaded",async run(e){await e.session.markSkillLoaded(),T(`# module/UI work also needs: ${Yu}`),k(`${await Zu()}
334
- `)}},Qg={name:"skill ref",usage:"fui skill ref <path> [path...] e.g. references/db-workflow.md",summary:"Print skill reference files; marks the skill loaded",async run(e){if(e.positionals.length===0)throw new m("missing <path>; see references/INDEX.md");await e.session.markSkillLoaded(),k(`${(await hi(e.positionals)).trimStart()}
335
- `)}},Zg={name:"skill loaded",usage:"fui skill loaded",summary:"Record that the skill was read (after reading installed skill files yourself); satisfies strict-mode gates",async run({session:e}){await e.markSkillLoaded(),k(`skill marked loaded for this session
336
- `)}},ew={name:"skill install",usage:"fui skill install [--agent claude,codex,antigravity|all] [--dir <skills-root>]",summary:"Install the fui and fui-skill skills for Claude, Codex and Antigravity (default: those found), plus the fui-target mod for Claude; a global install does this itself",options:{agent:{type:"string"},dir:{type:"string"}},async run({values:e}){let t=f(e,"dir");if(t){k(`${(await wi(t)).join(`
335
+ ${r}`}))).join("")}async function id(){return`${await sd("SKILL.md")??""}${await bi(["references/INDEX.md"])}`}import{cp as ad,mkdir as cd,rm as ld}from"node:fs/promises";import{homedir as Zg}from"node:os";import{join as qe}from"node:path";var co=["claude","codex","antigravity"];function Cn(e){return e.HOME||Zg()}function ew(e,t=process.env){if(e==="claude")return qe(t.CLAUDE_CONFIG_DIR||qe(Cn(t),".claude"),"skills");if(e==="codex")return qe(Cn(t),".agents","skills");return qe(Cn(t),".gemini","antigravity","skills")}async function ki(e){try{return await br(e)}catch{return!1}}async function vi(e=process.env){let t=[];if(await ki(e.CLAUDE_CONFIG_DIR||qe(Cn(e),".claude")))t.push("claude");if(await ki(e.CODEX_HOME||qe(Cn(e),".codex")))t.push("codex");if(await ki(qe(Cn(e),".gemini","antigravity")))t.push("antigravity");return t}async function $i(e){await L(qe(e,"fui","SKILL.md"),gr);let t=qe(e,wr);return await ld(t,{recursive:!0,force:!0}),await cd(e,{recursive:!0}),await ad(await Tt(),t,{recursive:!0}),[qe(e,"fui"),t]}async function tw(e){let t=qe(e,gi);return await ld(t,{recursive:!0,force:!0}),await cd(e,{recursive:!0}),await ad(await od(),t,{recursive:!0}),t}async function Si(e,t=process.env){let n=[];for(let i of e){let a=ew(i,t);if(n.push(...await $i(a)),i==="claude")n.push(await tw(a))}let o=await Kn(t),s=o.skillsVersion===le?o.skillsAgents??[]:[],r=co.filter((i)=>e.includes(i)||s.includes(i));return await ut({...o,skillsVersion:le,skillsAgents:r},t),n}async function ud(e=process.env,t=ht()){if(!Fo(t))return null;let n=e.FUI_AUTO_SKILLS?.trim().toLowerCase();if(n&&["0","off","false","no"].includes(n))return null;let o=await Kn(e),s=o.skillsVersion===le?o.skillsAgents??[]:[],r=(await vi(e)).filter((i)=>!s.includes(i));if(r.length===0)return null;return await Si(r,e),`# fui ${le} skills installed for ${r.join(", ")}`}var ow={name:"skill",usage:"fui skill",summary:"Print the fui CLI agent guide",async run(){b(gr)}},rw={name:"skill get",usage:"fui skill get",summary:"Print the FUI domain skill (SKILL.md + references/INDEX.md); marks it loaded",async run(e){await e.session.markSkillLoaded(),T(`# module/UI work also needs: ${rd}`),b(`${await id()}
336
+ `)}},sw={name:"skill ref",usage:"fui skill ref <path> [path...] e.g. references/db-workflow.md",summary:"Print skill reference files; marks the skill loaded",async run(e){if(e.positionals.length===0)throw new h("missing <path>; see references/INDEX.md");await e.session.markSkillLoaded(),b(`${(await bi(e.positionals)).trimStart()}
337
+ `)}},iw={name:"skill loaded",usage:"fui skill loaded",summary:"Record that the skill was read (after reading installed skill files yourself); satisfies strict-mode gates",async run({session:e}){await e.markSkillLoaded(),b(`skill marked loaded for this session
338
+ `)}},aw={name:"skill install",usage:"fui skill install [--agent claude,codex,antigravity|all] [--dir <skills-root>]",summary:"Install the fui and fui-skill skills for Claude, Codex and Antigravity (default: those found), plus the fui-target mod for Claude; a global install does this itself",options:{agent:{type:"string"},dir:{type:"string"}},async run({values:e}){let t=f(e,"dir");if(t){b(`${(await $i(t)).join(`
337
339
  `)}
338
- `);return}let n=f(e,"agent"),o=n==="all"?[...ao]:n?n.split(",").map((s)=>s.trim()):await gi();if(o.length===0)throw new m("no agent found (~/.claude, ~/.codex, ~/.gemini/antigravity); pass --agent or --dir");for(let s of o)if(!ao.includes(s))throw new m(`--agent takes ${ao.join(", ")} or all`);k(`${(await yi(o)).join(`
340
+ `);return}let n=f(e,"agent"),o=n==="all"?[...co]:n?n.split(",").map((s)=>s.trim()):await vi();if(o.length===0)throw new h("no agent found (~/.claude, ~/.codex, ~/.gemini/antigravity); pass --agent or --dir");for(let s of o)if(!co.includes(s))throw new h(`--agent takes ${co.join(", ")} or all`);b(`${(await Si(o)).join(`
339
341
  `)}
340
- `)}},tw={name:"design list",usage:"fui design list",summary:"List bundled design systems (brands) for fui design read",async run({render:e}){let t=await pi();T(`# ${t.length} brands`),k(e.json?`${JSON.stringify(t)}
342
+ `)}},cw={name:"design list",usage:"fui design list",summary:"List bundled design systems (brands) for fui design read",async run({render:e}){let t=await wi();T(`# ${t.length} brands`),b(e.json?`${JSON.stringify(t)}
341
343
  `:`${t.join(`
342
344
  `)}
343
- `)}},nw={name:"design read",usage:"fui design read <brand>",summary:"Print a brand's DESIGN.md",async run({positionals:e}){let t=e[0];if(!t)throw new m("missing <brand>; run: fui design list");let n=await pi();if(!n.includes(t))throw new m(`unknown brand "${t}"; available: ${n.join(", ")}`);let o=zg(await Tt(),"design-md",t,"DESIGN.md");T(`# source: ${o}`),k(await G(o))}},rd=[Xg,Yg,Qg,Zg,ew,tw,nw];function Gt(e){let t=e[0];if(!t)throw new m("missing <name>");return t}var ow={name:"sp list",usage:'fui sp list [--db ref] [--type Table|View|API|"API File"|Procedure|Function] [--search name]',summary:"List objects from the schema cache (every connection unless --db); count = params or columns",options:{type:{type:"string"},search:{type:"string"}},async run(e){let{rows:t,notes:n}=await Ha(await X(e),{db:f(e.values,"db"),objectType:ui(f(e.values,"type")),search:f(e.values,"search")});for(let r of n)T(`# ${r}`);let o=new Map,s=[];for(let r of t){let i=o.get(r.db);if(!i){let[a,c]=r.db.split("/");i=new Set((await e.store.localSqlNames(a,c)).map((l)=>l.toLowerCase())),o.set(r.db,i)}s.push({...r,local:i.has(r.name.toLowerCase())?"yes":"no"})}k(z(s,e.render))}},rw={name:"sp get",usage:"fui sp get <name> [--db ref] [--print | -o file]",summary:"Fetch a definition into _db/<alias>/<name>.sql (a differing local copy is archived first)",options:{print:{type:"boolean"},out:{type:"string",short:"o"}},async run(e){let t=await Va(await X(e),Gt(e.positionals),f(e.values,"db"));for(let o of t.notes)T(`# ${o}`);let n=f(e.values,"out");if(n)await F(n,t.body),k(`${n}
344
- `);else k(e.values.print===!0?t.body:`${t.path}
345
- `)}},sw={name:"sp save",usage:'fui sp save <name> (--sql "<text>" | -f file | -) [--db ref]',summary:"Write a script to _db/<alias>/<name>.sql locally; nothing reaches the database",options:{sql:{type:"string"},file:{type:"string",short:"f"}},async run(e){let t=Gt(e.positionals),n=f(e.values,"sql"),o=e.positionals[1];if(n!==void 0&&o!==void 0)throw new m("pass --sql or - / -f, not both");let s=await Dn(n??o,f(e.values,"file")),r=await Wa(await X(e),t,s,f(e.values,"db"));for(let i of r.notes)T(`# ${i}`);k(`${r.path}
345
+ `)}},lw={name:"design read",usage:"fui design read <brand>",summary:"Print a brand's DESIGN.md",async run({positionals:e}){let t=e[0];if(!t)throw new h("missing <brand>; run: fui design list");let n=await wi();if(!n.includes(t))throw new h(`unknown brand "${t}"; available: ${n.join(", ")}`);let o=nw(await Tt(),"design-md",t,"DESIGN.md");T(`# source: ${o}`),b(await G(o))}},dd=[ow,rw,sw,iw,aw,cw,lw];function Gt(e){let t=e[0];if(!t)throw new h("missing <name>");return t}var uw={name:"sp list",usage:'fui sp list [--db ref] [--type Table|View|API|"API File"|Procedure|Function] [--search name]',summary:"List objects from the schema cache (every connection unless --db); count = params or columns",options:{type:{type:"string"},search:{type:"string"}},async run(e){let{rows:t,notes:n}=await Ba(await X(e),{db:f(e.values,"db"),objectType:fi(f(e.values,"type")),search:f(e.values,"search")});for(let r of n)T(`# ${r}`);let o=new Map,s=[];for(let r of t){let i=o.get(r.db);if(!i){let[a,c]=r.db.split("/");i=new Set((await e.store.localSqlNames(a,c)).map((l)=>l.toLowerCase())),o.set(r.db,i)}s.push({...r,local:i.has(r.name.toLowerCase())?"yes":"no"})}b(z(s,e.render))}},dw={name:"sp get",usage:"fui sp get <name> [--db ref] [--print | -o file]",summary:"Fetch a definition into _db/<alias>/<name>.sql (a differing local copy is archived first)",options:{print:{type:"boolean"},out:{type:"string",short:"o"}},async run(e){let t=await Ja(await X(e),Gt(e.positionals),f(e.values,"db"));for(let o of t.notes)T(`# ${o}`);let n=f(e.values,"out");if(n)await L(n,t.body),b(`${n}
346
+ `);else b(e.values.print===!0?t.body:`${t.path}
347
+ `)}},pw={name:"sp save",usage:'fui sp save <name> (--sql "<text>" | -f file | -) [--db ref]',summary:"Write a script to _db/<alias>/<name>.sql locally; nothing reaches the database",options:{sql:{type:"string"},file:{type:"string",short:"f"}},async run(e){let t=Gt(e.positionals),n=f(e.values,"sql"),o=e.positionals[1];if(n!==void 0&&o!==void 0)throw new h("pass --sql or - / -f, not both");let s=await Dn(n??o,f(e.values,"file")),r=await Ka(await X(e),t,s,f(e.values,"db"));for(let i of r.notes)T(`# ${i}`);b(`${r.path}
346
348
  next: fui sp deploy ${t} --db ${r.alias}
347
- `)}},iw={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=bt(Gt(e.positionals)),n=await X(e),o=await e.store.resolve({db:f(e.values,"db"),project:n.project,contextProject:n.contextProject,objectName:t});k(`${e.store.paths.spFile(o.projectId,o.alias,t)}
348
- `)}},aw={name:"sp deploy",usage:"fui sp deploy <name> [--db ref]",summary:"Deploy the local .sql: rollback copy, CREATE/ALTER fix, grant, tAPI cache clear",async run(e){let t=await qa(await X(e),Gt(e.positionals),f(e.values,"db"));k(`${t.notes.join(`
349
+ `)}},mw={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=bt(Gt(e.positionals)),n=await X(e),o=await e.store.resolve({db:f(e.values,"db"),project:n.project,contextProject:n.contextProject,objectName:t});b(`${e.store.paths.spFile(o.projectId,o.alias,t)}
350
+ `)}},hw={name:"sp deploy",usage:"fui sp deploy <name> [--db ref]",summary:"Deploy the local .sql: rollback copy, CREATE/ALTER fix, grant, tAPI cache clear",async run(e){let t=await Ga(await X(e),Gt(e.positionals),f(e.values,"db"));b(`${t.notes.join(`
349
351
  `)}
350
- `)}},cw={name:"sp help",usage:"fui sp help <name|Fn> [--db ref] [--full]",summary:"Call tAPI /help: shows params and clears the tAPI cache (never fails)",options:{full:{type:"boolean"}},async run(e){let t=await Ba(await X(e),Gt(e.positionals),f(e.values,"db"),e.values.full===!0?0:_a);for(let n of t.notes)T(`# ${n}`);k(t.data??"")}};function lw(e){if(e===void 0)return;let t;try{t=JSON.parse(e)}catch(n){throw new m(`--params is not valid JSON: ${n.message}`)}if(!t||typeof t!=="object"||Array.isArray(t))throw new m("--params must be a JSON object");return t}var uw={name:"sp verify",usage:`fui sp verify <name|Fn> [--db ref] [--params '{"ClassID":"C01"}'] [--url-param v]... [--check-grant]`,summary:"Static contract check of an API SP from its real body (params, response shape, errors, read/write, mock); never calls it",options:{params:{type:"string"},"url-param":{type:"string",multiple:!0},"check-grant":{type:"boolean"}},async run(e){let t=e.values["url-param"],n=await Ga(await X(e),{name:Gt(e.positionals),db:f(e.values,"db"),params:lw(f(e.values,"params")),urlParams:Array.isArray(t)?t.map(String):void 0,checkGrant:e.values["check-grant"]===!0});k(`${n.notes.join(`
352
+ `)}},fw={name:"sp help",usage:"fui sp help <name|Fn> [--db ref] [--full]",summary:"Call tAPI /help: shows params and clears the tAPI cache (never fails)",options:{full:{type:"boolean"}},async run(e){let t=await za(await X(e),Gt(e.positionals),f(e.values,"db"),e.values.full===!0?0:Ua);for(let n of t.notes)T(`# ${n}`);b(t.data??"")}};function gw(e){if(e===void 0)return;let t;try{t=JSON.parse(e)}catch(n){throw new h(`--params is not valid JSON: ${n.message}`)}if(!t||typeof t!=="object"||Array.isArray(t))throw new h("--params must be a JSON object");return t}var ww={name:"sp verify",usage:`fui sp verify <name|Fn> [--db ref] [--params '{"ClassID":"C01"}'] [--url-param v]... [--check-grant]`,summary:"Static contract check of an API SP from its real body (params, response shape, errors, read/write, mock); never calls it",options:{params:{type:"string"},"url-param":{type:"string",multiple:!0},"check-grant":{type:"boolean"}},async run(e){let t=e.values["url-param"],n=await Qa(await X(e),{name:Gt(e.positionals),db:f(e.values,"db"),params:gw(f(e.values,"params")),urlParams:Array.isArray(t)?t.map(String):void 0,checkGrant:e.values["check-grant"]===!0});b(`${n.notes.join(`
351
353
  `)}
352
- `)}},dw={name:"sp delete",usage:"fui sp delete <name> --type Procedure|Function [--db ref]",summary:"DROP a procedure or function on the server (ask the user first); archives the local .sql",options:{type:{type:"string"}},async run(e){let t=f(e.values,"type");if(t!=="Procedure"&&t!=="Function")throw new m("--type must be Procedure or Function");let n=await za(await X(e),Gt(e.positionals),t,f(e.values,"db"));k(`${n.notes.join(`
354
+ `)}},yw={name:"sp delete",usage:"fui sp delete <name> --type Procedure|Function [--db ref]",summary:"DROP a procedure or function on the server (ask the user first); archives the local .sql",options:{type:{type:"string"}},async run(e){let t=f(e.values,"type");if(t!=="Procedure"&&t!=="Function")throw new h("--type must be Procedure or Function");let n=await Za(await X(e),Gt(e.positionals),t,f(e.values,"db"));b(`${n.notes.join(`
353
355
  `)}
354
- `)}},pw={name:"sp rename",usage:"fui sp rename <old> <new> [--db ref]",summary:"sp_rename a procedure, function or table on the server; renames the local .sql to match",async run(e){let[t,n]=e.positionals;if(!t||!n)throw new m("usage: fui sp rename <old> <new>");let o=await Xa(await X(e),t,n,f(e.values,"db"));k(`${o.notes.join(`
356
+ `)}},bw={name:"sp rename",usage:"fui sp rename <old> <new> [--db ref]",summary:"sp_rename a procedure, function or table on the server; renames the local .sql to match",async run(e){let[t,n]=e.positionals;if(!t||!n)throw new h("usage: fui sp rename <old> <new>");let o=await ec(await X(e),t,n,f(e.values,"db"));b(`${o.notes.join(`
355
357
  `)}
356
- `)}},sd=[ow,rw,sw,iw,aw,cw,uw,dw,pw];var mw={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,render:n}){let o=await e.resolve({db:f(t,"db"),project:f(t,"project")}),s={db:`${o.projectId}/${o.alias}`,...tn(o.dbToken)};k(z([s],n))}},hw={name:"token encode",usage:"fui token encode --server S --database D --uid U --pwd-env VAR",summary:"Print a FUI MSSQL dbToken (Base64)",options:{server:{type:"string"},database:{type:"string"},uid:{type:"string"},"pwd-env":{type:"string"}},async run({values:e}){let t=f(e,"server"),n=f(e,"database"),o=f(e,"uid"),s=await Te(void 0,f(e,"pwd-env"),"--pwd");if(!t||!n||!o||!s)throw new m("need --server, --database, --uid and --pwd-env");k(`${en(t,n,o,s)}
357
- `)}},id=[mw,hw];var fw={name:"update",usage:"fui update [--check]",summary:"Update fui to the latest npm release with the package manager that installed it (--check only reports)",options:{check:{type:"boolean"},background:{type:"boolean"}},async run({values:e}){if(e.background===!0){await ps().catch(()=>{return});return}if(e.check===!0){let n=await us();k(ds(n)?`update available: ${le} -> ${n}; run: fui update
358
+ `)}},pd=[uw,dw,pw,mw,hw,fw,ww,yw,bw];var kw={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,render:n}){let o=await e.resolve({db:f(t,"db"),project:f(t,"project")}),s={db:`${o.projectId}/${o.alias}`,...tn(o.dbToken)};b(z([s],n))}},vw={name:"token encode",usage:"fui token encode --server S --database D --uid U --pwd-env VAR",summary:"Print a FUI MSSQL dbToken (Base64)",options:{server:{type:"string"},database:{type:"string"},uid:{type:"string"},"pwd-env":{type:"string"}},async run({values:e}){let t=f(e,"server"),n=f(e,"database"),o=f(e,"uid"),s=await Te(void 0,f(e,"pwd-env"),"--pwd");if(!t||!n||!o||!s)throw new h("need --server, --database, --uid and --pwd-env");b(`${en(t,n,o,s)}
359
+ `)}},md=[kw,vw];var $w={name:"update",usage:"fui update [--check]",summary:"Update fui to the latest npm release with the package manager that installed it (--check only reports)",options:{check:{type:"boolean"},background:{type:"boolean"}},async run({values:e}){if(e.background===!0){await fs().catch(()=>{return});return}if(e.check===!0){let n=await ms();b(hs(n)?`update available: ${le} -> ${n}; run: fui update
358
360
  `:`fui ${le} is current (latest ${n})
359
- `);return}let t=await ps();if(t.action==="current")k(`fui ${le} is current
360
- `);else if(t.action==="installed")k(`updated ${le} -> ${t.latest}; skills refreshed for Claude, Codex and Antigravity (those found)
361
- `);else if(t.action==="manual")T(`# ${ht()} is not a global bun or npm install (a checkout); it cannot replace itself`),k(`update available: ${le} -> ${t.latest}; install: ${t.detail}
362
- `);else return T(t.detail??""),k(`update to ${t.latest} failed; retry: ${Oo(_o()?.manager??"bun",t.latest)}
363
- `),Oe.Runtime}},ad=[fw];var bi=[...gc,...uc,...id,...Ku,...Gu,...sd,...zc,...gu,...xu,...Ju,...yc,...rd,..._u,...Wu,...fc,...ad],ld=`global: -p/--project <id> --db <connection|project/connection> --workdir <dir> --json --no-header -h/--help -v/--version
361
+ `);return}let t=await fs();if(t.action==="current")b(`fui ${le} is current
362
+ `);else if(t.action==="installed")b(`updated ${le} -> ${t.latest}; skills refreshed for Claude, Codex and Antigravity (those found)
363
+ `);else if(t.action==="manual")T(`# ${ht()} is not a global bun or npm install (a checkout); it cannot replace itself`),b(`update available: ${le} -> ${t.latest}; install: ${t.detail}
364
+ `);else return T(t.detail??""),b(`update to ${t.latest} failed; retry: ${Mo(Lo()?.manager??"bun",t.latest)}
365
+ `),Oe.Runtime}},hd=[$w];var ji=[...kc,...hc,...md,...ed,...td,...pd,...Zc,...ku,...Lu,...Zu,...$c,...dd,...Uu,...Ku,...bc,...hd],gd=`global: -p/--project <id> --db <connection|project/connection> --workdir <dir> --json --no-header -h/--help -v/--version
364
366
  lists: --filter text --where col=value (repeatable) --fields a,b,c (client-side; the request is unchanged)
365
- exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked`;function cd(){let e=Math.max(...bi.map((n)=>n.name.length)),t=bi.map((n)=>` ${n.name.padEnd(e)} ${n.summary}`);return`fui ${le}: FUI platform CLI
367
+ exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked`;function fd(){let e=Math.max(...ji.map((n)=>n.name.length)),t=ji.map((n)=>` ${n.name.padEnd(e)} ${n.summary}`);return`fui ${le}: FUI platform CLI
366
368
 
367
369
  commands:
368
370
  ${t.join(`
369
371
  `)}
370
372
 
371
- ${ld}
373
+ ${gd}
372
374
  run "fui skill" for the agent guide
373
- `}function ww(e){return`${e.usage}
375
+ `}function jw(e){return`${e.usage}
374
376
  ${e.summary}
375
- ${ld}
376
- `}var yw=3;function bw(e){let t=[];for(let n of e.slice(0,yw)){if(n.startsWith("-"))break;t.push(n)}for(let n=t.length;n>0;n--){let o=t.slice(0,n).join(" "),s=bi.find((r)=>r.name===o);if(s)return{command:s,rest:e.slice(n)}}return null}async function kw(e,t,n,o){if(n instanceof Se&&n.severity==="soft")return;let s=n instanceof Se?n.logKind:"error",r=n instanceof Xe?n.report:`❌ ${o}`;await Fu(e,`fui ${t.name}`,r,s)}async function vw(e){let t=bw(e);if(!t){let r=e[0];if(!r||r==="-h"||r==="--help"||r==="help")return k(cd()),Oe.Ok;if(r==="-v"||r==="--version")return k(`${le}
377
+ ${gd}
378
+ `}var Iw=3;function Cw(e){let t=[];for(let n of e.slice(0,Iw)){if(n.startsWith("-"))break;t.push(n)}for(let n=t.length;n>0;n--){let o=t.slice(0,n).join(" "),s=ji.find((r)=>r.name===o);if(s)return{command:s,rest:e.slice(n)}}return null}async function Rw(e,t,n,o){if(n instanceof Se&&n.severity==="soft")return;let s=n instanceof Se?n.logKind:"error",r=n instanceof Xe?n.report:`❌ ${o}`;await Vu(e,`fui ${t.name}`,r,s)}async function Tw(e){let t=Cw(e);if(!t){let r=e[0];if(!r||r==="-h"||r==="--help"||r==="help")return b(fd()),Oe.Ok;if(r==="-v"||r==="--version")return b(`${le}
377
379
  `),Oe.Ok;return T(`unknown command: ${e.slice(0,2).join(" ")}
378
- `),T(cd()),Oe.Usage}let{command:n,rest:o}=t,s;try{let r;try{r=gw({args:o,options:{...Ki,...n.options},allowPositionals:!0,strict:!0})}catch(i){throw new m(`${i.message}
379
- usage: ${n.usage}`)}if(r.values.help===!0)return k(ww(n)),Oe.Ok;return s=await Gi(r.values,r.positionals),await n.run(s)??Oe.Ok}catch(r){let i=Si(r),a=i===Oe.Guard?"guard":"error",c=r.message;if(T(`${a}: ${c}`),s)await kw(s.paths.root,n,r,c);return i}}async function BI(e=process.argv.slice(2)){let t=await mc().catch(()=>null);if(t)T(t);let n=await od().catch(()=>null);if(n)T(n);process.exitCode=await vw(e)}export{BI as run};
380
+ `),T(fd()),Oe.Usage}let{command:n,rest:o}=t,s;try{let r;try{r=Sw({args:o,options:{...Qi,...n.options},allowPositionals:!0,strict:!0})}catch(i){throw new h(`${i.message}
381
+ usage: ${n.usage}`)}if(r.values.help===!0)return b(jw(n)),Oe.Ok;return s=await Zi(r.values,r.positionals),await n.run(s)??Oe.Ok}catch(r){let i=Ti(r),a=i===Oe.Guard?"guard":"error",c=r.message;if(T(`${a}: ${c}`),s)await Rw(s.paths.root,n,r,c);return i}}async function oC(e=process.argv.slice(2)){let t=await wc().catch(()=>null);if(t)T(t);let n=await ud().catch(()=>null);if(n)T(n);process.exitCode=await Tw(e)}export{oC as run};