bunqueue 2.8.8 → 2.8.10
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/client.d.ts +9 -0
- package/dist/cli/client.js +35 -9
- package/dist/cli/commands/backup.js +3 -3
- package/dist/cli/commands/cron.js +16 -3
- package/dist/cli/commands/job.js +5 -1
- package/dist/cli/commands/server.d.ts +3 -1
- package/dist/cli/commands/server.js +7 -105
- package/dist/cli/commands/webhook.js +3 -9
- package/dist/cli/help.js +3 -1
- package/dist/cli/index.js +112 -44
- package/dist/cli/output.js +57 -3
- package/dist/client/forwarder.d.ts +72 -0
- package/dist/client/forwarder.js +79 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +1 -0
- package/dist/client/queue/queue.d.ts +9 -0
- package/dist/client/queue/queue.js +19 -0
- package/dist/domain/types/cron.js +3 -1
- package/dist/domain/types/webhook.d.ts +12 -2
- package/dist/domain/types/webhook.js +13 -0
- package/dist/infrastructure/server/bootstrap.d.ts +9 -0
- package/dist/infrastructure/server/bootstrap.js +217 -0
- package/dist/infrastructure/server/handlers/advanced.js +8 -0
- package/dist/infrastructure/server/handlers/monitoring.js +7 -0
- package/dist/main.js +12 -224
- package/dist/mcp/tools/webhookTools.js +2 -10
- package/package.json +1 -1
package/dist/cli/client.d.ts
CHANGED
|
@@ -16,5 +16,14 @@ export interface ClientOptions {
|
|
|
16
16
|
/** Output as JSON */
|
|
17
17
|
json: boolean;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Client-side timeout for a command: base 30s; ONLY the long-poll commands
|
|
21
|
+
* (PULL/WaitJob, where `timeout` means "how long the server may hold the
|
|
22
|
+
* request") get the server-side timeout plus a 10s network buffer so the
|
|
23
|
+
* client never gives up first. On every other command a `timeout` field has
|
|
24
|
+
* a different meaning (e.g. PUSH: job execution timeout) and must not
|
|
25
|
+
* stretch the client wait.
|
|
26
|
+
*/
|
|
27
|
+
export declare function clientTimeoutFor(cmd: Record<string, unknown>): number;
|
|
19
28
|
/** Execute a CLI command against the server */
|
|
20
29
|
export declare function executeCommand(command: string, args: string[], options: ClientOptions): Promise<void>;
|
package/dist/cli/client.js
CHANGED
|
@@ -7,14 +7,27 @@ import { pack, unpack } from 'msgpackr';
|
|
|
7
7
|
import { FrameParser, FrameSizeError } from '../infrastructure/server/protocol';
|
|
8
8
|
import { CommandError } from './commands/types';
|
|
9
9
|
import { buildClientTls } from '../client/tcp/connection';
|
|
10
|
+
/**
|
|
11
|
+
* Client-side timeout for a command: base 30s; ONLY the long-poll commands
|
|
12
|
+
* (PULL/WaitJob, where `timeout` means "how long the server may hold the
|
|
13
|
+
* request") get the server-side timeout plus a 10s network buffer so the
|
|
14
|
+
* client never gives up first. On every other command a `timeout` field has
|
|
15
|
+
* a different meaning (e.g. PUSH: job execution timeout) and must not
|
|
16
|
+
* stretch the client wait.
|
|
17
|
+
*/
|
|
18
|
+
export function clientTimeoutFor(cmd) {
|
|
19
|
+
const isLongPoll = cmd.cmd === 'PULL' || cmd.cmd === 'WaitJob';
|
|
20
|
+
const cmdTimeout = isLongPoll && typeof cmd.timeout === 'number' ? cmd.timeout : 0;
|
|
21
|
+
return Math.max(30000, cmdTimeout + 10000);
|
|
22
|
+
}
|
|
10
23
|
/** Send a command and wait for response */
|
|
11
|
-
async function sendCommand(socket, command) {
|
|
24
|
+
async function sendCommand(socket, command, timeoutMs = 30000) {
|
|
12
25
|
return new Promise((resolve, reject) => {
|
|
13
26
|
const timeoutId = setTimeout(() => {
|
|
14
27
|
socket.data.resolve = null;
|
|
15
28
|
socket.data.reject = null;
|
|
16
29
|
reject(new Error('Command timeout'));
|
|
17
|
-
},
|
|
30
|
+
}, timeoutMs);
|
|
18
31
|
socket.data.resolve = (value) => {
|
|
19
32
|
clearTimeout(timeoutId);
|
|
20
33
|
resolve(value);
|
|
@@ -124,6 +137,13 @@ async function connect(options) {
|
|
|
124
137
|
export async function executeCommand(command, args, options) {
|
|
125
138
|
let connection = null;
|
|
126
139
|
try {
|
|
140
|
+
// Build the command first: unknown commands and parse errors must be
|
|
141
|
+
// reported without requiring a reachable server.
|
|
142
|
+
const cmd = await buildCommand(command, args);
|
|
143
|
+
if (!cmd) {
|
|
144
|
+
console.error(formatError(`Unknown command: ${command}`, options.json));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
127
147
|
connection = await connect(options);
|
|
128
148
|
// Authenticate if token provided
|
|
129
149
|
if (options.token) {
|
|
@@ -138,14 +158,8 @@ export async function executeCommand(command, args, options) {
|
|
|
138
158
|
process.exit(1);
|
|
139
159
|
}
|
|
140
160
|
}
|
|
141
|
-
// Build the command
|
|
142
|
-
const cmd = await buildCommand(command, args);
|
|
143
|
-
if (!cmd) {
|
|
144
|
-
console.error(formatError(`Unknown command: ${command}`, options.json));
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
161
|
// Execute command
|
|
148
|
-
const response = await sendCommand(connection.socket, cmd);
|
|
162
|
+
const response = await sendCommand(connection.socket, cmd, clientTimeoutFor(cmd));
|
|
149
163
|
// Extract subcommand (first positional arg) so the formatter can pick the
|
|
150
164
|
// right verb for batch-id responses (queue drain → "Drained", dlq retry
|
|
151
165
|
// → "Retried", etc.). Falls back to undefined when not applicable.
|
|
@@ -154,6 +168,18 @@ export async function executeCommand(command, args, options) {
|
|
|
154
168
|
console.error(formatOutput(response, command, options.json, subcommand));
|
|
155
169
|
process.exit(1);
|
|
156
170
|
}
|
|
171
|
+
// `job wait` that ran out of time replies ok:true + completed:false —
|
|
172
|
+
// for scripts that is a failure, not an "OK": exit 1 with a clear message.
|
|
173
|
+
if (cmd.cmd === 'WaitJob' && response.completed === false) {
|
|
174
|
+
console.error(formatError('Job not completed within timeout', options.json));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
// GetState replies ok:true + state:'unknown' for a missing job — align
|
|
178
|
+
// with `job get` (Job not found, exit 1) instead of a false success.
|
|
179
|
+
if (cmd.cmd === 'GetState' && response.state === 'unknown') {
|
|
180
|
+
console.error(formatError('Job not found', options.json));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
157
183
|
console.log(formatOutput(response, command, options.json, subcommand));
|
|
158
184
|
// Worker registrations are tied to the TCP connection — the server
|
|
159
185
|
// auto-unregisters when the client disconnects (see tcp.ts close handler).
|
|
@@ -12,12 +12,12 @@ import { CommandError, requireArg } from './types';
|
|
|
12
12
|
export async function executeBackupCommand(args) {
|
|
13
13
|
const subcommand = args[0];
|
|
14
14
|
const subArgs = args.slice(1);
|
|
15
|
-
// Get database path from env
|
|
16
|
-
const dataPath = Bun.env.DATA_PATH ?? Bun.env.SQLITE_PATH;
|
|
15
|
+
// Get database path from env — canonical priority (see config/resolve.ts)
|
|
16
|
+
const dataPath = Bun.env.BUNQUEUE_DATA_PATH ?? Bun.env.BQ_DATA_PATH ?? Bun.env.DATA_PATH ?? Bun.env.SQLITE_PATH;
|
|
17
17
|
if (!dataPath) {
|
|
18
18
|
return {
|
|
19
19
|
success: false,
|
|
20
|
-
message: '
|
|
20
|
+
message: 'BUNQUEUE_DATA_PATH not set. Backup requires persistent storage.',
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
// Create backup manager
|
|
@@ -56,14 +56,27 @@ function buildCronAdd(args) {
|
|
|
56
56
|
if (values.schedule)
|
|
57
57
|
cmd.schedule = values.schedule;
|
|
58
58
|
const every = parseNumberArg(values.every, 'every');
|
|
59
|
-
if (every !== undefined)
|
|
59
|
+
if (every !== undefined) {
|
|
60
|
+
// <= 0 would compute a nextRun that is always in the past: the scheduler
|
|
61
|
+
// would fire the cron on every tick, indefinitely.
|
|
62
|
+
if (every <= 0) {
|
|
63
|
+
throw new CommandError(`Invalid every: ${every}. Must be a positive interval in ms`);
|
|
64
|
+
}
|
|
60
65
|
cmd.repeatEvery = every;
|
|
66
|
+
}
|
|
61
67
|
const priority = parseNumberArg(values.priority, 'priority');
|
|
62
68
|
if (priority !== undefined)
|
|
63
69
|
cmd.priority = priority;
|
|
64
70
|
const maxLimit = parseNumberArg(values['max-limit'], 'max-limit');
|
|
65
|
-
if (maxLimit !== undefined)
|
|
66
|
-
|
|
71
|
+
if (maxLimit !== undefined) {
|
|
72
|
+
if (maxLimit < 0) {
|
|
73
|
+
throw new CommandError(`Invalid max-limit: ${maxLimit}. Must be >= 0 (0 = unlimited)`);
|
|
74
|
+
}
|
|
75
|
+
// 0 = unlimited: omit the field so the server stores null (no limit).
|
|
76
|
+
// Sending 0 would make isAtLimit treat the cron as already exhausted.
|
|
77
|
+
if (maxLimit > 0)
|
|
78
|
+
cmd.maxLimit = maxLimit;
|
|
79
|
+
}
|
|
67
80
|
if (values.timezone)
|
|
68
81
|
cmd.timezone = values.timezone;
|
|
69
82
|
return cmd;
|
package/dist/cli/commands/job.js
CHANGED
|
@@ -151,7 +151,11 @@ function buildWaitJob(args) {
|
|
|
151
151
|
id,
|
|
152
152
|
};
|
|
153
153
|
const timeout = parseNumberArg(values.timeout, 'timeout');
|
|
154
|
-
if (timeout !== undefined)
|
|
154
|
+
if (timeout !== undefined) {
|
|
155
|
+
if (timeout < 0) {
|
|
156
|
+
throw new CommandError(`Invalid timeout: ${timeout}. Must be >= 0 ms`);
|
|
157
|
+
}
|
|
155
158
|
cmd.timeout = timeout;
|
|
159
|
+
}
|
|
156
160
|
return cmd;
|
|
157
161
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server Command Handler
|
|
3
|
-
*
|
|
3
|
+
* Parses `bunqueue start` flags and boots the SAME full server as the bare
|
|
4
|
+
* `bunqueue` entry point (shared bootstrap — S3 backup, cloud agent, stats,
|
|
5
|
+
* crash handlers and graceful shutdown included).
|
|
4
6
|
*/
|
|
5
7
|
/** Run the server */
|
|
6
8
|
export declare function runServer(args: string[], showHelp: boolean): Promise<void>;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server Command Handler
|
|
3
|
-
*
|
|
3
|
+
* Parses `bunqueue start` flags and boots the SAME full server as the bare
|
|
4
|
+
* `bunqueue` entry point (shared bootstrap — S3 backup, cloud agent, stats,
|
|
5
|
+
* crash handlers and graceful shutdown included).
|
|
4
6
|
*/
|
|
5
7
|
import { parseArgs } from 'node:util';
|
|
6
8
|
import { printServerHelp } from '../help';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
+
import { loadConfigFile, resolveServerConfig } from '../../config';
|
|
10
|
+
import { bootServer } from '../../infrastructure/server/bootstrap';
|
|
9
11
|
/** Validate port number */
|
|
10
12
|
function validatePort(value, name, defaultPort) {
|
|
11
13
|
const port = parseInt(value, 10);
|
|
@@ -103,106 +105,6 @@ export async function runServer(args, showHelp) {
|
|
|
103
105
|
const fileConfig = await loadConfigFile(flags.configPath);
|
|
104
106
|
const mergedConfig = applyCliFlags(fileConfig, flags);
|
|
105
107
|
const config = resolveServerConfig(mergedConfig);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
let tlsConfig;
|
|
109
|
-
try {
|
|
110
|
-
tlsConfig = resolveTlsServerOptions(config);
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
// Import and start the server components
|
|
117
|
-
const { QueueManager } = await import('../../application/queueManager');
|
|
118
|
-
const { createTcpServer } = await import('../../infrastructure/server/tcp');
|
|
119
|
-
const { createHttpServer } = await import('../../infrastructure/server/http');
|
|
120
|
-
const { serverLog } = await import('../../shared/logger');
|
|
121
|
-
// Initialize
|
|
122
|
-
const qm = new QueueManager({
|
|
123
|
-
dataPath: config.dataPath,
|
|
124
|
-
});
|
|
125
|
-
const authTokens = config.authTokens.length > 0 ? config.authTokens : undefined;
|
|
126
|
-
// Start TCP and HTTP servers
|
|
127
|
-
let tcpServer;
|
|
128
|
-
let httpServer;
|
|
129
|
-
try {
|
|
130
|
-
tcpServer = createTcpServer(qm, {
|
|
131
|
-
port: config.tcpPort,
|
|
132
|
-
hostname: config.hostname,
|
|
133
|
-
authTokens,
|
|
134
|
-
...(tlsConfig && { tls: tlsConfig }),
|
|
135
|
-
});
|
|
136
|
-
httpServer = createHttpServer(qm, {
|
|
137
|
-
port: config.httpPort,
|
|
138
|
-
hostname: config.hostname,
|
|
139
|
-
authTokens,
|
|
140
|
-
...(tlsConfig && { tls: tlsConfig }),
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
145
|
-
console.error(`Failed to start server: ${msg}`);
|
|
146
|
-
qm.shutdown();
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
serverLog.info('bunqueue server started', {
|
|
150
|
-
tcpPort: config.tcpPort,
|
|
151
|
-
httpPort: config.httpPort,
|
|
152
|
-
host: config.hostname,
|
|
153
|
-
dataPath: config.dataPath ?? 'in-memory',
|
|
154
|
-
auth: authTokens ? 'enabled' : 'disabled',
|
|
155
|
-
});
|
|
156
|
-
// Initialize bunqueue Cloud agent (remote dashboard telemetry)
|
|
157
|
-
const { CloudAgent } = await import('../../infrastructure/cloud/cloudAgent');
|
|
158
|
-
const cloudAgent = cloudConfig ? CloudAgent.createFromConfig(qm, cloudConfig) : null;
|
|
159
|
-
if (cloudAgent) {
|
|
160
|
-
cloudAgent.setServerHandles({
|
|
161
|
-
getConnectionCount: () => tcpServer.getConnectionCount(),
|
|
162
|
-
getWsClientCount: () => httpServer.getWsClientCount(),
|
|
163
|
-
getSseClientCount: () => httpServer.getSseClientCount(),
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
const dim = '\x1b[2m';
|
|
167
|
-
const reset = '\x1b[0m';
|
|
168
|
-
const bold = '\x1b[1m';
|
|
169
|
-
const magenta = '\x1b[35m';
|
|
170
|
-
const green = '\x1b[32m';
|
|
171
|
-
const yellow = '\x1b[33m';
|
|
172
|
-
// Format endpoint display
|
|
173
|
-
const tcpDisplay = `${bold}${config.hostname}:${config.tcpPort}${reset}`;
|
|
174
|
-
const httpDisplay = `${bold}${config.hostname}:${config.httpPort}${reset}`;
|
|
175
|
-
console.log(`
|
|
176
|
-
${magenta} (\\(\\ ${reset}
|
|
177
|
-
${magenta} ( -.-) ${bold}bunqueue${reset} ${dim}v${VERSION}${reset}
|
|
178
|
-
${magenta} o_(")(") ${reset}${dim}High-performance job queue for Bun${reset}
|
|
179
|
-
|
|
180
|
-
${dim}─────────────────────────────────────────────────${reset}
|
|
181
|
-
|
|
182
|
-
${green}●${reset} TCP ${tcpDisplay}
|
|
183
|
-
${green}●${reset} HTTP ${httpDisplay}
|
|
184
|
-
${yellow}●${reset} Data ${config.dataPath ?? 'in-memory'}
|
|
185
|
-
${yellow}●${reset} TLS ${tlsConfig ? `${green}enabled${reset}` : `${dim}disabled${reset}`}
|
|
186
|
-
${yellow}●${reset} Auth ${authTokens ? `${green}enabled${reset}` : `${dim}disabled${reset}`}
|
|
187
|
-
${yellow}●${reset} Cloud ${cloudConfig ? `${green}enabled${reset} ${dim}→ ${cloudConfig.url}${reset}` : `${dim}disabled${reset}`}
|
|
188
|
-
|
|
189
|
-
${dim}─────────────────────────────────────────────────${reset}
|
|
190
|
-
|
|
191
|
-
${dim}Press ${bold}Ctrl+C${reset}${dim} to stop${reset}
|
|
192
|
-
`);
|
|
193
|
-
// Handle shutdown
|
|
194
|
-
const shutdown = () => {
|
|
195
|
-
serverLog.info('Shutting down...');
|
|
196
|
-
const doStop = async () => {
|
|
197
|
-
if (cloudAgent)
|
|
198
|
-
await cloudAgent.stop();
|
|
199
|
-
tcpServer.stop();
|
|
200
|
-
httpServer.stop();
|
|
201
|
-
qm.shutdown();
|
|
202
|
-
process.exit(0);
|
|
203
|
-
};
|
|
204
|
-
void doStop();
|
|
205
|
-
};
|
|
206
|
-
process.on('SIGINT', shutdown);
|
|
207
|
-
process.on('SIGTERM', shutdown);
|
|
108
|
+
// Same full server as the bare `bunqueue` entry (shared bootstrap)
|
|
109
|
+
bootServer(mergedConfig, config);
|
|
208
110
|
}
|
|
@@ -4,15 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
6
|
import { CommandError, requireArg } from './types';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'job.failed',
|
|
11
|
-
'job.progress',
|
|
12
|
-
'job.active',
|
|
13
|
-
'job.waiting',
|
|
14
|
-
'job.delayed',
|
|
15
|
-
];
|
|
7
|
+
import { WEBHOOK_EVENTS } from '../../domain/types/webhook';
|
|
8
|
+
/** Valid webhook events — canonical list shared with server/MCP validation */
|
|
9
|
+
const VALID_EVENTS = WEBHOOK_EVENTS;
|
|
16
10
|
/** Build a webhook subcommand */
|
|
17
11
|
export function buildWebhookCommand(args) {
|
|
18
12
|
const subcommand = args[0];
|
package/dist/cli/help.js
CHANGED
|
@@ -98,6 +98,8 @@ GLOBAL OPTIONS:
|
|
|
98
98
|
-H, --host <host> Server host (default: localhost)
|
|
99
99
|
-p, --port <port> TCP port (default: 6789)
|
|
100
100
|
-t, --token <token> Authentication token (env: BQ_TOKEN, BUNQUEUE_TOKEN)
|
|
101
|
+
Note: after \`pull\` / \`job wait\`, -t is their --timeout;
|
|
102
|
+
use --token there
|
|
101
103
|
--tls Connect with TLS (verify with system CAs)
|
|
102
104
|
--tls-ca <file> Trust a custom CA cert (implies --tls)
|
|
103
105
|
--tls-no-verify TLS without cert verification (self-signed, dev only)
|
|
@@ -126,7 +128,7 @@ Options:
|
|
|
126
128
|
--tcp-port <port> TCP server port (default: 6789, env: TCP_PORT)
|
|
127
129
|
--http-port <port> HTTP server port (default: 6790, env: HTTP_PORT)
|
|
128
130
|
--host <host> Bind address (default: 0.0.0.0, env: HOST)
|
|
129
|
-
--data-path <path> SQLite database path (env: DATA_PATH)
|
|
131
|
+
--data-path <path> SQLite database path (env: BUNQUEUE_DATA_PATH, DATA_PATH)
|
|
130
132
|
--auth-tokens <list> Comma-separated auth tokens (env: AUTH_TOKENS)
|
|
131
133
|
--tls-cert <file> PEM certificate for native TLS (env: TLS_CERT_FILE)
|
|
132
134
|
--tls-key <file> PEM private key for native TLS (env: TLS_KEY_FILE)
|
package/dist/cli/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { runServer } from './commands/server';
|
|
7
7
|
import { executeCommand } from './client';
|
|
8
|
-
import { printHelp, printVersion } from './help';
|
|
8
|
+
import { printHelp, printVersion, printPushHelp, printCronAddHelp } from './help';
|
|
9
9
|
import { isBackupCommand, executeBackupCommand } from './commands/backup';
|
|
10
10
|
import { runDoctor } from './commands/doctor';
|
|
11
11
|
import { VERSION } from '../shared/version';
|
|
@@ -82,47 +82,110 @@ function buildTlsOption(state) {
|
|
|
82
82
|
}
|
|
83
83
|
return state.enabled ? true : undefined;
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Exactly two subcommands define their own short `-t` (--timeout):
|
|
87
|
+
* `pull` and `job wait`. ONLY there `-t` is passed through instead of being
|
|
88
|
+
* consumed as the global token flag — every other command keeps `-t TOKEN`
|
|
89
|
+
* working anywhere on the line. The long form `--token` is global everywhere.
|
|
90
|
+
*/
|
|
91
|
+
function commandOwnsShortT(commandArgs) {
|
|
92
|
+
return commandArgs[0] === 'pull' || (commandArgs[0] === 'job' && commandArgs[1] === 'wait');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Handle `--token` / global `-t` and return the index to continue scanning
|
|
96
|
+
* from. When the command already parsed owns short `-t` (pull / job wait),
|
|
97
|
+
* the flag is passed through to the subcommand instead of being consumed.
|
|
98
|
+
*/
|
|
99
|
+
function applyTokenFlag(arg, allArgs, i, state, commandArgs) {
|
|
100
|
+
if (arg === '-t' && commandOwnsShortT(commandArgs)) {
|
|
101
|
+
commandArgs.push(arg); // pull / job wait own -t (timeout): pass through
|
|
102
|
+
return i;
|
|
103
|
+
}
|
|
104
|
+
const nextArg = allArgs[i + 1];
|
|
105
|
+
// A following flag is not a token: don't swallow it (--token --json ...)
|
|
106
|
+
if (nextArg === undefined || nextArg.startsWith('-')) {
|
|
107
|
+
console.warn('Warning: --token requires a value. Token not set.');
|
|
108
|
+
return i;
|
|
109
|
+
}
|
|
110
|
+
state.token = nextArg;
|
|
111
|
+
return i + 1;
|
|
112
|
+
}
|
|
113
|
+
/** Handle `--host`/`-H <value>`; refuses to swallow a following flag. */
|
|
114
|
+
function applyHostFlag(allArgs, i, state) {
|
|
115
|
+
const nextArg = allArgs[i + 1];
|
|
116
|
+
if (nextArg === undefined || nextArg.startsWith('-')) {
|
|
117
|
+
console.warn('Warning: --host requires a value. Using localhost.');
|
|
118
|
+
return i;
|
|
119
|
+
}
|
|
120
|
+
state.host = nextArg;
|
|
121
|
+
state.hostExplicit = true;
|
|
122
|
+
return i + 1;
|
|
123
|
+
}
|
|
124
|
+
/** Handle `--port`/`-p <value>`; refuses to swallow a following flag. */
|
|
125
|
+
function applyPortFlag(allArgs, i, state) {
|
|
126
|
+
const nextArg = allArgs[i + 1];
|
|
127
|
+
if (nextArg === undefined || nextArg.startsWith('-')) {
|
|
128
|
+
console.warn('Warning: --port requires a value. Using default port 6789.');
|
|
129
|
+
return i;
|
|
130
|
+
}
|
|
131
|
+
const parsed = parseInt(nextArg, 10);
|
|
132
|
+
if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
|
|
133
|
+
console.warn(`Warning: Invalid port "${nextArg}". Using default port 6789.`);
|
|
134
|
+
state.port = 6789;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
state.port = parsed;
|
|
138
|
+
state.portExplicit = true;
|
|
139
|
+
}
|
|
140
|
+
return i + 1;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Attached-value short flags (-p10, -Hfoo) shadowing a global flag letter are
|
|
144
|
+
* silently mis-parsed downstream (strict:false expands them to garbage
|
|
145
|
+
* booleans — e.g. push -p10 dropped the priority and pushed anyway). Warn so
|
|
146
|
+
* the user switches to the separated/long form. Exception: -t<value> after
|
|
147
|
+
* pull/job wait, where parseArgs handles the attached form correctly.
|
|
148
|
+
*/
|
|
149
|
+
function warnAmbiguousAttachedShort(arg, commandArgs) {
|
|
150
|
+
if (!/^-[Hpthv][^-\s]/.test(arg))
|
|
151
|
+
return;
|
|
152
|
+
if (arg.startsWith('-t') && commandOwnsShortT(commandArgs))
|
|
153
|
+
return;
|
|
154
|
+
console.warn(`Warning: "${arg}" looks like a short flag with an attached value; ` +
|
|
155
|
+
`use the separated form ("${arg.slice(0, 2)} ${arg.slice(2)}") or the long form.`);
|
|
156
|
+
}
|
|
85
157
|
/** Parse global options from process.argv */
|
|
86
158
|
export function parseGlobalOptions() {
|
|
87
159
|
const allArgs = process.argv.slice(2);
|
|
88
160
|
// Extract global options manually to preserve subcommand flags
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
161
|
+
const hp = {
|
|
162
|
+
host: 'localhost',
|
|
163
|
+
port: 6789,
|
|
164
|
+
hostExplicit: false,
|
|
165
|
+
portExplicit: false,
|
|
166
|
+
};
|
|
167
|
+
const tokenState = {};
|
|
92
168
|
let json = false;
|
|
93
169
|
let help = false;
|
|
94
170
|
let version = false;
|
|
95
|
-
let hostExplicit = false;
|
|
96
|
-
let portExplicit = false;
|
|
97
171
|
const tlsState = { enabled: false, noVerify: false };
|
|
98
172
|
const commandArgs = [];
|
|
99
173
|
let i = 0;
|
|
100
174
|
while (i < allArgs.length) {
|
|
101
175
|
const arg = allArgs[i];
|
|
176
|
+
if (arg === '--') {
|
|
177
|
+
// Separator: everything after -- is opaque to the global parser
|
|
178
|
+
commandArgs.push(...allArgs.slice(i + 1));
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
102
181
|
if (arg === '--host' || arg === '-H') {
|
|
103
|
-
|
|
104
|
-
hostExplicit = true;
|
|
182
|
+
i = applyHostFlag(allArgs, i, hp);
|
|
105
183
|
}
|
|
106
184
|
else if (arg === '--port' || arg === '-p') {
|
|
107
|
-
|
|
108
|
-
const parsed = parseInt(raw, 10);
|
|
109
|
-
if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
|
|
110
|
-
console.warn(`Warning: Invalid port "${raw}". Using default port 6789.`);
|
|
111
|
-
port = 6789;
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
port = parsed;
|
|
115
|
-
portExplicit = true;
|
|
116
|
-
}
|
|
185
|
+
i = applyPortFlag(allArgs, i, hp);
|
|
117
186
|
}
|
|
118
187
|
else if (arg === '--token' || arg === '-t') {
|
|
119
|
-
|
|
120
|
-
if (nextArg === undefined) {
|
|
121
|
-
console.warn('Warning: --token requires a value. Token not set.');
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
token = allArgs[++i];
|
|
125
|
-
}
|
|
188
|
+
i = applyTokenFlag(arg, allArgs, i, tokenState, commandArgs);
|
|
126
189
|
}
|
|
127
190
|
else if (arg.startsWith('--tls')) {
|
|
128
191
|
i = applyTlsFlag(arg, allArgs, i, tlsState, commandArgs);
|
|
@@ -130,26 +193,28 @@ export function parseGlobalOptions() {
|
|
|
130
193
|
else if (arg === '--json') {
|
|
131
194
|
json = true;
|
|
132
195
|
}
|
|
133
|
-
else if (arg === '--help' || arg === '-h') {
|
|
196
|
+
else if (arg === '--help' || (arg === '-h' && commandArgs.length === 0)) {
|
|
197
|
+
// Short -h is global help ONLY before the command: after it, a typo of
|
|
198
|
+
// -H (host) must not suppress execution with a false-success exit 0.
|
|
134
199
|
help = true;
|
|
135
200
|
}
|
|
136
|
-
else if (arg === '--version' || arg === '-v') {
|
|
201
|
+
else if (arg === '--version' || (arg === '-v' && commandArgs.length === 0)) {
|
|
137
202
|
version = true;
|
|
138
203
|
}
|
|
139
204
|
else if (arg.startsWith('--host=')) {
|
|
140
|
-
host = arg.slice(7);
|
|
141
|
-
hostExplicit = true;
|
|
205
|
+
hp.host = arg.slice(7);
|
|
206
|
+
hp.hostExplicit = true;
|
|
142
207
|
}
|
|
143
208
|
else if (arg.startsWith('--port=')) {
|
|
144
209
|
const raw = arg.slice(7);
|
|
145
210
|
const parsed = parseInt(raw, 10);
|
|
146
211
|
if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
|
|
147
212
|
console.warn(`Warning: Invalid port "${raw}". Using default port 6789.`);
|
|
148
|
-
port = 6789;
|
|
213
|
+
hp.port = 6789;
|
|
149
214
|
}
|
|
150
215
|
else {
|
|
151
|
-
port = parsed;
|
|
152
|
-
portExplicit = true;
|
|
216
|
+
hp.port = parsed;
|
|
217
|
+
hp.portExplicit = true;
|
|
153
218
|
}
|
|
154
219
|
}
|
|
155
220
|
else if (arg.startsWith('--token=')) {
|
|
@@ -158,10 +223,11 @@ export function parseGlobalOptions() {
|
|
|
158
223
|
console.warn('Warning: --token= requires a value. Token not set.');
|
|
159
224
|
}
|
|
160
225
|
else {
|
|
161
|
-
token = val;
|
|
226
|
+
tokenState.token = val;
|
|
162
227
|
}
|
|
163
228
|
}
|
|
164
229
|
else {
|
|
230
|
+
warnAmbiguousAttachedShort(arg, commandArgs);
|
|
165
231
|
// Not a global option, pass to command
|
|
166
232
|
commandArgs.push(arg);
|
|
167
233
|
}
|
|
@@ -173,20 +239,18 @@ export function parseGlobalOptions() {
|
|
|
173
239
|
// so they reach parseServerArgs in runServer().
|
|
174
240
|
// Global -p/--port maps to --tcp-port for the server command.
|
|
175
241
|
if (isServerMode) {
|
|
176
|
-
if (hostExplicit) {
|
|
177
|
-
commandArgs.push('--host', host);
|
|
242
|
+
if (hp.hostExplicit) {
|
|
243
|
+
commandArgs.push('--host', hp.host);
|
|
178
244
|
}
|
|
179
|
-
if (portExplicit) {
|
|
180
|
-
commandArgs.push('--tcp-port', String(port));
|
|
245
|
+
if (hp.portExplicit) {
|
|
246
|
+
commandArgs.push('--tcp-port', String(hp.port));
|
|
181
247
|
}
|
|
182
248
|
}
|
|
183
249
|
// Fall back to environment variables for token if not set via CLI flag
|
|
184
250
|
// Priority: --token flag > BQ_TOKEN > BUNQUEUE_TOKEN
|
|
185
|
-
token = resolveToken(token);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!hostExplicit)
|
|
189
|
-
host = resolveEnvHost(host);
|
|
251
|
+
const token = resolveToken(tokenState.token);
|
|
252
|
+
const port = hp.portExplicit ? hp.port : resolveEnvPort(hp.port);
|
|
253
|
+
const host = hp.hostExplicit ? hp.host : resolveEnvHost(hp.host);
|
|
190
254
|
return {
|
|
191
255
|
options: { host, port, token, tls: buildTlsOption(tlsState), json, help, version },
|
|
192
256
|
commandArgs,
|
|
@@ -236,8 +300,12 @@ export async function main() {
|
|
|
236
300
|
}
|
|
237
301
|
// Help for specific command
|
|
238
302
|
if (options.help) {
|
|
239
|
-
|
|
240
|
-
|
|
303
|
+
if (command === 'push')
|
|
304
|
+
printPushHelp();
|
|
305
|
+
else if (command === 'cron')
|
|
306
|
+
printCronAddHelp();
|
|
307
|
+
else
|
|
308
|
+
printHelp();
|
|
241
309
|
process.exit(0);
|
|
242
310
|
}
|
|
243
311
|
// Version command - shows client version + server version if reachable
|
package/dist/cli/output.js
CHANGED
|
@@ -143,6 +143,15 @@ function formatStats(stats) {
|
|
|
143
143
|
lines.push(` Total Completed: ${str(stats.totalCompleted)}`);
|
|
144
144
|
lines.push(` Total Failed: ${str(stats.totalFailed)}`);
|
|
145
145
|
}
|
|
146
|
+
if (stats.uptime !== undefined) {
|
|
147
|
+
lines.push('', ` ${color('Uptime:', colors.cyan)} ${str(stats.uptime)}s`);
|
|
148
|
+
}
|
|
149
|
+
if (stats.pushPerSec !== undefined) {
|
|
150
|
+
lines.push(` Push/sec: ${str(stats.pushPerSec)}`);
|
|
151
|
+
}
|
|
152
|
+
if (stats.pullPerSec !== undefined) {
|
|
153
|
+
lines.push(` Pull/sec: ${str(stats.pullPerSec)}`);
|
|
154
|
+
}
|
|
146
155
|
return lines.join('\n');
|
|
147
156
|
}
|
|
148
157
|
/** Format counts object */
|
|
@@ -167,7 +176,17 @@ function formatCronJobs(jobs) {
|
|
|
167
176
|
const schedule = job.schedule !== null && job.schedule !== undefined
|
|
168
177
|
? str(job.schedule)
|
|
169
178
|
: `every ${str(job.repeatEvery)}ms`;
|
|
170
|
-
|
|
179
|
+
let out = ` ${color(str(job.name), colors.bold)}\n Queue: ${str(job.queue)}\n Schedule: ${schedule}\n Executions: ${str(job.executions)}`;
|
|
180
|
+
if (typeof job.nextRun === 'number') {
|
|
181
|
+
out += `\n Next run: ${new Date(job.nextRun).toISOString()}`;
|
|
182
|
+
}
|
|
183
|
+
if (job.maxLimit !== null && job.maxLimit !== undefined) {
|
|
184
|
+
out += `\n Max: ${str(job.maxLimit)}`;
|
|
185
|
+
}
|
|
186
|
+
if (job.timezone !== null && job.timezone !== undefined) {
|
|
187
|
+
out += `\n Timezone: ${str(job.timezone)}`;
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
171
190
|
});
|
|
172
191
|
return lines.join('\n\n');
|
|
173
192
|
}
|
|
@@ -177,7 +196,25 @@ function formatWorkers(workers) {
|
|
|
177
196
|
return color('No workers registered', colors.yellow);
|
|
178
197
|
}
|
|
179
198
|
return workers
|
|
180
|
-
.map((w) =>
|
|
199
|
+
.map((w) => {
|
|
200
|
+
const queues = Array.isArray(w.queues) ? w.queues.join(', ') : 'none';
|
|
201
|
+
// status matters operationally: a stale worker must be visible
|
|
202
|
+
const status = w.status === 'stale'
|
|
203
|
+
? color('[stale]', colors.red)
|
|
204
|
+
: w.status !== undefined
|
|
205
|
+
? color(`[${str(w.status)}]`, colors.green)
|
|
206
|
+
: '';
|
|
207
|
+
const extra = [];
|
|
208
|
+
if (w.concurrency !== undefined)
|
|
209
|
+
extra.push(`concurrency=${str(w.concurrency)}`);
|
|
210
|
+
if (w.activeJobs !== undefined)
|
|
211
|
+
extra.push(`active=${str(w.activeJobs)}`);
|
|
212
|
+
if (w.processedJobs !== undefined) {
|
|
213
|
+
extra.push(`processed=${str(w.processedJobs)}/failed=${str(w.failedJobs, '0')}`);
|
|
214
|
+
}
|
|
215
|
+
const extraStr = extra.length > 0 ? `\n ${extra.join(' ')}` : '';
|
|
216
|
+
return ` ${color(str(w.id), colors.bold)}: ${str(w.name)} ${status} (${queues})${extraStr}`;
|
|
217
|
+
})
|
|
181
218
|
.join('\n');
|
|
182
219
|
}
|
|
183
220
|
/** Format webhooks list */
|
|
@@ -186,7 +223,15 @@ function formatWebhooks(webhooks) {
|
|
|
186
223
|
return color('No webhooks registered', colors.yellow);
|
|
187
224
|
}
|
|
188
225
|
return webhooks
|
|
189
|
-
.map((w) =>
|
|
226
|
+
.map((w) => {
|
|
227
|
+
const events = Array.isArray(w.events) ? w.events.join(', ') : 'none';
|
|
228
|
+
const enabled = w.enabled === false ? ` ${color('[disabled]', colors.yellow)}` : '';
|
|
229
|
+
const queue = w.queue !== null && w.queue !== undefined ? `\n Queue: ${str(w.queue)}` : '';
|
|
230
|
+
const counters = w.successCount !== undefined || w.failureCount !== undefined
|
|
231
|
+
? `\n Delivered: ${str(w.successCount, '0')} ok / ${str(w.failureCount, '0')} failed`
|
|
232
|
+
: '';
|
|
233
|
+
return ` ${color(str(w.id), colors.bold)}: ${str(w.url)}${enabled}\n Events: ${events}${queue}${counters}`;
|
|
234
|
+
})
|
|
190
235
|
.join('\n\n');
|
|
191
236
|
}
|
|
192
237
|
/** Format DLQ jobs */
|
|
@@ -300,6 +345,15 @@ function formatSuccess(response, command, subcommand) {
|
|
|
300
345
|
// Worker registered
|
|
301
346
|
if ('workerId' in r)
|
|
302
347
|
return color(`Worker registered: ${str(r.workerId)}`, colors.green);
|
|
348
|
+
// Webhook added — show the id, it is needed for `webhook remove`
|
|
349
|
+
if ('webhookId' in r)
|
|
350
|
+
return color(`Webhook added: ${str(r.webhookId)}`, colors.green);
|
|
351
|
+
// Cron scheduled — surface nextRun
|
|
352
|
+
if ('cron' in r && r.cron !== null && typeof r.cron === 'object') {
|
|
353
|
+
const c = r.cron;
|
|
354
|
+
const next = typeof c.nextRun === 'number' ? ` (next run: ${new Date(c.nextRun).toISOString()})` : '';
|
|
355
|
+
return color(`Cron scheduled: ${str(c.name)}${next}`, colors.green);
|
|
356
|
+
}
|
|
303
357
|
// State
|
|
304
358
|
if ('state' in r)
|
|
305
359
|
return `State: ${str(r.state)}`;
|