@fui-org/mlis-cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,7 +31,7 @@ Lần chạy đầu, bản đã build tự cài skill vào `~/.claude/skills/mli
31
31
  ## Cấu hình
32
32
 
33
33
  ```bash
34
- mlis token set # dán giá trị cookie jwtAuth (lấy trong DevTools → Application → Cookies)
34
+ mlis token set # dán mã ghép một lần lấy ở /admin → Thiết bị → "Tạo mã ghép cho mlis"
35
35
  mlis config set url https://lhu.edu.vn
36
36
  mlis config set cdn-cat 123 # CatID chứa hình/tài liệu cho CKFinder
37
37
  mlis config set site-src C:\APP\MLIS_2025 # mã nền tảng cho mlis render (mặc định nếu thư mục này có)
@@ -39,7 +39,7 @@ mlis config set render-port 5080 # cổng http://localhost của mlis rende
39
39
  mlis config # xem cấu hình (token chỉ hiện "(set)")
40
40
  ```
41
41
 
42
- Cấu hình nằm ở `~/.config/mlis/config.json`. Biến môi trường `MLIS_URL`, `MLIS_TOKEN`, `MLIS_WORKDIR`, `MLIS_CDN_CAT`, `MLIS_CDN_MODE`, `MLIS_SITE_SRC`, `MLIS_RENDER_PORT` ghi đè file. Token cần tài khoản có SystemRight ≥ 3; không truyền token làm tham số dòng lệnh.
42
+ Cấu hình nằm ở `~/.config/mlis/config.json`. Biến môi trường `MLIS_URL`, `MLIS_TOKEN`, `MLIS_WORKDIR`, `MLIS_CDN_CAT`, `MLIS_CDN_MODE`, `MLIS_SITE_SRC`, `MLIS_RENDER_PORT` ghi đè file. Đăng nhập là **phiên gắn với máy**: mã ghép chỉ dùng một lần; máy tạo khoá riêng `device-key.pem` (trong thư mục cấu hình, chỉ người dùng đọc được) và tự gia hạn token ngắn hạn bằng chữ ký, nên token bị chép sang máy khác không dùng được. Thu hồi một máy ở /admin → Thiết bị. Thiết kế ViewPage cần tài khoản có SystemRight ≥ 3; không truyền mã/token làm tham số dòng lệnh.
43
43
 
44
44
  ## Dùng hằng ngày
45
45
 
@@ -66,6 +66,7 @@ mlis push LHUENB4/@HomePage
66
66
  | `check [thư mục\|file] [--offline]` | kiểm tra trước khi đẩy: đường dẫn ViewPage hỏng, thẻ MLIS thiếu `runat`, `{`/`}` lạc trong format, `CodeFile`, `alt`, `<h1>`, thẻ SEO trùng |
67
67
  | `page <url> [--pull]` | URL của một trang (tên miền bất kỳ của máy chủ) → template trong ViewPage, các file LoadPage, HeaderOfSite, số thư mục dùng chung template; `--pull` tải chúng về |
68
68
  | `category sync\|tree\|ls\|show\|find\|pages` | cây thư mục (category) của site: CatID, kiểu, PageStyle/PageRead, Location1-3, tên miền chủ; thư mục nào dùng trang nào |
69
+ | `category pull\|diff\|push <CatID>`, `category new <ParentID>` | sửa mọi trường của form Thư mục (kiểu, tên, mô tả, hình, liên kết/tên miền, PageStyle/PageRead, vị trí, thứ tự, tuỳ chọn, thư mục cha) và Tài liệu HTML; file `_categories/<CatID>.html` |
69
70
  | `news ls <CatID>`, `news show\|pull\|diff\|push <NewsID>`, `news new <CatID>` | đọc/sửa bài viết (nội dung động của trang) qua API của trang quản trị Bài viết; file `_news/<NewsID>.html`; push dừng (exit 3) nếu bài đã bị sửa trên web |
70
71
  | `render shot <thư mục\|file.ascx> [--as home\|category\|read] [--device desktop\|mobile\|both] [--full] [-o dir]`, `render start <…>`, `render stop` | chạy trang trên máy (mã nền tảng từ `site-src` + dữ liệu mẫu, không ghi lên server) và chụp ảnh desktop/mobile vào `_mlis/shots/` |
71
72
  | `cdn info\|list\|upload\|mkdir\|rename\|url\|download\|rm\|rmdir` | tài liệu/hình ngoài ViewPage qua CKFinder |
@@ -80,6 +81,7 @@ Workspace: `<workdir>/…` giữ nguyên đường dẫn bên trong ViewPage, k
80
81
  - Đuôi khác (đặc biệt `.aspx`, `.ashx`, `.config`, `.dll`, `.html`) không được lưu vào ViewPage. Tài liệu bài viết (doc, xls, zip…) dùng `mlis cdn` (lưu ở `/Data/Folders/<CatID>/…`).
81
82
  - Không đổi tên file qua API; xoá thư mục chỉ khi rỗng.
82
83
  - `mlis news` dùng API của trang quản trị Bài viết: `GetNewsList` (trả dữ liệu), `GetNews`, `SaveNews` — cùng quy tắc kiểm tra với trang sửa bài (`App_Code\NewsEditor.vb`); cần `WebApiController.vb` và `NewsEditor.vb` bản mới.
84
+ - `mlis category pull/push/new` dùng `GetFolder`, `SaveFolder`, `GetFolderDoc`, `SaveFolderDoc` (cùng quy tắc với form Thư mục và trang Tài liệu HTML, `App_Code\FolderEditor.vb`) và `MoveFolder` có sẵn.
83
85
  - `mlis category` dùng API của trang quản trị thư mục (`/api/admin/GetAllFolder`, `GetFolderList`) — cần `WebApiController.vb` bản trả JSON (server cũ trả HTML, CLI báo cần deploy).
84
86
  - Server không có lịch sử phiên bản; mọi sao lưu nằm ở `_history/` trên máy chạy `mlis`.
85
87
  - Khách chưa đăng nhập có thể thấy trang cũ tới 60 giây (cache HTML của site).
package/dist/mlis.js CHANGED
@@ -1,86 +1,93 @@
1
1
  #!/usr/bin/env node
2
- var b=(e,t,r)=>()=>{if(e)try{t=e(e=0)}catch(n){r=[n]}if(r)throw r[0];return t};var Ae;var at=b(()=>{Ae={name:"@fui-org/mlis-cli",version:"0.6.0",description:"CLI thiết kế trang ascx (ViewPage) cho website MLIS qua API có sẵn, kèm render cục bộ và skill cho Claude Code",keywords:["mlis","ascx","viewpage","cli","bun","ai"],license:"MIT",repository:{type:"git",url:"git+https://github.com/fui-org/mlis-cli.git"},publishConfig:{access:"public"},bin:{mlis:"dist/mlis.js"},files:["dist","skills","render","README.md"],type:"module",scripts:{mlis:"bun src/bin.ts",build:"bun build src/bin.ts --target node --minify --outdir dist --entry-naming mlis.js","build:exe":"bun build src/bin.ts --compile --minify --outfile dist/mlis.exe",prepack:"bun run build",test:"bun test",typecheck:"tsc --noEmit"},devDependencies:{"@types/bun":"latest",typescript:"^5.6.0"},engines:{node:">=22"}}});function tr(e){if(e instanceof u)return j.Usage;if(e instanceof E)return j.Guard;return j.Runtime}var j,u,E;var x=b(()=>{j={Ok:0,Runtime:1,Usage:2,Guard:3};u=class u extends Error{name="UsageError"};E=class E extends Error{name="GuardError"}});class ve{baseUrl;token;request;constructor(e,t,r=fetch){this.baseUrl=e;this.token=t;this.request=r}async call(e,t={},r="viewpage"){let n=`${this.baseUrl}/api/${r}/${e}`,s=r==="admin"?"WebApiController.vb":"WebApiViewPageController.vb",o;try{o=await this.request(n,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",cookie:`jwtAuth=${this.token}`},body:JSON.stringify(t),signal:AbortSignal.timeout(ls)})}catch(l){throw Error(`cannot reach ${n}: ${l.message}`)}let i=await o.text(),c;try{c=JSON.parse(i)}catch{c=void 0}if(o.status===401||c?.Message==="NotAuthenticated")throw new u("server rejected the token (NotAuthenticated, or SystemRight < 3); run: mlis token set");if(!c){if(o.status===404)throw new M(`${e}: this server has no api/${r}/${e}; deploy the updated ${s}`,404,"");throw new M(`${e}: HTTP ${o.status}, not JSON: ${i.replace(/\s+/g," ").slice(0,160)}`,o.status,"")}let a=c.Message??"";if(o.status===404&&a.startsWith("No HTTP resource"))throw new M(`${e}: this server has no api/${r}/${e}; deploy the updated ${s}`,404,a);if(!o.ok||a!=="")throw new M(`${e}: ${a||`HTTP ${o.status}`}`,o.status,a);return{...c,Message:a}}async tree(){return(await this.call("GetDirectoryTree")).Data??[]}async folderTree(){return this.rows(await this.call("GetAllFolder",{},"admin"),"GetAllFolder")}async folderList(e){return this.rows(await this.call("GetFolderList",{CatID:e,IncludePageRead:!0},"admin"),"GetFolderList")}async newsList(e){let t={CatID:e.catId,NewsStatus:e.trash??!1,NewsImport:!1,FromDate:e.from??"",ToDate:e.to??"",Skip:e.skip??0,NumRows:e.limit??20,FS:e.search??""};return this.rows(await this.call("GetNewsList",t,"admin"),"GetNewsList")}async news(e){return(await this.call("GetNews",{NewsID:e},"admin")).Data}async saveNews(e){return(await this.call("SaveNews",{...e},"admin")).Data}rows(e,t){if(typeof e.Data==="string")throw new M(`${t}: this server still returns HTML; deploy the updated WebApiController.vb (admin APIs return data)`,200,"");return Array.isArray(e.Data)?e.Data:[]}async list(e){return(await this.call("GetFileList",{path:e,includeBinary:!0})).Data??[]}async download(e){return Buffer.from((await this.call("DownloadFile",{path:e})).Data??"","base64")}async upload(e,t,r,n){let s=Buffer.from(r).toString("base64");return this.call("UploadFile",{path:e,fileName:t,data:s,overwrite:n})}async read(e){let t=await this.call("ReadFile",{path:e});return{Content:t.Content??"",FileName:t.FileName,Extension:t.Extension}}async save(e,t){return(await this.call("SaveFile",{path:e,content:t})).SavedAt}async createFile(e,t){return(await this.call("CreateFile",{path:e,fileName:t})).Path}async deleteFile(e){await this.call("DeleteFile",{path:e})}async createDirectory(e,t){return(await this.call("CreateDirectory",{path:e,dirName:t})).Path}async deleteDirectory(e){await this.call("DeleteDirectory",{path:e})}}async function ee(e,t,r){let n=Array(e.length),s=0;async function o(){while(s<e.length){let i=s++;n[i]=await r(e[i])}}return await Promise.all(Array.from({length:Math.min(t,e.length)},o)),n}var ls=60000,M;var te=b(()=>{x();M=class M extends Error{status;serverMessage;name="ApiError";constructor(e,t,r){super(e);this.status=t;this.serverMessage=r}get notFound(){return this.status===404}}});import{existsSync as nr}from"node:fs";import{chmod as cs,mkdir as ds,readFile as us,rename as ms,writeFile as fs}from"node:fs/promises";import{homedir as sr}from"node:os";import{dirname as ps,join as _e}from"node:path";function lt(e=process.env){if(e.MLIS_CONFIG_DIR)return e.MLIS_CONFIG_DIR;let t=e.XDG_CONFIG_HOME||_e(e.HOME||e.USERPROFILE||sr(),".config");return _e(t,"mlis")}function le(e=process.env){return _e(lt(e),"config.json")}async function ke(e=process.env){try{let t=JSON.parse(await us(le(e),"utf-8"));return t&&typeof t==="object"?t:{}}catch(t){if(t.code==="ENOENT")return{};throw Error(`cannot read ${le(e)}: ${t.message}`)}}async function He(e,t=process.env){let r=le(t);await ds(ps(r),{recursive:!0});let n=`${r}.${process.pid}.tmp`;await fs(n,`${JSON.stringify(e,null,2)}
3
- `,{encoding:"utf-8",mode:384}),await ms(n,r),await cs(r,384).catch(()=>{return})}function hs(e){if(process.platform==="win32"&&nr("C:\\APP"))return"C:\\APP\\mlis-workspaces";return _e(e.HOME||e.USERPROFILE||sr(),"mlis-workspaces")}function ct(e){let t;try{t=new URL(/^https?:\/\//i.test(e)?e:`https://${e}`)}catch{throw new u(`invalid url ${JSON.stringify(e)}; expected e.g. https://lhu.edu.vn`)}if(t.username||t.password||t.search||t.hash)throw new u("url must be the site address only, e.g. https://lhu.edu.vn");return`${t.origin}${t.pathname.replace(/\/+$/,"")}`}function je(e,t){if(e==="url")return ct(t);if(e==="cdn-mode"&&t!=="folder"&&t!=="news")throw new u('cdn-mode must be "folder" or "news"');if(e==="cdn-cat"&&!/^\d+$/.test(t))throw new u("cdn-cat must be a numeric CatID");if(e==="token")return t.trim();if(e==="render-port"&&!/^\d{2,5}$/.test(t))throw new u("render-port must be a port number");return t}class Ve{file;env;flags;constructor(e,t=process.env,r={}){this.file=e;this.env=t;this.flags=r}static async load(e={},t=process.env){return new Ve(await ke(t),t,e)}resolve(e){let t=this.flags[e];if(t)return{value:je(e,t),source:"flag"};let r=this.env[rr[e]];if(r)return{value:je(e,r),source:"env"};let n=this.file[e];if(n)return{value:n,source:"config"};if(e==="url")return{value:gs,source:"default"};if(e==="workdir")return{value:hs(this.env),source:"default"};if(e==="cdn-mode")return{value:"folder",source:"default"};if(e==="render-port")return{value:"5080",source:"default"};if(e==="site-src"&&process.platform==="win32"&&nr("C:\\APP\\MLIS_2025\\Default.aspx"))return{value:"C:\\APP\\MLIS_2025",source:"default"};return{value:void 0,source:"unset"}}get url(){return this.resolve("url").value}get workdir(){return this.resolve("workdir").value}token(){let e=this.resolve("token").value;if(!e)throw new u("no token saved; run: mlis token set (paste the jwtAuth cookie value)");return e}envName(e){return rr[e]}}var gs="https://lhu.edu.vn",Ue,rr;var We=b(()=>{x();Ue=["url","token","workdir","cdn-cat","cdn-mode","site-src","render-port"],rr={url:"MLIS_URL",token:"MLIS_TOKEN",workdir:"MLIS_WORKDIR","cdn-cat":"MLIS_CDN_CAT","cdn-mode":"MLIS_CDN_MODE","site-src":"MLIS_SITE_SRC","render-port":"MLIS_RENDER_PORT"}});import{createHash as cr}from"node:crypto";import{mkdir as or,readdir as ws,readFile as dt,rename as ys,rm as Cs,stat as $s,writeFile as ir}from"node:fs/promises";import{dirname as ar,isAbsolute as vs,join as ut,relative as ks,resolve as lr,sep as bs}from"node:path";function J(e){let t=e.slice(e.lastIndexOf("/")+1),r=t.lastIndexOf(".");return r>0?t.slice(r).toLowerCase():""}function T(e){return Ss.includes(J(e))}function Ls(e){return Ps.includes(J(e))}function B(e){return T(e)||Ls(e)}function ce(e){let t=e.replace(/\\/g,"/").trim().replace(/^\/+/,"").replace(/^viewpage(\/|$)/i,"").split("/").filter(Boolean);for(let r of t)if(r==="."||r===".."||/[:*?"<>|]/.test(r)||[...r].some((n)=>n.charCodeAt(0)<32))throw new u(`invalid path segment ${JSON.stringify(r)} in ${JSON.stringify(e)}`);if(t[0]&&mr.has(t[0].toLowerCase()))throw new u(`${t[0]} is the CLI's own folder, not part of ViewPage`);return t.join("/")}function P(e){let t=e.lastIndexOf("/");return t<0?"":e.slice(0,t)}function L(e){return e.slice(e.lastIndexOf("/")+1)}function Ns(e,t){if(!t)return!0;let r=e.toLowerCase(),n=t.toLowerCase();return r===n||r.startsWith(`${n}/`)}function mt(e){let t=e.replace(/^/,"").replace(/\r\n/g,`
4
- `);return`sha1:${cr("sha1").update(t,"utf-8").digest("hex")}`}function F(e,t){if(T(e))return mt(Buffer.from(t).toString("utf-8"));return`sha1:${cr("sha1").update(t).digest("hex")}`}function Rs(e,t){let r=(n)=>n.toLowerCase().replace(/\/+$/,"");return[e.url,...e.aliases??[]].some((n)=>r(n)===r(t))}class ft{url;sameServer;root;state=null;constructor(e,t,r){this.url=t;this.sameServer=r;this.root=lr(e)}localPath(e){return e?ut(this.root,...e.split("/")):this.root}relOf(e){let t=ks(this.root,e);if(t.startsWith("..")||vs(t))return null;return t.split(bs).join("/")}resolveArg(e,t=process.cwd()){if(e===void 0){let n=this.relOf(t);return n===null?"":ce(n)}let r=this.relOf(lr(t,e));return ce(r??e)}get statePath(){return ut(this.root,dr,"state.json")}get historyRoot(){return ut(this.root,ur)}async loadState(){if(this.state)return this.state;let e=null;try{e=JSON.parse(await dt(this.statePath,"utf-8"))}catch(r){if(r.code!=="ENOENT")throw r}if(!e)return this.state={version:1,url:this.url,files:{}},this.state;let t={version:1,url:e.url??this.url,aliases:e.aliases??[],files:e.files??{}};if(!Rs(t,this.url)){let r=Object.values(t.files).slice(0,200);if(!this.sameServer||!await this.sameServer(this.url,r))throw new u(`workspace ${this.root} holds the ViewPage of ${t.url}, and ${this.url} is not the same MLIS server (its files differ); a different MLIS installation needs its own --workdir`);return t.aliases=[...t.aliases??[],this.url],this.state=t,await this.saveState(),t}return this.state=t,t}async urls(){let e=await this.loadState();return[e.url,...e.aliases??[]]}async entry(e){return(await this.loadState()).files[e.toLowerCase()]}async entries(e=""){return Object.values((await this.loadState()).files).filter((t)=>Ns(t.path,e))}async record(e){(await this.loadState()).files[e.path.toLowerCase()]={...e,syncedAt:new Date().toISOString()}}async forget(e){delete(await this.loadState()).files[e.toLowerCase()]}async saveState(){if(!this.state)return;await or(ar(this.statePath),{recursive:!0});let e=`${this.statePath}.${process.pid}.tmp`;await ir(e,`${JSON.stringify(this.state,null,1)}
5
- `,"utf-8"),await ys(e,this.statePath)}async readLocal(e){try{return(await dt(this.localPath(e),"utf-8")).replace(/^/,"")}catch(t){let r=t.code;if(r==="ENOENT"||r==="ENOTDIR"||r==="EISDIR")return null;throw t}}async readLocalBytes(e){try{return await dt(this.localPath(e))}catch(t){let r=t.code;if(r==="ENOENT"||r==="ENOTDIR"||r==="EISDIR")return null;throw t}}async writeLocal(e,t){let r=this.localPath(e);await or(ar(r),{recursive:!0}),await ir(r,t)}async removeLocal(e){await Cs(this.localPath(e),{force:!0})}async isLocalDir(e){try{return(await $s(this.localPath(e))).isDirectory()}catch{return!1}}async localFiles(e=""){let t=[],r=async(n)=>{let s;try{s=await ws(this.localPath(n),{withFileTypes:!0})}catch{return}for(let o of s){let i=n?`${n}/${o.name}`:o.name;if(!n&&mr.has(o.name.toLowerCase()))continue;if(o.isDirectory())await r(i);else if(o.isFile()&&B(i))t.push(i)}};if(e&&B(e)){if(await this.readLocalBytes(e)!==null)t.push(e)}else await r(e);return t.sort()}}var Ss,Ps,be=10485760,dr="_mlis",ur="_history",mr;var U=b(()=>{x();Ss=[".ascx",".css",".js"],Ps=[".png",".jpg",".jpeg",".gif",".webp",".svg",".ico",".bmp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp4",".webm",".mp3",".pdf"],mr=new Set([dr.toLowerCase(),ur.toLowerCase(),"_news"])});async function fr(e,t){if(t.length===0)return!0;let r=new Map;for(let n of t){let s=P(n.path);if(!s)continue;r.set(s,[...r.get(s)??[],n])}for(let[n,s]of[...r].slice(0,xs)){let o;try{o=await e.list(n)}catch{continue}for(let i of s){let c=o.find((a)=>a.Name.toLowerCase()===L(i.path).toLowerCase());if(c&&c.Size===i.size&&c.LastModified===i.lastModified)return!0}}return!1}var xs=4;var pr=b(()=>{U()});async function hr(e,t){let r=await Ve.load({url:v(e,"url"),workdir:v(e,"workdir")}),n=e.json===!0,s,o,i=()=>o??=new ve(r.url,r.token());return{settings:r,values:e,positionals:t,json:n,render:{json:n,header:e["no-header"]!==!0},workspace:()=>s??=new ft(r.workdir,r.url,(c,a)=>fr(i(),a)),api:i}}function v(e,t){let r=e[t];return typeof r==="string"?r:void 0}function k(e,t){return e[t]===!0}function C(e,t,r){let n=e.positionals[t];if(n===void 0||n==="")throw new u(`missing ${r}`);return n}var gr;var A=b(()=>{te();We();pr();U();x();gr={url:{type:"string"},workdir:{type:"string"},json:{type:"boolean"},"no-header":{type:"boolean"},help:{type:"boolean",short:"h"}}});function Cr(e){return e?.code==="EPIPE"}function h(e){if(wr)return wr(e);try{process.stdout.write(e)}catch(t){if(Cr(t))process.exit(0);throw t}}function g(e){let t=e.endsWith(`
2
+ var k=(e,t,r)=>()=>{if(e)try{t=e(e=0)}catch(n){r=[n]}if(r)throw r[0];return t};var it;var Rt=k(()=>{it={name:"@fui-org/mlis-cli",version:"0.7.0",description:"CLI thiết kế trang ascx (ViewPage) cho website MLIS qua API có sẵn, kèm render cục bộ và skill cho Claude Code",keywords:["mlis","ascx","viewpage","cli","bun","ai"],license:"MIT",repository:{type:"git",url:"git+https://github.com/fui-org/mlis-cli.git"},publishConfig:{access:"public"},bin:{mlis:"dist/mlis.js"},files:["dist","skills","render","README.md"],type:"module",scripts:{mlis:"bun src/bin.ts",build:"bun build src/bin.ts --target node --minify --outdir dist --entry-naming mlis.js","build:exe":"bun build src/bin.ts --compile --minify --outfile dist/mlis.exe",prepack:"bun run build",test:"bun test",typecheck:"tsc --noEmit"},devDependencies:{"@types/bun":"latest",typescript:"^5.6.0"},engines:{node:">=22"}}});function Rr(e){if(e instanceof m)return V.Usage;if(e instanceof E)return V.Guard;return V.Runtime}var V,m,E;var N=k(()=>{V={Ok:0,Runtime:1,Usage:2,Guard:3};m=class m extends Error{name="UsageError"};E=class E extends Error{name="GuardError"}});import{existsSync as Er}from"node:fs";import{chmod as to,mkdir as ro,readFile as no,rename as so,writeFile as oo}from"node:fs/promises";import{homedir as Tr}from"node:os";import{dirname as io,join as at}from"node:path";function Oe(e=process.env){if(e.MLIS_CONFIG_DIR)return e.MLIS_CONFIG_DIR;let t=e.XDG_CONFIG_HOME||at(e.HOME||e.USERPROFILE||Tr(),".config");return at(t,"mlis")}function ye(e=process.env){return at(Oe(e),"config.json")}async function ae(e=process.env){try{let t=JSON.parse(await no(ye(e),"utf-8"));return t&&typeof t==="object"?t:{}}catch(t){if(t.code==="ENOENT")return{};throw Error(`cannot read ${ye(e)}: ${t.message}`)}}async function le(e,t=process.env){let r=ye(t);await ro(io(r),{recursive:!0});let n=`${r}.${process.pid}.tmp`;await oo(n,`${JSON.stringify(e,null,2)}
3
+ `,{encoding:"utf-8",mode:384}),await so(n,r),await to(r,384).catch(()=>{return})}function lo(e){if(process.platform==="win32"&&Er("C:\\APP"))return"C:\\APP\\mlis-workspaces";return at(e.HOME||e.USERPROFILE||Tr(),"mlis-workspaces")}function Nt(e){let t;try{t=new URL(/^https?:\/\//i.test(e)?e:`https://${e}`)}catch{throw new m(`invalid url ${JSON.stringify(e)}; expected e.g. https://lhu.edu.vn`)}if(t.username||t.password||t.search||t.hash)throw new m("url must be the site address only, e.g. https://lhu.edu.vn");return`${t.origin}${t.pathname.replace(/\/+$/,"")}`}function lt(e,t){if(e==="url")return Nt(t);if(e==="cdn-mode"&&t!=="folder"&&t!=="news")throw new m('cdn-mode must be "folder" or "news"');if(e==="cdn-cat"&&!/^\d+$/.test(t))throw new m("cdn-cat must be a numeric CatID");if(e==="token")return t.trim();if(e==="render-port"&&!/^\d{2,5}$/.test(t))throw new m("render-port must be a port number");return t}function ce(e){let t=e.split(".")[1];if(!t)return null;try{let r=JSON.parse(Buffer.from(t.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf-8"));return typeof r.exp==="number"?new Date(r.exp*1000):null}catch{return null}}class dt{file;env;flags;constructor(e,t=process.env,r={}){this.file=e;this.env=t;this.flags=r}static async load(e={},t=process.env){return new dt(await ae(t),t,e)}resolve(e){let t=this.flags[e];if(t)return{value:lt(e,t),source:"flag"};let r=this.env[Nr[e]];if(r)return{value:lt(e,r),source:"env"};let n=this.file[e];if(n)return{value:n,source:"config"};if(e==="url")return{value:ao,source:"default"};if(e==="workdir")return{value:lo(this.env),source:"default"};if(e==="cdn-mode")return{value:"folder",source:"default"};if(e==="render-port")return{value:"5080",source:"default"};if(e==="site-src"&&process.platform==="win32"&&Er("C:\\APP\\MLIS_2025\\Default.aspx"))return{value:"C:\\APP\\MLIS_2025",source:"default"};return{value:void 0,source:"unset"}}get url(){return this.resolve("url").value}get workdir(){return this.resolve("workdir").value}token(){let e=this.resolve("token").value;if(!e)throw new m("no token saved; run: mlis token set (paste the jwtAuth cookie value)");return e}envName(e){return Nr[e]}}var ao="https://lhu.edu.vn",ct,Nr;var de=k(()=>{N();ct=["url","token","workdir","cdn-cat","cdn-mode","site-src","render-port"],Nr={url:"MLIS_URL",token:"MLIS_TOKEN",workdir:"MLIS_WORKDIR","cdn-cat":"MLIS_CDN_CAT","cdn-mode":"MLIS_CDN_MODE","site-src":"MLIS_SITE_SRC","render-port":"MLIS_RENDER_PORT"}});class Me{baseUrl;request;tokenFor;constructor(e,t,r=fetch){this.baseUrl=e;this.request=r;this.tokenFor=typeof t==="string"?async()=>t:t}async call(e,t={},r="viewpage"){let n=`${this.baseUrl}/api/${r}/${e}`,s=r==="admin"?"WebApiController.vb":"WebApiViewPageController.vb",o=await this.tokenFor(),i=()=>this.request(n,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",cookie:`jwtAuth=${o}`},body:JSON.stringify(t),signal:AbortSignal.timeout(co)}),c;try{if(c=await i(),c.status===401){let f=await this.tokenFor(!0);if(f!==o)o=f,c=await i()}}catch(f){if(f instanceof m)throw f;throw Error(`cannot reach ${n}: ${f.message}`)}let l=await c.text(),a;try{a=JSON.parse(l)}catch{a=void 0}if(c.status===401||a?.Message==="NotAuthenticated"){let f=ce(o),u=f&&f.getTime()<=Date.now()?`the token expired ${f.toLocaleString()}`:"NotAuthenticated, or not enough rights";throw new m(`server rejected the token (${u}); get a pairing code in /admin → Thiết bị and run: mlis token set`)}if(!a){if(c.status===404)throw new T(`${e}: this server has no api/${r}/${e}; deploy the updated ${s}`,404,"");throw new T(`${e}: HTTP ${c.status}, not JSON: ${l.replace(/\s+/g," ").slice(0,160)}`,c.status,"")}let d=a.Message??"";if(c.status===404&&d.startsWith("No HTTP resource"))throw new T(`${e}: this server has no api/${r}/${e}; deploy the updated ${s}`,404,d);if(!c.ok||d!=="")throw new T(`${e}: ${d||`HTTP ${c.status}`}`,c.status,d);return{...a,Message:d}}async tree(){return(await this.call("GetDirectoryTree")).Data??[]}async folderTree(){return this.rows(await this.call("GetAllFolder",{},"admin"),"GetAllFolder")}async folderList(e){return this.rows(await this.call("GetFolderList",{CatID:e,IncludePageRead:!0},"admin"),"GetFolderList")}async newsList(e){let t={CatID:e.catId,NewsStatus:e.trash??!1,NewsImport:!1,FromDate:e.from??"",ToDate:e.to??"",Skip:e.skip??0,NumRows:e.limit??20,FS:e.search??""};return this.rows(await this.call("GetNewsList",t,"admin"),"GetNewsList")}async news(e){return(await this.call("GetNews",{NewsID:e},"admin")).Data}async saveNews(e){return(await this.call("SaveNews",{...e},"admin")).Data}async folder(e){return(await this.call("GetFolder",{CatID:e},"admin")).Data}async saveFolder(e){return(await this.call("SaveFolder",e,"admin")).Data}async folderDoc(e){return(await this.call("GetFolderDoc",{CatID:e},"admin")).Data}async saveFolderDoc(e,t,r){let n={CatID:e,HTMLDoc:t};if(r)n.Version=r;return(await this.call("SaveFolderDoc",n,"admin")).Data}async moveFolder(e,t){await this.call("MoveFolder",{CatID:e,ToCatID:t},"admin")}rows(e,t){if(typeof e.Data==="string")throw new T(`${t}: this server still returns HTML; deploy the updated WebApiController.vb (admin APIs return data)`,200,"");return Array.isArray(e.Data)?e.Data:[]}async list(e){return(await this.call("GetFileList",{path:e,includeBinary:!0})).Data??[]}async download(e){return Buffer.from((await this.call("DownloadFile",{path:e})).Data??"","base64")}async upload(e,t,r,n){let s=Buffer.from(r).toString("base64");return this.call("UploadFile",{path:e,fileName:t,data:s,overwrite:n})}async read(e){let t=await this.call("ReadFile",{path:e});return{Content:t.Content??"",FileName:t.FileName,Extension:t.Extension}}async save(e,t){return(await this.call("SaveFile",{path:e,content:t})).SavedAt}async createFile(e,t){return(await this.call("CreateFile",{path:e,fileName:t})).Path}async deleteFile(e){await this.call("DeleteFile",{path:e})}async createDirectory(e,t){return(await this.call("CreateDirectory",{path:e,dirName:t})).Path}async deleteDirectory(e){await this.call("DeleteDirectory",{path:e})}}async function re(e,t,r){let n=Array(e.length),s=0;async function o(){while(s<e.length){let i=s++;n[i]=await r(e[i])}}return await Promise.all(Array.from({length:Math.min(t,e.length)},o)),n}var co=60000,T;var X=k(()=>{N();de();T=class T extends Error{status;serverMessage;name="ApiError";constructor(e,t,r){super(e);this.status=t;this.serverMessage=r}get notFound(){return this.status===404}}});import{createPrivateKey as uo,createPublicKey as mo,generateKeyPairSync as fo,sign as po}from"node:crypto";import{chmod as go,mkdir as ho,readFile as wo,rm as yo,writeFile as Co}from"node:fs/promises";import{dirname as $o,join as vo}from"node:path";function Ae(e=process.env){return vo(Oe(e),"device-key.pem")}async function Dr(e=process.env){let{privateKey:t}=fo("ec",{namedCurve:"P-256"}),r=Ae(e);return await ho($o(r),{recursive:!0}),await Co(r,t.export({type:"pkcs8",format:"pem"}),{encoding:"utf-8",mode:384}),await go(r,384).catch(()=>{return}),t}async function Fr(e=process.env){try{return uo(await wo(Ae(e),"utf-8"))}catch{return null}}async function Et(e=process.env){await yo(Ae(e),{force:!0})}function Or(e){let t=mo(e).export({format:"jwk"});return JSON.stringify({kty:t.kty,crv:t.crv,x:t.x,y:t.y})}function Mr(e,t){return po("sha256",Buffer.from(t,"utf-8"),{key:e,dsaEncoding:"ieee-p1363"}).toString("base64url")}var Tt=k(()=>{de()});import{hostname as ko}from"node:os";async function Dt(e,t,r){let n;try{n=await fetch(`${e}/api/auth/${t}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8"},body:JSON.stringify(r),signal:AbortSignal.timeout(So)})}catch(o){throw Error(`cannot reach ${e}: ${o.message}`)}let s=await n.text();try{return{status:n.status,data:JSON.parse(s)}}catch{if(n.status===404)throw new m(`${e} has no api/auth/${t}; deploy the device-session update (WebApiAuthController.vb)`);throw Error(`api/auth/${t}: HTTP ${n.status}`)}}function Ar(e){return/^[0-9A-Z]{5}-?[0-9A-Z]{5}$/i.test(e.trim())}async function _r(e,t){let r=await Dr(),{status:n,data:s}=await Dt(e,"pair",{Code:t.trim(),DeviceKey:Or(r),Label:`mlis@${ko()}`});if(n!==200||!s.Token||!s.SessionID)throw new m(`pairing refused: ${s.Message||`HTTP ${n}`}`);return{token:s.Token,session:s.SessionID,expires:s.SessionExpires}}function jr(e){let r=e.resolve("token").source==="env"?void 0:e.file.session,n=e.resolve("token").value;return async(s=!1)=>{if(!r){if(!n)throw new m("no token saved; get a pairing code in /admin → Thiết bị, then run: mlis token set");return n}let o=n?ce(n):null;if(!s&&n&&o&&o.getTime()-Date.now()>bo)return n;let i=await Fr();if(!i)throw new m("this machine's device key is missing; get a new pairing code in /admin → Thiết bị and run: mlis token set");let c=await Dt(e.url,"challenge",{SessionID:r}),l=c.status===200&&c.data.Nonce?await Dt(e.url,"refresh",{SessionID:r,Nonce:c.data.Nonce,Signature:Mr(i,c.data.Nonce)}):c;if(l.status!==200||!l.data.Token)throw new m(`this machine's mlis session ended (${l.data.Message||`HTTP ${l.status}`}: signed out, expired or account locked); `+"get a new pairing code in /admin → Thiết bị and run: mlis token set");n=l.data.Token;let a=await ae();if(a.session===r)await le({...a,token:n});return n}}var bo=60000,So=30000;var Ft=k(()=>{N();de();Tt()});import{createHash as Wr}from"node:crypto";import{mkdir as Ur,readdir as Po,readFile as Ot,rename as Io,rm as xo,stat as Lo,writeFile as Hr}from"node:fs/promises";import{dirname as Vr,isAbsolute as Ro,join as Mt,relative as No,resolve as Kr,sep as Eo}from"node:path";function z(e){let t=e.slice(e.lastIndexOf("/")+1),r=t.lastIndexOf(".");return r>0?t.slice(r).toLowerCase():""}function D(e){return To.includes(z(e))}function Fo(e){return Do.includes(z(e))}function J(e){return D(e)||Fo(e)}function Ce(e){let t=e.replace(/\\/g,"/").trim().replace(/^\/+/,"").replace(/^viewpage(\/|$)/i,"").split("/").filter(Boolean);for(let r of t)if(r==="."||r===".."||/[:*?"<>|]/.test(r)||[...r].some((n)=>n.charCodeAt(0)<32))throw new m(`invalid path segment ${JSON.stringify(r)} in ${JSON.stringify(e)}`);if(t[0]&&zr.has(t[0].toLowerCase()))throw new m(`${t[0]} is the CLI's own folder, not part of ViewPage`);return t.join("/")}function x(e){let t=e.lastIndexOf("/");return t<0?"":e.slice(0,t)}function L(e){return e.slice(e.lastIndexOf("/")+1)}function Oo(e,t){if(!t)return!0;let r=e.toLowerCase(),n=t.toLowerCase();return r===n||r.startsWith(`${n}/`)}function At(e){let t=e.replace(/^/,"").replace(/\r\n/g,`
4
+ `);return`sha1:${Wr("sha1").update(t,"utf-8").digest("hex")}`}function F(e,t){if(D(e))return At(Buffer.from(t).toString("utf-8"));return`sha1:${Wr("sha1").update(t).digest("hex")}`}function Mo(e,t){let r=(n)=>n.toLowerCase().replace(/\/+$/,"");return[e.url,...e.aliases??[]].some((n)=>r(n)===r(t))}class _t{url;sameServer;root;state=null;constructor(e,t,r){this.url=t;this.sameServer=r;this.root=Kr(e)}localPath(e){return e?Mt(this.root,...e.split("/")):this.root}relOf(e){let t=No(this.root,e);if(t.startsWith("..")||Ro(t))return null;return t.split(Eo).join("/")}resolveArg(e,t=process.cwd()){if(e===void 0){let n=this.relOf(t);return n===null?"":Ce(n)}let r=this.relOf(Kr(t,e));return Ce(r??e)}get statePath(){return Mt(this.root,Br,"state.json")}get historyRoot(){return Mt(this.root,Jr)}async loadState(){if(this.state)return this.state;let e=null;try{e=JSON.parse(await Ot(this.statePath,"utf-8"))}catch(r){if(r.code!=="ENOENT")throw r}if(!e)return this.state={version:1,url:this.url,files:{}},this.state;let t={version:1,url:e.url??this.url,aliases:e.aliases??[],files:e.files??{}};if(!Mo(t,this.url)){let r=Object.values(t.files).slice(0,200);if(!this.sameServer||!await this.sameServer(this.url,r))throw new m(`workspace ${this.root} holds the ViewPage of ${t.url}, and ${this.url} is not the same MLIS server (its files differ); a different MLIS installation needs its own --workdir`);return t.aliases=[...t.aliases??[],this.url],this.state=t,await this.saveState(),t}return this.state=t,t}async urls(){let e=await this.loadState();return[e.url,...e.aliases??[]]}async entry(e){return(await this.loadState()).files[e.toLowerCase()]}async entries(e=""){return Object.values((await this.loadState()).files).filter((t)=>Oo(t.path,e))}async record(e){(await this.loadState()).files[e.path.toLowerCase()]={...e,syncedAt:new Date().toISOString()}}async forget(e){delete(await this.loadState()).files[e.toLowerCase()]}async saveState(){if(!this.state)return;await Ur(Vr(this.statePath),{recursive:!0});let e=`${this.statePath}.${process.pid}.tmp`;await Hr(e,`${JSON.stringify(this.state,null,1)}
5
+ `,"utf-8"),await Io(e,this.statePath)}async readLocal(e){try{return(await Ot(this.localPath(e),"utf-8")).replace(/^/,"")}catch(t){let r=t.code;if(r==="ENOENT"||r==="ENOTDIR"||r==="EISDIR")return null;throw t}}async readLocalBytes(e){try{return await Ot(this.localPath(e))}catch(t){let r=t.code;if(r==="ENOENT"||r==="ENOTDIR"||r==="EISDIR")return null;throw t}}async writeLocal(e,t){let r=this.localPath(e);await Ur(Vr(r),{recursive:!0}),await Hr(r,t)}async removeLocal(e){await xo(this.localPath(e),{force:!0})}async isLocalDir(e){try{return(await Lo(this.localPath(e))).isDirectory()}catch{return!1}}async localFiles(e=""){let t=[],r=async(n)=>{let s;try{s=await Po(this.localPath(n),{withFileTypes:!0})}catch{return}for(let o of s){let i=n?`${n}/${o.name}`:o.name;if(!n&&zr.has(o.name.toLowerCase()))continue;if(o.isDirectory())await r(i);else if(o.isFile()&&J(i))t.push(i)}};if(e&&J(e)){if(await this.readLocalBytes(e)!==null)t.push(e)}else await r(e);return t.sort()}}var To,Do,_e=10485760,Br="_mlis",Jr="_history",zr;var K=k(()=>{N();To=[".ascx",".css",".js"],Do=[".png",".jpg",".jpeg",".gif",".webp",".svg",".ico",".bmp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp4",".webm",".mp3",".pdf"],zr=new Set([Br.toLowerCase(),Jr.toLowerCase(),"_news","_categories"])});async function Gr(e,t){if(t.length===0)return!0;let r=new Map;for(let n of t){let s=x(n.path);if(!s)continue;r.set(s,[...r.get(s)??[],n])}for(let[n,s]of[...r].slice(0,Ao)){let o;try{o=await e.list(n)}catch{continue}for(let i of s){let c=o.find((l)=>l.Name.toLowerCase()===L(i.path).toLowerCase());if(c&&c.Size===i.size&&c.LastModified===i.lastModified)return!0}}return!1}var Ao=4;var qr=k(()=>{K()});async function Yr(e,t){let r=await dt.load({url:b(e,"url"),workdir:b(e,"workdir")}),n=e.json===!0,s,o,i=jr(r),c=()=>o??=new Me(r.url,i);return{settings:r,values:e,positionals:t,json:n,render:{json:n,header:e["no-header"]!==!0},workspace:()=>s??=new _t(r.workdir,r.url,(l,a)=>Gr(c(),a)),api:c,token:i}}function b(e,t){let r=e[t];return typeof r==="string"?r:void 0}function v(e,t){return e[t]===!0}function C(e,t,r){let n=e.positionals[t];if(n===void 0||n==="")throw new m(`missing ${r}`);return n}var Xr;var j=k(()=>{X();de();Ft();qr();K();N();Xr={url:{type:"string"},workdir:{type:"string"},json:{type:"boolean"},"no-header":{type:"boolean"},help:{type:"boolean",short:"h"}}});function en(e){return e?.code==="EPIPE"}function h(e){if(Zr)return Zr(e);try{process.stdout.write(e)}catch(t){if(en(t))process.exit(0);throw t}}function g(e){let t=e.endsWith(`
6
6
  `)?e:`${e}
7
- `;if(yr)return yr(t);process.stderr.write(t)}async function $r(){let e=[];for await(let t of process.stdin)e.push(Buffer.from(t));return Buffer.concat(e).toString("utf-8")}async function vr(e){let t=process.stdin;if(!t.isTTY)throw new u("no terminal to prompt on; pipe the value on stdin instead");return process.stderr.write(e),t.setRawMode(!0),t.resume(),t.setEncoding("utf-8"),new Promise((r,n)=>{let s="",o=(c)=>{if(t.setRawMode(!1),t.pause(),t.off("data",i),process.stderr.write(`
8
- `),c)n(c);else r(s)},i=(c)=>{for(let a of c){if(a==="\r"||a===`
9
- `)return o();if(a==="\x03")return o(new u("cancelled"));if(a===""||a==="\b")s=s.slice(0,-1);else if(a>=" ")s+=a}};t.on("data",i)})}var wr=null,yr=null;var O=b(()=>{x();process.stdout.on("error",(e)=>{if(Cr(e))process.exit(0);throw e})});function Is(e){if(e===null||e===void 0)return"";return(typeof e==="object"?JSON.stringify(e):String(e)).replace(/\t/g,"\\t").replace(/\r/g,"\\r").replace(/\n/g,"\\n")}function kr(e,t){let r=[...new Set(e.flatMap((s)=>Object.keys(s)))],n=e.map((s)=>r.map((o)=>Is(s[o])));return t?[r,...n]:n}function Es(e,t=!0){if(e.length===0)return"";return`${kr(e,t).map((r)=>r.join("\t")).join(`
7
+ `;if(Qr)return Qr(t);process.stderr.write(t)}async function tn(){let e=[];for await(let t of process.stdin)e.push(Buffer.from(t));return Buffer.concat(e).toString("utf-8")}async function rn(e){let t=process.stdin;if(!t.isTTY)throw new m("no terminal to prompt on; pipe the value on stdin instead");return process.stderr.write(e),t.setRawMode(!0),t.resume(),t.setEncoding("utf-8"),new Promise((r,n)=>{let s="",o=(c)=>{if(t.setRawMode(!1),t.pause(),t.off("data",i),process.stderr.write(`
8
+ `),c)n(c);else r(s)},i=(c)=>{for(let l of c){if(l==="\r"||l===`
9
+ `)return o();if(l==="\x03")return o(new m("cancelled"));if(l===""||l==="\b")s=s.slice(0,-1);else if(l>=" ")s+=l}};t.on("data",i)})}var Zr=null,Qr=null;var M=k(()=>{N();process.stdout.on("error",(e)=>{if(en(e))process.exit(0);throw e})});function _o(e){if(e===null||e===void 0)return"";return(typeof e==="object"?JSON.stringify(e):String(e)).replace(/\t/g,"\\t").replace(/\r/g,"\\r").replace(/\n/g,"\\n")}function nn(e,t){let r=[...new Set(e.flatMap((s)=>Object.keys(s)))],n=e.map((s)=>r.map((o)=>_o(s[o])));return t?[r,...n]:n}function jo(e,t=!0){if(e.length===0)return"";return`${nn(e,t).map((r)=>r.join("\t")).join(`
10
10
  `)}
11
- `}function Ts(e,t=!0){if(e.length===0)return"";let r=kr(e,t),n=r[0].map((o,i)=>Math.max(...r.map((c)=>c[i].length)));return`${r.map((o)=>o.map((i,c)=>c===o.length-1?i:i.padEnd(n[c])).join(" ")).join(`
11
+ `}function Uo(e,t=!0){if(e.length===0)return"";let r=nn(e,t),n=r[0].map((o,i)=>Math.max(...r.map((c)=>c[i].length)));return`${r.map((o)=>o.map((i,c)=>c===o.length-1?i:i.padEnd(n[c])).join(" ")).join(`
12
12
  `)}
13
- `}function H(e){return`${JSON.stringify(e)}
14
- `}function N(e,t){let{json:r,header:n,isTty:s=process.stdout.isTTY===!0}=t;if(r)return H(e);return s?Ts(e,n):Es(e,n)}function Se(e,t){for(let r of e){if(r.Path.toLowerCase()===t.toLowerCase())return r;if(t.toLowerCase().startsWith(`${r.Path.toLowerCase()}/`))return Se(r.Children,t)}return null}function Be(e,t){let r=t?Se(e,t):null;if(t&&!r)throw new u(`folder ${t} does not exist on the server`);let n=[],s=(o)=>{n.push(o.Path),o.Children.forEach(s)};return(r?[r]:e).forEach(s),n}async function br(e,t){return Be(await e.api().tree(),t)}var Fs,Ds,Os,Sr;var Ke=b(()=>{A();x();O();te();U();Fs={name:"tree",usage:"mlis tree [folder] [--files]",summary:"Show the ViewPage folder tree; --files also lists the .ascx/.css/.js files",options:{files:{type:"boolean"}},async run(e){let t=e.positionals[0]?e.workspace().resolveArg(e.positionals[0]):"",r=await e.api().tree(),n=t?Se(r,t):null;if(t&&!n)throw new u(`folder ${t} does not exist on the server`);let s=n?[n]:r,o=k(e.values,"files"),i=new Map;if(o){let l=Be(r,t),d=await ee(l,4,(f)=>e.api().list(f));l.forEach((f,m)=>i.set(f,d[m]))}if(e.json){let l=(d)=>({name:d.Name,path:d.Path,...o?{files:(i.get(d.Path)??[]).map((f)=>f.Name)}:{},children:d.Children.map(l)});h(H(s.map(l)));return}let c=[],a=(l,d)=>{let f=" ".repeat(d);c.push(`${f}${l.Name}/`);for(let m of i.get(l.Path)??[])c.push(`${f} ${m.Name}`);l.Children.forEach((m)=>a(m,d+1))};s.forEach((l)=>a(l,0)),h(c.length?`${c.join(`
13
+ `}function W(e){return`${JSON.stringify(e)}
14
+ `}function P(e,t){let{json:r,header:n,isTty:s=process.stdout.isTTY===!0}=t;if(r)return W(e);return s?Uo(e,n):jo(e,n)}function je(e,t){for(let r of e){if(r.Path.toLowerCase()===t.toLowerCase())return r;if(t.toLowerCase().startsWith(`${r.Path.toLowerCase()}/`))return je(r.Children,t)}return null}function ut(e,t){let r=t?je(e,t):null;if(t&&!r)throw new m(`folder ${t} does not exist on the server`);let n=[],s=(o)=>{n.push(o.Path),o.Children.forEach(s)};return(r?[r]:e).forEach(s),n}async function sn(e,t){return ut(await e.api().tree(),t)}var Ho,Vo,Ko,on;var mt=k(()=>{j();N();M();X();K();Ho={name:"tree",usage:"mlis tree [folder] [--files]",summary:"Show the ViewPage folder tree; --files also lists the .ascx/.css/.js files",options:{files:{type:"boolean"}},async run(e){let t=e.positionals[0]?e.workspace().resolveArg(e.positionals[0]):"",r=await e.api().tree(),n=t?je(r,t):null;if(t&&!n)throw new m(`folder ${t} does not exist on the server`);let s=n?[n]:r,o=v(e.values,"files"),i=new Map;if(o){let a=ut(r,t),d=await re(a,4,(f)=>e.api().list(f));a.forEach((f,u)=>i.set(f,d[u]))}if(e.json){let a=(d)=>({name:d.Name,path:d.Path,...o?{files:(i.get(d.Path)??[]).map((f)=>f.Name)}:{},children:d.Children.map(a)});h(W(s.map(a)));return}let c=[],l=(a,d)=>{let f=" ".repeat(d);c.push(`${f}${a.Name}/`);for(let u of i.get(a.Path)??[])c.push(`${f} ${u.Name}`);a.Children.forEach((u)=>l(u,d+1))};s.forEach((a)=>l(a,0)),h(c.length?`${c.join(`
15
15
  `)}
16
- `:"")}},Ds={name:"ls",usage:"mlis ls [folder]",summary:"List the .ascx/.css/.js files of one folder (size, last modified)",async run(e){let t=e.positionals[0]?e.workspace().resolveArg(e.positionals[0]):"",r=await e.api().list(t);h(N(r.map((n)=>({name:n.Name,type:n.Extension,size:n.Size,modified:n.LastModified,path:n.Path})),e.render))}},Os={name:"cat",usage:"mlis cat <file>",summary:"Print one file as it is on the server",async run(e){let t=e.workspace().resolveArg(C(e,0,"file"));if(!T(t))throw new u("cat prints .ascx, .css and .js; images/fonts are fetched with: mlis pull <file>");let{Content:r}=await e.api().read(t);h(e.json?H({path:t,content:r}):r.endsWith(`
16
+ `:"")}},Vo={name:"ls",usage:"mlis ls [folder]",summary:"List the .ascx/.css/.js files of one folder (size, last modified)",async run(e){let t=e.positionals[0]?e.workspace().resolveArg(e.positionals[0]):"",r=await e.api().list(t);h(P(r.map((n)=>({name:n.Name,type:n.Extension,size:n.Size,modified:n.LastModified,path:n.Path})),e.render))}},Ko={name:"cat",usage:"mlis cat <file>",summary:"Print one file as it is on the server",async run(e){let t=e.workspace().resolveArg(C(e,0,"file"));if(!D(t))throw new m("cat prints .ascx, .css and .js; images/fonts are fetched with: mlis pull <file>");let{Content:r}=await e.api().read(t);h(e.json?W({path:t,content:r}):r.endsWith(`
17
17
  `)?r:`${r}
18
- `)}},Sr=[Fs,Ds,Os]});import{mkdir as Ms,readFile as As,writeFile as _s}from"node:fs/promises";import{dirname as Pr,join as js}from"node:path";function D(e,...t){for(let r of t){let n=Object.keys(e).find((s)=>s.toLowerCase()===r.toLowerCase());if(n!==void 0&&e[n]!==null&&e[n]!==void 0)return e[n]}return}function ne(e){if(e===void 0||e===null||e==="")return null;let t=Number(e);return Number.isFinite(t)?t:null}function de(e){return e===void 0||e===null?"":String(e).trim()}function Lr(e){return{id:ne(D(e,"CategoryID","CatID"))??0,parentId:ne(D(e,"ParentID")),name:de(D(e,"CatName")),alias:de(D(e,"CatAlias")),type:ne(D(e,"CatType")),pageStyle:de(D(e,"PageStyle")),pageRead:de(D(e,"PageRead")),locations:[1,2,3].map((t)=>ne(D(e,`Location${t}`))??0),link:de(D(e,"LinkURL")),sort:ne(D(e,"CatSort")),homePage:ne(D(e,"HomePage")),bits:ne(D(e,"BitOption")),description:de(D(e,"CatDescription")),raw:e}}function Pe(e){return e===null?"":Us[e]??String(e)}function ue(e,t){return e!==null&&(e&1<<t)!==0}function ht(e){return e.startsWith("{")?(e.slice(1).split("}")[0]??"").trim():""}function wt(e){return e.normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/đ/g,"d").replace(/Đ/g,"D").toLowerCase()}class yt{items;byId=new Map;children=new Map;roots=[];constructor(e){this.items=e;for(let r of e)this.byId.set(r.id,r);for(let r of e)if(r.parentId!==null&&this.byId.has(r.parentId)&&r.parentId!==r.id){let n=this.children.get(r.parentId)??[];n.push(r),this.children.set(r.parentId,n)}else this.roots.push(r);let t=(r,n)=>(r.sort??0)-(n.sort??0)||r.id-n.id;this.roots.sort(t);for(let r of this.children.values())r.sort(t)}childrenOf(e){return this.children.get(e)??[]}ancestors(e){let t=[],r=new Set,n=this.byId.get(e);while(n&&!r.has(n.id))t.unshift(n),r.add(n.id),n=n.parentId===null?void 0:this.byId.get(n.parentId);return t}pathText(e){return this.ancestors(e).map((t)=>t.name).join(" › ")}domainFor(e){let t=this.ancestors(e);for(let r=t.length-1;r>=0;r--){let n=ht(t[r].link);if(n)return n}return""}}function gt(e){return js(Pr(e.statePath),"categories.json")}async function Vs(e){let t=await e.folderTree(),r=[...new Set(t.map((o)=>Number(D(o,"ParentID")??0)))],n=await ee(r,4,(o)=>e.folderList(o)),s=new Map;for(let o of n.flat())s.set(Number(D(o,"CategoryID")),o);return t.map((o)=>({...o,...s.get(Number(D(o,"CategoryID")))??{}}))}async function Ct(e,t){let r=await Vs(t),n={url:e.url,fetchedAt:new Date().toISOString(),items:r};return await Ms(Pr(gt(e)),{recursive:!0}),await _s(gt(e),`${JSON.stringify(n)}
19
- `,"utf-8"),new yt(r.map(Lr))}async function Je(e,t,r=!1){if(!r)try{let s=JSON.parse(await As(gt(e),"utf-8"));if((await e.urls()).map((i)=>i.toLowerCase()).includes(String(s.url).toLowerCase())&&Array.isArray(s.items))return{index:new yt(s.items.map(Lr)),fetchedAt:s.fetchedAt,stale:Date.now()-Date.parse(s.fetchedAt)>Hs,fetched:!1}}catch{}return{index:await Ct(e,t()),fetchedAt:new Date().toISOString(),stale:!1,fetched:!0}}var Us,Hs=86400000;var $t=b(()=>{te();Us={0:"Ẩn",1:"Trang chủ",2:"Thư mục",3:"Thông báo",4:"Sự kiện",5:"Giới thiệu",6:"Thư viện ảnh",7:"Liên kết",8:"Tài liệu HTML"}});async function Le(e){let t=await Je(e.workspace(),()=>e.api(),k(e.values,"refresh"));if(t.fetched)g(`# fetched ${t.index.items.length} categories from the server`);else if(t.stale)g(`# category list saved ${t.fetchedAt}; refresh with: mlis category sync`);return t.index}function kt(e,t,r=0){let n=C(e,r,"CatID"),s=Number(n),o=Number.isInteger(s)?t.byId.get(s):void 0;if(!o)throw new u(`no category ${n}; find one with: mlis category find <name>`);return o}function vt(e){return e.locations.some((t)=>t!==0)?e.locations.join(","):""}function Ws(e){let t=[`${e.id} ${e.name}`];if(e.type!==null)t.push(`(${Pe(e.type)})`);if(e.link)t.push(`link:${e.link}`);if(e.pageStyle)t.push(`page:${e.pageStyle}`);if(e.pageRead&&e.pageRead!==e.pageStyle)t.push(`read:${e.pageRead}`);if(vt(e))t.push(`loc:${vt(e)}`);return t.join(" ")}function Nr(e,t={}){return{id:e.id,name:e.name,type:Pe(e.type),loc:vt(e),sort:e.sort??"",page:e.pageStyle,read:e.pageRead,link:e.link,...t}}var Ne,Bs,Ks,Js,zs,Gs,qs,Rr;var xr=b(()=>{A();x();O();$t();U();Ne={refresh:{type:"boolean"}},Bs={name:"category sync",usage:"mlis category sync",summary:"Download the category tree with PageStyle, PageRead, Location1-3 (saved in the workspace)",async run(e){let t=await Ct(e.workspace(),e.api());g(`# saved ${t.items.length} categories`)}},Ks={name:"category tree",usage:"mlis category tree [CatID] [--depth N] [--refresh]",summary:"Category tree: id, name, type, page/read (ViewPage folder), loc (Location1,2,3)",options:{...Ne,depth:{type:"string"}},async run(e){let t=await Le(e),r=v(e.values,"depth")?Number(v(e.values,"depth")):1/0,n=e.positionals[0]?[kt(e,t)]:t.roots;if(e.json){let i=(c,a)=>({...Nr(c),children:a<r?t.childrenOf(c.id).map((l)=>i(l,a+1)):[]});h(`${JSON.stringify(n.map((c)=>i(c,0)))}
20
- `);return}let s=[],o=(i,c)=>{if(s.push(`${" ".repeat(c)}${Ws(i)}`),c<r)for(let a of t.childrenOf(i.id))o(a,c+1)};n.forEach((i)=>o(i,0)),h(s.length?`${s.join(`
18
+ `)}},on=[Ho,Vo,Ko]});import{mkdir as Wo,readFile as Bo,rm as Jo,writeFile as zo}from"node:fs/promises";import{dirname as an,join as Go}from"node:path";function O(e,...t){for(let r of t){let n=Object.keys(e).find((s)=>s.toLowerCase()===r.toLowerCase());if(n!==void 0&&e[n]!==null&&e[n]!==void 0)return e[n]}return}function ue(e){if(e===void 0||e===null||e==="")return null;let t=Number(e);return Number.isFinite(t)?t:null}function $e(e){return e===void 0||e===null?"":String(e).trim()}function ln(e){return{id:ue(O(e,"CategoryID","CatID"))??0,parentId:ue(O(e,"ParentID")),name:$e(O(e,"CatName")),alias:$e(O(e,"CatAlias")),type:ue(O(e,"CatType")),pageStyle:$e(O(e,"PageStyle")),pageRead:$e(O(e,"PageRead")),locations:[1,2,3].map((t)=>ue(O(e,`Location${t}`))??0),link:$e(O(e,"LinkURL")),sort:ue(O(e,"CatSort")),homePage:ue(O(e,"HomePage")),bits:ue(O(e,"BitOption")),description:$e(O(e,"CatDescription")),raw:e}}function Ue(e){return e===null?"":qo[e]??String(e)}function ne(e,t){return e!==null&&(e&1<<t)!==0}function Ut(e){return e.startsWith("{")?(e.slice(1).split("}")[0]??"").trim():""}function Ht(e){return e.normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/đ/g,"d").replace(/Đ/g,"D").toLowerCase()}class Vt{items;byId=new Map;children=new Map;roots=[];constructor(e){this.items=e;for(let r of e)this.byId.set(r.id,r);for(let r of e)if(r.parentId!==null&&this.byId.has(r.parentId)&&r.parentId!==r.id){let n=this.children.get(r.parentId)??[];n.push(r),this.children.set(r.parentId,n)}else this.roots.push(r);let t=(r,n)=>(r.sort??0)-(n.sort??0)||r.id-n.id;this.roots.sort(t);for(let r of this.children.values())r.sort(t)}childrenOf(e){return this.children.get(e)??[]}ancestors(e){let t=[],r=new Set,n=this.byId.get(e);while(n&&!r.has(n.id))t.unshift(n),r.add(n.id),n=n.parentId===null?void 0:this.byId.get(n.parentId);return t}pathText(e){return this.ancestors(e).map((t)=>t.name).join(" › ")}domainFor(e){let t=this.ancestors(e);for(let r=t.length-1;r>=0;r--){let n=Ut(t[r].link);if(n)return n}return""}}function ft(e){return Go(an(e.statePath),"categories.json")}async function Yo(e){let t=await e.folderTree(),r=[...new Set(t.map((o)=>Number(O(o,"ParentID")??0)))],n=await re(r,4,(o)=>e.folderList(o)),s=new Map;for(let o of n.flat())s.set(Number(O(o,"CategoryID")),o);return t.map((o)=>({...o,...s.get(Number(O(o,"CategoryID")))??{}}))}async function cn(e){await Jo(ft(e),{force:!0})}async function Kt(e,t){let r=await Yo(t),n={url:e.url,fetchedAt:new Date().toISOString(),items:r};return await Wo(an(ft(e)),{recursive:!0}),await zo(ft(e),`${JSON.stringify(n)}
19
+ `,"utf-8"),new Vt(r.map(ln))}async function ve(e,t,r=!1){if(!r)try{let s=JSON.parse(await Bo(ft(e),"utf-8"));if((await e.urls()).map((i)=>i.toLowerCase()).includes(String(s.url).toLowerCase())&&Array.isArray(s.items))return{index:new Vt(s.items.map(ln)),fetchedAt:s.fetchedAt,stale:Date.now()-Date.parse(s.fetchedAt)>Xo,fetched:!1}}catch{}return{index:await Kt(e,t()),fetchedAt:new Date().toISOString(),stale:!1,fetched:!0}}var qo,Xo=86400000;var He=k(()=>{X();qo={0:"Ẩn",1:"Trang chủ",2:"Thư mục",3:"Thông báo",4:"Sự kiện",5:"Giới thiệu",6:"Thư viện ảnh",7:"Liên kết",8:"Tài liệu HTML"}});async function Ve(e){let t=await ve(e.workspace(),()=>e.api(),v(e.values,"refresh"));if(t.fetched)g(`# fetched ${t.index.items.length} categories from the server`);else if(t.stale)g(`# category list saved ${t.fetchedAt}; refresh with: mlis category sync`);return t.index}function Bt(e,t,r=0){let n=C(e,r,"CatID"),s=Number(n),o=Number.isInteger(s)?t.byId.get(s):void 0;if(!o)throw new m(`no category ${n}; find one with: mlis category find <name>`);return o}function Wt(e){return e.locations.some((t)=>t!==0)?e.locations.join(","):""}function Zo(e){let t=[`${e.id} ${e.name}`];if(e.type!==null)t.push(`(${Ue(e.type)})`);if(e.link)t.push(`link:${e.link}`);if(e.pageStyle)t.push(`page:${e.pageStyle}`);if(e.pageRead&&e.pageRead!==e.pageStyle)t.push(`read:${e.pageRead}`);if(Wt(e))t.push(`loc:${Wt(e)}`);return t.join(" ")}function dn(e,t={}){return{id:e.id,name:e.name,type:Ue(e.type),loc:Wt(e),sort:e.sort??"",page:e.pageStyle,read:e.pageRead,link:e.link,...t}}var Ke,Qo,ei,ti,ri,ni,si,un;var mn=k(()=>{j();N();M();He();K();Ke={refresh:{type:"boolean"}},Qo={name:"category sync",usage:"mlis category sync",summary:"Download the category tree with PageStyle, PageRead, Location1-3 (saved in the workspace)",async run(e){let t=await Kt(e.workspace(),e.api());g(`# saved ${t.items.length} categories`)}},ei={name:"category tree",usage:"mlis category tree [CatID] [--depth N] [--refresh]",summary:"Category tree: id, name, type, page/read (ViewPage folder), loc (Location1,2,3)",options:{...Ke,depth:{type:"string"}},async run(e){let t=await Ve(e),r=b(e.values,"depth")?Number(b(e.values,"depth")):1/0,n=e.positionals[0]?[Bt(e,t)]:t.roots;if(e.json){let i=(c,l)=>({...dn(c),children:l<r?t.childrenOf(c.id).map((a)=>i(a,l+1)):[]});h(`${JSON.stringify(n.map((c)=>i(c,0)))}
20
+ `);return}let s=[],o=(i,c)=>{if(s.push(`${" ".repeat(c)}${Zo(i)}`),c<r)for(let l of t.childrenOf(i.id))o(l,c+1)};n.forEach((i)=>o(i,0)),h(s.length?`${s.join(`
21
21
  `)}
22
- `:"")}},Js={name:"category ls",usage:"mlis category ls <CatID> [--refresh]",summary:"Child categories of one category with their Location1-3 (what NewsList Location=N picks)",options:Ne,async run(e){let t=await Le(e),r=kt(e,t);if(h(N(t.childrenOf(r.id).map((n)=>Nr(n)),e.render)),!e.json)g(`# ${t.pathText(r.id)}`)}},zs={name:"category show",usage:"mlis category show <CatID> [--refresh]",summary:"Every setting of one category, its path and the domain whose tree holds it",options:Ne,async run(e){let t=await Le(e),r=kt(e,t),n={path:t.pathText(r.id),domain:t.domainFor(r.id),typeName:Pe(r.type),link:ue(r.bits,3),newWindow:ue(r.bits,4),rss:ue(r.bits,2),indexAZ:ue(r.bits,5),addNews:ue(r.bits,0)};if(e.json){h(`${JSON.stringify({...r.raw,derived:n})}
23
- `);return}let s=[...Object.entries(r.raw).map(([o,i])=>({key:o,value:i})),...Object.entries(n).map(([o,i])=>({key:`(${o})`,value:i}))];h(N(s,e.render))}},Gs={name:"category find",usage:"mlis category find <text> [--refresh]",summary:"Find categories by name or alias (accents ignored)",options:Ne,async run(e){let t=await Le(e),r=wt(C(e,0,"text")),n=t.items.filter((s)=>wt(`${s.name} ${s.alias}`).includes(r)||String(s.id)===r);h(N(n.map((s)=>({id:s.id,name:s.name,type:Pe(s.type),page:s.pageStyle,path:t.pathText(s.id)})),e.render))}},qs={name:"category pages",usage:"mlis category pages [ViewPage folder] [--refresh]",summary:"Which categories use which ViewPage folder as page (PageStyle) or read page (PageRead)",options:Ne,async run(e){let t=await Le(e),r=e.positionals[0]?ce(e.positionals[0]).toLowerCase():"",n=[];for(let s of t.items)for(let[o,i]of[["page",s.pageStyle],["read",s.pageRead]]){if(!i)continue;if(r&&i.toLowerCase()!==r)continue;n.push({folder:i,role:o,id:s.id,name:s.name,domain:t.domainFor(s.id)||ht(s.link)})}if(n.sort((s,o)=>String(s.folder).localeCompare(String(o.folder))||Number(s.id)-Number(o.id)),h(N(n,e.render)),!e.json&&r&&n.length===0)g(`# no category uses ${r}`)}},Rr=[Bs,Ks,Js,zs,Gs,qs]});function Er(e,t){let r=1;for(let n=0;n<t&&n<e.length;n++)if(e.charCodeAt(n)===10)r++;return r}function Zs(e){let t=/^\/?viewpage\/(.+)$/i.exec(e.trim().replace(/\\/g,"/"));if(!t)return null;return decodeURIComponent(t[1].split(/[?#]/)[0]).replace(/\/+$/,"")}function Tr(e){let t=[],r=(s,o,i)=>{let c=Zs(i);if(c)t.push({line:Er(e,s),kind:o,path:c,raw:i})};for(let s of e.matchAll(/\bCssFolder\s*=\s*"([^"]+)"/gi))r(s.index,"folder",s[1]);for(let s of e.matchAll(/\b(?:Src|href)\s*=\s*["']([^"']+)["']/gi))r(s.index,"file",s[1]);for(let s of e.matchAll(/\bsrc\s*=\s*["']([^"']+)["']/g))r(s.index,"file",s[1]);for(let s of e.matchAll(/url\(\s*["']?([^"')]+)["']?\s*\)/gi))r(s.index,"file",s[1]);let n=new Set;return t.filter((s)=>{let o=`${s.line}|${s.kind}|${s.path}`;if(n.has(o))return!1;return n.add(o),!0})}function Qs(e){let t=[];for(let r of e.matchAll(/<MLIS:(\w+)/gi)){let n="",s=r.index+r[0].length;for(;s<e.length;s++){let o=e[s];if(n){if(o===n)n=""}else if(o==='"'||o==="'")n=o;else if(o===">")break}t.push({index:r.index,name:r[1],attrs:e.slice(r.index+r[0].length,s)})}return t}function Ir(e){let t=e.replace(/\{\{|\}\}/g,"").replace(/\{\d+(,-?\d+)?(:[^}]*)?\}/g,"");return/[{}]/.test(t)}function Fr(e,t,r){let n=[],s=(i,c,a,l)=>n.push({file:e,line:Er(t,i),level:c,rule:a,message:l});if(e.toLowerCase().endsWith(".ascx")){let i=/<%@\s*Control\b[^%]*CodeFile\s*=/i.exec(t);if(i)s(i.index,"error","codefile","ViewPage controls have no code-behind: remove the <%@ Control … CodeFile=… %> directive");for(let a of Qs(t)){if(!/runat\s*=\s*["']server["']/i.test(a.attrs))s(a.index,"error","runat",`<MLIS:${a.name}> needs runat="server"`);let l=a.name.toLowerCase()==="loadpage"?/\bSrc\s*=\s*"([^"]*)"/i.exec(a.attrs)?.[1]?.trim():void 0;if(l!==void 0&&!/\.ascx$/i.test(l))s(a.index,"warn","loadpage-src",`LoadPage Src="${l}" is not an .ascx file, so it loads nothing; for css only, keep CssFolder and drop Src`)}for(let a of Xs){for(let l of t.matchAll(new RegExp(`\\b${a}\\s*=\\s*"([^"]*)"`,"g")))if(Ir(l[1]))s(l.index,"error","braces",`${a} has a "{" or "}" that is not a placeholder; write {{ or }} for a literal brace`);for(let l of t.matchAll(new RegExp(`<${a}>([\\s\\S]*?)</${a}>`,"g")))if(Ir(l[1]))s(l.index,"error","braces",`<${a}> has a "{" or "}" that is not a placeholder; write {{ or }} for a literal brace`)}for(let a of t.matchAll(/<img\b[^>]*>/gi)){let l=/\balt\s*=\s*["']([^"']*)["']/i.exec(a[0]);if(!l)s(a.index,"warn","img-alt",'image without alt: describe it, or alt="" if it is decoration');else if(Ys.test(l[1].trim()))s(a.index,"warn","img-alt",`placeholder alt "${l[1]}": describe the image`)}let c=/<title\b|rel\s*=\s*["']canonical["']|property\s*=\s*["']og:|"@type"\s*:\s*"(BreadcrumbList|NewsArticle)"/i.exec(t);if(c)s(c.index,"warn","seo-dup","the platform already writes title, canonical, og:* and Breadcrumb/NewsArticle JSON-LD: remove the duplicate");if(r){let a=[...t.matchAll(/<h1\b/gi)].length,d=(t.match(/<MLIS:NewsDetail\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi)??[]).some((m)=>/\bNewsFormat\s*=\s*"[^"]*\{1\}/i.test(m))?1:0,f=a+d;if(f===0)s(0,"warn","h1",'page has no <h1>: give it one (e.g. CatShow CatFormat="<h1>{1}</h1>")');else if(f>1)s(0,"warn","h1",`page has ${f} <h1> (NewsDetail {1} is already an h1): keep one, use h2/h3 for sections`)}}return n}var Xs,Ys;var Dr=b(()=>{Xs=["ListFormat","NewsFormat","OtherFormat","CatFormat","HyperFormat","EventFormat","EventItemFormat","AttrFormat","CatName","Author","HitCounter"],Ys=/^(card image cap|smaple image|sample image|image|img|ảnh|hình|photo|picture|banner|logo\d*)$/i});function ze(e){let t=e.replace(/^/,"").replace(/\r\n/g,`
22
+ `:"")}},ti={name:"category ls",usage:"mlis category ls <CatID> [--refresh]",summary:"Child categories of one category with their Location1-3 (what NewsList Location=N picks)",options:Ke,async run(e){let t=await Ve(e),r=Bt(e,t);if(h(P(t.childrenOf(r.id).map((n)=>dn(n)),e.render)),!e.json)g(`# ${t.pathText(r.id)}`)}},ri={name:"category show",usage:"mlis category show <CatID> [--refresh]",summary:"Every setting of one category, its path and the domain whose tree holds it",options:Ke,async run(e){let t=await Ve(e),r=Bt(e,t),n={path:t.pathText(r.id),domain:t.domainFor(r.id),typeName:Ue(r.type),link:ne(r.bits,3),newWindow:ne(r.bits,4),rss:ne(r.bits,2),indexAZ:ne(r.bits,5),addNews:ne(r.bits,0)};if(e.json){h(`${JSON.stringify({...r.raw,derived:n})}
23
+ `);return}let s=[...Object.entries(r.raw).map(([o,i])=>({key:o,value:i})),...Object.entries(n).map(([o,i])=>({key:`(${o})`,value:i}))];h(P(s,e.render))}},ni={name:"category find",usage:"mlis category find <text> [--refresh]",summary:"Find categories by name or alias (accents ignored)",options:Ke,async run(e){let t=await Ve(e),r=Ht(C(e,0,"text")),n=t.items.filter((s)=>Ht(`${s.name} ${s.alias}`).includes(r)||String(s.id)===r);h(P(n.map((s)=>({id:s.id,name:s.name,type:Ue(s.type),page:s.pageStyle,path:t.pathText(s.id)})),e.render))}},si={name:"category pages",usage:"mlis category pages [ViewPage folder] [--refresh]",summary:"Which categories use which ViewPage folder as page (PageStyle) or read page (PageRead)",options:Ke,async run(e){let t=await Ve(e),r=e.positionals[0]?Ce(e.positionals[0]).toLowerCase():"",n=[];for(let s of t.items)for(let[o,i]of[["page",s.pageStyle],["read",s.pageRead]]){if(!i)continue;if(r&&i.toLowerCase()!==r)continue;n.push({folder:i,role:o,id:s.id,name:s.name,domain:t.domainFor(s.id)||Ut(s.link)})}if(n.sort((s,o)=>String(s.folder).localeCompare(String(o.folder))||Number(s.id)-Number(o.id)),h(P(n,e.render)),!e.json&&r&&n.length===0)g(`# no category uses ${r}`)}},un=[Qo,ei,ti,ri,ni,si]});function pt(e){let t=e.replace(/^/,"").replace(/\r\n/g,`
24
24
  `).split(`
25
- `);if(t.at(-1)==="")t.pop();return t}function Or(e,t){let r=0;while(r<e.length&&r<t.length&&e[r]===t[r])r++;let n=e.length,s=t.length;while(n>r&&s>r&&e[n-1]===t[s-1])n--,s--;let o=e.slice(0,r).map((m)=>({kind:" ",text:m})),i=e.slice(n).map((m)=>({kind:" ",text:m})),c=e.slice(r,n),a=t.slice(r,s),l=c.length,d=a.length,f;if(l*d>25000000)f=[...c.map((m)=>({kind:"-",text:m})),...a.map((m)=>({kind:"+",text:m}))];else{let m=d+1,w=new Uint32Array((l+1)*m);for(let I=l-1;I>=0;I--)for(let R=d-1;R>=0;R--)w[I*m+R]=c[I]===a[R]?w[(I+1)*m+R+1]+1:Math.max(w[(I+1)*m+R],w[I*m+R+1]);f=[];let p=0,S=0;while(p<l&&S<d)if(c[p]===a[S])f.push({kind:" ",text:c[p]}),p++,S++;else if(w[(p+1)*m+S]>=w[p*m+S+1])f.push({kind:"-",text:c[p++]});else f.push({kind:"+",text:a[S++]});while(p<l)f.push({kind:"-",text:c[p++]});while(S<d)f.push({kind:"+",text:a[S++]})}return[...o,...f,...i]}function Ge(e,t,r,n){let s=Or(ze(e),ze(t));if(!s.some((c)=>c.kind!==" "))return"";let o=[`--- ${r}`,`+++ ${n}`],i=0;while(i<s.length){while(i<s.length&&s[i].kind===" ")i++;if(i>=s.length)break;let c=Math.max(0,i-3),a=i;while(a<s.length){if(s[a].kind!==" "){a++;continue}let p=a;while(p<s.length&&s[p].kind===" ")p++;if(p>=s.length||p-a>6){a=Math.min(s.length,a+3);break}a=p}let l=1,d=1;for(let p=0;p<c;p++){if(s[p].kind!=="+")l++;if(s[p].kind!=="-")d++}let f=s.slice(c,a),m=f.filter((p)=>p.kind!=="+").length,w=f.filter((p)=>p.kind!=="-").length;o.push(`@@ -${l},${m} +${d},${w} @@`);for(let p of f)o.push(`${p.kind}${p.text}`);i=a}return`${o.join(`
25
+ `);if(t.at(-1)==="")t.pop();return t}function fn(e,t){let r=0;while(r<e.length&&r<t.length&&e[r]===t[r])r++;let n=e.length,s=t.length;while(n>r&&s>r&&e[n-1]===t[s-1])n--,s--;let o=e.slice(0,r).map((u)=>({kind:" ",text:u})),i=e.slice(n).map((u)=>({kind:" ",text:u})),c=e.slice(r,n),l=t.slice(r,s),a=c.length,d=l.length,f;if(a*d>25000000)f=[...c.map((u)=>({kind:"-",text:u})),...l.map((u)=>({kind:"+",text:u}))];else{let u=d+1,w=new Uint32Array((a+1)*u);for(let R=a-1;R>=0;R--)for(let I=d-1;I>=0;I--)w[R*u+I]=c[R]===l[I]?w[(R+1)*u+I+1]+1:Math.max(w[(R+1)*u+I],w[R*u+I+1]);f=[];let p=0,S=0;while(p<a&&S<d)if(c[p]===l[S])f.push({kind:" ",text:c[p]}),p++,S++;else if(w[(p+1)*u+S]>=w[p*u+S+1])f.push({kind:"-",text:c[p++]});else f.push({kind:"+",text:l[S++]});while(p<a)f.push({kind:"-",text:c[p++]});while(S<d)f.push({kind:"+",text:l[S++]})}return[...o,...f,...i]}function ke(e,t,r,n){let s=fn(pt(e),pt(t));if(!s.some((c)=>c.kind!==" "))return"";let o=[`--- ${r}`,`+++ ${n}`],i=0;while(i<s.length){while(i<s.length&&s[i].kind===" ")i++;if(i>=s.length)break;let c=Math.max(0,i-3),l=i;while(l<s.length){if(s[l].kind!==" "){l++;continue}let p=l;while(p<s.length&&s[p].kind===" ")p++;if(p>=s.length||p-l>6){l=Math.min(s.length,l+3);break}l=p}let a=1,d=1;for(let p=0;p<c;p++){if(s[p].kind!=="+")a++;if(s[p].kind!=="-")d++}let f=s.slice(c,l),u=f.filter((p)=>p.kind!=="+").length,w=f.filter((p)=>p.kind!=="-").length;o.push(`@@ -${a},${u} +${d},${w} @@`);for(let p of f)o.push(`${p.kind}${p.text}`);i=l}return`${o.join(`
26
26
  `)}
27
- `}function Mr(e,t){let r=Or(ze(e),ze(t));return{added:r.filter((n)=>n.kind==="+").length,removed:r.filter((n)=>n.kind==="-").length}}import{mkdir as eo,readdir as to,writeFile as ro}from"node:fs/promises";import{dirname as no,join as bt}from"node:path";function St(e=new Date){let t=(r)=>String(r).padStart(2,"0");return`${e.getFullYear()}${t(e.getMonth()+1)}${t(e.getDate())}_${t(e.getHours())}${t(e.getMinutes())}${t(e.getSeconds())}`}function Pt(e){let t=L(e),r=J(e)?t.slice(t.length-J(e).length):"";return{base:t.slice(0,t.length-r.length),ext:r}}function oo(e,t,r,n=St()){let{base:s,ext:o}=Pt(t),i=P(t);return bt(e.historyRoot,...i?i.split("/"):[],`${s}_${n}${r?`_${r}`:""}${o}`)}async function V(e,t,r,n){let s=oo(e,t,n);await eo(no(s),{recursive:!0});let o=s.slice(s.length-Pt(t).ext.length),i=s.slice(0,s.length-o.length);for(let c=1;c<=so;c++){let a=c===1?s:`${i}_${c}${o}`;try{return await ro(a,r,{flag:"wx"}),a}catch(l){if(l.code!=="EEXIST")throw l}}throw Error(`too many history copies of ${t} in one second`)}async function Lt(e,t){let{base:r,ext:n}=Pt(t),s=bt(e.historyRoot,...P(t)?P(t).split("/"):[]),o;try{o=await to(s)}catch{return[]}let i=(l)=>l.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),c=new RegExp(`^${i(r)}_(\\d{8}_\\d{6})(?:_(.+))?${i(n)}$`,"i"),a=[];for(let l of o){let d=c.exec(l);if(d)a.push({stamp:d[1],label:d[2]??"",file:bt(s,l)})}return a.sort((l,d)=>d.stamp.localeCompare(l.stamp)||d.file.localeCompare(l.file))}var so=50;var Re=b(()=>{U()});async function q(e,t){try{return await e.api().list(t)}catch(r){if(r instanceof M&&r.notFound)return null;throw r}}async function xe(e,t){if(T(t))return Buffer.from((await e.api().read(t)).Content,"utf-8");return e.api().download(t)}function G(e){return Buffer.from(e).toString("utf-8").replace(/^/,"")}async function Ar(e,t,r,n){let s=e.api();if(T(t)){if(!n)await s.createFile(P(t),L(t));await s.save(t,G(r));return}if(r.length>be)throw new u(`${t} is ${r.length} bytes; the server accepts at most ${be}`);await s.upload(P(t),L(t),r,!0)}async function _r(e,t){let r=new Map;if(t&&B(t)){let o=(await q(e,P(t)))?.find((i)=>i.Name.toLowerCase()===L(t).toLowerCase());if(o)r.set(o.Path.toLowerCase(),o);return r}let n=await br(e,t),s=await ee(n,Nt,(o)=>e.api().list(o));for(let o of s.flat())if(B(o.Path))r.set(o.Path.toLowerCase(),o);return r}function Rt(e,t=0){return e.workspace().resolveArg(e.positionals[t])}function jr(e,t,r){h(N(e.values.all===!0?r:t,e.render))}async function It(e,t){let r=[],n=await e.localFiles(t),s=new Set;for(let o of n){s.add(o.toLowerCase());let i=await e.entry(o),c=await e.readLocalBytes(o);r.push({path:i?.path??o,local:!i?"new":F(o,c)===i.hash?"unchanged":"changed"})}for(let o of await e.entries(t))if(!s.has(o.path.toLowerCase()))r.push({path:o.path,local:"missing"});return r.sort((o,i)=>o.path.localeCompare(i.path))}async function lo(e,t,r){let n=e.workspace(),s=await n.readLocalBytes(t);if(s===null)throw new u(`${t} does not exist in the workspace (${n.localPath(t)})`);let o=F(t,s),i=await n.entry(t),c=await q(e,P(t)),a=c?.some((w)=>w.Name.toLowerCase()===L(t).toLowerCase())??!1,l={rel:t,local:s,localHash:o,parentMissing:c===null};if(!a)return{...l,action:"create",conflict:i&&!r?"deleted on the server since your last pull":void 0};let d=await xe(e,t),f=F(t,d);if(f===o)return{...l,action:"unchanged",server:d};let m;if(!r&&(!i||i.hash!==f))m=i?"changed on the server since your last pull":"exists on the server but was never pulled";return{...l,action:"save",server:d,conflict:m}}async function qe(e,t){let r=t.split("/").filter(Boolean),n=new Set(Be(await e.api().tree(),"").map((s)=>s.toLowerCase()));for(let s=0;s<r.length;s++){let o=r.slice(0,s+1).join("/");if(n.has(o.toLowerCase()))continue;if(s===0)throw new u(`site folder ${r[0]} does not exist on the server; the API cannot create top-level folders`);try{await e.api().createDirectory(r.slice(0,s).join("/"),r[s])}catch(i){if(!(i instanceof M&&i.status===409))throw i}}}async function co(e){let t=e.workspace();if(e.positionals.length===0)throw new u("name the files or folders to push (see: mlis status), e.g. mlis push LHUENB4/@HomePage");let r=new Set;for(let n of e.positionals){let s=t.resolveArg(n);if(B(s))r.add(s);else if(!s||await t.isLocalDir(s)){for(let o of await It(t,s))if(o.local==="new"||o.local==="changed")r.add(o.path)}else throw new u(`${n}: this file type cannot be stored in ViewPage (see: mlis skill ref viewpage-structure)`)}return[...r]}var Nt=4,xt,io,ao,uo,Ur;var me=b(()=>{A();x();O();te();Re();U();Ke();xt={name:"pull",usage:"mlis pull [folder|file] [--force] [--all]",summary:"Download files (code, images, fonts...) into the workspace; local edits are kept unless --force",options:{force:{type:"boolean"},all:{type:"boolean"}},async run(e){let t=e.workspace(),r=Rt(e),n=k(e.values,"force"),s=await _r(e,r),o=await t.entries(r);if(s.size===0&&o.length>0&&!n)throw new E(`the server returned no files under "${r||"ViewPage"}" but ${o.length} were pulled before; check the url/token, or pass --force to mirror the empty answer`);let i=[...s.values()],c=await ee(i,Nt,(m)=>xe(e,m.Path)),a=[];try{for(let[m,w]of i.entries()){let p=w.Path,S=c[m],I=F(p,S),R=await t.readLocalBytes(p),$e=await t.entry(p),K;if(R===null)await t.writeLocal(p,S),K="new";else if(F(p,R)===I)K="unchanged";else if(!$e||F(p,R)!==$e.hash){if(!n){a.push({path:p,action:"kept-local",note:"local edits; push them, or pull --force"});continue}await V(t,p,R,"local"),await t.writeLocal(p,S),K="overwritten"}else await t.writeLocal(p,S),K="updated";await t.record({path:p,hash:I,size:w.Size,lastModified:w.LastModified}),a.push({path:p,action:K,note:K==="overwritten"?"local copy saved in _history":""})}for(let m of o){if(s.has(m.path.toLowerCase()))continue;let w=await t.readLocalBytes(m.path);if(await t.forget(m.path),w===null)continue;if(F(m.path,w)===m.hash)await V(t,m.path,w,"deleted-on-server"),await t.removeLocal(m.path),a.push({path:m.path,action:"removed",note:"deleted on server; copy in _history"});else a.push({path:m.path,action:"orphan",note:"deleted on server; local edits kept"})}}finally{await t.saveState()}let l=a.filter((m)=>m.action!=="unchanged");jr(e,l,a);let d=new Map;for(let m of a)d.set(String(m.action),(d.get(String(m.action))??0)+1);let f=[...d].map(([m,w])=>`${w} ${m}`).join(", ")||"nothing";g(`# ${i.length} server files → ${f} in ${t.localPath(r)}`)}};io={name:"status",usage:"mlis status [folder|file] [--remote] [--all]",summary:"Local files vs the last pull/push: new, changed, missing; --remote also checks the server",options:{remote:{type:"boolean"},all:{type:"boolean"}},async run(e){let t=e.workspace(),r=Rt(e),s=(await It(t,r)).map((i)=>({path:i.path,local:i.local}));if(k(e.values,"remote")){let i=await _r(e,r),c=new Map(s.map((a)=>[String(a.path).toLowerCase(),a]));for(let a of s){let l=await t.entry(String(a.path)),d=i.get(String(a.path).toLowerCase());a.remote=!d?l?"deleted":"absent":!l?"exists":d.Size===l.size&&d.LastModified===l.lastModified?"unchanged":"changed"}for(let a of i.values())if(!c.has(a.Path.toLowerCase()))s.push({path:a.Path,local:"absent",remote:"not pulled"})}let o=s.filter((i)=>i.local!=="unchanged"||i.remote&&i.remote!=="unchanged");if(jr(e,o,s),!e.json&&s.length===0)g("# nothing here yet; run: mlis pull <folder>");else if(!e.json&&o.length===0)g("# everything matches the last sync")}},ao={name:"diff",usage:"mlis diff [folder|file] [--stat]",summary:"Line diff of local files against the server (images/fonts: only whether they differ)",options:{stat:{type:"boolean"}},async run(e){let t=e.workspace(),r=Rt(e),n=B(r)?[r]:(await It(t,r)).filter((l)=>l.local==="new"||l.local==="changed").map((l)=>l.path);if(n.length===0){g("# no local changes");return}let s=new Map,o=async(l)=>{let d=P(l);if(!s.has(d))s.set(d,q(e,d));return(await s.get(d))?.some((f)=>f.Name.toLowerCase()===L(l).toLowerCase())??!1},i=k(e.values,"stat"),c=[],a="";for(let l of n){let d=await t.readLocalBytes(l);if(d===null)throw new u(`${l} is not in the workspace; run: mlis pull ${l}`);let f=await o(l),m=f?await xe(e,l):Buffer.alloc(0);if(!T(l)){let w=!f?"absent":F(l,m)===F(l,d)?"same":"differs";if(i)c.push({path:l,added:"",removed:"",server:w});else if(w!=="same")a+=`Binary file ${l}: server ${w==="absent"?"has no copy":"copy differs"}
28
- `;continue}if(i)c.push({path:l,...Mr(G(m),G(d)),server:f?"exists":"absent"});else a+=Ge(G(m),G(d),`server/${l}`,`local/${l}`)}if(i)h(N(c,e.render));else h(a);if(!i&&a==="")g("# local files match the server")}};uo={name:"push",usage:"mlis push <file|folder>... [--dry-run] [--force]",summary:"Upload local changes; stops (exit 3) if the server copy changed since the last pull",options:{force:{type:"boolean"},"dry-run":{type:"boolean"}},async run(e){let t=e.workspace(),r=e.api(),n=k(e.values,"force"),s=await co(e);if(s.length===0){g("# nothing to push: no new or changed files");return}let o=await ee(s,Nt,(l)=>lo(e,l,n)),i=o.filter((l)=>l.conflict);if(i.length>0){let l=i.map((d)=>` ${d.rel}: ${d.conflict}`).join(`
27
+ `}function pn(e,t){let r=fn(pt(e),pt(t));return{added:r.filter((n)=>n.kind==="+").length,removed:r.filter((n)=>n.kind==="-").length}}function ze(e){return`${Je}/${e}.html`}function Jt(e){return Be.filter((t,r)=>ne(e,r))}function Se(e){let t=0;for(let r of e){let n=Be.findIndex((s)=>s.toLowerCase()===r.toLowerCase());if(n<0)throw Error(`unknown option "${r}"; options: ${Be.join(", ")}`);t|=1<<n}return t}function hn(e,t){for(let r of gn[e]??[])t&=~(1<<r);return t&63}function wn(e,t,r){return{catId:oe(e.CategoryID),parentId:r,type:oe(e.CatType,2),name:se(e.CatName),alias:se(e.CatAlias),description:se(e.CatDescription),picture:se(e.CatPicture),icon:se(e.CatIcon),link:se(e.LinkURL),pageStyle:se(e.PageStyle),pageRead:se(e.PageRead),locations:[oe(e.Location1),oe(e.Location2),oe(e.Location3)],sort:oe(e.CatSort,1),homePage:oe(e.HomePage,1),options:Jt(oe(e.BitOption)),doc:t}}function be(e){return/[\r\n]|^\s|\s$|^"/.test(e)?JSON.stringify(e):e}function zt(e,t={}){let r=(gn[e.type]??[]).map((s)=>Be[s]),n=[We,`catId: ${e.catId}`,`parentId: ${e.parentId}`,`type: ${e.type}`,`name: ${be(e.name)}`,`alias: ${be(e.alias)}`,`description: ${be(e.description)}`,`picture: ${be(e.picture)}`,`icon: ${be(e.icon)}`,`link: ${be(e.link)}`,`pageStyle: ${e.pageStyle}`,`pageRead: ${e.pageRead}`,`locations: ${e.locations.join(", ")}`,`sort: ${e.sort}`,`homePage: ${e.homePage}`,`options: ${e.options.join(", ")}`,"# type: 0 Ẩn · 1 Trang chủ · 2 Thư mục · 3 Thông báo · 4 Sự kiện · 5 Giới thiệu · 6 Thư viện ảnh · 7 Liên kết · 8 Tài liệu HTML","# link: trống = trang của thư mục · http(s)://… = liên kết ngoài · {ten-mien} = trang chủ của tên miền (nhiều: {a}{b})","# pageStyle/pageRead: thư mục ViewPage cấp 2-3, không bắt đầu bằng _ (trang thư mục / trang đọc tin)","# locations: Vị trí 1,2,3 (0-255) — control của thư mục cha với Location=n lấy các thư mục con có vị trí n","# homePage: 0 = chính nó là trang chủ; 1 = thư mục cha; 2 = ông nội… · sort: thứ tự (0-255)",`# options: ${Be.map((s,o)=>`${s} (${oi[o]})`).join(", ")}${r.length?` — kiểu ${e.type} luôn bỏ: ${r.join(", ")}`:""}`,"# parentId: đổi = chuyển thư mục (như kéo thả trong /admin)"];if(t.path)n.push(`# đường dẫn: ${t.path}`);if(t.created)n.push(`# tạo: ${t.created}`);return n.push(e.doc===null?"# Tài liệu HTML: tài khoản không có quyền sửa (nội dung bên dưới bị bỏ qua)":"# bên dưới: Tài liệu HTML của thư mục (CatShow {5})"),n.push(gt),`${n.join(`\r
28
+ `)}\r
29
+ ${(e.doc??"").replace(/\r?\n/g,`\r
30
+ `)}\r
31
+ `}function Gt(e,t="category"){let r=e.replace(/^/,""),n=r.indexOf(gt,We.length);if(!r.startsWith(We)||n<0)throw Error(`${t}: must start with a ${We} … ${gt} header`);let s=new Map,o=!1;for(let a of r.slice(We.length,n).split(/\r?\n/)){let d=a.trim();if(d.startsWith("# Tài liệu HTML: tài khoản không có quyền"))o=!0;if(!d||d.startsWith("#"))continue;let f=d.indexOf(":");if(f<=0)throw Error(`${t}: header line "${d}" is not "key: value"`);let u=d.slice(f+1).trim();if(u.startsWith('"'))try{u=JSON.parse(u)}catch{throw Error(`${t}: ${d.slice(0,f)} starts with " but is not a valid JSON string`)}s.set(d.slice(0,f).trim(),u)}let i=(a,d=0,f=255)=>{let u=Number(s.get(a)??"");if(!Number.isInteger(u)||u<d||u>f)throw Error(`${t}: ${a} must be a whole number from ${d} to ${f}`);return u},c=(s.get("locations")??"0, 0, 0").split(",").map((a)=>Number(a.trim()));if(c.length!==3||c.some((a)=>!Number.isInteger(a)||a<0||a>255))throw Error(`${t}: locations must be three numbers 0-255, e.g. "10, 0, 0"`);let l=(s.get("options")??"").split(",").map((a)=>a.trim()).filter(Boolean);return Se(l),{catId:i("catId",0,2147483648),parentId:i("parentId",0,2147483648),type:i("type",0,8),name:s.get("name")??"",alias:s.get("alias")??"",description:s.get("description")??"",picture:s.get("picture")??"",icon:s.get("icon")??"",link:s.get("link")??"",pageStyle:s.get("pageStyle")??"",pageRead:s.get("pageRead")??"",locations:c,sort:i("sort"),homePage:i("homePage"),options:l,doc:o?null:r.slice(n+gt.length).replace(/^\r?\n/,"").replace(/\r?\n$/,"")}}function qt(e,t,r){let n={CatID:e.catId};if(!t)n.ParentID=e.parentId;let s=(o,i,c)=>{if(!t||(typeof i==="string"?!yn(i,String(c)):i!==c))n[o]=i};if(s("CatType",e.type,t?.type),s("CatName",e.name,t?.name),s("CatAlias",e.alias,t?.alias),s("CatDescription",e.description,t?.description),s("CatPicture",e.picture,t?.picture),s("CatIcon",e.icon,t?.icon),s("LinkURL",e.link,t?.link),s("PageStyle",e.pageStyle,t?.pageStyle),s("PageRead",e.pageRead,t?.pageRead),e.locations.forEach((o,i)=>s(`Location${i+1}`,o,t?.locations[i])),s("CatSort",e.sort,t?.sort),s("HomePage",e.homePage,t?.homePage),s("BitOption",Se(e.options),t?Se(t.options):void 0),r&&Object.keys(n).length>1)n.Version=r;return n}function Cn(e,t){return e.doc!==null&&t.doc!==null&&!yn(e.doc,t.doc)}var Je="_categories",We="<!--mlis-category",gt="-->",Be,oi,gn,se=(e)=>e===null||e===void 0?"":String(e),oe=(e,t=0)=>{let r=Number(e);return Number.isFinite(r)?r:t},yn=(e,t)=>e.replace(/\r\n/g,`
32
+ `)===t.replace(/\r\n/g,`
33
+ `);var $n=k(()=>{He();Be=["addNews","newsSort","rss","link","newWindow","indexAZ"],oi=["Cho thêm bài viết","Cho sắp xếp bài viết","RSS","Cho liên kết","Mở cửa sổ mới","Index A-Z"],gn={0:[3,4,2,5],5:[2],6:[2],7:[0,1,2],8:[0,1,2,3,4,5]}});import{mkdir as ii,readdir as ai,writeFile as li}from"node:fs/promises";import{dirname as ci,join as Xt}from"node:path";function Ge(e=new Date){let t=(r)=>String(r).padStart(2,"0");return`${e.getFullYear()}${t(e.getMonth()+1)}${t(e.getDate())}_${t(e.getHours())}${t(e.getMinutes())}${t(e.getSeconds())}`}function Yt(e){let t=L(e),r=z(e)?t.slice(t.length-z(e).length):"";return{base:t.slice(0,t.length-r.length),ext:r}}function ui(e,t,r,n=Ge()){let{base:s,ext:o}=Yt(t),i=x(t);return Xt(e.historyRoot,...i?i.split("/"):[],`${s}_${n}${r?`_${r}`:""}${o}`)}async function A(e,t,r,n){let s=ui(e,t,n);await ii(ci(s),{recursive:!0});let o=s.slice(s.length-Yt(t).ext.length),i=s.slice(0,s.length-o.length);for(let c=1;c<=di;c++){let l=c===1?s:`${i}_${c}${o}`;try{return await li(l,r,{flag:"wx"}),l}catch(a){if(a.code!=="EEXIST")throw a}}throw Error(`too many history copies of ${t} in one second`)}async function Zt(e,t){let{base:r,ext:n}=Yt(t),s=Xt(e.historyRoot,...x(t)?x(t).split("/"):[]),o;try{o=await ai(s)}catch{return[]}let i=(a)=>a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),c=new RegExp(`^${i(r)}_(\\d{8}_\\d{6})(?:_(.+))?${i(n)}$`,"i"),l=[];for(let a of o){let d=c.exec(a);if(d)l.push({stamp:d[1],label:d[2]??"",file:Xt(s,a)})}return l.sort((a,d)=>d.stamp.localeCompare(a.stamp)||d.file.localeCompare(a.file))}var di=50;var Pe=k(()=>{K()});import{createHash as mi}from"node:crypto";import{mkdir as fi,readFile as pi,rename as gi,writeFile as hi}from"node:fs/promises";import{dirname as wi,join as yi}from"node:path";function Ze(e){return`${Ye}/${e}.html`}function er(e){return Ci[e??0]??"news"}function _(e){return mi("sha1").update(e.replace(/^/,"").replace(/\r\n/g,`
34
+ `)).digest("hex")}function Qt(e){let t=/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(e??"");return t?`${t[1]} ${t[2]}`:""}function qe(e){return/[\r\n]|^\s|\s$|^"/.test(e)?JSON.stringify(e):e}function tr(e){let t={newsId:e.NewsID,catId:e.CategoryID,title:e.NewsTitle??"",intro:e.NewsIntro??"",images:e.NewsImages??"",keyword:e.NewsKeyword??"",source:e.NewsSource??"",content:e.NewsContent??""};if(e.CatType===4)t.orderTime=Qt(e.OrderTime);return t}function rr(e,t){let r=[Xe,`newsId: ${e.newsId}`,`catId: ${e.catId}`,`title: ${qe(e.title)}`],n=er(t?.CatType);if(n!=="event")r.push(`intro: ${qe(e.intro)}`);if(r.push(`images: ${qe(e.images)}`),n==="event")r.push(`orderTime: ${e.orderTime??""}`);else r.push(`keyword: ${qe(e.keyword)}`);if(r.push(`source: ${qe(e.source)}`),t){if(r.push(`# ${n} trong thư mục ${t.CatName??e.catId}`),t.CreateTime)r.push(`# tạo: ${Qt(t.CreateTime)} ${t.CreateUser??""}`.trimEnd());if(t.UpdateUser)r.push(`# sửa: ${Qt(t.UpdateTime)} ${t.UpdateUser}`);if(n==="event")r.push("# sự kiện: orderTime là thời gian diễn ra (yyyy-MM-dd HH:mm); không có tóm tắt/từ khoá");if(n==="gallery")r.push("# thư viện ảnh: chỉ đọc, sửa trong /admin")}return r.push(ht),`${r.join(`\r
35
+ `)}\r
36
+ ${e.content.replace(/\r?\n/g,`\r
37
+ `)}\r
38
+ `}function nr(e,t="article"){let r=e.replace(/^/,""),n=r.indexOf(Xe),s=r.indexOf(ht,n+Xe.length);if(n!==0||s<0)throw Error(`${t}: must start with a ${Xe} … ${ht} header`);let o=new Map;for(let l of r.slice(Xe.length,s).split(/\r?\n/)){let a=l.trim();if(!a||a.startsWith("#"))continue;let d=a.indexOf(":");if(d<=0)throw Error(`${t}: header line "${a}" is not "key: value"`);let f=a.slice(0,d).trim(),u=a.slice(d+1).trim();if(u.startsWith('"'))try{u=JSON.parse(u)}catch{throw Error(`${t}: ${f} starts with " but is not a valid JSON string`)}o.set(f,u)}let i=(l)=>{let a=Number(o.get(l)??"");if(!Number.isInteger(a)||a<0)throw Error(`${t}: ${l} must be a number`);return a},c={newsId:i("newsId"),catId:i("catId"),title:o.get("title")??"",intro:o.get("intro")??"",images:o.get("images")??"",keyword:o.get("keyword")??"",source:o.get("source")??"",content:r.slice(s+ht.length).replace(/^\r?\n/,"").replace(/\r?\n$/,"")};if(o.has("orderTime"))c.orderTime=o.get("orderTime");return c}function sr(e,t,r){let n={NewsID:e.newsId,CatID:e.catId},s=(o,i,c)=>{if(!r||!vn(i,c))n[o]=i??""};if(s("NewsTitle",e.title,r?.title),s("NewsIntro",e.intro,r?.intro),s("NewsContent",e.content,r?.content),s("NewsImages",e.images,r?.images),s("NewsKeyword",e.keyword,r?.keyword),s("NewsSource",e.source,r?.source),e.orderTime&&(!r||!vn(e.orderTime,r.orderTime)))n.OrderTime=e.orderTime;if(t)n.Version=t;return n}class me{ws;name;entries=null;constructor(e,t="news"){this.ws=e;this.name=t}get path(){return yi(this.ws.root,"_mlis",`${this.name}.json`)}async load(){if(this.entries)return this.entries;try{this.entries=JSON.parse(await pi(this.path,"utf-8"))}catch(e){if(e.code!=="ENOENT")throw e;this.entries={}}return this.entries}async get(e){return(await this.load())[String(e)]}async set(e,t,r){(await this.load())[String(e)]={newsId:e,version:t,hash:_(r),syncedAt:new Date().toISOString()}}async save(){if(!this.entries)return;await fi(wi(this.path),{recursive:!0});let e=`${this.path}.${process.pid}.tmp`;await hi(e,`${JSON.stringify(this.entries,null,1)}
39
+ `,"utf-8"),await gi(e,this.path)}}var Ye="_news",Xe="<!--mlis-news",ht="-->",Ci,vn=(e,t)=>(e??"").replace(/\r\n/g,`
40
+ `)===(t??"").replace(/\r\n/g,`
41
+ `);var or=k(()=>{Ci={4:"event",6:"gallery"}});import{rm as $i}from"node:fs/promises";import{isAbsolute as vi,relative as ki,resolve as bi,sep as Si}from"node:path";function bn(e,t){let r=Number(e);if(e===void 0||!Number.isInteger(r)||r<0)throw new m(`${t} must be a number`);return r}async function et(e,t=!1){return(await ve(e.workspace(),()=>e.api(),t)).index}async function Qe(e,t,r,n){let s=e.api(),o=await s.folder(r),i=null;try{i=await s.folderDoc(r)}catch(d){if(!(d instanceof T&&(d.status===404||d.status===403)))throw d}let c=wn(o,i?i.HTMLDoc:null,n??t.byId.get(r)?.parentId??0),l=/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(String(o.CreateTime??"")),a={path:t.pathText(r)||void 0,created:l?`${l[1]} ${l[2]} ${o.UserName??""}`.trim():void 0};return{file:c,info:a,folderVersion:o.Version,docVersion:i?.Version,version:`${o.Version}|${i?.Version??"-"}`,text:zt(c,a)}}function Sn(e,t){if(/^\d+$/.test(t))return ze(Number(t));let r=e.workspace(),n=ki(r.root,bi(t)).split(Si).join("/");if(n.startsWith("..")||vi(n)||!n.toLowerCase().startsWith(`${Je.toLowerCase()}/`))throw new m(`${t}: give a CatID or a file in ${r.localPath(Je)}`);return n}function Pi(e){if(e instanceof T&&e.status===409)throw new E("the category changed on the server while pushing; run mlis category diff, then pull or push --force");throw e}var kn="category-files",Ii,xi,Li,Ri,Pn;var In=k(()=>{j();N();M();X();He();$n();Pe();or();Ii={name:"category pull",usage:"mlis category pull <CatID>... [--force]",summary:"Download category settings + HTML document to <workspace>/_categories/<CatID>.html to edit (keeps local edits unless --force)",options:{force:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new m("missing CatID");let t=e.workspace(),r=new me(t,kn),n=await et(e),s=[];for(let o of e.positionals){let i=bn(o,"CatID"),c=await Qe(e,n,i),l=ze(i),a=await t.readLocal(l),d=await r.get(i),f=a===null?"new":"updated";if(a!==null&&_(a)===_(c.text))f="unchanged";else if(a!==null&&(!d||_(a)!==d.hash)){if(!v(e.values,"force")){s.push({path:l,action:"kept-local",note:"local edits; push them, or pull --force"});continue}await A(t,l,a,"local")}if(f!=="unchanged")await t.writeLocal(l,c.text);await r.set(i,c.version,c.text),s.push({path:l,action:f,note:c.file.name})}await r.save(),h(P(s,e.render))}},xi={name:"category diff",usage:"mlis category diff <CatID|file>",summary:"Diff local category settings/HTML document against the server",async run(e){let t=e.workspace(),r=Sn(e,C(e,0,"CatID or file")),n=await t.readLocal(r);if(n===null)throw new m(`${r} is not in the workspace; run: mlis category pull <CatID>`);let s=Gt(n,r);if(s.catId===0){h(n),g("# new category (catId 0): created on push");return}let o=await Qe(e,await et(e),s.catId),i=ke(o.text,n,`server/${s.catId}`,r);if(h(i||""),!i)g("# no difference")}},Li={name:"category push",usage:"mlis category push <CatID|file>... [--dry-run] [--force]",summary:"Save local category files (every form field, parent, HTML document); stops (exit 3) if the category changed on the server since the pull",options:{"dry-run":{type:"boolean"},force:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new m("missing CatID or file");let t=e.workspace(),r=e.api(),n=new me(t,kn),s=v(e.values,"dry-run"),o=v(e.values,"force"),i=await et(e),c=[],l=!1;try{for(let a of e.positionals){let d=Sn(e,a),f=await t.readLocal(d);if(f===null)throw new m(`${d} is not in the workspace`);let u=Gt(f,d);if(u.parentId!==0&&!i.byId.has(u.parentId))throw new m(`${d}: parent category ${u.parentId} does not exist (or is not visible to this account)`);if(u.catId===0){if(s){c.push({path:d,action:"would-create",note:`under ${u.parentId}`});continue}let we=await r.saveFolder(qt(u)),Fe=Number(we.CategoryID);if(u.doc)await r.saveFolderDoc(Fe,u.doc);l=!0;let xt=await Qe(e,i,Fe,u.parentId),Lt=ze(Fe);if(await t.writeLocal(Lt,xt.text),Lt!==d)await $i(t.localPath(d));await n.set(Fe,xt.version,xt.text),c.push({path:Lt,action:"created",note:`CatID ${Fe}`});continue}if(d.toLowerCase()!==ze(u.catId).toLowerCase())throw new m(`${d}: header says catId ${u.catId}; a category file is named <CatID>.html`);let w=await n.get(u.catId),p=await Qe(e,i,u.catId);if(!o&&!w)throw new E(`${d} was never pulled here; run: mlis category pull ${u.catId} (or push --force)`);if(!o&&w&&w.version!==p.version)throw new E(`${d}: the category changed on the server after your pull. See: mlis category diff ${u.catId}; then push --force or pull --force`);let S=qt(u,p.file,o?void 0:p.folderVersion),R=Object.keys(S).filter((we)=>we!=="CatID"&&we!=="Version"),I=u.parentId!==p.file.parentId,q=Cn(u,p.file);if(u.doc!==null&&p.file.doc===null)g(`# ${d}: no right to the HTML document; its content is ignored`);if(I){if(u.parentId===u.catId||i.ancestors(u.parentId).some((we)=>we.id===u.catId))throw new m(`${d}: cannot move category ${u.catId} into itself or one of its sub-categories`)}if(!R.length&&!I&&!q){c.push({path:d,action:"unchanged",note:""});continue}let U=[...R,...I?[`parent ${p.file.parentId}→${u.parentId}`]:[],...q?["HTMLDoc"]:[]].join(", ");if(s){c.push({path:d,action:"would-update",note:U});continue}if(await A(t,d,p.text,"server"),R.length)await r.saveFolder(S);if(q)await r.saveFolderDoc(u.catId,u.doc,o?void 0:p.docVersion);if(I)await r.moveFolder(u.catId,u.parentId);l=!0;let ot=await Qe(e,i,u.catId,u.parentId);await t.writeLocal(d,ot.text),await n.set(u.catId,ot.version,ot.text);let Lr=Jt(Se(u.options)&~hn(u.type,Se(u.options))),Qs=[U,Lr.length?`type ${u.type} drops ${Lr.join(", ")}`:"",_(ot.text)!==_(f)?"file rewritten as saved":""].filter(Boolean).join("; ");c.push({path:d,action:"updated",note:Qs})}}catch(a){Pi(a)}finally{if(await n.save(),l)await cn(t),i=await et(e,!0).catch(()=>i);if(c.length)h(P(c,e.render))}}},Ri={name:"category new",usage:"mlis category new <ParentID> [--name text]",summary:"Start a new category file (catId 0, settings copied from the parent, form defaults) in _categories; mlis category push creates it",options:{name:{type:"string"}},async run(e){let t=e.workspace(),r=bn(C(e,0,"ParentID"),"ParentID"),n=await et(e),s=n.byId.get(r);if(r!==0&&!s)throw new m(`category ${r} does not exist (or is not visible to this account)`);let o={catId:0,parentId:r,type:2,name:b(e.values,"name")??"Thư mục mới",alias:"",description:"",picture:"",icon:"",link:"",pageStyle:s?.pageStyle??"",pageRead:s?.pageRead??"",locations:[0,0,0],sort:1,homePage:1,options:["link","indexAZ"],doc:""},i=`${Je}/new-${r}-${Ge()}.html`;await t.writeLocal(i,zt(o,{path:s?`${n.pathText(r)} › (mới)`:"(mới)"})),h(`${t.localPath(i)}
42
+ `)}},Pn=[Ii,xi,Li,Ri]});function Ln(e,t){let r=1;for(let n=0;n<t&&n<e.length;n++)if(e.charCodeAt(n)===10)r++;return r}function Ti(e){let t=/^\/?viewpage\/(.+)$/i.exec(e.trim().replace(/\\/g,"/"));if(!t)return null;return decodeURIComponent(t[1].split(/[?#]/)[0]).replace(/\/+$/,"")}function Rn(e){let t=[],r=(s,o,i)=>{let c=Ti(i);if(c)t.push({line:Ln(e,s),kind:o,path:c,raw:i})};for(let s of e.matchAll(/\bCssFolder\s*=\s*"([^"]+)"/gi))r(s.index,"folder",s[1]);for(let s of e.matchAll(/\b(?:Src|href)\s*=\s*["']([^"']+)["']/gi))r(s.index,"file",s[1]);for(let s of e.matchAll(/\bsrc\s*=\s*["']([^"']+)["']/g))r(s.index,"file",s[1]);for(let s of e.matchAll(/url\(\s*["']?([^"')]+)["']?\s*\)/gi))r(s.index,"file",s[1]);let n=new Set;return t.filter((s)=>{let o=`${s.line}|${s.kind}|${s.path}`;if(n.has(o))return!1;return n.add(o),!0})}function Di(e){let t=[];for(let r of e.matchAll(/<MLIS:(\w+)/gi)){let n="",s=r.index+r[0].length;for(;s<e.length;s++){let o=e[s];if(n){if(o===n)n=""}else if(o==='"'||o==="'")n=o;else if(o===">")break}t.push({index:r.index,name:r[1],attrs:e.slice(r.index+r[0].length,s)})}return t}function xn(e){let t=e.replace(/\{\{|\}\}/g,"").replace(/\{\d+(,-?\d+)?(:[^}]*)?\}/g,"");return/[{}]/.test(t)}function Nn(e,t,r){let n=[],s=(i,c,l,a)=>n.push({file:e,line:Ln(t,i),level:c,rule:l,message:a});if(e.toLowerCase().endsWith(".ascx")){let i=/<%@\s*Control\b[^%]*CodeFile\s*=/i.exec(t);if(i)s(i.index,"error","codefile","ViewPage controls have no code-behind: remove the <%@ Control … CodeFile=… %> directive");for(let l of Di(t)){if(!/runat\s*=\s*["']server["']/i.test(l.attrs))s(l.index,"error","runat",`<MLIS:${l.name}> needs runat="server"`);let a=l.name.toLowerCase()==="loadpage"?/\bSrc\s*=\s*"([^"]*)"/i.exec(l.attrs)?.[1]?.trim():void 0;if(a!==void 0&&!/\.ascx$/i.test(a))s(l.index,"warn","loadpage-src",`LoadPage Src="${a}" is not an .ascx file, so it loads nothing; for css only, keep CssFolder and drop Src`)}for(let l of Ni){for(let a of t.matchAll(new RegExp(`\\b${l}\\s*=\\s*"([^"]*)"`,"g")))if(xn(a[1]))s(a.index,"error","braces",`${l} has a "{" or "}" that is not a placeholder; write {{ or }} for a literal brace`);for(let a of t.matchAll(new RegExp(`<${l}>([\\s\\S]*?)</${l}>`,"g")))if(xn(a[1]))s(a.index,"error","braces",`<${l}> has a "{" or "}" that is not a placeholder; write {{ or }} for a literal brace`)}for(let l of t.matchAll(/<img\b[^>]*>/gi)){let a=/\balt\s*=\s*["']([^"']*)["']/i.exec(l[0]);if(!a)s(l.index,"warn","img-alt",'image without alt: describe it, or alt="" if it is decoration');else if(Ei.test(a[1].trim()))s(l.index,"warn","img-alt",`placeholder alt "${a[1]}": describe the image`)}let c=/<title\b|rel\s*=\s*["']canonical["']|property\s*=\s*["']og:|"@type"\s*:\s*"(BreadcrumbList|NewsArticle)"/i.exec(t);if(c)s(c.index,"warn","seo-dup","the platform already writes title, canonical, og:* and Breadcrumb/NewsArticle JSON-LD: remove the duplicate");if(r){let l=[...t.matchAll(/<h1\b/gi)].length,d=(t.match(/<MLIS:NewsDetail\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi)??[]).some((u)=>/\bNewsFormat\s*=\s*"[^"]*\{1\}/i.test(u))?1:0,f=l+d;if(f===0)s(0,"warn","h1",'page has no <h1>: give it one (e.g. CatShow CatFormat="<h1>{1}</h1>")');else if(f>1)s(0,"warn","h1",`page has ${f} <h1> (NewsDetail {1} is already an h1): keep one, use h2/h3 for sections`)}}return n}var Ni,Ei;var En=k(()=>{Ni=["ListFormat","NewsFormat","OtherFormat","CatFormat","HyperFormat","EventFormat","EventItemFormat","AttrFormat","CatName","Author","HitCounter"],Ei=/^(card image cap|smaple image|sample image|image|img|ảnh|hình|photo|picture|banner|logo\d*)$/i});async function Z(e,t){try{return await e.api().list(t)}catch(r){if(r instanceof T&&r.notFound)return null;throw r}}async function tt(e,t){if(D(t))return Buffer.from((await e.api().read(t)).Content,"utf-8");return e.api().download(t)}function Y(e){return Buffer.from(e).toString("utf-8").replace(/^/,"")}async function Tn(e,t,r,n){let s=e.api();if(D(t)){if(!n)await s.createFile(x(t),L(t));await s.save(t,Y(r));return}if(r.length>_e)throw new m(`${t} is ${r.length} bytes; the server accepts at most ${_e}`);await s.upload(x(t),L(t),r,!0)}async function Dn(e,t){let r=new Map;if(t&&J(t)){let o=(await Z(e,x(t)))?.find((i)=>i.Name.toLowerCase()===L(t).toLowerCase());if(o)r.set(o.Path.toLowerCase(),o);return r}let n=await sn(e,t),s=await re(n,ir,(o)=>e.api().list(o));for(let o of s.flat())if(J(o.Path))r.set(o.Path.toLowerCase(),o);return r}function ar(e,t=0){return e.workspace().resolveArg(e.positionals[t])}function Fn(e,t,r){h(P(e.values.all===!0?r:t,e.render))}async function cr(e,t){let r=[],n=await e.localFiles(t),s=new Set;for(let o of n){s.add(o.toLowerCase());let i=await e.entry(o),c=await e.readLocalBytes(o);r.push({path:i?.path??o,local:!i?"new":F(o,c)===i.hash?"unchanged":"changed"})}for(let o of await e.entries(t))if(!s.has(o.path.toLowerCase()))r.push({path:o.path,local:"missing"});return r.sort((o,i)=>o.path.localeCompare(i.path))}async function Mi(e,t,r){let n=e.workspace(),s=await n.readLocalBytes(t);if(s===null)throw new m(`${t} does not exist in the workspace (${n.localPath(t)})`);let o=F(t,s),i=await n.entry(t),c=await Z(e,x(t)),l=c?.some((w)=>w.Name.toLowerCase()===L(t).toLowerCase())??!1,a={rel:t,local:s,localHash:o,parentMissing:c===null};if(!l)return{...a,action:"create",conflict:i&&!r?"deleted on the server since your last pull":void 0};let d=await tt(e,t),f=F(t,d);if(f===o)return{...a,action:"unchanged",server:d};let u;if(!r&&(!i||i.hash!==f))u=i?"changed on the server since your last pull":"exists on the server but was never pulled";return{...a,action:"save",server:d,conflict:u}}async function wt(e,t){let r=t.split("/").filter(Boolean),n=new Set(ut(await e.api().tree(),"").map((s)=>s.toLowerCase()));for(let s=0;s<r.length;s++){let o=r.slice(0,s+1).join("/");if(n.has(o.toLowerCase()))continue;if(s===0)throw new m(`site folder ${r[0]} does not exist on the server; the API cannot create top-level folders`);try{await e.api().createDirectory(r.slice(0,s).join("/"),r[s])}catch(i){if(!(i instanceof T&&i.status===409))throw i}}}async function Ai(e){let t=e.workspace();if(e.positionals.length===0)throw new m("name the files or folders to push (see: mlis status), e.g. mlis push LHUENB4/@HomePage");let r=new Set;for(let n of e.positionals){let s=t.resolveArg(n);if(J(s))r.add(s);else if(!s||await t.isLocalDir(s)){for(let o of await cr(t,s))if(o.local==="new"||o.local==="changed")r.add(o.path)}else throw new m(`${n}: this file type cannot be stored in ViewPage (see: mlis skill ref viewpage-structure)`)}return[...r]}var ir=4,lr,Fi,Oi,_i,On;var Ie=k(()=>{j();N();M();X();Pe();K();mt();lr={name:"pull",usage:"mlis pull [folder|file] [--force] [--all]",summary:"Download files (code, images, fonts...) into the workspace; local edits are kept unless --force",options:{force:{type:"boolean"},all:{type:"boolean"}},async run(e){let t=e.workspace(),r=ar(e),n=v(e.values,"force"),s=await Dn(e,r),o=await t.entries(r);if(s.size===0&&o.length>0&&!n)throw new E(`the server returned no files under "${r||"ViewPage"}" but ${o.length} were pulled before; check the url/token, or pass --force to mirror the empty answer`);let i=[...s.values()],c=await re(i,ir,(u)=>tt(e,u.Path)),l=[];try{for(let[u,w]of i.entries()){let p=w.Path,S=c[u],R=F(p,S),I=await t.readLocalBytes(p),q=await t.entry(p),U;if(I===null)await t.writeLocal(p,S),U="new";else if(F(p,I)===R)U="unchanged";else if(!q||F(p,I)!==q.hash){if(!n){l.push({path:p,action:"kept-local",note:"local edits; push them, or pull --force"});continue}await A(t,p,I,"local"),await t.writeLocal(p,S),U="overwritten"}else await t.writeLocal(p,S),U="updated";await t.record({path:p,hash:R,size:w.Size,lastModified:w.LastModified}),l.push({path:p,action:U,note:U==="overwritten"?"local copy saved in _history":""})}for(let u of o){if(s.has(u.path.toLowerCase()))continue;let w=await t.readLocalBytes(u.path);if(await t.forget(u.path),w===null)continue;if(F(u.path,w)===u.hash)await A(t,u.path,w,"deleted-on-server"),await t.removeLocal(u.path),l.push({path:u.path,action:"removed",note:"deleted on server; copy in _history"});else l.push({path:u.path,action:"orphan",note:"deleted on server; local edits kept"})}}finally{await t.saveState()}let a=l.filter((u)=>u.action!=="unchanged");Fn(e,a,l);let d=new Map;for(let u of l)d.set(String(u.action),(d.get(String(u.action))??0)+1);let f=[...d].map(([u,w])=>`${w} ${u}`).join(", ")||"nothing";g(`# ${i.length} server files → ${f} in ${t.localPath(r)}`)}};Fi={name:"status",usage:"mlis status [folder|file] [--remote] [--all]",summary:"Local files vs the last pull/push: new, changed, missing; --remote also checks the server",options:{remote:{type:"boolean"},all:{type:"boolean"}},async run(e){let t=e.workspace(),r=ar(e),s=(await cr(t,r)).map((i)=>({path:i.path,local:i.local}));if(v(e.values,"remote")){let i=await Dn(e,r),c=new Map(s.map((l)=>[String(l.path).toLowerCase(),l]));for(let l of s){let a=await t.entry(String(l.path)),d=i.get(String(l.path).toLowerCase());l.remote=!d?a?"deleted":"absent":!a?"exists":d.Size===a.size&&d.LastModified===a.lastModified?"unchanged":"changed"}for(let l of i.values())if(!c.has(l.Path.toLowerCase()))s.push({path:l.Path,local:"absent",remote:"not pulled"})}let o=s.filter((i)=>i.local!=="unchanged"||i.remote&&i.remote!=="unchanged");if(Fn(e,o,s),!e.json&&s.length===0)g("# nothing here yet; run: mlis pull <folder>");else if(!e.json&&o.length===0)g("# everything matches the last sync")}},Oi={name:"diff",usage:"mlis diff [folder|file] [--stat]",summary:"Line diff of local files against the server (images/fonts: only whether they differ)",options:{stat:{type:"boolean"}},async run(e){let t=e.workspace(),r=ar(e),n=J(r)?[r]:(await cr(t,r)).filter((a)=>a.local==="new"||a.local==="changed").map((a)=>a.path);if(n.length===0){g("# no local changes");return}let s=new Map,o=async(a)=>{let d=x(a);if(!s.has(d))s.set(d,Z(e,d));return(await s.get(d))?.some((f)=>f.Name.toLowerCase()===L(a).toLowerCase())??!1},i=v(e.values,"stat"),c=[],l="";for(let a of n){let d=await t.readLocalBytes(a);if(d===null)throw new m(`${a} is not in the workspace; run: mlis pull ${a}`);let f=await o(a),u=f?await tt(e,a):Buffer.alloc(0);if(!D(a)){let w=!f?"absent":F(a,u)===F(a,d)?"same":"differs";if(i)c.push({path:a,added:"",removed:"",server:w});else if(w!=="same")l+=`Binary file ${a}: server ${w==="absent"?"has no copy":"copy differs"}
43
+ `;continue}if(i)c.push({path:a,...pn(Y(u),Y(d)),server:f?"exists":"absent"});else l+=ke(Y(u),Y(d),`server/${a}`,`local/${a}`)}if(i)h(P(c,e.render));else h(l);if(!i&&l==="")g("# local files match the server")}};_i={name:"push",usage:"mlis push <file|folder>... [--dry-run] [--force]",summary:"Upload local changes; stops (exit 3) if the server copy changed since the last pull",options:{force:{type:"boolean"},"dry-run":{type:"boolean"}},async run(e){let t=e.workspace(),r=e.api(),n=v(e.values,"force"),s=await Ai(e);if(s.length===0){g("# nothing to push: no new or changed files");return}let o=await re(s,ir,(a)=>Mi(e,a,n)),i=o.filter((a)=>a.conflict);if(i.length>0){let a=i.map((d)=>` ${d.rel}: ${d.conflict}`).join(`
29
44
  `);throw new E(`nothing was pushed; ${i.length} file(s) conflict with the server:
30
- ${l}
45
+ ${a}
31
46
  compare with: mlis diff <file>; then either keep yours: mlis push <file> --force (server copy goes to _history)
32
- or take the server's: mlis pull <file> --force (your copy goes to _history) and redo the edit`)}let c=o.filter((l)=>!T(l.rel)&&l.local.length>be);if(c.length>0)throw new u(`larger than ${be/1024/1024} MB, nothing was pushed: ${c.map((l)=>l.rel).join(", ")}`);if(k(e.values,"dry-run")){h(N(o.map((l)=>({path:l.rel,action:l.action})),e.render)),g("# dry run: nothing was written");return}let a=[];try{for(let l of o){let d="";if(l.action==="save"){let m=await V(t,l.rel,l.server,"server");d=`server copy: ${t.relOf(m)??m}`,await Ar(e,l.rel,l.local,!0)}else if(l.action==="create"){if(l.parentMissing)await qe(e,P(l.rel));await Ar(e,l.rel,l.local,!1)}let f=(await r.list(P(l.rel))).find((m)=>m.Name.toLowerCase()===L(l.rel).toLowerCase());await t.record({path:f?.Path??l.rel,hash:l.localHash,size:f?.Size??0,lastModified:f?.LastModified??""}),a.push({path:l.rel,action:l.action==="save"?"saved":l.action==="create"?"created":"unchanged",note:d})}}finally{await t.saveState()}h(N(a,e.render))}},Ur=[xt,io,ao,uo]});function mo(e,t){let r=e.workspace(),n=null,s=new Map,o=async()=>{if(!n){n=new Set;let c=(a)=>{n.add(a.Path.toLowerCase()),a.Children.forEach(c)};(await e.api().tree()).forEach(c)}return n},i=(c)=>{let a=c.toLowerCase();if(!s.has(a))s.set(a,q(e,c).then((l)=>l?new Set(l.map((d)=>d.Name.toLowerCase())):null));return s.get(a)};return async(c,a)=>{if(c==="folder"?await r.isLocalDir(a):await r.readLocalBytes(a)!==null)return!0;if(t)return null;if(c==="folder")return(await o()).has(a.toLowerCase());let l=await i(P(a));if(l===null)return!1;if(l.has(L(a).toLowerCase()))return!0;return[".ascx",".css",".js",".png",".jpg",".jpeg",".gif",".webp",".svg",".ico",".woff",".woff2",".ttf",".pdf"].includes(J(a))?!1:null}}var fo,Hr;var Vr=b(()=>{A();O();Dr();U();me();fo={name:"check",usage:"mlis check [folder|file] [--offline]",summary:"Check local ViewPage files: broken ViewPage references, MLIS tag/format errors, alt text, h1, duplicate SEO tags",options:{offline:{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(e.positionals[0]),n=(await t.localFiles(r)).filter((l)=>[".ascx",".css"].includes(J(l))),s=mo(e,k(e.values,"offline")),o=[],i=0;for(let l of n){let d=G(await t.readLocalBytes(l));o.push(...Fr(l,d,L(l).toLowerCase()==="default.ascx"));for(let f of Tr(d)){let m=await s(f.kind,f.path);if(m===!1)o.push({file:l,line:f.line,level:"error",rule:"ref-missing",message:`${f.kind} ViewPage/${f.path} does not exist`});else if(m===null)i++}}let c=o;h(N(c.map((l)=>({...l})),e.render));let a=c.filter((l)=>l.level==="error").length;if(!e.json)g(`# ${n.length} files checked: ${a} errors, ${c.length-a} warnings${i?`, ${i} references not verified (offline or type the API does not list)`:""}`);return a>0?1:0}},Hr=[fo]});import{randomBytes as po}from"node:crypto";import{readFile as go,stat as ho,writeFile as wo}from"node:fs/promises";import{basename as yo}from"node:path";function bo(e){return e.replace(/&(#x[0-9a-f]+|#[0-9]+|amp|lt|gt|quot|apos);/gi,(t,r)=>{if(r.startsWith("#x"))return String.fromCodePoint(Number.parseInt(r.slice(2),16));if(r.startsWith("#"))return String.fromCodePoint(Number.parseInt(r.slice(1),10));return{amp:"&",lt:"<",gt:">",quot:'"',apos:"'"}[r]??t})}function So(e){let t={};for(let r of e.matchAll(/([A-Za-z][\w:-]*)="([^"]*)"/g))t[r[1]]=bo(r[2]);return t}function Ye(e,t){let r=[];for(let n of e.matchAll(new RegExp(`<${t}\\b([^>]*?)\\/?>`,"g")))r.push(So(n[1]));return r}function se(e,t){let r=Ye(e,"Error")[0];if(!r)throw Error(`CKFinder ${t} returned an invalid response: ${e.slice(0,200)}`);if(r.number!=="0"){let n=r.text||r.message||Po[r.number??""]||"";throw Error(`CKFinder ${t} failed (error ${r.number??"?"})${n?`: ${n}`:""}`)}}function pe(e){if(e.includes("\\")||e.includes("?")||e.includes("#"))throw new u(`invalid CDN path ${JSON.stringify(e)}`);let t=e.replace(/^\/+|\/+$/g,"").split("/").filter(Boolean);for(let r of t)if(r==="."||r===".."||r.startsWith(".")||/[:*"<>|]/.test(r)||[...r].some((n)=>n.charCodeAt(0)<32))throw new u(`invalid CDN path segment ${JSON.stringify(r)}`);return t}function z(e="/"){let t=pe(e);return t.length?`/${t.join("/")}/`:"/"}function Ie(e){let t=pe(e);if(t.length===0||e.endsWith("/"))throw new u("expected a file path, such as banner/logo.png");return`/${t.join("/")}`}function fe(e){let t=pe(e);return t.pop(),z(t.join("/"))}function Kr(e){return pe(e).at(-1)??""}function Xe(e,t,r){return`${e}${t}${r==="folder"?"/":""}`}function Jr(e){if(pe(e).length!==1||e.includes("/"))throw new u(`invalid file or folder name ${JSON.stringify(e)}`)}function zr(e){return e.includes(".")?e.slice(e.lastIndexOf(".")+1).toLowerCase():""}function Gr(e){return ko.includes(zr(e))?"Images":"Files"}function qr(e){if(e===void 0)return;let t=vo.find((r)=>r.toLowerCase()===e.toLowerCase());if(!t)throw new u("--type is Images or Files");return t}class Et{siteUrl;catId;mode;request;cookies=new Map;csrfToken=po(20).toString("hex");resources;seeded=!1;constructor(e,t,r,n,s=fetch){this.siteUrl=e;this.catId=r;this.mode=n;this.request=s;if(!/^\d+$/.test(r))throw new u("CatID must be numeric; set it with: mlis config set cdn-cat <CatID>");this.cookies.set("jwtAuth",t),this.cookies.set("ckCsrfToken",this.csrfToken)}cookieHeader(){return[...this.cookies].map(([e,t])=>`${e}=${t}`).join("; ")}async fetchWithCookies(e,t={}){let r=new Headers(t.headers);r.set("cookie",this.cookieHeader());let n;try{n=await this.request(e,{...t,headers:r,redirect:"manual",signal:AbortSignal.timeout($o)})}catch(s){throw Error(`cannot reach ${String(e)}: ${s.message}`)}for(let s of n.headers.getSetCookie()){let o=s.split(";",1)[0],i=o.indexOf("=");if(i>0)this.cookies.set(o.slice(0,i).trim(),o.slice(i+1))}return n}async seed(){if(this.seeded)return;let e=new URL(Wr,this.siteUrl);e.searchParams.set("CatID",this.catId),e.searchParams.set("mode",this.mode);let t=await this.fetchWithCookies(e);if(await t.text(),!this.cookies.has("ASP.NET_SessionId"))throw Error(`seeding the CKFinder session failed: ${Wr} returned HTTP ${t.status} without a session cookie`);this.seeded=!0}async send(e,t,r,n,s){await this.seed();let o=new URL(Co,this.siteUrl);if(o.searchParams.set("command",e),t){let c=await this.resource(t);o.searchParams.set("type",c.type),o.searchParams.set("currentFolder",z(r)),o.searchParams.set("langCode","vi"),o.searchParams.set("hash",c.hash)}for(let[c,a]of Object.entries(n??{}))o.searchParams.set(c,a);let i=await this.fetchWithCookies(o,{method:s?"POST":"GET",body:s});if(!i.ok)throw Error(`CKFinder ${e} returned HTTP ${i.status}`);return i.text()}postBody(e={}){return new URLSearchParams({...e,ckCsrfToken:this.csrfToken,CKFinderCommand:"true"})}async init(){if(!this.resources){let e=await this.send("Init",null);se(e,"Init"),this.resources=new Map;for(let t of Ye(e,"ResourceType")){if(!t.name||!t.hash||!t.url)continue;this.resources.set(t.name,{type:t.name,url:t.url,maxSize:Number(t.maxSize??0),allowedExtensions:(t.allowedExtensions??"").split(",").filter(Boolean),acl:Number(t.acl??0),hash:t.hash})}if(this.resources.size===0)throw Error("CKFinder Init exposed no resource types")}return[...this.resources.values()].map(({hash:e,...t})=>t)}async resource(e){if(!this.resources)await this.init();let t=this.resources.get(e);if(!t)throw Error(`CKFinder does not expose resource type ${e}`);return t}async urlOf(e,t){return`${(await this.resource(e)).url.replace(/\/?$/,"/")}${pe(Ie(t)).map(encodeURIComponent).join("/")}`}async list(e,t="/"){let r=z(t),n=await this.send("GetFolders",e,r);se(n,"GetFolders");let s=await this.send("GetFiles",e,r);return se(s,"GetFiles"),[...Ye(n,"Folder").map((o)=>({kind:"folder",name:o.name??"",path:Xe(r,o.name??"","folder"),hasChildren:o.hasChildren==="true"})),...Ye(s,"File").map((o)=>({kind:"file",name:o.name??"",path:Xe(r,o.name??"","file"),sizeKb:Number(o.size??0),date:o.date??""}))]}async listRecursive(e,t="/",r=20){let n=[{path:z(t),depth:0}],s=[];while(n.length){let o=n.shift(),i=await this.list(e,o.path);if(s.push(...i),s.length>Br)throw Error(`CDN listing exceeds ${Br} entries`);if(o.depth<r){for(let c of i)if(c.kind==="folder")n.push({path:c.path,depth:o.depth+1})}}return s}async findEntry(e,t){let r=Kr(t).toLowerCase();return(await this.list(e,fe(t))).find((n)=>n.name.toLowerCase()===r)}async requireEntry(e,t,r){let n=await this.findEntry(e,t);if(!n||n.kind!==r)throw new u(`${r} ${t} does not exist in ${e}`);return n}async requireMissing(e,t){if(await this.findEntry(e,t))throw new u(`${t} already exists in ${e}`)}async checkExtension(e,t){let r=await this.resource(e),n=zr(t);if(!n||!r.allowedExtensions.some((s)=>s.toLowerCase()===n))throw new u(`.${n||"?"} is not allowed in ${e}; allowed: ${r.allowedExtensions.join(",")}`)}async mkdir(e,t){let r=z(t);if(r==="/")throw new u("cannot create the root folder");let n=fe(r);if(n!=="/")await this.requireEntry(e,n,"folder");await this.requireMissing(e,r);let s=await this.send("CreateFolder",e,n,{NewFolderName:Kr(r)},this.postBody());return se(s,"CreateFolder"),r}async upload(e,t,r="/",n=yo(t)){let s=z(r);if(s!=="/")await this.requireEntry(e,s,"folder");Jr(n),await this.checkExtension(e,n);let o=await ho(t);if(!o.isFile())throw new u(`${t} is not a regular file`);let{maxSize:i}=await this.resource(e);if(i>0&&o.size>i&&e!=="Images")throw new u(`${t} is ${o.size} bytes; ${e} allows at most ${i}`);let c=new FormData;c.append("upload",new Blob([new Uint8Array(await go(t))]),n),c.append("ckCsrfToken",this.csrfToken);let a=await this.send("FileUpload",e,s,{response_type:"txt"},c),[l,...d]=a.trim().split("|");if(!l||!a.includes("|"))throw Error(`CKFinder FileUpload failed: ${d.join("|")||a.slice(0,200)}`);let f=Xe(s,l,"file");return await this.requireEntry(e,f,"file"),{path:f,warning:d.join("|")}}async rename(e,t,r){Jr(r);let n=await this.findEntry(e,t.replace(/\/$/,""));if(!n)throw new u(`${t} does not exist in ${e}`);let s=n.kind==="folder",o=Xe(fe(n.path),r,s?"folder":"file");if(n.path===o)return o;if(await this.requireMissing(e,o),!s)await this.checkExtension(e,r);let i=s?"RenameFolder":"RenameFile",c=s?{NewFolderName:r}:{fileName:n.name,newFileName:r},a=await this.send(i,e,s?n.path:fe(n.path),c,this.postBody());return se(a,i),o}async deleteFile(e,t){let r=Ie(t),n=await this.requireEntry(e,r,"file"),s=await this.send("DeleteFiles",e,fe(r),void 0,this.postBody({"files[0][name]":n.name,"files[0][type]":e,"files[0][folder]":fe(r)}));se(s,"DeleteFiles")}async deleteFolder(e,t,r=!1){let n=z(t);if(n==="/")throw new u("cannot delete the root folder");if(await this.requireEntry(e,n,"folder"),!r&&(await this.list(e,n)).length>0)throw new E(`${n} is not empty; pass --recursive with --confirm-delete to remove its contents`);let s=await this.send("DeleteFolder",e,n,void 0,this.postBody());se(s,"DeleteFolder")}async download(e,t,r){let n=new URL(await this.urlOf(e,t),this.siteUrl),s=await this.fetchWithCookies(n);if(!s.ok)throw Error(`download returned HTTP ${s.status} for ${n.pathname}`);let o=new Uint8Array(await s.arrayBuffer());return await wo(r,o),o.length}}var Co="/JScript/CKFinder/core/connector/aspx/connector.aspx",Wr="/Admin/CKFinder.aspx",Br=1e4,$o=120000,vo,ko,Po;var Xr=b(()=>{x();vo=["Images","Files"],ko=["gif","jpeg","jpg","png"];Po={"500":"CKFinder is disabled for this request: the token was rejected or the session was not seeded","102":"invalid name","103":"access denied by CKFinder ACL","104":"insufficient permissions","105":"invalid file extension","109":"invalid request","115":"a file or folder with that name already exists","116":"folder not found","117":"file not found"}});import{existsSync as Yr}from"node:fs";import{basename as Lo}from"node:path";function Y(e){let t=v(e.values,"cat")??e.settings.resolve("cdn-cat").value;if(!t)throw new u("which CatID holds the files? pass --cat <CatID> or run: mlis config set cdn-cat <CatID>");let r=v(e.values,"mode")??e.settings.resolve("cdn-mode").value??"folder";if(r!=="folder"&&r!=="news")throw new u('--mode is "folder" or "news"');return new Et(e.settings.url,e.settings.token(),t,r)}function re(e,t="Images"){return qr(v(e.values,"type"))??t}async function No(e,t,r){return{kind:r.kind,path:r.path,...r.kind==="file"?{sizeKb:r.sizeKb??0,date:r.date??"",url:await e.urlOf(t,r.path)}:{hasChildren:r.hasChildren??!1}}}function Zr(e,t){if(!k(e.values,"confirm-delete"))throw new E(`${t} is permanent; pass --confirm-delete once the user has approved this exact item`)}var X,Ro,xo,Io,Eo,To,Fo,Do,Oo,Mo,Qr;var en=b(()=>{A();x();O();Xr();X={cat:{type:"string"},mode:{type:"string"},type:{type:"string"}};Ro={name:"cdn info",usage:"mlis cdn info [--cat <CatID>] [--mode folder|news]",summary:"Show the CKFinder resource types (Images, Files): base URL, size limit, allowed extensions",options:X,async run(e){let t=Y(e),r=await t.init();h(H({catId:t.catId,mode:t.mode,resources:r}))}},xo={name:"cdn list",usage:"mlis cdn list [folder] [--type Images|Files] [--recursive] [--cat <CatID>]",summary:"List folders and files of one resource type (default Images)",options:{...X,recursive:{type:"boolean"}},async run(e){let t=Y(e),r=re(e),n=z(e.positionals[0]),s=k(e.values,"recursive")?await t.listRecursive(r,n):await t.list(r,n),o=[];for(let i of s)o.push(await No(t,r,i));h(N(o,e.render))}},Io={name:"cdn upload",usage:"mlis cdn upload <local-file> [--to folder] [--name new-name] [--type Images|Files] [--cat <CatID>]",summary:"Upload an image or document and print the URL to use in .ascx/.css",options:{...X,to:{type:"string"},name:{type:"string"}},async run(e){let t=C(e,0,"local file");if(!Yr(t))throw new u(`file not found: ${t}`);let r=v(e.values,"name")??Lo(t),n=re(e,Gr(r)),s=Y(e),{path:o,warning:i}=await s.upload(n,t,v(e.values,"to")??"/",r);if(i)g(`# CKFinder: ${i}`);if(!o.endsWith(`/${r}`))g(`# CKFinder renamed the upload to ${o}`);h(H({type:n,path:o,url:await s.urlOf(n,o)}))}},Eo={name:"cdn mkdir",usage:"mlis cdn mkdir <folder> [--type Images|Files]",summary:"Create a folder in a resource type",options:X,async run(e){let t=Y(e);h(H({path:await t.mkdir(re(e),C(e,0,"folder"))}))}},To={name:"cdn rename",usage:"mlis cdn rename <path> <new-name> [--type Images|Files]",summary:"Rename a file or folder (new name only, same folder)",options:X,async run(e){let r=await Y(e).rename(re(e),C(e,0,"path"),C(e,1,"new name"));h(H({path:r}))}},Fo={name:"cdn url",usage:"mlis cdn url <path> [--type Images|Files]",summary:"Print the site-relative and full URL of a file after checking it exists",options:X,async run(e){let t=Y(e),r=re(e),n=Ie(C(e,0,"path")),s=n.slice(n.lastIndexOf("/")+1).toLowerCase();if(!(await t.list(r,n.slice(0,n.lastIndexOf("/")+1))).some((c)=>c.kind==="file"&&c.name.toLowerCase()===s))throw new u(`${n} does not exist in ${r}`);let i=await t.urlOf(r,n);h(H({path:n,url:i,fullUrl:new URL(i,e.settings.url).href}))}},Do={name:"cdn download",usage:"mlis cdn download <path> -o <local-file> [--overwrite] [--type Images|Files]",summary:"Download a file through its public URL",options:{...X,output:{type:"string",short:"o"},overwrite:{type:"boolean"}},async run(e){let t=v(e.values,"output");if(!t)throw new u("pass -o <local-file>");if(Yr(t)&&!k(e.values,"overwrite"))throw new u(`${t} exists; pass --overwrite`);let n=await Y(e).download(re(e),C(e,0,"path"),t);g(`# saved ${n} bytes to ${t}`)}},Oo={name:"cdn rm",usage:"mlis cdn rm <path> --confirm-delete [--type Images|Files]",summary:"Delete a file permanently",options:{...X,"confirm-delete":{type:"boolean"}},async run(e){let t=Ie(C(e,0,"path"));Zr(e,`deleting ${t}`),await Y(e).deleteFile(re(e),t),g(`# deleted ${t}`)}},Mo={name:"cdn rmdir",usage:"mlis cdn rmdir <folder> --confirm-delete [--recursive] [--type Images|Files]",summary:"Delete a folder permanently (non-empty needs --recursive)",options:{...X,"confirm-delete":{type:"boolean"},recursive:{type:"boolean"}},async run(e){let t=z(C(e,0,"folder"));Zr(e,`deleting folder ${t}`),await Y(e).deleteFolder(re(e),t,k(e.values,"recursive")),g(`# deleted ${t}`)}},Qr=[Ro,xo,Io,Eo,To,Fo,Do,Oo,Mo]});function Ao(e){return/^[\w-]+\.[\w-]+\.[\w-]+$/.test(e)}function tn(e){if(!Ue.includes(e))throw new u(`unknown key ${JSON.stringify(e)}; keys: ${Ue.join(", ")}`);return e}var _o,jo,Uo,Ho,rn;var nn=b(()=>{A();x();O();te();We();_o={name:"token set",usage:"mlis token set [--url https://lhu.edu.vn] [--no-verify] (token on stdin, or pasted at the hidden prompt)",summary:"Save the jwtAuth token (from the browser cookie) used by every command",options:{"no-verify":{type:"boolean"}},async run(e){if(e.positionals.length>0)throw new u("the token is never an argument (it would stay in shell history); pipe it or paste it");let t=process.stdin.isTTY?await vr("jwtAuth token: "):await $r();if(t=t.trim().replace(/^jwtAuth=/,"").replace(/;.*$/,""),!t)throw new u("token is empty");if(!Ao(t))throw new u("that does not look like a JWT (three dot-separated parts)");let r=v(e.values,"url")?ct(v(e.values,"url")):e.settings.url;if(!k(e.values,"no-verify")){let s=await new ve(r,t).tree();g(`# token accepted by ${r} (${s.length} top-level ViewPage folders)`)}let n=await ke();await He({...n,token:t,url:r}),g(`# token saved to ${le()}`)}};jo={name:"config",usage:"mlis config",summary:"Show settings and where each value comes from (the token only as (set))",async run(e){let t=Ue.map((r)=>{let{value:n,source:s}=e.settings.resolve(r);return{key:r,value:r==="token"?n?"(set)":"":n??"",source:s}});if(h(N(t,e.render)),!e.json)g(`# file: ${le()}`)}},Uo={name:"config set",usage:"mlis config set <url|workdir|cdn-cat|cdn-mode> <value>",summary:"Change one setting",async run(e){let t=tn(C(e,0,"key"));if(t==="token")throw new u("use: mlis token set (keeps the token out of shell history)");let r=je(t,C(e,1,"value"));await He({...await ke(),[t]:r}),g(`# ${t} = ${r}`)}},Ho={name:"config unset",usage:"mlis config unset <key>",summary:"Remove one setting (unset token to forget it)",async run(e){let t=tn(C(e,0,"key")),{[t]:r,...n}=await ke();await He(n),g(`# ${t} removed`)}},rn=[_o,jo,Uo,Ho]});function Tt(e){let t=J(e);if(t===".css")return"css";if(t===".js")return"js";return L(e).toLowerCase()==="default.ascx"?"page":"control"}function sn(e,t=Tt(e)){let r=P(e),n=e.split("/")[0]??"",s=L(e).replace(/\.[^.]+$/,"");switch(t){case"page":return['<%@ Register TagPrefix="MLIS" Namespace="MLIS" %>',`<MLIS:LoadPage CssFolder="ViewPage/${r}" runat="server" />`,`<MLIS:LoadPage CssFolder="ViewPage/${n}/_default" Src="ViewPage/${n}/_default/Header.ascx" runat="server" />`,"",'<div class="container">'," <%-- Nội dung trang: đặt các control MLIS (NewsList, CatShow, ...) ở đây --%>","</div>","",`<MLIS:LoadPage Src="ViewPage/${n}/_default/Footer.ascx" runat="server" />`,""].join(`\r
47
+ or take the server's: mlis pull <file> --force (your copy goes to _history) and redo the edit`)}let c=o.filter((a)=>!D(a.rel)&&a.local.length>_e);if(c.length>0)throw new m(`larger than ${_e/1024/1024} MB, nothing was pushed: ${c.map((a)=>a.rel).join(", ")}`);if(v(e.values,"dry-run")){h(P(o.map((a)=>({path:a.rel,action:a.action})),e.render)),g("# dry run: nothing was written");return}let l=[];try{for(let a of o){let d="";if(a.action==="save"){let u=await A(t,a.rel,a.server,"server");d=`server copy: ${t.relOf(u)??u}`,await Tn(e,a.rel,a.local,!0)}else if(a.action==="create"){if(a.parentMissing)await wt(e,x(a.rel));await Tn(e,a.rel,a.local,!1)}let f=(await r.list(x(a.rel))).find((u)=>u.Name.toLowerCase()===L(a.rel).toLowerCase());await t.record({path:f?.Path??a.rel,hash:a.localHash,size:f?.Size??0,lastModified:f?.LastModified??""}),l.push({path:a.rel,action:a.action==="save"?"saved":a.action==="create"?"created":"unchanged",note:d})}}finally{await t.saveState()}h(P(l,e.render))}},On=[lr,Fi,Oi,_i]});function ji(e,t){let r=e.workspace(),n=null,s=new Map,o=async()=>{if(!n){n=new Set;let c=(l)=>{n.add(l.Path.toLowerCase()),l.Children.forEach(c)};(await e.api().tree()).forEach(c)}return n},i=(c)=>{let l=c.toLowerCase();if(!s.has(l))s.set(l,Z(e,c).then((a)=>a?new Set(a.map((d)=>d.Name.toLowerCase())):null));return s.get(l)};return async(c,l)=>{if(c==="folder"?await r.isLocalDir(l):await r.readLocalBytes(l)!==null)return!0;if(t)return null;if(c==="folder")return(await o()).has(l.toLowerCase());let a=await i(x(l));if(a===null)return!1;if(a.has(L(l).toLowerCase()))return!0;return[".ascx",".css",".js",".png",".jpg",".jpeg",".gif",".webp",".svg",".ico",".woff",".woff2",".ttf",".pdf"].includes(z(l))?!1:null}}var Ui,Mn;var An=k(()=>{j();M();En();K();Ie();Ui={name:"check",usage:"mlis check [folder|file] [--offline]",summary:"Check local ViewPage files: broken ViewPage references, MLIS tag/format errors, alt text, h1, duplicate SEO tags",options:{offline:{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(e.positionals[0]),n=(await t.localFiles(r)).filter((a)=>[".ascx",".css"].includes(z(a))),s=ji(e,v(e.values,"offline")),o=[],i=0;for(let a of n){let d=Y(await t.readLocalBytes(a));o.push(...Nn(a,d,L(a).toLowerCase()==="default.ascx"));for(let f of Rn(d)){let u=await s(f.kind,f.path);if(u===!1)o.push({file:a,line:f.line,level:"error",rule:"ref-missing",message:`${f.kind} ViewPage/${f.path} does not exist`});else if(u===null)i++}}let c=o;h(P(c.map((a)=>({...a})),e.render));let l=c.filter((a)=>a.level==="error").length;if(!e.json)g(`# ${n.length} files checked: ${l} errors, ${c.length-l} warnings${i?`, ${i} references not verified (offline or type the API does not list)`:""}`);return l>0?1:0}},Mn=[Ui]});import{randomBytes as Hi}from"node:crypto";import{readFile as Vi,stat as Ki,writeFile as Wi}from"node:fs/promises";import{basename as Bi}from"node:path";function Xi(e){return e.replace(/&(#x[0-9a-f]+|#[0-9]+|amp|lt|gt|quot|apos);/gi,(t,r)=>{if(r.startsWith("#x"))return String.fromCodePoint(Number.parseInt(r.slice(2),16));if(r.startsWith("#"))return String.fromCodePoint(Number.parseInt(r.slice(1),10));return{amp:"&",lt:"<",gt:">",quot:'"',apos:"'"}[r]??t})}function Yi(e){let t={};for(let r of e.matchAll(/([A-Za-z][\w:-]*)="([^"]*)"/g))t[r[1]]=Xi(r[2]);return t}function Ct(e,t){let r=[];for(let n of e.matchAll(new RegExp(`<${t}\\b([^>]*?)\\/?>`,"g")))r.push(Yi(n[1]));return r}function fe(e,t){let r=Ct(e,"Error")[0];if(!r)throw Error(`CKFinder ${t} returned an invalid response: ${e.slice(0,200)}`);if(r.number!=="0"){let n=r.text||r.message||Zi[r.number??""]||"";throw Error(`CKFinder ${t} failed (error ${r.number??"?"})${n?`: ${n}`:""}`)}}function Le(e){if(e.includes("\\")||e.includes("?")||e.includes("#"))throw new m(`invalid CDN path ${JSON.stringify(e)}`);let t=e.replace(/^\/+|\/+$/g,"").split("/").filter(Boolean);for(let r of t)if(r==="."||r===".."||r.startsWith(".")||/[:*"<>|]/.test(r)||[...r].some((n)=>n.charCodeAt(0)<32))throw new m(`invalid CDN path segment ${JSON.stringify(r)}`);return t}function G(e="/"){let t=Le(e);return t.length?`/${t.join("/")}/`:"/"}function rt(e){let t=Le(e);if(t.length===0||e.endsWith("/"))throw new m("expected a file path, such as banner/logo.png");return`/${t.join("/")}`}function xe(e){let t=Le(e);return t.pop(),G(t.join("/"))}function Un(e){return Le(e).at(-1)??""}function yt(e,t,r){return`${e}${t}${r==="folder"?"/":""}`}function Hn(e){if(Le(e).length!==1||e.includes("/"))throw new m(`invalid file or folder name ${JSON.stringify(e)}`)}function Vn(e){return e.includes(".")?e.slice(e.lastIndexOf(".")+1).toLowerCase():""}function Kn(e){return qi.includes(Vn(e))?"Images":"Files"}function Wn(e){if(e===void 0)return;let t=Gi.find((r)=>r.toLowerCase()===e.toLowerCase());if(!t)throw new m("--type is Images or Files");return t}class dr{siteUrl;catId;mode;request;cookies=new Map;csrfToken=Hi(20).toString("hex");resources;seeded=!1;constructor(e,t,r,n,s=fetch){this.siteUrl=e;this.catId=r;this.mode=n;this.request=s;if(!/^\d+$/.test(r))throw new m("CatID must be numeric; set it with: mlis config set cdn-cat <CatID>");this.cookies.set("jwtAuth",t),this.cookies.set("ckCsrfToken",this.csrfToken)}cookieHeader(){return[...this.cookies].map(([e,t])=>`${e}=${t}`).join("; ")}async fetchWithCookies(e,t={}){let r=new Headers(t.headers);r.set("cookie",this.cookieHeader());let n;try{n=await this.request(e,{...t,headers:r,redirect:"manual",signal:AbortSignal.timeout(zi)})}catch(s){throw Error(`cannot reach ${String(e)}: ${s.message}`)}for(let s of n.headers.getSetCookie()){let o=s.split(";",1)[0],i=o.indexOf("=");if(i>0)this.cookies.set(o.slice(0,i).trim(),o.slice(i+1))}return n}async seed(){if(this.seeded)return;let e=new URL(_n,this.siteUrl);e.searchParams.set("CatID",this.catId),e.searchParams.set("mode",this.mode);let t=await this.fetchWithCookies(e);if(await t.text(),!this.cookies.has("ASP.NET_SessionId"))throw Error(`seeding the CKFinder session failed: ${_n} returned HTTP ${t.status} without a session cookie`);this.seeded=!0}async send(e,t,r,n,s){await this.seed();let o=new URL(Ji,this.siteUrl);if(o.searchParams.set("command",e),t){let c=await this.resource(t);o.searchParams.set("type",c.type),o.searchParams.set("currentFolder",G(r)),o.searchParams.set("langCode","vi"),o.searchParams.set("hash",c.hash)}for(let[c,l]of Object.entries(n??{}))o.searchParams.set(c,l);let i=await this.fetchWithCookies(o,{method:s?"POST":"GET",body:s});if(!i.ok)throw Error(`CKFinder ${e} returned HTTP ${i.status}`);return i.text()}postBody(e={}){return new URLSearchParams({...e,ckCsrfToken:this.csrfToken,CKFinderCommand:"true"})}async init(){if(!this.resources){let e=await this.send("Init",null);fe(e,"Init"),this.resources=new Map;for(let t of Ct(e,"ResourceType")){if(!t.name||!t.hash||!t.url)continue;this.resources.set(t.name,{type:t.name,url:t.url,maxSize:Number(t.maxSize??0),allowedExtensions:(t.allowedExtensions??"").split(",").filter(Boolean),acl:Number(t.acl??0),hash:t.hash})}if(this.resources.size===0)throw Error("CKFinder Init exposed no resource types")}return[...this.resources.values()].map(({hash:e,...t})=>t)}async resource(e){if(!this.resources)await this.init();let t=this.resources.get(e);if(!t)throw Error(`CKFinder does not expose resource type ${e}`);return t}async urlOf(e,t){return`${(await this.resource(e)).url.replace(/\/?$/,"/")}${Le(rt(t)).map(encodeURIComponent).join("/")}`}async list(e,t="/"){let r=G(t),n=await this.send("GetFolders",e,r);fe(n,"GetFolders");let s=await this.send("GetFiles",e,r);return fe(s,"GetFiles"),[...Ct(n,"Folder").map((o)=>({kind:"folder",name:o.name??"",path:yt(r,o.name??"","folder"),hasChildren:o.hasChildren==="true"})),...Ct(s,"File").map((o)=>({kind:"file",name:o.name??"",path:yt(r,o.name??"","file"),sizeKb:Number(o.size??0),date:o.date??""}))]}async listRecursive(e,t="/",r=20){let n=[{path:G(t),depth:0}],s=[];while(n.length){let o=n.shift(),i=await this.list(e,o.path);if(s.push(...i),s.length>jn)throw Error(`CDN listing exceeds ${jn} entries`);if(o.depth<r){for(let c of i)if(c.kind==="folder")n.push({path:c.path,depth:o.depth+1})}}return s}async findEntry(e,t){let r=Un(t).toLowerCase();return(await this.list(e,xe(t))).find((n)=>n.name.toLowerCase()===r)}async requireEntry(e,t,r){let n=await this.findEntry(e,t);if(!n||n.kind!==r)throw new m(`${r} ${t} does not exist in ${e}`);return n}async requireMissing(e,t){if(await this.findEntry(e,t))throw new m(`${t} already exists in ${e}`)}async checkExtension(e,t){let r=await this.resource(e),n=Vn(t);if(!n||!r.allowedExtensions.some((s)=>s.toLowerCase()===n))throw new m(`.${n||"?"} is not allowed in ${e}; allowed: ${r.allowedExtensions.join(",")}`)}async mkdir(e,t){let r=G(t);if(r==="/")throw new m("cannot create the root folder");let n=xe(r);if(n!=="/")await this.requireEntry(e,n,"folder");await this.requireMissing(e,r);let s=await this.send("CreateFolder",e,n,{NewFolderName:Un(r)},this.postBody());return fe(s,"CreateFolder"),r}async upload(e,t,r="/",n=Bi(t)){let s=G(r);if(s!=="/")await this.requireEntry(e,s,"folder");Hn(n),await this.checkExtension(e,n);let o=await Ki(t);if(!o.isFile())throw new m(`${t} is not a regular file`);let{maxSize:i}=await this.resource(e);if(i>0&&o.size>i&&e!=="Images")throw new m(`${t} is ${o.size} bytes; ${e} allows at most ${i}`);let c=new FormData;c.append("upload",new Blob([new Uint8Array(await Vi(t))]),n),c.append("ckCsrfToken",this.csrfToken);let l=await this.send("FileUpload",e,s,{response_type:"txt"},c),[a,...d]=l.trim().split("|");if(!a||!l.includes("|"))throw Error(`CKFinder FileUpload failed: ${d.join("|")||l.slice(0,200)}`);let f=yt(s,a,"file");return await this.requireEntry(e,f,"file"),{path:f,warning:d.join("|")}}async rename(e,t,r){Hn(r);let n=await this.findEntry(e,t.replace(/\/$/,""));if(!n)throw new m(`${t} does not exist in ${e}`);let s=n.kind==="folder",o=yt(xe(n.path),r,s?"folder":"file");if(n.path===o)return o;if(await this.requireMissing(e,o),!s)await this.checkExtension(e,r);let i=s?"RenameFolder":"RenameFile",c=s?{NewFolderName:r}:{fileName:n.name,newFileName:r},l=await this.send(i,e,s?n.path:xe(n.path),c,this.postBody());return fe(l,i),o}async deleteFile(e,t){let r=rt(t),n=await this.requireEntry(e,r,"file"),s=await this.send("DeleteFiles",e,xe(r),void 0,this.postBody({"files[0][name]":n.name,"files[0][type]":e,"files[0][folder]":xe(r)}));fe(s,"DeleteFiles")}async deleteFolder(e,t,r=!1){let n=G(t);if(n==="/")throw new m("cannot delete the root folder");if(await this.requireEntry(e,n,"folder"),!r&&(await this.list(e,n)).length>0)throw new E(`${n} is not empty; pass --recursive with --confirm-delete to remove its contents`);let s=await this.send("DeleteFolder",e,n,void 0,this.postBody());fe(s,"DeleteFolder")}async download(e,t,r){let n=new URL(await this.urlOf(e,t),this.siteUrl),s=await this.fetchWithCookies(n);if(!s.ok)throw Error(`download returned HTTP ${s.status} for ${n.pathname}`);let o=new Uint8Array(await s.arrayBuffer());return await Wi(r,o),o.length}}var Ji="/JScript/CKFinder/core/connector/aspx/connector.aspx",_n="/Admin/CKFinder.aspx",jn=1e4,zi=120000,Gi,qi,Zi;var Bn=k(()=>{N();Gi=["Images","Files"],qi=["gif","jpeg","jpg","png"];Zi={"500":"CKFinder is disabled for this request: the token was rejected or the session was not seeded","102":"invalid name","103":"access denied by CKFinder ACL","104":"insufficient permissions","105":"invalid file extension","109":"invalid request","115":"a file or folder with that name already exists","116":"folder not found","117":"file not found"}});import{existsSync as Jn}from"node:fs";import{basename as Qi}from"node:path";async function ee(e){let t=b(e.values,"cat")??e.settings.resolve("cdn-cat").value;if(!t)throw new m("which CatID holds the files? pass --cat <CatID> or run: mlis config set cdn-cat <CatID>");let r=b(e.values,"mode")??e.settings.resolve("cdn-mode").value??"folder";if(r!=="folder"&&r!=="news")throw new m('--mode is "folder" or "news"');return new dr(e.settings.url,await e.token(),t,r)}function ie(e,t="Images"){return Wn(b(e.values,"type"))??t}async function ea(e,t,r){return{kind:r.kind,path:r.path,...r.kind==="file"?{sizeKb:r.sizeKb??0,date:r.date??"",url:await e.urlOf(t,r.path)}:{hasChildren:r.hasChildren??!1}}}function zn(e,t){if(!v(e.values,"confirm-delete"))throw new E(`${t} is permanent; pass --confirm-delete once the user has approved this exact item`)}var Q,ta,ra,na,sa,oa,ia,aa,la,ca,Gn;var qn=k(()=>{j();N();M();Bn();Q={cat:{type:"string"},mode:{type:"string"},type:{type:"string"}};ta={name:"cdn info",usage:"mlis cdn info [--cat <CatID>] [--mode folder|news]",summary:"Show the CKFinder resource types (Images, Files): base URL, size limit, allowed extensions",options:Q,async run(e){let t=await ee(e),r=await t.init();h(W({catId:t.catId,mode:t.mode,resources:r}))}},ra={name:"cdn list",usage:"mlis cdn list [folder] [--type Images|Files] [--recursive] [--cat <CatID>]",summary:"List folders and files of one resource type (default Images)",options:{...Q,recursive:{type:"boolean"}},async run(e){let t=await ee(e),r=ie(e),n=G(e.positionals[0]),s=v(e.values,"recursive")?await t.listRecursive(r,n):await t.list(r,n),o=[];for(let i of s)o.push(await ea(t,r,i));h(P(o,e.render))}},na={name:"cdn upload",usage:"mlis cdn upload <local-file> [--to folder] [--name new-name] [--type Images|Files] [--cat <CatID>]",summary:"Upload an image or document and print the URL to use in .ascx/.css",options:{...Q,to:{type:"string"},name:{type:"string"}},async run(e){let t=C(e,0,"local file");if(!Jn(t))throw new m(`file not found: ${t}`);let r=b(e.values,"name")??Qi(t),n=ie(e,Kn(r)),s=await ee(e),{path:o,warning:i}=await s.upload(n,t,b(e.values,"to")??"/",r);if(i)g(`# CKFinder: ${i}`);if(!o.endsWith(`/${r}`))g(`# CKFinder renamed the upload to ${o}`);h(W({type:n,path:o,url:await s.urlOf(n,o)}))}},sa={name:"cdn mkdir",usage:"mlis cdn mkdir <folder> [--type Images|Files]",summary:"Create a folder in a resource type",options:Q,async run(e){let t=await ee(e);h(W({path:await t.mkdir(ie(e),C(e,0,"folder"))}))}},oa={name:"cdn rename",usage:"mlis cdn rename <path> <new-name> [--type Images|Files]",summary:"Rename a file or folder (new name only, same folder)",options:Q,async run(e){let r=await(await ee(e)).rename(ie(e),C(e,0,"path"),C(e,1,"new name"));h(W({path:r}))}},ia={name:"cdn url",usage:"mlis cdn url <path> [--type Images|Files]",summary:"Print the site-relative and full URL of a file after checking it exists",options:Q,async run(e){let t=await ee(e),r=ie(e),n=rt(C(e,0,"path")),s=n.slice(n.lastIndexOf("/")+1).toLowerCase();if(!(await t.list(r,n.slice(0,n.lastIndexOf("/")+1))).some((c)=>c.kind==="file"&&c.name.toLowerCase()===s))throw new m(`${n} does not exist in ${r}`);let i=await t.urlOf(r,n);h(W({path:n,url:i,fullUrl:new URL(i,e.settings.url).href}))}},aa={name:"cdn download",usage:"mlis cdn download <path> -o <local-file> [--overwrite] [--type Images|Files]",summary:"Download a file through its public URL",options:{...Q,output:{type:"string",short:"o"},overwrite:{type:"boolean"}},async run(e){let t=b(e.values,"output");if(!t)throw new m("pass -o <local-file>");if(Jn(t)&&!v(e.values,"overwrite"))throw new m(`${t} exists; pass --overwrite`);let n=await(await ee(e)).download(ie(e),C(e,0,"path"),t);g(`# saved ${n} bytes to ${t}`)}},la={name:"cdn rm",usage:"mlis cdn rm <path> --confirm-delete [--type Images|Files]",summary:"Delete a file permanently",options:{...Q,"confirm-delete":{type:"boolean"}},async run(e){let t=rt(C(e,0,"path"));zn(e,`deleting ${t}`),await(await ee(e)).deleteFile(ie(e),t),g(`# deleted ${t}`)}},ca={name:"cdn rmdir",usage:"mlis cdn rmdir <folder> --confirm-delete [--recursive] [--type Images|Files]",summary:"Delete a folder permanently (non-empty needs --recursive)",options:{...Q,"confirm-delete":{type:"boolean"},recursive:{type:"boolean"}},async run(e){let t=G(C(e,0,"folder"));zn(e,`deleting folder ${t}`),await(await ee(e)).deleteFolder(ie(e),t,v(e.values,"recursive")),g(`# deleted ${t}`)}},Gn=[ta,ra,na,sa,oa,ia,aa,la,ca]});function da(e){return/^[\w-]+\.[\w-]+\.[\w-]+$/.test(e)}function Xn(e){if(!ct.includes(e))throw new m(`unknown key ${JSON.stringify(e)}; keys: ${ct.join(", ")}`);return e}var ua,ma,fa,pa,Yn;var Zn=k(()=>{j();N();M();X();Ft();Tt();de();ua={name:"token set",usage:"mlis token set [--url https://lhu.edu.vn] [--no-verify] (pairing code or token on stdin, or pasted at the hidden prompt)",summary:"Pair this machine with a one-time code from /admin → Thiết bị (or save a pasted jwtAuth token)",options:{"no-verify":{type:"boolean"}},async run(e){if(e.positionals.length>0)throw new m("the code or token is never an argument (it would stay in shell history); pipe it or paste it");let t=process.stdin.isTTY?await rn("pairing code (or jwtAuth token): "):await tn();if(t=t.trim().replace(/^jwtAuth=/,"").replace(/;.*$/,""),!t)throw new m("nothing pasted");let r=b(e.values,"url")?Nt(b(e.values,"url")):e.settings.url,{session:n,...s}=await ae();if(Ar(t)){let i=await _r(r,t);if(await le({...s,token:i.token,session:i.session,url:r}),g(`# this machine is paired with ${r}; key: ${Ae()} (keep it private)`),i.expires)g(`# the session lasts until ${new Date(i.expires).toLocaleString()} or until signed out in /admin → Thiết bị`);return}if(!da(t))throw new m("expected a pairing code (XXXXX-XXXXX, from /admin → Thiết bị) or a jwtAuth token");if(!v(e.values,"no-verify")){let i=await new Me(r,t).tree();g(`# token accepted by ${r} (${i.length} top-level ViewPage folders)`)}await le({...s,token:t,url:r}),await Et(),g(`# token saved to ${ye()}`);let o=ce(t);if(o)g(`# token valid until ${o.toLocaleString()}; a pairing code (/admin → Thiết bị) avoids pasting tokens again`)}};ma={name:"config",usage:"mlis config",summary:"Show settings and where each value comes from (the token only as (set))",async run(e){let t=ct.map((r)=>{let{value:n,source:s}=e.settings.resolve(r);if(r==="token"&&n){if(s==="config"&&e.settings.file.session)return{key:r,value:"(device session: renewed automatically)",source:s};let o=ce(n),i=!o?"(set)":o.getTime()<=Date.now()?`(set, expired ${o.toLocaleString()})`:`(set, valid until ${o.toLocaleString()})`;return{key:r,value:i,source:s}}return{key:r,value:r==="token"?"":n??"",source:s}});if(h(P(t,e.render)),!e.json)g(`# file: ${ye()}`)}},fa={name:"config set",usage:"mlis config set <url|workdir|cdn-cat|cdn-mode> <value>",summary:"Change one setting",async run(e){let t=Xn(C(e,0,"key"));if(t==="token")throw new m("use: mlis token set (keeps the token out of shell history)");let r=lt(t,C(e,1,"value"));await le({...await ae(),[t]:r}),g(`# ${t} = ${r}`)}},pa={name:"config unset",usage:"mlis config unset <key>",summary:"Remove one setting (unset token to forget it)",async run(e){let t=Xn(C(e,0,"key")),{[t]:r,...n}=await ae();if(t==="token")delete n.session,await Et();await le(n),g(`# ${t} removed`)}},Yn=[ua,ma,fa,pa]});function ur(e){let t=z(e);if(t===".css")return"css";if(t===".js")return"js";return L(e).toLowerCase()==="default.ascx"?"page":"control"}function Qn(e,t=ur(e)){let r=x(e),n=e.split("/")[0]??"",s=L(e).replace(/\.[^.]+$/,"");switch(t){case"page":return['<%@ Register TagPrefix="MLIS" Namespace="MLIS" %>',`<MLIS:LoadPage CssFolder="ViewPage/${r}" runat="server" />`,`<MLIS:LoadPage CssFolder="ViewPage/${n}/_default" Src="ViewPage/${n}/_default/Header.ascx" runat="server" />`,"",'<div class="container">'," <%-- Nội dung trang: đặt các control MLIS (NewsList, CatShow, ...) ở đây --%>","</div>","",`<MLIS:LoadPage Src="ViewPage/${n}/_default/Footer.ascx" runat="server" />`,""].join(`\r
33
48
  `);case"control":return['<%@ Register TagPrefix="MLIS" Namespace="MLIS" %>',`<%-- ${L(e)}: chèn vào trang bằng <MLIS:LoadPage Src="ViewPage/${e}" runat="server" /> --%>`,`<div class="${s}">`,"</div>",""].join(`\r
34
49
  `);case"css":return`/* ViewPage/${e} */\r
35
50
  `;case"js":return`/* ViewPage/${e} */\r
36
51
  $(function () {\r
37
52
  \r
38
53
  });\r
39
- `}}var on=b(()=>{U()});import{mkdir as Vo,rmdir as Wo}from"node:fs/promises";async function an(e,t){let r=await q(e,P(t));return{folderExists:r!==null,fileExists:r?.some((n)=>n.Name.toLowerCase()===L(t).toLowerCase())??!1}}function ln(e,t){if(!k(e.values,"confirm-delete"))throw new E(`${t} is permanent on the server; pass --confirm-delete once the user has approved it`)}var Bo,Ko,Jo,zo,cn;var dn=b(()=>{A();x();O();te();Re();on();U();me();Bo={name:"new",usage:"mlis new <file.ascx|.css|.js> [--kind page|control|css|js] [--push]",summary:"Create a file from a clean template in the workspace (render and review it, then mlis push); --push also creates it on the server now",options:{kind:{type:"string"},push:{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file"));if(!T(r)||!r.includes("/"))throw new u("expected a .ascx, .css or .js path inside a site folder, e.g. LHUENB4/MyPage/Default.ascx");let n=v(e.values,"kind")??Tt(r);if(!["page","control","css","js"].includes(n))throw new u("--kind is page, control, css or js");if(await t.readLocal(r)!==null)throw new u(`${r} already exists locally`);let s=k(e.values,"push"),o=s?await an(e,r):null;if(o?.fileExists)throw new u(`${r} already exists on the server; run: mlis pull ${r}`);let i=sn(r,n);if(await t.writeLocal(r,i),g(`# wrote ${t.localPath(r)} (${n})`),!s){g(`# local only; when it is ready: mlis render shot ${P(r)} → mlis push ${r}`);return}let c=e.api();if(!o.folderExists)await qe(e,P(r));await c.createFile(P(r),L(r)),await c.save(r,i);let a=(await c.list(P(r))).find((l)=>l.Name.toLowerCase()===L(r).toLowerCase());await t.record({path:a?.Path??r,hash:mt(i),size:a?.Size??0,lastModified:a?.LastModified??""}),await t.saveState(),g(`# created on the server: ViewPage/${r}`)}},Ko={name:"rm",usage:"mlis rm <file> --confirm-delete",summary:"Delete a file on the server and locally (both copies are kept in _history)",options:{"confirm-delete":{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file"));if(!B(r))throw new u("this file type is not stored in ViewPage through the API");ln(e,`deleting ${r}`);let{fileExists:n}=await an(e,r),s=null;if(n){let i=await xe(e,r);s=F(r,i),await V(t,r,i,"deleted"),await e.api().deleteFile(r),g(`# deleted on the server: ViewPage/${r}`)}let o=await t.readLocalBytes(r);if(o!==null){if(F(r,o)!==s)await V(t,r,o,"deleted-local");await t.removeLocal(r),g(`# deleted locally: ${t.localPath(r)}`)}if(await t.forget(r),await t.saveState(),!n&&o===null)throw new u(`${r} exists neither on the server nor locally`)}},Jo={name:"mkdir",usage:"mlis mkdir <folder>",summary:"Create a folder (and missing parents) on the server",async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"folder"));if(!r||T(r))throw new u("expected a folder path, e.g. LHUENB4/MyPage");await qe(e,r),await Vo(t.localPath(r),{recursive:!0}),g(`# folder ready: ViewPage/${r}`)}},zo={name:"rmdir",usage:"mlis rmdir <folder> --confirm-delete",summary:"Delete an empty folder on the server (the server refuses non-empty folders)",options:{"confirm-delete":{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"folder"));if(!r||T(r))throw new u("expected a folder path");ln(e,`deleting folder ${r}`);try{await e.api().deleteDirectory(r)}catch(n){if(n instanceof M)throw new u(`server refused: ${n.serverMessage||n.message}`);throw n}await Wo(t.localPath(r)).catch(()=>{return}),g(`# folder deleted on the server: ViewPage/${r}`)}},cn=[Bo,Ko,Jo,zo]});import{readFile as Go}from"node:fs/promises";var qo,Xo,un;var mn=b(()=>{A();x();O();Re();U();qo={name:"history",usage:"mlis history <file>",summary:"List saved copies of a file (server copies before push, local copies before pull --force, deletions)",async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file"));if(!B(r))throw new u("expected a file path, e.g. LHUENB4/@HomePage/default.ascx");let n=await Lt(t,r);if(h(N(n.map((s)=>({stamp:s.stamp,label:s.label,file:t.relOf(s.file)??s.file})),e.render)),!e.json&&n.length===0)g(`# no saved copies of ${r}`)}},Xo={name:"restore",usage:"mlis restore <file> [--at YYYYMMDD_HHMMSS]",summary:"Copy a saved version back into the workspace (newest by default); push it to publish",options:{at:{type:"string"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file")),n=await Lt(t,r),s=v(e.values,"at"),o=s?n.find((a)=>a.stamp===s):n[0];if(!o)throw new u(s?`no copy of ${r} saved at ${s}; see: mlis history ${r}`:`no saved copies of ${r}`);let i=await Go(o.file),c=await t.readLocalBytes(r);if(c!==null&&F(r,c)!==F(r,i))await V(t,r,c,"before-restore");await t.writeLocal(r,i),g(`# restored ${r} from ${o.stamp}${o.label?` (${o.label})`:""}; publish with: mlis push ${r}`)}},un=[qo,Xo]});import{createHash as Yo}from"node:crypto";import{mkdir as Zo,readFile as Qo,rename as ei,writeFile as ti}from"node:fs/promises";import{dirname as ri,join as ni}from"node:path";function De(e){return`${Fe}/${e}.html`}function Dt(e){return si[e??0]??"news"}function Z(e){return Yo("sha1").update(e.replace(/^/,"").replace(/\r\n/g,`
40
- `)).digest("hex")}function Ft(e){let t=/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(e??"");return t?`${t[1]} ${t[2]}`:""}function Ee(e){return/[\r\n]|^\s|\s$|^"/.test(e)?JSON.stringify(e):e}function Ot(e){let t={newsId:e.NewsID,catId:e.CategoryID,title:e.NewsTitle??"",intro:e.NewsIntro??"",images:e.NewsImages??"",keyword:e.NewsKeyword??"",source:e.NewsSource??"",content:e.NewsContent??""};if(e.CatType===4)t.orderTime=Ft(e.OrderTime);return t}function Mt(e,t){let r=[Te,`newsId: ${e.newsId}`,`catId: ${e.catId}`,`title: ${Ee(e.title)}`],n=Dt(t?.CatType);if(n!=="event")r.push(`intro: ${Ee(e.intro)}`);if(r.push(`images: ${Ee(e.images)}`),n==="event")r.push(`orderTime: ${e.orderTime??""}`);else r.push(`keyword: ${Ee(e.keyword)}`);if(r.push(`source: ${Ee(e.source)}`),t){if(r.push(`# ${n} trong thư mục ${t.CatName??e.catId}`),t.CreateTime)r.push(`# tạo: ${Ft(t.CreateTime)} ${t.CreateUser??""}`.trimEnd());if(t.UpdateUser)r.push(`# sửa: ${Ft(t.UpdateTime)} ${t.UpdateUser}`);if(n==="event")r.push("# sự kiện: orderTime là thời gian diễn ra (yyyy-MM-dd HH:mm); không có tóm tắt/từ khoá");if(n==="gallery")r.push("# thư viện ảnh: chỉ đọc, sửa trong /admin")}return r.push(Ze),`${r.join(`\r
41
- `)}\r
42
- ${e.content.replace(/\r?\n/g,`\r
43
- `)}\r
44
- `}function At(e,t="article"){let r=e.replace(/^/,""),n=r.indexOf(Te),s=r.indexOf(Ze,n+Te.length);if(n!==0||s<0)throw Error(`${t}: must start with a ${Te} … ${Ze} header`);let o=new Map;for(let a of r.slice(Te.length,s).split(/\r?\n/)){let l=a.trim();if(!l||l.startsWith("#"))continue;let d=l.indexOf(":");if(d<=0)throw Error(`${t}: header line "${l}" is not "key: value"`);let f=l.slice(0,d).trim(),m=l.slice(d+1).trim();if(m.startsWith('"'))try{m=JSON.parse(m)}catch{throw Error(`${t}: ${f} starts with " but is not a valid JSON string`)}o.set(f,m)}let i=(a)=>{let l=Number(o.get(a)??"");if(!Number.isInteger(l)||l<0)throw Error(`${t}: ${a} must be a number`);return l},c={newsId:i("newsId"),catId:i("catId"),title:o.get("title")??"",intro:o.get("intro")??"",images:o.get("images")??"",keyword:o.get("keyword")??"",source:o.get("source")??"",content:r.slice(s+Ze.length).replace(/^\r?\n/,"").replace(/\r?\n$/,"")};if(o.has("orderTime"))c.orderTime=o.get("orderTime");return c}function _t(e,t,r){let n={NewsID:e.newsId,CatID:e.catId},s=(o,i,c)=>{if(!r||!fn(i,c))n[o]=i??""};if(s("NewsTitle",e.title,r?.title),s("NewsIntro",e.intro,r?.intro),s("NewsContent",e.content,r?.content),s("NewsImages",e.images,r?.images),s("NewsKeyword",e.keyword,r?.keyword),s("NewsSource",e.source,r?.source),e.orderTime&&(!r||!fn(e.orderTime,r.orderTime)))n.OrderTime=e.orderTime;if(t)n.Version=t;return n}class Qe{ws;entries=null;constructor(e){this.ws=e}get path(){return ni(this.ws.root,"_mlis","news.json")}async load(){if(this.entries)return this.entries;try{this.entries=JSON.parse(await Qo(this.path,"utf-8"))}catch(e){if(e.code!=="ENOENT")throw e;this.entries={}}return this.entries}async get(e){return(await this.load())[String(e)]}async set(e,t,r){(await this.load())[String(e)]={newsId:e,version:t,hash:Z(r),syncedAt:new Date().toISOString()}}async save(){if(!this.entries)return;await Zo(ri(this.path),{recursive:!0});let e=`${this.path}.${process.pid}.tmp`;await ti(e,`${JSON.stringify(this.entries,null,1)}
45
- `,"utf-8"),await ei(e,this.path)}}var Fe="_news",Te="<!--mlis-news",Ze="-->",si,fn=(e,t)=>(e??"").replace(/\r\n/g,`
46
- `)===(t??"").replace(/\r\n/g,`
47
- `);var pn=b(()=>{si={4:"event",6:"gallery"}});import{rm as oi}from"node:fs/promises";import{isAbsolute as ii,relative as ai,resolve as li,sep as ci}from"node:path";function ge(e,t){let r=Number(e);if(e===void 0||!Number.isInteger(r)||r<0)throw new u(`${t} must be a number`);return r}function hn(e,t){if(/^\d+$/.test(t))return De(Number(t));let r=e.workspace(),n=ai(r.root,li(t)).split(ci).join("/");if(n.startsWith("..")||ii(n)||!n.toLowerCase().startsWith(`${Fe.toLowerCase()}/`))throw new u(`${t}: give a NewsID or a file in ${r.localPath(Fe)}`);return n}function he(e){return Mt(Ot(e),e)}function gn(e){let t=/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(String(e??""));return t?`${t[1]} ${t[2]}`:""}var di,ui,mi,fi,pi,gi,wn;var yn=b(()=>{A();x();O();te();Re();pn();di={name:"news ls",usage:"mlis news ls <CatID> [--search text] [--from yyyy/mm/dd] [--to yyyy/mm/dd] [--skip n] [--limit n] [--trash]",summary:"List the articles of a category (newest/sorted as in /admin → Bài viết)",options:{search:{type:"string"},from:{type:"string"},to:{type:"string"},skip:{type:"string"},limit:{type:"string"},trash:{type:"boolean"}},async run(e){let t=await e.api().newsList({catId:ge(C(e,0,"CatID"),"CatID"),search:v(e.values,"search"),from:v(e.values,"from"),to:v(e.values,"to"),skip:v(e.values,"skip")===void 0?0:ge(v(e.values,"skip"),"--skip"),limit:v(e.values,"limit")===void 0?20:ge(v(e.values,"limit"),"--limit"),trash:k(e.values,"trash")});if(e.json){h(N(t,e.render));return}if(h(N(t.map((r)=>({id:r.NewsID,title:r.NewsTitle,keyword:r.NewsKeyword??"",created:`${gn(r.CreateTime)} ${r.CreateUser??""}`.trim(),updated:r.UpdateUser?`${gn(r.UpdateTime)} ${r.UpdateUser}`:"",image:r.NewsImages??""})),e.render)),t.length===0)g("# no articles (or no right on this category)")}},ui={name:"news show",usage:"mlis news show <NewsID>",summary:"Print one article (fields + HTML content) from the server",async run(e){let t=await e.api().news(ge(C(e,0,"NewsID"),"NewsID"));h(e.json?`${JSON.stringify(t,null,2)}
48
- `:he(t))}},mi={name:"news pull",usage:"mlis news pull <NewsID>... [--force]",summary:"Download articles to <workspace>/_news/<NewsID>.html to edit (keeps local edits unless --force)",options:{force:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new u("missing NewsID");let t=e.workspace(),r=new Qe(t),n=[];for(let s of e.positionals){let o=ge(s,"NewsID"),i=await e.api().news(o),c=he(i),a=De(o),l=await t.readLocal(a),d=await r.get(o),f=l===null?"new":"updated";if(l!==null&&Z(l)===Z(c))f="unchanged";else if(l!==null&&(!d||Z(l)!==d.hash)){if(!k(e.values,"force")){n.push({path:a,action:"kept-local",note:"local edits; push them, or pull --force"});continue}await V(t,a,l,"local")}if(f!=="unchanged")await t.writeLocal(a,c);await r.set(o,i.Version,c),n.push({path:a,action:f,note:`${Dt(i.CatType)} · ${i.CatName??i.CategoryID}`})}await r.save(),h(N(n,e.render))}},fi={name:"news diff",usage:"mlis news diff <NewsID|file>",summary:"Diff a local article against the server",async run(e){let t=e.workspace(),r=hn(e,C(e,0,"NewsID or file")),n=await t.readLocal(r);if(n===null)throw new u(`${r} is not in the workspace; run: mlis news pull <NewsID>`);let s=At(n,r);if(s.newsId===0){h(n),g("# new article (newsId 0): all of it is added on push");return}let o=he(await e.api().news(s.newsId)),i=Ge(o,n,`server/${s.newsId}`,r);if(h(i||""),!i)g("# no difference")}},pi={name:"news push",usage:"mlis news push <NewsID|file>... [--dry-run] [--force]",summary:"Save local articles to the server (stops, exit 3, when the article changed on the server since the pull)",options:{"dry-run":{type:"boolean"},force:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new u("missing NewsID or file");let t=e.workspace(),r=new Qe(t),n=e.api(),s=k(e.values,"dry-run"),o=k(e.values,"force"),i=[];try{for(let c of e.positionals){let a=hn(e,c),l=await t.readLocal(a);if(l===null)throw new u(`${a} is not in the workspace`);let d=At(l,a);if(d.newsId===0){if(s){i.push({path:a,action:"would-create",note:`category ${d.catId}`});continue}let R=await n.saveNews(_t(d)),$e=he(R),K=De(R.NewsID);if(await t.writeLocal(K,$e),K!==a)await oi(t.localPath(a));await r.set(R.NewsID,R.Version,$e),i.push({path:K,action:"created",note:`NewsID ${R.NewsID}`});continue}if(a.toLowerCase()!==De(d.newsId).toLowerCase())throw new u(`${a}: header says newsId ${d.newsId}; an article file is named <NewsID>.html`);let f=await r.get(d.newsId),m=await n.news(d.newsId);if(m.CatType===6)throw new E(`${a}: gallery articles are edited in /admin only`);if(d.catId!==m.CategoryID)throw new u(`${a}: catId cannot change (${m.CategoryID} → ${d.catId}); move articles by dragging them in /admin → Bài viết`);let w=he(m);if(!o&&!f)throw new E(`${a} was never pulled here; run: mlis news pull ${d.newsId} (or push --force to overwrite the server)`);if(!o&&f&&f.version!==m.Version)throw new E(`${a}: the article changed on the server after your pull. See: mlis news diff ${d.newsId}; then keep yours (push --force) or take the server's (pull --force)`);if(Z(l)===Z(w)){i.push({path:a,action:"unchanged",note:""});continue}if(s){i.push({path:a,action:"would-update",note:"mlis news diff shows the change"});continue}await V(t,a,w,"server");let p=await n.saveNews(_t(d,o?void 0:m.Version,Ot(m))),S=he(p);await t.writeLocal(a,S),await r.set(p.NewsID,p.Version,S);let I=Z(S)!==Z(l);i.push({path:a,action:"updated",note:I?"server normalised some fields; file rewritten":""})}}catch(c){if(c instanceof M&&c.status===409)throw new E("the article changed on the server while pushing; run mlis news diff, then pull or push --force");throw c}finally{if(await r.save(),i.length)h(N(i,e.render))}}},gi={name:"news new",usage:"mlis news new <CatID> [--title text]",summary:"Start a new article file (newsId 0) in _news; mlis news push creates it on the server",options:{title:{type:"string"}},async run(e){let t=e.workspace(),r=ge(C(e,0,"CatID"),"CatID"),n=`${Fe}/new-${r}-${St()}.html`,s=Mt({newsId:0,catId:r,title:v(e.values,"title")??"Tiêu đề bài viết",intro:"",images:"",keyword:"",source:"",content:"<p>Nội dung bài viết (HTML, ít nhất 30 ký tự).</p>"}).replace("-->",`# thư mục sự kiện: thêm dòng orderTime: yyyy-MM-dd HH:mm\r
54
+ `}}var es=k(()=>{K()});import{mkdir as ga,rmdir as ha}from"node:fs/promises";async function ts(e,t){let r=await Z(e,x(t));return{folderExists:r!==null,fileExists:r?.some((n)=>n.Name.toLowerCase()===L(t).toLowerCase())??!1}}function rs(e,t){if(!v(e.values,"confirm-delete"))throw new E(`${t} is permanent on the server; pass --confirm-delete once the user has approved it`)}var wa,ya,Ca,$a,ns;var ss=k(()=>{j();N();M();X();Pe();es();K();Ie();wa={name:"new",usage:"mlis new <file.ascx|.css|.js> [--kind page|control|css|js] [--push]",summary:"Create a file from a clean template in the workspace (render and review it, then mlis push); --push also creates it on the server now",options:{kind:{type:"string"},push:{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file"));if(!D(r)||!r.includes("/"))throw new m("expected a .ascx, .css or .js path inside a site folder, e.g. LHUENB4/MyPage/Default.ascx");let n=b(e.values,"kind")??ur(r);if(!["page","control","css","js"].includes(n))throw new m("--kind is page, control, css or js");if(await t.readLocal(r)!==null)throw new m(`${r} already exists locally`);let s=v(e.values,"push"),o=s?await ts(e,r):null;if(o?.fileExists)throw new m(`${r} already exists on the server; run: mlis pull ${r}`);let i=Qn(r,n);if(await t.writeLocal(r,i),g(`# wrote ${t.localPath(r)} (${n})`),!s){g(`# local only; when it is ready: mlis render shot ${x(r)} → mlis push ${r}`);return}let c=e.api();if(!o.folderExists)await wt(e,x(r));await c.createFile(x(r),L(r)),await c.save(r,i);let l=(await c.list(x(r))).find((a)=>a.Name.toLowerCase()===L(r).toLowerCase());await t.record({path:l?.Path??r,hash:At(i),size:l?.Size??0,lastModified:l?.LastModified??""}),await t.saveState(),g(`# created on the server: ViewPage/${r}`)}},ya={name:"rm",usage:"mlis rm <file> --confirm-delete",summary:"Delete a file on the server and locally (both copies are kept in _history)",options:{"confirm-delete":{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file"));if(!J(r))throw new m("this file type is not stored in ViewPage through the API");rs(e,`deleting ${r}`);let{fileExists:n}=await ts(e,r),s=null;if(n){let i=await tt(e,r);s=F(r,i),await A(t,r,i,"deleted"),await e.api().deleteFile(r),g(`# deleted on the server: ViewPage/${r}`)}let o=await t.readLocalBytes(r);if(o!==null){if(F(r,o)!==s)await A(t,r,o,"deleted-local");await t.removeLocal(r),g(`# deleted locally: ${t.localPath(r)}`)}if(await t.forget(r),await t.saveState(),!n&&o===null)throw new m(`${r} exists neither on the server nor locally`)}},Ca={name:"mkdir",usage:"mlis mkdir <folder>",summary:"Create a folder (and missing parents) on the server",async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"folder"));if(!r||D(r))throw new m("expected a folder path, e.g. LHUENB4/MyPage");await wt(e,r),await ga(t.localPath(r),{recursive:!0}),g(`# folder ready: ViewPage/${r}`)}},$a={name:"rmdir",usage:"mlis rmdir <folder> --confirm-delete",summary:"Delete an empty folder on the server (the server refuses non-empty folders)",options:{"confirm-delete":{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"folder"));if(!r||D(r))throw new m("expected a folder path");rs(e,`deleting folder ${r}`);try{await e.api().deleteDirectory(r)}catch(n){if(n instanceof T)throw new m(`server refused: ${n.serverMessage||n.message}`);throw n}await ha(t.localPath(r)).catch(()=>{return}),g(`# folder deleted on the server: ViewPage/${r}`)}},ns=[wa,ya,Ca,$a]});import{readFile as va}from"node:fs/promises";var ka,ba,os;var is=k(()=>{j();N();M();Pe();K();ka={name:"history",usage:"mlis history <file>",summary:"List saved copies of a file (server copies before push, local copies before pull --force, deletions)",async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file"));if(!J(r))throw new m("expected a file path, e.g. LHUENB4/@HomePage/default.ascx");let n=await Zt(t,r);if(h(P(n.map((s)=>({stamp:s.stamp,label:s.label,file:t.relOf(s.file)??s.file})),e.render)),!e.json&&n.length===0)g(`# no saved copies of ${r}`)}},ba={name:"restore",usage:"mlis restore <file> [--at YYYYMMDD_HHMMSS]",summary:"Copy a saved version back into the workspace (newest by default); push it to publish",options:{at:{type:"string"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file")),n=await Zt(t,r),s=b(e.values,"at"),o=s?n.find((l)=>l.stamp===s):n[0];if(!o)throw new m(s?`no copy of ${r} saved at ${s}; see: mlis history ${r}`:`no saved copies of ${r}`);let i=await va(o.file),c=await t.readLocalBytes(r);if(c!==null&&F(r,c)!==F(r,i))await A(t,r,c,"before-restore");await t.writeLocal(r,i),g(`# restored ${r} from ${o.stamp}${o.label?` (${o.label})`:""}; publish with: mlis push ${r}`)}},os=[ka,ba]});import{rm as Sa}from"node:fs/promises";import{isAbsolute as Pa,relative as Ia,resolve as xa,sep as La}from"node:path";function Re(e,t){let r=Number(e);if(e===void 0||!Number.isInteger(r)||r<0)throw new m(`${t} must be a number`);return r}function ls(e,t){if(/^\d+$/.test(t))return Ze(Number(t));let r=e.workspace(),n=Ia(r.root,xa(t)).split(La).join("/");if(n.startsWith("..")||Pa(n)||!n.toLowerCase().startsWith(`${Ye.toLowerCase()}/`))throw new m(`${t}: give a NewsID or a file in ${r.localPath(Ye)}`);return n}function Ne(e){return rr(tr(e),e)}function as(e){let t=/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(String(e??""));return t?`${t[1]} ${t[2]}`:""}var Ra,Na,Ea,Ta,Da,Fa,cs;var ds=k(()=>{j();N();M();X();Pe();or();Ra={name:"news ls",usage:"mlis news ls <CatID> [--search text] [--from yyyy/mm/dd] [--to yyyy/mm/dd] [--skip n] [--limit n] [--trash]",summary:"List the articles of a category (newest/sorted as in /admin → Bài viết)",options:{search:{type:"string"},from:{type:"string"},to:{type:"string"},skip:{type:"string"},limit:{type:"string"},trash:{type:"boolean"}},async run(e){let t=await e.api().newsList({catId:Re(C(e,0,"CatID"),"CatID"),search:b(e.values,"search"),from:b(e.values,"from"),to:b(e.values,"to"),skip:b(e.values,"skip")===void 0?0:Re(b(e.values,"skip"),"--skip"),limit:b(e.values,"limit")===void 0?20:Re(b(e.values,"limit"),"--limit"),trash:v(e.values,"trash")});if(e.json){h(P(t,e.render));return}if(h(P(t.map((r)=>({id:r.NewsID,title:r.NewsTitle,keyword:r.NewsKeyword??"",created:`${as(r.CreateTime)} ${r.CreateUser??""}`.trim(),updated:r.UpdateUser?`${as(r.UpdateTime)} ${r.UpdateUser}`:"",image:r.NewsImages??""})),e.render)),t.length===0)g("# no articles (or no right on this category)")}},Na={name:"news show",usage:"mlis news show <NewsID>",summary:"Print one article (fields + HTML content) from the server",async run(e){let t=await e.api().news(Re(C(e,0,"NewsID"),"NewsID"));h(e.json?`${JSON.stringify(t,null,2)}
55
+ `:Ne(t))}},Ea={name:"news pull",usage:"mlis news pull <NewsID>... [--force]",summary:"Download articles to <workspace>/_news/<NewsID>.html to edit (keeps local edits unless --force)",options:{force:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new m("missing NewsID");let t=e.workspace(),r=new me(t),n=[];for(let s of e.positionals){let o=Re(s,"NewsID"),i=await e.api().news(o),c=Ne(i),l=Ze(o),a=await t.readLocal(l),d=await r.get(o),f=a===null?"new":"updated";if(a!==null&&_(a)===_(c))f="unchanged";else if(a!==null&&(!d||_(a)!==d.hash)){if(!v(e.values,"force")){n.push({path:l,action:"kept-local",note:"local edits; push them, or pull --force"});continue}await A(t,l,a,"local")}if(f!=="unchanged")await t.writeLocal(l,c);await r.set(o,i.Version,c),n.push({path:l,action:f,note:`${er(i.CatType)} · ${i.CatName??i.CategoryID}`})}await r.save(),h(P(n,e.render))}},Ta={name:"news diff",usage:"mlis news diff <NewsID|file>",summary:"Diff a local article against the server",async run(e){let t=e.workspace(),r=ls(e,C(e,0,"NewsID or file")),n=await t.readLocal(r);if(n===null)throw new m(`${r} is not in the workspace; run: mlis news pull <NewsID>`);let s=nr(n,r);if(s.newsId===0){h(n),g("# new article (newsId 0): all of it is added on push");return}let o=Ne(await e.api().news(s.newsId)),i=ke(o,n,`server/${s.newsId}`,r);if(h(i||""),!i)g("# no difference")}},Da={name:"news push",usage:"mlis news push <NewsID|file>... [--dry-run] [--force]",summary:"Save local articles to the server (stops, exit 3, when the article changed on the server since the pull)",options:{"dry-run":{type:"boolean"},force:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new m("missing NewsID or file");let t=e.workspace(),r=new me(t),n=e.api(),s=v(e.values,"dry-run"),o=v(e.values,"force"),i=[];try{for(let c of e.positionals){let l=ls(e,c),a=await t.readLocal(l);if(a===null)throw new m(`${l} is not in the workspace`);let d=nr(a,l);if(d.newsId===0){if(s){i.push({path:l,action:"would-create",note:`category ${d.catId}`});continue}let I=await n.saveNews(sr(d)),q=Ne(I),U=Ze(I.NewsID);if(await t.writeLocal(U,q),U!==l)await Sa(t.localPath(l));await r.set(I.NewsID,I.Version,q),i.push({path:U,action:"created",note:`NewsID ${I.NewsID}`});continue}if(l.toLowerCase()!==Ze(d.newsId).toLowerCase())throw new m(`${l}: header says newsId ${d.newsId}; an article file is named <NewsID>.html`);let f=await r.get(d.newsId),u=await n.news(d.newsId);if(u.CatType===6)throw new E(`${l}: gallery articles are edited in /admin only`);if(d.catId!==u.CategoryID)throw new m(`${l}: catId cannot change (${u.CategoryID} → ${d.catId}); move articles by dragging them in /admin → Bài viết`);let w=Ne(u);if(!o&&!f)throw new E(`${l} was never pulled here; run: mlis news pull ${d.newsId} (or push --force to overwrite the server)`);if(!o&&f&&f.version!==u.Version)throw new E(`${l}: the article changed on the server after your pull. See: mlis news diff ${d.newsId}; then keep yours (push --force) or take the server's (pull --force)`);if(_(a)===_(w)){i.push({path:l,action:"unchanged",note:""});continue}if(s){i.push({path:l,action:"would-update",note:"mlis news diff shows the change"});continue}await A(t,l,w,"server");let p=await n.saveNews(sr(d,o?void 0:u.Version,tr(u))),S=Ne(p);await t.writeLocal(l,S),await r.set(p.NewsID,p.Version,S);let R=_(S)!==_(a);i.push({path:l,action:"updated",note:R?"server normalised some fields; file rewritten":""})}}catch(c){if(c instanceof T&&c.status===409)throw new E("the article changed on the server while pushing; run mlis news diff, then pull or push --force");throw c}finally{if(await r.save(),i.length)h(P(i,e.render))}}},Fa={name:"news new",usage:"mlis news new <CatID> [--title text]",summary:"Start a new article file (newsId 0) in _news; mlis news push creates it on the server",options:{title:{type:"string"}},async run(e){let t=e.workspace(),r=Re(C(e,0,"CatID"),"CatID"),n=`${Ye}/new-${r}-${Ge()}.html`,s=rr({newsId:0,catId:r,title:b(e.values,"title")??"Tiêu đề bài viết",intro:"",images:"",keyword:"",source:"",content:"<p>Nội dung bài viết (HTML, ít nhất 30 ký tự).</p>"}).replace("-->",`# thư mục sự kiện: thêm dòng orderTime: yyyy-MM-dd HH:mm\r
49
56
  -->`);await t.writeLocal(n,s),h(`${t.localPath(n)}
50
- `)}},wn=[di,ui,mi,fi,pi,gi]});function wi(e){return[...e.matchAll(/\{([^}]+)\}/g)].map((t)=>t[1].trim().toLowerCase()).filter(Boolean)}function $n(e){return e.split("/")[0]??""}function yi(e,t){let r=t.toLowerCase().replace(/^www\./,"");return e.items.filter((n)=>wi(n.link).includes(r)).sort((n,s)=>e.ancestors(n.id).length-e.ancestors(s.id).length||(n.sort??0)-(s.sort??0)||n.id-s.id)}function vn(e,t){let r=t.hostname.toLowerCase(),n=t.pathname.split("/").filter(Boolean).map((d)=>decodeURIComponent(d)),s=yi(e,r),o=s[0],i={host:r,path:t.pathname,kind:"unknown",home:o,otherHomes:s.slice(1)},c=(d)=>d&&/^\d+$/.test(d)?e.byId.get(Number(d)):void 0;if(n.length&&hi.test(n[0]))return{...i,kind:"platform",note:`${n[0]} is generated by the platform (robotsTxt/llmsTxt/sitemapXml/rssFeeds/error .aspx), not a ViewPage template`};if(!o)return{...i,note:`no category has {${r}} in its link: the domain has no home page`};if(n.length===0)return{...i,kind:"home",category:o,template:o.pageStyle};let a=n[0];if(/^\d+$/.test(a)){let d=c(a);if(!d)return{...i,note:`category ${a} is unknown (or not visible to this account)`};let f=n[1];if(f&&/^\d+$/.test(f)&&Number(f)>0){let m=d.pageRead||d.pageStyle,w=d.pageRead?void 0:"the category has no PageRead; the article page then depends on the server's stored procedure (shown: PageStyle)";return{...i,kind:"read",category:d,newsId:Number(f),template:m,note:w}}return{...i,kind:"category",category:d,template:d.pageStyle}}let l=a.toLowerCase();if(l in Cn||l==="more-event"){let d=c(n[1]);if(!d)return{...i,note:`category ${n[1]??"?"} is unknown (or not visible to this account)`};let f=l==="more-news"&&n[2]&&/^\d+$/.test(n[2])?Number(n[2]):void 0,m=l==="more-event"?d.pageStyle:`${$n(d.pageStyle)}/${Cn[l]}`;return{...i,kind:"more",category:d,newsId:f,template:m}}if(/^\w+$/.test(a))return{...i,kind:"route",category:o,template:`${$n(o.pageStyle)}/_${a}`};return{...i,note:`/${a} matches no route: the site answers 404`}}function kn(e,t){let r=t.toLowerCase(),n=[];for(let s of e.items){if(s.pageStyle.toLowerCase()===r)n.push({category:s,role:"page"});if(s.pageRead.toLowerCase()===r)n.push({category:s,role:"read"})}return n}var Cn,hi;var bn=b(()=>{Cn={"more-list":"_more-List","more-related":"_more-Related","more-news":"_more-News"},hi=/^(robots\.txt|llms\.txt|sitemap(-[\w-]+)?\.xml|rss|error)$/i});function Sn(e,t,r){return`/__mlis/ph/${e}x${t}/${r}.svg`}function Pn(e,t){if(T(e)&&e.toLowerCase().endsWith(".ascx")&&L(e).toLowerCase()!=="default.ascx")return{kind:"control",folder:P(e),control:L(e)};let r=T(e)?P(e):e,n=L(r);if(!t&&r.split("/").length===2&&n.startsWith("_"))return{kind:"route",folder:r,route:n.slice(1)};let s=t??(/home/i.test(n)?"home":/read/i.test(n)?"read":"category");if(!["home","category","read"].includes(s))throw Error("--as must be home, category or read");return{kind:s,folder:r}}function vi(e){let t=[];for(let r of e.matchAll(/<MLIS:(\w+)/gi)){let n="",s=r.index+r[0].length;for(;s<e.length;s++){let l=e[s];if(n){if(l===n)n=""}else if(l==='"'||l==="'")n=l;else if(l===">")break}let o=e.slice(r.index+r[0].length,s),i=(l)=>new RegExp(`\\b${l}\\s*=\\s*["']\\s*(-?\\d+)\\s*["']`,"i").exec(o)?.[1],c=i("CatID"),a=c===void 0?null:Number(c)>=0?Number(c):null;t.push({name:r[1],catId:a,location:Number(i("Location")??0)})}return t}function et(e){let t=[];for(let r of e.matchAll(/<MLIS:LoadPage\b[^>]*?\bSrc\s*=\s*"([^"]+)"/gi)){let n=/^\/?viewpage\/(.+)$/i.exec(r[1].trim());if(n)t.push(ce(n[1]))}return t}function Ln(e,t,r){let n=new Map,s=2000,o=(m)=>{let w={alias:"",description:`Mô tả mẫu của thư mục "${m.name}".`,html:`<p>Nội dung HTML mẫu của thư mục <strong>${m.name}</strong> (soạn trong /admin → Thư mục → Tài liệu HTML).</p>`,pageStyle:"",pageRead:"",link:"",locations:[0,0,0],bits:Ci|$i,type:2,picture:Sn(600,400,`thu-muc-${m.id}`),icon:"fa fa-folder-open",...m};return n.set(w.id,w),w},i=t.folder.split("/")[0],a=o({id:oe,parent:0,name:"Đại học Lạc Hồng",alias:"Đại Học Lạc Hồng",type:1,link:`{${e}}`,pageStyle:t.kind==="home"?t.folder:`${i}/@HomePage`,pageRead:`${i}/@ReadPage`,html:[1,2,3].map((m)=>`<div><img src="${Sn(1600,520,`banner-${m}`)}" alt="Banner mẫu ${m}" style="width:100%" /></div>`).join("")});if(t.kind!=="home"){let m=t.kind==="control"?`${jt}/${Ut(t)}`:t.kind==="route"?`${i}/@HomePage`:t.folder;a=o({id:ie,parent:oe,name:"Thư mục mẫu",alias:"Thư mục mẫu",pageStyle:m,pageRead:t.kind==="read"?t.folder:`${i}/@ReadPage`})}let l=(m)=>n.get(m)??o({id:m,parent:oe,name:`Thư mục ${m}`,pageStyle:a.pageStyle,pageRead:a.pageRead}),d=new Set,f=(m,w)=>{let p=`${m}|${w}`;if(d.has(p))return;d.add(p);for(let S=1;S<=3;S++){let I=o({id:s++,parent:m,name:w?`Mục mẫu ${w}.${S}`:`Mục mẫu ${S}`,locations:[w,0,0],pageStyle:a.pageStyle,pageRead:a.pageRead});for(let R=1;R<=2;R++)o({id:s++,parent:I.id,name:`${I.name} – con ${R}`})}};for(let m of r)for(let w of vi(m)){if(w.name.toLowerCase()==="loadpage")continue;let p=w.catId===null?[...new Set([a.id,oe])]:[w.catId===0?oe:l(w.catId).id];for(let S of p)if(w.location>0||/^cat(menu|related)$/i.test(w.name))f(S,w.location)}return f(a.id,0),f(oe,0),{host:e,home:oe,newsCount:12,categories:[...n.values()]}}function Ut(e){return`Wrap${(e.control??"control").replace(/\.ascx$/i,"").replace(/[^A-Za-z0-9_]/g,"")}`}function Nn(e){let t=e.folder.split("/")[0];return['<%@ Register TagPrefix="MLIS" Namespace="MLIS" %>',`<%-- mlis render: trang bọc để xem riêng ${e.folder}/${e.control} --%>`,`<MLIS:LoadPage Src="ViewPage/${t}/_assets/HeaderOfSite.ascx" runat="server" />`,`<MLIS:LoadPage CssFolder="ViewPage/${e.folder}" Src="ViewPage/${e.folder}/${e.control}" runat="server" />`,""].join(`\r
51
- `)}function Rn(e){switch(e.kind){case"home":return"/";case"read":return`/${ie}/${ie*1000+1}/tin-mau.html`;case"route":{let t=e.route;if(/^more-(list|related)$/i.test(t))return`/${t.toLowerCase()}/${ie}/0/0/thu-muc-mau.html`;if(/^more-news$/i.test(t))return`/more-news/${ie}/${ie*1000+1}/0/thu-muc-mau.html`;if(/^index$/i.test(t))return"/Index/A";if(/^search$/i.test(t))return"/search/?q=lac+hong";return`/${t}`}default:return`/${ie}/thu-muc-mau.html`}}var oe=1,ie=900,jt="_mlis/render",Ci=8,$i=4;var Ht=b(()=>{U()});function ki(e,t){try{if(e.startsWith("/"))return new URL(e,t);return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`)}catch{throw new u(`not a page address: ${e}`)}}async function bi(e,t){let r=Se(await e.api().tree(),t);if(!r)return null;let s=(await q(e,r.Path)??[]).find((i)=>i.Name.toLowerCase()==="default.ascx"),o=s?et((await e.api().read(`${r.Path}/${s.Name}`)).Content):[];return{template:r.Path,page:s?`${r.Path}/${s.Name}`:null,includes:o,head:`${r.Path.split("/")[0]}/_assets/HeaderOfSite.ascx`}}var Si,xn;var In=b(()=>{A();x();O();$t();bn();Ht();Ke();me();Si={name:"page",usage:"mlis page <url> [--pull] [--refresh]",summary:"Which ViewPage template draws a page of the site (any of its domains), what it includes, and what else uses it; --pull downloads them",options:{pull:{type:"boolean"},refresh:{type:"boolean"}},async run(e){let t=ki(C(e,0,"page address"),e.settings.url),r=await Je(e.workspace(),()=>e.api(),k(e.values,"refresh")),n=vn(r.index,t),s=n.template?await bi(e,n.template):null,o=n.template?kn(r.index,n.template):[],i=r.index;if(e.json)h(H({url:t.href,kind:n.kind,home:n.home&&{id:n.home.id,name:n.home.name,pageStyle:n.home.pageStyle},otherHomes:n.otherHomes.map((c)=>c.id),category:n.category&&{id:n.category.id,name:n.category.name,path:i.pathText(n.category.id)},newsId:n.newsId??null,template:s?.template??n.template??null,exists:s!==null,page:s?.page??null,includes:s?.includes??[],head:s?.head??null,usedBy:o.map((c)=>({id:c.category.id,name:c.category.name,role:c.role,domain:i.domainFor(c.category.id)})),note:n.note??null}));else{let c=[["url",t.href]];if(c.push(["kind",n.kind+(n.newsId?` (article ${n.newsId})`:"")]),n.home)c.push(["domain",`${n.host} → home ${n.home.id} ${n.home.name}${n.otherHomes.length?` (also linked: ${n.otherHomes.map((d)=>d.id).join(", ")})`:""}`]);if(n.category&&n.category!==n.home)c.push(["category",`${n.category.id} ${i.pathText(n.category.id)}`]);if(n.template)c.push(["template",s?`${s.template} (${s.page??"no default.ascx!"})`:`${n.template} (not on the server: the page fails)`]);if(s?.includes.length)c.push(["includes",s.includes.map((d)=>/\.ascx$/i.test(d)?d:`${d} (not an .ascx: loads nothing)`).join(", ")]);if(s)c.push(["head",s.head]);let a=new Set(o.map((d)=>d.category.id)).size;if(a>1)c.push(["shared",`${a} categories use this template: editing it changes all of them — mlis category pages ${s?.template??n.template}`]);if(n.newsId)c.push(["content",`article text/images: mlis news pull ${n.newsId}`]);if(n.note)c.push(["note",n.note]);let l=Math.max(...c.map(([d])=>d.length));h(`${c.map(([d,f])=>`${d.padEnd(l)} ${f}`).join(`
57
+ `)}},cs=[Ra,Na,Ea,Ta,Da,Fa]});function Ma(e){return[...e.matchAll(/\{([^}]+)\}/g)].map((t)=>t[1].trim().toLowerCase()).filter(Boolean)}function ms(e){return e.split("/")[0]??""}function Aa(e,t){let r=t.toLowerCase().replace(/^www\./,"");return e.items.filter((n)=>Ma(n.link).includes(r)).sort((n,s)=>e.ancestors(n.id).length-e.ancestors(s.id).length||(n.sort??0)-(s.sort??0)||n.id-s.id)}function fs(e,t){let r=t.hostname.toLowerCase(),n=t.pathname.split("/").filter(Boolean).map((d)=>decodeURIComponent(d)),s=Aa(e,r),o=s[0],i={host:r,path:t.pathname,kind:"unknown",home:o,otherHomes:s.slice(1)},c=(d)=>d&&/^\d+$/.test(d)?e.byId.get(Number(d)):void 0;if(n.length&&Oa.test(n[0]))return{...i,kind:"platform",note:`${n[0]} is generated by the platform (robotsTxt/llmsTxt/sitemapXml/rssFeeds/error .aspx), not a ViewPage template`};if(!o)return{...i,note:`no category has {${r}} in its link: the domain has no home page`};if(n.length===0)return{...i,kind:"home",category:o,template:o.pageStyle};let l=n[0];if(/^\d+$/.test(l)){let d=c(l);if(!d)return{...i,note:`category ${l} is unknown (or not visible to this account)`};let f=n[1];if(f&&/^\d+$/.test(f)&&Number(f)>0){let u=d.pageRead||d.pageStyle,w=d.pageRead?void 0:"the category has no PageRead; the article page then depends on the server's stored procedure (shown: PageStyle)";return{...i,kind:"read",category:d,newsId:Number(f),template:u,note:w}}return{...i,kind:"category",category:d,template:d.pageStyle}}let a=l.toLowerCase();if(a in us||a==="more-event"){let d=c(n[1]);if(!d)return{...i,note:`category ${n[1]??"?"} is unknown (or not visible to this account)`};let f=a==="more-news"&&n[2]&&/^\d+$/.test(n[2])?Number(n[2]):void 0,u=a==="more-event"?d.pageStyle:`${ms(d.pageStyle)}/${us[a]}`;return{...i,kind:"more",category:d,newsId:f,template:u}}if(/^\w+$/.test(l))return{...i,kind:"route",category:o,template:`${ms(o.pageStyle)}/_${l}`};return{...i,note:`/${l} matches no route: the site answers 404`}}function ps(e,t){let r=t.toLowerCase(),n=[];for(let s of e.items){if(s.pageStyle.toLowerCase()===r)n.push({category:s,role:"page"});if(s.pageRead.toLowerCase()===r)n.push({category:s,role:"read"})}return n}var us,Oa;var gs=k(()=>{us={"more-list":"_more-List","more-related":"_more-Related","more-news":"_more-News"},Oa=/^(robots\.txt|llms\.txt|sitemap(-[\w-]+)?\.xml|rss|error)$/i});function hs(e,t,r){return`/__mlis/ph/${e}x${t}/${r}.svg`}function ws(e,t){if(D(e)&&e.toLowerCase().endsWith(".ascx")&&L(e).toLowerCase()!=="default.ascx")return{kind:"control",folder:x(e),control:L(e)};let r=D(e)?x(e):e,n=L(r);if(!t&&r.split("/").length===2&&n.startsWith("_"))return{kind:"route",folder:r,route:n.slice(1)};let s=t??(/home/i.test(n)?"home":/read/i.test(n)?"read":"category");if(!["home","category","read"].includes(s))throw Error("--as must be home, category or read");return{kind:s,folder:r}}function Ua(e){let t=[];for(let r of e.matchAll(/<MLIS:(\w+)/gi)){let n="",s=r.index+r[0].length;for(;s<e.length;s++){let a=e[s];if(n){if(a===n)n=""}else if(a==='"'||a==="'")n=a;else if(a===">")break}let o=e.slice(r.index+r[0].length,s),i=(a)=>new RegExp(`\\b${a}\\s*=\\s*["']\\s*(-?\\d+)\\s*["']`,"i").exec(o)?.[1],c=i("CatID"),l=c===void 0?null:Number(c)>=0?Number(c):null;t.push({name:r[1],catId:l,location:Number(i("Location")??0)})}return t}function $t(e){let t=[];for(let r of e.matchAll(/<MLIS:LoadPage\b[^>]*?\bSrc\s*=\s*"([^"]+)"/gi)){let n=/^\/?viewpage\/(.+)$/i.exec(r[1].trim());if(n)t.push(Ce(n[1]))}return t}function ys(e,t,r){let n=new Map,s=2000,o=(u)=>{let w={alias:"",description:`Mô tả mẫu của thư mục "${u.name}".`,html:`<p>Nội dung HTML mẫu của thư mục <strong>${u.name}</strong> (soạn trong /admin → Thư mục → Tài liệu HTML).</p>`,pageStyle:"",pageRead:"",link:"",locations:[0,0,0],bits:_a|ja,type:2,picture:hs(600,400,`thu-muc-${u.id}`),icon:"fa fa-folder-open",...u};return n.set(w.id,w),w},i=t.folder.split("/")[0],l=o({id:pe,parent:0,name:"Đại học Lạc Hồng",alias:"Đại Học Lạc Hồng",type:1,link:`{${e}}`,pageStyle:t.kind==="home"?t.folder:`${i}/@HomePage`,pageRead:`${i}/@ReadPage`,html:[1,2,3].map((u)=>`<div><img src="${hs(1600,520,`banner-${u}`)}" alt="Banner mẫu ${u}" style="width:100%" /></div>`).join("")});if(t.kind!=="home"){let u=t.kind==="control"?`${mr}/${fr(t)}`:t.kind==="route"?`${i}/@HomePage`:t.folder;l=o({id:ge,parent:pe,name:"Thư mục mẫu",alias:"Thư mục mẫu",pageStyle:u,pageRead:t.kind==="read"?t.folder:`${i}/@ReadPage`})}let a=(u)=>n.get(u)??o({id:u,parent:pe,name:`Thư mục ${u}`,pageStyle:l.pageStyle,pageRead:l.pageRead}),d=new Set,f=(u,w)=>{let p=`${u}|${w}`;if(d.has(p))return;d.add(p);for(let S=1;S<=3;S++){let R=o({id:s++,parent:u,name:w?`Mục mẫu ${w}.${S}`:`Mục mẫu ${S}`,locations:[w,0,0],pageStyle:l.pageStyle,pageRead:l.pageRead});for(let I=1;I<=2;I++)o({id:s++,parent:R.id,name:`${R.name} – con ${I}`})}};for(let u of r)for(let w of Ua(u)){if(w.name.toLowerCase()==="loadpage")continue;let p=w.catId===null?[...new Set([l.id,pe])]:[w.catId===0?pe:a(w.catId).id];for(let S of p)if(w.location>0||/^cat(menu|related)$/i.test(w.name))f(S,w.location)}return f(l.id,0),f(pe,0),{host:e,home:pe,newsCount:12,categories:[...n.values()]}}function fr(e){return`Wrap${(e.control??"control").replace(/\.ascx$/i,"").replace(/[^A-Za-z0-9_]/g,"")}`}function Cs(e){let t=e.folder.split("/")[0];return['<%@ Register TagPrefix="MLIS" Namespace="MLIS" %>',`<%-- mlis render: trang bọc để xem riêng ${e.folder}/${e.control} --%>`,`<MLIS:LoadPage Src="ViewPage/${t}/_assets/HeaderOfSite.ascx" runat="server" />`,`<MLIS:LoadPage CssFolder="ViewPage/${e.folder}" Src="ViewPage/${e.folder}/${e.control}" runat="server" />`,""].join(`\r
58
+ `)}function $s(e){switch(e.kind){case"home":return"/";case"read":return`/${ge}/${ge*1000+1}/tin-mau.html`;case"route":{let t=e.route;if(/^more-(list|related)$/i.test(t))return`/${t.toLowerCase()}/${ge}/0/0/thu-muc-mau.html`;if(/^more-news$/i.test(t))return`/more-news/${ge}/${ge*1000+1}/0/thu-muc-mau.html`;if(/^index$/i.test(t))return"/Index/A";if(/^search$/i.test(t))return"/search/?q=lac+hong";return`/${t}`}default:return`/${ge}/thu-muc-mau.html`}}var pe=1,ge=900,mr="_mlis/render",_a=8,ja=4;var pr=k(()=>{K()});function Ha(e,t){try{if(e.startsWith("/"))return new URL(e,t);return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`)}catch{throw new m(`not a page address: ${e}`)}}async function Va(e,t){let r=je(await e.api().tree(),t);if(!r)return null;let s=(await Z(e,r.Path)??[]).find((i)=>i.Name.toLowerCase()==="default.ascx"),o=s?$t((await e.api().read(`${r.Path}/${s.Name}`)).Content):[];return{template:r.Path,page:s?`${r.Path}/${s.Name}`:null,includes:o,head:`${r.Path.split("/")[0]}/_assets/HeaderOfSite.ascx`}}var Ka,vs;var ks=k(()=>{j();N();M();He();gs();pr();mt();Ie();Ka={name:"page",usage:"mlis page <url> [--pull] [--refresh]",summary:"Which ViewPage template draws a page of the site (any of its domains), what it includes, and what else uses it; --pull downloads them",options:{pull:{type:"boolean"},refresh:{type:"boolean"}},async run(e){let t=Ha(C(e,0,"page address"),e.settings.url),r=await ve(e.workspace(),()=>e.api(),v(e.values,"refresh")),n=fs(r.index,t),s=n.template?await Va(e,n.template):null,o=n.template?ps(r.index,n.template):[],i=r.index;if(e.json)h(W({url:t.href,kind:n.kind,home:n.home&&{id:n.home.id,name:n.home.name,pageStyle:n.home.pageStyle},otherHomes:n.otherHomes.map((c)=>c.id),category:n.category&&{id:n.category.id,name:n.category.name,path:i.pathText(n.category.id)},newsId:n.newsId??null,template:s?.template??n.template??null,exists:s!==null,page:s?.page??null,includes:s?.includes??[],head:s?.head??null,usedBy:o.map((c)=>({id:c.category.id,name:c.category.name,role:c.role,domain:i.domainFor(c.category.id)})),note:n.note??null}));else{let c=[["url",t.href]];if(c.push(["kind",n.kind+(n.newsId?` (article ${n.newsId})`:"")]),n.home)c.push(["domain",`${n.host} → home ${n.home.id} ${n.home.name}${n.otherHomes.length?` (also linked: ${n.otherHomes.map((d)=>d.id).join(", ")})`:""}`]);if(n.category&&n.category!==n.home)c.push(["category",`${n.category.id} ${i.pathText(n.category.id)}`]);if(n.template)c.push(["template",s?`${s.template} (${s.page??"no default.ascx!"})`:`${n.template} (not on the server: the page fails)`]);if(s?.includes.length)c.push(["includes",s.includes.map((d)=>/\.ascx$/i.test(d)?d:`${d} (not an .ascx: loads nothing)`).join(", ")]);if(s)c.push(["head",s.head]);let l=new Set(o.map((d)=>d.category.id)).size;if(l>1)c.push(["shared",`${l} categories use this template: editing it changes all of them — mlis category pages ${s?.template??n.template}`]);if(n.newsId)c.push(["content",`article text/images: mlis news pull ${n.newsId}`]);if(n.note)c.push(["note",n.note]);let a=Math.max(...c.map(([d])=>d.length));h(`${c.map(([d,f])=>`${d.padEnd(a)} ${f}`).join(`
52
59
  `)}
53
- `)}if(k(e.values,"pull")&&s)for(let c of[s.template,...s.includes.filter((a)=>/\.ascx$/i.test(a)),s.head])await xt.run({...e,positionals:[c],values:{...e.values,force:!1,all:!1}});else if(s&&!e.json)g(`# edit it locally: mlis page ${C(e,0,"page address")} --pull`)}},xn=[Si]});import{spawn as Pi,spawnSync as ye}from"node:child_process";import{existsSync as W,openSync as Li}from"node:fs";import{copyFile as Ni,mkdir as tt,readdir as Ri,readFile as Kt,stat as rt,writeFile as Vt}from"node:fs/promises";import{dirname as Wt,join as y,relative as Hc,resolve as we}from"node:path";import{fileURLToPath as xi}from"node:url";function Q(){return y(lt(),"render")}function Fn(){let e=Wt(xi(import.meta.url)),t=[y(e,"..","..","render"),y(e,"..","render"),y(Wt(process.execPath),"render")];return t.find((r)=>W(y(r,"MlisRender.cs")))??t[0]}function En(){if(process.platform!=="win32")throw new u("mlis render needs Windows with .NET Framework 4 (the MLIS site is ASP.NET)");let e=Ii.find((t)=>W(y(t,"csc.exe")));if(!e)throw new u(".NET Framework 4 csc.exe not found; install .NET Framework 4.x");return y(e,"csc.exe")}async function nt(e,t){if(!W(e))return!0;let r=(await rt(e)).mtimeMs;for(let n of t)if((await rt(n)).mtimeMs>r)return!0;return!1}function Tn(e,t){let r=ye(e,t,{encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw Error(`${e} failed:
54
- ${(r.stdout||"")+(r.stderr||"")}`.trim())}async function Di(e){let t=Fn(),r=y(Q(),"kit");await tt(r,{recursive:!0});let n=y(e,"bin","Newtonsoft.Json.dll");if(!W(n))throw new u(`${e}\\bin\\Newtonsoft.Json.dll not found: is site-src the MLIS site?`);let s=y(r,"DataConnection.dll");if(await nt(s,[y(t,"SampleDataConnection.cs")]))Tn(En(),["/nologo","/target:library",`/out:${s}`,"/r:System.Data.dll","/r:System.Core.dll",`/r:${n}`,y(t,"SampleDataConnection.cs")]);let o=y(r,"MlisRender.exe");if(await nt(o,[y(t,"MlisRender.cs")]))Tn(En(),["/nologo","/target:exe",`/out:${o}`,"/r:System.Web.dll","/r:System.dll",y(t,"MlisRender.cs")]);return r}async function Oi(){let e=Fn(),t=y(Q(),"kit");return await nt(y(t,"DataConnection.dll"),[y(e,"SampleDataConnection.cs")])||await nt(y(t,"MlisRender.exe"),[y(e,"MlisRender.cs")])}async function Bt(e,t){let r=await rt(e),n=W(t)?await rt(t):null;if(!n||n.size!==r.size||n.mtimeMs<r.mtimeMs)await Ni(e,t)}async function Mi(e,t){if(W(e)&&await Kt(e,"utf-8")===t)return;await Vt(e,t,"utf-8")}function Ai(e){if(/<globalization\b[^>]*fileEncoding/i.test(e))return e;if(/<globalization\b/i.test(e))return e.replace(/<globalization\b/i,'<globalization fileEncoding="utf-8"');return e.replace(/<system\.web>/i,`<system.web>\r
55
- <globalization fileEncoding="utf-8" />`)}async function Dn(e,t,r=!0){await tt(t,{recursive:!0});for(let n of await Ri(e,{withFileTypes:!0})){if(r&&(Ei.has(n.name.toLowerCase())||n.name.toLowerCase()===Fi))continue;let s=y(e,n.name),o=y(t,n.name);if(n.isDirectory())await Dn(s,o,!1);else if(n.isFile()&&!Ti.test(n.name))await Bt(s,o)}}function _i(e){let t=ye("cmd",["/c","dir","/al",Wt(e)],{encoding:"utf-8",windowsHide:!0}),r=new RegExp(`<JUNCTION>\\s+${ji(e)}\\s+\\[([^\\]]+)\\]`,"i").exec(t.stdout??"");return r?r[1]:null}function ji(e){return e.split(/[\\/]/).pop().replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Ui(e,t){let r=y(e,"ViewPage"),n=W(r)?_i(r):null;if(n&&we(n).toLowerCase()===we(t).toLowerCase())return;if(W(r)){if(!n)throw new u(`${r} exists and is not a junction; remove it`);ye("cmd",["/c","rmdir",r],{windowsHide:!0})}let s=ye("cmd",["/c","mklink","/J",r,t],{encoding:"utf-8",windowsHide:!0});if(s.status!==0)throw Error(`cannot link ${r} to ${t}: ${s.stdout}${s.stderr}`)}async function Hi(e,t){if(!W(y(e,"Web.config"))||!W(y(e,"bin")))throw new u(`site-src ${e} is not the MLIS site (needs Web.config and bin\\); set it: mlis config set site-src <folder>`);let r=await Di(e),n=y(Q(),"site");return await Dn(e,n),await Mi(y(n,"Web.config"),Ai(await Kt(y(e,"Web.config"),"utf-8"))),await Bt(y(r,"DataConnection.dll"),y(n,"bin","DataConnection.dll")),await Bt(y(r,"MlisRender.exe"),y(n,"bin","MlisRender.exe")),await tt(y(n,"Data"),{recursive:!0}),await tt(t,{recursive:!0}),Ui(n,t),n}function Me(){return y(Q(),"data.json")}async function On(){try{return JSON.parse(await Kt(y(Q(),"server.json"),"utf-8"))}catch{return null}}async function Oe(e){try{let t=await fetch(`http://localhost:${e}/__mlis/ping`,{signal:AbortSignal.timeout(1500)});return t.ok&&await t.text()==="mlis render"}catch{return!1}}async function Jt(){let e=await On();if(!e||!await Oe(e.port))return!1;return await fetch(`http://localhost:${e.port}/__mlis/stop`).catch(()=>{return}),!0}async function Mn(e,t){let r=e.resolve("site-src").value;if(!r)throw new u("where is the MLIS site code? run: mlis config set site-src <MLIS_2025 folder or its publish folder>");let n=Number(e.resolve("render-port").value),s=await On();if(s&&s.port===n&&await Oe(n)){if(!await Oi()&&we(s.workspace).toLowerCase()===we(t).toLowerCase()&&we(s.siteSrc).toLowerCase()===we(r).toLowerCase())return s;await Jt(),await new Promise((f)=>setTimeout(f,800))}else if(await Oe(n))throw new u(`port ${n} is used by another program; pick one: mlis config set render-port <port>`);let o=await Hi(r,t);if(!W(Me()))await Vt(Me(),`${JSON.stringify({host:new URL(e.url).hostname,home:1,newsCount:12,categories:[]})}
56
- `);let i=Li(y(Q(),"server.log"),"a"),c=new URL(e.url),a=["--site",o,"--port",String(n),"--host",c.hostname,"--origin",c.origin,"--cache",y(Q(),"live")],l=Pi(y(o,"bin","MlisRender.exe"),a,{detached:!0,stdio:["ignore",i,i],windowsHide:!0,env:{...process.env,MLIS_RENDER_DATA:Me()}});l.unref();for(let f=0;f<60&&!await Oe(n);f++)await new Promise((m)=>setTimeout(m,500));if(!await Oe(n))throw Error(`render server did not start; see ${y(Q(),"server.log")}`);let d={pid:l.pid??0,port:n,workspace:t,siteSrc:r};return await Vt(y(Q(),"server.json"),`${JSON.stringify(d)}
57
- `),d}async function An(e){let t=await fetch(e,{signal:AbortSignal.timeout(180000),redirect:"manual"});for(let a=0;a<60&&t.status===503;a++)await new Promise((l)=>setTimeout(l,1000)),t=await fetch(e,{signal:AbortSignal.timeout(180000),redirect:"manual"});let r=await t.text(),n=t.headers.get("location")??"";if(t.status>=300&&t.status<400&&/^\/error/i.test(new URL(n,e).pathname))return{status:t.status,error:`the page control failed (the site redirects to ${n})`};if(t.status<500)return{status:t.status,error:null};let s=r.replace(/<style[\s\S]*?<\/style>/gi,"").replace(/<[^>]+>/g," ").replace(/&nbsp;/g," ").replace(/\s+/g," "),o=(a)=>new RegExp(`${a}:?\\s*(.{0,300}?)(?=\\s(Source Error|Source File|Stack Trace|Line \\d+|Version Information)|$)`).exec(s)?.[1]?.trim(),i=o("Parser Error Message")??o("Compiler Error Message")??o("Exception Details")??s.slice(0,300),c=/Source File:\s*(\S+)\s+Line:\s*(\d+)/.exec(s);return{status:t.status,error:`${i}${c?` (${c[1]} line ${c[2]})`:""}`}}async function _n(e,t){return(await(await fetch(`http://localhost:${e}/__mlis/compile?path=${encodeURIComponent(`ViewPage/${t}`)}`,{signal:AbortSignal.timeout(120000)})).text()).trim()}function jn(){let e=process.env.USERPROFILE??process.env.HOME??"",t=[y(e,".bun","bin","playwright.exe"),y(e,"AppData","Roaming","npm","playwright.cmd")],r=ye("where",["playwright"],{encoding:"utf-8",windowsHide:!0});if(r.status===0)t.unshift(...r.stdout.split(/\r?\n/).filter(Boolean));return t.find((n)=>W(n))??null}function Un(e,t,r,n,s){let o=["screenshot","--wait-for-timeout","1500","--ignore-https-errors"];if(o.push("--browser","chromium","--viewport-size",n?"390, 844":"1366, 900"),s)o.push("--full-page");o.push(t,r);let i=ye(e,o,{encoding:"utf-8",windowsHide:!0,shell:e.endsWith(".cmd")});if(i.status!==0)throw Error(`playwright screenshot failed: ${(i.stdout||"")+(i.stderr||"")}`.trim())}var Ii,Ei,Ti,Fi="web.config";var Hn=b(()=>{x();We();Ii=["C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319","C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319"],Ei=new Set(["viewpage","data","app_data","obj",".vs","aspnet_client"]),Ti=/\.(sln|suo|publishproj|user|doc|docx)$/i});import{mkdir as Wn,writeFile as Vn}from"node:fs/promises";import{join as zt,resolve as Vi}from"node:path";async function Kn(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"ViewPage folder or file")),n=Pn(r,v(e.values,"as")),s=n.folder.split("/")[0],o=n.kind==="control"?`${n.folder}/${n.control}`:`${n.folder}/Default.ascx`,i=n.kind==="home"||n.kind==="category"||n.kind==="read"||n.kind==="control"?o:`${s}/@HomePage/Default.ascx`;if(await t.readLocal(o)===null)throw new u(`${o} is not in the workspace; run: mlis pull ${n.folder}`);let c=[],a=[],l=new Set,d=[o,i,`${s}/_assets/HeaderOfSite.ascx`];while(d.length){let p=d.shift();if(l.has(p.toLowerCase()))continue;l.add(p.toLowerCase());let S=await t.readLocalBytes(p);if(!S)continue;let I=G(S);c.push(I),a.push(p),d.push(...et(I))}let f=new URL(e.settings.url).hostname,m=Ln(f,n,c);if(n.kind==="control"){let p=t.localPath(`${jt}/${Ut(n)}`);await Wn(p,{recursive:!0}),await Vn(zt(p,"default.ascx"),`\uFEFF${Nn(n)}`,"utf-8")}let w=await Mn(e.settings,t.root);return await Vn(Me(),`${JSON.stringify(m,null,1)}
58
- `,"utf-8"),{target:n,url:`http://localhost:${w.port}${Rn(n)}`,port:w.port,files:a}}async function Jn({url:e,port:t,files:r}){g(`# rendering ${e} (the first request compiles the site, up to a minute)`);let n=await An(e),s=[];for(let o of r.filter((i)=>i.toLowerCase().endsWith(".ascx"))){let i=await _n(t,o);if(i)s.push(`${o}: ${i}`)}if(n.error)s.unshift(`the page does not render (HTTP ${n.status}): ${n.error}`);if(s.length)throw Error(s.join(`
59
- `));if(n.status>=400)g(`# warning: HTTP ${n.status}`)}var Bn,Wi,Bi,Ki,zn;var Gn=b(()=>{A();x();O();Ht();Hn();me();Bn={as:{type:"string"}};Wi={name:"render start",usage:"mlis render start <folder|file> [--as home|category|read]",summary:"Run the page locally with sample data (no server writes) and print its http://localhost URL",options:Bn,async run(e){let t=await Kn(e);await Jn(t),h(`${t.url}
60
- `)}},Bi={name:"render shot",usage:"mlis render shot <folder|file> [--as home|category|read] [--device desktop|mobile|both] [--full] [-o dir]",summary:"Render the page locally with sample data and save screenshots (desktop 1366px, mobile 390px)",options:{...Bn,device:{type:"string"},full:{type:"boolean"},out:{type:"string",short:"o"}},async run(e){let t=v(e.values,"device")??"both";if(!["desktop","mobile","both"].includes(t))throw new u("--device must be desktop, mobile or both");let r=jn();if(!r)throw new u("playwright not found; install it: bun add -g playwright && playwright install chromium");let n=await Kn(e),{target:s,url:o}=n;await Jn(n);let i=Vi(v(e.values,"out")??zt(e.workspace().root,"_mlis","shots"));await Wn(i,{recursive:!0});let c=`${s.folder}${s.control?`/${s.control.replace(/\.ascx$/i,"")}`:""}`.replace(/[\\/@]+/g,"_").replace(/^_+/,""),a=[];for(let l of t==="both"?["desktop","mobile"]:[t]){let d=zt(i,`${c}-${l}.png`);Un(r,o,d,l==="mobile",k(e.values,"full")),a.push(d)}h(`${a.join(`
60
+ `)}if(v(e.values,"pull")&&s)for(let c of[s.template,...s.includes.filter((l)=>/\.ascx$/i.test(l)),s.head])await lr.run({...e,positionals:[c],values:{...e.values,force:!1,all:!1}});else if(s&&!e.json)g(`# edit it locally: mlis page ${C(e,0,"page address")} --pull`)}},vs=[Ka]});import{spawn as Wa,spawnSync as Te}from"node:child_process";import{existsSync as B,openSync as Ba}from"node:fs";import{copyFile as Ja,mkdir as vt,readdir as za,readFile as yr,stat as kt,writeFile as gr}from"node:fs/promises";import{dirname as hr,join as y,relative as Ou,resolve as Ee}from"node:path";import{fileURLToPath as Ga}from"node:url";function te(){return y(Oe(),"render")}function Ps(){let e=hr(Ga(import.meta.url)),t=[y(e,"..","..","render"),y(e,"..","render"),y(hr(process.execPath),"render")];return t.find((r)=>B(y(r,"MlisRender.cs")))??t[0]}function bs(){if(process.platform!=="win32")throw new m("mlis render needs Windows with .NET Framework 4 (the MLIS site is ASP.NET)");let e=qa.find((t)=>B(y(t,"csc.exe")));if(!e)throw new m(".NET Framework 4 csc.exe not found; install .NET Framework 4.x");return y(e,"csc.exe")}async function bt(e,t){if(!B(e))return!0;let r=(await kt(e)).mtimeMs;for(let n of t)if((await kt(n)).mtimeMs>r)return!0;return!1}function Ss(e,t){let r=Te(e,t,{encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw Error(`${e} failed:
61
+ ${(r.stdout||"")+(r.stderr||"")}`.trim())}async function Qa(e){let t=Ps(),r=y(te(),"kit");await vt(r,{recursive:!0});let n=y(e,"bin","Newtonsoft.Json.dll");if(!B(n))throw new m(`${e}\\bin\\Newtonsoft.Json.dll not found: is site-src the MLIS site?`);let s=y(r,"DataConnection.dll");if(await bt(s,[y(t,"SampleDataConnection.cs")]))Ss(bs(),["/nologo","/target:library",`/out:${s}`,"/r:System.Data.dll","/r:System.Core.dll",`/r:${n}`,y(t,"SampleDataConnection.cs")]);let o=y(r,"MlisRender.exe");if(await bt(o,[y(t,"MlisRender.cs")]))Ss(bs(),["/nologo","/target:exe",`/out:${o}`,"/r:System.Web.dll","/r:System.dll",y(t,"MlisRender.cs")]);return r}async function el(){let e=Ps(),t=y(te(),"kit");return await bt(y(t,"DataConnection.dll"),[y(e,"SampleDataConnection.cs")])||await bt(y(t,"MlisRender.exe"),[y(e,"MlisRender.cs")])}async function wr(e,t){let r=await kt(e),n=B(t)?await kt(t):null;if(!n||n.size!==r.size||n.mtimeMs<r.mtimeMs)await Ja(e,t)}async function tl(e,t){if(B(e)&&await yr(e,"utf-8")===t)return;await gr(e,t,"utf-8")}function rl(e){if(/<globalization\b[^>]*fileEncoding/i.test(e))return e;if(/<globalization\b/i.test(e))return e.replace(/<globalization\b/i,'<globalization fileEncoding="utf-8"');return e.replace(/<system\.web>/i,`<system.web>\r
62
+ <globalization fileEncoding="utf-8" />`)}async function Is(e,t,r=!0){await vt(t,{recursive:!0});for(let n of await za(e,{withFileTypes:!0})){if(r&&(Xa.has(n.name.toLowerCase())||n.name.toLowerCase()===Za))continue;let s=y(e,n.name),o=y(t,n.name);if(n.isDirectory())await Is(s,o,!1);else if(n.isFile()&&!Ya.test(n.name))await wr(s,o)}}function nl(e){let t=Te("cmd",["/c","dir","/al",hr(e)],{encoding:"utf-8",windowsHide:!0}),r=new RegExp(`<JUNCTION>\\s+${sl(e)}\\s+\\[([^\\]]+)\\]`,"i").exec(t.stdout??"");return r?r[1]:null}function sl(e){return e.split(/[\\/]/).pop().replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function ol(e,t){let r=y(e,"ViewPage"),n=B(r)?nl(r):null;if(n&&Ee(n).toLowerCase()===Ee(t).toLowerCase())return;if(B(r)){if(!n)throw new m(`${r} exists and is not a junction; remove it`);Te("cmd",["/c","rmdir",r],{windowsHide:!0})}let s=Te("cmd",["/c","mklink","/J",r,t],{encoding:"utf-8",windowsHide:!0});if(s.status!==0)throw Error(`cannot link ${r} to ${t}: ${s.stdout}${s.stderr}`)}async function il(e,t){if(!B(y(e,"Web.config"))||!B(y(e,"bin")))throw new m(`site-src ${e} is not the MLIS site (needs Web.config and bin\\); set it: mlis config set site-src <folder>`);let r=await Qa(e),n=y(te(),"site");return await Is(e,n),await tl(y(n,"Web.config"),rl(await yr(y(e,"Web.config"),"utf-8"))),await wr(y(r,"DataConnection.dll"),y(n,"bin","DataConnection.dll")),await wr(y(r,"MlisRender.exe"),y(n,"bin","MlisRender.exe")),await vt(y(n,"Data"),{recursive:!0}),await vt(t,{recursive:!0}),ol(n,t),n}function st(){return y(te(),"data.json")}async function xs(){try{return JSON.parse(await yr(y(te(),"server.json"),"utf-8"))}catch{return null}}async function nt(e){try{let t=await fetch(`http://localhost:${e}/__mlis/ping`,{signal:AbortSignal.timeout(1500)});return t.ok&&await t.text()==="mlis render"}catch{return!1}}async function Cr(){let e=await xs();if(!e||!await nt(e.port))return!1;return await fetch(`http://localhost:${e.port}/__mlis/stop`).catch(()=>{return}),!0}async function Ls(e,t){let r=e.resolve("site-src").value;if(!r)throw new m("where is the MLIS site code? run: mlis config set site-src <MLIS_2025 folder or its publish folder>");let n=Number(e.resolve("render-port").value),s=await xs();if(s&&s.port===n&&await nt(n)){if(!await el()&&Ee(s.workspace).toLowerCase()===Ee(t).toLowerCase()&&Ee(s.siteSrc).toLowerCase()===Ee(r).toLowerCase())return s;await Cr(),await new Promise((f)=>setTimeout(f,800))}else if(await nt(n))throw new m(`port ${n} is used by another program; pick one: mlis config set render-port <port>`);let o=await il(r,t);if(!B(st()))await gr(st(),`${JSON.stringify({host:new URL(e.url).hostname,home:1,newsCount:12,categories:[]})}
63
+ `);let i=Ba(y(te(),"server.log"),"a"),c=new URL(e.url),l=["--site",o,"--port",String(n),"--host",c.hostname,"--origin",c.origin,"--cache",y(te(),"live")],a=Wa(y(o,"bin","MlisRender.exe"),l,{detached:!0,stdio:["ignore",i,i],windowsHide:!0,env:{...process.env,MLIS_RENDER_DATA:st()}});a.unref();for(let f=0;f<60&&!await nt(n);f++)await new Promise((u)=>setTimeout(u,500));if(!await nt(n))throw Error(`render server did not start; see ${y(te(),"server.log")}`);let d={pid:a.pid??0,port:n,workspace:t,siteSrc:r};return await gr(y(te(),"server.json"),`${JSON.stringify(d)}
64
+ `),d}async function Rs(e){let t=await fetch(e,{signal:AbortSignal.timeout(180000),redirect:"manual"});for(let l=0;l<60&&t.status===503;l++)await new Promise((a)=>setTimeout(a,1000)),t=await fetch(e,{signal:AbortSignal.timeout(180000),redirect:"manual"});let r=await t.text(),n=t.headers.get("location")??"";if(t.status>=300&&t.status<400&&/^\/error/i.test(new URL(n,e).pathname))return{status:t.status,error:`the page control failed (the site redirects to ${n})`};if(t.status<500)return{status:t.status,error:null};let s=r.replace(/<style[\s\S]*?<\/style>/gi,"").replace(/<[^>]+>/g," ").replace(/&nbsp;/g," ").replace(/\s+/g," "),o=(l)=>new RegExp(`${l}:?\\s*(.{0,300}?)(?=\\s(Source Error|Source File|Stack Trace|Line \\d+|Version Information)|$)`).exec(s)?.[1]?.trim(),i=o("Parser Error Message")??o("Compiler Error Message")??o("Exception Details")??s.slice(0,300),c=/Source File:\s*(\S+)\s+Line:\s*(\d+)/.exec(s);return{status:t.status,error:`${i}${c?` (${c[1]} line ${c[2]})`:""}`}}async function Ns(e,t){return(await(await fetch(`http://localhost:${e}/__mlis/compile?path=${encodeURIComponent(`ViewPage/${t}`)}`,{signal:AbortSignal.timeout(120000)})).text()).trim()}function Es(){let e=process.env.USERPROFILE??process.env.HOME??"",t=[y(e,".bun","bin","playwright.exe"),y(e,"AppData","Roaming","npm","playwright.cmd")],r=Te("where",["playwright"],{encoding:"utf-8",windowsHide:!0});if(r.status===0)t.unshift(...r.stdout.split(/\r?\n/).filter(Boolean));return t.find((n)=>B(n))??null}function Ts(e,t,r,n,s){let o=["screenshot","--wait-for-timeout","1500","--ignore-https-errors"];if(o.push("--browser","chromium","--viewport-size",n?"390, 844":"1366, 900"),s)o.push("--full-page");o.push(t,r);let i=Te(e,o,{encoding:"utf-8",windowsHide:!0,shell:e.endsWith(".cmd")});if(i.status!==0)throw Error(`playwright screenshot failed: ${(i.stdout||"")+(i.stderr||"")}`.trim())}var qa,Xa,Ya,Za="web.config";var Ds=k(()=>{N();de();qa=["C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319","C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319"],Xa=new Set(["viewpage","data","app_data","obj",".vs","aspnet_client"]),Ya=/\.(sln|suo|publishproj|user|doc|docx)$/i});import{mkdir as Os,writeFile as Fs}from"node:fs/promises";import{join as $r,resolve as al}from"node:path";async function As(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"ViewPage folder or file")),n=ws(r,b(e.values,"as")),s=n.folder.split("/")[0],o=n.kind==="control"?`${n.folder}/${n.control}`:`${n.folder}/Default.ascx`,i=n.kind==="home"||n.kind==="category"||n.kind==="read"||n.kind==="control"?o:`${s}/@HomePage/Default.ascx`;if(await t.readLocal(o)===null)throw new m(`${o} is not in the workspace; run: mlis pull ${n.folder}`);let c=[],l=[],a=new Set,d=[o,i,`${s}/_assets/HeaderOfSite.ascx`];while(d.length){let p=d.shift();if(a.has(p.toLowerCase()))continue;a.add(p.toLowerCase());let S=await t.readLocalBytes(p);if(!S)continue;let R=Y(S);c.push(R),l.push(p),d.push(...$t(R))}let f=new URL(e.settings.url).hostname,u=ys(f,n,c);if(n.kind==="control"){let p=t.localPath(`${mr}/${fr(n)}`);await Os(p,{recursive:!0}),await Fs($r(p,"default.ascx"),`\uFEFF${Cs(n)}`,"utf-8")}let w=await Ls(e.settings,t.root);return await Fs(st(),`${JSON.stringify(u,null,1)}
65
+ `,"utf-8"),{target:n,url:`http://localhost:${w.port}${$s(n)}`,port:w.port,files:l}}async function _s({url:e,port:t,files:r}){g(`# rendering ${e} (the first request compiles the site, up to a minute)`);let n=await Rs(e),s=[];for(let o of r.filter((i)=>i.toLowerCase().endsWith(".ascx"))){let i=await Ns(t,o);if(i)s.push(`${o}: ${i}`)}if(n.error)s.unshift(`the page does not render (HTTP ${n.status}): ${n.error}`);if(s.length)throw Error(s.join(`
66
+ `));if(n.status>=400)g(`# warning: HTTP ${n.status}`)}var Ms,ll,cl,dl,js;var Us=k(()=>{j();N();M();pr();Ds();Ie();Ms={as:{type:"string"}};ll={name:"render start",usage:"mlis render start <folder|file> [--as home|category|read]",summary:"Run the page locally with sample data (no server writes) and print its http://localhost URL",options:Ms,async run(e){let t=await As(e);await _s(t),h(`${t.url}
67
+ `)}},cl={name:"render shot",usage:"mlis render shot <folder|file> [--as home|category|read] [--device desktop|mobile|both] [--full] [-o dir]",summary:"Render the page locally with sample data and save screenshots (desktop 1366px, mobile 390px)",options:{...Ms,device:{type:"string"},full:{type:"boolean"},out:{type:"string",short:"o"}},async run(e){let t=b(e.values,"device")??"both";if(!["desktop","mobile","both"].includes(t))throw new m("--device must be desktop, mobile or both");let r=Es();if(!r)throw new m("playwright not found; install it: bun add -g playwright && playwright install chromium");let n=await As(e),{target:s,url:o}=n;await _s(n);let i=al(b(e.values,"out")??$r(e.workspace().root,"_mlis","shots"));await Os(i,{recursive:!0});let c=`${s.folder}${s.control?`/${s.control.replace(/\.ascx$/i,"")}`:""}`.replace(/[\\/@]+/g,"_").replace(/^_+/,""),l=[];for(let a of t==="both"?["desktop","mobile"]:[t]){let d=$r(i,`${c}-${a}.png`);Ts(r,o,d,a==="mobile",v(e.values,"full")),l.push(d)}h(`${l.join(`
61
68
  `)}
62
- `)}},Ki={name:"render stop",usage:"mlis render stop",summary:"Stop the local render server",async run(){g(await Jt()?"# render server stopped":"# render server is not running")}},zn=[Wi,Bi,Ki]});import{existsSync as Gt}from"node:fs";import{cp as Ji,readFile as zi,readdir as qn,rm as Gi,writeFile as qi}from"node:fs/promises";import{homedir as Xi}from"node:os";import{dirname as st,join as _}from"node:path";import{fileURLToPath as Xn}from"node:url";function ot(){let e=st(Xn(import.meta.url)),t=[_(e,"..","..","skills",Ce),_(e,"..","skills",Ce),_(st(process.execPath),"skills",Ce),_(st(process.execPath),"..","skills",Ce)];return t.find((r)=>Gt(_(r,"SKILL.md")))??t[0]}function ae(e=process.env){return _(e.CLAUDE_CONFIG_DIR||_(e.HOME||e.USERPROFILE||Xi(),".claude"),"skills")}async function Yn(e,t){let r=new Map((await qn(e,{withFileTypes:!0})).map((n)=>[n.name,n]));for(let n of await qn(t,{withFileTypes:!0})){if(n.name===qt)continue;let s=r.get(n.name),o=_(t,n.name);if(!s)await Gi(o,{recursive:!0,force:!0});else if(n.isDirectory()&&s.isDirectory())await Yn(_(e,n.name),o)}}async function Xt(e,t=ae()){let r=ot();if(!Gt(_(r,"SKILL.md")))throw Error(`skill bundle not found at ${r}`);let n=_(t,Ce);return await Ji(r,n,{recursive:!0,force:!0}),await Yn(r,n),await qi(_(n,qt),`${e}
63
- `,"utf-8"),n}async function Yi(e=ae()){try{return(await zi(_(e,Ce,qt),"utf-8")).trim()}catch{return null}}async function Zn(e,t=process.env){let r=t.MLIS_AUTO_SKILLS?.trim().toLowerCase();if(r&&["0","off","false","no"].includes(r))return null;if(Xn(import.meta.url).endsWith(".ts"))return null;let s=st(ae(t));if(!Gt(s))return null;if(await Yi(ae(t))===e)return null;try{let o=await Xt(e,ae(t));return`# mlis ${e} skill installed at ${o}`}catch(o){return`# mlis skill not installed: ${o.message}; run: mlis skill install`}}var Ce="mlis",qt=".mlis-version";var Yt=()=>{};import{readFile as Qn,readdir as Zi}from"node:fs/promises";import{join as Zt}from"node:path";var Qi,ea,ta,es;var ts=b(()=>{x();O();Yt();at();Qi={name:"skill",usage:"mlis skill",summary:"Print the agent guide (SKILL.md)",async run(){h(await Qn(Zt(ot(),"SKILL.md"),"utf-8"))}},ea={name:"skill ref",usage:"mlis skill ref [name]",summary:"List the reference pages, or print one (e.g. controls)",async run(e){let t=Zt(ot(),"references"),r=(await Zi(t)).filter((o)=>o.endsWith(".md")).map((o)=>o.slice(0,-3)),n=e.positionals[0];if(!n){h(`${r.join(`
69
+ `)}},dl={name:"render stop",usage:"mlis render stop",summary:"Stop the local render server",async run(){g(await Cr()?"# render server stopped":"# render server is not running")}},js=[ll,cl,dl]});import{existsSync as vr}from"node:fs";import{cp as ul,readFile as ml,readdir as Hs,rm as fl,writeFile as pl}from"node:fs/promises";import{homedir as gl}from"node:os";import{dirname as St,join as H}from"node:path";import{fileURLToPath as Vs}from"node:url";function Pt(){let e=St(Vs(import.meta.url)),t=[H(e,"..","..","skills",De),H(e,"..","skills",De),H(St(process.execPath),"skills",De),H(St(process.execPath),"..","skills",De)];return t.find((r)=>vr(H(r,"SKILL.md")))??t[0]}function he(e=process.env){return H(e.CLAUDE_CONFIG_DIR||H(e.HOME||e.USERPROFILE||gl(),".claude"),"skills")}async function Ks(e,t){let r=new Map((await Hs(e,{withFileTypes:!0})).map((n)=>[n.name,n]));for(let n of await Hs(t,{withFileTypes:!0})){if(n.name===kr)continue;let s=r.get(n.name),o=H(t,n.name);if(!s)await fl(o,{recursive:!0,force:!0});else if(n.isDirectory()&&s.isDirectory())await Ks(H(e,n.name),o)}}async function br(e,t=he()){let r=Pt();if(!vr(H(r,"SKILL.md")))throw Error(`skill bundle not found at ${r}`);let n=H(t,De);return await ul(r,n,{recursive:!0,force:!0}),await Ks(r,n),await pl(H(n,kr),`${e}
70
+ `,"utf-8"),n}async function hl(e=he()){try{return(await ml(H(e,De,kr),"utf-8")).trim()}catch{return null}}async function Ws(e,t=process.env){let r=t.MLIS_AUTO_SKILLS?.trim().toLowerCase();if(r&&["0","off","false","no"].includes(r))return null;if(Vs(import.meta.url).endsWith(".ts"))return null;let s=St(he(t));if(!vr(s))return null;if(await hl(he(t))===e)return null;try{let o=await br(e,he(t));return`# mlis ${e} skill installed at ${o}`}catch(o){return`# mlis skill not installed: ${o.message}; run: mlis skill install`}}var De="mlis",kr=".mlis-version";var Sr=()=>{};import{readFile as Bs,readdir as wl}from"node:fs/promises";import{join as Pr}from"node:path";var yl,Cl,$l,Js;var zs=k(()=>{N();M();Sr();Rt();yl={name:"skill",usage:"mlis skill",summary:"Print the agent guide (SKILL.md)",async run(){h(await Bs(Pr(Pt(),"SKILL.md"),"utf-8"))}},Cl={name:"skill ref",usage:"mlis skill ref [name]",summary:"List the reference pages, or print one (e.g. controls)",async run(e){let t=Pr(Pt(),"references"),r=(await wl(t)).filter((o)=>o.endsWith(".md")).map((o)=>o.slice(0,-3)),n=e.positionals[0];if(!n){h(`${r.join(`
64
71
  `)}
65
- `);return}let s=n.replace(/\.md$/,"");if(!r.includes(s))throw new u(`no reference "${s}"; available: ${r.join(", ")}`);h(await Qn(Zt(t,`${s}.md`),"utf-8"))}},ta={name:"skill install",usage:"mlis skill install [dir]",summary:"Copy the skill into Claude Code (~/.claude/skills/mlis) or another skills folder",async run(e){let t=e.positionals[0]??ae(),r=await Xt(Ae.version,t);g(`# skill installed at ${r}`)}},es=[Qi,ea,ta]});import{parseArgs as ra}from"node:util";function ss(e){let t=Math.max(...e.map((r)=>r.name.length));return e.map((r)=>` ${r.name.padEnd(t)} ${r.summary}`).join(`
66
- `)}function rs(){return`mlis ${Qt}: design MLIS ViewPage (.ascx/.css/.js) through the site's API
72
+ `);return}let s=n.replace(/\.md$/,"");if(!r.includes(s))throw new m(`no reference "${s}"; available: ${r.join(", ")}`);h(await Bs(Pr(t,`${s}.md`),"utf-8"))}},$l={name:"skill install",usage:"mlis skill install [dir]",summary:"Copy the skill into Claude Code (~/.claude/skills/mlis) or another skills folder",async run(e){let t=e.positionals[0]??he(),r=await br(it.version,t);g(`# skill installed at ${r}`)}},Js=[yl,Cl,$l]});import{parseArgs as vl}from"node:util";function Xs(e){let t=Math.max(...e.map((r)=>r.name.length));return e.map((r)=>` ${r.name.padEnd(t)} ${r.summary}`).join(`
73
+ `)}function Gs(){return`mlis ${Ir}: design MLIS ViewPage (.ascx/.css/.js) through the site's API
67
74
 
68
75
  commands:
69
- ${ss(er)}
76
+ ${Xs(xr)}
70
77
 
71
- ${ns}
78
+ ${qs}
72
79
  run "mlis skill" for the agent guide
73
- `}function na(e){return`usage: ${e.usage}
80
+ `}function kl(e){return`usage: ${e.usage}
74
81
  ${e.summary}
75
- ${ns}
76
- `}function it(e){let t=[];for(let r of e.slice(0,sa)){if(r.startsWith("-"))break;t.push(r)}return t}function oa(e){let t=it(e);for(let r=t.length;r>0;r--){let n=t.slice(0,r).join(" "),s=er.find((o)=>o.name===n);if(s)return{command:s,rest:e.slice(r)}}return null}function ia(e){let t=it(e);for(let r=t.length;r>0;r--){let n=t.slice(0,r).join(" "),s=er.filter((o)=>o.name.startsWith(`${n} `));if(s.length>0)return{name:n,commands:s}}return null}async function aa(e){let t=oa(e);if(!t){let s=e[0];if(!s||s==="-h"||s==="--help"||s==="help")return h(rs()),j.Ok;if(s==="-v"||s==="--version")return h(`${Qt}
77
- `),j.Ok;let o=ia(e);if(o){let i=`mlis ${o.name} commands:
78
- ${ss(o.commands)}
82
+ ${qs}
83
+ `}function It(e){let t=[];for(let r of e.slice(0,bl)){if(r.startsWith("-"))break;t.push(r)}return t}function Sl(e){let t=It(e);for(let r=t.length;r>0;r--){let n=t.slice(0,r).join(" "),s=xr.find((o)=>o.name===n);if(s)return{command:s,rest:e.slice(r)}}return null}function Pl(e){let t=It(e);for(let r=t.length;r>0;r--){let n=t.slice(0,r).join(" "),s=xr.filter((o)=>o.name.startsWith(`${n} `));if(s.length>0)return{name:n,commands:s}}return null}async function Il(e){let t=Sl(e);if(!t){let s=e[0];if(!s||s==="-h"||s==="--help"||s==="help")return h(Gs()),V.Ok;if(s==="-v"||s==="--version")return h(`${Ir}
84
+ `),V.Ok;let o=Pl(e);if(o){let i=`mlis ${o.name} commands:
85
+ ${Xs(o.commands)}
79
86
 
80
87
  mlis <command> --help shows the options of one command
81
- `,c=it(e).join(" ")===o.name;if(c&&e.some((a)=>a==="-h"||a==="--help"))return h(i),j.Ok;return g(`${c?"missing subcommand":"unknown command"}: ${it(e).join(" ")}
82
- `),g(i),j.Usage}return g(`unknown command: ${e.slice(0,2).join(" ")}
83
- `),g(rs()),j.Usage}let{command:r,rest:n}=t;try{let s;try{s=ra({args:n,options:{...gr,...r.options},allowPositionals:!0,strict:!0})}catch(i){throw new u(`${i.message}
84
- usage: ${r.usage}`)}if(s.values.help===!0)return h(na(r)),j.Ok;let o=await hr(s.values,s.positionals);return await r.run(o)??j.Ok}catch(s){let o=tr(s);return g(`${o===j.Guard?"guard":"error"}: ${s.message}`),o}}async function os(e=process.argv.slice(2)){let t=await Zn(Qt).catch(()=>null);if(t)g(t);process.exitCode=await aa(e)}var Qt,er,ns=`global: --url <site> --workdir <dir> --json --no-header -h/--help -v/--version
88
+ `,c=It(e).join(" ")===o.name;if(c&&e.some((l)=>l==="-h"||l==="--help"))return h(i),V.Ok;return g(`${c?"missing subcommand":"unknown command"}: ${It(e).join(" ")}
89
+ `),g(i),V.Usage}return g(`unknown command: ${e.slice(0,2).join(" ")}
90
+ `),g(Gs()),V.Usage}let{command:r,rest:n}=t;try{let s;try{s=vl({args:n,options:{...Xr,...r.options},allowPositionals:!0,strict:!0})}catch(i){throw new m(`${i.message}
91
+ usage: ${r.usage}`)}if(s.values.help===!0)return h(kl(r)),V.Ok;let o=await Yr(s.values,s.positionals);return await r.run(o)??V.Ok}catch(s){let o=Rr(s);return g(`${o===V.Guard?"guard":"error"}: ${s.message}`),o}}async function Ys(e=process.argv.slice(2)){let t=await Ws(Ir).catch(()=>null);if(t)g(t);process.exitCode=await Il(e)}var Ir,xr,qs=`global: --url <site> --workdir <dir> --json --no-header -h/--help -v/--version
85
92
  paths: ViewPage paths such as LHUENB4/@HomePage/default.ascx, or local paths inside the workspace
86
- exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked (conflict, missing --confirm-delete)`,sa=3;var is=b(()=>{at();A();x();O();Ke();xr();Vr();en();nn();dn();mn();yn();In();Gn();ts();me();Yt();Qt=Ae.version,er=[...rn,...Sr,...Ur,...cn,...un,...xn,...Rr,...wn,...Hr,...zn,...Qr,...es]});import{enableCompileCache as la}from"node:module";la?.();await Promise.resolve().then(() => is());await os();
93
+ exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked (conflict, missing --confirm-delete)`,bl=3;var Zs=k(()=>{Rt();j();N();M();mt();mn();In();An();qn();Zn();ss();is();ds();ks();Us();zs();Ie();Sr();Ir=it.version,xr=[...Yn,...on,...On,...ns,...os,...vs,...un,...Pn,...cs,...Mn,...js,...Gn,...Js]});import{enableCompileCache as xl}from"node:module";xl?.();await Promise.resolve().then(() => Zs());await Ys();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fui-org/mlis-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI thiết kế trang ascx (ViewPage) cho website MLIS qua API có sẵn, kèm render cục bộ và skill cho Claude Code",
5
5
  "keywords": ["mlis", "ascx", "viewpage", "cli", "bun", "ai"],
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@ public class RenderResult
23
23
 
24
24
  public class RenderRequest : SimpleWorkerRequest
25
25
  {
26
- readonly string host, cookie, verb, contentType;
26
+ readonly string host, cookie, verb, contentType, page;
27
27
  readonly byte[] body;
28
28
  readonly MemoryStream output = new MemoryStream();
29
29
  readonly ManualResetEvent done = new ManualResetEvent(false);
@@ -33,7 +33,7 @@ public class RenderRequest : SimpleWorkerRequest
33
33
  public RenderRequest(string page, string query, string host, string cookie, string verb, string contentType, byte[] body)
34
34
  : base(page, query, new StringWriter())
35
35
  {
36
- this.host = host; this.cookie = cookie; this.verb = verb; this.contentType = contentType; this.body = body;
36
+ this.host = host; this.cookie = cookie; this.verb = verb; this.contentType = contentType; this.body = body; this.page = page;
37
37
  }
38
38
 
39
39
  public override void SendStatus(int statusCode, string statusDescription) { Status = statusCode; }
@@ -42,6 +42,11 @@ public class RenderRequest : SimpleWorkerRequest
42
42
  public override void SendResponseFromMemory(byte[] data, int length) { output.Write(data, 0, length); }
43
43
  public override void EndOfRequest() { done.Set(); }
44
44
  public override string GetServerName() { return host; }
45
+ // SimpleWorkerRequest maps only pages at the site root to files; pages in folders (Admin/default.aspx) need real paths
46
+ public override string GetUriPath() { return "/" + page; }
47
+ public override string GetFilePath() { return "/" + page; }
48
+ public override string GetFilePathTranslated() { return Path.Combine(HttpRuntime.AppDomainAppPath, page.Replace("/", Path.DirectorySeparatorChar.ToString())); }
49
+ public override string GetPathInfo() { return ""; }
45
50
  public override string GetHttpVerbName() { return verb; }
46
51
  public override string GetKnownRequestHeader(int index)
47
52
  {
@@ -14,12 +14,12 @@ description: Dùng cho mọi việc thiết kế giao diện website MLIS (lhu.e
14
14
 
15
15
  ```bash
16
16
  mlis config # xem url, workdir, token (chỉ hiện "(set)")
17
- mlis token set # dán giá trị cookie jwtAuth (prompt ẩn, hoặc pipe qua stdin)
17
+ mlis token set # dán MÃ GHÉP một lần lấy ở /admin → Thiết bị (prompt ẩn, hoặc pipe qua stdin)
18
18
  mlis config set url https://lhu.edu.vn
19
19
  mlis config set cdn-cat <CatID> # thư mục (CatID) chứa hình/tài liệu cho CKFinder
20
20
  ```
21
21
 
22
- Token là cookie `jwtAuth` của tài khoản có SystemRight ≥ 3. **Không bao giờ** in, dán vào chat, ghi vào file hay truyền token làm tham số dòng lệnh; không `cat` file config. Lỗi "server rejected the token" → nhờ người dùng chạy lại `mlis token set`.
22
+ Đăng nhập của mlis là **phiên gắn với máy**: người dùng vào /admin → **Thiết bị** → "Tạo mã ghép cho mlis" (mã dùng một lần, sống 10 phút) rồi tự chạy `mlis token set` và dán mã. Máy có khoá riêng (`device-key.pem` trong thư mục cấu hình) và tự gia hạn token 15 phút; token chép sang máy khác vô dụng. `mlis config` hiện "(device session)". **Không bao giờ** in, dán vào chat, ghi vào file hay truyền mã ghép/token làm tham số dòng lệnh; không `cat` file config hay file khoá. Lỗi "session ended" (đăng xuất ở /admin → Thiết bị, hết hạn, tài khoản bị khoá) → nhờ người dùng tạo mã ghép mới và chạy `mlis token set`. Dán cookie `jwtAuth` vẫn được nhưng token đó chỉ sống vài phút.
23
23
 
24
24
  ## Người dùng đưa URL của một trang: tìm template đang vẽ trang đó
25
25
 
@@ -68,12 +68,23 @@ Kết quả: `template` (thư mục ViewPage có default.ascx), `includes` (Head
68
68
  ```
69
69
  `mlis render` chạy chính mã nền tảng (Default.aspx, App_Code, control MLIS) từ `site-src` (mặc định `C:\APP\MLIS_2025`) trên `http://localhost:<render-port>`, thư mục ViewPage trỏ thẳng vào workspace (sửa file là thấy khi chạy lại), cơ sở dữ liệu thay bằng **dữ liệu mẫu**: thư mục mẫu cho mọi `CatID`/`Location` mà trang và các file nó `LoadPage` dùng (3 thư mục con mỗi Location, mỗi cái 2 con), tin mẫu có tiêu đề/tóm tắt/nội dung tiếng Việt, hình giữ chỗ ghi kích thước (`tin-<cat>-<k> · 800×500`). Vì ViewPage chỉ là khuôn, dữ liệu thật không cần có sẵn. File tĩnh nền tảng mà bản local thiếu được lấy từ site thật (chỉ GET, lưu đệm). Lần đầu mất ~1 phút để biên dịch site, các lần sau vài giây. Lỗi biên dịch/parse của .ascx được in ra kèm file và dòng.
70
70
  **Mở các ảnh PNG (Read) và tự review**: bố cục, tràn ngang, chữ tiếng Việt, khoảng cách, mobile; đối chiếu yêu cầu, sửa rồi chạy lại cho tới khi đạt. Cần Windows + .NET Framework 4 và `playwright` (Chromium).
71
- 4. **Đẩy trang thật**: `mlis check` → `mlis diff` → `mlis push` → mở trang thật kiểm lại. Trang mới cần người dùng gán PageStyle/PageRead trong /admin (CLI không làm được).
71
+ 4. **Đẩy trang thật**: `mlis check` → `mlis diff` → `mlis push` → mở trang thật kiểm lại. Trang mới chỉ hiển thị khi một thư mục có PageStyle/PageRead trỏ tới nó: gán bằng `mlis category pull <CatID>` → sửa `pageStyle`/`pageRead` → `mlis category push` (hoặc tạo thư mục mới bằng `mlis category new`).
72
72
 
73
73
  ## Nội dung động: đặt chữ trong bài viết, không cứng trong .ascx
74
74
 
75
75
  Chữ/hình hay đổi (giới thiệu, thông báo, số liệu, thẻ, slider, FAQ) nên lấy từ **bài viết** qua `<MLIS:NewsDetail NewsID="…" NewsFormat="{4}">` (một khối) hoặc `<MLIS:NewsList CatID="…">` (mỗi bài một khối), để người quản trị sửa trong /admin mà không đụng mã. Khi người dùng muốn **đổi nội dung** đang hiển thị: tìm CatID/NewsID trong .ascx, rồi `mlis news ls <CatID>` → `mlis news pull <NewsID>` → sửa `_news/<NewsID>.html` → `mlis news diff` → `mlis news push`. Đổi chữ thì sửa bài, không sửa .ascx. Chi tiết, định dạng file và quy tắc: `mlis skill ref news`.
76
76
 
77
+ ## Thông tin thư mục (category): sửa mọi trường của form /admin
78
+
79
+ ```bash
80
+ mlis category pull 21 # → _categories/21.html: mọi trường của form Thư mục + Tài liệu HTML bên dưới header
81
+ mlis category diff 21 # so với server
82
+ mlis category push 21 [--dry-run] # chỉ gửi trường đã đổi; exit 3 nếu thư mục bị sửa trên web sau lần pull
83
+ mlis category new 21 --name "Học bổng" # bản nháp thư mục con (lấy pageStyle/pageRead của cha); push mới tạo
84
+ ```
85
+
86
+ Trường trong header: `type` (0 Ẩn · 1 Trang chủ · 2 Thư mục · 3 Thông báo · 4 Sự kiện · 5 Giới thiệu · 6 Thư viện ảnh · 7 Liên kết · 8 Tài liệu HTML), `name`, `alias`, `description` (meta description, llms.txt, `CatShow {3}`), `picture`, `icon`, `link` (trống / `http…` / `{ten-mien}` — **đổi `{…}` là đổi trang chủ của tên miền**), `pageStyle`, `pageRead` (thư mục ViewPage cấp 2-3 có trên server, không `_`), `locations` (3 số 0-255), `sort`, `homePage` (0 = chính nó là trang chủ, 1 = cha…), `options` (`addNews, newsSort, rss, link, newWindow, indexAZ`; mỗi kiểu tự bỏ vài tuỳ chọn như form), `parentId` (đổi = chuyển thư mục). Phần dưới header là **Tài liệu HTML** (`CatShow {5}`). Server kiểm như form: tên không có `<>`, số 0-255, pageStyle/pageRead phải nằm trong danh sách trang. Sửa link/pageStyle/parent ảnh hưởng ngay tới trang thật và có thể cả tên miền con: nói rõ với người dùng trước khi push. Không có lệnh xoá thư mục.
87
+
77
88
  ## Quy tắc thiết kế (SEO, AI search, truy cập)
78
89
 
79
90
  - **Một `<h1>` mỗi trang.** Trang đọc tin: h1 là tiêu đề bài (`{1}` của NewsDetail sinh `<h1 class=NTitle>`) — không thêm h1 khác. Trang chủ/trang thư mục: tự đặt một h1 (tên trang/thư mục, có thể ẩn bằng CSS chuẩn `visually-hidden` nếu thiết kế không muốn hiện); tiêu đề khối dùng h2/h3 (`CatFormat="<h2>…</h2>"`).
@@ -102,7 +113,7 @@ mlis new LHUENB4/_default/Box.ascx # control con, chèn bằng <MLIS:LoadPag
102
113
  mlis push LHUENB4/MyPage # khi đã đạt: tạo thư mục + file trên server (bản sạch, không CodeFile)
103
114
  ```
104
115
 
105
- `mlis new` chỉ ghi vào workspace (nội dung mẫu sạch, không có `<%@ Control CodeFile=… %>` — trang ViewPage không có code-behind); server chưa có gì cho tới `mlis push`. `--push` tạo luôn trên server — không dùng khi chưa render/review, nhất là với trang route `_<Tên>` (có URL ngay khi file tồn tại). Trang thư mục mới chỉ hiển thị khi một thư mục (category) trong /admin có PageStyle = `LHUENB4/MyPage` — việc này người dùng làm.
116
+ `mlis new` chỉ ghi vào workspace (nội dung mẫu sạch, không có `<%@ Control CodeFile=… %>` — trang ViewPage không có code-behind); server chưa có gì cho tới `mlis push`. `--push` tạo luôn trên server — không dùng khi chưa render/review, nhất là với trang route `_<Tên>` (có URL ngay khi file tồn tại). Trang thư mục mới chỉ hiển thị khi một thư mục (category) có PageStyle = `LHUENB4/MyPage` (đẩy trang lên trước, rồi gán bằng `mlis category push`; PageStyle phải là thư mục đã có trên server).
106
117
 
107
118
  **Đặt tên thư mục** (chi tiết: `mlis skill ref viewpage-structure`):
108
119
  - Không có `_` ở đầu → là "trang", chọn được làm PageStyle/PageRead của thư mục (chỉ cấp `<site>/<trang>` hoặc `<site>/<trang>/<con>`, tối đa 50 ký tự).
@@ -153,7 +153,7 @@ Chèn một trang .ascx vào vị trí đặt control và/hoặc nạp mọi fil
153
153
  ### RSS — các kênh RSS 2.0
154
154
  `UseAlias` (true: dùng tên khác; false: tên thư mục).
155
155
 
156
- ## Cấu hình thư mục liên quan (trong /admin, không sửa bằng CLI)
156
+ ## Cấu hình thư mục liên quan (sửa bằng `mlis category pull/push` hoặc /admin)
157
157
 
158
158
  - **Liên kết**: để trống = link tới chính thư mục; `http://…` = link ngoài; `{codien.lhu.edu.vn}` = dùng tên miền đó cho thư mục.
159
159
  - **Trang chủ (Home)**: 0 = chính nó là trang chủ; 1 = thư mục cha; 2 = ông nội; …
@@ -8,11 +8,11 @@ Trang .ascx là **khuôn**. Chữ, hình, danh sách hay đổi (giới thiệu,
8
8
  |---|---|---|
9
9
  | Một khối nội dung cố định (đoạn giới thiệu, bảng học phí, hướng dẫn) | `<MLIS:NewsDetail NewsID="3387" NewsFormat="{4}" CatName="" runat="server" />` — `{4}` là nội dung HTML của bài; `{1}` tiêu đề, `{0}` hình, `{3}` tóm tắt | bài 3387: `mlis news pull 3387` |
10
10
  | Một nhóm khối giống nhau (thẻ, slider, lý do, đối tác, câu hỏi thường gặp) | `<MLIS:NewsList CatID="512" NewsNumber="6" NewsFormat="<div class='card'>{0}<h3>{1}</h3>{3}</div>" runat="server" />` — mỗi bài là một khối, thứ tự theo thứ tự sắp trong /admin | thêm/sửa/ẩn bài trong thư mục 512: `mlis news ls 512` |
11
- | Mô tả/HTML gắn với chính thư mục | `<MLIS:CatShow CatFormat="{3}">` (mô tả), `{5}` (Tài liệu HTML của thư mục) | /admin → Thư mục → Tài liệu HTML (chưa có lệnh mlis) |
11
+ | Mô tả/HTML gắn với chính thư mục | `<MLIS:CatShow CatFormat="{3}">` (mô tả), `{5}` (Tài liệu HTML của thư mục) | `mlis category pull <CatID>`: `description` và phần dưới header |
12
12
 
13
13
  Placeholder đầy đủ: `mlis skill ref controls` (NewsFormat chung: `{0}` hình · `{1}` tiêu đề · `{2}` ngày · `{3}` tóm tắt · `{4}` nội dung · `{5}` từ khoá · `{6}` nguồn). Thư mục dùng làm kho nội dung cho trang thường để **không** cho hiện trong menu/RSS (cấu hình trong /admin).
14
14
 
15
- Khi thiết kế trang mới có nội dung động: đề xuất với người dùng thư mục/bài sẽ chứa nội dung (CatID/NewsID phải có sẵn hoặc tạo trong /admin — CLI không tạo thư mục), viết khuôn bằng NewsList/NewsDetail, rồi đưa nội dung vào bài bằng `mlis news`. `mlis render` dùng dữ liệu mẫu nên khuôn xem được cả khi bài chưa có.
15
+ Khi thiết kế trang mới có nội dung động: đề xuất với người dùng thư mục/bài sẽ chứa nội dung (thư mục tạo bằng `mlis category new <ParentID>` → `mlis category push`, bài bằng `mlis news new`), viết khuôn bằng NewsList/NewsDetail, rồi đưa nội dung vào bài bằng `mlis news`. `mlis render` dùng dữ liệu mẫu nên khuôn xem được cả khi bài chưa có.
16
16
 
17
17
  ## Tìm bài đang cấp nội dung cho một trang
18
18
 
@@ -3,7 +3,7 @@
3
3
  ## Site chọn trang nào để hiển thị (Default.aspx.vb)
4
4
 
5
5
  1. Mỗi thư mục (category) trong /admin có trường **PageStyle** (trang của thư mục, ví dụ `LHUENB4/@HomePage`, `LHUENB4/DefaultPageCatalog`) và **PageRead** (trang dùng khi đọc tin trong thư mục, ví dụ `LHUENB4/@ReadPage`). Xem bằng `mlis category tree` / `mlis category pages <thư mục ViewPage>`.
6
- 2. Khi mở `/<CatID>/…` site nạp `ViewPage/<PageStyle>/default.ascx`; khi đọc tin `/<CatID>/<NewsID>/…` dùng trang đọc của thư mục (PageRead). Việc chọn PageStyle/PageRead nằm trong stored procedure, CLI không đổi được — người dùng gán trong /admin → Thư mục.
6
+ 2. Khi mở `/<CatID>/…` site nạp `ViewPage/<PageStyle>/default.ascx`; khi đọc tin `/<CatID>/<NewsID>/…` dùng trang đọc của thư mục (PageRead). PageStyle/PageRead của thư mục gán trong /admin → Thư mục hoặc bằng `mlis category pull/push`.
7
7
  3. Khi URL là một route có tên (`/more-list/…`, `/more-news/…`, `/more-related/…`, `/search/…`, …), site nạp `ViewPage/<site>/_<tên-route>/default.ascx`, trong đó `<site>` là phần đầu của PageStyle. Ví dụ `/more-list/…` → `ViewPage/LHUENB4/_more-List/default.ascx`.
8
8
  4. Phần `<head>` luôn nạp thêm `ViewPage/<site>/_assets/HeaderOfSite.ascx`.
9
9
  5. Ảnh chia sẻ (og:image) là `ViewPage/<site>/_assets/default_image.png`, favicon là `ViewPage/<site>/_assets/favicon.ico` — phải đúng tên này (thay bằng `mlis push` như file khác, cần server đã có API `UploadFile`).