@aws-blocks/core 0.1.4 → 0.1.10

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 (91) hide show
  1. package/dist/cdk/index.d.ts +1 -1
  2. package/dist/cdk/index.d.ts.map +1 -1
  3. package/dist/cdk/index.js +1 -1
  4. package/dist/client/index.d.ts +1 -1
  5. package/dist/client/index.d.ts.map +1 -1
  6. package/dist/client/index.js +1 -1
  7. package/dist/db-naming.d.ts +17 -5
  8. package/dist/db-naming.d.ts.map +1 -1
  9. package/dist/db-naming.js +18 -6
  10. package/dist/db-naming.test.js +44 -3
  11. package/dist/errors.d.ts +28 -0
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +27 -1
  14. package/dist/errors.test.d.ts +2 -0
  15. package/dist/errors.test.d.ts.map +1 -0
  16. package/dist/errors.test.js +47 -0
  17. package/dist/hosting.d.ts +71 -1
  18. package/dist/hosting.d.ts.map +1 -1
  19. package/dist/hosting.js +47 -8
  20. package/dist/hosting.test.js +60 -0
  21. package/dist/index.cdk.d.ts +1 -1
  22. package/dist/index.cdk.d.ts.map +1 -1
  23. package/dist/index.cdk.js +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/scripts/deploy.d.ts.map +1 -1
  28. package/dist/scripts/deploy.js +4 -2
  29. package/dist/scripts/dev-server-supervisor.test.d.ts +2 -0
  30. package/dist/scripts/dev-server-supervisor.test.d.ts.map +1 -0
  31. package/dist/scripts/dev-server-supervisor.test.js +551 -0
  32. package/dist/scripts/dev-server.d.ts +73 -0
  33. package/dist/scripts/dev-server.d.ts.map +1 -1
  34. package/dist/scripts/dev-server.js +279 -29
  35. package/dist/scripts/ensure-secrets.d.ts +5 -2
  36. package/dist/scripts/ensure-secrets.d.ts.map +1 -1
  37. package/dist/scripts/ensure-secrets.js +14 -6
  38. package/dist/scripts/external-migrations-step.d.ts.map +1 -1
  39. package/dist/scripts/external-migrations-step.js +5 -1
  40. package/dist/scripts/index.d.ts +1 -0
  41. package/dist/scripts/index.d.ts.map +1 -1
  42. package/dist/scripts/index.js +1 -0
  43. package/dist/scripts/process-tree.d.ts +126 -0
  44. package/dist/scripts/process-tree.d.ts.map +1 -0
  45. package/dist/scripts/process-tree.js +198 -0
  46. package/dist/scripts/sandbox.d.ts.map +1 -1
  47. package/dist/scripts/sandbox.js +51 -4
  48. package/dist/scripts/stack-id.d.ts +37 -0
  49. package/dist/scripts/stack-id.d.ts.map +1 -0
  50. package/dist/scripts/stack-id.js +79 -0
  51. package/dist/scripts/stack-id.test.d.ts +2 -0
  52. package/dist/scripts/stack-id.test.d.ts.map +1 -0
  53. package/dist/scripts/stack-id.test.js +104 -0
  54. package/dist/telemetry/client.d.ts +3 -1
  55. package/dist/telemetry/client.d.ts.map +1 -1
  56. package/dist/telemetry/client.js +20 -24
  57. package/dist/telemetry/telemetry-send-worker.d.ts +2 -0
  58. package/dist/telemetry/telemetry-send-worker.d.ts.map +1 -0
  59. package/dist/telemetry/telemetry-send-worker.js +58 -0
  60. package/dist/telemetry/telemetry.test.js +77 -1
  61. package/dist/telemetry/trackCommand.d.ts +1 -1
  62. package/dist/telemetry/trackCommand.js +3 -3
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.d.ts.map +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +1 -1
  67. package/src/cdk/index.ts +1 -1
  68. package/src/client/index.ts +1 -1
  69. package/src/db-naming.test.ts +50 -5
  70. package/src/db-naming.ts +18 -6
  71. package/src/errors.test.ts +55 -0
  72. package/src/errors.ts +32 -1
  73. package/src/hosting.test.ts +79 -0
  74. package/src/hosting.ts +127 -13
  75. package/src/index.cdk.ts +1 -1
  76. package/src/index.ts +1 -1
  77. package/src/scripts/deploy.ts +4 -2
  78. package/src/scripts/dev-server-supervisor.test.ts +621 -0
  79. package/src/scripts/dev-server.ts +316 -27
  80. package/src/scripts/ensure-secrets.ts +17 -6
  81. package/src/scripts/external-migrations-step.ts +5 -1
  82. package/src/scripts/index.ts +1 -0
  83. package/src/scripts/process-tree.ts +245 -0
  84. package/src/scripts/sandbox.ts +50 -4
  85. package/src/scripts/stack-id.test.ts +123 -0
  86. package/src/scripts/stack-id.ts +87 -0
  87. package/src/telemetry/client.ts +22 -30
  88. package/src/telemetry/telemetry-send-worker.ts +60 -0
  89. package/src/telemetry/telemetry.test.ts +91 -1
  90. package/src/telemetry/trackCommand.ts +3 -3
  91. package/src/version.ts +1 -1
