@awsless/cli 0.1.7 → 0.1.9

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.
package/dist/bin.js CHANGED
@@ -6,60 +6,39 @@ var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- function __accessProp(key) {
10
- return this[key];
11
- }
12
- var __toESMCache_node;
13
- var __toESMCache_esm;
14
9
  var __toESM = (mod, isNodeMode, target) => {
15
- var canCache = mod != null && typeof mod === "object";
16
- if (canCache) {
17
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
18
- var cached = cache.get(mod);
19
- if (cached)
20
- return cached;
21
- }
22
10
  target = mod != null ? __create(__getProtoOf(mod)) : {};
23
11
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
24
12
  for (let key of __getOwnPropNames(mod))
25
13
  if (!__hasOwnProp.call(to, key))
26
14
  __defProp(to, key, {
27
- get: __accessProp.bind(mod, key),
15
+ get: () => mod[key],
28
16
  enumerable: true
29
17
  });
30
- if (canCache)
31
- cache.set(mod, to);
32
18
  return to;
33
19
  };
20
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
34
21
  var __toCommonJS = (from) => {
35
- var entry = (__moduleCache ??= new WeakMap).get(from), desc;
22
+ var entry = __moduleCache.get(from), desc;
36
23
  if (entry)
37
24
  return entry;
38
25
  entry = __defProp({}, "__esModule", { value: true });
39
- if (from && typeof from === "object" || typeof from === "function") {
40
- for (var key of __getOwnPropNames(from))
41
- if (!__hasOwnProp.call(entry, key))
42
- __defProp(entry, key, {
43
- get: __accessProp.bind(from, key),
44
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
45
- });
46
- }
26
+ if (from && typeof from === "object" || typeof from === "function")
27
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
28
+ get: () => from[key],
29
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
30
+ }));
47
31
  __moduleCache.set(from, entry);
48
32
  return entry;
49
33
  };
50
- var __moduleCache;
51
34
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
52
- var __returnValue = (v) => v;
53
- function __exportSetter(name, newValue) {
54
- this[name] = __returnValue.bind(null, newValue);
55
- }
56
35
  var __export = (target, all) => {
57
36
  for (var name in all)
58
37
  __defProp(target, name, {
59
38
  get: all[name],
60
39
  enumerable: true,
61
40
  configurable: true,
62
- set: __exportSetter.bind(all, name)
41
+ set: (newValue) => all[name] = () => newValue
63
42
  });
64
43
  };
65
44
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -853,7 +832,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
853
832
  this._exitCallback = (err) => {
854
833
  if (err.code !== "commander.executeSubCommandAsync") {
855
834
  throw err;
856
- }
835
+ } else {}
857
836
  };
858
837
  }
859
838
  return this;
