@fased/fased 0.1.38 → 0.1.40

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 (94) hide show
  1. package/dist/brand.js +1 -1
  2. package/dist/build-info.json +3 -3
  3. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  4. package/dist/cli/daemon-cli/install.js +38 -14
  5. package/dist/cli/daemon-cli/register-service-commands.js +1 -1
  6. package/dist/cli/update-cli/service-target.js +13 -0
  7. package/dist/cli/update-cli/update-command.js +122 -44
  8. package/dist/commands/daemon-install-helpers.js +3 -0
  9. package/dist/daemon/service.js +5 -0
  10. package/dist/daemon/systemd-system.js +237 -0
  11. package/dist/daemon/systemd.js +2 -2
  12. package/dist/infra/update-runner.js +41 -5
  13. package/dist/plugin-sdk/{accounts-COY-15zJ.js → accounts-C5BjfxoO.js} +1 -1
  14. package/dist/plugin-sdk/{allow-list-Cgjx0Mc-.js → allow-list-DqLmAAjC.js} +1 -1
  15. package/dist/plugin-sdk/{audio-preflight-BPK8t0tI.js → audio-preflight-CSRhaknd.js} +1 -1
  16. package/dist/plugin-sdk/{audio-preflight-kCa_KGRH.js → audio-preflight-DlkuhmkH.js} +2 -2
  17. package/dist/plugin-sdk/channel-plugin-common.js +2 -2
  18. package/dist/plugin-sdk/{chunk-items-DGHUeFuI.js → chunk-items-B1CvDQbk.js} +1 -1
  19. package/dist/plugin-sdk/command-status.js +2 -2
  20. package/dist/plugin-sdk/{config-C0qggNKQ.js → config-BxVjLTIv.js} +7 -7
  21. package/dist/plugin-sdk/{config-z0k05dwe.js → config-DLVjQxOu.js} +7 -7
  22. package/dist/plugin-sdk/{config-BULEb5QB.js → config-HEgnpfBH.js} +7 -7
  23. package/dist/plugin-sdk/{config-DgD5T-Ui.js → config-g1MG9Bet.js} +7 -7
  24. package/dist/plugin-sdk/discord.js +6 -6
  25. package/dist/plugin-sdk/{guilds-iTYlgBat.js → guilds-BivaIhOn.js} +1 -1
  26. package/dist/plugin-sdk/index.js +12 -2
  27. package/dist/plugin-sdk/{login-YveoqEvH.js → login-CfRfRNc7.js} +2 -2
  28. package/dist/plugin-sdk/{monitor-rOs3mUol.js → monitor-4qlGtlZm.js} +3 -3
  29. package/dist/plugin-sdk/{monitor-C6Ohw9BA.js → monitor-CA4i9Vhf.js} +4 -4
  30. package/dist/plugin-sdk/{monitor-6lkdOqhQ.js → monitor-CIQ75vj5.js} +3 -3
  31. package/dist/plugin-sdk/{monitor-o20KTHIi.js → monitor-uao4bwTm.js} +6 -6
  32. package/dist/plugin-sdk/{openresponses-http-BJONnUdy.js → openresponses-http-Do-lt3tH.js} +1 -1
  33. package/dist/plugin-sdk/{openresponses-http-DkWqYznu.js → openresponses-http-Y766bB2w.js} +2 -2
  34. package/dist/plugin-sdk/{pi-embedded-runner-DqDy0MWN.js → pi-embedded-runner-Cl-dGLKY.js} +7 -7
  35. package/dist/plugin-sdk/{pi-model-discovery-runtime-w4DRRv53.js → pi-model-discovery-runtime-CfDDK1n5.js} +2 -2
  36. package/dist/plugin-sdk/{pi-model-discovery-runtime-CQOauapl.js → pi-model-discovery-runtime-DYfUuqPN.js} +1 -1
  37. package/dist/plugin-sdk/{pi-model-discovery-runtime-C5ssE6EW.js → pi-model-discovery-runtime-DxoFm4Vd.js} +1 -1
  38. package/dist/plugin-sdk/{pi-model-discovery-runtime-CKKNoqwk.js → pi-model-discovery-runtime-LB-5lX5L.js} +1 -1
  39. package/dist/plugin-sdk/{pw-ai-Boor5vYw.js → pw-ai-SNCI9UXT.js} +1 -1
  40. package/dist/plugin-sdk/{resolve-channels-gIpOj1wt.js → resolve-channels-BptLzpDb.js} +3 -3
  41. package/dist/plugin-sdk/{resolve-users-CTKaESmf.js → resolve-users-C0YLyf20.js} +3 -3
  42. package/dist/plugin-sdk/{resolve-utils-bixHYDBj.js → resolve-utils-Bq0vrn3x.js} +1 -1
  43. package/dist/plugin-sdk/{retry-CB29XXbz.js → retry-CxA2fayk.js} +2 -2
  44. package/dist/plugin-sdk/{runtime-dependency-Deg_sw2k.js → runtime-dependency-B-1u41Ph.js} +9 -9
  45. package/dist/plugin-sdk/{slack-CGTAN4UD.js → slack-BnH97t7_.js} +3 -3
  46. package/dist/plugin-sdk/{slack-BErIh0uj.js → slack-DvaEXxo5.js} +4 -4
  47. package/dist/plugin-sdk/slack.js +2 -2
  48. package/dist/plugin-sdk/src/brand.d.ts +2 -2
  49. package/dist/plugin-sdk/src/daemon/systemd-system.d.ts +28 -0
  50. package/dist/plugin-sdk/{status-CCGFb0f2.js → status-DKwtdFcx.js} +15 -15
  51. package/dist/plugin-sdk/{sticker-cache-wxl1Me2i.js → sticker-cache-C6yLVx3S.js} +2 -2
  52. package/dist/plugin-sdk/{sticker-cache-CKAkkCoU.js → sticker-cache-Dmrdawoj.js} +2 -2
  53. package/dist/plugin-sdk/{telegram-actions-r86ntIKR.js → telegram-actions-DnvgAPkX.js} +2 -2
  54. package/dist/plugin-sdk/{telegram-actions-BRUNEXMr.js → telegram-actions-JyjH4UpU.js} +3 -3
  55. package/dist/plugin-sdk/{web-02I0vmdu.js → web-4BVlmvJD.js} +2 -2
  56. package/dist/plugin-sdk/{web-CXz56wyu.js → web-DsLHqCkb.js} +1 -1
  57. package/dist/plugin-sdk/whatsapp.js +4 -4
  58. package/extensions/acpx/package.json +1 -1
  59. package/extensions/bluebubbles/package.json +1 -1
  60. package/extensions/copilot-proxy/package.json +1 -1
  61. package/extensions/diagnostics-otel/package.json +1 -1
  62. package/extensions/fased-federation/package.json +1 -1
  63. package/extensions/google-gemini-cli-auth/package.json +1 -1
  64. package/extensions/imessage/package.json +1 -1
  65. package/extensions/irc/package.json +1 -1
  66. package/extensions/line/package.json +1 -1
  67. package/extensions/llm-task/package.json +1 -1
  68. package/extensions/lobster/package.json +1 -1
  69. package/extensions/matrix/CHANGELOG.md +6 -0
  70. package/extensions/matrix/package.json +1 -1
  71. package/extensions/mattermost/package.json +1 -1
  72. package/extensions/memory-core/package.json +2 -2
  73. package/extensions/memory-lancedb/package.json +1 -1
  74. package/extensions/minimax-portal-auth/package.json +1 -1
  75. package/extensions/msteams/CHANGELOG.md +6 -0
  76. package/extensions/msteams/package.json +1 -1
  77. package/extensions/nextcloud-talk/package.json +1 -1
  78. package/extensions/nostr/CHANGELOG.md +6 -0
  79. package/extensions/nostr/package.json +1 -1
  80. package/extensions/open-prose/package.json +1 -1
  81. package/extensions/sat-mining/package.json +1 -1
  82. package/extensions/signal/package.json +1 -1
  83. package/extensions/synology-chat/package.json +1 -1
  84. package/extensions/tlon/package.json +1 -1
  85. package/extensions/twitch/CHANGELOG.md +6 -0
  86. package/extensions/twitch/package.json +1 -1
  87. package/extensions/voice-call/CHANGELOG.md +6 -0
  88. package/extensions/voice-call/package.json +1 -1
  89. package/extensions/zalo/CHANGELOG.md +6 -0
  90. package/extensions/zalo/package.json +1 -1
  91. package/extensions/zalouser/CHANGELOG.md +6 -0
  92. package/extensions/zalouser/package.json +1 -1
  93. package/package.json +1 -1
  94. package/scripts/start-managed.sh +2 -2
