@coze-arch/cli 0.1.4-alpha.7fcfb3 → 0.1.4-alpha.99c367

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.
@@ -40,84 +40,246 @@ case "$WORK_DIR" in
40
40
  esac
41
41
 
42
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
+ lock_acquired=false
62
+ release_lock() {
63
+ local owner_pid=""
64
+ if [ "$lock_acquired" != true ] || [ ! -f "$LOCK_DIR/pid" ]; then
65
+ return 0
66
+ fi
67
+ IFS= read -r owner_pid < "$LOCK_DIR/pid" || true
68
+ if [ "$owner_pid" = "$$" ]; then
69
+ rm -f "$LOCK_DIR/pid"
70
+ rmdir "$LOCK_DIR" 2>/dev/null || true
71
+ fi
72
+ }
73
+
74
+ lock_started_at="$(date +%s)"
75
+ missing_owner_checks=0
43
76
  while ! mkdir "$LOCK_DIR" 2>/dev/null; do
77
+ if lock_owner_is_alive; then
78
+ missing_owner_checks=0
79
+ else
80
+ missing_owner_checks=$((missing_owner_checks + 1))
81
+ if [ "$missing_owner_checks" -ge 2 ]; then
82
+ stale_lock_dir="$LOCK_DIR.stale.$$"
83
+ if mv "$LOCK_DIR" "$stale_lock_dir" 2>/dev/null; then
84
+ echo "[node_modules] Recovering stale dependency preparation lock."
85
+ rm -rf "$stale_lock_dir"
86
+ fi
87
+ missing_owner_checks=0
88
+ continue
89
+ fi
90
+ fi
91
+
92
+ lock_now="$(date +%s)"
93
+ if [ $((lock_now - lock_started_at)) -ge "$LOCK_TIMEOUT_SECONDS" ]; then
94
+ echo "[node_modules] Timed out waiting ${LOCK_TIMEOUT_SECONDS}s for dependency preparation lock: $LOCK_DIR" >&2
95
+ exit 1
96
+ fi
44
97
  echo "[node_modules] Waiting for another dependency preparation for this project..."
45
98
  sleep 1
46
99
  done
47
- trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
100
+ printf '%s\n' "$$" > "$LOCK_DIR/pid"
101
+ lock_acquired=true
102
+ trap release_lock EXIT
48
103
 
49
104
  mkdir -p "$WORK_DIR"
105
+ INSTALL_STATE_FILE="$WORK_DIR/.install-state"
106
+
107
+ # Previous versions mirrored the whole project into WORK_DIR. That gives pnpm
108
+ # and framework dev servers two paths for the same source tree, which breaks
109
+ # file watching and on-demand compilation. Keep only managed local outputs.
110
+ cleanup_legacy_workspace() {
111
+ local workspace_dir="$1" entry name
112
+ if [ -L "$workspace_dir" ] || { [ -e "$workspace_dir" ] && [ ! -d "$workspace_dir" ]; }; then
113
+ rm -rf "$workspace_dir"
114
+ return
115
+ fi
116
+ if [ ! -d "$workspace_dir" ]; then
117
+ return 0
118
+ fi
119
+
120
+ while IFS= read -r -d '' entry; do
121
+ name="$(basename "$entry")"
122
+ [ "$name" = "node_modules" ] || rm -rf "$entry"
123
+ done < <(find "$workspace_dir" -mindepth 1 -maxdepth 1 -print0)
124
+ }
50
125
 
51
- # Mirror the project with symlinks. node_modules stays real in WORK_DIR;
52
- # workspace directories stay real too, so pnpm never writes them to Drive.
53
- mirror_root() {
126
+ cleanup_legacy_shadow_project() {
54
127
  local entry name
55
128
  while IFS= read -r -d '' entry; do
56
129
  name="$(basename "$entry")"
57
130
  case "$name" in
58
- node_modules|pnpm-lock.yaml) continue ;;
59
- client|server)
60
- [ -f "$PROJECT_ROOT/$name/package.json" ] && continue
61
- ;;
131
+ node_modules|client|server|.install-state|.next) continue ;;
62
132
  esac
63
133
  rm -rf "$entry"
64
134
  done < <(find "$WORK_DIR" -mindepth 1 -maxdepth 1 -print0)
65
135
 