@@ -3090,7 +3069,7 @@ var require_cell = __commonJS((exports, module) => {
3090
3069
  let content = utils.truncate(this.content, 10, this.truncate);
3091
3070
  if (!lineNum) {
3092
3071
  info(`${this.y}-${this.x}: ${this.rowSpan - lineNum}x${this.colSpan} Cell ${content}`);
3093
- }
3072
+ } else {}
3094
3073
  let padLen = Math.max(this.height - this.lines.length, 0);
3095
3074
  let padTop;
3096
3075
  switch (this.vAlign) {
@@ -26859,7 +26838,7 @@ var require_node_gyp_build_optional_packages = __commonJS((exports, module) => {
26859
26838
 
26860
26839
  // ../../node_modules/.pnpm/msgpackr-extract@3.0.4/node_modules/msgpackr-extract/index.js
26861
26840
  var require_msgpackr_extract = __commonJS((exports, module) => {
26862
- var __dirname = "/Users/work/Code/awsless/node_modules/.pnpm/msgpackr-extract@3.0.4/node_modules/msgpackr-extract";
26841
+ var __dirname = "/Users/ivandasilveira/Sites/@awsless/node_modules/.pnpm/msgpackr-extract@3.0.4/node_modules/msgpackr-extract";
26863
26842
  module.exports = require_node_gyp_build_optional_packages()(__dirname);
26864
26843
  });
26865
26844
 
@@ -39727,7 +39706,7 @@ var require_src3 = __commonJS((exports) => {
39727
39706
 
39728
39707
  // ../../node_modules/.pnpm/@grpc+grpc-js@1.12.6/node_modules/@grpc/grpc-js/build/src/channelz.js
39729
39708
  var require_channelz = __commonJS((exports) => {
39730
- var __dirname = "/Users/work/Code/awsless/node_modules/.pnpm/@grpc+grpc-js@1.12.6/node_modules/@grpc/grpc-js/build/src";
39709
+ var __dirname = "/Users/ivandasilveira/Sites/@awsless/node_modules/.pnpm/@grpc+grpc-js@1.12.6/node_modules/@grpc/grpc-js/build/src";
39731
39710
  Object.defineProperty(exports, "__esModule", { value: true });
39732
39711
  exports.registerChannelzSocket = exports.registerChannelzServer = exports.registerChannelzSubchannel = exports.registerChannelzChannel = exports.ChannelzCallTrackerStub = exports.ChannelzCallTracker = exports.ChannelzChildrenTrackerStub = exports.ChannelzChildrenTracker = exports.ChannelzTrace = exports.ChannelzTraceStub = undefined;
39733
39712
  exports.unregisterChannelzRef = unregisterChannelzRef;
@@ -57231,7 +57210,7 @@ var require_zipEntries = __commonJS((exports, module) => {
57231
57210
  if (this.centralDirRecords !== this.files.length) {
57232
57211
  if (this.centralDirRecords !== 0 && this.files.length === 0) {
57233
57212
  throw new Error("Corrupted zip or bug: expected " + this.centralDirRecords + " records in central dir, got " + this.files.length);
57234
- }
57213
+ } else {}
57235
57214
  }
57236
57215
  },
57237
57216
  readEndOfCentral: function() {
@@ -74452,7 +74431,7 @@ WD9f
74452
74431
 
74453
74432
  // ../../node_modules/.pnpm/ioredis@5.10.1_supports-color@8.1.1/node_modules/ioredis/built/utils/index.js
74454
74433
  var require_utils5 = __commonJS((exports) => {
74455
- var __dirname = "/Users/work/Code/awsless/node_modules/.pnpm/ioredis@5.10.1_supports-color@8.1.1/node_modules/ioredis/built/utils";
74434
+ var __dirname = "/Users/ivandasilveira/Sites/@awsless/node_modules/.pnpm/ioredis@5.10.1_supports-color@8.1.1/node_modules/ioredis/built/utils";
74456
74435
  Object.defineProperty(exports, "__esModule", { value: true });
74457
74436
  exports.noop = exports.defaults = exports.Debug = exports.getPackageMeta = exports.zipMap = exports.CONNECTION_CLOSED_ERROR_MSG = exports.shuffle = exports.sample = exports.resolveTLSProfile = exports.parseURL = exports.optimizeErrorStack = exports.toArg = exports.convertMapToArray = exports.convertObjectToArray = exports.timeout = exports.packObject = exports.isInt = exports.wrapMultiResult = exports.convertBufferToString = undefined;
74458
74437
  var fs_1 = __require("fs");
@@ -74738,7 +74717,7 @@ var require_argumentParsers = __commonJS((exports) => {
74738
74717
 
74739
74718
  // ../../node_modules/.pnpm/ioredis@5.10.1_supports-color@8.1.1/node_modules/ioredis/built/Command.js
74740
74719
  var require_Command = __commonJS((exports) => {
74741
- var __dirname = "/Users/work/Code/awsless/node_modules/.pnpm/ioredis@5.10.1_supports-color@8.1.1/node_modules/ioredis/built";
74720
+ var __dirname = "/Users/ivandasilveira/Sites/@awsless/node_modules/.pnpm/ioredis@5.10.1_supports-color@8.1.1/node_modules/ioredis/built";
74742
74721
  Object.defineProperty(exports, "__esModule", { value: true });
74743
74722
  var commands_1 = require_built();
74744
74723
  var calculateSlot = require_lib4();
@@ -95385,7 +95364,7 @@ var require_db_util = __commonJS((exports) => {
95385
95364
 
95386
95365
  // ../../node_modules/.pnpm/redis-memory-server@0.16.1_supports-color@8.1.1/node_modules/redis-memory-server/lib/util/RedisInstance.js
95387
95366
  var require_RedisInstance = __commonJS((exports) => {
95388
- var __dirname = "/Users/work/Code/awsless/node_modules/.pnpm/redis-memory-server@0.16.1_supports-color@8.1.1/node_modules/redis-memory-server/lib/util";
95367
+ var __dirname = "/Users/ivandasilveira/Sites/@awsless/node_modules/.pnpm/redis-memory-server@0.16.1_supports-color@8.1.1/node_modules/redis-memory-server/lib/util";
95389
95368
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P3, generator) {
95390
95369
  function adopt(value) {
95391
95370
  return value instanceof P3 ? value : new P3(function(resolve3) {
@@ -99153,15 +99132,15 @@ var require_pack = __commonJS((exports, module) => {
99153
99132
  process.nextTick(callback);
99154
99133
  return new Void;
99155
99134
  }
99156
- var sink2 = new Sink(this);
99157
- this._stream = sink2;
99158
- eos(sink2, function(err) {
99135
+ var sink3 = new Sink(this);
99136
+ this._stream = sink3;
99137
+ eos(sink3, function(err) {
99159
99138
  self2._stream = null;
99160
99139
  if (err) {
99161
99140
  self2.destroy();
99162
99141
  return callback(err);
99163
99142
  }
99164
- if (sink2.written !== header.size) {
99143
+ if (sink3.written !== header.size) {
99165
99144
  self2.destroy();
99166
99145
  return callback(new Error("size mismatch"));
99167
99146
  }
@@ -99170,7 +99149,7 @@ var require_pack = __commonJS((exports, module) => {
99170
99149
  self2.finalize();
99171
99150
  callback();
99172
99151
  });
99173
- return sink2;
99152
+ return sink3;
99174
99153
  };
99175
99154
  Pack.prototype.finalize = function() {
99176
99155
  if (this._stream) {
@@ -101299,7 +101278,7 @@ var require_unbzip2_stream = __commonJS((exports, module) => {
101299
101278
  stream2.queue(d5);
101300
101279
  if (d5 !== null) {
101301
101280
  outlength += d5.length;
101302
- }
101281
+ } else {}
101303
101282
  });
101304
101283
  } catch (e4) {
101305
101284
  stream2.emit("error", e4);
@@ -103168,7 +103147,15 @@ var import_decompress, import_sleep_await, mock, searchClient = (options = {}, s
103168
103147
  return Object.entries(settings).map(([key, value]) => {
103169
103148
  return ["-E", `${key}=${value}`];
103170
103149
  }).flat();
103171
- }, 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
+ }) => {
103172
103159
  return new Promise(async (resolve22, reject) => {
103173
103160
  const cache3 = join32(path6, "cache", String(port));
103174
103161
  const cleanUp = async () => {
@@ -103206,7 +103193,9 @@ ${output.join("")}`);
103206
103193
  done();
103207
103194
  }
103208
103195
  };
103196
+ let stopping = false;
103209
103197
  const kill = async () => {
103198
+ stopping = true;
103210
103199
  if (child.exitCode === null && !child.killed) {
103211
103200
  await new Promise((resolve32) => {
103212
103201
  child.once(`exit`, () => {
@@ -103235,6 +103224,23 @@ ${output.join("")}`);
103235
103224
  };
103236
103225
  const done = async () => {
103237
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
+ }
103238
103244
  resolve22(kill);
103239
103245
  };
103240
103246
  const fail = async (error3) => {
@@ -108467,6 +108473,10 @@ var clearDebugLog = () => {
108467
108473
  ready = true;
108468
108474
  } catch (_) {}
108469
108475
  };
108476
+ var sink;
108477
+ var setDebugSink = (listener) => {
108478
+ sink = listener;
108479
+ };
108470
108480
  var write = (type, message) => {
108471
108481
  try {
108472
108482
  if (!ready) {
@@ -108474,6 +108484,7 @@ var write = (type, message) => {
108474
108484
  }
108475
108485
  appendFileSync(debugLogFile, `${new Date().toISOString()} [${type}] ${message}
108476
108486
  `);
108487
+ sink?.(type, message);
108477
108488
  } catch (_) {}
108478
108489
  };
108479
108490
  var format = (parts) => {
@@ -114483,7 +114494,7 @@ var getSanitizedDevTypeScriptVersion = (version2 = "") => {
114483
114494
  };
114484
114495
 
114485
114496
  // ../../node_modules/.pnpm/@aws-sdk+util-user-agent-node@3.973.14_aws-crt@1.30.0_supports-color@8.1.1_/node_modules/@aws-sdk/util-user-agent-node/dist-es/getTypeScriptUserAgentPair.js
114486
- var __dirname = "/Users/work/Code/awsless/node_modules/.pnpm/@aws-sdk+util-user-agent-node@3.973.14_aws-crt@1.30.0_supports-color@8.1.1_/node_modules/@aws-sdk/util-user-agent-node/dist-es";
114497
+ var __dirname = "/Users/ivandasilveira/Sites/@awsless/node_modules/.pnpm/@aws-sdk+util-user-agent-node@3.973.14_aws-crt@1.30.0_supports-color@8.1.1_/node_modules/@aws-sdk/util-user-agent-node/dist-es";
114487
114498
  var tscVersion;
114488
114499
  var TS_PACKAGE_JSON = join7("node_modules", "typescript", "package.json");
114489
114500
  var getTypeScriptUserAgentPair = async () => {
@@ -131560,17 +131571,6 @@ var $forEachNodeInTopologicalOrder = sort.forEachNodeInTopologicalOrder;
131560
131571
  var $topologicalSort = sort.topologicalSort;
131561
131572
  var $topologicalGenerations = sort.topologicalGenerations;
131562
131573
  var $forEachTopologicalGeneration = sort.forEachTopologicalGeneration;
131563
- // ../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm/sha1.js
131564
- import { createHash as createHash3 } from "crypto";
131565
- function sha1(bytes) {
131566
- if (Array.isArray(bytes)) {
131567
- bytes = Buffer.from(bytes);
131568
- } else if (typeof bytes === "string") {
131569
- bytes = Buffer.from(bytes, "utf8");
131570
- }
131571
- return createHash3("sha1").update(bytes).digest();
131572
- }
131573
- var sha1_default = sha1;
131574
131574
 
131575
131575
  // ../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm/regex.js
131576
131576
  var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
@@ -131636,6 +131636,18 @@ function v35(version2, hash, value, namespace, buf, offset) {
131636
131636
  return unsafeStringify(bytes);
131637
131637
  }
131638
131638
 
131639
+ // ../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm/sha1.js
131640
+ import { createHash as createHash3 } from "crypto";
131641
+ function sha1(bytes) {
131642
+ if (Array.isArray(bytes)) {
131643
+ bytes = Buffer.from(bytes);
131644
+ } else if (typeof bytes === "string") {
131645
+ bytes = Buffer.from(bytes, "utf8");
131646
+ }
131647
+ return createHash3("sha1").update(bytes).digest();
131648
+ }
131649
+ var sha1_default = sha1;
131650
+
131639
131651
  // ../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm/v5.js
131640
131652
  function v5(value, namespace, buf, offset) {
131641
131653
  return v35(80, sha1_default, value, namespace, buf, offset);
@@ -132327,13 +132339,13 @@ var consoleSink = (group4, ...args) => {
132327
132339
  console.log(`${group4}:`, ...args);
132328
132340
  console.log();
132329
132341
  };
132330
- var sink;
132342
+ var sink2;
132331
132343
  var enableDebug = (customSink) => {
132332
- sink = customSink ?? consoleSink;
132344
+ sink2 = customSink ?? consoleSink;
132333
132345
  };
132334
132346
  var createDebugger = (group4) => {
132335
132347
  return (...args) => {
132336
- sink?.(group4, ...args);
132348
+ sink2?.(group4, ...args);
132337
132349
  };
132338
132350
  };
132339
132351
  var AlreadyLockedError = class extends Error {
@@ -141101,6 +141113,8 @@ if (!nativeAccelerationDisabled) {
141101
141113
  try {
141102
141114
  if (true)
141103
141115
  extractor = require_msgpackr_extract();
141116
+ else
141117
+ ;
141104
141118
  if (extractor)
141105
141119
  setExtractor(extractor.extractStrings);
141106
141120
  } catch (error3) {}
@@ -154054,6 +154068,7 @@ var __export3 = (target2, all5) => {
154054
154068
  var RedisServer = class {
154055
154069
  client;
154056
154070
  process;
154071
+ stopping = false;
154057
154072
  async start(port, version2 = "7.2.4", args = []) {
154058
154073
  if (this.process) {
154059
154074
  throw new Error(`Redis server is already listening on port: ${await this.process.getPort()}`);
@@ -154061,6 +154076,7 @@ var RedisServer = class {
154061
154076
  if (port && (port < 0 || port >= 65536)) {
154062
154077
  throw new RangeError(`Port should be >= 0 and < 65536. Received ${port}.`);
154063
154078
  }
154079
+ this.stopping = false;
154064
154080
  this.process = await import_redis_memory_server.RedisMemoryServer.create({
154065
154081
  instance: {
154066
154082
  port,
@@ -154069,8 +154085,30 @@ var RedisServer = class {
154069
154085
  binary: { version: version2 }
154070
154086
  });
154071
154087
  }
154088
+ onExit(handler) {
154089
+ const child = this.process?.instanceInfoSync?.childProcess;
154090
+ child?.once("exit", (code, signal) => {
154091
+ if (!this.stopping) {
154092
+ handler(code, signal);
154093
+ }
154094
+ });
154095
+ }
154096
+ onOutput(handler) {
154097
+ const child = this.process?.instanceInfoSync?.childProcess;
154098
+ const capture = (chunk3) => {
154099
+ for (const line of chunk3.toString().split(`
154100
+ `)) {
154101
+ if (line.trim() !== "") {
154102
+ handler(line);
154103
+ }
154104
+ }
154105
+ };
154106
+ child?.stdout?.on("data", capture);
154107
+ child?.stderr?.on("data", capture);
154108
+ }
154072
154109
  async kill() {
154073
154110
  if (this.process) {
154111
+ this.stopping = true;
154074
154112
  await this.client?.disconnect();
154075
154113
  await this.process.stop();
154076
154114
  this.process = undefined;
@@ -154852,12 +154890,31 @@ var cacheOnDev = async (ctx) => {
154852
154890
  return;
154853
154891
  }
154854
154892
  for (const { stackName, id } of caches) {
154855
- const port = await ctx.keep(`cache:${stackName}:${id}`, null, async () => {
154893
+ const { port, sink: sink3 } = await ctx.keep(`cache:${stackName}:${id}`, null, async () => {
154856
154894
  const server = new RedisServer;
154857
154895
  await server.start();
154858
154896
  await server.ping();
154859
- return { value: await server.getPort(), stop: () => server.kill() };
154897
+ const sink4 = { tail: [] };
154898
+ server.onExit((code, signal) => {
154899
+ sink4.crashed = code !== null ? `exited with code ${code}` : `killed by ${signal}`;
154900
+ sink4.health?.("down", sink4.crashed);
154901
+ });
154902
+ server.onOutput((line) => {
154903
+ sink4.tail.push(line);
154904
+ while (sink4.tail.length > 20) {
154905
+ sink4.tail.shift();
154906
+ }
154907
+ sink4.log?.(line);
154908
+ });
154909
+ return { value: { port: await server.getPort(), sink: sink4 }, stop: () => server.kill() };
154860
154910
  });
154911
+ const channel = `cache:${stackName}:${id}`;
154912
+ sink3.health = (status2, detail) => ctx.reportHealth(`cache ${stackName}/${id}`, status2, detail);
154913
+ sink3.health(sink3.crashed ? "down" : "up", sink3.crashed);
154914
+ sink3.log = (line) => ctx.emitEvent(channel, { date: Date.now(), line });
154915
+ for (const line of sink3.tail) {
154916
+ ctx.emitEvent(channel, { date: Date.now(), line });
154917
+ }
154861
154918
  const prefix = `CACHE_${constantCase(stackName)}_${constantCase(id)}`;
154862
154919
  ctx.addEnv(`${prefix}_HOST`, "localhost");
154863
154920
  ctx.addEnv(`${prefix}_PORT`, String(port));
@@ -154867,7 +154924,8 @@ var cacheOnDev = async (ctx) => {
154867
154924
  kind: "cache",
154868
154925
  stack: stackName,
154869
154926
  id,
154870
- detail: `localhost:${port}`
154927
+ detail: `localhost:${port}`,
154928
+ channel
154871
154929
  });
154872
154930
  }
154873
154931
  };
@@ -157300,13 +157358,18 @@ var parseQueryPublish = (body) => {
157300
157358
  MessageAttributes: attributes
157301
157359
  };
157302
157360
  };
157303
- var createSnsServer = () => {
157361
+ var createSnsServer = (props) => {
157304
157362
  let server;
157305
157363
  let closeServer;
157306
157364
  let dispatch;
157307
157365
  let reportFailure;
157308
157366
  const publish2 = (input) => {
157309
157367
  const messageId = randomUUID4();
157368
+ for (const capture of props?.captures ?? []) {
157369
+ if (capture(input)) {
157370
+ return messageId;
157371
+ }
157372
+ }
157310
157373
  const attributes = {};
157311
157374
  for (const [name, attribute] of Object.entries(input.MessageAttributes ?? {})) {
157312
157375
  attributes[name] = {
@@ -157414,22 +157477,29 @@ var pubsubOnDev = async (ctx) => {
157414
157477
  declared.add(event);
157415
157478
  }
157416
157479
  }
157417
- const { redisPort, sns, snsPort, wsPort } = await ctx.keep(`pubsub-core:${id}`, null, async () => {
157480
+ const { redisPort, sns, snsPort, wsPort, redisSink } = await ctx.keep(`pubsub-core:${id}`, null, async () => {
157418
157481
  const redis = new RedisServer;
157419
157482
  await redis.start();
157420
157483
  await redis.ping();
157484
+ const redisSink2 = {};
157485
+ redis.onExit((code, signal) => {
157486
+ redisSink2.crashed = code !== null ? `exited with code ${code}` : `killed by ${signal}`;
157487
+ redisSink2.health?.("down", redisSink2.crashed);
157488
+ });
157421
157489
  const redisPort2 = await redis.getPort();
157422
157490
  const sns2 = createSnsServer();
157423
157491
  const snsPort2 = await sns2.listen();
157424
157492
  const wsPort2 = await findFreePort();
157425
157493
  return {
157426
- value: { redisPort: redisPort2, sns: sns2, snsPort: snsPort2, wsPort: wsPort2 },
157494
+ value: { redisPort: redisPort2, sns: sns2, snsPort: snsPort2, wsPort: wsPort2, redisSink: redisSink2 },
157427
157495
  stop: async () => {
157428
157496
  await sns2.stop();
157429
157497
  await redis.kill();
157430
157498
  }
157431
157499
  };
157432
157500
  });
157501
+ redisSink.health = (status2, detail) => ctx.reportHealth(`pubsub ${id} redis`, status2, detail);
157502
+ redisSink.health(redisSink.crashed ? "down" : "up", redisSink.crashed);
157433
157503
  ctx.retain(`pubsub-ws:${id}`);
157434
157504
  const channel = `pubsub:${id}`;
157435
157505
  const publisherRoute = formatRouteKey("base", "pubsub", `${id}-publisher`);
@@ -157511,7 +157581,10 @@ var pubsubOnDev = async (ctx) => {
157511
157581
  }
157512
157582
  return dispatch(event);
157513
157583
  }, reportFailure);
157514
- await ctx.keep(`pubsub-ws:${id}`, { wsPort, redisPort, snsPort }, async () => {
157584
+ const ws = await ctx.keep(`pubsub-ws:${id}`, { wsPort, redisPort, snsPort }, async () => {
157585
+ const sink3 = {
157586
+ stopping: false
157587
+ };
157515
157588
  const runtime = join24(dirname12(fileURLToPath8(import.meta.url)), "handlers", "pubsub-server.js");
157516
157589
  const child = spawn2(process.execPath, [runtime], {
157517
157590
  stdio: ["ignore", "ignore", "pipe"],
@@ -157536,9 +157609,23 @@ var pubsubOnDev = async (ctx) => {
157536
157609
  }
157537
157610
  });
157538
157611
  child.stderr?.on("data", (chunk3) => process.stderr.write(chunk3));
157612
+ child.on("exit", (code, signal) => {
157613
+ if (!sink3.stopping) {
157614
+ sink3.health?.("down", code !== null ? `exited with code ${code}` : `killed by ${signal}`);
157615
+ }
157616
+ });
157539
157617
  await waitForHealth(wsPort, 30000);
157540
- return { value: child, stop: () => stopChild(child) };
157618
+ return {
157619
+ value: { child, sink: sink3 },
157620
+ stop: async () => {
157621
+ sink3.stopping = true;
157622
+ await stopChild(child);
157623
+ }
157624
+ };
157541
157625
  });
157626
+ ws.sink.health = (status2, detail) => ctx.reportHealth(`pubsub ${id}`, status2, detail);
157627
+ ws.sink.stopping = false;
157628
+ ws.sink.health(ws.child.exitCode === null ? "up" : "down");
157542
157629
  },
157543
157630
  async stop() {
157544
157631
  feed?.disconnect();
@@ -158756,19 +158843,37 @@ var searchOnDev = async (ctx) => {
158756
158843
  if (indexes.length === 0) {
158757
158844
  return;
158758
158845
  }
158759
- const { port } = await ctx.keep("opensearch", null, async () => {
158846
+ const { port, sink: sink3 } = await ctx.keep("opensearch", null, async () => {
158760
158847
  const port2 = await findFreePort();
158761
158848
  const path6 = await download(VERSION_3_5_0_MIN);
158849
+ const sink4 = { tail: [] };
158762
158850
  const kill = await launch({
158763
158851
  path: path6,
158764
158852
  port: port2,
158765
158853
  host: "localhost",
158766
158854
  version: VERSION_3_5_0_MIN,
158767
- debug: false
158855
+ debug: false,
158856
+ onExit(code, signal) {
158857
+ sink4.crashed = code !== null ? `exited with code ${code}` : `killed by ${signal}`;
158858
+ sink4.health?.("down", sink4.crashed);
158859
+ },
158860
+ onOutput(line) {
158861
+ sink4.tail.push(line);
158862
+ while (sink4.tail.length > 20) {
158863
+ sink4.tail.shift();
158864
+ }
158865
+ sink4.log?.(line);
158866
+ }
158768
158867
  });
158769
158868
  await waitForSearch(port2, 60000);
158770
- return { value: { port: port2 }, stop: kill };
158869
+ return { value: { port: port2, sink: sink4 }, stop: kill };
158771
158870
  });
158871
+ sink3.health = (status2, detail) => ctx.reportHealth("search", status2, detail);
158872
+ sink3.health(sink3.crashed ? "down" : "up", sink3.crashed);
158873
+ sink3.log = (line) => ctx.emitEvent("search", { date: Date.now(), line });
158874
+ for (const line of sink3.tail) {
158875
+ ctx.emitEvent("search", { date: Date.now(), line });
158876
+ }
158772
158877
  ctx.addEnv("SEARCH_DOMAIN", `localhost:${port}`);
158773
158878
  const client = new Client5({ node: `http://localhost:${port}` });
158774
158879
  for (const { stackName, id, props } of indexes) {
@@ -158781,7 +158886,8 @@ var searchOnDev = async (ctx) => {
158781
158886
  kind: "search",
158782
158887
  stack: stackName,
158783
158888
  id,
158784
- detail: `localhost:${port}`
158889
+ detail: `localhost:${port}`,
158890
+ channel: "search"
158785
158891
  });
158786
158892
  }
158787
158893
  };
@@ -159014,7 +159120,7 @@ var siteOnDev = async (ctx) => {
159014
159120
  const value = await ctx.keep(key, { command: command3, port, env: env4 }, async () => {
159015
159121
  const [bin, ...args] = command3.split(" ");
159016
159122
  const cwd2 = join33(directories.root, dirname16(stackConfig.file));
159017
- const sink2 = {
159123
+ const sink3 = {
159018
159124
  emit: () => {},
159019
159125
  stopping: false
159020
159126
  };
@@ -159032,7 +159138,7 @@ var siteOnDev = async (ctx) => {
159032
159138
  continue;
159033
159139
  }
159034
159140
  tail.push(line);
159035
- sink2.emit({ date: Date.now(), line });
159141
+ sink3.emit({ date: Date.now(), line });
159036
159142
  }
159037
159143
  while (tail.length > 20) {
159038
159144
  tail.shift();
@@ -159040,24 +159146,30 @@ var siteOnDev = async (ctx) => {
159040
159146
  };
159041
159147
  child.stdout?.on("data", capture);
159042
159148
  child.stderr?.on("data", capture);
159043
- child.on("exit", (code) => {
159044
- if (!sink2.stopping && code !== null && code !== 0) {
159149
+ child.on("exit", (code, signal) => {
159150
+ if (sink3.stopping) {
159151
+ return;
159152
+ }
159153
+ if (code !== null && code !== 0) {
159045
159154
  logs_exports.error(`The site "${id}" dev server exited with code ${code}:
159046
159155
  ${tail.join(`
159047
159156
  `)}`);
159048
- sink2.emit({ date: Date.now(), line: `Exited with code ${code}` });
159157
+ sink3.emit({ date: Date.now(), line: `Exited with code ${code}` });
159049
159158
  }
159159
+ sink3.health?.("down", code !== null ? `exited with code ${code}` : `killed by ${signal}`);
159050
159160
  });
159051
159161
  return {
159052
- value: { port, tail, sink: sink2, child },
159162
+ value: { port, tail, sink: sink3, child },
159053
159163
  stop: async () => {
159054
- sink2.stopping = true;
159164
+ sink3.stopping = true;
159055
159165
  await stopChild(child);
159056
159166
  }
159057
159167
  };
159058
159168
  });
159059
159169
  value.sink.emit = (data) => ctx.emitEvent(channel, data);
159170
+ value.sink.health = (status2, detail) => ctx.reportHealth(`site ${id}`, status2, detail);
159060
159171
  value.sink.stopping = false;
159172
+ value.sink.health(value.child.exitCode === null ? "up" : "down");
159061
159173
  for (const line of value.tail) {
159062
159174
  ctx.emitEvent(channel, { date: Date.now(), line });
159063
159175
  }
@@ -160173,18 +160285,7 @@ var topicFeature = defineFeature({
160173
160285
  });
160174
160286
  }
160175
160287
  }
160176
- const { server, port } = await ctx.keep("shim:sns", null, async () => {
160177
- const server2 = createSnsServer();
160178
- const port2 = await server2.listen();
160179
- return { value: { server: server2, port: port2 }, stop: () => server2.stop() };
160180
- });
160181
- ctx.addEnv("AWS_ENDPOINT_URL_SNS", `http://127.0.0.1:${port}`);
160182
- ctx.registerServer({
160183
- name: "sns",
160184
- start({ dispatch, reportFailure }) {
160185
- server.connect(dispatch, reportFailure);
160186
- }
160187
- });
160288
+ await ctx.useSns();
160188
160289
  },
160189
160290
  async onTypeGen(ctx) {
160190
160291
  const gen = new TypeFile("awsless");
@@ -160382,6 +160483,7 @@ var vpcFeature = defineFeature({
160382
160483
  });
160383
160484
 
160384
160485
  // src/feature/alert/index.ts
160486
+ import { randomUUID as randomUUID7 } from "crypto";
160385
160487
  var typeGenCode10 = `
160386
160488
  import type { PublishOptions } from '@awsless/sns'
160387
160489
  import type { Mock } from 'vitest'
@@ -160400,6 +160502,44 @@ type TestMockEntry = MockBuilder & Mock<(payload: unknown) => unknown>
160400
160502
  `;
160401
160503
  var alertFeature = defineFeature({
160402
160504
  name: "alert",
160505
+ async onDev(ctx) {
160506
+ const ids = Object.keys(ctx.appConfig.alerts ?? {});
160507
+ if (ids.length === 0) {
160508
+ return;
160509
+ }
160510
+ const { captures, alerts } = await ctx.useSns();
160511
+ const names = new Map(ids.map((id) => [
160512
+ formatGlobalResourceName({
160513
+ appName: ctx.appConfig.name,
160514
+ resourceType: "alert",
160515
+ resourceName: id
160516
+ }),
160517
+ id
160518
+ ]));
160519
+ captures.push((input) => {
160520
+ const id = names.get(input.TopicArn?.split(":").at(-1) ?? "");
160521
+ if (!id) {
160522
+ return false;
160523
+ }
160524
+ alerts.unshift({
160525
+ id: randomUUID7(),
160526
+ date: Date.now(),
160527
+ alert: id,
160528
+ subject: input.Subject,
160529
+ message: input.Message
160530
+ });
160531
+ ctx.emitEvent("problems", {
160532
+ date: Date.now(),
160533
+ kind: "alert",
160534
+ title: input.Subject ?? id,
160535
+ detail: id
160536
+ });
160537
+ return true;
160538
+ });
160539
+ for (const [name, id] of names) {
160540
+ ctx.registerResource({ kind: "alert", id, detail: name });
160541
+ }
160542
+ },
160403
160543
  async onTypeGen(ctx) {
160404
160544
  const gen = new TypeFile("awsless");
160405
160545
  const resources = new TypeObject(1);
@@ -161457,6 +161597,7 @@ var instanceOnDev = async (ctx) => {
161457
161597
  async start({ env: env4 }) {
161458
161598
  const tail = [];
161459
161599
  boot = () => {
161600
+ ctx.reportHealth(`instance ${id}`, "up");
161460
161601
  child = spawn5("bun", [file2], {
161461
161602
  cwd: cwd2,
161462
161603
  stdio: ["ignore", "pipe", "pipe"],
@@ -161485,13 +161626,17 @@ var instanceOnDev = async (ctx) => {
161485
161626
  };
161486
161627
  child.stdout?.on("data", capture);
161487
161628
  child.stderr?.on("data", capture);
161488
- child.on("exit", (code) => {
161489
- if (!stopping && code !== null && code !== 0) {
161629
+ child.on("exit", (code, signal) => {
161630
+ if (stopping) {
161631
+ return;
161632
+ }
161633
+ if (code !== null && code !== 0) {
161490
161634
  logs_exports.error(`The instance "${id}" exited with code ${code}:
161491
161635
  ${tail.join(`
161492
161636
  `)}`);
161493
161637
  ctx.emitEvent(channel, { date: Date.now(), line: `Exited with code ${code}`, error: true });
161494
161638
  }
161639
+ ctx.reportHealth(`instance ${id}`, "down", code !== null ? `exited with code ${code}` : `killed by ${signal}`);
161495
161640
  });
161496
161641
  };
161497
161642
  boot();
@@ -166580,7 +166725,7 @@ var createS3Server = (props) => {
166580
166725
  };
166581
166726
 
166582
166727
  // src/dev/servers/sqs.ts
166583
- import { createHash as createHash23, randomUUID as randomUUID7 } from "crypto";
166728
+ import { createHash as createHash23, randomUUID as randomUUID8 } from "crypto";
166584
166729
  import { createServer as createServer8 } from "http";
166585
166730
  var md5 = (value) => createHash23("md5").update(value).digest("hex");
166586
166731
  var md5OfAttributes = (attributes) => {
@@ -166655,7 +166800,7 @@ var createSqsServer = (props) => {
166655
166800
  Records: [
166656
166801
  {
166657
166802
  messageId,
166658
- receiptHandle: randomUUID7(),
166803
+ receiptHandle: randomUUID8(),
166659
166804
  body: input.MessageBody,
166660
166805
  attributes: {
166661
166806
  ApproximateReceiveCount: "1",
@@ -166723,7 +166868,7 @@ var createSqsServer = (props) => {
166723
166868
  if (!queue) {
166724
166869
  throw new Error(`Unknown local queue: ${input.QueueUrl}`);
166725
166870
  }
166726
- const messageId = randomUUID7();
166871
+ const messageId = randomUUID8();
166727
166872
  deliver(queue, input, messageId);
166728
166873
  return sendResult(input, messageId);
166729
166874
  },
@@ -166734,7 +166879,7 @@ var createSqsServer = (props) => {
166734
166879
  }
166735
166880
  return {
166736
166881
  Successful: (input.Entries ?? []).map((entry) => {
166737
- const messageId = randomUUID7();
166882
+ const messageId = randomUUID8();
166738
166883
  deliver(queue, entry, messageId);
166739
166884
  return {
166740
166885
  Id: entry.Id,
@@ -166758,7 +166903,7 @@ var createSqsServer = (props) => {
166758
166903
  const batch2 = visible.slice(0, Math.max(1, input.MaxNumberOfMessages ?? 1));
166759
166904
  return {
166760
166905
  Messages: batch2.map((message4) => {
166761
- message4.receipt = randomUUID7();
166906
+ message4.receipt = randomUUID8();
166762
166907
  message4.receiveCount += 1;
166763
166908
  message4.firstReceivedAt ??= now;
166764
166909
  message4.visibleAt = now + (input.VisibilityTimeout ?? 30) * 1000;
@@ -166915,6 +167060,12 @@ var createDevContext = (props) => {
166915
167060
  const servers = [];
166916
167061
  const restartPaths = [];
166917
167062
  const resources = [];
167063
+ const health = new Map;
167064
+ const reportHealth = (id, status2, detail) => {
167065
+ const entry = { id, status: status2, detail, date: Date.now() };
167066
+ health.set(id, entry);
167067
+ events.emit("health", entry);
167068
+ };
166918
167069
  const listeners2 = new Map;
166919
167070
  const replays = new Map;
166920
167071
  const events = {
@@ -167012,12 +167163,37 @@ var createDevContext = (props) => {
167012
167163
  })();
167013
167164
  return sqs;
167014
167165
  };
167166
+ let sns;
167167
+ const useSns = () => {
167168
+ sns ??= (async () => {
167169
+ const value = await props.pool.keep("shim:sns", null, async () => {
167170
+ const captures = [];
167171
+ const server = createSnsServer({ captures });
167172
+ const port = await server.listen();
167173
+ return {
167174
+ value: { server, port, captures, alerts: [] },
167175
+ stop: () => server.stop()
167176
+ };
167177
+ });
167178
+ value.captures.splice(0, value.captures.length);
167179
+ env4["AWS_ENDPOINT_URL_SNS"] = `http://127.0.0.1:${value.port}`;
167180
+ servers.push({
167181
+ name: "sns",
167182
+ start({ dispatch, reportFailure }) {
167183
+ value.server.connect(dispatch, reportFailure);
167184
+ }
167185
+ });
167186
+ return { port: value.port, captures: value.captures, alerts: value.alerts };
167187
+ })();
167188
+ return sns;
167189
+ };
167015
167190
  return {
167016
167191
  env: env4,
167017
167192
  routes,
167018
167193
  servers,
167019
167194
  restartPaths,
167020
167195
  resources,
167196
+ health,
167021
167197
  events,
167022
167198
  context: {
167023
167199
  appConfig: props.appConfig,
@@ -167036,6 +167212,7 @@ var createDevContext = (props) => {
167036
167212
  useDynamo,
167037
167213
  useStore,
167038
167214
  useSqs,
167215
+ useSns,
167039
167216
  addEnv(name, value) {
167040
167217
  if (name in env4 && env4[name] !== value) {
167041
167218
  throw new Error(`The env var "${name}" is defined multiple times with different values, while all bundled functions share the same env.`);
@@ -167055,7 +167232,8 @@ var createDevContext = (props) => {
167055
167232
  resources.push(resource);
167056
167233
  },
167057
167234
  log: props.log,
167058
- emitEvent: events.emit
167235
+ emitEvent: events.emit,
167236
+ reportHealth
167059
167237
  }
167060
167238
  };
167061
167239
  };
@@ -167112,7 +167290,7 @@ ${logs}`));
167112
167290
  var import_ioredis5 = __toESM(require_built3(), 1);
167113
167291
  import { DynamoDBClient as DynamoDBClient5 } from "@aws-sdk/client-dynamodb";
167114
167292
  import { DynamoDBDocumentClient, ScanCommand as ScanCommand2 } from "@aws-sdk/lib-dynamodb";
167115
- import { randomUUID as randomUUID8 } from "crypto";
167293
+ import { randomUUID as randomUUID9 } from "crypto";
167116
167294
  import { readdir as readdir9, readFile as readFile22, stat as stat8, writeFile as writeFile14 } from "fs/promises";
167117
167295
  import { createServer as createServer9 } from "http";
167118
167296
  import { join as join55, relative as relative11, sep as sep5 } from "path";
@@ -167321,6 +167499,7 @@ var createBundleWorker = (props) => {
167321
167499
  return run;
167322
167500
  };
167323
167501
  return {
167502
+ size: () => workers.length,
167324
167503
  start: () => serialize(doStart),
167325
167504
  stop: () => serialize(doStop),
167326
167505
  restart: () => serialize(async () => {
@@ -167364,7 +167543,7 @@ var dashboardHtml = `<!doctype html>
167364
167543
  --border: #262b36;
167365
167544
  --text: #d7dae0;
167366
167545
  --muted: #7d8590;
167367
- --accent: #f5a623;
167546
+ --accent: #ff9000;
167368
167547
  --good: #3fb950;
167369
167548
  --bad: #f85149;
167370
167549
  font-size: 14px;
@@ -167426,18 +167605,34 @@ var dashboardHtml = `<!doctype html>
167426
167605
  .logs .entry { padding: 4px 0; }
167427
167606
  .logs .entry + .entry, .logs .line + .entry, .logs .entry + .line { border-top: 1px solid var(--border); }
167428
167607
  .logs .entry .meta { display: flex; gap: 8px; margin-bottom: 2px; }
167429
- .config-form { display: flex; flex-direction: column; gap: 8px; max-width: 520px; }
167430
- .config-form .field { display: grid; grid-template-columns: 180px 1fr; gap: 12px; align-items: center; }
167431
- .config-form .name { color: var(--muted); overflow-wrap: anywhere; }
167432
- .config-form input {
167608
+ /* Every group is its own card, with a shared grid inside: the name
167609
+ column grows to the longest name (capped), so nothing truncates &
167610
+ the inputs line up. */
167611
+ .config-form { display: flex; flex-direction: column; gap: 14px; max-width: 720px; margin-top: 14px; }
167612
+ .config-group {
167433
167613
  background: var(--panel);
167614
+ border: 1px solid var(--border);
167615
+ border-radius: 8px;
167616
+ padding: 14px 16px;
167617
+ }
167618
+ .config-group h3 { margin: 0 0 12px; }
167619
+ .config-fields {
167620
+ display: grid;
167621
+ grid-template-columns: fit-content(340px) 1fr;
167622
+ gap: 10px 16px;
167623
+ align-items: center;
167624
+ }
167625
+ .config-fields .field { display: contents; }
167626
+ .config-fields .name { color: var(--muted); overflow-wrap: anywhere; }
167627
+ .config-fields input {
167628
+ background: var(--bg);
167434
167629
  color: var(--text);
167435
167630
  border: 1px solid var(--border);
167436
167631
  border-radius: 6px;
167437
167632
  padding: 6px 10px;
167438
167633
  font: inherit;
167439
167634
  }
167440
- .config-form input::placeholder { color: var(--muted); }
167635
+ .config-fields input::placeholder { color: var(--muted); }
167441
167636
  .groups { display: flex; flex-wrap: wrap; gap: 4px 16px; }
167442
167637
  .groups .group { display: flex; align-items: center; gap: 4px; cursor: pointer; }
167443
167638
  nav {
@@ -167448,10 +167643,13 @@ var dashboardHtml = `<!doctype html>
167448
167643
  nav h1 {
167449
167644
  font-size: 15px;
167450
167645
  margin: 4px 8px 16px;
167451
- color: var(--accent);
167452
167646
  cursor: pointer;
167453
167647
  }
167454
- nav h1 span { color: var(--muted); font-weight: normal; }
167648
+ /* The same two-tone logo as the cli: bold AWS in the brand orange,
167649
+ LESS dimmed, the command muted. */
167650
+ nav h1 .logo { color: var(--accent); font-weight: bold; }
167651
+ nav h1 .logo .dim { color: #a35d00; }
167652
+ nav h1 .cmd { color: var(--muted); font-weight: normal; }
167455
167653
  nav h3 {
167456
167654
  font-size: 10px;
167457
167655
  text-transform: uppercase;
@@ -167544,6 +167742,24 @@ var dashboardHtml = `<!doctype html>
167544
167742
  cursor: pointer;
167545
167743
  }
167546
167744
  .row:hover { background: var(--panel); }
167745
+ a.row { text-decoration: none; }
167746
+ .health { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 4px; }
167747
+ .health .chip {
167748
+ display: inline-flex;
167749
+ align-items: center;
167750
+ gap: 7px;
167751
+ background: var(--panel);
167752
+ border: 1px solid var(--border);
167753
+ border-radius: 999px;
167754
+ padding: 4px 12px;
167755
+ font-size: 12px;
167756
+ }
167757
+ .health .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--good); flex-shrink: 0; }
167758
+ .health .chip.down { border-color: var(--bad); }
167759
+ .health .chip.down .dot { background: var(--bad); }
167760
+ .health .chip .detail-text { color: var(--muted); }
167761
+ .row.problem .id { color: var(--bad); }
167762
+ .empty.good { color: var(--good); }
167547
167763
  .row .stack {
167548
167764
  color: var(--muted);
167549
167765
  min-width: 90px;
@@ -167675,9 +167891,10 @@ const GROUPS = [
167675
167891
  ['store', 'Stores'],
167676
167892
  ['config', 'Config'],
167677
167893
  ['auth', 'Auth'],
167894
+ ['alert', 'Alerts'],
167678
167895
  ['email', 'Emails'],
167679
167896
  ['worker', 'Logs'],
167680
- ['route', 'Routes'],
167897
+ ['route', 'Routers'],
167681
167898
  ]
167682
167899
 
167683
167900
  // The nav renders the features in scannable sections - a section only
@@ -167687,7 +167904,7 @@ const SECTIONS = [
167687
167904
  ['Compute', ['function', 'cron', 'task', 'instance']],
167688
167905
  ['Messaging', ['queue', 'topic', 'subscriber', 'pubsub']],
167689
167906
  ['Storage', ['table', 'cache', 'search', 'store']],
167690
- ['App', ['config', 'auth', 'email', 'worker']],
167907
+ ['App', ['config', 'auth', 'alert', 'email', 'worker']],
167691
167908
  ]
167692
167909
 
167693
167910
  const svg = inner =>
@@ -167710,6 +167927,7 @@ const ICONS = {
167710
167927
  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"/>'),
167711
167928
  search: svg('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'),
167712
167929
  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"/>'),
167930
+ 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"/>'),
167713
167931
  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"/>'),
167714
167932
  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"/>'),
167715
167933
  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"/>'),
@@ -167728,7 +167946,7 @@ const icon = kind => {
167728
167946
  let state
167729
167947
  let cleanupPanel
167730
167948
  const view = { kind: null, resource: null }
167731
- const filter = { query: '', stack: '' }
167949
+ const filter = { query: '', stack: '', router: '' }
167732
167950
 
167733
167951
  const label = r => (r.stack ? r.stack + ' / ' + r.id : r.id)
167734
167952
  const groupTitle = kind => GROUPS.find(([k]) => k === kind)?.[1] ?? kind
@@ -167744,6 +167962,7 @@ const filterParams = () => {
167744
167962
  const params = new URLSearchParams()
167745
167963
  if (filter.query) params.set('q', filter.query)
167746
167964
  if (filter.stack) params.set('stack', filter.stack)
167965
+ if (filter.router) params.set('router', filter.router)
167747
167966
  return params.toString()
167748
167967
  }
167749
167968
 
@@ -167764,6 +167983,7 @@ const selectKind = kind => {
167764
167983
  view.resource = null
167765
167984
  filter.query = ''
167766
167985
  filter.stack = ''
167986
+ filter.router = ''
167767
167987
  history.pushState(null, '', listUrl(kind))
167768
167988
  render()
167769
167989
  }
@@ -167789,6 +168009,7 @@ const applyLocation = () => {
167789
168009
  view.resource = null
167790
168010
  filter.query = params.get('q') ?? ''
167791
168011
  filter.stack = params.get('stack') ?? ''
168012
+ filter.router = params.get('router') ?? ''
167792
168013
 
167793
168014
  if (view.kind && segments.length >= 3) {
167794
168015
  const router = params.get('router')
@@ -168175,6 +168396,11 @@ const cachePanel = async (main, r) => {
168175
168396
  const holder = $('div', {}, $('p', { className: 'empty' }, 'Scanning...'))
168176
168397
  main.append(holder)
168177
168398
 
168399
+ // The redis server's own output, streamed live.
168400
+ if (r.channel) {
168401
+ cleanupPanel = attachLogFeed(main, r.channel, undefined, 'Server logs')
168402
+ }
168403
+
168178
168404
  try {
168179
168405
  const data = await api('/api/cache?target=' + encodeURIComponent(r.detail))
168180
168406
  holder.innerHTML = ''
@@ -168255,6 +168481,11 @@ const searchPanel = async (main, r) => {
168255
168481
  result,
168256
168482
  )
168257
168483
 
168484
+ // The opensearch server's own output, streamed live.
168485
+ if (r.channel) {
168486
+ cleanupPanel = attachLogFeed(main, r.channel, undefined, 'Server logs')
168487
+ }
168488
+
168258
168489
  try {
168259
168490
  const { data } = await proxy('/_cat/indices?format=json&h=index,health,docs.count,store.size&s=index')
168260
168491
  const rows = data.filter(entry => !entry.index.startsWith('.'))
@@ -168309,6 +168540,59 @@ const storePanel = async (main, r) => {
168309
168540
 
168310
168541
  // Every email sent through Email.send during this session - captured
168311
168542
  // by the local ses shim instead of being delivered.
168543
+ const alertPanel = async (main) => {
168544
+ const holder = $('div', {})
168545
+ main.append(holder)
168546
+
168547
+ const render = async () => {
168548
+ const data = await api('/api/alerts')
168549
+ const alerts = data.alerts ?? []
168550
+ holder.innerHTML = ''
168551
+
168552
+ const refresh = $('button', { className: 'primary', textContent: 'Refresh' })
168553
+ refresh.onclick = render
168554
+ holder.append($('div', { className: 'actions' }, [refresh]))
168555
+
168556
+ if (alerts.length === 0) {
168557
+ holder.append($('p', { className: 'empty' }, 'No alerts sent yet.'))
168558
+ return
168559
+ }
168560
+
168561
+ const rows = $('div', { className: 'rows' })
168562
+
168563
+ for (const alert of alerts) {
168564
+ const row = $('button', { className: 'row' }, [
168565
+ $('span', { className: 'stack' }, new Date(alert.date).toLocaleTimeString()),
168566
+ $('span', { className: 'id' }, alert.subject ?? '(no subject)'),
168567
+ $('span', { className: 'info' }, alert.alert),
168568
+ ])
168569
+ row.onclick = () => {
168570
+ holder.innerHTML = ''
168571
+
168572
+ const back = $('button', { className: 'back', textContent: '\u2190 Alerts' })
168573
+ back.onclick = render
168574
+
168575
+ let message = alert.message ?? '(no payload)'
168576
+ try {
168577
+ message = JSON.stringify(JSON.parse(alert.message), null, 2)
168578
+ } catch (_) {}
168579
+
168580
+ holder.append(
168581
+ back,
168582
+ $('h2', {}, alert.subject ?? '(no subject)'),
168583
+ $('p', { className: 'detail' }, alert.alert + ' \xB7 ' + new Date(alert.date).toLocaleString()),
168584
+ $('pre', { className: 'result' }, message),
168585
+ )
168586
+ }
168587
+ rows.append(row)
168588
+ }
168589
+
168590
+ holder.append(rows)
168591
+ }
168592
+
168593
+ render()
168594
+ }
168595
+
168312
168596
  const emailPanel = async (main) => {
168313
168597
  const holder = $('div', {})
168314
168598
  main.append(holder)
@@ -168371,7 +168655,34 @@ const configPanel = async (main) => {
168371
168655
  const inputs = new Map()
168372
168656
  const form = $('div', { className: 'config-form' })
168373
168657
 
168658
+ // Configs group by the first segment of their name: alphapo-api-key
168659
+ // & alphapo-host read as one "alphapo" block with the prefix
168660
+ // stripped from the rows. A prefix needs at least two members to
168661
+ // form a group - the loners keep their full name under "other".
168662
+ const byPrefix = new Map()
168663
+
168374
168664
  for (const name of names) {
168665
+ const prefix = name.split('-')[0]
168666
+ if (!byPrefix.has(prefix)) byPrefix.set(prefix, [])
168667
+ byPrefix.get(prefix).push(name)
168668
+ }
168669
+
168670
+ const sections = []
168671
+ const other = []
168672
+
168673
+ for (const [prefix, members] of byPrefix) {
168674
+ if (members.length > 1) {
168675
+ sections.push([prefix, members])
168676
+ } else {
168677
+ other.push(members[0])
168678
+ }
168679
+ }
168680
+
168681
+ if (other.length > 0) {
168682
+ sections.push(['other', other])
168683
+ }
168684
+
168685
+ const addField = (grid, name) => {
168375
168686
  // An empty input falls through to the value pulled from ssm on
168376
168687
  // boot - the placeholder shows whether the pull provided one.
168377
168688
  const input = $('input', {
@@ -168380,7 +168691,21 @@ const configPanel = async (main) => {
168380
168691
  spellcheck: false,
168381
168692
  })
168382
168693
  inputs.set(name, input)
168383
- form.append($('label', { className: 'field' }, [$('span', { className: 'name' }, name), input]))
168694
+ grid.append($('label', { className: 'field' }, [$('span', { className: 'name' }, name), input]))
168695
+ }
168696
+
168697
+ for (const [prefix, members] of sections) {
168698
+ const grid = $('div', { className: 'config-fields' })
168699
+
168700
+ for (const name of members) {
168701
+ addField(grid, name)
168702
+ }
168703
+
168704
+ // A lone unlabeled group skips its header - the card alone
168705
+ // carries the grouping.
168706
+ const header = sections.length > 1 || other.length === 0 ? [$('h3', {}, prefix)] : []
168707
+
168708
+ form.append($('div', { className: 'config-group' }, [...header, grid]))
168384
168709
  }
168385
168710
 
168386
168711
  if (names.length === 0) {
@@ -168549,7 +168874,9 @@ const authPanel = async (main, r) => {
168549
168874
 
168550
168875
  const renderResource = main => {
168551
168876
  const r = view.resource
168552
- const back = $('button', { className: 'back' }, '\u2190 ' + groupTitle(r.kind))
168877
+ // A route's back button lands on its router's route list, one level
168878
+ // below the Routers tab itself.
168879
+ const back = $('button', { className: 'back' }, '\u2190 ' + (r.kind === 'route' ? 'Routes' : groupTitle(r.kind)))
168553
168880
 
168554
168881
  back.onclick = () => {
168555
168882
  view.resource = null
@@ -168596,7 +168923,49 @@ const renderList = main => {
168596
168923
  const items = state.resources.filter(r => r.kind === view.kind)
168597
168924
  const stacks = [...new Set(items.map(r => r.stack).filter(Boolean))].sort()
168598
168925
 
168599
- main.append($('h2', {}, groupTitle(view.kind)))
168926
+ // The routes view drills down per router: every router runs as its
168927
+ // own local server (like its own domain in production), so a flat
168928
+ // list mixing every router's routes would only confuse.
168929
+ if (view.kind === 'route' && !filter.router) {
168930
+ main.append($('h2', {}, 'Routers'))
168931
+
168932
+ const rows = $('div', { className: 'rows' })
168933
+
168934
+ for (const [id, port] of Object.entries(state.routerPorts)) {
168935
+ const count = items.filter(r => r.router === id).length
168936
+ const row = $('button', { className: 'row' }, [
168937
+ $('span', { className: 'stack' }, id),
168938
+ $('span', { className: 'id' }, 'http://localhost:' + port),
168939
+ $('span', { className: 'info' }, count + (count === 1 ? ' route' : ' routes')),
168940
+ ])
168941
+ row.onclick = () => {
168942
+ filter.router = id
168943
+ history.pushState(null, '', listUrl('route'))
168944
+ render()
168945
+ }
168946
+ rows.append(row)
168947
+ }
168948
+
168949
+ main.append(rows)
168950
+ return
168951
+ }
168952
+
168953
+ if (view.kind === 'route') {
168954
+ const back = $('button', { className: 'back' }, '\u2190 Routers')
168955
+ back.onclick = () => {
168956
+ filter.router = ''
168957
+ history.pushState(null, '', listUrl('route'))
168958
+ render()
168959
+ }
168960
+
168961
+ main.append(
168962
+ back,
168963
+ $('h2', {}, 'Routes'),
168964
+ $('p', { className: 'detail' }, filter.router + ' \xB7 http://localhost:' + state.routerPorts[filter.router]),
168965
+ )
168966
+ } else {
168967
+ main.append($('h2', {}, groupTitle(view.kind)))
168968
+ }
168600
168969
 
168601
168970
  // The config page is a single form over every defined value,
168602
168971
  // instead of a resource list.
@@ -168609,15 +168978,27 @@ const renderList = main => {
168609
168978
  return emailPanel(main)
168610
168979
  }
168611
168980
 
168981
+ // The alert page lists every captured alert of the session.
168982
+ if (view.kind === 'alert') {
168983
+ return alertPanel(main)
168984
+ }
168985
+
168612
168986
  // The worker page streams the bundle worker output directly,
168613
168987
  // instead of listing its single resource.
168614
168988
  if (view.kind === 'worker') {
168615
- cleanupPanel = attachLogFeed(main, 'worker', undefined, null)
168989
+ const workerFeed = attachLogFeed(main, 'worker', undefined, 'Worker output')
168990
+ const debugFeed = attachLogFeed(main, 'debug', undefined, 'Dev server')
168991
+
168992
+ cleanupPanel = () => {
168993
+ workerFeed?.()
168994
+ debugFeed?.()
168995
+ }
168616
168996
  return
168617
168997
  }
168618
168998
 
168619
168999
  const matches = r => {
168620
169000
  if (filter.stack && r.stack !== filter.stack) return false
169001
+ if (filter.router && r.router !== filter.router) return false
168621
169002
  if (!filter.query) return true
168622
169003
 
168623
169004
  return (r.stack + ' ' + r.id + ' ' + (r.detail ?? ''))
@@ -168691,11 +169072,14 @@ const render = () => {
168691
169072
  cleanupPanel = undefined
168692
169073
 
168693
169074
  const nav = document.getElementById('nav')
168694
- nav.innerHTML = '<h1>awsless <span>dev</span></h1>'
169075
+ nav.innerHTML = '<h1><span class="logo">AWS<span class="dim">LESS</span></span> <span class="cmd">dev</span></h1>'
168695
169076
  nav.querySelector('h1').onclick = showHome
168696
169077
 
168697
169078
  const navButton = kind => {
168698
- const count = state.resources.filter(r => r.kind === kind).length
169079
+ // The Routers tab lists routers, so its count follows suit.
169080
+ const count = kind === 'route'
169081
+ ? Object.keys(state.routerPorts).length
169082
+ : state.resources.filter(r => r.kind === kind).length
168699
169083
  if (count === 0) return null
168700
169084
 
168701
169085
  // The email outbox is a single feed, so a count of registered
@@ -168703,7 +169087,7 @@ const render = () => {
168703
169087
  const button = $('button', { className: view.kind === kind ? 'active' : '' }, [
168704
169088
  icon(kind),
168705
169089
  groupTitle(kind),
168706
- kind === 'email' || kind === 'worker' ? '' : $('span', { className: 'count' }, String(count)),
169090
+ kind === 'email' || kind === 'worker' || kind === 'alert' ? '' : $('span', { className: 'count' }, String(count)),
168707
169091
  ])
168708
169092
  button.onclick = () => selectKind(kind)
168709
169093
  return button
@@ -168775,7 +169159,192 @@ const render = () => {
168775
169159
  } else if (view.kind) {
168776
169160
  renderList(main)
168777
169161
  } else {
168778
- main.append($('p', { className: 'empty' }, 'Select a feature to browse its resources.'))
169162
+ renderHome(main)
169163
+ }
169164
+ }
169165
+
169166
+ // ------------------------------------------------------------------
169167
+ // Homepage: the session at a glance - endpoints, problems & the live
169168
+ // activity feed of everything running through the bundle.
169169
+
169170
+ const timeAgo = ms => {
169171
+ const s = Math.max(0, Math.round(ms / 1000))
169172
+ if (s < 60) return s + 's'
169173
+ if (s < 3600) return Math.round(s / 60) + 'm'
169174
+ return Math.floor(s / 3600) + 'h ' + (Math.round(s / 60) % 60) + 'm'
169175
+ }
169176
+
169177
+ const renderHome = main => {
169178
+ const session = state.session ?? {}
169179
+
169180
+ const uptime = $('span', {}, '')
169181
+ const updateUptime = () => {
169182
+ uptime.textContent = session.startedAt ? 'up ' + timeAgo(Date.now() - session.startedAt) : ''
169183
+ }
169184
+ updateUptime()
169185
+ const uptimeTimer = setInterval(updateUptime, 1000)
169186
+
169187
+ main.append(
169188
+ $('h2', {}, state.app),
169189
+ $('p', { className: 'detail' }, [
169190
+ uptime,
169191
+ session.workers ? ' \\u00b7 ' + session.workers + ' workers' : '',
169192
+ ]),
169193
+ )
169194
+
169195
+ // ----------------------------------------------------------------
169196
+ // Health: one chip per moving part, live.
169197
+
169198
+ const chips = $('div', { className: 'health' })
169199
+ const chipEls = new Map()
169200
+
169201
+ const renderChip = entry => {
169202
+ let chip = chipEls.get(entry.id)
169203
+
169204
+ if (!chip) {
169205
+ chip = $('span', { className: 'chip' })
169206
+ chipEls.set(entry.id, chip)
169207
+ chips.append(chip)
169208
+ }
169209
+
169210
+ chip.className = 'chip' + (entry.status === 'down' ? ' down' : '')
169211
+ chip.innerHTML = ''
169212
+ chip.append(
169213
+ $('span', { className: 'dot' }),
169214
+ entry.id,
169215
+ entry.detail ? $('span', { className: 'detail-text' }, '\xB7 ' + entry.detail) : '',
169216
+ )
169217
+ }
169218
+
169219
+ for (const entry of state.health ?? []) renderChip(entry)
169220
+
169221
+ const healthSource = new EventSource('/api/events?channel=health')
169222
+ healthSource.onmessage = message => {
169223
+ try {
169224
+ renderChip(JSON.parse(message.data))
169225
+ } catch (_) {}
169226
+ }
169227
+
169228
+ if ((state.health ?? []).length > 0) {
169229
+ main.append(chips)
169230
+ }
169231
+
169232
+ // ----------------------------------------------------------------
169233
+ // Endpoints
169234
+
169235
+ main.append($('h3', {}, 'Endpoints'))
169236
+
169237
+ const endpoints = $('div', { className: 'rows' })
169238
+
169239
+ for (const [id, port] of Object.entries(state.routerPorts ?? {})) {
169240
+ const url = 'http://localhost:' + port
169241
+ const row = $('a', { className: 'row', href: url, target: '_blank' }, [
169242
+ $('span', { className: 'stack' }, 'router ' + id),
169243
+ $('span', { className: 'id' }, url),
169244
+ $('span', { className: 'info' }, ''),
169245
+ ])
169246
+ endpoints.append(row)
169247
+ }
169248
+
169249
+ main.append(endpoints)
169250
+
169251
+ // ----------------------------------------------------------------
169252
+ // Problems: failures, route errors & fired alerts, newest first.
169253
+
169254
+ main.append($('h3', {}, 'Problems'))
169255
+
169256
+ const problems = $('div', { className: 'rows' })
169257
+ const calm = $('p', { className: 'empty good' }, 'No problems - everything runs clean.')
169258
+ main.append(calm, problems)
169259
+
169260
+ const problemRows = []
169261
+
169262
+ const problemRow = data => {
169263
+ calm.hidden = true
169264
+
169265
+ const row = $('button', { className: 'row problem' }, [
169266
+ $('span', { className: 'stack' }, new Date(data.date).toLocaleTimeString() + ' \\u00b7 ' + data.kind),
169267
+ $('span', { className: 'id' }, data.title ?? ''),
169268
+ $('span', { className: 'info' }, (data.detail ?? '').slice(0, 120)),
169269
+ ])
169270
+
169271
+ row.onclick = () => {
169272
+ if (data.kind === 'alert') {
169273
+ selectKind('alert')
169274
+ return
169275
+ }
169276
+
169277
+ const resource = state.resources.find(r => r.routeKey === data.title)
169278
+ if (resource) selectResource(resource)
169279
+ }
169280
+
169281
+ problems.prepend(row)
169282
+ problemRows.push(row)
169283
+
169284
+ while (problemRows.length > 20) {
169285
+ problemRows.shift().remove()
169286
+ }
169287
+ }
169288
+
169289
+ const problemSource = new EventSource('/api/events?channel=problems')
169290
+ problemSource.onmessage = message => {
169291
+ try {
169292
+ problemRow(JSON.parse(message.data))
169293
+ } catch (_) {}
169294
+ }
169295
+
169296
+ // ----------------------------------------------------------------
169297
+ // Activity: every dispatch through the bundle, newest first.
169298
+
169299
+ main.append($('h3', {}, 'Activity'))
169300
+
169301
+ const activity = $('div', { className: 'rows' })
169302
+ const idle = $('p', { className: 'empty' }, 'Nothing ran yet.')
169303
+ main.append(idle, activity)
169304
+
169305
+ const activityRows = []
169306
+
169307
+ const activityRow = data => {
169308
+ idle.hidden = true
169309
+
169310
+ const resource = state.resources.find(r => r.routeKey === data.route)
169311
+ const row = $('button', { className: 'row' + (data.ok ? '' : ' problem') }, [
169312
+ $('span', { className: 'stack' }, new Date(data.date).toLocaleTimeString()),
169313
+ $('span', { className: 'id' }, (data.ok ? '' : '\\u2717 ') + data.route),
169314
+ $('span', { className: 'info' }, data.ok ? data.ms + 'ms' : (data.error ?? 'failed').slice(0, 80)),
169315
+ ])
169316
+
169317
+ if (resource) {
169318
+ row.onclick = () => selectResource(resource)
169319
+ }
169320
+
169321
+ activity.prepend(row)
169322
+ activityRows.push(row)
169323
+
169324
+ while (activityRows.length > 50) {
169325
+ activityRows.shift().remove()
169326
+ }
169327
+ }
169328
+
169329
+ const activitySource = new EventSource('/api/events?channel=activity')
169330
+ activitySource.onmessage = message => {
169331
+ try {
169332
+ activityRow(JSON.parse(message.data))
169333
+ } catch (_) {}
169334
+ }
169335
+
169336
+ // ----------------------------------------------------------------
169337
+ // Logs: the handlers' own console output, live - the full feed
169338
+ // (incl. the dev server stream) lives on the Logs tab.
169339
+
169340
+ const logsFeed = attachLogFeed(main, 'worker', undefined, 'Logs')
169341
+
169342
+ cleanupPanel = () => {
169343
+ clearInterval(uptimeTimer)
169344
+ healthSource.close()
169345
+ problemSource.close()
169346
+ activitySource.close()
169347
+ logsFeed?.()
168779
169348
  }
168780
169349
  }
168781
169350
 
@@ -168796,14 +169365,15 @@ try {
168796
169365
  document.title = state.app + ' \xB7 awsless dev'
168797
169366
  state.resources = [
168798
169367
  ...state.resources,
168799
- // Proxy routes carry no route key & are already listed by
168800
- // the feature that owns them.
168801
- ...state.routes.filter(route => route.routeKey).map(route => ({
169368
+ // The full route table per router, like the deployed router: both
169369
+ // bundle routes & proxy mounts (site dev servers, the pubsub
169370
+ // websocket). Only the raw per-file static asset routes stay out.
169371
+ ...state.routes.filter(route => !route.rawKey).map(route => ({
168802
169372
  kind: 'route',
168803
169373
  id: route.pattern,
168804
- stack: route.routeKey.split(':')[0],
169374
+ stack: route.routeKey ? route.routeKey.split(':')[0] : undefined,
168805
169375
  router: route.routerId,
168806
- detail: route.routeKey,
169376
+ detail: route.routeKey ?? ('proxy \u2192 ' + route.proxy),
168807
169377
  url: 'http://localhost:' + state.routerPorts[route.routerId] + route.pattern.split('{')[0].replace(/\\*$/, ''),
168808
169378
  })),
168809
169379
  ]
@@ -168863,7 +169433,9 @@ var createDashboardServer = (props) => {
168863
169433
  routerPorts: props.routerPorts,
168864
169434
  resources: props.resources,
168865
169435
  routes: props.routes,
168866
- seeds: Boolean(props.runSeeds)
169436
+ seeds: Boolean(props.runSeeds),
169437
+ session: props.getSession?.() ?? null,
169438
+ health: props.getHealth?.() ?? []
168867
169439
  }).replaceAll("<", "\\u003c");
168868
169440
  return { status: 200, body: dashboardHtml.replace("__STATE__", () => state), type: "text/html" };
168869
169441
  }
@@ -168881,7 +169453,7 @@ var createDashboardServer = (props) => {
168881
169453
  EventVersion: "1.0",
168882
169454
  Sns: {
168883
169455
  Type: "Notification",
168884
- MessageId: randomUUID8(),
169456
+ MessageId: randomUUID9(),
168885
169457
  TopicArn: `arn:aws:sns:${props.region}:000000000000:${topic}`,
168886
169458
  Message: typeof message4 === "string" ? message4 : JSON.stringify(message4 ?? {}),
168887
169459
  Timestamp: new Date().toISOString(),
@@ -169032,6 +169604,9 @@ var createDashboardServer = (props) => {
169032
169604
  if (url.pathname === "/api/emails") {
169033
169605
  return { status: 200, body: JSON.stringify({ emails: props.getEmails?.() ?? [] }) };
169034
169606
  }
169607
+ if (url.pathname === "/api/alerts") {
169608
+ return { status: 200, body: JSON.stringify({ alerts: props.getAlerts?.() ?? [] }) };
169609
+ }
169035
169610
  if (url.pathname.startsWith("/api/auth") && props.auth) {
169036
169611
  const auth2 = props.auth;
169037
169612
  try {
@@ -169145,13 +169720,13 @@ var createDashboardServer = (props) => {
169145
169720
  // src/dev/failure.ts
169146
169721
  import { stringify as stringify5 } from "@awsless/json";
169147
169722
  import { formatRouteKey as formatRouteKey2, formatRoutePayload as formatRoutePayload2 } from "awsless";
169148
- import { randomUUID as randomUUID9 } from "crypto";
169723
+ import { randomUUID as randomUUID10 } from "crypto";
169149
169724
  var CONSUMER_ROUTE = formatRouteKey2("base", "on-failure", "consumer");
169150
169725
  var createFailureReporter = (props) => {
169151
169726
  const format4 = (report) => {
169152
169727
  const error3 = report.error;
169153
169728
  const base = {
169154
- id: randomUUID9(),
169729
+ id: randomUUID10(),
169155
169730
  date: new Date,
169156
169731
  payload: report.event
169157
169732
  };
@@ -169191,6 +169766,7 @@ var createFailureReporter = (props) => {
169191
169766
  return (report) => {
169192
169767
  const message4 = report.error instanceof Error ? report.error.message : String(report.error);
169193
169768
  props.log(`${report.routeKey ?? report.queue?.name ?? "async invoke"} failed: ${message4}`);
169769
+ props.onReport?.(report);
169194
169770
  if (!props.enabled) {
169195
169771
  return;
169196
169772
  }
@@ -169383,6 +169959,7 @@ var startDev = async (props) => {
169383
169959
  const log = props.onLog ?? (() => {});
169384
169960
  const phase = props.phase ?? (async ({ start }, fn) => (log(start), fn(() => {})));
169385
169961
  const { appConfig, stackConfigs } = props;
169962
+ const startedAt = Date.now();
169386
169963
  useDevBuildDir();
169387
169964
  const { builders, appId, ready: ready2 } = createApp({
169388
169965
  appConfig,
@@ -169400,6 +169977,9 @@ var startDev = async (props) => {
169400
169977
  props.pool.begin();
169401
169978
  await props.pool.keep("session", null, async () => ({ value: true, stop: () => {} }));
169402
169979
  const dev = createDevContext({ appConfig, stackConfigs, appId, routerPorts, log, pool: props.pool });
169980
+ setDebugSink((type2, message4) => {
169981
+ dev.events.emit("debug", { date: Date.now(), line: message4, error: type2 === "error" });
169982
+ });
169403
169983
  const bundleName = getBundleFunctionName(appConfig.name);
169404
169984
  const buildDir = getBuildPath("bundle", bundleName, ".");
169405
169985
  const { env: env4, lambda } = await phase({ start: "Preparing the local resources...", done: "Prepared the local resources" }, async (detail) => {
@@ -169511,12 +170091,14 @@ var startDev = async (props) => {
169511
170091
  restartNeeded = true;
169512
170092
  await worker.restart();
169513
170093
  restartNeeded = false;
170094
+ dev.context.reportHealth("workers", "up", String(worker.size()));
169514
170095
  log(`Reloaded the bundle in ${Date.now() - started}ms (build ${built - started}ms, worker ${Date.now() - built}ms)`);
169515
170096
  } else if (!changed) {
169516
170097
  debug(`Rebuild found no changes in ${Date.now() - started}ms`);
169517
170098
  }
169518
170099
  } catch (error3) {
169519
170100
  dirty = true;
170101
+ dev.context.reportHealth("workers", "down", error3 instanceof Error ? error3.message : String(error3));
169520
170102
  throw error3;
169521
170103
  } finally {
169522
170104
  fresh = undefined;
@@ -169524,14 +170106,62 @@ var startDev = async (props) => {
169524
170106
  })();
169525
170107
  await fresh;
169526
170108
  };
170109
+ const describeDispatch = (event) => {
170110
+ const payload = event;
170111
+ const route = payload?.["$awsless-route"];
170112
+ if (typeof route === "string") {
170113
+ return route;
170114
+ }
170115
+ const record = payload?.Records?.[0];
170116
+ if (record?.eventSource === "aws:sqs") {
170117
+ return "queue " + String(record.eventSourceARN ?? "").split(":").at(-1);
170118
+ }
170119
+ if (record?.EventSource === "aws:sns") {
170120
+ return "topic " + String(record.Sns?.TopicArn ?? "").split(":").at(-1);
170121
+ }
170122
+ if (record?.eventSource === "aws:s3") {
170123
+ return "store " + String(record.s3?.bucket?.name ?? "");
170124
+ }
170125
+ if (String(record?.eventSourceARN ?? "").includes(":dynamodb:")) {
170126
+ return "stream " + (String(record.eventSourceARN).split("table/")[1]?.split("/")[0] ?? "table");
170127
+ }
170128
+ const header = payload?.headers?.[ROUTE_HEADER];
170129
+ if (typeof header === "string") {
170130
+ return header;
170131
+ }
170132
+ return "invoke";
170133
+ };
169527
170134
  const dispatch = async (event) => {
169528
170135
  await ensureFresh();
169529
- return worker.dispatch(event);
170136
+ const started = Date.now();
170137
+ const route = describeDispatch(event);
170138
+ try {
170139
+ const result = await worker.dispatch(event);
170140
+ dev.events.emit("activity", { date: started, route, ms: Date.now() - started, ok: true });
170141
+ return result;
170142
+ } catch (error3) {
170143
+ dev.events.emit("activity", {
170144
+ date: started,
170145
+ route,
170146
+ ms: Date.now() - started,
170147
+ ok: false,
170148
+ error: error3 instanceof Error ? error3.message : String(error3)
170149
+ });
170150
+ throw error3;
170151
+ }
169530
170152
  };
169531
170153
  const reportFailure = createFailureReporter({
169532
170154
  enabled: Boolean(appConfig.onFailure),
169533
170155
  dispatch,
169534
- log
170156
+ log,
170157
+ onReport(report) {
170158
+ dev.events.emit("problems", {
170159
+ date: Date.now(),
170160
+ kind: report.kind,
170161
+ title: report.routeKey ?? report.queue?.name ?? "async invoke",
170162
+ detail: report.error instanceof Error ? report.error.message : String(report.error)
170163
+ });
170164
+ }
169535
170165
  });
169536
170166
  lambda.connect(dispatch, reportFailure);
169537
170167
  await phase({ start: "Starting the local servers...", done: "Started the local servers" }, async (detail) => {
@@ -169546,9 +170176,11 @@ var startDev = async (props) => {
169546
170176
  await phase({ start: "Starting the bundle worker...", done: "Started the bundle worker" }, async (detail) => {
169547
170177
  try {
169548
170178
  await worker.start();
170179
+ dev.context.reportHealth("workers", "up", String(worker.size()));
169549
170180
  } catch (error3) {
169550
170181
  detail("FAILED - the next invoke retries");
169551
170182
  log(`The bundle worker failed to start: ${error3 instanceof Error ? error3.message : String(error3)}`);
170183
+ dev.context.reportHealth("workers", "down", error3 instanceof Error ? error3.message : String(error3));
169552
170184
  dirty = true;
169553
170185
  restartNeeded = true;
169554
170186
  }
@@ -169583,6 +170215,12 @@ var startDev = async (props) => {
169583
170215
  process.stderr.write(`Route ${routeKey} failed: ${detail}
169584
170216
  `);
169585
170217
  emitWorkerLine(`Route ${routeKey} failed: ${detail}`, true, routeKey);
170218
+ dev.events.emit("problems", {
170219
+ date: Date.now(),
170220
+ kind: "route",
170221
+ title: routeKey,
170222
+ detail: error3 instanceof Error ? error3.message : String(error3)
170223
+ });
169586
170224
  }
169587
170225
  }));
169588
170226
  }
@@ -169601,6 +170239,9 @@ var startDev = async (props) => {
169601
170239
  storeRoot: join57(directories.output, "local", "store"),
169602
170240
  configFile: join57(directories.output, "local", "config.json"),
169603
170241
  getEmails: () => props.pool.peek("shim:ses-email")?.server.list() ?? [],
170242
+ getAlerts: () => props.pool.peek("shim:sns")?.alerts ?? [],
170243
+ getSession: () => ({ startedAt, workers: worker.size() }),
170244
+ getHealth: () => [...dev.health.values()],
169604
170245
  configPulled: Object.keys(props.pool.peek("config:pull") ?? {}),
169605
170246
  auth: createAuthAdmin({
169606
170247
  appConfig,
@@ -169630,9 +170271,13 @@ var startDev = async (props) => {
169630
170271
  if (stopping) {
169631
170272
  return;
169632
170273
  }
169633
- worker.restart().then(() => log("Restarted the bundle worker.")).catch((error3) => {
170274
+ worker.restart().then(() => {
170275
+ dev.context.reportHealth("workers", "up", String(worker.size()));
170276
+ log("Restarted the bundle worker.");
170277
+ }).catch((error3) => {
169634
170278
  restartNeeded = true;
169635
170279
  dirty = true;
170280
+ dev.context.reportHealth("workers", "down", error3 instanceof Error ? error3.message : String(error3));
169636
170281
  log(`The bundle worker failed to restart: ${error3 instanceof Error ? error3.message : String(error3)}`);
169637
170282
  });
169638
170283
  });
@@ -177005,7 +177650,7 @@ ${job.error.stack.split(`
177005
177650
  stream2.shift();
177006
177651
  } else if (stream2.length > 0) {
177007
177652
  this._BOMseen = true;
177008
- }
177653
+ } else {}
177009
177654
  }
177010
177655
  return codePointsToString(stream2);
177011
177656
  }
@@ -185685,7 +186330,7 @@ var formatLog = (level, date2, origin, message4) => {
185685
186330
  import { DeleteObjectsCommand, ListObjectsV2Command, S3Client as S3Client5 } from "@aws-sdk/client-s3";
185686
186331
 
185687
186332
  // src/formation/cloudfront.ts
185688
- import { randomUUID as randomUUID10 } from "crypto";
186333
+ import { randomUUID as randomUUID11 } from "crypto";
185689
186334
  var createInvalidationForDistributionTenants = async (client, props) => {
185690
186335
  let cursor;
185691
186336
  do {
@@ -185705,7 +186350,7 @@ var createInvalidationForDistributionTenants = async (client, props) => {
185705
186350
  Quantity: props.paths.length,
185706
186351
  Items: props.paths
185707
186352
  },
185708
- CallerReference: randomUUID10()
186353
+ CallerReference: randomUUID11()
185709
186354
  }
185710
186355
  }));
185711
186356
  }