@fui-org/mlis-cli 0.0.0-stage → 0.6.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 +92 -2
- package/dist/mlis.js +86 -0
- package/package.json +32 -4
- package/render/MlisRender.cs +298 -0
- package/render/SampleDataConnection.cs +320 -0
- package/skills/mlis/SKILL.md +147 -0
- package/skills/mlis/references/cdn-files.md +38 -0
- package/skills/mlis/references/controls.md +161 -0
- package/skills/mlis/references/design-patterns.md +94 -0
- package/skills/mlis/references/news.md +65 -0
- package/skills/mlis/references/rendered-html.md +91 -0
- package/skills/mlis/references/viewpage-structure.md +105 -0
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mlis — CLI thiết kế trang ascx cho website MLIS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`mlis` giúp designer (và Claude Code) làm việc với giao diện website MLIS qua các API có sẵn của site (`/api/viewpage/*`): xem cây thư mục `ViewPage`, tải mã về máy, sửa bằng editor, so sánh và đẩy lên an toàn (phát hiện xung đột, tự sao lưu), upload hình/tài liệu qua CKFinder. Kèm skill `mlis` cho Claude Code mô tả các control `<MLIS:…>`.
|
|
4
|
+
|
|
5
|
+
## Cài đặt
|
|
6
|
+
|
|
7
|
+
Cần Node ≥ 22 (hoặc Bun):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @fui-org/mlis-cli # hoặc: bun add -g @fui-org/mlis-cli
|
|
11
|
+
mlis --version
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`mlis render` (chạy trang trên máy để xem trước) cần thêm Windows có .NET Framework 4, mã nguồn site MLIS (`mlis config set site-src <thư mục>`) và Playwright (`npm i -g playwright && playwright install chromium`).
|
|
15
|
+
|
|
16
|
+
Phát triển từ mã nguồn (cần [Bun](https://bun.sh)):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
git clone https://github.com/fui-org/mlis-cli && cd mlis-cli
|
|
20
|
+
bun install
|
|
21
|
+
bun run build # dist/mlis.js
|
|
22
|
+
bun link # lệnh "mlis" dùng toàn máy
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Phát hành: mỗi lần push lên `main` có đổi `src/`, `skills/`, `render/` hoặc `package.json`, GitHub Actions tự tăng version (patch; ghi `#minor`/`#major` trong commit để tăng nhiều hơn), gắn tag và `npm publish` (npm Trusted Publishing, không cần token).
|
|
26
|
+
|
|
27
|
+
Muốn một file chạy độc lập (không cần Bun/Node): `bun run build:exe` → `dist/mlis.exe` (chép thư mục `skills/` cạnh file exe).
|
|
28
|
+
|
|
29
|
+
Lần chạy đầu, bản đã build tự cài skill vào `~/.claude/skills/mlis` (tắt bằng `MLIS_AUTO_SKILLS=off`; cài tay: `mlis skill install`).
|
|
30
|
+
|
|
31
|
+
## Cấu hình
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
mlis token set # dán giá trị cookie jwtAuth (lấy trong DevTools → Application → Cookies)
|
|
35
|
+
mlis config set url https://lhu.edu.vn
|
|
36
|
+
mlis config set cdn-cat 123 # CatID chứa hình/tài liệu cho CKFinder
|
|
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ó)
|
|
38
|
+
mlis config set render-port 5080 # cổng http://localhost của mlis render
|
|
39
|
+
mlis config # xem cấu hình (token chỉ hiện "(set)")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Cấu hình nằm ở `~/.config/mlis/config.json`. Biến môi trường `MLIS_URL`, `MLIS_TOKEN`, `MLIS_WORKDIR`, `MLIS_CDN_CAT`, `MLIS_CDN_MODE`, `MLIS_SITE_SRC`, `MLIS_RENDER_PORT` ghi đè file. Token cần tài khoản có SystemRight ≥ 3; không truyền token làm tham số dòng lệnh.
|
|
43
|
+
|
|
44
|
+
## Dùng hằng ngày
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
mlis tree LHUENB4 --files # cây thư mục + file
|
|
48
|
+
mlis pull LHUENB4/@HomePage # tải về C:\APP\mlis-workspaces\LHUENB4\@HomePage
|
|
49
|
+
# ... sửa file ...
|
|
50
|
+
mlis status LHUENB4 # file new / changed / missing
|
|
51
|
+
mlis diff LHUENB4/@HomePage # so với bản trên server
|
|
52
|
+
mlis push LHUENB4/@HomePage --dry-run
|
|
53
|
+
mlis push LHUENB4/@HomePage
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Lệnh | Việc làm |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `tree [folder] [--files]`, `ls [folder]`, `cat <file>` | xem trên server |
|
|
59
|
+
| `pull [folder\|file] [--force]` | tải về workspace; không ghi đè file local đang sửa (trừ `--force`, bản local được lưu `_history`) |
|
|
60
|
+
| `status [scope] [--remote]` | so với lần đồng bộ cuối; `--remote` kiểm cả thay đổi trên server |
|
|
61
|
+
| `diff [scope] [--stat]` | diff dòng local ↔ server (hình/font: chỉ báo khác hay giống) |
|
|
62
|
+
| `push <file\|folder>... [--dry-run] [--force]` | đẩy lên; dừng (exit 3) nếu server đã đổi từ lần pull cuối; bản server được lưu `_history` trước khi ghi |
|
|
63
|
+
| `new <file> [--kind page\|control\|css\|js] [--push]` | tạo file từ mẫu sạch trong workspace (render, xem rồi `push`); `--push` tạo luôn trên server |
|
|
64
|
+
| `rm <file> --confirm-delete`, `mkdir <folder>`, `rmdir <folder> --confirm-delete` | xoá/tạo trên server |
|
|
65
|
+
| `history <file>`, `restore <file> [--at stamp]` | xem/khôi phục bản sao lưu |
|
|
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ề |
|
|
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
|
+
| `news ls <CatID>`, `news show\|pull\|diff\|push <NewsID>`, `news new <CatID>` | đọc/sửa bài viết (nội dung động của trang) qua API của trang quản trị Bài viết; file `_news/<NewsID>.html`; push dừng (exit 3) nếu bài đã bị sửa trên web |
|
|
70
|
+
| `render shot <thư mục\|file.ascx> [--as home\|category\|read] [--device desktop\|mobile\|both] [--full] [-o dir]`, `render start <…>`, `render stop` | chạy trang trên máy (mã nền tảng từ `site-src` + dữ liệu mẫu, không ghi lên server) và chụp ảnh desktop/mobile vào `_mlis/shots/` |
|
|
71
|
+
| `cdn info\|list\|upload\|mkdir\|rename\|url\|download\|rm\|rmdir` | tài liệu/hình ngoài ViewPage qua CKFinder |
|
|
72
|
+
| `skill`, `skill ref [name]`, `skill install` | hướng dẫn cho AI |
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
## Loại file và giới hạn của API server
|
|
77
|
+
|
|
78
|
+
- `.ascx`, `.css`, `.js`: sửa như văn bản (`ReadFile`/`SaveFile`), có diff.
|
|
79
|
+
- Hình, font, media, pdf (`png jpg jpeg gif webp svg ico bmp avif woff woff2 ttf otf eot mp4 webm mp3 pdf`, ≤ 10 MB): đồng bộ dạng byte qua `UploadFile`/`DownloadFile` — cần `WebApiViewPageController.vb` bản mới. Server cũ vẫn dùng được với file code; khi đẩy hình CLI báo cần deploy.
|
|
80
|
+
- Đuôi khác (đặc biệt `.aspx`, `.ashx`, `.config`, `.dll`, `.html`) không được lưu vào ViewPage. Tài liệu bài viết (doc, xls, zip…) dùng `mlis cdn` (lưu ở `/Data/Folders/<CatID>/…`).
|
|
81
|
+
- Không đổi tên file qua API; xoá thư mục chỉ khi rỗng.
|
|
82
|
+
- `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.
|
|
83
|
+
- `mlis category` dùng API của trang quản trị thư mục (`/api/admin/GetAllFolder`, `GetFolderList`) — cần `WebApiController.vb` bản trả JSON (server cũ trả HTML, CLI báo cần deploy).
|
|
84
|
+
- Server không có lịch sử phiên bản; mọi sao lưu nằm ở `_history/` trên máy chạy `mlis`.
|
|
85
|
+
- Khách chưa đăng nhập có thể thấy trang cũ tới 60 giây (cache HTML của site).
|
|
86
|
+
|
|
87
|
+
## Phát triển
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bun test # unit + integration với server giả lập (test/fake-server.ts)
|
|
91
|
+
bun run typecheck
|
|
92
|
+
bun src/bin.ts --help
|
|
93
|
+
```
|
package/dist/mlis.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var b=(e,t,r)=>()=>{if(e)try{t=e(e=0)}catch(n){r=[n]}if(r)throw r[0];return t};var Ae;var at=b(()=>{Ae={name:"@fui-org/mlis-cli",version:"0.6.0",description:"CLI thiết kế trang ascx (ViewPage) cho website MLIS qua API có sẵn, kèm render cục bộ và skill cho Claude Code",keywords:["mlis","ascx","viewpage","cli","bun","ai"],license:"MIT",repository:{type:"git",url:"git+https://github.com/fui-org/mlis-cli.git"},publishConfig:{access:"public"},bin:{mlis:"dist/mlis.js"},files:["dist","skills","render","README.md"],type:"module",scripts:{mlis:"bun src/bin.ts",build:"bun build src/bin.ts --target node --minify --outdir dist --entry-naming mlis.js","build:exe":"bun build src/bin.ts --compile --minify --outfile dist/mlis.exe",prepack:"bun run build",test:"bun test",typecheck:"tsc --noEmit"},devDependencies:{"@types/bun":"latest",typescript:"^5.6.0"},engines:{node:">=22"}}});function tr(e){if(e instanceof u)return j.Usage;if(e instanceof E)return j.Guard;return j.Runtime}var j,u,E;var x=b(()=>{j={Ok:0,Runtime:1,Usage:2,Guard:3};u=class u extends Error{name="UsageError"};E=class E extends Error{name="GuardError"}});class ve{baseUrl;token;request;constructor(e,t,r=fetch){this.baseUrl=e;this.token=t;this.request=r}async call(e,t={},r="viewpage"){let n=`${this.baseUrl}/api/${r}/${e}`,s=r==="admin"?"WebApiController.vb":"WebApiViewPageController.vb",o;try{o=await this.request(n,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",cookie:`jwtAuth=${this.token}`},body:JSON.stringify(t),signal:AbortSignal.timeout(ls)})}catch(l){throw Error(`cannot reach ${n}: ${l.message}`)}let i=await o.text(),c;try{c=JSON.parse(i)}catch{c=void 0}if(o.status===401||c?.Message==="NotAuthenticated")throw new u("server rejected the token (NotAuthenticated, or SystemRight < 3); run: mlis token set");if(!c){if(o.status===404)throw new M(`${e}: this server has no api/${r}/${e}; deploy the updated ${s}`,404,"");throw new M(`${e}: HTTP ${o.status}, not JSON: ${i.replace(/\s+/g," ").slice(0,160)}`,o.status,"")}let a=c.Message??"";if(o.status===404&&a.startsWith("No HTTP resource"))throw new M(`${e}: this server has no api/${r}/${e}; deploy the updated ${s}`,404,a);if(!o.ok||a!=="")throw new M(`${e}: ${a||`HTTP ${o.status}`}`,o.status,a);return{...c,Message:a}}async tree(){return(await this.call("GetDirectoryTree")).Data??[]}async folderTree(){return this.rows(await this.call("GetAllFolder",{},"admin"),"GetAllFolder")}async folderList(e){return this.rows(await this.call("GetFolderList",{CatID:e,IncludePageRead:!0},"admin"),"GetFolderList")}async newsList(e){let t={CatID:e.catId,NewsStatus:e.trash??!1,NewsImport:!1,FromDate:e.from??"",ToDate:e.to??"",Skip:e.skip??0,NumRows:e.limit??20,FS:e.search??""};return this.rows(await this.call("GetNewsList",t,"admin"),"GetNewsList")}async news(e){return(await this.call("GetNews",{NewsID:e},"admin")).Data}async saveNews(e){return(await this.call("SaveNews",{...e},"admin")).Data}rows(e,t){if(typeof e.Data==="string")throw new M(`${t}: this server still returns HTML; deploy the updated WebApiController.vb (admin APIs return data)`,200,"");return Array.isArray(e.Data)?e.Data:[]}async list(e){return(await this.call("GetFileList",{path:e,includeBinary:!0})).Data??[]}async download(e){return Buffer.from((await this.call("DownloadFile",{path:e})).Data??"","base64")}async upload(e,t,r,n){let s=Buffer.from(r).toString("base64");return this.call("UploadFile",{path:e,fileName:t,data:s,overwrite:n})}async read(e){let t=await this.call("ReadFile",{path:e});return{Content:t.Content??"",FileName:t.FileName,Extension:t.Extension}}async save(e,t){return(await this.call("SaveFile",{path:e,content:t})).SavedAt}async createFile(e,t){return(await this.call("CreateFile",{path:e,fileName:t})).Path}async deleteFile(e){await this.call("DeleteFile",{path:e})}async createDirectory(e,t){return(await this.call("CreateDirectory",{path:e,dirName:t})).Path}async deleteDirectory(e){await this.call("DeleteDirectory",{path:e})}}async function ee(e,t,r){let n=Array(e.length),s=0;async function o(){while(s<e.length){let i=s++;n[i]=await r(e[i])}}return await Promise.all(Array.from({length:Math.min(t,e.length)},o)),n}var ls=60000,M;var te=b(()=>{x();M=class M extends Error{status;serverMessage;name="ApiError";constructor(e,t,r){super(e);this.status=t;this.serverMessage=r}get notFound(){return this.status===404}}});import{existsSync as nr}from"node:fs";import{chmod as cs,mkdir as ds,readFile as us,rename as ms,writeFile as fs}from"node:fs/promises";import{homedir as sr}from"node:os";import{dirname as ps,join as _e}from"node:path";function lt(e=process.env){if(e.MLIS_CONFIG_DIR)return e.MLIS_CONFIG_DIR;let t=e.XDG_CONFIG_HOME||_e(e.HOME||e.USERPROFILE||sr(),".config");return _e(t,"mlis")}function le(e=process.env){return _e(lt(e),"config.json")}async function ke(e=process.env){try{let t=JSON.parse(await us(le(e),"utf-8"));return t&&typeof t==="object"?t:{}}catch(t){if(t.code==="ENOENT")return{};throw Error(`cannot read ${le(e)}: ${t.message}`)}}async function He(e,t=process.env){let r=le(t);await ds(ps(r),{recursive:!0});let n=`${r}.${process.pid}.tmp`;await fs(n,`${JSON.stringify(e,null,2)}
|
|
3
|
+
`,{encoding:"utf-8",mode:384}),await ms(n,r),await cs(r,384).catch(()=>{return})}function hs(e){if(process.platform==="win32"&&nr("C:\\APP"))return"C:\\APP\\mlis-workspaces";return _e(e.HOME||e.USERPROFILE||sr(),"mlis-workspaces")}function ct(e){let t;try{t=new URL(/^https?:\/\//i.test(e)?e:`https://${e}`)}catch{throw new u(`invalid url ${JSON.stringify(e)}; expected e.g. https://lhu.edu.vn`)}if(t.username||t.password||t.search||t.hash)throw new u("url must be the site address only, e.g. https://lhu.edu.vn");return`${t.origin}${t.pathname.replace(/\/+$/,"")}`}function je(e,t){if(e==="url")return ct(t);if(e==="cdn-mode"&&t!=="folder"&&t!=="news")throw new u('cdn-mode must be "folder" or "news"');if(e==="cdn-cat"&&!/^\d+$/.test(t))throw new u("cdn-cat must be a numeric CatID");if(e==="token")return t.trim();if(e==="render-port"&&!/^\d{2,5}$/.test(t))throw new u("render-port must be a port number");return t}class Ve{file;env;flags;constructor(e,t=process.env,r={}){this.file=e;this.env=t;this.flags=r}static async load(e={},t=process.env){return new Ve(await ke(t),t,e)}resolve(e){let t=this.flags[e];if(t)return{value:je(e,t),source:"flag"};let r=this.env[rr[e]];if(r)return{value:je(e,r),source:"env"};let n=this.file[e];if(n)return{value:n,source:"config"};if(e==="url")return{value:gs,source:"default"};if(e==="workdir")return{value:hs(this.env),source:"default"};if(e==="cdn-mode")return{value:"folder",source:"default"};if(e==="render-port")return{value:"5080",source:"default"};if(e==="site-src"&&process.platform==="win32"&&nr("C:\\APP\\MLIS_2025\\Default.aspx"))return{value:"C:\\APP\\MLIS_2025",source:"default"};return{value:void 0,source:"unset"}}get url(){return this.resolve("url").value}get workdir(){return this.resolve("workdir").value}token(){let e=this.resolve("token").value;if(!e)throw new u("no token saved; run: mlis token set (paste the jwtAuth cookie value)");return e}envName(e){return rr[e]}}var gs="https://lhu.edu.vn",Ue,rr;var We=b(()=>{x();Ue=["url","token","workdir","cdn-cat","cdn-mode","site-src","render-port"],rr={url:"MLIS_URL",token:"MLIS_TOKEN",workdir:"MLIS_WORKDIR","cdn-cat":"MLIS_CDN_CAT","cdn-mode":"MLIS_CDN_MODE","site-src":"MLIS_SITE_SRC","render-port":"MLIS_RENDER_PORT"}});import{createHash as cr}from"node:crypto";import{mkdir as or,readdir as ws,readFile as dt,rename as ys,rm as Cs,stat as $s,writeFile as ir}from"node:fs/promises";import{dirname as ar,isAbsolute as vs,join as ut,relative as ks,resolve as lr,sep as bs}from"node:path";function J(e){let t=e.slice(e.lastIndexOf("/")+1),r=t.lastIndexOf(".");return r>0?t.slice(r).toLowerCase():""}function T(e){return Ss.includes(J(e))}function Ls(e){return Ps.includes(J(e))}function B(e){return T(e)||Ls(e)}function ce(e){let t=e.replace(/\\/g,"/").trim().replace(/^\/+/,"").replace(/^viewpage(\/|$)/i,"").split("/").filter(Boolean);for(let r of t)if(r==="."||r===".."||/[:*?"<>|]/.test(r)||[...r].some((n)=>n.charCodeAt(0)<32))throw new u(`invalid path segment ${JSON.stringify(r)} in ${JSON.stringify(e)}`);if(t[0]&&mr.has(t[0].toLowerCase()))throw new u(`${t[0]} is the CLI's own folder, not part of ViewPage`);return t.join("/")}function P(e){let t=e.lastIndexOf("/");return t<0?"":e.slice(0,t)}function L(e){return e.slice(e.lastIndexOf("/")+1)}function Ns(e,t){if(!t)return!0;let r=e.toLowerCase(),n=t.toLowerCase();return r===n||r.startsWith(`${n}/`)}function mt(e){let t=e.replace(/^/,"").replace(/\r\n/g,`
|
|
4
|
+
`);return`sha1:${cr("sha1").update(t,"utf-8").digest("hex")}`}function F(e,t){if(T(e))return mt(Buffer.from(t).toString("utf-8"));return`sha1:${cr("sha1").update(t).digest("hex")}`}function Rs(e,t){let r=(n)=>n.toLowerCase().replace(/\/+$/,"");return[e.url,...e.aliases??[]].some((n)=>r(n)===r(t))}class ft{url;sameServer;root;state=null;constructor(e,t,r){this.url=t;this.sameServer=r;this.root=lr(e)}localPath(e){return e?ut(this.root,...e.split("/")):this.root}relOf(e){let t=ks(this.root,e);if(t.startsWith("..")||vs(t))return null;return t.split(bs).join("/")}resolveArg(e,t=process.cwd()){if(e===void 0){let n=this.relOf(t);return n===null?"":ce(n)}let r=this.relOf(lr(t,e));return ce(r??e)}get statePath(){return ut(this.root,dr,"state.json")}get historyRoot(){return ut(this.root,ur)}async loadState(){if(this.state)return this.state;let e=null;try{e=JSON.parse(await dt(this.statePath,"utf-8"))}catch(r){if(r.code!=="ENOENT")throw r}if(!e)return this.state={version:1,url:this.url,files:{}},this.state;let t={version:1,url:e.url??this.url,aliases:e.aliases??[],files:e.files??{}};if(!Rs(t,this.url)){let r=Object.values(t.files).slice(0,200);if(!this.sameServer||!await this.sameServer(this.url,r))throw new u(`workspace ${this.root} holds the ViewPage of ${t.url}, and ${this.url} is not the same MLIS server (its files differ); a different MLIS installation needs its own --workdir`);return t.aliases=[...t.aliases??[],this.url],this.state=t,await this.saveState(),t}return this.state=t,t}async urls(){let e=await this.loadState();return[e.url,...e.aliases??[]]}async entry(e){return(await this.loadState()).files[e.toLowerCase()]}async entries(e=""){return Object.values((await this.loadState()).files).filter((t)=>Ns(t.path,e))}async record(e){(await this.loadState()).files[e.path.toLowerCase()]={...e,syncedAt:new Date().toISOString()}}async forget(e){delete(await this.loadState()).files[e.toLowerCase()]}async saveState(){if(!this.state)return;await or(ar(this.statePath),{recursive:!0});let e=`${this.statePath}.${process.pid}.tmp`;await ir(e,`${JSON.stringify(this.state,null,1)}
|
|
5
|
+
`,"utf-8"),await ys(e,this.statePath)}async readLocal(e){try{return(await dt(this.localPath(e),"utf-8")).replace(/^/,"")}catch(t){let r=t.code;if(r==="ENOENT"||r==="ENOTDIR"||r==="EISDIR")return null;throw t}}async readLocalBytes(e){try{return await dt(this.localPath(e))}catch(t){let r=t.code;if(r==="ENOENT"||r==="ENOTDIR"||r==="EISDIR")return null;throw t}}async writeLocal(e,t){let r=this.localPath(e);await or(ar(r),{recursive:!0}),await ir(r,t)}async removeLocal(e){await Cs(this.localPath(e),{force:!0})}async isLocalDir(e){try{return(await $s(this.localPath(e))).isDirectory()}catch{return!1}}async localFiles(e=""){let t=[],r=async(n)=>{let s;try{s=await ws(this.localPath(n),{withFileTypes:!0})}catch{return}for(let o of s){let i=n?`${n}/${o.name}`:o.name;if(!n&&mr.has(o.name.toLowerCase()))continue;if(o.isDirectory())await r(i);else if(o.isFile()&&B(i))t.push(i)}};if(e&&B(e)){if(await this.readLocalBytes(e)!==null)t.push(e)}else await r(e);return t.sort()}}var Ss,Ps,be=10485760,dr="_mlis",ur="_history",mr;var U=b(()=>{x();Ss=[".ascx",".css",".js"],Ps=[".png",".jpg",".jpeg",".gif",".webp",".svg",".ico",".bmp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp4",".webm",".mp3",".pdf"],mr=new Set([dr.toLowerCase(),ur.toLowerCase(),"_news"])});async function fr(e,t){if(t.length===0)return!0;let r=new Map;for(let n of t){let s=P(n.path);if(!s)continue;r.set(s,[...r.get(s)??[],n])}for(let[n,s]of[...r].slice(0,xs)){let o;try{o=await e.list(n)}catch{continue}for(let i of s){let c=o.find((a)=>a.Name.toLowerCase()===L(i.path).toLowerCase());if(c&&c.Size===i.size&&c.LastModified===i.lastModified)return!0}}return!1}var xs=4;var pr=b(()=>{U()});async function hr(e,t){let r=await Ve.load({url:v(e,"url"),workdir:v(e,"workdir")}),n=e.json===!0,s,o,i=()=>o??=new ve(r.url,r.token());return{settings:r,values:e,positionals:t,json:n,render:{json:n,header:e["no-header"]!==!0},workspace:()=>s??=new ft(r.workdir,r.url,(c,a)=>fr(i(),a)),api:i}}function v(e,t){let r=e[t];return typeof r==="string"?r:void 0}function k(e,t){return e[t]===!0}function C(e,t,r){let n=e.positionals[t];if(n===void 0||n==="")throw new u(`missing ${r}`);return n}var gr;var A=b(()=>{te();We();pr();U();x();gr={url:{type:"string"},workdir:{type:"string"},json:{type:"boolean"},"no-header":{type:"boolean"},help:{type:"boolean",short:"h"}}});function Cr(e){return e?.code==="EPIPE"}function h(e){if(wr)return wr(e);try{process.stdout.write(e)}catch(t){if(Cr(t))process.exit(0);throw t}}function g(e){let t=e.endsWith(`
|
|
6
|
+
`)?e:`${e}
|
|
7
|
+
`;if(yr)return yr(t);process.stderr.write(t)}async function $r(){let e=[];for await(let t of process.stdin)e.push(Buffer.from(t));return Buffer.concat(e).toString("utf-8")}async function vr(e){let t=process.stdin;if(!t.isTTY)throw new u("no terminal to prompt on; pipe the value on stdin instead");return process.stderr.write(e),t.setRawMode(!0),t.resume(),t.setEncoding("utf-8"),new Promise((r,n)=>{let s="",o=(c)=>{if(t.setRawMode(!1),t.pause(),t.off("data",i),process.stderr.write(`
|
|
8
|
+
`),c)n(c);else r(s)},i=(c)=>{for(let a of c){if(a==="\r"||a===`
|
|
9
|
+
`)return o();if(a==="\x03")return o(new u("cancelled"));if(a===""||a==="\b")s=s.slice(0,-1);else if(a>=" ")s+=a}};t.on("data",i)})}var wr=null,yr=null;var O=b(()=>{x();process.stdout.on("error",(e)=>{if(Cr(e))process.exit(0);throw e})});function Is(e){if(e===null||e===void 0)return"";return(typeof e==="object"?JSON.stringify(e):String(e)).replace(/\t/g,"\\t").replace(/\r/g,"\\r").replace(/\n/g,"\\n")}function kr(e,t){let r=[...new Set(e.flatMap((s)=>Object.keys(s)))],n=e.map((s)=>r.map((o)=>Is(s[o])));return t?[r,...n]:n}function Es(e,t=!0){if(e.length===0)return"";return`${kr(e,t).map((r)=>r.join("\t")).join(`
|
|
10
|
+
`)}
|
|
11
|
+
`}function Ts(e,t=!0){if(e.length===0)return"";let r=kr(e,t),n=r[0].map((o,i)=>Math.max(...r.map((c)=>c[i].length)));return`${r.map((o)=>o.map((i,c)=>c===o.length-1?i:i.padEnd(n[c])).join(" ")).join(`
|
|
12
|
+
`)}
|
|
13
|
+
`}function H(e){return`${JSON.stringify(e)}
|
|
14
|
+
`}function N(e,t){let{json:r,header:n,isTty:s=process.stdout.isTTY===!0}=t;if(r)return H(e);return s?Ts(e,n):Es(e,n)}function Se(e,t){for(let r of e){if(r.Path.toLowerCase()===t.toLowerCase())return r;if(t.toLowerCase().startsWith(`${r.Path.toLowerCase()}/`))return Se(r.Children,t)}return null}function Be(e,t){let r=t?Se(e,t):null;if(t&&!r)throw new u(`folder ${t} does not exist on the server`);let n=[],s=(o)=>{n.push(o.Path),o.Children.forEach(s)};return(r?[r]:e).forEach(s),n}async function br(e,t){return Be(await e.api().tree(),t)}var Fs,Ds,Os,Sr;var Ke=b(()=>{A();x();O();te();U();Fs={name:"tree",usage:"mlis tree [folder] [--files]",summary:"Show the ViewPage folder tree; --files also lists the .ascx/.css/.js files",options:{files:{type:"boolean"}},async run(e){let t=e.positionals[0]?e.workspace().resolveArg(e.positionals[0]):"",r=await e.api().tree(),n=t?Se(r,t):null;if(t&&!n)throw new u(`folder ${t} does not exist on the server`);let s=n?[n]:r,o=k(e.values,"files"),i=new Map;if(o){let l=Be(r,t),d=await ee(l,4,(f)=>e.api().list(f));l.forEach((f,m)=>i.set(f,d[m]))}if(e.json){let l=(d)=>({name:d.Name,path:d.Path,...o?{files:(i.get(d.Path)??[]).map((f)=>f.Name)}:{},children:d.Children.map(l)});h(H(s.map(l)));return}let c=[],a=(l,d)=>{let f=" ".repeat(d);c.push(`${f}${l.Name}/`);for(let m of i.get(l.Path)??[])c.push(`${f} ${m.Name}`);l.Children.forEach((m)=>a(m,d+1))};s.forEach((l)=>a(l,0)),h(c.length?`${c.join(`
|
|
15
|
+
`)}
|
|
16
|
+
`:"")}},Ds={name:"ls",usage:"mlis ls [folder]",summary:"List the .ascx/.css/.js files of one folder (size, last modified)",async run(e){let t=e.positionals[0]?e.workspace().resolveArg(e.positionals[0]):"",r=await e.api().list(t);h(N(r.map((n)=>({name:n.Name,type:n.Extension,size:n.Size,modified:n.LastModified,path:n.Path})),e.render))}},Os={name:"cat",usage:"mlis cat <file>",summary:"Print one file as it is on the server",async run(e){let t=e.workspace().resolveArg(C(e,0,"file"));if(!T(t))throw new u("cat prints .ascx, .css and .js; images/fonts are fetched with: mlis pull <file>");let{Content:r}=await e.api().read(t);h(e.json?H({path:t,content:r}):r.endsWith(`
|
|
17
|
+
`)?r:`${r}
|
|
18
|
+
`)}},Sr=[Fs,Ds,Os]});import{mkdir as Ms,readFile as As,writeFile as _s}from"node:fs/promises";import{dirname as Pr,join as js}from"node:path";function D(e,...t){for(let r of t){let n=Object.keys(e).find((s)=>s.toLowerCase()===r.toLowerCase());if(n!==void 0&&e[n]!==null&&e[n]!==void 0)return e[n]}return}function ne(e){if(e===void 0||e===null||e==="")return null;let t=Number(e);return Number.isFinite(t)?t:null}function de(e){return e===void 0||e===null?"":String(e).trim()}function Lr(e){return{id:ne(D(e,"CategoryID","CatID"))??0,parentId:ne(D(e,"ParentID")),name:de(D(e,"CatName")),alias:de(D(e,"CatAlias")),type:ne(D(e,"CatType")),pageStyle:de(D(e,"PageStyle")),pageRead:de(D(e,"PageRead")),locations:[1,2,3].map((t)=>ne(D(e,`Location${t}`))??0),link:de(D(e,"LinkURL")),sort:ne(D(e,"CatSort")),homePage:ne(D(e,"HomePage")),bits:ne(D(e,"BitOption")),description:de(D(e,"CatDescription")),raw:e}}function Pe(e){return e===null?"":Us[e]??String(e)}function ue(e,t){return e!==null&&(e&1<<t)!==0}function ht(e){return e.startsWith("{")?(e.slice(1).split("}")[0]??"").trim():""}function wt(e){return e.normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/đ/g,"d").replace(/Đ/g,"D").toLowerCase()}class yt{items;byId=new Map;children=new Map;roots=[];constructor(e){this.items=e;for(let r of e)this.byId.set(r.id,r);for(let r of e)if(r.parentId!==null&&this.byId.has(r.parentId)&&r.parentId!==r.id){let n=this.children.get(r.parentId)??[];n.push(r),this.children.set(r.parentId,n)}else this.roots.push(r);let t=(r,n)=>(r.sort??0)-(n.sort??0)||r.id-n.id;this.roots.sort(t);for(let r of this.children.values())r.sort(t)}childrenOf(e){return this.children.get(e)??[]}ancestors(e){let t=[],r=new Set,n=this.byId.get(e);while(n&&!r.has(n.id))t.unshift(n),r.add(n.id),n=n.parentId===null?void 0:this.byId.get(n.parentId);return t}pathText(e){return this.ancestors(e).map((t)=>t.name).join(" › ")}domainFor(e){let t=this.ancestors(e);for(let r=t.length-1;r>=0;r--){let n=ht(t[r].link);if(n)return n}return""}}function gt(e){return js(Pr(e.statePath),"categories.json")}async function Vs(e){let t=await e.folderTree(),r=[...new Set(t.map((o)=>Number(D(o,"ParentID")??0)))],n=await ee(r,4,(o)=>e.folderList(o)),s=new Map;for(let o of n.flat())s.set(Number(D(o,"CategoryID")),o);return t.map((o)=>({...o,...s.get(Number(D(o,"CategoryID")))??{}}))}async function Ct(e,t){let r=await Vs(t),n={url:e.url,fetchedAt:new Date().toISOString(),items:r};return await Ms(Pr(gt(e)),{recursive:!0}),await _s(gt(e),`${JSON.stringify(n)}
|
|
19
|
+
`,"utf-8"),new yt(r.map(Lr))}async function Je(e,t,r=!1){if(!r)try{let s=JSON.parse(await As(gt(e),"utf-8"));if((await e.urls()).map((i)=>i.toLowerCase()).includes(String(s.url).toLowerCase())&&Array.isArray(s.items))return{index:new yt(s.items.map(Lr)),fetchedAt:s.fetchedAt,stale:Date.now()-Date.parse(s.fetchedAt)>Hs,fetched:!1}}catch{}return{index:await Ct(e,t()),fetchedAt:new Date().toISOString(),stale:!1,fetched:!0}}var Us,Hs=86400000;var $t=b(()=>{te();Us={0:"Ẩn",1:"Trang chủ",2:"Thư mục",3:"Thông báo",4:"Sự kiện",5:"Giới thiệu",6:"Thư viện ảnh",7:"Liên kết",8:"Tài liệu HTML"}});async function Le(e){let t=await Je(e.workspace(),()=>e.api(),k(e.values,"refresh"));if(t.fetched)g(`# fetched ${t.index.items.length} categories from the server`);else if(t.stale)g(`# category list saved ${t.fetchedAt}; refresh with: mlis category sync`);return t.index}function kt(e,t,r=0){let n=C(e,r,"CatID"),s=Number(n),o=Number.isInteger(s)?t.byId.get(s):void 0;if(!o)throw new u(`no category ${n}; find one with: mlis category find <name>`);return o}function vt(e){return e.locations.some((t)=>t!==0)?e.locations.join(","):""}function Ws(e){let t=[`${e.id} ${e.name}`];if(e.type!==null)t.push(`(${Pe(e.type)})`);if(e.link)t.push(`link:${e.link}`);if(e.pageStyle)t.push(`page:${e.pageStyle}`);if(e.pageRead&&e.pageRead!==e.pageStyle)t.push(`read:${e.pageRead}`);if(vt(e))t.push(`loc:${vt(e)}`);return t.join(" ")}function Nr(e,t={}){return{id:e.id,name:e.name,type:Pe(e.type),loc:vt(e),sort:e.sort??"",page:e.pageStyle,read:e.pageRead,link:e.link,...t}}var Ne,Bs,Ks,Js,zs,Gs,qs,Rr;var xr=b(()=>{A();x();O();$t();U();Ne={refresh:{type:"boolean"}},Bs={name:"category sync",usage:"mlis category sync",summary:"Download the category tree with PageStyle, PageRead, Location1-3 (saved in the workspace)",async run(e){let t=await Ct(e.workspace(),e.api());g(`# saved ${t.items.length} categories`)}},Ks={name:"category tree",usage:"mlis category tree [CatID] [--depth N] [--refresh]",summary:"Category tree: id, name, type, page/read (ViewPage folder), loc (Location1,2,3)",options:{...Ne,depth:{type:"string"}},async run(e){let t=await Le(e),r=v(e.values,"depth")?Number(v(e.values,"depth")):1/0,n=e.positionals[0]?[kt(e,t)]:t.roots;if(e.json){let i=(c,a)=>({...Nr(c),children:a<r?t.childrenOf(c.id).map((l)=>i(l,a+1)):[]});h(`${JSON.stringify(n.map((c)=>i(c,0)))}
|
|
20
|
+
`);return}let s=[],o=(i,c)=>{if(s.push(`${" ".repeat(c)}${Ws(i)}`),c<r)for(let a of t.childrenOf(i.id))o(a,c+1)};n.forEach((i)=>o(i,0)),h(s.length?`${s.join(`
|
|
21
|
+
`)}
|
|
22
|
+
`:"")}},Js={name:"category ls",usage:"mlis category ls <CatID> [--refresh]",summary:"Child categories of one category with their Location1-3 (what NewsList Location=N picks)",options:Ne,async run(e){let t=await Le(e),r=kt(e,t);if(h(N(t.childrenOf(r.id).map((n)=>Nr(n)),e.render)),!e.json)g(`# ${t.pathText(r.id)}`)}},zs={name:"category show",usage:"mlis category show <CatID> [--refresh]",summary:"Every setting of one category, its path and the domain whose tree holds it",options:Ne,async run(e){let t=await Le(e),r=kt(e,t),n={path:t.pathText(r.id),domain:t.domainFor(r.id),typeName:Pe(r.type),link:ue(r.bits,3),newWindow:ue(r.bits,4),rss:ue(r.bits,2),indexAZ:ue(r.bits,5),addNews:ue(r.bits,0)};if(e.json){h(`${JSON.stringify({...r.raw,derived:n})}
|
|
23
|
+
`);return}let s=[...Object.entries(r.raw).map(([o,i])=>({key:o,value:i})),...Object.entries(n).map(([o,i])=>({key:`(${o})`,value:i}))];h(N(s,e.render))}},Gs={name:"category find",usage:"mlis category find <text> [--refresh]",summary:"Find categories by name or alias (accents ignored)",options:Ne,async run(e){let t=await Le(e),r=wt(C(e,0,"text")),n=t.items.filter((s)=>wt(`${s.name} ${s.alias}`).includes(r)||String(s.id)===r);h(N(n.map((s)=>({id:s.id,name:s.name,type:Pe(s.type),page:s.pageStyle,path:t.pathText(s.id)})),e.render))}},qs={name:"category pages",usage:"mlis category pages [ViewPage folder] [--refresh]",summary:"Which categories use which ViewPage folder as page (PageStyle) or read page (PageRead)",options:Ne,async run(e){let t=await Le(e),r=e.positionals[0]?ce(e.positionals[0]).toLowerCase():"",n=[];for(let s of t.items)for(let[o,i]of[["page",s.pageStyle],["read",s.pageRead]]){if(!i)continue;if(r&&i.toLowerCase()!==r)continue;n.push({folder:i,role:o,id:s.id,name:s.name,domain:t.domainFor(s.id)||ht(s.link)})}if(n.sort((s,o)=>String(s.folder).localeCompare(String(o.folder))||Number(s.id)-Number(o.id)),h(N(n,e.render)),!e.json&&r&&n.length===0)g(`# no category uses ${r}`)}},Rr=[Bs,Ks,Js,zs,Gs,qs]});function Er(e,t){let r=1;for(let n=0;n<t&&n<e.length;n++)if(e.charCodeAt(n)===10)r++;return r}function Zs(e){let t=/^\/?viewpage\/(.+)$/i.exec(e.trim().replace(/\\/g,"/"));if(!t)return null;return decodeURIComponent(t[1].split(/[?#]/)[0]).replace(/\/+$/,"")}function Tr(e){let t=[],r=(s,o,i)=>{let c=Zs(i);if(c)t.push({line:Er(e,s),kind:o,path:c,raw:i})};for(let s of e.matchAll(/\bCssFolder\s*=\s*"([^"]+)"/gi))r(s.index,"folder",s[1]);for(let s of e.matchAll(/\b(?:Src|href)\s*=\s*["']([^"']+)["']/gi))r(s.index,"file",s[1]);for(let s of e.matchAll(/\bsrc\s*=\s*["']([^"']+)["']/g))r(s.index,"file",s[1]);for(let s of e.matchAll(/url\(\s*["']?([^"')]+)["']?\s*\)/gi))r(s.index,"file",s[1]);let n=new Set;return t.filter((s)=>{let o=`${s.line}|${s.kind}|${s.path}`;if(n.has(o))return!1;return n.add(o),!0})}function Qs(e){let t=[];for(let r of e.matchAll(/<MLIS:(\w+)/gi)){let n="",s=r.index+r[0].length;for(;s<e.length;s++){let o=e[s];if(n){if(o===n)n=""}else if(o==='"'||o==="'")n=o;else if(o===">")break}t.push({index:r.index,name:r[1],attrs:e.slice(r.index+r[0].length,s)})}return t}function Ir(e){let t=e.replace(/\{\{|\}\}/g,"").replace(/\{\d+(,-?\d+)?(:[^}]*)?\}/g,"");return/[{}]/.test(t)}function Fr(e,t,r){let n=[],s=(i,c,a,l)=>n.push({file:e,line:Er(t,i),level:c,rule:a,message:l});if(e.toLowerCase().endsWith(".ascx")){let i=/<%@\s*Control\b[^%]*CodeFile\s*=/i.exec(t);if(i)s(i.index,"error","codefile","ViewPage controls have no code-behind: remove the <%@ Control … CodeFile=… %> directive");for(let a of Qs(t)){if(!/runat\s*=\s*["']server["']/i.test(a.attrs))s(a.index,"error","runat",`<MLIS:${a.name}> needs runat="server"`);let l=a.name.toLowerCase()==="loadpage"?/\bSrc\s*=\s*"([^"]*)"/i.exec(a.attrs)?.[1]?.trim():void 0;if(l!==void 0&&!/\.ascx$/i.test(l))s(a.index,"warn","loadpage-src",`LoadPage Src="${l}" is not an .ascx file, so it loads nothing; for css only, keep CssFolder and drop Src`)}for(let a of Xs){for(let l of t.matchAll(new RegExp(`\\b${a}\\s*=\\s*"([^"]*)"`,"g")))if(Ir(l[1]))s(l.index,"error","braces",`${a} has a "{" or "}" that is not a placeholder; write {{ or }} for a literal brace`);for(let l of t.matchAll(new RegExp(`<${a}>([\\s\\S]*?)</${a}>`,"g")))if(Ir(l[1]))s(l.index,"error","braces",`<${a}> has a "{" or "}" that is not a placeholder; write {{ or }} for a literal brace`)}for(let a of t.matchAll(/<img\b[^>]*>/gi)){let l=/\balt\s*=\s*["']([^"']*)["']/i.exec(a[0]);if(!l)s(a.index,"warn","img-alt",'image without alt: describe it, or alt="" if it is decoration');else if(Ys.test(l[1].trim()))s(a.index,"warn","img-alt",`placeholder alt "${l[1]}": describe the image`)}let c=/<title\b|rel\s*=\s*["']canonical["']|property\s*=\s*["']og:|"@type"\s*:\s*"(BreadcrumbList|NewsArticle)"/i.exec(t);if(c)s(c.index,"warn","seo-dup","the platform already writes title, canonical, og:* and Breadcrumb/NewsArticle JSON-LD: remove the duplicate");if(r){let a=[...t.matchAll(/<h1\b/gi)].length,d=(t.match(/<MLIS:NewsDetail\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi)??[]).some((m)=>/\bNewsFormat\s*=\s*"[^"]*\{1\}/i.test(m))?1:0,f=a+d;if(f===0)s(0,"warn","h1",'page has no <h1>: give it one (e.g. CatShow CatFormat="<h1>{1}</h1>")');else if(f>1)s(0,"warn","h1",`page has ${f} <h1> (NewsDetail {1} is already an h1): keep one, use h2/h3 for sections`)}}return n}var Xs,Ys;var Dr=b(()=>{Xs=["ListFormat","NewsFormat","OtherFormat","CatFormat","HyperFormat","EventFormat","EventItemFormat","AttrFormat","CatName","Author","HitCounter"],Ys=/^(card image cap|smaple image|sample image|image|img|ảnh|hình|photo|picture|banner|logo\d*)$/i});function ze(e){let t=e.replace(/^/,"").replace(/\r\n/g,`
|
|
24
|
+
`).split(`
|
|
25
|
+
`);if(t.at(-1)==="")t.pop();return t}function Or(e,t){let r=0;while(r<e.length&&r<t.length&&e[r]===t[r])r++;let n=e.length,s=t.length;while(n>r&&s>r&&e[n-1]===t[s-1])n--,s--;let o=e.slice(0,r).map((m)=>({kind:" ",text:m})),i=e.slice(n).map((m)=>({kind:" ",text:m})),c=e.slice(r,n),a=t.slice(r,s),l=c.length,d=a.length,f;if(l*d>25000000)f=[...c.map((m)=>({kind:"-",text:m})),...a.map((m)=>({kind:"+",text:m}))];else{let m=d+1,w=new Uint32Array((l+1)*m);for(let I=l-1;I>=0;I--)for(let R=d-1;R>=0;R--)w[I*m+R]=c[I]===a[R]?w[(I+1)*m+R+1]+1:Math.max(w[(I+1)*m+R],w[I*m+R+1]);f=[];let p=0,S=0;while(p<l&&S<d)if(c[p]===a[S])f.push({kind:" ",text:c[p]}),p++,S++;else if(w[(p+1)*m+S]>=w[p*m+S+1])f.push({kind:"-",text:c[p++]});else f.push({kind:"+",text:a[S++]});while(p<l)f.push({kind:"-",text:c[p++]});while(S<d)f.push({kind:"+",text:a[S++]})}return[...o,...f,...i]}function Ge(e,t,r,n){let s=Or(ze(e),ze(t));if(!s.some((c)=>c.kind!==" "))return"";let o=[`--- ${r}`,`+++ ${n}`],i=0;while(i<s.length){while(i<s.length&&s[i].kind===" ")i++;if(i>=s.length)break;let c=Math.max(0,i-3),a=i;while(a<s.length){if(s[a].kind!==" "){a++;continue}let p=a;while(p<s.length&&s[p].kind===" ")p++;if(p>=s.length||p-a>6){a=Math.min(s.length,a+3);break}a=p}let l=1,d=1;for(let p=0;p<c;p++){if(s[p].kind!=="+")l++;if(s[p].kind!=="-")d++}let f=s.slice(c,a),m=f.filter((p)=>p.kind!=="+").length,w=f.filter((p)=>p.kind!=="-").length;o.push(`@@ -${l},${m} +${d},${w} @@`);for(let p of f)o.push(`${p.kind}${p.text}`);i=a}return`${o.join(`
|
|
26
|
+
`)}
|
|
27
|
+
`}function Mr(e,t){let r=Or(ze(e),ze(t));return{added:r.filter((n)=>n.kind==="+").length,removed:r.filter((n)=>n.kind==="-").length}}import{mkdir as eo,readdir as to,writeFile as ro}from"node:fs/promises";import{dirname as no,join as bt}from"node:path";function St(e=new Date){let t=(r)=>String(r).padStart(2,"0");return`${e.getFullYear()}${t(e.getMonth()+1)}${t(e.getDate())}_${t(e.getHours())}${t(e.getMinutes())}${t(e.getSeconds())}`}function Pt(e){let t=L(e),r=J(e)?t.slice(t.length-J(e).length):"";return{base:t.slice(0,t.length-r.length),ext:r}}function oo(e,t,r,n=St()){let{base:s,ext:o}=Pt(t),i=P(t);return bt(e.historyRoot,...i?i.split("/"):[],`${s}_${n}${r?`_${r}`:""}${o}`)}async function V(e,t,r,n){let s=oo(e,t,n);await eo(no(s),{recursive:!0});let o=s.slice(s.length-Pt(t).ext.length),i=s.slice(0,s.length-o.length);for(let c=1;c<=so;c++){let a=c===1?s:`${i}_${c}${o}`;try{return await ro(a,r,{flag:"wx"}),a}catch(l){if(l.code!=="EEXIST")throw l}}throw Error(`too many history copies of ${t} in one second`)}async function Lt(e,t){let{base:r,ext:n}=Pt(t),s=bt(e.historyRoot,...P(t)?P(t).split("/"):[]),o;try{o=await to(s)}catch{return[]}let i=(l)=>l.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),c=new RegExp(`^${i(r)}_(\\d{8}_\\d{6})(?:_(.+))?${i(n)}$`,"i"),a=[];for(let l of o){let d=c.exec(l);if(d)a.push({stamp:d[1],label:d[2]??"",file:bt(s,l)})}return a.sort((l,d)=>d.stamp.localeCompare(l.stamp)||d.file.localeCompare(l.file))}var so=50;var Re=b(()=>{U()});async function q(e,t){try{return await e.api().list(t)}catch(r){if(r instanceof M&&r.notFound)return null;throw r}}async function xe(e,t){if(T(t))return Buffer.from((await e.api().read(t)).Content,"utf-8");return e.api().download(t)}function G(e){return Buffer.from(e).toString("utf-8").replace(/^/,"")}async function Ar(e,t,r,n){let s=e.api();if(T(t)){if(!n)await s.createFile(P(t),L(t));await s.save(t,G(r));return}if(r.length>be)throw new u(`${t} is ${r.length} bytes; the server accepts at most ${be}`);await s.upload(P(t),L(t),r,!0)}async function _r(e,t){let r=new Map;if(t&&B(t)){let o=(await q(e,P(t)))?.find((i)=>i.Name.toLowerCase()===L(t).toLowerCase());if(o)r.set(o.Path.toLowerCase(),o);return r}let n=await br(e,t),s=await ee(n,Nt,(o)=>e.api().list(o));for(let o of s.flat())if(B(o.Path))r.set(o.Path.toLowerCase(),o);return r}function Rt(e,t=0){return e.workspace().resolveArg(e.positionals[t])}function jr(e,t,r){h(N(e.values.all===!0?r:t,e.render))}async function It(e,t){let r=[],n=await e.localFiles(t),s=new Set;for(let o of n){s.add(o.toLowerCase());let i=await e.entry(o),c=await e.readLocalBytes(o);r.push({path:i?.path??o,local:!i?"new":F(o,c)===i.hash?"unchanged":"changed"})}for(let o of await e.entries(t))if(!s.has(o.path.toLowerCase()))r.push({path:o.path,local:"missing"});return r.sort((o,i)=>o.path.localeCompare(i.path))}async function lo(e,t,r){let n=e.workspace(),s=await n.readLocalBytes(t);if(s===null)throw new u(`${t} does not exist in the workspace (${n.localPath(t)})`);let o=F(t,s),i=await n.entry(t),c=await q(e,P(t)),a=c?.some((w)=>w.Name.toLowerCase()===L(t).toLowerCase())??!1,l={rel:t,local:s,localHash:o,parentMissing:c===null};if(!a)return{...l,action:"create",conflict:i&&!r?"deleted on the server since your last pull":void 0};let d=await xe(e,t),f=F(t,d);if(f===o)return{...l,action:"unchanged",server:d};let m;if(!r&&(!i||i.hash!==f))m=i?"changed on the server since your last pull":"exists on the server but was never pulled";return{...l,action:"save",server:d,conflict:m}}async function qe(e,t){let r=t.split("/").filter(Boolean),n=new Set(Be(await e.api().tree(),"").map((s)=>s.toLowerCase()));for(let s=0;s<r.length;s++){let o=r.slice(0,s+1).join("/");if(n.has(o.toLowerCase()))continue;if(s===0)throw new u(`site folder ${r[0]} does not exist on the server; the API cannot create top-level folders`);try{await e.api().createDirectory(r.slice(0,s).join("/"),r[s])}catch(i){if(!(i instanceof M&&i.status===409))throw i}}}async function co(e){let t=e.workspace();if(e.positionals.length===0)throw new u("name the files or folders to push (see: mlis status), e.g. mlis push LHUENB4/@HomePage");let r=new Set;for(let n of e.positionals){let s=t.resolveArg(n);if(B(s))r.add(s);else if(!s||await t.isLocalDir(s)){for(let o of await It(t,s))if(o.local==="new"||o.local==="changed")r.add(o.path)}else throw new u(`${n}: this file type cannot be stored in ViewPage (see: mlis skill ref viewpage-structure)`)}return[...r]}var Nt=4,xt,io,ao,uo,Ur;var me=b(()=>{A();x();O();te();Re();U();Ke();xt={name:"pull",usage:"mlis pull [folder|file] [--force] [--all]",summary:"Download files (code, images, fonts...) into the workspace; local edits are kept unless --force",options:{force:{type:"boolean"},all:{type:"boolean"}},async run(e){let t=e.workspace(),r=Rt(e),n=k(e.values,"force"),s=await _r(e,r),o=await t.entries(r);if(s.size===0&&o.length>0&&!n)throw new E(`the server returned no files under "${r||"ViewPage"}" but ${o.length} were pulled before; check the url/token, or pass --force to mirror the empty answer`);let i=[...s.values()],c=await ee(i,Nt,(m)=>xe(e,m.Path)),a=[];try{for(let[m,w]of i.entries()){let p=w.Path,S=c[m],I=F(p,S),R=await t.readLocalBytes(p),$e=await t.entry(p),K;if(R===null)await t.writeLocal(p,S),K="new";else if(F(p,R)===I)K="unchanged";else if(!$e||F(p,R)!==$e.hash){if(!n){a.push({path:p,action:"kept-local",note:"local edits; push them, or pull --force"});continue}await V(t,p,R,"local"),await t.writeLocal(p,S),K="overwritten"}else await t.writeLocal(p,S),K="updated";await t.record({path:p,hash:I,size:w.Size,lastModified:w.LastModified}),a.push({path:p,action:K,note:K==="overwritten"?"local copy saved in _history":""})}for(let m of o){if(s.has(m.path.toLowerCase()))continue;let w=await t.readLocalBytes(m.path);if(await t.forget(m.path),w===null)continue;if(F(m.path,w)===m.hash)await V(t,m.path,w,"deleted-on-server"),await t.removeLocal(m.path),a.push({path:m.path,action:"removed",note:"deleted on server; copy in _history"});else a.push({path:m.path,action:"orphan",note:"deleted on server; local edits kept"})}}finally{await t.saveState()}let l=a.filter((m)=>m.action!=="unchanged");jr(e,l,a);let d=new Map;for(let m of a)d.set(String(m.action),(d.get(String(m.action))??0)+1);let f=[...d].map(([m,w])=>`${w} ${m}`).join(", ")||"nothing";g(`# ${i.length} server files → ${f} in ${t.localPath(r)}`)}};io={name:"status",usage:"mlis status [folder|file] [--remote] [--all]",summary:"Local files vs the last pull/push: new, changed, missing; --remote also checks the server",options:{remote:{type:"boolean"},all:{type:"boolean"}},async run(e){let t=e.workspace(),r=Rt(e),s=(await It(t,r)).map((i)=>({path:i.path,local:i.local}));if(k(e.values,"remote")){let i=await _r(e,r),c=new Map(s.map((a)=>[String(a.path).toLowerCase(),a]));for(let a of s){let l=await t.entry(String(a.path)),d=i.get(String(a.path).toLowerCase());a.remote=!d?l?"deleted":"absent":!l?"exists":d.Size===l.size&&d.LastModified===l.lastModified?"unchanged":"changed"}for(let a of i.values())if(!c.has(a.Path.toLowerCase()))s.push({path:a.Path,local:"absent",remote:"not pulled"})}let o=s.filter((i)=>i.local!=="unchanged"||i.remote&&i.remote!=="unchanged");if(jr(e,o,s),!e.json&&s.length===0)g("# nothing here yet; run: mlis pull <folder>");else if(!e.json&&o.length===0)g("# everything matches the last sync")}},ao={name:"diff",usage:"mlis diff [folder|file] [--stat]",summary:"Line diff of local files against the server (images/fonts: only whether they differ)",options:{stat:{type:"boolean"}},async run(e){let t=e.workspace(),r=Rt(e),n=B(r)?[r]:(await It(t,r)).filter((l)=>l.local==="new"||l.local==="changed").map((l)=>l.path);if(n.length===0){g("# no local changes");return}let s=new Map,o=async(l)=>{let d=P(l);if(!s.has(d))s.set(d,q(e,d));return(await s.get(d))?.some((f)=>f.Name.toLowerCase()===L(l).toLowerCase())??!1},i=k(e.values,"stat"),c=[],a="";for(let l of n){let d=await t.readLocalBytes(l);if(d===null)throw new u(`${l} is not in the workspace; run: mlis pull ${l}`);let f=await o(l),m=f?await xe(e,l):Buffer.alloc(0);if(!T(l)){let w=!f?"absent":F(l,m)===F(l,d)?"same":"differs";if(i)c.push({path:l,added:"",removed:"",server:w});else if(w!=="same")a+=`Binary file ${l}: server ${w==="absent"?"has no copy":"copy differs"}
|
|
28
|
+
`;continue}if(i)c.push({path:l,...Mr(G(m),G(d)),server:f?"exists":"absent"});else a+=Ge(G(m),G(d),`server/${l}`,`local/${l}`)}if(i)h(N(c,e.render));else h(a);if(!i&&a==="")g("# local files match the server")}};uo={name:"push",usage:"mlis push <file|folder>... [--dry-run] [--force]",summary:"Upload local changes; stops (exit 3) if the server copy changed since the last pull",options:{force:{type:"boolean"},"dry-run":{type:"boolean"}},async run(e){let t=e.workspace(),r=e.api(),n=k(e.values,"force"),s=await co(e);if(s.length===0){g("# nothing to push: no new or changed files");return}let o=await ee(s,Nt,(l)=>lo(e,l,n)),i=o.filter((l)=>l.conflict);if(i.length>0){let l=i.map((d)=>` ${d.rel}: ${d.conflict}`).join(`
|
|
29
|
+
`);throw new E(`nothing was pushed; ${i.length} file(s) conflict with the server:
|
|
30
|
+
${l}
|
|
31
|
+
compare with: mlis diff <file>; then either keep yours: mlis push <file> --force (server copy goes to _history)
|
|
32
|
+
or take the server's: mlis pull <file> --force (your copy goes to _history) and redo the edit`)}let c=o.filter((l)=>!T(l.rel)&&l.local.length>be);if(c.length>0)throw new u(`larger than ${be/1024/1024} MB, nothing was pushed: ${c.map((l)=>l.rel).join(", ")}`);if(k(e.values,"dry-run")){h(N(o.map((l)=>({path:l.rel,action:l.action})),e.render)),g("# dry run: nothing was written");return}let a=[];try{for(let l of o){let d="";if(l.action==="save"){let m=await V(t,l.rel,l.server,"server");d=`server copy: ${t.relOf(m)??m}`,await Ar(e,l.rel,l.local,!0)}else if(l.action==="create"){if(l.parentMissing)await qe(e,P(l.rel));await Ar(e,l.rel,l.local,!1)}let f=(await r.list(P(l.rel))).find((m)=>m.Name.toLowerCase()===L(l.rel).toLowerCase());await t.record({path:f?.Path??l.rel,hash:l.localHash,size:f?.Size??0,lastModified:f?.LastModified??""}),a.push({path:l.rel,action:l.action==="save"?"saved":l.action==="create"?"created":"unchanged",note:d})}}finally{await t.saveState()}h(N(a,e.render))}},Ur=[xt,io,ao,uo]});function mo(e,t){let r=e.workspace(),n=null,s=new Map,o=async()=>{if(!n){n=new Set;let c=(a)=>{n.add(a.Path.toLowerCase()),a.Children.forEach(c)};(await e.api().tree()).forEach(c)}return n},i=(c)=>{let a=c.toLowerCase();if(!s.has(a))s.set(a,q(e,c).then((l)=>l?new Set(l.map((d)=>d.Name.toLowerCase())):null));return s.get(a)};return async(c,a)=>{if(c==="folder"?await r.isLocalDir(a):await r.readLocalBytes(a)!==null)return!0;if(t)return null;if(c==="folder")return(await o()).has(a.toLowerCase());let l=await i(P(a));if(l===null)return!1;if(l.has(L(a).toLowerCase()))return!0;return[".ascx",".css",".js",".png",".jpg",".jpeg",".gif",".webp",".svg",".ico",".woff",".woff2",".ttf",".pdf"].includes(J(a))?!1:null}}var fo,Hr;var Vr=b(()=>{A();O();Dr();U();me();fo={name:"check",usage:"mlis check [folder|file] [--offline]",summary:"Check local ViewPage files: broken ViewPage references, MLIS tag/format errors, alt text, h1, duplicate SEO tags",options:{offline:{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(e.positionals[0]),n=(await t.localFiles(r)).filter((l)=>[".ascx",".css"].includes(J(l))),s=mo(e,k(e.values,"offline")),o=[],i=0;for(let l of n){let d=G(await t.readLocalBytes(l));o.push(...Fr(l,d,L(l).toLowerCase()==="default.ascx"));for(let f of Tr(d)){let m=await s(f.kind,f.path);if(m===!1)o.push({file:l,line:f.line,level:"error",rule:"ref-missing",message:`${f.kind} ViewPage/${f.path} does not exist`});else if(m===null)i++}}let c=o;h(N(c.map((l)=>({...l})),e.render));let a=c.filter((l)=>l.level==="error").length;if(!e.json)g(`# ${n.length} files checked: ${a} errors, ${c.length-a} warnings${i?`, ${i} references not verified (offline or type the API does not list)`:""}`);return a>0?1:0}},Hr=[fo]});import{randomBytes as po}from"node:crypto";import{readFile as go,stat as ho,writeFile as wo}from"node:fs/promises";import{basename as yo}from"node:path";function bo(e){return e.replace(/&(#x[0-9a-f]+|#[0-9]+|amp|lt|gt|quot|apos);/gi,(t,r)=>{if(r.startsWith("#x"))return String.fromCodePoint(Number.parseInt(r.slice(2),16));if(r.startsWith("#"))return String.fromCodePoint(Number.parseInt(r.slice(1),10));return{amp:"&",lt:"<",gt:">",quot:'"',apos:"'"}[r]??t})}function So(e){let t={};for(let r of e.matchAll(/([A-Za-z][\w:-]*)="([^"]*)"/g))t[r[1]]=bo(r[2]);return t}function Ye(e,t){let r=[];for(let n of e.matchAll(new RegExp(`<${t}\\b([^>]*?)\\/?>`,"g")))r.push(So(n[1]));return r}function se(e,t){let r=Ye(e,"Error")[0];if(!r)throw Error(`CKFinder ${t} returned an invalid response: ${e.slice(0,200)}`);if(r.number!=="0"){let n=r.text||r.message||Po[r.number??""]||"";throw Error(`CKFinder ${t} failed (error ${r.number??"?"})${n?`: ${n}`:""}`)}}function pe(e){if(e.includes("\\")||e.includes("?")||e.includes("#"))throw new u(`invalid CDN path ${JSON.stringify(e)}`);let t=e.replace(/^\/+|\/+$/g,"").split("/").filter(Boolean);for(let r of t)if(r==="."||r===".."||r.startsWith(".")||/[:*"<>|]/.test(r)||[...r].some((n)=>n.charCodeAt(0)<32))throw new u(`invalid CDN path segment ${JSON.stringify(r)}`);return t}function z(e="/"){let t=pe(e);return t.length?`/${t.join("/")}/`:"/"}function Ie(e){let t=pe(e);if(t.length===0||e.endsWith("/"))throw new u("expected a file path, such as banner/logo.png");return`/${t.join("/")}`}function fe(e){let t=pe(e);return t.pop(),z(t.join("/"))}function Kr(e){return pe(e).at(-1)??""}function Xe(e,t,r){return`${e}${t}${r==="folder"?"/":""}`}function Jr(e){if(pe(e).length!==1||e.includes("/"))throw new u(`invalid file or folder name ${JSON.stringify(e)}`)}function zr(e){return e.includes(".")?e.slice(e.lastIndexOf(".")+1).toLowerCase():""}function Gr(e){return ko.includes(zr(e))?"Images":"Files"}function qr(e){if(e===void 0)return;let t=vo.find((r)=>r.toLowerCase()===e.toLowerCase());if(!t)throw new u("--type is Images or Files");return t}class Et{siteUrl;catId;mode;request;cookies=new Map;csrfToken=po(20).toString("hex");resources;seeded=!1;constructor(e,t,r,n,s=fetch){this.siteUrl=e;this.catId=r;this.mode=n;this.request=s;if(!/^\d+$/.test(r))throw new u("CatID must be numeric; set it with: mlis config set cdn-cat <CatID>");this.cookies.set("jwtAuth",t),this.cookies.set("ckCsrfToken",this.csrfToken)}cookieHeader(){return[...this.cookies].map(([e,t])=>`${e}=${t}`).join("; ")}async fetchWithCookies(e,t={}){let r=new Headers(t.headers);r.set("cookie",this.cookieHeader());let n;try{n=await this.request(e,{...t,headers:r,redirect:"manual",signal:AbortSignal.timeout($o)})}catch(s){throw Error(`cannot reach ${String(e)}: ${s.message}`)}for(let s of n.headers.getSetCookie()){let o=s.split(";",1)[0],i=o.indexOf("=");if(i>0)this.cookies.set(o.slice(0,i).trim(),o.slice(i+1))}return n}async seed(){if(this.seeded)return;let e=new URL(Wr,this.siteUrl);e.searchParams.set("CatID",this.catId),e.searchParams.set("mode",this.mode);let t=await this.fetchWithCookies(e);if(await t.text(),!this.cookies.has("ASP.NET_SessionId"))throw Error(`seeding the CKFinder session failed: ${Wr} returned HTTP ${t.status} without a session cookie`);this.seeded=!0}async send(e,t,r,n,s){await this.seed();let o=new URL(Co,this.siteUrl);if(o.searchParams.set("command",e),t){let c=await this.resource(t);o.searchParams.set("type",c.type),o.searchParams.set("currentFolder",z(r)),o.searchParams.set("langCode","vi"),o.searchParams.set("hash",c.hash)}for(let[c,a]of Object.entries(n??{}))o.searchParams.set(c,a);let i=await this.fetchWithCookies(o,{method:s?"POST":"GET",body:s});if(!i.ok)throw Error(`CKFinder ${e} returned HTTP ${i.status}`);return i.text()}postBody(e={}){return new URLSearchParams({...e,ckCsrfToken:this.csrfToken,CKFinderCommand:"true"})}async init(){if(!this.resources){let e=await this.send("Init",null);se(e,"Init"),this.resources=new Map;for(let t of Ye(e,"ResourceType")){if(!t.name||!t.hash||!t.url)continue;this.resources.set(t.name,{type:t.name,url:t.url,maxSize:Number(t.maxSize??0),allowedExtensions:(t.allowedExtensions??"").split(",").filter(Boolean),acl:Number(t.acl??0),hash:t.hash})}if(this.resources.size===0)throw Error("CKFinder Init exposed no resource types")}return[...this.resources.values()].map(({hash:e,...t})=>t)}async resource(e){if(!this.resources)await this.init();let t=this.resources.get(e);if(!t)throw Error(`CKFinder does not expose resource type ${e}`);return t}async urlOf(e,t){return`${(await this.resource(e)).url.replace(/\/?$/,"/")}${pe(Ie(t)).map(encodeURIComponent).join("/")}`}async list(e,t="/"){let r=z(t),n=await this.send("GetFolders",e,r);se(n,"GetFolders");let s=await this.send("GetFiles",e,r);return se(s,"GetFiles"),[...Ye(n,"Folder").map((o)=>({kind:"folder",name:o.name??"",path:Xe(r,o.name??"","folder"),hasChildren:o.hasChildren==="true"})),...Ye(s,"File").map((o)=>({kind:"file",name:o.name??"",path:Xe(r,o.name??"","file"),sizeKb:Number(o.size??0),date:o.date??""}))]}async listRecursive(e,t="/",r=20){let n=[{path:z(t),depth:0}],s=[];while(n.length){let o=n.shift(),i=await this.list(e,o.path);if(s.push(...i),s.length>Br)throw Error(`CDN listing exceeds ${Br} entries`);if(o.depth<r){for(let c of i)if(c.kind==="folder")n.push({path:c.path,depth:o.depth+1})}}return s}async findEntry(e,t){let r=Kr(t).toLowerCase();return(await this.list(e,fe(t))).find((n)=>n.name.toLowerCase()===r)}async requireEntry(e,t,r){let n=await this.findEntry(e,t);if(!n||n.kind!==r)throw new u(`${r} ${t} does not exist in ${e}`);return n}async requireMissing(e,t){if(await this.findEntry(e,t))throw new u(`${t} already exists in ${e}`)}async checkExtension(e,t){let r=await this.resource(e),n=zr(t);if(!n||!r.allowedExtensions.some((s)=>s.toLowerCase()===n))throw new u(`.${n||"?"} is not allowed in ${e}; allowed: ${r.allowedExtensions.join(",")}`)}async mkdir(e,t){let r=z(t);if(r==="/")throw new u("cannot create the root folder");let n=fe(r);if(n!=="/")await this.requireEntry(e,n,"folder");await this.requireMissing(e,r);let s=await this.send("CreateFolder",e,n,{NewFolderName:Kr(r)},this.postBody());return se(s,"CreateFolder"),r}async upload(e,t,r="/",n=yo(t)){let s=z(r);if(s!=="/")await this.requireEntry(e,s,"folder");Jr(n),await this.checkExtension(e,n);let o=await ho(t);if(!o.isFile())throw new u(`${t} is not a regular file`);let{maxSize:i}=await this.resource(e);if(i>0&&o.size>i&&e!=="Images")throw new u(`${t} is ${o.size} bytes; ${e} allows at most ${i}`);let c=new FormData;c.append("upload",new Blob([new Uint8Array(await go(t))]),n),c.append("ckCsrfToken",this.csrfToken);let a=await this.send("FileUpload",e,s,{response_type:"txt"},c),[l,...d]=a.trim().split("|");if(!l||!a.includes("|"))throw Error(`CKFinder FileUpload failed: ${d.join("|")||a.slice(0,200)}`);let f=Xe(s,l,"file");return await this.requireEntry(e,f,"file"),{path:f,warning:d.join("|")}}async rename(e,t,r){Jr(r);let n=await this.findEntry(e,t.replace(/\/$/,""));if(!n)throw new u(`${t} does not exist in ${e}`);let s=n.kind==="folder",o=Xe(fe(n.path),r,s?"folder":"file");if(n.path===o)return o;if(await this.requireMissing(e,o),!s)await this.checkExtension(e,r);let i=s?"RenameFolder":"RenameFile",c=s?{NewFolderName:r}:{fileName:n.name,newFileName:r},a=await this.send(i,e,s?n.path:fe(n.path),c,this.postBody());return se(a,i),o}async deleteFile(e,t){let r=Ie(t),n=await this.requireEntry(e,r,"file"),s=await this.send("DeleteFiles",e,fe(r),void 0,this.postBody({"files[0][name]":n.name,"files[0][type]":e,"files[0][folder]":fe(r)}));se(s,"DeleteFiles")}async deleteFolder(e,t,r=!1){let n=z(t);if(n==="/")throw new u("cannot delete the root folder");if(await this.requireEntry(e,n,"folder"),!r&&(await this.list(e,n)).length>0)throw new E(`${n} is not empty; pass --recursive with --confirm-delete to remove its contents`);let s=await this.send("DeleteFolder",e,n,void 0,this.postBody());se(s,"DeleteFolder")}async download(e,t,r){let n=new URL(await this.urlOf(e,t),this.siteUrl),s=await this.fetchWithCookies(n);if(!s.ok)throw Error(`download returned HTTP ${s.status} for ${n.pathname}`);let o=new Uint8Array(await s.arrayBuffer());return await wo(r,o),o.length}}var Co="/JScript/CKFinder/core/connector/aspx/connector.aspx",Wr="/Admin/CKFinder.aspx",Br=1e4,$o=120000,vo,ko,Po;var Xr=b(()=>{x();vo=["Images","Files"],ko=["gif","jpeg","jpg","png"];Po={"500":"CKFinder is disabled for this request: the token was rejected or the session was not seeded","102":"invalid name","103":"access denied by CKFinder ACL","104":"insufficient permissions","105":"invalid file extension","109":"invalid request","115":"a file or folder with that name already exists","116":"folder not found","117":"file not found"}});import{existsSync as Yr}from"node:fs";import{basename as Lo}from"node:path";function Y(e){let t=v(e.values,"cat")??e.settings.resolve("cdn-cat").value;if(!t)throw new u("which CatID holds the files? pass --cat <CatID> or run: mlis config set cdn-cat <CatID>");let r=v(e.values,"mode")??e.settings.resolve("cdn-mode").value??"folder";if(r!=="folder"&&r!=="news")throw new u('--mode is "folder" or "news"');return new Et(e.settings.url,e.settings.token(),t,r)}function re(e,t="Images"){return qr(v(e.values,"type"))??t}async function No(e,t,r){return{kind:r.kind,path:r.path,...r.kind==="file"?{sizeKb:r.sizeKb??0,date:r.date??"",url:await e.urlOf(t,r.path)}:{hasChildren:r.hasChildren??!1}}}function Zr(e,t){if(!k(e.values,"confirm-delete"))throw new E(`${t} is permanent; pass --confirm-delete once the user has approved this exact item`)}var X,Ro,xo,Io,Eo,To,Fo,Do,Oo,Mo,Qr;var en=b(()=>{A();x();O();Xr();X={cat:{type:"string"},mode:{type:"string"},type:{type:"string"}};Ro={name:"cdn info",usage:"mlis cdn info [--cat <CatID>] [--mode folder|news]",summary:"Show the CKFinder resource types (Images, Files): base URL, size limit, allowed extensions",options:X,async run(e){let t=Y(e),r=await t.init();h(H({catId:t.catId,mode:t.mode,resources:r}))}},xo={name:"cdn list",usage:"mlis cdn list [folder] [--type Images|Files] [--recursive] [--cat <CatID>]",summary:"List folders and files of one resource type (default Images)",options:{...X,recursive:{type:"boolean"}},async run(e){let t=Y(e),r=re(e),n=z(e.positionals[0]),s=k(e.values,"recursive")?await t.listRecursive(r,n):await t.list(r,n),o=[];for(let i of s)o.push(await No(t,r,i));h(N(o,e.render))}},Io={name:"cdn upload",usage:"mlis cdn upload <local-file> [--to folder] [--name new-name] [--type Images|Files] [--cat <CatID>]",summary:"Upload an image or document and print the URL to use in .ascx/.css",options:{...X,to:{type:"string"},name:{type:"string"}},async run(e){let t=C(e,0,"local file");if(!Yr(t))throw new u(`file not found: ${t}`);let r=v(e.values,"name")??Lo(t),n=re(e,Gr(r)),s=Y(e),{path:o,warning:i}=await s.upload(n,t,v(e.values,"to")??"/",r);if(i)g(`# CKFinder: ${i}`);if(!o.endsWith(`/${r}`))g(`# CKFinder renamed the upload to ${o}`);h(H({type:n,path:o,url:await s.urlOf(n,o)}))}},Eo={name:"cdn mkdir",usage:"mlis cdn mkdir <folder> [--type Images|Files]",summary:"Create a folder in a resource type",options:X,async run(e){let t=Y(e);h(H({path:await t.mkdir(re(e),C(e,0,"folder"))}))}},To={name:"cdn rename",usage:"mlis cdn rename <path> <new-name> [--type Images|Files]",summary:"Rename a file or folder (new name only, same folder)",options:X,async run(e){let r=await Y(e).rename(re(e),C(e,0,"path"),C(e,1,"new name"));h(H({path:r}))}},Fo={name:"cdn url",usage:"mlis cdn url <path> [--type Images|Files]",summary:"Print the site-relative and full URL of a file after checking it exists",options:X,async run(e){let t=Y(e),r=re(e),n=Ie(C(e,0,"path")),s=n.slice(n.lastIndexOf("/")+1).toLowerCase();if(!(await t.list(r,n.slice(0,n.lastIndexOf("/")+1))).some((c)=>c.kind==="file"&&c.name.toLowerCase()===s))throw new u(`${n} does not exist in ${r}`);let i=await t.urlOf(r,n);h(H({path:n,url:i,fullUrl:new URL(i,e.settings.url).href}))}},Do={name:"cdn download",usage:"mlis cdn download <path> -o <local-file> [--overwrite] [--type Images|Files]",summary:"Download a file through its public URL",options:{...X,output:{type:"string",short:"o"},overwrite:{type:"boolean"}},async run(e){let t=v(e.values,"output");if(!t)throw new u("pass -o <local-file>");if(Yr(t)&&!k(e.values,"overwrite"))throw new u(`${t} exists; pass --overwrite`);let n=await Y(e).download(re(e),C(e,0,"path"),t);g(`# saved ${n} bytes to ${t}`)}},Oo={name:"cdn rm",usage:"mlis cdn rm <path> --confirm-delete [--type Images|Files]",summary:"Delete a file permanently",options:{...X,"confirm-delete":{type:"boolean"}},async run(e){let t=Ie(C(e,0,"path"));Zr(e,`deleting ${t}`),await Y(e).deleteFile(re(e),t),g(`# deleted ${t}`)}},Mo={name:"cdn rmdir",usage:"mlis cdn rmdir <folder> --confirm-delete [--recursive] [--type Images|Files]",summary:"Delete a folder permanently (non-empty needs --recursive)",options:{...X,"confirm-delete":{type:"boolean"},recursive:{type:"boolean"}},async run(e){let t=z(C(e,0,"folder"));Zr(e,`deleting folder ${t}`),await Y(e).deleteFolder(re(e),t,k(e.values,"recursive")),g(`# deleted ${t}`)}},Qr=[Ro,xo,Io,Eo,To,Fo,Do,Oo,Mo]});function Ao(e){return/^[\w-]+\.[\w-]+\.[\w-]+$/.test(e)}function tn(e){if(!Ue.includes(e))throw new u(`unknown key ${JSON.stringify(e)}; keys: ${Ue.join(", ")}`);return e}var _o,jo,Uo,Ho,rn;var nn=b(()=>{A();x();O();te();We();_o={name:"token set",usage:"mlis token set [--url https://lhu.edu.vn] [--no-verify] (token on stdin, or pasted at the hidden prompt)",summary:"Save the jwtAuth token (from the browser cookie) used by every command",options:{"no-verify":{type:"boolean"}},async run(e){if(e.positionals.length>0)throw new u("the token is never an argument (it would stay in shell history); pipe it or paste it");let t=process.stdin.isTTY?await vr("jwtAuth token: "):await $r();if(t=t.trim().replace(/^jwtAuth=/,"").replace(/;.*$/,""),!t)throw new u("token is empty");if(!Ao(t))throw new u("that does not look like a JWT (three dot-separated parts)");let r=v(e.values,"url")?ct(v(e.values,"url")):e.settings.url;if(!k(e.values,"no-verify")){let s=await new ve(r,t).tree();g(`# token accepted by ${r} (${s.length} top-level ViewPage folders)`)}let n=await ke();await He({...n,token:t,url:r}),g(`# token saved to ${le()}`)}};jo={name:"config",usage:"mlis config",summary:"Show settings and where each value comes from (the token only as (set))",async run(e){let t=Ue.map((r)=>{let{value:n,source:s}=e.settings.resolve(r);return{key:r,value:r==="token"?n?"(set)":"":n??"",source:s}});if(h(N(t,e.render)),!e.json)g(`# file: ${le()}`)}},Uo={name:"config set",usage:"mlis config set <url|workdir|cdn-cat|cdn-mode> <value>",summary:"Change one setting",async run(e){let t=tn(C(e,0,"key"));if(t==="token")throw new u("use: mlis token set (keeps the token out of shell history)");let r=je(t,C(e,1,"value"));await He({...await ke(),[t]:r}),g(`# ${t} = ${r}`)}},Ho={name:"config unset",usage:"mlis config unset <key>",summary:"Remove one setting (unset token to forget it)",async run(e){let t=tn(C(e,0,"key")),{[t]:r,...n}=await ke();await He(n),g(`# ${t} removed`)}},rn=[_o,jo,Uo,Ho]});function Tt(e){let t=J(e);if(t===".css")return"css";if(t===".js")return"js";return L(e).toLowerCase()==="default.ascx"?"page":"control"}function sn(e,t=Tt(e)){let r=P(e),n=e.split("/")[0]??"",s=L(e).replace(/\.[^.]+$/,"");switch(t){case"page":return['<%@ Register TagPrefix="MLIS" Namespace="MLIS" %>',`<MLIS:LoadPage CssFolder="ViewPage/${r}" runat="server" />`,`<MLIS:LoadPage CssFolder="ViewPage/${n}/_default" Src="ViewPage/${n}/_default/Header.ascx" runat="server" />`,"",'<div class="container">'," <%-- Nội dung trang: đặt các control MLIS (NewsList, CatShow, ...) ở đây --%>","</div>","",`<MLIS:LoadPage Src="ViewPage/${n}/_default/Footer.ascx" runat="server" />`,""].join(`\r
|
|
33
|
+
`);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
|
+
`);case"css":return`/* ViewPage/${e} */\r
|
|
35
|
+
`;case"js":return`/* ViewPage/${e} */\r
|
|
36
|
+
$(function () {\r
|
|
37
|
+
\r
|
|
38
|
+
});\r
|
|
39
|
+
`}}var on=b(()=>{U()});import{mkdir as Vo,rmdir as Wo}from"node:fs/promises";async function an(e,t){let r=await q(e,P(t));return{folderExists:r!==null,fileExists:r?.some((n)=>n.Name.toLowerCase()===L(t).toLowerCase())??!1}}function ln(e,t){if(!k(e.values,"confirm-delete"))throw new E(`${t} is permanent on the server; pass --confirm-delete once the user has approved it`)}var Bo,Ko,Jo,zo,cn;var dn=b(()=>{A();x();O();te();Re();on();U();me();Bo={name:"new",usage:"mlis new <file.ascx|.css|.js> [--kind page|control|css|js] [--push]",summary:"Create a file from a clean template in the workspace (render and review it, then mlis push); --push also creates it on the server now",options:{kind:{type:"string"},push:{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file"));if(!T(r)||!r.includes("/"))throw new u("expected a .ascx, .css or .js path inside a site folder, e.g. LHUENB4/MyPage/Default.ascx");let n=v(e.values,"kind")??Tt(r);if(!["page","control","css","js"].includes(n))throw new u("--kind is page, control, css or js");if(await t.readLocal(r)!==null)throw new u(`${r} already exists locally`);let s=k(e.values,"push"),o=s?await an(e,r):null;if(o?.fileExists)throw new u(`${r} already exists on the server; run: mlis pull ${r}`);let i=sn(r,n);if(await t.writeLocal(r,i),g(`# wrote ${t.localPath(r)} (${n})`),!s){g(`# local only; when it is ready: mlis render shot ${P(r)} → mlis push ${r}`);return}let c=e.api();if(!o.folderExists)await qe(e,P(r));await c.createFile(P(r),L(r)),await c.save(r,i);let a=(await c.list(P(r))).find((l)=>l.Name.toLowerCase()===L(r).toLowerCase());await t.record({path:a?.Path??r,hash:mt(i),size:a?.Size??0,lastModified:a?.LastModified??""}),await t.saveState(),g(`# created on the server: ViewPage/${r}`)}},Ko={name:"rm",usage:"mlis rm <file> --confirm-delete",summary:"Delete a file on the server and locally (both copies are kept in _history)",options:{"confirm-delete":{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file"));if(!B(r))throw new u("this file type is not stored in ViewPage through the API");ln(e,`deleting ${r}`);let{fileExists:n}=await an(e,r),s=null;if(n){let i=await xe(e,r);s=F(r,i),await V(t,r,i,"deleted"),await e.api().deleteFile(r),g(`# deleted on the server: ViewPage/${r}`)}let o=await t.readLocalBytes(r);if(o!==null){if(F(r,o)!==s)await V(t,r,o,"deleted-local");await t.removeLocal(r),g(`# deleted locally: ${t.localPath(r)}`)}if(await t.forget(r),await t.saveState(),!n&&o===null)throw new u(`${r} exists neither on the server nor locally`)}},Jo={name:"mkdir",usage:"mlis mkdir <folder>",summary:"Create a folder (and missing parents) on the server",async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"folder"));if(!r||T(r))throw new u("expected a folder path, e.g. LHUENB4/MyPage");await qe(e,r),await Vo(t.localPath(r),{recursive:!0}),g(`# folder ready: ViewPage/${r}`)}},zo={name:"rmdir",usage:"mlis rmdir <folder> --confirm-delete",summary:"Delete an empty folder on the server (the server refuses non-empty folders)",options:{"confirm-delete":{type:"boolean"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"folder"));if(!r||T(r))throw new u("expected a folder path");ln(e,`deleting folder ${r}`);try{await e.api().deleteDirectory(r)}catch(n){if(n instanceof M)throw new u(`server refused: ${n.serverMessage||n.message}`);throw n}await Wo(t.localPath(r)).catch(()=>{return}),g(`# folder deleted on the server: ViewPage/${r}`)}},cn=[Bo,Ko,Jo,zo]});import{readFile as Go}from"node:fs/promises";var qo,Xo,un;var mn=b(()=>{A();x();O();Re();U();qo={name:"history",usage:"mlis history <file>",summary:"List saved copies of a file (server copies before push, local copies before pull --force, deletions)",async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file"));if(!B(r))throw new u("expected a file path, e.g. LHUENB4/@HomePage/default.ascx");let n=await Lt(t,r);if(h(N(n.map((s)=>({stamp:s.stamp,label:s.label,file:t.relOf(s.file)??s.file})),e.render)),!e.json&&n.length===0)g(`# no saved copies of ${r}`)}},Xo={name:"restore",usage:"mlis restore <file> [--at YYYYMMDD_HHMMSS]",summary:"Copy a saved version back into the workspace (newest by default); push it to publish",options:{at:{type:"string"}},async run(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"file")),n=await Lt(t,r),s=v(e.values,"at"),o=s?n.find((a)=>a.stamp===s):n[0];if(!o)throw new u(s?`no copy of ${r} saved at ${s}; see: mlis history ${r}`:`no saved copies of ${r}`);let i=await Go(o.file),c=await t.readLocalBytes(r);if(c!==null&&F(r,c)!==F(r,i))await V(t,r,c,"before-restore");await t.writeLocal(r,i),g(`# restored ${r} from ${o.stamp}${o.label?` (${o.label})`:""}; publish with: mlis push ${r}`)}},un=[qo,Xo]});import{createHash as Yo}from"node:crypto";import{mkdir as Zo,readFile as Qo,rename as ei,writeFile as ti}from"node:fs/promises";import{dirname as ri,join as ni}from"node:path";function De(e){return`${Fe}/${e}.html`}function Dt(e){return si[e??0]??"news"}function Z(e){return Yo("sha1").update(e.replace(/^/,"").replace(/\r\n/g,`
|
|
40
|
+
`)).digest("hex")}function Ft(e){let t=/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(e??"");return t?`${t[1]} ${t[2]}`:""}function Ee(e){return/[\r\n]|^\s|\s$|^"/.test(e)?JSON.stringify(e):e}function Ot(e){let t={newsId:e.NewsID,catId:e.CategoryID,title:e.NewsTitle??"",intro:e.NewsIntro??"",images:e.NewsImages??"",keyword:e.NewsKeyword??"",source:e.NewsSource??"",content:e.NewsContent??""};if(e.CatType===4)t.orderTime=Ft(e.OrderTime);return t}function Mt(e,t){let r=[Te,`newsId: ${e.newsId}`,`catId: ${e.catId}`,`title: ${Ee(e.title)}`],n=Dt(t?.CatType);if(n!=="event")r.push(`intro: ${Ee(e.intro)}`);if(r.push(`images: ${Ee(e.images)}`),n==="event")r.push(`orderTime: ${e.orderTime??""}`);else r.push(`keyword: ${Ee(e.keyword)}`);if(r.push(`source: ${Ee(e.source)}`),t){if(r.push(`# ${n} trong thư mục ${t.CatName??e.catId}`),t.CreateTime)r.push(`# tạo: ${Ft(t.CreateTime)} ${t.CreateUser??""}`.trimEnd());if(t.UpdateUser)r.push(`# sửa: ${Ft(t.UpdateTime)} ${t.UpdateUser}`);if(n==="event")r.push("# sự kiện: orderTime là thời gian diễn ra (yyyy-MM-dd HH:mm); không có tóm tắt/từ khoá");if(n==="gallery")r.push("# thư viện ảnh: chỉ đọc, sửa trong /admin")}return r.push(Ze),`${r.join(`\r
|
|
41
|
+
`)}\r
|
|
42
|
+
${e.content.replace(/\r?\n/g,`\r
|
|
43
|
+
`)}\r
|
|
44
|
+
`}function At(e,t="article"){let r=e.replace(/^/,""),n=r.indexOf(Te),s=r.indexOf(Ze,n+Te.length);if(n!==0||s<0)throw Error(`${t}: must start with a ${Te} … ${Ze} header`);let o=new Map;for(let a of r.slice(Te.length,s).split(/\r?\n/)){let l=a.trim();if(!l||l.startsWith("#"))continue;let d=l.indexOf(":");if(d<=0)throw Error(`${t}: header line "${l}" is not "key: value"`);let f=l.slice(0,d).trim(),m=l.slice(d+1).trim();if(m.startsWith('"'))try{m=JSON.parse(m)}catch{throw Error(`${t}: ${f} starts with " but is not a valid JSON string`)}o.set(f,m)}let i=(a)=>{let l=Number(o.get(a)??"");if(!Number.isInteger(l)||l<0)throw Error(`${t}: ${a} must be a number`);return l},c={newsId:i("newsId"),catId:i("catId"),title:o.get("title")??"",intro:o.get("intro")??"",images:o.get("images")??"",keyword:o.get("keyword")??"",source:o.get("source")??"",content:r.slice(s+Ze.length).replace(/^\r?\n/,"").replace(/\r?\n$/,"")};if(o.has("orderTime"))c.orderTime=o.get("orderTime");return c}function _t(e,t,r){let n={NewsID:e.newsId,CatID:e.catId},s=(o,i,c)=>{if(!r||!fn(i,c))n[o]=i??""};if(s("NewsTitle",e.title,r?.title),s("NewsIntro",e.intro,r?.intro),s("NewsContent",e.content,r?.content),s("NewsImages",e.images,r?.images),s("NewsKeyword",e.keyword,r?.keyword),s("NewsSource",e.source,r?.source),e.orderTime&&(!r||!fn(e.orderTime,r.orderTime)))n.OrderTime=e.orderTime;if(t)n.Version=t;return n}class Qe{ws;entries=null;constructor(e){this.ws=e}get path(){return ni(this.ws.root,"_mlis","news.json")}async load(){if(this.entries)return this.entries;try{this.entries=JSON.parse(await Qo(this.path,"utf-8"))}catch(e){if(e.code!=="ENOENT")throw e;this.entries={}}return this.entries}async get(e){return(await this.load())[String(e)]}async set(e,t,r){(await this.load())[String(e)]={newsId:e,version:t,hash:Z(r),syncedAt:new Date().toISOString()}}async save(){if(!this.entries)return;await Zo(ri(this.path),{recursive:!0});let e=`${this.path}.${process.pid}.tmp`;await ti(e,`${JSON.stringify(this.entries,null,1)}
|
|
45
|
+
`,"utf-8"),await ei(e,this.path)}}var Fe="_news",Te="<!--mlis-news",Ze="-->",si,fn=(e,t)=>(e??"").replace(/\r\n/g,`
|
|
46
|
+
`)===(t??"").replace(/\r\n/g,`
|
|
47
|
+
`);var pn=b(()=>{si={4:"event",6:"gallery"}});import{rm as oi}from"node:fs/promises";import{isAbsolute as ii,relative as ai,resolve as li,sep as ci}from"node:path";function ge(e,t){let r=Number(e);if(e===void 0||!Number.isInteger(r)||r<0)throw new u(`${t} must be a number`);return r}function hn(e,t){if(/^\d+$/.test(t))return De(Number(t));let r=e.workspace(),n=ai(r.root,li(t)).split(ci).join("/");if(n.startsWith("..")||ii(n)||!n.toLowerCase().startsWith(`${Fe.toLowerCase()}/`))throw new u(`${t}: give a NewsID or a file in ${r.localPath(Fe)}`);return n}function he(e){return Mt(Ot(e),e)}function gn(e){let t=/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(String(e??""));return t?`${t[1]} ${t[2]}`:""}var di,ui,mi,fi,pi,gi,wn;var yn=b(()=>{A();x();O();te();Re();pn();di={name:"news ls",usage:"mlis news ls <CatID> [--search text] [--from yyyy/mm/dd] [--to yyyy/mm/dd] [--skip n] [--limit n] [--trash]",summary:"List the articles of a category (newest/sorted as in /admin → Bài viết)",options:{search:{type:"string"},from:{type:"string"},to:{type:"string"},skip:{type:"string"},limit:{type:"string"},trash:{type:"boolean"}},async run(e){let t=await e.api().newsList({catId:ge(C(e,0,"CatID"),"CatID"),search:v(e.values,"search"),from:v(e.values,"from"),to:v(e.values,"to"),skip:v(e.values,"skip")===void 0?0:ge(v(e.values,"skip"),"--skip"),limit:v(e.values,"limit")===void 0?20:ge(v(e.values,"limit"),"--limit"),trash:k(e.values,"trash")});if(e.json){h(N(t,e.render));return}if(h(N(t.map((r)=>({id:r.NewsID,title:r.NewsTitle,keyword:r.NewsKeyword??"",created:`${gn(r.CreateTime)} ${r.CreateUser??""}`.trim(),updated:r.UpdateUser?`${gn(r.UpdateTime)} ${r.UpdateUser}`:"",image:r.NewsImages??""})),e.render)),t.length===0)g("# no articles (or no right on this category)")}},ui={name:"news show",usage:"mlis news show <NewsID>",summary:"Print one article (fields + HTML content) from the server",async run(e){let t=await e.api().news(ge(C(e,0,"NewsID"),"NewsID"));h(e.json?`${JSON.stringify(t,null,2)}
|
|
48
|
+
`:he(t))}},mi={name:"news pull",usage:"mlis news pull <NewsID>... [--force]",summary:"Download articles to <workspace>/_news/<NewsID>.html to edit (keeps local edits unless --force)",options:{force:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new u("missing NewsID");let t=e.workspace(),r=new Qe(t),n=[];for(let s of e.positionals){let o=ge(s,"NewsID"),i=await e.api().news(o),c=he(i),a=De(o),l=await t.readLocal(a),d=await r.get(o),f=l===null?"new":"updated";if(l!==null&&Z(l)===Z(c))f="unchanged";else if(l!==null&&(!d||Z(l)!==d.hash)){if(!k(e.values,"force")){n.push({path:a,action:"kept-local",note:"local edits; push them, or pull --force"});continue}await V(t,a,l,"local")}if(f!=="unchanged")await t.writeLocal(a,c);await r.set(o,i.Version,c),n.push({path:a,action:f,note:`${Dt(i.CatType)} · ${i.CatName??i.CategoryID}`})}await r.save(),h(N(n,e.render))}},fi={name:"news diff",usage:"mlis news diff <NewsID|file>",summary:"Diff a local article against the server",async run(e){let t=e.workspace(),r=hn(e,C(e,0,"NewsID or file")),n=await t.readLocal(r);if(n===null)throw new u(`${r} is not in the workspace; run: mlis news pull <NewsID>`);let s=At(n,r);if(s.newsId===0){h(n),g("# new article (newsId 0): all of it is added on push");return}let o=he(await e.api().news(s.newsId)),i=Ge(o,n,`server/${s.newsId}`,r);if(h(i||""),!i)g("# no difference")}},pi={name:"news push",usage:"mlis news push <NewsID|file>... [--dry-run] [--force]",summary:"Save local articles to the server (stops, exit 3, when the article changed on the server since the pull)",options:{"dry-run":{type:"boolean"},force:{type:"boolean"}},async run(e){if(e.positionals.length===0)throw new u("missing NewsID or file");let t=e.workspace(),r=new Qe(t),n=e.api(),s=k(e.values,"dry-run"),o=k(e.values,"force"),i=[];try{for(let c of e.positionals){let a=hn(e,c),l=await t.readLocal(a);if(l===null)throw new u(`${a} is not in the workspace`);let d=At(l,a);if(d.newsId===0){if(s){i.push({path:a,action:"would-create",note:`category ${d.catId}`});continue}let R=await n.saveNews(_t(d)),$e=he(R),K=De(R.NewsID);if(await t.writeLocal(K,$e),K!==a)await oi(t.localPath(a));await r.set(R.NewsID,R.Version,$e),i.push({path:K,action:"created",note:`NewsID ${R.NewsID}`});continue}if(a.toLowerCase()!==De(d.newsId).toLowerCase())throw new u(`${a}: header says newsId ${d.newsId}; an article file is named <NewsID>.html`);let f=await r.get(d.newsId),m=await n.news(d.newsId);if(m.CatType===6)throw new E(`${a}: gallery articles are edited in /admin only`);if(d.catId!==m.CategoryID)throw new u(`${a}: catId cannot change (${m.CategoryID} → ${d.catId}); move articles by dragging them in /admin → Bài viết`);let w=he(m);if(!o&&!f)throw new E(`${a} was never pulled here; run: mlis news pull ${d.newsId} (or push --force to overwrite the server)`);if(!o&&f&&f.version!==m.Version)throw new E(`${a}: the article changed on the server after your pull. See: mlis news diff ${d.newsId}; then keep yours (push --force) or take the server's (pull --force)`);if(Z(l)===Z(w)){i.push({path:a,action:"unchanged",note:""});continue}if(s){i.push({path:a,action:"would-update",note:"mlis news diff shows the change"});continue}await V(t,a,w,"server");let p=await n.saveNews(_t(d,o?void 0:m.Version,Ot(m))),S=he(p);await t.writeLocal(a,S),await r.set(p.NewsID,p.Version,S);let I=Z(S)!==Z(l);i.push({path:a,action:"updated",note:I?"server normalised some fields; file rewritten":""})}}catch(c){if(c instanceof M&&c.status===409)throw new E("the article changed on the server while pushing; run mlis news diff, then pull or push --force");throw c}finally{if(await r.save(),i.length)h(N(i,e.render))}}},gi={name:"news new",usage:"mlis news new <CatID> [--title text]",summary:"Start a new article file (newsId 0) in _news; mlis news push creates it on the server",options:{title:{type:"string"}},async run(e){let t=e.workspace(),r=ge(C(e,0,"CatID"),"CatID"),n=`${Fe}/new-${r}-${St()}.html`,s=Mt({newsId:0,catId:r,title:v(e.values,"title")??"Tiêu đề bài viết",intro:"",images:"",keyword:"",source:"",content:"<p>Nội dung bài viết (HTML, ít nhất 30 ký tự).</p>"}).replace("-->",`# thư mục sự kiện: thêm dòng orderTime: yyyy-MM-dd HH:mm\r
|
|
49
|
+
-->`);await t.writeLocal(n,s),h(`${t.localPath(n)}
|
|
50
|
+
`)}},wn=[di,ui,mi,fi,pi,gi]});function wi(e){return[...e.matchAll(/\{([^}]+)\}/g)].map((t)=>t[1].trim().toLowerCase()).filter(Boolean)}function $n(e){return e.split("/")[0]??""}function yi(e,t){let r=t.toLowerCase().replace(/^www\./,"");return e.items.filter((n)=>wi(n.link).includes(r)).sort((n,s)=>e.ancestors(n.id).length-e.ancestors(s.id).length||(n.sort??0)-(s.sort??0)||n.id-s.id)}function vn(e,t){let r=t.hostname.toLowerCase(),n=t.pathname.split("/").filter(Boolean).map((d)=>decodeURIComponent(d)),s=yi(e,r),o=s[0],i={host:r,path:t.pathname,kind:"unknown",home:o,otherHomes:s.slice(1)},c=(d)=>d&&/^\d+$/.test(d)?e.byId.get(Number(d)):void 0;if(n.length&&hi.test(n[0]))return{...i,kind:"platform",note:`${n[0]} is generated by the platform (robotsTxt/llmsTxt/sitemapXml/rssFeeds/error .aspx), not a ViewPage template`};if(!o)return{...i,note:`no category has {${r}} in its link: the domain has no home page`};if(n.length===0)return{...i,kind:"home",category:o,template:o.pageStyle};let a=n[0];if(/^\d+$/.test(a)){let d=c(a);if(!d)return{...i,note:`category ${a} is unknown (or not visible to this account)`};let f=n[1];if(f&&/^\d+$/.test(f)&&Number(f)>0){let m=d.pageRead||d.pageStyle,w=d.pageRead?void 0:"the category has no PageRead; the article page then depends on the server's stored procedure (shown: PageStyle)";return{...i,kind:"read",category:d,newsId:Number(f),template:m,note:w}}return{...i,kind:"category",category:d,template:d.pageStyle}}let l=a.toLowerCase();if(l in Cn||l==="more-event"){let d=c(n[1]);if(!d)return{...i,note:`category ${n[1]??"?"} is unknown (or not visible to this account)`};let f=l==="more-news"&&n[2]&&/^\d+$/.test(n[2])?Number(n[2]):void 0,m=l==="more-event"?d.pageStyle:`${$n(d.pageStyle)}/${Cn[l]}`;return{...i,kind:"more",category:d,newsId:f,template:m}}if(/^\w+$/.test(a))return{...i,kind:"route",category:o,template:`${$n(o.pageStyle)}/_${a}`};return{...i,note:`/${a} matches no route: the site answers 404`}}function kn(e,t){let r=t.toLowerCase(),n=[];for(let s of e.items){if(s.pageStyle.toLowerCase()===r)n.push({category:s,role:"page"});if(s.pageRead.toLowerCase()===r)n.push({category:s,role:"read"})}return n}var Cn,hi;var bn=b(()=>{Cn={"more-list":"_more-List","more-related":"_more-Related","more-news":"_more-News"},hi=/^(robots\.txt|llms\.txt|sitemap(-[\w-]+)?\.xml|rss|error)$/i});function Sn(e,t,r){return`/__mlis/ph/${e}x${t}/${r}.svg`}function Pn(e,t){if(T(e)&&e.toLowerCase().endsWith(".ascx")&&L(e).toLowerCase()!=="default.ascx")return{kind:"control",folder:P(e),control:L(e)};let r=T(e)?P(e):e,n=L(r);if(!t&&r.split("/").length===2&&n.startsWith("_"))return{kind:"route",folder:r,route:n.slice(1)};let s=t??(/home/i.test(n)?"home":/read/i.test(n)?"read":"category");if(!["home","category","read"].includes(s))throw Error("--as must be home, category or read");return{kind:s,folder:r}}function vi(e){let t=[];for(let r of e.matchAll(/<MLIS:(\w+)/gi)){let n="",s=r.index+r[0].length;for(;s<e.length;s++){let l=e[s];if(n){if(l===n)n=""}else if(l==='"'||l==="'")n=l;else if(l===">")break}let o=e.slice(r.index+r[0].length,s),i=(l)=>new RegExp(`\\b${l}\\s*=\\s*["']\\s*(-?\\d+)\\s*["']`,"i").exec(o)?.[1],c=i("CatID"),a=c===void 0?null:Number(c)>=0?Number(c):null;t.push({name:r[1],catId:a,location:Number(i("Location")??0)})}return t}function et(e){let t=[];for(let r of e.matchAll(/<MLIS:LoadPage\b[^>]*?\bSrc\s*=\s*"([^"]+)"/gi)){let n=/^\/?viewpage\/(.+)$/i.exec(r[1].trim());if(n)t.push(ce(n[1]))}return t}function Ln(e,t,r){let n=new Map,s=2000,o=(m)=>{let w={alias:"",description:`Mô tả mẫu của thư mục "${m.name}".`,html:`<p>Nội dung HTML mẫu của thư mục <strong>${m.name}</strong> (soạn trong /admin → Thư mục → Tài liệu HTML).</p>`,pageStyle:"",pageRead:"",link:"",locations:[0,0,0],bits:Ci|$i,type:2,picture:Sn(600,400,`thu-muc-${m.id}`),icon:"fa fa-folder-open",...m};return n.set(w.id,w),w},i=t.folder.split("/")[0],a=o({id:oe,parent:0,name:"Đại học Lạc Hồng",alias:"Đại Học Lạc Hồng",type:1,link:`{${e}}`,pageStyle:t.kind==="home"?t.folder:`${i}/@HomePage`,pageRead:`${i}/@ReadPage`,html:[1,2,3].map((m)=>`<div><img src="${Sn(1600,520,`banner-${m}`)}" alt="Banner mẫu ${m}" style="width:100%" /></div>`).join("")});if(t.kind!=="home"){let m=t.kind==="control"?`${jt}/${Ut(t)}`:t.kind==="route"?`${i}/@HomePage`:t.folder;a=o({id:ie,parent:oe,name:"Thư mục mẫu",alias:"Thư mục mẫu",pageStyle:m,pageRead:t.kind==="read"?t.folder:`${i}/@ReadPage`})}let l=(m)=>n.get(m)??o({id:m,parent:oe,name:`Thư mục ${m}`,pageStyle:a.pageStyle,pageRead:a.pageRead}),d=new Set,f=(m,w)=>{let p=`${m}|${w}`;if(d.has(p))return;d.add(p);for(let S=1;S<=3;S++){let I=o({id:s++,parent:m,name:w?`Mục mẫu ${w}.${S}`:`Mục mẫu ${S}`,locations:[w,0,0],pageStyle:a.pageStyle,pageRead:a.pageRead});for(let R=1;R<=2;R++)o({id:s++,parent:I.id,name:`${I.name} – con ${R}`})}};for(let m of r)for(let w of vi(m)){if(w.name.toLowerCase()==="loadpage")continue;let p=w.catId===null?[...new Set([a.id,oe])]:[w.catId===0?oe:l(w.catId).id];for(let S of p)if(w.location>0||/^cat(menu|related)$/i.test(w.name))f(S,w.location)}return f(a.id,0),f(oe,0),{host:e,home:oe,newsCount:12,categories:[...n.values()]}}function Ut(e){return`Wrap${(e.control??"control").replace(/\.ascx$/i,"").replace(/[^A-Za-z0-9_]/g,"")}`}function Nn(e){let t=e.folder.split("/")[0];return['<%@ Register TagPrefix="MLIS" Namespace="MLIS" %>',`<%-- mlis render: trang bọc để xem riêng ${e.folder}/${e.control} --%>`,`<MLIS:LoadPage Src="ViewPage/${t}/_assets/HeaderOfSite.ascx" runat="server" />`,`<MLIS:LoadPage CssFolder="ViewPage/${e.folder}" Src="ViewPage/${e.folder}/${e.control}" runat="server" />`,""].join(`\r
|
|
51
|
+
`)}function Rn(e){switch(e.kind){case"home":return"/";case"read":return`/${ie}/${ie*1000+1}/tin-mau.html`;case"route":{let t=e.route;if(/^more-(list|related)$/i.test(t))return`/${t.toLowerCase()}/${ie}/0/0/thu-muc-mau.html`;if(/^more-news$/i.test(t))return`/more-news/${ie}/${ie*1000+1}/0/thu-muc-mau.html`;if(/^index$/i.test(t))return"/Index/A";if(/^search$/i.test(t))return"/search/?q=lac+hong";return`/${t}`}default:return`/${ie}/thu-muc-mau.html`}}var oe=1,ie=900,jt="_mlis/render",Ci=8,$i=4;var Ht=b(()=>{U()});function ki(e,t){try{if(e.startsWith("/"))return new URL(e,t);return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`)}catch{throw new u(`not a page address: ${e}`)}}async function bi(e,t){let r=Se(await e.api().tree(),t);if(!r)return null;let s=(await q(e,r.Path)??[]).find((i)=>i.Name.toLowerCase()==="default.ascx"),o=s?et((await e.api().read(`${r.Path}/${s.Name}`)).Content):[];return{template:r.Path,page:s?`${r.Path}/${s.Name}`:null,includes:o,head:`${r.Path.split("/")[0]}/_assets/HeaderOfSite.ascx`}}var Si,xn;var In=b(()=>{A();x();O();$t();bn();Ht();Ke();me();Si={name:"page",usage:"mlis page <url> [--pull] [--refresh]",summary:"Which ViewPage template draws a page of the site (any of its domains), what it includes, and what else uses it; --pull downloads them",options:{pull:{type:"boolean"},refresh:{type:"boolean"}},async run(e){let t=ki(C(e,0,"page address"),e.settings.url),r=await Je(e.workspace(),()=>e.api(),k(e.values,"refresh")),n=vn(r.index,t),s=n.template?await bi(e,n.template):null,o=n.template?kn(r.index,n.template):[],i=r.index;if(e.json)h(H({url:t.href,kind:n.kind,home:n.home&&{id:n.home.id,name:n.home.name,pageStyle:n.home.pageStyle},otherHomes:n.otherHomes.map((c)=>c.id),category:n.category&&{id:n.category.id,name:n.category.name,path:i.pathText(n.category.id)},newsId:n.newsId??null,template:s?.template??n.template??null,exists:s!==null,page:s?.page??null,includes:s?.includes??[],head:s?.head??null,usedBy:o.map((c)=>({id:c.category.id,name:c.category.name,role:c.role,domain:i.domainFor(c.category.id)})),note:n.note??null}));else{let c=[["url",t.href]];if(c.push(["kind",n.kind+(n.newsId?` (article ${n.newsId})`:"")]),n.home)c.push(["domain",`${n.host} → home ${n.home.id} ${n.home.name}${n.otherHomes.length?` (also linked: ${n.otherHomes.map((d)=>d.id).join(", ")})`:""}`]);if(n.category&&n.category!==n.home)c.push(["category",`${n.category.id} ${i.pathText(n.category.id)}`]);if(n.template)c.push(["template",s?`${s.template} (${s.page??"no default.ascx!"})`:`${n.template} (not on the server: the page fails)`]);if(s?.includes.length)c.push(["includes",s.includes.map((d)=>/\.ascx$/i.test(d)?d:`${d} (not an .ascx: loads nothing)`).join(", ")]);if(s)c.push(["head",s.head]);let a=new Set(o.map((d)=>d.category.id)).size;if(a>1)c.push(["shared",`${a} categories use this template: editing it changes all of them — mlis category pages ${s?.template??n.template}`]);if(n.newsId)c.push(["content",`article text/images: mlis news pull ${n.newsId}`]);if(n.note)c.push(["note",n.note]);let l=Math.max(...c.map(([d])=>d.length));h(`${c.map(([d,f])=>`${d.padEnd(l)} ${f}`).join(`
|
|
52
|
+
`)}
|
|
53
|
+
`)}if(k(e.values,"pull")&&s)for(let c of[s.template,...s.includes.filter((a)=>/\.ascx$/i.test(a)),s.head])await xt.run({...e,positionals:[c],values:{...e.values,force:!1,all:!1}});else if(s&&!e.json)g(`# edit it locally: mlis page ${C(e,0,"page address")} --pull`)}},xn=[Si]});import{spawn as Pi,spawnSync as ye}from"node:child_process";import{existsSync as W,openSync as Li}from"node:fs";import{copyFile as Ni,mkdir as tt,readdir as Ri,readFile as Kt,stat as rt,writeFile as Vt}from"node:fs/promises";import{dirname as Wt,join as y,relative as Hc,resolve as we}from"node:path";import{fileURLToPath as xi}from"node:url";function Q(){return y(lt(),"render")}function Fn(){let e=Wt(xi(import.meta.url)),t=[y(e,"..","..","render"),y(e,"..","render"),y(Wt(process.execPath),"render")];return t.find((r)=>W(y(r,"MlisRender.cs")))??t[0]}function En(){if(process.platform!=="win32")throw new u("mlis render needs Windows with .NET Framework 4 (the MLIS site is ASP.NET)");let e=Ii.find((t)=>W(y(t,"csc.exe")));if(!e)throw new u(".NET Framework 4 csc.exe not found; install .NET Framework 4.x");return y(e,"csc.exe")}async function nt(e,t){if(!W(e))return!0;let r=(await rt(e)).mtimeMs;for(let n of t)if((await rt(n)).mtimeMs>r)return!0;return!1}function Tn(e,t){let r=ye(e,t,{encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw Error(`${e} failed:
|
|
54
|
+
${(r.stdout||"")+(r.stderr||"")}`.trim())}async function Di(e){let t=Fn(),r=y(Q(),"kit");await tt(r,{recursive:!0});let n=y(e,"bin","Newtonsoft.Json.dll");if(!W(n))throw new u(`${e}\\bin\\Newtonsoft.Json.dll not found: is site-src the MLIS site?`);let s=y(r,"DataConnection.dll");if(await nt(s,[y(t,"SampleDataConnection.cs")]))Tn(En(),["/nologo","/target:library",`/out:${s}`,"/r:System.Data.dll","/r:System.Core.dll",`/r:${n}`,y(t,"SampleDataConnection.cs")]);let o=y(r,"MlisRender.exe");if(await nt(o,[y(t,"MlisRender.cs")]))Tn(En(),["/nologo","/target:exe",`/out:${o}`,"/r:System.Web.dll","/r:System.dll",y(t,"MlisRender.cs")]);return r}async function Oi(){let e=Fn(),t=y(Q(),"kit");return await nt(y(t,"DataConnection.dll"),[y(e,"SampleDataConnection.cs")])||await nt(y(t,"MlisRender.exe"),[y(e,"MlisRender.cs")])}async function Bt(e,t){let r=await rt(e),n=W(t)?await rt(t):null;if(!n||n.size!==r.size||n.mtimeMs<r.mtimeMs)await Ni(e,t)}async function Mi(e,t){if(W(e)&&await Kt(e,"utf-8")===t)return;await Vt(e,t,"utf-8")}function Ai(e){if(/<globalization\b[^>]*fileEncoding/i.test(e))return e;if(/<globalization\b/i.test(e))return e.replace(/<globalization\b/i,'<globalization fileEncoding="utf-8"');return e.replace(/<system\.web>/i,`<system.web>\r
|
|
55
|
+
<globalization fileEncoding="utf-8" />`)}async function Dn(e,t,r=!0){await tt(t,{recursive:!0});for(let n of await Ri(e,{withFileTypes:!0})){if(r&&(Ei.has(n.name.toLowerCase())||n.name.toLowerCase()===Fi))continue;let s=y(e,n.name),o=y(t,n.name);if(n.isDirectory())await Dn(s,o,!1);else if(n.isFile()&&!Ti.test(n.name))await Bt(s,o)}}function _i(e){let t=ye("cmd",["/c","dir","/al",Wt(e)],{encoding:"utf-8",windowsHide:!0}),r=new RegExp(`<JUNCTION>\\s+${ji(e)}\\s+\\[([^\\]]+)\\]`,"i").exec(t.stdout??"");return r?r[1]:null}function ji(e){return e.split(/[\\/]/).pop().replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Ui(e,t){let r=y(e,"ViewPage"),n=W(r)?_i(r):null;if(n&&we(n).toLowerCase()===we(t).toLowerCase())return;if(W(r)){if(!n)throw new u(`${r} exists and is not a junction; remove it`);ye("cmd",["/c","rmdir",r],{windowsHide:!0})}let s=ye("cmd",["/c","mklink","/J",r,t],{encoding:"utf-8",windowsHide:!0});if(s.status!==0)throw Error(`cannot link ${r} to ${t}: ${s.stdout}${s.stderr}`)}async function Hi(e,t){if(!W(y(e,"Web.config"))||!W(y(e,"bin")))throw new u(`site-src ${e} is not the MLIS site (needs Web.config and bin\\); set it: mlis config set site-src <folder>`);let r=await Di(e),n=y(Q(),"site");return await Dn(e,n),await Mi(y(n,"Web.config"),Ai(await Kt(y(e,"Web.config"),"utf-8"))),await Bt(y(r,"DataConnection.dll"),y(n,"bin","DataConnection.dll")),await Bt(y(r,"MlisRender.exe"),y(n,"bin","MlisRender.exe")),await tt(y(n,"Data"),{recursive:!0}),await tt(t,{recursive:!0}),Ui(n,t),n}function Me(){return y(Q(),"data.json")}async function On(){try{return JSON.parse(await Kt(y(Q(),"server.json"),"utf-8"))}catch{return null}}async function Oe(e){try{let t=await fetch(`http://localhost:${e}/__mlis/ping`,{signal:AbortSignal.timeout(1500)});return t.ok&&await t.text()==="mlis render"}catch{return!1}}async function Jt(){let e=await On();if(!e||!await Oe(e.port))return!1;return await fetch(`http://localhost:${e.port}/__mlis/stop`).catch(()=>{return}),!0}async function Mn(e,t){let r=e.resolve("site-src").value;if(!r)throw new u("where is the MLIS site code? run: mlis config set site-src <MLIS_2025 folder or its publish folder>");let n=Number(e.resolve("render-port").value),s=await On();if(s&&s.port===n&&await Oe(n)){if(!await Oi()&&we(s.workspace).toLowerCase()===we(t).toLowerCase()&&we(s.siteSrc).toLowerCase()===we(r).toLowerCase())return s;await Jt(),await new Promise((f)=>setTimeout(f,800))}else if(await Oe(n))throw new u(`port ${n} is used by another program; pick one: mlis config set render-port <port>`);let o=await Hi(r,t);if(!W(Me()))await Vt(Me(),`${JSON.stringify({host:new URL(e.url).hostname,home:1,newsCount:12,categories:[]})}
|
|
56
|
+
`);let i=Li(y(Q(),"server.log"),"a"),c=new URL(e.url),a=["--site",o,"--port",String(n),"--host",c.hostname,"--origin",c.origin,"--cache",y(Q(),"live")],l=Pi(y(o,"bin","MlisRender.exe"),a,{detached:!0,stdio:["ignore",i,i],windowsHide:!0,env:{...process.env,MLIS_RENDER_DATA:Me()}});l.unref();for(let f=0;f<60&&!await Oe(n);f++)await new Promise((m)=>setTimeout(m,500));if(!await Oe(n))throw Error(`render server did not start; see ${y(Q(),"server.log")}`);let d={pid:l.pid??0,port:n,workspace:t,siteSrc:r};return await Vt(y(Q(),"server.json"),`${JSON.stringify(d)}
|
|
57
|
+
`),d}async function An(e){let t=await fetch(e,{signal:AbortSignal.timeout(180000),redirect:"manual"});for(let a=0;a<60&&t.status===503;a++)await new Promise((l)=>setTimeout(l,1000)),t=await fetch(e,{signal:AbortSignal.timeout(180000),redirect:"manual"});let r=await t.text(),n=t.headers.get("location")??"";if(t.status>=300&&t.status<400&&/^\/error/i.test(new URL(n,e).pathname))return{status:t.status,error:`the page control failed (the site redirects to ${n})`};if(t.status<500)return{status:t.status,error:null};let s=r.replace(/<style[\s\S]*?<\/style>/gi,"").replace(/<[^>]+>/g," ").replace(/ /g," ").replace(/\s+/g," "),o=(a)=>new RegExp(`${a}:?\\s*(.{0,300}?)(?=\\s(Source Error|Source File|Stack Trace|Line \\d+|Version Information)|$)`).exec(s)?.[1]?.trim(),i=o("Parser Error Message")??o("Compiler Error Message")??o("Exception Details")??s.slice(0,300),c=/Source File:\s*(\S+)\s+Line:\s*(\d+)/.exec(s);return{status:t.status,error:`${i}${c?` (${c[1]} line ${c[2]})`:""}`}}async function _n(e,t){return(await(await fetch(`http://localhost:${e}/__mlis/compile?path=${encodeURIComponent(`ViewPage/${t}`)}`,{signal:AbortSignal.timeout(120000)})).text()).trim()}function jn(){let e=process.env.USERPROFILE??process.env.HOME??"",t=[y(e,".bun","bin","playwright.exe"),y(e,"AppData","Roaming","npm","playwright.cmd")],r=ye("where",["playwright"],{encoding:"utf-8",windowsHide:!0});if(r.status===0)t.unshift(...r.stdout.split(/\r?\n/).filter(Boolean));return t.find((n)=>W(n))??null}function Un(e,t,r,n,s){let o=["screenshot","--wait-for-timeout","1500","--ignore-https-errors"];if(o.push("--browser","chromium","--viewport-size",n?"390, 844":"1366, 900"),s)o.push("--full-page");o.push(t,r);let i=ye(e,o,{encoding:"utf-8",windowsHide:!0,shell:e.endsWith(".cmd")});if(i.status!==0)throw Error(`playwright screenshot failed: ${(i.stdout||"")+(i.stderr||"")}`.trim())}var Ii,Ei,Ti,Fi="web.config";var Hn=b(()=>{x();We();Ii=["C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319","C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319"],Ei=new Set(["viewpage","data","app_data","obj",".vs","aspnet_client"]),Ti=/\.(sln|suo|publishproj|user|doc|docx)$/i});import{mkdir as Wn,writeFile as Vn}from"node:fs/promises";import{join as zt,resolve as Vi}from"node:path";async function Kn(e){let t=e.workspace(),r=t.resolveArg(C(e,0,"ViewPage folder or file")),n=Pn(r,v(e.values,"as")),s=n.folder.split("/")[0],o=n.kind==="control"?`${n.folder}/${n.control}`:`${n.folder}/Default.ascx`,i=n.kind==="home"||n.kind==="category"||n.kind==="read"||n.kind==="control"?o:`${s}/@HomePage/Default.ascx`;if(await t.readLocal(o)===null)throw new u(`${o} is not in the workspace; run: mlis pull ${n.folder}`);let c=[],a=[],l=new Set,d=[o,i,`${s}/_assets/HeaderOfSite.ascx`];while(d.length){let p=d.shift();if(l.has(p.toLowerCase()))continue;l.add(p.toLowerCase());let S=await t.readLocalBytes(p);if(!S)continue;let I=G(S);c.push(I),a.push(p),d.push(...et(I))}let f=new URL(e.settings.url).hostname,m=Ln(f,n,c);if(n.kind==="control"){let p=t.localPath(`${jt}/${Ut(n)}`);await Wn(p,{recursive:!0}),await Vn(zt(p,"default.ascx"),`\uFEFF${Nn(n)}`,"utf-8")}let w=await Mn(e.settings,t.root);return await Vn(Me(),`${JSON.stringify(m,null,1)}
|
|
58
|
+
`,"utf-8"),{target:n,url:`http://localhost:${w.port}${Rn(n)}`,port:w.port,files:a}}async function Jn({url:e,port:t,files:r}){g(`# rendering ${e} (the first request compiles the site, up to a minute)`);let n=await An(e),s=[];for(let o of r.filter((i)=>i.toLowerCase().endsWith(".ascx"))){let i=await _n(t,o);if(i)s.push(`${o}: ${i}`)}if(n.error)s.unshift(`the page does not render (HTTP ${n.status}): ${n.error}`);if(s.length)throw Error(s.join(`
|
|
59
|
+
`));if(n.status>=400)g(`# warning: HTTP ${n.status}`)}var Bn,Wi,Bi,Ki,zn;var Gn=b(()=>{A();x();O();Ht();Hn();me();Bn={as:{type:"string"}};Wi={name:"render start",usage:"mlis render start <folder|file> [--as home|category|read]",summary:"Run the page locally with sample data (no server writes) and print its http://localhost URL",options:Bn,async run(e){let t=await Kn(e);await Jn(t),h(`${t.url}
|
|
60
|
+
`)}},Bi={name:"render shot",usage:"mlis render shot <folder|file> [--as home|category|read] [--device desktop|mobile|both] [--full] [-o dir]",summary:"Render the page locally with sample data and save screenshots (desktop 1366px, mobile 390px)",options:{...Bn,device:{type:"string"},full:{type:"boolean"},out:{type:"string",short:"o"}},async run(e){let t=v(e.values,"device")??"both";if(!["desktop","mobile","both"].includes(t))throw new u("--device must be desktop, mobile or both");let r=jn();if(!r)throw new u("playwright not found; install it: bun add -g playwright && playwright install chromium");let n=await Kn(e),{target:s,url:o}=n;await Jn(n);let i=Vi(v(e.values,"out")??zt(e.workspace().root,"_mlis","shots"));await Wn(i,{recursive:!0});let c=`${s.folder}${s.control?`/${s.control.replace(/\.ascx$/i,"")}`:""}`.replace(/[\\/@]+/g,"_").replace(/^_+/,""),a=[];for(let l of t==="both"?["desktop","mobile"]:[t]){let d=zt(i,`${c}-${l}.png`);Un(r,o,d,l==="mobile",k(e.values,"full")),a.push(d)}h(`${a.join(`
|
|
61
|
+
`)}
|
|
62
|
+
`)}},Ki={name:"render stop",usage:"mlis render stop",summary:"Stop the local render server",async run(){g(await Jt()?"# render server stopped":"# render server is not running")}},zn=[Wi,Bi,Ki]});import{existsSync as Gt}from"node:fs";import{cp as Ji,readFile as zi,readdir as qn,rm as Gi,writeFile as qi}from"node:fs/promises";import{homedir as Xi}from"node:os";import{dirname as st,join as _}from"node:path";import{fileURLToPath as Xn}from"node:url";function ot(){let e=st(Xn(import.meta.url)),t=[_(e,"..","..","skills",Ce),_(e,"..","skills",Ce),_(st(process.execPath),"skills",Ce),_(st(process.execPath),"..","skills",Ce)];return t.find((r)=>Gt(_(r,"SKILL.md")))??t[0]}function ae(e=process.env){return _(e.CLAUDE_CONFIG_DIR||_(e.HOME||e.USERPROFILE||Xi(),".claude"),"skills")}async function Yn(e,t){let r=new Map((await qn(e,{withFileTypes:!0})).map((n)=>[n.name,n]));for(let n of await qn(t,{withFileTypes:!0})){if(n.name===qt)continue;let s=r.get(n.name),o=_(t,n.name);if(!s)await Gi(o,{recursive:!0,force:!0});else if(n.isDirectory()&&s.isDirectory())await Yn(_(e,n.name),o)}}async function Xt(e,t=ae()){let r=ot();if(!Gt(_(r,"SKILL.md")))throw Error(`skill bundle not found at ${r}`);let n=_(t,Ce);return await Ji(r,n,{recursive:!0,force:!0}),await Yn(r,n),await qi(_(n,qt),`${e}
|
|
63
|
+
`,"utf-8"),n}async function Yi(e=ae()){try{return(await zi(_(e,Ce,qt),"utf-8")).trim()}catch{return null}}async function Zn(e,t=process.env){let r=t.MLIS_AUTO_SKILLS?.trim().toLowerCase();if(r&&["0","off","false","no"].includes(r))return null;if(Xn(import.meta.url).endsWith(".ts"))return null;let s=st(ae(t));if(!Gt(s))return null;if(await Yi(ae(t))===e)return null;try{let o=await Xt(e,ae(t));return`# mlis ${e} skill installed at ${o}`}catch(o){return`# mlis skill not installed: ${o.message}; run: mlis skill install`}}var Ce="mlis",qt=".mlis-version";var Yt=()=>{};import{readFile as Qn,readdir as Zi}from"node:fs/promises";import{join as Zt}from"node:path";var Qi,ea,ta,es;var ts=b(()=>{x();O();Yt();at();Qi={name:"skill",usage:"mlis skill",summary:"Print the agent guide (SKILL.md)",async run(){h(await Qn(Zt(ot(),"SKILL.md"),"utf-8"))}},ea={name:"skill ref",usage:"mlis skill ref [name]",summary:"List the reference pages, or print one (e.g. controls)",async run(e){let t=Zt(ot(),"references"),r=(await Zi(t)).filter((o)=>o.endsWith(".md")).map((o)=>o.slice(0,-3)),n=e.positionals[0];if(!n){h(`${r.join(`
|
|
64
|
+
`)}
|
|
65
|
+
`);return}let s=n.replace(/\.md$/,"");if(!r.includes(s))throw new u(`no reference "${s}"; available: ${r.join(", ")}`);h(await Qn(Zt(t,`${s}.md`),"utf-8"))}},ta={name:"skill install",usage:"mlis skill install [dir]",summary:"Copy the skill into Claude Code (~/.claude/skills/mlis) or another skills folder",async run(e){let t=e.positionals[0]??ae(),r=await Xt(Ae.version,t);g(`# skill installed at ${r}`)}},es=[Qi,ea,ta]});import{parseArgs as ra}from"node:util";function ss(e){let t=Math.max(...e.map((r)=>r.name.length));return e.map((r)=>` ${r.name.padEnd(t)} ${r.summary}`).join(`
|
|
66
|
+
`)}function rs(){return`mlis ${Qt}: design MLIS ViewPage (.ascx/.css/.js) through the site's API
|
|
67
|
+
|
|
68
|
+
commands:
|
|
69
|
+
${ss(er)}
|
|
70
|
+
|
|
71
|
+
${ns}
|
|
72
|
+
run "mlis skill" for the agent guide
|
|
73
|
+
`}function na(e){return`usage: ${e.usage}
|
|
74
|
+
${e.summary}
|
|
75
|
+
${ns}
|
|
76
|
+
`}function it(e){let t=[];for(let r of e.slice(0,sa)){if(r.startsWith("-"))break;t.push(r)}return t}function oa(e){let t=it(e);for(let r=t.length;r>0;r--){let n=t.slice(0,r).join(" "),s=er.find((o)=>o.name===n);if(s)return{command:s,rest:e.slice(r)}}return null}function ia(e){let t=it(e);for(let r=t.length;r>0;r--){let n=t.slice(0,r).join(" "),s=er.filter((o)=>o.name.startsWith(`${n} `));if(s.length>0)return{name:n,commands:s}}return null}async function aa(e){let t=oa(e);if(!t){let s=e[0];if(!s||s==="-h"||s==="--help"||s==="help")return h(rs()),j.Ok;if(s==="-v"||s==="--version")return h(`${Qt}
|
|
77
|
+
`),j.Ok;let o=ia(e);if(o){let i=`mlis ${o.name} commands:
|
|
78
|
+
${ss(o.commands)}
|
|
79
|
+
|
|
80
|
+
mlis <command> --help shows the options of one command
|
|
81
|
+
`,c=it(e).join(" ")===o.name;if(c&&e.some((a)=>a==="-h"||a==="--help"))return h(i),j.Ok;return g(`${c?"missing subcommand":"unknown command"}: ${it(e).join(" ")}
|
|
82
|
+
`),g(i),j.Usage}return g(`unknown command: ${e.slice(0,2).join(" ")}
|
|
83
|
+
`),g(rs()),j.Usage}let{command:r,rest:n}=t;try{let s;try{s=ra({args:n,options:{...gr,...r.options},allowPositionals:!0,strict:!0})}catch(i){throw new u(`${i.message}
|
|
84
|
+
usage: ${r.usage}`)}if(s.values.help===!0)return h(na(r)),j.Ok;let o=await hr(s.values,s.positionals);return await r.run(o)??j.Ok}catch(s){let o=tr(s);return g(`${o===j.Guard?"guard":"error"}: ${s.message}`),o}}async function os(e=process.argv.slice(2)){let t=await Zn(Qt).catch(()=>null);if(t)g(t);process.exitCode=await aa(e)}var Qt,er,ns=`global: --url <site> --workdir <dir> --json --no-header -h/--help -v/--version
|
|
85
|
+
paths: ViewPage paths such as LHUENB4/@HomePage/default.ascx, or local paths inside the workspace
|
|
86
|
+
exit: 0 ok, 1 runtime error, 2 usage error, 3 guard blocked (conflict, missing --confirm-delete)`,sa=3;var is=b(()=>{at();A();x();O();Ke();xr();Vr();en();nn();dn();mn();yn();In();Gn();ts();me();Yt();Qt=Ae.version,er=[...rn,...Sr,...Ur,...cn,...un,...xn,...Rr,...wn,...Hr,...zn,...Qr,...es]});import{enableCompileCache as la}from"node:module";la?.();await Promise.resolve().then(() => is());await os();
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fui-org/mlis-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.6.0",
|
|
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
|
+
"keywords": ["mlis", "ascx", "viewpage", "cli", "bun", "ai"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/fui-org/mlis-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"mlis": "dist/mlis.js"
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist", "skills", "render", "README.md"],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"mlis": "bun src/bin.ts",
|
|
21
|
+
"build": "bun build src/bin.ts --target node --minify --outdir dist --entry-naming mlis.js",
|
|
22
|
+
"build:exe": "bun build src/bin.ts --compile --minify --outfile dist/mlis.exe",
|
|
23
|
+
"prepack": "bun run build",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/bun": "latest",
|
|
29
|
+
"typescript": "^5.6.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22"
|
|
33
|
+
}
|
|
34
|
+
}
|