@@ -36,5 +36,78 @@ export interface DevServerOptions {
36
36
  /** Port the frontend dev server listens on. Default: 3100. */
37
37
  frontendPort?: number;
38
38
  }
39
+ /** Bounded auto-respawn policy for the frontend dev server. */
40
+ export interface FrontendRespawnPolicy {
41
+ /** Max restarts allowed within `windowMs` before giving up (prevents hot loops). */
42
+ maxRestarts: number;
43
+ /** Sliding window (ms) over which restarts are counted. */
44
+ windowMs: number;
45
+ /** Base backoff (ms); doubles for each restart already in the window. */
46
+ backoffMs: number;
47
+ /** Upper bound (ms) on any single backoff delay. */
48
+ maxBackoffMs: number;
49
+ }
50
+ /** Default frontend respawn budget: 5 restarts / 10s, 500ms→5s exponential backoff. */
51
+ export declare const DEFAULT_FRONTEND_RESPAWN_POLICY: FrontendRespawnPolicy;
52
+ /** Outcome of {@link evaluateFrontendRespawn}. */
53
+ export interface RespawnDecision {
54
+ /** Whether the frontend should be respawned now. */
55
+ restart: boolean;
56
+ /** Delay (ms) to wait before respawning when `restart` is true. */
57
+ delayMs: number;
58
+ /**
59
+ * Restart timestamps still inside the window — plus the new attempt when
60
+ * restarting. The caller persists this for the next decision.
61
+ */
62
+ recent: number[];
63
+ }
64
+ /**
65
+ * Decide whether to auto-respawn the frontend dev server after an unexpected
66
+ * exit, given the timestamps of restarts not yet "forgiven".
67
+ *
68
+ * Semantics — the budget counts only *failing* restarts:
69
+ * - Timestamps older than `windowMs` are dropped from the sliding window.
70
+ * - If `maxRestarts` are still within the window, the budget is exhausted and
71
+ * the frontend is left down (no hot restart loop) — `restart: false`.
72
+ * - Otherwise `restart: true` with an exponential backoff (`backoffMs` doubled
73
+ * per in-window restart, capped at `maxBackoffMs`) and the new attempt
74
+ * appended to `recent`.
75
+ *
76
+ * This function is pure; the *meaning* of the budget is enforced by the caller,
77
+ * which **resets `recentRestarts` to `[]` once a respawn demonstrably succeeds**
78
+ * (the frontend port becomes bound — see `announceFrontendReady`). As a result
79
+ * only *consecutive failing* restarts accumulate toward `maxRestarts`: a
80
+ * frontend that legitimately restarts many times in a burst (e.g.
81
+ * editor-triggered Vite full reloads) refreshes its budget on each healthy bind
82
+ * and is never permanently left down — only a genuine crash loop that never
83
+ * rebinds the port trips the limit.
84
+ */
85
+ export declare function evaluateFrontendRespawn(recentRestarts: number[], now: number, policy?: FrontendRespawnPolicy): RespawnDecision;
86
+ /**
87
+ * Wait (bounded) for a TCP port to STOP accepting connections, i.e. for the
88
+ * listener to actually release the socket. Used after killing the frontend so a
89
+ * `tsx watch` relaunch can rebind `:3100` cleanly instead of racing the kernel's
90
+ * socket teardown and hitting `--strictPort` `EADDRINUSE`. Resolves as soon as
91
+ * the port is free, or once `timeoutMs` elapses (never rejects).
92
+ */
93
+ export declare function waitForPortFree(port: number, timeoutMs?: number): Promise<void>;
94
+ /**
95
+ * Decide whether a "frontend is listening" probe should be *credited* as a
96
+ * successful (re)spawn — and thus reset the restart budget.
97
+ *
98
+ * `waitForPort` only proves *something* is listening on `:3100`; it cannot tell
99
+ * our Vite apart from a foreign listener (a leftover Vite, or a second dev
100
+ * server). Crediting any listener would let a foreign process on `:3100` make
101
+ * every `--strictPort`-failing respawn look successful, neutralizing the
102
+ * `maxRestarts` cap and hot-looping forever. So we credit the probe only when
103
+ * **our** spawned child is still the live frontend process — same identity and
104
+ * not yet exited. A child that already exited (e.g. it lost the `--strictPort`
105
+ * bind race to the foreign listener) is no longer `current`, so it is not
106
+ * credited and its failed attempt still counts toward the budget.
107
+ */
108
+ export declare function shouldCreditFrontendReady(child: {
109
+ exitCode: number | null;
110
+ signalCode: NodeJS.Signals | null;
111
+ } | null, current: unknown): boolean;
39
112
  export declare function startDevServer(options: DevServerOptions): Promise<void>;
