wawesome 0.5.0 → 0.7.0
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 +99 -8
- package/dist/index.mjs +823 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,11 @@ npx wawesome deploy
|
|
|
106
106
|
| `npx wawesome logs [func]` | List recent past invocations for a function |
|
|
107
107
|
| `npx wawesome logs --invocation <id>` | Fetch full stdout/stderr log body for a specific invocation |
|
|
108
108
|
| `npx wawesome logs <func> --follow` | Follow live output (waits for the next invocation if needed) |
|
|
109
|
+
| `npx wawesome invoke [func]` | Fire a function run immediately and follow its output live |
|
|
110
|
+
| `npx wawesome cron [func]` | List schedules for a function or app |
|
|
111
|
+
| `npx wawesome cron pause <name>` | Pause a schedule by name (survives future deploys) |
|
|
112
|
+
| `npx wawesome cron resume <name>` | Resume a paused schedule (no backfill) |
|
|
113
|
+
| `npx wawesome cron history [func]` | Read background run history for scheduled and manual runs |
|
|
109
114
|
| `npx wawesome version list` | List version history for the current function |
|
|
110
115
|
| `npx wawesome version switch <v>` | Roll back or promote a specific function version |
|
|
111
116
|
| `npx wawesome env list` | View environment variables for the current app |
|
|
@@ -203,6 +208,77 @@ exponential back-off (up to 3 retries). Non-recoverable errors like authenticati
|
|
|
203
208
|
|
|
204
209
|
---
|
|
205
210
|
|
|
211
|
+
## ⚡ Manual Invocation
|
|
212
|
+
|
|
213
|
+
Fire a background run of a Function immediately without waiting for a schedule tick or deploying code:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# Invoke the function in the current directory and follow its output
|
|
217
|
+
npx wawesome invoke
|
|
218
|
+
|
|
219
|
+
# Invoke a specific function by name
|
|
220
|
+
npx wawesome invoke my-function
|
|
221
|
+
|
|
222
|
+
# Send custom HTTP method and request body
|
|
223
|
+
npx wawesome invoke -m POST -d '{"event":"audit"}'
|
|
224
|
+
|
|
225
|
+
# Fire without following live output
|
|
226
|
+
npx wawesome invoke --no-follow
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## ⏰ Schedules & Cron Management
|
|
232
|
+
|
|
233
|
+
Manage recurring Schedules and inspect background run history directly from your terminal.
|
|
234
|
+
|
|
235
|
+
### 1. List Schedules
|
|
236
|
+
|
|
237
|
+
List a Function's Schedules with expression, state, and next run in UTC:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# List schedules for the function in the current directory
|
|
241
|
+
npx wawesome cron
|
|
242
|
+
|
|
243
|
+
# List schedules for a specific function
|
|
244
|
+
npx wawesome cron list my-function
|
|
245
|
+
|
|
246
|
+
# List schedules across all functions in an App
|
|
247
|
+
npx wawesome cron list --app my-app
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The output clearly distinguishes the three off-states:
|
|
251
|
+
- `paused`: stopped by a user, resumable with `wawesome cron resume <name>`
|
|
252
|
+
- `not in this config file`: disabled because it was removed from configuration, resumable only by declaring it again in code
|
|
253
|
+
- `suspended`: suspended by the non-payment ladder, resumable only after settling workspace balance
|
|
254
|
+
|
|
255
|
+
### 2. Pause and Resume Schedules
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# Pause a schedule by name (stops queued ticks and survives future deploys)
|
|
259
|
+
npx wawesome cron pause nightly-reconcile
|
|
260
|
+
|
|
261
|
+
# Pause with an optional reason for incident context
|
|
262
|
+
npx wawesome cron pause nightly-reconcile --reason "database maintenance"
|
|
263
|
+
|
|
264
|
+
# Resume a paused schedule (recomputes next run from now, no catch-up backfilling)
|
|
265
|
+
npx wawesome cron resume nightly-reconcile
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 3. Read Run History
|
|
269
|
+
|
|
270
|
+
Inspect past scheduled and manual runs, showing when each run was due, when it started, pool delay, and how it ended:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# View run history for the current function
|
|
274
|
+
npx wawesome cron history
|
|
275
|
+
|
|
276
|
+
# Filter run history by state (pending, running, dispatched, skipped, missed, cancelled, lost, failed)
|
|
277
|
+
npx wawesome cron history my-function --state failed
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
206
282
|
## ⚙️ Configuration & Custom Gateway
|
|
207
283
|
|
|
208
284
|
### `wawesome-function.json`
|
|
@@ -220,6 +296,12 @@ Every project directory includes a `wawesome-function.json` file generated durin
|
|
|
220
296
|
`app` is the App this Function is deployed into, and it is client-facing — every deploy from this
|
|
221
297
|
directory is scoped to it.
|
|
222
298
|
|
|
299
|
+
`function` is the address. Changing it does not rename anything: your Function's URL is built from
|
|
300
|
+
its name, so the next deploy lands on a Function of its own and the old one stays live at the old
|
|
301
|
+
URL, serving the code its callers already hold. The CLI remembers where this directory last
|
|
302
|
+
deployed and asks before that happens, naming both URLs. If you meant it, delete the old Function
|
|
303
|
+
from the dashboard once nothing calls it.
|
|
304
|
+
|
|
223
305
|
Add `"assets"` to deploy static files beside your code:
|
|
224
306
|
|
|
225
307
|
```json
|
|
@@ -313,17 +395,26 @@ A job on a timer should not also be sitting at a guessable URL where a stranger
|
|
|
313
395
|
}
|
|
314
396
|
```
|
|
315
397
|
|
|
316
|
-
A private Function has **no address at all** — not a hidden one, not one
|
|
317
|
-
request for it is answered with the same 404
|
|
318
|
-
|
|
319
|
-
|
|
398
|
+
A private Function has **no public address at all** in production — not a hidden one, not one
|
|
399
|
+
behind a credential. A request for it against your App's own hostname is answered with the same 404
|
|
400
|
+
as a Function that was never deployed. This is how you deploy a job with side effects without leaving
|
|
401
|
+
it where a stranger who guesses the slug can fire it.
|
|
320
402
|
|
|
321
403
|
Leave the line out and your Function is public, which is what every Function without it has always
|
|
322
404
|
been.
|
|
323
405
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
406
|
+
On your own machine, the local development surface serves Functions regardless of visibility, and
|
|
407
|
+
honours an explicit `x-wawesome-trigger` header so you can exercise a scheduled run by hand:
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
# Exercise a private or scheduled function locally on the development path form:
|
|
411
|
+
curl -X POST http://localhost:3000/x/my-tenant-slug/default-app/nightly-reconcile \
|
|
412
|
+
-H "x-wawesome-trigger: schedule"
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Production strips the reserved header namespace inbound, so the same header against your App's own
|
|
416
|
+
hostname reaches nothing that reads it: the run is a `caller`'s, and a private Function is a 404
|
|
417
|
+
either way. Skipping your own authorization for a `schedule` run therefore opens nothing.
|
|
327
418
|
|
|
328
419
|
Making a Function private takes nothing but the deploy. Making it public again does not: deleting
|
|
329
420
|
the line is refused, and the deploy tells you so having written nothing.
|
|
@@ -348,7 +439,7 @@ Four headers arrive or leave on it, and the stripping is what makes them worth t
|
|
|
348
439
|
| Header | Direction | What it means |
|
|
349
440
|
| --- | --- | --- |
|
|
350
441
|
| `x-wawesome-forwarded-prefix` | inbound | The mount that was stripped from the path. Join it to the path you observe to rebuild the caller's URL. |
|
|
351
|
-
| `x-wawesome-trigger` | inbound | How this run started: `caller` when someone called your address. A caller cannot forge it,
|
|
442
|
+
| `x-wawesome-trigger` | inbound | How this run started: `caller` when someone called your address, `schedule` when fired by a Schedule. A caller cannot forge it in production (stripped inbound). On the local development surface, pass `x-wawesome-trigger: schedule` to exercise a background run by hand with the collapsed budget. |
|
|
352
443
|
| `x-wawesome-invocation-id` | outbound | The id of this run — the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
|
|
353
444
|
| `x-wawesome-error` | outbound | Present only when the platform failed, never when your Function did. Its *absence* means the status on the wire is yours — up to the moment your response is committed, and no further. |
|
|
354
445
|
|
package/dist/index.mjs
CHANGED
|
@@ -759,7 +759,7 @@ async function buildJs(entryInput, options) {
|
|
|
759
759
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
760
760
|
* project pins — reads it here, so a release bumps one file.
|
|
761
761
|
*/
|
|
762
|
-
const CLI_VERSION = "0.
|
|
762
|
+
const CLI_VERSION = "0.7.0";
|
|
763
763
|
//#endregion
|
|
764
764
|
//#region src/prompt.ts
|
|
765
765
|
/**
|
|
@@ -1159,18 +1159,17 @@ async function whoami() {
|
|
|
1159
1159
|
/**
|
|
1160
1160
|
* When a schedule next runs, or why it does not run at all.
|
|
1161
1161
|
*
|
|
1162
|
-
* The three ways of being off never render alike
|
|
1163
|
-
*
|
|
1164
|
-
* and
|
|
1162
|
+
* The three ways of being off never render alike, and a schedule that is off
|
|
1163
|
+
* twice over says so: someone who reads only the pause will declare it again,
|
|
1164
|
+
* press resume, and file a bug when neither starts it.
|
|
1165
1165
|
*/
|
|
1166
1166
|
function whenItRuns(schedule) {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1167
|
+
if (schedule.state === "suspended") return "suspended — the workspace owes payment";
|
|
1168
|
+
const undeclared = schedule.declared === false;
|
|
1169
|
+
if (schedule.state === "paused") return undeclared ? "paused, and not in this config file — declare it again, then resume" : "paused — resume it to run again";
|
|
1170
|
+
if (undeclared) return "not in this config file — disabled, kept";
|
|
1171
|
+
if (schedule.state !== "active") return schedule.state;
|
|
1172
|
+
return schedule.next_fire_at ? `next run ${asUtc(schedule.next_fire_at)}` : "next run unknown";
|
|
1174
1173
|
}
|
|
1175
1174
|
/** `2026-08-23 03:00 UTC`, which is the zone every expression is read in. */
|
|
1176
1175
|
function asUtc(iso) {
|
|
@@ -1363,7 +1362,9 @@ function billingPageUrl() {
|
|
|
1363
1362
|
const PLAN_LIMITS = [
|
|
1364
1363
|
"app-slots-exhausted",
|
|
1365
1364
|
"storage-exhausted",
|
|
1366
|
-
"paid-plan-required"
|
|
1365
|
+
"paid-plan-required",
|
|
1366
|
+
"payment-required",
|
|
1367
|
+
"schedule-suspended"
|
|
1367
1368
|
];
|
|
1368
1369
|
/** The rule itself is the gateway's prose, and is deliberately not restated here. */
|
|
1369
1370
|
function planLimitAdvice(reason) {
|
|
@@ -1513,6 +1514,97 @@ function trimTrailingSlashes(origin) {
|
|
|
1513
1514
|
return origin.replace(/\/+$/, "");
|
|
1514
1515
|
}
|
|
1515
1516
|
//#endregion
|
|
1517
|
+
//#region src/deploy-target.ts
|
|
1518
|
+
const RECORD_PATH = path.join(".wawesome", "last-deploy.json");
|
|
1519
|
+
function readLastDeployTarget(projectDir = process.cwd()) {
|
|
1520
|
+
let parsed;
|
|
1521
|
+
try {
|
|
1522
|
+
parsed = JSON.parse(fs.readFileSync(path.join(projectDir, RECORD_PATH), "utf-8"));
|
|
1523
|
+
} catch {
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
if (typeof parsed.app !== "string" || typeof parsed.function !== "string") return null;
|
|
1527
|
+
return {
|
|
1528
|
+
app: parsed.app,
|
|
1529
|
+
function: parsed.function
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* The directory ignores itself rather than relying on the project's `.gitignore`,
|
|
1534
|
+
* since this is per-checkout state and most projects reaching it were scaffolded
|
|
1535
|
+
* before it existed. A write that fails is dropped: the deploy has already
|
|
1536
|
+
* landed, and reporting a read-only project directory as a failed deploy would
|
|
1537
|
+
* be a worse lie than forgetting the name.
|
|
1538
|
+
*/
|
|
1539
|
+
function recordDeployTarget(target, projectDir = process.cwd()) {
|
|
1540
|
+
const file = path.join(projectDir, RECORD_PATH);
|
|
1541
|
+
try {
|
|
1542
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1543
|
+
fs.writeFileSync(path.join(path.dirname(file), ".gitignore"), "*\n", "utf-8");
|
|
1544
|
+
fs.writeFileSync(file, JSON.stringify(target, null, 2) + "\n", "utf-8");
|
|
1545
|
+
} catch {
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/** See ADR-0016 for the fork this asks about, and why no rename is on offer. */
|
|
1550
|
+
async function confirmDeployTarget(creds, target, projectDir = process.cwd()) {
|
|
1551
|
+
const previous = readLastDeployTarget(projectDir);
|
|
1552
|
+
if (!previous || previous.app === target.app && previous.function === target.function) return true;
|
|
1553
|
+
const surface = await addressesFor(creds, previous, target);
|
|
1554
|
+
for (const line of divergenceLines(previous, target, surface)) console.warn(line);
|
|
1555
|
+
if (!isInteractive()) {
|
|
1556
|
+
console.warn("[wawesome] There is no terminal to confirm at, so the deploy continues.\n");
|
|
1557
|
+
return true;
|
|
1558
|
+
}
|
|
1559
|
+
const session = openPromptSession();
|
|
1560
|
+
try {
|
|
1561
|
+
const answer = await session.ask(` Deploy to '${qualifiedName(target)}' and leave '${qualifiedName(previous)}' live? [y/N]`, "");
|
|
1562
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1563
|
+
} finally {
|
|
1564
|
+
session.close();
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function divergenceLines(previous, target, addresses) {
|
|
1568
|
+
return [
|
|
1569
|
+
"",
|
|
1570
|
+
`[wawesome] \x1b[33mThis project last deployed to ${qualifiedName(previous)}.\x1b[0m`,
|
|
1571
|
+
`[wawesome] wawesome-function.json now says ${qualifiedName(target)}.`,
|
|
1572
|
+
"",
|
|
1573
|
+
"[wawesome] Deploying will not rename it. A Function's address is built from its",
|
|
1574
|
+
"[wawesome] name, so this deploy lands on a Function of its own and leaves the old",
|
|
1575
|
+
"[wawesome] one exactly as it is — promoted, and still answering the code on it:",
|
|
1576
|
+
"",
|
|
1577
|
+
`[wawesome] stays live: ${addresses.previous ?? qualifiedName(previous)}`,
|
|
1578
|
+
`[wawesome] gets this deploy: ${addresses.next ?? qualifiedName(target)}`,
|
|
1579
|
+
"",
|
|
1580
|
+
"[wawesome] Anything holding the old address keeps reaching the old code. Delete",
|
|
1581
|
+
"[wawesome] the old Function from the dashboard once nothing calls it.",
|
|
1582
|
+
""
|
|
1583
|
+
];
|
|
1584
|
+
}
|
|
1585
|
+
function qualifiedName(target) {
|
|
1586
|
+
return `${target.app}/${target.function}`;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* A gateway that cannot be reached must not turn this into a failed deploy: the
|
|
1590
|
+
* names alone still say what is about to happen, which is the point of saying it.
|
|
1591
|
+
*/
|
|
1592
|
+
async function addressesFor(creds, previous, target) {
|
|
1593
|
+
try {
|
|
1594
|
+
const { slug } = await resolveWorkspace(creds);
|
|
1595
|
+
const surface = await invocationSurfaceOrNone(creds.gateway_url);
|
|
1596
|
+
return {
|
|
1597
|
+
previous: publicAddress(surface, slug, previous.app, previous.function),
|
|
1598
|
+
next: publicAddress(surface, slug, target.app, target.function)
|
|
1599
|
+
};
|
|
1600
|
+
} catch {
|
|
1601
|
+
return {
|
|
1602
|
+
previous: null,
|
|
1603
|
+
next: null
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
//#endregion
|
|
1516
1608
|
//#region src/deploy.ts
|
|
1517
1609
|
function declaredFields(declared) {
|
|
1518
1610
|
return {
|
|
@@ -1564,6 +1656,13 @@ async function deploy(entryInput, options) {
|
|
|
1564
1656
|
console.log(`[wawesome:verbose] Deploying to app=${app}, function=${funcName}`);
|
|
1565
1657
|
console.log(`[wawesome:verbose] Gateway: ${creds.gateway_url}`);
|
|
1566
1658
|
}
|
|
1659
|
+
if (!await confirmDeployTarget(creds, {
|
|
1660
|
+
app,
|
|
1661
|
+
function: funcName
|
|
1662
|
+
})) {
|
|
1663
|
+
console.log("[wawesome] Nothing was deployed.");
|
|
1664
|
+
process.exit(0);
|
|
1665
|
+
}
|
|
1567
1666
|
let bundlePath;
|
|
1568
1667
|
if (options.skipBuild) {
|
|
1569
1668
|
bundlePath = path.resolve(options.out);
|
|
@@ -1631,8 +1730,12 @@ async function deploy(entryInput, options) {
|
|
|
1631
1730
|
process.exit(1);
|
|
1632
1731
|
}
|
|
1633
1732
|
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
1634
|
-
|
|
1635
|
-
console.error(
|
|
1733
|
+
const advice = planLimitAdvice(refusal.reason);
|
|
1734
|
+
if (advice) console.error(`[wawesome] ${advice}\n`);
|
|
1735
|
+
else if (!refusal.reason) {
|
|
1736
|
+
console.error("[wawesome] Code versions are immutable and cannot be overwritten.");
|
|
1737
|
+
console.error("[wawesome] To switch active version, run: \x1B[36mwawesome version switch\x1B[0m\n");
|
|
1738
|
+
}
|
|
1636
1739
|
} else {
|
|
1637
1740
|
console.error(`[wawesome] Error: Code upload failed (HTTP ${uploadRes.status}).`);
|
|
1638
1741
|
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
@@ -1670,6 +1773,10 @@ async function deploy(entryInput, options) {
|
|
|
1670
1773
|
process.exit(1);
|
|
1671
1774
|
}
|
|
1672
1775
|
}
|
|
1776
|
+
recordDeployTarget({
|
|
1777
|
+
app,
|
|
1778
|
+
function: funcName
|
|
1779
|
+
});
|
|
1673
1780
|
let address = null;
|
|
1674
1781
|
let surface = null;
|
|
1675
1782
|
if (visibility === "public") try {
|
|
@@ -3606,8 +3713,237 @@ async function followFunctionLog(gatewayUrl, tenantJwt, funcNameInput, appOverri
|
|
|
3606
3713
|
}
|
|
3607
3714
|
}
|
|
3608
3715
|
//#endregion
|
|
3716
|
+
//#region src/invoke.ts
|
|
3717
|
+
const MAX_STREAM_RETRIES = 3;
|
|
3718
|
+
function formatOutcomeDuration(meta) {
|
|
3719
|
+
if (!meta) return "-";
|
|
3720
|
+
if (meta.duration_ms !== void 0 && meta.duration_ms !== null && !isNaN(meta.duration_ms) && meta.duration_ms >= 0) {
|
|
3721
|
+
if (meta.duration_ms < 1e3) return `${meta.duration_ms}ms`;
|
|
3722
|
+
return `${(meta.duration_ms / 1e3).toFixed(2)}s`;
|
|
3723
|
+
}
|
|
3724
|
+
if (meta.started_at) return formatDuration(meta.started_at, meta.ended_at);
|
|
3725
|
+
return "-";
|
|
3726
|
+
}
|
|
3727
|
+
function printOutcome(meta) {
|
|
3728
|
+
const durationStr = formatOutcomeDuration(meta);
|
|
3729
|
+
const statusStr = colorizeStatus(meta.status, meta.status);
|
|
3730
|
+
if (meta.status === "success") console.log(`[wawesome] ✔ Run completed: ${statusStr} (${durationStr})`);
|
|
3731
|
+
else console.log(`[wawesome] ✖ Run completed: ${statusStr} (${durationStr})`);
|
|
3732
|
+
}
|
|
3733
|
+
async function triggerRun(gatewayUrl, tenantJwt, funcName, appSlug, appOverride, payload, isVerbose) {
|
|
3734
|
+
const url = appSlug ? `${gatewayUrl}/v1/apps/${encodeURIComponent(appSlug)}/functions/${encodeURIComponent(funcName)}/runs` : `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/runs`;
|
|
3735
|
+
if (isVerbose) console.log(`[wawesome:verbose] POST ${url}`);
|
|
3736
|
+
let res = await fetch(url, {
|
|
3737
|
+
method: "POST",
|
|
3738
|
+
headers: {
|
|
3739
|
+
Authorization: `Bearer ${tenantJwt}`,
|
|
3740
|
+
"Content-Type": "application/json"
|
|
3741
|
+
},
|
|
3742
|
+
body: JSON.stringify(payload)
|
|
3743
|
+
});
|
|
3744
|
+
if (res.status === 404 && appSlug && !appOverride) {
|
|
3745
|
+
const fallbackUrl = `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/runs`;
|
|
3746
|
+
if (isVerbose) console.log(`[wawesome:verbose] 404 on app-scoped route. Retrying unscoped: POST ${fallbackUrl}`);
|
|
3747
|
+
try {
|
|
3748
|
+
const fallbackRes = await fetch(fallbackUrl, {
|
|
3749
|
+
method: "POST",
|
|
3750
|
+
headers: {
|
|
3751
|
+
Authorization: `Bearer ${tenantJwt}`,
|
|
3752
|
+
"Content-Type": "application/json"
|
|
3753
|
+
},
|
|
3754
|
+
body: JSON.stringify(payload)
|
|
3755
|
+
});
|
|
3756
|
+
if (fallbackRes) res = fallbackRes;
|
|
3757
|
+
} catch {}
|
|
3758
|
+
}
|
|
3759
|
+
return res;
|
|
3760
|
+
}
|
|
3761
|
+
async function fetchInvocationMetadata(gatewayUrl, tenantJwt, invocationId, isVerbose) {
|
|
3762
|
+
const url = `${gatewayUrl}/v1/invocations/${encodeURIComponent(invocationId)}`;
|
|
3763
|
+
if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
|
|
3764
|
+
const res = await fetch(url, {
|
|
3765
|
+
method: "GET",
|
|
3766
|
+
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
3767
|
+
});
|
|
3768
|
+
if (!res.ok) return null;
|
|
3769
|
+
return await res.json();
|
|
3770
|
+
}
|
|
3771
|
+
async function fetchInvocationLogs(gatewayUrl, tenantJwt, invocationId, isVerbose) {
|
|
3772
|
+
const url = `${gatewayUrl}/v1/invocations/${encodeURIComponent(invocationId)}/logs`;
|
|
3773
|
+
if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
|
|
3774
|
+
const res = await fetch(url, {
|
|
3775
|
+
method: "GET",
|
|
3776
|
+
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
3777
|
+
});
|
|
3778
|
+
if (!res.ok) return null;
|
|
3779
|
+
return await res.text();
|
|
3780
|
+
}
|
|
3781
|
+
async function followRun(gatewayUrl, tenantJwt, invocationId, isVerbose, pollIntervalMs = 250) {
|
|
3782
|
+
const streamUrl = `${gatewayUrl}/v1/invocations/${encodeURIComponent(invocationId)}/logs/stream`;
|
|
3783
|
+
const controller = new AbortController();
|
|
3784
|
+
const onSigint = () => {
|
|
3785
|
+
controller.abort();
|
|
3786
|
+
process.stderr.write("\n[wawesome] Stopped following.\n");
|
|
3787
|
+
process.exit(0);
|
|
3788
|
+
};
|
|
3789
|
+
process.on("SIGINT", onSigint);
|
|
3790
|
+
try {
|
|
3791
|
+
let attempt = 0;
|
|
3792
|
+
let streamReadComplete = false;
|
|
3793
|
+
let printedAny = false;
|
|
3794
|
+
while (!streamReadComplete) {
|
|
3795
|
+
if (controller.signal.aborted) return;
|
|
3796
|
+
let res = null;
|
|
3797
|
+
try {
|
|
3798
|
+
res = await fetch(streamUrl, {
|
|
3799
|
+
method: "GET",
|
|
3800
|
+
headers: {
|
|
3801
|
+
Authorization: `Bearer ${tenantJwt}`,
|
|
3802
|
+
Accept: "text/event-stream"
|
|
3803
|
+
},
|
|
3804
|
+
signal: controller.signal
|
|
3805
|
+
});
|
|
3806
|
+
} catch (err) {
|
|
3807
|
+
if (controller.signal.aborted) return;
|
|
3808
|
+
if (attempt < MAX_STREAM_RETRIES) {
|
|
3809
|
+
const delay = retryDelay(attempt);
|
|
3810
|
+
if (isVerbose) console.error(`[wawesome:verbose] Stream connection failed (${String(err)}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${MAX_STREAM_RETRIES})...`);
|
|
3811
|
+
await retrySleep(delay, controller.signal);
|
|
3812
|
+
attempt++;
|
|
3813
|
+
continue;
|
|
3814
|
+
}
|
|
3815
|
+
console.error("[wawesome] Error: Failed to connect to the live tail after retries.");
|
|
3816
|
+
process.exit(1);
|
|
3817
|
+
}
|
|
3818
|
+
if (res.status === 404) {
|
|
3819
|
+
const meta = await fetchInvocationMetadata(gatewayUrl, tenantJwt, invocationId, isVerbose);
|
|
3820
|
+
if (!meta) {
|
|
3821
|
+
console.error(`[wawesome] Error: Invocation '${invocationId}' not found.`);
|
|
3822
|
+
process.exit(1);
|
|
3823
|
+
}
|
|
3824
|
+
if (meta.status === "queued" || meta.status === "running") {
|
|
3825
|
+
await retrySleep(pollIntervalMs, controller.signal);
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3828
|
+
if (meta.status === "success" || meta.status === "error" || meta.status === "timeout") {
|
|
3829
|
+
const logBody = await fetchInvocationLogs(gatewayUrl, tenantJwt, invocationId, isVerbose);
|
|
3830
|
+
if (logBody) for (const line of logBody.split("\n")) printFollowLine(line);
|
|
3831
|
+
printOutcome(meta);
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
console.error(`[wawesome] ✖ Run ${meta.status}${meta.failure_reason ? `: ${meta.failure_reason}` : ""}`);
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
if (res.status === 401) {
|
|
3838
|
+
console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
|
|
3839
|
+
process.exit(1);
|
|
3840
|
+
}
|
|
3841
|
+
if (!res.ok && res.status >= 500) {
|
|
3842
|
+
if (attempt < MAX_STREAM_RETRIES) {
|
|
3843
|
+
const delay = retryDelay(attempt);
|
|
3844
|
+
if (isVerbose) console.error(`[wawesome:verbose] Server error (${res.status}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${MAX_STREAM_RETRIES})...`);
|
|
3845
|
+
await retrySleep(delay, controller.signal);
|
|
3846
|
+
attempt++;
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3849
|
+
console.error(`[wawesome] Error: Failed to open live tail (HTTP ${res.status}) after retries.`);
|
|
3850
|
+
process.exit(1);
|
|
3851
|
+
}
|
|
3852
|
+
if (!res.ok) {
|
|
3853
|
+
console.error(`[wawesome] Error: Failed to open live tail (HTTP ${res.status}).`);
|
|
3854
|
+
process.exit(1);
|
|
3855
|
+
}
|
|
3856
|
+
if (!res.body) {
|
|
3857
|
+
console.error("[wawesome] Error: Live tail response had no body stream.");
|
|
3858
|
+
process.exit(1);
|
|
3859
|
+
}
|
|
3860
|
+
attempt = 0;
|
|
3861
|
+
try {
|
|
3862
|
+
const reader = res.body.getReader();
|
|
3863
|
+
const decoder = new TextDecoder();
|
|
3864
|
+
let buffer = "";
|
|
3865
|
+
for (;;) {
|
|
3866
|
+
const { value, done } = await reader.read();
|
|
3867
|
+
if (done) break;
|
|
3868
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3869
|
+
const { events, rest } = splitSseEvents(buffer);
|
|
3870
|
+
buffer = rest;
|
|
3871
|
+
for (const chunk of events) for (const line of chunk.split("\n")) {
|
|
3872
|
+
printFollowLine(line);
|
|
3873
|
+
printedAny = true;
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
streamReadComplete = true;
|
|
3877
|
+
} catch (streamErr) {
|
|
3878
|
+
if (controller.signal.aborted) return;
|
|
3879
|
+
if (attempt < MAX_STREAM_RETRIES) {
|
|
3880
|
+
await retrySleep(retryDelay(attempt), controller.signal);
|
|
3881
|
+
attempt++;
|
|
3882
|
+
continue;
|
|
3883
|
+
}
|
|
3884
|
+
console.error("[wawesome] Error: Live tail stream interrupted and retries exhausted.");
|
|
3885
|
+
process.exit(1);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
const meta = await fetchInvocationMetadata(gatewayUrl, tenantJwt, invocationId, isVerbose);
|
|
3889
|
+
if (meta) {
|
|
3890
|
+
if (!printedAny && (meta.status === "success" || meta.status === "error" || meta.status === "timeout")) {
|
|
3891
|
+
const logBody = await fetchInvocationLogs(gatewayUrl, tenantJwt, invocationId, isVerbose);
|
|
3892
|
+
if (logBody) for (const line of logBody.split("\n")) printFollowLine(line);
|
|
3893
|
+
}
|
|
3894
|
+
printOutcome(meta);
|
|
3895
|
+
}
|
|
3896
|
+
} finally {
|
|
3897
|
+
process.removeListener("SIGINT", onSigint);
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
async function invokeCommand(target, options = {}) {
|
|
3901
|
+
const isVerbose = Boolean(options.verbose);
|
|
3902
|
+
const creds = readCredentials();
|
|
3903
|
+
if (!creds) {
|
|
3904
|
+
console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
|
|
3905
|
+
process.exit(1);
|
|
3906
|
+
}
|
|
3907
|
+
const config = readFunctionConfig();
|
|
3908
|
+
const funcName = target || config?.function;
|
|
3909
|
+
const appSlug = options.app || config?.app;
|
|
3910
|
+
if (!funcName) {
|
|
3911
|
+
console.error("[wawesome] Error: Missing function name.");
|
|
3912
|
+
console.error("[wawesome] Usage: wawesome invoke [function-name] or run inside a function directory with wawesome-function.json.");
|
|
3913
|
+
process.exit(1);
|
|
3914
|
+
}
|
|
3915
|
+
const method = options.method?.trim();
|
|
3916
|
+
const body = options.body ?? options.data;
|
|
3917
|
+
const triggerPayload = {};
|
|
3918
|
+
if (method) triggerPayload.method = method;
|
|
3919
|
+
if (body !== void 0) triggerPayload.body = body;
|
|
3920
|
+
const res = await triggerRun(creds.gateway_url, creds.tenant_jwt, funcName, appSlug, options.app, triggerPayload, isVerbose);
|
|
3921
|
+
if (!res.ok) {
|
|
3922
|
+
const errorBody = await res.text();
|
|
3923
|
+
if (res.status === 401) {
|
|
3924
|
+
console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
|
|
3925
|
+
process.exit(1);
|
|
3926
|
+
}
|
|
3927
|
+
if (res.status === 404) {
|
|
3928
|
+
console.error(`[wawesome] Error: Function '${funcName}' not found.`);
|
|
3929
|
+
process.exit(1);
|
|
3930
|
+
}
|
|
3931
|
+
const refusal = rejectionOf(errorBody, res.status, `Failed to invoke function (HTTP ${res.status}).`);
|
|
3932
|
+
console.error(`[wawesome] Error: ${refusal.message}`);
|
|
3933
|
+
const advice = planLimitAdvice(refusal.reason) || (res.status === 402 ? `Where to resolve it: ${billingPageUrl()}` : "");
|
|
3934
|
+
if (advice) console.error(`[wawesome] ${advice}`);
|
|
3935
|
+
if (isVerbose && res.status !== 409 && res.status !== 402) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
3936
|
+
process.exit(1);
|
|
3937
|
+
}
|
|
3938
|
+
const data = await res.json();
|
|
3939
|
+
const displayTarget = appSlug ? `${appSlug}/${funcName}` : funcName;
|
|
3940
|
+
console.log(`[wawesome] ⏳ Run ${data.invocation_id} queued for '${displayTarget}'...`);
|
|
3941
|
+
if (options.follow === false) return;
|
|
3942
|
+
await followRun(creds.gateway_url, creds.tenant_jwt, data.invocation_id, isVerbose);
|
|
3943
|
+
}
|
|
3944
|
+
//#endregion
|
|
3609
3945
|
//#region src/workspace.ts
|
|
3610
|
-
function requireCredentials() {
|
|
3946
|
+
function requireCredentials$1() {
|
|
3611
3947
|
const creds = readCredentials();
|
|
3612
3948
|
if (!creds) {
|
|
3613
3949
|
console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
|
|
@@ -3620,7 +3956,7 @@ function requireCredentials() {
|
|
|
3620
3956
|
* change — the rename is offered here rather than discovered by attempting it.
|
|
3621
3957
|
*/
|
|
3622
3958
|
async function showWorkspace(options) {
|
|
3623
|
-
const creds = requireCredentials();
|
|
3959
|
+
const creds = requireCredentials$1();
|
|
3624
3960
|
let tenant;
|
|
3625
3961
|
try {
|
|
3626
3962
|
tenant = await fetchTenantDetails(creds);
|
|
@@ -3655,7 +3991,7 @@ async function showWorkspace(options) {
|
|
|
3655
3991
|
* gateway knows something this command could not.
|
|
3656
3992
|
*/
|
|
3657
3993
|
async function renameWorkspace(slug, options) {
|
|
3658
|
-
const creds = requireCredentials();
|
|
3994
|
+
const creds = requireCredentials$1();
|
|
3659
3995
|
if (!slug || !slug.trim()) {
|
|
3660
3996
|
console.error("[wawesome] Error: No name given. Usage: wawesome workspace rename <name>");
|
|
3661
3997
|
process.exit(1);
|
|
@@ -3699,6 +4035,442 @@ async function workspaceCommand(action, target, options) {
|
|
|
3699
4035
|
process.exit(1);
|
|
3700
4036
|
}
|
|
3701
4037
|
//#endregion
|
|
4038
|
+
//#region src/cron.ts
|
|
4039
|
+
/** `2026-08-23 03:00:00 UTC` with second-precision for run history timestamps. */
|
|
4040
|
+
function asUtcWithSeconds(iso) {
|
|
4041
|
+
const at = new Date(iso);
|
|
4042
|
+
if (Number.isNaN(at.getTime())) return iso;
|
|
4043
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
4044
|
+
return `${at.getUTCFullYear()}-${pad(at.getUTCMonth() + 1)}-${pad(at.getUTCDate())} ${pad(at.getUTCHours())}:${pad(at.getUTCMinutes())}:${pad(at.getUTCSeconds())} UTC`;
|
|
4045
|
+
}
|
|
4046
|
+
/** Format execution delay in ms or s. */
|
|
4047
|
+
function formatDelay(delayMs) {
|
|
4048
|
+
if (delayMs === void 0 || delayMs === null || isNaN(delayMs) || delayMs < 0) return "-";
|
|
4049
|
+
if (delayMs < 1e3) return `${delayMs}ms`;
|
|
4050
|
+
return `${(delayMs / 1e3).toFixed(2)}s`;
|
|
4051
|
+
}
|
|
4052
|
+
/** Colorize state names for terminal output. */
|
|
4053
|
+
function colorizeRunState(state, text) {
|
|
4054
|
+
const display = text ?? state;
|
|
4055
|
+
switch (state.toLowerCase()) {
|
|
4056
|
+
case "dispatched":
|
|
4057
|
+
case "success": return `\x1b[32m${display}\x1b[0m`;
|
|
4058
|
+
case "running": return `\x1b[36m${display}\x1b[0m`;
|
|
4059
|
+
case "pending":
|
|
4060
|
+
case "queued": return `\x1b[33m${display}\x1b[0m`;
|
|
4061
|
+
case "missed": return `\x1b[33m${display}\x1b[0m`;
|
|
4062
|
+
case "cancelled":
|
|
4063
|
+
case "skipped": return `\x1b[90m${display}\x1b[0m`;
|
|
4064
|
+
case "failed":
|
|
4065
|
+
case "lost":
|
|
4066
|
+
case "error": return `\x1b[31m${display}\x1b[0m`;
|
|
4067
|
+
default: return display;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
/**
|
|
4071
|
+
* Format a list of schedules for a single function.
|
|
4072
|
+
*/
|
|
4073
|
+
function formatFunctionSchedules(funcName, appSlug, schedules) {
|
|
4074
|
+
const displayTarget = appSlug ? `${appSlug}/${funcName}` : funcName;
|
|
4075
|
+
if (schedules.length === 0) return [`[wawesome] No schedules found for '${displayTarget}'.`];
|
|
4076
|
+
const nameWidth = Math.max(8, ...schedules.map((s) => s.name.length));
|
|
4077
|
+
const exprWidth = Math.max(10, ...schedules.map((s) => s.expression.length));
|
|
4078
|
+
return [
|
|
4079
|
+
`\n🗓 \x1b[1mSchedules for '${displayTarget}'\x1b[0m\n`,
|
|
4080
|
+
...[...schedules].sort((a, b) => a.name.localeCompare(b.name)).map((schedule) => ` ${schedule.name.padEnd(nameWidth)} ${schedule.expression.padEnd(exprWidth)} ${whenItRuns(schedule)}`),
|
|
4081
|
+
""
|
|
4082
|
+
];
|
|
4083
|
+
}
|
|
4084
|
+
/**
|
|
4085
|
+
* Format schedules across all functions in an App.
|
|
4086
|
+
*/
|
|
4087
|
+
function formatAppSchedules(appSlug, functionSchedules) {
|
|
4088
|
+
if (functionSchedules.reduce((acc, f) => acc + f.schedules.length, 0) === 0) return [`[wawesome] No schedules found for app '${appSlug}'.`];
|
|
4089
|
+
const allSchedules = functionSchedules.flatMap((f) => f.schedules);
|
|
4090
|
+
const nameWidth = Math.max(8, ...allSchedules.map((s) => s.name.length));
|
|
4091
|
+
const exprWidth = Math.max(10, ...allSchedules.map((s) => s.expression.length));
|
|
4092
|
+
const out = [`\n🗓 \x1b[1mSchedules for app '${appSlug}'\x1b[0m\n`];
|
|
4093
|
+
for (const fn of functionSchedules) {
|
|
4094
|
+
out.push(` Function: \x1b[1m${fn.functionName}\x1b[0m`);
|
|
4095
|
+
if (fn.schedules.length === 0) out.push(" (no schedules)");
|
|
4096
|
+
else for (const s of [...fn.schedules].sort((a, b) => a.name.localeCompare(b.name))) out.push(` ${s.name.padEnd(nameWidth)} ${s.expression.padEnd(exprWidth)} ${whenItRuns(s)}`);
|
|
4097
|
+
out.push("");
|
|
4098
|
+
}
|
|
4099
|
+
return out;
|
|
4100
|
+
}
|
|
4101
|
+
/**
|
|
4102
|
+
* Format background run history table.
|
|
4103
|
+
*/
|
|
4104
|
+
function formatRunHistoryTable(runs) {
|
|
4105
|
+
if (runs.length === 0) return [];
|
|
4106
|
+
const scheduleColHeader = "SCHEDULE";
|
|
4107
|
+
const stateColHeader = "STATE";
|
|
4108
|
+
const dueColHeader = "DUE AT (UTC)";
|
|
4109
|
+
const startedColHeader = "STARTED AT (UTC)";
|
|
4110
|
+
const delayColHeader = "DELAY";
|
|
4111
|
+
const invColHeader = "INVOCATION ID";
|
|
4112
|
+
const rows = runs.map((run) => {
|
|
4113
|
+
const schedName = run.schedule_name || "manual";
|
|
4114
|
+
const stateDisplay = run.failure_reason ? `${run.state} (${run.failure_reason})` : run.state;
|
|
4115
|
+
const dueAt = asUtcWithSeconds(run.fire_at);
|
|
4116
|
+
const startedAt = run.claimed_at ? asUtcWithSeconds(run.claimed_at) : "-";
|
|
4117
|
+
const delay = formatDelay(run.delay_ms);
|
|
4118
|
+
const invId = run.invocation_id;
|
|
4119
|
+
return {
|
|
4120
|
+
schedName,
|
|
4121
|
+
stateDisplay,
|
|
4122
|
+
rawState: run.state,
|
|
4123
|
+
dueAt,
|
|
4124
|
+
startedAt,
|
|
4125
|
+
delay,
|
|
4126
|
+
invId
|
|
4127
|
+
};
|
|
4128
|
+
});
|
|
4129
|
+
const schedWidth = Math.max(8, ...rows.map((r) => r.schedName.length));
|
|
4130
|
+
const stateWidth = Math.max(5, ...rows.map((r) => r.stateDisplay.length));
|
|
4131
|
+
const dueWidth = Math.max(12, ...rows.map((r) => r.dueAt.length));
|
|
4132
|
+
const startedWidth = Math.max(16, ...rows.map((r) => r.startedAt.length));
|
|
4133
|
+
const delayWidth = Math.max(5, ...rows.map((r) => r.delay.length));
|
|
4134
|
+
const invWidth = Math.max(13, ...rows.map((r) => r.invId.length));
|
|
4135
|
+
return [
|
|
4136
|
+
`${scheduleColHeader.padEnd(schedWidth)} | ${stateColHeader.padEnd(stateWidth)} | ${dueColHeader.padEnd(dueWidth)} | ${startedColHeader.padEnd(startedWidth)} | ${delayColHeader.padEnd(delayWidth)} | ${invColHeader.padEnd(invWidth)}`,
|
|
4137
|
+
`${"-".repeat(schedWidth)}-|-${"-".repeat(stateWidth)}-|-${"-".repeat(dueWidth)}-|-${"-".repeat(startedWidth)}-|-${"-".repeat(delayWidth)}-|-${"-".repeat(invWidth)}`,
|
|
4138
|
+
...rows.map((r) => {
|
|
4139
|
+
const schedPad = r.schedName.padEnd(schedWidth);
|
|
4140
|
+
const coloredState = colorizeRunState(r.rawState, r.stateDisplay);
|
|
4141
|
+
return `${schedPad} | ${r.stateDisplay.length < stateWidth ? coloredState + " ".repeat(stateWidth - r.stateDisplay.length) : coloredState} | ${r.dueAt.padEnd(dueWidth)} | ${r.startedAt.padEnd(startedWidth)} | ${r.delay.padEnd(delayWidth)} | ${r.invId.padEnd(invWidth)}`;
|
|
4142
|
+
})
|
|
4143
|
+
];
|
|
4144
|
+
}
|
|
4145
|
+
async function fetchFunctionSchedules(gatewayUrl, tenantJwt, funcName, appSlug, isVerbose) {
|
|
4146
|
+
const scopedUrl = appSlug ? `${gatewayUrl}/v1/apps/${encodeURIComponent(appSlug)}/functions/${encodeURIComponent(funcName)}/schedules` : `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/schedules`;
|
|
4147
|
+
if (isVerbose) console.log(`[wawesome:verbose] GET ${scopedUrl}`);
|
|
4148
|
+
let res = await fetch(scopedUrl, {
|
|
4149
|
+
method: "GET",
|
|
4150
|
+
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
4151
|
+
});
|
|
4152
|
+
if (res.status === 404 && appSlug) {
|
|
4153
|
+
const fallbackUrl = `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/schedules`;
|
|
4154
|
+
if (isVerbose) console.log(`[wawesome:verbose] 404 on app-scoped route. Retrying unscoped: GET ${fallbackUrl}`);
|
|
4155
|
+
try {
|
|
4156
|
+
const fallbackRes = await fetch(fallbackUrl, {
|
|
4157
|
+
method: "GET",
|
|
4158
|
+
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
4159
|
+
});
|
|
4160
|
+
if (fallbackRes.ok) res = fallbackRes;
|
|
4161
|
+
} catch {}
|
|
4162
|
+
}
|
|
4163
|
+
if (!res.ok) return null;
|
|
4164
|
+
const data = await res.json();
|
|
4165
|
+
return Array.isArray(data.schedules) ? data.schedules : [];
|
|
4166
|
+
}
|
|
4167
|
+
async function fetchAppFunctions(gatewayUrl, tenantJwt, appSlug, isVerbose) {
|
|
4168
|
+
const url = `${gatewayUrl}/v1/apps/${encodeURIComponent(appSlug)}/functions`;
|
|
4169
|
+
if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
|
|
4170
|
+
const res = await fetch(url, {
|
|
4171
|
+
method: "GET",
|
|
4172
|
+
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
4173
|
+
});
|
|
4174
|
+
if (!res.ok) return null;
|
|
4175
|
+
return await res.json();
|
|
4176
|
+
}
|
|
4177
|
+
async function pauseScheduleApi(gatewayUrl, tenantJwt, funcName, scheduleName, appSlug, reason, isVerbose) {
|
|
4178
|
+
const scopedUrl = appSlug ? `${gatewayUrl}/v1/apps/${encodeURIComponent(appSlug)}/functions/${encodeURIComponent(funcName)}/schedules/${encodeURIComponent(scheduleName)}/pause` : `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/schedules/${encodeURIComponent(scheduleName)}/pause`;
|
|
4179
|
+
const body = reason?.trim() ? JSON.stringify({ reason: reason.trim() }) : void 0;
|
|
4180
|
+
if (isVerbose) console.log(`[wawesome:verbose] POST ${scopedUrl}`);
|
|
4181
|
+
let res = await fetch(scopedUrl, {
|
|
4182
|
+
method: "POST",
|
|
4183
|
+
headers: {
|
|
4184
|
+
Authorization: `Bearer ${tenantJwt}`,
|
|
4185
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
4186
|
+
},
|
|
4187
|
+
body
|
|
4188
|
+
});
|
|
4189
|
+
if (res.status === 404 && appSlug) {
|
|
4190
|
+
const fallbackUrl = `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/schedules/${encodeURIComponent(scheduleName)}/pause`;
|
|
4191
|
+
if (isVerbose) console.log(`[wawesome:verbose] 404 on app-scoped route. Retrying unscoped: POST ${fallbackUrl}`);
|
|
4192
|
+
try {
|
|
4193
|
+
const fallbackRes = await fetch(fallbackUrl, {
|
|
4194
|
+
method: "POST",
|
|
4195
|
+
headers: {
|
|
4196
|
+
Authorization: `Bearer ${tenantJwt}`,
|
|
4197
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
4198
|
+
},
|
|
4199
|
+
body
|
|
4200
|
+
});
|
|
4201
|
+
if (fallbackRes) res = fallbackRes;
|
|
4202
|
+
} catch {}
|
|
4203
|
+
}
|
|
4204
|
+
return res;
|
|
4205
|
+
}
|
|
4206
|
+
async function resumeScheduleApi(gatewayUrl, tenantJwt, funcName, scheduleName, appSlug, isVerbose) {
|
|
4207
|
+
const scopedUrl = appSlug ? `${gatewayUrl}/v1/apps/${encodeURIComponent(appSlug)}/functions/${encodeURIComponent(funcName)}/schedules/${encodeURIComponent(scheduleName)}/resume` : `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/schedules/${encodeURIComponent(scheduleName)}/resume`;
|
|
4208
|
+
if (isVerbose) console.log(`[wawesome:verbose] POST ${scopedUrl}`);
|
|
4209
|
+
let res = await fetch(scopedUrl, {
|
|
4210
|
+
method: "POST",
|
|
4211
|
+
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
4212
|
+
});
|
|
4213
|
+
if (res.status === 404 && appSlug) {
|
|
4214
|
+
const fallbackUrl = `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/schedules/${encodeURIComponent(scheduleName)}/resume`;
|
|
4215
|
+
if (isVerbose) console.log(`[wawesome:verbose] 404 on app-scoped route. Retrying unscoped: POST ${fallbackUrl}`);
|
|
4216
|
+
try {
|
|
4217
|
+
const fallbackRes = await fetch(fallbackUrl, {
|
|
4218
|
+
method: "POST",
|
|
4219
|
+
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
4220
|
+
});
|
|
4221
|
+
if (fallbackRes) res = fallbackRes;
|
|
4222
|
+
} catch {}
|
|
4223
|
+
}
|
|
4224
|
+
return res;
|
|
4225
|
+
}
|
|
4226
|
+
async function fetchBackgroundRunsApi(gatewayUrl, tenantJwt, funcName, appSlug, queryParams, isVerbose) {
|
|
4227
|
+
const params = new URLSearchParams();
|
|
4228
|
+
if (queryParams?.limit) params.set("limit", String(queryParams.limit));
|
|
4229
|
+
if (queryParams?.page) params.set("page", String(queryParams.page));
|
|
4230
|
+
if (queryParams?.state && queryParams.state.toLowerCase() !== "all") params.set("state", queryParams.state.toLowerCase());
|
|
4231
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
4232
|
+
const scopedUrl = appSlug ? `${gatewayUrl}/v1/apps/${encodeURIComponent(appSlug)}/functions/${encodeURIComponent(funcName)}/runs${qs}` : `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/runs${qs}`;
|
|
4233
|
+
if (isVerbose) console.log(`[wawesome:verbose] GET ${scopedUrl}`);
|
|
4234
|
+
let res = await fetch(scopedUrl, {
|
|
4235
|
+
method: "GET",
|
|
4236
|
+
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
4237
|
+
});
|
|
4238
|
+
if (res.status === 404 && appSlug) {
|
|
4239
|
+
const fallbackUrl = `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/runs${qs}`;
|
|
4240
|
+
if (isVerbose) console.log(`[wawesome:verbose] 404 on app-scoped route. Retrying unscoped: GET ${fallbackUrl}`);
|
|
4241
|
+
try {
|
|
4242
|
+
const fallbackRes = await fetch(fallbackUrl, {
|
|
4243
|
+
method: "GET",
|
|
4244
|
+
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
4245
|
+
});
|
|
4246
|
+
if (fallbackRes) res = fallbackRes;
|
|
4247
|
+
} catch {}
|
|
4248
|
+
}
|
|
4249
|
+
return res;
|
|
4250
|
+
}
|
|
4251
|
+
function requireCredentials() {
|
|
4252
|
+
const creds = readCredentials();
|
|
4253
|
+
if (!creds) {
|
|
4254
|
+
console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
|
|
4255
|
+
process.exit(1);
|
|
4256
|
+
}
|
|
4257
|
+
return creds;
|
|
4258
|
+
}
|
|
4259
|
+
async function listSchedulesCommand(target, options = {}) {
|
|
4260
|
+
const isVerbose = Boolean(options.verbose);
|
|
4261
|
+
const creds = requireCredentials();
|
|
4262
|
+
const config = readFunctionConfig();
|
|
4263
|
+
const explicitApp = options.app;
|
|
4264
|
+
const appSlug = explicitApp || config?.app;
|
|
4265
|
+
const funcName = target || options.function || (explicitApp ? void 0 : config?.function);
|
|
4266
|
+
if (funcName) {
|
|
4267
|
+
const schedules = await fetchFunctionSchedules(creds.gateway_url, creds.tenant_jwt, funcName, appSlug, isVerbose);
|
|
4268
|
+
if (schedules === null) {
|
|
4269
|
+
if (target && !options.app) {
|
|
4270
|
+
const appFuncs = await fetchAppFunctions(creds.gateway_url, creds.tenant_jwt, target, isVerbose);
|
|
4271
|
+
if (appFuncs !== null) {
|
|
4272
|
+
const functionSchedules = [];
|
|
4273
|
+
for (const item of appFuncs) {
|
|
4274
|
+
const scheds = await fetchFunctionSchedules(creds.gateway_url, creds.tenant_jwt, item.function.name, target, isVerbose);
|
|
4275
|
+
functionSchedules.push({
|
|
4276
|
+
functionName: item.function.name,
|
|
4277
|
+
schedules: scheds || []
|
|
4278
|
+
});
|
|
4279
|
+
}
|
|
4280
|
+
const lines = formatAppSchedules(target, functionSchedules);
|
|
4281
|
+
for (const line of lines) console.log(line);
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
console.error(`[wawesome] Error: Function '${funcName}' not found.`);
|
|
4286
|
+
process.exit(1);
|
|
4287
|
+
}
|
|
4288
|
+
const lines = formatFunctionSchedules(funcName, appSlug, schedules);
|
|
4289
|
+
for (const line of lines) console.log(line);
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
if (appSlug) {
|
|
4293
|
+
const appFuncs = await fetchAppFunctions(creds.gateway_url, creds.tenant_jwt, appSlug, isVerbose);
|
|
4294
|
+
if (appFuncs === null) {
|
|
4295
|
+
console.error(`[wawesome] Error: App '${appSlug}' not found.`);
|
|
4296
|
+
process.exit(1);
|
|
4297
|
+
}
|
|
4298
|
+
const functionSchedules = [];
|
|
4299
|
+
for (const item of appFuncs) {
|
|
4300
|
+
const scheds = await fetchFunctionSchedules(creds.gateway_url, creds.tenant_jwt, item.function.name, appSlug, isVerbose);
|
|
4301
|
+
functionSchedules.push({
|
|
4302
|
+
functionName: item.function.name,
|
|
4303
|
+
schedules: scheds || []
|
|
4304
|
+
});
|
|
4305
|
+
}
|
|
4306
|
+
const lines = formatAppSchedules(appSlug, functionSchedules);
|
|
4307
|
+
for (const line of lines) console.log(line);
|
|
4308
|
+
return;
|
|
4309
|
+
}
|
|
4310
|
+
console.error("[wawesome] Error: Missing function or app name.");
|
|
4311
|
+
console.error("[wawesome] Usage: wawesome cron list [function-or-app] or run inside a function directory with wawesome-function.json.");
|
|
4312
|
+
process.exit(1);
|
|
4313
|
+
}
|
|
4314
|
+
async function pauseScheduleCommand(target, subtarget, options = {}) {
|
|
4315
|
+
const isVerbose = Boolean(options.verbose);
|
|
4316
|
+
const creds = requireCredentials();
|
|
4317
|
+
const config = readFunctionConfig();
|
|
4318
|
+
let funcName;
|
|
4319
|
+
let scheduleName;
|
|
4320
|
+
if (target && subtarget) {
|
|
4321
|
+
funcName = target;
|
|
4322
|
+
scheduleName = subtarget;
|
|
4323
|
+
} else if (target) {
|
|
4324
|
+
scheduleName = target;
|
|
4325
|
+
funcName = options.function || config?.function;
|
|
4326
|
+
}
|
|
4327
|
+
if (options.function) funcName = options.function;
|
|
4328
|
+
const appSlug = options.app || config?.app;
|
|
4329
|
+
if (!scheduleName) {
|
|
4330
|
+
console.error("[wawesome] Error: Missing schedule name.");
|
|
4331
|
+
console.error("[wawesome] Usage: wawesome cron pause <schedule-name> [--reason <reason>] or wawesome cron pause <function> <schedule-name>");
|
|
4332
|
+
process.exit(1);
|
|
4333
|
+
}
|
|
4334
|
+
if (!funcName) {
|
|
4335
|
+
console.error("[wawesome] Error: Missing function name.");
|
|
4336
|
+
console.error("[wawesome] Usage: wawesome cron pause <function> <schedule-name> or specify -f/--function <name>.");
|
|
4337
|
+
process.exit(1);
|
|
4338
|
+
}
|
|
4339
|
+
const res = await pauseScheduleApi(creds.gateway_url, creds.tenant_jwt, funcName, scheduleName, appSlug, options.reason, isVerbose);
|
|
4340
|
+
if (!res.ok) {
|
|
4341
|
+
const errorBody = await res.text();
|
|
4342
|
+
if (res.status === 401) {
|
|
4343
|
+
console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
|
|
4344
|
+
process.exit(1);
|
|
4345
|
+
}
|
|
4346
|
+
if (res.status === 404) {
|
|
4347
|
+
console.error(`[wawesome] Error: Schedule '${scheduleName}' or function '${funcName}' not found.`);
|
|
4348
|
+
process.exit(1);
|
|
4349
|
+
}
|
|
4350
|
+
const refusal = rejectionOf(errorBody, res.status, `Failed to pause schedule (HTTP ${res.status}).`);
|
|
4351
|
+
console.error(`[wawesome] Error: ${refusal.message}`);
|
|
4352
|
+
const advice = planLimitAdvice(refusal.reason) || (res.status === 402 ? `Where to resolve it: ${billingPageUrl()}` : "");
|
|
4353
|
+
if (advice) console.error(`[wawesome] ${advice}`);
|
|
4354
|
+
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
4355
|
+
process.exit(1);
|
|
4356
|
+
}
|
|
4357
|
+
const data = await res.json();
|
|
4358
|
+
const displayTarget = appSlug ? `${appSlug}/${funcName}` : funcName;
|
|
4359
|
+
console.log(`[wawesome] ✔ Paused schedule '${data.schedule.name}' on '${displayTarget}'.`);
|
|
4360
|
+
console.log(` State: ${whenItRuns(data.schedule)} (survives future deploys)`);
|
|
4361
|
+
if (data.schedule.pause_reason) console.log(` Reason: ${data.schedule.pause_reason}`);
|
|
4362
|
+
if (data.cancelled_runs && data.cancelled_runs.length > 0) {
|
|
4363
|
+
const count = data.cancelled_runs.length;
|
|
4364
|
+
console.log(` Cancelled ${count} queued run${count === 1 ? "" : "s"} (never started).`);
|
|
4365
|
+
}
|
|
4366
|
+
if (data.running_runs && data.running_runs.length > 0) {
|
|
4367
|
+
const count = data.running_runs.length;
|
|
4368
|
+
console.log(` Left ${count} in-flight run${count === 1 ? "" : "s"} to complete.`);
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
async function resumeScheduleCommand(target, subtarget, options = {}) {
|
|
4372
|
+
const isVerbose = Boolean(options.verbose);
|
|
4373
|
+
const creds = requireCredentials();
|
|
4374
|
+
const config = readFunctionConfig();
|
|
4375
|
+
let funcName;
|
|
4376
|
+
let scheduleName;
|
|
4377
|
+
if (target && subtarget) {
|
|
4378
|
+
funcName = target;
|
|
4379
|
+
scheduleName = subtarget;
|
|
4380
|
+
} else if (target) {
|
|
4381
|
+
scheduleName = target;
|
|
4382
|
+
funcName = options.function || config?.function;
|
|
4383
|
+
}
|
|
4384
|
+
if (options.function) funcName = options.function;
|
|
4385
|
+
const appSlug = options.app || config?.app;
|
|
4386
|
+
if (!scheduleName) {
|
|
4387
|
+
console.error("[wawesome] Error: Missing schedule name.");
|
|
4388
|
+
console.error("[wawesome] Usage: wawesome cron resume <schedule-name> or wawesome cron resume <function> <schedule-name>");
|
|
4389
|
+
process.exit(1);
|
|
4390
|
+
}
|
|
4391
|
+
if (!funcName) {
|
|
4392
|
+
console.error("[wawesome] Error: Missing function name.");
|
|
4393
|
+
console.error("[wawesome] Usage: wawesome cron resume <function> <schedule-name> or specify -f/--function <name>.");
|
|
4394
|
+
process.exit(1);
|
|
4395
|
+
}
|
|
4396
|
+
const res = await resumeScheduleApi(creds.gateway_url, creds.tenant_jwt, funcName, scheduleName, appSlug, isVerbose);
|
|
4397
|
+
if (!res.ok) {
|
|
4398
|
+
const errorBody = await res.text();
|
|
4399
|
+
if (res.status === 401) {
|
|
4400
|
+
console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
|
|
4401
|
+
process.exit(1);
|
|
4402
|
+
}
|
|
4403
|
+
if (res.status === 404) {
|
|
4404
|
+
console.error(`[wawesome] Error: Schedule '${scheduleName}' or function '${funcName}' not found.`);
|
|
4405
|
+
process.exit(1);
|
|
4406
|
+
}
|
|
4407
|
+
const refusal = rejectionOf(errorBody, res.status, `Failed to resume schedule (HTTP ${res.status}).`);
|
|
4408
|
+
console.error(`[wawesome] Error: ${refusal.message}`);
|
|
4409
|
+
const advice = planLimitAdvice(refusal.reason) || (res.status === 402 ? `Where to resolve it: ${billingPageUrl()}` : "");
|
|
4410
|
+
if (advice) console.error(`[wawesome] ${advice}`);
|
|
4411
|
+
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
4412
|
+
process.exit(1);
|
|
4413
|
+
}
|
|
4414
|
+
const data = await res.json();
|
|
4415
|
+
const displayTarget = appSlug ? `${appSlug}/${funcName}` : funcName;
|
|
4416
|
+
console.log(`[wawesome] ✔ Resumed schedule '${data.schedule.name}' on '${displayTarget}'.`);
|
|
4417
|
+
console.log(` ${whenItRuns(data.schedule)} (no backfill)`);
|
|
4418
|
+
}
|
|
4419
|
+
async function runHistoryCommand(target, options = {}) {
|
|
4420
|
+
const isVerbose = Boolean(options.verbose);
|
|
4421
|
+
const creds = requireCredentials();
|
|
4422
|
+
const config = readFunctionConfig();
|
|
4423
|
+
const funcName = target || options.function || config?.function;
|
|
4424
|
+
const appSlug = options.app || config?.app;
|
|
4425
|
+
if (!funcName) {
|
|
4426
|
+
console.error("[wawesome] Error: Missing function name.");
|
|
4427
|
+
console.error("[wawesome] Usage: wawesome cron history [function-name] or run inside a function directory with wawesome-function.json.");
|
|
4428
|
+
process.exit(1);
|
|
4429
|
+
}
|
|
4430
|
+
const limit = options.limit ? Number(options.limit) : 50;
|
|
4431
|
+
const stateFilter = options.state || options.status;
|
|
4432
|
+
const res = await fetchBackgroundRunsApi(creds.gateway_url, creds.tenant_jwt, funcName, appSlug, {
|
|
4433
|
+
state: stateFilter,
|
|
4434
|
+
limit
|
|
4435
|
+
}, isVerbose);
|
|
4436
|
+
if (!res.ok) {
|
|
4437
|
+
const errorBody = await res.text();
|
|
4438
|
+
if (res.status === 401) {
|
|
4439
|
+
console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
|
|
4440
|
+
process.exit(1);
|
|
4441
|
+
}
|
|
4442
|
+
if (res.status === 404) {
|
|
4443
|
+
console.error(`[wawesome] Error: Function '${funcName}' not found.`);
|
|
4444
|
+
process.exit(1);
|
|
4445
|
+
}
|
|
4446
|
+
const refusal = rejectionOf(errorBody, res.status, `Failed to fetch run history (HTTP ${res.status}).`);
|
|
4447
|
+
console.error(`[wawesome] Error: ${refusal.message}`);
|
|
4448
|
+
const advice = planLimitAdvice(refusal.reason) || (res.status === 402 ? `Where to resolve it: ${billingPageUrl()}` : "");
|
|
4449
|
+
if (advice) console.error(`[wawesome] ${advice}`);
|
|
4450
|
+
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
4451
|
+
process.exit(1);
|
|
4452
|
+
}
|
|
4453
|
+
const data = await res.json();
|
|
4454
|
+
const displayTarget = appSlug ? `${appSlug}/${funcName}` : funcName;
|
|
4455
|
+
if (!data.runs || data.runs.length === 0) {
|
|
4456
|
+
console.log(`[wawesome] No run history found for '${displayTarget}'.`);
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4459
|
+
const total = data.total ?? data.runs.length;
|
|
4460
|
+
console.log(`\n📜 \x1b[1mRun history for '${displayTarget}' (Showing ${data.runs.length} of ${total} records)\x1b[0m\n`);
|
|
4461
|
+
const tableLines = formatRunHistoryTable(data.runs);
|
|
4462
|
+
for (const line of tableLines) console.log(line);
|
|
4463
|
+
console.log("");
|
|
4464
|
+
}
|
|
4465
|
+
async function cronCommand(action, target, subtarget, options = {}) {
|
|
4466
|
+
const normAction = (action || "").toLowerCase().trim();
|
|
4467
|
+
if (!normAction || normAction === "list" || normAction === "ls") return listSchedulesCommand(target, options);
|
|
4468
|
+
if (normAction === "pause") return pauseScheduleCommand(target, subtarget, options);
|
|
4469
|
+
if (normAction === "resume") return resumeScheduleCommand(target, subtarget, options);
|
|
4470
|
+
if (normAction === "history" || normAction === "runs" || normAction === "log" || normAction === "logs") return runHistoryCommand(target, options);
|
|
4471
|
+
return listSchedulesCommand(action, options);
|
|
4472
|
+
}
|
|
4473
|
+
//#endregion
|
|
3702
4474
|
//#region src/index.ts
|
|
3703
4475
|
const cli = cac("wawesome");
|
|
3704
4476
|
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));
|
|
@@ -3741,6 +4513,40 @@ cli.command("logs [function-name-or-invocation-id]", "View invocation history, f
|
|
|
3741
4513
|
With a function name, --follow streams the function's output continuously across
|
|
3742
4514
|
invocations — new output appears each time the function runs, no need to catch a
|
|
3743
4515
|
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));
|
|
4516
|
+
cli.command("invoke [function]", "Fire a background run of a serverless function").usage(`invoke [function] [options]
|
|
4517
|
+
|
|
4518
|
+
Fire a Function run now through the authenticated API, without deploying anything
|
|
4519
|
+
or waiting for a scheduled tick.
|
|
4520
|
+
|
|
4521
|
+
By default, follows the run's output live and prints the outcome with duration.
|
|
4522
|
+
|
|
4523
|
+
Examples:
|
|
4524
|
+
wawesome invoke # invoke function in current directory
|
|
4525
|
+
wawesome invoke my-function # invoke 'my-function'
|
|
4526
|
+
wawesome invoke --no-follow # fire and exit without following output
|
|
4527
|
+
wawesome invoke -m POST -d '{"k":"v"}' # send custom HTTP method and body`).option("-m, --method <method>", "HTTP method to send to the function (default: POST)").option("-b, --body <body>", "Request body to send to the function").option("-d, --data <data>", "Alias for --body").option("--no-follow", "Do not follow the run's output live").option("-a, --app <app>", "App slug override (defaults to wawesome-function.json)").option("-v, --verbose", "Enable verbose debug output").action((target, options) => invokeCommand(target, options));
|
|
4528
|
+
cli.command("cron [action] [target] [subtarget]", "Manage schedules and read run history").usage(`cron <action> [options]
|
|
4529
|
+
|
|
4530
|
+
Actions:
|
|
4531
|
+
list [function] List schedules for a function or app (default)
|
|
4532
|
+
pause <schedule> Pause a schedule by name
|
|
4533
|
+
resume <schedule> Resume a paused schedule
|
|
4534
|
+
history [function] Read run history for scheduled and manual runs
|
|
4535
|
+
|
|
4536
|
+
Examples:
|
|
4537
|
+
wawesome cron # list schedules for current function
|
|
4538
|
+
wawesome cron list my-function # list schedules for 'my-function'
|
|
4539
|
+
wawesome cron list --app my-app # list schedules across all functions in 'my-app'
|
|
4540
|
+
wawesome cron pause nightly-reconcile # pause a schedule
|
|
4541
|
+
wawesome cron pause nightly-reconcile -r "maint" # pause with reason
|
|
4542
|
+
wawesome cron resume nightly-reconcile # resume a schedule
|
|
4543
|
+
wawesome cron history # view run history
|
|
4544
|
+
wawesome cron history my-function --state failed # filter run history by state`).option("-r, --reason <reason>", "Reason for pausing a schedule").option("-f, --function <function>", "Function name override").option("-a, --app <app>", "App slug override (defaults to wawesome-function.json)").option("-s, --state <state>", "Filter run history by state (pending, running, dispatched, skipped, missed, cancelled, lost, failed)").option("--status <status>", "Alias for --state").option("-l, --limit <limit>", "Limit number of history rows (default: 50)").option("-v, --verbose", "Enable verbose debug output").action((action, target, subtarget, options) => cronCommand(action, target, subtarget, options));
|
|
4545
|
+
cli.command("cron list [target]", "List schedules for a function or app").alias("cron ls").option("-a, --app <app>", "App slug override").option("-v, --verbose", "Enable verbose debug output").action((target, options) => listSchedulesCommand(target, options));
|
|
4546
|
+
cli.command("cron pause <schedule>", "Pause a schedule by name").option("-r, --reason <reason>", "Reason for pausing a schedule").option("-f, --function <function>", "Function name override").option("-a, --app <app>", "App slug override").option("-v, --verbose", "Enable verbose debug output").action((schedule, options) => pauseScheduleCommand(schedule, void 0, options));
|
|
4547
|
+
cli.command("cron resume <schedule>", "Resume a paused schedule").option("-f, --function <function>", "Function name override").option("-a, --app <app>", "App slug override").option("-v, --verbose", "Enable verbose debug output").action((schedule, options) => resumeScheduleCommand(schedule, void 0, options));
|
|
4548
|
+
cli.command("cron history [target]", "Read background run history").alias("cron runs").option("-s, --state <state>", "Filter run history by state (pending, running, dispatched, skipped, missed, cancelled, lost, failed)").option("--status <status>", "Alias for --state").option("-l, --limit <limit>", "Limit number of history rows (default: 50)").option("-a, --app <app>", "App slug override").option("-v, --verbose", "Enable verbose debug output").action((target, options) => runHistoryCommand(target, options));
|
|
4549
|
+
cli.command("schedules [action] [target]", "Alias for 'cron'").option("-a, --app <app>", "App slug override").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => cronCommand(action, target, void 0, options));
|
|
3744
4550
|
cli.help();
|
|
3745
4551
|
cli.version(CLI_VERSION);
|
|
3746
4552
|
cli.parse();
|