66
- while IFS= read -r -d '' entry; do
67
- name="$(basename "$entry")"
68
- case "$name" in
69
- node_modules|pnpm-lock.yaml) continue ;;
70
- client|server)
71
- [ -f "$entry/package.json" ] && continue
72
- ;;
73
- esac
74
- ln -s "$entry" "$WORK_DIR/$name"
75
- done < <(find "$PROJECT_ROOT" -mindepth 1 -maxdepth 1 -print0)
136
+ for workspace in client server; do
137
+ cleanup_legacy_workspace "$WORK_DIR/$workspace"
138
+ done
76
139
  }
77
140
 
78
- mirror_workspace() {
79
- local workspace="$1" source_dir="$PROJECT_ROOT/$1" work_dir="$WORK_DIR/$1"
80
- local entry name
81
- if [ -L "$work_dir" ] || { [ -e "$work_dir" ] && [ ! -d "$work_dir" ]; }; then
82
- rm -rf "$work_dir"
141
+ link_install_entry() {
142
+ local source_path="$1" target_path="$2"
143
+ rm -rf "$target_path"
144
+ if [ -e "$source_path" ]; then
145
+ ln -s "$source_path" "$target_path"
83
146
  fi
84
- mkdir -p "$work_dir"
147
+ }
85
148
 
86
- while IFS= read -r -d '' entry; do
87
- [ "$(basename "$entry")" = "node_modules" ] || rm -rf "$entry"
88
- done < <(find "$work_dir" -mindepth 1 -maxdepth 1 -print0)
149
+ hash_install_input() {
150
+ if command -v sha256sum >/dev/null 2>&1; then
151
+ sha256sum | awk '{print $1}'
152
+ elif command -v shasum >/dev/null 2>&1; then
153
+ shasum -a 256 | awk '{print $1}'
154
+ else
155
+ cksum | awk '{print $1 "-" $2}'
156
+ fi
157
+ }
89
158
 
90
- while IFS= read -r -d '' entry; do
91
- name="$(basename "$entry")"
92
- [ "$name" = "node_modules" ] || ln -s "$entry" "$work_dir/$name"
93
- done < <(find "$source_dir" -mindepth 1 -maxdepth 1 -print0)
159
+ calculate_install_fingerprint() {
160
+ local filename workspace
161
+ {
162
+ printf 'prepare-node-modules-version=2\n'
163
+ printf 'pnpm-version=%s\n' "$(pnpm --version)"
164
+ for filename in package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc; do
165
+ printf 'file=%s\n' "$filename"
166
+ if [ -f "$PROJECT_ROOT/$filename" ]; then
167
+ cat "$PROJECT_ROOT/$filename"
168
+ else
169
+ printf '<missing>\n'
170
+ fi
171
+ done
172
+ for workspace in client server; do
173
+ printf 'file=%s/package.json\n' "$workspace"
174
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
175
+ cat "$PROJECT_ROOT/$workspace/package.json"
176
+ else
177
+ printf '<missing>\n'
178
+ fi
179
+ done
180
+ } | hash_install_input
94
181
  }
95
182
 
96
- mirror_root
97
- for workspace in client server; do
98
- if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
99
- mirror_workspace "$workspace"
183
+ dependencies_are_ready() {
184
+ local workspace
185
+ [ -d "$WORK_DIR/node_modules" ] || return 1
186
+ for workspace in client server; do
187
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ] && [ ! -d "$WORK_DIR/$workspace/node_modules" ]; then
188
+ return 1
189
+ fi
190
+ done
191
+ }
192
+
193
+ write_install_state() {
194
+ local fingerprint="$1" temporary_state
195
+ temporary_state="$(mktemp "$WORK_DIR/.install-state.XXXXXX")"
196
+ printf '%s\n' "$fingerprint" > "$temporary_state"
197
+ mv -f "$temporary_state" "$INSTALL_STATE_FILE"
198
+ }
199
+
200
+ prepare_next_output() {
201
+ local next_root="$PROJECT_ROOT/.next"
202
+ local next_dev="$next_root/dev"
203
+ local target="$WORK_DIR/.next/dev"
204
+ local current=""
205
+ if [ ! -f "$PROJECT_ROOT/next.config.js" ] &&
206
+ [ ! -f "$PROJECT_ROOT/next.config.mjs" ] &&
207
+ [ ! -f "$PROJECT_ROOT/next.config.ts" ]; then
208
+ return 0
100
209
  fi
101
- done
102
210
 
103
- # pnpm refuses to update a symlinked lockfile, so install against a local copy.
104
- rm -rf "$WORK_DIR/pnpm-lock.yaml"
105
- if [ -f "$PROJECT_ROOT/pnpm-lock.yaml" ]; then
106
- cp "$PROJECT_ROOT/pnpm-lock.yaml" "$WORK_DIR/pnpm-lock.yaml"
107
- fi
211
+ # Migrate links created by the previous implementation, which linked the
212
+ # entire .next directory. Keep the root local so production output stays in
213
+ # the project while only development output is redirected to local disk.
214
+ if [ -L "$next_root" ]; then
215
+ current="$(readlink "$next_root")"
216
+ case "$current" in
217
+ "$NM_ROOT"/*/.next)
218
+ rm "$next_root"
219
+ ;;
220
+ *)
221
+ echo "[node_modules] refusing to replace unmanaged .next symlink: $next_root" >&2
222
+ exit 1
223
+ ;;
224
+ esac
225
+ elif [ -e "$next_root" ] && [ ! -d "$next_root" ]; then
226
+ echo "[node_modules] refusing to replace non-directory Next output: $next_root" >&2
227
+ exit 1
228
+ fi
229
+ mkdir -p "$next_root"
108
230
 
109
- echo "[node_modules] Installing dependencies in $WORK_DIR"
110
- if ! (cd "$WORK_DIR" && pnpm install "$@"); then
111
- rm -f "$WORK_DIR/pnpm-lock.yaml"
112
- exit 1
113
- fi
231
+ if [ "${COZE_LOCAL_NEXT_OUTPUT:-}" != "1" ]; then
232
+ # .next/dev is separate from production output in Next 16. Leave its
233
+ # managed link in place so production preparation does not discard a dev
234
+ # cache or modify output used by a running dev server.
235
+ return 0
236
+ fi
114
237
 
115
- if [ -f "$WORK_DIR/pnpm-lock.yaml" ]; then
116
- temporary_lockfile="$(mktemp "$PROJECT_ROOT/.pnpm-lock.yaml.XXXXXX")"
117
- cp "$WORK_DIR/pnpm-lock.yaml" "$temporary_lockfile"
118
- chmod 0644 "$temporary_lockfile"
119
- mv -f "$temporary_lockfile" "$PROJECT_ROOT/pnpm-lock.yaml"
120
- fi
238
+ mkdir -p "$target"
239
+ if [ -L "$next_dev" ]; then
240
+ current="$(readlink "$next_dev")"
241
+ if [ "$current" = "$target" ]; then
242
+ return 0
243
+ fi
244
+ case "$current" in
245
+ "$NM_ROOT"/*/.next/dev) rm "$next_dev" ;;
246
+ *)
247
+ echo "[node_modules] refusing to replace unmanaged .next/dev symlink: $next_dev" >&2
248
+ exit 1
249
+ ;;
250
+ esac
251
+ elif [ -e "$next_dev" ]; then
252
+ rm -rf "$next_dev"
253
+ fi
254
+ # macOS 的 ln -sfn 不会直接替换实体目录,所以上面先清理,再覆盖链接。
255
+ ln -sfn "$target" "$next_dev"
256
+ }
257
+
258
+ copy_install_metadata() {
259
+ local filename source_file target_file workspace
260
+ for filename in pnpm-lock.yaml pnpm-workspace.yaml .npmrc; do
261
+ source_file="$PROJECT_ROOT/$filename"
262
+ target_file="$WORK_DIR/$filename"
263
+ rm -f "$target_file"
264
+ if [ -f "$source_file" ]; then
265
+ cp "$source_file" "$target_file"
266
+ fi
267
+ done
268
+
269
+ link_install_entry "$PROJECT_ROOT/package.json" "$WORK_DIR/package.json"
270
+ link_install_entry "$PROJECT_ROOT/patches" "$WORK_DIR/patches"
271
+ link_install_entry "$PROJECT_ROOT/scripts" "$WORK_DIR/scripts"
272
+
273
+ for workspace in client server; do
274
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
275
+ mkdir -p "$WORK_DIR/$workspace"
276
+ link_install_entry "$PROJECT_ROOT/$workspace/package.json" "$WORK_DIR/$workspace/package.json"
277
+ link_install_entry "$PROJECT_ROOT/$workspace/scripts" "$WORK_DIR/$workspace/scripts"
278
+ else
279
+ rm -rf "$WORK_DIR/$workspace"
280
+ fi
281
+ done
282
+ }
121
283
 
