nodus-wechat 0.5.1 → 0.6.0

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 CHANGED
@@ -14,12 +14,15 @@ npx nodus-wechat
14
14
  npx nodus-wechat setup
15
15
  npx nodus-wechat setup --install-hermes
16
16
  npx nodus-wechat install-hermes
17
+ npx nodus-wechat install-openilink
17
18
  npx nodus-wechat doctor
18
19
  npx nodus-wechat start
20
+ npx nodus-wechat start --docker
19
21
  npx nodus-wechat status
20
22
  npx nodus-wechat logs
21
23
  npx nodus-wechat stop
22
24
  npx nodus-wechat uninstall --yes
25
+ npx nodus-wechat clean --yes
23
26
  ```
24
27
 
25
28
  For a server-bound OpeniLink origin:
@@ -34,23 +37,45 @@ npx nodus-wechat setup \
34
37
  The default gateway base URL is `https://api.nodus.sbs/`.
35
38
  Use `--install-hermes` or `install-hermes` to run the official Hermes installer
36
39
  with `--skip-setup` and the same Hermes home used by this CLI.
40
+ Use `install-openilink` to install the native OpeniLink Hub CLI (`oih`) through
41
+ the official OpeniLink installer.
37
42
 
38
43
  ## Current behavior
39
44
 
40
45
  - Creates local configuration at `~/.nodus-wechat/config.json`.
41
46
  - Can install Hermes Agent CLI through the official NousResearch installer.
47
+ - Can install OpeniLink Hub native CLI through the official OpeniLink installer.
42
48
  - Installs the OpeniLink + webhook POC runtime at `~/.nodus-wechat/runtime`.
43
49
  - Writes Hermes common settings to `~/.hermes/config.yaml`.
44
50
  - Writes the AstraGate key to `~/.hermes/.env` as `ASTRAGATE_API_KEY`.
45
51
  - Writes runtime `.env`, Docker Compose, webhook server, helper scripts, and the OpeniLink reply plugin.
46
52
  - Stores gateway base URL, api key, model, Hermes paths, OpeniLink origin, webhook port, and runtime path.
47
- - Checks Node.js, local configuration, Hermes files, runtime files, Docker Compose availability, Hermes CLI availability, and WeChat app detection with `doctor`.
48
- - Starts/stops the local runtime through Docker Compose.
49
- - Removes only files created by this CLI with `uninstall --yes`.
53
+ - Checks Node.js, local configuration, Hermes files, runtime files, Python, OpeniLink CLI, optional Docker Compose availability, Hermes CLI availability, and WeChat app detection with `doctor`.
54
+ - Starts/stops the local runtime with native local processes by default.
55
+ - Keeps Docker Compose available only when `--docker` is passed.
56
+ - Removes Nodus WeChat config/runtime files with `uninstall --yes`.
57
+ - Cleans Nodus WeChat config/runtime plus generated Hermes settings with `clean --yes`.
58
+
59
+ ## Uninstall and clean
60
+
61
+ ```sh
62
+ npx nodus-wechat stop
63
+ npx nodus-wechat uninstall --yes
64
+ ```
65
+
66
+ Use `clean --yes` when you also want to remove the generated Hermes settings:
67
+
68
+ ```sh
69
+ npx nodus-wechat clean --yes
70
+ ```
71
+
72
+ `clean --yes` removes `~/.nodus-wechat`, removes `ASTRAGATE_API_KEY` from
73
+ `~/.hermes/.env`, and removes `~/.hermes/config.yaml` only when it still matches
74
+ the config generated by this CLI.
50
75
 
51
76
  ## Current non-goals
52
77
 
53
- - Does not run a third-party installer unless `--install-hermes` or `install-hermes` is explicitly requested.
78
+ - Does not run a third-party installer unless `--install-hermes`, `install-hermes`, or `install-openilink` is explicitly requested.
54
79
  - Does not automate, inject into, read, or control WeChat directly.
55
80
  - Does not start a daemon, LaunchAgent, or background worker outside Docker Compose.
56
81
  - Does not redeem real CDKs or mutate sub2api accounts; the bundled webhook keeps the existing dry-run POC boundary.
@@ -3,11 +3,12 @@
3
3
  "use strict";
