@stacksjs/ts-cloud 0.5.32 → 0.5.34

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.
@@ -25,23 +25,36 @@ export interface FrameworkExecContext {
25
25
  export interface AppFrameworkDriver {
26
26
  readonly id: FrameworkId;
27
27
  /**
28
- * Wrap a bare command so a systemd unit / cron entry runs it with the
29
- * framework's runtime on PATH (systemd units inherit no shell env).
28
+ * Wrap a command for a systemd unit / cron entry. Prefer a BARE command +
29
+ * {@link AppFrameworkDriver.execEnv} (like the app's own service) so starting
30
+ * a long-lived unit over SSH doesn't hold the deploy channel open; only wrap
31
+ * in a shell when the env must be computed at runtime (e.g. Laravel/pantry).
30
32
  */
31
33
  wrapExec: (command: string) => string;
34
+ /** Static `Environment=` vars for a unit (systemd inherits no shell env). */
35
+ readonly execEnv?: Record<string, string>;
32
36
  /**
33
- * The command cron runs every minute to fire due scheduled tasks. Includes
34
- * its own env setup + output redirection; the reconciler may append a
35
- * heartbeat ping with `&&`.
37
+ * How the app scheduler runs:
38
+ * - `'cron'` → `schedule:run` is one-shot; run it every minute via cron (Laravel).
39
+ * - `'daemon'` `schedule:run` is long-lived (holds in-process timers); run it
40
+ * as a single always-on systemd unit (Stacks).
41
+ */
42
+ readonly schedulerMode: 'cron' | 'daemon';
43
+ /**
44
+ * The scheduler command WITHOUT output redirection. For `'cron'` it is run
45
+ * from cron and sets up its own cwd/env; for `'daemon'` it becomes a unit's
46
+ * ExecStart (systemd supplies WorkingDirectory), so no `cd` is needed.
36
47
  */
37
48
  schedulerCommand: (ctx: FrameworkExecContext) => string;
38
49
  /** `ExecStart` for a single queue-worker process. */
39
50
  queueWorkerCommand: (worker: QueueWorkerConfig, ctx: FrameworkExecContext) => string;
40
51
  }
41
52
  /**
42
- * Stacks (Bun) — the default. The scheduler is cron-driven (`buddy schedule:run`
43
- * is one-shot); queue workers are long-running (`buddy queue:work`). bun lives
44
- * at an absolute path installed by the box bootstrap.
53
+ * Stacks (Bun) — the default. bun lives at an absolute path installed by the box
54
+ * bootstrap, so units use a BARE ExecStart (no shell wrapper) + `Environment=`
55
+ * for PATH/BUN_INSTALL matching the app's own service. This matters: wrapping
56
+ * a long-lived unit in `/bin/sh -lc` makes `systemctl restart` over SSH hold the
57
+ * deploy channel open and hang the deploy.
45
58
  */
46
59
  export declare const stacksDriver: AppFrameworkDriver;
47
60
  /** Laravel (PHP / Artisan) — the original Forge-style behavior, now a driver. */
package/dist/index.js CHANGED
@@ -87741,13 +87741,12 @@ function resolveSiteFramework(site) {
87741
87741
  var PANTRY_ENV_EVAL = `eval "$(cd ${PANTRY_PROJECT_DIR} && pantry env 2>/dev/null)"`;
87742
87742
  var BUN_BIN = "/usr/local/bin/bun";
87743
87743
  var STACKS_CLI = "storage/framework/core/buddy/src/cli.ts";
87744
- function bunEnvWrap(command) {
87745
- return `/bin/sh -lc 'export PATH="/usr/local/bin:$PATH"; export BUN_INSTALL="/root/.bun"; exec ${command}'`;
87746
- }
87747
87744
  var stacksDriver = {
87748
87745
  id: "stacks",
87749
- wrapExec: bunEnvWrap,
87750
- schedulerCommand: ({ current }) => `cd ${current} && ${BUN_BIN} ${STACKS_CLI} schedule:run >> /dev/null 2>&1`,
87746
+ wrapExec: (command) => command,
87747
+ execEnv: { PATH: "/usr/local/bin:/usr/bin:/bin", BUN_INSTALL: "/root/.bun" },
87748
+ schedulerMode: "daemon",
87749
+ schedulerCommand: () => `${BUN_BIN} ${STACKS_CLI} schedule:run`,
87751
87750
  queueWorkerCommand: (worker, { current }) => {
87752
87751
  const flags = [
87753
87752
  `--queue=${worker.queue || "default"}`,
@@ -87761,7 +87760,8 @@ var stacksDriver = {
87761
87760
  var laravelDriver = {
87762
87761
  id: "laravel",
87763
87762
  wrapExec: (command) => `/bin/sh -lc '${PANTRY_ENV_EVAL}; exec ${command}'`,
87764
- schedulerCommand: ({ current }) => `cd ${current} && ${PANTRY_ENV_EVAL} && php artisan schedule:run >> /dev/null 2>&1`,
87763
+ schedulerMode: "cron",
87764
+ schedulerCommand: ({ current }) => `cd ${current} && ${PANTRY_ENV_EVAL} && php artisan schedule:run`,
87765
87765
  queueWorkerCommand: (worker, { current }) => {
87766
87766
  const artisan = `${current}/artisan`;
87767
87767
  if (worker.horizon)
@@ -87814,6 +87814,7 @@ function systemdUnit(opts) {
87814
87814
  "[Service]",
87815
87815
  "Type=simple",
87816
87816
  `WorkingDirectory=${opts.workingDir}`,
87817
+ ...Object.entries(opts.environment ?? {}).map(([k, v]) => `Environment="${k}=${v}"`),
87817
87818
  `ExecStart=${opts.execStart}`,
87818
87819
  `Restart=${opts.restart || "always"}`,
87819
87820
  "RestartSec=5"
@@ -87855,6 +87856,7 @@ function buildSiteServicesScript(options) {
87855
87856
  description: `${siteName} queue worker ${qIndex}.${p} (managed by ts-cloud)`,
87856
87857
  workingDir: current,
87857
87858
  execStart: driver.wrapExec(driver.queueWorkerCommand(worker, ctx)),
87859
+ environment: driver.execEnv,
87858
87860
  stopWaitSecs: worker.stopWaitSecs ?? 90
87859
87861
  })));
87860
87862
  }
@@ -87869,22 +87871,33 @@ function buildSiteServicesScript(options) {
87869
87871
  description: `${siteName} daemon ${daemon.name || daemon.command} (managed by ts-cloud)`,
87870
87872
  workingDir: daemon.directory || current,
87871
87873
  execStart: driver.wrapExec(daemon.command),
87874
+ environment: driver.execEnv,
87872
87875
  restart: daemon.restart,
87873
87876
  user: daemon.user
87874
87877
  })));
87875
87878
  }
87876
87879
  });
