@lightfish/cli 0.0.4 → 0.0.6

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.
Files changed (30) hide show
  1. package/dist/create-lightfish.js +126 -60
  2. package/dist/lightfish.js +527 -63
  3. package/dist/templates/react-vite/.agents/skills/lightfish-server/SKILL.md +447 -0
  4. package/dist/templates/react-vite/.vscode/settings.json +42 -0
  5. package/dist/templates/react-vite/AGENTS.md +254 -0
  6. package/dist/templates/react-vite/README.md +150 -0
  7. package/dist/templates/react-vite/eslint.config.js +25 -0
  8. package/dist/templates/react-vite/index.html +13 -0
  9. package/dist/templates/react-vite/lightfish-server.config.js +6 -0
  10. package/dist/templates/react-vite/package.json +41 -0
  11. package/dist/templates/react-vite/pnpm-lock.yaml +3649 -0
  12. package/dist/templates/react-vite/public/favicon.svg +1 -0
  13. package/dist/templates/react-vite/public/icons.svg +24 -0
  14. package/dist/templates/react-vite/server/router/hello/index.ts +20 -0
  15. package/dist/templates/react-vite/server/schema/index.ts +18 -0
  16. package/dist/templates/react-vite/src/assets/hero.png +0 -0
  17. package/dist/templates/react-vite/src/assets/react.svg +1 -0
  18. package/dist/templates/react-vite/src/assets/vite.svg +1 -0
  19. package/dist/templates/react-vite/src/main.css +1 -0
  20. package/dist/templates/react-vite/src/main.tsx +43 -0
  21. package/dist/templates/react-vite/src/pages/example/index.tsx +26 -0
  22. package/dist/templates/react-vite/src/pages/example/layout.tsx +31 -0
  23. package/dist/templates/react-vite/src/pages/example/model.ts +20 -0
  24. package/dist/templates/react-vite/src/pages/index.tsx +17 -0
  25. package/dist/templates/react-vite/src/pages/layout.tsx +15 -0
  26. package/dist/templates/react-vite/tsconfig.app.json +30 -0
  27. package/dist/templates/react-vite/tsconfig.json +7 -0
  28. package/dist/templates/react-vite/tsconfig.node.json +24 -0
  29. package/dist/templates/react-vite/vite.config.ts +61 -0
  30. package/package.json +6 -2
package/dist/lightfish.js CHANGED
@@ -2,20 +2,42 @@
2
2
 
3
3
  // src/lightfish.ts
4
4
  import cac from "cac";
5
- import { execa as execa3 } from "execa";
5
+ import { execa as execa4 } from "execa";
6
6
 
7
7
  // src/commands/create.ts
8
- import path4 from "path";
8
+ import path6 from "path";
9
9
  import { execa as execa2 } from "execa";
10
- import fs4 from "fs-extra";
10
+ import fs6 from "fs-extra";
11
11
  import prompts3 from "prompts";
12
12
 
13
- // src/utils/download-template.ts
13
+ // src/utils/copy-template.ts
14
14
  import path from "path";
15
+ import fs from "fs-extra";
16
+ var SKIP_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist"]);
17
+ async function copyLocalTemplate(srcDir, targetDir) {
18
+ await fs.ensureDir(targetDir);
19
+ const entries = await fs.readdir(srcDir, { withFileTypes: true });
20
+ for (const entry of entries) {
21
+ if (SKIP_NAMES.has(entry.name)) continue;
22
+ const src = path.join(srcDir, entry.name);
23
+ const dest = path.join(targetDir, entry.name);
24
+ if (entry.isDirectory()) {
25
+ await copyLocalTemplate(src, dest);
26
+ } else if (entry.isSymbolicLink()) {
27
+ const linkTarget = await fs.readlink(src);
28
+ await fs.symlink(linkTarget, dest);
29
+ } else {
30
+ await fs.copy(src, dest);
31
+ }
32
+ }
33
+ }
34
+
35
+ // src/utils/download-template.ts
36
+ import path2 from "path";
15
37
  import { tmpdir } from "os";
16
38
  import { randomUUID } from "crypto";
17
39
  import { execa } from "execa";
