@coze-arch/cli 0.1.8-alpha.6e99f9 → 0.1.8-alpha.7654a3

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 (48) hide show
  1. package/README.md +18 -0
  2. package/lib/__templates__/expo/.cozeproj/scripts/prepare-node-modules.sh +1 -3
  3. package/lib/__templates__/native-static/.coze +2 -2
  4. package/lib/__templates__/nextjs/.coze +1 -1
  5. package/lib/__templates__/nextjs/README.md +18 -0
  6. package/lib/__templates__/nextjs/eslint.config.mjs +1 -1
  7. package/lib/__templates__/nextjs/next.config.ts +0 -9
  8. package/lib/__templates__/nextjs/scripts/build.ps1 +1 -1
  9. package/lib/__templates__/nextjs/scripts/build.sh +2 -2
  10. package/lib/__templates__/nextjs/scripts/dev.ps1 +2 -2
  11. package/lib/__templates__/nextjs/scripts/dev.sh +16 -62
  12. package/lib/__templates__/nextjs/scripts/local-workspace.cjs +302 -0
  13. package/lib/__templates__/nextjs/scripts/prepare.sh +10 -2
  14. package/lib/__templates__/nextjs/scripts/validate.sh +11 -1
  15. package/lib/__templates__/nextjs/src/server.ts +1 -1
  16. package/lib/__templates__/nextjs/template.config.js +20 -6
  17. package/lib/__templates__/nextjs/tsconfig.json +0 -1
  18. package/lib/__templates__/nuxt-vue/.coze +1 -1
  19. package/lib/__templates__/nuxt-vue/README.md +20 -2
  20. package/lib/__templates__/nuxt-vue/scripts/build.sh +1 -1
  21. package/lib/__templates__/nuxt-vue/scripts/dev.sh +10 -4
  22. package/lib/__templates__/nuxt-vue/scripts/local-workspace.cjs +302 -0
  23. package/lib/__templates__/nuxt-vue/scripts/prepare.sh +10 -2
  24. package/lib/__templates__/nuxt-vue/scripts/validate.sh +11 -1
  25. package/lib/__templates__/taro/.coze +2 -2
  26. package/lib/__templates__/taro/.cozeproj/scripts/deploy_build.sh +6 -1
  27. package/lib/__templates__/taro/.cozeproj/scripts/dev_build.sh +10 -1
  28. package/lib/__templates__/taro/.cozeproj/scripts/dev_run.sh +56 -4
  29. package/lib/__templates__/taro/.cozeproj/scripts/local-workspace.cjs +351 -0
  30. package/lib/__templates__/taro/.cozeproj/scripts/pack.sh +16 -6
  31. package/lib/__templates__/taro/.cozeproj/scripts/prepare-node-modules.sh +1 -3
  32. package/lib/__templates__/taro/.cozeproj/scripts/taro-build.cjs +66 -0
  33. package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +3 -0
  34. package/lib/__templates__/taro/package.json +5 -5
  35. package/lib/__templates__/vite/.coze +1 -1
  36. package/lib/__templates__/vite/README.md +20 -0
  37. package/lib/__templates__/vite/scripts/build.sh +1 -1
  38. package/lib/__templates__/vite/scripts/dev.sh +10 -4
  39. package/lib/__templates__/vite/scripts/local-workspace.cjs +302 -0
  40. package/lib/__templates__/vite/scripts/prepare.sh +10 -2
  41. package/lib/__templates__/vite/scripts/validate.sh +7 -1
  42. package/lib/__templates__/vite/template.config.js +20 -6
  43. package/lib/cli.js +367 -184
  44. package/package.json +2 -1
  45. package/lib/__templates__/nextjs/scripts/prepare-next-dev-output.sh +0 -122
  46. package/lib/__templates__/nextjs/scripts/prepare-node-modules.sh +0 -382
  47. package/lib/__templates__/nuxt-vue/scripts/prepare-node-modules.sh +0 -382
  48. package/lib/__templates__/vite/scripts/prepare-node-modules.sh +0 -382
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。
@@ -119,9 +119,7 @@ cleanup_legacy_workspace() {
119
119
  done < <(find "$workspace_dir" -mindepth 1 -maxdepth 1 -print0)
120
120
  }
121
121
 
122
- # caches is reserved for the sibling prepare scripts of this project. They keep
123
- # regenerable framework output there (the Next helper points .next/dev into it), and that
124
- # output has to outlive an install, so this cleanup must not read it as leftover.
122
+ # Preserve existing project caches during dependency preparation.
125
123
  cleanup_legacy_shadow_project() {
126
124
  local entry name
127
125
  while IFS= read -r -d '' entry; do
@@ -5,10 +5,10 @@ project_type = "web"
5
5
 
6
6
  [dev]
7
7
  build = []
8
- run = ["python3", "-m", "http.server", "${DEPLOY_RUN_PORT}", "--bind", "0.0.0.0"]
8
+ run = [ "sh", "-c", "exec python3 -m http.server ${DEPLOY_RUN_PORT} --bind 0.0.0.0" ]
9
9
  run_win = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "./scripts/serve.ps1"]
