@lark-apaas/fullstack-cli 1.1.16-beta.0 → 1.1.16-beta.2

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.
@@ -38,6 +38,60 @@ export const fileAttachment = customType<{
38
38
  },
39
39
  });
40
40
 
41
+ /** Escape single quotes in SQL string literals */
42
+ function escapeLiteral(str: string): string {
43
+ return `'${str.replace(/'/g, "''")}'`;
44
+ }
45
+
46
+ export const userProfileArray = customType<{
47
+ data: string[];
48
+ driverData: string;
49
+ }>({
50
+ dataType() {
51
+ return 'user_profile[]';
52
+ },
53
+ toDriver(value: string[]) {
54
+ if (!value || value.length === 0) {
55
+ return sql`'{}'::user_profile[]`;
56
+ }
57
+ const elements = value.map(id => `ROW(${escapeLiteral(id)})::user_profile`).join(',');
58
+ return sql.raw(`ARRAY[${elements}]::user_profile[]`);
59
+ },
60
+ fromDriver(value: string): string[] {
61
+ if (!value || value === '{}') return [];
62
+ const inner = value.slice(1, -1);
63
+ const matches = inner.match(/\([^)]*\)/g) || [];
64
+ return matches.map(m => m.slice(1, -1).split(',')[0].trim());
65
+ },
66
+ });
67
+
68
+ export const fileAttachmentArray = customType<{
69
+ data: FileAttachment[];
70
+ driverData: string;
71
+ }>({
72
+ dataType() {
73
+ return 'file_attachment[]';
74
+ },
75
+ toDriver(value: FileAttachment[]) {
76
+ if (!value || value.length === 0) {
77
+ return sql`'{}'::file_attachment[]`;
78
+ }
79
+ const elements = value.map(f =>
80
+ `ROW(${escapeLiteral(f.bucket_id)},${escapeLiteral(f.file_path)})::file_attachment`
81
+ ).join(',');
82
+ return sql.raw(`ARRAY[${elements}]::file_attachment[]`);
83
+ },
84
+ fromDriver(value: string): FileAttachment[] {
85
+ if (!value || value === '{}') return [];
86
+ const inner = value.slice(1, -1);
87
+ const matches = inner.match(/\([^)]*\)/g) || [];
88
+ return matches.map(m => {
89
+ const [bucketId, filePath] = m.slice(1, -1).split(',');
90
+ return { bucket_id: bucketId.trim(), file_path: filePath.trim() };
91
+ });
92
+ },
93
+ });
94
+
41
95
  export const customTimestamptz = customType<{
42
96
  data: Date;
43
97
  driverData: string;
package/dist/index.js CHANGED
@@ -472,6 +472,10 @@ var KNOWN_TYPE_FACTORIES = {
472
472
  user_profile: "userProfile",
473
473
  file_attachment: "fileAttachment"
474
474
  };
475
+ var KNOWN_ARRAY_TYPE_FACTORIES = {
476
+ user_profile: "userProfileArray",
477
+ file_attachment: "fileAttachmentArray"
478
+ };
475
479
  var replaceUnknownTransform = {
476
480
  name: "replace-unknown",
477
481
  transform(ctx) {
@@ -490,12 +494,17 @@ var replaceUnknownTransform = {
490
494
  const lines = textBefore.split("\n");
491
495
  let factoryName = "text";
492
496
  let foundKnownType = false;
497
+ let isArrayType = false;
493
498
  for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
494
499
  const line = lines[i];
495
500
  const todoMatch = line.match(/\/\/ TODO: failed to parse database type '(?:\w+\.)?([\w_]+)(\[\])?'/);
496
501
  if (todoMatch) {
497
502
  const typeName = todoMatch[1];
498
- if (KNOWN_TYPE_FACTORIES[typeName]) {
503
+ isArrayType = todoMatch[2] === "[]";
504
+ if (isArrayType && KNOWN_ARRAY_TYPE_FACTORIES[typeName]) {
505
+ factoryName = KNOWN_ARRAY_TYPE_FACTORIES[typeName];
506
+ foundKnownType = true;
507
+ } else if (KNOWN_TYPE_FACTORIES[typeName]) {
499
508
  factoryName = KNOWN_TYPE_FACTORIES[typeName];
500
509
  foundKnownType = true;
501
510
  }
@@ -503,6 +512,15 @@ var replaceUnknownTransform = {
503
512
  }
504
513
  }
505
514
  expression.replaceWithText(factoryName);
515
+ if (isArrayType && foundKnownType) {
516
+ const parent = node.getParent();
517
+ if (Node5.isPropertyAccessExpression(parent) && parent.getName() === "array") {
518
+ const grandParent = parent.getParent();
519
+ if (Node5.isCallExpression(grandParent)) {
520
+ grandParent.replaceWithText(node.getText());
521
+ }
522
+ }
523
+ }
506
524
  if (foundKnownType) {
507
525
  stats.replacedUnknown++;
508
526
  } else {
@@ -1005,6 +1023,139 @@ function resolveTemplateTypesPath() {
1005
1023
  return void 0;
1006
1024
  }
1007
1025
 
1026
+ // src/commands/db/gen-dbschema/utils/fetch-column-comments.ts
1027
+ import postgres from "postgres";
1028
+ var DEFAULT_TIMEOUT_MS = 1e4;
1029
+ async function fetchColumnComments(connectionString, options = {}) {
1030
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1031
+ const url = new URL(connectionString);
1032
+ const schemaName = url.searchParams.get("schema") ?? "public";
1033
+ const sql = postgres(connectionString, {
1034
+ connect_timeout: Math.ceil(timeoutMs / 1e3),
1035
+ idle_timeout: Math.ceil(timeoutMs / 1e3)
1036
+ });
1037
+ try {
1038
+ const queryPromise = sql`
1039
+ SELECT
1040
+ c.table_name AS "tableName",
1041
+ c.column_name AS "columnName",
1042
+ pgd.description AS "comment"
1043
+ FROM information_schema.columns c
1044
+ JOIN pg_catalog.pg_statio_all_tables st
1045
+ ON c.table_name = st.relname AND c.table_schema = st.schemaname
1046
+ JOIN pg_catalog.pg_description pgd
1047
+ ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
1048
+ WHERE c.table_schema = ${schemaName}
1049
+ AND pgd.description IS NOT NULL
1050
+ AND pgd.description != ''
1051
+ `;
1052
+ const timeoutPromise = new Promise((_, reject) => {
1053
+ setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
1054
+ });
1055
+ const result = await Promise.race([queryPromise, timeoutPromise]);
1056
+ const commentMap = /* @__PURE__ */ new Map();
1057
+ for (const row of result) {
1058
+ const key = `${row.tableName}.${row.columnName}`;
1059
+ commentMap.set(key, row.comment);
1060
+ }
1061
+ return commentMap;
1062
+ } finally {
1063
+ await sql.end().catch(() => {
1064
+ });
1065
+ }
1066
+ }
1067
+ function extractTypeAnnotation(comment) {
1068
+ const typeStart = comment.indexOf("@type");
1069
+ if (typeStart === -1) return null;
1070
+ const afterType = comment.slice(typeStart + 5).trimStart();
1071
+ if (!afterType.startsWith("{")) return null;
1072
+ let depth = 0;
1073
+ let endIndex = 0;
1074
+ for (let i = 0; i < afterType.length; i++) {
1075
+ if (afterType[i] === "{") depth++;
1076
+ if (afterType[i] === "}") depth--;
1077
+ if (depth === 0) {
1078
+ endIndex = i + 1;
1079
+ break;
1080
+ }
1081
+ }
1082
+ if (endIndex === 0) return null;
1083
+ return afterType.slice(0, endIndex);
1084
+ }
1085
+ function parseColumnComment(comment) {
1086
+ const typeValue = extractTypeAnnotation(comment);
1087
+ if (typeValue) {
1088
+ const descMatch = comment.match(/@description\s+([^@]+)/);
1089
+ return {
1090
+ type: typeValue,
1091
+ description: descMatch?.[1]?.trim()
1092
+ };
1093
+ }
1094
+ return { description: comment.trim() };
1095
+ }
1096
+
1097
+ // src/commands/db/gen-dbschema/transforms/text/jsonb-comments.ts
1098
+ function addJsonbTypeComments(source, columnComments) {
1099
+ if (!columnComments || columnComments.size === 0) {
1100
+ return { text: source, added: 0 };
1101
+ }
1102
+ const lines = source.split("\n");
1103
+ const result = [];
1104
+ let added = 0;
1105
+ let currentTableName = null;
1106
+ const tableDefRegex = /export const\s+\w+\s*=\s*pgTable\(\s*["'`]([^"'`]+)["'`]/;
1107
+ const jsonFieldWithNameRegex = /^\s*(\w+):\s*(?:json|jsonb)\(\s*["'`]([^"'`]+)["'`]\)/;
1108
+ const jsonFieldNoNameRegex = /^\s*(\w+):\s*(?:json|jsonb)\(\s*\)/;
1109
+ for (let i = 0; i < lines.length; i++) {
1110
+ const line = lines[i];
1111
+ const tableMatch = line.match(tableDefRegex);
1112
+ if (tableMatch) {
1113
+ currentTableName = tableMatch[1];
1114
+ }
1115
+ let columnName = null;
1116
+ const jsonMatchWithName = line.match(jsonFieldWithNameRegex);
1117
+ const jsonMatchNoName = line.match(jsonFieldNoNameRegex);
1118
+ if (jsonMatchWithName) {
1119
+ columnName = jsonMatchWithName[2];
1120
+ } else if (jsonMatchNoName) {
1121
+ columnName = jsonMatchNoName[1];
1122
+ }
1123
+ if (columnName && currentTableName) {
1124
+ const commentKey = `${currentTableName}.${columnName}`;
1125
+ const comment = columnComments.get(commentKey);
1126
+ if (comment) {
1127
+ const parsed = parseColumnComment(comment);
1128
+ const indentMatch = line.match(/^\s*/);
1129
+ const indent = indentMatch ? indentMatch[0] : "";
1130
+ const prevLine = result[result.length - 1]?.trim() ?? "";
1131
+ if (!prevLine.startsWith("/**") && !prevLine.startsWith("*") && !prevLine.startsWith("//")) {
1132
+ const commentLines = [];
1133
+ commentLines.push(`${indent}/**`);
1134
+ if (parsed.description) {
1135
+ const safeDesc = parsed.description.replace(/[\r\n]+/g, " ").trim();
1136
+ commentLines.push(`${indent} * ${safeDesc}`);
1137
+ }
1138
+ if (parsed.type) {
1139
+ if (parsed.description) {
1140
+ commentLines.push(`${indent} *`);
1141
+ }
1142
+ const safeType = parsed.type.replace(/[\r\n]+/g, " ").trim();
1143
+ commentLines.push(`${indent} * @type ${safeType}`);
1144
+ }
1145
+ commentLines.push(`${indent} */`);
1146
+ result.push(...commentLines);
1147
+ added++;
1148
+ }
1149
+ }
1150
+ }
1151
+ if (line.match(/^\}\);?\s*$/) || line.match(/^}\s*,\s*\{/)) {
1152
+ currentTableName = null;
1153
+ }
1154
+ result.push(line);
1155
+ }
1156
+ return { text: result.join("\n"), added };
1157
+ }
1158
+
1008
1159
  // src/commands/db/gen-dbschema/transforms/text/table-aliases.ts
1009
1160
  var TABLE_ALIAS_MARKER = "// table aliases";
1010
1161
  function generateTableAliases(source) {
@@ -1041,7 +1192,7 @@ function formatSource(source) {
1041
1192
  }
1042
1193
 
1043
1194
  // src/commands/db/gen-dbschema/postprocess.ts
1044
- function postprocessSchema(rawSource) {
1195
+ function postprocessSchema(rawSource, options = {}) {
1045
1196
  const patchResult = patchDefects(rawSource);
1046
1197
  let source = patchResult.text;
1047
1198
  const { sourceFile } = parseSource(source);
@@ -1051,12 +1202,15 @@ function postprocessSchema(rawSource) {
1051
1202
  source = ensureHeader(source);
1052
1203
  source = addSystemFieldComments(source);
1053
1204
  source = inlineCustomTypes(source);
1205
+ const jsonbCommentsResult = addJsonbTypeComments(source, options.columnComments);
1206
+ source = jsonbCommentsResult.text;
1054
1207
  source = generateTableAliases(source);
1055
1208
  source = formatSource(source);
1056
1209
  return {
1057
1210
  source,
1058
1211
  astStats,
1059
- patchedDefects: patchResult.fixed
1212
+ patchedDefects: patchResult.fixed,
1213
+ addedJsonbComments: jsonbCommentsResult.added
1060
1214
  };
1061
1215
  }
1062
1216
  function logStats(result, prefix = "[postprocess]") {
@@ -1103,17 +1257,20 @@ function logStats(result, prefix = "[postprocess]") {
1103
1257
  if (astStats.removedImports.length > 0) {
1104
1258
  console.info(`${prefix} Removed imports: ${astStats.removedImports.join(", ")}`);
1105
1259
  }
1260
+ if (result.addedJsonbComments > 0) {
1261
+ console.info(`${prefix} Added ${result.addedJsonbComments} JSDoc comments for jsonb fields`);
1262
+ }
1106
1263
  }
1107
1264
 
1108
1265
  // src/commands/db/gen-dbschema/index.ts
1109
- function postprocessDrizzleSchema(targetPath) {
1266
+ async function postprocessDrizzleSchema(targetPath, options = {}) {
1110
1267
  const resolvedPath = path.resolve(targetPath);
1111
1268
  if (!fs3.existsSync(resolvedPath)) {
1112
1269
  console.warn(`[postprocess-drizzle-schema] File not found: ${resolvedPath}`);
1113
1270
  return void 0;
1114
1271
  }
1115
1272
  const rawSource = fs3.readFileSync(resolvedPath, "utf8");
1116
- const result = postprocessSchema(rawSource);
1273
+ const result = postprocessSchema(rawSource, { columnComments: options.columnComments });
1117
1274
  fs3.writeFileSync(resolvedPath, result.source, "utf8");
1118
1275
  logStats(result, "[postprocess-drizzle-schema]");
1119
1276
  return {
@@ -1122,7 +1279,8 @@ function postprocessDrizzleSchema(targetPath) {
1122
1279
  unmatchedUnknown: result.astStats.unmatchedUnknown,
1123
1280
  patchedDefects: result.patchedDefects,
1124
1281
  replacedTimestamps: result.astStats.replacedTimestamp,
1125
- replacedDefaultNow: result.astStats.replacedDefaultNow
1282
+ replacedDefaultNow: result.astStats.replacedDefaultNow,
1283
+ addedJsonbComments: result.addedJsonbComments
1126
1284
  };
1127
1285
  }
1128
1286
 
@@ -1847,6 +2005,16 @@ async function run(options = {}) {
1847
2005
  }
1848
2006
  throw new Error("Unable to locate drizzle-kit package root");
1849
2007
  };
2008
+ let columnComments;
2009
+ try {
2010
+ columnComments = await fetchColumnComments(databaseUrl, { timeoutMs: 1e4 });
2011
+ console.log(`[gen-db-schema] \u2713 Fetched ${columnComments.size} column comments`);
2012
+ } catch (err) {
2013
+ console.warn(
2014
+ "[gen-db-schema] \u26A0 Failed to fetch column comments (skipping):",
2015
+ err instanceof Error ? err.message : String(err)
2016
+ );
2017
+ }
1850
2018
  try {
1851
2019
  const env = {
1852
2020
  ...process.env,
@@ -1870,7 +2038,9 @@ async function run(options = {}) {
1870
2038
  console.error("[gen-db-schema] schema.ts not generated");
1871
2039
  throw new Error("drizzle-kit introspect failed to generate schema.ts");
1872
2040
  }
1873
- const stats = postprocessDrizzleSchema(generatedSchema);
2041
+ const stats = await postprocessDrizzleSchema(generatedSchema, {
2042
+ columnComments
2043
+ });
1874
2044
  if (stats?.unmatchedUnknown?.length) {
1875
2045
  console.warn("[gen-db-schema] Unmatched custom types detected:", stats.unmatchedUnknown);
1876
2046
  }
@@ -1960,6 +2130,27 @@ var syncConfig = {
1960
2130
  type: "remove-line",
1961
2131
  to: ".gitignore",
1962
2132
  pattern: "package-lock.json"
2133
+ },
2134
+ // 5. 注册 postinstall 脚本,自动恢复 action plugins
2135
+ {
2136
+ type: "add-script",
2137
+ name: "postinstall",
2138
+ command: "fullstack-cli action-plugin init",
2139
+ overwrite: false
2140
+ },
2141
+ // 6. 替换 drizzle.config.ts(仅当文件存在时)
2142
+ {
2143
+ from: "templates/drizzle.config.ts",
2144
+ to: "drizzle.config.ts",
2145
+ type: "file",
2146
+ overwrite: true,
2147
+ onlyIfExists: true
2148
+ },
2149
+ // 7. 确保 .gitignore 包含 .agent/ 目录
2150
+ {
2151
+ type: "add-line",
2152
+ to: ".gitignore",
2153
+ line: ".agent/"
1963
2154
  }
1964
2155
  ],
1965
2156
  // 文件权限设置
@@ -2053,6 +2244,16 @@ async function syncRule(rule, pluginRoot, userProjectRoot) {
2053
2244
  removeLineFromFile(destPath2, rule.pattern);
2054
2245
  return;
2055
2246
  }
2247
+ if (rule.type === "add-script") {
2248
+ const packageJsonPath = path4.join(userProjectRoot, "package.json");
2249
+ addScript(packageJsonPath, rule.name, rule.command, rule.overwrite ?? false);
2250
+ return;
2251
+ }
2252
+ if (rule.type === "add-line") {
2253
+ const destPath2 = path4.join(userProjectRoot, rule.to);
2254
+ addLineToFile(destPath2, rule.line);
2255
+ return;
2256
+ }
2056
2257
  if (!("from" in rule)) {
2057
2258
  return;
2058
2259
  }
@@ -2067,14 +2268,18 @@ async function syncRule(rule, pluginRoot, userProjectRoot) {
2067
2268
  syncDirectory(srcPath, destPath, rule.overwrite ?? true);
2068
2269
  break;
2069
2270
  case "file":
2070
- syncFile(srcPath, destPath, rule.overwrite ?? true);
2271
+ syncFile(srcPath, destPath, rule.overwrite ?? true, rule.onlyIfExists ?? false);
2071
2272
  break;
2072
2273
  case "append":
2073
2274
  appendToFile(srcPath, destPath);
2074
2275
  break;
2075
2276
  }
2076
2277
  }
2077
- function syncFile(src, dest, overwrite = true) {
2278
+ function syncFile(src, dest, overwrite = true, onlyIfExists = false) {
2279
+ if (onlyIfExists && !fs6.existsSync(dest)) {
2280
+ console.log(`[fullstack-cli] \u25CB ${path4.basename(dest)} (skipped, target not exists)`);
2281
+ return;
2282
+ }
2078
2283
  const destDir = path4.dirname(dest);
2079
2284
  if (!fs6.existsSync(destDir)) {
2080
2285
  fs6.mkdirSync(destDir, { recursive: true });
@@ -2155,6 +2360,42 @@ function deleteDirectory(dirPath) {
2155
2360
  console.log(`[fullstack-cli] \u25CB ${path4.basename(dirPath)} (not found)`);
2156
2361
  }
2157
2362
  }
2363
+ function addScript(packageJsonPath, name, command, overwrite) {
2364
+ if (!fs6.existsSync(packageJsonPath)) {
2365
+ console.log(`[fullstack-cli] \u25CB package.json (not found)`);
2366
+ return;
2367
+ }
2368
+ const content = fs6.readFileSync(packageJsonPath, "utf-8");
2369
+ const pkg2 = JSON.parse(content);
2370
+ if (!pkg2.scripts) {
2371
+ pkg2.scripts = {};
2372
+ }
2373
+ if (pkg2.scripts[name]) {
2374
+ if (!overwrite) {
2375
+ console.log(`[fullstack-cli] \u25CB scripts.${name} (already exists)`);
2376
+ return;
2377
+ }
2378
+ }
2379
+ pkg2.scripts[name] = command;
2380
+ fs6.writeFileSync(packageJsonPath, JSON.stringify(pkg2, null, 2) + "\n");
2381
+ console.log(`[fullstack-cli] \u2713 scripts.${name}`);
2382
+ }
2383
+ function addLineToFile(filePath, line) {
2384
+ const fileName = path4.basename(filePath);
2385
+ if (!fs6.existsSync(filePath)) {
2386
+ console.log(`[fullstack-cli] \u25CB ${fileName} (not found, skipped)`);
2387
+ return;
2388
+ }
2389
+ const content = fs6.readFileSync(filePath, "utf-8");
2390
+ const lines = content.split("\n").map((l) => l.trim());
2391
+ if (lines.includes(line)) {
2392
+ console.log(`[fullstack-cli] \u25CB ${fileName} (line already exists: ${line})`);
2393
+ return;
2394
+ }
2395
+ const appendContent = (content.endsWith("\n") ? "" : "\n") + line + "\n";
2396
+ fs6.appendFileSync(filePath, appendContent);
2397
+ console.log(`[fullstack-cli] \u2713 ${fileName} (added: ${line})`);
2398
+ }
2158
2399
 
2159
2400
  // src/commands/sync/index.ts
2160
2401
  var syncCommand = {
@@ -2438,9 +2679,24 @@ function ensureCacheDir() {
2438
2679
  function getTempFilePath(pluginKey, version) {
2439
2680
  ensureCacheDir();
2440
2681
  const safeKey = pluginKey.replace(/[/@]/g, "_");
2441
- const filename = `${safeKey}-${version}.tgz`;
2682
+ const filename = `${safeKey}@${version}.tgz`;
2442
2683
  return path6.join(getPluginCacheDir(), filename);
2443
2684
  }
2685
+ var MAX_RETRIES = 2;
2686
+ async function withRetry(operation, description, maxRetries = MAX_RETRIES) {
2687
+ let lastError;
2688
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2689
+ try {
2690
+ return await operation();
2691
+ } catch (error) {
2692
+ lastError = error instanceof Error ? error : new Error(String(error));
2693
+ if (attempt < maxRetries) {
2694
+ console.log(`[action-plugin] ${description} failed, retrying (${attempt + 1}/${maxRetries})...`);
2695
+ }
2696
+ }
2697
+ }
2698
+ throw lastError;
2699
+ }
2444
2700
  async function downloadPlugin(pluginKey, requestedVersion) {
2445
2701
  console.log(`[action-plugin] Fetching plugin info for ${pluginKey}@${requestedVersion}...`);
2446
2702
  const pluginInfo = await getPluginVersion(pluginKey, requestedVersion);
@@ -2449,10 +2705,16 @@ async function downloadPlugin(pluginKey, requestedVersion) {
2449
2705
  let tgzBuffer;
2450
2706
  if (pluginInfo.downloadApproach === "inner") {
2451
2707
  console.log(`[action-plugin] Downloading from inner API...`);
2452
- tgzBuffer = await downloadFromInner(pluginKey, pluginInfo.version);
2708
+ tgzBuffer = await withRetry(
2709
+ () => downloadFromInner(pluginKey, pluginInfo.version),
2710
+ "Download"
2711
+ );
2453
2712
  } else {
2454
2713
  console.log(`[action-plugin] Downloading from public URL...`);
2455
- tgzBuffer = await downloadFromPublic(pluginInfo.downloadURL);
2714
+ tgzBuffer = await withRetry(
2715
+ () => downloadFromPublic(pluginInfo.downloadURL),
2716
+ "Download"
2717
+ );
2456
2718
  }
2457
2719
  const tgzPath = getTempFilePath(pluginKey, pluginInfo.version);
2458
2720
  fs8.writeFileSync(tgzPath, tgzBuffer);
@@ -2464,25 +2726,99 @@ async function downloadPlugin(pluginKey, requestedVersion) {
2464
2726
  };
2465
2727
  }
2466
2728
  function cleanupTempFile(tgzPath) {
2467
- try {
2468
- if (fs8.existsSync(tgzPath)) {
2469
- fs8.unlinkSync(tgzPath);
2729
+ }
2730
+ function getCachePath(pluginKey, version) {
2731
+ ensureCacheDir();
2732
+ const safeKey = pluginKey.replace(/[/@]/g, "_");
2733
+ const filename = `${safeKey}@${version}.tgz`;
2734
+ return path6.join(getPluginCacheDir(), filename);
2735
+ }
2736
+ function hasCachedPlugin(pluginKey, version) {
2737
+ const cachePath = getCachePath(pluginKey, version);
2738
+ return fs8.existsSync(cachePath);
2739
+ }
2740
+ function listCachedPlugins() {
2741
+ const cacheDir = getPluginCacheDir();
2742
+ if (!fs8.existsSync(cacheDir)) {
2743
+ return [];
2744
+ }
2745
+ const files = fs8.readdirSync(cacheDir);
2746
+ const result = [];
2747
+ for (const file of files) {
2748
+ if (!file.endsWith(".tgz")) continue;
2749
+ const match = file.match(/^(.+)@(.+)\.tgz$/);
2750
+ if (!match) continue;
2751
+ const [, rawName, version] = match;
2752
+ const name = rawName.replace(/^_/, "@").replace(/_/, "/");
2753
+ const filePath = path6.join(cacheDir, file);
2754
+ const stat = fs8.statSync(filePath);
2755
+ result.push({
2756
+ name,
2757
+ version,
2758
+ filePath,
2759
+ size: stat.size,
2760
+ mtime: stat.mtime
2761
+ });
2762
+ }
2763
+ return result;
2764
+ }
2765
+ function cleanAllCache() {
2766
+ const cacheDir = getPluginCacheDir();
2767
+ if (!fs8.existsSync(cacheDir)) {
2768
+ return 0;
2769
+ }
2770
+ const files = fs8.readdirSync(cacheDir);
2771
+ let count = 0;
2772
+ for (const file of files) {
2773
+ if (file.endsWith(".tgz")) {
2774
+ fs8.unlinkSync(path6.join(cacheDir, file));
2775
+ count++;
2776
+ }
2777
+ }
2778
+ return count;
2779
+ }
2780
+ function cleanPluginCache(pluginKey, version) {
2781
+ const cacheDir = getPluginCacheDir();
2782
+ if (!fs8.existsSync(cacheDir)) {
2783
+ return 0;
2784
+ }
2785
+ const safeKey = pluginKey.replace(/[/@]/g, "_");
2786
+ const files = fs8.readdirSync(cacheDir);
2787
+ let count = 0;
2788
+ for (const file of files) {
2789
+ if (version) {
2790
+ if (file === `${safeKey}@${version}.tgz`) {
2791
+ fs8.unlinkSync(path6.join(cacheDir, file));
2792
+ count++;
2793
+ }
2794
+ } else {
2795
+ if (file.startsWith(`${safeKey}@`) && file.endsWith(".tgz")) {
2796
+ fs8.unlinkSync(path6.join(cacheDir, file));
2797
+ count++;
2798
+ }
2470
2799
  }
2471
- } catch {
2472
2800
  }
2801
+ return count;
2473
2802
  }
2474
2803
 
2475
2804
  // src/commands/action-plugin/init.handler.ts
2476
2805
  async function installOneForInit(name, version) {
2477
- let tgzPath;
2478
2806
  try {
2479
2807
  const installedVersion = getPackageVersion(name);
2480
2808
  if (installedVersion === version) {
2481
2809
  return { name, version, success: true, skipped: true };
2482
2810
  }
2483
- console.log(`[action-plugin] Installing ${name}@${version}...`);
2484
- const downloadResult = await downloadPlugin(name, version);
2485
- tgzPath = downloadResult.tgzPath;
2811
+ let tgzPath;
2812
+ let fromCache = false;
2813
+ if (hasCachedPlugin(name, version)) {
2814
+ console.log(`[action-plugin] \u21BB Restoring ${name}@${version} from cache...`);
2815
+ tgzPath = getCachePath(name, version);
2816
+ fromCache = true;
2817
+ } else {
2818
+ console.log(`[action-plugin] \u2193 Downloading ${name}@${version}...`);
2819
+ const downloadResult = await downloadPlugin(name, version);
2820
+ tgzPath = downloadResult.tgzPath;
2821
+ }
2486
2822
  const pluginDir = extractTgzToNodeModules(tgzPath, name);
2487
2823
  const pluginPkg = readPluginPackageJson(pluginDir);
2488
2824
  if (pluginPkg?.peerDependencies) {
@@ -2491,16 +2827,13 @@ async function installOneForInit(name, version) {
2491
2827
  installMissingDeps(missingDeps);
2492
2828
  }
2493
2829
  }
2494
- console.log(`[action-plugin] \u2713 Installed ${name}@${version}`);
2830
+ const source = fromCache ? "from cache" : "downloaded";
2831
+ console.log(`[action-plugin] \u2713 Installed ${name}@${version} (${source})`);
2495
2832
  return { name, version, success: true };
2496
2833
  } catch (error) {
2497
2834
  const message = error instanceof Error ? error.message : String(error);
2498
2835
  console.error(`[action-plugin] \u2717 Failed to install ${name}: ${message}`);
2499
2836
  return { name, version, success: false, error: message };
2500
- } finally {
2501
- if (tgzPath) {
2502
- cleanupTempFile(tgzPath);
2503
- }
2504
2837
  }
2505
2838
  }
2506
2839
  async function init() {
@@ -2546,7 +2879,6 @@ function syncActionPluginsRecord(name, version) {
2546
2879
  }
2547
2880
  }
2548
2881
  async function installOne(nameWithVersion) {
2549
- let tgzPath;
2550
2882
  const { name, version: requestedVersion } = parsePluginName(nameWithVersion);
2551
2883
  try {
2552
2884
  console.log(`[action-plugin] Installing ${name}@${requestedVersion}...`);
@@ -2558,17 +2890,28 @@ async function installOne(nameWithVersion) {
2558
2890
  return { name, version: actualVersion, success: true, skipped: true };
2559
2891
  }
2560
2892
  }
2561
- if (actualVersion && requestedVersion === "latest") {
2893
+ let targetVersion = requestedVersion;
2894
+ if (requestedVersion === "latest") {
2562
2895
  const latestInfo = await getPluginVersion(name, "latest");
2563
- if (actualVersion === latestInfo.version) {
2896
+ targetVersion = latestInfo.version;
2897
+ if (actualVersion === targetVersion) {
2564
2898
  console.log(`[action-plugin] Plugin ${name} is already up to date (version: ${actualVersion})`);
2565
2899
  syncActionPluginsRecord(name, actualVersion);
2566
2900
  return { name, version: actualVersion, success: true, skipped: true };
2567
2901
  }
2568
- console.log(`[action-plugin] Found newer version: ${latestInfo.version} (installed: ${actualVersion})`);
2902
+ console.log(`[action-plugin] Found newer version: ${targetVersion} (installed: ${actualVersion || "none"})`);
2903
+ }
2904
+ let tgzPath;
2905
+ let fromCache = false;
2906
+ if (hasCachedPlugin(name, targetVersion)) {
2907
+ console.log(`[action-plugin] \u21BB Using cached ${name}@${targetVersion}...`);
2908
+ tgzPath = getCachePath(name, targetVersion);
2909
+ fromCache = true;
2910
+ } else {
2911
+ console.log(`[action-plugin] \u2193 Downloading ${name}@${targetVersion}...`);
2912
+ const downloadResult = await downloadPlugin(name, requestedVersion);
2913
+ tgzPath = downloadResult.tgzPath;
2569
2914
  }
2570
- const downloadResult = await downloadPlugin(name, requestedVersion);
2571
- tgzPath = downloadResult.tgzPath;
2572
2915
  console.log(`[action-plugin] Extracting to node_modules...`);
2573
2916
  const pluginDir = extractTgzToNodeModules(tgzPath, name);
2574
2917
  const pluginPkg = readPluginPackageJson(pluginDir);
@@ -2578,20 +2921,17 @@ async function installOne(nameWithVersion) {
2578
2921
  installMissingDeps(missingDeps);
2579
2922
  }
2580
2923
  }
2581
- const installedVersion = getPackageVersion(name) || downloadResult.version;
2924
+ const installedVersion = getPackageVersion(name) || targetVersion;
2582
2925
  const plugins = readActionPlugins();
2583
2926
  plugins[name] = installedVersion;
2584
2927
  writeActionPlugins(plugins);
2585
- console.log(`[action-plugin] Successfully installed ${name}@${installedVersion}`);
2928
+ const source = fromCache ? "from cache" : "downloaded";
2929
+ console.log(`[action-plugin] Successfully installed ${name}@${installedVersion} (${source})`);
2586
2930
  return { name, version: installedVersion, success: true };
2587
2931
  } catch (error) {
2588
2932
  const message = error instanceof Error ? error.message : String(error);
2589
2933
  console.error(`[action-plugin] Failed to install ${name}: ${message}`);
2590
2934
  return { name, version: requestedVersion, success: false, error: message };
2591
- } finally {
2592
- if (tgzPath) {
2593
- cleanupTempFile(tgzPath);
2594
- }
2595
2935
  }
2596
2936
  }
2597
2937
  async function install(namesWithVersion) {
@@ -2745,6 +3085,58 @@ async function list() {
2745
3085
  }
2746
3086
  }
2747
3087
 
3088
+ // src/commands/action-plugin/cache.handler.ts
3089
+ async function cacheList() {
3090
+ const cached = listCachedPlugins();
3091
+ if (cached.length === 0) {
3092
+ console.log("[action-plugin] No cached plugins found");
3093
+ return;
3094
+ }
3095
+ console.log("[action-plugin] Cached plugins:\n");
3096
+ console.log(" Name Version Size Modified");
3097
+ 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\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\u2500");
3098
+ let totalSize = 0;
3099
+ for (const plugin of cached) {
3100
+ const name = plugin.name.padEnd(38);
3101
+ const version = plugin.version.padEnd(10);
3102
+ const size = formatSize(plugin.size).padEnd(9);
3103
+ const mtime = plugin.mtime.toISOString().replace("T", " ").slice(0, 19);
3104
+ console.log(` ${name} ${version} ${size} ${mtime}`);
3105
+ totalSize += plugin.size;
3106
+ }
3107
+ 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\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\u2500");
3108
+ console.log(` Total: ${cached.length} cached file(s), ${formatSize(totalSize)}`);
3109
+ }
3110
+ async function cacheClean(pluginName, version) {
3111
+ let count;
3112
+ if (pluginName) {
3113
+ console.log(`[action-plugin] Cleaning cache for ${pluginName}${version ? `@${version}` : ""}...`);
3114
+ count = cleanPluginCache(pluginName, version);
3115
+ if (count === 0) {
3116
+ console.log(`[action-plugin] No cached files found for ${pluginName}${version ? `@${version}` : ""}`);
3117
+ } else {
3118
+ console.log(`[action-plugin] Cleaned ${count} cached file(s)`);
3119
+ }
3120
+ } else {
3121
+ console.log("[action-plugin] Cleaning all cached plugins...");
3122
+ count = cleanAllCache();
3123
+ if (count === 0) {
3124
+ console.log("[action-plugin] No cached files found");
3125
+ } else {
3126
+ console.log(`[action-plugin] Cleaned ${count} cached file(s)`);
3127
+ }
3128
+ }
3129
+ }
3130
+ function formatSize(bytes) {
3131
+ if (bytes < 1024) {
3132
+ return `${bytes} B`;
3133
+ } else if (bytes < 1024 * 1024) {
3134
+ return `${(bytes / 1024).toFixed(1)} KB`;
3135
+ } else {
3136
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3137
+ }
3138
+ }
3139
+
2748
3140
  // src/commands/action-plugin/index.ts
2749
3141
  var initCommand = {
2750
3142
  name: "init",
@@ -2795,14 +3187,33 @@ var listCommand = {
2795
3187
  });
2796
3188
  }
2797
3189
  };
3190
+ var cacheListCommand = {
3191
+ name: "cache-list",
3192
+ description: "List all cached plugin packages",
3193
+ register(program) {
3194
+ program.command(this.name).description(this.description).action(async () => {
3195
+ await cacheList();
3196
+ });
3197
+ }
3198
+ };
3199
+ var cacheCleanCommand = {
3200
+ name: "cache-clean",
3201
+ description: "Clean cached plugin packages",
3202
+ register(program) {
3203
+ program.command(this.name).description(this.description).argument("[name]", "Plugin name to clean (e.g., @office/feishu-create-group). If not provided, clean all cache.").option("-v, --version <version>", "Specific version to clean").action(async (name, options) => {
3204
+ await cacheClean(name, options.version);
3205
+ });
3206
+ }
3207
+ };
2798
3208
  var actionPluginCommandGroup = {
2799
3209
  name: "action-plugin",
2800
3210
  description: "Manage action plugins",
2801
- commands: [initCommand, installCommand, updateCommand, removeCommand, listCommand]
3211
+ commands: [initCommand, installCommand, updateCommand, removeCommand, listCommand, cacheListCommand, cacheCleanCommand]
2802
3212
  };
2803
3213
 
2804
3214
  // src/commands/capability/utils.ts
2805
3215
  import fs9 from "fs";
3216
+ import { createRequire as createRequire2 } from "module";
2806
3217
  import path7 from "path";
2807
3218
  var CAPABILITIES_DIR = "server/capabilities";
2808
3219
  function getProjectRoot2() {
@@ -2882,7 +3293,10 @@ function hasValidParamsSchema(paramsSchema) {
2882
3293
  }
2883
3294
  async function loadPlugin(pluginKey) {
2884
3295
  try {
2885
- const pluginPackage = (await import(pluginKey)).default;
3296
+ const userRequire = createRequire2(path7.join(getProjectRoot2(), "package.json"));
3297
+ const resolvedPath = userRequire.resolve(pluginKey);
3298
+ const pluginModule = await import(resolvedPath);
3299
+ const pluginPackage = pluginModule.default ?? pluginModule;
2886
3300
  if (!pluginPackage || typeof pluginPackage.create !== "function") {
2887
3301
  throw new Error(`Plugin ${pluginKey} does not export a valid create function`);
2888
3302
  }
@@ -5121,6 +5535,28 @@ function extractClientStdSegment(lines, maxLines, offset) {
5121
5535
  };
5122
5536
  }
5123
5537
 
5538
+ // src/commands/read-logs/dev.ts
5539
+ function readDevSegment(filePath, maxLines, offset) {
5540
+ const marker = (line) => {
5541
+ if (!line) return false;
5542
+ if (line.includes("Dev session started")) return true;
5543
+ return false;
5544
+ };
5545
+ const segment = readStdLinesTailFromLastMarkerPaged(filePath, maxLines, offset, marker);
5546
+ return { lines: segment.lines, totalLinesCount: segment.totalLinesCount };
5547
+ }
5548
+
5549
+ // src/commands/read-logs/dev-std.ts
5550
+ function readDevStdSegment(filePath, maxLines, offset) {
5551
+ const marker = (line) => {
5552
+ if (!line) return false;
5553
+ if (line.includes("Dev session started")) return true;
5554
+ return false;
5555
+ };
5556
+ const segment = readStdLinesTailFromLastMarkerPaged(filePath, maxLines, offset, marker);
5557
+ return { lines: segment.lines, totalLinesCount: segment.totalLinesCount };
5558
+ }
5559
+
5124
5560
  // src/commands/read-logs/json-lines.ts
5125
5561
  import fs20 from "fs";
5126
5562
  function normalizePid(value) {
@@ -5445,7 +5881,7 @@ function readJsonLinesTailByLevel(filePath, maxLines, offset, levels) {
5445
5881
  }
5446
5882
 
5447
5883
  // src/commands/read-logs/index.ts
5448
- var LOG_TYPES = ["server", "trace", "server-std", "client-std", "browser"];
5884
+ var LOG_TYPES = ["server", "trace", "server-std", "client-std", "dev", "dev-std", "install-dep-std", "browser"];
5449
5885
  function normalizeObjectKey(key) {
5450
5886
  return key.toLowerCase().replace(/_/g, "");
5451
5887
  }
@@ -5607,6 +6043,15 @@ async function readLatestLogLinesMeta(options) {
5607
6043
  if (options.type === "client-std") {
5608
6044
  return readClientStdSegment(filePath, maxLines, offset);
5609
6045
  }
6046
+ if (options.type === "dev") {
6047
+ return readDevSegment(filePath, maxLines, offset);
6048
+ }
6049
+ if (options.type === "dev-std") {
6050
+ return readDevStdSegment(filePath, maxLines, offset);
6051
+ }
6052
+ if (options.type === "install-dep-std") {
6053
+ return readFileTailNonEmptyLinesWithOffset(filePath, maxLines, offset);
6054
+ }
5610
6055
  const traceId = typeof options.traceId === "string" ? options.traceId.trim() : "";
5611
6056
  if (traceId) {
5612
6057
  return readJsonLinesByTraceId(filePath, traceId, maxLines, offset, levels);
@@ -5624,7 +6069,7 @@ async function readLatestLogLinesMeta(options) {
5624
6069
  }
5625
6070
  async function readLogsJsonResult(options) {
5626
6071
  const { lines, totalLinesCount } = await readLatestLogLinesMeta(options);
5627
- if (options.type === "server-std" || options.type === "client-std") {
6072
+ if (options.type === "server-std" || options.type === "client-std" || options.type === "dev" || options.type === "dev-std" || options.type === "install-dep-std") {
5628
6073
  return {
5629
6074
  hasError: hasErrorInStdLines(lines),
5630
6075
  totalLinesCount,
@@ -5668,6 +6113,15 @@ function resolveLogFilePath(logDir, type) {
5668
6113
  if (type === "client-std") {
5669
6114
  return path16.join(base, "client.std.log");
5670
6115
  }
6116
+ if (type === "dev") {
6117
+ return path16.join(base, "dev.log");
6118
+ }
6119
+ if (type === "dev-std") {
6120
+ return path16.join(base, "dev.std.log");
6121
+ }
6122
+ if (type === "install-dep-std") {
6123
+ return path16.join(base, "install-dep.std.log");
6124
+ }
5671
6125
  if (type === "browser") {
5672
6126
  return path16.join(base, "browser.log");
5673
6127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.16-beta.0",
3
+ "version": "1.1.16-beta.2",
4
4
  "description": "CLI tool for fullstack template management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,55 @@
1
+ import { defineConfig, Config } from 'drizzle-kit';
2
+ require('dotenv').config();
3
+
4
+ const outputDir = process.env.__DRIZZLE_OUT_DIR__ || './server/database/.introspect';
5
+ const schemaPath = process.env.__DRIZZLE_SCHEMA_PATH__ || './server/database/schema.ts';
6
+
7
+ const parsedUrl = new URL(process.env.SUDA_DATABASE_URL || '');
8
+
9
+ const envSchemaFilter = process.env.DRIZZLE_SCHEMA_FILTER;
10
+ const urlSchemaFilter = parsedUrl.searchParams.get('schema');
11
+
12
+ const schemaFilter = (envSchemaFilter ?? urlSchemaFilter ?? '')
13
+ .split(',')
14
+ .map((s) => s.trim())
15
+ .filter(Boolean);
16
+
17
+ parsedUrl.searchParams.delete('schema'); // 移除schema参数,避免 drizzle-kit 解析错误
18
+
19
+ // 默认排除的系统对象(PostgreSQL 扩展和系统视图)
20
+ // 这些对象在 drizzle-kit introspect 时可能导致无效的 JS 代码生成
21
+ const SYSTEM_OBJECTS_EXCLUSIONS = [
22
+ '!spatial_ref_sys', // PostGIS 空间参考系统表
23
+ '!geography_columns', // PostGIS 地理列视图
24
+ '!geometry_columns', // PostGIS 几何列视图
25
+ '!raster_columns', // PostGIS 栅格列视图
26
+ '!raster_overviews', // PostGIS 栅格概览视图
27
+ '!pg_stat_statements', // pg_stat_statements 扩展
28
+ '!pg_stat_statements_info', // pg_stat_statements 扩展
29
+ '!part_config', // pg_partman 分区配置表
30
+ '!part_config_sub', // pg_partman 子分区配置表
31
+ '!table_privs', // 系统权限视图
32
+ ];
33
+
34
+ const envTablesFilter = process.env.DRIZZLE_TABLES_FILTER;
35
+ const userTablesFilter = (envTablesFilter ?? '*')
36
+ .split(',')
37
+ .map((s) => s.trim())
38
+ .filter(Boolean);
39
+
40
+ // 合并用户过滤器和系统对象排除
41
+ // 用户可以通过设置 DRIZZLE_TABLES_FILTER 来覆盖(如果需要包含某些系统对象)
42
+ const tablesFilter = [...userTablesFilter, ...SYSTEM_OBJECTS_EXCLUSIONS];
43
+
44
+ const config:Config = {
45
+ schema: schemaPath,
46
+ out: outputDir,
47
+ tablesFilter,
48
+ schemaFilter,
49
+ dialect: 'postgresql',
50
+ dbCredentials: {
51
+ url: parsedUrl.toString(),
52
+ },
53
+ }
54
+
55
+ export default defineConfig(config);
@@ -21,6 +21,10 @@ CLEANUP_DONE=false
21
21
 
22
22
  mkdir -p "${LOG_DIR}"
23
23
 
24
+ # Redirect all stdout/stderr to both terminal and log file
25
+ DEV_STD_LOG="${LOG_DIR}/dev.std.log"
26
+ exec > >(tee -a "$DEV_STD_LOG") 2>&1
27
+
24
28
  # Log event to dev.log with timestamp
25
29
  log_event() {
26
30
  local level=$1
@@ -234,7 +238,7 @@ log_event "INFO" "client" "Supervisor started with PID ${CLIENT_PID}"
234
238
  log_event "INFO" "main" "All processes started, monitoring..."
235
239
  echo ""
236
240
  echo "📋 Dev processes running. Press Ctrl+C to stop."
237
- echo "📄 Logs: ${DEV_LOG}"
241
+ echo "📄 Logs: ${DEV_STD_LOG}"
238
242
  echo ""
239
243
 
240
244
  # Wait for all background processes