@coze-arch/cli 0.1.5 → 0.1.8-alpha.2ee669

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/README.md +18 -0
  2. package/docs/deploy.md +12 -1
  3. package/lib/__templates__/expo/.cozeproj/scripts/dev_build.sh +3 -3
  4. package/lib/__templates__/expo/.cozeproj/scripts/dev_run.sh +11 -3
  5. package/lib/__templates__/expo/.cozeproj/scripts/prepare-node-modules.sh +380 -0
  6. package/lib/__templates__/expo/.cozeproj/scripts/prod_build.sh +3 -3
  7. package/lib/__templates__/expo/client/components/Screen.tsx +65 -25
  8. package/lib/__templates__/expo/client/heroui/components/toast/toast.tsx +6 -2
  9. package/lib/__templates__/expo/client/heroui/helpers/internal/hooks/use-controllable-state.ts +1 -1
  10. package/lib/__templates__/native-static/.coze +2 -2
  11. package/lib/__templates__/nextjs/.coze +1 -1
  12. package/lib/__templates__/nextjs/README.md +18 -0
  13. package/lib/__templates__/nextjs/eslint.config.mjs +11 -5
  14. package/lib/__templates__/nextjs/next.config.ts +10 -0
  15. package/lib/__templates__/nextjs/package.json +2 -5
  16. package/lib/__templates__/nextjs/pnpm-lock.yaml +14 -945
  17. package/lib/__templates__/nextjs/scripts/build.ps1 +1 -1
  18. package/lib/__templates__/nextjs/scripts/build.sh +1 -1
  19. package/lib/__templates__/nextjs/scripts/dev.ps1 +2 -2
  20. package/lib/__templates__/nextjs/scripts/dev.sh +70 -12
  21. package/lib/__templates__/nextjs/scripts/local-workspace.cjs +302 -0
  22. package/lib/__templates__/nextjs/scripts/prepare-node-modules.sh +380 -0
  23. package/lib/__templates__/nextjs/scripts/prepare.sh +9 -4
  24. package/lib/__templates__/nextjs/scripts/validate.sh +11 -1
  25. package/lib/__templates__/nextjs/src/app/layout.tsx +0 -4
  26. package/lib/__templates__/nextjs/src/server.ts +1 -1
  27. package/lib/__templates__/nextjs/template.config.js +20 -6
  28. package/lib/__templates__/nextjs/tsconfig.json +0 -1
  29. package/lib/__templates__/nuxt-vue/.coze +1 -1
  30. package/lib/__templates__/nuxt-vue/README.md +20 -2
  31. package/lib/__templates__/nuxt-vue/scripts/dev.sh +21 -7
  32. package/lib/__templates__/nuxt-vue/scripts/local-workspace.cjs +302 -0
  33. package/lib/__templates__/nuxt-vue/scripts/prepare-node-modules.sh +380 -0
  34. package/lib/__templates__/nuxt-vue/scripts/prepare.sh +9 -4
  35. package/lib/__templates__/nuxt-vue/scripts/validate.sh +11 -1
  36. package/lib/__templates__/taro/.coze +2 -2
  37. package/lib/__templates__/taro/.cozeproj/scripts/deploy_build.sh +10 -1
  38. package/lib/__templates__/taro/.cozeproj/scripts/deploy_run.sh +4 -0
  39. package/lib/__templates__/taro/.cozeproj/scripts/dev_build.sh +12 -0
  40. package/lib/__templates__/taro/.cozeproj/scripts/dev_run.ps1 +4 -2
  41. package/lib/__templates__/taro/.cozeproj/scripts/dev_run.sh +84 -10
  42. package/lib/__templates__/taro/.cozeproj/scripts/init_env.sh +4 -0
  43. package/lib/__templates__/taro/.cozeproj/scripts/local-workspace.cjs +351 -0
  44. package/lib/__templates__/taro/.cozeproj/scripts/pack.ps1 +3 -1
  45. package/lib/__templates__/taro/.cozeproj/scripts/pack.sh +24 -6
  46. package/lib/__templates__/taro/.cozeproj/scripts/prepare-node-modules.sh +380 -0
  47. package/lib/__templates__/taro/.cozeproj/scripts/taro-build.cjs +66 -0
  48. package/lib/__templates__/taro/.cozeproj/scripts/validate.ps1 +3 -1
  49. package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +7 -0
  50. package/lib/__templates__/taro/_gitignore +1 -0
  51. package/lib/__templates__/taro/package.json +5 -5
  52. package/lib/__templates__/vite/.coze +1 -1
  53. package/lib/__templates__/vite/README.md +20 -0
  54. package/lib/__templates__/vite/scripts/dev.ps1 +2 -2
  55. package/lib/__templates__/vite/scripts/dev.sh +21 -7
  56. package/lib/__templates__/vite/scripts/local-workspace.cjs +302 -0
  57. package/lib/__templates__/vite/scripts/prepare-node-modules.sh +380 -0
  58. package/lib/__templates__/vite/scripts/prepare.sh +9 -4
  59. package/lib/__templates__/vite/scripts/validate.sh +7 -1
  60. package/lib/__templates__/vite/template.config.js +20 -6
  61. package/lib/cli.js +511 -199
  62. package/package.json +3 -2
  63. package/lib/__templates__/nextjs/.babelrc +0 -15