package/dist/brand.js CHANGED
@@ -2,7 +2,7 @@
2
2
  const FASED_BRAND_NAME = "Fased";
3
3
  const FASED_AGENT_NAME = "Fased Agent";
4
4
  const FASED_CONTROL_NAME = "Fased Control";
5
- const FASED_PRODUCT_VERSION = "0.1.38";
5
+ const FASED_PRODUCT_VERSION = "0.1.40";
6
6
  const FASED_DISPLAY_VERSION = `v${FASED_PRODUCT_VERSION}`;
7
7
  function formatFasedDisplayLine() {
8
8
  return `${FASED_AGENT_NAME} ${FASED_DISPLAY_VERSION}`;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.38",
3
- "commit": "68d9d1e95f7fb0b92a327631dae5f0bb4221ec2f",
4
- "builtAt": "2026-07-10T19:08:10.142Z"
2
+ "version": "0.1.40",
3
+ "commit": "a8403b7a8a07f287ffa8c564c20015b41c376fc5",
4
+ "builtAt": "2026-07-10T23:09:12.393Z"
5
5
  }
@@ -1 +1 @@
1
- d91da5fc7935c5d64a96871c12c0f9f54ab7c949c1edb6d966a4df86dbf6f149
1
+ d43118ba792b198aa36c870735aede71aa66e9f53636ca999d50426d55459175
@@ -7,6 +7,7 @@ import { randomToken } from "../../commands/onboard-helpers.js";
7
7
  import { resolveGatewayAuth } from "../../gateway/auth.js";
8
8
  import { buildGatewayInstallPlan } from "../../commands/daemon-install-helpers.js";
9
9
  import { DEFAULT_GATEWAY_DAEMON_RUNTIME, isGatewayDaemonRuntime } from "../../commands/daemon-runtime.js";
10
+ import { installHostedSystemdService } from "../../daemon/systemd-system.js";
10
11
  import { resolveGatewayService } from "../../daemon/service.js";