4
4
 
5
5
  const fs = require("node:fs");
6
+ const http = require("node:http");
6
7
  const os = require("node:os");
7
8
  const path = require("node:path");
8
9
  const childProcess = require("node:child_process");
9
10
 
10
- const VERSION = "0.5.1";
11
+ const VERSION = "0.6.0";
11
12
  const DEFAULT_BASE_URL = "https://api.nodus.sbs/";
12
13
  const DEFAULT_MODEL = "gpt-5.5";
13
14
  const DEFAULT_OPENILINK_ORIGIN = "http://localhost:9800";
@@ -40,22 +41,27 @@ Usage:
40
41
  [--openilink-rp-id <id>] [--webhook-port <port>]
41
42
  [--webhook-token <token>] [--install-hermes]
42
43
  nodus-wechat install-hermes
44
+ nodus-wechat install-openilink
43
45
  nodus-wechat doctor
44
- nodus-wechat start
45
- nodus-wechat status
46
- nodus-wechat logs
47
- nodus-wechat stop
46
+ nodus-wechat start [--docker]
47
+ nodus-wechat status [--docker]
48
+ nodus-wechat logs [--docker]
49
+ nodus-wechat stop [--docker]
48
50
  nodus-wechat uninstall --yes
51
+ nodus-wechat clean --yes
49
52
 
50
53
  Commands:
51
54
  setup Create or update local configuration and runtime files. This is the default.
52
55
  install-hermes Install Hermes Agent CLI with the official installer.
56
+ install-openilink
57
+ Install OpeniLink Hub native CLI with the official installer.
53
58
  doctor Check local prerequisites and configuration.
54
- start Start the local OpeniLink + webhook runtime with Docker Compose.
55
- status Show Docker Compose service status.
56
- logs Follow webhook logs.
59
+ start Start OpeniLink + webhook with local processes by default.
60
+ status Show local process status.
61
+ logs Follow local runtime logs.
57
62
  stop Stop the local runtime.
58
- uninstall Remove files created by this CLI.
63
+ uninstall Remove Nodus WeChat config and runtime files.
64
+ clean Stop runtime if possible, then remove Nodus WeChat files and generated Hermes settings.
59
65
 
60
66
  This version installs an OpeniLink webhook POC runtime. It does not inject into,
61
67
  read, or control WeChat directly.`);
@@ -72,7 +78,7 @@ function parseArgs(argv) {
72
78
  }
73
79
 
74
80
  const key = item.slice(2);
75
- if (key === "help" || key === "yes" || key === "install-hermes") {
81
+ if (key === "help" || key === "yes" || key === "install-hermes" || key === "docker") {
76
82
  result[key] = true;
77
83
  continue;
78
84
  }
@@ -233,6 +239,13 @@ function hermesInstallCommand() {
233
239
  );
234
240
  }
235
241
 
242
+ function openiLinkInstallCommand() {
243
+ return (
244
+ process.env.NODUS_WECHAT_OPENILINK_INSTALL_COMMAND ||
245
+ "curl -fsSL https://raw.githubusercontent.com/openilink/openilink-hub/main/install.sh | sh"
246
+ );
247
+ }
248
+
236
249
  function runHermesInstaller(hermesDir) {
237
250
  const args = ["--skip-setup", "--hermes-home", hermesDir];
238
251
  const command = `${hermesInstallCommand()} ${args.map(shellQuote).join(" ")}`;
@@ -376,6 +389,21 @@ function installHermes() {
376
389
  return 0;
377
390
  }
378
391
 
392
+ function installOpeniLink() {
393
+ const result = childProcess.spawnSync(openiLinkInstallCommand(), {
394
+ shell: true,
395
+ stdio: "inherit",
396
+ });
397
+ if (result.error) {
398
+ throw result.error;
399
+ }
400
+ if (result.status !== 0) {
401
+ throw new Error(`OpeniLink installer failed with exit code ${result.status}`);
402
+ }
403
+ console.log("OpeniLink installer completed. Run `nodus-wechat start`.");
404
+ return 0;
405
+ }
406
+
379
407
  function doctor() {
380
408
  let ok = true;
381
409
  const major = Number.parseInt(process.versions.node.split(".")[0], 10);
@@ -415,11 +443,24 @@ function doctor() {
415
443
  ok = false;
416
444
  console.log(`runtime: missing (${config.runtime?.dir || path.join(configHome(), "runtime")})`);
417
445
  }
446
+ const python = commandPath("python3") || commandPath("python");
447
+ if (python) {
448
+ console.log(`python: ok (${python})`);
449
+ } else {
450
+ ok = false;
451
+ console.log("python: failed (needed for local webhook runtime)");
452
+ }
453
+ const oih = commandPath("oih");
454
+ if (oih) {
455
+ console.log(`openilink cli: ok (${oih})`);
456
+ } else {
457
+ console.log("openilink cli: missing (run `nodus-wechat install-openilink`)");
458
+ }
418
459
  const docker = dockerComposeAvailable();
419
460
  if (docker.ok) {
420
- console.log(`docker compose: ok (${docker.version})`);
461
+ console.log(`docker compose: ok (${docker.version}; optional with --docker)`);
421
462
  } else {
422
- console.log("docker compose: missing (needed for `nodus-wechat start`)");
463
+ console.log("docker compose: missing (optional; only needed for `--docker`)");
423
464
  }
424
465
  console.log(`openilink: ${config.openilink?.publicOrigin || DEFAULT_OPENILINK_ORIGIN}`);
425
466
  console.log(`webhook: http://127.0.0.1:${config.webhook?.port || DEFAULT_WEBHOOK_PORT}/health`);
