@0xmaxma/claude-gateway 1.8.8 → 1.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.
Files changed (66) hide show
  1. package/README.md +32 -5
  2. package/dist/agent/runner.d.ts +14 -1
  3. package/dist/agent/runner.d.ts.map +1 -1
  4. package/dist/agent/runner.js +96 -3
  5. package/dist/agent/runner.js.map +1 -1
  6. package/dist/api/router.d.ts.map +1 -1
  7. package/dist/api/router.js +37 -3
  8. package/dist/api/router.js.map +1 -1
  9. package/dist/api/share-router.d.ts.map +1 -1
  10. package/dist/api/share-router.js +24 -0
  11. package/dist/api/share-router.js.map +1 -1
  12. package/dist/cli/args.d.ts +34 -0
  13. package/dist/cli/args.d.ts.map +1 -1
  14. package/dist/cli/args.js +55 -0
  15. package/dist/cli/args.js.map +1 -1
  16. package/dist/cli/commands/app.d.ts +28 -0
  17. package/dist/cli/commands/app.d.ts.map +1 -0
  18. package/dist/cli/commands/app.js +411 -0
  19. package/dist/cli/commands/app.js.map +1 -0
  20. package/dist/cli/commands/doctor.d.ts.map +1 -1
  21. package/dist/cli/commands/doctor.js +77 -16
  22. package/dist/cli/commands/doctor.js.map +1 -1
  23. package/dist/cli/commands/service.d.ts.map +1 -1
  24. package/dist/cli/commands/service.js +373 -56
  25. package/dist/cli/commands/service.js.map +1 -1
  26. package/dist/cli/commands/update.d.ts.map +1 -1
  27. package/dist/cli/commands/update.js +1 -17
  28. package/dist/cli/commands/update.js.map +1 -1
  29. package/dist/cli/http-client.d.ts +8 -0
  30. package/dist/cli/http-client.d.ts.map +1 -1
  31. package/dist/cli/http-client.js +5 -2
  32. package/dist/cli/http-client.js.map +1 -1
  33. package/dist/cli/index.d.ts.map +1 -1
  34. package/dist/cli/index.js +16 -4
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/prompt.d.ts +17 -2
  37. package/dist/cli/prompt.d.ts.map +1 -1
  38. package/dist/cli/prompt.js +33 -2
  39. package/dist/cli/prompt.js.map +1 -1
  40. package/dist/config/loader.d.ts +8 -0
  41. package/dist/config/loader.d.ts.map +1 -1
  42. package/dist/config/loader.js +15 -4
  43. package/dist/config/loader.js.map +1 -1
  44. package/dist/config/watcher.d.ts.map +1 -1
  45. package/dist/config/watcher.js +12 -0
  46. package/dist/config/watcher.js.map +1 -1
  47. package/dist/index.js +8 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/session/store.d.ts +3 -2
  50. package/dist/session/store.d.ts.map +1 -1
  51. package/dist/session/store.js +1 -1
  52. package/dist/session/store.js.map +1 -1
  53. package/dist/share/session-video-catalog.d.ts +19 -0
  54. package/dist/share/session-video-catalog.d.ts.map +1 -0
  55. package/dist/share/session-video-catalog.js +119 -0
  56. package/dist/share/session-video-catalog.js.map +1 -0
  57. package/dist/types.d.ts +25 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/mcp/instructions.ts +14 -1
  60. package/mcp/server.ts +11 -5
  61. package/mcp/tools/image/module.ts +8 -71
  62. package/mcp/tools/shared/media.ts +76 -0
  63. package/mcp/tools/telegram/receiver-server.ts +15 -8
  64. package/mcp/tools/telegram/reply-attachment.ts +81 -0
  65. package/mcp/tools/video/module.ts +681 -0
  66. package/package.json +1 -1
@@ -42,6 +42,7 @@ const child_process_1 = require("child_process");
42
42
  const fs = __importStar(require("fs"));
43
43
  const os = __importStar(require("os"));
44
44
  const path = __importStar(require("path"));
45
+ const args_1 = require("../args");
45
46
  const http_client_1 = require("../http-client");
46
47
  const prompt_1 = require("../prompt");
47
48
  const health_1 = require("../health");
@@ -69,16 +70,42 @@ const UNIT_NAME = 'claude-gateway.service';
69
70
  const PM2_NAME = 'gateway';
