@blinkdotnew/cli 0.5.3 → 0.6.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/dist/cli.js +376 -4
- package/package.json +1 -1
- package/src/cli.ts +9 -0
- package/src/commands/queue.ts +414 -0
package/dist/cli.js
CHANGED
|
@@ -3831,6 +3831,370 @@ Examples:
|
|
|
3831
3831
|
});
|
|
3832
3832
|
}
|
|
3833
3833
|
|
|
3834
|
+
// src/commands/queue.ts
|
|
3835
|
+
init_project();
|
|
3836
|
+
import chalk25 from "chalk";
|
|
3837
|
+
function registerQueueCommands(program2) {
|
|
3838
|
+
const queue = program2.command("queue").description("Manage background task queues and cron schedules").addHelpText("after", `
|
|
3839
|
+
Blink Queue provides background task processing and cron scheduling.
|
|
3840
|
+
Tasks are delivered to your backend at /api/queue. Pro+ plans only.
|
|
3841
|
+
|
|
3842
|
+
Examples:
|
|
3843
|
+
$ blink queue enqueue send-email --payload '{"to":"user@example.com"}'
|
|
3844
|
+
$ blink queue schedule create daily-cleanup "0 3 * * *"
|
|
3845
|
+
$ blink queue list --status pending
|
|
3846
|
+
$ blink queue stats
|
|
3847
|
+
$ blink queue dlq list
|
|
3848
|
+
`);
|
|
3849
|
+
registerEnqueue(queue);
|
|
3850
|
+
registerList(queue);
|
|
3851
|
+
registerGet(queue);
|
|
3852
|
+
registerCancel(queue);
|
|
3853
|
+
registerStats(queue);
|
|
3854
|
+
registerScheduleCommands(queue);
|
|
3855
|
+
registerQueueCrud(queue);
|
|
3856
|
+
registerDlqCommands(queue);
|
|
3857
|
+
}
|
|
3858
|
+
function queuePath(projectId, path) {
|
|
3859
|
+
return `/api/project/${projectId}/queue/${path}`;
|
|
3860
|
+
}
|
|
3861
|
+
function registerEnqueue(queue) {
|
|
3862
|
+
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", `
|
|
3863
|
+
Examples:
|
|
3864
|
+
$ blink queue enqueue send-email --payload '{"to":"user@example.com"}'
|
|
3865
|
+
$ blink queue enqueue process-image --queue media --delay 5m
|
|
3866
|
+
$ blink queue enqueue cleanup --retries 5 --timeout 60s
|
|
3867
|
+
`).action(async (taskName, opts) => {
|
|
3868
|
+
requireToken();
|
|
3869
|
+
const projectId = requireProjectId();
|
|
3870
|
+
let payload = {};
|
|
3871
|
+
if (opts.payload && opts.payload !== "{}") {
|
|
3872
|
+
payload = JSON.parse(opts.payload);
|
|
3873
|
+
}
|
|
3874
|
+
const body = { taskName, payload, retries: parseInt(opts.retries) };
|
|
3875
|
+
if (opts.queue) body.queue = opts.queue;
|
|
3876
|
+
if (opts.delay) body.delay = opts.delay;
|
|
3877
|
+
if (opts.timeout) body.timeout = opts.timeout;
|
|
3878
|
+
const result = await withSpinner(
|
|
3879
|
+
`Enqueuing ${taskName}...`,
|
|
3880
|
+
() => appRequest(queuePath(projectId, "enqueue"), { body })
|
|
3881
|
+
);
|
|
3882
|
+
if (isJsonMode()) return printJson(result);
|
|
3883
|
+
printSuccess(`Task enqueued: ${result?.taskId ?? "ok"}`);
|
|
3884
|
+
if (result?.taskId) printKv("Task ID", result.taskId);
|
|
3885
|
+
if (opts.queue) printKv("Queue", opts.queue);
|
|
3886
|
+
});
|
|
3887
|
+
}
|
|
3888
|
+
function registerList(queue) {
|
|
3889
|
+
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", `
|
|
3890
|
+
Examples:
|
|
3891
|
+
$ blink queue list
|
|
3892
|
+
$ blink queue list --status pending
|
|
3893
|
+
$ blink queue list --queue emails --limit 50
|
|
3894
|
+
$ blink queue list --json
|
|
3895
|
+
`).action(async (opts) => {
|
|
3896
|
+
requireToken();
|
|
3897
|
+
const projectId = requireProjectId();
|
|
3898
|
+
const params = new URLSearchParams();
|
|
3899
|
+
if (opts.status) params.set("status", opts.status);
|
|
3900
|
+
if (opts.queue) params.set("queue", opts.queue);
|
|
3901
|
+
if (opts.limit) params.set("limit", opts.limit);
|
|
3902
|
+
const result = await withSpinner(
|
|
3903
|
+
"Loading tasks...",
|
|
3904
|
+
() => appRequest(queuePath(projectId, `tasks?${params}`))
|
|
3905
|
+
);
|
|
3906
|
+
const tasks = result?.tasks ?? result ?? [];
|
|
3907
|
+
if (isJsonMode()) return printJson(tasks);
|
|
3908
|
+
if (!tasks.length) {
|
|
3909
|
+
console.log(chalk25.dim("(no tasks)"));
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3912
|
+
const table = createTable(["ID", "Task", "Status", "Queue", "Created"]);
|
|
3913
|
+
for (const t of tasks) {
|
|
3914
|
+
table.push([t.id, t.task_name, t.status, t.queue ?? "-", t.created_at?.slice(0, 19) ?? "-"]);
|
|
3915
|
+
}
|
|
3916
|
+
console.log(table.toString());
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
function registerGet(queue) {
|
|
3920
|
+
queue.command("get <taskId>").description("Get task details").addHelpText("after", `
|
|
3921
|
+
Examples:
|
|
3922
|
+
$ blink queue get tsk_abc123
|
|
3923
|
+
$ blink queue get tsk_abc123 --json
|
|
3924
|
+
`).action(async (taskId) => {
|
|
3925
|
+
requireToken();
|
|
3926
|
+
const projectId = requireProjectId();
|
|
3927
|
+
const result = await withSpinner(
|
|
3928
|
+
"Loading task...",
|
|
3929
|
+
() => appRequest(queuePath(projectId, `tasks/${taskId}`))
|
|
3930
|
+
);
|
|
3931
|
+
if (isJsonMode()) return printJson(result);
|
|
3932
|
+
const t = result?.task ?? result;
|
|
3933
|
+
printKv("ID", t.id);
|
|
3934
|
+
printKv("Task", t.task_name);
|
|
3935
|
+
printKv("Status", t.status);
|
|
3936
|
+
if (t.queue) printKv("Queue", t.queue);
|
|
3937
|
+
if (t.error) printKv("Error", chalk25.red(t.error));
|
|
3938
|
+
printKv("Created", t.created_at);
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
function registerCancel(queue) {
|
|
3942
|
+
queue.command("cancel <taskId>").description("Cancel a pending task").addHelpText("after", `
|
|
3943
|
+
Examples:
|
|
3944
|
+
$ blink queue cancel tsk_abc123
|
|
3945
|
+
`).action(async (taskId) => {
|
|
3946
|
+
requireToken();
|
|
3947
|
+
const projectId = requireProjectId();
|
|
3948
|
+
await withSpinner(
|
|
3949
|
+
"Cancelling task...",
|
|
3950
|
+
() => appRequest(queuePath(projectId, `tasks/${taskId}`), { method: "DELETE" })
|
|
3951
|
+
);
|
|
3952
|
+
if (isJsonMode()) return printJson({ status: "ok", task_id: taskId });
|
|
3953
|
+
printSuccess(`Cancelled ${taskId}`);
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
function registerStats(queue) {
|
|
3957
|
+
queue.command("stats").description("Show queue statistics").addHelpText("after", `
|
|
3958
|
+
Examples:
|
|
3959
|
+
$ blink queue stats
|
|
3960
|
+
$ blink queue stats --json
|
|
3961
|
+
`).action(async () => {
|
|
3962
|
+
requireToken();
|
|
3963
|
+
const projectId = requireProjectId();
|
|
3964
|
+
const result = await withSpinner(
|
|
3965
|
+
"Loading stats...",
|
|
3966
|
+
() => appRequest(queuePath(projectId, "stats"))
|
|
3967
|
+
);
|
|
3968
|
+
if (isJsonMode()) return printJson(result);
|
|
3969
|
+
console.log();
|
|
3970
|
+
printKv("Pending", String(result?.pending ?? 0));
|
|
3971
|
+
printKv("Completed", String(result?.completed ?? 0));
|
|
3972
|
+
printKv("Failed", String(result?.failed ?? 0));
|
|
3973
|
+
printKv("Dead", String(result?.dead ?? 0));
|
|
3974
|
+
printKv("Schedules", String(result?.schedules ?? 0));
|
|
3975
|
+
if (result?.tier) printKv("Tier", result.tier);
|
|
3976
|
+
console.log();
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
function registerScheduleCommands(queue) {
|
|
3980
|
+
const sched = queue.command("schedule").description("Manage cron schedules").addHelpText("after", `
|
|
3981
|
+
Examples:
|
|
3982
|
+
$ blink queue schedule create daily-report "0 9 * * *"
|
|
3983
|
+
$ blink queue schedule list
|
|
3984
|
+
$ blink queue schedule pause daily-report
|
|
3985
|
+
$ blink queue schedule delete daily-report
|
|
3986
|
+
`);
|
|
3987
|
+
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", `
|
|
3988
|
+
Examples:
|
|
3989
|
+
$ blink queue schedule create daily-report "0 9 * * *"
|
|
3990
|
+
$ blink queue schedule create cleanup "0 3 * * *" --timezone America/New_York
|
|
3991
|
+
$ blink queue schedule create sync "*/15 * * * *" --payload '{"source":"api"}'
|
|
3992
|
+
`).action(async (name, cron, opts) => {
|
|
3993
|
+
requireToken();
|
|
3994
|
+
const projectId = requireProjectId();
|
|
3995
|
+
let payload = {};
|
|
3996
|
+
if (opts.payload && opts.payload !== "{}") payload = JSON.parse(opts.payload);
|
|
3997
|
+
const body = { name, cron, payload, timezone: opts.timezone, retries: parseInt(opts.retries) };
|
|
3998
|
+
const result = await withSpinner(
|
|
3999
|
+
`Creating schedule "${name}"...`,
|
|
4000
|
+
() => appRequest(queuePath(projectId, "schedule"), { body })
|
|
4001
|
+
);
|
|
4002
|
+
if (isJsonMode()) return printJson(result);
|
|
4003
|
+
printSuccess(`Schedule "${name}" created: ${cron}`);
|
|
4004
|
+
if (result?.scheduleId) printKv("Schedule ID", result.scheduleId);
|
|
4005
|
+
});
|
|
4006
|
+
sched.command("list").description("List all cron schedules").addHelpText("after", `
|
|
4007
|
+
Examples:
|
|
4008
|
+
$ blink queue schedule list
|
|
4009
|
+
$ blink queue schedule list --json
|
|
4010
|
+
`).action(async () => {
|
|
4011
|
+
requireToken();
|
|
4012
|
+
const projectId = requireProjectId();
|
|
4013
|
+
const result = await withSpinner(
|
|
4014
|
+
"Loading schedules...",
|
|
4015
|
+
() => appRequest(queuePath(projectId, "schedules"))
|
|
4016
|
+
);
|
|
4017
|
+
const schedules = result?.schedules ?? result ?? [];
|
|
4018
|
+
if (isJsonMode()) return printJson(schedules);
|
|
4019
|
+
if (!schedules.length) {
|
|
4020
|
+
console.log(chalk25.dim("(no schedules)"));
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
const table = createTable(["Name", "Cron", "Timezone", "Paused", "Next Run"]);
|
|
4024
|
+
for (const s of schedules) {
|
|
4025
|
+
table.push([s.name, s.cron, s.timezone ?? "UTC", s.is_paused ? "yes" : "no", s.next_run_at ?? "-"]);
|
|
4026
|
+
}
|
|
4027
|
+
console.log(table.toString());
|
|
4028
|
+
});
|
|
4029
|
+
sched.command("pause <name>").description("Pause a schedule").action(async (name) => {
|
|
4030
|
+
requireToken();
|
|
4031
|
+
const projectId = requireProjectId();
|
|
4032
|
+
await withSpinner(
|
|
4033
|
+
`Pausing "${name}"...`,
|
|
4034
|
+
() => appRequest(queuePath(projectId, `schedules/${name}/pause`), { method: "POST", body: {} })
|
|
4035
|
+
);
|
|
4036
|
+
if (isJsonMode()) return printJson({ status: "ok", name });
|
|
4037
|
+
printSuccess(`Schedule "${name}" paused`);
|
|
4038
|
+
});
|
|
4039
|
+
sched.command("resume <name>").description("Resume a paused schedule").action(async (name) => {
|
|
4040
|
+
requireToken();
|
|
4041
|
+
const projectId = requireProjectId();
|
|
4042
|
+
await withSpinner(
|
|
4043
|
+
`Resuming "${name}"...`,
|
|
4044
|
+
() => appRequest(queuePath(projectId, `schedules/${name}/resume`), { method: "POST", body: {} })
|
|
4045
|
+
);
|
|
4046
|
+
if (isJsonMode()) return printJson({ status: "ok", name });
|
|
4047
|
+
printSuccess(`Schedule "${name}" resumed`);
|
|
4048
|
+
});
|
|
4049
|
+
sched.command("delete <name>").description("Delete a schedule").option("--yes", "Skip confirmation").action(async (name, opts) => {
|
|
4050
|
+
requireToken();
|
|
4051
|
+
const projectId = requireProjectId();
|
|
4052
|
+
const skip = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode() || !process.stdout.isTTY;
|
|
4053
|
+
if (!skip) {
|
|
4054
|
+
const { confirm } = await import("@clack/prompts");
|
|
4055
|
+
const ok = await confirm({ message: `Delete schedule "${name}"?` });
|
|
4056
|
+
if (!ok) {
|
|
4057
|
+
console.log("Cancelled.");
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
await withSpinner(
|
|
4062
|
+
`Deleting "${name}"...`,
|
|
4063
|
+
() => appRequest(queuePath(projectId, `schedules/${name}`), { method: "DELETE" })
|
|
4064
|
+
);
|
|
4065
|
+
if (isJsonMode()) return printJson({ status: "ok", name });
|
|
4066
|
+
printSuccess(`Schedule "${name}" deleted`);
|
|
4067
|
+
});
|
|
4068
|
+
}
|
|
4069
|
+
function registerQueueCrud(queue) {
|
|
4070
|
+
queue.command("create-queue <name>").description("Create a named queue with parallelism control").option("--parallelism <n>", "Max concurrent tasks", "1").addHelpText("after", `
|
|
4071
|
+
Named queues provide FIFO ordering and parallelism control.
|
|
4072
|
+
|
|
4073
|
+
Examples:
|
|
4074
|
+
$ blink queue create-queue emails
|
|
4075
|
+
$ blink queue create-queue media --parallelism 5
|
|
4076
|
+
`).action(async (name, opts) => {
|
|
4077
|
+
requireToken();
|
|
4078
|
+
const projectId = requireProjectId();
|
|
4079
|
+
const result = await withSpinner(
|
|
4080
|
+
`Creating queue "${name}"...`,
|
|
4081
|
+
() => appRequest(queuePath(projectId, "queues"), { body: { name, parallelism: parseInt(opts.parallelism) } })
|
|
4082
|
+
);
|
|
4083
|
+
if (isJsonMode()) return printJson(result);
|
|
4084
|
+
printSuccess(`Queue "${name}" created (parallelism: ${opts.parallelism})`);
|
|
4085
|
+
});
|
|
4086
|
+
queue.command("queues").description("List all named queues").addHelpText("after", `
|
|
4087
|
+
Examples:
|
|
4088
|
+
$ blink queue queues
|
|
4089
|
+
$ blink queue queues --json
|
|
4090
|
+
`).action(async () => {
|
|
4091
|
+
requireToken();
|
|
4092
|
+
const projectId = requireProjectId();
|
|
4093
|
+
const result = await withSpinner(
|
|
4094
|
+
"Loading queues...",
|
|
4095
|
+
() => appRequest(queuePath(projectId, "queues"))
|
|
4096
|
+
);
|
|
4097
|
+
const queues = result?.queues ?? result ?? [];
|
|
4098
|
+
if (isJsonMode()) return printJson(queues);
|
|
4099
|
+
if (!queues.length) {
|
|
4100
|
+
console.log(chalk25.dim("(no named queues)"));
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
const table = createTable(["Name", "Parallelism", "Pending", "Created"]);
|
|
4104
|
+
for (const q of queues) {
|
|
4105
|
+
table.push([q.name, String(q.parallelism ?? 1), String(q.pending ?? 0), q.created_at?.slice(0, 10) ?? "-"]);
|
|
4106
|
+
}
|
|
4107
|
+
console.log(table.toString());
|
|
4108
|
+
});
|
|
4109
|
+
queue.command("delete-queue <name>").description("Delete a named queue").option("--yes", "Skip confirmation").action(async (name, opts) => {
|
|
4110
|
+
requireToken();
|
|
4111
|
+
const projectId = requireProjectId();
|
|
4112
|
+
const skip = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode() || !process.stdout.isTTY;
|
|
4113
|
+
if (!skip) {
|
|
4114
|
+
const { confirm } = await import("@clack/prompts");
|
|
4115
|
+
const ok = await confirm({ message: `Delete queue "${name}"? Pending tasks will be lost.` });
|
|
4116
|
+
if (!ok) {
|
|
4117
|
+
console.log("Cancelled.");
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
await withSpinner(
|
|
4122
|
+
`Deleting queue "${name}"...`,
|
|
4123
|
+
() => appRequest(queuePath(projectId, `queues/${name}`), { method: "DELETE" })
|
|
4124
|
+
);
|
|
4125
|
+
if (isJsonMode()) return printJson({ status: "ok", name });
|
|
4126
|
+
printSuccess(`Queue "${name}" deleted`);
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
function registerDlqCommands(queue) {
|
|
4130
|
+
const dlq = queue.command("dlq").description("Manage dead letter queue (failed tasks)").addHelpText("after", `
|
|
4131
|
+
Tasks that exhaust all retries are moved to the dead letter queue.
|
|
4132
|
+
|
|
4133
|
+
Examples:
|
|
4134
|
+
$ blink queue dlq list
|
|
4135
|
+
$ blink queue dlq retry <taskId>
|
|
4136
|
+
$ blink queue dlq purge --yes
|
|
4137
|
+
`);
|
|
4138
|
+
dlq.command("list").description("List dead tasks").action(async () => {
|
|
4139
|
+
requireToken();
|
|
4140
|
+
const projectId = requireProjectId();
|
|
4141
|
+
const result = await withSpinner(
|
|
4142
|
+
"Loading DLQ...",
|
|
4143
|
+
() => appRequest(queuePath(projectId, "dlq"))
|
|
4144
|
+
);
|
|
4145
|
+
const tasks = result?.tasks ?? result ?? [];
|
|
4146
|
+
if (isJsonMode()) return printJson(tasks);
|
|
4147
|
+
if (!tasks.length) {
|
|
4148
|
+
console.log(chalk25.dim("(no dead tasks)"));
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
4151
|
+
const table = createTable(["ID", "Task", "Error", "Failed At"]);
|
|
4152
|
+
for (const t of tasks) {
|
|
4153
|
+
table.push([t.id, t.task_name, (t.error ?? "-").slice(0, 40), t.failed_at?.slice(0, 19) ?? "-"]);
|
|
4154
|
+
}
|
|
4155
|
+
console.log(table.toString());
|
|
4156
|
+
});
|
|
4157
|
+
dlq.command("retry <taskId>").description("Retry a dead task").action(async (taskId) => {
|
|
4158
|
+
requireToken();
|
|
4159
|
+
const projectId = requireProjectId();
|
|
4160
|
+
await withSpinner(
|
|
4161
|
+
`Retrying ${taskId}...`,
|
|
4162
|
+
() => appRequest(queuePath(projectId, `dlq/${taskId}/retry`), { method: "POST", body: {} })
|
|
4163
|
+
);
|
|
4164
|
+
if (isJsonMode()) return printJson({ status: "ok", task_id: taskId });
|
|
4165
|
+
printSuccess(`Retried ${taskId}`);
|
|
4166
|
+
});
|
|
4167
|
+
dlq.command("delete <taskId>").description("Delete a dead task").action(async (taskId) => {
|
|
4168
|
+
requireToken();
|
|
4169
|
+
const projectId = requireProjectId();
|
|
4170
|
+
await withSpinner(
|
|
4171
|
+
`Deleting ${taskId}...`,
|
|
4172
|
+
() => appRequest(queuePath(projectId, `dlq/${taskId}`), { method: "DELETE" })
|
|
4173
|
+
);
|
|
4174
|
+
if (isJsonMode()) return printJson({ status: "ok", task_id: taskId });
|
|
4175
|
+
printSuccess(`Deleted ${taskId} from DLQ`);
|
|
4176
|
+
});
|
|
4177
|
+
dlq.command("purge").description("Delete all dead tasks").option("--yes", "Skip confirmation").action(async (opts) => {
|
|
4178
|
+
requireToken();
|
|
4179
|
+
const projectId = requireProjectId();
|
|
4180
|
+
const skip = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode() || !process.stdout.isTTY;
|
|
4181
|
+
if (!skip) {
|
|
4182
|
+
const { confirm } = await import("@clack/prompts");
|
|
4183
|
+
const ok = await confirm({ message: "Purge all dead tasks? This cannot be undone." });
|
|
4184
|
+
if (!ok) {
|
|
4185
|
+
console.log("Cancelled.");
|
|
4186
|
+
return;
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
await withSpinner(
|
|
4190
|
+
"Purging DLQ...",
|
|
4191
|
+
() => appRequest(queuePath(projectId, "dlq"), { method: "DELETE" })
|
|
4192
|
+
);
|
|
4193
|
+
if (isJsonMode()) return printJson({ status: "ok" });
|
|
4194
|
+
printSuccess("DLQ purged");
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
|
|
3834
4198
|
// src/cli.ts
|
|
3835
4199
|
var require2 = createRequire(import.meta.url);
|
|
3836
4200
|
var pkg = require2("../package.json");
|
|
@@ -3953,6 +4317,13 @@ Functions (legacy edge functions):
|
|
|
3953
4317
|
$ blink functions logs index View function logs
|
|
3954
4318
|
$ blink functions delete old-fn --yes Delete a function
|
|
3955
4319
|
|
|
4320
|
+
Queue (background tasks + cron, Pro+):
|
|
4321
|
+
$ blink queue enqueue send-email --payload '{"to":"a@b.com"}'
|
|
4322
|
+
$ blink queue schedule create daily "0 9 * * *"
|
|
4323
|
+
$ blink queue list --status pending List tasks
|
|
4324
|
+
$ blink queue stats Queue overview
|
|
4325
|
+
$ blink queue dlq list Dead letter queue
|
|
4326
|
+
|
|
3956
4327
|
Versions:
|
|
3957
4328
|
$ blink versions list List saved versions
|
|
3958
4329
|
$ blink versions save --message "v1.0" Save a snapshot
|
|
@@ -4023,6 +4394,7 @@ registerWorkspaceCommands(program);
|
|
|
4023
4394
|
registerVersionCommands(program);
|
|
4024
4395
|
registerBillingCommands(program);
|
|
4025
4396
|
registerTokenCommands(program);
|
|
4397
|
+
registerQueueCommands(program);
|
|
4026
4398
|
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
4399
|
Examples:
|
|
4028
4400
|
$ blink use proj_xxx Shows the export command to run
|
|
@@ -4038,10 +4410,10 @@ After setting:
|
|
|
4038
4410
|
process.stdout.write(`export BLINK_ACTIVE_PROJECT=${projectId}
|
|
4039
4411
|
`);
|
|
4040
4412
|
} else {
|
|
4041
|
-
const { default:
|
|
4042
|
-
console.log(
|
|
4043
|
-
console.log(
|
|
4044
|
-
console.log(
|
|
4413
|
+
const { default: chalk26 } = await import("chalk");
|
|
4414
|
+
console.log(chalk26.bold("Active project: ") + projectId);
|
|
4415
|
+
console.log(chalk26.dim(`Run: export BLINK_ACTIVE_PROJECT=${projectId}`));
|
|
4416
|
+
console.log(chalk26.dim(`Or: eval $(blink use ${projectId} --export)`));
|
|
4045
4417
|
}
|
|
4046
4418
|
});
|
|
4047
4419
|
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)')
|
|
@@ -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
|
+
}
|