@awsless/cli 0.1.6 → 0.1.8

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 (2) hide show
  1. package/dist/bin.js +856 -200
  2. package/package.json +14 -14
package/dist/bin.js CHANGED
@@ -99132,15 +99132,15 @@ var require_pack = __commonJS((exports, module) => {
99132
99132
  process.nextTick(callback);
99133
99133
  return new Void;
99134
99134
  }
99135
- var sink2 = new Sink(this);
99136
- this._stream = sink2;
99137
- eos(sink2, function(err) {
99135
+ var sink3 = new Sink(this);
99136
+ this._stream = sink3;
99137
+ eos(sink3, function(err) {
99138
99138
  self2._stream = null;
99139
99139
  if (err) {
99140
99140
  self2.destroy();
99141
99141
  return callback(err);
99142
99142
  }
99143
- if (sink2.written !== header.size) {
99143
+ if (sink3.written !== header.size) {
99144
99144
  self2.destroy();
99145
99145
  return callback(new Error("size mismatch"));
99146
99146
  }
@@ -99149,7 +99149,7 @@ var require_pack = __commonJS((exports, module) => {
99149
99149
  self2.finalize();
99150
99150
  callback();
99151
99151
  });
99152
- return sink2;
99152
+ return sink3;
99153
99153
  };
99154
99154
  Pack.prototype.finalize = function() {
99155
99155
  if (this._stream) {
@@ -103147,7 +103147,15 @@ var import_decompress, import_sleep_await, mock, searchClient = (options = {}, s
103147
103147
  return Object.entries(settings).map(([key, value]) => {
103148
103148
  return ["-E", `${key}=${value}`];
103149
103149
  }).flat();
103150
- }, launch = ({ path: path6, host, port, version: version2, debug: debug4 }) => {
103150
+ }, launch = ({
103151
+ path: path6,
103152
+ host,
103153
+ port,
103154
+ version: version2,
103155
+ debug: debug4,
103156
+ onExit: onDied,
103157
+ onOutput
103158
+ }) => {
103151
103159
  return new Promise(async (resolve22, reject) => {
103152
103160
  const cache3 = join32(path6, "cache", String(port));
103153
103161
  const cleanUp = async () => {
@@ -103185,7 +103193,9 @@ ${output.join("")}`);
103185
103193
  done();
103186
103194
  }
103187
103195
  };
103196
+ let stopping = false;
103188
103197
  const kill = async () => {
103198
+ stopping = true;
103189
103199
  if (child.exitCode === null && !child.killed) {
103190
103200
  await new Promise((resolve32) => {
103191
103201
  child.once(`exit`, () => {
@@ -103214,6 +103224,23 @@ ${output.join("")}`);
103214
103224
  };
103215
103225
  const done = async () => {
103216
103226
  off();
103227
+ child.once("exit", (code, signal) => {
103228
+ if (!stopping) {
103229
+ onDied?.(code, signal);
103230
+ }
103231
+ });
103232
+ if (onOutput) {
103233
+ const capture = (chunk3) => {
103234
+ for (const line of chunk3.toString().split(`
103235
+ `)) {
103236
+ if (line.trim() !== "") {
103237
+ onOutput(line);
103238
+ }
103239
+ }
103240
+ };
103241
+ child.stdout.on("data", capture);
103242
+ child.stderr.on("data", capture);
103243
+ }
103217
103244
  resolve22(kill);
103218
103245
  };
103219
103246
  const fail = async (error3) => {
@@ -108442,6 +108469,10 @@ var clearDebugLog = () => {
108442
108469
  ready = true;
108443
108470
  } catch (_) {}
108444
108471
  };
108472
+ var sink;
108473
+ var setDebugSink = (listener) => {
108474
+ sink = listener;
108475
+ };
108445
108476
  var write = (type, message) => {
108446
108477
  try {
108447
108478
  if (!ready) {
@@ -108449,6 +108480,7 @@ var write = (type, message) => {
108449
108480
  }
108450
108481
  appendFileSync(debugLogFile, `${new Date().toISOString()} [${type}] ${message}
108451
108482
  `);
108483
+ sink?.(type, message);
108452
108484
  } catch (_) {}
108453
108485
  };
108454
108486
  var format = (parts) => {
@@ -128380,7 +128412,7 @@ import {
128380
128412
  UpdateKeysCommand
128381
128413
  } from "@aws-sdk/client-cloudfront-keyvaluestore";
128382
128414
 
128383
- // ../../node_modules/.pnpm/@terraforge+core@0.0.43_aws-crt@1.30.0_debug@4.4.3_supports-color@8.1.1__supports-color@8.1.1__graphology-types@0.24.8/node_modules/@terraforge/core/dist/index.mjs
128415
+ // ../../node_modules/.pnpm/@terraforge+core@0.0.44_aws-crt@1.30.0_debug@4.4.3_supports-color@8.1.1__supports-color@8.1.1__graphology-types@0.24.8/node_modules/@terraforge/core/dist/index.mjs
128384
128416
  var import_async_on_exit = __toESM(require_async_on_exit(), 1);
128385
128417
 
128386
128418
  // ../../node_modules/.pnpm/p-limit@6.2.0/node_modules/p-limit/index.js
@@ -131718,7 +131750,7 @@ var getter = (options = {}) => {
131718
131750
  };
131719
131751
  var get = getter();
131720
131752
 
131721
- // ../../node_modules/.pnpm/@terraforge+core@0.0.43_aws-crt@1.30.0_debug@4.4.3_supports-color@8.1.1__supports-color@8.1.1__graphology-types@0.24.8/node_modules/@terraforge/core/dist/index.mjs
131753
+ // ../../node_modules/.pnpm/@terraforge+core@0.0.44_aws-crt@1.30.0_debug@4.4.3_supports-color@8.1.1__supports-color@8.1.1__graphology-types@0.24.8/node_modules/@terraforge/core/dist/index.mjs
131722
131754
  import { appendFile, mkdir, readFile as readFile4, rename, rm, writeFile as writeFile2 } from "fs/promises";
131723
131755
  var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
131724
131756
  import { DynamoDB } from "@aws-sdk/client-dynamodb";
@@ -131991,7 +132023,7 @@ var unmarshall = (data, options) => {
131991
132023
  return convertToNative({ M: data }, options);
131992
132024
  };
131993
132025
 
131994
- // ../../node_modules/.pnpm/@terraforge+core@0.0.43_aws-crt@1.30.0_debug@4.4.3_supports-color@8.1.1__supports-color@8.1.1__graphology-types@0.24.8/node_modules/@terraforge/core/dist/index.mjs
132026
+ // ../../node_modules/.pnpm/@terraforge+core@0.0.44_aws-crt@1.30.0_debug@4.4.3_supports-color@8.1.1__supports-color@8.1.1__graphology-types@0.24.8/node_modules/@terraforge/core/dist/index.mjs
131995
132027
  import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
131996
132028
  import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
131997
132029
  var nodeMetaSymbol = Symbol("metadata");
@@ -132009,9 +132041,6 @@ var isDataSource = (obj) => {
132009
132041
  return isNode(obj) && obj[nodeMetaSymbol].tag === "data";
132010
132042
  };
132011
132043
  var Group = class Group2 {
132012
- parent;
132013
- type;
132014
- name;
132015
132044
  children = [];
132016
132045
  constructor(parent, type, name) {
132017
132046
  this.parent = parent;
@@ -132057,7 +132086,6 @@ var Group = class Group2 {
132057
132086
  }
132058
132087
  };
132059
132088
  var Stack = class extends Group {
132060
- app;
132061
132089
  dependencies = /* @__PURE__ */ new Set;
132062
132090
  constructor(app, name) {
132063
132091
  super(app, "stack", name);
@@ -132072,7 +132100,6 @@ var findParentStack = (group4) => {
132072
132100
  return findParentStack(group4.parent);
132073
132101
  };
132074
132102
  var App = class extends Group {
132075
- name;
132076
132103
  constructor(name) {
132077
132104
  super(undefined, "app", name);
132078
132105
  this.name = name;
@@ -132086,8 +132113,6 @@ var PENDING = 1;
132086
132113
  var RESOLVED = 2;
132087
132114
  var REJECTED = 3;
132088
132115
  var Future = class Future2 {
132089
- callback;
132090
- volatile;
132091
132116
  listeners = /* @__PURE__ */ new Set;
132092
132117
  status = IDLE;
132093
132118
  data;
@@ -132109,28 +132134,28 @@ var Future = class Future2 {
132109
132134
  }
132110
132135
  }
132111
132136
  pipe(cb) {
132112
- return new Future2((resolve, reject) => {
132137
+ return new Future2((resolve$1, reject) => {
132113
132138
  this.then((value) => {
132114
- Promise.resolve(cb(value)).then((value2) => {
132115
- resolve(value2);
132139
+ Promise.resolve(cb(value)).then((value$1) => {
132140
+ resolve$1(value$1);
132116
132141
  }).catch(reject);
132117
132142
  }, reject);
132118
132143
  }, this.volatile);
132119
132144
  }
132120
- then(resolve, reject) {
132145
+ then(resolve$1, reject) {
132121
132146
  if (this.volatile)
132122
132147
  try {
132123
- Promise.resolve(this.callback(resolve, (error3) => reject?.(error3))).catch((error3) => reject?.(error3));
132148
+ Promise.resolve(this.callback(resolve$1, (error3) => reject?.(error3))).catch((error3) => reject?.(error3));
132124
132149
  } catch (error3) {
132125
132150
  reject?.(error3);
132126
132151
  }
132127
132152
  else if (this.status === RESOLVED)
132128
- resolve(this.data);
132153
+ resolve$1(this.data);
132129
132154
  else if (this.status === REJECTED)
132130
132155
  reject?.(this.error);
132131
132156
  else {
132132
132157
  this.listeners.add({
132133
- resolve,
132158
+ resolve: resolve$1,
132134
132159
  reject
132135
132160
  });
132136
132161
  if (this.status === IDLE) {
@@ -132139,7 +132164,7 @@ var Future = class Future2 {
132139
132164
  if (this.status === PENDING) {
132140
132165
  this.status = RESOLVED;
132141
132166
  this.data = data;
132142
- this.listeners.forEach(({ resolve: resolve2 }) => resolve2(data));
132167
+ this.listeners.forEach(({ resolve: resolve$2 }) => resolve$2(data));
132143
132168
  this.listeners.clear();
132144
132169
  }
132145
132170
  };
@@ -132147,7 +132172,7 @@ var Future = class Future2 {
132147
132172
  if (this.status === PENDING) {
132148
132173
  this.status = REJECTED;
132149
132174
  this.error = error3;
132150
- this.listeners.forEach(({ reject: reject2 }) => reject2?.(error3));
132175
+ this.listeners.forEach(({ reject: reject$1 }) => reject$1?.(error3));
132151
132176
  this.listeners.clear();
132152
132177
  }
132153
132178
  };
@@ -132162,13 +132187,13 @@ var Future = class Future2 {
132162
132187
  };
132163
132188
  var findInputDeps = (props) => {
132164
132189
  const deps = [];
132165
- const find = (props2) => {
132166
- if (props2 instanceof Output)
132167
- deps.push(...props2.dependencies);
132168
- else if (Array.isArray(props2))
132169
- props2.map(find);
132170
- else if (props2?.constructor === Object)
132171
- Object.values(props2).map(find);
132190
+ const find = (props$1) => {
132191
+ if (props$1 instanceof Output)
132192
+ deps.push(...props$1.dependencies);
132193
+ else if (Array.isArray(props$1))
132194
+ props$1.map(find);
132195
+ else if (props$1?.constructor === Object)
132196
+ Object.values(props$1).map(find);
132172
132197
  };
132173
132198
  find(props);
132174
132199
  return deps;
@@ -132191,7 +132216,7 @@ var resolveWithTimeout = async (promise) => {
132191
132216
  }
132192
132217
  };
132193
132218
  var resolveInputs = async (inputs, fallback) => {
132194
- const resolve = async (value, path) => {
132219
+ const resolve$1 = async (value, path) => {
132195
132220
  if (value instanceof Output || value instanceof Future || value instanceof Promise)
132196
132221
  try {
132197
132222
  return await resolveWithTimeout(value);
@@ -132201,31 +132226,30 @@ var resolveInputs = async (inputs, fallback) => {
132201
132226
  throw error3;
132202
132227
  }
132203
132228
  if (Array.isArray(value))
132204
- return Promise.all(value.map((item, index) => resolve(item, [...path, index])));
132229
+ return Promise.all(value.map((item, index) => resolve$1(item, [...path, index])));
132205
132230
  if (value?.constructor === Object) {
132206
- const entries = Object.entries(value);
132207
- const resolved = await Promise.all(entries.map(([key, item]) => resolve(item, [...path, key])));
132231
+ const entries$1 = Object.entries(value);
132232
+ const resolved = await Promise.all(entries$1.map(([key, item]) => resolve$1(item, [...path, key])));
132208
132233
  const result = {};
132209
- entries.forEach(([key], i3) => {
132234
+ entries$1.forEach(([key], i3) => {
132210
132235
  result[key] = resolved[i3];
132211
132236
  });
132212
132237
  return result;
132213
132238
  }
132214
132239
  return value;
132215
132240
  };
132216
- return resolve(inputs, []);
132241
+ return resolve$1(inputs, []);
132217
132242
  };
132218
132243
  var Output = class Output2 extends Future {
132219
- dependencies;
132220
132244
  constructor(dependencies, callback, volatile = false) {
132221
132245
  super(callback, volatile);
132222
132246
  this.dependencies = dependencies;
132223
132247
  }
132224
132248
  pipe(cb) {
132225
- return new Output2(this.dependencies, (resolve, reject) => {
132249
+ return new Output2(this.dependencies, (resolve$1, reject) => {
132226
132250
  this.then((value) => {
132227
- Promise.resolve(cb(value)).then((value2) => {
132228
- resolve(value2);
132251
+ Promise.resolve(cb(value)).then((value$1) => {
132252
+ resolve$1(value$1);
132229
132253
  }).catch(reject);
132230
132254
  }, reject);
132231
132255
  }, this.volatile);
@@ -132241,9 +132265,9 @@ var hasVolatileInput = (props) => {
132241
132265
  return false;
132242
132266
  };
132243
132267
  var combine = (...inputs) => {
132244
- return new Output(new Set(findInputDeps(inputs)), (resolve, reject) => {
132268
+ return new Output(new Set(findInputDeps(inputs)), (resolve$1, reject) => {
132245
132269
  Promise.all(inputs).then(resolveInputs).then((result) => {
132246
- resolve(result);
132270
+ resolve$1(result);
132247
132271
  }, reject);
132248
132272
  }, hasVolatileInput(inputs));
132249
132273
  };
@@ -132267,7 +132291,7 @@ var createUrn = (tag, type, name, parentUrn) => {
132267
132291
  var createMeta = (tag, provider, parent, type, logicalId, input, config) => {
132268
132292
  const urn = createUrn(tag, type, logicalId, parent.urn);
132269
132293
  const stack = findParentStack(parent);
132270
- let output;
132294
+ let output$1;
132271
132295
  let resolved = false;
132272
132296
  return {
132273
132297
  tag,
@@ -132292,16 +132316,16 @@ var createMeta = (tag, provider, parent, type, logicalId, input, config) => {
132292
132316
  return dependencies;
132293
132317
  },
132294
132318
  resolve(data) {
132295
- output = data;
132319
+ output$1 = data;
132296
132320
  resolved = true;
132297
132321
  },
132298
132322
  output(cb) {
132299
- return new Output(/* @__PURE__ */ new Set([this]), (resolve2, reject) => {
132323
+ return new Output(new Set([this]), (resolve$1, reject) => {
132300
132324
  if (!resolved) {
132301
132325
  reject(/* @__PURE__ */ new Error(`Unresolved output for ${tag}: ${urn}`));
132302
132326
  return;
132303
132327
  }
132304
- resolve2(cb(output));
132328
+ resolve$1(cb(output$1));
132305
132329
  }, true);
132306
132330
  }
132307
132331
  };
@@ -132311,17 +132335,16 @@ var consoleSink = (group4, ...args) => {
132311
132335
  console.log(`${group4}:`, ...args);
132312
132336
  console.log();
132313
132337
  };
132314
- var sink;
132338
+ var sink2;
132315
132339
  var enableDebug = (customSink) => {
132316
- sink = customSink ?? consoleSink;
132340
+ sink2 = customSink ?? consoleSink;
132317
132341
  };
132318
132342
  var createDebugger = (group4) => {
132319
132343
  return (...args) => {
132320
- sink?.(group4, ...args);
132344
+ sink2?.(group4, ...args);
132321
132345
  };
132322
132346
  };
132323
132347
  var AlreadyLockedError = class extends Error {
132324
- urn;
132325
132348
  constructor(urn) {
132326
132349
  super(`Already locked: ${urn}`);
132327
132350
  this.urn = urn;
@@ -132527,9 +132550,6 @@ var allowsDependentReplace = (replaceOnChanges, dependencyPaths) => {
132527
132550
  return false;
132528
132551
  };
132529
132552
  var ResourceError = class ResourceError2 extends Error {
132530
- urn;
132531
- type;
132532
- operation;
132533
132553
  static wrap(urn, type, operation, error3) {
132534
132554
  if (error3 instanceof Error)
132535
132555
  return new ResourceError2(urn, type, operation, error3.message);
@@ -132543,8 +132563,6 @@ var ResourceError = class ResourceError2 extends Error {
132543
132563
  }
132544
132564
  };
132545
132565
  var AppError = class extends Error {
132546
- app;
132547
- issues;
132548
132566
  constructor(app, issues, message4) {
132549
132567
  super(message4);
132550
132568
  this.app = app;
@@ -132573,8 +132591,8 @@ var v1 = (oldAppState) => {
132573
132591
  const stacks = {};
132574
132592
  for (const [urn, stack] of entries(oldAppState.stacks)) {
132575
132593
  const nodes2 = {};
132576
- for (const [urn2, resource] of entries(stack.resources))
132577
- nodes2[urn2] = {
132594
+ for (const [urn$1, resource] of entries(stack.resources))
132595
+ nodes2[urn$1] = {
132578
132596
  ...resource,
132579
132597
  tag: "resource"
132580
132598
  };
@@ -132815,6 +132833,8 @@ var importResource = async (resource, input, opt) => {
132815
132833
  }
132816
132834
  });
132817
132835
  } catch (error3) {
132836
+ if (error3 instanceof ResourceNotFound)
132837
+ throw new ResourceNotFound(`The "${meta.type}" resource import "${meta.config?.import}" doesn't exist. (${meta.urn})`);
132818
132838
  throw ResourceError.wrap(meta.urn, meta.type, "import", error3);
132819
132839
  }
132820
132840
  return {
@@ -132996,11 +133016,11 @@ var deployApp = async (app, opt) => {
132996
133016
  });
132997
133017
  const isFilteredIn = !opt.filters?.length || opt.filters.includes(stackState.name);
132998
133018
  if (!found && isFilteredIn)
132999
- for (const [urn2, nodeState] of entries(stackState.nodes))
133000
- graph.add(urn2, dependentsOn(allNodes, urn2), async () => {
133019
+ for (const [urn$1, nodeState] of entries(stackState.nodes))
133020
+ graph.add(urn$1, dependentsOn(allNodes, urn$1), async () => {
133001
133021
  if (nodeState.tag === "resource")
133002
- await queue(() => deleteResource(appState.idempotentToken, urn2, nodeState, opt));
133003
- delete stackState.nodes[urn2];
133022
+ await queue(() => deleteResource(appState.idempotentToken, urn$1, nodeState, opt));
133023
+ delete stackState.nodes[urn$1];
133004
133024
  });
133005
133025
  }
133006
133026
  for (const stack of stacks) {
@@ -133029,16 +133049,16 @@ var deployApp = async (app, opt) => {
133029
133049
  throw ResourceError.wrap(meta.urn, meta.type, "resolve", error3);
133030
133050
  }
133031
133051
  if (isDataSource(node)) {
133032
- const meta2 = getMeta(node);
133052
+ const meta$1 = getMeta(node);
133033
133053
  if (!nodeState) {
133034
- const dataSourceState = await getDataSource(meta2, input, opt);
133035
- nodeState = stackState.nodes[meta2.urn] = {
133054
+ const dataSourceState = await getDataSource(meta$1, input, opt);
133055
+ nodeState = stackState.nodes[meta$1.urn] = {
133036
133056
  ...dataSourceState,
133037
133057
  drifted: undefined,
133038
133058
  ...partialNewResourceState
133039
133059
  };
133040
133060
  } else if (!compareState(nodeState.input, input) || nodeState.drifted) {
133041
- const dataSourceState = await getDataSource(meta2, input, opt);
133061
+ const dataSourceState = await getDataSource(meta$1, input, opt);
133042
133062
  Object.assign(nodeState, {
133043
133063
  ...dataSourceState,
133044
133064
  drifted: undefined,
@@ -133048,78 +133068,87 @@ var deployApp = async (app, opt) => {
133048
133068
  Object.assign(nodeState, partialNewResourceState);
133049
133069
  }
133050
133070
  if (isResource(node)) {
133051
- const meta2 = getMeta(node);
133052
- if (!nodeState)
133053
- if (meta2.config?.import) {
133071
+ const meta$1 = getMeta(node);
133072
+ if (!nodeState) {
133073
+ if (meta$1.config?.import) {
133054
133074
  const importedState = await importResource(node, input, opt);
133055
- const newResourceState = await updateResource(node, appState.idempotentToken, importedState.input, importedState.output, input, opt);
133056
- nodeState = stackState.nodes[meta2.urn] = {
133057
- ...importedState,
133058
- ...newResourceState,
133059
- ...partialNewResourceState
133060
- };
133061
- } else {
133075
+ if (importedState) {
133076
+ const newResourceState = await updateResource(node, appState.idempotentToken, importedState.input, importedState.output, input, opt);
133077
+ nodeState = stackState.nodes[meta$1.urn] = {
133078
+ ...importedState,
133079
+ ...newResourceState,
133080
+ ...partialNewResourceState
133081
+ };
133082
+ }
133083
+ }
133084
+ if (!nodeState) {
133062
133085
  const newResourceState = await createResource(node, appState.idempotentToken, input, opt);
133063
- nodeState = stackState.nodes[meta2.urn] = {
133086
+ nodeState = stackState.nodes[meta$1.urn] = {
133064
133087
  ...newResourceState,
133065
133088
  ...partialNewResourceState
133066
133089
  };
133067
133090
  }
133068
- else {
133091
+ } else {
133069
133092
  const inputChanged = !compareState(nodeState.input, input);
133070
133093
  const hasDrift = !!nodeState.drifted;
133071
133094
  if (!inputChanged && !hasDrift)
133072
133095
  Object.assign(nodeState, partialNewResourceState);
133073
133096
  else {
133074
133097
  let newResourceState;
133075
- const ignoreReplace = forcedUpdateDependents.has(meta2.urn);
133076
- if (!ignoreReplace && requiresReplacement(nodeState.input, input, meta2.config?.replaceOnChanges ?? []))
133077
- if (meta2.config?.createBeforeReplace) {
133078
- meta2.resolve(input);
133098
+ const ignoreReplace = forcedUpdateDependents.has(meta$1.urn);
133099
+ if (!ignoreReplace && requiresReplacement(nodeState.input, input, meta$1.config?.replaceOnChanges ?? []))
133100
+ if (meta$1.config?.createBeforeReplace) {
133101
+ meta$1.resolve(input);
133079
133102
  try {
133080
133103
  for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
133081
133104
  if (!isResource(dependentNode))
133082
133105
  continue;
133083
133106
  const dependentMeta = getMeta(dependentNode);
133084
- if (!dependentMeta.dependencies.has(meta2.urn))
133107
+ if (!dependentMeta.dependencies.has(meta$1.urn))
133085
133108
  continue;
133086
133109
  const dependentStackState = stackStates.get(dependentMeta.stack.urn);
133087
133110
  const dependentState = dependentStackState?.nodes[dependentUrn];
133088
133111
  if (!dependentStackState || !dependentState)
133089
133112
  continue;
133090
- const dependencyPaths = findDependencyPaths(dependentMeta.input, meta2.urn);
133113
+ const dependencyPaths = findDependencyPaths(dependentMeta.input, meta$1.urn);
133091
133114
  if (dependencyPaths.length === 0)
133092
133115
  continue;
133093
133116
  const dependentProvider = findProvider(opt.providers, dependentMeta.provider);
133094
133117
  if (dependentProvider.planResourceChange) {
133095
133118
  const dependentProposedInput = await resolveInputs(dependentMeta.input, (path) => getAtPath(dependentState.input, path));
133096
- if ((await dependentProvider.planResourceChange({
133097
- type: dependentMeta.type,
133098
- priorState: dependentState.output,
133099
- proposedState: dependentProposedInput
133100
- })).requiresReplacement) {
133119
+ let dependentRequiresReplacement;
133120
+ try {
133121
+ dependentRequiresReplacement = !!(await dependentProvider.planResourceChange({
133122
+ type: dependentMeta.type,
133123
+ priorState: dependentState.output,
133124
+ proposedState: dependentProposedInput
133125
+ })).requiresReplacement;
133126
+ } catch {
133127
+ dependentRequiresReplacement = true;
133128
+ }
133129
+ if (dependentRequiresReplacement) {
133101
133130
  if (!allowsDependentReplace(dependentMeta.config?.replaceOnChanges, dependencyPaths))
133102
- throw ResourceError.wrap(dependentMeta.urn, dependentMeta.type, "update", /* @__PURE__ */ new Error(`Replacing ${meta2.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`));
133131
+ throw ResourceError.wrap(dependentMeta.urn, dependentMeta.type, "update", /* @__PURE__ */ new Error(`Replacing ${meta$1.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`));
133103
133132
  }
133104
133133
  }
133105
133134
  }
133106
133135
  } finally {
133107
- meta2.resolve(nodeState.output);
133136
+ meta$1.resolve(nodeState.output);
133108
133137
  }
133109
133138
  const priorState = { ...nodeState };
133110
133139
  newResourceState = await createResource(node, appState.idempotentToken, input, opt);
133111
133140
  if (newResourceState.output)
133112
- meta2.resolve(newResourceState.output);
133113
- if (!meta2.config?.retainOnDelete) {
133141
+ meta$1.resolve(newResourceState.output);
133142
+ if (!meta$1.config?.retainOnDelete) {
133114
133143
  appState.pendingDeletes ??= {};
133115
- appState.pendingDeletes[meta2.urn] = priorState;
133144
+ appState.pendingDeletes[meta$1.urn] = priorState;
133116
133145
  }
133117
133146
  } else {
133118
133147
  for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
133119
133148
  if (!isResource(dependentNode))
133120
133149
  continue;
133121
133150
  const dependentMeta = getMeta(dependentNode);
133122
- if (!dependentMeta.dependencies.has(meta2.urn))
133151
+ if (!dependentMeta.dependencies.has(meta$1.urn))
133123
133152
  continue;
133124
133153
  if (plannedDependents.has(dependentUrn))
133125
133154
  continue;
@@ -133127,10 +133156,10 @@ var deployApp = async (app, opt) => {
133127
133156
  const dependentState = dependentStackState?.nodes[dependentUrn];
133128
133157
  if (!dependentStackState || !dependentState)
133129
133158
  continue;
133130
- const dependencyPaths = findDependencyPaths(dependentMeta.input, meta2.urn);
133159
+ const dependencyPaths = findDependencyPaths(dependentMeta.input, meta$1.urn);
133131
133160
  if (dependencyPaths.length === 0)
133132
133161
  continue;
133133
- const detachedInput = stripDependencyInputs(dependentState.input, dependentMeta.input, meta2.urn);
133162
+ const detachedInput = stripDependencyInputs(dependentState.input, dependentMeta.input, meta$1.urn);
133134
133163
  if (compareState(dependentState.input, detachedInput))
133135
133164
  continue;
133136
133165
  plannedDependents.add(dependentUrn);
@@ -133148,7 +133177,7 @@ var deployApp = async (app, opt) => {
133148
133177
  }
133149
133178
  if (dependentRequiresReplacement) {
133150
133179
  if (!allowsDependentReplace(dependentMeta.config?.replaceOnChanges, dependencyPaths))
133151
- throw ResourceError.wrap(dependentMeta.urn, dependentMeta.type, "update", /* @__PURE__ */ new Error(`Replacing ${meta2.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`));
133180
+ throw ResourceError.wrap(dependentMeta.urn, dependentMeta.type, "update", /* @__PURE__ */ new Error(`Replacing ${meta$1.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`));
133152
133181
  await deleteResource(appState.idempotentToken, dependentUrn, dependentState, opt);
133153
133182
  delete dependentStackState.nodes[dependentUrn];
133154
133183
  } else {
@@ -133162,12 +133191,12 @@ var deployApp = async (app, opt) => {
133162
133191
  }
133163
133192
  newResourceState = await replaceResource(node, appState.idempotentToken, nodeState.input, nodeState.output, input, opt);
133164
133193
  if (newResourceState.output)
133165
- meta2.resolve(newResourceState.output);
133194
+ meta$1.resolve(newResourceState.output);
133166
133195
  }
133167
133196
  else {
133168
133197
  newResourceState = await updateResource(node, appState.idempotentToken, nodeState.input, nodeState.output, input, opt);
133169
133198
  if (ignoreReplace)
133170
- forcedUpdateDependents.delete(meta2.urn);
133199
+ forcedUpdateDependents.delete(meta$1.urn);
133171
133200
  }
133172
133201
  Object.assign(nodeState, {
133173
133202
  input,
@@ -133416,7 +133445,6 @@ var status = async (app, opt) => {
133416
133445
  return stacks;
133417
133446
  };
133418
133447
  var WorkSpace = class {
133419
- props;
133420
133448
  constructor(props) {
133421
133449
  this.props = props;
133422
133450
  }
@@ -133511,7 +133539,6 @@ var isConditionalCheckFailed = (error3) => {
133511
133539
  return error3 instanceof Error && error3.name === "ConditionalCheckFailedException";
133512
133540
  };
133513
133541
  var DynamoLockBackend = class {
133514
- props;
133515
133542
  client;
133516
133543
  constructor(props) {
133517
133544
  this.props = props;
@@ -133595,7 +133622,6 @@ var DynamoLockBackend = class {
133595
133622
  }
133596
133623
  };
133597
133624
  var S3StateBackend = class {
133598
- props;
133599
133625
  client;
133600
133626
  constructor(props) {
133601
133627
  this.props = props;
@@ -133633,16 +133659,16 @@ var S3StateBackend = class {
133633
133659
  }
133634
133660
  };
133635
133661
  var file = (path, encoding = "utf8") => {
133636
- return new Future(async (resolve2, reject) => {
133662
+ return new Future(async (resolve$1, reject) => {
133637
133663
  try {
133638
- resolve2(await readFile4(path, { encoding }));
133664
+ resolve$1(await readFile4(path, { encoding }));
133639
133665
  } catch (error3) {
133640
133666
  reject(error3);
133641
133667
  }
133642
133668
  });
133643
133669
  };
133644
133670
  var hash = (path, algo = "sha256") => {
133645
- return file(path).pipe((file2) => createHash4(algo).update(file2).digest("hex"));
133671
+ return file(path).pipe((file$1) => createHash4(algo).update(file$1).digest("hex"));
133646
133672
  };
133647
133673
  globalThis.$resolve = resolve;
133648
133674
  globalThis.$combine = combine;
@@ -133653,7 +133679,7 @@ var createCustomResourceClass = (providerId, resourceType) => {
133653
133679
  return new Proxy(class {
133654
133680
  }, { construct(_3, [parent, id, input, config]) {
133655
133681
  const meta = createMeta("resource", `custom:${providerId}`, parent, resourceType, id, input, config);
133656
- const node = new Proxy({}, { get(_4, key) {
133682
+ const node = new Proxy({}, { get(_$1, key) {
133657
133683
  if (key === nodeMetaSymbol)
133658
133684
  return meta;
133659
133685
  if (key === "urn")
@@ -133668,68 +133694,67 @@ var createCustomResourceClass = (providerId, resourceType) => {
133668
133694
  };
133669
133695
  var createCustomProvider = (providerId, resourceProviders) => {
133670
133696
  const version2 = 1;
133671
- const hasRefreshResource = Object.values(resourceProviders).some((provider2) => !!provider2.refreshResource);
133697
+ const hasRefreshResource = Object.values(resourceProviders).some((provider$1) => !!provider$1.refreshResource);
133672
133698
  const getProvider = (type) => {
133673
- const provider2 = resourceProviders[type];
133674
- if (!provider2)
133699
+ const provider$1 = resourceProviders[type];
133700
+ if (!provider$1)
133675
133701
  throw new Error(`The "${providerId}" provider doesn't support the "${type}" resource type.`);
133676
- return provider2;
133702
+ return provider$1;
133677
133703
  };
133678
133704
  const provider = {
133679
133705
  ownResource(id) {
133680
133706
  return id === `custom:${providerId}`;
133681
133707
  },
133682
133708
  async getResource({ type, ...props }) {
133683
- const provider2 = getProvider(type);
133684
- if (!provider2.getResource)
133709
+ const provider$1 = getProvider(type);
133710
+ if (!provider$1.getResource)
133685
133711
  return {
133686
133712
  version: version2,
133687
133713
  state: props.state
133688
133714
  };
133689
133715
  return {
133690
133716
  version: version2,
133691
- state: await provider2.getResource(props)
133717
+ state: await provider$1.getResource(props)
133692
133718
  };
133693
133719
  },
133694
133720
  async createResource({ type, ...props }) {
133695
- const provider2 = getProvider(type);
133696
- if (!provider2.createResource)
133721
+ const provider$1 = getProvider(type);
133722
+ if (!provider$1.createResource)
133697
133723
  return {
133698
133724
  version: version2,
133699
133725
  state: props.state
133700
133726
  };
133701
133727
  return {
133702
133728
  version: version2,
133703
- state: await provider2.createResource(props)
133729
+ state: await provider$1.createResource(props)
133704
133730
  };
133705
133731
  },
133706
133732
  async updateResource({ type, ...props }) {
133707
- const provider2 = getProvider(type);
133708
- if (!provider2.updateResource)
133733
+ const provider$1 = getProvider(type);
133734
+ if (!provider$1.updateResource)
133709
133735
  return {
133710
133736
  version: version2,
133711
133737
  state: props.proposedState
133712
133738
  };
133713
133739
  return {
133714
133740
  version: version2,
133715
- state: await provider2.updateResource(props)
133741
+ state: await provider$1.updateResource(props)
133716
133742
  };
133717
133743
  },
133718
133744
  async deleteResource({ type, ...props }) {
133719
133745
  await getProvider(type).deleteResource?.(props);
133720
133746
  },
133721
133747
  async planResourceChange({ type, ...props }) {
133722
- const provider2 = getProvider(type);
133723
- if (!provider2.planResourceChange)
133748
+ const provider$1 = getProvider(type);
133749
+ if (!provider$1.planResourceChange)
133724
133750
  return {
133725
133751
  version: version2,
133726
133752
  state: props.proposedState,
133727
133753
  requiresReplacement: false
133728
133754
  };
133729
- const result = await provider2.planResourceChange(props);
133730
133755
  return {
133731
133756
  version: version2,
133732
- ...result
133757
+ ...await provider$1.planResourceChange(props)
133733
133758
  };
133734
133759
  },
133735
133760
  async getData({ type, ...props }) {
@@ -141091,7 +141116,7 @@ if (!nativeAccelerationDisabled) {
141091
141116
  } catch (error3) {}
141092
141117
  }
141093
141118
 
141094
- // ../../node_modules/.pnpm/@terraforge+terraform@0.0.35_@terraforge+core@0.0.43_aws-crt@1.30.0_debug@4.4.3_support_5cbec650153056d375307c2d151398c3/node_modules/@terraforge/terraform/dist/index.mjs
141119
+ // ../../node_modules/.pnpm/@terraforge+terraform@0.0.35_@terraforge+core@0.0.44_aws-crt@1.30.0_debug@4.4.3_support_4501f5c353e25a801cf638e931a8dd2d/node_modules/@terraforge/terraform/dist/index.mjs
141095
141120
  var import_grpc_js = __toESM(require_src4(), 1);
141096
141121
  var import_proto_loader = __toESM(require_src3(), 1);
141097
141122
  var import_jszip = __toESM(require_lib3(), 1);
@@ -143602,7 +143627,7 @@ var createTerraformProxy = (props) => {
143602
143627
  });
143603
143628
  };
143604
143629
 
143605
- // ../../node_modules/.pnpm/@terraforge+aws@6.53.0_@terraforge+core@0.0.43_aws-crt@1.30.0_debug@4.4.3_supports-colo_8ae6ada9f1b5b5808a0d42265f99939f/node_modules/@terraforge/aws/dist/index.js
143630
+ // ../../node_modules/.pnpm/@terraforge+aws@6.53.0_@terraforge+core@0.0.44_aws-crt@1.30.0_debug@4.4.3_supports-colo_7835ee9c943a9b106f7a0c962824b2e8/node_modules/@terraforge/aws/dist/index.js
143606
143631
  var aws2 = createTerraformProxy({
143607
143632
  namespace: "aws",
143608
143633
  provider: { org: "hashicorp", type: "aws", version: "6.53.0" }
@@ -154031,6 +154056,7 @@ var __export3 = (target2, all5) => {
154031
154056
  var RedisServer = class {
154032
154057
  client;
154033
154058
  process;
154059
+ stopping = false;
154034
154060
  async start(port, version2 = "7.2.4", args = []) {
154035
154061
  if (this.process) {
154036
154062
  throw new Error(`Redis server is already listening on port: ${await this.process.getPort()}`);
@@ -154038,6 +154064,7 @@ var RedisServer = class {
154038
154064
  if (port && (port < 0 || port >= 65536)) {
154039
154065
  throw new RangeError(`Port should be >= 0 and < 65536. Received ${port}.`);
154040
154066
  }
154067
+ this.stopping = false;
154041
154068
  this.process = await import_redis_memory_server.RedisMemoryServer.create({
154042
154069
  instance: {
154043
154070
  port,
@@ -154046,8 +154073,30 @@ var RedisServer = class {
154046
154073
  binary: { version: version2 }
154047
154074
  });
154048
154075
  }
154076
+ onExit(handler) {
154077
+ const child = this.process?.instanceInfoSync?.childProcess;
154078
+ child?.once("exit", (code, signal) => {
154079
+ if (!this.stopping) {
154080
+ handler(code, signal);
154081
+ }
154082
+ });
154083
+ }
154084
+ onOutput(handler) {
154085
+ const child = this.process?.instanceInfoSync?.childProcess;
154086
+ const capture = (chunk3) => {
154087
+ for (const line of chunk3.toString().split(`
154088
+ `)) {
154089
+ if (line.trim() !== "") {
154090
+ handler(line);
154091
+ }
154092
+ }
154093
+ };
154094
+ child?.stdout?.on("data", capture);
154095
+ child?.stderr?.on("data", capture);
154096
+ }
154049
154097
  async kill() {
154050
154098
  if (this.process) {
154099
+ this.stopping = true;
154051
154100
  await this.client?.disconnect();
154052
154101
  await this.process.stop();
154053
154102
  this.process = undefined;
@@ -154829,12 +154878,31 @@ var cacheOnDev = async (ctx) => {
154829
154878
  return;
154830
154879
  }
154831
154880
  for (const { stackName, id } of caches) {
154832
- const port = await ctx.keep(`cache:${stackName}:${id}`, null, async () => {
154881
+ const { port, sink: sink3 } = await ctx.keep(`cache:${stackName}:${id}`, null, async () => {
154833
154882
  const server = new RedisServer;
154834
154883
  await server.start();
154835
154884
  await server.ping();
154836
- return { value: await server.getPort(), stop: () => server.kill() };
154885
+ const sink4 = { tail: [] };
154886
+ server.onExit((code, signal) => {
154887
+ sink4.crashed = code !== null ? `exited with code ${code}` : `killed by ${signal}`;
154888
+ sink4.health?.("down", sink4.crashed);
154889
+ });
154890
+ server.onOutput((line) => {
154891
+ sink4.tail.push(line);
154892
+ while (sink4.tail.length > 20) {
154893
+ sink4.tail.shift();
154894
+ }
154895
+ sink4.log?.(line);
154896
+ });
154897
+ return { value: { port: await server.getPort(), sink: sink4 }, stop: () => server.kill() };
154837
154898
  });
154899
+ const channel = `cache:${stackName}:${id}`;
154900
+ sink3.health = (status2, detail) => ctx.reportHealth(`cache ${stackName}/${id}`, status2, detail);
154901
+ sink3.health(sink3.crashed ? "down" : "up", sink3.crashed);
154902
+ sink3.log = (line) => ctx.emitEvent(channel, { date: Date.now(), line });
154903
+ for (const line of sink3.tail) {
154904
+ ctx.emitEvent(channel, { date: Date.now(), line });
154905
+ }
154838
154906
  const prefix = `CACHE_${constantCase(stackName)}_${constantCase(id)}`;
154839
154907
  ctx.addEnv(`${prefix}_HOST`, "localhost");
154840
154908
  ctx.addEnv(`${prefix}_PORT`, String(port));
@@ -154844,7 +154912,8 @@ var cacheOnDev = async (ctx) => {
154844
154912
  kind: "cache",
154845
154913
  stack: stackName,
154846
154914
  id,
154847
- detail: `localhost:${port}`
154915
+ detail: `localhost:${port}`,
154916
+ channel
154848
154917
  });
154849
154918
  }
154850
154919
  };
@@ -157276,13 +157345,18 @@ var parseQueryPublish = (body) => {
157276
157345
  MessageAttributes: attributes
157277
157346
  };
157278
157347
  };
157279
- var createSnsServer = () => {
157348
+ var createSnsServer = (props) => {
157280
157349
  let server;
157281
157350
  let closeServer;
157282
157351
  let dispatch;
157283
157352
  let reportFailure;
157284
157353
  const publish2 = (input) => {
157285
157354
  const messageId = randomUUID4();
157355
+ for (const capture of props?.captures ?? []) {
157356
+ if (capture(input)) {
157357
+ return messageId;
157358
+ }
157359
+ }
157286
157360
  const attributes = {};
157287
157361
  for (const [name, attribute] of Object.entries(input.MessageAttributes ?? {})) {
157288
157362
  attributes[name] = {
@@ -157390,22 +157464,29 @@ var pubsubOnDev = async (ctx) => {
157390
157464
  declared.add(event);
157391
157465
  }
157392
157466
  }
157393
- const { redisPort, sns, snsPort, wsPort } = await ctx.keep(`pubsub-core:${id}`, null, async () => {
157467
+ const { redisPort, sns, snsPort, wsPort, redisSink } = await ctx.keep(`pubsub-core:${id}`, null, async () => {
157394
157468
  const redis = new RedisServer;
157395
157469
  await redis.start();
157396
157470
  await redis.ping();
157471
+ const redisSink2 = {};
157472
+ redis.onExit((code, signal) => {
157473
+ redisSink2.crashed = code !== null ? `exited with code ${code}` : `killed by ${signal}`;
157474
+ redisSink2.health?.("down", redisSink2.crashed);
157475
+ });
157397
157476
  const redisPort2 = await redis.getPort();
157398
157477
  const sns2 = createSnsServer();
157399
157478
  const snsPort2 = await sns2.listen();
157400
157479
  const wsPort2 = await findFreePort();
157401
157480
  return {
157402
- value: { redisPort: redisPort2, sns: sns2, snsPort: snsPort2, wsPort: wsPort2 },
157481
+ value: { redisPort: redisPort2, sns: sns2, snsPort: snsPort2, wsPort: wsPort2, redisSink: redisSink2 },
157403
157482
  stop: async () => {
157404
157483
  await sns2.stop();
157405
157484
  await redis.kill();
157406
157485
  }
157407
157486
  };
157408
157487
  });
157488
+ redisSink.health = (status2, detail) => ctx.reportHealth(`pubsub ${id} redis`, status2, detail);
157489
+ redisSink.health(redisSink.crashed ? "down" : "up", redisSink.crashed);
157409
157490
  ctx.retain(`pubsub-ws:${id}`);
157410
157491
  const channel = `pubsub:${id}`;
157411
157492
  const publisherRoute = formatRouteKey("base", "pubsub", `${id}-publisher`);
@@ -157487,7 +157568,10 @@ var pubsubOnDev = async (ctx) => {
157487
157568
  }
157488
157569
  return dispatch(event);
157489
157570
  }, reportFailure);
157490
- await ctx.keep(`pubsub-ws:${id}`, { wsPort, redisPort, snsPort }, async () => {
157571
+ const ws = await ctx.keep(`pubsub-ws:${id}`, { wsPort, redisPort, snsPort }, async () => {
157572
+ const sink3 = {
157573
+ stopping: false
157574
+ };
157491
157575
  const runtime = join24(dirname12(fileURLToPath8(import.meta.url)), "handlers", "pubsub-server.js");
157492
157576
  const child = spawn2(process.execPath, [runtime], {
157493
157577
  stdio: ["ignore", "ignore", "pipe"],
@@ -157512,9 +157596,23 @@ var pubsubOnDev = async (ctx) => {
157512
157596
  }
157513
157597
  });
157514
157598
  child.stderr?.on("data", (chunk3) => process.stderr.write(chunk3));
157599
+ child.on("exit", (code, signal) => {
157600
+ if (!sink3.stopping) {
157601
+ sink3.health?.("down", code !== null ? `exited with code ${code}` : `killed by ${signal}`);
157602
+ }
157603
+ });
157515
157604
  await waitForHealth(wsPort, 30000);
157516
- return { value: child, stop: () => stopChild(child) };
157605
+ return {
157606
+ value: { child, sink: sink3 },
157607
+ stop: async () => {
157608
+ sink3.stopping = true;
157609
+ await stopChild(child);
157610
+ }
157611
+ };
157517
157612
  });
157613
+ ws.sink.health = (status2, detail) => ctx.reportHealth(`pubsub ${id}`, status2, detail);
157614
+ ws.sink.stopping = false;
157615
+ ws.sink.health(ws.child.exitCode === null ? "up" : "down");
157518
157616
  },
157519
157617
  async stop() {
157520
157618
  feed?.disconnect();
@@ -158732,19 +158830,37 @@ var searchOnDev = async (ctx) => {
158732
158830
  if (indexes.length === 0) {
158733
158831
  return;
158734
158832
  }
158735
- const { port } = await ctx.keep("opensearch", null, async () => {
158833
+ const { port, sink: sink3 } = await ctx.keep("opensearch", null, async () => {
158736
158834
  const port2 = await findFreePort();
158737
158835
  const path6 = await download(VERSION_3_5_0_MIN);
158836
+ const sink4 = { tail: [] };
158738
158837
  const kill = await launch({
158739
158838
  path: path6,
158740
158839
  port: port2,
158741
158840
  host: "localhost",
158742
158841
  version: VERSION_3_5_0_MIN,
158743
- debug: false
158842
+ debug: false,
158843
+ onExit(code, signal) {
158844
+ sink4.crashed = code !== null ? `exited with code ${code}` : `killed by ${signal}`;
158845
+ sink4.health?.("down", sink4.crashed);
158846
+ },
158847
+ onOutput(line) {
158848
+ sink4.tail.push(line);
158849
+ while (sink4.tail.length > 20) {
158850
+ sink4.tail.shift();
158851
+ }
158852
+ sink4.log?.(line);
158853
+ }
158744
158854
  });
158745
158855
  await waitForSearch(port2, 60000);
158746
- return { value: { port: port2 }, stop: kill };
158856
+ return { value: { port: port2, sink: sink4 }, stop: kill };
158747
158857
  });
158858
+ sink3.health = (status2, detail) => ctx.reportHealth("search", status2, detail);
158859
+ sink3.health(sink3.crashed ? "down" : "up", sink3.crashed);
158860
+ sink3.log = (line) => ctx.emitEvent("search", { date: Date.now(), line });
158861
+ for (const line of sink3.tail) {
158862
+ ctx.emitEvent("search", { date: Date.now(), line });
158863
+ }
158748
158864
  ctx.addEnv("SEARCH_DOMAIN", `localhost:${port}`);
158749
158865
  const client = new Client5({ node: `http://localhost:${port}` });
158750
158866
  for (const { stackName, id, props } of indexes) {
@@ -158757,7 +158873,8 @@ var searchOnDev = async (ctx) => {
158757
158873
  kind: "search",
158758
158874
  stack: stackName,
158759
158875
  id,
158760
- detail: `localhost:${port}`
158876
+ detail: `localhost:${port}`,
158877
+ channel: "search"
158761
158878
  });
158762
158879
  }
158763
158880
  };
@@ -158988,7 +159105,7 @@ var siteOnDev = async (ctx) => {
158988
159105
  const value = await ctx.keep(key, { command: command3, port, env: env4 }, async () => {
158989
159106
  const [bin, ...args] = command3.split(" ");
158990
159107
  const cwd2 = join33(directories.root, dirname16(stackConfig.file));
158991
- const sink2 = {
159108
+ const sink3 = {
158992
159109
  emit: () => {},
158993
159110
  stopping: false
158994
159111
  };
@@ -159006,7 +159123,7 @@ var siteOnDev = async (ctx) => {
159006
159123
  continue;
159007
159124
  }
159008
159125
  tail.push(line);
159009
- sink2.emit({ date: Date.now(), line });
159126
+ sink3.emit({ date: Date.now(), line });
159010
159127
  }
159011
159128
  while (tail.length > 20) {
159012
159129
  tail.shift();
@@ -159014,24 +159131,30 @@ var siteOnDev = async (ctx) => {
159014
159131
  };
159015
159132
  child.stdout?.on("data", capture);
159016
159133
  child.stderr?.on("data", capture);
159017
- child.on("exit", (code) => {
159018
- if (!sink2.stopping && code !== null && code !== 0) {
159134
+ child.on("exit", (code, signal) => {
159135
+ if (sink3.stopping) {
159136
+ return;
159137
+ }
159138
+ if (code !== null && code !== 0) {
159019
159139
  logs_exports.error(`The site "${id}" dev server exited with code ${code}:
159020
159140
  ${tail.join(`
159021
159141
  `)}`);
159022
- sink2.emit({ date: Date.now(), line: `Exited with code ${code}` });
159142
+ sink3.emit({ date: Date.now(), line: `Exited with code ${code}` });
159023
159143
  }
159144
+ sink3.health?.("down", code !== null ? `exited with code ${code}` : `killed by ${signal}`);
159024
159145
  });
159025
159146
  return {
159026
- value: { port, tail, sink: sink2, child },
159147
+ value: { port, tail, sink: sink3, child },
159027
159148
  stop: async () => {
159028
- sink2.stopping = true;
159149
+ sink3.stopping = true;
159029
159150
  await stopChild(child);
159030
159151
  }
159031
159152
  };
159032
159153
  });
159033
159154
  value.sink.emit = (data) => ctx.emitEvent(channel, data);
159155
+ value.sink.health = (status2, detail) => ctx.reportHealth(`site ${id}`, status2, detail);
159034
159156
  value.sink.stopping = false;
159157
+ value.sink.health(value.child.exitCode === null ? "up" : "down");
159035
159158
  for (const line of value.tail) {
159036
159159
  ctx.emitEvent(channel, { date: Date.now(), line });
159037
159160
  }
@@ -160147,18 +160270,7 @@ var topicFeature = defineFeature({
160147
160270
  });
160148
160271
  }
160149
160272
  }
160150
- const { server, port } = await ctx.keep("shim:sns", null, async () => {
160151
- const server2 = createSnsServer();
160152
- const port2 = await server2.listen();
160153
- return { value: { server: server2, port: port2 }, stop: () => server2.stop() };
160154
- });
160155
- ctx.addEnv("AWS_ENDPOINT_URL_SNS", `http://127.0.0.1:${port}`);
160156
- ctx.registerServer({
160157
- name: "sns",
160158
- start({ dispatch, reportFailure }) {
160159
- server.connect(dispatch, reportFailure);
160160
- }
160161
- });
160273
+ await ctx.useSns();
160162
160274
  },
160163
160275
  async onTypeGen(ctx) {
160164
160276
  const gen = new TypeFile("awsless");
@@ -160356,6 +160468,7 @@ var vpcFeature = defineFeature({
160356
160468
  });
160357
160469
 
160358
160470
  // src/feature/alert/index.ts
160471
+ import { randomUUID as randomUUID7 } from "crypto";
160359
160472
  var typeGenCode10 = `
160360
160473
  import type { PublishOptions } from '@awsless/sns'
160361
160474
  import type { Mock } from 'vitest'
@@ -160374,6 +160487,44 @@ type TestMockEntry = MockBuilder & Mock<(payload: unknown) => unknown>
160374
160487
  `;
160375
160488
  var alertFeature = defineFeature({
160376
160489
  name: "alert",
160490
+ async onDev(ctx) {
160491
+ const ids = Object.keys(ctx.appConfig.alerts ?? {});
160492
+ if (ids.length === 0) {
160493
+ return;
160494
+ }
160495
+ const { captures, alerts } = await ctx.useSns();
160496
+ const names = new Map(ids.map((id) => [
160497
+ formatGlobalResourceName({
160498
+ appName: ctx.appConfig.name,
160499
+ resourceType: "alert",
160500
+ resourceName: id
160501
+ }),
160502
+ id
160503
+ ]));
160504
+ captures.push((input) => {
160505
+ const id = names.get(input.TopicArn?.split(":").at(-1) ?? "");
160506
+ if (!id) {
160507
+ return false;
160508
+ }
160509
+ alerts.unshift({
160510
+ id: randomUUID7(),
160511
+ date: Date.now(),
160512
+ alert: id,
160513
+ subject: input.Subject,
160514
+ message: input.Message
160515
+ });
160516
+ ctx.emitEvent("problems", {
160517
+ date: Date.now(),
160518
+ kind: "alert",
160519
+ title: input.Subject ?? id,
160520
+ detail: id
160521
+ });
160522
+ return true;
160523
+ });
160524
+ for (const [name, id] of names) {
160525
+ ctx.registerResource({ kind: "alert", id, detail: name });
160526
+ }
160527
+ },
160377
160528
  async onTypeGen(ctx) {
160378
160529
  const gen = new TypeFile("awsless");
160379
160530
  const resources = new TypeObject(1);
@@ -161431,6 +161582,7 @@ var instanceOnDev = async (ctx) => {
161431
161582
  async start({ env: env4 }) {
161432
161583
  const tail = [];
161433
161584
  boot = () => {
161585
+ ctx.reportHealth(`instance ${id}`, "up");
161434
161586
  child = spawn5("bun", [file2], {
161435
161587
  cwd: cwd2,
161436
161588
  stdio: ["ignore", "pipe", "pipe"],
@@ -161459,13 +161611,17 @@ var instanceOnDev = async (ctx) => {
161459
161611
  };
161460
161612
  child.stdout?.on("data", capture);
161461
161613
  child.stderr?.on("data", capture);
161462
- child.on("exit", (code) => {
161463
- if (!stopping && code !== null && code !== 0) {
161614
+ child.on("exit", (code, signal) => {
161615
+ if (stopping) {
161616
+ return;
161617
+ }
161618
+ if (code !== null && code !== 0) {
161464
161619
  logs_exports.error(`The instance "${id}" exited with code ${code}:
161465
161620
  ${tail.join(`
161466
161621
  `)}`);
161467
161622
  ctx.emitEvent(channel, { date: Date.now(), line: `Exited with code ${code}`, error: true });
161468
161623
  }
161624
+ ctx.reportHealth(`instance ${id}`, "down", code !== null ? `exited with code ${code}` : `killed by ${signal}`);
161469
161625
  });
161470
161626
  };
161471
161627
  boot();
@@ -166533,7 +166689,7 @@ var createS3Server = (props) => {
166533
166689
  };
166534
166690
 
166535
166691
  // src/dev/servers/sqs.ts
166536
- import { createHash as createHash23, randomUUID as randomUUID7 } from "crypto";
166692
+ import { createHash as createHash23, randomUUID as randomUUID8 } from "crypto";
166537
166693
  import { createServer as createServer8 } from "http";
166538
166694
  var md5 = (value) => createHash23("md5").update(value).digest("hex");
166539
166695
  var md5OfAttributes = (attributes) => {
@@ -166608,7 +166764,7 @@ var createSqsServer = (props) => {
166608
166764
  Records: [
166609
166765
  {
166610
166766
  messageId,
166611
- receiptHandle: randomUUID7(),
166767
+ receiptHandle: randomUUID8(),
166612
166768
  body: input.MessageBody,
166613
166769
  attributes: {
166614
166770
  ApproximateReceiveCount: "1",
@@ -166676,7 +166832,7 @@ var createSqsServer = (props) => {
166676
166832
  if (!queue) {
166677
166833
  throw new Error(`Unknown local queue: ${input.QueueUrl}`);
166678
166834
  }
166679
- const messageId = randomUUID7();
166835
+ const messageId = randomUUID8();
166680
166836
  deliver(queue, input, messageId);
166681
166837
  return sendResult(input, messageId);
166682
166838
  },
@@ -166687,7 +166843,7 @@ var createSqsServer = (props) => {
166687
166843
  }
166688
166844
  return {
166689
166845
  Successful: (input.Entries ?? []).map((entry) => {
166690
- const messageId = randomUUID7();
166846
+ const messageId = randomUUID8();
166691
166847
  deliver(queue, entry, messageId);
166692
166848
  return {
166693
166849
  Id: entry.Id,
@@ -166711,7 +166867,7 @@ var createSqsServer = (props) => {
166711
166867
  const batch2 = visible.slice(0, Math.max(1, input.MaxNumberOfMessages ?? 1));
166712
166868
  return {
166713
166869
  Messages: batch2.map((message4) => {
166714
- message4.receipt = randomUUID7();
166870
+ message4.receipt = randomUUID8();
166715
166871
  message4.receiveCount += 1;
166716
166872
  message4.firstReceivedAt ??= now;
166717
166873
  message4.visibleAt = now + (input.VisibilityTimeout ?? 30) * 1000;
@@ -166868,6 +167024,12 @@ var createDevContext = (props) => {
166868
167024
  const servers = [];
166869
167025
  const restartPaths = [];
166870
167026
  const resources = [];
167027
+ const health = new Map;
167028
+ const reportHealth = (id, status2, detail) => {
167029
+ const entry = { id, status: status2, detail, date: Date.now() };
167030
+ health.set(id, entry);
167031
+ events.emit("health", entry);
167032
+ };
166871
167033
  const listeners2 = new Map;
166872
167034
  const replays = new Map;
166873
167035
  const events = {
@@ -166965,12 +167127,37 @@ var createDevContext = (props) => {
166965
167127
  })();
166966
167128
  return sqs;
166967
167129
  };
167130
+ let sns;
167131
+ const useSns = () => {
167132
+ sns ??= (async () => {
167133
+ const value = await props.pool.keep("shim:sns", null, async () => {
167134
+ const captures = [];
167135
+ const server = createSnsServer({ captures });
167136
+ const port = await server.listen();
167137
+ return {
167138
+ value: { server, port, captures, alerts: [] },
167139
+ stop: () => server.stop()
167140
+ };
167141
+ });
167142
+ value.captures.splice(0, value.captures.length);
167143
+ env4["AWS_ENDPOINT_URL_SNS"] = `http://127.0.0.1:${value.port}`;
167144
+ servers.push({
167145
+ name: "sns",
167146
+ start({ dispatch, reportFailure }) {
167147
+ value.server.connect(dispatch, reportFailure);
167148
+ }
167149
+ });
167150
+ return { port: value.port, captures: value.captures, alerts: value.alerts };
167151
+ })();
167152
+ return sns;
167153
+ };
166968
167154
  return {
166969
167155
  env: env4,
166970
167156
  routes,
166971
167157
  servers,
166972
167158
  restartPaths,
166973
167159
  resources,
167160
+ health,
166974
167161
  events,
166975
167162
  context: {
166976
167163
  appConfig: props.appConfig,
@@ -166989,6 +167176,7 @@ var createDevContext = (props) => {
166989
167176
  useDynamo,
166990
167177
  useStore,
166991
167178
  useSqs,
167179
+ useSns,
166992
167180
  addEnv(name, value) {
166993
167181
  if (name in env4 && env4[name] !== value) {
166994
167182
  throw new Error(`The env var "${name}" is defined multiple times with different values, while all bundled functions share the same env.`);
@@ -167008,7 +167196,8 @@ var createDevContext = (props) => {
167008
167196
  resources.push(resource);
167009
167197
  },
167010
167198
  log: props.log,
167011
- emitEvent: events.emit
167199
+ emitEvent: events.emit,
167200
+ reportHealth
167012
167201
  }
167013
167202
  };
167014
167203
  };
@@ -167065,7 +167254,7 @@ ${logs}`));
167065
167254
  var import_ioredis5 = __toESM(require_built3(), 1);
167066
167255
  import { DynamoDBClient as DynamoDBClient5 } from "@aws-sdk/client-dynamodb";
167067
167256
  import { DynamoDBDocumentClient, ScanCommand as ScanCommand2 } from "@aws-sdk/lib-dynamodb";
167068
- import { randomUUID as randomUUID8 } from "crypto";
167257
+ import { randomUUID as randomUUID9 } from "crypto";
167069
167258
  import { readdir as readdir9, readFile as readFile22, stat as stat8, writeFile as writeFile14 } from "fs/promises";
167070
167259
  import { createServer as createServer9 } from "http";
167071
167260
  import { join as join55, relative as relative11, sep as sep5 } from "path";
@@ -167274,6 +167463,7 @@ var createBundleWorker = (props) => {
167274
167463
  return run;
167275
167464
  };
167276
167465
  return {
167466
+ size: () => workers.length,
167277
167467
  start: () => serialize(doStart),
167278
167468
  stop: () => serialize(doStop),
167279
167469
  restart: () => serialize(async () => {
@@ -167317,7 +167507,7 @@ var dashboardHtml = `<!doctype html>
167317
167507
  --border: #262b36;
167318
167508
  --text: #d7dae0;
167319
167509
  --muted: #7d8590;
167320
- --accent: #f5a623;
167510
+ --accent: #ff9000;
167321
167511
  --good: #3fb950;
167322
167512
  --bad: #f85149;
167323
167513
  font-size: 14px;
@@ -167379,18 +167569,34 @@ var dashboardHtml = `<!doctype html>
167379
167569
  .logs .entry { padding: 4px 0; }
167380
167570
  .logs .entry + .entry, .logs .line + .entry, .logs .entry + .line { border-top: 1px solid var(--border); }
167381
167571
  .logs .entry .meta { display: flex; gap: 8px; margin-bottom: 2px; }
167382
- .config-form { display: flex; flex-direction: column; gap: 8px; max-width: 520px; }
167383
- .config-form .field { display: grid; grid-template-columns: 180px 1fr; gap: 12px; align-items: center; }
167384
- .config-form .name { color: var(--muted); overflow-wrap: anywhere; }
167385
- .config-form input {
167572
+ /* Every group is its own card, with a shared grid inside: the name
167573
+ column grows to the longest name (capped), so nothing truncates &
167574
+ the inputs line up. */
167575
+ .config-form { display: flex; flex-direction: column; gap: 14px; max-width: 720px; margin-top: 14px; }
167576
+ .config-group {
167386
167577
  background: var(--panel);
167578
+ border: 1px solid var(--border);
167579
+ border-radius: 8px;
167580
+ padding: 14px 16px;
167581
+ }
167582
+ .config-group h3 { margin: 0 0 12px; }
167583
+ .config-fields {
167584
+ display: grid;
167585
+ grid-template-columns: fit-content(340px) 1fr;
167586
+ gap: 10px 16px;
167587
+ align-items: center;
167588
+ }
167589
+ .config-fields .field { display: contents; }
167590
+ .config-fields .name { color: var(--muted); overflow-wrap: anywhere; }
167591
+ .config-fields input {
167592
+ background: var(--bg);
167387
167593
  color: var(--text);
167388
167594
  border: 1px solid var(--border);
167389
167595
  border-radius: 6px;
167390
167596
  padding: 6px 10px;
167391
167597
  font: inherit;
167392
167598
  }
167393
- .config-form input::placeholder { color: var(--muted); }
167599
+ .config-fields input::placeholder { color: var(--muted); }
167394
167600
  .groups { display: flex; flex-wrap: wrap; gap: 4px 16px; }
167395
167601
  .groups .group { display: flex; align-items: center; gap: 4px; cursor: pointer; }
167396
167602
  nav {
@@ -167401,10 +167607,13 @@ var dashboardHtml = `<!doctype html>
167401
167607
  nav h1 {
167402
167608
  font-size: 15px;
167403
167609
  margin: 4px 8px 16px;
167404
- color: var(--accent);
167405
167610
  cursor: pointer;
167406
167611
  }
167407
- nav h1 span { color: var(--muted); font-weight: normal; }
167612
+ /* The same two-tone logo as the cli: bold AWS in the brand orange,
167613
+ LESS dimmed, the command muted. */
167614
+ nav h1 .logo { color: var(--accent); font-weight: bold; }
167615
+ nav h1 .logo .dim { color: #a35d00; }
167616
+ nav h1 .cmd { color: var(--muted); font-weight: normal; }
167408
167617
  nav h3 {
167409
167618
  font-size: 10px;
167410
167619
  text-transform: uppercase;
@@ -167497,6 +167706,24 @@ var dashboardHtml = `<!doctype html>
167497
167706
  cursor: pointer;
167498
167707
  }
167499
167708
  .row:hover { background: var(--panel); }
167709
+ a.row { text-decoration: none; }
167710
+ .health { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 4px; }
167711
+ .health .chip {
167712
+ display: inline-flex;
167713
+ align-items: center;
167714
+ gap: 7px;
167715
+ background: var(--panel);
167716
+ border: 1px solid var(--border);
167717
+ border-radius: 999px;
167718
+ padding: 4px 12px;
167719
+ font-size: 12px;
167720
+ }
167721
+ .health .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--good); flex-shrink: 0; }
167722
+ .health .chip.down { border-color: var(--bad); }
167723
+ .health .chip.down .dot { background: var(--bad); }
167724
+ .health .chip .detail-text { color: var(--muted); }
167725
+ .row.problem .id { color: var(--bad); }
167726
+ .empty.good { color: var(--good); }
167500
167727
  .row .stack {
167501
167728
  color: var(--muted);
167502
167729
  min-width: 90px;
@@ -167628,9 +167855,10 @@ const GROUPS = [
167628
167855
  ['store', 'Stores'],
167629
167856
  ['config', 'Config'],
167630
167857
  ['auth', 'Auth'],
167858
+ ['alert', 'Alerts'],
167631
167859
  ['email', 'Emails'],
167632
167860
  ['worker', 'Logs'],
167633
- ['route', 'Routes'],
167861
+ ['route', 'Routers'],
167634
167862
  ]
167635
167863
 
167636
167864
  // The nav renders the features in scannable sections - a section only
@@ -167640,7 +167868,7 @@ const SECTIONS = [
167640
167868
  ['Compute', ['function', 'cron', 'task', 'instance']],
167641
167869
  ['Messaging', ['queue', 'topic', 'subscriber', 'pubsub']],
167642
167870
  ['Storage', ['table', 'cache', 'search', 'store']],
167643
- ['App', ['config', 'auth', 'email', 'worker']],
167871
+ ['App', ['config', 'auth', 'alert', 'email', 'worker']],
167644
167872
  ]
167645
167873
 
167646
167874
  const svg = inner =>
@@ -167663,6 +167891,7 @@ const ICONS = {
167663
167891
  cache: svg('<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>'),
167664
167892
  search: svg('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'),
167665
167893
  store: svg('<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>'),
167894
+ alert: svg('<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>'),
167666
167895
  email: svg('<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>'),
167667
167896
  config: svg('<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>'),
167668
167897
  route: svg('<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>'),
@@ -167681,7 +167910,7 @@ const icon = kind => {
167681
167910
  let state
167682
167911
  let cleanupPanel
167683
167912
  const view = { kind: null, resource: null }
167684
- const filter = { query: '', stack: '' }
167913
+ const filter = { query: '', stack: '', router: '' }
167685
167914
 
167686
167915
  const label = r => (r.stack ? r.stack + ' / ' + r.id : r.id)
167687
167916
  const groupTitle = kind => GROUPS.find(([k]) => k === kind)?.[1] ?? kind
@@ -167697,6 +167926,7 @@ const filterParams = () => {
167697
167926
  const params = new URLSearchParams()
167698
167927
  if (filter.query) params.set('q', filter.query)
167699
167928
  if (filter.stack) params.set('stack', filter.stack)
167929
+ if (filter.router) params.set('router', filter.router)
167700
167930
  return params.toString()
167701
167931
  }
167702
167932
 
@@ -167717,6 +167947,7 @@ const selectKind = kind => {
167717
167947
  view.resource = null
167718
167948
  filter.query = ''
167719
167949
  filter.stack = ''
167950
+ filter.router = ''
167720
167951
  history.pushState(null, '', listUrl(kind))
167721
167952
  render()
167722
167953
  }
@@ -167742,6 +167973,7 @@ const applyLocation = () => {
167742
167973
  view.resource = null
167743
167974
  filter.query = params.get('q') ?? ''
167744
167975
  filter.stack = params.get('stack') ?? ''
167976
+ filter.router = params.get('router') ?? ''
167745
167977
 
167746
167978
  if (view.kind && segments.length >= 3) {
167747
167979
  const router = params.get('router')
@@ -168128,6 +168360,11 @@ const cachePanel = async (main, r) => {
168128
168360
  const holder = $('div', {}, $('p', { className: 'empty' }, 'Scanning...'))
168129
168361
  main.append(holder)
168130
168362
 
168363
+ // The redis server's own output, streamed live.
168364
+ if (r.channel) {
168365
+ cleanupPanel = attachLogFeed(main, r.channel, undefined, 'Server logs')
168366
+ }
168367
+
168131
168368
  try {
168132
168369
  const data = await api('/api/cache?target=' + encodeURIComponent(r.detail))
168133
168370
  holder.innerHTML = ''
@@ -168208,6 +168445,11 @@ const searchPanel = async (main, r) => {
168208
168445
  result,
168209
168446
  )
168210
168447
 
168448
+ // The opensearch server's own output, streamed live.
168449
+ if (r.channel) {
168450
+ cleanupPanel = attachLogFeed(main, r.channel, undefined, 'Server logs')
168451
+ }
168452
+
168211
168453
  try {
168212
168454
  const { data } = await proxy('/_cat/indices?format=json&h=index,health,docs.count,store.size&s=index')
168213
168455
  const rows = data.filter(entry => !entry.index.startsWith('.'))
@@ -168262,6 +168504,59 @@ const storePanel = async (main, r) => {
168262
168504
 
168263
168505
  // Every email sent through Email.send during this session - captured
168264
168506
  // by the local ses shim instead of being delivered.
168507
+ const alertPanel = async (main) => {
168508
+ const holder = $('div', {})
168509
+ main.append(holder)
168510
+
168511
+ const render = async () => {
168512
+ const data = await api('/api/alerts')
168513
+ const alerts = data.alerts ?? []
168514
+ holder.innerHTML = ''
168515
+
168516
+ const refresh = $('button', { className: 'primary', textContent: 'Refresh' })
168517
+ refresh.onclick = render
168518
+ holder.append($('div', { className: 'actions' }, [refresh]))
168519
+
168520
+ if (alerts.length === 0) {
168521
+ holder.append($('p', { className: 'empty' }, 'No alerts sent yet.'))
168522
+ return
168523
+ }
168524
+
168525
+ const rows = $('div', { className: 'rows' })
168526
+
168527
+ for (const alert of alerts) {
168528
+ const row = $('button', { className: 'row' }, [
168529
+ $('span', { className: 'stack' }, new Date(alert.date).toLocaleTimeString()),
168530
+ $('span', { className: 'id' }, alert.subject ?? '(no subject)'),
168531
+ $('span', { className: 'info' }, alert.alert),
168532
+ ])
168533
+ row.onclick = () => {
168534
+ holder.innerHTML = ''
168535
+
168536
+ const back = $('button', { className: 'back', textContent: '\u2190 Alerts' })
168537
+ back.onclick = render
168538
+
168539
+ let message = alert.message ?? '(no payload)'
168540
+ try {
168541
+ message = JSON.stringify(JSON.parse(alert.message), null, 2)
168542
+ } catch (_) {}
168543
+
168544
+ holder.append(
168545
+ back,
168546
+ $('h2', {}, alert.subject ?? '(no subject)'),
168547
+ $('p', { className: 'detail' }, alert.alert + ' \xB7 ' + new Date(alert.date).toLocaleString()),
168548
+ $('pre', { className: 'result' }, message),
168549
+ )
168550
+ }
168551
+ rows.append(row)
168552
+ }
168553
+
168554
+ holder.append(rows)
168555
+ }
168556
+
168557
+ render()
168558
+ }
168559
+
168265
168560
  const emailPanel = async (main) => {
168266
168561
  const holder = $('div', {})
168267
168562
  main.append(holder)
@@ -168324,7 +168619,34 @@ const configPanel = async (main) => {
168324
168619
  const inputs = new Map()
168325
168620
  const form = $('div', { className: 'config-form' })
168326
168621
 
168622
+ // Configs group by the first segment of their name: alphapo-api-key
168623
+ // & alphapo-host read as one "alphapo" block with the prefix
168624
+ // stripped from the rows. A prefix needs at least two members to
168625
+ // form a group - the loners keep their full name under "other".
168626
+ const byPrefix = new Map()
168627
+
168327
168628
  for (const name of names) {
168629
+ const prefix = name.split('-')[0]
168630
+ if (!byPrefix.has(prefix)) byPrefix.set(prefix, [])
168631
+ byPrefix.get(prefix).push(name)
168632
+ }
168633
+
168634
+ const sections = []
168635
+ const other = []
168636
+
168637
+ for (const [prefix, members] of byPrefix) {
168638
+ if (members.length > 1) {
168639
+ sections.push([prefix, members])
168640
+ } else {
168641
+ other.push(members[0])
168642
+ }
168643
+ }
168644
+
168645
+ if (other.length > 0) {
168646
+ sections.push(['other', other])
168647
+ }
168648
+
168649
+ const addField = (grid, name) => {
168328
168650
  // An empty input falls through to the value pulled from ssm on
168329
168651
  // boot - the placeholder shows whether the pull provided one.
168330
168652
  const input = $('input', {
@@ -168333,7 +168655,21 @@ const configPanel = async (main) => {
168333
168655
  spellcheck: false,
168334
168656
  })
168335
168657
  inputs.set(name, input)
168336
- form.append($('label', { className: 'field' }, [$('span', { className: 'name' }, name), input]))
168658
+ grid.append($('label', { className: 'field' }, [$('span', { className: 'name' }, name), input]))
168659
+ }
168660
+
168661
+ for (const [prefix, members] of sections) {
168662
+ const grid = $('div', { className: 'config-fields' })
168663
+
168664
+ for (const name of members) {
168665
+ addField(grid, name)
168666
+ }
168667
+
168668
+ // A lone unlabeled group skips its header - the card alone
168669
+ // carries the grouping.
168670
+ const header = sections.length > 1 || other.length === 0 ? [$('h3', {}, prefix)] : []
168671
+
168672
+ form.append($('div', { className: 'config-group' }, [...header, grid]))
168337
168673
  }
168338
168674
 
168339
168675
  if (names.length === 0) {
@@ -168502,7 +168838,9 @@ const authPanel = async (main, r) => {
168502
168838
 
168503
168839
  const renderResource = main => {
168504
168840
  const r = view.resource
168505
- const back = $('button', { className: 'back' }, '\u2190 ' + groupTitle(r.kind))
168841
+ // A route's back button lands on its router's route list, one level
168842
+ // below the Routers tab itself.
168843
+ const back = $('button', { className: 'back' }, '\u2190 ' + (r.kind === 'route' ? 'Routes' : groupTitle(r.kind)))
168506
168844
 
168507
168845
  back.onclick = () => {
168508
168846
  view.resource = null
@@ -168549,7 +168887,49 @@ const renderList = main => {
168549
168887
  const items = state.resources.filter(r => r.kind === view.kind)
168550
168888
  const stacks = [...new Set(items.map(r => r.stack).filter(Boolean))].sort()
168551
168889
 
168552
- main.append($('h2', {}, groupTitle(view.kind)))
168890
+ // The routes view drills down per router: every router runs as its
168891
+ // own local server (like its own domain in production), so a flat
168892
+ // list mixing every router's routes would only confuse.
168893
+ if (view.kind === 'route' && !filter.router) {
168894
+ main.append($('h2', {}, 'Routers'))
168895
+
168896
+ const rows = $('div', { className: 'rows' })
168897
+
168898
+ for (const [id, port] of Object.entries(state.routerPorts)) {
168899
+ const count = items.filter(r => r.router === id).length
168900
+ const row = $('button', { className: 'row' }, [
168901
+ $('span', { className: 'stack' }, id),
168902
+ $('span', { className: 'id' }, 'http://localhost:' + port),
168903
+ $('span', { className: 'info' }, count + (count === 1 ? ' route' : ' routes')),
168904
+ ])
168905
+ row.onclick = () => {
168906
+ filter.router = id
168907
+ history.pushState(null, '', listUrl('route'))
168908
+ render()
168909
+ }
168910
+ rows.append(row)
168911
+ }
168912
+
168913
+ main.append(rows)
168914
+ return
168915
+ }
168916
+
168917
+ if (view.kind === 'route') {
168918
+ const back = $('button', { className: 'back' }, '\u2190 Routers')
168919
+ back.onclick = () => {
168920
+ filter.router = ''
168921
+ history.pushState(null, '', listUrl('route'))
168922
+ render()
168923
+ }
168924
+
168925
+ main.append(
168926
+ back,
168927
+ $('h2', {}, 'Routes'),
168928
+ $('p', { className: 'detail' }, filter.router + ' \xB7 http://localhost:' + state.routerPorts[filter.router]),
168929
+ )
168930
+ } else {
168931
+ main.append($('h2', {}, groupTitle(view.kind)))
168932
+ }
168553
168933
 
168554
168934
  // The config page is a single form over every defined value,
168555
168935
  // instead of a resource list.
@@ -168562,15 +168942,27 @@ const renderList = main => {
168562
168942
  return emailPanel(main)
168563
168943
  }
168564
168944
 
168945
+ // The alert page lists every captured alert of the session.
168946
+ if (view.kind === 'alert') {
168947
+ return alertPanel(main)
168948
+ }
168949
+
168565
168950
  // The worker page streams the bundle worker output directly,
168566
168951
  // instead of listing its single resource.
168567
168952
  if (view.kind === 'worker') {
168568
- cleanupPanel = attachLogFeed(main, 'worker', undefined, null)
168953
+ const workerFeed = attachLogFeed(main, 'worker', undefined, 'Worker output')
168954
+ const debugFeed = attachLogFeed(main, 'debug', undefined, 'Dev server')
168955
+
168956
+ cleanupPanel = () => {
168957
+ workerFeed?.()
168958
+ debugFeed?.()
168959
+ }
168569
168960
  return
168570
168961
  }
168571
168962
 
168572
168963
  const matches = r => {
168573
168964
  if (filter.stack && r.stack !== filter.stack) return false
168965
+ if (filter.router && r.router !== filter.router) return false
168574
168966
  if (!filter.query) return true
168575
168967
 
168576
168968
  return (r.stack + ' ' + r.id + ' ' + (r.detail ?? ''))
@@ -168644,11 +169036,14 @@ const render = () => {
168644
169036
  cleanupPanel = undefined
168645
169037
 
168646
169038
  const nav = document.getElementById('nav')
168647
- nav.innerHTML = '<h1>awsless <span>dev</span></h1>'
169039
+ nav.innerHTML = '<h1><span class="logo">AWS<span class="dim">LESS</span></span> <span class="cmd">dev</span></h1>'
168648
169040
  nav.querySelector('h1').onclick = showHome
168649
169041
 
168650
169042
  const navButton = kind => {
168651
- const count = state.resources.filter(r => r.kind === kind).length
169043
+ // The Routers tab lists routers, so its count follows suit.
169044
+ const count = kind === 'route'
169045
+ ? Object.keys(state.routerPorts).length
169046
+ : state.resources.filter(r => r.kind === kind).length
168652
169047
  if (count === 0) return null
168653
169048
 
168654
169049
  // The email outbox is a single feed, so a count of registered
@@ -168656,7 +169051,7 @@ const render = () => {
168656
169051
  const button = $('button', { className: view.kind === kind ? 'active' : '' }, [
168657
169052
  icon(kind),
168658
169053
  groupTitle(kind),
168659
- kind === 'email' || kind === 'worker' ? '' : $('span', { className: 'count' }, String(count)),
169054
+ kind === 'email' || kind === 'worker' || kind === 'alert' ? '' : $('span', { className: 'count' }, String(count)),
168660
169055
  ])
168661
169056
  button.onclick = () => selectKind(kind)
168662
169057
  return button
@@ -168728,7 +169123,192 @@ const render = () => {
168728
169123
  } else if (view.kind) {
168729
169124
  renderList(main)
168730
169125
  } else {
168731
- main.append($('p', { className: 'empty' }, 'Select a feature to browse its resources.'))
169126
+ renderHome(main)
169127
+ }
169128
+ }
169129
+
169130
+ // ------------------------------------------------------------------
169131
+ // Homepage: the session at a glance - endpoints, problems & the live
169132
+ // activity feed of everything running through the bundle.
169133
+
169134
+ const timeAgo = ms => {
169135
+ const s = Math.max(0, Math.round(ms / 1000))
169136
+ if (s < 60) return s + 's'
169137
+ if (s < 3600) return Math.round(s / 60) + 'm'
169138
+ return Math.floor(s / 3600) + 'h ' + (Math.round(s / 60) % 60) + 'm'
169139
+ }
169140
+
169141
+ const renderHome = main => {
169142
+ const session = state.session ?? {}
169143
+
169144
+ const uptime = $('span', {}, '')
169145
+ const updateUptime = () => {
169146
+ uptime.textContent = session.startedAt ? 'up ' + timeAgo(Date.now() - session.startedAt) : ''
169147
+ }
169148
+ updateUptime()
169149
+ const uptimeTimer = setInterval(updateUptime, 1000)
169150
+
169151
+ main.append(
169152
+ $('h2', {}, state.app),
169153
+ $('p', { className: 'detail' }, [
169154
+ uptime,
169155
+ session.workers ? ' \\u00b7 ' + session.workers + ' workers' : '',
169156
+ ]),
169157
+ )
169158
+
169159
+ // ----------------------------------------------------------------
169160
+ // Health: one chip per moving part, live.
169161
+
169162
+ const chips = $('div', { className: 'health' })
169163
+ const chipEls = new Map()
169164
+
169165
+ const renderChip = entry => {
169166
+ let chip = chipEls.get(entry.id)
169167
+
169168
+ if (!chip) {
169169
+ chip = $('span', { className: 'chip' })
169170
+ chipEls.set(entry.id, chip)
169171
+ chips.append(chip)
169172
+ }
169173
+
169174
+ chip.className = 'chip' + (entry.status === 'down' ? ' down' : '')
169175
+ chip.innerHTML = ''
169176
+ chip.append(
169177
+ $('span', { className: 'dot' }),
169178
+ entry.id,
169179
+ entry.detail ? $('span', { className: 'detail-text' }, '\xB7 ' + entry.detail) : '',
169180
+ )
169181
+ }
169182
+
169183
+ for (const entry of state.health ?? []) renderChip(entry)
169184
+
169185
+ const healthSource = new EventSource('/api/events?channel=health')
169186
+ healthSource.onmessage = message => {
169187
+ try {
169188
+ renderChip(JSON.parse(message.data))
169189
+ } catch (_) {}
169190
+ }
169191
+
169192
+ if ((state.health ?? []).length > 0) {
169193
+ main.append(chips)
169194
+ }
169195
+
169196
+ // ----------------------------------------------------------------
169197
+ // Endpoints
169198
+
169199
+ main.append($('h3', {}, 'Endpoints'))
169200
+
169201
+ const endpoints = $('div', { className: 'rows' })
169202
+
169203
+ for (const [id, port] of Object.entries(state.routerPorts ?? {})) {
169204
+ const url = 'http://localhost:' + port
169205
+ const row = $('a', { className: 'row', href: url, target: '_blank' }, [
169206
+ $('span', { className: 'stack' }, 'router ' + id),
169207
+ $('span', { className: 'id' }, url),
169208
+ $('span', { className: 'info' }, ''),
169209
+ ])
169210
+ endpoints.append(row)
169211
+ }
169212
+
169213
+ main.append(endpoints)
169214
+
169215
+ // ----------------------------------------------------------------
169216
+ // Problems: failures, route errors & fired alerts, newest first.
169217
+
169218
+ main.append($('h3', {}, 'Problems'))
169219
+
169220
+ const problems = $('div', { className: 'rows' })
169221
+ const calm = $('p', { className: 'empty good' }, 'No problems - everything runs clean.')
169222
+ main.append(calm, problems)
169223
+
169224
+ const problemRows = []
169225
+
169226
+ const problemRow = data => {
169227
+ calm.hidden = true
169228
+
169229
+ const row = $('button', { className: 'row problem' }, [
169230
+ $('span', { className: 'stack' }, new Date(data.date).toLocaleTimeString() + ' \\u00b7 ' + data.kind),
169231
+ $('span', { className: 'id' }, data.title ?? ''),
169232
+ $('span', { className: 'info' }, (data.detail ?? '').slice(0, 120)),
169233
+ ])
169234
+
169235
+ row.onclick = () => {
169236
+ if (data.kind === 'alert') {
169237
+ selectKind('alert')
169238
+ return
169239
+ }
169240
+
169241
+ const resource = state.resources.find(r => r.routeKey === data.title)
169242
+ if (resource) selectResource(resource)
169243
+ }
169244
+
169245
+ problems.prepend(row)
169246
+ problemRows.push(row)
169247
+
169248
+ while (problemRows.length > 20) {
169249
+ problemRows.shift().remove()
169250
+ }
169251
+ }
169252
+
169253
+ const problemSource = new EventSource('/api/events?channel=problems')
169254
+ problemSource.onmessage = message => {
169255
+ try {
169256
+ problemRow(JSON.parse(message.data))
169257
+ } catch (_) {}
169258
+ }
169259
+
169260
+ // ----------------------------------------------------------------
169261
+ // Activity: every dispatch through the bundle, newest first.
169262
+
169263
+ main.append($('h3', {}, 'Activity'))
169264
+
169265
+ const activity = $('div', { className: 'rows' })
169266
+ const idle = $('p', { className: 'empty' }, 'Nothing ran yet.')
169267
+ main.append(idle, activity)
169268
+
169269
+ const activityRows = []
169270
+
169271
+ const activityRow = data => {
169272
+ idle.hidden = true
169273
+
169274
+ const resource = state.resources.find(r => r.routeKey === data.route)
169275
+ const row = $('button', { className: 'row' + (data.ok ? '' : ' problem') }, [
169276
+ $('span', { className: 'stack' }, new Date(data.date).toLocaleTimeString()),
169277
+ $('span', { className: 'id' }, (data.ok ? '' : '\\u2717 ') + data.route),
169278
+ $('span', { className: 'info' }, data.ok ? data.ms + 'ms' : (data.error ?? 'failed').slice(0, 80)),
169279
+ ])
169280
+
169281
+ if (resource) {
169282
+ row.onclick = () => selectResource(resource)
169283
+ }
169284
+
169285
+ activity.prepend(row)
169286
+ activityRows.push(row)
169287
+
169288
+ while (activityRows.length > 50) {
169289
+ activityRows.shift().remove()
169290
+ }
169291
+ }
169292
+
169293
+ const activitySource = new EventSource('/api/events?channel=activity')
169294
+ activitySource.onmessage = message => {
169295
+ try {
169296
+ activityRow(JSON.parse(message.data))
169297
+ } catch (_) {}
169298
+ }
169299
+
169300
+ // ----------------------------------------------------------------
169301
+ // Logs: the handlers' own console output, live - the full feed
169302
+ // (incl. the dev server stream) lives on the Logs tab.
169303
+
169304
+ const logsFeed = attachLogFeed(main, 'worker', undefined, 'Logs')
169305
+
169306
+ cleanupPanel = () => {
169307
+ clearInterval(uptimeTimer)
169308
+ healthSource.close()
169309
+ problemSource.close()
169310
+ activitySource.close()
169311
+ logsFeed?.()
168732
169312
  }
168733
169313
  }
168734
169314
 
@@ -168749,14 +169329,15 @@ try {
168749
169329
  document.title = state.app + ' \xB7 awsless dev'
168750
169330
  state.resources = [
168751
169331
  ...state.resources,
168752
- // Proxy routes carry no route key & are already listed by
168753
- // the feature that owns them.
168754
- ...state.routes.filter(route => route.routeKey).map(route => ({
169332
+ // The full route table per router, like the deployed router: both
169333
+ // bundle routes & proxy mounts (site dev servers, the pubsub
169334
+ // websocket). Only the raw per-file static asset routes stay out.
169335
+ ...state.routes.filter(route => !route.rawKey).map(route => ({
168755
169336
  kind: 'route',
168756
169337
  id: route.pattern,
168757
- stack: route.routeKey.split(':')[0],
169338
+ stack: route.routeKey ? route.routeKey.split(':')[0] : undefined,
168758
169339
  router: route.routerId,
168759
- detail: route.routeKey,
169340
+ detail: route.routeKey ?? ('proxy \u2192 ' + route.proxy),
168760
169341
  url: 'http://localhost:' + state.routerPorts[route.routerId] + route.pattern.split('{')[0].replace(/\\*$/, ''),
168761
169342
  })),
168762
169343
  ]
@@ -168816,7 +169397,9 @@ var createDashboardServer = (props) => {
168816
169397
  routerPorts: props.routerPorts,
168817
169398
  resources: props.resources,
168818
169399
  routes: props.routes,
168819
- seeds: Boolean(props.runSeeds)
169400
+ seeds: Boolean(props.runSeeds),
169401
+ session: props.getSession?.() ?? null,
169402
+ health: props.getHealth?.() ?? []
168820
169403
  }).replaceAll("<", "\\u003c");
168821
169404
  return { status: 200, body: dashboardHtml.replace("__STATE__", () => state), type: "text/html" };
168822
169405
  }
@@ -168834,7 +169417,7 @@ var createDashboardServer = (props) => {
168834
169417
  EventVersion: "1.0",
168835
169418
  Sns: {
168836
169419
  Type: "Notification",
168837
- MessageId: randomUUID8(),
169420
+ MessageId: randomUUID9(),
168838
169421
  TopicArn: `arn:aws:sns:${props.region}:000000000000:${topic}`,
168839
169422
  Message: typeof message4 === "string" ? message4 : JSON.stringify(message4 ?? {}),
168840
169423
  Timestamp: new Date().toISOString(),
@@ -168985,6 +169568,9 @@ var createDashboardServer = (props) => {
168985
169568
  if (url.pathname === "/api/emails") {
168986
169569
  return { status: 200, body: JSON.stringify({ emails: props.getEmails?.() ?? [] }) };
168987
169570
  }
169571
+ if (url.pathname === "/api/alerts") {
169572
+ return { status: 200, body: JSON.stringify({ alerts: props.getAlerts?.() ?? [] }) };
169573
+ }
168988
169574
  if (url.pathname.startsWith("/api/auth") && props.auth) {
168989
169575
  const auth2 = props.auth;
168990
169576
  try {
@@ -169098,13 +169684,13 @@ var createDashboardServer = (props) => {
169098
169684
  // src/dev/failure.ts
169099
169685
  import { stringify as stringify5 } from "@awsless/json";
169100
169686
  import { formatRouteKey as formatRouteKey2, formatRoutePayload as formatRoutePayload2 } from "awsless";
169101
- import { randomUUID as randomUUID9 } from "crypto";
169687
+ import { randomUUID as randomUUID10 } from "crypto";
169102
169688
  var CONSUMER_ROUTE = formatRouteKey2("base", "on-failure", "consumer");
169103
169689
  var createFailureReporter = (props) => {
169104
169690
  const format4 = (report) => {
169105
169691
  const error3 = report.error;
169106
169692
  const base = {
169107
- id: randomUUID9(),
169693
+ id: randomUUID10(),
169108
169694
  date: new Date,
169109
169695
  payload: report.event
169110
169696
  };
@@ -169144,6 +169730,7 @@ var createFailureReporter = (props) => {
169144
169730
  return (report) => {
169145
169731
  const message4 = report.error instanceof Error ? report.error.message : String(report.error);
169146
169732
  props.log(`${report.routeKey ?? report.queue?.name ?? "async invoke"} failed: ${message4}`);
169733
+ props.onReport?.(report);
169147
169734
  if (!props.enabled) {
169148
169735
  return;
169149
169736
  }
@@ -169336,6 +169923,7 @@ var startDev = async (props) => {
169336
169923
  const log = props.onLog ?? (() => {});
169337
169924
  const phase = props.phase ?? (async ({ start }, fn) => (log(start), fn(() => {})));
169338
169925
  const { appConfig, stackConfigs } = props;
169926
+ const startedAt = Date.now();
169339
169927
  const { builders, appId, ready: ready2 } = createApp({
169340
169928
  appConfig,
169341
169929
  stackConfigs,
@@ -169352,6 +169940,9 @@ var startDev = async (props) => {
169352
169940
  props.pool.begin();
169353
169941
  await props.pool.keep("session", null, async () => ({ value: true, stop: () => {} }));
169354
169942
  const dev = createDevContext({ appConfig, stackConfigs, appId, routerPorts, log, pool: props.pool });
169943
+ setDebugSink((type2, message4) => {
169944
+ dev.events.emit("debug", { date: Date.now(), line: message4, error: type2 === "error" });
169945
+ });
169355
169946
  const bundleName = getBundleFunctionName(appConfig.name);
169356
169947
  const buildDir = getBuildPath("bundle", bundleName, ".");
169357
169948
  const { env: env4, lambda } = await phase({ start: "Preparing the local resources...", done: "Prepared the local resources" }, async (detail) => {
@@ -169463,12 +170054,14 @@ var startDev = async (props) => {
169463
170054
  restartNeeded = true;
169464
170055
  await worker.restart();
169465
170056
  restartNeeded = false;
170057
+ dev.context.reportHealth("workers", "up", String(worker.size()));
169466
170058
  log(`Reloaded the bundle in ${Date.now() - started}ms (build ${built - started}ms, worker ${Date.now() - built}ms)`);
169467
170059
  } else if (!changed) {
169468
170060
  debug(`Rebuild found no changes in ${Date.now() - started}ms`);
169469
170061
  }
169470
170062
  } catch (error3) {
169471
170063
  dirty = true;
170064
+ dev.context.reportHealth("workers", "down", error3 instanceof Error ? error3.message : String(error3));
169472
170065
  throw error3;
169473
170066
  } finally {
169474
170067
  fresh = undefined;
@@ -169476,14 +170069,62 @@ var startDev = async (props) => {
169476
170069
  })();
169477
170070
  await fresh;
169478
170071
  };
170072
+ const describeDispatch = (event) => {
170073
+ const payload = event;
170074
+ const route = payload?.["$awsless-route"];
170075
+ if (typeof route === "string") {
170076
+ return route;
170077
+ }
170078
+ const record = payload?.Records?.[0];
170079
+ if (record?.eventSource === "aws:sqs") {
170080
+ return "queue " + String(record.eventSourceARN ?? "").split(":").at(-1);
170081
+ }
170082
+ if (record?.EventSource === "aws:sns") {
170083
+ return "topic " + String(record.Sns?.TopicArn ?? "").split(":").at(-1);
170084
+ }
170085
+ if (record?.eventSource === "aws:s3") {
170086
+ return "store " + String(record.s3?.bucket?.name ?? "");
170087
+ }
170088
+ if (String(record?.eventSourceARN ?? "").includes(":dynamodb:")) {
170089
+ return "stream " + (String(record.eventSourceARN).split("table/")[1]?.split("/")[0] ?? "table");
170090
+ }
170091
+ const header = payload?.headers?.[ROUTE_HEADER];
170092
+ if (typeof header === "string") {
170093
+ return header;
170094
+ }
170095
+ return "invoke";
170096
+ };
169479
170097
  const dispatch = async (event) => {
169480
170098
  await ensureFresh();
169481
- return worker.dispatch(event);
170099
+ const started = Date.now();
170100
+ const route = describeDispatch(event);
170101
+ try {
170102
+ const result = await worker.dispatch(event);
170103
+ dev.events.emit("activity", { date: started, route, ms: Date.now() - started, ok: true });
170104
+ return result;
170105
+ } catch (error3) {
170106
+ dev.events.emit("activity", {
170107
+ date: started,
170108
+ route,
170109
+ ms: Date.now() - started,
170110
+ ok: false,
170111
+ error: error3 instanceof Error ? error3.message : String(error3)
170112
+ });
170113
+ throw error3;
170114
+ }
169482
170115
  };
169483
170116
  const reportFailure = createFailureReporter({
169484
170117
  enabled: Boolean(appConfig.onFailure),
169485
170118
  dispatch,
169486
- log
170119
+ log,
170120
+ onReport(report) {
170121
+ dev.events.emit("problems", {
170122
+ date: Date.now(),
170123
+ kind: report.kind,
170124
+ title: report.routeKey ?? report.queue?.name ?? "async invoke",
170125
+ detail: report.error instanceof Error ? report.error.message : String(report.error)
170126
+ });
170127
+ }
169487
170128
  });
169488
170129
  lambda.connect(dispatch, reportFailure);
169489
170130
  await phase({ start: "Starting the local servers...", done: "Started the local servers" }, async (detail) => {
@@ -169498,9 +170139,11 @@ var startDev = async (props) => {
169498
170139
  await phase({ start: "Starting the bundle worker...", done: "Started the bundle worker" }, async (detail) => {
169499
170140
  try {
169500
170141
  await worker.start();
170142
+ dev.context.reportHealth("workers", "up", String(worker.size()));
169501
170143
  } catch (error3) {
169502
170144
  detail("FAILED - the next invoke retries");
169503
170145
  log(`The bundle worker failed to start: ${error3 instanceof Error ? error3.message : String(error3)}`);
170146
+ dev.context.reportHealth("workers", "down", error3 instanceof Error ? error3.message : String(error3));
169504
170147
  dirty = true;
169505
170148
  restartNeeded = true;
169506
170149
  }
@@ -169535,6 +170178,12 @@ var startDev = async (props) => {
169535
170178
  process.stderr.write(`Route ${routeKey} failed: ${detail}
169536
170179
  `);
169537
170180
  emitWorkerLine(`Route ${routeKey} failed: ${detail}`, true, routeKey);
170181
+ dev.events.emit("problems", {
170182
+ date: Date.now(),
170183
+ kind: "route",
170184
+ title: routeKey,
170185
+ detail: error3 instanceof Error ? error3.message : String(error3)
170186
+ });
169538
170187
  }
169539
170188
  }));
169540
170189
  }
@@ -169553,6 +170202,9 @@ var startDev = async (props) => {
169553
170202
  storeRoot: join57(directories.output, "local", "store"),
169554
170203
  configFile: join57(directories.output, "local", "config.json"),
169555
170204
  getEmails: () => props.pool.peek("shim:ses-email")?.server.list() ?? [],
170205
+ getAlerts: () => props.pool.peek("shim:sns")?.alerts ?? [],
170206
+ getSession: () => ({ startedAt, workers: worker.size() }),
170207
+ getHealth: () => [...dev.health.values()],
169556
170208
  configPulled: Object.keys(props.pool.peek("config:pull") ?? {}),
169557
170209
  auth: createAuthAdmin({
169558
170210
  appConfig,
@@ -169582,9 +170234,13 @@ var startDev = async (props) => {
169582
170234
  if (stopping) {
169583
170235
  return;
169584
170236
  }
169585
- worker.restart().then(() => log("Restarted the bundle worker.")).catch((error3) => {
170237
+ worker.restart().then(() => {
170238
+ dev.context.reportHealth("workers", "up", String(worker.size()));
170239
+ log("Restarted the bundle worker.");
170240
+ }).catch((error3) => {
169586
170241
  restartNeeded = true;
169587
170242
  dirty = true;
170243
+ dev.context.reportHealth("workers", "down", error3 instanceof Error ? error3.message : String(error3));
169588
170244
  log(`The bundle worker failed to restart: ${error3 instanceof Error ? error3.message : String(error3)}`);
169589
170245
  });
169590
170246
  });
@@ -185637,7 +186293,7 @@ var formatLog = (level, date2, origin, message4) => {
185637
186293
  import { DeleteObjectsCommand, ListObjectsV2Command, S3Client as S3Client5 } from "@aws-sdk/client-s3";
185638
186294
 
185639
186295
  // src/formation/cloudfront.ts
185640
- import { randomUUID as randomUUID10 } from "crypto";
186296
+ import { randomUUID as randomUUID11 } from "crypto";
185641
186297
  var createInvalidationForDistributionTenants = async (client, props) => {
185642
186298
  let cursor;
185643
186299
  do {
@@ -185657,7 +186313,7 @@ var createInvalidationForDistributionTenants = async (client, props) => {
185657
186313
  Quantity: props.paths.length,
185658
186314
  Items: props.paths
185659
186315
  },
185660
- CallerReference: randomUUID10()
186316
+ CallerReference: randomUUID11()
185661
186317
  }
185662
186318
  }));
185663
186319
  }