@hookbase/cli 1.0.1 → 1.0.3

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.
Files changed (113) hide show
  1. package/dist/commands/api-keys.d.ts.map +1 -1
  2. package/dist/commands/api-keys.js +106 -50
  3. package/dist/commands/api-keys.js.map +1 -1
  4. package/dist/commands/applications.d.ts +27 -0
  5. package/dist/commands/applications.d.ts.map +1 -0
  6. package/dist/commands/applications.js +216 -0
  7. package/dist/commands/applications.js.map +1 -0
  8. package/dist/commands/cron-groups.d.ts +26 -0
  9. package/dist/commands/cron-groups.d.ts.map +1 -0
  10. package/dist/commands/cron-groups.js +282 -0
  11. package/dist/commands/cron-groups.js.map +1 -0
  12. package/dist/commands/cron.d.ts +60 -0
  13. package/dist/commands/cron.d.ts.map +1 -0
  14. package/dist/commands/cron.js +953 -0
  15. package/dist/commands/cron.js.map +1 -0
  16. package/dist/commands/deliveries.d.ts.map +1 -1
  17. package/dist/commands/deliveries.js +36 -29
  18. package/dist/commands/deliveries.js.map +1 -1
  19. package/dist/commands/destinations.d.ts.map +1 -1
  20. package/dist/commands/destinations.js +82 -52
  21. package/dist/commands/destinations.js.map +1 -1
  22. package/dist/commands/endpoints.d.ts +39 -0
  23. package/dist/commands/endpoints.d.ts.map +1 -0
  24. package/dist/commands/endpoints.js +349 -0
  25. package/dist/commands/endpoints.js.map +1 -0
  26. package/dist/commands/events.d.ts.map +1 -1
  27. package/dist/commands/events.js +15 -14
  28. package/dist/commands/events.js.map +1 -1
  29. package/dist/commands/groups/inbound.d.ts +8 -0
  30. package/dist/commands/groups/inbound.d.ts.map +1 -0
  31. package/dist/commands/groups/inbound.js +226 -0
  32. package/dist/commands/groups/inbound.js.map +1 -0
  33. package/dist/commands/groups/outbound.d.ts +8 -0
  34. package/dist/commands/groups/outbound.d.ts.map +1 -0
  35. package/dist/commands/groups/outbound.js +228 -0
  36. package/dist/commands/groups/outbound.js.map +1 -0
  37. package/dist/commands/groups/tools.d.ts +6 -0
  38. package/dist/commands/groups/tools.d.ts.map +1 -0
  39. package/dist/commands/groups/tools.js +243 -0
  40. package/dist/commands/groups/tools.js.map +1 -0
  41. package/dist/commands/logs.js +3 -3
  42. package/dist/commands/logs.js.map +1 -1
  43. package/dist/commands/outbound.d.ts +40 -0
  44. package/dist/commands/outbound.d.ts.map +1 -0
  45. package/dist/commands/outbound.js +389 -0
  46. package/dist/commands/outbound.js.map +1 -0
  47. package/dist/commands/routes.d.ts.map +1 -1
  48. package/dist/commands/routes.js +98 -72
  49. package/dist/commands/routes.js.map +1 -1
  50. package/dist/commands/send.d.ts +9 -0
  51. package/dist/commands/send.d.ts.map +1 -0
  52. package/dist/commands/send.js +132 -0
  53. package/dist/commands/send.js.map +1 -0
  54. package/dist/commands/sources.d.ts.map +1 -1
  55. package/dist/commands/sources.js +78 -42
  56. package/dist/commands/sources.js.map +1 -1
  57. package/dist/commands/tunnels.d.ts.map +1 -1
  58. package/dist/commands/tunnels.js +76 -40
  59. package/dist/commands/tunnels.js.map +1 -1
  60. package/dist/index.js +147 -310
  61. package/dist/index.js.map +1 -1
  62. package/dist/lib/api.d.ts +368 -5
  63. package/dist/lib/api.d.ts.map +1 -1
  64. package/dist/lib/api.js +308 -14
  65. package/dist/lib/api.js.map +1 -1
  66. package/dist/lib/config.d.ts +9 -0
  67. package/dist/lib/config.d.ts.map +1 -1
  68. package/dist/lib/config.js +22 -0
  69. package/dist/lib/config.js.map +1 -1
  70. package/dist/lib/logger.d.ts.map +1 -1
  71. package/dist/lib/logger.js +32 -15
  72. package/dist/lib/logger.js.map +1 -1
  73. package/dist/tui/App.d.ts.map +1 -1
  74. package/dist/tui/App.js +397 -47
  75. package/dist/tui/App.js.map +1 -1
  76. package/dist/tui/Dashboard.js +1 -1
  77. package/dist/tui/Dashboard.js.map +1 -1
  78. package/dist/tui/views/Analytics.d.ts.map +1 -1
  79. package/dist/tui/views/Analytics.js +11 -2
  80. package/dist/tui/views/Analytics.js.map +1 -1
  81. package/dist/tui/views/ApiKeys.d.ts +10 -0
  82. package/dist/tui/views/ApiKeys.d.ts.map +1 -0
  83. package/dist/tui/views/ApiKeys.js +211 -0
  84. package/dist/tui/views/ApiKeys.js.map +1 -0
  85. package/dist/tui/views/Cron.d.ts +10 -0
  86. package/dist/tui/views/Cron.d.ts.map +1 -0
  87. package/dist/tui/views/Cron.js +312 -0
  88. package/dist/tui/views/Cron.js.map +1 -0
  89. package/dist/tui/views/Destinations.d.ts.map +1 -1
  90. package/dist/tui/views/Destinations.js +38 -19
  91. package/dist/tui/views/Destinations.js.map +1 -1
  92. package/dist/tui/views/Events.d.ts.map +1 -1
  93. package/dist/tui/views/Events.js +22 -4
  94. package/dist/tui/views/Events.js.map +1 -1
  95. package/dist/tui/views/Outbound.d.ts +8 -0
  96. package/dist/tui/views/Outbound.d.ts.map +1 -0
  97. package/dist/tui/views/Outbound.js +543 -0
  98. package/dist/tui/views/Outbound.js.map +1 -0
  99. package/dist/tui/views/Overview.d.ts +4 -0
  100. package/dist/tui/views/Overview.d.ts.map +1 -1
  101. package/dist/tui/views/Overview.js +35 -11
  102. package/dist/tui/views/Overview.js.map +1 -1
  103. package/dist/tui/views/Routes.d.ts +12 -0
  104. package/dist/tui/views/Routes.d.ts.map +1 -0
  105. package/dist/tui/views/Routes.js +220 -0
  106. package/dist/tui/views/Routes.js.map +1 -0
  107. package/dist/tui/views/Sources.d.ts.map +1 -1
  108. package/dist/tui/views/Sources.js +22 -12
  109. package/dist/tui/views/Sources.js.map +1 -1
  110. package/dist/tui/views/Tunnels.d.ts.map +1 -1
  111. package/dist/tui/views/Tunnels.js +72 -13
  112. package/dist/tui/views/Tunnels.js.map +1 -1
  113. package/package.json +1 -1
