unitup 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/index.d.ts +61 -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 +172 -51
- package/src/core/supervisor.js +25 -8
- 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 +29 -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,10 +99,16 @@ 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}`,
|
|
105
109
|
'After=network.target',
|
|
110
|
+
'StartLimitIntervalSec=30s',
|
|
111
|
+
'StartLimitBurst=5',
|
|
106
112
|
'',
|
|
107
113
|
'[Service]',
|
|
108
114
|
'Type=simple',
|
|
@@ -111,8 +117,12 @@ export function generateUnitContent(opts) {
|
|
|
111
117
|
`ExecStart=${execStartLine}`,
|
|
112
118
|
`Restart=${restartPolicy}`,
|
|
113
119
|
'RestartSec=3',
|
|
120
|
+
'RestartPreventExitStatus=78',
|
|
114
121
|
'StandardOutput=journal',
|
|
115
|
-
'StandardError=journal'
|
|
122
|
+
'StandardError=journal',
|
|
123
|
+
`LogLevelMax=${logLevelMax}`,
|
|
124
|
+
`LogRateLimitIntervalSec=${logRateLimitIntervalSec}`,
|
|
125
|
+
`LogRateLimitBurst=${logRateLimitBurst}`
|
|
116
126
|
];
|
|
117
127
|
|
|
118
128
|
const effectiveLimits = resolveEffectiveMemoryLimits(opts);
|
|
@@ -173,6 +183,8 @@ export function writeUnitFile(opts) {
|
|
|
173
183
|
script: opts.script,
|
|
174
184
|
cwd: opts.cwd,
|
|
175
185
|
node: opts.nodePath,
|
|
186
|
+
port: opts.port,
|
|
187
|
+
deploy: opts.deploy,
|
|
176
188
|
memoryHigh: opts.memoryHigh || opts.resources?.memoryHigh,
|
|
177
189
|
memoryMax: opts.memoryMax || opts.resources?.memoryMax,
|
|
178
190
|
memorySwapMax: opts.memorySwapMax || opts.resources?.memorySwapMax,
|
|
@@ -257,12 +269,20 @@ export function parseUnitContent(content) {
|
|
|
257
269
|
}
|
|
258
270
|
} else if (trimmed.startsWith('Restart=')) {
|
|
259
271
|
result.restart = trimmed.slice(8).trim();
|
|
272
|
+
} else if (trimmed.startsWith('RestartPreventExitStatus=')) {
|
|
273
|
+
result.restartPreventExitStatus = trimmed.slice(25).trim();
|
|
260
274
|
} else if (trimmed.startsWith('MemoryHigh=')) {
|
|
261
275
|
result.memoryHigh = trimmed.slice(11).trim();
|
|
262
276
|
} else if (trimmed.startsWith('MemoryMax=')) {
|
|
263
277
|
result.memoryMax = trimmed.slice(10).trim();
|
|
264
278
|
} else if (trimmed.startsWith('MemorySwapMax=')) {
|
|
265
279
|
result.memorySwapMax = trimmed.slice(14).trim();
|
|
280
|
+
} else if (trimmed.startsWith('LogLevelMax=')) {
|
|
281
|
+
result.logLevelMax = trimmed.slice(12).trim();
|
|
282
|
+
} else if (trimmed.startsWith('LogRateLimitIntervalSec=')) {
|
|
283
|
+
result.logRateLimitIntervalSec = trimmed.slice(24).trim();
|
|
284
|
+
} else if (trimmed.startsWith('LogRateLimitBurst=')) {
|
|
285
|
+
result.logRateLimitBurst = Number.parseInt(trimmed.slice(18).trim(), 10);
|
|
266
286
|
}
|
|
267
287
|
}
|
|
268
288
|
|
|
@@ -320,6 +340,10 @@ export function generateScheduleServiceContent(opts) {
|
|
|
320
340
|
.filter((p, i, self) => p && self.indexOf(p) === i)
|
|
321
341
|
.join(':');
|
|
322
342
|
|
|
343
|
+
const logLevelMax = opts.logLevelMax || 'info';
|
|
344
|
+
const logRateLimitIntervalSec = opts.logRateLimitIntervalSec || '10s';
|
|
345
|
+
const logRateLimitBurst = opts.logRateLimitBurst !== undefined ? opts.logRateLimitBurst : 200;
|
|
346
|
+
|
|
323
347
|
const serviceLines = [
|
|
324
348
|
'[Unit]',
|
|
325
349
|
`Description=unitup scheduled task: ${safeName}`,
|
|
@@ -331,7 +355,10 @@ export function generateScheduleServiceContent(opts) {
|
|
|
331
355
|
`WorkingDirectory=${cwd}`,
|
|
332
356
|
`ExecStart=${execStartLine}`,
|
|
333
357
|
'StandardOutput=journal',
|
|
334
|
-
'StandardError=journal'
|
|
358
|
+
'StandardError=journal',
|
|
359
|
+
`LogLevelMax=${logLevelMax}`,
|
|
360
|
+
`LogRateLimitIntervalSec=${logRateLimitIntervalSec}`,
|
|
361
|
+
`LogRateLimitBurst=${logRateLimitBurst}`
|
|
335
362
|
];
|
|
336
363
|
|
|
337
364
|
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
|
-
```
|