@coze-arch/cli 0.0.1-alpha.dffbaa → 0.0.1-alpha.f37dff

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 (63) hide show
  1. package/lib/__templates__/expo/.coze +7 -2
  2. package/lib/__templates__/expo/.cozeproj/scripts/dev_build.sh +46 -0
  3. package/lib/__templates__/expo/.cozeproj/scripts/dev_run.sh +220 -0
  4. package/lib/__templates__/expo/.cozeproj/scripts/prod_build.sh +47 -0
  5. package/lib/__templates__/expo/.cozeproj/scripts/prod_run.sh +34 -0
  6. package/lib/__templates__/expo/README.md +66 -7
  7. package/lib/__templates__/expo/_gitignore +1 -1
  8. package/lib/__templates__/expo/_npmrc +2 -4
  9. package/lib/__templates__/expo/client/app/_layout.tsx +1 -1
  10. package/lib/__templates__/expo/client/app/home.tsx +1 -0
  11. package/lib/__templates__/expo/client/app/index.tsx +1 -0
  12. package/lib/__templates__/expo/client/app.config.ts +75 -0
  13. package/lib/__templates__/expo/client/assets/images/coze-logo.png +0 -0
  14. package/lib/__templates__/expo/client/components/ThemedText.tsx +33 -0
  15. package/lib/__templates__/expo/client/components/ThemedView.tsx +38 -0
  16. package/lib/__templates__/expo/client/constants/theme.ts +780 -48
  17. package/lib/__templates__/expo/client/hooks/useColorScheme.ts +34 -1
  18. package/lib/__templates__/expo/client/hooks/useTheme.ts +1 -1
  19. package/lib/__templates__/expo/client/metro.config.js +121 -0
  20. package/lib/__templates__/expo/client/package.json +93 -0
  21. package/lib/__templates__/expo/client/screens/home/index.tsx +8 -38
  22. package/lib/__templates__/expo/client/screens/home/styles.ts +16 -52
  23. package/lib/__templates__/expo/client/tsconfig.json +24 -0
  24. package/lib/__templates__/expo/package.json +13 -103
  25. package/lib/__templates__/expo/pnpm-lock.yaml +421 -867
  26. package/lib/__templates__/expo/pnpm-workspace.yaml +3 -0
  27. package/lib/__templates__/expo/server/package.json +31 -0
  28. package/lib/__templates__/expo/{src → server/src}/index.ts +8 -2
  29. package/lib/__templates__/expo/server/tsconfig.json +24 -0
  30. package/lib/__templates__/expo/template.config.js +1 -0
  31. package/lib/__templates__/expo/tsconfig.json +1 -24
  32. package/lib/__templates__/nextjs/.coze +1 -0
  33. package/lib/__templates__/nextjs/next.config.ts +10 -0
  34. package/lib/__templates__/nextjs/package.json +2 -1
  35. package/lib/__templates__/nextjs/scripts/prepare.sh +9 -0
  36. package/lib/__templates__/nextjs/src/app/globals.css +99 -87
  37. package/lib/__templates__/nextjs/src/app/page.tsx +2 -3
  38. package/lib/__templates__/nextjs/src/components/ui/resizable.tsx +29 -22
  39. package/lib/__templates__/nextjs/src/components/ui/sidebar.tsx +228 -230
  40. package/lib/__templates__/nextjs/template.config.js +24 -0
  41. package/lib/__templates__/templates.json +61 -43
  42. package/lib/__templates__/vite/.coze +1 -0
  43. package/lib/__templates__/vite/eslint.config.mjs +9 -0
  44. package/lib/__templates__/vite/package.json +6 -3
  45. package/lib/__templates__/vite/pnpm-lock.yaml +961 -120
  46. package/lib/__templates__/vite/scripts/prepare.sh +9 -0
  47. package/lib/__templates__/vite/template.config.js +4 -0
  48. package/lib/cli.js +144 -31
  49. package/package.json +8 -3
  50. package/lib/__templates__/expo/.cozeproj/scripts/deploy_build.sh +0 -116
  51. package/lib/__templates__/expo/.cozeproj/scripts/deploy_run.sh +0 -239
  52. package/lib/__templates__/expo/app.json +0 -63
  53. package/lib/__templates__/expo/babel.config.js +0 -9
  54. package/lib/__templates__/expo/client/app/(tabs)/_layout.tsx +0 -43
  55. package/lib/__templates__/expo/client/app/(tabs)/home.tsx +0 -1
  56. package/lib/__templates__/expo/client/app/(tabs)/index.tsx +0 -7
  57. package/lib/__templates__/expo/client/app/+not-found.tsx +0 -79
  58. package/lib/__templates__/expo/client/index.js +0 -12
  59. package/lib/__templates__/expo/metro.config.js +0 -53
  60. package/lib/__templates__/nextjs/.vscode/settings.json +0 -121
  61. package/lib/__templates__/vite/.vscode/settings.json +0 -7
  62. /package/lib/__templates__/expo/{eslint-formatter-simple.mjs → client/eslint-formatter-simple.mjs} +0 -0
  63. /package/lib/__templates__/expo/{eslint.config.mjs → client/eslint.config.mjs} +0 -0
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ set -Eeuo pipefail
3
+
4
+ COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
5
+
6
+ cd "${COZE_WORKSPACE_PATH}"
7
+
8
+ echo "Installing dependencies..."
9
+ pnpm install --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only
@@ -37,7 +37,11 @@ export const paramsSchema = {
37
37
  additionalProperties: false,
38
38
  };