10
10
 
11
11
  [deploy]
12
12
  build = []
13
- run = ["python3", "-m", "http.server", "${DEPLOY_RUN_PORT}", "--bind", "0.0.0.0"]
13
+ run = [ "sh", "-c", "exec python3 -m http.server ${DEPLOY_RUN_PORT} --bind 0.0.0.0" ]
14
14
  run_win = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "./scripts/serve.ps1"]
@@ -9,7 +9,7 @@ run = ["bash", "./scripts/dev.sh"]
9
9
  run_win = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "./scripts/dev.ps1"]
10
10
  validate = ["bash", "./scripts/validate.sh"]
11
11
  validate_win = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "./scripts/validate.ps1"]
12
- deps = ["git"] # -> apt install git
12
+ deps = ["git", "rsync"] # -> apt install git rsync
13
13
 
14
14
  [deploy]
15
15
  build = ["bash","./scripts/build.sh"]
@@ -361,3 +361,21 @@ export const useStore = create<Store>((set) => ({
361
361
  3. **遵循 Next.js App Router 规范**,正确区分服务端/客户端组件
362
362
  4. **使用 TypeScript** 进行类型安全开发
363
363
  5. **使用 `@/` 路径别名** 导入模块(已配置)
364
+
365
+ ## 云盘源码与本地开发
366
+
367
+ Vite、Next.js、Nuxt 的 Unix 开发脚本在 `COZE_DRIVE_ROOT`(默认 `/Coze/Drive`)内通过
368
+ `scripts/local-workspace.cjs` 将源码用 `rsync` 同步到本机临时目录。系统需提供 Node.js、pnpm、bash 和 rsync。
369
+ 所有开发与修复仍在云盘源码目录完成,从该目录执行 `.coze` 的 `[dev]` 命令。
370
+
371
+ - `prepare.sh` 同步并在本地安装依赖;安装成功且云盘依赖输入未变化时,仅将更新的锁文件回写云盘。
372
+ - `dev.sh` 在本地镜像运行开发服务,每轮 rsync 结束后等待一秒再同步,框架监听本地文件进行热更新。
373
+ 同步失败或依赖输入变化时停止服务;修复后重新运行 `prepare.sh` / `dev.sh`。
374
+ - `validate.sh` 使用独立的本地 `check` 目录,避免类型生成影响正在运行的 `dev` 服务。
375
+ - 同步排除 `.git`、`node_modules`、日志和框架输出;不要在镜像修复源码或将依赖、编译产物回传云盘。
376
+ 日志和 PID 默认保留在源码目录的 `logs/`,实际镜像路径见脚本输出。
377
+
378
+ 本地镜像仅用于 `[dev].build/run/validate`。部署配置、`build.sh`、`start.sh` 保持原流程;
379
+ 普通本地目录和 Windows PowerShell 入口也保持原流程。
380
+ 存量项目通过新版 CLI 的 `coze-dev patch --dry-run` / `coze-dev patch` 升级,沿用 patch 总开关。
381
+ 仅升级完整匹配旧模板的三个 Unix 开发脚本并保留端口;自定义开发脚本、命令或同名 helper 会跳过。
@@ -52,7 +52,7 @@ const eslintConfig = defineConfig([
52
52
  'server.js',
53
53
  'dist/',
54
54
  // Script files (CommonJS):
55
- 'scripts/**/*.js',
55
+ 'scripts/**/*.{js,cjs}',
56
56
  ]),
57
57
  ]);
58
58
 
@@ -1,18 +1,9 @@
1
1
  import type { NextConfig } from 'next';
2
- import path from 'path';
3
2
 
4
3
  const nextConfig: NextConfig = {
5
4
  // outputFileTracingRoot: path.resolve(__dirname, '../../'), // Uncomment and add 'import path from "path"' if needed
6
5
  /* config options here */
7
6
  serverExternalPackages: ['coze-coding-dev-sdk'],
8
- webpack: (config, { dev }) => {
9
- if (dev && config.cache && config.cache.type === 'filesystem') {
10
- config.cache.cacheDirectory = path.resolve(
11
- 'node_modules/.cache/next-webpack',
12
- );
13
- }
14
- return config;
15
- },
16
7
  allowedDevOrigins: ['*.dev.coze.site'],
17
8
  images: {
18
9
  remotePatterns: [
@@ -8,7 +8,7 @@ Write-Host "Installing dependencies..."
8
8
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
9
9
 
10
10
  Write-Host "Building the Next.js project..."
11
- & pnpm next build --webpack
11
+ & pnpm next build
12
12
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
13
13
 
14
14
  Write-Host "Bundling server with tsup..."
@@ -6,10 +6,10 @@ COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
6
6
  cd "${COZE_WORKSPACE_PATH}"
7
7
 
8
8
  echo "Installing dependencies..."
9
- bash "$COZE_WORKSPACE_PATH/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only
9
+ pnpm install --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only
10
10
 
11
11
  echo "Building the Next.js project..."
12
- pnpm next build --webpack
12
+ pnpm next build
13
13
 
14
14
  echo "Bundling server with tsup..."
15
15
  pnpm tsup src/server.ts --format cjs --platform node --target node20 --outDir dist --no-splitting --no-minify
@@ -28,7 +28,7 @@ Stop-ListeningProcess $port
28
28
 
29
29
  <% if (process.env.NODE_ENV === 'test') { %>
30
30
  $env:PORT = "$port"
31
- & pnpm next dev --webpack --hostname 0.0.0.0 --port $port
31
+ & pnpm next dev --hostname 0.0.0.0 --port $port
32
32
  exit $LASTEXITCODE
33
33
  <% } else { %>
34
34
  $logDirectory = if ($env:COZE_LOG_DIR) { $env:COZE_LOG_DIR } else { Join-Path $workspace "logs" }
@@ -45,7 +45,7 @@ if (Test-Path $pidFile) {
45
45
  $previousPort = $env:PORT
46
46
  $env:PORT = "$port"
47
47
  try {
48
- $process = Start-Process -FilePath $env:ComSpec -ArgumentList @("/d", "/s", "/c", "pnpm next dev --webpack --hostname 0.0.0.0 --port $port") -WorkingDirectory $workspace -RedirectStandardOutput $logFile -RedirectStandardError $errorLogFile -PassThru
48
+ $process = Start-Process -FilePath $env:ComSpec -ArgumentList @("/d", "/s", "/c", "pnpm next dev --hostname 0.0.0.0 --port $port") -WorkingDirectory $workspace -RedirectStandardOutput $logFile -RedirectStandardError $errorLogFile -PassThru
49
49
  } finally {
50
50
  if ($null -eq $previousPort) { Remove-Item Env:PORT -ErrorAction SilentlyContinue } else { $env:PORT = $previousPort }
51
51
  }
@@ -1,14 +1,20 @@
1
1
  #!/bin/bash
2
2
  set -Eeuo pipefail
3
3
 
4
+ # Resolve the source project from this script, then let the Web runner choose its local mirror.
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
6
+ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)"
7
+ if [[ "${COZE_WEB_LOCAL_ACTIVE:-}" != "${PROJECT_ROOT}" ]]; then
8
+ exec node "${SCRIPT_DIR}/local-workspace.cjs" dev "$@"
9
+ fi
10
+ COZE_WORKSPACE_PATH="${PROJECT_ROOT}"
11
+
4
12
  <% if (process.env.NODE_ENV === 'test') { %>
5
13
  # 测试环境:支持环境变量覆盖端口
6
14
  PORT="${PORT:-<%= port %>}"
7
- COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
8
15
  DEPLOY_RUN_PORT="${DEPLOY_RUN_PORT:-${PORT}}"
9
16
  <% } else { %>
10
17
  PORT="${DEPLOY_RUN_PORT:-${PORT:-<%= port %>}}"
11
- COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
12
18
  DEPLOY_RUN_PORT="${DEPLOY_RUN_PORT:-${PORT}}"
13
19
  <% } %>
14
20
 
@@ -52,68 +58,17 @@ kill_port_if_listening() {
52
58
  echo "Port ${DEPLOY_RUN_PORT} cleared."
53
59
  }
54
60
 
55
- # Next 的路由清单由 watchpack 每次启动重新扫 src 下的 app/pages 目录现算,而 watchpack
56
- # 不会进入软链目录,软链子树里的 page/route 会被静默跳过:既不注册路由,也不打任何日志,
57
- # 表现为该路由一直 404 而 .next 里没有它的产物。这里只做提示,不改动源码。
58
- warn_source_symlinks() {
59
- local source_dir="${COZE_WORKSPACE_PATH}/src"
60
- local links link
61
- if [[ ! -e "${source_dir}" ]]; then
62
- return
63
- fi
64
- links=$(find "${source_dir}" -type l 2>/dev/null || true)
65
- if [[ -z "${links}" ]]; then
66
- return
67
- fi
68
- echo "Warning: 以下路径是软链,Next 不会进入其中,里面的 page/route 不会被编译:" >&2
69
- while IFS= read -r link; do
70
- echo " ${link}" >&2
71
- done <<< "${links}"
72
- }
73
-
74
- # 云盘挂载偶尔会把新建文件的 mtime 设为 Unix epoch,这种时间戳会让所有按 mtime 判断新旧的
75
- # 环节(Next 的首轮路由扫描、webpack 的 timestamp 快照、tsbuildinfo)拿到错误的结论。
76
- # 只修 mtime 确实异常的那些条目:无条件刷新整棵 app/pages 会让 webpack 每次启动都重算全部
77
- # 路由文件的 hash,并让云盘把它们全部重传一遍。不跟随软链、不改文件内容。
78
- # 判定 mtime 是否异常的下界。真实的项目文件不可能早于这个时间,落在它之前的只有被云盘写坏的
79
- # 时间戳,所以用它把"需要修"和"本来就好"区分开。
80
- DRIVE_MTIME_FLOOR="1971-01-01"
81
-
82
- refresh_drive_route_mtimes() {
83
- local drive_root="${COZE_DRIVE_ROOT:-/Coze/Drive}"
84
- local project_root route_dir
85
- project_root="$(pwd -P)"
86
- if [[ -d "${drive_root}" ]]; then
87
- drive_root="$(cd "${drive_root}" && pwd -P)"
88
- else
89
- drive_root="${drive_root%/}"
90
- fi
91
-
92
- case "${project_root}" in
93
- "${drive_root}"|"${drive_root}"/*) ;;
94
- *) return ;;
95
- esac
96
-
97
- for route_dir in "${COZE_WORKSPACE_PATH}/src/app" "${COZE_WORKSPACE_PATH}/src/pages"; do
98
- [[ -d "${route_dir}" ]] || continue
99
- if ! find -P "${route_dir}" \( -type f -o -type d \) ! -newermt "${DRIVE_MTIME_FLOOR}" -exec touch {} + 2>/dev/null; then
100
- echo "Warning: failed to refresh Next route timestamps in ${route_dir}." >&2
101
- fi
102
- done
103
- }
104
-
105
61
  <% if (process.env.NODE_ENV === 'test') { %>
106
62
  echo "Clearing port ${DEPLOY_RUN_PORT} before start."
107
63
  kill_port_if_listening
108
- COZE_REUSE_NODE_MODULES=1 bash "${COZE_WORKSPACE_PATH}/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only
109
- bash "${COZE_WORKSPACE_PATH}/scripts/prepare-next-dev-output.sh"
110
- warn_source_symlinks
111
- refresh_drive_route_mtimes
64
+ if [[ "${COZE_WEB_LOCAL_MIRROR:-}" != "1" ]]; then
65
+ pnpm install --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only
66
+ fi
112
67
  echo "Starting HTTP service on port ${DEPLOY_RUN_PORT} for dev..."
113
68
 
114
69
  # 测试环境保持服务为前台进程,便于 e2e 收集输出并停止进程。
115
70
  # 开发环境直接使用 Next 的 dev CLI,避免云盘文件事件在 custom server 链路中丢失。
116
- exec pnpm next dev --webpack --hostname 0.0.0.0 --port "${DEPLOY_RUN_PORT}"
71
+ exec node "${SCRIPT_DIR}/local-workspace.cjs" watch pnpm next dev --hostname 0.0.0.0 --port "${DEPLOY_RUN_PORT}"
117
72
  <% } else { %>
118
73
  LOG_DIR="${COZE_LOG_DIR:-${COZE_WORKSPACE_PATH}/logs}"
119
74
  LOG_FILE="${LOG_DIR}/app.log"
@@ -300,10 +255,9 @@ fi
300
255
 
301
256
  echo "Clearing port ${DEPLOY_RUN_PORT} before start."
302
257
  kill_port_if_listening
303
- COZE_REUSE_NODE_MODULES=1 bash "${COZE_WORKSPACE_PATH}/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only
304
- bash "${COZE_WORKSPACE_PATH}/scripts/prepare-next-dev-output.sh"
305
- warn_source_symlinks
306
- refresh_drive_route_mtimes
258
+ if [[ "${COZE_WEB_LOCAL_MIRROR:-}" != "1" ]]; then
259
+ pnpm install --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only
260
+ fi
307
261
  echo "Starting HTTP service on port ${DEPLOY_RUN_PORT} for dev..."
308
262
 
309
263
  mkdir -p "${LOG_DIR}"
@@ -311,7 +265,7 @@ mkdir -p "${LOG_DIR}"
311
265
 
312
266
  export PORT="${DEPLOY_RUN_PORT}"
313
267
  server_pid="$(spawn_detached "${COZE_WORKSPACE_PATH}" "${LOG_FILE}" \
314
- "$(command -v pnpm)" next dev --webpack --hostname 0.0.0.0 --port "${DEPLOY_RUN_PORT}")"
268
+ "$(command -v node)" "${SCRIPT_DIR}/local-workspace.cjs" watch "$(command -v pnpm)" next dev --hostname 0.0.0.0 --port "${DEPLOY_RUN_PORT}")"
315
269
  if [[ -z "${server_pid}" ]]; then
316
270
  echo "Dev server failed to start: 未获取到后台进程 PID。" >&2
317
271
  dump_log
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+ // Web source is owned by Drive. Only these wrappers may write to the local mirror.
3
+ /* eslint-disable @typescript-eslint/no-require-imports -- Standalone CommonJS runtime shipped without CLI dependencies. */
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+ const { createHash } = require('node:crypto');
8
+ const { spawn, execFileSync } = require('node:child_process');
9
+ const { setTimeout: delay } = require('node:timers/promises');
10
+
11
+ const scriptRoot = fs.realpathSync(path.resolve(__dirname, '..'));
12
+ const source = fs.realpathSync(
13
+ process.env.COZE_WEB_LOCAL_ACTIVE === scriptRoot && process.env.COZE_WEB_SOURCE_PATH
14
+ ? process.env.COZE_WEB_SOURCE_PATH : scriptRoot,
15
+ );
16
+ const drivePath = path.resolve(process.env.COZE_DRIVE_ROOT || '/Coze/Drive');
17
+ const drive = fs.existsSync(drivePath) ? fs.realpathSync(drivePath) : drivePath;
18
+ const inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path.sep}`);
19
+ const onDrive = inside(drive, source);
20
+ const digest = value => createHash('sha256').update(value).digest('hex');
21
+ // Separate from /tmp/nm: older dependency helpers clean unknown entries there.
22
+ const cacheRoot = path.join(os.tmpdir(), `coze-web-${process.getuid()}`);
23
+ const projectCache = path.join(cacheRoot, digest(source));
24
+ const exclusions = [
25
+ 'node_modules', '.git', '/logs', '.pnpm-store', '.next', '.nuxt', '.output',
26
+ '/dist', '/dist-server', '.cache', '.turbo', '*.tsbuildinfo',
27
+ '.eslintcache', '.stylelintcache', '/next-env.d.ts', '/tsconfig.json',
28
+ ];
29
+ const installArgs = ['install', '--prefer-frozen-lockfile', '--prefer-offline'];
30
+ let interrupted = false;
31
+ const children = new Set();
32
+
33
+ function stopTree(child) {
34
+ if (!child.pid || child.exitCode !== null) return;
35
+ // Keep descendants in the detached launcher's group so readiness ownership works.
36
+ // Also stop grandchildren when the test/foreground path is signalled directly.
37
+ let rows = [];
38
+ try {
39
+ rows = execFileSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8' })
40
+ .trim().split('\n').map(line => line.trim().split(/\s+/).map(Number));
41
+ } catch { /* The launcher still owns and reaps its process group. */ }
42
+ const pids = [child.pid];
43
+ for (let i = 0; i < pids.length; i++) {
44
+ for (const [pid, parent] of rows) if (parent === pids[i]) pids.push(pid);
45
+ }
46
+ for (const pid of pids.reverse()) {
47
+ try { process.kill(pid, 'SIGTERM'); } catch { /* Already exited. */ }
48
+ }
49
+ const timer = setTimeout(() => {
50
+ for (const pid of pids) {
51
+ try { process.kill(pid, 'SIGKILL'); } catch { /* Already exited. */ }
52
+ }
53
+ }, 2000);
54
+ timer.unref();
55
+ }
56
+
57
+ for (const signal of ['SIGINT', 'SIGTERM']) {
58
+ process.on(signal, () => {
59
+ interrupted = true;
60
+ process.exitCode = signal === 'SIGINT' ? 130 : 143;
61
+ for (const child of children) stopTree(child);
62
+ });
63
+ }
64
+
65
+ function run(command, args, cwd, env = process.env) {
66
+ if (interrupted) return Promise.reject(new Error('Interrupted'));
67
+ return new Promise((resolve, reject) => {
68
+ const child = spawn(command, args, { cwd, env, stdio: 'inherit' });
69
+ children.add(child);
70
+ child.once('error', error => { children.delete(child); reject(error); });
71
+ child.once('exit', (code, signal) => {
72
+ children.delete(child);
73
+ if (code === 0) resolve();
74
+ else reject(new Error(`${command} exited with ${signal || code}`));
75
+ });
76
+ });
77
+ }
78
+
79
+ function ensureCache() {
80
+ for (const dir of [cacheRoot, projectCache]) {
81
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
82
+ if (fs.lstatSync(dir).isSymbolicLink() || fs.statSync(dir).uid !== process.getuid()) {
83
+ throw new Error(`Refusing unmanaged workspace directory: ${dir}`);
84
+ }
85
+ if (inside(drive, fs.realpathSync(dir))) {
86
+ throw new Error(`Local workspace must be outside Coze Drive: ${dir}`);
87
+ }
88
+ }
89
+ }
90
+
91
+ async function locked(action, name = 'sync') {
92
+ const lock = path.join(projectCache, `${name}.lock`);
93
+ const started = Date.now();
94
+ let missingOwner = 0;
95
+ while (true) {
96
+ if (interrupted) throw new Error('Interrupted');
97
+ try { fs.mkdirSync(lock); break; } catch (error) {
98
+ if (error.code !== 'EEXIST') throw error;
99
+ }
100
+ let alive = false;
101
+ try {
102
+ const pid = Number(fs.readFileSync(path.join(lock, 'pid'), 'utf8'));
103
+ if (Number.isInteger(pid) && pid > 0) { process.kill(pid, 0); alive = true; }
104
+ } catch (error) { if (error.code === 'EPERM') alive = true; }
105
+ missingOwner = alive ? 0 : missingOwner + 1;
106
+ if (missingOwner >= 2) {
107
+ const stale = `${lock}.stale-${process.pid}`;
108
+ try {
109
+ fs.renameSync(lock, stale);
110
+ fs.rmSync(stale, { recursive: true });
111
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
112
+ missingOwner = 0;
113
+ continue;
114
+ }
115
+ if (Date.now() - started > 120000) throw new Error(`Timed out waiting for ${lock}`);
116
+ await delay(500);
117
+ }
118
+ fs.writeFileSync(path.join(lock, 'pid'), String(process.pid));
119
+ try { return await action(); }
120
+ finally { fs.rmSync(lock, { recursive: true, force: true }); }
121
+ }
122
+
123
+ async function sync(local) {
124
+ if (!fs.existsSync(path.join(source, 'package.json'))) {
125
+ throw new Error(`Source project is unavailable: ${source}`);
126
+ }
127
+ fs.mkdirSync(local, { recursive: true });
128
+ if (fs.realpathSync(local) !== local) throw new Error(`Refusing workspace symlink: ${local}`);
129
+ // Checksums catch same-size edits with unchanged/epoch Drive mtimes. Do not use -t:
130
+ // changed local files get fresh mtimes and unchanged files retain watcher snapshots.
131
+ // Materialize source links so the framework never follows them back onto Drive.
132
+ await run('rsync', [
133
+ '-rLp', '--checksum', '--delete', '--delay-updates',
134
+ ...exclusions.map(entry => `--exclude=${entry}`), `${source}/`, `${local}/`,
135
+ ], source);
136
+ // Next rewrites these inputs (dev/types vs types and tsconfig defaults). Preserve
137
+ // generated local changes until their canonical source content actually changes.
138
+ for (const file of ['next-env.d.ts', 'tsconfig.json']) {
139
+ const input = path.join(source, file);
140
+ const output = path.join(local, file);
141
+ const state = path.join(projectCache, `${path.basename(local)}.${file}.source`);
142
+ const fingerprint = fs.existsSync(input) ? digest(fs.readFileSync(input)) : 'missing';
143
+ if (readOptional(state).toString() !== fingerprint || !fs.existsSync(output)) {
144
+ if (fingerprint !== 'missing') fs.copyFileSync(input, output);
145
+ else fs.rmSync(output, { force: true });
146
+ fs.writeFileSync(state, fingerprint);
147
+ }
148
+ }
149
+ }
150
+
151
+ function readOptional(file) {
152
+ try { return fs.readFileSync(file); }
153
+ catch (error) { if (error.code === 'ENOENT') return Buffer.alloc(0); throw error; }
154
+ }
155
+
156
+ function installInput(root) {
157
+ const hash = createHash('sha256');
158
+ const visit = (dir, all = false) => {
159
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
160
+ if (['node_modules', '.git', 'logs', '.pnpm-store', '.next', '.nuxt', '.output', 'dist', 'dist-server', '.cache', '.turbo'].includes(entry.name)) continue;
161
+ const file = path.join(dir, entry.name);
162
+ // Follow source links just as rsync -L does, while detecting loops explicitly.
163
+ const real = fs.realpathSync(file);
164
+ if (entry.isDirectory() || (entry.isSymbolicLink() && fs.statSync(file).isDirectory())) {
165
+ if (ancestors.has(real)) throw new Error(`Source symlink cycle: ${file}`);
166
+ ancestors.add(real);
167
+ visit(file, all || entry.name === 'patches');
168
+ ancestors.delete(real);
169
+ }
170
+ else if (all || ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc', '.pnpmfile.cjs', '.pnpmfile.js'].includes(entry.name)) {
171
+ hash.update(path.relative(root, file)).update('\0').update(readOptional(file)).update('\0');
172
+ }
173
+ }
174
+ };
175
+ const ancestors = new Set([fs.realpathSync(root)]);
176
+ visit(root);
177
+ return hash.digest('hex');
178
+ }
179
+
180
+ function localEnv(local) {
181
+ return {
182
+ ...process.env,
183
+ COZE_WEB_LOCAL_ACTIVE: local,
184
+ COZE_WEB_SOURCE_PATH: source,
185
+ COZE_WEB_LOCAL_MIRROR: onDrive ? '1' : '',
186
+ COZE_WORKSPACE_PATH: local,
187
+ // Keep the existing discoverable log/PID location in the source project.
188
+ COZE_LOG_DIR: process.env.COZE_LOG_DIR || path.join(source, 'logs'),
189
+ PWD: local,
190
+ INIT_CWD: local,
191
+ ...(onDrive ? { npm_config_store_dir: path.join(cacheRoot, 'store') } : {}),
192
+ };
193
+ }
194
+
195
+ async function install(local, mode) {
196
+ const state = path.join(projectCache, `${mode}.install`);
197
+ const version = execFileSync('pnpm', ['--version'], { encoding: 'utf8' }).trim();
198
+ const fingerprint = () => digest([
199
+ installInput(local), version, process.version, process.env.NODE_ENV,
200
+ process.env.npm_config_production, process.env.NPM_CONFIG_PRODUCTION,
201
+ process.env.PNPM_CONFIG_PRODUCTION,
202
+ ].join('\n'));
203
+ // Lifecycle scripts may generate files from arbitrary source, so do not reuse
204
+ // their install based on dependency metadata alone. Read manifests as text here
205
+ // (no executable hooks or private CLI dependencies in the standalone helper).
206
+ const manifest = readOptional(path.join(local, 'package.json')).toString();
207
+ const hasLifecycle = /"(?:install|postinstall|prepare)"\s*:/.test(manifest);
208
+ // Workspace and local file dependencies can run lifecycles outside the root.
209
+ // Let pnpm validate those installs instead of assuming the root marker is enough.
210
+ const hasLocalDependencies = fs.existsSync(path.join(local, 'pnpm-workspace.yaml')) ||
211
+ /"(?:workspace|file|link):/.test(manifest);
212
+ const hasHooks = ['.pnpmfile.cjs', '.pnpmfile.js'].some(file => fs.existsSync(path.join(local, file))) ||
213
+ /^\s*pnpmfile\s*=/m.test(readOptional(path.join(local, '.npmrc')).toString());
214
+ if (!hasLifecycle && !hasHooks && !hasLocalDependencies && readOptional(state).toString() === fingerprint() &&
215
+ fs.existsSync(path.join(local, 'node_modules', '.modules.yaml'))) return;
216
+ fs.rmSync(state, { force: true });
217
+ const before = installInput(source);
218
+ if (before !== installInput(local)) {
219
+ throw new Error('Dependency inputs changed during sync; rerun prepare.sh.');
220
+ }
221
+ await run('pnpm', [...installArgs, '--store-dir', path.join(cacheRoot, 'store')], local, localEnv(local));
222
+ if (before !== installInput(source)) {
223
+ throw new Error('Dependency inputs changed on Drive during installation; rerun prepare.sh. Lockfile was not overwritten.');
224
+ }
225
+ const lockfile = path.join(local, 'pnpm-lock.yaml');
226
+ if (fs.existsSync(lockfile) && !readOptional(lockfile).equals(readOptional(path.join(source, 'pnpm-lock.yaml')))) {
227
+ const temporary = path.join(source, `.pnpm-lock.yaml.${process.pid}.tmp`);
228
+ try {
229
+ fs.copyFileSync(lockfile, temporary);
230
+ fs.renameSync(temporary, path.join(source, 'pnpm-lock.yaml'));
231
+ } finally { fs.rmSync(temporary, { force: true }); }
232
+ }
233
+ fs.writeFileSync(state, fingerprint());
234
+ }
235
+
236
+ async function watch(args) {
237
+ const local = fs.realpathSync(process.env.COZE_WEB_LOCAL_ACTIVE || path.resolve(__dirname, '..'));
238
+ if (!onDrive) return run(args[0], args.slice(1), local);
239
+ ensureCache();
240
+ if (local !== path.join(fs.realpathSync(projectCache), 'dev')) {
241
+ throw new Error(`Refusing to watch an unmanaged workspace: ${local}`);
242
+ }
243
+ const preparedInput = installInput(local);
244
+ let running = true;
245
+ let syncError;
246
+ const command = run(args[0], args.slice(1), local).finally(() => { running = false; });
247
+ const poll = (async () => {
248
+ while (running && !interrupted) {
249
+ await delay(1000);
250
+ if (!running || interrupted) break;
251
+ try {
252
+ await locked(async () => {
253
+ await sync(local);
254
+ if (installInput(local) !== preparedInput) {
255
+ throw new Error('Dependency inputs changed; rerun prepare.sh and dev.sh before previewing.');
256
+ }
257
+ });
258
+ }
259
+ catch (error) {
260
+ syncError = error;
261
+ for (const child of children) stopTree(child);
262
+ break;
263
+ }
264
+ }
265
+ })();
266
+ const results = await Promise.allSettled([command, poll]);
267
+ if (syncError) throw syncError;
268
+ if (results[0].status === 'rejected') throw results[0].reason;
269
+ }
270
+
271
+ async function main() {
272
+ const [action, ...args] = process.argv.slice(2);
273
+ if (action === 'watch') return watch(args);
274
+ if (!['prepare', 'dev', 'validate'].includes(action)) throw new Error(`Unknown Web action: ${action}`);
275
+ if (!onDrive) {
276
+ return run('bash', [path.join(source, 'scripts', `${action}.sh`), ...args], source, localEnv(source));
277
+ }
278
+ ensureCache();
279
+ const mode = action === 'validate' ? 'check' : 'dev';
280
+ const local = path.join(fs.realpathSync(projectCache), mode);
281
+ console.log(`[web] Source: ${source}\n[web] Local ${mode} workspace: ${local}`);
282
+ if (action === 'validate') {
283
+ return locked(async () => {
284
+ await locked(async () => {
285
+ await sync(local);
286
+ await install(local, mode);
287
+ });
288
+ await run('bash', [path.join(local, 'scripts', 'validate.sh'), ...args], local, localEnv(local));
289
+ }, 'check');
290
+ } else {
291
+ await locked(async () => {
292
+ await sync(local);
293
+ await install(local, mode);
294
+ });
295
+ }
296
+ await run('bash', [path.join(local, 'scripts', `${action}.sh`), ...args], local, localEnv(local));
297
+ }
298
+
299
+ main().catch(error => {
300
+ console.error(`[web] ${error.message}`);
301
+ process.exitCode = process.exitCode || 1;
302
+ });
@@ -1,9 +1,17 @@
1
1
  #!/bin/bash
2
2
  set -Eeuo pipefail
3
3
 
4
- COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
4
+ # Resolve the source project from this script, then let the Web runner choose its local mirror.
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
6
+ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)"
7
+ if [[ "${COZE_WEB_LOCAL_ACTIVE:-}" != "${PROJECT_ROOT}" ]]; then
8
+ exec node "${SCRIPT_DIR}/local-workspace.cjs" prepare "$@"
9
+ fi
10
+ COZE_WORKSPACE_PATH="${PROJECT_ROOT}"
5
11
 
6
12
  cd "${COZE_WORKSPACE_PATH}"
7
13
 
8
14
  echo "Installing dependencies..."
9
- bash "$COZE_WORKSPACE_PATH/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only
15
+ if [[ "${COZE_WEB_LOCAL_MIRROR:-}" != "1" ]]; then
16
+ pnpm install --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only
17
+ fi
@@ -1,10 +1,20 @@
1
1
  #!/bin/bash
2
2
  set -Eeuo pipefail
3
3
 
4
- COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
4
+ # Resolve the source project from this script, then let the Web runner choose its local mirror.
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
6
+ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)"
7
+ if [[ "${COZE_WEB_LOCAL_ACTIVE:-}" != "${PROJECT_ROOT}" ]]; then
8
+ exec node "${SCRIPT_DIR}/local-workspace.cjs" validate "$@"
9
+ fi
10
+ COZE_WORKSPACE_PATH="${PROJECT_ROOT}"
5
11
 
6
12
  cd "${COZE_WORKSPACE_PATH}"
7
13
 
14
+ if [[ "${COZE_WEB_LOCAL_MIRROR:-}" == "1" ]]; then
15
+ pnpm next typegen
16
+ fi
17
+
8
18
  echo "🔍 Running validate..."
9
19
  pnpm validate
10
20
  echo "✅ Validate passed!"
@@ -7,7 +7,7 @@ const hostname = process.env.HOSTNAME || 'localhost';
7
7
  const port = parseInt(process.env.PORT || '<%= port %>', 10);
8
8
 
9
9
  // Create Next.js app
10
- const app = next({ dev, hostname, port, webpack: true });
10
+ const app = next({ dev, hostname, port });
11
11
  const handle = app.getRequestHandler();
12
12
 
13
13
  app.prepare().then(() => {