@lark-apaas/fullstack-cli 1.1.58 → 1.1.59-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -847,117 +847,6 @@ var tweakImportsTransform = {
847
847
  }
848
848
  };
849
849
 
850
- // src/commands/db/gen-dbschema/transforms/ast/fix-opclass.ts
851
- import { Node as Node9 } from "ts-morph";
852
- var TYPE_TO_OPCLASS = {
853
- uuid: "uuid_ops",
854
- date: "date_ops",
855
- varchar: "text_ops",
856
- text: "text_ops",
857
- boolean: "bool_ops",
858
- integer: "int4_ops",
859
- serial: "int4_ops",
860
- numeric: "numeric_ops",
861
- jsonb: "jsonb_ops",
862
- customTimestamptz: "timestamptz_ops",
863
- bigint: "int8_ops",
864
- smallint: "int2_ops",
865
- real: "float4_ops",
866
- doublePrecision: "float8_ops"
867
- };
868
- var fixOpclassTransform = {
869
- name: "fix-opclass",
870
- transform(ctx) {
871
- const { sourceFile } = ctx;
872
- sourceFile.forEachDescendant((node) => {
873
- if (!Node9.isCallExpression(node)) return;
874
- const expr = node.getExpression();
875
- if (!Node9.isIdentifier(expr)) return;
876
- const fnName = expr.getText();
877
- if (fnName !== "pgTable") return;
878
- const args = node.getArguments();
879
- if (args.length < 3) return;
880
- const tableColumns = collectTableColumns(args[1]);
881
- if (!tableColumns.size) return;
882
- const indexFn = args[2];
883
- if (!Node9.isArrowFunction(indexFn)) return;
884
- fixOpclassInIndexes(indexFn, tableColumns);
885
- });
886
- }
887
- };
888
- function collectTableColumns(columnsArg) {
889
- const columns = /* @__PURE__ */ new Map();
890
- if (!Node9.isObjectLiteralExpression(columnsArg)) return columns;
891
- for (const prop of columnsArg.getProperties()) {
892
- if (!Node9.isPropertyAssignment(prop)) continue;
893
- const colName = prop.getName();
894
- const initializer = prop.getInitializer();
895
- if (!initializer) continue;
896
- const colType = extractColumnType(initializer);
897
- if (colType) {
898
- columns.set(colName, colType);
899
- }
900
- }
901
- return columns;
902
- }
903
- function extractColumnType(node) {
904
- const text = node.getText();
905
- for (const typeName of Object.keys(TYPE_TO_OPCLASS)) {
906
- const pattern = new RegExp(`\\b${typeName}\\s*\\(`);
907
- if (pattern.test(text)) {
908
- return typeName;
909
- }
910
- }
911
- return void 0;
912
- }
913
- function fixOpclassInIndexes(indexFn, tableColumns) {
914
- const paramName = getArrowParam(indexFn);
915
- if (!paramName) return;
916
- indexFn.forEachDescendant((node) => {
917
- if (!Node9.isCallExpression(node)) return;
918
- const expr = node.getExpression();
919
- if (!Node9.isPropertyAccessExpression(expr)) return;
920
- if (expr.getName() !== "op") return;
921
- const opArgs = node.getArguments();
922
- if (opArgs.length !== 1) return;
923
- const currentOp = opArgs[0];
924
- if (!Node9.isStringLiteral(currentOp)) return;
925
- const colName = resolveColumnName(expr.getExpression(), paramName);
926
- if (!colName) return;
927
- const colType = tableColumns.get(colName);
928
- if (!colType) return;
929
- const correctOp = TYPE_TO_OPCLASS[colType];
930
- if (!correctOp) return;
931
- const currentValue = currentOp.getLiteralValue();
932
- if (currentValue !== correctOp) {
933
- currentOp.setLiteralValue(correctOp);
934
- }
935
- });
936
- }
937
- function getArrowParam(node) {
938
- if (!Node9.isArrowFunction(node)) return void 0;
939
- const params = node.getParameters();
940
- if (params.length !== 1) return void 0;
941
- return params[0].getName();
942
- }
943
- function resolveColumnName(node, tableParam) {
944
- if (Node9.isPropertyAccessExpression(node)) {
945
- const obj = node.getExpression();
946
- const propName = node.getName();
947
- if (Node9.isIdentifier(obj) && obj.getText() === tableParam) {
948
- return propName;
949
- }
950
- return resolveColumnName(obj, tableParam);
951
- }
952
- if (Node9.isCallExpression(node)) {
953
- const callExpr = node.getExpression();
954
- if (Node9.isPropertyAccessExpression(callExpr)) {
955
- return resolveColumnName(callExpr.getExpression(), tableParam);
956
- }
957
- }
958
- return void 0;
959
- }
960
-
961
850
  // src/commands/db/gen-dbschema/transforms/ast/index.ts
962
851
  var defaultTransforms = [
963
852
  patchDefectsTransform,
@@ -976,8 +865,6 @@ var defaultTransforms = [
976
865
  // #9 Replace .defaultNow()
977
866
  removeSystemFieldsTransform,
978
867
  // #10 Remove conflicting system fields
979
- fixOpclassTransform,
980
- // #11 Fix multi-column index opclass mapping
981
868
  tweakImportsTransform
982
869
  // #12 Adjust imports
983
870
  ];
@@ -2014,7 +1901,7 @@ export class ${className}Module {}
2014
1901
  }
2015
1902
 
2016
1903
  // src/commands/db/gen-nest-resource/schema-parser.ts
