@fui-org/fui-cli 1.0.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fui.js CHANGED
@@ -1,24 +1,23 @@
1
- #!/usr/bin/env bun
2
- // @bun
3
- import{parseArgs as Rg}from"util";import{chmod as Mu}from"fs/promises";import{homedir as Fu}from"os";import{join as Ct,resolve as Lu}from"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 pt extends Error{report;name="ReportedFailure";constructor(e,t){super(e);this.report=t}}function Ks(e){if(e instanceof h)return Oe.Usage;if(e instanceof Se)return Oe.Guard;return Oe.Runtime}var io=["workdir","api-base","api-token","auto-update"];function $n(e){return e.HOME||Fu()}function Wt(e,t=Bun.env,n=process.cwd()){let o=e.trim();if(o==="~")return $n(t);if(o.startsWith("~/"))return Ct($n(t),o.slice(2));return Lu(n,o)}function tt(e=Bun.env){let t=e.XDG_CONFIG_HOME||Ct($n(e),".config");return Ct(t,"fui","config.json")}async function mt(e=Bun.env){let t=tt(e),n=Bun.file(t);if(!await n.exists())return{};try{let o=await n.json();if(o&&typeof o==="object"&&!Array.isArray(o))return o}catch{}throw new h(`${t} is not a JSON object; fix or delete it`)}async function Zo(e,t=Bun.env){let n=tt(t);return await Bun.write(n,`${JSON.stringify(e,null,2)}
4
- `),await Mu(n,384),n}function Uu(e){return e.CLAUDE_CONFIG_DIR?Ct(e.CLAUDE_CONFIG_DIR,".claude.json"):Ct($n(e),".claude.json")}function Hu(e,t){return[t.command,...Array.isArray(t.args)?t.args:[]].join(" ").includes("fui-mcp")||e.toLowerCase().includes("fui")}function Gs(e,t){if(!e||typeof e!=="object")return;for(let[n,o]of Object.entries(e)){let s=o?.env?.[t];if(typeof s==="string"&&s.trim()&&Hu(n,o))return s}return}async function Xs(e,t=Bun.env,n=process.cwd()){let o=Uu(t),s=Bun.file(o);if(!await s.exists())return null;let r;try{r=await s.json()}catch{return null}let i=Gs(r.projects?.[n]?.mcpServers,e)??Gs(r.mcpServers,e);return i?{value:i,file:o}:null}async function Bu(e=Bun.env,t=process.cwd()){let n=await Xs("FUI_MCP_WORKDIR",e,t);return n?{path:Wt(n.value,e,t),file:n.file}:null}var er=Ct(".fui-mcp","workspaces");async function Ys(e,t=Bun.env,n=process.cwd()){if(e)return{path:Wt(e,t,n),source:"flag",detail:"--workdir"};if(t.FUI_MCP_WORKDIR)return{path:Wt(t.FUI_MCP_WORKDIR,t,n),source:"env",detail:"FUI_MCP_WORKDIR"};let o=await mt(t);if(typeof o.workdir==="string"&&o.workdir)return{path:Wt(o.workdir,t,n),source:"config",detail:tt(t)};let s=await Bu(t,n);if(s)return{path:s.path,source:"mcp",detail:s.file};return{path:Ct($n(t),er),source:"default",detail:"built-in"}}async function zs(e,t,n,o){let s=n[e];if(s)return{value:s,source:"env",detail:e};let i=(await mt(n))[t];if(typeof i==="string"&&i)return{value:i,source:"config",detail:tt(n)};let a=await Xs(e,n,o);if(a)return{value:a.value,source:"mcp",detail:a.file};return{value:void 0,source:"missing",detail:`set ${e} or run: fui config set ${t} ...`}}async function ao(e=Bun.env,t=process.cwd()){let[n,o]=await Promise.all([zs("FUI_API_BASE","api-base",e,t),zs("FUI_API_TOKEN","api-token",e,t)]);return{base:n,token:o}}function Qs(e){return e?.code==="EPIPE"}process.stdout.on("error",(e)=>{if(Qs(e))process.exit(0);throw e});function k(e){try{process.stdout.write(e)}catch(t){if(Qs(t))process.exit(0);throw t}}function T(e){process.stderr.write(e.endsWith(`
1
+ #!/usr/bin/env node
2
+ import{parseArgs as Cw}from"node:util";import{chmod as Sd}from"node:fs/promises";import{homedir as $d}from"node:os";import{join as Nt,resolve as jd}from"node:path";var Me={Ok:0,Runtime:1,Usage:2,Guard:3};class m extends Error{name="UsageError"}class $e extends Error{severity;logKind;name="GuardError";constructor(e,t="hard",n="guard"){super(e);this.severity=t;this.logKind=n}}class ft extends Error{report;name="ReportedFailure";constructor(e,t){super(e);this.report=t}}function di(e){if(e instanceof m)return Me.Usage;if(e instanceof $e)return Me.Guard;return Me.Runtime}import{createHash as md}from"node:crypto";import{mkdir as hd,readdir as fd,readFile as gd,stat as pi,writeFile as wd}from"node:fs/promises";import{dirname as yd,join as bd}from"node:path";function gt(){return process.argv[1]??""}var mi=new TextDecoder("utf-8");async function Qe(e){return mi.decode(await gd(e))}async function At(e){return JSON.parse(await Qe(e))}async function He(e){try{return(await pi(e)).isFile()}catch{return!1}}async function pr(e){try{return(await pi(e)).isDirectory()}catch{return!1}}async function x(e,t){await hd(yd(e),{recursive:!0}),await wd(e,t)}async function*Yt(e,t){if(!await pr(t))return;yield*mo(t,"",e.split("/"))}function kd(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*mo(e,t,n){let[o,...s]=n;if(o===void 0)return;let r;try{r=await fd(bd(e,t),{withFileTypes:!0})}catch{return}if(o==="**"){yield*mo(e,t,s);for(let a of r)if(a.isDirectory())yield*mo(e,t?`${t}/${a.name}`:a.name,n);return}let i=kd(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*mo(e,c,s)}}function Qt(e,t){return md(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 mi.decode(Buffer.concat(e))}var vd=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g");function mr(e){let t=0;for(let n of e.replace(vd,"")){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 hi(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 ho=["workdir","api-base","api-token","auto-update"];function _n(e){return e.HOME||$d()}function Zt(e,t=process.env,n=process.cwd()){let o=e.trim();if(o==="~")return _n(t);if(o.startsWith("~/"))return Nt(_n(t),o.slice(2));return jd(n,o)}function rt(e=process.env){let t=e.XDG_CONFIG_HOME||Nt(_n(e),".config");return Nt(t,"fui","config.json")}async function wt(e=process.env){let t=rt(e);if(!await He(t))return{};try{let n=await At(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 hr(e,t=process.env){let n=rt(t);return await x(n,`${JSON.stringify(e,null,2)}
3
+ `),await Sd(n,384),n}function Id(e){return e.CLAUDE_CONFIG_DIR?Nt(e.CLAUDE_CONFIG_DIR,".claude.json"):Nt(_n(e),".claude.json")}function Cd(e,t){return[t.command,...Array.isArray(t.args)?t.args:[]].join(" ").includes("fui-mcp")||e.toLowerCase().includes("fui")}function fi(e,t){if(!e||typeof e!=="object")return;for(let[n,o]of Object.entries(e)){let s=o?.env?.[t];if(typeof s==="string"&&s.trim()&&Cd(n,o))return s}return}async function wi(e,t=process.env,n=process.cwd()){let o=Id(t);if(!await He(o))return null;let s;try{s=await At(o)}catch{return null}let r=fi(s.projects?.[n]?.mcpServers,e)??fi(s.mcpServers,e);return r?{value:r,file:o}:null}async function Rd(e=process.env,t=process.cwd()){let n=await wi("FUI_MCP_WORKDIR",e,t);return n?{path:Zt(n.value,e,t),file:n.file}:null}var fr=Nt(".fui-mcp","workspaces");async function yi(e,t=process.env,n=process.cwd()){if(e)return{path:Zt(e,t,n),source:"flag",detail:"--workdir"};for(let r of["FUI_WORKDIR","FUI_MCP_WORKDIR"]){let i=t[r];if(i)return{path:Zt(i,t,n),source:"env",detail:r}}let o=await wt(t);if(typeof o.workdir==="string"&&o.workdir)return{path:Zt(o.workdir,t,n),source:"config",detail:rt(t)};let s=await Rd(t,n);if(s)return{path:s.path,source:"mcp",detail:s.file};return{path:Nt(_n(t),fr),source:"default",detail:"built-in"}}async function gi(e,t,n,o){let s=n[e];if(s)return{value:s,source:"env",detail:e};let i=(await wt(n))[t];if(typeof i==="string"&&i)return{value:i,source:"config",detail:rt(n)};let a=await wi(e,n,o);if(a)return{value:a.value,source:"mcp",detail:a.file};return{value:void 0,source:"missing",detail:`set ${e} or run: fui config set ${t} ...`}}async function fo(e=process.env,t=process.cwd()){let[n,o]=await Promise.all([gi("FUI_API_BASE","api-base",e,t),gi("FUI_API_TOKEN","api-token",e,t)]);return{base:n,token:o}}function bi(e){return e?.code==="EPIPE"}process.stdout.on("error",(e)=>{if(bi(e))process.exit(0);throw e});function k(e){try{process.stdout.write(e)}catch(t){if(bi(t))process.exit(0);throw t}}function T(e){process.stderr.write(e.endsWith(`
5
4
  `)?e:`${e}
6
- `)}async function co(){return Bun.stdin.text()}async function jn(e,t){if(t&&e)throw new h("pass SQL as an argument or with -f, not both");let n;if(t){let o=Bun.file(t);if(!await o.exists())throw new h(`file not found: ${t}`);n=await o.text()}else if(e==="-")n=await co();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 Pe(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=Bun.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 co()).trim();if(!o)throw new h(`${n}: stdin is empty`);return o}var Zs=60000,ei=400;function ti(e){return e.length<=ei?e:`${e.slice(0,ei)}\u2026`}async function qu(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` \u2014 ${ti(s.trim())}`}}catch{}return` \u2014 ${ti(t)}`}function nt(e){return e.data||[]}class ni{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(Zs)})}catch(o){let s=o?.name;if(s==="TimeoutError"||s==="AbortError")throw Error(`FUI API timeout: ${e} did not answer within ${Zs/1000}s`);throw Error(`FUI API network error: ${e} \u2014 ${o?.message??String(o)}`)}if(!n.ok)throw Error(`FUI API error: ${n.status} ${n.statusText}${await qu(n)}`);return await n.json()}async listProjects(){return nt(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 nt(await this.request("/fp/FUI_Dev_UserRightList",{PID:e,MID:""}))}async searchUsers(e,t){return nt(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 nt(await this.request("/fp/FUI_Runtime_ModuleVersionSelect",{domain:e,url1_ID:t,url2_ID:n}))}async listModules(e){return nt(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 nt(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 nt(await this.request("/fp/FUI_Dev_File/select",{ProjectID:e,ModuleID:t}))}async listFrameworkImportFiles(e){return nt(await this.request("/fp/FUI_Dev_File/select",{frameWork:e}))}async listImportFiles(e,t){return nt(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 lo(){let{base:e,token:t}=await ao();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, run fui config set api-base <url> / fui config set api-token -, or configure the Fui MCP server`)}return new ni(e.value,t.value)}import{mkdir as Wu,rename as Ju}from"fs/promises";import{homedir as Ku}from"os";import{join as tr}from"path";var Vu=new TextDecoder("utf-8",{ignoreBOM:!0});async function B(e){return Vu.decode(await Bun.file(e).bytes())}async function le(e){try{return await B(e)}catch{return null}}function In(e){return e?.code}var Gu=/^[A-Za-z0-9_-]+$/;function zu(e=Bun.env){return e.FUI_MCP_SESSION_DIR||tr(e.HOME||Ku(),".fui-mcp","session")}function nr(e=Bun.env){let t=e.FUI_SESSION||"default";if(!Gu.test(t))throw new h(`invalid FUI_SESSION "${t}": letters, digits, "_" or "-"`);return tr(zu(e),`cli-${t}.json`)}class Cn{file;constructor(e=nr()){this.file=e}async read(){let e=await le(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 Wu(tr(this.file,".."),{recursive:!0});let t=`${this.file}.${process.pid}.tmp`;await Bun.write(t,JSON.stringify(e,null,2)),await Ju(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 Jt(e=Bun.env){return["1","true","yes"].includes((e.FUI_MCP_STRICT??"").trim().toLowerCase())}import{readdir as Xu}from"fs/promises";import{isAbsolute as Yu,join as rr,relative as Qu,sep as Zu}from"path";var ed=/^[A-Za-z0-9._ -]+$/,or=20;function ot(e,t){if(!e||!ed.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 td(e,t){let n=Qu(e,t);if(!n||n.startsWith("..")||Yu(n))return null;let[o,s,r]=n.split(Zu);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 oi(e,t){try{return(await Xu(e,{withFileTypes:!0})).filter((o)=>o.isDirectory()&&!(t&&(o.name.startsWith("_")||o.name.startsWith(".")))).map((o)=>o.name).sort()}catch{return[]}}function ri(e){return e.length>or?[...e.slice(0,or),` ... and ${e.length-or} more`]:e}class sr{paths;session;cwd;constructor(e,t,n=process.cwd()){this.paths=e;this.session=t;this.cwd=n}cwdTarget(){return td(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:ot(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 ot(e,"projectId"),ot(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 oi(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}
7
- workspace ${this.paths.root} has no projects; run: fui project list`;if(e==="module"&&t.length===1){let o=await oi(rr(this.paths.root,t[0],"modules"),!1);if(o.length>0)return[n,...ri(o.map((s)=>` fui use ${t[0]}/${s}`))].join(`
8
- `)}return[n,...ri(t.map((o)=>` -p ${o}`))].join(`
9
- `)}}function ir(e){return new TextEncoder().encode(e).toBase64()}function Rn(e){try{return new TextDecoder("utf-8",{fatal:!0}).decode(Uint8Array.fromBase64(e.trim()))}catch{throw new h("token is not valid Base64 UTF-8")}}function Kt(e,t,n,o){return ir(`Server=${e}; Database=${t}; UID=${n}; PWD=${o}; Encrypt=True; TrustServerCertificate=True;`)}function si(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 nd(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=si(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 Pn(e){let t=Rn(e);return{engine:nd(t),plain:t}}function uo(e){let{engine:t,plain:n}=Pn(e);if(t==="mssql"){let s=si(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{join as ud}from"path";import{mkdir as od,readdir as rd,writeFile as sd}from"fs/promises";import{basename as ii,dirname as ci,extname as li,isAbsolute as id,join as ar,relative as ui,sep as ad}from"path";var ai=50;function po(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 cd(e,t,n={}){let o=ui(e.root,t),r=o!==""&&!o.startsWith("..")&&!id(o)?o:ar("_external",ii(t)),i=li(r),a=ii(r,i),c=n.stamp??po(),l=n.suffix?`_${n.suffix}`:"";return ar(e.historyRoot(),ci(r),`${a}_${c}${l}${i}`)}async function ld(e,t){let n=li(e),o=e.slice(0,e.length-n.length);for(let r=1;r<=ai;r++){let i=r===1?e:`${o}_${r}${n}`;try{return await sd(i,t,{encoding:"utf-8",flag:"wx"}),i}catch(a){if(In(a)!=="EEXIST")throw a}}let s=`${o}_${ai}${n}`;return await Bun.write(s,t),s}async function ge(e,t,n={}){let o=n.content;if(o===void 0)try{o=await B(t)}catch(r){let i=In(r);if(i==="ENOENT"||i==="ENOTDIR")return null;throw r}let s=cd(e,t,n);return await od(ci(s),{recursive:!0}),ld(s,o)}function Rt(e,t){let n=ui(e.root,t);return(n.startsWith("..")?t:n).split(ad).join("/")}async function mo(e,t,n={}){let o=n.stamp??po(),s=new Set(n.skipDirNames??["_preview"]),r=[];async function i(a){let c;try{c=await rd(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 ge(e,u,{suffix:n.suffix,stamp:o});if(d)r.push(d)}}}return await i(t),r}var dd=/^[A-Za-z0-9_-]+$/,pd=/^[A-Za-z0-9_.-]+$/;function Gt(e){if(!dd.test(e))throw new h(`invalid alias "${e}": use letters, digits, "_" or "-"`);return e}function cr(e){if(!pd.test(e)||e.startsWith("_")||e.startsWith("."))throw new h(`invalid project id "${e}"`);return e}function di(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 <alias> or <project>/<alias>`)}function he(e,t){let n=t.toLowerCase();return Object.keys(e.connections).find((o)=>o.toLowerCase()===n)??null}class rt{paths;constructor(e){this.paths=e}async load(e){let t=this.paths.connectionsFile(e);if(!await Bun.file(t).exists())return{connections:{}};let o=await B(t);if(!o.trim())return{connections:{}};let s;try{s=JSON.parse(o)}catch(i){throw Error(`${t} is not valid JSON (${i.message}); fix it by hand before writing`)}if(!s||typeof s!=="object"||Array.isArray(s))throw Error(`${t} must be an object { default, connections }`);let r=s;if(r.connections===void 0)return{...r,connections:{}};if(!r.connections||typeof r.connections!=="object"||Array.isArray(r.connections))throw Error(`${t}: "connections" must be an object of alias -> config`);return r}async write(e,t){let n=this.paths.connectionsFile(e);await ge(this.paths,n),await Bun.write(n,JSON.stringify(t,null,2))}async save(e,t,n,o){let s=await this.load(e),r=he(s,t)??Gt(t);if(s.connections[r]=n,o||!s.default||!he(s,s.default))s.default=r;return await this.write(e,s),r}async setDefault(e,t){let n=await this.load(e),o=he(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 loadLegacy(e){let t=await le(ud(this.paths.dbDir(e),"config.json"));if(!t)return null;try{let n=JSON.parse(t);if(!n||typeof n!=="object"||Array.isArray(n))return null;let o=n;return typeof o.dbToken==="string"&&o.dbToken?o:null}catch{return null}}async listLegacyProjects(){let e=[];try{for await(let t of new Bun.Glob("*/_db/config.json").scan({cwd:this.paths.root,onlyFiles:!0})){let n=t.split(/[\\/]/)[0];if(!n.startsWith("_"))e.push(n)}}catch(t){if(t.code!=="ENOENT")throw t}return e.sort()}async listProjects(){let e=[],t=new Bun.Glob("*/_db/_connections.json");try{for await(let n of t.scan({cwd:this.paths.root,onlyFiles:!0})){let o=n.split(/[\\/]/)[0];if(!o.startsWith("_"))e.push(o)}}catch(n){if(n.code!=="ENOENT")throw n}return e.sort()}async resolveProject(e){let t=e.db?di(e.db):void 0,n=t?.project??e.project??Bun.env.FUI_PROJECT??e.contextProject;if(n)return cr(n);let o=await this.listProjects();if(t){let r=[];for(let i of o)if(he(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:
5
+ `)}async function go(){return En()}async function An(e,t){if(t&&e)throw new m("pass SQL as an argument or with -f, not both");let n;if(t){if(!await He(t))throw new m(`file not found: ${t}`);n=await Qe(t)}else if(e==="-")n=await go();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 Ee(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 go()).trim();if(!o)throw new m(`${n}: stdin is empty`);return o}var ki=60000,vi=400;function Si(e){return e.length<=vi?e:`${e.slice(0,vi)}…`}async function Td(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` — ${Si(s.trim())}`}}catch{}return` — ${Si(t)}`}function st(e){return e.data||[]}class $i{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(ki)})}catch(o){let s=o?.name;if(s==="TimeoutError"||s==="AbortError")throw Error(`FUI API timeout: ${e} did not answer within ${ki/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 Td(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 wo(){let{base:e,token:t}=await fo();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, run fui config set api-base <url> / fui config set api-token -, or configure the Fui MCP server`)}return new $i(e.value,t.value)}import{mkdir as _d,rename as Ad}from"node:fs/promises";import{homedir as Nd}from"node:os";import{join as gr}from"node:path";import{readFile as Pd}from"node:fs/promises";var Ed=new TextDecoder("utf-8",{ignoreBOM:!0});async function W(e){return Ed.decode(await Pd(e))}async function ue(e){try{return await W(e)}catch{return null}}function Nn(e){return e?.code}var Dd=/^[A-Za-z0-9_-]+$/;function xd(e=process.env){return e.FUI_SESSION_DIR||e.FUI_MCP_SESSION_DIR||gr(e.HOME||Nd(),".fui-mcp","session")}function wr(e=process.env){let t=e.FUI_SESSION||"default";if(!Dd.test(t))throw new m(`invalid FUI_SESSION "${t}": letters, digits, "_" or "-"`);return gr(xd(e),`cli-${t}.json`)}class Dn{file;constructor(e=wr()){this.file=e}async read(){let e=await ue(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 _d(gr(this.file,".."),{recursive:!0});let t=`${this.file}.${process.pid}.tmp`;await x(t,JSON.stringify(e,null,2)),await Ad(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 en(e=process.env){return["1","true","yes"].includes((e.FUI_STRICT||e.FUI_MCP_STRICT||"").trim().toLowerCase())}import{readdir as Od}from"node:fs/promises";import{isAbsolute as Md,join as br,relative as Fd,sep as Ld}from"node:path";var Ud=/^[A-Za-z0-9._ -]+$/,yr=20;function it(e,t){if(!e||!Ud.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 Hd(e,t){let n=Fd(e,t);if(!n||n.startsWith("..")||Md(n))return null;let[o,s,r]=n.split(Ld);if(!o||o.startsWith("_")||o.startsWith("."))return null;if(s==="modules"&&r)return{projectId:o,moduleId:r,moduleDir:br(e,o,"modules",r)};return{projectId:o}}async function ji(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 Ii(e){return e.length>yr?[...e.slice(0,yr),` ... and ${e.length-yr} more`]:e}class kr{paths;session;cwd;constructor(e,t,n=process.cwd()){this.paths=e;this.session=t;this.cwd=n}cwdTarget(){return Hd(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:br(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 ji(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}
6
+ workspace ${this.paths.root} has no projects; run: fui project list`;if(e==="module"&&t.length===1){let o=await ji(br(this.paths.root,t[0],"modules"),!1);if(o.length>0)return[n,...Ii(o.map((s)=>` fui use ${t[0]}/${s}`))].join(`
7
+ `)}return[n,...Ii(t.map((o)=>` -p ${o}`))].join(`
8
+ `)}}function vr(e){return Buffer.from(e,"utf8").toString("base64")}function xn(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 tn(e,t,n,o){return vr(`Server=${e}; Database=${t}; UID=${n}; PWD=${o}; Encrypt=True; TrustServerCertificate=True;`)}function Ci(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=Ci(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 On(e){let t=xn(e);return{engine:Vd(t),plain:t}}function yo(e){let{engine:t,plain:n}=On(e);if(t==="mssql"){let s=Ci(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{join as Xd}from"node:path";import{mkdir as Wd,readdir as qd,writeFile as Bd}from"node:fs/promises";import{basename as Ri,dirname as Pi,extname as Ei,isAbsolute as Jd,join as Sr,relative as _i,sep as Kd}from"node:path";var Ti=50;function bo(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=_i(e.root,t),r=o!==""&&!o.startsWith("..")&&!Jd(o)?o:Sr("_external",Ri(t)),i=Ei(r),a=Ri(r,i),c=n.stamp??bo(),l=n.suffix?`_${n.suffix}`:"";return Sr(e.historyRoot(),Pi(r),`${a}_${c}${l}${i}`)}async function zd(e,t){let n=Ei(e),o=e.slice(0,e.length-n.length);for(let r=1;r<=Ti;r++){let i=r===1?e:`${o}_${r}${n}`;try{return await Bd(i,t,{encoding:"utf-8",flag:"wx"}),i}catch(a){if(Nn(a)!=="EEXIST")throw a}}let s=`${o}_${Ti}${n}`;return await x(s,t),s}async function we(e,t,n={}){let o=n.content;if(o===void 0)try{o=await W(t)}catch(r){let i=Nn(r);if(i==="ENOENT"||i==="ENOTDIR")return null;throw r}let s=Gd(e,t,n);return await Wd(Pi(s),{recursive:!0}),zd(s,o)}function Dt(e,t){let n=_i(e.root,t);return(n.startsWith("..")?t:n).split(Kd).join("/")}async function ko(e,t,n={}){let o=n.stamp??bo(),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=Sr(a,l.name);if(l.isDirectory()){if(!s.has(l.name))await i(u)}else if(l.isFile()){let d=await we(e,u,{suffix:n.suffix,stamp:o});if(d)r.push(d)}}}return await i(t),r}var Yd=/^[A-Za-z0-9_-]+$/,Qd=/^[A-Za-z0-9_.-]+$/;function nn(e){if(!Yd.test(e))throw new m(`invalid alias "${e}": use letters, digits, "_" or "-"`);return e}function $r(e){if(!Qd.test(e)||e.startsWith("_")||e.startsWith("."))throw new m(`invalid project id "${e}"`);return e}function Ai(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 <alias> or <project>/<alias>`)}function fe(e,t){let n=t.toLowerCase();return Object.keys(e.connections).find((o)=>o.toLowerCase()===n)??null}class at{paths;constructor(e){this.paths=e}async load(e){let t=this.paths.connectionsFile(e);if(!await He(t))return{connections:{}};let n=await W(t);if(!n.trim())return{connections:{}};let o;try{o=JSON.parse(n)}catch(r){throw Error(`${t} is not valid JSON (${r.message}); fix it by hand before writing`)}if(!o||typeof o!=="object"||Array.isArray(o))throw Error(`${t} must be an object { default, connections }`);let s=o;if(s.connections===void 0)return{...s,connections:{}};if(!s.connections||typeof s.connections!=="object"||Array.isArray(s.connections))throw Error(`${t}: "connections" must be an object of alias -> config`);return s}async write(e,t){let n=this.paths.connectionsFile(e);await we(this.paths,n),await x(n,JSON.stringify(t,null,2))}async save(e,t,n,o){let s=await this.load(e),r=fe(s,t)??nn(t);if(s.connections[r]=n,o||!s.default||!fe(s,s.default))s.default=r;return await this.write(e,s),r}async setDefault(e,t){let n=await this.load(e),o=fe(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 loadLegacy(e){let t=await ue(Xd(this.paths.dbDir(e),"config.json"));if(!t)return null;try{let n=JSON.parse(t);if(!n||typeof n!=="object"||Array.isArray(n))return null;let o=n;return typeof o.dbToken==="string"&&o.dbToken?o:null}catch{return null}}async listLegacyProjects(){let e=[];try{for await(let t of Yt("*/_db/config.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 listProjects(){let e=[];try{for await(let t of Yt("*/_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?Ai(e.db):void 0,n=t?.project??e.project??process.env.FUI_PROJECT??e.contextProject;if(n)return $r(n);let o=await this.listProjects();if(t){let r=[];for(let i of o)if(fe(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:
10
9
  `+r.map((i)=>` --db ${i}/${t.alias}`).join(`
11
- `))}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 <alias> -p <project> --token -");throw new h(`several projects have connections; pass -p <project> or --db <project>/<alias>:
10
+ `))}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 <alias> -p <project> --token -");throw new m(`several projects have connections; pass -p <project> or --db <project>/<alias>:
12
11
  `+s.map((r)=>` -p ${r}`).join(`
13
- `))}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 <alias> -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}=Pn(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}=di(n.db),i=he(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?he(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=Bun.file(this.paths.schemaFile(e,t));if(await o.exists())try{let s=await o.json();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 new Bun.Glob("*.sql").scan({cwd:this.paths.connDir(e,t)}))n.push(o.slice(0,-4))}catch(o){if(o.code!=="ENOENT")throw o}return n.sort()}menu(e,t,n){let o=Object.keys(t.connections).map((s)=>` --db ${e}/${s}${t.default===s?" (default)":""}`);return[n,...o].join(`
14
- `)}}import{join as Pt}from"path";class Tn{root;constructor(e){this.root=e}projectDir(e){return Pt(this.root,e)}dbDir(e){return Pt(this.projectDir(e),"_db")}connectionsFile(e){return Pt(this.dbDir(e),"_connections.json")}connDir(e,t){return Pt(this.dbDir(e),t)}schemaFile(e,t){return Pt(this.connDir(e,t),"schema.json")}spFile(e,t,n){return Pt(this.connDir(e,t),`${n}.sql`)}historyRoot(){return Pt(this.root,"_history")}}var pi={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 mi(e,t){let n=await Ys(f(e,"workdir")),o=new Tn(n.path),s=e.json===!0,r={json:s,header:e["no-header"]!==!0,filter:f(e,"filter"),where:md(e.where),fields:f(e,"fields")?.split(",").map((c)=>c.trim()).filter(Boolean)},i=new Cn,a=new sr(o,i);return{paths:o,session:i,targets:a,workdir:n,store:new rt(o),values:e,positionals:t,json:s,render:r}}function md(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 z(e){return{store:e.store,session:e.session,project:f(e.values,"project"),contextProject:await e.targets.contextProjectId()}}function O(e){let t;return{paths:e.paths,session:e.session,targets:e.targets,api:()=>t??=lo()}}function hd(e){if(e===null||e===void 0)return"NULL";if(e instanceof Date)return e.toISOString();if(e instanceof Uint8Array)return`0x${e.toHex()}`;return(typeof e==="object"?JSON.stringify(e):String(e)).replace(/\\/g,"\\\\").replace(/\t/g,"\\t").replace(/\r/g,"\\r").replace(/\n/g,"\\n")}function fi(e,t){let n=[...new Set(e.flatMap((s)=>Object.keys(s)))],o=e.map((s)=>n.map((r)=>hd(s[r])));return t?[n,...o]:o}function fd(e,t=!0){if(e.length===0)return"";return`${fi(e,t).map((n)=>n.join("\t")).join(`
12
+ `))}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 <alias> -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}=On(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}=Ai(n.db),i=fe(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?fe(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 He(o))try{let s=await At(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 Yt("*.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.keys(t.connections).map((s)=>` --db ${e}/${s}${t.default===s?" (default)":""}`);return[n,...o].join(`
13
+ `)}}import{join as xt}from"node:path";class Mn{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 Ni={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 Di(e,t){let n=await yi(f(e,"workdir")),o=new Mn(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 Dn,a=new kr(o,i);return{paths:o,session:i,targets:a,workdir:n,store:new at(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 M(e){let t;return{paths:e.paths,session:e.session,targets:e.targets,api:()=>t??=wo()}}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 Oi(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`${Oi(e,t).map((n)=>n.join("\t")).join(`
15
14
  `)}
16
- `}function gd(e,t=!0){if(e.length===0)return"";let n=fi(e,t),o=n[0].map((r,i)=>Math.max(...n.map((a)=>Bun.stringWidth(a[i]))));return`${n.map((r)=>r.map((i,a)=>a===r.length-1?i:i+" ".repeat(o[a]-Bun.stringWidth(i))).join(" ")).join(`
15
+ `}function np(e,t=!0){if(e.length===0)return"";let n=Oi(e,t),o=n[0].map((r,i)=>Math.max(...n.map((a)=>mr(a[i]))));return`${n.map((r)=>r.map((i,a)=>a===r.length-1?i:i+" ".repeat(o[a]-mr(i))).join(" ")).join(`
17
16
  `)}
18
- `}function lr(e){return`${JSON.stringify(e,(t,n)=>typeof n==="bigint"?n.toString():n)}
19
- `}function hi(e){return e===null||e===void 0?"":typeof e==="object"?JSON.stringify(e):String(e)}function wd(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)=>hi(l).toLowerCase().includes(a)))}for(let[a,c]of n??[]){let l=r(a);if(l)i=i.filter((u)=>hi(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 X(e,t){let{json:n,header:o,isTty:s=process.stdout.isTTY===!0}=t,r=wd(e,t);if(n)return lr(r);return s?gd(r,o):fd(r,o)}function gi(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 bd}from"fs/promises";import{join as ki}from"path";function ht(e){return e.trim().replace(/^https?:\/\//i,"").replace(/\/.*$/,"").toLowerCase()}function Te(e){let t=ht(e);return`${/^(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/.test(t)?"http":"https"}://${t}`}function yd(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 wi(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}: ${yd(r)||s.statusText}`);try{return JSON.parse(r)}catch{throw Error(`tAPI returned non-JSON: ${r.slice(0,400)}`)}}class st{origin;dbToken;engine="mssql";constructor(e,t){this.origin=e;this.dbToken=t}async query(e){return(await wi(this.origin,"/help/_sql_execute",this.dbToken,e)).data??[]}exec(e){return wi(this.origin,"/help/_sql_execute_NonQuery",this.dbToken,e)}async close(){}}async function ur(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()}}var kd=20000;async function En(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(`${Te(e)}/acc/${n}`,{method:"POST",headers:{"Content-Type":"application/json",authorization:`Bearer ${t}`},body:JSON.stringify(o),signal:AbortSignal.timeout(kd)}),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 vi(e){try{return(await bd(e,{withFileTypes:!0})).filter((t)=>t.isDirectory()&&!t.name.startsWith("_")).map((t)=>t.name)}catch{return[]}}async function dr(e){let t=[];for(let n of await vi(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?ht(r.apiDomain):""})}return t}function pr(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 yi(e){return e.map((t)=>`${t.projectId}/${t.alias}`).join(", ")}function Si(e){return e.map((t)=>`${t.projectId}/${t.alias} (${t.apiDomain||"?"})`).join(", ")}function $i(e){return new Bun.CryptoHasher("sha256").update(e).digest("hex").slice(-8)}function ji(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 Ii(e,t,n){let{projectId:o,alias:s}=ji(t,n,"--from"),r=await e.load(o),i=he(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 vd(e,t){let n=await le(ki(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 ht(i)}async function mr(e,t,n){if(n){let o=he(await e.load(t),n);if(o)return e.resolve({project:t,db:o})}return e.resolve({project:t})}async function ho(e,t,n){let o=n.apiDomain?ht(n.apiDomain):void 0;if(o&&n.userToken)return{apiDomain:o,userToken:n.userToken,note:null};try{let{alias:i,config:a}=await mr(e,t,n.apiName),c=o??(a.apiDomain?ht(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 vd(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=pr((await dr(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 ${yi(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)=>` ${yi(i.sources)}`),"pass --user-token-env VAR with the token of the source you mean"].join(`
20
- `))}async function An(e){return(await En(e.apiDomain,e.userToken,"SM_Modules_ManagerSelect",{})).data??[]}function hr(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 bi(e){let t;try{t=Rn(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 Ci(e,t,n,o){let s=async(a,c)=>{let l=await e.load(a),u=he(l,c);if(!u)throw new h(`no connection "${c}" in project "${a}"; see: fui db list -p ${a}`);let d=bi(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}=ji(t,n,"--borrow-from");return s(a,c)}let r=[];for(let a of await vi(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=bi(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 <alias> --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(`
21
- `))}async function Ri(e,t){let n,o;try{let i=he(await e.load(t),"acc");if(!i)throw Error("no acc alias");let a=await e.resolve({project:t,db:i});n=Te(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 st(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 fr(e,t,n,o){try{let s=ki(e.dbDir(t),"rights.json"),r={};try{r=JSON.parse(await le(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 Bun.write(s,JSON.stringify(r,null,2))}catch{}}import{mkdir as cp,rename as lp,unlink as up}from"fs/promises";import{dirname as dp}from"path";var{SQL:Sd}=globalThis.Bun;class gr{engine;sql;constructor(e,t){this.engine=e;this.sql=new Sd(t)}async query(e){return[...await this.sql.unsafe(e)]}async exec(e){return{count:(await this.sql.unsafe(e)).count??null}}close(){return this.sql.close()}}function wr(e){if(e.engine==="mssql")return new st(Te(e.config.apiDomain),e.dbToken);return new gr(e.engine,Rn(e.dbToken))}async function $e(e,t){let n=wr(e);try{return await t(n)}finally{await n.close()}}import{mkdir as qd,readdir as Vd,unlink as Wd}from"fs/promises";import{dirname as Jd,join as Kd}from"path";var Ti=`
17
+ `}function jr(e){return`${JSON.stringify(e,(t,n)=>typeof n==="bigint"?n.toString():n)}
18
+ `}function xi(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)=>xi(l).toLowerCase().includes(a)))}for(let[a,c]of n??[]){let l=r(a);if(l)i=i.filter((u)=>xi(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 Y(e,t){let{json:n,header:o,isTty:s=process.stdout.isTTY===!0}=t,r=op(e,t);if(n)return jr(r);return s?np(r,o):tp(r,o)}function Mi(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 sp}from"node:fs/promises";import{join as Hi}from"node:path";function yt(e){return e.trim().replace(/^https?:\/\//i,"").replace(/\/.*$/,"").toLowerCase()}function _e(e){let t=yt(e);return`${/^(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/.test(t)?"http":"https"}://${t}`}function rp(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 Fi(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}: ${rp(r)||s.statusText}`);try{return JSON.parse(r)}catch{throw Error(`tAPI returned non-JSON: ${r.slice(0,400)}`)}}class ct{origin;dbToken;engine="mssql";constructor(e,t){this.origin=e;this.dbToken=t}async query(e){return(await Fi(this.origin,"/help/_sql_execute",this.dbToken,e)).data??[]}exec(e){return Fi(this.origin,"/help/_sql_execute_NonQuery",this.dbToken,e)}async close(){}}async function Ir(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()}}var ip=20000;async function Fn(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(`${_e(e)}/acc/${n}`,{method:"POST",headers:{"Content-Type":"application/json",authorization:`Bearer ${t}`},body:JSON.stringify(o),signal:AbortSignal.timeout(ip)}),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 Vi(e){try{return(await sp(e,{withFileTypes:!0})).filter((t)=>t.isDirectory()&&!t.name.startsWith("_")).map((t)=>t.name)}catch{return[]}}async function Cr(e){let t=[];for(let n of await Vi(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?yt(r.apiDomain):""})}return t}function Rr(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 Li(e){return e.map((t)=>`${t.projectId}/${t.alias}`).join(", ")}function Wi(e){return e.map((t)=>`${t.projectId}/${t.alias} (${t.apiDomain||"?"})`).join(", ")}function qi(e){return Qt("sha256",e).slice(-8)}function Bi(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 Ji(e,t,n){let{projectId:o,alias:s}=Bi(t,n,"--from"),r=await e.load(o),i=fe(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 ap(e,t){let n=await ue(Hi(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 yt(i)}async function Tr(e,t,n){if(n){let o=fe(await e.load(t),n);if(o)return e.resolve({project:t,db:o})}return e.resolve({project:t})}async function vo(e,t,n){let o=n.apiDomain?yt(n.apiDomain):void 0;if(o&&n.userToken)return{apiDomain:o,userToken:n.userToken,note:null};try{let{alias:i,config:a}=await Tr(e,t,n.apiName),c=o??(a.apiDomain?yt(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 ap(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=Rr((await Cr(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 ${Li(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)=>` ${Li(i.sources)}`),"pass --user-token-env VAR with the token of the source you mean"].join(`
19
+ `))}async function Ln(e){return(await Fn(e.apiDomain,e.userToken,"SM_Modules_ManagerSelect",{})).data??[]}function Pr(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 Ui(e){let t;try{t=xn(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 Ki(e,t,n,o){let s=async(a,c)=>{let l=await e.load(a),u=fe(l,c);if(!u)throw new m(`no connection "${c}" in project "${a}"; see: fui db list -p ${a}`);let d=Ui(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}=Bi(t,n,"--borrow-from");return s(a,c)}let r=[];for(let a of await Vi(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=Ui(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 <alias> --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(`
20
+ `))}async function Gi(e,t){let n,o;try{let i=fe(await e.load(t),"acc");if(!i)throw Error("no acc alias");let a=await e.resolve({project:t,db:i});n=_e(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 ct(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 Er(e,t,n,o){try{let s=Hi(e.dbDir(t),"rights.json"),r={};try{r=JSON.parse(await ue(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 x(s,JSON.stringify(r,null,2))}catch{}}import{mkdir as Kp,rename as Gp,unlink as zp}from"node:fs/promises";import{dirname as Xp}from"node:path";class _r{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 Ar(e){if(e.engine==="mssql")return new ct(_e(e.config.apiDomain),e.dbToken);return new _r(e.engine,xn(e.dbToken))}async function je(e,t){let n=Ar(e);try{return await t(n)}finally{await n.close()}}import{mkdir as Tp,readdir as Pp,unlink as Ep}from"node:fs/promises";import{dirname as _p,join as Ap}from"node:path";var Xi=`
22
21
  SELECT
23
22
  o.type AS ObjectTypeCode, o.name AS ObjectName, c.name AS ColumnName, t.name AS DataType,
24
23
  c.max_length, c.precision, c.scale, c.is_nullable, c.is_identity,
@@ -42,7 +41,7 @@ WHERE o.is_ms_shipped = 0 AND o.type IN ('U','V') AND s.name NOT IN ('sys','INFO
42
41
  AND o.name NOT LIKE 'sys%' AND o.name NOT LIKE 'dt[_]%'
43
42
  AND o.name NOT LIKE 'queue[_]%' AND o.name NOT LIKE 'filestream[_]%' AND o.name NOT LIKE 'cdc.%'
44
43
  ORDER BY ObjectTypeCode, ObjectName
45
- `.trim(),Ei=`
44
+ `.trim(),Yi=`
46
45
  SELECT
47
46
  o.type AS ObjectTypeCode, o.name AS ObjectName,
48
47
  p.name AS ParameterName, pt.name AS ParameterType,
@@ -57,42 +56,42 @@ WHERE o.is_ms_shipped = 0 AND o.type IN ('P','FN','TF','IF') AND s.name NOT IN (
57
56
  AND o.name NOT IN ('fn_diagramobjects','sp_alterdiagram','sp_creatediagram','sp_dropdiagram',
58
57
  'sp_helpdiagramdefinition','sp_helpdiagrams','sp_renamediagram','sp_upgraddiagrams')
59
58
  ORDER BY ObjectTypeCode, ObjectName
60
- `.trim(),$d=new Set(["nvarchar","nchar","ntext"]);function Ai(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=$d.has(s)?t/2:t;return r>0?`${e}(${r})`:e}function jd(e){let t=[String(e.ColumnName),Ai(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 yr(e,t){if(t)return"Function";if(/^spAPIFILE_/i.test(e))return"API File";if(/^spAPI_/i.test(e))return"API";return"Procedure"}function Ni(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(jd(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=Ai(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 Tt(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 _i(e){return/^CREATE\s+(?:TABLE|VIEW|PROCEDURE|FUNCTION)\s+(\S+)/i.exec(e)?.[1]??null}var br=["Table","View","API","API File","Procedure","Function"];function kr(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?yr(t[2],t[1].toUpperCase()==="FUNCTION"):null}var Id=/^(\S+)\s+(\S+)\s+(NULL|NOT NULL)(\s+PRIMARY KEY)?(\s+IDENTITY)?(?:\s+REFERENCES\s+(\S+)\((\S+)\))?$/,Pi=/^@(\S+)\s+(\S+)$/;function Di(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 Sr(e){let t=e.match(/^CREATE (TABLE|VIEW) (\S+) \((.*)\);?$/s),n=t?Di(t[3]):null;if(t&&n.every((s)=>Id.test(s.trim())))return{name:t[2],objectType:t[1]==="VIEW"?"View":"Table",count:n.length};let o=_i(e);if(!o)throw Error(`Cannot parse table DDL: ${e.slice(0,120)}`);return{name:o,objectType:kr(e)??"Table",count:0,opaque:!0}}function fo(e){let t=e.match(/^CREATE (PROCEDURE|FUNCTION) (\S+) \((.*)\);?$/s),n=t?Di(t[3]):null;if(t&&n.every((r)=>Pi.test(r.trim())))return{name:t[2],objectType:yr(t[2],t[1]==="FUNCTION"),count:n.length,parameters:n.map((r)=>Pi.exec(r.trim())[1])};let o=_i(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)??yr(o,s),count:0,parameters:[],opaque:!0}}import{mkdir as Nd,stat as _d}from"fs/promises";import{basename as Dd,dirname as xd,join as jr,resolve as Mi,sep as Od}from"path";import{mkdir as Cd,readdir as Rd}from"fs/promises";import{join as zt,sep as Pd}from"path";var Xt="_publishState.json";function ve(e){return e.split(Pd).join("/")}function Et(e){return`sha1:${new Bun.CryptoHasher("sha1").update(e.replace(/\r\n/g,`
61
- `)).digest("hex")}`}async function go(e){try{let t=JSON.parse(await B(zt(e,Xt)));return t&&typeof t==="object"&&!Array.isArray(t)?t:{}}catch{return{}}}async function xi(e,t){await Cd(e,{recursive:!0}),await Bun.write(zt(e,Xt),JSON.stringify(t,null,2))}async function Ee(e,t){if(t.length===0)return;let n=await go(e),o=!1;for(let s of t){let r=ve(s);try{n[r]=Et(await B(zt(e,r))),o=!0}catch{if(r in n)delete n[r],o=!0}}if(o)await xi(e,n)}async function Oi(e,t){if(t.length===0)return;let n=await go(e),o=!1;for(let s of t){let r=ve(s);if(r in n)delete n[r],o=!0}if(o)await xi(e,n)}var Td=["module.json","header.html","body.html","script.js","style.css","project.json"];async function Ed(e){try{return(await Rd(zt(e,"components"),{withFileTypes:!0})).filter((n)=>n.isFile()&&n.name.toLowerCase().endsWith(".vue")).map((n)=>ve(zt("components",n.name)))}catch{return[]}}async function Nn(e){let t=await go(e),n=[];for(let o of[...Td,...await Ed(e)]){let s;try{s=await B(zt(e,o))}catch{continue}let r=t[o];if(r===void 0)n.push({relPath:o,reason:"new"});else if(r!==Et(s))n.push({relPath:o,reason:"changed"})}return n}async function Yt(e,t,n){let o=(await go(e))[ve(t)];return o!==void 0&&Et(n)===o}var Ad={"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 wo(e){if(e.startsWith("components/"))return"fui component update";return Ad[e]??"fui module publish json"}function $r(e){if(e.length===0)return null;return`unpublished local files: ${e.map((n)=>`${n.relPath} (${wo(n.relPath)})`).join(", ")}`}var Md=172800000,Fd=new Set(["_moduleInfo.json","_projectInfo.json","_modules.json","_components.json","_imports.json",Xt]);function Fi(e){return Fd.has(Dd(e))}function Ld(e,t){return e.replace(/\r\n/g,`
59
+ `.trim(),cp=new Set(["nvarchar","nchar","ntext"]);function Qi(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=cp.has(s)?t/2:t;return r>0?`${e}(${r})`:e}function lp(e){let t=[String(e.ColumnName),Qi(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 Nr(e,t){if(t)return"Function";if(/^spAPIFILE_/i.test(e))return"API File";if(/^spAPI_/i.test(e))return"API";return"Procedure"}function Zi(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(lp(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=Qi(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 ea(e){return/^CREATE\s+(?:TABLE|VIEW|PROCEDURE|FUNCTION)\s+(\S+)/i.exec(e)?.[1]??null}var Dr=["Table","View","API","API File","Procedure","Function"];function xr(e){if(/^CREATE TABLE/i.test(e))return"Table";if(/^CREATE VIEW/i.test(e))return"View";return null}function Or(e){let t=e.match(/^CREATE (PROCEDURE|FUNCTION) (\S+)/i);return t?Nr(t[2],t[1].toUpperCase()==="FUNCTION"):null}var up=/^(\S+)\s+(\S+)\s+(NULL|NOT NULL)(\s+PRIMARY KEY)?(\s+IDENTITY)?(?:\s+REFERENCES\s+(\S+)\((\S+)\))?$/,zi=/^@(\S+)\s+(\S+)$/;function ta(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 Mr(e){let t=e.match(/^CREATE (TABLE|VIEW) (\S+) \((.*)\);?$/s),n=t?ta(t[3]):null;if(t&&n.every((s)=>up.test(s.trim())))return{name:t[2],objectType:t[1]==="VIEW"?"View":"Table",count:n.length};let o=ea(e);if(!o)throw Error(`Cannot parse table DDL: ${e.slice(0,120)}`);return{name:o,objectType:xr(e)??"Table",count:0,opaque:!0}}function So(e){let t=e.match(/^CREATE (PROCEDURE|FUNCTION) (\S+) \((.*)\);?$/s),n=t?ta(t[3]):null;if(t&&n.every((r)=>zi.test(r.trim())))return{name:t[2],objectType:Nr(t[2],t[1]==="FUNCTION"),count:n.length,parameters:n.map((r)=>zi.exec(r.trim())[1])};let o=ea(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:Or(e)??Nr(o,s),count:0,parameters:[],opaque:!0}}import{mkdir as wp,stat as yp}from"node:fs/promises";import{basename as bp,dirname as kp,join as Lr,resolve as ra,sep as vp}from"node:path";import{mkdir as dp,readdir as pp}from"node:fs/promises";import{join as on,sep as mp}from"node:path";var rn="_publishState.json";function Se(e){return e.split(mp).join("/")}function Mt(e){return`sha1:${Qt("sha1",e.replace(/\r\n/g,`
60
+ `))}`}async function $o(e){try{let t=JSON.parse(await W(on(e,rn)));return t&&typeof t==="object"&&!Array.isArray(t)?t:{}}catch{return{}}}async function na(e,t){await dp(e,{recursive:!0}),await x(on(e,rn),JSON.stringify(t,null,2))}async function Ae(e,t){if(t.length===0)return;let n=await $o(e),o=!1;for(let s of t){let r=Se(s);try{n[r]=Mt(await W(on(e,r))),o=!0}catch{if(r in n)delete n[r],o=!0}}if(o)await na(e,n)}async function oa(e,t){if(t.length===0)return;let n=await $o(e),o=!1;for(let s of t){let r=Se(s);if(r in n)delete n[r],o=!0}if(o)await na(e,n)}var hp=["module.json","header.html","body.html","script.js","style.css","project.json"];async function fp(e){try{return(await pp(on(e,"components"),{withFileTypes:!0})).filter((n)=>n.isFile()&&n.name.toLowerCase().endsWith(".vue")).map((n)=>Se(on("components",n.name)))}catch{return[]}}async function Un(e){let t=await $o(e),n=[];for(let o of[...hp,...await fp(e)]){let s;try{s=await W(on(e,o))}catch{continue}let r=t[o];if(r===void 0)n.push({relPath:o,reason:"new"});else if(r!==Mt(s))n.push({relPath:o,reason:"changed"})}return n}async function sn(e,t,n){let o=(await $o(e))[Se(t)];return o!==void 0&&Mt(n)===o}var gp={"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 jo(e){if(e.startsWith("components/"))return"fui component update";return gp[e]??"fui module publish json"}function Fr(e){if(e.length===0)return null;return`unpublished local files: ${e.map((n)=>`${n.relPath} (${jo(n.relPath)})`).join(", ")}`}var Sp=172800000,$p=new Set(["_moduleInfo.json","_projectInfo.json","_modules.json","_components.json","_imports.json",rn]);function sa(e){return $p.has(bp(e))}function jp(e,t){return e.replace(/\r\n/g,`
62
61
  `)===t.replace(/\r\n/g,`
63
- `)}function Ud(e,t,n){let o=Mi(e),s=Mi(t);if(s!==o&&!s.startsWith(o+Od))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=po(),a=[];for(let c of Object.keys(n)){let l=ve(c);Ud(t,jr(t,l),l)}for(let[c,l]of Object.entries(n)){let u=ve(c),d=jr(t,u),p=await le(d);if(p!==null&&Ld(p,l))s.push(u);else if(p!==null&&!Fi(u)){let m=await ge(e,d,{stamp:i});if(m)r.push({relPath:u,historyPath:Rt(e,m)})}if(await Nd(xd(d),{recursive:!0}),await Bun.write(d,l),o.push(d),!Fi(u))a.push(u)}if(a.length>0)await Ee(t,a).catch(()=>{return});return{written:o,unchanged:s,archived:r}}function Ir(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 Hd=8;function Cr(e){if(e.length===0)return[];let t=e.slice(0,Hd).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 Bd(e,t){try{return(await _d(jr(e,t))).mtime}catch{return null}}async function At(e,t,n,o,s=Date.now()){let r=await Bd(e,o);if(!r)return null;let i=s-r.getTime();if(i<Md)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 Gd=/^[A-Za-z0-9_]+$/,Li=50;class yo extends Error{name="SchemaFormatError"}async function zd(e){try{return JSON.parse(await B(e))}catch{return null}}async function Rr(e,t,n){return zd(e.schemaFile(t,n))}async function Pr(e,t,n){let o=await Rr(e,t,n);if(!o)return null;if(!Tt(o))throw new yo(`_db/${n}/schema.json of project "${t}" is not in the current compact format`);try{return{fetchedAt:o.fetchedAt,tables:o.tables.map(Sr),procedures:o.procedures.map(fo)}}catch(s){throw new yo(`_db/${n}/schema.json of project "${t}": ${s.message}`)}}async function Ui(e){try{return(await Vd(e)).filter((t)=>t.toLowerCase().endsWith(".sql"))}catch{return null}}async function Xd(e,t){try{let n=await B(t);return await ge(e,t,{content:n,suffix:"deleted"}),await Wd(t),!0}catch{return!1}}async function Yd(e,t,n,o,s,r){let i=[],a=[],c=await Ui(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 Xd(e,Kd(e.connDir(t,n),p)))i.push(p)}else a.push(p)}return{removed:i,localOnly:a}}async function Qd(e,t,n,o){let s=await Ui(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(Gd.test(p))r.push(p);else i.push(d)}let a=new Map;for(let d=0;d<r.length;d+=Li){let p=r.slice(d,d+Li).map((w)=>`N'${w}'`).join(","),m=await t.query(`SELECT o.name AS name, ISNULL(OBJECT_DEFINITION(o.object_id), '') AS sqlText
64
- 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 bo(e,t,n,o,s=!1){let r=null;try{r=await Pr(e,n,o)}catch{}let i=new Set((r?.procedures??[]).map((w)=>w.name)),[a,c]=await Promise.all([t.query(Ti),t.query(Ei)]),l=Ni(a,c),u=e.schemaFile(n,o);await qd(Jd(u),{recursive:!0}),await Bun.write(u,JSON.stringify(l,null,2));let d=new Set(l.procedures.map((w)=>fo(w).name)),p=await Yd(e,n,o,i,d,s),m=await Qd(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 ko(e,t,n,o){try{return await Pr(e,t,n)}catch(s){if(!(s instanceof yo))throw s;return await o(),Pr(e,t,n)}}async function Hi(e,t,n,o){let s=await Rr(e,t,n);if(!s)return null;if(Tt(s))return s;await o();let r=await Rr(e,t,n);if(!Tt(r))throw Error("refetched the schema but it is still not in the compact format");return r}function Bi(e){let t={Table:0,View:0,API:0,"API File":0,Procedure:0,Function:0};for(let n of e.tables)t[Sr(n).objectType]+=1;for(let n of e.procedures)t[fo(n).objectType]+=1;return t}var Zd=/token|passw|pwd|secret|api[-_]?key|credential|authorization/i,qi=[/\b(?:PWD|Password)\s*=\s*[^;]+/i,/^Bearer\s+\S+/i,/:\/\/[^/\s:@]+:[^@\s]+@/];function Vi(e){return`<redacted ${e.length} chars>`}function Wi(e){if(qi.some((t)=>t.test(e)))return!0;if(e.length<40||!/^[A-Za-z0-9+/=]+$/.test(e))return!1;try{return qi.some((t)=>t.test(atob(e)))}catch{return!1}}function Qt(e){if(Array.isArray(e))return e.map((t)=>Qt(t));if(e&&typeof e==="object")return Object.fromEntries(Object.entries(e).map(([t,n])=>[t,typeof n==="string"&&(Zd.test(t)||Wi(n))?Vi(n):Qt(n)]));if(typeof e==="string"&&Wi(e))return Vi(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]!==`
65
- `)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 Nt(e){return e.replace(/--[^\n]*/g," ").replace(/\/\*[\s\S]*?\*\//g," ").replace(/N?'(?:[^']|'')*'/gi,"''")}function Tr(e){return Ue(e,{strings:!1})}function Er(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 _n(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 _t(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 Ar(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 Ji(e){return/\b(?:CREATE|ALTER)\s+FUNCTION\b/i.test(Ue(e))}function Ki(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 Nr(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=_n(a(t.slice(o,r))),l=_n(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 ep(e){return`N'${e.replace(/'/g,"''")}'`}function Dn(e){return`GRANT EXECUTE ON [dbo].[${e}] TO [public] AS [dbo]`}function Gi(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(${ep(e)}) AND p.permission_name = 'EXECUTE' AND p.state IN ('G','W') AND u.name = 'public'`}var tp=/\bALTER\s+TABLE\s+(?:(?:\[[^\]]*\]|\w+)\s*\.\s*)*(?:\[[^\]]*\]|\w+)\s+(?:WITH\s+(?:CHECK|NOCHECK)\s+)?(\w+)/i,np=new Set(["ADD","ALTER","DROP"]);function op(e){let t=Tr(e),n=null;for(let o of Er(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=tp.exec(s)?.[1]?.toUpperCase();if(!i||!np.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 zi(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 rp(e){let t=Tr(e);for(let{start:n,end:o}of Er(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)&&!zi(s,"DELETE"))return{severity:"block",reason:"DELETE without WHERE",statement:r};if(/\bUPDATE\b/i.test(s)&&/\bSET\b/i.test(s)&&!zi(s,"UPDATE"))return{severity:"block",reason:"UPDATE without WHERE",statement:r}}return null}function Xi(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 sp=["INSERT","UPDATE","DELETE","MERGE","TRUNCATE"];function Zt(e){let t=new Set;return Xi(Nt(e),t),Xi(Ue(e),t),sp.filter((n)=>t.has(n))}function _r(e,t,n){let o=op(e);if(o&&(o.severity==="block"||!t))return o;if(n==="nonquery"){let r=rp(e);if(r)return r}let s=Zt(e);if(s.length>0&&!t)return{severity:"confirm",reason:`writes real data (${s.join(", ")})`,statement:e.trim()};return null}function ip(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)}\u2026`:t}function Dr(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 E=e.slice(y).match(/^[A-Za-z]+/)?.[0]?.toUpperCase();if(E&&["INSERT","UPDATE","DELETE","MERGE","SELECT"].includes(E)){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=_n(p,",").map(ip).filter(Boolean);t.push({columns:w,hasStar:m,isInto:l})}return t}function Yi(e){return/RAISERROR\s*\([^)]*\)[^;]*;?\s*(\r?\n)?\s*RETURN\b/i.test(e)}function xr(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 Qi(e){let t=Dr(Nt(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]=xr(o);return{data:[n,{...n}]}}function ap(e){let t=Nt(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 Zi(e){if(e===null)return"unknown";if(Zt(e).length>0)return"unsafe-write";if(ap(e).length>0)return"opaque";return"safe-read"}var pp=/^[A-Za-z0-9_]+$/,ea=200,ta=2000;function ft(e){if(!pp.test(e))throw new h(`invalid object name "${e}": letters, digits and "_" only (it becomes _db/<alias>/<name>.sql)`);return e}function je(e,t){return{project:e.project,contextProject:e.contextProject,...t}}function it(e,t){if(e.engine!=="mssql")throw new h(`${t} supports MSSQL connections only; ${e.alias} is ${e.engine}`)}function na(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}
62
+ `)}function Ip(e,t,n){let o=ra(e),s=ra(t);if(s!==o&&!s.startsWith(o+vp))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=bo(),a=[];for(let c of Object.keys(n)){let l=Se(c);Ip(t,Lr(t,l),l)}for(let[c,l]of Object.entries(n)){let u=Se(c),d=Lr(t,u),p=await ue(d);if(p!==null&&jp(p,l))s.push(u);else if(p!==null&&!sa(u)){let h=await we(e,d,{stamp:i});if(h)r.push({relPath:u,historyPath:Dt(e,h)})}if(await wp(kp(d),{recursive:!0}),await x(d,l),o.push(d),!sa(u))a.push(u)}if(a.length>0)await Ae(t,a).catch(()=>{return});return{written:o,unchanged:s,archived:r}}function Ur(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 Cp=8;function Hr(e){if(e.length===0)return[];let t=e.slice(0,Cp).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 Rp(e,t){try{return(await yp(Lr(e,t))).mtime}catch{return null}}async function Ft(e,t,n,o,s=Date.now()){let r=await Rp(e,o);if(!r)return null;let i=s-r.getTime();if(i<Sp)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_]+$/,ia=50;class Io extends Error{name="SchemaFormatError"}async function Dp(e){try{return JSON.parse(await W(e))}catch{return null}}async function Vr(e,t,n){return Dp(e.schemaFile(t,n))}async function Wr(e,t,n){let o=await Vr(e,t,n);if(!o)return null;if(!Ot(o))throw new Io(`_db/${n}/schema.json of project "${t}" is not in the current compact format`);try{return{fetchedAt:o.fetchedAt,tables:o.tables.map(Mr),procedures:o.procedures.map(So)}}catch(s){throw new Io(`_db/${n}/schema.json of project "${t}": ${s.message}`)}}async function aa(e){try{return(await Pp(e)).filter((t)=>t.toLowerCase().endsWith(".sql"))}catch{return null}}async function xp(e,t){try{let n=await W(t);return await we(e,t,{content:n,suffix:"deleted"}),await Ep(t),!0}catch{return!1}}async function Op(e,t,n,o,s,r){let i=[],a=[],c=await aa(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 xp(e,Ap(e.connDir(t,n),p)))i.push(p)}else a.push(p)}return{removed:i,localOnly:a}}async function Mp(e,t,n,o){let s=await aa(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+=ia){let p=r.slice(d,d+ia).map((w)=>`N'${w}'`).join(","),h=await t.query(`SELECT o.name AS name, ISNULL(OBJECT_DEFINITION(o.object_id), '') AS sqlText
63
+ 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 Co(e,t,n,o,s=!1){let r=null;try{r=await Wr(e,n,o)}catch{}let i=new Set((r?.procedures??[]).map((w)=>w.name)),[a,c]=await Promise.all([t.query(Xi),t.query(Yi)]),l=Zi(a,c),u=e.schemaFile(n,o);await Tp(_p(u),{recursive:!0}),await x(u,JSON.stringify(l,null,2));let d=new Set(l.procedures.map((w)=>So(w).name)),p=await Op(e,n,o,i,d,s),h=await Mp(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 Ro(e,t,n,o){try{return await Wr(e,t,n)}catch(s){if(!(s instanceof Io))throw s;return await o(),Wr(e,t,n)}}async function ca(e,t,n,o){let s=await Vr(e,t,n);if(!s)return null;if(Ot(s))return s;await o();let r=await Vr(e,t,n);if(!Ot(r))throw Error("refetched the schema but it is still not in the compact format");return r}function la(e){let t={Table:0,View:0,API:0,"API File":0,Procedure:0,Function:0};for(let n of e.tables)t[Mr(n).objectType]+=1;for(let n of e.procedures)t[So(n).objectType]+=1;return t}var Fp=/token|passw|pwd|secret|api[-_]?key|credential|authorization/i,ua=[/\b(?:PWD|Password)\s*=\s*[^;]+/i,/^Bearer\s+\S+/i,/:\/\/[^/\s:@]+:[^@\s]+@/];function da(e){return`<redacted ${e.length} chars>`}function pa(e){if(ua.some((t)=>t.test(e)))return!0;if(e.length<40||!/^[A-Za-z0-9+/=]+$/.test(e))return!1;try{return ua.some((t)=>t.test(atob(e)))}catch{return!1}}function an(e){if(Array.isArray(e))return e.map((t)=>an(t));if(e&&typeof e==="object")return Object.fromEntries(Object.entries(e).map(([t,n])=>[t,typeof n==="string"&&(Fp.test(t)||pa(n))?da(n):an(n)]));if(typeof e==="string"&&pa(e))return da(e);return e}function Ve(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]!==`
64
+ `)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 qr(e){return Ve(e,{strings:!1})}function Br(e){let t=Ve(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 Hn(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 Ut(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 Jr(e){let t=Ve(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 ma(e){return/\b(?:CREATE|ALTER)\s+FUNCTION\b/i.test(Ve(e))}function ha(e,t){let n={sql:e,changed:!1,from:null,to:null};if(t===null)return n;let o=[...Ve(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 Kr(e){let t=Ve(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=Hn(a(t.slice(o,r))),l=Hn(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 Lp(e){return`N'${e.replace(/'/g,"''")}'`}function Vn(e){return`GRANT EXECUTE ON [dbo].[${e}] TO [public] AS [dbo]`}function fa(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(${Lp(e)}) AND p.permission_name = 'EXECUTE' AND p.state IN ('G','W') AND u.name = 'public'`}var Up=/\bALTER\s+TABLE\s+(?:(?:\[[^\]]*\]|\w+)\s*\.\s*)*(?:\[[^\]]*\]|\w+)\s+(?:WITH\s+(?:CHECK|NOCHECK)\s+)?(\w+)/i,Hp=new Set(["ADD","ALTER","DROP"]);function Vp(e){let t=qr(e),n=null;for(let o of Br(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=Up.exec(s)?.[1]?.toUpperCase();if(!i||!Hp.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 ga(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 Wp(e){let t=qr(e);for(let{start:n,end:o}of Br(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)&&!ga(s,"DELETE"))return{severity:"block",reason:"DELETE without WHERE",statement:r};if(/\bUPDATE\b/i.test(s)&&/\bSET\b/i.test(s)&&!ga(s,"UPDATE"))return{severity:"block",reason:"UPDATE without WHERE",statement:r}}return null}function wa(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 qp=["INSERT","UPDATE","DELETE","MERGE","TRUNCATE"];function cn(e){let t=new Set;return wa(Lt(e),t),wa(Ve(e),t),qp.filter((n)=>t.has(n))}function Gr(e,t,n){let o=Vp(e);if(o&&(o.severity==="block"||!t))return o;if(n==="nonquery"){let r=Wp(e);if(r)return r}let s=cn(e);if(s.length>0&&!t)return{severity:"confirm",reason:`writes real data (${s.join(", ")})`,statement:e.trim()};return null}function Bp(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 zr(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 E=e.slice(y).match(/^[A-Za-z]+/)?.[0]?.toUpperCase();if(E&&["INSERT","UPDATE","DELETE","MERGE","SELECT"].includes(E)){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=Hn(p,",").map(Bp).filter(Boolean);t.push({columns:w,hasStar:h,isInto:l})}return t}function ya(e){return/RAISERROR\s*\([^)]*\)[^;]*;?\s*(\r?\n)?\s*RETURN\b/i.test(e)}function Xr(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 ba(e){let t=zr(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]=Xr(o);return{data:[n,{...n}]}}function Jp(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 ka(e){if(e===null)return"unknown";if(cn(e).length>0)return"unsafe-write";if(Jp(e).length>0)return"opaque";return"safe-read"}var Yp=/^[A-Za-z0-9_]+$/,va=200,Sa=2000;function bt(e){if(!Yp.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 $a(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 $e(`${e.reason}
66
65
  ${n}
67
66
  ---
68
- ${t}`,e.severity==="block"?"hard":"soft")}function oa(e,t){let n=Bi(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 xn(e,t,n){Gt(n.alias);let{engine:o}=Pn(n.dbToken);if(o==="mssql"&&!n.apiDomain)throw new h("MSSQL connections need --api-domain <host>");let s=n.apiDomain?ht(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 $e(r,(w)=>w.query("SELECT 1 AS ok"));let i=await e.store.load(t),a=he(i,n.alias),c=a?i.connections[a]:void 0,l=n.userToken??c?.userToken,u={...s?{apiDomain:s}:{},...n.sqlServer?{sqlServer:n.sqlServer}:{},...n.database?{database:n.database}:{},dbToken:n.dbToken,...l?{userToken:l}:{}},d=await e.store.save(t,n.alias,u,n.makeDefault),m=[`saved ${t}/${d} ${o}${o==="mssql"&&!l?" (no userToken; tAPI calls need it: fui db add ... --user-token-env VAR)":""}`];if(o==="mssql"){let w={...r,alias:d,config:u},y=await $e(w,(v)=>bo(e.store.paths,v,t,d));m.push(...oa(d,y))}return{notes:m}}async function ra(e,t,n){let o=await e.store.resolve(je(e,{db:t}));it(o,"schema pull");let s=await $e(o,(r)=>bo(e.store.paths,r,o.projectId,o.alias,n));return{notes:[...o.note?[o.note]:[],`ok ${o.projectId}/${o.alias} schema refreshed`,...oa(o.alias,s)]}}function vo(e,t){return()=>$e(t,(n)=>bo(e.store.paths,n,t.projectId,t.alias))}async function sa(e,t){let n=await e.store.resolve(je(e,{db:t.db}));it(n,"schema");let{projectId:o,alias:s}=n,r=await Hi(e.store.paths,o,s,vo(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||kr(y)===c)&&a(y)):[],p=u?r.procedures.filter((y)=>(!c||vr(y)===c)&&a(y)):[],m=await At(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
+ ${t}`,e.severity==="block"?"hard":"soft")}function ja(e,t){let n=la(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 Wn(e,t,n){nn(n.alias);let{engine:o}=On(n.dbToken);if(o==="mssql"&&!n.apiDomain)throw new m("MSSQL connections need --api-domain <host>");let s=n.apiDomain?yt(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,(w)=>w.query("SELECT 1 AS ok"));let i=await e.store.load(t),a=fe(i,n.alias),c=a?i.connections[a]:void 0,l=n.userToken??c?.userToken,u={...s?{apiDomain:s}:{},...n.sqlServer?{sqlServer:n.sqlServer}:{},...n.database?{database:n.database}:{},dbToken:n.dbToken,...l?{userToken:l}:{}},d=await e.store.save(t,n.alias,u,n.makeDefault),h=[`saved ${t}/${d} ${o}${o==="mssql"&&!l?" (no userToken; tAPI calls need it: fui db add ... --user-token-env VAR)":""}`];if(o==="mssql"){let w={...r,alias:d,config:u},y=await je(w,(v)=>Co(e.store.paths,v,t,d));h.push(...ja(d,y))}return{notes:h}}async function Ia(e,t,n){let o=await e.store.resolve(Ie(e,{db:t}));lt(o,"schema pull");let s=await je(o,(r)=>Co(e.store.paths,r,o.projectId,o.alias,n));return{notes:[...o.note?[o.note]:[],`ok ${o.projectId}/${o.alias} schema refreshed`,...ja(o.alias,s)]}}function To(e,t){return()=>je(t,(n)=>Co(e.store.paths,n,t.projectId,t.alias))}async function Ca(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,To(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||xr(y)===c)&&a(y)):[],p=u?r.procedures.filter((y)=>(!c||Or(y)===c)&&a(y)):[],h=await Ft(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(`
69
68
  `)}
70
- `:""}}async function ia(e,t){let n=await e.store.resolveProject(je(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 <alias> -p ${n}`);let s=t.db?[(await e.store.resolve(je(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 ko(e.store.paths,n,u,vo(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 Or=(e)=>e.replace(/'/g,"''");async function aa(e,t,n){ft(t);let o=await e.store.resolve(je(e,{db:n,objectName:t}));it(o,"sp get");let s=await $e(o,(l)=>l.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(N'${Or(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 ca(e,t,n,o){ft(t);let s=await e.store.resolve(je(e,{db:o,objectName:t})),r=e.store.paths.spFile(s.projectId,s.alias,t);return await cp(dp(r),{recursive:!0}),await Bun.write(r,n),{path:r,alias:s.alias,notes:s.note?[s.note]:[]}}function mp(e){return/(CREATE\s+PROCEDURE|CREATE\s+PROC|CREATE\s+FUNCTION)/im.test(e)}async function Mr(e,t){try{let n=await e.query(Gi(t)),o=Number(n[0]?.granted??0);return Number.isFinite(o)?o>0:null}catch{return null}}async function hp(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 fp(e,t){if(!Jt()||await e.wasSkillLoaded())return;throw new Se(`[FUI_MCP_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 la(e,t,n){await fp(e.session,"sp deploy"),ft(t);let o=await e.store.resolve(je(e,{db:n,objectName:t}));it(o,"sp deploy");let{projectId:s,alias:r}=o,i=e.store.paths,a=i.spFile(s,r,t),c;try{c=await B(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=Ar(c)??t,u=_t(l);if(u){let m=Nr(c);if(m.length>0)throw new Se(`tAPI does not support parameter default values; remove "= ..." and save before deploying:
71
- `+m.map((w)=>` - ${w}`).join(`
72
- `),"hard","error")}let d=`${s}/${r}`,p=[];return await $e(o,async(m)=>{let w=null,y="";try{let b=await m.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(N'${Or(l)}')), '') AS def`);y=String(b[0]?.def??"").trim(),w=y.length>0}catch{}let v="";if(y)try{v=`rollback copy: ${await ge(i,a,{content:y})}`}catch(b){throw Error(`could not save the rollback copy (${b.message}); not deploying`)}let E=Ki(c,w);if(await m.exec(E.sql),o.note)p.push(o.note);if(p.push(`ok deployed ${t} to ${d}`,`file: ${a}`),E.changed)p.push(`ran as ${E.to} because "${l}" ${E.to==="ALTER"?"exists":"does not exist"}; file unchanged`);if(v)p.push(v);let I=w===null?mp(c):!w,g=Ji(c);if(I&&!g){let b=Dn(l);try{await m.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 Mr(m,l);if(b===!1)p.push(`[public] lacks EXECUTE on "${l}"; tAPI calls will fail. run: fui exec "${Dn(l)}" --db ${d}`);else if(b===!0&&!I)p.push(`[public] has EXECUTE on "${l}"`)}if(u)try{let b=await ur(Te(o.config.apiDomain),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 hp(e.session),...p]}}async function ua(e,t,n,o){let s=[t,`spAPI_${t}`,`spAPI_AUTH_${t}`,`spAPIFILE_${t}`,`spAPIFILE_UPLOAD_${t}`],r=await e.store.resolve(je(e,{db:n,objectName:s}));it(r,"sp help");let i=null;try{i=await ko(e.store.paths,r.projectId,r.alias,vo(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=_t(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 ur(Te(r.config.apiDomain),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(Qt(JSON.parse(d.text)));p=o>0&&m.length>o?`${m.slice(0,o)}\u2026(truncated)`:m}catch{p=o>0?d.text.slice(0,o):d.text}return{notes:u,data:`${p}
73
- `}}async function da(e,t,n){let o=_r(t,n.confirmWrite,"query");if(o)throw na(o);let s=await e.store.resolve(je(e,{db:n.db,write:!n.db&&Zt(t).length>0}));return{rows:await $e(s,(i)=>i.query(t)),connection:s}}function gp(e){if(!/\b(?:CREATE|ALTER)\s+(?:PROCEDURE|PROC)\b/i.test(Ue(e)))return null;return Ar(e)}async function pa(e,t,n){let o=_r(t,n.confirmWrite,"nonquery");if(o)throw na(o);let s=await e.store.resolve(je(e,{db:n.db,write:!n.db})),r=wr(s),i=[];try{let a=await r.exec(t),c=s.engine==="mssql"?gp(t):null;if(c){let l=`${s.projectId}/${s.alias}`,u=await Mr(r,c);if(u===!1)i.push(`[public] lacks EXECUTE on "${c}"; tAPI calls will fail. run: fui exec "${Dn(c)}" --db ${l} (fui sp save + sp deploy grants automatically)`);else if(u===!0)i.push(`[public] has EXECUTE on "${c}"`);if(_t(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 ma(e,t){let{name:n}=t,o=[n,`spAPI_${n}`,`spAPI_AUTH_${n}`,`spAPIFILE_${n}`,`spAPIFILE_UPLOAD_${n}`],s=await e.store.resolve(je(e,{db:t.db,objectName:o}));it(s,"sp verify");let{projectId:r,alias:i,config:a}=s,c=e.store.paths,l=null;try{l=await ko(c,r,i,vo(e,s))}catch{}let u=n,d=l?.procedures.find((H)=>o.some((ae)=>ae.toLowerCase()===H.name.toLowerCase()));if(d)u=d.name;let p=_t(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),E=/^spAPIFILE_/i.test(m)&&!v,I=await wp(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 At(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":E?"API File Get":"API",C=E||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}/${i}/${w?"auth/":""}${y}`,"","## params"],N=0;if(d){let H=[];for(let we of d.parameters??[]){let Ve=we.replace(/^@/,"");if(/^sys_/i.test(Ve))continue;if(/^url\d+_/i.test(Ve)){N++;continue}H.push(Ve)}if(S.push(`route params (@url*_): ${N}`,`body params: ${H.length?H.join(", "):"(none)"}`),t.params){let we=new Set(Object.keys(t.params).map((Ce)=>Ce.toLowerCase())),Ve=H.filter((Ce)=>!we.has(Ce.toLowerCase())),Ze=Object.keys(t.params).filter((Ce)=>!/^@?sys_/i.test(Ce)&&!H.some((jt)=>jt.toLowerCase()===Ce.replace(/^@/,"").toLowerCase()));if(Ve.length)S.push(` warning: missing from --params: ${Ve.join(", ")}`);if(Ze.length)S.push(` warning: not declared by the SP: ${Ze.join(", ")}`)}let ae=(t.urlParams??[]).length;if(t.urlParams&&ae!==N)S.push(` warning: the SP declares ${N} route params, --url-param gave ${ae}`)}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 H=await $e(s,(ae)=>Mr(ae,u));S.push(H===!0?`[public] has EXECUTE on "${u}"`:H===!1?`[public] LACKS EXECUTE on "${u}"; the endpoint fails with a permission error. fix: fui exec "${Dn(u)}" --db ${r}/${i}`:"warning: could not read sys.database_permissions; EXECUTE grant unknown")}let D=Nr(I.sql);if(D.length)S.push(`warning: tAPI does not support default values; sp deploy will block: ${D.join("; ")}`);S.push("","## response shape (static, best effort)");let A=Dr(Nt(I.sql)),G=A.filter((H)=>!H.isInto),_=A.length-G.length;if(_>0)S.push(`(${_} SELECT ... INTO #temp/@table not returned to the client; not counted)`);if(G.length===0)S.push("warning: no result-returning SELECT; the SP may only write (check any OUT binding in module.json)");else if(G.length===1){let H=G[0];if(S.push("1 SELECT: data is an array (one resultset)"),H.hasStar)S.push("warning: SELECT *: columns unknown from the body; check fui schema --search <table>");else if(H.columns)S.push(`columns: ${H.columns.join(", ")}`)}else S.push(`${G.length} result SELECTs: data may be array-of-arrays (data[0], data[1], ...)`),G.forEach((H,ae)=>{S.push(` data[${ae}]: ${H.hasStar?"SELECT * (columns unknown)":H.columns?H.columns.join(", "):"(unknown)"}`)});S.push(Yi(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 de=Zt(I.sql);if(de.length===0)S.push("read API: no INSERT/UPDATE+SET/DELETE/MERGE in the body");else S.push(`WRITE API: ${de.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 Ie=G[0];if(Ie&&!Ie.hasStar&&Ie.columns?.length){let H={};for(let ae of Ie.columns)H[ae]=xr(ae);S.push(JSON.stringify({apiMocks:{[y]:{data:[H,H]}}},null,2))}else S.push("(not generated: SELECT * or unknown columns; write apiMocks from fui schema --search <table>)");return{notes:S}}async function wp(e,t,n){try{let o=await B(e.store.paths.spFile(t.projectId,t.alias,n));if(o.trim())return{sql:o,source:"local"}}catch{}try{let o=await $e(t,(r)=>r.query(`SELECT ISNULL(OBJECT_DEFINITION(OBJECT_ID(N'${Or(n)}')),'') AS sqlText`)),s=String(o[0]?.sqlText??"");if(s.trim())return{sql:s,source:"live"}}catch{}return null}async function ha(e,t,n,o){ft(t);let s=await e.store.resolve(je(e,{db:o,objectName:t}));it(s,"sp delete");let{projectId:r,alias:i}=s,a=n==="Function"?`DROP FUNCTION [dbo].[${t}]`:`DROP PROCEDURE [dbo].[${t}]`;await $e(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 B(l),d=await ge(e.store.paths,l,{content:u,suffix:"deleted"});await up(l),c.push(`local file archived: ${d?Rt(e.store.paths,d):""} (removed from _db/${i}/)`)}catch{}return{notes:c}}async function fa(e,t,n,o){ft(t),ft(n);let s=await e.store.resolve(je(e,{db:o,objectName:t}));it(s,"sp rename");let{projectId:r,alias:i}=s;await $e(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 lp(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 ga(e,t){let n=await e.store.resolve(je(e,{db:t.db,write:!t.db}));it(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=Kt(r.sqlServer,r.database,t.uid,t.password);return await new st(Te(r.apiDomain),i).query("SELECT 1 AS ok"),await e.store.save(o,s,{...r,dbToken:i},!1),{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 On(e){return e.store.resolveProject({project:e.project,contextProject:e.contextProject})}async function wa(e,t){let n=await On(e),o=e.store.paths.connectionsFile(n);if(t.tokenSource){let{userToken:r,source:i}=await Ii(e.store,n,t.tokenSource),a=await e.store.resolve({project:n,db:t.db});return await e.store.save(n,a.alias,{...a.config,userToken:r},!1),{notes:[...a.note?[a.note]:[],`ok borrowed the userToken of ${i} into ${n}/${a.alias}`,`config: ${o}`]}}if(!t.userToken){let r=await dr(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=pr(r);return{notes:[`${i.length} distinct userToken(s) across ${r.length} connection(s); pick one (same apiDomain preferred):`,...i.map((a)=>`fingerprint ${$i(a.token)} sources: ${Si(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.save(n,s.alias,{...s.config,userToken:t.userToken},!1),{notes:[...s.note?[s.note]:[],`ok userToken saved for ${n}/${s.alias}`,`config: ${o}`]}}async function ya(e,t){let n=await On(e),o=await ho(e.store,n,t),s=await An(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:hr(i),notes:a,total:s.length}}async function ba(e,t){Gt(t.apiName);let n=await On(e),o=await ho(e.store,n,{...t}),s=(await An(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 Ri(e.store,n);await En(o.apiDomain,o.userToken,"SM_Modules_Update",{ModuleID:r,ModuleName:t.moduleName,APIName:t.apiName,Enable:1,UpdateConnection:0}),await En(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 An(o).catch(()=>null))?.find((l)=>String(l.APIName??"").toLowerCase()===t.apiName.toLowerCase()),c=a?hr([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 ka(e,t){let n=await On(e);Gt(t.apiName);let o=await ho(e.store,n,t),s=(await An(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 Ci(e.store,n,t.borrowFrom,o.apiDomain),c=Kt(r.SQLServer,r.DatabaseName,i.uid,i.password);try{await new st(Te(o.apiDomain),c).query("SELECT 1 AS ok")}catch(u){throw Error(`test connection to ${r.SQLServer}/${r.DatabaseName} failed; nothing saved.
69
+ `:""}}async function Ra(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 <alias> -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 Ro(e.store.paths,n,u,To(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 Yr=(e)=>e.replace(/'/g,"''");async function Ta(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'${Yr(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 Pa(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 Kp(Xp(r),{recursive:!0}),await x(r,n),{path:r,alias:s.alias,notes:s.note?[s.note]:[]}}function Qp(e){return/(CREATE\s+PROCEDURE|CREATE\s+PROC|CREATE\s+FUNCTION)/im.test(e)}async function Qr(e,t){try{let n=await e.query(fa(t)),o=Number(n[0]?.granted??0);return Number.isFinite(o)?o>0:null}catch{return null}}async function Zp(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 em(e,t){if(!en()||await e.wasSkillLoaded())return;throw new $e(`[FUI_MCP_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 Ea(e,t,n){await em(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 W(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=Jr(c)??t,u=Ut(l);if(u){let h=Kr(c);if(h.length>0)throw new $e(`tAPI does not support parameter default values; remove "= ..." and save before deploying:
70
+ `+h.map((w)=>` - ${w}`).join(`
71
+ `),"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'${Yr(l)}')), '') AS def`);y=String(b[0]?.def??"").trim(),w=y.length>0}catch{}let v="";if(y)try{v=`rollback copy: ${await we(i,a,{content:y})}`}catch(b){throw Error(`could not save the rollback copy (${b.message}); not deploying`)}let E=ha(c,w);if(await h.exec(E.sql),o.note)p.push(o.note);if(p.push(`ok deployed ${t} to ${d}`,`file: ${a}`),E.changed)p.push(`ran as ${E.to} because "${l}" ${E.to==="ALTER"?"exists":"does not exist"}; file unchanged`);if(v)p.push(v);let I=w===null?Qp(c):!w,g=ma(c);if(I&&!g){let b=Vn(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 Qr(h,l);if(b===!1)p.push(`[public] lacks EXECUTE on "${l}"; tAPI calls will fail. run: fui exec "${Vn(l)}" --db ${d}`);else if(b===!0&&!I)p.push(`[public] has EXECUTE on "${l}"`)}if(u)try{let b=await Ir(_e(o.config.apiDomain),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 Zp(e.session),...p]}}async function _a(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 Ro(e.store.paths,r.projectId,r.alias,To(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=Ut(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 Ir(_e(r.config.apiDomain),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(an(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}
72
+ `}}async function Aa(e,t,n){let o=Gr(t,n.confirmWrite,"query");if(o)throw $a(o);let s=await e.store.resolve(Ie(e,{db:n.db,write:!n.db&&cn(t).length>0}));return{rows:await je(s,(i)=>i.query(t)),connection:s}}function tm(e){if(!/\b(?:CREATE|ALTER)\s+(?:PROCEDURE|PROC)\b/i.test(Ve(e)))return null;return Jr(e)}async function Na(e,t,n){let o=Gr(t,n.confirmWrite,"nonquery");if(o)throw $a(o);let s=await e.store.resolve(Ie(e,{db:n.db,write:!n.db})),r=Ar(s),i=[];try{let a=await r.exec(t),c=s.engine==="mssql"?tm(t):null;if(c){let l=`${s.projectId}/${s.alias}`,u=await Qr(r,c);if(u===!1)i.push(`[public] lacks EXECUTE on "${c}"; tAPI calls will fail. run: fui exec "${Vn(c)}" --db ${l} (fui sp save + sp deploy grants automatically)`);else if(u===!0)i.push(`[public] has EXECUTE on "${c}"`);if(Ut(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 Da(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 Ro(c,r,i,To(e,s))}catch{}let u=n,d=l?.procedures.find((V)=>o.some((ce)=>ce.toLowerCase()===V.name.toLowerCase()));if(d)u=d.name;let p=Ut(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),E=/^spAPIFILE_/i.test(h)&&!v,I=await nm(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 Ft(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":E?"API File Get":"API",C=E||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}/${i}/${w?"auth/":""}${y}`,"","## params"],A=0;if(d){let V=[];for(let ye of d.parameters??[]){let Ke=ye.replace(/^@/,"");if(/^sys_/i.test(Ke))continue;if(/^url\d+_/i.test(Ke)){A++;continue}V.push(Ke)}if(S.push(`route params (@url*_): ${A}`,`body params: ${V.length?V.join(", "):"(none)"}`),t.params){let ye=new Set(Object.keys(t.params).map((Te)=>Te.toLowerCase())),Ke=V.filter((Te)=>!ye.has(Te.toLowerCase())),nt=Object.keys(t.params).filter((Te)=>!/^@?sys_/i.test(Te)&&!V.some((Et)=>Et.toLowerCase()===Te.replace(/^@/,"").toLowerCase()));if(Ke.length)S.push(` warning: missing from --params: ${Ke.join(", ")}`);if(nt.length)S.push(` warning: not declared by the SP: ${nt.join(", ")}`)}let ce=(t.urlParams??[]).length;if(t.urlParams&&ce!==A)S.push(` warning: the SP declares ${A} route params, --url-param gave ${ce}`)}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,(ce)=>Qr(ce,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 "${Vn(u)}" --db ${r}/${i}`:"warning: could not read sys.database_permissions; EXECUTE grant unknown")}let D=Kr(I.sql);if(D.length)S.push(`warning: tAPI does not support default values; sp deploy will block: ${D.join("; ")}`);S.push("","## response shape (static, best effort)");let _=zr(Lt(I.sql)),z=_.filter((V)=>!V.isInto),N=_.length-z.length;if(N>0)S.push(`(${N} SELECT ... INTO #temp/@table not returned to the client; not counted)`);if(z.length===0)S.push("warning: no result-returning SELECT; the SP may only write (check any OUT binding in module.json)");else if(z.length===1){let V=z[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(`${z.length} result SELECTs: data may be array-of-arrays (data[0], data[1], ...)`),z.forEach((V,ce)=>{S.push(` data[${ce}]: ${V.hasStar?"SELECT * (columns unknown)":V.columns?V.columns.join(", "):"(unknown)"}`)});S.push(ya(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 pe=cn(I.sql);if(pe.length===0)S.push("read API: no INSERT/UPDATE+SET/DELETE/MERGE in the body");else S.push(`WRITE API: ${pe.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 Re=z[0];if(Re&&!Re.hasStar&&Re.columns?.length){let V={};for(let ce of Re.columns)V[ce]=Xr(ce);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 nm(e,t,n){try{let o=await W(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'${Yr(n)}')),'') AS sqlText`)),s=String(o[0]?.sqlText??"");if(s.trim())return{sql:s,source:"live"}}catch{}return null}async function xa(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 W(l),d=await we(e.store.paths,l,{content:u,suffix:"deleted"});await zp(l),c.push(`local file archived: ${d?Dt(e.store.paths,d):""} (removed from _db/${i}/)`)}catch{}return{notes:c}}async function Oa(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 Gp(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 Ma(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=tn(r.sqlServer,r.database,t.uid,t.password);return await new ct(_e(r.apiDomain),i).query("SELECT 1 AS ok"),await e.store.save(o,s,{...r,dbToken:i},!1),{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 qn(e){return e.store.resolveProject({project:e.project,contextProject:e.contextProject})}async function Fa(e,t){let n=await qn(e),o=e.store.paths.connectionsFile(n);if(t.tokenSource){let{userToken:r,source:i}=await Ji(e.store,n,t.tokenSource),a=await e.store.resolve({project:n,db:t.db});return await e.store.save(n,a.alias,{...a.config,userToken:r},!1),{notes:[...a.note?[a.note]:[],`ok borrowed the userToken of ${i} into ${n}/${a.alias}`,`config: ${o}`]}}if(!t.userToken){let r=await Cr(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=Rr(r);return{notes:[`${i.length} distinct userToken(s) across ${r.length} connection(s); pick one (same apiDomain preferred):`,...i.map((a)=>`fingerprint ${qi(a.token)} sources: ${Wi(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.save(n,s.alias,{...s.config,userToken:t.userToken},!1),{notes:[...s.note?[s.note]:[],`ok userToken saved for ${n}/${s.alias}`,`config: ${o}`]}}async function La(e,t){let n=await qn(e),o=await vo(e.store,n,t),s=await Ln(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:Pr(i),notes:a,total:s.length}}async function Ua(e,t){nn(t.apiName);let n=await qn(e),o=await vo(e.store,n,{...t}),s=(await Ln(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 Gi(e.store,n);await Fn(o.apiDomain,o.userToken,"SM_Modules_Update",{ModuleID:r,ModuleName:t.moduleName,APIName:t.apiName,Enable:1,UpdateConnection:0}),await Fn(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 Ln(o).catch(()=>null))?.find((l)=>String(l.APIName??"").toLowerCase()===t.apiName.toLowerCase()),c=a?Pr([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 Ha(e,t){let n=await qn(e);nn(t.apiName);let o=await vo(e.store,n,t),s=(await Ln(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 Ki(e.store,n,t.borrowFrom,o.apiDomain),c=tn(r.SQLServer,r.DatabaseName,i.uid,i.password);try{await new ct(_e(o.apiDomain),c).query("SELECT 1 AS ok")}catch(u){throw Error(`test connection to ${r.SQLServer}/${r.DatabaseName} failed; nothing saved.
74
73
  ${u.message}
75
- 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 xn(e,n,{alias:t.apiName,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 va(e,t){let n=await On(e),o=await mr(e.store,n,t),{apiDomain:s,userToken:r}=o.config;return{projectId:n,call:(i,a)=>En(s,r,i,a),target:{ModuleID:0,APIName:t??o.alias}}}async function Sa(e,t,n){let{projectId:o,call:s,target:r}=await va(e,n),a=(await s(t==="system"?"SM_SystemRight_Select":"SM_FunctionRight_Select",r)).data??[];return await fr(e.store.paths,o,r.APIName,t==="system"?{system:a}:{functions:a}),{apiName:r.APIName,rows:a}}async function $a(e,t,n){let{projectId:o,call:s,target:r}=await va(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 fr(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 Lr={"api-domain":{type:"string"},"user-token":{type:"string"},"user-token-env":{type:"string"}};async function Ur(e){return{apiDomain:f(e.values,"api-domain"),userToken:await Pe(f(e.values,"user-token"),f(e.values,"user-token-env"),"--user-token")}}function Mn(e){k(`${e.join(`
74
+ 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 Wn(e,n,{alias:t.apiName,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 Va(e,t){let n=await qn(e),o=await Tr(e.store,n,t),{apiDomain:s,userToken:r}=o.config;return{projectId:n,call:(i,a)=>Fn(s,r,i,a),target:{ModuleID:0,APIName:t??o.alias}}}async function Wa(e,t,n){let{projectId:o,call:s,target:r}=await Va(e,n),a=(await s(t==="system"?"SM_SystemRight_Select":"SM_FunctionRight_Select",r)).data??[];return await Er(e.store.paths,o,r.APIName,t==="system"?{system:a}:{functions:a}),{apiName:r.APIName,rows:a}}async function qa(e,t,n){let{projectId:o,call:s,target:r}=await Va(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 Er(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 es={"api-domain":{type:"string"},"user-token":{type:"string"},"user-token-env":{type:"string"}};async function ts(e){return{apiDomain:f(e.values,"api-domain"),userToken:await Ee(f(e.values,"user-token"),f(e.values,"user-token-env"),"--user-token")}}function Bn(e){k(`${e.join(`
76
75
  `)}
77
- `)}var yp={name:"db user-token",mcpName:"db_user_token_set",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 Pe(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 wa(await z(e),{db:f(e.values,"db"),userToken:t,tokenSource:n});Mn(o.notes)}},bp={name:"db token-rebuild",mcpName:"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 Pe(f(e.values,"pwd"),f(e.values,"pwd-env"),"--pwd");if(!n)throw new h("missing --pwd - or --pwd-env VAR");Mn((await ga(await z(e),{db:f(e.values,"db"),uid:t,password:n})).notes)}},kp={name:"db alias list",mcpName:"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"},...Lr},async run(e){let t=await ya(await z(e),{...await Ur(e),search:f(e.values,"search")});for(let n of t.notes)T(`# ${n}`);if(t.rows.length>0)k(X(t.rows,e.render))}},vp={name:"db alias new",mcpName:"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"},...Lr},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 Pe(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 ba(await z(e),{...await Ur(e),apiName:n,moduleName:o,sqlServer:s,database:r,username:i,password:a,moduleId:l});Mn(u.notes)}},Sp={name:"db add-by-name",mcpName:"db_connect_by_name",usage:"fui db add-by-name <apiName> [-p P] [--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:{"borrow-from":{type:"string"},default:{type:"boolean"},...Lr},async run(e){let t=e.positionals[0];if(!t)throw new h("missing <apiName>; see: fui db alias list");let n=await ka(await z(e),{...await Ur(e),apiName:t,borrowFrom:f(e.values,"borrow-from"),makeDefault:e.values.default===!0});Mn(n.notes)}};function Ca(e){return e.map((t)=>({sysRight:String(t.SysRight??""),note:String(t.Note??"")}))}function Ra(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 Pa={"api-name":{type:"string"}};function Fr(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 ja(e,t,n){if(e!==void 0&&e.length>t)throw new h(`${n} is at most ${t} characters`);return e}function Ia(e){return{name:`right ${e} list`,mcpName:`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:Pa,async run(t){let{apiName:n,rows:o}=await Sa(await z(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(X(e==="system"?Ca(o):Ra(o),t.render))}}}function en(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}`,mcpName:`right_${e}_${t}`,usage:n,summary:o[e][t],options:{...Pa,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:Fr(i[0],"<level>",(l)=>l>=0&&l<=9,"an integer 0-9"),note:t==="delete"?void 0:ja(f(r,"note"),50,"--note")};else{let l=f(r,"code");a={kind:e,mode:t,functionId:t==="new"?0:Fr(i[0],"<functionId>",(u)=>u>0,"a positive integer"),functionCode:t==="delete"&&l===void 0?0:Fr(l,"--code",()=>!0,"an integer"),functionName:t==="delete"?void 0:ja(f(r,"name"),100,"--name")}}let c=await $a(await z(s),a,f(r,"api-name"));Mn(c.notes),k(X(e==="system"?Ca(c.rows):Ra(c.rows),s.render))}}}var Ta=[yp,bp,kp,vp,Sp,Ia("system"),en("system","new"),en("system","update"),en("system","delete"),Ia("function"),en("function","new"),en("function","update"),en("function","delete")];import{mkdir as Tp}from"fs/promises";import{realpathSync as jp}from"fs";import{join as Ea,sep as tn}from"path";var ue="1.0.5";var Fn="@fui-org/fui-cli",Ip=3600000,Cp=5000;function Aa(e=Bun.env){return Ea(tt(e),"..","update.json")}async function Ln(e=Bun.env){try{let t=await Bun.file(Aa(e)).json();if(t&&typeof t==="object"&&!Array.isArray(t))return t}catch{}return{}}async function at(e,t=Bun.env){await Bun.write(Aa(t),`${JSON.stringify(e,null,2)}
78
- `)}function Un(e=Bun.main){let t;try{t=jp(e)}catch{return null}let n=`${tn}install${tn}global${tn}node_modules${tn}@fui-org${tn}fui-cli${tn}`,o=t.indexOf(n);return o<0?null:t.slice(0,o+n.length-1)}async function Hr(e=Bun.env){let t=e.FUI_AUTO_UPDATE?.trim().toLowerCase();if(t)return!["0","off","false","no"].includes(t);try{return(await mt(e))["auto-update"]!=="off"}catch{return!0}}async function Br(e=Bun.env){let t=(e.FUI_UPDATE_REGISTRY||"https://registry.npmjs.org").replace(/\/+$/,""),n=await fetch(`${t}/${Fn.replace("/","%2F")}/latest`,{signal:AbortSignal.timeout(Cp),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 qr(e,t=ue){return Bun.semver.order(e,t)===1}async function Rp(e){let t=await Bun.$`${process.execPath} add -g ${`${Fn}@${e}`}`.quiet().nothrow();return{ok:t.exitCode===0,output:`${t.stdout}${t.stderr}`.trim()}}async function Vr(e=Bun.env){let t=await Ln(e),n=new Date().toISOString(),o;try{o=await Br(e)}catch(r){throw await at({...t,checkedAt:n,error:r.message},e),r}if(!qr(o))return await at({...t,checkedAt:n,latest:o,error:void 0},e),{latest:o,action:"current"};if(!Un())return await at({...t,checkedAt:n,latest:o,error:void 0},e),{latest:o,action:"manual",detail:`bun add -g ${Fn}@${o}`};let s=await Rp(o);if(!s.ok)return await at({...t,checkedAt:n,latest:o,error:s.output.slice(-500)},e),{latest:o,action:"failed",detail:s.output};return await at({checkedAt:n,latest:o,updatedFrom:ue,updatedTo:o,noticeShown:!1,skillsVersion:t.skillsVersion,skillsAgents:t.skillsAgents},e),await Pp(e),{latest:o,action:"installed"}}async function Pp(e){let t=Un();if(!t)return;await Bun.$`${process.execPath} ${Ea(t,"dist","fui.js")} skill install`.env({...e,FUI_AUTO_SKILLS:"off",FUI_AUTO_UPDATE:"off"}).quiet().nothrow()}async function Na(e=Bun.env){if(!Un()||!await Hr(e))return null;let t=await Ln(e),n=null;if(t.updatedTo===ue&&t.noticeShown===!1)n=`# fui updated ${t.updatedFrom} -> ${ue}`,t.noticeShown=!0,await at(t,e);let o=t.checkedAt?Date.parse(t.checkedAt):0;if(Date.now()-o<Ip)return n;return await at({...t,checkedAt:new Date().toISOString()},e),Bun.spawn([process.execPath,Bun.main,"update","--background"],{stdio:["ignore","ignore","ignore"],env:{...e,FUI_AUTO_UPDATE:"off"}}).unref(),n}function _a(e){let t=e[0];if(!t)throw new h(`missing <key>; keys: ${io.join(", ")}`);if(!io.includes(t))throw new h(`unknown key "${t}"; keys: ${io.join(", ")}`);return t}async function Ep(){let e=await Hr(),t=Bun.env.FUI_AUTO_UPDATE?.trim(),n=(await mt())["auto-update"],o=t?"env":n?"config":"default",s=t?"FUI_AUTO_UPDATE":n?tt():"built-in (global bun installs only)";return{key:"auto-update",value:e?"on":"off",source:o,from:s}}var Ap={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 ao();T(`# config file: ${tt()}`),k(X([{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 Ep()],t))}},Np={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=_a(e),n=e[1];if(!n)throw new h(`missing <value> for ${t}`);let o;if(t==="workdir")o=Wt(n),await Tp(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 Pe(n,void 0,"api-token");else o=n.trim().replace(/\/+$/,"");let s=await mt();s[t]=o;let r=await Zo(s),i={workdir:"FUI_MCP_WORKDIR","api-base":"FUI_API_BASE","api-token":"FUI_API_TOKEN","auto-update":"FUI_AUTO_UPDATE"}[t];if(Bun.env[i])T(`# note: ${i} is set in this shell and overrides the config file`);k(`set ${t}${t==="api-token"?"":`=${o}`} in ${r}
79
- `)}},_p={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=_a(e),n=await mt();delete n[t];let o=await Zo(n);k(`unset ${t} in ${o}
80
- `)}},Da=[Ap,Np,_p];var Dp={name:"db list",mcpName:"db_config_read",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?[cr(o)]:[...new Set([...await e.listProjects(),...await e.listLegacyProjects()])].sort(),r=[];for(let i of s){let a=await e.load(i),c=await e.loadLegacy(i),l=c?.apiName??"legacy";if(c&&!he(a,l))r.push({db:`${i}/${l}`,engine:"legacy",default:"",apiDomain:c.apiDomain??"",database:c.database??"",userToken:c.userToken?"yes":"no"}),T(`# ${i}: _db/config.json is the pre-1.4 format and unused; run: fui db import-legacy -p ${i}`);for(let[u,d]of Object.entries(a.connections)){let p="invalid",m=d.database??"";try{if(d.dbToken){let w=uo(d.dbToken);p=w.engine,m||=w.database}}catch{}r.push({db:`${i}/${u}`,engine:p,default:a.default===u?"*":"",apiDomain:d.apiDomain??"",database:m,userToken:d.userToken?"yes":"no"})}}if(r.length===0){T("no connections; add one: fui db add <alias> -p <project> --token -");return}k(X(r,n))}},xp={name:"db add",mcpName:"db_connect",usage:`fui db add <alias> -p <project> (--token - | --token-env VAR | --conn - | --conn-env VAR)
81
- [--api-domain host] [--sql-server host] [--database name] [--user-token-env VAR] [--default] [--no-test]`,summary:"Add or replace a connection: test, save, and (MSSQL) fetch the schema",options:{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 <alias>");let r=(await o.resolveProject(f(t,"project"))).projectId,i=await Pe(f(t,"token"),f(t,"token-env"),"--token"),a=await Pe(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 xn(await z(e),r,{alias:s,dbToken:i??ir(a),apiDomain:f(t,"api-domain"),sqlServer:f(t,"sql-server"),database:f(t,"database"),userToken:await Pe(void 0,f(t,"user-token-env"),"--user-token"),makeDefault:t.default===!0,test:t["no-test"]!==!0});k(`${c.notes.join(`
76
+ `)}var om={name:"db user-token",mcpName:"db_user_token_set",usage:"fui db user-token [--db ref] (--from <project/alias> | --token - | --token-env VAR) no source: list fingerprints",summary:"Save a userToken (Bearer for spAPI_* calls) into one connection; never prints tokens",options:{from:{type:"string"},token:{type:"string"},"token-env":{type:"string"}},async run(e){let t=await Ee(f(e.values,"token"),f(e.values,"token-env"),"--token"),n=f(e.values,"from");if(n&&t)throw new m("pass --from or --token/--token-env, not both");let o=await Fa(await X(e),{db:f(e.values,"db"),userToken:t,tokenSource:n});Bn(o.notes)}},rm={name:"db token-rebuild",mcpName:"db_token_rebuild",usage:"fui db token-rebuild [--db ref] --uid <login> (--pwd - | --pwd-env VAR)",summary:"Rebuild a connection's dbToken after a UID/PWD change (sqlServer/database from the saved connection), test, save",options:{uid:{type:"string"},pwd:{type:"string"},"pwd-env":{type:"string"}},async run(e){let t=f(e.values,"uid");if(!t)throw new m("missing --uid");let n=await Ee(f(e.values,"pwd"),f(e.values,"pwd-env"),"--pwd");if(!n)throw new m("missing --pwd - or --pwd-env VAR");Bn((await Ma(await X(e),{db:f(e.values,"db"),uid:t,password:n})).notes)}},sm={name:"db alias list",mcpName:"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"},...es},async run(e){let t=await La(await X(e),{...await ts(e),search:f(e.values,"search")});for(let n of t.notes)T(`# ${n}`);if(t.rows.length>0)k(Y(t.rows,e.render))}},im={name:"db alias new",mcpName:"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"},...es},async run(e){let{values:t}=e,n=e.positionals[0],o=f(t,"module-name"),s=f(t,"sql-server"),r=f(t,"database"),i=f(t,"username");if(!n||!o||!s||!r||!i)throw new m("need <apiName>, --module-name, --sql-server, --database and --username");let a=await Ee(f(t,"password"),f(t,"password-env"),"--password");if(!a)throw new m("missing --password - or --password-env VAR");let c=f(t,"module-id"),l=c===void 0?void 0:Number(c);if(l!==void 0&&!Number.isInteger(l))throw new m("--module-id must be an integer");let u=await Ua(await X(e),{...await ts(e),apiName:n,moduleName:o,sqlServer:s,database:r,username:i,password:a,moduleId:l});Bn(u.notes)}},am={name:"db add-by-name",mcpName:"db_connect_by_name",usage:"fui db add-by-name <apiName> [-p P] [--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:{"borrow-from":{type:"string"},default:{type:"boolean"},...es},async run(e){let t=e.positionals[0];if(!t)throw new m("missing <apiName>; see: fui db alias list");let n=await Ha(await X(e),{...await ts(e),apiName:t,borrowFrom:f(e.values,"borrow-from"),makeDefault:e.values.default===!0});Bn(n.notes)}};function Ka(e){return e.map((t)=>({sysRight:String(t.SysRight??""),note:String(t.Note??"")}))}function Ga(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 za={"api-name":{type:"string"}};function Zr(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 Ba(e,t,n){if(e!==void 0&&e.length>t)throw new m(`${n} is at most ${t} characters`);return e}function Ja(e){return{name:`right ${e} list`,mcpName:`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:za,async run(t){let{apiName:n,rows:o}=await Wa(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(Y(e==="system"?Ka(o):Ga(o),t.render))}}}function ln(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}`,mcpName:`right_${e}_${t}`,usage:n,summary:o[e][t],options:{...za,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:Zr(i[0],"<level>",(l)=>l>=0&&l<=9,"an integer 0-9"),note:t==="delete"?void 0:Ba(f(r,"note"),50,"--note")};else{let l=f(r,"code");a={kind:e,mode:t,functionId:t==="new"?0:Zr(i[0],"<functionId>",(u)=>u>0,"a positive integer"),functionCode:t==="delete"&&l===void 0?0:Zr(l,"--code",()=>!0,"an integer"),functionName:t==="delete"?void 0:Ba(f(r,"name"),100,"--name")}}let c=await qa(await X(s),a,f(r,"api-name"));Bn(c.notes),k(Y(e==="system"?Ka(c.rows):Ga(c.rows),s.render))}}}var Xa=[om,rm,sm,im,am,Ja("system"),ln("system","new"),ln("system","update"),ln("system","delete"),Ja("function"),ln("function","new"),ln("function","update"),ln("function","delete")];import{mkdir as ym}from"node:fs/promises";import{execFile as lm,spawn as um}from"node:child_process";import{realpathSync as dm}from"node:fs";import{join as Ya,sep as kt}from"node:path";var de="1.2.0";var Po="@fui-org/fui-cli",pm=3600000,mm=5000;function Qa(e=process.env){return Ya(rt(e),"..","update.json")}async function Jn(e=process.env){try{let t=await At(Qa(e));if(t&&typeof t==="object"&&!Array.isArray(t))return t}catch{}return{}}async function ut(e,t=process.env){await x(Qa(t),`${JSON.stringify(e,null,2)}
77
+ `)}var hm=`${kt}node_modules${kt}@fui-org${kt}fui-cli${kt}`,fm=[[`${kt}install${kt}global`,"bun",!1],[`${kt}lib`,"npm",!1],[`${kt}npm`,"npm",!0]];function _o(e=gt()){let t;try{t=dm(e)}catch{return null}for(let[n,o,s]of fm){let r=`${n}${hm}`,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 Ao(e=gt()){return _o(e)?.dir??null}function Eo(e,t){return e==="bun"?`bun add -g ${Po}@${t}`:`npm install -g ${Po}@${t}`}function ns(e,t,n=process.env){return new Promise((o)=>{lm(e,t,{env:n,shell:process.platform==="win32",maxBuffer:16777216,windowsHide:!0},(s,r,i)=>o({ok:!s,output:`${r}${i}`.trim()}))})}async function os(e=process.env){let t=e.FUI_AUTO_UPDATE?.trim().toLowerCase();if(t)return!["0","off","false","no"].includes(t);try{return(await wt(e))["auto-update"]!=="off"}catch{return!0}}async function rs(e=process.env){let t=(e.FUI_UPDATE_REGISTRY||"https://registry.npmjs.org").replace(/\/+$/,""),n=await fetch(`${t}/${Po.replace("/","%2F")}/latest`,{signal:AbortSignal.timeout(mm),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 ss(e,t=de){return hi(e,t)===1}async function gm(e,t){let n=`${Po}@${t}`;if(e.manager==="npm")return ns("npm",["install","-g","--prefix",e.prefix,n]);return ns("bun",["add","-g",n],{...process.env,BUN_INSTALL:e.prefix})}async function is(e=process.env){let t=await Jn(e),n=new Date().toISOString(),o;try{o=await rs(e)}catch(i){throw await ut({...t,checkedAt:n,error:i.message},e),i}if(!ss(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:`${Eo("bun",o)} (or ${Eo("npm",o)})`};let r=await gm(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:de,updatedTo:o,noticeShown:!1,skillsVersion:t.skillsVersion,skillsAgents:t.skillsAgents},e),await wm(e),{latest:o,action:"installed"}}async function wm(e){let t=Ao();if(!t)return;await ns(process.execPath,[Ya(t,"dist","fui.js"),"skill","install"],{...e,FUI_AUTO_SKILLS:"off",FUI_AUTO_UPDATE:"off"})}async function Za(e=process.env){if(!Ao()||!await os(e))return null;let t=await Jn(e),n=null;if(t.updatedTo===de&&t.noticeShown===!1)n=`# fui updated ${t.updatedFrom} -> ${de}`,t.noticeShown=!0,await ut(t,e);let o=t.checkedAt?Date.parse(t.checkedAt):0;if(Date.now()-o<pm)return n;return await ut({...t,checkedAt:new Date().toISOString()},e),um(process.execPath,[gt(),"update","--background"],{stdio:"ignore",env:{...e,FUI_AUTO_UPDATE:"off"},detached:!0,windowsHide:!0}).unref(),n}function ec(e){let t=e[0];if(!t)throw new m(`missing <key>; keys: ${ho.join(", ")}`);if(!ho.includes(t))throw new m(`unknown key "${t}"; keys: ${ho.join(", ")}`);return t}async function bm(){let e=await os(),t=process.env.FUI_AUTO_UPDATE?.trim(),n=(await wt())["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 km={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 fo();T(`# config file: ${rt()}`),k(Y([{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 bm()],t))}},vm={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=ec(e),n=e[1];if(!n)throw new m(`missing <value> for ${t}`);let o;if(t==="workdir")o=Zt(n),await ym(o,{recursive:!0});else if(t==="auto-update"){if(n!=="on"&&n!=="off")throw new m("auto-update takes on or off");o=n}else if(t==="api-token")o=await Ee(n,void 0,"api-token");else o=n.trim().replace(/\/+$/,"");let s=await wt();s[t]=o;let r=await hr(s),i={workdir:process.env.FUI_MCP_WORKDIR&&!process.env.FUI_WORKDIR?"FUI_MCP_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}
78
+ `)}},Sm={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=ec(e),n=await wt();delete n[t];let o=await hr(n);k(`unset ${t} in ${o}
79
+ `)}},tc=[km,vm,Sm];var $m={name:"db list",mcpName:"db_config_read",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?[$r(o)]:[...new Set([...await e.listProjects(),...await e.listLegacyProjects()])].sort(),r=[];for(let i of s){let a=await e.load(i),c=await e.loadLegacy(i),l=c?.apiName??"legacy";if(c&&!fe(a,l))r.push({db:`${i}/${l}`,engine:"legacy",default:"",apiDomain:c.apiDomain??"",database:c.database??"",userToken:c.userToken?"yes":"no"}),T(`# ${i}: _db/config.json is the pre-1.4 format and unused; run: fui db import-legacy -p ${i}`);for(let[u,d]of Object.entries(a.connections)){let p="invalid",h=d.database??"";try{if(d.dbToken){let w=yo(d.dbToken);p=w.engine,h||=w.database}}catch{}r.push({db:`${i}/${u}`,engine:p,default:a.default===u?"*":"",apiDomain:d.apiDomain??"",database:h,userToken:d.userToken?"yes":"no"})}}if(r.length===0){T("no connections; add one: fui db add <alias> -p <project> --token -");return}k(Y(r,n))}},jm={name:"db add",mcpName:"db_connect",usage:`fui db add <alias> -p <project> (--token - | --token-env VAR | --conn - | --conn-env VAR)
80
+ [--api-domain host] [--sql-server host] [--database name] [--user-token-env VAR] [--default] [--no-test]`,summary:"Add or replace a connection: test, save, and (MSSQL) fetch the schema",options:{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 <alias>");let r=(await o.resolveProject(f(t,"project"))).projectId,i=await Ee(f(t,"token"),f(t,"token-env"),"--token"),a=await Ee(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 Wn(await X(e),r,{alias:s,dbToken:i??vr(a),apiDomain:f(t,"api-domain"),sqlServer:f(t,"sql-server"),database:f(t,"database"),userToken:await Ee(void 0,f(t,"user-token-env"),"--user-token"),makeDefault:t.default===!0,test:t["no-test"]!==!0});k(`${c.notes.join(`
82
81
  `)}
83
- `)}},Op={name:"db default",usage:"fui db default <project>/<alias>",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>/<alias>");let n=await z(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
- `)}},Mp={name:"db test",usage:"fui db test [--db ref]",summary:"Run SELECT 1 and report latency",async run(e){let t=await z(e),n=await e.store.resolve({db:f(e.values,"db"),project:t.project,contextProject:t.contextProject}),o=Bun.nanoseconds();await $e(n,(r)=>r.query("SELECT 1 AS ok"));let s=Math.round((Bun.nanoseconds()-o)/1e6);k(`ok ${n.projectId}/${n.alias} ${n.engine} ${s}ms
85
- `)}},Fp={name:"db import-legacy",usage:"fui db import-legacy -p <project> [--alias name] [--default] [--no-test]",summary:"Import a pre-1.4 _db/config.json as a named connection (same flow as db add; secrets never printed)",options:{alias:{type:"string"},default:{type:"boolean"},"no-test":{type:"boolean"}},async run(e){let{values:t,targets:n,store:o}=e,s=(await n.resolveProject(f(t,"project"))).projectId,r=await o.loadLegacy(s);if(!r)throw new h(`project "${s}" has no legacy _db/config.json with a dbToken`);let i=f(t,"alias")??r.apiName;if(!i)throw new h("legacy config has no apiName; pass --alias <name> (the tAPI alias in module.json API paths)");let a=await xn(await z(e),s,{alias:i,dbToken:r.dbToken,apiDomain:r.apiDomain,sqlServer:r.sqlServer,database:r.database,userToken:r.userToken,makeDefault:t.default===!0,test:t["no-test"]!==!0});k(`${a.notes.join(`
82
+ `)}},Im={name:"db default",usage:"fui db default <project>/<alias>",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>/<alias>");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}
83
+ `)}},Cm={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
84
+ `)}},Rm={name:"db import-legacy",usage:"fui db import-legacy -p <project> [--alias name] [--default] [--no-test]",summary:"Import a pre-1.4 _db/config.json as a named connection (same flow as db add; secrets never printed)",options:{alias:{type:"string"},default:{type:"boolean"},"no-test":{type:"boolean"}},async run(e){let{values:t,targets:n,store:o}=e,s=(await n.resolveProject(f(t,"project"))).projectId,r=await o.loadLegacy(s);if(!r)throw new m(`project "${s}" has no legacy _db/config.json with a dbToken`);let i=f(t,"alias")??r.apiName;if(!i)throw new m("legacy config has no apiName; pass --alias <name> (the tAPI alias in module.json API paths)");let a=await Wn(await X(e),s,{alias:i,dbToken:r.dbToken,apiDomain:r.apiDomain,sqlServer:r.sqlServer,database:r.database,userToken:r.userToken,makeDefault:t.default===!0,test:t["no-test"]!==!0});k(`${a.notes.join(`
86
85
  `)}
87
86
  legacy _db/config.json left unchanged
88
- `)}},xa=[Dp,xp,Fp,Op,Mp];import{readdir as Jp}from"fs/promises";import{join as Jr}from"path";import{readdir as Lp}from"fs/promises";import{basename as Up,dirname as Hp,extname as Bp,isAbsolute as qp,join as $o,relative as So,resolve as Vp,sep as Wp}from"path";function jo(e,t,n=process.cwd()){let o=Vp(n,t),s=So(e.root,o);if(!s||s.startsWith("..")||qp(s))return null;let r=s.split(Wp),i=r[0];if(i.startsWith("_")||r.length<2)return null;if(r[1]==="modules"&&r.length>=4){let c=r[2],l=$o(e.root,i,"modules",c);return{abs:o,baseDir:l,rel:ve(So(l,o)),projectId:i,moduleId:c}}let a=$o(e.root,i);return{abs:o,baseDir:a,rel:ve(So(a,o)),projectId:i}}var Oa=(e)=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");async function Wr(e,t){let n=So(e.root,t),o=Bp(n),s=Up(n,o),r=$o(e.historyRoot(),Hp(n)),i=new RegExp(`^${Oa(s)}_(\\d{8}_\\d{6})((?:_[A-Za-z0-9-]+)*)${Oa(o)}$`),a;try{a=await Lp(r)}catch{return[]}return a.map((c)=>{let l=i.exec(c);return l?{stamp:l[1],suffix:l[2].replace(/^_/,""),path:$o(r,c)}:null}).filter((c)=>c!==null).sort((c,l)=>l.stamp.localeCompare(c.stamp)||l.path.localeCompare(c.path))}function Kr(e,t){let n=jo(e.paths,t);if(!n)throw new h(`${t} is not inside a project of workspace ${e.paths.root}`);return n}var Kp={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(X(a,i))}};async function Gp(e){try{return(await Jp(Jr(e,"modules"),{withFileTypes:!0})).filter((n)=>n.isDirectory()).map((n)=>Jr(e,"modules",n.name))}catch{return[]}}var zp={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 Gp(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 Nn(a))i.push({dir:a,path:c.relPath,reason:c.reason,push:wo(c.relPath)});T(`# ${i.length} unpublished file(s)`),k(X(i,s))}},Xp={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=Kr(e,t),o=await Wr(e.paths,n.abs);T(`# ${o.length} version(s) of ${n.rel}`),k(X(o.map((s)=>({...s,path:Rt(e.paths,s.path)})),e.render))}},Yp={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=Kr(e,t),o=await Wr(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 le(n.abs),a=i===null?null:await ge(e.paths,n.abs,{content:i});if(await Bun.write(n.abs,await B(r.path)),a)T(`# previous content archived: ${Rt(e.paths,a)}`);k(`restored ${n.rel} from ${r.stamp}; push it with: ${wo(n.rel)}
89
- `)}},Qp={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)=>Kr(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 Oi(a,l)}let i=[];for(let[a,c]of r){let l={};try{l=JSON.parse(await B(Jr(a,Xt)))}catch{}for(let u of c){let d=await le(u.abs),p=l[u.rel]??"",m=d===null?"missing":!p?"new":p===Et(d)?"published":"changed";i.push({path:u.rel,state:m,ledger:p,dir:a})}}k(X(i,o))}},Ma=[Kp,zp,Xp,Yp,Qp];import{rename as Sm}from"fs/promises";import{basename as $m,dirname as as,join as He}from"path";function nn(e){return e==="text/css"||e==="text/javascript"}function Zp(e){return(e||"").trim().replace(/[<>:"/\\|?*\x00-\x1F]/g,"-").replace(/\s+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"import-file"}function Hn(e,t,n){let o=n.startsWith(".")?n:`.${n}`;return`${Zp(e)}-${t}${o}`}function gt(e){return e.map((t)=>({name:t.FileName,type:t.FileType,contentType:t.ContentType,fileID:t.fileID,scope:t.Scope,sort:t.FileSort??null,...nn(t.ContentType)?{filePath:`imports/${Hn(t.FileName,t.fileID,t.FileType)}`}:{}}))}function on(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(gt(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 Gr(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(gt(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 Fa(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(`
90
- `)}import{mkdir as rn,readdir as Dt,rm as em,stat as tm,unlink as zr}from"fs/promises";import{basename as La,dirname as Io,join as se}from"path";function xt(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}async function F(e,t=""){try{return await B(e)}catch{return t}}async function Xr(e,t){try{return await B(e)}catch(n){if(In(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 U(e,t){try{return JSON.parse(await B(e))}catch{return t}}async function yt(e){try{return(await tm(e)).isDirectory()}catch{return!1}}async function Me(e){try{return(await Dt(e,{withFileTypes:!0})).filter((n)=>n.isFile()&&n.name.endsWith(".vue")).map((n)=>n.name)}catch{return[]}}async function Bn(e,t){let n=[];for(let[o,s]of Object.entries(t)){let r=se(e,o);await rn(Io(r),{recursive:!0}),await Bun.write(r,s),n.push(r)}return n}async function Ua(e,t,n){let o=[],s;try{s=await Dt(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=se(t,r.name);await ge(e,i,{suffix:"deleted"}),await zr(i),o.push(i)}return o}function Ha(e){return new Set(Object.keys(e).filter((t)=>t.startsWith("components/")&&t.endsWith(".vue")).map((t)=>La(t)))}async function Yr(e,t,n){return await rn(t,{recursive:!0}),await Ua(e,se(t,"components"),Ha(n)),Ae(e,t,n)}async function Ba(e,t,n,o,s){await rn(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 B(se(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 Ua(e,se(t,"components"),Ha(r)),c=await Ae(e,t,r);return{moduleDir:t,removedFiles:a,warnings:i,sync:c}}async function qa(e,t,n){let o=await U(se(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=se(t,"components",`${r.comName}.vue`);try{await ge(e,i,{suffix:"deleted"}),await zr(i),s.push(`${r.comName}.vue`)}catch{}}return s}async function qn(e,t,n){let o=se(t,"imports"),s=[],r;try{r=await Dt(o,{withFileTypes:!0})}catch{return s}let i=new Set([...n].map((a)=>La(a)));for(let a of r){if(!a.isFile()||a.name==="_imports.json"||i.has(a.name))continue;let c=se(o,a.name);try{await ge(e,c,{suffix:"deleted"}),await zr(c),s.push(`imports/${a.name}`)}catch{}}return s}async function Va(e,t,n){let o=se(t,"modules"),s=[],r;try{r=await Dt(o,{withFileTypes:!0})}catch{return s}for(let i of r){if(!i.isDirectory()||n.has(i.name))continue;let a=se(o,i.name);await mo(e,a,{suffix:"deleted"}),await em(a,{recursive:!0,force:!0}),s.push(i.name)}return s}function Qr(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 Zr(e,t){return se(e.projectDir(t),"modules","_modules.json")}async function Wa(e,t){let n=await U(Zr(e,t),[]);return Array.isArray(n)?n.filter(Qr):[]}async function Vn(e,t,n){let o=e.projectDir(t);await Bn(o,{"modules/_modules.json":JSON.stringify(n,null,2)});for(let s of n){let r=se(o,"modules",s.ModuleID,"_moduleInfo.json"),i=await U(r,null);if(!i)continue;await Bun.write(r,JSON.stringify({...i,...s},null,2))}return n.length}async function Co(e,t,n,o){let s=Zr(e,t),r=await U(s,[]),i=Array.isArray(r)?r.filter(Qr):[],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 rn(Io(s),{recursive:!0}),await Bun.write(s,JSON.stringify(i,null,2))}async function Ja(e,t,n){let o=Zr(e,t),s=await U(o,[]),r=Array.isArray(s)?s.filter(Qr):[],i=r.findIndex((a)=>a.ModuleID===n);if(i<0)return!1;return r.splice(i,1),await rn(Io(o),{recursive:!0}),await Bun.write(o,JSON.stringify(r,null,2)),!0}async function Ka(e){try{return(await Dt(e.root,{withFileTypes:!0})).filter((n)=>n.isDirectory()&&!n.name.startsWith("_")).map((n)=>n.name)}catch{return[]}}async function Ga(e){let t=[],n;try{n=await Dt(e.root,{withFileTypes:!0})}catch{return t}for(let o of n){if(!o.isDirectory()||o.name.startsWith("_"))continue;let s=se(e.root,o.name);t.push({baseDir:s,projectId:o.name});try{for(let r of await Dt(se(s,"modules"),{withFileTypes:!0}))if(r.isDirectory())t.push({baseDir:se(s,"modules",r.name),projectId:o.name,moduleId:r.name})}catch{}}return t}async function es(e,t){let n=[];for(let o of await Ga(e)){let s=await U(se(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 ts(e,t,n){let o=[];for(let{baseDir:s}of await Ga(e)){let r=await U(se(s,"imports/_imports.json"),[]),i=Array.isArray(r)?r.find((c)=>c?.fileID===t&&c?.filePath):void 0;if(!i?.filePath)continue;let a=se(s,i.filePath);try{await rn(Io(a),{recursive:!0}),await Bun.write(a,n),o.push(a)}catch{}}return o}var nm=20,om=50;function ns(e,t){let n=t==="module"?om:nm;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 bt(e){return JSON.stringify(e??{},null,2)}function wt(e){return e.replace(/\r\n/g,`
91
- `).trim()}var rm=["module.json","header.html","body.html","script.js","style.css"];async function Ro(e){let t=[];for(let o of rm)t.push(`${o}
92
- ${await F(se(e,o))}`);let n=se(e,"components");for(let o of(await Me(n)).slice().sort())t.push(`components/${o}
93
- ${await F(se(n,o))}`);return Et(t.join(`
87
+ `)}},nc=[$m,jm,Rm,Im,Cm];import{readdir as xm}from"node:fs/promises";import{join as cs}from"node:path";import{readdir as Tm}from"node:fs/promises";import{basename as Pm,dirname as Em,extname as _m,isAbsolute as Am,join as Do,relative as No,resolve as Nm,sep as Dm}from"node:path";function xo(e,t,n=process.cwd()){let o=Nm(n,t),s=No(e.root,o);if(!s||s.startsWith("..")||Am(s))return null;let r=s.split(Dm),i=r[0];if(i.startsWith("_")||r.length<2)return null;if(r[1]==="modules"&&r.length>=4){let c=r[2],l=Do(e.root,i,"modules",c);return{abs:o,baseDir:l,rel:Se(No(l,o)),projectId:i,moduleId:c}}let a=Do(e.root,i);return{abs:o,baseDir:a,rel:Se(No(a,o)),projectId:i}}var oc=(e)=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");async function as(e,t){let n=No(e.root,t),o=_m(n),s=Pm(n,o),r=Do(e.historyRoot(),Em(n)),i=new RegExp(`^${oc(s)}_(\\d{8}_\\d{6})((?:_[A-Za-z0-9-]+)*)${oc(o)}$`),a;try{a=await Tm(r)}catch{return[]}return a.map((c)=>{let l=i.exec(c);return l?{stamp:l[1],suffix:l[2].replace(/^_/,""),path:Do(r,c)}:null}).filter((c)=>c!==null).sort((c,l)=>l.stamp.localeCompare(c.stamp)||l.path.localeCompare(c.path))}function ls(e,t){let n=xo(e.paths,t);if(!n)throw new m(`${t} is not inside a project of workspace ${e.paths.root}`);return n}var Om={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(Y(a,i))}};async function Mm(e){try{return(await xm(cs(e,"modules"),{withFileTypes:!0})).filter((n)=>n.isDirectory()).map((n)=>cs(e,"modules",n.name))}catch{return[]}}var Fm={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 Mm(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 Un(a))i.push({dir:a,path:c.relPath,reason:c.reason,push:jo(c.relPath)});T(`# ${i.length} unpublished file(s)`),k(Y(i,s))}},Lm={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=ls(e,t),o=await as(e.paths,n.abs);T(`# ${o.length} version(s) of ${n.rel}`),k(Y(o.map((s)=>({...s,path:Dt(e.paths,s.path)})),e.render))}},Um={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=ls(e,t),o=await as(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 ue(n.abs),a=i===null?null:await we(e.paths,n.abs,{content:i});if(await x(n.abs,await W(r.path)),a)T(`# previous content archived: ${Dt(e.paths,a)}`);k(`restored ${n.rel} from ${r.stamp}; push it with: ${jo(n.rel)}
88
+ `)}},Hm={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)=>ls(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 Ae(a,l);if(n.forget===!0)await oa(a,l)}let i=[];for(let[a,c]of r){let l={};try{l=JSON.parse(await W(cs(a,rn)))}catch{}for(let u of c){let d=await ue(u.abs),p=l[u.rel]??"",h=d===null?"missing":!p?"new":p===Mt(d)?"published":"changed";i.push({path:u.rel,state:h,ledger:p,dir:a})}}k(Y(i,o))}},rc=[Om,Fm,Lm,Um,Hm];import{rename as dh}from"node:fs/promises";import{basename as ph,dirname as $s,join as We}from"node:path";function un(e){return e==="text/css"||e==="text/javascript"}function Vm(e){return(e||"").trim().replace(/[<>:"/\\|?*\x00-\x1F]/g,"-").replace(/\s+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"import-file"}function Kn(e,t,n){let o=n.startsWith(".")?n:`.${n}`;return`${Vm(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,...un(t.ContentType)?{filePath:`imports/${Kn(t.FileName,t.fileID,t.FileType)}`}:{}}))}function dn(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 us(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 pn,readdir as Ht,rm as Wm,stat as qm,unlink as ds}from"node:fs/promises";import{basename as ic,dirname as Oo,join as ie}from"node:path";function Vt(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}async function L(e,t=""){try{return await W(e)}catch{return t}}async function ps(e,t){try{return await W(e)}catch(n){if(Nn(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 W(e))}catch{return t}}async function $t(e){try{return(await qm(e)).isDirectory()}catch{return!1}}async function Fe(e){try{return(await Ht(e,{withFileTypes:!0})).filter((n)=>n.isFile()&&n.name.endsWith(".vue")).map((n)=>n.name)}catch{return[]}}async function Gn(e,t){let n=[];for(let[o,s]of Object.entries(t)){let r=ie(e,o);await pn(Oo(r),{recursive:!0}),await x(r,s),n.push(r)}return n}async function ac(e,t,n){let o=[],s;try{s=await Ht(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 we(e,i,{suffix:"deleted"}),await ds(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)=>ic(t)))}async function ms(e,t,n){return await pn(t,{recursive:!0}),await ac(e,ie(t,"components"),cc(n)),Ne(e,t,n)}async function lc(e,t,n,o,s){await pn(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 W(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 ac(e,ie(t,"components"),cc(r)),c=await Ne(e,t,r);return{moduleDir:t,removedFiles:a,warnings:i,sync:c}}async function uc(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 we(e,i,{suffix:"deleted"}),await ds(i),s.push(`${r.comName}.vue`)}catch{}}return s}async function zn(e,t,n){let o=ie(t,"imports"),s=[],r;try{r=await Ht(o,{withFileTypes:!0})}catch{return s}let i=new Set([...n].map((a)=>ic(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 we(e,c,{suffix:"deleted"}),await ds(c),s.push(`imports/${a.name}`)}catch{}}return s}async function dc(e,t,n){let o=ie(t,"modules"),s=[],r;try{r=await Ht(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 ko(e,a,{suffix:"deleted"}),await Wm(a,{recursive:!0,force:!0}),s.push(i.name)}return s}function hs(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 fs(e,t){return ie(e.projectDir(t),"modules","_modules.json")}async function pc(e,t){let n=await H(fs(e,t),[]);return Array.isArray(n)?n.filter(hs):[]}async function Xn(e,t,n){let o=e.projectDir(t);await Gn(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 x(r,JSON.stringify({...i,...s},null,2))}return n.length}async function Mo(e,t,n,o){let s=fs(e,t),r=await H(s,[]),i=Array.isArray(r)?r.filter(hs):[],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 pn(Oo(s),{recursive:!0}),await x(s,JSON.stringify(i,null,2))}async function mc(e,t,n){let o=fs(e,t),s=await H(o,[]),r=Array.isArray(s)?s.filter(hs):[],i=r.findIndex((a)=>a.ModuleID===n);if(i<0)return!1;return r.splice(i,1),await pn(Oo(o),{recursive:!0}),await x(o,JSON.stringify(r,null,2)),!0}async function hc(e){try{return(await Ht(e.root,{withFileTypes:!0})).filter((n)=>n.isDirectory()&&!n.name.startsWith("_")).map((n)=>n.name)}catch{return[]}}async function fc(e){let t=[],n;try{n=await Ht(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 Ht(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 gs(e,t){let n=[];for(let o of await fc(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 ws(e,t,n){let o=[];for(let{baseDir:s}of await fc(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 pn(Oo(a),{recursive:!0}),await x(a,n),o.push(a)}catch{}}return o}var Bm=20,Jm=50;function ys(e,t){let n=t==="module"?Jm:Bm;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 St(e){return e.replace(/\r\n/g,`
90
+ `).trim()}var Km=["module.json","header.html","body.html","script.js","style.css"];async function Fo(e){let t=[];for(let o of Km)t.push(`${o}
91
+ ${await L(ie(e,o))}`);let n=ie(e,"components");for(let o of(await Fe(n)).slice().sort())t.push(`components/${o}
92
+ ${await L(ie(n,o))}`);return Mt(t.join(`
94
93
  \x00
95
- `))}function za(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 Xa(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(wt(a)!==wt(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 os(e){if(!xt(e))throw new h("project.json must be a JSON object at the top level");if(xt(e.data))return{config:e.data,wasEnveloped:!0};return{config:e,wasEnveloped:!1}}function Po(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 ie}from"path";function kt(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=im(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=am(e.substring(a.index+a[0].length,r).trim()),u=sm(e,o,r);return{template:c,script:l,style:u}}function sm(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 im(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 am(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 sn(e){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}function Ya(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 cm(e){let t=e.trim();if(/^(https?:)?\/\//i.test(t)){let{host:o,path:s}=Ya(t);return o?{host:o,path:s}:null}let n=t.split("/")[0];if(n&&n.includes(".")){let{host:o,path:s}=Ya(`//${t}`);return o?{host:o,path:s}:null}return null}function Qa(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 lm(e){let t=[e.ModuleID,e.ModuleName,e.mDomain,e.url].filter((n)=>Boolean(n));return[...new Set(t.flatMap((n)=>[n.toLowerCase(),sn(n),n.replace(/^\/+|\/+$/g,"").toLowerCase()]))]}function rs(e,t){let n=t.trim().toLowerCase().replace(/^\/+|\/+$/g,""),o=sn(t);return e.filter((s)=>{let r=lm(s);return r.includes(n)||r.includes(o)})}function an(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 Wn{paths;api;constructor(e,t){this.paths=e;this.api=t}baseDir(e,t){return t?ie(this.paths.projectDir(e),"modules",t):this.paths.projectDir(e)}async lookupModuleInfo(e,t){let n=t(await Wa(this.paths,e));if(n)return{found:n,refreshed:!1};let o;try{o=await this.api.listModules(e),await Vn(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 yt(t))return null;let n=(await this.api.listProjects()).find((o)=>o.ProjectID===e);if(!n)return null;return await Bn(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=sn(e),o=(await Ka(this.paths)).filter((a)=>a.toLowerCase()===t||sn(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)=>sn(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)||sn(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 an(s.ProjectID,s.ModuleID,r);return{projectId:s.ProjectID,moduleId:s.ModuleID,moduleInfo:r.found,matchedBy:"runtime_lookup"}}async resolveProjectAndModuleHint(e){let t=cm(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=Qa(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=Qa(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 an(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=rs(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 an(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(!nn(o.ContentType))continue;try{let r=(await this.api.getFileContent(o.fileID))?.data?.FileContent??"";t[`imports/${Hn(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 U(ie(e,"imports/_imports.json"),[]);for(let n of t){if(!n.fileID||!n.filePath||!nn(n.contentType??""))continue;let o=await F(ie(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 qa(this.paths,s,new Set(o.map((i)=>i.comName)));return await Bn(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=gt(o),i=new Set(r.map((c)=>c.filePath).filter((c)=>!!c)),a=await qn(this.paths,s,i);return await Bn(s,{"imports/_imports.json":JSON.stringify(r,null,2)}),{list:n,count:o.length,removed:a}}async publishStagedModuleDir(e){let t=await U(ie(e,"_moduleInfo.json"),null);if(!t)throw new h(`missing _moduleInfo.json in ${e}; run fui module get first`);let n=await F(ie(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(ie(e,"header.html")),r=await F(ie(e,"body.html")),i=await F(ie(e,"script.js")),a=await F(ie(e,"style.css")),{pageHeader:c,pageBody:l,mTitle:u}=za(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=ie(e,"components"),w=await U(ie(m,"_components.json"),[]),y=new Map(w.map((g)=>[g.comName,g.comID])),v=await Me(m),E=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}=kt(await F(ie(m,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(!E.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(ve(ie("components",g)));return await Ee(e,I).catch(()=>{return}),t}async previewStagedModuleDir(e){let t=await U(ie(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=on(n.moduleInfo,n.content,n.components,n.script,n.css,n.dependencies),s;try{s=JSON.parse(await F(ie(e,"module.json")))}catch(p){throw new h(`module.json has invalid JSON syntax: ${p.message}; fix it before previewing`)}let r=await F(ie(e,"header.html")),i=await F(ie(e,"body.html")),a=await F(ie(e,"script.js")),c=await F(ie(e,"style.css")),l=await U(ie(e,"imports/_imports.json"),[]),u={};for(let p of await Me(ie(e,"components")))u[p.replace(/\.vue$/i,"")]=await F(ie(e,"components",p));let d=[];if(bt(s)!==bt(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(wt(m)!==wt(o[p]||""))d.push(`${p} will be updated`)}if(bt(l)!==bt(JSON.parse(o["imports/_imports.json"]||"[]")))d.push("imports/_imports.json differs locally (metadata only, not sent)");return d.push(...Xa(n.components,u)),{moduleInfo:t,changes:d,remoteSummary:Po(n.content)}}}import{cp as um,mkdir as ss,readdir as dm,rm as pm}from"fs/promises";import{dirname as mm,isAbsolute as hm,join as Ne,relative as fm,sep as gm}from"path";var vt="fui-skill",wm=["references/controls-patterns.md","references/module-structure.md","references/INDEX.md"],Za=`# FUI Skill Summary
94
+ `))}function gc(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 wc(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(St(a)!==St(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 bs(e){if(!Vt(e))throw new m("project.json must be a JSON object at the top level");if(Vt(e.data))return{config:e.data,wasEnveloped:!0};return{config:e,wasEnveloped:!1}}function Lo(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=zm(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=Xm(e.substring(a.index+a[0].length,r).trim()),u=Gm(e,o,r);return{template:c,script:l,style:u}}function Gm(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 zm(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 Xm(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 mn(e){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}function yc(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 Ym(e){let t=e.trim();if(/^(https?:)?\/\//i.test(t)){let{host:o,path:s}=yc(t);return o?{host:o,path:s}:null}let n=t.split("/")[0];if(n&&n.includes(".")){let{host:o,path:s}=yc(`//${t}`);return o?{host:o,path:s}:null}return null}function bc(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 Qm(e){let t=[e.ModuleID,e.ModuleName,e.mDomain,e.url].filter((n)=>Boolean(n));return[...new Set(t.flatMap((n)=>[n.toLowerCase(),mn(n),n.replace(/^\/+|\/+$/g,"").toLowerCase()]))]}function ks(e,t){let n=t.trim().toLowerCase().replace(/^\/+|\/+$/g,""),o=mn(t);return e.filter((s)=>{let r=Qm(s);return r.includes(n)||r.includes(o)})}function hn(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 Yn{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 pc(this.paths,e));if(n)return{found:n,refreshed:!1};let o;try{o=await this.api.listModules(e),await Xn(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 $t(t))return null;let n=(await this.api.listProjects()).find((o)=>o.ProjectID===e);if(!n)return null;return await Gn(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=mn(e),o=(await hc(this.paths)).filter((a)=>a.toLowerCase()===t||mn(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)=>mn(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)||mn(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 hn(s.ProjectID,s.ModuleID,r);return{projectId:s.ProjectID,moduleId:s.ModuleID,moduleInfo:r.found,matchedBy:"runtime_lookup"}}async resolveProjectAndModuleHint(e){let t=Ym(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=bc(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=bc(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 hn(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=ks(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 hn(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(!un(o.ContentType))continue;try{let r=(await this.api.getFileContent(o.fileID))?.data?.FileContent??"";t[`imports/${Kn(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||!un(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 uc(this.paths,s,new Set(o.map((i)=>i.comName)));return await Gn(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 zn(this.paths,s,i);return await Gn(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}=gc(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 Fe(h),E=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(!E.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(Se(ae("components",g)));return await Ae(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=dn(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 Fe(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(St(h)!==St(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(...wc(n.components,u)),{moduleInfo:t,changes:d,remoteSummary:Lo(n.content)}}}import{cp as Zm,mkdir as vs,readdir as eh,rm as th}from"node:fs/promises";import{dirname as vc,isAbsolute as nh,join as Ce,relative as oh,sep as rh}from"node:path";import{fileURLToPath as sh}from"node:url";var Ct="fui-skill",ih=["references/controls-patterns.md","references/module-structure.md","references/INDEX.md"],Sc=`# FUI Skill Summary
96
95
 
97
96
  Use these rules whenever editing a FUI module:
98
97
 
@@ -103,30 +102,30 @@ Use these rules whenever editing a FUI module:
103
102
  - In action IN blocks, force literal strings when needed by using backtick or quoted-string patterns.
104
103
  - Reference reactive state with vueData. prefixes.
105
104
  - Custom Vue components should use the uc- prefix.
106
- - Run module_preview first, then ask the user if they want to push. Only call module_publish_html (with the returned approvalToken) \u2014 or module_publish_json / module_publish_script / component_update for their respective files \u2014 after explicit approval.
105
+ - Run module_preview first, then ask the user if they want to push. Only call module_publish_html (with the returned approvalToken) — or module_publish_json / module_publish_script / component_update for their respective files — after explicit approval.
107
106
  - Build fullstack modules UI-first: design the UI with hardcoded/fake data and verify it visually (module_simulate renderUI) before designing the API contract and wiring real bindings.
108
- - Never call a live API/SP endpoint to verify it \u2014 read the SP body and reason about it (db_sp_verify) instead; that rule does not apply to db_sql_execute/db_sql_execute_nonquery, which remain the user's direct data-edit tools.
109
- `,ym="https://github.com/fui-org/fui-skill";function bm(){return`${Za}
107
+ - Never call a live API/SP endpoint to verify it — read the SP body and reason about it (db_sp_verify) instead; that rule does not apply to db_sql_execute/db_sql_execute_nonquery, which remain the user's direct data-edit tools.
108
+ `,ah="https://github.com/fui-org/fui-skill";function ch(){return`${Sc}
110
109
 
111
110
  ## Source
112
111
 
113
- - Bundled with fui-mcp from ${ym}
112
+ - Bundled with fui-mcp from ${ah}
114
113
 
115
114
  ## Notes
116
115
 
117
116
  - This fallback file exists so every editor can read a project-local skill snapshot after checkout.
118
117
  - fui-mcp copies the package-level src/skills/ folder into each staged project workspace.
119
- - If the bundled src/skills/fui-skill folder is missing from this MCP package, this summary still gives the agent the critical FUI rules.`}async function km(){let e=import.meta.dir;for(let t of[Ne(e,"..","..","..","skills"),Ne(e,"..","skills")])if(await Bun.file(Ne(t,vt,"SKILL.md")).exists())return t;return Ne(e,"..","..","..","skills")}async function Ot(){return Ne(await km(),vt)}function vm(e){return Ne(e,"_skills",vt,"SKILL.md")}async function To(e){let t=Ne(e,"_skills"),n=Ne(t,vt);await ss(e,{recursive:!0});try{let o=await Ot();if(!await Bun.file(Ne(o,"SKILL.md")).exists())throw Error(`bundled skill missing at ${o}`);return await pm(t,{recursive:!0,force:!0}),await ss(t,{recursive:!0}),await um(o,n,{recursive:!0,filter:(s)=>!s.split(gm).includes(".git")}),{workspaceRoot:e,skillDir:n,source:"bundled-copy"}}catch(o){let s=`Could not copy bundled skills/ into ${n}: ${o.message}`;return await ss(n,{recursive:!0}),await Bun.write(Ne(n,"SKILL.md"),`${bm()}
118
+ - If the bundled src/skills/fui-skill folder is missing from this MCP package, this summary still gives the agent the critical FUI rules.`}async function lh(){let e=vc(sh(import.meta.url));for(let t of[Ce(e,"..","..","..","skills"),Ce(e,"..","skills")])if(await He(Ce(t,Ct,"SKILL.md")))return t;return Ce(e,"..","..","..","skills")}async function Rt(){return Ce(await lh(),Ct)}function uh(e){return Ce(e,"_skills",Ct,"SKILL.md")}async function Uo(e){let t=Ce(e,"_skills"),n=Ce(t,Ct);await vs(e,{recursive:!0});try{let o=await Rt();if(!await He(Ce(o,"SKILL.md")))throw Error(`bundled skill missing at ${o}`);return await th(t,{recursive:!0,force:!0}),await vs(t,{recursive:!0}),await Zm(o,n,{recursive:!0,filter:(s)=>!s.split(rh).includes(".git")}),{workspaceRoot:e,skillDir:n,source:"bundled-copy"}}catch(o){let s=`Could not copy bundled skills/ into ${n}: ${o.message}`;return await vs(n,{recursive:!0}),await x(Ce(n,"SKILL.md"),`${ch()}
120
119
 
121
120
  ## Sync Warning
122
121
 
123
122
  ${s}
124
- `),{workspaceRoot:e,skillDir:n,source:"bundled"}}}async function ec(e){if(e){let t=vm(e),n=await le(t);if(n&&n.trim()){let o=[n];for(let s of wm){let r=await le(Ne(mm(t),s));if(r?.trim())o.push(`
123
+ `),{workspaceRoot:e,skillDir:n,source:"bundled"}}}async function kc(e){let t=await ue(e);if(!t?.trim())return null;let n=[t];for(let o of ih){let s=await ue(Ce(vc(e),o));if(s?.trim())n.push(`
125
124
 
126
125
  ---
127
- <!-- ${s} -->
126
+ <!-- ${o} -->
128
127
 
129
- ${r}`)}return{text:o.join(""),source:t}}}return{text:Za.trim(),source:"bundled-summary"}}async function tc(e,t){return(await Promise.all(t.map(async(o)=>{let s=Ne(e,o),r=fm(e,s);if(r.startsWith("..")||hm(r))return`
128
+ ${s}`)}return{text:n.join(""),source:e}}async function $c(e){return(e?await kc(uh(e)):null)??await kc(Ce(await Rt(),"SKILL.md"))??{text:Sc.trim(),source:"bundled-summary"}}async function jc(e,t){return(await Promise.all(t.map(async(o)=>{let s=Ce(e,o),r=oh(e,s);if(r.startsWith("..")||nh(r))return`
130
129
 
131
130
  ---
132
131
  <!-- ${o} -->
@@ -136,63 +135,63 @@ invalid path (outside the skill folder)`;try{return`
136
135
  ---
137
136
  <!-- ${o} -->
138
137
 
139
- ${await B(s)}`}catch{return`
138
+ ${await W(s)}`}catch{return`
140
139
 
141
140
  ---
142
141
  <!-- ${o} -->
143
142
 
144
- not found at ${s}`}}))).join("")}async function is(){let e=Ne(await Ot(),"design-md");return(await dm(e,{withFileTypes:!0})).filter((n)=>n.isDirectory()).map((n)=>n.name).sort()}async function ce(e){return new Wn(e.paths,await e.api())}function nc(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 Mt(e,t){let n=await e.wasSkillLoaded();if(!n&&Jt())throw new Se(`[FUI_MCP_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}
143
+ not found at ${s}`}}))).join("")}async function Ss(){let e=Ce(await Rt(),"design-md");return(await eh(e,{withFileTypes:!0})).filter((n)=>n.isDirectory()).map((n)=>n.name).sort()}async function le(e){return new Yn(e.paths,await e.api())}function Ic(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 Wt(e,t){let n=await e.wasSkillLoaded();if(!n&&en())throw new $e(`[FUI_MCP_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 Cc(e,t){let n=await(await le(e)).resolveModuleTarget(t);return{notes:[],data:`project ${n.projectId}
145
144
  module ${n.moduleId}
146
145
  matchedBy ${n.matchedBy}
147
146
  moduleDir ${e.paths.projectDir(n.projectId)}/modules/${n.moduleId}
148
- `}}async function rc(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 U(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?as(as(as(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 sc(e){return(await e.api()).listProjects()}async function ic(e,t){let n=await ce(e),o=e.paths.projectDir(t),s=await n.fetchProjectBundle(t),r=Gr(s.projectInfo,s.projectJson,s.modules,s.projectComponents,s.projectImports),{"modules/_modules.json":i,...a}=r,c=new Set(gt(s.projectImports).map((y)=>y.filePath).filter((y)=>!!y)),l=await qn(e.paths,o,c),u=await n.downloadImportContents(s.projectImports),d=await Yr(e.paths,o,{...a,...u.files}),p=await Va(e.paths,o,new Set(s.modules.map((y)=>y.ModuleID)));await Vn(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: ${Ir(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"]:[],...Cr(d.archived),...nc(u.failures)]}}async function ac(e,t){let n=await(await e.api()).listModules(t),o;try{await Vn(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 cc(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 Co(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 lc(e,t){let n=await ce(e),{projectId:o,moduleId:s,newModuleId:r}=t,i=He(e.paths.projectDir(o),"modules",s),a=await U(He(i,"_moduleInfo.json"),null),c=a?null:await n.resolveModuleInfoFromCache(o,s),l=a??c?.found;if(!l)throw an(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 Co(e.paths,o,v,s);let E=["modules/_modules.json updated"];if(a){let I=i;if(r&&r!==s){let b=He(e.paths.projectDir(o),"modules",r);if(await yt(b))u.push(`warning: cannot rename the local folder: ${b} exists; _moduleInfo.json updated in place`);else await Sm(i,b),I=b,E.push(`local folder renamed: modules/${s} -> modules/${r}`)}await Bun.write(He(I,"_moduleInfo.json"),JSON.stringify({...a,...v},null,2)),E.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}),E.push("session target updated")}return{notes:[`ok updated ${v.id} url ${v.url} name ${v.ModuleName} framework ${v.Framework} htmlOnly ${v.HTMLOnly}`,...E,...u]}}async function uc(e,t,n){await(await e.api()).deleteModule(t,n);let o=[`ok deleted module ${n} from project ${t}`];try{o.push(await Ja(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 yt(s)){await mo(e.paths,s,{suffix:"deleted"});let{rm:i}=await import("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 dc(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 _=await n.resolveProjectAndModuleHint(t.target);o=_.projectId,s=_.moduleInfo?.ModuleID??_.moduleHint,r=_.matchedBy,i=_.moduleInfo}else{let _=await e.targets.resolveProjectModule();o=_.projectId,s=_.moduleId,r=_.source==="cwd"?"cwd":"active_session"}let a=e.paths.projectDir(o),c=await U(He(a,"_projectInfo.json"),null)!==null,l=[];if(!c){let _=await n.fetchProjectBundle(o);await Yr(e.paths,a,Gr(_.projectInfo,_.projectJson,_.modules,_.projectComponents,_.projectImports));let de=await n.downloadImportContents(_.projectImports);if(l.push(...de.failures),Object.keys(de.files).length>0)await Ae(e.paths,a,de.files)}let u=i??null;if(!u){let _=await n.lookupModuleInfo(o,(de)=>{let Ie=de.find((ae)=>ae.ModuleID===s);if(Ie||r!=="target_path")return Ie??null;let H=rs(de,s);if(H.length>1)throw new h(`target "${t.target}" matched several modules in project ${o}: ${H.map((ae)=>ae.ModuleID).join(", ")}`);return H[0]??null});if(u=_.found,!u)throw an(o,s,_)}let{content:d,components:p,script:m,css:w,dependencies:y}=await n.fetchModuleBundle(u),v=on(u,d,p,m,w,y),E=He(e.paths.projectDir(u.ProjectID),"modules",u.ModuleID),I=new Set(gt(y.filter((_)=>_.Scope!=="project")).map((_)=>_.filePath).filter((_)=>!!_)),g=await qn(e.paths,E,I),[b,R]=await Promise.all([n.downloadImportContents(y.filter((_)=>_.Scope==="module")),n.downloadImportContents(c?y.filter((_)=>_.Scope==="project"):[])]);l.push(...b.failures,...R.failures);let C=await Ba(e.paths,E,u,d,{...v,...b.files}),S=Object.keys(R.files).length>0?await Ae(e.paths,a,R.files):null;await Co(e.paths,o,{...u,Framework:d.Framework,HTMLOnly:d.HTMLOnly});let N=await To(e.paths.root);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((_)=>$m(_)),...g],A=[...C.sync.archived,...S?.archived??[]],G=c?await At(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: ${Ir(C.sync)}`,...D.length>0?[`removed orphaned local files: ${D.join(", ")}`]:[],...Cr(A),c?"project data already local (not fetched); refresh: fui project sync":"project data bootstrapped (project.json, components, imports, modules/_modules.json)",...G?[G]:[],`skills: ${N.skillDir}${N.source==="bundled"?" (fallback summary; copy failed)":""}`,...C.warnings.map((_)=>`warning: ${_}`),...nc(l),...Po(d),"now active for fui module preview / publish / validate"],data:`${Fa(u.ModuleID,{"_moduleInfo.json":"{}",...v})}
149
- `}}async function pc(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 yt(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 yt(a))await Ae(e.paths,a,{"body.html":i.mBody}),c.push(`body.html written to ${a}`);return{notes:c,data:`${JSON.stringify(r)}
150
- `}}async function cn(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 mc(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 yt(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 ln(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 cs(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 Bun.file(n).text():t==="-"?await Bun.stdin.text():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 Eo(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 Bun.file(s).text():o==="-"?await Bun.stdin.text():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 ls(...e){return Object.fromEntries(e.flatMap((t)=>[[t,{type:"string"}],[`${t}-file`,{type:"string"}]]))}var un={module:{type:"string",short:"m"}};function Ao(e){for(let t of e.notes)T(`# ${t}`);if(e.data)k(e.data)}function Jn(e){if(k(`${e.notes.join(`
147
+ `}}async function Rc(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 le(e)).resolveModuleTarget(t.target,n,o);n=a.projectId,o=a.moduleId,s=We(e.paths.projectDir(n),"modules",o),r=a.matchedBy}else{let a=await H(We(s,"_moduleInfo.json"),null);if(!a)throw new m(`missing _moduleInfo.json in ${s}`);n=a.ProjectID,o=a.ModuleID}let i=t.moduleDir?$s($s($s(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 Tc(e){return(await e.api()).listProjects()}async function Pc(e,t){let n=await le(e),o=e.paths.projectDir(t),s=await n.fetchProjectBundle(t),r=us(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 zn(e.paths,o,c),u=await n.downloadImportContents(s.projectImports),d=await ms(e.paths,o,{...a,...u.files}),p=await dc(e.paths,o,new Set(s.modules.map((y)=>y.ModuleID)));await Xn(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: ${Ur(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"]:[],...Hr(d.archived),...Ic(u.failures)]}}async function Ec(e,t){let n=await(await e.api()).listModules(t),o;try{await Xn(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 _c(e,t){let n=await le(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 Mo(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 Ac(e,t){let n=await le(e),{projectId:o,moduleId:s,newModuleId:r}=t,i=We(e.paths.projectDir(o),"modules",s),a=await H(We(i,"_moduleInfo.json"),null),c=a?null:await n.resolveModuleInfoFromCache(o,s),l=a??c?.found;if(!l)throw hn(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 Mo(e.paths,o,v,s);let E=["modules/_modules.json updated"];if(a){let I=i;if(r&&r!==s){let b=We(e.paths.projectDir(o),"modules",r);if(await $t(b))u.push(`warning: cannot rename the local folder: ${b} exists; _moduleInfo.json updated in place`);else await dh(i,b),I=b,E.push(`local folder renamed: modules/${s} -> modules/${r}`)}await x(We(I,"_moduleInfo.json"),JSON.stringify({...a,...v},null,2)),E.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}),E.push("session target updated")}return{notes:[`ok updated ${v.id} url ${v.url} name ${v.ModuleName} framework ${v.Framework} htmlOnly ${v.HTMLOnly}`,...E,...u]}}async function Nc(e,t,n){await(await e.api()).deleteModule(t,n);let o=[`ok deleted module ${n} from project ${t}`];try{o.push(await mc(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=We(e.paths.projectDir(t),"modules",n);if(await $t(s)){await ko(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 Dc(e,t){let n=await le(e),o,s,r,i;if(t.projectId&&t.moduleId)o=t.projectId,s=t.moduleId,r="explicit_ids";else if(t.target){let N=await n.resolveProjectAndModuleHint(t.target);o=N.projectId,s=N.moduleInfo?.ModuleID??N.moduleHint,r=N.matchedBy,i=N.moduleInfo}else{let N=await e.targets.resolveProjectModule();o=N.projectId,s=N.moduleId,r=N.source==="cwd"?"cwd":"active_session"}let a=e.paths.projectDir(o),c=await H(We(a,"_projectInfo.json"),null)!==null,l=[];if(!c){let N=await n.fetchProjectBundle(o);await ms(e.paths,a,us(N.projectInfo,N.projectJson,N.modules,N.projectComponents,N.projectImports));let pe=await n.downloadImportContents(N.projectImports);if(l.push(...pe.failures),Object.keys(pe.files).length>0)await Ne(e.paths,a,pe.files)}let u=i??null;if(!u){let N=await n.lookupModuleInfo(o,(pe)=>{let Re=pe.find((ce)=>ce.ModuleID===s);if(Re||r!=="target_path")return Re??null;let V=ks(pe,s);if(V.length>1)throw new m(`target "${t.target}" matched several modules in project ${o}: ${V.map((ce)=>ce.ModuleID).join(", ")}`);return V[0]??null});if(u=N.found,!u)throw hn(o,s,N)}let{content:d,components:p,script:h,css:w,dependencies:y}=await n.fetchModuleBundle(u),v=dn(u,d,p,h,w,y),E=We(e.paths.projectDir(u.ProjectID),"modules",u.ModuleID),I=new Set(vt(y.filter((N)=>N.Scope!=="project")).map((N)=>N.filePath).filter((N)=>!!N)),g=await zn(e.paths,E,I),[b,R]=await Promise.all([n.downloadImportContents(y.filter((N)=>N.Scope==="module")),n.downloadImportContents(c?y.filter((N)=>N.Scope==="project"):[])]);l.push(...b.failures,...R.failures);let C=await lc(e.paths,E,u,d,{...v,...b.files}),S=Object.keys(R.files).length>0?await Ne(e.paths,a,R.files):null;await Mo(e.paths,o,{...u,Framework:d.Framework,HTMLOnly:d.HTMLOnly});let A=await Uo(e.paths.root);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((N)=>ph(N)),...g],_=[...C.sync.archived,...S?.archived??[]],z=c?await Ft(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: ${Ur(C.sync)}`,...D.length>0?[`removed orphaned local files: ${D.join(", ")}`]:[],...Hr(_),c?"project data already local (not fetched); refresh: fui project sync":"project data bootstrapped (project.json, components, imports, modules/_modules.json)",...z?[z]:[],`skills: ${A.skillDir}${A.source==="bundled"?" (fallback summary; copy failed)":""}`,...C.warnings.map((N)=>`warning: ${N}`),...Ic(l),...Lo(d),"now active for fui module preview / publish / validate"],data:`${sc(u.ModuleID,{"_moduleInfo.json":"{}",...v})}
148
+ `}}async function xc(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=We(e.paths.projectDir(t),"modules",n),c=[];if(o==="ui"&&i?.moduleUI!==void 0&&i.moduleUI!==null&&await $t(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 $t(a))await Ne(e.paths,a,{"body.html":i.mBody}),c.push(`body.html written to ${a}`);return{notes:c,data:`${JSON.stringify(r)}
149
+ `}}async function fn(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?We(e.paths.projectDir(o),"modules",s):e.paths.projectDir(o);return{projectId:o,moduleId:s,baseDir:r}}async function Oc(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 $t(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 gn(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 js(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 Qe(n):t==="-"?await En():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 Ho(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 Qe(s):o==="-"?await En():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 Is(...e){return Object.fromEntries(e.flatMap((t)=>[[t,{type:"string"}],[`${t}-file`,{type:"string"}]]))}var wn={module:{type:"string",short:"m"}};function Vo(e){for(let t of e.notes)T(`# ${t}`);if(e.data)k(e.data)}function Qn(e){if(k(`${e.notes.join(`
151
150
  `)}
152
- `),e.data)k(e.data)}async function us(e){return(await e.targets.resolveProject(f(e.values,"project"))).projectId}async function fc(e){return e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module"))}function jm(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 gc(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 Im={name:"use",mcpName:"session_set",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:{...un,"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
153
- `);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(X([{...l,session:o.file}],s));return}if(i&&!a||!i&&a)throw new h("pass both -p and -m");Jn(await rc(O(e),{target:r,projectId:i,moduleId:a,moduleDir:c}))}},Cm={name:"resolve",mcpName:"session_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>");Ao(await oc(O(e),t))}},Rm={name:"project list",mcpName:"project_list",usage:"fui project list",summary:"List projects you can access",async run(e){let t=(await sc(O(e))).map((n)=>({group:n.GroupName||"Khac",id:n.ProjectID,name:n.ProjectName,framework:n.Framework??"",domain:n.pDomain??""}));k(X(t,e.render))}},Pm={name:"project sync",mcpName:"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){Jn(await ic(O(e),await us(e)))}},Tm={name:"module list",mcpName:"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 ac(O(e),await us(e));for(let o of n)T(`# ${o}`);k(X(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))}},Em={name:"module new",mcpName:"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");Jn(await cc(O(e),{projectId:await us(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:ln(e.values.set)}))}},Am={name:"module update",mcpName:"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:{...un,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 fc(e);Jn(await lc(O(e),{projectId:t,moduleId:n,moduleName:f(e.values,"name"),framework:gc(f(e.values,"framework")),htmlOnly:jm(f(e.values,"html-only"),"--html-only"),mDomain:f(e.values,"domain"),newModuleId:f(e.values,"new-id"),extra:ln(e.values.set)}))}},Nm={name:"module delete",mcpName:"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:un,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>");Jn(await uc(O(e),t,n))}},_m={name:"module get",mcpName:"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:un,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");Ao(await dc(O(e),{target:e.positionals[0],projectId:t,moduleId:n}))}};function hc(e){return{name:`module get-${e}`,mcpName:`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:un,async run(t){let{projectId:n,moduleId:o}=await fc(t);Ao(await pc(O(t),n,o,e))}}}var Dm={name:"component get",mcpName:"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:un,async run(e){let t=e.positionals[0];if(!t)throw new h("missing <name>");let n=O(e),o=await cn(n,f(e.values,"project"),f(e.values,"module"));Ao(await mc(n,o,t))}},wc=[Im,Cm,Rm,Pm,Tm,Em,Am,Nm,_m,hc("ui"),hc("html"),Dm];import{resolve as cf}from"path";import{join as lt}from"path";import ct from"path";import{join as yc}from"path";async function Ft(e,t){let n={functionRights:null,systemRights:null},o;try{o=JSON.parse(await B(yc(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 U(yc(e,"_db","_connections.json"),null);i=l?.default?r(l.default):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 Lt(e){return kc("FunctionRight",e)}function bc(e){return kc("SystemRight",e)}function Kn(e,t){if(e.length===0)return null;if(t===null)return`Ch\u01B0a c\xF3 cache _db/rights.json \u2014 KH\xD4NG ki\u1EC3m \u0111\u01B0\u1EE3c ${e.length} m\xE3 FunctionRight (${e.join(", ")}) `+"c\xF3 th\u1EADt hay kh\xF4ng. Preview c\u1EA5p s\u1EB5n \u0111\xFAng nh\u1EEFng m\xE3 n\xE0y n\xEAn control v\u1EABn hi\u1EC7n \u1EDF \u0110\xC2Y; user th\u1EADt ch\u1EC9 th\u1EA5y n\u1EBFu m\xE3 \u0111\xE3 "+"\u0111\u01B0\u1EE3c \u0111\u1ECBnh ngh\u0129a. Ch\u1EA1y right_function_list (m\u1ED9t l\u1EA7n cho m\u1ED7i project) r\u1ED3i render l\u1EA1i.";let n=e.filter((o)=>!t.has(o));if(n.length===0)return null;return`\uD83D\uDEA8 FunctionRight CH\u01AFA \u0110\u01AF\u1EE2C \u0110\u1ECANH NGH\u0128A tr\xEAn h\u1EC7 th\u1ED1ng acc: ${n.join(", ")} \u2014 preview c\u1EA5p s\u1EB5n n\xEAn control v\u1EABn `+"hi\u1EC7n \u1EDF \u0110\xC2Y, nh\u01B0ng ch\u1EA1y th\u1EADt th\xEC rightTest() lu\xF4n tr\u1EA3 false \u2192 control/menu bi\u1EBFn m\u1EA5t m\xE0 kh\xF4ng c\xF3 l\u1ED7i JS n\xE0o. "+'T\u1EA1o m\xE3: right_function_new({ functionCode: \u2026, functionName: "\u2026" }); ho\u1EB7c right_function_list n\u1EBFu cache c\u0169.'}function kc(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 xm=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-openwindow-control","f-table-item-component"]),vc=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"]),Om={"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 dn=new Set(["API","CALL","IF","FUN","EXE","MESS","MESSBOX","ARRAY","CONFIRM","IN","OUT","THEN","ELSE","CALLBACK","ERROROUT","ERRORCALLBACK","CANCEL","METHOD","HEADER","COL"]),Sc=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 ms(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)=>dn.has(n)));if(!j(e))return!1;return Object.keys(e).some((t)=>dn.has(t))}function ze(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 Gn(e){return!!e&&!e.includes(" ")&&!e.includes("{{")&&!e.includes("(")&&!e.startsWith("`")&&!e.startsWith("#")&&!e.startsWith("!")&&/^[A-Za-z_$]/.test(e.replace(/^vueData\./,""))}function Mm(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 Fm(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 Lm=[{re:/\bfunction\s*\(/,what:'khai b\xE1o "function(...)"'},{re:/\bfunction\s+[A-Za-z_$]/,what:'khai b\xE1o "function ten(...)"'},{re:/\breturn\b/,what:'l\u1EC7nh "return"'},{re:/\bvar\s+[A-Za-z_$]/,what:'khai b\xE1o "var"'},{re:/\blet\s+[A-Za-z_$]/,what:'khai b\xE1o "let"'},{re:/\bconst\s+[A-Za-z_$]/,what:'khai b\xE1o "const"'}];function Um(e,t,n,o){if(typeof t!=="string")return;if(!(e.startsWith(":")&&!e.startsWith("::")||e.startsWith("v-bind:")))return;let r=Fm(t),i=[];for(let{re:a,what:c}of Lm)if(a.test(r))i.push(c);if(r.replace(/;\s*$/,"").includes(";"))i.push('d\u1EA5u ";" n\u1ED1i nhi\u1EC1u c\xE2u l\u1EC7nh');if(i.length===0)return;o.push(`attr "${e}" (t\u1EA1i ${n}) ch\u1EE9a ${i.join(" + ")} \u2014 gi\xE1 tr\u1ECB binding \u0111i th\u1EB3ng v\xE0o template compiler c\u1EE7a Vue, `+"ch\u1EC9 nh\u1EADn M\u1ED8T BI\u1EC2U TH\u1EE8C thu\u1EA7n (object/array literal, ternary, chu\u1ED7i .map()/.filter() arrow, truy c\u1EADp thu\u1ED9c t\xEDnh). "+"Statement \u1EDF \u0111\xE2y l\xE0m compile template h\u1ECFng \u2192 TRANG TR\u1EAENG m\xE0 kh\xF4ng c\xF3 JS error hay [Vue warn] n\xE0o. "+'C\xE1ch s\u1EEDa: \u0111\u01B0a logic v\xE0o m\u1ED9t h\xE0m trong script.js (function buildX(){ \u2026 vueData.x = \u2026; }), g\u1ECDi qua "EXE": "buildX()" '+`trong CALLBACK c\u1EE7a action n\u1EA1p d\u1EEF li\u1EC7u, r\u1ED3i "${e}" ch\u1EC9 tr\u1ECF t\u1EDBi bi\u1EBFn vueData \u0111\xF3 (vd "${e}": "x"). `+"Chi ti\u1EBFt: references/advanced-techniques.md.")}function Fe(e,t,n="$",o=!1){if(Array.isArray(e))e.forEach((s,r)=>Fe(s,t,`${n}[${r}]`,o));else if(j(e)){t(e,n,o);for(let[s,r]of Object.entries(e))Fe(r,t,`${n}.${s}`,o||s.startsWith("#"))}}var Hm="(chu\u1EA9n m\u1EB7c \u0111\u1ECBnh fui/DESIGN.md \u2014 ch\u1EC9 gi\u1EEF nguy\xEAn n\u1EBFu user y\xEAu c\u1EA7u r\xF5 ki\u1EC3u kh\xE1c)",Bm=new Set([400,550,600,650,800]),qm=/(x\u00F3a|xo\u00E1|\bdelete\b|\bremove\b)/i,Vm=/(delete|remove|xoa)/i,Wm=new Set(["v-text-field","v-textarea","v-select","v-combobox","v-autocomplete","f-date","f-time","f-search"]),$c=new Set(["9"]);function Jm(e,t,n){let o=JSON.stringify(e),s=Lt([o]),r=bc([o]).filter((l)=>!$c.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\xE1c quy\u1EC1n b\u1EB1ng ${i} m\xE3 literal (${[...s,...r].join(", ")}) nh\u01B0ng ch\u01B0a c\xF3 cache _db/rights.json `+"\u0111\u1EC3 \u0111\u1ED1i chi\u1EBFu \u2014 KH\xD4NG ki\u1EC3m \u0111\u01B0\u1EE3c m\xE3 n\xE0o c\xF3 th\u1EADt. M\xE3 kh\xF4ng t\u1ED3n t\u1EA1i l\xE0m rightTest() lu\xF4n tr\u1EA3 false: "+"control/menu bi\u1EBFn m\u1EA5t im l\u1EB7ng, kh\xF4ng l\u1ED7i JS. Ch\u1EA1y right_function_list + right_system_list (m\u1ED9t l\u1EA7n cho m\u1ED7i project) r\u1ED3i validate l\u1EA1i."),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\u01B0a \u0111\u01B0\u1EE3c \u0111\u1ECBnh ngh\u0129a cho project (_db/rights.json c\xF3 ${l.size} m\xE3`+`${l.size?`: ${Array.from(l).slice(0,15).join(", ")}${l.size>15?", \u2026":""}`:""}). `+"rightTest() s\u1EBD lu\xF4n tr\u1EA3 false \u2192 control/menu g\xE1c b\u1EB1ng m\xE3 n\xE0y bi\u1EBFn m\u1EA5t m\xE0 kh\xF4ng b\xE1o l\u1ED7i. "+`T\u1EA1o m\xE3: right_function_new({ functionCode: ${/^\d+$/.test(d)?d:`"${d}"`}, functionName: "\u2026" }); `+"ho\u1EB7c ch\u1EA1y right_function_list n\u1EBFu m\xE3 \u0111\xE3 c\xF3 tr\xEAn server m\xE0 cache c\u0169.")}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\u01B0a \u0111\u01B0\u1EE3c \u0111\u1ECBnh ngh\u0129a cho project (_db/rights.json c\xF3: `+`${l.size?Array.from(l).sort().join(", "):"(ch\u01B0a m\u1EE9c n\xE0o)"}${$c.size?" \u2014 m\u1EE9c 9 lu\xF4n h\u1EE3p l\u1EC7":""}). `+"Kh\xF4ng ai \u0111\u01B0\u1EE3c c\u1EA5p m\u1EE9c n\xE0y th\xEC control g\xE1c b\u1EB1ng n\xF3 bi\u1EBFn m\u1EA5t im l\u1EB7ng. "+`\u0110\u1ECBnh ngh\u0129a: right_system_new({ sysRight: ${d}, note: "\u2026" }); ho\u1EB7c ch\u1EA1y right_system_list n\u1EBFu cache c\u0169.`)}return i}function Km(e,t){let n=(a)=>t.push(`[design] ${a} ${Hm}`),o=(a)=>typeof a==="string"?a:"";Fe(e,(a,c)=>{let l=a.API;if(typeof l==="string"&&Vm.test(l)&&a.CONFIRM===void 0)n(`Action g\u1ECDi API "${l}" (t\u1EA1i ${c}) c\xF3 v\u1EBB l\xE0 thao t\xE1c X\xD3A nh\u01B0ng thi\u1EBFu CONFIRM \u2014 m\u1ECDi action x\xF3a ph\u1EA3i h\u1ECFi x\xE1c nh\u1EADn.`)});let s=new Set;Fe(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(!qm.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\xFAt/m\u1EE5c x\xF3a (t\u1EA1i ${c}) \u0111ang m\xE0u "${p}" \u2014 n\xFAt x\xF3a d\xF9ng m\xE0u "warning" (cam), kh\xF4ng d\xF9ng error/\u0111\u1ECF.`)});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\u1EA1i ${c}) c\xF3 color/dark \u2014 toolbar ti\xEAu \u0111\u1EC1 ph\u1EA3i "dense flat" c\xF9ng m\xE0u n\u1EC1n v\u1EDBi th\xE2n dialog, kh\xF4ng t\xF4 m\xE0u/dark ri\xEAng.`);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)&&!Bm.has(g))n(`Dialog (t\u1EA1i ${c}) width=${g} \u2014 b\u1ED9 width chu\u1EA9n: 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\u1EA1i ${c}) c\xF3 elevation/outlined \u2014 b\u1ECF \u0111i: v-dialog \u0111\xE3 t\u1EF1 c\xF3 b\xF3ng \u0111\u1ED5, th\xEAm n\u1EEFa l\xE0 b\xF3ng ch\u1ED3ng b\xF3ng. v-card ch\u1EC9 c\u1EA7n class "pa-0" (tri\u1EC7t \u0111\u1EC7m \u0111\u1EC3 toolbar ti\xEAu \u0111\u1EC1 s\xE1t m\xE9p).`);let v=o(m.class),E=v.includes("flex-md-grow-0")&&v.includes("ml-2");if(E)n(`Control (t\u1EA1i ${c}) mang class "flex-md-grow-0 ml-2" \u2014 d\u1EA5u hi\u1EC7u pattern label-div r\u1EDDi \u0111\xE3 lo\u1EA1i b\u1ECF; form d\xF9ng floating label ("label" trong attr c\u1EE7a field).`);if(w&&!E&&Wm.has(d)&&!("label"in m)&&!(":label"in m)&&!("placeholder"in m))n(`${d} trong dialog (t\u1EA1i ${c}) thi\u1EBFu "label" \u2014 form dialog d\xF9ng floating label.`);if(d==="v-snackbar"||d==="v-alert")n(`${d} (t\u1EA1i ${c}) \u2014 ph\u1EA3n h\u1ED3i/th\xF4ng b\xE1o d\xF9ng h\u1EC7 th\u1ED1ng c\xF3 s\u1EB5n c\u1EE7a FUI: MESS (toast) / MESSBOX (h\u1ED9p tho\u1EA1i) trong action, CONFIRM \u0111\u1EC3 x\xE1c nh\u1EADn \u2014 kh\xF4ng t\u1EF1 d\u1EF1ng ${d} trong controls.`);if(d==="f-table"||d==="f-sheet"){let I=m[":height"]??m.height;if(I===void 0)n(`${d} (t\u1EA1i ${c}) kh\xF4ng c\xF3 ":height" \u2014 b\u1EA3ng s\u1EBD t\u1EF1 gi\xE3n theo s\u1ED1 d\xF2ng v\xE0 l\xE0m c\u1EA3 TRANG cu\u1ED9n. B\u1EA3ng data ch\xEDnh n\xEAn \u0111\u1EB7t \`":height": -16\` (fill t\u1EDBi \u0111\xE1y m\xE0n h\xECnh, ch\u1EEBa 16px \u2014 component t\u1EF1 \u0111o v\u1ECB tr\xED th\u1EADt, kh\xF4ng c\u1EA7n t\xEDnh tay); trong dialog d\xF9ng s\u1ED1 d\u01B0\u01A1ng (px c\u1ED1 \u0111\u1ECBnh, vd 360). B\u1EA3ng ng\u1EAFn/lookup ch\u1EE7 \xFD t\u1EF1 gi\xE3n th\xEC b\u1ECF qua.`);else if(typeof I==="string"&&/\$\s*\(\s*window\s*\)\s*\.\s*height\s*\(\s*\)/.test(I))n(`${d} (t\u1EA1i ${c}) t\xEDnh ":height" th\u1EE7 c\xF4ng theo viewport (\`${I}\`) \u2014 kh\xF4ng c\u1EA7n n\u1EEFa: ${d} t\u1EF1 \u0111o v\u1ECB tr\xED th\u1EADt c\u1EE7a b\u1EA3ng sau layout v\xE0 t\u1EF1 c\u1EADp nh\u1EADt khi resize. Thay b\u1EB1ng \`":height": -16\` (s\u1ED1 \xE2m = kho\u1EA3ng ch\u1EEBa \u1EDF \u0111\xE1y).`)}if(y&&(d==="f-button"||d==="v-btn")){let I=o(m.label)||(typeof a.innerHTML==="string"?a.innerHTML:"");if(/^\s*(\u0111\u00F3ng|dong|close)\s*$/i.test(I))n(`N\xFAt "${I.trim()}" trong v-card-actions (t\u1EA1i ${c}) \u2014 dialog chu\u1EA9n ch\u1EC9 d\xF9ng icon close tr\xEAn toolbar, kh\xF4ng th\xEAm n\xFAt \u0110\xF3ng \u1EDF 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 Gm=12,ds=25,Ic=/\b(shrink|flex-grow-0)\b/;function Cc(e){return typeof e==="string"?e:j(e)?[e.class,e[":class"]].filter((t)=>typeof t==="string").join(" "):""}function zm(e){return e.w===void 0||e.w===null||e.w===""||e.w===0}function Xm(e,t){Fe(e.controls,(n,o)=>{if(!Array.isArray(n.cols))return;Ym(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<=Gm)return;let l=`${o}.cols[${r}]${typeof i.el==="string"?` (el: ${i.el})`:""}`;if(c<ds){t.push(`${l} c\xF3 "w": ${c} \u2014 v\xF9ng ch\u1EBFt 13\u201324: V2 hi\u1EC3u l\xE0 ${c}px, c\xF2n V3 sinh class "v-col-${c}" m\xE0 Vuetify 3 kh\xF4ng \u0111\u1ECBnh ngh\u0129a (ch\u1EC9 c\xF3 .v-col-1 \u2026 .v-col-12) n\xEAn c\u1ED9t KH\xD4NG c\xF3 b\u1EC1 r\u1ED9ng n\xE0o, r\u01A1i v\u1EC1 chia \u0111\u1EC1u. D\xF9ng 1\u201312 (chia c\u1ED9t) ho\u1EB7c >= ${ds} (px, k\xE8m \`"col": { "class": "shrink" }\`).`);return}let u=i.col;if(Ic.test(Cc(u)))return;t.push(j(u)&&"shrink"in u?`${l} c\xF3 "w": ${c} (px) k\xE8m \`"col": { "shrink": ... }\` \u2014 d\u1EA1ng thu\u1ED9c t\xEDnh tr\u1EA7n l\xE0 prop c\u1EE7a v-flex (Vuetify 2); Vuetify 3 kh\xF4ng c\xF3 prop \u0111\xF3 n\xEAn n\xF3 tr\u01A1 v\xE0 c\u1ED9t v\u1EABn gi\xE3n h\u1EBFt ch\u1ED7 tr\u1ED1ng. Vi\u1EBFt \`"col": { "class": "shrink" }\` \u0111\u1EC3 \u0111\xFAng \u1EDF c\u1EA3 hai framework.`:`${l} c\xF3 "w": ${c} \u2014 s\u1ED1 >= ${ds} ngh\u0129a l\xE0 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh px, nh\u01B0ng c\u1ED9t grid m\u1EB7c \u0111\u1ECBnh flex-grow:1 n\xEAn flex-basis ${c}px KH\xD4NG c\xF3 t\xE1c d\u1EE5ng: c\u1ED9t v\u1EABn gi\xE3n h\u1EBFt ch\u1ED7 tr\u1ED1ng (\u0111o th\u1EADt: w=300 ra 687px \u1EDF V2, 738px \u1EDF V3). Th\xEAm \`"col": { "class": "shrink" }\`.`)})},"$.controls")}function Ym(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=Ic.test(Cc(c));if(i.el==="v-spacer"){if(l)n.push(`${a} \u2014 \`v-spacer\` m\xE0 c\xF3 \`shrink\` th\xEC co l\u1EA1i c\xF2n 0px (\u0111o th\u1EADt: 24px \u1EDF V3, 0px \u1EDF V2), kh\xF4ng \u0111\u1EA9y \u0111\u01B0\u1EE3c nh\xF3m sau sang ph\u1EA3i n\u1EEFa v\xE0 c\u1EA3 h\xE0ng d\u1ED3n v\u1EC1 tr\xE1i. B\u1ECF \`shrink\` kh\u1ECFi ri\xEAng c\u1ED9t n\xE0y.`);return}if(!zm(i)||l)return;n.push(j(c)&&"shrink"in c?`${a} n\u1EB1m c\xF9ng h\xE0ng v\u1EDBi \`v-spacer\`, \`w\` r\u1ED7ng v\xE0 ch\u1EC9 c\xF3 \`"col": { "shrink": ... }\` \u2014 d\u1EA1ng thu\u1ED9c t\xEDnh tr\u1EA7n l\xE0 prop c\u1EE7a v-flex (Vuetify 2); Vuetify 3 kh\xF4ng c\xF3 prop \u0111\xF3 n\xEAn c\u1ED9t v\u1EABn gi\xE3n v\xE0 chi\u1EBFm m\u1ED9t ph\u1EA7n \u0111\u1EC1u. Vi\u1EBFt \`"col": "shrink"\` (ho\u1EB7c \`{ "class": "shrink" }\`).`:`${a} n\u1EB1m c\xF9ng h\xE0ng v\u1EDBi \`v-spacer\` nh\u01B0ng thi\u1EBFu \`shrink\` \u2014 \`w\` r\u1ED7ng KH\xD4NG c\xF3 ngh\u0129a "t\u1EF1 co theo n\u1ED9i dung": runtime b\u1ECDc ph\u1EA7n b\u1EC1 r\u1ED9ng trong \`if (c.w)\` n\xEAn c\u1ED9t n\xE0y kh\xF4ng c\xF3 class grid l\u1EABn flex-basis, c\xF2n tr\u01A1 \`flex-grow: 1\` v\xE0 \u0111\u01B0\u1EE3c CHIA PH\u1EA6N TR\u1ED0NG y nh\u01B0 \`v-spacer\`. H\u1EC7 qu\u1EA3: control b\u1ECB k\xE9o d\xE3n, h\u1EDF kho\u1EA3ng gi\u1EEFa c\xE1c n\xFAt, nh\xF3m cu\u1ED1i h\xE0ng kh\xF4ng s\xE1t m\xE9p ph\u1EA3i. Chu\u1EA9n cho h\xE0ng nhi\u1EC1u control: M\u1ECCI c\u1ED9t \`"col": "shrink"\`, tr\u1EEB \u0111\xFAng \`v-spacer\`.`)})}var jc=["API","CALL","EXE","FUN","IF","MESS","MESSBOX"],Qm=new Set(["IN","OUT","HEADER","METHOD","CALLBACK","ERROROUT","ERRORCALLBACK","THEN","ELSE","CONFIRM","CANCEL","ARRAY","COL","MAP"]);function Zm(e,t,n){for(let[o,s]of Object.entries(e)){if(o==="set")continue;Fe(s,(r,i)=>{let a=Object.keys(r),c=jc.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\u1ED9p ${c.length} l\u1EC7nh trong M\u1ED8T object \`{}\` (${c.join(", ")}) \u2014 ch\u1EC9 \`${l}\` ch\u1EA1y, c\xF2n ${d.map((m)=>`\`${m}\``).join(", ")} b\u1ECB b\u1ECF IM L\u1EB6NG (kh\xF4ng JS error, kh\xF4ng [Vue warn]). \`vueAction\` d\xF2 key l\u1EC7nh theo th\u1EE9 t\u1EF1 c\u1ED1 \u0111\u1ECBnh API \u2192 CALL \u2192 EXE \u2192 FUN \u2192 IF \u2192 MESS \u2192 MESSBOX v\xE0 \`return\` ngay \u1EDF nh\xE1nh \u0111\u1EA7u ti\xEAn kh\u1EDBp, n\xEAn th\u1EE9 t\u1EF1 vi\u1EBFt kh\xF4ng c\xF3 t\xE1c d\u1EE5ng. Nhi\u1EC1u l\u1EC7nh c\xF3 th\u1EE9 t\u1EF1 tr\u01B0\u1EDBc sau th\xEC ph\u1EA3i l\xE0 M\u1EA2NG, m\u1ED7i l\u1EC7nh m\u1ED9t object: \`[{ "${c[0]}": \u2026 }, { "${c[1]}": \u2026 }]\`.`);return}let p=a.filter((m)=>!u.has(m)&&!Qm.has(m)&&!jc.includes(m));if(p.length>0)n.push(`${i} c\xF3 l\u1EC7nh \`${l}\` n\u1EB1m chung object v\u1EDBi key g\xE1n d\u1EEF li\u1EC7u ${p.map((m)=>`\`${m}\``).join(", ")} \u2014 fragment g\xE1n ch\u1EA1y \u1EDF nh\xE1nh CU\u1ED0I c\u1EE7a \`vueAction\`, nh\xE1nh \u0111\xF3 ch\u1EC9 t\u1EDBi l\u01B0\u1EE3t khi KH\xD4NG key l\u1EC7nh n\xE0o kh\u1EDBp, n\xEAn c\xE1c key n\xE0y kh\xF4ng bao gi\u1EDD \u0111\u01B0\u1EE3c g\xE1n. T\xE1ch th\xE0nh m\u1EA3ng: \`[{ ${p.map((m)=>`"${m}": \u2026`).join(", ")} }, { "${l}": \u2026 }]\`.`)},`$.${o}`)}}function eh(e,t){let n=[],o=[];if(t.schemaWarning)o.push(t.schemaWarning);if(!j(e))return{errors:["module.json g\u1ED1c ph\u1EA3i l\xE0 m\u1ED9t object v\u1EDBi c\xE1c kh\u1ED1i data/watch/controls/set."],warnings:o,stats:ps()};let s=e,r=new Set(["data","watch","controls","set"]);for(let g of Object.keys(s))if(!r.has(g))o.push(`Kh\u1ED1i c\u1EA5p 1 kh\xF4ng chu\u1EA9n: "${g}" \u2014 module.json ch\u1EC9 c\xF3 data/watch/controls/set (xem module-json-anatomy.md).`);if(s.data!==void 0&&!Array.isArray(s.data))n.push(`"data" ph\u1EA3i l\xE0 M\u1EA2NG (array), hi\u1EC7n l\xE0 ${typeof s.data}.`);if(s.watch!==void 0&&!j(s.watch))n.push(`"watch" ph\u1EA3i l\xE0 OBJECT, hi\u1EC7n l\xE0 ${Array.isArray(s.watch)?"array":typeof s.watch}.`);if(s.controls!==void 0&&!Array.isArray(s.controls))n.push(`"controls" ph\u1EA3i l\xE0 M\u1EA2NG container, hi\u1EC7n l\xE0 ${typeof s.controls}.`);if(s.set!==void 0&&!j(s.set))n.push(`"set" ph\u1EA3i l\xE0 OBJECT, hi\u1EC7n l\xE0 ${Array.isArray(s.set)?"array":typeof s.set}.`);let i=new Set(Sc),a=new Set,c=Array.isArray(s.data)?s.data:[];for(let g of c){if(!j(g)){o.push(`data[] ch\u1EE9a ph\u1EA7n t\u1EED kh\xF4ng ph\u1EA3i object: ${JSON.stringify(g)?.slice(0,60)}`);continue}for(let[b,R]of Object.entries(g)){if(dn.has(b))continue;if(b.startsWith("#"))continue;if(ms(R))a.add(b);else i.add(ze(b))}}let l=0;Fe(s,(g,b,R)=>{if(R)return;for(let C of["OUT","ERROROUT"]){let S=g[C];if(typeof S==="string"){if(Gn(S))i.add(ze(S));else if(S.trim())l++}else if(j(S))for(let N of Object.keys(S))i.add(ze(N))}});let u=new Map,d=new Map;Fe(s,(g,b,R)=>{let C=g.CALL;if(typeof C==="string"){if(Gn(C))(R?d:u).set(ze(C),b);else if(C.trim()&&!R)l++}let S=g.API;if(typeof S==="string"&&S.trim()&&!S.startsWith("/")&&!Gn(S))l++;for(let[N,D]of Object.entries(g)){if(typeof D!=="string")continue;let A=R||N.startsWith("#")?d:u;for(let G of D.matchAll(/\bCALL\(\s*([A-Za-z_$][\w$.]*)\s*\)/g))A.set(ze(G[1]),b)}});for(let[g,b]of u)if(!a.has(g))n.push(`CALL "${g}" (t\u1EA1i ${b}) kh\xF4ng kh\u1EDBp named action n\xE0o trong data[]. Named actions hi\u1EC7n c\xF3: ${[...a].join(", ")||"(kh\xF4ng c\xF3)"}.`);let p=0,m=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\u1EA3i l\xE0 container c\xF3 "rows" (grid b\u1EAFt bu\u1ED9c: 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\u1EA3i c\xF3 "cols" (m\u1EA3ng Control Object).`);continue}for(let S=0;S<C.cols.length;S++){let N=C.cols[S];if(m++,!j(N))continue;if(N.w===void 0&&N.el!==void 0)o.push(`controls[${g}].rows[${R}].cols[${S}] (el: ${String(N.el)}) thi\u1EBFu "w" \u2014 b\u1EAFt bu\u1ED9c cho item trong cols.`)}}}let y=new Set;if(Fe(s,(g,b)=>{if("children"in g&&(("el"in g)||("rows"in g)||("cols"in g)))n.push(`"children" kh\xF4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3 (t\u1EA1i ${b}) \u2014 d\xF9ng "innerHTML" (m\u1EA3ng Control Object) \u0111\u1EC3 l\u1ED3ng.`);let R=g.el;if(typeof R==="string"&&R){if(y.add(R),(R.startsWith("f-")||R.startsWith("fp-"))&&!xm.has(R)&&!t.localComponents.has(R))o.push(`Component "${R}" (t\u1EA1i ${b}) kh\xF4ng c\xF3 trong registry FUI (component-quickref.md) v\xE0 kh\xF4ng th\u1EA5y trong components local \u2014 ki\u1EC3m tra ch\xEDnh t\u1EA3 ho\u1EB7c ch\u1EA1y component_list.`);else if(R.startsWith("t-")&&!vc.has(R))o.push(`Cell component "${R}" (t\u1EA1i ${b}) kh\xF4ng c\xF3 trong danh s\xE1ch t-* h\u1EE3p l\u1EC7: ${[...vc].join(", ")}.`);else if(R.startsWith("uc-")&&!t.localComponents.has(R))o.push(`Custom component "${R}" (t\u1EA1i ${b}) ch\u01B0a th\u1EA5y file .vue/local metadata \u2014 t\u1EA1o component tr\u01B0\u1EDBc ho\u1EB7c ch\u1EA1y component_list.`)}let C=g.attr;if(j(C))for(let[S,N]of Object.entries(C)){if(S.startsWith("@"))n.push(`attr "${S}" (t\u1EA1i ${b}) \u2014 module.json KH\xD4NG h\u1ED7 tr\u1EE3 shorthand "@"; d\xF9ng "v-on:${S.slice(1)}".`);else if(S.startsWith("#"))n.push(`attr "${S}" (t\u1EA1i ${b}) \u2014 module.json KH\xD4NG h\u1ED7 tr\u1EE3 slot shorthand "#"; d\xF9ng "v-slot:${S.slice(1)}".`);else if((S==="style"||S===":style")&&typeof N==="string"){if(N.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 "${N}" (t\u1EA1i ${b}) \u2014 module th\u1EF1c t\u1EBF g\u1EA7n nh\u01B0 kh\xF4ng d\xF9ng style ngo\xE0i width (controls-styling-vocabulary.md). C\xE2n nh\u1EAFc b\u1ECF.`)}else if((S.startsWith(":")||S.startsWith("v-bind:"))&&typeof N==="string"&&!Mm(N))l++;else if(S==="v-model"&&typeof N==="string"){if(Gn(N)){let D=ze(N);if(!i.has(D)&&!a.has(D))o.push(`v-model "${N}" (t\u1EA1i ${b}) \u2014 bi\u1EBFn g\u1ED1c "${D}" ch\u01B0a khai b\xE1o trong data[]. Khai b\xE1o state tr\u01B0\u1EDBc \u0111\u1EC3 reactive ho\u1EA1t \u0111\u1ED9ng \u0111\xFAng.`)}else if(N.trim())l++}Um(S,N,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=ze(C);if(!i.has(S))o.push(`watch "${C}" (${R}) \u2014 bi\u1EBFn "${S}" ch\u01B0a khai b\xE1o trong data[], watcher s\u1EBD kh\xF4ng bao gi\u1EDD ch\u1EA1y.`)}};g(s.watch,"watch")}for(let g of y){let b=Om[g];if(!b)continue;let R=b.filter((C)=>!t.importsText.includes(C.toLowerCase()));if(R.length>0)n.push(`"${g}" c\u1EA7n import: ${R.join(", ")} \u2014 ch\u01B0a th\u1EA5y trong _imports.json (module l\u1EABn project). D\xF9ng file_import_list/file_import_new (xem component-quickref.md).`)}let v=0,E=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(Fe(s,(b,R)=>{let C=b.API;if(typeof C!=="string"||!C||C.includes("{{"))return;v++;let S=null,N=null;if(C.startsWith("/")){let A=C.replace(/^\/+/,"").split("/").filter(Boolean);if(A.length<2)return;if(N=t.schemaProcs.get(A[0].toLowerCase())??null,!N){E.add(A[0]);return}S=A[1]==="auth"?A[2]??null:A[1]}else if(Gn(C))S=C,N=g;if(!S||S.includes("`")||!N)return;if(![`spAPI_${S}`,`spAPI_AUTH_${S}`,`spAPIFILE_${S}`,`spAPIFILE_UPLOAD_${S}`,S].some((A)=>N.has(A)))o.push(`API "${C}" (t\u1EA1i ${R}) \u2014 kh\xF4ng t\xECm th\u1EA5y spAPI_${S} trong schema local. Deploy SP tr\u01B0\u1EDBc ho\u1EB7c ch\u1EA1y db_schema_get (b\u1ECF qua n\u1EBFu API thu\u1ED9c backend kh\xE1c).`)}),E.size>0){let b=[...E].sort();o.push(`${b.length} alias \u0111\u01B0\u1EE3c g\u1ECDi nh\u01B0ng ch\u01B0a c\xF3 k\u1EBFt n\u1ED1i local \u2014 KH\xD4NG \u0111\u1ED1i chi\u1EBFu \u0111\u01B0\u1EE3c t\xEAn SP: ${b.join(", ")}. `+`K\u1EBFt n\u1ED1i t\u1EEBng c\xE1i b\u1EB1ng db_connect({ apiName: "${b[0]}", apiDomain: "\u2026", dbToken: "\u2026" }) \u0111\u1EC3 b\u01B0\u1EDBc ki\u1EC3m n\xE0y ch\u1EA1y (b\u1ECF qua n\u1EBFu alias thu\u1ED9c backend c\u1EE7a nh\xF3m kh\xE1c).`)}}let I=Jm(s,t,o);return Km(s,o),Xm(s,o),Zm(s,n,o),{errors:n,warnings:o,stats:{stateKeys:i.size-Sc.size,namedActions:a.size,containers:p,controls:m,callRefs:u.size,apiRefs:v,crossWindowCalls:d.size,dynamicRefs:l,rightRefs:I}}}function ps(){return{stateKeys:0,namedActions:0,containers:0,controls:0,callRefs:0,apiRefs:0,crossWindowCalls:0,dynamicRefs:0,rightRefs:0}}function th(e){return e.match(/^CREATE (?:PROCEDURE|FUNCTION)\s+(\S+)\s*\(/i)?.[1]??null}async function nh(e){let t=ct.dirname(ct.dirname(e)),n=new Set;for(let c of[ct.join(e,"components"),ct.join(t,"components")]){for(let u of await Me(c))n.add(u.replace(/\.vue$/i,""));let l=await U(ct.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(ct.join(c,"imports","_imports.json")));let s=await U(ct.join(t,"_db","_connections.json"),null),r=new Map,i=[];for(let c of Object.keys(s?.connections??{})){let l=await U(ct.join(t,"_db",c,"schema.json"),null),u=Array.isArray(l?.procedures)?l.procedures:null;if(u===null)continue;if(!u.every((d)=>typeof d==="string")){i.push(c);continue}r.set(c.toLowerCase(),new Set(u.map(th).filter((d)=>d!==null)))}let a=await Ft(t);return{localComponents:n,importsText:o.join(`
154
- `).toLowerCase(),schemaProcs:r.size>0?r:null,schemaWarning:i.length>0?`_db/${i.join("/schema.json, _db/")}/schema.json sai \u0111\u1ECBnh d\u1EA1ng (b\u1EA3n cache c\u0169) \u2014 b\u1ECF qua b\u01B0\u1EDBc \u0111\u1ED1i chi\u1EBFu t\xEAn API v\u1EDBi stored procedure \u1EDF ${i.length>1?"c\xE1c k\u1EBFt n\u1ED1i n\xE0y":"k\u1EBFt n\u1ED1i n\xE0y"}. Ch\u1EA1y db_schema_get \u0111\u1EC3 l\u1EA5y b\u1EA3n m\u1EDBi.`:null,definedFunctionRights:a.functionRights,definedSystemRights:a.systemRights}}async function No(e){let t=await F(ct.join(e,"module.json"));if(!t.trim())return{errors:[`module.json r\u1ED7ng ho\u1EB7c kh\xF4ng t\u1ED3n t\u1EA1i trong ${e}.`],warnings:[],stats:ps()};let n;try{n=JSON.parse(t)}catch(s){return{errors:[`module.json c\xF3 l\u1ED7i JSON syntax: ${s.message}`],warnings:[],stats:ps()}}let o=await nh(e);return eh(n,o)}function _o(e){let t=[];if(e.errors.length===0&&e.warnings.length===0)t.push("\u2705 module.json h\u1EE3p l\u1EC7 \u2014 kh\xF4ng ph\xE1t hi\u1EC7n l\u1ED7i ng\u1EEF ngh\u0129a.");else{if(e.errors.length>0)t.push(`\u274C ${e.errors.length} l\u1ED7i (s\u1EBD h\u1ECFng khi ch\u1EA1y):`),t.push(...e.errors.map((o)=>` - ${o}`));if(e.warnings.length>0){if(t.length)t.push("");t.push(`\u26A0\uFE0F ${e.warnings.length} c\u1EA3nh b\xE1o (n\xEAn xem l\u1EA1i):`),t.push(...e.warnings.map((o)=>` - ${o}`))}}let n=e.stats;if(t.push(""),n.dynamicRefs>0)t.push(`\u2139\uFE0F ${n.dynamicRefs} bi\u1EC3u th\u1EE9c \u0111\u1ED9ng (\`:attr\` ph\u1EE9c t\u1EA1p, CALL/API/v-model/OUT d\u1EA1ng \`{{\u2026}}\`/g\u1ECDi h\xE0m/template string) KH\xD4NG ki\u1EC3m t\u0129nh \u0111\u01B0\u1EE3c \u2014 bi\u1EBFn b\xEAn trong ch\xFAng kh\xF4ng \u0111\u01B0\u1EE3c \u0111\u1ED1i chi\u1EBFu v\u1EDBi data[]. "H\u1EE3p l\u1EC7" \u1EDF \u0111\xE2y KH\xD4NG c\xF3 ngh\u0129a l\xE0 ch\u1EA1y \u0111\xFAng: d\xF9ng module_simulate \u0111\u1EC3 ph\u1EE7 ph\u1EA7n n\xE0y.`);return t.push(`Th\u1ED1ng k\xEA: ${n.stateKeys} state \xB7 ${n.namedActions} named action \xB7 ${n.containers} container / ${n.controls} control \xB7 ${n.callRefs} CALL ref${n.apiRefs?` \xB7 ${n.apiRefs} API ref \u0111\u1ED1i chi\u1EBFu schema`:""}${n.crossWindowCalls?` \xB7 ${n.crossWindowCalls} CALL xuy\xEAn c\u1EEDa s\u1ED5 (thu\u1ED9c module kh\xE1c, kh\xF4ng ki\u1EC3m \u0111\u01B0\u1EE3c t\u1EEB \u0111\xE2y)`:""}${n.rightRefs?` \xB7 ${n.rightRefs} m\xE3 quy\u1EC1n \u0111\u1ED1i chi\u1EBFu _db/rights.json`:""}`),t.join(`
155
- `)}function ee(e,t){let n=e.replace(/\s+/g," ").trim();return n.length>t?n.slice(0,t)+"\u2026":n}function zn(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 _e(e,t=0){if(t>3)return"\u2026";if(typeof e==="string")return ee(e,70);if(Array.isArray(e))return e.map((o)=>_e(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\u2192${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[${_e(e.THEN,t+1)}]`);if(e.ELSE!==void 0)n.push(`ELSE[${_e(e.ELSE,t+1)}]`);if(e.CALLBACK!==void 0)n.push(`CALLBACK[${_e(e.CALLBACK,t+1)}]`);if(e.ERROROUT!==void 0)n.push(`ERROROUT\u2192${String(e.ERROROUT)}`);if(e.ERRORCALLBACK!==void 0)n.push(`ERRORCALLBACK[${_e(e.ERRORCALLBACK,t+1)}]`);if(e.CANCEL!==void 0)n.push(`CANCEL[${_e(e.CANCEL,t+1)}]`);return n.join(" ")||ee(JSON.stringify(e)??"",60)}var oh=["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 rh(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 oh){let i=s[r];if(i===void 0)continue;t.push(`${r}=${typeof i==="string"?ee(i,45):zn(i)}`)}for(let[r,i]of Object.entries(s)){if(!r.startsWith("v-on:"))continue;let a=r.slice(5);t.push(`${a}\u2192${typeof i==="string"?ee(i,60):_e(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 Rc(e,t,n,o){let s=o?"\u2514\u2500":"\u251C\u2500",r=t+(o?" ":"\u2502 ");if(!j(e)){n.push(`${t}${s} ${ee(JSON.stringify(e)??String(e),60)}`);return}n.push(`${t}${s} ${rh(e)}`);let i=e.innerHTML;if(Array.isArray(i))i.forEach((a,c)=>Rc(a,r,n,c===i.length-1));if(Array.isArray(e.rows))Pc(e.rows,r,n)}function Pc(e,t,n){e.forEach((o,s)=>{if(!j(o)||!Array.isArray(o.cols)){n.push(`${t}row ${s+1}: \u274C thi\u1EBFu "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=` (\u03A3w=${u}${u>12?" \u26A0 v\u01B0\u1EE3t 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)=>Rc(u,t+" ",n,d===r.length-1))})}function Tc(e){if(!j(e))return"\u274C module.json g\u1ED1c kh\xF4ng ph\u1EA3i 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)=>dn.has(m))){r.push(d);continue}for(let[m,w]of Object.entries(d)){if(dn.has(m)){r.push({[m]:w});continue}if(m.startsWith("#"))continue;if(ms(w))s.set(m,w);else o.set(ze(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("\u2550\u2550\u2550 LAYOUT (controls) \u2550\u2550\u2550");let l=Array.isArray(t.controls)?t.controls:[];if(l.length===0)n.push("(kh\xF4ng c\xF3 controls)");let u=[];if(l.forEach((d,p)=>{if(!j(d)){n.push(`[container ${p+1}] \u274C kh\xF4ng ph\u1EA3i 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 E of["v-if","v-show","v-model"]){let I=d[E]??y[E];if(typeof I==="string")v.push(`${E}=${ee(I,40)}`)}if(n.push(`[container ${p+1}${w?" \u2014 DIALOG":""}] ${v.join(" ")||"(m\u1EB7c \u0111\u1ECBnh)"}`),w){let E=d["v-model"]??y["v-model"];if(typeof E==="string")u.push({vModel:ze(E),where:`container ${p+1}`});else u.push({vModel:"",where:`container ${p+1}`})}if(Array.isArray(d.rows))Pc(d.rows," ",n);else n.push(' \u274C thi\u1EBFu "rows"')}),n.push("","\u2550\u2550\u2550 STATE (data) \u2550\u2550\u2550"),o.size===0)n.push("(kh\xF4ng c\xF3 state)");for(let[d,p]of o){let m=c(d);n.push(`${d} = ${zn(p)}${m?"":" \u26A0 kh\xF4ng th\u1EA5y d\xF9ng \u1EDF controls/watch/actions"}`)}if(n.push("","\u2550\u2550\u2550 ACTIONS \u2550\u2550\u2550"),s.size===0&&r.length===0)n.push("(kh\xF4ng c\xF3 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}: ${_e(p)}${w?"":" \u26A0 kh\xF4ng th\u1EA5y n\u01A1i g\u1ECDi (CALL/watch/v-on)"}`)}if(r.forEach((d,p)=>{n.push(`(auto-startup ${p+1}): ${_e(d)}`)}),j(t.watch)&&Object.keys(t.watch).length>0){n.push("","\u2550\u2550\u2550 WATCH \u2550\u2550\u2550");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} \u2192 ${_e(y)}`)}};d(t.watch,"")}if(u.length>0){n.push("","\u2550\u2550\u2550 DIALOGS \u2550\u2550\u2550");let d=JSON.stringify(t)??"";for(let p of u){if(!p.vModel){n.push(`${p.where}: \u26A0 hidden-container kh\xF4ng c\xF3 v-model \u2014 kh\xF4ng \u0111i\u1EC1u khi\u1EC3n m\u1EDF/\u0111\xF3ng \u0111\u01B0\u1EE3c`);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?` \u2014 c\xF3 l\u1EC7nh m\u1EDF (${p.vModel} = true)`:` \u26A0 kh\xF4ng th\u1EA5y l\u1EC7nh m\u1EDF "${p.vModel} = true" \u1EDF \u0111\xE2u`}`)}}if(j(t.set)&&Object.keys(t.set).length>0){n.push("","\u2550\u2550\u2550 SET (override) \u2550\u2550\u2550");for(let[d,p]of Object.entries(t.set))n.push(`${d} = ${typeof p==="string"?ee(p,60):zn(p)}`)}return n.push("",`Th\u1ED1ng k\xEA: ${o.size} state \xB7 ${s.size} named action \xB7 ${r.length} auto-startup \xB7 ${l.length} container \xB7 ${u.length} dialog`),n.join(`
156
- `)}var sh=[[/c\u0169 (\d+) ng\u00E0y/g,"$1 days old"],[/c\u0169 (\d+) gi\u1EDD/g,"$1 h old"],[/\(control trong f-table \u2014 d\u00F9ng row (\d+) c\u1EE7a /g,"(control in f-table; using row $1 of "],[/(\u2718 STEP \d+) l\u1ED7i: /g,"$1 error: "]],ih={project_sync:"fui project sync",module_get:"fui module get",file_import_list:"fui import list"},ah={'khai b\xE1o "function(...)"':'"function(...)" declaration','khai b\xE1o "function ten(...)"':'"function name(...)" declaration','l\u1EC7nh "return"':'"return" statement','khai b\xE1o "var"':'"var" declaration','khai b\xE1o "let"':'"let" declaration','khai b\xE1o "const"':'"const" declaration','d\u1EA5u ";" n\u1ED1i nhi\u1EC1u c\xE2u l\u1EC7nh':'";" joining statements','" (t\u1EA1i ':'" (at ',") ch\u1EE9a ":") contains "," \u2014 gi\xE1 tr\u1ECB binding \u0111i th\u1EB3ng v\xE0o template compiler c\u1EE7a Vue, ":" \u2014 Vue compiles a binding as ONE expression ","ch\u1EC9 nh\u1EADn M\u1ED8T BI\u1EC2U TH\u1EE8C thu\u1EA7n (object/array literal, ternary, chu\u1ED7i .map()/.filter() arrow, truy c\u1EADp thu\u1ED9c t\xEDnh). ":"(literal, ternary, .map()/.filter() arrow, property access). ","Statement \u1EDF \u0111\xE2y l\xE0m compile template h\u1ECFng \u2192 TRANG TR\u1EAENG m\xE0 kh\xF4ng c\xF3 JS error hay [Vue warn] n\xE0o. ":"A statement breaks the template: BLANK PAGE with no JS error or [Vue warn]. ",'C\xE1ch s\u1EEDa: \u0111\u01B0a logic v\xE0o m\u1ED9t h\xE0m trong script.js (function buildX(){ \u2026 vueData.x = \u2026; }), g\u1ECDi qua "EXE": "buildX()" ':'Fix: move the logic into a script.js function (function buildX(){ \u2026 vueData.x = \u2026; }), call it with "EXE": "buildX()" ','trong CALLBACK c\u1EE7a action n\u1EA1p d\u1EEF li\u1EC7u, r\u1ED3i "':'in the load action CALLBACK, and point "','" ch\u1EC9 tr\u1ECF t\u1EDBi bi\u1EBFn vueData \u0111\xF3 (vd "':'" at that vueData var (e.g. "',"Chi ti\u1EBFt: references/advanced-techniques.md.":"See: fui skill ref references/advanced-techniques.md.","(chu\u1EA9n m\u1EB7c \u0111\u1ECBnh fui/DESIGN.md \u2014 ch\u1EC9 gi\u1EEF nguy\xEAn n\u1EBFu user y\xEAu c\u1EA7u r\xF5 ki\u1EC3u kh\xE1c)":"(fui/DESIGN.md default; keep only if the user asked for another style)","Module g\xE1c quy\u1EC1n b\u1EB1ng ":"Module gates on "," m\xE3 literal (":" literal right codes (",") nh\u01B0ng ch\u01B0a c\xF3 cache _db/rights.json ":") but _db/rights.json is not cached, ","\u0111\u1EC3 \u0111\u1ED1i chi\u1EBFu \u2014 KH\xD4NG ki\u1EC3m \u0111\u01B0\u1EE3c m\xE3 n\xE0o c\xF3 th\u1EADt. M\xE3 kh\xF4ng t\u1ED3n t\u1EA1i l\xE0m rightTest() lu\xF4n tr\u1EA3 false: ":"so none can be checked. An unknown code makes rightTest() always false: ","control/menu bi\u1EBFn m\u1EA5t im l\u1EB7ng, kh\xF4ng l\u1ED7i JS. Ch\u1EA1y right_function_list + right_system_list (m\u1ED9t l\u1EA7n cho m\u1ED7i project) r\u1ED3i validate l\u1EA1i.":"the control/menu silently disappears. Run fui right function list and fui right system list (once per project), then validate again.",'" ch\u01B0a \u0111\u01B0\u1EE3c \u0111\u1ECBnh ngh\u0129a cho project (_db/rights.json c\xF3 ':'" is not defined for the project (_db/rights.json has '," m\xE3: ":" codes: "," m\xE3). ":" codes). ","rightTest() s\u1EBD lu\xF4n tr\u1EA3 false \u2192 control/menu g\xE1c b\u1EB1ng m\xE3 n\xE0y bi\u1EBFn m\u1EA5t m\xE0 kh\xF4ng b\xE1o l\u1ED7i. ":"rightTest() is always false, so controls gated by it silently disappear. ","T\u1EA1o m\xE3: right_function_new({ functionCode: ":"Create: fui right function new --code ",', functionName: "\u2026" }); ho\u1EB7c ch\u1EA1y right_function_list n\u1EBFu m\xE3 \u0111\xE3 c\xF3 tr\xEAn server m\xE0 cache c\u0169.':' --name "\u2026", or fui right function list if the cache is stale.',"ho\u1EB7c ch\u1EA1y right_function_list n\u1EBFu m\xE3 \u0111\xE3 c\xF3 tr\xEAn server m\xE0 cache c\u0169.":"or fui right function list if the cache is stale."," ch\u01B0a \u0111\u01B0\u1EE3c \u0111\u1ECBnh ngh\u0129a cho project (_db/rights.json c\xF3: ":" is not defined for the project (_db/rights.json has: ","(ch\u01B0a m\u1EE9c n\xE0o)":"(none)"," \u2014 m\u1EE9c 9 lu\xF4n h\u1EE3p l\u1EC7":"; 9 is always valid","Kh\xF4ng ai \u0111\u01B0\u1EE3c c\u1EA5p m\u1EE9c n\xE0y th\xEC control g\xE1c b\u1EB1ng n\xF3 bi\u1EBFn m\u1EA5t im l\u1EB7ng. ":"Nobody holds this level, so gated controls silently disappear. ","\u0110\u1ECBnh ngh\u0129a: right_system_new({ sysRight: ":"Define: fui right system new ",', note: "\u2026" }); ho\u1EB7c ch\u1EA1y right_system_list n\u1EBFu cache c\u0169.':' --note "\u2026", or fui right system list if the cache is stale.','Action g\u1ECDi API "':'Action calling API "',") c\xF3 v\u1EBB l\xE0 thao t\xE1c X\xD3A nh\u01B0ng thi\u1EBFu CONFIRM \u2014 m\u1ECDi action x\xF3a ph\u1EA3i h\u1ECFi x\xE1c nh\u1EADn.":") looks like a DELETE without CONFIRM; every delete action must confirm.","N\xFAt/m\u1EE5c x\xF3a (t\u1EA1i ":"Delete button/item (at ",') \u0111ang m\xE0u "':') is color "','" \u2014 n\xFAt x\xF3a d\xF9ng m\xE0u "warning" (cam), kh\xF4ng d\xF9ng error/\u0111\u1ECF.':'"; delete buttons use "warning" (orange), not error/red.',"v-toolbar (t\u1EA1i ":"v-toolbar (at ",') c\xF3 color/dark \u2014 toolbar ti\xEAu \u0111\u1EC1 ph\u1EA3i "dense flat" c\xF9ng m\xE0u n\u1EC1n v\u1EDBi th\xE2n dialog, kh\xF4ng t\xF4 m\xE0u/dark ri\xEAng.':') has color/dark; a title toolbar must be "dense flat" on the dialog background, no own color/dark.',"Dialog (t\u1EA1i ":"Dialog (at "," \u2014 b\u1ED9 width chu\u1EA9n: 400 / 550 / 600 / 650 / 800.":"; standard widths: 400 / 550 / 600 / 650 / 800.","v-card trong dialog (t\u1EA1i ":"v-card in dialog (at ",') c\xF3 elevation/outlined \u2014 b\u1ECF \u0111i: v-dialog \u0111\xE3 t\u1EF1 c\xF3 b\xF3ng \u0111\u1ED5, th\xEAm n\u1EEFa l\xE0 b\xF3ng ch\u1ED3ng b\xF3ng. v-card ch\u1EC9 c\u1EA7n class "pa-0" (tri\u1EC7t \u0111\u1EC7m \u0111\u1EC3 toolbar ti\xEAu \u0111\u1EC1 s\xE1t m\xE9p).':') has elevation/outlined; remove it (v-dialog already casts a shadow). v-card needs only class "pa-0".',"Control (t\u1EA1i ":"Control (at ",') mang class "flex-md-grow-0 ml-2" \u2014 d\u1EA5u hi\u1EC7u pattern label-div r\u1EDDi \u0111\xE3 lo\u1EA1i b\u1ECF; form d\xF9ng floating label ("label" trong attr c\u1EE7a 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\u1EA1i ":" in dialog (at ",') thi\u1EBFu "label" \u2014 form dialog d\xF9ng floating label.':') lacks "label"; dialog forms use floating labels.'," (t\u1EA1i ":" (at ",") \u2014 ph\u1EA3n h\u1ED3i/th\xF4ng b\xE1o d\xF9ng h\u1EC7 th\u1ED1ng c\xF3 s\u1EB5n c\u1EE7a FUI: MESS (toast) / MESSBOX (h\u1ED9p tho\u1EA1i) trong action, CONFIRM \u0111\u1EC3 x\xE1c nh\u1EADn \u2014 kh\xF4ng t\u1EF1 d\u1EF1ng ":"); use FUI feedback instead: MESS (toast) / MESSBOX (dialog) in actions, CONFIRM to confirm; do not build "," trong controls.":" in controls.",') kh\xF4ng c\xF3 ":height" \u2014 b\u1EA3ng s\u1EBD t\u1EF1 gi\xE3n theo s\u1ED1 d\xF2ng v\xE0 l\xE0m c\u1EA3 TRANG cu\u1ED9n. B\u1EA3ng data ch\xEDnh n\xEAn \u0111\u1EB7t `":height": -16` (fill t\u1EDBi \u0111\xE1y m\xE0n h\xECnh, ch\u1EEBa 16px \u2014 component t\u1EF1 \u0111o v\u1ECB tr\xED th\u1EADt, kh\xF4ng c\u1EA7n t\xEDnh tay); trong dialog d\xF9ng s\u1ED1 d\u01B0\u01A1ng (px c\u1ED1 \u0111\u1ECBnh, vd 360). B\u1EA3ng ng\u1EAFn/lookup ch\u1EE7 \xFD t\u1EF1 gi\xE3n th\xEC b\u1ECF 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\xEDnh ":height" th\u1EE7 c\xF4ng theo viewport (`':') computes ":height" from the viewport (`',"`) \u2014 kh\xF4ng c\u1EA7n n\u1EEFa: ":"`); not needed: ",' t\u1EF1 \u0111o v\u1ECB tr\xED th\u1EADt c\u1EE7a b\u1EA3ng sau layout v\xE0 t\u1EF1 c\u1EADp nh\u1EADt khi resize. Thay b\u1EB1ng `":height": -16` (s\u1ED1 \xE2m = kho\u1EA3ng ch\u1EEBa \u1EDF \u0111\xE1y).':' measures itself after layout and on resize. Use `":height": -16` (negative = bottom gap).','N\xFAt "':'Button "','" trong v-card-actions (t\u1EA1i ':'" in v-card-actions (at ',") \u2014 dialog chu\u1EA9n ch\u1EC9 d\xF9ng icon close tr\xEAn toolbar, kh\xF4ng th\xEAm n\xFAt \u0110\xF3ng \u1EDF actions.":"); standard dialogs close via the toolbar icon only, no Close button in actions.",' c\xF3 "w": ':' has "w": '," \u2014 v\xF9ng ch\u1EBFt 13\u201324: V2 hi\u1EC3u l\xE0 ":" \u2014 dead zone 13\u201324: V2 reads ",'px, c\xF2n V3 sinh class "v-col-':'px, V3 emits class "v-col-','" m\xE0 Vuetify 3 kh\xF4ng \u0111\u1ECBnh ngh\u0129a (ch\u1EC9 c\xF3 .v-col-1 \u2026 .v-col-12) n\xEAn c\u1ED9t KH\xD4NG c\xF3 b\u1EC1 r\u1ED9ng n\xE0o, r\u01A1i v\u1EC1 chia \u0111\u1EC1u. D\xF9ng 1\u201312 (chia c\u1ED9t) ho\u1EB7c >= ':'", which Vuetify 3 lacks (only .v-col-1 \u2026 .v-col-12), so the column gets NO width. Use 1\u201312 (grid) or >= ',' (px, k\xE8m `"col": { "class": "shrink" }`).':' (px, with `"col": { "class": "shrink" }`).',' (px) k\xE8m `"col": { "shrink": ... }` \u2014 d\u1EA1ng thu\u1ED9c t\xEDnh tr\u1EA7n l\xE0 prop c\u1EE7a v-flex (Vuetify 2); Vuetify 3 kh\xF4ng c\xF3 prop \u0111\xF3 n\xEAn n\xF3 tr\u01A1 v\xE0 c\u1ED9t v\u1EABn gi\xE3n h\u1EBFt ch\u1ED7 tr\u1ED1ng. Vi\u1EBFt `"col": { "class": "shrink" }` \u0111\u1EC3 \u0111\xFAng \u1EDF c\u1EA3 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.'," \u2014 s\u1ED1 >= ":" \u2014 >= "," ngh\u0129a l\xE0 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh px, nh\u01B0ng c\u1ED9t grid m\u1EB7c \u0111\u1ECBnh flex-grow:1 n\xEAn flex-basis ":" means fixed px, but grid columns default to flex-grow:1, so flex-basis ",'px KH\xD4NG c\xF3 t\xE1c d\u1EE5ng: c\u1ED9t v\u1EABn gi\xE3n h\u1EBFt ch\u1ED7 tr\u1ED1ng (\u0111o th\u1EADt: w=300 ra 687px \u1EDF V2, 738px \u1EDF V3). Th\xEAm `"col": { "class": "shrink" }`.':'px has NO effect and the column still grows (measured: w=300 gives 687px V2, 738px V3). Add `"col": { "class": "shrink" }`.'," \u2014 `v-spacer` m\xE0 c\xF3 `shrink` th\xEC co l\u1EA1i c\xF2n 0px (\u0111o th\u1EADt: 24px \u1EDF V3, 0px \u1EDF V2), kh\xF4ng \u0111\u1EA9y \u0111\u01B0\u1EE3c nh\xF3m sau sang ph\u1EA3i n\u1EEFa v\xE0 c\u1EA3 h\xE0ng d\u1ED3n v\u1EC1 tr\xE1i. B\u1ECF `shrink` kh\u1ECFi ri\xEAng c\u1ED9t n\xE0y.":" \u2014 `v-spacer` with `shrink` collapses to 0px and no longer pushes the next group right. Remove `shrink` from this column.",' n\u1EB1m c\xF9ng h\xE0ng v\u1EDBi `v-spacer`, `w` r\u1ED7ng v\xE0 ch\u1EC9 c\xF3 `"col": { "shrink": ... }` \u2014 d\u1EA1ng thu\u1ED9c t\xEDnh tr\u1EA7n l\xE0 prop c\u1EE7a v-flex (Vuetify 2); Vuetify 3 kh\xF4ng c\xF3 prop \u0111\xF3 n\xEAn c\u1ED9t v\u1EABn gi\xE3n v\xE0 chi\u1EBFm m\u1ED9t ph\u1EA7n \u0111\u1EC1u. Vi\u1EBFt `"col": "shrink"` (ho\u1EB7c `{ "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\u1EB1m c\xF9ng h\xE0ng v\u1EDBi `v-spacer` nh\u01B0ng thi\u1EBFu `shrink` \u2014 `w` r\u1ED7ng KH\xD4NG c\xF3 ngh\u0129a "t\u1EF1 co theo n\u1ED9i dung": runtime b\u1ECDc ph\u1EA7n b\u1EC1 r\u1ED9ng trong `if (c.w)` n\xEAn c\u1ED9t n\xE0y kh\xF4ng c\xF3 class grid l\u1EABn flex-basis, c\xF2n tr\u01A1 `flex-grow: 1` v\xE0 \u0111\u01B0\u1EE3c CHIA PH\u1EA6N TR\u1ED0NG y nh\u01B0 `v-spacer`. H\u1EC7 qu\u1EA3: control b\u1ECB k\xE9o d\xE3n, h\u1EDF kho\u1EA3ng gi\u1EEFa c\xE1c n\xFAt, nh\xF3m cu\u1ED1i h\xE0ng kh\xF4ng s\xE1t m\xE9p ph\u1EA3i. Chu\u1EA9n cho h\xE0ng nhi\u1EC1u control: M\u1ECCI c\u1ED9t `"col": "shrink"`, tr\u1EEB \u0111\xFAng `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\u1ED9p ":" puts "," l\u1EC7nh trong M\u1ED8T object `{}` (":" commands in ONE object `{}` (",") \u2014 ch\u1EC9 `":"); only `","` ch\u1EA1y, c\xF2n ":"` runs, ",' b\u1ECB b\u1ECF IM L\u1EB6NG (kh\xF4ng JS error, kh\xF4ng [Vue warn]). `vueAction` d\xF2 key l\u1EC7nh theo th\u1EE9 t\u1EF1 c\u1ED1 \u0111\u1ECBnh API \u2192 CALL \u2192 EXE \u2192 FUN \u2192 IF \u2192 MESS \u2192 MESSBOX v\xE0 `return` ngay \u1EDF nh\xE1nh \u0111\u1EA7u ti\xEAn kh\u1EDBp, n\xEAn th\u1EE9 t\u1EF1 vi\u1EBFt kh\xF4ng c\xF3 t\xE1c d\u1EE5ng. Nhi\u1EC1u l\u1EC7nh c\xF3 th\u1EE9 t\u1EF1 tr\u01B0\u1EDBc sau th\xEC ph\u1EA3i l\xE0 M\u1EA2NG, m\u1ED7i l\u1EC7nh m\u1ED9t object: `[{ "':' are SILENTLY dropped: `vueAction` checks API \u2192 CALL \u2192 EXE \u2192 FUN \u2192 IF \u2192 MESS \u2192 MESSBOX and runs only the first match. Ordered commands need an ARRAY, one per object: `[{ "'," c\xF3 l\u1EC7nh `":" has command `","` n\u1EB1m chung object v\u1EDBi key g\xE1n d\u1EEF li\u1EC7u ":"` in the same object as data keys "," \u2014 fragment g\xE1n ch\u1EA1y \u1EDF nh\xE1nh CU\u1ED0I c\u1EE7a `vueAction`, nh\xE1nh \u0111\xF3 ch\u1EC9 t\u1EDBi l\u01B0\u1EE3t khi KH\xD4NG key l\u1EC7nh n\xE0o kh\u1EDBp, n\xEAn c\xE1c key n\xE0y kh\xF4ng bao gi\u1EDD \u0111\u01B0\u1EE3c g\xE1n. T\xE1ch th\xE0nh m\u1EA3ng: `[{ ":" \u2014 `vueAction` assigns only when NO command key matches, so these keys are never set. Split into an array: `[{ ","module.json g\u1ED1c ph\u1EA3i l\xE0 m\u1ED9t object v\u1EDBi c\xE1c kh\u1ED1i data/watch/controls/set.":"module.json root must be an object with data/watch/controls/set.",'Kh\u1ED1i c\u1EA5p 1 kh\xF4ng chu\u1EA9n: "':'Unknown top-level key: "','" \u2014 module.json ch\u1EC9 c\xF3 data/watch/controls/set (xem module-json-anatomy.md).':'"; module.json has only data/watch/controls/set (see module-json-anatomy.md).','"data" ph\u1EA3i l\xE0 M\u1EA2NG (array), hi\u1EC7n l\xE0 ':'"data" must be an array, got ','"watch" ph\u1EA3i l\xE0 OBJECT, hi\u1EC7n l\xE0 ':'"watch" must be an object, got ','"controls" ph\u1EA3i l\xE0 M\u1EA2NG container, hi\u1EC7n l\xE0 ':'"controls" must be an array of containers, got ','"set" ph\u1EA3i l\xE0 OBJECT, hi\u1EC7n l\xE0 ':'"set" must be an object, got ',"data[] ch\u1EE9a ph\u1EA7n t\u1EED kh\xF4ng ph\u1EA3i object: ":"data[] has a non-object item: ",") kh\xF4ng kh\u1EDBp named action n\xE0o trong data[]. Named actions hi\u1EC7n c\xF3: ":") matches no named action in data[]. Named actions: ","(kh\xF4ng c\xF3)":"(none)",'] ph\u1EA3i l\xE0 container c\xF3 "rows" (grid b\u1EAFt bu\u1ED9c: container > rows > cols).':'] must be a container with "rows" (container > rows > cols).','] ph\u1EA3i c\xF3 "cols" (m\u1EA3ng Control Object).':'] needs "cols" (array of Control Objects).',') thi\u1EBFu "w" \u2014 b\u1EAFt bu\u1ED9c cho item trong cols.':') lacks "w" (required in cols).','"children" kh\xF4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3 (t\u1EA1i ':'"children" is not supported (at ',') \u2014 d\xF9ng "innerHTML" (m\u1EA3ng Control Object) \u0111\u1EC3 l\u1ED3ng.':'); nest with "innerHTML" (array of Control Objects).',") kh\xF4ng c\xF3 trong registry FUI (component-quickref.md) v\xE0 kh\xF4ng th\u1EA5y trong components local \u2014 ki\u1EC3m tra ch\xEDnh t\u1EA3 ho\u1EB7c ch\u1EA1y component_list.":") is neither in the FUI registry (component-quickref.md) nor a local component; check spelling or run fui component list.",") kh\xF4ng c\xF3 trong danh s\xE1ch t-* h\u1EE3p l\u1EC7: ":") is not a valid t-* cell: ",") ch\u01B0a th\u1EA5y file .vue/local metadata \u2014 t\u1EA1o component tr\u01B0\u1EDBc ho\u1EB7c ch\u1EA1y component_list.":") has no .vue/local metadata; create it first or run fui component list.",') \u2014 module.json KH\xD4NG h\u1ED7 tr\u1EE3 shorthand "@"; d\xF9ng "v-on:':'); module.json has no "@" shorthand; use "v-on:',') \u2014 module.json KH\xD4NG h\u1ED7 tr\u1EE3 slot shorthand "#"; d\xF9ng "v-slot:':'); module.json has no "#" slot shorthand; use "v-slot:',") \u2014 module th\u1EF1c t\u1EBF g\u1EA7n nh\u01B0 kh\xF4ng d\xF9ng style ngo\xE0i width (controls-styling-vocabulary.md). C\xE2n nh\u1EAFc b\u1ECF.":"); real modules rarely style beyond width (controls-styling-vocabulary.md). Consider removing it.",') \u2014 bi\u1EBFn g\u1ED1c "':'); root var "','" ch\u01B0a khai b\xE1o trong data[]. Khai b\xE1o state tr\u01B0\u1EDBc \u0111\u1EC3 reactive ho\u1EA1t \u0111\u1ED9ng \u0111\xFAng.':'" is not declared in data[]; declare it so reactivity works.',') \u2014 bi\u1EBFn "':') \u2014 var "','" ch\u01B0a khai b\xE1o trong data[], watcher s\u1EBD kh\xF4ng bao gi\u1EDD ch\u1EA1y.':'" is not declared in data[]; the watcher never runs.','" c\u1EA7n import: ':'" needs import: '," \u2014 ch\u01B0a th\u1EA5y trong _imports.json (module l\u1EABn project). D\xF9ng 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).",") \u2014 kh\xF4ng t\xECm th\u1EA5y spAPI_":"); no spAPI_"," trong schema local. Deploy SP tr\u01B0\u1EDBc ho\u1EB7c ch\u1EA1y db_schema_get (b\u1ECF qua n\u1EBFu API thu\u1ED9c backend kh\xE1c).":" in the local schema. Deploy the SP or run fui schema pull (ignore if another backend)."," alias \u0111\u01B0\u1EE3c g\u1ECDi nh\u01B0ng ch\u01B0a c\xF3 k\u1EBFt n\u1ED1i local \u2014 KH\xD4NG \u0111\u1ED1i chi\u1EBFu \u0111\u01B0\u1EE3c t\xEAn SP: ":" called alias(es) have no local connection, so SP names are NOT checked: ",'K\u1EBFt n\u1ED1i t\u1EEBng c\xE1i b\u1EB1ng db_connect({ apiName: "':"Connect each: fui db add ",'", apiDomain: "\u2026", dbToken: "\u2026" }) \u0111\u1EC3 b\u01B0\u1EDBc ki\u1EC3m n\xE0y ch\u1EA1y (b\u1ECF qua n\u1EBFu alias thu\u1ED9c backend c\u1EE7a nh\xF3m kh\xE1c).':" -p <project> --token - --api-domain \u2026 (ignore if another team's backend).","/schema.json sai \u0111\u1ECBnh d\u1EA1ng (b\u1EA3n cache c\u0169) \u2014 b\u1ECF qua b\u01B0\u1EDBc \u0111\u1ED1i chi\u1EBFu t\xEAn API v\u1EDBi stored procedure \u1EDF ":"/schema.json has an old format; API names were not checked against SPs for ","c\xE1c k\u1EBFt n\u1ED1i n\xE0y":"these connections","k\u1EBFt n\u1ED1i n\xE0y":"this connection",". Ch\u1EA1y db_schema_get \u0111\u1EC3 l\u1EA5y b\u1EA3n m\u1EDBi.":". Run fui schema pull.","module.json r\u1ED7ng ho\u1EB7c kh\xF4ng t\u1ED3n t\u1EA1i trong ":"module.json is empty or missing in ","module.json c\xF3 l\u1ED7i JSON syntax: ":"module.json JSON syntax error: ","\u2705 module.json h\u1EE3p l\u1EC7 \u2014 kh\xF4ng ph\xE1t hi\u1EC7n l\u1ED7i ng\u1EEF ngh\u0129a.":"\u2705 module.json valid: no semantic errors."," l\u1ED7i (s\u1EBD h\u1ECFng khi ch\u1EA1y):":" error(s) (break at runtime):"," c\u1EA3nh b\xE1o (n\xEAn xem l\u1EA1i):":" warning(s) (review):",' bi\u1EC3u th\u1EE9c \u0111\u1ED9ng (`:attr` ph\u1EE9c t\u1EA1p, CALL/API/v-model/OUT d\u1EA1ng `{{\u2026}}`/g\u1ECDi h\xE0m/template string) KH\xD4NG ki\u1EC3m t\u0129nh \u0111\u01B0\u1EE3c \u2014 bi\u1EBFn b\xEAn trong ch\xFAng kh\xF4ng \u0111\u01B0\u1EE3c \u0111\u1ED1i chi\u1EBFu v\u1EDBi data[]. "H\u1EE3p l\u1EC7" \u1EDF \u0111\xE2y KH\xD4NG c\xF3 ngh\u0129a l\xE0 ch\u1EA1y \u0111\xFAng: d\xF9ng module_simulate \u0111\u1EC3 ph\u1EE7 ph\u1EA7n n\xE0y.':' dynamic expression(s) (complex `:attr`, `{{\u2026}}` CALL/API/v-model/OUT, calls, template strings) are NOT checked statically. "Valid" does NOT mean it runs: use fui module simulate.',"Th\u1ED1ng k\xEA: ":"Stats: "," API ref \u0111\u1ED1i chi\u1EBFu schema":" API ref checked against schema"," CALL xuy\xEAn c\u1EEDa s\u1ED5 (thu\u1ED9c module kh\xE1c, kh\xF4ng ki\u1EC3m \u0111\u01B0\u1EE3c t\u1EEB \u0111\xE2y)":" cross-window CALL (other module, not checked)"," m\xE3 quy\u1EC1n \u0111\u1ED1i chi\u1EBFu _db/rights.json":" right code checked against _db/rights.json","Ch\u01B0a c\xF3 cache _db/rights.json \u2014 KH\xD4NG ki\u1EC3m \u0111\u01B0\u1EE3c ":"_db/rights.json not cached: cannot check whether "," m\xE3 FunctionRight (":" FunctionRight code(s) (","c\xF3 th\u1EADt hay kh\xF4ng. Preview c\u1EA5p s\u1EB5n \u0111\xFAng nh\u1EEFng m\xE3 n\xE0y n\xEAn control v\u1EABn hi\u1EC7n \u1EDF \u0110\xC2Y; user th\u1EADt ch\u1EC9 th\u1EA5y n\u1EBFu m\xE3 \u0111\xE3 ":"exist. The preview grants them, so controls show HERE; real users see them only if the codes are ","\u0111\u01B0\u1EE3c \u0111\u1ECBnh ngh\u0129a. Ch\u1EA1y right_function_list (m\u1ED9t l\u1EA7n cho m\u1ED7i project) r\u1ED3i render l\u1EA1i.":"defined. Run fui right function list (once per project), then render again.","\uD83D\uDEA8 FunctionRight CH\u01AFA \u0110\u01AF\u1EE2C \u0110\u1ECANH NGH\u0128A tr\xEAn h\u1EC7 th\u1ED1ng acc: ":"\uD83D\uDEA8 FunctionRight NOT DEFINED in acc: "," \u2014 preview c\u1EA5p s\u1EB5n n\xEAn control v\u1EABn ":" \u2014 the preview grants them, so controls ","hi\u1EC7n \u1EDF \u0110\xC2Y, nh\u01B0ng ch\u1EA1y th\u1EADt th\xEC rightTest() lu\xF4n tr\u1EA3 false \u2192 control/menu bi\u1EBFn m\u1EA5t m\xE0 kh\xF4ng c\xF3 l\u1ED7i JS n\xE0o. ":"show HERE, but live rightTest() is false and they silently disappear. ",'T\u1EA1o m\xE3: right_function_new({ functionCode: \u2026, functionName: "\u2026" }); ho\u1EB7c right_function_list n\u1EBFu cache c\u0169.':'Create: fui right function new --code \u2026 --name "\u2026", or fui right function list if the cache is stale.',"\uD83D\uDEA8 C\u1EA4U TR\xDAC MOCK CH\u01AFA \u0110\u01AF\u1EE2C KI\u1EC2M CH\u1EE8NG \u2014 nghi ng\u1EDD t\u1EF1 suy di\u1EC5n thay v\xEC \u0111\u1ECDc t\u1EEB SP th\u1EADt:":"\uD83D\uDEA8 UNVERIFIED MOCK SHAPE: likely guessed instead of read from the real SP:",'"] kh\u1EDBp t\xEAn SP C\xD3 TH\u1EACT "':'"] matches REAL SP "','" trong schema local, nh\u01B0ng SP \u0111\xF3 ch\u01B0a t\u1EEBng \u0111\u01B0\u1EE3c db_sp_verify \u0111\u1ECDc th\xE2n trong phi\xEAn n\xE0y.':'", whose body fui sp verify has not read in this session.',' \u2192 Ch\u1EA1y db_sp_verify({ name: "':" \u2192 Run fui sp verify ",'" }) \u0111\u1EC3 \u0111\u1ECDc th\xE2n SP th\u1EADt r\u1ED3i COPY NGUY\xCAN V\u0102N snippet apiMocks n\xF3 sinh s\u1EB5n \u2014 kh\xF4ng t\u1EF1 \u0111o\xE1n t\xEAn c\u1ED9t/shape cho SP \u0111\xE3 t\u1ED3n t\u1EA1i, k\u1EBFt qu\u1EA3 m\xF4 ph\u1ECFng s\u1EBD l\u1EC7ch v\u1EDBi th\u1EF1c t\u1EBF.':" and copy its apiMocks snippet VERBATIM; do not guess columns/shape of an existing SP.",") \u2014 g\u1ECDi tAPI th\u1EADt th\xE0nh c\xF4ng":"): live tAPI call OK",") g\u1ECDi th\u1EADt th\u1EA5t b\u1EA1i (":") live call failed (",") \u2014 r\u01A1i xu\u1ED1ng cache/t\u1EF1 sinh":"); fell back to cache/generated",") c\xF3 l\u1EC7nh ghi \u2014 KH\xD4NG g\u1ECDi th\u1EADt, d\xF9ng cache/t\u1EF1 sinh":") writes: NOT called live, cache/generated used",") c\xF3 \u0111o\u1EA1n \u0111\u1ECDc t\u0129nh kh\xF4ng nh\xECn xuy\xEAn qua \u0111\u01B0\u1EE3c (SQL \u0111\u1ED9ng / g\u1ECDi SP kh\xE1c) \u2014 KH\xD4NG g\u1ECDi th\u1EADt, d\xF9ng cache/t\u1EF1 sinh":") has opaque reads (dynamic SQL / nested SP): NOT called live, cache/generated used",") ch\u01B0a c\xF3 th\xE2n SP local \u0111\u1EC3 ph\xE2n lo\u1EA1i an to\xE0n \u2014 KH\xD4NG g\u1ECDi th\u1EADt, d\xF9ng cache/t\u1EF1 sinh":") has no local body to classify: NOT called live, cache/generated used",") \u0111\xE3 \u0111\u1EA1t tr\u1EA7n ":") hit the cap of "," l\u01B0\u1EE3t g\u1ECDi th\u1EADt/l\u01B0\u1EE3t simulate \u2014 d\xF9ng cache/t\u1EF1 sinh":" live calls per run; cache/generated used",") \u2014 d\xF9ng _testdata cache (ngu\u1ED3n: ":"): _testdata cache (source: ",", l\u01B0u l\xFAc ":", saved ",") \u2014 t\u1EF1 sinh mock t\u1EEB c\u1EA5u tr\xFAc SELECT, \u0111\xE3 l\u01B0u cache":"): mock generated from the SELECT shape, cached","\u2026 (trace b\u1ECB c\u1EAFt \u2014 qu\xE1 d\xE0i)":"\u2026 (trace truncated)",") \u2014 stub, kh\xF4ng m\xF4 ph\u1ECFng UI th\u1EADt":"): stub, UI not simulated"," ch\u01B0a h\u1ED7 tr\u1EE3 trong simulator \u2014 tr\u1EA3 undefined":" unsupported in the simulator: undefined"," \u2014 stub (sub-window kh\xF4ng m\xF4 ph\u1ECFng)":": stub (sub-window not simulated)","(gi\u1EA3 l\u1EADp user \u0111\xF3ng dialog \u2192 ch\u1EA1y onclose)":"(simulated dialog close \u2192 onclose)",'" \u2192 OK (m\u1EB7c \u0111\u1ECBnh simulator)':'" \u2192 OK (simulator default)','" \u2192 H\u1EE6Y':'" \u2192 CANCEL','\u0111\u1ECDc cross-window "':'cross-window read "','" \u2014 kh\xF4ng m\xF4 ph\u1ECFng \u0111\u01B0\u1EE3c, g\xE1n undefined':'": not simulated, undefined'," (kh\xF4ng m\xF4 ph\u1ECFng)":" (not simulated)",'" undefined \u2014 CALL tr\u1ECF t\u1EDBi action kh\xF4ng t\u1ED3n t\u1EA1i?':'" undefined: CALL to a missing action?',"L\u1ED7i khi ch\u1EA1y action: ":"Action error: ",'" kh\xF4ng t\u1ED3n t\u1EA1i (simulator ch\u1EC9 c\xF3 c\xE1c h\xE0m FUI chu\u1EA9n)':'" does not exist (simulator has only standard FUI functions)','" l\u1ED7i eval: ':'" eval error: '," \u2192 coi nh\u01B0 false":" \u2192 treated as false",'(mock kh\u1EDBp "':'(mock matched "','kh\xF4ng c\xF3 mock cho API "':'no mock for API "','" \u2014 tr\u1EA3 m\u1EB7c \u0111\u1ECBnh {data: []}':'": default {data: []}'," (mock l\u1ED7i)":" (error mock)",'" l\u1ED7i kh\xF4ng \u0111\u01B0\u1EE3c b\u1EAFt (kh\xF4ng c\xF3 ERROROUT) \u2192 hi\u1EC7n ERROR DIALOG m\u1EB7c \u0111\u1ECBnh: ':'" error not handled (no ERROROUT) \u2192 default ERROR DIALOG: ',"\u2190 (kh\xF4ng c\xF3 OUT)":"\u2190 (no OUT)"," thay \u0111\u1ED5i \u2192 ":" changed \u2192 ","watch cascade v\u01B0\u1EE3t ":"watch cascade over "," v\xF2ng \u2014 nguy c\u01A1 V\xD2NG L\u1EB6P V\xD4 H\u1EA0N gi\u1EEFa c\xE1c watch/action":" rounds: possible INFINITE LOOP between watch/actions","M\u1EDE \uD83D\uDD32":"OPEN \uD83D\uDD32","\u0110\xD3NG \u25AA":"CLOSED \u25AA","L\u1ED7i khi init data[]: ":"data[] init error: ",'kh\xF4ng t\xECm th\u1EA5y control c\xF3 label "':'no control labeled "','" k\xE8m v-on:click/:action':'" with v-on:click/:action',"(kh\u1EDBp ":"(matched "," control \u2014 d\xF9ng #":" controls; using #",'control n\u1EB1m trong f-table nh\u01B0ng "':'control is in an f-table but "','" r\u1ED7ng \u2014 item = {} (truy\u1EC1n "item" trong event \u0111\u1EC3 ch\u1EC9 \u0111\u1ECBnh)':'" is empty: item = {} (pass "item" in the event)',"exe l\u1ED7i: ":"exe error: "," kh\xF4ng h\u1EE3p l\u1EC7 \u2014 c\u1EA7n m\u1ED9t trong: set / click / call / exe / assert":" invalid: needs one of set / click / call / exe / assert","SystemRight=9 (m\u1EB7c \u0111\u1ECBnh, quy\u1EC1n cao nh\u1EA5t)":"SystemRight=9 (default, highest)","] (m\u1EB7c \u0111\u1ECBnh \u2014 qu\xE9t \u0111\u01B0\u1EE3c t\u1EEB module.json)":"] (default: scanned from module.json)","FunctionRight=[] (m\u1EB7c \u0111\u1ECBnh \u2014 kh\xF4ng qu\xE9t \u0111\u01B0\u1EE3c m\xE3 n\xE0o trong module.json)":"FunctionRight=[] (default: no codes found in module.json)",". rightTest()/v-if theo FunctionRight kh\xE1c m\xE3 tr\xEAn s\u1EBD tr\u1EA3 false \u2014 truy\u1EC1n tham s\u1ED1 `user` \u0111\u1EC3 test vai tr\xF2 h\u1EA1n ch\u1EBF h\u01A1n.":". rightTest()/v-if on other FunctionRight codes is false; pass --user to test a narrower role.","\u2550\u2550\u2550 K\u1EBET QU\u1EA2 \u2550\u2550\u2550":"\u2550\u2550\u2550 RESULT \u2550\u2550\u2550","\u2500\u2500 C\u1EA3nh b\xE1o \u2500\u2500":"\u2500\u2500 Warnings \u2500\u2500","(stub \u0111\xE3 d\xF9ng \u2014 k\u1EBFt qu\u1EA3 c\xE1c h\xE0m n\xE0y KH\xD4NG ph\u1EA3n \xE1nh runtime th\u1EADt: ":"(stubs used; their results do NOT reflect the real runtime: ","\u2500\u2500 STATE CU\u1ED0I \u2500\u2500":"\u2500\u2500 FINAL STATE \u2500\u2500"," assert \u0111\u1EA1t":" assert passed"," \xB7 dialog \u0111ang m\u1EDF: ":" \xB7 open dialogs: ","\n\n\u2500\u2500 Live/Cache data (SP ch\u01B0a c\xF3 mock, \u0111\xE3 t\u1EF1 resolve) \u2500\u2500\n":`
157
-
158
- \u2500\u2500 Live/Cache data (SPs without mocks, auto-resolved) \u2500\u2500
159
- `,"Ph\u1EA3n h\u1ED3i kh\xF4ng ph\u1EA3i JSON":"Response is not JSON",': \u274C thi\u1EBFu "cols"':': \u274C missing "cols"'," \u26A0 v\u01B0\u1EE3t 12":" \u26A0 over 12","\u274C module.json g\u1ED1c kh\xF4ng ph\u1EA3i object.":"\u274C module.json root is not an object.","(kh\xF4ng c\xF3 controls)":"(no controls)","] \u274C kh\xF4ng ph\u1EA3i object":"] \u274C not an object","(m\u1EB7c \u0111\u1ECBnh)":"(default)",' \u274C thi\u1EBFu "rows"':' \u274C missing "rows"',"(kh\xF4ng c\xF3 state)":"(no state)"," \u26A0 kh\xF4ng th\u1EA5y d\xF9ng \u1EDF controls/watch/actions":" \u26A0 unused in controls/watch/actions","(kh\xF4ng c\xF3 action)":"(no actions)"," \u26A0 kh\xF4ng th\u1EA5y n\u01A1i g\u1ECDi (CALL/watch/v-on)":" \u26A0 never called (CALL/watch/v-on)",": \u26A0 hidden-container kh\xF4ng c\xF3 v-model \u2014 kh\xF4ng \u0111i\u1EC1u khi\u1EC3n m\u1EDF/\u0111\xF3ng \u0111\u01B0\u1EE3c":": \u26A0 hidden-container without v-model: cannot open/close"," \u2014 c\xF3 l\u1EC7nh m\u1EDF (":" \u2014 opened by (",' \u26A0 kh\xF4ng th\u1EA5y l\u1EC7nh m\u1EDF "':' \u26A0 no opener "',' = true" \u1EDF \u0111\xE2u':' = true" found'," file, t\u1EA3i l\xFAc ":" files, fetched ","API tr\u1EA3 danh m\u1EE5c r\u1ED7ng":"API returned an empty list","cache C\u0168 (":"STALE cache (","Kh\xF4ng g\u1ECDi \u0111\u01B0\u1EE3c API danh m\u1EE5c framework ":"Framework list API failed for ",") \u2014 d\xF9ng cache c\u0169 tr\xEAn \u0111\u0129a.":"); using the stale disk cache.","danh s\xE1ch d\u1EF1 ph\xF2ng hardcode (":"hardcoded fallback list (",") v\xE0 ch\u01B0a c\xF3 cache \u2014 ":") and no cache: ","d\xF9ng danh s\xE1ch hardcode trong renderAssets.ts. Danh s\xE1ch n\xE0y c\xF3 th\u1EC3 \u0111\xE3 l\u1EC7ch v\u1EDBi server.":"using the hardcoded list, which may differ from the server.","projectdata.js t\u1EA3i v\u1EC1 t\u1EEB ":"projectdata.js from ",' kh\xF4ng ch\u1EE9a "$projectData =" (':' lacks "$projectData =" (',"kh\xF4ng d\xF9ng, quay v\u1EC1 d\u1EF1ng $projectData + component project t\u1EEB workspace local.":"ignored; building $projectData + project components from the local workspace.","Kh\xF4ng t\u1EA3i \u0111\u01B0\u1EE3c ":"Cannot fetch "," \u2014 quay v\u1EC1 d\u1EF1ng t\u1EEB workspace local.":"; building from the local workspace.","StyleCSS c\u1EA5p project t\u1EA3i v\u1EC1 t\u1EEB ":"Project StyleCSS from "," l\xE0 HTML ch\u1EE9 kh\xF4ng ph\u1EA3i CSS \u2014 b\u1ECF qua.":" is HTML, not CSS; skipped.","StyleCSS project r\u1ED7ng (":"Project StyleCSS empty (",") \u2014 project ch\u01B0a c\xF3 component n\xE0o khai style.":"): no project component declares a style."," \u2014 CSS component c\u1EA5p project s\u1EBD d\u1EF1ng t\u1EEB local.":"; project component CSS comes from local.",'.vue" kh\xF4ng c\xF3 khu\xF4n `export default { \u2026 }` \u2014 server bi\xEAn d\u1ECBch b\u1EB1ng c\xE1ch ':'.vue" lacks `export default { \u2026 }`; the server ',"n\u1ED1i th\u1EB3ng ph\u1EA7n <script> v\xE0o `{template:`\u2026`, \u2026}` n\xEAn b\u1EA3n tr\xEAn server c\xF3 th\u1EC3 l\u1ED7i c\xFA ph\xE1p. ":"splices <script> into `{template:`\u2026`, \u2026}`, so the server build may have a syntax error. ","renderUI \u0111\xE3 b\u1ECDc an to\xE0n \u0111\u1EC3 ch\u1EA1y ti\u1EBFp, nh\u01B0ng n\xEAn s\u1EEDa .vue v\u1EC1 \u0111\xFAng khu\xF4n.":"The render wraps it to continue; fix the .vue shape."," (bundle server kh\xF4ng theo khu\xF4n ComponentBuild)":" (server bundle not in ComponentBuild shape)"," (ch\u1EC9 \u0111\u1ED1i chi\u1EBFu \u0111\u01B0\u1EE3c template, kh\xF4ng b\xF3c \u0111\u01B0\u1EE3c th\xE2n script)":" (template checked only; script body not extractable)",'": FUI kh\xF4ng nh\u1EADn c\xFA ph\xE1p @/# trong JSON \u2014 \u0111\u1ED5i th\xE0nh "v-on:"/"v-slot:". \u0110\xE3 b\u1ECF qua.':'": FUI does not accept @/# in JSON; use "v-on:"/"v-slot:". Skipped.',"Kh\xF4ng x\xE1c \u0111\u1ECBnh \u0111\u01B0\u1EE3c projectId. Truy\u1EC1n projectId ho\u1EB7c ch\u1EA1y session_set tr\u01B0\u1EDBc.":"No project: pass -p or run fui use first.","Kh\xF4ng t\xECm th\u1EA5y ":"Not found: ",".vue trong ":".vue in "," C\xF3 s\u1EB5n: ":" Available: "," Th\u01B0 m\u1EE5c components r\u1ED7ng.":" The components folder is empty."," N\u1EBFu \u0111\xE2y l\xE0 component c\u1EA5p module, truy\u1EC1n th\xEAm moduleId.":" For a module-level component, also pass -m."," l\u1EA7n render, v\u01B0\u1EE3t gi\u1EDBi h\u1EA1n ":" renders, over the limit of ","M\u1ED7i l\u1EA7n render l\xE0 m\u1ED9t phi\xEAn browser th\u1EADt \u2014 chia nh\u1ECF th\xE0nh nhi\u1EC1u l\u1EA7n g\u1ECDi.":"Each render is a real browser session; split into several calls.","Ngu\u1ED3n: ":"Source: ","(kh\xF4ng t\xEAn)":"(unnamed)"," l\u1EA7n render":" renders","\u1EA2nh: ":"Screenshots: ","Kh\xF4ng gian ri\xEAng: ch\u1EC9 component n\xE0y + CSS c\u1EE7a ch\xEDnh n\xF3 + theme/import c\u1EA5p project.":"Isolated: this component + its own CSS + project theme/imports.","style.css c\u1EE7a module KH\xD4NG n\u1EA1p \u2014 component ph\u1EA3i t\u1EF1 \u0111\u1EE9ng \u0111\u01B0\u1EE3c. T\u1EA7ng project d\u1EF1ng t\u1EEB workspace":"Module style.css is NOT loaded; the component must stand alone. The project layer comes from the workspace,","local n\xEAn b\u1EA3n ch\u01B0a push v\u1EABn hi\u1EC7n; ch\u1EA1y `project_sync` n\u1EBFu nghi file local \u0111\xE3 c\u0169.":"so unpushed edits show; run `fui project sync` if local files look stale.","\u26A0 Ghi ch\xFA d\u1EF1ng harness:":"\u26A0 Harness notes:","file m\xF4i tr\u01B0\u1EDDng thi\u1EBFu":"missing env files","text nghi v\u1EA5n":"suspect text","\u1EA3nh v\u1EE1":"broken images","canvas ch\u1EBFt":"dead canvas","text d\xEDnh/ch\u1ED3ng l\u1EA5n":"text collisions","assert h\u1ECFng":"failed asserts","app render R\u1ED6NG (tr\u01B0\u1EDBc c\xF3 n\u1ED9i dung) \u2191":"app EMPTY (had content before) \u2191","app \u0111\xE3 c\xF3 n\u1ED9i dung \u2193":"app has content now \u2193","tr\xE0n ngang (tr\u01B0\u1EDBc kh\xF4ng) \u2191":"horizontal overflow (none before) \u2191","h\u1EBFt tr\xE0n ngang \u2193":"no horizontal overflow \u2193","\u0394 so v\u1EDBi l\u01B0\u1EE3t tr\u01B0\u1EDBc (":"\u0394 vs previous run (","): KH\xD4NG ch\u1EC9 s\u1ED1 n\xE0o \u0111\u1ED5i \u2014 thay \u0111\u1ED5i v\u1EEBa r\u1ED3i kh\xF4ng ch\u1EA1m t\u1EDBi l\u1ED7i \u0111ang c\xF3. \u0110\u1ED5i h\u01B0\u1EDBng ch\u1EA9n \u0111o\xE1n, \u0111\u1EEBng render l\u1EA1i c\xF9ng m\u1ED9t c\xE1ch.":"): NO metric changed; the last edit missed the failure. Change the diagnosis, do not render again the same way.","): kh\xF4ng \u0111\u1ED5i \u2014 c\u1EA3 hai l\u01B0\u1EE3t \u0111\u1EC1u s\u1EA1ch.":"): unchanged; both runs clean.","\uD83D\uDED1 \u0110ANG GI\u1EACM CH\xC2N \u2014 ":"\uD83D\uDED1 STUCK: "," l\u01B0\u1EE3t render h\u1ECFng li\xEAn ti\u1EBFp m\xE0 KH\xD4NG ch\u1EC9 s\u1ED1 n\xE0o \u0111\u1ED5i.":" failed renders in a row with NO metric change."," Render l\u1EA1i l\u1EA7n n\u1EEFa theo c\xF9ng h\u01B0\u1EDBng s\u1EBD cho \u0111\xFAng k\u1EBFt qu\u1EA3 n\xE0y. Ch\u1ECDn M\u1ED8T trong ba:":" Rendering again the same way gives the same result. Pick ONE:"," 1. Ch\u1EA1y t\u1EA7ng r\u1EBB h\u01A1n \u0111\u01B0\u1EE3c n\xEAu \u1EDF d\xF2ng \u21AA tr\xEAn \u2014 n\xF3 ch\u1EC9 th\u1EB3ng v\xE0o ch\u1ED7 h\u1ECFng.":" 1. Run the cheaper check on the \u21AA line above; it points at the fault."," 1. L\xF9i v\u1EC1 t\u1EA7ng t\u0129nh: module_validate (l\u1ED7i quy t\u1EAFc) \u2192 module_outline (c\u1EA5u tr\xFAc) \u2192 module_simulate kh\xF4ng renderUI (action/watch).":" 1. Step back: fui module validate (rules) \u2192 fui module outline (structure) \u2192 fui module simulate without --render (actions/watch)."," 2. \u0110\u1ECDc l\u1EA1i screenshot + kh\u1ED1i l\u1ED7i \u1EDF cu\u1ED1i report b\u1EB1ng m\u1EAFt, \u0111\u1EEBng ch\u1EC9 \u0111\u1ECDc con s\u1ED1.":" 2. Look at the screenshot and the error block at the end, not only the numbers."," 3. H\u1ECEI NG\u01AF\u1EDCI D\xD9NG. Ba l\u01B0\u1EE3t kh\xF4ng nh\xFAc nh\xEDch th\u01B0\u1EDDng ngh\u0129a l\xE0 gi\u1EA3 \u0111\u1ECBnh ban \u0111\u1EA7u sai, kh\xF4ng ph\u1EA3i s\u1EEDa ch\u01B0a \u0111\u1EE7.":" 3. ASK THE USER. Three stuck runs usually mean a wrong assumption, not an incomplete fix.","\u2550\u2550\u2550 CH\u1EA8N \u0110O\xC1N \u2550\u2550\u2550":"\u2550\u2550\u2550 DIAGNOSIS \u2550\u2550\u2550","[M\xD4I TR\u01AF\u1EDCNG] ":"[ENV] "," file import/asset KH\xD4NG n\u1EA1p \u0111\u01B0\u1EE3c \u2192 s\u1EEDa ph\u1EA7n n\xE0y TR\u01AF\u1EDAC (project_sync / module_get / file_import_*). L\u1ED7i \u1EDF hai nh\xF3m d\u01B0\u1EDBi c\xF3 th\u1EC3 ch\u1EC9 l\xE0 h\u1EC7 qu\u1EA3.":" import/asset file(s) NOT loaded; fix this FIRST (fui project sync / fui module get / fui import). Errors below may only be consequences.","[M\xD4I TR\u01AF\u1EDCNG] \u2713 m\u1ECDi import/asset n\u1EA1p \u0111\u1EE7.":"[ENV] \u2713 all imports/assets loaded."," l\xFAc init)":" at init)"," assert h\u1ECFng":" failed asserts"," \u2192 l\u1ED7i n\u1EB1m trong module.json / script.js / component.":" \u2192 fault is in module.json / script.js / component.","[MODULE] \u2713 kh\xF4ng JS error, kh\xF4ng Vue warn.":"[MODULE] \u2713 no JS error, no Vue warn.","app render R\u1ED6NG":"app EMPTY","tr\xE0n ngang viewport":"horizontal viewport overflow"," text nghi v\u1EA5n (undefined/NaN)":" suspect text (undefined/NaN)"," \u1EA3nh v\u1EE1":" broken images"," ch\u1ED7 text d\xEDnh/ch\u1ED3ng l\u1EA5n":" text collisions","[HI\u1EC2N TH\u1ECA] ":"[VIEW] "," \u2192 xem screenshot; \u0111\xE2y l\xE0 nh\xF3m CH\u1EC8 renderUI th\u1EA5y \u0111\u01B0\u1EE3c.":" \u2192 check the screenshot; only --render sees these.","[HI\u1EC2N TH\u1ECA] \u2713 DOM audit s\u1EA1ch.":"[VIEW] \u2713 DOM audit clean.","`\u2026 is not defined` = m\u1ED9t \u0111\u1ECBnh danh tr\u1EA7n kh\xF4ng c\xF3 tr\xEAn window. Ca th\u01B0\u1EDDng g\u1EB7p nh\u1EA5t: `EXE`/`FUN` g\u1ECDi h\xE0m ch\u01B0a vi\u1EBFt trong script.js \u2014 ho\u1EB7c vi\u1EBFt r\u1ED3i m\xE0 qu\xEAn `module_publish_script` (module_publish_json KH\xD4NG \u0111\u1EA9y script.js). So\xE1t t\xEAn h\xE0m trong script.js tr\u01B0\u1EDBc khi \u0111i t\xECm ch\u1ED7 kh\xE1c.":"`\u2026 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\u1EAFng m\xE0 KH\xD4NG c\xF3 JS error/Vue warn l\xE0 d\u1EA5u hi\u1EC7u kinh \u0111i\u1EC3n c\u1EE7a statement trong m\u1ED9t `:attr` (Vue nu\u1ED1t l\u1ED7i compile template) \u2014 ch\u1EA1y module_validate, n\xF3 b\u1EAFt t\u0129nh \u0111\u01B0\u1EE3c ca n\xE0y.":"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\u1EC3u "property is not defined" = bi\u1EBFn d\xF9ng trong template ch\u01B0a khai trong data[] \u2014 module_validate b\u1EAFt \u0111\u01B0\u1EE3c (v-model/watch/state), kh\xF4ng c\u1EA7n 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\u01B0\u1EDDng l\xE0 OUT/mapData tr\u1ECF sai kho\xE1 ch\u1EE9 kh\xF4ng ph\u1EA3i l\u1ED7i hi\u1EC3n th\u1ECB \u2014 module_simulate (engine vm) ch\u1EC9 ra ch\u1ED7 g\xE1n, ch\u1EA1y nhanh h\u01A1n nhi\u1EC1u.':'Text "undefined"/"NaN" usually means OUT/mapData uses a wrong key; fui module simulate (no --render) shows the assignment, much faster.',"kh\xF4ng r\xF5 tu\u1ED5i":"unknown age","c\u0169 d\u01B0\u1EDBi 1 gi\u1EDD":"under 1 h old","offline: ch\u01B0a c\xF3 b\u1EA3n cache n\xE0o cho URL n\xE0y":"offline: no cached copy of this URL","offline \u2014 d\xF9ng cache ":"offline: cache ","n\u1ED9i dung \u0110\u1ED4I so v\u1EDBi b\u1EA3n cache ":"content CHANGED vs cache ","kh\xF4ng revalidate \u0111\u01B0\u1EE3c (":"revalidate failed (",") \u2014 d\xF9ng cache ":"); cache ","_moduleInfo.json kh\xF4ng c\xF3 url k\xE8m host \u2014 origin l\u1EA5y t\u1EEB ":"_moduleInfo.json has no url with a host; origin from ","domain asset m\u1EB7c \u0111\u1ECBnh":"default asset domain","Ch\u1EA1y `module_get` \u0111\u1EC3 c\xF3 url th\u1EADt n\u1EBFu render sai domain.":"Run `fui module get` for the real url if the domain is wrong.","Project/module ph\u1EE5c v\u1EE5 tr\xEAn nhi\u1EC1u domain (":"Project/module served on several domains (",') \u2014 l\u1EA7n render n\xE0y ch\u1EA1y \u1EDF "':'); this render uses "',"v_Set.domainSetting c\u1EE7a domain kh\xE1c KH\xD4NG \u0111\u01B0\u1EE3c \xE1p d\u1EE5ng; mu\u1ED1n th\u1EED domain kh\xE1c th\xEC render module c\xF3 url thu\u1ED9c domain \u0111\xF3.":"v_Set.domainSetting of other domains is NOT applied; to test another domain, render a module whose url is on it.","(tr\u1ED1ng)":"(empty)",'" kh\xF4ng ph\u1EA3i .js/.css \u2014 kh\xF4ng n\u1EA1p v\xE0o trang render':'" is not .js/.css; not loaded'," import c\u1EA5p ":" import(s), scope "," kh\xF4ng c\xF3 FileSort trong _imports.json (":", lack FileSort in _imports.json (",") \u2192 t\u1EA1m x\u1EBFp cu\u1ED1i ":") \u2192 sorted last ","). Th\u1EE9 t\u1EF1 n\u1EA1p c\xF3 th\u1EC3 kh\xE1c production; ch\u1EA1y `":"). Load order may differ from production; run `","` \u0111\u1EC3 l\u1EA5y FileSort th\u1EADt.":"` for the real FileSort.",': file n\u1ED9i b\u1ED9 "':': inline file "','" thi\u1EBFu/r\u1ED7ng v\xE0 kh\xF4ng c\xF3 fileID \u0111\u1EC3 l\u1EA5y b\u1EA3n server \u2192 KH\xD4NG n\u1EA1p \u0111\u01B0\u1EE3c. ':'" missing/empty and no fileID to fetch it \u2192 NOT loaded. ',"Ch\u1EA1y `":"Run `","`. L\u1ED7i JS/hi\u1EC3n th\u1ECB b\xEAn d\u01B0\u1EDBi c\xF3 th\u1EC3 ch\u1EC9 l\xE0 h\u1EC7 qu\u1EA3 c\u1EE7a vi\u1EC7c n\xE0y.":"`. JS/display errors below may only be consequences.",": ch\u01B0a c\xF3 b\u1EA3n local \u2192 d\xF9ng b\u1EA3n tr\xEAn server. Ch\u1EA1y `":": no local copy \u2192 server copy used. Run `","` n\u1EBFu c\u1EA7n s\u1EEDa file n\xE0y.":"` to edit it.",": kh\xF4ng c\xF3 file n\u1ED9i b\u1ED9 v\xE0 t\xEAn kh\xF4ng ph\u1EA3i URL/\u0111\u01B0\u1EDDng d\u1EABn tuy\u1EC7t \u0111\u1ED1i \u2192 kh\xF4ng x\xE1c \u0111\u1ECBnh \u0111\u01B0\u1EE3c ngu\u1ED3n \u0111\u1EC3 n\u1EA1p. ":": no inline file and the name is not a URL/absolute path \u2192 nothing to load. ","N\u1EBFu \u0111\xE2y l\xE0 file t\u1EF1 t\u1EA1o, ch\u1EA1y `":"If you created it, run `"," \u2014 s\u1EEDa tr\u01B0\u1EDBc khi renderUI.":"; fix it before --render.","module.json r\u1ED7ng \u2014 render shell v\u1EDBi module tr\u1ED1ng":"module.json empty: rendering an empty shell","(b\u1ECF qua \u2014 HTMLOnly)":"(skipped: HTMLOnly)",": KH\xD4NG nh\xFAng (HTMLOnly=true \u2014 server c\u0169ng kh\xF4ng nh\xFAng)":": NOT embedded (HTMLOnly=true, same as the server)",": danh m\u1EE5c t\u1EEB ":": list from ","HTMLOnly=true \u2192 render KH\xD4NG nh\xFAng framework ":"HTMLOnly=true \u2192 no framework ",", KH\xD4NG nh\xFAng projectdata/StyleCSS c\u1EA5p project ":", no project projectdata/StyleCSS ","v\xE0 KH\xD4NG d\u1EF1ng #fastProjectAPP \u2014 gi\u1ED1ng h\u1EC7t trang production. Th\u01B0 vi\u1EC7n n\xE0o module c\u1EA7n (Vue, jQuery, Vuetify, ":"and no #fastProjectAPP, exactly like production. Libraries the module needs (Vue, jQuery, Vuetify, ","lodash\u2026) ph\u1EA3i do CH\xCDNH module khai l\xE0m import; thi\u1EBFu l\xE0 tr\u1EAFng trang th\u1EADt, kh\xF4ng ph\u1EA3i l\u1ED7i c\u1EE7a render.":"lodash\u2026) must be module imports; a missing one blanks the real page too.","T\u1EA7ng project d\u1EF1ng t\u1EEB workspace LOCAL (ch\u1EBF \u0111\u1ED9 preview c\u1EA5p component) \u2014 kh\xF4ng d\xF9ng bundle server. ":"Project layer built from the LOCAL workspace (component preview), not the server bundle. ","Ch\u1EA1y `project_sync` n\u1EBFu nghi file project trong workspace \u0111\xE3 c\u0169.":"Run `fui project sync` if workspace project files look stale."," (\u0111\xE3 s\u1EEDa local, ch\u01B0a push)":" (edited locally, not pushed)"," (ch\u01B0a c\xF3 tr\xEAn server)":" (not on the server)"," (<style> \u0111\xE3 s\u1EEDa local, ch\u01B0a push)":" (<style> edited locally, not pushed)","\u26D4 Component c\u1EA5p project l\u1EC7ch v\u1EDBi b\u1EA3n tr\xEAn server: ":"\u26D4 Project components differ from the server: ","renderUI d\u1EF1ng trang b\u1EB1ng bundle project T\u1EA2I T\u1EEA SERVER (_aset/.../projectdata) n\xEAn b\u1EA3n local ch\u01B0a push ":"The render uses the project bundle FROM THE SERVER (_aset/.../projectdata), so unpushed local edits ","kh\xF4ng xu\u1EA5t hi\u1EC7n \u2014 n\u1EBFu v\u1EABn render, t\u1EA5m \u1EA3nh b\u1EA1n nh\u1EADn \u0111\u01B0\u1EE3c l\xE0 B\u1EA2N C\u0168, kh\xF4ng ph\u1EA3i b\u1EA3n v\u1EEBa s\u1EEDa.\n\n":`do not show; a render now shows the OLD version.
160
-
161
- `,"\u2022 N\u1EBFu b\u1EA3n local l\xE0 b\u1EA3n b\u1EA1n v\u1EEBa s\u1EEDa \u2192 `component_preview` \u0111\u1EC3 ki\u1EC3m ch\u1EE9ng ri\xEAng component (ch\u1EA1y \u0111\u01B0\u1EE3c b\u1EB1ng ":"\u2022 Local is your edit \u2192 `fui component preview` checks the component alone (uses ","file local, kh\xF4ng c\u1EA7n push), r\u1ED3i `component_update` t\u1EEBng component, r\u1ED3i render l\u1EA1i.\n":"local files), then `fui component update` each one, then render again.\n","\u2022 N\u1EBFu b\u1EA3n tr\xEAn server m\u1EDBi h\u01A1n (ai \u0111\xF3 s\u1EEDa tr\xEAn web IDE) \u2192 `project_sync` \u0111\u1EC3 l\u1EA5y v\u1EC1, \u0110\u1EEANG push \u0111\xE8.":"\u2022 Server is newer (edited in the web IDE) \u2192 `fui project sync`; do NOT overwrite it.","Kh\xF4ng \u0111\u1ED1i chi\u1EBFu \u0111\u01B0\u1EE3c component c\u1EA5p project: ":"Cannot compare project components: ","Kh\xF4ng ch\u1EB7n render, nh\u01B0ng n\u1EBFu giao di\u1EC7n kh\xE1c mong \u0111\u1EE3i th\xEC \u0111\xE2y l\xE0 ch\u1ED7 \u0111\xE1ng nghi \u0111\u1EA7u ti\xEAn.":"Render continues; if the UI looks wrong, suspect this first.","StyleCSS c\u1EA5p project t\u1EA3i v\u1EC1 R\u1ED6NG nh\u01B0ng component local c\xF3 khai <style>: ":"Project StyleCSS is EMPTY but local components declare <style>: ","Ho\u1EB7c c\xE1c component \u0111\xF3 ch\u01B0a push (`component_update`), ho\u1EB7c route StyleCSS \u0111ang h\u1ECFng \u2014 ":"Either they are not pushed (`fui component update`) or the StyleCSS route is broken; ","d\xF9 nguy\xEAn nh\xE2n n\xE0o th\xEC style c\u1EA5p project trong l\u1EA7n render n\xE0y c\u0169ng \u0111ang thi\u1EBFu.":"either way project styles are missing in this render.","Kh\xF4ng b\xF3c \u0111\u01B0\u1EE3c `var $projectData` kh\u1ECFi bundle projectdata t\u1EA3i t\u1EEB server \u2014 apiDomain/userInfo ":"Cannot extract `var $projectData` from the server projectdata bundle; apiDomain/userInfo ","trong l\u1EA7n render n\xE0y suy ra t\u1EEB project.json local, c\xF3 th\u1EC3 l\u1EC7ch v\u1EDBi b\u1EA3n trang th\u1EADt \u0111ang ch\u1EA1y.":"come from local project.json and may differ from the live page.",'project.json local c\xF3 apiDomain "':'Local project.json apiDomain "','" nh\u01B0ng b\u1EA3n tr\xEAn server l\xE0 "':'" but the server has "',"render d\xF9ng b\u1EA3n server (gi\u1ED1ng trang th\u1EADt). Ch\u1EA1y `project_sync` \u0111\u1EC3 \u0111\u1ED3ng b\u1ED9 file local.":"render uses the server value (like the live page). Run `fui project sync`.","(kh\xF4ng t\u1EA7ng n\xE0o khai)":"(not set anywhere)",'Kh\xF4ng t\u1EA7ng n\xE0o ($projectGroupSetting \u2192 $projectData \u2192 module.json set \u2192 domainSetting["':'No layer ($projectGroupSetting \u2192 $projectData \u2192 module.json set \u2192 domainSetting["','"]) khai apiDomain cho "':'"]) sets apiDomain for "','" \u2014 API s\u1EBD g\u1ECDi b\u1EB1ng \u0111\u01B0\u1EDDng d\u1EABn t\u01B0\u01A1ng \u0111\u1ED1i v\xE0 apiMocks kh\u1EDBp theo URL ':'"; APIs use relative paths, so URL-matched mocks ',"g\u1EA7n nh\u01B0 ch\u1EAFc ch\u1EAFn tr\u01B0\u1EE3t. Ch\u1EA1y `project_sync` n\u1EBFu d\u1EEF li\u1EC7u project \u0111\xE3 c\u0169.":"almost surely miss. Run `fui project sync` if project data is stale.","] (m\u1EB7c \u0111\u1ECBnh \u2014 qu\xE9t \u0111\u01B0\u1EE3c t\u1EEB module.json/project)":"] (default: scanned from module.json/project)","FunctionRight=[] (m\u1EB7c \u0111\u1ECBnh \u2014 kh\xF4ng qu\xE9t \u0111\u01B0\u1EE3c m\xE3 n\xE0o)":"FunctionRight=[] (default: no codes found)",". rightTest()/v-if theo FunctionRight kh\xE1c m\xE3 tr\xEAn s\u1EBD \u1EA9n \u2014 truy\u1EC1n tham s\u1ED1 `user` \u0111\u1EC3 test vai tr\xF2 h\u1EA1n ch\u1EBF h\u01A1n.":". rightTest()/v-if on other FunctionRight codes hides; pass --user to test a narrower role.","projectdata/StyleCSS: KH\xD4NG nh\xFAng (HTMLOnly=true \u2014 server c\u0169ng kh\xF4ng nh\xFAng)":"projectdata/StyleCSS: NOT embedded (HTMLOnly=true, same as the server)","projectdata/StyleCSS: d\u1EF1ng t\u1EEB workspace local (ch\u1EBF \u0111\u1ED9 preview c\u1EA5p component \u2014 b\u1EA3n local ch\u01B0a push v\u1EABn hi\u1EC7n)":"projectdata/StyleCSS: from the local workspace (component preview; unpushed edits show)","projectdata/StyleCSS: d\u1EF1ng t\u1EEB workspace local (kh\xF4ng t\u1EA3i \u0111\u01B0\u1EE3c b\u1EA3n server)":"projectdata/StyleCSS: from the local workspace (server copy unavailable)","g\u1ECDi tAPI th\u1EADt (SP ":"live tAPI call (SP ",", ngu\u1ED3n: ":", source: ","t\u1EF1 sinh mock (SP ":"generated mock (SP ","), \u0111\xE3 l\u01B0u cache":"), cached","Kh\xF4ng launch \u0111\u01B0\u1EE3c Chromium: ":"Cannot launch Chromium: ","\n\u2192 Ch\u1EA1y m\u1ED9t l\u1EA7n: npx playwright install chromium":`
162
- \u2192 Run once: bunx playwright install chromium`,'runtime \u0111i\u1EC1u h\u01B0\u1EDBng ra "':'runtime navigated to "','" \u2014 \u0111\xE3 ch\u1EB7n (th\u01B0\u1EDDng do login gate / redirect trong action)':'"; blocked (usually a login gate / redirect in an action)','" t\u1EA3i th\u1EA5t b\u1EA1i (':'" failed to load ('," \u2014 tr\u1EA3 404 (offline + ch\u01B0a cache?)":"; answered 404 (offline, not cached?)"," \u2192 user preview (userInfo c\u1EE7a project, auto-mock)":" \u2192 preview user (project userInfo, auto-mock)",'" \u2014 tr\u1EA3 {data: []}':'": {data: []}'," \u2192 {data:[]} (kh\xF4ng c\xF3 mock)":" \u2192 {data:[]} (no mock)","module m\u1EDF WebSocket t\u1EDBi backend th\u1EADt \u2014 \u0111\xE3 ch\u1EB7n (renderUI ch\u1EC9 cho ph\xE9p g\u1ECDi th\u1EADt qua tAPI SP \u0111\xE3 x\xE1c nh\u1EADn an to\xE0n, kh\xF4ng qua WebSocket)":"module opened a WebSocket to the real backend; blocked (only verified-safe tAPI SPs run live)","Vue ch\u01B0a mount sau 15s \u2014 trang ch\u01B0a init xong (xem JS error / asset h\u1ECFng / API treo b\xEAn d\u01B0\u1EDBi)":"Vue not mounted after 15s; page init unfinished (see JS errors / broken assets / hung APIs below)"," C\u1EACP NH\u1EACT":" UPDATED"," t\u1EA3i m\u1EDBi":" fetched"," g\u1ECDi th\u1EADt":" live"," t\u1EF1 sinh":" generated"," (API kh\xF4ng-mock: ":" (unmocked API: ","trang load xong sau ":"page loaded in ",': kh\xF4ng t\xECm th\u1EA5y ph\u1EA7n t\u1EED c\xF3 text "':': no element with text "','" tr\xEAn DOM':'" in the DOM'," ph\u1EA7n t\u1EED \u2014 d\xF9ng #":" elements; using #",'CONFIRM \u2192 b\u1EA5m "':'CONFIRM \u2192 click "',"CONFIRM \u2192 b\u1EA5m n\xFAt cu\u1ED1i (kh\xF4ng nh\u1EADn di\u1EC7n \u0111\u01B0\u1EE3c label)":"CONFIRM \u2192 click the last button (label not recognized)","CONFIRM \u2192 b\u1EA5m n\xFAt \u0111\u1EA7u (kh\xF4ng nh\u1EADn di\u1EC7n \u0111\u01B0\u1EE3c label)":"CONFIRM \u2192 click the first button (label not recognized)","global CALL kh\xF4ng t\u1ED3n t\u1EA1i trong runtime n\xE0y":"global CALL missing in this runtime","ch\u1ED3ng l\u1EA5n":"overlap","d\xEDnh s\xE1t (gap ":"touching (gap ","canvas trong ":"canvas in ","\u2718 <body> kh\xF4ng render \u0111\u01B0\u1EE3c ph\u1EA7n t\u1EED n\xE0o (HTMLOnly \u2014 body.html L\xC0 c\u1EA3 trang)":"\u2718 <body> rendered nothing (HTMLOnly: body.html IS the page)","\u2718 #fastProjectAPP r\u1ED7ng \u2014 controls kh\xF4ng render":"\u2718 #fastProjectAPP empty: controls not rendered"," (C\xD3 file to\xE0n c\u1EE5c n\u1EA1p thi\u1EBFu \u2014 xem m\u1EE5c M\xD4I TR\u01AF\u1EDCNG THI\u1EBEU FILE tr\u01B0\u1EDBc khi nghi module.json)":" (global files FAILED to load; see MISSING ENV FILES before suspecting module.json)"," (ki\u1EC3m tra pageerror / module.json)":" (check pageerror / module.json)","\u26A0 trang tr\xE0n ngang viewport ":"\u26A0 page overflows the viewport ","px \u2014 ph\u1EA7n t\u1EED tr\xE0n: ":"px; overflowing: ","(kh\xF4ng x\xE1c \u0111\u1ECBnh)":"(unknown)","\u26A0 text nghi v\u1EA5n hi\u1EC3n th\u1ECB tr\xEAn trang: ":"\u26A0 suspect text on page: "," (binding thi\u1EBFu data?)":" (binding missing data?)","\u26A0 \u1EA3nh v\u1EE1 (load fail nh\u01B0ng \u0111ang chi\u1EBFm ch\u1ED7): ":"\u26A0 broken images (failed but taking space): ","\u26A0 canvas chart 0-size \u2014 chart init khi container ch\u01B0a c\xF3 k\xEDch th\u01B0\u1EDBc: ":"\u26A0 0-size chart canvas (init before the container had a size): ","\u26A0 text d\xEDnh/ch\u1ED3ng l\u1EA5n (th\u1EA9m m\u1EF9 \u2014 PH\u1EA2I s\u1EEDa, kh\xF4ng \u0111\u01B0\u1EE3c b\u1ECF qua): ":"\u26A0 text collisions (visual; MUST fix): ","RENDER (browser th\u1EADt): ":"RENDER (real browser): "," \xB7 HTMLOnly=true (KH\xD4NG nh\xFAng framework/projectdata \u2014 nh\u01B0 production)":" \xB7 HTMLOnly=true (no framework/projectdata, like production)","Origin trang: ":"Page origin: ","Import (framework \u222A project \u222A module, sort theo FileSort/FileName): ":"Imports (framework \u222A project \u222A module, by FileSort/FileName): "," n\u1EA1p \u0111\u01B0\u1EE3c":" loaded"," KH\xD4NG n\u1EA1p \u0111\u01B0\u1EE3c":" NOT loaded","\u2500\u2500 \u26D4 M\xD4I TR\u01AF\u1EDCNG THI\u1EBEU FILE (\u0111\u1ECDc TR\u01AF\u1EDAC khi s\u1EEDa module) \u2500\u2500":"\u2500\u2500 \u26D4 MISSING ENV FILES (read BEFORE fixing the module) \u2500\u2500","C\xE1c file d\u01B0\u1EDBi \u0111\xE2y kh\xF4ng n\u1EA1p \u0111\u01B0\u1EE3c v\xE0o trang test. H\xE0m/bi\u1EBFn/style to\xE0n c\u1EE5c do ch\xFAng":"These files did not load. Their global functions/vars/styles","\u0111\u1ECBnh ngh\u0129a s\u1EBD kh\xF4ng t\u1ED3n t\u1EA1i \u2192 l\u1ED7i JS v\xE0 l\u1ED7i hi\u1EC3n th\u1ECB b\xEAn d\u01B0\u1EDBi C\xD3 TH\u1EC2 ch\u1EC9 l\xE0 h\u1EC7 qu\u1EA3,":"are missing, so JS and display errors below MAY only be consequences,","kh\xF4ng ph\u1EA3i l\u1ED7i c\u1EE7a module. Kh\u1EAFc ph\u1EE5c ph\u1EA7n n\xE0y tr\u01B0\u1EDBc r\u1ED3i render l\u1EA1i.":"not module faults. Fix this first, then render again.","\u2500\u2500 \u26D4 ASSET C\u0168, KH\xD4NG X\xC1C MINH \u0110\u01AF\u1EE2C (\u0111\u1ECDc TR\u01AF\u1EDAC khi k\u1EBFt lu\u1EADn v\u1EC1 platform) \u2500\u2500":"\u2500\u2500 \u26D4 STALE, UNVERIFIED ASSETS (read BEFORE blaming the platform) \u2500\u2500","Kh\xF4ng h\u1ECFi \u0111\u01B0\u1EE3c server xem c\xE1c file d\u01B0\u1EDBi \u0111\xE2y c\xF2n \u0111\xFAng kh\xF4ng, n\xEAn trang test \u0111ang ch\u1EA1y":"The server could not confirm these files, so the page ran",'b\u1EB1ng b\u1EA3n tr\xEAn \u0111\u0129a. M\u1ECDi k\u1EBFt lu\u1EADn ki\u1EC3u "th\u01B0 vi\u1EC7n X c\xF3 bug" r\xFAt ra t\u1EEB l\u01B0\u1EE3t render n\xE0y':'on disk copies. Any "library X has a bug" conclusion from this render',"\u0111\u1EC1u KH\xD4NG d\xF9ng \u0111\u01B0\u1EE3c \u2014 b\u1EA3n th\u1EADt tr\xEAn CDN c\xF3 th\u1EC3 \u0111\xE3 kh\xE1c. N\u1ED1i m\u1EA1ng r\u1ED3i render l\u1EA1i,":"is NOT valid; the CDN copy may differ. Go online and render again,","ho\u1EB7c \xE9p t\u1EA3i m\u1EDBi b\u1EB1ng refreshAssets: true.":"or force a fetch with --refresh-assets.","\u2500\u2500 \u2139\uFE0F ASSET PLATFORM \u0110\xC3 \u0110\u1ED4I so v\u1EDBi l\u01B0\u1EE3t render tr\u01B0\u1EDBc \u2500\u2500":"\u2500\u2500 \u2139\uFE0F PLATFORM ASSETS CHANGED since the previous render \u2500\u2500","H\xE0nh vi \u0111\u1ED5i m\xE0 module.json kh\xF4ng \u0111\u1EE5ng t\u1EDBi c\xF3 th\u1EC3 \u0111\u1EBFn t\u1EEB \u0111\xE2y, kh\xF4ng ph\u1EA3i t\u1EEB module.":"Behavior changes not caused by module.json may come from here.","\u2500\u2500 STATE CU\u1ED0I (vueData) \u2500\u2500":"\u2500\u2500 FINAL STATE (vueData) \u2500\u2500"," (\u0111\xEDnh k\xE8m b\xEAn d\u01B0\u1EDBi)":" (attached below)"," (qu\xE1 l\u1EDBn \u0111\u1EC3 inline \u2014 d\xF9ng Read tool \u0111\u1EC3 xem)":" (too large to inline; open it with the Read tool)","Dialog \u0111ang m\u1EDF \u2014 ch\u1EE5p ri\xEAng: ":"Open dialog screenshot: "," API call (t\u1EA5t c\u1EA3 mock) \xB7 ":" API call (all mocked) \xB7 "," l\xFAc init) \xB7 ":" at init) \xB7 "," \xB7 dialog \u0111ang m\u1EDF":" \xB7 open dialog"},ch=Object.entries(ah).sort(([e],[t])=>t.length-e.length);function Be(e){let t=e;for(let[n,o]of sh)t=t.replace(n,o);for(let[n,o]of ch)t=t.replaceAll(n,o);return t.replace(/`(project_sync|module_get|file_import_list)`/g,(n,o)=>`\`${ih[o]}\``)}async function mn(e){let t=$r(await Nn(e));return t?[t]:[]}async function Ec(e,t){let n=await No(e);if(n.errors.length===0&&n.warnings.length===0)return[];let o=Be(_o(n));if(n.errors.length>0&&Jt())throw new Se(`[FUI_MCP_STRICT] ${t} blocked: module.json has semantic errors; fix them and retry
163
-
164
- ${o}`,"hard","error");return["--- module validate ---",o]}async function Ut(e){let t=await U(lt(e,"_moduleInfo.json"),null),n=t?.ProjectID&&t?.ModuleID?`fui module get -p ${t.ProjectID} -m ${t.ModuleID}`:"fui module get";return At(e,"local module",n,"_moduleInfo.json")}async function Ac(e){let t=await Ut(e),n=await No(e);return{notes:[...t?[t]:[],...await mn(e)],data:`${Be(_o(n))}
165
- `,errorCount:n.errors.length}}async function Nc(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 Ro(t)}),r=await Ut(t),i=await No(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 ---",Be(_o(i)),`approval token: ${s.token}`,"ask the user; only after explicit approval run: fui module publish html --approval <token>"]}}async function _c(e,t,n){let o=await Mt(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 Ro(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 Ec(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 mn(t)]}}async function xo(e,t){let n=await Mt(e.session,"module publish json"),o=await F(lt(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 Yt(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 Ec(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 mn(t.moduleDir)]}}async function Oo(e,t,n){let o=`module publish ${n==="script.js"?"script":"css"}`,s=await Mt(e.session,o),r=await Xr(lt(t.moduleDir,n),n);if(await Yt(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 mn(t.moduleDir)]}}function pn(e){return e.moduleId?`module (${e.moduleId})`:"project (global, loaded in all modules)"}async function Dc(e,t){let n=await F(lt(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 Do(e){return U(lt(e,"components","_components.json"),[])}async function xc(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 Mo(e,t,n){let o=await Mt(e.session,"component new"),s=await Dc(t,n),r=await ce(e);await r.syncComponentsLocal(t.projectId,t.moduleId);let i=(await Do(t.baseDir)).find((m)=>m.comName===n);if(i)throw new h(`component "${n}" already exists in ${pn(t)} (ComID ${i.comID}); overwrite it: fui component update ${n}, or pick another name`);let{template:a,script:c,style:l}=kt(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 Do(t.baseDir)).find((m)=>m.comName===n);return await Ee(t.baseDir,[ve(lt("components",`${n}.vue`))]).catch(()=>{return}),{notes:[...o,`ok created component ${n} in ${pn(t)} (ComID ${p?.comID??"not visible yet; run fui component list"})`,`${u} component(s) synced${d.length>0?`; removed orphaned: ${d.join(", ")}`:""}`,...await mn(t.baseDir)]}}async function Fo(e,t,n){let o=await Mt(e.session,"component update"),s=await Dc(t,n),r=(await Do(t.baseDir)).find((m)=>m.comName===n)?.comID;if(r===void 0)throw new h(`component "${n}" is not in _components.json of ${pn(t)}, so there is no ComID to overwrite. new component: fui component new ${n}; stale cache: fui component list, then retry`);let i=ve(lt("components",`${n}.vue`));if(await Yt(t.baseDir,i,s))return{notes:[...o,`skip ${n}.vue unchanged since the last publish/sync`]};let{template:a,script:c,style:l}=kt(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 ${pn(t)} (ComID ${r})`,`${d} component(s) synced${p.length>0?`; removed orphaned: ${p.join(", ")}`:""}`,...await mn(t.baseDir)]}}async function Oc(e,t,n){let o=(await Do(t.baseDir)).find((a)=>a.comName===n);if(!o)throw new h(`component "${n}" not in the local _components.json of ${pn(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 ${pn(t)}; ${r} remain`,...i.length>0?[`local files removed (archived): ${i.join(", ")}`]:[]]}}async function Mc(e,t){let{id:n,...o}=t,s=await ce(e),r=await s.api.editProject(o),i=await s.projectInfoRefreshNote(Lc(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)}
166
- `}}async function Fc(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(Lc(t,s));return{notes:["ok project updated",...r?[r]:[]],data:`${JSON.stringify(s)}
167
- `}}function Lc(e,t){let n=xt(e)?e.id??e.ProjectID:void 0;if(typeof n==="string"&&n.trim())return n;let o=xt(t)?t.data:void 0,s=Array.isArray(o)?o[0]:o,r=xt(s)?s.ProjectID??s.id:void 0;return typeof r==="string"&&r.trim()?r:null}async function Lo(e,t){let n=await Mt(e.session,"project publish-config"),o=e.paths.projectDir(t),s=await F(lt(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}=os(r);if(await Yt(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)}
168
- `}}async function lh(e,t){let n=await F(lt(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 Uc(e){let t=await lh(e,"outline"),n=await Ut(e);return{notes:n?[n]:[],data:`Outline: ${e}
169
-
170
- ${Be(Tc(t))}
171
- `}}var uh=["\u2718","\u2714","\u274C","\u26A0","\u2757","\uD83D\uDEA8","Stats:","Screenshot:","screenshot:","Full-page:"],dh=["Simulate:","Render UI:","live ","skip ","cache ","gen "];function Hc(e){let t=e.split(`
172
- `).filter((o)=>o.trim()),n=t.filter((o)=>uh.some((s)=>o.includes(s))||dh.some((s)=>o.startsWith(s)));return{text:`${n.join(`
151
+ `),e.data)k(e.data)}async function Cs(e){return(await e.targets.resolveProject(f(e.values,"project"))).projectId}async function Fc(e){return e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module"))}function mh(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 Lc(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 hh={name:"use",mcpName:"session_set",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:{...wn,"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
152
+ `);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(Y([{...l,session:o.file}],s));return}if(i&&!a||!i&&a)throw new m("pass both -p and -m");Qn(await Rc(M(e),{target:r,projectId:i,moduleId:a,moduleDir:c}))}},fh={name:"resolve",mcpName:"session_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>");Vo(await Cc(M(e),t))}},gh={name:"project list",mcpName:"project_list",usage:"fui project list",summary:"List projects you can access",async run(e){let t=(await Tc(M(e))).map((n)=>({group:n.GroupName||"Khac",id:n.ProjectID,name:n.ProjectName,framework:n.Framework??"",domain:n.pDomain??""}));k(Y(t,e.render))}},wh={name:"project sync",mcpName:"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){Qn(await Pc(M(e),await Cs(e)))}},yh={name:"module list",mcpName:"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 Ec(M(e),await Cs(e));for(let o of n)T(`# ${o}`);k(Y(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))}},bh={name:"module new",mcpName:"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");Qn(await _c(M(e),{projectId:await Cs(e),moduleId:t,moduleName:n,framework:Lc(f(e.values,"framework")),htmlOnly:e.values["html-only"]===!0?!0:void 0,mDomain:f(e.values,"domain"),extra:gn(e.values.set)}))}},kh={name:"module update",mcpName:"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:{...wn,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 Fc(e);Qn(await Ac(M(e),{projectId:t,moduleId:n,moduleName:f(e.values,"name"),framework:Lc(f(e.values,"framework")),htmlOnly:mh(f(e.values,"html-only"),"--html-only"),mDomain:f(e.values,"domain"),newModuleId:f(e.values,"new-id"),extra:gn(e.values.set)}))}},vh={name:"module delete",mcpName:"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:wn,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>");Qn(await Nc(M(e),t,n))}},Sh={name:"module get",mcpName:"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:wn,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");Vo(await Dc(M(e),{target:e.positionals[0],projectId:t,moduleId:n}))}};function Mc(e){return{name:`module get-${e}`,mcpName:`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:wn,async run(t){let{projectId:n,moduleId:o}=await Fc(t);Vo(await xc(M(t),n,o,e))}}}var $h={name:"component get",mcpName:"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:wn,async run(e){let t=e.positionals[0];if(!t)throw new m("missing <name>");let n=M(e),o=await fn(n,f(e.values,"project"),f(e.values,"module"));Vo(await Oc(n,o,t))}},Uc=[hh,fh,gh,wh,yh,bh,kh,vh,Sh,Mc("ui"),Mc("html"),$h];import{resolve as Zf}from"node:path";import{join as pt}from"node:path";import dt from"node:path";import{join as Hc}from"node:path";async function qt(e,t){let n={functionRights:null,systemRights:null},o;try{o=JSON.parse(await W(Hc(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(Hc(e,"_db","_connections.json"),null);i=l?.default?r(l.default):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 Bt(e){return Wc("FunctionRight",e)}function Vc(e){return Wc("SystemRight",e)}function Zn(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 Wc(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 jh=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-openwindow-control","f-table-item-component"]),qc=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"]),Ih={"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 yn=new Set(["API","CALL","IF","FUN","EXE","MESS","MESSBOX","ARRAY","CONFIRM","IN","OUT","THEN","ELSE","CALLBACK","ERROROUT","ERRORCALLBACK","CANCEL","METHOD","HEADER","COL"]),Bc=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 Ps(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)=>yn.has(n)));if(!j(e))return!1;return Object.keys(e).some((t)=>yn.has(t))}function Ze(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 eo(e){return!!e&&!e.includes(" ")&&!e.includes("{{")&&!e.includes("(")&&!e.startsWith("`")&&!e.startsWith("#")&&!e.startsWith("!")&&/^[A-Za-z_$]/.test(e.replace(/^vueData\./,""))}function Ch(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 Rh(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 Th=[{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 Ph(e,t,n,o){if(typeof t!=="string")return;if(!(e.startsWith(":")&&!e.startsWith("::")||e.startsWith("v-bind:")))return;let r=Rh(t),i=[];for(let{re:a,what:c}of Th)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 Le(e,t,n="$",o=!1){if(Array.isArray(e))e.forEach((s,r)=>Le(s,t,`${n}[${r}]`,o));else if(j(e)){t(e,n,o);for(let[s,r]of Object.entries(e))Le(r,t,`${n}.${s}`,o||s.startsWith("#"))}}var Eh="(chuẩn mặc định fui/DESIGN.md — chỉ giữ nguyên nếu user yêu cầu rõ kiểu khác)",_h=new Set([400,550,600,650,800]),Ah=/(xóa|xoá|\bdelete\b|\bremove\b)/i,Nh=/(delete|remove|xoa)/i,Dh=new Set(["v-text-field","v-textarea","v-select","v-combobox","v-autocomplete","f-date","f-time","f-search"]),Jc=new Set(["9"]);function xh(e,t,n){let o=JSON.stringify(e),s=Bt([o]),r=Vc([o]).filter((l)=>!Jc.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)"}${Jc.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 Oh(e,t){let n=(a)=>t.push(`[design] ${a} ${Eh}`),o=(a)=>typeof a==="string"?a:"";Le(e,(a,c)=>{let l=a.API;if(typeof l==="string"&&Nh.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;Le(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(!Ah.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)&&!_h.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),E=v.includes("flex-md-grow-0")&&v.includes("ml-2");if(E)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&&!E&&Dh.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 Mh=12,Rs=25,Gc=/\b(shrink|flex-grow-0)\b/;function zc(e){return typeof e==="string"?e:j(e)?[e.class,e[":class"]].filter((t)=>typeof t==="string").join(" "):""}function Fh(e){return e.w===void 0||e.w===null||e.w===""||e.w===0}function Lh(e,t){Le(e.controls,(n,o)=>{if(!Array.isArray(n.cols))return;Uh(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<=Mh)return;let l=`${o}.cols[${r}]${typeof i.el==="string"?` (el: ${i.el})`:""}`;if(c<Rs){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 >= ${Rs} (px, kèm \`"col": { "class": "shrink" }\`).`);return}let u=i.col;if(Gc.test(zc(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ố >= ${Rs} 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 Uh(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=Gc.test(zc(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(!Fh(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 Kc=["API","CALL","EXE","FUN","IF","MESS","MESSBOX"],Hh=new Set(["IN","OUT","HEADER","METHOD","CALLBACK","ERROROUT","ERRORCALLBACK","THEN","ELSE","CONFIRM","CANCEL","ARRAY","COL","MAP"]);function Vh(e,t,n){for(let[o,s]of Object.entries(e)){if(o==="set")continue;Le(s,(r,i)=>{let a=Object.keys(r),c=Kc.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)&&!Hh.has(h)&&!Kc.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 Wh(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:Ts()};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(Bc),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(yn.has(b))continue;if(b.startsWith("#"))continue;if(Ps(R))a.add(b);else i.add(Ze(b))}}let l=0;Le(s,(g,b,R)=>{if(R)return;for(let C of["OUT","ERROROUT"]){let S=g[C];if(typeof S==="string"){if(eo(S))i.add(Ze(S));else if(S.trim())l++}else if(j(S))for(let A of Object.keys(S))i.add(Ze(A))}});let u=new Map,d=new Map;Le(s,(g,b,R)=>{let C=g.CALL;if(typeof C==="string"){if(eo(C))(R?d:u).set(Ze(C),b);else if(C.trim()&&!R)l++}let S=g.API;if(typeof S==="string"&&S.trim()&&!S.startsWith("/")&&!eo(S))l++;for(let[A,D]of Object.entries(g)){if(typeof D!=="string")continue;let _=R||A.startsWith("#")?d:u;for(let z of D.matchAll(/\bCALL\(\s*([A-Za-z_$][\w$.]*)\s*\)/g))_.set(Ze(z[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 A=C.cols[S];if(h++,!j(A))continue;if(A.w===void 0&&A.el!==void 0)o.push(`controls[${g}].rows[${R}].cols[${S}] (el: ${String(A.el)}) thiếu "w" — bắt buộc cho item trong cols.`)}}}let y=new Set;if(Le(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-"))&&!jh.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-")&&!qc.has(R))o.push(`Cell component "${R}" (tại ${b}) không có trong danh sách t-* hợp lệ: ${[...qc].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,A]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 A==="string"){if(A.split(";").map((_)=>_.trim()).filter(Boolean).filter((_)=>!/^(max-|min-)?width\s*:/i.test(_)).filter((_)=>!(g.el==="v-card"&&/^padding\s*:\s*0(px)?$/i.test(_))).length>0)o.push(`attr style "${A}" (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 A==="string"&&!Ch(A))l++;else if(S==="v-model"&&typeof A==="string"){if(eo(A)){let D=Ze(A);if(!i.has(D)&&!a.has(D))o.push(`v-model "${A}" (tại ${b}) — biến gốc "${D}" chưa khai báo trong data[]. Khai báo state trước để reactive hoạt động đúng.`)}else if(A.trim())l++}Ph(S,A,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=Ze(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=Ih[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,E=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(Le(s,(b,R)=>{let C=b.API;if(typeof C!=="string"||!C||C.includes("{{"))return;v++;let S=null,A=null;if(C.startsWith("/")){let _=C.replace(/^\/+/,"").split("/").filter(Boolean);if(_.length<2)return;if(A=t.schemaProcs.get(_[0].toLowerCase())??null,!A){E.add(_[0]);return}S=_[1]==="auth"?_[2]??null:_[1]}else if(eo(C))S=C,A=g;if(!S||S.includes("`")||!A)return;if(![`spAPI_${S}`,`spAPI_AUTH_${S}`,`spAPIFILE_${S}`,`spAPIFILE_UPLOAD_${S}`,S].some((_)=>A.has(_)))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).`)}),E.size>0){let b=[...E].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=xh(s,t,o);return Oh(s,o),Lh(s,o),Vh(s,n,o),{errors:n,warnings:o,stats:{stateKeys:i.size-Bc.size,namedActions:a.size,containers:p,controls:h,callRefs:u.size,apiRefs:v,crossWindowCalls:d.size,dynamicRefs:l,rightRefs:I}}}function Ts(){return{stateKeys:0,namedActions:0,containers:0,controls:0,callRefs:0,apiRefs:0,crossWindowCalls:0,dynamicRefs:0,rightRefs:0}}function qh(e){return e.match(/^CREATE (?:PROCEDURE|FUNCTION)\s+(\S+)\s*\(/i)?.[1]??null}async function Bh(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 Fe(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 of Object.keys(s?.connections??{})){let l=await H(dt.join(t,"_db",c,"schema.json"),null),u=Array.isArray(l?.procedures)?l.procedures:null;if(u===null)continue;if(!u.every((d)=>typeof d==="string")){i.push(c);continue}r.set(c.toLowerCase(),new Set(u.map(qh).filter((d)=>d!==null)))}let a=await qt(t);return{localComponents:n,importsText:o.join(`
153
+ `).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 Wo(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:Ts()};let n;try{n=JSON.parse(t)}catch(s){return{errors:[`module.json có lỗi JSON syntax: ${s.message}`],warnings:[],stats:Ts()}}let o=await Bh(e);return Wh(n,o)}function qo(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(`
154
+ `)}function te(e,t){let n=e.replace(/\s+/g," ").trim();return n.length>t?n.slice(0,t)+"…":n}function to(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?"{}":`{${te(t.join(","),40)}}`}if(typeof e==="string")return`"${te(e,30)}"`;return String(e)}function De(e,t=0){if(t>3)return"…";if(typeof e==="string")return te(e,70);if(Array.isArray(e))return e.map((o)=>De(o,t+1)).join(" ; ");if(!j(e))return te(JSON.stringify(e)??"",60);let n=[];if(e.CONFIRM!==void 0)n.push(`CONFIRM "${te(String(e.CONFIRM),40)}"`);if(e.IF!==void 0)n.push(`IF(${te(String(e.IF),50)})`);if(e.API!==void 0)n.push(`API ${te(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)?te(Object.keys(e.IN).join(","),50):te(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 ${te(String(e.ARRAY),30)}${e.COL!==void 0?` COL ${te(String(e.COL),30)}`:""}`);if(e.CALL!==void 0)n.push(`CALL ${te(String(e.CALL),40)}`);if(e.FUN!==void 0)n.push(`FUN ${te(String(e.FUN),40)}`);if(e.EXE!==void 0)n.push(`EXE ${te(String(e.EXE),70)}`);if(e.MESS!==void 0)n.push(`MESS "${te(String(e.MESS),40)}"`);if(e.MESSBOX!==void 0)n.push(`MESSBOX "${te(String(e.MESSBOX),40)}"`);if(e.THEN!==void 0)n.push(`THEN[${De(e.THEN,t+1)}]`);if(e.ELSE!==void 0)n.push(`ELSE[${De(e.ELSE,t+1)}]`);if(e.CALLBACK!==void 0)n.push(`CALLBACK[${De(e.CALLBACK,t+1)}]`);if(e.ERROROUT!==void 0)n.push(`ERROROUT→${String(e.ERROROUT)}`);if(e.ERRORCALLBACK!==void 0)n.push(`ERRORCALLBACK[${De(e.ERRORCALLBACK,t+1)}]`);if(e.CANCEL!==void 0)n.push(`CANCEL[${De(e.CANCEL,t+1)}]`);return n.join(" ")||te(JSON.stringify(e)??"",60)}var Jh=["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 Kh(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 Jh){let i=s[r];if(i===void 0)continue;t.push(`${r}=${typeof i==="string"?te(i,45):to(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"?te(i,60):De(i)}`)}for(let r of["innerText","html","text"])if(typeof e[r]==="string"&&e[r])t.push(`"${te(String(e[r]),35)}"`);return t.join(" ")}function Xc(e,t,n,o){let s=o?"└─":"├─",r=t+(o?" ":"│ ");if(!j(e)){n.push(`${t}${s} ${te(JSON.stringify(e)??String(e),60)}`);return}n.push(`${t}${s} ${Kh(e)}`);let i=e.innerHTML;if(Array.isArray(i))i.forEach((a,c)=>Xc(a,r,n,c===i.length-1));if(Array.isArray(e.rows))Yc(e.rows,r,n)}function Yc(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}=${te(String(o[u]),40)}`);n.push(`${t}row ${s+1}${l.length?` [${l.join(" ")}]`:""}${c}`),r.forEach((u,d)=>Xc(u,t+" ",n,d===r.length-1))})}function Qc(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)=>yn.has(h))){r.push(d);continue}for(let[h,w]of Object.entries(d)){if(yn.has(h)){r.push({[h]:w});continue}if(h.startsWith("#"))continue;if(Ps(w))s.set(h,w);else o.set(Ze(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="${te(d.prop,40)}"`);for(let E of["v-if","v-show","v-model"]){let I=d[E]??y[E];if(typeof I==="string")v.push(`${E}=${te(I,40)}`)}if(n.push(`[container ${p+1}${w?" — DIALOG":""}] ${v.join(" ")||"(mặc định)"}`),w){let E=d["v-model"]??y["v-model"];if(typeof E==="string")u.push({vModel:Ze(E),where:`container ${p+1}`});else u.push({vModel:"",where:`container ${p+1}`})}if(Array.isArray(d.rows))Yc(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} = ${to(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}: ${De(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}): ${De(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} → ${De(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"?te(p,60):to(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(`
155
+ `)}var Gh=[[/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: "]],zh={project_sync:"fui project sync",module_get:"fui module get",file_import_list:"fui import list"},Xh={'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":`
156
+
157
+ ── Live/Cache data (SPs without mocks, auto-resolved) ──
158
+ `,"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.
159
+
160
+ `,"• 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":`
161
+ → 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"},Yh=Object.entries(Xh).sort(([e],[t])=>t.length-e.length);function qe(e){let t=e;for(let[n,o]of Gh)t=t.replace(n,o);for(let[n,o]of Yh)t=t.replaceAll(n,o);return t.replace(/`(project_sync|module_get|file_import_list)`/g,(n,o)=>`\`${zh[o]}\``)}async function kn(e){let t=Fr(await Un(e));return t?[t]:[]}async function Zc(e,t){let n=await Wo(e);if(n.errors.length===0&&n.warnings.length===0)return[];let o=qe(qo(n));if(n.errors.length>0&&en())throw new $e(`[FUI_MCP_STRICT] ${t} blocked: module.json has semantic errors; fix them and retry
162
+
163
+ ${o}`,"hard","error");return["--- module validate ---",o]}async function Jt(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 Ft(e,"local module",n,"_moduleInfo.json")}async function el(e){let t=await Jt(e),n=await Wo(e);return{notes:[...t?[t]:[],...await kn(e)],data:`${qe(qo(n))}
164
+ `,errorCount:n.errors.length}}async function tl(e,t){let o=await(await le(e)).previewStagedModuleDir(t),s=await e.session.createApproval({projectId:o.moduleInfo.ProjectID,moduleId:o.moduleInfo.ModuleID,moduleDir:t,contentHash:await Fo(t)}),r=await Jt(t),i=await Wo(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 ---",qe(qo(i)),`approval token: ${s.token}`,"ask the user; only after explicit approval run: fui module publish html --approval <token>"]}}async function nl(e,t,n){let o=await Wt(e.session,"module publish html"),s=(c)=>new $e(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 Fo(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 Zc(t,"module publish html"),a=await(await le(e)).publishStagedModuleDir(t);return await e.session.clearApproval(),{notes:[...o,`ok published ${a.ProjectID}/${a.ModuleID} (${t})`,...i,...await kn(t)]}}async function Jo(e,t){let n=await Wt(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 sn(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 Zc(t.moduleDir,"module publish json");return await(await e.api()).updateModuleUI(t.projectId,t.moduleId,o),await Ae(t.moduleDir,["module.json"]).catch(()=>{return}),{notes:[...n,`ok published module.json to ${t.projectId}/${t.moduleId}`,...s,...await kn(t.moduleDir)]}}async function Ko(e,t,n){let o=`module publish ${n==="script.js"?"script":"css"}`,s=await Wt(e.session,o),r=await ps(pt(t.moduleDir,n),n);if(await sn(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 Ae(t.moduleDir,[n]).catch(()=>{return}),{notes:[...s,`ok published ${n} to ${t.projectId}/${t.moduleId}`,...await kn(t.moduleDir)]}}function bn(e){return e.moduleId?`module (${e.moduleId})`:"project (global, loaded in all modules)"}async function ol(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 Bo(e){return H(pt(e,"components","_components.json"),[])}async function rl(e,t){let{count:n,removed:o}=await(await le(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 Go(e,t,n){let o=await Wt(e.session,"component new"),s=await ol(t,n),r=await le(e);await r.syncComponentsLocal(t.projectId,t.moduleId);let i=(await Bo(t.baseDir)).find((h)=>h.comName===n);if(i)throw new m(`component "${n}" already exists in ${bn(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 Bo(t.baseDir)).find((h)=>h.comName===n);return await Ae(t.baseDir,[Se(pt("components",`${n}.vue`))]).catch(()=>{return}),{notes:[...o,`ok created component ${n} in ${bn(t)} (ComID ${p?.comID??"not visible yet; run fui component list"})`,`${u} component(s) synced${d.length>0?`; removed orphaned: ${d.join(", ")}`:""}`,...await kn(t.baseDir)]}}async function zo(e,t,n){let o=await Wt(e.session,"component update"),s=await ol(t,n),r=(await Bo(t.baseDir)).find((h)=>h.comName===n)?.comID;if(r===void 0)throw new m(`component "${n}" is not in _components.json of ${bn(t)}, so there is no ComID to overwrite. new component: fui component new ${n}; stale cache: fui component list, then retry`);let i=Se(pt("components",`${n}.vue`));if(await sn(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 le(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 Ae(t.baseDir,[i]).catch(()=>{return}),{notes:[...o,`ok updated component ${n} in ${bn(t)} (ComID ${r})`,`${d} component(s) synced${p.length>0?`; removed orphaned: ${p.join(", ")}`:""}`,...await kn(t.baseDir)]}}async function sl(e,t,n){let o=(await Bo(t.baseDir)).find((a)=>a.comName===n);if(!o)throw new m(`component "${n}" not in the local _components.json of ${bn(t)}; run fui component list first`);let s=await le(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 ${bn(t)}; ${r} remain`,...i.length>0?[`local files removed (archived): ${i.join(", ")}`]:[]]}}async function il(e,t){let{id:n,...o}=t,s=await le(e),r=await s.api.editProject(o),i=await s.projectInfoRefreshNote(cl(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)}
165
+ `}}async function al(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 le(e),s=await o.api.editProject(t),r=await o.projectInfoRefreshNote(cl(t,s));return{notes:["ok project updated",...r?[r]:[]],data:`${JSON.stringify(s)}
166
+ `}}function cl(e,t){let n=Vt(e)?e.id??e.ProjectID:void 0;if(typeof n==="string"&&n.trim())return n;let o=Vt(t)?t.data:void 0,s=Array.isArray(o)?o[0]:o,r=Vt(s)?s.ProjectID??s.id:void 0;return typeof r==="string"&&r.trim()?r:null}async function Xo(e,t){let n=await Wt(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}=bs(r);if(await sn(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 Ae(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)}
167
+ `}}async function Qh(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 ll(e){let t=await Qh(e,"outline"),n=await Jt(e);return{notes:n?[n]:[],data:`Outline: ${e}
168
+
169
+ ${qe(Qc(t))}
170
+ `}}var Zh=["✘","✔","❌","⚠","❗","\uD83D\uDEA8","Stats:","Screenshot:","screenshot:","Full-page:"],ef=["Simulate:","Render UI:","live ","skip ","cache ","gen "];function ul(e){let t=e.split(`
171
+ `).filter((o)=>o.trim()),n=t.filter((o)=>Zh.some((s)=>o.includes(s))||ef.some((s)=>o.startsWith(s)));return{text:`${n.join(`
173
172
  `)}
174
- `,kept:n.length,total:t.length}}import{join as ph}from"path";function Bc(e,t){let n=nr(),o=new Bun.CryptoHasher("sha1").update(`${t}
175
- ${e}`).digest("hex").slice(0,16);return ph(n.replace(/\.json$/,""),"last-input",`${o}.json`)}async function qc(e,t){let n=await le(Bc(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 Vc(e,t,n){await Bun.write(Bc(e,t),JSON.stringify({moduleDir:e,savedAt:new Date().toISOString(),flags:n},null,2))}function Wc(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 sf}from"path";import{join as mh}from"path";var Ht=8,Uo=5000;function hh(e){let t=e.split(/[?#]/)[0],n=t.split("/").filter(Boolean);return(n.length?n[n.length-1]:t).toLowerCase()}function Kc(e,t,n){let o=new Map;for(let r of t){o.set(r.toLowerCase(),r);let i=_t(r);if(i)o.set(i.fnName.toLowerCase(),r)}let s=[];for(let r of e){let i=o.get(hh(r));if(i&&!n.has(i.toLowerCase()))s.push({mockKey:r,spName:i})}return s}function Xn(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 B(e))}catch{return null}}function Jc(e){if(!Tt(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 hn(e,t){let n=e.paths,o=(r,i,a)=>mh(n.projectDir(r),"_testdata",i,`${a}.json`),s=async(r,i,a)=>{let c=await le(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 Jc(await hs(n.schemaFile(r,a))))i.add(c);return[...i]},getVerifiedSpNames:()=>t.getVerifiedSpNames(),async getConnectionAlias(r,i){return he(await e.load(r),i)},async resolveApiSpName(r,i,a){let c=await hs(n.schemaFile(r,i));if(!Tt(c))return null;let l=Jc(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 Zi(await s(r,i,a))},async callLiveApiSp(r,i,a,c,l="GET",u,d=Uo){try{let{config:p}=await e.resolve({project:r,db:i}),m=`${Te(p.apiDomain)}/${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(),E;try{E=JSON.parse(v)}catch{return{ok:!1,status:y.status,error:"Ph\u1EA3n h\u1ED3i kh\xF4ng ph\u1EA3i JSON"}}if(!y.ok)return{ok:!1,status:y.status,data:E,error:`HTTP ${y.status}`};return{ok:!0,status:y.status,data:E}}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 Bun.write(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:Qi(c)}}}import{promises as Xe}from"fs";import pe from"path";import{homedir as fh}from"os";import{join as gs}from"path";var fs=null;function ws(e){fs={...e,live:hn(e.store,e.session)}}function qe(){if(!fs){let e=Bun.env.FUI_MCP_WORKDIR||gs(fh(),er),t=new Cn;ws({store:new rt(new Tn(e)),session:t,api:lo,activeTarget:()=>t.getTarget()})}return fs}var fn=()=>qe().store.paths.root,gn=(e)=>gs(qe().store.paths.root,ot(e,"projectId")),ys=(e,t)=>gs(gn(e),"modules",ot(t,"moduleId")),Gc=()=>qe().activeTarget();async function zc(e){return await(await qe().api()).listFrameworkImportFiles(e)}async function Xc(e,t){let n=await qe().store.resolve({project:e,db:t.db});return{alias:n.alias,config:{...n.config,apiName:n.alias},note:n.note}}var Yc=(...e)=>qe().live.getConnectionAlias(...e),Qc=(...e)=>qe().live.resolveApiSpName(...e),Zc=(...e)=>qe().live.classifySpSafety(...e),el=(...e)=>qe().live.readTestData(...e),bs=(...e)=>qe().live.writeTestData(...e),tl=(...e)=>qe().live.generateMockForLocalSp(...e);import{promises as ut}from"fs";import{createHash as yl}from"crypto";import K from"path";import{promises as ks}from"fs";import sl from"path";var gh=86400000,nl={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 wh(e){return sl.join(fn(),"_cache","framework",`${e}.json`)}function yh(e,t){let n=nl[e]??nl.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 il(e,t){let n=wh(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<gh)return{deps:o.data,source:`cache (${o.data.length} file, t\u1EA3i l\xFAc ${o.fetchedAt})`};try{let r=await zc(e);if(r.length===0)throw Error("API tr\u1EA3 danh m\u1EE5c r\u1ED7ng");try{await ks.mkdir(sl.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\u0168 (${o.data.length} file, t\u1EA3i l\xFAc ${o.fetchedAt})`,warning:`Kh\xF4ng g\u1ECDi \u0111\u01B0\u1EE3c API danh m\u1EE5c framework ${e} (${r.message}) \u2014 d\xF9ng cache c\u0169 tr\xEAn \u0111\u0129a.`};let i=yh(e,t);return{deps:i,source:`danh s\xE1ch d\u1EF1 ph\xF2ng hardcode (${i.length} file)`,warning:`Kh\xF4ng g\u1ECDi \u0111\u01B0\u1EE3c API danh m\u1EE5c framework ${e} (${r.message}) v\xE0 ch\u01B0a c\xF3 cache \u2014 `+"d\xF9ng danh s\xE1ch hardcode trong renderAssets.ts. Danh s\xE1ch n\xE0y c\xF3 th\u1EC3 \u0111\xE3 l\u1EC7ch v\u1EDBi server."}}}function bh(e){if(e===".css")return"css";if(e===".js")return"js";return}function al(e){return[...e].sort((t,n)=>t.sort-n.sort||(t.name<n.name?-1:t.name>n.name?1:0))}function cl(e,t){let n=bh(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"}}var ol="render";async function rl(e){try{let t=await fetch(e,{headers:{Accept:"*/*"}});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 ll(e,t){let n=t.replace(/^https?:\/\//,"").replace(/\/+$/,""),o=`https://${n}/_aset/fp/auth/projectdata/${e}@${n.replace(/\//g,"")}/${ol}.js`,s=`https://${n}/_aset/fp/auth/StyleCSS/${e}/${ol}.css`,r=[],i=[],[a,c]=await Promise.all([rl(o),rl(s)]),l;if(a.ok)if(/\$projectData\s*=/.test(a.text))l=a.text,r.push(`projectdata.js \u2190 ${o} (${a.text.length} bytes)`);else i.push(`projectdata.js t\u1EA3i v\u1EC1 t\u1EEB ${o} kh\xF4ng ch\u1EE9a "$projectData =" (${a.text.length} bytes) \u2014 `+"kh\xF4ng d\xF9ng, quay v\u1EC1 d\u1EF1ng $projectData + component project t\u1EEB workspace local.");else i.push(`Kh\xF4ng t\u1EA3i \u0111\u01B0\u1EE3c ${o}: ${a.reason} \u2014 quay v\u1EC1 d\u1EF1ng t\u1EEB workspace local.`);let u;if(c.ok)if(/<html|<!DOCTYPE/i.test(c.text.slice(0,200)))i.push(`StyleCSS c\u1EA5p project t\u1EA3i v\u1EC1 t\u1EEB ${s} l\xE0 HTML ch\u1EE9 kh\xF4ng ph\u1EA3i CSS \u2014 b\u1ECF qua.`);else if(c.text.trim())u=c.text,r.push(`StyleCSS project \u2190 ${s} (${c.text.length} bytes)`);else r.push(`StyleCSS project r\u1ED7ng (${s}) \u2014 project ch\u01B0a c\xF3 component n\xE0o khai style.`);else i.push(`Kh\xF4ng t\u1EA3i \u0111\u01B0\u1EE3c ${s}: ${c.reason} \u2014 CSS component c\u1EA5p project s\u1EBD d\u1EF1ng t\u1EEB local.`);return{js:l,css:u,jsUrl:o,cssUrl:s,notes:r,problems:i}}function vs(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 kh(e){return e.replace(/[\r\n\t]+/g," ").replace(/\\/g,"\\\\").replace(/`/g,"\\`").replace(/\$\{/g,"\\${")}function vh(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=vs(t,"template")[0]??"",o=vs(t,"script")[0]??"",s=vs(t,"style").map((c)=>c.trim()).filter(Boolean).join(`
176
- `);if(!n&&!o&&t.includes("Vue.component("))return{compiled:{name:e,script:t,css:s}};let r=vh(o),i=kh(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:`;(() => {
173
+ `,kept:n.length,total:t.length}}import{join as tf}from"node:path";function dl(e,t){let n=wr(),o=Qt("sha1",`${t}
174
+ ${e}`).slice(0,16);return tf(n.replace(/\.json$/,""),"last-input",`${o}.json`)}async function pl(e,t){let n=await ue(dl(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 ml(e,t,n){await x(dl(e,t),JSON.stringify({moduleDir:e,savedAt:new Date().toISOString(),flags:n},null,2))}function hl(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 Yf}from"node:path";import{join as nf}from"node:path";var Kt=8,Yo=5000;function of(e){let t=e.split(/[?#]/)[0],n=t.split("/").filter(Boolean);return(n.length?n[n.length-1]:t).toLowerCase()}function gl(e,t,n){let o=new Map;for(let r of t){o.set(r.toLowerCase(),r);let i=Ut(r);if(i)o.set(i.fnName.toLowerCase(),r)}let s=[];for(let r of e){let i=o.get(of(r));if(i&&!n.has(i.toLowerCase()))s.push({mockKey:r,spName:i})}return s}function no(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 Es(e){try{return JSON.parse(await W(e))}catch{return null}}function fl(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 vn(e,t){let n=e.paths,o=(r,i,a)=>nf(n.projectDir(r),"_testdata",i,`${a}.json`),s=async(r,i,a)=>{let c=await ue(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 fl(await Es(n.schemaFile(r,a))))i.add(c);return[...i]},getVerifiedSpNames:()=>t.getVerifiedSpNames(),async getConnectionAlias(r,i){return fe(await e.load(r),i)},async resolveApiSpName(r,i,a){let c=await Es(n.schemaFile(r,i));if(!Ot(c))return null;let l=fl(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 ka(await s(r,i,a))},async callLiveApiSp(r,i,a,c,l="GET",u,d=Yo){try{let{config:p}=await e.resolve({project:r,db:i}),h=`${_e(p.apiDomain)}/${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(),E;try{E=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:E,error:`HTTP ${y.status}`};return{ok:!0,status:y.status,data:E}}catch(p){return{ok:!1,error:p instanceof Error?p.message:String(p)}}},async readTestData(r,i,a){return await Es(o(r,i,a))},async writeTestData(r,i,a,c,l){try{let u={source:c,savedAt:new Date().toISOString(),data:l};await x(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:ba(c)}}}import{promises as et}from"node:fs";import me from"node:path";import{homedir as rf}from"node:os";import{join as As}from"node:path";var _s=null;function Ns(e){_s={...e,live:vn(e.store,e.session)}}function Be(){if(!_s){let e=process.env.FUI_WORKDIR||process.env.FUI_MCP_WORKDIR||As(rf(),fr),t=new Dn;Ns({store:new at(new Mn(e)),session:t,api:wo,activeTarget:()=>t.getTarget()})}return _s}var Sn=()=>Be().store.paths.root,$n=(e)=>As(Be().store.paths.root,it(e,"projectId")),Ds=(e,t)=>As($n(e),"modules",it(t,"moduleId")),wl=()=>Be().activeTarget();async function yl(e){return await(await Be().api()).listFrameworkImportFiles(e)}async function bl(e,t){let n=await Be().store.resolve({project:e,db:t.db});return{alias:n.alias,config:{...n.config,apiName:n.alias},note:n.note}}var kl=(...e)=>Be().live.getConnectionAlias(...e),vl=(...e)=>Be().live.resolveApiSpName(...e),Sl=(...e)=>Be().live.classifySpSafety(...e),$l=(...e)=>Be().live.readTestData(...e),xs=(...e)=>Be().live.writeTestData(...e),jl=(...e)=>Be().live.generateMockForLocalSp(...e);import{promises as mt}from"node:fs";import{createHash as Hl}from"node:crypto";import{createRequire as hf}from"node:module";import K from"node:path";import{pathToFileURL as ff}from"node:url";import{promises as Os}from"node:fs";import Tl from"node:path";var sf=86400000,Il={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 af(e){return Tl.join(Sn(),"_cache","framework",`${e}.json`)}function cf(e,t){let n=Il[e]??Il.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 Pl(e,t){let n=af(e),o;try{o=JSON.parse(await Os.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<sf)return{deps:o.data,source:`cache (${o.data.length} file, tải lúc ${o.fetchedAt})`};try{let r=await yl(e);if(r.length===0)throw Error("API trả danh mục rỗng");try{await Os.mkdir(Tl.dirname(n),{recursive:!0}),await Os.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=cf(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 lf(e){if(e===".css")return"css";if(e===".js")return"js";return}function El(e){return[...e].sort((t,n)=>t.sort-n.sort||(t.name<n.name?-1:t.name>n.name?1:0))}function _l(e,t){let n=lf(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"}}var Cl="render";async function Rl(e){try{let t=await fetch(e,{headers:{Accept:"*/*"}});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 Al(e,t){let n=t.replace(/^https?:\/\//,"").replace(/\/+$/,""),o=`https://${n}/_aset/fp/auth/projectdata/${e}@${n.replace(/\//g,"")}/${Cl}.js`,s=`https://${n}/_aset/fp/auth/StyleCSS/${e}/${Cl}.css`,r=[],i=[],[a,c]=await Promise.all([Rl(o),Rl(s)]),l;if(a.ok)if(/\$projectData\s*=/.test(a.text))l=a.text,r.push(`projectdata.js ← ${o} (${a.text.length} bytes)`);else i.push(`projectdata.js tải về từ ${o} không chứa "$projectData =" (${a.text.length} bytes) — `+"không dùng, quay về dựng $projectData + component project từ workspace local.");else i.push(`Không tải được ${o}: ${a.reason} — quay về dựng từ workspace local.`);let u;if(c.ok)if(/<html|<!DOCTYPE/i.test(c.text.slice(0,200)))i.push(`StyleCSS cấp project tải về từ ${s} là HTML chứ không phải CSS — bỏ qua.`);else if(c.text.trim())u=c.text,r.push(`StyleCSS project ← ${s} (${c.text.length} bytes)`);else r.push(`StyleCSS project rỗng (${s}) — project chưa có component nào khai style.`);else i.push(`Không tải được ${s}: ${c.reason} — CSS component cấp project sẽ dựng từ local.`);return{js:l,css:u,jsUrl:o,cssUrl:s,notes:r,problems:i}}function Ms(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 uf(e){return e.replace(/[\r\n\t]+/g," ").replace(/\\/g,"\\\\").replace(/`/g,"\\`").replace(/\$\{/g,"\\${")}function df(e){let t=/export\s+default\s*\{([\s\S]*)\}\s*;?\s*$/.exec(e.trim());return t?t[1].trim():void 0}function Nl(e,t){let n=Ms(t,"template")[0]??"",o=Ms(t,"script")[0]??"",s=Ms(t,"style").map((c)=>c.trim()).filter(Boolean).join(`
175
+ `);if(!n&&!o&&t.includes("Vue.component("))return{compiled:{name:e,script:t,css:s}};let r=df(o),i=uf(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:`;(() => {
177
176
  const component = (() => {
178
177
  ${a}
179
178
  })() || {};
180
179
  component.template = \`${i}\`;
181
180
  Vue.component(${JSON.stringify(e)}, component);
182
- })();`,css:s},warning:`component "${e}.vue" kh\xF4ng c\xF3 khu\xF4n \`export default { \u2026 }\` \u2014 server bi\xEAn d\u1ECBch b\u1EB1ng c\xE1ch `+"n\u1ED1i th\u1EB3ng ph\u1EA7n <script> v\xE0o `{template:`\u2026`, \u2026}` n\xEAn b\u1EA3n tr\xEAn server c\xF3 th\u1EC3 l\u1ED7i c\xFA ph\xE1p. "+"renderUI \u0111\xE3 b\u1ECDc an to\xE0n \u0111\u1EC3 ch\u1EA1y ti\u1EBFp, nh\u01B0ng n\xEAn s\u1EEDa .vue v\u1EC1 \u0111\xFAng khu\xF4n."}}function wn(e){return e.replace(/\s+/g," ").trim()}function Sh(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 $h(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=Sh(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 dl(e,t,n){let o={missing:[],changed:[],changedCss:[],unverifiable:[]},s=$h(t),r=wn(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\xF4ng theo khu\xF4n ComponentBuild)`);continue}if(wn(i.template??"")!==wn(c.template)){o.changed.push(i.name);continue}if(c.body===void 0)o.unverifiable.push(`${i.name} (ch\u1EC9 \u0111\u1ED1i chi\u1EBFu \u0111\u01B0\u1EE3c template, kh\xF4ng b\xF3c \u0111\u01B0\u1EE3c th\xE2n script)`);else if(wn(i.script??"")!==wn(c.body)){o.changed.push(i.name);continue}let l=wn(i.css??"");if(l&&!r.includes(l))o.changedCss.push(i.name)}return o}function Ss(e){if(e.length===0)return"";return`${e.map((t)=>t.script).join(`;
181
+ })();`,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 jn(e){return e.replace(/\s+/g," ").trim()}function pf(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 mf(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=pf(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 Dl(e,t,n){let o={missing:[],changed:[],changedCss:[],unverifiable:[]},s=mf(t),r=jn(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(jn(i.template??"")!==jn(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(jn(i.script??"")!==jn(c.body)){o.changed.push(i.name);continue}let l=jn(i.css??"");if(l&&!r.includes(l))o.changedCss.push(i.name)}return o}function Fs(e){if(e.length===0)return"";return`${e.map((t)=>t.script).join(`;
183
182
  `)};
184
- `}function $s(e){let t=e.map((n)=>n.css).filter((n)=>n.trim());if(t.length===0)return"";return`${t.join(`
183
+ `}function Ls(e){let t=e.map((n)=>n.css).filter((n)=>n.trim());if(t.length===0)return"";return`${t.join(`
185
184
  `)}
186
- `}var Bo="https://fui.vn",jh=45000,Ih=700,js=4000,pl=1200000,Ch=new Set(["script","stylesheet","image","font","media","manifest","texttrack"]),Rh=new Set([".js",".css",".map",".woff",".woff2",".ttf",".otf",".eot",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp"]),ml={project:90000,module:95000},Ph={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"}},Th=3;function bl(e,t){return K.join(e,`last-vitals-${t}.json`)}async function Eh(e,t){try{let n=await ut.readFile(bl(e,t),"utf8"),o=JSON.parse(n);return typeof o?.jsErrors==="number"?o:null}catch{return null}}function kl(e,t){let n=[],o=(s,r,i)=>{if(r!==i)n.push(`${s} ${r}\u2192${i}${i<r?" \u2193":" \u2191"}`)};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\xF4i tr\u01B0\u1EDDng thi\u1EBFu",e.envMissing,t.envMissing),o("text nghi v\u1EA5n",e.suspectTexts,t.suspectTexts),o("\u1EA3nh v\u1EE1",e.brokenImages,t.brokenImages),o("canvas ch\u1EBFt",e.deadCanvas,t.deadCanvas),o("text d\xEDnh/ch\u1ED3ng l\u1EA5n",e.textCollisions,t.textCollisions),o("assert h\u1ECFng",e.assertFail,t.assertFail),e.appEmpty!==t.appEmpty)n.push(t.appEmpty?"app render R\u1ED6NG (tr\u01B0\u1EDBc c\xF3 n\u1ED9i dung) \u2191":"app \u0111\xE3 c\xF3 n\u1ED9i dung \u2193");if(e.overflowX!==t.overflowX)n.push(t.overflowX?"tr\xE0n ngang (tr\u01B0\u1EDBc kh\xF4ng) \u2191":"h\u1EBFt tr\xE0n ngang \u2193");return n}function Ah(e,t){let n=kl(e,t),o=e.ts.replace("T"," ").slice(0,19);if(n.length>0)return`\u0394 so v\u1EDBi l\u01B0\u1EE3t tr\u01B0\u1EDBc (${o}): ${n.join(" \xB7 ")}`;return Rs(t)?`\u0394 so v\u1EDBi l\u01B0\u1EE3t tr\u01B0\u1EDBc (${o}): KH\xD4NG ch\u1EC9 s\u1ED1 n\xE0o \u0111\u1ED5i \u2014 thay \u0111\u1ED5i v\u1EEBa r\u1ED3i kh\xF4ng ch\u1EA1m t\u1EDBi l\u1ED7i \u0111ang c\xF3. \u0110\u1ED5i h\u01B0\u1EDBng ch\u1EA9n \u0111o\xE1n, \u0111\u1EEBng render l\u1EA1i c\xF9ng m\u1ED9t c\xE1ch.`:`\u0394 so v\u1EDBi l\u01B0\u1EE3t tr\u01B0\u1EDBc (${o}): kh\xF4ng \u0111\u1ED5i \u2014 c\u1EA3 hai l\u01B0\u1EE3t \u0111\u1EC1u s\u1EA1ch.`}function hl(e,t){let n=new Set;for(let o of e){let s=String(o).split(`
187
- `)[0].trim();if(s)n.add(s.length>200?s.slice(0,200)+"\u2026":s);if(n.size>=t)break}return[...n]}function Nh(e,t){return["",`\uD83D\uDED1 \u0110ANG GI\u1EACM CH\xC2N \u2014 ${e} l\u01B0\u1EE3t render h\u1ECFng li\xEAn ti\u1EBFp m\xE0 KH\xD4NG ch\u1EC9 s\u1ED1 n\xE0o \u0111\u1ED5i.`," Render l\u1EA1i l\u1EA7n n\u1EEFa theo c\xF9ng h\u01B0\u1EDBng s\u1EBD cho \u0111\xFAng k\u1EBFt qu\u1EA3 n\xE0y. Ch\u1ECDn M\u1ED8T trong ba:",t.length>0?" 1. Ch\u1EA1y t\u1EA7ng r\u1EBB h\u01A1n \u0111\u01B0\u1EE3c n\xEAu \u1EDF d\xF2ng \u21AA tr\xEAn \u2014 n\xF3 ch\u1EC9 th\u1EB3ng v\xE0o ch\u1ED7 h\u1ECFng.":" 1. L\xF9i v\u1EC1 t\u1EA7ng t\u0129nh: module_validate (l\u1ED7i quy t\u1EAFc) \u2192 module_outline (c\u1EA5u tr\xFAc) \u2192 module_simulate kh\xF4ng renderUI (action/watch)."," 2. \u0110\u1ECDc l\u1EA1i screenshot + kh\u1ED1i l\u1ED7i \u1EDF cu\u1ED1i report b\u1EB1ng m\u1EAFt, \u0111\u1EEBng ch\u1EC9 \u0111\u1ECDc con s\u1ED1."," 3. H\u1ECEI NG\u01AF\u1EDCI D\xD9NG. Ba l\u01B0\u1EE3t kh\xF4ng nh\xFAc nh\xEDch th\u01B0\u1EDDng ngh\u0129a l\xE0 gi\u1EA3 \u0111\u1ECBnh ban \u0111\u1EA7u sai, kh\xF4ng ph\u1EA3i s\u1EEDa ch\u01B0a \u0111\u1EE7."]}function Rs(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 _h(e,t,n,o){let s=["\u2550\u2550\u2550 CH\u1EA8N \u0110O\xC1N \u2550\u2550\u2550"];s.push(e.envMissing>0?`[M\xD4I TR\u01AF\u1EDCNG] ${e.envMissing} file import/asset KH\xD4NG n\u1EA1p \u0111\u01B0\u1EE3c \u2192 s\u1EEDa ph\u1EA7n n\xE0y TR\u01AF\u1EDAC (project_sync / module_get / file_import_*). L\u1ED7i \u1EDF hai nh\xF3m d\u01B0\u1EDBi c\xF3 th\u1EC3 ch\u1EC9 l\xE0 h\u1EC7 qu\u1EA3.`:"[M\xD4I TR\u01AF\u1EDCNG] \u2713 m\u1ECDi import/asset n\u1EA1p \u0111\u1EE7.");let r=[];if(e.jsErrors>0)r.push(`${e.jsErrors} JS error${e.initErrors?` (${e.initErrors} l\xFAc init)`:""}`);if(e.vueWarns>0)r.push(`${e.vueWarns} [Vue warn]`);if(e.assertFail>0)r.push(`${e.assertFail} assert h\u1ECFng`);s.push(r.length>0?`[MODULE] ${r.join(" \xB7 ")} \u2192 l\u1ED7i n\u1EB1m trong module.json / script.js / component.`:"[MODULE] \u2713 kh\xF4ng JS error, kh\xF4ng Vue warn.");for(let a of hl(n,3))s.push(` \u2718 ${a}`);for(let a of hl(o,2))s.push(` \u26A0 ${a}`);let i=[];if(e.appEmpty)i.push("app render R\u1ED6NG");if(e.overflowX)i.push("tr\xE0n ngang viewport");if(e.suspectTexts>0)i.push(`${e.suspectTexts} text nghi v\u1EA5n (undefined/NaN)`);if(e.brokenImages>0)i.push(`${e.brokenImages} \u1EA3nh v\u1EE1`);if(e.deadCanvas>0)i.push(`${e.deadCanvas} canvas chart 0-size`);if(e.textCollisions>0)i.push(`${e.textCollisions} ch\u1ED7 text d\xEDnh/ch\u1ED3ng l\u1EA5n`);s.push(i.length>0?`[HI\u1EC2N TH\u1ECA] ${i.join(" \xB7 ")} \u2192 xem screenshot; \u0111\xE2y l\xE0 nh\xF3m CH\u1EC8 renderUI th\u1EA5y \u0111\u01B0\u1EE3c.`:"[HI\u1EC2N TH\u1ECA] \u2713 DOM audit s\u1EA1ch.");for(let a of t)s.push(` \u21AA ${a}`);return s}function Dh(e,t,n){let o=[];if(n.some((s)=>/\bis not defined\b/.test(s)))o.push("`\u2026 is not defined` = m\u1ED9t \u0111\u1ECBnh danh tr\u1EA7n kh\xF4ng c\xF3 tr\xEAn window. Ca th\u01B0\u1EDDng g\u1EB7p nh\u1EA5t: `EXE`/`FUN` g\u1ECDi h\xE0m ch\u01B0a vi\u1EBFt trong script.js \u2014 ho\u1EB7c vi\u1EBFt r\u1ED3i m\xE0 qu\xEAn `module_publish_script` (module_publish_json KH\xD4NG \u0111\u1EA9y script.js). So\xE1t t\xEAn h\xE0m trong script.js tr\u01B0\u1EDBc khi \u0111i t\xECm ch\u1ED7 kh\xE1c.");if(e.appEmpty&&e.jsErrors===0&&e.vueWarns===0&&e.envMissing===0)o.push("Trang tr\u1EAFng m\xE0 KH\xD4NG c\xF3 JS error/Vue warn l\xE0 d\u1EA5u hi\u1EC7u kinh \u0111i\u1EC3n c\u1EE7a statement trong m\u1ED9t `:attr` (Vue nu\u1ED1t l\u1ED7i compile template) \u2014 ch\u1EA1y module_validate, n\xF3 b\u1EAFt t\u0129nh \u0111\u01B0\u1EE3c ca n\xE0y.");if(t.some((s)=>/is not defined|Property or method/i.test(s)))o.push('[Vue warn] ki\u1EC3u "property is not defined" = bi\u1EBFn d\xF9ng trong template ch\u01B0a khai trong data[] \u2014 module_validate b\u1EAFt \u0111\u01B0\u1EE3c (v-model/watch/state), kh\xF4ng c\u1EA7n browser.');if(e.suspectTexts>0&&e.jsErrors===0)o.push('Text "undefined"/"NaN" th\u01B0\u1EDDng l\xE0 OUT/mapData tr\u1ECF sai kho\xE1 ch\u1EE9 kh\xF4ng ph\u1EA3i l\u1ED7i hi\u1EC3n th\u1ECB \u2014 module_simulate (engine vm) ch\u1EC9 ra ch\u1ED7 g\xE1n, ch\u1EA1y nhanh h\u01A1n nhi\u1EC1u.');return o}async function xh(){for(let e of["playwright","playwright-core"])for(let t of[void 0,process.cwd()])try{return await(t?import(Bun.resolveSync(e,t)):import(e))}catch{}throw Error(`rendering needs Playwright (optional, not installed). Install once:
185
+ `}var Zo="https://fui.vn",gf=45000,wf=700,Us=4000,xl=1200000,yf=new Set(["script","stylesheet","image","font","media","manifest","texttrack"]),bf=new Set([".js",".css",".map",".woff",".woff2",".ttf",".otf",".eot",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp"]),Ol={project:90000,module:95000},kf={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"}},vf=3;function Vl(e,t){return K.join(e,`last-vitals-${t}.json`)}async function Sf(e,t){try{let n=await mt.readFile(Vl(e,t),"utf8"),o=JSON.parse(n);return typeof o?.jsErrors==="number"?o:null}catch{return null}}function Wl(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 $f(e,t){let n=Wl(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 Ws(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 Ml(e,t){let n=new Set;for(let o of e){let s=String(o).split(`
186
+ `)[0].trim();if(s)n.add(s.length>200?s.slice(0,200)+"…":s);if(n.size>=t)break}return[...n]}function jf(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 Ws(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 If(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 Ml(n,3))s.push(` ✘ ${a}`);for(let a of Ml(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 Cf(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 Rf(){for(let e of["playwright","playwright-core"])for(let t of[void 0,process.cwd()])try{return await(t?import(ff(hf(K.join(t,"noop.js")).resolve(e)).href):import(e))}catch{}throw Error(`rendering needs Playwright (optional, not installed). Install once:
188
187
  bun add -g playwright
189
188
  bunx playwright install chromium
190
- (module simulate without --render works without Playwright.)`)}var Oh=60000,Is=new Map;function Ps(){return K.join(fn(),"_cache","cdn")}function Ts(e){return yl("sha1").update(e).digest("hex")}function vl(e){return yl("sha256").update(e).digest("hex")}async function Mh(e){let t=Ts(e),n=Ps();try{let o=JSON.parse(await ut.readFile(K.join(n,`${t}.meta.json`),"utf-8"));return{body:await ut.readFile(K.join(n,`${t}.bin`)),contentType:o.contentType??Sl(e),meta:o}}catch{return}}async function Fh(e,t,n,o){try{let s=Ts(e),r=Ps();await ut.mkdir(r,{recursive:!0}),await ut.writeFile(K.join(r,`${s}.bin`),t);let i={url:e,contentType:n,cachedAt:new Date().toISOString(),etag:o.etag,lastModified:o.lastModified,sha256:vl(t)};await ut.writeFile(K.join(r,`${s}.meta.json`),JSON.stringify(i),"utf-8")}catch{}}async function Lh(e,t){try{let n=Ts(e);await ut.writeFile(K.join(Ps(),`${n}.meta.json`),JSON.stringify({...t,cachedAt:new Date().toISOString()}),"utf-8")}catch{}}function Cs(e){if(!e)return"kh\xF4ng r\xF5 tu\u1ED5i";let t=Date.now()-Date.parse(e);if(!Number.isFinite(t))return"kh\xF4ng r\xF5 tu\u1ED5i";let n=Math.floor(t/86400000);if(n>=1)return`c\u0169 ${n} ng\xE0y`;let o=Math.floor(t/3600000);return o>=1?`c\u0169 ${o} gi\u1EDD`:"c\u0169 d\u01B0\u1EDBi 1 gi\u1EDD"}async function Uh(e,t){let n=t.refresh?void 0:await Mh(e);if(t.offline){if(!n)throw Error("offline: ch\u01B0a c\xF3 b\u1EA3n cache n\xE0o cho URL n\xE0y");return{body:n.body,contentType:n.contentType,origin:"stale",note:`offline \u2014 d\xF9ng cache ${Cs(n.meta.cachedAt)}`}}if(n&&!t.refresh){let r=Is.get(e);if(r!==void 0&&Date.now()-r<Oh)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 Is.set(e,Date.now()),await Lh(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")??Sl(e),c=r.headers.get("etag")??void 0,l=r.headers.get("last-modified")??void 0;await Fh(e,i,a,{etag:c,lastModified:l}),Is.set(e,Date.now());let u=Boolean(n?.meta.sha256&&n.meta.sha256!==vl(i));return{body:i,contentType:a,origin:n?"updated":"fetched",changed:u,note:u?`n\u1ED9i dung \u0110\u1ED4I so v\u1EDBi b\u1EA3n cache ${Cs(n?.meta.cachedAt)}`:void 0}}catch(r){if(n)return{body:n.body,contentType:n.contentType,origin:"stale",note:`kh\xF4ng revalidate \u0111\u01B0\u1EE3c (${r.message}) \u2014 d\xF9ng cache ${Cs(n.meta.cachedAt)}`};throw r}}function Sl(e){switch(K.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 yn(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function St(e,t){return e.replace(new RegExp(`</(?=${t})`,"gi"),"<\\/")}function qo(e,t){return JSON.stringify(e,null,t).replace(/</g,"\\u003c")}function Hh(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 fl(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 Bh(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 qh(e,t,n){let o=[],s=Bh(e.url),r=fl(e.mDomain),i=fl(t?.pDomain),a=s.host;if(!a)a=r[0]??i[0]??n,o.push("_moduleInfo.json kh\xF4ng c\xF3 url k\xE8m host \u2014 origin l\u1EA5y t\u1EEB "+`${r.length?"mDomain":i.length?"pDomain":"domain asset m\u1EB7c \u0111\u1ECBnh"}: ${a}. `+"Ch\u1EA1y `module_get` \u0111\u1EC3 c\xF3 url th\u1EADt n\u1EBFu render sai domain.");let c=[...new Set([...s.host?[s.host]:[],...r,...i])];if(c.length>1)o.push(`Project/module ph\u1EE5c v\u1EE5 tr\xEAn nhi\u1EC1u domain (${c.join(", ")}) \u2014 l\u1EA7n render n\xE0y ch\u1EA1y \u1EDF "${a}". `+"v_Set.domainSetting c\u1EE7a domain kh\xE1c KH\xD4NG \u0111\u01B0\u1EE3c \xE1p d\u1EE5ng; mu\u1ED1n th\u1EED domain kh\xE1c th\xEC render module c\xF3 url thu\u1ED9c domain \u0111\xF3.");let l=s.path&&s.path!=="/"?s.path:`/${e.ModuleID}`;return{host:a,basePath:l,notes:o}}function gl(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 Vh(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 Me(e),n=[],o=[];for(let s of t){let r=await F(K.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 Wh(e){let t=await Me(e),n=[];for(let o of t){let s=o.replace(/\.vue$/i,""),r=await F(K.join(e,o));if(!r.trim())continue;try{let{template:i,script:a,style:c}=kt(r);n.push({name:s,template:i,script:a,css:c})}catch(i){n.push({name:s,parseError:i.message})}}return n}async function Jh(e,t,n){let o=[],s=[],r=[],i={project:[],module:[]},a=[{scope:"project",items:await U(K.join(t,"imports","_imports.json"),[]),baseDir:t,fixHint:"project_sync"},{scope:"module",items:await U(K.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\u1ED1ng)"}" kh\xF4ng ph\u1EA3i .js/.css \u2014 kh\xF4ng n\u1EA1p v\xE0o 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:ml[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\u1EA5p ${c} kh\xF4ng c\xF3 FileSort trong _imports.json (${l.join(", ")}) \u2192 t\u1EA1m x\u1EBFp cu\u1ED1i `+`(${ml[c]}). Th\u1EE9 t\u1EF1 n\u1EA1p c\xF3 th\u1EC3 kh\xE1c production; ch\u1EA1y \`${c==="project"?"project_sync":"module_get"}\` \u0111\u1EC3 l\u1EA5y FileSort th\u1EADt.`)}return{entries:o,problems:s,notes:r}}async function Kh(e){let t=[],n=[],o=[],s=[],r=[],i=new Set;for(let a of al(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(K.join(a.baseDir,a.filePath));if(p.trim()){i.add(c);let m=yn(a.label);if(a.kind==="css")t.push(`<style data-fui-import="${m}">
191
- ${St(p,"style")}
192
- </style>`);else n.push(`<script data-fui-import="${m}">
193
- ${St(p,"script")}
194
- </script>`);s.push(`${a.sort}. ${a.label} \u2014 inline ${p.length} bytes`);continue}if(!a.fileID){o.push(`import ${a.label}: file n\u1ED9i b\u1ED9 "${a.filePath}" thi\u1EBFu/r\u1ED7ng v\xE0 kh\xF4ng c\xF3 fileID \u0111\u1EC3 l\u1EA5y b\u1EA3n server \u2192 KH\xD4NG n\u1EA1p \u0111\u01B0\u1EE3c. `+`Ch\u1EA1y \`${a.fixHint}\`. L\u1ED7i JS/hi\u1EC3n th\u1ECB b\xEAn d\u01B0\u1EDBi c\xF3 th\u1EC3 ch\u1EC9 l\xE0 h\u1EC7 qu\u1EA3 c\u1EE7a vi\u1EC7c n\xE0y.`);continue}r.push(`${a.label}: ch\u01B0a c\xF3 b\u1EA3n local \u2192 d\xF9ng b\u1EA3n tr\xEAn server. Ch\u1EA1y \`${a.fixHint}\` n\u1EBFu c\u1EA7n s\u1EEDa file n\xE0y.`)}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=Hh(u,a.domain);if(!d){o.push(`import ${a.label}: kh\xF4ng c\xF3 file n\u1ED9i b\u1ED9 v\xE0 t\xEAn kh\xF4ng ph\u1EA3i URL/\u0111\u01B0\u1EDDng d\u1EABn tuy\u1EC7t \u0111\u1ED1i \u2192 kh\xF4ng x\xE1c \u0111\u1ECBnh \u0111\u01B0\u1EE3c ngu\u1ED3n \u0111\u1EC3 n\u1EA1p. `+`N\u1EBFu \u0111\xE2y l\xE0 file t\u1EF1 t\u1EA1o, ch\u1EA1y \`${a.fixHint}\`.`);continue}if(i.has(`url:${d}`))continue;if(i.add(c),i.add(`url:${d}`),a.kind==="css")t.push(`<link href="${yn(d)}" rel="stylesheet" type="text/css" />`);else n.push(`<script src="${yn(d)}" type="text/javascript"></script>`);s.push(`${a.sort}. ${a.label} \u2192 ${d}`)}return{styles:t,scripts:n,problems:o,loaded:s,serverFallbackNotes:r}}function Gh(e,t,n){return`(() => {
195
- const previewVueData = ${qo(n,2)};
189
+ (module simulate without --render works without Playwright.)`)}var Tf=60000,Hs=new Map;function qs(){return K.join(Sn(),"_cache","cdn")}function Bs(e){return Hl("sha1").update(e).digest("hex")}function ql(e){return Hl("sha256").update(e).digest("hex")}async function Pf(e){let t=Bs(e),n=qs();try{let o=JSON.parse(await mt.readFile(K.join(n,`${t}.meta.json`),"utf-8"));return{body:await mt.readFile(K.join(n,`${t}.bin`)),contentType:o.contentType??Bl(e),meta:o}}catch{return}}async function Ef(e,t,n,o){try{let s=Bs(e),r=qs();await mt.mkdir(r,{recursive:!0}),await mt.writeFile(K.join(r,`${s}.bin`),t);let i={url:e,contentType:n,cachedAt:new Date().toISOString(),etag:o.etag,lastModified:o.lastModified,sha256:ql(t)};await mt.writeFile(K.join(r,`${s}.meta.json`),JSON.stringify(i),"utf-8")}catch{}}async function _f(e,t){try{let n=Bs(e);await mt.writeFile(K.join(qs(),`${n}.meta.json`),JSON.stringify({...t,cachedAt:new Date().toISOString()}),"utf-8")}catch{}}function Vs(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 Af(e,t){let n=t.refresh?void 0:await Pf(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 ${Vs(n.meta.cachedAt)}`}}if(n&&!t.refresh){let r=Hs.get(e);if(r!==void 0&&Date.now()-r<Tf)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 Hs.set(e,Date.now()),await _f(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")??Bl(e),c=r.headers.get("etag")??void 0,l=r.headers.get("last-modified")??void 0;await Ef(e,i,a,{etag:c,lastModified:l}),Hs.set(e,Date.now());let u=Boolean(n?.meta.sha256&&n.meta.sha256!==ql(i));return{body:i,contentType:a,origin:n?"updated":"fetched",changed:u,note:u?`nội dung ĐỔI so với bản cache ${Vs(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 ${Vs(n.meta.cachedAt)}`};throw r}}function Bl(e){switch(K.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 In(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function Tt(e,t){return e.replace(new RegExp(`</(?=${t})`,"gi"),"<\\/")}function er(e,t){return JSON.stringify(e,null,t).replace(/</g,"\\u003c")}function Nf(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 Fl(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 Df(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 xf(e,t,n){let o=[],s=Df(e.url),r=Fl(e.mDomain),i=Fl(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 Ll(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 Of(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 Ul(e){let t=await Fe(e),n=[],o=[];for(let s of t){let r=await L(K.join(e,s));if(!r.trim())continue;let{compiled:i,warning:a}=Nl(s.replace(/\.vue$/,""),r);if(n.push(i),a)o.push(a)}return{components:n,warnings:o}}async function Mf(e){let t=await Fe(e),n=[];for(let o of t){let s=o.replace(/\.vue$/i,""),r=await L(K.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 Ff(e,t,n){let o=[],s=[],r=[],i={project:[],module:[]},a=[{scope:"project",items:await H(K.join(t,"imports","_imports.json"),[]),baseDir:t,fixHint:"project_sync"},{scope:"module",items:await H(K.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:Ol[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 `+`(${Ol[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 Lf(e){let t=[],n=[],o=[],s=[],r=[],i=new Set;for(let a of El(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(K.join(a.baseDir,a.filePath));if(p.trim()){i.add(c);let h=In(a.label);if(a.kind==="css")t.push(`<style data-fui-import="${h}">
190
+ ${Tt(p,"style")}
191
+ </style>`);else n.push(`<script data-fui-import="${h}">
192
+ ${Tt(p,"script")}
193
+ </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=Nf(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="${In(d)}" rel="stylesheet" type="text/css" />`);else n.push(`<script src="${In(d)}" type="text/javascript"></script>`);s.push(`${a.sort}. ${a.label} → ${d}`)}return{styles:t,scripts:n,problems:o,loaded:s,serverFallbackNotes:r}}function Uf(e,t,n){return`(() => {
194
+ const previewVueData = ${er(n,2)};
196
195
  const basePath = ${JSON.stringify(t)};
197
196
  const moduleId = ${JSON.stringify(e.ModuleID)};
198
197
 
@@ -231,34 +230,34 @@ ${St(p,"script")}
231
230
  if (window.vueData && Object.keys(window.vueData).length) {
232
231
  syncRouteState(window.vueData);
233
232
  }
234
- })();`}async function zh(e,t){let n=[],o=await U(K.join(e,"_moduleInfo.json"),{ProjectID:"render-project",ModuleID:K.basename(e)}),s=gn(o.ProjectID),r=await U(K.join(s,"_projectInfo.json"),null),i=await U(K.join(s,"project.json"),null),a=(o.Framework??"V3").toUpperCase()==="V2"?"V2":"V3",c=Bo.replace(/^https:\/\//,""),l=qh(o,r,c),u=l.host,d=`https://${u}`;n.push(...l.notes);let p=await F(K.join(e,"module.json")),m={ModuleName:o.ModuleName??o.ModuleID,data:[],watch:{},controls:[],set:{}};if(p.trim())try{m=JSON.parse(p)}catch(ne){throw Error(`module.json c\xF3 l\u1ED7i JSON syntax: ${ne.message} \u2014 s\u1EEDa tr\u01B0\u1EDBc khi renderUI.`)}else n.push("module.json r\u1ED7ng \u2014 render shell v\u1EDBi module tr\u1ED1ng");let w=await F(K.join(e,"header.html")),y=await F(K.join(e,"body.html")),v=await F(K.join(e,"script.js")),E=await F(K.join(e,"style.css")),I=o.HTMLOnly===!0,g=I?{deps:[],source:"(b\u1ECF qua \u2014 HTMLOnly)",warning:""}:await il(a,Bo);if(g.warning)n.push(g.warning);let b=g.deps.map((ne)=>cl(ne,Bo)).filter((ne)=>Boolean(ne)),R=await Jh(e,s,u),C=await Kh([...b,...R.entries]);C.problems.unshift(...R.problems),n.push(...R.notes);let S=I?`Framework ${a}: KH\xD4NG nh\xFAng (HTMLOnly=true \u2014 server c\u0169ng kh\xF4ng nh\xFAng)`:`Framework ${a}: danh m\u1EE5c t\u1EEB ${g.source}`;n.push(...C.serverFallbackNotes);let N=await wl(K.join(s,"components"));n.push(...N.warnings);let D=I?{js:"",css:"",jsUrl:"",notes:[],problems:[]}:await ll(o.ProjectID,u),A=Boolean(D.js)&&!t.forceLocalProjectBundle;if(I)n.push("HTMLOnly=true \u2192 render KH\xD4NG nh\xFAng framework "+a+", KH\xD4NG nh\xFAng projectdata/StyleCSS c\u1EA5p project "+"v\xE0 KH\xD4NG d\u1EF1ng #fastProjectAPP \u2014 gi\u1ED1ng h\u1EC7t trang production. Th\u01B0 vi\u1EC7n n\xE0o module c\u1EA7n (Vue, jQuery, Vuetify, "+"lodash\u2026) ph\u1EA3i do CH\xCDNH module khai l\xE0m import; thi\u1EBFu l\xE0 tr\u1EAFng trang th\u1EADt, kh\xF4ng ph\u1EA3i l\u1ED7i c\u1EE7a render.");else if(t.forceLocalProjectBundle)n.push("T\u1EA7ng project d\u1EF1ng t\u1EEB workspace LOCAL (ch\u1EBF \u0111\u1ED9 preview c\u1EA5p component) \u2014 kh\xF4ng d\xF9ng bundle server. "+"Ch\u1EA1y `project_sync` n\u1EBFu nghi file project trong workspace \u0111\xE3 c\u0169.");else if(!A)n.push(...D.problems);else{let ne=await Wh(K.join(s,"components")),De=dl(ne,D.js,D.css??""),Vt=[...De.changed.map((me)=>`${me} (\u0111\xE3 s\u1EEDa local, ch\u01B0a push)`),...De.missing.map((me)=>`${me} (ch\u01B0a c\xF3 tr\xEAn server)`),...De.changedCss.filter((me)=>!De.changed.includes(me)).map((me)=>`${me} (<style> \u0111\xE3 s\u1EEDa local, ch\u01B0a push)`)];if(Vt.length>0)throw Error(`\u26D4 Component c\u1EA5p project l\u1EC7ch v\u1EDBi b\u1EA3n tr\xEAn server: ${Vt.join(", ")}.
233
+ })();`}async function Hf(e,t){let n=[],o=await H(K.join(e,"_moduleInfo.json"),{ProjectID:"render-project",ModuleID:K.basename(e)}),s=$n(o.ProjectID),r=await H(K.join(s,"_projectInfo.json"),null),i=await H(K.join(s,"project.json"),null),a=(o.Framework??"V3").toUpperCase()==="V2"?"V2":"V3",c=Zo.replace(/^https:\/\//,""),l=xf(o,r,c),u=l.host,d=`https://${u}`;n.push(...l.notes);let p=await L(K.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(K.join(e,"header.html")),y=await L(K.join(e,"body.html")),v=await L(K.join(e,"script.js")),E=await L(K.join(e,"style.css")),I=o.HTMLOnly===!0,g=I?{deps:[],source:"(bỏ qua — HTMLOnly)",warning:""}:await Pl(a,Zo);if(g.warning)n.push(g.warning);let b=g.deps.map((oe)=>_l(oe,Zo)).filter((oe)=>Boolean(oe)),R=await Ff(e,s,u),C=await Lf([...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 A=await Ul(K.join(s,"components"));n.push(...A.warnings);let D=I?{js:"",css:"",jsUrl:"",notes:[],problems:[]}:await Al(o.ProjectID,u),_=Boolean(D.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.push(...D.problems);else{let oe=await Mf(K.join(s,"components")),xe=Dl(oe,D.js,D.css??""),Xt=[...xe.changed.map((he)=>`${he} (đã sửa local, chưa push)`),...xe.missing.map((he)=>`${he} (chưa có trên server)`),...xe.changedCss.filter((he)=>!xe.changed.includes(he)).map((he)=>`${he} (<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(", ")}.
235
234
 
236
- `+"renderUI d\u1EF1ng trang b\u1EB1ng bundle project T\u1EA2I T\u1EEA SERVER (_aset/.../projectdata) n\xEAn b\u1EA3n local ch\u01B0a push "+`kh\xF4ng xu\u1EA5t hi\u1EC7n \u2014 n\u1EBFu v\u1EABn render, t\u1EA5m \u1EA3nh b\u1EA1n nh\u1EADn \u0111\u01B0\u1EE3c l\xE0 B\u1EA2N C\u0168, kh\xF4ng ph\u1EA3i b\u1EA3n v\u1EEBa s\u1EEDa.
235
+ `+"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.
237
236
 
238
- `+"\u2022 N\u1EBFu b\u1EA3n local l\xE0 b\u1EA3n b\u1EA1n v\u1EEBa s\u1EEDa \u2192 `component_preview` \u0111\u1EC3 ki\u1EC3m ch\u1EE9ng ri\xEAng component (ch\u1EA1y \u0111\u01B0\u1EE3c b\u1EB1ng "+"file local, kh\xF4ng c\u1EA7n push), r\u1ED3i `component_update` t\u1EEBng component, r\u1ED3i render l\u1EA1i.\n"+"\u2022 N\u1EBFu b\u1EA3n tr\xEAn server m\u1EDBi h\u01A1n (ai \u0111\xF3 s\u1EEDa tr\xEAn web IDE) \u2192 `project_sync` \u0111\u1EC3 l\u1EA5y v\u1EC1, \u0110\u1EEANG push \u0111\xE8.");if(De.unverifiable.length>0)n.push(`Kh\xF4ng \u0111\u1ED1i chi\u1EBFu \u0111\u01B0\u1EE3c component c\u1EA5p project: ${De.unverifiable.join(", ")}. `+"Kh\xF4ng ch\u1EB7n render, nh\u01B0ng n\u1EBFu giao di\u1EC7n kh\xE1c mong \u0111\u1EE3i th\xEC \u0111\xE2y l\xE0 ch\u1ED7 \u0111\xE1ng nghi \u0111\u1EA7u ti\xEAn.");let Re=N.components.filter((me)=>me.css.trim()).map((me)=>me.name);if(Re.length>0&&!(D.css??"").trim())n.push(`StyleCSS c\u1EA5p project t\u1EA3i v\u1EC1 R\u1ED6NG nh\u01B0ng component local c\xF3 khai <style>: ${Re.join(", ")}. `+"Ho\u1EB7c c\xE1c component \u0111\xF3 ch\u01B0a push (`component_update`), ho\u1EB7c route StyleCSS \u0111ang h\u1ECFng \u2014 "+"d\xF9 nguy\xEAn nh\xE2n n\xE0o th\xEC style c\u1EA5p project trong l\u1EA7n render n\xE0y c\u0169ng \u0111ang thi\u1EBFu.")}let G=await wl(K.join(e,"components"));n.push(...G.warnings);let _=j(i?.data)?i.data:i??{},de={..._,ProjectName:_.ProjectName??r?.ProjectName??o.ProjectID,title:_.title??o.mTitle??o.ModuleName??o.ModuleID},Ie=gl(D.js,"projectGroupSetting"),H=gl(D.js,"projectData");if(A&&!H)n.push("Kh\xF4ng b\xF3c \u0111\u01B0\u1EE3c `var $projectData` kh\u1ECFi bundle projectdata t\u1EA3i t\u1EEB server \u2014 apiDomain/userInfo "+"trong l\u1EA7n render n\xE0y suy ra t\u1EEB project.json local, c\xF3 th\u1EC3 l\u1EC7ch v\u1EDBi b\u1EA3n trang th\u1EADt \u0111ang ch\u1EA1y.");let ae=String(_.apiDomain??""),we=typeof H?.apiDomain==="string"?H.apiDomain:"";if(H&&ae&&we&&ae!==we)n.push(`project.json local c\xF3 apiDomain "${ae}" nh\u01B0ng b\u1EA3n tr\xEAn server l\xE0 "${we}" \u2014 `+"render d\xF9ng b\u1EA3n server (gi\u1ED1ng trang th\u1EADt). Ch\u1EA1y `project_sync` \u0111\u1EC3 \u0111\u1ED3ng b\u1ED9 file local.");let Ve=j(m.set)?m.set:null,Ze=Vh({groupSetting:Ie,projectData:H??_,moduleSet:Ve},u),Ce=String(Ze.settings.apiDomain??""),jt=Ze.origins.apiDomain??"(kh\xF4ng t\u1EA7ng n\xE0o khai)",We=(()=>{let ne=typeof Ze.settings.userInfo==="string"?Ze.settings.userInfo.trim():"";if(!ne)return"";return/^https?:\/\//i.test(ne)?ne:`${Ce}${ne}`})();if(!Ce)n.push(`Kh\xF4ng t\u1EA7ng n\xE0o ($projectGroupSetting \u2192 $projectData \u2192 module.json set \u2192 domainSetting["${u}"]) khai apiDomain cho "${o.ProjectID}" \u2014 API s\u1EBD g\u1ECDi b\u1EB1ng \u0111\u01B0\u1EDDng d\u1EABn t\u01B0\u01A1ng \u0111\u1ED1i v\xE0 apiMocks kh\u1EDBp theo URL `+"g\u1EA7n nh\u01B0 ch\u1EAFc ch\u1EAFn tr\u01B0\u1EE3t. Ch\u1EA1y `project_sync` n\u1EBFu d\u1EEF li\u1EC7u project \u0111\xE3 c\u0169.");let et={},It=l.basePath,Y=t.user??{},to=!("SystemRight"in Y),kn=!("FunctionRight"in Y),qt=Lt([p,JSON.stringify(_),w,y,v]),no={user:{UserID:"render-preview",FirstName:"Render",LastName:"Preview",Email:"render@preview.local",SystemRight:9,FunctionRight:qt,...Y},...t.vueData??{}},oo=kn?Kn(qt,(await Ft(s)).functionRights):null,ro=(()=>{let ne=[];if(to)ne.push("SystemRight=9 (m\u1EB7c \u0111\u1ECBnh, quy\u1EC1n cao nh\u1EA5t)");if(kn)ne.push(qt.length?`FunctionRight=[${qt.join(", ")}] (m\u1EB7c \u0111\u1ECBnh \u2014 qu\xE9t \u0111\u01B0\u1EE3c t\u1EEB module.json/project)`:"FunctionRight=[] (m\u1EB7c \u0111\u1ECBnh \u2014 kh\xF4ng qu\xE9t \u0111\u01B0\u1EE3c m\xE3 n\xE0o)");return ne.length?`user preview: ${ne.join(" \xB7 ")}. rightTest()/v-if theo FunctionRight kh\xE1c m\xE3 tr\xEAn s\u1EBD \u1EA9n \u2014 truy\u1EC1n tham s\u1ED1 \`user\` \u0111\u1EC3 test vai tr\xF2 h\u1EA1n ch\u1EBF h\u01A1n.`:null})(),Je=yn(o.mTitle||o.ModuleName||o.ModuleID),te=I?"":A?D.css??"":$s(N.components),Le=[E,$s(G.components)].filter((ne)=>ne.trim()).join(`
239
- `),vn=I?"":A?`<script data-fui-projectdata="${yn(D.jsUrl)}">
240
- ${St(D.js??"","script")}
237
+ `+"• 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(xe.unverifiable.length>0)n.push(`Không đối chiếu được component cấp project: ${xe.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 Pe=A.components.filter((he)=>he.css.trim()).map((he)=>he.name);if(Pe.length>0&&!(D.css??"").trim())n.push(`StyleCSS cấp project tải về RỖNG nhưng component local có khai <style>: ${Pe.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 z=await Ul(K.join(e,"components"));n.push(...z.warnings);let N=j(i?.data)?i.data:i??{},pe={...N,ProjectName:N.ProjectName??r?.ProjectName??o.ProjectID,title:N.title??o.mTitle??o.ModuleName??o.ModuleID},Re=Ll(D.js,"projectGroupSetting"),V=Ll(D.js,"projectData");if(_&&!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 ce=String(N.apiDomain??""),ye=typeof V?.apiDomain==="string"?V.apiDomain:"";if(V&&ce&&ye&&ce!==ye)n.push(`project.json local có apiDomain "${ce}" nhưng bản trên server là "${ye}" — `+"render dùng bản server (giống trang thật). Chạy `project_sync` để đồng bộ file local.");let Ke=j(h.set)?h.set:null,nt=Of({groupSetting:Re,projectData:V??N,moduleSet:Ke},u),Te=String(nt.settings.apiDomain??""),Et=nt.origins.apiDomain??"(không tầng nào khai)",Ge=(()=>{let oe=typeof nt.settings.userInfo==="string"?nt.settings.userInfo.trim():"";if(!oe)return"";return/^https?:\/\//i.test(oe)?oe:`${Te}${oe}`})();if(!Te)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 ot={},_t=l.basePath,Q=t.user??{},ao=!("SystemRight"in Q),Rn=!("FunctionRight"in Q),zt=Bt([p,JSON.stringify(N),w,y,v]),co={user:{UserID:"render-preview",FirstName:"Render",LastName:"Preview",Email:"render@preview.local",SystemRight:9,FunctionRight:zt,...Q},...t.vueData??{}},lo=Rn?Zn(zt,(await qt(s)).functionRights):null,uo=(()=>{let oe=[];if(ao)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})(),ze=In(o.mTitle||o.ModuleName||o.ModuleID),ne=I?"":_?D.css??"":Ls(A.components),Ue=[E,Ls(z.components)].filter((oe)=>oe.trim()).join(`
238
+ `),Tn=I?"":_?`<script data-fui-projectdata="${In(D.jsUrl)}">
239
+ ${Tt(D.js??"","script")}
241
240
  </script>`:`<script data-fui-projectdata="local">
242
- var $projectData = ${qo(de,2)};
243
- ${St(Ss(N.components),"script")}
244
- </script>`,so=`<script data-fui-moduledata="local">
245
- var $moduleUI = ${qo(m,2)};
246
- ${St(v,"script")}
247
- ${St(Ss(G.components),"script")}
241
+ var $projectData = ${er(pe,2)};
242
+ ${Tt(Fs(A.components),"script")}
243
+ </script>`,po=`<script data-fui-moduledata="local">
244
+ var $moduleUI = ${er(h,2)};
245
+ ${Tt(v,"script")}
246
+ ${Tt(Fs(z.components),"script")}
248
247
  </script>`;return{html:`<!DOCTYPE html>
249
248
  <html>
250
249
  <head>
251
250
  <meta charset="UTF-8" />
252
251
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
253
- <title>${Je}</title>
252
+ <title>${ze}</title>
254
253
  ${C.styles.join(`
255
254
  `)}
256
255
  ${w}
257
- ${te.trim()?`<style data-fui-project-css>
258
- ${St(te,"style")}
256
+ ${ne.trim()?`<style data-fui-project-css>
257
+ ${Tt(ne,"style")}
259
258
  </style>`:""}
260
- ${Le.trim()?`<style data-fui-module-css>
261
- ${St(Le,"style")}
259
+ ${Ue.trim()?`<style data-fui-module-css>
260
+ ${Tt(Ue,"style")}
262
261
  </style>`:""}
263
262
  <script>
264
263
  var urlReferrer = '';
@@ -267,7 +266,7 @@ ${St(Le,"style")}
267
266
  var Framework = ${JSON.stringify(a)};
268
267
  </script>
269
268
  <script>
270
- ${Gh(o,It,no)}
269
+ ${Uf(o,_t,co)}
271
270
  </script>
272
271
  </head>
273
272
  <body>
@@ -280,131 +279,152 @@ ${Gh(o,It,no)}
280
279
  ${C.scripts.join(`
281
280
  `)}
282
281
  ${I?"":`<script>
283
- var $projectGroupSetting = ${qo(et,2)};
282
+ var $projectGroupSetting = ${er(ot,2)};
284
283
  </script>`}
285
- ${vn}
286
- ${so}
284
+ ${Tn}
285
+ ${po}
287
286
  </body>
288
- </html>`,moduleInfo:o,basePath:It,origin:d,apiDomain:Ce,apiDomainOrigin:jt,userInfoUrl:We,previewUser:no.user,importsLoaded:C.loaded,importProblems:C.problems,frameworkNote:S,htmlOnly:I,projectBundleNotes:I?["projectdata/StyleCSS: KH\xD4NG nh\xFAng (HTMLOnly=true \u2014 server c\u0169ng kh\xF4ng nh\xFAng)"]:A?D.notes:[...t.forceLocalProjectBundle?[]:D.notes,t.forceLocalProjectBundle?"projectdata/StyleCSS: d\u1EF1ng t\u1EEB workspace local (ch\u1EBF \u0111\u1ED9 preview c\u1EA5p component \u2014 b\u1EA3n local ch\u01B0a push v\u1EABn hi\u1EC7n)":"projectdata/StyleCSS: d\u1EF1ng t\u1EEB workspace local (kh\xF4ng t\u1EA3i \u0111\u01B0\u1EE3c b\u1EA3n server)"],rightDefaultNote:ro,undefinedRightsNote:oo,warnings:n}}function Xh(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 Ho={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS","Access-Control-Allow-Headers":"*"};function Yh(e){if(!e)return null;try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return null}}async function Qh(e,t,n,o,s,r,i,a){if(!o||n!==o)return null;let c=Xn(s);if(!c)return null;let l=await Yc(t,c.alias);if(!l)return null;let u=await Qc(t,l,c.fnName);if(!u)return null;let d=`${l}:${u}`,p=r.get(d);if(p===void 0)p=await Zc(t,l,u),r.set(d,p);if(p==="safe-read"&&i.count<Ht){i.count++;try{let y;if(!c.isAuth){let{config:E}=await Xc(t,{db:l});if(E.userToken)y={...await e.request().allHeaders(),authorization:`Bearer ${E.userToken}`}}let v=await e.fetch({timeout:Uo,headers:y});if(v.ok()){let E=await v.json().catch(()=>null);if(E!==null)return await bs(t,l,u,"live",E),a.live++,{payload:E,note:`g\u1ECDi tAPI th\u1EADt (SP ${u}, ${l})`}}}catch{}}let m=await el(t,l,u);if(m)return a.cache++,{payload:m.data,note:`_testdata cache (SP ${u}, ${l}, ngu\u1ED3n: ${m.source})`};let w=await tl(t,l,u);if(w)return await bs(t,l,u,"generated",w),a.generated++,{payload:w,note:`t\u1EF1 sinh mock (SP ${u}, ${l}), \u0111\xE3 l\u01B0u cache`};return null}async function Vo(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:E}=await zh(e,t),I=await xh(),g=[],b=new Set(E),R=new Set,C=[],S=[],N=[],D=[],A={revalidated:0,updated:0,fetched:0,memo:0,stale:0},G=new Set,_=new Set,de=!1,Ie=new Map,H={count:0},ae=Yh(i),we={live:0,cache:0,generated:0},Ve=t.urlParams&&Object.keys(t.urlParams).length?"?"+new URLSearchParams(t.urlParams).toString():"",Ze=`${r}${s}${Ve}`,Ce=(()=>{try{return c?new URL(c).pathname:""}catch{return""}})(),jt;try{jt=await I.chromium.launch({headless:!0})}catch(We){let et=We.message;if(/executable doesn't exist|Executable doesn't exist|browserType.launch/i.test(et))throw Error(`Kh\xF4ng launch \u0111\u01B0\u1EE3c Chromium: ${et}
289
- \u2192 Ch\u1EA1y m\u1ED9t l\u1EA7n: npx playwright install chromium`);throw We}try{let We=t.device??"desktop",et=Ph[We],It=t.viewport??et.viewport,Y=await jt.newPage({viewport:It,isMobile:et.isMobile,hasTouch:et.hasTouch,userAgent:et.userAgent,deviceScaleFactor:1}),to=Math.min(Math.max(t.settleMs??Ih,100),5000);Y.on("pageerror",(P)=>{C.push(String(P?.message??P))}),Y.on("console",(P)=>{let q=P.type(),V=P.text();if(q==="error"){if(!/Failed to load resource/i.test(V))C.push(V)}else if(q==="warning"&&/\[Vue warn\]|\[Vuetify\]/i.test(V))S.push(V)}),await Y.route("**/*",async(P)=>{let q=P.request(),V=q.url(),fe=q.method(),x=q.resourceType();if(q.isNavigationRequest()){if(V.startsWith(r))await P.fulfill({status:200,contentType:"text/html; charset=utf-8",body:n});else b.add(`runtime \u0111i\u1EC1u h\u01B0\u1EDBng ra "${V}" \u2014 \u0111\xE3 ch\u1EB7n (th\u01B0\u1EDDng do login gate / redirect trong action)`),await P.fulfill({status:200,contentType:"text/html; charset=utf-8",body:`<html><body>[fui-render] navigation blocked: ${yn(V)}</body></html>`});return}let M;try{M=new URL(V)}catch{await P.fulfill({status:400,contentType:"text/plain",body:""});return}if(fe==="OPTIONS"){await P.fulfill({status:204,headers:Ho,body:""});return}if(M.pathname==="/favicon.ico"){await P.fulfill({status:204,body:""});return}let ye=K.extname(M.pathname).toLowerCase();if(Ch.has(x)||fe==="GET"&&Rh.has(ye)){try{let J=await Uh(V,{refresh:t.refreshAssets,offline:t.offline});if(A[J.origin]++,J.origin==="stale")G.add(`${x} "${V}" \u2014 ${J.note}`);else if(J.changed)_.add(`${V} \u2014 ${J.note}`);await P.fulfill({status:200,contentType:J.contentType,body:J.body,headers:Ho})}catch(J){let ke=`${x} "${V}" t\u1EA3i th\u1EA5t b\u1EA1i (${J.message})`;if(x==="script"||x==="stylesheet")R.add(ke);else b.add(`${ke} \u2014 tr\u1EA3 404 (offline + ch\u01B0a cache?)`);await P.fulfill({status:404,contentType:"text/plain",body:""})}return}N.push(`${fe} ${V}`);let be=Xh(t.apiMocks,V,M.pathname),Z;if(be)Z=be.value,g.push(`API ${fe} ${M.pathname} \u2192 mock "${be.key}"`);else if(Ce&&(V===c||M.pathname===Ce))Z=l,g.push(`API ${fe} ${M.pathname} \u2192 user preview (userInfo c\u1EE7a project, auto-mock)`);else{let J=await Qh(P,o.ProjectID,M.hostname,ae,M.pathname,Ie,H,we);if(J)Z=J.payload,g.push(`API ${fe} ${M.pathname} \u2192 ${J.note}`);else Z={data:[]},b.add(`kh\xF4ng c\xF3 mock cho API "${V}" \u2014 tr\u1EA3 {data: []}`),g.push(`API ${fe} ${M.pathname} \u2192 {data:[]} (kh\xF4ng c\xF3 mock)`)}if(j(Z)&&Z.__error!==void 0){let J=j(Z.__error)?Z.__error:{};await P.fulfill({status:Number(J.status??500),contentType:"application/json; charset=utf-8",headers:Ho,body:JSON.stringify(J.responseJSON??{Message:"Simulated error"})});return}await P.fulfill({status:200,contentType:"application/json; charset=utf-8",headers:Ho,body:JSON.stringify(Z)})});let kn=Y;if(typeof kn.routeWebSocket==="function")await kn.routeWebSocket("**/*",(P)=>{b.add("module m\u1EDF WebSocket t\u1EDBi backend th\u1EADt \u2014 \u0111\xE3 ch\u1EB7n (renderUI ch\u1EC9 cho ph\xE9p g\u1ECDi th\u1EADt qua tAPI SP \u0111\xE3 x\xE1c nh\u1EADn an to\xE0n, kh\xF4ng qua WebSocket)"),P.close()});g.push("\u2500\u2500 INIT \u2500\u2500");let qt=Date.now();if(await Y.goto(Ze,{waitUntil:"networkidle",timeout:jh}),!m)try{await Y.waitForFunction("!!window.vm || !!document.querySelector('#mainContainer, #fastProjectAPP .v-application')",void 0,{timeout:15000})}catch{b.add("Vue ch\u01B0a mount sau 15s \u2014 trang ch\u01B0a init xong (xem JS error / asset h\u1ECFng / API treo b\xEAn d\u01B0\u1EDBi)")}await Y.waitForTimeout(to);let no=[A.revalidated?`${A.revalidated} revalidate 304`:"",A.updated?`${A.updated} C\u1EACP NH\u1EACT`:"",A.fetched?`${A.fetched} t\u1EA3i m\u1EDBi`:"",A.memo?`${A.memo} memo`:"",A.stale?`${A.stale} STALE`:""].filter(Boolean).join(" + ")||"0",oo=[we.live?`${we.live} g\u1ECDi th\u1EADt`:"",we.cache?`${we.cache} cache`:"",we.generated?`${we.generated} t\u1EF1 sinh`:""].filter(Boolean).join(" + ");g.push(`trang load xong sau ${((Date.now()-qt)/1000).toFixed(1)}s \xB7 asset: ${no} \xB7 ${N.length} API call${oo?` (API kh\xF4ng-mock: ${oo})`:""}`);let ro=C.length,Je=K.join(e,"_preview");await ut.mkdir(Je,{recursive:!0});for(let P=0;P<t.scenario.length;P++){let q=t.scenario[P],V=P+1,fe=C.length;try{if(j(q.set))g.push("",`\u2500\u2500 STEP ${V}: set ${Object.keys(q.set).join(", ")} \u2500\u2500`),await Y.evaluate((x)=>{let M=window,ye=(Z,J,ke)=>{if(M.Vue&&typeof M.Vue.set==="function")M.Vue.set(Z,J,ke);else Z[J]=ke},be=(Z,J,ke)=>{let xe=String(J).match(/[^.[\]']+/g)||[J],W=Z;for(let Q=0;Q<xe.length-1;Q++){if(W[xe[Q]]==null)ye(W,xe[Q],/^\d+$/.test(xe[Q+1])?[]:{});W=W[xe[Q]]}ye(W,xe[xe.length-1],ke)};for(let[Z,J]of x)be(M.vueData,Z,J)},Object.entries(q.set));else if(typeof q.click==="string"){g.push("",`\u2500\u2500 STEP ${V}: click "${q.click}" \u2500\u2500`);let x=q.click,M=typeof q.nth==="number"?q.nth-1:0,ye=Y.locator(".v-btn, button, [role='button'], .v-list-item, .v-tab, a",{hasText:x}),be=await ye.count();if(be===0)ye=Y.locator(`text=${x}`),be=await ye.count();if(be===0)b.add(`STEP ${V}: kh\xF4ng t\xECm th\u1EA5y ph\u1EA7n t\u1EED c\xF3 text "${x}" tr\xEAn DOM`),de=!0;else{if(be>1)g.push(`(kh\u1EDBp ${be} ph\u1EA7n t\u1EED \u2014 d\xF9ng #${M+1})`);await ye.nth(Math.min(M,be-1)).click({timeout:js}),await Y.waitForTimeout(350);let Z=Y.locator(".jconfirm .jconfirm-buttons button"),J=await Z.count();if(J>0){let ke=q.cancelConfirm===!0,xe=!1;for(let W=0;W<J;W++){let Q=(await Z.nth(W).innerText()).trim().toLowerCase(),oe=/h\u1EE7y|hu\u1EF7|cancel|kh\u00F4ng|no|\u0111\u00F3ng/.test(Q);if(ke===oe){g.push(`CONFIRM \u2192 b\u1EA5m "${Q}"`),await Z.nth(W).click({timeout:js}),xe=!0;break}}if(!xe)g.push(`CONFIRM \u2192 b\u1EA5m n\xFAt ${ke?"cu\u1ED1i":"\u0111\u1EA7u"} (kh\xF4ng nh\u1EADn di\u1EC7n \u0111\u01B0\u1EE3c label)`),await Z.nth(ke?J-1:0).click({timeout:js})}}}else if(typeof q.call==="string")g.push("",`\u2500\u2500 STEP ${V}: call ${q.call} \u2500\u2500`),await Y.evaluate((x)=>{let M=window;if(typeof M.CALL!=="function")throw Error("global CALL kh\xF4ng t\u1ED3n t\u1EA1i trong runtime n\xE0y");let ye=M.vueData&&M.vueData[x]!==void 0?M.vueData[x]:x;M.CALL(ye)},q.call);else if(typeof q.exe==="string")g.push("",`\u2500\u2500 STEP ${V}: exe \u2500\u2500`),await Y.evaluate((x)=>{Function(x)()},q.exe);else if(typeof q.assert==="string"){let x=typeof q.label==="string"?q.label:q.assert,M=!1;try{M=await Y.evaluate((ye)=>{try{return!!Function("with(window.vueData||{}){return ("+ye+")}")()}catch{return!1}},q.assert)}catch{M=!1}D.push({label:x,ok:M}),g.push("",`\u2500\u2500 STEP ${V}: assert ${M?"\u2714":"\u2718"} ${x} \u2500\u2500`);continue}else{b.add(`event ${V} kh\xF4ng h\u1EE3p l\u1EC7 \u2014 c\u1EA7n m\u1ED9t trong: set / click / call / exe / assert`);continue}await Y.waitForTimeout(to);for(let x=fe;x<C.length;x++)g.push(`\u2718 pageerror: ${C[x]}`);if(t.screenshotSteps){let x=K.join(Je,`render-step-${V}.png`);await Y.screenshot({path:x}),g.push(`screenshot: ${x}`)}}catch(x){de=!0,g.push(`\u2718 STEP ${V} l\u1ED7i: ${x.message.split(`
290
- `)[0]}`)}}let te=await Y.evaluate(()=>{let P=document.querySelector("#fastProjectAPP"),q=P?P.children.length:Array.from(document.body.children).filter((W)=>!/^(SCRIPT|STYLE|LINK|TEMPLATE|NOSCRIPT)$/.test(W.tagName)).length,V=window.innerWidth,fe=document.documentElement.scrollWidth>V+4,x=[],M=(W)=>W&&typeof W.className==="string"?W.className.trim().split(/\s+/).slice(0,2).join("."):"";if(fe)for(let W of document.body.querySelectorAll("*")){let Q=W.getBoundingClientRect();if(Q.width>0&&Q.right>V+4&&x.length<5){let oe=M(W);x.push(W.tagName.toLowerCase()+(oe?"."+oe:"")+" (right="+Math.round(Q.right)+"px)")}}let ye=document.body.innerText||"",be=[];for(let W of["undefined","NaN","[object Object]"])if(ye.includes(W))be.push(W);let Z=[];for(let W of document.querySelectorAll("img"))if(W.complete&&W.naturalWidth===0&&W.getBoundingClientRect().width>0&&Z.length<5)Z.push((W.getAttribute("src")||"(no src)").slice(0,120));let J=[];for(let W of document.querySelectorAll("canvas")){let Q=W.getBoundingClientRect();if((Q.width===0||Q.height===0)&&J.length<5){let oe=W.parentElement,re=M(oe);J.push("canvas trong "+(oe?oe.tagName.toLowerCase():"?")+(re?"."+re:"")+" ("+Math.round(Q.width)+"\xD7"+Math.round(Q.height)+")")}}let ke=[];{let W=(oe)=>{let re="";for(let Ke of oe.childNodes)if(Ke.nodeType===Node.TEXT_NODE)re+=Ke.textContent||"";return re.trim()},Q=[];for(let oe of document.body.querySelectorAll("*")){if(Q.length>=600)break;let re=W(oe);if(!re)continue;let Ke=oe.getBoundingClientRect();if(Ke.width<=0||Ke.height<=0)continue;Q.push({el:oe,rect:Ke,text:re.slice(0,24)})}Q.sort((oe,re)=>oe.rect.top-re.rect.top||oe.rect.left-re.rect.left);e:for(let oe=0;oe<Q.length;oe++){let re=Q[oe];for(let Ke=oe+1;Ke<Q.length;Ke++){let Ge=Q[Ke];if(Ge.rect.top>re.rect.bottom+4)break;if(re.el.contains(Ge.el)||Ge.el.contains(re.el))continue;let Du=re.rect.top+re.rect.height/2,xu=Ge.rect.top+Ge.rect.height/2;if(Math.abs(Du-xu)>Math.min(re.rect.height,Ge.rect.height)*0.6)continue;let Qo=re.rect.left<=Ge.rect.left?Ge.rect.left-re.rect.right:re.rect.left-Ge.rect.right;if(Qo<2){let Ou=Qo<0?"ch\u1ED3ng l\u1EA5n":`d\xEDnh s\xE1t (gap ${Math.round(Qo)}px)`;if(ke.push(`${Ou}: "${re.text}" \u2194 "${Ge.text}"`),ke.length>=6)break e}}}}let xe=!!document.querySelector(".v-dialog--active, .v-overlay--active");return{appChildCount:q,overflowX:fe,overflowers:x,suspectTexts:be,brokenImages:Z,deadCanvas:J,textCollisions:ke,dialogOpen:xe}}),Le=[];if(te.appChildCount===0)Le.push((m?"\u2718 <body> kh\xF4ng render \u0111\u01B0\u1EE3c ph\u1EA7n t\u1EED n\xE0o (HTMLOnly \u2014 body.html L\xC0 c\u1EA3 trang)":"\u2718 #fastProjectAPP r\u1ED7ng \u2014 controls kh\xF4ng render")+(d.length>0||R.size>0?" (C\xD3 file to\xE0n c\u1EE5c n\u1EA1p thi\u1EBFu \u2014 xem m\u1EE5c M\xD4I TR\u01AF\u1EDCNG THI\u1EBEU FILE tr\u01B0\u1EDBc khi nghi module.json)":" (ki\u1EC3m tra pageerror / module.json)"));if(te.overflowX)Le.push(`\u26A0 trang tr\xE0n ngang viewport ${It.width}px \u2014 ph\u1EA7n t\u1EED tr\xE0n: ${te.overflowers.join(", ")||"(kh\xF4ng x\xE1c \u0111\u1ECBnh)"}`);if(te.suspectTexts.length>0)Le.push(`\u26A0 text nghi v\u1EA5n hi\u1EC3n th\u1ECB tr\xEAn trang: ${te.suspectTexts.map((P)=>`"${P}"`).join(", ")} (binding thi\u1EBFu data?)`);if(te.brokenImages.length>0)Le.push(`\u26A0 \u1EA3nh v\u1EE1 (load fail nh\u01B0ng \u0111ang chi\u1EBFm ch\u1ED7): ${te.brokenImages.join(", ")}`);if(te.deadCanvas.length>0)Le.push(`\u26A0 canvas chart 0-size \u2014 chart init khi container ch\u01B0a c\xF3 k\xEDch th\u01B0\u1EDBc: ${te.deadCanvas.join(", ")}`);if(te.textCollisions.length>0)Le.push(`\u26A0 text d\xEDnh/ch\u1ED3ng l\u1EA5n (th\u1EA9m m\u1EF9 \u2014 PH\u1EA2I s\u1EEDa, kh\xF4ng \u0111\u01B0\u1EE3c b\u1ECF qua): ${te.textCollisions.join("; ")}`);let vn=K.join(Je,"render.png"),so=await Y.screenshot({path:vn}),Sn,ne="image/png";if(so.length<=pl)Sn=so.toString("base64");else try{let P=await Y.screenshot({type:"jpeg",quality:60});if(P.length<=pl)Sn=P.toString("base64"),ne="image/jpeg"}catch{}try{await Y.screenshot({path:K.join(Je,"render-full.png"),fullPage:!0})}catch{}let De;if(te.dialogOpen)try{De=K.join(Je,"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 Vt=[];try{Vt=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"]),q=[],V=window.vueData||{};for(let fe of Object.keys(V)){if(P.has(fe)||q.length>=40)continue;let x=V[fe];if(x&&typeof x==="object"&&!Array.isArray(x)&&(x.API!==void 0||x.CALL!==void 0||x.EXE!==void 0||x.FUN!==void 0||x.IF!==void 0))continue;let M;try{M=JSON.stringify(x)}catch{M=String(x)}if(M===void 0)M="undefined";if(M.length>120)M=M.slice(0,120)+"\u2026";q.push(fe+" = "+M)}return q})}catch{}let Re={ts:new Date().toISOString(),jsErrors:C.length,initErrors:ro,vueWarns:S.length,envMissing:d.length+R.size,appEmpty:te.appChildCount===0,overflowX:te.overflowX,suspectTexts:te.suspectTexts.length,brokenImages:te.brokenImages.length,deadCanvas:te.deadCanvas.length,textCollisions:te.textCollisions.length,assertFail:D.filter((P)=>!P.ok).length},me=await Eh(Je,We),Nu=me!==null&&Rs(Re)&&kl(me,Re).length===0;Re.failStreak=!Rs(Re)?0:Nu?(me?.failStreak??1)+1:1,await ut.writeFile(bl(Je,We),JSON.stringify(Re,null,1),"utf8").catch(()=>{return});let Vs=[...new Set(C)],Ws=[...S],Js=Dh(Re,Ws,Vs),L=[];if(L.push(t.reportLabel?`RENDER (browser th\u1EADt): ${t.reportLabel}`:`RENDER (browser th\u1EADt): ${e}`),L.push(..._h(Re,Js,Vs,Ws)),me)L.push(Ah(me,Re));if((Re.failStreak??0)>=Th)L.push(...Nh(Re.failStreak,Js));L.push(""),L.push(`Framework ${o.Framework??"V3"}${m?" \xB7 HTMLOnly=true (KH\xD4NG nh\xFAng framework/projectdata \u2014 nh\u01B0 production)":""} \xB7 viewport ${It.width}\xD7${It.height}${We!=="desktop"?` (${We})`:""} \xB7 CDN ${Bo}`),L.push(`Origin trang: ${r} \xB7 apiDomain: ${i||"(tr\u1ED1ng)"} [${a}]${c?` \xB7 userInfo auto-mock: ${c}`:""}`),L.push(p);for(let P of w)L.push(` \u2713 ${P}`);L.push(`Import (framework \u222A project \u222A module, sort theo FileSort/FileName): ${u.length} n\u1EA1p \u0111\u01B0\u1EE3c${d.length?`, ${d.length} KH\xD4NG n\u1EA1p \u0111\u01B0\u1EE3c`:""}`);for(let P of u)L.push(` \u2713 ${P}`);if(y)L.push(`\u2139 ${y}`);if(v)L.push(v);L.push(""),L.push(...g),L.push(""),L.push("\u2550\u2550\u2550 K\u1EBET QU\u1EA2 \u2550\u2550\u2550");for(let P of D)L.push(`${P.ok?"\u2714":"\u2718"} assert: ${P.label}`);if(d.length>0||R.size>0){L.push("","\u2500\u2500 \u26D4 M\xD4I TR\u01AF\u1EDCNG THI\u1EBEU FILE (\u0111\u1ECDc TR\u01AF\u1EDAC khi s\u1EEDa module) \u2500\u2500","C\xE1c file d\u01B0\u1EDBi \u0111\xE2y kh\xF4ng n\u1EA1p \u0111\u01B0\u1EE3c v\xE0o trang test. H\xE0m/bi\u1EBFn/style to\xE0n c\u1EE5c do ch\xFAng","\u0111\u1ECBnh ngh\u0129a s\u1EBD kh\xF4ng t\u1ED3n t\u1EA1i \u2192 l\u1ED7i JS v\xE0 l\u1ED7i hi\u1EC3n th\u1ECB b\xEAn d\u01B0\u1EDBi C\xD3 TH\u1EC2 ch\u1EC9 l\xE0 h\u1EC7 qu\u1EA3,","kh\xF4ng ph\u1EA3i l\u1ED7i c\u1EE7a module. Kh\u1EAFc ph\u1EE5c ph\u1EA7n n\xE0y tr\u01B0\u1EDBc r\u1ED3i render l\u1EA1i.");for(let P of d)L.push(`\u26D4 ${P}`);for(let P of R)L.push(`\u26D4 ${P}`)}if(G.size>0){L.push("","\u2500\u2500 \u26D4 ASSET C\u0168, KH\xD4NG X\xC1C MINH \u0110\u01AF\u1EE2C (\u0111\u1ECDc TR\u01AF\u1EDAC khi k\u1EBFt lu\u1EADn v\u1EC1 platform) \u2500\u2500","Kh\xF4ng h\u1ECFi \u0111\u01B0\u1EE3c server xem c\xE1c file d\u01B0\u1EDBi \u0111\xE2y c\xF2n \u0111\xFAng kh\xF4ng, n\xEAn trang test \u0111ang ch\u1EA1y",'b\u1EB1ng b\u1EA3n tr\xEAn \u0111\u0129a. M\u1ECDi k\u1EBFt lu\u1EADn ki\u1EC3u "th\u01B0 vi\u1EC7n X c\xF3 bug" r\xFAt ra t\u1EEB l\u01B0\u1EE3t render n\xE0y',"\u0111\u1EC1u KH\xD4NG d\xF9ng \u0111\u01B0\u1EE3c \u2014 b\u1EA3n th\u1EADt tr\xEAn CDN c\xF3 th\u1EC3 \u0111\xE3 kh\xE1c. N\u1ED1i m\u1EA1ng r\u1ED3i render l\u1EA1i,","ho\u1EB7c \xE9p t\u1EA3i m\u1EDBi b\u1EB1ng refreshAssets: true.");for(let P of G)L.push(`\u26D4 ${P}`)}if(_.size>0){L.push("","\u2500\u2500 \u2139\uFE0F ASSET PLATFORM \u0110\xC3 \u0110\u1ED4I so v\u1EDBi l\u01B0\u1EE3t render tr\u01B0\u1EDBc \u2500\u2500","H\xE0nh vi \u0111\u1ED5i m\xE0 module.json kh\xF4ng \u0111\u1EE5ng t\u1EDBi c\xF3 th\u1EC3 \u0111\u1EBFn t\u1EEB \u0111\xE2y, kh\xF4ng ph\u1EA3i t\u1EEB module.");for(let P of _)L.push(`\u2139\uFE0F ${P}`)}if(C.length>0){L.push("","\u2500\u2500 JS/Vue errors \u2500\u2500");for(let P of[...new Set(C)].slice(0,20))L.push(`\u2718 ${P.split(`
291
- `)[0].slice(0,300)}`)}if(S.length>0){L.push("","\u2500\u2500 [Vue warn] \u2500\u2500");for(let P of[...new Set(S)].slice(0,15))L.push(`\u26A0 ${P.split(`
292
- `)[0].slice(0,300)}`)}if(Le.length>0)L.push("","\u2500\u2500 DOM AUDIT \u2500\u2500",...Le);if(b.size>0){L.push("","\u2500\u2500 C\u1EA3nh b\xE1o \u2500\u2500");for(let P of b)L.push(`\u26A0 ${P}`)}if(Vt.length>0)L.push("","\u2500\u2500 STATE CU\u1ED0I (vueData) \u2500\u2500",...Vt);if(L.push(""),L.push(`Screenshot: ${vn}${Sn?" (\u0111\xEDnh k\xE8m b\xEAn d\u01B0\u1EDBi)":" (qu\xE1 l\u1EDBn \u0111\u1EC3 inline \u2014 d\xF9ng Read tool \u0111\u1EC3 xem)"}`),L.push(`Full-page: ${K.join(Je,"render-full.png")}`),De)L.push(`Dialog \u0111ang m\u1EDF \u2014 ch\u1EE5p ri\xEAng: ${De}`);L.push(""),L.push(`Th\u1ED1ng k\xEA: ${N.length} API call (t\u1EA5t c\u1EA3 mock) \xB7 ${C.length} JS error (${ro} l\xFAc init) \xB7 ${S.length} Vue warn \xB7 ${D.filter((P)=>P.ok).length}/${D.length} assert \u0111\u1EA1t${te.dialogOpen?" \xB7 dialog \u0111ang m\u1EDF":""}`);let _u=D.some((P)=>!P.ok)||de||C.length>0||te.appChildCount===0||d.length>0||R.size>0||G.size>0&&!t.offline;return{report:L.join(`
293
- `),hasFailures:_u,screenshotPath:vn,inlineScreenshotBase64:Sn,inlineScreenshotMime:ne}}finally{await jt.close().catch(()=>{return})}}var Zh="_componentPreview",$l=8;function ef(e){return e.normalize("NFD").replace(/[\u0300-\u036F]/g,"").replace(/[^a-zA-Z0-9]+/g,"-").replace(/^-+|-+$/g,"").toLowerCase()||"case"}function tf(e){let t={},n={},o=[];for(let[s,r]of Object.entries(e)){if(s.startsWith("@")||s.startsWith("#")){o.push(`prop "${s}": FUI kh\xF4ng nh\u1EADn c\xFA ph\xE1p @/# trong JSON \u2014 \u0111\u1ED5i th\xE0nh "v-on:"/"v-slot:". \u0110\xE3 b\u1ECF 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 nf(e,t,n){let{attr:o,hoisted:s,notes:r}=tf(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} \u2014 ${t}`,data:Object.keys(i).length>0?[i]:[],watch:{},controls:[{prop:"fluid pa-0",rows:[{prop:"",cols:[a]}]}],set:{}},notes:r}}async function jl(e,t){try{await Xe.access(e)}catch{return!1}return await Xe.cp(e,t,{recursive:!0}),!0}async function of(e,t){let n=gn(e),o=[];if(t){let i=await U(pe.join(ys(e,t),"_moduleInfo.json"),null);o.push(i?.Framework)}let s=await U(pe.join(n,"_projectInfo.json"),null);if(o.push(s?.Framework),!o.some(Boolean))try{let i=await Xe.readdir(pe.join(n,"modules"),{withFileTypes:!0});for(let a of i){if(!a.isDirectory())continue;let c=await U(pe.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 Il(e){let t=await Gc(),n=e.projectId||t?.projectId;if(!n)throw Error("Kh\xF4ng x\xE1c \u0111\u1ECBnh \u0111\u01B0\u1EE3c projectId. Truy\u1EC1n projectId ho\u1EB7c ch\u1EA1y session_set tr\u01B0\u1EDBc.");let o=e.moduleId||(!e.projectId?t?.moduleId:void 0),s=gn(n),r=o?ys(n,o):s,i=pe.join(r,"components"),a=o?`module (${o})`:"project (global)",c=pe.join(i,`${e.componentName}.vue`);if(!(await F(c)).trim()){let I=await Me(i);throw Error(`Kh\xF4ng t\xECm th\u1EA5y ${e.componentName}.vue trong ${i}.`+(I.length?` C\xF3 s\u1EB5n: ${I.join(", ")}.`:" Th\u01B0 m\u1EE5c components r\u1ED7ng.")+(o?"":" N\u1EBFu \u0111\xE2y l\xE0 component c\u1EA5p module, truy\u1EC1n th\xEAm 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>$l)throw Error(`${u.length} case \xD7 ${d.length} device = ${u.length*d.length} l\u1EA7n render, v\u01B0\u1EE3t gi\u1EDBi h\u1EA1n ${$l}. `+"M\u1ED7i l\u1EA7n render l\xE0 m\u1ED9t phi\xEAn browser th\u1EADt \u2014 chia nh\u1ECF th\xE0nh nhi\u1EC1u l\u1EA7n g\u1ECDi.");let p=await of(n,o),m=pe.join(fn(),Zh,ot(n,"projectId"),ot(e.componentName,"componentName")),w=pe.join(m,"_preview");await Xe.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=ef(b),C=pe.join(m,R);await Xe.rm(C,{recursive:!0,force:!0}),await Xe.mkdir(C,{recursive:!0}),await Xe.writeFile(pe.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:N}=nf(e.componentName,b,g);for(let D of N)v.add(`[${b}] ${D}`);if(await Xe.writeFile(pe.join(C,"module.json"),JSON.stringify(S,null,2),"utf-8"),await jl(i,pe.join(C,"components")),o)await jl(pe.join(r,"imports"),pe.join(C,"imports"));for(let D of d){let A=`${e.componentName} \xB7 ${b} \xB7 ${D}`,G=await Vo(C,{scenario:g.scenario??[],apiMocks:g.apiMocks??{},vueData:void 0,user:e.user,device:D,viewport:e.viewport,settleMs:e.settleMs,screenshotSteps:e.screenshotSteps,forceLocalProjectBundle:!0,reportLabel:A}),_=G.screenshotPath;if(G.screenshotPath){let de=pe.join(w,`${R}-${D}.png`);try{await Xe.rename(G.screenshotPath,de),_=de;let Ie=pe.join(pe.dirname(G.screenshotPath),"render-full.png");await Xe.rename(Ie,pe.join(w,`${R}-${D}-full.png`)).catch(()=>{})}catch{}}y.push({caseName:b,device:D,report:G.report,hasFailures:G.hasFailures,screenshotPath:_,inlineScreenshotBase64:G.inlineScreenshotBase64,inlineScreenshotMime:G.inlineScreenshotMime})}}let E=[`PREVIEW COMPONENT: ${e.componentName}`,`Scope: ${a} \xB7 Project: ${n} \xB7 Framework ${p}`,`Ngu\u1ED3n: ${c}`,`Cases: ${u.map((I)=>I.name?.trim()||"(kh\xF4ng t\xEAn)").join(", ")} \xD7 Devices: ${d.join(", ")} = ${y.length} l\u1EA7n render`,`\u1EA2nh: ${w}`,"","Kh\xF4ng gian ri\xEAng: ch\u1EC9 component n\xE0y + CSS c\u1EE7a ch\xEDnh n\xF3 + theme/import c\u1EA5p project.","style.css c\u1EE7a module KH\xD4NG n\u1EA1p \u2014 component ph\u1EA3i t\u1EF1 \u0111\u1EE9ng \u0111\u01B0\u1EE3c. T\u1EA7ng project d\u1EF1ng t\u1EEB workspace","local n\xEAn b\u1EA3n ch\u01B0a push v\u1EABn hi\u1EC7n; ch\u1EA1y `project_sync` n\u1EBFu nghi file local \u0111\xE3 c\u0169."];if(v.size>0){E.push("","\u26A0 Ghi ch\xFA d\u1EF1ng harness:");for(let I of v)E.push(` \u2022 ${I}`)}return{header:E.join(`
294
- `),runs:y,hasFailures:y.some((I)=>I.hasFailures)}}import Es from"vm";var Cl=500,Rl=15,Pl=500;function Ye(e){if(typeof e==="string")return`"${e.length>40?e.slice(0,40)+"\u2026":e}"`;return zn(e)}function As(e){return e.replace(/^`+|`+$/g,"")}function Tl(e){let t=e.split(/[\\/]+/).filter(Boolean),n=t.lastIndexOf("modules");return n>0?t[n-1]:null}async function Wo(e,t,n){let{loadAllSchemaProcNames:o,getVerifiedSpNames:s}=e;if(!t||Object.keys(t).length===0)return null;let r=Tl(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=Kc(Object.keys(t),i,a);if(c.length===0)return null;return["\uD83D\uDEA8 C\u1EA4U TR\xDAC MOCK CH\u01AFA \u0110\u01AF\u1EE2C KI\u1EC2M CH\u1EE8NG \u2014 nghi ng\u1EDD t\u1EF1 suy di\u1EC5n thay v\xEC \u0111\u1ECDc t\u1EEB SP th\u1EADt:",...c.map((l)=>` - apiMocks["${l.mockKey}"] kh\u1EDBp t\xEAn SP C\xD3 TH\u1EACT "${l.spName}" trong schema local, nh\u01B0ng SP \u0111\xF3 ch\u01B0a t\u1EEBng \u0111\u01B0\u1EE3c db_sp_verify \u0111\u1ECDc th\xE2n trong phi\xEAn n\xE0y.`),` \u2192 Ch\u1EA1y db_sp_verify({ name: "${c[0].spName}" }) \u0111\u1EC3 \u0111\u1ECDc th\xE2n SP th\u1EADt r\u1ED3i COPY NGUY\xCAN V\u0102N snippet apiMocks n\xF3 sinh s\u1EB5n \u2014 kh\xF4ng t\u1EF1 \u0111o\xE1n t\xEAn c\u1ED9t/shape cho SP \u0111\xE3 t\u1ED3n t\u1EA1i, k\u1EBFt qu\u1EA3 m\xF4 ph\u1ECFng s\u1EBD l\u1EC7ch v\u1EDBi th\u1EF1c t\u1EBF.`].join(`
295
- `)}function rf(e){let t=[],n=new Set;return Fe(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 El(e,t,n,o){let{getConnectionAlias:s,resolveApiSpName:r,writeTestData:i,classifySpSafety:a,callLiveApiSp:c,readTestData:l,generateMockForLocalSp:u}=e,d={...o},p=[],m=Tl(n);if(!m)return{mocks:d,trace:p};let w=rf(t);if(w.length===0)return{mocks:d,trace:p};let y=0;for(let{url:v,method:E}of w){let I=Xn(v);if(!I)continue;let g=await s(m,I.alias);if(!g)continue;let b=await r(m,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(m,g,b,"user",o[R]);continue}let C=await a(m,g,b),S=!1;if(C==="safe-read"&&y<Ht){y++;let A=await c(m,g,I.isAuth,I.fnName,E);if(A.ok)d[v]=A.data,p.push(`live ${v} \u2192 SP ${b} (${g}) \u2014 g\u1ECDi tAPI th\u1EADt th\xE0nh c\xF4ng`),await i(m,g,b,"live",A.data),S=!0;else p.push(`\u26A0 live ${v} \u2192 SP ${b} (${g}) g\u1ECDi th\u1EADt th\u1EA5t b\u1EA1i (${A.error??`HTTP ${A.status}`}) \u2014 r\u01A1i xu\u1ED1ng cache/t\u1EF1 sinh`)}else if(C==="unsafe-write")p.push(`skip ${v} \u2192 SP ${b} (${g}) c\xF3 l\u1EC7nh ghi \u2014 KH\xD4NG g\u1ECDi th\u1EADt, d\xF9ng cache/t\u1EF1 sinh`);else if(C==="opaque")p.push(`skip ${v} \u2192 SP ${b} (${g}) c\xF3 \u0111o\u1EA1n \u0111\u1ECDc t\u0129nh kh\xF4ng nh\xECn xuy\xEAn qua \u0111\u01B0\u1EE3c (SQL \u0111\u1ED9ng / g\u1ECDi SP kh\xE1c) \u2014 KH\xD4NG g\u1ECDi th\u1EADt, d\xF9ng cache/t\u1EF1 sinh`);else if(C==="unknown")p.push(`skip ${v} \u2192 SP ${b} (${g}) ch\u01B0a c\xF3 th\xE2n SP local \u0111\u1EC3 ph\xE2n lo\u1EA1i an to\xE0n \u2014 KH\xD4NG g\u1ECDi th\u1EADt, d\xF9ng cache/t\u1EF1 sinh`);else if(y>=Ht)p.push(`skip ${v} \u2192 SP ${b} (${g}) \u0111\xE3 \u0111\u1EA1t tr\u1EA7n ${Ht} l\u01B0\u1EE3t g\u1ECDi th\u1EADt/l\u01B0\u1EE3t simulate \u2014 d\xF9ng cache/t\u1EF1 sinh`);if(S)continue;let N=await l(m,g,b);if(N){d[v]=N.data,p.push(`cache ${v} \u2192 SP ${b} (${g}) \u2014 d\xF9ng _testdata cache (ngu\u1ED3n: ${N.source}, l\u01B0u l\xFAc ${N.savedAt})`);continue}let D=await u(m,g,b);if(D)d[v]=D,p.push(`gen ${v} \u2192 SP ${b} (${g}) \u2014 t\u1EF1 sinh mock t\u1EEB c\u1EA5u tr\xFAc SELECT, \u0111\xE3 l\u01B0u cache`),await i(m,g,b,"generated",D)}return{mocks:d,trace:p}}class Ns{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=Lt([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=Es.createContext(this.sandbox)}log(e){if(this.trace.length===Cl)this.trace.push("\u2026 (trace b\u1ECB c\u1EAFt \u2014 qu\xE1 d\xE0i)");if(this.trace.length>Cl)return;this.trace.push(" ".repeat(Math.min(this.depth,6))+e)}warn(e){this.warnings.add(e),this.log(`\u26A0 ${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)=>Ye(c)).join(", ")}) \u2014 stub, kh\xF4ng m\xF4 ph\u1ECFng UI th\u1EADt`),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\u01B0a h\u1ED7 tr\u1EE3 trong simulator \u2014 tr\u1EA3 undefined`);return}}});return{vueData:this.vueData,console:{log:(...r)=>e.log(`console.log: ${r.map((i)=>typeof i==="string"?i:Ye(i)).join(" ")}`),error:(...r)=>e.log(`console.error: ${r.map((i)=>typeof i==="string"?i:Ye(i)).join(" ")}`),warn:(...r)=>e.log(`console.warn: ${r.map((i)=>typeof i==="string"?i:Ye(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??"?")} \u2014 stub (sub-window kh\xF4ng m\xF4 ph\u1ECFng)`)},showMessage:(r)=>{if(e.log(`MESSBOX/showMessage "${String(r?.title??"")}"${r?.message?` \u2014 ${String(r.message)}`:""}`),r?.onclose)e.log("(gi\u1EA3 l\u1EADp user \u0111\xF3ng dialog \u2192 ch\u1EA1y onclose)"),e.runAction(r.onclose)},confirm:(r)=>{if(e.log(`CONFIRM "${String(r?.title??"")}" \u2192 OK (m\u1EB7c \u0111\u1ECBnh 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,Es.runInContext(`with(__scope__) { __ret__ = ${e} }`,this.ctx,{timeout:Pl}),this.sandbox.__ret__}evalJs(e,t){this.sandbox.__scope__=t,Es.runInContext(`with(__scope__) { ${e} }`,this.ctx,{timeout:Pl})}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 As(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 As(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(`\u0111\u1ECDc cross-window "${r}" \u2014 kh\xF4ng m\xF4 ph\u1ECFng \u0111\u01B0\u1EE3c, g\xE1n undefined`),this.setValue(t,i,void 0);else if(i[0]==="#")this.log(`\u2192 ghi cross-window ${i} = ${Ye(this.getVueData(r,n))} (kh\xF4ng m\xF4 ph\u1ECFng)`);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 \u2014 CALL tr\u1ECF t\u1EDBi action kh\xF4ng t\u1ED3n t\u1EA1i?`);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}" \u2192 ${i?"H\u1EE6Y":"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\u1ED7i khi ch\u1EA1y action: ${n.message} \u2014 action: ${_e(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\xF4ng t\u1ED3n t\u1EA1i (simulator ch\u1EC9 c\xF3 c\xE1c h\xE0m FUI chu\u1EA9n)`);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\u1ED7i eval: ${a.message} \u2192 coi nh\u01B0 false`)}this.log(`IF(${String(n.IF).slice(0,60)}) \u2192 ${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} = ${Ye(r[0]==="#"?"(cross-window)":this.getPath(this.vueData,r))}`).join(" \xB7 ")}`)}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\u1EDBp "${r}")`);else i={data:[]},this.warn(`kh\xF4ng c\xF3 mock cho API "${n}" \u2014 tr\u1EA3 m\u1EB7c \u0111\u1ECBnh {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(`\u2190 HTTP ${String(c.status)} (mock l\u1ED7i)`),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\u1ED7i kh\xF4ng \u0111\u01B0\u1EE3c b\u1EAFt (kh\xF4ng c\xF3 ERROROUT) \u2192 hi\u1EC7n ERROR DIALOG m\u1EB7c \u0111\u1ECBnh: ${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} = ${Ye(this.vueData[e.OUT])}`:Object.keys(e.OUT).map((l)=>`${l} = ${Ye(this.getPath(this.vueData,l))}`).join(" \xB7 ");this.log(`\u2190 OUT ${c}`)}else this.log("\u2190 (kh\xF4ng c\xF3 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<Rl;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 \u0111\u1ED5i \u2192 ${_e(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\u01B0\u1EE3t ${Rl} v\xF2ng \u2014 nguy c\u01A1 V\xD2NG L\u1EB6P V\xD4 H\u1EA0N gi\u1EEFa c\xE1c 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\u1EDE \uD83D\uDD32":"\u0110\xD3NG \u25AA"}`)}}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(As(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("\u2500\u2500 INIT \u2500\u2500"),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\u1ED7i 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(`\u2500\u2500 STEP ${s}: set ${Object.entries(n.set).map(([r,i])=>`${r}=${Ye(i)}`).join(", ")} \u2500\u2500`);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(`\u2500\u2500 STEP ${s}: click "${n.click}" \u2500\u2500`);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\xF4ng t\xECm th\u1EA5y control c\xF3 label "${n.click}" k\xE8m v-on:click/:action`);return}let c=typeof n.nth==="number"?n.nth-1:0;if(a.length>1)this.log(`(kh\u1EDBp ${a.length} control \u2014 d\xF9ng #${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 \u2014 d\xF9ng row ${p} c\u1EE7a ${l.tableItemsExpr})`);else d={},this.warn(`control n\u1EB1m trong f-table nh\u01B0ng "${l.tableItemsExpr??""}" r\u1ED7ng \u2014 item = {} (truy\u1EC1n "item" trong event \u0111\u1EC3 ch\u1EC9 \u0111\u1ECBnh)`)}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(`\u2500\u2500 STEP ${s}: call ${n.call} \u2500\u2500`);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(`\u2500\u2500 STEP ${s}: exe \u2500\u2500`),this.log(`EXE ${n.exe.slice(0,100)}`);try{this.evalJs(n.exe,{...this.vueData})}catch(r){this.warn(`exe l\u1ED7i: ${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\u1ED7i eval: ${a.message}`)}this.assertResults.push({label:r,ok:i}),this.log(""),this.log(`\u2500\u2500 STEP ${s}: assert ${i?"\u2714":"\u2718"} ${r} \u2500\u2500`);return}else{this.warn(`event ${s} kh\xF4ng h\u1EE3p l\u1EC7 \u2014 c\u1EA7n m\u1ED9t 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\u1EB7c \u0111\u1ECBnh, quy\u1EC1n cao nh\u1EA5t)");if(this.functionRightDefaulted)e.push(this.scannedFunctionRightCodes.length?`FunctionRight=[${this.scannedFunctionRightCodes.join(", ")}] (m\u1EB7c \u0111\u1ECBnh \u2014 qu\xE9t \u0111\u01B0\u1EE3c t\u1EEB module.json)`:"FunctionRight=[] (m\u1EB7c \u0111\u1ECBnh \u2014 kh\xF4ng qu\xE9t \u0111\u01B0\u1EE3c m\xE3 n\xE0o trong module.json)");if(e.length===0)return null;return`user preview: ${e.join(" \xB7 ")}. rightTest()/v-if theo FunctionRight kh\xE1c m\xE3 tr\xEAn s\u1EBD tr\u1EA3 false \u2014 truy\u1EC1n tham s\u1ED1 \`user\` \u0111\u1EC3 test vai tr\xF2 h\u1EA1n ch\u1EBF h\u01A1n.`}buildReport(){let e=[...this.trace,""];e.push("\u2550\u2550\u2550 K\u1EBET QU\u1EA2 \u2550\u2550\u2550");let t=this.buildRightDefaultNote();if(t)e.push(`\u2139 ${t}`);if(this.functionRightDefaulted){let s=Kn(this.scannedFunctionRightCodes,this.opts.definedFunctionRights??null);if(s)e.push(s)}for(let s of this.assertResults)e.push(`${s.ok?"\u2714":"\u2718"} assert: ${s.label}`);if(this.warnings.size>0){e.push("","\u2500\u2500 C\u1EA3nh b\xE1o \u2500\u2500");for(let s of this.warnings)e.push(`\u26A0 ${s}`)}if(this.stubUsed.size>0)e.push("",`(stub \u0111\xE3 d\xF9ng \u2014 k\u1EBFt qu\u1EA3 c\xE1c h\xE0m n\xE0y KH\xD4NG ph\u1EA3n \xE1nh runtime th\u1EADt: ${[...this.stubUsed].join(", ")})`);e.push("","\u2500\u2500 STATE CU\u1ED0I \u2500\u2500");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} = ${Ye(r)}`)}let o=this.dialogs.filter((s)=>s.open).map((s)=>s.vModel);return e.push("",`Th\u1ED1ng k\xEA: ${this.apiCallCount} API call \xB7 ${this.toastCount} toast \xB7 ${this.watchTriggerCount} watch trigger \xB7 ${this.assertResults.filter((s)=>s.ok).length}/${this.assertResults.length} assert \u0111\u1EA1t${o.length>0?` \xB7 dialog \u0111ang m\u1EDF: ${o.join(", ")}`:""}`),e.join(`
296
- `)}}function Al(e){let t=new rt(e.paths);return ws({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 Nl(e){try{return await e()}catch(t){if(t instanceof Error)t.message=Be(t.message);throw t}}async function _l(e,t,n){let o=Al(e),s=await Ut(t),r=await Wo(hn(o,e.session),n.apiMocks,t),i=`${s?`${s}
287
+ </html>`,moduleInfo:o,basePath:_t,origin:d,apiDomain:Te,apiDomainOrigin:Et,userInfoUrl:Ge,previewUser:co.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)"]:_?D.notes:[...t.forceLocalProjectBundle?[]:D.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:uo,undefinedRightsNote:lo,warnings:n}}function Vf(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 Qo={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS","Access-Control-Allow-Headers":"*"};function Wf(e){if(!e)return null;try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return null}}async function qf(e,t,n,o,s,r,i,a){if(!o||n!==o)return null;let c=no(s);if(!c)return null;let l=await kl(t,c.alias);if(!l)return null;let u=await vl(t,l,c.fnName);if(!u)return null;let d=`${l}:${u}`,p=r.get(d);if(p===void 0)p=await Sl(t,l,u),r.set(d,p);if(p==="safe-read"&&i.count<Kt){i.count++;try{let y;if(!c.isAuth){let{config:E}=await bl(t,{db:l});if(E.userToken)y={...await e.request().allHeaders(),authorization:`Bearer ${E.userToken}`}}let v=await e.fetch({timeout:Yo,headers:y});if(v.ok()){let E=await v.json().catch(()=>null);if(E!==null)return await xs(t,l,u,"live",E),a.live++,{payload:E,note:`gọi tAPI thật (SP ${u}, ${l})`}}}catch{}}let h=await $l(t,l,u);if(h)return a.cache++,{payload:h.data,note:`_testdata cache (SP ${u}, ${l}, nguồn: ${h.source})`};let w=await jl(t,l,u);if(w)return await xs(t,l,u,"generated",w),a.generated++,{payload:w,note:`tự sinh mock (SP ${u}, ${l}), đã lưu cache`};return null}async function tr(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:E}=await Hf(e,t),I=await Rf(),g=[],b=new Set(E),R=new Set,C=[],S=[],A=[],D=[],_={revalidated:0,updated:0,fetched:0,memo:0,stale:0},z=new Set,N=new Set,pe=!1,Re=new Map,V={count:0},ce=Wf(i),ye={live:0,cache:0,generated:0},Ke=t.urlParams&&Object.keys(t.urlParams).length?"?"+new URLSearchParams(t.urlParams).toString():"",nt=`${r}${s}${Ke}`,Te=(()=>{try{return c?new URL(c).pathname:""}catch{return""}})(),Et;try{Et=await I.chromium.launch({headless:!0})}catch(Ge){let ot=Ge.message;if(/executable doesn't exist|Executable doesn't exist|browserType.launch/i.test(ot))throw Error(`Không launch được Chromium: ${ot}
288
+ → Chạy một lần: npx playwright install chromium`);throw Ge}try{let Ge=t.device??"desktop",ot=kf[Ge],_t=t.viewport??ot.viewport,Q=await Et.newPage({viewport:_t,isMobile:ot.isMobile,hasTouch:ot.hasTouch,userAgent:ot.userAgent,deviceScaleFactor:1}),ao=Math.min(Math.max(t.settleMs??wf,100),5000);Q.on("pageerror",(P)=>{C.push(String(P?.message??P))}),Q.on("console",(P)=>{let q=P.type(),B=P.text();if(q==="error"){if(!/Failed to load resource/i.test(B))C.push(B)}else if(q==="warning"&&/\[Vue warn\]|\[Vuetify\]/i.test(B))S.push(B)}),await Q.route("**/*",async(P)=>{let q=P.request(),B=q.url(),ge=q.method(),O=q.resourceType();if(q.isNavigationRequest()){if(B.startsWith(r))await P.fulfill({status:200,contentType:"text/html; charset=utf-8",body:n});else b.add(`runtime điều hướng ra "${B}" — đã 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: ${In(B)}</body></html>`});return}let F;try{F=new URL(B)}catch{await P.fulfill({status:400,contentType:"text/plain",body:""});return}if(ge==="OPTIONS"){await P.fulfill({status:204,headers:Qo,body:""});return}if(F.pathname==="/favicon.ico"){await P.fulfill({status:204,body:""});return}let be=K.extname(F.pathname).toLowerCase();if(yf.has(O)||ge==="GET"&&bf.has(be)){try{let G=await Af(B,{refresh:t.refreshAssets,offline:t.offline});if(_[G.origin]++,G.origin==="stale")z.add(`${O} "${B}" — ${G.note}`);else if(G.changed)N.add(`${B} — ${G.note}`);await P.fulfill({status:200,contentType:G.contentType,body:G.body,headers:Qo})}catch(G){let ve=`${O} "${B}" tải thất bại (${G.message})`;if(O==="script"||O==="stylesheet")R.add(ve);else b.add(`${ve} — trả 404 (offline + chưa cache?)`);await P.fulfill({status:404,contentType:"text/plain",body:""})}return}A.push(`${ge} ${B}`);let ke=Vf(t.apiMocks,B,F.pathname),ee;if(ke)ee=ke.value,g.push(`API ${ge} ${F.pathname} → mock "${ke.key}"`);else if(Te&&(B===c||F.pathname===Te))ee=l,g.push(`API ${ge} ${F.pathname} → user preview (userInfo của project, auto-mock)`);else{let G=await qf(P,o.ProjectID,F.hostname,ce,F.pathname,Re,V,ye);if(G)ee=G.payload,g.push(`API ${ge} ${F.pathname} → ${G.note}`);else ee={data:[]},b.add(`không có mock cho API "${B}" — trả {data: []}`),g.push(`API ${ge} ${F.pathname} → {data:[]} (không có mock)`)}if(j(ee)&&ee.__error!==void 0){let G=j(ee.__error)?ee.__error:{};await P.fulfill({status:Number(G.status??500),contentType:"application/json; charset=utf-8",headers:Qo,body:JSON.stringify(G.responseJSON??{Message:"Simulated error"})});return}await P.fulfill({status:200,contentType:"application/json; charset=utf-8",headers:Qo,body:JSON.stringify(ee)})});let Rn=Q;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 Q.goto(nt,{waitUntil:"networkidle",timeout:gf}),!h)try{await Q.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 Q.waitForTimeout(ao);let co=[_.revalidated?`${_.revalidated} revalidate 304`:"",_.updated?`${_.updated} CẬP NHẬT`:"",_.fetched?`${_.fetched} tải mới`:"",_.memo?`${_.memo} memo`:"",_.stale?`${_.stale} STALE`:""].filter(Boolean).join(" + ")||"0",lo=[ye.live?`${ye.live} gọi thật`:"",ye.cache?`${ye.cache} cache`:"",ye.generated?`${ye.generated} tự sinh`:""].filter(Boolean).join(" + ");g.push(`trang load xong sau ${((Date.now()-zt)/1000).toFixed(1)}s · asset: ${co} · ${A.length} API call${lo?` (API không-mock: ${lo})`:""}`);let uo=C.length,ze=K.join(e,"_preview");await mt.mkdir(ze,{recursive:!0});for(let P=0;P<t.scenario.length;P++){let q=t.scenario[P],B=P+1,ge=C.length;try{if(j(q.set))g.push("",`── STEP ${B}: set ${Object.keys(q.set).join(", ")} ──`),await Q.evaluate((O)=>{let F=window,be=(ee,G,ve)=>{if(F.Vue&&typeof F.Vue.set==="function")F.Vue.set(ee,G,ve);else ee[G]=ve},ke=(ee,G,ve)=>{let Oe=String(G).match(/[^.[\]']+/g)||[G],J=ee;for(let Z=0;Z<Oe.length-1;Z++){if(J[Oe[Z]]==null)be(J,Oe[Z],/^\d+$/.test(Oe[Z+1])?[]:{});J=J[Oe[Z]]}be(J,Oe[Oe.length-1],ve)};for(let[ee,G]of O)ke(F.vueData,ee,G)},Object.entries(q.set));else if(typeof q.click==="string"){g.push("",`── STEP ${B}: click "${q.click}" ──`);let O=q.click,F=typeof q.nth==="number"?q.nth-1:0,be=Q.locator(".v-btn, button, [role='button'], .v-list-item, .v-tab, a",{hasText:O}),ke=await be.count();if(ke===0)be=Q.locator(`text=${O}`),ke=await be.count();if(ke===0)b.add(`STEP ${B}: không tìm thấy phần tử có text "${O}" trên DOM`),pe=!0;else{if(ke>1)g.push(`(khớp ${ke} phần tử — dùng #${F+1})`);await be.nth(Math.min(F,ke-1)).click({timeout:Us}),await Q.waitForTimeout(350);let ee=Q.locator(".jconfirm .jconfirm-buttons button"),G=await ee.count();if(G>0){let ve=q.cancelConfirm===!0,Oe=!1;for(let J=0;J<G;J++){let Z=(await ee.nth(J).innerText()).trim().toLowerCase(),re=/hủy|huỷ|cancel|không|no|đóng/.test(Z);if(ve===re){g.push(`CONFIRM → bấm "${Z}"`),await ee.nth(J).click({timeout:Us}),Oe=!0;break}}if(!Oe)g.push(`CONFIRM → bấm nút ${ve?"cuối":"đầu"} (không nhận diện được label)`),await ee.nth(ve?G-1:0).click({timeout:Us})}}}else if(typeof q.call==="string")g.push("",`── STEP ${B}: call ${q.call} ──`),await Q.evaluate((O)=>{let F=window;if(typeof F.CALL!=="function")throw Error("global CALL không tồn tại trong runtime này");let be=F.vueData&&F.vueData[O]!==void 0?F.vueData[O]:O;F.CALL(be)},q.call);else if(typeof q.exe==="string")g.push("",`── STEP ${B}: exe ──`),await Q.evaluate((O)=>{Function(O)()},q.exe);else if(typeof q.assert==="string"){let O=typeof q.label==="string"?q.label:q.assert,F=!1;try{F=await Q.evaluate((be)=>{try{return!!Function("with(window.vueData||{}){return ("+be+")}")()}catch{return!1}},q.assert)}catch{F=!1}D.push({label:O,ok:F}),g.push("",`── STEP ${B}: assert ${F?"✔":"✘"} ${O} ──`);continue}else{b.add(`event ${B} không hợp lệ — cần một trong: set / click / call / exe / assert`);continue}await Q.waitForTimeout(ao);for(let O=ge;O<C.length;O++)g.push(`✘ pageerror: ${C[O]}`);if(t.screenshotSteps){let O=K.join(ze,`render-step-${B}.png`);await Q.screenshot({path:O}),g.push(`screenshot: ${O}`)}}catch(O){pe=!0,g.push(`✘ STEP ${B} lỗi: ${O.message.split(`
289
+ `)[0]}`)}}let ne=await Q.evaluate(()=>{let P=document.querySelector("#fastProjectAPP"),q=P?P.children.length:Array.from(document.body.children).filter((J)=>!/^(SCRIPT|STYLE|LINK|TEMPLATE|NOSCRIPT)$/.test(J.tagName)).length,B=window.innerWidth,ge=document.documentElement.scrollWidth>B+4,O=[],F=(J)=>J&&typeof J.className==="string"?J.className.trim().split(/\s+/).slice(0,2).join("."):"";if(ge)for(let J of document.body.querySelectorAll("*")){let Z=J.getBoundingClientRect();if(Z.width>0&&Z.right>B+4&&O.length<5){let re=F(J);O.push(J.tagName.toLowerCase()+(re?"."+re:"")+" (right="+Math.round(Z.right)+"px)")}}let be=document.body.innerText||"",ke=[];for(let J of["undefined","NaN","[object Object]"])if(be.includes(J))ke.push(J);let ee=[];for(let J of document.querySelectorAll("img"))if(J.complete&&J.naturalWidth===0&&J.getBoundingClientRect().width>0&&ee.length<5)ee.push((J.getAttribute("src")||"(no src)").slice(0,120));let G=[];for(let J of document.querySelectorAll("canvas")){let Z=J.getBoundingClientRect();if((Z.width===0||Z.height===0)&&G.length<5){let re=J.parentElement,se=F(re);G.push("canvas trong "+(re?re.tagName.toLowerCase():"?")+(se?"."+se:"")+" ("+Math.round(Z.width)+"×"+Math.round(Z.height)+")")}}let ve=[];{let J=(re)=>{let se="";for(let Xe of re.childNodes)if(Xe.nodeType===Node.TEXT_NODE)se+=Xe.textContent||"";return se.trim()},Z=[];for(let re of document.body.querySelectorAll("*")){if(Z.length>=600)break;let se=J(re);if(!se)continue;let Xe=re.getBoundingClientRect();if(Xe.width<=0||Xe.height<=0)continue;Z.push({el:re,rect:Xe,text:se.slice(0,24)})}Z.sort((re,se)=>re.rect.top-se.rect.top||re.rect.left-se.rect.left);e:for(let re=0;re<Z.length;re++){let se=Z[re];for(let Xe=re+1;Xe<Z.length;Xe++){let Ye=Z[Xe];if(Ye.rect.top>se.rect.bottom+4)break;if(se.el.contains(Ye.el)||Ye.el.contains(se.el))continue;let ud=se.rect.top+se.rect.height/2,dd=Ye.rect.top+Ye.rect.height/2;if(Math.abs(ud-dd)>Math.min(se.rect.height,Ye.rect.height)*0.6)continue;let dr=se.rect.left<=Ye.rect.left?Ye.rect.left-se.rect.right:se.rect.left-Ye.rect.right;if(dr<2){let pd=dr<0?"chồng lấn":`dính sát (gap ${Math.round(dr)}px)`;if(ve.push(`${pd}: "${se.text}" ↔ "${Ye.text}"`),ve.length>=6)break e}}}}let Oe=!!document.querySelector(".v-dialog--active, .v-overlay--active");return{appChildCount:q,overflowX:ge,overflowers:O,suspectTexts:ke,brokenImages:ee,deadCanvas:G,textCollisions:ve,dialogOpen:Oe}}),Ue=[];if(ne.appChildCount===0)Ue.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)Ue.push(`⚠ trang tràn ngang viewport ${_t.width}px — phần tử tràn: ${ne.overflowers.join(", ")||"(không xác định)"}`);if(ne.suspectTexts.length>0)Ue.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)Ue.push(`⚠ ảnh vỡ (load fail nhưng đang chiếm chỗ): ${ne.brokenImages.join(", ")}`);if(ne.deadCanvas.length>0)Ue.push(`⚠ canvas chart 0-size — chart init khi container chưa có kích thước: ${ne.deadCanvas.join(", ")}`);if(ne.textCollisions.length>0)Ue.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=K.join(ze,"render.png"),po=await Q.screenshot({path:Tn}),Pn,oe="image/png";if(po.length<=xl)Pn=po.toString("base64");else try{let P=await Q.screenshot({type:"jpeg",quality:60});if(P.length<=xl)Pn=P.toString("base64"),oe="image/jpeg"}catch{}try{await Q.screenshot({path:K.join(ze,"render-full.png"),fullPage:!0})}catch{}let xe;if(ne.dialogOpen)try{xe=K.join(ze,"render-dialog.jpg"),await Q.locator(".v-dialog--active, .v-overlay--active").first().screenshot({path:xe,type:"jpeg",quality:60,timeout:3000})}catch{xe=void 0}let Xt=[];try{Xt=await Q.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"]),q=[],B=window.vueData||{};for(let ge of Object.keys(B)){if(P.has(ge)||q.length>=40)continue;let O=B[ge];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 F;try{F=JSON.stringify(O)}catch{F=String(O)}if(F===void 0)F="undefined";if(F.length>120)F=F.slice(0,120)+"…";q.push(ge+" = "+F)}return q})}catch{}let Pe={ts:new Date().toISOString(),jsErrors:C.length,initErrors:uo,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:D.filter((P)=>!P.ok).length},he=await Sf(ze,Ge),cd=he!==null&&Ws(Pe)&&Wl(he,Pe).length===0;Pe.failStreak=!Ws(Pe)?0:cd?(he?.failStreak??1)+1:1,await mt.writeFile(Vl(ze,Ge),JSON.stringify(Pe,null,1),"utf8").catch(()=>{return});let ci=[...new Set(C)],li=[...S],ui=Cf(Pe,li,ci),U=[];if(U.push(t.reportLabel?`RENDER (browser thật): ${t.reportLabel}`:`RENDER (browser thật): ${e}`),U.push(...If(Pe,ui,ci,li)),he)U.push($f(he,Pe));if((Pe.failStreak??0)>=vf)U.push(...jf(Pe.failStreak,ui));U.push(""),U.push(`Framework ${o.Framework??"V3"}${h?" · HTMLOnly=true (KHÔNG nhúng framework/projectdata — như production)":""} · viewport ${_t.width}×${_t.height}${Ge!=="desktop"?` (${Ge})`:""} · CDN ${Zo}`),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 D)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(z.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 z)U.push(`⛔ ${P}`)}if(N.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 N)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(`
290
+ `)[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(`
291
+ `)[0].slice(0,300)}`)}if(Ue.length>0)U.push("","── DOM AUDIT ──",...Ue);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}${Pn?" (đính kèm bên dưới)":" (quá lớn để inline — dùng Read tool để xem)"}`),U.push(`Full-page: ${K.join(ze,"render-full.png")}`),xe)U.push(`Dialog đang mở — chụp riêng: ${xe}`);U.push(""),U.push(`Thống kê: ${A.length} API call (tất cả mock) · ${C.length} JS error (${uo} lúc init) · ${S.length} Vue warn · ${D.filter((P)=>P.ok).length}/${D.length} assert đạt${ne.dialogOpen?" · dialog đang mở":""}`);let ld=D.some((P)=>!P.ok)||pe||C.length>0||ne.appChildCount===0||d.length>0||R.size>0||z.size>0&&!t.offline;return{report:U.join(`
292
+ `),hasFailures:ld,screenshotPath:Tn,inlineScreenshotBase64:Pn,inlineScreenshotMime:oe}}finally{await Et.close().catch(()=>{return})}}var Bf="_componentPreview",Jl=8;function Jf(e){return e.normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/[^a-zA-Z0-9]+/g,"-").replace(/^-+|-+$/g,"").toLowerCase()||"case"}function Kf(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 Gf(e,t,n){let{attr:o,hoisted:s,notes:r}=Kf(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 Kl(e,t){try{await et.access(e)}catch{return!1}return await et.cp(e,t,{recursive:!0}),!0}async function zf(e,t){let n=$n(e),o=[];if(t){let i=await H(me.join(Ds(e,t),"_moduleInfo.json"),null);o.push(i?.Framework)}let s=await H(me.join(n,"_projectInfo.json"),null);if(o.push(s?.Framework),!o.some(Boolean))try{let i=await et.readdir(me.join(n,"modules"),{withFileTypes:!0});for(let a of i){if(!a.isDirectory())continue;let c=await H(me.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 Gl(e){let t=await wl(),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?Ds(n,o):s,i=me.join(r,"components"),a=o?`module (${o})`:"project (global)",c=me.join(i,`${e.componentName}.vue`);if(!(await L(c)).trim()){let I=await Fe(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>Jl)throw Error(`${u.length} case × ${d.length} device = ${u.length*d.length} lần render, vượt giới hạn ${Jl}. `+"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 zf(n,o),h=me.join(Sn(),Bf,it(n,"projectId"),it(e.componentName,"componentName")),w=me.join(h,"_preview");await et.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=Jf(b),C=me.join(h,R);await et.rm(C,{recursive:!0,force:!0}),await et.mkdir(C,{recursive:!0}),await et.writeFile(me.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:A}=Gf(e.componentName,b,g);for(let D of A)v.add(`[${b}] ${D}`);if(await et.writeFile(me.join(C,"module.json"),JSON.stringify(S,null,2),"utf-8"),await Kl(i,me.join(C,"components")),o)await Kl(me.join(r,"imports"),me.join(C,"imports"));for(let D of d){let _=`${e.componentName} · ${b} · ${D}`,z=await tr(C,{scenario:g.scenario??[],apiMocks:g.apiMocks??{},vueData:void 0,user:e.user,device:D,viewport:e.viewport,settleMs:e.settleMs,screenshotSteps:e.screenshotSteps,forceLocalProjectBundle:!0,reportLabel:_}),N=z.screenshotPath;if(z.screenshotPath){let pe=me.join(w,`${R}-${D}.png`);try{await et.rename(z.screenshotPath,pe),N=pe;let Re=me.join(me.dirname(z.screenshotPath),"render-full.png");await et.rename(Re,me.join(w,`${R}-${D}-full.png`)).catch(()=>{})}catch{}}y.push({caseName:b,device:D,report:z.report,hasFailures:z.hasFailures,screenshotPath:N,inlineScreenshotBase64:z.inlineScreenshotBase64,inlineScreenshotMime:z.inlineScreenshotMime})}}let E=[`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){E.push("","⚠ Ghi chú dựng harness:");for(let I of v)E.push(` • ${I}`)}return{header:E.join(`
293
+ `),runs:y,hasFailures:y.some((I)=>I.hasFailures)}}import Js from"node:vm";var zl=500,Xl=15,Yl=500;function tt(e){if(typeof e==="string")return`"${e.length>40?e.slice(0,40)+"…":e}"`;return to(e)}function Ks(e){return e.replace(/^`+|`+$/g,"")}function Ql(e){let t=e.split(/[\\/]+/).filter(Boolean),n=t.lastIndexOf("modules");return n>0?t[n-1]:null}async function nr(e,t,n){let{loadAllSchemaProcNames:o,getVerifiedSpNames:s}=e;if(!t||Object.keys(t).length===0)return null;let r=Ql(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=gl(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(`
294
+ `)}function Xf(e){let t=[],n=new Set;return Le(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 Zl(e,t,n,o){let{getConnectionAlias:s,resolveApiSpName:r,writeTestData:i,classifySpSafety:a,callLiveApiSp:c,readTestData:l,generateMockForLocalSp:u}=e,d={...o},p=[],h=Ql(n);if(!h)return{mocks:d,trace:p};let w=Xf(t);if(w.length===0)return{mocks:d,trace:p};let y=0;for(let{url:v,method:E}of w){let I=no(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((_)=>_===v);if(R??=Object.keys(o).find((_)=>v.endsWith(_)||_.endsWith(v)),R??=Object.keys(o).find((_)=>v.includes(_)||_.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<Kt){y++;let _=await c(h,g,I.isAuth,I.fnName,E);if(_.ok)d[v]=_.data,p.push(`live ${v} → SP ${b} (${g}) — gọi tAPI thật thành công`),await i(h,g,b,"live",_.data),S=!0;else p.push(`⚠ live ${v} → SP ${b} (${g}) gọi thật thất bại (${_.error??`HTTP ${_.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>=Kt)p.push(`skip ${v} → SP ${b} (${g}) đã đạt trần ${Kt} lượt gọi thật/lượt simulate — dùng cache/tự sinh`);if(S)continue;let A=await l(h,g,b);if(A){d[v]=A.data,p.push(`cache ${v} → SP ${b} (${g}) — dùng _testdata cache (nguồn: ${A.source}, lưu lúc ${A.savedAt})`);continue}let D=await u(h,g,b);if(D)d[v]=D,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",D)}return{mocks:d,trace:p}}class Gs{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=Bt([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=Js.createContext(this.sandbox)}log(e){if(this.trace.length===zl)this.trace.push("… (trace bị cắt — quá dài)");if(this.trace.length>zl)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)=>tt(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:tt(i)).join(" ")}`),error:(...r)=>e.log(`console.error: ${r.map((i)=>typeof i==="string"?i:tt(i)).join(" ")}`),warn:(...r)=>e.log(`console.warn: ${r.map((i)=>typeof i==="string"?i:tt(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,Js.runInContext(`with(__scope__) { __ret__ = ${e} }`,this.ctx,{timeout:Yl}),this.sandbox.__ret__}evalJs(e,t){this.sandbox.__scope__=t,Js.runInContext(`with(__scope__) { ${e} }`,this.ctx,{timeout:Yl})}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 Ks(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 Ks(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} = ${tt(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: ${De(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} = ${tt(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} = ${tt(this.vueData[e.OUT])}`:Object.keys(e.OUT).map((l)=>`${l} = ${tt(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<Xl;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 → ${De(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 ${Xl} 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(Ks(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}=${tt(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=Zn(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} = ${tt(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(`
295
+ `)}}function eu(e){let t=new at(e.paths);return Ns({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 tu(e){try{return await e()}catch(t){if(t instanceof Error)t.message=qe(t.message);throw t}}async function nu(e,t,n){let o=eu(e),s=await Jt(t),r=await nr(vn(o,e.session),n.apiMocks,t),i=`${s?`${s}
297
296
 
298
297
  `:""}${r?`${r}
299
298
 
300
- `:""}`,a=await Nl(()=>Vo(t,n));return{text:Be(`${i}Render UI: ${t}
299
+ `:""}`,a=await tu(()=>tr(t,n));return{text:qe(`${i}Render UI: ${t}
301
300
 
302
301
  ${a.report}
303
- `),hasFailures:a.hasFailures,screenshots:a.screenshotPath?[a.screenshotPath]:[]}}async function Dl(e,t){Al(e);let n=await Nl(()=>Il(t)),o=n.runs.map((r)=>`${r.hasFailures?"\u2718":"\u2714"} ${r.caseName} \xB7 ${r.device}${r.screenshotPath?` \u2192 ${sf(r.screenshotPath)}`:""}`).join(`
302
+ `),hasFailures:a.hasFailures,screenshots:a.screenshotPath?[a.screenshotPath]:[]}}async function ou(e,t){eu(e);let n=await tu(()=>Gl(t)),o=n.runs.map((r)=>`${r.hasFailures?"✘":"✔"} ${r.caseName} · ${r.device}${r.screenshotPath?` → ${Yf(r.screenshotPath)}`:""}`).join(`
304
303
  `),s=[`${n.header}
305
304
 
306
- \u2500\u2500 SUMMARY \u2500\u2500
305
+ ── SUMMARY ──
307
306
  ${o}`];for(let r of n.runs)if(s.push(`
308
- \u2550\u2550 ${r.caseName} \xB7 ${r.device} \u2550\u2550
309
- ${r.report}`),r.screenshotPath)s.push(`screenshot: ${r.screenshotPath}`);return{text:Be(`${s.join(`
307
+ ══ ${r.caseName} · ${r.device} ══
308
+ ${r.report}`),r.screenshotPath)s.push(`screenshot: ${r.screenshotPath}`);return{text:qe(`${s.join(`
310
309
  `)}
311
- `),hasFailures:n.hasFailures,screenshots:n.runs.flatMap((r)=>r.screenshotPath?[r.screenshotPath]:[])}}import{dirname as xl,join as af}from"path";async function Ol(e,t,n){let o=hn(new rt(e.paths),e.session),s=await Ut(t),r=await Wo(o,n.apiMocks,t),i=`${s?`${s}
310
+ `),hasFailures:n.hasFailures,screenshots:n.runs.flatMap((r)=>r.screenshotPath?[r.screenshotPath]:[])}}import{dirname as ru,join as Qf}from"node:path";async function su(e,t,n){let o=vn(new at(e.paths),e.session),s=await Jt(t),r=await nr(o,n.apiMocks,t),i=`${s?`${s}
312
311
 
313
312
  `:""}${r?`${r}
314
313
 
315
- `:""}`,a=await F(af(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 Ft(xl(xl(t))),{mocks:u,trace:d}=await El(o,c,t,n.apiMocks??{}),p=new Ns(c,{apiMocks:u,user:n.user,urlParams:n.urlParams,definedFunctionRights:l.functionRights}),{report:m,hasFailures:w}=p.simulate(n.scenario??[]),y=d.length>0?`
314
+ `:""}`,a=await L(Qf(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 qt(ru(ru(t))),{mocks:u,trace:d}=await Zl(o,c,t,n.apiMocks??{}),p=new Gs(c,{apiMocks:u,user:n.user,urlParams:n.urlParams,definedFunctionRights:l.functionRights}),{report:h,hasFailures:w}=p.simulate(n.scenario??[]),y=d.length>0?`
316
315
 
317
- \u2500\u2500 Live/Cache data (SPs without mocks, auto-resolved) \u2500\u2500
316
+ ── Live/Cache data (SPs without mocks, auto-resolved) ──
318
317
  ${d.join(`
319
- `)}`:"";return{text:Be(`${i}Simulate: ${t}
318
+ `)}`:"";return{text:qe(`${i}Simulate: ${t}
320
319
 
321
- ${m}${y}
322
- `),hasFailures:w}}var dt={module:{type:"string",short:"m"}},Ul={data:{type:"string"},"data-file":{type:"string"}};function $t(e){if(k(`${e.notes.join(`
320
+ ${h}${y}
321
+ `),hasFailures:w}}var ht={module:{type:"string",short:"m"}},lu={data:{type:"string"},"data-file":{type:"string"}};function Pt(e){if(k(`${e.notes.join(`
323
322
  `)}
324
- `),e.data)k(e.data)}async function Hl(e){return e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module"))}async function Yn(e){return e.targets.resolveModuleDir(f(e.values,"module-dir"),f(e.values,"project"),f(e.values,"module"))}var lf={name:"module validate",mcpName:"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:{...dt,"module-dir":{type:"string"}},async run(e){let t=await Yn(e),n=await Ac(t);for(let o of n.notes)T(`# ${o}`);if(k(n.data??""),n.errorCount>0)throw new pt(`module.json has ${n.errorCount} error(s)`,[...n.notes,`Validate: ${t}`,"",n.data??""].join(`
325
- `))}},uf={name:"module outline",mcpName:"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:{...dt,"module-dir":{type:"string"}},async run(e){let t=await Uc(await Yn(e));for(let n of t.notes)T(`# ${n}`);k(t.data??"")}},Bl={device:{type:"string"},viewport:{type:"string"},"settle-ms":{type:"string"},"screenshot-steps":{type:"boolean"}},Ml=["desktop","tablet","mobile"];function ql(e){let n=(typeof e.device==="string"?e.device:void 0)?.split(",").map((r)=>r.trim());for(let r of n??[])if(!Ml.includes(r))throw new h(`--device takes ${Ml.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 Vl=[["scenario","array"],["mocks","object"],["user","object"],["url-params","object"],["vue-data","object"]],Wl=[...Vl.map(([e])=>[e,`${e}-file`]),["render"],["device"],["viewport"],["settle-ms"],["screenshot-steps"],["offline"],["brief"]];function df(e,t){let n={};for(let o of Wl.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")?cf(s):s}return n}function Fl(e,t){if(!t){k(e);return}let n=Hc(e);T(`# brief: ${n.kept} of ${n.total} lines (failures, warnings, asserts, stats, screenshots); full report: rerun without --brief`),k(n.text)}var pf={name:"module simulate",mcpName:"module_simulate",usage:`fui module simulate [-p P -m M | --module-dir D] [--scenario '[{"click":"Save"}]'] [--mocks '{"Fn":{"data":[]}}']
323
+ `),e.data)k(e.data)}async function uu(e){return e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module"))}async function oo(e){return e.targets.resolveModuleDir(f(e.values,"module-dir"),f(e.values,"project"),f(e.values,"module"))}var eg={name:"module validate",mcpName:"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:{...ht,"module-dir":{type:"string"}},async run(e){let t=await oo(e),n=await el(t);for(let o of n.notes)T(`# ${o}`);if(k(n.data??""),n.errorCount>0)throw new ft(`module.json has ${n.errorCount} error(s)`,[...n.notes,`Validate: ${t}`,"",n.data??""].join(`
324
+ `))}},tg={name:"module outline",mcpName:"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:{...ht,"module-dir":{type:"string"}},async run(e){let t=await ll(await oo(e));for(let n of t.notes)T(`# ${n}`);k(t.data??"")}},du={device:{type:"string"},viewport:{type:"string"},"settle-ms":{type:"string"},"screenshot-steps":{type:"boolean"}},iu=["desktop","tablet","mobile"];function pu(e){let n=(typeof e.device==="string"?e.device:void 0)?.split(",").map((r)=>r.trim());for(let r of n??[])if(!iu.includes(r))throw new m(`--device takes ${iu.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 mu=[["scenario","array"],["mocks","object"],["user","object"],["url-params","object"],["vue-data","object"]],hu=[...mu.map(([e])=>[e,`${e}-file`]),["render"],["device"],["viewport"],["settle-ms"],["screenshot-steps"],["offline"],["brief"]];function ng(e,t){let n={};for(let o of hu.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")?Zf(s):s}return n}function au(e,t){if(!t){k(e);return}let n=ul(e);T(`# brief: ${n.kept} of ${n.total} lines (failures, warnings, asserts, stats, screenshots); full report: rerun without --brief`),k(n.text)}var og={name:"module simulate",mcpName:"module_simulate",usage:`fui module simulate [-p P -m M | --module-dir D] [--scenario '[{"click":"Save"}]'] [--mocks '{"Fn":{"data":[]}}']
326
325
  [--user '{"SystemRight":3}'] [--url-params '{"id":"1"}'] (each also as --<name>-file path)
327
326
  real browser: --render [--device desktop|tablet|mobile] [--viewport 1366x768] [--vue-data {...}] [--settle-ms N]
328
327
  [--screenshot-steps] [--refresh-assets | --offline] (needs Playwright)
329
328
  --last: reuse this module's last input (files are re-read); flags given now replace their saved value
330
- --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:{...dt,"module-dir":{type:"string"},...ls("scenario","mocks","user","url-params","vue-data"),...Bl,render:{type:"boolean"},"refresh-assets":{type:"boolean"},offline:{type:"boolean"},last:{type:"boolean"},brief:{type:"boolean"}},async run(e){let t=await Yn(e),n=e.values;if(n.last===!0){let a=await qc(t,"module simulate");if(!a)throw new h("no saved input for this module yet; run fui module simulate with flags first");let c=Wc(n,a,Wl);n=c.merged,T(`# reused last input: ${c.reused.join(", ")||"none (every input given now)"}`)}let o={};for(let[a,c]of Vl)o[a]=await Eo(n,a,c);await Vc(t,"module simulate",df(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 Ol(O(e),t,s);if(Fl(c.text,n.brief===!0),c.hasFailures)throw new pt("simulate: an assert failed",c.text);return}let r=ql(n);if((r.devices?.length??0)>1)throw new h("module render takes one --device; component preview takes several");let i=await _l(O(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});Fl(i.text,n.brief===!0);for(let a of i.screenshots)if(!i.text.includes(a))k(`screenshot: ${a}
331
- `);if(i.hasFailures)throw new pt("render: the page has failures",i.text)}},mf={name:"component preview",mcpName:"component_preview",usage:`fui component preview <uc-name> [-p P] [-m M] [--cases '[{"name":"empty","props":{"items":[]}}]'] [--device desktop,mobile]
332
- [--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:{...dt,...ls("cases","user"),...Bl},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=ql(t),s=await Dl(O(e),{componentName:n,projectId:f(t,"project"),moduleId:f(t,"module"),cases:await Eo(t,"cases","array"),devices:o.devices,viewport:o.viewport,user:await Eo(t,"user","object"),settleMs:o.settleMs,screenshotSteps:o.screenshotSteps});if(k(s.text),s.hasFailures)throw new pt("component preview: a run has failures",s.text)}},hf={name:"module preview",mcpName:"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:{...dt,"module-dir":{type:"string"}},async run(e){$t(await Nc(O(e),await Yn(e)))}},ff={name:"module publish html",mcpName:"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:{...dt,"module-dir":{type:"string"},approval:{type:"string"}},async run(e){$t(await _c(O(e),await Yn(e),f(e.values,"approval")))}},gf={name:"module publish json",mcpName:"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:dt,async run(e){$t(await xo(O(e),await Hl(e)))}};function Ll(e){let t=e==="script"?"script.js":"style.css";return{name:`module publish ${e}`,mcpName:`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:dt,async run(n){$t(await Oo(O(n),await Hl(n),t))}}}function Jo(e,t,n){return{name:`component ${e}`,mcpName:`component_${e}`,usage:`fui component ${e}${e==="list"?"":" <name>"} [-p P [-m M]]`,summary:n,options:dt,async run(o){let s=o.positionals[0];if(e!=="list"&&!s)throw new h("missing <name>");$t(await t(o,s))}}}async function Ko(e){return cn(O(e),f(e.values,"project"),f(e.values,"module"))}var wf=[Jo("list",async(e)=>xc(O(e),await Ko(e)),"Refresh _components.json from the server (archives .vue files the server dropped)"),Jo("new",async(e,t)=>Mo(O(e),await Ko(e),t),"Create a component on the server from local components/<name>.vue"),Jo("update",async(e,t)=>Fo(O(e),await Ko(e),t),"Overwrite an existing component from local components/<name>.vue (skips when unchanged)"),Jo("delete",async(e,t)=>Oc(O(e),await Ko(e),t),"Delete a component on the server (ask the user first); archives the local .vue")],yf={name:"project new",mcpName:"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:Ul,async run(e){$t(await Mc(O(e),await cs(e.values)))}},bf={name:"project update",mcpName:"project_update",usage:"fui project update (--data '<json with id>' | --data - | --data-file f)",summary:"Edit project metadata; projectData.id is required",options:Ul,async run(e){$t(await Fc(O(e),await cs(e.values)))}},kf={name:"project publish-config",mcpName:"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"));$t(await Lo(O(e),t))}},Jl=[lf,uf,pf,hf,ff,gf,Ll("script"),Ll("css"),...wf,mf,yf,bf,kf];import{rename as vf}from"fs/promises";import{join as Kl}from"path";async function zl(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 Xl(e,t){return(await e.api()).listFrameworkImportFiles(t)}async function Yl(e,t){let o=(await(await e.api()).getFileContent(t))?.data?.FileContent??"",s=await ts(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 Ql(e,t,n){let o=await(await e.api()).uploadFileContent(t,n),s=await ts(e.paths,t,n);return{notes:[`ok uploaded content of import ${t}`,...s.map((r)=>`local copy synced: ${r}`)],data:`${JSON.stringify(o)}
333
- `}}async function Zl(e,t){let n=await ce(e),{effectiveSort:o,note:s}=ns(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)}
334
- `}}async function eu(e,t){let n=await ce(e),o=await es(e.paths,t.fileId),s=t.fileSort,r="";if(t.fileSort!==void 0&&o.length>0){let u=ns(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=nn(t.contentType)?`imports/${Hn(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 vf(Kl(u.baseDir,d),Kl(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)}
335
- `}}async function tu(e,t){let n=await ce(e),o=await es(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)}
336
- `}}var Sf={1:"View only",2:"Develop",3:"Admin"};function Qn(e){let t=typeof e==="number"?e:Number(e);if(!Number.isFinite(t))return String(e??"");let n=Sf[t];return n?`${t} (${n})`:String(t)}function Gl(e,t){let n=t.trim().toLowerCase();return e.find((o)=>String(o.UserID??"").trim().toLowerCase()===n)}function Go(e,t){return e.map((n)=>({userId:String(n.UserID??"?"),username:n.UserName||"\u2014",fullName:n.FullName||"\u2014",...t?{right:Qn(n.UserRight)}:{}}))}async function nu(e,t,n){return(await e.api()).searchUsers(t,n??"%")}async function ou(e,t){return(await e.api()).listProjectUserRights(t)}async function ru(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=Gl(await o.listProjectUserRights(t.projectId),r);if(n==="new"&&i)throw new h(`user "${r}" already has right ${Qn(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=Gl(c,r),u=n==="delete"?`ok revoked the right of "${r}" on ${s}`:`ok set right ${Qn(a)} for "${r}" on ${s}`,d=n==="delete"?l?`warning: post-check: the user is still listed with right ${Qn(l.UserRight)}; the server did not remove it`:"post-check: user no longer listed":l?`post-check: ${r} has ${Qn(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 iu={module:{type:"string",short:"m"}},au={type:{type:"string"},"content-type":{type:"string"},name:{type:"string"},sort:{type:"string"},set:{type:"string",multiple:!0}};function zo(e){if(k(`${e.notes.join(`
329
+ --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:{...ht,"module-dir":{type:"string"},...Is("scenario","mocks","user","url-params","vue-data"),...du,render:{type:"boolean"},"refresh-assets":{type:"boolean"},offline:{type:"boolean"},last:{type:"boolean"},brief:{type:"boolean"}},async run(e){let t=await oo(e),n=e.values;if(n.last===!0){let a=await pl(t,"module simulate");if(!a)throw new m("no saved input for this module yet; run fui module simulate with flags first");let c=hl(n,a,hu);n=c.merged,T(`# reused last input: ${c.reused.join(", ")||"none (every input given now)"}`)}let o={};for(let[a,c]of mu)o[a]=await Ho(n,a,c);await ml(t,"module simulate",ng(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 su(M(e),t,s);if(au(c.text,n.brief===!0),c.hasFailures)throw new ft("simulate: an assert failed",c.text);return}let r=pu(n);if((r.devices?.length??0)>1)throw new m("module render takes one --device; component preview takes several");let i=await nu(M(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});au(i.text,n.brief===!0);for(let a of i.screenshots)if(!i.text.includes(a))k(`screenshot: ${a}
330
+ `);if(i.hasFailures)throw new ft("render: the page has failures",i.text)}},rg={name:"component preview",mcpName:"component_preview",usage:`fui component preview <uc-name> [-p P] [-m M] [--cases '[{"name":"empty","props":{"items":[]}}]'] [--device desktop,mobile]
331
+ [--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:{...ht,...Is("cases","user"),...du},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=pu(t),s=await ou(M(e),{componentName:n,projectId:f(t,"project"),moduleId:f(t,"module"),cases:await Ho(t,"cases","array"),devices:o.devices,viewport:o.viewport,user:await Ho(t,"user","object"),settleMs:o.settleMs,screenshotSteps:o.screenshotSteps});if(k(s.text),s.hasFailures)throw new ft("component preview: a run has failures",s.text)}},sg={name:"module preview",mcpName:"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:{...ht,"module-dir":{type:"string"}},async run(e){Pt(await tl(M(e),await oo(e)))}},ig={name:"module publish html",mcpName:"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:{...ht,"module-dir":{type:"string"},approval:{type:"string"}},async run(e){Pt(await nl(M(e),await oo(e),f(e.values,"approval")))}},ag={name:"module publish json",mcpName:"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:ht,async run(e){Pt(await Jo(M(e),await uu(e)))}};function cu(e){let t=e==="script"?"script.js":"style.css";return{name:`module publish ${e}`,mcpName:`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:ht,async run(n){Pt(await Ko(M(n),await uu(n),t))}}}function or(e,t,n){return{name:`component ${e}`,mcpName:`component_${e}`,usage:`fui component ${e}${e==="list"?"":" <name>"} [-p P [-m M]]`,summary:n,options:ht,async run(o){let s=o.positionals[0];if(e!=="list"&&!s)throw new m("missing <name>");Pt(await t(o,s))}}}async function rr(e){return fn(M(e),f(e.values,"project"),f(e.values,"module"))}var cg=[or("list",async(e)=>rl(M(e),await rr(e)),"Refresh _components.json from the server (archives .vue files the server dropped)"),or("new",async(e,t)=>Go(M(e),await rr(e),t),"Create a component on the server from local components/<name>.vue"),or("update",async(e,t)=>zo(M(e),await rr(e),t),"Overwrite an existing component from local components/<name>.vue (skips when unchanged)"),or("delete",async(e,t)=>sl(M(e),await rr(e),t),"Delete a component on the server (ask the user first); archives the local .vue")],lg={name:"project new",mcpName:"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:lu,async run(e){Pt(await il(M(e),await js(e.values)))}},ug={name:"project update",mcpName:"project_update",usage:"fui project update (--data '<json with id>' | --data - | --data-file f)",summary:"Edit project metadata; projectData.id is required",options:lu,async run(e){Pt(await al(M(e),await js(e.values)))}},dg={name:"project publish-config",mcpName:"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"));Pt(await Xo(M(e),t))}},fu=[eg,tg,og,sg,ig,ag,cu("script"),cu("css"),...cg,rg,lg,ug,dg];import{rename as pg}from"node:fs/promises";import{join as gu}from"node:path";async function yu(e,t,n){let o=await le(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 bu(e,t){return(await e.api()).listFrameworkImportFiles(t)}async function ku(e,t){let o=(await(await e.api()).getFileContent(t))?.data?.FileContent??"",s=await ws(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 vu(e,t,n){let o=await(await e.api()).uploadFileContent(t,n),s=await ws(e.paths,t,n);return{notes:[`ok uploaded content of import ${t}`,...s.map((r)=>`local copy synced: ${r}`)],data:`${JSON.stringify(o)}
332
+ `}}async function Su(e,t){let n=await le(e),{effectiveSort:o,note:s}=ys(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)}
333
+ `}}async function $u(e,t){let n=await le(e),o=await gs(e.paths,t.fileId),s=t.fileSort,r="";if(t.fileSort!==void 0&&o.length>0){let u=ys(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=un(t.contentType)?`imports/${Kn(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 pg(gu(u.baseDir,d),gu(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)}
334
+ `}}async function ju(e,t){let n=await le(e),o=await gs(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)}
335
+ `}}var mg={1:"View only",2:"Develop",3:"Admin"};function ro(e){let t=typeof e==="number"?e:Number(e);if(!Number.isFinite(t))return String(e??"");let n=mg[t];return n?`${t} (${n})`:String(t)}function wu(e,t){let n=t.trim().toLowerCase();return e.find((o)=>String(o.UserID??"").trim().toLowerCase()===n)}function sr(e,t){return e.map((n)=>({userId:String(n.UserID??"?"),username:n.UserName||"—",fullName:n.FullName||"—",...t?{right:ro(n.UserRight)}:{}}))}async function Iu(e,t,n){return(await e.api()).searchUsers(t,n??"%")}async function Cu(e,t){return(await e.api()).listProjectUserRights(t)}async function Ru(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=wu(await o.listProjectUserRights(t.projectId),r);if(n==="new"&&i)throw new m(`user "${r}" already has right ${ro(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=wu(c,r),u=n==="delete"?`ok revoked the right of "${r}" on ${s}`:`ok set right ${ro(a)} for "${r}" on ${s}`,d=n==="delete"?l?`warning: post-check: the user is still listed with right ${ro(l.UserRight)}; the server did not remove it`:"post-check: user no longer listed":l?`post-check: ${r} has ${ro(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"}},Eu={type:{type:"string"},"content-type":{type:"string"},name:{type:"string"},sort:{type:"string"},set:{type:"string",multiple:!0}};function ir(e){if(k(`${e.notes.join(`
336
+ `)}
337
+ `),e.data)k(e.data)}function _u(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 ar(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 Tu(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 hg={name:"import list",mcpName:"file_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=M(e),n=f(e.values,"framework");if(n){k(Y(Tu(await bu(t,n.toUpperCase())),e.render));return}let{rows:o,notes:s}=await yu(t,f(e.values,"project"),f(e.values,"module"));for(let r of s)T(`# ${r}`);k(Y(Tu(o),e.render))}},fg={name:"import get",mcpName:"file_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 ku(M(e),ar(e));for(let o of t.notes)T(`# ${o}`);let n=f(e.values,"out");if(n)await x(n,t.data??""),k(`${n}
338
+ `);else k(t.data??"")}},gg={name:"import upload",mcpName:"file_import_upload_content",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=ar(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 Qe(n):await go();ir(await vu(M(e),t,s))}},wg={name:"import new",mcpName:"file_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,...Eu},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");ir(await Su(M(e),{projectId:f(t,"project"),moduleId:f(t,"module"),fileType:n,contentType:o,fileName:s,fileSort:_u(f(t,"sort")),extra:gn(t.set)}))}},yg={name:"import update",mcpName:"file_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:Eu,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)");ir(await $u(M(e),{fileId:ar(e),fileType:n,contentType:o,fileName:s,fileSort:_u(f(t,"sort")),extra:gn(t.set)}))}},bg={name:"import delete",mcpName:"file_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){ir(await ju(M(e),ar(e)))}};async function Au(e){return(await e.targets.resolveProject(f(e.values,"project"))).projectId}function kg(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 vg={name:"user search",mcpName:"project_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 Iu(M(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(Y(sr(n,!1),e.render))}},Sg={name:"user list",mcpName:"project_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 Au(e),n=await Cu(M(e),t);if(n.length===0){T(`# nobody has a right on project ${t}; find a user: fui user search <text>`);return}k(Y(sr(n,!0),e.render))}};function zs(e,t,n){return{name:`user ${e}`,mcpName:`project_user_${t}`,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 Ru(M(o),{projectId:await Au(o),userId:s,userRight:t==="delete"?void 0:kg(f(o.values,"right"))},t);k(`${r.notes.join(`
337
339
  `)}
338
- `),e.data)k(e.data)}function cu(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 Xo(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 su(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 $f={name:"import list",mcpName:"file_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:{...iu,framework:{type:"string"}},async run(e){let t=O(e),n=f(e.values,"framework");if(n){k(X(su(await Xl(t,n.toUpperCase())),e.render));return}let{rows:o,notes:s}=await zl(t,f(e.values,"project"),f(e.values,"module"));for(let r of s)T(`# ${r}`);k(X(su(o),e.render))}},jf={name:"import get",mcpName:"file_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 Yl(O(e),Xo(e));for(let o of t.notes)T(`# ${o}`);let n=f(e.values,"out");if(n)await Bun.write(n,t.data??""),k(`${n}
339
- `);else k(t.data??"")}},If={name:"import upload",mcpName:"file_import_upload_content",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=Xo(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 Bun.file(n).text():await co();zo(await Ql(O(e),t,s))}},Cf={name:"import new",mcpName:"file_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:{...iu,...au},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");zo(await Zl(O(e),{projectId:f(t,"project"),moduleId:f(t,"module"),fileType:n,contentType:o,fileName:s,fileSort:cu(f(t,"sort")),extra:ln(t.set)}))}},Rf={name:"import update",mcpName:"file_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:au,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)");zo(await eu(O(e),{fileId:Xo(e),fileType:n,contentType:o,fileName:s,fileSort:cu(f(t,"sort")),extra:ln(t.set)}))}},Pf={name:"import delete",mcpName:"file_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){zo(await tu(O(e),Xo(e)))}};async function lu(e){return(await e.targets.resolveProject(f(e.values,"project"))).projectId}function Tf(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 Ef={name:"user search",mcpName:"project_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 nu(O(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(X(Go(n,!1),e.render))}},Af={name:"user list",mcpName:"project_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 lu(e),n=await ou(O(e),t);if(n.length===0){T(`# nobody has a right on project ${t}; find a user: fui user search <text>`);return}k(X(Go(n,!0),e.render))}};function _s(e,t,n){return{name:`user ${e}`,mcpName:`project_user_${t}`,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 ru(O(o),{projectId:await lu(o),userId:s,userRight:t==="delete"?void 0:Tf(f(o.values,"right"))},t);k(`${r.notes.join(`
340
+ `),k(Y(sr(r.rows,!0),o.render))}}}var Nu=[hg,fg,gg,wg,yg,bg,vg,Sg,zs("add","new","Grant a right to a user who has none (exact UserID from fui user search)"),zs("set","update","Change the right level of a user who already has one"),zs("remove","delete","Revoke a user's right (ask the user first)")];import{mkdir as $g,readFile as jg,stat as Ig,writeFile as Cg}from"node:fs/promises";import{join as Xs}from"node:path";var cr={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"},Rg=86400000;async function Du(e){try{return(await Ig(e)).mtimeMs}catch{return null}}var Tg={name:"library",usage:`fui library [${Object.keys(cr).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=Xs(n.root,"_cache","library"),s=e[0];if(!s){let c=await Promise.all(Object.entries(cr).map(async([l,u])=>{let d=Xs(o,u.slice(u.lastIndexOf("/")+1)),p=await Du(d);return`${l} ${u} ${p?new Date(p).toISOString():"not cached"}`}));k(`name url cached
341
+ ${c.join(`
340
342
  `)}
341
- `),k(X(Go(r.rows,!0),o.render))}}}var uu=[$f,jf,If,Cf,Rf,Pf,Ef,Af,_s("add","new","Grant a right to a user who has none (exact UserID from fui user search)"),_s("set","update","Change the right level of a user who already has one"),_s("remove","delete","Revoke a user's right (ask the user first)")];import{rm as fu}from"fs/promises";import{appendFile as Nf,mkdir as _f,rename as Df,stat as xf}from"fs/promises";import{dirname as Of,join as Mf}from"path";var Ff=1048576,du=["\u274C","\u26D4","\u2718","\u26A0\uFE0F"];function Zn(e){return Mf(e,"_logs","tool-errors.jsonl")}function Lf(e,t){let n=e.split(`
342
- `).map((i)=>i.trim()).filter(Boolean),o=[],s=new Set,r=0;for(let i of du)for(let a of n){if(!a.startsWith(i)||s.has(a))continue;let c=a.length>200?`${a.slice(0,200)}\u2026`:a;if(r+c.length+1>t)return o;s.add(a),o.push(c),r+=c.length+1}return o}function Ds(e,t,n){if(e.length<=t+n)return e;let o=e.slice(t,e.length-n),s=Lf(o,Math.round((t+n)/2)),r=s.length>0?[`\u2026[c\u1EAFt ${o.length} k\xFD t\u1EF1 \u2014 gi\u1EEF l\u1EA1i ${s.length} d\xF2ng l\xFD do h\u1ECFng]\u2026`,...s]:[`\u2026[c\u1EAFt ${o.length} k\xFD t\u1EF1]\u2026`];return`${e.slice(0,t)}
343
+ `);return}let r=cr[s];if(!r)throw new m(`unknown library "${s}"; choose ${Object.keys(cr).join(", ")}`);let i=Xs(o,r.slice(r.lastIndexOf("/")+1)),a=await Du(i);if(t.refresh===!0||a===null||Date.now()-a>Rg){let c=await fetch(r,{signal:AbortSignal.timeout(30000)});if(!c.ok)throw Error(`${r} answered HTTP ${c.status}`);await $g(o,{recursive:!0}),await Cg(i,await c.text()),T(`# fetched ${r}`)}else T(`# cached ${new Date(a).toISOString()}; --refresh to fetch again`);k(t.path===!0?`${i}
344
+ `:await jg(i,"utf8"))}},xu=[Tg];import{rm as Uu}from"node:fs/promises";import{appendFile as Pg,mkdir as Eg,rename as _g,stat as Ag}from"node:fs/promises";import{dirname as Ng,join as Dg}from"node:path";var xg=1048576,Ou=["❌","⛔","✘","⚠️"];function so(e){return Dg(e,"_logs","tool-errors.jsonl")}function Og(e,t){let n=e.split(`
345
+ `).map((i)=>i.trim()).filter(Boolean),o=[],s=new Set,r=0;for(let i of Ou)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 Ys(e,t,n){if(e.length<=t+n)return e;let o=e.slice(t,e.length-n),s=Og(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)}
343
346
  ${r.join(`
344
347
  `)}
345
- ${e.slice(-n)}`}async function pu(e,t,n,o){try{let s=Zn(e);await _f(Of(s),{recursive:!0});try{if((await xf(s)).size>Ff)await Df(s,s.replace(/\.jsonl$/,".1.jsonl")).catch(()=>{return})}catch{}let r={ts:new Date().toISOString(),pid:process.pid,tool:t,message:Ds(n,400,600),...o&&o!=="error"?{kind:o}:{}};await Nf(s,`${JSON.stringify(r)}
346
- `,"utf-8")}catch{}}async function mu(e){let t="";try{t=await B(Zn(e))}catch{}let n=[];for(let o of t.split(`
347
- `)){if(!o.trim())continue;try{n.push(JSON.parse(o))}catch{}}return n}function Uf(e){let t=e.split(`
348
- `).map((o)=>o.trim()).filter(Boolean),n=du.map((o)=>t.find((s)=>s.startsWith(o))).find(Boolean);if(!n)return"(b\u1EA3n ghi kh\xF4ng ch\u1EE9a d\xF2ng l\xFD do h\u1ECFng \u2014 xem v\xED d\u1EE5 b\xEAn d\u01B0\u1EDBi)";return n.replace(/^[\u274C\u26D4\u2718\u26A0\uFE0F]+\uFE0F?\s*/,"").replace(/'[^'\s]*'|"[^"\s]*"|`[^`\s]*`/g,"'\u2026'").replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f-]{12,}\b/gi,"\u2026").replace(/[A-Za-z]:[\\/][^\s,)]+|\/[^\s,)]*\/[^\s,)]+/g,"\u2026").replace(/\b\d+\b/g,"N").replace(/\s+/g," ").trim().slice(0,140)}function hu(e){return{errors:e.filter((t)=>t.kind!=="guard"),guards:e.filter((t)=>t.kind==="guard")}}function xs(e){let t=new Map;for(let n of e){let o=Uf(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 gu(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 Hf={name:"log read",mcpName:"error_log_read",usage:"fui log read [--tool mcp_tool_name] [--limit N] [--summary] [--sample-chars N]",summary:"Shared tool-error log (Fui MCP + fui) grouped by tool 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=gu(f(t,"limit"),"--limit",0,Number.MAX_SAFE_INTEGER,15),s=gu(f(t,"sample-chars"),"--sample-chars",200,1e5,900),r=Zn(e.root),i=await mu(e.root);if(n)i=i.filter((m)=>m.tool===n);let{errors:a,guards:c}=hu(i);if(a.length===0&&c.length===0){k(`no errors logged${n?` for ${n}`:""}
348
+ ${e.slice(-n)}`}async function Mu(e,t,n,o){try{let s=so(e);await Eg(Ng(s),{recursive:!0});try{if((await Ag(s)).size>xg)await _g(s,s.replace(/\.jsonl$/,".1.jsonl")).catch(()=>{return})}catch{}let r={ts:new Date().toISOString(),pid:process.pid,tool:t,message:Ys(n,400,600),...o&&o!=="error"?{kind:o}:{}};await Pg(s,`${JSON.stringify(r)}
349
+ `,"utf-8")}catch{}}async function Fu(e){let t="";try{t=await W(so(e))}catch{}let n=[];for(let o of t.split(`
350
+ `)){if(!o.trim())continue;try{n.push(JSON.parse(o))}catch{}}return n}function Mg(e){let t=e.split(`
351
+ `).map((o)=>o.trim()).filter(Boolean),n=Ou.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 Lu(e){return{errors:e.filter((t)=>t.kind!=="guard"),guards:e.filter((t)=>t.kind==="guard")}}function Qs(e){let t=new Map;for(let n of e){let o=Mg(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 Hu(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 Fg={name:"log read",mcpName:"error_log_read",usage:"fui log read [--tool mcp_tool_name] [--limit N] [--summary] [--sample-chars N]",summary:"Shared tool-error log (Fui MCP + fui) grouped by tool 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=Hu(f(t,"limit"),"--limit",0,Number.MAX_SAFE_INTEGER,15),s=Hu(f(t,"sample-chars"),"--sample-chars",200,1e5,900),r=so(e.root),i=await Fu(e.root);if(n)i=i.filter((h)=>h.tool===n);let{errors:a,guards:c}=Lu(i);if(a.length===0&&c.length===0){k(`no errors logged${n?` for ${n}`:""}
349
352
  log file: ${r}
350
- `);return}let l=new Map;for(let m of a)l.set(m.tool,(l.get(m.tool)??0)+1);let u=xs(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 Ds(m.sample,Math.round(s*0.4),Math.round(s*0.6)).split(`
351
- `))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 xs(c).slice(0,5))p.push(` ${String(m.count).padStart(4)}x ${m.tool} ${m.signature}`)}k(`${p.join(`
353
+ `);return}let l=new Map;for(let h of a)l.set(h.tool,(l.get(h.tool)??0)+1);let u=Qs(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 Ys(h.sample,Math.round(s*0.4),Math.round(s*0.6)).split(`
354
+ `))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 Qs(c).slice(0,5))p.push(` ${String(h.count).padStart(4)}x ${h.tool} ${h.signature}`)}k(`${p.join(`
352
355
  `)}
353
- `)}},Bf={name:"log clear",mcpName:"error_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=Zn(e.root);await fu(t,{force:!0}),await fu(t.replace(/\.jsonl$/,".1.jsonl"),{force:!0}),k(`cleared ${t}
354
- `)}},wu=[Hf,Bf];import{join as qf}from"path";var Vf={module:{type:"string",short:"m"}},Os=["load","ui","html","script","css","components","imports"],Ms=["info","config","modules","components","imports"];async function yu(e,t){let n=typeof t==="string"?t:`${JSON.stringify(t,null,e.json?0:2)}
355
- `,o=f(e.values,"out");if(o)await Bun.write(o,n),k(`${o}
356
- `);else k(n)}var Wf={name:"remote module",usage:`fui remote module [-p P -m M] --part ${Os.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:{...Vf,part:{type:"string"},out:{type:"string",short:"o"}},async run(e){let t=f(e.values,"part");if(!t||!Os.includes(t))throw new h(`--part must be one of: ${Os.join(", ")}`);let{projectId:n,moduleId:o}=await e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module")),s=await O(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)}},Jf={name:"remote project",usage:`fui remote project [-p P] --part ${Ms.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||!Ms.includes(t))throw new h(`--part must be one of: ${Ms.join(", ")}`);let{projectId:n}=await e.targets.resolveProject(f(e.values,"project")),o=await O(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 bu(e,t){let n=jo(e.paths,t);if(!n)throw new h(`${t} is not inside a project of workspace ${e.paths.root}`);return n}var Kf={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 h("missing <path>");let t=e.positionals.map((r)=>bu(e,r)),n=new Wn(e.paths,await O(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 h(`module ${i} unknown; run fui module list -p ${r.projectId}`);let p=await n.fetchModuleBundle(d);a=on(d,p.content,p.components,p.script,p.css,p.dependencies),o.set(i,a)}let c=await F(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=bt(JSON.parse(c))===bt(JSON.parse(l))}catch{}u=d?"same":"differs"}else u=wt(c)===wt(l)?"same":"differs";s.push(`${r.rel} ${u} ${qf(r.projectId,"modules",r.moduleId)}`)}k(`${s.join(`
356
+ `)}},Lg={name:"log clear",mcpName:"error_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=so(e.root);await Uu(t,{force:!0}),await Uu(t.replace(/\.jsonl$/,".1.jsonl"),{force:!0}),k(`cleared ${t}
357
+ `)}},Vu=[Fg,Lg];import{join as Ug}from"node:path";var Hg={module:{type:"string",short:"m"}},Zs=["load","ui","html","script","css","components","imports"],ei=["info","config","modules","components","imports"];async function Wu(e,t){let n=typeof t==="string"?t:`${JSON.stringify(t,null,e.json?0:2)}
358
+ `,o=f(e.values,"out");if(o)await x(o,n),k(`${o}
359
+ `);else k(n)}var Vg={name:"remote module",usage:`fui remote module [-p P -m M] --part ${Zs.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:{...Hg,part:{type:"string"},out:{type:"string",short:"o"}},async run(e){let t=f(e.values,"part");if(!t||!Zs.includes(t))throw new m(`--part must be one of: ${Zs.join(", ")}`);let{projectId:n,moduleId:o}=await e.targets.resolveProjectModule(f(e.values,"project"),f(e.values,"module")),s=await M(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 Wu(e,r)}},Wg={name:"remote project",usage:`fui remote project [-p P] --part ${ei.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||!ei.includes(t))throw new m(`--part must be one of: ${ei.join(", ")}`);let{projectId:n}=await e.targets.resolveProject(f(e.values,"project")),o=await M(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 Wu(e,s)}};function qu(e,t){let n=xo(e.paths,t);if(!n)throw new m(`${t} is not inside a project of workspace ${e.paths.root}`);return n}var qg={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)=>qu(e,r)),n=new Yn(e.paths,await M(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=dn(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=St(c)===St(l)?"same":"differs";s.push(`${r.rel} ${u} ${Ug(r.projectId,"modules",r.moduleId)}`)}k(`${s.join(`
357
360
  `)}
358
- `)}},Gf={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=O(e),n=0;for(let o of e.positionals){let s=bu(e,o),r;if(!s.moduleId&&s.rel==="project.json")r=await Lo(t,s.projectId);else if(s.rel.startsWith("components/")&&s.rel.endsWith(".vue")&&!s.rel.slice(11).includes("/")){let i=await cn(t,s.projectId,s.moduleId),a=s.rel.slice(11,-4);r=e.values.new===!0?await Mo(t,i,a):await Fo(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 Oo(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(`
361
+ `)}},Bg={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=M(e),n=0;for(let o of e.positionals){let s=qu(e,o),r;if(!s.moduleId&&s.rel==="project.json")r=await Xo(t,s.projectId);else if(s.rel.startsWith("components/")&&s.rel.endsWith(".vue")&&!s.rel.slice(11).includes("/")){let i=await fn(t,s.projectId,s.moduleId),a=s.rel.slice(11,-4);r=e.values.new===!0?await Go(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 Jo(t,i):await Ko(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(`
359
362
  `)}
360
- `)}return n}},ku=[Wf,Jf,Kf,Gf];function zf(e){if(e===void 0)return ea;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 Xf={name:"query",mcpName:"db_sql_execute",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 jn(n[0],f(t,"file")),r=zf(f(t,"limit")),{rows:i,connection:a}=await da(await z(e),s,{db:f(t,"db"),confirmWrite:t["confirm-write"]===!0});if(a.note)T(`# ${a.note}`);let c=gi(i,r);k(X(c.rows,o)),T(c.truncated?`# rows: ${c.rows.length} of ${c.total} (truncated; use --limit 0 or TOP/WHERE)`:`# rows: ${c.total}`)}},Yf={name:"exec",mcpName:"db_sql_execute_nonquery",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 jn(n[0],f(t,"file")),{result:r,connection:i,notes:a}=await pa(await z(e),s,{db:f(t,"db"),confirmWrite:t["confirm-write"]===!0}),c=`${i.projectId}/${i.alias}`;if(i.note)T(`# ${i.note}`);let l=Qt(r);k(o?lr({db:c,result:l}):`ok ${c} ${JSON.stringify(l)}
361
- `);for(let u of a)T(`# ${u}`)}},vu=[Xf,Yf];function Fs(e){if(e===void 0)return;let t=br.find((n)=>n.toLowerCase()===e.toLowerCase());if(!t)throw new h(`--type must be one of: ${br.join(", ")}`);return t}var Qf={name:"schema",mcpName:"db_schema_read",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 sa(await z(e),{db:f(e.values,"db"),objectType:Fs(f(e.values,"type")),search:f(e.values,"search")});for(let n of t.notes)T(`# ${n}`);k(t.data??"")}},Zf={name:"schema pull",mcpName:"db_schema_get",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 ra(await z(e),f(e.values,"db"),e.values["prune-local-only"]===!0);k(`${t.notes.join(`
363
+ `)}return n}},Bu=[Vg,Wg,qg,Bg];function Jg(e){if(e===void 0)return va;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 Kg={name:"query",mcpName:"db_sql_execute",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 An(n[0],f(t,"file")),r=Jg(f(t,"limit")),{rows:i,connection:a}=await Aa(await X(e),s,{db:f(t,"db"),confirmWrite:t["confirm-write"]===!0});if(a.note)T(`# ${a.note}`);let c=Mi(i,r);k(Y(c.rows,o)),T(c.truncated?`# rows: ${c.rows.length} of ${c.total} (truncated; use --limit 0 or TOP/WHERE)`:`# rows: ${c.total}`)}},Gg={name:"exec",mcpName:"db_sql_execute_nonquery",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 An(n[0],f(t,"file")),{result:r,connection:i,notes:a}=await Na(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=an(r);k(o?jr({db:c,result:l}):`ok ${c} ${JSON.stringify(l)}
364
+ `);for(let u of a)T(`# ${u}`)}},Ju=[Kg,Gg];function ti(e){if(e===void 0)return;let t=Dr.find((n)=>n.toLowerCase()===e.toLowerCase());if(!t)throw new m(`--type must be one of: ${Dr.join(", ")}`);return t}var zg={name:"schema",mcpName:"db_schema_read",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 Ca(await X(e),{db:f(e.values,"db"),objectType:ti(f(e.values,"type")),search:f(e.values,"search")});for(let n of t.notes)T(`# ${n}`);k(t.data??"")}},Xg={name:"schema pull",mcpName:"db_schema_get",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 Ia(await X(e),f(e.values,"db"),e.values["prune-local-only"]===!0);k(`${t.notes.join(`
362
365
  `)}
363
- `)}},Su=[Qf,Zf];import{join as ju}from"path";var Yo="---\nname: fui\ndescription: Use the `fui` CLI for FUI (low-code platform) work \u2014 projects, modules (get, validate, preview, publish), components, imports, project users, DB connections, SQL, schema, stored procedures (tAPI spAPI_*), workspace files, publish ledger and history. Prefer it over Fui MCP tools whenever `fui` covers the tool (see the map below); same guards, a fraction of the tokens.\n---\n\n# fui CLI\n\n`fui` is the shell replacement for Fui MCP tools. Run it with your shell tool. It uses the same workspace, connection files, publish ledger, `_history` and error log as Fui MCP, so you can mix the two during the transition.\n\nDomain rules (module.json, controls, tAPI conventions) live in the `fui-skill` skill. Read its `references/` as needed, then run `fui skill loaded`; strict mode (`FUI_MCP_STRICT=1`) blocks deploys until you do.\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 <alias>` or `--db <project>/<alias>`.\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. They inherit from the Fui MCP entry in `~/.claude.json`.\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## Fui MCP tool \u2192 fui command\n\nImplemented (same guards, same files, same requests as Fui MCP 1.4.0):\n\n| MCP tool | fui |\n| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| session_set / session_read / session_clear / session_resolve | `use <target>` / `use` / `use --clear` / `resolve <target>` |\n| project_list / project_sync | `project list` / `project sync` |\n| project_new / project_update / project_publish_config | `project new --data {\u2026}` / `project update --data {id:\u2026}` / `project publish-config` |\n| project_user_search / list / new / update / delete | `user search <text>` / `user list` / `user add <UserID> --right N` / `user set \u2026` / `user remove <UserID>` |\n| module_list / new / update / delete | `module list` / `module new <id> --name N` / `module update [--new-id] [--framework] \u2026` / `module delete -p P -m M` |\n| module_get / module_get_ui / module_get_html | `module get [<target>]` / `module get-ui` / `module get-html` |\n| module_validate / module_preview | `module validate` / `module preview` |\n| module_outline / module_simulate | `module outline` / `module simulate [--scenario [...]] [--mocks {...}] [--user {...}] [--url-params {...}]` |\n| module_simulate renderUI / component_preview | `module simulate --render [--device mobile] [--vue-data {...}]` / `component preview <uc-name> [--cases [...]] [--device desktop,mobile]` |\n| error_log_read / error_log_clear | `log read [--tool T] [--limit N\\|0] [--summary]` / `log clear` |\n| module_publish_html / json / script / css | `module publish html --approval <token>` / `module publish json` / `\u2026 script` / `\u2026 css` |\n| component_list / get / new / update / delete | `component list` / `component get <name>` / `component new <name>` / `component update <name>` / `component delete <name>` |\n| file_import_list / get / new / update / upload_content / delete | `import list` / `import get <id>` / `import new \u2026` / `import update <id> \u2026` / `import upload <id> -f file` / `import delete <id>` |\n| db_config_read / db_connect | `db list` / `db add <alias> --token - --api-domain host` |\n| db_connect_by_name / db_alias_list / db_alias_new | `db add-by-name <alias>` / `db alias list [--search s]` / `db alias new <alias> \u2026` |\n| db_user_token_set / db_token_rebuild | `db user-token [--from p/alias]` / `db token-rebuild --uid U --pwd-env VAR` |\n| right_system_* / right_function_* | `right system list\\|new <0-9>\\|update <0-9>\\|delete <0-9>` / `right function list\\|new --code N\\|update <id>\\|delete <id>` |\n| db_schema_get / db_schema_read | `schema pull [--prune-local-only]` / `schema [--type T] [--search text]` |\n| db_sp_list / db_sp_get / db_sp_save | `sp list` / `sp get <name> [--print]` / `sp save <name> -f file` |\n| db_sp_deploy / db_sp_help | `sp deploy <name>` / `sp help <name\\|Fn> [--full]` |\n| db_sp_verify / db_sp_delete / db_sp_rename | `sp verify <name\\|Fn> [--params {\u2026}] [--check-grant]` / `sp delete <name> --type Procedure\\|Function` / `sp rename <old> <new>` |\n| db_sql_execute / db_sql_execute_nonquery | `query \"<sql>\"` / `exec \"<sql>\"` (+ `--confirm-write`) |\n| skill_get / skill_reference_get / skill_sync | `skill get` / `skill ref <path...>` / `skill sync` |\n| design_list / design_read | `design list` / `design read <brand>` |\n\nEvery Fui MCP 1.4.0 tool has a fui command (the SSE-only `workspace_*` and `*_stage` tools are out of scope).\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:\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: `bun add -g playwright && bunx playwright install chromium`.\n4. Publish:\n - **Whole module, including header/body:** `fui module preview` \u2192 show the changes to the user \u2192 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|\u2026` 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## 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 PG_URL # postgres:// mysql:// sqlite:// connect directly\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`, `_db/config.json` or MCP config files. 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 <alias>`: it borrows the SQL login of a connection on the same apiDomain.\n- `right *` and `db alias new` write to the shared acc system. Ask the user first, especially for `delete`.\n- `fui db list` shows a pre-1.4 `_db/config.json` as `engine=legacy`. Migrate it with `fui db import-legacy -p <project>`.\n\n## FUI conventions\n\n- tAPI routes:\n - `spAPI_<Fn>` \u2192 `/<alias>/<Fn>`\n - `spAPI_AUTH_<Fn>` \u2192 `/<alias>/auth/<Fn>`\n - `spAPIFILE_*` \u2192 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{cp as tg,mkdir as ng,rm as og}from"fs/promises";import{homedir as rg}from"os";import{join as Qe}from"path";var eo=["claude","codex","antigravity"];function bn(e){return e.HOME||rg()}function sg(e,t=Bun.env){if(e==="claude")return Qe(t.CLAUDE_CONFIG_DIR||Qe(bn(t),".claude"),"skills");if(e==="codex")return Qe(bn(t),".agents","skills");return Qe(bn(t),".gemini","antigravity","skills")}async function Ls(e){try{return(await Bun.file(e).stat()).isDirectory()}catch{return!1}}async function Us(e=Bun.env){let t=[];if(await Ls(e.CLAUDE_CONFIG_DIR||Qe(bn(e),".claude")))t.push("claude");if(await Ls(e.CODEX_HOME||Qe(bn(e),".codex")))t.push("codex");if(await Ls(Qe(bn(e),".gemini","antigravity")))t.push("antigravity");return t}async function Hs(e){await Bun.write(Qe(e,"fui","SKILL.md"),Yo);let t=Qe(e,vt);return await og(t,{recursive:!0,force:!0}),await ng(e,{recursive:!0}),await tg(await Ot(),t,{recursive:!0}),[Qe(e,"fui"),t]}async function Bs(e,t=Bun.env){let n=[];for(let i of e)n.push(...await Hs(sg(i,t)));let o=await Ln(t),s=o.skillsVersion===ue?o.skillsAgents??[]:[],r=eo.filter((i)=>e.includes(i)||s.includes(i));return await at({...o,skillsVersion:ue,skillsAgents:r},t),n}async function $u(e=Bun.env,t=Bun.main){if(!Un(t))return null;let n=e.FUI_AUTO_SKILLS?.trim().toLowerCase();if(n&&["0","off","false","no"].includes(n))return null;let o=await Ln(e),s=o.skillsVersion===ue?o.skillsAgents??[]:[],r=(await Us(e)).filter((i)=>!s.includes(i));if(r.length===0)return null;return await Bs(r,e),`# fui ${ue} skills installed for ${r.join(", ")}`}async function Iu(e){if(e.targets.cwdTarget())return e.paths.root;return(await e.session.getTarget())?.workspaceRoot}var ig={name:"skill",usage:"fui skill",summary:"Print the fui CLI agent guide",async run(){k(Yo)}},ag={name:"skill get",mcpName:"skill_get",usage:"fui skill get",summary:"Print the FUI domain skill (SKILL.md + Tier-A refs with an active target, else the summary); marks it loaded",async run(e){let t=await ec(await Iu(e));await e.session.markSkillLoaded(),T(`# source: ${t.source}; more: fui skill ref <references/x.md ...>`),k(`${t.text}
364
- `)}},cg={name:"skill ref",mcpName:"skill_reference_get",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();let t=await Iu(e),n=t?ju(t,"_skills",vt):await Ot();T(`# skill dir: ${n}`),k(`${(await tc(n,e.positionals)).trimStart()}
365
- `)}},lg={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
366
- `)}},ug={name:"skill sync",mcpName:"skill_sync",usage:"fui skill sync",summary:"Re-copy the bundled fui-skill into <workspace>/_skills/",async run({paths:e}){let t=await To(e.root);k(`${t.skillDir} (${t.source})
367
- `)}},dg={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); 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 Hs(t)).join(`
366
+ `)}},Ku=[zg,Xg];import{join as Zu}from"node:path";var lr="---\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`, pick the reference files for your task from `references/INDEX.md`, and load them with `fui skill ref references/<file>.md ...`. 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 validate` (fix every error) → `module outline` → `module simulate` → `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 <alias>` or `--db <project>/<alias>`.\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`), `fui config set`, then a Fui MCP entry in `~/.claude.json` if one exists.\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:\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`, `_db/config.json` or MCP config files. 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 <alias>`: it borrows the SQL login of a connection on the same apiDomain.\n- `right *` and `db alias new` write to the shared acc system. Ask the user first, especially for `delete`.\n- `fui db list` shows a pre-1.4 `_db/config.json` as `engine=legacy`. Migrate it with `fui db import-legacy -p <project>`.\n\n## FUI conventions\n\n- tAPI routes:\n - `spAPI_<Fn>` → `/<alias>/<Fn>`\n - `spAPI_AUTH_<Fn>` → `/<alias>/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\n## Coming from Fui MCP\n\n`fui` shares the workspace, connection files, publish ledger, `_history` and error log with Fui MCP, so the two can run side by side. Each Fui MCP 1.4.0 tool maps to a command with the same guards, files and requests:\n\n| MCP tool | fui |\n| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| session_set / session_read / session_clear / session_resolve | `use <target>` / `use` / `use --clear` / `resolve <target>` |\n| project_list / project_sync | `project list` / `project sync` |\n| project_new / project_update / project_publish_config | `project new --data {…}` / `project update --data {id:…}` / `project publish-config` |\n| project_user_search / list / new / update / delete | `user search <text>` / `user list` / `user add <UserID> --right N` / `user set …` / `user remove <UserID>` |\n| module_list / new / update / delete | `module list` / `module new <id> --name N` / `module update [--new-id] [--framework] …` / `module delete -p P -m M` |\n| module_get / module_get_ui / module_get_html | `module get [<target>]` / `module get-ui` / `module get-html` |\n| module_validate / module_preview | `module validate` / `module preview` |\n| module_outline / module_simulate | `module outline` / `module simulate [--scenario [...]] [--mocks {...}] [--user {...}] [--url-params {...}]` |\n| module_simulate renderUI / component_preview | `module simulate --render [--device mobile] [--vue-data {...}]` / `component preview <uc-name> [--cases [...]] [--device desktop,mobile]` |\n| error_log_read / error_log_clear | `log read [--tool T] [--limit N\\|0] [--summary]` / `log clear` |\n| module_publish_html / json / script / css | `module publish html --approval <token>` / `module publish json` / `… script` / `… css` |\n| component_list / get / new / update / delete | `component list` / `component get <name>` / `component new <name>` / `component update <name>` / `component delete <name>` |\n| file_import_list / get / new / update / upload_content / delete | `import list` / `import get <id>` / `import new …` / `import update <id> …` / `import upload <id> -f file` / `import delete <id>` |\n| db_config_read / db_connect | `db list` / `db add <alias> --token - --api-domain host` |\n| db_connect_by_name / db_alias_list / db_alias_new | `db add-by-name <alias>` / `db alias list [--search s]` / `db alias new <alias> …` |\n| db_user_token_set / db_token_rebuild | `db user-token [--from p/alias]` / `db token-rebuild --uid U --pwd-env VAR` |\n| right_system_* / right_function_* | `right system list\\|new <0-9>\\|update <0-9>\\|delete <0-9>` / `right function list\\|new --code N\\|update <id>\\|delete <id>` |\n| db_schema_get / db_schema_read | `schema pull [--prune-local-only]` / `schema [--type T] [--search text]` |\n| db_sp_list / db_sp_get / db_sp_save | `sp list` / `sp get <name> [--print]` / `sp save <name> -f file` |\n| db_sp_deploy / db_sp_help | `sp deploy <name>` / `sp help <name\\|Fn> [--full]` |\n| db_sp_verify / db_sp_delete / db_sp_rename | `sp verify <name\\|Fn> [--params {…}] [--check-grant]` / `sp delete <name> --type Procedure\\|Function` / `sp rename <old> <new>` |\n| db_sql_execute / db_sql_execute_nonquery | `query \"<sql>\"` / `exec \"<sql>\"` (+ `--confirm-write`) |\n| skill_get / skill_reference_get / skill_sync | `skill get` / `skill ref <path...>` / `skill sync` |\n| design_list / design_read | `design list` / `design read <brand>` |\n| resources lowcode://library/v2/\\* / prompt generate-vue-component | `library <name>` / the Components section above |\n\nEvery Fui MCP 1.4.0 tool has a fui command (the SSE-only `workspace_*` and `*_stage` tools are out of scope).\n";var zu={session_set:"use",session_read:"use",session_clear:"use --clear",session_resolve:"resolve",project_list:"project list",project_sync:"project sync",project_new:"project new",project_update:"project update",project_publish_config:"project publish-config",project_user_search:"user search",project_user_list:"user list",project_user_new:"user add",project_user_update:"user set",project_user_delete:"user remove",module_list:"module list",module_new:"module new",module_update:"module update",module_delete:"module delete",module_get:"module get",module_get_ui:"module get-ui",module_get_html:"module get-html",module_validate:"module validate",module_preview:"module preview",module_outline:"module outline",module_simulate:"module simulate",module_publish_html:"module publish html",module_publish_json:"module publish json",module_publish_script:"module publish script",module_publish_css:"module publish css",component_list:"component list",component_get:"component get",component_new:"component new",component_update:"component update",component_delete:"component delete",component_preview:"component preview",file_import_list:"import list",file_import_get:"import get",file_import_new:"import new",file_import_update:"import update",file_import_upload_content:"import upload",file_import_delete:"import delete",error_log_read:"log read",error_log_clear:"log clear",db_config_read:"db list",db_connect:"db add",db_connect_by_name:"db add-by-name",db_alias_list:"db alias list",db_alias_new:"db alias new",db_user_token_set:"db user-token",db_token_rebuild:"db token-rebuild",right_system_list:"right system list",right_system_new:"right system new",right_system_update:"right system update",right_system_delete:"right system delete",right_function_list:"right function list",right_function_new:"right function new",right_function_update:"right function update",right_function_delete:"right function delete",db_schema_get:"schema pull",db_schema_read:"schema",db_sp_list:"sp list",db_sp_get:"sp get",db_sp_save:"sp save",db_sp_deploy:"sp deploy",db_sp_help:"sp help",db_sp_verify:"sp verify",db_sp_delete:"sp delete",db_sp_rename:"sp rename",db_sql_execute:"query",db_sql_execute_nonquery:"exec",skill_get:"skill get",skill_reference_get:"skill ref",skill_sync:"skill sync",design_list:"design list",design_read:"design read"},Xu={project_user_:"user",module_publish_:"module publish",right_system_:"right system",right_function_:"right function",file_import_:"import"},Qg=new RegExp(`\\b(${Object.keys(zu).sort((e,t)=>t.length-e.length).join("|")})\\b(?!\\*)`,"g"),Zg=new RegExp(`\\b(${Object.keys(Xu).join("|")})\\*`,"g");function Yu(e){return e.replace(Zg,(t,n)=>`fui ${Xu[n]} *`).replace(Qg,(t,n)=>`fui ${zu[n]}`).replace(/\bFUI MCP\b/g,()=>"fui").replace(/\bMCP (tool)(s?)\b/g,(t,n,o)=>`fui command${o}`).replace(/\bMCP\b/g,()=>"fui")}var Gu="> For the `fui` CLI: each tool below is the `fui` command named in it, with the same guards. fui is the STDIO/CLI mode, so the SSE-only search and staging tools do not exist; use your own file tools. `fui --help` lists every command.";function ur(e,t){let n=e.replaceAll("\\","/");if(n==="SKILL.md")return ni(t);let o=Yu(t);if(n!=="references/tools-registry.md")return o;let s=o.indexOf(`
367
+ `);return s<0?`${Gu}
368
+
369
+ ${o}`:`${o.slice(0,s+1)}
370
+ ${Gu}
371
+ ${o.slice(s+1)}`}function ni(e){let t=Yu(e),n=t.startsWith(`---
372
+ `)?t.indexOf(`
373
+ ---
374
+ `,4):-1;if(n<0)return`> From the \`fui\` CLI: tool names below are \`fui\` commands (run them in the shell). Object arguments such as \`({ name: "X" })\` become positionals or flags; \`fui <command> --help\` shows them.
375
+
376
+ ${t}`;let o=n+5;return`${t.slice(0,o)}
377
+ > From the \`fui\` CLI: tool names below are \`fui\` commands (run them in the shell). Object arguments such as \`({ name: "X" })\` become positionals or flags; \`fui <command> --help\` shows them.
378
+ ${t.slice(o)}`}import{cp as ew,mkdir as tw,rm as nw}from"node:fs/promises";import{homedir as ow}from"node:os";import{join as Je}from"node:path";var io=["claude","codex","antigravity"];function Cn(e){return e.HOME||ow()}function rw(e,t=process.env){if(e==="claude")return Je(t.CLAUDE_CONFIG_DIR||Je(Cn(t),".claude"),"skills");if(e==="codex")return Je(Cn(t),".agents","skills");return Je(Cn(t),".gemini","antigravity","skills")}async function oi(e){try{return await pr(e)}catch{return!1}}async function ri(e=process.env){let t=[];if(await oi(e.CLAUDE_CONFIG_DIR||Je(Cn(e),".claude")))t.push("claude");if(await oi(e.CODEX_HOME||Je(Cn(e),".codex")))t.push("codex");if(await oi(Je(Cn(e),".gemini","antigravity")))t.push("antigravity");return t}async function si(e){await x(Je(e,"fui","SKILL.md"),lr);let t=Je(e,Ct);await nw(t,{recursive:!0,force:!0}),await tw(e,{recursive:!0}),await ew(await Rt(),t,{recursive:!0});for await(let n of Yt("**/*.md",t)){let o=Je(t,n),s=await Qe(o);await x(o,ur(n,s))}return[Je(e,"fui"),t]}async function ii(e,t=process.env){let n=[];for(let i of e)n.push(...await si(rw(i,t)));let o=await Jn(t),s=o.skillsVersion===de?o.skillsAgents??[]:[],r=io.filter((i)=>e.includes(i)||s.includes(i));return await ut({...o,skillsVersion:de,skillsAgents:r},t),n}async function Qu(e=process.env,t=gt()){if(!Ao(t))return null;let n=e.FUI_AUTO_SKILLS?.trim().toLowerCase();if(n&&["0","off","false","no"].includes(n))return null;let o=await Jn(e),s=o.skillsVersion===de?o.skillsAgents??[]:[],r=(await ri(e)).filter((i)=>!s.includes(i));if(r.length===0)return null;return await ii(r,e),`# fui ${de} skills installed for ${r.join(", ")}`}async function ed(e){if(e.targets.cwdTarget())return e.paths.root;return(await e.session.getTarget())?.workspaceRoot}var sw={name:"skill",usage:"fui skill",summary:"Print the fui CLI agent guide",async run(){k(lr)}},iw={name:"skill get",mcpName:"skill_get",usage:"fui skill get",summary:"Print the FUI domain skill (SKILL.md + Tier-A refs with an active target, else the summary); marks it loaded",async run(e){let t=await $c(await ed(e));await e.session.markSkillLoaded(),T(`# source: ${t.source}; more: fui skill ref <references/x.md ...>`),k(`${t.source==="bundled-summary"?t.text:ni(t.text)}
379
+ `)}},aw={name:"skill ref",mcpName:"skill_reference_get",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();let t=await ed(e),n=t?Zu(t,"_skills",Ct):await Rt();T(`# skill dir: ${n}`);let o=await Promise.all(e.positionals.map(async(s)=>{let r=await jc(n,[s]),i=`
380
+
381
+ ---
382
+ <!-- ${s} -->
383
+
384
+ `;return r.startsWith(i)?i+ur(s,r.slice(i.length)):r}));k(`${o.join("").trimStart()}
385
+ `)}},cw={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
386
+ `)}},lw={name:"skill sync",mcpName:"skill_sync",usage:"fui skill sync",summary:"Re-copy the bundled fui-skill into <workspace>/_skills/",async run({paths:e}){let t=await Uo(e.root);k(`${t.skillDir} (${t.source})
387
+ `)}},uw={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); 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 si(t)).join(`
368
388
  `)}
369
- `);return}let n=f(e,"agent"),o=n==="all"?[...eo]:n?n.split(",").map((s)=>s.trim()):await Us();if(o.length===0)throw new h("no agent found (~/.claude, ~/.codex, ~/.gemini/antigravity); pass --agent or --dir");for(let s of o)if(!eo.includes(s))throw new h(`--agent takes ${eo.join(", ")} or all`);k(`${(await Bs(o)).join(`
389
+ `);return}let n=f(e,"agent"),o=n==="all"?[...io]:n?n.split(",").map((s)=>s.trim()):await ri();if(o.length===0)throw new m("no agent found (~/.claude, ~/.codex, ~/.gemini/antigravity); pass --agent or --dir");for(let s of o)if(!io.includes(s))throw new m(`--agent takes ${io.join(", ")} or all`);k(`${(await ii(o)).join(`
370
390
  `)}
371
- `)}},pg={name:"design list",mcpName:"design_list",usage:"fui design list",summary:"List bundled design systems (brands) for fui design read",async run({render:e}){let t=await is();T(`# ${t.length} brands`),k(e.json?`${JSON.stringify(t)}
391
+ `)}},dw={name:"design list",mcpName:"design_list",usage:"fui design list",summary:"List bundled design systems (brands) for fui design read",async run({render:e}){let t=await Ss();T(`# ${t.length} brands`),k(e.json?`${JSON.stringify(t)}
372
392
  `:`${t.join(`
373
393
  `)}
374
- `)}},mg={name:"design read",mcpName:"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 is();if(!n.includes(t))throw new h(`unknown brand "${t}"; available: ${n.join(", ")}`);let o=ju(await Ot(),"design-md",t,"DESIGN.md");T(`# source: ${o}`),k(await B(o))}},Cu=[ig,ag,cg,lg,ug,dg,pg,mg];function Bt(e){let t=e[0];if(!t)throw new h("missing <name>");return t}var hg={name:"sp list",mcpName:"db_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 ia(await z(e),{db:f(e.values,"db"),objectType:Fs(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(X(s,e.render))}},fg={name:"sp get",mcpName:"db_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 aa(await z(e),Bt(e.positionals),f(e.values,"db"));for(let o of t.notes)T(`# ${o}`);let n=f(e.values,"out");if(n)await Bun.write(n,t.body),k(`${n}
394
+ `)}},pw={name:"design read",mcpName:"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 Ss();if(!n.includes(t))throw new m(`unknown brand "${t}"; available: ${n.join(", ")}`);let o=Zu(await Rt(),"design-md",t,"DESIGN.md");T(`# source: ${o}`),k(await W(o))}},td=[sw,iw,aw,cw,lw,uw,dw,pw];function Gt(e){let t=e[0];if(!t)throw new m("missing <name>");return t}var mw={name:"sp list",mcpName:"db_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 Ra(await X(e),{db:f(e.values,"db"),objectType:ti(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(Y(s,e.render))}},hw={name:"sp get",mcpName:"db_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 Ta(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 x(n,t.body),k(`${n}
375
395
  `);else k(e.values.print===!0?t.body:`${t.path}
376
- `)}},gg={name:"sp save",mcpName:"db_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=Bt(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 jn(n??o,f(e.values,"file")),r=await ca(await z(e),t,s,f(e.values,"db"));for(let i of r.notes)T(`# ${i}`);k(`${r.path}
396
+ `)}},fw={name:"sp save",mcpName:"db_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 An(n??o,f(e.values,"file")),r=await Pa(await X(e),t,s,f(e.values,"db"));for(let i of r.notes)T(`# ${i}`);k(`${r.path}
377
397
  next: fui sp deploy ${t} --db ${r.alias}
378
- `)}},wg={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=ft(Bt(e.positionals)),n=await z(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)}
379
- `)}},yg={name:"sp deploy",mcpName:"db_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 la(await z(e),Bt(e.positionals),f(e.values,"db"));k(`${t.notes.join(`
398
+ `)}},gw={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)}
399
+ `)}},ww={name:"sp deploy",mcpName:"db_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 Ea(await X(e),Gt(e.positionals),f(e.values,"db"));k(`${t.notes.join(`
380
400
  `)}
381
- `)}},bg={name:"sp help",mcpName:"db_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 ua(await z(e),Bt(e.positionals),f(e.values,"db"),e.values.full===!0?0:ta);for(let n of t.notes)T(`# ${n}`);k(t.data??"")}};function kg(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 vg={name:"sp verify",mcpName:"db_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 ma(await z(e),{name:Bt(e.positionals),db:f(e.values,"db"),params:kg(f(e.values,"params")),urlParams:Array.isArray(t)?t.map(String):void 0,checkGrant:e.values["check-grant"]===!0});k(`${n.notes.join(`
401
+ `)}},yw={name:"sp help",mcpName:"db_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 _a(await X(e),Gt(e.positionals),f(e.values,"db"),e.values.full===!0?0:Sa);for(let n of t.notes)T(`# ${n}`);k(t.data??"")}};function bw(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 kw={name:"sp verify",mcpName:"db_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 Da(await X(e),{name:Gt(e.positionals),db:f(e.values,"db"),params:bw(f(e.values,"params")),urlParams:Array.isArray(t)?t.map(String):void 0,checkGrant:e.values["check-grant"]===!0});k(`${n.notes.join(`
382
402
  `)}
383
- `)}},Sg={name:"sp delete",mcpName:"db_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 ha(await z(e),Bt(e.positionals),t,f(e.values,"db"));k(`${n.notes.join(`
403
+ `)}},vw={name:"sp delete",mcpName:"db_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 xa(await X(e),Gt(e.positionals),t,f(e.values,"db"));k(`${n.notes.join(`
384
404
  `)}
385
- `)}},$g={name:"sp rename",mcpName:"db_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 fa(await z(e),t,n,f(e.values,"db"));k(`${o.notes.join(`
405
+ `)}},Sw={name:"sp rename",mcpName:"db_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 Oa(await X(e),t,n,f(e.values,"db"));k(`${o.notes.join(`
386
406
  `)}
387
- `)}},Ru=[hg,fg,gg,wg,yg,bg,vg,Sg,$g];var jg={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}`,...uo(o.dbToken)};k(X([s],n))}},Ig={name:"token encode",usage:"fui token encode --server S --database D --uid U --pwd-env VAR",summary:"Print a Fui MSSQL dbToken (Base64) built like Fui MCP",options:{server:{type:"string"},database:{type:"string"},uid:{type:"string"},"pwd-env":{type:"string"}},async run({values:e}){let t=f(e,"server"),n=f(e,"database"),o=f(e,"uid"),s=await Pe(void 0,f(e,"pwd-env"),"--pwd");if(!t||!n||!o||!s)throw new h("need --server, --database, --uid and --pwd-env");k(`${Kt(t,n,o,s)}
388
- `)}},Pu=[jg,Ig];var Cg={name:"update",usage:"fui update [--check]",summary:"Update fui to the latest npm release via bun add -g (--check only reports)",options:{check:{type:"boolean"},background:{type:"boolean"}},async run({values:e}){if(e.background===!0){await Vr().catch(()=>{return});return}if(e.check===!0){let n=await Br();k(qr(n)?`update available: ${ue} -> ${n}; run: fui update
389
- `:`fui ${ue} is current (latest ${n})
390
- `);return}let t=await Vr();if(t.action==="current")k(`fui ${ue} is current
391
- `);else if(t.action==="installed")k(`updated ${ue} -> ${t.latest}; skills refreshed for Claude, Codex and Antigravity (those found)
392
- `);else if(t.action==="manual")T(`# ${Bun.main} is not a bun global install (bun link checkout or compiled binary); it cannot replace itself`),k(`update available: ${ue} -> ${t.latest}; install: ${t.detail}
393
- `);else return T(t.detail??""),k(`update to ${t.latest} failed; retry: bun add -g ${Fn}@${t.latest}
394
- `),Oe.Runtime}},Tu=[Cg];var qs=[...xa,...Ta,...Pu,...vu,...Su,...Ru,...wc,...Jl,...uu,...ku,...Ma,...Cu,...wu,...Da,...Tu],Au=`global: -p/--project <id> --db <alias|project/alias> --workdir <dir> --json --no-header -h/--help -v/--version
407
+ `)}},nd=[mw,hw,fw,gw,ww,yw,kw,vw,Sw];var $w={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}`,...yo(o.dbToken)};k(Y([s],n))}},jw={name:"token encode",usage:"fui token encode --server S --database D --uid U --pwd-env VAR",summary:"Print a Fui MSSQL dbToken (Base64) built like Fui MCP",options:{server:{type:"string"},database:{type:"string"},uid:{type:"string"},"pwd-env":{type:"string"}},async run({values:e}){let t=f(e,"server"),n=f(e,"database"),o=f(e,"uid"),s=await Ee(void 0,f(e,"pwd-env"),"--pwd");if(!t||!n||!o||!s)throw new m("need --server, --database, --uid and --pwd-env");k(`${tn(t,n,o,s)}
408
+ `)}},od=[$w,jw];var Iw={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 is().catch(()=>{return});return}if(e.check===!0){let n=await rs();k(ss(n)?`update available: ${de} -> ${n}; run: fui update
409
+ `:`fui ${de} is current (latest ${n})
410
+ `);return}let t=await is();if(t.action==="current")k(`fui ${de} is current
411
+ `);else if(t.action==="installed")k(`updated ${de} -> ${t.latest}; skills refreshed for Claude, Codex and Antigravity (those found)
412
+ `);else if(t.action==="manual")T(`# ${gt()} is not a global bun or npm install (a checkout); it cannot replace itself`),k(`update available: ${de} -> ${t.latest}; install: ${t.detail}
413
+ `);else return T(t.detail??""),k(`update to ${t.latest} failed; retry: ${Eo(_o()?.manager??"bun",t.latest)}
414
+ `),Me.Runtime}},rd=[Iw];var ai=[...nc,...Xa,...od,...Ju,...Ku,...nd,...Uc,...fu,...Nu,...Bu,...rc,...td,...xu,...Vu,...tc,...rd],id=`global: -p/--project <id> --db <alias|project/alias> --workdir <dir> --json --no-header -h/--help -v/--version
395
415
  lists: --filter text --where col=value (repeatable) --fields a,b,c (client-side; the request is unchanged)
396
- exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked`;function Eu(){let e=Math.max(...qs.map((n)=>n.name.length)),t=qs.map((n)=>` ${n.name.padEnd(e)} ${n.summary}`);return`fui ${ue}: FUI platform CLI
416
+ exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked`;function sd(){let e=Math.max(...ai.map((n)=>n.name.length)),t=ai.map((n)=>` ${n.name.padEnd(e)} ${n.summary}`);return`fui ${de}: FUI platform CLI
397
417
 
398
418
  commands:
399
419
  ${t.join(`
400
420
  `)}
401
421
 
402
- ${Au}
422
+ ${id}
403
423
  run "fui skill" for the agent guide
404
- `}function Pg(e){return`${e.usage}
424
+ `}function Rw(e){return`${e.usage}
405
425
  ${e.summary}
406
- ${Au}
407
- `}var Tg=3;function Eg(e){let t=[];for(let n of e.slice(0,Tg)){if(n.startsWith("-"))break;t.push(n)}for(let n=t.length;n>0;n--){let o=t.slice(0,n).join(" "),s=qs.find((r)=>r.name===o);if(s)return{command:s,rest:e.slice(n)}}return null}async function Ag(e,t,n,o){if(n instanceof Se&&n.severity==="soft")return;let s=n instanceof Se?n.logKind:"error",r=n instanceof pt?n.report:`\u274C ${o}`;await pu(e,t.mcpName??`fui ${t.name}`,r,s)}async function Ng(e){let t=Eg(e);if(!t){let r=e[0];if(!r||r==="-h"||r==="--help"||r==="help")return k(Eu()),Oe.Ok;if(r==="-v"||r==="--version")return k(`${ue}
408
- `),Oe.Ok;return T(`unknown command: ${e.slice(0,2).join(" ")}
409
- `),T(Eu()),Oe.Usage}let{command:n,rest:o}=t,s;try{let r;try{r=Rg({args:o,options:{...pi,...n.options},allowPositionals:!0,strict:!0})}catch(i){throw new h(`${i.message}
410
- usage: ${n.usage}`)}if(r.values.help===!0)return k(Pg(n)),Oe.Ok;return s=await mi(r.values,r.positionals),await n.run(s)??Oe.Ok}catch(r){let i=Ks(r),a=i===Oe.Guard?"guard":"error",c=r.message;if(T(`${a}: ${c}`),s)await Ag(s.paths.root,n,r,c);return i}}if(import.meta.main){let e=await Na().catch(()=>null);if(e)T(e);let t=await $u().catch(()=>null);if(t)T(t);process.exitCode=await Ng(Bun.argv.slice(2))}export{Ng as main};
426
+ ${id}
427
+ `}var Tw=3;function Pw(e){let t=[];for(let n of e.slice(0,Tw)){if(n.startsWith("-"))break;t.push(n)}for(let n=t.length;n>0;n--){let o=t.slice(0,n).join(" "),s=ai.find((r)=>r.name===o);if(s)return{command:s,rest:e.slice(n)}}return null}async function Ew(e,t,n,o){if(n instanceof $e&&n.severity==="soft")return;let s=n instanceof $e?n.logKind:"error",r=n instanceof ft?n.report:`❌ ${o}`;await Mu(e,t.mcpName??`fui ${t.name}`,r,s)}async function _w(e){let t=Pw(e);if(!t){let r=e[0];if(!r||r==="-h"||r==="--help"||r==="help")return k(sd()),Me.Ok;if(r==="-v"||r==="--version")return k(`${de}
428
+ `),Me.Ok;return T(`unknown command: ${e.slice(0,2).join(" ")}
429
+ `),T(sd()),Me.Usage}let{command:n,rest:o}=t,s;try{let r;try{r=Cw({args:o,options:{...Ni,...n.options},allowPositionals:!0,strict:!0})}catch(i){throw new m(`${i.message}
430
+ usage: ${n.usage}`)}if(r.values.help===!0)return k(Rw(n)),Me.Ok;return s=await Di(r.values,r.positionals),await n.run(s)??Me.Ok}catch(r){let i=di(r),a=i===Me.Guard?"guard":"error",c=r.message;if(T(`${a}: ${c}`),s)await Ew(s.paths.root,n,r,c);return i}}async function ad(e=process.argv.slice(2)){let t=await Za().catch(()=>null);if(t)T(t);let n=await Qu().catch(()=>null);if(n)T(n);process.exitCode=await _w(e)}await ad();