@absolutejs/deploy 0.0.1 → 0.2.0

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/README.md CHANGED
@@ -110,7 +110,51 @@ Default is `null` (no verify). Recommend always wiring one — a green deploy th
110
110
  - `listReleases()` returns the sorted list.
111
111
  - `prune({ keep: N })` removes the N oldest.
112
112
 
113
- ## DigitalOcean Droplet first deploy
113
+ ## `@absolutejs/deploy/digitalocean`provision-or-reuse from code (0.2.0)
114
+
115
+ Skip the click-through DO dashboard. `digitalOceanTarget(options)` looks
116
+ up a droplet by name; creates it via the v2 API if absent; waits for
117
+ `status === 'active'` + IPv4; waits for SSH; returns a `Target` ready to
118
+ hand to `createDeployer`.
119
+
120
+ ```ts
121
+ import { createDeployer } from '@absolutejs/deploy';
122
+ import { digitalOceanTarget } from '@absolutejs/deploy/digitalocean';
123
+
124
+ const target = await digitalOceanTarget({
125
+ token: process.env.DO_TOKEN!,
126
+ name: 'absolutejs-prod-1', // idempotency key
127
+ region: 'nyc3',
128
+ size: 's-1vcpu-1gb',
129
+ image: 'ubuntu-22-04-x64',
130
+ sshKeys: [process.env.DO_KEY_FINGERPRINT!],
131
+ tags: ['absolutejs'],
132
+ userData: '#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash',
133
+ onLog: (line) => console.log(line),
134
+ });
135
+
136
+ console.log(`droplet ${target.dropletId} at ${target.ipv4}`);
137
+
138
+ const deployer = createDeployer({ appName: 'my-app', target });
139
+ await deployer.deploy({ source: { kind: 'directory', path: './build' } });
140
+
141
+ // Tear it down when you're done:
142
+ await target.destroy();
143
+ ```
144
+
145
+ Idempotent by `name` — calling twice returns the same droplet, no
146
+ duplicates created. Pair with `cloud-init` user data to install Bun /
147
+ configure the deploy user / set up firewall rules on first boot, then
148
+ the deploy pipeline runs against an SSH-ready box.
149
+
150
+ Admin helpers: `listDigitalOceanDroplets({ token, tag? })` for
151
+ inventory; `destroyDigitalOceanDroplet({ token, id })` for cleanup
152
+ (404 is treated as idempotent success). Narrow `DigitalOceanClientLike`
153
+ interface so you can BYO `request(method, path, body?)` for retry /
154
+ observability — the bundled `createDigitalOceanClient(token)` is just
155
+ a sensible default.
156
+
157
+ ## DigitalOcean Droplet — first deploy (manual)
114
158
 
115
159
  Assuming a fresh Ubuntu/Debian Droplet:
116
160
 
@@ -129,9 +173,9 @@ bun run my-deploy-script.ts
129
173
 
130
174
  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
175
 
132
- ## What v0.0.1 does NOT include
176
+ ## What v0.2.0 does NOT include
133
177
 
134
- - Provider-specific HTTP-API adapters (Cloudflare Workers, Fly Machines, AWS Fargate, GCP Cloud Run).
178
+ - Provider-specific HTTP-API adapters beyond DigitalOcean (Cloudflare Workers, Fly Machines, AWS Fargate, GCP Cloud Run). Hetzner / Linode / Vultr follow the same shape as `digitalOceanTarget` and are next on the list.
135
179
  - Bun installation on the remote — caller does it once, out of band.
136
180
  - Multi-target / fan-out deploys (caller iterates).
137
181
  - 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.
@@ -37,6 +37,18 @@ export type VerifySpec = {
37
37
  kind: 'custom';
38
38
  check: (ctx: DeployContext) => Promise<boolean>;
39
39
  };