18
- import fs from "fs-extra";
40
+ import fs2 from "fs-extra";
19
41
  function parseRepo(repo) {
20
42
  const [repoPart, branch] = repo.split("#");
21
43
  let url;
@@ -29,8 +51,8 @@ function parseRepo(repo) {
29
51
  return { url, branch: branch || void 0 };
30
52
  }
31
53
  async function gitClone(url, branch, targetDir) {
32
- const tmpDir = path.join(tmpdir(), `lightfish-template-${randomUUID()}`);
33
- await fs.ensureDir(tmpDir);
54
+ const tmpDir = path2.join(tmpdir(), `lightfish-template-${randomUUID()}`);
55
+ await fs2.ensureDir(tmpDir);
34
56
  const args = ["clone", "--depth", "1"];
35
57
  if (branch) {
36
58
  args.push("--branch", branch);
@@ -39,30 +61,30 @@ async function gitClone(url, branch, targetDir) {
39
61
  try {
40
62
  await execa("git", args, { stdio: "inherit" });
41
63
  } catch (error) {
42
- await fs.remove(tmpDir).catch(() => {
64
+ await fs2.remove(tmpDir).catch(() => {
43
65
  });
44
66
  const detail = error instanceof Error ? error.message : String(error);
45
67
  throw new Error(`Git \u514B\u9686\u5931\u8D25\uFF08${url}\uFF09\uFF1A${detail}`);
46
68
  }
47
- const entries = await fs.readdir(tmpDir);
69
+ const entries = await fs2.readdir(tmpDir);
48
70
  for (const entry of entries) {
49
71
  if (entry === ".git") continue;
50
- const src = path.join(tmpDir, entry);
51
- const dest = path.join(targetDir, entry);
52
- await fs.move(src, dest, { overwrite: true });
72
+ const src = path2.join(tmpDir, entry);
73
+ const dest = path2.join(targetDir, entry);
74
+ await fs2.move(src, dest, { overwrite: true });
53
75
  }
54
- await fs.remove(tmpDir).catch(() => {
76
+ await fs2.remove(tmpDir).catch(() => {
55
77
  });
56
78
  }
57
79
  async function downloadTemplate(repo, targetDir) {
58
- await fs.ensureDir(targetDir);
80
+ await fs2.ensureDir(targetDir);
59
81
  const { url, branch } = parseRepo(repo);
60
82
  console.log(`\u6B63\u5728\u62C9\u53D6\u6A21\u677F: ${url}${branch ? ` (\u5206\u652F: ${branch})` : ""}`);
61
83
  await gitClone(url, branch, targetDir);
62
84
  console.log("\u6A21\u677F\u62C9\u53D6\u5B8C\u6210");
63
- const gitDir = path.join(targetDir, ".git");
64
- if (await fs.pathExists(gitDir)) {
65
- await fs.remove(gitDir);
85
+ const gitDir = path2.join(targetDir, ".git");
86
+ if (await fs2.pathExists(gitDir)) {
87
+ await fs2.remove(gitDir);
66
88
  console.log("\u5DF2\u5220\u9664 .git \u76EE\u5F55");
67
89
  }
68
90
  }
@@ -86,26 +108,26 @@ function installCommand(packageManager) {
86
108
  }
87
109
 
88
110
  // src/utils/project.ts
89
- import path2 from "path";
90
- import fs2 from "fs-extra";
111
+ import path3 from "path";
112
+ import fs3 from "fs-extra";
91
113
  import prompts from "prompts";
92
114
  function normalizeProjectName(name) {
93
115
  return name.trim();
94
116
  }
95
117
  function resolveTargetDir(cwd, projectName) {
96
- return path2.resolve(cwd, projectName);
118
+ return path3.resolve(cwd, projectName);
97
119
  }
98
120
  async function assertCanCreateProject(targetDir, yes) {
99
- if (!await fs2.pathExists(targetDir)) {
121
+ if (!await fs3.pathExists(targetDir)) {
100
122
  return true;
101
123
  }
102
- const files = await fs2.readdir(targetDir);
124
+ const files = await fs3.readdir(targetDir);
103
125
  if (files.length === 0) {
104
126
  return true;
105
127
  }
106
128
  if (yes) {
107
- await fs2.remove(targetDir);
108
- await fs2.ensureDir(targetDir);
129
+ await fs3.remove(targetDir);
130
+ await fs3.ensureDir(targetDir);
109
131
  return true;
110
132
  }
111
133
  const { overwrite } = await prompts(
@@ -124,8 +146,8 @@ async function assertCanCreateProject(targetDir, yes) {
124
146
  if (!overwrite) {
125
147
  return false;
126
148
  }
127
- await fs2.remove(targetDir);
128
- await fs2.ensureDir(targetDir);
149
+ await fs3.remove(targetDir);
150
+ await fs3.ensureDir(targetDir);
129
151
  return true;
130
152
  }
131
153
  function toPackageName(projectName) {
@@ -133,8 +155,8 @@ function toPackageName(projectName) {
133
155
  }
134
156
 
135
157
  // src/utils/replace-token.ts
136
- import path3 from "path";
137
- import fs3 from "fs-extra";
158
+ import path4 from "path";
159
+ import fs4 from "fs-extra";
138
160
  var SKIP_DIRS = /* @__PURE__ */ new Set([
139
161
  ".git",
140
162
  "node_modules",
@@ -166,22 +188,22 @@ var SKIP_EXTS = /* @__PURE__ */ new Set([
166
188
  ".lock"
167
189
  ]);
168
190
  async function replaceToken(targetDir, token, value) {
169
- const entries = await fs3.readdir(targetDir);
191
+ const entries = await fs4.readdir(targetDir);
170
192
  for (const entry of entries) {
171
- const fullPath = path3.join(targetDir, entry);
172
- const stat = await fs3.stat(fullPath);
193
+ const fullPath = path4.join(targetDir, entry);
194
+ const stat = await fs4.stat(fullPath);
173
195
  if (stat.isDirectory()) {
174
196
  if (SKIP_DIRS.has(entry)) continue;
175
197
  await replaceToken(fullPath, token, value);
176
198
  continue;
177
199
  }
178
- const ext = path3.extname(entry).toLowerCase();
200
+ const ext = path4.extname(entry).toLowerCase();
179
201
  if (SKIP_EXTS.has(ext)) continue;
180
202
  try {
181
- const content = await fs3.readFile(fullPath, "utf8");
203
+ const content = await fs4.readFile(fullPath, "utf8");
182
204
  if (content.includes(token)) {
183
205
  const replaced = content.replaceAll(token, value);
184
- await fs3.writeFile(fullPath, replaced, "utf8");
206
+ await fs4.writeFile(fullPath, replaced, "utf8");
185
207
  console.log(` [replace] ${entry}`);
186
208
  }
187
209
  } catch {
@@ -197,6 +219,7 @@ var BUILTIN_TEMPLATES = [
197
219
  {
198
220
  id: "react-vite",
199
221
  label: "React + Vite",
222
+ localDir: "react-vite",
200
223
  repo: "QGtiger/template-vite-server#main"
201
224
  }
202
225
  ];
@@ -208,6 +231,21 @@ function listTemplateIds() {
208
231
  return BUILTIN_TEMPLATES.map((entry) => entry.id);
209
232
  }
210
233
 
234
+ // src/utils/builtin-template-dir.ts
235
+ import path5 from "path";
236
+ import fs5 from "fs";
237
+ import { fileURLToPath } from "url";
238
+ var dirname = path5.dirname(fileURLToPath(import.meta.url));
239
+ function resolveBuiltinTemplateDir(localDir) {
240
+ const candidates = [
241
+ // 产物模式:dirname=dist -> dist/templates/<localDir>
242
+ path5.resolve(dirname, "templates", localDir),
243
+ // 源码模式:dirname=src/utils -> packages/cli/templates/<localDir>
244
+ path5.resolve(dirname, "..", "..", "templates", localDir)
245
+ ];
246
+ return candidates.find((candidate) => fs5.existsSync(candidate));
247
+ }
248
+
211
249
  // src/utils/resolve-template.ts
212
250
  async function promptTemplateId() {
213
251
  const { templateId } = await prompts2(
@@ -231,41 +269,59 @@ async function promptTemplateId() {
231
269
  }
232
270
  return templateId;
233
271
  }
234
- async function resolveTemplateRepo(options) {
235
- if (options.templateRepo) {
236
- return options.templateRepo;
237
- }
272
+ async function selectEntry(options) {
238
273
  if (options.template) {
239
274
  const entry2 = getTemplateById(options.template);
240
275
  if (!entry2) {
241
276
  throw new Error(`\u672A\u77E5\u6A21\u677F\uFF1A${options.template}\u3002\u53EF\u9009\uFF1A${listTemplateIds().join("\u3001")}`);
242
277
  }
243
- return entry2.repo;
278
+ return entry2;
279
+ }
280
+ if (BUILTIN_TEMPLATES.length === 1) {
281
+ const entry2 = getTemplateById(DEFAULT_TEMPLATE_ID ?? BUILTIN_TEMPLATES[0].id);
282
+ if (!entry2) {
283
+ throw new Error("\u5185\u7F6E\u6A21\u677F\u5217\u8868\u4E3A\u7A7A");
284
+ }
285
+ return entry2;
244
286
  }
245
287
  if (options.yes) {
246
288
  const entry2 = getTemplateById(DEFAULT_TEMPLATE_ID ?? BUILTIN_TEMPLATES[0].id);
247
289
  if (!entry2) {
248
290
  throw new Error("\u5185\u7F6E\u6A21\u677F\u5217\u8868\u4E3A\u7A7A");
249
291
  }
250
- return entry2.repo;
292
+ return entry2;
251
293
  }
252
294
  const templateId = await promptTemplateId();
253
295
  const entry = getTemplateById(templateId);
254
296
  if (!entry) {
255
297
  throw new Error(`\u672A\u77E5\u6A21\u677F\uFF1A${templateId}`);
256
298
  }
257
- return entry.repo;
299
+ return entry;
300
+ }
301
+ async function resolveTemplate(options) {
302
+ const entry = await selectEntry(options);
303
+ if (!options.remote && entry.localDir) {
304
+ const dir = resolveBuiltinTemplateDir(entry.localDir);
305
+ if (dir) {
306
+ return { type: "local", dir, id: entry.id };
307
+ }
308
+ console.warn(`[template] \u5185\u7F6E\u6A21\u677F\u526F\u672C\u7F3A\u5931\uFF08${entry.localDir}\uFF09\uFF0C\u56DE\u9000\u5230 git \u62C9\u53D6\u3002`);
309
+ }
310
+ if (!entry.repo) {
311
+ throw new Error(`\u6A21\u677F ${entry.id} \u672A\u914D\u7F6E git \u6E90\uFF0C\u4E14\u672C\u5730\u526F\u672C\u4E0D\u53EF\u7528`);
312
+ }
313
+ return { type: "git", repo: entry.repo, id: entry.id };
258
314
  }
259
315
 
260
316
  // src/commands/create.ts
261
317
  async function patchPackageJsonName(targetDir, projectName) {
262
- const packageJsonPath = path4.join(targetDir, "package.json");
263
- if (!await fs4.pathExists(packageJsonPath)) {
318
+ const packageJsonPath = path6.join(targetDir, "package.json");
319
+ if (!await fs6.pathExists(packageJsonPath)) {
264
320
  throw new Error("\u6A21\u677F\u7F3A\u5C11 package.json");
265
321
  }
266
- const packageJson = await fs4.readJson(packageJsonPath);
322
+ const packageJson = await fs6.readJson(packageJsonPath);
267
323
  packageJson.name = toPackageName(projectName);
268
- await fs4.writeJson(packageJsonPath, packageJson, { spaces: 2 });
324
+ await fs6.writeJson(packageJsonPath, packageJson, { spaces: 2 });
269
325
  }
270
326
  async function resolveCreateOptions(options) {
271
327
  const cwd = options.cwd ?? process.cwd();
@@ -305,9 +361,9 @@ async function resolveCreateOptions(options) {
305
361
  if (!isPackageManager(packageManager)) {
306
362
  throw new Error(`\u4E0D\u652F\u6301\u7684\u5305\u7BA1\u7406\u5668\uFF1A${String(packageManager)}`);
307
363
  }
308
- const templateRepo = await resolveTemplateRepo({
364
+ const template = await resolveTemplate({
309
365
  template: options.template,
310
- templateRepo: options.templateRepo,
366
+ remote: options.remote,
311
367
  yes: options.yes
312
368
  });
313
369
  const yesMode = options.yes === true;
@@ -316,7 +372,7 @@ async function resolveCreateOptions(options) {
316
372
  projectName,
317
373
  targetDir: resolveTargetDir(cwd, projectName),
318
374
  packageManager,
319
- templateRepo,
375
+ template,
320
376
  install: options.skipInstall ? false : options.install ?? (yesMode ? true : answers.install ?? false),
321
377
  git: options.git ?? (yesMode ? true : answers.git ?? false)
322
378
  };
@@ -340,15 +396,25 @@ async function maybeInitGit(options) {
340
396
  stdio: "inherit"
341
397
  });
342
398
  }
399
+ async function writeTemplate(template, targetDir) {
400
+ if (template.type === "local") {
401
+ console.log(`[create] \u4F7F\u7528\u5185\u7F6E\u6A21\u677F\uFF1A${template.id}`);
402
+ await copyLocalTemplate(template.dir, targetDir);
403
+ console.log("[create] \u5185\u7F6E\u6A21\u677F\u62F7\u8D1D\u5B8C\u6210");
404
+ return;
405
+ }
406
+ console.log(`[create] \u4F7F\u7528 git \u6A21\u677F\uFF1A${template.id}`);
407
+ await downloadTemplate(template.repo, targetDir);
408
+ }
343
409
  async function createProject(options = {}) {
344
410
  const resolvedOptions = await resolveCreateOptions(options);
345
411
  const canCreate = await assertCanCreateProject(resolvedOptions.targetDir, options.yes);
346
412
  if (!canCreate) {
347
413
  throw new Error("\u5DF2\u53D6\u6D88\u521B\u5EFA\u9879\u76EE");
348
414
  }
349
- console.log("[create] \u5F00\u59CB\u4E0B\u8F7D\u6A21\u677F...");
350
- await downloadTemplate(resolvedOptions.templateRepo, resolvedOptions.targetDir);
351
- console.log("[create] \u6A21\u677F\u4E0B\u8F7D\u5B8C\u6210");
415
+ console.log("[create] \u5F00\u59CB\u5199\u5165\u6A21\u677F...");
416
+ await writeTemplate(resolvedOptions.template, resolvedOptions.targetDir);
417
+ console.log("[create] \u6A21\u677F\u5199\u5165\u5B8C\u6210");
352
418
  console.log("[create] \u5F00\u59CB\u4FEE\u6539 package.json...");
353
419
  await patchPackageJsonName(resolvedOptions.targetDir, resolvedOptions.projectName);
354
420
  console.log("[create] package.json \u4FEE\u6539\u5B8C\u6210");
@@ -364,36 +430,415 @@ async function createProject(options = {}) {
364
430
  return resolvedOptions;
365
431
  }
366
432
 
433
+ // src/commands/publish.ts
434
+ import fs10 from "fs";
435
+ import path10 from "path";
436
+
437
+ // src/utils/env.ts
438
+ import fs7 from "fs";
439
+ import path7 from "path";
440
+ import { parse } from "dotenv";
441
+ var DEFAULT_BASE_ROOT = "https://lf-frontend.oss-cn-beijing.aliyuncs.com";
442
+ function loadDotenv({ cwd, envPath }) {
443
+ let merged;
444
+ if (envPath) {
445
+ merged = readEnvFile(path7.resolve(cwd, envPath));
446
+ } else {
447
+ const base = readEnvFile(path7.resolve(cwd, ".env"));
448
+ const local = readEnvFile(path7.resolve(cwd, ".env.local"));
449
+ merged = { ...base, ...local };
450
+ }
451
+ for (const [key, value] of Object.entries(merged)) {
452
+ if (process.env[key] === void 0) {
453
+ process.env[key] = value;
454
+ }
455
+ }
456
+ }
457
+ function readEnvFile(file) {
458
+ if (!fs7.existsSync(file)) return {};
459
+ return parse(fs7.readFileSync(file, "utf8"));
460
+ }
461
+ function getRequiredEnvNames({ notify, serverDetected }) {
462
+ const required = ["OSS_REGION", "OSS_BUCKET", "OSS_ACCESS_KEYID", "OSS_ACCESS_KEYSECRECT"];
463
+ if (notify) required.push("DEPLOY_API_URL");
464
+ if (serverDetected) required.push("DATABASE_URL");
465
+ return required;
466
+ }
467
+ function resolvePublishEnv(options, env = process.env) {
468
+ const required = getRequiredEnvNames(options);
469
+ const missing = required.filter((name) => !env[name]);
470
+ if (missing.length > 0) {
471
+ throw new Error(
472
+ `\u7F3A\u5C11\u5FC5\u9700\u73AF\u5883\u53D8\u91CF:${missing.join(", ")}(\u8BF7\u5728 .env / .env.local \u6216\u73AF\u5883\u53D8\u91CF\u4E2D\u914D\u7F6E)`
473
+ );
474
+ }
475
+ return {
476
+ ossRegion: env.OSS_REGION,
477
+ ossBucket: env.OSS_BUCKET,
478
+ ossAccessKeyId: env.OSS_ACCESS_KEYID,
479
+ // 注意:变量名沿用 CI secrets 的拼写(SECRECT),保持 .env 兼容
480
+ ossAccessKeySecret: env.OSS_ACCESS_KEYSECRECT,
481
+ deployApiUrl: options.notify ? env.DEPLOY_API_URL : void 0,
482
+ databaseUrl: env.DATABASE_URL,
483
+ baseRoot: env.BASE_ROOT || DEFAULT_BASE_ROOT
484
+ };
485
+ }
486
+
487
+ // src/utils/build.ts
488
+ import fs8 from "fs";
489
+ import path8 from "path";
490
+ import { execa as execa3 } from "execa";
491
+ var SERVER_CONFIG_FILES = [
492
+ "lightfish-server.config.js",
493
+ "lightfish-server.config.ts",
494
+ "lightfish-server.config.mjs",
495
+ "lightfish-server.config.cjs"
496
+ ];
497
+ var VITE_CONFIG_FILES = [
498
+ "vite.config.ts",
499
+ "vite.config.js",
500
+ "vite.config.mts",
501
+ "vite.config.mjs",
502
+ "vite.config.cjs"
503
+ ];
504
+ function readPackageJson(cwd) {
505
+ const file = path8.join(cwd, "package.json");
506
+ if (!fs8.existsSync(file)) return void 0;
507
+ return JSON.parse(fs8.readFileSync(file, "utf8"));
508
+ }
509
+ function hasVite(cwd) {
510
+ if (VITE_CONFIG_FILES.some((f) => fs8.existsSync(path8.join(cwd, f)))) return true;
511
+ const pkg = readPackageJson(cwd);
512
+ if (pkg) {
513
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
514
+ if ("vite" in deps) return true;
515
+ }
516
+ return false;
517
+ }
518
+ function detectBuild(cwd) {
519
+ return {
520
+ vite: hasVite(cwd),
521
+ server: SERVER_CONFIG_FILES.some((f) => fs8.existsSync(path8.join(cwd, f)))
522
+ };
523
+ }
524
+ async function runBuildSteps(options) {
525
+ const detection = detectBuild(options.cwd);
526
+ if (!detection.vite && !detection.server) {
527
+ throw new Error("\u672A\u68C0\u6D4B\u5230 Vite \u6216 lightfish-server,\u65E0\u6CD5\u6784\u5EFA");
528
+ }
529
+ const execOptions = {
530
+ cwd: options.cwd,
531
+ preferLocal: true,
532
+ stdio: "inherit",
533
+ env: { ...process.env, ...options.env }
534
+ };
535
+ if (detection.vite) {
536
+ await execa3("vite", ["build"], execOptions);
537
+ }
538
+ if (detection.server) {
539
+ await execa3("lightfish-server", ["build"], execOptions);
540
+ await execa3("lightfish-server", ["db", "push"], execOptions);
541
+ }
542
+ }
543
+
544
+ // src/utils/oss.ts
545
+ import OSS from "ali-oss";
546
+ import fs9 from "fs";
547
+ import path9 from "path";
548
+ function withPrefix(prefix, key) {
549
+ const normalized = prefix.replace(/^\/+/, "").replace(/\/+$/, "");
550
+ if (!normalized) return key;
551
+ return `${normalized}/${key}`;
552
+ }
553
+ function walkDir(dir, base = "") {
554
+ if (!fs9.existsSync(dir)) return [];
555
+ const entries = fs9.readdirSync(dir, { withFileTypes: true });
556
+ const files = [];
557
+ for (const entry of entries) {
558
+ const fullPath = path9.join(dir, entry.name);
559
+ const key = base ? `${base}/${entry.name}` : entry.name;
560
+ if (entry.isDirectory()) {
561
+ files.push(...walkDir(fullPath, key));
562
+ } else {
563
+ files.push({ localPath: fullPath, key });
564
+ }
565
+ }
566
+ return files;
567
+ }
568
+ async function uploadDist(options, client) {
569
+ const oss = client ?? createClient(options);
570
+ const files = walkDir(options.distDir);
571
+ console.log(`[publish] Found ${files.length} files to upload`);
572
+ const results = await Promise.all(
573
+ files.map(({ localPath, key }) => {
574
+ const objectKey = withPrefix(options.objectPrefix, key);
575
+ return oss.put(objectKey, localPath).then((res) => ({ key: objectKey, ok: res.res.status === 200 })).catch((err) => {
576
+ const message = err instanceof Error ? err.message : String(err);
577
+ console.error(`[publish] ERROR ${objectKey}: ${message}`);
578
+ return { key: objectKey, ok: false };
579
+ });
580
+ })
581
+ );
582
+ for (const r of results.filter((r2) => r2.ok)) {
583
+ console.log(`[publish] ok ${r.key}`);
584
+ }
585
+ const ok = results.filter((r) => r.ok).length;
586
+ const fail = results.filter((r) => !r.ok).length;
587
+ console.log(`[publish] Done: ${ok} succeeded, ${fail} failed`);
588
+ return { ok, fail };
589
+ }
590
+ function createClient(options) {
591
+ return new OSS({
592
+ region: options.region,
593
+ accessKeyId: options.accessKeyId,
594
+ accessKeySecret: options.accessKeySecret,
595
+ bucket: options.bucket
596
+ });
597
+ }
598
+
599
+ // src/utils/notify.ts
600
+ async function notifyDeploy(options) {
601
+ const fetchFn = options.fetchImpl ?? fetch;
602
+ const body = JSON.stringify(options.payload);
603
+ console.log(`[publish] POST ${options.apiUrl}`);
604
+ console.log(`[publish] body: ${body}`);
605
+ let res;
606
+ try {
607
+ res = await fetchFn(options.apiUrl, {
608
+ method: "POST",
609
+ headers: { "Content-Type": "application/json" },
610
+ body
611
+ });
612
+ } catch (err) {
613
+ const message = err instanceof Error ? err.message : String(err);
614
+ throw new Error(`\u90E8\u7F72 API \u8BF7\u6C42\u5931\u8D25:${message}`);
615
+ }
616
+ if (!res.ok) {
617
+ throw new Error(`\u90E8\u7F72 API \u8FD4\u56DE HTTP ${res.status}`);
618
+ }
619
+ let data;
620
+ try {
621
+ data = await res.json();
622
+ } catch {
623
+ throw new Error("\u90E8\u7F72 API \u54CD\u5E94\u975E JSON");
624
+ }
625
+ console.log(`[publish] response: ${JSON.stringify(data)}`);
626
+ if (typeof data?.success !== "boolean" || data.success !== true) {
627
+ throw new Error("\u90E8\u7F72 API \u672A\u8FD4\u56DE success=true");
628
+ }
629
+ }
630
+
631
+ // src/commands/publish.ts
632
+ function generateVersion(date = /* @__PURE__ */ new Date()) {
633
+ return date.toISOString().replace(/[-T:.Z]/g, "").slice(0, 14);
634
+ }
635
+ function readPackageJsonName(cwd) {
636
+ const file = path10.join(cwd, "package.json");
637
+ if (!fs10.existsSync(file)) return void 0;
638
+ const pkg = JSON.parse(fs10.readFileSync(file, "utf8"));
639
+ return pkg.name;
640
+ }
641
+ function resolveAppName(options) {
642
+ if (options.app) return options.app;
643
+ const pkgName = readPackageJsonName(options.cwd);
644
+ if (pkgName) {
645
+ return pkgName.startsWith("@") ? pkgName.replace(/^@[^/]+\//, "") : pkgName;
646
+ }
647
+ throw new Error("\u65E0\u6CD5\u786E\u5B9A APP_NAME:\u8BF7\u901A\u8FC7 --app \u6307\u5B9A,\u6216\u786E\u4FDD package.json \u542B name \u5B57\u6BB5");
648
+ }
649
+ function assertNumericVersion(version) {
650
+ if (!/^\d+$/.test(version)) {
651
+ throw new Error(`version \u5FC5\u987B\u4E3A\u7EAF\u6570\u5B57(\u4E0E\u90E8\u7F72 API \u7684 number \u5951\u7EA6\u4E00\u81F4),\u6536\u5230:${version}`);
652
+ }
653
+ }
654
+ function resolveOptions(options) {
655
+ const cwd = options.cwd ?? process.cwd();
656
+ const appName = resolveAppName({ cwd, app: options.app });
657
+ const version = options.version ?? generateVersion();
658
+ assertNumericVersion(version);
659
+ const distDir = path10.resolve(cwd, options.dist ?? "dist");
660
+ const notify = !options.noNotify;
661
+ const baseRoot = process.env.BASE_ROOT || DEFAULT_BASE_ROOT;
662
+ const root = baseRoot.replace(/\/+$/, "");
663
+ const viteBaseUrl = root ? `${root}/${appName}/${version}/` : `/${appName}/${version}/`;
664
+ const objectPrefix = `${appName}/${version}`;
665
+ const ossIndexUrl = `${root}/${appName}/${version}/index.html`;
666
+ return {
667
+ cwd,
668
+ appName,
669
+ version,
670
+ distDir,
671
+ baseRoot,
672
+ root,
673
+ viteBaseUrl,
674
+ objectPrefix,
675
+ ossIndexUrl,
676
+ notify,
677
+ skipBuild: options.skipBuild ?? false,
678
+ note: options.note
679
+ };
680
+ }
681
+ function printBuildPlanDryRun(detection) {
682
+ if (!detection.vite && !detection.server) {
683
+ console.log("[publish] (dry-run) \u672A\u68C0\u6D4B\u5230 Vite \u6216 lightfish-server,\u6784\u5EFA\u5C06\u5931\u8D25");
684
+ return;
685
+ }
686
+ console.log("[publish] (dry-run) \u5C06\u6267\u884C\u6784\u5EFA:");
687
+ if (detection.vite) console.log("[publish] (dry-run) vite build");
688
+ if (detection.server) {
689
+ console.log("[publish] (dry-run) lightfish-server build");
690
+ console.log("[publish] (dry-run) lightfish-server db push");
691
+ }
692
+ }
693
+ function printEnvDryRun(serverDetected, notify) {
694
+ const required = getRequiredEnvNames({ notify, serverDetected });
695
+ console.log("[publish] (dry-run) \u73AF\u5883\u53D8\u91CF\u9884\u68C0:");
696
+ for (const name of required) {
697
+ const present = process.env[name] !== void 0 && process.env[name] !== "";
698
+ console.log(`[publish] (dry-run) ${present ? "\u2713" : "\u2717"} ${name}`);
699
+ }
700
+ }
701
+ function printUploadPlanDryRun(options) {
702
+ const files = walkDir(options.distDir);
703
+ if (files.length === 0) {
704
+ console.log(`[publish] (dry-run) \u4EA7\u7269\u76EE\u5F55 ${options.distDir} \u4E3A\u7A7A\u6216\u4E0D\u5B58\u5728(\u6784\u5EFA\u540E\u5C06\u751F\u6210)`);
705
+ return;
706
+ }
707
+ console.log(`[publish] (dry-run) \u5C06\u4E0A\u4F20 ${files.length} \u4E2A\u6587\u4EF6\u5230 OSS \u524D\u7F00:${options.objectPrefix}/`);
708
+ for (const { key } of files) {
709
+ console.log(`[publish] (dry-run) ${withPrefix(options.objectPrefix, key)}`);
710
+ }
711
+ }
712
+ function buildNotifyPayload(params) {
713
+ const payload = {
714
+ id: params.appName,
715
+ version: Number(params.version),
716
+ ossIndexUrl: params.ossIndexUrl,
717
+ domain: `${params.appName}.lightfish.top`
718
+ };
719
+ if (params.note) payload.note = params.note;
720
+ return payload;
721
+ }
722
+ function printNotifyPlanDryRun(options, pubEnv) {
723
+ if (!options.notify) {
724
+ console.log("[publish] (dry-run) \u901A\u77E5\u5DF2\u8DF3\u8FC7(--no-notify)");
725
+ return;
726
+ }
727
+ const payload = buildNotifyPayload({
728
+ appName: options.appName,
729
+ version: options.version,
730
+ ossIndexUrl: options.ossIndexUrl,
731
+ note: options.note
732
+ });
733
+ console.log("[publish] (dry-run) \u5C06 POST \u901A\u77E5:");
734
+ console.log(`[publish] (dry-run) url: ${pubEnv.deployApiUrl ?? "(\u672A\u914D\u7F6E DEPLOY_API_URL)"}`);
735
+ console.log(`[publish] (dry-run) body: ${JSON.stringify(payload)}`);
736
+ }
737
+ async function runPublish(options = {}) {
738
+ const cwd = options.cwd ?? process.cwd();
739
+ loadDotenv({ cwd, envPath: options.envPath });
740
+ const resolved = resolveOptions(options);
741
+ console.log(`[publish] app=${resolved.appName} version=${resolved.version} dist=${resolved.distDir}`);
742
+ let serverDetected = false;
743
+ if (!resolved.skipBuild) {
744
+ const detection = detectBuild(resolved.cwd);
745
+ serverDetected = detection.server;
746
+ if (options.dryRun) {
747
+ printBuildPlanDryRun(detection);
748
+ } else {
749
+ console.log("[publish] \u5F00\u59CB\u6784\u5EFA...");
750
+ const buildEnv = {
751
+ VITE_BASE_URL: resolved.viteBaseUrl,
752
+ BASE_ROOT: resolved.baseRoot,
753
+ APP_NAME: resolved.appName,
754
+ BUILD_VERSION: resolved.version
755
+ };
756
+ if (process.env.DATABASE_URL) buildEnv.DATABASE_URL = process.env.DATABASE_URL;
757
+ await runBuildSteps({
758
+ cwd: resolved.cwd,
759
+ env: buildEnv
760
+ });
761
+ console.log("[publish] \u6784\u5EFA\u5B8C\u6210");
762
+ }
763
+ }
764
+ const pubEnv = options.dryRun ? softResolveEnv(resolved.notify, serverDetected) : resolvePublishEnv({ notify: resolved.notify, serverDetected });
765
+ if (options.dryRun) {
766
+ printEnvDryRun(serverDetected, resolved.notify);
767
+ printUploadPlanDryRun(resolved);
768
+ printNotifyPlanDryRun(resolved, pubEnv);
769
+ console.log("[publish] (dry-run) \u5B8C\u6210,\u672A\u6267\u884C\u4EFB\u4F55\u526F\u4F5C\u7528");
770
+ return;
771
+ }
772
+ if (!fs10.existsSync(resolved.distDir)) {
773
+ throw new Error(`\u4EA7\u7269\u76EE\u5F55\u4E0D\u5B58\u5728:${resolved.distDir}`);
774
+ }
775
+ console.log("[publish] \u5F00\u59CB\u4E0A\u4F20 OSS...");
776
+ const { fail } = await uploadDist({
777
+ region: pubEnv.ossRegion,
778
+ bucket: pubEnv.ossBucket,
779
+ accessKeyId: pubEnv.ossAccessKeyId,
780
+ accessKeySecret: pubEnv.ossAccessKeySecret,
781
+ objectPrefix: resolved.objectPrefix,
782
+ distDir: resolved.distDir
783
+ });
784
+ if (fail > 0) {
785
+ throw new Error(`OSS \u4E0A\u4F20\u6709 ${fail} \u4E2A\u6587\u4EF6\u5931\u8D25`);
786
+ }
787
+ console.log("[publish] OSS \u4E0A\u4F20\u5B8C\u6210");
788
+ if (resolved.notify) {
789
+ console.log("[publish] \u5F00\u59CB\u901A\u77E5\u90E8\u7F72 API...");
790
+ const payload = buildNotifyPayload({
791
+ appName: resolved.appName,
792
+ version: resolved.version,
793
+ ossIndexUrl: resolved.ossIndexUrl,
794
+ note: resolved.note
795
+ });
796
+ await notifyDeploy({ apiUrl: pubEnv.deployApiUrl, payload });
797
+ console.log("[publish] \u901A\u77E5\u5B8C\u6210");
798
+ }
799
+ }
800
+ function softResolveEnv(notify, serverDetected) {
801
+ return {
802
+ ossRegion: process.env.OSS_REGION ?? "",
803
+ ossBucket: process.env.OSS_BUCKET ?? "",
804
+ ossAccessKeyId: process.env.OSS_ACCESS_KEYID ?? "",
805
+ ossAccessKeySecret: process.env.OSS_ACCESS_KEYSECRECT ?? "",
806
+ deployApiUrl: notify ? process.env.DEPLOY_API_URL : void 0,
807
+ databaseUrl: process.env.DATABASE_URL,
808
+ baseRoot: process.env.BASE_ROOT || DEFAULT_BASE_ROOT
809
+ };
810
+ }
811
+
367
812
  // src/utils/package-info.ts
368
- import fs5 from "fs";
369
- import path5 from "path";
370
- import { fileURLToPath } from "url";
371
- var dirname = path5.dirname(fileURLToPath(import.meta.url));
813
+ import fs11 from "fs";
814
+ import path11 from "path";
815
+ import { fileURLToPath as fileURLToPath2 } from "url";
816
+ var dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
372
817
  function readPackageVersion() {
373
818
  const packageJsonPathCandidates = [
374
819
  // 源码模式:src/utils/package-info.ts -> packages/cli/package.json
375
- path5.resolve(dirname, "../../package.json"),
820
+ path11.resolve(dirname2, "../../package.json"),
376
821
  // 构建产物模式:dist/lightfish.js -> packages/cli/package.json
377
- path5.resolve(dirname, "../package.json")
822
+ path11.resolve(dirname2, "../package.json")
378
823
  ];
379
- const packageJsonPath = packageJsonPathCandidates.find((candidate) => fs5.existsSync(candidate));
824
+ const packageJsonPath = packageJsonPathCandidates.find((candidate) => fs11.existsSync(candidate));
380
825
  if (!packageJsonPath) {
381
826
  return "0.0.0";
382
827
  }
383
- const packageJson = JSON.parse(fs5.readFileSync(packageJsonPath, "utf8"));
828
+ const packageJson = JSON.parse(fs11.readFileSync(packageJsonPath, "utf8"));
384
829
  return packageJson.version ?? "0.0.0";
385
830
  }
386
831
 
387
832
  // src/lightfish.ts
388
833
  var cli = cac("lightfish");
389
834
  async function runBuild() {
390
- await execa3("vite", ["build"], {
835
+ await execa4("vite", ["build"], {
391
836
  cwd: process.cwd(),
392
837
  preferLocal: true,
393
838
  stdio: "inherit"
394
839
  });
395
840
  }
396
- cli.command("create [projectName]", "\u521B\u5EFA Lightfish React Vite \u9879\u76EE").option("--pm <pm>", "\u5305\u7BA1\u7406\u5668\uFF1Anpm\u3001yarn\u3001pnpm\u3001bun", { default: "pnpm" }).option("--template <id>", "\u5185\u7F6E\u6A21\u677F id\uFF0C\u4F8B\u5982 react-vite").option("--template-repo <repo>", "\u76F4\u63A5\u6307\u5B9A git \u4ED3\u5E93\u6E90\uFF0C\u5982 QGtiger/template-vite-server#main\uFF08\u4F18\u5148\u4E8E --template\uFF09").option("--skip-install", "\u8DF3\u8FC7\u4F9D\u8D56\u5B89\u88C5").option("--git", "\u521B\u5EFA\u540E\u521D\u59CB\u5316 git").option("-y, --yes", "\u4F7F\u7528\u9ED8\u8BA4\u9009\u9879\u5E76\u8DF3\u8FC7\u786E\u8BA4").action(async (projectName, options) => {
841
+ cli.command("create [projectName]", "\u521B\u5EFA Lightfish React Vite \u9879\u76EE").option("--pm <pm>", "\u5305\u7BA1\u7406\u5668\uFF1Anpm\u3001yarn\u3001pnpm\u3001bun", { default: "pnpm" }).option("--template <id>", "\u5185\u7F6E\u6A21\u677F id\uFF0C\u4F8B\u5982 react-vite").option("--remote", "\u5F3A\u5236\u8D70 git \u62C9\u53D6\u5185\u7F6E\u6A21\u677F\uFF08\u5FFD\u7565\u672C\u5730\u526F\u672C\uFF09").option("--skip-install", "\u8DF3\u8FC7\u4F9D\u8D56\u5B89\u88C5").option("--git", "\u521B\u5EFA\u540E\u521D\u59CB\u5316 git").option("-y, --yes", "\u4F7F\u7528\u9ED8\u8BA4\u9009\u9879\u5E76\u8DF3\u8FC7\u786E\u8BA4").action(async (projectName, options) => {
397
842
  try {
398
843
  const packageManager = options.pm ?? "pnpm";
399
844
  if (!isPackageManager(packageManager)) {
@@ -403,7 +848,7 @@ cli.command("create [projectName]", "\u521B\u5EFA Lightfish React Vite \u9879\u7
403
848
  projectName,
404
849
  packageManager,
405
850
  template: options.template,
406
- templateRepo: options.templateRepo,
851
+ remote: options.remote,
407
852
  skipInstall: options.skipInstall,
408
853
  git: options.git,
409
854
  yes: options.yes
@@ -424,9 +869,28 @@ cli.command("build", "\u6267\u884C Vite \u6784\u5EFA\uFF0C\u540E\u7EED\u4F1A\u53
424
869
  process.exitCode = 1;
425
870
  }
426
871
  });
872
+ cli.command("publish", "\u6784\u5EFA\u5E76\u90E8\u7F72\u5230 OSS\uFF0C\u901A\u77E5\u90E8\u7F72 API").option("--app <name>", "\u8986\u76D6 APP_NAME\uFF08\u9ED8\u8BA4 package.json#name \u53BB scope\uFF09").option("--version <v>", "\u8986\u76D6 BUILD_VERSION\uFF08\u9ED8\u8BA4 UTC \u65F6\u95F4\u6233 YYYYMMDDHHmmss\uFF09").option("--dist <dir>", "\u4EA7\u7269\u76EE\u5F55", { default: "dist" }).option("--note <text>", "\u901A\u77E5 body \u7684 note \u5B57\u6BB5\uFF0C\u4E0D\u4F20\u5219\u7701\u7565").option("--env <path>", "\u6307\u5B9A .env \u6587\u4EF6\u8DEF\u5F84\uFF08\u9ED8\u8BA4 cwd/.env + .env.local\uFF09").option("--no-build", "\u8DF3\u8FC7\u6784\u5EFA\uFF0C\u4F7F\u7528\u5DF2\u6709 dist").option("--no-notify", "\u8DF3\u8FC7\u901A\u77E5\u90E8\u7F72 API").option("--dry-run", "\u53EA\u6253\u5370\u5C06\u505A\u4EC0\u4E48\uFF0C\u4E0D\u6267\u884C").action(async (options) => {
873
+ try {
874
+ await runPublish({
875
+ app: options.app,
876
+ version: options.version,
877
+ dist: options.dist,
878
+ note: options.note,
879
+ envPath: options.env,
880
+ skipBuild: options.build === false,
881
+ noNotify: options.notify === false,
882
+ dryRun: options.dryRun
883
+ });
884
+ process.exit(0);
885
+ } catch (error) {
886
+ const message = error instanceof Error ? error.message : String(error);
887
+ console.error(`\u53D1\u5E03\u5931\u8D25\uFF1A${message}`);
888
+ process.exit(1);
889
+ }
890
+ });
427
891
  cli.command("", "\u663E\u793A\u5E2E\u52A9").action(() => {
428
892
  cli.outputHelp();
429
893
  });
430
894
  cli.help();
431
- cli.version(readPackageVersion());
895
+ cli.version(readPackageVersion(), "-v, --cli-version");
432
896
  cli.parse();