40
113
  //# sourceMappingURL=dev-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-server.ts"],"names":[],"mappings":"AAmCA,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAEhF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,sFAAsF;AACtF,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,mBAAmB,CAKvF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE/E;AAED,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAmCD,wBAAsB,cAAc,CAAC,OAAO,EAAE,gBAAgB,iBA8P7D"}
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-server.ts"],"names":[],"mappings":"AAoCA,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAEhF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,sFAAsF;AACtF,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,mBAAmB,CAKvF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE/E;AAED,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAmCD,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,uFAAuF;AACvF,eAAO,MAAM,+BAA+B,EAAE,qBAK7C,CAAC;AAEF,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,OAAO,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EAAE,EACxB,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,qBAAuD,GAC9D,eAAe,CAOjB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAenF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,EAC5E,OAAO,EAAE,OAAO,GACf,OAAO,CAOT;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,gBAAgB,iBAya7D"}
@@ -16,6 +16,7 @@ import { parseRpcRequest, successResponse, errorResponseFromCatch, methodNotFoun
16
16
  import { redactToJson } from '../redact.js';
17
17
  import { buildAndSendEvent } from '../telemetry/client.js';
18
18
  import { applyDevMigrations } from './external-migrations-step.js';
19
+ import { killFrontendTree, terminateProcessTree } from './process-tree.js';
19
20
  function toBodyStream(text) {
20
21
  if (!text)
21
22
  return null;
@@ -87,6 +88,86 @@ async function waitForPort(port, maxAttempts = 60) {
87
88
  }
88
89
  throw new Error(`Frontend server on port ${port} did not start within ${maxAttempts * 500}ms`);
89
90
  }
91
+ /** Default frontend respawn budget: 5 restarts / 10s, 500ms→5s exponential backoff. */
92
+ export const DEFAULT_FRONTEND_RESPAWN_POLICY = {
93
+ maxRestarts: 5,
94
+ windowMs: 10_000,
95
+ backoffMs: 500,
96
+ maxBackoffMs: 5_000,
97
+ };
98
+ /**
99
+ * Decide whether to auto-respawn the frontend dev server after an unexpected
100
+ * exit, given the timestamps of restarts not yet "forgiven".
101
+ *
102
+ * Semantics — the budget counts only *failing* restarts:
103
+ * - Timestamps older than `windowMs` are dropped from the sliding window.
104
+ * - If `maxRestarts` are still within the window, the budget is exhausted and
105
+ * the frontend is left down (no hot restart loop) — `restart: false`.
106
+ * - Otherwise `restart: true` with an exponential backoff (`backoffMs` doubled
107
+ * per in-window restart, capped at `maxBackoffMs`) and the new attempt
108
+ * appended to `recent`.
109
+ *
110
+ * This function is pure; the *meaning* of the budget is enforced by the caller,
111
+ * which **resets `recentRestarts` to `[]` once a respawn demonstrably succeeds**
112
+ * (the frontend port becomes bound — see `announceFrontendReady`). As a result
113
+ * only *consecutive failing* restarts accumulate toward `maxRestarts`: a
114
+ * frontend that legitimately restarts many times in a burst (e.g.
115
+ * editor-triggered Vite full reloads) refreshes its budget on each healthy bind
116
+ * and is never permanently left down — only a genuine crash loop that never
117
+ * rebinds the port trips the limit.
118
+ */
119
+ export function evaluateFrontendRespawn(recentRestarts, now, policy = DEFAULT_FRONTEND_RESPAWN_POLICY) {
120
+ const recent = recentRestarts.filter((t) => now - t < policy.windowMs);
121
+ if (recent.length >= policy.maxRestarts) {
122
+ return { restart: false, delayMs: 0, recent };
123
+ }
124
+ const delayMs = Math.min(policy.backoffMs * 2 ** recent.length, policy.maxBackoffMs);
125
+ return { restart: true, delayMs, recent: [...recent, now] };
126
+ }
127
+ /**
128
+ * Wait (bounded) for a TCP port to STOP accepting connections, i.e. for the
129
+ * listener to actually release the socket. Used after killing the frontend so a
130
+ * `tsx watch` relaunch can rebind `:3100` cleanly instead of racing the kernel's
131
+ * socket teardown and hitting `--strictPort` `EADDRINUSE`. Resolves as soon as
132
+ * the port is free, or once `timeoutMs` elapses (never rejects).
133
+ */
134
+ export async function waitForPortFree(port, timeoutMs = 2000) {
135
+ const { setTimeout: sleep } = await import('node:timers/promises');
136
+ const deadline = Date.now() + timeoutMs;
137
+ while (Date.now() < deadline) {
138
+ const open = await new Promise((resolve) => {
139
+ const socket = createConnection({ port, host: 'localhost' }, () => {
140
+ socket.destroy();
141
+ resolve(true);
142
+ });
143
+ socket.on('error', () => { socket.destroy(); resolve(false); });
144
+ socket.setTimeout(200, () => { socket.destroy(); resolve(false); });
145
+ });
146
+ if (!open)
147
+ return;
148
+ await sleep(100);
149
+ }
150
+ }
151
+ /**
152
+ * Decide whether a "frontend is listening" probe should be *credited* as a
153
+ * successful (re)spawn — and thus reset the restart budget.
154
+ *
155
+ * `waitForPort` only proves *something* is listening on `:3100`; it cannot tell
156
+ * our Vite apart from a foreign listener (a leftover Vite, or a second dev
157
+ * server). Crediting any listener would let a foreign process on `:3100` make
158
+ * every `--strictPort`-failing respawn look successful, neutralizing the
159
+ * `maxRestarts` cap and hot-looping forever. So we credit the probe only when
160
+ * **our** spawned child is still the live frontend process — same identity and
161
+ * not yet exited. A child that already exited (e.g. it lost the `--strictPort`
162
+ * bind race to the foreign listener) is no longer `current`, so it is not
163
+ * credited and its failed attempt still counts toward the budget.
164
+ */
165
+ export function shouldCreditFrontendReady(child, current) {
166
+ return (!!child &&
167
+ child === current &&
168
+ child.exitCode === null &&
169
+ child.signalCode === null);
170
+ }
90
171
  export async function startDevServer(options) {
91
172
  const { port = 3000, backendPath, frontendCommand, frontendPort = 3100, } = options;
92
173
  const devStartTime = Date.now();
@@ -155,6 +236,165 @@ export async function startDevServer(options) {
155
236
  res.writeHead(502);
156
237
  res.end('Frontend server unavailable');
157
238
  });
239
+ // ── Frontend supervisor ─────────────────────────────────────────────────
240
+ // The frontend runs under `shell: true`, so the real dev server (Vite) is a
241
+ // grandchild of this process. We spawn it `detached` (its own process group)
242
+ // on POSIX so cleanup/restart can signal the *whole* tree and free the port;
243
+ // otherwise the orphaned grandchild keeps `:3100` and every `/` request 502s
244
+ // forever (the proxy target is hardcoded to `frontendPort`). We also bound-
245
+ // respawn it on unexpected death and suppress all of this during shutdown.
246
+ //
247
+ // ── POST-EXIT GROUP-KILL POLICY ─────────────────────────────────────────
248
+ // The exact bug this supervisor fixes is the shell *exiting* while the
249
+ // detached grandchild survives, orphaned, still holding `:3100`. Reaping that
250
+ // orphan REQUIRES a group kill (`process.kill(-pid, …)`) issued *after* the
251
+ // shell has already exited — so all three post-exit kill sites below agree:
252
+ // the respawn path, `terminateFrontend`, and the `process.on('exit')` net all
253
+ // group-kill rather than skip when the shell is already gone.
254
+ //
255
+ // Why this is safe against the classic `-pid` PID-reuse hazard:
256
+ // 1. A surviving grandchild keeps the process group non-empty, so POSIX
257
+ // keeps `pid` reserved as the group id — it cannot be recycled as a new
258
+ // process id while it is still a live group's id. Hence `-pid` is
259
+ // guaranteed to target *our* group precisely when it matters (an orphan
260
+ // is still alive in it).
261
+ // 2. We only ever issue the kill synchronously, the instant we observe the
262
+ // shell's exit — there is no intervening `await` that could let the group
263
+ // drain and the pid be recycled — so the residual window is minimal.
264
+ // Residual accepted risk: if the ENTIRE group is already gone *and* `pid` has
265
+ // since been recycled into a brand-new group leader, `-pid` could signal an
266
+ // unrelated group. This is an accepted best-effort trade-off — there is then
267
+ // nothing of ours left to reap, whereas skipping the kill would otherwise
268
+ // leave `:3100` wedged, which is the failure this PR exists to prevent.
269
+ //
270
+ // Where each post-exit kill site lands on this trade-off: the two sites *in
271
+ // this file* — the respawn reap (in the child's `exit` handler) and the
272
+ // `process.on('exit')` net — fire synchronously the instant we observe the
273
+ // exit, so they lean on point (2) above and stay unconditional. The third
274
+ // path, `terminateFrontend` → `terminateProcessTree` (process-tree.ts), can
275
+ // run outside that minimal synchronous window, so it additionally PROBES group
276
+ // liveness (POSIX signal 0) and skips the reap once the group has fully
277
+ // drained — see its "POST-EXIT GROUP-KILL (scoped)" comment.
278
+ const usePosixProcessGroups = process.platform !== 'win32';
279
+ let isShuttingDown = false;
280
+ let frontendRestarts = [];
281
+ let respawnTimer = null;
282
+ const announceFrontendReady = async (child, suffix = '') => {
283
+ try {
284
+ await waitForPort(frontendPort);
285
+ // Reset the restart budget only when OUR child is the one now bound to
286
+ // `:3100`. `waitForPort` is a liveness-only probe — it cannot tell our
287
+ // Vite from a foreign listener (a leftover Vite or a second dev server),
288
+ // and crediting a foreign listener would make every `--strictPort`-failing
289
+ // respawn look successful, neutralizing the `maxRestarts` cap and
290
+ // hot-looping forever (see {@link shouldCreditFrontendReady}). Only
291
+ // *consecutive failing* restarts should count toward the give-up
292
+ // threshold, so a frontend that legitimately restarts many times (e.g.
293
+ // editor-triggered Vite full reloads) still never gets left down.
294
+ if (shouldCreditFrontendReady(child, frontendProcess)) {
295
+ frontendRestarts = [];
296
+ }
297
+ console.log(`\n ➜ http://localhost:${port}/${suffix}\n`);
298
+ }
299
+ catch (e) {
300
+ console.error(`⚠️ Frontend did not start: ${e.message}`);
301
+ console.log(`\n ➜ http://localhost:${port}/ (API only — frontend unavailable)\n`);
302
+ }
303
+ };
304
+ const spawnFrontend = (command) => {
305
+ const child = spawn(command, {
306
+ shell: true,
307
+ // Own process group on POSIX so we can reap the Vite grandchild too.
308
+ detached: usePosixProcessGroups,
309
+ stdio: ['ignore', 'pipe', 'pipe'],
310
+ env: { ...process.env, NODE_OPTIONS: '' },
311
+ });
312
+ frontendProcess = child;
313
+ // Suppress frontend output — only show errors.
314
+ child.stderr?.on('data', (d) => {
315
+ const msg = d.toString();
316
+ if (!msg.includes('DeprecationWarning'))
317
+ process.stderr.write(msg);
318
+ });
319
+ child.on('exit', (code, signal) => {
320
+ // Ignore exits from a process we've already replaced or torn down.
321
+ if (child !== frontendProcess)
322
+ return;
323
+ frontendProcess = null;
324
+ if (isShuttingDown)
325
+ return;
326
+ // Reap any orphaned grandchild left in this child's group so `:3100` is
327
+ // free before we respawn — otherwise `--strictPort` makes the new Vite
328
+ // exit on bind and we'd spin until the restart budget is gone. The shell
329
+ // has already exited here (we are inside its `exit` handler), so this is a
330
+ // post-exit group kill; it is issued synchronously in this handler and is
331
+ // safe against PID reuse — see POST-EXIT GROUP-KILL POLICY above.
332
+ killFrontendTree(child, 'SIGKILL');
333
+ const decision = evaluateFrontendRespawn(frontendRestarts, Date.now());
334
+ frontendRestarts = decision.recent;
335
+ const why = `code=${code ?? 'null'}, signal=${signal ?? 'null'}`;
336
+ if (!decision.restart) {
337
+ console.error(`⚠️ Frontend dev server exited (${why}) and exceeded ` +
338
+ `${DEFAULT_FRONTEND_RESPAWN_POLICY.maxRestarts} restarts within ` +
339
+ `${DEFAULT_FRONTEND_RESPAWN_POLICY.windowMs / 1000}s — leaving it down. ` +
340
+ `Fix the error above, then restart \`npm run dev\`.`);
341
+ return;
342
+ }
343
+ console.error(`⚠️ Frontend dev server exited (${why}); restarting in ${decision.delayMs}ms…`);
344
+ respawnTimer = setTimeout(() => {
345
+ respawnTimer = null;
346
+ if (isShuttingDown)
347
+ return;
348
+ // Before relaunching, wait (bounded) for `:3100` to actually free —
349
+ // mirroring the graceful `terminateFrontend` path. The synchronous
350
+ // post-exit SIGKILL above only *initiates* teardown of the orphaned
351
+ // group; the kernel can still hold the listening socket for a beat, and a
352
+ // relaunched `--strictPort` Vite would then hit `EADDRINUSE` and burn a
353
+ // restart-budget slot on a race that isn't a real crash. The budget was
354
+ // already debited above, so this never double-counts a restart; re-check
355
+ // `isShuttingDown` after the await, since a shutdown signal can land while
356
+ // we wait (`waitForPortFree` is bounded, so it can't deadlock shutdown).
357
+ void (async () => {
358
+ await waitForPortFree(frontendPort);
359
+ if (isShuttingDown)
360
+ return;
361
+ const next = spawnFrontend(command);
362
+ await announceFrontendReady(next, ' (frontend restarted)');
363
+ })();
364
+ }, decision.delayMs);
365
+ // INTENTIONAL unref: the listening HTTP `server` (created below) owns this
366
+ // process's lifetime — the backoff timer must NOT, by itself, keep the
367
+ // event loop alive. Without unref a pending respawn timer would hold the
368
+ // process up during shutdown (or after the server has closed), delaying or
369
+ // blocking a clean exit. This never drops a legitimately-needed respawn:
370
+ // `cleanup` explicitly clears this timer, and both the timer body and the
371
+ // awaited relaunch re-check `isShuttingDown`. Do NOT remove the unref to
372
+ // "fix" a perceived missed restart — it would reintroduce that shutdown hang.
373
+ respawnTimer.unref?.();
374
+ });
375
+ return child;
376
+ };
377
+ /**
378
+ * Gracefully terminate the frontend tree and wait (bounded) for the port to
379
+ * actually free before this process exits, so a `tsx watch` relaunch can
380
+ * rebind `:3100` cleanly. SIGTERM the group, escalate to SIGKILL if it lingers
381
+ * (via the shared {@link terminateProcessTree}), then poll until `:3100` is
382
+ * released. tsx-watch gives us ~5s before it force-kills us, so this budget is
383
+ * safe. Crucially the port-free wait runs on *both* paths — including when the
384
+ * shell has already exited — so the post-exit branch no longer drops the
385
+ * "wait for the port to free" guarantee.
386
+ */
387
+ const terminateFrontend = async (child) => {
388
+ if (!child)
389
+ return;
390
+ // SIGTERM→SIGKILL the whole tree, reaping the detached Vite grandchild even
391
+ // when the shell has already exited (post-exit group kill — see policy).
392
+ await terminateProcessTree(child, 1500);
393
+ // Then wait (bounded) for `:3100` to be released. The old post-exit branch
394
+ // returned right after SIGKILL with no port poll, so a relaunch could race
395
+ // the kernel's socket teardown and hit `--strictPort` `EADDRINUSE`.
396
+ await waitForPortFree(frontendPort);
397
+ };
158
398
  // ── API Gateway proxy (sandbox mode) ───────────────────────────────────
159
399
  // `changeOrigin: true` rewrites the outgoing `Host` to the execute-api target
160
400
  // (required for API Gateway's TLS SNI / host-based routing). That would make
@@ -266,35 +506,13 @@ export async function startDevServer(options) {
266
506
  await writeClientCode(resolvedPath, clientPath);
267
507
  }
268
508
  // ── Start listening ────────────────────────────────────────────────────
269
- server.listen(port, '127.0.0.1', async () => {
509
+ server.listen(port, async () => {
270
510
  console.log(`AWS Blocks local server running on http://localhost:${port}`);
271
511
  buildAndSendEvent({ command: 'dev', state: 'SUCCESS', duration: Date.now() - devStartTime });
272
512
  // Spawn frontend dev server after Blocks server is ready
273
513
  if (frontendCommand) {
274
- frontendProcess = spawn(frontendCommand, {
275
- shell: true,
276
- stdio: ['ignore', 'pipe', 'pipe'],
277
- env: { ...process.env, NODE_OPTIONS: '' },
278
- });
279
- // Suppress frontend output — only show errors
280
- frontendProcess.stderr?.on('data', (d) => {
281
- const msg = d.toString();
282
- if (!msg.includes('DeprecationWarning'))
283
- process.stderr.write(msg);
284
- });
285
- frontendProcess.on('exit', (code) => {
286
- if (code !== 0 && code !== null) {
287
- console.error(`⚠️ Frontend process exited with code ${code}`);
288
- }
289
- });
290
- try {
291
- await waitForPort(frontendPort);
292
- console.log(`\n ➜ http://localhost:${port}/\n`);
293
- }
294
- catch (e) {
295
- console.error(`⚠️ Frontend did not start: ${e.message}`);
296
- console.log(`\n ➜ http://localhost:${port}/ (API only — frontend unavailable)\n`);
297
- }
514
+ const child = spawnFrontend(frontendCommand);
515
+ await announceFrontendReady(child);
298
516
  }
299
517
  else {
300
518
  console.log(`\n ➜ http://localhost:${port}/\n`);
@@ -305,10 +523,26 @@ export async function startDevServer(options) {
305
523
  buildAndSendEvent({ command: 'dev', state: 'FAIL', duration: Date.now() - devStartTime, error: { code: errorCode, phase: 'startup' } });
306
524
  });
307
525
  // ── Cleanup ────────────────────────────────────────────────────────────
526
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
527
+ let cleaningUp = false;
308
528
  const cleanup = async () => {
529
+ if (cleaningUp)
530
+ return; // idempotent — a second signal must not re-enter
531
+ cleaningUp = true;
532
+ isShuttingDown = true; // stop the supervisor from respawning the frontend
309
533
  console.log('\nShutting down...');
310
- if (frontendProcess)
311
- frontendProcess.kill('SIGTERM');
534
+ if (respawnTimer) {
535
+ clearTimeout(respawnTimer);
536
+ respawnTimer = null;
537
+ }
538
+ // Detach our own listeners so repeated signals can't pile up handlers.
539
+ for (const sig of signals)
540
+ process.removeListener(sig, cleanup);
541
+ // Kill the frontend process *group* and wait for the port to free before
542
+ // we exit, so a tsx-watch restart can rebind `:3100` cleanly.
543
+ const child = frontendProcess;
544
+ frontendProcess = null;
545
+ await terminateFrontend(child);
312
546
  if (typeof backend.__cleanup === 'function') {
313
547
  try {
314
548
  await backend.__cleanup();
@@ -320,8 +554,24 @@ export async function startDevServer(options) {
320
554
  server.close(() => process.exit(0));
321
555
  setTimeout(() => process.exit(0), 2000).unref();
322
556
  };
323
- process.on('SIGINT', cleanup);
324
- process.on('SIGTERM', cleanup);
557
+ for (const sig of signals)
558
+ process.on(sig, cleanup);
559
+ // Last-resort safety net for paths that bypass `cleanup` (e.g. an uncaught
560
+ // exception terminating the process): synchronously reap the frontend tree so
561
+ // a `detached` Vite is never left orphaned on `:3100`. Reuses
562
+ // `killFrontendTree`, so unlike the old hand-rolled `process.kill(-pid)` it
563
+ // also reaps on Windows (via `taskkill`) instead of early-returning and
564
+ // leaking the Vite tree, and stays in lockstep with the other kill sites. Both
565
+ // the POSIX group kill and the Windows `taskkill` are synchronous, so this is
566
+ // legal in an `exit` handler; it reaps even when the shell has already exited
567
+ // (a surviving grandchild keeps the group alive) — see POST-EXIT GROUP-KILL
568
+ // POLICY above.
569
+ process.once('exit', () => {
570
+ const child = frontendProcess;
571
+ if (!child)
572
+ return;
573
+ killFrontendTree(child, 'SIGKILL');
574
+ });
325
575
  }
326
576
  // ── Local API handler ────────────────────────────────────────────────────────
327
577
  function handleApiRequest(req, res, url, method, apis) {
@@ -8,9 +8,12 @@ export declare function findConnectionString(): {
8
8
  value: string;
9
9
  } | null;
10
10
  /**
11
- * Ensure the connection string is stored in SSM for the current stage.
11
+ * Ensure the connection string is stored in SSM under this app's stack-scoped
12
+ * parameter name. `projectRoot` locates the committed `.blocks/config.json`
13
+ * that defines the stack name; it must match the root used at synth (the deploy
14
+ * commands pass it explicitly) so the written name equals the name the app reads.
12
15
  */
13
- export declare function ensureSecrets(stage?: 'sandbox' | 'production'): Promise<EnsureSecretsResult>;
16
+ export declare function ensureSecrets(stage?: string, projectRoot?: string): Promise<EnsureSecretsResult>;
14
17
  /**
15
18
  * Load environment for production deployment.
16
19
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ensure-secrets.d.ts","sourceRoot":"","sources":["../../src/scripts/ensure-secrets.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAgB,oBAAoB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO7E;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsClG;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAIxC;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAelD"}
1
+ {"version":3,"file":"ensure-secrets.d.ts","sourceRoot":"","sources":["../../src/scripts/ensure-secrets.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAgB,oBAAoB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO7E;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,mBAAmB,CAAC,CAsC9B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAIxC;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAelD"}
@@ -3,15 +3,20 @@
3
3
  /**
4
4
  * Pre-deploy secret provisioning.
5
5
  *
6
- * Writes the connection string to an SSM SecureString parameter.
7
- * Parameter name includes the stage to prevent sandbox/prod collision:
8
- * /blocks/{stage}/db-connection-string
6
+ * Writes the connection string to an SSM SecureString parameter. The parameter
7
+ * name is stack-scoped (`/<stackName>-db-url` via `dbConnectionParameterName`),
8
+ * so two Blocks apps in the same account/region/stage never collide. The synth
9
+ * step names the parameter with the same function and the same inputs
10
+ * (`projectRoot` + stage), so the value written here is read back under the
11
+ * identical name — which is why this must be given the same `projectRoot` the
12
+ * deploy command passes to synth.
9
13
  *
10
14
  * On first deploy: creates the parameter.
11
15
  * On subsequent deploys: updates if value changed, no-op otherwise.
12
16
  */
13
17
  import { existsSync, readFileSync } from 'node:fs';
14
18
  import { dbConnectionParameterName } from '../db-naming.js';
19
+ import { getStackName } from './stack-id.js';
15
20
  const CONNECTION_STRING_PATTERN = /_(DB_URL|CONNECTION_STRING)$/;
16
21
  export function findConnectionString() {
17
22
  for (const [name, value] of Object.entries(process.env)) {
@@ -22,9 +27,12 @@ export function findConnectionString() {
22
27
  return null;
23
28
  }
24
29
  /**
25
- * Ensure the connection string is stored in SSM for the current stage.
30
+ * Ensure the connection string is stored in SSM under this app's stack-scoped
31
+ * parameter name. `projectRoot` locates the committed `.blocks/config.json`
32
+ * that defines the stack name; it must match the root used at synth (the deploy
33
+ * commands pass it explicitly) so the written name equals the name the app reads.
26
34
  */
27
- export async function ensureSecrets(stage) {
35
+ export async function ensureSecrets(stage, projectRoot) {
28
36
  const result = { created: [], updated: [], unchanged: [] };
29
37
  const conn = findConnectionString();
30
38
  if (!conn)
@@ -32,7 +40,7 @@ export async function ensureSecrets(stage) {
32
40
  const resolvedStage = stage ?? process.env.BLOCKS_STAGE ?? 'sandbox';
33
41
  const { SSMClient, GetParameterCommand, PutParameterCommand } = await import('@aws-sdk/client-ssm');
34
42
  const client = new SSMClient();
35
- const parameterName = dbConnectionParameterName(resolvedStage);
43
+ const parameterName = dbConnectionParameterName(getStackName({ sandbox: resolvedStage !== 'production', projectRoot }));
36
44
  let isNew = false;
37
45
  try {
38
46
  const current = await client.send(new GetParameterCommand({
@@ -1 +1 @@
1
- {"version":3,"file":"external-migrations-step.d.ts","sourceRoot":"","sources":["../../src/scripts/external-migrations-step.ts"],"names":[],"mappings":"AAmCA;;;;;GAKG;AACH;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAI5G;AAcD,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,SAAS,GAAG,YAAY,CAAC;IAChC,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,SAAS,GAAG,YAAY,GAAG,OAAO,CAErF;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,8BAA8B,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBpG;AAoBD,qFAAqF;AACrF,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAe3E;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAExF;AAuED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BnH"}
1
+ {"version":3,"file":"external-migrations-step.d.ts","sourceRoot":"","sources":["../../src/scripts/external-migrations-step.ts"],"names":[],"mappings":"AAoCA;;;;;GAKG;AACH;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAI5G;AAcD,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,SAAS,GAAG,YAAY,CAAC;IAChC,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,SAAS,GAAG,YAAY,GAAG,OAAO,CAErF;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,8BAA8B,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBpG;AAoBD,qFAAqF;AACrF,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAe3E;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAExF;AA0ED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BnH"}
@@ -25,6 +25,7 @@
25
25
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
26
26
  import { findConnectionString } from './ensure-secrets.js';
27
27
  import { extractDbRef, dbConnectionParameterName } from '../db-naming.js';
28
+ import { getStackName } from './stack-id.js';
28
29
  import { runSync } from './run-command.js';
29
30
  const DEFAULT_MIGRATIONS_DIR = './migrations';
30
31
  /** Default output dir for db-pull generated files (database.types.ts / database.meta.ts). */
@@ -150,7 +151,10 @@ async function productionRefs(devRef) {
150
151
  }
151
152
  try {
152
153
  const { SSMClient, GetParameterCommand } = await import('@aws-sdk/client-ssm');
153
- const res = await new SSMClient().send(new GetParameterCommand({ Name: dbConnectionParameterName('production'), WithDecryption: true }));
154
+ const res = await new SSMClient().send(new GetParameterCommand({
155
+ Name: dbConnectionParameterName(getStackName({ sandbox: false })),
156
+ WithDecryption: true,
157
+ }));
154
158
  const v = res.Parameter?.Value;
155
159
  const r = v ? safeRef(v) : null;
156
160
  if (r)
@@ -9,4 +9,5 @@ export { openConsole, type ConsoleOptions } from './console.js';
9
9
  export { ensureSecrets, loadProductionEnv, loadEnvFile } from './ensure-secrets.js';
10
10
  export { trackCommand, buildAndSendEvent, classifyError, type CommandName, type CommandState, type BuildAndSendEventOptions, } from '../telemetry/index.js';
11
11
  export { telemetry, type TelemetryOptions } from './telemetry.js';
12
+ export { getStackId, getSandboxId, getStackName } from './stack-id.js';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scripts/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,wBAAwB,GAC9B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scripts/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,wBAAwB,GAC9B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC"}
@@ -11,3 +11,4 @@ export { openConsole } from './console.js';
11
11
  export { ensureSecrets, loadProductionEnv, loadEnvFile } from './ensure-secrets.js';
12
12
  export { trackCommand, buildAndSendEvent, classifyError, } from '../telemetry/index.js';
13
13
  export { telemetry } from './telemetry.js';
14
+ export { getStackId, getSandboxId, getStackName } from './stack-id.js';