40
+ export type ReleaseAnnotations = {
41
+ /** Git commit SHA being deployed (40-char hex; truncated forms accepted). */
42
+ commitSha?: string;
43
+ /** Git ref (e.g. `refs/heads/main`, `v1.2.3`). */
44
+ ref?: string;
45
+ /** Commit message (or any human-readable description). */
46
+ message?: string;
47
+ /** Committer / deployer identity. */
48
+ author?: string;
49
+ /** Arbitrary tags for downstream filtering (status pages, audits). */
50
+ tags?: Record<string, string>;
51
+ };
40
52
  export type DeployContext = {
41
53
  target: Target;
42
54
  source: Source;
@@ -48,6 +60,9 @@ export type DeployContext = {
48
60
  hooks: ResolvedHooks;
49
61
  processManager: ProcessManager;
50
62
  verify: VerifySpec | null;
63
+ annotations: ReleaseAnnotations;
64
+ /** When `true`, steps log what they WOULD do via hooks.onLog and do not mutate the target. */
65
+ dryRun: boolean;
51
66
  };
52
67
  export type DeployStep = {
53
68
  name: string;
@@ -73,6 +88,25 @@ export type DeployHooks = {
73
88
  type ResolvedHooks = Required<{
74
89
  [K in keyof DeployHooks]: NonNullable<DeployHooks[K]>;
75
90
  }>;
91
+ export type DeployOptions = {
92
+ /** Per-release annotations stored alongside the release dir as `.deploy-meta.json`. */
93
+ annotations?: ReleaseAnnotations;
94
+ /**
95
+ * When `true`, the deploy plan is logged but no mutation happens on the
96
+ * target — steps still call `target.exec` only via `cmd === 'echo'`-style
97
+ * dry-run probes. Use this from `gh actions` to verify pipeline shape
98
+ * before flipping a real `current` symlink.
99
+ */
100
+ dryRun?: boolean;
101
+ /**
102
+ * Resume a previously-failed release. The deployer reads the release's
103
+ * `.deploy-meta.json`, finds the step that died, and starts from there.
104
+ * Steps that completed successfully are skipped. Use this when a deploy
105
+ * fails on `verify` (e.g. health-check timeout) but the release is
106
+ * otherwise intact on disk.
107
+ */
108
+ resumeReleaseId?: string;
109
+ };
76
110
  export type DeployerOptions = {
77
111
  target: Target;
78
112
  source: Source;
@@ -92,6 +126,15 @@ export type DeployerOptions = {
92
126
  /** Override `Date.now` for deterministic release ids in tests. */
93
127
  clock?: () => number;
94
128
  };
129
+ export type ReleaseRecord = {
130
+ releaseId: string;
131
+ annotations: ReleaseAnnotations;
132
+ status: 'in-progress' | 'completed' | 'failed';
133
+ failedStep?: string;
134
+ completedSteps: string[];
135
+ startedAt: number;
136
+ endedAt?: number;
137
+ };
95
138
  export type DeployResult = {
96
139
  releaseId: string;
97
140
  releasePath: string;
@@ -100,12 +143,16 @@ export type DeployResult = {
100
143
  steps: {
101
144
  name: string;
102
145
  durationMs: number;
146
+ skipped?: boolean;
103
147
  }[];
148
+ annotations: ReleaseAnnotations;
104
149
  };
105
150
  export type Deployer = {
106
- deploy: () => Promise<DeployResult>;
151
+ deploy: (options?: DeployOptions) => Promise<DeployResult>;
107
152
  rollback: (releaseId: string) => Promise<DeployResult>;
108
153
  listReleases: () => Promise<string[]>;
154
+ /** Read the deploy meta for a specific release (or null if missing). */
155
+ readReleaseMeta: (releaseId: string) => Promise<ReleaseRecord | null>;
109
156
  prune: (options: {
110
157
  keep: number;
111
158
  }) => Promise<{
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @absolutejs/deploy/digitalocean — provision-or-reuse Target adapter
3
+ * for DigitalOcean droplets.
4
+ *
5
+ * What it does:
6
+ *
7
+ * 1. Looks up a droplet by `name`. If present and active, reuses it.
8
+ * 2. If not present, creates it via the DO v2 API and waits for
9
+ * `status === 'active'` with a public IPv4 assigned.
10
+ * 3. Waits for SSH readiness (TCP connect on port 22 with backoff,
11
+ * or a caller-supplied probe).
12
+ * 4. Returns a Target that wraps sshTarget against the droplet's
13
+ * public IPv4, plus `dropletId`, `ipv4`, and a `destroy()` helper.
14
+ *
15
+ * Idempotent by name — calling twice with the same name returns the
16
+ * same droplet, no duplicates created. If multiple droplets share
17
+ * the name, throws (the caller has drifted state to clean up).
18
+ *
19
+ * Narrow DigitalOceanClientLike interface keeps the dots-on-the-i
20
+ * SDK out as a hard dep. Default client uses `fetch` against
21
+ * `api.digitalocean.com`; pass your own for retry / observability.
22
+ */
23
+ import type { Target } from './targets';
24
+ /**
25
+ * Minimal subset of DO API calls we make. Lets callers BYO a client
26
+ * with retry / observability / etc. (e.g. wrap got, undici, or a
27
+ * tenant-scoped client that injects different tokens per call).
28
+ */
29
+ export type DigitalOceanClientLike = {
30
+ request: <T = unknown>(method: 'GET' | 'POST' | 'DELETE', path: string, body?: unknown) => Promise<T>;
31
+ };
32
+ /** A DigitalOcean droplet record, narrowed to what we inspect. */
33
+ export type DigitalOceanDroplet = {
34
+ id: number;
35
+ name: string;
36
+ status: 'new' | 'active' | 'off' | 'archive';
37
+ region?: {
38
+ slug: string;
39
+ };
40
+ size_slug?: string;
41
+ networks: {
42
+ v4: Array<{
43
+ ip_address: string;
44
+ type: 'public' | 'private';
45
+ }>;
46
+ v6?: Array<{
47
+ ip_address: string;
48
+ type: 'public' | 'private';
49
+ }>;
50
+ };
51
+ tags?: string[];
52
+ };
53
+ export type DigitalOceanTargetOptions = {
54
+ /** API token (https://cloud.digitalocean.com/account/api/tokens). Required unless `client` is set. */
55
+ token?: string;
56
+ /** Custom client. Overrides token-built default. */
57
+ client?: DigitalOceanClientLike;
58
+ /** Droplet name. Also the idempotency key. */
59
+ name: string;
60
+ /** Region slug — `'nyc3'`, `'sfo3'`, `'ams3'`, etc. */
61
+ region: string;
62
+ /** Size slug — `'s-1vcpu-1gb'`, `'s-2vcpu-4gb'`, etc. */
63
+ size: string;
64
+ /** Image slug, snapshot id, or backup id. e.g. `'ubuntu-22-04-x64'`. */
65
+ image: string | number;
66
+ /** SSH key fingerprints OR numeric ids. At least one required to ssh in. */
67
+ sshKeys: ReadonlyArray<string | number>;
68
+ /** Tags applied at creation. Useful for `listDroplets({ tag })`. */
69
+ tags?: ReadonlyArray<string>;
70
+ /** cloud-init user data — a shell script or YAML config. */
71
+ userData?: string;
72
+ /** VPC UUID. Defaults to the account's default VPC for the region. */
73
+ vpcUuid?: string;
74
+ /** Enable IPv6. Default false. */
75
+ ipv6?: boolean;
76
+ /** Enable monitoring agent. Default false. */
77
+ monitoring?: boolean;
78
+ /** SSH login user. Default `'root'`. */
79
+ user?: string;
80
+ /** Path to SSH identity file forwarded to sshTarget. */
81
+ identity?: string;
82
+ /** SSH port. Default 22. */
83
+ port?: number;
84
+ /** Max time to wait for droplet `active` + IPv4. Default 5 min. */
85
+ provisionTimeoutMs?: number;
86
+ /** Max time to wait for SSH probe to succeed. Default 2 min. */
87
+ sshReadinessTimeoutMs?: number;
88
+ /** Poll interval for provision + ssh probe. Default 5 s. */
89
+ pollIntervalMs?: number;
90
+ /** Called with status updates (one line each). Default: noop. */
91
+ onLog?: (line: string) => void;
92
+ /**
93
+ * Override the SSH readiness probe. Default opens a TCP socket to
94
+ * `host:port`. Tests pass a fake probe to skip real network IO.
95
+ */
96
+ probeSsh?: (host: string, port: number) => Promise<boolean>;
97
+ /**
98
+ * Sleep used between polls. Default `setTimeout`-based. Tests can
99
+ * pass a synchronous resolver to skip real waits.
100
+ */
101
+ sleep?: (ms: number) => Promise<void>;
102
+ /** Wall clock. Defaults to `Date.now`. Tests can swap. */
103
+ now?: () => number;
104
+ };
105
+ export type DigitalOceanTarget = Target & {
106
+ readonly dropletId: number;
107
+ readonly ipv4: string;
108
+ /** Destroy the droplet via the DO API. */
109
+ destroy: () => Promise<void>;
110
+ };
111
+ export declare class DigitalOceanError extends Error {
112
+ readonly status: number;
113
+ readonly body: unknown;
114
+ constructor(message: string, status: number, body: unknown);
115
+ }
116
+ /**
117
+ * fetch-backed default client. Talks JSON to `api.digitalocean.com/v2`.
118
+ * Throws DigitalOceanError on non-2xx with the response body attached
119
+ * so the caller can switch on `err.status`.
120
+ */
121
+ export declare const createDigitalOceanClient: (token: string, options?: {
122
+ baseUrl?: string;
123
+ fetch?: typeof fetch;
124
+ }) => DigitalOceanClientLike;
125
+ /**
126
+ * Find a droplet by name. Returns undefined if absent.
127
+ * Throws if more than one droplet shares the name (drifted state).
128
+ */
129
+ export declare const findDigitalOceanDroplet: (client: DigitalOceanClientLike, name: string) => Promise<DigitalOceanDroplet | undefined>;
130
+ /** List droplets, optionally filtered by tag. Useful for cleanup tasks. */
131
+ export declare const listDigitalOceanDroplets: (options: {
132
+ token?: string;
133
+ client?: DigitalOceanClientLike;
134
+ tag?: string;
135
+ }) => Promise<DigitalOceanDroplet[]>;
136
+ /** Destroy a droplet by id. No-op if already gone. */
137
+ export declare const destroyDigitalOceanDroplet: (options: {
138
+ token?: string;
139
+ client?: DigitalOceanClientLike;
140
+ id: number;
141
+ }) => Promise<void>;
142
+ /**
143
+ * Provision-or-reuse a DO droplet by name, wait for SSH, return a
144
+ * Target. Idempotent: same name → same droplet.
145
+ */
146
+ export declare const digitalOceanTarget: (options: DigitalOceanTargetOptions) => Promise<DigitalOceanTarget>;
@@ -0,0 +1,369 @@
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 sink = proc.stdin;
52
+ const wrote = sink.write(options.stdin);
53
+ if (wrote && typeof wrote.then === "function") {
54
+ await wrote;
55
+ }
56
+ const ended = sink.end();
57
+ if (ended && typeof ended.then === "function") {
58
+ await ended;
59
+ }
60
+ }
61
+ const timeout = options.timeoutMs ?? 600000;
62
+ let timer;
63
+ if (timeout > 0) {
64
+ timer = setTimeout(() => {
65
+ try {
66
+ proc.kill();
67
+ } catch {}
68
+ }, timeout);
69
+ }
70
+ const stdoutPromise = decodeChunks(proc.stdout, options.onLog ? (line) => options.onLog(line, "stdout") : undefined);
71
+ const stderrPromise = decodeChunks(proc.stderr, options.onLog ? (line) => options.onLog(line, "stderr") : undefined);
72
+ const [stdout, stderr, exitCode] = await Promise.all([
73
+ stdoutPromise,
74
+ stderrPromise,
75
+ proc.exited
76
+ ]);
77
+ if (timer)
78
+ clearTimeout(timer);
79
+ return { exitCode: exitCode ?? -1, stderr, stdout };
80
+ };
81
+ var localTarget = (options) => {
82
+ const baseEnv = { ...options.env };
83
+ const ensureRoot = async () => {
84
+ await mkdir(options.root, { recursive: true });
85
+ };
86
+ return {
87
+ description: `local ${options.root}`,
88
+ exec: async (cmd, opts) => {
89
+ await ensureRoot();
90
+ return runSpawn(["sh", "-c", cmd], {
91
+ cwd: opts?.cwd ?? options.root,
92
+ env: { ...process.env, ...baseEnv, ...opts?.env ?? {} },
93
+ onLog: opts?.onLog,
94
+ stdin: opts?.stdin,
95
+ timeoutMs: opts?.timeoutMs
96
+ });
97
+ },
98
+ upload: async (localPath, remotePath, opts) => {
99
+ await ensureRoot();
100
+ const dest = remotePath.startsWith("/") ? remotePath : join(options.root, remotePath);
101
+ const argv = ["rsync", "-a"];
102
+ if (opts?.deleteOrphans)
103
+ argv.push("--delete");
104
+ for (const pattern of opts?.exclude ?? [])
105
+ argv.push("--exclude", pattern);
106
+ argv.push(localPath, dest);
107
+ const result = await runSpawn(argv, { timeoutMs: 600000 });
108
+ if (result.exitCode !== 0) {
109
+ throw new Error(`local upload failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
110
+ }
111
+ }
112
+ };
113
+ };
114
+ var sshTargetString = (options) => {
115
+ const user = options.user ?? "root";
116
+ return `${user}@${options.host}`;
117
+ };
118
+ var sshBaseFlags = (options) => {
119
+ const flags = [];
120
+ if (options.port !== undefined && options.port !== 22)
121
+ flags.push("-p", String(options.port));
122
+ if (options.identity !== undefined)
123
+ flags.push("-i", options.identity);
124
+ flags.push("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new");
125
+ for (const flag of options.sshFlags ?? [])
126
+ flags.push(flag);
127
+ return flags;
128
+ };
129
+ var shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
130
+ var buildRemoteCmd = (cmd, opts) => {
131
+ const env = opts?.env;
132
+ const envPrefix = env ? Object.entries(env).map(([k, v]) => `${k}=${shellQuote(v)}`).join(" ") + " " : "";
133
+ if (opts?.cwd) {
134
+ return `cd ${shellQuote(opts.cwd)} && ${envPrefix}${cmd}`;
135
+ }
136
+ return `${envPrefix}${cmd}`;
137
+ };
138
+ var sshTarget = (options) => {
139
+ const remote = sshTargetString(options);
140
+ const useRsync = options.rsync ?? true;
141
+ return {
142
+ description: `ssh ${remote}${options.port && options.port !== 22 ? `:${options.port}` : ""}`,
143
+ exec: async (cmd, opts) => {
144
+ const argv = ["ssh", ...sshBaseFlags(options)];
145
+ for (const name of options.forwardEnv ?? [])
146
+ argv.push("-o", `SendEnv=${name}`);
147
+ argv.push(remote, buildRemoteCmd(cmd, opts));
148
+ return runSpawn(argv, {
149
+ onLog: opts?.onLog,
150
+ stdin: opts?.stdin,
151
+ timeoutMs: opts?.timeoutMs
152
+ });
153
+ },
154
+ upload: async (localPath, remotePath, opts) => {
155
+ if (useRsync) {
156
+ const sshCmd = ["ssh", ...sshBaseFlags(options)].map((part) => part.includes(" ") ? `'${part}'` : part).join(" ");
157
+ const argv2 = ["rsync", "-az", "-e", sshCmd];
158
+ if (opts?.deleteOrphans)
159
+ argv2.push("--delete");
160
+ for (const pattern of opts?.exclude ?? [])
161
+ argv2.push("--exclude", pattern);
162
+ argv2.push(localPath, `${remote}:${remotePath}`);
163
+ const result2 = await runSpawn(argv2, { timeoutMs: 600000 });
164
+ if (result2.exitCode !== 0) {
165
+ throw new Error(`rsync upload failed (exit ${result2.exitCode}): ${result2.stderr || result2.stdout}`);
166
+ }
167
+ return;
168
+ }
169
+ const argv = ["scp", "-r", ...sshBaseFlags(options), localPath, `${remote}:${remotePath}`];
170
+ const result = await runSpawn(argv, { timeoutMs: 600000 });
171
+ if (result.exitCode !== 0) {
172
+ throw new Error(`scp upload failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
173
+ }
174
+ }
175
+ };
176
+ };
177
+
178
+ // src/digitalocean.ts
179
+ var DO_API_BASE = "https://api.digitalocean.com/v2";
180
+
181
+ class DigitalOceanError extends Error {
182
+ status;
183
+ body;
184
+ constructor(message, status, body) {
185
+ super(message);
186
+ this.name = "DigitalOceanError";
187
+ this.status = status;
188
+ this.body = body;
189
+ }
190
+ }
191
+ var createDigitalOceanClient = (token, options = {}) => {
192
+ const base = options.baseUrl ?? DO_API_BASE;
193
+ const f = options.fetch ?? fetch;
194
+ return {
195
+ request: async (method, path, body) => {
196
+ const init = {
197
+ headers: {
198
+ authorization: `Bearer ${token}`,
199
+ "content-type": "application/json"
200
+ },
201
+ method
202
+ };
203
+ if (body !== undefined)
204
+ init.body = JSON.stringify(body);
205
+ const response = await f(`${base}${path}`, init);
206
+ if (response.status === 204)
207
+ return;
208
+ const text = await response.text();
209
+ const parsed = text.length > 0 ? JSON.parse(text) : undefined;
210
+ if (!response.ok) {
211
+ throw new DigitalOceanError(`DigitalOcean API ${method} ${path} failed: ${response.status} ${response.statusText}`, response.status, parsed);
212
+ }
213
+ return parsed;
214
+ }
215
+ };
216
+ };
217
+ var resolveClient = (options) => {
218
+ if (options.client !== undefined)
219
+ return options.client;
220
+ if (options.token !== undefined && options.token.length > 0) {
221
+ return createDigitalOceanClient(options.token);
222
+ }
223
+ throw new Error("[deploy/digitalocean] either `token` or `client` must be provided");
224
+ };
225
+ var publicIpv4 = (droplet) => droplet.networks.v4.find((net) => net.type === "public")?.ip_address;
226
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
227
+ var defaultProbeSsh = async (host, port) => {
228
+ const PROBE_TIMEOUT_MS = 2000;
229
+ return new Promise((resolve) => {
230
+ let settled = false;
231
+ const settle = (value) => {
232
+ if (settled)
233
+ return;
234
+ settled = true;
235
+ resolve(value);
236
+ };
237
+ const timer = setTimeout(() => settle(false), PROBE_TIMEOUT_MS);
238
+ Bun.connect({
239
+ hostname: host,
240
+ port,
241
+ socket: {
242
+ data: () => {},
243
+ error: () => {
244
+ clearTimeout(timer);
245
+ settle(false);
246
+ },
247
+ open: (socket) => {
248
+ clearTimeout(timer);
249
+ socket.end();
250
+ settle(true);
251
+ }
252
+ }
253
+ }).catch(() => {
254
+ clearTimeout(timer);
255
+ settle(false);
256
+ });
257
+ });
258
+ };
259
+ var findDigitalOceanDroplet = async (client, name) => {
260
+ const body = await client.request("GET", `/droplets?name=${encodeURIComponent(name)}`);
261
+ const matches = body.droplets.filter((droplet) => droplet.name === name);
262
+ if (matches.length === 0)
263
+ return;
264
+ if (matches.length > 1) {
265
+ throw new Error(`[deploy/digitalocean] multiple droplets named "${name}" (${matches.map((droplet) => droplet.id).join(", ")}). Resolve manually before adopting.`);
266
+ }
267
+ return matches[0];
268
+ };
269
+ var listDigitalOceanDroplets = async (options) => {
270
+ const client = resolveClient(options);
271
+ const path = options.tag !== undefined ? `/droplets?tag_name=${encodeURIComponent(options.tag)}` : "/droplets";
272
+ const body = await client.request("GET", path);
273
+ return body.droplets;
274
+ };
275
+ var destroyDigitalOceanDroplet = async (options) => {
276
+ const client = resolveClient(options);
277
+ try {
278
+ await client.request("DELETE", `/droplets/${options.id}`);
279
+ } catch (error) {
280
+ if (error instanceof DigitalOceanError && error.status === 404) {
281
+ return;
282
+ }
283
+ throw error;
284
+ }
285
+ };
286
+ var digitalOceanTarget = async (options) => {
287
+ const client = resolveClient(options);
288
+ const log = options.onLog ?? (() => {});
289
+ const probeSsh = options.probeSsh ?? defaultProbeSsh;
290
+ const sleep = options.sleep ?? defaultSleep;
291
+ const now = options.now ?? Date.now;
292
+ const pollMs = options.pollIntervalMs ?? 5000;
293
+ const provisionTimeout = options.provisionTimeoutMs ?? 5 * 60000;
294
+ const sshTimeout = options.sshReadinessTimeoutMs ?? 2 * 60000;
295
+ const port = options.port ?? 22;
296
+ const existing = await findDigitalOceanDroplet(client, options.name);
297
+ let current;
298
+ if (existing === undefined) {
299
+ log(`[do] creating droplet "${options.name}" in ${options.region}`);
300
+ const created = await client.request("POST", "/droplets", {
301
+ name: options.name,
302
+ region: options.region,
303
+ size: options.size,
304
+ image: options.image,
305
+ ssh_keys: [...options.sshKeys],
306
+ ...options.tags !== undefined ? { tags: [...options.tags] } : {},
307
+ ...options.userData !== undefined ? { user_data: options.userData } : {},
308
+ ...options.vpcUuid !== undefined ? { vpc_uuid: options.vpcUuid } : {},
309
+ ...options.ipv6 === true ? { ipv6: true } : {},
310
+ ...options.monitoring === true ? { monitoring: true } : {}
311
+ });
312
+ current = created.droplet;
313
+ } else {
314
+ log(`[do] reusing droplet "${options.name}" (id ${existing.id}, status ${existing.status})`);
315
+ current = existing;
316
+ }
317
+ const provisionStart = now();
318
+ let ipv4 = publicIpv4(current);
319
+ while (current.status !== "active" || ipv4 === undefined) {
320
+ if (now() - provisionStart > provisionTimeout) {
321
+ throw new Error(`[deploy/digitalocean] provision timeout after ${provisionTimeout}ms \u2014 droplet ${current.id} status "${current.status}", ipv4 ${ipv4 ?? "(unassigned)"}`);
322
+ }
323
+ await sleep(pollMs);
324
+ const refreshed = await client.request("GET", `/droplets/${current.id}`);
325
+ current = refreshed.droplet;
326
+ ipv4 = publicIpv4(current);
327
+ log(`[do] poll: status=${current.status} ipv4=${ipv4 ?? "(none yet)"}`);
328
+ }
329
+ log(`[do] droplet active at ${ipv4}`);
330
+ const sshStart = now();
331
+ while (!await probeSsh(ipv4, port)) {
332
+ if (now() - sshStart > sshTimeout) {
333
+ throw new Error(`[deploy/digitalocean] SSH readiness timeout after ${sshTimeout}ms \u2014 ${ipv4}:${port} did not accept connections`);
334
+ }
335
+ await sleep(pollMs);
336
+ log(`[do] waiting on ssh ${ipv4}:${port}`);
337
+ }
338
+ log(`[do] ssh ready at ${ipv4}:${port}`);
339
+ const ssh = sshTarget({
340
+ host: ipv4,
341
+ ...options.user !== undefined ? { user: options.user } : {},
342
+ ...options.identity !== undefined ? { identity: options.identity } : {},
343
+ ...options.port !== undefined ? { port: options.port } : {}
344
+ });
345
+ const dropletId = current.id;
346
+ const resolvedIpv4 = ipv4;
347
+ return {
348
+ description: `digitalocean droplet "${options.name}" (${ssh.description})`,
349
+ dropletId,
350
+ ipv4: resolvedIpv4,
351
+ destroy: () => destroyDigitalOceanDroplet({ client, id: dropletId }).then(() => {
352
+ log(`[do] destroyed droplet ${dropletId}`);
353
+ }),
354
+ exec: ssh.exec,
355
+ upload: ssh.upload,
356
+ ...ssh.close !== undefined ? { close: ssh.close } : {}
357
+ };
358
+ };
359
+ export {
360
+ listDigitalOceanDroplets,
361
+ findDigitalOceanDroplet,
362
+ digitalOceanTarget,
363
+ destroyDigitalOceanDroplet,
364
+ createDigitalOceanClient,
365
+ DigitalOceanError
366
+ };
367
+
368
+ //# debugId=54E293FAC3F67A4164756E2164756E21
369
+ //# sourceMappingURL=digitalocean.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/targets.ts", "../src/digitalocean.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\t// Bun.spawn returns a FileSink for piped stdin — `write` + `end`, not a\n\t\t// WritableStream. (We use a permissive cast because @types/bun's\n\t\t// Subprocess.stdin discriminant flips based on the stdin generic.)\n\t\tconst sink = proc.stdin as unknown as {\n\t\t\twrite: (chunk: string | Uint8Array) => number | Promise<number>;\n\t\t\tend: () => void | Promise<void>;\n\t\t};\n\t\tconst wrote = sink.write(options.stdin);\n\t\tif (wrote && typeof (wrote as Promise<number>).then === 'function') {\n\t\t\tawait wrote;\n\t\t}\n\t\tconst ended = sink.end();\n\t\tif (ended && typeof (ended as Promise<void>).then === 'function') {\n\t\t\tawait ended;\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 * @absolutejs/deploy/digitalocean — provision-or-reuse Target adapter\n * for DigitalOcean droplets.\n *\n * What it does:\n *\n * 1. Looks up a droplet by `name`. If present and active, reuses it.\n * 2. If not present, creates it via the DO v2 API and waits for\n * `status === 'active'` with a public IPv4 assigned.\n * 3. Waits for SSH readiness (TCP connect on port 22 with backoff,\n * or a caller-supplied probe).\n * 4. Returns a Target that wraps sshTarget against the droplet's\n * public IPv4, plus `dropletId`, `ipv4`, and a `destroy()` helper.\n *\n * Idempotent by name — calling twice with the same name returns the\n * same droplet, no duplicates created. If multiple droplets share\n * the name, throws (the caller has drifted state to clean up).\n *\n * Narrow DigitalOceanClientLike interface keeps the dots-on-the-i\n * SDK out as a hard dep. Default client uses `fetch` against\n * `api.digitalocean.com`; pass your own for retry / observability.\n */\n\nimport type { Target } from './targets';\nimport { sshTarget } from './targets';\n\nconst DO_API_BASE = 'https://api.digitalocean.com/v2';\n\n/**\n * Minimal subset of DO API calls we make. Lets callers BYO a client\n * with retry / observability / etc. (e.g. wrap got, undici, or a\n * tenant-scoped client that injects different tokens per call).\n */\nexport type DigitalOceanClientLike = {\n\trequest: <T = unknown>(\n\t\tmethod: 'GET' | 'POST' | 'DELETE',\n\t\tpath: string,\n\t\tbody?: unknown\n\t) => Promise<T>;\n};\n\n/** A DigitalOcean droplet record, narrowed to what we inspect. */\nexport type DigitalOceanDroplet = {\n\tid: number;\n\tname: string;\n\tstatus: 'new' | 'active' | 'off' | 'archive';\n\tregion?: { slug: string };\n\tsize_slug?: string;\n\tnetworks: {\n\t\tv4: Array<{ ip_address: string; type: 'public' | 'private' }>;\n\t\tv6?: Array<{ ip_address: string; type: 'public' | 'private' }>;\n\t};\n\ttags?: string[];\n};\n\nexport type DigitalOceanTargetOptions = {\n\t/** API token (https://cloud.digitalocean.com/account/api/tokens). Required unless `client` is set. */\n\ttoken?: string;\n\t/** Custom client. Overrides token-built default. */\n\tclient?: DigitalOceanClientLike;\n\n\t// ── Droplet shape ────────────────────────────────────────────────\n\t/** Droplet name. Also the idempotency key. */\n\tname: string;\n\t/** Region slug — `'nyc3'`, `'sfo3'`, `'ams3'`, etc. */\n\tregion: string;\n\t/** Size slug — `'s-1vcpu-1gb'`, `'s-2vcpu-4gb'`, etc. */\n\tsize: string;\n\t/** Image slug, snapshot id, or backup id. e.g. `'ubuntu-22-04-x64'`. */\n\timage: string | number;\n\t/** SSH key fingerprints OR numeric ids. At least one required to ssh in. */\n\tsshKeys: ReadonlyArray<string | number>;\n\t/** Tags applied at creation. Useful for `listDroplets({ tag })`. */\n\ttags?: ReadonlyArray<string>;\n\t/** cloud-init user data — a shell script or YAML config. */\n\tuserData?: string;\n\t/** VPC UUID. Defaults to the account's default VPC for the region. */\n\tvpcUuid?: string;\n\t/** Enable IPv6. Default false. */\n\tipv6?: boolean;\n\t/** Enable monitoring agent. Default false. */\n\tmonitoring?: boolean;\n\n\t// ── SSH wrap ────────────────────────────────────────────────────\n\t/** SSH login user. Default `'root'`. */\n\tuser?: string;\n\t/** Path to SSH identity file forwarded to sshTarget. */\n\tidentity?: string;\n\t/** SSH port. Default 22. */\n\tport?: number;\n\n\t// ── Timing ──────────────────────────────────────────────────────\n\t/** Max time to wait for droplet `active` + IPv4. Default 5 min. */\n\tprovisionTimeoutMs?: number;\n\t/** Max time to wait for SSH probe to succeed. Default 2 min. */\n\tsshReadinessTimeoutMs?: number;\n\t/** Poll interval for provision + ssh probe. Default 5 s. */\n\tpollIntervalMs?: number;\n\n\t// ── Observability + injection points ───────────────────────────\n\t/** Called with status updates (one line each). Default: noop. */\n\tonLog?: (line: string) => void;\n\t/**\n\t * Override the SSH readiness probe. Default opens a TCP socket to\n\t * `host:port`. Tests pass a fake probe to skip real network IO.\n\t */\n\tprobeSsh?: (host: string, port: number) => Promise<boolean>;\n\t/**\n\t * Sleep used between polls. Default `setTimeout`-based. Tests can\n\t * pass a synchronous resolver to skip real waits.\n\t */\n\tsleep?: (ms: number) => Promise<void>;\n\t/** Wall clock. Defaults to `Date.now`. Tests can swap. */\n\tnow?: () => number;\n};\n\nexport type DigitalOceanTarget = Target & {\n\treadonly dropletId: number;\n\treadonly ipv4: string;\n\t/** Destroy the droplet via the DO API. */\n\tdestroy: () => Promise<void>;\n};\n\nexport class DigitalOceanError extends Error {\n\treadonly status: number;\n\treadonly body: unknown;\n\tconstructor(message: string, status: number, body: unknown) {\n\t\tsuper(message);\n\t\tthis.name = 'DigitalOceanError';\n\t\tthis.status = status;\n\t\tthis.body = body;\n\t}\n}\n\n/**\n * fetch-backed default client. Talks JSON to `api.digitalocean.com/v2`.\n * Throws DigitalOceanError on non-2xx with the response body attached\n * so the caller can switch on `err.status`.\n */\nexport const createDigitalOceanClient = (\n\ttoken: string,\n\toptions: { baseUrl?: string; fetch?: typeof fetch } = {}\n): DigitalOceanClientLike => {\n\tconst base = options.baseUrl ?? DO_API_BASE;\n\tconst f = options.fetch ?? fetch;\n\treturn {\n\t\trequest: async <T>(\n\t\t\tmethod: 'GET' | 'POST' | 'DELETE',\n\t\t\tpath: string,\n\t\t\tbody?: unknown\n\t\t): Promise<T> => {\n\t\t\tconst init: RequestInit = {\n\t\t\t\theaders: {\n\t\t\t\t\tauthorization: `Bearer ${token}`,\n\t\t\t\t\t'content-type': 'application/json'\n\t\t\t\t},\n\t\t\t\tmethod\n\t\t\t};\n\t\t\tif (body !== undefined) init.body = JSON.stringify(body);\n\t\t\tconst response = await f(`${base}${path}`, init);\n\t\t\tif (response.status === 204) return undefined as T;\n\t\t\tconst text = await response.text();\n\t\t\tconst parsed = text.length > 0 ? JSON.parse(text) : undefined;\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new DigitalOceanError(\n\t\t\t\t\t`DigitalOcean API ${method} ${path} failed: ${response.status} ${response.statusText}`,\n\t\t\t\t\tresponse.status,\n\t\t\t\t\tparsed\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn parsed as T;\n\t\t}\n\t};\n};\n\nconst resolveClient = (\n\toptions: Pick<DigitalOceanTargetOptions, 'client' | 'token'>\n): DigitalOceanClientLike => {\n\tif (options.client !== undefined) return options.client;\n\tif (options.token !== undefined && options.token.length > 0) {\n\t\treturn createDigitalOceanClient(options.token);\n\t}\n\tthrow new Error(\n\t\t'[deploy/digitalocean] either `token` or `client` must be provided'\n\t);\n};\n\nconst publicIpv4 = (droplet: DigitalOceanDroplet): string | undefined =>\n\tdroplet.networks.v4.find((net) => net.type === 'public')?.ip_address;\n\nconst defaultSleep = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => setTimeout(resolve, ms));\n\nconst defaultProbeSsh = async (host: string, port: number): Promise<boolean> => {\n\tconst PROBE_TIMEOUT_MS = 2_000;\n\treturn new Promise<boolean>((resolve) => {\n\t\tlet settled = false;\n\t\tconst settle = (value: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tresolve(value);\n\t\t};\n\t\tconst timer = setTimeout(() => settle(false), PROBE_TIMEOUT_MS);\n\t\tBun.connect({\n\t\t\thostname: host,\n\t\t\tport,\n\t\t\tsocket: {\n\t\t\t\tdata: () => {},\n\t\t\t\terror: () => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tsettle(false);\n\t\t\t\t},\n\t\t\t\topen: (socket) => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tsocket.end();\n\t\t\t\t\tsettle(true);\n\t\t\t\t}\n\t\t\t}\n\t\t}).catch(() => {\n\t\t\tclearTimeout(timer);\n\t\t\tsettle(false);\n\t\t});\n\t});\n};\n\n/**\n * Find a droplet by name. Returns undefined if absent.\n * Throws if more than one droplet shares the name (drifted state).\n */\nexport const findDigitalOceanDroplet = async (\n\tclient: DigitalOceanClientLike,\n\tname: string\n): Promise<DigitalOceanDroplet | undefined> => {\n\t// DO's list endpoint supports `name=` exact-match filtering.\n\tconst body = await client.request<{ droplets: DigitalOceanDroplet[] }>(\n\t\t'GET',\n\t\t`/droplets?name=${encodeURIComponent(name)}`\n\t);\n\tconst matches = body.droplets.filter((droplet) => droplet.name === name);\n\tif (matches.length === 0) return undefined;\n\tif (matches.length > 1) {\n\t\tthrow new Error(\n\t\t\t`[deploy/digitalocean] multiple droplets named \"${name}\" (${matches\n\t\t\t\t.map((droplet) => droplet.id)\n\t\t\t\t.join(', ')}). Resolve manually before adopting.`\n\t\t);\n\t}\n\treturn matches[0];\n};\n\n/** List droplets, optionally filtered by tag. Useful for cleanup tasks. */\nexport const listDigitalOceanDroplets = async (options: {\n\ttoken?: string;\n\tclient?: DigitalOceanClientLike;\n\ttag?: string;\n}): Promise<DigitalOceanDroplet[]> => {\n\tconst client = resolveClient(options);\n\tconst path =\n\t\toptions.tag !== undefined\n\t\t\t? `/droplets?tag_name=${encodeURIComponent(options.tag)}`\n\t\t\t: '/droplets';\n\tconst body = await client.request<{ droplets: DigitalOceanDroplet[] }>(\n\t\t'GET',\n\t\tpath\n\t);\n\treturn body.droplets;\n};\n\n/** Destroy a droplet by id. No-op if already gone. */\nexport const destroyDigitalOceanDroplet = async (options: {\n\ttoken?: string;\n\tclient?: DigitalOceanClientLike;\n\tid: number;\n}): Promise<void> => {\n\tconst client = resolveClient(options);\n\ttry {\n\t\tawait client.request('DELETE', `/droplets/${options.id}`);\n\t} catch (error) {\n\t\tif (error instanceof DigitalOceanError && error.status === 404) {\n\t\t\treturn; // already destroyed — idempotent\n\t\t}\n\t\tthrow error;\n\t}\n};\n\n/**\n * Provision-or-reuse a DO droplet by name, wait for SSH, return a\n * Target. Idempotent: same name → same droplet.\n */\nexport const digitalOceanTarget = async (\n\toptions: DigitalOceanTargetOptions\n): Promise<DigitalOceanTarget> => {\n\tconst client = resolveClient(options);\n\tconst log = options.onLog ?? (() => {});\n\tconst probeSsh = options.probeSsh ?? defaultProbeSsh;\n\tconst sleep = options.sleep ?? defaultSleep;\n\tconst now = options.now ?? Date.now;\n\tconst pollMs = options.pollIntervalMs ?? 5_000;\n\tconst provisionTimeout = options.provisionTimeoutMs ?? 5 * 60_000;\n\tconst sshTimeout = options.sshReadinessTimeoutMs ?? 2 * 60_000;\n\tconst port = options.port ?? 22;\n\n\tconst existing = await findDigitalOceanDroplet(client, options.name);\n\tlet current: DigitalOceanDroplet;\n\tif (existing === undefined) {\n\t\tlog(`[do] creating droplet \"${options.name}\" in ${options.region}`);\n\t\tconst created = await client.request<{ droplet: DigitalOceanDroplet }>(\n\t\t\t'POST',\n\t\t\t'/droplets',\n\t\t\t{\n\t\t\t\tname: options.name,\n\t\t\t\tregion: options.region,\n\t\t\t\tsize: options.size,\n\t\t\t\timage: options.image,\n\t\t\t\tssh_keys: [...options.sshKeys],\n\t\t\t\t...(options.tags !== undefined ? { tags: [...options.tags] } : {}),\n\t\t\t\t...(options.userData !== undefined\n\t\t\t\t\t? { user_data: options.userData }\n\t\t\t\t\t: {}),\n\t\t\t\t...(options.vpcUuid !== undefined\n\t\t\t\t\t? { vpc_uuid: options.vpcUuid }\n\t\t\t\t\t: {}),\n\t\t\t\t...(options.ipv6 === true ? { ipv6: true } : {}),\n\t\t\t\t...(options.monitoring === true ? { monitoring: true } : {})\n\t\t\t}\n\t\t);\n\t\tcurrent = created.droplet;\n\t} else {\n\t\tlog(\n\t\t\t`[do] reusing droplet \"${options.name}\" (id ${existing.id}, status ${existing.status})`\n\t\t);\n\t\tcurrent = existing;\n\t}\n\n\t// Wait for status=active AND public IPv4 assigned.\n\tconst provisionStart = now();\n\tlet ipv4 = publicIpv4(current);\n\twhile (current.status !== 'active' || ipv4 === undefined) {\n\t\tif (now() - provisionStart > provisionTimeout) {\n\t\t\tthrow new Error(\n\t\t\t\t`[deploy/digitalocean] provision timeout after ${provisionTimeout}ms — droplet ${current.id} status \"${current.status}\", ipv4 ${ipv4 ?? '(unassigned)'}`\n\t\t\t);\n\t\t}\n\t\tawait sleep(pollMs);\n\t\tconst refreshed: { droplet: DigitalOceanDroplet } = await client.request(\n\t\t\t'GET',\n\t\t\t`/droplets/${current.id}`\n\t\t);\n\t\tcurrent = refreshed.droplet;\n\t\tipv4 = publicIpv4(current);\n\t\tlog(`[do] poll: status=${current.status} ipv4=${ipv4 ?? '(none yet)'}`);\n\t}\n\tlog(`[do] droplet active at ${ipv4}`);\n\n\t// Wait for SSH readiness.\n\tconst sshStart = now();\n\twhile (!(await probeSsh(ipv4, port))) {\n\t\tif (now() - sshStart > sshTimeout) {\n\t\t\tthrow new Error(\n\t\t\t\t`[deploy/digitalocean] SSH readiness timeout after ${sshTimeout}ms — ${ipv4}:${port} did not accept connections`\n\t\t\t);\n\t\t}\n\t\tawait sleep(pollMs);\n\t\tlog(`[do] waiting on ssh ${ipv4}:${port}`);\n\t}\n\tlog(`[do] ssh ready at ${ipv4}:${port}`);\n\n\tconst ssh = sshTarget({\n\t\thost: ipv4,\n\t\t...(options.user !== undefined ? { user: options.user } : {}),\n\t\t...(options.identity !== undefined ? { identity: options.identity } : {}),\n\t\t...(options.port !== undefined ? { port: options.port } : {})\n\t});\n\n\tconst dropletId = current.id;\n\tconst resolvedIpv4 = ipv4;\n\n\treturn {\n\t\tdescription: `digitalocean droplet \"${options.name}\" (${ssh.description})`,\n\t\tdropletId,\n\t\tipv4: resolvedIpv4,\n\t\tdestroy: () =>\n\t\t\tdestroyDigitalOceanDroplet({ client, id: dropletId }).then(() => {\n\t\t\t\tlog(`[do] destroyed droplet ${dropletId}`);\n\t\t\t}),\n\t\texec: ssh.exec,\n\t\tupload: ssh.upload,\n\t\t...(ssh.close !== undefined ? { close: ssh.close } : {})\n\t};\n};\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "54E293FAC3F67A4164756E2164756E21",
10
+ "names": []
11
+ }
package/dist/index.d.ts CHANGED
@@ -14,5 +14,5 @@ export type { ExecOptions, ExecResult, LocalTargetOptions, SshTargetOptions, Tar
14
14
  export { localTarget, sshTarget } from './targets';
15
15
  export type { BareManagerOptions, ProcessManager, ProcessManagerContext, SystemdManagerOptions, } from './processManagers';
16
16
  export { bareManager, systemdManager } from './processManagers';
17
- export type { DeployContext, DeployHooks, DeployResult, DeployStep, Deployer, DeployerOptions, Source, VerifySpec, } from './deployer';
17
+ export type { DeployContext, DeployHooks, DeployOptions, DeployResult, DeployStep, Deployer, DeployerOptions, ReleaseAnnotations, ReleaseRecord, Source, VerifySpec, } from './deployer';
18
18
  export { createDeployer, defaultBunPipeline } from './deployer';
package/dist/index.js CHANGED
@@ -48,10 +48,14 @@ var runSpawn = async (argv, options) => {
48
48
  stdout: "pipe"
49
49
  });
50
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();
51
+ const sink = proc.stdin;
52
+ const wrote = sink.write(options.stdin);
53
+ if (wrote && typeof wrote.then === "function") {
54
+ await wrote;
55
+ }
56
+ const ended = sink.end();
57
+ if (ended && typeof ended.then === "function") {
58
+ await ended;
55
59
  }
56
60
  }
57
61
  const timeout = options.timeoutMs ?? 600000;
@@ -443,9 +447,11 @@ var createDeployer = (options) => {
443
447
  const processManager = options.processManager ?? bareManager();
444
448
  const verify = options.verify === undefined ? null : options.verify;
445
449
  let disposed = false;
446
- const buildCtx = (releaseId) => ({
450
+ const buildCtx = (releaseId, opts) => ({
451
+ annotations: opts.annotations,
447
452
  appName: options.appName,
448
453
  currentPath,
454
+ dryRun: opts.dryRun,
449
455
  env,
450
456
  hooks,
451
457
  processManager,
@@ -455,25 +461,86 @@ var createDeployer = (options) => {
455
461
  target: options.target,
456
462
  verify
457
463
  });
458
- const runSteps = async (steps, releaseId) => {
459
- const ctx = buildCtx(releaseId);
464
+ const metaPath = (releaseId) => `${releasesPath}/${releaseId}/.deploy-meta.json`;
465
+ const writeMeta = async (releaseId, record) => {
466
+ const json = JSON.stringify(record);
467
+ const result = await options.target.exec(`cat > ${metaPath(releaseId)}`, {
468
+ stdin: json,
469
+ timeoutMs: 1e4
470
+ });
471
+ if (result.exitCode !== 0) {
472
+ console.warn(`[deploy] writeMeta(${releaseId}) failed: ${result.stderr || result.stdout}`);
473
+ }
474
+ };
475
+ const readMeta = async (releaseId) => {
476
+ const result = await options.target.exec(`cat ${metaPath(releaseId)} 2>/dev/null || true`, {
477
+ timeoutMs: 1e4
478
+ });
479
+ const text = result.stdout.trim();
480
+ if (text.length === 0)
481
+ return null;
482
+ try {
483
+ return JSON.parse(text);
484
+ } catch {
485
+ return null;
486
+ }
487
+ };
488
+ const cleanOrphanedSymlink = async () => {
489
+ await options.target.exec(`rm -f ${currentPath}.next`, { timeoutMs: 5000 });
490
+ };
491
+ const runSteps = async (steps, releaseId, runOpts) => {
492
+ const ctx = buildCtx(releaseId, {
493
+ annotations: runOpts.annotations,
494
+ dryRun: runOpts.dryRun
495
+ });
460
496
  const stepDurations = [];
461
497
  const startedAt = clock();
498
+ const completedSteps = [...runOpts.alreadyCompleted];
499
+ const record = {
500
+ annotations: runOpts.annotations,
501
+ completedSteps,
502
+ releaseId,
503
+ startedAt,
504
+ status: "in-progress"
505
+ };
462
506
  for (const step of steps) {
507
+ if (completedSteps.includes(step.name) && step.name !== "verify") {
508
+ stepDurations.push({ durationMs: 0, name: step.name, skipped: true });
509
+ continue;
510
+ }
463
511
  const stepStartedAt = clock();
464
512
  await hooks.onStepStart({ name: step.name, releaseId });
513
+ if (runOpts.dryRun) {
514
+ hooks.onLog(`[dry-run] would run: ${step.name}`, "stdout", step.name);
515
+ stepDurations.push({ durationMs: 0, name: step.name, skipped: true });
516
+ await hooks.onStepEnd({ durationMs: 0, name: step.name, releaseId });
517
+ continue;
518
+ }
465
519
  try {
466
520
  await step.run(ctx);
467
521
  } catch (error) {
468
522
  const err = error instanceof Error ? error : new Error(String(error));
523
+ record.status = "failed";
524
+ record.failedStep = step.name;
525
+ record.endedAt = clock();
526
+ await writeMeta(releaseId, record);
469
527
  await hooks.onError({ error: err, releaseId, step: step.name });
470
528
  throw err;
471
529
  }
472
530
  const durationMs = clock() - stepStartedAt;
473
531
  stepDurations.push({ durationMs, name: step.name });
532
+ completedSteps.push(step.name);
474
533
  await hooks.onStepEnd({ durationMs, name: step.name, releaseId });
534
+ if (step.name === "prepare") {
535
+ await writeMeta(releaseId, record);
536
+ }
475
537
  }
538
+ record.status = "completed";
539
+ record.endedAt = clock();
540
+ if (!runOpts.dryRun)
541
+ await writeMeta(releaseId, record);
476
542
  return {
543
+ annotations: runOpts.annotations,
477
544
  currentPath,
478
545
  durationMs: clock() - startedAt,
479
546
  releaseId,
@@ -486,12 +553,33 @@ var createDeployer = (options) => {
486
553
  requireSuccess("ensureRoot", result);
487
554
  };
488
555
  return {
489
- deploy: async () => {
556
+ deploy: async (deployOpts = {}) => {
490
557
  if (disposed)
491
558
  throw new Error("Deployer is disposed");
492
559
  await ensureRoot();
560
+ await cleanOrphanedSymlink();
561
+ const annotations = deployOpts.annotations ?? {};
562
+ const dryRun = deployOpts.dryRun ?? false;
563
+ if (deployOpts.resumeReleaseId !== undefined) {
564
+ const prior = await readMeta(deployOpts.resumeReleaseId);
565
+ if (!prior) {
566
+ throw new Error(`resume: no .deploy-meta.json for release ${deployOpts.resumeReleaseId}`);
567
+ }
568
+ if (prior.status === "completed") {
569
+ throw new Error(`resume: release ${deployOpts.resumeReleaseId} already completed`);
570
+ }
571
+ return runSteps(options.steps ?? defaultBunPipeline(), deployOpts.resumeReleaseId, {
572
+ alreadyCompleted: prior.completedSteps,
573
+ annotations: prior.annotations ?? annotations,
574
+ dryRun
575
+ });
576
+ }
493
577
  const releaseId = makeReleaseId(clock);
494
- return runSteps(options.steps ?? defaultBunPipeline(), releaseId);
578
+ return runSteps(options.steps ?? defaultBunPipeline(), releaseId, {
579
+ alreadyCompleted: [],
580
+ annotations,
581
+ dryRun
582
+ });
495
583
  },
496
584
  dispose: async () => {
497
585
  disposed = true;
@@ -520,16 +608,23 @@ var createDeployer = (options) => {
520
608
  }
521
609
  return { removed };
522
610
  },
611
+ readReleaseMeta: readMeta,
523
612
  rollback: async (releaseId) => {
524
613
  if (disposed)
525
614
  throw new Error("Deployer is disposed");
526
615
  await ensureRoot();
616
+ await cleanOrphanedSymlink();
527
617
  const exists = await options.target.exec(`test -d ${releasesPath}/${releaseId} && echo ok || echo missing`, { timeoutMs: 5000 });
528
618
  if (!exists.stdout.includes("ok")) {
529
619
  throw new Error(`rollback: release ${releaseId} not found at ${releasesPath}/${releaseId}`);
530
620
  }
531
621
  const rollbackSteps = defaultBunPipeline().filter((step) => step.name === "link" || step.name === "restart" || step.name === "verify");
532
- return runSteps(rollbackSteps, releaseId);
622
+ const prior = await readMeta(releaseId);
623
+ return runSteps(rollbackSteps, releaseId, {
624
+ alreadyCompleted: [],
625
+ annotations: prior?.annotations ?? {},
626
+ dryRun: false
627
+ });
533
628
  }
534
629
  };
535
630
  };
@@ -542,5 +637,5 @@ export {
542
637
  bareManager
543
638
  };
544
639
 
545
- //# debugId=044FAFA4F653318F64756E2164756E21
640
+ //# debugId=52A04E705ABACCE264756E2164756E21
546
641
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -2,11 +2,11 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/targets.ts", "../src/processManagers.ts", "../src/deployer.ts"],
4
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",
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\t// Bun.spawn returns a FileSink for piped stdin — `write` + `end`, not a\n\t\t// WritableStream. (We use a permissive cast because @types/bun's\n\t\t// Subprocess.stdin discriminant flips based on the stdin generic.)\n\t\tconst sink = proc.stdin as unknown as {\n\t\t\twrite: (chunk: string | Uint8Array) => number | Promise<number>;\n\t\t\tend: () => void | Promise<void>;\n\t\t};\n\t\tconst wrote = sink.write(options.stdin);\n\t\tif (wrote && typeof (wrote as Promise<number>).then === 'function') {\n\t\t\tawait wrote;\n\t\t}\n\t\tconst ended = sink.end();\n\t\tif (ended && typeof (ended as Promise<void>).then === 'function') {\n\t\t\tawait ended;\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
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"
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 ReleaseAnnotations = {\n\t/** Git commit SHA being deployed (40-char hex; truncated forms accepted). */\n\tcommitSha?: string;\n\t/** Git ref (e.g. `refs/heads/main`, `v1.2.3`). */\n\tref?: string;\n\t/** Commit message (or any human-readable description). */\n\tmessage?: string;\n\t/** Committer / deployer identity. */\n\tauthor?: string;\n\t/** Arbitrary tags for downstream filtering (status pages, audits). */\n\ttags?: Record<string, string>;\n};\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\tannotations: ReleaseAnnotations;\n\t/** When `true`, steps log what they WOULD do via hooks.onLog and do not mutate the target. */\n\tdryRun: boolean;\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 DeployOptions = {\n\t/** Per-release annotations stored alongside the release dir as `.deploy-meta.json`. */\n\tannotations?: ReleaseAnnotations;\n\t/**\n\t * When `true`, the deploy plan is logged but no mutation happens on the\n\t * target — steps still call `target.exec` only via `cmd === 'echo'`-style\n\t * dry-run probes. Use this from `gh actions` to verify pipeline shape\n\t * before flipping a real `current` symlink.\n\t */\n\tdryRun?: boolean;\n\t/**\n\t * Resume a previously-failed release. The deployer reads the release's\n\t * `.deploy-meta.json`, finds the step that died, and starts from there.\n\t * Steps that completed successfully are skipped. Use this when a deploy\n\t * fails on `verify` (e.g. health-check timeout) but the release is\n\t * otherwise intact on disk.\n\t */\n\tresumeReleaseId?: string;\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 ReleaseRecord = {\n\treleaseId: string;\n\tannotations: ReleaseAnnotations;\n\tstatus: 'in-progress' | 'completed' | 'failed';\n\tfailedStep?: string;\n\tcompletedSteps: string[];\n\tstartedAt: number;\n\tendedAt?: number;\n};\n\nexport type DeployResult = {\n\treleaseId: string;\n\treleasePath: string;\n\tcurrentPath: string;\n\tdurationMs: number;\n\tsteps: { name: string; durationMs: number; skipped?: boolean }[];\n\tannotations: ReleaseAnnotations;\n};\n\nexport type Deployer = {\n\tdeploy: (options?: DeployOptions) => Promise<DeployResult>;\n\trollback: (releaseId: string) => Promise<DeployResult>;\n\tlistReleases: () => Promise<string[]>;\n\t/** Read the deploy meta for a specific release (or null if missing). */\n\treadReleaseMeta: (releaseId: string) => Promise<ReleaseRecord | null>;\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 = (\n\t\treleaseId: string,\n\t\topts: { annotations: ReleaseAnnotations; dryRun: boolean },\n\t): DeployContext => ({\n\t\tannotations: opts.annotations,\n\t\tappName: options.appName,\n\t\tcurrentPath,\n\t\tdryRun: opts.dryRun,\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 metaPath = (releaseId: string) => `${releasesPath}/${releaseId}/.deploy-meta.json`;\n\n\tconst writeMeta = async (releaseId: string, record: ReleaseRecord): Promise<void> => {\n\t\tconst json = JSON.stringify(record);\n\t\t// Use stdin to avoid quoting hassles + so the JSON doesn't appear in `ps`.\n\t\tconst result = await options.target.exec(`cat > ${metaPath(releaseId)}`, {\n\t\t\tstdin: json,\n\t\t\ttimeoutMs: 10_000,\n\t\t});\n\t\tif (result.exitCode !== 0) {\n\t\t\t// Non-fatal — the deploy doesn't depend on the meta file for success.\n\t\t\tconsole.warn(`[deploy] writeMeta(${releaseId}) failed: ${result.stderr || result.stdout}`);\n\t\t}\n\t};\n\n\tconst readMeta = async (releaseId: string): Promise<ReleaseRecord | null> => {\n\t\tconst result = await options.target.exec(`cat ${metaPath(releaseId)} 2>/dev/null || true`, {\n\t\t\ttimeoutMs: 10_000,\n\t\t});\n\t\tconst text = result.stdout.trim();\n\t\tif (text.length === 0) return null;\n\t\ttry {\n\t\t\treturn JSON.parse(text) as ReleaseRecord;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t};\n\n\tconst cleanOrphanedSymlink = async (): Promise<void> => {\n\t\t// A prior deploy that crashed between `ln -sfn ... current.next` and\n\t\t// `mv -Tf current.next current` leaves `current.next` dangling. Clean\n\t\t// it so the next link step's `mv -Tf` is unambiguous.\n\t\tawait options.target.exec(`rm -f ${currentPath}.next`, { timeoutMs: 5_000 });\n\t};\n\n\tconst runSteps = async (\n\t\tsteps: DeployStep[],\n\t\treleaseId: string,\n\t\trunOpts: {\n\t\t\tannotations: ReleaseAnnotations;\n\t\t\tdryRun: boolean;\n\t\t\talreadyCompleted: string[];\n\t\t},\n\t): Promise<DeployResult> => {\n\t\tconst ctx = buildCtx(releaseId, {\n\t\t\tannotations: runOpts.annotations,\n\t\t\tdryRun: runOpts.dryRun,\n\t\t});\n\t\tconst stepDurations: { name: string; durationMs: number; skipped?: boolean }[] = [];\n\t\tconst startedAt = clock();\n\t\tconst completedSteps: string[] = [...runOpts.alreadyCompleted];\n\n\t\tconst record: ReleaseRecord = {\n\t\t\tannotations: runOpts.annotations,\n\t\t\tcompletedSteps,\n\t\t\treleaseId,\n\t\t\tstartedAt,\n\t\t\tstatus: 'in-progress',\n\t\t};\n\t\t// Write the meta-record as soon as the release directory exists, which\n\t\t// `prepare` sets up. Until then we have nowhere to put it.\n\n\t\tfor (const step of steps) {\n\t\t\tif (completedSteps.includes(step.name) && step.name !== 'verify') {\n\t\t\t\t// Resume: skip steps already done. (Always re-run verify so a\n\t\t\t\t// healthy probe is recorded post-resume.)\n\t\t\t\tstepDurations.push({ durationMs: 0, name: step.name, skipped: true });\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst stepStartedAt = clock();\n\t\t\tawait hooks.onStepStart({ name: step.name, releaseId });\n\n\t\t\tif (runOpts.dryRun) {\n\t\t\t\thooks.onLog(`[dry-run] would run: ${step.name}`, 'stdout', step.name);\n\t\t\t\tstepDurations.push({ durationMs: 0, name: step.name, skipped: true });\n\t\t\t\tawait hooks.onStepEnd({ durationMs: 0, name: step.name, releaseId });\n\t\t\t\tcontinue;\n\t\t\t}\n\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\trecord.status = 'failed';\n\t\t\t\trecord.failedStep = step.name;\n\t\t\t\trecord.endedAt = clock();\n\t\t\t\t// Best-effort meta write so resume() works later.\n\t\t\t\tawait writeMeta(releaseId, record);\n\t\t\t\tawait hooks.onError({ error: err, releaseId, step: step.name });\n\t\t\t\tthrow err;\n\t\t\t}\n\n\t\t\tconst durationMs = clock() - stepStartedAt;\n\t\t\tstepDurations.push({ durationMs, name: step.name });\n\t\t\tcompletedSteps.push(step.name);\n\t\t\tawait hooks.onStepEnd({ durationMs, name: step.name, releaseId });\n\n\t\t\t// After `prepare` (the first step that creates the dir), persist meta.\n\t\t\tif (step.name === 'prepare') {\n\t\t\t\tawait writeMeta(releaseId, record);\n\t\t\t}\n\t\t}\n\n\t\trecord.status = 'completed';\n\t\trecord.endedAt = clock();\n\t\tif (!runOpts.dryRun) await writeMeta(releaseId, record);\n\n\t\treturn {\n\t\t\tannotations: runOpts.annotations,\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 (deployOpts: DeployOptions = {}) => {\n\t\t\tif (disposed) throw new Error('Deployer is disposed');\n\t\t\tawait ensureRoot();\n\t\t\tawait cleanOrphanedSymlink();\n\n\t\t\tconst annotations = deployOpts.annotations ?? {};\n\t\t\tconst dryRun = deployOpts.dryRun ?? false;\n\n\t\t\tif (deployOpts.resumeReleaseId !== undefined) {\n\t\t\t\tconst prior = await readMeta(deployOpts.resumeReleaseId);\n\t\t\t\tif (!prior) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`resume: no .deploy-meta.json for release ${deployOpts.resumeReleaseId}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (prior.status === 'completed') {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`resume: release ${deployOpts.resumeReleaseId} already completed`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn runSteps(options.steps ?? defaultBunPipeline(), deployOpts.resumeReleaseId, {\n\t\t\t\t\talreadyCompleted: prior.completedSteps,\n\t\t\t\t\tannotations: prior.annotations ?? annotations,\n\t\t\t\t\tdryRun,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst releaseId = makeReleaseId(clock);\n\t\t\treturn runSteps(options.steps ?? defaultBunPipeline(), releaseId, {\n\t\t\t\talreadyCompleted: [],\n\t\t\t\tannotations,\n\t\t\t\tdryRun,\n\t\t\t});\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\treadReleaseMeta: readMeta,\n\t\trollback: async (releaseId) => {\n\t\t\tif (disposed) throw new Error('Deployer is disposed');\n\t\t\tawait ensureRoot();\n\t\t\tawait cleanOrphanedSymlink();\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\tconst prior = await readMeta(releaseId);\n\t\t\treturn runSteps(rollbackSteps, releaseId, {\n\t\t\t\talreadyCompleted: [],\n\t\t\t\tannotations: prior?.annotations ?? {},\n\t\t\t\tdryRun: false,\n\t\t\t});\n\t\t},\n\t};\n};\n"
8
8
  ],
9
- "mappings": "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",
10
- "debugId": "044FAFA4F653318F64756E2164756E21",
9
+ "mappings": "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",
10
+ "debugId": "52A04E705ABACCE264756E2164756E21",
11
11
  "names": []
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/deploy",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
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
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "release"
27
27
  ],
28
28
  "scripts": {
29
- "build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json",
29
+ "build": "rm -rf dist && bun build src/index.ts src/digitalocean.ts --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json",
30
30
  "test": "bun test tests/",
31
31
  "typecheck": "tsc --noEmit",
32
32
  "format": "prettier --write \"./**/*.{ts,json,md}\"",
@@ -46,6 +46,11 @@
46
46
  "types": "./dist/index.d.ts",
47
47
  "import": "./dist/index.js",
48
48
  "default": "./dist/index.js"
49
+ },
50
+ "./digitalocean": {
51
+ "types": "./dist/digitalocean.d.ts",
52
+ "import": "./dist/digitalocean.js",
53
+ "default": "./dist/digitalocean.js"
49
54
  }
50
55
  },
51
56
  "files": ["dist", "README.md"]