@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 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(), __toCommonJS(agent_exports));
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: chalk25 } = await import("chalk");
4042
- console.log(chalk25.bold("Active project: ") + projectId);
4043
- console.log(chalk25.dim(`Run: export BLINK_ACTIVE_PROJECT=${projectId}`));
4044
- console.log(chalk25.dim(`Or: eval $(blink use ${projectId} --export)`));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/cli",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
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)')
@@ -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
 
@@ -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 } = require('../lib/agent.js') as typeof import('../lib/agent.js')
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
+ }