wawesome 0.0.1 → 0.0.5

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 (3) hide show
  1. package/README.md +193 -0
  2. package/dist/index.mjs +507 -0
  3. package/package.json +3 -3
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # 🚀 wawesome
2
+
3
+ > Official CLI for building, bundling, and deploying serverless WebAssembly functions on the
4
+ > **[wawesome.io](https://wawesome.io)** platform.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/wawesome.svg?color=cyan)](https://www.npmjs.com/package/wawesome)
7
+
8
+ Deploy ultra-fast, lightweight serverless WebAssembly functions directly from your terminal in under 60 seconds.
9
+
10
+ ---
11
+
12
+ ## ⚡ Quick Start
13
+
14
+ ### 1. Create an account
15
+
16
+ Sign up for free at **[https://wawesome.io](https://wawesome.io)** to set up your workspace.
17
+
18
+ ### 2. Authenticate CLI
19
+
20
+ Login via browser OAuth:
21
+
22
+ ```bash
23
+ npx wawesome login
24
+ ```
25
+
26
+ ### 3. Initialize a project
27
+
28
+ Create a new directory and scaffold a WebAssembly TypeScript starter function:
29
+
30
+ ```bash
31
+ mkdir my-wasm-app && cd my-wasm-app
32
+ npx wawesome init
33
+ ```
34
+
35
+ ### 4. Build & Deploy
36
+
37
+ Deploy your serverless function to Wawesome Cloud instantly:
38
+
39
+ ```bash
40
+ npx wawesome deploy
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 📖 Command Reference
46
+
47
+ | Command | Description |
48
+ |:----------------------------------------|:--------------------------------------------------------------------|
49
+ | `npx wawesome login` | Authenticate CLI with your Wawesome account via browser |
50
+ | `npx wawesome logout` | Log out and clear saved credentials from your machine |
51
+ | `npx wawesome whoami` | View current logged-in user, workspace, and gateway info |
52
+ | `npx wawesome init` | Scaffold a new serverless function project in the current directory |
53
+ | `npx wawesome build` | Bundle TypeScript entry code into an optimized JS bundle |
54
+ | `npx wawesome deploy` | Build, upload, and promote a function version to production |
55
+ | `npx wawesome logs [func]` | List recent past invocations for a function |
56
+ | `npx wawesome logs --invocation <id>` | Fetch full stdout/stderr log body for a specific invocation |
57
+ | `npx wawesome logs <func> --follow` | Follow live output (waits for the next invocation if needed) |
58
+ | `npx wawesome version list` | List version history for the current function |
59
+ | `npx wawesome version switch <v>` | Roll back or promote a specific function version |
60
+ | `npx wawesome env list` | View environment variables for the current app |
61
+ | `npx wawesome env set <key> <val>` | Set an environment variable (add `--secret` for write-only) |
62
+ | `npx wawesome env rm <key>` | Delete an environment variable |
63
+
64
+ ---
65
+
66
+ ## 📜 Invocation Logs
67
+
68
+ Inspect past function runs or view raw `stdout` / `stderr` log outputs directly in your terminal.
69
+
70
+ ### 1. List Recent Invocations
71
+
72
+ List past executions (including status, trigger type, timestamp, and duration) for the function in the
73
+ current directory:
74
+
75
+ ```bash
76
+ npx wawesome logs
77
+ ```
78
+
79
+ Or list invocations for a specific function by name:
80
+
81
+ ```bash
82
+ npx wawesome logs my-function
83
+ ```
84
+
85
+ Filter by invocation status — only show errors, timeouts, etc.:
86
+
87
+ ```bash
88
+ npx wawesome logs my-function --error
89
+ npx wawesome logs my-function --status timeout
90
+ npx wawesome logs my-function --running
91
+ ```
92
+
93
+ ### 2. View Invocation Log Body (`stdout`/`stderr`)
94
+
95
+ Fetch and print the captured `console.log` / `console.error` text for a specific invocation:
96
+
97
+ ```bash
98
+ npx wawesome logs --invocation 019fb344-ea0c-78f2-8a9b-d04e188b9823
99
+ ```
100
+
101
+ Or pass the UUID directly as the target:
102
+
103
+ ```bash
104
+ npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b9823
105
+ ```
106
+
107
+ ### 3. Follow Live Output (`--follow`)
108
+
109
+ Stream an invocation's output as it runs — like `tail -f` for your serverless function.
110
+
111
+ #### Follow by function name (recommended)
112
+
113
+ ```bash
114
+ npx wawesome logs my-function --follow
115
+ ```
116
+
117
+ If the function is currently running, its output is streamed immediately. If the latest invocation
118
+ already finished, the CLI **waits for the next invocation** to start and then streams it live.
119
+ Press `Ctrl-C` at any time to stop.
120
+
121
+ #### Follow a specific invocation by ID
122
+
123
+ ```bash
124
+ npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b9823 --follow
125
+ ```
126
+
127
+ #### Reconnection
128
+
129
+ On transient network errors or server issues (5xx), the CLI automatically reconnects with
130
+ exponential back-off (up to 3 retries). Non-recoverable errors like authentication failures
131
+ (401) or unknown invocations (404) exit immediately with a clear message.
132
+
133
+ ---
134
+
135
+ ## ⚙️ Configuration & Custom Gateway
136
+
137
+ ### `wawesome-function.json`
138
+
139
+ Every project directory includes a `wawesome-function.json` file generated during `npx wawesome init`:
140
+
141
+ ```json
142
+ {
143
+ "app": "my-app",
144
+ "function": "hello-world",
145
+ "entry": "src/index.ts"
146
+ }
147
+ ```
148
+
149
+ ### Local Development / Gateway Overrides
150
+
151
+ If you are running a local gateway or self-hosted instance, you can configure your CLI Gateway URL using any of the
152
+ following:
153
+
154
+ #### 1. Custom Settings (`~/.wawesome/settings.json`)
155
+
156
+ Create `~/.wawesome/settings.json`:
157
+
158
+ ```json
159
+ {
160
+ "gateway_url": "http://localhost:3000"
161
+ }
162
+ ```
163
+
164
+ #### 2. Environment Variables
165
+
166
+ ```bash
167
+ export WAWESOME_GATEWAY_URL="http://localhost:3000"
168
+ ```
169
+
170
+ #### 3. CLI Flag
171
+
172
+ ```bash
173
+ npx wawesome login --gateway http://localhost:3000
174
+ ```
175
+
176
+ ---
177
+
178
+ ## 🔒 Security & Secrets
179
+
180
+ Wawesome encrypts environment variables at rest using two-tier envelope encryption (AES-256-GCM with per-app data keys
181
+ and AAD context binding). Use `--secret` when setting sensitive keys:
182
+
183
+ ```bash
184
+ npx wawesome env set STRIPE_SECRET_KEY sk_live_xxx --secret
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 🌐 Resources & Support
190
+
191
+ - **Platform Homepage**: [https://wawesome.io](https://wawesome.io)
192
+ - **Documentation**: [https://docs.wawesome.io](https://docs.wawesome.io)
193
+
package/dist/index.mjs CHANGED
@@ -837,6 +837,492 @@ async function envCommand(action, key, value, options) {
837
837
  process.exit(1);
838
838
  }
839
839
  //#endregion
840
+ //#region src/logs.ts
841
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
842
+ function isUuid(str) {
843
+ return UUID_REGEX.test(str);
844
+ }
845
+ function formatDuration(startedAt, endedAt) {
846
+ if (!endedAt) return "-";
847
+ const start = new Date(startedAt).getTime();
848
+ const end = new Date(endedAt).getTime();
849
+ if (isNaN(start) || isNaN(end)) return "-";
850
+ const durationMs = end - start;
851
+ if (durationMs < 0) return "-";
852
+ if (durationMs < 1e3) return `${durationMs}ms`;
853
+ return `${(durationMs / 1e3).toFixed(2)}s`;
854
+ }
855
+ function formatDate(dateStr) {
856
+ const d = new Date(dateStr);
857
+ if (isNaN(d.getTime())) return dateStr;
858
+ return d.toISOString().replace("T", " ").slice(0, 19);
859
+ }
860
+ function colorizeStatus(text, status) {
861
+ switch (status.toLowerCase()) {
862
+ case "success": return `\x1b[32m${text}\x1b[0m`;
863
+ case "error": return `\x1b[31m${text}\x1b[0m`;
864
+ case "timeout": return `\x1b[33m${text}\x1b[0m`;
865
+ case "running": return `\x1b[36m${text}\x1b[0m`;
866
+ default: return text;
867
+ }
868
+ }
869
+ /**
870
+ * Main handler for `wawesome logs [target] [--invocation <id>] [--app <app>]`
871
+ */
872
+ async function logsCommand(target, options = {}) {
873
+ const isVerbose = Boolean(options.verbose);
874
+ const creds = readCredentials();
875
+ if (!creds) {
876
+ console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
877
+ process.exit(1);
878
+ }
879
+ let invocationId = options.invocation;
880
+ let funcNameInput;
881
+ if (!invocationId && target && isUuid(target)) invocationId = target;
882
+ else if (target && !isUuid(target)) funcNameInput = target;
883
+ let statusFilter = options.status;
884
+ if (!statusFilter) {
885
+ if (options.success) statusFilter = "success";
886
+ else if (options.error) statusFilter = "error";
887
+ else if (options.timeout) statusFilter = "timeout";
888
+ else if (options.running) statusFilter = "running";
889
+ }
890
+ if (options.follow) {
891
+ if (invocationId) return followInvocationLog(creds.gateway_url, creds.tenant_jwt, invocationId, isVerbose);
892
+ return followFunctionLog(creds.gateway_url, creds.tenant_jwt, funcNameInput, options.app, isVerbose);
893
+ }
894
+ if (invocationId) return fetchInvocationLogBody(creds.gateway_url, creds.tenant_jwt, invocationId, isVerbose);
895
+ else return listInvocations(creds.gateway_url, creds.tenant_jwt, funcNameInput, options.app, statusFilter, isVerbose);
896
+ }
897
+ /**
898
+ * Fetch and display raw log body for a single invocation.
899
+ */
900
+ async function fetchInvocationLogBody(gatewayUrl, tenantJwt, invocationId, isVerbose) {
901
+ const url = `${gatewayUrl}/v1/invocations/${encodeURIComponent(invocationId)}/logs`;
902
+ if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
903
+ const res = await fetch(url, {
904
+ method: "GET",
905
+ headers: { Authorization: `Bearer ${tenantJwt}` }
906
+ });
907
+ if (!res.ok) {
908
+ const errorText = await res.text();
909
+ if (res.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
910
+ else if (res.status === 404) console.error(`[wawesome] Error: Invocation log '${invocationId}' not found or expired (logs are retained for 14 days).`);
911
+ else if (res.status === 400) console.error(`[wawesome] Error: Invalid invocation ID '${invocationId}'.`);
912
+ else {
913
+ console.error(`[wawesome] Error: Failed to fetch invocation logs (HTTP ${res.status}).`);
914
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorText}`);
915
+ }
916
+ process.exit(1);
917
+ }
918
+ const body = await res.text();
919
+ process.stdout.write(body.endsWith("\n") ? body : body + "\n");
920
+ }
921
+ /**
922
+ * List recent invocations for a function.
923
+ */
924
+ async function listInvocations(gatewayUrl, tenantJwt, funcNameInput, appOverride, statusFilter, isVerbose) {
925
+ const config = readFunctionConfig();
926
+ const appSlug = appOverride || config?.app;
927
+ const funcName = funcNameInput || config?.function;
928
+ if (!funcName) {
929
+ console.error("[wawesome] Error: Missing function name or invocation ID.");
930
+ console.error("[wawesome] Usage: wawesome logs <function-name-or-id> or run inside a function directory with wawesome-function.json.");
931
+ process.exit(1);
932
+ }
933
+ const queryParams = new URLSearchParams();
934
+ queryParams.set("limit", "50");
935
+ if (statusFilter && statusFilter.toLowerCase() !== "all") queryParams.set("status", statusFilter.toLowerCase());
936
+ const res = await fetchInvocationsResponse(gatewayUrl, tenantJwt, funcName, appSlug, appOverride, `?${queryParams.toString()}`, isVerbose);
937
+ if (!res.ok) {
938
+ const errorText = await res.text();
939
+ if (res.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
940
+ else if (res.status === 404) console.error(`[wawesome] Error: Function '${funcName}' not found.`);
941
+ else {
942
+ console.error(`[wawesome] Error: Failed to list invocations (HTTP ${res.status}).`);
943
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorText}`);
944
+ }
945
+ process.exit(1);
946
+ }
947
+ const data = await res.json();
948
+ if (!data.invocations || data.invocations.length === 0) {
949
+ console.log(`[wawesome] No invocations found for function '${funcName}'.`);
950
+ return;
951
+ }
952
+ const displayTarget = appSlug ? `${appSlug}/${funcName}` : funcName;
953
+ const totalRecords = data.total ?? data.invocations.length;
954
+ const shownRecords = data.invocations.length;
955
+ console.log(`\n📜 \x1b[1mInvocations for '${displayTarget}' (Showing ${shownRecords} of ${totalRecords} records)\x1b[0m\n`);
956
+ console.log("INVOCATION ID | STATUS | TRIGGER | STARTED AT | DURATION ");
957
+ console.log("------------------------------------|-----------|---------|---------------------|----------");
958
+ for (const inv of data.invocations) {
959
+ const idStr = inv.id.padEnd(36);
960
+ const statusStr = colorizeStatus(inv.status.padEnd(9), inv.status);
961
+ const triggerStr = inv.trigger_type.padEnd(7);
962
+ const dateStr = formatDate(inv.started_at).padEnd(19);
963
+ const durationStr = formatDuration(inv.started_at, inv.ended_at).padEnd(9);
964
+ console.log(`${idStr} | ${statusStr} | ${triggerStr} | ${dateStr} | ${durationStr}`);
965
+ }
966
+ console.log("\nTo view logs for a specific invocation, run:\n wawesome logs --invocation <id>\n");
967
+ console.log("To follow live output (waits for the next invocation if none is running):\n wawesome logs <function-name> --follow\n");
968
+ console.log("To follow a specific invocation:\n wawesome logs --invocation <id> --follow\n");
969
+ }
970
+ /**
971
+ * Fetch a Function's invocations, transparently falling back from the app-scoped
972
+ * route to the unscoped (default-app) route on a 404 when `--app` wasn't given.
973
+ * Shared by the list view and the `--follow` latest-invocation resolver so both
974
+ * hit the same routes and fallback behaviour.
975
+ */
976
+ async function fetchInvocationsResponse(gatewayUrl, tenantJwt, funcName, appSlug, appOverride, queryString, isVerbose) {
977
+ const url = appSlug ? `${gatewayUrl}/v1/apps/${encodeURIComponent(appSlug)}/functions/${encodeURIComponent(funcName)}/invocations${queryString}` : `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/invocations${queryString}`;
978
+ if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
979
+ let res = await fetch(url, {
980
+ method: "GET",
981
+ headers: { Authorization: `Bearer ${tenantJwt}` }
982
+ });
983
+ if (res.status === 404 && appSlug && !appOverride) {
984
+ const fallbackUrl = `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/invocations${queryString}`;
985
+ if (isVerbose) console.log(`[wawesome:verbose] 404 on app-scoped route. Retrying unscoped: GET ${fallbackUrl}`);
986
+ try {
987
+ const fallbackRes = await fetch(fallbackUrl, {
988
+ method: "GET",
989
+ headers: { Authorization: `Bearer ${tenantJwt}` }
990
+ });
991
+ if (fallbackRes) res = fallbackRes;
992
+ } catch {}
993
+ }
994
+ return res;
995
+ }
996
+ /**
997
+ * Split a growing SSE buffer into complete event `data` payloads, returning the
998
+ * still-incomplete remainder. Each SSE event is terminated by a blank line; its
999
+ * `data:` field lines are rejoined with newlines (so a multi-line NDJSON chunk
1000
+ * survives intact). Comment/keep-alive events (no `data:` line) are dropped.
1001
+ */
1002
+ function splitSseEvents(buffer) {
1003
+ const events = [];
1004
+ let idx;
1005
+ while ((idx = buffer.indexOf("\n\n")) !== -1) {
1006
+ const rawEvent = buffer.slice(0, idx);
1007
+ buffer = buffer.slice(idx + 2);
1008
+ const data = rawEvent.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
1009
+ if (data) events.push(data);
1010
+ }
1011
+ return {
1012
+ events,
1013
+ rest: buffer
1014
+ };
1015
+ }
1016
+ /**
1017
+ * Print one NDJSON log line the way `logs` does, colorizing `stderr` records
1018
+ * red. Falls back to the raw line if it isn't the expected structured shape.
1019
+ */
1020
+ function printFollowLine(line) {
1021
+ const trimmed = line.trim();
1022
+ if (!trimmed) return;
1023
+ try {
1024
+ const entry = JSON.parse(trimmed);
1025
+ if (entry && typeof entry.msg === "string") {
1026
+ if (entry.stream === "stderr") process.stdout.write(`\x1b[31m${entry.msg}\x1b[0m\n`);
1027
+ else process.stdout.write(`${entry.msg}\n`);
1028
+ return;
1029
+ }
1030
+ } catch {}
1031
+ process.stdout.write(trimmed + "\n");
1032
+ }
1033
+ /** Maximum number of reconnection attempts on transient errors. */
1034
+ const MAX_RETRIES = 3;
1035
+ /** Base delay in milliseconds for exponential back-off (1 s → 2 s → 4 s). */
1036
+ const BASE_RETRY_DELAY_MS = 1e3;
1037
+ /**
1038
+ * Sleep for `ms` milliseconds, respecting an `AbortSignal` so Ctrl-C doesn't
1039
+ * hang during back-off waits.
1040
+ */
1041
+ function retrySleep(ms, signal) {
1042
+ return new Promise((resolve, reject) => {
1043
+ if (signal.aborted) {
1044
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1045
+ return;
1046
+ }
1047
+ const timer = setTimeout(resolve, ms);
1048
+ const onAbort = () => {
1049
+ clearTimeout(timer);
1050
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1051
+ };
1052
+ signal.addEventListener("abort", onAbort, { once: true });
1053
+ });
1054
+ }
1055
+ /**
1056
+ * Calculate exponential back-off delay with jitter for a given attempt.
1057
+ * attempt 0 → ~1 s, attempt 1 → ~2 s, attempt 2 → ~4 s.
1058
+ */
1059
+ function retryDelay(attempt) {
1060
+ const base = BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
1061
+ return base + Math.random() * base * .25;
1062
+ }
1063
+ /**
1064
+ * Returns `true` for errors that are worth retrying: network-level failures
1065
+ * (fetch throws) and 5xx server errors. Client errors (4xx) are not transient.
1066
+ */
1067
+ function isTransientError(res) {
1068
+ if (!res) return true;
1069
+ return res.status >= 500;
1070
+ }
1071
+ /**
1072
+ * Follow a single invocation's output live over the SSE endpoint, printing each
1073
+ * flushed chunk's lines as they arrive. The connection is consumed with `fetch`
1074
+ * + a streaming body reader (rather than `EventSource`) so the tenant `Bearer`
1075
+ * token can be sent.
1076
+ *
1077
+ * On transient network errors or 5xx responses, the connection is retried with
1078
+ * exponential back-off (max 3 retries). Non-retriable errors (401, 404, 400)
1079
+ * exit immediately.
1080
+ *
1081
+ * Returns when the invocation finishes, the server closes the stream at its
1082
+ * max-duration cap, or the user interrupts with Ctrl-C.
1083
+ */
1084
+ async function followInvocationLog(gatewayUrl, tenantJwt, invocationId, isVerbose) {
1085
+ const url = `${gatewayUrl}/v1/invocations/${encodeURIComponent(invocationId)}/logs/stream`;
1086
+ if (isVerbose) console.log(`[wawesome:verbose] GET ${url} (SSE)`);
1087
+ const controller = new AbortController();
1088
+ const onSigint = () => {
1089
+ controller.abort();
1090
+ process.stderr.write("\n[wawesome] Stopped following.\n");
1091
+ process.exit(0);
1092
+ };
1093
+ process.on("SIGINT", onSigint);
1094
+ try {
1095
+ let attempt = 0;
1096
+ while (true) {
1097
+ let res = null;
1098
+ try {
1099
+ res = await fetch(url, {
1100
+ method: "GET",
1101
+ headers: {
1102
+ Authorization: `Bearer ${tenantJwt}`,
1103
+ Accept: "text/event-stream"
1104
+ },
1105
+ signal: controller.signal
1106
+ });
1107
+ } catch (err) {
1108
+ if (controller.signal.aborted) throw err;
1109
+ if (attempt < MAX_RETRIES) {
1110
+ const delay = retryDelay(attempt);
1111
+ if (isVerbose) console.error(`[wawesome:verbose] Connection failed (${String(err)}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${MAX_RETRIES})...`);
1112
+ else console.error(`[wawesome] Connection lost, reconnecting (${attempt + 1}/${MAX_RETRIES})...`);
1113
+ await retrySleep(delay, controller.signal);
1114
+ attempt++;
1115
+ continue;
1116
+ }
1117
+ console.error("[wawesome] Error: Failed to connect to the live tail after retries.");
1118
+ if (isVerbose) console.error(`[wawesome:verbose] ${String(err)}`);
1119
+ process.exit(1);
1120
+ }
1121
+ if (res.status === 401) {
1122
+ console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
1123
+ process.exit(1);
1124
+ }
1125
+ if (res.status === 404) {
1126
+ console.error(`[wawesome] Error: Invocation '${invocationId}' not found or expired (logs are retained for 14 days).`);
1127
+ process.exit(1);
1128
+ }
1129
+ if (res.status === 400) {
1130
+ console.error(`[wawesome] Error: Invalid invocation ID '${invocationId}'.`);
1131
+ process.exit(1);
1132
+ }
1133
+ if (!res.ok && isTransientError(res)) {
1134
+ if (attempt < MAX_RETRIES) {
1135
+ const delay = retryDelay(attempt);
1136
+ if (isVerbose) console.error(`[wawesome:verbose] Server error (HTTP ${res.status}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${MAX_RETRIES})...`);
1137
+ else console.error(`[wawesome] Server error, reconnecting (${attempt + 1}/${MAX_RETRIES})...`);
1138
+ await retrySleep(delay, controller.signal);
1139
+ attempt++;
1140
+ continue;
1141
+ }
1142
+ console.error(`[wawesome] Error: Failed to open live tail (HTTP ${res.status}) after retries.`);
1143
+ process.exit(1);
1144
+ }
1145
+ if (!res.ok) {
1146
+ console.error(`[wawesome] Error: Failed to open live tail (HTTP ${res.status}).`);
1147
+ process.exit(1);
1148
+ }
1149
+ if (!res.body) {
1150
+ console.error("[wawesome] Error: Live tail response had no body stream.");
1151
+ process.exit(1);
1152
+ }
1153
+ attempt = 0;
1154
+ if (isVerbose) console.error(`[wawesome:verbose] Connected to SSE stream.`);
1155
+ console.error(`[wawesome] 📡 Following invocation ${invocationId} (Ctrl-C to stop)...`);
1156
+ try {
1157
+ const reader = res.body.getReader();
1158
+ const decoder = new TextDecoder();
1159
+ let buffer = "";
1160
+ for (;;) {
1161
+ const { value, done } = await reader.read();
1162
+ if (done) break;
1163
+ buffer += decoder.decode(value, { stream: true });
1164
+ const { events, rest } = splitSseEvents(buffer);
1165
+ buffer = rest;
1166
+ for (const chunk of events) for (const line of chunk.split("\n")) printFollowLine(line);
1167
+ }
1168
+ console.error(`[wawesome] ✔ Live tail ended for ${invocationId}.`);
1169
+ return;
1170
+ } catch (streamErr) {
1171
+ if (controller.signal.aborted) throw streamErr;
1172
+ if (attempt < MAX_RETRIES) {
1173
+ const delay = retryDelay(attempt);
1174
+ if (isVerbose) console.error(`[wawesome:verbose] Stream interrupted (${String(streamErr)}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${MAX_RETRIES})...`);
1175
+ else console.error(`[wawesome] Stream interrupted, reconnecting (${attempt + 1}/${MAX_RETRIES})...`);
1176
+ await retrySleep(delay, controller.signal);
1177
+ attempt++;
1178
+ continue;
1179
+ }
1180
+ console.error("[wawesome] Error: Live tail stream interrupted and retries exhausted.");
1181
+ if (isVerbose) console.error(`[wawesome:verbose] ${String(streamErr)}`);
1182
+ process.exit(1);
1183
+ }
1184
+ }
1185
+ } finally {
1186
+ process.removeListener("SIGINT", onSigint);
1187
+ }
1188
+ }
1189
+ /**
1190
+ * Follow a whole Function's live output over SSE: the gateway streams output from
1191
+ * every invocation of the Function as it happens, across invocation boundaries,
1192
+ * so this is a continuous `tail -f` for the Function rather than one run.
1193
+ *
1194
+ * Resolves the Function from the `funcNameInput` argument or the
1195
+ * `wawesome-function.json` in the current directory, prefers the App-scoped route
1196
+ * and falls back to the unscoped (default-app) route on a 404, and reconnects
1197
+ * automatically when the server closes the stream at its max-duration cap so the
1198
+ * terminal keeps following. Ctrl-C stops. Auth/'function not found' errors exit
1199
+ * immediately; transient network/5xx errors retry with exponential back-off.
1200
+ */
1201
+ async function followFunctionLog(gatewayUrl, tenantJwt, funcNameInput, appOverride, isVerbose) {
1202
+ const config = readFunctionConfig();
1203
+ const appSlug = appOverride || config?.app;
1204
+ const funcName = funcNameInput || config?.function;
1205
+ if (!funcName) {
1206
+ console.error("[wawesome] Error: Missing function name.");
1207
+ console.error("[wawesome] Usage: wawesome logs <function-name> --follow, or run inside a function directory with wawesome-function.json.");
1208
+ process.exit(1);
1209
+ }
1210
+ const scopedUrl = appSlug ? `${gatewayUrl}/v1/apps/${encodeURIComponent(appSlug)}/functions/${encodeURIComponent(funcName)}/logs/stream` : null;
1211
+ const unscopedUrl = `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/logs/stream`;
1212
+ const controller = new AbortController();
1213
+ const onSigint = () => {
1214
+ controller.abort();
1215
+ process.stderr.write("\n[wawesome] Stopped following.\n");
1216
+ process.exit(0);
1217
+ };
1218
+ process.on("SIGINT", onSigint);
1219
+ const headers = {
1220
+ Authorization: `Bearer ${tenantJwt}`,
1221
+ Accept: "text/event-stream"
1222
+ };
1223
+ try {
1224
+ let attempt = 0;
1225
+ let announced = false;
1226
+ while (true) {
1227
+ if (controller.signal.aborted) return;
1228
+ let res = null;
1229
+ try {
1230
+ if (scopedUrl) {
1231
+ res = await fetch(scopedUrl, {
1232
+ headers,
1233
+ signal: controller.signal
1234
+ });
1235
+ if (res.status === 404) {
1236
+ if (isVerbose) console.error(`[wawesome:verbose] 404 on app-scoped route, retrying unscoped.`);
1237
+ res = await fetch(unscopedUrl, {
1238
+ headers,
1239
+ signal: controller.signal
1240
+ });
1241
+ }
1242
+ } else res = await fetch(unscopedUrl, {
1243
+ headers,
1244
+ signal: controller.signal
1245
+ });
1246
+ } catch (err) {
1247
+ if (controller.signal.aborted) return;
1248
+ if (attempt < MAX_RETRIES) {
1249
+ const delay = retryDelay(attempt);
1250
+ console.error(`[wawesome] Connection lost, reconnecting (${attempt + 1}/${MAX_RETRIES})...`);
1251
+ if (isVerbose) console.error(`[wawesome:verbose] ${String(err)}`);
1252
+ await retrySleep(delay, controller.signal);
1253
+ attempt++;
1254
+ continue;
1255
+ }
1256
+ console.error("[wawesome] Error: Failed to connect to the live tail after retries.");
1257
+ process.exit(1);
1258
+ }
1259
+ if (res.status === 401) {
1260
+ console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
1261
+ process.exit(1);
1262
+ }
1263
+ if (res.status === 404) {
1264
+ console.error(`[wawesome] Error: Function '${funcName}' not found.`);
1265
+ process.exit(1);
1266
+ }
1267
+ if (!res.ok && isTransientError(res)) {
1268
+ if (attempt < MAX_RETRIES) {
1269
+ const delay = retryDelay(attempt);
1270
+ console.error(`[wawesome] Server error, reconnecting (${attempt + 1}/${MAX_RETRIES})...`);
1271
+ await retrySleep(delay, controller.signal);
1272
+ attempt++;
1273
+ continue;
1274
+ }
1275
+ console.error(`[wawesome] Error: Failed to open live tail (HTTP ${res.status}) after retries.`);
1276
+ process.exit(1);
1277
+ }
1278
+ if (!res.ok) {
1279
+ console.error(`[wawesome] Error: Failed to open live tail (HTTP ${res.status}).`);
1280
+ process.exit(1);
1281
+ }
1282
+ if (!res.body) {
1283
+ console.error("[wawesome] Error: Live tail response had no body stream.");
1284
+ process.exit(1);
1285
+ }
1286
+ attempt = 0;
1287
+ if (!announced) {
1288
+ const target = appSlug ? `${appSlug}/${funcName}` : funcName;
1289
+ console.error(`[wawesome] 📡 Following function ${target} (Ctrl-C to stop)...`);
1290
+ announced = true;
1291
+ }
1292
+ try {
1293
+ const reader = res.body.getReader();
1294
+ const decoder = new TextDecoder();
1295
+ let buffer = "";
1296
+ for (;;) {
1297
+ const { value, done } = await reader.read();
1298
+ if (done) break;
1299
+ buffer += decoder.decode(value, { stream: true });
1300
+ const { events, rest } = splitSseEvents(buffer);
1301
+ buffer = rest;
1302
+ for (const chunk of events) for (const line of chunk.split("\n")) printFollowLine(line);
1303
+ }
1304
+ if (controller.signal.aborted) return;
1305
+ if (isVerbose) console.error(`[wawesome:verbose] Stream closed by server (cap); reconnecting.`);
1306
+ continue;
1307
+ } catch (streamErr) {
1308
+ if (controller.signal.aborted) return;
1309
+ if (attempt < MAX_RETRIES) {
1310
+ const delay = retryDelay(attempt);
1311
+ console.error(`[wawesome] Stream interrupted, reconnecting (${attempt + 1}/${MAX_RETRIES})...`);
1312
+ if (isVerbose) console.error(`[wawesome:verbose] ${String(streamErr)}`);
1313
+ await retrySleep(delay, controller.signal);
1314
+ attempt++;
1315
+ continue;
1316
+ }
1317
+ console.error("[wawesome] Error: Live tail stream interrupted and retries exhausted.");
1318
+ process.exit(1);
1319
+ }
1320
+ }
1321
+ } finally {
1322
+ process.removeListener("SIGINT", onSigint);
1323
+ }
1324
+ }
1325
+ //#endregion
840
1326
  //#region src/index.ts
841
1327
  const cli = cac("wawesome");
842
1328
  cli.command("build [entry]", "Bundle a serverless function to an optimized JS file").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").action((entry, options) => buildJs(entry, options));
@@ -856,6 +1342,27 @@ cli.command("login", "Authenticate with the wawesome.io platform").option("--api
856
1342
  cli.command("logout", "Clear stored authentication credentials").action(() => logout());
857
1343
  cli.command("whoami", "Show current login session info").action(() => whoami());
858
1344
  cli.command("init", "Scaffold a new function project in the current directory").option("-v, --verbose", "Enable verbose debug output").action((options) => init(options));
1345
+ cli.command("logs [function-name-or-invocation-id]", "View invocation history, fetch log output, or follow live").usage(`logs [target] [options]
1346
+
1347
+ The target argument determines what the command does:
1348
+
1349
+ MODE 1 — List invocations (no UUID target)
1350
+ wawesome logs # list invocations for the function in the current directory
1351
+ wawesome logs my-function # list invocations for 'my-function'
1352
+ wawesome logs my-function --error # only show failed invocations
1353
+ wawesome logs my-function --running # only show currently running invocations
1354
+
1355
+ MODE 2 — Fetch captured log body (UUID target or --invocation)
1356
+ wawesome logs <invocation-id> # print stdout/stderr for a specific invocation
1357
+ wawesome logs my-function --invocation <id> # same, explicit flag form
1358
+
1359
+ MODE 3 — Follow live output (--follow / -f)
1360
+ wawesome logs my-function --follow # follow the function: stream output from every invocation as it runs
1361
+ wawesome logs <invocation-id> --follow # follow one specific in-flight invocation by ID
1362
+
1363
+ With a function name, --follow streams the function's output continuously across
1364
+ invocations — new output appears each time the function runs, no need to catch a
1365
+ specific invocation. Press Ctrl-C to stop at any time.`).option("-f, --follow", "Stream live output (tail -f style). Follows a running invocation or waits for the next one. Ctrl-C to stop").option("-i, --invocation <id>", "Fetch stdout/stderr log body for a specific invocation ID").option("-a, --app <app>", "App slug override (defaults to wawesome-function.json)").option("-s, --status <status>", "Filter invocations by status (success, error, timeout, running)").option("--success", "Shorthand for --status success").option("--error", "Shorthand for --status error").option("--timeout", "Shorthand for --status timeout").option("--running", "Shorthand for --status running").option("-v, --verbose", "Enable verbose debug output").action((target, options) => logsCommand(target, options));
859
1366
  cli.help();
860
1367
  cli.version("1.0.0");
861
1368
  cli.parse();
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.0.1",
3
+ "version": "0.0.5",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {
7
- "wawesome": "./bin/wawesome.js"
7
+ "wawesome": "bin/wawesome.js"
8
8
  },
9
9
  "main": "./dist/index.mjs",
10
10
  "types": "./dist/index.d.mts",
@@ -16,7 +16,7 @@
16
16
  "build": "tsdown",
17
17
  "dev": "tsdown --watch",
18
18
  "test": "vitest run",
19
- "check": "publint",
19
+ "check": "publint --pack npm",
20
20
  "changeset": "changeset"
21
21
  },
22
22
  "dependencies": {