39
39
 
40
+ const description = `Vite(简单项目):\`coze init \${COZE_WORKSPACE_PATH} --template vite\`
41
+ - 适用:轻量级 SPA、纯前端交互、仪表盘等轻量级项目。`;
42
+
40
43
  const config = {
44
+ description: description,
41
45
  paramsSchema,
42
46
 
43
47
  defaultParams: {
package/lib/cli.js CHANGED
@@ -1248,17 +1248,139 @@ const convertDotfileName = (filePath) => {
1248
1248
  };
1249
1249
 
1250
1250
  /**
1251
- * 复制并处理模板文件到目标目录
1251
+ * 执行文件渲染钩子
1252
1252
  *
1253
- * @param templatePath - 模板目录路径
1254
- * @param outputPath - 输出目录路径
1253
+ * @param templateConfig - 模板配置
1254
+ * @param fileInfo - 文件渲染信息
1255
1255
  * @param context - 模板上下文
1256
+ * @returns 处理后的文件信息,或 null 表示跳过该文件
1256
1257
  */
1257
- const processTemplateFiles = async (
1258
- templatePath,
1259
- outputPath,
1258
+ const executeFileRenderHook = async (
1259
+ templateConfig,
1260
+ fileInfo,
1260
1261
  context,
1261
1262
  ) => {
1263
+ if (!templateConfig.onFileRender) {
1264
+ return fileInfo;
1265
+ }
1266
+
1267
+ const result = await templateConfig.onFileRender(
1268
+ fileInfo,
1269
+ context,
1270
+ );
1271
+
1272
+ // false: 跳过文件
1273
+ if (result === false) {
1274
+ return null;
1275
+ }
1276
+
1277
+ // undefined/void: 使用默认内容
1278
+ if (result === undefined || result === null) {
1279
+ return fileInfo;
1280
+ }
1281
+
1282
+ // string: 作为 content,其他不变
1283
+ if (typeof result === 'string') {
1284
+ return {
1285
+ ...fileInfo,
1286
+ content: result,
1287
+ };
1288
+ }
1289
+
1290
+ // FileRenderInfo: 使用新对象的信息
1291
+ return result;
1292
+ };
1293
+
1294
+ /**
1295
+ * 处理单个文件
1296
+ */
1297
+ const processSingleFile = async (options
1298
+
1299
+
1300
+
1301
+
1302
+
1303
+ ) => {
1304
+ const { file, templatePath, outputPath, context, templateConfig } = options;
1305
+
1306
+ const srcPath = path.join(templatePath, file);
1307
+ const destFile = convertDotfileName(file);
1308
+
1309
+ logger.verbose(
1310
+ ` - Processing: ${file}${destFile !== file ? ` -> ${destFile}` : ''}`,
1311
+ );
1312
+
1313
+ // 判断是否为二进制文件
1314
+ const isBinary = !shouldRenderFile(srcPath);
1315
+ let content;
1316
+ let wasRendered = false;
1317
+
1318
+ if (isBinary) {
1319
+ // 二进制文件,读取为 buffer 然后转为 base64
1320
+ const buffer = await fs$1.readFile(srcPath);
1321
+ content = buffer.toString('base64');
1322
+ } else {
1323
+ // 文本文件,渲染后的内容
1324
+ content = await renderTemplate(srcPath, context);
1325
+ wasRendered = true;
1326
+ }
1327
+
1328
+ // 构造文件信息对象
1329
+ const fileInfo = {
1330
+ path: file,
1331
+ destPath: destFile,
1332
+ content,
1333
+ isBinary,
1334
+ wasRendered,
1335
+ };
1336
+
1337
+ // 执行文件渲染钩子
1338
+ const processedFileInfo = await executeFileRenderHook(
1339
+ templateConfig,
1340
+ fileInfo,
1341
+ context,
1342
+ );
1343
+
1344
+ // 如果返回 null,跳过该文件
1345
+ if (processedFileInfo === null) {
1346
+ logger.verbose(' ⊘ Skipped by onFileRender hook');
1347
+ return;
1348
+ }
1349
+
1350
+ // 使用处理后的目标路径
1351
+ const finalDestPath = path.join(outputPath, processedFileInfo.destPath);
1352
+
1353
+ // 确保目标目录存在
1354
+ await ensureDir(path.dirname(finalDestPath));
1355
+
1356
+ // 写入文件
1357
+ if (processedFileInfo.isBinary) {
1358
+ // 二进制文件:如果内容没变,直接复制;否则从 base64 解码写入
1359
+ if (processedFileInfo.content === content) {
1360
+ await fs$1.copyFile(srcPath, finalDestPath);
1361
+ logger.verbose(' ✓ Copied (binary)');
1362
+ } else {
1363
+ const buffer = Buffer.from(processedFileInfo.content, 'base64');
1364
+ await fs$1.writeFile(finalDestPath, buffer);
1365
+ logger.verbose(' ✓ Written (binary, modified by hook)');
1366
+ }
1367
+ } else {
1368
+ // 文本文件
1369
+ await fs$1.writeFile(finalDestPath, processedFileInfo.content, 'utf-8');
1370
+ logger.verbose(' ✓ Rendered and written');
1371
+ }
1372
+ };
1373
+
1374
+ /**
1375
+ * 复制并处理模板文件到目标目录
1376
+ */
1377
+ const processTemplateFiles = async (options
1378
+
1379
+
1380
+
1381
+
1382
+ ) => {
1383
+ const { templatePath, outputPath, context, templateConfig } = options;
1262
1384
  logger.verbose('Processing template files:');
1263
1385
  logger.verbose(` - Template path: ${templatePath}`);
1264
1386
  logger.verbose(` - Output path: ${outputPath}`);
@@ -1289,29 +1411,15 @@ const processTemplateFiles = async (
1289
1411
  }
1290
1412
 
1291
1413
  await Promise.all(
1292
- files.map(async file => {
1293
- const srcPath = path.join(templatePath, file);
1294
- const destFile = convertDotfileName(file);
1295
- const destPath = path.join(outputPath, destFile);
1296
-
1297
- logger.verbose(
1298
- ` - Processing: ${file}${destFile !== file ? ` -> ${destFile}` : ''}`,
1299
- );
1300
-
1301
- // 确保目标目录存在
1302
- await ensureDir(path.dirname(destPath));
1303
-
1304
- if (shouldRenderFile(srcPath)) {
1305
- // 渲染文本文件
1306
- const rendered = await renderTemplate(srcPath, context);
1307
- await fs$1.writeFile(destPath, rendered, 'utf-8');
1308
- logger.verbose(' ✓ Rendered and written');
1309
- } else {
1310
- // 直接复制二进制文件
1311
- await fs$1.copyFile(srcPath, destPath);
1312
- logger.verbose(' ✓ Copied');
1313
- }
1314
- }),
1414
+ files.map(file =>
1415
+ processSingleFile({
1416
+ file,
1417
+ templatePath,
1418
+ outputPath,
1419
+ context,
1420
+ templateConfig,
1421
+ }),
1422
+ ),
1315
1423
  );
1316
1424
 
1317
1425
  logger.verbose('✓ All files processed successfully');
@@ -1498,7 +1606,12 @@ const execute = async (
1498
1606
  const absoluteOutputPath = await prepareOutputDirectory(outputPath);
1499
1607
 
1500
1608
  // 6. 处理模板文件
1501
- await processTemplateFiles(templatePath, absoluteOutputPath, context);
1609
+ await processTemplateFiles({
1610
+ templatePath,
1611
+ outputPath: absoluteOutputPath,
1612
+ context,
1613
+ templateConfig,
1614
+ });
1502
1615
 
1503
1616
  // 7. 执行 onAfterRender 钩子
1504
1617
  await executeAfterRenderHook(templateConfig, context, absoluteOutputPath);
@@ -1741,7 +1854,7 @@ const registerCommand = program => {
1741
1854
  });
1742
1855
  };
1743
1856
 
1744
- var version = "0.0.1-alpha.dffbaa";
1857
+ var version = "0.0.1-alpha.f37dff";
1745
1858
  var packageJson = {
1746
1859
  version: version};
1747
1860
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coze-arch/cli",
3
- "version": "0.0.1-alpha.dffbaa",
3
+ "version": "0.0.1-alpha.f37dff",
4
4
  "private": false,
5
5
  "description": "coze coding devtools cli",
6
6
  "license": "MIT",
@@ -19,12 +19,13 @@
19
19
  "scripts": {
20
20
  "prebuild": "tsx scripts/prebuild.ts",
21
21
  "build": "tsx scripts/build.ts",
22
+ "create": "tsx scripts/create-template.ts",
22
23
  "lint": "eslint ./ --cache",
23
24
  "postpublish": "bash scripts/sync-npmmirror.sh",
24
25
  "test": "vitest --run --passWithNoTests",
25
26
  "test:all": "bash scripts/test-coverage.sh",
26
27
  "test:cov": "vitest --run --passWithNoTests --coverage",
27
- "test:e2e": "bash scripts/e2e.sh",
28
+ "test:e2e": "NODE_ENV=test bash scripts/e2e.sh",
28
29
  "test:perf": "vitest bench --run --config vitest.perf.config.ts",
29
30
  "test:perf:compare": "bash scripts/compare-perf.sh",
30
31
  "test:perf:save": "bash scripts/run-perf-with-output.sh"
@@ -48,21 +49,25 @@
48
49
  "@coze-arch/ts-config": "workspace:*",
49
50
  "@coze-arch/vitest-config": "workspace:*",
50
51
  "@coze-coding/lambda": "workspace:*",
52
+ "@inquirer/prompts": "^3.2.0",
51
53
  "@types/ejs": "^3.1.5",
52
54
  "@types/iarna__toml": "^2.0.5",
53
55
  "@types/js-yaml": "^4.0.9",
56
+ "@types/minimatch": "^5.1.2",
54
57
  "@types/minimist": "^1.2.5",
55
58
  "@types/node": "^24",
56
59
  "@types/shelljs": "^0.10.0",
57
60
  "@vitest/coverage-v8": "~4.0.16",
58
61
  "json-schema-to-typescript": "^15.0.3",
62
+ "minimatch": "^10.0.1",
59
63
  "rollup": "^4.41.1",
60
64
  "sucrase": "^3.35.0",
61
65
  "tsx": "^4.20.6",
62
66
  "vitest": "~4.0.16"
63
67
  },
64
68
  "publishConfig": {
65
- "access": "public"
69
+ "access": "public",
70
+ "registry": "https://registry.npmjs.org"
66
71
  },
67
72
  "cozePublishConfig": {
68
73
  "bin": {
@@ -1,116 +0,0 @@
1
- #!/bin/bash
2
- if [ -z "${BASH_VERSION:-}" ]; then exec /usr/bin/env bash "$0" "$@"; fi
3
- set -euo pipefail
4
- ROOT_DIR="$(pwd)"
5
- PREVIEW_DIR="${COZE_PREVIEW_DIR:-$ROOT_DIR}"
6
- LOG_DIR="${COZE_LOG_DIR:-$ROOT_DIR/logs}"
7
- LOG_FILE="$LOG_DIR/app.log"
8
- mkdir -p "$LOG_DIR"
9
-
10
- # ==================== 配置项 ====================
11
- SERVER_DIR="app"
12
- EXPO_DIR="expo"
13
- CHECK_HASH_SCRIPT="$ROOT_DIR/check_hash.py"
14
- # ==================== 工具函数 ====================
15
- info() {
16
- echo -e "\033[32m[INFO] $1\033[0m"
17
- }
18
- warn() {
19
- echo -e "\033[33m[WARN] $1\033[0m"
20
- }
21
- error() {
22
- echo -e "\033[31m[ERROR] $1\033[0m"
23
- exit 1
24
- }
25
- check_command() {
26
- if ! command -v "$1" &> /dev/null; then
27
- error "命令 $1 未找到,请先安装"
28
- fi
29
- }
30
- write_log() {
31
- local level="${1:-INFO}"
32
- local msg="${2:-}"
33
- node -e '
34
- const fs=require("fs");
35
- const path=process.argv[1];
36
- const level=(process.argv[2]||"").toUpperCase();
37
- let msg=String(process.argv[3]||"");
38
- const ts=Date.now();
39
- const dt=new Date();
40
- const parts=new Intl.DateTimeFormat("en-GB",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:false}).formatToParts(dt);
41
- const o={};
42
- for(const p of parts){o[p.type]=p.value}
43
- const dt_str=`${o.year}-${o.month}-${o.day} ${o.hour}:${o.minute}:${o.second}`;
44
- msg=msg.replace(/\\n/g,"\n");
45
- msg=`${dt_str} [${level}] ${msg}`;
46
- const record=JSON.stringify({level, message: msg, timestamp: ts},null,0);
47
- const fd=fs.openSync(path,"a");
48
- fs.writeSync(fd, record+"\n", null, "utf8");
49
- fs.fsyncSync(fd);
50
- fs.closeSync(fd);
51
- ' "$LOG_FILE" "$level" "$msg"
52
-
53
- case "$level" in
54
- INFO) info "$msg" ;;
55
- WARN) warn "$msg" ;;
56
- ERROR) echo -e "\033[31m[ERROR] $msg\033[0m" ;;
57
- *) info "$msg" ;;
58
- esac
59
- }
60
-
61
- # ==================== 前置检查 ====================
62
- write_log "INFO" "==================== 开始构建 ===================="
63
-
64
- write_log "INFO" "检查根目录 pre_install.py"
65
- if [ -f "$PREVIEW_DIR/pre_install.py" ]; then
66
- write_log "INFO" "执行:python $PREVIEW_DIR/pre_install.py"
67
- python "$PREVIEW_DIR/pre_install.py" || write_log "ERROR" "pre_install.py 执行失败"
68
- fi
69
-
70
- write_log "INFO" "开始执行构建脚本(build_dev.sh)..."
71
- write_log "INFO" "正在检查依赖命令是否存在..."
72
- # 检查核心命令
73
- # check_command "pip"
74
- # check_command "python"
75
- check_command "pnpm"
76
- check_command "npm"
77
-
78
- # ==================== 步骤 1:安装项目依赖 ====================
79
- write_log "INFO" "==================== 安装项目依赖 ===================="
80
- if [ ! -f "package.json" ]; then
81
- write_log "ERROR" "项目目录下无 package.json,不是合法的 Node.js 项目"
82
- fi
83
- # 步骤 2.1/2.2:安装 Expo 项目依赖(按哈希变化执行)
84
- if [ -f "$CHECK_HASH_SCRIPT" ]; then
85
- write_log "INFO" "检测 package.json 哈希是否变化"
86
- set +e
87
- python "$CHECK_HASH_SCRIPT" --config "package.json" --check
88
- rc_node=$?
89
- set -e
90
- if [ $rc_node -eq 1 ]; then
91
- write_log "INFO" "依赖哈希已变化,执行:pnpm install"
92
- pnpm install --registry=https://registry.npmmirror.com || write_log "ERROR" "Expo 项目依赖安装失败(pnpm 执行出错)"
93
-
94
- write_log "INFO" "依赖哈希已变化,执行:npm run install-missing"
95
- npm run install-missing || write_log "ERROR" "npm run install-missing 执行失败,请检查 package.json 中的脚本配置"
96
-
97
- set +e
98
- python "$CHECK_HASH_SCRIPT" --config "package.json" --update
99
- set -e
100
- else
101
- write_log "INFO" "跳过 pnpm install 与 npm run install-missing"
102
- fi
103
- else
104
- write_log "WARN" "未找到 check_hash.py,默认执行:pnpm install 与 npm run install-missing"
105
- pnpm install --registry=https://registry.npmmirror.com || write_log "ERROR" "Expo 项目依赖安装失败(pnpm 执行出错)"
106
- npm run install-missing || write_log "ERROR" "npm run install-missing 执行失败,请检查 package.json 中的脚本配置"
107
- fi
108
-
109
- write_log "INFO" "检查根目录 post_install.py"
110
- if [ -f "$ROOT_DIR/post_install.py" ]; then
111
- write_log "INFO" "执行:python $ROOT_DIR/post_install.py"
112
- python "$ROOT_DIR/post_install.py" || write_log "ERROR" "post_install.py 执行失败"
113
- fi
114
-
115
- write_log "INFO" "==================== 依赖安装完成!====================\n"
116
- write_log "INFO" "下一步:执行 ./deploy_run.sh 启动服务"
@@ -1,239 +0,0 @@
1
- ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
2
- LOG_DIR="${COZE_LOG_DIR:-$ROOT_DIR/logs}"
3
- LOG_FILE="$LOG_DIR/app.log"
4
- mkdir -p "$LOG_DIR"
5
-
6
- # ==================== 配置项 ====================
7
- # Server 服务配置
8
- SERVER_HOST="0.0.0.0"
9
- SERVER_PORT="9091"
10
- # Expo 项目配置
11
- EXPO_HOST="0.0.0.0"
12
- EXPO_DIR="expo"
13
- EXPO_PORT="9090"
14
- WEB_URL="${COZE_PROJECT_DOMAIN_DEFAULT:-http://127.0.0.1:${SERVER_PORT}}"
15
- ASSUME_YES="1"
16
- EXPO_PUBLIC_BACKEND_BASE_URL="${EXPO_PUBLIC_BACKEND_BASE_URL:-$WEB_URL}"
17
-
18
-
19
- EXPO_PACKAGER_PROXY_URL="${EXPO_PUBLIC_BACKEND_BASE_URL}"
20
- export EXPO_PUBLIC_BACKEND_BASE_URL EXPO_PACKAGER_PROXY_URL
21
- # 运行时变量(为避免 set -u 的未绑定错误,预置为空)
22
- SERVER_PID=""
23
- EXPO_PID=""
24
- # ==================== 工具函数 ====================
25
- info() {
26
- echo -e "\033[32m[INFO] $1\033[0m"
27
- }
28
- warn() {
29
- echo -e "\033[33m[WARN] $1\033[0m"
30
- }
31
- error() {
32
- echo -e "\033[31m[ERROR] $1\033[0m"
33
- exit 1
34
- }
35
- check_command() {
36
- if ! command -v "$1" &> /dev/null; then
37
- error "命令 $1 未找到,请先安装"
38
- fi
39
- }
40
- while [ $# -gt 0 ]; do
41
- case "$1" in
42
- -y|--yes)
43
- ASSUME_YES="1"
44
- shift
45
- ;;
46
- *)
47
- shift
48
- ;;
49
- esac
50
- done
51
- is_port_free() {
52
- ! lsof -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1
53
- }
54
- choose_next_free_port() {
55
- local start=$1
56
- local p=$start
57
- while ! is_port_free "$p"; do
58
- p=$((p+1))
59
- done
60
- echo "$p"
61
- }
62
- ensure_port() {
63
- local var_name=$1
64
- local port_val=$2
65
- if is_port_free "$port_val"; then
66
- info "端口未占用:$port_val"
67
- eval "$var_name=$port_val"
68
- else
69
- warn "端口已占用:$port_val"
70
- local choice
71
- if [ "$ASSUME_YES" = "1" ]; then choice="Y"; else read -r -p "是否关闭该端口的进程?[Y/n] " choice || choice="Y"; fi
72
- if [ -z "$choice" ] || [ "$choice" = "y" ] || [ "$choice" = "Y" ]; then
73
- if command -v lsof &> /dev/null; then
74
- local pids
75
- pids=$(lsof -t -i tcp:"$port_val" -sTCP:LISTEN 2>/dev/null || true)
76
- if [ -n "$pids" ]; then
77
- info "正在关闭进程:$pids"
78
- kill -9 $pids 2>/dev/null || warn "关闭进程失败:$pids"
79
- eval "$var_name=$port_val"
80
- else
81
- warn "未获取到占用该端口的进程"
82
- eval "$var_name=$port_val"
83
- fi
84
- else
85
- warn "缺少 lsof,无法自动关闭进程"
86
- eval "$var_name=$port_val"
87
- fi
88
- else
89
- local new_port
90
- new_port=$(choose_next_free_port "$port_val")
91
- info "使用新的端口:$new_port"
92
- eval "$var_name=$new_port"
93
- fi
94
- fi
95
- }
96
-
97
- write_log() {
98
- local level="${1:-INFO}"
99
- local msg="${2:-}"
100
- node -e '
101
- const fs=require("fs");
102
- const path=process.argv[1];
103
- const level=(process.argv[2]||"").toUpperCase();
104
- let msg=String(process.argv[3]||"");
105
- const ts=Date.now();
106
- msg=msg.replace(/\\n/g,"\n");
107
- const dt=new Date();
108
- const parts=new Intl.DateTimeFormat("en-GB",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:false}).formatToParts(dt);
109
- const o={};
110
- for(const p of parts){o[p.type]=p.value}
111
- const dt_str=`${o.year}-${o.month}-${o.day} ${o.hour}:${o.minute}:${o.second}`;
112
- msg=`${dt_str} [${level}] ${msg}`;
113
- const record=JSON.stringify({level, message: msg, timestamp: ts},null,0);
114
- const fd=fs.openSync(path,"a");
115
- fs.writeSync(fd, record+"\n", null, "utf8");
116
- fs.fsyncSync(fd);
117
- fs.closeSync(fd);
118
- ' "$LOG_FILE" "$level" "$msg"
119
- case "$level" in
120
- INFO) echo -e "\033[32m[INFO] $msg\033[0m" ;;
121
- WARN) echo -e "\033[33m[WARN] $msg\033[0m" ;;
122
- ERROR) echo -e "\033[31m[ERROR] $msg\033[0m" ;;
123
- *) echo -e "\033[32m[INFO] $msg\033[0m" ;;
124
- esac
125
- }
126
-
127
- wait_port_connectable() {
128
- local host=$1 port=$2 retries=${3:-10}
129
- for _ in $(seq 1 "$retries"); do
130
- nc -z "$host" "$port" && return 0
131
- sleep 1
132
- done
133
- return 1
134
- }
135
-
136
- start_expo() {
137
- local offline="${1:-0}"
138
- if [ "$offline" = "1" ]; then
139
- EXPO_OFFLINE=1 EXPO_NO_DOCTOR=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" nohup npx expo start --port "$EXPO_PORT" > logs/expo.log 2>&1 &
140
- else
141
- EXPO_NO_DOCTOR=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" nohup npx expo start --port "$EXPO_PORT" > logs/expo.log 2>&1 &
142
- fi
143
- EXPO_PID=$!
144
- echo "$EXPO_PID"
145
- }
146
-
147
- detect_expo_fetch_failed() {
148
- local timeout="${1:-8}"
149
- local waited=0
150
- local log_file="logs/expo.log"
151
- while [ "$waited" -lt "$timeout" ]; do
152
- if [ -f "$log_file" ] && grep -q "TypeError: fetch failed" "$log_file" 2>/dev/null; then
153
- return 0
154
- fi
155
- sleep 1
156
- waited=$((waited+1))
157
- done
158
- return 1
159
- }
160
-
161
- # ==================== 前置检查 ====================
162
- write_log "INFO" "==================== 开始启动 ===================="
163
- write_log "INFO" "开始执行服务启动脚本(start_dev.sh)..."
164
- write_log "INFO" "正在检查依赖命令和目录是否存在..."
165
- # 检查核心命令
166
- # check_command "python"
167
- check_command "npm"
168
- check_command "pnpm"
169
- check_command "lsof"
170
- check_command "bash"
171
-
172
- info "准备日志目录:logs"
173
- mkdir -p logs
174
- # 端口占用预检查与处理
175
- ensure_port SERVER_PORT "$SERVER_PORT"
176
- ensure_port EXPO_PORT "$EXPO_PORT"
177
-
178
- # ==================== 启动 Server 服务 ====================
179
- write_log "INFO" "检查 Nginx 端口 (5000)..."
180
- if is_port_free 5000; then
181
- write_log "INFO" "端口 5000 未被占用,正在启动 Nginx..."
182
- service nginx start
183
- else
184
- write_log "INFO" "端口 5000 已被占用,跳过 Nginx 启动"
185
- fi
186
-
187
- write_log "INFO" "==================== 启动 server 服务 ===================="
188
- write_log "INFO" "正在执行:npm run server"
189
- PORT="$SERVER_PORT" nohup npm run server > logs/app.log 2>&1 &
190
- SERVER_PID=$!
191
- if [ -z "${SERVER_PID}" ]; then
192
- write_log "ERROR" "无法获取 server 后台进程 PID"
193
- fi
194
- write_log "INFO" "server 服务已启动,进程 ID:${SERVER_PID:-unknown}"
195
-
196
- write_log "INFO" "==================== 启动 Expo 项目 ===================="
197
- write_log "INFO" "正在执行:npx expo start --port ${EXPO_PORT}"
198
- write_log "INFO" "开始启动 Expo 服务"
199
- EXPO_PID=$(start_expo 0)
200
- if detect_expo_fetch_failed 8; then
201
- write_log "WARN" "Expo 启动检测到网络错误:TypeError: fetch failed,启用离线模式重试"
202
- if [ -n "${EXPO_PID}" ]; then kill -9 "$EXPO_PID" 2>/dev/null || true; fi
203
- : > logs/expo.log
204
- EXPO_PID=$(start_expo 1)
205
- fi
206
- # 输出以下环境变量,确保 Expo 项目能正确连接到 Server 服务
207
- write_log "INFO" "Expo 环境变量配置:"
208
- write_log "INFO" "EXPO_PUBLIC_BACKEND_BASE_URL=${EXPO_PUBLIC_BACKEND_BASE_URL}"
209
- write_log "INFO" "EXPO_PACKAGER_PROXY_URL=${EXPO_PACKAGER_PROXY_URL}"
210
- if [ -z "${EXPO_PID}" ]; then
211
- write_log "ERROR" "无法获取 Expo 后台进程 PID"
212
- fi
213
-
214
- write_log "INFO" "所有服务已启动。Server PID: ${SERVER_PID}, Expo PID: ${EXPO_PID}"
215
-
216
- write_log "INFO" "检查 Server 服务端口:$SERVER_HOST:$SERVER_PORT"
217
- if wait_port_connectable "$SERVER_HOST" "$SERVER_PORT" 10 2; then
218
- write_log "INFO" "端口可连接:$SERVER_HOST:$SERVER_PORT"
219
- else
220
- write_log "WARN" "端口不可连接:$SERVER_HOST:$SERVER_PORT 10 次)"
221
- fi
222
-
223
- write_log "INFO" "检查 Expo 服务端口:$EXPO_HOST:$EXPO_PORT"
224
- if wait_port_connectable "$EXPO_HOST" "$EXPO_PORT" 10 2; then
225
- write_log "INFO" "端口可连接:$EXPO_HOST:$EXPO_PORT"
226
- else
227
- write_log "WARN" "端口不可连接:$EXPO_HOST:$EXPO_PORT(已尝试 10 次)"
228
- fi
229
-
230
- write_log "INFO" "服务端口检查完成"
231
-
232
- write_log "INFO" "检查根目录 post_run.py"
233
- if [ -f "$ROOT_DIR/post_run.py" ]; then
234
- write_log "INFO" "启动检查中"
235
- python "$ROOT_DIR/post_run.py" --port "$EXPO_PORT" || write_log "ERROR" "post_run.py 执行失败"
236
- write_log "INFO" "启动检查结束"
237
- fi
238
-
239
- write_log "INFO" "==================== 服务启动完成 ===================="