unitup 0.3.0 → 0.4.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/CHANGELOG.md +17 -0
- package/index.d.ts +55 -8
- package/package.json +5 -3
- package/src/cli.js +7 -7
- package/src/core/process-manager.js +1 -1
- package/src/core/readiness.js +2 -1
- package/src/core/router.js +167 -50
- package/src/core/supervisor.js +5 -5
- package/src/deploy/deployment-manager.js +15 -34
- package/src/deploy/rollback-manager.js +2 -1
- package/src/index.js +23 -23
- package/src/platform/windows-host.js +1 -1
- package/src/unit.js +24 -2
- package/.agents/AGENTS.md +0 -35
- package/.agents/skills/multi-runtime-management/SKILL.md +0 -80
- package/.agents/skills/platform-service-adapters/SKILL.md +0 -108
- package/.agents/skills/unitup-development-and-qa/SKILL.md +0 -88
- package/.agents/skills/zero-downtime-deployment/SKILL.md +0 -135
- package/SECURITY.md +0 -21
- package/biome.json +0 -44
- package/docs/index.html +0 -587
- package/tsconfig.json +0 -13
- package/types-test/index.ts +0 -158
|
@@ -171,11 +171,7 @@ export class DeploymentManager extends EventEmitter {
|
|
|
171
171
|
);
|
|
172
172
|
|
|
173
173
|
// 4. WAITING_READY
|
|
174
|
-
this._setState(
|
|
175
|
-
DeploymentState.WAITING_READY,
|
|
176
|
-
{ service: serviceName, generation: next.id },
|
|
177
|
-
options.onProgress
|
|
178
|
-
);
|
|
174
|
+
this._setState(DeploymentState.WAITING_READY, { service: serviceName, generation: next.id }, options.onProgress);
|
|
179
175
|
|
|
180
176
|
try {
|
|
181
177
|
const readyPath = options.readyPath || config.deploy?.ready;
|
|
@@ -201,18 +197,19 @@ export class DeploymentManager extends EventEmitter {
|
|
|
201
197
|
const isCanary = Boolean(options.canary);
|
|
202
198
|
const rawWeight =
|
|
203
199
|
options.weight !== undefined ? options.weight : options.canaryWeight !== undefined ? options.canaryWeight : 0.1;
|
|
204
|
-
const parsedWeight =
|
|
200
|
+
const parsedWeight =
|
|
201
|
+
typeof rawWeight === 'string' ? Number.parseFloat(rawWeight.replace('%', '')) : Number(rawWeight);
|
|
205
202
|
const numWeight = Number.isNaN(parsedWeight) ? 0.1 : parsedWeight;
|
|
206
203
|
const canaryWeight = Math.max(0, Math.min(1, numWeight > 1 ? numWeight / 100 : numWeight));
|
|
207
204
|
|
|
208
205
|
if (isCanary) {
|
|
209
206
|
const publicPort = options.publicPort || config.port;
|
|
210
|
-
if (publicPort
|
|
211
|
-
await this.router.listen(publicPort);
|
|
207
|
+
if (publicPort) {
|
|
208
|
+
await this.router.listen(publicPort, '0.0.0.0', serviceName);
|
|
212
209
|
}
|
|
213
210
|
|
|
214
211
|
if (!previous) {
|
|
215
|
-
this.router.switchBackend(serviceName, next);
|
|
212
|
+
this.router.switchBackend(serviceName, next, publicPort);
|
|
216
213
|
this.generationManager.markActive(next);
|
|
217
214
|
}
|
|
218
215
|
|
|
@@ -222,7 +219,7 @@ export class DeploymentManager extends EventEmitter {
|
|
|
222
219
|
options.onProgress
|
|
223
220
|
);
|
|
224
221
|
|
|
225
|
-
this.router.setCanary(serviceName, next, canaryWeight);
|
|
222
|
+
this.router.setCanary(serviceName, next, canaryWeight, publicPort);
|
|
226
223
|
this.generationManager.markCanary(next, canaryWeight);
|
|
227
224
|
|
|
228
225
|
const result = {
|
|
@@ -240,26 +237,18 @@ export class DeploymentManager extends EventEmitter {
|
|
|
240
237
|
}
|
|
241
238
|
|
|
242
239
|
// 5. SWITCHING
|
|
243
|
-
this._setState(
|
|
244
|
-
DeploymentState.SWITCHING,
|
|
245
|
-
{ service: serviceName, generation: next.id },
|
|
246
|
-
options.onProgress
|
|
247
|
-
);
|
|
240
|
+
this._setState(DeploymentState.SWITCHING, { service: serviceName, generation: next.id }, options.onProgress);
|
|
248
241
|
|
|
249
242
|
const publicPort = options.publicPort || config.port;
|
|
250
|
-
if (publicPort
|
|
251
|
-
await this.router.listen(publicPort);
|
|
243
|
+
if (publicPort) {
|
|
244
|
+
await this.router.listen(publicPort, '0.0.0.0', serviceName);
|
|
252
245
|
}
|
|
253
246
|
|
|
254
|
-
this.router.switchBackend(serviceName, next);
|
|
247
|
+
this.router.switchBackend(serviceName, next, publicPort);
|
|
255
248
|
this.generationManager.markActive(next);
|
|
256
249
|
|
|
257
250
|
// 6. STABILIZING
|
|
258
|
-
this._setState(
|
|
259
|
-
DeploymentState.STABILIZING,
|
|
260
|
-
{ service: serviceName, generation: next.id },
|
|
261
|
-
options.onProgress
|
|
262
|
-
);
|
|
251
|
+
this._setState(DeploymentState.STABILIZING, { service: serviceName, generation: next.id }, options.onProgress);
|
|
263
252
|
|
|
264
253
|
const stabilizationMs =
|
|
265
254
|
options.stabilizationWindowMs !== undefined ? options.stabilizationWindowMs : this.defaultStabilizationMs;
|
|
@@ -271,7 +260,7 @@ export class DeploymentManager extends EventEmitter {
|
|
|
271
260
|
if (!this.processManager.isAlive(next.pid)) {
|
|
272
261
|
// New generation crashed during stabilization window! Revert to previous!
|
|
273
262
|
if (previous && this.processManager.isAlive(previous.pid)) {
|
|
274
|
-
this.router.switchBackend(serviceName, previous);
|
|
263
|
+
this.router.switchBackend(serviceName, previous, publicPort);
|
|
275
264
|
this.generationManager.markActive(previous);
|
|
276
265
|
}
|
|
277
266
|
this.generationManager.remove(next, true);
|
|
@@ -293,11 +282,7 @@ export class DeploymentManager extends EventEmitter {
|
|
|
293
282
|
|
|
294
283
|
// 7. DRAINING
|
|
295
284
|
if (previous && previous.id !== next.id) {
|
|
296
|
-
this._setState(
|
|
297
|
-
DeploymentState.DRAINING,
|
|
298
|
-
{ service: serviceName, generation: previous.id },
|
|
299
|
-
options.onProgress
|
|
300
|
-
);
|
|
285
|
+
this._setState(DeploymentState.DRAINING, { service: serviceName, generation: previous.id }, options.onProgress);
|
|
301
286
|
|
|
302
287
|
this.generationManager.markDraining(previous);
|
|
303
288
|
const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 10000;
|
|
@@ -370,11 +355,7 @@ export class DeploymentManager extends EventEmitter {
|
|
|
370
355
|
|
|
371
356
|
// 2. Drain previous generation
|
|
372
357
|
if (previous && previous.id !== canaryGen.id) {
|
|
373
|
-
this._setState(
|
|
374
|
-
DeploymentState.DRAINING,
|
|
375
|
-
{ service: serviceName, generation: previous.id },
|
|
376
|
-
options.onProgress
|
|
377
|
-
);
|
|
358
|
+
this._setState(DeploymentState.DRAINING, { service: serviceName, generation: previous.id }, options.onProgress);
|
|
378
359
|
this.generationManager.markDraining(previous);
|
|
379
360
|
const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 10000;
|
|
380
361
|
await this.drainManager.drain(previous, { timeout: drainTimeout, stopProcess: false });
|
|
@@ -151,7 +151,8 @@ export class RollbackManager {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// Switch router to previous generation
|
|
154
|
-
|
|
154
|
+
const publicPort = options.publicPort || config.port;
|
|
155
|
+
this.router.switchBackend(serviceName, previous, publicPort);
|
|
155
156
|
this.generationManager.markActive(previous);
|
|
156
157
|
|
|
157
158
|
// Drain and stop the rolled-back current generation
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeploymentLock,
|
|
3
|
+
DeploymentLockError,
|
|
4
|
+
DrainManager,
|
|
5
|
+
GenerationManager,
|
|
6
|
+
IPCClient,
|
|
7
|
+
IPCServer,
|
|
8
|
+
PortMismatchError,
|
|
9
|
+
ProcessManager,
|
|
10
|
+
ProcessStartupError,
|
|
11
|
+
ReadinessChecker,
|
|
12
|
+
ReadinessTimeoutError,
|
|
13
|
+
Router,
|
|
14
|
+
Supervisor,
|
|
15
|
+
findFreePort
|
|
16
|
+
} from './core/index.js';
|
|
17
|
+
import {
|
|
18
|
+
DeploymentManager,
|
|
19
|
+
DeploymentState,
|
|
20
|
+
RollbackManager,
|
|
21
|
+
defaultDeploymentManager,
|
|
22
|
+
defaultRollbackManager
|
|
23
|
+
} from './deploy/index.js';
|
|
1
24
|
import { getDoctorInfo, runDoctor } from './doctor.js';
|
|
2
25
|
import {
|
|
3
26
|
ExecutableNotFoundError,
|
|
@@ -95,29 +118,6 @@ import {
|
|
|
95
118
|
validateDuration,
|
|
96
119
|
validateMemorySize
|
|
97
120
|
} from './utils.js';
|
|
98
|
-
import {
|
|
99
|
-
ProcessManager,
|
|
100
|
-
GenerationManager,
|
|
101
|
-
Router,
|
|
102
|
-
ReadinessChecker,
|
|
103
|
-
ReadinessTimeoutError,
|
|
104
|
-
ProcessStartupError,
|
|
105
|
-
PortMismatchError,
|
|
106
|
-
DrainManager,
|
|
107
|
-
DeploymentLock,
|
|
108
|
-
DeploymentLockError,
|
|
109
|
-
Supervisor,
|
|
110
|
-
IPCServer,
|
|
111
|
-
IPCClient,
|
|
112
|
-
findFreePort
|
|
113
|
-
} from './core/index.js';
|
|
114
|
-
import {
|
|
115
|
-
DeploymentManager,
|
|
116
|
-
DeploymentState,
|
|
117
|
-
defaultDeploymentManager,
|
|
118
|
-
RollbackManager,
|
|
119
|
-
defaultRollbackManager
|
|
120
|
-
} from './deploy/index.js';
|
|
121
121
|
|
|
122
122
|
// Aliases for unified cross-platform API
|
|
123
123
|
const install = (opts) => defaultManager.install(opts);
|
package/src/unit.js
CHANGED
|
@@ -99,6 +99,10 @@ export function generateUnitContent(opts) {
|
|
|
99
99
|
.filter((p, i, self) => p && self.indexOf(p) === i)
|
|
100
100
|
.join(':');
|
|
101
101
|
|
|
102
|
+
const logLevelMax = opts.logLevelMax || 'info';
|
|
103
|
+
const logRateLimitIntervalSec = opts.logRateLimitIntervalSec || '10s';
|
|
104
|
+
const logRateLimitBurst = opts.logRateLimitBurst !== undefined ? opts.logRateLimitBurst : 200;
|
|
105
|
+
|
|
102
106
|
const serviceLines = [
|
|
103
107
|
'[Unit]',
|
|
104
108
|
`Description=unitup service: ${safeName}`,
|
|
@@ -112,7 +116,10 @@ export function generateUnitContent(opts) {
|
|
|
112
116
|
`Restart=${restartPolicy}`,
|
|
113
117
|
'RestartSec=3',
|
|
114
118
|
'StandardOutput=journal',
|
|
115
|
-
'StandardError=journal'
|
|
119
|
+
'StandardError=journal',
|
|
120
|
+
`LogLevelMax=${logLevelMax}`,
|
|
121
|
+
`LogRateLimitIntervalSec=${logRateLimitIntervalSec}`,
|
|
122
|
+
`LogRateLimitBurst=${logRateLimitBurst}`
|
|
116
123
|
];
|
|
117
124
|
|
|
118
125
|
const effectiveLimits = resolveEffectiveMemoryLimits(opts);
|
|
@@ -173,6 +180,8 @@ export function writeUnitFile(opts) {
|
|
|
173
180
|
script: opts.script,
|
|
174
181
|
cwd: opts.cwd,
|
|
175
182
|
node: opts.nodePath,
|
|
183
|
+
port: opts.port,
|
|
184
|
+
deploy: opts.deploy,
|
|
176
185
|
memoryHigh: opts.memoryHigh || opts.resources?.memoryHigh,
|
|
177
186
|
memoryMax: opts.memoryMax || opts.resources?.memoryMax,
|
|
178
187
|
memorySwapMax: opts.memorySwapMax || opts.resources?.memorySwapMax,
|
|
@@ -263,6 +272,12 @@ export function parseUnitContent(content) {
|
|
|
263
272
|
result.memoryMax = trimmed.slice(10).trim();
|
|
264
273
|
} else if (trimmed.startsWith('MemorySwapMax=')) {
|
|
265
274
|
result.memorySwapMax = trimmed.slice(14).trim();
|
|
275
|
+
} else if (trimmed.startsWith('LogLevelMax=')) {
|
|
276
|
+
result.logLevelMax = trimmed.slice(12).trim();
|
|
277
|
+
} else if (trimmed.startsWith('LogRateLimitIntervalSec=')) {
|
|
278
|
+
result.logRateLimitIntervalSec = trimmed.slice(24).trim();
|
|
279
|
+
} else if (trimmed.startsWith('LogRateLimitBurst=')) {
|
|
280
|
+
result.logRateLimitBurst = Number.parseInt(trimmed.slice(18).trim(), 10);
|
|
266
281
|
}
|
|
267
282
|
}
|
|
268
283
|
|
|
@@ -320,6 +335,10 @@ export function generateScheduleServiceContent(opts) {
|
|
|
320
335
|
.filter((p, i, self) => p && self.indexOf(p) === i)
|
|
321
336
|
.join(':');
|
|
322
337
|
|
|
338
|
+
const logLevelMax = opts.logLevelMax || 'info';
|
|
339
|
+
const logRateLimitIntervalSec = opts.logRateLimitIntervalSec || '10s';
|
|
340
|
+
const logRateLimitBurst = opts.logRateLimitBurst !== undefined ? opts.logRateLimitBurst : 200;
|
|
341
|
+
|
|
323
342
|
const serviceLines = [
|
|
324
343
|
'[Unit]',
|
|
325
344
|
`Description=unitup scheduled task: ${safeName}`,
|
|
@@ -331,7 +350,10 @@ export function generateScheduleServiceContent(opts) {
|
|
|
331
350
|
`WorkingDirectory=${cwd}`,
|
|
332
351
|
`ExecStart=${execStartLine}`,
|
|
333
352
|
'StandardOutput=journal',
|
|
334
|
-
'StandardError=journal'
|
|
353
|
+
'StandardError=journal',
|
|
354
|
+
`LogLevelMax=${logLevelMax}`,
|
|
355
|
+
`LogRateLimitIntervalSec=${logRateLimitIntervalSec}`,
|
|
356
|
+
`LogRateLimitBurst=${logRateLimitBurst}`
|
|
335
357
|
];
|
|
336
358
|
|
|
337
359
|
const effectiveLimits = resolveEffectiveMemoryLimits(opts);
|
package/.agents/AGENTS.md
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# Unitup Workspace Guidelines for AI Agents
|
|
2
|
-
|
|
3
|
-
Welcome to **Unitup** — a lightweight, zero-dependency, cross-platform process manager and zero-downtime deployment orchestration engine.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Core Invariants
|
|
8
|
-
|
|
9
|
-
1. **Zero External Dependencies**:
|
|
10
|
-
- Production code in `src/` must NEVER import anything outside Node.js built-ins (`node:*`).
|
|
11
|
-
- `package.json` must keep `"dependencies": {}`.
|
|
12
|
-
2. **Zero-Downtime Architectural Boundary**:
|
|
13
|
-
- Zero-downtime runtime primitives belong to `src/core/` (ProcessManager, Router, GenerationManager, ReadinessChecker, DrainManager, Supervisor, IPC).
|
|
14
|
-
- Deployment sequencing belongs to `src/deploy/` (DeploymentManager, RollbackManager).
|
|
15
|
-
- CLI (`src/cli.js`) is only a thin translation layer.
|
|
16
|
-
3. **Multi-OS Native Daemon Support**:
|
|
17
|
-
- Linux: systemd user/system services & timers.
|
|
18
|
-
- macOS: launchd LaunchAgents / LaunchDaemons `.plist`.
|
|
19
|
-
- Windows: Windows Services via `sc.exe` and `WindowsServiceHost`.
|
|
20
|
-
4. **Validation Triad**:
|
|
21
|
-
- Every code modification must pass:
|
|
22
|
-
```bash
|
|
23
|
-
npm test && npx tsc --noEmit && npx biome check src/ test/
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Workspace Skills Available
|
|
29
|
-
|
|
30
|
-
For deep-dive runbooks and procedures, activate the specialized skills in `.agents/skills/`:
|
|
31
|
-
|
|
32
|
-
- **[zero-downtime-deployment](file:///Users/ahmet/projects/unitup/.agents/skills/zero-downtime-deployment/SKILL.md)**: State machine, canary weighted routing, rollback, connection draining, and streaming protocol handling.
|
|
33
|
-
- **[platform-service-adapters](file:///Users/ahmet/projects/unitup/.agents/skills/platform-service-adapters/SKILL.md)**: Systemd, launchd, and Windows service integration, escaping, user lingering, and command runner mocking.
|
|
34
|
-
- **[multi-runtime-management](file:///Users/ahmet/projects/unitup/.agents/skills/multi-runtime-management/SKILL.md)**: Auto-detection and arguments for Node, Python, Bun, Deno, Go, Ruby, PHP, and generic binaries.
|
|
35
|
-
- **[unitup-development-and-qa](file:///Users/ahmet/projects/unitup/.agents/skills/unitup-development-and-qa/SKILL.md)**: Testing standards with `node:test`, TypeScript declaration sync, and Biome linting.
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: multi-runtime-management
|
|
3
|
-
description: >-
|
|
4
|
-
Manage, extend, and test multi-runtime support in Unitup: Node.js, Python, Bun, Deno, Go, Ruby,
|
|
5
|
-
PHP, Elixir, and generic executables. Use when modifying runtime detection, shebang parsing,
|
|
6
|
-
argument building, environment variable normalization, or memory limits.
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Multi-Runtime Management in Unitup
|
|
10
|
-
|
|
11
|
-
This skill details how Unitup automatically detects, configures, and executes services across different programming runtimes without requiring external dependencies or containers.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 1. Runtime Architecture
|
|
16
|
-
|
|
17
|
-
Runtime logic resides in:
|
|
18
|
-
```text
|
|
19
|
-
src/runtimes/
|
|
20
|
-
├── base.js # BaseRuntime contract: detect(), getExecutable(), getArgs()
|
|
21
|
-
├── node.js # NodeRuntime (.js, .mjs, .cjs)
|
|
22
|
-
├── python.js # PythonRuntime (.py, python shebang, unbuffered -u flag)
|
|
23
|
-
├── bun.js # BunRuntime (.js, .ts with bun)
|
|
24
|
-
├── deno.js # DenoRuntime (.ts with deno, auto permission flags)
|
|
25
|
-
├── go.js # GoRuntime (.go -> go run, or compiled binaries)
|
|
26
|
-
├── ruby.js # RubyRuntime (.rb)
|
|
27
|
-
├── php.js # PHPRuntime (.php -> built-in web server or CLI)
|
|
28
|
-
├── shell.js # ShellRuntime (.sh, bash shebang)
|
|
29
|
-
├── elixir.js # ElixirRuntime (.ex, .exs)
|
|
30
|
-
├── generic.js # GenericExecutableRuntime (arbitrary command line)
|
|
31
|
-
└── index.js # detectRuntime(), resolveRuntimeConfig(), listSupportedRuntimes()
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## 2. Detection & Resolution Rules
|
|
37
|
-
|
|
38
|
-
### 1. File Extension Detection
|
|
39
|
-
- `.js`, `.mjs`, `.cjs` -> Node.js (or Bun/Deno if explicitly specified via `--runtime`)
|
|
40
|
-
- `.py` -> Python (`python3` or `python` with `-u` for unbuffered logs)
|
|
41
|
-
- `.rb` -> Ruby (`ruby`)
|
|
42
|
-
- `.php` -> PHP (`php`)
|
|
43
|
-
- `.sh` -> Shell (`bash` or `sh`)
|
|
44
|
-
- `.go` -> Go (`go run`)
|
|
45
|
-
- `.ex`, `.exs` -> Elixir (`elixir`)
|
|
46
|
-
|
|
47
|
-
### 2. TypeScript Ambiguity Rule
|
|
48
|
-
- `.ts` files **cannot** be auto-resolved to a single engine because both Bun and Deno execute TypeScript natively.
|
|
49
|
-
- An error is thrown requiring the user to specify `--runtime bun` or `--runtime deno`.
|
|
50
|
-
|
|
51
|
-
### 3. Shebang Header Detection
|
|
52
|
-
If a file has no extension or is executable:
|
|
53
|
-
- Reads the first 256 bytes of the file.
|
|
54
|
-
- Inspects `#!/usr/bin/env ...` or `#!/bin/...` to detect runtime (e.g. `python3`, `ruby`, `node`, `bash`).
|
|
55
|
-
|
|
56
|
-
### 4. Generic Command (`--command`)
|
|
57
|
-
- When `--command "my-binary --flag"` is provided:
|
|
58
|
-
- Bypasses runtime detection.
|
|
59
|
-
- Resolves executable using `resolveExecutable()`.
|
|
60
|
-
- Splits and preserves argument tokens safely without shell injection vulnerabilities.
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## 3. Normalization & App Metadata
|
|
65
|
-
|
|
66
|
-
[normalizeServiceConfig](file:///Users/ahmet/projects/unitup/src/service/normalize.js) converts raw CLI/API flags into a canonical service descriptor:
|
|
67
|
-
|
|
68
|
-
- Resolves absolute `cwd` (Working directory).
|
|
69
|
-
- Resolves executable path or validates binary exists in `$PATH`.
|
|
70
|
-
- Normalizes memory limits: parses `512M`, `1G`, `256MB` into bytes and native OS config strings.
|
|
71
|
-
- Reads and saves metadata in `~/.config/unitup/apps/<name>.json`.
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## 4. Testing Multi-Runtime Capabilities
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
# Run multi-runtime detection and argument builder tests
|
|
79
|
-
node --test test/multi-runtime.test.js
|
|
80
|
-
```
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: platform-service-adapters
|
|
3
|
-
description: >-
|
|
4
|
-
Implement, test, and debug cross-platform OS service adapters in Unitup: systemd (Linux),
|
|
5
|
-
launchd (macOS), and Windows Services (Windows). Use when working with service definition generators,
|
|
6
|
-
system commands (systemctl, launchctl, sc.exe), user vs. system scopes, linger, or timer units.
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Cross-Platform Service Adapters in Unitup
|
|
10
|
-
|
|
11
|
-
This skill covers the implementation patterns, escaping rules, permission boundaries, and testing practices for Unitup's native OS service adapters.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 1. Adapter Architecture
|
|
16
|
-
|
|
17
|
-
Unitup implements a unified interface across operating systems via [ServiceAdapter](file:///Users/ahmet/projects/unitup/src/platform/base.js):
|
|
18
|
-
|
|
19
|
-
```text
|
|
20
|
-
src/platform/
|
|
21
|
-
├── base.js # Base ServiceAdapter abstract class with standard contract
|
|
22
|
-
├── linux.js # LinuxAdapter: systemd user & system units via systemctl
|
|
23
|
-
├── macos.js # MacOSAdapter: launchd launchd.plist via launchctl
|
|
24
|
-
├── windows.js # WindowsAdapter: Windows Service via sc.exe
|
|
25
|
-
├── windows-host.js # WindowsServiceHost child process manager for Windows
|
|
26
|
-
└── index.js # getAdapter(platform), getPlatformCapabilities()
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Standard Adapter Methods:
|
|
30
|
-
Each adapter implements:
|
|
31
|
-
- `getCapabilities()`: `{ systemd, launchd, windowsService, userUnits, systemUnits, timers, memoryLimits }`
|
|
32
|
-
- `generateService(config)`: Returns formatted service definition string
|
|
33
|
-
- `install(config, opts)`: Writes service file and registers with OS daemon
|
|
34
|
-
- `uninstall(name, opts)`: Unregisters service and deletes service file
|
|
35
|
-
- `start(name, opts)`, `stop(name, opts)`, `restart(name, opts)`
|
|
36
|
-
- `status(name, opts)`: Queries native service state (running, stopped, PID, uptime)
|
|
37
|
-
- `enable(name, opts)`, `disable(name, opts)`
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## 2. Platform Specifics & Service Formats
|
|
42
|
-
|
|
43
|
-
### A. Linux (systemd)
|
|
44
|
-
- **User scope (default)**: `~/.config/systemd/user/unitup-<name>.service`
|
|
45
|
-
- Requires user lingering for persistence across logout: `loginctl enable-linger <user>` (verified via `checkUserLinger()`).
|
|
46
|
-
- Controlled with `systemctl --user`.
|
|
47
|
-
- **System scope**: `/etc/systemd/system/unitup-<name>.service`
|
|
48
|
-
- Requires root privileges; throws `PermissionRequiredError` if unprivileged.
|
|
49
|
-
- **Unit Content Structure**:
|
|
50
|
-
```ini
|
|
51
|
-
[Unit]
|
|
52
|
-
Description=Unitup Service - <name>
|
|
53
|
-
After=network.target
|
|
54
|
-
|
|
55
|
-
[Service]
|
|
56
|
-
Type=simple
|
|
57
|
-
WorkingDirectory=/path/to/app
|
|
58
|
-
ExecStart=/path/to/executable ...
|
|
59
|
-
Restart=always
|
|
60
|
-
RestartSec=3
|
|
61
|
-
MemoryMax=512M
|
|
62
|
-
|
|
63
|
-
[Install]
|
|
64
|
-
WantedBy=default.target
|
|
65
|
-
```
|
|
66
|
-
- **Escaping**: Always use `formatSystemdEnv()` and `escapeExecArg()` from [src/utils.js](file:///Users/ahmet/projects/unitup/src/utils.js).
|
|
67
|
-
|
|
68
|
-
### B. macOS (launchd)
|
|
69
|
-
- **User scope (default)**: `~/Library/LaunchAgents/com.unitup.<name>.plist`
|
|
70
|
-
- Domain target: `gui/<uid>` (for logged-in user) or `user/<uid>`.
|
|
71
|
-
- Bootstrapping: `launchctl bootstrap gui/<uid> <plistPath>`
|
|
72
|
-
- Kickstarting: `launchctl kickstart -k gui/<uid>/com.unitup.<name>`
|
|
73
|
-
- **System scope**: `/Library/LaunchDaemons/com.unitup.<name>.plist`
|
|
74
|
-
- Domain target: `system` (requires root).
|
|
75
|
-
- **XML Plist Generation**: Generated cleanly with standard XML elements (`ProgramArguments`, `WorkingDirectory`, `KeepAlive`, `EnvironmentVariables`, `StandardOutPath`, `StandardErrorPath`).
|
|
76
|
-
|
|
77
|
-
### C. Windows (Windows Services)
|
|
78
|
-
- **Tooling**: Uses `sc.exe create <name> binPath= ... start= auto`.
|
|
79
|
-
- **WindowsServiceHost**: Because standard node scripts cannot run directly as Windows Services without a service wrapper, Unitup uses [WindowsServiceHost](file:///Users/ahmet/projects/unitup/src/platform/windows-host.js) to bridge process signals, log redirection, and Windows shutdown events.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## 3. Testing Platform Adapters Safely
|
|
84
|
-
|
|
85
|
-
Native system commands (`systemctl`, `launchctl`, `sc.exe`) cannot and should not be executed directly in unit tests.
|
|
86
|
-
|
|
87
|
-
Unitup provides runner injection via [src/utils.js](file:///Users/ahmet/projects/unitup/src/utils.js):
|
|
88
|
-
|
|
89
|
-
```javascript
|
|
90
|
-
import { setCommandRunner, resetCommandRunner } from '../src/utils.js';
|
|
91
|
-
|
|
92
|
-
// Mock commands in tests:
|
|
93
|
-
setCommandRunner(async (cmd, args) => {
|
|
94
|
-
if (cmd === 'systemctl' && args.includes('is-active')) {
|
|
95
|
-
return { stdout: 'active\n', stderr: '', code: 0 };
|
|
96
|
-
}
|
|
97
|
-
return { stdout: '', stderr: '', code: 0 };
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Always reset runner in afterEach / finally:
|
|
101
|
-
resetCommandRunner();
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Verification Commands:
|
|
105
|
-
```bash
|
|
106
|
-
# Test all platform adapters
|
|
107
|
-
node --test test/platform.test.js test/macos.test.js test/windows.test.js test/unit.test.js
|
|
108
|
-
```
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: unitup-development-and-qa
|
|
3
|
-
description: >-
|
|
4
|
-
Development standards, testing procedures, TypeScript type verification, Biome linting,
|
|
5
|
-
and zero-external-dependency constraints for the Unitup codebase. Use when adding new features,
|
|
6
|
-
running tests, updating index.d.ts, or validating pull requests.
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Unitup Development, QA & Quality Standards
|
|
10
|
-
|
|
11
|
-
This skill outlines the core engineering invariants, code quality checks, and testing workflows required for any contribution to Unitup.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 1. Golden Constraint: Zero External Dependencies
|
|
16
|
-
|
|
17
|
-
**Unitup has strictly ZERO production external dependencies (`dependencies: {}` in package.json).**
|
|
18
|
-
|
|
19
|
-
- Only Node.js built-in standard modules are allowed (`node:fs`, `node:path`, `node:os`, `node:child_process`, `node:http`, `node:net`, `node:events`, `node:assert`, `node:test`).
|
|
20
|
-
- Never add npm packages to `dependencies`.
|
|
21
|
-
- Development dependencies (`devDependencies`) are strictly limited to developer tooling: `biome` for linting/formatting and `typescript` for `.d.ts` type-checking.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## 2. Testing Principles & Node.js Test Runner
|
|
26
|
-
|
|
27
|
-
Unitup uses the native Node.js test runner (`node:test` and `node:assert`). No Jest, Mocha, or Vitest.
|
|
28
|
-
|
|
29
|
-
### Test Authoring Rules:
|
|
30
|
-
1. **Never use hardcoded network ports**:
|
|
31
|
-
- Always use `port: 0` for ephemeral port allocation or `findFreePort()`.
|
|
32
|
-
- Hardcoding ports like `3000` or `8080` breaks developer machines running local web servers.
|
|
33
|
-
2. **Deterministic Cleanup**:
|
|
34
|
-
- Every test that opens an HTTP server, router, socket, or temporary file must clean it up in an `afterEach` or `finally` block.
|
|
35
|
-
3. **Mock Process & System Calls**:
|
|
36
|
-
- Do not call real `systemctl`, `launchctl`, or `sc.exe` in unit tests. Use `setCommandRunner()`.
|
|
37
|
-
- Use simulated process managers when testing deployment state machines to keep tests blazing fast (< 100ms per suite).
|
|
38
|
-
|
|
39
|
-
### Common Test Commands:
|
|
40
|
-
```bash
|
|
41
|
-
# Run a specific test suite
|
|
42
|
-
node --test test/canary.test.js
|
|
43
|
-
node --test test/router.test.js
|
|
44
|
-
node --test test/deployment-manager.test.js
|
|
45
|
-
|
|
46
|
-
# Run the complete test suite across all platforms
|
|
47
|
-
npm test
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## 3. TypeScript Type Synchronization (`index.d.ts`)
|
|
53
|
-
|
|
54
|
-
Unitup is written in standard ES Modules (`"type": "module"`), but provides high-grade TypeScript declarations in [index.d.ts](file:///Users/ahmet/projects/unitup/index.d.ts).
|
|
55
|
-
|
|
56
|
-
Whenever a public function, class, option, or result object is added or modified in `src/`:
|
|
57
|
-
1. Update `index.d.ts` with matching types.
|
|
58
|
-
2. Verify with the TypeScript compiler:
|
|
59
|
-
```bash
|
|
60
|
-
npx tsc --noEmit
|
|
61
|
-
```
|
|
62
|
-
3. Ensure `npx tsc --noEmit` exits with **0 errors**.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## 4. Linting & Formatting with Biome
|
|
67
|
-
|
|
68
|
-
Unitup uses Biome for sub-second formatting and static analysis:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
# Check code style, formatting, and lint rules
|
|
72
|
-
npx biome check src/ test/
|
|
73
|
-
|
|
74
|
-
# Automatically apply safe fixes
|
|
75
|
-
npx biome check --write src/ test/
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Always ensure `npx biome check src/ test/` exits cleanly with zero errors before concluding tasks.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## 5. Pre-Commit / Pre-Push Checklist
|
|
83
|
-
|
|
84
|
-
Before submitting any task, run the full validation chain:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
npm test && npx tsc --noEmit && npx biome check src/ test/
|
|
88
|
-
```
|