@jiayunxie/aerial 0.1.11 → 0.2.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/src/service.js CHANGED
@@ -16,6 +16,8 @@ const DEFAULT_WRAPPER_LOG_BACKUPS = 3;
16
16
  const HEALTH_TIMEOUT_MS = 1500;
17
17
  const HEALTH_START_TIMEOUT_MS = 5000;
18
18
  const HEALTH_POLL_INTERVAL_MS = 250;
19
+ const MIN_SERVICE_NODE_MAJOR = 24;
20
+ const DARWIN_CODEX_NODE = "/Applications/Codex.app/Contents/Resources/node";
19
21
 
20
22
  function wrapperLogConfig() {
21
23
  const out = { maxBytes: DEFAULT_WRAPPER_LOG_MAX_BYTES, backups: DEFAULT_WRAPPER_LOG_BACKUPS };
@@ -76,8 +78,43 @@ function defaultRunCommand(file, args, opts = {}) {
76
78
  };
77
79
  }
78
80
 
81
+ function parseNodeMajor(versionText) {
82
+ const match = /^v?(\d+)\./.exec(String(versionText || "").trim());
83
+ return match ? Number(match[1]) : undefined;
84
+ }
85
+
86
+ function nodeMajorOfBinary(file) {
87
+ if (!file || !fs.existsSync(file)) return undefined;
88
+ const result = spawnSync(file, ["--version"], {
89
+ stdio: "pipe",
90
+ encoding: "utf8",
91
+ timeout: 3000,
92
+ windowsHide: true
93
+ });
94
+ if (result.status !== 0) return undefined;
95
+ return parseNodeMajor(result.stdout || result.stderr);
96
+ }
97
+
98
+ function selectServiceNodeBinary({ requested, current, candidates = [], versionOf = nodeMajorOfBinary } = {}) {
99
+ if (requested) return requested;
100
+ if (versionOf(current) >= MIN_SERVICE_NODE_MAJOR) return current;
101
+ for (const candidate of candidates) {
102
+ if (versionOf(candidate) >= MIN_SERVICE_NODE_MAJOR) return candidate;
103
+ }
104
+ return current;
105
+ }
106
+
107
+ function serviceNodeCandidates() {
108
+ if (process.platform === "darwin") return [DARWIN_CODEX_NODE];
109
+ return [];
110
+ }
111
+
79
112
  function nodeBinary() {
80
- return process.env.AERIAL_SERVICE_NODE || process.execPath;
113
+ return selectServiceNodeBinary({
114
+ requested: process.env.AERIAL_SERVICE_NODE,
115
+ current: process.execPath,
116
+ candidates: serviceNodeCandidates()
117
+ });
81
118
  }
82
119
 
