@lark-apaas/fullstack-cli 1.1.6-alpha.8 → 1.1.6-alpha.9
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/README.md +158 -2
- package/dist/config/drizzle.config.d.ts +3 -1
- package/dist/config/drizzle.config.js +20 -16
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2907 -48
- package/package.json +13 -4
- package/templates/nest-cli.json +25 -0
- package/templates/scripts/build.sh +16 -9
- package/templates/scripts/dev.sh +9 -0
- package/templates/scripts/prune-smart.js +41 -7
- package/dist/commands/gen-db-schema.d.ts +0 -20
- package/dist/commands/gen-db-schema.js +0 -115
- package/dist/commands/sync.d.ts +0 -6
- package/dist/commands/sync.js +0 -204
- package/dist/config/sync.d.ts +0 -43
- package/dist/config/sync.js +0 -48
package/dist/index.js
CHANGED
|
@@ -1,54 +1,2913 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import fs15 from "fs";
|
|
3
|
+
import path14 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 cliRoot = path.resolve(__dirname2, "../");
|
|
145
|
+
const configPath = path.join(cliRoot, "config", "drizzle.config.js");
|
|
146
|
+
if (!fs.existsSync(configPath)) {
|
|
147
|
+
console.error("[gen-db-schema] Error: drizzle config not found in CLI package");
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const env = {
|
|
152
|
+
...process.env,
|
|
153
|
+
__DRIZZLE_OUT_DIR__: OUT_DIR,
|
|
154
|
+
__DRIZZLE_SCHEMA_PATH__: SCHEMA_FILE
|
|
155
|
+
};
|
|
156
|
+
const args = process.argv.slice(3).filter((arg) => !arg.startsWith("--enable-nest-module-generate"));
|
|
157
|
+
const spawnArgs = ["--yes", "drizzle-kit", "introspect", "--config", configPath, ...args];
|
|
158
|
+
const result = spawnSync("npx", spawnArgs, { stdio: "inherit", env });
|
|
159
|
+
if (result.error) {
|
|
160
|
+
console.error("[gen-db-schema] Execution failed:", result.error);
|
|
161
|
+
throw result.error;
|
|
162
|
+
}
|
|
163
|
+
if ((result.status ?? 0) !== 0) {
|
|
164
|
+
throw new Error(`drizzle-kit introspect failed with status ${result.status}`);
|
|
165
|
+
}
|
|
166
|
+
const generatedSchema = path.join(OUT_DIR, "schema.ts");
|
|
167
|
+
if (!fs.existsSync(generatedSchema)) {
|
|
168
|
+
console.error("[gen-db-schema] schema.ts not generated");
|
|
169
|
+
throw new Error("drizzle-kit introspect failed to generate schema.ts");
|
|
170
|
+
}
|
|
171
|
+
fs.mkdirSync(path.dirname(SCHEMA_FILE), { recursive: true });
|
|
172
|
+
fs.copyFileSync(generatedSchema, SCHEMA_FILE);
|
|
173
|
+
console.log(`[gen-db-schema] \u2713 Copied to ${outputPath}`);
|
|
174
|
+
const { postprocessDrizzleSchema } = require2("@lark-apaas/devtool-kits");
|
|
175
|
+
const stats = postprocessDrizzleSchema(SCHEMA_FILE);
|
|
176
|
+
if (stats?.unmatchedUnknown?.length) {
|
|
177
|
+
console.warn("[gen-db-schema] Unmatched custom types detected:", stats.unmatchedUnknown);
|
|
178
|
+
}
|
|
179
|
+
console.log("[gen-db-schema] \u2713 Postprocessed schema");
|
|
29
180
|
try {
|
|
30
|
-
|
|
181
|
+
if (options.enableNestModuleGenerate) {
|
|
182
|
+
const { parseAndGenerateNestResourceTemplate } = require2("@lark-apaas/devtool-kits");
|
|
183
|
+
const tsConfigFilePath = path.resolve(process.cwd(), "tsconfig.json");
|
|
184
|
+
const schemaFilePath = SCHEMA_FILE;
|
|
185
|
+
await parseAndGenerateNestResourceTemplate({
|
|
186
|
+
tsConfigFilePath,
|
|
187
|
+
schemaFilePath,
|
|
188
|
+
moduleOutputDir: path.resolve(process.cwd(), "server/modules")
|
|
189
|
+
});
|
|
190
|
+
console.log("[gen-db-schema] \u2713 Generate NestJS Module Boilerplate Successfully");
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.warn("[gen-db-schema] Generate NestJS Module Boilerplate failed:", error instanceof Error ? error.message : String(error));
|
|
31
194
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
195
|
+
console.log("[gen-db-schema] \u2713 Complete");
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error("[gen-db-schema] Failed:", err instanceof Error ? err.message : String(err));
|
|
198
|
+
exitCode = 1;
|
|
199
|
+
} finally {
|
|
200
|
+
if (fs.existsSync(OUT_DIR)) {
|
|
201
|
+
fs.rmSync(OUT_DIR, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
process.exit(exitCode);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/commands/db/index.ts
|
|
208
|
+
var genDbSchemaCommand = {
|
|
209
|
+
name: "gen-db-schema",
|
|
210
|
+
description: "Generate database schema from existing database",
|
|
211
|
+
register(program) {
|
|
212
|
+
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) => {
|
|
213
|
+
await run(options);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/commands/sync/run.handler.ts
|
|
219
|
+
import path2 from "path";
|
|
220
|
+
import fs2 from "fs";
|
|
221
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
222
|
+
|
|
223
|
+
// src/config/sync.ts
|
|
224
|
+
var syncConfig = {
|
|
225
|
+
// 派生规则
|
|
226
|
+
sync: [
|
|
227
|
+
// 1. 派生 scripts 目录(总是覆盖)
|
|
228
|
+
{
|
|
229
|
+
from: "templates/scripts",
|
|
230
|
+
to: "scripts",
|
|
231
|
+
type: "directory",
|
|
232
|
+
overwrite: true
|
|
233
|
+
},
|
|
234
|
+
// 2. 覆写 nest-cli.json 配置,禁止用户修改
|
|
235
|
+
{
|
|
236
|
+
from: "templates/nest-cli.json",
|
|
237
|
+
to: "nest-cli.json",
|
|
238
|
+
type: "file",
|
|
239
|
+
overwrite: true
|
|
240
|
+
},
|
|
241
|
+
// // 2. 追加内容到 .gitignore
|
|
242
|
+
// {
|
|
243
|
+
// from: 'templates/.gitignore.append',
|
|
244
|
+
// to: '.gitignore',
|
|
245
|
+
// type: 'append',
|
|
246
|
+
// },
|
|
247
|
+
// 3. 派生 server/type.ts 文件(总是覆盖)
|
|
248
|
+
// {
|
|
249
|
+
// from: 'templates/server/global.d.ts',
|
|
250
|
+
// to: 'server/types/global.d.ts',
|
|
251
|
+
// type: 'file',
|
|
252
|
+
// },
|
|
253
|
+
{
|
|
254
|
+
type: "delete-directory",
|
|
255
|
+
to: ".swc"
|
|
256
|
+
// 删除 .swc 目录(如果存在)
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
// 文件权限设置
|
|
260
|
+
permissions: {
|
|
261
|
+
// 所有 .sh 文件设置为可执行
|
|
262
|
+
// '**/*.sh': 0o755,
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
function genSyncConfig(perms = {}) {
|
|
266
|
+
if (!perms.disableGenOpenapi) {
|
|
267
|
+
syncConfig.sync.push({
|
|
268
|
+
from: "templates/helper/gen-openapi.ts",
|
|
269
|
+
to: "scripts/gen-openapi.ts",
|
|
270
|
+
type: "file",
|
|
271
|
+
overwrite: true
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return syncConfig;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/commands/sync/run.handler.ts
|
|
278
|
+
async function run2(options) {
|
|
279
|
+
const userProjectRoot = process.env.INIT_CWD || process.cwd();
|
|
280
|
+
const __filename = fileURLToPath2(import.meta.url);
|
|
281
|
+
const __dirname2 = path2.dirname(__filename);
|
|
282
|
+
const pluginRoot = path2.resolve(__dirname2, "..");
|
|
283
|
+
if (userProjectRoot === pluginRoot) {
|
|
284
|
+
console.log("[fullstack-cli] Skip syncing (installing plugin itself)");
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
const userPackageJson = path2.join(userProjectRoot, "package.json");
|
|
288
|
+
if (!fs2.existsSync(userPackageJson)) {
|
|
289
|
+
console.log("[fullstack-cli] Skip syncing (not a valid npm project)");
|
|
290
|
+
process.exit(0);
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
console.log("[fullstack-cli] Starting sync...");
|
|
294
|
+
const config = genSyncConfig({
|
|
295
|
+
disableGenOpenapi: options.disableGenOpenapi ?? false
|
|
296
|
+
});
|
|
297
|
+
if (!config || !config.sync) {
|
|
298
|
+
console.warn("[fullstack-cli] No sync configuration found");
|
|
299
|
+
process.exit(0);
|
|
300
|
+
}
|
|
301
|
+
for (const rule of config.sync) {
|
|
302
|
+
await syncRule(rule, pluginRoot, userProjectRoot);
|
|
303
|
+
}
|
|
304
|
+
if (config.permissions) {
|
|
305
|
+
setPermissions(config.permissions, userProjectRoot);
|
|
306
|
+
}
|
|
307
|
+
console.log("[fullstack-cli] Sync completed successfully \u2705");
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
310
|
+
console.error("[fullstack-cli] Failed to sync:", message);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function syncRule(rule, pluginRoot, userProjectRoot) {
|
|
315
|
+
if (rule.type === "delete-file" || rule.type === "delete-directory") {
|
|
316
|
+
const destPath2 = path2.join(userProjectRoot, rule.to);
|
|
317
|
+
if (rule.type === "delete-file") {
|
|
318
|
+
deleteFile(destPath2);
|
|
319
|
+
} else {
|
|
320
|
+
deleteDirectory(destPath2);
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!("from" in rule)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const srcPath = path2.join(pluginRoot, rule.from);
|
|
328
|
+
const destPath = path2.join(userProjectRoot, rule.to);
|
|
329
|
+
if (!fs2.existsSync(srcPath)) {
|
|
330
|
+
console.warn(`[fullstack-cli] Source not found: ${rule.from}`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
switch (rule.type) {
|
|
334
|
+
case "directory":
|
|
335
|
+
syncDirectory(srcPath, destPath, rule.overwrite ?? true);
|
|
336
|
+
break;
|
|
337
|
+
case "file":
|
|
338
|
+
syncFile(srcPath, destPath, rule.overwrite ?? true);
|
|
339
|
+
break;
|
|
340
|
+
case "append":
|
|
341
|
+
appendToFile(srcPath, destPath);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function syncFile(src, dest, overwrite = true) {
|
|
346
|
+
const destDir = path2.dirname(dest);
|
|
347
|
+
if (!fs2.existsSync(destDir)) {
|
|
348
|
+
fs2.mkdirSync(destDir, { recursive: true });
|
|
349
|
+
}
|
|
350
|
+
if (fs2.existsSync(dest) && !overwrite) {
|
|
351
|
+
console.log(`[fullstack-cli] \u25CB ${path2.basename(dest)} (skipped, already exists)`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
fs2.copyFileSync(src, dest);
|
|
355
|
+
console.log(`[fullstack-cli] \u2713 ${path2.basename(dest)}`);
|
|
356
|
+
}
|
|
357
|
+
function syncDirectory(src, dest, overwrite = true) {
|
|
358
|
+
if (!fs2.existsSync(dest)) {
|
|
359
|
+
fs2.mkdirSync(dest, { recursive: true });
|
|
360
|
+
}
|
|
361
|
+
const files = fs2.readdirSync(src);
|
|
362
|
+
let count = 0;
|
|
363
|
+
files.forEach((file) => {
|
|
364
|
+
const srcFile = path2.join(src, file);
|
|
365
|
+
const destFile = path2.join(dest, file);
|
|
366
|
+
const stats = fs2.statSync(srcFile);
|
|
367
|
+
if (stats.isDirectory()) {
|
|
368
|
+
syncDirectory(srcFile, destFile, overwrite);
|
|
369
|
+
} else {
|
|
370
|
+
if (overwrite || !fs2.existsSync(destFile)) {
|
|
371
|
+
fs2.copyFileSync(srcFile, destFile);
|
|
372
|
+
console.log(`[fullstack-cli] \u2713 ${path2.relative(dest, destFile)}`);
|
|
373
|
+
count++;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
if (count > 0) {
|
|
378
|
+
console.log(`[fullstack-cli] Synced ${count} files to ${path2.basename(dest)}/`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function appendToFile(src, dest) {
|
|
382
|
+
const content = fs2.readFileSync(src, "utf-8");
|
|
383
|
+
let existingContent = "";
|
|
384
|
+
if (fs2.existsSync(dest)) {
|
|
385
|
+
existingContent = fs2.readFileSync(dest, "utf-8");
|
|
386
|
+
}
|
|
387
|
+
if (existingContent.includes(content.trim())) {
|
|
388
|
+
console.log(`[fullstack-cli] \u25CB ${path2.basename(dest)} (already contains content)`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
fs2.appendFileSync(dest, content);
|
|
392
|
+
console.log(`[fullstack-cli] \u2713 ${path2.basename(dest)} (appended)`);
|
|
393
|
+
}
|
|
394
|
+
function setPermissions(permissions, projectRoot) {
|
|
395
|
+
for (const [pattern, mode] of Object.entries(permissions)) {
|
|
396
|
+
if (pattern === "**/*.sh") {
|
|
397
|
+
const scriptsDir = path2.join(projectRoot, "scripts");
|
|
398
|
+
if (fs2.existsSync(scriptsDir)) {
|
|
399
|
+
const files = fs2.readdirSync(scriptsDir);
|
|
400
|
+
files.forEach((file) => {
|
|
401
|
+
if (file.endsWith(".sh")) {
|
|
402
|
+
const filePath = path2.join(scriptsDir, file);
|
|
403
|
+
fs2.chmodSync(filePath, mode);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function deleteFile(filePath) {
|
|
411
|
+
if (fs2.existsSync(filePath)) {
|
|
412
|
+
fs2.unlinkSync(filePath);
|
|
413
|
+
console.log(`[fullstack-cli] \u2713 ${path2.basename(filePath)} (deleted)`);
|
|
414
|
+
} else {
|
|
415
|
+
console.log(`[fullstack-cli] \u25CB ${path2.basename(filePath)} (not found)`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function deleteDirectory(dirPath) {
|
|
419
|
+
if (fs2.existsSync(dirPath)) {
|
|
420
|
+
fs2.rmSync(dirPath, { recursive: true });
|
|
421
|
+
console.log(`[fullstack-cli] \u2713 ${path2.basename(dirPath)} (deleted)`);
|
|
422
|
+
} else {
|
|
423
|
+
console.log(`[fullstack-cli] \u25CB ${path2.basename(dirPath)} (not found)`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/commands/sync/index.ts
|
|
428
|
+
var syncCommand = {
|
|
429
|
+
name: "sync",
|
|
430
|
+
description: "Sync template files (scripts, configs) to user project",
|
|
431
|
+
register(program) {
|
|
432
|
+
program.command(this.name).description(this.description).option("--disable-gen-openapi", "Disable generating openapi.ts").action(async (options) => {
|
|
433
|
+
await run2(options);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// src/commands/action-plugin/utils.ts
|
|
439
|
+
import fs3 from "fs";
|
|
440
|
+
import path3 from "path";
|
|
441
|
+
import { spawnSync as spawnSync2, execSync } from "child_process";
|
|
442
|
+
function parsePluginName(input) {
|
|
443
|
+
const match = input.match(/^(@[^/]+\/[^@]+)(?:@(.+))?$/);
|
|
444
|
+
if (!match) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Invalid plugin name format: ${input}. Expected format: @scope/name or @scope/name@version`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
name: match[1],
|
|
451
|
+
version: match[2] || "latest"
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function getProjectRoot() {
|
|
455
|
+
return process.cwd();
|
|
456
|
+
}
|
|
457
|
+
function getPackageJsonPath() {
|
|
458
|
+
return path3.join(getProjectRoot(), "package.json");
|
|
459
|
+
}
|
|
460
|
+
function getPluginPath(pluginName) {
|
|
461
|
+
return path3.join(getProjectRoot(), "node_modules", pluginName);
|
|
462
|
+
}
|
|
463
|
+
function readPackageJson() {
|
|
464
|
+
const pkgPath = getPackageJsonPath();
|
|
465
|
+
if (!fs3.existsSync(pkgPath)) {
|
|
466
|
+
throw new Error("package.json not found in current directory");
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
const content = fs3.readFileSync(pkgPath, "utf-8");
|
|
470
|
+
return JSON.parse(content);
|
|
471
|
+
} catch {
|
|
472
|
+
throw new Error("Failed to parse package.json");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function writePackageJson(pkg2) {
|
|
476
|
+
const pkgPath = getPackageJsonPath();
|
|
477
|
+
fs3.writeFileSync(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
|
|
478
|
+
}
|
|
479
|
+
function readActionPlugins() {
|
|
480
|
+
const pkg2 = readPackageJson();
|
|
481
|
+
return pkg2.actionPlugins || {};
|
|
482
|
+
}
|
|
483
|
+
function writeActionPlugins(plugins) {
|
|
484
|
+
const pkg2 = readPackageJson();
|
|
485
|
+
pkg2.actionPlugins = plugins;
|
|
486
|
+
writePackageJson(pkg2);
|
|
487
|
+
}
|
|
488
|
+
function isPluginInstalled(pluginName) {
|
|
489
|
+
const plugins = readActionPlugins();
|
|
490
|
+
return pluginName in plugins;
|
|
491
|
+
}
|
|
492
|
+
function getInstalledPluginVersion(pluginName) {
|
|
493
|
+
const plugins = readActionPlugins();
|
|
494
|
+
return plugins[pluginName] || null;
|
|
495
|
+
}
|
|
496
|
+
function npmInstall(tgzPath) {
|
|
497
|
+
console.log(`[action-plugin] Running npm install ${tgzPath}...`);
|
|
498
|
+
const result = spawnSync2("npm", ["install", tgzPath, "--no-save", "--no-package-lock", "--ignore-scripts"], {
|
|
499
|
+
cwd: getProjectRoot(),
|
|
500
|
+
stdio: "inherit"
|
|
501
|
+
});
|
|
502
|
+
if (result.error) {
|
|
503
|
+
throw new Error(`npm install failed: ${result.error.message}`);
|
|
504
|
+
}
|
|
505
|
+
if (result.status !== 0) {
|
|
506
|
+
throw new Error(`npm install failed with exit code ${result.status}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function getPackageVersion(pluginName) {
|
|
510
|
+
const pkgJsonPath = path3.join(getPluginPath(pluginName), "package.json");
|
|
511
|
+
if (!fs3.existsSync(pkgJsonPath)) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const content = fs3.readFileSync(pkgJsonPath, "utf-8");
|
|
516
|
+
const pkg2 = JSON.parse(content);
|
|
517
|
+
return pkg2.version || null;
|
|
518
|
+
} catch {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function readPluginPackageJson(pluginPath) {
|
|
523
|
+
const pkgJsonPath = path3.join(pluginPath, "package.json");
|
|
524
|
+
if (!fs3.existsSync(pkgJsonPath)) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
const content = fs3.readFileSync(pkgJsonPath, "utf-8");
|
|
529
|
+
return JSON.parse(content);
|
|
530
|
+
} catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function extractTgzToNodeModules(tgzPath, pluginName) {
|
|
535
|
+
const nodeModulesPath = path3.join(getProjectRoot(), "node_modules");
|
|
536
|
+
const targetDir = path3.join(nodeModulesPath, pluginName);
|
|
537
|
+
const scopeDir = path3.dirname(targetDir);
|
|
538
|
+
if (!fs3.existsSync(scopeDir)) {
|
|
539
|
+
fs3.mkdirSync(scopeDir, { recursive: true });
|
|
540
|
+
}
|
|
541
|
+
if (fs3.existsSync(targetDir)) {
|
|
542
|
+
fs3.rmSync(targetDir, { recursive: true });
|
|
543
|
+
}
|
|
544
|
+
const tempDir = path3.join(nodeModulesPath, ".cache", "fullstack-cli", "extract-temp");
|
|
545
|
+
if (fs3.existsSync(tempDir)) {
|
|
546
|
+
fs3.rmSync(tempDir, { recursive: true });
|
|
547
|
+
}
|
|
548
|
+
fs3.mkdirSync(tempDir, { recursive: true });
|
|
549
|
+
try {
|
|
550
|
+
execSync(`tar -xzf "${tgzPath}" -C "${tempDir}"`, { stdio: "pipe" });
|
|
551
|
+
const extractedDir = path3.join(tempDir, "package");
|
|
552
|
+
if (fs3.existsSync(extractedDir)) {
|
|
553
|
+
fs3.renameSync(extractedDir, targetDir);
|
|
554
|
+
} else {
|
|
555
|
+
const files = fs3.readdirSync(tempDir);
|
|
556
|
+
if (files.length === 1) {
|
|
557
|
+
fs3.renameSync(path3.join(tempDir, files[0]), targetDir);
|
|
558
|
+
} else {
|
|
559
|
+
throw new Error("Unexpected tgz structure");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return targetDir;
|
|
563
|
+
} finally {
|
|
564
|
+
if (fs3.existsSync(tempDir)) {
|
|
565
|
+
fs3.rmSync(tempDir, { recursive: true });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function checkMissingPeerDeps(peerDeps) {
|
|
570
|
+
if (!peerDeps || Object.keys(peerDeps).length === 0) {
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
const missing = [];
|
|
574
|
+
const nodeModulesPath = path3.join(getProjectRoot(), "node_modules");
|
|
575
|
+
for (const [depName, _version] of Object.entries(peerDeps)) {
|
|
576
|
+
const depPath = path3.join(nodeModulesPath, depName);
|
|
577
|
+
if (!fs3.existsSync(depPath)) {
|
|
578
|
+
missing.push(depName);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return missing;
|
|
582
|
+
}
|
|
583
|
+
function installMissingDeps(deps) {
|
|
584
|
+
if (deps.length === 0) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
console.log(`[action-plugin] Installing missing dependencies: ${deps.join(", ")}`);
|
|
588
|
+
const result = spawnSync2("npm", ["install", ...deps, "--no-save", "--no-package-lock"], {
|
|
589
|
+
cwd: getProjectRoot(),
|
|
590
|
+
stdio: "inherit"
|
|
591
|
+
});
|
|
592
|
+
if (result.error) {
|
|
593
|
+
throw new Error(`npm install failed: ${result.error.message}`);
|
|
594
|
+
}
|
|
595
|
+
if (result.status !== 0) {
|
|
596
|
+
throw new Error(`npm install failed with exit code ${result.status}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function removePluginDirectory(pluginName) {
|
|
600
|
+
const pluginPath = getPluginPath(pluginName);
|
|
601
|
+
if (fs3.existsSync(pluginPath)) {
|
|
602
|
+
fs3.rmSync(pluginPath, { recursive: true });
|
|
603
|
+
console.log(`[action-plugin] Removed ${pluginName}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/commands/action-plugin/api-client.ts
|
|
608
|
+
import { HttpClient as HttpClient2 } from "@lark-apaas/http-client";
|
|
609
|
+
import fs4 from "fs";
|
|
610
|
+
import path4 from "path";
|
|
611
|
+
|
|
612
|
+
// src/utils/http-client.ts
|
|
613
|
+
import { HttpClient } from "@lark-apaas/http-client";
|
|
614
|
+
var clientInstance = null;
|
|
615
|
+
function getHttpClient() {
|
|
616
|
+
if (!clientInstance) {
|
|
617
|
+
clientInstance = new HttpClient({
|
|
618
|
+
timeout: 3e4,
|
|
619
|
+
platform: {
|
|
620
|
+
enabled: true
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
clientInstance.interceptors.request.use((req) => {
|
|
624
|
+
req.headers["x-tt-env"] = "boe_miaoda_plugin";
|
|
625
|
+
return req;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return clientInstance;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/commands/action-plugin/api-client.ts
|
|
632
|
+
var PLUGIN_CACHE_DIR = "node_modules/.cache/fullstack-cli/plugins";
|
|
633
|
+
async function getPluginVersions(keys, latestOnly = true) {
|
|
634
|
+
const client = getHttpClient();
|
|
635
|
+
const response = await client.post(`/api/v1/studio/innerapi/plugins/-/versions/batch_get?keys=${keys.join(",")}&latest_only=${latestOnly}`);
|
|
636
|
+
if (!response.ok || response.status !== 200) {
|
|
637
|
+
throw new Error(`Failed to get plugin versions: ${response.status} ${response.statusText}`);
|
|
638
|
+
}
|
|
639
|
+
const result = await response.json();
|
|
640
|
+
console.log("getPluginVersions response:", result);
|
|
641
|
+
if (result.status_code !== "0") {
|
|
642
|
+
throw new Error(`API error: ${result.message}`);
|
|
643
|
+
}
|
|
644
|
+
return result.data.pluginVersions;
|
|
645
|
+
}
|
|
646
|
+
async function getPluginVersion(pluginKey, requestedVersion) {
|
|
647
|
+
const isLatest = requestedVersion === "latest";
|
|
648
|
+
const versions = await getPluginVersions([pluginKey], isLatest);
|
|
649
|
+
const pluginVersions = versions[pluginKey];
|
|
650
|
+
if (!pluginVersions || pluginVersions.length === 0) {
|
|
651
|
+
throw new Error(`Plugin not found: ${pluginKey}`);
|
|
652
|
+
}
|
|
653
|
+
if (isLatest) {
|
|
654
|
+
return pluginVersions[0];
|
|
655
|
+
}
|
|
656
|
+
const targetVersion = pluginVersions.find((v) => v.version === requestedVersion);
|
|
657
|
+
if (!targetVersion) {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`Version ${requestedVersion} not found for plugin ${pluginKey}. Available versions: ${pluginVersions.map((v) => v.version).join(", ")}`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
return targetVersion;
|
|
663
|
+
}
|
|
664
|
+
function parsePluginKey(key) {
|
|
665
|
+
const match = key.match(/^(@[^/]+)\/(.+)$/);
|
|
666
|
+
if (!match) {
|
|
667
|
+
throw new Error(`Invalid plugin key format: ${key}`);
|
|
668
|
+
}
|
|
669
|
+
return { scope: match[1], name: match[2] };
|
|
670
|
+
}
|
|
671
|
+
async function downloadFromInner(pluginKey, version) {
|
|
672
|
+
const client = getHttpClient();
|
|
673
|
+
const { scope, name } = parsePluginKey(pluginKey);
|
|
674
|
+
const url = `/api/v1/studio/innerapi/plugins/${scope}/${name}/versions/${version}/package`;
|
|
675
|
+
console.log(`[action-plugin] Downloading plugin from inner API: ${url}`);
|
|
676
|
+
const response = await client.get(url);
|
|
677
|
+
if (!response.ok) {
|
|
678
|
+
throw new Error(`Failed to download plugin: ${response.status} ${response.statusText}`);
|
|
679
|
+
}
|
|
680
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
681
|
+
return Buffer.from(arrayBuffer);
|
|
682
|
+
}
|
|
683
|
+
async function downloadFromPublic(downloadURL) {
|
|
684
|
+
const client = new HttpClient2({
|
|
685
|
+
timeout: 6e4
|
|
686
|
+
});
|
|
687
|
+
const response = await client.get(downloadURL);
|
|
688
|
+
if (!response.ok) {
|
|
689
|
+
throw new Error(`Failed to download plugin from public URL: ${response.status} ${response.statusText}`);
|
|
690
|
+
}
|
|
691
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
692
|
+
return Buffer.from(arrayBuffer);
|
|
693
|
+
}
|
|
694
|
+
function getPluginCacheDir() {
|
|
695
|
+
return path4.join(process.cwd(), PLUGIN_CACHE_DIR);
|
|
696
|
+
}
|
|
697
|
+
function ensureCacheDir() {
|
|
698
|
+
const cacheDir = getPluginCacheDir();
|
|
699
|
+
if (!fs4.existsSync(cacheDir)) {
|
|
700
|
+
fs4.mkdirSync(cacheDir, { recursive: true });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function getTempFilePath(pluginKey, version) {
|
|
704
|
+
ensureCacheDir();
|
|
705
|
+
const safeKey = pluginKey.replace(/[/@]/g, "_");
|
|
706
|
+
const filename = `${safeKey}-${version}.tgz`;
|
|
707
|
+
return path4.join(getPluginCacheDir(), filename);
|
|
708
|
+
}
|
|
709
|
+
async function downloadPlugin(pluginKey, requestedVersion) {
|
|
710
|
+
console.log(`[action-plugin] Fetching plugin info for ${pluginKey}@${requestedVersion}...`);
|
|
711
|
+
const pluginInfo = await getPluginVersion(pluginKey, requestedVersion);
|
|
712
|
+
console.log(`[action-plugin] Found: ${pluginInfo.name} v${pluginInfo.version}`);
|
|
713
|
+
console.log(`[action-plugin] Download approach: ${pluginInfo.downloadApproach}`);
|
|
714
|
+
let tgzBuffer;
|
|
715
|
+
if (pluginInfo.downloadApproach === "inner") {
|
|
716
|
+
console.log(`[action-plugin] Downloading from inner API...`);
|
|
717
|
+
tgzBuffer = await downloadFromInner(pluginKey, pluginInfo.version);
|
|
718
|
+
} else {
|
|
719
|
+
console.log(`[action-plugin] Downloading from public URL...`);
|
|
720
|
+
tgzBuffer = await downloadFromPublic(pluginInfo.downloadURL);
|
|
721
|
+
}
|
|
722
|
+
const tgzPath = getTempFilePath(pluginKey, pluginInfo.version);
|
|
723
|
+
fs4.writeFileSync(tgzPath, tgzBuffer);
|
|
724
|
+
console.log(`[action-plugin] Downloaded to ${tgzPath} (${(tgzBuffer.length / 1024).toFixed(2)} KB)`);
|
|
725
|
+
return {
|
|
726
|
+
tgzPath,
|
|
727
|
+
version: pluginInfo.version,
|
|
728
|
+
pluginInfo
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function cleanupTempFile(tgzPath) {
|
|
732
|
+
try {
|
|
733
|
+
if (fs4.existsSync(tgzPath)) {
|
|
734
|
+
fs4.unlinkSync(tgzPath);
|
|
735
|
+
}
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/commands/action-plugin/init.handler.ts
|
|
741
|
+
async function installOneForInit(name, version) {
|
|
742
|
+
let tgzPath;
|
|
743
|
+
try {
|
|
744
|
+
const installedVersion = getPackageVersion(name);
|
|
745
|
+
if (installedVersion === version) {
|
|
746
|
+
return { name, version, success: true, skipped: true };
|
|
747
|
+
}
|
|
748
|
+
console.log(`[action-plugin] Installing ${name}@${version}...`);
|
|
749
|
+
const downloadResult = await downloadPlugin(name, version);
|
|
750
|
+
tgzPath = downloadResult.tgzPath;
|
|
751
|
+
const pluginDir = extractTgzToNodeModules(tgzPath, name);
|
|
752
|
+
const pluginPkg = readPluginPackageJson(pluginDir);
|
|
753
|
+
if (pluginPkg?.peerDependencies) {
|
|
754
|
+
const missingDeps = checkMissingPeerDeps(pluginPkg.peerDependencies);
|
|
755
|
+
if (missingDeps.length > 0) {
|
|
756
|
+
installMissingDeps(missingDeps);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
console.log(`[action-plugin] \u2713 Installed ${name}@${version}`);
|
|
760
|
+
return { name, version, success: true };
|
|
761
|
+
} catch (error) {
|
|
762
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
763
|
+
console.error(`[action-plugin] \u2717 Failed to install ${name}: ${message}`);
|
|
764
|
+
return { name, version, success: false, error: message };
|
|
765
|
+
} finally {
|
|
766
|
+
if (tgzPath) {
|
|
767
|
+
cleanupTempFile(tgzPath);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function init() {
|
|
772
|
+
console.log("[action-plugin] Reading plugins from package.json...");
|
|
773
|
+
const plugins = readActionPlugins();
|
|
774
|
+
const entries = Object.entries(plugins);
|
|
775
|
+
if (entries.length === 0) {
|
|
776
|
+
console.log("[action-plugin] No plugins found in package.json");
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
console.log(`[action-plugin] Found ${entries.length} plugin(s) to install
|
|
780
|
+
`);
|
|
781
|
+
const results = [];
|
|
782
|
+
for (const [name, version] of entries) {
|
|
783
|
+
const result = await installOneForInit(name, version);
|
|
784
|
+
results.push(result);
|
|
785
|
+
}
|
|
786
|
+
console.log("");
|
|
787
|
+
const successful = results.filter((r) => r.success && !r.skipped);
|
|
788
|
+
const skipped = results.filter((r) => r.skipped);
|
|
789
|
+
const failed = results.filter((r) => !r.success);
|
|
790
|
+
if (successful.length > 0 || skipped.length > 0) {
|
|
791
|
+
console.log(
|
|
792
|
+
`[action-plugin] \u2713 All plugins installed successfully (${successful.length + skipped.length}/${entries.length})`
|
|
793
|
+
);
|
|
794
|
+
if (skipped.length > 0) {
|
|
795
|
+
console.log(` Skipped (already installed): ${skipped.map((r) => r.name).join(", ")}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (failed.length > 0) {
|
|
799
|
+
console.log(`[action-plugin] \u2717 Failed to install ${failed.length} plugin(s): ${failed.map((r) => r.name).join(", ")}`);
|
|
800
|
+
process.exit(1);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/commands/action-plugin/install.handler.ts
|
|
805
|
+
async function installOne(nameWithVersion) {
|
|
806
|
+
let tgzPath;
|
|
807
|
+
const { name, version: requestedVersion } = parsePluginName(nameWithVersion);
|
|
808
|
+
try {
|
|
809
|
+
console.log(`[action-plugin] Installing ${name}@${requestedVersion}...`);
|
|
810
|
+
const actualVersion = getPackageVersion(name);
|
|
811
|
+
if (actualVersion && requestedVersion !== "latest") {
|
|
812
|
+
if (actualVersion === requestedVersion) {
|
|
813
|
+
console.log(`[action-plugin] Plugin ${name}@${requestedVersion} is already installed`);
|
|
814
|
+
return { name, version: actualVersion, success: true, skipped: true };
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (actualVersion && requestedVersion === "latest") {
|
|
818
|
+
const latestInfo = await getPluginVersion(name, "latest");
|
|
819
|
+
if (actualVersion === latestInfo.version) {
|
|
820
|
+
console.log(`[action-plugin] Plugin ${name} is already up to date (version: ${actualVersion})`);
|
|
821
|
+
return { name, version: actualVersion, success: true, skipped: true };
|
|
822
|
+
}
|
|
823
|
+
console.log(`[action-plugin] Found newer version: ${latestInfo.version} (installed: ${actualVersion})`);
|
|
824
|
+
}
|
|
825
|
+
const downloadResult = await downloadPlugin(name, requestedVersion);
|
|
826
|
+
tgzPath = downloadResult.tgzPath;
|
|
827
|
+
console.log(`[action-plugin] Extracting to node_modules...`);
|
|
828
|
+
const pluginDir = extractTgzToNodeModules(tgzPath, name);
|
|
829
|
+
const pluginPkg = readPluginPackageJson(pluginDir);
|
|
830
|
+
if (pluginPkg?.peerDependencies) {
|
|
831
|
+
const missingDeps = checkMissingPeerDeps(pluginPkg.peerDependencies);
|
|
832
|
+
if (missingDeps.length > 0) {
|
|
833
|
+
installMissingDeps(missingDeps);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const installedVersion = getPackageVersion(name) || downloadResult.version;
|
|
837
|
+
const plugins = readActionPlugins();
|
|
838
|
+
plugins[name] = installedVersion;
|
|
839
|
+
writeActionPlugins(plugins);
|
|
840
|
+
console.log(`[action-plugin] Successfully installed ${name}@${installedVersion}`);
|
|
841
|
+
return { name, version: installedVersion, success: true };
|
|
842
|
+
} catch (error) {
|
|
843
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
844
|
+
console.error(`[action-plugin] Failed to install ${name}: ${message}`);
|
|
845
|
+
return { name, version: requestedVersion, success: false, error: message };
|
|
846
|
+
} finally {
|
|
847
|
+
if (tgzPath) {
|
|
848
|
+
cleanupTempFile(tgzPath);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function install(namesWithVersion) {
|
|
853
|
+
if (namesWithVersion.length === 0) {
|
|
854
|
+
console.error("[action-plugin] No plugin names provided");
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
const results = [];
|
|
858
|
+
for (const nameWithVersion of namesWithVersion) {
|
|
859
|
+
const result = await installOne(nameWithVersion);
|
|
860
|
+
results.push(result);
|
|
861
|
+
}
|
|
862
|
+
if (namesWithVersion.length > 1) {
|
|
863
|
+
console.log("\n[action-plugin] Installation summary:");
|
|
864
|
+
const successful = results.filter((r) => r.success && !r.skipped);
|
|
865
|
+
const skipped = results.filter((r) => r.skipped);
|
|
866
|
+
const failed = results.filter((r) => !r.success);
|
|
867
|
+
if (successful.length > 0) {
|
|
868
|
+
console.log(` \u2713 Installed: ${successful.map((r) => `${r.name}@${r.version}`).join(", ")}`);
|
|
869
|
+
}
|
|
870
|
+
if (skipped.length > 0) {
|
|
871
|
+
console.log(` \u25CB Skipped (up to date): ${skipped.map((r) => r.name).join(", ")}`);
|
|
872
|
+
}
|
|
873
|
+
if (failed.length > 0) {
|
|
874
|
+
console.log(` \u2717 Failed: ${failed.map((r) => r.name).join(", ")}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (results.some((r) => !r.success)) {
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/commands/action-plugin/update.handler.ts
|
|
883
|
+
async function updateOne(nameWithVersion) {
|
|
884
|
+
let tgzPath;
|
|
885
|
+
const { name } = parsePluginName(nameWithVersion);
|
|
886
|
+
try {
|
|
887
|
+
console.log(`[action-plugin] Updating ${name}...`);
|
|
888
|
+
if (!isPluginInstalled(name)) {
|
|
889
|
+
console.error(`[action-plugin] Plugin ${name} is not installed`);
|
|
890
|
+
return { name, success: false, notInstalled: true };
|
|
891
|
+
}
|
|
892
|
+
const oldVersion = getInstalledPluginVersion(name) || "unknown";
|
|
893
|
+
console.log(`[action-plugin] Current version: ${oldVersion}`);
|
|
894
|
+
const downloadResult = await downloadPlugin(name, "latest");
|
|
895
|
+
tgzPath = downloadResult.tgzPath;
|
|
896
|
+
if (oldVersion === downloadResult.version) {
|
|
897
|
+
console.log(`[action-plugin] Plugin ${name} is already up to date (version: ${downloadResult.version})`);
|
|
898
|
+
return { name, oldVersion, newVersion: downloadResult.version, success: true, skipped: true };
|
|
899
|
+
}
|
|
900
|
+
npmInstall(tgzPath);
|
|
901
|
+
const installedVersion = getPackageVersion(name) || downloadResult.version;
|
|
902
|
+
const plugins = readActionPlugins();
|
|
903
|
+
plugins[name] = installedVersion;
|
|
904
|
+
writeActionPlugins(plugins);
|
|
905
|
+
console.log(`[action-plugin] Successfully updated ${name} to ${installedVersion}`);
|
|
906
|
+
return { name, oldVersion, newVersion: installedVersion, success: true };
|
|
907
|
+
} catch (error) {
|
|
908
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
909
|
+
console.error(`[action-plugin] Failed to update ${name}: ${message}`);
|
|
910
|
+
return { name, success: false, error: message };
|
|
911
|
+
} finally {
|
|
912
|
+
if (tgzPath) {
|
|
913
|
+
cleanupTempFile(tgzPath);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async function update(names) {
|
|
918
|
+
if (names.length === 0) {
|
|
919
|
+
console.error("[action-plugin] No plugin names provided");
|
|
920
|
+
process.exit(1);
|
|
921
|
+
}
|
|
922
|
+
const results = [];
|
|
923
|
+
for (const name of names) {
|
|
924
|
+
const result = await updateOne(name);
|
|
925
|
+
results.push(result);
|
|
926
|
+
}
|
|
927
|
+
if (names.length > 1) {
|
|
928
|
+
console.log("\n[action-plugin] Update summary:");
|
|
929
|
+
const updated = results.filter((r) => r.success && !r.skipped);
|
|
930
|
+
const skipped = results.filter((r) => r.skipped);
|
|
931
|
+
const notInstalled = results.filter((r) => r.notInstalled);
|
|
932
|
+
const failed = results.filter((r) => !r.success && !r.notInstalled);
|
|
933
|
+
if (updated.length > 0) {
|
|
934
|
+
console.log(` \u2713 Updated: ${updated.map((r) => `${r.name} (${r.oldVersion} \u2192 ${r.newVersion})`).join(", ")}`);
|
|
935
|
+
}
|
|
936
|
+
if (skipped.length > 0) {
|
|
937
|
+
console.log(` \u25CB Skipped (up to date): ${skipped.map((r) => r.name).join(", ")}`);
|
|
938
|
+
}
|
|
939
|
+
if (notInstalled.length > 0) {
|
|
940
|
+
console.log(` \u26A0 Not installed: ${notInstalled.map((r) => r.name).join(", ")}`);
|
|
941
|
+
}
|
|
942
|
+
if (failed.length > 0) {
|
|
943
|
+
console.log(` \u2717 Failed: ${failed.map((r) => r.name).join(", ")}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (results.some((r) => !r.success && !r.notInstalled)) {
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/commands/action-plugin/remove.handler.ts
|
|
952
|
+
async function remove(nameWithVersion) {
|
|
953
|
+
try {
|
|
954
|
+
const { name } = parsePluginName(nameWithVersion);
|
|
955
|
+
console.log(`[action-plugin] Removing ${name}...`);
|
|
956
|
+
if (!isPluginInstalled(name)) {
|
|
957
|
+
console.error(`[action-plugin] Plugin ${name} is not installed`);
|
|
958
|
+
process.exit(1);
|
|
959
|
+
}
|
|
960
|
+
removePluginDirectory(name);
|
|
961
|
+
const plugins = readActionPlugins();
|
|
962
|
+
delete plugins[name];
|
|
963
|
+
writeActionPlugins(plugins);
|
|
964
|
+
console.log(`[action-plugin] Successfully removed ${name}`);
|
|
965
|
+
} catch (error) {
|
|
966
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
967
|
+
console.error(`[action-plugin] Failed to remove: ${message}`);
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/commands/action-plugin/list.handler.ts
|
|
973
|
+
async function list() {
|
|
974
|
+
try {
|
|
975
|
+
const plugins = Object.entries(readActionPlugins());
|
|
976
|
+
if (plugins.length === 0) {
|
|
977
|
+
console.log("[action-plugin] No plugins installed");
|
|
978
|
+
console.log('[action-plugin] Use "action-plugin install <name>" to install a plugin');
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
console.log("[action-plugin] Installed plugins:\n");
|
|
982
|
+
const nameWidth = Math.max(40, ...plugins.map(([name]) => name.length + 2));
|
|
983
|
+
const versionWidth = 15;
|
|
984
|
+
console.log(
|
|
985
|
+
" " + "Plugin".padEnd(nameWidth) + "Version"
|
|
986
|
+
);
|
|
987
|
+
console.log(" " + "-".repeat(nameWidth + versionWidth));
|
|
988
|
+
for (const [name, version] of plugins) {
|
|
989
|
+
console.log(
|
|
990
|
+
" " + name.padEnd(nameWidth) + version
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
console.log(`
|
|
994
|
+
Total: ${plugins.length} plugin(s)`);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
997
|
+
console.error(`[action-plugin] Failed to list plugins: ${message}`);
|
|
998
|
+
process.exit(1);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/commands/action-plugin/index.ts
|
|
1003
|
+
var initCommand = {
|
|
1004
|
+
name: "init",
|
|
1005
|
+
description: "Install all plugins from package.json",
|
|
1006
|
+
register(program) {
|
|
1007
|
+
program.command(this.name).description(this.description).action(async () => {
|
|
1008
|
+
await init();
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
var installCommand = {
|
|
1013
|
+
name: "install",
|
|
1014
|
+
description: "Install action plugin(s) (default: latest version)",
|
|
1015
|
+
aliases: ["i"],
|
|
1016
|
+
register(program) {
|
|
1017
|
+
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) => {
|
|
1018
|
+
await install(names);
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
var updateCommand = {
|
|
1023
|
+
name: "update",
|
|
1024
|
+
description: "Update action plugin(s) to the latest version",
|
|
1025
|
+
aliases: ["up"],
|
|
1026
|
+
register(program) {
|
|
1027
|
+
program.command(this.name).alias("up").description(this.description).argument("<names...>", "Plugin name(s) (e.g., @office/feishu-create-group)").action(async (names) => {
|
|
1028
|
+
await update(names);
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
var removeCommand = {
|
|
1033
|
+
name: "remove",
|
|
1034
|
+
description: "Remove an installed action plugin",
|
|
1035
|
+
aliases: ["rm"],
|
|
1036
|
+
register(program) {
|
|
1037
|
+
program.command(this.name).alias("rm").description(this.description).argument("<name>", "Plugin name (e.g., @office/feishu-create-group)").action(async (name) => {
|
|
1038
|
+
await remove(name);
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
var listCommand = {
|
|
1043
|
+
name: "list",
|
|
1044
|
+
description: "List all installed action plugins",
|
|
1045
|
+
aliases: ["ls"],
|
|
1046
|
+
register(program) {
|
|
1047
|
+
program.command(this.name).alias("ls").description(this.description).action(async () => {
|
|
1048
|
+
await list();
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
var actionPluginCommandGroup = {
|
|
1053
|
+
name: "action-plugin",
|
|
1054
|
+
description: "Manage action plugins",
|
|
1055
|
+
commands: [initCommand, installCommand, updateCommand, removeCommand, listCommand]
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
// src/commands/capability/utils.ts
|
|
1059
|
+
import fs5 from "fs";
|
|
1060
|
+
import path5 from "path";
|
|
1061
|
+
var CAPABILITIES_DIR = "server/capabilities";
|
|
1062
|
+
function getProjectRoot2() {
|
|
1063
|
+
return process.cwd();
|
|
1064
|
+
}
|
|
1065
|
+
function getCapabilitiesDir() {
|
|
1066
|
+
return path5.join(getProjectRoot2(), CAPABILITIES_DIR);
|
|
1067
|
+
}
|
|
1068
|
+
function getCapabilityPath(id) {
|
|
1069
|
+
return path5.join(getCapabilitiesDir(), `${id}.json`);
|
|
1070
|
+
}
|
|
1071
|
+
function getPluginManifestPath(pluginKey) {
|
|
1072
|
+
return path5.join(getProjectRoot2(), "node_modules", pluginKey, "manifest.json");
|
|
1073
|
+
}
|
|
1074
|
+
function capabilitiesDirExists() {
|
|
1075
|
+
return fs5.existsSync(getCapabilitiesDir());
|
|
1076
|
+
}
|
|
1077
|
+
function listCapabilityIds() {
|
|
1078
|
+
const dir = getCapabilitiesDir();
|
|
1079
|
+
if (!fs5.existsSync(dir)) {
|
|
1080
|
+
return [];
|
|
1081
|
+
}
|
|
1082
|
+
const files = fs5.readdirSync(dir);
|
|
1083
|
+
return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
|
|
1084
|
+
}
|
|
1085
|
+
function readCapability(id) {
|
|
1086
|
+
const filePath = getCapabilityPath(id);
|
|
1087
|
+
if (!fs5.existsSync(filePath)) {
|
|
1088
|
+
throw new Error(`Capability not found: ${id}`);
|
|
1089
|
+
}
|
|
1090
|
+
try {
|
|
1091
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
1092
|
+
return JSON.parse(content);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (error instanceof SyntaxError) {
|
|
1095
|
+
throw new Error(`Invalid JSON in capability file: ${id}.json`);
|
|
1096
|
+
}
|
|
1097
|
+
throw error;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
function readAllCapabilities() {
|
|
1101
|
+
const ids = listCapabilityIds();
|
|
1102
|
+
return ids.map((id) => readCapability(id));
|
|
1103
|
+
}
|
|
1104
|
+
function readPluginManifest(pluginKey) {
|
|
1105
|
+
const manifestPath = getPluginManifestPath(pluginKey);
|
|
1106
|
+
if (!fs5.existsSync(manifestPath)) {
|
|
1107
|
+
throw new Error(`Plugin not installed: ${pluginKey} (manifest.json not found)`);
|
|
1108
|
+
}
|
|
1109
|
+
try {
|
|
1110
|
+
const content = fs5.readFileSync(manifestPath, "utf-8");
|
|
1111
|
+
return JSON.parse(content);
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
if (error instanceof SyntaxError) {
|
|
1114
|
+
throw new Error(`Invalid JSON in plugin manifest: ${pluginKey}/manifest.json`);
|
|
1115
|
+
}
|
|
1116
|
+
throw error;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function isDynamicSchema(schema) {
|
|
1120
|
+
return schema !== void 0 && typeof schema === "object" && "dynamic" in schema && schema.dynamic === true;
|
|
1121
|
+
}
|
|
1122
|
+
function hasValidParamsSchema(paramsSchema) {
|
|
1123
|
+
return paramsSchema !== void 0 && Object.keys(paramsSchema).length > 0;
|
|
1124
|
+
}
|
|
1125
|
+
async function loadPlugin(pluginKey) {
|
|
1126
|
+
try {
|
|
1127
|
+
const pluginPackage = (await import(pluginKey)).default;
|
|
1128
|
+
if (!pluginPackage || typeof pluginPackage.create !== "function") {
|
|
1129
|
+
throw new Error(`Plugin ${pluginKey} does not export a valid create function`);
|
|
1130
|
+
}
|
|
1131
|
+
return pluginPackage;
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
if (error.code === "MODULE_NOT_FOUND") {
|
|
1134
|
+
throw new Error(`Plugin not installed: ${pluginKey}`);
|
|
1135
|
+
}
|
|
1136
|
+
throw new Error(
|
|
1137
|
+
`Failed to load plugin ${pluginKey}: ${error instanceof Error ? error.message : String(error)}`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
async function zodToJsonSchema(zodSchema) {
|
|
1142
|
+
try {
|
|
1143
|
+
const { zodToJsonSchema: convert } = await import("zod-to-json-schema");
|
|
1144
|
+
return convert(zodSchema);
|
|
1145
|
+
} catch {
|
|
1146
|
+
throw new Error("Failed to convert Zod schema to JSON Schema. Make sure zod-to-json-schema is installed.");
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async function hydrateCapability(capability) {
|
|
1150
|
+
try {
|
|
1151
|
+
const manifest = readPluginManifest(capability.pluginKey);
|
|
1152
|
+
if (!manifest.actions || manifest.actions.length === 0) {
|
|
1153
|
+
throw new Error(`Plugin ${capability.pluginKey} has no actions defined`);
|
|
1154
|
+
}
|
|
1155
|
+
const hasDynamicSchema = manifest.actions.some(
|
|
1156
|
+
(action) => isDynamicSchema(action.inputSchema) || isDynamicSchema(action.outputSchema)
|
|
1157
|
+
);
|
|
1158
|
+
let pluginInstance = null;
|
|
1159
|
+
if (hasDynamicSchema) {
|
|
1160
|
+
const plugin = await loadPlugin(capability.pluginKey);
|
|
1161
|
+
pluginInstance = plugin.create(capability.formValue || {});
|
|
1162
|
+
}
|
|
1163
|
+
const actions = [];
|
|
1164
|
+
for (let index = 0; index < manifest.actions.length; index++) {
|
|
1165
|
+
const manifestAction = manifest.actions[index];
|
|
1166
|
+
let inputSchema;
|
|
1167
|
+
if (index === 0 && hasValidParamsSchema(capability.paramsSchema)) {
|
|
1168
|
+
inputSchema = capability.paramsSchema;
|
|
1169
|
+
} else if (isDynamicSchema(manifestAction.inputSchema)) {
|
|
1170
|
+
if (!pluginInstance) {
|
|
1171
|
+
throw new Error(`Plugin instance not available for dynamic schema`);
|
|
1172
|
+
}
|
|
1173
|
+
const zodSchema = pluginInstance.getInputSchema(manifestAction.key);
|
|
1174
|
+
if (!zodSchema) {
|
|
1175
|
+
throw new Error(`Failed to get input schema for action: ${manifestAction.key}`);
|
|
1176
|
+
}
|
|
1177
|
+
inputSchema = await zodToJsonSchema(zodSchema);
|
|
1178
|
+
} else {
|
|
1179
|
+
inputSchema = manifestAction.inputSchema;
|
|
1180
|
+
}
|
|
1181
|
+
let outputSchema;
|
|
1182
|
+
if (isDynamicSchema(manifestAction.outputSchema)) {
|
|
1183
|
+
if (!pluginInstance) {
|
|
1184
|
+
throw new Error(`Plugin instance not available for dynamic schema`);
|
|
1185
|
+
}
|
|
1186
|
+
const zodSchema = pluginInstance.getOutputSchema(manifestAction.key, {});
|
|
1187
|
+
if (!zodSchema) {
|
|
1188
|
+
throw new Error(`Failed to get output schema for action: ${manifestAction.key}`);
|
|
1189
|
+
}
|
|
1190
|
+
outputSchema = await zodToJsonSchema(zodSchema);
|
|
1191
|
+
} else {
|
|
1192
|
+
outputSchema = manifestAction.outputSchema;
|
|
1193
|
+
}
|
|
1194
|
+
actions.push({
|
|
1195
|
+
key: manifestAction.key,
|
|
1196
|
+
inputSchema,
|
|
1197
|
+
outputSchema,
|
|
1198
|
+
outputMode: manifestAction.outputMode || ""
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
id: capability.id,
|
|
1203
|
+
pluginKey: capability.pluginKey,
|
|
1204
|
+
pluginVersion: capability.pluginVersion,
|
|
1205
|
+
name: capability.name,
|
|
1206
|
+
description: capability.description,
|
|
1207
|
+
actions,
|
|
1208
|
+
createdAt: capability.createdAt,
|
|
1209
|
+
updatedAt: capability.updatedAt
|
|
1210
|
+
};
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1213
|
+
return {
|
|
1214
|
+
...capability,
|
|
1215
|
+
_hydrateError: message
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function formatJsonOutput(data, compact = false) {
|
|
1220
|
+
return compact ? JSON.stringify(data) : JSON.stringify(data, null, 2);
|
|
1221
|
+
}
|
|
1222
|
+
function printJson(data, compact = false) {
|
|
1223
|
+
console.log(formatJsonOutput(data, compact));
|
|
1224
|
+
}
|
|
1225
|
+
function logError(message) {
|
|
1226
|
+
console.error(`[capability] Error: ${message}`);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/commands/capability/list.handler.ts
|
|
1230
|
+
async function list2(options) {
|
|
1231
|
+
try {
|
|
1232
|
+
if (!capabilitiesDirExists()) {
|
|
1233
|
+
logError("server/capabilities directory not found");
|
|
1234
|
+
process.exit(1);
|
|
1235
|
+
}
|
|
1236
|
+
if (options.id) {
|
|
1237
|
+
await listSingle(options.id, options.summary);
|
|
1238
|
+
} else {
|
|
1239
|
+
await listAll(options.summary);
|
|
1240
|
+
}
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1243
|
+
logError(message);
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
async function listSingle(id, summary) {
|
|
1248
|
+
const capability = readCapability(id);
|
|
1249
|
+
if (summary) {
|
|
1250
|
+
printJson(capability);
|
|
1251
|
+
} else {
|
|
1252
|
+
const hydrated = await hydrateCapability(capability);
|
|
1253
|
+
printJson(hydrated);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function listAll(summary) {
|
|
1257
|
+
const capabilities = readAllCapabilities();
|
|
1258
|
+
if (capabilities.length === 0) {
|
|
1259
|
+
printJson([]);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (summary) {
|
|
1263
|
+
printJson(capabilities);
|
|
1264
|
+
} else {
|
|
1265
|
+
const hydrated = [];
|
|
1266
|
+
for (const capability of capabilities) {
|
|
1267
|
+
const result = await hydrateCapability(capability);
|
|
1268
|
+
hydrated.push(result);
|
|
1269
|
+
}
|
|
1270
|
+
printJson(hydrated);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/commands/capability/index.ts
|
|
1275
|
+
var listCommand2 = {
|
|
1276
|
+
name: "list",
|
|
1277
|
+
description: "List capability configurations",
|
|
1278
|
+
aliases: ["ls"],
|
|
1279
|
+
register(program) {
|
|
1280
|
+
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) => {
|
|
1281
|
+
await list2(options);
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
var capabilityCommandGroup = {
|
|
1286
|
+
name: "capability",
|
|
1287
|
+
description: "Manage capability configurations",
|
|
1288
|
+
commands: [listCommand2]
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
// src/commands/migration/version-manager.ts
|
|
1292
|
+
import fs6 from "fs";
|
|
1293
|
+
import path6 from "path";
|
|
1294
|
+
var PACKAGE_JSON = "package.json";
|
|
1295
|
+
var VERSION_FIELD = "migrationVersion";
|
|
1296
|
+
function getPackageJsonPath2() {
|
|
1297
|
+
return path6.join(process.cwd(), PACKAGE_JSON);
|
|
1298
|
+
}
|
|
1299
|
+
function getCurrentVersion() {
|
|
1300
|
+
const pkgPath = getPackageJsonPath2();
|
|
1301
|
+
if (!fs6.existsSync(pkgPath)) {
|
|
1302
|
+
throw new Error("package.json not found");
|
|
1303
|
+
}
|
|
1304
|
+
const pkg2 = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
1305
|
+
return pkg2[VERSION_FIELD] ?? 0;
|
|
1306
|
+
}
|
|
1307
|
+
function setCurrentVersion(version) {
|
|
1308
|
+
const pkgPath = getPackageJsonPath2();
|
|
1309
|
+
const pkg2 = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
1310
|
+
pkg2[VERSION_FIELD] = version;
|
|
1311
|
+
fs6.writeFileSync(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/commands/migration/versions/v001_capability/json-migrator/detector.ts
|
|
1315
|
+
import fs8 from "fs";
|
|
1316
|
+
import path8 from "path";
|
|
1317
|
+
|
|
1318
|
+
// src/commands/migration/versions/v001_capability/utils.ts
|
|
1319
|
+
import fs7 from "fs";
|
|
1320
|
+
import path7 from "path";
|
|
1321
|
+
var CAPABILITIES_DIR2 = "server/capabilities";
|
|
1322
|
+
function getProjectRoot3() {
|
|
1323
|
+
return process.cwd();
|
|
1324
|
+
}
|
|
1325
|
+
function getCapabilitiesDir2() {
|
|
1326
|
+
return path7.join(getProjectRoot3(), CAPABILITIES_DIR2);
|
|
1327
|
+
}
|
|
1328
|
+
function getPluginManifestPath2(pluginKey) {
|
|
1329
|
+
return path7.join(getProjectRoot3(), "node_modules", pluginKey, "manifest.json");
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/commands/migration/versions/v001_capability/json-migrator/detector.ts
|
|
1333
|
+
function detectJsonMigration() {
|
|
1334
|
+
const capabilitiesDir = getCapabilitiesDir2();
|
|
1335
|
+
const oldFilePath = path8.join(capabilitiesDir, "capabilities.json");
|
|
1336
|
+
if (!fs8.existsSync(oldFilePath)) {
|
|
1337
|
+
return {
|
|
1338
|
+
needsMigration: false,
|
|
1339
|
+
reason: "capabilities.json not found"
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
const content = fs8.readFileSync(oldFilePath, "utf-8");
|
|
1344
|
+
const parsed = JSON.parse(content);
|
|
1345
|
+
const capabilities = Array.isArray(parsed) ? parsed : [];
|
|
1346
|
+
return {
|
|
1347
|
+
needsMigration: true,
|
|
1348
|
+
oldCapabilities: capabilities,
|
|
1349
|
+
oldFilePath
|
|
1350
|
+
};
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
return {
|
|
1353
|
+
needsMigration: true,
|
|
1354
|
+
oldCapabilities: [],
|
|
1355
|
+
oldFilePath
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/commands/migration/versions/v001_capability/check.ts
|
|
1361
|
+
async function check(options) {
|
|
1362
|
+
const detection = detectJsonMigration();
|
|
1363
|
+
if (!detection.needsMigration) {
|
|
1364
|
+
return {
|
|
1365
|
+
needsMigration: false,
|
|
1366
|
+
message: detection.reason || "No migration needed"
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
const capabilityCount = detection.oldCapabilities?.length || 0;
|
|
1370
|
+
return {
|
|
1371
|
+
needsMigration: true,
|
|
1372
|
+
message: `found ${capabilityCount} capabilities to migrate`,
|
|
1373
|
+
items: [
|
|
1374
|
+
`server/capabilities/capabilities.json \u2192 server/capabilities/*.json`,
|
|
1375
|
+
`${capabilityCount} capabilities will be converted to new format`
|
|
1376
|
+
]
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/commands/migration/versions/v001_capability/json-migrator/index.ts
|
|
1381
|
+
import fs9 from "fs";
|
|
1382
|
+
import path9 from "path";
|
|
1383
|
+
|
|
1384
|
+
// src/commands/migration/versions/v001_capability/mapping.ts
|
|
1385
|
+
var DEFAULT_PLUGIN_VERSION = "1.0.0";
|
|
1386
|
+
var PLUGIN_MAPPING = {
|
|
1387
|
+
// 飞书相关
|
|
1388
|
+
"official_feishu/send_message": {
|
|
1389
|
+
pluginKey: "@official-plugins/send-feishu-message",
|
|
1390
|
+
pluginVersion: "1.0.0"
|
|
1391
|
+
},
|
|
1392
|
+
"official_feishu/create_group": {
|
|
1393
|
+
pluginKey: "@official-plugins/feishu-group-create",
|
|
1394
|
+
pluginVersion: "1.0.0"
|
|
1395
|
+
},
|
|
1396
|
+
// AI 相关
|
|
1397
|
+
"official_ai/text_generate": {
|
|
1398
|
+
pluginKey: "@official-plugins/ai-text-generate",
|
|
1399
|
+
pluginVersion: "1.0.0"
|
|
1400
|
+
},
|
|
1401
|
+
"official_ai/image_understanding": {
|
|
1402
|
+
pluginKey: "@official-plugins/ai-image-understanding",
|
|
1403
|
+
pluginVersion: "1.0.0"
|
|
1404
|
+
},
|
|
1405
|
+
"official_ai/image_generate": {
|
|
1406
|
+
pluginKey: "@official-plugins/ai-text-to-image",
|
|
1407
|
+
pluginVersion: "1.0.0"
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
function getPluginInfo(sourceActionID) {
|
|
1411
|
+
const item = PLUGIN_MAPPING[sourceActionID];
|
|
1412
|
+
if (!item) {
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
`Unknown sourceActionID: "${sourceActionID}". This sourceActionID is not in the built-in mapping. Please contact the developer to add it.`
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
return {
|
|
1418
|
+
pluginKey: item.pluginKey,
|
|
1419
|
+
pluginVersion: item.pluginVersion ?? DEFAULT_PLUGIN_VERSION
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/send-feishu-message.ts
|
|
1424
|
+
function transformSendFeishuMessage(input) {
|
|
1425
|
+
const actionInput = input;
|
|
1426
|
+
const cardContent = actionInput.card_content;
|
|
1427
|
+
const header = cardContent?.header;
|
|
1428
|
+
const bodyElements = cardContent?.body?.elements;
|
|
1429
|
+
const title = {
|
|
1430
|
+
title: header?.title?.content ?? "",
|
|
1431
|
+
titleColor: header?.template
|
|
1432
|
+
};
|
|
1433
|
+
const markdownElement = bodyElements?.find((el) => el.tag === "markdown");
|
|
1434
|
+
const content = markdownElement?.content ?? "";
|
|
1435
|
+
const columnSet = bodyElements?.find((el) => el.tag === "column_set");
|
|
1436
|
+
const buttons = columnSet?.columns?.flatMap(
|
|
1437
|
+
(column) => column.elements?.filter((el) => el.tag === "button").map((btn) => ({
|
|
1438
|
+
style: btn.type,
|
|
1439
|
+
text: btn.text?.content,
|
|
1440
|
+
url: btn.behaviors?.[0]?.default_url
|
|
1441
|
+
})) ?? []
|
|
1442
|
+
) ?? [];
|
|
1443
|
+
return {
|
|
1444
|
+
sender: "bot",
|
|
1445
|
+
receiverUserList: actionInput.receiver_user_list ?? [],
|
|
1446
|
+
receiverGroupList: actionInput.receiver_group_list ?? [],
|
|
1447
|
+
title,
|
|
1448
|
+
content,
|
|
1449
|
+
buttons
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/feishu-group-create.ts
|
|
1454
|
+
function transformFeishuGroupCreate(input) {
|
|
1455
|
+
const actionInput = input;
|
|
1456
|
+
return {
|
|
1457
|
+
owner: actionInput.owner,
|
|
1458
|
+
members: actionInput.members ?? [],
|
|
1459
|
+
groupName: actionInput.group_name ?? "",
|
|
1460
|
+
groupDescription: actionInput.group_description ?? "",
|
|
1461
|
+
welcomeMessage: actionInput.welcome_message ?? ""
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/utils.ts
|
|
1466
|
+
function convertToNumber(value, defaultValue) {
|
|
1467
|
+
if (value === void 0 || value === "") {
|
|
1468
|
+
return defaultValue;
|
|
1469
|
+
}
|
|
1470
|
+
const num = Number(value);
|
|
1471
|
+
return isNaN(num) ? defaultValue : num;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/ai-text-generate.ts
|
|
1475
|
+
var DEFAULT_MODEL_ID = "87";
|
|
1476
|
+
var DEFAULT_MAX_TOKENS = 8192;
|
|
1477
|
+
var DEFAULT_TEMPERATURE = 0.7;
|
|
1478
|
+
function transformAITextGenerate(input) {
|
|
1479
|
+
const actionInput = input;
|
|
1480
|
+
return {
|
|
1481
|
+
modelID: actionInput.model_id ?? DEFAULT_MODEL_ID,
|
|
1482
|
+
prompt: actionInput.prompt ?? "",
|
|
1483
|
+
modelParams: {
|
|
1484
|
+
maxTokens: convertToNumber(actionInput.max_tokens, DEFAULT_MAX_TOKENS),
|
|
1485
|
+
temperature: convertToNumber(actionInput.temperature, DEFAULT_TEMPERATURE)
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/ai-image-understanding.ts
|
|
1491
|
+
var DEFAULT_MODEL_ID2 = "87";
|
|
1492
|
+
var DEFAULT_MAX_TOKENS2 = 8192;
|
|
1493
|
+
var DEFAULT_TEMPERATURE2 = 0.7;
|
|
1494
|
+
function transformAIImageUnderstanding(input) {
|
|
1495
|
+
const actionInput = input;
|
|
1496
|
+
return {
|
|
1497
|
+
modelID: actionInput.model_id ?? DEFAULT_MODEL_ID2,
|
|
1498
|
+
prompt: actionInput.prompt ?? "",
|
|
1499
|
+
modelParams: {
|
|
1500
|
+
maxTokens: convertToNumber(actionInput.max_tokens, DEFAULT_MAX_TOKENS2),
|
|
1501
|
+
temperature: convertToNumber(actionInput.temperature, DEFAULT_TEMPERATURE2)
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/ai-text-to-image.ts
|
|
1507
|
+
var DEFAULT_RATIO = "1:1";
|
|
1508
|
+
var DEFAULT_WATERMARK = true;
|
|
1509
|
+
function transformAITextToImage(input) {
|
|
1510
|
+
const actionInput = input;
|
|
1511
|
+
return {
|
|
1512
|
+
prompt: actionInput.prompt ?? "",
|
|
1513
|
+
ratio: actionInput.image_ratio ?? DEFAULT_RATIO,
|
|
1514
|
+
watermark: actionInput.watermark ?? DEFAULT_WATERMARK
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/commands/migration/versions/v001_capability/json-migrator/form-value-transformers/index.ts
|
|
1519
|
+
var TRANSFORMER_MAP = {
|
|
1520
|
+
"official_feishu/send_message": transformSendFeishuMessage,
|
|
1521
|
+
"official_feishu/create_group": transformFeishuGroupCreate,
|
|
1522
|
+
"official_ai/text_generate": transformAITextGenerate,
|
|
1523
|
+
"official_ai/image_understanding": transformAIImageUnderstanding,
|
|
1524
|
+
"official_ai/image_generate": transformAITextToImage
|
|
1525
|
+
};
|
|
1526
|
+
function transformFormValue(sourceActionID, actionInput) {
|
|
1527
|
+
const transformer = TRANSFORMER_MAP[sourceActionID];
|
|
1528
|
+
if (!transformer) {
|
|
1529
|
+
throw new Error(
|
|
1530
|
+
`No formValue transformer found for sourceActionID: "${sourceActionID}". Please implement the transformer and add it to TRANSFORMER_MAP.`
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
return transformer(actionInput);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// src/commands/migration/versions/v001_capability/json-migrator/transformer.ts
|
|
1537
|
+
function transformCapability(old) {
|
|
1538
|
+
const { pluginKey, pluginVersion } = getPluginInfo(old.sourceActionID);
|
|
1539
|
+
const formValue = transformFormValue(old.sourceActionID, old.actionInput);
|
|
1540
|
+
return {
|
|
1541
|
+
id: old.id,
|
|
1542
|
+
pluginKey,
|
|
1543
|
+
pluginVersion,
|
|
1544
|
+
name: old.name,
|
|
1545
|
+
description: old.desc,
|
|
1546
|
+
paramsSchema: old.inputSchema,
|
|
1547
|
+
formValue,
|
|
1548
|
+
createdAt: old.createdAt,
|
|
1549
|
+
updatedAt: old.updatedAt
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function transformCapabilities(oldCapabilities) {
|
|
1553
|
+
return oldCapabilities.map((old) => transformCapability(old));
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/commands/migration/versions/v001_capability/json-migrator/index.ts
|
|
1557
|
+
async function migrateJsonFiles(options) {
|
|
1558
|
+
const detection = detectJsonMigration();
|
|
1559
|
+
if (!detection.needsMigration) {
|
|
1560
|
+
return {
|
|
1561
|
+
success: true,
|
|
1562
|
+
skipped: true,
|
|
1563
|
+
reason: detection.reason,
|
|
1564
|
+
count: 0,
|
|
1565
|
+
capabilities: []
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
const { oldCapabilities, oldFilePath } = detection;
|
|
1569
|
+
if (!oldCapabilities || !oldFilePath) {
|
|
1570
|
+
return {
|
|
1571
|
+
success: false,
|
|
1572
|
+
skipped: false,
|
|
1573
|
+
reason: "Detection error: missing data",
|
|
1574
|
+
count: 0,
|
|
1575
|
+
capabilities: []
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
let newCapabilities;
|
|
1579
|
+
try {
|
|
1580
|
+
newCapabilities = transformCapabilities(oldCapabilities);
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
return {
|
|
1583
|
+
success: false,
|
|
1584
|
+
skipped: false,
|
|
1585
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1586
|
+
count: 0,
|
|
1587
|
+
capabilities: []
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
if (options.dryRun) {
|
|
1591
|
+
console.log(` Would create ${newCapabilities.length} capability files:`);
|
|
1592
|
+
for (const cap of newCapabilities) {
|
|
1593
|
+
console.log(` - ${cap.id}.json (pluginKey: ${cap.pluginKey})`);
|
|
1594
|
+
}
|
|
1595
|
+
return {
|
|
1596
|
+
success: true,
|
|
1597
|
+
skipped: false,
|
|
1598
|
+
count: newCapabilities.length,
|
|
1599
|
+
capabilities: newCapabilities
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
const capabilitiesDir = getCapabilitiesDir2();
|
|
1603
|
+
for (const cap of newCapabilities) {
|
|
1604
|
+
const filePath = path9.join(capabilitiesDir, `${cap.id}.json`);
|
|
1605
|
+
const content = JSON.stringify(cap, null, 2);
|
|
1606
|
+
fs9.writeFileSync(filePath, content, "utf-8");
|
|
1607
|
+
console.log(` \u2713 Created: ${cap.id}.json`);
|
|
1608
|
+
}
|
|
1609
|
+
return {
|
|
1610
|
+
success: true,
|
|
1611
|
+
skipped: false,
|
|
1612
|
+
count: newCapabilities.length,
|
|
1613
|
+
capabilities: newCapabilities
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/commands/migration/versions/v001_capability/plugin-installer/detector.ts
|
|
1618
|
+
import fs10 from "fs";
|
|
1619
|
+
function isPluginInstalled2(pluginKey) {
|
|
1620
|
+
const manifestPath = getPluginManifestPath2(pluginKey);
|
|
1621
|
+
return fs10.existsSync(manifestPath);
|
|
1622
|
+
}
|
|
1623
|
+
function detectPluginsToInstall(capabilities) {
|
|
1624
|
+
const pluginKeys = /* @__PURE__ */ new Set();
|
|
1625
|
+
for (const cap of capabilities) {
|
|
1626
|
+
pluginKeys.add(cap.pluginKey);
|
|
1627
|
+
}
|
|
1628
|
+
const toInstall = [];
|
|
1629
|
+
const alreadyInstalled = [];
|
|
1630
|
+
for (const pluginKey of pluginKeys) {
|
|
1631
|
+
if (isPluginInstalled2(pluginKey)) {
|
|
1632
|
+
alreadyInstalled.push(pluginKey);
|
|
1633
|
+
} else {
|
|
1634
|
+
toInstall.push(pluginKey);
|
|
36
1635
|
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1636
|
+
}
|
|
1637
|
+
return { toInstall, alreadyInstalled };
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// src/commands/migration/versions/v001_capability/plugin-installer/index.ts
|
|
1641
|
+
async function installPlugins(capabilities, options) {
|
|
1642
|
+
const detection = detectPluginsToInstall(capabilities);
|
|
1643
|
+
const result = {
|
|
1644
|
+
installed: [],
|
|
1645
|
+
alreadyInstalled: detection.alreadyInstalled,
|
|
1646
|
+
failed: []
|
|
1647
|
+
};
|
|
1648
|
+
if (detection.toInstall.length === 0) {
|
|
1649
|
+
return result;
|
|
1650
|
+
}
|
|
1651
|
+
if (options.dryRun) {
|
|
1652
|
+
console.log(` Would install ${detection.toInstall.length} plugins:`);
|
|
1653
|
+
for (const pluginKey of detection.toInstall) {
|
|
1654
|
+
console.log(` - ${pluginKey}`);
|
|
1655
|
+
}
|
|
1656
|
+
result.installed = detection.toInstall;
|
|
1657
|
+
return result;
|
|
1658
|
+
}
|
|
1659
|
+
console.log(` \u2B07 Installing ${detection.toInstall.length} plugins...`);
|
|
1660
|
+
for (const pluginKey of detection.toInstall) {
|
|
1661
|
+
console.log(` - ${pluginKey}`);
|
|
1662
|
+
}
|
|
1663
|
+
try {
|
|
1664
|
+
const originalExit = process.exit;
|
|
1665
|
+
let exitCalled = false;
|
|
1666
|
+
process.exit = ((code) => {
|
|
1667
|
+
exitCalled = true;
|
|
1668
|
+
if (code !== 0) {
|
|
1669
|
+
throw new Error(`Plugin installation failed with exit code ${code}`);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
44
1672
|
try {
|
|
45
|
-
|
|
1673
|
+
await install(detection.toInstall);
|
|
1674
|
+
result.installed = detection.toInstall;
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
for (const pluginKey of detection.toInstall) {
|
|
1677
|
+
result.failed.push({
|
|
1678
|
+
pluginKey,
|
|
1679
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
} finally {
|
|
1683
|
+
process.exit = originalExit;
|
|
46
1684
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
for (const pluginKey of detection.toInstall) {
|
|
1687
|
+
result.failed.push({
|
|
1688
|
+
pluginKey,
|
|
1689
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return result;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/commands/migration/versions/v001_capability/code-migrator/index.ts
|
|
1697
|
+
import fs12 from "fs";
|
|
1698
|
+
import path11 from "path";
|
|
1699
|
+
import * as ts4 from "typescript";
|
|
1700
|
+
|
|
1701
|
+
// src/commands/migration/versions/v001_capability/code-migrator/scanner.ts
|
|
1702
|
+
import fs11 from "fs";
|
|
1703
|
+
import path10 from "path";
|
|
1704
|
+
var EXCLUDED_DIRS = [
|
|
1705
|
+
"node_modules",
|
|
1706
|
+
"dist",
|
|
1707
|
+
"build",
|
|
1708
|
+
".git",
|
|
1709
|
+
"__test__",
|
|
1710
|
+
"__tests__"
|
|
1711
|
+
];
|
|
1712
|
+
var EXCLUDED_PATTERNS = [
|
|
1713
|
+
/\.spec\.ts$/,
|
|
1714
|
+
/\.test\.ts$/,
|
|
1715
|
+
/\.d\.ts$/
|
|
1716
|
+
];
|
|
1717
|
+
function scanDirectory(dir, files = []) {
|
|
1718
|
+
const entries = fs11.readdirSync(dir, { withFileTypes: true });
|
|
1719
|
+
for (const entry of entries) {
|
|
1720
|
+
const fullPath = path10.join(dir, entry.name);
|
|
1721
|
+
if (entry.isDirectory()) {
|
|
1722
|
+
if (EXCLUDED_DIRS.includes(entry.name)) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
scanDirectory(fullPath, files);
|
|
1726
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
1727
|
+
const shouldExclude = EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
1728
|
+
if (!shouldExclude) {
|
|
1729
|
+
files.push(fullPath);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return files;
|
|
1734
|
+
}
|
|
1735
|
+
function scanServerFiles() {
|
|
1736
|
+
const serverDir = path10.join(getProjectRoot3(), "server");
|
|
1737
|
+
if (!fs11.existsSync(serverDir)) {
|
|
1738
|
+
return [];
|
|
1739
|
+
}
|
|
1740
|
+
return scanDirectory(serverDir);
|
|
1741
|
+
}
|
|
1742
|
+
function hasCapabilityImport(filePath) {
|
|
1743
|
+
const content = fs11.readFileSync(filePath, "utf-8");
|
|
1744
|
+
return /import\s+.*from\s+['"][^'"]*capabilities[^'"]*['"]/.test(content);
|
|
1745
|
+
}
|
|
1746
|
+
function scanFilesToMigrate() {
|
|
1747
|
+
const allFiles = scanServerFiles();
|
|
1748
|
+
return allFiles.filter(hasCapabilityImport);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// src/commands/migration/versions/v001_capability/code-migrator/analyzers/import-analyzer.ts
|
|
1752
|
+
import * as ts from "typescript";
|
|
1753
|
+
function extractCapabilityId(importPath) {
|
|
1754
|
+
const match = importPath.match(/capabilities\/([^/]+)$/);
|
|
1755
|
+
if (match) {
|
|
1756
|
+
return match[1];
|
|
1757
|
+
}
|
|
1758
|
+
return null;
|
|
1759
|
+
}
|
|
1760
|
+
function isCapabilityImport(importPath) {
|
|
1761
|
+
return importPath.includes("capabilities/") || importPath.includes("capabilities\\");
|
|
1762
|
+
}
|
|
1763
|
+
function analyzeImports(sourceFile) {
|
|
1764
|
+
const imports = [];
|
|
1765
|
+
function visit(node) {
|
|
1766
|
+
if (ts.isImportDeclaration(node)) {
|
|
1767
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
1768
|
+
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
1769
|
+
const importPath = moduleSpecifier.text;
|
|
1770
|
+
if (isCapabilityImport(importPath)) {
|
|
1771
|
+
const capabilityId = extractCapabilityId(importPath);
|
|
1772
|
+
if (capabilityId && node.importClause) {
|
|
1773
|
+
const importClause = node.importClause;
|
|
1774
|
+
let importName = null;
|
|
1775
|
+
if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
|
|
1776
|
+
for (const element of importClause.namedBindings.elements) {
|
|
1777
|
+
importName = element.name.text;
|
|
1778
|
+
imports.push({
|
|
1779
|
+
importName,
|
|
1780
|
+
capabilityId,
|
|
1781
|
+
start: node.getStart(),
|
|
1782
|
+
end: node.getEnd(),
|
|
1783
|
+
text: node.getText()
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
} else if (importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings)) {
|
|
1787
|
+
importName = importClause.namedBindings.name.text;
|
|
1788
|
+
imports.push({
|
|
1789
|
+
importName,
|
|
1790
|
+
capabilityId,
|
|
1791
|
+
start: node.getStart(),
|
|
1792
|
+
end: node.getEnd(),
|
|
1793
|
+
text: node.getText()
|
|
1794
|
+
});
|
|
1795
|
+
} else if (importClause.name) {
|
|
1796
|
+
importName = importClause.name.text;
|
|
1797
|
+
imports.push({
|
|
1798
|
+
importName,
|
|
1799
|
+
capabilityId,
|
|
1800
|
+
start: node.getStart(),
|
|
1801
|
+
end: node.getEnd(),
|
|
1802
|
+
text: node.getText()
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
ts.forEachChild(node, visit);
|
|
1810
|
+
}
|
|
1811
|
+
visit(sourceFile);
|
|
1812
|
+
return imports;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/commands/migration/versions/v001_capability/code-migrator/analyzers/call-site-analyzer.ts
|
|
1816
|
+
import * as ts2 from "typescript";
|
|
1817
|
+
function analyzeCallSites(sourceFile, imports) {
|
|
1818
|
+
const callSites = [];
|
|
1819
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
1820
|
+
for (const imp of imports) {
|
|
1821
|
+
importMap.set(imp.importName, imp.capabilityId);
|
|
1822
|
+
}
|
|
1823
|
+
function visit(node) {
|
|
1824
|
+
if (ts2.isCallExpression(node)) {
|
|
1825
|
+
const expression = node.expression;
|
|
1826
|
+
if (ts2.isIdentifier(expression)) {
|
|
1827
|
+
const functionName = expression.text;
|
|
1828
|
+
const capabilityId = importMap.get(functionName);
|
|
1829
|
+
if (capabilityId) {
|
|
1830
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
1831
|
+
callSites.push({
|
|
1832
|
+
functionName,
|
|
1833
|
+
capabilityId,
|
|
1834
|
+
start: node.getStart(),
|
|
1835
|
+
end: node.getEnd(),
|
|
1836
|
+
line: line + 1,
|
|
1837
|
+
// 转为 1-based
|
|
1838
|
+
text: node.getText()
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
} else if (ts2.isPropertyAccessExpression(expression)) {
|
|
1842
|
+
const objectName = expression.expression;
|
|
1843
|
+
if (ts2.isIdentifier(objectName)) {
|
|
1844
|
+
const capabilityId = importMap.get(objectName.text);
|
|
1845
|
+
if (capabilityId) {
|
|
1846
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
1847
|
+
callSites.push({
|
|
1848
|
+
functionName: `${objectName.text}.${expression.name.text}`,
|
|
1849
|
+
capabilityId,
|
|
1850
|
+
start: node.getStart(),
|
|
1851
|
+
end: node.getEnd(),
|
|
1852
|
+
line: line + 1,
|
|
1853
|
+
text: node.getText()
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
ts2.forEachChild(node, visit);
|
|
1860
|
+
}
|
|
1861
|
+
visit(sourceFile);
|
|
1862
|
+
return callSites;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// src/commands/migration/versions/v001_capability/code-migrator/analyzers/class-analyzer.ts
|
|
1866
|
+
import * as ts3 from "typescript";
|
|
1867
|
+
function hasDecorator(node, decoratorName) {
|
|
1868
|
+
const decorators = ts3.getDecorators(node);
|
|
1869
|
+
if (!decorators) {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
return decorators.some((decorator) => {
|
|
1873
|
+
const expression = decorator.expression;
|
|
1874
|
+
if (ts3.isIdentifier(expression)) {
|
|
1875
|
+
return expression.text === decoratorName;
|
|
1876
|
+
}
|
|
1877
|
+
if (ts3.isCallExpression(expression) && ts3.isIdentifier(expression.expression)) {
|
|
1878
|
+
return expression.expression.text === decoratorName;
|
|
1879
|
+
}
|
|
1880
|
+
return false;
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
function analyzeClass(sourceFile) {
|
|
1884
|
+
let classInfo;
|
|
1885
|
+
function visit(node) {
|
|
1886
|
+
if (ts3.isClassDeclaration(node) && node.name) {
|
|
1887
|
+
const isInjectable = hasDecorator(node, "Injectable");
|
|
1888
|
+
const isController = hasDecorator(node, "Controller");
|
|
1889
|
+
if (classInfo && classInfo.isInjectable && !isInjectable && !isController) {
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
const info = {
|
|
1893
|
+
name: node.name.text,
|
|
1894
|
+
isInjectable,
|
|
1895
|
+
isController,
|
|
1896
|
+
constructorParamCount: 0
|
|
1897
|
+
};
|
|
1898
|
+
for (const member of node.members) {
|
|
1899
|
+
if (ts3.isConstructorDeclaration(member)) {
|
|
1900
|
+
info.constructorParamCount = member.parameters.length;
|
|
1901
|
+
info.constructorStart = member.getStart();
|
|
1902
|
+
info.constructorEnd = member.getEnd();
|
|
1903
|
+
if (member.parameters.length > 0) {
|
|
1904
|
+
const lastParam = member.parameters[member.parameters.length - 1];
|
|
1905
|
+
info.constructorParamsEnd = lastParam.getEnd();
|
|
1906
|
+
} else {
|
|
1907
|
+
const constructorText = member.getText();
|
|
1908
|
+
const parenIndex = constructorText.indexOf("(");
|
|
1909
|
+
if (parenIndex !== -1) {
|
|
1910
|
+
info.constructorParamsEnd = member.getStart() + parenIndex + 1;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
break;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (isInjectable || isController || !classInfo) {
|
|
1917
|
+
classInfo = info;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
ts3.forEachChild(node, visit);
|
|
1921
|
+
}
|
|
1922
|
+
visit(sourceFile);
|
|
1923
|
+
return classInfo;
|
|
1924
|
+
}
|
|
1925
|
+
function canAutoMigrate(classInfo) {
|
|
1926
|
+
if (!classInfo) {
|
|
1927
|
+
return {
|
|
1928
|
+
canMigrate: false,
|
|
1929
|
+
reason: "No class found in file"
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
if (!classInfo.isInjectable && !classInfo.isController) {
|
|
1933
|
+
return {
|
|
1934
|
+
canMigrate: false,
|
|
1935
|
+
reason: `Class "${classInfo.name}" is not @Injectable or @Controller`
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
return { canMigrate: true };
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// src/commands/migration/versions/v001_capability/code-migrator/transformers/import-transformer.ts
|
|
1942
|
+
var CAPABILITY_SERVICE_IMPORT = `import { CapabilityService, migrationAdaptor } from '@lark-apaas/fullstack-nestjs-core';`;
|
|
1943
|
+
var INJECT_IMPORT = `import { Inject } from '@nestjs/common';`;
|
|
1944
|
+
function hasCapabilityServiceImport(content) {
|
|
1945
|
+
return /import\s+.*CapabilityService.*from\s+['"]@lark-apaas\/fullstack-nestjs-core['"]/.test(content);
|
|
1946
|
+
}
|
|
1947
|
+
function hasInjectImport(content) {
|
|
1948
|
+
return /import\s+.*\bInject\b.*from\s+['"]@nestjs\/common['"]/.test(content);
|
|
1949
|
+
}
|
|
1950
|
+
function findImportInsertPosition(content) {
|
|
1951
|
+
const importRegex = /^import\s+.*?from\s+['"][^'"]+['"];?\s*$/gm;
|
|
1952
|
+
let lastMatch = null;
|
|
1953
|
+
let match;
|
|
1954
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1955
|
+
lastMatch = match;
|
|
1956
|
+
}
|
|
1957
|
+
if (lastMatch) {
|
|
1958
|
+
return lastMatch.index + lastMatch[0].length;
|
|
1959
|
+
}
|
|
1960
|
+
return 0;
|
|
1961
|
+
}
|
|
1962
|
+
function transformImports(content, imports) {
|
|
1963
|
+
const sortedImports = [...imports].sort((a, b) => b.start - a.start);
|
|
1964
|
+
let result = content;
|
|
1965
|
+
for (const imp of sortedImports) {
|
|
1966
|
+
const before = result.substring(0, imp.start);
|
|
1967
|
+
const after = result.substring(imp.end);
|
|
1968
|
+
const trimmedAfter = after.replace(/^\r?\n/, "");
|
|
1969
|
+
result = before + trimmedAfter;
|
|
1970
|
+
}
|
|
1971
|
+
let importAdded = false;
|
|
1972
|
+
if (!hasCapabilityServiceImport(result)) {
|
|
1973
|
+
const insertPos = findImportInsertPosition(result);
|
|
1974
|
+
if (insertPos > 0) {
|
|
1975
|
+
result = result.substring(0, insertPos) + "\n" + CAPABILITY_SERVICE_IMPORT + result.substring(insertPos);
|
|
1976
|
+
} else {
|
|
1977
|
+
result = CAPABILITY_SERVICE_IMPORT + "\n" + result;
|
|
1978
|
+
}
|
|
1979
|
+
importAdded = true;
|
|
1980
|
+
}
|
|
1981
|
+
if (!hasInjectImport(result)) {
|
|
1982
|
+
const insertPos = findImportInsertPosition(result);
|
|
1983
|
+
if (insertPos > 0) {
|
|
1984
|
+
result = result.substring(0, insertPos) + "\n" + INJECT_IMPORT + result.substring(insertPos);
|
|
1985
|
+
} else {
|
|
1986
|
+
result = INJECT_IMPORT + "\n" + result;
|
|
1987
|
+
}
|
|
1988
|
+
importAdded = true;
|
|
1989
|
+
}
|
|
1990
|
+
return { content: result, importAdded };
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/commands/migration/versions/v001_capability/code-migrator/transformers/injection-transformer.ts
|
|
1994
|
+
var INJECTION_PARAM = "@Inject() private readonly capabilityService: CapabilityService";
|
|
1995
|
+
function hasCapabilityServiceInjection(content) {
|
|
1996
|
+
return /capabilityService\s*:\s*CapabilityService/.test(content);
|
|
1997
|
+
}
|
|
1998
|
+
function addInjection(content, classInfo) {
|
|
1999
|
+
if (hasCapabilityServiceInjection(content)) {
|
|
2000
|
+
return { content, injectionAdded: false };
|
|
2001
|
+
}
|
|
2002
|
+
if (classInfo.constructorParamsEnd === void 0) {
|
|
2003
|
+
return { content, injectionAdded: false };
|
|
2004
|
+
}
|
|
2005
|
+
let result = content;
|
|
2006
|
+
const insertPos = classInfo.constructorParamsEnd;
|
|
2007
|
+
if (classInfo.constructorParamCount > 0) {
|
|
2008
|
+
const afterParams = content.substring(insertPos);
|
|
2009
|
+
const beforeParams = content.substring(0, insertPos);
|
|
2010
|
+
result = beforeParams + ",\n " + INJECTION_PARAM + afterParams;
|
|
2011
|
+
} else {
|
|
2012
|
+
const constructorMatch = content.match(/constructor\s*\(\s*/);
|
|
2013
|
+
if (constructorMatch && constructorMatch.index !== void 0) {
|
|
2014
|
+
const paramStart = constructorMatch.index + constructorMatch[0].length;
|
|
2015
|
+
const before = content.substring(0, paramStart);
|
|
2016
|
+
const after = content.substring(paramStart);
|
|
2017
|
+
result = before + INJECTION_PARAM + after;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return { content: result, injectionAdded: result !== content };
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// src/commands/migration/versions/v001_capability/code-migrator/transformers/call-site-transformer.ts
|
|
2024
|
+
function generateNewCall(capabilityId, originalCall) {
|
|
2025
|
+
const parenIndex = originalCall.indexOf("(");
|
|
2026
|
+
if (parenIndex === -1) {
|
|
2027
|
+
return originalCall;
|
|
2028
|
+
}
|
|
2029
|
+
const argsStart = parenIndex + 1;
|
|
2030
|
+
const argsEnd = originalCall.lastIndexOf(")");
|
|
2031
|
+
let args = "{}";
|
|
2032
|
+
if (argsEnd !== -1 && argsEnd > argsStart) {
|
|
2033
|
+
const extractedArgs = originalCall.substring(argsStart, argsEnd).trim();
|
|
2034
|
+
if (extractedArgs) {
|
|
2035
|
+
args = extractedArgs;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
return `migrationAdaptor(this.capabilityService.load('${capabilityId}').call('run', ${args}))`;
|
|
2039
|
+
}
|
|
2040
|
+
function transformCallSites(content, callSites) {
|
|
2041
|
+
const sortedCallSites = [...callSites].sort((a, b) => b.start - a.start);
|
|
2042
|
+
let result = content;
|
|
2043
|
+
let replacedCount = 0;
|
|
2044
|
+
for (const callSite of sortedCallSites) {
|
|
2045
|
+
const newCall = generateNewCall(callSite.capabilityId, callSite.text);
|
|
2046
|
+
const before = result.substring(0, callSite.start);
|
|
2047
|
+
const after = result.substring(callSite.end);
|
|
2048
|
+
result = before + newCall + after;
|
|
2049
|
+
replacedCount++;
|
|
2050
|
+
}
|
|
2051
|
+
return { content: result, replacedCount };
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/commands/migration/versions/v001_capability/code-migrator/index.ts
|
|
2055
|
+
function analyzeFile(filePath) {
|
|
2056
|
+
const content = fs12.readFileSync(filePath, "utf-8");
|
|
2057
|
+
const sourceFile = ts4.createSourceFile(
|
|
2058
|
+
filePath,
|
|
2059
|
+
content,
|
|
2060
|
+
ts4.ScriptTarget.Latest,
|
|
2061
|
+
true
|
|
2062
|
+
);
|
|
2063
|
+
const imports = analyzeImports(sourceFile);
|
|
2064
|
+
const callSites = analyzeCallSites(sourceFile, imports);
|
|
2065
|
+
const classInfo = analyzeClass(sourceFile);
|
|
2066
|
+
const { canMigrate, reason } = canAutoMigrate(classInfo);
|
|
2067
|
+
const relativePath = path11.relative(getProjectRoot3(), filePath);
|
|
2068
|
+
return {
|
|
2069
|
+
filePath: relativePath,
|
|
2070
|
+
imports,
|
|
2071
|
+
callSites,
|
|
2072
|
+
classInfo,
|
|
2073
|
+
canAutoMigrate: canMigrate,
|
|
2074
|
+
manualMigrationReason: reason
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
function migrateFile(analysis, dryRun) {
|
|
2078
|
+
const absolutePath = path11.join(getProjectRoot3(), analysis.filePath);
|
|
2079
|
+
if (!analysis.canAutoMigrate) {
|
|
2080
|
+
return {
|
|
2081
|
+
filePath: analysis.filePath,
|
|
2082
|
+
success: false,
|
|
2083
|
+
importsRemoved: 0,
|
|
2084
|
+
callSitesReplaced: 0,
|
|
2085
|
+
injectionAdded: false,
|
|
2086
|
+
error: analysis.manualMigrationReason
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
try {
|
|
2090
|
+
let content = fs12.readFileSync(absolutePath, "utf-8");
|
|
2091
|
+
const callResult = transformCallSites(content, analysis.callSites);
|
|
2092
|
+
content = callResult.content;
|
|
2093
|
+
const sourceFile = ts4.createSourceFile(
|
|
2094
|
+
analysis.filePath,
|
|
2095
|
+
content,
|
|
2096
|
+
ts4.ScriptTarget.Latest,
|
|
2097
|
+
true
|
|
2098
|
+
);
|
|
2099
|
+
const newImports = analyzeImports(sourceFile);
|
|
2100
|
+
const importResult = transformImports(content, newImports);
|
|
2101
|
+
content = importResult.content;
|
|
2102
|
+
let injectionAdded = false;
|
|
2103
|
+
if (analysis.classInfo) {
|
|
2104
|
+
const newSourceFile = ts4.createSourceFile(
|
|
2105
|
+
analysis.filePath,
|
|
2106
|
+
content,
|
|
2107
|
+
ts4.ScriptTarget.Latest,
|
|
2108
|
+
true
|
|
2109
|
+
);
|
|
2110
|
+
const newClassInfo = analyzeClass(newSourceFile);
|
|
2111
|
+
if (newClassInfo) {
|
|
2112
|
+
const injectionResult = addInjection(content, newClassInfo);
|
|
2113
|
+
content = injectionResult.content;
|
|
2114
|
+
injectionAdded = injectionResult.injectionAdded;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
if (!dryRun) {
|
|
2118
|
+
fs12.writeFileSync(absolutePath, content, "utf-8");
|
|
2119
|
+
}
|
|
2120
|
+
return {
|
|
2121
|
+
filePath: analysis.filePath,
|
|
2122
|
+
success: true,
|
|
2123
|
+
importsRemoved: analysis.imports.length,
|
|
2124
|
+
callSitesReplaced: callResult.replacedCount,
|
|
2125
|
+
injectionAdded
|
|
2126
|
+
};
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
return {
|
|
2129
|
+
filePath: analysis.filePath,
|
|
2130
|
+
success: false,
|
|
2131
|
+
importsRemoved: 0,
|
|
2132
|
+
callSitesReplaced: 0,
|
|
2133
|
+
injectionAdded: false,
|
|
2134
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
async function migrateCode(options) {
|
|
2139
|
+
const result = {
|
|
2140
|
+
autoMigrated: [],
|
|
2141
|
+
manualRequired: []
|
|
2142
|
+
};
|
|
2143
|
+
const filesToMigrate = scanFilesToMigrate();
|
|
2144
|
+
if (filesToMigrate.length === 0) {
|
|
2145
|
+
console.log(" No files need code migration.\n");
|
|
2146
|
+
return result;
|
|
2147
|
+
}
|
|
2148
|
+
const analyses = [];
|
|
2149
|
+
for (const filePath of filesToMigrate) {
|
|
2150
|
+
try {
|
|
2151
|
+
const analysis = analyzeFile(filePath);
|
|
2152
|
+
analyses.push(analysis);
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
console.error(` \u2717 Failed to analyze ${filePath}: ${error}`);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
const autoMigratable = analyses.filter((a) => a.canAutoMigrate);
|
|
2158
|
+
const manualRequired = analyses.filter((a) => !a.canAutoMigrate);
|
|
2159
|
+
for (const analysis of manualRequired) {
|
|
2160
|
+
const capabilityIds = [...new Set(analysis.imports.map((i) => i.capabilityId))];
|
|
2161
|
+
result.manualRequired.push({
|
|
2162
|
+
filePath: analysis.filePath,
|
|
2163
|
+
reason: analysis.manualMigrationReason || "Unknown reason",
|
|
2164
|
+
capabilities: capabilityIds,
|
|
2165
|
+
suggestion: getSuggestion(analysis)
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
for (const analysis of autoMigratable) {
|
|
2169
|
+
const migrationResult = migrateFile(analysis, options.dryRun);
|
|
2170
|
+
result.autoMigrated.push(migrationResult);
|
|
2171
|
+
}
|
|
2172
|
+
return result;
|
|
2173
|
+
}
|
|
2174
|
+
function getSuggestion(analysis) {
|
|
2175
|
+
if (!analysis.classInfo) {
|
|
2176
|
+
return "Consider moving capability usage to an @Injectable service";
|
|
2177
|
+
}
|
|
2178
|
+
if (!analysis.classInfo.isInjectable && !analysis.classInfo.isController) {
|
|
2179
|
+
return `Add @Injectable() decorator to class "${analysis.classInfo.name}" or move capability usage to an injectable service`;
|
|
2180
|
+
}
|
|
2181
|
+
return "Manual review required";
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/commands/migration/versions/v001_capability/cleanup.ts
|
|
2185
|
+
import fs13 from "fs";
|
|
2186
|
+
import path12 from "path";
|
|
2187
|
+
function cleanupOldFiles(capabilities, dryRun) {
|
|
2188
|
+
const deletedFiles = [];
|
|
2189
|
+
const errors = [];
|
|
2190
|
+
const capabilitiesDir = getCapabilitiesDir2();
|
|
2191
|
+
const oldJsonPath = path12.join(capabilitiesDir, "capabilities.json");
|
|
2192
|
+
if (fs13.existsSync(oldJsonPath)) {
|
|
2193
|
+
try {
|
|
2194
|
+
if (!dryRun) {
|
|
2195
|
+
fs13.unlinkSync(oldJsonPath);
|
|
2196
|
+
}
|
|
2197
|
+
deletedFiles.push("capabilities.json");
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
errors.push(`Failed to delete capabilities.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
for (const cap of capabilities) {
|
|
2203
|
+
const tsFilePath = path12.join(capabilitiesDir, `${cap.id}.ts`);
|
|
2204
|
+
if (fs13.existsSync(tsFilePath)) {
|
|
2205
|
+
try {
|
|
2206
|
+
if (!dryRun) {
|
|
2207
|
+
fs13.unlinkSync(tsFilePath);
|
|
2208
|
+
}
|
|
2209
|
+
deletedFiles.push(`${cap.id}.ts`);
|
|
2210
|
+
} catch (error) {
|
|
2211
|
+
errors.push(`Failed to delete ${cap.id}.ts: ${error instanceof Error ? error.message : String(error)}`);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return {
|
|
2216
|
+
success: errors.length === 0,
|
|
2217
|
+
deletedFiles,
|
|
2218
|
+
errors
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// src/commands/migration/versions/v001_capability/report-generator.ts
|
|
2223
|
+
import fs14 from "fs";
|
|
2224
|
+
import path13 from "path";
|
|
2225
|
+
var REPORT_FILE = "capability-migration-report.md";
|
|
2226
|
+
function printSummary(result) {
|
|
2227
|
+
const { jsonMigration, pluginInstallation, codeMigration, cleanup } = result;
|
|
2228
|
+
console.log("\u{1F4CA} Migration Summary");
|
|
2229
|
+
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");
|
|
2230
|
+
if (!jsonMigration.skipped) {
|
|
2231
|
+
console.log(` JSON files migrated: ${jsonMigration.count}`);
|
|
2232
|
+
}
|
|
2233
|
+
const totalPlugins = pluginInstallation.installed.length + pluginInstallation.alreadyInstalled.length;
|
|
2234
|
+
if (totalPlugins > 0) {
|
|
2235
|
+
console.log(` Plugins installed: ${pluginInstallation.installed.length}`);
|
|
2236
|
+
console.log(` Plugins already present: ${pluginInstallation.alreadyInstalled.length}`);
|
|
2237
|
+
if (pluginInstallation.failed.length > 0) {
|
|
2238
|
+
console.log(` Plugins failed: ${pluginInstallation.failed.length}`);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
const successfulMigrations = codeMigration.autoMigrated.filter((f) => f.success);
|
|
2242
|
+
const failedMigrations = codeMigration.autoMigrated.filter((f) => !f.success);
|
|
2243
|
+
const totalCallSites = successfulMigrations.reduce((sum, f) => sum + f.callSitesReplaced, 0);
|
|
2244
|
+
console.log(` Files auto-migrated: ${successfulMigrations.length}`);
|
|
2245
|
+
if (failedMigrations.length > 0) {
|
|
2246
|
+
console.log(` Files failed: ${failedMigrations.length}`);
|
|
2247
|
+
}
|
|
2248
|
+
if (codeMigration.manualRequired.length > 0) {
|
|
2249
|
+
console.log(` Files need manual work: ${codeMigration.manualRequired.length}`);
|
|
2250
|
+
}
|
|
2251
|
+
console.log(` Total call sites fixed: ${totalCallSites}`);
|
|
2252
|
+
if (cleanup.deletedFiles.length > 0) {
|
|
2253
|
+
console.log(` Old files cleaned up: ${cleanup.deletedFiles.length}`);
|
|
2254
|
+
}
|
|
2255
|
+
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");
|
|
2256
|
+
const hasErrors = pluginInstallation.failed.length > 0 || failedMigrations.length > 0;
|
|
2257
|
+
const hasManual = codeMigration.manualRequired.length > 0;
|
|
2258
|
+
if (hasErrors) {
|
|
2259
|
+
console.log("\u26A0\uFE0F Migration completed with errors!\n");
|
|
2260
|
+
} else if (hasManual) {
|
|
2261
|
+
console.log("\u2705 Migration completed! (some files need manual work)\n");
|
|
2262
|
+
} else {
|
|
2263
|
+
console.log("\u2705 Migration completed!\n");
|
|
2264
|
+
}
|
|
2265
|
+
console.log("\u{1F4DD} Next steps:");
|
|
2266
|
+
console.log(" 1. Review changes: git diff");
|
|
2267
|
+
console.log(" 2. Run TypeScript check: npm run typecheck");
|
|
2268
|
+
console.log(" 3. Run tests: npm test");
|
|
2269
|
+
if (hasManual) {
|
|
2270
|
+
console.log(" 4. Handle manual migrations (see report)");
|
|
2271
|
+
}
|
|
2272
|
+
console.log("");
|
|
2273
|
+
}
|
|
2274
|
+
async function generateReport(result) {
|
|
2275
|
+
const { jsonMigration, pluginInstallation, codeMigration, cleanup } = result;
|
|
2276
|
+
const lines = [];
|
|
2277
|
+
lines.push("# Capability \u8FC1\u79FB\u62A5\u544A");
|
|
2278
|
+
lines.push("");
|
|
2279
|
+
lines.push(`\u751F\u6210\u65F6\u95F4: ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
|
|
2280
|
+
lines.push("");
|
|
2281
|
+
lines.push("## 1. \u914D\u7F6E\u8FC1\u79FB");
|
|
2282
|
+
lines.push("");
|
|
2283
|
+
if (jsonMigration.skipped) {
|
|
2284
|
+
lines.push(`\u8DF3\u8FC7: ${jsonMigration.reason}`);
|
|
2285
|
+
} else {
|
|
2286
|
+
lines.push("### 1.1 \u5DF2\u8FC1\u79FB\u7684 Capabilities");
|
|
2287
|
+
lines.push("");
|
|
2288
|
+
lines.push("| ID | pluginKey |");
|
|
2289
|
+
lines.push("|----|-----------|");
|
|
2290
|
+
for (const cap of jsonMigration.capabilities) {
|
|
2291
|
+
lines.push(`| ${cap.id} | ${cap.pluginKey} |`);
|
|
2292
|
+
}
|
|
2293
|
+
lines.push("");
|
|
2294
|
+
}
|
|
2295
|
+
lines.push("## 2. \u63D2\u4EF6\u5B89\u88C5");
|
|
2296
|
+
lines.push("");
|
|
2297
|
+
if (pluginInstallation.installed.length === 0 && pluginInstallation.alreadyInstalled.length === 0) {
|
|
2298
|
+
lines.push("\u65E0\u63D2\u4EF6\u9700\u8981\u5B89\u88C5\u3002");
|
|
2299
|
+
} else {
|
|
2300
|
+
lines.push("| pluginKey | \u72B6\u6001 |");
|
|
2301
|
+
lines.push("|-----------|------|");
|
|
2302
|
+
for (const pluginKey of pluginInstallation.alreadyInstalled) {
|
|
2303
|
+
lines.push(`| ${pluginKey} | \u5DF2\u5B89\u88C5 |`);
|
|
2304
|
+
}
|
|
2305
|
+
for (const pluginKey of pluginInstallation.installed) {
|
|
2306
|
+
lines.push(`| ${pluginKey} | \u65B0\u5B89\u88C5 |`);
|
|
2307
|
+
}
|
|
2308
|
+
for (const failed of pluginInstallation.failed) {
|
|
2309
|
+
lines.push(`| ${failed.pluginKey} | \u274C \u5931\u8D25: ${failed.error} |`);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
lines.push("");
|
|
2313
|
+
lines.push("## 3. \u4EE3\u7801\u8FC1\u79FB");
|
|
2314
|
+
lines.push("");
|
|
2315
|
+
const successfulMigrations = codeMigration.autoMigrated.filter((f) => f.success);
|
|
2316
|
+
const failedMigrations = codeMigration.autoMigrated.filter((f) => !f.success);
|
|
2317
|
+
if (successfulMigrations.length > 0) {
|
|
2318
|
+
lines.push(`### 3.1 \u81EA\u52A8\u8FC1\u79FB\u7684\u6587\u4EF6 (${successfulMigrations.length} \u4E2A)`);
|
|
2319
|
+
lines.push("");
|
|
2320
|
+
for (const file of successfulMigrations) {
|
|
2321
|
+
lines.push(`#### ${file.filePath}`);
|
|
2322
|
+
lines.push("");
|
|
2323
|
+
lines.push("**\u53D8\u66F4\uFF1A**");
|
|
2324
|
+
lines.push(`- \u5220\u9664 ${file.importsRemoved} \u4E2A capability import`);
|
|
2325
|
+
if (file.injectionAdded) {
|
|
2326
|
+
lines.push("- \u6DFB\u52A0 CapabilityService \u4F9D\u8D56\u6CE8\u5165");
|
|
2327
|
+
}
|
|
2328
|
+
lines.push(`- \u66FF\u6362 ${file.callSitesReplaced} \u4E2A\u8C03\u7528\u70B9`);
|
|
2329
|
+
lines.push("");
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
if (failedMigrations.length > 0) {
|
|
2333
|
+
lines.push(`### 3.2 \u8FC1\u79FB\u5931\u8D25\u7684\u6587\u4EF6 (${failedMigrations.length} \u4E2A)`);
|
|
2334
|
+
lines.push("");
|
|
2335
|
+
for (const file of failedMigrations) {
|
|
2336
|
+
lines.push(`#### ${file.filePath}`);
|
|
2337
|
+
lines.push("");
|
|
2338
|
+
lines.push(`**\u9519\u8BEF\uFF1A** ${file.error}`);
|
|
2339
|
+
lines.push("");
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
if (codeMigration.manualRequired.length > 0) {
|
|
2343
|
+
lines.push(`### 3.3 \u9700\u8981\u624B\u52A8\u8FC1\u79FB\u7684\u6587\u4EF6 (${codeMigration.manualRequired.length} \u4E2A)`);
|
|
2344
|
+
lines.push("");
|
|
2345
|
+
for (const item of codeMigration.manualRequired) {
|
|
2346
|
+
lines.push(`#### ${item.filePath}`);
|
|
2347
|
+
lines.push("");
|
|
2348
|
+
lines.push(`- **\u539F\u56E0\uFF1A** ${item.reason}`);
|
|
2349
|
+
lines.push(`- **\u6D89\u53CA\u7684 Capabilities\uFF1A** ${item.capabilities.join(", ")}`);
|
|
2350
|
+
lines.push(`- **\u5EFA\u8BAE\uFF1A** ${item.suggestion}`);
|
|
2351
|
+
lines.push("");
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
lines.push("## 4. \u6E05\u7406\u8001\u6587\u4EF6");
|
|
2355
|
+
lines.push("");
|
|
2356
|
+
if (cleanup.deletedFiles.length === 0 && cleanup.errors.length === 0) {
|
|
2357
|
+
lines.push("\u65E0\u6587\u4EF6\u9700\u8981\u6E05\u7406\u3002");
|
|
2358
|
+
} else {
|
|
2359
|
+
if (cleanup.deletedFiles.length > 0) {
|
|
2360
|
+
lines.push("### 4.1 \u5DF2\u5220\u9664\u7684\u6587\u4EF6");
|
|
2361
|
+
lines.push("");
|
|
2362
|
+
for (const file of cleanup.deletedFiles) {
|
|
2363
|
+
lines.push(`- \`${file}\``);
|
|
2364
|
+
}
|
|
2365
|
+
lines.push("");
|
|
51
2366
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
2367
|
+
if (cleanup.errors.length > 0) {
|
|
2368
|
+
lines.push("### 4.2 \u6E05\u7406\u5931\u8D25");
|
|
2369
|
+
lines.push("");
|
|
2370
|
+
for (const err of cleanup.errors) {
|
|
2371
|
+
lines.push(`- \u274C ${err}`);
|
|
2372
|
+
}
|
|
2373
|
+
lines.push("");
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
lines.push("");
|
|
2377
|
+
lines.push("## 5. \u9A8C\u8BC1\u6E05\u5355");
|
|
2378
|
+
lines.push("");
|
|
2379
|
+
lines.push("- [ ] \u8FD0\u884C `git diff` \u68C0\u67E5\u4EE3\u7801\u53D8\u66F4");
|
|
2380
|
+
lines.push("- [ ] \u8FD0\u884C `npm run typecheck` \u68C0\u67E5\u7C7B\u578B");
|
|
2381
|
+
lines.push("- [ ] \u8FD0\u884C `npm test` \u9A8C\u8BC1\u529F\u80FD");
|
|
2382
|
+
if (codeMigration.manualRequired.length > 0) {
|
|
2383
|
+
lines.push("- [ ] \u5904\u7406\u624B\u52A8\u8FC1\u79FB\u9879");
|
|
2384
|
+
}
|
|
2385
|
+
lines.push("");
|
|
2386
|
+
const reportPath = path13.join(getProjectRoot3(), REPORT_FILE);
|
|
2387
|
+
fs14.writeFileSync(reportPath, lines.join("\n"), "utf-8");
|
|
2388
|
+
console.log(`\u{1F4C4} Report generated: ${REPORT_FILE}`);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// src/commands/migration/versions/v001_capability/migration-runner.ts
|
|
2392
|
+
async function runCapabilityMigration(options = {}) {
|
|
2393
|
+
const fullOptions = {
|
|
2394
|
+
dryRun: options.dryRun ?? false
|
|
2395
|
+
};
|
|
2396
|
+
console.log("\u{1F50D} Analyzing project...\n");
|
|
2397
|
+
if (fullOptions.dryRun) {
|
|
2398
|
+
console.log("\u{1F4CB} Running in dry-run mode (no files will be modified)\n");
|
|
2399
|
+
}
|
|
2400
|
+
let jsonResult = {
|
|
2401
|
+
success: false,
|
|
2402
|
+
skipped: true,
|
|
2403
|
+
reason: "Not executed",
|
|
2404
|
+
count: 0,
|
|
2405
|
+
capabilities: []
|
|
2406
|
+
};
|
|
2407
|
+
let pluginResult = {
|
|
2408
|
+
installed: [],
|
|
2409
|
+
alreadyInstalled: [],
|
|
2410
|
+
failed: []
|
|
2411
|
+
};
|
|
2412
|
+
let codeResult = {
|
|
2413
|
+
autoMigrated: [],
|
|
2414
|
+
manualRequired: []
|
|
2415
|
+
};
|
|
2416
|
+
let cleanupResult = {
|
|
2417
|
+
success: true,
|
|
2418
|
+
deletedFiles: [],
|
|
2419
|
+
errors: []
|
|
2420
|
+
};
|
|
2421
|
+
console.log("\u{1F4C1} Step 1: JSON File Migration");
|
|
2422
|
+
try {
|
|
2423
|
+
jsonResult = await migrateJsonFiles(fullOptions);
|
|
2424
|
+
if (jsonResult.skipped) {
|
|
2425
|
+
console.log(` \u25CB Skipped: ${jsonResult.reason}
|
|
2426
|
+
`);
|
|
2427
|
+
} else if (jsonResult.success) {
|
|
2428
|
+
console.log(` \u2713 Created ${jsonResult.count} capability files
|
|
2429
|
+
`);
|
|
2430
|
+
} else {
|
|
2431
|
+
console.log(` \u2717 Failed: ${jsonResult.reason}
|
|
2432
|
+
`);
|
|
2433
|
+
return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
|
|
2434
|
+
}
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2437
|
+
console.log(` \u2717 Error: ${errorMsg}
|
|
2438
|
+
`);
|
|
2439
|
+
jsonResult = {
|
|
2440
|
+
success: false,
|
|
2441
|
+
skipped: false,
|
|
2442
|
+
reason: errorMsg,
|
|
2443
|
+
count: 0,
|
|
2444
|
+
capabilities: []
|
|
2445
|
+
};
|
|
2446
|
+
return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
|
|
2447
|
+
}
|
|
2448
|
+
if (jsonResult.skipped) {
|
|
2449
|
+
return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
|
|
2450
|
+
}
|
|
2451
|
+
console.log("\u{1F4E6} Step 2: Plugin Installation");
|
|
2452
|
+
try {
|
|
2453
|
+
pluginResult = await installPlugins(jsonResult.capabilities, fullOptions);
|
|
2454
|
+
if (pluginResult.alreadyInstalled.length > 0) {
|
|
2455
|
+
console.log(` \u2713 Already installed: ${pluginResult.alreadyInstalled.join(", ")}`);
|
|
2456
|
+
}
|
|
2457
|
+
if (pluginResult.installed.length > 0) {
|
|
2458
|
+
console.log(` \u2713 Installed: ${pluginResult.installed.join(", ")}`);
|
|
2459
|
+
}
|
|
2460
|
+
if (pluginResult.failed.length > 0) {
|
|
2461
|
+
console.log(` \u26A0 Failed (will continue): ${pluginResult.failed.map((f) => f.pluginKey).join(", ")}`);
|
|
2462
|
+
for (const f of pluginResult.failed) {
|
|
2463
|
+
console.log(` - ${f.pluginKey}: ${f.error}`);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
console.log("");
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2469
|
+
console.log(` \u26A0 Error (will continue): ${errorMsg}
|
|
2470
|
+
`);
|
|
2471
|
+
pluginResult = {
|
|
2472
|
+
installed: [],
|
|
2473
|
+
alreadyInstalled: [],
|
|
2474
|
+
failed: [{ pluginKey: "unknown", error: errorMsg }]
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
console.log("\u{1F527} Step 3: Code Migration");
|
|
2478
|
+
console.log(" Scanning server/ directory...\n");
|
|
2479
|
+
try {
|
|
2480
|
+
codeResult = await migrateCode(fullOptions);
|
|
2481
|
+
let hasCodeError = false;
|
|
2482
|
+
for (const file of codeResult.autoMigrated) {
|
|
2483
|
+
if (file.success) {
|
|
2484
|
+
console.log(` \u2713 ${file.filePath}`);
|
|
2485
|
+
console.log(` - Removed ${file.importsRemoved} capability imports`);
|
|
2486
|
+
if (file.injectionAdded) {
|
|
2487
|
+
console.log(` - Added CapabilityService injection`);
|
|
2488
|
+
}
|
|
2489
|
+
console.log(` - Replaced ${file.callSitesReplaced} call sites`);
|
|
2490
|
+
} else {
|
|
2491
|
+
console.log(` \u2717 ${file.filePath}: ${file.error}`);
|
|
2492
|
+
hasCodeError = true;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
if (codeResult.manualRequired.length > 0) {
|
|
2496
|
+
console.log("");
|
|
2497
|
+
for (const item of codeResult.manualRequired) {
|
|
2498
|
+
console.log(` \u26A0 ${item.filePath} (manual migration required)`);
|
|
2499
|
+
console.log(` - Reason: ${item.reason}`);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
console.log("");
|
|
2503
|
+
if (hasCodeError) {
|
|
2504
|
+
console.log(" \u2717 Code migration failed, aborting...\n");
|
|
2505
|
+
return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
|
|
2506
|
+
}
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2509
|
+
console.log(` \u2717 Error: ${errorMsg}
|
|
2510
|
+
`);
|
|
2511
|
+
codeResult = {
|
|
2512
|
+
autoMigrated: [],
|
|
2513
|
+
manualRequired: []
|
|
2514
|
+
};
|
|
2515
|
+
return buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
|
|
2516
|
+
}
|
|
2517
|
+
console.log("\u{1F9F9} Step 4: Cleanup Old Files");
|
|
2518
|
+
try {
|
|
2519
|
+
cleanupResult = cleanupOldFiles(jsonResult.capabilities, fullOptions.dryRun);
|
|
2520
|
+
if (cleanupResult.deletedFiles.length > 0) {
|
|
2521
|
+
for (const file of cleanupResult.deletedFiles) {
|
|
2522
|
+
console.log(` \u2713 Deleted: ${file}`);
|
|
2523
|
+
}
|
|
2524
|
+
} else {
|
|
2525
|
+
console.log(" \u25CB No files to clean up");
|
|
2526
|
+
}
|
|
2527
|
+
if (cleanupResult.errors.length > 0) {
|
|
2528
|
+
for (const err of cleanupResult.errors) {
|
|
2529
|
+
console.log(` \u26A0 ${err}`);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
console.log("");
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2535
|
+
console.log(` \u26A0 Error: ${errorMsg}
|
|
2536
|
+
`);
|
|
2537
|
+
cleanupResult = {
|
|
2538
|
+
success: false,
|
|
2539
|
+
deletedFiles: [],
|
|
2540
|
+
errors: [errorMsg]
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
const result = buildResult(jsonResult, pluginResult, codeResult, cleanupResult);
|
|
2544
|
+
printSummary(result);
|
|
2545
|
+
if (!fullOptions.dryRun) {
|
|
2546
|
+
await generateReport(result);
|
|
2547
|
+
}
|
|
2548
|
+
return result;
|
|
2549
|
+
}
|
|
2550
|
+
function buildResult(jsonMigration, pluginInstallation, codeMigration, cleanup) {
|
|
2551
|
+
return {
|
|
2552
|
+
jsonMigration,
|
|
2553
|
+
pluginInstallation,
|
|
2554
|
+
codeMigration,
|
|
2555
|
+
cleanup
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// src/commands/migration/versions/v001_capability/run.ts
|
|
2560
|
+
async function run3(options) {
|
|
2561
|
+
try {
|
|
2562
|
+
const migrationOptions = {
|
|
2563
|
+
dryRun: options.dryRun ?? false
|
|
2564
|
+
};
|
|
2565
|
+
const result = await runCapabilityMigration(migrationOptions);
|
|
2566
|
+
const { jsonMigration, pluginInstallation, codeMigration, cleanup } = result;
|
|
2567
|
+
const jsonFailed = !jsonMigration.success && !jsonMigration.skipped;
|
|
2568
|
+
const codeFailed = codeMigration.autoMigrated.some((f) => !f.success);
|
|
2569
|
+
if (jsonFailed) {
|
|
2570
|
+
return {
|
|
2571
|
+
success: false,
|
|
2572
|
+
message: jsonMigration.reason || "JSON migration failed",
|
|
2573
|
+
error: new Error(jsonMigration.reason || "Unknown error")
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
if (codeFailed) {
|
|
2577
|
+
return {
|
|
2578
|
+
success: false,
|
|
2579
|
+
message: "Code migration failed",
|
|
2580
|
+
error: new Error("Some files failed to migrate")
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
const messages = [];
|
|
2584
|
+
if (jsonMigration.success) {
|
|
2585
|
+
messages.push(`${jsonMigration.count} capabilities migrated`);
|
|
2586
|
+
}
|
|
2587
|
+
if (pluginInstallation.installed.length > 0) {
|
|
2588
|
+
messages.push(`${pluginInstallation.installed.length} plugins installed`);
|
|
2589
|
+
}
|
|
2590
|
+
if (codeMigration.autoMigrated.length > 0) {
|
|
2591
|
+
const successCount = codeMigration.autoMigrated.filter((f) => f.success).length;
|
|
2592
|
+
if (successCount > 0) {
|
|
2593
|
+
messages.push(`${successCount} files auto-migrated`);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
if (cleanup.deletedFiles.length > 0) {
|
|
2597
|
+
messages.push(`${cleanup.deletedFiles.length} old files cleaned up`);
|
|
2598
|
+
}
|
|
2599
|
+
if (codeMigration.manualRequired.length > 0) {
|
|
2600
|
+
messages.push(`${codeMigration.manualRequired.length} files need manual migration`);
|
|
2601
|
+
}
|
|
2602
|
+
if (pluginInstallation.failed.length > 0) {
|
|
2603
|
+
messages.push(`${pluginInstallation.failed.length} plugins failed to install`);
|
|
2604
|
+
}
|
|
2605
|
+
return {
|
|
2606
|
+
success: true,
|
|
2607
|
+
message: messages.join(", ") || "No migration needed"
|
|
2608
|
+
};
|
|
2609
|
+
} catch (error) {
|
|
2610
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2611
|
+
return {
|
|
2612
|
+
success: false,
|
|
2613
|
+
message: err.message,
|
|
2614
|
+
error: err
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// src/commands/migration/versions/v001_capability/index.ts
|
|
2620
|
+
var v001CapabilityMigration = {
|
|
2621
|
+
version: 1,
|
|
2622
|
+
name: "capability",
|
|
2623
|
+
description: "Migrate capability configurations from old format (capabilities.json array) to new format (individual JSON files)",
|
|
2624
|
+
check,
|
|
2625
|
+
run: run3
|
|
2626
|
+
};
|
|
2627
|
+
|
|
2628
|
+
// src/commands/migration/versions/index.ts
|
|
2629
|
+
var versionedMigrations = [
|
|
2630
|
+
v001CapabilityMigration
|
|
2631
|
+
// 未来新增的迁移脚本在此添加,如:
|
|
2632
|
+
// v002ConfigMigration,
|
|
2633
|
+
// v003RoutesMigration,
|
|
2634
|
+
];
|
|
2635
|
+
function getPendingMigrations(currentVersion) {
|
|
2636
|
+
return versionedMigrations.filter((m) => m.version > currentVersion).sort((a, b) => a.version - b.version);
|
|
2637
|
+
}
|
|
2638
|
+
function getLatestVersion() {
|
|
2639
|
+
if (versionedMigrations.length === 0) return 0;
|
|
2640
|
+
return Math.max(...versionedMigrations.map((m) => m.version));
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// src/commands/migration/runner.ts
|
|
2644
|
+
function log(message) {
|
|
2645
|
+
console.log(`[migration] ${message}`);
|
|
2646
|
+
}
|
|
2647
|
+
async function executeSingle(migration, options) {
|
|
2648
|
+
const { version, name } = migration;
|
|
2649
|
+
const versionTag = `v${version}`;
|
|
2650
|
+
try {
|
|
2651
|
+
log(`Checking [${versionTag}] ${name}...`);
|
|
2652
|
+
const checkResult = await migration.check(options);
|
|
2653
|
+
if (!checkResult.needsMigration) {
|
|
2654
|
+
log(`[${versionTag}] ${name}: no migration needed`);
|
|
2655
|
+
return {
|
|
2656
|
+
name,
|
|
2657
|
+
status: "skipped",
|
|
2658
|
+
message: checkResult.message || "No migration needed"
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
log(`[${versionTag}] ${name}: needs migration (${checkResult.message})`);
|
|
2662
|
+
if (checkResult.items && checkResult.items.length > 0) {
|
|
2663
|
+
for (const item of checkResult.items) {
|
|
2664
|
+
console.log(` - ${item}`);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
if (options.dryRun) {
|
|
2668
|
+
return {
|
|
2669
|
+
name,
|
|
2670
|
+
status: "skipped",
|
|
2671
|
+
message: `Dry-run: ${checkResult.message}`
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
log(`Running [${versionTag}] ${name} migration...`);
|
|
2675
|
+
const runResult = await migration.run(options);
|
|
2676
|
+
if (runResult.success) {
|
|
2677
|
+
log(`\u2713 [${versionTag}] ${name}: completed (${runResult.message})`);
|
|
2678
|
+
return {
|
|
2679
|
+
name,
|
|
2680
|
+
status: "completed",
|
|
2681
|
+
message: runResult.message
|
|
2682
|
+
};
|
|
2683
|
+
} else {
|
|
2684
|
+
log(`\u2717 [${versionTag}] ${name}: failed`);
|
|
2685
|
+
if (runResult.error) {
|
|
2686
|
+
console.log(` Error: ${runResult.error.message}`);
|
|
2687
|
+
}
|
|
2688
|
+
return {
|
|
2689
|
+
name,
|
|
2690
|
+
status: "failed",
|
|
2691
|
+
message: runResult.message,
|
|
2692
|
+
error: runResult.error
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
} catch (error) {
|
|
2696
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2697
|
+
log(`\u2717 [${versionTag}] ${name}: failed`);
|
|
2698
|
+
console.log(` Error: ${err.message}`);
|
|
2699
|
+
return {
|
|
2700
|
+
name,
|
|
2701
|
+
status: "failed",
|
|
2702
|
+
message: err.message,
|
|
2703
|
+
error: err
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
function printSummary2(summary, dryRun) {
|
|
2708
|
+
console.log("");
|
|
2709
|
+
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");
|
|
2710
|
+
console.log(dryRun ? "Dry-run Summary:" : "Migration Summary:");
|
|
2711
|
+
if (dryRun) {
|
|
2712
|
+
const needsMigration = summary.skipped.filter((s) => s.message.startsWith("Dry-run:")).length;
|
|
2713
|
+
const noAction = summary.skipped.filter((s) => !s.message.startsWith("Dry-run:")).length;
|
|
2714
|
+
console.log(` Needs migration: ${needsMigration}`);
|
|
2715
|
+
console.log(` No action needed: ${noAction}`);
|
|
2716
|
+
} else {
|
|
2717
|
+
console.log(` \u2713 Completed: ${summary.completed.length}`);
|
|
2718
|
+
console.log(` \u25CB Skipped: ${summary.skipped.length} (no migration needed)`);
|
|
2719
|
+
console.log(` \u2717 Failed: ${summary.failed.length}`);
|
|
2720
|
+
}
|
|
2721
|
+
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");
|
|
2722
|
+
}
|
|
2723
|
+
async function runMigrations(options) {
|
|
2724
|
+
const currentVersion = getCurrentVersion();
|
|
2725
|
+
const latestVersion = getLatestVersion();
|
|
2726
|
+
console.log("");
|
|
2727
|
+
log(`Current version: ${currentVersion || "0 (not set)"}`);
|
|
2728
|
+
if (currentVersion >= latestVersion) {
|
|
2729
|
+
log("All migrations are up to date.");
|
|
2730
|
+
return {
|
|
2731
|
+
needsMigration: false,
|
|
2732
|
+
completed: [],
|
|
2733
|
+
skipped: [],
|
|
2734
|
+
failed: []
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
const pendingMigrations = getPendingMigrations(currentVersion);
|
|
2738
|
+
if (options.dryRun) {
|
|
2739
|
+
log("Dry-run mode enabled, no changes will be made.");
|
|
2740
|
+
} else {
|
|
2741
|
+
log("Applying migrations...");
|
|
2742
|
+
}
|
|
2743
|
+
console.log("");
|
|
2744
|
+
const summary = {
|
|
2745
|
+
needsMigration: false,
|
|
2746
|
+
completed: [],
|
|
2747
|
+
skipped: [],
|
|
2748
|
+
failed: []
|
|
2749
|
+
};
|
|
2750
|
+
let hasAnyMigration = false;
|
|
2751
|
+
for (const migration of pendingMigrations) {
|
|
2752
|
+
const result = await executeSingle(migration, options);
|
|
2753
|
+
switch (result.status) {
|
|
2754
|
+
case "completed":
|
|
2755
|
+
summary.completed.push(result);
|
|
2756
|
+
hasAnyMigration = true;
|
|
2757
|
+
if (!options.dryRun) {
|
|
2758
|
+
setCurrentVersion(migration.version);
|
|
2759
|
+
}
|
|
2760
|
+
break;
|
|
2761
|
+
case "skipped":
|
|
2762
|
+
summary.skipped.push(result);
|
|
2763
|
+
if (result.message.startsWith("Dry-run:")) {
|
|
2764
|
+
hasAnyMigration = true;
|
|
2765
|
+
}
|
|
2766
|
+
break;
|
|
2767
|
+
case "failed":
|
|
2768
|
+
summary.failed.push(result);
|
|
2769
|
+
hasAnyMigration = true;
|
|
2770
|
+
log("Migration aborted due to failure.");
|
|
2771
|
+
printSummary2(summary, !!options.dryRun);
|
|
2772
|
+
return summary;
|
|
2773
|
+
}
|
|
2774
|
+
console.log("");
|
|
2775
|
+
}
|
|
2776
|
+
summary.needsMigration = hasAnyMigration;
|
|
2777
|
+
if (currentVersion === 0 && !hasAnyMigration && !options.dryRun) {
|
|
2778
|
+
log(`All migrations skipped. Project is up to date.`);
|
|
2779
|
+
log(`Updated migrationVersion to ${latestVersion}.`);
|
|
2780
|
+
setCurrentVersion(latestVersion);
|
|
2781
|
+
} else if (!options.dryRun && summary.completed.length > 0) {
|
|
2782
|
+
log(`Migration complete. Updated to version ${getCurrentVersion()}.`);
|
|
2783
|
+
}
|
|
2784
|
+
printSummary2(summary, !!options.dryRun);
|
|
2785
|
+
return summary;
|
|
2786
|
+
}
|
|
2787
|
+
async function checkMigrations(options) {
|
|
2788
|
+
const currentVersion = getCurrentVersion();
|
|
2789
|
+
const latestVersion = getLatestVersion();
|
|
2790
|
+
console.log("");
|
|
2791
|
+
log(`Current version: ${currentVersion || "0 (not set)"}`);
|
|
2792
|
+
if (currentVersion >= latestVersion) {
|
|
2793
|
+
log("All migrations are up to date.");
|
|
2794
|
+
return {
|
|
2795
|
+
needsMigration: false,
|
|
2796
|
+
completed: [],
|
|
2797
|
+
skipped: [],
|
|
2798
|
+
failed: []
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
log("Checking pending migrations...");
|
|
2802
|
+
console.log("");
|
|
2803
|
+
const pendingMigrations = getPendingMigrations(currentVersion);
|
|
2804
|
+
const summary = {
|
|
2805
|
+
needsMigration: false,
|
|
2806
|
+
completed: [],
|
|
2807
|
+
skipped: [],
|
|
2808
|
+
failed: []
|
|
2809
|
+
};
|
|
2810
|
+
let needsCount = 0;
|
|
2811
|
+
let noActionCount = 0;
|
|
2812
|
+
for (const migration of pendingMigrations) {
|
|
2813
|
+
const { version, name } = migration;
|
|
2814
|
+
const versionTag = `v${version}`;
|
|
2815
|
+
try {
|
|
2816
|
+
const checkResult = await migration.check(options);
|
|
2817
|
+
if (checkResult.needsMigration) {
|
|
2818
|
+
needsCount++;
|
|
2819
|
+
console.log(` ${versionTag} (${name}): needs migration`);
|
|
2820
|
+
if (checkResult.items && checkResult.items.length > 0) {
|
|
2821
|
+
for (const item of checkResult.items) {
|
|
2822
|
+
console.log(` - ${item}`);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
summary.skipped.push({
|
|
2826
|
+
name,
|
|
2827
|
+
status: "skipped",
|
|
2828
|
+
message: `Needs migration: ${checkResult.message}`
|
|
2829
|
+
});
|
|
2830
|
+
} else {
|
|
2831
|
+
noActionCount++;
|
|
2832
|
+
console.log(` ${versionTag} (${name}): no migration needed`);
|
|
2833
|
+
summary.skipped.push({
|
|
2834
|
+
name,
|
|
2835
|
+
status: "skipped",
|
|
2836
|
+
message: checkResult.message || "No migration needed"
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
} catch (error) {
|
|
2840
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2841
|
+
console.log(` ${versionTag} (${name}): check failed`);
|
|
2842
|
+
console.log(` Error: ${err.message}`);
|
|
2843
|
+
summary.failed.push({
|
|
2844
|
+
name,
|
|
2845
|
+
status: "failed",
|
|
2846
|
+
message: err.message,
|
|
2847
|
+
error: err
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
console.log("");
|
|
2852
|
+
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");
|
|
2853
|
+
console.log("Check Summary:");
|
|
2854
|
+
console.log(` Needs migration: ${needsCount}`);
|
|
2855
|
+
console.log(` No action needed: ${noActionCount}`);
|
|
2856
|
+
if (summary.failed.length > 0) {
|
|
2857
|
+
console.log(` Check failed: ${summary.failed.length}`);
|
|
2858
|
+
}
|
|
2859
|
+
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");
|
|
2860
|
+
if (needsCount > 0) {
|
|
2861
|
+
console.log("");
|
|
2862
|
+
log("Run 'fullstack-cli migration' to apply.");
|
|
2863
|
+
}
|
|
2864
|
+
summary.needsMigration = needsCount > 0;
|
|
2865
|
+
if (needsCount === 0 && summary.failed.length === 0) {
|
|
2866
|
+
setCurrentVersion(latestVersion);
|
|
2867
|
+
console.log("");
|
|
2868
|
+
log(`Updated migrationVersion to ${latestVersion}.`);
|
|
2869
|
+
}
|
|
2870
|
+
return summary;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/commands/migration/index.ts
|
|
2874
|
+
var migrationCommand = {
|
|
2875
|
+
name: "migration",
|
|
2876
|
+
description: "Run versioned migration scripts",
|
|
2877
|
+
register(program) {
|
|
2878
|
+
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) => {
|
|
2879
|
+
const summary = await runMigrations(options);
|
|
2880
|
+
if (summary.failed.length > 0) {
|
|
2881
|
+
process.exit(1);
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
migrationCmd.command("check").description("Check pending migrations without applying them").action(async () => {
|
|
2885
|
+
const summary = await checkMigrations({});
|
|
2886
|
+
if (summary.failed.length > 0) {
|
|
2887
|
+
process.exit(2);
|
|
2888
|
+
} else if (summary.needsMigration) {
|
|
2889
|
+
process.exit(1);
|
|
2890
|
+
}
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
|
|
2895
|
+
// src/commands/index.ts
|
|
2896
|
+
var commands = [
|
|
2897
|
+
genDbSchemaCommand,
|
|
2898
|
+
syncCommand,
|
|
2899
|
+
actionPluginCommandGroup,
|
|
2900
|
+
capabilityCommandGroup,
|
|
2901
|
+
migrationCommand
|
|
2902
|
+
];
|
|
2903
|
+
|
|
2904
|
+
// src/index.ts
|
|
2905
|
+
var envPath = path14.join(process.cwd(), ".env");
|
|
2906
|
+
if (fs15.existsSync(envPath)) {
|
|
2907
|
+
dotenvConfig({ path: envPath });
|
|
2908
|
+
}
|
|
2909
|
+
var __dirname = path14.dirname(fileURLToPath3(import.meta.url));
|
|
2910
|
+
var pkg = JSON.parse(fs15.readFileSync(path14.join(__dirname, "../package.json"), "utf-8"));
|
|
2911
|
+
var cli = new FullstackCLI(pkg.version);
|
|
2912
|
+
cli.useAll(commands);
|
|
2913
|
+
cli.run();
|