@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.
- package/README.md +32 -5
- package/dist/agent/runner.d.ts +14 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +96 -3
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +37 -3
- package/dist/api/router.js.map +1 -1
- package/dist/api/share-router.d.ts.map +1 -1
- package/dist/api/share-router.js +24 -0
- package/dist/api/share-router.js.map +1 -1
- package/dist/cli/args.d.ts +34 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +55 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/app.d.ts +28 -0
- package/dist/cli/commands/app.d.ts.map +1 -0
- package/dist/cli/commands/app.js +411 -0
- package/dist/cli/commands/app.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +77 -16
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/service.d.ts.map +1 -1
- package/dist/cli/commands/service.js +373 -56
- package/dist/cli/commands/service.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +1 -17
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/http-client.d.ts +8 -0
- package/dist/cli/http-client.d.ts.map +1 -1
- package/dist/cli/http-client.js +5 -2
- package/dist/cli/http-client.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +16 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prompt.d.ts +17 -2
- package/dist/cli/prompt.d.ts.map +1 -1
- package/dist/cli/prompt.js +33 -2
- package/dist/cli/prompt.js.map +1 -1
- package/dist/config/loader.d.ts +8 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +15 -4
- package/dist/config/loader.js.map +1 -1
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +12 -0
- package/dist/config/watcher.js.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/session/store.d.ts +3 -2
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +1 -1
- package/dist/session/store.js.map +1 -1
- package/dist/share/session-video-catalog.d.ts +19 -0
- package/dist/share/session-video-catalog.d.ts.map +1 -0
- package/dist/share/session-video-catalog.js +119 -0
- package/dist/share/session-video-catalog.js.map +1 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/mcp/instructions.ts +14 -1
- package/mcp/server.ts +11 -5
- package/mcp/tools/image/module.ts +8 -71
- package/mcp/tools/shared/media.ts +76 -0
- package/mcp/tools/telegram/receiver-server.ts +15 -8
- package/mcp/tools/telegram/reply-attachment.ts +81 -0
- package/mcp/tools/video/module.ts +681 -0
- 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
|
-
/**
|
|
73
|
-
* started) but `/health` never
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
|
472
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
896
|
-
* so it can't silently pick a different manager
|
|
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' &&
|
|
985
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|