@lark-apaas/fullstack-cli 1.1.59 → 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.
@@ -0,0 +1,681 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import net from 'node:net';
6
+ import http from 'node:http';
7
+ import { randomUUID } from 'node:crypto';
8
+ import { spawn } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+ import {
11
+ validateCacheGeneration,
12
+ validateCacheGenerationReceipt,
13
+ writeCacheGenerationValidationReceipt,
14
+ } from './cache-generation-preflight.mjs';
15
+
16
+ const projectRoot = path.resolve(
17
+ process.env.MIAODA_WORKSPACE_ROOT || process.cwd()
18
+ );
19
+ const defaultCachePointer = path.join(projectRoot, '.miaoda-cache', 'current');
20
+ const requestedCacheRoot = path.resolve(
21
+ process.env.MIAODA_CACHE_ROOT || defaultCachePointer
22
+ );
23
+ const cacheRoot = fs.realpathSync(requestedCacheRoot);
24
+ if (!process.env.MIAODA_CACHE_ROOT) {
25
+ const generationStore = fs.realpathSync(
26
+ path.join(projectRoot, '.miaoda-cache', 'generations')
27
+ );
28
+ const generationRelative = path.relative(generationStore, cacheRoot);
29
+ if (!/^[a-f0-9]{64}$/.test(generationRelative)) {
30
+ throw new Error('MIAODA_CACHE_GENERATION_POINTER_REJECTED');
31
+ }
32
+ }
33
+ const runtimeManifestFile = path.resolve(
34
+ process.env.MIAODA_PLATFORM_RUNTIME_MANIFEST ||
35
+ '/opt/miaoda/preview-runtime/runtime-manifest.json'
36
+ );
37
+ const validateOnly = process.argv.includes('--validate-only');
38
+ const artifactScopeOptionIndex = process.argv.indexOf('--artifact-scope');
39
+ const validationArtifactScope =
40
+ artifactScopeOptionIndex >= 0
41
+ ? process.argv[artifactScopeOptionIndex + 1]
42
+ : 'all';
43
+ const skipWorkspaceInputs = process.argv.includes('--skip-workspace-inputs');
44
+ const servingReadyMarker = '/tmp/event/MIAODA_NEST_CACHE_SERVING_READY';
45
+ const skipConfigEnvironment = process.argv.includes(
46
+ '--skip-config-environment'
47
+ );
48
+ const runtimeStartedAt = process.hrtime.bigint();
49
+ const durationMs = startedAt =>
50
+ Number(process.hrtime.bigint() - startedAt) / 1_000_000;
51
+ function emitRuntimePhase(phase, startedAt, details = {}) {
52
+ if (validateOnly) return;
53
+ process.stdout.write(
54
+ `${JSON.stringify({
55
+ event: 'miaoda_cache_runtime_phase',
56
+ runtime: 'nest',
57
+ phase,
58
+ durationMs: Number(durationMs(startedAt).toFixed(3)),
59
+ ...details,
60
+ })}\n`
61
+ );
62
+ }
63
+ function publishServingReadyMarker(file) {
64
+ fs.mkdirSync(path.dirname(file), { recursive: true });
65
+ const temporaryFile = `${file}.${process.pid}.tmp`;
66
+ fs.writeFileSync(
67
+ temporaryFile,
68
+ `${JSON.stringify({ schemaVersion: 1, readyAt: new Date().toISOString() })}\n`,
69
+ { mode: 0o600 }
70
+ );
71
+ fs.renameSync(temporaryFile, file);
72
+ }
73
+ if (!validateOnly) {
74
+ fs.rmSync(servingReadyMarker, { force: true });
75
+ process.once('exit', () => {
76
+ try {
77
+ fs.rmSync(servingReadyMarker, { force: true });
78
+ } catch {}
79
+ });
80
+ }
81
+ if (skipConfigEnvironment && !validateOnly) {
82
+ process.stderr.write(
83
+ 'MIAODA_CACHE_CONFIG_ENVIRONMENT_SKIP_REQUIRES_VALIDATE_ONLY\n'
84
+ );
85
+ process.exit(2);
86
+ }
87
+ if (
88
+ (!validateOnly && artifactScopeOptionIndex >= 0) ||
89
+ (!validateOnly && skipWorkspaceInputs) ||
90
+ !['all', 'server', 'vite'].includes(validationArtifactScope) ||
91
+ (skipWorkspaceInputs && validationArtifactScope !== 'vite')
92
+ ) {
93
+ process.stderr.write('MIAODA_CACHE_VALIDATION_OPTIONS_INVALID\n');
94
+ process.exit(2);
95
+ }
96
+
97
+ let validated;
98
+ let validatedBy = 'full-sha';
99
+ let validationReceiptMiss;
100
+ const validationStartedAt = process.hrtime.bigint();
101
+ try {
102
+ const validationReceiptFile =
103
+ process.env.MIAODA_CACHE_VALIDATION_RECEIPT_FILE;
104
+ if (validationReceiptFile && !skipWorkspaceInputs) {
105
+ try {
106
+ validated = validateCacheGenerationReceipt({
107
+ projectRoot,
108
+ cacheRoot,
109
+ runtimeManifestFile,
110
+ receiptFile: validationReceiptFile,
111
+ validateConfigEnvironment: !skipConfigEnvironment,
112
+ artifactScope: validateOnly ? validationArtifactScope : 'server',
113
+ });
114
+ validatedBy = 'platform-receipt';
115
+ } catch (error) {
116
+ validationReceiptMiss = String(
117
+ error instanceof Error ? error.message : error
118
+ ).split(':')[0];
119
+ // A missing/stale receipt is only a fast-path miss. Preserve the legacy
120
+ // full preflight and dependency-backed fallback semantics below.
121
+ }
122
+ }
123
+ if (!validated) {
124
+ validated = validateCacheGeneration({
125
+ projectRoot,
126
+ cacheRoot,
127
+ runtimeManifestFile,
128
+ validateConfigEnvironment: !skipConfigEnvironment,
129
+ artifactScope: validateOnly ? validationArtifactScope : 'server',
130
+ validateWorkspaceInputs: !skipWorkspaceInputs,
131
+ });
132
+ }
133
+ if (
134
+ !process.env.MIAODA_CACHE_ROOT &&
135
+ path.basename(cacheRoot) !== validated.generation.generationId
136
+ ) {
137
+ throw new Error('MIAODA_CACHE_GENERATION_POINTER_ID_MISMATCH');
138
+ }
139
+ if (validateOnly) {
140
+ if (
141
+ validationReceiptFile &&
142
+ validatedBy === 'full-sha' &&
143
+ !skipWorkspaceInputs
144
+ ) {
145
+ writeCacheGenerationValidationReceipt({
146
+ validated,
147
+ receiptFile: validationReceiptFile,
148
+ });
149
+ }
150
+ process.stdout.write(
151
+ `${JSON.stringify({
152
+ valid: true,
153
+ generationId: validated.generation.generationId,
154
+ artifactScope: validationArtifactScope,
155
+ workspaceInputsValidated: !skipWorkspaceInputs,
156
+ validatedBy,
157
+ receiptWritten: Boolean(
158
+ validationReceiptFile &&
159
+ validatedBy === 'full-sha' &&
160
+ !skipWorkspaceInputs
161
+ ),
162
+ })}\n`
163
+ );
164
+ process.exit(0);
165
+ }
166
+ } catch (error) {
167
+ process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
168
+ process.exit(2);
169
+ }
170
+ emitRuntimePhase('generation_validation', validationStartedAt, {
171
+ artifactScope: 'server',
172
+ validatedBy,
173
+ ...(validationReceiptMiss ? { validationReceiptMiss } : {}),
174
+ });
175
+
176
+ const publicPort = Number(process.env.SERVER_PORT || 3000);
177
+ const publicHost = process.env.SERVER_HOST || '0.0.0.0';
178
+ const dependencyReadyFile = path.resolve(
179
+ process.env.MIAODA_DEPENDENCY_READY_FILE ||
180
+ path.join(projectRoot, '.miaoda-runtime', 'dependencies-ready.json')
181
+ );
182
+ function positiveDuration(name, fallback) {
183
+ const value = Number(process.env[name] || fallback);
184
+ return Number.isFinite(value) && value > 0 ? value : fallback;
185
+ }
186
+ const sourceReadyTimeoutMs = positiveDuration(
187
+ 'MIAODA_NEST_SOURCE_READY_TIMEOUT_MS',
188
+ 60_000
189
+ );
190
+ const cacheDrainTimeoutMs = positiveDuration(
191
+ 'MIAODA_NEST_CACHE_DRAIN_TIMEOUT_MS',
192
+ 5_000
193
+ );
194
+ const serviceId = randomUUID();
195
+ let stopping = false;
196
+
197
+ function resolveProbeHost(host) {
198
+ const normalized = String(host || '').trim();
199
+ const lower = normalized.toLowerCase();
200
+ if (!normalized || lower === '0.0.0.0') return '127.0.0.1';
201
+ if (lower === '::' || lower === '[::]') return '::1';
202
+ if (normalized.startsWith('[') && normalized.endsWith(']')) {
203
+ return normalized.slice(1, -1);
204
+ }
205
+ return normalized;
206
+ }
207
+
208
+ function waitForPort(host, port, timeoutMs = 60_000) {
209
+ const deadline = Date.now() + timeoutMs;
210
+ const probeHost = resolveProbeHost(host);
211
+ return new Promise((resolve, reject) => {
212
+ const attempt = () => {
213
+ if (stopping) return reject(new Error('MIAODA_SERVER_RUNTIME_STOPPING'));
214
+ const socket = net.createConnection({ host: probeHost, port });
215
+ const failed = () => {
216
+ socket.destroy();
217
+ if (Date.now() >= deadline) {
218
+ reject(new Error(`MIAODA_SERVER_UPSTREAM_TIMEOUT: ${port}`));
219
+ } else {
220
+ setTimeout(attempt, 100);
221
+ }
222
+ };
223
+ socket.once('connect', () => {
224
+ socket.destroy();
225
+ resolve();
226
+ });
227
+ socket.once('error', failed);
228
+ };
229
+ attempt();
230
+ });
231
+ }
232
+
233
+ function reservePort(host) {
234
+ return new Promise((resolve, reject) => {
235
+ const server = net.createServer();
236
+ server.once('error', reject);
237
+ server.listen(0, host, () => {
238
+ const address = server.address();
239
+ if (!address || typeof address === 'string') {
240
+ server.close(() =>
241
+ reject(new Error('MIAODA_SERVER_UPSTREAM_PORT_RESERVATION_FAILED'))
242
+ );
243
+ return;
244
+ }
245
+ server.close(error => (error ? reject(error) : resolve(address.port)));
246
+ });
247
+ });
248
+ }
249
+
250
+ function delay(ms) {
251
+ return new Promise(resolve => setTimeout(resolve, ms));
252
+ }
253
+
254
+ function childIsRunning(child) {
255
+ return Boolean(
256
+ child?.pid && child.exitCode === null && child.signalCode === null
257
+ );
258
+ }
259
+
260
+ function terminate(child) {
261
+ if (!child?.pid) return;
262
+ try {
263
+ process.kill(-child.pid, 'SIGTERM');
264
+ } catch {
265
+ try {
266
+ child.kill('SIGTERM');
267
+ } catch {}
268
+ }
269
+ }
270
+
271
+ function waitForChildExit(child, timeoutMs) {
272
+ if (!childIsRunning(child)) return Promise.resolve();
273
+ return Promise.race([
274
+ new Promise(resolve => child.once('exit', resolve)),
275
+ delay(timeoutMs),
276
+ ]);
277
+ }
278
+
279
+ function pruneSupersededGenerations() {
280
+ if (process.env.MIAODA_CACHE_ROOT) return;
281
+ const pruneScript = fileURLToPath(
282
+ new URL('./cache-generation-prune.mjs', import.meta.url)
283
+ );
284
+ if (!fs.existsSync(pruneScript)) {
285
+ process.stderr.write(
286
+ `${JSON.stringify({ event: 'miaoda_cache_generation_prune_failed', error: 'MIAODA_CACHE_GENERATION_PRUNE_SCRIPT_MISSING' })}\n`
287
+ );
288
+ return;
289
+ }
290
+ const pruneChild = spawn(process.execPath, [pruneScript, '--wait-ms=60000'], {
291
+ cwd: projectRoot,
292
+ stdio: 'inherit',
293
+ env: { ...process.env, MIAODA_WORKSPACE_ROOT: projectRoot },
294
+ });
295
+ pruneChild.once('error', error => {
296
+ process.stderr.write(
297
+ `${JSON.stringify({ event: 'miaoda_cache_generation_prune_failed', error: error.message })}\n`
298
+ );
299
+ });
300
+ pruneChild.once('exit', (code, signal) => {
301
+ if (code === 0) return;
302
+ process.stderr.write(
303
+ `${JSON.stringify({
304
+ event: 'miaoda_cache_generation_prune_failed',
305
+ error: signal ? `signal-${signal}` : `exit-${code}`,
306
+ })}\n`
307
+ );
308
+ });
309
+ }
310
+
311
+ const internalHost = resolveProbeHost(publicHost);
312
+ const cacheUpstreamPort = await reservePort(internalHost);
313
+ let sourceUpstreamPort = await reservePort(internalHost);
314
+ while (sourceUpstreamPort === cacheUpstreamPort) {
315
+ sourceUpstreamPort = await reservePort(internalHost);
316
+ }
317
+ const cacheUpstream = {
318
+ name: 'cache',
319
+ host: internalHost,
320
+ port: cacheUpstreamPort,
321
+ inflight: 0,
322
+ };
323
+ const sourceUpstream = {
324
+ name: 'source',
325
+ host: internalHost,
326
+ port: sourceUpstreamPort,
327
+ inflight: 0,
328
+ };
329
+ let activeUpstream = cacheUpstream;
330
+ let cacheRetired = false;
331
+ let sourceChild;
332
+ let actionPluginInitChild;
333
+ let actionPluginsInitialized = false;
334
+
335
+ function retain(upstream) {
336
+ upstream.inflight += 1;
337
+ let released = false;
338
+ return () => {
339
+ if (released) return;
340
+ released = true;
341
+ upstream.inflight = Math.max(0, upstream.inflight - 1);
342
+ };
343
+ }
344
+
345
+ const proxy = http.createServer((request, response) => {
346
+ const upstream = activeUpstream;
347
+ const release = retain(upstream);
348
+ const upstreamRequest = http.request(
349
+ {
350
+ hostname: upstream.host,
351
+ port: upstream.port,
352
+ method: request.method,
353
+ path: request.url,
354
+ headers: request.headers,
355
+ },
356
+ upstreamResponse => {
357
+ if (upstreamResponse.statusMessage) {
358
+ response.writeHead(
359
+ upstreamResponse.statusCode || 502,
360
+ upstreamResponse.statusMessage,
361
+ upstreamResponse.headers
362
+ );
363
+ } else {
364
+ response.writeHead(
365
+ upstreamResponse.statusCode || 502,
366
+ upstreamResponse.headers
367
+ );
368
+ }
369
+ upstreamResponse.pipe(response);
370
+ }
371
+ );
372
+ upstreamRequest.once('error', error => {
373
+ if (!response.headersSent) {
374
+ response.writeHead(502, { 'content-type': 'application/json' });
375
+ response.end(
376
+ `${JSON.stringify({
377
+ error: 'MIAODA_NEST_UPSTREAM_UNAVAILABLE',
378
+ upstream: upstream.name,
379
+ })}\n`
380
+ );
381
+ } else {
382
+ response.destroy(error);
383
+ }
384
+ });
385
+ request.once('aborted', () => upstreamRequest.destroy());
386
+ response.once('finish', release);
387
+ response.once('close', release);
388
+ request.pipe(upstreamRequest);
389
+ });
390
+
391
+ proxy.on('upgrade', (request, clientSocket, head) => {
392
+ const upstream = activeUpstream;
393
+ const release = retain(upstream);
394
+ const upstreamSocket = net.createConnection({
395
+ host: upstream.host,
396
+ port: upstream.port,
397
+ });
398
+ const close = () => {
399
+ release();
400
+ clientSocket.destroy();
401
+ upstreamSocket.destroy();
402
+ };
403
+ upstreamSocket.once('connect', () => {
404
+ const requestLine = `${request.method} ${request.url} HTTP/${request.httpVersion}\r\n`;
405
+ const headers = [];
406
+ for (let index = 0; index < request.rawHeaders.length; index += 2) {
407
+ headers.push(
408
+ `${request.rawHeaders[index]}: ${request.rawHeaders[index + 1]}`
409
+ );
410
+ }
411
+ upstreamSocket.write(`${requestLine}${headers.join('\r\n')}\r\n\r\n`);
412
+ if (head.length > 0) upstreamSocket.write(head);
413
+ clientSocket.pipe(upstreamSocket).pipe(clientSocket);
414
+ });
415
+ upstreamSocket.once('error', close);
416
+ clientSocket.once('error', close);
417
+ upstreamSocket.once('close', release);
418
+ clientSocket.once('close', release);
419
+ });
420
+
421
+ proxy.on('clientError', (_error, socket) => {
422
+ if (socket.writable) socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
423
+ });
424
+
425
+ function listen(server, host, port) {
426
+ return new Promise((resolve, reject) => {
427
+ server.once('error', reject);
428
+ server.listen(port, host, resolve);
429
+ });
430
+ }
431
+
432
+ const bundleSpawnStartedAt = process.hrtime.bigint();
433
+ const cacheChild = spawn(process.execPath, [validated.artifacts.serverBundle], {
434
+ cwd: projectRoot,
435
+ detached: true,
436
+ stdio: 'inherit',
437
+ env: {
438
+ ...process.env,
439
+ NODE_PATH: '',
440
+ SERVER_HOST: cacheUpstream.host,
441
+ SERVER_PORT: String(cacheUpstream.port),
442
+ },
443
+ });
444
+ emitRuntimePhase('bundle_spawn', bundleSpawnStartedAt, {
445
+ childPid: cacheChild.pid,
446
+ upstreamPort: cacheUpstream.port,
447
+ });
448
+ cacheChild.once('exit', (code, signal) => {
449
+ if (stopping || cacheRetired) return;
450
+ process.stderr.write(
451
+ `${JSON.stringify({ event: 'miaoda_nest_cache_runtime_exited', code, signal })}\n`
452
+ );
453
+ if (activeUpstream === cacheUpstream) process.exit(code || 1);
454
+ });
455
+
456
+ function dependenciesAreReady() {
457
+ try {
458
+ const marker = JSON.parse(fs.readFileSync(dependencyReadyFile, 'utf8'));
459
+ return (
460
+ marker?.schemaVersion === 1 &&
461
+ marker.generationId === validated.generation.generationId &&
462
+ fs.statSync(path.join(projectRoot, 'node_modules')).isDirectory()
463
+ );
464
+ } catch {
465
+ return false;
466
+ }
467
+ }
468
+
469
+ async function waitForDependencies() {
470
+ while (!stopping) {
471
+ if (dependenciesAreReady()) return;
472
+ await delay(100);
473
+ }
474
+ throw new Error('MIAODA_SERVER_RUNTIME_STOPPING');
475
+ }
476
+
477
+ async function initializeActionPlugins() {
478
+ if (actionPluginsInitialized) return;
479
+ const cli = [
480
+ process.env.MIAODA_FULLSTACK_CLI_BIN,
481
+ path.join(
482
+ projectRoot,
483
+ 'node_modules',
484
+ '@lark-apaas',
485
+ 'fullstack-cli',
486
+ 'bin',
487
+ 'cli.js'
488
+ ),
489
+ '/usr/lib/node_modules/@lark-apaas/fullstack-cli/bin/cli.js',
490
+ ].find(candidate => candidate && fs.existsSync(candidate));
491
+ if (!cli) {
492
+ process.stdout.write(
493
+ `${JSON.stringify({ event: 'miaoda_action_plugin_init_skipped', reason: 'project-cli-missing' })}\n`
494
+ );
495
+ return;
496
+ }
497
+ const startedAt = process.hrtime.bigint();
498
+ actionPluginInitChild = spawn(
499
+ process.execPath,
500
+ [cli, 'action-plugin', 'init'],
501
+ {
502
+ cwd: projectRoot,
503
+ detached: true,
504
+ stdio: 'inherit',
505
+ env: { ...process.env },
506
+ }
507
+ );
508
+ const code = await new Promise(resolve => {
509
+ actionPluginInitChild.once('exit', exitCode => resolve(exitCode ?? 1));
510
+ actionPluginInitChild.once('error', () => resolve(1));
511
+ });
512
+ actionPluginInitChild = undefined;
513
+ process.stdout.write(
514
+ `${JSON.stringify({
515
+ event: 'miaoda_action_plugin_init_finished',
516
+ code,
517
+ durationMs: Number(durationMs(startedAt).toFixed(3)),
518
+ })}\n`
519
+ );
520
+ if (code !== 0) {
521
+ process.stderr.write(
522
+ `${JSON.stringify({
523
+ event: 'miaoda_action_plugin_init_failed',
524
+ code,
525
+ continueStartup: true,
526
+ })}\n`
527
+ );
528
+ return;
529
+ }
530
+ actionPluginsInitialized = true;
531
+ }
532
+
533
+ async function drainCacheRuntime() {
534
+ const deadline = Date.now() + cacheDrainTimeoutMs;
535
+ while (!stopping && cacheUpstream.inflight > 0 && Date.now() < deadline) {
536
+ await delay(25);
537
+ }
538
+ cacheRetired = true;
539
+ terminate(cacheChild);
540
+ await waitForChildExit(cacheChild, 1_000);
541
+ process.stdout.write(
542
+ `${JSON.stringify({
543
+ event: 'miaoda_nest_cache_runtime_retired',
544
+ remainingInflight: cacheUpstream.inflight,
545
+ })}\n`
546
+ );
547
+ pruneSupersededGenerations();
548
+ }
549
+
550
+ async function promoteSourceRuntime() {
551
+ await waitForDependencies();
552
+ if (stopping) return;
553
+ const startedAt = process.hrtime.bigint();
554
+ process.stdout.write(
555
+ `${JSON.stringify({
556
+ event: 'miaoda_nest_source_runtime_starting',
557
+ dependencyReadyFile,
558
+ })}\n`
559
+ );
560
+ await initializeActionPlugins();
561
+ if (stopping) return;
562
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
563
+ sourceChild = spawn(npmCommand, ['run', 'dev:server'], {
564
+ cwd: projectRoot,
565
+ detached: true,
566
+ stdio: 'inherit',
567
+ env: {
568
+ ...process.env,
569
+ MIAODA_CACHE_BOOTSTRAP_ENABLED: 'false',
570
+ SERVER_HOST: sourceUpstream.host,
571
+ SERVER_PORT: String(sourceUpstream.port),
572
+ },
573
+ });
574
+ let sourceExit;
575
+ let sourcePromoted = false;
576
+ const sourceExitPromise = new Promise((_, reject) => {
577
+ sourceChild.once('exit', (code, signal) => {
578
+ sourceExit = { code, signal };
579
+ if (stopping) return;
580
+ process.stderr.write(
581
+ `${JSON.stringify({ event: 'miaoda_nest_source_runtime_exited', code, signal })}\n`
582
+ );
583
+ if (sourcePromoted) {
584
+ // The outer dev supervisor restarts this bridge. It first restores the
585
+ // already-sealed cache worker, then promotes the dependency-backed worker.
586
+ process.exit(code || 1);
587
+ return;
588
+ }
589
+ reject(
590
+ new Error(
591
+ `MIAODA_NEST_SOURCE_RUNTIME_EXITED_BEFORE_READY: code=${code} signal=${signal}`
592
+ )
593
+ );
594
+ });
595
+ sourceChild.once('error', reject);
596
+ });
597
+ await Promise.race([
598
+ waitForPort(sourceUpstream.host, sourceUpstream.port, sourceReadyTimeoutMs),
599
+ sourceExitPromise,
600
+ ]);
601
+ sourcePromoted = true;
602
+ if (sourceExit || !childIsRunning(sourceChild)) {
603
+ throw new Error('MIAODA_NEST_SOURCE_RUNTIME_EXITED_DURING_PROMOTION');
604
+ }
605
+ if (stopping) return;
606
+ activeUpstream = sourceUpstream;
607
+ process.stdout.write(
608
+ `${JSON.stringify({
609
+ event: 'miaoda_nest_source_runtime_ready',
610
+ pid: sourceChild.pid,
611
+ supervisorPid: process.pid,
612
+ serviceId,
613
+ port: publicPort,
614
+ upstreamPort: sourceUpstream.port,
615
+ cutoverDurationMs: Number(durationMs(startedAt).toFixed(3)),
616
+ })}\n`
617
+ );
618
+ void drainCacheRuntime();
619
+ }
620
+
621
+ async function close(signal) {
622
+ if (stopping) return;
623
+ stopping = true;
624
+ process.stdout.write(
625
+ `${JSON.stringify({ event: 'miaoda_nest_runtime_stopping', signal })}\n`
626
+ );
627
+ proxy.close();
628
+ terminate(actionPluginInitChild);
629
+ terminate(sourceChild);
630
+ terminate(cacheChild);
631
+ setTimeout(() => process.exit(0), 250).unref();
632
+ }
633
+
634
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
635
+ process.once(signal, () => void close(signal));
636
+ }
637
+
638
+ const listenWaitStartedAt = process.hrtime.bigint();
639
+ await waitForPort(cacheUpstream.host, cacheUpstream.port);
640
+ await listen(proxy, publicHost, publicPort);
641
+ emitRuntimePhase('listen_wait', listenWaitStartedAt, {
642
+ port: publicPort,
643
+ cacheUpstreamPort: cacheUpstream.port,
644
+ });
645
+ publishServingReadyMarker(servingReadyMarker);
646
+ process.stdout.write(
647
+ `${JSON.stringify({
648
+ event: 'miaoda_nest_cache_runtime_ready',
649
+ pid: cacheChild.pid,
650
+ supervisorPid: process.pid,
651
+ serviceId,
652
+ generationId: validated.generation.generationId,
653
+ port: publicPort,
654
+ startupDurationMs: Number(durationMs(runtimeStartedAt).toFixed(3)),
655
+ })}\n`
656
+ );
657
+ async function promoteSourceRuntimeWithRetry() {
658
+ try {
659
+ await promoteSourceRuntime();
660
+ } catch (error) {
661
+ if (stopping) return;
662
+ terminate(sourceChild);
663
+ await waitForChildExit(sourceChild, 1_000);
664
+ cacheRetired = true;
665
+ proxy.close();
666
+ terminate(cacheChild);
667
+ process.stderr.write(
668
+ `${JSON.stringify({
669
+ event: 'miaoda_nest_source_runtime_promotion_failed',
670
+ cacheServicePreserved: false,
671
+ fallback: 'original-preview-retry',
672
+ error: error instanceof Error ? error.message : String(error),
673
+ })}\n`
674
+ );
675
+ process.exitCode = 1;
676
+ setTimeout(() => process.exit(1), 250).unref();
677
+ }
678
+ }
679
+
680
+ void promoteSourceRuntimeWithRetry();
681
+ await new Promise(() => {});