122
284
  link_node_modules() {
123
285
  local source_dir="$1" target_dir="$2"
@@ -139,11 +301,39 @@ link_node_modules() {
139
301
  ln -s "$target_dir" "$source_dir/node_modules"
140
302
  }
141
303
 
304
+ cleanup_legacy_shadow_project
305
+ install_fingerprint="$(calculate_install_fingerprint)"
306
+ copy_install_metadata
307
+
308
+ installed_fingerprint=""
309
+ if [ -f "$INSTALL_STATE_FILE" ]; then
310
+ IFS= read -r installed_fingerprint < "$INSTALL_STATE_FILE" || true
311
+ fi
312
+
313
+ if [ "$installed_fingerprint" = "$install_fingerprint" ] && dependencies_are_ready; then
314
+ echo "[node_modules] Reusing unchanged dependencies in $WORK_DIR"
315
+ else
316
+ rm -f "$INSTALL_STATE_FILE"
317
+ echo "[node_modules] Installing dependencies in $WORK_DIR"
318
+ if ! (cd "$WORK_DIR" && pnpm install "$@"); then
319
+ exit 1
320
+ fi
321
+
322
+ if [ -f "$WORK_DIR/pnpm-lock.yaml" ]; then
323
+ temporary_lockfile="$(mktemp "$PROJECT_ROOT/.pnpm-lock.yaml.XXXXXX")"
324
+ cp "$WORK_DIR/pnpm-lock.yaml" "$temporary_lockfile"
325
+ chmod 0644 "$temporary_lockfile"
326
+ mv -f "$temporary_lockfile" "$PROJECT_ROOT/pnpm-lock.yaml"
327
+ fi
328
+ write_install_state "$(calculate_install_fingerprint)"
329
+ fi
330
+
142
331
  link_node_modules "$PROJECT_ROOT" "$WORK_DIR/node_modules"
143
332
  for workspace in client server; do
144
333
  if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
145
334
  link_node_modules "$PROJECT_ROOT/$workspace" "$WORK_DIR/$workspace/node_modules"
146
335
  fi
147
336
  done
337
+ prepare_next_output
148
338
 
149
339
  echo "[node_modules] Dependencies ready"
@@ -40,84 +40,246 @@ case "$WORK_DIR" in
40
40
  esac
41
41
 
42
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
+ lock_acquired=false
62
+ release_lock() {
63
+ local owner_pid=""
64
+ if [ "$lock_acquired" != true ] || [ ! -f "$LOCK_DIR/pid" ]; then
65
+ return 0
66
+ fi
67
+ IFS= read -r owner_pid < "$LOCK_DIR/pid" || true
68
+ if [ "$owner_pid" = "$$" ]; then
69
+ rm -f "$LOCK_DIR/pid"
70
+ rmdir "$LOCK_DIR" 2>/dev/null || true
71
+ fi
72
+ }
73
+
74
+ lock_started_at="$(date +%s)"
75
+ missing_owner_checks=0
43
76
  while ! mkdir "$LOCK_DIR" 2>/dev/null; do
77
+ if lock_owner_is_alive; then
78
+ missing_owner_checks=0
79
+ else
80
+ missing_owner_checks=$((missing_owner_checks + 1))
81
+ if [ "$missing_owner_checks" -ge 2 ]; then
82
+ stale_lock_dir="$LOCK_DIR.stale.$$"
83
+ if mv "$LOCK_DIR" "$stale_lock_dir" 2>/dev/null; then
84
+ echo "[node_modules] Recovering stale dependency preparation lock."
85
+ rm -rf "$stale_lock_dir"
86
+ fi
87
+ missing_owner_checks=0
88
+ continue
89
+ fi
90
+ fi
91
+
92
+ lock_now="$(date +%s)"
93
+ if [ $((lock_now - lock_started_at)) -ge "$LOCK_TIMEOUT_SECONDS" ]; then
94
+ echo "[node_modules] Timed out waiting ${LOCK_TIMEOUT_SECONDS}s for dependency preparation lock: $LOCK_DIR" >&2
95
+ exit 1
96
+ fi
44
97
  echo "[node_modules] Waiting for another dependency preparation for this project..."
45
98
  sleep 1
46
99
  done
47
- trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
100
+ printf '%s\n' "$$" > "$LOCK_DIR/pid"
101
+ lock_acquired=true
102
+ trap release_lock EXIT
48
103
 
49
104
  mkdir -p "$WORK_DIR"
105
+ INSTALL_STATE_FILE="$WORK_DIR/.install-state"
106
+
107
+ # Previous versions mirrored the whole project into WORK_DIR. That gives pnpm
108
+ # and framework dev servers two paths for the same source tree, which breaks
109
+ # file watching and on-demand compilation. Keep only managed local outputs.
110
+ cleanup_legacy_workspace() {
111
+ local workspace_dir="$1" entry name
112
+ if [ -L "$workspace_dir" ] || { [ -e "$workspace_dir" ] && [ ! -d "$workspace_dir" ]; }; then
113
+ rm -rf "$workspace_dir"
114
+ return
115
+ fi
116
+ if [ ! -d "$workspace_dir" ]; then
117
+ return 0
118
+ fi
119
+
120
+ while IFS= read -r -d '' entry; do
121
+ name="$(basename "$entry")"
122
+ [ "$name" = "node_modules" ] || rm -rf "$entry"
123
+ done < <(find "$workspace_dir" -mindepth 1 -maxdepth 1 -print0)
124
+ }
50
125
 
51
- # Mirror the project with symlinks. node_modules stays real in WORK_DIR;
52
- # workspace directories stay real too, so pnpm never writes them to Drive.
53
- mirror_root() {
126
+ cleanup_legacy_shadow_project() {
54
127
  local entry name
55
128
  while IFS= read -r -d '' entry; do
56
129
  name="$(basename "$entry")"
57
130
  case "$name" in
58
- node_modules|pnpm-lock.yaml) continue ;;
59
- client|server)
60
- [ -f "$PROJECT_ROOT/$name/package.json" ] && continue
61
- ;;
131
+ node_modules|client|server|.install-state|.next) continue ;;
62
132
  esac
