@stacksjs/ts-cloud 0.5.31 → 0.5.33
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/dist/bin/cli.js +371 -371
- package/dist/drivers/shared/app-frameworks.d.ts +57 -0
- package/dist/drivers/shared/{laravel-services.d.ts → app-services.d.ts} +14 -7
- package/dist/index.js +84 -32
- package/dist/ui/index.html +3 -3
- package/dist/ui/server/actions.html +3 -3
- package/dist/ui/server/backups.html +3 -3
- package/dist/ui/server/database.html +3 -3
- package/dist/ui/server/deployments.html +3 -3
- package/dist/ui/server/logs.html +3 -3
- package/dist/ui/server/services.html +3 -3
- package/dist/ui/server/sites.html +3 -3
- package/dist/ui/server/ssh-keys.html +3 -3
- package/dist/ui/server/workers.html +3 -3
- package/dist/ui/serverless/logs.html +3 -3
- package/package.json +3 -3
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-framework drivers.
|
|
3
|
+
*
|
|
4
|
+
* ts-cloud runs a site's background work — the scheduler, queue workers, and
|
|
5
|
+
* daemons — as systemd units / cron on the box. HOW those are invoked differs
|
|
6
|
+
* per app framework: Stacks (Bun, `buddy …`) vs Laravel (PHP, `artisan …`), and
|
|
7
|
+
* the runtime environment a systemd/cron process needs differs too (bun on PATH
|
|
8
|
+
* vs pantry's php env). This module isolates those differences behind one small
|
|
9
|
+
* driver so the reconciler (`app-services.ts`) stays framework-agnostic.
|
|
10
|
+
*
|
|
11
|
+
* Stacks-first: `getAppFrameworkDriver()` defaults to the Stacks driver.
|
|
12
|
+
*/
|
|
13
|
+
import type { QueueWorkerConfig, SiteConfig } from '@ts-cloud/core';
|
|
14
|
+
export type FrameworkId = 'stacks' | 'laravel';
|
|
15
|
+
/**
|
|
16
|
+
* Resolve which framework driver a site uses. An explicit `framework` wins; a
|
|
17
|
+
* PHP-oriented `type` (laravel/php/statamic/wordpress) implies Laravel for
|
|
18
|
+
* backward compatibility; everything else uses the Stacks-first default.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveSiteFramework(site: Pick<SiteConfig, 'framework' | 'type'>): FrameworkId;
|
|
21
|
+
export interface FrameworkExecContext {
|
|
22
|
+
/** The active-release directory (`/var/www/<site>/current`). */
|
|
23
|
+
current: string;
|
|
24
|
+
}
|
|
25
|
+
export interface AppFrameworkDriver {
|
|
26
|
+
readonly id: FrameworkId;
|
|
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).
|
|
30
|
+
*/
|
|
31
|
+
wrapExec: (command: string) => string;
|
|
32
|
+
/**
|
|
33
|
+
* How the app scheduler runs:
|
|
34
|
+
* - `'cron'` → `schedule:run` is one-shot; run it every minute via cron (Laravel).
|
|
35
|
+
* - `'daemon'` → `schedule:run` is long-lived (holds in-process timers); run it
|
|
36
|
+
* as a single always-on systemd unit (Stacks).
|
|
37
|
+
*/
|
|
38
|
+
readonly schedulerMode: 'cron' | 'daemon';
|
|
39
|
+
/**
|
|
40
|
+
* The scheduler command WITHOUT output redirection. For `'cron'` it is run
|
|
41
|
+
* from cron and sets up its own cwd/env; for `'daemon'` it becomes a unit's
|
|
42
|
+
* ExecStart (systemd supplies WorkingDirectory), so no `cd` is needed.
|
|
43
|
+
*/
|
|
44
|
+
schedulerCommand: (ctx: FrameworkExecContext) => string;
|
|
45
|
+
/** `ExecStart` for a single queue-worker process. */
|
|
46
|
+
queueWorkerCommand: (worker: QueueWorkerConfig, ctx: FrameworkExecContext) => string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Stacks (Bun) — the default. The scheduler is cron-driven (`buddy schedule:run`
|
|
50
|
+
* is one-shot); queue workers are long-running (`buddy queue:work`). bun lives
|
|
51
|
+
* at an absolute path installed by the box bootstrap.
|
|
52
|
+
*/
|
|
53
|
+
export declare const stacksDriver: AppFrameworkDriver;
|
|
54
|
+
/** Laravel (PHP / Artisan) — the original Forge-style behavior, now a driver. */
|
|
55
|
+
export declare const laravelDriver: AppFrameworkDriver;
|
|
56
|
+
/** Resolve the app-framework driver for a site. Stacks-first default. */
|
|
57
|
+
export declare function getAppFrameworkDriver(framework?: FrameworkId): AppFrameworkDriver;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generate the per-site runtime services for a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Generate the per-site runtime services for a server-app box: queue workers
|
|
3
|
+
* (systemd), the app scheduler (cron), and arbitrary daemons (systemd).
|
|
4
|
+
* Reconciled on every deploy so units track the config — units no longer in the
|
|
5
|
+
* config are stopped and removed.
|
|
6
|
+
*
|
|
7
|
+
* HOW the scheduler + queue workers are invoked (Stacks/Bun vs Laravel/PHP) is
|
|
8
|
+
* delegated to an {@link AppFrameworkDriver} selected by `site.framework`
|
|
9
|
+
* (Stacks-first default). This file only owns unit/cron plumbing + pruning.
|
|
6
10
|
*
|
|
7
11
|
* Unit naming (so a site's units can be globbed for pruning):
|
|
8
12
|
* <slug>-<site>-queue-<i>.service
|
|
@@ -13,18 +17,21 @@
|
|
|
13
17
|
* so workers/daemons always run the live code; `queue:restart` (run by the
|
|
14
18
|
* deploy's $RESTART_QUEUES macro) cycles them onto the new release.
|
|
15
19
|
*/
|
|
16
|
-
import type {
|
|
20
|
+
import type { SiteConfig } from '@ts-cloud/core';
|
|
17
21
|
export interface SiteServicesOptions {
|
|
18
22
|
slug: string;
|
|
19
23
|
siteName: string;
|
|
20
24
|
site: SiteConfig;
|
|
21
|
-
/** PHP version selecting the `phpX.Y` binary. @default '8.3' */
|
|
25
|
+
/** PHP version selecting the `phpX.Y` binary (Laravel only). @default '8.3' */
|
|
22
26
|
phpVersion?: string;
|
|
23
27
|
/** Site base dir. @default `/var/www/<siteName>` */
|
|
24
28
|
appBase?: string;
|
|
25
29
|
}
|
|
26
30
|
export declare function queueUnitName(slug: string, siteName: string, index: number): string;
|
|
27
|
-
export declare function daemonUnitName(slug: string, siteName: string, daemon:
|
|
31
|
+
export declare function daemonUnitName(slug: string, siteName: string, daemon: {
|
|
32
|
+
name?: string;
|
|
33
|
+
command: string;
|
|
34
|
+
}, index: number): string;
|
|
28
35
|
/** Path of the scheduler cron file for a site. */
|
|
29
36
|
export declare function schedulerCronPath(slug: string, siteName: string): string;
|
|
30
37
|
/**
|
package/dist/index.js
CHANGED
|
@@ -87729,11 +87729,69 @@ function buildLaravelDeployScript(options) {
|
|
|
87729
87729
|
return out;
|
|
87730
87730
|
}
|
|
87731
87731
|
|
|
87732
|
-
// src/drivers/shared/
|
|
87732
|
+
// src/drivers/shared/app-frameworks.ts
|
|
87733
|
+
var PHP_SITE_TYPES2 = new Set(["laravel", "php", "statamic", "wordpress"]);
|
|
87734
|
+
function resolveSiteFramework(site) {
|
|
87735
|
+
if (site.framework)
|
|
87736
|
+
return site.framework;
|
|
87737
|
+
if (site.type && PHP_SITE_TYPES2.has(site.type))
|
|
87738
|
+
return "laravel";
|
|
87739
|
+
return "stacks";
|
|
87740
|
+
}
|
|
87733
87741
|
var PANTRY_ENV_EVAL = `eval "$(cd ${PANTRY_PROJECT_DIR} && pantry env 2>/dev/null)"`;
|
|
87734
|
-
|
|
87735
|
-
|
|
87742
|
+
var BUN_BIN = "/usr/local/bin/bun";
|
|
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
|
+
var stacksDriver = {
|
|
87748
|
+
id: "stacks",
|
|
87749
|
+
wrapExec: bunEnvWrap,
|
|
87750
|
+
schedulerMode: "daemon",
|
|
87751
|
+
schedulerCommand: () => `${BUN_BIN} ${STACKS_CLI} schedule:run`,
|
|
87752
|
+
queueWorkerCommand: (worker, { current }) => {
|
|
87753
|
+
const flags = [
|
|
87754
|
+
`--queue=${worker.queue || "default"}`,
|
|
87755
|
+
`--sleep=${worker.sleep ?? 3}`,
|
|
87756
|
+
`--tries=${worker.tries ?? 3}`,
|
|
87757
|
+
`--timeout=${worker.timeout ?? 60}`
|
|
87758
|
+
];
|
|
87759
|
+
return `${BUN_BIN} ${current}/${STACKS_CLI} queue:work ${flags.join(" ")}`;
|
|
87760
|
+
}
|
|
87761
|
+
};
|
|
87762
|
+
var laravelDriver = {
|
|
87763
|
+
id: "laravel",
|
|
87764
|
+
wrapExec: (command) => `/bin/sh -lc '${PANTRY_ENV_EVAL}; exec ${command}'`,
|
|
87765
|
+
schedulerMode: "cron",
|
|
87766
|
+
schedulerCommand: ({ current }) => `cd ${current} && ${PANTRY_ENV_EVAL} && php artisan schedule:run`,
|
|
87767
|
+
queueWorkerCommand: (worker, { current }) => {
|
|
87768
|
+
const artisan = `${current}/artisan`;
|
|
87769
|
+
if (worker.horizon)
|
|
87770
|
+
return `php ${artisan} horizon`;
|
|
87771
|
+
const flags = [
|
|
87772
|
+
worker.connection || "default",
|
|
87773
|
+
`--queue=${worker.queue || "default"}`,
|
|
87774
|
+
`--sleep=${worker.sleep ?? 3}`,
|
|
87775
|
+
`--tries=${worker.tries ?? 3}`,
|
|
87776
|
+
`--timeout=${worker.timeout ?? 60}`,
|
|
87777
|
+
`--memory=${worker.memory ?? 128}`
|
|
87778
|
+
];
|
|
87779
|
+
if (worker.maxJobs)
|
|
87780
|
+
flags.push(`--max-jobs=${worker.maxJobs}`);
|
|
87781
|
+
if (worker.maxTime)
|
|
87782
|
+
flags.push(`--max-time=${worker.maxTime}`);
|
|
87783
|
+
return `php ${artisan} queue:work ${flags.join(" ")}`;
|
|
87784
|
+
}
|
|
87785
|
+
};
|
|
87786
|
+
var DRIVERS = {
|
|
87787
|
+
stacks: stacksDriver,
|
|
87788
|
+
laravel: laravelDriver
|
|
87789
|
+
};
|
|
87790
|
+
function getAppFrameworkDriver(framework) {
|
|
87791
|
+
return DRIVERS[framework] ?? stacksDriver;
|
|
87736
87792
|
}
|
|
87793
|
+
|
|
87794
|
+
// src/drivers/shared/app-services.ts
|
|
87737
87795
|
function reEscape(value) {
|
|
87738
87796
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
87739
87797
|
}
|
|
@@ -87749,23 +87807,6 @@ function queueUnitName(slug, siteName, index) {
|
|
|
87749
87807
|
function daemonUnitName(slug, siteName, daemon, index) {
|
|
87750
87808
|
return `${slug}-${siteName}-daemon-${daemon.name ? slugify(daemon.name) : slugify(daemon.command).slice(0, 32) || String(index)}`;
|
|
87751
87809
|
}
|
|
87752
|
-
function queueExecStart(worker, phpBin, artisan) {
|
|
87753
|
-
if (worker.horizon)
|
|
87754
|
-
return `${phpBin} ${artisan} horizon`;
|
|
87755
|
-
const flags = [
|
|
87756
|
-
worker.connection || "default",
|
|
87757
|
-
`--queue=${worker.queue || "default"}`,
|
|
87758
|
-
`--sleep=${worker.sleep ?? 3}`,
|
|
87759
|
-
`--tries=${worker.tries ?? 3}`,
|
|
87760
|
-
`--timeout=${worker.timeout ?? 60}`,
|
|
87761
|
-
`--memory=${worker.memory ?? 128}`
|
|
87762
|
-
];
|
|
87763
|
-
if (worker.maxJobs)
|
|
87764
|
-
flags.push(`--max-jobs=${worker.maxJobs}`);
|
|
87765
|
-
if (worker.maxTime)
|
|
87766
|
-
flags.push(`--max-time=${worker.maxTime}`);
|
|
87767
|
-
return `${phpBin} ${artisan} queue:work ${flags.join(" ")}`;
|
|
87768
|
-
}
|
|
87769
87810
|
function systemdUnit(opts) {
|
|
87770
87811
|
const lines = [
|
|
87771
87812
|
"[Unit]",
|
|
@@ -87800,10 +87841,10 @@ function schedulerCronPath(slug, siteName) {
|
|
|
87800
87841
|
}
|
|
87801
87842
|
function buildSiteServicesScript(options) {
|
|
87802
87843
|
const { slug, siteName, site } = options;
|
|
87803
|
-
const phpBin = "php";
|
|
87804
87844
|
const base = options.appBase ?? `/var/www/${siteName}`;
|
|
87805
87845
|
const current = `${base}/current`;
|
|
87806
|
-
const
|
|
87846
|
+
const driver = getAppFrameworkDriver(resolveSiteFramework(site));
|
|
87847
|
+
const ctx = { current };
|
|
87807
87848
|
const out = [];
|
|
87808
87849
|
const desiredUnits = [];
|
|
87809
87850
|
const queues = site.queues || [];
|
|
@@ -87815,7 +87856,7 @@ function buildSiteServicesScript(options) {
|
|
|
87815
87856
|
out.push(...writeUnitScript(name, systemdUnit({
|
|
87816
87857
|
description: `${siteName} queue worker ${qIndex}.${p} (managed by ts-cloud)`,
|
|
87817
87858
|
workingDir: current,
|
|
87818
|
-
execStart:
|
|
87859
|
+
execStart: driver.wrapExec(driver.queueWorkerCommand(worker, ctx)),
|
|
87819
87860
|
stopWaitSecs: worker.stopWaitSecs ?? 90
|
|
87820
87861
|
})));
|
|
87821
87862
|
}
|
|
@@ -87829,23 +87870,32 @@ function buildSiteServicesScript(options) {
|
|
|
87829
87870
|
out.push(...writeUnitScript(name, systemdUnit({
|
|
87830
87871
|
description: `${siteName} daemon ${daemon.name || daemon.command} (managed by ts-cloud)`,
|
|
87831
87872
|
workingDir: daemon.directory || current,
|
|
87832
|
-
execStart:
|
|
87873
|
+
execStart: driver.wrapExec(daemon.command),
|
|
87833
87874
|
restart: daemon.restart,
|
|
87834
87875
|
user: daemon.user
|
|
87835
87876
|
})));
|
|
87836
87877
|
}
|
|
87837
87878
|
});
|
|
87879
|
+
const scheduler2 = site.scheduler;
|
|
87880
|
+
const schedulerEnabled = scheduler2 === true || typeof scheduler2 === "object" && scheduler2 !== null;
|
|
87881
|
+
const schedulerUnit = `${slug}-${siteName}-scheduler`;
|
|
87882
|
+
if (schedulerEnabled && driver.schedulerMode === "daemon") {
|
|
87883
|
+
desiredUnits.push(schedulerUnit);
|
|
87884
|
+
out.push(...writeUnitScript(schedulerUnit, systemdUnit({
|
|
87885
|
+
description: `${siteName} scheduler (managed by ts-cloud)`,
|
|
87886
|
+
workingDir: current,
|
|
87887
|
+
execStart: driver.wrapExec(driver.schedulerCommand(ctx))
|
|
87888
|
+
})));
|
|
87889
|
+
}
|
|
87838
87890
|
const desiredList = desiredUnits.map((n) => `${n}.service`).join(" ");
|
|
87839
|
-
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)
|
|
87891
|
+
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");
|
|
87840
87892
|
for (const name of desiredUnits) {
|
|
87841
87893
|
out.push(`systemctl enable ${name}.service`, `systemctl restart ${name}.service`);
|
|
87842
87894
|
}
|
|
87843
87895
|
const cronPath = schedulerCronPath(slug, siteName);
|
|
87844
|
-
|
|
87845
|
-
const schedulerEnabled = scheduler2 === true || typeof scheduler2 === "object" && scheduler2 !== null;
|
|
87846
|
-
if (schedulerEnabled) {
|
|
87896
|
+
if (schedulerEnabled && driver.schedulerMode === "cron") {
|
|
87847
87897
|
const heartbeat = typeof scheduler2 === "object" && scheduler2 !== null ? scheduler2 : undefined;
|
|
87848
|
-
let command =
|
|
87898
|
+
let command = `${driver.schedulerCommand(ctx)} >> /dev/null 2>&1`;
|
|
87849
87899
|
if (heartbeat?.heartbeatUrl) {
|
|
87850
87900
|
const method = heartbeat.heartbeatMethod || "GET";
|
|
87851
87901
|
const methodFlag = method === "GET" ? "" : `-X ${method} `;
|
|
@@ -87992,12 +88042,12 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger2) {
|
|
|
87992
88042
|
security: site.security
|
|
87993
88043
|
}) : [];
|
|
87994
88044
|
const sslScript = useNginx ? buildSslScript(site) : [];
|
|
87995
|
-
const
|
|
88045
|
+
const servicesScript2 = siteHasServices(site) ? buildSiteServicesScript({ slug, siteName, site, phpVersion, appBase }) : [];
|
|
87996
88046
|
const healthCheckScript = useNginx ? buildHealthCheckScript(site) : [];
|
|
87997
88047
|
logger4.step(`Deploying PHP site '${siteName}' to ${targets.length} target(s)...`);
|
|
87998
88048
|
const phpResult = await driver.runRemoteDeploy({
|
|
87999
88049
|
targets,
|
|
88000
|
-
commands: [...deployScript, ...poolScript, ...vhostScript, ...sslScript, ...
|
|
88050
|
+
commands: [...deployScript, ...poolScript, ...vhostScript, ...sslScript, ...servicesScript2, ...healthCheckScript],
|
|
88001
88051
|
comment: `ts-cloud deploy ${slug}/${siteName}@${sha}`,
|
|
88002
88052
|
tags: { Project: slug, Environment: environment, Role: "app" }
|
|
88003
88053
|
});
|
|
@@ -88063,6 +88113,7 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger2) {
|
|
|
88063
88113
|
security: site.security
|
|
88064
88114
|
}) : [];
|
|
88065
88115
|
const staticSsl = wantsNginxStatic ? buildSslScript(site) : [];
|
|
88116
|
+
const servicesScript = siteHasServices(site) ? buildSiteServicesScript({ slug, siteName, site, appBase: `/var/www/${siteName}` }) : [];
|
|
88066
88117
|
const remoteScript = [
|
|
88067
88118
|
...buildDeployHistoryHeader(`/var/www/${siteName}`, {
|
|
88068
88119
|
releaseId: sha,
|
|
@@ -88071,7 +88122,8 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger2) {
|
|
|
88071
88122
|
}),
|
|
88072
88123
|
...baseScript,
|
|
88073
88124
|
...staticVhost,
|
|
88074
|
-
...staticSsl
|
|
88125
|
+
...staticSsl,
|
|
88126
|
+
...servicesScript
|
|
88075
88127
|
];
|
|
88076
88128
|
logger4.step(`Deploying to ${targets.length} target(s)...`);
|
|
88077
88129
|
const result = await driver.runRemoteDeploy({
|