87880
+ const scheduler2 = site.scheduler;
87881
+ const schedulerEnabled = scheduler2 === true || typeof scheduler2 === "object" && scheduler2 !== null;
87882
+ const schedulerUnit = `${slug}-${siteName}-scheduler`;
87883
+ if (schedulerEnabled && driver.schedulerMode === "daemon") {
87884
+ desiredUnits.push(schedulerUnit);
87885
+ out.push(...writeUnitScript(schedulerUnit, systemdUnit({
87886
+ description: `${siteName} scheduler (managed by ts-cloud)`,
87887
+ workingDir: current,
87888
+ execStart: driver.wrapExec(driver.schedulerCommand(ctx)),
87889
+ environment: driver.execEnv
87890
+ })));
87891
+ }
87877
87892
  const desiredList = desiredUnits.map((n) => `${n}.service`).join(" ");
87878
- out.push("systemctl daemon-reload", `TS_CLOUD_DESIRED="${desiredList}"`, `for unit in $(ls /etc/systemd/system/ 2>/dev/null | grep -E '^${reEscape(slug)}-${reEscape(siteName)}-(queue|daemon)-.*\\.service$' || true); do`, ' case " $TS_CLOUD_DESIRED " in', ' *" $unit "*) ;;', ' *) systemctl stop "$unit" 2>/dev/null || true; systemctl disable "$unit" 2>/dev/null || true; rm -f "/etc/systemd/system/$unit" ;;', " esac", "done", "systemctl daemon-reload");
87893
+ out.push("systemctl daemon-reload", `TS_CLOUD_DESIRED="${desiredList}"`, `for unit in $(ls /etc/systemd/system/ 2>/dev/null | grep -E '^${reEscape(slug)}-${reEscape(siteName)}-((queue|daemon)-.*|scheduler)\\.service$' || true); do`, ' case " $TS_CLOUD_DESIRED " in', ' *" $unit "*) ;;', ' *) systemctl stop "$unit" 2>/dev/null || true; systemctl disable "$unit" 2>/dev/null || true; rm -f "/etc/systemd/system/$unit" ;;', " esac", "done", "systemctl daemon-reload");
87879
87894
  for (const name of desiredUnits) {
87880
87895
  out.push(`systemctl enable ${name}.service`, `systemctl restart ${name}.service`);
87881
87896
  }
