@blinkdotnew/cli 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +379 -24
- package/package.json +1 -1
- package/src/cli.ts +9 -0
- package/src/commands/auth.ts +1 -1
- package/src/commands/project.ts +2 -2
- package/src/commands/queue.ts +414 -0
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
7
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
8
|
-
}) : x)(function(x) {
|
|
9
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
10
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
11
|
-
});
|
|
12
4
|
var __esm = (fn, res) => function __init() {
|
|
13
5
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
14
6
|
};
|
|
@@ -16,15 +8,6 @@ var __export = (target, all) => {
|
|
|
16
8
|
for (var name in all)
|
|
17
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
10
|
};
|
|
19
|
-
var __copyProps = (to, from, except, desc) => {
|
|
20
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
-
for (let key of __getOwnPropNames(from))
|
|
22
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
-
}
|
|
25
|
-
return to;
|
|
26
|
-
};
|
|
27
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
11
|
|
|
29
12
|
// src/lib/project.ts
|
|
30
13
|
var project_exports = {};
|
|
@@ -2076,9 +2059,9 @@ After linking, most commands work without specifying a project_id:
|
|
|
2076
2059
|
clearProjectConfig();
|
|
2077
2060
|
console.log("Unlinked.");
|
|
2078
2061
|
});
|
|
2079
|
-
program2.command("status").description("Show current agent, project, and auth context").action(() => {
|
|
2062
|
+
program2.command("status").description("Show current agent, project, and auth context").action(async () => {
|
|
2080
2063
|
const config = readProjectConfig();
|
|
2081
|
-
const { resolveAgentId: resolveAgentId3 } = (init_agent(),
|
|
2064
|
+
const { resolveAgentId: resolveAgentId3 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
|
|
2082
2065
|
const agentId = resolveAgentId3();
|
|
2083
2066
|
const agentSource = process.env.BLINK_AGENT_ID ? "BLINK_AGENT_ID env" : process.env.BLINK_ACTIVE_AGENT ? "BLINK_ACTIVE_AGENT env" : null;
|
|
2084
2067
|
if (agentId) {
|
|
@@ -2106,12 +2089,12 @@ After linking, most commands work without specifying a project_id:
|
|
|
2106
2089
|
// src/commands/auth.ts
|
|
2107
2090
|
import chalk10 from "chalk";
|
|
2108
2091
|
import { createServer } from "http";
|
|
2092
|
+
import { randomBytes } from "crypto";
|
|
2109
2093
|
var TIMEOUT_MS = 12e4;
|
|
2110
2094
|
function getBaseUrl() {
|
|
2111
2095
|
return process.env.BLINK_APP_URL || "https://blink.new";
|
|
2112
2096
|
}
|
|
2113
2097
|
function generateState() {
|
|
2114
|
-
const { randomBytes } = __require("crypto");
|
|
2115
2098
|
return randomBytes(24).toString("base64url");
|
|
2116
2099
|
}
|
|
2117
2100
|
function findFreePort() {
|
|
@@ -3831,6 +3814,370 @@ Examples:
|
|
|
3831
3814
|
});
|
|
3832
3815
|
}
|
|
3833
3816
|
|
|
3817
|
+
// src/commands/queue.ts
|
|
3818
|
+
init_project();
|
|
3819
|
+
import chalk25 from "chalk";
|
|
3820
|
+
function registerQueueCommands(program2) {
|
|
3821
|
+
const queue = program2.command("queue").description("Manage background task queues and cron schedules").addHelpText("after", `
|
|
3822
|
+
Blink Queue provides background task processing and cron scheduling.
|
|
3823
|
+
Tasks are delivered to your backend at /api/queue. Pro+ plans only.
|
|
3824
|
+
|
|
3825
|
+
Examples:
|
|
3826
|
+
$ blink queue enqueue send-email --payload '{"to":"user@example.com"}'
|
|
3827
|
+
$ blink queue schedule create daily-cleanup "0 3 * * *"
|
|
3828
|
+
$ blink queue list --status pending
|
|
3829
|
+
$ blink queue stats
|
|
3830
|
+
$ blink queue dlq list
|
|
3831
|
+
`);
|
|
3832
|
+
registerEnqueue(queue);
|
|
3833
|
+
registerList(queue);
|
|
3834
|
+
registerGet(queue);
|
|
3835
|
+
registerCancel(queue);
|
|
3836
|
+
registerStats(queue);
|
|
3837
|
+
registerScheduleCommands(queue);
|
|
3838
|
+
registerQueueCrud(queue);
|
|
3839
|
+
registerDlqCommands(queue);
|
|
3840
|
+
}
|
|
3841
|
+
function queuePath(projectId, path) {
|
|
3842
|
+
return `/api/project/${projectId}/queue/${path}`;
|
|
3843
|
+
}
|
|
3844
|
+
function registerEnqueue(queue) {
|
|
3845
|
+
queue.command("enqueue <taskName>").description("Enqueue a background task").option("--payload <json>", "JSON payload", "{}").option("--queue <name>", "Named queue for FIFO ordering").option("--delay <duration>", "Delay before execution (e.g. 30s, 5m, 1h)").option("--retries <n>", "Max retry attempts", "3").option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m)").addHelpText("after", `
|
|
3846
|
+
Examples:
|
|
3847
|
+
$ blink queue enqueue send-email --payload '{"to":"user@example.com"}'
|
|
3848
|
+
$ blink queue enqueue process-image --queue media --delay 5m
|
|
3849
|
+
$ blink queue enqueue cleanup --retries 5 --timeout 60s
|
|
3850
|
+
`).action(async (taskName, opts) => {
|
|
3851
|
+
requireToken();
|
|
3852
|
+
const projectId = requireProjectId();
|
|
3853
|
+
let payload = {};
|
|
3854
|
+
if (opts.payload && opts.payload !== "{}") {
|
|
3855
|
+
payload = JSON.parse(opts.payload);
|
|
3856
|
+
}
|
|
3857
|
+
const body = { taskName, payload, retries: parseInt(opts.retries) };
|
|
3858
|
+
if (opts.queue) body.queue = opts.queue;
|
|
3859
|
+
if (opts.delay) body.delay = opts.delay;
|
|
3860
|
+
if (opts.timeout) body.timeout = opts.timeout;
|
|
3861
|
+
const result = await withSpinner(
|
|
3862
|
+
`Enqueuing ${taskName}...`,
|
|
3863
|
+
() => appRequest(queuePath(projectId, "enqueue"), { body })
|
|
3864
|
+
);
|
|
3865
|
+
if (isJsonMode()) return printJson(result);
|
|
3866
|
+
printSuccess(`Task enqueued: ${result?.taskId ?? "ok"}`);
|
|
3867
|
+
if (result?.taskId) printKv("Task ID", result.taskId);
|
|
3868
|
+
if (opts.queue) printKv("Queue", opts.queue);
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
function registerList(queue) {
|
|
3872
|
+
queue.command("list").description("List tasks").option("--status <status>", "Filter: pending, completed, failed, dead").option("--queue <name>", "Filter by queue name").option("--limit <n>", "Max results", "20").addHelpText("after", `
|
|
3873
|
+
Examples:
|
|
3874
|
+
$ blink queue list
|
|
3875
|
+
$ blink queue list --status pending
|
|
3876
|
+
$ blink queue list --queue emails --limit 50
|
|
3877
|
+
$ blink queue list --json
|
|
3878
|
+
`).action(async (opts) => {
|
|
3879
|
+
requireToken();
|
|
3880
|
+
const projectId = requireProjectId();
|
|
3881
|
+
const params = new URLSearchParams();
|
|
3882
|
+
if (opts.status) params.set("status", opts.status);
|
|
3883
|
+
if (opts.queue) params.set("queue", opts.queue);
|
|
3884
|
+
if (opts.limit) params.set("limit", opts.limit);
|
|
3885
|
+
const result = await withSpinner(
|
|
3886
|
+
"Loading tasks...",
|
|
3887
|
+
() => appRequest(queuePath(projectId, `tasks?${params}`))
|
|
3888
|
+
);
|
|
3889
|
+
const tasks = result?.tasks ?? result ?? [];
|
|
3890
|
+
if (isJsonMode()) return printJson(tasks);
|
|
3891
|
+
if (!tasks.length) {
|
|
3892
|
+
console.log(chalk25.dim("(no tasks)"));
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
const table = createTable(["ID", "Task", "Status", "Queue", "Created"]);
|
|
3896
|
+
for (const t of tasks) {
|
|
3897
|
+
table.push([t.id, t.task_name, t.status, t.queue ?? "-", t.created_at?.slice(0, 19) ?? "-"]);
|
|
3898
|
+
}
|
|
3899
|
+
console.log(table.toString());
|
|
3900
|
+
});
|
|
3901
|
+
}
|
|
3902
|
+
function registerGet(queue) {
|
|
3903
|
+
queue.command("get <taskId>").description("Get task details").addHelpText("after", `
|
|
3904
|
+
Examples:
|
|
3905
|
+
$ blink queue get tsk_abc123
|
|
3906
|
+
$ blink queue get tsk_abc123 --json
|
|
3907
|
+
`).action(async (taskId) => {
|
|
3908
|
+
requireToken();
|
|
3909
|
+
const projectId = requireProjectId();
|
|
3910
|
+
const result = await withSpinner(
|
|
3911
|
+
"Loading task...",
|
|
3912
|
+
() => appRequest(queuePath(projectId, `tasks/${taskId}`))
|
|
3913
|
+
);
|
|
3914
|
+
if (isJsonMode()) return printJson(result);
|
|
3915
|
+
const t = result?.task ?? result;
|
|
3916
|
+
printKv("ID", t.id);
|
|
3917
|
+
printKv("Task", t.task_name);
|
|
3918
|
+
printKv("Status", t.status);
|
|
3919
|
+
if (t.queue) printKv("Queue", t.queue);
|
|
3920
|
+
if (t.error) printKv("Error", chalk25.red(t.error));
|
|
3921
|
+
printKv("Created", t.created_at);
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
function registerCancel(queue) {
|
|
3925
|
+
queue.command("cancel <taskId>").description("Cancel a pending task").addHelpText("after", `
|
|
3926
|
+
Examples:
|
|
3927
|
+
$ blink queue cancel tsk_abc123
|
|
3928
|
+
`).action(async (taskId) => {
|
|
3929
|
+
requireToken();
|
|
3930
|
+
const projectId = requireProjectId();
|
|
3931
|
+
await withSpinner(
|
|
3932
|
+
"Cancelling task...",
|
|
3933
|
+
() => appRequest(queuePath(projectId, `tasks/${taskId}`), { method: "DELETE" })
|
|
3934
|
+
);
|
|
3935
|
+
if (isJsonMode()) return printJson({ status: "ok", task_id: taskId });
|
|
3936
|
+
printSuccess(`Cancelled ${taskId}`);
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
function registerStats(queue) {
|
|
3940
|
+
queue.command("stats").description("Show queue statistics").addHelpText("after", `
|
|
3941
|
+
Examples:
|
|
3942
|
+
$ blink queue stats
|
|
3943
|
+
$ blink queue stats --json
|
|
3944
|
+
`).action(async () => {
|
|
3945
|
+
requireToken();
|
|
3946
|
+
const projectId = requireProjectId();
|
|
3947
|
+
const result = await withSpinner(
|
|
3948
|
+
"Loading stats...",
|
|
3949
|
+
() => appRequest(queuePath(projectId, "stats"))
|
|
3950
|
+
);
|
|
3951
|
+
if (isJsonMode()) return printJson(result);
|
|
3952
|
+
console.log();
|
|
3953
|
+
printKv("Pending", String(result?.pending ?? 0));
|
|
3954
|
+
printKv("Completed", String(result?.completed ?? 0));
|
|
3955
|
+
printKv("Failed", String(result?.failed ?? 0));
|
|
3956
|
+
printKv("Dead", String(result?.dead ?? 0));
|
|
3957
|
+
printKv("Schedules", String(result?.schedules ?? 0));
|
|
3958
|
+
if (result?.tier) printKv("Tier", result.tier);
|
|
3959
|
+
console.log();
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
function registerScheduleCommands(queue) {
|
|
3963
|
+
const sched = queue.command("schedule").description("Manage cron schedules").addHelpText("after", `
|
|
3964
|
+
Examples:
|
|
3965
|
+
$ blink queue schedule create daily-report "0 9 * * *"
|
|
3966
|
+
$ blink queue schedule list
|
|
3967
|
+
$ blink queue schedule pause daily-report
|
|
3968
|
+
$ blink queue schedule delete daily-report
|
|
3969
|
+
`);
|
|
3970
|
+
sched.command("create <name> <cron>").description("Create or update a cron schedule").option("--payload <json>", "JSON payload", "{}").option("--timezone <tz>", "Timezone (e.g. America/New_York)", "UTC").option("--retries <n>", "Max retries per execution", "3").addHelpText("after", `
|
|
3971
|
+
Examples:
|
|
3972
|
+
$ blink queue schedule create daily-report "0 9 * * *"
|
|
3973
|
+
$ blink queue schedule create cleanup "0 3 * * *" --timezone America/New_York
|
|
3974
|
+
$ blink queue schedule create sync "*/15 * * * *" --payload '{"source":"api"}'
|
|
3975
|
+
`).action(async (name, cron, opts) => {
|
|
3976
|
+
requireToken();
|
|
3977
|
+
const projectId = requireProjectId();
|
|
3978
|
+
let payload = {};
|
|
3979
|
+
if (opts.payload && opts.payload !== "{}") payload = JSON.parse(opts.payload);
|
|
3980
|
+
const body = { name, cron, payload, timezone: opts.timezone, retries: parseInt(opts.retries) };
|
|
3981
|
+
const result = await withSpinner(
|
|
3982
|
+
`Creating schedule "${name}"...`,
|
|
3983
|
+
() => appRequest(queuePath(projectId, "schedule"), { body })
|
|
3984
|
+
);
|
|
3985
|
+
if (isJsonMode()) return printJson(result);
|
|
3986
|
+
printSuccess(`Schedule "${name}" created: ${cron}`);
|
|
3987
|
+
if (result?.scheduleId) printKv("Schedule ID", result.scheduleId);
|
|
3988
|
+
});
|
|
3989
|
+
sched.command("list").description("List all cron schedules").addHelpText("after", `
|
|
3990
|
+
Examples:
|
|
3991
|
+
$ blink queue schedule list
|
|
3992
|
+
$ blink queue schedule list --json
|
|
3993
|
+
`).action(async () => {
|
|
3994
|
+
requireToken();
|
|
3995
|
+
const projectId = requireProjectId();
|
|
3996
|
+
const result = await withSpinner(
|
|
3997
|
+
"Loading schedules...",
|
|
3998
|
+
() => appRequest(queuePath(projectId, "schedules"))
|
|
3999
|
+
);
|
|
4000
|
+
const schedules = result?.schedules ?? result ?? [];
|
|
4001
|
+
if (isJsonMode()) return printJson(schedules);
|
|
4002
|
+
if (!schedules.length) {
|
|
4003
|
+
console.log(chalk25.dim("(no schedules)"));
|
|
4004
|
+
return;
|
|
4005
|
+
}
|
|
4006
|
+
const table = createTable(["Name", "Cron", "Timezone", "Paused", "Next Run"]);
|
|
4007
|
+
for (const s of schedules) {
|
|
4008
|
+
table.push([s.name, s.cron, s.timezone ?? "UTC", s.is_paused ? "yes" : "no", s.next_run_at ?? "-"]);
|
|
4009
|
+
}
|
|
4010
|
+
console.log(table.toString());
|
|
4011
|
+
});
|
|
4012
|
+
sched.command("pause <name>").description("Pause a schedule").action(async (name) => {
|
|
4013
|
+
requireToken();
|
|
4014
|
+
const projectId = requireProjectId();
|
|
4015
|
+
await withSpinner(
|
|
4016
|
+
`Pausing "${name}"...`,
|
|
4017
|
+
() => appRequest(queuePath(projectId, `schedules/${name}/pause`), { method: "POST", body: {} })
|
|
4018
|
+
);
|
|
4019
|
+
if (isJsonMode()) return printJson({ status: "ok", name });
|
|
4020
|
+
printSuccess(`Schedule "${name}" paused`);
|
|
4021
|
+
});
|
|
4022
|
+
sched.command("resume <name>").description("Resume a paused schedule").action(async (name) => {
|
|
4023
|
+
requireToken();
|
|
4024
|
+
const projectId = requireProjectId();
|
|
4025
|
+
await withSpinner(
|
|
4026
|
+
`Resuming "${name}"...`,
|
|
4027
|
+
() => appRequest(queuePath(projectId, `schedules/${name}/resume`), { method: "POST", body: {} })
|
|
4028
|
+
);
|
|
4029
|
+
if (isJsonMode()) return printJson({ status: "ok", name });
|
|
4030
|
+
printSuccess(`Schedule "${name}" resumed`);
|
|
4031
|
+
});
|
|
4032
|
+
sched.command("delete <name>").description("Delete a schedule").option("--yes", "Skip confirmation").action(async (name, opts) => {
|
|
4033
|
+
requireToken();
|
|
4034
|
+
const projectId = requireProjectId();
|
|
4035
|
+
const skip = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode() || !process.stdout.isTTY;
|
|
4036
|
+
if (!skip) {
|
|
4037
|
+
const { confirm } = await import("@clack/prompts");
|
|
4038
|
+
const ok = await confirm({ message: `Delete schedule "${name}"?` });
|
|
4039
|
+
if (!ok) {
|
|
4040
|
+
console.log("Cancelled.");
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
await withSpinner(
|
|
4045
|
+
`Deleting "${name}"...`,
|
|
4046
|
+
() => appRequest(queuePath(projectId, `schedules/${name}`), { method: "DELETE" })
|
|
4047
|
+
);
|
|
4048
|
+
if (isJsonMode()) return printJson({ status: "ok", name });
|
|
4049
|
+
printSuccess(`Schedule "${name}" deleted`);
|
|
4050
|
+
});
|
|
4051
|
+
}
|
|
4052
|
+
function registerQueueCrud(queue) {
|
|
4053
|
+
queue.command("create-queue <name>").description("Create a named queue with parallelism control").option("--parallelism <n>", "Max concurrent tasks", "1").addHelpText("after", `
|
|
4054
|
+
Named queues provide FIFO ordering and parallelism control.
|
|
4055
|
+
|
|
4056
|
+
Examples:
|
|
4057
|
+
$ blink queue create-queue emails
|
|
4058
|
+
$ blink queue create-queue media --parallelism 5
|
|
4059
|
+
`).action(async (name, opts) => {
|
|
4060
|
+
requireToken();
|
|
4061
|
+
const projectId = requireProjectId();
|
|
4062
|
+
const result = await withSpinner(
|
|
4063
|
+
`Creating queue "${name}"...`,
|
|
4064
|
+
() => appRequest(queuePath(projectId, "queues"), { body: { name, parallelism: parseInt(opts.parallelism) } })
|
|
4065
|
+
);
|
|
4066
|
+
if (isJsonMode()) return printJson(result);
|
|
4067
|
+
printSuccess(`Queue "${name}" created (parallelism: ${opts.parallelism})`);
|
|
4068
|
+
});
|
|
4069
|
+
queue.command("queues").description("List all named queues").addHelpText("after", `
|
|
4070
|
+
Examples:
|
|
4071
|
+
$ blink queue queues
|
|
4072
|
+
$ blink queue queues --json
|
|
4073
|
+
`).action(async () => {
|
|
4074
|
+
requireToken();
|
|
4075
|
+
const projectId = requireProjectId();
|
|
4076
|
+
const result = await withSpinner(
|
|
4077
|
+
"Loading queues...",
|
|
4078
|
+
() => appRequest(queuePath(projectId, "queues"))
|
|
4079
|
+
);
|
|
4080
|
+
const queues = result?.queues ?? result ?? [];
|
|
4081
|
+
if (isJsonMode()) return printJson(queues);
|
|
4082
|
+
if (!queues.length) {
|
|
4083
|
+
console.log(chalk25.dim("(no named queues)"));
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
4086
|
+
const table = createTable(["Name", "Parallelism", "Pending", "Created"]);
|
|
4087
|
+
for (const q of queues) {
|
|
4088
|
+
table.push([q.name, String(q.parallelism ?? 1), String(q.pending ?? 0), q.created_at?.slice(0, 10) ?? "-"]);
|
|
4089
|
+
}
|
|
4090
|
+
console.log(table.toString());
|
|
4091
|
+
});
|
|
4092
|
+
queue.command("delete-queue <name>").description("Delete a named queue").option("--yes", "Skip confirmation").action(async (name, opts) => {
|
|
4093
|
+
requireToken();
|
|
4094
|
+
const projectId = requireProjectId();
|
|
4095
|
+
const skip = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode() || !process.stdout.isTTY;
|
|
4096
|
+
if (!skip) {
|
|
4097
|
+
const { confirm } = await import("@clack/prompts");
|
|
4098
|
+
const ok = await confirm({ message: `Delete queue "${name}"? Pending tasks will be lost.` });
|
|
4099
|
+
if (!ok) {
|
|
4100
|
+
console.log("Cancelled.");
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
await withSpinner(
|
|
4105
|
+
`Deleting queue "${name}"...`,
|
|
4106
|
+
() => appRequest(queuePath(projectId, `queues/${name}`), { method: "DELETE" })
|
|
4107
|
+
);
|
|
4108
|
+
if (isJsonMode()) return printJson({ status: "ok", name });
|
|
4109
|
+
printSuccess(`Queue "${name}" deleted`);
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
4112
|
+
function registerDlqCommands(queue) {
|
|
4113
|
+
const dlq = queue.command("dlq").description("Manage dead letter queue (failed tasks)").addHelpText("after", `
|
|
4114
|
+
Tasks that exhaust all retries are moved to the dead letter queue.
|
|
4115
|
+
|
|
4116
|
+
Examples:
|
|
4117
|
+
$ blink queue dlq list
|
|
4118
|
+
$ blink queue dlq retry <taskId>
|
|
4119
|
+
$ blink queue dlq purge --yes
|
|
4120
|
+
`);
|
|
4121
|
+
dlq.command("list").description("List dead tasks").action(async () => {
|
|
4122
|
+
requireToken();
|
|
4123
|
+
const projectId = requireProjectId();
|
|
4124
|
+
const result = await withSpinner(
|
|
4125
|
+
"Loading DLQ...",
|
|
4126
|
+
() => appRequest(queuePath(projectId, "dlq"))
|
|
4127
|
+
);
|
|
4128
|
+
const tasks = result?.tasks ?? result ?? [];
|
|
4129
|
+
if (isJsonMode()) return printJson(tasks);
|
|
4130
|
+
if (!tasks.length) {
|
|
4131
|
+
console.log(chalk25.dim("(no dead tasks)"));
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
const table = createTable(["ID", "Task", "Error", "Failed At"]);
|
|
4135
|
+
for (const t of tasks) {
|
|
4136
|
+
table.push([t.id, t.task_name, (t.error ?? "-").slice(0, 40), t.failed_at?.slice(0, 19) ?? "-"]);
|
|
4137
|
+
}
|
|
4138
|
+
console.log(table.toString());
|
|
4139
|
+
});
|
|
4140
|
+
dlq.command("retry <taskId>").description("Retry a dead task").action(async (taskId) => {
|
|
4141
|
+
requireToken();
|
|
4142
|
+
const projectId = requireProjectId();
|
|
4143
|
+
await withSpinner(
|
|
4144
|
+
`Retrying ${taskId}...`,
|
|
4145
|
+
() => appRequest(queuePath(projectId, `dlq/${taskId}/retry`), { method: "POST", body: {} })
|
|
4146
|
+
);
|
|
4147
|
+
if (isJsonMode()) return printJson({ status: "ok", task_id: taskId });
|
|
4148
|
+
printSuccess(`Retried ${taskId}`);
|
|
4149
|
+
});
|
|
4150
|
+
dlq.command("delete <taskId>").description("Delete a dead task").action(async (taskId) => {
|
|
4151
|
+
requireToken();
|
|
4152
|
+
const projectId = requireProjectId();
|
|
4153
|
+
await withSpinner(
|
|
4154
|
+
`Deleting ${taskId}...`,
|
|
4155
|
+
() => appRequest(queuePath(projectId, `dlq/${taskId}`), { method: "DELETE" })
|
|
4156
|
+
);
|
|
4157
|
+
if (isJsonMode()) return printJson({ status: "ok", task_id: taskId });
|
|
4158
|
+
printSuccess(`Deleted ${taskId} from DLQ`);
|
|
4159
|
+
});
|
|
4160
|
+
dlq.command("purge").description("Delete all dead tasks").option("--yes", "Skip confirmation").action(async (opts) => {
|
|
4161
|
+
requireToken();
|
|
4162
|
+
const projectId = requireProjectId();
|
|
4163
|
+
const skip = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode() || !process.stdout.isTTY;
|
|
4164
|
+
if (!skip) {
|
|
4165
|
+
const { confirm } = await import("@clack/prompts");
|
|
4166
|
+
const ok = await confirm({ message: "Purge all dead tasks? This cannot be undone." });
|
|
4167
|
+
if (!ok) {
|
|
4168
|
+
console.log("Cancelled.");
|
|
4169
|
+
return;
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
await withSpinner(
|
|
4173
|
+
"Purging DLQ...",
|
|
4174
|
+
() => appRequest(queuePath(projectId, "dlq"), { method: "DELETE" })
|
|
4175
|
+
);
|
|
4176
|
+
if (isJsonMode()) return printJson({ status: "ok" });
|
|
4177
|
+
printSuccess("DLQ purged");
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
|
|
3834
4181
|
// src/cli.ts
|
|
3835
4182
|
var require2 = createRequire(import.meta.url);
|
|
3836
4183
|
var pkg = require2("../package.json");
|
|
@@ -3953,6 +4300,13 @@ Functions (legacy edge functions):
|
|
|
3953
4300
|
$ blink functions logs index View function logs
|
|
3954
4301
|
$ blink functions delete old-fn --yes Delete a function
|
|
3955
4302
|
|
|
4303
|
+
Queue (background tasks + cron, Pro+):
|
|
4304
|
+
$ blink queue enqueue send-email --payload '{"to":"a@b.com"}'
|
|
4305
|
+
$ blink queue schedule create daily "0 9 * * *"
|
|
4306
|
+
$ blink queue list --status pending List tasks
|
|
4307
|
+
$ blink queue stats Queue overview
|
|
4308
|
+
$ blink queue dlq list Dead letter queue
|
|
4309
|
+
|
|
3956
4310
|
Versions:
|
|
3957
4311
|
$ blink versions list List saved versions
|
|
3958
4312
|
$ blink versions save --message "v1.0" Save a snapshot
|
|
@@ -4023,6 +4377,7 @@ registerWorkspaceCommands(program);
|
|
|
4023
4377
|
registerVersionCommands(program);
|
|
4024
4378
|
registerBillingCommands(program);
|
|
4025
4379
|
registerTokenCommands(program);
|
|
4380
|
+
registerQueueCommands(program);
|
|
4026
4381
|
program.command("use <project_id>").description("Set active project for this shell session (alternative to blink link)").option("--export", "Output a shell export statement \u2014 use with eval to actually set it").addHelpText("after", `
|
|
4027
4382
|
Examples:
|
|
4028
4383
|
$ blink use proj_xxx Shows the export command to run
|
|
@@ -4038,10 +4393,10 @@ After setting:
|
|
|
4038
4393
|
process.stdout.write(`export BLINK_ACTIVE_PROJECT=${projectId}
|
|
4039
4394
|
`);
|
|
4040
4395
|
} else {
|
|
4041
|
-
const { default:
|
|
4042
|
-
console.log(
|
|
4043
|
-
console.log(
|
|
4044
|
-
console.log(
|
|
4396
|
+
const { default: chalk26 } = await import("chalk");
|
|
4397
|
+
console.log(chalk26.bold("Active project: ") + projectId);
|
|
4398
|
+
console.log(chalk26.dim(`Run: export BLINK_ACTIVE_PROJECT=${projectId}`));
|
|
4399
|
+
console.log(chalk26.dim(`Or: eval $(blink use ${projectId} --export)`));
|
|
4045
4400
|
}
|
|
4046
4401
|
});
|
|
4047
4402
|
program.action(async () => {
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { registerBillingCommands } from './commands/billing.js'
|
|
|
31
31
|
// PAT tokens are deprecated in favor of workspace API keys (blnk_ak_*).
|
|
32
32
|
// The tokens command is kept for backward compat but hidden from help.
|
|
33
33
|
import { registerTokenCommands } from './commands/tokens.js'
|
|
34
|
+
import { registerQueueCommands } from './commands/queue.js'
|
|
34
35
|
|
|
35
36
|
const require = createRequire(import.meta.url)
|
|
36
37
|
const pkg = require('../package.json') as { version: string }
|
|
@@ -164,6 +165,13 @@ Functions (legacy edge functions):
|
|
|
164
165
|
$ blink functions logs index View function logs
|
|
165
166
|
$ blink functions delete old-fn --yes Delete a function
|
|
166
167
|
|
|
168
|
+
Queue (background tasks + cron, Pro+):
|
|
169
|
+
$ blink queue enqueue send-email --payload '{"to":"a@b.com"}'
|
|
170
|
+
$ blink queue schedule create daily "0 9 * * *"
|
|
171
|
+
$ blink queue list --status pending List tasks
|
|
172
|
+
$ blink queue stats Queue overview
|
|
173
|
+
$ blink queue dlq list Dead letter queue
|
|
174
|
+
|
|
167
175
|
Versions:
|
|
168
176
|
$ blink versions list List saved versions
|
|
169
177
|
$ blink versions save --message "v1.0" Save a snapshot
|
|
@@ -236,6 +244,7 @@ registerWorkspaceCommands(program)
|
|
|
236
244
|
registerVersionCommands(program)
|
|
237
245
|
registerBillingCommands(program)
|
|
238
246
|
registerTokenCommands(program)
|
|
247
|
+
registerQueueCommands(program)
|
|
239
248
|
|
|
240
249
|
program.command('use <project_id>')
|
|
241
250
|
.description('Set active project for this shell session (alternative to blink link)')
|
package/src/commands/auth.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { writeConfig, clearConfig } from '../lib/config.js'
|
|
|
4
4
|
import { printJson, isJsonMode } from '../lib/output.js'
|
|
5
5
|
import chalk from 'chalk'
|
|
6
6
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
7
|
+
import { randomBytes } from 'node:crypto'
|
|
7
8
|
import type { AddressInfo } from 'node:net'
|
|
8
9
|
|
|
9
10
|
const TIMEOUT_MS = 120_000
|
|
@@ -20,7 +21,6 @@ function getBaseUrl(): string {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function generateState(): string {
|
|
23
|
-
const { randomBytes } = require('node:crypto')
|
|
24
24
|
return randomBytes(24).toString('base64url')
|
|
25
25
|
}
|
|
26
26
|
|
package/src/commands/project.ts
CHANGED
|
@@ -131,9 +131,9 @@ After linking, most commands work without specifying a project_id:
|
|
|
131
131
|
|
|
132
132
|
program.command('status')
|
|
133
133
|
.description('Show current agent, project, and auth context')
|
|
134
|
-
.action(() => {
|
|
134
|
+
.action(async () => {
|
|
135
135
|
const config = readProjectConfig()
|
|
136
|
-
const { resolveAgentId } =
|
|
136
|
+
const { resolveAgentId } = await import('../lib/agent.js')
|
|
137
137
|
|
|
138
138
|
// Agent context
|
|
139
139
|
const agentId = resolveAgentId()
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { appRequest } from '../lib/api-app.js'
|
|
3
|
+
import { requireToken } from '../lib/auth.js'
|
|
4
|
+
import { requireProjectId } from '../lib/project.js'
|
|
5
|
+
import { printJson, printSuccess, isJsonMode, withSpinner, createTable, printKv } from '../lib/output.js'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
|
|
8
|
+
export function registerQueueCommands(program: Command) {
|
|
9
|
+
const queue = program.command('queue')
|
|
10
|
+
.description('Manage background task queues and cron schedules')
|
|
11
|
+
.addHelpText('after', `
|
|
12
|
+
Blink Queue provides background task processing and cron scheduling.
|
|
13
|
+
Tasks are delivered to your backend at /api/queue. Pro+ plans only.
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
$ blink queue enqueue send-email --payload '{"to":"user@example.com"}'
|
|
17
|
+
$ blink queue schedule create daily-cleanup "0 3 * * *"
|
|
18
|
+
$ blink queue list --status pending
|
|
19
|
+
$ blink queue stats
|
|
20
|
+
$ blink queue dlq list
|
|
21
|
+
`)
|
|
22
|
+
|
|
23
|
+
registerEnqueue(queue)
|
|
24
|
+
registerList(queue)
|
|
25
|
+
registerGet(queue)
|
|
26
|
+
registerCancel(queue)
|
|
27
|
+
registerStats(queue)
|
|
28
|
+
registerScheduleCommands(queue)
|
|
29
|
+
registerQueueCrud(queue)
|
|
30
|
+
registerDlqCommands(queue)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function queuePath(projectId: string, path: string) {
|
|
34
|
+
return `/api/project/${projectId}/queue/${path}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function registerEnqueue(queue: Command) {
|
|
38
|
+
queue.command('enqueue <taskName>')
|
|
39
|
+
.description('Enqueue a background task')
|
|
40
|
+
.option('--payload <json>', 'JSON payload', '{}')
|
|
41
|
+
.option('--queue <name>', 'Named queue for FIFO ordering')
|
|
42
|
+
.option('--delay <duration>', 'Delay before execution (e.g. 30s, 5m, 1h)')
|
|
43
|
+
.option('--retries <n>', 'Max retry attempts', '3')
|
|
44
|
+
.option('--timeout <duration>', 'Execution timeout (e.g. 30s, 5m)')
|
|
45
|
+
.addHelpText('after', `
|
|
46
|
+
Examples:
|
|
47
|
+
$ blink queue enqueue send-email --payload '{"to":"user@example.com"}'
|
|
48
|
+
$ blink queue enqueue process-image --queue media --delay 5m
|
|
49
|
+
$ blink queue enqueue cleanup --retries 5 --timeout 60s
|
|
50
|
+
`)
|
|
51
|
+
.action(async (taskName: string, opts) => {
|
|
52
|
+
requireToken()
|
|
53
|
+
const projectId = requireProjectId()
|
|
54
|
+
let payload: Record<string, unknown> = {}
|
|
55
|
+
if (opts.payload && opts.payload !== '{}') {
|
|
56
|
+
payload = JSON.parse(opts.payload)
|
|
57
|
+
}
|
|
58
|
+
const body: Record<string, unknown> = { taskName, payload, retries: parseInt(opts.retries) }
|
|
59
|
+
if (opts.queue) body.queue = opts.queue
|
|
60
|
+
if (opts.delay) body.delay = opts.delay
|
|
61
|
+
if (opts.timeout) body.timeout = opts.timeout
|
|
62
|
+
const result = await withSpinner(`Enqueuing ${taskName}...`, () =>
|
|
63
|
+
appRequest(queuePath(projectId, 'enqueue'), { body })
|
|
64
|
+
)
|
|
65
|
+
if (isJsonMode()) return printJson(result)
|
|
66
|
+
printSuccess(`Task enqueued: ${result?.taskId ?? 'ok'}`)
|
|
67
|
+
if (result?.taskId) printKv('Task ID', result.taskId)
|
|
68
|
+
if (opts.queue) printKv('Queue', opts.queue)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function registerList(queue: Command) {
|
|
73
|
+
queue.command('list')
|
|
74
|
+
.description('List tasks')
|
|
75
|
+
.option('--status <status>', 'Filter: pending, completed, failed, dead')
|
|
76
|
+
.option('--queue <name>', 'Filter by queue name')
|
|
77
|
+
.option('--limit <n>', 'Max results', '20')
|
|
78
|
+
.addHelpText('after', `
|
|
79
|
+
Examples:
|
|
80
|
+
$ blink queue list
|
|
81
|
+
$ blink queue list --status pending
|
|
82
|
+
$ blink queue list --queue emails --limit 50
|
|
83
|
+
$ blink queue list --json
|
|
84
|
+
`)
|
|
85
|
+
.action(async (opts) => {
|
|
86
|
+
requireToken()
|
|
87
|
+
const projectId = requireProjectId()
|
|
88
|
+
const params = new URLSearchParams()
|
|
89
|
+
if (opts.status) params.set('status', opts.status)
|
|
90
|
+
if (opts.queue) params.set('queue', opts.queue)
|
|
91
|
+
if (opts.limit) params.set('limit', opts.limit)
|
|
92
|
+
const result = await withSpinner('Loading tasks...', () =>
|
|
93
|
+
appRequest(queuePath(projectId, `tasks?${params}`))
|
|
94
|
+
)
|
|
95
|
+
const tasks = result?.tasks ?? result ?? []
|
|
96
|
+
if (isJsonMode()) return printJson(tasks)
|
|
97
|
+
if (!tasks.length) { console.log(chalk.dim('(no tasks)')); return }
|
|
98
|
+
const table = createTable(['ID', 'Task', 'Status', 'Queue', 'Created'])
|
|
99
|
+
for (const t of tasks) {
|
|
100
|
+
table.push([t.id, t.task_name, t.status, t.queue ?? '-', t.created_at?.slice(0, 19) ?? '-'])
|
|
101
|
+
}
|
|
102
|
+
console.log(table.toString())
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function registerGet(queue: Command) {
|
|
107
|
+
queue.command('get <taskId>')
|
|
108
|
+
.description('Get task details')
|
|
109
|
+
.addHelpText('after', `
|
|
110
|
+
Examples:
|
|
111
|
+
$ blink queue get tsk_abc123
|
|
112
|
+
$ blink queue get tsk_abc123 --json
|
|
113
|
+
`)
|
|
114
|
+
.action(async (taskId: string) => {
|
|
115
|
+
requireToken()
|
|
116
|
+
const projectId = requireProjectId()
|
|
117
|
+
const result = await withSpinner('Loading task...', () =>
|
|
118
|
+
appRequest(queuePath(projectId, `tasks/${taskId}`))
|
|
119
|
+
)
|
|
120
|
+
if (isJsonMode()) return printJson(result)
|
|
121
|
+
const t = result?.task ?? result
|
|
122
|
+
printKv('ID', t.id)
|
|
123
|
+
printKv('Task', t.task_name)
|
|
124
|
+
printKv('Status', t.status)
|
|
125
|
+
if (t.queue) printKv('Queue', t.queue)
|
|
126
|
+
if (t.error) printKv('Error', chalk.red(t.error))
|
|
127
|
+
printKv('Created', t.created_at)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function registerCancel(queue: Command) {
|
|
132
|
+
queue.command('cancel <taskId>')
|
|
133
|
+
.description('Cancel a pending task')
|
|
134
|
+
.addHelpText('after', `
|
|
135
|
+
Examples:
|
|
136
|
+
$ blink queue cancel tsk_abc123
|
|
137
|
+
`)
|
|
138
|
+
.action(async (taskId: string) => {
|
|
139
|
+
requireToken()
|
|
140
|
+
const projectId = requireProjectId()
|
|
141
|
+
await withSpinner('Cancelling task...', () =>
|
|
142
|
+
appRequest(queuePath(projectId, `tasks/${taskId}`), { method: 'DELETE' })
|
|
143
|
+
)
|
|
144
|
+
if (isJsonMode()) return printJson({ status: 'ok', task_id: taskId })
|
|
145
|
+
printSuccess(`Cancelled ${taskId}`)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function registerStats(queue: Command) {
|
|
150
|
+
queue.command('stats')
|
|
151
|
+
.description('Show queue statistics')
|
|
152
|
+
.addHelpText('after', `
|
|
153
|
+
Examples:
|
|
154
|
+
$ blink queue stats
|
|
155
|
+
$ blink queue stats --json
|
|
156
|
+
`)
|
|
157
|
+
.action(async () => {
|
|
158
|
+
requireToken()
|
|
159
|
+
const projectId = requireProjectId()
|
|
160
|
+
const result = await withSpinner('Loading stats...', () =>
|
|
161
|
+
appRequest(queuePath(projectId, 'stats'))
|
|
162
|
+
)
|
|
163
|
+
if (isJsonMode()) return printJson(result)
|
|
164
|
+
console.log()
|
|
165
|
+
printKv('Pending', String(result?.pending ?? 0))
|
|
166
|
+
printKv('Completed', String(result?.completed ?? 0))
|
|
167
|
+
printKv('Failed', String(result?.failed ?? 0))
|
|
168
|
+
printKv('Dead', String(result?.dead ?? 0))
|
|
169
|
+
printKv('Schedules', String(result?.schedules ?? 0))
|
|
170
|
+
if (result?.tier) printKv('Tier', result.tier)
|
|
171
|
+
console.log()
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function registerScheduleCommands(queue: Command) {
|
|
176
|
+
const sched = queue.command('schedule')
|
|
177
|
+
.description('Manage cron schedules')
|
|
178
|
+
.addHelpText('after', `
|
|
179
|
+
Examples:
|
|
180
|
+
$ blink queue schedule create daily-report "0 9 * * *"
|
|
181
|
+
$ blink queue schedule list
|
|
182
|
+
$ blink queue schedule pause daily-report
|
|
183
|
+
$ blink queue schedule delete daily-report
|
|
184
|
+
`)
|
|
185
|
+
|
|
186
|
+
sched.command('create <name> <cron>')
|
|
187
|
+
.description('Create or update a cron schedule')
|
|
188
|
+
.option('--payload <json>', 'JSON payload', '{}')
|
|
189
|
+
.option('--timezone <tz>', 'Timezone (e.g. America/New_York)', 'UTC')
|
|
190
|
+
.option('--retries <n>', 'Max retries per execution', '3')
|
|
191
|
+
.addHelpText('after', `
|
|
192
|
+
Examples:
|
|
193
|
+
$ blink queue schedule create daily-report "0 9 * * *"
|
|
194
|
+
$ blink queue schedule create cleanup "0 3 * * *" --timezone America/New_York
|
|
195
|
+
$ blink queue schedule create sync "*/15 * * * *" --payload '{"source":"api"}'
|
|
196
|
+
`)
|
|
197
|
+
.action(async (name: string, cron: string, opts) => {
|
|
198
|
+
requireToken()
|
|
199
|
+
const projectId = requireProjectId()
|
|
200
|
+
let payload: Record<string, unknown> = {}
|
|
201
|
+
if (opts.payload && opts.payload !== '{}') payload = JSON.parse(opts.payload)
|
|
202
|
+
const body = { name, cron, payload, timezone: opts.timezone, retries: parseInt(opts.retries) }
|
|
203
|
+
const result = await withSpinner(`Creating schedule "${name}"...`, () =>
|
|
204
|
+
appRequest(queuePath(projectId, 'schedule'), { body })
|
|
205
|
+
)
|
|
206
|
+
if (isJsonMode()) return printJson(result)
|
|
207
|
+
printSuccess(`Schedule "${name}" created: ${cron}`)
|
|
208
|
+
if (result?.scheduleId) printKv('Schedule ID', result.scheduleId)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
sched.command('list')
|
|
212
|
+
.description('List all cron schedules')
|
|
213
|
+
.addHelpText('after', `
|
|
214
|
+
Examples:
|
|
215
|
+
$ blink queue schedule list
|
|
216
|
+
$ blink queue schedule list --json
|
|
217
|
+
`)
|
|
218
|
+
.action(async () => {
|
|
219
|
+
requireToken()
|
|
220
|
+
const projectId = requireProjectId()
|
|
221
|
+
const result = await withSpinner('Loading schedules...', () =>
|
|
222
|
+
appRequest(queuePath(projectId, 'schedules'))
|
|
223
|
+
)
|
|
224
|
+
const schedules = result?.schedules ?? result ?? []
|
|
225
|
+
if (isJsonMode()) return printJson(schedules)
|
|
226
|
+
if (!schedules.length) { console.log(chalk.dim('(no schedules)')); return }
|
|
227
|
+
const table = createTable(['Name', 'Cron', 'Timezone', 'Paused', 'Next Run'])
|
|
228
|
+
for (const s of schedules) {
|
|
229
|
+
table.push([s.name, s.cron, s.timezone ?? 'UTC', s.is_paused ? 'yes' : 'no', s.next_run_at ?? '-'])
|
|
230
|
+
}
|
|
231
|
+
console.log(table.toString())
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
sched.command('pause <name>')
|
|
235
|
+
.description('Pause a schedule')
|
|
236
|
+
.action(async (name: string) => {
|
|
237
|
+
requireToken()
|
|
238
|
+
const projectId = requireProjectId()
|
|
239
|
+
await withSpinner(`Pausing "${name}"...`, () =>
|
|
240
|
+
appRequest(queuePath(projectId, `schedules/${name}/pause`), { method: 'POST', body: {} })
|
|
241
|
+
)
|
|
242
|
+
if (isJsonMode()) return printJson({ status: 'ok', name })
|
|
243
|
+
printSuccess(`Schedule "${name}" paused`)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
sched.command('resume <name>')
|
|
247
|
+
.description('Resume a paused schedule')
|
|
248
|
+
.action(async (name: string) => {
|
|
249
|
+
requireToken()
|
|
250
|
+
const projectId = requireProjectId()
|
|
251
|
+
await withSpinner(`Resuming "${name}"...`, () =>
|
|
252
|
+
appRequest(queuePath(projectId, `schedules/${name}/resume`), { method: 'POST', body: {} })
|
|
253
|
+
)
|
|
254
|
+
if (isJsonMode()) return printJson({ status: 'ok', name })
|
|
255
|
+
printSuccess(`Schedule "${name}" resumed`)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
sched.command('delete <name>')
|
|
259
|
+
.description('Delete a schedule')
|
|
260
|
+
.option('--yes', 'Skip confirmation')
|
|
261
|
+
.action(async (name: string, opts) => {
|
|
262
|
+
requireToken()
|
|
263
|
+
const projectId = requireProjectId()
|
|
264
|
+
const skip = opts.yes || process.argv.includes('--yes') || process.argv.includes('-y') || isJsonMode() || !process.stdout.isTTY
|
|
265
|
+
if (!skip) {
|
|
266
|
+
const { confirm } = await import('@clack/prompts')
|
|
267
|
+
const ok = await confirm({ message: `Delete schedule "${name}"?` })
|
|
268
|
+
if (!ok) { console.log('Cancelled.'); return }
|
|
269
|
+
}
|
|
270
|
+
await withSpinner(`Deleting "${name}"...`, () =>
|
|
271
|
+
appRequest(queuePath(projectId, `schedules/${name}`), { method: 'DELETE' })
|
|
272
|
+
)
|
|
273
|
+
if (isJsonMode()) return printJson({ status: 'ok', name })
|
|
274
|
+
printSuccess(`Schedule "${name}" deleted`)
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function registerQueueCrud(queue: Command) {
|
|
279
|
+
queue.command('create-queue <name>')
|
|
280
|
+
.description('Create a named queue with parallelism control')
|
|
281
|
+
.option('--parallelism <n>', 'Max concurrent tasks', '1')
|
|
282
|
+
.addHelpText('after', `
|
|
283
|
+
Named queues provide FIFO ordering and parallelism control.
|
|
284
|
+
|
|
285
|
+
Examples:
|
|
286
|
+
$ blink queue create-queue emails
|
|
287
|
+
$ blink queue create-queue media --parallelism 5
|
|
288
|
+
`)
|
|
289
|
+
.action(async (name: string, opts) => {
|
|
290
|
+
requireToken()
|
|
291
|
+
const projectId = requireProjectId()
|
|
292
|
+
const result = await withSpinner(`Creating queue "${name}"...`, () =>
|
|
293
|
+
appRequest(queuePath(projectId, 'queues'), { body: { name, parallelism: parseInt(opts.parallelism) } })
|
|
294
|
+
)
|
|
295
|
+
if (isJsonMode()) return printJson(result)
|
|
296
|
+
printSuccess(`Queue "${name}" created (parallelism: ${opts.parallelism})`)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
queue.command('queues')
|
|
300
|
+
.description('List all named queues')
|
|
301
|
+
.addHelpText('after', `
|
|
302
|
+
Examples:
|
|
303
|
+
$ blink queue queues
|
|
304
|
+
$ blink queue queues --json
|
|
305
|
+
`)
|
|
306
|
+
.action(async () => {
|
|
307
|
+
requireToken()
|
|
308
|
+
const projectId = requireProjectId()
|
|
309
|
+
const result = await withSpinner('Loading queues...', () =>
|
|
310
|
+
appRequest(queuePath(projectId, 'queues'))
|
|
311
|
+
)
|
|
312
|
+
const queues = result?.queues ?? result ?? []
|
|
313
|
+
if (isJsonMode()) return printJson(queues)
|
|
314
|
+
if (!queues.length) { console.log(chalk.dim('(no named queues)')); return }
|
|
315
|
+
const table = createTable(['Name', 'Parallelism', 'Pending', 'Created'])
|
|
316
|
+
for (const q of queues) {
|
|
317
|
+
table.push([q.name, String(q.parallelism ?? 1), String(q.pending ?? 0), q.created_at?.slice(0, 10) ?? '-'])
|
|
318
|
+
}
|
|
319
|
+
console.log(table.toString())
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
queue.command('delete-queue <name>')
|
|
323
|
+
.description('Delete a named queue')
|
|
324
|
+
.option('--yes', 'Skip confirmation')
|
|
325
|
+
.action(async (name: string, opts) => {
|
|
326
|
+
requireToken()
|
|
327
|
+
const projectId = requireProjectId()
|
|
328
|
+
const skip = opts.yes || process.argv.includes('--yes') || process.argv.includes('-y') || isJsonMode() || !process.stdout.isTTY
|
|
329
|
+
if (!skip) {
|
|
330
|
+
const { confirm } = await import('@clack/prompts')
|
|
331
|
+
const ok = await confirm({ message: `Delete queue "${name}"? Pending tasks will be lost.` })
|
|
332
|
+
if (!ok) { console.log('Cancelled.'); return }
|
|
333
|
+
}
|
|
334
|
+
await withSpinner(`Deleting queue "${name}"...`, () =>
|
|
335
|
+
appRequest(queuePath(projectId, `queues/${name}`), { method: 'DELETE' })
|
|
336
|
+
)
|
|
337
|
+
if (isJsonMode()) return printJson({ status: 'ok', name })
|
|
338
|
+
printSuccess(`Queue "${name}" deleted`)
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function registerDlqCommands(queue: Command) {
|
|
343
|
+
const dlq = queue.command('dlq')
|
|
344
|
+
.description('Manage dead letter queue (failed tasks)')
|
|
345
|
+
.addHelpText('after', `
|
|
346
|
+
Tasks that exhaust all retries are moved to the dead letter queue.
|
|
347
|
+
|
|
348
|
+
Examples:
|
|
349
|
+
$ blink queue dlq list
|
|
350
|
+
$ blink queue dlq retry <taskId>
|
|
351
|
+
$ blink queue dlq purge --yes
|
|
352
|
+
`)
|
|
353
|
+
|
|
354
|
+
dlq.command('list')
|
|
355
|
+
.description('List dead tasks')
|
|
356
|
+
.action(async () => {
|
|
357
|
+
requireToken()
|
|
358
|
+
const projectId = requireProjectId()
|
|
359
|
+
const result = await withSpinner('Loading DLQ...', () =>
|
|
360
|
+
appRequest(queuePath(projectId, 'dlq'))
|
|
361
|
+
)
|
|
362
|
+
const tasks = result?.tasks ?? result ?? []
|
|
363
|
+
if (isJsonMode()) return printJson(tasks)
|
|
364
|
+
if (!tasks.length) { console.log(chalk.dim('(no dead tasks)')); return }
|
|
365
|
+
const table = createTable(['ID', 'Task', 'Error', 'Failed At'])
|
|
366
|
+
for (const t of tasks) {
|
|
367
|
+
table.push([t.id, t.task_name, (t.error ?? '-').slice(0, 40), t.failed_at?.slice(0, 19) ?? '-'])
|
|
368
|
+
}
|
|
369
|
+
console.log(table.toString())
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
dlq.command('retry <taskId>')
|
|
373
|
+
.description('Retry a dead task')
|
|
374
|
+
.action(async (taskId: string) => {
|
|
375
|
+
requireToken()
|
|
376
|
+
const projectId = requireProjectId()
|
|
377
|
+
await withSpinner(`Retrying ${taskId}...`, () =>
|
|
378
|
+
appRequest(queuePath(projectId, `dlq/${taskId}/retry`), { method: 'POST', body: {} })
|
|
379
|
+
)
|
|
380
|
+
if (isJsonMode()) return printJson({ status: 'ok', task_id: taskId })
|
|
381
|
+
printSuccess(`Retried ${taskId}`)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
dlq.command('delete <taskId>')
|
|
385
|
+
.description('Delete a dead task')
|
|
386
|
+
.action(async (taskId: string) => {
|
|
387
|
+
requireToken()
|
|
388
|
+
const projectId = requireProjectId()
|
|
389
|
+
await withSpinner(`Deleting ${taskId}...`, () =>
|
|
390
|
+
appRequest(queuePath(projectId, `dlq/${taskId}`), { method: 'DELETE' })
|
|
391
|
+
)
|
|
392
|
+
if (isJsonMode()) return printJson({ status: 'ok', task_id: taskId })
|
|
393
|
+
printSuccess(`Deleted ${taskId} from DLQ`)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
dlq.command('purge')
|
|
397
|
+
.description('Delete all dead tasks')
|
|
398
|
+
.option('--yes', 'Skip confirmation')
|
|
399
|
+
.action(async (opts) => {
|
|
400
|
+
requireToken()
|
|
401
|
+
const projectId = requireProjectId()
|
|
402
|
+
const skip = opts.yes || process.argv.includes('--yes') || process.argv.includes('-y') || isJsonMode() || !process.stdout.isTTY
|
|
403
|
+
if (!skip) {
|
|
404
|
+
const { confirm } = await import('@clack/prompts')
|
|
405
|
+
const ok = await confirm({ message: 'Purge all dead tasks? This cannot be undone.' })
|
|
406
|
+
if (!ok) { console.log('Cancelled.'); return }
|
|
407
|
+
}
|
|
408
|
+
await withSpinner('Purging DLQ...', () =>
|
|
409
|
+
appRequest(queuePath(projectId, 'dlq'), { method: 'DELETE' })
|
|
410
|
+
)
|
|
411
|
+
if (isJsonMode()) return printJson({ status: 'ok' })
|
|
412
|
+
printSuccess('DLQ purged')
|
|
413
|
+
})
|
|
414
|
+
}
|