@blinkdotnew/cli 0.5.2 → 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 CHANGED
@@ -2019,6 +2019,31 @@ After creating a project, link it to your current directory:
2019
2019
  if (!isJsonMode()) console.log("Deleted.");
2020
2020
  else printJson({ status: "ok" });
2021
2021
  });
2022
+ project.command("update [project_id]").description("Update project settings").option("--name <name>", "Rename the project").option("--visibility <vis>", "Set visibility: public or private").addHelpText("after", `
2023
+ Examples:
2024
+ $ blink project update --visibility private
2025
+ $ blink project update --name "New Name"
2026
+ $ blink project update proj_xxx --visibility public
2027
+ $ blink project update --visibility private --name "Secret Project"
2028
+ `).action(async (projectArg, opts) => {
2029
+ requireToken();
2030
+ const projectId = requireProjectId(projectArg);
2031
+ const body = {};
2032
+ if (opts.name) body.name = opts.name;
2033
+ if (opts.visibility) body.visibility = opts.visibility;
2034
+ if (!Object.keys(body).length) {
2035
+ console.error("Error: Provide at least one option (--name, --visibility)");
2036
+ process.exit(1);
2037
+ }
2038
+ const result = await withSpinner(
2039
+ "Updating project...",
2040
+ () => appRequest(`/api/projects/${projectId}`, { method: "PATCH", body })
2041
+ );
2042
+ if (isJsonMode()) return printJson(result);
2043
+ printSuccess("Project updated");
2044
+ if (opts.name) printKv("Name", opts.name);
2045
+ if (opts.visibility) printKv("Visibility", opts.visibility);
2046
+ });
2022
2047
  program2.command("link [project_id]").description("Link current directory to a project \u2014 saves project_id to .blink/project.json").addHelpText("after", `
2023
2048
  Examples:
2024
2049
  $ blink link Interactive picker \u2014 choose from your projects
@@ -3411,13 +3436,14 @@ init_project();
3411
3436
  import { basename as basename3 } from "path";
3412
3437
  import chalk20 from "chalk";
3413
3438
  function registerInitCommands(program2) {
3414
- program2.command("init").description("Initialize a new Blink project and link it to the current directory").option("--name <name>", "Project name (defaults to current directory name)").option("--from <project_id>", "Create a new project named after an existing one").addHelpText("after", `
3439
+ program2.command("init").description("Initialize a new Blink project and link it to the current directory").option("--name <name>", "Project name (defaults to current directory name)").option("--visibility <vis>", "Project visibility: public or private", "public").option("--from <project_id>", "Create a new project named after an existing one").addHelpText("after", `
3415
3440
  Creates a new Blink project and writes .blink/project.json in the current directory.
3416
3441
  After init, all commands work without specifying a project_id.
3417
3442
 
3418
3443
  Examples:
3419
3444
  $ blink init Create project named after current dir
3420
3445
  $ blink init --name "My SaaS App" Create with custom name
3446
+ $ blink init --visibility private Create a private project
3421
3447
  $ blink init --from proj_xxx Clone from existing project
3422
3448
  $ blink init --json Machine-readable output
3423
3449
 
@@ -3428,13 +3454,13 @@ After init:
3428
3454
  `).action(async (opts) => {
3429
3455
  requireToken();
3430
3456
  if (opts.from) {
3431
- await initFromExisting(opts.from);
3457
+ await initFromExisting(opts.from, opts.visibility);
3432
3458
  } else {
3433
- await initNew(opts.name);
3459
+ await initNew(opts.name, opts.visibility);
3434
3460
  }
3435
3461
  });
3436
3462
  }