@@ -467,6 +508,116 @@ function dockerComposeAvailable() {
467
508
  };
468
509
  }
469
510
 
511
+ function commandPath(name) {
512
+ const result = childProcess.spawnSync("/bin/sh", ["-c", `command -v ${shellQuote(name)}`], {
513
+ encoding: "utf8",
514
+ });
515
+ if (result.error || result.status !== 0) {
516
+ return null;
517
+ }
518
+ return result.stdout.trim() || null;
519
+ }
520
+
521
+ function runtimePath(config, name) {
522
+ return path.join(config.runtime.dir, name);
523
+ }
524
+
525
+ function pidPath(config, name) {
526
+ return runtimePath(config, `.nodus-${name}.pid`);
527
+ }
528
+
529
+ function logPath(config, name) {
530
+ return runtimePath(config, `${name}.log`);
531
+ }
532
+
533
+ function readPid(filePath) {
534
+ if (!fs.existsSync(filePath)) {
535
+ return null;
536
+ }
537
+ const pid = Number.parseInt(fs.readFileSync(filePath, "utf8"), 10);
538
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
539
+ }
540
+
541
+ function processRunning(pid) {
542
+ if (!pid) {
543
+ return false;
544
+ }
545
+ try {
546
+ process.kill(pid, 0);
547
+ return true;
548
+ } catch (_error) {
549
+ return false;
550
+ }
551
+ }
552
+
553
+ function startManagedProcess(config, name, command, args, env) {
554
+ const filePath = pidPath(config, name);
555
+ const existingPid = readPid(filePath);
556
+ if (processRunning(existingPid)) {
557
+ console.log(`${name}: already running (pid ${existingPid})`);
558
+ return 0;
559
+ }
560
+
561
+ fs.mkdirSync(config.runtime.dir, { recursive: true, mode: 0o700 });
562
+ const out = fs.openSync(logPath(config, name), "a");
563
+ const child = childProcess.spawn(command, args, {
564
+ cwd: config.runtime.dir,
565
+ env,
566
+ detached: true,
567
+ stdio: ["ignore", out, out],
568
+ });
569
+ child.unref();
570
+ fs.writeFileSync(filePath, `${child.pid}\n`, { mode: 0o600 });
571
+ console.log(`${name}: started (pid ${child.pid}, log ${logPath(config, name)})`);
572
+ return 0;
573
+ }
574
+
575
+ function stopManagedProcess(config, name) {
576
+ const filePath = pidPath(config, name);
577
+ const pid = readPid(filePath);
578
+ if (!processRunning(pid)) {
579
+ fs.rmSync(filePath, { force: true });
580
+ console.log(`${name}: stopped`);
581
+ return 0;
582
+ }
583
+
584
+ try {
585
+ process.kill(-pid, "SIGTERM");
586
+ } catch (_error) {
587
+ process.kill(pid, "SIGTERM");
588
+ }
589
+ fs.rmSync(filePath, { force: true });
590
+ console.log(`${name}: stopped (pid ${pid})`);
591
+ return 0;
592
+ }
593
+
594
+ function localRuntimeEnv(config) {
595
+ const env = parseDotEnv(path.join(config.runtime.dir, ".env"));
596
+ return {
597
+ ...process.env,
598
+ ...env,
599
+ LISTEN: `:${config.openilink?.port || DEFAULT_OPENILINK_PORT}`,
600
+ RP_ORIGIN: config.openilink?.publicOrigin || DEFAULT_OPENILINK_ORIGIN,
601
+ RP_ID: config.openilink?.rpId || DEFAULT_OPENILINK_RP_ID,
602
+ POC_WEBHOOK_BIND: config.webhook?.bind || "127.0.0.1",
603
+ POC_WEBHOOK_PORT: String(config.webhook?.port || DEFAULT_WEBHOOK_PORT),
604
+ };
605
+ }
606
+
607
+ function httpGet(url, timeoutMs = 1200) {
608
+ return new Promise((resolve) => {
609
+ const request = http.get(url, { timeout: timeoutMs }, (response) => {
610
+ response.resume();
611
+ response.on("end", () => resolve({ ok: response.statusCode >= 200 && response.statusCode < 500, statusCode: response.statusCode }));
612
+ });
613
+ request.on("timeout", () => {
614
+ request.destroy();
615
+ resolve({ ok: false });
616
+ });
617
+ request.on("error", () => resolve({ ok: false }));
618
+ });
619
+ }
620
+
470
621
  function loadRuntimeConfig() {
471
622
  if (!fs.existsSync(configPath())) {
472
623
  console.error(`Config missing: ${configPath()}`);
@@ -509,7 +660,7 @@ function runDockerCompose(config, args, stdio = "inherit") {
509
660
  return result.status || 0;
510
661
  }
511
662
 
512
- function start() {
663
+ function startDocker() {
513
664
  const config = loadRuntimeConfig();
514
665
  if (!config) {
515
666
  return 1;
@@ -518,7 +669,39 @@ function start() {
518
669
  return runDockerCompose(config, ["up", "-d"]);
519
670
  }
520
671
 
521
- function status() {
672
+ function startLocal() {
673
+ const config = loadRuntimeConfig();
674
+ if (!config) {
675
+ return 1;
676
+ }
677
+
678
+ const python = commandPath("python3") || commandPath("python");
679
+ if (!python) {
680
+ console.error("Python is required for the local webhook runtime.");
681
+ return 1;
682
+ }
683
+
684
+ const oih = commandPath("oih");
685
+ if (!oih) {
686
+ console.error("OpeniLink Hub CLI `oih` is not installed.");
687
+ console.error("Run: nodus-wechat install-openilink");
688
+ return 1;
689
+ }
690
+
691
+ const env = localRuntimeEnv(config);
692
+ const webhookPath = path.join(config.runtime.dir, "poc-webhook", "server.py");
693
+ startManagedProcess(config, "webhook", python, [webhookPath], env);
694
+ startManagedProcess(config, "openilink", oih, [], env);
695
+ console.log(`OpeniLink Hub: ${config.openilink?.publicOrigin || DEFAULT_OPENILINK_ORIGIN}`);
696
+ console.log(`Webhook health: http://127.0.0.1:${config.webhook?.port || DEFAULT_WEBHOOK_PORT}/health`);
697
+ return 0;
698
+ }
699
+
700
+ function start(options) {
701
+ return options.docker ? startDocker() : startLocal();
702
+ }
703
+
704
+ function statusDocker() {
522
705
  const config = loadRuntimeConfig();
523
706
  if (!config) {
524
707
  return 1;
@@ -527,7 +710,26 @@ function status() {
527
710
  return runDockerCompose(config, ["ps"]);
528
711
  }
529
712
 
530
- function logs() {
713
+ async function statusLocal() {
714
+ const config = loadRuntimeConfig();
715
+ if (!config) {
716
+ return 1;
717
+ }
718
+
719
+ for (const name of ["openilink", "webhook"]) {
720
+ const pid = readPid(pidPath(config, name));
721
+ console.log(`${name}: ${processRunning(pid) ? `running (pid ${pid})` : "stopped"}`);
722
+ }
723
+ const health = await httpGet(`http://127.0.0.1:${config.webhook?.port || DEFAULT_WEBHOOK_PORT}/health`);
724
+ console.log(`webhook health: ${health.ok ? `ok (${health.statusCode})` : "unreachable"}`);
725
+ return 0;
726
+ }
727
+
728
+ function status(options) {
729
+ return options.docker ? statusDocker() : statusLocal();
730
+ }
731
+
732
+ function logsDocker() {
531
733
  const config = loadRuntimeConfig();
532
734
  if (!config) {
533
735
  return 1;
@@ -536,7 +738,36 @@ function logs() {
536
738
  return runDockerCompose(config, ["logs", "-f", "poc-webhook"]);
537
739
  }
538
740
 
539
- function stop() {
741
+ function logsLocal() {
742
+ const config = loadRuntimeConfig();
743
+ if (!config) {
744
+ return 1;
745
+ }
746
+
747
+ const files = ["openilink", "webhook"].map((name) => logPath(config, name)).filter((filePath) => fs.existsSync(filePath));
748
+ if (files.length === 0) {
749
+ console.error("No local runtime logs found.");
750
+ return 1;
751
+ }
752
+
753
+ const tail = commandPath("tail");
754
+ if (!tail) {
755
+ for (const filePath of files) {
756
+ console.log(`==> ${filePath} <==`);
757
+ console.log(fs.readFileSync(filePath, "utf8"));
758
+ }
759
+ return 0;
760
+ }
761
+
762
+ const result = childProcess.spawnSync(tail, ["-f", ...files], { stdio: "inherit" });
763
+ return result.status || 0;
764
+ }
765
+
766
+ function logs(options) {
767
+ return options.docker ? logsDocker() : logsLocal();
768
+ }
769
+
770
+ function stopDocker() {
540
771
  const config = loadRuntimeConfig();
541
772
  if (!config) {
542
773
  return 1;
@@ -545,6 +776,21 @@ function stop() {
545
776
  return runDockerCompose(config, ["down"]);
546
777
  }
547
778
 
779
+ function stopLocal() {
780
+ const config = loadRuntimeConfig();
781
+ if (!config) {
782
+ return 1;
783
+ }
784
+
785
+ stopManagedProcess(config, "webhook");
786
+ stopManagedProcess(config, "openilink");
787
+ return 0;
788
+ }
789
+
790
+ function stop(options) {
791
+ return options.docker ? stopDocker() : stopLocal();
792
+ }
793
+
548
794
  function uninstall(options) {
549
795
  if (!options.yes) {
550
796
  console.error("Refusing to uninstall without --yes.");
@@ -556,7 +802,65 @@ function uninstall(options) {
556
802
  return 0;
557
803
  }
558
804
 
559
- function main() {
805
+ function removeHermesApiKey(envPath) {
806
+ if (!fs.existsSync(envPath)) {
807
+ return "missing";
808
+ }
809
+
810
+ const lines = fs.readFileSync(envPath, "utf8").split(/\r?\n/);
811
+ const kept = lines.filter((line) => line && !line.startsWith("ASTRAGATE_API_KEY="));
812
+ if (kept.length === 0) {
813
+ fs.rmSync(envPath, { force: true });
814
+ return "removed";
815
+ }
816
+
817
+ fs.writeFileSync(envPath, `${kept.join("\n")}\n`, { mode: 0o600 });
818
+ return "updated";
819
+ }
820
+
821
+ function removeGeneratedHermesConfig(config) {
822
+ const hermesConfigPath = config.hermes?.configPath || path.join(hermesHome(), "config.yaml");
823
+ if (!fs.existsSync(hermesConfigPath)) {
824
+ return "missing";
825
+ }
826
+
827
+ const current = fs.readFileSync(hermesConfigPath, "utf8");
828
+ if (current !== buildHermesConfig(config)) {
829
+ return "kept";
830
+ }
831
+
832
+ fs.rmSync(hermesConfigPath, { force: true });
833
+ return "removed";
834
+ }
835
+
836
+ function clean(options) {
837
+ if (!options.yes) {
838
+ console.error("Refusing to clean without --yes.");
839
+ return 1;
840
+ }
841
+
842
+ const config = fs.existsSync(configPath()) ? readConfig() : null;
843
+ if (config) {
844
+ for (const name of ["webhook", "openilink"]) {
845
+ stopManagedProcess({ ...config, runtime: { ...config.runtime, dir: config.runtime?.dir || path.join(configHome(), "runtime") } }, name);
846
+ }
847
+ const docker = dockerComposeAvailable();
848
+ if (docker.ok && config.runtime?.dir && fs.existsSync(path.join(config.runtime.dir, "docker-compose.yml"))) {
849
+ runDockerCompose(config, ["down"], "pipe");
850
+ }
851
+
852
+ const hermesConfigStatus = removeGeneratedHermesConfig(config);
853
+ const hermesEnvStatus = removeHermesApiKey(config.hermes?.envPath || path.join(hermesHome(), ".env"));
854
+ console.log(`Hermes config: ${hermesConfigStatus}`);
855
+ console.log(`Hermes env: ${hermesEnvStatus}`);
856
+ }
857
+
858
+ fs.rmSync(configHome(), { recursive: true, force: true });
859
+ console.log(`Removed: ${configHome()}`);
860
+ return 0;
861
+ }
862
+
863
+ async function main() {
560
864
  let args;
561
865
  try {
562
866
  args = parseArgs(process.argv.slice(2));
@@ -581,29 +885,37 @@ function main() {
581
885
  return installHermes();
582
886
  }
583
887
 
888
+ if (command === "install-openilink") {
889
+ return installOpeniLink();
890
+ }
891
+
584
892
  if (command === "doctor") {
585
893
  return doctor();
586
894
  }
587
895
 
588
896
  if (command === "start") {
589
- return start();
897
+ return start(args);
590
898
  }
591
899
 
592
900
  if (command === "status") {
593
- return status();
901
+ return await status(args);
594
902
  }
595
903
 
596
904
  if (command === "logs") {
597
- return logs();
905
+ return logs(args);
598
906
  }
599
907
 
600
908
  if (command === "stop") {
601
- return stop();
909
+ return stop(args);
602
910
  }
603
911
 
604
912
  if (command === "uninstall") {
605
913
  return uninstall(args);
606
914
  }
915
+
916
+ if (command === "clean") {
917
+ return clean(args);
918
+ }
607
919
  } catch (error) {
608
920
  console.error(error.message);
609
921
  return 1;
@@ -614,4 +926,11 @@ function main() {
614
926
  return 1;
615
927
  }
616
928
 
617
- process.exitCode = main();
929
+ main()
930
+ .then((code) => {
931
+ process.exitCode = code;
932
+ })
933
+ .catch((error) => {
934
+ console.error(error.message);
935
+ process.exitCode = 1;
936
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodus-wechat",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "CLI installer for Nodus WeChat, Hermes, and the local OpeniLink webhook runtime.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -115,4 +115,6 @@ class Handler(BaseHTTPRequestHandler):
115
115
 
116
116
 
117
117
  if __name__ == "__main__":
118
- HTTPServer(("0.0.0.0", 9811), Handler).serve_forever()
118
+ bind = os.environ.get("POC_WEBHOOK_BIND", "127.0.0.1")
119
+ port = int(os.environ.get("POC_WEBHOOK_PORT", "9811"))
120
+ HTTPServer((bind, port), Handler).serve_forever()
@@ -2,4 +2,4 @@
2
2
  set -euo pipefail
3
3
 
4
4
  cd "$(dirname "$0")/.."
5
- docker compose logs -f poc-webhook
5
+ tail -f openilink.log webhook.log
@@ -7,5 +7,51 @@ if [ ! -f .env ]; then
7
7
  cp .env.example .env
8
8
  fi
9
9
 
10
- docker compose up -d
11
- docker compose ps
10
+ set -a
11
+ . ./.env
12
+ set +a
13
+
14
+ if ! command -v oih >/dev/null 2>&1; then
15
+ echo "OpeniLink Hub CLI 'oih' is not installed. Run: npx nodus-wechat install-openilink" >&2
16
+ exit 1
17
+ fi
18
+
19
+ PYTHON_BIN="${PYTHON_BIN:-}"
20
+ if [ -z "$PYTHON_BIN" ]; then
21
+ if command -v python3 >/dev/null 2>&1; then
22
+ PYTHON_BIN=python3
23
+ elif command -v python >/dev/null 2>&1; then
24
+ PYTHON_BIN=python
25
+ else
26
+ echo "Python is required for the local webhook runtime." >&2
27
+ exit 1
28
+ fi
29
+ fi
30
+
31
+ OPENILINK_PID=".nodus-openilink.pid"
32
+ WEBHOOK_PID=".nodus-webhook.pid"
33
+
34
+ if [ -f "$WEBHOOK_PID" ] && kill -0 "$(cat "$WEBHOOK_PID")" >/dev/null 2>&1; then
35
+ echo "webhook: already running (pid $(cat "$WEBHOOK_PID"))"
36
+ else
37
+ POC_WEBHOOK_BIND="${POC_WEBHOOK_BIND:-127.0.0.1}" \
38
+ POC_WEBHOOK_PORT="${POC_WEBHOOK_PORT:-9811}" \
39
+ POC_WEBHOOK_TOKEN="${POC_WEBHOOK_TOKEN:-}" \
40
+ "$PYTHON_BIN" poc-webhook/server.py >>webhook.log 2>&1 &
41
+ echo $! >"$WEBHOOK_PID"
42
+ echo "webhook: started (pid $(cat "$WEBHOOK_PID"))"
43
+ fi
44
+
45
+ if [ -f "$OPENILINK_PID" ] && kill -0 "$(cat "$OPENILINK_PID")" >/dev/null 2>&1; then
46
+ echo "openilink: already running (pid $(cat "$OPENILINK_PID"))"
47
+ else
48
+ LISTEN=":${OPENILINK_PORT:-9800}" \
49
+ RP_ORIGIN="${OPENILINK_PUBLIC_ORIGIN:-http://localhost:9800}" \
50
+ RP_ID="${OPENILINK_RP_ID:-localhost}" \
51
+ oih >>openilink.log 2>&1 &
52
+ echo $! >"$OPENILINK_PID"
53
+ echo "openilink: started (pid $(cat "$OPENILINK_PID"))"
54
+ fi
55
+
56
+ echo "OpeniLink Hub: ${OPENILINK_PUBLIC_ORIGIN:-http://localhost:9800}"
57
+ echo "Webhook health: http://127.0.0.1:${POC_WEBHOOK_PORT:-9811}/health"
@@ -3,8 +3,21 @@ set -euo pipefail
3
3
 
4
4
  cd "$(dirname "$0")/.."
5
5
 
6
- docker compose ps
6
+ for name in openilink webhook; do
7
+ pid_file=".nodus-${name}.pid"
8
+ if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" >/dev/null 2>&1; then
9
+ echo "$name: running (pid $(cat "$pid_file"))"
10
+ else
11
+ echo "$name: stopped"
12
+ fi
13
+ done
14
+
15
+ if [ -f .env ]; then
16
+ set -a
17
+ . ./.env
18
+ set +a
19
+ fi
7
20
  printf "\nWebhook health:\n"
8
21
  curl -fsS "http://127.0.0.1:${POC_WEBHOOK_PORT:-9811}/health" || true
9
22
  printf "\n\nRecent webhook logs:\n"
10
- docker compose logs --tail=80 poc-webhook
23
+ tail -n 80 webhook.log 2>/dev/null || true
@@ -2,4 +2,13 @@
2
2
  set -euo pipefail
3
3
 
4
4
  cd "$(dirname "$0")/.."
5
- docker compose down
5
+ for name in webhook openilink; do
6
+ pid_file=".nodus-${name}.pid"
7
+ if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" >/dev/null 2>&1; then
8
+ kill "$(cat "$pid_file")" || true
9
+ echo "$name: stopped (pid $(cat "$pid_file"))"
10
+ else
11
+ echo "$name: stopped"
12
+ fi
13
+ rm -f "$pid_file"
14
+ done