70
71
  const HEALTH_ATTEMPTS = 20;
71
72
  const HEALTH_INTERVAL_MS = 500;
72
- /** `install`'s exit code when everything succeeded (unit written, enabled/
73
- * started) but `/health` never answered within the poll window — distinct
74
- * from `1` (install/enable itself failed, or a validation/confirmation gate
75
- * refused) so a caller checking the exit code alone, not just the JSON
76
- * result on stdout, can tell "didn't happen" apart from "happened, health
77
- * unconfirmed". */
73
+ /** Exit code for "the manager-level action succeeded (unit written and
74
+ * enabled/started, or start/restart itself succeeded) but `/health` never
75
+ * answered within the poll window" — shared by `install`, `start`, and
76
+ * `restart` (both managers). Distinct from `1` (the action itself failed, or
77
+ * a validation/confirmation gate refused) so a caller checking the exit code
78
+ * alone, not just the JSON result on stdout, can tell "didn't happen" apart
79
+ * from "happened, health unconfirmed". */
78
80
  const EXIT_HEALTH_TIMEOUT = 2;
79
81
  /** Env var names the installer itself sets — `--env` may not override these. */
80
82
  const RESERVED_ENV_KEYS = new Set(['HOME', 'PATH', 'GATEWAY_CONFIG']);
81
83
  const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
84
+ /** Every flag `service` accepts, across all six actions. Anything else is a
85
+ * typo and is reported rather than dropped: this command generates a unit
86
+ * file out of named flags, so a mistyped `--env-fil secrets.env` would write
87
+ * a unit with no EnvironmentFile= at all, install it, and exit 0 — the
88
+ * service then starts without the secrets it was meant to have. Same rule
89
+ * (and same reasoning) as `runResourceCommand` in ../index.ts.
90
+ *
91
+ * Action-specific flags are validated for *applicability* further down
92
+ * (`--print` outside `install`, the systemd-only set under `--manager pm2`);
93
+ * this set only answers "is this a flag `service` knows at all". */
94
+ const SERVICE_FLAG_NAMES = new Set([
95
+ 'help',
96
+ 'json',
97
+ 'yes',
98
+ 'print',
99
+ 'force',
100
+ 'url',
101
+ 'config',
102
+ 'manager',
103
+ 'scope',
104
+ 'after',
105
+ 'env',
106
+ 'env-file',
107
+ 'run-as',
108
+ ]);
82
109
  function gatewayHome(home = os.homedir()) {
83
110
  return path.join(home, '.claude-gateway');
84
111
  }