@@ -0,0 +1,953 @@
1
+ import { input, confirm, select } from '@inquirer/prompts';
2
+ import { ExitPromptError } from '@inquirer/core';
3
+ import * as api from '../lib/api.js';
4
+ import * as config from '../lib/config.js';
5
+ import * as logger from '../lib/logger.js';
6
+ /** Helper to check if an error is a prompt cancellation (Ctrl+C) */
7
+ function isPromptCancelled(error) {
8
+ return error instanceof ExitPromptError ||
9
+ (error instanceof Error && error.name === 'ExitPromptError');
10
+ }
11
+ const COMMON_TIMEZONES = [
12
+ { name: 'UTC', value: 'UTC' },
13
+ { name: 'America/New_York (Eastern)', value: 'America/New_York' },
14
+ { name: 'America/Chicago (Central)', value: 'America/Chicago' },
15
+ { name: 'America/Denver (Mountain)', value: 'America/Denver' },
16
+ { name: 'America/Los_Angeles (Pacific)', value: 'America/Los_Angeles' },
17
+ { name: 'Europe/London', value: 'Europe/London' },
18
+ { name: 'Europe/Paris', value: 'Europe/Paris' },
19
+ { name: 'Europe/Berlin', value: 'Europe/Berlin' },
20
+ { name: 'Asia/Tokyo', value: 'Asia/Tokyo' },
21
+ { name: 'Asia/Shanghai', value: 'Asia/Shanghai' },
22
+ { name: 'Asia/Singapore', value: 'Asia/Singapore' },
23
+ { name: 'Australia/Sydney', value: 'Australia/Sydney' },
24
+ ];
25
+ const HTTP_METHODS = [
26
+ { name: 'POST', value: 'POST' },
27
+ { name: 'GET', value: 'GET' },
28
+ { name: 'PUT', value: 'PUT' },
29
+ { name: 'PATCH', value: 'PATCH' },
30
+ { name: 'DELETE', value: 'DELETE' },
31
+ ];
32
+ const COMMON_CRON_PRESETS = [
33
+ { name: 'Every minute', value: '* * * * *' },
34
+ { name: 'Every 5 minutes', value: '*/5 * * * *' },
35
+ { name: 'Every 15 minutes', value: '*/15 * * * *' },
36
+ { name: 'Every 30 minutes', value: '*/30 * * * *' },
37
+ { name: 'Every hour', value: '0 * * * *' },
38
+ { name: 'Every 6 hours', value: '0 */6 * * *' },
39
+ { name: 'Every 12 hours', value: '0 */12 * * *' },
40
+ { name: 'Daily at midnight', value: '0 0 * * *' },
41
+ { name: 'Daily at noon', value: '0 12 * * *' },
42
+ { name: 'Weekly (Sunday midnight)', value: '0 0 * * 0' },
43
+ { name: 'Monthly (1st at midnight)', value: '0 0 1 * *' },
44
+ { name: 'Custom...', value: 'custom' },
45
+ ];
46
+ function requireAuth() {
47
+ if (!config.isAuthenticated()) {
48
+ logger.error('Not logged in. Run "hookbase login" first.');
49
+ process.exit(1);
50
+ }
51
+ return true;
52
+ }
53
+ // Parse date string from API (stored as UTC without Z suffix) to Date object
54
+ function parseUTCDate(dateStr) {
55
+ if (!dateStr)
56
+ return null;
57
+ // API stores dates as "YYYY-MM-DD HH:MM:SS" in UTC
58
+ // Add Z suffix to parse as UTC, or handle ISO format
59
+ const normalized = dateStr.includes('T') ? dateStr : dateStr.replace(' ', 'T') + 'Z';
60
+ return new Date(normalized);
61
+ }
62
+ function formatDate(dateStr) {
63
+ const date = parseUTCDate(dateStr);
64
+ if (!date)
65
+ return '-';
66
+ return date.toLocaleString();
67
+ }
68
+ function formatRelativeTime(dateStr) {
69
+ const date = parseUTCDate(dateStr);
70
+ if (!date)
71
+ return '-';
72
+ const now = new Date();
73
+ const diffMs = date.getTime() - now.getTime();
74
+ const diffMinutes = Math.floor(diffMs / (1000 * 60));
75
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
76
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
77
+ if (diffMs < 0) {
78
+ // Past
79
+ const absDiffMinutes = Math.abs(diffMinutes);
80
+ const absDiffHours = Math.abs(diffHours);
81
+ const absDiffDays = Math.abs(diffDays);
82
+ if (absDiffMinutes < 1)
83
+ return 'just now';
84
+ if (absDiffMinutes < 60)
85
+ return `${absDiffMinutes}m ago`;
86
+ if (absDiffHours < 24)
87
+ return `${absDiffHours}h ago`;
88
+ return `${absDiffDays}d ago`;
89
+ }
90
+ else {
91
+ // Future
92
+ if (diffMinutes < 1)
93
+ return 'in <1m';
94
+ if (diffMinutes < 60)
95
+ return `in ${diffMinutes}m`;
96
+ if (diffHours < 24)
97
+ return `in ${diffHours}h`;
98
+ return `in ${diffDays}d`;
99
+ }
100
+ }
101
+ function describeCronExpression(expr) {
102
+ const parts = expr.split(' ');
103
+ if (parts.length !== 5)
104
+ return expr;
105
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
106
+ // Simple descriptions for common patterns
107
+ if (expr === '* * * * *')
108
+ return 'Every minute';
109
+ if (expr === '0 * * * *')
110
+ return 'Every hour';
111
+ if (expr === '0 0 * * *')
112
+ return 'Daily at midnight';
113
+ if (expr === '0 12 * * *')
114
+ return 'Daily at noon';
115
+ if (expr === '0 0 * * 0')
116
+ return 'Weekly on Sunday';
117
+ if (expr === '0 0 1 * *')
118
+ return 'Monthly on the 1st';
119
+ if (minute.startsWith('*/'))
120
+ return `Every ${minute.slice(2)} minutes`;
121
+ if (hour.startsWith('*/') && minute === '0')
122
+ return `Every ${hour.slice(2)} hours`;
123
+ return expr;
124
+ }
125
+ function validateCronExpression(expr) {
126
+ const parts = expr.trim().split(/\s+/);
127
+ if (parts.length !== 5)
128
+ return false;
129
+ const patterns = [
130
+ /^(\*|([0-5]?\d)(,([0-5]?\d))*|(\*|([0-5]?\d))\/\d+|([0-5]?\d)-([0-5]?\d))$/, // minute
131
+ /^(\*|([01]?\d|2[0-3])(,([01]?\d|2[0-3]))*|(\*|([01]?\d|2[0-3]))\/\d+|([01]?\d|2[0-3])-([01]?\d|2[0-3]))$/, // hour
132
+ /^(\*|([1-9]|[12]\d|3[01])(,([1-9]|[12]\d|3[01]))*|(\*|([1-9]|[12]\d|3[01]))\/\d+|([1-9]|[12]\d|3[01])-([1-9]|[12]\d|3[01]))$/, // day of month
133
+ /^(\*|([1-9]|1[0-2])(,([1-9]|1[0-2]))*|(\*|([1-9]|1[0-2]))\/\d+|([1-9]|1[0-2])-([1-9]|1[0-2]))$/, // month
134
+ /^(\*|[0-6](,[0-6])*|(\*|[0-6])\/\d+|[0-6]-[0-6])$/, // day of week
135
+ ];
136
+ return parts.every((part, i) => patterns[i].test(part));
137
+ }
138
+ export async function cronListCommand(options) {
139
+ requireAuth();
140
+ const spinner = logger.spinner('Fetching cron jobs...');
141
+ const result = await api.getCronJobs();
142
+ if (result.error) {
143
+ spinner.fail('Failed to fetch cron jobs');
144
+ logger.error(result.error);
145
+ return;
146
+ }
147
+ spinner.stop();
148
+ const raw = result.data;
149
+ let jobs = raw?.cronJobs || raw?.cron_jobs || raw?.data || [];
150
+ // Filter to active only by default
151
+ if (!options.all) {
152
+ jobs = jobs.filter((j) => j.is_active ?? j.isActive);
153
+ }
154
+ if (options.json) {
155
+ console.log(JSON.stringify(jobs, null, 2));
156
+ return;
157
+ }
158
+ if (jobs.length === 0) {
159
+ logger.info(options.all ? 'No cron jobs found' : 'No active cron jobs found');
160
+ logger.dim('Create cron jobs with "hookbase cron create"');
161
+ if (!options.all) {
162
+ logger.dim('Use --all to show inactive jobs');
163
+ }
164
+ return;
165
+ }
166
+ logger.table(['ID', 'Name', 'Schedule', 'URL', 'Status', 'Next Run', 'Last Run'], jobs.map((j) => [
167
+ j.id,
168
+ j.name.slice(0, 20) + (j.name.length > 20 ? '...' : ''),
169
+ describeCronExpression(j.cron_expression || j.cronExpression),
170
+ j.url.slice(0, 30) + (j.url.length > 30 ? '...' : ''),
171
+ (j.is_active ?? j.isActive) ? logger.green('active') : logger.dimText('inactive'),
172
+ formatRelativeTime(j.next_run_at || j.nextRunAt),
173
+ formatRelativeTime(j.last_run_at || j.lastRunAt),
174
+ ]));
175
+ logger.log('');
176
+ logger.dim(`Showing ${jobs.length} job(s)${!options.all ? ' (active only, use --all for all)' : ''}`);
177
+ }
178
+ export async function cronCreateCommand(options) {
179
+ requireAuth();
180
+ let name = options.name;
181
+ let cronExpression = options.schedule;
182
+ let url = options.url;
183
+ let method = options.method || 'POST';
184
+ let timezone = options.timezone || 'UTC';
185
+ let payload = options.payload;
186
+ let headers;
187
+ let groupId = options.group;
188
+ // Parse headers if provided
189
+ if (options.headers) {
190
+ try {
191
+ headers = JSON.parse(options.headers);
192
+ }
193
+ catch {
194
+ logger.error('Invalid headers JSON. Use format: \'{"Header-Name": "value"}\'');
195
+ return;
196
+ }
197
+ }
198
+ // Interactive mode - wrapped in try-catch to handle Ctrl+C gracefully
199
+ try {
200
+ if (!name || !cronExpression || !url) {
201
+ name = name || await input({
202
+ message: 'Job name:',
203
+ validate: (value) => value.length > 0 || 'Name is required',
204
+ });
205
+ // Schedule selection
206
+ if (!cronExpression) {
207
+ const preset = await select({
208
+ message: 'Select schedule:',
209
+ choices: COMMON_CRON_PRESETS,
210
+ });
211
+ if (preset === 'custom') {
212
+ cronExpression = await input({
213
+ message: 'Enter cron expression (minute hour day month weekday):',
214
+ validate: (value) => validateCronExpression(value) || 'Invalid cron expression. Use 5-field format: * * * * *',
215
+ });
216
+ }
217
+ else {
218
+ cronExpression = preset;
219
+ }
220
+ }
221
+ url = url || await input({
222
+ message: 'URL to call:',
223
+ validate: (value) => {
224
+ try {
225
+ new URL(value);
226
+ return true;
227
+ }
228
+ catch {
229
+ return 'Invalid URL';
230
+ }
231
+ },
232
+ });
233
+ method = await select({
234
+ message: 'HTTP method:',
235
+ choices: HTTP_METHODS,
236
+ default: 'POST',
237
+ });
238
+ timezone = await select({
239
+ message: 'Timezone:',
240
+ choices: [...COMMON_TIMEZONES, { name: 'Other (enter manually)', value: 'other' }],
241
+ default: 'UTC',
242
+ });
243
+ if (timezone === 'other') {
244
+ timezone = await input({
245
+ message: 'Enter timezone (e.g., America/New_York):',
246
+ default: 'UTC',
247
+ });
248
+ }
249
+ // Optional payload
250
+ const addPayload = await confirm({
251
+ message: 'Add request payload?',
252
+ default: false,
253
+ });
254
+ if (addPayload) {
255
+ payload = await input({
256
+ message: 'Enter JSON payload:',
257
+ validate: (value) => {
258
+ if (!value)
259
+ return true;
260
+ try {
261
+ JSON.parse(value);
262
+ return true;
263
+ }
264
+ catch {
265
+ return 'Invalid JSON';
266
+ }
267
+ },
268
+ });
269
+ }
270
+ // Optional headers
271
+ const addHeaders = await confirm({
272
+ message: 'Add custom headers?',
273
+ default: false,
274
+ });
275
+ if (addHeaders) {
276
+ const headerInput = await input({
277
+ message: 'Enter headers as JSON (e.g., {"X-Api-Key": "secret"}):',
278
+ validate: (value) => {
279
+ if (!value)
280
+ return true;
281
+ try {
282
+ JSON.parse(value);
283
+ return true;
284
+ }
285
+ catch {
286
+ return 'Invalid JSON';
287
+ }
288
+ },
289
+ });
290
+ if (headerInput) {
291
+ headers = JSON.parse(headerInput);
292
+ }
293
+ }
294
+ // Optional group selection
295
+ const groupsResult = await api.getCronGroups();
296
+ const grpRaw = groupsResult.data;
297
+ const availGroups = grpRaw?.groups || grpRaw?.data || [];
298
+ if (availGroups.length > 0) {
299
+ const groupChoice = await select({
300
+ message: 'Add to a group? (optional):',
301
+ choices: [
302
+ { name: 'No group', value: '' },
303
+ ...availGroups.map((g) => ({ name: g.name, value: g.id })),
304
+ ],
305
+ });
306
+ if (groupChoice) {
307
+ groupId = groupChoice;
308
+ }
309
+ }
310
+ }
311
+ if (!options.yes && !options.name) {
312
+ logger.log('');
313
+ logger.log(logger.bold('Summary:'));
314
+ logger.log(` Name: ${name}`);
315
+ logger.log(` Schedule: ${cronExpression} (${describeCronExpression(cronExpression)})`);
316
+ logger.log(` URL: ${method} ${url}`);
317
+ logger.log(` Timezone: ${timezone}`);
318
+ if (payload)
319
+ logger.log(` Payload: ${payload.slice(0, 50)}${payload.length > 50 ? '...' : ''}`);
320
+ if (headers)
321
+ logger.log(` Headers: ${JSON.stringify(headers)}`);
322
+ logger.log('');
323
+ const confirmed = await confirm({
324
+ message: 'Create this cron job?',
325
+ default: true,
326
+ });
327
+ if (!confirmed) {
328
+ logger.info('Cancelled');
329
+ return;
330
+ }
331
+ }
332
+ }
333
+ catch (error) {
334
+ if (isPromptCancelled(error)) {
335
+ logger.log('');
336
+ logger.info('Cancelled');
337
+ return;
338
+ }
339
+ throw error;
340
+ }
341
+ const spinner = logger.spinner('Creating cron job...');
342
+ const result = await api.createCronJob({
343
+ name: name,
344
+ cronExpression: cronExpression,
345
+ url: url,
346
+ method,
347
+ timezone,
348
+ payload,
349
+ headers,
350
+ timeoutMs: options.timeout,
351
+ groupId,
352
+ });
353
+ if (result.error) {
354
+ spinner.fail('Failed to create cron job');
355
+ logger.error(result.error);
356
+ return;
357
+ }
358
+ spinner.succeed('Cron job created');
359
+ const createRaw = result.data;
360
+ const job = createRaw?.cronJob || createRaw?.cron_job || createRaw?.data;
361
+ if (options.json) {
362
+ console.log(JSON.stringify(job, null, 2));
363
+ return;
364
+ }
365
+ if (job) {
366
+ logger.log('');
367
+ logger.box('Cron Job Created', [
368
+ `ID: ${job.id}`,
369
+ `Name: ${job.name}`,
370
+ `Schedule: ${describeCronExpression(job.cron_expression || job.cronExpression)}`,
371
+ `URL: ${job.method} ${job.url}`,
372
+ `Timezone: ${job.timezone}`,
373
+ ``,
374
+ `Next run: ${formatDate(job.next_run_at || job.nextRunAt)}`,
375
+ ].join('\n'));
376
+ }
377
+ }
378
+ export async function cronGetCommand(jobId, options) {
379
+ requireAuth();
380
+ const spinner = logger.spinner('Fetching cron job...');
381
+ const result = await api.getCronJob(jobId);
382
+ if (result.error) {
383
+ spinner.fail('Failed to fetch cron job');
384
+ logger.error(result.error);
385
+ return;
386
+ }
387
+ spinner.stop();
388
+ const getRaw = result.data;
389
+ const job = getRaw?.cronJob || getRaw?.cron_job || getRaw?.data;
390
+ if (options.json) {
391
+ console.log(JSON.stringify(job, null, 2));
392
+ return;
393
+ }
394
+ if (!job) {
395
+ logger.error('Cron job not found');
396
+ return;
397
+ }
398
+ const cronExpr = job.cron_expression || job.cronExpression;
399
+ const nextRun = job.next_run_at || job.nextRunAt;
400
+ const lastRun = job.last_run_at || job.lastRunAt;
401
+ const createdAt = job.created_at || job.createdAt;
402
+ const updatedAt = job.updated_at || job.updatedAt;
403
+ const timeoutMs = job.timeout_ms ?? job.timeoutMs ?? 30000;
404
+ const notifyFailure = job.notify_on_failure ?? job.notifyOnFailure;
405
+ const notifySuccess = job.notify_on_success ?? job.notifyOnSuccess;
406
+ const notifyEmails = job.notify_emails || job.notifyEmails;
407
+ const consecutiveFailures = job.consecutive_failures ?? job.consecutiveFailures;
408
+ logger.log('');
409
+ logger.log(logger.bold('Cron Job Details'));
410
+ logger.log('');
411
+ logger.log(`ID: ${job.id}`);
412
+ logger.log(`Name: ${job.name}`);
413
+ if (job.description) {
414
+ logger.log(`Description: ${job.description}`);
415
+ }
416
+ logger.log(`Schedule: ${cronExpr} (${describeCronExpression(cronExpr)})`);
417
+ logger.log(`Timezone: ${job.timezone}`);
418
+ logger.log(`Status: ${(job.is_active ?? job.isActive) ? logger.green('active') : logger.red('inactive')}`);
419
+ logger.log('');
420
+ logger.log(logger.bold('Request'));
421
+ logger.log(`URL: ${job.url}`);
422
+ logger.log(`Method: ${job.method}`);
423
+ logger.log(`Timeout: ${timeoutMs}ms`);
424
+ if (job.headers) {
425
+ logger.log(`Headers: ${job.headers}`);
426
+ }
427
+ if (job.payload) {
428
+ logger.log(`Payload: ${job.payload}`);
429
+ }
430
+ logger.log('');
431
+ logger.log(logger.bold('Timing'));
432
+ logger.log(`Next run: ${formatDate(nextRun)} (${formatRelativeTime(nextRun)})`);
433
+ logger.log(`Last run: ${formatDate(lastRun)} (${formatRelativeTime(lastRun)})`);
434
+ logger.log(`Created: ${formatDate(createdAt)}`);
435
+ logger.log(`Updated: ${formatDate(updatedAt)}`);
436
+ if (notifyFailure || notifySuccess) {
437
+ logger.log('');
438
+ logger.log(logger.bold('Notifications'));
439
+ logger.log(`On failure: ${notifyFailure ? 'Yes' : 'No'}`);
440
+ logger.log(`On success: ${notifySuccess ? 'Yes' : 'No'}`);
441
+ if (notifyEmails) {
442
+ logger.log(`Emails: ${notifyEmails}`);
443
+ }
444
+ if (consecutiveFailures) {
445
+ logger.log(`Consecutive failures: ${consecutiveFailures}`);
446
+ }
447
+ }
448
+ logger.log('');
449
+ }
450
+ export async function cronUpdateCommand(jobId, options) {
451
+ requireAuth();
452
+ const updateData = {};
453
+ if (options.name)
454
+ updateData.name = options.name;
455
+ if (options.schedule) {
456
+ if (!validateCronExpression(options.schedule)) {
457
+ logger.error('Invalid cron expression. Use 5-field format: * * * * *');
458
+ return;
459
+ }
460
+ updateData.cronExpression = options.schedule;
461
+ }
462
+ if (options.url)
463
+ updateData.url = options.url;
464
+ if (options.method)
465
+ updateData.method = options.method;
466
+ if (options.timezone)
467
+ updateData.timezone = options.timezone;
468
+ if (options.payload)
469
+ updateData.payload = options.payload;
470
+ if (options.headers) {
471
+ try {
472
+ updateData.headers = JSON.parse(options.headers);
473
+ }
474
+ catch {
475
+ logger.error('Invalid headers JSON');
476
+ return;
477
+ }
478
+ }
479
+ if (options.timeout)
480
+ updateData.timeoutMs = options.timeout;
481
+ if (options.active)
482
+ updateData.isActive = true;
483
+ if (options.inactive)
484
+ updateData.isActive = false;
485
+ if (Object.keys(updateData).length === 0) {
486
+ logger.error('No updates specified. Use --name, --schedule, --url, --method, --timezone, --active, or --inactive');
487
+ return;
488
+ }
489
+ const spinner = logger.spinner('Updating cron job...');
490
+ const result = await api.updateCronJob(jobId, updateData);
491
+ if (result.error) {
492
+ spinner.fail('Failed to update cron job');
493
+ logger.error(result.error);
494
+ return;
495
+ }
496
+ spinner.succeed('Cron job updated');
497
+ if (options.json) {
498
+ console.log(JSON.stringify({ success: true, jobId }, null, 2));
499
+ }
500
+ }
501
+ export async function cronDeleteCommand(jobId, options) {
502
+ requireAuth();
503
+ if (!options.yes) {
504
+ const confirmed = await confirm({
505
+ message: `Are you sure you want to delete cron job ${jobId}? This will also delete all execution history. This cannot be undone.`,
506
+ default: false,
507
+ });
508
+ if (!confirmed) {
509
+ logger.info('Cancelled');
510
+ return;
511
+ }
512
+ }
513
+ const spinner = logger.spinner('Deleting cron job...');
514
+ const result = await api.deleteCronJob(jobId);
515
+ if (result.error) {
516
+ spinner.fail('Failed to delete cron job');
517
+ logger.error(result.error);
518
+ return;
519
+ }
520
+ spinner.succeed('Cron job deleted');
521
+ if (options.json) {
522
+ console.log(JSON.stringify({ success: true, jobId }, null, 2));
523
+ }
524
+ }
525
+ export async function cronTriggerCommand(jobId, options) {
526
+ requireAuth();
527
+ const spinner = logger.spinner('Triggering cron job...');
528
+ const result = await api.triggerCronJob(jobId);
529
+ if (result.error) {
530
+ spinner.fail('Failed to trigger cron job');
531
+ logger.error(result.error);
532
+ return;
533
+ }
534
+ spinner.succeed('Cron job triggered');
535
+ const trigRaw = result.data;
536
+ const execution = trigRaw?.execution || trigRaw?.data;
537
+ if (options.json) {
538
+ console.log(JSON.stringify(execution, null, 2));
539
+ return;
540
+ }
541
+ if (execution) {
542
+ logger.log('');
543
+ logger.log(`Execution ID: ${execution.id}`);
544
+ logger.log(`Status: ${getStatusDisplay(execution.status)}`);
545
+ const respStatus = execution.responseStatus ?? execution.response_status;
546
+ if (respStatus) {
547
+ logger.log(`Response: ${respStatus}`);
548
+ }
549
+ const latency = execution.latencyMs ?? execution.latency_ms;
550
+ if (latency) {
551
+ logger.log(`Latency: ${latency}ms`);
552
+ }
553
+ const execError = execution.error || execution.error_message || execution.errorMessage;
554
+ if (execError) {
555
+ logger.log(`Error: ${logger.red(execError)}`);
556
+ }
557
+ logger.log('');
558
+ }
559
+ }
560
+ function getStatusDisplay(status) {
561
+ switch (status) {
562
+ case 'success':
563
+ return logger.green('success');
564
+ case 'failed':
565
+ return logger.red('failed');
566
+ case 'pending':
567
+ return logger.yellow('pending');
568
+ default:
569
+ return status;
570
+ }
571
+ }
572
+ export async function cronHistoryCommand(jobId, options) {
573
+ requireAuth();
574
+ const limit = options.limit || 20;
575
+ const spinner = logger.spinner('Fetching execution history...');
576
+ const result = await api.getCronExecutions(jobId, limit);
577
+ if (result.error) {
578
+ spinner.fail('Failed to fetch execution history');
579
+ logger.error(result.error);
580
+ return;
581
+ }
582
+ spinner.stop();
583
+ const histRaw = result.data;
584
+ const executions = histRaw?.executions || histRaw?.data || [];
585
+ if (options.json) {
586
+ console.log(JSON.stringify(executions, null, 2));
587
+ return;
588
+ }
589
+ if (executions.length === 0) {
590
+ logger.info('No execution history found');
591
+ logger.dim('Trigger the job with "hookbase cron trigger <jobId>"');
592
+ return;
593
+ }
594
+ logger.table(['ID', 'Status', 'HTTP Status', 'Latency', 'Started', 'Completed'], executions.map((e) => {
595
+ const respStatus = e.response_status ?? e.responseStatus;
596
+ const latency = e.latency_ms ?? e.latencyMs;
597
+ const startedAt = e.started_at || e.startedAt;
598
+ const completedAt = e.completed_at || e.completedAt;
599
+ return [
600
+ e.id,
601
+ getStatusDisplay(e.status),
602
+ respStatus ? String(respStatus) : '-',
603
+ latency ? `${latency}ms` : '-',
604
+ formatRelativeTime(startedAt),
605
+ completedAt ? formatRelativeTime(completedAt) : '-',
606
+ ];
607
+ }));
608
+ // Show summary stats
609
+ const successCount = executions.filter((e) => e.status === 'success').length;
610
+ const failedCount = executions.filter((e) => e.status === 'failed').length;
611
+ const execsWithLatency = executions.filter((e) => e.latency_ms ?? e.latencyMs);
612
+ const avgLatency = execsWithLatency.reduce((sum, e) => sum + (e.latency_ms ?? e.latencyMs ?? 0), 0) / (execsWithLatency.length || 1);
613
+ logger.log('');
614
+ logger.dim(`Showing ${executions.length} execution(s)`);
615
+ logger.dim(`Success: ${successCount}, Failed: ${failedCount}, Avg Latency: ${Math.round(avgLatency) || 0}ms`);
616
+ }
617
+ export async function cronEnableCommand(jobId, options) {
618
+ requireAuth();
619
+ const spinner = logger.spinner('Enabling cron job...');
620
+ const result = await api.updateCronJob(jobId, { isActive: true });
621
+ if (result.error) {
622
+ spinner.fail('Failed to enable cron job');
623
+ logger.error(result.error);
624
+ return;
625
+ }
626
+ spinner.succeed('Cron job enabled');
627
+ if (options.json) {
628
+ console.log(JSON.stringify({ success: true, jobId }, null, 2));
629
+ }
630
+ }
631
+ export async function cronDisableCommand(jobId, options) {
632
+ requireAuth();
633
+ const spinner = logger.spinner('Disabling cron job...');
634
+ const result = await api.updateCronJob(jobId, { isActive: false });
635
+ if (result.error) {
636
+ spinner.fail('Failed to disable cron job');
637
+ logger.error(result.error);
638
+ return;
639
+ }
640
+ spinner.succeed('Cron job disabled');
641
+ if (options.json) {
642
+ console.log(JSON.stringify({ success: true, jobId }, null, 2));
643
+ }
644
+ }
645
+ // Interactive cron expression builder
646
+ export async function cronBuilderCommand() {
647
+ logger.log('');
648
+ logger.log(logger.bold('Interactive Cron Expression Builder'));
649
+ logger.dim('Build a cron expression step by step');
650
+ logger.log('');
651
+ // Minute
652
+ const minuteChoice = await select({
653
+ message: 'Minute:',
654
+ choices: [
655
+ { name: 'Every minute (*)', value: '*' },
656
+ { name: 'Every 5 minutes (*/5)', value: '*/5' },
657
+ { name: 'Every 10 minutes (*/10)', value: '*/10' },
658
+ { name: 'Every 15 minutes (*/15)', value: '*/15' },
659
+ { name: 'Every 30 minutes (*/30)', value: '*/30' },
660
+ { name: 'At minute 0', value: '0' },
661
+ { name: 'Custom...', value: 'custom' },
662
+ ],
663
+ });
664
+ let minute = minuteChoice;
665
+ if (minuteChoice === 'custom') {
666
+ minute = await input({
667
+ message: 'Enter minute value (0-59, *, */n, or n-m):',
668
+ validate: (v) => /^(\*|([0-5]?\d)(,([0-5]?\d))*|(\*|([0-5]?\d))\/\d+|([0-5]?\d)-([0-5]?\d))$/.test(v) || 'Invalid minute',
669
+ });
670
+ }
671
+ // Hour
672
+ const hourChoice = await select({
673
+ message: 'Hour:',
674
+ choices: [
675
+ { name: 'Every hour (*)', value: '*' },
676
+ { name: 'Every 2 hours (*/2)', value: '*/2' },
677
+ { name: 'Every 6 hours (*/6)', value: '*/6' },
678
+ { name: 'Every 12 hours (*/12)', value: '*/12' },
679
+ { name: 'At midnight (0)', value: '0' },
680
+ { name: 'At noon (12)', value: '12' },
681
+ { name: 'Business hours (9-17)', value: '9-17' },
682
+ { name: 'Custom...', value: 'custom' },
683
+ ],
684
+ });
685
+ let hour = hourChoice;
686
+ if (hourChoice === 'custom') {
687
+ hour = await input({
688
+ message: 'Enter hour value (0-23, *, */n, or n-m):',
689
+ validate: (v) => /^(\*|([01]?\d|2[0-3])(,([01]?\d|2[0-3]))*|(\*|([01]?\d|2[0-3]))\/\d+|([01]?\d|2[0-3])-([01]?\d|2[0-3]))$/.test(v) || 'Invalid hour',
690
+ });
691
+ }
692
+ // Day of month
693
+ const dayChoice = await select({
694
+ message: 'Day of month:',
695
+ choices: [
696
+ { name: 'Every day (*)', value: '*' },
697
+ { name: '1st of month', value: '1' },
698
+ { name: '15th of month', value: '15' },
699
+ { name: 'Last week (25-31)', value: '25-31' },
700
+ { name: 'Custom...', value: 'custom' },
701
+ ],
702
+ });
703
+ let dayOfMonth = dayChoice;
704
+ if (dayChoice === 'custom') {
705
+ dayOfMonth = await input({
706
+ message: 'Enter day of month (1-31, *, */n, or n-m):',
707
+ validate: (v) => /^(\*|([1-9]|[12]\d|3[01])(,([1-9]|[12]\d|3[01]))*|(\*|([1-9]|[12]\d|3[01]))\/\d+|([1-9]|[12]\d|3[01])-([1-9]|[12]\d|3[01]))$/.test(v) || 'Invalid day',
708
+ });
709
+ }
710
+ // Month
711
+ const monthChoice = await select({
712
+ message: 'Month:',
713
+ choices: [
714
+ { name: 'Every month (*)', value: '*' },
715
+ { name: 'Q1 (Jan-Mar)', value: '1-3' },
716
+ { name: 'Q2 (Apr-Jun)', value: '4-6' },
717
+ { name: 'Q3 (Jul-Sep)', value: '7-9' },
718
+ { name: 'Q4 (Oct-Dec)', value: '10-12' },
719
+ { name: 'Custom...', value: 'custom' },
720
+ ],
721
+ });
722
+ let month = monthChoice;
723
+ if (monthChoice === 'custom') {
724
+ month = await input({
725
+ message: 'Enter month (1-12, *, */n, or n-m):',
726
+ validate: (v) => /^(\*|([1-9]|1[0-2])(,([1-9]|1[0-2]))*|(\*|([1-9]|1[0-2]))\/\d+|([1-9]|1[0-2])-([1-9]|1[0-2]))$/.test(v) || 'Invalid month',
727
+ });
728
+ }
729
+ // Day of week
730
+ const dowChoice = await select({
731
+ message: 'Day of week:',
732
+ choices: [
733
+ { name: 'Every day (*)', value: '*' },
734
+ { name: 'Weekdays (Mon-Fri)', value: '1-5' },
735
+ { name: 'Weekends (Sat-Sun)', value: '0,6' },
736
+ { name: 'Monday', value: '1' },
737
+ { name: 'Friday', value: '5' },
738
+ { name: 'Sunday', value: '0' },
739
+ { name: 'Custom...', value: 'custom' },
740
+ ],
741
+ });
742
+ let dayOfWeek = dowChoice;
743
+ if (dowChoice === 'custom') {
744
+ dayOfWeek = await input({
745
+ message: 'Enter day of week (0-6 where 0=Sunday, *, or n-m):',
746
+ validate: (v) => /^(\*|[0-6](,[0-6])*|(\*|[0-6])\/\d+|[0-6]-[0-6])$/.test(v) || 'Invalid day of week',
747
+ });
748
+ }
749
+ const expression = `${minute} ${hour} ${dayOfMonth} ${month} ${dayOfWeek}`;
750
+ logger.log('');
751
+ logger.box('Generated Cron Expression', [
752
+ expression,
753
+ '',
754
+ `Description: ${describeCronExpression(expression)}`,
755
+ '',
756
+ 'Field reference:',
757
+ ' minute hour day-of-month month day-of-week',
758
+ ' (0-59) (0-23) (1-31) (1-12) (0-6, Sun=0)',
759
+ ].join('\n'));
760
+ const copyAction = await select({
761
+ message: 'What would you like to do?',
762
+ choices: [
763
+ { name: 'Use this to create a new cron job', value: 'create' },
764
+ { name: 'Just show the expression', value: 'show' },
765
+ ],
766
+ });
767
+ if (copyAction === 'create') {
768
+ await cronCreateCommand({ schedule: expression });
769
+ }
770
+ }
771
+ // Monitor cron executions in real-time
772
+ export async function cronFollowCommand(options) {
773
+ requireAuth();
774
+ const pollInterval = (options.interval || 5) * 1000; // Default 5 seconds
775
+ let lastSeenExecutions = new Set();
776
+ let isFirstPoll = true;
777
+ logger.log('');
778
+ logger.log(logger.bold('Monitoring Cron Executions'));
779
+ logger.dim('Press Ctrl+C to stop');
780
+ logger.log('');
781
+ // Get job info if specific job is requested
782
+ let jobName;
783
+ if (options.job) {
784
+ const jobResult = await api.getCronJob(options.job);
785
+ const followJobRaw = jobResult.data;
786
+ const followJob = followJobRaw?.cronJob || followJobRaw?.cron_job || followJobRaw?.data;
787
+ if (followJob) {
788
+ jobName = followJob.name;
789
+ logger.log(`Monitoring job: ${logger.cyan(jobName)}`);
790
+ logger.log('');
791
+ }
792
+ }
793
+ else {
794
+ logger.log('Monitoring all cron jobs');
795
+ logger.log('');
796
+ }
797
+ const poll = async () => {
798
+ try {
799
+ if (options.job) {
800
+ // Monitor specific job
801
+ const result = await api.getCronExecutions(options.job, 10);
802
+ const pollRaw = result.data;
803
+ const pollExecs = pollRaw?.executions || pollRaw?.data || [];
804
+ if (pollExecs.length > 0) {
805
+ const executions = pollExecs;
806
+ for (const exec of executions.reverse()) {
807
+ if (!lastSeenExecutions.has(exec.id)) {
808
+ if (!isFirstPoll) {
809
+ printExecution(exec, jobName);
810
+ }
811
+ lastSeenExecutions.add(exec.id);
812
+ }
813
+ }
814
+ }
815
+ }
816
+ else {
817
+ // Monitor all jobs - need to fetch all jobs first
818
+ const jobsResult = await api.getCronJobs();
819
+ const allJobsRaw = jobsResult.data;
820
+ const allJobs = allJobsRaw?.cronJobs || allJobsRaw?.cron_jobs || allJobsRaw?.data || [];
821
+ if (allJobs.length > 0) {
822
+ for (const job of allJobs) {
823
+ const execResult = await api.getCronExecutions(job.id, 5);
824
+ const execRaw = execResult.data;
825
+ const jobExecs = execRaw?.executions || execRaw?.data || [];
826
+ if (jobExecs.length > 0) {
827
+ for (const exec of jobExecs.reverse()) {
828
+ if (!lastSeenExecutions.has(exec.id)) {
829
+ if (!isFirstPoll) {
830
+ printExecution(exec, job.name);
831
+ }
832
+ lastSeenExecutions.add(exec.id);
833
+ }
834
+ }
835
+ }
836
+ }
837
+ }
838
+ }
839
+ isFirstPoll = false;
840
+ }
841
+ catch (error) {
842
+ // Silently continue on errors
843
+ }
844
+ };
845
+ // Initial poll to get baseline
846
+ await poll();
847
+ isFirstPoll = false;
848
+ // Set up polling interval
849
+ const intervalId = setInterval(poll, pollInterval);
850
+ // Handle Ctrl+C gracefully
851
+ process.on('SIGINT', () => {
852
+ clearInterval(intervalId);
853
+ logger.log('');
854
+ logger.info('Stopped monitoring');
855
+ process.exit(0);
856
+ });
857
+ // Keep the process running
858
+ await new Promise(() => { });
859
+ }
860
+ function printExecution(exec, jobName) {
861
+ const startedAt = exec.started_at || exec.startedAt;
862
+ const timestamp = new Date(startedAt).toLocaleTimeString();
863
+ const status = exec.status === 'success'
864
+ ? logger.green('SUCCESS')
865
+ : exec.status === 'failed'
866
+ ? logger.red('FAILED')
867
+ : logger.yellow('PENDING');
868
+ const respStatus = exec.response_status ?? exec.responseStatus;
869
+ const httpStatus = respStatus
870
+ ? (respStatus >= 200 && respStatus < 300
871
+ ? logger.green(String(respStatus))
872
+ : logger.red(String(respStatus)))
873
+ : '-';
874
+ const latency = (exec.latency_ms ?? exec.latencyMs) ? `${exec.latency_ms ?? exec.latencyMs}ms` : '-';
875
+ logger.log(`${logger.dimText(timestamp)} ` +
876
+ `${jobName ? logger.cyan(jobName.padEnd(20)) + ' ' : ''}` +
877
+ `${status.padEnd(17)} ` +
878
+ `HTTP ${httpStatus.padEnd(12)} ` +
879
+ `${logger.dimText(latency)}`);
880
+ const errorMsg = exec.error_message || exec.errorMessage;
881
+ if (errorMsg) {
882
+ logger.log(` ${logger.red('Error:')} ${errorMsg}`);
883
+ }
884
+ }
885
+ // Quick status overview of all cron jobs
886
+ export async function cronStatusCommand(options) {
887
+ requireAuth();
888
+ const spinner = logger.spinner('Fetching cron status...');
889
+ const [jobsResult, groupsResult] = await Promise.all([
890
+ api.getCronJobs(),
891
+ api.getCronGroups(),
892
+ ]);
893
+ if (jobsResult.error) {
894
+ spinner.fail('Failed to fetch cron jobs');
895
+ logger.error(jobsResult.error);
896
+ return;
897
+ }
898
+ spinner.stop();
899
+ const statusJobsRaw = jobsResult.data;
900
+ const jobs = statusJobsRaw?.cronJobs || statusJobsRaw?.cron_jobs || statusJobsRaw?.data || [];
901
+ const statusGroupsRaw = groupsResult.data;
902
+ const groups = statusGroupsRaw?.groups || statusGroupsRaw?.data || [];
903
+ if (options.json) {
904
+ console.log(JSON.stringify({ jobs, groups }, null, 2));
905
+ return;
906
+ }
907
+ const activeJobs = jobs.filter((j) => j.is_active ?? j.isActive);
908
+ const inactiveJobs = jobs.filter((j) => !(j.is_active ?? j.isActive));
909
+ // Jobs with upcoming executions (next 1 hour)
910
+ const now = new Date();
911
+ const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
912
+ const upcomingJobs = activeJobs.filter((j) => {
913
+ const nextRunAt = j.next_run_at || j.nextRunAt;
914
+ if (!nextRunAt)
915
+ return false;
916
+ const nextRun = new Date(nextRunAt);
917
+ return nextRun >= now && nextRun <= oneHourLater;
918
+ });
919
+ logger.log('');
920
+ logger.log(logger.bold('Cron Jobs Status'));
921
+ logger.log('');
922
+ logger.log(`Total jobs: ${jobs.length}`);
923
+ logger.log(`Active: ${logger.green(String(activeJobs.length))}`);
924
+ logger.log(`Inactive: ${logger.dimText(String(inactiveJobs.length))}`);
925
+ logger.log(`Groups: ${groups.length}`);
926
+ logger.log('');
927
+ if (upcomingJobs.length > 0) {
928
+ logger.log(logger.bold('Upcoming Executions (next hour):'));
929
+ logger.log('');
930
+ upcomingJobs
931
+ .sort((a, b) => new Date(a.next_run_at || a.nextRunAt).getTime() - new Date(b.next_run_at || b.nextRunAt).getTime())
932
+ .forEach((job) => {
933
+ const nextRun = new Date(job.next_run_at || job.nextRunAt);
934
+ const diffMinutes = Math.round((nextRun.getTime() - now.getTime()) / (1000 * 60));
935
+ logger.log(` ${logger.cyan(job.name.padEnd(25))} in ${diffMinutes}m (${nextRun.toLocaleTimeString()})`);
936
+ });
937
+ logger.log('');
938
+ }
939
+ // Show recently failed jobs if any
940
+ const recentlyFailed = jobs.filter((j) => (j.consecutive_failures ?? j.consecutiveFailures ?? 0) > 0);
941
+ if (recentlyFailed.length > 0) {
942
+ logger.log(logger.bold(logger.red('Jobs with Recent Failures:')));
943
+ logger.log('');
944
+ recentlyFailed.forEach((job) => {
945
+ const failures = job.consecutive_failures ?? job.consecutiveFailures ?? 0;
946
+ logger.log(` ${logger.red(job.name.padEnd(25))} ${failures} consecutive failure(s)`);
947
+ });
948
+ logger.log('');
949
+ }
950
+ logger.dim('Use "hookbase cron list" for full job listing');
951
+ logger.dim('Use "hookbase cron follow" to monitor executions in real-time');
952
+ }
953
+ //# sourceMappingURL=cron.js.map