@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/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Măng — Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 RumitX. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and its source code (the "Software"), including the Măng shell,
|
|
6
|
+
the @mangtre/* packages, and the bundled mini-apps in this repository, are the
|
|
7
|
+
confidential and proprietary property of RumitX.
|
|
8
|
+
|
|
9
|
+
No license, right, or permission is granted to any person to use, copy, modify,
|
|
10
|
+
merge, publish, distribute, sublicense, sell, or create derivative works of the
|
|
11
|
+
Software, in whole or in part, without the prior written consent of RumitX.
|
|
12
|
+
|
|
13
|
+
Unauthorized copying, distribution, or use of the Software, via any medium, is
|
|
14
|
+
strictly prohibited.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL RUMITX BE LIABLE
|
|
19
|
+
FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
Note: this proprietary notice protects the codebase during the early (NOW)
|
|
23
|
+
horizon. When Măng opens to third-party creators, a separate license may be
|
|
24
|
+
issued for the fork-able starter kit.
|
|
25
|
+
|
|
26
|
+
For licensing inquiries: RumitX — https://rumitx.com
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractManifestComment
|
|
3
|
+
} from "./chunk-MLWT65G7.js";
|
|
4
|
+
|
|
5
|
+
// ../standard/src/manifest.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var APP_SLUG = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
|
|
8
|
+
var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
9
|
+
var PERMISSIONS = ["storage", "theme", "ai", "identity", "context", "data"];
|
|
10
|
+
var SURFACES = ["desktop", "web", "zalo"];
|
|
11
|
+
var AI_CAPABILITIES = ["fast", "balanced", "quality", "code", "reasoning"];
|
|
12
|
+
var localizedText = z.object({ vi: z.string().min(1), en: z.string().min(1) });
|
|
13
|
+
var commandSchema = z.object({
|
|
14
|
+
id: z.string().min(1).max(40).regex(/^[a-z][a-z0-9-]*$/),
|
|
15
|
+
title: localizedText,
|
|
16
|
+
keywords: z.array(z.string().max(40)).max(20).optional(),
|
|
17
|
+
icon: z.string().max(16).optional(),
|
|
18
|
+
surfaces: z.array(z.enum(SURFACES)).optional()
|
|
19
|
+
});
|
|
20
|
+
var flowStepSchema = z.object({
|
|
21
|
+
id: z.string().min(1).max(40).regex(/^[a-z][a-z0-9-]*$/),
|
|
22
|
+
title: localizedText,
|
|
23
|
+
command: z.string().min(1).max(40).regex(/^[a-z][a-z0-9-]*$/).optional(),
|
|
24
|
+
checkpoint: z.string().min(1).max(60),
|
|
25
|
+
caption: localizedText.optional(),
|
|
26
|
+
surfaces: z.array(z.enum(SURFACES)).optional()
|
|
27
|
+
});
|
|
28
|
+
var manifestSchema = z.object({
|
|
29
|
+
id: z.string().regex(APP_SLUG, "id must be a lowercase slug, 3\u201340 chars"),
|
|
30
|
+
name: z.string().min(1).max(60),
|
|
31
|
+
icon: z.string().min(1).max(16),
|
|
32
|
+
version: z.string().regex(SEMVER, "version must be semver (x.y.z)"),
|
|
33
|
+
permissions: z.array(z.enum(PERMISSIONS)),
|
|
34
|
+
commands: z.array(commandSchema).max(50).optional(),
|
|
35
|
+
surfaces: z.array(z.enum(SURFACES)).optional(),
|
|
36
|
+
// The app's main flow — the canonical happy-path screens walked for review + screenshot capture.
|
|
37
|
+
// Mirror of `@mangtre/core` MangManifest.flow. See `@mangtre/core` FlowStep + Tech Foundations §2.8.
|
|
38
|
+
flow: z.array(flowStepSchema).max(20).optional(),
|
|
39
|
+
ai: z.object({ capabilities: z.array(z.enum(AI_CAPABILITIES)) }).optional(),
|
|
40
|
+
// Declared remote-data needs (present when `permissions` includes "data"). Drives the cloud/realtime
|
|
41
|
+
// badges; declaration-only (no runtime gate), mirroring `ai`. See `@mangtre/core` MangDataDeclaration.
|
|
42
|
+
data: z.object({ shared: z.boolean().optional(), realtime: z.boolean().optional() }).optional()
|
|
43
|
+
});
|
|
44
|
+
function parseManifest(value) {
|
|
45
|
+
const res = manifestSchema.safeParse(value);
|
|
46
|
+
if (res.success) return { ok: true, manifest: res.data };
|
|
47
|
+
const errors = res.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
|
|
48
|
+
return { ok: false, errors };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ../standard/src/safety-scan.ts
|
|
52
|
+
var RULES = [
|
|
53
|
+
{
|
|
54
|
+
check: "safety.eval",
|
|
55
|
+
severity: "fail",
|
|
56
|
+
pattern: /\beval\s*\(/,
|
|
57
|
+
message: "D\xF9ng eval() \u2014 b\u1ECB c\u1EA5m (code injection). H\xE3y b\u1ECF eval."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
check: "safety.new-function",
|
|
61
|
+
severity: "fail",
|
|
62
|
+
pattern: /\bnew\s+Function\s*\(/,
|
|
63
|
+
message: "D\xF9ng new Function() \u2014 b\u1ECB c\u1EA5m (code injection)."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
check: "safety.remote-import",
|
|
67
|
+
severity: "fail",
|
|
68
|
+
pattern: /\bimport\s*\(\s*['"`]https?:\/\//,
|
|
69
|
+
message: "import() t\u1EEB URL ngo\xE0i \u2014 b\u1ECB c\u1EA5m (supply-chain). Bundle ph\u1EA3i t\u1EF1 \u0111\xF3ng g\xF3i."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
check: "safety.websocket",
|
|
73
|
+
severity: "fail",
|
|
74
|
+
pattern: /\bnew\s+WebSocket\s*\(|\bnew\s+EventSource\s*\(/,
|
|
75
|
+
message: "M\u1EDF WebSocket/EventSource \u2014 b\u1ECB c\u1EA5m. App ch\u1EC9 trao \u0111\u1ED5i d\u1EEF li\u1EC7u qua sdk.*."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
check: "safety.fetch",
|
|
79
|
+
severity: "warn",
|
|
80
|
+
pattern: /\bfetch\s*\(|\bXMLHttpRequest\b|\bnavigator\.sendBeacon\b/,
|
|
81
|
+
message: "C\xF3 d\u1EA5u hi\u1EC7u g\u1ECDi m\u1EA1ng tr\u1EF1c ti\u1EBFp (fetch/XHR/sendBeacon). D\u1EEF li\u1EC7u ph\u1EA3i \u0111i qua sdk.*."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
check: "safety.storage",
|
|
85
|
+
severity: "warn",
|
|
86
|
+
pattern: /\b(?:local|session)Storage\b|\bindexedDB\b/,
|
|
87
|
+
message: "Truy c\u1EADp localStorage/indexedDB tr\u1EF1c ti\u1EBFp. H\xE3y d\xF9ng sdk.storage."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
check: "safety.document-write",
|
|
91
|
+
severity: "warn",
|
|
92
|
+
pattern: /\bdocument\s*\.\s*write\s*\(/,
|
|
93
|
+
message: "D\xF9ng document.write \u2014 kh\xF4ng khuy\u1EBFn kh\xEDch."
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
function scanBundleSource(source) {
|
|
97
|
+
const findings = [];
|
|
98
|
+
for (const rule of RULES) {
|
|
99
|
+
if (rule.pattern.test(source)) {
|
|
100
|
+
findings.push({ check: rule.check, severity: rule.severity, message: rule.message });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return findings;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ../standard/src/validate.ts
|
|
107
|
+
var MAX_BUNDLE_BYTES = 2 * 1024 * 1024;
|
|
108
|
+
function validateBundle(input) {
|
|
109
|
+
const findings = [];
|
|
110
|
+
let permissions = [];
|
|
111
|
+
let surfaces;
|
|
112
|
+
if (input.bytes > MAX_BUNDLE_BYTES) {
|
|
113
|
+
findings.push({
|
|
114
|
+
check: "size",
|
|
115
|
+
severity: "fail",
|
|
116
|
+
message: `Bundle ${(input.bytes / 1024).toFixed(0)} KB v\u01B0\u1EE3t gi\u1EDBi h\u1EA1n ${MAX_BUNDLE_BYTES / 1024 / 1024} MB.`
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (input.bytes === 0) {
|
|
120
|
+
findings.push({ check: "size.empty", severity: "fail", message: "Bundle r\u1ED7ng." });
|
|
121
|
+
}
|
|
122
|
+
const parsed = parseManifest(input.manifest);
|
|
123
|
+
if (!parsed.ok) {
|
|
124
|
+
for (const err of parsed.errors) {
|
|
125
|
+
findings.push({ check: "manifest.schema", severity: "fail", message: err });
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
permissions = parsed.manifest.permissions;
|
|
129
|
+
surfaces = parsed.manifest.surfaces;
|
|
130
|
+
if (parsed.manifest.id !== input.appId) {
|
|
131
|
+
findings.push({
|
|
132
|
+
check: "manifest.slug",
|
|
133
|
+
severity: "fail",
|
|
134
|
+
message: `manifest.id "${parsed.manifest.id}" kh\xF4ng kh\u1EDBp app "${input.appId}".`
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const ungranted = permissions.filter((p) => p === "ai" || p === "identity" || p === "context");
|
|
138
|
+
if (ungranted.length > 0) {
|
|
139
|
+
findings.push({
|
|
140
|
+
check: "manifest.permissions",
|
|
141
|
+
severity: "warn",
|
|
142
|
+
message: `Quy\u1EC1n ${ungranted.join(", ")} ch\u01B0a \u0111\u01B0\u1EE3c c\u1EA5p cho app sandbox tr\xEAn web (ch\u1EC9 ch\u1EA1y \u1EDF desktop ch\xEDnh ch\u1EE7).`
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (permissions.includes("data")) {
|
|
146
|
+
const decl = parsed.manifest.data;
|
|
147
|
+
const shape = decl?.shared ? "ph\xF2ng chia s\u1EBB (nhi\u1EC1u ng\u01B0\u1EDDi)" : "l\u01B0u ri\xEAng theo thi\u1EBFt b\u1ECB";
|
|
148
|
+
findings.push({
|
|
149
|
+
check: "manifest.data",
|
|
150
|
+
severity: "warn",
|
|
151
|
+
message: `App d\xF9ng d\u1EEF li\u1EC7u tr\xEAn m\xE1y ch\u1EE7 M\u0103ng (${shape}). Ki\u1EC3m tra m\u1EE5c \u0111\xEDch l\u01B0u tr\u1EEF khi duy\u1EC7t.`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
findings.push(...checkFlow(parsed.manifest));
|
|
155
|
+
}
|
|
156
|
+
findings.push(...scanBundleSource(input.source));
|
|
157
|
+
const passed = !findings.some((f) => f.severity === "fail");
|
|
158
|
+
return {
|
|
159
|
+
passed,
|
|
160
|
+
findings,
|
|
161
|
+
bytes: input.bytes,
|
|
162
|
+
permissions,
|
|
163
|
+
surfaces,
|
|
164
|
+
checkedAt: input.now
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function checkFlow(manifest) {
|
|
168
|
+
const findings = [];
|
|
169
|
+
const flow = manifest.flow;
|
|
170
|
+
if (!flow || flow.length === 0) {
|
|
171
|
+
findings.push({
|
|
172
|
+
check: "flow.declared",
|
|
173
|
+
severity: "warn",
|
|
174
|
+
message: "App ch\u01B0a khai b\xE1o lu\u1ED3ng ch\xEDnh (flow). B\u1EAFt bu\u1ED9c s\u1EAFp t\u1EDBi \u0111\u1EC3 duy\u1EC7t + ch\u1EE5p \u1EA3nh t\u1EF1 \u0111\u1ED9ng."
|
|
175
|
+
});
|
|
176
|
+
return findings;
|
|
177
|
+
}
|
|
178
|
+
const commandIds = new Set((manifest.commands ?? []).map((c) => c.id));
|
|
179
|
+
const seenStepIds = /* @__PURE__ */ new Set();
|
|
180
|
+
const seenCheckpoints = /* @__PURE__ */ new Set();
|
|
181
|
+
for (const step of flow) {
|
|
182
|
+
if (step.command && !commandIds.has(step.command)) {
|
|
183
|
+
findings.push({
|
|
184
|
+
check: "flow.command-ref",
|
|
185
|
+
severity: "fail",
|
|
186
|
+
message: `B\u01B0\u1EDBc "${step.id}" tham chi\u1EBFu command "${step.command}" kh\xF4ng t\u1ED3n t\u1EA1i trong commands.`
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (seenStepIds.has(step.id)) {
|
|
190
|
+
findings.push({
|
|
191
|
+
check: "flow.step-dup",
|
|
192
|
+
severity: "fail",
|
|
193
|
+
message: `Tr\xF9ng id b\u01B0\u1EDBc flow "${step.id}".`
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
seenStepIds.add(step.id);
|
|
197
|
+
if (seenCheckpoints.has(step.checkpoint)) {
|
|
198
|
+
findings.push({
|
|
199
|
+
check: "flow.checkpoint-dup",
|
|
200
|
+
severity: "fail",
|
|
201
|
+
message: `Tr\xF9ng checkpoint "${step.checkpoint}" gi\u1EEFa c\xE1c b\u01B0\u1EDBc flow.`
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
seenCheckpoints.add(step.checkpoint);
|
|
205
|
+
}
|
|
206
|
+
return findings;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/lib/check.ts
|
|
210
|
+
function manifestId(manifest) {
|
|
211
|
+
if (typeof manifest === "object" && manifest !== null && !Array.isArray(manifest)) {
|
|
212
|
+
const id = manifest.id;
|
|
213
|
+
if (typeof id === "string") return id;
|
|
214
|
+
}
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
function checkBundle(opts) {
|
|
218
|
+
const manifest = opts.manifest ?? extractManifestComment(opts.source);
|
|
219
|
+
const manifestFound = manifest !== void 0;
|
|
220
|
+
const appId = opts.appId ?? manifestId(manifest) ?? "";
|
|
221
|
+
const report = validateBundle({
|
|
222
|
+
source: opts.source,
|
|
223
|
+
bytes: Buffer.byteLength(opts.source, "utf8"),
|
|
224
|
+
appId,
|
|
225
|
+
manifest: manifest ?? {},
|
|
226
|
+
now: opts.now ?? Date.now()
|
|
227
|
+
});
|
|
228
|
+
return { ...report, manifestFound };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/lib/api.ts
|
|
232
|
+
var ApiError = class extends Error {
|
|
233
|
+
constructor(status, message) {
|
|
234
|
+
super(message);
|
|
235
|
+
this.status = status;
|
|
236
|
+
this.name = "ApiError";
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
async function apiFetch(path, opts) {
|
|
240
|
+
const headers = {};
|
|
241
|
+
if (opts.token) headers.Authorization = `Bearer ${opts.token}`;
|
|
242
|
+
let body;
|
|
243
|
+
if (opts.body instanceof FormData) {
|
|
244
|
+
body = opts.body;
|
|
245
|
+
} else if (opts.body !== void 0) {
|
|
246
|
+
headers["Content-Type"] = "application/json";
|
|
247
|
+
body = JSON.stringify(opts.body);
|
|
248
|
+
}
|
|
249
|
+
const res = await fetch(`${opts.base}${path}`, {
|
|
250
|
+
method: opts.method ?? (body ? "POST" : "GET"),
|
|
251
|
+
headers,
|
|
252
|
+
body
|
|
253
|
+
});
|
|
254
|
+
const text = await res.text();
|
|
255
|
+
const data = text ? safeJson(text) : {};
|
|
256
|
+
if (!res.ok) {
|
|
257
|
+
let message = `HTTP ${res.status}`;
|
|
258
|
+
if (data && typeof data === "object" && "error" in data) {
|
|
259
|
+
message = String(data.error);
|
|
260
|
+
}
|
|
261
|
+
throw new ApiError(res.status, message);
|
|
262
|
+
}
|
|
263
|
+
return data;
|
|
264
|
+
}
|
|
265
|
+
function safeJson(text) {
|
|
266
|
+
try {
|
|
267
|
+
return JSON.parse(text);
|
|
268
|
+
} catch {
|
|
269
|
+
return { raw: text };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/lib/config.ts
|
|
274
|
+
import { homedir } from "os";
|
|
275
|
+
import { join } from "path";
|
|
276
|
+
|
|
277
|
+
// src/commands/args.ts
|
|
278
|
+
function getFlag(argv, name) {
|
|
279
|
+
const eq = argv.find((a) => a.startsWith(`--${name}=`));
|
|
280
|
+
if (eq) return eq.slice(name.length + 3);
|
|
281
|
+
const idx = argv.indexOf(`--${name}`);
|
|
282
|
+
if (idx >= 0 && idx + 1 < argv.length) {
|
|
283
|
+
const next = argv[idx + 1];
|
|
284
|
+
if (next && !next.startsWith("-")) return next;
|
|
285
|
+
}
|
|
286
|
+
return void 0;
|
|
287
|
+
}
|
|
288
|
+
function titleFromSlug(slug) {
|
|
289
|
+
return slug.split("-").filter(Boolean).map((w) => w[0]?.toUpperCase() + w.slice(1)).join(" ");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/lib/config.ts
|
|
293
|
+
var DEFAULT_API_BASE = "https://api.mang.rumitx.com";
|
|
294
|
+
function normalizeApiBase(raw) {
|
|
295
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
296
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
297
|
+
}
|
|
298
|
+
function resolveApiBase(argv) {
|
|
299
|
+
return normalizeApiBase(getFlag(argv, "api") ?? process.env.MANG_API ?? DEFAULT_API_BASE);
|
|
300
|
+
}
|
|
301
|
+
function authFilePath() {
|
|
302
|
+
return join(homedir(), ".mang", "auth.json");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/lib/auth-store.ts
|
|
306
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
307
|
+
import { dirname } from "path";
|
|
308
|
+
function loadAuth() {
|
|
309
|
+
const path = authFilePath();
|
|
310
|
+
if (!existsSync(path)) return null;
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
313
|
+
if (parsed && typeof parsed.token === "string" && typeof parsed.api === "string") {
|
|
314
|
+
return parsed;
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
function saveAuth(auth) {
|
|
321
|
+
const path = authFilePath();
|
|
322
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
323
|
+
writeFileSync(path, `${JSON.stringify(auth, null, 2)}
|
|
324
|
+
`, { mode: 384 });
|
|
325
|
+
}
|
|
326
|
+
function clearAuth() {
|
|
327
|
+
const path = authFilePath();
|
|
328
|
+
if (existsSync(path)) rmSync(path);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/lib/scaffold.ts
|
|
332
|
+
import { existsSync as existsSync2 } from "fs";
|
|
333
|
+
import { mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
334
|
+
import { join as join2 } from "path";
|
|
335
|
+
import { fileURLToPath } from "url";
|
|
336
|
+
function templateRoot() {
|
|
337
|
+
return fileURLToPath(new URL("../templates/app", import.meta.url));
|
|
338
|
+
}
|
|
339
|
+
function applyPlaceholders(text, opts) {
|
|
340
|
+
return text.replaceAll("__SLUG__", opts.slug).replaceAll("__NAME__", opts.name).replaceAll("__ICON__", opts.icon);
|
|
341
|
+
}
|
|
342
|
+
async function copyTree(srcDir, destDir, opts) {
|
|
343
|
+
await mkdir(destDir, { recursive: true });
|
|
344
|
+
const written = [];
|
|
345
|
+
for (const entry of await readdir(srcDir, { withFileTypes: true })) {
|
|
346
|
+
const srcPath = join2(srcDir, entry.name);
|
|
347
|
+
const outName = entry.name.endsWith(".tmpl") ? entry.name.slice(0, -5) : entry.name;
|
|
348
|
+
const destPath = join2(destDir, outName);
|
|
349
|
+
if (entry.isDirectory()) {
|
|
350
|
+
written.push(...await copyTree(srcPath, destPath, opts));
|
|
351
|
+
} else {
|
|
352
|
+
const raw = await readFile(srcPath, "utf8");
|
|
353
|
+
await writeFile(destPath, applyPlaceholders(raw, opts), "utf8");
|
|
354
|
+
written.push(destPath);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return written;
|
|
358
|
+
}
|
|
359
|
+
async function scaffoldApp(opts) {
|
|
360
|
+
if (existsSync2(opts.targetDir)) {
|
|
361
|
+
throw new Error(`Th\u01B0 m\u1EE5c \u0111\xE3 t\u1ED3n t\u1EA1i: ${opts.targetDir}`);
|
|
362
|
+
}
|
|
363
|
+
return copyTree(templateRoot(), opts.targetDir, opts);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export {
|
|
367
|
+
APP_SLUG,
|
|
368
|
+
checkBundle,
|
|
369
|
+
getFlag,
|
|
370
|
+
titleFromSlug,
|
|
371
|
+
ApiError,
|
|
372
|
+
apiFetch,
|
|
373
|
+
DEFAULT_API_BASE,
|
|
374
|
+
resolveApiBase,
|
|
375
|
+
authFilePath,
|
|
376
|
+
loadAuth,
|
|
377
|
+
saveAuth,
|
|
378
|
+
clearAuth,
|
|
379
|
+
scaffoldApp
|
|
380
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/lib/manifest-comment.ts
|
|
2
|
+
var MANIFEST_COMMENT_PREFIX = "// mang-manifest:";
|
|
3
|
+
function extractManifestComment(source) {
|
|
4
|
+
const firstLine = source.split("\n", 1)[0] ?? "";
|
|
5
|
+
const match = firstLine.match(/^\/\/\s*mang-manifest:\s*(\{.*\})\s*$/);
|
|
6
|
+
if (!match?.[1]) return void 0;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(match[1]);
|
|
9
|
+
} catch {
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
MANIFEST_COMMENT_PREFIX,
|
|
16
|
+
extractManifestComment
|
|
17
|
+
};
|