@lark-apaas/fullstack-cli 1.1.59-alpha.3 → 1.1.59

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,6 +847,117 @@ 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
+
850
961
  // src/commands/db/gen-dbschema/transforms/ast/index.ts
851
962
  var defaultTransforms = [
852
963
  patchDefectsTransform,
@@ -865,6 +976,8 @@ var defaultTransforms = [
865
976
  // #9 Replace .defaultNow()
866
977
  removeSystemFieldsTransform,
867
978
  // #10 Remove conflicting system fields
979
+ fixOpclassTransform,
980
+ // #11 Fix multi-column index opclass mapping
868
981
  tweakImportsTransform
869
982
  // #12 Adjust imports
870
983
  ];
@@ -1901,7 +2014,7 @@ export class ${className}Module {}
1901
2014
  }
1902
2015
 
1903
2016
  // src/commands/db/gen-nest-resource/schema-parser.ts
1904
- import { Project as Project2, Node as Node9 } from "ts-morph";
2017
+ import { Project as Project2, Node as Node10 } from "ts-morph";
1905
2018
  var DrizzleSchemaParser = class {
1906
2019
  constructor(projectOptions) {
1907
2020
  this.project = new Project2(projectOptions);
@@ -1914,7 +2027,7 @@ var DrizzleSchemaParser = class {
1914
2027
  const declarations = statement.getDeclarations();
1915
2028
  for (const declaration of declarations) {
1916
2029
  const initializer = declaration.getInitializer();
1917
- if (initializer && Node9.isCallExpression(initializer)) {
2030
+ if (initializer && Node10.isCallExpression(initializer)) {
1918
2031
  const expression = initializer.getExpression();
1919
2032
  if (expression.getText() === "pgTable") {
1920
2033
  const tableInfo = this.parsePgTable(
@@ -1937,13 +2050,13 @@ var DrizzleSchemaParser = class {
1937
2050
  }
1938
2051
  const tableName = args[0].getText().replace(/['"]/g, "");
1939
2052
  const fieldsArg = args[1];
1940
- if (!Node9.isObjectLiteralExpression(fieldsArg)) {
2053
+ if (!Node10.isObjectLiteralExpression(fieldsArg)) {
1941
2054
  return null;
1942
2055
  }
1943
2056
  const fields = [];
1944
2057
  const properties = fieldsArg.getProperties();
1945
2058
  for (const prop of properties) {
1946
- if (Node9.isPropertyAssignment(prop)) {
2059
+ if (Node10.isPropertyAssignment(prop)) {
1947
2060
  const fieldName = prop.getName();
1948
2061
  const initializer = prop.getInitializer();
1949
2062
  const leadingComments = prop.getLeadingCommentRanges();
@@ -1951,7 +2064,7 @@ var DrizzleSchemaParser = class {
1951
2064
  if (leadingComments.length > 0) {
1952
2065
  comment = leadingComments.map((c) => c.getText()).join("\n").replace(/\/\//g, "").trim();
1953
2066
  }
1954
- if (initializer && Node9.isCallExpression(initializer)) {
2067
+ if (initializer && Node10.isCallExpression(initializer)) {
1955
2068
  const fieldInfo = this.parseField(fieldName, initializer, comment);
1956
2069
  fields.push(fieldInfo);
1957
2070
  }
@@ -1983,10 +2096,10 @@ var DrizzleSchemaParser = class {
1983
2096
  parseBaseType(callExpr, fieldInfo) {
1984
2097
  let current = callExpr;
1985
2098
  let baseCall = null;
1986
- while (Node9.isCallExpression(current)) {
2099
+ while (Node10.isCallExpression(current)) {
1987
2100
  baseCall = current;
1988
2101
  const expression2 = current.getExpression();
1989
- if (Node9.isPropertyAccessExpression(expression2)) {
2102
+ if (Node10.isPropertyAccessExpression(expression2)) {
1990
2103
  current = expression2.getExpression();
1991
2104
  } else {
1992
2105
  break;
@@ -1997,7 +2110,7 @@ var DrizzleSchemaParser = class {
1997
2110
  }
1998
2111
  const expression = baseCall.getExpression();
1999
2112
  let typeName = "";
2000
- if (Node9.isPropertyAccessExpression(expression)) {
2113
+ if (Node10.isPropertyAccessExpression(expression)) {
2001
2114
  typeName = expression.getName();
2002
2115
  } else {
2003
2116
  typeName = expression.getText();
@@ -2006,25 +2119,25 @@ var DrizzleSchemaParser = class {
2006
2119
  const args = baseCall.getArguments();
2007
2120
  if (args.length > 0) {
2008
2121
  const firstArg = args[0];
2009
- if (Node9.isStringLiteral(firstArg)) {
2122
+ if (Node10.isStringLiteral(firstArg)) {
2010
2123
  fieldInfo.columnName = firstArg.getLiteralText();
2011
- } else if (Node9.isObjectLiteralExpression(firstArg)) {
2124
+ } else if (Node10.isObjectLiteralExpression(firstArg)) {
2012
2125
  this.parseTypeConfig(firstArg, fieldInfo);
2013
- } else if (Node9.isArrayLiteralExpression(firstArg)) {
2126
+ } else if (Node10.isArrayLiteralExpression(firstArg)) {
2014
2127
  fieldInfo.enumValues = firstArg.getElements().map((el) => el.getText().replace(/['"]/g, ""));
2015
2128
  }
2016
2129
  }
2017
- if (args.length > 1 && Node9.isObjectLiteralExpression(args[1])) {
2130
+ if (args.length > 1 && Node10.isObjectLiteralExpression(args[1])) {
2018
2131
  this.parseTypeConfig(args[1], fieldInfo);
2019
2132
  }
2020
2133
  }
2021
2134
  parseTypeConfig(objLiteral, fieldInfo) {
2022
- if (!Node9.isObjectLiteralExpression(objLiteral)) {
2135
+ if (!Node10.isObjectLiteralExpression(objLiteral)) {
2023
2136
  return;
2024
2137
  }
2025
2138
  const properties = objLiteral.getProperties();
2026
2139
  for (const prop of properties) {
2027
- if (Node9.isPropertyAssignment(prop)) {
2140
+ if (Node10.isPropertyAssignment(prop)) {
2028
2141
  const propName = prop.getName();
2029
2142
  const value = prop.getInitializer()?.getText();
2030
2143
  switch (propName) {
@@ -2056,9 +2169,9 @@ var DrizzleSchemaParser = class {
2056
2169
  }
2057
2170
  parseCallChain(callExpr, fieldInfo) {
2058
2171
  let current = callExpr;
2059
- while (Node9.isCallExpression(current)) {
2172
+ while (Node10.isCallExpression(current)) {
2060
2173
  const expression = current.getExpression();
2061
- if (Node9.isPropertyAccessExpression(expression)) {
2174
+ if (Node10.isPropertyAccessExpression(expression)) {
2062
2175
  const methodName = expression.getName();
2063
2176
  const args = current.getArguments();
2064
2177
  switch (methodName) {
@@ -2154,9 +2267,9 @@ async function parseAndGenerateNestResourceTemplate(options) {
2154
2267
  var require2 = createRequire(import.meta.url);
2155
2268
  async function run(options = {}) {
2156
2269
  let exitCode = 0;
2157
- const envPath2 = path2.resolve(process.cwd(), ".env");
2158
- if (fs4.existsSync(envPath2)) {
2159
- loadEnv({ path: envPath2 });
2270
+ const envPath = path2.resolve(process.cwd(), ".env");
2271
+ if (fs4.existsSync(envPath)) {
2272
+ loadEnv({ path: envPath });
2160
2273
  console.log("[gen-db-schema] \u2713 Loaded .env file");
2161
2274
  }
2162
2275
  const databaseUrl = process.env.SUDA_DATABASE_URL;
@@ -2455,6 +2568,12 @@ function buildDefaultRules(opts) {
2455
2568
  to: ".gitignore",
2456
2569
  line: ".agent/"
2457
2570
  },
2571
+ // 7a. 确保 .gitignore 包含 .npm_cache 目录
2572
+ {
2573
+ type: "add-line",
2574
+ to: ".gitignore",
2575
+ line: ".npm_cache"
2576
+ },
2458
2577
  // 8. 同步 .spark_project 配置文件(总是覆盖)
2459
2578
  {
2460
2579
  from: "templates/.spark_project",
@@ -2507,7 +2626,9 @@ var SYNC_PROFILES = {
2507
2626
  "design-stack": defaultProfile,
2508
2627
  "nestjs-react-fullstack": defaultProfile,
2509
2628
  "vite-react": viteReactProfile,
2510
- html: emptyProfile
2629
+ html: emptyProfile,
2630
+ // design-html 与 html 同语义:空 sync + 不激活 git hooks(彻底 no-op)
2631
+ "design-html": emptyProfile
2511
2632
  };
2512
2633
  function genSyncConfig(stack, opts = {}) {
2513
2634
  const factory = stack && SYNC_PROFILES[stack] || defaultProfile;
@@ -5641,17 +5762,17 @@ function analyzeImports(sourceFile) {
5641
5762
  }
5642
5763
 
5643
5764
  // src/commands/migration/versions/v001_capability/code-migrator/analyzers/call-site-analyzer.ts
5644
- import { SyntaxKind as SyntaxKind4 } from "ts-morph";
5765
+ import { SyntaxKind as SyntaxKind5 } from "ts-morph";
5645
5766
  function analyzeCallSites(sourceFile, imports) {
5646
5767
  const callSites = [];
5647
5768
  const importMap = /* @__PURE__ */ new Map();
5648
5769
  for (const imp of imports) {
5649
5770
  importMap.set(imp.importName, imp.capabilityId);
5650
5771
  }
5651
- const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression);
5772
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind5.CallExpression);
5652
5773
  for (const callExpr of callExpressions) {
5653
5774
  const expression = callExpr.getExpression();
5654
- if (expression.getKind() === SyntaxKind4.Identifier) {
5775
+ if (expression.getKind() === SyntaxKind5.Identifier) {
5655
5776
  const functionName = expression.getText();
5656
5777
  const capabilityId = importMap.get(functionName);
5657
5778
  if (capabilityId) {
@@ -5664,11 +5785,11 @@ function analyzeCallSites(sourceFile, imports) {
5664
5785
  text: callExpr.getText()
5665
5786
  });
5666
5787
  }
5667
- } else if (expression.getKind() === SyntaxKind4.PropertyAccessExpression) {
5668
- const propAccess = expression.asKind(SyntaxKind4.PropertyAccessExpression);
5788
+ } else if (expression.getKind() === SyntaxKind5.PropertyAccessExpression) {
5789
+ const propAccess = expression.asKind(SyntaxKind5.PropertyAccessExpression);
5669
5790
  if (propAccess) {
5670
5791
  const objectExpr = propAccess.getExpression();
5671
- if (objectExpr.getKind() === SyntaxKind4.Identifier) {
5792
+ if (objectExpr.getKind() === SyntaxKind5.Identifier) {
5672
5793
  const objectName = objectExpr.getText();
5673
5794
  const capabilityId = importMap.get(objectName);
5674
5795
  if (capabilityId) {
@@ -5897,7 +6018,7 @@ function addInjection(sourceFile) {
5897
6018
  }
5898
6019
 
5899
6020
  // src/commands/migration/versions/v001_capability/code-migrator/transformers/call-site-transformer.ts
5900
- import { SyntaxKind as SyntaxKind5 } from "ts-morph";
6021
+ import { SyntaxKind as SyntaxKind6 } from "ts-morph";
5901
6022
  var DEFAULT_ACTION_NAME = "run";
5902
6023
  function generateNewCallText(capabilityId, actionName, args) {
5903
6024
  const argsText = args.trim() || "{}";
@@ -5912,19 +6033,19 @@ function transformCallSites(sourceFile, imports) {
5912
6033
  });
5913
6034
  }
5914
6035
  let replacedCount = 0;
5915
- const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind5.CallExpression);
6036
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind6.CallExpression);
5916
6037
  const sortedCalls = [...callExpressions].sort((a, b) => b.getStart() - a.getStart());
5917
6038
  for (const callExpr of sortedCalls) {
5918
6039
  const expression = callExpr.getExpression();
5919
6040
  let importInfo;
5920
- if (expression.getKind() === SyntaxKind5.Identifier) {
6041
+ if (expression.getKind() === SyntaxKind6.Identifier) {
5921
6042
  const functionName = expression.getText();
5922
6043
  importInfo = importMap.get(functionName);
5923
- } else if (expression.getKind() === SyntaxKind5.PropertyAccessExpression) {
5924
- const propAccess = expression.asKind(SyntaxKind5.PropertyAccessExpression);
6044
+ } else if (expression.getKind() === SyntaxKind6.PropertyAccessExpression) {
6045
+ const propAccess = expression.asKind(SyntaxKind6.PropertyAccessExpression);
5925
6046
  if (propAccess) {
5926
6047
  const objectExpr = propAccess.getExpression();
5927
- if (objectExpr.getKind() === SyntaxKind5.Identifier) {
6048
+ if (objectExpr.getKind() === SyntaxKind6.Identifier) {
5928
6049
  const objectName = objectExpr.getText();
5929
6050
  importInfo = importMap.get(objectName);
5930
6051
  }
@@ -7577,8 +7698,10 @@ function sanitizeStructuredLog(value) {
7577
7698
  delete sanitized.pid;
7578
7699
  return sanitized;
7579
7700
  }
7701
+ var TRANSIENT_CONNECTION_ERROR_PATTERN = /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|socket hang up|proxy error|\[Proxy\] (?:Error:\s*$|Error during|Connection error|Non-connection error|Headers already sent|Service (?:recovered|did not recover))/i;
7580
7702
  function hasErrorInStdLines(lines) {
7581
- const combined = lines.join("\n");
7703
+ const filtered = lines.filter((line) => !TRANSIENT_CONNECTION_ERROR_PATTERN.test(line));
7704
+ const combined = filtered.join("\n");
7582
7705
  if (!combined) return false;
7583
7706
  const strong = [
7584
7707
  /compiled with errors/i,
@@ -7597,7 +7720,7 @@ function hasErrorInStdLines(lines) {
7597
7720
  /\b0\s+errors?\b/i,
7598
7721
  /Server Error \d{3}/i
7599
7722
  ];
7600
- return lines.some((line) => {
7723
+ return filtered.some((line) => {
7601
7724
  const text = line.trim();
7602
7725
  if (!text) return false;
7603
7726
  if (ignorePatterns.some((re) => re.test(text))) return false;
@@ -8146,9 +8269,11 @@ var commands = [
8146
8269
  ];
8147
8270
 
8148
8271
  // src/index.ts
8149
- var envPath = path25.join(process.cwd(), ".env");
8150
- if (fs29.existsSync(envPath)) {
8151
- dotenvConfig({ path: envPath });
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
+ }
8152
8277
  }
8153
8278
  var __dirname = path25.dirname(fileURLToPath5(import.meta.url));
8154
8279
  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.59-alpha.3",
3
+ "version": "1.1.59",
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", "tmp", ".spark_project", ".playwright-cli"]
2
+ hidden = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", ".claude", "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", "tmp", ".spark_project", ".playwright-cli"]
16
+ pathPatterns = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", ".claude", "tmp", ".spark_project", ".playwright-cli"]
@@ -21,9 +21,5 @@
21
21
  }
22
22
  }
23
23
  ]
24
- },
25
- "x-miaoda-preview": {
26
- "compiler": "swc",
27
- "typeCheck": "deferred"
28
24
  }
29
- }
25
+ }
@@ -0,0 +1,113 @@
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
+ });