@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,655 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import crypto from 'node:crypto';
5
+ import http from 'node:http';
6
+ import net from 'node:net';
7
+ import path from 'node:path';
8
+ import { createRequire } from 'node:module';
9
+ import { spawn } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+ import {
12
+ validateCacheGeneration,
13
+ validateCacheGenerationReceipt,
14
+ } from './cache-generation-preflight.mjs';
15
+
16
+ const projectRoot = path.resolve(
17
+ process.env.MIAODA_WORKSPACE_ROOT || process.cwd()
18
+ );
19
+ const requestedCacheRoot = path.resolve(
20
+ process.env.MIAODA_CACHE_ROOT ||
21
+ path.join(projectRoot, '.miaoda-cache', 'current')
22
+ );
23
+ const cacheRoot = fs.realpathSync(requestedCacheRoot);
24
+ const runtimeManifestFile = path.resolve(
25
+ process.env.MIAODA_PLATFORM_RUNTIME_MANIFEST ||
26
+ '/opt/miaoda/preview-runtime/runtime-manifest.json'
27
+ );
28
+ const validateOnly = process.argv.includes('--validate-only');
29
+ const publicHost = process.env.SERVER_HOST || '0.0.0.0';
30
+ const publicPort = Number(process.env.SERVER_PORT || 3000);
31
+ const cacheNodeModules = path.join(cacheRoot, 'server', 'node_modules');
32
+ const cacheBundleFile = path.join(
33
+ cacheRoot,
34
+ 'server',
35
+ 'bundle',
36
+ 'server.bundle.cjs'
37
+ );
38
+ const cacheBundleMetadataFile = `${cacheBundleFile}.meta.json`;
39
+ const resolverFile = path.join(
40
+ path.dirname(fileURLToPath(import.meta.url)),
41
+ 'server-cache-module-resolver.cjs'
42
+ );
43
+ const readyMarker =
44
+ process.env.MIAODA_NEST_TRANSITION_READY_FILE ||
45
+ '/tmp/event/MIAODA_NEST_TRANSITION_SERVING_READY';
46
+ const pollIntervalMs = Number(
47
+ process.env.MIAODA_NEST_SOURCE_POLL_INTERVAL_MS || 250
48
+ );
49
+ const restartDebounceMs = Number(
50
+ process.env.MIAODA_NEST_SOURCE_RESTART_DEBOUNCE_MS || 120
51
+ );
52
+ const startupTimeoutMs = Number(
53
+ process.env.MIAODA_NEST_SOURCE_READY_TIMEOUT_MS || 60_000
54
+ );
55
+ const drainTimeoutMs = Number(
56
+ process.env.MIAODA_NEST_TRANSITION_DRAIN_TIMEOUT_MS || 5_000
57
+ );
58
+
59
+ function output(name, message) {
60
+ for (const line of String(message).split(/\r?\n/)) {
61
+ if (line) process.stdout.write(`MIAODA_LOG\t${name}\t${line}\n`);
62
+ }
63
+ }
64
+
65
+ function event(name, details = {}) {
66
+ process.stdout.write(
67
+ `${JSON.stringify({ event: name, runtime: 'nest-transition', ...details })}\n`
68
+ );
69
+ }
70
+
71
+ function validateGeneration() {
72
+ let validated;
73
+ const receiptFile = process.env.MIAODA_CACHE_VALIDATION_RECEIPT_FILE;
74
+ if (receiptFile) {
75
+ try {
76
+ validated = validateCacheGenerationReceipt({
77
+ projectRoot,
78
+ cacheRoot,
79
+ runtimeManifestFile,
80
+ receiptFile,
81
+ artifactScope: 'server',
82
+ });
83
+ } catch {
84
+ // Receipt is an optimization only; a miss falls back to full validation.
85
+ }
86
+ }
87
+ validated ??= validateCacheGeneration({
88
+ projectRoot,
89
+ cacheRoot,
90
+ runtimeManifestFile,
91
+ artifactScope: 'server',
92
+ });
93
+ if (
94
+ validated.generation.schemaVersion !== 4 ||
95
+ validated.generation.serverRuntimeMode !==
96
+ 'workspace-source-dependencies' ||
97
+ !validated.serverDependencies
98
+ ) {
99
+ throw new Error('MIAODA_SERVER_TRANSITION_GENERATION_UNSUPPORTED');
100
+ }
101
+ return validated;
102
+ }
103
+
104
+ let validated;
105
+ try {
106
+ validated = validateGeneration();
107
+ if (!fs.existsSync(resolverFile)) {
108
+ throw new Error(`MIAODA_CACHE_BOOTSTRAP_INPUT_MISSING: ${resolverFile}`);
109
+ }
110
+ if (validateOnly) {
111
+ process.stdout.write(
112
+ `${JSON.stringify({
113
+ valid: true,
114
+ generationId: validated.generation.generationId,
115
+ sourceEntry: validated.serverDependencies.sourceEntry,
116
+ })}\n`
117
+ );
118
+ process.exit(0);
119
+ }
120
+ } catch (error) {
121
+ process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
122
+ process.exit(2);
123
+ }
124
+
125
+ if (
126
+ !Number.isSafeInteger(publicPort) ||
127
+ publicPort <= 0 ||
128
+ publicPort > 65_535
129
+ ) {
130
+ throw new Error('MIAODA_NEST_TRANSITION_PORT_INVALID');
131
+ }
132
+
133
+ const packageJsonFile = path.join(projectRoot, 'package.json');
134
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8'));
135
+ const configuredProbe =
136
+ packageJson.miaodaCache?.availabilityProbe?.readiness ?? {};
137
+ const probe = {
138
+ ...configuredProbe,
139
+ // The platform mounts the application server below CLIENT_BASE_PATH. Use
140
+ // that runtime fact before template defaults so old and new workspaces share
141
+ // the same readiness contract.
142
+ path:
143
+ process.env.MIAODA_SERVER_READY_PATH ||
144
+ process.env.CLIENT_BASE_PATH ||
145
+ configuredProbe.path ||
146
+ '/',
147
+ expectedStatus: Number(configuredProbe.expectedStatus ?? 200),
148
+ };
149
+ const sourceEntry = path.resolve(
150
+ projectRoot,
151
+ validated.serverDependencies.sourceEntry
152
+ );
153
+ if (!fs.existsSync(sourceEntry)) {
154
+ throw new Error(`MIAODA_NEST_SOURCE_ENTRY_MISSING: ${sourceEntry}`);
155
+ }
156
+ const cacheRequire = createRequire(
157
+ path.join(cacheRoot, 'server', 'cache-runtime-entry.cjs')
158
+ );
159
+ const bootstrapFiles = validated.serverDependencies.bootstrapModules.map(
160
+ specifier => {
161
+ try {
162
+ return cacheRequire.resolve(specifier);
163
+ } catch {
164
+ throw new Error(`MIAODA_NEST_BOOTSTRAP_MODULE_MISSING: ${specifier}`);
165
+ }
166
+ }
167
+ );
168
+ const projectRootToken = '${MIAODA_WORKSPACE_ROOT}';
169
+ const materializeProjectPaths = value => {
170
+ if (typeof value === 'string') {
171
+ if (value === projectRootToken) return projectRoot;
172
+ if (value.startsWith(`${projectRootToken}/`)) {
173
+ const resolved = path.resolve(
174
+ projectRoot,
175
+ value.slice(projectRootToken.length + 1)
176
+ );
177
+ if (!resolved.startsWith(`${projectRoot}${path.sep}`)) {
178
+ throw new Error('MIAODA_NEST_TRANSITION_TSCONFIG_PATH_REJECTED');
179
+ }
180
+ return resolved;
181
+ }
182
+ return value;
183
+ }
184
+ if (Array.isArray(value)) return value.map(materializeProjectPaths);
185
+ if (value && typeof value === 'object') {
186
+ return Object.fromEntries(
187
+ Object.entries(value).map(([key, entry]) => [
188
+ key,
189
+ materializeProjectPaths(entry),
190
+ ])
191
+ );
192
+ }
193
+ return value;
194
+ };
195
+ const transitionTsconfigDirectory = path.join(
196
+ projectRoot,
197
+ '.miaoda-runtime',
198
+ 'nest-transition'
199
+ );
200
+ fs.mkdirSync(transitionTsconfigDirectory, { recursive: true });
201
+ const transitionTsconfigFile = path.join(
202
+ transitionTsconfigDirectory,
203
+ `tsconfig.${validated.generation.generationId}.json`
204
+ );
205
+ const transitionTsconfigTemporary = `${transitionTsconfigFile}.${process.pid}.tmp`;
206
+ fs.writeFileSync(
207
+ transitionTsconfigTemporary,
208
+ `${JSON.stringify(
209
+ {
210
+ compilerOptions: {
211
+ ...materializeProjectPaths(
212
+ validated.serverDependencies.transitionCompilerOptions
213
+ ),
214
+ // The transition runtime only transpiles files that Node actually loads.
215
+ // Type-check/build outputs remain owned by the unchanged full service.
216
+ incremental: false,
217
+ composite: false,
218
+ declaration: false,
219
+ declarationMap: false,
220
+ },
221
+ 'ts-node': { transpileOnly: true, files: false },
222
+ },
223
+ null,
224
+ 2
225
+ )}\n`,
226
+ { mode: 0o600 }
227
+ );
228
+ fs.renameSync(transitionTsconfigTemporary, transitionTsconfigFile);
229
+
230
+ function reservePort() {
231
+ return new Promise((resolve, reject) => {
232
+ const server = net.createServer();
233
+ server.once('error', reject);
234
+ server.listen(0, '127.0.0.1', () => {
235
+ const address = server.address();
236
+ const port = address && typeof address !== 'string' ? address.port : 0;
237
+ server.close(error => (error ? reject(error) : resolve(port)));
238
+ });
239
+ });
240
+ }
241
+
242
+ async function waitForProbe(port, child) {
243
+ const deadline = Date.now() + startupTimeoutMs;
244
+ let lastError;
245
+ while (Date.now() < deadline) {
246
+ if (child.exitCode !== null || child.signalCode !== null) {
247
+ throw new Error(
248
+ `MIAODA_NEST_TRANSITION_EXITED: ${child.exitCode ?? child.signalCode}`
249
+ );
250
+ }
251
+ try {
252
+ const response = await fetch(
253
+ `http://127.0.0.1:${port}${probe.path || '/'}`
254
+ );
255
+ const body = await response.text();
256
+ if (
257
+ response.status === Number(probe.expectedStatus ?? 200) &&
258
+ (!probe.bodyIncludes || body.includes(probe.bodyIncludes))
259
+ ) {
260
+ return;
261
+ }
262
+ lastError = new Error(
263
+ `probe returned ${response.status}, expected ${probe.expectedStatus ?? 200}`
264
+ );
265
+ } catch (error) {
266
+ lastError = error;
267
+ }
268
+ await new Promise(resolve => setTimeout(resolve, 100));
269
+ }
270
+ throw new Error(
271
+ `MIAODA_NEST_TRANSITION_READY_TIMEOUT: ${
272
+ lastError instanceof Error ? lastError.message : String(lastError)
273
+ }`
274
+ );
275
+ }
276
+
277
+ function terminate(child, signal = 'SIGTERM') {
278
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
279
+ try {
280
+ child.kill(signal);
281
+ } catch {}
282
+ }
283
+
284
+ let activeBackend;
285
+ let closing = false;
286
+ let starting = false;
287
+ let preferInitialBundle = false;
288
+ let restartPending = false;
289
+ let restartTimer;
290
+ let watchTimer;
291
+ const children = new Set();
292
+
293
+ function spawnSourceWorker(port) {
294
+ const args = [resolverFile, ...bootstrapFiles].flatMap(file => ['-r', file]);
295
+ args.push(sourceEntry);
296
+ const childEnvironment = {
297
+ ...process.env,
298
+ NODE_ENV: 'development',
299
+ SERVER_HOST: '127.0.0.1',
300
+ SERVER_PORT: String(port),
301
+ NODE_PATH: cacheNodeModules,
302
+ MIAODA_SERVER_CACHE_NODE_MODULES: cacheNodeModules,
303
+ MIAODA_CACHE_BOOTSTRAP_ENABLED: 'false',
304
+ TS_NODE_TRANSPILE_ONLY: 'true',
305
+ TS_NODE_PREFER_TS_EXTS: 'true',
306
+ TS_NODE_PROJECT: transitionTsconfigFile,
307
+ };
308
+ // This is a user-facing development service, not the isolated generation
309
+ // probe. Inherit the same real database/auth environment as the unchanged
310
+ // full service. In particular, never carry the OpenAPI probe's
311
+ // DEPRECATED_SKIP_INIT_DB_CONNECTION flag into the transition runtime: that
312
+ // flag injects a placeholder database object and makes business routes fail.
313
+ delete childEnvironment.MIAODA_SERVER_CACHE_PROBE;
314
+ delete childEnvironment.DEPRECATED_SKIP_INIT_DB_CONNECTION;
315
+ const child = spawn(process.execPath, args, {
316
+ cwd: projectRoot,
317
+ env: childEnvironment,
318
+ stdio: ['ignore', 'pipe', 'pipe'],
319
+ });
320
+ children.add(child);
321
+ child.stdout?.on('data', chunk => output('server', chunk));
322
+ child.stderr?.on('data', chunk => output('server', chunk));
323
+ child.once('exit', (code, signal) => {
324
+ children.delete(child);
325
+ event('miaoda_nest_transition_worker_exit', {
326
+ pid: child.pid,
327
+ code,
328
+ signal,
329
+ active: activeBackend?.child === child,
330
+ });
331
+ if (!closing && activeBackend?.child === child) scheduleRestart(0);
332
+ });
333
+ return child;
334
+ }
335
+
336
+ function workspaceSourceSha256() {
337
+ const hash = crypto.createHash('sha256');
338
+ for (const file of workspaceFiles()) {
339
+ const relative = path.relative(projectRoot, file).split(path.sep).join('/');
340
+ if (
341
+ !['server/', 'shared/', 'src/'].some(root => relative.startsWith(root))
342
+ ) {
343
+ continue;
344
+ }
345
+ hash.update(relative);
346
+ hash.update('\0');
347
+ hash.update(fs.readFileSync(file));
348
+ hash.update('\0');
349
+ }
350
+ return hash.digest('hex');
351
+ }
352
+
353
+ function initialBundleIsConsumable() {
354
+ try {
355
+ const metadata = JSON.parse(
356
+ fs.readFileSync(cacheBundleMetadataFile, 'utf8')
357
+ );
358
+ return (
359
+ metadata.schemaVersion === 2 &&
360
+ metadata.consumable === true &&
361
+ metadata.workspaceSourceSha256 === workspaceSourceSha256() &&
362
+ metadata.bundleSha256 ===
363
+ crypto
364
+ .createHash('sha256')
365
+ .update(fs.readFileSync(cacheBundleFile))
366
+ .digest('hex')
367
+ );
368
+ } catch {
369
+ return false;
370
+ }
371
+ }
372
+
373
+ function spawnBundleWorker(port) {
374
+ const childEnvironment = {
375
+ ...process.env,
376
+ NODE_ENV: 'development',
377
+ SERVER_HOST: '127.0.0.1',
378
+ SERVER_PORT: String(port),
379
+ NODE_PATH: '',
380
+ MIAODA_WORKSPACE_ROOT: projectRoot,
381
+ MIAODA_CACHE_BOOTSTRAP_ENABLED: 'false',
382
+ };
383
+ delete childEnvironment.MIAODA_SERVER_CACHE_PROBE;
384
+ delete childEnvironment.DEPRECATED_SKIP_INIT_DB_CONNECTION;
385
+ const child = spawn(process.execPath, [cacheBundleFile], {
386
+ // Keep process.cwd() identical to the normal NestJS service. Application
387
+ // code and framework modules resolve .env, server/capabilities,
388
+ // dist/client and other workspace files from this root. Assets derived
389
+ // from __dirname, __filename and import.meta.url are still sealed beside
390
+ // the bundle and do not depend on cwd.
391
+ cwd: projectRoot,
392
+ env: childEnvironment,
393
+ stdio: ['ignore', 'pipe', 'pipe'],
394
+ });
395
+ children.add(child);
396
+ child.stdout?.on('data', chunk => output('server', chunk));
397
+ child.stderr?.on('data', chunk => output('server', chunk));
398
+ child.once('exit', (code, signal) => {
399
+ children.delete(child);
400
+ event('miaoda_nest_transition_worker_exit', {
401
+ pid: child.pid,
402
+ code,
403
+ signal,
404
+ workerMode: 'bundle',
405
+ active: activeBackend?.child === child,
406
+ });
407
+ if (!closing && activeBackend?.child === child) {
408
+ preferInitialBundle = false;
409
+ scheduleRestart(0);
410
+ }
411
+ });
412
+ return child;
413
+ }
414
+
415
+ async function drainBackend(backend) {
416
+ const deadline = Date.now() + drainTimeoutMs;
417
+ while (backend.activeRequests > 0 && Date.now() < deadline) {
418
+ await new Promise(resolve => setTimeout(resolve, 25));
419
+ }
420
+ terminate(backend.child);
421
+ setTimeout(() => terminate(backend.child, 'SIGKILL'), 1_000).unref();
422
+ }
423
+
424
+ async function startCandidate() {
425
+ if (closing || starting) {
426
+ restartPending = true;
427
+ return;
428
+ }
429
+ starting = true;
430
+ const startedAt = Date.now();
431
+ let child;
432
+ let workerMode;
433
+ try {
434
+ const port = await reservePort();
435
+ workerMode = preferInitialBundle ? 'bundle' : 'workspace-source';
436
+ child = preferInitialBundle
437
+ ? spawnBundleWorker(port)
438
+ : spawnSourceWorker(port);
439
+ await waitForProbe(port, child);
440
+ const previous = activeBackend;
441
+ activeBackend = {
442
+ kind: 'transition',
443
+ workerMode,
444
+ port,
445
+ child,
446
+ activeRequests: 0,
447
+ sourceFingerprint: workspaceFingerprint(),
448
+ };
449
+ event('miaoda_nest_transition_ready', {
450
+ generationId: validated.generation.generationId,
451
+ pid: child.pid,
452
+ port: publicPort,
453
+ upstreamPort: port,
454
+ startupMs: Date.now() - startedAt,
455
+ workerMode,
456
+ replacedPid: previous?.child.pid,
457
+ });
458
+ publishReady();
459
+ if (previous) void drainBackend(previous);
460
+ } catch (error) {
461
+ terminate(child);
462
+ if (workerMode === 'bundle') {
463
+ // A bundle is attempted only once. If it cannot become ready, retrying
464
+ // the same immutable bytes cannot heal it; fall back to live Workspace
465
+ // source plus the sealed dependency tree.
466
+ preferInitialBundle = false;
467
+ }
468
+ output(
469
+ 'server',
470
+ `Transition source candidate failed; keeping current service: ${
471
+ error instanceof Error ? error.message : String(error)
472
+ }`
473
+ );
474
+ if (!activeBackend && !closing) scheduleRestart(500);
475
+ } finally {
476
+ starting = false;
477
+ if (restartPending) {
478
+ restartPending = false;
479
+ scheduleRestart(restartDebounceMs);
480
+ }
481
+ }
482
+ }
483
+
484
+ function scheduleRestart(delay = restartDebounceMs) {
485
+ if (closing) return;
486
+ if (restartTimer) clearTimeout(restartTimer);
487
+ restartTimer = setTimeout(() => {
488
+ restartTimer = undefined;
489
+ void startCandidate();
490
+ }, delay);
491
+ }
492
+
493
+ function workspaceFiles() {
494
+ const files = [];
495
+ const visit = directory => {
496
+ if (!fs.existsSync(directory)) return;
497
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
498
+ const candidate = path.join(directory, entry.name);
499
+ if (entry.isSymbolicLink()) continue;
500
+ if (entry.isDirectory()) visit(candidate);
501
+ else if (entry.isFile()) files.push(candidate);
502
+ }
503
+ };
504
+ for (const root of ['server', 'shared', 'src']) {
505
+ visit(path.join(projectRoot, root));
506
+ }
507
+ for (const relative of [
508
+ 'package.json',
509
+ 'nest-cli.json',
510
+ 'tsconfig.json',
511
+ 'tsconfig.node.json',
512
+ '.env',
513
+ '.env.development',
514
+ '.env.development.local',
515
+ ]) {
516
+ const file = path.join(projectRoot, relative);
517
+ if (fs.existsSync(file)) files.push(file);
518
+ }
519
+ return files.sort();
520
+ }
521
+
522
+ function workspaceFingerprint() {
523
+ return workspaceFiles()
524
+ .map(file => {
525
+ const stat = fs.statSync(file);
526
+ return `${path.relative(projectRoot, file)}:${stat.size}:${stat.mtimeMs}`;
527
+ })
528
+ .join('|');
529
+ }
530
+
531
+ preferInitialBundle =
532
+ process.env.MIAODA_NEST_DISABLE_INITIAL_BUNDLE !== 'true' &&
533
+ initialBundleIsConsumable();
534
+
535
+ function publishReady() {
536
+ fs.mkdirSync(path.dirname(readyMarker), { recursive: true });
537
+ const temporary = `${readyMarker}.${process.pid}.tmp`;
538
+ fs.writeFileSync(
539
+ temporary,
540
+ `${JSON.stringify({
541
+ schemaVersion: 1,
542
+ generationId: validated.generation.generationId,
543
+ pid: process.pid,
544
+ port: publicPort,
545
+ })}\n`,
546
+ { mode: 0o600 }
547
+ );
548
+ fs.renameSync(temporary, readyMarker);
549
+ }
550
+
551
+ const proxy = http.createServer((request, response) => {
552
+ if (request.url === '/dev/cache-runtime') {
553
+ response.statusCode = activeBackend ? 200 : 503;
554
+ response.setHeader('content-type', 'application/json');
555
+ response.end(
556
+ JSON.stringify({
557
+ mode: 'transition',
558
+ runtime: 'nest',
559
+ generationId: validated.generation.generationId,
560
+ pid: activeBackend?.child.pid,
561
+ ready: Boolean(activeBackend),
562
+ })
563
+ );
564
+ return;
565
+ }
566
+ const backend = activeBackend;
567
+ if (!backend) {
568
+ response.statusCode = 503;
569
+ response.end('MIAODA_NEST_TRANSITION_NOT_READY');
570
+ return;
571
+ }
572
+ backend.activeRequests++;
573
+ const upstream = http.request(
574
+ {
575
+ host: '127.0.0.1',
576
+ port: backend.port,
577
+ method: request.method,
578
+ path: request.url,
579
+ headers: request.headers,
580
+ },
581
+ upstreamResponse => {
582
+ response.writeHead(
583
+ upstreamResponse.statusCode || 502,
584
+ upstreamResponse.headers
585
+ );
586
+ upstreamResponse.pipe(response);
587
+ }
588
+ );
589
+ const release = () => {
590
+ if (backend.activeRequests > 0) backend.activeRequests--;
591
+ };
592
+ response.once('close', release);
593
+ upstream.once('error', error => {
594
+ if (!response.headersSent) response.statusCode = 502;
595
+ response.end(error.message);
596
+ });
597
+ request.pipe(upstream);
598
+ });
599
+
600
+ proxy.on('upgrade', (request, socket, head) => {
601
+ const backend = activeBackend;
602
+ if (!backend) return socket.destroy();
603
+ const upstream = net.createConnection(
604
+ { host: '127.0.0.1', port: backend.port },
605
+ () => {
606
+ let headers = `${request.method} ${request.url} HTTP/${request.httpVersion}\r\n`;
607
+ for (let index = 0; index < request.rawHeaders.length; index += 2) {
608
+ headers += `${request.rawHeaders[index]}: ${request.rawHeaders[index + 1]}\r\n`;
609
+ }
610
+ upstream.write(`${headers}\r\n`);
611
+ if (head.length > 0) upstream.write(head);
612
+ socket.pipe(upstream).pipe(socket);
613
+ }
614
+ );
615
+ upstream.once('error', () => socket.destroy());
616
+ });
617
+
618
+ async function close(signal) {
619
+ if (closing) return;
620
+ closing = true;
621
+ if (watchTimer) clearInterval(watchTimer);
622
+ if (restartTimer) clearTimeout(restartTimer);
623
+ fs.rmSync(readyMarker, { force: true });
624
+ await new Promise(resolve => proxy.close(resolve));
625
+ for (const child of children) terminate(child);
626
+ setTimeout(() => {
627
+ for (const child of children) terminate(child, 'SIGKILL');
628
+ }, 1_000).unref();
629
+ event('miaoda_nest_transition_stopped', { signal });
630
+ }
631
+
632
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
633
+ process.once(signal, () => void close(signal));
634
+ }
635
+
636
+ fs.rmSync(readyMarker, { force: true });
637
+ await new Promise((resolve, reject) => {
638
+ proxy.once('error', reject);
639
+ proxy.listen(publicPort, publicHost, resolve);
640
+ });
641
+ await startCandidate();
642
+ let fingerprint = workspaceFingerprint();
643
+ watchTimer = setInterval(() => {
644
+ try {
645
+ const current = workspaceFingerprint();
646
+ if (current !== fingerprint) {
647
+ fingerprint = current;
648
+ preferInitialBundle = false;
649
+ scheduleRestart();
650
+ }
651
+ } catch (error) {
652
+ output('server', error instanceof Error ? error.message : String(error));
653
+ }
654
+ }, pollIntervalMs);
655
+ watchTimer.unref();