63
133
  rm -rf "$entry"
64
134
  done < <(find "$WORK_DIR" -mindepth 1 -maxdepth 1 -print0)
65
135
 
66
- while IFS= read -r -d '' entry; do
67
- name="$(basename "$entry")"
68
- case "$name" in
69
- node_modules|pnpm-lock.yaml) continue ;;
70
- client|server)
71
- [ -f "$entry/package.json" ] && continue
72
- ;;
73
- esac
74
- ln -s "$entry" "$WORK_DIR/$name"
75
- done < <(find "$PROJECT_ROOT" -mindepth 1 -maxdepth 1 -print0)
136
+ for workspace in client server; do
137
+ cleanup_legacy_workspace "$WORK_DIR/$workspace"
138
+ done
76
139
  }
77
140
 
78
- mirror_workspace() {
79
- local workspace="$1" source_dir="$PROJECT_ROOT/$1" work_dir="$WORK_DIR/$1"
80
- local entry name
81
- if [ -L "$work_dir" ] || { [ -e "$work_dir" ] && [ ! -d "$work_dir" ]; }; then
82
- rm -rf "$work_dir"
141
+ link_install_entry() {
142
+ local source_path="$1" target_path="$2"
143
+ rm -rf "$target_path"
144
+ if [ -e "$source_path" ]; then
145
+ ln -s "$source_path" "$target_path"
83
146
  fi
84
- mkdir -p "$work_dir"
147
+ }
85
148
 
86
- while IFS= read -r -d '' entry; do
87
- [ "$(basename "$entry")" = "node_modules" ] || rm -rf "$entry"
88
- done < <(find "$work_dir" -mindepth 1 -maxdepth 1 -print0)
149
+ hash_install_input() {
150
+ if command -v sha256sum >/dev/null 2>&1; then
151
+ sha256sum | awk '{print $1}'
152
+ elif command -v shasum >/dev/null 2>&1; then
153
+ shasum -a 256 | awk '{print $1}'
154
+ else
155
+ cksum | awk '{print $1 "-" $2}'
156
+ fi
157
+ }
89
158
 
90
- while IFS= read -r -d '' entry; do
91
- name="$(basename "$entry")"
92
- [ "$name" = "node_modules" ] || ln -s "$entry" "$work_dir/$name"
93
- done < <(find "$source_dir" -mindepth 1 -maxdepth 1 -print0)
159
+ calculate_install_fingerprint() {
160
+ local filename workspace
161
+ {
162
+ printf 'prepare-node-modules-version=2\n'
163
+ printf 'pnpm-version=%s\n' "$(pnpm --version)"
164
+ for filename in package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc; do
165
+ printf 'file=%s\n' "$filename"
166
+ if [ -f "$PROJECT_ROOT/$filename" ]; then
167
+ cat "$PROJECT_ROOT/$filename"
168
+ else
169
+ printf '<missing>\n'
170
+ fi
171
+ done
172
+ for workspace in client server; do
173
+ printf 'file=%s/package.json\n' "$workspace"
174
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
175
+ cat "$PROJECT_ROOT/$workspace/package.json"
176
+ else
177
+ printf '<missing>\n'
178
+ fi
179
+ done
180
+ } | hash_install_input
94
181
  }
95
182
 
96
- mirror_root
97
- for workspace in client server; do
98
- if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
99
- mirror_workspace "$workspace"
183
+ dependencies_are_ready() {
184
+ local workspace
185
+ [ -d "$WORK_DIR/node_modules" ] || return 1
186
+ for workspace in client server; do
187
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ] && [ ! -d "$WORK_DIR/$workspace/node_modules" ]; then
188
+ return 1
189
+ fi
190
+ done
191
+ }
192
+
193
+ write_install_state() {
194
+ local fingerprint="$1" temporary_state
195
+ temporary_state="$(mktemp "$WORK_DIR/.install-state.XXXXXX")"
196
+ printf '%s\n' "$fingerprint" > "$temporary_state"
197
+ mv -f "$temporary_state" "$INSTALL_STATE_FILE"
198
+ }
199
+
200
+ prepare_next_output() {
201
+ local next_root="$PROJECT_ROOT/.next"
202
+ local next_dev="$next_root/dev"
203
+ local target="$WORK_DIR/.next/dev"
204
+ local current=""
205
+ if [ ! -f "$PROJECT_ROOT/next.config.js" ] &&
206
+ [ ! -f "$PROJECT_ROOT/next.config.mjs" ] &&
207
+ [ ! -f "$PROJECT_ROOT/next.config.ts" ]; then
208
+ return 0
100
209
  fi
101
- done
102
210
 
103
- # pnpm refuses to update a symlinked lockfile, so install against a local copy.
104
- rm -rf "$WORK_DIR/pnpm-lock.yaml"
105
- if [ -f "$PROJECT_ROOT/pnpm-lock.yaml" ]; then
106
- cp "$PROJECT_ROOT/pnpm-lock.yaml" "$WORK_DIR/pnpm-lock.yaml"
107
- fi
211
+ # Migrate links created by the previous implementation, which linked the
212
+ # entire .next directory. Keep the root local so production output stays in
213
+ # the project while only development output is redirected to local disk.
214
+ if [ -L "$next_root" ]; then
215
+ current="$(readlink "$next_root")"
216
+ case "$current" in
217
+ "$NM_ROOT"/*/.next)
218
+ rm "$next_root"
219
+ ;;
220
+ *)
221
+ echo "[node_modules] refusing to replace unmanaged .next symlink: $next_root" >&2
222
+ exit 1
223
+ ;;
224
+ esac
225
+ elif [ -e "$next_root" ] && [ ! -d "$next_root" ]; then
226
+ echo "[node_modules] refusing to replace non-directory Next output: $next_root" >&2
227
+ exit 1
228
+ fi
229
+ mkdir -p "$next_root"
108
230
 
109
- echo "[node_modules] Installing dependencies in $WORK_DIR"
110
- if ! (cd "$WORK_DIR" && pnpm install "$@"); then
111
- rm -f "$WORK_DIR/pnpm-lock.yaml"
112
- exit 1
113
- fi
231
+ if [ "${COZE_LOCAL_NEXT_OUTPUT:-}" != "1" ]; then
232
+ # .next/dev is separate from production output in Next 16. Leave its
233
+ # managed link in place so production preparation does not discard a dev
234
+ # cache or modify output used by a running dev server.
235
+ return 0
236
+ fi
114
237
 
115
- if [ -f "$WORK_DIR/pnpm-lock.yaml" ]; then
116
- temporary_lockfile="$(mktemp "$PROJECT_ROOT/.pnpm-lock.yaml.XXXXXX")"
117
- cp "$WORK_DIR/pnpm-lock.yaml" "$temporary_lockfile"
118
- chmod 0644 "$temporary_lockfile"
119
- mv -f "$temporary_lockfile" "$PROJECT_ROOT/pnpm-lock.yaml"
120
- fi
238
+ mkdir -p "$target"
239
+ if [ -L "$next_dev" ]; then
240
+ current="$(readlink "$next_dev")"
241
+ if [ "$current" = "$target" ]; then
242
+ return 0
243
+ fi
244
+ case "$current" in
245
+ "$NM_ROOT"/*/.next/dev) rm "$next_dev" ;;
246
+ *)
247
+ echo "[node_modules] refusing to replace unmanaged .next/dev symlink: $next_dev" >&2
248
+ exit 1
249
+ ;;
250
+ esac
251
+ elif [ -e "$next_dev" ]; then
252
+ rm -rf "$next_dev"
253
+ fi
254
+ # macOS 的 ln -sfn 不会直接替换实体目录,所以上面先清理,再覆盖链接。
255
+ ln -sfn "$target" "$next_dev"
256
+ }
257
+
258
+ copy_install_metadata() {
259
+ local filename source_file target_file workspace
260
+ for filename in pnpm-lock.yaml pnpm-workspace.yaml .npmrc; do
261
+ source_file="$PROJECT_ROOT/$filename"
262
+ target_file="$WORK_DIR/$filename"
263
+ rm -f "$target_file"
264
+ if [ -f "$source_file" ]; then
265
+ cp "$source_file" "$target_file"
266
+ fi
267
+ done
268
+
269
+ link_install_entry "$PROJECT_ROOT/package.json" "$WORK_DIR/package.json"
270
+ link_install_entry "$PROJECT_ROOT/patches" "$WORK_DIR/patches"
271
+ link_install_entry "$PROJECT_ROOT/scripts" "$WORK_DIR/scripts"
272
+
273
+ for workspace in client server; do
274
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
275
+ mkdir -p "$WORK_DIR/$workspace"
276
+ link_install_entry "$PROJECT_ROOT/$workspace/package.json" "$WORK_DIR/$workspace/package.json"
277
+ link_install_entry "$PROJECT_ROOT/$workspace/scripts" "$WORK_DIR/$workspace/scripts"
278
+ else
279
+ rm -rf "$WORK_DIR/$workspace"
280
+ fi
281
+ done
282
+ }
121
283
 
