@lark-apaas/miaoda-cli 0.1.6 → 0.1.7-alpha.eb0aa5c

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.
Files changed (42) hide show
  1. package/dist/cli/commands/app/index.js +67 -7
  2. package/dist/cli/commands/index.js +36 -1
  3. package/dist/cli/commands/skills/index.js +18 -2
  4. package/dist/cli/handlers/app/index.js +4 -1
  5. package/dist/cli/handlers/app/init.js +53 -9
  6. package/dist/cli/handlers/app/sync.js +220 -0
  7. package/dist/cli/handlers/skills/sync.js +15 -4
  8. package/dist/config/fullstack-cli-pin.js +13 -0
  9. package/dist/config/sync-configs/design-stack.js +98 -0
  10. package/dist/config/sync-configs/index.js +62 -0
  11. package/dist/config/sync-configs/nestjs-react-fullstack.js +177 -0
  12. package/dist/config/sync.js +14 -0
  13. package/dist/services/app/init/install.js +35 -13
  14. package/dist/services/app/init/template.js +23 -6
  15. package/dist/utils/coding-steering.js +107 -28
  16. package/dist/utils/file-ops.js +45 -0
  17. package/dist/utils/githooks.js +55 -0
  18. package/dist/utils/merge-json.js +63 -0
  19. package/dist/utils/platform-sync.js +160 -0
  20. package/dist/utils/sync-rule.js +295 -0
  21. package/package.json +5 -3
  22. package/upgrade/templates/README.md +34 -0
  23. package/upgrade/templates/design-stack/templates/.githooks/pre-commit +4 -0
  24. package/upgrade/templates/design-stack/templates/scripts/dev-local.js +83 -0
  25. package/upgrade/templates/design-stack/templates/scripts/dev.sh +25 -0
  26. package/upgrade/templates/design-stack/templates/scripts/hooks/run-precommit.js +37 -0
  27. package/upgrade/templates/nestjs-react-fullstack/templates/.githooks/pre-commit +4 -0
  28. package/upgrade/templates/nestjs-react-fullstack/templates/.gitignore.append +8 -0
  29. package/upgrade/templates/nestjs-react-fullstack/templates/.spark_project +16 -0
  30. package/upgrade/templates/nestjs-react-fullstack/templates/drizzle.config.ts +55 -0
  31. package/upgrade/templates/nestjs-react-fullstack/templates/helper/gen-openapi.ts +34 -0
  32. package/upgrade/templates/nestjs-react-fullstack/templates/nest-cli.json +25 -0
  33. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/build.sh +207 -0
  34. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev-local.js +111 -0
  35. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.js +295 -0
  36. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.sh +25 -0
  37. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/hooks/run-precommit.js +37 -0
  38. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +150 -0
  39. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/prune-smart.js +330 -0
  40. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/run.sh +8 -0
  41. package/upgrade/templates/nestjs-react-fullstack/templates/server/global.d.ts +19 -0
  42. package/upgrade/templates/nestjs-react-fullstack/templates/tsconfig.node.json +5 -0