11
12
  import { buildDaemonServiceSnapshot, createDaemonActionContext } from "./response.js";
12
13
  import { parsePort } from "../shared/parse-port.js";
@@ -40,6 +41,11 @@ async function runDaemonInstall(opts) {
40
41
  return;
41
42
  }
42
43
  const service = resolveGatewayService();
44
+ const systemInstall = Boolean(opts.system || service.label === "systemd system service");
45
+ if (opts.system && process.platform !== "linux") {
46
+ fail("--system is supported on Linux VPS Hosting installs only.");
47
+ return;
48
+ }
43
49
  let loaded = false;
44
50
  try {
45
51
  loaded = await service.isLoaded({ env: process.env });
@@ -113,7 +119,10 @@ async function runDaemonInstall(opts) {
113
119
  }
114
120
  }
115
121
  const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
116
- env: process.env,
122
+ env: systemInstall ? {
123
+ ...process.env,
124
+ FASED_GATEWAY_MODE: "managed"
125
+ } : process.env,
117
126
  port,
118
127
  token,
119
128
  runtime: runtimeRaw,
@@ -124,30 +133,45 @@ async function runDaemonInstall(opts) {
124
133
  defaultRuntime.log(message);
125
134
  }
126
135
  },
127
- config: cfg
136
+ config: cfg,
137
+ startupMode: systemInstall ? "managed-up" : undefined
128
138
  });
129
139
  try {
130
- await service.install({
131
- env: process.env,
132
- stdout,
133
- programArguments,
134
- workingDirectory,
135
- environment
136
- });
140
+ if (systemInstall) {
141
+ const installed = await installHostedSystemdService({
142
+ programArguments,
143
+ workingDirectory,
144
+ environment
145
+ });
146
+ if (!json) {
147
+ defaultRuntime.log(`Installed hosted system service: ${installed.unitPath}`);
148
+ }
149
+ } else {
150
+ await service.install({
151
+ env: process.env,
152
+ stdout,
153
+ programArguments,
154
+ workingDirectory,
155
+ environment
156
+ });
157
+ }
137
158
  } catch (err) {
138
159
  fail(`Gateway install failed: ${String(err)}`);
139
160
  return;
140
161
  }
162
+ const installedService = systemInstall ? resolveGatewayService() : service;
141
163
  let installed = true;
142
- try {
143
- installed = await service.isLoaded({ env: process.env });
144
- } catch {
145
- installed = true;
164
+ if (!systemInstall) {
165
+ try {
166
+ installed = await installedService.isLoaded({ env: process.env });
167
+ } catch {
168
+ installed = true;
169
+ }
146
170
  }
147
171
  emit({
148
172
  ok: true,
149
173
  result: "installed",
150
- service: buildDaemonServiceSnapshot(service, installed),
174
+ service: buildDaemonServiceSnapshot(installedService, installed),
151
175
  warnings: warnings.length ? warnings : undefined
152
176
  });
153
177
  }
@@ -34,7 +34,7 @@ function addGatewayServiceCommands(parent, opts) {
34
34
  json: Boolean(cmdOpts.json)
35
35
  });
36
36
  });
