@lark-apaas/fullstack-cli 1.1.9 → 1.1.10

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 CHANGED
@@ -1,54 +1,3881 @@
1
- import { fileURLToPath } from 'node:url';
2
- import path from 'node:path';
3
- import fs from 'node:fs';
4
- import cac from 'cac';
5
- import { run as runGenDbSchema } from './commands/gen-db-schema.js';
6
- import { run as runSync } from './commands/sync.js';
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
- // 读取 package.json
10
- const pkgPath = path.join(__dirname, '../package.json');
11
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
12
- const cli = cac('fullstack-cli');
13
- // 设置全局描述和版本
14
- cli
15
- .version(pkg.version)
16
- .help();
17
- // gen-db-schema 命令
18
- cli
19
- .command('gen-db-schema', 'Generate database schema from existing database')
20
- .option('--output <path>', 'Output path for schema', {
21
- default: 'server/database/schema.ts',
22
- })
23
- .option('--schema-filter <schemas>', 'Schema filter, comma-separated')
24
- .option('--tables-filter <tables>', 'Tables filter, comma-separated')
25
- .option('--enable-nest-module-generate', 'Enable generate NestJS module boilerplate')
26
- .example('fullstack-cli gen-db-schema')
27
- .example('fullstack-cli gen-db-schema --output custom/schema.ts')
28
- .action(async (options) => {
29
- try {
30
- await runGenDbSchema(options);
31
- }
32
- catch (error) {
33
- console.error(`Failed to execute command:`, error.message);
34
- console.error(error.stack);
35
- process.exit(1);
1
+ // src/index.ts
2
+ import fs18 from "fs";
3
+ import path16 from "path";
4
+ import { fileURLToPath as fileURLToPath3 } from "url";
5
+ import { config as dotenvConfig } from "dotenv";
6
+
7
+ // src/cli.ts
8
+ import { Command } from "commander";
9
+
10
+ // src/hooks.ts
11
+ var HooksManager = class {
12
+ constructor() {
13
+ this.beforeHooks = [];
14
+ this.afterHooks = [];
15
+ }
16
+ /** 注册 beforeRun hook */
17
+ beforeRun(hook) {
18
+ this.beforeHooks.push(hook);
19
+ return this;
20
+ }
21
+ /** 注册 afterRun hook */
22
+ afterRun(hook) {
23
+ this.afterHooks.push(hook);
24
+ return this;
25
+ }
26
+ /** 执行 beforeRun hooks */
27
+ async runBeforeHooks(ctx) {
28
+ for (const hook of this.beforeHooks) {
29
+ await hook(ctx);
30
+ }
31
+ }
32
+ /** 执行 afterRun hooks */
33
+ async runAfterHooks(ctx, error) {
34
+ for (const hook of this.afterHooks) {
35
+ await hook(ctx, error);
36
+ }
37
+ }
38
+ /** 包装命令 action,自动执行 hooks */
39
+ wrapAction(commandName, action) {
40
+ return async (...args) => {
41
+ const ctx = {
42
+ commandName,
43
+ args: args.filter((a) => typeof a === "string"),
44
+ options: args.find((a) => typeof a === "object" && a !== null) || {},
45
+ startTime: Date.now()
46
+ };
47
+ let error;
48
+ try {
49
+ await this.runBeforeHooks(ctx);
50
+ await action(...args);
51
+ } catch (e) {
52
+ error = e instanceof Error ? e : new Error(String(e));
53
+ throw error;
54
+ } finally {
55
+ await this.runAfterHooks(ctx, error);
56
+ }
57
+ };
58
+ }
59
+ };
60
+
61
+ // src/cli.ts
62
+ var FullstackCLI = class {
63
+ constructor(version) {
64
+ this.program = new Command();
65
+ this.hooks = new HooksManager();
66
+ this.program.name("fullstack-cli").description("CLI tool for fullstack template management").version(version);
67
+ this.setupDefaultHooks();
68
+ }
69
+ setupDefaultHooks() {
70
+ this.hooks.afterRun((ctx, error) => {
71
+ if (error) {
72
+ console.error(`
73
+ Command "${ctx.commandName}" failed:`, error.message);
74
+ if (process.env.DEBUG) {
75
+ console.error(error.stack);
76
+ }
77
+ process.exitCode = 1;
78
+ }
79
+ });
80
+ this.hooks.afterRun((ctx, error) => {
81
+ if (process.env.DEBUG && !error) {
82
+ const elapsed = Date.now() - ctx.startTime;
83
+ console.log(`
84
+ Command "${ctx.commandName}" completed in ${elapsed}ms`);
85
+ }
86
+ });
87
+ }
88
+ /** 注册单个命令 */
89
+ use(command) {
90
+ command.register(this.program);
91
+ return this;
92
+ }
93
+ /** 注册命令组 */
94
+ useGroup(group) {
95
+ const groupCmd = this.program.command(group.name).description(group.description);
96
+ for (const cmd of group.commands) {
97
+ cmd.register(groupCmd);
98
+ }
99
+ return this;
100
+ }
101
+ /** 批量注册 */
102
+ useAll(items) {
103
+ for (const item of items) {
104
+ if ("commands" in item) {
105
+ this.useGroup(item);
106
+ } else {
107
+ this.use(item);
108
+ }
109
+ }
110
+ return this;
111
+ }
112
+ /** 运行 CLI */
113
+ run(argv = process.argv) {
114
+ this.program.parse(argv);
115
+ }
116
+ };
117
+
118
+ // src/commands/db/schema.handler.ts
119
+ import path from "path";
120
+ import fs from "fs";
121
+ import { fileURLToPath } from "url";
122
+ import { spawnSync } from "child_process";
123
+ import { createRequire } from "module";
124
+ import { config as loadEnv } from "dotenv";
125
+ var require2 = createRequire(import.meta.url);
126
+ async function run(options = {}) {
127
+ let exitCode = 0;
128
+ const envPath2 = path.resolve(process.cwd(), ".env");
129
+ if (fs.existsSync(envPath2)) {
130
+ loadEnv({ path: envPath2 });
131
+ console.log("[gen-db-schema] \u2713 Loaded .env file");
132
+ }
133
+ const databaseUrl = process.env.SUDA_DATABASE_URL;
134
+ if (!databaseUrl) {
135
+ console.error("[gen-db-schema] Error: SUDA_DATABASE_URL environment variable is required");
136
+ process.exit(1);
137
+ }
138
+ const outputPath = options.output || process.env.DB_SCHEMA_OUTPUT || "server/database/schema.ts";
139
+ const OUT_DIR = path.resolve(process.cwd(), "server/database/.introspect");
140
+ const SCHEMA_FILE = path.resolve(process.cwd(), outputPath);
141
+ console.log("[gen-db-schema] Starting...");
142
+ const __filename = fileURLToPath(import.meta.url);
143
+ const __dirname2 = path.dirname(__filename);
144
+ const configPath = path.join(__dirname2, "config", "drizzle.config.js");
145
+ if (!fs.existsSync(configPath)) {
146
+ console.error("[gen-db-schema] Error: drizzle config not found in CLI package");
147
+ process.exit(1);
148
+ }
149
+ try {
150
+ const env = {
151
+ ...process.env,
152
+ __DRIZZLE_OUT_DIR__: OUT_DIR,
153
+ __DRIZZLE_SCHEMA_PATH__: SCHEMA_FILE
154
+ };
155
+ const args = process.argv.slice(3).filter((arg) => !arg.startsWith("--enable-nest-module-generate"));
156
+ const spawnArgs = ["--yes", "drizzle-kit", "introspect", "--config", configPath, ...args];
157
+ const result = spawnSync("npx", spawnArgs, { stdio: "inherit", env });
158
+ if (result.error) {
159
+ console.error("[gen-db-schema] Execution failed:", result.error);
160
+ throw result.error;
161
+ }
162
+ if ((result.status ?? 0) !== 0) {
163
+ throw new Error(`drizzle-kit introspect failed with status ${result.status}`);
164
+ }
165
+ const generatedSchema = path.join(OUT_DIR, "schema.ts");
166
+ if (!fs.existsSync(generatedSchema)) {
167
+ console.error("[gen-db-schema] schema.ts not generated");
168
+ throw new Error("drizzle-kit introspect failed to generate schema.ts");
169
+ }
170
+ const { postprocessDrizzleSchema } = require2("@lark-apaas/devtool-kits");
171
+ const stats = postprocessDrizzleSchema(generatedSchema);
172
+ if (stats?.unmatchedUnknown?.length) {
173
+ console.warn("[gen-db-schema] Unmatched custom types detected:", stats.unmatchedUnknown);
174
+ }
175
+ console.log("[gen-db-schema] \u2713 Postprocessed schema");
176
+ fs.mkdirSync(path.dirname(SCHEMA_FILE), { recursive: true });
177
+ fs.copyFileSync(generatedSchema, SCHEMA_FILE);
178
+ console.log(`[gen-db-schema] \u2713 Copied to ${outputPath}`);
179
+ try {
180
+ if (options.enableNestModuleGenerate) {
181
+ const { parseAndGenerateNestResourceTemplate } = require2("@lark-apaas/devtool-kits");
182
+ const tsConfigFilePath = path.resolve(process.cwd(), "tsconfig.json");
183
+ const schemaFilePath = SCHEMA_FILE;
184
+ await parseAndGenerateNestResourceTemplate({
185
+ tsConfigFilePath,
186
+ schemaFilePath,
187
+ moduleOutputDir: path.resolve(process.cwd(), "server/modules")
188
+ });
189
+ console.log("[gen-db-schema] \u2713 Generate NestJS Module Boilerplate Successfully");
190
+ }
191
+ } catch (error) {
192
+ console.warn("[gen-db-schema] Generate NestJS Module Boilerplate failed:", error instanceof Error ? error.message : String(error));
193
+ }
194
+ console.log("[gen-db-schema] \u2713 Complete");
195
+ } catch (err) {
196
+ console.error("[gen-db-schema] Failed:", err instanceof Error ? err.message : String(err));
197
+ exitCode = 1;
198
+ } finally {
199
+ if (fs.existsSync(OUT_DIR)) {
200
+ fs.rmSync(OUT_DIR, { recursive: true, force: true });
201
+ }
202
+ process.exit(exitCode);
203
+ }
204
+ }
205
+
206
+ // src/commands/db/index.ts
207
+ var genDbSchemaCommand = {
208
+ name: "gen-db-schema",
209
+ description: "Generate database schema from existing database",
210
+ register(program) {
211
+ program.command(this.name).description(this.description).option("--output <path>", "Output path for schema", "server/database/schema.ts").option("--schema-filter <schemas>", "Schema filter, comma-separated").option("--tables-filter <tables>", "Tables filter, comma-separated").option("--enable-nest-module-generate", "Enable generate NestJS module boilerplate").action(async (options) => {
212
+ await run(options);
213
+ });
214
+ }
215
+ };
216
+
217
+ // src/commands/sync/run.handler.ts
218
+ import path3 from "path";
219
+ import fs3 from "fs";
220
+ import { fileURLToPath as fileURLToPath2 } from "url";
221
+
222
+ // src/config/sync.ts
223
+ var syncConfig = {
224
+ // 派生规则
225
+ sync: [
226
+ // 1. 派生 scripts 目录(总是覆盖)
227
+ {
228
+ from: "templates/scripts",
229
+ to: "scripts",
230
+ type: "directory",
231
+ overwrite: true
232
+ },
233
+ // 2. 覆写 nest-cli.json 配置,禁止用户修改
234
+ {
235
+ from: "templates/nest-cli.json",
236
+ to: "nest-cli.json",
237
+ type: "file",
238
+ overwrite: true
239
+ },
240
+ // // 2. 追加内容到 .gitignore
241
+ // {
242
+ // from: 'templates/.gitignore.append',
243
+ // to: '.gitignore',
244
+ // type: 'append',
245
+ // },
246
+ // 3. 派生 server/type.ts 文件(总是覆盖)
247
+ // {
248
+ // from: 'templates/server/global.d.ts',
249
+ // to: 'server/types/global.d.ts',
250
+ // type: 'file',
251
+ // },
252
+ {
253
+ type: "delete-directory",
254
+ to: ".swc"
255
+ // 删除 .swc 目录(如果存在)
256
+ },
257
+ // 4. 从 .gitignore 中移除 package-lock.json
258
+ {
259
+ type: "remove-line",
260
+ to: ".gitignore",
261
+ pattern: "package-lock.json"
262
+ }
263
+ ],
264
+ // 文件权限设置
265
+ permissions: {
266
+ // 所有 .sh 文件设置为可执行
267
+ // '**/*.sh': 0o755,
268
+ }
269
+ };
270
+ function genSyncConfig(perms = {}) {
271
+ if (!perms.disableGenOpenapi) {
272
+ syncConfig.sync.push({
273
+ from: "templates/helper/gen-openapi.ts",
274
+ to: "scripts/gen-openapi.ts",
275
+ type: "file",
276
+ overwrite: true
277
+ });
278
+ }
279
+ return syncConfig;
280
+ }
281
+
282
+ // src/utils/file-ops.ts
283
+ import fs2 from "fs";
284
+ import path2 from "path";
285
+ function removeLineFromFile(filePath, pattern) {
286
+ if (!fs2.existsSync(filePath)) {
287
+ console.log(`[fullstack-cli] \u25CB ${path2.basename(filePath)} (not found)`);
288
+ return false;
289
+ }
290
+ const content = fs2.readFileSync(filePath, "utf-8");
291
+ const lines = content.split("\n");
292
+ const trimmedPattern = pattern.trim();
293
+ const filteredLines = lines.filter((line) => line.trim() !== trimmedPattern);
294
+ if (filteredLines.length === lines.length) {
295
+ console.log(`[fullstack-cli] \u25CB ${path2.basename(filePath)} (pattern not found: ${pattern})`);
296
+ return false;
297
+ }
298
+ fs2.writeFileSync(filePath, filteredLines.join("\n"));
299
+ console.log(`[fullstack-cli] \u2713 ${path2.basename(filePath)} (removed: ${pattern})`);
300
+ return true;
301
+ }
302
+
303
+ // src/commands/sync/run.handler.ts
304
+ async function run2(options) {
305
+ const userProjectRoot = process.env.INIT_CWD || process.cwd();
306
+ const __filename = fileURLToPath2(import.meta.url);
307
+ const __dirname2 = path3.dirname(__filename);
308
+ const pluginRoot = path3.resolve(__dirname2, "..");
309
+ if (userProjectRoot === pluginRoot) {
310
+ console.log("[fullstack-cli] Skip syncing (installing plugin itself)");
311
+ process.exit(0);
312
+ }
313
+ const userPackageJson = path3.join(userProjectRoot, "package.json");
314
+ if (!fs3.existsSync(userPackageJson)) {
315
+ console.log("[fullstack-cli] Skip syncing (not a valid npm project)");
316
+ process.exit(0);
317
+ }
318
+ try {
319
+ console.log("[fullstack-cli] Starting sync...");
320
+ const config = genSyncConfig({
321
+ disableGenOpenapi: options.disableGenOpenapi ?? false
322
+ });
323
+ if (!config || !config.sync) {
324
+ console.warn("[fullstack-cli] No sync configuration found");
325
+ process.exit(0);
326
+ }
327
+ for (const rule of config.sync) {
328
+ await syncRule(rule, pluginRoot, userProjectRoot);
329
+ }
330
+ if (config.permissions) {
331
+ setPermissions(config.permissions, userProjectRoot);
332
+ }
333
+ console.log("[fullstack-cli] Sync completed successfully \u2705");
334
+ } catch (error) {
335
+ const message = error instanceof Error ? error.message : String(error);
336
+ console.error("[fullstack-cli] Failed to sync:", message);
337
+ process.exit(1);
338
+ }
339
+ }
340
+ async function syncRule(rule, pluginRoot, userProjectRoot) {
341
+ if (rule.type === "delete-file" || rule.type === "delete-directory") {
342
+ const destPath2 = path3.join(userProjectRoot, rule.to);
343
+ if (rule.type === "delete-file") {
344
+ deleteFile(destPath2);
345
+ } else {
346
+ deleteDirectory(destPath2);
347
+ }
348
+ return;
349
+ }
350
+ if (rule.type === "remove-line") {
351
+ const destPath2 = path3.join(userProjectRoot, rule.to);
352
+ removeLineFromFile(destPath2, rule.pattern);
353
+ return;
354
+ }
355
+ if (!("from" in rule)) {
356
+ return;
357
+ }
358
+ const srcPath = path3.join(pluginRoot, rule.from);
359
+ const destPath = path3.join(userProjectRoot, rule.to);
360
+ if (!fs3.existsSync(srcPath)) {
361
+ console.warn(`[fullstack-cli] Source not found: ${rule.from}`);
362
+ return;
363
+ }
364
+ switch (rule.type) {
365
+ case "directory":
366
+ syncDirectory(srcPath, destPath, rule.overwrite ?? true);
367
+ break;
368
+ case "file":
369
+ syncFile(srcPath, destPath, rule.overwrite ?? true);
370
+ break;
371
+ case "append":
372
+ appendToFile(srcPath, destPath);
373
+ break;
374
+ }
375
+ }
376
+ function syncFile(src, dest, overwrite = true) {
377
+ const destDir = path3.dirname(dest);
378
+ if (!fs3.existsSync(destDir)) {
379
+ fs3.mkdirSync(destDir, { recursive: true });
380
+ }
381
+ if (fs3.existsSync(dest) && !overwrite) {
382
+ console.log(`[fullstack-cli] \u25CB ${path3.basename(dest)} (skipped, already exists)`);
383
+ return;
384
+ }
385
+ fs3.copyFileSync(src, dest);
386
+ console.log(`[fullstack-cli] \u2713 ${path3.basename(dest)}`);
387
+ }
388
+ function syncDirectory(src, dest, overwrite = true) {
389
+ if (!fs3.existsSync(dest)) {
390
+ fs3.mkdirSync(dest, { recursive: true });
391
+ }
392
+ const files = fs3.readdirSync(src);
393
+ let count = 0;
394
+ files.forEach((file) => {
395
+ const srcFile = path3.join(src, file);
396
+ const destFile = path3.join(dest, file);
397
+ const stats = fs3.statSync(srcFile);
398
+ if (stats.isDirectory()) {
399
+ syncDirectory(srcFile, destFile, overwrite);
400
+ } else {
401
+ if (overwrite || !fs3.existsSync(destFile)) {
402
+ fs3.copyFileSync(srcFile, destFile);
403
+ console.log(`[fullstack-cli] \u2713 ${path3.relative(dest, destFile)}`);
404
+ count++;
405
+ }
406
+ }
407
+ });
408
+ if (count > 0) {
409
+ console.log(`[fullstack-cli] Synced ${count} files to ${path3.basename(dest)}/`);
410
+ }
411
+ }
412
+ function appendToFile(src, dest) {
413
+ const content = fs3.readFileSync(src, "utf-8");
414
+ let existingContent = "";
415
+ if (fs3.existsSync(dest)) {
416
+ existingContent = fs3.readFileSync(dest, "utf-8");
417
+ }
418
+ if (existingContent.includes(content.trim())) {
419
+ console.log(`[fullstack-cli] \u25CB ${path3.basename(dest)} (already contains content)`);
420
+ return;
421
+ }
422
+ fs3.appendFileSync(dest, content);
423
+ console.log(`[fullstack-cli] \u2713 ${path3.basename(dest)} (appended)`);
424
+ }
425
+ function setPermissions(permissions, projectRoot) {
426
+ for (const [pattern, mode] of Object.entries(permissions)) {
427
+ if (pattern === "**/*.sh") {
428
+ const scriptsDir = path3.join(projectRoot, "scripts");
429
+ if (fs3.existsSync(scriptsDir)) {
430
+ const files = fs3.readdirSync(scriptsDir);
431
+ files.forEach((file) => {
432
+ if (file.endsWith(".sh")) {
433
+ const filePath = path3.join(scriptsDir, file);
434
+ fs3.chmodSync(filePath, mode);
435
+ }
436
+ });
437
+ }
438
+ }
439
+ }
440
+ }
441
+ function deleteFile(filePath) {
442
+ if (fs3.existsSync(filePath)) {
443
+ fs3.unlinkSync(filePath);
444
+ console.log(`[fullstack-cli] \u2713 ${path3.basename(filePath)} (deleted)`);
445
+ } else {
446
+ console.log(`[fullstack-cli] \u25CB ${path3.basename(filePath)} (not found)`);
447
+ }
448
+ }
449
+ function deleteDirectory(dirPath) {
450
+ if (fs3.existsSync(dirPath)) {
451
+ fs3.rmSync(dirPath, { recursive: true });
452
+ console.log(`[fullstack-cli] \u2713 ${path3.basename(dirPath)} (deleted)`);
453
+ } else {
454
+ console.log(`[fullstack-cli] \u25CB ${path3.basename(dirPath)} (not found)`);
455
+ }
456
+ }
457
+
458
+ // src/commands/sync/index.ts
459
+ var syncCommand = {
460
+ name: "sync",
461
+ description: "Sync template files (scripts, configs) to user project",
462
+ register(program) {
463
+ program.command(this.name).description(this.description).option("--disable-gen-openapi", "Disable generating openapi.ts").action(async (options) => {
464
+ await run2(options);
465
+ });
466
+ }
467
+ };
468
+
469
+ // src/commands/action-plugin/utils.ts
470
+ import fs4 from "fs";
471
+ import path4 from "path";
472
+ import { spawnSync as spawnSync2, execSync } from "child_process";
473
+ function parsePluginName(input) {
474
+ const match = input.match(/^(@[^/]+\/[^@]+)(?:@(.+))?$/);
475
+ if (!match) {
476
+ throw new Error(
477
+ `Invalid plugin name format: ${input}. Expected format: @scope/name or @scope/name@version`
478
+ );
479
+ }
480
+ return {
481
+ name: match[1],
482
+ version: match[2] || "latest"
483
+ };
484
+ }
485
+ function getProjectRoot() {
486
+ return process.cwd();
487
+ }
488
+ function getPackageJsonPath() {
489
+ return path4.join(getProjectRoot(), "package.json");
490
+ }
491
+ function getPluginPath(pluginName) {
492
+ return path4.join(getProjectRoot(), "node_modules", pluginName);
493
+ }
494
+ function readPackageJson() {
495
+ const pkgPath = getPackageJsonPath();
496
+ if (!fs4.existsSync(pkgPath)) {
497
+ throw new Error("package.json not found in current directory");
498
+ }
499
+ try {
500
+ const content = fs4.readFileSync(pkgPath, "utf-8");
501
+ return JSON.parse(content);
502
+ } catch {
503
+ throw new Error("Failed to parse package.json");
504
+ }
505
+ }
506
+ function writePackageJson(pkg2) {
507
+ const pkgPath = getPackageJsonPath();
508
+ fs4.writeFileSync(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
509
+ }
510
+ function readActionPlugins() {
511
+ const pkg2 = readPackageJson();
512
+ return pkg2.actionPlugins || {};
513
+ }
514
+ function writeActionPlugins(plugins) {
515
+ const pkg2 = readPackageJson();
516
+ pkg2.actionPlugins = plugins;
517
+ writePackageJson(pkg2);
518
+ }
519
+ function isPluginInstalled(pluginName) {
520
+ const plugins = readActionPlugins();
521
+ return pluginName in plugins;
522
+ }
523
+ function getInstalledPluginVersion(pluginName) {
524
+ const plugins = readActionPlugins();
525
+ return plugins[pluginName] || null;
526
+ }
527
+ function npmInstall(tgzPath) {
528
+ console.log(`[action-plugin] Running npm install ${tgzPath}...`);
529
+ const result = spawnSync2("npm", ["install", tgzPath, "--no-save", "--no-package-lock", "--ignore-scripts"], {
530
+ cwd: getProjectRoot(),
531
+ stdio: "inherit"
532
+ });
533
+ if (result.error) {
534
+ throw new Error(`npm install failed: ${result.error.message}`);
535
+ }
536
+ if (result.status !== 0) {
537
+ throw new Error(`npm install failed with exit code ${result.status}`);
538
+ }
539
+ }
540
+ function getPackageVersion(pluginName) {
541
+ const pkgJsonPath = path4.join(getPluginPath(pluginName), "package.json");
542
+ if (!fs4.existsSync(pkgJsonPath)) {
543
+ return null;
544
+ }
545
+ try {
546
+ const content = fs4.readFileSync(pkgJsonPath, "utf-8");
547
+ const pkg2 = JSON.parse(content);
548
+ return pkg2.version || null;
549
+ } catch {
550
+ return null;
551
+ }
552
+ }
553
+ function readPluginPackageJson(pluginPath) {
554
+ const pkgJsonPath = path4.join(pluginPath, "package.json");
555
+ if (!fs4.existsSync(pkgJsonPath)) {
556
+ return null;
557
+ }
558
+ try {
559
+ const content = fs4.readFileSync(pkgJsonPath, "utf-8");
560
+ return JSON.parse(content);
561
+ } catch {
562
+ return null;
563
+ }
564
+ }
565
+ function extractTgzToNodeModules(tgzPath, pluginName) {
566
+ const nodeModulesPath = path4.join(getProjectRoot(), "node_modules");
567
+ const targetDir = path4.join(nodeModulesPath, pluginName);
568
+ const scopeDir = path4.dirname(targetDir);
569
+ if (!fs4.existsSync(scopeDir)) {
570
+ fs4.mkdirSync(scopeDir, { recursive: true });
571
+ }
572
+ if (fs4.existsSync(targetDir)) {
573
+ fs4.rmSync(targetDir, { recursive: true });
574
+ }
575
+ const tempDir = path4.join(nodeModulesPath, ".cache", "fullstack-cli", "extract-temp");
576
+ if (fs4.existsSync(tempDir)) {
577
+ fs4.rmSync(tempDir, { recursive: true });
578
+ }
579
+ fs4.mkdirSync(tempDir, { recursive: true });
580
+ try {
581
+ execSync(`tar -xzf "${tgzPath}" -C "${tempDir}"`, { stdio: "pipe" });
582
+ const extractedDir = path4.join(tempDir, "package");
583
+ if (fs4.existsSync(extractedDir)) {
584
+ fs4.renameSync(extractedDir, targetDir);
585
+ } else {
586
+ const files = fs4.readdirSync(tempDir);
587
+ if (files.length === 1) {
588
+ fs4.renameSync(path4.join(tempDir, files[0]), targetDir);
589
+ } else {
590
+ throw new Error("Unexpected tgz structure");
591
+ }
592
+ }
593
+ return targetDir;
594
+ } finally {
595
+ if (fs4.existsSync(tempDir)) {
596
+ fs4.rmSync(tempDir, { recursive: true });
597
+ }
598
+ }
599
+ }
600
+ function checkMissingPeerDeps(peerDeps) {
601
+ if (!peerDeps || Object.keys(peerDeps).length === 0) {
602
+ return [];
603
+ }
604
+ const missing = [];
605
+ const nodeModulesPath = path4.join(getProjectRoot(), "node_modules");
606
+ for (const [depName, _version] of Object.entries(peerDeps)) {
607
+ const depPath = path4.join(nodeModulesPath, depName);
608
+ if (!fs4.existsSync(depPath)) {
609
+ missing.push(depName);
610
+ }
611
+ }
612
+ return missing;
613
+ }
614
+ function installMissingDeps(deps) {
615
+ if (deps.length === 0) {
616
+ return;
617
+ }
618
+ console.log(`[action-plugin] Installing missing dependencies: ${deps.join(", ")}`);
619
+ const result = spawnSync2("npm", ["install", ...deps, "--no-save", "--no-package-lock"], {
620
+ cwd: getProjectRoot(),
621
+ stdio: "inherit"
622
+ });
623
+ if (result.error) {
624
+ throw new Error(`npm install failed: ${result.error.message}`);
625
+ }
626
+ if (result.status !== 0) {
627
+ throw new Error(`npm install failed with exit code ${result.status}`);
628
+ }
629
+ }
630
+ function removePluginDirectory(pluginName) {
631
+ const pluginPath = getPluginPath(pluginName);
632
+ if (fs4.existsSync(pluginPath)) {
633
+ fs4.rmSync(pluginPath, { recursive: true });
634
+ console.log(`[action-plugin] Removed ${pluginName}`);
635
+ }
636
+ }
637
+
638
+ // src/commands/action-plugin/api-client.ts
639
+ import { HttpClient as HttpClient2 } from "@lark-apaas/http-client";
640
+ import fs5 from "fs";
641
+ import path5 from "path";
642
+
643
+ // src/utils/http-client.ts
644
+ import { HttpClient } from "@lark-apaas/http-client";
645
+ var clientInstance = null;
646
+ function getHttpClient() {
647
+ if (!clientInstance) {
648
+ clientInstance = new HttpClient({
649
+ timeout: 3e4,
650
+ platform: {
651
+ enabled: true
652
+ }
653
+ });
654
+ clientInstance.interceptors.request.use((req) => {
655
+ req.headers["x-tt-env"] = "boe_miaoda_plugin";
656
+ return req;
657
+ });
658
+ }
659
+ return clientInstance;
660
+ }
661
+
662
+ // src/commands/action-plugin/api-client.ts
663
+ var PLUGIN_CACHE_DIR = "node_modules/.cache/fullstack-cli/plugins";
664
+ async function getPluginVersions(keys, latestOnly = true) {
665
+ const client = getHttpClient();
666
+ const response = await client.post(`/api/v1/studio/innerapi/plugins/-/versions/batch_get?keys=${keys.join(",")}&latest_only=${latestOnly}`);
667
+ if (!response.ok || response.status !== 200) {
668
+ throw new Error(`Failed to get plugin versions: ${response.status} ${response.statusText}`);
669
+ }
670
+ const result = await response.json();
671
+ console.log("getPluginVersions response:", result);
672
+ if (result.status_code !== "0") {
673
+ throw new Error(`API error: ${result.message}`);
674
+ }
675
+ return result.data.pluginVersions;
676
+ }
677
+ async function getPluginVersion(pluginKey, requestedVersion) {
678
+ const isLatest = requestedVersion === "latest";
679
+ const versions = await getPluginVersions([pluginKey], isLatest);
680
+ const pluginVersions = versions[pluginKey];
681
+ if (!pluginVersions || pluginVersions.length === 0) {
682
+ throw new Error(`Plugin not found: ${pluginKey}`);
683
+ }
684
+ if (isLatest) {
685
+ return pluginVersions[0];
686
+ }
687
+ const targetVersion = pluginVersions.find((v) => v.version === requestedVersion);
688
+ if (!targetVersion) {
689
+ throw new Error(
690
+ `Version ${requestedVersion} not found for plugin ${pluginKey}. Available versions: ${pluginVersions.map((v) => v.version).join(", ")}`
691
+ );
692
+ }
693
+ return targetVersion;
694
+ }
695
+ function parsePluginKey(key) {
696
+ const match = key.match(/^(@[^/]+)\/(.+)$/);
697
+ if (!match) {
698
+ throw new Error(`Invalid plugin key format: ${key}`);
699
+ }
700
+ return { scope: match[1], name: match[2] };
701
+ }
702
+ async function downloadFromInner(pluginKey, version) {
703
+ const client = getHttpClient();
704
+ const { scope, name } = parsePluginKey(pluginKey);
705
+ const url = `/api/v1/studio/innerapi/plugins/${scope}/${name}/versions/${version}/package`;
706
+ console.log(`[action-plugin] Downloading plugin from inner API: ${url}`);
707
+ const response = await client.get(url);
708
+ if (!response.ok) {
709
+ throw new Error(`Failed to download plugin: ${response.status} ${response.statusText}`);
710
+ }
711
+ const arrayBuffer = await response.arrayBuffer();
712
+ return Buffer.from(arrayBuffer);
713
+ }
714
+ async function downloadFromPublic(downloadURL) {
715
+ const client = new HttpClient2({
716
+ timeout: 6e4
717
+ });
718
+ const response = await client.get(downloadURL);
719
+ if (!response.ok) {
720
+ throw new Error(`Failed to download plugin from public URL: ${response.status} ${response.statusText}`);
721
+ }
722
+ const arrayBuffer = await response.arrayBuffer();
723
+ return Buffer.from(arrayBuffer);
724
+ }
725
+ function getPluginCacheDir() {
726
+ return path5.join(process.cwd(), PLUGIN_CACHE_DIR);
727
+ }
728
+ function ensureCacheDir() {
729
+ const cacheDir = getPluginCacheDir();
730
+ if (!fs5.existsSync(cacheDir)) {
731
+ fs5.mkdirSync(cacheDir, { recursive: true });
732
+ }
733
+ }
734
+ function getTempFilePath(pluginKey, version) {
735
+ ensureCacheDir();
736
+ const safeKey = pluginKey.replace(/[/@]/g, "_");
737
+ const filename = `${safeKey}-${version}.tgz`;
738
+ return path5.join(getPluginCacheDir(), filename);
739
+ }
740
+ async function downloadPlugin(pluginKey, requestedVersion) {
741
+ console.log(`[action-plugin] Fetching plugin info for ${pluginKey}@${requestedVersion}...`);
742
+ const pluginInfo = await getPluginVersion(pluginKey, requestedVersion);
743
+ console.log(`[action-plugin] Found: ${pluginInfo.name} v${pluginInfo.version}`);
744
+ console.log(`[action-plugin] Download approach: ${pluginInfo.downloadApproach}`);
745
+ let tgzBuffer;
746
+ if (pluginInfo.downloadApproach === "inner") {
747
+ console.log(`[action-plugin] Downloading from inner API...`);
748
+ tgzBuffer = await downloadFromInner(pluginKey, pluginInfo.version);
749
+ } else {
750
+ console.log(`[action-plugin] Downloading from public URL...`);
751
+ tgzBuffer = await downloadFromPublic(pluginInfo.downloadURL);
752
+ }
753
+ const tgzPath = getTempFilePath(pluginKey, pluginInfo.version);
754
+ fs5.writeFileSync(tgzPath, tgzBuffer);
755
+ console.log(`[action-plugin] Downloaded to ${tgzPath} (${(tgzBuffer.length / 1024).toFixed(2)} KB)`);
756
+ return {
757
+ tgzPath,
758
+ version: pluginInfo.version,
759
+ pluginInfo
760
+ };
761
+ }
762
+ function cleanupTempFile(tgzPath) {
763
+ try {
764
+ if (fs5.existsSync(tgzPath)) {
765
+ fs5.unlinkSync(tgzPath);
766
+ }
767
+ } catch {
768
+ }
769
+ }
770
+
771
+ // src/commands/action-plugin/init.handler.ts
772
+ async function installOneForInit(name, version) {
773
+ let tgzPath;
774
+ try {
775
+ const installedVersion = getPackageVersion(name);
776
+ if (installedVersion === version) {
777
+ return { name, version, success: true, skipped: true };
778
+ }
779
+ console.log(`[action-plugin] Installing ${name}@${version}...`);
780
+ const downloadResult = await downloadPlugin(name, version);
781
+ tgzPath = downloadResult.tgzPath;
782
+ const pluginDir = extractTgzToNodeModules(tgzPath, name);
783
+ const pluginPkg = readPluginPackageJson(pluginDir);
784
+ if (pluginPkg?.peerDependencies) {
785
+ const missingDeps = checkMissingPeerDeps(pluginPkg.peerDependencies);
786
+ if (missingDeps.length > 0) {
787
+ installMissingDeps(missingDeps);
788
+ }
789
+ }
790
+ console.log(`[action-plugin] \u2713 Installed ${name}@${version}`);
791
+ return { name, version, success: true };
792
+ } catch (error) {
793
+ const message = error instanceof Error ? error.message : String(error);
794
+ console.error(`[action-plugin] \u2717 Failed to install ${name}: ${message}`);
795
+ return { name, version, success: false, error: message };
796
+ } finally {
797
+ if (tgzPath) {
798
+ cleanupTempFile(tgzPath);
799
+ }
800
+ }
801
+ }
802
+ async function init() {
803
+ console.log("[action-plugin] Reading plugins from package.json...");
804
+ const plugins = readActionPlugins();
805
+ const entries = Object.entries(plugins);
806
+ if (entries.length === 0) {
807
+ console.log("[action-plugin] No plugins found in package.json");
808
+ return;
809
+ }
810
+ console.log(`[action-plugin] Found ${entries.length} plugin(s) to install
811
+ `);
812
+ const results = [];
813
+ for (const [name, version] of entries) {
814
+ const result = await installOneForInit(name, version);
815
+ results.push(result);
816
+ }
817
+ console.log("");
818
+ const successful = results.filter((r) => r.success && !r.skipped);
819
+ const skipped = results.filter((r) => r.skipped);
820
+ const failed = results.filter((r) => !r.success);
821
+ if (successful.length > 0 || skipped.length > 0) {
822
+ console.log(
823
+ `[action-plugin] \u2713 All plugins installed successfully (${successful.length + skipped.length}/${entries.length})`
824
+ );
825
+ if (skipped.length > 0) {
826
+ console.log(` Skipped (already installed): ${skipped.map((r) => r.name).join(", ")}`);
827
+ }
828
+ }
829
+ if (failed.length > 0) {
830
+ console.log(`[action-plugin] \u2717 Failed to install ${failed.length} plugin(s): ${failed.map((r) => r.name).join(", ")}`);
831
+ process.exit(1);
832
+ }
833
+ }
834
+
835
+ // src/commands/action-plugin/install.handler.ts
836
+ async function installOne(nameWithVersion) {
837
+ let tgzPath;
838
+ const { name, version: requestedVersion } = parsePluginName(nameWithVersion);
839
+ try {
840
+ console.log(`[action-plugin] Installing ${name}@${requestedVersion}...`);
841
+ const actualVersion = getPackageVersion(name);
842
+ if (actualVersion && requestedVersion !== "latest") {
843
+ if (actualVersion === requestedVersion) {
844
+ console.log(`[action-plugin] Plugin ${name}@${requestedVersion} is already installed`);
845
+ return { name, version: actualVersion, success: true, skipped: true };
846
+ }
847
+ }
848
+ if (actualVersion && requestedVersion === "latest") {
849
+ const latestInfo = await getPluginVersion(name, "latest");
850
+ if (actualVersion === latestInfo.version) {
851
+ console.log(`[action-plugin] Plugin ${name} is already up to date (version: ${actualVersion})`);
852
+ return { name, version: actualVersion, success: true, skipped: true };
853
+ }
854
+ console.log(`[action-plugin] Found newer version: ${latestInfo.version} (installed: ${actualVersion})`);
855
+ }
856
+ const downloadResult = await downloadPlugin(name, requestedVersion);
857
+ tgzPath = downloadResult.tgzPath;
858
+ console.log(`[action-plugin] Extracting to node_modules...`);
859
+ const pluginDir = extractTgzToNodeModules(tgzPath, name);
860
+ const pluginPkg = readPluginPackageJson(pluginDir);
861
+ if (pluginPkg?.peerDependencies) {
862
+ const missingDeps = checkMissingPeerDeps(pluginPkg.peerDependencies);
863
+ if (missingDeps.length > 0) {
864
+ installMissingDeps(missingDeps);
865
+ }
866
+ }
867
+ const installedVersion = getPackageVersion(name) || downloadResult.version;
868
+ const plugins = readActionPlugins();
869
+ plugins[name] = installedVersion;
870
+ writeActionPlugins(plugins);
871
+ console.log(`[action-plugin] Successfully installed ${name}@${installedVersion}`);
872
+ return { name, version: installedVersion, success: true };
873
+ } catch (error) {
874
+ const message = error instanceof Error ? error.message : String(error);
875
+ console.error(`[action-plugin] Failed to install ${name}: ${message}`);
876
+ return { name, version: requestedVersion, success: false, error: message };
877
+ } finally {
878
+ if (tgzPath) {
879
+ cleanupTempFile(tgzPath);
880
+ }
881
+ }
882
+ }
883
+ async function install(namesWithVersion) {
884
+ if (namesWithVersion.length === 0) {
885
+ console.error("[action-plugin] No plugin names provided");
886
+ process.exit(1);
887
+ }
888
+ const results = [];
889
+ for (const nameWithVersion of namesWithVersion) {
890
+ const result = await installOne(nameWithVersion);
891
+ results.push(result);
892
+ }
893
+ if (namesWithVersion.length > 1) {
894
+ console.log("\n[action-plugin] Installation summary:");
895
+ const successful = results.filter((r) => r.success && !r.skipped);
896
+ const skipped = results.filter((r) => r.skipped);
897
+ const failed = results.filter((r) => !r.success);
898
+ if (successful.length > 0) {
899
+ console.log(` \u2713 Installed: ${successful.map((r) => `${r.name}@${r.version}`).join(", ")}`);
900
+ }
901
+ if (skipped.length > 0) {
902
+ console.log(` \u25CB Skipped (up to date): ${skipped.map((r) => r.name).join(", ")}`);
903
+ }
904
+ if (failed.length > 0) {
905
+ console.log(` \u2717 Failed: ${failed.map((r) => r.name).join(", ")}`);
906
+ }
907
+ }
908
+ if (results.some((r) => !r.success)) {
909
+ process.exit(1);
910
+ }
911
+ }
912
+
913
+ // src/commands/action-plugin/update.handler.ts
914
+ async function updateOne(nameWithVersion) {
915
+ let tgzPath;
916
+ const { name } = parsePluginName(nameWithVersion);
917
+ try {
918
+ console.log(`[action-plugin] Updating ${name}...`);
919
+ if (!isPluginInstalled(name)) {
920
+ console.error(`[action-plugin] Plugin ${name} is not installed`);
921
+ return { name, success: false, notInstalled: true };
922
+ }
923
+ const oldVersion = getInstalledPluginVersion(name) || "unknown";
924
+ console.log(`[action-plugin] Current version: ${oldVersion}`);
925
+ const downloadResult = await downloadPlugin(name, "latest");
926
+ tgzPath = downloadResult.tgzPath;
927
+ if (oldVersion === downloadResult.version) {
928
+ console.log(`[action-plugin] Plugin ${name} is already up to date (version: ${downloadResult.version})`);
929
+ return { name, oldVersion, newVersion: downloadResult.version, success: true, skipped: true };
930
+ }
931
+ npmInstall(tgzPath);
932
+ const installedVersion = getPackageVersion(name) || downloadResult.version;
933
+ const plugins = readActionPlugins();
934
+ plugins[name] = installedVersion;
935
+ writeActionPlugins(plugins);
936
+ console.log(`[action-plugin] Successfully updated ${name} to ${installedVersion}`);
937
+ return { name, oldVersion, newVersion: installedVersion, success: true };
938
+ } catch (error) {
939
+ const message = error instanceof Error ? error.message : String(error);
940
+ console.error(`[action-plugin] Failed to update ${name}: ${message}`);
941
+ return { name, success: false, error: message };
942
+ } finally {
943
+ if (tgzPath) {
944
+ cleanupTempFile(tgzPath);
945
+ }
946
+ }
947
+ }
948
+ async function update(names) {
949
+ if (names.length === 0) {
950
+ console.error("[action-plugin] No plugin names provided");
951
+ process.exit(1);
952
+ }
953
+ const results = [];
954
+ for (const name of names) {
955
+ const result = await updateOne(name);
956
+ results.push(result);
957
+ }
958
+ if (names.length > 1) {
959
+ console.log("\n[action-plugin] Update summary:");
960
+ const updated = results.filter((r) => r.success && !r.skipped);
961
+ const skipped = results.filter((r) => r.skipped);
962
+ const notInstalled = results.filter((r) => r.notInstalled);
963
+ const failed = results.filter((r) => !r.success && !r.notInstalled);
964
+ if (updated.length > 0) {
965
+ console.log(` \u2713 Updated: ${updated.map((r) => `${r.name} (${r.oldVersion} \u2192 ${r.newVersion})`).join(", ")}`);
966
+ }
967
+ if (skipped.length > 0) {
968
+ console.log(` \u25CB Skipped (up to date): ${skipped.map((r) => r.name).join(", ")}`);
969
+ }
970
+ if (notInstalled.length > 0) {
971
+ console.log(` \u26A0 Not installed: ${notInstalled.map((r) => r.name).join(", ")}`);
972
+ }
973
+ if (failed.length > 0) {
974
+ console.log(` \u2717 Failed: ${failed.map((r) => r.name).join(", ")}`);
975
+ }
976
+ }
977
+ if (results.some((r) => !r.success && !r.notInstalled)) {
978
+ process.exit(1);
979
+ }
980
+ }
981
+
982
+ // src/commands/action-plugin/remove.handler.ts
983
+ async function remove(nameWithVersion) {
984
+ try {
985
+ const { name } = parsePluginName(nameWithVersion);
986
+ console.log(`[action-plugin] Removing ${name}...`);
987
+ if (!isPluginInstalled(name)) {
988
+ console.error(`[action-plugin] Plugin ${name} is not installed`);
989
+ process.exit(1);
990
+ }
991
+ removePluginDirectory(name);
992
+ const plugins = readActionPlugins();
993
+ delete plugins[name];
994
+ writeActionPlugins(plugins);
995
+ console.log(`[action-plugin] Successfully removed ${name}`);
996
+ } catch (error) {
997
+ const message = error instanceof Error ? error.message : String(error);
998
+ console.error(`[action-plugin] Failed to remove: ${message}`);
999
+ process.exit(1);
1000
+ }
1001
+ }
1002
+
1003
+ // src/commands/action-plugin/list.handler.ts
1004
+ async function list() {
1005
+ try {
1006
+ const plugins = Object.entries(readActionPlugins());
1007
+ if (plugins.length === 0) {
1008
+ console.log("[action-plugin] No plugins installed");
1009
+ console.log('[action-plugin] Use "action-plugin install <name>" to install a plugin');
1010
+ return;
1011
+ }
1012
+ console.log("[action-plugin] Installed plugins:\n");
1013
+ const nameWidth = Math.max(40, ...plugins.map(([name]) => name.length + 2));
1014
+ const versionWidth = 15;
1015
+ console.log(
1016
+ " " + "Plugin".padEnd(nameWidth) + "Version"
1017
+ );
1018
+ console.log(" " + "-".repeat(nameWidth + versionWidth));
1019
+ for (const [name, version] of plugins) {
1020
+ console.log(
1021
+ " " + name.padEnd(nameWidth) + version
1022
+ );
1023
+ }
1024
+ console.log(`
1025
+ Total: ${plugins.length} plugin(s)`);
1026
+ } catch (error) {
1027
+ const message = error instanceof Error ? error.message : String(error);
1028
+ console.error(`[action-plugin] Failed to list plugins: ${message}`);
1029
+ process.exit(1);
1030
+ }
1031
+ }
1032
+
1033
+ // src/commands/action-plugin/index.ts
1034
+ var initCommand = {
1035
+ name: "init",
1036
+ description: "Install all plugins from package.json",
1037
+ register(program) {
1038
+ program.command(this.name).description(this.description).action(async () => {
1039
+ await init();
1040
+ });
1041
+ }
1042
+ };
1043
+ var installCommand = {
1044
+ name: "install",
1045
+ description: "Install action plugin(s) (default: latest version)",
1046
+ aliases: ["i"],
1047
+ register(program) {
1048
+ program.command(this.name).alias("i").description(this.description).argument("<names...>", "Plugin name(s) (e.g., @office/feishu-create-group or @office/feishu-create-group@1.0.0)").action(async (names) => {
1049
+ await install(names);
1050
+ });
1051
+ }
1052
+ };
1053
+ var updateCommand = {
1054
+ name: "update",
1055
+ description: "Update action plugin(s) to the latest version",
1056
+ aliases: ["up"],
1057
+ register(program) {
1058
+ program.command(this.name).alias("up").description(this.description).argument("<names...>", "Plugin name(s) (e.g., @office/feishu-create-group)").action(async (names) => {
1059
+ await update(names);
1060
+ });
1061
+ }
1062
+ };
1063
+ var removeCommand = {
1064
+ name: "remove",
1065
+ description: "Remove an installed action plugin",
1066
+ aliases: ["rm"],
1067
+ register(program) {
1068
+ program.command(this.name).alias("rm").description(this.description).argument("<name>", "Plugin name (e.g., @office/feishu-create-group)").action(async (name) => {
1069
+ await remove(name);
1070
+ });
1071
+ }
1072
+ };
1073
+ var listCommand = {
1074
+ name: "list",
1075
+ description: "List all installed action plugins",
1076
+ aliases: ["ls"],
1077
+ register(program) {
1078
+ program.command(this.name).alias("ls").description(this.description).action(async () => {
1079
+ await list();
1080
+ });
1081
+ }
1082
+ };
1083
+ var actionPluginCommandGroup = {
1084
+ name: "action-plugin",
1085
+ description: "Manage action plugins",
1086
+ commands: [initCommand, installCommand, updateCommand, removeCommand, listCommand]
1087
+ };
1088
+
1089
+ // src/commands/capability/utils.ts
1090
+ import fs6 from "fs";
1091
+ import path6 from "path";
1092
+ var CAPABILITIES_DIR = "server/capabilities";
1093
+ function getProjectRoot2() {
1094
+ return process.cwd();
1095
+ }
1096
+ function getCapabilitiesDir() {
1097
+ return path6.join(getProjectRoot2(), CAPABILITIES_DIR);
1098
+ }
1099
+ function getCapabilityPath(id) {
1100
+ return path6.join(getCapabilitiesDir(), `${id}.json`);
1101
+ }
1102
+ function getPluginManifestPath(pluginKey) {
1103
+ return path6.join(getProjectRoot2(), "node_modules", pluginKey, "manifest.json");
1104
+ }
1105
+ function capabilitiesDirExists() {
1106
+ return fs6.existsSync(getCapabilitiesDir());
1107
+ }
1108
+ function listCapabilityIds() {
1109
+ const dir = getCapabilitiesDir();
1110
+ if (!fs6.existsSync(dir)) {
1111
+ return [];
1112
+ }
1113
+ const files = fs6.readdirSync(dir);
1114
+ return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
1115
+ }
1116
+ function readCapability(id) {
1117
+ const filePath = getCapabilityPath(id);
1118
+ if (!fs6.existsSync(filePath)) {
1119
+ throw new Error(`Capability not found: ${id}`);
1120
+ }
1121
+ try {
1122
+ const content = fs6.readFileSync(filePath, "utf-8");
1123
+ return JSON.parse(content);
1124
+ } catch (error) {
1125
+ if (error instanceof SyntaxError) {
1126
+ throw new Error(`Invalid JSON in capability file: ${id}.json`);
1127
+ }
1128
+ throw error;
1129
+ }
1130
+ }
1131
+ function readAllCapabilities() {
1132
+ const ids = listCapabilityIds();
1133
+ return ids.map((id) => readCapability(id));
1134
+ }
1135
+ function readPluginManifest(pluginKey) {
1136
+ const manifestPath = getPluginManifestPath(pluginKey);
1137
+ if (!fs6.existsSync(manifestPath)) {
1138
+ throw new Error(`Plugin not installed: ${pluginKey} (manifest.json not found)`);
1139
+ }
1140
+ try {
1141
+ const content = fs6.readFileSync(manifestPath, "utf-8");
1142
+ return JSON.parse(content);
1143
+ } catch (error) {
1144
+ if (error instanceof SyntaxError) {
1145
+ throw new Error(`Invalid JSON in plugin manifest: ${pluginKey}/manifest.json`);
1146
+ }
1147
+ throw error;
1148
+ }
1149
+ }
1150
+ function isDynamicSchema(schema) {
1151
+ return schema !== void 0 && typeof schema === "object" && "dynamic" in schema && schema.dynamic === true;
1152
+ }
1153
+ function hasValidParamsSchema(paramsSchema) {
1154
+ return paramsSchema !== void 0 && Object.keys(paramsSchema).length > 0;
1155
+ }
1156
+ async function loadPlugin(pluginKey) {
1157
+ try {
1158
+ const pluginPackage = (await import(pluginKey)).default;
1159
+ if (!pluginPackage || typeof pluginPackage.create !== "function") {
1160
+ throw new Error(`Plugin ${pluginKey} does not export a valid create function`);
1161
+ }
1162
+ return pluginPackage;
1163
+ } catch (error) {
1164
+ if (error.code === "MODULE_NOT_FOUND") {
1165
+ throw new Error(`Plugin not installed: ${pluginKey}`);
1166
+ }
1167
+ throw new Error(
1168
+ `Failed to load plugin ${pluginKey}: ${error instanceof Error ? error.message : String(error)}`
1169
+ );
1170
+ }
1171
+ }
1172
+ async function hydrateCapability(capability) {
1173
+ try {
1174
+ const manifest = readPluginManifest(capability.pluginKey);
1175
+ if (!manifest.actions || manifest.actions.length === 0) {
1176
+ throw new Error(`Plugin ${capability.pluginKey} has no actions defined`);
1177
+ }
1178
+ const hasDynamicSchema = manifest.actions.some(
1179
+ (action) => isDynamicSchema(action.inputSchema) || isDynamicSchema(action.outputSchema)
1180
+ );
1181
+ let pluginInstance = null;
1182
+ if (hasDynamicSchema) {
1183
+ const plugin = await loadPlugin(capability.pluginKey);
1184
+ pluginInstance = plugin.create(capability.formValue || {});
1185
+ }
1186
+ const actions = [];
1187
+ for (let index = 0; index < manifest.actions.length; index++) {
1188
+ const manifestAction = manifest.actions[index];
1189
+ let inputSchema;
1190
+ if (index === 0 && hasValidParamsSchema(capability.paramsSchema)) {
1191
+ inputSchema = capability.paramsSchema;
1192
+ } else if (isDynamicSchema(manifestAction.inputSchema)) {
1193
+ if (!pluginInstance) {
1194
+ throw new Error(`Plugin instance not available for dynamic schema`);
1195
+ }
1196
+ const jsonSchema = pluginInstance.getInputJsonSchema(manifestAction.key);
1197
+ if (!jsonSchema) {
1198
+ throw new Error(`Failed to get input schema for action: ${manifestAction.key}`);
1199
+ }
1200
+ inputSchema = jsonSchema;
1201
+ } else {
1202
+ inputSchema = manifestAction.inputSchema;
1203
+ }
1204
+ let outputSchema;
1205
+ if (isDynamicSchema(manifestAction.outputSchema)) {
1206
+ if (!pluginInstance) {
1207
+ throw new Error(`Plugin instance not available for dynamic schema`);
1208
+ }
1209
+ const jsonSchema = pluginInstance.getOutputJsonSchema(manifestAction.key, capability.formValue || {});
1210
+ if (!jsonSchema) {
1211
+ throw new Error(`Failed to get output schema for action: ${manifestAction.key}`);
1212
+ }
1213
+ outputSchema = jsonSchema;
1214
+ } else {
1215
+ outputSchema = manifestAction.outputSchema;
1216
+ }
1217
+ actions.push({
1218
+ key: manifestAction.key,
1219
+ inputSchema,
1220
+ outputSchema,
1221
+ outputMode: manifestAction.outputMode || ""
1222
+ });
1223
+ }
1224
+ return {
1225
+ id: capability.id,
1226
+ pluginKey: capability.pluginKey,
1227
+ pluginVersion: capability.pluginVersion,
1228
+ name: capability.name,
1229
+ description: capability.description,
1230
+ actions,
1231
+ createdAt: capability.createdAt,
1232
+ updatedAt: capability.updatedAt
1233
+ };
1234
+ } catch (error) {
1235
+ const message = error instanceof Error ? error.message : String(error);
1236
+ return {
1237
+ ...capability,
1238
+ _hydrateError: message
1239
+ };
1240
+ }
1241
+ }
1242
+ function formatJsonOutput(data, compact = false) {
1243
+ return compact ? JSON.stringify(data) : JSON.stringify(data, null, 2);
1244
+ }
1245
+ function printJson(data, compact = false) {
1246
+ console.log(formatJsonOutput(data, compact));
1247
+ }
1248
+ function logError(message) {
1249
+ console.error(`[capability] Error: ${message}`);
1250
+ }
1251
+
1252
+ // src/commands/capability/list.handler.ts
1253
+ async function list2(options) {
1254
+ try {
1255
+ if (!capabilitiesDirExists()) {
1256
+ logError("server/capabilities directory not found");
1257
+ process.exit(1);
1258
+ }
1259
+ if (options.id) {
1260
+ await listSingle(options.id, options.summary);
1261
+ } else {
1262
+ await listAll(options.summary);
1263
+ }
1264
+ } catch (error) {
1265
+ const message = error instanceof Error ? error.message : String(error);
1266
+ logError(message);
1267
+ process.exit(1);
1268
+ }
1269
+ }
1270
+ async function listSingle(id, summary) {
1271
+ const capability = readCapability(id);
1272
+ if (summary) {
1273
+ printJson(capability);
1274
+ } else {
1275
+ const hydrated = await hydrateCapability(capability);
1276
+ printJson(hydrated);
1277
+ }
1278
+ }
1279
+ async function listAll(summary) {
1280
+ const capabilities = readAllCapabilities();
1281
+ if (capabilities.length === 0) {
1282
+ printJson([]);
1283
+ return;
1284
+ }
1285
+ if (summary) {
1286
+ printJson(capabilities);
1287
+ } else {
1288
+ const hydrated = [];
1289
+ for (const capability of capabilities) {
1290
+ const result = await hydrateCapability(capability);
1291
+ hydrated.push(result);
1292
+ }
1293
+ printJson(hydrated);
1294
+ }
1295
+ }
1296
+
1297
+ // src/commands/capability/index.ts
1298
+ var listCommand2 = {
1299
+ name: "list",
1300
+ description: "List capability configurations",
1301
+ aliases: ["ls"],
1302
+ register(program) {
1303
+ program.command(this.name).alias("ls").description(this.description).option("--summary", "Return raw capability config (without hydration)").option("--id <id>", "Specify capability ID").action(async (options) => {
1304
+ await list2(options);
1305
+ });
1306
+ }
1307
+ };
1308
+ var capabilityCommandGroup = {
1309
+ name: "capability",
1310
+ description: "Manage capability configurations",
1311
+ commands: [listCommand2]
1312
+ };
1313
+
1314
+ // src/commands/migration/version-manager.ts
1315
+ import fs7 from "fs";
1316
+ import path7 from "path";
1317
+ var PACKAGE_JSON = "package.json";
1318
+ var VERSION_FIELD = "migrationVersion";
1319
+ function getPackageJsonPath2() {
1320
+ return path7.join(process.cwd(), PACKAGE_JSON);
1321
+ }
1322
+ function getCurrentVersion() {
1323
+ const pkgPath = getPackageJsonPath2();
1324
+ if (!fs7.existsSync(pkgPath)) {
1325
+ throw new Error("package.json not found");
1326
+ }
1327
+ const pkg2 = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
1328
+ return pkg2[VERSION_FIELD] ?? 0;
1329
+ }
1330
+ function setCurrentVersion(version) {
1331
+ const pkgPath = getPackageJsonPath2();
1332
+ const pkg2 = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
1333
+ pkg2[VERSION_FIELD] = version;
1334
+ fs7.writeFileSync(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
1335
+ }
1336
+
1337
+ // src/commands/migration/versions/v001_capability/json-migrator/detector.ts
1338
+ import fs9 from "fs";
1339
+ import path9 from "path";
1340
+
1341
+ // src/commands/migration/versions/v001_capability/utils.ts
1342
+ import fs8 from "fs";
1343
+ import path8 from "path";
1344
+ var CAPABILITIES_DIR2 = "server/capabilities";
1345
+ function getProjectRoot3() {
1346
+ return process.cwd();
1347
+ }
1348
+ function getCapabilitiesDir2() {
1349
+ return path8.join(getProjectRoot3(), CAPABILITIES_DIR2);
1350
+ }
1351
+ function getPluginManifestPath2(pluginKey) {
1352
+ return path8.join(getProjectRoot3(), "node_modules", pluginKey, "manifest.json");
1353
+ }
1354
+
1355
+ // src/commands/migration/versions/v001_capability/json-migrator/detector.ts
1356
+ function detectJsonMigration() {
1357
+ const capabilitiesDir = getCapabilitiesDir2();
1358
+ const oldFilePath = path9.join(capabilitiesDir, "capabilities.json");
1359
+ if (!fs9.existsSync(oldFilePath)) {
1360
+ return {
1361
+ needsMigration: false,
1362
+ reason: "capabilities.json not found"
1363
+ };
1364
+ }
1365
+ try {
1366
+ const content = fs9.readFileSync(oldFilePath, "utf-8");
1367
+ const parsed = JSON.parse(content);
1368
+ const capabilities = Array.isArray(parsed) ? parsed : [];
1369
+ return {
1370
+ needsMigration: true,
1371
+ oldCapabilities: capabilities,
1372
+ oldFilePath
1373
+ };
1374
+ } catch (error) {
1375
+ const errorMessage = error instanceof Error ? error.message : String(error);
1376
+ console.warn(`[migration] Warning: capabilities.json is not valid JSON, skipping migration`);
1377
+ console.warn(`[migration] Error: ${errorMessage}`);
1378
+ return {
1379
+ needsMigration: false,
1380
+ reason: `capabilities.json is not valid JSON: ${errorMessage}`
1381
+ };
1382
+ }
1383
+ }
1384
+
1385
+ // src/commands/migration/versions/v001_capability/check.ts
1386
+ async function check(options) {
1387
+ const detection = detectJsonMigration();
1388
+ if (!detection.needsMigration) {
1389
+ return {
1390
+ needsMigration: false,
1391
+ message: detection.reason || "No migration needed"
1392
+ };
1393
+ }
1394
+ const capabilityCount = detection.oldCapabilities?.length || 0;
1395
+ return {
1396
+ needsMigration: true,
1397
+ message: `found ${capabilityCount} capabilities to migrate`,
1398
+ items: [
1399
+ `server/capabilities/capabilities.json \u2192 server/capabilities/*.json`,
1400
+ `${capabilityCount} capabilities will be converted to new format`
1401
+ ]
1402
+ };
1403
+ }
1404
+
1405
+ // src/commands/migration/versions/v001_capability/json-migrator/index.ts
1406
+ import fs10 from "fs";
1407
+ import path10 from "path";
1408
+
1409
+ // src/commands/migration/versions/v001_capability/mapping.ts
1410
+ var DEFAULT_PLUGIN_VERSION = "1.0.0";
1411
+ var PLUGIN_MAPPING = {
1412
+ // 飞书相关
1413
+ "@official-plugins/send-feishu-message": {
1414
+ sourceActionID: "official_feishu/send_message",
1415
+ pluginVersion: "1.0.0",
1416
+ actionName: "send_feishu_message"
1417
+ },
1418
+ "@official-plugins/feishu-group-create": {
1419
+ sourceActionID: "official_feishu/create_group",
1420
+ pluginVersion: "1.0.0",
1421
+ actionName: "createGroup"
1422
+ },
1423
+ // AI 相关
1424
+ "@official-plugins/ai-text-generate": {
1425
+ sourceActionID: "official_ai/text_generate",
1426
+ pluginVersion: "1.0.0",
1427
+ actionName: "textGenerate"
1428
+ },
1429
+ "@official-plugins/ai-image-understanding": {
1430
+ sourceActionID: "official_ai/image_understanding",
1431
+ pluginVersion: "1.0.0",
1432
+ actionName: "imageUnderstanding"
1433
+ },
1434
+ "@official-plugins/ai-text-to-image": {
1435
+ sourceActionID: "official_ai/image_generate",
1436
+ pluginVersion: "1.0.0",
1437
+ actionName: "textToImage"
1438
+ }
1439
+ };
1440
+ function getPluginInfoBySourceActionID(sourceActionID) {
1441
+ for (const [pluginKey, item] of Object.entries(PLUGIN_MAPPING)) {
1442
+ if (item.sourceActionID === sourceActionID) {
1443
+ return {
1444
+ pluginKey,
1445
+ pluginVersion: item.pluginVersion ?? DEFAULT_PLUGIN_VERSION,
1446
+ actionName: item.actionName
1447
+ };
1448
+ }
1449
+ }
1450
+ throw new Error(
1451
+ `Unknown sourceActionID: "${sourceActionID}". This sourceActionID is not in the built-in mapping. Please contact the developer to add it.`
1452
+ );
1453
+ }
1454
+ function getActionNameByPluginKey(pluginKey) {
1455
+ const item = PLUGIN_MAPPING[pluginKey];
1456
+ if (!item) {
1457
+ throw new Error(
1458
+ `Unknown pluginKey: "${pluginKey}". This pluginKey is not in the built-in mapping. Please contact the developer to add it.`
1459
+ );
1460
+ }
1461
+ return item.actionName;
1462
+ }
1463
+
1464
+ // src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/capabilities/send-feishu-message.ts
1465
+ function transformSendFeishuMessage(input) {
1466
+ const actionInput = input;
1467
+ const cardContent = actionInput.card_content;
1468
+ const header = cardContent?.header;
1469
+ const bodyElements = cardContent?.body?.elements;
1470
+ const title = {
1471
+ title: header?.title?.content ?? "",
1472
+ titleColor: header?.template
1473
+ };
1474
+ const markdownElement = bodyElements?.find((el) => el.tag === "markdown");
1475
+ const content = markdownElement?.content ?? "";
1476
+ const columnSet = bodyElements?.find((el) => el.tag === "column_set");
1477
+ const buttons = columnSet?.columns?.flatMap(
1478
+ (column) => column.elements?.filter((el) => el.tag === "button").map((btn) => ({
1479
+ style: btn.type,
1480
+ text: btn.text?.content,
1481
+ url: btn.behaviors?.[0]?.default_url
1482
+ })) ?? []
1483
+ ) ?? [];
1484
+ return {
1485
+ sender: "bot",
1486
+ receiverUserList: actionInput.receiver_user_list ?? [],
1487
+ receiverGroupList: actionInput.receiver_group_list ?? [],
1488
+ title,
1489
+ content,
1490
+ buttons
1491
+ };
1492
+ }
1493
+
1494
+ // src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/capabilities/feishu-group-create.ts
1495
+ function transformFeishuGroupCreate(input) {
1496
+ const actionInput = input;
1497
+ return {
1498
+ owner: actionInput.owner,
1499
+ members: actionInput.members ?? [],
1500
+ groupName: actionInput.group_name ?? "",
1501
+ groupDescription: actionInput.group_description ?? "",
1502
+ welcomeMessage: actionInput.welcome_message ?? ""
1503
+ };
1504
+ }
1505
+
1506
+ // src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/utils.ts
1507
+ function convertToNumber(value, defaultValue) {
1508
+ if (value === void 0 || value === "") {
1509
+ return defaultValue;
1510
+ }
1511
+ const num = Number(value);
1512
+ return isNaN(num) ? defaultValue : num;
1513
+ }
1514
+
1515
+ // src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/capabilities/ai-text-generate.ts
1516
+ var DEFAULT_MODEL_ID = "87";
1517
+ var DEFAULT_MAX_TOKENS = 8192;
1518
+ var DEFAULT_TEMPERATURE = 0.7;
1519
+ function transformAITextGenerate(input) {
1520
+ const actionInput = input;
1521
+ return {
1522
+ modelID: DEFAULT_MODEL_ID,
1523
+ prompt: actionInput.prompt ?? "",
1524
+ modelParams: {
1525
+ maxTokens: convertToNumber(actionInput.max_tokens, DEFAULT_MAX_TOKENS),
1526
+ temperature: convertToNumber(actionInput.temperature, DEFAULT_TEMPERATURE)
1527
+ }
1528
+ };
1529
+ }
1530
+
1531
+ // src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/capabilities/ai-image-understanding.ts
1532
+ var DEFAULT_MODEL_ID2 = "87";
1533
+ var DEFAULT_MAX_TOKENS2 = 8192;
1534
+ var DEFAULT_TEMPERATURE2 = 0.7;
1535
+ function transformAIImageUnderstanding(input) {
1536
+ const actionInput = input;
1537
+ return {
1538
+ prompt: actionInput.prompt ?? "",
1539
+ images: actionInput.image_list ?? [],
1540
+ modelID: DEFAULT_MODEL_ID2,
1541
+ modelParams: {
1542
+ maxTokens: convertToNumber(actionInput.max_tokens, DEFAULT_MAX_TOKENS2),
1543
+ temperature: convertToNumber(actionInput.temperature, DEFAULT_TEMPERATURE2)
1544
+ }
1545
+ };
1546
+ }
1547
+
1548
+ // src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/capabilities/ai-text-to-image.ts
1549
+ var DEFAULT_RATIO = "1:1";
1550
+ var DEFAULT_WATERMARK = true;
1551
+ function transformAITextToImage(input) {
1552
+ const actionInput = input;
1553
+ return {
1554
+ prompt: actionInput.prompt ?? "",
1555
+ ratio: actionInput.image_ratio ?? DEFAULT_RATIO,
1556
+ watermark: actionInput.watermark ?? DEFAULT_WATERMARK
1557
+ };
1558
+ }
1559
+
1560
+ // src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/index.ts
1561
+ var TRANSFORMER_MAP = {
1562
+ "official_feishu/send_message": transformSendFeishuMessage,
1563
+ "official_feishu/create_group": transformFeishuGroupCreate,
1564
+ "official_ai/text_generate": transformAITextGenerate,
1565
+ "official_ai/image_understanding": transformAIImageUnderstanding,
1566
+ "official_ai/image_generate": transformAITextToImage
1567
+ };
1568
+ function transformFormValue(sourceActionID, actionInput) {
1569
+ const transformer = TRANSFORMER_MAP[sourceActionID];
1570
+ if (!transformer) {
1571
+ throw new Error(
1572
+ `No formValue transformer found for sourceActionID: "${sourceActionID}". Please implement the transformer and add it to TRANSFORMER_MAP.`
1573
+ );
1574
+ }
1575
+ return transformer(actionInput);
1576
+ }
1577
+
1578
+ // src/commands/migration/versions/v001_capability/json-migrator/transformer.ts
1579
+ function transformCapability(old) {
1580
+ const { pluginKey, pluginVersion } = getPluginInfoBySourceActionID(old.sourceActionID);
1581
+ const formValue = transformFormValue(old.sourceActionID, old.actionInput);
1582
+ return {
1583
+ id: old.id,
1584
+ pluginKey,
1585
+ pluginVersion,
1586
+ name: old.name,
1587
+ description: old.desc,
1588
+ paramsSchema: old.inputSchema,
1589
+ formValue,
1590
+ createdAt: old.createdAt,
1591
+ updatedAt: old.updatedAt
1592
+ };
1593
+ }
1594
+ function transformCapabilities(oldCapabilities) {
1595
+ return oldCapabilities.map((old) => transformCapability(old));
1596
+ }
1597
+
1598
+ // src/commands/migration/versions/v001_capability/json-migrator/index.ts
1599
+ function loadExistingCapabilities() {
1600
+ const capabilitiesDir = getCapabilitiesDir2();
1601
+ if (!fs10.existsSync(capabilitiesDir)) {
1602
+ return [];
1603
+ }
1604
+ const files = fs10.readdirSync(capabilitiesDir);
1605
+ const capabilities = [];
1606
+ for (const file of files) {
1607
+ if (file === "capabilities.json" || !file.endsWith(".json")) {
1608
+ continue;
1609
+ }
1610
+ try {
1611
+ const filePath = path10.join(capabilitiesDir, file);
1612
+ const content = fs10.readFileSync(filePath, "utf-8");
1613
+ const capability = JSON.parse(content);
1614
+ if (capability.id && capability.pluginKey) {
1615
+ capabilities.push(capability);
1616
+ }
1617
+ } catch {
1618
+ }
1619
+ }
1620
+ return capabilities;
1621
+ }
1622
+ async function migrateJsonFiles(options) {
1623
+ const detection = detectJsonMigration();
1624
+ if (!detection.needsMigration) {
1625
+ const existingCapabilities = loadExistingCapabilities();
1626
+ return {
1627
+ success: true,
1628
+ skipped: true,
1629
+ reason: detection.reason,
1630
+ count: existingCapabilities.length,
1631
+ capabilities: existingCapabilities
1632
+ };
1633
+ }
1634
+ const { oldCapabilities, oldFilePath } = detection;
1635
+ if (!oldCapabilities || !oldFilePath) {
1636
+ return {
1637
+ success: false,
1638
+ skipped: false,
1639
+ reason: "Detection error: missing data",
1640
+ count: 0,
1641
+ capabilities: []
1642
+ };
1643
+ }
1644
+ let newCapabilities;
1645
+ try {
1646
+ newCapabilities = transformCapabilities(oldCapabilities);
1647
+ } catch (error) {
1648
+ return {
1649
+ success: false,
1650
+ skipped: false,
1651
+ reason: error instanceof Error ? error.message : String(error),
1652
+ count: 0,
1653
+ capabilities: []
1654
+ };
1655
+ }
1656
+ if (options.dryRun) {
1657
+ console.log(` Would create ${newCapabilities.length} capability files:`);
1658
+ for (const cap of newCapabilities) {
1659
+ console.log(` - ${cap.id}.json (pluginKey: ${cap.pluginKey})`);
1660
+ }
1661
+ return {
1662
+ success: true,
1663
+ skipped: false,
1664
+ count: newCapabilities.length,
1665
+ capabilities: newCapabilities
1666
+ };
1667
+ }
1668
+ const capabilitiesDir = getCapabilitiesDir2();
1669
+ for (const cap of newCapabilities) {
1670
+ const filePath = path10.join(capabilitiesDir, `${cap.id}.json`);
1671
+ const content = JSON.stringify(cap, null, 2);
1672
+ fs10.writeFileSync(filePath, content, "utf-8");
1673
+ console.log(` \u2713 Created: ${cap.id}.json`);
1674
+ }
1675
+ return {
1676
+ success: true,
1677
+ skipped: false,
1678
+ count: newCapabilities.length,
1679
+ capabilities: newCapabilities
1680
+ };
1681
+ }
1682
+
1683
+ // src/commands/migration/versions/v001_capability/plugin-installer/detector.ts
1684
+ import fs11 from "fs";
1685
+ function isPluginInstalled2(pluginKey) {
1686
+ const manifestPath = getPluginManifestPath2(pluginKey);
1687
+ return fs11.existsSync(manifestPath);
1688
+ }
1689
+ function detectPluginsToInstall(capabilities) {
1690
+ const pluginKeys = /* @__PURE__ */ new Set();
1691
+ for (const cap of capabilities) {
1692
+ pluginKeys.add(cap.pluginKey);
1693
+ }
1694
+ const toInstall = [];
1695
+ const alreadyInstalled = [];
1696
+ for (const pluginKey of pluginKeys) {
1697
+ if (isPluginInstalled2(pluginKey)) {
1698
+ alreadyInstalled.push(pluginKey);
1699
+ } else {
1700
+ toInstall.push(pluginKey);
1701
+ }
1702
+ }
1703
+ return { toInstall, alreadyInstalled };
1704
+ }
1705
+
1706
+ // src/commands/migration/versions/v001_capability/plugin-installer/index.ts
1707
+ async function installPlugins(capabilities, options) {
1708
+ const detection = detectPluginsToInstall(capabilities);
1709
+ const result = {
1710
+ installed: [],
1711
+ alreadyInstalled: detection.alreadyInstalled,
1712
+ failed: []
1713
+ };
1714
+ if (detection.toInstall.length === 0) {
1715
+ return result;
1716
+ }
1717
+ if (options.dryRun) {
1718
+ console.log(` Would install ${detection.toInstall.length} plugins:`);
1719
+ for (const pluginKey of detection.toInstall) {
1720
+ console.log(` - ${pluginKey}`);
1721
+ }
1722
+ result.installed = detection.toInstall;
1723
+ return result;
1724
+ }
1725
+ console.log(` \u2B07 Installing ${detection.toInstall.length} plugins...`);
1726
+ for (const pluginKey of detection.toInstall) {
1727
+ console.log(` - ${pluginKey}`);
1728
+ }
1729
+ try {
1730
+ const originalExit = process.exit;
1731
+ let exitCalled = false;
1732
+ process.exit = ((code) => {
1733
+ exitCalled = true;
1734
+ if (code !== 0) {
1735
+ throw new Error(`Plugin installation failed with exit code ${code}`);
1736
+ }
1737
+ });
1738
+ try {
1739
+ await install(detection.toInstall);
1740
+ result.installed = detection.toInstall;
1741
+ } catch (error) {
1742
+ for (const pluginKey of detection.toInstall) {
1743
+ result.failed.push({
1744
+ pluginKey,
1745
+ error: error instanceof Error ? error.message : String(error)
1746
+ });
1747
+ }
1748
+ } finally {
1749
+ process.exit = originalExit;
1750
+ }
1751
+ } catch (error) {
1752
+ for (const pluginKey of detection.toInstall) {
1753
+ result.failed.push({
1754
+ pluginKey,
1755
+ error: error instanceof Error ? error.message : String(error)
1756
+ });
1757
+ }
1758
+ }
1759
+ return result;
1760
+ }
1761
+
1762
+ // src/commands/migration/versions/v001_capability/code-migrator/index.ts
1763
+ import path12 from "path";
1764
+ import { Project } from "ts-morph";
1765
+
1766
+ // src/commands/migration/versions/v001_capability/code-migrator/scanner.ts
1767
+ import fs12 from "fs";
1768
+ import path11 from "path";
1769
+ var EXCLUDED_DIRS = [
1770
+ "node_modules",
1771
+ "dist",
1772
+ "build",
1773
+ ".git",
1774
+ "__test__",
1775
+ "__tests__"
1776
+ ];
1777
+ var EXCLUDED_PATTERNS = [
1778
+ /\.spec\.ts$/,
1779
+ /\.test\.ts$/,
1780
+ /\.d\.ts$/
1781
+ ];
1782
+ function scanDirectory(dir, files = []) {
1783
+ const entries = fs12.readdirSync(dir, { withFileTypes: true });
1784
+ for (const entry of entries) {
1785
+ const fullPath = path11.join(dir, entry.name);
1786
+ if (entry.isDirectory()) {
1787
+ if (EXCLUDED_DIRS.includes(entry.name)) {
1788
+ continue;
1789
+ }
1790
+ scanDirectory(fullPath, files);
1791
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
1792
+ const shouldExclude = EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
1793
+ if (!shouldExclude) {
1794
+ files.push(fullPath);
1795
+ }
1796
+ }
1797
+ }
1798
+ return files;
1799
+ }
1800
+ function scanServerFiles() {
1801
+ const serverDir = path11.join(getProjectRoot3(), "server");
1802
+ if (!fs12.existsSync(serverDir)) {
1803
+ return [];
1804
+ }
1805
+ return scanDirectory(serverDir);
1806
+ }
1807
+ function hasCapabilityImport(filePath) {
1808
+ const content = fs12.readFileSync(filePath, "utf-8");
1809
+ return /import\s+.*from\s+['"][^'"]*capabilities[^'"]*['"]/.test(content);
1810
+ }
1811
+ function scanFilesToMigrate() {
1812
+ const allFiles = scanServerFiles();
1813
+ return allFiles.filter(hasCapabilityImport);
1814
+ }
1815
+
1816
+ // src/commands/migration/versions/v001_capability/code-migrator/analyzers/import-analyzer.ts
1817
+ function extractCapabilityId(importPath) {
1818
+ const match = importPath.match(/capabilities\/([^/]+)$/);
1819
+ return match ? match[1] : null;
1820
+ }
1821
+ function isCapabilityImport(importPath) {
1822
+ return importPath.includes("capabilities/") || importPath.includes("capabilities\\");
1823
+ }
1824
+ function analyzeImports(sourceFile) {
1825
+ const imports = [];
1826
+ const importDeclarations = sourceFile.getImportDeclarations();
1827
+ for (const importDecl of importDeclarations) {
1828
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
1829
+ if (!isCapabilityImport(moduleSpecifier)) {
1830
+ continue;
1831
+ }
1832
+ const capabilityId = extractCapabilityId(moduleSpecifier);
1833
+ if (!capabilityId) {
1834
+ continue;
1835
+ }
1836
+ const namedImports = importDecl.getNamedImports();
1837
+ for (const namedImport of namedImports) {
1838
+ const importName = namedImport.getAliasNode()?.getText() || namedImport.getName();
1839
+ imports.push({
1840
+ importName,
1841
+ capabilityId,
1842
+ start: importDecl.getStart(),
1843
+ end: importDecl.getEnd(),
1844
+ text: importDecl.getText()
1845
+ });
1846
+ }
1847
+ const namespaceImport = importDecl.getNamespaceImport();
1848
+ if (namespaceImport) {
1849
+ imports.push({
1850
+ importName: namespaceImport.getText(),
1851
+ capabilityId,
1852
+ start: importDecl.getStart(),
1853
+ end: importDecl.getEnd(),
1854
+ text: importDecl.getText()
1855
+ });
1856
+ }
1857
+ const defaultImport = importDecl.getDefaultImport();
1858
+ if (defaultImport) {
1859
+ imports.push({
1860
+ importName: defaultImport.getText(),
1861
+ capabilityId,
1862
+ start: importDecl.getStart(),
1863
+ end: importDecl.getEnd(),
1864
+ text: importDecl.getText()
1865
+ });
1866
+ }
1867
+ }
1868
+ return imports;
1869
+ }
1870
+
1871
+ // src/commands/migration/versions/v001_capability/code-migrator/analyzers/call-site-analyzer.ts
1872
+ import { SyntaxKind } from "ts-morph";
1873
+ function analyzeCallSites(sourceFile, imports) {
1874
+ const callSites = [];
1875
+ const importMap = /* @__PURE__ */ new Map();
1876
+ for (const imp of imports) {
1877
+ importMap.set(imp.importName, imp.capabilityId);
1878
+ }
1879
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
1880
+ for (const callExpr of callExpressions) {
1881
+ const expression = callExpr.getExpression();
1882
+ if (expression.getKind() === SyntaxKind.Identifier) {
1883
+ const functionName = expression.getText();
1884
+ const capabilityId = importMap.get(functionName);
1885
+ if (capabilityId) {
1886
+ callSites.push({
1887
+ functionName,
1888
+ capabilityId,
1889
+ start: callExpr.getStart(),
1890
+ end: callExpr.getEnd(),
1891
+ line: callExpr.getStartLineNumber(),
1892
+ text: callExpr.getText()
1893
+ });
1894
+ }
1895
+ } else if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
1896
+ const propAccess = expression.asKind(SyntaxKind.PropertyAccessExpression);
1897
+ if (propAccess) {
1898
+ const objectExpr = propAccess.getExpression();
1899
+ if (objectExpr.getKind() === SyntaxKind.Identifier) {
1900
+ const objectName = objectExpr.getText();
1901
+ const capabilityId = importMap.get(objectName);
1902
+ if (capabilityId) {
1903
+ callSites.push({
1904
+ functionName: `${objectName}.${propAccess.getName()}`,
1905
+ capabilityId,
1906
+ start: callExpr.getStart(),
1907
+ end: callExpr.getEnd(),
1908
+ line: callExpr.getStartLineNumber(),
1909
+ text: callExpr.getText()
1910
+ });
1911
+ }
1912
+ }
1913
+ }
1914
+ }
1915
+ }
1916
+ return callSites;
1917
+ }
1918
+
1919
+ // src/commands/migration/versions/v001_capability/code-migrator/analyzers/class-analyzer.ts
1920
+ function hasDecorator(classDecl, decoratorName) {
1921
+ const decorators = classDecl.getDecorators();
1922
+ return decorators.some((decorator) => {
1923
+ const name = decorator.getName();
1924
+ return name === decoratorName;
1925
+ });
1926
+ }
1927
+ function analyzeClass(sourceFile) {
1928
+ const classes = sourceFile.getClasses();
1929
+ let classInfo;
1930
+ for (const classDecl of classes) {
1931
+ const name = classDecl.getName();
1932
+ if (!name) continue;
1933
+ const isInjectable = hasDecorator(classDecl, "Injectable");
1934
+ const isController = hasDecorator(classDecl, "Controller");
1935
+ if (classInfo && classInfo.isInjectable && !isInjectable && !isController) {
1936
+ continue;
1937
+ }
1938
+ const info = {
1939
+ name,
1940
+ isInjectable,
1941
+ isController,
1942
+ constructorParamCount: 0
1943
+ };
1944
+ const classBody = classDecl.getChildSyntaxListOrThrow();
1945
+ info.classBodyStart = classBody.getStart();
1946
+ const constructors = classDecl.getConstructors();
1947
+ if (constructors.length > 0) {
1948
+ const ctor = constructors[0];
1949
+ info.constructorParamCount = ctor.getParameters().length;
1950
+ info.constructorStart = ctor.getStart();
1951
+ info.constructorEnd = ctor.getEnd();
1952
+ const params = ctor.getParameters();
1953
+ if (params.length > 0) {
1954
+ const lastParam = params[params.length - 1];
1955
+ info.constructorParamsEnd = lastParam.getEnd();
1956
+ } else {
1957
+ info.constructorParamsEnd = ctor.getStart();
1958
+ }
36
1959
  }
37
- });
38
- // sync 命令
39
- cli
40
- .command('sync', 'Sync template files (scripts, configs) to user project')
41
- .option('--disable-gen-openapi', 'Disable generating openapi.ts')
42
- .example('fullstack-cli sync')
43
- .action(async (options) => {
1960
+ if (isInjectable || isController || !classInfo) {
1961
+ classInfo = info;
1962
+ }
1963
+ }
1964
+ return classInfo;
1965
+ }
1966
+ function canAutoMigrate(classInfo) {
1967
+ if (!classInfo) {
1968
+ return {
1969
+ canMigrate: false,
1970
+ reason: "No class found in file"
1971
+ };
1972
+ }
1973
+ if (!classInfo.isInjectable && !classInfo.isController) {
1974
+ return {
1975
+ canMigrate: false,
1976
+ reason: `Class "${classInfo.name}" is not @Injectable or @Controller`
1977
+ };
1978
+ }
1979
+ return { canMigrate: true };
1980
+ }
1981
+
1982
+ // src/commands/migration/versions/v001_capability/code-migrator/transformers/import-transformer.ts
1983
+ var CORE_MODULE = "@lark-apaas/fullstack-nestjs-core";
1984
+ var NESTJS_COMMON_MODULE = "@nestjs/common";
1985
+ function hasCapabilityServiceImport(sourceFile) {
1986
+ const imports = sourceFile.getImportDeclarations();
1987
+ for (const importDecl of imports) {
1988
+ if (importDecl.getModuleSpecifierValue() === CORE_MODULE) {
1989
+ const namedImports = importDecl.getNamedImports();
1990
+ if (namedImports.some((ni) => ni.getName() === "CapabilityService")) {
1991
+ return true;
1992
+ }
1993
+ }
1994
+ }
1995
+ return false;
1996
+ }
1997
+ function hasInjectImport(sourceFile) {
1998
+ const imports = sourceFile.getImportDeclarations();
1999
+ for (const importDecl of imports) {
2000
+ if (importDecl.getModuleSpecifierValue() === NESTJS_COMMON_MODULE) {
2001
+ const namedImports = importDecl.getNamedImports();
2002
+ if (namedImports.some((ni) => ni.getName() === "Inject")) {
2003
+ return true;
2004
+ }
2005
+ }
2006
+ }
2007
+ return false;
2008
+ }
2009
+ function transformImports(sourceFile, capabilityImports) {
2010
+ let importAdded = false;
2011
+ let importsRemoved = 0;
2012
+ const importPaths = new Set(capabilityImports.map((imp) => {
2013
+ const match = imp.text.match(/from\s+['"]([^'"]+)['"]/);
2014
+ return match ? match[1] : "";
2015
+ }));
2016
+ const importDeclarations = sourceFile.getImportDeclarations();
2017
+ for (const importDecl of importDeclarations) {
2018
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
2019
+ if (importPaths.has(moduleSpecifier)) {
2020
+ importDecl.remove();
2021
+ importsRemoved++;
2022
+ }
2023
+ }
2024
+ if (!hasCapabilityServiceImport(sourceFile)) {
2025
+ const existingCoreImport = sourceFile.getImportDeclaration(CORE_MODULE);
2026
+ if (existingCoreImport) {
2027
+ const namedImports = existingCoreImport.getNamedImports();
2028
+ const existingNames = namedImports.map((ni) => ni.getName());
2029
+ if (!existingNames.includes("CapabilityService")) {
2030
+ existingCoreImport.addNamedImport("CapabilityService");
2031
+ }
2032
+ if (!existingNames.includes("migrationAdaptor")) {
2033
+ existingCoreImport.addNamedImport("migrationAdaptor");
2034
+ }
2035
+ } else {
2036
+ sourceFile.addImportDeclaration({
2037
+ moduleSpecifier: CORE_MODULE,
2038
+ namedImports: ["CapabilityService", "migrationAdaptor"]
2039
+ });
2040
+ }
2041
+ importAdded = true;
2042
+ }
2043
+ if (!hasInjectImport(sourceFile)) {
2044
+ const existingNestImport = sourceFile.getImportDeclaration(NESTJS_COMMON_MODULE);
2045
+ if (existingNestImport) {
2046
+ const namedImports = existingNestImport.getNamedImports();
2047
+ const existingNames = namedImports.map((ni) => ni.getName());
2048
+ if (!existingNames.includes("Inject")) {
2049
+ existingNestImport.addNamedImport("Inject");
2050
+ }
2051
+ } else {
2052
+ sourceFile.addImportDeclaration({
2053
+ moduleSpecifier: NESTJS_COMMON_MODULE,
2054
+ namedImports: ["Inject"]
2055
+ });
2056
+ }
2057
+ importAdded = true;
2058
+ }
2059
+ return { importAdded, importsRemoved };
2060
+ }
2061
+
2062
+ // src/commands/migration/versions/v001_capability/code-migrator/transformers/injection-transformer.ts
2063
+ import { Scope } from "ts-morph";
2064
+ var PARAM_NAME = "capabilityService";
2065
+ var PARAM_TYPE = "CapabilityService";
2066
+ function hasCapabilityServiceInjection(sourceFile) {
2067
+ const classes = sourceFile.getClasses();
2068
+ for (const classDecl of classes) {
2069
+ const constructors = classDecl.getConstructors();
2070
+ for (const ctor of constructors) {
2071
+ const params = ctor.getParameters();
2072
+ for (const param of params) {
2073
+ if (param.getName() === PARAM_NAME) {
2074
+ return true;
2075
+ }
2076
+ }
2077
+ }
2078
+ }
2079
+ return false;
2080
+ }
2081
+ function findInjectableClass(sourceFile) {
2082
+ const classes = sourceFile.getClasses();
2083
+ for (const classDecl of classes) {
2084
+ const decorators = classDecl.getDecorators();
2085
+ const isInjectable = decorators.some((d) => d.getName() === "Injectable");
2086
+ const isController = decorators.some((d) => d.getName() === "Controller");
2087
+ if (isInjectable || isController) {
2088
+ return classDecl;
2089
+ }
2090
+ }
2091
+ return void 0;
2092
+ }
2093
+ function addInjection(sourceFile) {
2094
+ if (hasCapabilityServiceInjection(sourceFile)) {
2095
+ return { injectionAdded: false };
2096
+ }
2097
+ const classDecl = findInjectableClass(sourceFile);
2098
+ if (!classDecl) {
2099
+ return { injectionAdded: false };
2100
+ }
2101
+ const constructors = classDecl.getConstructors();
2102
+ if (constructors.length > 0) {
2103
+ const ctor = constructors[0];
2104
+ ctor.addParameter({
2105
+ name: PARAM_NAME,
2106
+ type: PARAM_TYPE,
2107
+ scope: Scope.Private,
2108
+ isReadonly: true,
2109
+ decorators: [{ name: "Inject", arguments: [] }]
2110
+ });
2111
+ } else {
2112
+ classDecl.addConstructor({
2113
+ parameters: [{
2114
+ name: PARAM_NAME,
2115
+ type: PARAM_TYPE,
2116
+ scope: Scope.Private,
2117
+ isReadonly: true,
2118
+ decorators: [{ name: "Inject", arguments: [] }]
2119
+ }]
2120
+ });
2121
+ }
2122
+ return { injectionAdded: true };
2123
+ }
2124
+
2125
+ // src/commands/migration/versions/v001_capability/code-migrator/transformers/call-site-transformer.ts
2126
+ import { SyntaxKind as SyntaxKind2 } from "ts-morph";
2127
+ var DEFAULT_ACTION_NAME = "run";
2128
+ function generateNewCallText(capabilityId, actionName, args) {
2129
+ const argsText = args.trim() || "{}";
2130
+ return `migrationAdaptor(this.capabilityService.load('${capabilityId}').call('${actionName}', ${argsText}))`;
2131
+ }
2132
+ function transformCallSites(sourceFile, imports) {
2133
+ const importMap = /* @__PURE__ */ new Map();
2134
+ for (const imp of imports) {
2135
+ importMap.set(imp.importName, {
2136
+ capabilityId: imp.capabilityId,
2137
+ actionName: imp.actionName ?? DEFAULT_ACTION_NAME
2138
+ });
2139
+ }
2140
+ let replacedCount = 0;
2141
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
2142
+ const sortedCalls = [...callExpressions].sort((a, b) => b.getStart() - a.getStart());
2143
+ for (const callExpr of sortedCalls) {
2144
+ const expression = callExpr.getExpression();
2145
+ let importInfo;
2146
+ if (expression.getKind() === SyntaxKind2.Identifier) {
2147
+ const functionName = expression.getText();
2148
+ importInfo = importMap.get(functionName);
2149
+ } else if (expression.getKind() === SyntaxKind2.PropertyAccessExpression) {
2150
+ const propAccess = expression.asKind(SyntaxKind2.PropertyAccessExpression);
2151
+ if (propAccess) {
2152
+ const objectExpr = propAccess.getExpression();
2153
+ if (objectExpr.getKind() === SyntaxKind2.Identifier) {
2154
+ const objectName = objectExpr.getText();
2155
+ importInfo = importMap.get(objectName);
2156
+ }
2157
+ }
2158
+ }
2159
+ if (importInfo) {
2160
+ const args = callExpr.getArguments();
2161
+ const argsText = args.map((arg) => arg.getText()).join(", ");
2162
+ const newCallText = generateNewCallText(importInfo.capabilityId, importInfo.actionName, argsText);
2163
+ callExpr.replaceWithText(newCallText);
2164
+ replacedCount++;
2165
+ }
2166
+ }
2167
+ return { replacedCount };
2168
+ }
2169
+
2170
+ // src/commands/migration/versions/v001_capability/code-migrator/index.ts
2171
+ function analyzeFile(project, filePath, actionNameMap) {
2172
+ const sourceFile = project.addSourceFileAtPath(filePath);
2173
+ const imports = analyzeImports(sourceFile);
2174
+ for (const imp of imports) {
2175
+ const actionName = actionNameMap.get(imp.capabilityId);
2176
+ if (actionName) {
2177
+ imp.actionName = actionName;
2178
+ }
2179
+ }
2180
+ const callSites = analyzeCallSites(sourceFile, imports);
2181
+ const classInfo = analyzeClass(sourceFile);
2182
+ const { canMigrate, reason } = canAutoMigrate(classInfo);
2183
+ const relativePath = path12.relative(getProjectRoot3(), filePath);
2184
+ return {
2185
+ filePath: relativePath,
2186
+ imports,
2187
+ callSites,
2188
+ classInfo,
2189
+ canAutoMigrate: canMigrate,
2190
+ manualMigrationReason: reason
2191
+ };
2192
+ }
2193
+ function migrateFile(project, analysis, dryRun) {
2194
+ const absolutePath = path12.join(getProjectRoot3(), analysis.filePath);
2195
+ if (!analysis.canAutoMigrate) {
2196
+ return {
2197
+ filePath: analysis.filePath,
2198
+ success: false,
2199
+ importsRemoved: 0,
2200
+ callSitesReplaced: 0,
2201
+ injectionAdded: false,
2202
+ error: analysis.manualMigrationReason
2203
+ };
2204
+ }
2205
+ try {
2206
+ const sourceFile = project.getSourceFileOrThrow(absolutePath);
2207
+ const callResult = transformCallSites(sourceFile, analysis.imports);
2208
+ const importResult = transformImports(sourceFile, analysis.imports);
2209
+ const injectionResult = addInjection(sourceFile);
2210
+ if (!dryRun) {
2211
+ sourceFile.saveSync();
2212
+ }
2213
+ return {
2214
+ filePath: analysis.filePath,
2215
+ success: true,
2216
+ importsRemoved: importResult.importsRemoved,
2217
+ callSitesReplaced: callResult.replacedCount,
2218
+ injectionAdded: injectionResult.injectionAdded
2219
+ };
2220
+ } catch (error) {
2221
+ return {
2222
+ filePath: analysis.filePath,
2223
+ success: false,
2224
+ importsRemoved: 0,
2225
+ callSitesReplaced: 0,
2226
+ injectionAdded: false,
2227
+ error: error instanceof Error ? error.message : String(error)
2228
+ };
2229
+ }
2230
+ }
2231
+ function buildActionNameMap(capabilities) {
2232
+ const map = /* @__PURE__ */ new Map();
2233
+ for (const cap of capabilities) {
2234
+ try {
2235
+ const actionName = getActionNameByPluginKey(cap.pluginKey);
2236
+ map.set(cap.id, actionName);
2237
+ } catch {
2238
+ map.set(cap.id, "run");
2239
+ }
2240
+ }
2241
+ return map;
2242
+ }
2243
+ async function migrateCode(options, capabilities) {
2244
+ const result = {
2245
+ autoMigrated: [],
2246
+ manualRequired: []
2247
+ };
2248
+ const actionNameMap = buildActionNameMap(capabilities);
2249
+ const filesToMigrate = scanFilesToMigrate();
2250
+ if (filesToMigrate.length === 0) {
2251
+ console.log(" No files need code migration.\n");
2252
+ return result;
2253
+ }
2254
+ const project = new Project({
2255
+ skipAddingFilesFromTsConfig: true,
2256
+ compilerOptions: {
2257
+ allowJs: true
2258
+ }
2259
+ });
2260
+ const analyses = [];
2261
+ for (const filePath of filesToMigrate) {
2262
+ try {
2263
+ const analysis = analyzeFile(project, filePath, actionNameMap);
2264
+ analyses.push(analysis);
2265
+ } catch (error) {
2266
+ console.error(` \u2717 Failed to analyze ${filePath}: ${error}`);
2267
+ }
2268
+ }
2269
+ const autoMigratable = analyses.filter((a) => a.canAutoMigrate);
2270
+ const manualRequired = analyses.filter((a) => !a.canAutoMigrate);
2271
+ for (const analysis of manualRequired) {
2272
+ const capabilityIds = [...new Set(analysis.imports.map((i) => i.capabilityId))];
2273
+ result.manualRequired.push({
2274
+ filePath: analysis.filePath,
2275
+ reason: analysis.manualMigrationReason || "Unknown reason",
2276
+ capabilities: capabilityIds,
2277
+ suggestion: getSuggestion(analysis)
2278
+ });
2279
+ }
2280
+ for (const analysis of autoMigratable) {
2281
+ const migrationResult = migrateFile(project, analysis, options.dryRun);
2282
+ result.autoMigrated.push(migrationResult);
2283
+ }
2284
+ return result;
2285
+ }
2286
+ function getSuggestion(analysis) {
2287
+ if (!analysis.classInfo) {
2288
+ return "Consider moving capability usage to an @Injectable service";
2289
+ }
2290
+ if (!analysis.classInfo.isInjectable && !analysis.classInfo.isController) {
2291
+ return `Add @Injectable() decorator to class "${analysis.classInfo.name}" or move capability usage to an injectable service`;
2292
+ }
2293
+ return "Manual review required";
2294
+ }
2295
+
2296
+ // src/commands/migration/versions/v001_capability/cleanup.ts
2297
+ import fs13 from "fs";
2298
+ import path13 from "path";
2299
+ function cleanupOldFiles(capabilities, dryRun) {
2300
+ const deletedFiles = [];
2301
+ const errors = [];
2302
+ const capabilitiesDir = getCapabilitiesDir2();
2303
+ const oldJsonPath = path13.join(capabilitiesDir, "capabilities.json");
2304
+ if (fs13.existsSync(oldJsonPath)) {
2305
+ try {
2306
+ if (!dryRun) {
2307
+ fs13.unlinkSync(oldJsonPath);
2308
+ }
2309
+ deletedFiles.push("capabilities.json");
2310
+ } catch (error) {
2311
+ errors.push(`Failed to delete capabilities.json: ${error instanceof Error ? error.message : String(error)}`);
2312
+ }
2313
+ }
2314
+ for (const cap of capabilities) {
2315
+ const tsFilePath = path13.join(capabilitiesDir, `${cap.id}.ts`);
2316
+ if (fs13.existsSync(tsFilePath)) {
2317
+ try {
2318
+ if (!dryRun) {
2319
+ fs13.unlinkSync(tsFilePath);
2320
+ }
2321
+ deletedFiles.push(`${cap.id}.ts`);
2322
+ } catch (error) {
2323
+ errors.push(`Failed to delete ${cap.id}.ts: ${error instanceof Error ? error.message : String(error)}`);
2324
+ }
2325
+ }
2326
+ }
2327
+ return {
2328
+ success: errors.length === 0,
2329
+ deletedFiles,
2330
+ errors
2331
+ };
2332
+ }
2333
+
2334
+ // src/commands/migration/versions/v001_capability/report-generator.ts
2335
+ import fs14 from "fs";
2336
+ import path14 from "path";
2337
+ var REPORT_FILE = "capability-migration-report.md";
2338
+ function printSummary(result) {
2339
+ const { jsonMigration, pluginInstallation, codeMigration, cleanup } = result;
2340
+ console.log("\u{1F4CA} Migration Summary");
2341
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2342
+ if (!jsonMigration.skipped) {
2343
+ console.log(` JSON files migrated: ${jsonMigration.count}`);
2344
+ }
2345
+ const totalPlugins = pluginInstallation.installed.length + pluginInstallation.alreadyInstalled.length;
2346
+ if (totalPlugins > 0) {
2347
+ console.log(` Plugins installed: ${pluginInstallation.installed.length}`);
2348
+ console.log(` Plugins already present: ${pluginInstallation.alreadyInstalled.length}`);
2349
+ if (pluginInstallation.failed.length > 0) {
2350
+ console.log(` Plugins failed: ${pluginInstallation.failed.length}`);
2351
+ }
2352
+ }
2353
+ const successfulMigrations = codeMigration.autoMigrated.filter((f) => f.success);
2354
+ const failedMigrations = codeMigration.autoMigrated.filter((f) => !f.success);
2355
+ const totalCallSites = successfulMigrations.reduce((sum, f) => sum + f.callSitesReplaced, 0);
2356
+ console.log(` Files auto-migrated: ${successfulMigrations.length}`);
2357
+ if (failedMigrations.length > 0) {
2358
+ console.log(` Files failed: ${failedMigrations.length}`);
2359
+ }
2360
+ if (codeMigration.manualRequired.length > 0) {
2361
+ console.log(` Files need manual work: ${codeMigration.manualRequired.length}`);
2362
+ }
2363
+ console.log(` Total call sites fixed: ${totalCallSites}`);
2364
+ if (cleanup.deletedFiles.length > 0) {
2365
+ console.log(` Old files cleaned up: ${cleanup.deletedFiles.length}`);
2366
+ }
2367
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
2368
+ const hasErrors = pluginInstallation.failed.length > 0 || failedMigrations.length > 0;
2369
+ const hasManual = codeMigration.manualRequired.length > 0;
2370
+ if (hasErrors) {
2371
+ console.log("\u26A0\uFE0F Migration completed with errors!\n");
2372
+ } else if (hasManual) {
2373
+ console.log("\u2705 Migration completed! (some files need manual work)\n");
2374
+ } else {
2375
+ console.log("\u2705 Migration completed!\n");
2376
+ }
2377
+ console.log("\u{1F4DD} Next steps:");
2378
+ console.log(" 1. Review changes: git diff");
2379
+ console.log(" 2. Run TypeScript check: npm run typecheck");
2380
+ console.log(" 3. Run tests: npm test");
2381
+ if (hasManual) {
2382
+ console.log(" 4. Handle manual migrations (see report)");
2383
+ }
2384
+ console.log("");
2385
+ }
2386
+ async function generateReport(result) {
2387
+ const { jsonMigration, pluginInstallation, codeMigration, cleanup } = result;
2388
+ const lines = [];
2389
+ lines.push("# Capability \u8FC1\u79FB\u62A5\u544A");
2390
+ lines.push("");
2391
+ lines.push(`\u751F\u6210\u65F6\u95F4: ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
2392
+ lines.push("");
2393
+ lines.push("## 1. \u914D\u7F6E\u8FC1\u79FB");
2394
+ lines.push("");
2395
+ if (jsonMigration.skipped) {
2396
+ lines.push(`\u8DF3\u8FC7: ${jsonMigration.reason}`);
2397
+ } else {
2398
+ lines.push("### 1.1 \u5DF2\u8FC1\u79FB\u7684 Capabilities");
2399
+ lines.push("");
2400
+ lines.push("| ID | pluginKey |");
2401
+ lines.push("|----|-----------|");
2402
+ for (const cap of jsonMigration.capabilities) {
2403
+ lines.push(`| ${cap.id} | ${cap.pluginKey} |`);
2404
+ }
2405
+ lines.push("");
2406
+ }
2407
+ lines.push("## 2. \u63D2\u4EF6\u5B89\u88C5");
2408
+ lines.push("");
2409
+ if (pluginInstallation.installed.length === 0 && pluginInstallation.alreadyInstalled.length === 0) {
2410
+ lines.push("\u65E0\u63D2\u4EF6\u9700\u8981\u5B89\u88C5\u3002");
2411
+ } else {
2412
+ lines.push("| pluginKey | \u72B6\u6001 |");
2413
+ lines.push("|-----------|------|");
2414
+ for (const pluginKey of pluginInstallation.alreadyInstalled) {
2415
+ lines.push(`| ${pluginKey} | \u5DF2\u5B89\u88C5 |`);
2416
+ }
2417
+ for (const pluginKey of pluginInstallation.installed) {
2418
+ lines.push(`| ${pluginKey} | \u65B0\u5B89\u88C5 |`);
2419
+ }
2420
+ for (const failed of pluginInstallation.failed) {
2421
+ lines.push(`| ${failed.pluginKey} | \u274C \u5931\u8D25: ${failed.error} |`);
2422
+ }
2423
+ }
2424
+ lines.push("");
2425
+ lines.push("## 3. \u4EE3\u7801\u8FC1\u79FB");
2426
+ lines.push("");
2427
+ const successfulMigrations = codeMigration.autoMigrated.filter((f) => f.success);
2428
+ const failedMigrations = codeMigration.autoMigrated.filter((f) => !f.success);
2429
+ if (successfulMigrations.length > 0) {
2430
+ lines.push(`### 3.1 \u81EA\u52A8\u8FC1\u79FB\u7684\u6587\u4EF6 (${successfulMigrations.length} \u4E2A)`);
2431
+ lines.push("");
2432
+ for (const file of successfulMigrations) {
2433
+ lines.push(`#### ${file.filePath}`);
2434
+ lines.push("");
2435
+ lines.push("**\u53D8\u66F4\uFF1A**");
2436
+ lines.push(`- \u5220\u9664 ${file.importsRemoved} \u4E2A capability import`);
2437
+ if (file.injectionAdded) {
2438
+ lines.push("- \u6DFB\u52A0 CapabilityService \u4F9D\u8D56\u6CE8\u5165");
2439
+ }
2440
+ lines.push(`- \u66FF\u6362 ${file.callSitesReplaced} \u4E2A\u8C03\u7528\u70B9`);
2441
+ lines.push("");
2442
+ }
2443
+ }
2444
+ if (failedMigrations.length > 0) {
2445
+ lines.push(`### 3.2 \u8FC1\u79FB\u5931\u8D25\u7684\u6587\u4EF6 (${failedMigrations.length} \u4E2A)`);
2446
+ lines.push("");
2447
+ for (const file of failedMigrations) {
2448
+ lines.push(`#### ${file.filePath}`);
2449
+ lines.push("");
2450
+ lines.push(`**\u9519\u8BEF\uFF1A** ${file.error}`);
2451
+ lines.push("");
2452
+ }
2453
+ }
2454
+ if (codeMigration.manualRequired.length > 0) {
2455
+ lines.push(`### 3.3 \u9700\u8981\u624B\u52A8\u8FC1\u79FB\u7684\u6587\u4EF6 (${codeMigration.manualRequired.length} \u4E2A)`);
2456
+ lines.push("");
2457
+ for (const item of codeMigration.manualRequired) {
2458
+ lines.push(`#### ${item.filePath}`);
2459
+ lines.push("");
2460
+ lines.push(`- **\u539F\u56E0\uFF1A** ${item.reason}`);
2461
+ lines.push(`- **\u6D89\u53CA\u7684 Capabilities\uFF1A** ${item.capabilities.join(", ")}`);
2462
+ lines.push(`- **\u5EFA\u8BAE\uFF1A** ${item.suggestion}`);
2463
+ lines.push("");
2464
+ }
2465
+ }
2466
+ lines.push("## 4. \u6E05\u7406\u8001\u6587\u4EF6");
2467
+ lines.push("");
2468
+ if (cleanup.deletedFiles.length === 0 && cleanup.errors.length === 0) {
2469
+ lines.push("\u65E0\u6587\u4EF6\u9700\u8981\u6E05\u7406\u3002");
2470
+ } else {
2471
+ if (cleanup.deletedFiles.length > 0) {
2472
+ lines.push("### 4.1 \u5DF2\u5220\u9664\u7684\u6587\u4EF6");
2473
+ lines.push("");
2474
+ for (const file of cleanup.deletedFiles) {
2475
+ lines.push(`- \`${file}\``);
2476
+ }
2477
+ lines.push("");
2478
+ }
2479
+ if (cleanup.errors.length > 0) {
2480
+ lines.push("### 4.2 \u6E05\u7406\u5931\u8D25");
2481
+ lines.push("");
2482
+ for (const err of cleanup.errors) {
2483
+ lines.push(`- \u274C ${err}`);
2484
+ }
2485
+ lines.push("");
2486
+ }
2487
+ }
2488
+ lines.push("");
2489
+ lines.push("## 5. \u9A8C\u8BC1\u6E05\u5355");
2490
+ lines.push("");
2491
+ lines.push("- [ ] \u8FD0\u884C `git diff` \u68C0\u67E5\u4EE3\u7801\u53D8\u66F4");
2492
+ lines.push("- [ ] \u8FD0\u884C `npm run typecheck` \u68C0\u67E5\u7C7B\u578B");
2493
+ lines.push("- [ ] \u8FD0\u884C `npm test` \u9A8C\u8BC1\u529F\u80FD");
2494
+ if (codeMigration.manualRequired.length > 0) {
2495
+ lines.push("- [ ] \u5904\u7406\u624B\u52A8\u8FC1\u79FB\u9879");
2496
+ }
2497
+ lines.push("");
2498
+ const reportPath = path14.join(getProjectRoot3(), REPORT_FILE);
2499
+ fs14.writeFileSync(reportPath, lines.join("\n"), "utf-8");
2500
+ console.log(`\u{1F4C4} Report generated: ${REPORT_FILE}`);
2501
+ }
2502
+
2503
+ // src/commands/migration/versions/v001_capability/migration-runner.ts
2504
+ async function runCapabilityMigration(options = {}) {
2505
+ const fullOptions = {
2506
+ dryRun: options.dryRun ?? false
2507
+ };
2508
+ console.log("\u{1F50D} Analyzing project...\n");
2509
+ if (fullOptions.dryRun) {
2510
+ console.log("\u{1F4CB} Running in dry-run mode (no files will be modified)\n");
2511
+ }
2512
+ let jsonResult = {
2513
+ success: false,
2514
+ skipped: true,
2515
+ reason: "Not executed",
2516
+ count: 0,
2517
+ capabilities: []
2518
+ };
2519
+ let pluginResult = {
2520
+ installed: [],
2521
+ alreadyInstalled: [],
2522
+ failed: []
2523
+ };
2524
+ let codeResult = {
2525
+ autoMigrated: [],
2526
+ manualRequired: []
2527
+ };
2528
+ let cleanupResult = {
2529
+ success: true,
2530
+ deletedFiles: [],
2531
+ errors: []
2532
+ };
2533
+ console.log("\u{1F4C1} Step 1: JSON File Migration");
2534
+ try {
2535
+ jsonResult = await migrateJsonFiles(fullOptions);
2536
+ if (jsonResult.skipped) {
2537
+ console.log(` \u25CB Skipped: ${jsonResult.reason}
2538
+ `);
2539
+ } else if (jsonResult.success) {
2540
+ console.log(` \u2713 Created ${jsonResult.count} capability files
2541
+ `);
2542
+ } else {
2543
+ console.log(` \u2717 Failed: ${jsonResult.reason}
2544
+ `);
2545
+ return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
2546
+ }
2547
+ } catch (error) {
2548
+ const errorMsg = error instanceof Error ? error.message : String(error);
2549
+ console.log(` \u2717 Error: ${errorMsg}
2550
+ `);
2551
+ jsonResult = {
2552
+ success: false,
2553
+ skipped: false,
2554
+ reason: errorMsg,
2555
+ count: 0,
2556
+ capabilities: []
2557
+ };
2558
+ return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
2559
+ }
2560
+ if (jsonResult.skipped) {
2561
+ return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
2562
+ }
2563
+ console.log("\u{1F4E6} Step 2: Plugin Installation");
2564
+ try {
2565
+ pluginResult = await installPlugins(jsonResult.capabilities, fullOptions);
2566
+ if (pluginResult.alreadyInstalled.length > 0) {
2567
+ console.log(` \u2713 Already installed: ${pluginResult.alreadyInstalled.join(", ")}`);
2568
+ }
2569
+ if (pluginResult.installed.length > 0) {
2570
+ console.log(` \u2713 Installed: ${pluginResult.installed.join(", ")}`);
2571
+ }
2572
+ if (pluginResult.failed.length > 0) {
2573
+ console.log(` \u26A0 Failed (will continue): ${pluginResult.failed.map((f) => f.pluginKey).join(", ")}`);
2574
+ for (const f of pluginResult.failed) {
2575
+ console.log(` - ${f.pluginKey}: ${f.error}`);
2576
+ }
2577
+ }
2578
+ console.log("");
2579
+ } catch (error) {
2580
+ const errorMsg = error instanceof Error ? error.message : String(error);
2581
+ console.log(` \u26A0 Error (will continue): ${errorMsg}
2582
+ `);
2583
+ pluginResult = {
2584
+ installed: [],
2585
+ alreadyInstalled: [],
2586
+ failed: [{ pluginKey: "unknown", error: errorMsg }]
2587
+ };
2588
+ }
2589
+ console.log("\u{1F527} Step 3: Code Migration");
2590
+ console.log(" Scanning server/ directory...\n");
2591
+ try {
2592
+ codeResult = await migrateCode(fullOptions, jsonResult.capabilities);
2593
+ let hasCodeError = false;
2594
+ for (const file of codeResult.autoMigrated) {
2595
+ if (file.success) {
2596
+ console.log(` \u2713 ${file.filePath}`);
2597
+ console.log(` - Removed ${file.importsRemoved} capability imports`);
2598
+ if (file.injectionAdded) {
2599
+ console.log(` - Added CapabilityService injection`);
2600
+ }
2601
+ console.log(` - Replaced ${file.callSitesReplaced} call sites`);
2602
+ } else {
2603
+ console.log(` \u2717 ${file.filePath}: ${file.error}`);
2604
+ hasCodeError = true;
2605
+ }
2606
+ }
2607
+ if (codeResult.manualRequired.length > 0) {
2608
+ console.log("");
2609
+ for (const item of codeResult.manualRequired) {
2610
+ console.log(` \u26A0 ${item.filePath} (manual migration required)`);
2611
+ console.log(` - Reason: ${item.reason}`);
2612
+ }
2613
+ }
2614
+ console.log("");
2615
+ if (hasCodeError) {
2616
+ console.log(" \u2717 Code migration failed, aborting...\n");
2617
+ return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
2618
+ }
2619
+ } catch (error) {
2620
+ const errorMsg = error instanceof Error ? error.message : String(error);
2621
+ console.log(` \u2717 Error: ${errorMsg}
2622
+ `);
2623
+ codeResult = {
2624
+ autoMigrated: [],
2625
+ manualRequired: []
2626
+ };
2627
+ return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
2628
+ }
2629
+ console.log("\u{1F9F9} Step 4: Cleanup Old Files");
2630
+ try {
2631
+ cleanupResult = cleanupOldFiles(jsonResult.capabilities, fullOptions.dryRun);
2632
+ if (cleanupResult.deletedFiles.length > 0) {
2633
+ for (const file of cleanupResult.deletedFiles) {
2634
+ console.log(` \u2713 Deleted: ${file}`);
2635
+ }
2636
+ } else {
2637
+ console.log(" \u25CB No files to clean up");
2638
+ }
2639
+ if (cleanupResult.errors.length > 0) {
2640
+ for (const err of cleanupResult.errors) {
2641
+ console.log(` \u26A0 ${err}`);
2642
+ }
2643
+ }
2644
+ console.log("");
2645
+ } catch (error) {
2646
+ const errorMsg = error instanceof Error ? error.message : String(error);
2647
+ console.log(` \u26A0 Error: ${errorMsg}
2648
+ `);
2649
+ cleanupResult = {
2650
+ success: false,
2651
+ deletedFiles: [],
2652
+ errors: [errorMsg]
2653
+ };
2654
+ }
2655
+ const result = buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
2656
+ printSummary(result);
2657
+ if (!fullOptions.dryRun) {
2658
+ await generateReport(result);
2659
+ }
2660
+ return result;
2661
+ }
2662
+ function buildResult(jsonMigration, pluginInstallation, codeMigration, cleanup) {
2663
+ return {
2664
+ jsonMigration,
2665
+ pluginInstallation,
2666
+ codeMigration,
2667
+ cleanup
2668
+ };
2669
+ }
2670
+
2671
+ // src/commands/migration/versions/v001_capability/run.ts
2672
+ async function run3(options) {
2673
+ try {
2674
+ const migrationOptions = {
2675
+ dryRun: options.dryRun ?? false
2676
+ };
2677
+ const result = await runCapabilityMigration(migrationOptions);
2678
+ const { jsonMigration, pluginInstallation, codeMigration, cleanup } = result;
2679
+ const jsonFailed = !jsonMigration.success && !jsonMigration.skipped;
2680
+ const codeFailed = codeMigration.autoMigrated.some((f) => !f.success);
2681
+ if (jsonFailed) {
2682
+ return {
2683
+ success: false,
2684
+ message: jsonMigration.reason || "JSON migration failed",
2685
+ error: new Error(jsonMigration.reason || "Unknown error")
2686
+ };
2687
+ }
2688
+ if (codeFailed) {
2689
+ return {
2690
+ success: false,
2691
+ message: "Code migration failed",
2692
+ error: new Error("Some files failed to migrate")
2693
+ };
2694
+ }
2695
+ const messages = [];
2696
+ if (jsonMigration.success) {
2697
+ messages.push(`${jsonMigration.count} capabilities migrated`);
2698
+ }
2699
+ if (pluginInstallation.installed.length > 0) {
2700
+ messages.push(`${pluginInstallation.installed.length} plugins installed`);
2701
+ }
2702
+ if (codeMigration.autoMigrated.length > 0) {
2703
+ const successCount = codeMigration.autoMigrated.filter((f) => f.success).length;
2704
+ if (successCount > 0) {
2705
+ messages.push(`${successCount} files auto-migrated`);
2706
+ }
2707
+ }
2708
+ if (cleanup.deletedFiles.length > 0) {
2709
+ messages.push(`${cleanup.deletedFiles.length} old files cleaned up`);
2710
+ }
2711
+ if (codeMigration.manualRequired.length > 0) {
2712
+ messages.push(`${codeMigration.manualRequired.length} files need manual migration`);
2713
+ }
2714
+ if (pluginInstallation.failed.length > 0) {
2715
+ messages.push(`${pluginInstallation.failed.length} plugins failed to install`);
2716
+ }
2717
+ return {
2718
+ success: true,
2719
+ message: messages.join(", ") || "No migration needed"
2720
+ };
2721
+ } catch (error) {
2722
+ const err = error instanceof Error ? error : new Error(String(error));
2723
+ return {
2724
+ success: false,
2725
+ message: err.message,
2726
+ error: err
2727
+ };
2728
+ }
2729
+ }
2730
+
2731
+ // src/commands/migration/versions/v001_capability/index.ts
2732
+ var v001CapabilityMigration = {
2733
+ version: 1,
2734
+ name: "capability",
2735
+ description: "Migrate capability configurations from old format (capabilities.json array) to new format (individual JSON files)",
2736
+ check,
2737
+ run: run3
2738
+ };
2739
+
2740
+ // src/commands/migration/versions/index.ts
2741
+ var versionedMigrations = [
2742
+ v001CapabilityMigration
2743
+ // 未来新增的迁移脚本在此添加,如:
2744
+ // v002ConfigMigration,
2745
+ // v003RoutesMigration,
2746
+ ];
2747
+ function getPendingMigrations(currentVersion) {
2748
+ return versionedMigrations.filter((m) => m.version > currentVersion).sort((a, b) => a.version - b.version);
2749
+ }
2750
+ function getLatestVersion() {
2751
+ if (versionedMigrations.length === 0) return 0;
2752
+ return Math.max(...versionedMigrations.map((m) => m.version));
2753
+ }
2754
+
2755
+ // src/commands/migration/runner.ts
2756
+ function log(message) {
2757
+ console.log(`[migration] ${message}`);
2758
+ }
2759
+ async function executeSingle(migration, options) {
2760
+ const { version, name } = migration;
2761
+ const versionTag = `v${version}`;
2762
+ try {
2763
+ log(`Checking [${versionTag}] ${name}...`);
2764
+ const checkResult = await migration.check(options);
2765
+ if (!checkResult.needsMigration) {
2766
+ log(`[${versionTag}] ${name}: no migration needed`);
2767
+ return {
2768
+ name,
2769
+ status: "skipped",
2770
+ message: checkResult.message || "No migration needed"
2771
+ };
2772
+ }
2773
+ log(`[${versionTag}] ${name}: needs migration (${checkResult.message})`);
2774
+ if (checkResult.items && checkResult.items.length > 0) {
2775
+ for (const item of checkResult.items) {
2776
+ console.log(` - ${item}`);
2777
+ }
2778
+ }
2779
+ if (options.dryRun) {
2780
+ return {
2781
+ name,
2782
+ status: "skipped",
2783
+ message: `Dry-run: ${checkResult.message}`
2784
+ };
2785
+ }
2786
+ log(`Running [${versionTag}] ${name} migration...`);
2787
+ const runResult = await migration.run(options);
2788
+ if (runResult.success) {
2789
+ log(`\u2713 [${versionTag}] ${name}: completed (${runResult.message})`);
2790
+ return {
2791
+ name,
2792
+ status: "completed",
2793
+ message: runResult.message
2794
+ };
2795
+ } else {
2796
+ log(`\u2717 [${versionTag}] ${name}: failed`);
2797
+ if (runResult.error) {
2798
+ console.log(` Error: ${runResult.error.message}`);
2799
+ }
2800
+ return {
2801
+ name,
2802
+ status: "failed",
2803
+ message: runResult.message,
2804
+ error: runResult.error
2805
+ };
2806
+ }
2807
+ } catch (error) {
2808
+ const err = error instanceof Error ? error : new Error(String(error));
2809
+ log(`\u2717 [${versionTag}] ${name}: failed`);
2810
+ console.log(` Error: ${err.message}`);
2811
+ return {
2812
+ name,
2813
+ status: "failed",
2814
+ message: err.message,
2815
+ error: err
2816
+ };
2817
+ }
2818
+ }
2819
+ function printSummary2(summary, dryRun) {
2820
+ console.log("");
2821
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2822
+ console.log(dryRun ? "Dry-run Summary:" : "Migration Summary:");
2823
+ if (dryRun) {
2824
+ const needsMigration = summary.skipped.filter((s) => s.message.startsWith("Dry-run:")).length;
2825
+ const noAction = summary.skipped.filter((s) => !s.message.startsWith("Dry-run:")).length;
2826
+ console.log(` Needs migration: ${needsMigration}`);
2827
+ console.log(` No action needed: ${noAction}`);
2828
+ } else {
2829
+ console.log(` \u2713 Completed: ${summary.completed.length}`);
2830
+ console.log(` \u25CB Skipped: ${summary.skipped.length} (no migration needed)`);
2831
+ console.log(` \u2717 Failed: ${summary.failed.length}`);
2832
+ }
2833
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2834
+ }
2835
+ async function runMigrations(options) {
2836
+ const currentVersion = getCurrentVersion();
2837
+ const latestVersion = getLatestVersion();
2838
+ console.log("");
2839
+ log(`Current version: ${currentVersion || "0 (not set)"}`);
2840
+ if (currentVersion >= latestVersion) {
2841
+ log("All migrations are up to date.");
2842
+ return {
2843
+ needsMigration: false,
2844
+ completed: [],
2845
+ skipped: [],
2846
+ failed: []
2847
+ };
2848
+ }
2849
+ const pendingMigrations = getPendingMigrations(currentVersion);
2850
+ if (options.dryRun) {
2851
+ log("Dry-run mode enabled, no changes will be made.");
2852
+ } else {
2853
+ log("Applying migrations...");
2854
+ }
2855
+ console.log("");
2856
+ const summary = {
2857
+ needsMigration: false,
2858
+ completed: [],
2859
+ skipped: [],
2860
+ failed: []
2861
+ };
2862
+ let hasAnyMigration = false;
2863
+ for (const migration of pendingMigrations) {
2864
+ const result = await executeSingle(migration, options);
2865
+ switch (result.status) {
2866
+ case "completed":
2867
+ summary.completed.push(result);
2868
+ hasAnyMigration = true;
2869
+ if (!options.dryRun) {
2870
+ setCurrentVersion(migration.version);
2871
+ }
2872
+ break;
2873
+ case "skipped":
2874
+ summary.skipped.push(result);
2875
+ if (result.message.startsWith("Dry-run:")) {
2876
+ hasAnyMigration = true;
2877
+ }
2878
+ break;
2879
+ case "failed":
2880
+ summary.failed.push(result);
2881
+ hasAnyMigration = true;
2882
+ log("Migration aborted due to failure.");
2883
+ printSummary2(summary, !!options.dryRun);
2884
+ return summary;
2885
+ }
2886
+ console.log("");
2887
+ }
2888
+ summary.needsMigration = hasAnyMigration;
2889
+ if (currentVersion === 0 && !hasAnyMigration && !options.dryRun) {
2890
+ log(`All migrations skipped. Project is up to date.`);
2891
+ log(`Updated migrationVersion to ${latestVersion}.`);
2892
+ setCurrentVersion(latestVersion);
2893
+ } else if (!options.dryRun && summary.completed.length > 0) {
2894
+ log(`Migration complete. Updated to version ${getCurrentVersion()}.`);
2895
+ }
2896
+ printSummary2(summary, !!options.dryRun);
2897
+ return summary;
2898
+ }
2899
+ function printMigrationResult(result) {
2900
+ console.log("");
2901
+ console.log(`MigrationResult=${JSON.stringify(result)}`);
2902
+ }
2903
+ async function checkMigrations(options) {
2904
+ const currentVersion = getCurrentVersion();
2905
+ const latestVersion = getLatestVersion();
2906
+ console.log("");
2907
+ log(`Current version: ${currentVersion || "0 (not set)"}`);
2908
+ if (currentVersion >= latestVersion) {
2909
+ log("All migrations are up to date.");
2910
+ printMigrationResult({
2911
+ needMigration: false,
2912
+ migrationVersion: currentVersion,
2913
+ targetVersion: latestVersion,
2914
+ pendingMigrations: []
2915
+ });
2916
+ return {
2917
+ needsMigration: false,
2918
+ completed: [],
2919
+ skipped: [],
2920
+ failed: []
2921
+ };
2922
+ }
2923
+ log("Checking pending migrations...");
2924
+ console.log("");
2925
+ const pendingMigrations = getPendingMigrations(currentVersion);
2926
+ const summary = {
2927
+ needsMigration: false,
2928
+ completed: [],
2929
+ skipped: [],
2930
+ failed: []
2931
+ };
2932
+ let needsCount = 0;
2933
+ let noActionCount = 0;
2934
+ const pendingMigrationNames = [];
2935
+ for (const migration of pendingMigrations) {
2936
+ const { version, name } = migration;
2937
+ const versionTag = `v${version}`;
2938
+ const versionName = `v${String(version).padStart(3, "0")}_${name}`;
44
2939
  try {
45
- await runSync(options);
2940
+ const checkResult = await migration.check(options);
2941
+ if (checkResult.needsMigration) {
2942
+ needsCount++;
2943
+ pendingMigrationNames.push(versionName);
2944
+ console.log(` ${versionTag} (${name}): needs migration`);
2945
+ if (checkResult.items && checkResult.items.length > 0) {
2946
+ for (const item of checkResult.items) {
2947
+ console.log(` - ${item}`);
2948
+ }
2949
+ }
2950
+ summary.skipped.push({
2951
+ name,
2952
+ status: "skipped",
2953
+ message: `Needs migration: ${checkResult.message}`
2954
+ });
2955
+ } else {
2956
+ noActionCount++;
2957
+ console.log(` ${versionTag} (${name}): no migration needed`);
2958
+ summary.skipped.push({
2959
+ name,
2960
+ status: "skipped",
2961
+ message: checkResult.message || "No migration needed"
2962
+ });
2963
+ }
2964
+ } catch (error) {
2965
+ const err = error instanceof Error ? error : new Error(String(error));
2966
+ console.log(` ${versionTag} (${name}): check failed`);
2967
+ console.log(` Error: ${err.message}`);
2968
+ summary.failed.push({
2969
+ name,
2970
+ status: "failed",
2971
+ message: err.message,
2972
+ error: err
2973
+ });
46
2974
  }
47
- catch (error) {
48
- console.error(`Failed to execute command:`, error.message);
49
- console.error(error.stack);
2975
+ }
2976
+ console.log("");
2977
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2978
+ console.log("Check Summary:");
2979
+ console.log(` Needs migration: ${needsCount}`);
2980
+ console.log(` No action needed: ${noActionCount}`);
2981
+ if (summary.failed.length > 0) {
2982
+ console.log(` Check failed: ${summary.failed.length}`);
2983
+ }
2984
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2985
+ if (needsCount > 0) {
2986
+ console.log("");
2987
+ log("Run 'fullstack-cli migration' to apply.");
2988
+ }
2989
+ summary.needsMigration = needsCount > 0;
2990
+ let finalVersion = currentVersion;
2991
+ if (needsCount === 0 && summary.failed.length === 0) {
2992
+ setCurrentVersion(latestVersion);
2993
+ finalVersion = latestVersion;
2994
+ console.log("");
2995
+ log(`Updated migrationVersion to ${latestVersion}.`);
2996
+ }
2997
+ printMigrationResult({
2998
+ needMigration: needsCount > 0,
2999
+ migrationVersion: finalVersion,
3000
+ targetVersion: latestVersion,
3001
+ pendingMigrations: pendingMigrationNames
3002
+ });
3003
+ return summary;
3004
+ }
3005
+
3006
+ // src/commands/migration/index.ts
3007
+ var migrationCommand = {
3008
+ name: "migration",
3009
+ description: "Run versioned migration scripts",
3010
+ register(program) {
3011
+ const migrationCmd = program.command(this.name).description("Run all pending migrations based on current migrationVersion").option("--dry-run", "Preview mode, only run check() without executing run()").action(async (options) => {
3012
+ const summary = await runMigrations(options);
3013
+ if (summary.failed.length > 0) {
50
3014
  process.exit(1);
3015
+ }
3016
+ });
3017
+ migrationCmd.command("check").description("Check pending migrations without applying them").action(async () => {
3018
+ const summary = await checkMigrations({});
3019
+ if (summary.failed.length > 0) {
3020
+ process.exit(1);
3021
+ }
3022
+ });
3023
+ }
3024
+ };
3025
+
3026
+ // src/commands/read-logs/index.ts
3027
+ import path15 from "path";
3028
+
3029
+ // src/commands/read-logs/std-utils.ts
3030
+ import fs15 from "fs";
3031
+ function formatStdPrefixTime(localTime) {
3032
+ const match = localTime.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
3033
+ if (!match) return localTime;
3034
+ const year = Number(match[1]);
3035
+ const month = Number(match[2]);
3036
+ const day = Number(match[3]);
3037
+ const hour = Number(match[4]);
3038
+ const minute = Number(match[5]);
3039
+ const second = Number(match[6]);
3040
+ const date = new Date(year, month - 1, day, hour, minute, second, 0);
3041
+ if (Number.isNaN(date.getTime())) return localTime;
3042
+ const pad2 = (n) => String(n).padStart(2, "0");
3043
+ const tzOffsetMinutes = -date.getTimezoneOffset();
3044
+ const sign = tzOffsetMinutes >= 0 ? "+" : "-";
3045
+ const abs = Math.abs(tzOffsetMinutes);
3046
+ const offsetHH = pad2(Math.floor(abs / 60));
3047
+ const offsetMM = pad2(abs % 60);
3048
+ return `${localTime.replace(" ", "T")}${sign}${offsetHH}:${offsetMM}`;
3049
+ }
3050
+ function stripPrefixFromStdLine(line) {
3051
+ const match = line.match(/^(\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(server|client)\] )(.*)$/);
3052
+ if (!match) {
3053
+ return line;
3054
+ }
3055
+ const time = formatStdPrefixTime(match[2] || "");
3056
+ const content = match[4] || "";
3057
+ return `[${time}] ${content}`;
3058
+ }
3059
+ function readStdLinesTailFromLastMarkerPaged(filePath, maxLines, offset, isMarker) {
3060
+ const stat = fs15.statSync(filePath);
3061
+ if (stat.size === 0) {
3062
+ return { lines: [], markerFound: false, totalLinesCount: 0 };
3063
+ }
3064
+ const fd = fs15.openSync(filePath, "r");
3065
+ const chunkSize = 64 * 1024;
3066
+ let position = stat.size;
3067
+ let remainder = "";
3068
+ let markerFound = false;
3069
+ let finished = false;
3070
+ let totalLinesCount = 0;
3071
+ let skipped = 0;
3072
+ const collected = [];
3073
+ try {
3074
+ while (position > 0 && !finished) {
3075
+ const length = Math.min(chunkSize, position);
3076
+ position -= length;
3077
+ const buffer = Buffer.alloc(length);
3078
+ fs15.readSync(fd, buffer, 0, length, position);
3079
+ let chunk = buffer.toString("utf8");
3080
+ if (remainder) {
3081
+ chunk += remainder;
3082
+ remainder = "";
3083
+ }
3084
+ const parts = chunk.split("\n");
3085
+ remainder = parts.shift() ?? "";
3086
+ for (let i = parts.length - 1; i >= 0; i -= 1) {
3087
+ const rawLine = parts[i];
3088
+ const line = stripPrefixFromStdLine(rawLine);
3089
+ if (totalLinesCount === 0 && line === "") {
3090
+ continue;
3091
+ }
3092
+ totalLinesCount += 1;
3093
+ if (skipped < offset) {
3094
+ skipped += 1;
3095
+ } else if (collected.length < maxLines) {
3096
+ collected.push(line);
3097
+ }
3098
+ if (isMarker(line)) {
3099
+ markerFound = true;
3100
+ finished = true;
3101
+ break;
3102
+ }
3103
+ }
3104
+ }
3105
+ if (!finished && remainder) {
3106
+ const line = stripPrefixFromStdLine(remainder);
3107
+ if (!(totalLinesCount === 0 && line === "")) {
3108
+ totalLinesCount += 1;
3109
+ if (skipped < offset) {
3110
+ skipped += 1;
3111
+ } else if (collected.length < maxLines) {
3112
+ collected.push(line);
3113
+ }
3114
+ }
3115
+ if (isMarker(line)) {
3116
+ markerFound = true;
3117
+ }
3118
+ }
3119
+ } finally {
3120
+ fs15.closeSync(fd);
3121
+ }
3122
+ return { lines: collected.reverse(), markerFound, totalLinesCount };
3123
+ }
3124
+
3125
+ // src/commands/read-logs/server-std.ts
3126
+ function readServerStdSegment(filePath, maxLines, offset) {
3127
+ const marker = (line) => {
3128
+ if (!line) return false;
3129
+ if (/\bdev:server\b/.test(line)) return true;
3130
+ if (line.includes("Starting compilation in watch mode")) return true;
3131
+ if (line.includes("File change detected. Starting incremental compilation")) return true;
3132
+ if (line.includes("Starting Nest application")) return true;
3133
+ if (line.includes("Nest application successfully started")) return true;
3134
+ return false;
3135
+ };
3136
+ const segment = readStdLinesTailFromLastMarkerPaged(filePath, maxLines, offset, marker);
3137
+ return { lines: segment.lines, totalLinesCount: segment.totalLinesCount };
3138
+ }
3139
+
3140
+ // src/commands/read-logs/tail.ts
3141
+ import fs16 from "fs";
3142
+ function fileExists(filePath) {
3143
+ try {
3144
+ fs16.accessSync(filePath, fs16.constants.F_OK | fs16.constants.R_OK);
3145
+ return true;
3146
+ } catch {
3147
+ return false;
3148
+ }
3149
+ }
3150
+ function readFileTailLines(filePath, maxLines) {
3151
+ const stat = fs16.statSync(filePath);
3152
+ if (stat.size === 0) {
3153
+ return [];
3154
+ }
3155
+ const fd = fs16.openSync(filePath, "r");
3156
+ const chunkSize = 64 * 1024;
3157
+ const chunks = [];
3158
+ let position = stat.size;
3159
+ let collectedLines = 0;
3160
+ try {
3161
+ while (position > 0 && collectedLines <= maxLines) {
3162
+ const length = Math.min(chunkSize, position);
3163
+ position -= length;
3164
+ const buffer = Buffer.alloc(length);
3165
+ fs16.readSync(fd, buffer, 0, length, position);
3166
+ chunks.unshift(buffer.toString("utf8"));
3167
+ const chunkLines = buffer.toString("utf8").split("\n").length - 1;
3168
+ collectedLines += chunkLines;
3169
+ }
3170
+ } finally {
3171
+ fs16.closeSync(fd);
3172
+ }
3173
+ const content = chunks.join("");
3174
+ const allLines = content.split("\n");
3175
+ if (allLines.length === 0) {
3176
+ return [];
3177
+ }
3178
+ if (allLines[allLines.length - 1] === "") {
3179
+ allLines.pop();
3180
+ }
3181
+ if (allLines.length <= maxLines) {
3182
+ return allLines;
3183
+ }
3184
+ return allLines.slice(allLines.length - maxLines);
3185
+ }
3186
+ function readFileTailNonEmptyLinesWithOffset(filePath, maxLines, offset) {
3187
+ const stat = fs16.statSync(filePath);
3188
+ if (stat.size === 0) {
3189
+ return { lines: [], totalLinesCount: 0 };
3190
+ }
3191
+ const fd = fs16.openSync(filePath, "r");
3192
+ const chunkSize = 64 * 1024;
3193
+ let position = stat.size;
3194
+ let remainder = "";
3195
+ let totalLinesCount = 0;
3196
+ let skipped = 0;
3197
+ const collected = [];
3198
+ try {
3199
+ while (position > 0) {
3200
+ const length = Math.min(chunkSize, position);
3201
+ position -= length;
3202
+ const buffer = Buffer.alloc(length);
3203
+ fs16.readSync(fd, buffer, 0, length, position);
3204
+ let chunk = buffer.toString("utf8");
3205
+ if (remainder) {
3206
+ chunk += remainder;
3207
+ remainder = "";
3208
+ }
3209
+ const parts = chunk.split("\n");
3210
+ remainder = parts.shift() ?? "";
3211
+ for (let i = parts.length - 1; i >= 0; i -= 1) {
3212
+ const line = parts[i].trim();
3213
+ if (!line) continue;
3214
+ totalLinesCount += 1;
3215
+ if (skipped < offset) {
3216
+ skipped += 1;
3217
+ } else if (collected.length < maxLines) {
3218
+ collected.push(line);
3219
+ }
3220
+ }
3221
+ }
3222
+ if (remainder) {
3223
+ const line = remainder.trim();
3224
+ if (line) {
3225
+ totalLinesCount += 1;
3226
+ if (skipped < offset) {
3227
+ skipped += 1;
3228
+ } else if (collected.length < maxLines) {
3229
+ collected.push(line);
3230
+ }
3231
+ }
3232
+ }
3233
+ } finally {
3234
+ fs16.closeSync(fd);
3235
+ }
3236
+ return { lines: collected.reverse(), totalLinesCount };
3237
+ }
3238
+
3239
+ // src/commands/read-logs/client-std.ts
3240
+ function readClientStdSegment(filePath, maxLines, offset) {
3241
+ const lines = readFileTailLines(filePath, Math.max((maxLines + offset) * 5, 2e3));
3242
+ return extractClientStdSegment(lines, maxLines, offset);
3243
+ }
3244
+ function extractClientStdSegment(lines, maxLines, offset) {
3245
+ const bodyLines = lines.map(stripPrefixFromStdLine);
3246
+ const hotStartMarkers = [
3247
+ /file change detected\..*incremental compilation/i,
3248
+ /starting incremental compilation/i,
3249
+ /starting compilation/i,
3250
+ /\bcompiling\b/i,
3251
+ /\brecompil/i
3252
+ ];
3253
+ const hotEndMarkers = [
3254
+ /file change detected\..*incremental compilation/i,
3255
+ /\bwebpack compiled\b/i,
3256
+ /compiled successfully/i,
3257
+ /compiled with warnings/i,
3258
+ /compiled with errors/i,
3259
+ /failed to compile/i,
3260
+ /fast refresh/i,
3261
+ /\bhmr\b/i,
3262
+ /hot update/i,
3263
+ /\bhot reload\b/i,
3264
+ /\bhmr update\b/i
3265
+ ];
3266
+ const compiledMarkers = [
3267
+ /\bwebpack compiled\b/i,
3268
+ /compiled successfully/i,
3269
+ /compiled with warnings/i,
3270
+ /compiled with errors/i,
3271
+ /failed to compile/i
3272
+ ];
3273
+ let startIndex = -1;
3274
+ for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
3275
+ const line = bodyLines[i];
3276
+ if (!line) continue;
3277
+ if (hotStartMarkers.some((re) => re.test(line))) {
3278
+ startIndex = i;
3279
+ break;
3280
+ }
3281
+ }
3282
+ if (startIndex === -1) {
3283
+ let pivotIndex = -1;
3284
+ for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
3285
+ const line = bodyLines[i];
3286
+ if (!line) continue;
3287
+ if (hotEndMarkers.some((re) => re.test(line))) {
3288
+ pivotIndex = i;
3289
+ break;
3290
+ }
3291
+ }
3292
+ if (pivotIndex !== -1) {
3293
+ if (compiledMarkers.some((re) => re.test(bodyLines[pivotIndex] ?? ""))) {
3294
+ startIndex = pivotIndex;
3295
+ } else {
3296
+ const searchLimit = 80;
3297
+ const from = Math.max(0, pivotIndex - searchLimit);
3298
+ for (let i = pivotIndex; i >= from; i -= 1) {
3299
+ const line = bodyLines[i];
3300
+ if (!line) continue;
3301
+ if (compiledMarkers.some((re) => re.test(line))) {
3302
+ startIndex = i;
3303
+ break;
3304
+ }
3305
+ }
3306
+ if (startIndex === -1) {
3307
+ startIndex = pivotIndex;
3308
+ }
3309
+ }
3310
+ }
3311
+ }
3312
+ if (startIndex === -1) {
3313
+ for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
3314
+ const line = bodyLines[i];
3315
+ if (!line) continue;
3316
+ if (/\bdev:client\b/.test(line)) {
3317
+ startIndex = i;
3318
+ break;
3319
+ }
3320
+ }
3321
+ }
3322
+ const segment = startIndex === -1 ? bodyLines : bodyLines.slice(startIndex);
3323
+ if (segment.length === 0) {
3324
+ return { lines: [], totalLinesCount: 0 };
3325
+ }
3326
+ const totalLinesCount = segment.length;
3327
+ const endExclusive = Math.max(0, segment.length - offset);
3328
+ const start = Math.max(0, endExclusive - maxLines);
3329
+ return {
3330
+ lines: segment.slice(start, endExclusive),
3331
+ totalLinesCount
3332
+ };
3333
+ }
3334
+
3335
+ // src/commands/read-logs/json-lines.ts
3336
+ import fs17 from "fs";
3337
+ function normalizePid(value) {
3338
+ if (typeof value === "number") {
3339
+ return String(value);
3340
+ }
3341
+ if (typeof value === "string" && value.length > 0) {
3342
+ return value;
3343
+ }
3344
+ return "unknown";
3345
+ }
3346
+ function normalizeLevelName(value) {
3347
+ if (typeof value === "string") {
3348
+ const trimmed = value.trim().toLowerCase();
3349
+ if (!trimmed) return null;
3350
+ if (trimmed === "warning") return "warn";
3351
+ if (trimmed === "err") return "error";
3352
+ const allowed = ["trace", "debug", "info", "warn", "error", "fatal"];
3353
+ return allowed.includes(trimmed) ? trimmed : null;
3354
+ }
3355
+ if (typeof value === "number" && Number.isFinite(value)) {
3356
+ const num = Math.floor(value);
3357
+ if (num <= 10) return "trace";
3358
+ if (num <= 20) return "debug";
3359
+ if (num <= 30) return "info";
3360
+ if (num <= 40) return "warn";
3361
+ if (num <= 50) return "error";
3362
+ return "fatal";
3363
+ }
3364
+ return null;
3365
+ }
3366
+ function extractLevelName(obj) {
3367
+ if (!obj || typeof obj !== "object") return null;
3368
+ const record = obj;
3369
+ const direct = normalizeLevelName(record["level"]);
3370
+ if (direct) return direct;
3371
+ const meta = record["meta"];
3372
+ if (meta && typeof meta === "object") {
3373
+ return normalizeLevelName(meta["level"]);
3374
+ }
3375
+ return null;
3376
+ }
3377
+ function buildWantedLevelSet(levels) {
3378
+ if (!levels || levels.length === 0) return null;
3379
+ const set = /* @__PURE__ */ new Set();
3380
+ for (const raw of levels) {
3381
+ const norm = normalizeLevelName(raw);
3382
+ if (norm) set.add(norm);
3383
+ }
3384
+ return set.size > 0 ? set : null;
3385
+ }
3386
+ function readJsonLinesLastPid(filePath, maxLines, offset, levels) {
3387
+ const stat = fs17.statSync(filePath);
3388
+ if (stat.size === 0) {
3389
+ return { lines: [], totalLinesCount: 0 };
3390
+ }
3391
+ const fd = fs17.openSync(filePath, "r");
3392
+ const chunkSize = 64 * 1024;
3393
+ let position = stat.size;
3394
+ let remainder = "";
3395
+ let targetPid = null;
3396
+ let finished = false;
3397
+ let totalLinesCount = 0;
3398
+ let skipped = 0;
3399
+ const collected = [];
3400
+ const wantedLevelSet = buildWantedLevelSet(levels);
3401
+ try {
3402
+ while (position > 0 && !finished) {
3403
+ const length = Math.min(chunkSize, position);
3404
+ position -= length;
3405
+ const buffer = Buffer.alloc(length);
3406
+ fs17.readSync(fd, buffer, 0, length, position);
3407
+ let chunk = buffer.toString("utf8");
3408
+ if (remainder) {
3409
+ chunk += remainder;
3410
+ remainder = "";
3411
+ }
3412
+ const parts = chunk.split("\n");
3413
+ remainder = parts.shift() ?? "";
3414
+ for (let i = parts.length - 1; i >= 0; i -= 1) {
3415
+ const line = parts[i].trim();
3416
+ if (!line) continue;
3417
+ let parsed = null;
3418
+ try {
3419
+ parsed = JSON.parse(line);
3420
+ } catch {
3421
+ continue;
3422
+ }
3423
+ const pid = normalizePid(parsed?.pid);
3424
+ if (targetPid === null) {
3425
+ targetPid = pid;
3426
+ }
3427
+ if (pid !== targetPid) {
3428
+ finished = true;
3429
+ break;
3430
+ }
3431
+ const levelName = extractLevelName(parsed);
3432
+ if (!wantedLevelSet || levelName && wantedLevelSet.has(levelName)) {
3433
+ totalLinesCount += 1;
3434
+ if (skipped < offset) {
3435
+ skipped += 1;
3436
+ } else if (collected.length < maxLines) {
3437
+ collected.push(line);
3438
+ }
3439
+ }
3440
+ }
3441
+ }
3442
+ if (!finished && remainder) {
3443
+ const line = remainder.trim();
3444
+ if (line) {
3445
+ try {
3446
+ const parsed = JSON.parse(line);
3447
+ const pid = normalizePid(parsed?.pid);
3448
+ if (targetPid === null) {
3449
+ targetPid = pid;
3450
+ }
3451
+ if (pid === targetPid) {
3452
+ const levelName = extractLevelName(parsed);
3453
+ if (!wantedLevelSet || levelName && wantedLevelSet.has(levelName)) {
3454
+ totalLinesCount += 1;
3455
+ if (skipped < offset) {
3456
+ skipped += 1;
3457
+ } else if (collected.length < maxLines) {
3458
+ collected.push(line);
3459
+ }
3460
+ }
3461
+ }
3462
+ } catch {
3463
+ return { lines: [], totalLinesCount: 0 };
3464
+ }
3465
+ }
3466
+ }
3467
+ } finally {
3468
+ fs17.closeSync(fd);
3469
+ }
3470
+ return { lines: collected.reverse(), totalLinesCount };
3471
+ }
3472
+ function normalizeTraceId(value) {
3473
+ if (typeof value === "string") {
3474
+ const trimmed = value.trim();
3475
+ return trimmed ? trimmed : null;
3476
+ }
3477
+ if (typeof value === "number" && Number.isFinite(value)) {
3478
+ return String(value);
3479
+ }
3480
+ return null;
3481
+ }
3482
+ function extractTraceId(obj) {
3483
+ if (!obj || typeof obj !== "object") return null;
3484
+ const record = obj;
3485
+ const directKeys = ["trace_id", "traceId", "traceID", "traceid"];
3486
+ for (const key of directKeys) {
3487
+ const value = normalizeTraceId(record[key]);
3488
+ if (value) return value;
3489
+ }
3490
+ const meta = record["meta"];
3491
+ if (meta && typeof meta === "object") {
3492
+ const metaRecord = meta;
3493
+ for (const key of directKeys) {
3494
+ const value = normalizeTraceId(metaRecord[key]);
3495
+ if (value) return value;
3496
+ }
3497
+ }
3498
+ const attributes = record["attributes"];
3499
+ if (attributes && typeof attributes === "object") {
3500
+ const attrRecord = attributes;
3501
+ for (const key of ["traceID", "trace_id", "traceId", "traceid"]) {
3502
+ const value = normalizeTraceId(attrRecord[key]);
3503
+ if (value) return value;
3504
+ }
3505
+ }
3506
+ return null;
3507
+ }
3508
+ function readJsonLinesByTraceId(filePath, traceId, maxLines, offset, levels) {
3509
+ const wanted = traceId.trim();
3510
+ if (!wanted) return { lines: [], totalLinesCount: 0 };
3511
+ const stat = fs17.statSync(filePath);
3512
+ if (stat.size === 0) {
3513
+ return { lines: [], totalLinesCount: 0 };
3514
+ }
3515
+ const fd = fs17.openSync(filePath, "r");
3516
+ const chunkSize = 64 * 1024;
3517
+ let position = stat.size;
3518
+ let remainder = "";
3519
+ let totalLinesCount = 0;
3520
+ let skipped = 0;
3521
+ const collected = [];
3522
+ const wantedLevelSet = buildWantedLevelSet(levels);
3523
+ try {
3524
+ while (position > 0) {
3525
+ const length = Math.min(chunkSize, position);
3526
+ position -= length;
3527
+ const buffer = Buffer.alloc(length);
3528
+ fs17.readSync(fd, buffer, 0, length, position);
3529
+ let chunk = buffer.toString("utf8");
3530
+ if (remainder) {
3531
+ chunk += remainder;
3532
+ remainder = "";
3533
+ }
3534
+ const parts = chunk.split("\n");
3535
+ remainder = parts.shift() ?? "";
3536
+ for (let i = parts.length - 1; i >= 0; i -= 1) {
3537
+ const line = parts[i].trim();
3538
+ if (!line) continue;
3539
+ try {
3540
+ const parsed = JSON.parse(line);
3541
+ const lineTraceId = extractTraceId(parsed);
3542
+ if (lineTraceId === wanted) {
3543
+ const levelName = extractLevelName(parsed);
3544
+ if (!wantedLevelSet || levelName && wantedLevelSet.has(levelName)) {
3545
+ totalLinesCount += 1;
3546
+ if (skipped < offset) {
3547
+ skipped += 1;
3548
+ } else if (collected.length < maxLines) {
3549
+ collected.push(line);
3550
+ }
3551
+ }
3552
+ }
3553
+ } catch {
3554
+ continue;
3555
+ }
3556
+ }
3557
+ }
3558
+ if (remainder) {
3559
+ const line = remainder.trim();
3560
+ if (line) {
3561
+ try {
3562
+ const parsed = JSON.parse(line);
3563
+ const lineTraceId = extractTraceId(parsed);
3564
+ if (lineTraceId === wanted) {
3565
+ const levelName = extractLevelName(parsed);
3566
+ if (!wantedLevelSet || levelName && wantedLevelSet.has(levelName)) {
3567
+ totalLinesCount += 1;
3568
+ if (skipped < offset) {
3569
+ skipped += 1;
3570
+ } else if (collected.length < maxLines) {
3571
+ collected.push(line);
3572
+ }
3573
+ }
3574
+ }
3575
+ } catch {
3576
+ return { lines: [], totalLinesCount: 0 };
3577
+ }
3578
+ }
3579
+ }
3580
+ } finally {
3581
+ fs17.closeSync(fd);
3582
+ }
3583
+ return { lines: collected.reverse(), totalLinesCount };
3584
+ }
3585
+ function readJsonLinesTailByLevel(filePath, maxLines, offset, levels) {
3586
+ const wantedLevelSet = buildWantedLevelSet(levels);
3587
+ if (!wantedLevelSet) {
3588
+ return { lines: [], totalLinesCount: 0 };
3589
+ }
3590
+ const stat = fs17.statSync(filePath);
3591
+ if (stat.size === 0) {
3592
+ return { lines: [], totalLinesCount: 0 };
3593
+ }
3594
+ const fd = fs17.openSync(filePath, "r");
3595
+ const chunkSize = 64 * 1024;
3596
+ let position = stat.size;
3597
+ let remainder = "";
3598
+ let totalLinesCount = 0;
3599
+ let skipped = 0;
3600
+ const collected = [];
3601
+ try {
3602
+ while (position > 0) {
3603
+ const length = Math.min(chunkSize, position);
3604
+ position -= length;
3605
+ const buffer = Buffer.alloc(length);
3606
+ fs17.readSync(fd, buffer, 0, length, position);
3607
+ let chunk = buffer.toString("utf8");
3608
+ if (remainder) {
3609
+ chunk += remainder;
3610
+ remainder = "";
3611
+ }
3612
+ const parts = chunk.split("\n");
3613
+ remainder = parts.shift() ?? "";
3614
+ for (let i = parts.length - 1; i >= 0; i -= 1) {
3615
+ const line = parts[i].trim();
3616
+ if (!line) continue;
3617
+ try {
3618
+ const parsed = JSON.parse(line);
3619
+ const levelName = extractLevelName(parsed);
3620
+ if (levelName && wantedLevelSet.has(levelName)) {
3621
+ totalLinesCount += 1;
3622
+ if (skipped < offset) {
3623
+ skipped += 1;
3624
+ } else if (collected.length < maxLines) {
3625
+ collected.push(line);
3626
+ }
3627
+ }
3628
+ } catch {
3629
+ continue;
3630
+ }
3631
+ }
3632
+ }
3633
+ if (remainder) {
3634
+ const line = remainder.trim();
3635
+ if (line) {
3636
+ try {
3637
+ const parsed = JSON.parse(line);
3638
+ const levelName = extractLevelName(parsed);
3639
+ if (levelName && wantedLevelSet.has(levelName)) {
3640
+ totalLinesCount += 1;
3641
+ if (skipped < offset) {
3642
+ skipped += 1;
3643
+ } else if (collected.length < maxLines) {
3644
+ collected.push(line);
3645
+ }
3646
+ }
3647
+ } catch {
3648
+ return { lines: [], totalLinesCount: 0 };
3649
+ }
3650
+ }
3651
+ }
3652
+ } finally {
3653
+ fs17.closeSync(fd);
3654
+ }
3655
+ return { lines: collected.reverse(), totalLinesCount };
3656
+ }
3657
+
3658
+ // src/commands/read-logs/index.ts
3659
+ var LOG_TYPES = ["server", "trace", "server-std", "client-std", "browser"];
3660
+ function sanitizeStructuredLog(value) {
3661
+ if (!value || typeof value !== "object") return value;
3662
+ if (Array.isArray(value)) return value;
3663
+ const obj = value;
3664
+ const sanitized = { ...obj };
3665
+ delete sanitized.tenant_id;
3666
+ delete sanitized.app_id;
3667
+ delete sanitized.pid;
3668
+ return sanitized;
3669
+ }
3670
+ function hasErrorInStdLines(lines) {
3671
+ const combined = lines.join("\n");
3672
+ if (!combined) return false;
3673
+ const strong = [
3674
+ /compiled with errors/i,
3675
+ /failed to compile/i,
3676
+ /error: \w+/i,
3677
+ /uncaught/i,
3678
+ /unhandled/i,
3679
+ /eaddrinuse/i,
3680
+ /cannot find module/i,
3681
+ /module not found/i,
3682
+ /ts\d{3,5}:/i
3683
+ ];
3684
+ if (strong.some((re) => re.test(combined))) return true;
3685
+ const weakLine = /\b(error|fatal|exception)\b/i;
3686
+ const ignorePatterns = [
3687
+ /\b0\s+errors?\b/i,
3688
+ /Server Error \d{3}/i
3689
+ ];
3690
+ return lines.some((line) => {
3691
+ const text = line.trim();
3692
+ if (!text) return false;
3693
+ if (ignorePatterns.some((re) => re.test(text))) return false;
3694
+ return weakLine.test(text);
3695
+ });
3696
+ }
3697
+ function hasErrorInLogObject(value) {
3698
+ if (!value || typeof value !== "object") return false;
3699
+ const obj = value;
3700
+ const rawLevel = obj.level;
3701
+ const level = typeof rawLevel === "string" ? rawLevel.toLowerCase() : "";
3702
+ if (level === "error" || level === "fatal") return true;
3703
+ if (level === "err") return true;
3704
+ if (typeof rawLevel === "number" && Number.isFinite(rawLevel) && rawLevel >= 50) return true;
3705
+ if (level === "warn" && typeof obj.stack === "string" && obj.stack.length > 0) return true;
3706
+ if (typeof obj.stack === "string" && obj.stack.length > 0) return true;
3707
+ const statusCode = obj.statusCode;
3708
+ const status_code = obj.status_code;
3709
+ const meta = obj.meta;
3710
+ const metaObj = meta && typeof meta === "object" ? meta : null;
3711
+ const metaStatusCode = metaObj?.statusCode;
3712
+ const metaStatus_code = metaObj?.status_code;
3713
+ const candidates = [statusCode, status_code, metaStatusCode, metaStatus_code];
3714
+ for (const candidate of candidates) {
3715
+ if (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 400) {
3716
+ return true;
3717
+ }
3718
+ }
3719
+ const message = typeof obj.message === "string" ? obj.message : "";
3720
+ if (message && hasErrorInStdLines([message])) return true;
3721
+ return false;
3722
+ }
3723
+ async function readLatestLogLinesMeta(options) {
3724
+ const maxLines = options.maxLines ?? 30;
3725
+ const offset = options.offset ?? 0;
3726
+ const filePath = resolveLogFilePath(options.logDir, options.type);
3727
+ const levels = Array.isArray(options.levels) ? options.levels : void 0;
3728
+ if (!fileExists(filePath)) {
3729
+ throw new Error(`Log file not found: ${filePath}`);
3730
+ }
3731
+ if (options.type === "server-std") {
3732
+ return readServerStdSegment(filePath, maxLines, offset);
3733
+ }
3734
+ if (options.type === "client-std") {
3735
+ return readClientStdSegment(filePath, maxLines, offset);
3736
+ }
3737
+ const traceId = typeof options.traceId === "string" ? options.traceId.trim() : "";
3738
+ if (traceId) {
3739
+ return readJsonLinesByTraceId(filePath, traceId, maxLines, offset, levels);
3740
+ }
3741
+ if (options.type === "server" || options.type === "trace") {
3742
+ return readJsonLinesLastPid(filePath, maxLines, offset, levels);
3743
+ }
3744
+ if (options.type === "browser") {
3745
+ if (levels && levels.length > 0) {
3746
+ return readJsonLinesTailByLevel(filePath, maxLines, offset, levels);
3747
+ }
3748
+ return readFileTailNonEmptyLinesWithOffset(filePath, maxLines, offset);
3749
+ }
3750
+ return { lines: [], totalLinesCount: 0 };
3751
+ }
3752
+ async function readLogsJsonResult(options) {
3753
+ const { lines, totalLinesCount } = await readLatestLogLinesMeta(options);
3754
+ if (options.type === "server-std" || options.type === "client-std") {
3755
+ return {
3756
+ hasError: hasErrorInStdLines(lines),
3757
+ totalLinesCount,
3758
+ logs: lines
3759
+ };
3760
+ }
3761
+ const logs = [];
3762
+ let hasError = false;
3763
+ for (const line of lines) {
3764
+ if (!hasError && hasErrorInStdLines([line])) {
3765
+ hasError = true;
3766
+ }
3767
+ try {
3768
+ const parsed = JSON.parse(line);
3769
+ logs.push(sanitizeStructuredLog(parsed));
3770
+ if (!hasError && hasErrorInLogObject(parsed)) {
3771
+ hasError = true;
3772
+ }
3773
+ } catch {
3774
+ continue;
51
3775
  }
52
- });
53
- // 解析命令行参数
54
- cli.parse();
3776
+ }
3777
+ return {
3778
+ hasError,
3779
+ totalLinesCount,
3780
+ logs
3781
+ };
3782
+ }
3783
+ function resolveLogFilePath(logDir, type) {
3784
+ const base = path15.isAbsolute(logDir) ? logDir : path15.join(process.cwd(), logDir);
3785
+ if (type === "server") {
3786
+ return path15.join(base, "server.log");
3787
+ }
3788
+ if (type === "trace") {
3789
+ return path15.join(base, "trace.log");
3790
+ }
3791
+ if (type === "server-std") {
3792
+ return path15.join(base, "server.std.log");
3793
+ }
3794
+ if (type === "client-std") {
3795
+ return path15.join(base, "client.std.log");
3796
+ }
3797
+ if (type === "browser") {
3798
+ return path15.join(base, "browser.log");
3799
+ }
3800
+ throw new Error(`Unsupported log type: ${type}`);
3801
+ }
3802
+ async function run4(options) {
3803
+ try {
3804
+ const result = await readLogsJsonResult(options);
3805
+ process.stdout.write(JSON.stringify(result) + "\n");
3806
+ } catch (error) {
3807
+ const message = error instanceof Error ? error.message : String(error);
3808
+ const result = { hasError: true, totalLinesCount: 0, logs: [message] };
3809
+ process.stdout.write(JSON.stringify(result) + "\n");
3810
+ process.exitCode = 1;
3811
+ }
3812
+ }
3813
+ function parseLogType(input) {
3814
+ const value = typeof input === "string" ? input.trim() : "";
3815
+ if (LOG_TYPES.includes(value)) {
3816
+ return value;
3817
+ }
3818
+ throw new Error(`Invalid --type: ${String(input)}. Expected one of: ${LOG_TYPES.join(", ")}`);
3819
+ }
3820
+ function parsePositiveInt(input, flagName) {
3821
+ const value = typeof input === "number" ? input : Number.parseInt(String(input), 10);
3822
+ if (!Number.isFinite(value) || value <= 0) {
3823
+ throw new Error(`Invalid ${flagName}: ${String(input)}. Expected a positive integer.`);
3824
+ }
3825
+ return value;
3826
+ }
3827
+ function parseNonNegativeInt(input, flagName) {
3828
+ const value = typeof input === "number" ? input : Number.parseInt(String(input), 10);
3829
+ if (!Number.isFinite(value) || value < 0) {
3830
+ throw new Error(`Invalid ${flagName}: ${String(input)}. Expected a non-negative integer.`);
3831
+ }
3832
+ return value;
3833
+ }
3834
+ function parseCommaSeparatedList(input) {
3835
+ const value = typeof input === "string" ? input : "";
3836
+ const items = value.split(",").map((s) => s.trim()).filter(Boolean);
3837
+ return items.length > 0 ? items : void 0;
3838
+ }
3839
+ var readLogsCommand = {
3840
+ name: "read-logs",
3841
+ description: "Read latest logs from log files",
3842
+ register(program) {
3843
+ program.command(this.name).description(this.description).option("--dir <path>", "Logs directory", "logs").option("--type <type>", `Log type: ${LOG_TYPES.join("|")}`, "server-std").option("--max-lines <lines>", "Max lines to return", "30").option("--offset <lines>", "Skip latest N lines for pagination", "0").option("--trace-id <id>", "Filter structured logs by trace id").option("--level <levels>", "Filter structured logs by level (comma-separated)").action(async (rawOptions) => {
3844
+ try {
3845
+ const logDir = typeof rawOptions.dir === "string" ? rawOptions.dir : "logs";
3846
+ const type = parseLogType(rawOptions.type);
3847
+ const maxLines = parsePositiveInt(rawOptions.maxLines, "--max-lines");
3848
+ const offset = parseNonNegativeInt(rawOptions.offset, "--offset");
3849
+ const traceId = typeof rawOptions.traceId === "string" ? rawOptions.traceId : void 0;
3850
+ const levels = parseCommaSeparatedList(rawOptions.level);
3851
+ await run4({ logDir, type, maxLines, offset, traceId, levels });
3852
+ } catch (error) {
3853
+ const message = error instanceof Error ? error.message : String(error);
3854
+ const result = { hasError: true, totalLinesCount: 0, logs: [message] };
3855
+ process.stdout.write(JSON.stringify(result) + "\n");
3856
+ process.exitCode = 1;
3857
+ }
3858
+ });
3859
+ }
3860
+ };
3861
+
3862
+ // src/commands/index.ts
3863
+ var commands = [
3864
+ genDbSchemaCommand,
3865
+ syncCommand,
3866
+ actionPluginCommandGroup,
3867
+ capabilityCommandGroup,
3868
+ migrationCommand,
3869
+ readLogsCommand
3870
+ ];
3871
+
3872
+ // src/index.ts
3873
+ var envPath = path16.join(process.cwd(), ".env");
3874
+ if (fs18.existsSync(envPath)) {
3875
+ dotenvConfig({ path: envPath });
3876
+ }
3877
+ var __dirname = path16.dirname(fileURLToPath3(import.meta.url));
3878
+ var pkg = JSON.parse(fs18.readFileSync(path16.join(__dirname, "../package.json"), "utf-8"));
3879
+ var cli = new FullstackCLI(pkg.version);
3880
+ cli.useAll(commands);
3881
+ cli.run();