@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.
- package/LICENSE +26 -0
- package/dist/chunk-2ZQJSRHJ.js +380 -0
- package/dist/chunk-MLWT65G7.js +17 -0
- package/dist/index.js +524 -0
- package/dist/lib.js +28 -0
- package/dist/vite.js +36 -0
- package/package.json +40 -0
- package/src/commands/args.ts +22 -0
- package/src/commands/build.ts +62 -0
- package/src/commands/check.ts +45 -0
- package/src/commands/login.ts +96 -0
- package/src/commands/new.ts +41 -0
- package/src/commands/publish.ts +118 -0
- package/src/commands/run.ts +143 -0
- package/src/commands/status.ts +70 -0
- package/src/index.ts +78 -0
- package/src/lib/api.ts +61 -0
- package/src/lib/auth-store.ts +42 -0
- package/src/lib/check.test.ts +57 -0
- package/src/lib/check.ts +49 -0
- package/src/lib/config.ts +27 -0
- package/src/lib/index.ts +12 -0
- package/src/lib/manifest-comment.ts +23 -0
- package/src/lib/scaffold.ts +56 -0
- package/src/vite.ts +73 -0
- package/templates/app/README.md.tmpl +28 -0
- package/templates/app/package.json.tmpl +30 -0
- package/templates/app/src/App.tsx.tmpl +48 -0
- package/templates/app/src/index.tsx.tmpl +40 -0
- package/templates/app/src/manifest.ts.tmpl +21 -0
- package/templates/app/src/sample.ts.tmpl +12 -0
- package/templates/app/tsconfig.json.tmpl +8 -0
- package/templates/app/vite.config.ts.tmpl +11 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
APP_SLUG,
|
|
4
|
+
ApiError,
|
|
5
|
+
apiFetch,
|
|
6
|
+
checkBundle,
|
|
7
|
+
getFlag,
|
|
8
|
+
loadAuth,
|
|
9
|
+
resolveApiBase,
|
|
10
|
+
saveAuth,
|
|
11
|
+
scaffoldApp,
|
|
12
|
+
titleFromSlug
|
|
13
|
+
} from "./chunk-2ZQJSRHJ.js";
|
|
14
|
+
import {
|
|
15
|
+
extractManifestComment
|
|
16
|
+
} from "./chunk-MLWT65G7.js";
|
|
17
|
+
|
|
18
|
+
// src/commands/build.ts
|
|
19
|
+
import { spawnSync } from "child_process";
|
|
20
|
+
import { existsSync as existsSync2 } from "fs";
|
|
21
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
22
|
+
import { createRequire } from "module";
|
|
23
|
+
import { dirname, join, resolve as resolve2 } from "path";
|
|
24
|
+
|
|
25
|
+
// src/commands/check.ts
|
|
26
|
+
import { existsSync } from "fs";
|
|
27
|
+
import { readFile } from "fs/promises";
|
|
28
|
+
import { resolve } from "path";
|
|
29
|
+
var DEFAULT_BUNDLE = "dist/app.js";
|
|
30
|
+
function reportCheck(result) {
|
|
31
|
+
if (!result.manifestFound) {
|
|
32
|
+
console.error(
|
|
33
|
+
"\u26A0\uFE0F Kh\xF4ng t\xECm th\u1EA5y manifest (d\xF2ng `// mang-manifest:` \u1EDF \u0111\u1EA7u bundle). Ch\u1EA1y `mang build` tr\u01B0\u1EDBc."
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const fails = result.findings.filter((f) => f.severity === "fail");
|
|
37
|
+
const warns = result.findings.filter((f) => f.severity === "warn");
|
|
38
|
+
for (const f of fails) console.error(` \u2716 [${f.check}] ${f.message}`);
|
|
39
|
+
for (const f of warns) console.warn(` \u26A0 [${f.check}] ${f.message}`);
|
|
40
|
+
const kb = (result.bytes / 1024).toFixed(0);
|
|
41
|
+
if (result.passed) {
|
|
42
|
+
console.log(`\u2705 \u0110\u1EA1t "chu\u1EA9n M\u0103ng" (${kb} KB \xB7 ${warns.length} c\u1EA3nh b\xE1o). S\u1EB5n s\xE0ng \u0111\u0103ng.`);
|
|
43
|
+
} else {
|
|
44
|
+
console.error(`\u274C Ch\u01B0a \u0111\u1EA1t (${fails.length} l\u1ED7i \xB7 ${warns.length} c\u1EA3nh b\xE1o \xB7 ${kb} KB).`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function runCheck(argv) {
|
|
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\xF4ng th\u1EA5y bundle: ${path}. Ch\u1EA1y \`mang build\` tr\u01B0\u1EDBc, ho\u1EB7c truy\u1EC1n \u0111\u01B0\u1EDDng d\u1EABn.`);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const source = await readFile(abs, "utf8");
|
|
56
|
+
const result = checkBundle({ source, appId: getFlag(argv, "app") });
|
|
57
|
+
reportCheck(result);
|
|
58
|
+
if (!result.passed) process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/commands/build.ts
|
|
62
|
+
var OUTPUT = "dist/app.js";
|
|
63
|
+
function resolveViteBin(cwd) {
|
|
64
|
+
try {
|
|
65
|
+
const require2 = createRequire(resolve2(cwd, "package.json"));
|
|
66
|
+
const pkgPath = require2.resolve("vite/package.json");
|
|
67
|
+
return join(dirname(pkgPath), "bin", "vite.js");
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function runBuild(_argv) {
|
|
73
|
+
const cwd = process.cwd();
|
|
74
|
+
if (!existsSync2(resolve2(cwd, "vite.config.ts"))) {
|
|
75
|
+
console.error("Kh\xF4ng th\u1EA5y vite.config.ts trong th\u01B0 m\u1EE5c n\xE0y. Ch\u1EA1y trong th\u01B0 m\u1EE5c mini-app.");
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const viteBin = resolveViteBin(cwd);
|
|
80
|
+
if (!viteBin) {
|
|
81
|
+
console.error("Kh\xF4ng t\xECm th\u1EA5y vite. Ch\u1EA1y `pnpm install` trong th\u01B0 m\u1EE5c app tr\u01B0\u1EDBc.");
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log("\u{1F528} \u0110ang build (vite)\u2026");
|
|
86
|
+
const res = spawnSync(process.execPath, [viteBin, "build"], { stdio: "inherit", cwd });
|
|
87
|
+
if (res.status !== 0) {
|
|
88
|
+
console.error("\u274C Vite build th\u1EA5t b\u1EA1i.");
|
|
89
|
+
process.exitCode = res.status ?? 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const out = resolve2(cwd, OUTPUT);
|
|
93
|
+
if (!existsSync2(out)) {
|
|
94
|
+
console.error(`Build xong nh\u01B0ng kh\xF4ng th\u1EA5y ${OUTPUT}. Ki\u1EC3m tra c\u1EA5u h\xECnh mangLibConfig.`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const source = await readFile2(out, "utf8");
|
|
99
|
+
console.log("\n\u{1F50E} Preflight (chu\u1EA9n M\u0103ng):");
|
|
100
|
+
const result = checkBundle({ source });
|
|
101
|
+
reportCheck(result);
|
|
102
|
+
if (!result.passed) process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/commands/login.ts
|
|
106
|
+
import { spawn } from "child_process";
|
|
107
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
108
|
+
function openBrowser(url) {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = new URL(url);
|
|
111
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return;
|
|
112
|
+
if (process.platform === "darwin") {
|
|
113
|
+
spawn("open", [parsed.href], { stdio: "ignore", detached: true }).unref();
|
|
114
|
+
} else if (process.platform === "win32") {
|
|
115
|
+
spawn("cmd", ["/c", "start", "", parsed.href], { stdio: "ignore", detached: true }).unref();
|
|
116
|
+
} else {
|
|
117
|
+
spawn("xdg-open", [parsed.href], { stdio: "ignore", detached: true }).unref();
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function runLogin(argv) {
|
|
123
|
+
const base = resolveApiBase(argv);
|
|
124
|
+
let start;
|
|
125
|
+
try {
|
|
126
|
+
start = await apiFetch("/v1/auth/device/start", { base, method: "POST" });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
const is404 = err instanceof ApiError && err.status === 404;
|
|
129
|
+
console.error(`Kh\xF4ng b\u1EAFt \u0111\u1EA7u \u0111\u01B0\u1EE3c \u0111\u0103ng nh\u1EADp: ${err.message}`);
|
|
130
|
+
if (is404) {
|
|
131
|
+
console.error(
|
|
132
|
+
` API ${base} ch\u01B0a h\u1ED7 tr\u1EE3 \u0111\u0103ng nh\u1EADp thi\u1EBFt b\u1ECB (c\xF3 th\u1EC3 ch\u01B0a deploy Phase 2).
|
|
133
|
+
Th\u1EED: mang login --api https://api-dev.mang.rumitx.com (ho\u1EB7c export MANG_API=\u2026)`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
console.log("\n\u{1F511} \u0110\u0103ng nh\u1EADp M\u1ECDc");
|
|
140
|
+
console.log(` M\xE3 c\u1EE7a b\u1EA1n: \x1B[1m${start.userCode}\x1B[0m`);
|
|
141
|
+
console.log(` M\u1EDF: ${start.verificationUriComplete}
|
|
142
|
+
`);
|
|
143
|
+
openBrowser(start.verificationUriComplete);
|
|
144
|
+
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
145
|
+
const intervalMs = Math.max(2, start.interval) * 1e3;
|
|
146
|
+
process.stdout.write(" \u0110ang ch\u1EDD duy\u1EC7t");
|
|
147
|
+
while (Date.now() < deadline) {
|
|
148
|
+
await sleep(intervalMs);
|
|
149
|
+
process.stdout.write(".");
|
|
150
|
+
let poll;
|
|
151
|
+
try {
|
|
152
|
+
poll = await apiFetch("/v1/auth/device/poll", {
|
|
153
|
+
base,
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: { deviceCode: start.deviceCode }
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (poll.status === "approved" && poll.token) {
|
|
161
|
+
saveAuth({ token: poll.token, api: base, handle: poll.handle });
|
|
162
|
+
console.log(`
|
|
163
|
+
\u2705 \u0110\xE3 \u0111\u0103ng nh\u1EADp${poll.handle ? ` (@${poll.handle})` : ""}.`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (poll.status === "denied") {
|
|
167
|
+
console.error("\n\u274C Y\xEAu c\u1EA7u b\u1ECB t\u1EEB ch\u1ED1i.");
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
console.error("\n\u274C H\u1EBFt h\u1EA1n. Ch\u1EA1y l\u1EA1i `mang login`.");
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/commands/new.ts
|
|
177
|
+
import { resolve as resolve3 } from "path";
|
|
178
|
+
async function runNew(argv) {
|
|
179
|
+
const slug = argv.find((a) => !a.startsWith("-"));
|
|
180
|
+
if (!slug) {
|
|
181
|
+
console.error("Thi\u1EBFu slug. D\xF9ng: mang new <slug>");
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!APP_SLUG.test(slug)) {
|
|
186
|
+
console.error(`Slug kh\xF4ng h\u1EE3p l\u1EC7: "${slug}" \u2014 ph\u1EA3i l\xE0 ch\u1EEF th\u01B0\u1EDDng, 3\u201340 k\xFD t\u1EF1 (a-z, 0-9, -).`);
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const name = getFlag(argv, "name") ?? titleFromSlug(slug);
|
|
191
|
+
const icon = getFlag(argv, "icon") ?? "\u{1F331}";
|
|
192
|
+
const dir = getFlag(argv, "dir") ?? slug;
|
|
193
|
+
const targetDir = resolve3(process.cwd(), dir);
|
|
194
|
+
try {
|
|
195
|
+
const written = await scaffoldApp({ slug, name, icon, targetDir });
|
|
196
|
+
console.log(`\u2705 T\u1EA1o mini-app "${name}" (${slug}) t\u1EA1i ${dir}`);
|
|
197
|
+
console.log(` ${written.length} t\u1EC7p.`);
|
|
198
|
+
console.log("\nTi\u1EBFp theo:");
|
|
199
|
+
console.log(` cd ${dir} && pnpm install`);
|
|
200
|
+
console.log(" mang build # build \u2192 dist/app.js + t\u1EF1 check");
|
|
201
|
+
console.log(" mang check # preflight offline (gi\u1ED1ng c\u1ED5ng duy\u1EC7t c\u1EE7a server)");
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/commands/publish.ts
|
|
209
|
+
import { existsSync as existsSync3 } from "fs";
|
|
210
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
211
|
+
import { resolve as resolve4 } from "path";
|
|
212
|
+
var DEFAULT_BUNDLE2 = "dist/app.js";
|
|
213
|
+
function asManifest(v) {
|
|
214
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
215
|
+
const m = v;
|
|
216
|
+
if (typeof m.id === "string" && typeof m.name === "string" && typeof m.icon === "string" && typeof m.version === "string") {
|
|
217
|
+
return m;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
async function runPublish(argv) {
|
|
223
|
+
const auth = loadAuth();
|
|
224
|
+
if (!auth) {
|
|
225
|
+
console.error("Ch\u01B0a \u0111\u0103ng nh\u1EADp. Ch\u1EA1y `mang login` tr\u01B0\u1EDBc.");
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const path = argv.find((a) => !a.startsWith("-")) ?? DEFAULT_BUNDLE2;
|
|
230
|
+
const abs = resolve4(process.cwd(), path);
|
|
231
|
+
if (!existsSync3(abs)) {
|
|
232
|
+
console.error(`Kh\xF4ng th\u1EA5y bundle: ${path}. Ch\u1EA1y \`mang build\` tr\u01B0\u1EDBc.`);
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const source = await readFile3(abs, "utf8");
|
|
237
|
+
const manifest = asManifest(extractManifestComment(source));
|
|
238
|
+
if (!manifest) {
|
|
239
|
+
console.error("Bundle thi\u1EBFu manifest h\u1EE3p l\u1EC7 (d\xF2ng `// mang-manifest:`). Ch\u1EA1y `mang build`.");
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
console.log("\u{1F50E} Preflight (chu\u1EA9n M\u0103ng):");
|
|
244
|
+
const result = checkBundle({ source, manifest });
|
|
245
|
+
reportCheck(result);
|
|
246
|
+
if (!result.passed) {
|
|
247
|
+
console.error("S\u1EEDa c\xE1c l\u1ED7i tr\xEAn r\u1ED3i `mang publish` l\u1EA1i.");
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const base = auth.api;
|
|
252
|
+
try {
|
|
253
|
+
await apiFetch("/v1/apps", {
|
|
254
|
+
base,
|
|
255
|
+
token: auth.token,
|
|
256
|
+
body: { id: manifest.id, nameVi: manifest.name, icon: manifest.icon }
|
|
257
|
+
});
|
|
258
|
+
console.log(`\u{1F4E6} \u0110\xE3 t\u1EA1o app "${manifest.id}".`);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (!(err instanceof ApiError) || err.status !== 409) {
|
|
261
|
+
console.error(`Kh\xF4ng t\u1EA1o \u0111\u01B0\u1EE3c app: ${err.message}`);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const mine = await apiFetch("/v1/apps/mine", {
|
|
266
|
+
base,
|
|
267
|
+
token: auth.token
|
|
268
|
+
}).catch(() => ({ apps: [] }));
|
|
269
|
+
if (!mine.apps.some((a) => a.id === manifest.id)) {
|
|
270
|
+
console.error(`Slug "${manifest.id}" \u0111\xE3 thu\u1ED9c v\u1EC1 creator kh\xE1c.`);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const form = new FormData();
|
|
276
|
+
form.set("bundle", new Blob([source], { type: "text/javascript" }), "app.js");
|
|
277
|
+
form.set("version", manifest.version);
|
|
278
|
+
form.set("manifest", JSON.stringify(manifest));
|
|
279
|
+
try {
|
|
280
|
+
const res = await apiFetch(
|
|
281
|
+
`/v1/apps/${manifest.id}/versions`,
|
|
282
|
+
{ base, token: auth.token, body: form }
|
|
283
|
+
);
|
|
284
|
+
console.log(`
|
|
285
|
+
\u{1F680} \u0110\xE3 \u0111\u0103ng v${manifest.version} (${res.status}). Version: ${res.versionId}`);
|
|
286
|
+
console.log(" Theo d\xF5i duy\u1EC7t: `mang status`.");
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.error(`\u0110\u0103ng th\u1EA5t b\u1EA1i: ${err.message}`);
|
|
289
|
+
process.exitCode = 1;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/commands/run.ts
|
|
294
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
295
|
+
import { existsSync as existsSync4 } from "fs";
|
|
296
|
+
import { readFile as readFile4, writeFile } from "fs/promises";
|
|
297
|
+
import { createServer } from "http";
|
|
298
|
+
import { createRequire as createRequire2 } from "module";
|
|
299
|
+
import { dirname as dirname2, join as join2, resolve as resolve5 } from "path";
|
|
300
|
+
var FIRST_PORT = 5174;
|
|
301
|
+
function resolveViteBin2(cwd) {
|
|
302
|
+
try {
|
|
303
|
+
const require2 = createRequire2(resolve5(cwd, "package.json"));
|
|
304
|
+
return join2(dirname2(require2.resolve("vite/package.json")), "bin", "vite.js");
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function openBrowser2(url) {
|
|
310
|
+
try {
|
|
311
|
+
const u = new URL(url);
|
|
312
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return;
|
|
313
|
+
if (process.platform === "darwin")
|
|
314
|
+
spawn2("open", [u.href], { stdio: "ignore", detached: true }).unref();
|
|
315
|
+
else if (process.platform === "win32")
|
|
316
|
+
spawn2("cmd", ["/c", "start", "", u.href], { stdio: "ignore", detached: true }).unref();
|
|
317
|
+
else spawn2("xdg-open", [u.href], { stdio: "ignore", detached: true }).unref();
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
var PREVIEW_THEME = JSON.stringify({
|
|
322
|
+
color: {
|
|
323
|
+
mangGreen: "#37B693",
|
|
324
|
+
shoot: "#8FE3B8",
|
|
325
|
+
bamboo: "#1F7A5C",
|
|
326
|
+
soil: "#B5793A",
|
|
327
|
+
sun: "#E8C547",
|
|
328
|
+
paper: "#F4F1E8",
|
|
329
|
+
dark: "#14201C"
|
|
330
|
+
},
|
|
331
|
+
font: { heading: "Quicksand", body: "Inter" }
|
|
332
|
+
});
|
|
333
|
+
function harnessHtml() {
|
|
334
|
+
return `<!doctype html>
|
|
335
|
+
<html lang="vi"><head><meta charset="utf-8"/>
|
|
336
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
337
|
+
<title>M\u1ECDc preview</title><style>body{margin:0}</style></head>
|
|
338
|
+
<body><div id="root"></div>
|
|
339
|
+
<script type="module">
|
|
340
|
+
import { mount, prepareDemo } from "./app.js";
|
|
341
|
+
const NS = "mang:preview:";
|
|
342
|
+
const storage = {
|
|
343
|
+
get: async (k) => { const v = localStorage.getItem(NS + k); return v == null ? null : JSON.parse(v); },
|
|
344
|
+
set: async (k, val) => { localStorage.setItem(NS + k, JSON.stringify(val)); },
|
|
345
|
+
list: async (p = "") => Object.keys(localStorage).filter((k) => k.startsWith(NS + p)).map((k) => k.slice(NS.length)),
|
|
346
|
+
};
|
|
347
|
+
const sdk = { storage, theme: ${PREVIEW_THEME}, locale: "vi" };
|
|
348
|
+
try { if (prepareDemo) await prepareDemo(sdk); } catch (e) { console.warn("prepareDemo:", e); }
|
|
349
|
+
mount(document.getElementById("root"), sdk);
|
|
350
|
+
</script></body></html>`;
|
|
351
|
+
}
|
|
352
|
+
function contentType(file) {
|
|
353
|
+
if (file.endsWith(".html")) return "text/html; charset=utf-8";
|
|
354
|
+
if (file.endsWith(".js")) return "text/javascript; charset=utf-8";
|
|
355
|
+
if (file.endsWith(".css")) return "text/css; charset=utf-8";
|
|
356
|
+
return "application/octet-stream";
|
|
357
|
+
}
|
|
358
|
+
async function runRun(_argv) {
|
|
359
|
+
const cwd = process.cwd();
|
|
360
|
+
if (!existsSync4(resolve5(cwd, "vite.config.ts"))) {
|
|
361
|
+
console.error("Kh\xF4ng th\u1EA5y vite.config.ts. Ch\u1EA1y trong th\u01B0 m\u1EE5c mini-app.");
|
|
362
|
+
process.exitCode = 1;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const viteBin = resolveViteBin2(cwd);
|
|
366
|
+
if (!viteBin) {
|
|
367
|
+
console.error("Kh\xF4ng t\xECm th\u1EA5y vite. Ch\u1EA1y `pnpm install` trong th\u01B0 m\u1EE5c app tr\u01B0\u1EDBc.");
|
|
368
|
+
process.exitCode = 1;
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
console.log("\u{1F528} \u0110ang build \u0111\u1EC3 xem tr\u01B0\u1EDBc\u2026");
|
|
372
|
+
const res = spawnSync2(process.execPath, [viteBin, "build"], { stdio: "inherit", cwd });
|
|
373
|
+
if (res.status !== 0) {
|
|
374
|
+
console.error("\u274C Build th\u1EA5t b\u1EA1i.");
|
|
375
|
+
process.exitCode = res.status ?? 1;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const distDir = resolve5(cwd, "dist");
|
|
379
|
+
if (!existsSync4(join2(distDir, "app.js"))) {
|
|
380
|
+
console.error("Build xong nh\u01B0ng kh\xF4ng th\u1EA5y dist/app.js.");
|
|
381
|
+
process.exitCode = 1;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
await writeFile(join2(distDir, "index.html"), harnessHtml(), "utf8");
|
|
385
|
+
const server = createServer(async (req, res2) => {
|
|
386
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
387
|
+
const file = path === "/" ? "index.html" : path.replace(/^\/+/, "");
|
|
388
|
+
try {
|
|
389
|
+
const buf = await readFile4(join2(distDir, file));
|
|
390
|
+
res2.setHeader("Content-Type", contentType(file));
|
|
391
|
+
res2.end(buf);
|
|
392
|
+
} catch {
|
|
393
|
+
res2.statusCode = 404;
|
|
394
|
+
res2.end("not found");
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
let port = FIRST_PORT;
|
|
398
|
+
server.on("error", (err) => {
|
|
399
|
+
if (err.code === "EADDRINUSE" && port < FIRST_PORT + 10) {
|
|
400
|
+
port += 1;
|
|
401
|
+
server.listen(port);
|
|
402
|
+
} else {
|
|
403
|
+
console.error(`Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c server: ${err.message}`);
|
|
404
|
+
process.exitCode = 1;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
server.listen(port, () => {
|
|
408
|
+
const url = `http://localhost:${port}/`;
|
|
409
|
+
console.log(`
|
|
410
|
+
\u{1F331} Xem tr\u01B0\u1EDBc: ${url}`);
|
|
411
|
+
console.log(" sdk = storage(local) + theme + locale gi\u1EA3 l\u1EADp \xB7 Ctrl+C \u0111\u1EC3 d\u1EEBng.");
|
|
412
|
+
console.log(" S\u1EEDa code \u2192 ch\u1EA1y l\u1EA1i `mang run` \u0111\u1EC3 build + xem l\u1EA1i.");
|
|
413
|
+
openBrowser2(url);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/commands/status.ts
|
|
418
|
+
var STATUS_ICON = {
|
|
419
|
+
live: "\u{1F7E2}",
|
|
420
|
+
approved: "\u{1F7E2}",
|
|
421
|
+
pending_review: "\u{1F7E1}",
|
|
422
|
+
validating: "\u{1F7E1}",
|
|
423
|
+
in_review: "\u{1F7E1}",
|
|
424
|
+
rejected: "\u{1F534}",
|
|
425
|
+
draft: "\u26AA",
|
|
426
|
+
delisted: "\u26AB"
|
|
427
|
+
};
|
|
428
|
+
async function runStatus(_argv) {
|
|
429
|
+
const auth = loadAuth();
|
|
430
|
+
if (!auth) {
|
|
431
|
+
console.error("Ch\u01B0a \u0111\u0103ng nh\u1EADp. Ch\u1EA1y `mang login` tr\u01B0\u1EDBc.");
|
|
432
|
+
process.exitCode = 1;
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
let data;
|
|
436
|
+
try {
|
|
437
|
+
data = await apiFetch("/v1/apps/mine", {
|
|
438
|
+
base: auth.api,
|
|
439
|
+
token: auth.token
|
|
440
|
+
});
|
|
441
|
+
} catch (err) {
|
|
442
|
+
console.error(`Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c tr\u1EA1ng th\xE1i: ${err.message}`);
|
|
443
|
+
process.exitCode = 1;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (data.apps.length === 0) {
|
|
447
|
+
console.log("Ch\u01B0a c\xF3 app n\xE0o. T\u1EA1o b\u1EB1ng `mang new`, r\u1ED3i `mang publish`.");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
for (const app of data.apps) {
|
|
451
|
+
const latest = app.versions[0];
|
|
452
|
+
const icon = STATUS_ICON[app.status] ?? "\u2022";
|
|
453
|
+
console.log(`
|
|
454
|
+
${icon} ${app.icon} ${app.nameVi} (${app.id}) \u2014 ${app.status}`);
|
|
455
|
+
if (latest) {
|
|
456
|
+
const fails = latest.findings?.filter((f) => f.severity === "fail").length ?? 0;
|
|
457
|
+
const warns = latest.findings?.filter((f) => f.severity === "warn").length ?? 0;
|
|
458
|
+
console.log(` v${latest.version}: ${latest.status} \xB7 ${fails} l\u1ED7i \xB7 ${warns} c\u1EA3nh b\xE1o`);
|
|
459
|
+
if (latest.reason) console.log(` L\xFD do: ${latest.reason}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/index.ts
|
|
465
|
+
var VERSION = "0.1.0";
|
|
466
|
+
function printHelp() {
|
|
467
|
+
console.log(`M\u1ECDc \u2014 b\u1ED9 c\xF4ng c\u1EE5 creator c\u1EE7a M\u0103ng (CLI: mang v${VERSION})
|
|
468
|
+
|
|
469
|
+
C\xE1ch d\xF9ng:
|
|
470
|
+
mang new <slug> [--dir <path>] [--name <t\xEAn>] [--icon <emoji>]
|
|
471
|
+
T\u1EA1o m\u1ED9t mini-app m\u1EDBi \u0111\xFAng "chu\u1EA9n M\u0103ng" (React + Vite + @mangtre/ui).
|
|
472
|
+
mang build
|
|
473
|
+
Build mini-app trong th\u01B0 m\u1EE5c hi\u1EC7n t\u1EA1i \u2192 dist/app.js, r\u1ED3i t\u1EF1 preflight.
|
|
474
|
+
mang run
|
|
475
|
+
Build + xem tr\u01B0\u1EDBc app ngay tr\xEAn tr\xECnh duy\u1EC7t (sdk gi\u1EA3 l\u1EADp) \u2014 kh\xF4ng c\u1EA7n \u0111\u0103ng.
|
|
476
|
+
mang check [bundlePath] [--app <id>]
|
|
477
|
+
Preflight offline m\u1ED9t bundle \u0111\xE3 build (gi\u1ED1ng c\u1ED5ng duy\u1EC7t c\u1EE7a server).
|
|
478
|
+
mang login [--api <url>]
|
|
479
|
+
\u0110\u0103ng nh\u1EADp (device flow) \u2014 duy\u1EC7t m\xE3 tr\xEAn tr\xECnh duy\u1EC7t, l\u01B0u token c\u1EE5c b\u1ED9.
|
|
480
|
+
mang publish [bundlePath]
|
|
481
|
+
Preflight + \u0111\u0103ng bundle l\xEAn M\u0103ng (t\u1EA1o app n\u1EBFu ch\u01B0a c\xF3). C\u1EA7n \u0111\u0103ng nh\u1EADp.
|
|
482
|
+
mang status
|
|
483
|
+
Xem app c\u1EE7a b\u1EA1n + tr\u1EA1ng th\xE1i duy\u1EC7t m\u1ED7i version. C\u1EA7n \u0111\u0103ng nh\u1EADp.
|
|
484
|
+
|
|
485
|
+
mang help | --version`);
|
|
486
|
+
}
|
|
487
|
+
async function main() {
|
|
488
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
489
|
+
switch (cmd) {
|
|
490
|
+
case "new":
|
|
491
|
+
return runNew(rest);
|
|
492
|
+
case "check":
|
|
493
|
+
return runCheck(rest);
|
|
494
|
+
case "build":
|
|
495
|
+
return runBuild(rest);
|
|
496
|
+
case "run":
|
|
497
|
+
return runRun(rest);
|
|
498
|
+
case "login":
|
|
499
|
+
return runLogin(rest);
|
|
500
|
+
case "publish":
|
|
501
|
+
return runPublish(rest);
|
|
502
|
+
case "status":
|
|
503
|
+
return runStatus(rest);
|
|
504
|
+
case "-v":
|
|
505
|
+
case "--version":
|
|
506
|
+
console.log(VERSION);
|
|
507
|
+
return;
|
|
508
|
+
case void 0:
|
|
509
|
+
case "help":
|
|
510
|
+
case "-h":
|
|
511
|
+
case "--help":
|
|
512
|
+
printHelp();
|
|
513
|
+
return;
|
|
514
|
+
default:
|
|
515
|
+
console.error(`L\u1EC7nh kh\xF4ng r\xF5: ${cmd}
|
|
516
|
+
`);
|
|
517
|
+
printHelp();
|
|
518
|
+
process.exitCode = 1;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
main().catch((err) => {
|
|
522
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
523
|
+
process.exitCode = 1;
|
|
524
|
+
});
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiError,
|
|
3
|
+
DEFAULT_API_BASE,
|
|
4
|
+
apiFetch,
|
|
5
|
+
authFilePath,
|
|
6
|
+
checkBundle,
|
|
7
|
+
clearAuth,
|
|
8
|
+
loadAuth,
|
|
9
|
+
saveAuth,
|
|
10
|
+
scaffoldApp
|
|
11
|
+
} from "./chunk-2ZQJSRHJ.js";
|
|
12
|
+
import {
|
|
13
|
+
MANIFEST_COMMENT_PREFIX,
|
|
14
|
+
extractManifestComment
|
|
15
|
+
} from "./chunk-MLWT65G7.js";
|
|
16
|
+
export {
|
|
17
|
+
ApiError,
|
|
18
|
+
DEFAULT_API_BASE,
|
|
19
|
+
MANIFEST_COMMENT_PREFIX,
|
|
20
|
+
apiFetch,
|
|
21
|
+
authFilePath,
|
|
22
|
+
checkBundle,
|
|
23
|
+
clearAuth,
|
|
24
|
+
extractManifestComment,
|
|
25
|
+
loadAuth,
|
|
26
|
+
saveAuth,
|
|
27
|
+
scaffoldApp
|
|
28
|
+
};
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MANIFEST_COMMENT_PREFIX
|
|
3
|
+
} from "./chunk-MLWT65G7.js";
|
|
4
|
+
|
|
5
|
+
// src/vite.ts
|
|
6
|
+
function mangLibConfig(opts) {
|
|
7
|
+
const entry = opts.entry ?? "src/index.tsx";
|
|
8
|
+
const banner = `${MANIFEST_COMMENT_PREFIX}${JSON.stringify(opts.manifest)}`;
|
|
9
|
+
const manifestBannerPlugin = {
|
|
10
|
+
name: "mang:manifest-banner",
|
|
11
|
+
generateBundle(_options, bundle) {
|
|
12
|
+
for (const file of Object.values(bundle)) {
|
|
13
|
+
if (file.type === "chunk" && file.isEntry && typeof file.code === "string") {
|
|
14
|
+
file.code = `${banner}
|
|
15
|
+
${file.code}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
build: {
|
|
22
|
+
target: "es2022",
|
|
23
|
+
emptyOutDir: true,
|
|
24
|
+
lib: { entry, formats: ["es"], fileName: () => "app.js" }
|
|
25
|
+
},
|
|
26
|
+
// The mini-app runs in a browser sandbox with no `process`; bake the prod NODE_ENV in so React
|
|
27
|
+
// (and other libs that branch on it) don't reference a missing `process` global at runtime.
|
|
28
|
+
define: {
|
|
29
|
+
"process.env.NODE_ENV": JSON.stringify("production")
|
|
30
|
+
},
|
|
31
|
+
plugins: [manifestBannerPlugin]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
mangLibConfig
|
|
36
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mangtre/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"bin": {
|
|
9
|
+
"mang": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
"./vite": {
|
|
13
|
+
"types": "./src/vite.ts",
|
|
14
|
+
"default": "./dist/vite.js"
|
|
15
|
+
},
|
|
16
|
+
"./lib": {
|
|
17
|
+
"types": "./src/lib/index.ts",
|
|
18
|
+
"default": "./dist/lib.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"templates",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"zod": "^3.24.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.17.6",
|
|
31
|
+
"tsup": "^8.3.5",
|
|
32
|
+
"vitest": "^2.1.8",
|
|
33
|
+
"@mangtre/standard": "0.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Tiny argv helpers — a 3-command CLI doesn't need a parser dependency. */
|
|
2
|
+
|
|
3
|
+
/** Read `--flag value` or `--flag=value` from argv, or undefined. */
|
|
4
|
+
export function getFlag(argv: string[], name: string): string | undefined {
|
|
5
|
+
const eq = argv.find((a) => a.startsWith(`--${name}=`));
|
|
6
|
+
if (eq) return eq.slice(name.length + 3);
|
|
7
|
+
const idx = argv.indexOf(`--${name}`);
|
|
8
|
+
if (idx >= 0 && idx + 1 < argv.length) {
|
|
9
|
+
const next = argv[idx + 1];
|
|
10
|
+
if (next && !next.startsWith("-")) return next;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** "chia-tien-nhom" → "Chia Tien Nhom" (a sensible default display name). */
|
|
16
|
+
export function titleFromSlug(slug: string): string {
|
|
17
|
+
return slug
|
|
18
|
+
.split("-")
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map((w) => w[0]?.toUpperCase() + w.slice(1))
|
|
21
|
+
.join(" ");
|
|
22
|
+
}
|