37
- parent.command("install").description("Install the Gateway service (launchd/systemd/schtasks)").option("--port <port>", "Gateway port").option("--runtime <runtime>", "Daemon runtime (node|bun). Default: node").option("--token <token>", "Gateway token (token auth)").option("--force", "Reinstall/overwrite if already installed", false).option("--json", "Output JSON", false).action(async (cmdOpts, command) => {
37
+ parent.command("install").description("Install the Gateway service (launchd/systemd/schtasks)").option("--port <port>", "Gateway port").option("--runtime <runtime>", "Daemon runtime (node|bun). Default: node").option("--token <token>", "Gateway token (token auth)").option("--force", "Reinstall/overwrite if already installed", false).option("--system", "Install the root-managed VPS Hosting service", false).option("--json", "Output JSON", false).action(async (cmdOpts, command) => {
38
38
  await runDaemonInstall(resolveInstallOptions(cmdOpts, command));
39
39
  });
40
40
  parent.command("uninstall").description("Uninstall the Gateway service (launchd/systemd/schtasks)").option("--json", "Output JSON", false).action(async (cmdOpts) => {
@@ -0,0 +1,13 @@
1
+ import { resolveGatewayService } from "../../daemon/service.js";
2
+
3
+ //#region src/cli/update-cli/service-target.ts
4
+ async function resolveUpdateGatewayServiceTarget() {
5
+ const service = resolveGatewayService();
6
+ return {
7
+ scope: service.label === "systemd system service" ? "system" : "platform",
8
+ service
9
+ };
10
+ }
11
+
12
+ //#endregion
13
+ export { resolveUpdateGatewayServiceTarget };
@@ -7,8 +7,8 @@ import { replaceCliName, resolveCliName } from "../cli-name.js";
7
7
  import { formatCliCommand } from "../command-format.js";
8
8
  import { readConfigFileSnapshot, writeConfigFile } from "../../config/io.js";
9
9
  import "../../config/config.js";
10
+ import { loadGatewayTlsRuntime } from "../../infra/tls/gateway.js";
10
11
  import { stylePromptMessage } from "../../terminal/prompt-style.js";
11
- import { resolveGatewayService } from "../../daemon/service.js";
12
12
  import { runDaemonInstall } from "../daemon-cli/install.js";
13
13
  import { renderRestartDiagnostics, terminateStaleGatewayPids, waitForGatewayHealthyRestart } from "../daemon-cli/restart-health.js";
14
14
  import { runDaemonRestart } from "../daemon-cli/lifecycle.js";
@@ -19,13 +19,14 @@ import { DEFAULT_GIT_CHANNEL, DEFAULT_PACKAGE_CHANNEL, channelToNpmTag, normaliz
19
19
  import { cleanupGlobalRenameDirs, globalInstallArgs, resolveGlobalPackageRoot, resolveNodeModulesRootForPackageRoot } from "../../infra/update-global.js";
20
20
  import { checkUpdateStatus, compareSemverStrings, resolveNpmChannelTag } from "../../infra/update-check.js";
21
21
  import { readPackageName, readPackageVersion } from "../../infra/package-json.js";
22
- import { runGatewayUpdate } from "../../infra/update-runner.js";
22
+ import { finalizeUpdateTransaction, rollbackUpdateTransaction, runGatewayUpdate } from "../../infra/update-runner.js";
23
23
  import { checkShellCompletionStatus, ensureCompletionCacheExists } from "../../commands/doctor-completion.js";
24
24
  import { runPostUpdateDoctorRepair } from "../../commands/doctor-update.js";
25
25
  import { doctorCommand } from "../../commands/doctor.js";
26
26
  import { DEFAULT_PACKAGE_NAME, createGlobalCommandRunner, ensureGitCheckout, normalizeTag, parseTimeoutMsOrExit, resolveGitInstallDir, resolveGlobalManager, resolveNodeRunner, resolveTargetVersion, resolveUpdateRoot, runUpdateStep, tryWriteCompletionCache } from "./shared.js";
27
27
  import { createUpdateProgress, printResult } from "./progress.js";
28
28
  import { prepareRestartScript, runRestartScript } from "./restart-helper.js";
29
+ import { resolveUpdateGatewayServiceTarget } from "./service-target.js";
29
30
  import { suppressDeprecations } from "./suppress-deprecations.js";
30
31
  import path from "node:path";
31
32
  import { confirm, isCancel } from "@clack/prompts";
@@ -265,6 +266,17 @@ async function runPackageInstallUpdate(params) {
265
266
  steps.push(doctorStep);
266
267
  }
267
268
  }
269
+ const expectedVersion = params.tag.trim().replace(/^v/, "");
270
+ if (pkgRoot && compareSemverStrings(expectedVersion, expectedVersion) === 0 && compareSemverStrings(afterVersion, expectedVersion) !== 0) {
271
+ steps.push({
272
+ name: "version verify",
273
+ command: `verify ${packageName}@${expectedVersion}`,
274
+ cwd: pkgRoot,
275
+ durationMs: 0,
276
+ exitCode: 1,
277
+ stderrTail: `expected ${expectedVersion}, found ${afterVersion ?? "unknown"}`
278
+ });
279
+ }
268
280
  const failedStep = steps.find((step) => step.exitCode !== 0);
269
281
  return {
270
282
  status: failedStep ? "error" : "ok",
@@ -455,52 +467,68 @@ async function maybeRestartService(params) {
455
467
  }
456
468
  }
457
469
  }
458
- if (params.restartScriptPath) {
470
+ if (params.serviceTarget.scope === "system" && params.serviceLoaded) {
471
+ await params.serviceTarget.service.restart({
472
+ env: process.env,
473
+ stdout: process.stdout
474
+ });
475
+ restartInitiated = true;
476
+ } else if (params.restartScriptPath) {
459
477
  await runRestartScript(params.restartScriptPath);
460
478
  restartInitiated = true;
461
- } else {
479
+ } else if (params.serviceTarget.scope === "platform") {
462
480
  restarted = await runDaemonRestart();
481
+ restartInitiated = restarted;
463
482
  }
464
- if (!params.opts.json && restarted) {
465
- defaultRuntime.log(theme.success("Daemon restarted successfully."));
466
- defaultRuntime.log("");
467
- process.env.FASED_UPDATE_IN_PROGRESS = "1";
468
- try {
469
- const interactiveDoctor = Boolean(process.stdin.isTTY) && !params.opts.json && params.opts.yes !== true;
470
- await doctorCommand(defaultRuntime, { nonInteractive: !interactiveDoctor });
471
- } catch (err) {
472
- defaultRuntime.log(theme.warn(`Doctor failed: ${String(err)}`));
473
- } finally {
474
- delete process.env.FASED_UPDATE_IN_PROGRESS;
475
- }
476
- }
477
- if (!params.opts.json && restartInitiated) {
478
- const service = resolveGatewayService();
483
+ if (restartInitiated) {
479
484
  let health = await waitForGatewayHealthyRestart({
480
- service,
481
- port: params.gatewayPort
485
+ service: params.serviceTarget.service,
486
+ port: params.gatewayPort,
487
+ rpc: params.rpc
482
488
  });
483
489
  if (!health.healthy && health.staleGatewayPids.length > 0) {
484
490
  if (!params.opts.json) {
485
491
  defaultRuntime.log(theme.warn(`Found stale gateway process(es) after restart: ${health.staleGatewayPids.join(", ")}. Cleaning up...`));
486
492
  }
487
493
  await terminateStaleGatewayPids(health.staleGatewayPids);
488
- await runDaemonRestart();
494
+ await params.serviceTarget.service.restart({
495
+ env: process.env,
496
+ stdout: process.stdout
497
+ });
489
498
  health = await waitForGatewayHealthyRestart({
490
- service,
491
- port: params.gatewayPort
499
+ service: params.serviceTarget.service,
500
+ port: params.gatewayPort,
501
+ rpc: params.rpc
492
502
  });
493
503
  }
494
504
  if (health.healthy) {
495
- defaultRuntime.log(theme.success("Daemon restart completed."));
505
+ if (!params.opts.json) {
506
+ defaultRuntime.log(theme.success("Daemon restart completed."));
507
+ }
496
508
  } else {
497
- defaultRuntime.log(theme.warn("Gateway did not become healthy after restart."));
498
- for (const line of renderRestartDiagnostics(health)) {
499
- defaultRuntime.log(theme.muted(line));
509
+ partial = true;
510
+ if (!params.opts.json) {
511
+ defaultRuntime.log(theme.warn("Gateway did not become healthy after restart."));
512
+ for (const line of renderRestartDiagnostics(health)) {
513
+ defaultRuntime.log(theme.muted(line));
514
+ }
515
+ defaultRuntime.log(theme.muted(`Run \`${replaceCliName(formatCliCommand("fased gateway status --deep"), CLI_NAME)}\` for details.`));
500
516
  }
501
- defaultRuntime.log(theme.muted(`Run \`${replaceCliName(formatCliCommand("fased gateway status --deep"), CLI_NAME)}\` for details.`));
502
517
  }
503
- defaultRuntime.log("");
518
+ if (!params.opts.json) {
519
+ defaultRuntime.log("");
520
+ }
521
+ }
522
+ if (!partial && restarted && !params.opts.json) {
523
+ process.env.FASED_UPDATE_IN_PROGRESS = "1";
524
+ try {
525
+ const interactiveDoctor = Boolean(process.stdin.isTTY) && params.opts.yes !== true;
526
+ await doctorCommand(defaultRuntime, { nonInteractive: !interactiveDoctor });
527
+ } catch (err) {
528
+ defaultRuntime.log(theme.warn(`Doctor failed: ${String(err)}`));
529
+ } finally {
530
+ delete process.env.FASED_UPDATE_IN_PROGRESS;
531
+ }
504
532
  }
505
533
  } catch (err) {
506
534
  partial = true;
@@ -509,7 +537,10 @@ async function maybeRestartService(params) {
509
537
  defaultRuntime.log(theme.muted(`You may need to restart the service manually: ${replaceCliName(formatCliCommand("fased gateway restart"), CLI_NAME)}`));
510
538
  }
511
539
  }
512
- return { partial };
540
+ return {
541
+ partial,
542
+ healthy: !partial
543
+ };
513
544
  }
514
545
  if (!params.opts.json) {
515
546
  defaultRuntime.log("");
@@ -519,7 +550,10 @@ async function maybeRestartService(params) {
519
550
  defaultRuntime.log(theme.muted(`Tip: Run \`${replaceCliName(formatCliCommand("fased gateway restart"), CLI_NAME)}\` to apply updates to a running gateway.`));
520
551
  }
521
552
  }
522
- return { partial };
553
+ return {
554
+ partial,
555
+ healthy: true
556
+ };
523
557
  }
524
558
  async function updateCommand(opts) {
525
559
  suppressDeprecations();
@@ -678,12 +712,16 @@ async function updateCommand(opts) {
678
712
  const startedAt = Date.now();
679
713
  let restartScriptPath = null;
680
714
  let refreshGatewayServiceEnv = false;
715
+ let serviceLoaded = false;
716
+ const serviceTarget = await resolveUpdateGatewayServiceTarget();
681
717
  if (shouldRestart) {
682
718
  try {
683
- const loaded = await resolveGatewayService().isLoaded({ env: process.env });
684
- if (loaded) {
685
- restartScriptPath = await prepareRestartScript(process.env);
686
- refreshGatewayServiceEnv = true;
719
+ serviceLoaded = await serviceTarget.service.isLoaded({ env: process.env });
720
+ if (serviceLoaded) {
721
+ if (serviceTarget.scope === "platform") {
722
+ restartScriptPath = await prepareRestartScript(process.env);
723
+ refreshGatewayServiceEnv = true;
724
+ }
687
725
  }
688
726
  } catch {}
689
727
  }
@@ -727,6 +765,54 @@ async function updateCommand(opts) {
727
765
  defaultRuntime.exit(0);
728
766
  return;
729
767
  }
768
+ const gatewayPort = resolveGatewayPort(configSnapshot.valid ? configSnapshot.config : undefined);
769
+ const gatewayConfig = configSnapshot.valid ? configSnapshot.config.gateway : undefined;
770
+ const tlsRuntime = await loadGatewayTlsRuntime(gatewayConfig?.tls);
771
+ const restartRpc = {
772
+ url: `${tlsRuntime.enabled ? "wss" : "ws"}://127.0.0.1:${gatewayPort}`,
773
+ token: gatewayConfig?.auth?.token,
774
+ password: gatewayConfig?.auth?.password,
775
+ tlsFingerprint: tlsRuntime.enabled ? tlsRuntime.fingerprintSha256 : undefined,
776
+ timeoutMs: 5e3
777
+ };
778
+ const restartResult = await maybeRestartService({
779
+ shouldRestart,
780
+ result,
781
+ opts,
782
+ refreshServiceEnv: refreshGatewayServiceEnv,
783
+ gatewayPort,
784
+ restartScriptPath,
785
+ serviceTarget,
786
+ serviceLoaded,
787
+ rpc: restartRpc
788
+ });
789
+ if (!restartResult.healthy) {
790
+ if (result.transaction) {
791
+ if (!opts.json) {
792
+ defaultRuntime.log(theme.warn("Gateway verification failed; restoring the previous runtime."));
793
+ }
794
+ try {
795
+ await rollbackUpdateTransaction(result.transaction);
796
+ await serviceTarget.service.restart({
797
+ env: process.env,
798
+ stdout: process.stdout
799
+ });
800
+ const rollbackHealth = await waitForGatewayHealthyRestart({
801
+ service: serviceTarget.service,
802
+ port: gatewayPort,
803
+ rpc: restartRpc
804
+ });
805
+ if (!rollbackHealth.healthy) {
806
+ defaultRuntime.error("Previous runtime was restored, but the gateway is not healthy.");
807
+ }
808
+ } catch (error) {
809
+ defaultRuntime.error(`Automatic runtime rollback failed: ${String(error)}`);
810
+ }
811
+ }
812
+ defaultRuntime.exit(1);
813
+ return;
814
+ }
815
+ await finalizeUpdateTransaction(result.transaction);
730
816
  await updatePluginsAfterCoreUpdate({
731
817
  root,
732
818
  channel,
@@ -738,14 +824,6 @@ async function updateCommand(opts) {
738
824
  jsonMode: Boolean(opts.json),
739
825
  skipPrompt: Boolean(opts.yes)
740
826
  });
741
- const restartResult = await maybeRestartService({
742
- shouldRestart,
743
- result,
744
- opts,
745
- refreshServiceEnv: refreshGatewayServiceEnv,
746
- gatewayPort: resolveGatewayPort(configSnapshot.valid ? configSnapshot.config : undefined),
747
- restartScriptPath
748
- });
749
827
  if (!opts.json && !restartResult.partial) {
750
828
  defaultRuntime.log(theme.muted(pickUpdateQuip()));
751
829
  }
@@ -59,6 +59,9 @@ async function buildGatewayInstallPlan(params) {
59
59
  });
60
60
  const environment = { ...collectConfigServiceEnvVars(params.config) };
61
61
  Object.assign(environment, serviceEnvironment);
62
+ if (startupMode === "managed-up") {
63
+ environment.FASED_GATEWAY_MODE = "managed";
64
+ }
62
65
  const serviceNodeProgram = programArguments[0];
63
66
  if (typeof serviceNodeProgram === "string" && serviceNodeProgram.includes(path.sep) && path.basename(serviceNodeProgram).toLowerCase().startsWith("node")) {
64
67
  environment.FASED_NODE_BIN = serviceNodeProgram;
@@ -1,6 +1,7 @@
1
1
  import { installLaunchAgent, isLaunchAgentLoaded, readLaunchAgentProgramArguments, readLaunchAgentRuntime, restartLaunchAgent, stopLaunchAgent, uninstallLaunchAgent } from "./launchd.js";
2
2
  import { installScheduledTask, isScheduledTaskInstalled, readScheduledTaskCommand, readScheduledTaskRuntime, restartScheduledTask, stopScheduledTask, uninstallScheduledTask } from "./schtasks.js";
3
3
  import { installSystemdService, isSystemdServiceEnabled, readSystemdServiceExecStart, readSystemdServiceRuntime, restartSystemdService, stopSystemdService, uninstallSystemdService } from "./systemd.js";
4
+ import { resolveHostedSystemdService } from "./systemd-system.js";
4
5
 
5
6
  //#region src/daemon/service.ts
6
7
  function ignoreInstallResult(install) {
@@ -24,6 +25,10 @@ function resolveGatewayService() {
24
25
  };
25
26
  }
26
27
  if (process.platform === "linux") {
28
+ const hostedSystemService = resolveHostedSystemdService();
29
+ if (hostedSystemService) {
30
+ return hostedSystemService;
31
+ }
27
32
  return {
28
33
  label: "systemd",
29
34
  loadedText: "enabled",
@@ -0,0 +1,237 @@
1
+ import { execFileUtf8 } from "./exec-file.js";
2
+ import { buildSystemdUnit, parseSystemdEnvAssignment, parseSystemdExecStart } from "./systemd-unit.js";
3
+ import { parseSystemdShow } from "./systemd.js";
4
+ import { existsSync } from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import fs$1 from "node:fs/promises";
8
+ import { spawn } from "node:child_process";
9
+
10
+ //#region src/daemon/systemd-system.ts
11
+ const SERVICE_NAME = "fased-gateway";
12
+ const INSTALL_HELPER = "/usr/local/sbin/fased-install-gateway-service";
13
+ const SYSTEM_UNIT_CANDIDATES = [
14
+ `/etc/systemd/system/${SERVICE_NAME}.service`,
15
+ `/usr/lib/systemd/system/${SERVICE_NAME}.service`,
16
+ `/lib/systemd/system/${SERVICE_NAME}.service`
17
+ ];
18
+ function findHostedSystemdUnitPath(candidates = SYSTEM_UNIT_CANDIDATES) {
19
+ if (process.platform !== "linux") {
20
+ return null;
21
+ }
22
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
23
+ }
24
+ async function readHostedSystemdCommand(unitPath) {
25
+ try {
26
+ const content = await fs$1.readFile(unitPath, "utf8");
27
+ let workingDirectory = "";
28
+ let execStart = "";
29
+ const environment = {};
30
+ for (const rawLine of content.split("\n")) {
31
+ const line = rawLine.trim();
32
+ if (line.startsWith("ExecStart=")) {
33
+ execStart = line.slice("ExecStart=".length).trim();
34
+ } else if (line.startsWith("WorkingDirectory=")) {
35
+ workingDirectory = line.slice("WorkingDirectory=".length).trim();
36
+ } else if (line.startsWith("Environment=")) {
37
+ const parsed = parseSystemdEnvAssignment(line.slice("Environment=".length).trim());
38
+ if (parsed) {
39
+ environment[parsed.key] = parsed.value;
40
+ }
41
+ }
42
+ }
43
+ if (!execStart) {
44
+ return null;
45
+ }
46
+ return {
47
+ programArguments: parseSystemdExecStart(execStart),
48
+ ...workingDirectory ? { workingDirectory } : {},
49
+ ...Object.keys(environment).length > 0 ? { environment } : {},
50
+ sourcePath: unitPath
51
+ };
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ function resolveHostedSystemdService() {
57
+ const unitPath = findHostedSystemdUnitPath();
58
+ if (!unitPath) {
59
+ return null;
60
+ }
61
+ const runSystemctl = async (args) => await execFileUtf8("systemctl", args);
62
+ const runPrivilegedSystemctl = async (args) => await execFileUtf8("sudo", [
63
+ "-n",
64
+ "systemctl",
65
+ ...args
66
+ ]);
67
+ const control = async (action, stdout) => {
68
+ const args = action === "restart" ? [
69
+ action,
70
+ "--no-block",
71
+ `${SERVICE_NAME}.service`
72
+ ] : [action, `${SERVICE_NAME}.service`];
73
+ const result = await runPrivilegedSystemctl(args);
74
+ if (result.code !== 0) {
75
+ throw new Error(result.stderr || result.stdout || `system service ${action} failed`);
76
+ }
77
+ stdout.write(`${action === "restart" ? "Restarted" : "Stopped"} system service: ${SERVICE_NAME}.service\n`);
78
+ };
79
+ return {
80
+ label: "systemd system service",
81
+ loadedText: "enabled",
82
+ notLoadedText: "disabled",
83
+ install: async ({ programArguments, workingDirectory, environment }) => {
84
+ await installHostedSystemdService({
85
+ programArguments,
86
+ workingDirectory,
87
+ environment
88
+ });
89
+ },
90
+ uninstall: async () => {
91
+ throw new Error("Hosted system service removal requires the VPS Hosting installer.");
92
+ },
93
+ stop: async ({ stdout }) => await control("stop", stdout),
94
+ restart: async ({ stdout }) => await control("restart", stdout),
95
+ isLoaded: async () => (await runSystemctl(["is-enabled", `${SERVICE_NAME}.service`])).code === 0,
96
+ readCommand: async () => await readHostedSystemdCommand(unitPath),
97
+ readRuntime: async () => {
98
+ const result = await runSystemctl([
99
+ "show",
100
+ `${SERVICE_NAME}.service`,
101
+ "--no-page",
102
+ "--property",
103
+ "ActiveState,SubState,MainPID,ExecMainStatus,ExecMainCode"
104
+ ]);
105
+ if (result.code !== 0) {
106
+ return {
107
+ status: "unknown",
108
+ detail: result.stderr || result.stdout || undefined
109
+ };
110
+ }
111
+ const parsed = parseSystemdShow(result.stdout);
112
+ const activeState = parsed.activeState?.toLowerCase();
113
+ return {
114
+ status: activeState === "active" ? "running" : activeState ? "stopped" : "unknown",
115
+ state: parsed.activeState,
116
+ subState: parsed.subState,
117
+ pid: parsed.mainPid,
118
+ lastExitStatus: parsed.execMainStatus,
119
+ lastExitReason: parsed.execMainCode
120
+ };
121
+ }
122
+ };
123
+ }
124
+ function resolveRunAsUser() {
125
+ const user = process.env.USER?.trim() || process.env.LOGNAME?.trim() || os.userInfo().username;
126
+ if (!user || user === "root" || !/^[A-Za-z0-9_.@-]+$/.test(user)) {
127
+ throw new Error("Hosted gateway repair must run as the non-root Fased app user.");
128
+ }
129
+ return user;
130
+ }
131
+ function buildHostedSystemdUnit(params) {
132
+ const baseUnit = buildSystemdUnit({
133
+ description: "Fased Gateway (managed)",
134
+ programArguments: params.programArguments,
135
+ workingDirectory: params.workingDirectory,
136
+ environment: params.environment
137
+ });
138
+ const lines = baseUnit.split("\n");
139
+ const serviceIndex = lines.findIndex((line) => line.trim() === "[Service]");
140
+ if (serviceIndex !== -1) {
141
+ lines.splice(serviceIndex + 1, 0, "Type=simple", `User=${params.runAsUser}`, `Group=${params.runAsUser}`);
142
+ }
143
+ const installIndex = lines.findIndex((line) => line.trim() === "[Install]");
144
+ if (installIndex !== -1) {
145
+ lines.splice(installIndex, 0, "NoNewPrivileges=true", "PrivateTmp=true");
146
+ }
147
+ const wantedByIndex = lines.findIndex((line) => line.trim() === "WantedBy=default.target");
148
+ if (wantedByIndex !== -1) {
149
+ lines[wantedByIndex] = "WantedBy=multi-user.target";
150
+ }
151
+ return lines.join("\n");
152
+ }
153
+ async function runHelper(unit, runAsUser) {
154
+ await fs$1.access(INSTALL_HELPER);
155
+ await new Promise((resolve, reject) => {
156
+ const child = spawn("sudo", [
157
+ "-n",
158
+ INSTALL_HELPER,
159
+ SERVICE_NAME,
160
+ runAsUser
161
+ ], { stdio: [
162
+ "pipe",
163
+ "pipe",
164
+ "pipe"
165
+ ] });
166
+ let stdout = "";
167
+ let stderr = "";
168
+ const timer = setTimeout(() => {
169
+ child.kill("SIGTERM");
170
+ reject(new Error("hosted gateway service helper timed out"));
171
+ }, 6e4);
172
+ child.stdout.on("data", (chunk) => {
173
+ stdout += String(chunk);
174
+ });
175
+ child.stderr.on("data", (chunk) => {
176
+ stderr += String(chunk);
177
+ });
178
+ child.on("error", (error) => {
179
+ clearTimeout(timer);
180
+ reject(error);
181
+ });
182
+ child.on("close", (code) => {
183
+ clearTimeout(timer);
184
+ if (code === 0) {
185
+ resolve();
186
+ } else {
187
+ reject(new Error((stderr || stdout || `service helper exited ${code}`).trim()));
188
+ }
189
+ });
190
+ child.stdin.end(unit);
191
+ });
192
+ }
193
+ async function installHostedSystemdService(params) {
194
+ if (process.platform !== "linux") {
195
+ throw new Error("Hosted system service repair is supported on Linux only.");
196
+ }
197
+ const runAsUser = resolveRunAsUser();
198
+ const unit = buildHostedSystemdUnit({
199
+ ...params,
200
+ runAsUser
201
+ });
202
+ await spawnCommand("systemctl", [
203
+ "--user",
204
+ "disable",
205
+ "--now",
206
+ `${SERVICE_NAME}.service`
207
+ ]).catch(() => undefined);
208
+ const userUnit = path.join(os.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
209
+ await fs$1.rm(userUnit, { force: true }).catch(() => undefined);
210
+ await fs$1.rm(`${userUnit}.d`, {
211
+ recursive: true,
212
+ force: true
213
+ }).catch(() => undefined);
214
+ await spawnCommand("systemctl", ["--user", "daemon-reload"]).catch(() => undefined);
215
+ await runHelper(unit, runAsUser);
216
+ return { unitPath: `/etc/systemd/system/${SERVICE_NAME}.service` };
217
+ }
218
+ async function spawnCommand(command, args) {
219
+ await new Promise((resolve, reject) => {
220
+ const child = spawn(command, args, { stdio: "ignore" });
221
+ child.on("error", reject);
222
+ child.on("close", (code) => {
223
+ if (code === 0) {
224
+ resolve();
225
+ } else {
226
+ reject(new Error(`${command} ${args.join(" ")} exited ${code}`));
227
+ }
228
+ });
229
+ });
230
+ }
231
+ const __testing = {
232
+ readHostedSystemdCommand,
233
+ resolveRunAsUser
234
+ };
235
+
236
+ //#endregion
237
+ export { installHostedSystemdService, resolveHostedSystemdService };
@@ -3,8 +3,8 @@ import { execFileUtf8 } from "./exec-file.js";
3
3
  import { formatLine, toPosixPath, writeFormattedLines } from "./output.js";
4
4
  import { resolveHomeDir } from "./paths.js";
5
5
  import { parseKeyValueOutput } from "./runtime-parse.js";
6
- import { enableSystemdUserLinger, readSystemdUserLingerStatus } from "./systemd-linger.js";
7
6
  import { buildSystemdUnit, parseSystemdEnvAssignment, parseSystemdExecStart } from "./systemd-unit.js";
7
+ import { enableSystemdUserLinger, readSystemdUserLingerStatus } from "./systemd-linger.js";
8
8
  import path from "node:path";
9
9
  import fs from "node:fs/promises";
10
10
 
@@ -354,4 +354,4 @@ async function uninstallLegacySystemdUnits({ env, stdout }) {
354
354
  }
355
355
 
356
356
  //#endregion
357
- export { installSystemdService, isSystemdServiceEnabled, isSystemdUserServiceAvailable, readSystemdServiceExecStart, readSystemdServiceRuntime, resolveSystemdUserUnitPath, restartSystemdService, stopSystemdService, uninstallLegacySystemdUnits, uninstallSystemdService };
357
+ export { installSystemdService, isSystemdServiceEnabled, isSystemdUserServiceAvailable, parseSystemdShow, readSystemdServiceExecStart, readSystemdServiceRuntime, resolveSystemdUserUnitPath, restartSystemdService, stopSystemdService, uninstallLegacySystemdUnits, uninstallSystemdService };