@mangtre/cli 0.1.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.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `mang build` — run the app's Vite library build (in cwd) → `dist/app.js`, then auto-preflight it
3
+ * with the same gate the server uses. Resolves the app's locally-installed `vite` bin directly
4
+ * (package-manager agnostic — no npx/pnpm guessing).
5
+ */
6
+
7
+ import { spawnSync } from "node:child_process";
8
+ import { existsSync } from "node:fs";
9
+ import { readFile } from "node:fs/promises";
10
+ import { createRequire } from "node:module";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { checkBundle } from "../lib/check";
13
+ import { reportCheck } from "./check";
14
+
15
+ const OUTPUT = "dist/app.js";
16
+
17
+ /** Locate the app's installed `vite` bin, or null if Vite isn't a dependency yet. */
18
+ function resolveViteBin(cwd: string): string | null {
19
+ try {
20
+ const require = createRequire(resolve(cwd, "package.json"));
21
+ const pkgPath = require.resolve("vite/package.json");
22
+ return join(dirname(pkgPath), "bin", "vite.js");
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export async function runBuild(_argv: string[]): Promise<void> {
29
+ const cwd = process.cwd();
30
+ if (!existsSync(resolve(cwd, "vite.config.ts"))) {
31
+ console.error("Không thấy vite.config.ts trong thư mục này. Chạy trong thư mục mini-app.");
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+
36
+ const viteBin = resolveViteBin(cwd);
37
+ if (!viteBin) {
38
+ console.error("Không tìm thấy vite. Chạy `pnpm install` trong thư mục app trước.");
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+
43
+ console.log("🔨 Đang build (vite)…");
44
+ const res = spawnSync(process.execPath, [viteBin, "build"], { stdio: "inherit", cwd });
45
+ if (res.status !== 0) {
46
+ console.error("❌ Vite build thất bại.");
47
+ process.exitCode = res.status ?? 1;
48
+ return;
49
+ }
50
+
51
+ const out = resolve(cwd, OUTPUT);
52
+ if (!existsSync(out)) {
53
+ console.error(`Build xong nhưng không thấy ${OUTPUT}. Kiểm tra cấu hình mangLibConfig.`);
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ const source = await readFile(out, "utf8");
58
+ console.log("\n🔎 Preflight (chuẩn Măng):");
59
+ const result = checkBundle({ source });
60
+ reportCheck(result);
61
+ if (!result.passed) process.exitCode = 1;
62
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `mang check [bundlePath] [--app <id>]` — offline preflight of a built bundle against the shared
3
+ * "chuẩn Măng" gate. Exits non-zero on any `fail` finding. Default path: `dist/app.js`.
4
+ */
5
+
6
+ import { existsSync } from "node:fs";
7
+ import { readFile } from "node:fs/promises";
8
+ import { resolve } from "node:path";
9
+ import { type CheckResult, checkBundle } from "../lib/check";
10
+ import { getFlag } from "./args";
11
+
12
+ const DEFAULT_BUNDLE = "dist/app.js";
13
+
14
+ /** Print findings + a verdict; returns the result. */
15
+ export function reportCheck(result: CheckResult): void {
16
+ if (!result.manifestFound) {
17
+ console.error(
18
+ "⚠️ Không tìm thấy manifest (dòng `// mang-manifest:` ở đầu bundle). Chạy `mang build` trước.",
19
+ );
20
+ }
21
+ const fails = result.findings.filter((f) => f.severity === "fail");
22
+ const warns = result.findings.filter((f) => f.severity === "warn");
23
+ for (const f of fails) console.error(` ✖ [${f.check}] ${f.message}`);
24
+ for (const f of warns) console.warn(` ⚠ [${f.check}] ${f.message}`);
25
+ const kb = (result.bytes / 1024).toFixed(0);
26
+ if (result.passed) {
27
+ console.log(`✅ Đạt "chuẩn Măng" (${kb} KB · ${warns.length} cảnh báo). Sẵn sàng đăng.`);
28
+ } else {
29
+ console.error(`❌ Chưa đạt (${fails.length} lỗi · ${warns.length} cảnh báo · ${kb} KB).`);
30
+ }
31
+ }
32
+
33
+ export async function runCheck(argv: string[]): Promise<void> {
34
+ const path = argv.find((a) => !a.startsWith("-")) ?? DEFAULT_BUNDLE;
35
+ const abs = resolve(process.cwd(), path);
36
+ if (!existsSync(abs)) {
37
+ console.error(`Không thấy bundle: ${path}. Chạy \`mang build\` trước, hoặc truyền đường dẫn.`);
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ const source = await readFile(abs, "utf8");
42
+ const result = checkBundle({ source, appId: getFlag(argv, "app") });
43
+ reportCheck(result);
44
+ if (!result.passed) process.exitCode = 1;
45
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `mang login` — device-authorization flow. The CLI gets a short code, the user approves it in a
3
+ * browser where they're signed in, and the CLI is handed an opaque Bearer token (stored 0600).
4
+ */
5
+
6
+ import { spawn } from "node:child_process";
7
+ import { ApiError, apiFetch } from "../lib/api";
8
+ import { saveAuth } from "../lib/auth-store";
9
+ import { resolveApiBase } from "../lib/config";
10
+
11
+ interface DeviceStart {
12
+ deviceCode: string;
13
+ userCode: string;
14
+ verificationUriComplete: string;
15
+ interval: number;
16
+ expiresIn: number;
17
+ }
18
+ interface DevicePoll {
19
+ status: "pending" | "approved" | "denied" | "expired";
20
+ token?: string;
21
+ handle?: string;
22
+ }
23
+
24
+ const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
25
+
26
+ /** Best-effort: open an http(s) URL in the default browser (never throws). Guards the scheme so a
27
+ * malicious `--api` server can't return a URL with shell metacharacters (Windows `start`). */
28
+ function openBrowser(url: string): void {
29
+ try {
30
+ const parsed = new URL(url);
31
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return;
32
+ if (process.platform === "darwin") {
33
+ spawn("open", [parsed.href], { stdio: "ignore", detached: true }).unref();
34
+ } else if (process.platform === "win32") {
35
+ // `start "" <url>` with shell:false — the empty title arg prevents URL-as-command injection.
36
+ spawn("cmd", ["/c", "start", "", parsed.href], { stdio: "ignore", detached: true }).unref();
37
+ } else {
38
+ spawn("xdg-open", [parsed.href], { stdio: "ignore", detached: true }).unref();
39
+ }
40
+ } catch {
41
+ // ignore — the URL is printed for manual opening
42
+ }
43
+ }
44
+
45
+ export async function runLogin(argv: string[]): Promise<void> {
46
+ const base = resolveApiBase(argv);
47
+ let start: DeviceStart;
48
+ try {
49
+ start = await apiFetch<DeviceStart>("/v1/auth/device/start", { base, method: "POST" });
50
+ } catch (err) {
51
+ const is404 = err instanceof ApiError && err.status === 404;
52
+ console.error(`Không bắt đầu được đăng nhập: ${(err as Error).message}`);
53
+ if (is404) {
54
+ console.error(
55
+ ` API ${base} chưa hỗ trợ đăng nhập thiết bị (có thể chưa deploy Phase 2).\n Thử: mang login --api https://api-dev.mang.rumitx.com (hoặc export MANG_API=…)`,
56
+ );
57
+ }
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+
62
+ console.log("\n🔑 Đăng nhập Mọc");
63
+ console.log(` Mã của bạn: \x1b[1m${start.userCode}\x1b[0m`);
64
+ console.log(` Mở: ${start.verificationUriComplete}\n`);
65
+ openBrowser(start.verificationUriComplete);
66
+
67
+ const deadline = Date.now() + start.expiresIn * 1000;
68
+ const intervalMs = Math.max(2, start.interval) * 1000;
69
+ process.stdout.write(" Đang chờ duyệt");
70
+ while (Date.now() < deadline) {
71
+ await sleep(intervalMs);
72
+ process.stdout.write(".");
73
+ let poll: DevicePoll;
74
+ try {
75
+ poll = await apiFetch<DevicePoll>("/v1/auth/device/poll", {
76
+ base,
77
+ method: "POST",
78
+ body: { deviceCode: start.deviceCode },
79
+ });
80
+ } catch {
81
+ continue; // transient — keep polling until the deadline
82
+ }
83
+ if (poll.status === "approved" && poll.token) {
84
+ saveAuth({ token: poll.token, api: base, handle: poll.handle });
85
+ console.log(`\n✅ Đã đăng nhập${poll.handle ? ` (@${poll.handle})` : ""}.`);
86
+ return;
87
+ }
88
+ if (poll.status === "denied") {
89
+ console.error("\n❌ Yêu cầu bị từ chối.");
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ }
94
+ console.error("\n❌ Hết hạn. Chạy lại `mang login`.");
95
+ process.exitCode = 1;
96
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `mang new <slug> [--dir <path>] [--name <name>] [--icon <emoji>]` — scaffold a standard-compliant
3
+ * mini-app. Default target dir is `./<slug>`.
4
+ */
5
+
6
+ import { resolve } from "node:path";
7
+ import { APP_SLUG } from "@mangtre/standard";
8
+ import { scaffoldApp } from "../lib/scaffold";
9
+ import { getFlag, titleFromSlug } from "./args";
10
+
11
+ export async function runNew(argv: string[]): Promise<void> {
12
+ const slug = argv.find((a) => !a.startsWith("-"));
13
+ if (!slug) {
14
+ console.error("Thiếu slug. Dùng: mang new <slug>");
15
+ process.exitCode = 1;
16
+ return;
17
+ }
18
+ if (!APP_SLUG.test(slug)) {
19
+ console.error(`Slug không hợp lệ: "${slug}" — phải là chữ thường, 3–40 ký tự (a-z, 0-9, -).`);
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+
24
+ const name = getFlag(argv, "name") ?? titleFromSlug(slug);
25
+ const icon = getFlag(argv, "icon") ?? "🌱";
26
+ const dir = getFlag(argv, "dir") ?? slug;
27
+ const targetDir = resolve(process.cwd(), dir);
28
+
29
+ try {
30
+ const written = await scaffoldApp({ slug, name, icon, targetDir });
31
+ console.log(`✅ Tạo mini-app "${name}" (${slug}) tại ${dir}`);
32
+ console.log(` ${written.length} tệp.`);
33
+ console.log("\nTiếp theo:");
34
+ console.log(` cd ${dir} && pnpm install`);
35
+ console.log(" mang build # build → dist/app.js + tự check");
36
+ console.log(" mang check # preflight offline (giống cổng duyệt của server)");
37
+ } catch (err) {
38
+ console.error(err instanceof Error ? err.message : String(err));
39
+ process.exitCode = 1;
40
+ }
41
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `mang publish [bundle]` — preflight a built bundle, ensure the app exists, then upload it as a new
3
+ * version. Requires `mang login`. Default bundle: `dist/app.js`.
4
+ */
5
+
6
+ import { existsSync } from "node:fs";
7
+ import { readFile } from "node:fs/promises";
8
+ import { resolve } from "node:path";
9
+ import { ApiError, apiFetch } from "../lib/api";
10
+ import { loadAuth } from "../lib/auth-store";
11
+ import { checkBundle } from "../lib/check";
12
+ import { extractManifestComment } from "../lib/manifest-comment";
13
+ import { reportCheck } from "./check";
14
+
15
+ const DEFAULT_BUNDLE = "dist/app.js";
16
+
17
+ interface BundleManifest {
18
+ id: string;
19
+ name: string;
20
+ icon: string;
21
+ version: string;
22
+ [k: string]: unknown;
23
+ }
24
+
25
+ function asManifest(v: unknown): BundleManifest | null {
26
+ if (v && typeof v === "object" && !Array.isArray(v)) {
27
+ const m = v as Record<string, unknown>;
28
+ if (
29
+ typeof m.id === "string" &&
30
+ typeof m.name === "string" &&
31
+ typeof m.icon === "string" &&
32
+ typeof m.version === "string"
33
+ ) {
34
+ return m as BundleManifest;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+
40
+ export async function runPublish(argv: string[]): Promise<void> {
41
+ const auth = loadAuth();
42
+ if (!auth) {
43
+ console.error("Chưa đăng nhập. Chạy `mang login` trước.");
44
+ process.exitCode = 1;
45
+ return;
46
+ }
47
+
48
+ const path = argv.find((a) => !a.startsWith("-")) ?? DEFAULT_BUNDLE;
49
+ const abs = resolve(process.cwd(), path);
50
+ if (!existsSync(abs)) {
51
+ console.error(`Không thấy bundle: ${path}. Chạy \`mang build\` trước.`);
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+
56
+ const source = await readFile(abs, "utf8");
57
+ const manifest = asManifest(extractManifestComment(source));
58
+ if (!manifest) {
59
+ console.error("Bundle thiếu manifest hợp lệ (dòng `// mang-manifest:`). Chạy `mang build`.");
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+
64
+ // Local preflight first — never upload something that won't pass the server gate.
65
+ console.log("🔎 Preflight (chuẩn Măng):");
66
+ const result = checkBundle({ source, manifest });
67
+ reportCheck(result);
68
+ if (!result.passed) {
69
+ console.error("Sửa các lỗi trên rồi `mang publish` lại.");
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+
74
+ const base = auth.api;
75
+ // Ensure the app exists (claim the slug on first publish). 409 = already claimed.
76
+ try {
77
+ await apiFetch("/v1/apps", {
78
+ base,
79
+ token: auth.token,
80
+ body: { id: manifest.id, nameVi: manifest.name, icon: manifest.icon },
81
+ });
82
+ console.log(`📦 Đã tạo app "${manifest.id}".`);
83
+ } catch (err) {
84
+ if (!(err instanceof ApiError) || err.status !== 409) {
85
+ console.error(`Không tạo được app: ${(err as Error).message}`);
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ // 409 → slug exists. Confirm it's ours before uploading.
90
+ const mine = await apiFetch<{ apps: { id: string }[] }>("/v1/apps/mine", {
91
+ base,
92
+ token: auth.token,
93
+ }).catch(() => ({ apps: [] }));
94
+ if (!mine.apps.some((a) => a.id === manifest.id)) {
95
+ console.error(`Slug "${manifest.id}" đã thuộc về creator khác.`);
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+ }
100
+
101
+ // Upload the version (multipart).
102
+ const form = new FormData();
103
+ form.set("bundle", new Blob([source], { type: "text/javascript" }), "app.js");
104
+ form.set("version", manifest.version);
105
+ form.set("manifest", JSON.stringify(manifest));
106
+
107
+ try {
108
+ const res = await apiFetch<{ versionId: string; status: string }>(
109
+ `/v1/apps/${manifest.id}/versions`,
110
+ { base, token: auth.token, body: form },
111
+ );
112
+ console.log(`\n🚀 Đã đăng v${manifest.version} (${res.status}). Version: ${res.versionId}`);
113
+ console.log(" Theo dõi duyệt: `mang status`.");
114
+ } catch (err) {
115
+ console.error(`Đăng thất bại: ${(err as Error).message}`);
116
+ process.exitCode = 1;
117
+ }
118
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * `mang run` — build the mini-app and preview it locally in a browser with a MOCK `MangCore` sdk
3
+ * (localStorage-backed storage + a stock theme + locale), so a creator SEES their app render before
4
+ * publishing. Offline, no auth. Build-then-serve; edit code and re-run to refresh.
5
+ */
6
+
7
+ import { spawn, spawnSync } from "node:child_process";
8
+ import { existsSync } from "node:fs";
9
+ import { readFile, writeFile } from "node:fs/promises";
10
+ import { createServer } from "node:http";
11
+ import { createRequire } from "node:module";
12
+ import { dirname, join, resolve } from "node:path";
13
+
14
+ const FIRST_PORT = 5174;
15
+
16
+ /** Locate the app's installed `vite` bin (package-manager agnostic). */
17
+ function resolveViteBin(cwd: string): string | null {
18
+ try {
19
+ const require = createRequire(resolve(cwd, "package.json"));
20
+ return join(dirname(require.resolve("vite/package.json")), "bin", "vite.js");
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /** Open an http(s) URL in the default browser (never throws). */
27
+ function openBrowser(url: string): void {
28
+ try {
29
+ const u = new URL(url);
30
+ if (u.protocol !== "http:" && u.protocol !== "https:") return;
31
+ if (process.platform === "darwin")
32
+ spawn("open", [u.href], { stdio: "ignore", detached: true }).unref();
33
+ else if (process.platform === "win32")
34
+ spawn("cmd", ["/c", "start", "", u.href], { stdio: "ignore", detached: true }).unref();
35
+ else spawn("xdg-open", [u.href], { stdio: "ignore", detached: true }).unref();
36
+ } catch {
37
+ // ignore — URL is printed for manual opening
38
+ }
39
+ }
40
+
41
+ // A stock ThemeTokens for the preview (the real one comes from the shell at runtime).
42
+ const PREVIEW_THEME = JSON.stringify({
43
+ color: {
44
+ mangGreen: "#37B693",
45
+ shoot: "#8FE3B8",
46
+ bamboo: "#1F7A5C",
47
+ soil: "#B5793A",
48
+ sun: "#E8C547",
49
+ paper: "#F4F1E8",
50
+ dark: "#14201C",
51
+ },
52
+ font: { heading: "Quicksand", body: "Inter" },
53
+ });
54
+
55
+ /** The harness page: imports the built bundle, hands it a mock sdk, runs prepareDemo + mount. */
56
+ function harnessHtml(): string {
57
+ return `<!doctype html>
58
+ <html lang="vi"><head><meta charset="utf-8"/>
59
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
60
+ <title>Mọc preview</title><style>body{margin:0}</style></head>
61
+ <body><div id="root"></div>
62
+ <script type="module">
63
+ import { mount, prepareDemo } from "./app.js";
64
+ const NS = "mang:preview:";
65
+ const storage = {
66
+ get: async (k) => { const v = localStorage.getItem(NS + k); return v == null ? null : JSON.parse(v); },
67
+ set: async (k, val) => { localStorage.setItem(NS + k, JSON.stringify(val)); },
68
+ list: async (p = "") => Object.keys(localStorage).filter((k) => k.startsWith(NS + p)).map((k) => k.slice(NS.length)),
69
+ };
70
+ const sdk = { storage, theme: ${PREVIEW_THEME}, locale: "vi" };
71
+ try { if (prepareDemo) await prepareDemo(sdk); } catch (e) { console.warn("prepareDemo:", e); }
72
+ mount(document.getElementById("root"), sdk);
73
+ </script></body></html>`;
74
+ }
75
+
76
+ function contentType(file: string): string {
77
+ if (file.endsWith(".html")) return "text/html; charset=utf-8";
78
+ if (file.endsWith(".js")) return "text/javascript; charset=utf-8";
79
+ if (file.endsWith(".css")) return "text/css; charset=utf-8";
80
+ return "application/octet-stream";
81
+ }
82
+
83
+ export async function runRun(_argv: string[]): Promise<void> {
84
+ const cwd = process.cwd();
85
+ if (!existsSync(resolve(cwd, "vite.config.ts"))) {
86
+ console.error("Không thấy vite.config.ts. Chạy trong thư mục mini-app.");
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+ const viteBin = resolveViteBin(cwd);
91
+ if (!viteBin) {
92
+ console.error("Không tìm thấy vite. Chạy `pnpm install` trong thư mục app trước.");
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+
97
+ console.log("🔨 Đang build để xem trước…");
98
+ const res = spawnSync(process.execPath, [viteBin, "build"], { stdio: "inherit", cwd });
99
+ if (res.status !== 0) {
100
+ console.error("❌ Build thất bại.");
101
+ process.exitCode = res.status ?? 1;
102
+ return;
103
+ }
104
+
105
+ const distDir = resolve(cwd, "dist");
106
+ if (!existsSync(join(distDir, "app.js"))) {
107
+ console.error("Build xong nhưng không thấy dist/app.js.");
108
+ process.exitCode = 1;
109
+ return;
110
+ }
111
+ await writeFile(join(distDir, "index.html"), harnessHtml(), "utf8");
112
+
113
+ const server = createServer(async (req, res) => {
114
+ const path = (req.url ?? "/").split("?")[0] ?? "/";
115
+ const file = path === "/" ? "index.html" : path.replace(/^\/+/, "");
116
+ try {
117
+ const buf = await readFile(join(distDir, file));
118
+ res.setHeader("Content-Type", contentType(file));
119
+ res.end(buf);
120
+ } catch {
121
+ res.statusCode = 404;
122
+ res.end("not found");
123
+ }
124
+ });
125
+
126
+ let port = FIRST_PORT;
127
+ server.on("error", (err: NodeJS.ErrnoException) => {
128
+ if (err.code === "EADDRINUSE" && port < FIRST_PORT + 10) {
129
+ port += 1;
130
+ server.listen(port);
131
+ } else {
132
+ console.error(`Không mở được server: ${err.message}`);
133
+ process.exitCode = 1;
134
+ }
135
+ });
136
+ server.listen(port, () => {
137
+ const url = `http://localhost:${port}/`;
138
+ console.log(`\n🌱 Xem trước: ${url}`);
139
+ console.log(" sdk = storage(local) + theme + locale giả lập · Ctrl+C để dừng.");
140
+ console.log(" Sửa code → chạy lại `mang run` để build + xem lại.");
141
+ openBrowser(url);
142
+ });
143
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `mang status` — list my apps and each one's latest version review state (live / pending_review /
3
+ * rejected …) with finding counts + the rejection reason. Requires `mang login`.
4
+ */
5
+
6
+ import { apiFetch } from "../lib/api";
7
+ import { loadAuth } from "../lib/auth-store";
8
+
9
+ interface VersionInfo {
10
+ version: string;
11
+ status: string;
12
+ findings?: { severity: "fail" | "warn" }[];
13
+ reason?: string | null;
14
+ }
15
+ interface AppInfo {
16
+ id: string;
17
+ nameVi: string;
18
+ icon: string;
19
+ status: string;
20
+ versions: VersionInfo[];
21
+ }
22
+
23
+ const STATUS_ICON: Record<string, string> = {
24
+ live: "🟢",
25
+ approved: "🟢",
26
+ pending_review: "🟡",
27
+ validating: "🟡",
28
+ in_review: "🟡",
29
+ rejected: "🔴",
30
+ draft: "⚪",
31
+ delisted: "⚫",
32
+ };
33
+
34
+ export async function runStatus(_argv: string[]): Promise<void> {
35
+ const auth = loadAuth();
36
+ if (!auth) {
37
+ console.error("Chưa đăng nhập. Chạy `mang login` trước.");
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+
42
+ let data: { apps: AppInfo[] };
43
+ try {
44
+ data = await apiFetch<{ apps: AppInfo[] }>("/v1/apps/mine", {
45
+ base: auth.api,
46
+ token: auth.token,
47
+ });
48
+ } catch (err) {
49
+ console.error(`Không lấy được trạng thái: ${(err as Error).message}`);
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+
54
+ if (data.apps.length === 0) {
55
+ console.log("Chưa có app nào. Tạo bằng `mang new`, rồi `mang publish`.");
56
+ return;
57
+ }
58
+
59
+ for (const app of data.apps) {
60
+ const latest = app.versions[0];
61
+ const icon = STATUS_ICON[app.status] ?? "•";
62
+ console.log(`\n${icon} ${app.icon} ${app.nameVi} (${app.id}) — ${app.status}`);
63
+ if (latest) {
64
+ const fails = latest.findings?.filter((f) => f.severity === "fail").length ?? 0;
65
+ const warns = latest.findings?.filter((f) => f.severity === "warn").length ?? 0;
66
+ console.log(` v${latest.version}: ${latest.status} · ${fails} lỗi · ${warns} cảnh báo`);
67
+ if (latest.reason) console.log(` Lý do: ${latest.reason}`);
68
+ }
69
+ }
70
+ }
package/src/index.ts ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `mang` — the Mọc CLI. The vibe-coder's path from idea → product → production: scaffold a
4
+ * standard-compliant mini-app, build it to one ESM, and preflight it against the SAME "chuẩn Măng"
5
+ * gate the server runs. (Auth + publish/status land in a later Mọc phase.)
6
+ * Canonical: `sot/Mang_Creator_Toolchain_Moc_v0.1.md`.
7
+ */
8
+
9
+ import { runBuild } from "./commands/build";
10
+ import { runCheck } from "./commands/check";
11
+ import { runLogin } from "./commands/login";
12
+ import { runNew } from "./commands/new";
13
+ import { runPublish } from "./commands/publish";
14
+ import { runRun } from "./commands/run";
15
+ import { runStatus } from "./commands/status";
16
+
17
+ const VERSION = "0.1.0";
18
+
19
+ function printHelp(): void {
20
+ console.log(`Mọc — bộ công cụ creator của Măng (CLI: mang v${VERSION})
21
+
22
+ Cách dùng:
23
+ mang new <slug> [--dir <path>] [--name <tên>] [--icon <emoji>]
24
+ Tạo một mini-app mới đúng "chuẩn Măng" (React + Vite + @mangtre/ui).
25
+ mang build
26
+ Build mini-app trong thư mục hiện tại → dist/app.js, rồi tự preflight.
27
+ mang run
28
+ Build + xem trước app ngay trên trình duyệt (sdk giả lập) — không cần đăng.
29
+ mang check [bundlePath] [--app <id>]
30
+ Preflight offline một bundle đã build (giống cổng duyệt của server).
31
+ mang login [--api <url>]
32
+ Đăng nhập (device flow) — duyệt mã trên trình duyệt, lưu token cục bộ.
33
+ mang publish [bundlePath]
34
+ Preflight + đăng bundle lên Măng (tạo app nếu chưa có). Cần đăng nhập.
35
+ mang status
36
+ Xem app của bạn + trạng thái duyệt mỗi version. Cần đăng nhập.
37
+
38
+ mang help | --version`);
39
+ }
40
+
41
+ async function main(): Promise<void> {
42
+ const [cmd, ...rest] = process.argv.slice(2);
43
+ switch (cmd) {
44
+ case "new":
45
+ return runNew(rest);
46
+ case "check":
47
+ return runCheck(rest);
48
+ case "build":
49
+ return runBuild(rest);
50
+ case "run":
51
+ return runRun(rest);
52
+ case "login":
53
+ return runLogin(rest);
54
+ case "publish":
55
+ return runPublish(rest);
56
+ case "status":
57
+ return runStatus(rest);
58
+ case "-v":
59
+ case "--version":
60
+ console.log(VERSION);
61
+ return;
62
+ case undefined:
63
+ case "help":
64
+ case "-h":
65
+ case "--help":
66
+ printHelp();
67
+ return;
68
+ default:
69
+ console.error(`Lệnh không rõ: ${cmd}\n`);
70
+ printHelp();
71
+ process.exitCode = 1;
72
+ }
73
+ }
74
+
75
+ main().catch((err) => {
76
+ console.error(err instanceof Error ? err.message : String(err));
77
+ process.exitCode = 1;
78
+ });