@stacksjs/ts-cloud 0.5.30 → 0.5.32
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 +341 -341
- package/dist/drivers/shared/app-frameworks.d.ts +50 -0
- package/dist/drivers/shared/{laravel-services.d.ts → app-services.d.ts} +14 -7
- package/dist/index.js +87 -28
- 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,50 @@
|
|
|
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
|
+
* 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 `&&`.
|
|
36
|
+
*/
|
|
37
|
+
schedulerCommand: (ctx: FrameworkExecContext) => string;
|
|
38
|
+
/** `ExecStart` for a single queue-worker process. */
|
|
39
|
+
queueWorkerCommand: (worker: QueueWorkerConfig, ctx: FrameworkExecContext) => string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
export declare const stacksDriver: AppFrameworkDriver;
|
|
47
|
+
/** Laravel (PHP / Artisan) — the original Forge-style behavior, now a driver. */
|
|
48
|
+
export declare const laravelDriver: AppFrameworkDriver;
|
|
49
|
+
/** Resolve the app-framework driver for a site. Stacks-first default. */
|
|
50
|
+
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
|
@@ -86906,13 +86906,23 @@ class HetznerDriver {
|
|
|
86906
86906
|
}
|
|
86907
86907
|
async findComputeTargets(options) {
|
|
86908
86908
|
const servers = await this.client.listServers();
|
|
86909
|
-
|
|
86909
|
+
const role = options.role || "app";
|
|
86910
|
+
const toTarget = (server) => ({
|
|
86910
86911
|
id: String(server.id),
|
|
86911
86912
|
name: server.name,
|
|
86912
86913
|
publicIp: server.public_net.ipv4?.ip,
|
|
86913
86914
|
privateIp: server.private_net?.[0]?.ip,
|
|
86914
86915
|
status: server.status
|
|
86915
|
-
})
|
|
86916
|
+
});
|
|
86917
|
+
const exact = servers.filter((server) => matchesTsCloudLabels(server.labels, options.slug, options.environment, role));
|
|
86918
|
+
if (exact.length > 0)
|
|
86919
|
+
return exact.map(toTarget);
|
|
86920
|
+
if (role === "app") {
|
|
86921
|
+
const candidates = servers.filter((server) => server.status !== "off" && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/managed-by`] === "ts-cloud" && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/environment`] === options.environment && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/role`] === "app");
|
|
86922
|
+
if (candidates.length === 1)
|
|
86923
|
+
return candidates.map(toTarget);
|
|
86924
|
+
}
|
|
86925
|
+
return [];
|
|
86916
86926
|
}
|
|
86917
86927
|
async runRemoteDeploy(options) {
|
|
86918
86928
|
if (options.targets.length === 0) {
|
|
@@ -86979,7 +86989,17 @@ class HetznerDriver {
|
|
|
86979
86989
|
}
|
|
86980
86990
|
async findExistingServer(slug, environment, serverName) {
|
|
86981
86991
|
const servers = await this.client.listServers();
|
|
86982
|
-
|
|
86992
|
+
const exact = servers.find((server) => matchesTsCloudLabels(server.labels, slug, environment, "app") || server.name === serverName);
|
|
86993
|
+
if (exact)
|
|
86994
|
+
return exact;
|
|
86995
|
+
const candidates = servers.filter((server) => server.status !== "off" && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/managed-by`] === "ts-cloud" && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/environment`] === environment && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/role`] === "app");
|
|
86996
|
+
if (candidates.length === 1) {
|
|
86997
|
+
const adopted = candidates[0];
|
|
86998
|
+
const adoptedProject = adopted.labels?.[`${TS_CLOUD_LABEL_PREFIX}/project`] ?? "unknown";
|
|
86999
|
+
console.warn(`[ts-cloud] No server named '${serverName}' found; adopting existing ts-cloud app server ` + `'${adopted.name}' (project label '${adoptedProject}') for project '${slug}' — ` + `updating it in place instead of provisioning a new server.`);
|
|
87000
|
+
return adopted;
|
|
87001
|
+
}
|
|
87002
|
+
return;
|
|
86983
87003
|
}
|
|
86984
87004
|
async ensureFirewall(name, labels, rules) {
|
|
86985
87005
|
const existing = await this.client.listFirewalls();
|
|
@@ -87709,11 +87729,67 @@ function buildLaravelDeployScript(options) {
|
|
|
87709
87729
|
return out;
|
|
87710
87730
|
}
|
|
87711
87731
|
|
|
87712
|
-
// 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
|
+
}
|
|
87713
87741
|
var PANTRY_ENV_EVAL = `eval "$(cd ${PANTRY_PROJECT_DIR} && pantry env 2>/dev/null)"`;
|
|
87714
|
-
|
|
87715
|
-
|
|
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
|
+
schedulerCommand: ({ current }) => `cd ${current} && ${BUN_BIN} ${STACKS_CLI} schedule:run >> /dev/null 2>&1`,
|
|
87751
|
+
queueWorkerCommand: (worker, { current }) => {
|
|
87752
|
+
const flags = [
|
|
87753
|
+
`--queue=${worker.queue || "default"}`,
|
|
87754
|
+
`--sleep=${worker.sleep ?? 3}`,
|
|
87755
|
+
`--tries=${worker.tries ?? 3}`,
|
|
87756
|
+
`--timeout=${worker.timeout ?? 60}`
|
|
87757
|
+
];
|
|
87758
|
+
return `${BUN_BIN} ${current}/${STACKS_CLI} queue:work ${flags.join(" ")}`;
|
|
87759
|
+
}
|
|
87760
|
+
};
|
|
87761
|
+
var laravelDriver = {
|
|
87762
|
+
id: "laravel",
|
|
87763
|
+
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`,
|
|
87765
|
+
queueWorkerCommand: (worker, { current }) => {
|
|
87766
|
+
const artisan = `${current}/artisan`;
|
|
87767
|
+
if (worker.horizon)
|
|
87768
|
+
return `php ${artisan} horizon`;
|
|
87769
|
+
const flags = [
|
|
87770
|
+
worker.connection || "default",
|
|
87771
|
+
`--queue=${worker.queue || "default"}`,
|
|
87772
|
+
`--sleep=${worker.sleep ?? 3}`,
|
|
87773
|
+
`--tries=${worker.tries ?? 3}`,
|
|
87774
|
+
`--timeout=${worker.timeout ?? 60}`,
|
|
87775
|
+
`--memory=${worker.memory ?? 128}`
|
|
87776
|
+
];
|
|
87777
|
+
if (worker.maxJobs)
|
|
87778
|
+
flags.push(`--max-jobs=${worker.maxJobs}`);
|
|
87779
|
+
if (worker.maxTime)
|
|
87780
|
+
flags.push(`--max-time=${worker.maxTime}`);
|
|
87781
|
+
return `php ${artisan} queue:work ${flags.join(" ")}`;
|
|
87782
|
+
}
|
|
87783
|
+
};
|
|
87784
|
+
var DRIVERS = {
|
|
87785
|
+
stacks: stacksDriver,
|
|
87786
|
+
laravel: laravelDriver
|
|
87787
|
+
};
|
|
87788
|
+
function getAppFrameworkDriver(framework) {
|
|
87789
|
+
return DRIVERS[framework] ?? stacksDriver;
|
|
87716
87790
|
}
|
|
87791
|
+
|
|
87792
|
+
// src/drivers/shared/app-services.ts
|
|
87717
87793
|
function reEscape(value) {
|
|
87718
87794
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
87719
87795
|
}
|
|
@@ -87729,23 +87805,6 @@ function queueUnitName(slug, siteName, index) {
|
|
|
87729
87805
|
function daemonUnitName(slug, siteName, daemon, index) {
|
|
87730
87806
|
return `${slug}-${siteName}-daemon-${daemon.name ? slugify(daemon.name) : slugify(daemon.command).slice(0, 32) || String(index)}`;
|
|
87731
87807
|
}
|
|
87732
|
-
function queueExecStart(worker, phpBin, artisan) {
|
|
87733
|
-
if (worker.horizon)
|
|
87734
|
-
return `${phpBin} ${artisan} horizon`;
|
|
87735
|
-
const flags = [
|
|
87736
|
-
worker.connection || "default",
|
|
87737
|
-
`--queue=${worker.queue || "default"}`,
|
|
87738
|
-
`--sleep=${worker.sleep ?? 3}`,
|
|
87739
|
-
`--tries=${worker.tries ?? 3}`,
|
|
87740
|
-
`--timeout=${worker.timeout ?? 60}`,
|
|
87741
|
-
`--memory=${worker.memory ?? 128}`
|
|
87742
|
-
];
|
|
87743
|
-
if (worker.maxJobs)
|
|
87744
|
-
flags.push(`--max-jobs=${worker.maxJobs}`);
|
|
87745
|
-
if (worker.maxTime)
|
|
87746
|
-
flags.push(`--max-time=${worker.maxTime}`);
|
|
87747
|
-
return `${phpBin} ${artisan} queue:work ${flags.join(" ")}`;
|
|
87748
|
-
}
|
|
87749
87808
|
function systemdUnit(opts) {
|
|
87750
87809
|
const lines = [
|
|
87751
87810
|
"[Unit]",
|
|
@@ -87780,10 +87839,10 @@ function schedulerCronPath(slug, siteName) {
|
|
|
87780
87839
|
}
|
|
87781
87840
|
function buildSiteServicesScript(options) {
|
|
87782
87841
|
const { slug, siteName, site } = options;
|
|
87783
|
-
const phpBin = "php";
|
|
87784
87842
|
const base = options.appBase ?? `/var/www/${siteName}`;
|
|
87785
87843
|
const current = `${base}/current`;
|
|
87786
|
-
const
|
|
87844
|
+
const driver = getAppFrameworkDriver(resolveSiteFramework(site));
|
|
87845
|
+
const ctx = { current };
|
|
87787
87846
|
const out = [];
|
|
87788
87847
|
const desiredUnits = [];
|
|
87789
87848
|
const queues = site.queues || [];
|
|
@@ -87795,7 +87854,7 @@ function buildSiteServicesScript(options) {
|
|
|
87795
87854
|
out.push(...writeUnitScript(name, systemdUnit({
|
|
87796
87855
|
description: `${siteName} queue worker ${qIndex}.${p} (managed by ts-cloud)`,
|
|
87797
87856
|
workingDir: current,
|
|
87798
|
-
execStart:
|
|
87857
|
+
execStart: driver.wrapExec(driver.queueWorkerCommand(worker, ctx)),
|
|
87799
87858
|
stopWaitSecs: worker.stopWaitSecs ?? 90
|
|
87800
87859
|
})));
|
|
87801
87860
|
}
|
|
@@ -87809,7 +87868,7 @@ function buildSiteServicesScript(options) {
|
|
|
87809
87868
|
out.push(...writeUnitScript(name, systemdUnit({
|
|
87810
87869
|
description: `${siteName} daemon ${daemon.name || daemon.command} (managed by ts-cloud)`,
|
|
87811
87870
|
workingDir: daemon.directory || current,
|
|
87812
|
-
execStart:
|
|
87871
|
+
execStart: driver.wrapExec(daemon.command),
|
|
87813
87872
|
restart: daemon.restart,
|
|
87814
87873
|
user: daemon.user
|
|
87815
87874
|
})));
|
|
@@ -87825,7 +87884,7 @@ function buildSiteServicesScript(options) {
|
|
|
87825
87884
|
const schedulerEnabled = scheduler2 === true || typeof scheduler2 === "object" && scheduler2 !== null;
|
|
87826
87885
|
if (schedulerEnabled) {
|
|
87827
87886
|
const heartbeat = typeof scheduler2 === "object" && scheduler2 !== null ? scheduler2 : undefined;
|
|
87828
|
-
let command =
|
|
87887
|
+
let command = driver.schedulerCommand(ctx);
|
|
87829
87888
|
if (heartbeat?.heartbeatUrl) {
|
|
87830
87889
|
const method = heartbeat.heartbeatMethod || "GET";
|
|
87831
87890
|
const methodFlag = method === "GET" ? "" : `-X ${method} `;
|