@agile-team/jh4j-cloud-cli 0.3.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/dist/index.js ADDED
@@ -0,0 +1,1352 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "@agile-team/jh4j-cloud-cli",
9
+ version: "0.3.0",
10
+ description: "JH4J Cloud \u4F01\u4E1A\u5185\u90E8\u6807\u51C6\u5316\u9879\u76EE\u811A\u624B\u67B6",
11
+ repository: {
12
+ type: "git",
13
+ url: "git+https://github.com/ChenyCHENYU/jh4j-cloud-cli.git"
14
+ },
15
+ homepage: "https://github.com/ChenyCHENYU/jh4j-cloud-cli#readme",
16
+ bugs: {
17
+ url: "https://github.com/ChenyCHENYU/jh4j-cloud-cli/issues"
18
+ },
19
+ type: "module",
20
+ engines: {
21
+ node: "^22.12.0 || ^24.0.0",
22
+ pnpm: ">=11.8.0"
23
+ },
24
+ packageManager: "pnpm@11.8.0",
25
+ bin: {
26
+ jh4j: "bin/jh4j.js"
27
+ },
28
+ files: [
29
+ "bin",
30
+ "dist",
31
+ "README.md",
32
+ "CHANGELOG.md",
33
+ "catalog.schema.json"
34
+ ],
35
+ scripts: {
36
+ build: "tsup",
37
+ dev: "tsup --watch",
38
+ typecheck: "tsc --noEmit",
39
+ test: "vitest run",
40
+ "test:watch": "vitest",
41
+ start: "node bin/jh4j.js",
42
+ check: "pnpm typecheck && pnpm test && pnpm build",
43
+ prepack: "pnpm check"
44
+ },
45
+ dependencies: {
46
+ "@clack/prompts": "^1.7.0",
47
+ commander: "^15.0.0",
48
+ semver: "^7.8.5",
49
+ tar: "^7.5.19"
50
+ },
51
+ publishConfig: {
52
+ access: "public",
53
+ registry: "https://registry.npmjs.org/"
54
+ },
55
+ devDependencies: {
56
+ "@types/node": "^24.13.3",
57
+ "@types/semver": "7.7.1",
58
+ tsup: "8.5.1",
59
+ typescript: "7.0.2",
60
+ vitest: "4.1.10"
61
+ }
62
+ };
63
+
64
+ // src/constants.ts
65
+ var CLI_NAME = "@agile-team/jh4j-cloud-cli";
66
+ var CLI_VERSION = package_default.version;
67
+ var COMMAND_NAME = "jh4j";
68
+ var TEMPLATE_SOURCE_ENV = "JH4J_UI_TEMPLATE_SOURCE";
69
+ var JH4J_HOME_ENV = "JH4J_HOME";
70
+ var CATALOG_FILE_ENV = "JH4J_CATALOG_FILE";
71
+ var USER_CONFIG_SCHEMA_VERSION = 1;
72
+
73
+ // src/catalog.ts
74
+ import { fileURLToPath } from "url";
75
+ import path2 from "path";
76
+ import { existsSync as existsSync2 } from "fs";
77
+
78
+ // src/utils/fs.ts
79
+ import { cp, mkdir, readFile, rm, stat, writeFile } from "fs/promises";
80
+ import { existsSync } from "fs";
81
+ import path from "path";
82
+ var EXCLUDED_TEMPLATE_PATHS = /* @__PURE__ */ new Set([
83
+ ".git",
84
+ ".jhlc",
85
+ "dist",
86
+ "node_modules"
87
+ ]);
88
+ async function readJson(file) {
89
+ return JSON.parse(await readFile(file, "utf8"));
90
+ }
91
+ async function writeJson(file, value) {
92
+ await mkdir(path.dirname(file), { recursive: true });
93
+ await writeFile(file, `${JSON.stringify(value, null, 2)}
94
+ `, "utf8");
95
+ }
96
+ async function isDirectory(directory) {
97
+ try {
98
+ return (await stat(directory)).isDirectory();
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ async function isDirectoryEmpty(directory) {
104
+ if (!existsSync(directory)) return true;
105
+ const { readdir: readdir3 } = await import("fs/promises");
106
+ return (await readdir3(directory)).length === 0;
107
+ }
108
+ async function copyTemplateTree(sourceRoot, targetRoot) {
109
+ await mkdir(targetRoot, { recursive: true });
110
+ await cp(sourceRoot, targetRoot, {
111
+ recursive: true,
112
+ force: true,
113
+ filter(source) {
114
+ const relative = path.relative(sourceRoot, source);
115
+ if (!relative) return true;
116
+ const [topLevel] = relative.split(path.sep);
117
+ return !EXCLUDED_TEMPLATE_PATHS.has(topLevel);
118
+ }
119
+ });
120
+ }
121
+ async function removePath(target) {
122
+ await rm(target, { recursive: true, force: true, maxRetries: 3 });
123
+ }
124
+
125
+ // src/catalog.ts
126
+ var siblingTemplatePath = fileURLToPath(
127
+ new URL("../../jh4j-ui-template/", import.meta.url)
128
+ );
129
+ var remoteTemplateSources = [
130
+ "https://github.com/ChenyCHENYU/jh4j-ui-template.git",
131
+ "https://gitee.com/ycyplus163/jh4j-ui-template.git"
132
+ ];
133
+ var BUILTIN_TEMPLATES = [
134
+ {
135
+ id: "web.jh4j-mf-remote",
136
+ name: "JH4J PC \u5FAE\u524D\u7AEF\u4E1A\u52A1\u6A21\u677F",
137
+ description: "Vue 3 + Vite + Module Federation \u6807\u51C6\u4E1A\u52A1\u5B50\u7CFB\u7EDF",
138
+ category: "frontend",
139
+ sourceEnvironment: TEMPLATE_SOURCE_ENV,
140
+ defaultSource: existsSync2(siblingTemplatePath) ? siblingTemplatePath : remoteTemplateSources[0],
141
+ sources: remoteTemplateSources,
142
+ defaultRef: "main",
143
+ status: "beta",
144
+ tags: ["vue", "vite", "module-federation", "pc"]
145
+ }
146
+ ];
147
+ function validateCatalogTemplate(template) {
148
+ if (!template.id || !template.name || !template.category) {
149
+ throw new Error("Catalog \u6A21\u677F\u7F3A\u5C11 id\u3001name \u6216 category");
150
+ }
151
+ if (!(/* @__PURE__ */ new Set(["frontend", "backend", "mobile"])).has(template.category)) {
152
+ throw new Error(`Catalog \u6A21\u677F ${template.id} \u7684 category \u65E0\u6548`);
153
+ }
154
+ if (!(/* @__PURE__ */ new Set(["stable", "beta"])).has(template.status)) {
155
+ throw new Error(`Catalog \u6A21\u677F ${template.id} \u7684 status \u65E0\u6548`);
156
+ }
157
+ if (!template.defaultRef) {
158
+ throw new Error(`Catalog \u6A21\u677F ${template.id} \u7F3A\u5C11 defaultRef`);
159
+ }
160
+ if (!template.defaultSource && !template.sourceEnvironment) {
161
+ throw new Error(`Catalog \u6A21\u677F ${template.id} \u6CA1\u6709\u53EF\u89E3\u6790\u7684\u6A21\u677F\u6E90`);
162
+ }
163
+ if (template.sources?.some((source) => !source.trim())) {
164
+ throw new Error(`Catalog \u6A21\u677F ${template.id} \u5305\u542B\u7A7A\u7684\u5907\u7528\u6E90`);
165
+ }
166
+ }
167
+ function resolveCatalogSource(source, catalogDirectory) {
168
+ const isRemote = /^(?:https?|ssh|file):\/\//.test(source) || source.startsWith("git@");
169
+ return isRemote || path2.isAbsolute(source) ? source : path2.resolve(catalogDirectory, source);
170
+ }
171
+ async function loadCatalog(config) {
172
+ const configuredFile = process.env[CATALOG_FILE_ENV] || config?.catalogFile;
173
+ if (!configuredFile) return [...BUILTIN_TEMPLATES];
174
+ const catalogPath = path2.resolve(configuredFile);
175
+ if (!existsSync2(catalogPath)) {
176
+ throw new Error(`Catalog \u6587\u4EF6\u4E0D\u5B58\u5728: ${catalogPath}`);
177
+ }
178
+ const external = await readJson(catalogPath);
179
+ if (external.schemaVersion !== 1 || !Array.isArray(external.templates)) {
180
+ throw new Error(`Catalog \u6587\u4EF6\u683C\u5F0F\u65E0\u6548: ${catalogPath}`);
181
+ }
182
+ const catalogDirectory = path2.dirname(catalogPath);
183
+ external.templates = external.templates.map((template) => {
184
+ return {
185
+ ...template,
186
+ defaultSource: resolveCatalogSource(
187
+ template.defaultSource,
188
+ catalogDirectory
189
+ ),
190
+ sources: template.sources?.map(
191
+ (source) => resolveCatalogSource(source, catalogDirectory)
192
+ )
193
+ };
194
+ });
195
+ external.templates.forEach(validateCatalogTemplate);
196
+ const merged = new Map(BUILTIN_TEMPLATES.map((item) => [item.id, item]));
197
+ external.templates.forEach((item) => merged.set(item.id, item));
198
+ return [...merged.values()];
199
+ }
200
+ function findTemplate(catalog, id) {
201
+ const selected = id ?? catalog[0]?.id;
202
+ const template = catalog.find((item) => item.id === selected);
203
+ if (!template) {
204
+ throw new Error(
205
+ `\u6A21\u677F\u4E0D\u5B58\u5728: ${selected}\u3002\u53EF\u7528\u6A21\u677F: ${catalog.map((item) => item.id).join(", ")}`
206
+ );
207
+ }
208
+ return template;
209
+ }
210
+
211
+ // src/core/template-source.ts
212
+ import path5 from "path";
213
+ import { existsSync as existsSync5 } from "fs";
214
+ import { mkdir as mkdir3, readdir as readdir2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
215
+ import { x as extractTar } from "tar";
216
+
217
+ // src/utils/process.ts
218
+ import { spawn } from "child_process";
219
+ function platformCommand(command) {
220
+ if (process.platform !== "win32") return command;
221
+ return command === "pnpm" || command === "npm" || command === "npx" ? `${command}.cmd` : command;
222
+ }
223
+ async function runCommand(command, args, options = {}) {
224
+ const stdio = options.stdio ?? "inherit";
225
+ return new Promise((resolve, reject) => {
226
+ const executable = platformCommand(command);
227
+ const isWindowsCommand = process.platform === "win32" && executable.endsWith(".cmd");
228
+ const spawnExecutable = isWindowsCommand ? process.env.ComSpec || "cmd.exe" : executable;
229
+ const spawnArgs = isWindowsCommand ? ["/d", "/s", "/c", executable, ...args] : args;
230
+ const child = spawn(spawnExecutable, spawnArgs, {
231
+ cwd: options.cwd,
232
+ stdio,
233
+ env: { ...process.env, ...options.env },
234
+ windowsHide: true
235
+ });
236
+ let output = "";
237
+ if (stdio === "pipe") {
238
+ child.stdout?.on("data", (chunk) => {
239
+ output += chunk.toString();
240
+ });
241
+ child.stderr?.on("data", (chunk) => {
242
+ output += chunk.toString();
243
+ });
244
+ }
245
+ child.on("error", reject);
246
+ child.on("exit", (code) => {
247
+ if (code === 0) {
248
+ resolve(output.trim());
249
+ return;
250
+ }
251
+ reject(
252
+ new Error(
253
+ `${command} ${args.join(" ")} \u6267\u884C\u5931\u8D25\uFF08exit ${code ?? "unknown"}\uFF09${output ? `
254
+ ${output.trim()}` : ""}`
255
+ )
256
+ );
257
+ });
258
+ });
259
+ }
260
+ async function inspectCommand(command, args = ["--version"]) {
261
+ try {
262
+ return { ok: true, output: await runCommand(command, args, { stdio: "pipe" }) };
263
+ } catch (error) {
264
+ return { ok: false, output: error.message };
265
+ }
266
+ }
267
+
268
+ // src/core/template-cache.ts
269
+ import { createHash } from "crypto";
270
+ import { existsSync as existsSync4 } from "fs";
271
+ import { mkdir as mkdir2, readdir, rename, stat as stat2 } from "fs/promises";
272
+ import path4 from "path";
273
+
274
+ // src/core/user-config.ts
275
+ import os from "os";
276
+ import path3 from "path";
277
+ import { existsSync as existsSync3 } from "fs";
278
+ var DEFAULT_USER_CONFIG = {
279
+ schemaVersion: USER_CONFIG_SCHEMA_VERSION,
280
+ autoInstall: true,
281
+ autoGit: true,
282
+ cacheTtlMinutes: 60
283
+ };
284
+ function getJh4jHome() {
285
+ return path3.resolve(process.env[JH4J_HOME_ENV] || path3.join(os.homedir(), ".jh4j"));
286
+ }
287
+ function getUserConfigPath() {
288
+ return path3.join(getJh4jHome(), "config.json");
289
+ }
290
+ async function loadUserConfig() {
291
+ const configPath = getUserConfigPath();
292
+ if (!existsSync3(configPath)) return { ...DEFAULT_USER_CONFIG };
293
+ const stored = await readJson(configPath);
294
+ if (stored.schemaVersion !== void 0 && stored.schemaVersion !== USER_CONFIG_SCHEMA_VERSION) {
295
+ throw new Error(
296
+ `\u4E0D\u652F\u6301\u7684\u7528\u6237\u914D\u7F6E\u7248\u672C: ${stored.schemaVersion}\uFF0C\u5F53\u524D\u8981\u6C42 ${USER_CONFIG_SCHEMA_VERSION}`
297
+ );
298
+ }
299
+ return { ...DEFAULT_USER_CONFIG, ...stored };
300
+ }
301
+ async function saveUserConfig(config) {
302
+ await writeJson(getUserConfigPath(), {
303
+ ...config,
304
+ schemaVersion: USER_CONFIG_SCHEMA_VERSION
305
+ });
306
+ }
307
+ var CONFIG_KEYS = /* @__PURE__ */ new Set([
308
+ "catalogFile",
309
+ "templateSource",
310
+ "templateRef",
311
+ "npmRegistry",
312
+ "jhlcRegistry",
313
+ "autoInstall",
314
+ "autoGit",
315
+ "cacheTtlMinutes"
316
+ ]);
317
+ function parseUserConfigValue(key, rawValue) {
318
+ if (!CONFIG_KEYS.has(key)) {
319
+ throw new Error(`\u4E0D\u652F\u6301\u7684\u914D\u7F6E\u9879: ${key}`);
320
+ }
321
+ const typedKey = key;
322
+ if (typedKey === "autoInstall" || typedKey === "autoGit") {
323
+ const normalized = rawValue.toLowerCase();
324
+ if (!(/* @__PURE__ */ new Set(["true", "false"])).has(normalized)) {
325
+ throw new Error(`${key} \u53EA\u80FD\u662F true \u6216 false`);
326
+ }
327
+ return [typedKey, normalized === "true"];
328
+ }
329
+ if (typedKey === "cacheTtlMinutes") {
330
+ const value = Number(rawValue);
331
+ if (!Number.isInteger(value) || value < 0 || value > 10080) {
332
+ throw new Error("cacheTtlMinutes \u5FC5\u987B\u662F 0 \u5230 10080 \u4E4B\u95F4\u7684\u6574\u6570");
333
+ }
334
+ return [typedKey, value];
335
+ }
336
+ return [typedKey, rawValue.trim()];
337
+ }
338
+
339
+ // src/core/template-cache.ts
340
+ var CACHE_SCHEMA_VERSION = 1;
341
+ function getTemplateCacheRoot() {
342
+ return path4.join(getJh4jHome(), "cache", "templates");
343
+ }
344
+ function getTemplateCacheKey(source, ref) {
345
+ return createHash("sha256").update(`${source}\0${ref}`).digest("hex").slice(0, 20);
346
+ }
347
+ function getEntryPaths(key) {
348
+ const entryRoot = path4.join(getTemplateCacheRoot(), key);
349
+ return {
350
+ entryRoot,
351
+ templateRoot: path4.join(entryRoot, "template"),
352
+ metadataPath: path4.join(entryRoot, "metadata.json")
353
+ };
354
+ }
355
+ async function readTemplateCacheEntry(source, ref) {
356
+ const key = getTemplateCacheKey(source, ref);
357
+ const paths = getEntryPaths(key);
358
+ if (!existsSync4(paths.templateRoot) || !existsSync4(paths.metadataPath)) return null;
359
+ try {
360
+ const metadata = await readJson(paths.metadataPath);
361
+ if (metadata.schemaVersion !== CACHE_SCHEMA_VERSION || metadata.source !== source || metadata.ref !== ref) {
362
+ return null;
363
+ }
364
+ return {
365
+ key,
366
+ root: paths.templateRoot,
367
+ metadata,
368
+ sizeBytes: await calculateDirectorySize(paths.entryRoot)
369
+ };
370
+ } catch {
371
+ return null;
372
+ }
373
+ }
374
+ function isTemplateCacheFresh(entry, ttlMinutes) {
375
+ if (ttlMinutes <= 0) return false;
376
+ const cachedAt = Date.parse(entry.metadata.cachedAt);
377
+ return Number.isFinite(cachedAt) && Date.now() - cachedAt < ttlMinutes * 6e4;
378
+ }
379
+ async function createTemplateCacheStaging(source, ref) {
380
+ const key = getTemplateCacheKey(source, ref);
381
+ const cacheRoot = getTemplateCacheRoot();
382
+ await mkdir2(cacheRoot, { recursive: true });
383
+ const stagingRoot = path4.join(
384
+ cacheRoot,
385
+ `.${key}.tmp-${process.pid}-${Date.now()}`
386
+ );
387
+ const templateRoot = path4.join(stagingRoot, "template");
388
+ const entryRoot = path4.join(cacheRoot, key);
389
+ await mkdir2(stagingRoot, { recursive: true });
390
+ return {
391
+ key,
392
+ entryRoot,
393
+ stagingRoot,
394
+ templateRoot,
395
+ async commit() {
396
+ await writeJson(path4.join(stagingRoot, "metadata.json"), {
397
+ schemaVersion: CACHE_SCHEMA_VERSION,
398
+ source,
399
+ ref,
400
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString()
401
+ });
402
+ await removePath(entryRoot);
403
+ await rename(stagingRoot, entryRoot);
404
+ },
405
+ cleanup: () => removePath(stagingRoot)
406
+ };
407
+ }
408
+ async function calculateDirectorySize(directory) {
409
+ let total = 0;
410
+ const entries = await readdir(directory, { withFileTypes: true });
411
+ for (const entry of entries) {
412
+ const absolute = path4.join(directory, entry.name);
413
+ if (entry.isDirectory()) total += await calculateDirectorySize(absolute);
414
+ else if (entry.isFile()) total += (await stat2(absolute)).size;
415
+ }
416
+ return total;
417
+ }
418
+ async function listTemplateCache() {
419
+ const cacheRoot = getTemplateCacheRoot();
420
+ if (!existsSync4(cacheRoot)) return [];
421
+ const entries = [];
422
+ for (const directory of await readdir(cacheRoot, { withFileTypes: true })) {
423
+ if (!directory.isDirectory() || directory.name.startsWith(".")) continue;
424
+ const paths = getEntryPaths(directory.name);
425
+ if (!existsSync4(paths.metadataPath) || !existsSync4(paths.templateRoot)) continue;
426
+ try {
427
+ entries.push({
428
+ key: directory.name,
429
+ root: paths.templateRoot,
430
+ metadata: await readJson(paths.metadataPath),
431
+ sizeBytes: await calculateDirectorySize(paths.entryRoot)
432
+ });
433
+ } catch {
434
+ }
435
+ }
436
+ return entries.sort(
437
+ (a, b) => b.metadata.cachedAt.localeCompare(a.metadata.cachedAt)
438
+ );
439
+ }
440
+ async function clearTemplateCache() {
441
+ await removePath(getTemplateCacheRoot());
442
+ }
443
+
444
+ // src/core/template-source.ts
445
+ var MAX_ARCHIVE_BYTES = 200 * 1024 * 1024;
446
+ function resolveTemplateSources(template, override, configuredSource) {
447
+ const exclusiveSource = override || (template.sourceEnvironment ? process.env[template.sourceEnvironment] : void 0) || configuredSource;
448
+ if (exclusiveSource) return [exclusiveSource];
449
+ return [.../* @__PURE__ */ new Set([template.defaultSource, ...template.sources ?? []])];
450
+ }
451
+ function isGitSource(source) {
452
+ return source.startsWith("git@") || source.startsWith("ssh://") || source.startsWith("file://") || source.startsWith("http://") || source.startsWith("https://") || source.endsWith(".git");
453
+ }
454
+ function isArchiveSource(source) {
455
+ const pathname = source.split(/[?#]/, 1)[0].toLowerCase();
456
+ return pathname.endsWith(".tgz") || pathname.endsWith(".tar.gz") || pathname.endsWith(".tar");
457
+ }
458
+ async function getFreshCache(source, ref, options) {
459
+ const cacheEntry = await readTemplateCacheEntry(source, ref);
460
+ if (!options.noCache && cacheEntry && isTemplateCacheFresh(cacheEntry, options.cacheTtlMinutes ?? 60)) {
461
+ return {
462
+ root: cacheEntry.root,
463
+ source: `${source}#${ref} (cache)`,
464
+ async cleanup() {
465
+ }
466
+ };
467
+ }
468
+ return null;
469
+ }
470
+ async function normalizeExtractedTemplate(stagingRoot, templateRoot) {
471
+ if (existsSync5(path5.join(templateRoot, "template.manifest.json"))) return;
472
+ const children = await readdir2(templateRoot, { withFileTypes: true });
473
+ const candidates = children.filter(
474
+ (entry) => entry.isDirectory() && existsSync5(path5.join(templateRoot, entry.name, "template.manifest.json"))
475
+ );
476
+ if (candidates.length !== 1) {
477
+ throw new Error("\u538B\u7F29\u5305\u6839\u76EE\u5F55\u6216\u552F\u4E00\u4E00\u7EA7\u76EE\u5F55\u4E2D\u672A\u627E\u5230 template.manifest.json");
478
+ }
479
+ const normalizedRoot = path5.join(stagingRoot, ".normalized-template");
480
+ await rename2(path5.join(templateRoot, candidates[0].name), normalizedRoot);
481
+ await rm2(templateRoot, { recursive: true, force: true });
482
+ await rename2(normalizedRoot, templateRoot);
483
+ }
484
+ async function acquireArchiveTemplate(source, ref, options) {
485
+ const cached = await getFreshCache(source, ref, options);
486
+ if (cached) return cached;
487
+ const cacheStaging = await createTemplateCacheStaging(source, ref);
488
+ const archiveFile = path5.join(cacheStaging.stagingRoot, "template.tgz");
489
+ try {
490
+ if (source.startsWith("http://") || source.startsWith("https://")) {
491
+ const response = await fetch(source, { signal: AbortSignal.timeout(6e4) });
492
+ if (!response.ok) {
493
+ throw new Error(`\u6A21\u677F\u538B\u7F29\u5305\u4E0B\u8F7D\u5931\u8D25: HTTP ${response.status}`);
494
+ }
495
+ const declaredLength = Number(response.headers.get("content-length") || 0);
496
+ if (declaredLength > MAX_ARCHIVE_BYTES) {
497
+ throw new Error("\u6A21\u677F\u538B\u7F29\u5305\u8D85\u8FC7 200 MB \u9650\u5236");
498
+ }
499
+ const archiveBuffer = Buffer.from(await response.arrayBuffer());
500
+ if (archiveBuffer.byteLength > MAX_ARCHIVE_BYTES) {
501
+ throw new Error("\u6A21\u677F\u538B\u7F29\u5305\u8D85\u8FC7 200 MB \u9650\u5236");
502
+ }
503
+ await writeFile2(archiveFile, archiveBuffer);
504
+ } else {
505
+ const { copyFile } = await import("fs/promises");
506
+ await copyFile(path5.resolve(source), archiveFile);
507
+ }
508
+ await mkdir3(cacheStaging.templateRoot, { recursive: true });
509
+ await extractTar({ file: archiveFile, cwd: cacheStaging.templateRoot });
510
+ await normalizeExtractedTemplate(
511
+ cacheStaging.stagingRoot,
512
+ cacheStaging.templateRoot
513
+ );
514
+ await rm2(archiveFile, { force: true });
515
+ await cacheStaging.commit();
516
+ return {
517
+ root: path5.join(cacheStaging.entryRoot, "template"),
518
+ source: `${source} (archive)`,
519
+ async cleanup() {
520
+ }
521
+ };
522
+ } catch (error) {
523
+ await cacheStaging.cleanup();
524
+ throw error;
525
+ }
526
+ }
527
+ async function acquireTemplate(source, ref, options = {}) {
528
+ const localPath = path5.resolve(source);
529
+ if (existsSync5(localPath)) {
530
+ if (await isDirectory(localPath)) {
531
+ return {
532
+ root: localPath,
533
+ source: localPath,
534
+ async cleanup() {
535
+ }
536
+ };
537
+ }
538
+ if (isArchiveSource(localPath)) {
539
+ return acquireArchiveTemplate(localPath, ref, options);
540
+ }
541
+ throw new Error(`\u6A21\u677F\u6E90\u4E0D\u662F\u76EE\u5F55\u6216\u652F\u6301\u7684 tar \u538B\u7F29\u5305: ${localPath}`);
542
+ }
543
+ if (isArchiveSource(source)) {
544
+ return acquireArchiveTemplate(source, ref, options);
545
+ }
546
+ if (!isGitSource(source)) {
547
+ throw new Error(`\u6A21\u677F\u6E90\u4E0D\u5B58\u5728: ${source}`);
548
+ }
549
+ const cached = await getFreshCache(source, ref, options);
550
+ if (cached) return cached;
551
+ const cacheStaging = await createTemplateCacheStaging(source, ref);
552
+ try {
553
+ await runCommand(
554
+ "git",
555
+ [
556
+ "-c",
557
+ "http.lowSpeedLimit=1024",
558
+ "-c",
559
+ "http.lowSpeedTime=15",
560
+ "clone",
561
+ "--depth",
562
+ "1",
563
+ "--branch",
564
+ ref,
565
+ source,
566
+ cacheStaging.templateRoot
567
+ ],
568
+ { stdio: "pipe", env: { GIT_TERMINAL_PROMPT: "0" } }
569
+ );
570
+ await cacheStaging.commit();
571
+ return {
572
+ root: path5.join(cacheStaging.entryRoot, "template"),
573
+ source: `${source}#${ref}`,
574
+ async cleanup() {
575
+ }
576
+ };
577
+ } catch (error) {
578
+ await cacheStaging.cleanup();
579
+ throw error;
580
+ }
581
+ }
582
+ async function acquireTemplateFromSources(sources, ref, options = {}) {
583
+ if (!sources.length) throw new Error("\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u677F\u6E90");
584
+ const failures = [];
585
+ for (const source of [...new Set(sources)]) {
586
+ try {
587
+ return await acquireTemplate(source, ref, options);
588
+ } catch (error) {
589
+ failures.push(`${source}: ${error.message}`);
590
+ }
591
+ }
592
+ throw new Error(`\u6240\u6709\u6A21\u677F\u6E90\u5747\u4E0D\u53EF\u7528\uFF1A
593
+ - ${failures.join("\n- ")}`);
594
+ }
595
+
596
+ // src/commands/create.ts
597
+ import * as prompts2 from "@clack/prompts";
598
+
599
+ // src/core/project-generator.ts
600
+ import { existsSync as existsSync6 } from "fs";
601
+ import { rename as rename3, rm as rm3 } from "fs/promises";
602
+ import path7 from "path";
603
+ import * as prompts from "@clack/prompts";
604
+ import { satisfies } from "semver";
605
+
606
+ // src/core/template-manifest.ts
607
+ import path6 from "path";
608
+ async function loadTemplateManifest(templateRoot) {
609
+ const manifest = await readJson(
610
+ path6.join(templateRoot, "template.manifest.json")
611
+ );
612
+ const errors = [];
613
+ if (manifest.schemaVersion !== 1) errors.push("schemaVersion \u5FC5\u987B\u4E3A 1");
614
+ if (!manifest.id) errors.push("\u7F3A\u5C11\u6A21\u677F id");
615
+ if (!manifest.name) errors.push("\u7F3A\u5C11\u6A21\u677F\u540D\u79F0");
616
+ if (!/^\d+\.\d+\.\d+([+-].+)?$/.test(manifest.version ?? "")) {
617
+ errors.push("\u6A21\u677F version \u4E0D\u662F\u6709\u6548\u7684\u8BED\u4E49\u5316\u7248\u672C");
618
+ }
619
+ if (!manifest.runtime?.node || !manifest.runtime?.packageManager) {
620
+ errors.push("\u7F3A\u5C11\u6A21\u677F\u8FD0\u884C\u65F6\u7EA6\u675F");
621
+ }
622
+ if (!manifest.entry?.nonInteractive) {
623
+ errors.push("\u7F3A\u5C11\u975E\u4EA4\u4E92\u521D\u59CB\u5316\u5165\u53E3");
624
+ }
625
+ const featureIds = /* @__PURE__ */ new Set();
626
+ for (const feature of manifest.features ?? []) {
627
+ if (!/^[a-z][a-z0-9-]*$/.test(feature.id)) {
628
+ errors.push(`\u6A21\u677F\u80FD\u529B id \u65E0\u6548: ${feature.id}`);
629
+ }
630
+ if (featureIds.has(feature.id)) {
631
+ errors.push(`\u6A21\u677F\u80FD\u529B id \u91CD\u590D: ${feature.id}`);
632
+ }
633
+ featureIds.add(feature.id);
634
+ if (!feature.name || !feature.description) {
635
+ errors.push(`\u6A21\u677F\u80FD\u529B ${feature.id} \u7F3A\u5C11\u540D\u79F0\u6216\u8BF4\u660E`);
636
+ }
637
+ }
638
+ if (errors.length) {
639
+ throw new Error(`\u6A21\u677F manifest \u65E0\u6548\uFF1A
640
+ - ${errors.join("\n- ")}`);
641
+ }
642
+ return manifest;
643
+ }
644
+
645
+ // src/core/project-generator.ts
646
+ var ENV_NAMES = ["dev", "sit", "uat", "pre", "prd"];
647
+ function cancelled(value) {
648
+ return prompts.isCancel(value);
649
+ }
650
+ function ensureProjectName(value) {
651
+ const name = value.trim();
652
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
653
+ throw new Error("\u9879\u76EE\u540D\u79F0\u53EA\u80FD\u5305\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u548C\u8FDE\u5B57\u7B26");
654
+ }
655
+ return name;
656
+ }
657
+ function inferModuleName(projectName) {
658
+ const inferred = projectName.replace(/^(?:jh4j|wl)-ui-/, "").replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
659
+ return /^[a-z][a-z0-9-]*$/.test(inferred) ? inferred : "app";
660
+ }
661
+ function ensureModuleName(value) {
662
+ const name = value.trim();
663
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
664
+ throw new Error("\u6A21\u5757\u6807\u8BC6\u5FC5\u987B\u4EE5\u5C0F\u5199\u5B57\u6BCD\u5F00\u5934\uFF0C\u53EA\u80FD\u5305\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26");
665
+ }
666
+ return name;
667
+ }
668
+ function ensurePort(value) {
669
+ const port = Number(value);
670
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
671
+ throw new Error("\u5F00\u53D1\u7AEF\u53E3\u5FC5\u987B\u662F 1024 \u5230 65535 \u4E4B\u95F4\u7684\u6574\u6570");
672
+ }
673
+ return port;
674
+ }
675
+ function ensureHttpUrl(value, label) {
676
+ try {
677
+ const url = new URL(value);
678
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error();
679
+ return value.replace(/\/+$/, "");
680
+ } catch {
681
+ throw new Error(`${label} \u5FC5\u987B\u662F http/https URL`);
682
+ }
683
+ }
684
+ async function askText(message, initialValue) {
685
+ const answer = await prompts.text({ message, initialValue });
686
+ if (cancelled(answer)) throw new Error("\u7528\u6237\u53D6\u6D88\u521B\u5EFA");
687
+ return String(answer).trim() || initialValue;
688
+ }
689
+ async function loadCreateConfig(configFile, cwd) {
690
+ if (!configFile) return {};
691
+ const resolved = path7.resolve(cwd, configFile);
692
+ if (!existsSync6(resolved)) throw new Error(`\u521B\u5EFA\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${resolved}`);
693
+ return readJson(resolved);
694
+ }
695
+ function parseFeatureIds(value) {
696
+ return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
697
+ }
698
+ async function collectTemplateFeatures(manifest, fileConfig, options) {
699
+ const available = manifest.features ?? [];
700
+ if (!available.length) return [];
701
+ const knownIds = new Set(available.map((feature) => feature.id));
702
+ let selected = options.features ? parseFeatureIds(options.features) : Array.isArray(fileConfig.features) ? fileConfig.features : available.filter((feature) => feature.defaultEnabled || feature.required).map((feature) => feature.id);
703
+ const unknown = selected.filter((id) => !knownIds.has(id));
704
+ if (unknown.length) {
705
+ throw new Error(
706
+ `\u6A21\u677F\u4E0D\u652F\u6301\u4EE5\u4E0B\u80FD\u529B: ${unknown.join(", ")}\u3002\u53EF\u7528\u80FD\u529B: ${[...knownIds].join(", ")}`
707
+ );
708
+ }
709
+ if (options.standards === false) {
710
+ selected = selected.filter((id) => {
711
+ const feature = available.find((item) => item.id === id);
712
+ return feature?.package !== "@robot-admin/git-standards";
713
+ });
714
+ }
715
+ if (!options.yes) {
716
+ const selectable = available.filter(
717
+ (feature) => options.standards !== false || feature.package !== "@robot-admin/git-standards"
718
+ );
719
+ if (selectable.length) {
720
+ const answer = await prompts.multiselect({
721
+ message: "\u9009\u62E9\u9879\u76EE\u6807\u51C6\u5316\u80FD\u529B",
722
+ options: selectable.map((feature) => ({
723
+ value: feature.id,
724
+ label: feature.name,
725
+ hint: `${feature.description}${feature.package ? ` \xB7 ${feature.package}` : ""}`
726
+ })),
727
+ initialValues: selected,
728
+ required: false
729
+ });
730
+ if (cancelled(answer)) throw new Error("\u7528\u6237\u53D6\u6D88\u521B\u5EFA");
731
+ selected = answer.map(String);
732
+ }
733
+ }
734
+ for (const feature of available) {
735
+ if (feature.required && !selected.includes(feature.id)) {
736
+ selected.push(feature.id);
737
+ }
738
+ }
739
+ return selected;
740
+ }
741
+ async function collectProjectInput(projectName, sourceConfig, fileConfig, options, userConfig, features) {
742
+ let moduleName = options.module ?? fileConfig.moduleName ?? inferModuleName(projectName);
743
+ let title = options.title ?? fileConfig.title ?? sourceConfig.title;
744
+ let port = options.port ?? fileConfig.devServerPort ?? sourceConfig.devServerPort;
745
+ let npmRegistry = options.npmRegistry ?? fileConfig.npmRegistry ?? userConfig.npmRegistry ?? sourceConfig.npmRegistry;
746
+ let jhlcRegistry = options.jhlcRegistry ?? fileConfig.jhlcRegistry ?? userConfig.jhlcRegistry ?? sourceConfig.jhlcRegistry;
747
+ let localBackendUrl = options.localBackend ?? fileConfig.localBackendUrl ?? sourceConfig.localBackendUrl;
748
+ let localPublicUrl = options.localPublic ?? fileConfig.localPublicUrl ?? sourceConfig.localPublicUrl;
749
+ const environments = structuredClone(sourceConfig.environments);
750
+ for (const [env, value] of Object.entries(fileConfig.environments ?? {})) {
751
+ if (value && environments[env]) {
752
+ environments[env] = { ...environments[env], ...value };
753
+ }
754
+ }
755
+ if (!options.yes) {
756
+ moduleName = await askText("\u6A21\u5757\u6807\u8BC6", moduleName);
757
+ title = await askText("\u7CFB\u7EDF\u6807\u9898", title);
758
+ port = await askText("\u5F00\u53D1\u7AEF\u53E3", String(port));
759
+ npmRegistry = await askText("npm registry", npmRegistry);
760
+ jhlcRegistry = await askText("@jhlc \u79C1\u6709 registry", jhlcRegistry);
761
+ localBackendUrl = await askText("\u672C\u5730\u540E\u7AEF\u5730\u5740", localBackendUrl);
762
+ localPublicUrl = await askText("\u672C\u5730 public \u5730\u5740", localPublicUrl);
763
+ const configureEnvironments = await prompts.confirm({
764
+ message: "\u662F\u5426\u9010\u9879\u786E\u8BA4\u4E94\u5957\u73AF\u5883\u5730\u5740",
765
+ initialValue: false
766
+ });
767
+ if (cancelled(configureEnvironments)) throw new Error("\u7528\u6237\u53D6\u6D88\u521B\u5EFA");
768
+ if (configureEnvironments) {
769
+ for (const env of ENV_NAMES) {
770
+ environments[env].webUrl = await askText(
771
+ `${env.toUpperCase()} \u5E73\u53F0\u5730\u5740`,
772
+ environments[env].webUrl
773
+ );
774
+ environments[env].apiPrefix = await askText(
775
+ `${env.toUpperCase()} API \u524D\u7F00`,
776
+ environments[env].apiPrefix
777
+ );
778
+ }
779
+ }
780
+ }
781
+ if (!title.trim()) throw new Error("\u7CFB\u7EDF\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A");
782
+ for (const env of ENV_NAMES) {
783
+ if (!environments[env]) throw new Error(`\u7F3A\u5C11 ${env.toUpperCase()} \u73AF\u5883\u914D\u7F6E`);
784
+ environments[env] = {
785
+ webUrl: ensureHttpUrl(environments[env].webUrl, `${env.toUpperCase()} \u5E73\u53F0\u5730\u5740`),
786
+ apiPrefix: environments[env].apiPrefix.replace(/^\/+|\/+$/g, "")
787
+ };
788
+ if (!environments[env].apiPrefix) {
789
+ throw new Error(`${env.toUpperCase()} API \u524D\u7F00\u4E0D\u80FD\u4E3A\u7A7A`);
790
+ }
791
+ }
792
+ return {
793
+ projectName,
794
+ moduleName: ensureModuleName(moduleName),
795
+ title: title.trim(),
796
+ devServerPort: ensurePort(port),
797
+ npmRegistry: ensureHttpUrl(npmRegistry, "npm registry"),
798
+ jhlcRegistry: ensureHttpUrl(jhlcRegistry, "@jhlc \u79C1\u6709 registry"),
799
+ localBackendUrl: ensureHttpUrl(localBackendUrl, "\u672C\u5730\u540E\u7AEF\u5730\u5740"),
800
+ localPublicUrl: ensureHttpUrl(localPublicUrl, "\u672C\u5730 public \u5730\u5740"),
801
+ environments,
802
+ features
803
+ };
804
+ }
805
+ function assertSafeTarget(cwd, target) {
806
+ if (path7.dirname(target) !== cwd || target === cwd) {
807
+ throw new Error("\u4EC5\u5141\u8BB8\u5728\u5F53\u524D\u76EE\u5F55\u521B\u5EFA\u4E00\u7EA7\u9879\u76EE\u76EE\u5F55");
808
+ }
809
+ }
810
+ async function promoteStaging(stagingRoot, targetRoot, force) {
811
+ const targetExists = existsSync6(targetRoot);
812
+ if (!targetExists) {
813
+ await rename3(stagingRoot, targetRoot);
814
+ return;
815
+ }
816
+ if (!await isDirectoryEmpty(targetRoot) && !force) {
817
+ throw new Error(`\u76EE\u6807\u76EE\u5F55\u5DF2\u5B58\u5728\u4E14\u975E\u7A7A: ${targetRoot}`);
818
+ }
819
+ const backupRoot = `${targetRoot}.jh4j-backup-${Date.now()}`;
820
+ await rename3(targetRoot, backupRoot);
821
+ try {
822
+ await rename3(stagingRoot, targetRoot);
823
+ } catch (error) {
824
+ await rename3(backupRoot, targetRoot);
825
+ throw error;
826
+ }
827
+ try {
828
+ await removePath(backupRoot);
829
+ } catch {
830
+ prompts.log.warn(`\u65E7\u76EE\u5F55\u5907\u4EFD\u672A\u80FD\u81EA\u52A8\u6E05\u7406: ${backupRoot}`);
831
+ }
832
+ }
833
+ async function generateProject(catalogTemplate, requestedProjectName, options, cwd = process.cwd(), configuredUserConfig = DEFAULT_USER_CONFIG) {
834
+ const userConfig = { ...DEFAULT_USER_CONFIG, ...configuredUserConfig };
835
+ const projectName = ensureProjectName(requestedProjectName);
836
+ const resolvedCwd = path7.resolve(cwd);
837
+ const targetRoot = path7.resolve(resolvedCwd, projectName);
838
+ assertSafeTarget(resolvedCwd, targetRoot);
839
+ if (existsSync6(targetRoot) && !await isDirectoryEmpty(targetRoot) && !options.force) {
840
+ throw new Error(`\u76EE\u6807\u76EE\u5F55\u5DF2\u5B58\u5728\u4E14\u975E\u7A7A: ${targetRoot}`);
841
+ }
842
+ const ref = options.ref ?? userConfig.templateRef ?? catalogTemplate.defaultRef;
843
+ const sourceValues = resolveTemplateSources(
844
+ catalogTemplate,
845
+ options.source,
846
+ userConfig.templateSource
847
+ );
848
+ const acquired = await acquireTemplateFromSources(sourceValues, ref, {
849
+ noCache: options.cache === false,
850
+ cacheTtlMinutes: userConfig.cacheTtlMinutes
851
+ });
852
+ const stagingRoot = path7.join(
853
+ resolvedCwd,
854
+ `.${projectName}.jh4j-tmp-${process.pid}-${Date.now()}`
855
+ );
856
+ let promoted = false;
857
+ try {
858
+ const manifest = await loadTemplateManifest(acquired.root);
859
+ if (manifest.id !== catalogTemplate.id) {
860
+ throw new Error(
861
+ `\u6A21\u677F ID \u4E0D\u5339\u914D\uFF1ACatalog=${catalogTemplate.id}\uFF0CManifest=${manifest.id}`
862
+ );
863
+ }
864
+ if (!satisfies(process.versions.node, manifest.runtime.node)) {
865
+ throw new Error(
866
+ `\u5F53\u524D Node ${process.versions.node} \u4E0D\u6EE1\u8DB3\u6A21\u677F\u8981\u6C42 ${manifest.runtime.node}`
867
+ );
868
+ }
869
+ const sourceProjectConfig = await readJson(path7.join(acquired.root, "project.config.json"));
870
+ const fileConfig = await loadCreateConfig(options.config, resolvedCwd);
871
+ const features = await collectTemplateFeatures(manifest, fileConfig, options);
872
+ const input = await collectProjectInput(
873
+ projectName,
874
+ {
875
+ ...sourceProjectConfig,
876
+ npmRegistry: manifest.defaults.npmRegistry,
877
+ jhlcRegistry: manifest.defaults.jhlcRegistry
878
+ },
879
+ fileConfig,
880
+ options,
881
+ userConfig,
882
+ features
883
+ );
884
+ const shouldInstall = !options.skipInstall && userConfig.autoInstall;
885
+ const shouldInitializeGit = !options.skipGit && userConfig.autoGit;
886
+ if (options.dryRun) {
887
+ prompts.note(
888
+ [
889
+ `\u6A21\u677F: ${manifest.id}@${manifest.version}`,
890
+ `\u6765\u6E90: ${acquired.source}`,
891
+ `\u76EE\u6807: ${targetRoot}`,
892
+ `\u6A21\u5757: ${input.moduleName}`,
893
+ `\u7AEF\u53E3: ${input.devServerPort}`,
894
+ `\u80FD\u529B: ${input.features.length ? input.features.join(", ") : "\u65E0\u53EF\u9009\u80FD\u529B"}`,
895
+ `\u5B89\u88C5\u4F9D\u8D56: ${shouldInstall ? "\u662F" : "\u5426"}`,
896
+ `\u521D\u59CB\u5316 Git: ${shouldInitializeGit ? "\u662F" : "\u5426"}`
897
+ ].join("\n"),
898
+ "Dry Run"
899
+ );
900
+ return {
901
+ targetRoot,
902
+ templateId: manifest.id,
903
+ templateVersion: manifest.version,
904
+ source: acquired.source,
905
+ features: input.features,
906
+ installed: false,
907
+ gitInitialized: false
908
+ };
909
+ }
910
+ await copyTemplateTree(acquired.root, stagingRoot);
911
+ const inputFile = path7.join(stagingRoot, ".jh4j-cli-input.json");
912
+ await writeJson(inputFile, input);
913
+ try {
914
+ await runCommand(
915
+ process.execPath,
916
+ [
917
+ path7.join(stagingRoot, "scripts", "setup-project.mjs"),
918
+ "--yes",
919
+ "--config",
920
+ inputFile,
921
+ "--created-by",
922
+ `${CLI_NAME}@${CLI_VERSION}`
923
+ ],
924
+ { cwd: stagingRoot, stdio: "pipe" }
925
+ );
926
+ } finally {
927
+ await rm3(inputFile, { force: true });
928
+ }
929
+ if (!existsSync6(path7.join(stagingRoot, manifest.generatedMetadata))) {
930
+ throw new Error(`\u6A21\u677F\u521D\u59CB\u5316\u672A\u751F\u6210\u5143\u6570\u636E: ${manifest.generatedMetadata}`);
931
+ }
932
+ await promoteStaging(stagingRoot, targetRoot, Boolean(options.force));
933
+ promoted = true;
934
+ let gitInitialized = false;
935
+ if (shouldInitializeGit) {
936
+ await runCommand("git", ["init", "-b", "main"], {
937
+ cwd: targetRoot,
938
+ stdio: "pipe"
939
+ });
940
+ gitInitialized = true;
941
+ }
942
+ if (shouldInstall) {
943
+ try {
944
+ await runCommand("pnpm", ["install"], { cwd: targetRoot });
945
+ } catch (error) {
946
+ throw new Error(
947
+ `\u9879\u76EE\u5DF2\u751F\u6210\uFF0C\u4F46\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25\uFF1B\u76EE\u5F55\u5DF2\u4FDD\u7559\u5728 ${targetRoot}
948
+ ${error.message}`
949
+ );
950
+ }
951
+ }
952
+ if (shouldInitializeGit) {
953
+ await runCommand("git", ["add", "-A"], { cwd: targetRoot, stdio: "pipe" });
954
+ try {
955
+ await runCommand(
956
+ "git",
957
+ ["commit", "-m", `feat(init): \u57FA\u4E8E ${manifest.id}@${manifest.version} \u521D\u59CB\u5316`],
958
+ {
959
+ cwd: targetRoot,
960
+ stdio: "pipe",
961
+ env: { HUSKY: "0" }
962
+ }
963
+ );
964
+ } catch (error) {
965
+ prompts.log.warn(`Git \u5DF2\u521D\u59CB\u5316\uFF0C\u4F46\u521D\u59CB\u63D0\u4EA4\u672A\u5B8C\u6210\uFF1A${error.message}`);
966
+ }
967
+ }
968
+ return {
969
+ targetRoot,
970
+ templateId: manifest.id,
971
+ templateVersion: manifest.version,
972
+ source: acquired.source,
973
+ features: input.features,
974
+ installed: shouldInstall,
975
+ gitInitialized
976
+ };
977
+ } finally {
978
+ if (!promoted && existsSync6(stagingRoot)) await removePath(stagingRoot);
979
+ await acquired.cleanup();
980
+ }
981
+ }
982
+
983
+ // src/commands/create.ts
984
+ var CATEGORY_OPTIONS = [
985
+ { value: "frontend", label: "\u524D\u7AEF", hint: "PC\u3001Web \u4E0E\u5FAE\u524D\u7AEF\u5E94\u7528" },
986
+ { value: "backend", label: "\u540E\u7AEF", hint: "Java \u670D\u52A1\u4E0E\u4E91\u539F\u751F\u5E94\u7528" },
987
+ { value: "mobile", label: "\u79FB\u52A8\u7AEF", hint: "App\u3001H5 \u4E0E\u8DE8\u7AEF\u5E94\u7528" }
988
+ ];
989
+ function isCategory(value) {
990
+ return CATEGORY_OPTIONS.some((item) => item.value === value);
991
+ }
992
+ function templatesByCategory(templates, category) {
993
+ return templates.filter((template) => template.category === category);
994
+ }
995
+ async function selectCategory(templates, requestedCategory, nonInteractive) {
996
+ if (requestedCategory) {
997
+ if (!isCategory(requestedCategory)) {
998
+ throw new Error(
999
+ `\u9879\u76EE\u7C7B\u578B\u65E0\u6548: ${requestedCategory}\u3002\u53EF\u7528\u503C: frontend\u3001backend\u3001mobile`
1000
+ );
1001
+ }
1002
+ if (!templatesByCategory(templates, requestedCategory).length) {
1003
+ throw new Error(`\u9879\u76EE\u7C7B\u578B ${requestedCategory} \u6682\u65E0\u53EF\u7528\u6A21\u677F`);
1004
+ }
1005
+ return requestedCategory;
1006
+ }
1007
+ const firstAvailable = CATEGORY_OPTIONS.find(
1008
+ (item) => templatesByCategory(templates, item.value).length > 0
1009
+ );
1010
+ if (!firstAvailable) throw new Error("Catalog \u4E2D\u6CA1\u6709\u53EF\u7528\u6A21\u677F");
1011
+ if (nonInteractive) return firstAvailable.value;
1012
+ const selected = await prompts2.select({
1013
+ message: "\u9009\u62E9\u9879\u76EE\u7C7B\u578B",
1014
+ options: CATEGORY_OPTIONS.map((item) => {
1015
+ const count = templatesByCategory(templates, item.value).length;
1016
+ return {
1017
+ value: item.value,
1018
+ label: item.label,
1019
+ hint: count ? `${item.hint} \xB7 ${count} \u4E2A\u6A21\u677F` : `${item.hint} \xB7 \u6682\u672A\u63D0\u4F9B`,
1020
+ disabled: count === 0
1021
+ };
1022
+ })
1023
+ });
1024
+ if (prompts2.isCancel(selected)) throw new Error("\u7528\u6237\u53D6\u6D88\u521B\u5EFA");
1025
+ return selected;
1026
+ }
1027
+ async function selectTemplate(templates, options) {
1028
+ if (options.template) {
1029
+ const selected2 = findTemplate(templates, options.template);
1030
+ if (options.category && selected2.category !== options.category) {
1031
+ throw new Error(
1032
+ `\u6A21\u677F ${selected2.id} \u5C5E\u4E8E ${selected2.category}\uFF0C\u4E0E --category ${options.category} \u4E0D\u4E00\u81F4`
1033
+ );
1034
+ }
1035
+ return selected2;
1036
+ }
1037
+ const category = await selectCategory(
1038
+ templates,
1039
+ options.category,
1040
+ Boolean(options.yes)
1041
+ );
1042
+ const candidates = templatesByCategory(templates, category);
1043
+ if (options.yes) return findTemplate(candidates);
1044
+ const selected = await prompts2.select({
1045
+ message: "\u9009\u62E9\u9879\u76EE\u6A21\u677F",
1046
+ options: candidates.map((template) => ({
1047
+ value: template.id,
1048
+ label: template.name,
1049
+ hint: `${template.status} \xB7 ${template.description}`
1050
+ }))
1051
+ });
1052
+ if (prompts2.isCancel(selected)) throw new Error("\u7528\u6237\u53D6\u6D88\u521B\u5EFA");
1053
+ return findTemplate(candidates, String(selected));
1054
+ }
1055
+ async function createCommand(requestedName, options) {
1056
+ prompts2.intro("JH4J Cloud \u9879\u76EE\u521B\u5EFA");
1057
+ const userConfig = await loadUserConfig();
1058
+ const catalog = await loadCatalog(userConfig);
1059
+ let projectName = requestedName;
1060
+ if (!projectName) {
1061
+ if (options.yes) throw new Error("\u975E\u4EA4\u4E92\u6A21\u5F0F\u5FC5\u987B\u6307\u5B9A\u9879\u76EE\u540D\u79F0");
1062
+ const answer = await prompts2.text({
1063
+ message: "\u9879\u76EE\u540D\u79F0",
1064
+ placeholder: "jh4j-ui-app"
1065
+ });
1066
+ if (prompts2.isCancel(answer)) throw new Error("\u7528\u6237\u53D6\u6D88\u521B\u5EFA");
1067
+ projectName = String(answer);
1068
+ }
1069
+ const template = await selectTemplate(catalog, options);
1070
+ const result = await generateProject(
1071
+ template,
1072
+ projectName,
1073
+ options,
1074
+ process.cwd(),
1075
+ userConfig
1076
+ );
1077
+ if (options.dryRun) {
1078
+ prompts2.outro("Dry Run \u5B8C\u6210\uFF0C\u672A\u5199\u5165\u4EFB\u4F55\u9879\u76EE\u6587\u4EF6");
1079
+ return;
1080
+ }
1081
+ prompts2.note(
1082
+ [
1083
+ `\u76EE\u5F55: ${result.targetRoot}`,
1084
+ `\u6A21\u677F: ${result.templateId}@${result.templateVersion}`,
1085
+ `\u6765\u6E90: ${result.source}`,
1086
+ `\u80FD\u529B: ${result.features.length ? result.features.join(", ") : "\u65E0\u53EF\u9009\u80FD\u529B"}`,
1087
+ `\u4F9D\u8D56: ${result.installed ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5"}`,
1088
+ `Git: ${result.gitInitialized ? "main \u5DF2\u521D\u59CB\u5316" : "\u672A\u521D\u59CB\u5316"}`,
1089
+ "",
1090
+ `cd ${projectName}`,
1091
+ ...!result.installed ? ["pnpm install"] : [],
1092
+ "pnpm dev"
1093
+ ].join("\n"),
1094
+ "\u521B\u5EFA\u6210\u529F"
1095
+ );
1096
+ prompts2.outro("Happy coding!");
1097
+ }
1098
+
1099
+ // src/commands/doctor.ts
1100
+ import { existsSync as existsSync7 } from "fs";
1101
+ import { mkdir as mkdir4, writeFile as writeFile3, rm as rm4 } from "fs/promises";
1102
+ import path8 from "path";
1103
+ import { coerce, gte, satisfies as satisfies2 } from "semver";
1104
+ async function checkHomeWritable() {
1105
+ const home = getJh4jHome();
1106
+ const probe = path8.join(home, `.write-probe-${process.pid}`);
1107
+ try {
1108
+ await mkdir4(home, { recursive: true });
1109
+ await writeFile3(probe, "ok", "utf8");
1110
+ await rm4(probe, { force: true });
1111
+ return { name: "JH4J_HOME", ok: true, detail: home };
1112
+ } catch (error) {
1113
+ return { name: "JH4J_HOME", ok: false, detail: error.message };
1114
+ }
1115
+ }
1116
+ async function collectDoctorChecks(suppliedConfig, suppliedCatalog) {
1117
+ const config = suppliedConfig ?? await loadUserConfig();
1118
+ const catalog = suppliedCatalog ?? await loadCatalog(config);
1119
+ const [git, pnpm, home] = await Promise.all([
1120
+ inspectCommand("git"),
1121
+ inspectCommand("pnpm"),
1122
+ checkHomeWritable()
1123
+ ]);
1124
+ const pnpmVersion = coerce(pnpm.output)?.version;
1125
+ const checks = [
1126
+ {
1127
+ name: "Node.js",
1128
+ ok: satisfies2(process.versions.node, "^22.12.0 || ^24.0.0"),
1129
+ detail: `${process.versions.node}\uFF08\u8981\u6C42 Node 22.12+ \u6216 24.x\uFF09`
1130
+ },
1131
+ { name: "Git", ok: git.ok, detail: git.output },
1132
+ {
1133
+ name: "pnpm",
1134
+ ok: pnpm.ok && Boolean(pnpmVersion && gte(pnpmVersion, "11.8.0")),
1135
+ detail: pnpm.output
1136
+ },
1137
+ home
1138
+ ];
1139
+ for (const template of catalog) {
1140
+ const sources = resolveTemplateSources(
1141
+ template,
1142
+ void 0,
1143
+ config.templateSource
1144
+ );
1145
+ const source = sources[0];
1146
+ if (!existsSync7(source)) {
1147
+ checks.push({
1148
+ name: `\u6A21\u677F ${template.id}`,
1149
+ ok: source.startsWith("http://") || source.startsWith("https://") || source.startsWith("git@"),
1150
+ detail: `\u8FDC\u7A0B\u5019\u9009\u6E90\uFF08\u521B\u5EFA\u65F6\u4F9D\u6B21\u9A8C\u8BC1\uFF09: ${sources.join(" \u2192 ")}`
1151
+ });
1152
+ continue;
1153
+ }
1154
+ try {
1155
+ const manifest = await loadTemplateManifest(source);
1156
+ checks.push({
1157
+ name: `\u6A21\u677F ${template.id}`,
1158
+ ok: manifest.id === template.id && satisfies2(process.versions.node, manifest.runtime.node),
1159
+ detail: `${manifest.id}@${manifest.version} (${source})`
1160
+ });
1161
+ } catch (error) {
1162
+ checks.push({
1163
+ name: `\u6A21\u677F ${template.id}`,
1164
+ ok: false,
1165
+ detail: error.message
1166
+ });
1167
+ }
1168
+ }
1169
+ return checks;
1170
+ }
1171
+ async function doctorCommand(options = {}) {
1172
+ const checks = await collectDoctorChecks();
1173
+ const failed = checks.filter((check) => !check.ok);
1174
+ if (options.json) {
1175
+ console.log(JSON.stringify({ ok: failed.length === 0, checks }, null, 2));
1176
+ } else {
1177
+ for (const check of checks) {
1178
+ console.log(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}`);
1179
+ }
1180
+ console.log(`
1181
+ \u7ED3\u679C: ${checks.length - failed.length} \u901A\u8FC7, ${failed.length} \u5931\u8D25`);
1182
+ }
1183
+ if (failed.length) process.exitCode = 1;
1184
+ }
1185
+
1186
+ // src/commands/info.ts
1187
+ import path9 from "path";
1188
+ import { existsSync as existsSync8 } from "fs";
1189
+ async function infoCommand(projectPath = process.cwd(), options = {}) {
1190
+ const root = path9.resolve(projectPath);
1191
+ const metadataPath = path9.join(root, ".jhlc", "project.json");
1192
+ if (!existsSync8(metadataPath)) {
1193
+ throw new Error(`\u5F53\u524D\u76EE\u5F55\u4E0D\u662F JH4J CLI \u521B\u5EFA\u7684\u9879\u76EE: ${metadataPath}`);
1194
+ }
1195
+ const metadata = await readJson(metadataPath);
1196
+ if (options.json) {
1197
+ console.log(JSON.stringify({ projectRoot: root, ...metadata }, null, 2));
1198
+ return;
1199
+ }
1200
+ console.log(`\u9879\u76EE\u76EE\u5F55: ${root}`);
1201
+ console.log(`\u6A21\u677F: ${metadata.template.id}@${metadata.template.version}`);
1202
+ console.log(`\u5E73\u53F0\u7248\u672C: ${metadata.platformVersion ?? "\u672A\u58F0\u660E"}`);
1203
+ console.log(`\u521B\u5EFA\u65F6\u95F4: ${metadata.createdAt}`);
1204
+ console.log(`\u521B\u5EFA\u5DE5\u5177: ${metadata.createdBy}`);
1205
+ console.log("\u9879\u76EE\u53C2\u6570:");
1206
+ console.log(JSON.stringify(metadata.parameters, null, 2));
1207
+ }
1208
+
1209
+ // src/commands/list.ts
1210
+ async function listCommand(options = {}) {
1211
+ const templates = await loadCatalog(await loadUserConfig());
1212
+ if (options.json) {
1213
+ console.log(JSON.stringify(templates, null, 2));
1214
+ return;
1215
+ }
1216
+ console.table(
1217
+ templates.map((template) => ({
1218
+ ID: template.id,
1219
+ \u540D\u79F0: template.name,
1220
+ \u7C7B\u578B: template.category,
1221
+ \u72B6\u6001: template.status,
1222
+ \u9ED8\u8BA4\u5206\u652F: template.defaultRef,
1223
+ \u6807\u7B7E: template.tags?.join(", ") ?? ""
1224
+ }))
1225
+ );
1226
+ }
1227
+
1228
+ // src/commands/template.ts
1229
+ import path10 from "path";
1230
+ async function validateTemplateCommand(templatePath) {
1231
+ const root = path10.resolve(templatePath);
1232
+ const manifest = await loadTemplateManifest(root);
1233
+ await runCommand(process.execPath, [path10.join(root, "scripts", "validate-template.mjs")], {
1234
+ cwd: root
1235
+ });
1236
+ console.log(`\u6A21\u677F\u53EF\u7528: ${manifest.id}@${manifest.version}`);
1237
+ }
1238
+
1239
+ // src/commands/config.ts
1240
+ async function configListCommand(options = {}) {
1241
+ const config = await loadUserConfig();
1242
+ if (options.json) console.log(JSON.stringify(config, null, 2));
1243
+ else console.table(Object.entries(config).map(([key, value]) => ({ key, value })));
1244
+ console.log(`\u914D\u7F6E\u6587\u4EF6: ${getUserConfigPath()}`);
1245
+ }
1246
+ async function configGetCommand(key) {
1247
+ const config = await loadUserConfig();
1248
+ if (!(key in config)) throw new Error(`\u914D\u7F6E\u9879\u4E0D\u5B58\u5728: ${key}`);
1249
+ console.log(String(config[key]));
1250
+ }
1251
+ async function configSetCommand(key, value) {
1252
+ const config = await loadUserConfig();
1253
+ const [typedKey, typedValue] = parseUserConfigValue(key, value);
1254
+ config[typedKey] = typedValue;
1255
+ await saveUserConfig(config);
1256
+ console.log(`\u5DF2\u8BBE\u7F6E ${typedKey}=${String(typedValue)}`);
1257
+ }
1258
+ async function configUnsetCommand(key) {
1259
+ const config = await loadUserConfig();
1260
+ if (!(key in DEFAULT_USER_CONFIG) && !(key in config)) {
1261
+ throw new Error(`\u914D\u7F6E\u9879\u4E0D\u5B58\u5728: ${key}`);
1262
+ }
1263
+ const defaults = DEFAULT_USER_CONFIG;
1264
+ const mutable = config;
1265
+ if (key in defaults) mutable[key] = defaults[key];
1266
+ else delete mutable[key];
1267
+ await saveUserConfig(config);
1268
+ console.log(`\u5DF2\u91CD\u7F6E\u914D\u7F6E\u9879: ${key}`);
1269
+ }
1270
+ async function configResetCommand() {
1271
+ await saveUserConfig({ ...DEFAULT_USER_CONFIG });
1272
+ console.log(`\u7528\u6237\u914D\u7F6E\u5DF2\u6062\u590D\u9ED8\u8BA4\u503C: ${getUserConfigPath()}`);
1273
+ }
1274
+
1275
+ // src/commands/cache.ts
1276
+ function formatBytes(bytes) {
1277
+ if (bytes < 1024) return `${bytes} B`;
1278
+ if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`;
1279
+ return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
1280
+ }
1281
+ async function cacheListCommand(options = {}) {
1282
+ const entries = await listTemplateCache();
1283
+ if (options.json) {
1284
+ console.log(JSON.stringify(entries, null, 2));
1285
+ return;
1286
+ }
1287
+ if (!entries.length) {
1288
+ console.log(`\u6A21\u677F\u7F13\u5B58\u4E3A\u7A7A: ${getTemplateCacheRoot()}`);
1289
+ return;
1290
+ }
1291
+ console.table(
1292
+ entries.map((entry) => ({
1293
+ Key: entry.key,
1294
+ \u6765\u6E90: entry.metadata.source,
1295
+ Ref: entry.metadata.ref,
1296
+ \u7F13\u5B58\u65F6\u95F4: entry.metadata.cachedAt,
1297
+ \u5927\u5C0F: formatBytes(entry.sizeBytes)
1298
+ }))
1299
+ );
1300
+ }
1301
+ async function cacheClearCommand() {
1302
+ await clearTemplateCache();
1303
+ console.log(`\u6A21\u677F\u7F13\u5B58\u5DF2\u6E05\u7406: ${getTemplateCacheRoot()}`);
1304
+ }
1305
+
1306
+ // src/cli.ts
1307
+ function createProgram() {
1308
+ const program = new Command();
1309
+ program.name(COMMAND_NAME).description("JH4J Cloud \u4F01\u4E1A\u5185\u90E8\u6807\u51C6\u5316\u9879\u76EE\u811A\u624B\u67B6").version(CLI_VERSION);
1310
+ program.command("create [name]").description("\u6839\u636E\u6807\u51C6\u6A21\u677F\u521B\u5EFA\u9879\u76EE").option("-c, --category <category>", "\u9879\u76EE\u7C7B\u578B\uFF1Afrontend\u3001backend \u6216 mobile").option("-t, --template <id>", "\u6A21\u677F ID").option("--features <ids>", "\u542F\u7528\u7684\u6A21\u677F\u80FD\u529B\uFF0C\u591A\u4E2A ID \u4F7F\u7528\u9017\u53F7\u5206\u9694").option("--no-standards", "\u4E0D\u542F\u7528\u6A21\u677F\u63D0\u4F9B\u7684 Git/\u4EE3\u7801\u6807\u51C6\u5316\u80FD\u529B").option("--source <path-or-url>", "\u8986\u76D6\u6A21\u677F\u6E90\uFF08\u672C\u5730\u76EE\u5F55\u6216 Git URL\uFF09").option("--ref <branch-or-tag>", "Git \u5206\u652F\u6216\u6807\u7B7E", "main").option("--module <name>", "\u5E73\u53F0\u6A21\u5757\u6807\u8BC6").option("--title <title>", "\u7CFB\u7EDF\u6807\u9898").option("--port <port>", "\u5F00\u53D1\u7AEF\u53E3").option("--npm-registry <url>", "npm registry\uFF08\u9700\u5305\u542B\u4F01\u4E1A\u5B9A\u5236\u5305\uFF09").option("--jhlc-registry <url>", "@jhlc \u79C1\u6709 registry").option("--local-backend <url>", "\u672C\u5730\u540E\u7AEF\u5730\u5740").option("--local-public <url>", "\u672C\u5730 public \u5730\u5740").option("--config <json-file>", "\u4ECE JSON \u6587\u4EF6\u8BFB\u53D6\u521B\u5EFA\u53C2\u6570").option("-y, --yes", "\u63A5\u53D7\u6A21\u677F\u9ED8\u8BA4\u503C\uFF0C\u975E\u4EA4\u4E92\u521B\u5EFA").option("--dry-run", "\u53EA\u9884\u89C8\uFF0C\u4E0D\u5199\u5165\u6587\u4EF6").option("--skip-install", "\u8DF3\u8FC7\u4F9D\u8D56\u5B89\u88C5").option("--skip-git", "\u8DF3\u8FC7 Git \u521D\u59CB\u5316").option("--force", "\u8986\u76D6\u5DF2\u5B58\u5728\u7684\u540C\u540D\u76EE\u5F55").option("--no-cache", "\u4E0D\u4F7F\u7528\u5DF2\u6709\u8FDC\u7A0B\u6A21\u677F\u7F13\u5B58").action(createCommand);
1311
+ program.command("list").description("\u5217\u51FA\u53EF\u7528\u6A21\u677F").option("--json", "\u8F93\u51FA JSON").action(listCommand);
1312
+ program.command("doctor").description("\u68C0\u67E5\u672C\u673A\u73AF\u5883\u548C\u6A21\u677F\u53EF\u7528\u6027").option("--json", "\u8F93\u51FA JSON").action(doctorCommand);
1313
+ program.command("info [path]").description("\u663E\u793A\u5DF2\u751F\u6210\u9879\u76EE\u7684\u6A21\u677F\u4E0E\u7248\u672C\u4FE1\u606F").option("--json", "\u8F93\u51FA JSON").action(infoCommand);
1314
+ const template = program.command("template").description("\u6A21\u677F\u7EF4\u62A4\u547D\u4EE4");
1315
+ template.command("validate [path]").description("\u6821\u9A8C\u6A21\u677F manifest \u4E0E\u6A21\u677F\u5951\u7EA6").action(async (templatePath) => {
1316
+ if (templatePath) return validateTemplateCommand(templatePath);
1317
+ const config2 = await loadUserConfig();
1318
+ const selected = findTemplate(await loadCatalog(config2));
1319
+ const acquired = await acquireTemplateFromSources(
1320
+ resolveTemplateSources(selected, void 0, config2.templateSource),
1321
+ config2.templateRef ?? selected.defaultRef,
1322
+ { cacheTtlMinutes: config2.cacheTtlMinutes }
1323
+ );
1324
+ try {
1325
+ return await validateTemplateCommand(acquired.root);
1326
+ } finally {
1327
+ await acquired.cleanup();
1328
+ }
1329
+ });
1330
+ const config = program.command("config").description("\u7BA1\u7406\u7528\u6237\u9ED8\u8BA4\u914D\u7F6E");
1331
+ config.command("list").option("--json", "\u8F93\u51FA JSON").action(configListCommand);
1332
+ config.command("get <key>").action(configGetCommand);
1333
+ config.command("set <key> <value>").action(configSetCommand);
1334
+ config.command("unset <key>").action(configUnsetCommand);
1335
+ config.command("reset").action(configResetCommand);
1336
+ const cache = program.command("cache").description("\u7BA1\u7406\u8FDC\u7A0B\u6A21\u677F\u7F13\u5B58");
1337
+ cache.command("list").option("--json", "\u8F93\u51FA JSON").action(cacheListCommand);
1338
+ cache.command("clear").action(cacheClearCommand);
1339
+ return program;
1340
+ }
1341
+ async function runCli(argv = process.argv) {
1342
+ await createProgram().parseAsync(argv);
1343
+ }
1344
+
1345
+ // src/index.ts
1346
+ runCli().catch((error) => {
1347
+ console.error(`
1348
+ \u9519\u8BEF: ${error.message}
1349
+ `);
1350
+ process.exitCode = 1;
1351
+ });
1352
+ //# sourceMappingURL=index.js.map