3437
- async function initNew(nameOpt) {
3463
+ async function initNew(nameOpt, visibility) {
3438
3464
  const name = nameOpt ?? basename3(process.cwd());
3439
3465
  const result = await withSpinner(
3440
3466
  `Creating project "${name}"...`,
@@ -3445,14 +3471,18 @@ async function initNew(nameOpt) {
3445
3471
  printError("Failed to create project \u2014 no ID returned");
3446
3472
  process.exit(1);
3447
3473
  }
3474
+ if (visibility === "private") {
3475
+ await appRequest(`/api/projects/${proj.id}`, { method: "PATCH", body: { visibility: "private" } });
3476
+ }
3448
3477
  writeProjectConfig({ projectId: proj.id });
3449
- if (isJsonMode()) return printJson({ project_id: proj.id, name: proj.name ?? name });
3478
+ if (isJsonMode()) return printJson({ project_id: proj.id, name: proj.name ?? name, visibility: visibility ?? "public" });
3450
3479
  printSuccess(`Project created and linked`);
3451
3480
  printKv("ID", proj.id);
3452
3481
  printKv("Name", proj.name ?? name);
3482
+ if (visibility === "private") printKv("Visibility", "private");
3453
3483
  console.log(chalk20.dim("\n Run `blink deploy ./dist --prod` to deploy"));
3454
3484
  }
3455
- async function initFromExisting(sourceId) {
3485
+ async function initFromExisting(sourceId, visibility) {
3456
3486
  const source = await withSpinner(
3457
3487
  "Loading source project...",
3458
3488
  () => appRequest(`/api/projects/${sourceId}`)
@@ -3801,6 +3831,370 @@ Examples:
3801
3831
  });
3802
3832
  }
3803
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
+
3804
4198
  // src/cli.ts
3805
4199
  var require2 = createRequire(import.meta.url);
3806
4200
  var pkg = require2("../package.json");
@@ -3923,6 +4317,13 @@ Functions (legacy edge functions):
3923
4317
  $ blink functions logs index View function logs
3924
4318
  $ blink functions delete old-fn --yes Delete a function
3925
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
+
3926
4327
  Versions:
3927
4328
  $ blink versions list List saved versions
3928
4329
  $ blink versions save --message "v1.0" Save a snapshot
@@ -3993,6 +4394,7 @@ registerWorkspaceCommands(program);
3993
4394
  registerVersionCommands(program);
3994
4395
  registerBillingCommands(program);
3995
4396
  registerTokenCommands(program);
4397
+ registerQueueCommands(program);
3996
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", `
3997
4399
  Examples:
3998
4400
  $ blink use proj_xxx Shows the export command to run
@@ -4008,10 +4410,10 @@ After setting:
4008
4410
  process.stdout.write(`export BLINK_ACTIVE_PROJECT=${projectId}
4009
4411
  `);
4010
4412
  } else {
4011
- const { default: chalk25 } = await import("chalk");
4012
- console.log(chalk25.bold("Active project: ") + projectId);
4013
- console.log(chalk25.dim(`Run: export BLINK_ACTIVE_PROJECT=${projectId}`));
4014
- console.log(chalk25.dim(`Or: eval $(blink use ${projectId} --export)`));
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)`));
4015
4417
  }
4016
4418
  });
4017
4419
  program.action(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/cli",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Blink CLI — full-stack cloud infrastructure from your terminal. Deploy, database, auth, storage, backend, domains, and more.",
5
5
  "bin": {
6
6
  "blink": "dist/cli.js"
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)')
@@ -10,6 +10,7 @@ export function registerInitCommands(program: Command) {
10
10
  program.command('init')
11
11
  .description('Initialize a new Blink project and link it to the current directory')
12
12
  .option('--name <name>', 'Project name (defaults to current directory name)')
13
+ .option('--visibility <vis>', 'Project visibility: public or private', 'public')
13
14
  .option('--from <project_id>', 'Create a new project named after an existing one')
14
15
  .addHelpText('after', `
15
16
  Creates a new Blink project and writes .blink/project.json in the current directory.
@@ -18,6 +19,7 @@ After init, all commands work without specifying a project_id.
18
19
  Examples:
19
20
  $ blink init Create project named after current dir
20
21
  $ blink init --name "My SaaS App" Create with custom name
22
+ $ blink init --visibility private Create a private project
21
23
  $ blink init --from proj_xxx Clone from existing project
22
24
  $ blink init --json Machine-readable output
23
25
 
@@ -29,14 +31,14 @@ After init:
29
31
  .action(async (opts) => {
30
32
  requireToken()
31
33
  if (opts.from) {
32
- await initFromExisting(opts.from)
34
+ await initFromExisting(opts.from, opts.visibility)
33
35
  } else {
34
- await initNew(opts.name)
36
+ await initNew(opts.name, opts.visibility)
35
37
  }
36
38
  })
37
39
  }