package/README.md CHANGED
@@ -42,6 +42,20 @@ coze-dev validate # 读取 dev.validate
42
42
  `dev` 和 `build` 在执行前会尝试应用模板补丁;补丁失败会记录调试信息,但不会阻止主命令。
43
43
  子进程日志默认写入 `~/.coze-logs/dev.log`,可用 `--log-file <path>` 覆盖。
44
44
 
45
+ ## Web 云盘开发
46
+
47
+ Vite、Next.js、Nuxt 的 Unix 开发 wrapper 在 `COZE_DRIVE_ROOT`(默认 `/Coze/Drive`)内通过
48
+ `rsync` 同步源码到本机临时目录 `coze-web-<uid>/<source-path-hash>/`,再安装依赖和执行。
49
+ `prepare/dev` 使用 `dev` 子目录,`validate` 使用独立的 `check` 子目录,避免类型生成干扰热更新。
50
+ 依赖和开发编译产物留在本地;只有安装成功且云盘依赖输入未变化时才回写锁文件。所有修复仍在云盘完成。
51
+ dev 的 detached 进程持续同步,框架只监听本地镜像;同步失败或依赖输入变化时停止服务。
52
+
53
+ 本地镜像仅用于 `[dev].build/run/validate`;部署配置、`build.sh` 和 `start.sh` 保持原流程。
54
+ 普通本地目录和 Windows PowerShell 入口保持原流程。`rsync` 由开发环境提供,`[dev].deps` 仅作声明。
55
+ 存量项目通过 `coze-dev patch --dry-run` / `coze-dev patch` 升级,沿用 patch 总开关。
56
+ 仅升级完整匹配旧模板的三个 Unix 开发脚本并保留端口;自定义开发脚本、命令和同名 helper 会跳过。
57
+ 只更新开发插件不会替换存量项目脚本。
58
+
45
59
  ## Windows 与 WSL
46
60
 
47
61
  原生 Windows(`process.platform === "win32"`)优先使用 `.coze` 中非空的 Windows
