@qodalis/cli-server-jobs 2.0.0-beta.9 → 2.0.0-rc.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/public-api.js CHANGED
@@ -93,7 +93,7 @@ var CliJobsService = class {
93
93
 
94
94
  // src/lib/processors/cli-jobs-command-processor.ts
95
95
  function getServices(context, args) {
96
- const manager = context.services.get("cli-server-manager");
96
+ const manager = context.services.getRequired("cli-server-manager");
97
97
  if (!manager || !manager.connections) {
98
98
  return [];
99
99
  }
@@ -104,7 +104,7 @@ function getServices(context, args) {
104
104
  if (!connection.connected) continue;
105
105
  const config = connection.config;
106
106
  const baseUrl = config.url.endsWith("/") ? config.url.slice(0, -1) : config.url;
107
- const headers = config.headers ?? {};
107
+ const headers = cliCore.resolveServerHeaders(context.services, name, config.headers);
108
108
  results.push({ name, service: new CliJobsService(baseUrl, headers) });
109
109
  }
110
110
  return results;
@@ -117,10 +117,41 @@ async function resolveJobId(service, nameOrId) {
117
117
  }
118
118
  }
119
119
  const jobs = await service.listJobs();
120
- const match = jobs.find(
121
- (j) => j.name.toLowerCase() === nameOrId.toLowerCase() || j.id === nameOrId
120
+ const lower = nameOrId.toLowerCase();
121
+ return jobs.find((j) => j.id === nameOrId) ?? jobs.find((j) => j.name === nameOrId) ?? jobs.find((j) => j.name.toLowerCase() === lower) ?? null;
122
+ }
123
+ async function findJobOnServers(context, services, nameOrId) {
124
+ const matches = [];
125
+ const missing = [];
126
+ for (const { name, service } of services) {
127
+ try {
128
+ const job = await resolveJobId(service, nameOrId);
129
+ if (job) {
130
+ matches.push({ name, service, job });
131
+ } else {
132
+ missing.push(name);
133
+ }
134
+ } catch (e) {
135
+ context.writer.writeError(`Error on server ${name}: ${e.message}`);
136
+ }
137
+ }
138
+ if (matches.length === 0 && missing.length > 0) {
139
+ if (missing.length === services.length) {
140
+ context.writer.writeError(
141
+ services.length === 1 ? `Job "${nameOrId}" not found on server ${missing[0]}.` : `Job "${nameOrId}" not found on any of the ${services.length} connected servers.`
142
+ );
143
+ } else {
144
+ context.writer.writeError(
145
+ `Job "${nameOrId}" not found on ${missing.length === 1 ? "server" : "servers"} ${missing.join(", ")}.`
146
+ );
147
+ }
148
+ }
149
+ return matches;
150
+ }
151
+ function writeServerHeader(context, name) {
152
+ context.writer.writeln(
153
+ context.writer.wrapInColor(`Server: ${name}`, cliCore.CliForegroundColor.Cyan)
122
154
  );
123
- return match ?? null;
124
155
  }
125
156
  function formatRelativeTime(isoString) {
126
157
  if (!isoString) return "-";
@@ -195,6 +226,26 @@ var CliJobsCommandProcessor = class {
195
226
  command: "jobs",
196
227
  description: "Manage background jobs on connected servers",
197
228
  author: cliCore.DefaultLibraryAuthor,
229
+ writeDescription: (context) => {
230
+ const t = context.translator;
231
+ cliCore.writeWrapped(
232
+ context,
233
+ t.t("cli.server.jobs.help.see_control_background_jobs", "See and control the background jobs that run on connected servers: list them, run one now, pause or stop it, read its history and logs, change its settings, or watch events live. A job is picked by name (any case) or ID and looked up on every connected server; actions apply to each server that has the job, and --server limits a command to one server.")
234
+ );
235
+ context.writer.writeln();
236
+ cliCore.writeUsage(context, [
237
+ ["server jobs", t.t("cli.server.jobs.help.same_server_jobs_list", "Same as server jobs list")],
238
+ ["server jobs <action> <name|id>", "info, trigger, pause, resume, stop, cancel, history, logs, edit"],
239
+ ["server jobs watch", t.t("cli.server.jobs.help.live_job_events", "Live job events")]
240
+ ]);
241
+ context.writer.writeln();
242
+ cliCore.writeExamples(context, [
243
+ ["server jobs", t.t("cli.server.jobs.help.all_jobs", "All jobs")],
244
+ ["server jobs trigger cleanup", t.t("cli.server.jobs.help.run_cleanup_job_now", "Run the cleanup job now")],
245
+ ["server jobs history cleanup --status failed", t.t("cli.server.jobs.help.failed_runs", "Its failed runs")],
246
+ ["server jobs logs cleanup --server node", t.t("cli.server.jobs.help.last_run_output_node", "Last run output, on node")]
247
+ ]);
248
+ },
198
249
  metadata: {
199
250
  icon: cliCore.CliIcon.Gear,
200
251
  module: "@qodalis/cli-server-jobs"
@@ -205,6 +256,23 @@ var CliJobsCommandProcessor = class {
205
256
  {
206
257
  command: "list",
207
258
  description: "List all jobs across connected servers",
259
+ writeDescription: (context) => {
260
+ const t = context.translator;
261
+ cliCore.writeWrapped(
262
+ context,
263
+ t.t("cli.server.jobs.list.help.list_jobs_every_connected", "List the jobs of every connected server: group, status, schedule, last and next run, and how long the last run took. With several servers, each gets its own table.")
264
+ );
265
+ context.writer.writeln();
266
+ cliCore.writeUsage(context, [
267
+ ["server jobs list", t.t("cli.server.jobs.list.help.jobs_all_connected_servers", "Jobs on all connected servers")],
268
+ ["server jobs ls --group <name>", t.t("cli.server.jobs.list.help.only_one_group", "Only one group")]
269
+ ]);
270
+ context.writer.writeln();
271
+ cliCore.writeExamples(context, [
272
+ ["server jobs ls --server node", t.t("cli.server.jobs.list.help.jobs_node_server", "Jobs on the node server")],
273
+ ["server jobs list --group reports", t.t("cli.server.jobs.list.help.jobs_reports_group", "Jobs in the reports group")]
274
+ ]);
275
+ },
208
276
  aliases: ["ls"],
209
277
  parameters: [
210
278
  serverParam,
@@ -223,6 +291,21 @@ var CliJobsCommandProcessor = class {
223
291
  {
224
292
  command: "info",
225
293
  description: "Show detailed information about a job",
294
+ writeDescription: (context) => {
295
+ const t = context.translator;
296
+ cliCore.writeWrapped(
297
+ context,
298
+ t.t("cli.server.jobs.info.help.show_everything_about_one", "Show everything about one job: schedule, retries, timeout, overlap policy, the execution running now and the result of the last run. The job is found by name (any case) or ID.")
299
+ );
300
+ context.writer.writeln();
301
+ cliCore.writeUsage(context, [
302
+ ["server jobs info <name|id>", ""]
303
+ ]);
304
+ context.writer.writeln();
305
+ cliCore.writeExamples(context, [
306
+ ["server jobs info cleanup", t.t("cli.server.jobs.info.help.details_cleanup_job", "Details of the cleanup job")]
307
+ ]);
308
+ },
226
309
  valueRequired: true,
227
310
  parameters: [serverParam],
228
311
  processCommand: async (cmd, context) => {
@@ -233,6 +316,22 @@ var CliJobsCommandProcessor = class {
233
316
  {
234
317
  command: "trigger",
235
318
  description: "Trigger immediate execution of a job",
319
+ writeDescription: (context) => {
320
+ const t = context.translator;
321
+ cliCore.writeWrapped(
322
+ context,
323
+ t.t("cli.server.jobs.trigger.help.run_job_now_without", "Run a job now, without waiting for its next scheduled time. Its schedule does not change.")
324
+ );
325
+ context.writer.writeln();
326
+ cliCore.writeUsage(context, [
327
+ ["server jobs trigger <name|id>", ""]
328
+ ]);
329
+ context.writer.writeln();
330
+ cliCore.writeExamples(context, [
331
+ ["server jobs trigger cleanup", t.t("cli.server.jobs.trigger.help.run_cleanup_now", "Run cleanup now")],
332
+ ["server jobs logs cleanup", t.t("cli.server.jobs.trigger.help.then_read_output", "Then read its output")]
333
+ ]);
334
+ },
236
335
  valueRequired: true,
237
336
  parameters: [serverParam],
238
337
  processCommand: async (cmd, context) => {
@@ -243,6 +342,22 @@ var CliJobsCommandProcessor = class {
243
342
  {
244
343
  command: "pause",
245
344
  description: "Pause scheduled execution of a job",
345
+ writeDescription: (context) => {
346
+ const t = context.translator;
347
+ cliCore.writeWrapped(
348
+ context,
349
+ t.t("cli.server.jobs.pause.help.stop_running_job_schedule", "Stop running a job on its schedule until it is resumed. Use cancel or stop for a run that is already in progress.")
350
+ );
351
+ context.writer.writeln();
352
+ cliCore.writeUsage(context, [
353
+ ["server jobs pause <name|id>", ""]
354
+ ]);
355
+ context.writer.writeln();
356
+ cliCore.writeExamples(context, [
357
+ ["server jobs pause cleanup", t.t("cli.server.jobs.pause.help.hold_cleanup", "Hold cleanup")],
358
+ ["server jobs resume cleanup", t.t("cli.server.jobs.pause.help.put_back_schedule", "Put it back on schedule")]
359
+ ]);
360
+ },
246
361
  valueRequired: true,
247
362
  parameters: [serverParam],
248
363
  processCommand: async (cmd, context) => {
@@ -253,6 +368,21 @@ var CliJobsCommandProcessor = class {
253
368
  {
254
369
  command: "resume",
255
370
  description: "Resume a paused job",
371
+ writeDescription: (context) => {
372
+ const t = context.translator;
373
+ cliCore.writeWrapped(
374
+ context,
375
+ t.t("cli.server.jobs.resume.help.put_paused_job_back", "Put a paused job back on its schedule.")
376
+ );
377
+ context.writer.writeln();
378
+ cliCore.writeUsage(context, [
379
+ ["server jobs resume <name|id>", ""]
380
+ ]);
381
+ context.writer.writeln();
382
+ cliCore.writeExamples(context, [
383
+ ["server jobs resume cleanup", t.t("cli.server.jobs.resume.help.run_cleanup_schedule_again", "Run cleanup on schedule again")]
384
+ ]);
385
+ },
256
386
  valueRequired: true,
257
387
  parameters: [serverParam],
258
388
  processCommand: async (cmd, context) => {
@@ -263,6 +393,21 @@ var CliJobsCommandProcessor = class {
263
393
  {
264
394
  command: "stop",
265
395
  description: "Stop a job and cancel current execution if running",
396
+ writeDescription: (context) => {
397
+ const t = context.translator;
398
+ cliCore.writeWrapped(
399
+ context,
400
+ t.t("cli.server.jobs.stop.help.take_job_off_schedule", "Take a job off its schedule and cancel the run in progress, if there is one.")
401
+ );
402
+ context.writer.writeln();
403
+ cliCore.writeUsage(context, [
404
+ ["server jobs stop <name|id>", ""]
405
+ ]);
406
+ context.writer.writeln();
407
+ cliCore.writeExamples(context, [
408
+ ["server jobs stop cleanup", t.t("cli.server.jobs.stop.help.stop_cleanup_completely", "Stop cleanup completely")]
409
+ ]);
410
+ },
266
411
  valueRequired: true,
267
412
  parameters: [serverParam],
268
413
  processCommand: async (cmd, context) => {
@@ -273,6 +418,21 @@ var CliJobsCommandProcessor = class {
273
418
  {
274
419
  command: "cancel",
275
420
  description: "Cancel the current execution of a job",
421
+ writeDescription: (context) => {
422
+ const t = context.translator;
423
+ cliCore.writeWrapped(
424
+ context,
425
+ t.t("cli.server.jobs.cancel.help.cancel_run_progress_job", "Cancel the run in progress of a job. The job stays on its schedule and runs again at the next time.")
426
+ );
427
+ context.writer.writeln();
428
+ cliCore.writeUsage(context, [
429
+ ["server jobs cancel <name|id>", ""]
430
+ ]);
431
+ context.writer.writeln();
432
+ cliCore.writeExamples(context, [
433
+ ["server jobs cancel cleanup", t.t("cli.server.jobs.cancel.help.abort_current_cleanup_run", "Abort the current cleanup run")]
434
+ ]);
435
+ },
276
436
  valueRequired: true,
277
437
  parameters: [serverParam],
278
438
  processCommand: async (cmd, context) => {
@@ -283,6 +443,23 @@ var CliJobsCommandProcessor = class {
283
443
  {
284
444
  command: "history",
285
445
  description: "Show execution history for a job",
446
+ writeDescription: (context) => {
447
+ const t = context.translator;
448
+ cliCore.writeWrapped(
449
+ context,
450
+ t.t("cli.server.jobs.history.help.list_past_runs_job", "List the past runs of a job: status, start time, duration, retry attempt and the start of the error message. Use the short ID with logs --exec to read the output of one run.")
451
+ );
452
+ context.writer.writeln();
453
+ cliCore.writeUsage(context, [
454
+ ["server jobs history <name|id>", t.t("cli.server.jobs.history.help.last_20_runs", "The last 20 runs")],
455
+ ["server jobs history <name|id> --limit <n> --offset <n>", t.t("cli.server.jobs.history.help.page_through_older_runs", "Page through older runs")]
456
+ ]);
457
+ context.writer.writeln();
458
+ cliCore.writeExamples(context, [
459
+ ["server jobs history cleanup --status failed", t.t("cli.server.jobs.history.help.only_failed_runs", "Only failed runs")],
460
+ ["server jobs history cleanup --limit 5", t.t("cli.server.jobs.history.help.last_5_runs", "The last 5 runs")]
461
+ ]);
462
+ },
286
463
  valueRequired: true,
287
464
  parameters: [
288
465
  serverParam,
@@ -315,6 +492,22 @@ var CliJobsCommandProcessor = class {
315
492
  {
316
493
  command: "logs",
317
494
  description: "Show logs from the latest or a specific execution",
495
+ writeDescription: (context) => {
496
+ const t = context.translator;
497
+ cliCore.writeWrapped(
498
+ context,
499
+ t.t("cli.server.jobs.logs.help.print_log_entries_latest", "Print the log entries of the latest run of a job, or of one run picked by its execution ID (from history).")
500
+ );
501
+ context.writer.writeln();
502
+ cliCore.writeUsage(context, [
503
+ ["server jobs logs <name|id>", t.t("cli.server.jobs.logs.help.logs_latest_run", "Logs of the latest run")],
504
+ ["server jobs logs <name|id> --exec <id>", t.t("cli.server.jobs.logs.help.logs_one_run", "Logs of one run")]
505
+ ]);
506
+ context.writer.writeln();
507
+ cliCore.writeExamples(context, [
508
+ ["server jobs logs cleanup", t.t("cli.server.jobs.logs.help.what_last_cleanup_run", "What the last cleanup run printed")]
509
+ ]);
510
+ },
318
511
  valueRequired: true,
319
512
  parameters: [
320
513
  serverParam,
@@ -333,6 +526,21 @@ var CliJobsCommandProcessor = class {
333
526
  {
334
527
  command: "edit",
335
528
  description: "Interactively edit job settings",
529
+ writeDescription: (context) => {
530
+ const t = context.translator;
531
+ cliCore.writeWrapped(
532
+ context,
533
+ t.t("cli.server.jobs.edit.help.change_job_step_step", "Change a job step by step: description, group, cron schedule or interval, max retries, timeout and overlap policy (skip, queue or cancel). Press Enter to keep a value and Ctrl+C or Escape to cancel; nothing changes until you confirm. A job on several servers needs --server.")
534
+ );
535
+ context.writer.writeln();
536
+ cliCore.writeUsage(context, [
537
+ ["server jobs edit <name|id>", ""]
538
+ ]);
539
+ context.writer.writeln();
540
+ cliCore.writeExamples(context, [
541
+ ["server jobs edit cleanup", t.t("cli.server.jobs.edit.help.edit_cleanup_server_has", "Edit cleanup on the server that has it")]
542
+ ]);
543
+ },
336
544
  valueRequired: true,
337
545
  parameters: [serverParam],
338
546
  processCommand: async (cmd, context) => {
@@ -343,6 +551,21 @@ var CliJobsCommandProcessor = class {
343
551
  {
344
552
  command: "watch",
345
553
  description: "Live view of job events via WebSocket",
554
+ writeDescription: (context) => {
555
+ const t = context.translator;
556
+ cliCore.writeWrapped(
557
+ context,
558
+ t.t("cli.server.jobs.watch.help.follow_job_events_they", "Follow job events as they happen (started, completed, failed and so on) on every connected server, one line per event. Press Ctrl+C to stop.")
559
+ );
560
+ context.writer.writeln();
561
+ cliCore.writeUsage(context, [
562
+ ["server jobs watch", t.t("cli.server.jobs.watch.help.events_all_connected_servers", "Events from all connected servers")]
563
+ ]);
564
+ context.writer.writeln();
565
+ cliCore.writeExamples(context, [
566
+ ["server jobs watch --server node", t.t("cli.server.jobs.watch.help.only_events_node", "Only events from node")]
567
+ ]);
568
+ },
346
569
  parameters: [serverParam],
347
570
  processCommand: async (cmd, context) => {
348
571
  await this.handleWatch(cmd, context);
@@ -416,9 +639,9 @@ var CliJobsCommandProcessor = class {
416
639
  }
417
640
  // ── Info ──────────────────────────────────────────────────────────
418
641
  async handleInfo(cmd, context) {
419
- const nameOrId = (cmd.value ?? "").trim();
642
+ const nameOrId = cliCore.joinShellWords(cmd.value ?? "");
420
643
  if (!nameOrId) {
421
- context.writer.writeError("Usage: jobs info <id|name>");
644
+ context.writer.writeError("Usage: server jobs info <id|name>");
422
645
  return;
423
646
  }
424
647
  const services = getServices(context, cmd.args);
@@ -426,74 +649,53 @@ var CliJobsCommandProcessor = class {
426
649
  context.writer.writeInfo("No connected servers.");
427
650
  return;
428
651
  }
429
- for (const { name, service } of services) {
430
- try {
431
- const job = await resolveJobId(service, nameOrId);
432
- if (!job) {
433
- if (services.length === 1) {
434
- context.writer.writeError(
435
- `Job "${nameOrId}" not found on server ${name}.`
436
- );
437
- }
438
- continue;
439
- }
440
- if (services.length > 1) {
441
- context.writer.writeln(
442
- context.writer.wrapInColor(
443
- `Server: ${name}`,
444
- cliCore.CliForegroundColor.Cyan
445
- )
446
- );
447
- }
448
- const lines = [
449
- ["ID", job.id],
450
- ["Name", job.name],
451
- ["Description", job.description],
452
- ["Group", job.group ?? "-"],
453
- ["Status", statusColor(job.status, context.writer)],
454
- [
455
- "Schedule",
456
- job.schedule ?? (job.interval ? `every ${job.interval}ms` : "-")
457
- ],
458
- ["Max Retries", String(job.maxRetries)],
459
- [
460
- "Timeout",
461
- job.timeout ? `${job.timeout}ms` : "none"
462
- ],
463
- ["Overlap Policy", job.overlapPolicy],
464
- [
465
- "Current Execution",
466
- job.currentExecutionId ?? "none"
467
- ],
468
- ["Next Run", formatRelativeTime(job.nextRunAt)],
469
- ["Last Run", formatRelativeTime(job.lastRunAt)],
470
- [
471
- "Last Run Status",
472
- job.lastRunStatus ? statusColor(job.lastRunStatus, context.writer) : "-"
473
- ],
474
- ["Last Run Duration", formatDuration(job.lastRunDuration)]
475
- ];
476
- for (const [label, value] of lines) {
477
- context.writer.writeln(
478
- ` ${context.writer.wrapInColor(
479
- label.padEnd(20),
480
- cliCore.CliForegroundColor.Yellow
481
- )} ${value}`
482
- );
483
- }
484
- return;
485
- } catch (e) {
486
- context.writer.writeError(
487
- `Error on server ${name}: ${e.message}`
652
+ const matches = await findJobOnServers(context, services, nameOrId);
653
+ for (const { name, job } of matches) {
654
+ if (services.length > 1) {
655
+ writeServerHeader(context, name);
656
+ }
657
+ const lines = [
658
+ ["ID", job.id],
659
+ ["Name", job.name],
660
+ ["Description", job.description],
661
+ ["Group", job.group ?? "-"],
662
+ ["Status", statusColor(job.status, context.writer)],
663
+ [
664
+ "Schedule",
665
+ job.schedule ?? (job.interval ? `every ${job.interval}ms` : "-")
666
+ ],
667
+ ["Max Retries", String(job.maxRetries)],
668
+ ["Timeout", job.timeout ? `${job.timeout}ms` : "none"],
669
+ ["Overlap Policy", job.overlapPolicy],
670
+ ["Current Execution", job.currentExecutionId ?? "none"],
671
+ ["Next Run", formatRelativeTime(job.nextRunAt)],
672
+ ["Last Run", formatRelativeTime(job.lastRunAt)],
673
+ [
674
+ "Last Run Status",
675
+ job.lastRunStatus ? statusColor(job.lastRunStatus, context.writer) : "-"
676
+ ],
677
+ ["Last Run Duration", formatDuration(job.lastRunDuration)]
678
+ ];
679
+ for (const [label, value] of lines) {
680
+ context.writer.writeln(
681
+ ` ${context.writer.wrapInColor(
682
+ label.padEnd(20),
683
+ cliCore.CliForegroundColor.Yellow
684
+ )} ${value}`
488
685
  );
489
686
  }
490
687
  }
491
688
  }
492
689
  // ── Action (trigger/pause/resume/stop/cancel) ────────────────────
690
+ /**
691
+ * Without --server the action applies to every connected server that has
692
+ * the job, each reported on its own line. When several servers have it,
693
+ * a note says so (and how to target one).
694
+ */
493
695
  async handleAction(cmd, context, action) {
494
- const nameOrId = (cmd.value ?? "").trim();
696
+ const nameOrId = cliCore.joinShellWords(cmd.value ?? "");
495
697
  if (!nameOrId) {
496
- context.writer.writeError(`Usage: jobs ${action} <id|name>`);
698
+ context.writer.writeError(`Usage: server jobs ${action} <id|name>`);
497
699
  return;
498
700
  }
499
701
  const services = getServices(context, cmd.args);
@@ -501,17 +703,14 @@ var CliJobsCommandProcessor = class {
501
703
  context.writer.writeInfo("No connected servers.");
502
704
  return;
503
705
  }
504
- for (const { name, service } of services) {
706
+ const matches = await findJobOnServers(context, services, nameOrId);
707
+ if (matches.length > 1) {
708
+ context.writer.writeInfo(
709
+ `Job "${nameOrId}" is on ${matches.length} servers (${matches.map((m) => m.name).join(", ")}); applying ${action} to each. Use --server <name> to target one.`
710
+ );
711
+ }
712
+ for (const { name, service, job } of matches) {
505
713
  try {
506
- const job = await resolveJobId(service, nameOrId);
507
- if (!job) {
508
- if (services.length === 1) {
509
- context.writer.writeError(
510
- `Job "${nameOrId}" not found on server ${name}.`
511
- );
512
- }
513
- continue;
514
- }
515
714
  switch (action) {
516
715
  case "trigger":
517
716
  await service.triggerJob(job.id);
@@ -531,9 +730,8 @@ var CliJobsCommandProcessor = class {
531
730
  }
532
731
  const serverSuffix = services.length > 1 ? ` on server ${name}` : "";
533
732
  context.writer.writeSuccess(
534
- `Job "${job.name}" ${action}${action.endsWith("e") ? "d" : "ed"}${serverSuffix}.`
733
+ `Job "${job.name}" ${PAST_TENSE[action] ?? `${action}ed`}${serverSuffix}.`
535
734
  );
536
- return;
537
735
  } catch (e) {
538
736
  context.writer.writeError(
539
737
  `Failed to ${action} job on ${name}: ${e.message}`
@@ -543,9 +741,9 @@ var CliJobsCommandProcessor = class {
543
741
  }
544
742
  // ── History ──────────────────────────────────────────────────────
545
743
  async handleHistory(cmd, context) {
546
- const nameOrId = (cmd.value ?? "").trim();
744
+ const nameOrId = cliCore.joinShellWords(cmd.value ?? "");
547
745
  if (!nameOrId) {
548
- context.writer.writeError("Usage: jobs history <id|name>");
746
+ context.writer.writeError("Usage: server jobs history <id|name>");
549
747
  return;
550
748
  }
551
749
  const services = getServices(context, cmd.args);
@@ -556,24 +754,11 @@ var CliJobsCommandProcessor = class {
556
754
  const limit = cmd.args["limit"] ?? 20;
557
755
  const offset = cmd.args["offset"] ?? 0;
558
756
  const status = cmd.args["status"];
559
- for (const { name, service } of services) {
757
+ const matches = await findJobOnServers(context, services, nameOrId);
758
+ for (const { name, service, job } of matches) {
560
759
  try {
561
- const job = await resolveJobId(service, nameOrId);
562
- if (!job) {
563
- if (services.length === 1) {
564
- context.writer.writeError(
565
- `Job "${nameOrId}" not found on server ${name}.`
566
- );
567
- }
568
- continue;
569
- }
570
760
  if (services.length > 1) {
571
- context.writer.writeln(
572
- context.writer.wrapInColor(
573
- `Server: ${name}`,
574
- cliCore.CliForegroundColor.Cyan
575
- )
576
- );
761
+ writeServerHeader(context, name);
577
762
  }
578
763
  const history = await service.getHistory(job.id, {
579
764
  limit,
@@ -584,7 +769,7 @@ var CliJobsCommandProcessor = class {
584
769
  context.writer.writeInfo(
585
770
  `No execution history for "${job.name}".`
586
771
  );
587
- return;
772
+ continue;
588
773
  }
589
774
  context.writer.writeln(
590
775
  `Showing ${history.items.length} of ${history.total} executions for "${job.name}":`
@@ -606,7 +791,6 @@ var CliJobsCommandProcessor = class {
606
791
  exec.error ? exec.error.substring(0, 40) : "-"
607
792
  ]);
608
793
  context.writer.writeTable(headers, rows);
609
- return;
610
794
  } catch (e) {
611
795
  context.writer.writeError(
612
796
  `Error on server ${name}: ${e.message}`
@@ -616,9 +800,9 @@ var CliJobsCommandProcessor = class {
616
800
  }
617
801
  // ── Logs ─────────────────────────────────────────────────────────
618
802
  async handleLogs(cmd, context) {
619
- const nameOrId = (cmd.value ?? "").trim();
803
+ const nameOrId = cliCore.joinShellWords(cmd.value ?? "");
620
804
  if (!nameOrId) {
621
- context.writer.writeError("Usage: jobs logs <id|name>");
805
+ context.writer.writeError("Usage: server jobs logs <id|name>");
622
806
  return;
623
807
  }
624
808
  const services = getServices(context, cmd.args);
@@ -627,16 +811,11 @@ var CliJobsCommandProcessor = class {
627
811
  return;
628
812
  }
629
813
  const execId = cmd.args["exec"];
630
- for (const { name, service } of services) {
814
+ const matches = await findJobOnServers(context, services, nameOrId);
815
+ for (const { name, service, job } of matches) {
631
816
  try {
632
- const job = await resolveJobId(service, nameOrId);
633
- if (!job) {
634
- if (services.length === 1) {
635
- context.writer.writeError(
636
- `Job "${nameOrId}" not found on server ${name}.`
637
- );
638
- }
639
- continue;
817
+ if (services.length > 1) {
818
+ writeServerHeader(context, name);
640
819
  }
641
820
  let execution;
642
821
  if (execId) {
@@ -649,27 +828,19 @@ var CliJobsCommandProcessor = class {
649
828
  context.writer.writeInfo(
650
829
  `No executions found for "${job.name}".`
651
830
  );
652
- return;
831
+ continue;
653
832
  }
654
833
  execution = await service.getExecution(
655
834
  job.id,
656
835
  history.items[0].id
657
836
  );
658
837
  }
659
- if (services.length > 1) {
660
- context.writer.writeln(
661
- context.writer.wrapInColor(
662
- `Server: ${name}`,
663
- cliCore.CliForegroundColor.Cyan
664
- )
665
- );
666
- }
667
838
  context.writer.writeln(
668
839
  `Logs for "${job.name}" execution ${execution.id.substring(0, 8)} (${statusColor(execution.status, context.writer)}):`
669
840
  );
670
841
  if (!execution.logs || execution.logs.length === 0) {
671
842
  context.writer.writeInfo(" No log entries.");
672
- return;
843
+ continue;
673
844
  }
674
845
  for (const entry of execution.logs) {
675
846
  const ts = new Date(entry.timestamp).toLocaleTimeString();
@@ -681,7 +852,6 @@ var CliJobsCommandProcessor = class {
681
852
  ` ${context.writer.wrapInColor(ts, cliCore.CliForegroundColor.White)} ${level} ${entry.message}`
682
853
  );
683
854
  }
684
- return;
685
855
  } catch (e) {
686
856
  context.writer.writeError(
687
857
  `Error on server ${name}: ${e.message}`
@@ -690,10 +860,15 @@ var CliJobsCommandProcessor = class {
690
860
  }
691
861
  }
692
862
  // ── Edit ─────────────────────────────────────────────────────────
863
+ /**
864
+ * Prompt for each setting; Enter keeps the current value. Ctrl+C or
865
+ * Escape at any prompt (the reader resolves null) cancels the whole edit
866
+ * and nothing is sent. A job on several servers needs --server.
867
+ */
693
868
  async handleEdit(cmd, context) {
694
- const nameOrId = (cmd.value ?? "").trim();
869
+ const nameOrId = cliCore.joinShellWords(cmd.value ?? "");
695
870
  if (!nameOrId) {
696
- context.writer.writeError("Usage: jobs edit <id|name>");
871
+ context.writer.writeError("Usage: server jobs edit <id|name>");
697
872
  return;
698
873
  }
699
874
  const services = getServices(context, cmd.args);
@@ -701,118 +876,113 @@ var CliJobsCommandProcessor = class {
701
876
  context.writer.writeInfo("No connected servers.");
702
877
  return;
703
878
  }
704
- for (const { name, service } of services) {
705
- try {
706
- const job = await resolveJobId(service, nameOrId);
707
- if (!job) {
708
- if (services.length === 1) {
709
- context.writer.writeError(
710
- `Job "${nameOrId}" not found on server ${name}.`
711
- );
712
- }
713
- continue;
714
- }
715
- context.writer.writeln(
716
- `Editing job "${context.writer.wrapInColor(job.name, cliCore.CliForegroundColor.Cyan)}" on server ${name}`
717
- );
718
- context.writer.writeln(
719
- "Press Enter to keep current value, or type a new value."
720
- );
721
- context.writer.writeln("");
722
- const update = {};
723
- const newDesc = await context.reader.readLine(
724
- `Description [${job.description}]: `
725
- );
726
- if (newDesc == null) {
727
- context.writer.writeInfo("Edit cancelled.");
728
- return;
729
- }
730
- if (newDesc.trim()) {
731
- update.description = newDesc.trim();
732
- }
733
- const newGroup = await context.reader.readLine(
734
- `Group [${job.group ?? "none"}]: `
735
- );
736
- if (newGroup == null) {
737
- context.writer.writeInfo("Edit cancelled.");
738
- return;
739
- }
740
- if (newGroup.trim()) {
741
- update.group = newGroup.trim();
742
- }
743
- const currentSchedule = job.schedule ?? (job.interval ? `interval: ${job.interval}ms` : "none");
744
- const newSchedule = await context.reader.readLine(
745
- `Cron schedule [${currentSchedule}]: `
746
- );
747
- if (newSchedule == null) {
748
- context.writer.writeInfo("Edit cancelled.");
749
- return;
750
- }
751
- if (newSchedule.trim()) {
752
- update.schedule = newSchedule.trim();
753
- }
754
- if (!newSchedule.trim()) {
755
- const newInterval = await context.reader.readLine(
756
- `Interval (e.g. 30s, 5m) [${job.interval ? `${job.interval}ms` : "none"}]: `
757
- );
758
- if (newInterval != null && newInterval.trim()) {
759
- update.interval = newInterval.trim();
760
- }
761
- }
762
- const newRetries = await context.reader.readLine(
763
- `Max retries [${job.maxRetries}]: `
764
- );
765
- if (newRetries != null && newRetries.trim()) {
766
- const parsed = parseInt(newRetries.trim(), 10);
767
- if (!isNaN(parsed)) {
768
- update.maxRetries = parsed;
769
- }
770
- }
771
- const newTimeout = await context.reader.readLine(
772
- `Timeout (e.g. 5m, 1h) [${job.timeout ? `${job.timeout}ms` : "none"}]: `
773
- );
774
- if (newTimeout != null && newTimeout.trim()) {
775
- update.timeout = newTimeout.trim();
776
- }
777
- const newOverlap = await context.reader.readLine(
778
- `Overlap policy (skip/queue/cancel) [${job.overlapPolicy}]: `
779
- );
780
- if (newOverlap != null && newOverlap.trim() && ["skip", "queue", "cancel"].includes(
781
- newOverlap.trim().toLowerCase()
782
- )) {
783
- update.overlapPolicy = newOverlap.trim().toLowerCase();
784
- }
785
- if (Object.keys(update).length === 0) {
786
- context.writer.writeInfo("No changes made.");
787
- return;
788
- }
789
- const confirm = await context.reader.readConfirm(
790
- "Apply changes?"
879
+ const matches = await findJobOnServers(context, services, nameOrId);
880
+ if (matches.length === 0) {
881
+ return;
882
+ }
883
+ if (matches.length > 1) {
884
+ context.writer.writeError(
885
+ `Job "${nameOrId}" is on ${matches.length} servers (${matches.map((m) => m.name).join(", ")}). Use --server <name> to choose the one to edit.`
886
+ );
887
+ return;
888
+ }
889
+ const { name, service, job } = matches[0];
890
+ const cancelled = () => {
891
+ context.writer.writeInfo("Edit cancelled.");
892
+ };
893
+ const aborted = () => !!context.signal?.aborted;
894
+ const ask = async (prompt) => {
895
+ if (aborted()) return null;
896
+ const answer = await context.reader.readLine(prompt);
897
+ return answer == null || aborted() ? null : answer.trim();
898
+ };
899
+ try {
900
+ context.writer.writeln(
901
+ `Editing job "${context.writer.wrapInColor(job.name, cliCore.CliForegroundColor.Cyan)}" on server ${name}`
902
+ );
903
+ context.writer.writeln(
904
+ "Press Enter to keep current value, or type a new value. Ctrl+C cancels."
905
+ );
906
+ context.writer.writeln("");
907
+ const update = {};
908
+ const newDesc = await ask(`Description [${job.description}]: `);
909
+ if (newDesc == null) return cancelled();
910
+ if (newDesc) update.description = newDesc;
911
+ const newGroup = await ask(`Group [${job.group ?? "none"}]: `);
912
+ if (newGroup == null) return cancelled();
913
+ if (newGroup) update.group = newGroup;
914
+ const currentSchedule = job.schedule ?? (job.interval ? `interval: ${job.interval}ms` : "none");
915
+ const newSchedule = await ask(
916
+ `Cron schedule [${currentSchedule}]: `
917
+ );
918
+ if (newSchedule == null) return cancelled();
919
+ if (newSchedule) update.schedule = newSchedule;
920
+ if (!newSchedule) {
921
+ const newInterval = await ask(
922
+ `Interval (e.g. 30s, 5m) [${job.interval ? `${job.interval}ms` : "none"}]: `
791
923
  );
792
- if (!confirm) {
793
- context.writer.writeInfo("Edit cancelled.");
794
- return;
924
+ if (newInterval == null) return cancelled();
925
+ if (newInterval) update.interval = newInterval;
926
+ }
927
+ const newRetries = await ask(`Max retries [${job.maxRetries}]: `);
928
+ if (newRetries == null) return cancelled();
929
+ if (newRetries) {
930
+ const parsed = parseInt(newRetries, 10);
931
+ if (!isNaN(parsed)) {
932
+ update.maxRetries = parsed;
795
933
  }
796
- await service.updateJob(job.id, update);
797
- context.writer.writeSuccess(
798
- `Job "${job.name}" updated successfully.`
799
- );
934
+ }
935
+ const newTimeout = await ask(
936
+ `Timeout (e.g. 5m, 1h) [${job.timeout ? `${job.timeout}ms` : "none"}]: `
937
+ );
938
+ if (newTimeout == null) return cancelled();
939
+ if (newTimeout) update.timeout = newTimeout;
940
+ const newOverlap = await ask(
941
+ `Overlap policy (skip/queue/cancel) [${job.overlapPolicy}]: `
942
+ );
943
+ if (newOverlap == null) return cancelled();
944
+ if (newOverlap && ["skip", "queue", "cancel"].includes(newOverlap.toLowerCase())) {
945
+ update.overlapPolicy = newOverlap.toLowerCase();
946
+ }
947
+ if (Object.keys(update).length === 0) {
948
+ context.writer.writeInfo("No changes made.");
800
949
  return;
801
- } catch (e) {
802
- context.writer.writeError(
803
- `Error on server ${name}: ${e.message}`
804
- );
805
950
  }
951
+ const confirm = aborted() ? null : await context.reader.readConfirm("Apply changes?");
952
+ if (!confirm || aborted()) return cancelled();
953
+ await service.updateJob(job.id, update);
954
+ context.writer.writeSuccess(
955
+ `Job "${job.name}" updated successfully.`
956
+ );
957
+ } catch (e) {
958
+ context.writer.writeError(`Error on server ${name}: ${e.message}`);
806
959
  }
807
960
  }
808
961
  // ── Watch ────────────────────────────────────────────────────────
962
+ /**
963
+ * Stream job events until Ctrl+C (context.onAbort / context.signal) or
964
+ * until every watched server has closed its connection. On exit the
965
+ * sockets are closed and every subscription and listener is removed.
966
+ */
809
967
  async handleWatch(cmd, context) {
810
- const manager = context.services.get("cli-server-manager");
968
+ const manager = context.services.getRequired("cli-server-manager");
811
969
  if (!manager || !manager.connections) {
812
970
  context.writer.writeInfo("No connected servers.");
813
971
  return;
814
972
  }
815
973
  const targetServer = cmd.args["server"];
974
+ const targets = [];
975
+ for (const [serverName, connection] of manager.connections) {
976
+ if (targetServer && serverName !== targetServer) continue;
977
+ if (!connection.connected) continue;
978
+ targets.push([serverName, connection]);
979
+ }
980
+ if (targets.length === 0) {
981
+ context.writer.writeInfo(
982
+ targetServer ? `Server "${targetServer}" is not connected.` : "No connected servers."
983
+ );
984
+ return;
985
+ }
816
986
  context.writer.writeln(
817
987
  context.writer.wrapInColor(
818
988
  "Watching job events... (press Ctrl+C to stop)",
@@ -820,33 +990,60 @@ var CliJobsCommandProcessor = class {
820
990
  )
821
991
  );
822
992
  context.writer.writeln("");
823
- const sockets = [];
824
- const abortHandler = () => {
825
- for (const ws of sockets) {
826
- try {
827
- ws.close();
828
- } catch {
829
- }
993
+ const open = /* @__PURE__ */ new Set();
994
+ let stopped = false;
995
+ let reason = "aborted";
996
+ let resolveDone;
997
+ const done = new Promise((resolve) => resolveDone = resolve);
998
+ const closeSocket = (ws) => {
999
+ ws.onmessage = null;
1000
+ ws.onerror = null;
1001
+ ws.onclose = null;
1002
+ try {
1003
+ ws.close();
1004
+ } catch {
830
1005
  }
831
1006
  };
832
- context.onAbort.subscribe(abortHandler);
833
- for (const [serverName, connection] of manager.connections) {
834
- if (targetServer && serverName !== targetServer) continue;
835
- if (!connection.connected) continue;
1007
+ const onAbortSignal = () => stop("aborted");
1008
+ const abortSub = context.onAbort?.subscribe(onAbortSignal);
1009
+ context.signal?.addEventListener("abort", onAbortSignal);
1010
+ function stop(why) {
1011
+ if (stopped) return;
1012
+ stopped = true;
1013
+ reason = why;
1014
+ abortSub?.unsubscribe();
1015
+ context.signal?.removeEventListener("abort", onAbortSignal);
1016
+ for (const ws of open) closeSocket(ws);
1017
+ open.clear();
1018
+ resolveDone();
1019
+ }
1020
+ if (context.signal?.aborted) {
1021
+ stop("aborted");
1022
+ }
1023
+ const multiServer = targets.length > 1;
1024
+ for (const [serverName, connection] of targets) {
1025
+ if (stopped) break;
836
1026
  const config = connection.config;
837
1027
  const baseUrl = config.url.endsWith("/") ? config.url.slice(0, -1) : config.url;
838
- const wsUrl = baseUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:") + "/ws/v1/qcli/events";
839
1028
  try {
1029
+ const wsUrl = await cliCore.resolveServerWebSocketUrl(
1030
+ context.services,
1031
+ serverName,
1032
+ baseUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:") + "/ws/v1/qcli/events",
1033
+ config.headers
1034
+ );
1035
+ if (stopped) break;
840
1036
  const ws = new WebSocket(wsUrl);
841
- sockets.push(ws);
1037
+ open.add(ws);
842
1038
  ws.onmessage = (event) => {
1039
+ if (stopped) return;
843
1040
  try {
844
1041
  const data = JSON.parse(event.data);
845
1042
  if (!data.type || !data.type.startsWith("job:")) {
846
1043
  return;
847
1044
  }
848
1045
  const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
849
- const serverPrefix = manager.connections.size > 1 ? context.writer.wrapInColor(
1046
+ const serverPrefix = multiServer ? context.writer.wrapInColor(
850
1047
  `[${serverName}] `,
851
1048
  cliCore.CliForegroundColor.Cyan
852
1049
  ) : "";
@@ -874,11 +1071,19 @@ var CliJobsCommandProcessor = class {
874
1071
  }
875
1072
  };
876
1073
  ws.onerror = () => {
1074
+ if (stopped) return;
877
1075
  context.writer.writeError(
878
1076
  `WebSocket error on server ${serverName}`
879
1077
  );
880
1078
  };
881
1079
  ws.onclose = () => {
1080
+ if (stopped || !open.delete(ws)) return;
1081
+ context.writer.writeWarning(
1082
+ `Connection to server ${serverName} closed.`
1083
+ );
1084
+ if (open.size === 0) {
1085
+ stop("closed");
1086
+ }
882
1087
  };
883
1088
  } catch (e) {
884
1089
  context.writer.writeError(
@@ -886,24 +1091,23 @@ var CliJobsCommandProcessor = class {
886
1091
  );
887
1092
  }
888
1093
  }
889
- await new Promise((resolve) => {
890
- const sub = context.onAbort.subscribe(() => {
891
- sub.unsubscribe();
892
- resolve();
893
- });
894
- if (context.signal) {
895
- context.signal.addEventListener("abort", () => {
896
- sub.unsubscribe();
897
- resolve();
898
- });
899
- }
900
- });
1094
+ if (!stopped && open.size === 0) {
1095
+ stop("closed");
1096
+ }
1097
+ await done;
901
1098
  context.writer.writeln("");
902
- context.writer.writeInfo("Stopped watching job events.");
1099
+ context.writer.writeInfo(
1100
+ reason === "closed" ? "Stopped watching job events: no server connection left." : "Stopped watching job events."
1101
+ );
903
1102
  }
904
1103
  };
905
-
906
- // src/lib/completion/cli-job-name-completion-provider.ts
1104
+ var PAST_TENSE = {
1105
+ trigger: "triggered",
1106
+ pause: "paused",
1107
+ resume: "resumed",
1108
+ stop: "stopped",
1109
+ cancel: "cancelled"
1110
+ };
907
1111
  var JOB_NAME_SUBCOMMANDS = /* @__PURE__ */ new Set([
908
1112
  "info",
909
1113
  "trigger",
@@ -943,16 +1147,16 @@ var CliJobNameCompletionProvider = class {
943
1147
  }
944
1148
  const lowerPrefix = token.toLowerCase();
945
1149
  try {
946
- const manager = this.services.get("cli-server-manager");
1150
+ const manager = this.services.getRequired("cli-server-manager");
947
1151
  if (!manager?.connections) {
948
1152
  return [];
949
1153
  }
950
1154
  const names = [];
951
- for (const [, connection] of manager.connections) {
1155
+ for (const [name, connection] of manager.connections) {
952
1156
  if (!connection.connected) continue;
953
1157
  const config = connection.config;
954
1158
  const baseUrl = config.url.endsWith("/") ? config.url.slice(0, -1) : config.url;
955
- const headers = config.headers ?? {};
1159
+ const headers = cliCore.resolveServerHeaders(this.services, name, config.headers);
956
1160
  const service = new CliJobsService(baseUrl, headers);
957
1161
  try {
958
1162
  const jobs = await service.listJobs();
@@ -972,7 +1176,7 @@ var CliJobNameCompletionProvider = class {
972
1176
  };
973
1177
 
974
1178
  // src/lib/version.ts
975
- var LIBRARY_VERSION = "2.0.0-beta.9";
1179
+ var LIBRARY_VERSION = "2.0.0-rc.1";
976
1180
  var API_VERSION = 2;
977
1181
  var completionProvider = new CliJobNameCompletionProvider();
978
1182
  var jobsModule = {