87882
87897
  const cronPath = schedulerCronPath(slug, siteName);
87883
- const scheduler2 = site.scheduler;
87884
- const schedulerEnabled = scheduler2 === true || typeof scheduler2 === "object" && scheduler2 !== null;
87885
- if (schedulerEnabled) {
87898
+ if (schedulerEnabled && driver.schedulerMode === "cron") {
87886
87899
  const heartbeat = typeof scheduler2 === "object" && scheduler2 !== null ? scheduler2 : undefined;
87887
- let command = driver.schedulerCommand(ctx);
87900
+ let command = `${driver.schedulerCommand(ctx)} >> /dev/null 2>&1`;
87888
87901
  if (heartbeat?.heartbeatUrl) {
87889
87902
  const method = heartbeat.heartbeatMethod || "GET";
87890
87903
  const methodFlag = method === "GET" ? "" : `-X ${method} `;
@@ -88031,12 +88044,12 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger2) {
88031
88044
  security: site.security
88032
88045
  }) : [];
88033
88046
  const sslScript = useNginx ? buildSslScript(site) : [];
88034
- const servicesScript = siteHasServices(site) ? buildSiteServicesScript({ slug, siteName, site, phpVersion, appBase }) : [];
88047
+ const servicesScript2 = siteHasServices(site) ? buildSiteServicesScript({ slug, siteName, site, phpVersion, appBase }) : [];
88035
88048
  const healthCheckScript = useNginx ? buildHealthCheckScript(site) : [];
88036
88049
  logger4.step(`Deploying PHP site '${siteName}' to ${targets.length} target(s)...`);
88037
88050
  const phpResult = await driver.runRemoteDeploy({
88038
88051
  targets,
88039
- commands: [...deployScript, ...poolScript, ...vhostScript, ...sslScript, ...servicesScript, ...healthCheckScript],
88052
+ commands: [...deployScript, ...poolScript, ...vhostScript, ...sslScript, ...servicesScript2, ...healthCheckScript],
88040
88053
  comment: `ts-cloud deploy ${slug}/${siteName}@${sha}`,
88041
88054
  tags: { Project: slug, Environment: environment, Role: "app" }
88042
88055
  });
@@ -88102,6 +88115,7 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger2) {
88102
88115
  security: site.security
88103
88116
  }) : [];
88104
88117
  const staticSsl = wantsNginxStatic ? buildSslScript(site) : [];
88118
+ const servicesScript = siteHasServices(site) ? buildSiteServicesScript({ slug, siteName, site, appBase: `/var/www/${siteName}` }) : [];
88105
88119
  const remoteScript = [
88106
88120
  ...buildDeployHistoryHeader(`/var/www/${siteName}`, {
88107
88121
  releaseId: sha,
@@ -88110,7 +88124,8 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger2) {
88110
88124
  }),
88111
88125
  ...baseScript,
88112
88126
  ...staticVhost,
88113
- ...staticSsl
88127
+ ...staticSsl,
88128
+ ...servicesScript
88114
88129
  ];
88115
88130
  logger4.step(`Deploying to ${targets.length} target(s)...`);
88116
88131
  const result = await driver.runRemoteDeploy({