83
120
  function cliEntry() {
@@ -622,8 +659,8 @@ function windowsServiceState(ctx) {
622
659
  }
623
660
 
624
661
  function serviceState(ctx) {
625
- if (process.platform === "darwin") return darwinServiceState(ctx);
626
- if (process.platform === "win32") return windowsServiceState(ctx);
662
+ const adapter = serviceAdapter(ctx);
663
+ if (adapter) return adapter.state();
627
664
  return { installed: false, loaded: false, reason: "unsupported_platform" };
628
665
  }
629
666
 
@@ -700,162 +737,237 @@ function windowsWriteDefinition(ctx) {
700
737
  return { wrapper, create };
701
738
  }
702
739
 
703
- async function describeRunning(ctx, host, port, healthFetch) {
740
+ function serviceAdapter(ctx) {
741
+ if (process.platform === "darwin") {
742
+ return {
743
+ platform: "darwin",
744
+ wrapperPath: darwinWrapperPath,
745
+ state: () => darwinServiceState(ctx),
746
+ writeDefinition: () => {
747
+ const written = darwinWriteDefinition();
748
+ return {
749
+ ok: true,
750
+ info: { file: written.file, wrapper: written.wrapper, label: SERVICE_LABEL }
751
+ };
752
+ },
753
+ triggerStart: () => darwinBootstrap(ctx),
754
+ triggerStop: () => darwinBootout(ctx),
755
+ startFailureReason: "bootstrap_failed",
756
+ startResultKey: "bootstrap",
757
+ uninstall: (state) => darwinUninstall(ctx, state)
758
+ };
759
+ }
760
+ if (process.platform === "win32") {
761
+ return {
762
+ platform: "win32",
763
+ wrapperPath: winWrapperPath,
764
+ state: () => windowsServiceState(ctx),
765
+ writeDefinition: () => {
766
+ const written = windowsWriteDefinition(ctx);
767
+ const info = {
768
+ taskName: WIN_TASK_NAME,
769
+ wrapper: written.wrapper,
770
+ create: { status: written.create.status, stderr: written.create.stderr }
771
+ };
772
+ return { ok: written.create.status === 0, info };
773
+ },
774
+ triggerStart: () => ctx.run("schtasks.exe", buildSchtasksArgs("run")),
775
+ triggerStop: () => ctx.run("schtasks.exe", buildSchtasksArgs("end")),
776
+ startFailureReason: "run_failed",
777
+ startResultKey: "run",
778
+ uninstall: (state) => windowsUninstall(ctx, state)
779
+ };
780
+ }
781
+ return undefined;
782
+ }
783
+
784
+ function requireServiceAdapter(ctx, action) {
785
+ const adapter = serviceAdapter(ctx);
786
+ if (!adapter) throw unsupportedError(action);
787
+ return adapter;
788
+ }
789
+
790
+ function commandResultBlock(key, result) {
791
+ return { [key]: { status: result.status, stderr: result.stderr } };
792
+ }
793
+
794
+ function buildDefinitionRefreshFailure({ adapter, supervisor, config, definition }) {
795
+ const isManaged = supervisor === "service-managed";
796
+ const status = definition.info.create?.status;
797
+ const reason = isManaged ? "managed_definition_refresh_failed" : "foreground_definition_refresh_failed";
798
+ const message = isManaged
799
+ ? `schtasks /Create failed (status ${status}) while refreshing the managed-service definition. The running service was not disturbed, but wrapper/env changes were NOT applied. Resolve the underlying schtasks error and rerun \`aerial service install\`.`
800
+ : `Aerial is already running in the foreground on port ${config.port}. The wrapper was rewritten but schtasks /Create failed (status ${status}), so the Task Scheduler definition was NOT refreshed. Resolve the underlying schtasks error and rerun \`aerial service install\`.`;
801
+ return {
802
+ ok: false,
803
+ action: "install",
804
+ platform: adapter.platform,
805
+ reason,
806
+ ...(isManaged ? {} : { definitionUpdated: false }),
807
+ message,
808
+ warning: tokenWarning(),
809
+ ...definition.info
810
+ };
811
+ }
812
+
813
+ function buildRunningDefinitionResult({ adapter, supervisor, config, definition }) {
814
+ const managed = supervisor === "service-managed";
815
+ return {
816
+ ok: managed,
817
+ action: "install",
818
+ platform: adapter.platform,
819
+ ...(managed
820
+ ? {
821
+ note: "already running (service-managed); definition refreshed; run `aerial service restart` to apply wrapper/env changes"
822
+ }
823
+ : {
824
+ reason: "foreground_running",
825
+ message: `Aerial is already running in the foreground on port ${config.port}. The service definition has been updated, but the service was NOT started to avoid running two instances. Next step: stop the foreground process, then run \`aerial service start\`.`
826
+ }),
827
+ definitionUpdated: true,
828
+ warning: tokenWarning(),
829
+ ...definition.info
830
+ };
831
+ }
832
+
833
+ async function confirmStarted({ action, result, config, wrapper, healthFetch, healthDeadlineMs }) {
834
+ if (!result.ok) return result;
835
+ if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
836
+ return { ...result, health: { ok: true, attempts: 0, elapsedMs: 0, dryRun: true } };
837
+ }
838
+ const poll = await pollForAerialUp(config.host, config.port, healthFetch, healthDeadlineMs);
839
+ if (poll.cls.mode === "aerial_running") {
840
+ return { ...result, health: { ok: true, attempts: poll.attempts, elapsedMs: poll.elapsedMs } };
841
+ }
842
+ const diagnostics = healthFailedDiagnostics({
843
+ wrapper,
844
+ probe: poll.probe,
845
+ attempts: poll.attempts,
846
+ elapsedMs: poll.elapsedMs
847
+ });
848
+ if (poll.cls.mode === "port_conflict") {
849
+ const prefix = action === "install" ? "After install" : "After start";
850
+ return {
851
+ ...result,
852
+ ok: false,
853
+ reason: "port_conflict",
854
+ message: `${prefix}, port ${config.port} is responding as a non-Aerial process: ${poll.cls.reason}. Free the port and rerun.`,
855
+ diagnostics
856
+ };
857
+ }
858
+ const message = action === "install"
859
+ ? `Service definition was written and start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`
860
+ : `Start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`;
861
+ return {
862
+ ...result,
863
+ ok: false,
864
+ reason: "health_check_failed",
865
+ ...(action === "install" ? { definitionWritten: true } : {}),
866
+ startAttempted: true,
867
+ message,
868
+ diagnostics
869
+ };
870
+ }
871
+
872
+ async function describeRunning(adapter, host, port, healthFetch, knownState) {
704
873
  const probe = await (healthFetch || defaultHealthFetch)(host, port);
705
874
  const cls = classifyHealth(probe);
706
875
  if (cls.mode !== "aerial_running") return { cls, probe };
707
- const state = serviceState(ctx);
876
+ const state = knownState || adapter.state();
708
877
  const supervisor = state.installed && state.loaded ? "service-managed" : "foreground";
709
878
  return { cls, probe, supervisor };
710
879
  }
711
880
 
712
881
  export async function serviceInstall({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
713
- if (isUnsupportedPlatform()) throw unsupportedError("install");
714
882
  const ctx = { run };
883
+ const adapter = requireServiceAdapter(ctx, "install");
715
884
  const config = loadConfig();
716
- const { cls, supervisor } = await describeRunning(ctx, config.host, config.port, healthFetch);
885
+ const { cls, supervisor } = await describeRunning(adapter, config.host, config.port, healthFetch);
717
886
  if (cls.mode === "port_conflict") {
718
- logEvent("service_install", { platform: process.platform, ok: false, reason: "port_conflict" });
887
+ logEvent("service_install", { platform: adapter.platform, ok: false, reason: "port_conflict" });
719
888
  return {
720
889
  ok: false,
721
890
  action: "install",
722
- platform: process.platform,
891
+ platform: adapter.platform,
723
892
  reason: "port_conflict",
724
893
  message: `Port ${config.port} is already in use by a non-Aerial process: ${cls.reason}. Free the port and rerun.`
725
894
  };
726
895
  }
727
- if (cls.mode === "aerial_running" && supervisor === "service-managed") {
728
- let writeInfo;
729
- if (process.platform === "darwin") {
730
- const written = darwinWriteDefinition();
731
- writeInfo = { file: written.file, wrapper: written.wrapper, label: SERVICE_LABEL };
732
- } else {
733
- const written = windowsWriteDefinition(ctx);
734
- writeInfo = { taskName: WIN_TASK_NAME, wrapper: written.wrapper, create: { status: written.create.status, stderr: written.create.stderr } };
735
- if (written.create.status !== 0) {
736
- logEvent("service_install", { platform: "win32", ok: false, reason: "managed_definition_refresh_failed", status: written.create.status });
737
- return {
738
- ok: false,
739
- action: "install",
740
- platform: "win32",
741
- reason: "managed_definition_refresh_failed",
742
- message: `schtasks /Create failed (status ${written.create.status}) while refreshing the managed-service definition. The running service was not disturbed, but wrapper/env changes were NOT applied. Resolve the underlying schtasks error and rerun \`aerial service install\`.`,
743
- warning: tokenWarning(),
744
- ...writeInfo
745
- };
746
- }
747
- }
748
- logEvent("service_install", { platform: process.platform, ok: true, note: "managed_refreshed", definitionUpdated: true });
749
- return {
750
- ok: true,
751
- action: "install",
752
- platform: process.platform,
753
- note: "already running (service-managed); definition refreshed; run `aerial service restart` to apply wrapper/env changes",
754
- definitionUpdated: true,
755
- warning: tokenWarning(),
756
- ...writeInfo
757
- };
758
- }
759
- if (cls.mode === "aerial_running" && supervisor === "foreground") {
760
- let writeInfo;
761
- if (process.platform === "darwin") {
762
- const written = darwinWriteDefinition();
763
- writeInfo = { file: written.file, wrapper: written.wrapper, label: SERVICE_LABEL };
764
- } else {
765
- const written = windowsWriteDefinition(ctx);
766
- writeInfo = { taskName: WIN_TASK_NAME, wrapper: written.wrapper, create: { status: written.create.status, stderr: written.create.stderr } };
767
- if (written.create.status !== 0) {
768
- logEvent("service_install", { platform: "win32", ok: false, reason: "foreground_definition_refresh_failed", status: written.create.status });
769
- return {
770
- ok: false,
771
- action: "install",
772
- platform: "win32",
773
- reason: "foreground_definition_refresh_failed",
774
- definitionUpdated: false,
775
- message: `Aerial is already running in the foreground on port ${config.port}. The wrapper was rewritten but schtasks /Create failed (status ${written.create.status}), so the Task Scheduler definition was NOT refreshed. Resolve the underlying schtasks error and rerun \`aerial service install\`.`,
776
- warning: tokenWarning(),
777
- ...writeInfo
778
- };
779
- }
896
+ if (cls.mode === "aerial_running") {
897
+ const definition = adapter.writeDefinition();
898
+ if (!definition.ok) {
899
+ const failure = buildDefinitionRefreshFailure({ adapter, supervisor, config, definition });
900
+ logEvent("service_install", {
901
+ platform: adapter.platform,
902
+ ok: false,
903
+ reason: failure.reason,
904
+ status: definition.info.create?.status
905
+ });
906
+ return failure;
780
907
  }
781
- logEvent("service_install", { platform: process.platform, ok: false, reason: "foreground_running", definitionUpdated: true });
782
- return {
783
- ok: false,
784
- action: "install",
785
- platform: process.platform,
786
- reason: "foreground_running",
787
- definitionUpdated: true,
788
- message: `Aerial is already running in the foreground on port ${config.port}. The service definition has been updated, but the service was NOT started to avoid running two instances. Next step: stop the foreground process, then run \`aerial service start\`.`,
789
- warning: tokenWarning(),
790
- ...writeInfo
791
- };
908
+ const result = buildRunningDefinitionResult({ adapter, supervisor, config, definition });
909
+ logEvent("service_install", {
910
+ platform: adapter.platform,
911
+ ok: result.ok,
912
+ reason: result.reason,
913
+ note: supervisor === "service-managed" ? "managed_refreshed" : undefined,
914
+ definitionUpdated: true
915
+ });
916
+ return result;
792
917
  }
793
- let result;
794
- if (process.platform === "darwin") {
795
- const written = darwinWriteDefinition();
796
- const bootstrap = darwinBootstrap(ctx);
797
- const ok = bootstrap.status === 0;
798
- result = { ok, action: "install", platform: "darwin", file: written.file, wrapper: written.wrapper, label: SERVICE_LABEL, bootstrap: { status: bootstrap.status, stderr: bootstrap.stderr } };
799
- if (!ok) result.reason = "bootstrap_failed";
918
+
919
+ const definition = adapter.writeDefinition();
920
+ let result = {
921
+ ok: definition.ok,
922
+ action: "install",
923
+ platform: adapter.platform,
924
+ ...definition.info
925
+ };
926
+ if (!definition.ok) {
927
+ result.reason = "create_failed";
800
928
  } else {
801
- const written = windowsWriteDefinition(ctx);
802
- const createOk = written.create.status === 0;
803
- if (!createOk) {
804
- result = { ok: false, action: "install", platform: "win32", taskName: WIN_TASK_NAME, wrapper: written.wrapper, create: { status: written.create.status, stderr: written.create.stderr }, reason: "create_failed" };
805
- } else {
806
- const runRes = ctx.run("schtasks.exe", buildSchtasksArgs("run"));
807
- const ok = runRes.status === 0;
808
- result = { ok, action: "install", platform: "win32", taskName: WIN_TASK_NAME, wrapper: written.wrapper, create: { status: written.create.status, stderr: written.create.stderr }, run: { status: runRes.status, stderr: runRes.stderr } };
809
- if (!ok) result.reason = "run_failed";
810
- }
811
- }
812
- if (result.ok) {
813
- if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
814
- result.health = { ok: true, attempts: 0, elapsedMs: 0, dryRun: true };
815
- } else {
816
- const poll = await pollForAerialUp(config.host, config.port, healthFetch, healthDeadlineMs);
817
- if (poll.cls.mode === "aerial_running") {
818
- result.health = { ok: true, attempts: poll.attempts, elapsedMs: poll.elapsedMs };
819
- } else if (poll.cls.mode === "port_conflict") {
820
- result.ok = false;
821
- result.reason = "port_conflict";
822
- result.message = `After install, port ${config.port} is responding as a non-Aerial process: ${poll.cls.reason}. Free the port and rerun.`;
823
- result.diagnostics = healthFailedDiagnostics({ wrapper: result.wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs });
824
- } else {
825
- result.ok = false;
826
- result.reason = "health_check_failed";
827
- result.definitionWritten = true;
828
- result.startAttempted = true;
829
- result.message = `Service definition was written and start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`;
830
- result.diagnostics = healthFailedDiagnostics({ wrapper: result.wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs });
831
- }
832
- }
929
+ const start = adapter.triggerStart();
930
+ const triggerOk = start.status === 0;
931
+ result = {
932
+ ...result,
933
+ ok: triggerOk,
934
+ ...commandResultBlock(adapter.startResultKey, start),
935
+ ...(triggerOk ? {} : { reason: adapter.startFailureReason })
936
+ };
833
937
  }
938
+ result = await confirmStarted({
939
+ action: "install",
940
+ result,
941
+ config,
942
+ wrapper: result.wrapper,
943
+ healthFetch,
944
+ healthDeadlineMs
945
+ });
834
946
  result.warning = tokenWarning();
835
- logEvent("service_install", { platform: process.platform, ok: result.ok, reason: result.reason });
947
+ logEvent("service_install", { platform: adapter.platform, ok: result.ok, reason: result.reason });
836
948
  return result;
837
949
  }
838
950
 
839
951
  export async function serviceStart({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
840
- if (isUnsupportedPlatform()) throw unsupportedError("start");
841
952
  const ctx = { run };
953
+ const adapter = requireServiceAdapter(ctx, "start");
842
954
  const config = loadConfig();
843
- const state = serviceState(ctx);
955
+ const state = adapter.state();
844
956
  if (!state.installed) {
845
957
  return {
846
958
  ok: false,
847
959
  action: "start",
848
- platform: process.platform,
960
+ platform: adapter.platform,
849
961
  reason: "not_installed",
850
962
  message: "Service is not installed. Run `aerial service install` first."
851
963
  };
852
964
  }
853
- const { cls, supervisor } = await describeRunning(ctx, config.host, config.port, healthFetch);
965
+ const { cls, supervisor } = await describeRunning(adapter, config.host, config.port, healthFetch, state);
854
966
  if (cls.mode === "port_conflict") {
855
967
  return {
856
968
  ok: false,
857
969
  action: "start",
858
- platform: process.platform,
970
+ platform: adapter.platform,
859
971
  reason: "port_conflict",
860
972
  message: `Port ${config.port} is already in use by a non-Aerial process: ${cls.reason}. Free the port and rerun.`
861
973
  };
@@ -864,7 +976,7 @@ export async function serviceStart({ run = defaultRunCommand, healthFetch, healt
864
976
  return {
865
977
  ok: false,
866
978
  action: "start",
867
- platform: process.platform,
979
+ platform: adapter.platform,
868
980
  reason: "foreground_running",
869
981
  message: `Aerial is already running in the foreground on port ${config.port}. Stop the foreground process before starting the service.`
870
982
  };
@@ -873,82 +985,51 @@ export async function serviceStart({ run = defaultRunCommand, healthFetch, healt
873
985
  return {
874
986
  ok: true,
875
987
  action: "start",
876
- platform: process.platform,
988
+ platform: adapter.platform,
877
989
  note: "already running (service-managed)",
878
990
  warning: tokenWarning()
879
991
  };
880
992
  }
881
- let r;
882
- if (process.platform === "darwin") {
883
- r = darwinBootstrap(ctx);
884
- } else {
885
- r = ctx.run("schtasks.exe", buildSchtasksArgs("run"));
886
- }
993
+ const r = adapter.triggerStart();
887
994
  const triggerOk = r.status === 0;
888
- const base = {
995
+ let result = {
889
996
  ok: triggerOk,
890
997
  action: "start",
891
- platform: process.platform,
998
+ platform: adapter.platform,
892
999
  status: r.status,
893
1000
  stderr: r.stderr,
894
1001
  warning: tokenWarning(),
895
- ...(triggerOk ? {} : { reason: process.platform === "darwin" ? "bootstrap_failed" : "run_failed" })
1002
+ ...(triggerOk ? {} : { reason: adapter.startFailureReason })
896
1003
  };
897
1004
  if (!triggerOk) {
898
- logEvent("service_start", { platform: process.platform, status: r.status, reason: base.reason });
899
- return base;
900
- }
901
- if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
902
- base.health = { ok: true, attempts: 0, elapsedMs: 0, dryRun: true };
903
- logEvent("service_start", { platform: process.platform, status: r.status, ok: true, dryRun: true });
904
- return base;
905
- }
906
- const poll = await pollForAerialUp(config.host, config.port, healthFetch, healthDeadlineMs);
907
- if (poll.cls.mode === "aerial_running") {
908
- base.health = { ok: true, attempts: poll.attempts, elapsedMs: poll.elapsedMs };
909
- logEvent("service_start", { platform: process.platform, status: r.status, ok: true });
910
- return base;
911
- }
912
- const wrapper = process.platform === "darwin" ? darwinWrapperPath() : winWrapperPath();
913
- if (poll.cls.mode === "port_conflict") {
914
- logEvent("service_start", { platform: process.platform, ok: false, reason: "port_conflict" });
915
- return {
916
- ...base,
917
- ok: false,
918
- reason: "port_conflict",
919
- message: `After start, port ${config.port} is responding as a non-Aerial process: ${poll.cls.reason}. Free the port and rerun.`,
920
- diagnostics: healthFailedDiagnostics({ wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs })
921
- };
1005
+ logEvent("service_start", { platform: adapter.platform, status: r.status, reason: result.reason });
1006
+ return result;
922
1007
  }
923
- logEvent("service_start", { platform: process.platform, ok: false, reason: "health_check_failed" });
924
- return {
925
- ...base,
926
- ok: false,
927
- reason: "health_check_failed",
928
- startAttempted: true,
929
- message: `Start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`,
930
- diagnostics: healthFailedDiagnostics({ wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs })
931
- };
1008
+ result = await confirmStarted({
1009
+ action: "start",
1010
+ result,
1011
+ config,
1012
+ wrapper: adapter.wrapperPath(),
1013
+ healthFetch,
1014
+ healthDeadlineMs
1015
+ });
1016
+ logEvent("service_start", { platform: adapter.platform, status: r.status, ok: result.ok, reason: result.reason, dryRun: result.health?.dryRun });
1017
+ return result;
932
1018
  }
933
1019
 
934
1020
  export function serviceStop({ run = defaultRunCommand } = {}) {
935
- if (isUnsupportedPlatform()) throw unsupportedError("stop");
936
1021
  const ctx = { run };
937
- const state = serviceState(ctx);
1022
+ const adapter = requireServiceAdapter(ctx, "stop");
1023
+ const state = adapter.state();
938
1024
  if (!state.installed) {
939
- return { ok: true, action: "stop", platform: process.platform, note: "not installed" };
1025
+ return { ok: true, action: "stop", platform: adapter.platform, note: "not installed" };
940
1026
  }
941
1027
  if (!state.loaded) {
942
- return { ok: true, action: "stop", platform: process.platform, note: "not running" };
1028
+ return { ok: true, action: "stop", platform: adapter.platform, note: "not running" };
943
1029
  }
944
- let r;
945
- if (process.platform === "darwin") {
946
- r = darwinBootout(ctx);
947
- } else {
948
- r = ctx.run("schtasks.exe", buildSchtasksArgs("end"));
949
- }
950
- logEvent("service_stop", { platform: process.platform, status: r.status });
951
- return { ok: r.status === 0, action: "stop", platform: process.platform, status: r.status, stderr: r.stderr };
1030
+ const r = adapter.triggerStop();
1031
+ logEvent("service_stop", { platform: adapter.platform, status: r.status });
1032
+ return { ok: r.status === 0, action: "stop", platform: adapter.platform, status: r.status, stderr: r.stderr };
952
1033
  }
953
1034
 
954
1035
  export async function serviceRestart(opts = {}) {
@@ -960,54 +1041,52 @@ export async function serviceRestart(opts = {}) {
960
1041
  return { ok: start.ok, action: "restart", platform: process.platform, stop, start, warning: start.warning };
961
1042
  }
962
1043
 
963
- export function serviceUninstall({ run = defaultRunCommand } = {}) {
964
- if (isUnsupportedPlatform()) throw unsupportedError("uninstall");
965
- const ctx = { run };
966
- const state = serviceState(ctx);
967
- if (!state.installed) {
968
- return { ok: true, action: "uninstall", platform: process.platform, note: "no service installed" };
1044
+ function removeFileIfExists(file) {
1045
+ if (!fs.existsSync(file)) return false;
1046
+ try {
1047
+ fs.unlinkSync(file);
1048
+ return true;
1049
+ } catch {
1050
+ return false;
969
1051
  }
970
- if (process.platform === "darwin") {
971
- const file = plistPath();
972
- const wrapper = darwinWrapperPath();
973
- if (state.loaded) {
974
- const bootout = darwinBootout(ctx);
975
- if (bootout.status !== 0) {
976
- logEvent("service_uninstall", { platform: "darwin", ok: false, reason: "bootout_failed", status: bootout.status });
977
- return {
978
- ok: false,
979
- action: "uninstall",
980
- platform: "darwin",
981
- reason: "bootout_failed",
982
- file,
983
- wrapper,
984
- bootout: { status: bootout.status, stderr: bootout.stderr },
985
- message: `launchctl bootout failed (status ${bootout.status}). Service is still loaded; plist and wrapper were preserved. Retry with \`aerial service uninstall\`.`
986
- };
987
- }
988
- try { fs.unlinkSync(file); } catch {}
989
- if (fs.existsSync(wrapper)) {
990
- try { fs.unlinkSync(wrapper); } catch {}
991
- }
992
- logEvent("service_uninstall", { platform: "darwin", ok: true });
993
- return { ok: true, action: "uninstall", platform: "darwin", file, wrapper, bootout: { status: bootout.status, stderr: bootout.stderr } };
994
- }
995
- try { fs.unlinkSync(file); } catch {}
996
- if (fs.existsSync(wrapper)) {
997
- try { fs.unlinkSync(wrapper); } catch {}
1052
+ }
1053
+
1054
+ function darwinUninstall(ctx, state) {
1055
+ const file = plistPath();
1056
+ const wrapper = darwinWrapperPath();
1057
+ if (state.loaded) {
1058
+ const bootout = darwinBootout(ctx);
1059
+ if (bootout.status !== 0) {
1060
+ logEvent("service_uninstall", { platform: "darwin", ok: false, reason: "bootout_failed", status: bootout.status });
1061
+ return {
1062
+ ok: false,
1063
+ action: "uninstall",
1064
+ platform: "darwin",
1065
+ reason: "bootout_failed",
1066
+ file,
1067
+ wrapper,
1068
+ bootout: { status: bootout.status, stderr: bootout.stderr },
1069
+ message: `launchctl bootout failed (status ${bootout.status}). Service is still loaded; plist and wrapper were preserved. Retry with \`aerial service uninstall\`.`
1070
+ };
998
1071
  }
1072
+ removeFileIfExists(file);
1073
+ removeFileIfExists(wrapper);
999
1074
  logEvent("service_uninstall", { platform: "darwin", ok: true });
1000
- return { ok: true, action: "uninstall", platform: "darwin", file, wrapper, bootout: { status: 0, skipped: "not_loaded" } };
1075
+ return { ok: true, action: "uninstall", platform: "darwin", file, wrapper, bootout: { status: bootout.status, stderr: bootout.stderr } };
1001
1076
  }
1077
+ removeFileIfExists(file);
1078
+ removeFileIfExists(wrapper);
1079
+ logEvent("service_uninstall", { platform: "darwin", ok: true });
1080
+ return { ok: true, action: "uninstall", platform: "darwin", file, wrapper, bootout: { status: 0, skipped: "not_loaded" } };
1081
+ }
1082
+
1083
+ function windowsUninstall(ctx, state) {
1002
1084
  if (state.loaded) {
1003
1085
  ctx.run("schtasks.exe", buildSchtasksArgs("end"));
1004
1086
  }
1005
1087
  const del = ctx.run("schtasks.exe", buildSchtasksArgs("delete"));
1006
1088
  const wrapper = winWrapperPath();
1007
- let wrapperRemoved = false;
1008
- if (del.status === 0 && fs.existsSync(wrapper)) {
1009
- try { fs.unlinkSync(wrapper); wrapperRemoved = true; } catch {}
1010
- }
1089
+ const wrapperRemoved = del.status === 0 ? removeFileIfExists(wrapper) : false;
1011
1090
  logEvent("service_uninstall", { platform: "win32", ok: del.status === 0 });
1012
1091
  return {
1013
1092
  ok: del.status === 0,
@@ -1021,6 +1100,16 @@ export function serviceUninstall({ run = defaultRunCommand } = {}) {
1021
1100
  };
1022
1101
  }
1023
1102
 
1103
+ export function serviceUninstall({ run = defaultRunCommand } = {}) {
1104
+ const ctx = { run };
1105
+ const adapter = requireServiceAdapter(ctx, "uninstall");
1106
+ const state = adapter.state();
1107
+ if (!state.installed) {
1108
+ return { ok: true, action: "uninstall", platform: adapter.platform, note: "no service installed" };
1109
+ }
1110
+ return adapter.uninstall(state);
1111
+ }
1112
+
1024
1113
  export async function serviceStatus({ run = defaultRunCommand, healthFetch } = {}) {
1025
1114
  const config = loadConfig();
1026
1115
  const ctx = { run };
@@ -1087,5 +1176,7 @@ export const _internal = {
1087
1176
  wrapperBlock,
1088
1177
  buildSchtasksArgs,
1089
1178
  quoteSchtasksTR,
1090
- classifyHealth
1179
+ classifyHealth,
1180
+ parseNodeMajor,
1181
+ selectServiceNodeBinary
1091
1182
  };