38
40
 
39
- async function initNew(nameOpt?: string) {
41
+ async function initNew(nameOpt?: string, visibility?: string) {
40
42
  const name = nameOpt ?? basename(process.cwd())
41
43
  const result = await withSpinner(`Creating project "${name}"...`, () =>
42
44
  appRequest('/api/projects/create', { method: 'POST', body: { prompt: name } })
@@ -46,15 +48,19 @@ async function initNew(nameOpt?: string) {
46
48
  printError('Failed to create project — no ID returned')
47
49
  process.exit(1)
48
50
  }
51
+ if (visibility === 'private') {
52
+ await appRequest(`/api/projects/${proj.id}`, { method: 'PATCH', body: { visibility: 'private' } })
53
+ }
49
54
  writeProjectConfig({ projectId: proj.id })
50
- if (isJsonMode()) return printJson({ project_id: proj.id, name: proj.name ?? name })
55
+ if (isJsonMode()) return printJson({ project_id: proj.id, name: proj.name ?? name, visibility: visibility ?? 'public' })
51
56
  printSuccess(`Project created and linked`)
52
57
  printKv('ID', proj.id)
53
58
  printKv('Name', proj.name ?? name)
59
+ if (visibility === 'private') printKv('Visibility', 'private')
54
60
  console.log(chalk.dim('\n Run `blink deploy ./dist --prod` to deploy'))
55
61
  }
56
62
 
57
- async function initFromExisting(sourceId: string) {
63
+ async function initFromExisting(sourceId: string, visibility?: string) {
58
64
  const source = await withSpinner('Loading source project...', () =>
59
65
  appRequest(`/api/projects/${sourceId}`)
60
66
  )
@@ -1,8 +1,8 @@
1
1
  import { Command } from 'commander'
2
2
  import { appRequest } from '../lib/api-app.js'
3
3
  import { requireToken } from '../lib/auth.js'
4
- import { writeProjectConfig, clearProjectConfig, readProjectConfig } from '../lib/project.js'
5
- import { printJson, isJsonMode, withSpinner, createTable } from '../lib/output.js'
4
+ import { writeProjectConfig, clearProjectConfig, readProjectConfig, requireProjectId } from '../lib/project.js'
5
+ import { printJson, printSuccess, isJsonMode, withSpinner, createTable, printKv } from '../lib/output.js'
6
6
  import chalk from 'chalk'
7
7
 
8
8
  export function registerProjectCommands(program: Command) {
@@ -60,6 +60,36 @@ After creating a project, link it to your current directory:
60
60
  else printJson({ status: 'ok' })
61
61
  })
62
62
 
63
+ project.command('update [project_id]')
64
+ .description('Update project settings')
65
+ .option('--name <name>', 'Rename the project')
66
+ .option('--visibility <vis>', 'Set visibility: public or private')
67
+ .addHelpText('after', `
68
+ Examples:
69
+ $ blink project update --visibility private
70
+ $ blink project update --name "New Name"
71
+ $ blink project update proj_xxx --visibility public
72
+ $ blink project update --visibility private --name "Secret Project"
73
+ `)
74
+ .action(async (projectArg: string | undefined, opts) => {
75
+ requireToken()
76
+ const projectId = requireProjectId(projectArg)
77
+ const body: Record<string, string> = {}
78
+ if (opts.name) body.name = opts.name
79
+ if (opts.visibility) body.visibility = opts.visibility
80
+ if (!Object.keys(body).length) {
81
+ console.error('Error: Provide at least one option (--name, --visibility)')
82
+ process.exit(1)
83
+ }
84
+ const result = await withSpinner('Updating project...', () =>
85
+ appRequest(`/api/projects/${projectId}`, { method: 'PATCH', body })
86
+ )
87
+ if (isJsonMode()) return printJson(result)
88
+ printSuccess('Project updated')
89
+ if (opts.name) printKv('Name', opts.name)
90
+ if (opts.visibility) printKv('Visibility', opts.visibility)
91
+ })
92
+
63
93
  program.command('link [project_id]')
64
94
  .description('Link current directory to a project — saves project_id to .blink/project.json')
65
95
  .addHelpText('after', `
@@ -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
+ }