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.
@@ -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 = typeof rawWeight === 'string' ? Number.parseFloat(rawWeight.replace('%', '')) : Number(rawWeight);
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 && (!this.router.server || this.router.listeningPort !== 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 && (!this.router.server || this.router.listeningPort !== 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
- this.router.switchBackend(serviceName, previous);
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);
@@ -1,4 +1,4 @@
1
- import { spawn, execFile } from 'node:child_process';
1
+ import { execFile, spawn } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
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
- ```