@@ -315,27 +342,6 @@ function capture(file, args) {
315
342
  function isMissingBinary(err) {
316
343
  return err?.code === 'ENOENT';
317
344
  }
318
- /**
319
- * Ask before changing service state — installing one, or stopping and removing
320
- * one. `--yes` skips the prompt; a non-interactive stdin without `--yes`
321
- * refuses rather than blocking forever, so this is safe in scripts and CI.
322
- */
323
- async function confirm(flags, action, question) {
324
- if (flags.yes === true)
325
- return true;
326
- if (!process.stdin.isTTY) {
327
- process.stderr.write(`Refusing to ${action} non-interactively without --yes.\n`);
328
- return false;
329
- }
330
- const rl = (0, prompt_1.createRl)();
331
- try {
332
- const answer = (await (0, prompt_1.ask)(rl, `${question} (y/N): `)).trim().toLowerCase();
333
- return answer === 'y' || answer === 'yes';
334
- }
335
- finally {
336
- rl.close();
337
- }
338
- }
339
345
  /** Poll /health so `service install` reports whether the service actually came
340
346
  * up, instead of only whether the manager accepted the unit.
341
347
  *
@@ -468,25 +474,11 @@ function parseAfterTargets(flags) {
468
474
  * producing a unit that starts the wrong binary. Never for secrets — this
469
475
  * text is written straight into the unit file; point at --env-file instead. */
470
476
  function parseExtraEnv(flags) {
471
- const raw = flags.env;
472
- if (raw === undefined)
473
- return {};
474
- if (typeof raw !== 'string' || raw.trim() === '') {
475
- process.stderr.write('--env requires a comma-separated list of KEY=VALUE pairs.\n');
477
+ const pairs = (0, args_1.parseKeyValueList)('env', flags.env, 'KEY=VALUE');
478
+ if (pairs === null)
476
479
  return null;
477
- }
478
480
  const out = {};
479
- for (const pair of raw.split(',')) {
480
- const trimmed = pair.trim();
481
- if (!trimmed)
482
- continue;
483
- const eq = trimmed.indexOf('=');
484
- if (eq <= 0) {
485
- process.stderr.write(`Invalid --env entry "${trimmed}" — expected KEY=VALUE.\n`);
486
- return null;
487
- }
488
- const key = trimmed.slice(0, eq);
489
- const value = trimmed.slice(eq + 1);
481
+ for (const { key, value } of pairs) {
490
482
  if (!ENV_KEY_RE.test(key)) {
491
483
  process.stderr.write(`Invalid --env key "${key}" — must match [A-Za-z_][A-Za-z0-9_]*.\n`);
492
484
  return null;
@@ -639,7 +631,7 @@ async function systemdInstall(flags, config, scope) {
639
631
  const confirmQuestion = scope === 'system'
640
632
  ? `Install and start ${UNIT_NAME} at system scope, running as ${unitOpts.runAs}?`
641
633
  : `Install and start ${UNIT_NAME} for user ${os.userInfo().username}?`;
642
- if (!(await confirm(flags, 'install', confirmQuestion))) {
634
+ if (!(await (0, prompt_1.confirmAction)(flags, 'install', confirmQuestion))) {
643
635
  process.stderr.write('Aborted — nothing was written.\n');
644
636
  return 1;
645
637
  }
@@ -745,7 +737,7 @@ async function systemdUninstall(flags, scope) {
745
737
  return 0;
746
738
  }
747
739
  // `disable --now` stops a running gateway, so this asks like install does.
748
- if (!(await confirm(flags, 'uninstall', `Stop and remove ${UNIT_NAME}?`))) {
740
+ if (!(await (0, prompt_1.confirmAction)(flags, 'uninstall', `Stop and remove ${UNIT_NAME}?`))) {
749
741
  process.stderr.write('Aborted — the service was left in place.\n');
750
742
  return 1;
751
743
  }
@@ -781,10 +773,162 @@ async function systemdUninstall(flags, scope) {
781
773
  }
782
774
  return 0;
783
775
  }
776
+ /**
777
+ * `service start|stop|restart` act on the unit selected by `--manager`/`--scope`
778
+ * exactly like `install`/`status`/`uninstall` do — discovered from disk
779
+ * (`systemdState().installed`), not from whether it is currently active. That
780
+ * distinction is the whole point of these commands: `gateway restart`/`stop`
781
+ * (src/cli/commands/gateway.ts) only find a manager that `detectManager()`
782
+ * sees as ACTIVE right now, so they can never start an installed-but-stopped
783
+ * unit, and could otherwise be fooled by an unrelated foreground process. These
784
+ * act on the specific installed service the caller named (or the one auto-
785
+ * detected the same way `status`/`uninstall` already do), never a foreground
786
+ * process.
787
+ */
788
+ async function systemdStart(flags, config, scope) {
789
+ if (scope === 'system' && !isRoot()) {
790
+ process.stderr.write(`--scope system must be run as root — it starts ${unitPath('system')}.\n`);
791
+ return 1;
792
+ }
793
+ const scopeArgs = scopeCliArgs(scope);
794
+ const before = systemdState(scope);
795
+ if (!before.installed) {
796
+ (0, output_1.printJson)({ manager: systemdManagerLabel(scope), unit: unitPath(scope), ...before }, flags);
797
+ process.stderr.write(`${UNIT_NAME} is not installed at ${scope} scope — run \`service install\` first.\n`);
798
+ return 1;
799
+ }
800
+ if (before.active) {
801
+ // Already active is still a `start` that has to answer "is it up?" — a
802
+ // wedged service reported active by systemd but answering nothing is
803
+ // exactly the state this exit code exists for. Reporting 0 with no
804
+ // `health` field at all would make the no-op path the one case where
805
+ // `start` says less than it knows, and a caller checking the exit code
806
+ // could not tell it apart from a healthy service.
807
+ const healthy = await waitForHealth(config, flags);
808
+ (0, output_1.printJson)({ manager: systemdManagerLabel(scope), unit: unitPath(scope), ...before, health: healthy ? 'up' : 'down' }, flags);
809
+ process.stderr.write(`${UNIT_NAME} is already active.\n`);
810
+ if (!healthy) {
811
+ process.stderr.write(`Service did not answer /health yet — check: journalctl ${scopeHintPrefix(scope)}-u ${UNIT_NAME} -n 50 --no-pager\n`);
812
+ return EXIT_HEALTH_TIMEOUT;
813
+ }
814
+ return 0;
815
+ }
816
+ try {
817
+ run('systemctl', [...scopeArgs, 'start', UNIT_NAME]);
818
+ }
819
+ catch (err) {
820
+ process.stderr.write(`Could not start ${UNIT_NAME}: ${err.message}\n` +
821
+ `Check: journalctl ${scopeHintPrefix(scope)}-u ${UNIT_NAME} -n 50 --no-pager\n`);
822
+ return 1;
823
+ }
824
+ const healthy = await waitForHealth(config, flags);
825
+ const state = systemdState(scope);
826
+ (0, output_1.printJson)({ manager: systemdManagerLabel(scope), unit: unitPath(scope), ...state, health: healthy ? 'up' : 'down' }, flags);
827
+ if (!state.active) {
828
+ process.stderr.write(`${UNIT_NAME} did not become active — check: systemctl ${scopeHintPrefix(scope)}status ${UNIT_NAME} --no-pager\n`);
829
+ return 1;
830
+ }
831
+ if (!healthy) {
832
+ process.stderr.write(`Service did not answer /health yet — check: journalctl ${scopeHintPrefix(scope)}-u ${UNIT_NAME} -n 50 --no-pager\n`);
833
+ return EXIT_HEALTH_TIMEOUT;
834
+ }
835
+ return 0;
836
+ }
837
+ /** Stopping an already-absent or already-inactive unit is treated as success —
838
+ * same idempotence convention as `uninstall` ("nothing to remove" → 0) —
839
+ * because the caller's goal (the service is not running) is already true. */
840
+ async function systemdStop(flags, scope) {
841
+ if (scope === 'system' && !isRoot()) {
842
+ process.stderr.write(`--scope system must be run as root — it stops ${unitPath('system')}.\n`);
843
+ return 1;
844
+ }
845
+ const scopeArgs = scopeCliArgs(scope);
846
+ const before = systemdState(scope);
847
+ if (!before.installed) {
848
+ (0, output_1.printJson)({ manager: systemdManagerLabel(scope), unit: unitPath(scope), ...before }, flags);
849
+ process.stderr.write(`${UNIT_NAME} is not installed at ${scope} scope — nothing to stop.\n`);
850
+ return 0;
851
+ }
852
+ if (!before.active) {
853
+ (0, output_1.printJson)({ manager: systemdManagerLabel(scope), unit: unitPath(scope), ...before }, flags);
854
+ process.stderr.write(`${UNIT_NAME} is already inactive.\n`);
855
+ return 0;
856
+ }
857
+ try {
858
+ run('systemctl', [...scopeArgs, 'stop', UNIT_NAME]);
859
+ }
860
+ catch (err) {
861
+ process.stderr.write(`Could not stop ${UNIT_NAME}: ${err.message}\n` +
862
+ `Check: systemctl ${scopeHintPrefix(scope)}status ${UNIT_NAME} --no-pager\n`);
863
+ return 1;
864
+ }
865
+ const state = systemdState(scope);
866
+ (0, output_1.printJson)({ manager: systemdManagerLabel(scope), unit: unitPath(scope), ...state }, flags);
867
+ if (state.active) {
868
+ process.stderr.write(`${UNIT_NAME} is still active — check: systemctl ${scopeHintPrefix(scope)}status ${UNIT_NAME} --no-pager\n`);
869
+ return 1;
870
+ }
871
+ return 0;
872
+ }
873
+ async function systemdRestart(flags, config, scope) {
874
+ if (scope === 'system' && !isRoot()) {
875
+ process.stderr.write(`--scope system must be run as root — it restarts ${unitPath('system')}.\n`);
876
+ return 1;
877
+ }
878
+ const scopeArgs = scopeCliArgs(scope);
879
+ const before = systemdState(scope);
880
+ if (!before.installed) {
881
+ (0, output_1.printJson)({ manager: systemdManagerLabel(scope), unit: unitPath(scope), ...before }, flags);
882
+ process.stderr.write(`${UNIT_NAME} is not installed at ${scope} scope — run \`service install\` first.\n`);
883
+ return 1;
884
+ }
885
+ try {
886
+ // `systemctl restart` on an inactive unit starts it — same as `start` —
887
+ // so this covers "restart a stopped service" too, with no separate branch.
888
+ run('systemctl', [...scopeArgs, 'restart', UNIT_NAME]);
889
+ }
890
+ catch (err) {
891
+ process.stderr.write(`Could not restart ${UNIT_NAME}: ${err.message}\n` +
892
+ `Check: journalctl ${scopeHintPrefix(scope)}-u ${UNIT_NAME} -n 50 --no-pager\n`);
893
+ return 1;
894
+ }
895
+ const healthy = await waitForHealth(config, flags);
896
+ const state = systemdState(scope);
897
+ (0, output_1.printJson)({ manager: systemdManagerLabel(scope), unit: unitPath(scope), ...state, health: healthy ? 'up' : 'down' }, flags);
898
+ if (!state.active) {
899
+ process.stderr.write(`${UNIT_NAME} did not become active — check: systemctl ${scopeHintPrefix(scope)}status ${UNIT_NAME} --no-pager\n`);
900
+ return 1;
901
+ }
902
+ if (!healthy) {
903
+ process.stderr.write(`Service did not answer /health yet — check: journalctl ${scopeHintPrefix(scope)}-u ${UNIT_NAME} -n 50 --no-pager\n`);
904
+ return EXIT_HEALTH_TIMEOUT;
905
+ }
906
+ return 0;
907
+ }
784
908
  function pm2Entry() {
785
909
  const list = JSON.parse(capture('pm2', ['jlist']));
786
910
  return list.find((item) => item.name === PM2_NAME);
787
911
  }
912
+ /** The state PM2 reports *after* an action already succeeded.
913
+ *
914
+ * `pm2Entry()` throws when `pm2 jlist` exits non-zero or prints something that
915
+ * isn't JSON — and a re-read that fails is not the action failing: `pm2 start`
916
+ * already returned 0. Letting it throw sends the exception all the way to
917
+ * `runCli`'s catch, which prints `Error: Unexpected token …`, exits 1, and
918
+ * writes nothing at all to stdout — reporting a start that genuinely worked as
919
+ * a failure, with no JSON for `--json` callers to read. `pm2Uninstall()`
920
+ * already guards its own re-read for this reason; these are the same case.
921
+ *
922
+ * `unread` is kept distinct from "absent": one means PM2 says the process is
923
+ * gone, the other means PM2 said nothing usable. */
924
+ function pm2StateAfterAction() {
925
+ try {
926
+ return { entry: pm2Entry(), unread: false };
927
+ }
928
+ catch {
929
+ return { unread: true };
930
+ }
931
+ }
788
932
  function pm2Status(flags) {
789
933
  let entry;
790
934
  try {
@@ -808,7 +952,7 @@ async function pm2Install(flags, config) {
808
952
  process.stderr.write(`\nWould run:\n pm2 ${args.join(' ')}\n pm2 save\n`);
809
953
  if (flags.print === true)
810
954
  return 0;
811
- if (!(await confirm(flags, 'install', `Register and start the PM2 process "${PM2_NAME}"?`))) {
955
+ if (!(await (0, prompt_1.confirmAction)(flags, 'install', `Register and start the PM2 process "${PM2_NAME}"?`))) {
812
956
  process.stderr.write('Aborted — nothing was registered.\n');
813
957
  return 1;
814
958
  }
@@ -858,7 +1002,7 @@ async function pm2Uninstall(flags) {
858
1002
  return 0;
859
1003
  }
860
1004
  // `pm2 delete` stops a running gateway, so this asks like install does.
861
- if (!(await confirm(flags, 'uninstall', `Stop and remove the PM2 process "${PM2_NAME}"?`))) {
1005
+ if (!(await (0, prompt_1.confirmAction)(flags, 'uninstall', `Stop and remove the PM2 process "${PM2_NAME}"?`))) {
862
1006
  process.stderr.write('Aborted — the process was left in place.\n');
863
1007
  return 1;
864
1008
  }
@@ -888,12 +1032,156 @@ async function pm2Uninstall(flags) {
888
1032
  (0, output_1.printJson)({ manager: 'pm2', name: PM2_NAME, installed: !!entry, active: entry?.pm2_env?.status === 'online' }, flags);
889
1033
  return entry ? 1 : 0;
890
1034
  }
1035
+ async function pm2Start(flags, config) {
1036
+ let entry;
1037
+ try {
1038
+ entry = pm2Entry();
1039
+ }
1040
+ catch (err) {
1041
+ process.stderr.write(isMissingBinary(err)
1042
+ ? 'PM2 is not installed or not on PATH.\n'
1043
+ : 'Could not read the PM2 process list. Is PM2 running?\n');
1044
+ return 1;
1045
+ }
1046
+ if (!entry) {
1047
+ (0, output_1.printJson)({ manager: 'pm2', name: PM2_NAME, installed: false, active: false }, flags);
1048
+ process.stderr.write(`No PM2 process named "${PM2_NAME}" — run \`service install --manager pm2\` first.\n`);
1049
+ return 1;
1050
+ }
1051
+ if (entry.pm2_env?.status === 'online') {
1052
+ // Same reasoning as systemdStart()'s already-active path: "PM2 says
1053
+ // online" is not "the gateway answers", and this is still a `start`.
1054
+ const healthy = await waitForHealth(config, flags);
1055
+ (0, output_1.printJson)({ manager: 'pm2', name: PM2_NAME, installed: true, active: true, status: entry.pm2_env.status, health: healthy ? 'up' : 'down' }, flags);
1056
+ process.stderr.write(`${PM2_NAME} is already online.\n`);
1057
+ if (!healthy) {
1058
+ process.stderr.write(`Service did not answer /health yet — check: pm2 logs ${PM2_NAME}\n`);
1059
+ return EXIT_HEALTH_TIMEOUT;
1060
+ }
1061
+ return 0;
1062
+ }
1063
+ try {
1064
+ run('pm2', ['start', PM2_NAME]);
1065
+ }
1066
+ catch (err) {
1067
+ process.stderr.write(`Could not start the PM2 process "${PM2_NAME}": ${err.message}\nCheck: pm2 logs ${PM2_NAME}\n`);
1068
+ return 1;
1069
+ }
1070
+ const healthy = await waitForHealth(config, flags);
1071
+ const after = pm2StateAfterAction();
1072
+ const status = after.unread ? 'unknown' : after.entry?.pm2_env?.status ?? 'absent';
1073
+ (0, output_1.printJson)({ manager: 'pm2', name: PM2_NAME, installed: after.unread ? true : !!after.entry, active: status === 'online', status, health: healthy ? 'up' : 'down' }, flags);
1074
+ if (after.unread) {
1075
+ // The start itself succeeded; only the confirmation read did not. Health is
1076
+ // the stronger signal anyway, so fall through to it rather than calling a
1077
+ // working service failed.
1078
+ process.stderr.write(`Started ${PM2_NAME}, but could not re-read the PM2 process list to confirm it — reporting /health only.\n`);
1079
+ }
1080
+ else if (status !== 'online') {
1081
+ process.stderr.write(`${PM2_NAME} did not come online — check: pm2 logs ${PM2_NAME}\n`);
1082
+ return 1;
1083
+ }
1084
+ if (!healthy) {
1085
+ process.stderr.write(`Service did not answer /health yet — check: pm2 logs ${PM2_NAME}\n`);
1086
+ return EXIT_HEALTH_TIMEOUT;
1087
+ }
1088
+ return 0;
1089
+ }
1090
+ /** Same idempotence convention as `systemdStop()` — an absent or already-
1091
+ * stopped process is success, not an error, because the goal is already true. */
1092
+ async function pm2Stop(flags) {
1093
+ let entry;
1094
+ try {
1095
+ entry = pm2Entry();
1096
+ }
1097
+ catch (err) {
1098
+ if (isMissingBinary(err)) {
1099
+ process.stderr.write('PM2 is not installed — nothing to stop.\n');
1100
+ return 0;
1101
+ }
1102
+ process.stderr.write('Could not read the PM2 process list. Is PM2 running?\n');
1103
+ return 1;
1104
+ }
1105
+ if (!entry) {
1106
+ (0, output_1.printJson)({ manager: 'pm2', name: PM2_NAME, installed: false, active: false }, flags);
1107
+ process.stderr.write(`No PM2 process named "${PM2_NAME}" — nothing to stop.\n`);
1108
+ return 0;
1109
+ }
1110
+ if (entry.pm2_env?.status !== 'online') {
1111
+ (0, output_1.printJson)({ manager: 'pm2', name: PM2_NAME, installed: true, active: false, status: entry.pm2_env?.status }, flags);
1112
+ process.stderr.write(`${PM2_NAME} is already stopped.\n`);
1113
+ return 0;
1114
+ }
1115
+ try {
1116
+ run('pm2', ['stop', PM2_NAME]);
1117
+ }
1118
+ catch (err) {
1119
+ process.stderr.write(`Could not stop the PM2 process "${PM2_NAME}": ${err.message}\nCheck: pm2 logs ${PM2_NAME}\n`);
1120
+ return 1;
1121
+ }
1122
+ const after = pm2StateAfterAction();
1123
+ const status = after.unread ? 'unknown' : after.entry?.pm2_env?.status ?? 'absent';
1124
+ (0, output_1.printJson)({ manager: 'pm2', name: PM2_NAME, installed: after.unread ? true : !!after.entry, active: status === 'online', status }, flags);
1125
+ if (after.unread) {
1126
+ // `pm2 stop` returned 0 — only the confirmation read failed, which is not
1127
+ // grounds for reporting a stop that worked as a failure.
1128
+ process.stderr.write(`Stopped ${PM2_NAME}, but could not re-read the PM2 process list to confirm it.\n`);
1129
+ return 0;
1130
+ }
1131
+ if (status === 'online') {
1132
+ process.stderr.write(`${PM2_NAME} is still online — check: pm2 logs ${PM2_NAME}\n`);
1133
+ return 1;
1134
+ }
1135
+ return 0;
1136
+ }
1137
+ async function pm2Restart(flags, config) {
1138
+ let entry;
1139
+ try {
1140
+ entry = pm2Entry();
1141
+ }
1142
+ catch (err) {
1143
+ process.stderr.write(isMissingBinary(err)
1144
+ ? 'PM2 is not installed or not on PATH.\n'
1145
+ : 'Could not read the PM2 process list. Is PM2 running?\n');
1146
+ return 1;
1147
+ }
1148
+ if (!entry) {
1149
+ (0, output_1.printJson)({ manager: 'pm2', name: PM2_NAME, installed: false, active: false }, flags);
1150
+ process.stderr.write(`No PM2 process named "${PM2_NAME}" — run \`service install --manager pm2\` first.\n`);
1151
+ return 1;
1152
+ }
1153
+ try {
1154
+ // `pm2 restart` starts an already-stopped process too, same as systemd.
1155
+ run('pm2', ['restart', PM2_NAME]);
1156
+ }
1157
+ catch (err) {
1158
+ process.stderr.write(`Could not restart the PM2 process "${PM2_NAME}": ${err.message}\nCheck: pm2 logs ${PM2_NAME}\n`);
1159
+ return 1;
1160
+ }
1161
+ const healthy = await waitForHealth(config, flags);
1162
+ const after = pm2StateAfterAction();
1163
+ const status = after.unread ? 'unknown' : after.entry?.pm2_env?.status ?? 'absent';
1164
+ (0, output_1.printJson)({ manager: 'pm2', name: PM2_NAME, installed: after.unread ? true : !!after.entry, active: status === 'online', status, health: healthy ? 'up' : 'down' }, flags);
1165
+ if (after.unread) {
1166
+ process.stderr.write(`Restarted ${PM2_NAME}, but could not re-read the PM2 process list to confirm it — reporting /health only.\n`);
1167
+ }
1168
+ else if (status !== 'online') {
1169
+ process.stderr.write(`${PM2_NAME} did not come online — check: pm2 logs ${PM2_NAME}\n`);
1170
+ return 1;
1171
+ }
1172
+ if (!healthy) {
1173
+ process.stderr.write(`Service did not answer /health yet — check: pm2 logs ${PM2_NAME}\n`);
1174
+ return EXIT_HEALTH_TIMEOUT;
1175
+ }
1176
+ return 0;
1177
+ }
891
1178
  // ─── entry point ──────────────────────────────────────────────────────────────
892
- const USAGE_LINE = 'claude-gateway service <install|status|uninstall> [--manager systemd|pm2] [--scope user|system] [--run-as <user>] ' +
1179
+ const USAGE_LINE = 'claude-gateway service <install|status|uninstall|start|stop|restart> [--manager systemd|pm2] [--scope user|system] [--run-as <user>] ' +
893
1180
  '[--after <target,...>] [--env-file <path>] [--env KEY=VALUE,...] [--config <path>] [--yes] [--print] [--force]';
894
- /** Pick the manager to act on when `--manager` is omitted. `status`/`uninstall`
895
- * act on whatever is actually installed; `install` always defaults to systemd
896
- * so it can't silently pick a different manager than the one documented. */
1181
+ /** Pick the manager to act on when `--manager` is omitted. `status`/`uninstall`/
1182
+ * `start`/`stop`/`restart` act on whatever is actually installed; `install`
1183
+ * always defaults to systemd so it can't silently pick a different manager
1184
+ * than the one documented. */
897
1185
  function detectServiceManager(action, scope) {
898
1186
  if (action === 'install')
899
1187
  return 'systemd';
@@ -978,11 +1266,24 @@ async function runService(positionals, flags, config = {}) {
978
1266
  ' --scope system installs a root-owned unit in /etc/systemd/system instead —',
979
1267
  ' requires running as root already (never escalates via sudo) and --run-as <user>.',
980
1268
  ' --after, --env-file, and --env customize the generated unit further (systemd only).',
1269
+ ' start/stop/restart act on the installed service for the selected --manager/--scope,',
1270
+ ' discovering it even when inactive — unlike `gateway restart|stop`, which only drive',
1271
+ ' a manager currently reported as active.',
981
1272
  ]);
982
1273
  return flags.help === true ? 0 : 1;
983
1274
  }
984
- if (action !== 'install' && action !== 'status' && action !== 'uninstall') {
985
- process.stderr.write(`Unknown: service ${action} (expected install|status|uninstall)\n`);
1275
+ if (action !== 'install' &&
1276
+ action !== 'status' &&
1277
+ action !== 'uninstall' &&
1278
+ action !== 'start' &&
1279
+ action !== 'stop' &&
1280
+ action !== 'restart') {
1281
+ process.stderr.write(`Unknown: service ${action} (expected install|status|uninstall|start|stop|restart)\n`);
1282
+ return 1;
1283
+ }
1284
+ const unknownFlags = (0, args_1.unknownFlagNames)(flags, SERVICE_FLAG_NAMES);
1285
+ if (unknownFlags.length) {
1286
+ process.stderr.write(`Unknown flag(s): ${unknownFlags.map((f) => `--${f}`).join(' ')}\n`);
986
1287
  return 1;
987
1288
  }
988
1289
  if (flags.print === true && action !== 'install') {
@@ -1014,10 +1315,26 @@ async function runService(positionals, flags, config = {}) {
1014
1315
  if (manager === 'systemd') {
1015
1316
  if (action === 'install')
1016
1317
  return systemdInstall(flags, config, scope);
1017
- return action === 'status' ? systemdStatus(flags, scope) : await systemdUninstall(flags, scope);
1318
+ if (action === 'status')
1319
+ return systemdStatus(flags, scope);
1320
+ if (action === 'uninstall')
1321
+ return await systemdUninstall(flags, scope);
1322
+ if (action === 'start')
1323
+ return await systemdStart(flags, config, scope);
1324
+ if (action === 'stop')
1325
+ return await systemdStop(flags, scope);
1326
+ return await systemdRestart(flags, config, scope);
1018
1327
  }
1019
1328
  if (action === 'install')
1020
1329
  return pm2Install(flags, config);
1021
- return action === 'status' ? pm2Status(flags) : await pm2Uninstall(flags);
1330
+ if (action === 'status')
1331
+ return pm2Status(flags);
1332
+ if (action === 'uninstall')
1333
+ return await pm2Uninstall(flags);
1334
+ if (action === 'start')
1335
+ return await pm2Start(flags, config);
1336
+ if (action === 'stop')
1337
+ return await pm2Stop(flags);
1338
+ return await pm2Restart(flags, config);
1022
1339
  }
1023
1340
  //# sourceMappingURL=service.js.map