@@ -106,3 +120,7 @@ npm run build
106
120
  ```
107
121
 
108
122
  端到端模板验证使用 `npm run test:e2e`,会真实安装模板依赖并启动服务。发布前需完成该验证。
123
+
124
+ Web 镜像集成测试使用 `npm run test:integration:web`,依赖 Node.js、bash、rsync 和临时
125
+ loopback 监听权限;包管理器和框架命令使用替身,验证目录、同步、锁文件与进程生命周期,
126
+ 不替代真实框架 E2E。
package/docs/deploy.md CHANGED
@@ -57,7 +57,7 @@ coze-dev deploy app rename <name> --app-id <app_id> --yes
57
57
  coze-dev deploy app delete --app-id <app_id> --yes
58
58
  coze-dev deploy app analytics|metrics --app-id <app_id> [--time-range <range>]
59
59
  coze-dev deploy app usage --app-id <app_id> --bill-month <YYYY-MM>
60
- coze-dev deploy [--lane <ppe_lane>] [--source-root <directory>] [--app-id <app_id>] [--application-type <type>] [--name <name>] [--connector-id <id>]
60
+ coze-dev deploy [--lane <ppe_lane>] [--source-root <directory>] [--app-id <app_id>] [--application-type <type>] [--name <name>] [--deploy-config <json>] [--connector-id <id>]
61
61
  coze-dev deploy status|list|online|rollback-check|cancel|rollback|stop|resume|pause
62
62
  coze-dev deploy logs|log-count|count|build-log|config
63
63
  coze-dev deploy domain list [--unit-type dev|prod]
@@ -76,6 +76,17 @@ coze-dev deploy mini-program status|bind|submit-code|unbind|auth|preview
76
76
  Application;类型不一致时创建新 Application、覆盖 `app_id` 并提示。也可用 `--project-id`
77
77
  或 `--app-id` 显式指定已有资源,显式指定时不会额外创建 Project。
78
78
 
79
+ 部署预检默认返回服务端给出的 `deploy_config`。需要调整部署规格时,传入包含完整四个字段的
80
+ `--deploy-config` JSON;预检会返回覆盖后的最终配置,确认部署时必须原样复用该参数:
81
+
82
+ ```bash
83
+ coze-dev deploy --source-root . \
84
+ --deploy-config '{"cpu":4,"memory_mb":8192,"max_instance":2,"max_concurrency":100}'
85
+ ```
86
+
87
+ CPU 与内存必须使用固定组合:`1/2048`、`4/8192`、`8/16384`、`16/32768`;
88
+ `max_instance` 为 1–100,`max_concurrency` 为 10–1000。四个值均为整数。
89
+
79
90
  首次部署的应用名称可通过 `--name` 指定;显式参数优先于 `.coze` 的
80
91
  `[project].name`。Pages 部署没有 `.coze` 时,CLI 会先生成最小配置再写入绑定。
81
92
 
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  if [ -z "${BASH_VERSION:-}" ]; then exec /usr/bin/env bash "$0" "$@"; fi
3
3
  set -euo pipefail
4
- ROOT_DIR="$(pwd)"
4
+ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
5
  PREVIEW_DIR="/source/preview"
6
6
  SOURCE_DIR="/source"
7
7
 
@@ -34,8 +34,8 @@ echo "==================== 安装项目依赖 ===================="
34
34
  if [ ! -f "package.json" ]; then
35
35
  echo "项目目录下无 package.json,不是合法的 Node.js 项目"
36
36
  fi
37
- # 步骤 2.1/2.2:安装项目依赖
38
- pnpm install --registry=https://registry.npmmirror.com || echo "Expo 项目依赖安装失败(pnpm 执行出错)"
37
+ # 步骤 2.1/2.2:通过受管入口安装项目依赖
38
+ bash "$ROOT_DIR/.cozeproj/scripts/prepare-node-modules.sh" --registry=https://registry.npmmirror.com
39
39
 
40
40
  echo "检查根目录 post_install.py"
41
41
  if [ -f "$PREVIEW_DIR/post_install.py" ]; then
@@ -123,11 +123,15 @@ wait_port_connectable() {
123
123
  }
124
124
 
125
125
  # detached 出去的进程没人负责回收,超过这个时长就自己退出,避免端口与内存长期泄露。
126
- MAX_RUNTIME_SECONDS=1200
126
+ MAX_RUNTIME_SECONDS=3600
127
+
128
+ timeout_watchdog_enabled() {
129
+ [[ -z "${COZE_EVAL:-}" && -z "${COZE_PROJECT_TYPE:-}" ]]
130
+ }
127
131
 
128
132
  # 真正被 detach 的是这层 bash wrapper:它是进程组 leader,组内 watchdog 到点回收整组
129
133
  # (wrapper -> pnpm -> expo/node);被包的进程自己先退出时也顺手清空进程组,不留残余。
130
- RUN_WITH_TIMEOUT='
134
+ RUN_WITH_TIMEOUT="$(declare -f timeout_watchdog_enabled)"'
131
135
  timeout_seconds=$1
132
136
  shift
133
137
 
@@ -135,6 +139,7 @@ shift
135
139
  child_pid=$!
136
140
 
137
141
  # 先忽略 TERM,才能在向整组发 TERM(自己也在组里)之后存活下来补一发 KILL。
142
+ if timeout_watchdog_enabled; then
138
143
  ( trap "" TERM
139
144
  sleep "${timeout_seconds}"
140
145
  echo "[dev] 后台进程运行超过 ${timeout_seconds}s,回收进程组 $$。"
@@ -142,6 +147,7 @@ child_pid=$!
142
147
  sleep 5
143
148
  kill -KILL -- "-$$" 2>/dev/null || true
144
149
  ) &
150
+ fi
145
151
 
146
152
  wait "${child_pid}"
147
153
  kill -KILL -- "-$$" 2>/dev/null || true
@@ -290,7 +296,9 @@ if [ -z "${EXPO_PID}" ]; then
290
296
  fi
291
297
 
292
298
  echo "所有服务已启动。Server PID: ${SERVER_PID}, Expo PID: ${EXPO_PID}"
293
- echo "两个服务均在 ${MAX_RUNTIME_SECONDS}s 后自动退出。"
299
+ if timeout_watchdog_enabled; then
300
+ echo "两个服务均在 ${MAX_RUNTIME_SECONDS}s 后自动退出。"
301
+ fi
294
302
 
295
303
  echo "检查 Server 服务端口:$SERVER_HOST:$SERVER_PORT"
296
304
  if wait_port_connectable "$SERVER_HOST" "$SERVER_PORT" 10 2; then
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env bash
2
+ # Install dependencies on local disk and expose them to the source tree by symlink.
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
6
+ if [ "$(basename "$(dirname "$SCRIPT_DIR")")" = ".cozeproj" ]; then
7
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd -P)"
8
+ else
9
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
10
+ fi
11
+
12
+ command -v pnpm >/dev/null 2>&1 || {
13
+ echo "[node_modules] pnpm is required" >&2
14
+ exit 1
15
+ }
16
+
17
+ DRIVE_ROOT="${COZE_DRIVE_ROOT:-/Coze/Drive}"
18
+ if [ -d "$DRIVE_ROOT" ]; then
19
+ DRIVE_ROOT="$(cd "$DRIVE_ROOT" && pwd -P)"
20
+ else
21
+ DRIVE_ROOT="${DRIVE_ROOT%/}"
22
+ fi
23
+ case "$PROJECT_ROOT" in
24
+ "$DRIVE_ROOT"|"$DRIVE_ROOT"/*) ;;
25
+ *)
26
+ (cd "$PROJECT_ROOT" && pnpm install "$@")
27
+ exit 0
28
+ ;;
29
+ esac
30
+
31
+ NM_ROOT="/tmp/nm"
32
+ PROJECT_ID="$(basename "$PROJECT_ROOT")-$(printf '%s' "$PROJECT_ROOT" | cksum | awk '{print $1}')"
33
+ mkdir -p "$NM_ROOT"
34
+ WORK_DIR="$NM_ROOT/$PROJECT_ID"
35
+ case "$WORK_DIR" in
36
+ "$DRIVE_ROOT"|"$DRIVE_ROOT"/*)
37
+ echo "[node_modules] local dependency directory must not be on Coze Drive: $WORK_DIR" >&2
38
+ exit 1
39
+ ;;
40
+ esac
41
+
42
+ LOCK_DIR="$NM_ROOT/$PROJECT_ID.lock"
43
+ LOCK_TIMEOUT_SECONDS="${COZE_NODE_MODULES_LOCK_TIMEOUT_SECONDS:-120}"
44
+ case "$LOCK_TIMEOUT_SECONDS" in
45
+ ''|*[!0-9]*|0)
46
+ echo "[node_modules] COZE_NODE_MODULES_LOCK_TIMEOUT_SECONDS must be a positive integer" >&2
47
+ exit 1
48
+ ;;
49
+ esac
50
+
51
+ lock_owner_is_alive() {
52
+ local owner_pid=""
53
+ [ -f "$LOCK_DIR/pid" ] || return 1
54
+ IFS= read -r owner_pid < "$LOCK_DIR/pid" || return 1
55
+ case "$owner_pid" in
56
+ ''|*[!0-9]*) return 1 ;;
57
+ esac
58
+ kill -0 "$owner_pid" 2>/dev/null
59
+ }
60
+
61
+ release_lock() {
62
+ local owner_pid=""
63
+ [ -f "$LOCK_DIR/pid" ] || return 0
64
+ IFS= read -r owner_pid < "$LOCK_DIR/pid" || true
65
+ if [ "$owner_pid" = "$$" ]; then
66
+ rm -f "$LOCK_DIR/pid"
67
+ rmdir "$LOCK_DIR" 2>/dev/null || true
68
+ fi
69
+ }
70
+
71
+ lock_started_at="$(date +%s)"
72
+ missing_owner_checks=0
73
+ while ! mkdir "$LOCK_DIR" 2>/dev/null; do
74
+ if lock_owner_is_alive; then
75
+ missing_owner_checks=0
76
+ else
77
+ missing_owner_checks=$((missing_owner_checks + 1))
78
+ if [ "$missing_owner_checks" -ge 2 ]; then
79
+ stale_lock_dir="$LOCK_DIR.stale.$$"
80
+ if mv "$LOCK_DIR" "$stale_lock_dir" 2>/dev/null; then
81
+ echo "[node_modules] Recovering stale dependency preparation lock."
82
+ rm -rf "$stale_lock_dir"
83
+ fi
84
+ missing_owner_checks=0
85
+ continue
86
+ fi
87
+ fi
88
+
89
+ lock_now="$(date +%s)"
90
+ if [ $((lock_now - lock_started_at)) -ge "$LOCK_TIMEOUT_SECONDS" ]; then
91
+ echo "[node_modules] Timed out waiting ${LOCK_TIMEOUT_SECONDS}s for dependency preparation lock: $LOCK_DIR" >&2
92
+ exit 1
93
+ fi
94
+ echo "[node_modules] Waiting for another dependency preparation for this project..."
95
+ sleep 1
96
+ done
97
+ printf '%s\n' "$$" > "$LOCK_DIR/pid"
98
+ trap release_lock EXIT
99
+
100
+ mkdir -p "$WORK_DIR"
101
+ INSTALL_STATE_FILE="$WORK_DIR/.install-state"
102
+
103
+ # Previous versions mirrored the whole project into WORK_DIR. That gives pnpm
104
+ # and framework dev servers two paths for the same source tree, which breaks
105
+ # file watching and on-demand compilation. Keep only managed local outputs.
106
+ cleanup_legacy_workspace() {
107
+ local workspace_dir="$1" entry name
108
+ if [ -L "$workspace_dir" ] || { [ -e "$workspace_dir" ] && [ ! -d "$workspace_dir" ]; }; then
109
+ rm -rf "$workspace_dir"
110
+ return
111
+ fi
112
+ if [ ! -d "$workspace_dir" ]; then
113
+ return 0
114
+ fi
115
+
116
+ while IFS= read -r -d '' entry; do
117
+ name="$(basename "$entry")"
118
+ [ "$name" = "node_modules" ] || rm -rf "$entry"
119
+ done < <(find "$workspace_dir" -mindepth 1 -maxdepth 1 -print0)
120
+ }
121
+
122
+ # Preserve existing project caches during dependency preparation.
123
+ cleanup_legacy_shadow_project() {
124
+ local entry name
125
+ while IFS= read -r -d '' entry; do
126
+ name="$(basename "$entry")"
127
+ case "$name" in
128
+ node_modules|client|server|caches|.install-state) continue ;;
129
+ esac
130
+ rm -rf "$entry"
131
+ done < <(find "$WORK_DIR" -mindepth 1 -maxdepth 1 -print0)
132
+
133
+ for workspace in client server; do
134
+ cleanup_legacy_workspace "$WORK_DIR/$workspace"
135
+ done
136
+ }
137
+
138
+ link_install_entry() {
139
+ local source_path="$1" target_path="$2"
140
+ rm -rf "$target_path"
141
+ if [ -e "$source_path" ]; then
142
+ ln -s "$source_path" "$target_path"
143
+ fi
144
+ }
145
+
146
+ hash_install_input() {
147
+ if command -v sha256sum >/dev/null 2>&1; then
148
+ sha256sum | awk '{print $1}'
149
+ elif command -v shasum >/dev/null 2>&1; then
150
+ shasum -a 256 | awk '{print $1}'
151
+ else
152
+ cksum | awk '{print $1 "-" $2}'
153
+ fi
154
+ }
155
+
156
+ append_file_to_install_fingerprint() {
157
+ local relative_path="$1"
158
+ local source_path="$PROJECT_ROOT/$relative_path"
159
+ printf 'path=%s\n' "$relative_path"
160
+ if [ -f "$source_path" ]; then
161
+ cat "$source_path"
162
+ else
163
+ printf '<missing>\n'
164
+ fi
165
+ }
166
+
167
+ append_directory_to_install_fingerprint() {
168
+ local relative_path="$1"
169
+ local source_path="$PROJECT_ROOT/$relative_path"
170
+ local file
171
+ printf 'path=%s\n' "$relative_path"
172
+ if [ ! -d "$source_path" ]; then
173
+ printf '<missing>\n'
174
+ return
175
+ fi
176
+
177
+ while IFS= read -r file; do
178
+ printf 'file=%s\n' "${file#"${PROJECT_ROOT}/"}"
179
+ cat "$file"
180
+ done < <(find -P "$source_path" -type f -print | LC_ALL=C sort)
181
+ }
182
+
183
+ # Everything that can change the resolved dependency tree. The leading version marker
184
+ # invalidates every stored state when this scheme itself changes.
185
+ calculate_install_fingerprint() {
186
+ local filename workspace argument
187
+ {
188
+ printf 'prepare-node-modules-install-state=1\n'
189
+ printf 'node-version=%s\n' "$(node --version)"
190
+ printf 'pnpm-version=%s\n' "$(pnpm --version)"
191
+ printf 'node-env=%s\n' "${NODE_ENV:-}"
192
+ printf 'npm-config-production=%s\n' "${npm_config_production:-}"
193
+ printf 'npm-config-production-upper=%s\n' "${NPM_CONFIG_PRODUCTION:-}"
194
+ printf 'pnpm-config-production=%s\n' "${PNPM_CONFIG_PRODUCTION:-}"
195
+ for argument in "$@"; do
196
+ printf 'install-argument=%s\n' "$argument"
197
+ done
198
+ for filename in package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc; do
199
+ append_file_to_install_fingerprint "$filename"
200
+ done
201
+ for workspace in client server; do
202
+ append_file_to_install_fingerprint "$workspace/package.json"
203
+ done
204
+ append_directory_to_install_fingerprint patches
205
+ } | hash_install_input
206
+ }
207
+
208
+ install_requires_refresh() {
209
+ local argument
210
+ for argument in "$@"; do
211
+ case "$argument" in
212
+ --force|-f|--lockfile-only|--fix-lockfile) return 0 ;;
213
+ esac
214
+ done
215
+ return 1
216
+ }
217
+
218
+ # Reuse skips pnpm install entirely, so it must not skip side effects that live outside
219
+ # node_modules. preinstall is exempt: it only guards which package manager may run, and
220
+ # there is no install to guard when the tree is already in place.
221
+ package_has_install_lifecycle() {
222
+ local package_path="$1"
223
+ node - "$package_path" <<'NODE'
224
+ const fs = require('node:fs');
225
+ const [packagePath] = process.argv.slice(2);
226
+
227
+ try {
228
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
229
+ const scripts = packageJson && typeof packageJson.scripts === 'object' && packageJson.scripts
230
+ ? packageJson.scripts
231
+ : {};
232
+ const hasLifecycle = ['install', 'postinstall', 'prepare'].some(
233
+ name => typeof scripts[name] === 'string' && scripts[name].trim() !== '',
234
+ );
235
+
236
+ process.exit(hasLifecycle ? 0 : 1);
237
+ } catch {
238
+ process.exit(0);
239
+ }
240
+ NODE
241
+ }
242
+
243
+ install_reuse_is_safe() {
244
+ local workspace package_path
245
+ for package_path in "$PROJECT_ROOT/.pnpmfile.cjs" "$PROJECT_ROOT/.pnpmfile.js"; do
246
+ if [ -e "$package_path" ]; then
247
+ echo "[node_modules] Reinstalling because pnpm hooks are present: $package_path"
248
+ return 1
249
+ fi
250
+ done
251
+
252
+ if [ -f "$PROJECT_ROOT/.npmrc" ] && grep -Eq '^[[:space:]]*pnpmfile[[:space:]]*=' "$PROJECT_ROOT/.npmrc"; then
253
+ echo "[node_modules] Reinstalling because .npmrc configures a pnpm hook."
254
+ return 1
255
+ fi
256
+
257
+ for workspace in . client server; do
258
+ package_path="$PROJECT_ROOT/$workspace/package.json"
259
+ [ -f "$package_path" ] || continue
260
+ if package_has_install_lifecycle "$package_path"; then
261
+ echo "[node_modules] Reinstalling because $package_path has an install lifecycle script."
262
+ return 1
263
+ fi
264
+ done
265
+ return 0
266
+ }
267
+
268
+ dependencies_are_ready() {
269
+ local workspace
270
+ [ -f "$WORK_DIR/node_modules/.modules.yaml" ] || return 1
271
+ for workspace in client server; do
272
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ] &&
273
+ [ ! -f "$WORK_DIR/$workspace/node_modules/.modules.yaml" ]; then
274
+ return 1
275
+ fi
276
+ done
277
+ return 0
278
+ }
279
+
280
+ # 判断顺序按代价从小到大排。指纹要读云盘上的 lockfile 和整个 patches/,所以放最后:只有其余
281
+ # 条件都满足、确实可能复用时才付这份开销。build.sh 和 prepare.sh 不设开关,第一行就返回了。
282
+ install_can_be_reused() {
283
+ local installed_fingerprint=""
284
+
285
+ [ "${COZE_REUSE_NODE_MODULES:-}" = "1" ] || return 1
286
+ install_requires_refresh "$@" && return 1
287
+ [ -f "$INSTALL_STATE_FILE" ] || return 1
288
+ IFS= read -r installed_fingerprint < "$INSTALL_STATE_FILE" || return 1
289
+ dependencies_are_ready || return 1
290
+ install_reuse_is_safe || return 1
291
+ [ "$installed_fingerprint" = "$(calculate_install_fingerprint "$@")" ] || return 1
292
+ return 0
293
+ }
294
+
295
+ write_install_state() {
296
+ local fingerprint="$1" temporary_state
297
+ temporary_state="$(mktemp "$WORK_DIR/.install-state.XXXXXX")"
298
+ printf '%s\n' "$fingerprint" > "$temporary_state"
299
+ mv -f "$temporary_state" "$INSTALL_STATE_FILE"
300
+ }
301
+
302
+ copy_install_metadata() {
303
+ local filename source_file target_file workspace
304
+ for filename in pnpm-lock.yaml pnpm-workspace.yaml .npmrc; do
305
+ source_file="$PROJECT_ROOT/$filename"
306
+ target_file="$WORK_DIR/$filename"
307
+ rm -f "$target_file"
308
+ if [ -f "$source_file" ]; then
309
+ cp "$source_file" "$target_file"
310
+ fi
311
+ done
312
+
313
+ link_install_entry "$PROJECT_ROOT/package.json" "$WORK_DIR/package.json"
314
+ link_install_entry "$PROJECT_ROOT/patches" "$WORK_DIR/patches"
315
+ link_install_entry "$PROJECT_ROOT/scripts" "$WORK_DIR/scripts"
316
+
317
+ for workspace in client server; do
318
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
319
+ mkdir -p "$WORK_DIR/$workspace"
320
+ link_install_entry "$PROJECT_ROOT/$workspace/package.json" "$WORK_DIR/$workspace/package.json"
321
+ link_install_entry "$PROJECT_ROOT/$workspace/scripts" "$WORK_DIR/$workspace/scripts"
322
+ else
323
+ rm -rf "$WORK_DIR/$workspace"
324
+ fi
325
+ done
326
+ }
327
+
328
+ link_node_modules() {
329
+ local source_dir="$1" target_dir="$2"
330
+ local current=""
331
+ mkdir -p "$target_dir"
332
+ if [ -L "$source_dir/node_modules" ]; then
333
+ current="$(readlink "$source_dir/node_modules")"
334
+ case "$current" in
335
+ "$NM_ROOT"/*/node_modules|"$NM_ROOT"/*/*/node_modules) ;;
336
+ *)
337
+ echo "[node_modules] refusing to replace unmanaged symlink: $source_dir/node_modules" >&2
338
+ exit 1
339
+ ;;
340
+ esac
341
+ rm "$source_dir/node_modules"
342
+ elif [ -e "$source_dir/node_modules" ]; then
343
+ rm -rf "$source_dir/node_modules"
344
+ fi
345
+ ln -s "$target_dir" "$source_dir/node_modules"
346
+ }
347
+
348
+ cleanup_legacy_shadow_project
349
+ copy_install_metadata
350
+
351
+ if install_can_be_reused "$@"; then
352
+ echo "[node_modules] Reusing unchanged dependencies in $WORK_DIR"
353
+ else
354
+ # A stale state file must not survive a failed install.
355
+ rm -f "$INSTALL_STATE_FILE"
356
+ echo "[node_modules] Installing dependencies in $WORK_DIR"
357
+ if ! (cd "$WORK_DIR" && pnpm install "$@"); then
358
+ exit 1
359
+ fi
360
+
361
+ if [ -f "$WORK_DIR/pnpm-lock.yaml" ]; then
362
+ temporary_lockfile="$(mktemp "$PROJECT_ROOT/.pnpm-lock.yaml.XXXXXX")"
363
+ cp "$WORK_DIR/pnpm-lock.yaml" "$temporary_lockfile"
364
+ chmod 0644 "$temporary_lockfile"
365
+ mv -f "$temporary_lockfile" "$PROJECT_ROOT/pnpm-lock.yaml"
366
+ fi
367
+
368
+ # pnpm may have rewritten the lockfile, so record the fingerprint of the tree as it
369
+ # now stands. Otherwise the next run would see a changed input and reinstall.
370
+ write_install_state "$(calculate_install_fingerprint "$@")"
371
+ fi
372
+
373
+ link_node_modules "$PROJECT_ROOT" "$WORK_DIR/node_modules"
374
+ for workspace in client server; do
375
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
376
+ link_node_modules "$PROJECT_ROOT/$workspace" "$WORK_DIR/$workspace/node_modules"
377
+ fi
378
+ done
379
+
380
+ echo "[node_modules] Dependencies ready"
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  if [ -z "${BASH_VERSION:-}" ]; then exec /usr/bin/env bash "$0" "$@"; fi
3
3
  set -euo pipefail
4
- ROOT_DIR="$(pwd)"
4
+ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
5
 
6
6
  # ==================== 工具函数 ====================
7
7
  info() {
@@ -32,8 +32,8 @@ info "==================== 安装 Node 依赖 ===================="
32
32
  info "开始安装 Node 依赖"
33
33
  if [ -f "$ROOT_DIR/package.json" ]; then
34
34
  info "进入目录:$ROOT_DIR"
35
- info "正在执行:pnpm install"
36
- (cd "$ROOT_DIR" && pnpm install --registry=https://registry.npmmirror.com) || error "Node 依赖安装失败"
35
+ info "正在执行受管依赖准备入口"
36
+ bash "$ROOT_DIR/.cozeproj/scripts/prepare-node-modules.sh" --registry=https://registry.npmmirror.com || error "Node 依赖安装失败"
37
37
  else
38
38
  warn "未找到 $ROOT_DIR/package.json 文件,请检查路径是否正确"
39
39
  fi
@@ -6,13 +6,19 @@ import {
6
6
  View,
7
7
  TouchableWithoutFeedback,
8
8
  Keyboard,
9
- ViewStyle,
9
+ type ViewStyle,
10
10
  FlatList,
11
+ type FlatListProps,
11
12
  SectionList,
13
+ type SectionListProps,
14
+ type ScrollViewProps,
12
15
  Modal,
13
16
  } from 'react-native';
14
17
  import { withUniwind } from 'uniwind';
15
- import { useSafeAreaInsets, Edge } from 'react-native-safe-area-context';
18
+ import {
19
+ useSafeAreaInsets,
20
+ type Edge,
21
+ } from 'react-native-safe-area-context';
16
22
  import { StatusBar } from 'expo-status-bar';
17
23
  // 引入 KeyboardAware 系列组件
18
24
  import {
@@ -86,58 +92,92 @@ type KeyboardAwareProps = {
86
92
  contentInsetBehaviorIOS: 'automatic' | 'never';
87
93
  };
88
94
 
95
+ type NativeScrollableProps = Pick<
96
+ ScrollViewProps,
97
+ | 'contentContainerStyle'
98
+ | 'keyboardShouldPersistTaps'
99
+ | 'keyboardDismissMode'
100
+ | 'contentInsetAdjustmentBehavior'
101
+ >;
102
+
89
103
  const KeyboardAwareScrollable = ({
90
104
  element,
91
105
  extraPadding,
92
106
  contentInsetBehaviorIOS,
93
107
  }: KeyboardAwareProps) => {
94
- // 获取原始组件的 props
95
- const childAttrs = ((element as React.ReactElement).props ?? {}) as Record<string, unknown>;
96
- const originStyle = childAttrs['contentContainerStyle'];
97
- const styleArray = Array.isArray(originStyle) ? originStyle : originStyle ? [originStyle] : [];
98
- const merged = Object.assign({}, ...styleArray);
99
- const currentPB = typeof merged.paddingBottom === 'number' ? merged.paddingBottom : 0;
108
+ const createEnhancedContentStyle = (
109
+ contentContainerStyle: ScrollViewProps['contentContainerStyle'],
110
+ ) => {
111
+ const mergedStyle = StyleSheet.flatten(contentContainerStyle) ?? {};
112
+ const currentPaddingBottom =
113
+ typeof mergedStyle.paddingBottom === 'number'
114
+ ? mergedStyle.paddingBottom
115
+ : 0;
100
116
 
101
- // 合并 paddingBottom (安全区 + 额外留白)
102
- const enhancedContentStyle = [{ ...merged, paddingBottom: currentPB + extraPadding }];
117
+ return [
118
+ {
119
+ ...mergedStyle,
120
+ paddingBottom: currentPaddingBottom + extraPadding,
121
+ },
122
+ ];
123
+ };
103
124
 
104
- // 基础配置 props,用于传递给 KeyboardAware 组件
105
- const commonProps = {
106
- ...childAttrs,
107
- contentContainerStyle: enhancedContentStyle,
108
- keyboardShouldPersistTaps: childAttrs['keyboardShouldPersistTaps'] ?? 'handled',
109
- keyboardDismissMode: childAttrs['keyboardDismissMode'] ?? 'on-drag',
125
+ const createKeyboardAwareProps = <Props extends NativeScrollableProps>(
126
+ childProps: Props,
127
+ ) => ({
128
+ ...childProps,
129
+ contentContainerStyle: createEnhancedContentStyle(
130
+ childProps.contentContainerStyle,
131
+ ),
132
+ keyboardShouldPersistTaps:
133
+ childProps.keyboardShouldPersistTaps ?? 'handled',
134
+ keyboardDismissMode: childProps.keyboardDismissMode ?? 'on-drag',
110
135
  enableOnAndroid: true,
111
136
  // 类似于原代码中的 setTimeout/scrollToEnd 逻辑,这里设置额外的滚动高度确保输入框可见
112
137
  extraHeight: 100,
113
138
  // 禁用自带的 ScrollView 自动 inset,由外部 padding 控制
114
139
  enableAutomaticScroll: true,
115
140
  ...(Platform.OS === 'ios'
116
- ? { contentInsetAdjustmentBehavior: childAttrs['contentInsetAdjustmentBehavior'] ?? contentInsetBehaviorIOS }
141
+ ? {
142
+ contentInsetAdjustmentBehavior:
143
+ childProps.contentInsetAdjustmentBehavior ??
144
+ contentInsetBehaviorIOS,
145
+ }
117
146
  : {}),
118
- };
147
+ });
119
148
 
120
- const t = (element as React.ReactElement).type;
149
+ const t = element.type;
121
150
 
122
151
  // 根据组件类型返回对应的 KeyboardAware 版本
123
152
  // 注意:不再使用 KeyboardAvoidingView,直接替换为增强版 ScrollView
124
153
  if (t === ScrollView) {
125
- return <KeyboardAwareScrollView {...commonProps} />;
154
+ const childProps = element.props as ScrollViewProps;
155
+ return (
156
+ <KeyboardAwareScrollView {...createKeyboardAwareProps(childProps)} />
157
+ );
126
158
  }
127
159
 
128
160
  if (t === FlatList) {
129
- return <KeyboardAwareFlatList {...commonProps} />;
161
+ const childProps = element.props as FlatListProps<unknown>;
162
+ return <KeyboardAwareFlatList {...createKeyboardAwareProps(childProps)} />;
130
163
  }
131
164
 
132
165
  if (t === SectionList) {
133
- return <KeyboardAwareSectionList {...commonProps} />;
166
+ const childProps = element.props as SectionListProps<unknown>;
167
+ return (
168
+ <KeyboardAwareSectionList {...createKeyboardAwareProps(childProps)} />
169
+ );
134
170
  }
135
171
 
136
172
  // 理论上不应运行到这里,如果是非标准组件则原样返回,仅修改样式
173
+ const childProps = element.props as NativeScrollableProps;
137
174
  return React.cloneElement(element, {
138
- contentContainerStyle: enhancedContentStyle,
139
- keyboardShouldPersistTaps: childAttrs['keyboardShouldPersistTaps'] ?? 'handled',
140
- keyboardDismissMode: childAttrs['keyboardDismissMode'] ?? 'on-drag',
175
+ contentContainerStyle: createEnhancedContentStyle(
176
+ childProps.contentContainerStyle,
177
+ ),
178
+ keyboardShouldPersistTaps:
179
+ childProps.keyboardShouldPersistTaps ?? 'handled',
180
+ keyboardDismissMode: childProps.keyboardDismissMode ?? 'on-drag',
141
181
  });
142
182
  };
143
183
 
@@ -1,5 +1,9 @@
1
1
  import React, { forwardRef, useMemo } from 'react';
2
- import { View, Text as RNText } from 'react-native';
2
+ import {
3
+ type GestureResponderEvent,
4
+ View,
5
+ Text as RNText,
6
+ } from 'react-native';
3
7
  import { GestureDetector } from 'react-native-gesture-handler';
4
8
  import Animated from 'react-native-reanimated';
5
9
  import { useThemeColor } from '../../helpers/external/hooks';
@@ -339,7 +343,7 @@ const ToastClose = forwardRef<View, ToastCloseProps>((props, ref) => {
339
343
  * If hide and id are available from context, use them to hide the toast
340
344
  * Otherwise, use the provided onPress handler
341
345
  */
342
- const handlePress = (event: Parameters<NonNullable<React.ComponentProps<typeof Button>['onPress']>>[0]) => {
346
+ const handlePress = (event: GestureResponderEvent) => {
343
347
  if (hide && id) {
344
348
  hide(id);
345
349
  }