@fui-org/mlis-cli 0.5.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 +6 -3
- package/dist/mlis.js +67 -58
- package/package.json +1 -1
- package/render/MlisRender.cs +7 -2
- package/skills/mlis/SKILL.md +28 -5
- package/skills/mlis/references/controls.md +1 -1
- package/skills/mlis/references/news.md +3 -3
- package/skills/mlis/references/viewpage-structure.md +1 -1
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
|
|
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.
|
|
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
|
|
|
@@ -64,13 +64,15 @@ mlis push LHUENB4/@HomePage
|
|
|
64
64
|
| `rm <file> --confirm-delete`, `mkdir <folder>`, `rmdir <folder> --confirm-delete` | xoá/tạo trên server |
|
|
65
65
|
| `history <file>`, `restore <file> [--at stamp]` | xem/khôi phục bản sao lưu |
|
|
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
|
+
| `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ề |
|
|
67
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` |
|
|
68
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 |
|
|
69
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/` |
|
|
70
72
|
| `cdn info\|list\|upload\|mkdir\|rename\|url\|download\|rm\|rmdir` | tài liệu/hình ngoài ViewPage qua CKFinder |
|
|
71
73
|
| `skill`, `skill ref [name]`, `skill install` | hướng dẫn cho AI |
|
|
72
74
|
|
|
73
|
-
Workspace: `<workdir>/…` giữ nguyên đường dẫn bên trong ViewPage, kèm `_mlis/state.json` (hash lần đồng bộ cuối) và `_history/` (bản sao lưu có dấu thời gian). Mỗi workspace dành cho một
|
|
75
|
+
Workspace: `<workdir>/…` giữ nguyên đường dẫn bên trong ViewPage, kèm `_mlis/state.json` (hash lần đồng bộ cuối) và `_history/` (bản sao lưu có dấu thời gian). Mỗi workspace dành cho một máy chủ MLIS: mọi tên miền của nó (lhu.edu.vn, tuyensinh.lhu.edu.vn…) dùng chung ViewPage nên dùng chung workspace; CLI tự nhận ra địa chỉ khác của cùng máy chủ. Đầu ra: bảng khi ở terminal, TSV khi pipe, `--json` cho máy; ghi chú/lỗi ra stderr. Exit code: 0 ok · 1 lỗi · 2 sai cú pháp · 3 bị chặn an toàn.
|
|
74
76
|
|
|
75
77
|
## Loại file và giới hạn của API server
|
|
76
78
|
|
|
@@ -79,6 +81,7 @@ Workspace: `<workdir>/…` giữ nguyên đường dẫn bên trong ViewPage, k
|
|
|
79
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>/…`).
|
|
80
82
|
- Không đổi tên file qua API; xoá thư mục chỉ khi rỗng.
|
|
81
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.
|
|
82
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).
|
|
83
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`.
|
|
84
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,84 +1,93 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`,{encoding:"utf-8",mode:384}),await rs(n,r),await Qn(r,384).catch(()=>{return})}function is(e){if(process.platform==="win32"&&Zt("C:\\APP"))return"C:\\APP\\mlis-workspaces";return Ae(e.HOME||e.USERPROFILE||Qt(),"mlis-workspaces")}function ot(e){let t;try{t=new URL(/^https?:\/\//i.test(e)?e:`https://${e}`)}catch{throw new d(`invalid url ${JSON.stringify(e)}; expected e.g. https://lhu.edu.vn`)}if(t.username||t.password||t.search||t.hash)throw new d("url must be the site address only, e.g. https://lhu.edu.vn");return`${t.origin}${t.pathname.replace(/\/+$/,"")}`}function _e(e,t){if(e==="url")return ot(t);if(e==="cdn-mode"&&t!=="folder"&&t!=="news")throw new d('cdn-mode must be "folder" or "news"');if(e==="cdn-cat"&&!/^\d+$/.test(t))throw new d("cdn-cat must be a numeric CatID");if(e==="token")return t.trim();if(e==="render-port"&&!/^\d{2,5}$/.test(t))throw new d("render-port must be a port number");return t}class He{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 He(await $e(t),t,e)}resolve(e){let t=this.flags[e];if(t)return{value:_e(e,t),source:"flag"};let r=this.env[Yt[e]];if(r)return{value:_e(e,r),source:"env"};let n=this.file[e];if(n)return{value:n,source:"config"};if(e==="url")return{value:os,source:"default"};if(e==="workdir")return{value:is(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"&&Zt("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 d("no token saved; run: mlis token set (paste the jwtAuth cookie value)");return e}envName(e){return Yt[e]}}var os="https://lhu.edu.vn",je,Yt;var Ve=b(()=>{E();je=["url","token","workdir","cdn-cat","cdn-mode","site-src","render-port"],Yt={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 sr}from"node:crypto";import{mkdir as er,readdir as as,readFile as it,rename as ls,rm as cs,stat as ds,writeFile as tr}from"node:fs/promises";import{dirname as rr,isAbsolute as us,join as at,relative as ms,resolve as nr,sep as fs}from"node:path";function K(e){let t=e.slice(e.lastIndexOf("/")+1),r=t.lastIndexOf(".");return r>0?t.slice(r).toLowerCase():""}function T(e){return ps.includes(K(e))}function hs(e){return gs.includes(K(e))}function V(e){return T(e)||hs(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 d(`invalid path segment ${JSON.stringify(r)} in ${JSON.stringify(e)}`);if(t[0]&&ar.has(t[0].toLowerCase()))throw new d(`${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 R(e){return e.slice(e.lastIndexOf("/")+1)}function ws(e,t){if(!t)return!0;let r=e.toLowerCase(),n=t.toLowerCase();return r===n||r.startsWith(`${n}/`)}function lt(e){let t=e.replace(/^/,"").replace(/\r\n/g,`
|
|
4
|
-
`);return`sha1:${
|
|
5
|
-
`,"utf-8"),await
|
|
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(
|
|
8
|
-
`),c)n(c);else r(s)},i=(c)=>{for(let
|
|
9
|
-
`)return o();if(
|
|
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
|
|
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
|
|
14
|
-
`}function
|
|
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
|
-
`:"")}},
|
|
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
|
-
`)}},
|
|
19
|
-
`,"utf-8"),new
|
|
20
|
-
`);return}let s=[],o=(i,c)=>{if(s.push(`${" ".repeat(c)}${
|
|
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
|
-
`:"")}},
|
|
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(
|
|
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
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(`
|
|
44
|
+
`);throw new E(`nothing was pushed; ${i.length} file(s) conflict with the server:
|
|
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>ke);if(c.length>0)throw new d(`larger than ${ke/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 m="";if(l.action==="save"){let u=await U(t,l.rel,l.server,"server");m=`server copy: ${t.relOf(u)??u}`,await Tr(e,l.rel,l.local,!0)}else if(l.action==="create"){if(l.parentMissing)await Je(e,P(l.rel));await Tr(e,l.rel,l.local,!1)}let f=(await r.list(P(l.rel))).find((u)=>u.Name.toLowerCase()===R(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:m})}}finally{await t.saveState()}h(N(a,e.render))}},Or=[Gs,qs,Xs,Qs]});function eo(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,ne(e,c).then((l)=>l?new Set(l.map((m)=>m.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(R(a).toLowerCase()))return!0;return[".ascx",".css",".js",".png",".jpg",".jpeg",".gif",".webp",".svg",".ico",".woff",".woff2",".ttf",".pdf"].includes(K(a))?!1:null}}var to,Mr;var Ar=b(()=>{j();M();Er();W();Le();to={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(K(l))),s=eo(e,k(e.values,"offline")),o=[],i=0;for(let l of n){let m=G(await t.readLocalBytes(l));o.push(...Lr(l,m,R(l).toLowerCase()==="default.ascx"));for(let f of Rr(m)){let u=await s(f.kind,f.path);if(u===!1)o.push({file:l,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(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}},Mr=[to]});import{randomBytes as ro}from"node:crypto";import{readFile as no,stat as so,writeFile as oo}from"node:fs/promises";import{basename as io}from"node:path";function mo(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 fo(e){let t={};for(let r of e.matchAll(/([A-Za-z][\w:-]*)="([^"]*)"/g))t[r[1]]=mo(r[2]);return t}function Ge(e,t){let r=[];for(let n of e.matchAll(new RegExp(`<${t}\\b([^>]*?)\\/?>`,"g")))r.push(fo(n[1]));return r}function se(e,t){let r=Ge(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 fe(e){if(e.includes("\\")||e.includes("?")||e.includes("#"))throw new d(`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 d(`invalid CDN path segment ${JSON.stringify(r)}`);return t}function z(e="/"){let t=fe(e);return t.length?`/${t.join("/")}/`:"/"}function Ee(e){let t=fe(e);if(t.length===0||e.endsWith("/"))throw new d("expected a file path, such as banner/logo.png");return`/${t.join("/")}`}function me(e){let t=fe(e);return t.pop(),z(t.join("/"))}function Ur(e){return fe(e).at(-1)??""}function ze(e,t,r){return`${e}${t}${r==="folder"?"/":""}`}function Hr(e){if(fe(e).length!==1||e.includes("/"))throw new d(`invalid file or folder name ${JSON.stringify(e)}`)}function Vr(e){return e.includes(".")?e.slice(e.lastIndexOf(".")+1).toLowerCase():""}function Wr(e){return uo.includes(Vr(e))?"Images":"Files"}function Br(e){if(e===void 0)return;let t=co.find((r)=>r.toLowerCase()===e.toLowerCase());if(!t)throw new d("--type is Images or Files");return t}class Rt{siteUrl;catId;mode;request;cookies=new Map;csrfToken=ro(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 d("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(lo)})}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(_r,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: ${_r} 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(ao,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 Ge(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(/\/?$/,"/")}${fe(Ee(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"),[...Ge(n,"Folder").map((o)=>({kind:"folder",name:o.name??"",path:ze(r,o.name??"","folder"),hasChildren:o.hasChildren==="true"})),...Ge(s,"File").map((o)=>({kind:"file",name:o.name??"",path:ze(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>jr)throw Error(`CDN listing exceeds ${jr} 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=Ur(t).toLowerCase();return(await this.list(e,me(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 d(`${r} ${t} does not exist in ${e}`);return n}async requireMissing(e,t){if(await this.findEntry(e,t))throw new d(`${t} already exists in ${e}`)}async checkExtension(e,t){let r=await this.resource(e),n=Vr(t);if(!n||!r.allowedExtensions.some((s)=>s.toLowerCase()===n))throw new d(`.${n||"?"} is not allowed in ${e}; allowed: ${r.allowedExtensions.join(",")}`)}async mkdir(e,t){let r=z(t);if(r==="/")throw new d("cannot create the root folder");let n=me(r);if(n!=="/")await this.requireEntry(e,n,"folder");await this.requireMissing(e,r);let s=await this.send("CreateFolder",e,n,{NewFolderName:Ur(r)},this.postBody());return se(s,"CreateFolder"),r}async upload(e,t,r="/",n=io(t)){let s=z(r);if(s!=="/")await this.requireEntry(e,s,"folder");Hr(n),await this.checkExtension(e,n);let o=await so(t);if(!o.isFile())throw new d(`${t} is not a regular file`);let{maxSize:i}=await this.resource(e);if(i>0&&o.size>i&&e!=="Images")throw new d(`${t} is ${o.size} bytes; ${e} allows at most ${i}`);let c=new FormData;c.append("upload",new Blob([new Uint8Array(await no(t))]),n),c.append("ckCsrfToken",this.csrfToken);let a=await this.send("FileUpload",e,s,{response_type:"txt"},c),[l,...m]=a.trim().split("|");if(!l||!a.includes("|"))throw Error(`CKFinder FileUpload failed: ${m.join("|")||a.slice(0,200)}`);let f=ze(s,l,"file");return await this.requireEntry(e,f,"file"),{path:f,warning:m.join("|")}}async rename(e,t,r){Hr(r);let n=await this.findEntry(e,t.replace(/\/$/,""));if(!n)throw new d(`${t} does not exist in ${e}`);let s=n.kind==="folder",o=ze(me(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:me(n.path),c,this.postBody());return se(a,i),o}async deleteFile(e,t){let r=Ee(t),n=await this.requireEntry(e,r,"file"),s=await this.send("DeleteFiles",e,me(r),void 0,this.postBody({"files[0][name]":n.name,"files[0][type]":e,"files[0][folder]":me(r)}));se(s,"DeleteFiles")}async deleteFolder(e,t,r=!1){let n=z(t);if(n==="/")throw new d("cannot delete the root folder");if(await this.requireEntry(e,n,"folder"),!r&&(await this.list(e,n)).length>0)throw new I(`${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 oo(r,o),o.length}}var ao="/JScript/CKFinder/core/connector/aspx/connector.aspx",_r="/Admin/CKFinder.aspx",jr=1e4,lo=120000,co,uo,po;var Kr=b(()=>{E();co=["Images","Files"],uo=["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 Jr}from"node:fs";import{basename as go}from"node:path";function X(e){let t=v(e.values,"cat")??e.settings.resolve("cdn-cat").value;if(!t)throw new d("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 d('--mode is "folder" or "news"');return new Rt(e.settings.url,e.settings.token(),t,r)}function te(e,t="Images"){return Br(v(e.values,"type"))??t}async function ho(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 I(`${t} is permanent; pass --confirm-delete once the user has approved this exact item`)}var q,wo,yo,Co,vo,$o,ko,bo,So,Po,Gr;var qr=b(()=>{j();E();M();Kr();q={cat:{type:"string"},mode:{type:"string"},type:{type:"string"}};wo={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=X(e),r=await t.init();h(J({catId:t.catId,mode:t.mode,resources:r}))}},yo={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=X(e),r=te(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 ho(t,r,i));h(N(o,e.render))}},Co={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(!Jr(t))throw new d(`file not found: ${t}`);let r=v(e.values,"name")??go(t),n=te(e,Wr(r)),s=X(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(J({type:n,path:o,url:await s.urlOf(n,o)}))}},vo={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=X(e);h(J({path:await t.mkdir(te(e),C(e,0,"folder"))}))}},$o={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 X(e).rename(te(e),C(e,0,"path"),C(e,1,"new name"));h(J({path:r}))}},ko={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=X(e),r=te(e),n=Ee(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 d(`${n} does not exist in ${r}`);let i=await t.urlOf(r,n);h(J({path:n,url:i,fullUrl:new URL(i,e.settings.url).href}))}},bo={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=v(e.values,"output");if(!t)throw new d("pass -o <local-file>");if(Jr(t)&&!k(e.values,"overwrite"))throw new d(`${t} exists; pass --overwrite`);let n=await X(e).download(te(e),C(e,0,"path"),t);g(`# saved ${n} bytes to ${t}`)}},So={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=Ee(C(e,0,"path"));zr(e,`deleting ${t}`),await X(e).deleteFile(te(e),t),g(`# deleted ${t}`)}},Po={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=z(C(e,0,"folder"));zr(e,`deleting folder ${t}`),await X(e).deleteFolder(te(e),t,k(e.values,"recursive")),g(`# deleted ${t}`)}},Gr=[wo,yo,Co,vo,$o,ko,bo,So,Po]});function No(e){return/^[\w-]+\.[\w-]+\.[\w-]+$/.test(e)}function Xr(e){if(!je.includes(e))throw new d(`unknown key ${JSON.stringify(e)}; keys: ${je.join(", ")}`);return e}var Ro,Lo,Eo,xo,Yr;var Zr=b(()=>{j();E();M();ee();Ve();Ro={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 d("the token is never an argument (it would stay in shell history); pipe it or paste it");let t=process.stdin.isTTY?await pr("jwtAuth token: "):await fr();if(t=t.trim().replace(/^jwtAuth=/,"").replace(/;.*$/,""),!t)throw new d("token is empty");if(!No(t))throw new d("that does not look like a JWT (three dot-separated parts)");let r=v(e.values,"url")?ot(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 $e();await Ue({...n,token:t,url:r}),g(`# token saved to ${le()}`)}};Lo={name:"config",usage:"mlis config",summary:"Show settings and where each value comes from (the token only as (set))",async run(e){let t=je.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()}`)}},Eo={name:"config set",usage:"mlis config set <url|workdir|cdn-cat|cdn-mode> <value>",summary:"Change one setting",async run(e){let t=Xr(C(e,0,"key"));if(t==="token")throw new d("use: mlis token set (keeps the token out of shell history)");let r=_e(t,C(e,1,"value"));await Ue({...await $e(),[t]:r}),g(`# ${t} = ${r}`)}},xo={name:"config unset",usage:"mlis config unset <key>",summary:"Remove one setting (unset token to forget it)",async run(e){let t=Xr(C(e,0,"key")),{[t]:r,...n}=await $e();await Ue(n),g(`# ${t} removed`)}},Yr=[Ro,Lo,Eo,xo]});function Lt(e){let t=K(e);if(t===".css")return"css";if(t===".js")return"js";return R(e).toLowerCase()==="default.ascx"?"page":"control"}function Qr(e,t=Lt(e)){let r=P(e),n=e.split("/")[0]??"",s=R(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
|
-
`);case"control":return['<%@ Register TagPrefix="MLIS" Namespace="MLIS" %>',`<%-- ${
|
|
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
|
|
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
|
|
40
|
-
|
|
41
|
-
`)}\r
|
|
42
|
-
${e.content.replace(/\r?\n/g,`\r
|
|
43
|
-
`)}\r
|
|
44
|
-
`}function Dt(e,t="article"){let r=e.replace(/^/,""),n=r.indexOf(Ie),s=r.indexOf(qe,n+Ie.length);if(n!==0||s<0)throw Error(`${t}: must start with a ${Ie} … ${qe} header`);let o=new Map;for(let a of r.slice(Ie.length,s).split(/\r?\n/)){let l=a.trim();if(!l||l.startsWith("#"))continue;let m=l.indexOf(":");if(m<=0)throw Error(`${t}: header line "${l}" is not "key: value"`);let f=l.slice(0,m).trim(),u=l.slice(m+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=(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+qe.length).replace(/^\r?\n/,"").replace(/\r?\n$/,"")};if(o.has("orderTime"))c.orderTime=o.get("orderTime");return c}function Ft(e,t,r){let n={NewsID:e.newsId,CatID:e.catId},s=(o,i,c)=>{if(!r||!ln(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||!ln(e.orderTime,r.orderTime)))n.OrderTime=e.orderTime;if(t)n.Version=t;return n}class Xe{ws;entries=null;constructor(e){this.ws=e}get path(){return Jo(this.ws.root,"_mlis","news.json")}async load(){if(this.entries)return this.entries;try{this.entries=JSON.parse(await Vo(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:Y(r),syncedAt:new Date().toISOString()}}async save(){if(!this.entries)return;await Ho(Ko(this.path),{recursive:!0});let e=`${this.path}.${process.pid}.tmp`;await Bo(e,`${JSON.stringify(this.entries,null,1)}
|
|
45
|
-
`,"utf-8"),await Wo(e,this.path)}}var Te="_news",Ie="<!--mlis-news",qe="-->",zo,ln=(e,t)=>(e??"").replace(/\r\n/g,`
|
|
46
|
-
`)===(t??"").replace(/\r\n/g,`
|
|
47
|
-
`);var cn=b(()=>{zo={4:"event",6:"gallery"}});import{rm as Go}from"node:fs/promises";import{isAbsolute as qo,relative as Xo,resolve as Yo,sep as Zo}from"node:path";function pe(e,t){let r=Number(e);if(e===void 0||!Number.isInteger(r)||r<0)throw new d(`${t} must be a number`);return r}function un(e,t){if(/^\d+$/.test(t))return De(Number(t));let r=e.workspace(),n=Xo(r.root,Yo(t)).split(Zo).join("/");if(n.startsWith("..")||qo(n)||!n.toLowerCase().startsWith(`${Te.toLowerCase()}/`))throw new d(`${t}: give a NewsID or a file in ${r.localPath(Te)}`);return n}function ge(e){return Tt(It(e),e)}function dn(e){let t=/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(String(e??""));return t?`${t[1]} ${t[2]}`:""}var Qo,ei,ti,ri,ni,si,mn;var fn=b(()=>{j();E();M();ee();Ne();cn();Qo={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:pe(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:pe(v(e.values,"skip"),"--skip"),limit:v(e.values,"limit")===void 0?20:pe(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:`${dn(r.CreateTime)} ${r.CreateUser??""}`.trim(),updated:r.UpdateUser?`${dn(r.UpdateTime)} ${r.UpdateUser}`:"",image:r.NewsImages??""})),e.render)),t.length===0)g("# no articles (or no right on this category)")}},ei={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(pe(C(e,0,"NewsID"),"NewsID"));h(e.json?`${JSON.stringify(t,null,2)}
|
|
48
|
-
`:ge(t))}},ti={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 d("missing NewsID");let t=e.workspace(),r=new Xe(t),n=[];for(let s of e.positionals){let o=pe(s,"NewsID"),i=await e.api().news(o),c=ge(i),a=De(o),l=await t.readLocal(a),m=await r.get(o),f=l===null?"new":"updated";if(l!==null&&Y(l)===Y(c))f="unchanged";else if(l!==null&&(!m||Y(l)!==m.hash)){if(!k(e.values,"force")){n.push({path:a,action:"kept-local",note:"local edits; push them, or pull --force"});continue}await U(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:`${xt(i.CatType)} · ${i.CatName??i.CategoryID}`})}await r.save(),h(N(n,e.render))}},ri={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=un(e,C(e,0,"NewsID or file")),n=await t.readLocal(r);if(n===null)throw new d(`${r} is not in the workspace; run: mlis news pull <NewsID>`);let s=Dt(n,r);if(s.newsId===0){h(n),g("# new article (newsId 0): all of it is added on push");return}let o=ge(await e.api().news(s.newsId)),i=Ke(o,n,`server/${s.newsId}`,r);if(h(i||""),!i)g("# no difference")}},ni={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 d("missing NewsID or file");let t=e.workspace(),r=new Xe(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=un(e,c),l=await t.readLocal(a);if(l===null)throw new d(`${a} is not in the workspace`);let m=Dt(l,a);if(m.newsId===0){if(s){i.push({path:a,action:"would-create",note:`category ${m.catId}`});continue}let L=await n.saveNews(Ft(m)),Ce=ge(L),B=De(L.NewsID);if(await t.writeLocal(B,Ce),B!==a)await Go(t.localPath(a));await r.set(L.NewsID,L.Version,Ce),i.push({path:B,action:"created",note:`NewsID ${L.NewsID}`});continue}if(a.toLowerCase()!==De(m.newsId).toLowerCase())throw new d(`${a}: header says newsId ${m.newsId}; an article file is named <NewsID>.html`);let f=await r.get(m.newsId),u=await n.news(m.newsId);if(u.CatType===6)throw new I(`${a}: gallery articles are edited in /admin only`);if(m.catId!==u.CategoryID)throw new d(`${a}: catId cannot change (${u.CategoryID} → ${m.catId}); move articles by dragging them in /admin → Bài viết`);let w=ge(u);if(!o&&!f)throw new I(`${a} was never pulled here; run: mlis news pull ${m.newsId} (or push --force to overwrite the server)`);if(!o&&f&&f.version!==u.Version)throw new I(`${a}: the article changed on the server after your pull. See: mlis news diff ${m.newsId}; then keep yours (push --force) or take the server's (pull --force)`);if(Y(l)===Y(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 U(t,a,w,"server");let p=await n.saveNews(Ft(m,o?void 0:u.Version,It(u))),S=ge(p);await t.writeLocal(a,S),await r.set(p.NewsID,p.Version,S);let x=Y(S)!==Y(l);i.push({path:a,action:"updated",note:x?"server normalised some fields; file rewritten":""})}}catch(c){if(c instanceof O&&c.status===409)throw new I("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))}}},si={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=pe(C(e,0,"CatID"),"CatID"),n=`${Te}/new-${r}-${$t()}.html`,s=Tt({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
|
-
`)}},
|
|
51
|
-
`)}function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`));
|
|
58
|
-
|
|
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(`
|
|
59
|
+
`)}
|
|
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(/ /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(`
|
|
59
68
|
`)}
|
|
60
|
-
`)}},
|
|
61
|
-
`,"utf-8"),n}async function
|
|
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(`
|
|
62
71
|
`)}
|
|
63
|
-
`);return}let s=n.replace(/\.md$/,"");if(!r.includes(s))throw new
|
|
64
|
-
`)}function
|
|
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
|
|
65
74
|
|
|
66
75
|
commands:
|
|
67
|
-
${
|
|
76
|
+
${Xs(xr)}
|
|
68
77
|
|
|
69
|
-
${
|
|
78
|
+
${qs}
|
|
70
79
|
run "mlis skill" for the agent guide
|
|
71
|
-
`}function
|
|
80
|
+
`}function kl(e){return`usage: ${e.usage}
|
|
72
81
|
${e.summary}
|
|
73
|
-
${
|
|
74
|
-
`}function
|
|
75
|
-
`),
|
|
76
|
-
${
|
|
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)}
|
|
77
86
|
|
|
78
87
|
mlis <command> --help shows the options of one command
|
|
79
|
-
`,c=
|
|
80
|
-
`),g(i),
|
|
81
|
-
`),g(
|
|
82
|
-
usage: ${r.usage}`)}if(s.values.help===!0)return h(
|
|
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
|
|
83
92
|
paths: ViewPage paths such as LHUENB4/@HomePage/default.ascx, or local paths inside the workspace
|
|
84
|
-
exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked (conflict, missing --confirm-delete)`,
|
|
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.
|
|
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",
|
package/render/MlisRender.cs
CHANGED
|
@@ -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
|
{
|
package/skills/mlis/SKILL.md
CHANGED
|
@@ -14,17 +14,29 @@ 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
|
|
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
|
-
|
|
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
|
+
|
|
24
|
+
## Người dùng đưa URL của một trang: tìm template đang vẽ trang đó
|
|
25
|
+
|
|
26
|
+
Sửa "trang `https://…`" nghĩa là sửa **template trong ViewPage** mà site dùng cho URL đó (hoặc bài viết, nếu chỉ đổi chữ). Không đoán, hỏi CLI:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
mlis page https://tuyensinh.lhu.edu.vn/ # tên miền → thư mục trang chủ → PageStyle
|
|
30
|
+
mlis page https://lhu.edu.vn/446/3387/ten-bai.html # /CatID/NewsID → PageRead của thư mục 446 (+ NewsID để sửa nội dung)
|
|
31
|
+
mlis page https://lhu.edu.vn/more-list/21/0/0/x.html --pull # tải template + các file nó LoadPage + HeaderOfSite về workspace
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Kết quả: `template` (thư mục ViewPage có default.ascx), `includes` (Header/Footer… nạp bằng LoadPage — chữ nằm ở header/footer thì sửa file đó), `head` (HeaderOfSite), `shared` (**bao nhiêu thư mục cùng dùng template này** — sửa template là đổi tất cả; nếu chỉ một thư mục cần khác, báo người dùng và đề xuất template riêng), `content` (NewsID → `mlis news pull`). Một máy chủ MLIS phục vụ nhiều tên miền (lhu.edu.vn, tuyensinh.lhu.edu.vn, sinhvientuonglai.lhu.edu.vn…) với **chung một ViewPage**: dùng chung một workspace, không tạo `--workdir` mới cho tên miền khác (CLI tự nhận ra cùng máy chủ; chỉ máy chủ MLIS khác mới cần workspace riêng).
|
|
23
35
|
|
|
24
36
|
## Quy trình chuẩn
|
|
25
37
|
|
|
26
38
|
1. **Xem**: `mlis tree [folder] [--files]`, `mlis ls <folder>`, `mlis cat <file>`.
|
|
27
|
-
2. **Tải về**: `mlis pull <folder|file>` → bản sao ở `<workdir>/<đường dẫn ViewPage>` (mặc định `C:\APP\mlis-workspaces\…`). Mỗi workspace dành cho một
|
|
39
|
+
2. **Tải về**: `mlis pull <folder|file>` → bản sao ở `<workdir>/<đường dẫn ViewPage>` (mặc định `C:\APP\mlis-workspaces\…`). Mỗi workspace dành cho một máy chủ MLIS (mọi tên miền của nó). Luôn pull trước khi sửa; file local lâu ngày có thể đã cũ — chạy `mlis status <folder> --remote` để kiểm.
|
|
28
40
|
3. **Sửa** file trong workspace (Edit/Write trực tiếp file local).
|
|
29
41
|
4. **Kiểm**: `mlis status <folder>` (new/changed/missing) và `mlis diff <file|folder>` (so với bản đang chạy trên server). Đọc kỹ diff trước khi đẩy.
|
|
30
42
|
5. **Đẩy**: `mlis push <file|folder>...` (`--dry-run` để xem trước). Trước khi ghi, bản server được lưu vào `_history/`.
|
|
@@ -56,12 +68,23 @@ Token là cookie `jwtAuth` của tài khoản có SystemRight ≥ 3. **Không ba
|
|
|
56
68
|
```
|
|
57
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.
|
|
58
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).
|
|
59
|
-
4. **Đẩy trang thật**: `mlis check` → `mlis diff` → `mlis push` → mở trang thật kiểm lại. Trang mới
|
|
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`).
|
|
60
72
|
|
|
61
73
|
## Nội dung động: đặt chữ trong bài viết, không cứng trong .ascx
|
|
62
74
|
|
|
63
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`.
|
|
64
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
|
+
|
|
65
88
|
## Quy tắc thiết kế (SEO, AI search, truy cập)
|
|
66
89
|
|
|
67
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>"`).
|
|
@@ -90,7 +113,7 @@ mlis new LHUENB4/_default/Box.ascx # control con, chèn bằng <MLIS:LoadPag
|
|
|
90
113
|
mlis push LHUENB4/MyPage # khi đã đạt: tạo thư mục + file trên server (bản sạch, không CodeFile)
|
|
91
114
|
```
|
|
92
115
|
|
|
93
|
-
`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)
|
|
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).
|
|
94
117
|
|
|
95
118
|
**Đặt tên thư mục** (chi tiết: `mlis skill ref viewpage-structure`):
|
|
96
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 (
|
|
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,17 +8,17 @@ 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) |
|
|
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 (
|
|
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
|
|
|
19
19
|
1. Đọc .ascx (`mlis pull <thư mục>`), tìm `CatID="…"`/`NewsID="…"` trong `<MLIS:NewsList|NewsDetail|NewsRelated|EventList>`. `CatID="-100"` = thư mục đang mở, `0` = trang chủ tên miền; `Location="n"` = các thư mục con có Location n (`mlis category ls <CatID>`).
|
|
20
20
|
2. `mlis news ls <CatID> [--search chữ]` → NewsID, tiêu đề, từ khoá, người tạo/sửa (giống danh sách trong /admin: tài khoản chỉ có quyền đóng góp thì chỉ thấy bài của mình).
|
|
21
|
-
3.
|
|
21
|
+
3. Có URL thì `mlis page <url>`: trang đọc tin `/<CatID>/<NewsID>/<tên>.html` cho biết NewsID và template.
|
|
22
22
|
|
|
23
23
|
## Lệnh
|
|
24
24
|
|
|
@@ -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).
|
|
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`).
|