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