@absolutejs/deploy 0.0.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/LICENSE ADDED
@@ -0,0 +1,95 @@
1
+ # Business Source License 1.1
2
+
3
+ **Licensor:** Alex Kahn
4
+
5
+ **Licensed Work:** @absolutejs/deploy (https://github.com/absolutejs/deploy)
6
+
7
+ **Change Date:** May 29, 2030
8
+
9
+ **Change License:** Apache License, Version 2.0
10
+
11
+ ---
12
+
13
+ ## Terms
14
+
15
+ The Licensor hereby grants you the right to copy, modify, create derivative
16
+ works, redistribute, and make non-production use of the Licensed Work. The
17
+ Licensor may make an Additional Use Grant, permitting limited production use.
18
+
19
+ ### Additional Use Grant
20
+
21
+ You may use the Licensed Work in production, provided your use does not include
22
+ any of the following:
23
+
24
+ 1. **Offering a Competing Service.** You may not offer the Licensed Work, or
25
+ any derivative or substantial portion of it, to third parties as a hosted or
26
+ managed service that competes with a hosted application-deploy or
27
+ git-push-deploy platform for application code (including, but not limited
28
+ to, services like Vercel, Render, Railway, Fly.io's deploy half, Netlify,
29
+ Heroku, Cloud66, Coolify, Cloudflare Pages / Workers deploy, DigitalOcean
30
+ App Platform, Azure App Service deploy, AWS Amplify Hosting, AWS Elastic
31
+ Beanstalk, GCP Cloud Run deploy, or any similar hosted offering whose
32
+ primary value to its users is git-to-running-process / repo-to-URL
33
+ deploy automation). This includes any product whose primary value to its
34
+ users is the functionality the Licensed Work provides.
35
+
36
+ 2. **Resale or Redistribution as a Standalone Product.** You may not sell,
37
+ license, or distribute the Licensed Work, or any derivative or fork of it,
38
+ as a standalone commercial product.
39
+
40
+ 3. **Removal of Attribution.** Any derivative work, fork, or redistribution of
41
+ the Licensed Work must prominently credit AbsoluteJS and include a link to
42
+ the original project repository (https://github.com/absolutejs/deploy).
43
+
44
+ For clarity, the following uses are expressly permitted:
45
+
46
+ - Using the Licensed Work to build and operate your own applications, websites,
47
+ internal tools, or SaaS products (whether commercial or non-commercial), so
48
+ long as the Licensed Work itself is not the primary product you are selling.
49
+ - Using the Licensed Work as a dependency in commercial software you build and
50
+ sell, as long as the software is not itself a competing managed service of
51
+ the kind described in clause 1.
52
+ - Providing consulting, development, or professional services to clients using
53
+ the Licensed Work — including running the Licensed Work as part of your own
54
+ client-engagement deploy automation.
55
+ - Forking and modifying the Licensed Work for your own internal use, provided
56
+ attribution is maintained.
57
+
58
+ ### Change Date and Change License
59
+
60
+ On the Change Date specified above, or on such other date as the Licensor may
61
+ specify by written notice, the Licensed Work will be made available under the
62
+ Change License (Apache License, Version 2.0). Until the Change Date, the terms
63
+ of this Business Source License 1.1 apply.
64
+
65
+ ### Trademark
66
+
67
+ This license does not grant you any rights to use the "AbsoluteJS" or
68
+ "@absolutejs" name, logo, or any related trademarks. Forks and derivative works
69
+ must not be named or branded in a manner that suggests endorsement by or
70
+ affiliation with AbsoluteJS or the Licensor.
71
+
72
+ ### Notices
73
+
74
+ You must not remove or obscure any licensing, copyright, or other notices
75
+ included in the Licensed Work.
76
+
77
+ ### No Warranty
78
+
79
+ THE LICENSED WORK IS PROVIDED "AS IS". THE LICENSOR HEREBY DISCLAIMS ALL
80
+ WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
81
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO
82
+ EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
83
+ WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
84
+ IN CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE
85
+ LICENSED WORK.
86
+
87
+ ---
88
+
89
+ ## Contact
90
+
91
+ For commercial licensing inquiries or additional permissions, contact:
92
+
93
+ - **Alex Kahn**
94
+ - alexkahndev@gmail.com
95
+ - alexkahndev.github.io
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @absolutejs/deploy
2
+
3
+ Generic Bun-project deploy pipeline. A `Target` is anywhere you can run a
4
+ command and copy a file — a DigitalOcean Droplet over SSH, a Linode box,
5
+ your own laptop. Two ops, four words: **exec and upload**.
6
+
7
+ The bundled `defaultBunPipeline()` covers the case that matters most for
8
+ Bun apps on Linux: `prepare → upload → install → build → link → restart
9
+ → verify`. Releases live in `releases/<id>/`, a `current` symlink swaps
10
+ atomically, `rollback(releaseId)` re-points the symlink and restarts.
11
+
12
+ Zero `ssh2` / `node-ssh` dependency — `sshTarget` shells out to the system
13
+ `ssh` / `rsync` binaries that already ship on Mac, Linux, and WSL.
14
+
15
+ ```ts
16
+ import {
17
+ createDeployer,
18
+ sshTarget,
19
+ systemdManager,
20
+ } from '@absolutejs/deploy';
21
+
22
+ const deployer = createDeployer({
23
+ appName: 'my-app',
24
+ target: sshTarget({
25
+ host: 'droplet-1.example.com',
26
+ user: 'deploy',
27
+ identity: '~/.ssh/id_ed25519',
28
+ }),
29
+ source: { kind: 'directory', root: './' },
30
+ env: { PORT: '3000', DATABASE_URL: process.env.DATABASE_URL! },
31
+ processManager: systemdManager({ user: 'deploy' }),
32
+ verify: { kind: 'http', url: 'http://localhost:3000/health' },
33
+ hooks: {
34
+ onStepStart: ({ name, releaseId }) => console.log(`▸ ${releaseId} ${name}`),
35
+ onLog: (line, stream, step) => process[stream === 'stderr' ? 'stderr' : 'stdout'].write(`[${step}] ${line}\n`),
36
+ },
37
+ });
38
+
39
+ const release = await deployer.deploy();
40
+ console.log(`Deployed ${release.releaseId} in ${release.durationMs}ms`);
41
+
42
+ // later — atomic rollback
43
+ const previous = (await deployer.listReleases()).at(-2);
44
+ if (previous) await deployer.rollback(previous);
45
+
46
+ // optional housekeeping
47
+ await deployer.prune({ keep: 5 });
48
+ ```
49
+
50
+ ## v0.0.1 surface
51
+
52
+ ### Targets
53
+
54
+ | Adapter | Use |
55
+ |---|---|
56
+ | `localTarget({ root, env? })` | Tests, local-dev, and "deploy to the same box" workflows. |
57
+ | `sshTarget({ host, user?, port?, identity?, sshFlags?, rsync? })` | Any VPS — DigitalOcean Droplets, Linode, Hetzner, Vultr, Lightsail, Scaleway. Uses the system `ssh` / `rsync` — no npm dep. |
58
+
59
+ A `Target` is just:
60
+
61
+ ```ts
62
+ type Target = {
63
+ description: string;
64
+ exec(cmd: string, opts?: { cwd?; env?; timeoutMs?; onLog?; stdin? }): Promise<{ stdout; stderr; exitCode }>;
65
+ upload(local: string, remote: string, opts?: { exclude?; deleteOrphans? }): Promise<void>;
66
+ close?(): Promise<void>;
67
+ };
68
+ ```
69
+
70
+ If you can implement those two methods, you can deploy through `@absolutejs/deploy`. Provider-specific adapters that don't fit this shape (Cloudflare Workers API, Fly Machines API, AWS Fargate task-run) ship later as sibling packages.
71
+
72
+ ### Process managers
73
+
74
+ | Manager | What it does |
75
+ |---|---|
76
+ | `bareManager({ command? })` | Default. `nohup bun run start &`, pid file under `/var/lib/<appName>/`, logs to `/var/log/<appName>/`. Zero remote dependency. |
77
+ | `systemdManager({ user?, group?, execStart?, restart?, ... })` | Templated systemd unit pointing at `current/`, `daemon-reload` + `restart`. The production answer for VMs. |
78
+
79
+ A `ProcessManager` is just `{ reload, stop?, status? }`. Wrap PM2, supervisord, runit, or even `@absolutejs/runtime` — whatever your remote uses.
80
+
81
+ ### Pipeline
82
+
83
+ The default Bun pipeline:
84
+
85
+ 1. **prepare** — `mkdir -p releases/<id>/`
86
+ 2. **upload** — `rsync` source → release dir (excludes `node_modules`, `dist`, `build`, `.git`, `*.log`, `.DS_Store` by default)
87
+ 3. **install** — `bun install --production` in the release dir
88
+ 4. **build** — if `package.json` has a `build` script, `bun run build`
89
+ 5. **link** — `ln -sfn release current.next && mv -Tf current.next current` (atomic-ish swap)
90
+ 6. **restart** — delegate to the configured `ProcessManager`
91
+ 7. **verify** — HTTP / TCP / custom probe (when `verify` is set)
92
+
93
+ Replace any step by passing `steps: [...]` — the default is a normal array you can splice into.
94
+
95
+ ### Verify
96
+
97
+ ```ts
98
+ { kind: 'http', url: 'http://localhost:3000/health', retries: 30, intervalMs: 1000, expectStatus: 200 }
99
+ { kind: 'tcp', host: 'localhost', port: 3000, retries: 30, intervalMs: 1000 }
100
+ { kind: 'custom', check: async (ctx) => ctx.target.exec('myhealthcheck').then((r) => r.exitCode === 0) }
101
+ ```
102
+
103
+ Default is `null` (no verify). Recommend always wiring one — a green deploy that nobody can reach is a yellow deploy.
104
+
105
+ ### Release model
106
+
107
+ - Every `deploy()` mints a new `releases/<YYYYMMDD-HHMMSS>/`.
108
+ - `current` symlink → newest release.
109
+ - `rollback(releaseId)` re-points the symlink and restarts. No re-upload, no re-build. Fast.
110
+ - `listReleases()` returns the sorted list.
111
+ - `prune({ keep: N })` removes the N oldest.
112
+
113
+ ## DigitalOcean Droplet — first deploy
114
+
115
+ Assuming a fresh Ubuntu/Debian Droplet:
116
+
117
+ ```bash
118
+ # 1. Install Bun on the Droplet (one-time):
119
+ ssh root@<droplet> 'curl -fsSL https://bun.sh/install | bash && ln -sf $HOME/.bun/bin/bun /usr/local/bin/bun'
120
+
121
+ # 2. Create a deploy user with sudo for systemctl (one-time):
122
+ ssh root@<droplet> 'adduser --disabled-password --gecos "" deploy && mkdir -p /home/deploy/.ssh'
123
+ ssh root@<droplet> 'cat >> /home/deploy/.ssh/authorized_keys' < ~/.ssh/id_ed25519.pub
124
+ ssh root@<droplet> 'chown -R deploy:deploy /home/deploy/.ssh && chmod 700 /home/deploy/.ssh && chmod 600 /home/deploy/.ssh/authorized_keys'
125
+
126
+ # 3. Now deploy:
127
+ bun run my-deploy-script.ts
128
+ ```
129
+
130
+ The first run creates `/srv/<appName>/releases/<id>/`, drops a systemd unit at `/etc/systemd/system/<appName>.service` (if you're using `systemdManager`), starts the service, and probes. Subsequent runs just add a new release dir and swap the symlink.
131
+
132
+ ## What v0.0.1 does NOT include
133
+
134
+ - Provider-specific HTTP-API adapters (Cloudflare Workers, Fly Machines, AWS Fargate, GCP Cloud Run).
135
+ - Bun installation on the remote — caller does it once, out of band.
136
+ - Multi-target / fan-out deploys (caller iterates).
137
+ - Zero-downtime port-swap (start new release on a fresh port, then nginx-reload). The default pipeline does stop-then-start; for true zero-downtime, replace the `restart` step.
138
+ - Secrets injection (use `@absolutejs/secrets` alongside, or set them as systemd `Environment=` lines).
139
+
140
+ ## Architectural role
141
+
142
+ - **`@absolutejs/runtime`** — in-process child-spawning. Use it INSIDE the deployed app for multi-tenant work; `@absolutejs/deploy` is what gets the app onto the box.
143
+ - **`@absolutejs/secrets`** — resolves credentials at request time. `deploy`'s `env` option is fine for boot-time config; secrets that rotate live in the secrets broker.
144
+ - **`@absolutejs/metering` + `/router`** — operate on the running app inside the deployed process; `deploy` doesn't touch them.
145
+
146
+ ## License
147
+
148
+ BSL 1.1 with a named carveout for the hosted application-deploy / git-push-deploy / repo-to-URL category (Vercel, Render, Railway, Fly.io's deploy half, Netlify, Heroku, Cloud66, Coolify, Cloudflare Pages, Cloudflare Workers deploy, DigitalOcean App Platform, Azure App Service deploy, AWS Amplify Hosting, AWS Elastic Beanstalk, GCP Cloud Run deploy). See [LICENSE](./LICENSE). Change Date: 4 years from first release; Change License: Apache 2.0.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * createDeployer — drives a step pipeline against a Target.
3
+ *
4
+ * The default pipeline (`defaultBunPipeline`) is the right thing for a Bun
5
+ * project on a Linux host: prepare → upload → install → build → link →
6
+ * restart → verify. Callers can replace steps wholesale or splice their own
7
+ * in via `steps: [...]`.
8
+ *
9
+ * Release model: every `deploy()` creates a fresh timestamped directory
10
+ * under `<root>/releases/`, uploads into it, then atomically swaps the
11
+ * `<root>/current` symlink. `rollback(id)` re-points the symlink and
12
+ * reloads the process manager — no re-upload, no re-build, just a fast
13
+ * switch.
14
+ */
15
+ import type { ProcessManager } from './processManagers';
16
+ import type { Target } from './targets';
17
+ export type Source = {
18
+ /** Local directory to copy. */
19
+ kind: 'directory';
20
+ root: string;
21
+ /** Globs excluded from upload. Defaults to common dev artifacts. */
22
+ exclude?: string[];
23
+ };
24
+ export type VerifySpec = {
25
+ kind: 'http';
26
+ url: string;
27
+ retries?: number;
28
+ intervalMs?: number;
29
+ expectStatus?: number;
30
+ } | {
31
+ kind: 'tcp';
32
+ host: string;
33
+ port: number;
34
+ retries?: number;
35
+ intervalMs?: number;
36
+ } | {
37
+ kind: 'custom';
38
+ check: (ctx: DeployContext) => Promise<boolean>;
39
+ };
40
+ export type DeployContext = {
41
+ target: Target;
42
+ source: Source;
43
+ releaseId: string;
44
+ releasePath: string;
45
+ currentPath: string;
46
+ appName: string;
47
+ env: Record<string, string>;
48
+ hooks: ResolvedHooks;
49
+ processManager: ProcessManager;
50
+ verify: VerifySpec | null;
51
+ };
52
+ export type DeployStep = {
53
+ name: string;
54
+ run: (ctx: DeployContext) => Promise<void>;
55
+ };
56
+ export type DeployHooks = {
57
+ onStepStart?: (step: {
58
+ name: string;
59
+ releaseId: string;
60
+ }) => void | Promise<void>;
61
+ onStepEnd?: (step: {
62
+ name: string;
63
+ releaseId: string;
64
+ durationMs: number;
65
+ }) => void | Promise<void>;
66
+ onLog?: (line: string, stream: 'stdout' | 'stderr', step: string) => void;
67
+ onError?: (error: {
68
+ step: string;
69
+ releaseId: string;
70
+ error: Error;
71
+ }) => void | Promise<void>;
72
+ };
73
+ type ResolvedHooks = Required<{
74
+ [K in keyof DeployHooks]: NonNullable<DeployHooks[K]>;
75
+ }>;
76
+ export type DeployerOptions = {
77
+ target: Target;
78
+ source: Source;
79
+ /** App name; used by ProcessManagers for unit names, pid files, log paths. Required. */
80
+ appName: string;
81
+ /** Where deploys live on the target. Default `/srv/<appName>`. */
82
+ rootPath?: string;
83
+ /** Steps in order. Default: `defaultBunPipeline()`. */
84
+ steps?: DeployStep[];
85
+ /** Env merged into install / build / start. */
86
+ env?: Record<string, string>;
87
+ /** Process manager. Default `bareManager()`. */
88
+ processManager?: ProcessManager;
89
+ /** How to verify the deploy is up. Default null (skip verify). */
90
+ verify?: VerifySpec | null;
91
+ hooks?: DeployHooks;
92
+ /** Override `Date.now` for deterministic release ids in tests. */
93
+ clock?: () => number;
94
+ };
95
+ export type DeployResult = {
96
+ releaseId: string;
97
+ releasePath: string;
98
+ currentPath: string;
99
+ durationMs: number;
100
+ steps: {
101
+ name: string;
102
+ durationMs: number;
103
+ }[];
104
+ };
105
+ export type Deployer = {
106
+ deploy: () => Promise<DeployResult>;
107
+ rollback: (releaseId: string) => Promise<DeployResult>;
108
+ listReleases: () => Promise<string[]>;
109
+ prune: (options: {
110
+ keep: number;
111
+ }) => Promise<{
112
+ removed: string[];
113
+ }>;
114
+ dispose: () => Promise<void>;
115
+ };
116
+ export declare const defaultBunPipeline: () => DeployStep[];
117
+ export declare const createDeployer: (options: DeployerOptions) => Deployer;
118
+ export {};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @absolutejs/deploy — generic Bun-project deploy pipeline.
3
+ *
4
+ * Surface:
5
+ *
6
+ * - `Target` + bundled `localTarget` / `sshTarget`
7
+ * - `ProcessManager` + bundled `bareManager` / `systemdManager`
8
+ * - `createDeployer({ target, source, appName, ... })` + `defaultBunPipeline()`
9
+ * - `VerifySpec` for HTTP / TCP / custom readiness checks
10
+ *
11
+ * See README for the typical "deploy to a DigitalOcean droplet" recipe.
12
+ */
13
+ export type { ExecOptions, ExecResult, LocalTargetOptions, SshTargetOptions, Target, UploadOptions, } from './targets';
14
+ export { localTarget, sshTarget } from './targets';
15
+ export type { BareManagerOptions, ProcessManager, ProcessManagerContext, SystemdManagerOptions, } from './processManagers';
16
+ export { bareManager, systemdManager } from './processManagers';
17
+ export type { DeployContext, DeployHooks, DeployResult, DeployStep, Deployer, DeployerOptions, Source, VerifySpec, } from './deployer';
18
+ export { createDeployer, defaultBunPipeline } from './deployer';
package/dist/index.js ADDED
@@ -0,0 +1,546 @@
1
+ // @bun
2
+ // src/targets.ts
3
+ import { mkdir } from "fs/promises";
4
+ import { join } from "path";
5
+ var decodeChunks = async (reader, onLine) => {
6
+ if (!reader)
7
+ return "";
8
+ const decoder = new TextDecoder;
9
+ let buffer = "";
10
+ let collected = "";
11
+ const stream = reader.getReader();
12
+ try {
13
+ while (true) {
14
+ const { done, value } = await stream.read();
15
+ if (done)
16
+ break;
17
+ const chunk = decoder.decode(value, { stream: true });
18
+ collected += chunk;
19
+ if (!onLine)
20
+ continue;
21
+ buffer += chunk;
22
+ let newline = buffer.indexOf(`
23
+ `);
24
+ while (newline !== -1) {
25
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
26
+ if (line.length > 0)
27
+ onLine(line);
28
+ buffer = buffer.slice(newline + 1);
29
+ newline = buffer.indexOf(`
30
+ `);
31
+ }
32
+ }
33
+ const tail = decoder.decode();
34
+ collected += tail;
35
+ if (onLine && (buffer + tail).length > 0)
36
+ onLine((buffer + tail).replace(/\r$/, ""));
37
+ } finally {
38
+ stream.releaseLock();
39
+ }
40
+ return collected;
41
+ };
42
+ var runSpawn = async (argv, options) => {
43
+ const proc = Bun.spawn(argv, {
44
+ cwd: options.cwd,
45
+ env: options.env,
46
+ stderr: "pipe",
47
+ stdin: options.stdin === undefined ? "ignore" : "pipe",
48
+ stdout: "pipe"
49
+ });
50
+ if (options.stdin !== undefined && proc.stdin) {
51
+ const writer = proc.stdin.getWriter?.();
52
+ if (writer) {
53
+ await writer.write(new TextEncoder().encode(options.stdin));
54
+ await writer.close();
55
+ }
56
+ }
57
+ const timeout = options.timeoutMs ?? 600000;
58
+ let timer;
59
+ if (timeout > 0) {
60
+ timer = setTimeout(() => {
61
+ try {
62
+ proc.kill();
63
+ } catch {}
64
+ }, timeout);
65
+ }
66
+ const stdoutPromise = decodeChunks(proc.stdout, options.onLog ? (line) => options.onLog(line, "stdout") : undefined);
67
+ const stderrPromise = decodeChunks(proc.stderr, options.onLog ? (line) => options.onLog(line, "stderr") : undefined);
68
+ const [stdout, stderr, exitCode] = await Promise.all([
69
+ stdoutPromise,
70
+ stderrPromise,
71
+ proc.exited
72
+ ]);
73
+ if (timer)
74
+ clearTimeout(timer);
75
+ return { exitCode: exitCode ?? -1, stderr, stdout };
76
+ };
77
+ var localTarget = (options) => {
78
+ const baseEnv = { ...options.env };
79
+ const ensureRoot = async () => {
80
+ await mkdir(options.root, { recursive: true });
81
+ };
82
+ return {
83
+ description: `local ${options.root}`,
84
+ exec: async (cmd, opts) => {
85
+ await ensureRoot();
86
+ return runSpawn(["sh", "-c", cmd], {
87
+ cwd: opts?.cwd ?? options.root,
88
+ env: { ...process.env, ...baseEnv, ...opts?.env ?? {} },
89
+ onLog: opts?.onLog,
90
+ stdin: opts?.stdin,
91
+ timeoutMs: opts?.timeoutMs
92
+ });
93
+ },
94
+ upload: async (localPath, remotePath, opts) => {
95
+ await ensureRoot();
96
+ const dest = remotePath.startsWith("/") ? remotePath : join(options.root, remotePath);
97
+ const argv = ["rsync", "-a"];
98
+ if (opts?.deleteOrphans)
99
+ argv.push("--delete");
100
+ for (const pattern of opts?.exclude ?? [])
101
+ argv.push("--exclude", pattern);
102
+ argv.push(localPath, dest);
103
+ const result = await runSpawn(argv, { timeoutMs: 600000 });
104
+ if (result.exitCode !== 0) {
105
+ throw new Error(`local upload failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
106
+ }
107
+ }
108
+ };
109
+ };
110
+ var sshTargetString = (options) => {
111
+ const user = options.user ?? "root";
112
+ return `${user}@${options.host}`;
113
+ };
114
+ var sshBaseFlags = (options) => {
115
+ const flags = [];
116
+ if (options.port !== undefined && options.port !== 22)
117
+ flags.push("-p", String(options.port));
118
+ if (options.identity !== undefined)
119
+ flags.push("-i", options.identity);
120
+ flags.push("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new");
121
+ for (const flag of options.sshFlags ?? [])
122
+ flags.push(flag);
123
+ return flags;
124
+ };
125
+ var shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
126
+ var buildRemoteCmd = (cmd, opts) => {
127
+ const env = opts?.env;
128
+ const envPrefix = env ? Object.entries(env).map(([k, v]) => `${k}=${shellQuote(v)}`).join(" ") + " " : "";
129
+ if (opts?.cwd) {
130
+ return `cd ${shellQuote(opts.cwd)} && ${envPrefix}${cmd}`;
131
+ }
132
+ return `${envPrefix}${cmd}`;
133
+ };
134
+ var sshTarget = (options) => {
135
+ const remote = sshTargetString(options);
136
+ const useRsync = options.rsync ?? true;
137
+ return {
138
+ description: `ssh ${remote}${options.port && options.port !== 22 ? `:${options.port}` : ""}`,
139
+ exec: async (cmd, opts) => {
140
+ const argv = ["ssh", ...sshBaseFlags(options)];
141
+ for (const name of options.forwardEnv ?? [])
142
+ argv.push("-o", `SendEnv=${name}`);
143
+ argv.push(remote, buildRemoteCmd(cmd, opts));
144
+ return runSpawn(argv, {
145
+ onLog: opts?.onLog,
146
+ stdin: opts?.stdin,
147
+ timeoutMs: opts?.timeoutMs
148
+ });
149
+ },
150
+ upload: async (localPath, remotePath, opts) => {
151
+ if (useRsync) {
152
+ const sshCmd = ["ssh", ...sshBaseFlags(options)].map((part) => part.includes(" ") ? `'${part}'` : part).join(" ");
153
+ const argv2 = ["rsync", "-az", "-e", sshCmd];
154
+ if (opts?.deleteOrphans)
155
+ argv2.push("--delete");
156
+ for (const pattern of opts?.exclude ?? [])
157
+ argv2.push("--exclude", pattern);
158
+ argv2.push(localPath, `${remote}:${remotePath}`);
159
+ const result2 = await runSpawn(argv2, { timeoutMs: 600000 });
160
+ if (result2.exitCode !== 0) {
161
+ throw new Error(`rsync upload failed (exit ${result2.exitCode}): ${result2.stderr || result2.stdout}`);
162
+ }
163
+ return;
164
+ }
165
+ const argv = ["scp", "-r", ...sshBaseFlags(options), localPath, `${remote}:${remotePath}`];
166
+ const result = await runSpawn(argv, { timeoutMs: 600000 });
167
+ if (result.exitCode !== 0) {
168
+ throw new Error(`scp upload failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
169
+ }
170
+ }
171
+ };
172
+ };
173
+ // src/processManagers.ts
174
+ var pidPath = (appName) => `/var/lib/${appName}/${appName}.pid`;
175
+ var logDir = (appName) => `/var/log/${appName}`;
176
+ var bareManager = (options = {}) => {
177
+ const command = options.command ?? "bun run start";
178
+ const logBase = options.logFileBaseName ?? "app";
179
+ const envPrefix = (env) => Object.entries(env).map(([k, v]) => `${k}='${v.replace(/'/g, `'\\''`)}'`).join(" ");
180
+ const startCmd = (ctx) => {
181
+ const env = envPrefix(ctx.env);
182
+ const pid = pidPath(ctx.appName);
183
+ const out = `${logDir(ctx.appName)}/${logBase}.out.log`;
184
+ const err = `${logDir(ctx.appName)}/${logBase}.err.log`;
185
+ return `
186
+ mkdir -p $(dirname ${pid}) ${logDir(ctx.appName)} &&
187
+ cd ${ctx.currentPath} &&
188
+ nohup env ${env} sh -c '${command.replace(/'/g, `'\\''`)}' >> ${out} 2>> ${err} &
189
+ echo $! > ${pid}
190
+ `.trim();
191
+ };
192
+ const stopCmd = (ctx) => `
193
+ PID=$(cat ${pidPath(ctx.appName)} 2>/dev/null || true);
194
+ if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
195
+ kill "$PID" 2>/dev/null || true;
196
+ for i in 1 2 3 4 5; do
197
+ if ! kill -0 "$PID" 2>/dev/null; then break; fi;
198
+ sleep 1;
199
+ done;
200
+ kill -9 "$PID" 2>/dev/null || true;
201
+ fi
202
+ rm -f ${pidPath(ctx.appName)}
203
+ `.trim();
204
+ return {
205
+ reload: async (target, ctx) => {
206
+ const stop = await target.exec(stopCmd(ctx), { onLog: ctx.onLog, timeoutMs: 30000 });
207
+ if (stop.exitCode !== 0) {
208
+ throw new Error(`bareManager.stop failed (exit ${stop.exitCode}): ${stop.stderr}`);
209
+ }
210
+ const start = await target.exec(startCmd(ctx), { onLog: ctx.onLog, timeoutMs: 30000 });
211
+ if (start.exitCode !== 0) {
212
+ throw new Error(`bareManager.start failed (exit ${start.exitCode}): ${start.stderr}`);
213
+ }
214
+ },
215
+ status: async (target, ctx) => {
216
+ const result = await target.exec(`PID=$(cat ${pidPath(ctx.appName)} 2>/dev/null || true); if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then echo running; else echo stopped; fi`, { timeoutMs: 5000 });
217
+ const out = result.stdout.trim();
218
+ if (out === "running")
219
+ return "running";
220
+ if (out === "stopped")
221
+ return "stopped";
222
+ return "unknown";
223
+ },
224
+ stop: async (target, ctx) => {
225
+ const result = await target.exec(stopCmd(ctx), { onLog: ctx.onLog, timeoutMs: 30000 });
226
+ if (result.exitCode !== 0) {
227
+ throw new Error(`bareManager.stop failed (exit ${result.exitCode}): ${result.stderr}`);
228
+ }
229
+ }
230
+ };
231
+ };
232
+ var renderSystemdUnit = (ctx, options) => {
233
+ const user = options.user ?? "deploy";
234
+ const group = options.group ?? user;
235
+ const execStart = options.execStart ?? "/usr/local/bin/bun run start";
236
+ const restart = options.restart ?? "always";
237
+ const envLines = Object.entries(ctx.env).map(([k, v]) => `Environment=${k}=${v.replace(/"/g, "\\\"")}`).join(`
238
+ `);
239
+ return `[Unit]
240
+ Description=${ctx.appName} (managed by @absolutejs/deploy)
241
+ After=network.target
242
+
243
+ [Service]
244
+ Type=simple
245
+ WorkingDirectory=${ctx.currentPath}
246
+ ExecStart=${execStart}
247
+ Restart=${restart}
248
+ RestartSec=2
249
+ User=${user}
250
+ Group=${group}
251
+ ${envLines}
252
+ StandardOutput=append:/var/log/${ctx.appName}/app.out.log
253
+ StandardError=append:/var/log/${ctx.appName}/app.err.log
254
+
255
+ [Install]
256
+ WantedBy=multi-user.target
257
+ `;
258
+ };
259
+ var systemdManager = (options = {}) => {
260
+ const systemctl = options.systemctl ?? "systemctl";
261
+ const unitDir = options.unitDir ?? "/etc/systemd/system";
262
+ const unitName = (ctx) => options.unitName ?? `${ctx.appName}.service`;
263
+ return {
264
+ reload: async (target, ctx) => {
265
+ const unit = renderSystemdUnit(ctx, options);
266
+ const name = unitName(ctx);
267
+ const writeUnit = await target.exec(`mkdir -p /var/log/${ctx.appName} && cat > ${unitDir}/${name}`, { onLog: ctx.onLog, stdin: unit, timeoutMs: 30000 });
268
+ if (writeUnit.exitCode !== 0) {
269
+ throw new Error(`systemdManager: writing unit failed (exit ${writeUnit.exitCode}): ${writeUnit.stderr}`);
270
+ }
271
+ const reload = await target.exec(`${systemctl} daemon-reload`, { onLog: ctx.onLog, timeoutMs: 15000 });
272
+ if (reload.exitCode !== 0) {
273
+ throw new Error(`systemdManager: daemon-reload failed (exit ${reload.exitCode}): ${reload.stderr}`);
274
+ }
275
+ const enable = await target.exec(`${systemctl} enable ${name}`, { onLog: ctx.onLog, timeoutMs: 15000 });
276
+ if (enable.exitCode !== 0) {
277
+ throw new Error(`systemdManager: enable failed (exit ${enable.exitCode}): ${enable.stderr}`);
278
+ }
279
+ const restart = await target.exec(`${systemctl} restart ${name}`, { onLog: ctx.onLog, timeoutMs: 60000 });
280
+ if (restart.exitCode !== 0) {
281
+ throw new Error(`systemdManager: restart failed (exit ${restart.exitCode}): ${restart.stderr}`);
282
+ }
283
+ },
284
+ status: async (target, ctx) => {
285
+ const result = await target.exec(`${systemctl} is-active ${unitName(ctx)} || true`, { timeoutMs: 1e4 });
286
+ const out = result.stdout.trim();
287
+ if (out === "active")
288
+ return "running";
289
+ if (out === "inactive" || out === "failed")
290
+ return "stopped";
291
+ return "unknown";
292
+ },
293
+ stop: async (target, ctx) => {
294
+ const result = await target.exec(`${systemctl} stop ${unitName(ctx)}`, { onLog: ctx.onLog, timeoutMs: 30000 });
295
+ if (result.exitCode !== 0) {
296
+ throw new Error(`systemdManager: stop failed (exit ${result.exitCode}): ${result.stderr}`);
297
+ }
298
+ }
299
+ };
300
+ };
301
+ // src/deployer.ts
302
+ var DEFAULT_EXCLUDES = ["node_modules", "dist", "build", ".git", ".DS_Store", "*.log"];
303
+ var noopHooks = {
304
+ onError: () => {},
305
+ onLog: () => {},
306
+ onStepEnd: () => {},
307
+ onStepStart: () => {}
308
+ };
309
+ var resolveHooks = (hooks) => ({
310
+ onError: hooks?.onError ?? noopHooks.onError,
311
+ onLog: hooks?.onLog ?? noopHooks.onLog,
312
+ onStepEnd: hooks?.onStepEnd ?? noopHooks.onStepEnd,
313
+ onStepStart: hooks?.onStepStart ?? noopHooks.onStepStart
314
+ });
315
+ var makeReleaseId = (clock) => {
316
+ const t = clock();
317
+ const date = new Date(t);
318
+ const pad = (n, w = 2) => n.toString().padStart(w, "0");
319
+ return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;
320
+ };
321
+ var requireSuccess = (label, result) => {
322
+ if (result.exitCode !== 0) {
323
+ throw new Error(`${label} failed (exit ${result.exitCode}): ${result.stderr || result.stdout || "(no output)"}`);
324
+ }
325
+ };
326
+ var defaultBunPipeline = () => [
327
+ {
328
+ name: "prepare",
329
+ run: async (ctx) => {
330
+ const result = await ctx.target.exec(`mkdir -p ${ctx.releasePath}`, { onLog: (line, stream) => ctx.hooks.onLog(line, stream, "prepare") });
331
+ requireSuccess("prepare: mkdir", result);
332
+ }
333
+ },
334
+ {
335
+ name: "upload",
336
+ run: async (ctx) => {
337
+ if (ctx.source.kind !== "directory") {
338
+ throw new Error(`Unsupported source kind: ${ctx.source.kind}`);
339
+ }
340
+ const localPath = ctx.source.root.endsWith("/") ? ctx.source.root : `${ctx.source.root}/`;
341
+ await ctx.target.upload(localPath, ctx.releasePath, {
342
+ exclude: ctx.source.exclude ?? DEFAULT_EXCLUDES
343
+ });
344
+ }
345
+ },
346
+ {
347
+ name: "install",
348
+ run: async (ctx) => {
349
+ const result = await ctx.target.exec(`bun install --production`, {
350
+ cwd: ctx.releasePath,
351
+ env: ctx.env,
352
+ onLog: (line, stream) => ctx.hooks.onLog(line, stream, "install"),
353
+ timeoutMs: 600000
354
+ });
355
+ requireSuccess("install", result);
356
+ }
357
+ },
358
+ {
359
+ name: "build",
360
+ run: async (ctx) => {
361
+ const probe = await ctx.target.exec(`grep -E '"build"\\s*:' package.json || true`, { cwd: ctx.releasePath, timeoutMs: 1e4 });
362
+ if (!probe.stdout.includes('"build"'))
363
+ return;
364
+ const result = await ctx.target.exec(`bun run build`, {
365
+ cwd: ctx.releasePath,
366
+ env: ctx.env,
367
+ onLog: (line, stream) => ctx.hooks.onLog(line, stream, "build"),
368
+ timeoutMs: 600000
369
+ });
370
+ requireSuccess("build", result);
371
+ }
372
+ },
373
+ {
374
+ name: "link",
375
+ run: async (ctx) => {
376
+ const tmpLink = `${ctx.currentPath}.next`;
377
+ const result = await ctx.target.exec(`ln -sfn ${ctx.releasePath} ${tmpLink} && mv -Tf ${tmpLink} ${ctx.currentPath}`, { onLog: (line, stream) => ctx.hooks.onLog(line, stream, "link"), timeoutMs: 1e4 });
378
+ requireSuccess("link", result);
379
+ }
380
+ },
381
+ {
382
+ name: "restart",
383
+ run: async (ctx) => {
384
+ await ctx.processManager.reload(ctx.target, {
385
+ appName: ctx.appName,
386
+ currentPath: ctx.currentPath,
387
+ env: ctx.env,
388
+ onLog: (line, stream) => ctx.hooks.onLog(line, stream, "restart"),
389
+ releaseId: ctx.releaseId,
390
+ releasePath: ctx.releasePath
391
+ });
392
+ }
393
+ },
394
+ {
395
+ name: "verify",
396
+ run: async (ctx) => {
397
+ if (!ctx.verify)
398
+ return;
399
+ await runVerify(ctx);
400
+ }
401
+ }
402
+ ];
403
+ var runVerify = async (ctx) => {
404
+ const spec = ctx.verify;
405
+ if (spec.kind === "custom") {
406
+ const ok = await spec.check(ctx);
407
+ if (!ok)
408
+ throw new Error("verify: custom check returned false");
409
+ return;
410
+ }
411
+ if (spec.kind === "http") {
412
+ const retries2 = spec.retries ?? 30;
413
+ const intervalMs2 = spec.intervalMs ?? 1000;
414
+ const expectStatus = spec.expectStatus ?? 200;
415
+ for (let attempt = 0;attempt <= retries2; attempt++) {
416
+ const probe = await ctx.target.exec(`curl -s -o /dev/null -w '%{http_code}' --max-time 5 ${spec.url}`, { timeoutMs: 1e4 });
417
+ const code = Number(probe.stdout.trim());
418
+ if (code === expectStatus)
419
+ return;
420
+ if (attempt < retries2)
421
+ await new Promise((resolve) => setTimeout(resolve, intervalMs2));
422
+ }
423
+ throw new Error(`verify: HTTP ${spec.url} did not return ${expectStatus} after ${retries2} retries`);
424
+ }
425
+ const retries = spec.retries ?? 30;
426
+ const intervalMs = spec.intervalMs ?? 1000;
427
+ for (let attempt = 0;attempt <= retries; attempt++) {
428
+ const probe = await ctx.target.exec(`bash -c 'cat < /dev/tcp/${spec.host}/${spec.port}' 2>/dev/null && echo open || echo closed`, { timeoutMs: 1e4 });
429
+ if (probe.stdout.includes("open"))
430
+ return;
431
+ if (attempt < retries)
432
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
433
+ }
434
+ throw new Error(`verify: TCP ${spec.host}:${spec.port} not open after ${retries} retries`);
435
+ };
436
+ var createDeployer = (options) => {
437
+ const clock = options.clock ?? Date.now;
438
+ const hooks = resolveHooks(options.hooks);
439
+ const rootPath = options.rootPath ?? `/srv/${options.appName}`;
440
+ const currentPath = `${rootPath}/current`;
441
+ const releasesPath = `${rootPath}/releases`;
442
+ const env = { NODE_ENV: "production", ...options.env };
443
+ const processManager = options.processManager ?? bareManager();
444
+ const verify = options.verify === undefined ? null : options.verify;
445
+ let disposed = false;
446
+ const buildCtx = (releaseId) => ({
447
+ appName: options.appName,
448
+ currentPath,
449
+ env,
450
+ hooks,
451
+ processManager,
452
+ releaseId,
453
+ releasePath: `${releasesPath}/${releaseId}`,
454
+ source: options.source,
455
+ target: options.target,
456
+ verify
457
+ });
458
+ const runSteps = async (steps, releaseId) => {
459
+ const ctx = buildCtx(releaseId);
460
+ const stepDurations = [];
461
+ const startedAt = clock();
462
+ for (const step of steps) {
463
+ const stepStartedAt = clock();
464
+ await hooks.onStepStart({ name: step.name, releaseId });
465
+ try {
466
+ await step.run(ctx);
467
+ } catch (error) {
468
+ const err = error instanceof Error ? error : new Error(String(error));
469
+ await hooks.onError({ error: err, releaseId, step: step.name });
470
+ throw err;
471
+ }
472
+ const durationMs = clock() - stepStartedAt;
473
+ stepDurations.push({ durationMs, name: step.name });
474
+ await hooks.onStepEnd({ durationMs, name: step.name, releaseId });
475
+ }
476
+ return {
477
+ currentPath,
478
+ durationMs: clock() - startedAt,
479
+ releaseId,
480
+ releasePath: ctx.releasePath,
481
+ steps: stepDurations
482
+ };
483
+ };
484
+ const ensureRoot = async () => {
485
+ const result = await options.target.exec(`mkdir -p ${releasesPath}`, { timeoutMs: 1e4 });
486
+ requireSuccess("ensureRoot", result);
487
+ };
488
+ return {
489
+ deploy: async () => {
490
+ if (disposed)
491
+ throw new Error("Deployer is disposed");
492
+ await ensureRoot();
493
+ const releaseId = makeReleaseId(clock);
494
+ return runSteps(options.steps ?? defaultBunPipeline(), releaseId);
495
+ },
496
+ dispose: async () => {
497
+ disposed = true;
498
+ if (options.target.close)
499
+ await options.target.close();
500
+ },
501
+ listReleases: async () => {
502
+ await ensureRoot();
503
+ const result = await options.target.exec(`ls -1 ${releasesPath} 2>/dev/null || true`, { timeoutMs: 1e4 });
504
+ return result.stdout.split(`
505
+ `).map((line) => line.trim()).filter((line) => line.length > 0).sort();
506
+ },
507
+ prune: async ({ keep }) => {
508
+ if (disposed)
509
+ throw new Error("Deployer is disposed");
510
+ const all = await (async () => {
511
+ const result = await options.target.exec(`ls -1 ${releasesPath} 2>/dev/null || true`, { timeoutMs: 1e4 });
512
+ return result.stdout.split(`
513
+ `).map((line) => line.trim()).filter((line) => line.length > 0).sort();
514
+ })();
515
+ if (all.length <= keep)
516
+ return { removed: [] };
517
+ const removed = all.slice(0, all.length - keep);
518
+ for (const releaseId of removed) {
519
+ await options.target.exec(`rm -rf ${releasesPath}/${releaseId}`, { timeoutMs: 60000 });
520
+ }
521
+ return { removed };
522
+ },
523
+ rollback: async (releaseId) => {
524
+ if (disposed)
525
+ throw new Error("Deployer is disposed");
526
+ await ensureRoot();
527
+ const exists = await options.target.exec(`test -d ${releasesPath}/${releaseId} && echo ok || echo missing`, { timeoutMs: 5000 });
528
+ if (!exists.stdout.includes("ok")) {
529
+ throw new Error(`rollback: release ${releaseId} not found at ${releasesPath}/${releaseId}`);
530
+ }
531
+ const rollbackSteps = defaultBunPipeline().filter((step) => step.name === "link" || step.name === "restart" || step.name === "verify");
532
+ return runSteps(rollbackSteps, releaseId);
533
+ }
534
+ };
535
+ };
536
+ export {
537
+ systemdManager,
538
+ sshTarget,
539
+ localTarget,
540
+ defaultBunPipeline,
541
+ createDeployer,
542
+ bareManager
543
+ };
544
+
545
+ //# debugId=044FAFA4F653318F64756E2164756E21
546
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/targets.ts", "../src/processManagers.ts", "../src/deployer.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Target interface + bundled adapters (localTarget, sshTarget).\n *\n * A Target is the narrowest abstraction over \"a place I can deploy to\":\n *\n * - `exec(cmd, opts?)` — run a shell command, capture stdout/stderr/exitCode.\n * - `upload(localPath, remotePath, opts?)` — copy a local file or directory\n * to the target. Implementation is free to use whatever is fast (rsync,\n * scp, mv).\n * - `close?()` — optional teardown.\n *\n * Two adapters are bundled:\n *\n * - `localTarget` runs in a temp directory on the local filesystem. Useful\n * for tests and for \"deploy\" workflows that happen on the same host.\n * - `sshTarget` shells out to the system `ssh` and `rsync` binaries. No\n * `ssh2` npm dependency — the controller machine just needs `ssh` and\n * (optionally) `rsync` in PATH, which is universal on Mac/Linux/WSL.\n *\n * Provider-specific targets (Cloudflare Workers HTTP API, Fly Machines API,\n * AWS Fargate) don't fit \"exec + upload\" and ship as siblings later.\n */\n\nimport { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport type ExecOptions = {\n\t/** Working directory on the target. Default: target's root. */\n\tcwd?: string;\n\t/** Env vars to set for this command (merged onto target.env). */\n\tenv?: Record<string, string>;\n\t/** Hard kill after this many ms. Default 600_000 (10 min). 0 disables. */\n\ttimeoutMs?: number;\n\t/** Pipe stdout/stderr through here as it streams (lines, newline-stripped). */\n\tonLog?: (line: string, stream: 'stdout' | 'stderr') => void;\n\t/** Stdin payload — a string is written verbatim. */\n\tstdin?: string;\n};\n\nexport type ExecResult = {\n\tstdout: string;\n\tstderr: string;\n\texitCode: number;\n};\n\nexport type UploadOptions = {\n\t/** Exclude paths matching these globs from a directory upload. */\n\texclude?: string[];\n\t/** When uploading a directory, delete remote files not present locally. */\n\tdeleteOrphans?: boolean;\n};\n\nexport type Target = {\n\t/** Human-readable description (e.g. \"ssh root@droplet-1.example.com\"). */\n\treadonly description: string;\n\texec: (cmd: string, opts?: ExecOptions) => Promise<ExecResult>;\n\tupload: (localPath: string, remotePath: string, opts?: UploadOptions) => Promise<void>;\n\tclose?: () => Promise<void>;\n};\n\n// -----------------------------------------------------------------------------\n// localTarget\n// -----------------------------------------------------------------------------\n\nexport type LocalTargetOptions = {\n\t/** Root directory the target operates in. Created if missing. */\n\troot: string;\n\t/** Env merged into every exec. */\n\tenv?: Record<string, string>;\n};\n\nconst decodeChunks = async (\n\treader: ReadableStream<Uint8Array> | null,\n\tonLine: ((line: string) => void) | undefined,\n): Promise<string> => {\n\tif (!reader) return '';\n\tconst decoder = new TextDecoder();\n\tlet buffer = '';\n\tlet collected = '';\n\tconst stream = reader.getReader();\n\ttry {\n\t\twhile (true) {\n\t\t\tconst { done, value } = await stream.read();\n\t\t\tif (done) break;\n\t\t\tconst chunk = decoder.decode(value, { stream: true });\n\t\t\tcollected += chunk;\n\t\t\tif (!onLine) continue;\n\t\t\tbuffer += chunk;\n\t\t\tlet newline = buffer.indexOf('\\n');\n\t\t\twhile (newline !== -1) {\n\t\t\t\tconst line = buffer.slice(0, newline).replace(/\\r$/, '');\n\t\t\t\tif (line.length > 0) onLine(line);\n\t\t\t\tbuffer = buffer.slice(newline + 1);\n\t\t\t\tnewline = buffer.indexOf('\\n');\n\t\t\t}\n\t\t}\n\t\tconst tail = decoder.decode();\n\t\tcollected += tail;\n\t\tif (onLine && (buffer + tail).length > 0) onLine((buffer + tail).replace(/\\r$/, ''));\n\t} finally {\n\t\tstream.releaseLock();\n\t}\n\treturn collected;\n};\n\nconst runSpawn = async (\n\targv: string[],\n\toptions: {\n\t\tcwd?: string;\n\t\tenv?: Record<string, string>;\n\t\ttimeoutMs?: number;\n\t\tonLog?: ExecOptions['onLog'];\n\t\tstdin?: string;\n\t},\n): Promise<ExecResult> => {\n\tconst proc = Bun.spawn(argv, {\n\t\tcwd: options.cwd,\n\t\tenv: options.env,\n\t\tstderr: 'pipe',\n\t\tstdin: options.stdin === undefined ? 'ignore' : 'pipe',\n\t\tstdout: 'pipe',\n\t});\n\n\tif (options.stdin !== undefined && proc.stdin) {\n\t\tconst writer = (proc.stdin as { getWriter?: () => { write: (data: Uint8Array) => Promise<void>; close: () => Promise<void> } }).getWriter?.();\n\t\tif (writer) {\n\t\t\tawait writer.write(new TextEncoder().encode(options.stdin));\n\t\t\tawait writer.close();\n\t\t}\n\t}\n\n\tconst timeout = options.timeoutMs ?? 600_000;\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tif (timeout > 0) {\n\t\ttimer = setTimeout(() => {\n\t\t\ttry { proc.kill(); } catch { /* already gone */ }\n\t\t}, timeout);\n\t}\n\n\tconst stdoutPromise = decodeChunks(\n\t\tproc.stdout as unknown as ReadableStream<Uint8Array>,\n\t\toptions.onLog ? (line) => options.onLog!(line, 'stdout') : undefined,\n\t);\n\tconst stderrPromise = decodeChunks(\n\t\tproc.stderr as unknown as ReadableStream<Uint8Array>,\n\t\toptions.onLog ? (line) => options.onLog!(line, 'stderr') : undefined,\n\t);\n\n\tconst [stdout, stderr, exitCode] = await Promise.all([\n\t\tstdoutPromise,\n\t\tstderrPromise,\n\t\tproc.exited,\n\t]);\n\tif (timer) clearTimeout(timer);\n\n\treturn { exitCode: exitCode ?? -1, stderr, stdout };\n};\n\nexport const localTarget = (options: LocalTargetOptions): Target => {\n\tconst baseEnv = { ...options.env };\n\tconst ensureRoot = async () => { await mkdir(options.root, { recursive: true }); };\n\n\treturn {\n\t\tdescription: `local ${options.root}`,\n\t\texec: async (cmd, opts) => {\n\t\t\tawait ensureRoot();\n\t\t\treturn runSpawn(['sh', '-c', cmd], {\n\t\t\t\tcwd: opts?.cwd ?? options.root,\n\t\t\t\tenv: { ...process.env, ...baseEnv, ...(opts?.env ?? {}) } as Record<string, string>,\n\t\t\t\tonLog: opts?.onLog,\n\t\t\t\tstdin: opts?.stdin,\n\t\t\t\ttimeoutMs: opts?.timeoutMs,\n\t\t\t});\n\t\t},\n\t\tupload: async (localPath, remotePath, opts) => {\n\t\t\tawait ensureRoot();\n\t\t\tconst dest = remotePath.startsWith('/') ? remotePath : join(options.root, remotePath);\n\t\t\tconst argv = ['rsync', '-a'];\n\t\t\tif (opts?.deleteOrphans) argv.push('--delete');\n\t\t\tfor (const pattern of opts?.exclude ?? []) argv.push('--exclude', pattern);\n\t\t\t// rsync semantics: a trailing slash on the source copies *contents*; without it the dir itself is nested.\n\t\t\targv.push(localPath, dest);\n\t\t\tconst result = await runSpawn(argv, { timeoutMs: 600_000 });\n\t\t\tif (result.exitCode !== 0) {\n\t\t\t\tthrow new Error(`local upload failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);\n\t\t\t}\n\t\t},\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// sshTarget\n// -----------------------------------------------------------------------------\n\nexport type SshTargetOptions = {\n\t/** Hostname or IP of the remote. */\n\thost: string;\n\t/** Login user. Default `root`. */\n\tuser?: string;\n\t/** SSH port. Default 22. */\n\tport?: number;\n\t/** Path to SSH identity file. Default: ssh's own search. */\n\tidentity?: string;\n\t/** Extra flags appended to every `ssh` invocation. */\n\tsshFlags?: string[];\n\t/**\n\t * Use rsync for `upload`. Default true. When false, falls back to `scp`\n\t * which is universal but doesn't support delete / exclude.\n\t */\n\trsync?: boolean;\n\t/**\n\t * Env vars to forward via `ssh -o SendEnv=...`. Most remote sshd configs\n\t * accept only `LANG` and `LC_*` by default; for app env vars use the\n\t * step `env` option instead, which prepends `KEY=value` to the command.\n\t */\n\tforwardEnv?: string[];\n};\n\nconst sshTargetString = (options: SshTargetOptions): string => {\n\tconst user = options.user ?? 'root';\n\treturn `${user}@${options.host}`;\n};\n\nconst sshBaseFlags = (options: SshTargetOptions): string[] => {\n\tconst flags: string[] = [];\n\tif (options.port !== undefined && options.port !== 22) flags.push('-p', String(options.port));\n\tif (options.identity !== undefined) flags.push('-i', options.identity);\n\t// Never get stuck on a host-key prompt; treat unknown hosts as a fatal config issue rather than a UX detour.\n\tflags.push('-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new');\n\tfor (const flag of options.sshFlags ?? []) flags.push(flag);\n\treturn flags;\n};\n\nconst shellQuote = (value: string): string => `'${value.replace(/'/g, `'\\\\''`)}'`;\n\nconst buildRemoteCmd = (cmd: string, opts: ExecOptions | undefined): string => {\n\tconst env = opts?.env;\n\tconst envPrefix = env\n\t\t? Object.entries(env).map(([k, v]) => `${k}=${shellQuote(v)}`).join(' ') + ' '\n\t\t: '';\n\tif (opts?.cwd) {\n\t\treturn `cd ${shellQuote(opts.cwd)} && ${envPrefix}${cmd}`;\n\t}\n\treturn `${envPrefix}${cmd}`;\n};\n\nexport const sshTarget = (options: SshTargetOptions): Target => {\n\tconst remote = sshTargetString(options);\n\tconst useRsync = options.rsync ?? true;\n\n\treturn {\n\t\tdescription: `ssh ${remote}${options.port && options.port !== 22 ? `:${options.port}` : ''}`,\n\t\texec: async (cmd, opts) => {\n\t\t\tconst argv = ['ssh', ...sshBaseFlags(options)];\n\t\t\tfor (const name of options.forwardEnv ?? []) argv.push('-o', `SendEnv=${name}`);\n\t\t\targv.push(remote, buildRemoteCmd(cmd, opts));\n\t\t\treturn runSpawn(argv, {\n\t\t\t\tonLog: opts?.onLog,\n\t\t\t\tstdin: opts?.stdin,\n\t\t\t\ttimeoutMs: opts?.timeoutMs,\n\t\t\t});\n\t\t},\n\t\tupload: async (localPath, remotePath, opts) => {\n\t\t\tif (useRsync) {\n\t\t\t\tconst sshCmd = ['ssh', ...sshBaseFlags(options)].map((part) => part.includes(' ') ? `'${part}'` : part).join(' ');\n\t\t\t\tconst argv = ['rsync', '-az', '-e', sshCmd];\n\t\t\t\tif (opts?.deleteOrphans) argv.push('--delete');\n\t\t\t\tfor (const pattern of opts?.exclude ?? []) argv.push('--exclude', pattern);\n\t\t\t\targv.push(localPath, `${remote}:${remotePath}`);\n\t\t\t\tconst result = await runSpawn(argv, { timeoutMs: 600_000 });\n\t\t\t\tif (result.exitCode !== 0) {\n\t\t\t\t\tthrow new Error(`rsync upload failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// scp fallback — no exclude, no delete. We still need -r to copy directories.\n\t\t\tconst argv = ['scp', '-r', ...sshBaseFlags(options), localPath, `${remote}:${remotePath}`];\n\t\t\tconst result = await runSpawn(argv, { timeoutMs: 600_000 });\n\t\t\tif (result.exitCode !== 0) {\n\t\t\t\tthrow new Error(`scp upload failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);\n\t\t\t}\n\t\t},\n\t};\n};\n",
6
+ "/**\n * ProcessManager — the abstraction that turns \"files are on the target\" into\n * \"the app is running.\" Two strategies ship: `bareManager` (nohup, lowest\n * dependency) and `systemdManager` (templated unit file, the way production\n * VMs should run).\n *\n * Callers can supply their own — anything that implements `start` / `stop` /\n * `reload` / `status` against a `Target` works. PM2, supervisord, runit,\n * @absolutejs/runtime all fit if someone writes the adapter.\n */\n\nimport type { Target } from './targets';\n\nexport type ProcessManagerContext = {\n\t/** Absolute path on the target to the active release dir (the symlink target). */\n\tcurrentPath: string;\n\t/** Absolute path on the target to the new release dir we just uploaded. */\n\treleasePath: string;\n\t/** Release id (timestamped). */\n\treleaseId: string;\n\t/** App name — supplied via deployer config; used for unit names, pid files, etc. */\n\tappName: string;\n\t/** Optional env to set on the process. */\n\tenv: Record<string, string>;\n\t/** Log sink for any commands the manager runs. */\n\tonLog?: (line: string, stream: 'stdout' | 'stderr') => void;\n};\n\nexport type ProcessManager = {\n\t/** Bring the new release up. Called after the `current` symlink has been swapped. */\n\treload: (target: Target, ctx: ProcessManagerContext) => Promise<void>;\n\t/** Stop the running process. */\n\tstop?: (target: Target, ctx: ProcessManagerContext) => Promise<void>;\n\t/** Return current status (best-effort; used by callers for diagnostics). */\n\tstatus?: (target: Target, ctx: ProcessManagerContext) => Promise<'running' | 'stopped' | 'unknown'>;\n};\n\n// -----------------------------------------------------------------------------\n// bareManager — nohup background + pid file\n// -----------------------------------------------------------------------------\n\nexport type BareManagerOptions = {\n\t/** Command to run. Default `bun run start`. */\n\tcommand?: string;\n\t/** Log files inside the app's data dir (default: alongside pid). */\n\tlogFileBaseName?: string;\n};\n\nconst pidPath = (appName: string) => `/var/lib/${appName}/${appName}.pid`;\nconst logDir = (appName: string) => `/var/log/${appName}`;\n\nexport const bareManager = (options: BareManagerOptions = {}): ProcessManager => {\n\tconst command = options.command ?? 'bun run start';\n\tconst logBase = options.logFileBaseName ?? 'app';\n\n\tconst envPrefix = (env: Record<string, string>) =>\n\t\tObject.entries(env).map(([k, v]) => `${k}='${v.replace(/'/g, `'\\\\''`)}'`).join(' ');\n\n\tconst startCmd = (ctx: ProcessManagerContext): string => {\n\t\tconst env = envPrefix(ctx.env);\n\t\tconst pid = pidPath(ctx.appName);\n\t\tconst out = `${logDir(ctx.appName)}/${logBase}.out.log`;\n\t\tconst err = `${logDir(ctx.appName)}/${logBase}.err.log`;\n\t\treturn `\nmkdir -p $(dirname ${pid}) ${logDir(ctx.appName)} &&\ncd ${ctx.currentPath} &&\nnohup env ${env} sh -c '${command.replace(/'/g, `'\\\\''`)}' >> ${out} 2>> ${err} &\necho $! > ${pid}\n`.trim();\n\t};\n\n\tconst stopCmd = (ctx: ProcessManagerContext): string => `\nPID=$(cat ${pidPath(ctx.appName)} 2>/dev/null || true);\nif [ -n \"$PID\" ] && kill -0 \"$PID\" 2>/dev/null; then\n kill \"$PID\" 2>/dev/null || true;\n for i in 1 2 3 4 5; do\n if ! kill -0 \"$PID\" 2>/dev/null; then break; fi;\n sleep 1;\n done;\n kill -9 \"$PID\" 2>/dev/null || true;\nfi\nrm -f ${pidPath(ctx.appName)}\n`.trim();\n\n\treturn {\n\t\treload: async (target, ctx) => {\n\t\t\tconst stop = await target.exec(stopCmd(ctx), { onLog: ctx.onLog, timeoutMs: 30_000 });\n\t\t\tif (stop.exitCode !== 0) {\n\t\t\t\tthrow new Error(`bareManager.stop failed (exit ${stop.exitCode}): ${stop.stderr}`);\n\t\t\t}\n\t\t\tconst start = await target.exec(startCmd(ctx), { onLog: ctx.onLog, timeoutMs: 30_000 });\n\t\t\tif (start.exitCode !== 0) {\n\t\t\t\tthrow new Error(`bareManager.start failed (exit ${start.exitCode}): ${start.stderr}`);\n\t\t\t}\n\t\t},\n\t\tstatus: async (target, ctx) => {\n\t\t\tconst result = await target.exec(\n\t\t\t\t`PID=$(cat ${pidPath(ctx.appName)} 2>/dev/null || true); if [ -n \"$PID\" ] && kill -0 \"$PID\" 2>/dev/null; then echo running; else echo stopped; fi`,\n\t\t\t\t{ timeoutMs: 5_000 },\n\t\t\t);\n\t\t\tconst out = result.stdout.trim();\n\t\t\tif (out === 'running') return 'running';\n\t\t\tif (out === 'stopped') return 'stopped';\n\t\t\treturn 'unknown';\n\t\t},\n\t\tstop: async (target, ctx) => {\n\t\t\tconst result = await target.exec(stopCmd(ctx), { onLog: ctx.onLog, timeoutMs: 30_000 });\n\t\t\tif (result.exitCode !== 0) {\n\t\t\t\tthrow new Error(`bareManager.stop failed (exit ${result.exitCode}): ${result.stderr}`);\n\t\t\t}\n\t\t},\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// systemdManager — generates a unit file pointing at current/, restarts via systemctl\n// -----------------------------------------------------------------------------\n\nexport type SystemdManagerOptions = {\n\t/** Unit file name (defaults to `${appName}.service`). */\n\tunitName?: string;\n\t/** ExecStart command. Default `/usr/local/bin/bun run start`. */\n\texecStart?: string;\n\t/** User to run as. Default the deploy user. */\n\tuser?: string;\n\t/** Group. Default the deploy user. */\n\tgroup?: string;\n\t/** Restart policy. Default `always`. */\n\trestart?: 'always' | 'on-failure' | 'no';\n\t/** systemctl path. Default `systemctl`. */\n\tsystemctl?: string;\n\t/** Unit file directory. Default `/etc/systemd/system`. */\n\tunitDir?: string;\n};\n\nconst renderSystemdUnit = (\n\tctx: ProcessManagerContext,\n\toptions: SystemdManagerOptions,\n): string => {\n\tconst user = options.user ?? 'deploy';\n\tconst group = options.group ?? user;\n\tconst execStart = options.execStart ?? '/usr/local/bin/bun run start';\n\tconst restart = options.restart ?? 'always';\n\tconst envLines = Object.entries(ctx.env)\n\t\t.map(([k, v]) => `Environment=${k}=${v.replace(/\"/g, '\\\\\"')}`)\n\t\t.join('\\n');\n\treturn `[Unit]\nDescription=${ctx.appName} (managed by @absolutejs/deploy)\nAfter=network.target\n\n[Service]\nType=simple\nWorkingDirectory=${ctx.currentPath}\nExecStart=${execStart}\nRestart=${restart}\nRestartSec=2\nUser=${user}\nGroup=${group}\n${envLines}\nStandardOutput=append:/var/log/${ctx.appName}/app.out.log\nStandardError=append:/var/log/${ctx.appName}/app.err.log\n\n[Install]\nWantedBy=multi-user.target\n`;\n};\n\nexport const systemdManager = (options: SystemdManagerOptions = {}): ProcessManager => {\n\tconst systemctl = options.systemctl ?? 'systemctl';\n\tconst unitDir = options.unitDir ?? '/etc/systemd/system';\n\tconst unitName = (ctx: ProcessManagerContext) => options.unitName ?? `${ctx.appName}.service`;\n\n\treturn {\n\t\treload: async (target, ctx) => {\n\t\t\tconst unit = renderSystemdUnit(ctx, options);\n\t\t\tconst name = unitName(ctx);\n\t\t\t// Write the unit via a heredoc executed by the remote shell. tee/cat work but\n\t\t\t// stdin is the cleanest path that doesn't reveal unit text in `ps`.\n\t\t\tconst writeUnit = await target.exec(\n\t\t\t\t`mkdir -p /var/log/${ctx.appName} && cat > ${unitDir}/${name}`,\n\t\t\t\t{ onLog: ctx.onLog, stdin: unit, timeoutMs: 30_000 },\n\t\t\t);\n\t\t\tif (writeUnit.exitCode !== 0) {\n\t\t\t\tthrow new Error(`systemdManager: writing unit failed (exit ${writeUnit.exitCode}): ${writeUnit.stderr}`);\n\t\t\t}\n\t\t\tconst reload = await target.exec(`${systemctl} daemon-reload`, { onLog: ctx.onLog, timeoutMs: 15_000 });\n\t\t\tif (reload.exitCode !== 0) {\n\t\t\t\tthrow new Error(`systemdManager: daemon-reload failed (exit ${reload.exitCode}): ${reload.stderr}`);\n\t\t\t}\n\t\t\tconst enable = await target.exec(`${systemctl} enable ${name}`, { onLog: ctx.onLog, timeoutMs: 15_000 });\n\t\t\tif (enable.exitCode !== 0) {\n\t\t\t\tthrow new Error(`systemdManager: enable failed (exit ${enable.exitCode}): ${enable.stderr}`);\n\t\t\t}\n\t\t\tconst restart = await target.exec(`${systemctl} restart ${name}`, { onLog: ctx.onLog, timeoutMs: 60_000 });\n\t\t\tif (restart.exitCode !== 0) {\n\t\t\t\tthrow new Error(`systemdManager: restart failed (exit ${restart.exitCode}): ${restart.stderr}`);\n\t\t\t}\n\t\t},\n\t\tstatus: async (target, ctx) => {\n\t\t\tconst result = await target.exec(`${systemctl} is-active ${unitName(ctx)} || true`, { timeoutMs: 10_000 });\n\t\t\tconst out = result.stdout.trim();\n\t\t\tif (out === 'active') return 'running';\n\t\t\tif (out === 'inactive' || out === 'failed') return 'stopped';\n\t\t\treturn 'unknown';\n\t\t},\n\t\tstop: async (target, ctx) => {\n\t\t\tconst result = await target.exec(`${systemctl} stop ${unitName(ctx)}`, { onLog: ctx.onLog, timeoutMs: 30_000 });\n\t\t\tif (result.exitCode !== 0) {\n\t\t\t\tthrow new Error(`systemdManager: stop failed (exit ${result.exitCode}): ${result.stderr}`);\n\t\t\t}\n\t\t},\n\t};\n};\n",
7
+ "/**\n * createDeployer — drives a step pipeline against a Target.\n *\n * The default pipeline (`defaultBunPipeline`) is the right thing for a Bun\n * project on a Linux host: prepare → upload → install → build → link →\n * restart → verify. Callers can replace steps wholesale or splice their own\n * in via `steps: [...]`.\n *\n * Release model: every `deploy()` creates a fresh timestamped directory\n * under `<root>/releases/`, uploads into it, then atomically swaps the\n * `<root>/current` symlink. `rollback(id)` re-points the symlink and\n * reloads the process manager — no re-upload, no re-build, just a fast\n * switch.\n */\n\nimport type { ProcessManager, ProcessManagerContext } from './processManagers';\nimport { bareManager } from './processManagers';\nimport type { ExecResult, Target } from './targets';\n\nexport type Source = {\n\t/** Local directory to copy. */\n\tkind: 'directory';\n\troot: string;\n\t/** Globs excluded from upload. Defaults to common dev artifacts. */\n\texclude?: string[];\n};\n\nexport type VerifySpec =\n\t| { kind: 'http'; url: string; retries?: number; intervalMs?: number; expectStatus?: number }\n\t| { kind: 'tcp'; host: string; port: number; retries?: number; intervalMs?: number }\n\t| { kind: 'custom'; check: (ctx: DeployContext) => Promise<boolean> };\n\nexport type DeployContext = {\n\ttarget: Target;\n\tsource: Source;\n\treleaseId: string;\n\treleasePath: string;\n\tcurrentPath: string;\n\tappName: string;\n\tenv: Record<string, string>;\n\thooks: ResolvedHooks;\n\tprocessManager: ProcessManager;\n\tverify: VerifySpec | null;\n};\n\nexport type DeployStep = {\n\tname: string;\n\trun: (ctx: DeployContext) => Promise<void>;\n};\n\nexport type DeployHooks = {\n\tonStepStart?: (step: { name: string; releaseId: string }) => void | Promise<void>;\n\tonStepEnd?: (step: { name: string; releaseId: string; durationMs: number }) => void | Promise<void>;\n\tonLog?: (line: string, stream: 'stdout' | 'stderr', step: string) => void;\n\tonError?: (error: { step: string; releaseId: string; error: Error }) => void | Promise<void>;\n};\n\ntype ResolvedHooks = Required<{\n\t[K in keyof DeployHooks]: NonNullable<DeployHooks[K]>;\n}>;\n\nexport type DeployerOptions = {\n\ttarget: Target;\n\tsource: Source;\n\t/** App name; used by ProcessManagers for unit names, pid files, log paths. Required. */\n\tappName: string;\n\t/** Where deploys live on the target. Default `/srv/<appName>`. */\n\trootPath?: string;\n\t/** Steps in order. Default: `defaultBunPipeline()`. */\n\tsteps?: DeployStep[];\n\t/** Env merged into install / build / start. */\n\tenv?: Record<string, string>;\n\t/** Process manager. Default `bareManager()`. */\n\tprocessManager?: ProcessManager;\n\t/** How to verify the deploy is up. Default null (skip verify). */\n\tverify?: VerifySpec | null;\n\thooks?: DeployHooks;\n\t/** Override `Date.now` for deterministic release ids in tests. */\n\tclock?: () => number;\n};\n\nexport type DeployResult = {\n\treleaseId: string;\n\treleasePath: string;\n\tcurrentPath: string;\n\tdurationMs: number;\n\tsteps: { name: string; durationMs: number }[];\n};\n\nexport type Deployer = {\n\tdeploy: () => Promise<DeployResult>;\n\trollback: (releaseId: string) => Promise<DeployResult>;\n\tlistReleases: () => Promise<string[]>;\n\tprune: (options: { keep: number }) => Promise<{ removed: string[] }>;\n\tdispose: () => Promise<void>;\n};\n\nconst DEFAULT_EXCLUDES = ['node_modules', 'dist', 'build', '.git', '.DS_Store', '*.log'];\n\nconst noopHooks: ResolvedHooks = {\n\tonError: () => {},\n\tonLog: () => {},\n\tonStepEnd: () => {},\n\tonStepStart: () => {},\n};\n\nconst resolveHooks = (hooks?: DeployHooks): ResolvedHooks => ({\n\tonError: hooks?.onError ?? noopHooks.onError,\n\tonLog: hooks?.onLog ?? noopHooks.onLog,\n\tonStepEnd: hooks?.onStepEnd ?? noopHooks.onStepEnd,\n\tonStepStart: hooks?.onStepStart ?? noopHooks.onStepStart,\n});\n\nconst makeReleaseId = (clock: () => number): string => {\n\tconst t = clock();\n\tconst date = new Date(t);\n\tconst pad = (n: number, w = 2) => n.toString().padStart(w, '0');\n\treturn `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;\n};\n\nconst requireSuccess = (label: string, result: ExecResult) => {\n\tif (result.exitCode !== 0) {\n\t\tthrow new Error(`${label} failed (exit ${result.exitCode}): ${result.stderr || result.stdout || '(no output)'}`);\n\t}\n};\n\n// -----------------------------------------------------------------------------\n// Default Bun pipeline steps\n// -----------------------------------------------------------------------------\n\nexport const defaultBunPipeline = (): DeployStep[] => [\n\t{\n\t\tname: 'prepare',\n\t\trun: async (ctx) => {\n\t\t\tconst result = await ctx.target.exec(\n\t\t\t\t`mkdir -p ${ctx.releasePath}`,\n\t\t\t\t{ onLog: (line, stream) => ctx.hooks.onLog(line, stream, 'prepare') },\n\t\t\t);\n\t\t\trequireSuccess('prepare: mkdir', result);\n\t\t},\n\t},\n\t{\n\t\tname: 'upload',\n\t\trun: async (ctx) => {\n\t\t\tif (ctx.source.kind !== 'directory') {\n\t\t\t\tthrow new Error(`Unsupported source kind: ${(ctx.source as { kind: string }).kind}`);\n\t\t\t}\n\t\t\t// Trailing slash on source = copy contents, not the dir itself.\n\t\t\tconst localPath = ctx.source.root.endsWith('/') ? ctx.source.root : `${ctx.source.root}/`;\n\t\t\tawait ctx.target.upload(localPath, ctx.releasePath, {\n\t\t\t\texclude: ctx.source.exclude ?? DEFAULT_EXCLUDES,\n\t\t\t});\n\t\t},\n\t},\n\t{\n\t\tname: 'install',\n\t\trun: async (ctx) => {\n\t\t\tconst result = await ctx.target.exec(\n\t\t\t\t`bun install --production`,\n\t\t\t\t{\n\t\t\t\t\tcwd: ctx.releasePath,\n\t\t\t\t\tenv: ctx.env,\n\t\t\t\t\tonLog: (line, stream) => ctx.hooks.onLog(line, stream, 'install'),\n\t\t\t\t\ttimeoutMs: 600_000,\n\t\t\t\t},\n\t\t\t);\n\t\t\trequireSuccess('install', result);\n\t\t},\n\t},\n\t{\n\t\tname: 'build',\n\t\trun: async (ctx) => {\n\t\t\t// Run only if the project declares a `build` script. Detect by reading package.json on the remote.\n\t\t\tconst probe = await ctx.target.exec(\n\t\t\t\t`grep -E '\"build\"\\\\s*:' package.json || true`,\n\t\t\t\t{ cwd: ctx.releasePath, timeoutMs: 10_000 },\n\t\t\t);\n\t\t\tif (!probe.stdout.includes('\"build\"')) return;\n\t\t\tconst result = await ctx.target.exec(\n\t\t\t\t`bun run build`,\n\t\t\t\t{\n\t\t\t\t\tcwd: ctx.releasePath,\n\t\t\t\t\tenv: ctx.env,\n\t\t\t\t\tonLog: (line, stream) => ctx.hooks.onLog(line, stream, 'build'),\n\t\t\t\t\ttimeoutMs: 600_000,\n\t\t\t\t},\n\t\t\t);\n\t\t\trequireSuccess('build', result);\n\t\t},\n\t},\n\t{\n\t\tname: 'link',\n\t\trun: async (ctx) => {\n\t\t\t// Atomic-ish symlink swap: write a NEW symlink to a temp name, then rename onto current.\n\t\t\tconst tmpLink = `${ctx.currentPath}.next`;\n\t\t\tconst result = await ctx.target.exec(\n\t\t\t\t`ln -sfn ${ctx.releasePath} ${tmpLink} && mv -Tf ${tmpLink} ${ctx.currentPath}`,\n\t\t\t\t{ onLog: (line, stream) => ctx.hooks.onLog(line, stream, 'link'), timeoutMs: 10_000 },\n\t\t\t);\n\t\t\trequireSuccess('link', result);\n\t\t},\n\t},\n\t{\n\t\tname: 'restart',\n\t\trun: async (ctx) => {\n\t\t\tawait ctx.processManager.reload(ctx.target, {\n\t\t\t\tappName: ctx.appName,\n\t\t\t\tcurrentPath: ctx.currentPath,\n\t\t\t\tenv: ctx.env,\n\t\t\t\tonLog: (line, stream) => ctx.hooks.onLog(line, stream, 'restart'),\n\t\t\t\treleaseId: ctx.releaseId,\n\t\t\t\treleasePath: ctx.releasePath,\n\t\t\t});\n\t\t},\n\t},\n\t{\n\t\tname: 'verify',\n\t\trun: async (ctx) => {\n\t\t\tif (!ctx.verify) return;\n\t\t\tawait runVerify(ctx);\n\t\t},\n\t},\n];\n\nconst runVerify = async (ctx: DeployContext): Promise<void> => {\n\tconst spec = ctx.verify!;\n\tif (spec.kind === 'custom') {\n\t\tconst ok = await spec.check(ctx);\n\t\tif (!ok) throw new Error('verify: custom check returned false');\n\t\treturn;\n\t}\n\tif (spec.kind === 'http') {\n\t\tconst retries = spec.retries ?? 30;\n\t\tconst intervalMs = spec.intervalMs ?? 1_000;\n\t\tconst expectStatus = spec.expectStatus ?? 200;\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\tconst probe = await ctx.target.exec(\n\t\t\t\t`curl -s -o /dev/null -w '%{http_code}' --max-time 5 ${spec.url}`,\n\t\t\t\t{ timeoutMs: 10_000 },\n\t\t\t);\n\t\t\tconst code = Number(probe.stdout.trim());\n\t\t\tif (code === expectStatus) return;\n\t\t\tif (attempt < retries) await new Promise((resolve) => setTimeout(resolve, intervalMs));\n\t\t}\n\t\tthrow new Error(`verify: HTTP ${spec.url} did not return ${expectStatus} after ${retries} retries`);\n\t}\n\t// tcp\n\tconst retries = spec.retries ?? 30;\n\tconst intervalMs = spec.intervalMs ?? 1_000;\n\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\tconst probe = await ctx.target.exec(\n\t\t\t`bash -c 'cat < /dev/tcp/${spec.host}/${spec.port}' 2>/dev/null && echo open || echo closed`,\n\t\t\t{ timeoutMs: 10_000 },\n\t\t);\n\t\tif (probe.stdout.includes('open')) return;\n\t\tif (attempt < retries) await new Promise((resolve) => setTimeout(resolve, intervalMs));\n\t}\n\tthrow new Error(`verify: TCP ${spec.host}:${spec.port} not open after ${retries} retries`);\n};\n\n// -----------------------------------------------------------------------------\n// Deployer\n// -----------------------------------------------------------------------------\n\nexport const createDeployer = (options: DeployerOptions): Deployer => {\n\tconst clock = options.clock ?? Date.now;\n\tconst hooks = resolveHooks(options.hooks);\n\tconst rootPath = options.rootPath ?? `/srv/${options.appName}`;\n\tconst currentPath = `${rootPath}/current`;\n\tconst releasesPath = `${rootPath}/releases`;\n\tconst env: Record<string, string> = { NODE_ENV: 'production', ...options.env };\n\tconst processManager = options.processManager ?? bareManager();\n\tconst verify = options.verify === undefined ? null : options.verify;\n\tlet disposed = false;\n\n\tconst buildCtx = (releaseId: string): DeployContext => ({\n\t\tappName: options.appName,\n\t\tcurrentPath,\n\t\tenv,\n\t\thooks,\n\t\tprocessManager,\n\t\treleaseId,\n\t\treleasePath: `${releasesPath}/${releaseId}`,\n\t\tsource: options.source,\n\t\ttarget: options.target,\n\t\tverify,\n\t});\n\n\tconst runSteps = async (steps: DeployStep[], releaseId: string): Promise<DeployResult> => {\n\t\tconst ctx = buildCtx(releaseId);\n\t\tconst stepDurations: { name: string; durationMs: number }[] = [];\n\t\tconst startedAt = clock();\n\t\tfor (const step of steps) {\n\t\t\tconst stepStartedAt = clock();\n\t\t\tawait hooks.onStepStart({ name: step.name, releaseId });\n\t\t\ttry {\n\t\t\t\tawait step.run(ctx);\n\t\t\t} catch (error) {\n\t\t\t\tconst err = error instanceof Error ? error : new Error(String(error));\n\t\t\t\tawait hooks.onError({ error: err, releaseId, step: step.name });\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t\tconst durationMs = clock() - stepStartedAt;\n\t\t\tstepDurations.push({ durationMs, name: step.name });\n\t\t\tawait hooks.onStepEnd({ durationMs, name: step.name, releaseId });\n\t\t}\n\t\treturn {\n\t\t\tcurrentPath,\n\t\t\tdurationMs: clock() - startedAt,\n\t\t\treleaseId,\n\t\t\treleasePath: ctx.releasePath,\n\t\t\tsteps: stepDurations,\n\t\t};\n\t};\n\n\tconst ensureRoot = async () => {\n\t\tconst result = await options.target.exec(`mkdir -p ${releasesPath}`, { timeoutMs: 10_000 });\n\t\trequireSuccess('ensureRoot', result);\n\t};\n\n\treturn {\n\t\tdeploy: async () => {\n\t\t\tif (disposed) throw new Error('Deployer is disposed');\n\t\t\tawait ensureRoot();\n\t\t\tconst releaseId = makeReleaseId(clock);\n\t\t\treturn runSteps(options.steps ?? defaultBunPipeline(), releaseId);\n\t\t},\n\t\tdispose: async () => {\n\t\t\tdisposed = true;\n\t\t\tif (options.target.close) await options.target.close();\n\t\t},\n\t\tlistReleases: async () => {\n\t\t\tawait ensureRoot();\n\t\t\tconst result = await options.target.exec(\n\t\t\t\t`ls -1 ${releasesPath} 2>/dev/null || true`,\n\t\t\t\t{ timeoutMs: 10_000 },\n\t\t\t);\n\t\t\treturn result.stdout\n\t\t\t\t.split('\\n')\n\t\t\t\t.map((line) => line.trim())\n\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t.sort();\n\t\t},\n\t\tprune: async ({ keep }) => {\n\t\t\tif (disposed) throw new Error('Deployer is disposed');\n\t\t\tconst all = await (async () => {\n\t\t\t\tconst result = await options.target.exec(\n\t\t\t\t\t`ls -1 ${releasesPath} 2>/dev/null || true`,\n\t\t\t\t\t{ timeoutMs: 10_000 },\n\t\t\t\t);\n\t\t\t\treturn result.stdout\n\t\t\t\t\t.split('\\n')\n\t\t\t\t\t.map((line) => line.trim())\n\t\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t\t.sort();\n\t\t\t})();\n\t\t\tif (all.length <= keep) return { removed: [] };\n\t\t\tconst removed = all.slice(0, all.length - keep);\n\t\t\tfor (const releaseId of removed) {\n\t\t\t\tawait options.target.exec(`rm -rf ${releasesPath}/${releaseId}`, { timeoutMs: 60_000 });\n\t\t\t}\n\t\t\treturn { removed };\n\t\t},\n\t\trollback: async (releaseId) => {\n\t\t\tif (disposed) throw new Error('Deployer is disposed');\n\t\t\tawait ensureRoot();\n\t\t\tconst exists = await options.target.exec(\n\t\t\t\t`test -d ${releasesPath}/${releaseId} && echo ok || echo missing`,\n\t\t\t\t{ timeoutMs: 5_000 },\n\t\t\t);\n\t\t\tif (!exists.stdout.includes('ok')) {\n\t\t\t\tthrow new Error(`rollback: release ${releaseId} not found at ${releasesPath}/${releaseId}`);\n\t\t\t}\n\t\t\t// Rollback steps: re-link + restart (no upload, no install, no build).\n\t\t\tconst rollbackSteps: DeployStep[] = defaultBunPipeline().filter((step) =>\n\t\t\t\tstep.name === 'link' || step.name === 'restart' || step.name === 'verify',\n\t\t\t);\n\t\t\treturn runSteps(rollbackSteps, releaseId);\n\t\t},\n\t};\n};\n"
8
+ ],
9
+ "mappings": "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",
10
+ "debugId": "044FAFA4F653318F64756E2164756E21",
11
+ "names": []
12
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ProcessManager — the abstraction that turns "files are on the target" into
3
+ * "the app is running." Two strategies ship: `bareManager` (nohup, lowest
4
+ * dependency) and `systemdManager` (templated unit file, the way production
5
+ * VMs should run).
6
+ *
7
+ * Callers can supply their own — anything that implements `start` / `stop` /
8
+ * `reload` / `status` against a `Target` works. PM2, supervisord, runit,
9
+ * @absolutejs/runtime all fit if someone writes the adapter.
10
+ */
11
+ import type { Target } from './targets';
12
+ export type ProcessManagerContext = {
13
+ /** Absolute path on the target to the active release dir (the symlink target). */
14
+ currentPath: string;
15
+ /** Absolute path on the target to the new release dir we just uploaded. */
16
+ releasePath: string;
17
+ /** Release id (timestamped). */
18
+ releaseId: string;
19
+ /** App name — supplied via deployer config; used for unit names, pid files, etc. */
20
+ appName: string;
21
+ /** Optional env to set on the process. */
22
+ env: Record<string, string>;
23
+ /** Log sink for any commands the manager runs. */
24
+ onLog?: (line: string, stream: 'stdout' | 'stderr') => void;
25
+ };
26
+ export type ProcessManager = {
27
+ /** Bring the new release up. Called after the `current` symlink has been swapped. */
28
+ reload: (target: Target, ctx: ProcessManagerContext) => Promise<void>;
29
+ /** Stop the running process. */
30
+ stop?: (target: Target, ctx: ProcessManagerContext) => Promise<void>;
31
+ /** Return current status (best-effort; used by callers for diagnostics). */
32
+ status?: (target: Target, ctx: ProcessManagerContext) => Promise<'running' | 'stopped' | 'unknown'>;
33
+ };
34
+ export type BareManagerOptions = {
35
+ /** Command to run. Default `bun run start`. */
36
+ command?: string;
37
+ /** Log files inside the app's data dir (default: alongside pid). */
38
+ logFileBaseName?: string;
39
+ };
40
+ export declare const bareManager: (options?: BareManagerOptions) => ProcessManager;
41
+ export type SystemdManagerOptions = {
42
+ /** Unit file name (defaults to `${appName}.service`). */
43
+ unitName?: string;
44
+ /** ExecStart command. Default `/usr/local/bin/bun run start`. */
45
+ execStart?: string;
46
+ /** User to run as. Default the deploy user. */
47
+ user?: string;
48
+ /** Group. Default the deploy user. */
49
+ group?: string;
50
+ /** Restart policy. Default `always`. */
51
+ restart?: 'always' | 'on-failure' | 'no';
52
+ /** systemctl path. Default `systemctl`. */
53
+ systemctl?: string;
54
+ /** Unit file directory. Default `/etc/systemd/system`. */
55
+ unitDir?: string;
56
+ };
57
+ export declare const systemdManager: (options?: SystemdManagerOptions) => ProcessManager;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Target interface + bundled adapters (localTarget, sshTarget).
3
+ *
4
+ * A Target is the narrowest abstraction over "a place I can deploy to":
5
+ *
6
+ * - `exec(cmd, opts?)` — run a shell command, capture stdout/stderr/exitCode.
7
+ * - `upload(localPath, remotePath, opts?)` — copy a local file or directory
8
+ * to the target. Implementation is free to use whatever is fast (rsync,
9
+ * scp, mv).
10
+ * - `close?()` — optional teardown.
11
+ *
12
+ * Two adapters are bundled:
13
+ *
14
+ * - `localTarget` runs in a temp directory on the local filesystem. Useful
15
+ * for tests and for "deploy" workflows that happen on the same host.
16
+ * - `sshTarget` shells out to the system `ssh` and `rsync` binaries. No
17
+ * `ssh2` npm dependency — the controller machine just needs `ssh` and
18
+ * (optionally) `rsync` in PATH, which is universal on Mac/Linux/WSL.
19
+ *
20
+ * Provider-specific targets (Cloudflare Workers HTTP API, Fly Machines API,
21
+ * AWS Fargate) don't fit "exec + upload" and ship as siblings later.
22
+ */
23
+ export type ExecOptions = {
24
+ /** Working directory on the target. Default: target's root. */
25
+ cwd?: string;
26
+ /** Env vars to set for this command (merged onto target.env). */
27
+ env?: Record<string, string>;
28
+ /** Hard kill after this many ms. Default 600_000 (10 min). 0 disables. */
29
+ timeoutMs?: number;
30
+ /** Pipe stdout/stderr through here as it streams (lines, newline-stripped). */
31
+ onLog?: (line: string, stream: 'stdout' | 'stderr') => void;
32
+ /** Stdin payload — a string is written verbatim. */
33
+ stdin?: string;
34
+ };
35
+ export type ExecResult = {
36
+ stdout: string;
37
+ stderr: string;
38
+ exitCode: number;
39
+ };
40
+ export type UploadOptions = {
41
+ /** Exclude paths matching these globs from a directory upload. */
42
+ exclude?: string[];
43
+ /** When uploading a directory, delete remote files not present locally. */
44
+ deleteOrphans?: boolean;
45
+ };
46
+ export type Target = {
47
+ /** Human-readable description (e.g. "ssh root@droplet-1.example.com"). */
48
+ readonly description: string;
49
+ exec: (cmd: string, opts?: ExecOptions) => Promise<ExecResult>;
50
+ upload: (localPath: string, remotePath: string, opts?: UploadOptions) => Promise<void>;
51
+ close?: () => Promise<void>;
52
+ };
53
+ export type LocalTargetOptions = {
54
+ /** Root directory the target operates in. Created if missing. */
55
+ root: string;
56
+ /** Env merged into every exec. */
57
+ env?: Record<string, string>;
58
+ };
59
+ export declare const localTarget: (options: LocalTargetOptions) => Target;
60
+ export type SshTargetOptions = {
61
+ /** Hostname or IP of the remote. */
62
+ host: string;
63
+ /** Login user. Default `root`. */
64
+ user?: string;
65
+ /** SSH port. Default 22. */
66
+ port?: number;
67
+ /** Path to SSH identity file. Default: ssh's own search. */
68
+ identity?: string;
69
+ /** Extra flags appended to every `ssh` invocation. */
70
+ sshFlags?: string[];
71
+ /**
72
+ * Use rsync for `upload`. Default true. When false, falls back to `scp`
73
+ * which is universal but doesn't support delete / exclude.
74
+ */
75
+ rsync?: boolean;
76
+ /**
77
+ * Env vars to forward via `ssh -o SendEnv=...`. Most remote sshd configs
78
+ * accept only `LANG` and `LC_*` by default; for app env vars use the
79
+ * step `env` option instead, which prepends `KEY=value` to the command.
80
+ */
81
+ forwardEnv?: string[];
82
+ };
83
+ export declare const sshTarget: (options: SshTargetOptions) => Target;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@absolutejs/deploy",
3
+ "version": "0.0.1",
4
+ "description": "Generic Bun-project deploy pipeline. A Target (localTarget / sshTarget) is anywhere you can exec + upload — DigitalOcean droplets, Linode, Hetzner, Vultr, your own boxes. Bundled pipeline: prepare → upload → install → build → link → restart → verify. Atomic symlink swap, release history, prune, hooks. SSH shells out to system ssh/rsync — zero ssh2 deps.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/absolutejs/deploy.git"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "type": "module",
13
+ "license": "BSL-1.1",
14
+ "author": "Alex Kahn",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "keywords": [
19
+ "absolutejs",
20
+ "bun",
21
+ "deploy",
22
+ "ssh",
23
+ "droplet",
24
+ "vps",
25
+ "rsync",
26
+ "release"
27
+ ],
28
+ "scripts": {
29
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json",
30
+ "test": "bun test tests/",
31
+ "typecheck": "tsc --noEmit",
32
+ "format": "prettier --write \"./**/*.{ts,json,md}\"",
33
+ "check:package": "bun run typecheck && bun run build && bun run test",
34
+ "release": "bun run format && bun run check:package && bun publish"
35
+ },
36
+ "peerDependencies": {
37
+ "bun-types": "^1.3.14"
38
+ },
39
+ "devDependencies": {
40
+ "@types/bun": "^1.3.14",
41
+ "prettier": "^3.8.3",
42
+ "typescript": "^6.0.3"
43
+ },
44
+ "exports": {
45
+ ".": {
46
+ "types": "./dist/index.d.ts",
47
+ "import": "./dist/index.js",
48
+ "default": "./dist/index.js"
49
+ }
50
+ },
51
+ "files": ["dist", "README.md"]
52
+ }