@@ -0,0 +1,34 @@
1
+ # upgrade/templates/
2
+
3
+ 平台管控的脚手架增量内容,按 stack 分目录,由 `miaoda app upgrade` 同步到用户项目。
4
+
5
+ ## 目录约定
6
+
7
+ ```
8
+ upgrade/
9
+ └── templates/
10
+ └── <stack>/
11
+ ├── files/ ← 文件模式:按相对路径整文件覆盖
12
+ │ └── <rel-path> ← 例如 scripts/dev-local.js → <app>/scripts/dev-local.js
13
+ └── patches/ ← JSON merge 模式:按相对路径 deep merge(任意 JSON 文件)
14
+ └── <rel-path>.json ← 例如 package.json / tsconfig.json / nest-cli.json
15
+ ```
16
+
17
+ ## 同步行为
18
+
19
+ - **files/**:递归遍历,按相对路径全量覆盖 targetDir。`.sh` 后缀文件同步后自动 `chmod +x`。
20
+ - **patches/**:递归遍历,**每个 `.json` 文件** 都跟 targetDir 同路径文件做 deep merge:
21
+ - 两边都必须是 JSON object(否则报错)
22
+ - value 是 object → 递归 merge;value 是数组 / 标量 → 整体替换
23
+ - 目标文件不存在则报错(patches 是补丁性质,不负责创建文件;app init 应该先创建)
24
+ - patches 下非 `.json` 文件会被跳过(预留给将来其它 patch 类型)
25
+ - 不做用户改动保护(本期约定:`upgrade/templates/` 下的文件路径由平台拥有,用户不应手改)。
26
+
27
+ ## 跟 `miaoda skills sync` 的区别
28
+
29
+ | 链路 | 源 | 内容 | 目标 |
30
+ |---|---|---|---|
31
+ | `miaoda skills sync` | NPM 包 `@lark-apaas/coding-steering` | agent skills(`.md`) | `<app>/.agent/skills/steering/<stack>/` |
32
+ | `miaoda app upgrade` | 本仓 `upgrade/templates/<stack>/` | 启动脚本、package.json 字段等 | `<app>/` 项目根 |
33
+
34
+ 两者独立调用,互不依赖。
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ [ "$SKIP_GIT_HOOKS" = "1" ] && exit 0
3
+ export PATH="node_modules/.bin:$PATH"
4
+ npm run precommit
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================================
3
+ // 本地开发启动脚本(由 miaoda app sync 维护,请勿手改)
4
+ // Stack: design-stack
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(design-stack 单进程,无 server/client 拆分)
11
+ //
12
+ // 设计同 nestjs-react-fullstack/dev-local.js,SDK 包不需要自己 require('dotenv'),
13
+ // env 加载收敛在启动脚本单点。SUDA_WEBUSER 适配同 nrf 那份。
14
+ // ============================================================================
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+ const { execSync, spawn, spawnSync } = require('node:child_process');
18
+
19
+ process.chdir(path.resolve(__dirname, '..'));
20
+
21
+ function warn(msg) {
22
+ if (process.stderr.isTTY) process.stderr.write(`\x1b[33mWARNING: ${msg}\x1b[0m\n`);
23
+ else process.stderr.write(`WARNING: ${msg}\n`);
24
+ }
25
+
26
+ if (!process.env.MIAODA_APP_TYPE) process.env.MIAODA_APP_TYPE = '4';
27
+ process.env.MIAODA_LOCAL_DEV = '1';
28
+
29
+ console.log('[dev-local] (1/4) env pull...');
30
+ const hasLarkCli = spawnSync('command', ['-v', 'lark-cli'], { shell: true, stdio: 'ignore' }).status === 0;
31
+ if (hasLarkCli) {
32
+ let appId = '';
33
+ try {
34
+ appId = JSON.parse(fs.readFileSync('.spark/meta.json', 'utf8')).app_id || '';
35
+ } catch {
36
+ /* meta.json 不存在或非法 JSON */
37
+ }
38
+ if (appId) {
39
+ const r = spawnSync('lark-cli', ['apps', '+env-pull', '--app-id', appId, '--as', 'user'], {
40
+ stdio: 'inherit',
41
+ });
42
+ if (r.status !== 0) warn('env pull 失败,继续按 .env.local 现状启动');
43
+ } else {
44
+ warn('.spark/meta.json 缺 app_id,请先跑 `miaoda app init --app-id <id>`');
45
+ }
46
+ } else {
47
+ warn('lark-cli 未安装,跳过 env pull;请确保 .env.local 已就绪');
48
+ }
49
+
50
+ console.log('[dev-local] (2/4) miaoda skills sync...');
51
+ try {
52
+ execSync('npx -y @lark-apaas/miaoda-cli@latest skills sync', { stdio: 'inherit' });
53
+ } catch {
54
+ console.log(' (skills sync 失败,继续启动)');
55
+ }
56
+
57
+ console.log('[dev-local] (3/4) loading .env / .env.local...');
58
+ const dotenv = require('dotenv');
59
+ dotenv.config({ path: '.env.local' });
60
+ dotenv.config({ path: '.env' });
61
+
62
+ if (process.env.SUDA_WEBUSER) {
63
+ const raw = process.env.SUDA_WEBUSER;
64
+ try {
65
+ JSON.parse(raw);
66
+ } catch {
67
+ try {
68
+ const unescaped = raw.replace(/\\"/g, '"');
69
+ JSON.parse(unescaped);
70
+ process.env.SUDA_WEBUSER = unescaped;
71
+ } catch {
72
+ warn(`SUDA_WEBUSER 解析失败,值头部: ${raw.slice(0, 80)}...`);
73
+ }
74
+ }
75
+ }
76
+
77
+ console.log('[dev-local] (4/4) npm run dev');
78
+ const child = spawn('npm', ['run', 'dev'], { stdio: 'inherit', env: process.env });
79
+ child.on('exit', (code) => process.exit(code ?? 0));
80
+ child.on('error', (err) => {
81
+ console.error(err);
82
+ process.exit(1);
83
+ });
@@ -0,0 +1,25 @@
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 不依赖用户全局装 miaoda;跟 latest dist-tag。
23
+ npx -y @lark-apaas/miaoda-cli@latest app sync || echo "[dev] miaoda app sync 失败,按现状继续" >&2
24
+
25
+ exec node "$SCRIPT_DIR/dev-local.js" "$@"
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ // FULLSTACK_PRECOMMIT_V1
3
+ 'use strict';
4
+
5
+ const { spawnSync } = require('node:child_process');
6
+
7
+ const SEP = ' ' + '─'.repeat(36);
8
+
9
+ function failAndExit(step, body) {
10
+ process.stderr.write('\n✗ pre-commit failed: ' + step + '\n');
11
+ process.stderr.write(SEP + '\n');
12
+ if (body && body.length > 0) {
13
+ process.stderr.write(body.replace(/\s+$/, '') + '\n');
14
+ }
15
+ process.stderr.write(SEP + '\n');
16
+ process.stderr.write(' bypass: git commit --no-verify\n');
17
+ process.exit(1);
18
+ }
19
+
20
+ function runLint() {
21
+ const cwd = process.cwd();
22
+ const res = spawnSync('npm', ['run', 'lint'], {
23
+ cwd,
24
+ stdio: ['ignore', 'pipe', 'pipe'],
25
+ env: process.env,
26
+ });
27
+ if (res.error) {
28
+ failAndExit('lint', String(res.error.message || res.error));
29
+ }
30
+ if (res.status !== 0) {
31
+ const stdout = res.stdout ? res.stdout.toString() : '';
32
+ const stderr = res.stderr ? res.stderr.toString() : '';
33
+ failAndExit('lint', stdout + '\n' + stderr);
34
+ }
35
+ }
36
+
37
+ runLint();
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ [ "$SKIP_GIT_HOOKS" = "1" ] && exit 0
3
+ export PATH="node_modules/.bin:$PATH"
4
+ npm run precommit
@@ -0,0 +1,8 @@
1
+
2
+ # ==========================================
3
+ # fullstack-cli managed files
4
+ # ==========================================
5
+ # scripts/ directory is auto-generated
6
+ # DO NOT modify manually
7
+ scripts/
8
+
@@ -0,0 +1,16 @@
1
+ run = ["npm", "run", "dev"] # 默认 spark-cli dev
2
+ hidden = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", "tmp", ".spark_project", ".playwright-cli"]
3
+ lint = ["npm", "run", "lint"]
4
+ test = ["npm", "run", "test"]
5
+ genDbSchema = ["npm", "run", "gen:db-schema"]
6
+ genOpenApiClient = ["npm", "run", "gen:openapi"]
7
+
8
+ [deployment]
9
+ build = ["npm", "run", "build"]
10
+ run = ["npm", "run", "start"]
11
+
12
+ [files]
13
+ [files.restrict]
14
+ pathPatterns = ["client/src/api/gen", "package.json", ".spark_project", ".gitignore"]
15
+ [files.hidden]
16
+ pathPatterns = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", "tmp", ".spark_project", ".playwright-cli"]
@@ -0,0 +1,55 @@
1
+ import { defineConfig, Config } from 'drizzle-kit';
2
+ require('dotenv').config();
3
+
4
+ const outputDir = process.env.__DRIZZLE_OUT_DIR__ || './tmp/.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);
@@ -0,0 +1,34 @@
1
+ // This file is auto-generated by @lark-apaas/fullstack-cli
2
+ import { NestFactory } from '@nestjs/core';
3
+ import { DevToolsV2Module } from '@lark-apaas/fullstack-nestjs-core';
4
+
5
+ import { AppModule } from '../server/app.module';
6
+
7
+ /**
8
+ * 生成 OpenAPI 文档和客户端 SDK
9
+ *
10
+ * 此文件由 @lark-apaas/fullstack-cli 自动派生,请勿手动修改
11
+ * 每次运行 npm install 时会自动更新
12
+ */
13
+ async function generateOpenApi() {
14
+ const app = await NestFactory.create(AppModule, { logger: false });
15
+ const basePath = process.env.CLIENT_BASE_PATH;
16
+
17
+ if (basePath) {
18
+ app.setGlobalPrefix(basePath);
19
+ }
20
+
21
+ await DevToolsV2Module.mount(app, {
22
+ basePath,
23
+ docsPath: '/api_docs',
24
+ needSetupServer: false,
25
+ needGenerateClientSdk: true,
26
+ });
27
+
28
+ process.exit(0); // 主动退出进程,不等待 app 关闭(用户存在场景,未释放 timer 导致 app 卡死问题)
29
+ }
30
+
31
+ generateOpenApi().catch((err) => {
32
+ console.error('[OpenAPI] Failed to generate schema', err);
33
+ process.exitCode = 1;
34
+ });
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "server",
5
+ "compilerOptions": {
6
+ "deleteOutDir": false,
7
+ "tsConfigPath": "tsconfig.node.json",
8
+ "assets": [
9
+ {
10
+ "include": "capabilities/**/*.json",
11
+ "outDir": "dist/server",
12
+ "watchAssets": true
13
+ }
14
+ ],
15
+ "plugins": [
16
+ {
17
+ "name": "@nestjs/swagger",
18
+ "options": {
19
+ "introspectComments": true,
20
+ "classValidatorShim": true
21
+ }
22
+ }
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bash
2
+ # This file is auto-generated by @lark-apaas/fullstack-cli
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(pwd)"
6
+ DIST_DIR="$ROOT_DIR/dist"
7
+
8
+ # 记录总开始时间
9
+ TOTAL_START=$(node -e "console.log(Date.now())")
10
+
11
+ # 打印耗时的辅助函数
12
+ print_time() {
13
+ local start=$1
14
+ local end=$(node -e "console.log(Date.now())")
15
+ local elapsed=$((end - start))
16
+ local seconds=$((elapsed / 1000))
17
+ local ms=$((elapsed % 1000))
18
+ echo " ⏱️ 耗时: ${seconds}.$(printf "%03d" $ms)s"
19
+ }
20
+
21
+ # ==================== 步骤 0 ====================
22
+ echo "🗑️ [0/6] 安装插件"
23
+ STEP_START=$(node -e "console.log(Date.now())")
24
+ npx fullstack-cli action-plugin init
25
+ print_time $STEP_START
26
+ echo ""
27
+
28
+ # ==================== 步骤 1 ====================
29
+ echo "📝 [1/6] 更新 openapi 代码"
30
+ STEP_START=$(node -e "console.log(Date.now())")
31
+ npm run gen:openapi
32
+ print_time $STEP_START
33
+ echo ""
34
+
35
+ # ==================== 步骤 2 ====================
36
+ echo "🗑️ [2/6] 清理 dist 目录"
37
+ STEP_START=$(node -e "console.log(Date.now())")
38
+ rm -rf "$ROOT_DIR/dist"
39
+ print_time $STEP_START
40
+ echo ""
41
+
42
+ # ==================== 步骤 3 ====================
43
+ echo "🗺️ [3/6] 生成路由定义"
44
+ STEP_START=$(node -e "console.log(Date.now())")
45
+
46
+ # 在 client/server 构建之前生成到 dist/,供 DefinePlugin 注入前端 bundle
47
+ # 注意:nest-cli.json 中 deleteOutDir 必须为 false(模板默认值),否则 nest build 会清掉 dist/
48
+ echo " ├─ 生成 API 路由定义..."
49
+ npx generate-api-routes --server-dir ./server --out-dir ./dist > /tmp/gen-api-routes.log 2>&1 &
50
+ API_ROUTES_PID=$!
51
+
52
+ echo " ├─ 生成页面路由定义..."
53
+ npx generate-page-routes --app-path ./client/src/app.tsx --out-dir ./dist > /tmp/gen-page-routes.log 2>&1 &
54
+ PAGE_ROUTES_PID=$!
55
+
56
+ API_ROUTES_EXIT=0
57
+ PAGE_ROUTES_EXIT=0
58
+
59
+ wait $API_ROUTES_PID || API_ROUTES_EXIT=$?
60
+ wait $PAGE_ROUTES_PID || PAGE_ROUTES_EXIT=$?
61
+
62
+ if [ $API_ROUTES_EXIT -ne 0 ]; then
63
+ echo " ⚠️ API 路由生成失败(不影响构建)"
64
+ cat /tmp/gen-api-routes.log
65
+ else
66
+ echo " ✅ API 路由生成完成"
67
+ fi
68
+
69
+ if [ $PAGE_ROUTES_EXIT -ne 0 ]; then
70
+ echo " ⚠️ 页面路由生成失败(不影响构建)"
71
+ cat /tmp/gen-page-routes.log
72
+ else
73
+ echo " ✅ 页面路由生成完成"
74
+ fi
75
+ print_time $STEP_START
76
+ echo ""
77
+
78
+ # ==================== 步骤 4 ====================
79
+ echo "🔨 [4/6] 并行构建 server 和 client"
80
+ STEP_START=$(node -e "console.log(Date.now())")
81
+
82
+ # 给 server/client 构建子进程预留 8GB heap,缓解 vite build transform 阶段 OOM
83
+ # (典型错误:Reached heap limit Allocation failed)。
84
+ # 仅在外部未设置 NODE_OPTIONS 时注入,允许 CI / 用户通过外部环境变量完全覆盖
85
+ BUILD_NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=8192}"
86
+
87
+ # 根据 only_frontend_change 决定是否构建 server
88
+ if [[ "${only_frontend_change:-false}" == "true" ]]; then
89
+ echo "🔨 [4/6] 仅构建 client (only_frontend_change=true)"
90
+
91
+ echo " ├─ 启动 client 构建..."
92
+ NODE_OPTIONS="$BUILD_NODE_OPTIONS" npm run build:client > /tmp/build-client.log 2>&1
93
+ CLIENT_EXIT=$?
94
+
95
+ if [ $CLIENT_EXIT -ne 0 ]; then
96
+ echo " ❌ Client 构建失败"
97
+ cat /tmp/build-client.log
98
+ exit 1
99
+ fi
100
+
101
+ echo " ✅ Client 构建完成"
102
+ else
103
+ echo "🔨 [4/6] 并行构建 server 和 client"
104
+
105
+ # 并行构建
106
+ echo " ├─ 启动 server 构建..."
107
+ NODE_OPTIONS="$BUILD_NODE_OPTIONS" npm run build:server > /tmp/build-server.log 2>&1 &
108
+ SERVER_PID=$!
109
+
110
+ echo " ├─ 启动 client 构建..."
111
+ NODE_OPTIONS="$BUILD_NODE_OPTIONS" npm run build:client > /tmp/build-client.log 2>&1 &
112
+ CLIENT_PID=$!
113
+
114
+ # 等待两个构建完成
115
+ SERVER_EXIT=0
116
+ CLIENT_EXIT=0
117
+
118
+ wait $SERVER_PID || SERVER_EXIT=$?
119
+ wait $CLIENT_PID || CLIENT_EXIT=$?
120
+
121
+ # 检查构建结果
122
+ if [ $SERVER_EXIT -ne 0 ]; then
123
+ echo " ❌ Server 构建失败"
124
+ cat /tmp/build-server.log
125
+ exit 1
126
+ fi
127
+
128
+ if [ $CLIENT_EXIT -ne 0 ]; then
129
+ echo " ❌ Client 构建失败"
130
+ cat /tmp/build-client.log
131
+ exit 1
132
+ fi
133
+
134
+ echo " ✅ Server 构建完成"
135
+ echo " ✅ Client 构建完成"
136
+ fi
137
+
138
+ print_time $STEP_START
139
+ echo ""
140
+
141
+ # ==================== 步骤 5 ====================
142
+ echo "📦 [5/6] 准备产物"
143
+ STEP_START=$(node -e "console.log(Date.now())")
144
+
145
+ # 移动 client 下的 HTML 文件到 dist/dist/client,保证 views 路径在 dev/prod 下一致
146
+ # 使用 mv 而非 cp:HTML 不能上传到公网 CDN,移走后 dist/client 中不再包含 HTML
147
+ if [ -d "$DIST_DIR/client" ]; then
148
+ mkdir -p "$DIST_DIR/dist/client"
149
+ find "$DIST_DIR/client" -maxdepth 1 -name "*.html" -exec mv {} "$DIST_DIR/dist/client/" \;
150
+ fi
151
+
152
+ # server 相关产物准备(only_frontend_change=true 时跳过)
153
+ if [[ "${only_frontend_change:-false}" == "true" ]]; then
154
+ echo " [skip] 跳过 run.sh/.env 复制 (only_frontend_change=true)"
155
+ else
156
+ # 拷贝 run.sh 到 dist/(prod 从 dist/ 启动,确保 cwd 一致性)
157
+ cp "$ROOT_DIR/scripts/run.sh" "$DIST_DIR/"
158
+
159
+ # 拷贝 .env 文件(如果存在)
160
+ if [ -f "$ROOT_DIR/.env" ]; then
161
+ cp "$ROOT_DIR/.env" "$DIST_DIR/"
162
+ fi
163
+ fi
164
+
165
+ # 清理无用文件
166
+ rm -rf "$DIST_DIR/scripts"
167
+ rm -rf "$DIST_DIR/tsconfig.node.tsbuildinfo"
168
+
169
+ print_time $STEP_START
170
+ echo ""
171
+
172
+ # ==================== 步骤 6 ====================
173
+ echo "✂️ [6/6] 智能依赖裁剪"
174
+ STEP_START=$(node -e "console.log(Date.now())")
175
+
176
+ # 智能依赖裁剪(仅全量构建时执行,纯前端场景不打包 server,无需裁剪)
177
+ if [[ "${only_frontend_change:-false}" == "true" ]]; then
178
+ echo "✂️ [6/6] 跳过智能依赖裁剪 (only_frontend_change=true)"
179
+ else
180
+ echo "✂️ [6/6] 智能依赖裁剪"
181
+
182
+ # 分析实际依赖、复制并裁剪 node_modules、生成精简的 package.json
183
+ node "$ROOT_DIR/scripts/prune-smart.js"
184
+ fi
185
+
186
+ print_time $STEP_START
187
+ echo ""
188
+
189
+ # 总耗时
190
+ echo "构建完成"
191
+ print_time $TOTAL_START
192
+
193
+ # 输出产物信息
194
+ DIST_SIZE=$(du -sh "$DIST_DIR" | cut -f1)
195
+ if [[ "${only_frontend_change:-false}" == "true" ]]; then
196
+ echo ""
197
+ echo "📊 构建产物统计:"
198
+ echo " 产物大小: $DIST_SIZE"
199
+ echo ""
200
+ else
201
+ NODE_MODULES_SIZE=$(du -sh "$DIST_DIR/node_modules" | cut -f1)
202
+ echo ""
203
+ echo "📊 构建产物统计:"
204
+ echo " 产物大小: $DIST_SIZE"
205
+ echo " node_modules: $NODE_MODULES_SIZE"
206
+ echo ""
207
+ fi
@@ -0,0 +1,111 @@
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
59
+ console.log('[dev-local] (2/4) miaoda skills sync...');
60
+ try {
61
+ execSync('npx -y @lark-apaas/miaoda-cli@latest skills sync', { stdio: 'inherit' });
62
+ } catch {
63
+ console.log(' (skills sync 失败,继续启动)');
64
+ }
65
+
66
+ // 3. 加载 .env / .env.local 到 process.env
67
+ // dotenv 默认 override:false,先到先得 → 先 .env.local 让它优先于 .env;
68
+ // shell env 已在 process.env,两次 config 都不会覆盖。
69
+ console.log('[dev-local] (3/4) loading .env / .env.local...');
70
+ const dotenv = require('dotenv');
71
+ dotenv.config({ path: '.env.local' });
72
+ dotenv.config({ path: '.env' });
73
+
74
+ // SUDA_WEBUSER 适配(详见文件头注释)
75
+ if (process.env.SUDA_WEBUSER) {
76
+ const raw = process.env.SUDA_WEBUSER;
77
+ try {
78
+ JSON.parse(raw);
79
+ } catch {
80
+ try {
81
+ const unescaped = raw.replace(/\\"/g, '"');
82
+ JSON.parse(unescaped);
83
+ process.env.SUDA_WEBUSER = unescaped;
84
+ } catch {
85
+ warn(`SUDA_WEBUSER 解析失败,值头部: ${raw.slice(0, 80)}...`);
86
+ }
87
+ }
88
+ }
89
+
90
+ // 4. 并发起前后端 dev server
91
+ console.log('[dev-local] (4/4) 并发起 dev:server + dev:client');
92
+ const child = spawn(
93
+ 'npx',
94
+ [
95
+ '--no-install',
96
+ 'concurrently',
97
+ '--names',
98
+ 'server,client',
99
+ '--prefix-colors',
100
+ 'blue,green',
101
+ '--kill-others-on-fail',
102
+ 'npm run dev:server',
103
+ 'npm run dev:client',
104
+ ],
105
+ { stdio: 'inherit', env: process.env },
106
+ );
107
+ child.on('exit', (code) => process.exit(code ?? 0));
108
+ child.on('error', (err) => {
109
+ console.error(err);
110
+ process.exit(1);
111
+ });