@lark-apaas/fullstack-cli 1.1.60 → 1.1.61-alpha.20260818172555

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.
@@ -3,12 +3,15 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { spawn, execSync } = require('child_process');
6
+ const { spawn, execSync, execFileSync } = require('child_process');
7
7
  const readline = require('readline');
8
8
 
9
9
  // ── Project root ──────────────────────────────────────────────────────────────
10
10
  const PROJECT_ROOT = path.resolve(__dirname, '..');
11
11
  process.chdir(PROJECT_ROOT);
12
+ // Runtime scripts may be image-owned and therefore live outside the workspace.
13
+ // Keep the project identity explicit for validation and all supervised children.
14
+ process.env.MIAODA_WORKSPACE_ROOT ||= PROJECT_ROOT;
12
15
 
13
16
  // ── Load .env ─────────────────────────────────────────────────────────────────
14
17
  function loadEnv() {
@@ -31,13 +34,73 @@ loadEnv();
31
34
 
32
35
  // ── Configuration ─────────────────────────────────────────────────────────────
33
36
  const LOG_DIR = process.env.LOG_DIR || 'logs';
34
- const MAX_RESTART_COUNT = process.env.MAX_RESTART_COUNT != null && process.env.MAX_RESTART_COUNT !== ''
35
- ? parseInt(process.env.MAX_RESTART_COUNT, 10)
36
- : Infinity;
37
+ const MAX_RESTART_COUNT =
38
+ process.env.MAX_RESTART_COUNT != null && process.env.MAX_RESTART_COUNT !== ''
39
+ ? parseInt(process.env.MAX_RESTART_COUNT, 10)
40
+ : Infinity;
37
41
  const RESTART_DELAY = parseInt(process.env.RESTART_DELAY, 10) || 2;
38
42
  const MAX_DELAY = 8;
39
43
  const SERVER_PORT = process.env.SERVER_PORT || '3000';
40
44
  const CLIENT_DEV_PORT = process.env.CLIENT_DEV_PORT || '8080';
45
+ const CACHE_HANDOFF_EXIT_CODE = 75;
46
+ const NEST_ORCHESTRATOR_OWNER_FILE = path.resolve(
47
+ process.env.MIAODA_NEST_ORCHESTRATOR_OWNER_FILE ||
48
+ '/tmp/event/MIAODA_NEST_ORCHESTRATOR_OWNER.json'
49
+ );
50
+ let CACHE_BOOTSTRAP_ENABLED =
51
+ process.env.MIAODA_CACHE_BOOTSTRAP_ENABLED === 'true';
52
+ let STARTUP_EXPERIMENT_ENABLED =
53
+ process.env.MIAODA_STARTUP_EXPERIMENT_ENABLED === 'true';
54
+ const CACHE_RUNTIME_SCRIPTS_ROOT = path.resolve(
55
+ process.env.MIAODA_CACHE_RUNTIME_SCRIPTS_ROOT ||
56
+ path.join(PROJECT_ROOT, 'scripts')
57
+ );
58
+
59
+ function readExternalNestOrchestratorOwner() {
60
+ try {
61
+ const owner = JSON.parse(
62
+ fs.readFileSync(NEST_ORCHESTRATOR_OWNER_FILE, 'utf8')
63
+ );
64
+ const pid = Number(owner.pid);
65
+ if (
66
+ owner.schemaVersion !== 1 ||
67
+ owner.mode !== (process.env.MIAODA_NEST_START_MODE || 'workspace') ||
68
+ !Number.isSafeInteger(pid) ||
69
+ pid <= 1 ||
70
+ pid === process.pid
71
+ ) {
72
+ return undefined;
73
+ }
74
+ process.kill(pid, 0);
75
+ return { pid, mode: owner.mode };
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }
80
+
81
+ function dependencyHandoffAlreadyCompleted() {
82
+ if (!fs.existsSync(path.join(PROJECT_ROOT, 'node_modules'))) return false;
83
+ try {
84
+ const cacheRoot = path.resolve(
85
+ process.env.MIAODA_CACHE_ROOT ||
86
+ path.join(PROJECT_ROOT, '.miaoda-cache', 'current')
87
+ );
88
+ const readyFile = path.resolve(
89
+ process.env.MIAODA_DEPENDENCY_READY_FILE ||
90
+ path.join(PROJECT_ROOT, '.miaoda-runtime', 'dependencies-ready.json')
91
+ );
92
+ const generation = JSON.parse(
93
+ fs.readFileSync(path.join(cacheRoot, 'generation.json'), 'utf8')
94
+ );
95
+ const ready = JSON.parse(fs.readFileSync(readyFile, 'utf8'));
96
+ return (
97
+ /^[a-f0-9]{64}$/.test(generation.generationId || '') &&
98
+ ready.generationId === generation.generationId
99
+ );
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
41
104
 
42
105
  fs.mkdirSync(LOG_DIR, { recursive: true });
43
106
 
@@ -50,11 +113,16 @@ const devLogFd = fs.openSync(devLogPath, 'a');
50
113
  function timestamp() {
51
114
  const now = new Date();
52
115
  return (
53
- now.getFullYear() + '-' +
54
- String(now.getMonth() + 1).padStart(2, '0') + '-' +
55
- String(now.getDate()).padStart(2, '0') + ' ' +
56
- String(now.getHours()).padStart(2, '0') + ':' +
57
- String(now.getMinutes()).padStart(2, '0') + ':' +
116
+ now.getFullYear() +
117
+ '-' +
118
+ String(now.getMonth() + 1).padStart(2, '0') +
119
+ '-' +
120
+ String(now.getDate()).padStart(2, '0') +
121
+ ' ' +
122
+ String(now.getHours()).padStart(2, '0') +
123
+ ':' +
124
+ String(now.getMinutes()).padStart(2, '0') +
125
+ ':' +
58
126
  String(now.getSeconds()).padStart(2, '0')
59
127
  );
60
128
  }
@@ -68,21 +136,33 @@ const STDOUT_MAX_INFLIGHT = 1000;
68
136
  function writeOutput(msg) {
69
137
  // File first and synchronously — read-logs reads this file; it must never be gated
70
138
  // by the terminal consumer.
71
- try { fs.writeSync(devStdLogFd, msg); } catch {}
139
+ try {
140
+ fs.writeSync(devStdLogFd, msg);
141
+ } catch {}
72
142
  // stdout mirror via async fs.write: if process.stdout is a pty/pipe whose consumer
73
143
  // stalls, the block happens on the libuv threadpool, NOT the event loop — so the
74
144
  // synchronous log-FILE writes above (and subsequent readline callbacks) keep running.
75
145
  // Drop overflow when too many writes are already pending (best-effort mirror).
76
- if (_stdoutInFlight >= STDOUT_MAX_INFLIGHT) { return; }
146
+ if (_stdoutInFlight >= STDOUT_MAX_INFLIGHT) {
147
+ return;
148
+ }
77
149
  _stdoutInFlight++;
78
- try { fs.write(1, msg, () => { _stdoutInFlight--; }); } catch { _stdoutInFlight--; }
150
+ try {
151
+ fs.write(1, msg, () => {
152
+ _stdoutInFlight--;
153
+ });
154
+ } catch {
155
+ _stdoutInFlight--;
156
+ }
79
157
  }
80
158
 
81
159
  /** Structured event log → terminal + dev.std.log + dev.log */
82
160
  function logEvent(level, name, message) {
83
161
  const msg = `[${timestamp()}] [${level}] [${name}] ${message}\n`;
84
162
  writeOutput(msg);
85
- try { fs.writeSync(devLogFd, msg); } catch {}
163
+ try {
164
+ fs.writeSync(devLogFd, msg);
165
+ } catch {}
86
166
  }
87
167
 
88
168
  // ── Process group management ──────────────────────────────────────────────────
@@ -94,11 +174,16 @@ function killProcessGroup(pid, signal) {
94
174
 
95
175
  function killOrphansByPort(port) {
96
176
  try {
97
- const pids = execSync(`lsof -ti :${port}`, { encoding: 'utf8', timeout: 5000 }).trim();
177
+ const pids = execSync(`lsof -ti :${port}`, {
178
+ encoding: 'utf8',
179
+ timeout: 5000,
180
+ }).trim();
98
181
  if (pids) {
99
182
  const pidList = pids.split('\n').filter(Boolean);
100
183
  for (const p of pidList) {
101
- try { process.kill(parseInt(p, 10), 'SIGKILL'); } catch {}
184
+ try {
185
+ process.kill(parseInt(p, 10), 'SIGKILL');
186
+ } catch {}
102
187
  }
103
188
  return pidList;
104
189
  }
@@ -111,16 +196,23 @@ let stopping = false;
111
196
  const managedProcesses = []; // { name, pid, child }
112
197
 
113
198
  function sleep(ms) {
114
- return new Promise((resolve) => setTimeout(resolve, ms));
199
+ return new Promise(resolve => setTimeout(resolve, ms));
115
200
  }
116
201
 
117
202
  /**
118
203
  * Start and supervise a process with auto-restart and log piping.
119
204
  * Returns a promise that resolves when the process loop ends.
120
205
  */
121
- function startProcess({ name, command, args, cleanupPort }) {
206
+ function startProcess({
207
+ name,
208
+ command,
209
+ args,
210
+ cleanupPort,
211
+ stopRestartOnExitCodes = [],
212
+ }) {
122
213
  const logFilePath = path.join(LOG_DIR, `${name}.std.log`);
123
214
  const logFd = fs.openSync(logFilePath, 'a');
215
+ const logicalLogFds = new Map();
124
216
 
125
217
  const entry = { name, pid: null, child: null };
126
218
  managedProcesses.push(entry);
@@ -141,14 +233,39 @@ function startProcess({ name, command, args, cleanupPort }) {
141
233
  entry.child = child;
142
234
 
143
235
  const startTime = Date.now();
144
- logEvent('INFO', name, `Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`);
236
+ logEvent(
237
+ 'INFO',
238
+ name,
239
+ `Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`
240
+ );
145
241
 
146
242
  // Pipe stdout and stderr through readline for timestamped logging
147
- const pipeLines = (stream) => {
148
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
149
- rl.on('line', (line) => {
150
- const msg = `[${timestamp()}] [${name}] ${line}\n`;
151
- try { fs.writeSync(logFd, msg); } catch {}
243
+ const pipeLines = stream => {
244
+ const rl = readline.createInterface({
245
+ input: stream,
246
+ crlfDelay: Infinity,
247
+ });
248
+ rl.on('line', line => {
249
+ const tagged = /^MIAODA_LOG\t(client|server)\t(.*)$/.exec(line);
250
+ const logicalName = tagged?.[1] || name;
251
+ const logicalLine = tagged?.[2] ?? line;
252
+ const msg = `[${timestamp()}] [${logicalName}] ${logicalLine}\n`;
253
+ try {
254
+ fs.writeSync(logFd, msg);
255
+ } catch {}
256
+ if (tagged) {
257
+ try {
258
+ let logicalFd = logicalLogFds.get(logicalName);
259
+ if (logicalFd === undefined) {
260
+ logicalFd = fs.openSync(
261
+ path.join(LOG_DIR, `${logicalName}.std.log`),
262
+ 'a'
263
+ );
264
+ logicalLogFds.set(logicalName, logicalFd);
265
+ }
266
+ fs.writeSync(logicalFd, msg);
267
+ } catch {}
268
+ }
152
269
  writeOutput(msg);
153
270
  });
154
271
  };
@@ -159,15 +276,16 @@ function startProcess({ name, command, args, cleanupPort }) {
159
276
  // NOTE: must use 'exit', not 'close'. With shell:true, grandchild processes
160
277
  // (e.g. nest's server) inherit stdout pipes. 'close' won't fire until ALL
161
278
  // pipe holders exit, causing dev.js to hang when npm/nest dies but server survives.
162
- const exitCode = await new Promise((resolve) => {
163
- child.on('exit', (code) => resolve(code ?? 1));
279
+ const exitCode = await new Promise(resolve => {
280
+ child.on('exit', code => resolve(code ?? 1));
164
281
  child.on('error', () => resolve(1));
165
282
  });
283
+ const completedHandoff = stopRestartOnExitCodes.includes(exitCode);
166
284
 
167
285
  // Kill the entire process group
168
286
  if (entry.pid) {
169
287
  killProcessGroup(entry.pid, 'SIGTERM');
170
- await sleep(2000);
288
+ await sleep(completedHandoff ? 100 : 2000);
171
289
  killProcessGroup(entry.pid, 'SIGKILL');
172
290
  }
173
291
  entry.pid = null;
@@ -177,31 +295,74 @@ function startProcess({ name, command, args, cleanupPort }) {
177
295
  if (cleanupPort) {
178
296
  const orphans = killOrphansByPort(cleanupPort);
179
297
  if (orphans.length > 0) {
180
- logEvent('WARN', name, `Killed orphan processes on port ${cleanupPort}: ${orphans.join(' ')}`);
298
+ logEvent(
299
+ 'WARN',
300
+ name,
301
+ `Killed orphan processes on port ${cleanupPort}: ${orphans.join(' ')}`
302
+ );
181
303
  await sleep(500);
182
304
  }
183
305
  }
184
306
 
185
307
  if (stopping) break;
186
308
 
309
+ if (completedHandoff) {
310
+ logEvent(
311
+ 'INFO',
312
+ name,
313
+ `Process completed one-way handoff with code ${exitCode}`
314
+ );
315
+ try {
316
+ fs.closeSync(logFd);
317
+ } catch {}
318
+ for (const logicalFd of logicalLogFds.values()) {
319
+ try {
320
+ fs.closeSync(logicalFd);
321
+ } catch {}
322
+ }
323
+ return exitCode;
324
+ }
325
+
187
326
  const runDuration = (Date.now() - startTime) / 1000;
188
327
  if (runDuration >= 60) {
189
328
  restartCount = 0;
190
- logEvent('INFO', name, `Ran for ${Math.round(runDuration)}s, resetting restart counter`);
329
+ logEvent(
330
+ 'INFO',
331
+ name,
332
+ `Ran for ${Math.round(runDuration)}s, resetting restart counter`
333
+ );
191
334
  } else {
192
335
  restartCount++;
193
336
  }
194
337
  if (restartCount >= MAX_RESTART_COUNT) {
195
- logEvent('ERROR', name, `Max restart count (${MAX_RESTART_COUNT}) reached, giving up`);
338
+ logEvent(
339
+ 'ERROR',
340
+ name,
341
+ `Max restart count (${MAX_RESTART_COUNT}) reached, giving up`
342
+ );
196
343
  break;
197
344
  }
198
345
 
199
- const delay = Math.min(RESTART_DELAY * (1 << Math.max(0, restartCount - 1)), MAX_DELAY);
200
- logEvent('WARN', name, `Process exited with code ${exitCode}, restarting (${restartCount}/${MAX_RESTART_COUNT}) in ${delay}s...`);
346
+ const delay = Math.min(
347
+ RESTART_DELAY * (1 << Math.max(0, restartCount - 1)),
348
+ MAX_DELAY
349
+ );
350
+ logEvent(
351
+ 'WARN',
352
+ name,
353
+ `Process exited with code ${exitCode}, restarting (${restartCount}/${MAX_RESTART_COUNT}) in ${delay}s...`
354
+ );
201
355
  await sleep(delay * 1000);
202
356
  }
203
357
 
204
- try { fs.closeSync(logFd); } catch {}
358
+ try {
359
+ fs.closeSync(logFd);
360
+ } catch {}
361
+ for (const logicalFd of logicalLogFds.values()) {
362
+ try {
363
+ fs.closeSync(logicalFd);
364
+ } catch {}
365
+ }
205
366
  };
206
367
 
207
368
  return run();
@@ -231,7 +392,11 @@ async function cleanup() {
231
392
  // Force kill any remaining
232
393
  for (const entry of managedProcesses) {
233
394
  if (entry.pid) {
234
- logEvent('WARN', 'main', `Force killing process group (PGID: ${entry.pid})`);
395
+ logEvent(
396
+ 'WARN',
397
+ 'main',
398
+ `Force killing process group (PGID: ${entry.pid})`
399
+ );
235
400
  killProcessGroup(entry.pid, 'SIGKILL');
236
401
  }
237
402
  }
@@ -242,8 +407,12 @@ async function cleanup() {
242
407
 
243
408
  logEvent('INFO', 'main', 'All processes stopped');
244
409
 
245
- try { fs.closeSync(devStdLogFd); } catch {}
246
- try { fs.closeSync(devLogFd); } catch {}
410
+ try {
411
+ fs.closeSync(devStdLogFd);
412
+ } catch {}
413
+ try {
414
+ fs.closeSync(devLogFd);
415
+ } catch {}
247
416
 
248
417
  process.exit(0);
249
418
  }
@@ -261,48 +430,253 @@ function cleanStaleDist() {
261
430
  }
262
431
  }
263
432
 
433
+ async function followExternalNestOrchestratorOrStartLocal(
434
+ serverStartupRuntimeScript
435
+ ) {
436
+ const owner = readExternalNestOrchestratorOwner();
437
+ if (!owner) {
438
+ return startProcess({
439
+ name: 'server',
440
+ command: process.execPath,
441
+ args: [serverStartupRuntimeScript],
442
+ cleanupPort: SERVER_PORT,
443
+ });
444
+ }
445
+
446
+ logEvent(
447
+ 'INFO',
448
+ 'server',
449
+ `Following image-owned Nest orchestrator pid=${owner.pid} mode=${owner.mode}`
450
+ );
451
+ while (!stopping) {
452
+ try {
453
+ process.kill(owner.pid, 0);
454
+ await sleep(200);
455
+ } catch {
456
+ break;
457
+ }
458
+ }
459
+ if (stopping) return 0;
460
+
461
+ // If the image-owned orchestrator exits, resume the normal supervised
462
+ // workspace service. By this point node_modules is expected to be ready; if
463
+ // it is not, server-startup-runtime keeps the original bounded wait.
464
+ logEvent(
465
+ 'WARN',
466
+ 'server',
467
+ `Image-owned Nest orchestrator pid=${owner.pid} exited; starting local supervisor`
468
+ );
469
+ return startProcess({
470
+ name: 'server',
471
+ command: process.execPath,
472
+ args: [serverStartupRuntimeScript],
473
+ cleanupPort: SERVER_PORT,
474
+ });
475
+ }
476
+
264
477
  // ── Main ──────────────────────────────────────────────────────────────────────
265
478
  async function main() {
266
479
  logEvent('INFO', 'main', '========== Dev session started ==========');
267
480
 
268
- cleanStaleDist();
481
+ if (process.env.MIAODA_HMR_ROUTING_MODE === 'same-origin') {
482
+ delete process.env.MIAODA_WS_HOST;
483
+ logEvent(
484
+ 'INFO',
485
+ 'main',
486
+ 'Using same-origin HMR routing; ignored the dedicated MIAODA_WS_HOST'
487
+ );
488
+ }
269
489
 
270
- // Initialize action plugins
271
- writeOutput('\n🔌 Initializing action plugins...\n');
272
- try {
273
- execSync('fullstack-cli action-plugin init', { cwd: PROJECT_ROOT, stdio: 'inherit' });
274
- writeOutput('✅ Action plugins initialized\n\n');
275
- } catch {
276
- writeOutput('⚠️ Action plugin initialization failed, continuing anyway...\n\n');
490
+ const coordinatorRuntimeScript = path.join(
491
+ CACHE_RUNTIME_SCRIPTS_ROOT,
492
+ 'cache-runtime-coordinator.mjs'
493
+ );
494
+ const serverStartupRuntimeScript = path.join(
495
+ CACHE_RUNTIME_SCRIPTS_ROOT,
496
+ 'server-startup-runtime.mjs'
497
+ );
498
+ const workspaceClientRuntimeScript = path.join(
499
+ CACHE_RUNTIME_SCRIPTS_ROOT,
500
+ 'workspace-client-runtime.mjs'
501
+ );
502
+ if (
503
+ CACHE_BOOTSTRAP_ENABLED &&
504
+ !STARTUP_EXPERIMENT_ENABLED &&
505
+ dependencyHandoffAlreadyCompleted()
506
+ ) {
507
+ CACHE_BOOTSTRAP_ENABLED = false;
508
+ process.env.MIAODA_CACHE_BOOTSTRAP_ENABLED = 'false';
509
+ logEvent(
510
+ 'INFO',
511
+ 'main',
512
+ 'Dependency handoff already completed; starting original services directly'
513
+ );
514
+ }
515
+ if (STARTUP_EXPERIMENT_ENABLED) {
516
+ try {
517
+ for (const requiredPath of [
518
+ serverStartupRuntimeScript,
519
+ workspaceClientRuntimeScript,
520
+ ]) {
521
+ if (!fs.existsSync(requiredPath)) {
522
+ throw new Error(
523
+ `MIAODA_STARTUP_EXPERIMENT_INPUT_MISSING: ${requiredPath}`
524
+ );
525
+ }
526
+ }
527
+ } catch (error) {
528
+ STARTUP_EXPERIMENT_ENABLED = false;
529
+ process.env.MIAODA_STARTUP_EXPERIMENT_ENABLED = 'false';
530
+ logEvent(
531
+ 'WARN',
532
+ 'main',
533
+ `Startup experiment unavailable; using original services: ${error.message}`
534
+ );
535
+ }
536
+ }
537
+ if (CACHE_BOOTSTRAP_ENABLED && !STARTUP_EXPERIMENT_ENABLED) {
538
+ process.env.MIAODA_CACHE_VALIDATION_RECEIPT_FILE ||= path.join(
539
+ PROJECT_ROOT,
540
+ '.miaoda-runtime',
541
+ 'cache-validation-receipt.json'
542
+ );
543
+ try {
544
+ for (const requiredPath of [coordinatorRuntimeScript]) {
545
+ if (!fs.existsSync(requiredPath)) {
546
+ throw new Error(
547
+ `MIAODA_CACHE_BOOTSTRAP_INPUT_MISSING: ${requiredPath}`
548
+ );
549
+ }
550
+ }
551
+ execFileSync(
552
+ process.execPath,
553
+ [coordinatorRuntimeScript, '--validate-only'],
554
+ {
555
+ cwd: PROJECT_ROOT,
556
+ env: { ...process.env },
557
+ stdio: ['ignore', 'pipe', 'pipe'],
558
+ }
559
+ );
560
+ } catch (error) {
561
+ CACHE_BOOTSTRAP_ENABLED = false;
562
+ const reason = error?.stderr?.toString().trim() || error.message;
563
+ logEvent(
564
+ 'WARN',
565
+ 'main',
566
+ `Cache generation preflight rejected; using dependency-backed startup: ${reason}`
567
+ );
568
+ }
277
569
  }
278
570
 
279
- // Start server and client
280
- const serverPromise = startProcess({
281
- name: 'server',
282
- command: 'npm',
283
- args: ['run', 'dev:server'],
284
- cleanupPort: SERVER_PORT,
285
- });
571
+ cleanStaleDist();
286
572
 
287
- const clientPromise = startProcess({
288
- name: 'client',
289
- command: 'npm',
290
- args: ['run', 'dev:client'],
291
- cleanupPort: CLIENT_DEV_PORT,
292
- });
573
+ const initializeActionPlugins = () => {
574
+ writeOutput('\n🔌 Initializing action plugins...\n');
575
+ try {
576
+ execSync('fullstack-cli action-plugin init', {
577
+ cwd: PROJECT_ROOT,
578
+ stdio: 'inherit',
579
+ });
580
+ writeOutput('✅ Action plugins initialized\n\n');
581
+ } catch {
582
+ writeOutput(
583
+ '⚠️ Action plugin initialization failed, continuing anyway...\n\n'
584
+ );
585
+ }
586
+ };
587
+
588
+ // The transition runtime resolves Action Plugin dependencies from the sealed
589
+ // dependency cache. Running the project CLI here would reintroduce a project
590
+ // node_modules dependency before restoration completes.
591
+ if (!CACHE_BOOTSTRAP_ENABLED && !STARTUP_EXPERIMENT_ENABLED) {
592
+ initializeActionPlugins();
593
+ } else if (!STARTUP_EXPERIMENT_ENABLED) {
594
+ logEvent(
595
+ 'INFO',
596
+ 'main',
597
+ 'Skipped dynamic Action Plugin initialization in cache-only mode'
598
+ );
599
+ }
293
600
 
294
601
  writeOutput(`📋 Dev processes running. Press Ctrl+C to stop.\n`);
295
602
  writeOutput(`📄 Logs: ${devStdLogPath}\n\n`);
296
603
 
297
- // Wait for both (they loop until stopping or max restarts)
298
- await Promise.all([serverPromise, clientPromise]);
604
+ if (STARTUP_EXPERIMENT_ENABLED) {
605
+ logEvent(
606
+ 'INFO',
607
+ 'main',
608
+ `Starting normal workspace Vite with Nest mode ${
609
+ process.env.MIAODA_NEST_START_MODE || 'workspace'
610
+ }`
611
+ );
612
+ const serverPromise =
613
+ (process.env.MIAODA_NEST_START_MODE || 'workspace') ===
614
+ 'bundle-handoff'
615
+ ? followExternalNestOrchestratorOrStartLocal(
616
+ serverStartupRuntimeScript
617
+ )
618
+ : startProcess({
619
+ name: 'server',
620
+ command: process.execPath,
621
+ args: [serverStartupRuntimeScript],
622
+ cleanupPort: SERVER_PORT,
623
+ });
624
+ const clientPromise = startProcess({
625
+ name: 'client',
626
+ command: process.execPath,
627
+ args: [workspaceClientRuntimeScript],
628
+ cleanupPort: CLIENT_DEV_PORT,
629
+ });
630
+ await Promise.all([serverPromise, clientPromise]);
631
+ } else if (CACHE_BOOTSTRAP_ENABLED) {
632
+ const exitCode = await startProcess({
633
+ name: 'coordinator',
634
+ command: process.execPath,
635
+ args: [coordinatorRuntimeScript],
636
+ stopRestartOnExitCodes: [CACHE_HANDOFF_EXIT_CODE],
637
+ });
638
+ if (exitCode === CACHE_HANDOFF_EXIT_CODE && !stopping) {
639
+ CACHE_BOOTSTRAP_ENABLED = false;
640
+ process.env.MIAODA_CACHE_BOOTSTRAP_ENABLED = 'false';
641
+ process.env.NODE_PATH = '';
642
+ logEvent(
643
+ 'INFO',
644
+ 'main',
645
+ 'Dependencies restored; cache runtime retired before original services start'
646
+ );
647
+ initializeActionPlugins();
648
+ }
649
+ }
650
+
651
+ if (
652
+ !STARTUP_EXPERIMENT_ENABLED &&
653
+ !CACHE_BOOTSTRAP_ENABLED &&
654
+ !stopping
655
+ ) {
656
+ // Feature-off and post-cache handoff keep the original process boundary
657
+ // and exact npm commands. Failures therefore surface through the original
658
+ // Preview starting/failed/retry UI instead of a cached-page fallback.
659
+ const serverPromise = startProcess({
660
+ name: 'server',
661
+ command: 'npm',
662
+ args: ['run', 'dev:server'],
663
+ cleanupPort: SERVER_PORT,
664
+ });
665
+ const clientPromise = startProcess({
666
+ name: 'client',
667
+ command: 'npm',
668
+ args: ['run', 'dev:client'],
669
+ cleanupPort: CLIENT_DEV_PORT,
670
+ });
671
+ await Promise.all([serverPromise, clientPromise]);
672
+ }
299
673
 
300
674
  if (!cleanupDone) {
301
675
  await cleanup();
302
676
  }
303
677
  }
304
678
 
305
- main().catch((err) => {
679
+ main().catch(err => {
306
680
  console.error('Fatal error:', err);
307
681
  process.exit(1);
308
682
  });
@@ -7,6 +7,8 @@
7
7
  # 显式想跑本地路径可用 `npm run dev:local`(绕过 SANDBOX_ID 判断)。
8
8
  set -euo pipefail
9
9
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ export MIAODA_CACHE_RUNTIME_SCRIPTS_ROOT="${MIAODA_CACHE_RUNTIME_SCRIPTS_ROOT:-$SCRIPT_DIR}"
11
+ export MIAODA_WORKSPACE_ROOT="${MIAODA_WORKSPACE_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
10
12
 
11
13
  if [ -n "${SANDBOX_ID:-}" ]; then
12
14
  exec node "$SCRIPT_DIR/dev.js" "$@"