wawesome 0.0.4 → 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.
- package/README.md +86 -15
- package/dist/index.mjs +507 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
> **[wawesome.io](https://wawesome.io)** platform.
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/wawesome)
|
|
7
|
-
[](https://opensource.org/licenses/MIT)
|
|
8
7
|
|
|
9
8
|
Deploy ultra-fast, lightweight serverless WebAssembly functions directly from your terminal in under 60 seconds.
|
|
10
9
|
|
|
@@ -45,19 +44,91 @@ npx wawesome deploy
|
|
|
45
44
|
|
|
46
45
|
## 📖 Command Reference
|
|
47
46
|
|
|
48
|
-
| Command
|
|
49
|
-
|
|
50
|
-
| `npx wawesome login`
|
|
51
|
-
| `npx wawesome logout`
|
|
52
|
-
| `npx wawesome whoami`
|
|
53
|
-
| `npx wawesome init`
|
|
54
|
-
| `npx wawesome build`
|
|
55
|
-
| `npx wawesome deploy`
|
|
56
|
-
| `npx wawesome
|
|
57
|
-
| `npx wawesome
|
|
58
|
-
| `npx wawesome
|
|
59
|
-
| `npx wawesome
|
|
60
|
-
| `npx wawesome
|
|
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.
|
|
61
132
|
|
|
62
133
|
---
|
|
63
134
|
|
|
@@ -119,4 +190,4 @@ npx wawesome env set STRIPE_SECRET_KEY sk_live_xxx --secret
|
|
|
119
190
|
|
|
120
191
|
- **Platform Homepage**: [https://wawesome.io](https://wawesome.io)
|
|
121
192
|
- **Documentation**: [https://docs.wawesome.io](https://docs.wawesome.io)
|
|
122
|
-
|
|
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();
|