2017
- import { Project as Project2, Node as Node10 } from "ts-morph";
1904
+ import { Project as Project2, Node as Node9 } from "ts-morph";
2018
1905
  var DrizzleSchemaParser = class {
2019
1906
  constructor(projectOptions) {
2020
1907
  this.project = new Project2(projectOptions);
@@ -2027,7 +1914,7 @@ var DrizzleSchemaParser = class {
2027
1914
  const declarations = statement.getDeclarations();
2028
1915
  for (const declaration of declarations) {
2029
1916
  const initializer = declaration.getInitializer();
2030
- if (initializer && Node10.isCallExpression(initializer)) {
1917
+ if (initializer && Node9.isCallExpression(initializer)) {
2031
1918
  const expression = initializer.getExpression();
2032
1919
  if (expression.getText() === "pgTable") {
2033
1920
  const tableInfo = this.parsePgTable(
@@ -2050,13 +1937,13 @@ var DrizzleSchemaParser = class {
2050
1937
  }
2051
1938
  const tableName = args[0].getText().replace(/['"]/g, "");
2052
1939
  const fieldsArg = args[1];
2053
- if (!Node10.isObjectLiteralExpression(fieldsArg)) {
1940
+ if (!Node9.isObjectLiteralExpression(fieldsArg)) {
2054
1941
  return null;
2055
1942
  }
2056
1943
  const fields = [];
2057
1944
  const properties = fieldsArg.getProperties();
2058
1945
  for (const prop of properties) {
2059
- if (Node10.isPropertyAssignment(prop)) {
1946
+ if (Node9.isPropertyAssignment(prop)) {
2060
1947
  const fieldName = prop.getName();
2061
1948
  const initializer = prop.getInitializer();
2062
1949
  const leadingComments = prop.getLeadingCommentRanges();
@@ -2064,7 +1951,7 @@ var DrizzleSchemaParser = class {
2064
1951
  if (leadingComments.length > 0) {
2065
1952
  comment = leadingComments.map((c) => c.getText()).join("\n").replace(/\/\//g, "").trim();
2066
1953
  }
2067
- if (initializer && Node10.isCallExpression(initializer)) {
1954
+ if (initializer && Node9.isCallExpression(initializer)) {
2068
1955
  const fieldInfo = this.parseField(fieldName, initializer, comment);
2069
1956
  fields.push(fieldInfo);
2070
1957
  }
@@ -2096,10 +1983,10 @@ var DrizzleSchemaParser = class {
2096
1983
  parseBaseType(callExpr, fieldInfo) {
2097
1984
  let current = callExpr;
2098
1985
  let baseCall = null;
2099
- while (Node10.isCallExpression(current)) {
1986
+ while (Node9.isCallExpression(current)) {
2100
1987
  baseCall = current;
2101
1988
  const expression2 = current.getExpression();
2102
- if (Node10.isPropertyAccessExpression(expression2)) {
1989
+ if (Node9.isPropertyAccessExpression(expression2)) {
2103
1990
  current = expression2.getExpression();
2104
1991
  } else {
2105
1992
  break;
@@ -2110,7 +1997,7 @@ var DrizzleSchemaParser = class {
2110
1997
  }
2111
1998
  const expression = baseCall.getExpression();
2112
1999
  let typeName = "";
2113
- if (Node10.isPropertyAccessExpression(expression)) {
2000
+ if (Node9.isPropertyAccessExpression(expression)) {
2114
2001
  typeName = expression.getName();
2115
2002
  } else {
2116
2003
  typeName = expression.getText();
@@ -2119,25 +2006,25 @@ var DrizzleSchemaParser = class {
2119
2006
  const args = baseCall.getArguments();
2120
2007
  if (args.length > 0) {
2121
2008
  const firstArg = args[0];
2122
- if (Node10.isStringLiteral(firstArg)) {
2009
+ if (Node9.isStringLiteral(firstArg)) {
2123
2010
  fieldInfo.columnName = firstArg.getLiteralText();
2124
- } else if (Node10.isObjectLiteralExpression(firstArg)) {
2011
+ } else if (Node9.isObjectLiteralExpression(firstArg)) {
2125
2012
  this.parseTypeConfig(firstArg, fieldInfo);
2126
- } else if (Node10.isArrayLiteralExpression(firstArg)) {
2013
+ } else if (Node9.isArrayLiteralExpression(firstArg)) {
2127
2014
  fieldInfo.enumValues = firstArg.getElements().map((el) => el.getText().replace(/['"]/g, ""));
2128
2015
  }
2129
2016
  }
2130
- if (args.length > 1 && Node10.isObjectLiteralExpression(args[1])) {
2017
+ if (args.length > 1 && Node9.isObjectLiteralExpression(args[1])) {
2131
2018
  this.parseTypeConfig(args[1], fieldInfo);
2132
2019
  }
2133
2020
  }
2134
2021
  parseTypeConfig(objLiteral, fieldInfo) {
2135
- if (!Node10.isObjectLiteralExpression(objLiteral)) {
2022
+ if (!Node9.isObjectLiteralExpression(objLiteral)) {
2136
2023
  return;
2137
2024
  }
2138
2025
  const properties = objLiteral.getProperties();
2139
2026
  for (const prop of properties) {
2140
- if (Node10.isPropertyAssignment(prop)) {
2027
+ if (Node9.isPropertyAssignment(prop)) {
2141
2028
  const propName = prop.getName();
2142
2029
  const value = prop.getInitializer()?.getText();
2143
2030
  switch (propName) {
@@ -2169,9 +2056,9 @@ var DrizzleSchemaParser = class {
2169
2056
  }
2170
2057
  parseCallChain(callExpr, fieldInfo) {
2171
2058
  let current = callExpr;
2172
- while (Node10.isCallExpression(current)) {
2059
+ while (Node9.isCallExpression(current)) {
2173
2060
  const expression = current.getExpression();
2174
- if (Node10.isPropertyAccessExpression(expression)) {
2061
+ if (Node9.isPropertyAccessExpression(expression)) {
2175
2062
  const methodName = expression.getName();
2176
2063
  const args = current.getArguments();
2177
2064
  switch (methodName) {
@@ -2267,9 +2154,9 @@ async function parseAndGenerateNestResourceTemplate(options) {
2267
2154
  var require2 = createRequire(import.meta.url);
2268
2155
  async function run(options = {}) {
2269
2156
  let exitCode = 0;
2270
- const envPath = path2.resolve(process.cwd(), ".env");
2271
- if (fs4.existsSync(envPath)) {
2272
- loadEnv({ path: envPath });
2157
+ const envPath2 = path2.resolve(process.cwd(), ".env");
2158
+ if (fs4.existsSync(envPath2)) {
2159
+ loadEnv({ path: envPath2 });
2273
2160
  console.log("[gen-db-schema] \u2713 Loaded .env file");
2274
2161
  }
2275
2162
  const databaseUrl = process.env.SUDA_DATABASE_URL;
@@ -2568,12 +2455,6 @@ function buildDefaultRules(opts) {
2568
2455
  to: ".gitignore",
2569
2456
  line: ".agent/"
2570
2457
  },
2571
- // 7a. 确保 .gitignore 包含 .npm_cache 目录
2572
- {
2573
- type: "add-line",
2574
- to: ".gitignore",
2575
- line: ".npm_cache"
2576
- },
2577
2458
  // 8. 同步 .spark_project 配置文件(总是覆盖)
2578
2459
  {
2579
2460
  from: "templates/.spark_project",
@@ -2626,9 +2507,7 @@ var SYNC_PROFILES = {
2626
2507
  "design-stack": defaultProfile,
2627
2508
  "nestjs-react-fullstack": defaultProfile,
2628
2509
  "vite-react": viteReactProfile,
2629
- html: emptyProfile,
2630
- // design-html 与 html 同语义:空 sync + 不激活 git hooks(彻底 no-op)
2631
- "design-html": emptyProfile
2510
+ html: emptyProfile
2632
2511
  };
2633
2512
  function genSyncConfig(stack, opts = {}) {
2634
2513
  const factory = stack && SYNC_PROFILES[stack] || defaultProfile;
@@ -5762,17 +5641,17 @@ function analyzeImports(sourceFile) {
5762
5641
  }
5763
5642
 
5764
5643
  // src/commands/migration/versions/v001_capability/code-migrator/analyzers/call-site-analyzer.ts
5765
- import { SyntaxKind as SyntaxKind5 } from "ts-morph";
5644
+ import { SyntaxKind as SyntaxKind4 } from "ts-morph";
5766
5645
  function analyzeCallSites(sourceFile, imports) {
5767
5646
  const callSites = [];
5768
5647
  const importMap = /* @__PURE__ */ new Map();
5769
5648
  for (const imp of imports) {
5770
5649
  importMap.set(imp.importName, imp.capabilityId);
5771
5650
  }
5772
- const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind5.CallExpression);
5651
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression);
5773
5652
  for (const callExpr of callExpressions) {
5774
5653
  const expression = callExpr.getExpression();
5775
- if (expression.getKind() === SyntaxKind5.Identifier) {
5654
+ if (expression.getKind() === SyntaxKind4.Identifier) {
5776
5655
  const functionName = expression.getText();
5777
5656
  const capabilityId = importMap.get(functionName);
5778
5657
  if (capabilityId) {
@@ -5785,11 +5664,11 @@ function analyzeCallSites(sourceFile, imports) {
5785
5664
  text: callExpr.getText()
5786
5665
  });
5787
5666
  }
5788
- } else if (expression.getKind() === SyntaxKind5.PropertyAccessExpression) {
5789
- const propAccess = expression.asKind(SyntaxKind5.PropertyAccessExpression);
5667
+ } else if (expression.getKind() === SyntaxKind4.PropertyAccessExpression) {
5668
+ const propAccess = expression.asKind(SyntaxKind4.PropertyAccessExpression);
5790
5669
  if (propAccess) {
5791
5670
  const objectExpr = propAccess.getExpression();
5792
- if (objectExpr.getKind() === SyntaxKind5.Identifier) {
5671
+ if (objectExpr.getKind() === SyntaxKind4.Identifier) {
5793
5672
  const objectName = objectExpr.getText();
5794
5673
  const capabilityId = importMap.get(objectName);
5795
5674
  if (capabilityId) {
@@ -6018,7 +5897,7 @@ function addInjection(sourceFile) {
6018
5897
  }
6019
5898
 
6020
5899
  // src/commands/migration/versions/v001_capability/code-migrator/transformers/call-site-transformer.ts
6021
- import { SyntaxKind as SyntaxKind6 } from "ts-morph";
5900
+ import { SyntaxKind as SyntaxKind5 } from "ts-morph";
6022
5901
  var DEFAULT_ACTION_NAME = "run";
6023
5902
  function generateNewCallText(capabilityId, actionName, args) {
6024
5903
  const argsText = args.trim() || "{}";
@@ -6033,19 +5912,19 @@ function transformCallSites(sourceFile, imports) {
6033
5912
  });
6034
5913
  }
6035
5914
  let replacedCount = 0;
6036
- const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind6.CallExpression);
5915
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind5.CallExpression);
6037
5916
  const sortedCalls = [...callExpressions].sort((a, b) => b.getStart() - a.getStart());
6038
5917
  for (const callExpr of sortedCalls) {
6039
5918
  const expression = callExpr.getExpression();
6040
5919
  let importInfo;
6041
- if (expression.getKind() === SyntaxKind6.Identifier) {
5920
+ if (expression.getKind() === SyntaxKind5.Identifier) {
6042
5921
  const functionName = expression.getText();
6043
5922
  importInfo = importMap.get(functionName);
6044
- } else if (expression.getKind() === SyntaxKind6.PropertyAccessExpression) {
6045
- const propAccess = expression.asKind(SyntaxKind6.PropertyAccessExpression);
5923
+ } else if (expression.getKind() === SyntaxKind5.PropertyAccessExpression) {
5924
+ const propAccess = expression.asKind(SyntaxKind5.PropertyAccessExpression);
6046
5925
  if (propAccess) {
6047
5926
  const objectExpr = propAccess.getExpression();
6048
- if (objectExpr.getKind() === SyntaxKind6.Identifier) {
5927
+ if (objectExpr.getKind() === SyntaxKind5.Identifier) {
6049
5928
  const objectName = objectExpr.getText();
6050
5929
  importInfo = importMap.get(objectName);
6051
5930
  }
@@ -7698,10 +7577,8 @@ function sanitizeStructuredLog(value) {
7698
7577
  delete sanitized.pid;
7699
7578
  return sanitized;
7700
7579
  }
7701
- var TRANSIENT_CONNECTION_ERROR_PATTERN = /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|proxy error|\[Proxy\] (?:Error:\s*$|Error during|Connection error|Non-connection error|Headers already sent|Service (?:recovered|did not recover))/i;
7702
7580
  function hasErrorInStdLines(lines) {
7703
- const filtered = lines.filter((line) => !TRANSIENT_CONNECTION_ERROR_PATTERN.test(line));
7704
- const combined = filtered.join("\n");
7581
+ const combined = lines.join("\n");
7705
7582
  if (!combined) return false;
7706
7583
  const strong = [
7707
7584
  /compiled with errors/i,
@@ -7720,7 +7597,7 @@ function hasErrorInStdLines(lines) {
7720
7597
  /\b0\s+errors?\b/i,
7721
7598
  /Server Error \d{3}/i
7722
7599
  ];
7723
- return filtered.some((line) => {
7600
+ return lines.some((line) => {
7724
7601
  const text = line.trim();
7725
7602
  if (!text) return false;
7726
7603
  if (ignorePatterns.some((re) => re.test(text))) return false;
@@ -8269,11 +8146,9 @@ var commands = [
8269
8146
  ];
8270
8147
 
8271
8148
  // src/index.ts
8272
- for (const filename of [".env.local", ".env"]) {
8273
- const envPath = path25.join(process.cwd(), filename);
8274
- if (fs29.existsSync(envPath)) {
8275
- dotenvConfig({ path: envPath });
8276
- }
8149
+ var envPath = path25.join(process.cwd(), ".env");
8150
+ if (fs29.existsSync(envPath)) {
8151
+ dotenvConfig({ path: envPath });
8277
8152
  }
8278
8153
  var __dirname = path25.dirname(fileURLToPath5(import.meta.url));
8279
8154
  var pkg = JSON.parse(fs29.readFileSync(path25.join(__dirname, "../package.json"), "utf-8"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.58",
3
+ "version": "1.1.59-alpha.0",
4
4
  "description": "CLI tool for fullstack template management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,5 +1,5 @@
1
1
  run = ["npm", "run", "dev"] # 默认 spark-cli dev
2
- hidden = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", ".claude", "tmp", ".spark_project", ".playwright-cli"]
2
+ hidden = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", "tmp", ".spark_project", ".playwright-cli"]
3
3
  lint = ["npm", "run", "lint"]
4
4
  test = ["npm", "run", "test"]
5
5
  genDbSchema = ["npm", "run", "gen:db-schema"]
@@ -13,4 +13,4 @@ run = ["npm", "run", "start"]
13
13
  [files.restrict]
14
14
  pathPatterns = ["client/src/api/gen", "package.json", ".spark_project", ".gitignore"]
15
15
  [files.hidden]
16
- pathPatterns = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", ".claude", "tmp", ".spark_project", ".playwright-cli"]
16
+ pathPatterns = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", "tmp", ".spark_project", ".playwright-cli"]
@@ -5,6 +5,10 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { spawn, execSync } = require('child_process');
7
7
  const readline = require('readline');
8
+ const {
9
+ createPreviewPhaseReporter,
10
+ waitForTcpReady,
11
+ } = require('./preview-startup-timing.cjs');
8
12
 
9
13
  // ── Project root ──────────────────────────────────────────────────────────────
10
14
  const PROJECT_ROOT = path.resolve(__dirname, '..');
@@ -78,6 +82,10 @@ function writeOutput(msg) {
78
82
  try { fs.write(1, msg, () => { _stdoutInFlight--; }); } catch { _stdoutInFlight--; }
79
83
  }
80
84
 
85
+ const previewPhaseReporter = createPreviewPhaseReporter({
86
+ write: (line) => writeOutput(`${line}\n`),
87
+ });
88
+
81
89
  /** Structured event log → terminal + dev.std.log + dev.log */
82
90
  function logEvent(level, name, message) {
83
91
  const msg = `[${timestamp()}] [${level}] [${name}] ${message}\n`;
@@ -141,7 +149,39 @@ function startProcess({ name, command, args, cleanupPort }) {
141
149
  entry.child = child;
142
150
 
143
151
  const startTime = Date.now();
152
+ const attempt = restartCount + 1;
144
153
  logEvent('INFO', name, `Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`);
154
+ const processPhase = name === 'server' ? 'backend_process_spawn' : 'client_process_spawn';
155
+ previewPhaseReporter.emit(processPhase, 'success', {
156
+ at_ms: startTime,
157
+ attempt,
158
+ exact: true,
159
+ });
160
+ if (name === 'server') {
161
+ void waitForTcpReady({
162
+ port: Number(SERVER_PORT),
163
+ started_at_ms: startTime,
164
+ interval_ms: 50,
165
+ timeout_ms: 120000,
166
+ should_continue: () => entry.child === child && !stopping,
167
+ }).then((result) => {
168
+ if (result.cancelled) return;
169
+ previewPhaseReporter.emit(
170
+ 'backend_tcp_ready',
171
+ result.ready ? 'success' : 'error',
172
+ { ...result, attempt, exact: false }
173
+ );
174
+ }).catch((error) => {
175
+ if (entry.child !== child || stopping) return;
176
+ previewPhaseReporter.emit('backend_tcp_ready', 'error', {
177
+ duration_ms: Date.now() - startTime,
178
+ attempt,
179
+ exact: false,
180
+ precision_ms: 50,
181
+ error: error instanceof Error ? error.message : String(error),
182
+ });
183
+ });
184
+ }
145
185
 
146
186
  // Pipe stdout and stderr through readline for timestamped logging
147
187
  const pipeLines = (stream) => {
@@ -264,15 +304,30 @@ function cleanStaleDist() {
264
304
  // ── Main ──────────────────────────────────────────────────────────────────────
265
305
  async function main() {
266
306
  logEvent('INFO', 'main', '========== Dev session started ==========');
307
+ previewPhaseReporter.emit('dev_orchestrator_start', 'success', { exact: true });
267
308
 
268
309
  cleanStaleDist();
269
310
 
270
311
  // Initialize action plugins
271
312
  writeOutput('\n🔌 Initializing action plugins...\n');
313
+ const actionPluginStartedAt = Date.now();
314
+ previewPhaseReporter.emit('action_plugin_init', 'start', {
315
+ at_ms: actionPluginStartedAt,
316
+ exact: true,
317
+ });
272
318
  try {
273
319
  execSync('fullstack-cli action-plugin init', { cwd: PROJECT_ROOT, stdio: 'inherit' });
320
+ previewPhaseReporter.emit('action_plugin_init', 'success', {
321
+ duration_ms: Date.now() - actionPluginStartedAt,
322
+ exact: true,
323
+ });
274
324
  writeOutput('✅ Action plugins initialized\n\n');
275
- } catch {
325
+ } catch (error) {
326
+ previewPhaseReporter.emit('action_plugin_init', 'error', {
327
+ duration_ms: Date.now() - actionPluginStartedAt,
328
+ exact: true,
329
+ error: error instanceof Error ? error.message : String(error),
330
+ });
276
331
  writeOutput('⚠️ Action plugin initialization failed, continuing anyway...\n\n');
277
332
  }
278
333
 
@@ -1,24 +1,2 @@
1
1
  #!/usr/bin/env bash
2
- # `npm run dev` 入口;按 SANDBOX_ID 是否非空判断运行环境:
3
- # - SANDBOX_ID 非空(沙箱平台注入应用所属沙箱 ID)→ 直接跑 dev.js
4
- # (保活 / restart loop / 文件日志 —— 沙箱生产形态)。脚本同步由平台 pod 启动阶段做过,
5
- # dev 入口不再额外 `npm run upgrade`。
6
- # - 否则(本地)→ 走 miaoda app sync 兜底 + 跑 dev-local.js:纯 stdout、崩了就崩、Agent 友好。
7
- # 显式想跑本地路径可用 `npm run dev:local`(绕过 SANDBOX_ID 判断)。
8
- set -euo pipefail
9
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
-
11
- if [ -n "${SANDBOX_ID:-}" ]; then
12
- exec node "$SCRIPT_DIR/dev.js" "$@"
13
- fi
14
-
15
- if [ ! -f "$SCRIPT_DIR/dev-local.js" ]; then
16
- echo "[dev] scripts/dev-local.js 缺失;先跑 \`npx -y @lark-apaas/miaoda-cli@latest app sync\` 同步平台脚本" >&2
17
- exit 1
18
- fi
19
-
20
- # 本地启动前先跑一次 miaoda app sync:同步 platform-controlled 内容 + 升 @lark-apaas/* 到
21
- # latest + 迁移老 npm scripts。沙箱不走这里(SANDBOX_ID 分支已经 exec return)。
22
- npx -y @lark-apaas/miaoda-cli@latest app sync || echo "[dev] miaoda app sync 失败,按现状继续" >&2
23
-
24
- exec node "$SCRIPT_DIR/dev-local.js" "$@"
2
+ exec node "$(dirname "$0")/dev.js" "$@"
@@ -23,19 +23,6 @@ function runCommand(command, args) {
23
23
  });
24
24
  }
25
25
 
26
- // 串行依次执行每个任务,全部跑完后再聚合退出码:
27
- // 降低并发资源占用、让各任务输出按顺序清晰可读,同时保留“一次暴露所有 lint 问题”的行为。
28
- async function runTasksSerially(taskSpecs) {
29
- let exitCode = 0;
30
- for (const [command, args] of taskSpecs) {
31
- const code = await runCommand(command, args);
32
- if (code !== 0) {
33
- exitCode = 1;
34
- }
35
- }
36
- return exitCode;
37
- }
38
-
39
26
  function normalizeProjectFile(filePath) {
40
27
  const absolutePath = path.isAbsolute(filePath)
41
28
  ? filePath
@@ -77,13 +64,13 @@ function isStylelintTarget(filePath) {
77
64
  }
78
65
 
79
66
  async function runDefaultLint() {
80
- const taskSpecs = [
81
- [getBinName('npm'), ['run', 'eslint']],
82
- [getBinName('npm'), ['run', 'type:check']],
83
- [getBinName('npm'), ['run', 'stylelint']],
84
- ];
85
-
86
- process.exit(await runTasksSerially(taskSpecs));
67
+ const code = await runCommand(getBinName('npx'), [
68
+ 'concurrently',
69
+ 'npm run eslint',
70
+ 'npm run type:check',
71
+ 'npm run stylelint',
72
+ ]);
73
+ process.exit(code);
87
74
  }
88
75
 
89
76
  async function runSelectiveLint(inputFiles) {
@@ -114,30 +101,31 @@ async function runSelectiveLint(inputFiles) {
114
101
  }
115
102
  }
116
103
 
117
- const taskSpecs = [];
104
+ const tasks = [];
118
105
 
119
106
  if (eslintFiles.length > 0) {
120
- taskSpecs.push([getBinName('npx'), ['eslint', '--quiet', ...eslintFiles]]);
107
+ tasks.push(runCommand(getBinName('npx'), ['eslint', '--quiet', ...eslintFiles]));
121
108
  }
122
109
 
123
110
  if (stylelintFiles.length > 0) {
124
- taskSpecs.push([getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]]);
111
+ tasks.push(runCommand(getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]));
125
112
  }
126
113
 
127
114
  if (clientTypeFiles.length > 0) {
128
- taskSpecs.push([getBinName('npm'), ['run', 'type:check:client']]);
115
+ tasks.push(runCommand(getBinName('npm'), ['run', 'type:check:client']));
129
116
  }
130
117
 
131
118
  if (serverTypeFiles.length > 0) {
132
- taskSpecs.push([getBinName('npm'), ['run', 'type:check:server']]);
119
+ tasks.push(runCommand(getBinName('npm'), ['run', 'type:check:server']));
133
120
  }
134
121
 
135
- if (taskSpecs.length === 0) {
122
+ if (tasks.length === 0) {
136
123
  console.log('[lint] No supported files matched for lint');
137
124
  process.exit(0);
138
125
  }
139
126
 
140
- process.exit(await runTasksSerially(taskSpecs));
127
+ const results = await Promise.all(tasks);
128
+ process.exit(results.some(code => code !== 0) ? 1 : 0);
141
129
  }
142
130
 
143
131
  async function main() {
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const net = require('net');
4
+
5
+ const PREFIX = '[MiaodaPreviewPhase] ';
6
+ const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,128}$/;
7
+
8
+ function resolveAppId(env) {
9
+ const basePath = env.CLIENT_BASE_PATH || '';
10
+ const match = /^\/(?:app|af\/p)\/([^/]+)/.exec(basePath);
11
+ return match ? match[1] : env.MIAODA_APP_ID || '';
12
+ }
13
+
14
+ function createPreviewPhaseReporter(options = {}) {
15
+ const env = options.env || process.env;
16
+ const write = options.write || (line => process.stdout.write(`${line}\n`));
17
+ const now = options.now || Date.now;
18
+
19
+ function emit(phase, status, detail = {}) {
20
+ const runId = env.MIAODA_PREVIEW_RUN_ID || '';
21
+ if (!RUN_ID_PATTERN.test(runId)) return false;
22
+ const atMs = detail.at_ms == null ? now() : detail.at_ms;
23
+ const event = {
24
+ ...detail,
25
+ schema_version: 1,
26
+ run_id: runId,
27
+ app_id: resolveAppId(env),
28
+ sandbox_id: env.SANDBOX_ID || '',
29
+ phase,
30
+ status,
31
+ at_ms: atMs,
32
+ };
33
+ write(`${PREFIX}${JSON.stringify(event)}`);
34
+ return true;
35
+ }
36
+
37
+ return { emit };
38
+ }
39
+
40
+ function connectTcpOnce({ host, port, timeout_ms }) {
41
+ return new Promise(resolve => {
42
+ const socket = net.createConnection({ host, port });
43
+ let settled = false;
44
+ const finish = ready => {
45
+ if (settled) return;
46
+ settled = true;
47
+ socket.destroy();
48
+ resolve(ready);
49
+ };
50
+ socket.setTimeout(timeout_ms, () => finish(false));
51
+ socket.once('connect', () => finish(true));
52
+ socket.once('error', () => finish(false));
53
+ });
54
+ }
55
+
56
+ async function waitForTcpReady(options) {
57
+ const host = options.host || '127.0.0.1';
58
+ const port = Number(options.port);
59
+ const timeoutMs = options.timeout_ms == null ? 120000 : options.timeout_ms;
60
+ const intervalMs = options.interval_ms == null ? 50 : options.interval_ms;
61
+ const now = options.now || Date.now;
62
+ const sleep =
63
+ options.sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
64
+ const connect = options.connect || connectTcpOnce;
65
+ const shouldContinue = options.should_continue || (() => true);
66
+ const startedAtMs =
67
+ options.started_at_ms == null ? now() : options.started_at_ms;
68
+ let attempts = 0;
69
+
70
+ while (now() - startedAtMs < timeoutMs) {
71
+ if (!shouldContinue()) {
72
+ const atMs = now();
73
+ return {
74
+ ready: false,
75
+ cancelled: true,
76
+ attempts,
77
+ at_ms: atMs,
78
+ duration_ms: atMs - startedAtMs,
79
+ precision_ms: intervalMs,
80
+ };
81
+ }
82
+ attempts += 1;
83
+ const ready = await connect({
84
+ host,
85
+ port,
86
+ timeout_ms: Math.max(1, Math.min(intervalMs, timeoutMs)),
87
+ });
88
+ const atMs = now();
89
+ if (!shouldContinue()) {
90
+ return {
91
+ ready: false,
92
+ cancelled: true,
93
+ attempts,
94
+ at_ms: atMs,
95
+ duration_ms: atMs - startedAtMs,
96
+ precision_ms: intervalMs,
97
+ };
98
+ }
99
+ if (ready) {
100
+ return {
101
+ ready: true,
102
+ attempts,
103
+ at_ms: atMs,
104
+ duration_ms: atMs - startedAtMs,
105
+ precision_ms: intervalMs,
106
+ };
107
+ }
108
+ const elapsed = now() - startedAtMs;
109
+ if (elapsed >= timeoutMs) break;
110
+ await sleep(Math.min(intervalMs, timeoutMs - elapsed));
111
+ }
112
+
113
+ const atMs = now();
114
+ return {
115
+ ready: false,
116
+ attempts,
117
+ at_ms: atMs,
118
+ duration_ms: atMs - startedAtMs,
119
+ precision_ms: intervalMs,
120
+ };
121
+ }
122
+
123
+ module.exports = {
124
+ createPreviewPhaseReporter,
125
+ waitForTcpReady,
126
+ };
@@ -6,13 +6,6 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
 
8
8
  const ROOT_DIR = path.resolve(__dirname, '..');
9
-
10
- // 加载提交的 .env,供读取构建期配置(如 MIAODA_RUNTIME_ENTRIES)。dotenv 不一定安装 → 静默降级。
11
- try {
12
- require('dotenv').config({ path: path.join(ROOT_DIR, '.env') });
13
- } catch {
14
- // dotenv 未安装:跳过;MIAODA_RUNTIME_ENTRIES 仍可来自真实环境变量
15
- }
16
9
  const DIST_DIR = path.join(ROOT_DIR, 'dist');
17
10
  const DIST_SERVER_DIR = path.join(DIST_DIR, 'server');
18
11
  const ROOT_PACKAGE_JSON = path.join(ROOT_DIR, 'package.json');
@@ -23,36 +16,6 @@ const OUT_PACKAGE_JSON = path.join(DIST_DIR, 'package.json');
23
16
  // Server 入口文件
24
17
  const SERVER_ENTRY = path.join(DIST_SERVER_DIR, 'main.js');
25
18
 
26
- // nest sourceRoot(源码根,默认 server):把 .env 里声明的源码路径映射到构建产物位置
27
- let SOURCE_ROOT = 'server';
28
- try {
29
- const nestCli = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'nest-cli.json'), 'utf8'));
30
- if (nestCli.sourceRoot) SOURCE_ROOT = nestCli.sourceRoot;
31
- } catch {
32
- // 读不到 nest-cli.json 则用默认 server
33
- }
34
- const SRC_TO_OUT_EXT = { '.mts': '.mjs', '.cts': '.cjs', '.ts': '.js' };
35
-
36
- // 额外 trace 入口:经 new Function/动态 import 加载、nft 无法从 main.js 静态发现的 ESM runtime。
37
- // 应用在提交的 .env 里用 MIAODA_RUNTIME_ENTRIES 声明【源码路径】(相对项目根,逗号分隔),例如:
38
- // MIAODA_RUNTIME_ENTRIES=server/modules/ethics/runtime/review-run.mjs,server/modules/ethics/runtime/parse-core.mjs
39
- // 脚本按 nest sourceRoot 自动映射到 dist/server 下的构建产物(并重写 .mts→.mjs / .ts→.js)。
40
- const RUNTIME_ENTRIES = (process.env.MIAODA_RUNTIME_ENTRIES || '')
41
- .split(',')
42
- .map((s) => s.trim())
43
- .filter(Boolean)
44
- .map((srcRel) => {
45
- const fromSrc = path.relative(SOURCE_ROOT, srcRel); // 去掉 sourceRoot 前缀
46
- const ext = path.extname(fromSrc);
47
- const outRel = SRC_TO_OUT_EXT[ext] ? fromSrc.slice(0, -ext.length) + SRC_TO_OUT_EXT[ext] : fromSrc;
48
- return path.join(DIST_SERVER_DIR, outRel);
49
- })
50
- .filter((p) => {
51
- if (fs.existsSync(p)) return true;
52
- console.warn(`⚠️ MIAODA_RUNTIME_ENTRIES 入口在构建产物中不存在,已忽略: ${path.relative(ROOT_DIR, p)}`);
53
- return false;
54
- });
55
-
56
19
  // Node.js 内置模块列表
57
20
  const BUILTIN_MODULES = new Set([
58
21
  'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns',
@@ -182,15 +145,12 @@ async function smartPrune() {
182
145
  }
183
146
 
184
147
  console.log(`📂 分析入口文件: ${path.relative(ROOT_DIR, SERVER_ENTRY)}`);
185
- if (RUNTIME_ENTRIES.length) {
186
- console.log(`📎 额外 trace 入口 (${RUNTIME_ENTRIES.length}): ${RUNTIME_ENTRIES.map((p) => path.relative(DIST_SERVER_DIR, p)).join(', ')}`);
187
- }
188
148
 
189
149
  // 2. 使用 @vercel/nft 追踪依赖
190
150
  console.log('🔎 追踪实际依赖...');
191
151
  const analyzeStart = Date.now();
192
152
 
193
- const { fileList } = await nodeFileTrace([SERVER_ENTRY, ...RUNTIME_ENTRIES], {
153
+ const { fileList } = await nodeFileTrace([SERVER_ENTRY], {
194
154
  base: ROOT_DIR,
195
155
  processCwd: ROOT_DIR,
196
156
  ts: false, // 禁用 TS 解析
@@ -1,113 +0,0 @@
1
- #!/usr/bin/env node
2
- // ============================================================================
3
- // 本地开发启动脚本(由 miaoda app sync 维护,请勿手改)
4
- // Stack: nestjs-react-fullstack
5
- //
6
- // 流程:
7
- // 1. env pull —— 拉沙箱身份/凭证到 .env.local
8
- // 2. skills sync —— 同步当前 stack 的 agent skills
9
- // 3. dotenv 加载 .env / .env.local 到 process.env(含 SUDA_WEBUSER 适配)
10
- // 4. 并发起 dev:server + dev:client(子进程继承 process.env)
11
- //
12
- // 关键设计:本脚本在 spawn 子进程之前先把 .env / .env.local 加载到 process.env,
13
- // 然后 spawn 的 server / client 进程通过 env 继承直接拿到——SDK(fullstack-nestjs-core
14
- // / fullstack-vite-preset / fullstack-rspack-preset)无需自己 require('dotenv')。
15
- //
16
- // SUDA_WEBUSER 适配:沙箱 env pull 下发到 .env.local 的形态是
17
- // `SUDA_WEBUSER="{\"user_id\":\"...\"}"`(shell-quoted JSON),dotenv@17 剥外层引号后
18
- // 保留内部 `\"` 转义不还原,导致 process.env.SUDA_WEBUSER 是 `{\"user_id\":\"...\"}`
19
- // 这种带反斜杠串,下游 JSON.parse 直接挂。这里做一次「直接 parse 失败则 unescape
20
- // 后重 parse」兜底,把容错收敛在启动期单点,下游拿到干净 JSON 字符串。
21
- // ============================================================================
22
- const fs = require('node:fs');
23
- const path = require('node:path');
24
- const { execSync, spawn, spawnSync } = require('node:child_process');
25
-
26
- process.chdir(path.resolve(__dirname, '..'));
27
-
28
- function warn(msg) {
29
- if (process.stderr.isTTY) process.stderr.write(`\x1b[33mWARNING: ${msg}\x1b[0m\n`);
30
- else process.stderr.write(`WARNING: ${msg}\n`);
31
- }
32
-
33
- if (!process.env.MIAODA_APP_TYPE) process.env.MIAODA_APP_TYPE = '3';
34
- process.env.MIAODA_LOCAL_DEV = '1';
35
-
36
- // 1. env pull
37
- console.log('[dev-local] (1/4) env pull...');
38
- const hasLarkCli = spawnSync('command', ['-v', 'lark-cli'], { shell: true, stdio: 'ignore' }).status === 0;
39
- if (hasLarkCli) {
40
- let appId = '';
41
- try {
42
- appId = JSON.parse(fs.readFileSync('.spark/meta.json', 'utf8')).app_id || '';
43
- } catch {
44
- /* meta.json 不存在或非法 JSON */
45
- }
46
- if (appId) {
47
- const r = spawnSync('lark-cli', ['apps', '+env-pull', '--app-id', appId, '--as', 'user'], {
48
- stdio: 'inherit',
49
- });
50
- if (r.status !== 0) warn('env pull 失败,继续按 .env.local 现状启动');
51
- } else {
52
- warn('.spark/meta.json 缺 app_id,请先跑 `miaoda app init --app-id <id>`');
53
- }
54
- } else {
55
- warn('lark-cli 未安装,跳过 env pull;请确保 .env.local 已就绪');
56
- }
57
-
58
- // 2. skills sync —— --local 切到 flat layout (.agents/skills + .claude/skills 软链),
59
- // 跟沙箱 nested layout 区分。不传 --version,handler 默认拉 coding-steering@latest,
60
- // 保证每次本地 npm run dev 都把 skills 升到最新。
61
- console.log('[dev-local] (2/4) miaoda skills sync...');
62
- try {
63
- execSync('npx -y @lark-apaas/miaoda-cli@latest skills sync --local', { stdio: 'inherit' });
64
- } catch {
65
- console.log(' (skills sync 失败,继续启动)');
66
- }
67
-
68
- // 3. 加载 .env / .env.local 到 process.env
69
- // dotenv 默认 override:false,先到先得 → 先 .env.local 让它优先于 .env;
70
- // shell env 已在 process.env,两次 config 都不会覆盖。
71
- console.log('[dev-local] (3/4) loading .env / .env.local...');
72
- const dotenv = require('dotenv');
73
- dotenv.config({ path: '.env.local' });
74
- dotenv.config({ path: '.env' });
75
-
76
- // SUDA_WEBUSER 适配(详见文件头注释)
77
- if (process.env.SUDA_WEBUSER) {
78
- const raw = process.env.SUDA_WEBUSER;
79
- try {
80
- JSON.parse(raw);
81
- } catch {
82
- try {
83
- const unescaped = raw.replace(/\\"/g, '"');
84
- JSON.parse(unescaped);
85
- process.env.SUDA_WEBUSER = unescaped;
86
- } catch {
87
- warn(`SUDA_WEBUSER 解析失败,值头部: ${raw.slice(0, 80)}...`);
88
- }
89
- }
90
- }
91
-
92
- // 4. 并发起前后端 dev server
93
- console.log('[dev-local] (4/4) 并发起 dev:server + dev:client');
94
- const child = spawn(
95
- 'npx',
96
- [
97
- '--no-install',
98
- 'concurrently',
99
- '--names',
100
- 'server,client',
101
- '--prefix-colors',
102
- 'blue,green',
103
- '--kill-others-on-fail',
104
- 'npm run dev:server',
105
- 'npm run dev:client',
106
- ],
107
- { stdio: 'inherit', env: process.env },
108
- );
109
- child.on('exit', (code) => process.exit(code ?? 0));
110
- child.on('error', (err) => {
111
- console.error(err);
112
- process.exit(1);
113
- });