122
284
  link_node_modules() {
123
285
  local source_dir="$1" target_dir="$2"
@@ -139,11 +301,39 @@ link_node_modules() {
139
301
  ln -s "$target_dir" "$source_dir/node_modules"
140
302
  }
141
303
 
304
+ cleanup_legacy_shadow_project
305
+ install_fingerprint="$(calculate_install_fingerprint)"
306
+ copy_install_metadata
307
+
308
+ installed_fingerprint=""
309
+ if [ -f "$INSTALL_STATE_FILE" ]; then
310
+ IFS= read -r installed_fingerprint < "$INSTALL_STATE_FILE" || true
311
+ fi
312
+
313
+ if [ "$installed_fingerprint" = "$install_fingerprint" ] && dependencies_are_ready; then
314
+ echo "[node_modules] Reusing unchanged dependencies in $WORK_DIR"
315
+ else
316
+ rm -f "$INSTALL_STATE_FILE"
317
+ echo "[node_modules] Installing dependencies in $WORK_DIR"
318
+ if ! (cd "$WORK_DIR" && pnpm install "$@"); then
319
+ exit 1
320
+ fi
321
+
322
+ if [ -f "$WORK_DIR/pnpm-lock.yaml" ]; then
323
+ temporary_lockfile="$(mktemp "$PROJECT_ROOT/.pnpm-lock.yaml.XXXXXX")"
324
+ cp "$WORK_DIR/pnpm-lock.yaml" "$temporary_lockfile"
325
+ chmod 0644 "$temporary_lockfile"
326
+ mv -f "$temporary_lockfile" "$PROJECT_ROOT/pnpm-lock.yaml"
327
+ fi
328
+ write_install_state "$(calculate_install_fingerprint)"
329
+ fi
330
+
142
331
  link_node_modules "$PROJECT_ROOT" "$WORK_DIR/node_modules"
143
332
  for workspace in client server; do
144
333
  if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
145
334
  link_node_modules "$PROJECT_ROOT/$workspace" "$WORK_DIR/$workspace/node_modules"
146
335
  fi
147
336
  done
337
+ prepare_next_output
148
338
 
149
339
  echo "[node_modules] Dependencies ready"