@radleta/just-one 1.0.0 → 1.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/CHANGELOG.md CHANGED
@@ -1,21 +1,40 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
3
+ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+ ## [1.2.0](https://github.com/radleta/just-one/compare/v1.1.0...v1.2.0) (2026-02-11)
7
6
 
8
- ## [Unreleased]
7
+ ### Features
8
+
9
+ - **cli:** add status, kill-all, ensure, clean, pid, and wait operations ([5d1909b](https://github.com/radleta/just-one/commit/5d1909bc794a7a5b357d725fa59c7942dca7023d))
10
+
11
+ ### Bug Fixes
12
+
13
+ - **windows:** allow child process to run cleanup handlers on Ctrl+C ([ac3bca0](https://github.com/radleta/just-one/commit/ac3bca07fcbfeb39e98ad3f31c280b45152525e0))
14
+
15
+ ## [1.1.0](https://github.com/radleta/just-one/compare/v1.0.0...v1.1.0) (2026-01-29)
16
+
17
+ ### Features
18
+
19
+ - add automatic changelog generation with standard-version ([380ee82](https://github.com/radleta/just-one/commit/380ee826e93c900df52c189dd495909794ce1a84))
20
+
21
+ ### Bug Fixes
22
+
23
+ - read version from package.json instead of hardcoded value ([3803889](https://github.com/radleta/just-one/commit/3803889d2349d0a03d42ccbc45412bb0908ae895))
24
+
25
+ ## [1.0.0] - 2026-01-29
9
26
 
10
27
  ### Added
11
28
 
12
29
  - PID reuse protection: verifies process identity before killing by comparing PID file modification time with process start time
13
30
  - New dependency: [pidusage](https://github.com/soyuka/pidusage) for cross-platform process metrics
31
+ - Automatic changelog generation using [standard-version](https://github.com/conventional-changelog/standard-version)
14
32
 
15
33
  ### Changed
16
34
 
17
35
  - `handleRun()` and `handleKill()` now verify process identity before killing
18
36
  - Updated documentation to reflect PID reuse protection feature
37
+ - Release workflow now uses `npm run release` instead of `npm version`
19
38
 
20
39
  ### Fixed
21
40
 
package/README.md CHANGED
@@ -1,181 +1,233 @@
1
- # just-one
2
-
3
- A CLI tool that ensures only one instance of a command runs at a time. Kills the previous instance before starting a new one.
4
-
5
- ## Why This Exists
6
-
7
- When developing with dev servers (Storybook, Vite, webpack-dev-server, etc.), you often get:
8
-
9
- ```
10
- Error: Port 6006 is already in use
11
- ```
12
-
13
- Existing solutions have drawbacks:
14
- - **kill-port** - Kills ANY process on that port (imprecise, might kill unrelated processes)
15
- - **Manual** - Find PID, kill it, restart (tedious)
16
- - **pm2** - Overkill for dev servers
17
-
18
- `just-one` tracks processes by name using PID files. When you run a command, it kills the previous instance (if any) and starts fresh—precisely targeting only the process it started.
19
-
20
- ## Features
21
-
22
- - **Named process tracking** - Each process gets a unique name for precise targeting
23
- - **Automatic cleanup** - Previous instance killed before starting new one
24
- - **Cross-platform** - Works on Windows, macOS, and Linux
25
- - **Minimal dependencies** - Only [pidusage](https://github.com/soyuka/pidusage) for process verification
26
- - **PID file management** - Survives terminal closes and system restarts
27
- - **PID reuse protection** - Verifies process identity before killing to prevent accidents
28
-
29
- ## Installation
30
-
31
- ```bash
32
- npm install -g @radleta/just-one
33
- ```
34
-
35
- Or use with npx (no install required):
36
-
37
- ```bash
38
- npx @radleta/just-one -n myapp -- npm run dev
39
- ```
40
-
41
- ## Usage
42
-
43
- ### Basic usage
44
-
45
- ```bash
46
- # Run storybook, killing any previous instance named "storybook"
47
- just-one -n storybook -- npx storybook dev -p 6006
48
-
49
- # Run vite dev server
50
- just-one -n vite -- npm run dev
51
-
52
- # Run any command
53
- just-one -n myapp -- node server.js
54
- ```
55
-
56
- ### Kill a named process
57
-
58
- ```bash
59
- just-one -k storybook
60
- just-one --kill myapp
61
- ```
62
-
63
- ### List tracked processes
64
-
65
- ```bash
66
- just-one -l
67
- just-one --list
68
- ```
69
-
70
- ### Specify custom PID directory
71
-
72
- ```bash
73
- # Default: ./.just-one/<name>.pid
74
- just-one -n storybook -- npx storybook dev
75
-
76
- # Custom directory
77
- just-one -n storybook -d /tmp -- npx storybook dev
78
- ```
79
-
80
- ## CLI Options
81
-
82
- | Option | Alias | Description |
83
- |--------|-------|-------------|
84
- | `--name <name>` | `-n` | Required for run. Name to identify this process |
85
- | `--kill <name>` | `-k` | Kill the named process and exit |
86
- | `--list` | `-l` | List all tracked processes and their status |
87
- | `--pid-dir <dir>` | `-d` | Directory for PID files (default: `.just-one/`) |
88
- | `--quiet` | `-q` | Suppress output |
89
- | `--help` | `-h` | Show help |
90
- | `--version` | `-v` | Show version |
91
-
92
- ## package.json Scripts
93
-
94
- ```json
95
- {
96
- "scripts": {
97
- "storybook": "just-one -n storybook -- storybook dev -p 6006",
98
- "dev": "just-one -n vite -- vite",
99
- "dev:api": "just-one -n api -- node server.js",
100
- "stop": "just-one -k storybook && just-one -k vite && just-one -k api"
101
- }
102
- }
103
- ```
104
-
105
- ## How It Works
106
-
107
- ```
108
- .just-one/
109
- storybook.pid # Contains: 12345
110
- vite.pid # Contains: 67890
111
- ```
112
-
113
- 1. Check if a PID file exists for that name
114
- 2. If yes, verify it's the same process we started (by comparing start times)
115
- 3. If verified, kill that specific process (and its children)
116
- 4. Start the new process
117
- 5. Save its PID for next time
118
-
119
- ### PID Reuse Protection
120
-
121
- Operating systems can reuse PIDs after a process terminates. To prevent accidentally killing an unrelated process that received the same PID, `just-one` compares:
122
- - The PID file's modification time (when we recorded the PID)
123
- - The process's actual start time (from the OS)
124
-
125
- If these don't match within 5 seconds, the PID file is considered stale and the process is not killed.
126
-
127
- ### Cross-Platform Process Handling
128
-
129
- | Platform | Kill Method |
130
- |----------|-------------|
131
- | Windows | `taskkill /PID <pid> /T /F` (kills process tree) |
132
- | Unix/Mac | `kill -SIGTERM -<pid>` (process group) |
133
-
134
- ## Use Cases
135
-
136
- - **Dev servers** - Storybook, Vite, webpack-dev-server, Next.js
137
- - **Background processes** - API servers, database seeders, watchers
138
- - **CI/CD** - Ensure clean state before running tests
139
- - **Multiple instances** - Run named instances on different ports
140
-
141
- ```bash
142
- # Run two storybooks on different ports
143
- just-one -n storybook-main -- storybook dev -p 6006
144
- just-one -n storybook-docs -- storybook dev -p 6007
145
- ```
146
-
147
- ## Comparison
148
-
149
- | Feature | just-one | kill-port | pm2 |
150
- |---------|----------|-----------|-----|
151
- | Kills by PID (precise) | Yes | No (by port) | Yes |
152
- | PID reuse protection | Yes | No | No |
153
- | Cross-platform | Yes | Yes | Yes |
154
- | Zero config | Yes | Yes | No |
155
- | Remembers processes | Yes (PID file) | No | Yes (daemon) |
156
- | Lightweight | Yes (1 dep) | Yes | Heavy |
157
- | Daemon required | No | No | Yes |
158
-
159
- ## Requirements
160
-
161
- - Node.js >= 18.0.0
162
-
163
- ## Development
164
-
165
- ```bash
166
- # Install dependencies
167
- npm install
168
-
169
- # Build
170
- npm run build
171
-
172
- # Run tests
173
- npm test
174
-
175
- # Lint + typecheck + test
176
- npm run validate
177
- ```
178
-
179
- ## License
180
-
181
- MIT
1
+ # just-one
2
+
3
+ A CLI tool that ensures only one instance of a command runs at a time. Kills the previous instance before starting a new one.
4
+
5
+ ## Why This Exists
6
+
7
+ When developing with dev servers (Storybook, Vite, webpack-dev-server, etc.), you often get:
8
+
9
+ ```
10
+ Error: Port 6006 is already in use
11
+ ```
12
+
13
+ Existing solutions have drawbacks:
14
+
15
+ - **kill-port** - Kills ANY process on that port (imprecise, might kill unrelated processes)
16
+ - **Manual** - Find PID, kill it, restart (tedious)
17
+ - **pm2** - Overkill for dev servers
18
+
19
+ `just-one` tracks processes by name using PID files. When you run a command, it kills the previous instance (if any) and starts fresh—precisely targeting only the process it started.
20
+
21
+ ## Features
22
+
23
+ - **Named process tracking** - Each process gets a unique name for precise targeting
24
+ - **Automatic cleanup** - Previous instance killed before starting new one
25
+ - **Cross-platform** - Works on Windows, macOS, and Linux
26
+ - **Minimal dependencies** - Only [pidusage](https://github.com/soyuka/pidusage) for process verification
27
+ - **PID file management** - Survives terminal closes and system restarts
28
+ - **PID reuse protection** - Verifies process identity before killing to prevent accidents
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install -g @radleta/just-one
34
+ ```
35
+
36
+ Or use with npx (no install required):
37
+
38
+ ```bash
39
+ npx @radleta/just-one -n myapp -- npm run dev
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ### Basic usage
45
+
46
+ ```bash
47
+ # Run storybook, killing any previous instance named "storybook"
48
+ just-one -n storybook -- npx storybook dev -p 6006
49
+
50
+ # Run vite dev server
51
+ just-one -n vite -- npm run dev
52
+
53
+ # Run any command
54
+ just-one -n myapp -- node server.js
55
+ ```
56
+
57
+ ### Ensure a process is running (idempotent)
58
+
59
+ ```bash
60
+ # Only starts if not already running — safe to call repeatedly
61
+ just-one -n vite -e -- npm run dev
62
+ ```
63
+
64
+ ### Check if a process is running
65
+
66
+ ```bash
67
+ just-one -s storybook # exit 0 if running, exit 1 if stopped
68
+ just-one --status myapp
69
+ ```
70
+
71
+ ### Get the PID for scripting
72
+
73
+ ```bash
74
+ pid=$(just-one -p myapp -q) # prints just the PID number
75
+ ```
76
+
77
+ ### Kill a named process
78
+
79
+ ```bash
80
+ just-one -k storybook
81
+ just-one --kill myapp
82
+ ```
83
+
84
+ ### Kill all tracked processes
85
+
86
+ ```bash
87
+ just-one -K
88
+ just-one --kill-all
89
+ ```
90
+
91
+ ### Wait for a process to exit
92
+
93
+ ```bash
94
+ just-one -w myapp # wait indefinitely
95
+ just-one -w myapp -t 30 # wait up to 30 seconds
96
+ ```
97
+
98
+ ### List tracked processes
99
+
100
+ ```bash
101
+ just-one -l
102
+ just-one --list
103
+ ```
104
+
105
+ ### Clean up stale PID files
106
+
107
+ ```bash
108
+ just-one --clean # removes PID files for processes that are no longer running
109
+ ```
110
+
111
+ ### Specify custom PID directory
112
+
113
+ ```bash
114
+ # Default: ./.just-one/<name>.pid
115
+ just-one -n storybook -- npx storybook dev
116
+
117
+ # Custom directory
118
+ just-one -n storybook -d /tmp -- npx storybook dev
119
+ ```
120
+
121
+ ## CLI Options
122
+
123
+ | Option | Alias | Description |
124
+ | ------------------ | ----- | ------------------------------------------------- |
125
+ | `--name <name>` | `-n` | Required for run. Name to identify this process |
126
+ | `--kill <name>` | `-k` | Kill the named process and exit |
127
+ | `--kill-all` | `-K` | Kill all tracked processes |
128
+ | `--status <name>` | `-s` | Check if a named process is running (exit 0/1) |
129
+ | `--ensure` | `-e` | Only start if not already running (use with `-n`) |
130
+ | `--pid <name>` | `-p` | Print the PID of a named process |
131
+ | `--wait <name>` | `-w` | Wait for a named process to exit |
132
+ | `--timeout <secs>` | `-t` | Timeout in seconds (use with `--wait`) |
133
+ | `--clean` | | Remove stale PID files |
134
+ | `--list` | `-l` | List all tracked processes and their status |
135
+ | `--pid-dir <dir>` | `-d` | Directory for PID files (default: `.just-one/`) |
136
+ | `--quiet` | `-q` | Suppress output |
137
+ | `--help` | `-h` | Show help |
138
+ | `--version` | `-v` | Show version |
139
+
140
+ ## package.json Scripts
141
+
142
+ ```json
143
+ {
144
+ "scripts": {
145
+ "storybook": "just-one -n storybook -- storybook dev -p 6006",
146
+ "dev": "just-one -n vite -e -- vite",
147
+ "dev:api": "just-one -n api -e -- node server.js",
148
+ "stop": "just-one -K"
149
+ }
150
+ }
151
+ ```
152
+
153
+ ## How It Works
154
+
155
+ ```
156
+ .just-one/
157
+ storybook.pid # Contains: 12345
158
+ vite.pid # Contains: 67890
159
+ ```
160
+
161
+ 1. Check if a PID file exists for that name
162
+ 2. If yes, verify it's the same process we started (by comparing start times)
163
+ 3. If verified, kill that specific process (and its children)
164
+ 4. Start the new process
165
+ 5. Save its PID for next time
166
+
167
+ ### PID Reuse Protection
168
+
169
+ Operating systems can reuse PIDs after a process terminates. To prevent accidentally killing an unrelated process that received the same PID, `just-one` compares:
170
+
171
+ - The PID file's modification time (when we recorded the PID)
172
+ - The process's actual start time (from the OS)
173
+
174
+ If these don't match within 5 seconds, the PID file is considered stale and the process is not killed.
175
+
176
+ ### Cross-Platform Process Handling
177
+
178
+ | Platform | Kill Method | Signal Handling |
179
+ | -------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
180
+ | Windows | `taskkill /PID <pid> /T /F` (kills process tree) | On Ctrl+C, relies on OS-delivered `CTRL_C_EVENT` for graceful shutdown with a force-kill safety net |
181
+ | Unix/Mac | `kill -SIGTERM -<pid>` (process group) | Forwards `SIGTERM` to child process |
182
+
183
+ **Windows graceful shutdown**: When the child shares the console (`stdio: 'inherit'`), Windows delivers `CTRL_C_EVENT` to all processes in the console group. `just-one` avoids calling `process.kill()` on the child (which uses `TerminateProcess` on Windows) to give the child time to run cleanup handlers. If the child doesn't exit within 2 seconds, it is force-killed as a safety net.
184
+
185
+ ## Use Cases
186
+
187
+ - **Dev servers** - Storybook, Vite, webpack-dev-server, Next.js
188
+ - **Background processes** - API servers, database seeders, watchers
189
+ - **CI/CD** - Ensure clean state before running tests
190
+ - **Multiple instances** - Run named instances on different ports
191
+
192
+ ```bash
193
+ # Run two storybooks on different ports
194
+ just-one -n storybook-main -- storybook dev -p 6006
195
+ just-one -n storybook-docs -- storybook dev -p 6007
196
+ ```
197
+
198
+ ## Comparison
199
+
200
+ | Feature | just-one | kill-port | pm2 |
201
+ | ---------------------- | -------------- | ------------ | ------------ |
202
+ | Kills by PID (precise) | Yes | No (by port) | Yes |
203
+ | PID reuse protection | Yes | No | No |
204
+ | Status check | Yes | No | Yes |
205
+ | Cross-platform | Yes | Yes | Yes |
206
+ | Zero config | Yes | Yes | No |
207
+ | Remembers processes | Yes (PID file) | No | Yes (daemon) |
208
+ | Lightweight | Yes (1 dep) | Yes | Heavy |
209
+ | Daemon required | No | No | Yes |
210
+
211
+ ## Requirements
212
+
213
+ - Node.js >= 18.0.0
214
+
215
+ ## Development
216
+
217
+ ```bash
218
+ # Install dependencies
219
+ npm install
220
+
221
+ # Build
222
+ npm run build
223
+
224
+ # Run tests
225
+ npm test
226
+
227
+ # Lint + typecheck + test
228
+ npm run validate
229
+ ```
230
+
231
+ ## License
232
+
233
+ MIT
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/index.ts
4
+ import { createRequire } from "module";
5
+
3
6
  // src/lib/cli.ts
4
7
  var DEFAULT_PID_DIR = ".just-one";
5
8
  var MAX_NAME_LENGTH = 255;
@@ -29,6 +32,13 @@ function parseArgs(args) {
29
32
  name: void 0,
30
33
  kill: void 0,
31
34
  list: false,
35
+ status: void 0,
36
+ killAll: false,
37
+ ensure: false,
38
+ clean: false,
39
+ pid: void 0,
40
+ wait: void 0,
41
+ timeout: void 0,
32
42
  pidDir: DEFAULT_PID_DIR,
33
43
  quiet: false,
34
44
  help: false,
@@ -68,7 +78,10 @@ function parseArgs(args) {
68
78
  return { success: false, error: "Option --name requires a value" };
69
79
  }
70
80
  if (!isValidName(value)) {
71
- return { success: false, error: "Invalid name: must not contain path separators or be too long" };
81
+ return {
82
+ success: false,
83
+ error: "Invalid name: must not contain path separators or be too long"
84
+ };
72
85
  }
73
86
  options.name = value;
74
87
  i += 2;
@@ -80,7 +93,10 @@ function parseArgs(args) {
80
93
  return { success: false, error: "Option --kill requires a value" };
81
94
  }
82
95
  if (!isValidName(value)) {
83
- return { success: false, error: "Invalid name: must not contain path separators or be too long" };
96
+ return {
97
+ success: false,
98
+ error: "Invalid name: must not contain path separators or be too long"
99
+ };
84
100
  }
85
101
  options.kill = value;
86
102
  i += 2;
@@ -92,12 +108,88 @@ function parseArgs(args) {
92
108
  return { success: false, error: "Option --pid-dir requires a value" };
93
109
  }
94
110
  if (!isValidPidDir(value)) {
95
- return { success: false, error: "Invalid PID directory: must not contain path traversal sequences" };
111
+ return {
112
+ success: false,
113
+ error: "Invalid PID directory: must not contain path traversal sequences"
114
+ };
96
115
  }
97
116
  options.pidDir = value;
98
117
  i += 2;
99
118
  continue;
100
119
  }
120
+ if (arg === "--status" || arg === "-s") {
121
+ const value = args[i + 1];
122
+ if (!value || value.startsWith("-")) {
123
+ return { success: false, error: "Option --status requires a value" };
124
+ }
125
+ if (!isValidName(value)) {
126
+ return {
127
+ success: false,
128
+ error: "Invalid name: must not contain path separators or be too long"
129
+ };
130
+ }
131
+ options.status = value;
132
+ i += 2;
133
+ continue;
134
+ }
135
+ if (arg === "--kill-all" || arg === "-K") {
136
+ options.killAll = true;
137
+ i++;
138
+ continue;
139
+ }
140
+ if (arg === "--ensure" || arg === "-e") {
141
+ options.ensure = true;
142
+ i++;
143
+ continue;
144
+ }
145
+ if (arg === "--clean") {
146
+ options.clean = true;
147
+ i++;
148
+ continue;
149
+ }
150
+ if (arg === "--pid" || arg === "-p") {
151
+ const value = args[i + 1];
152
+ if (!value || value.startsWith("-")) {
153
+ return { success: false, error: "Option --pid requires a value" };
154
+ }
155
+ if (!isValidName(value)) {
156
+ return {
157
+ success: false,
158
+ error: "Invalid name: must not contain path separators or be too long"
159
+ };
160
+ }
161
+ options.pid = value;
162
+ i += 2;
163
+ continue;
164
+ }
165
+ if (arg === "--wait" || arg === "-w") {
166
+ const value = args[i + 1];
167
+ if (!value || value.startsWith("-")) {
168
+ return { success: false, error: "Option --wait requires a value" };
169
+ }
170
+ if (!isValidName(value)) {
171
+ return {
172
+ success: false,
173
+ error: "Invalid name: must not contain path separators or be too long"
174
+ };
175
+ }
176
+ options.wait = value;
177
+ i += 2;
178
+ continue;
179
+ }
180
+ if (arg === "--timeout" || arg === "-t") {
181
+ const value = args[i + 1];
182
+ if (!value || value.startsWith("-")) {
183
+ return { success: false, error: "Option --timeout requires a positive number" };
184
+ }
185
+ const num = Number(value);
186
+ if (isNaN(num) || num <= 0) {
187
+ return { success: false, error: "Option --timeout requires a positive number" };
188
+ }
189
+ options.timeout = num;
190
+ i += 2;
191
+ continue;
192
+ }
101
193
  if (arg.startsWith("-")) {
102
194
  return { success: false, error: `Unknown option: ${arg}` };
103
195
  }
@@ -115,6 +207,27 @@ function validateOptions(options) {
115
207
  if (options.kill) {
116
208
  return { success: true, options };
117
209
  }
210
+ if (options.status) {
211
+ return { success: true, options };
212
+ }
213
+ if (options.killAll) {
214
+ return { success: true, options };
215
+ }
216
+ if (options.clean) {
217
+ return { success: true, options };
218
+ }
219
+ if (options.pid) {
220
+ return { success: true, options };
221
+ }
222
+ if (options.wait) {
223
+ if (options.timeout !== void 0 && options.timeout <= 0) {
224
+ return { success: false, error: "Option --timeout requires a positive number" };
225
+ }
226
+ return { success: true, options };
227
+ }
228
+ if (options.timeout !== void 0 && !options.wait) {
229
+ return { success: false, error: "Option --timeout can only be used with --wait" };
230
+ }
118
231
  if (!options.name) {
119
232
  return { success: false, error: "Option --name is required when running a command" };
120
233
  }
@@ -128,24 +241,52 @@ function getHelpText() {
128
241
 
129
242
  Usage:
130
243
  just-one -n <name> -- <command> Run command, killing any previous instance
244
+ just-one -n <name> -e -- <command> Run only if not already running (ensure mode)
131
245
  just-one -k <name> Kill a named process
246
+ just-one -K Kill all tracked processes
247
+ just-one -s <name> Check if a named process is running
248
+ just-one -p <name> Print the PID of a named process
249
+ just-one -w <name> Wait for a named process to exit
132
250
  just-one -l List all tracked processes
251
+ just-one --clean Remove stale PID files
133
252
 
134
253
  Options:
135
- -n, --name <name> Name to identify this process (required for running)
136
- -k, --kill <name> Kill the named process and exit
137
- -l, --list List all tracked processes and their status
138
- -d, --pid-dir <dir> Directory for PID files (default: .just-one/)
139
- -q, --quiet Suppress output
140
- -h, --help Show this help message
141
- -v, --version Show version number
254
+ -n, --name <name> Name to identify this process (required for running)
255
+ -k, --kill <name> Kill the named process and exit
256
+ -K, --kill-all Kill all tracked processes
257
+ -s, --status <name> Check if a named process is running (exit 0=running, 1=stopped)
258
+ -e, --ensure Only start if not already running (use with -n and command)
259
+ -p, --pid <name> Print the PID of a named process
260
+ -w, --wait <name> Wait for a named process to exit
261
+ -t, --timeout <secs> Timeout in seconds (use with --wait)
262
+ --clean Remove stale PID files
263
+ -l, --list List all tracked processes and their status
264
+ -d, --pid-dir <dir> Directory for PID files (default: .just-one/)
265
+ -q, --quiet Suppress output
266
+ -h, --help Show this help message
267
+ -v, --version Show version number
142
268
 
143
269
  Examples:
144
270
  # Run storybook, killing any previous instance
145
271
  just-one -n storybook -- npx storybook dev -p 6006
146
272
 
147
- # Run vite dev server
148
- just-one -n vite -- npm run dev
273
+ # Run vite dev server only if not already running
274
+ just-one -n vite -e -- npm run dev
275
+
276
+ # Check if a process is running
277
+ just-one -s storybook
278
+
279
+ # Get the PID for scripting
280
+ pid=$(just-one -p storybook -q)
281
+
282
+ # Kill all tracked processes
283
+ just-one -K
284
+
285
+ # Wait for a process to exit (with 30s timeout)
286
+ just-one -w myapp -t 30
287
+
288
+ # Clean up stale PID files
289
+ just-one --clean
149
290
 
150
291
  # Kill a named process
151
292
  just-one -k storybook
@@ -325,15 +466,25 @@ function spawnCommand(command, args) {
325
466
  pid: child.pid
326
467
  };
327
468
  }
469
+ var WINDOWS_GRACEFUL_TIMEOUT_MS = 2e3;
328
470
  function setupSignalHandlers(child, onExit) {
471
+ let forceKillTimer = null;
472
+ const forceKillWindows = () => {
473
+ if (child.pid && isValidPid(child.pid) && isProcessAlive(child.pid)) {
474
+ try {
475
+ execSync(`taskkill /PID ${child.pid} /T /F`, {
476
+ stdio: ["pipe", "pipe", "pipe"]
477
+ });
478
+ } catch {
479
+ }
480
+ }
481
+ };
329
482
  const handleSignal = (_signal) => {
330
483
  if (child.pid && isValidPid(child.pid)) {
331
484
  if (isWindows) {
332
- try {
333
- execSync(`taskkill /PID ${child.pid} /T /F`, {
334
- stdio: ["pipe", "pipe", "pipe"]
335
- });
336
- } catch {
485
+ if (forceKillTimer === null) {
486
+ forceKillTimer = setTimeout(forceKillWindows, WINDOWS_GRACEFUL_TIMEOUT_MS);
487
+ forceKillTimer.unref();
337
488
  }
338
489
  } else {
339
490
  child.kill("SIGTERM");
@@ -343,6 +494,10 @@ function setupSignalHandlers(child, onExit) {
343
494
  process.on("SIGINT", () => handleSignal("SIGINT"));
344
495
  process.on("SIGTERM", () => handleSignal("SIGTERM"));
345
496
  child.on("exit", (code, signal) => {
497
+ if (forceKillTimer !== null) {
498
+ clearTimeout(forceKillTimer);
499
+ forceKillTimer = null;
500
+ }
346
501
  if (onExit) {
347
502
  onExit();
348
503
  }
@@ -358,7 +513,8 @@ function setupSignalHandlers(child, onExit) {
358
513
  }
359
514
 
360
515
  // src/index.ts
361
- var VERSION = "0.1.0";
516
+ var require2 = createRequire(import.meta.url);
517
+ var { version: VERSION } = require2("../package.json");
362
518
  function log(message, options) {
363
519
  if (!options.quiet) {
364
520
  console.log(message);
@@ -422,6 +578,10 @@ async function handleRun(options) {
422
578
  const pidFileMtime = getPidFileMtime(name, options.pidDir);
423
579
  const shouldKill = pidFileMtime !== null && await isSameProcessInstance(existingPid, pidFileMtime);
424
580
  if (shouldKill) {
581
+ if (options.ensure) {
582
+ log(`Process ${name} is already running (PID: ${existingPid}), skipping`, options);
583
+ return 0;
584
+ }
425
585
  log(`Killing existing process ${name} (PID: ${existingPid})...`, options);
426
586
  killProcess(existingPid);
427
587
  await waitForProcessToDie(existingPid);
@@ -446,6 +606,124 @@ async function handleRun(options) {
446
606
  return 1;
447
607
  }
448
608
  }
609
+ async function handleStatus(name, options) {
610
+ const pid = readPid(name, options.pidDir);
611
+ if (pid === null) {
612
+ log(`Process ${name}: not tracked`, options);
613
+ return 1;
614
+ }
615
+ const pidFileMtime = getPidFileMtime(name, options.pidDir);
616
+ const isSameInstance = pidFileMtime !== null && await isSameProcessInstance(pid, pidFileMtime);
617
+ if (isSameInstance) {
618
+ log(`Process ${name}: running (PID ${pid})`, options);
619
+ return 0;
620
+ }
621
+ if (isProcessAlive(pid)) {
622
+ log(`Process ${name}: stopped (PID ${pid} belongs to a different process)`, options);
623
+ } else {
624
+ log(`Process ${name}: stopped`, options);
625
+ }
626
+ return 1;
627
+ }
628
+ async function handleKillAll(options) {
629
+ const pids = listPids(options.pidDir);
630
+ if (pids.length === 0) {
631
+ log("No tracked processes", options);
632
+ return 0;
633
+ }
634
+ let failed = false;
635
+ for (const info of pids) {
636
+ if (!info.exists || info.pid <= 0) {
637
+ deletePid(info.name, options.pidDir);
638
+ continue;
639
+ }
640
+ const pidFileMtime = getPidFileMtime(info.name, options.pidDir);
641
+ const isSameInstance = pidFileMtime !== null && await isSameProcessInstance(info.pid, pidFileMtime);
642
+ if (!isSameInstance) {
643
+ log(`Process ${info.name} (PID: ${info.pid}) is stale, cleaning up`, options);
644
+ deletePid(info.name, options.pidDir);
645
+ continue;
646
+ }
647
+ log(`Killing process ${info.name} (PID: ${info.pid})...`, options);
648
+ const killed = killProcess(info.pid);
649
+ if (killed) {
650
+ await waitForProcessToDie(info.pid);
651
+ deletePid(info.name, options.pidDir);
652
+ log(`Process ${info.name} killed`, options);
653
+ } else {
654
+ logError(`Failed to kill process ${info.name} (PID: ${info.pid})`);
655
+ failed = true;
656
+ }
657
+ }
658
+ return failed ? 1 : 0;
659
+ }
660
+ async function handleClean(options) {
661
+ const pids = listPids(options.pidDir);
662
+ if (pids.length === 0) {
663
+ log("No PID files to clean", options);
664
+ return 0;
665
+ }
666
+ let cleaned = 0;
667
+ for (const info of pids) {
668
+ if (!info.exists || info.pid <= 0) {
669
+ deletePid(info.name, options.pidDir);
670
+ cleaned++;
671
+ continue;
672
+ }
673
+ const pidFileMtime = getPidFileMtime(info.name, options.pidDir);
674
+ const isSameInstance = pidFileMtime !== null && await isSameProcessInstance(info.pid, pidFileMtime);
675
+ if (!isSameInstance) {
676
+ log(`Removing stale PID file: ${info.name} (PID: ${info.pid})`, options);
677
+ deletePid(info.name, options.pidDir);
678
+ cleaned++;
679
+ }
680
+ }
681
+ if (cleaned === 0) {
682
+ log("No stale PID files found", options);
683
+ } else {
684
+ log(`Cleaned ${cleaned} stale PID file${cleaned === 1 ? "" : "s"}`, options);
685
+ }
686
+ return 0;
687
+ }
688
+ async function handlePid(name, options) {
689
+ const pid = readPid(name, options.pidDir);
690
+ if (pid === null) {
691
+ log(`No process found with name: ${name}`, options);
692
+ return 1;
693
+ }
694
+ const pidFileMtime = getPidFileMtime(name, options.pidDir);
695
+ const isSameInstance = pidFileMtime !== null && await isSameProcessInstance(pid, pidFileMtime);
696
+ if (isSameInstance) {
697
+ log(String(pid), options);
698
+ return 0;
699
+ }
700
+ log(`Process ${name} is not running`, options);
701
+ return 1;
702
+ }
703
+ async function handleWait(name, options) {
704
+ const pid = readPid(name, options.pidDir);
705
+ if (pid === null) {
706
+ log(`No process found with name: ${name}`, options);
707
+ return 1;
708
+ }
709
+ if (!isProcessAlive(pid)) {
710
+ log(`Process ${name} (PID: ${pid}) is not running`, options);
711
+ return 1;
712
+ }
713
+ log(`Waiting for process ${name} (PID: ${pid}) to exit...`, options);
714
+ const timeoutMs = options.timeout !== void 0 ? options.timeout * 1e3 : void 0;
715
+ const startTime = Date.now();
716
+ const pollInterval = 500;
717
+ while (isProcessAlive(pid)) {
718
+ if (timeoutMs !== void 0 && Date.now() - startTime >= timeoutMs) {
719
+ log(`Timeout waiting for process ${name} (PID: ${pid})`, options);
720
+ return 1;
721
+ }
722
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
723
+ }
724
+ log(`Process ${name} (PID: ${pid}) has exited`, options);
725
+ return 0;
726
+ }
449
727
  async function main() {
450
728
  const args = process.argv.slice(2);
451
729
  const parseResult = parseArgs(args);
@@ -475,6 +753,21 @@ async function main() {
475
753
  if (options.kill) {
476
754
  return await handleKill(options.kill, options);
477
755
  }
756
+ if (options.killAll) {
757
+ return await handleKillAll(options);
758
+ }
759
+ if (options.status) {
760
+ return await handleStatus(options.status, options);
761
+ }
762
+ if (options.clean) {
763
+ return await handleClean(options);
764
+ }
765
+ if (options.pid) {
766
+ return await handlePid(options.pid, options);
767
+ }
768
+ if (options.wait) {
769
+ return await handleWait(options.wait, options);
770
+ }
478
771
  return await handleRun(options);
479
772
  }
480
773
  main().then((code) => {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/cli.ts","../src/lib/pid.ts","../src/lib/process.ts","../src/index.ts"],"sourcesContent":["/**\r\n * CLI argument parsing for just-one\r\n */\r\n\r\nexport interface CliOptions {\r\n name?: string;\r\n kill?: string;\r\n list: boolean;\r\n pidDir: string;\r\n quiet: boolean;\r\n help: boolean;\r\n version: boolean;\r\n command: string[];\r\n}\r\n\r\nexport interface ParseResult {\r\n success: true;\r\n options: CliOptions;\r\n}\r\n\r\nexport interface ParseError {\r\n success: false;\r\n error: string;\r\n}\r\n\r\nexport type ParseOutput = ParseResult | ParseError;\r\n\r\nconst DEFAULT_PID_DIR = '.just-one';\r\nconst MAX_NAME_LENGTH = 255;\r\n\r\n/**\r\n * Validate a process name for safe file operations\r\n * Rejects names containing path separators or traversal sequences\r\n */\r\nfunction isValidName(name: string): boolean {\r\n if (!name || name.length > MAX_NAME_LENGTH) {\r\n return false;\r\n }\r\n // Reject path separators and traversal sequences\r\n if (name.includes('/') || name.includes('\\\\') || name.includes('..')) {\r\n return false;\r\n }\r\n // Reject names that are only dots or whitespace\r\n if (/^[\\s.]*$/.test(name)) {\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * Validate a PID directory path for safe file operations\r\n * Rejects paths containing traversal sequences\r\n */\r\nfunction isValidPidDir(dir: string): boolean {\r\n if (!dir || dir.length > 1024) {\r\n return false;\r\n }\r\n // Reject path traversal sequences\r\n if (dir.includes('..')) {\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * Parse command line arguments\r\n */\r\nexport function parseArgs(args: string[]): ParseOutput {\r\n const options: CliOptions = {\r\n name: undefined,\r\n kill: undefined,\r\n list: false,\r\n pidDir: DEFAULT_PID_DIR,\r\n quiet: false,\r\n help: false,\r\n version: false,\r\n command: [],\r\n };\r\n\r\n let i = 0;\r\n while (i < args.length) {\r\n // TypeScript requires this check due to noUncheckedIndexedAccess\r\n const arg = args[i]!;\r\n\r\n // Everything after -- is the command\r\n if (arg === '--') {\r\n options.command = args.slice(i + 1);\r\n break;\r\n }\r\n\r\n // Help\r\n if (arg === '--help' || arg === '-h') {\r\n options.help = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // Version\r\n if (arg === '--version' || arg === '-v') {\r\n options.version = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // List\r\n if (arg === '--list' || arg === '-l') {\r\n options.list = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // Quiet\r\n if (arg === '--quiet' || arg === '-q') {\r\n options.quiet = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // Name (requires value)\r\n if (arg === '--name' || arg === '-n') {\r\n const value = args[i + 1];\r\n if (!value || value.startsWith('-')) {\r\n return { success: false, error: 'Option --name requires a value' };\r\n }\r\n if (!isValidName(value)) {\r\n return { success: false, error: 'Invalid name: must not contain path separators or be too long' };\r\n }\r\n options.name = value;\r\n i += 2;\r\n continue;\r\n }\r\n\r\n // Kill (requires value)\r\n if (arg === '--kill' || arg === '-k') {\r\n const value = args[i + 1];\r\n if (!value || value.startsWith('-')) {\r\n return { success: false, error: 'Option --kill requires a value' };\r\n }\r\n if (!isValidName(value)) {\r\n return { success: false, error: 'Invalid name: must not contain path separators or be too long' };\r\n }\r\n options.kill = value;\r\n i += 2;\r\n continue;\r\n }\r\n\r\n // PID directory (requires value)\r\n if (arg === '--pid-dir' || arg === '-d') {\r\n const value = args[i + 1];\r\n if (!value || value.startsWith('-')) {\r\n return { success: false, error: 'Option --pid-dir requires a value' };\r\n }\r\n if (!isValidPidDir(value)) {\r\n return { success: false, error: 'Invalid PID directory: must not contain path traversal sequences' };\r\n }\r\n options.pidDir = value;\r\n i += 2;\r\n continue;\r\n }\r\n\r\n // Unknown option\r\n if (arg.startsWith('-')) {\r\n return { success: false, error: `Unknown option: ${arg}` };\r\n }\r\n\r\n // Unexpected positional argument\r\n return { success: false, error: `Unexpected argument: ${arg}` };\r\n }\r\n\r\n return { success: true, options };\r\n}\r\n\r\n/**\r\n * Validate parsed options\r\n */\r\nexport function validateOptions(options: CliOptions): ParseOutput {\r\n // Help and version don't need validation\r\n if (options.help || options.version) {\r\n return { success: true, options };\r\n }\r\n\r\n // List doesn't need name or command\r\n if (options.list) {\r\n return { success: true, options };\r\n }\r\n\r\n // Kill only needs a name\r\n if (options.kill) {\r\n return { success: true, options };\r\n }\r\n\r\n // Running a command requires both name and command\r\n if (!options.name) {\r\n return { success: false, error: 'Option --name is required when running a command' };\r\n }\r\n\r\n if (options.command.length === 0) {\r\n return { success: false, error: 'No command specified. Use: just-one -n <name> -- <command>' };\r\n }\r\n\r\n return { success: true, options };\r\n}\r\n\r\n/**\r\n * Get help text\r\n */\r\nexport function getHelpText(): string {\r\n return `just-one - Ensure only one instance of a command runs at a time\r\n\r\nUsage:\r\n just-one -n <name> -- <command> Run command, killing any previous instance\r\n just-one -k <name> Kill a named process\r\n just-one -l List all tracked processes\r\n\r\nOptions:\r\n -n, --name <name> Name to identify this process (required for running)\r\n -k, --kill <name> Kill the named process and exit\r\n -l, --list List all tracked processes and their status\r\n -d, --pid-dir <dir> Directory for PID files (default: .just-one/)\r\n -q, --quiet Suppress output\r\n -h, --help Show this help message\r\n -v, --version Show version number\r\n\r\nExamples:\r\n # Run storybook, killing any previous instance\r\n just-one -n storybook -- npx storybook dev -p 6006\r\n\r\n # Run vite dev server\r\n just-one -n vite -- npm run dev\r\n\r\n # Kill a named process\r\n just-one -k storybook\r\n\r\n # List all tracked processes\r\n just-one -l\r\n`;\r\n}\r\n","/**\r\n * PID file operations for just-one\r\n */\r\n\r\nimport { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';\r\nimport { join, dirname } from 'path';\r\n\r\nexport interface PidInfo {\r\n name: string;\r\n pid: number;\r\n exists: boolean;\r\n}\r\n\r\n/**\r\n * Get the path to a PID file for a given name\r\n */\r\nexport function getPidFilePath(name: string, pidDir: string): string {\r\n return join(pidDir, `${name}.pid`);\r\n}\r\n\r\n/**\r\n * Read the PID from a PID file\r\n * Returns null if the file doesn't exist or is invalid\r\n */\r\nexport function readPid(name: string, pidDir: string): number | null {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n\r\n if (!existsSync(pidFile)) {\r\n return null;\r\n }\r\n\r\n try {\r\n const content = readFileSync(pidFile, 'utf8').trim();\r\n const pid = parseInt(content, 10);\r\n\r\n if (isNaN(pid) || pid <= 0) {\r\n return null;\r\n }\r\n\r\n return pid;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Write a PID to a PID file\r\n * Creates the directory if it doesn't exist\r\n */\r\nexport function writePid(name: string, pid: number, pidDir: string): void {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n const dir = dirname(pidFile);\r\n\r\n if (!existsSync(dir)) {\r\n mkdirSync(dir, { recursive: true });\r\n }\r\n\r\n writeFileSync(pidFile, String(pid), 'utf8');\r\n}\r\n\r\n/**\r\n * Delete a PID file\r\n * Returns true if the file was deleted, false if it didn't exist\r\n */\r\nexport function deletePid(name: string, pidDir: string): boolean {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n\r\n if (!existsSync(pidFile)) {\r\n return false;\r\n }\r\n\r\n try {\r\n unlinkSync(pidFile);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Get the modification time of a PID file as Unix timestamp (milliseconds)\r\n * Returns null if file doesn't exist\r\n */\r\nexport function getPidFileMtime(name: string, pidDir: string): number | null {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n try {\r\n const stats = statSync(pidFile);\r\n return stats.mtimeMs;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * List all PID files in the directory\r\n * Returns information about each tracked process\r\n */\r\nexport function listPids(pidDir: string): PidInfo[] {\r\n if (!existsSync(pidDir)) {\r\n return [];\r\n }\r\n\r\n const files = readdirSync(pidDir);\r\n const pidFiles = files.filter(f => f.endsWith('.pid'));\r\n\r\n return pidFiles.map(file => {\r\n // Remove .pid suffix (use slice to only remove from end)\r\n const name = file.slice(0, -4);\r\n const pid = readPid(name, pidDir);\r\n\r\n return {\r\n name,\r\n pid: pid ?? 0,\r\n exists: pid !== null,\r\n };\r\n });\r\n}\r\n","/**\r\n * Cross-platform process handling for just-one\r\n */\r\n\r\nimport { spawn, execSync, ChildProcess } from 'child_process';\r\nimport pidusage from 'pidusage';\r\n\r\nconst isWindows = process.platform === 'win32';\r\n\r\n// Constants for process polling\r\nconst DEFAULT_WAIT_TIMEOUT_MS = 2000;\r\nconst CHECK_INTERVAL_MS = 100;\r\n\r\n/**\r\n * Validate that a PID is a safe positive integer for use in system calls\r\n */\r\nexport function isValidPid(pid: number): boolean {\r\n return Number.isInteger(pid) && pid > 0 && pid <= 4194304; // Max PID on most systems\r\n}\r\n\r\n// Tolerance for comparing PID file mtime with process start time\r\nconst START_TIME_TOLERANCE_MS = 5000; // 5 seconds\r\n\r\n/**\r\n * Get the start time of a process as Unix timestamp (milliseconds)\r\n * Returns null if process doesn't exist or start time can't be determined\r\n */\r\nexport async function getProcessStartTime(pid: number): Promise<number | null> {\r\n if (!isValidPid(pid)) {\r\n return null;\r\n }\r\n\r\n try {\r\n const stats = await pidusage(pid);\r\n // Calculate start time from current timestamp minus elapsed time\r\n return stats.timestamp - stats.elapsed;\r\n } catch {\r\n return null; // Process doesn't exist or can't get stats\r\n }\r\n}\r\n\r\n/**\r\n * Check if a running process is the same instance we originally spawned.\r\n * Compares process start time with PID file modification time.\r\n *\r\n * Returns true if:\r\n * - Process exists AND start time is within tolerance of pidFileMtime\r\n *\r\n * Returns false if:\r\n * - Process doesn't exist\r\n * - Can't determine process start time\r\n * - Start time doesn't match (likely PID reuse)\r\n */\r\nexport async function isSameProcessInstance(\r\n pid: number,\r\n pidFileMtimeMs: number\r\n): Promise<boolean> {\r\n const processStartTime = await getProcessStartTime(pid);\r\n if (processStartTime === null) {\r\n return false;\r\n }\r\n\r\n const diff = Math.abs(processStartTime - pidFileMtimeMs);\r\n return diff <= START_TIME_TOLERANCE_MS;\r\n}\r\n\r\n/**\r\n * Check if a process with the given PID is still running\r\n */\r\nexport function isProcessAlive(pid: number): boolean {\r\n try {\r\n if (!isValidPid(pid)) {\r\n return false;\r\n }\r\n if (isWindows) {\r\n // Windows: tasklist returns exit code 0 if process found\r\n // PID is validated as a safe integer above before interpolation\r\n const output = execSync(`tasklist /FI \"PID eq ${pid}\" /NH`, {\r\n encoding: 'utf8',\r\n stdio: ['pipe', 'pipe', 'pipe'],\r\n });\r\n return output.includes(String(pid));\r\n } else {\r\n // Unix/Mac: kill -0 checks if process exists without killing it\r\n process.kill(pid, 0);\r\n return true;\r\n }\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Kill a process by PID\r\n * Returns true if the process was killed, false if it wasn't running\r\n */\r\nexport function killProcess(pid: number): boolean {\r\n if (!isValidPid(pid) || !isProcessAlive(pid)) {\r\n return false;\r\n }\r\n\r\n try {\r\n if (isWindows) {\r\n // Windows: taskkill with /T kills the process tree, /F forces\r\n // PID is validated as a safe integer above before interpolation\r\n execSync(`taskkill /PID ${pid} /T /F`, {\r\n stdio: ['pipe', 'pipe', 'pipe'],\r\n });\r\n } else {\r\n // Unix: try to kill process group first (catches child processes),\r\n // fall back to killing just the process if group kill fails\r\n const killed = tryKillUnix(-pid) || tryKillUnix(pid);\r\n if (!killed) {\r\n return false;\r\n }\r\n }\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Helper to attempt Unix kill with error handling\r\n */\r\nfunction tryKillUnix(pid: number): boolean {\r\n try {\r\n process.kill(pid, 'SIGTERM');\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Wait for a process to die, with timeout\r\n * @param pid - Process ID to wait for\r\n * @param timeoutMs - Maximum time to wait (default: 2000ms)\r\n */\r\nexport async function waitForProcessToDie(\r\n pid: number,\r\n timeoutMs: number = DEFAULT_WAIT_TIMEOUT_MS\r\n): Promise<boolean> {\r\n const startTime = Date.now();\r\n\r\n while (Date.now() - startTime < timeoutMs) {\r\n if (!isProcessAlive(pid)) {\r\n return true;\r\n }\r\n await new Promise(resolve => setTimeout(resolve, CHECK_INTERVAL_MS));\r\n }\r\n\r\n return !isProcessAlive(pid);\r\n}\r\n\r\nexport interface SpawnResult {\r\n child: ChildProcess;\r\n pid: number;\r\n}\r\n\r\n/**\r\n * Spawn a command with stdio forwarding\r\n */\r\nexport function spawnCommand(command: string, args: string[]): SpawnResult {\r\n // On Windows, pass entire command as a single string to avoid escaping issues\r\n // with shell: true (DEP0190 warning and argument handling)\r\n const spawnCmd = isWindows ? `${command} ${args.join(' ')}` : command;\r\n const spawnArgs = isWindows ? [] : args;\r\n\r\n const child = spawn(spawnCmd, spawnArgs, {\r\n stdio: 'inherit',\r\n shell: isWindows,\r\n detached: !isWindows,\r\n });\r\n\r\n if (child.pid === undefined) {\r\n throw new Error('Failed to spawn process');\r\n }\r\n\r\n return {\r\n child,\r\n pid: child.pid,\r\n };\r\n}\r\n\r\n/**\r\n * Set up signal handlers to forward signals to child process\r\n * Note: Both SIGINT and SIGTERM are forwarded as SIGTERM to ensure\r\n * consistent graceful shutdown behavior across different termination methods.\r\n */\r\nexport function setupSignalHandlers(child: ChildProcess, onExit?: () => void): void {\r\n const handleSignal = (_signal: NodeJS.Signals) => {\r\n if (child.pid && isValidPid(child.pid)) {\r\n if (isWindows) {\r\n try {\r\n // PID is validated as a safe integer above before interpolation\r\n execSync(`taskkill /PID ${child.pid} /T /F`, {\r\n stdio: ['pipe', 'pipe', 'pipe'],\r\n });\r\n } catch {\r\n // Process might already be dead\r\n }\r\n } else {\r\n // Forward as SIGTERM for graceful shutdown\r\n child.kill('SIGTERM');\r\n }\r\n }\r\n };\r\n\r\n // Forward both SIGINT (Ctrl+C) and SIGTERM to child as SIGTERM\r\n process.on('SIGINT', () => handleSignal('SIGINT'));\r\n process.on('SIGTERM', () => handleSignal('SIGTERM'));\r\n\r\n child.on('exit', (code, signal) => {\r\n if (onExit) {\r\n onExit();\r\n }\r\n if (signal) {\r\n process.exit(128 + (signal === 'SIGTERM' ? 15 : signal === 'SIGINT' ? 2 : 1));\r\n }\r\n process.exit(code ?? 0);\r\n });\r\n\r\n child.on('error', err => {\r\n console.error(`Failed to start process: ${err.message}`);\r\n process.exit(1);\r\n });\r\n}\r\n","#!/usr/bin/env node\r\n/**\r\n * just-one - Ensure only one instance of a command runs at a time\r\n */\r\n\r\nimport { parseArgs, validateOptions, getHelpText, type CliOptions } from './lib/cli.js';\r\nimport { readPid, writePid, deletePid, listPids, getPidFileMtime } from './lib/pid.js';\r\nimport {\r\n isProcessAlive,\r\n killProcess,\r\n waitForProcessToDie,\r\n spawnCommand,\r\n setupSignalHandlers,\r\n isSameProcessInstance,\r\n} from './lib/process.js';\r\n\r\n// Read version from package.json at build time\r\nconst VERSION = '0.1.0';\r\n\r\nfunction log(message: string, options: CliOptions): void {\r\n if (!options.quiet) {\r\n console.log(message);\r\n }\r\n}\r\n\r\nfunction logError(message: string): void {\r\n console.error(message);\r\n}\r\n\r\nasync function handleKill(name: string, options: CliOptions): Promise<number> {\r\n const pid = readPid(name, options.pidDir);\r\n\r\n if (pid === null) {\r\n log(`No process found with name: ${name}`, options);\r\n return 0;\r\n }\r\n\r\n // Verify this is the same process we originally started (prevents killing\r\n // unrelated processes that reused the same PID)\r\n const pidFileMtime = getPidFileMtime(name, options.pidDir);\r\n const isSameInstance =\r\n pidFileMtime !== null && (await isSameProcessInstance(pid, pidFileMtime));\r\n\r\n if (!isSameInstance) {\r\n if (isProcessAlive(pid)) {\r\n log(`PID ${pid} belongs to a different process, not killing`, options);\r\n } else {\r\n log(`Process ${name} (PID: ${pid}) is not running, cleaning up PID file`, options);\r\n }\r\n deletePid(name, options.pidDir);\r\n return 0;\r\n }\r\n\r\n log(`Killing process ${name} (PID: ${pid})...`, options);\r\n const killed = killProcess(pid);\r\n\r\n if (killed) {\r\n await waitForProcessToDie(pid);\r\n deletePid(name, options.pidDir);\r\n log(`Process ${name} killed`, options);\r\n return 0;\r\n } else {\r\n logError(`Failed to kill process ${name} (PID: ${pid})`);\r\n return 1;\r\n }\r\n}\r\n\r\nfunction handleList(options: CliOptions): number {\r\n const pids = listPids(options.pidDir);\r\n\r\n if (pids.length === 0) {\r\n log('No tracked processes', options);\r\n return 0;\r\n }\r\n\r\n log('Tracked processes:', options);\r\n for (const info of pids) {\r\n const status = info.exists && isProcessAlive(info.pid) ? 'running' : 'stopped';\r\n const pidStr = info.pid > 0 ? String(info.pid) : 'unknown';\r\n log(` ${info.name}: PID ${pidStr} (${status})`, options);\r\n }\r\n\r\n return 0;\r\n}\r\n\r\nasync function handleRun(options: CliOptions): Promise<number> {\r\n const name = options.name!;\r\n const [command, ...args] = options.command;\r\n\r\n if (!command) {\r\n logError('No command specified');\r\n return 1;\r\n }\r\n\r\n // Check for existing process\r\n const existingPid = readPid(name, options.pidDir);\r\n if (existingPid !== null) {\r\n const pidFileMtime = getPidFileMtime(name, options.pidDir);\r\n const shouldKill =\r\n pidFileMtime !== null &&\r\n (await isSameProcessInstance(existingPid, pidFileMtime));\r\n\r\n if (shouldKill) {\r\n log(`Killing existing process ${name} (PID: ${existingPid})...`, options);\r\n killProcess(existingPid);\r\n await waitForProcessToDie(existingPid);\r\n } else if (isProcessAlive(existingPid)) {\r\n // PID exists but doesn't match our process - likely PID reuse\r\n log(\r\n `Stale PID file detected (PID ${existingPid} belongs to different process), skipping kill`,\r\n options\r\n );\r\n }\r\n deletePid(name, options.pidDir);\r\n }\r\n\r\n // Spawn the new process\r\n log(`Starting: ${command} ${args.join(' ')}`, options);\r\n\r\n try {\r\n const { child, pid } = spawnCommand(command, args);\r\n\r\n // Save PID\r\n writePid(name, pid, options.pidDir);\r\n log(`Process started with PID: ${pid}`, options);\r\n\r\n // Set up signal handlers\r\n // Note: We intentionally do NOT delete the PID file on exit.\r\n // If the process exits unexpectedly, the PID file allows the next run\r\n // to find and kill any orphaned processes.\r\n setupSignalHandlers(child);\r\n\r\n // The process will keep running until it exits or is killed\r\n // The exit handler in setupSignalHandlers will call process.exit\r\n return 0;\r\n } catch (err) {\r\n const message = err instanceof Error ? err.message : String(err);\r\n logError(`Failed to start process: ${message}`);\r\n return 1;\r\n }\r\n}\r\n\r\nasync function main(): Promise<number> {\r\n const args = process.argv.slice(2);\r\n\r\n // Parse arguments\r\n const parseResult = parseArgs(args);\r\n if (!parseResult.success) {\r\n logError(`Error: ${parseResult.error}`);\r\n logError('Use --help for usage information');\r\n return 1;\r\n }\r\n\r\n const options = parseResult.options;\r\n\r\n // Validate options\r\n const validateResult = validateOptions(options);\r\n if (!validateResult.success) {\r\n logError(`Error: ${validateResult.error}`);\r\n logError('Use --help for usage information');\r\n return 1;\r\n }\r\n\r\n // Handle help\r\n if (options.help) {\r\n console.log(getHelpText());\r\n return 0;\r\n }\r\n\r\n // Handle version\r\n if (options.version) {\r\n console.log(`just-one v${VERSION}`);\r\n return 0;\r\n }\r\n\r\n // Handle list\r\n if (options.list) {\r\n return handleList(options);\r\n }\r\n\r\n // Handle kill\r\n if (options.kill) {\r\n return await handleKill(options.kill, options);\r\n }\r\n\r\n // Handle run\r\n return await handleRun(options);\r\n}\r\n\r\n// Run the CLI\r\nmain()\r\n .then(code => {\r\n // Only exit if we're not running a child process\r\n // The child process exit handler will call process.exit\r\n if (code !== 0) {\r\n process.exit(code);\r\n }\r\n })\r\n .catch(err => {\r\n console.error('Unexpected error:', err);\r\n process.exit(1);\r\n });\r\n\r\n// Export for testing\r\nexport { main };\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/lib/cli.ts","../src/lib/pid.ts","../src/lib/process.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * just-one - Ensure only one instance of a command runs at a time\n */\n\nimport { createRequire } from 'module';\nimport { parseArgs, validateOptions, getHelpText, type CliOptions } from './lib/cli.js';\nimport { readPid, writePid, deletePid, listPids, getPidFileMtime } from './lib/pid.js';\nimport {\n isProcessAlive,\n killProcess,\n waitForProcessToDie,\n spawnCommand,\n setupSignalHandlers,\n isSameProcessInstance,\n} from './lib/process.js';\n\n// Read version from package.json at runtime\nconst require = createRequire(import.meta.url);\nconst { version: VERSION } = require('../package.json');\n\nfunction log(message: string, options: CliOptions): void {\n if (!options.quiet) {\n console.log(message);\n }\n}\n\nfunction logError(message: string): void {\n console.error(message);\n}\n\nasync function handleKill(name: string, options: CliOptions): Promise<number> {\n const pid = readPid(name, options.pidDir);\n\n if (pid === null) {\n log(`No process found with name: ${name}`, options);\n return 0;\n }\n\n // Verify this is the same process we originally started (prevents killing\n // unrelated processes that reused the same PID)\n const pidFileMtime = getPidFileMtime(name, options.pidDir);\n const isSameInstance = pidFileMtime !== null && (await isSameProcessInstance(pid, pidFileMtime));\n\n if (!isSameInstance) {\n if (isProcessAlive(pid)) {\n log(`PID ${pid} belongs to a different process, not killing`, options);\n } else {\n log(`Process ${name} (PID: ${pid}) is not running, cleaning up PID file`, options);\n }\n deletePid(name, options.pidDir);\n return 0;\n }\n\n log(`Killing process ${name} (PID: ${pid})...`, options);\n const killed = killProcess(pid);\n\n if (killed) {\n await waitForProcessToDie(pid);\n deletePid(name, options.pidDir);\n log(`Process ${name} killed`, options);\n return 0;\n } else {\n logError(`Failed to kill process ${name} (PID: ${pid})`);\n return 1;\n }\n}\n\nfunction handleList(options: CliOptions): number {\n const pids = listPids(options.pidDir);\n\n if (pids.length === 0) {\n log('No tracked processes', options);\n return 0;\n }\n\n log('Tracked processes:', options);\n for (const info of pids) {\n const status = info.exists && isProcessAlive(info.pid) ? 'running' : 'stopped';\n const pidStr = info.pid > 0 ? String(info.pid) : 'unknown';\n log(` ${info.name}: PID ${pidStr} (${status})`, options);\n }\n\n return 0;\n}\n\nasync function handleRun(options: CliOptions): Promise<number> {\n const name = options.name!;\n const [command, ...args] = options.command;\n\n if (!command) {\n logError('No command specified');\n return 1;\n }\n\n // Check for existing process\n const existingPid = readPid(name, options.pidDir);\n if (existingPid !== null) {\n const pidFileMtime = getPidFileMtime(name, options.pidDir);\n const shouldKill =\n pidFileMtime !== null && (await isSameProcessInstance(existingPid, pidFileMtime));\n\n if (shouldKill) {\n // In ensure mode, if the process is verified running, skip restart\n if (options.ensure) {\n log(`Process ${name} is already running (PID: ${existingPid}), skipping`, options);\n return 0;\n }\n log(`Killing existing process ${name} (PID: ${existingPid})...`, options);\n killProcess(existingPid);\n await waitForProcessToDie(existingPid);\n } else if (isProcessAlive(existingPid)) {\n // PID exists but doesn't match our process - likely PID reuse\n log(\n `Stale PID file detected (PID ${existingPid} belongs to different process), skipping kill`,\n options\n );\n }\n deletePid(name, options.pidDir);\n }\n\n // Spawn the new process\n log(`Starting: ${command} ${args.join(' ')}`, options);\n\n try {\n const { child, pid } = spawnCommand(command, args);\n\n // Save PID\n writePid(name, pid, options.pidDir);\n log(`Process started with PID: ${pid}`, options);\n\n // Set up signal handlers\n // Note: We intentionally do NOT delete the PID file on exit.\n // If the process exits unexpectedly, the PID file allows the next run\n // to find and kill any orphaned processes.\n setupSignalHandlers(child);\n\n // The process will keep running until it exits or is killed\n // The exit handler in setupSignalHandlers will call process.exit\n return 0;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logError(`Failed to start process: ${message}`);\n return 1;\n }\n}\n\nasync function handleStatus(name: string, options: CliOptions): Promise<number> {\n const pid = readPid(name, options.pidDir);\n\n if (pid === null) {\n log(`Process ${name}: not tracked`, options);\n return 1;\n }\n\n const pidFileMtime = getPidFileMtime(name, options.pidDir);\n const isSameInstance = pidFileMtime !== null && (await isSameProcessInstance(pid, pidFileMtime));\n\n if (isSameInstance) {\n log(`Process ${name}: running (PID ${pid})`, options);\n return 0;\n }\n\n if (isProcessAlive(pid)) {\n log(`Process ${name}: stopped (PID ${pid} belongs to a different process)`, options);\n } else {\n log(`Process ${name}: stopped`, options);\n }\n return 1;\n}\n\nasync function handleKillAll(options: CliOptions): Promise<number> {\n const pids = listPids(options.pidDir);\n\n if (pids.length === 0) {\n log('No tracked processes', options);\n return 0;\n }\n\n let failed = false;\n for (const info of pids) {\n if (!info.exists || info.pid <= 0) {\n deletePid(info.name, options.pidDir);\n continue;\n }\n\n const pidFileMtime = getPidFileMtime(info.name, options.pidDir);\n const isSameInstance =\n pidFileMtime !== null && (await isSameProcessInstance(info.pid, pidFileMtime));\n\n if (!isSameInstance) {\n log(`Process ${info.name} (PID: ${info.pid}) is stale, cleaning up`, options);\n deletePid(info.name, options.pidDir);\n continue;\n }\n\n log(`Killing process ${info.name} (PID: ${info.pid})...`, options);\n const killed = killProcess(info.pid);\n\n if (killed) {\n await waitForProcessToDie(info.pid);\n deletePid(info.name, options.pidDir);\n log(`Process ${info.name} killed`, options);\n } else {\n logError(`Failed to kill process ${info.name} (PID: ${info.pid})`);\n failed = true;\n }\n }\n\n return failed ? 1 : 0;\n}\n\nasync function handleClean(options: CliOptions): Promise<number> {\n const pids = listPids(options.pidDir);\n\n if (pids.length === 0) {\n log('No PID files to clean', options);\n return 0;\n }\n\n let cleaned = 0;\n for (const info of pids) {\n if (!info.exists || info.pid <= 0) {\n deletePid(info.name, options.pidDir);\n cleaned++;\n continue;\n }\n\n const pidFileMtime = getPidFileMtime(info.name, options.pidDir);\n const isSameInstance =\n pidFileMtime !== null && (await isSameProcessInstance(info.pid, pidFileMtime));\n\n if (!isSameInstance) {\n log(`Removing stale PID file: ${info.name} (PID: ${info.pid})`, options);\n deletePid(info.name, options.pidDir);\n cleaned++;\n }\n }\n\n if (cleaned === 0) {\n log('No stale PID files found', options);\n } else {\n log(`Cleaned ${cleaned} stale PID file${cleaned === 1 ? '' : 's'}`, options);\n }\n\n return 0;\n}\n\nasync function handlePid(name: string, options: CliOptions): Promise<number> {\n const pid = readPid(name, options.pidDir);\n\n if (pid === null) {\n log(`No process found with name: ${name}`, options);\n return 1;\n }\n\n const pidFileMtime = getPidFileMtime(name, options.pidDir);\n const isSameInstance = pidFileMtime !== null && (await isSameProcessInstance(pid, pidFileMtime));\n\n if (isSameInstance) {\n log(String(pid), options);\n return 0;\n }\n\n log(`Process ${name} is not running`, options);\n return 1;\n}\n\nasync function handleWait(name: string, options: CliOptions): Promise<number> {\n const pid = readPid(name, options.pidDir);\n\n if (pid === null) {\n log(`No process found with name: ${name}`, options);\n return 1;\n }\n\n // Check if process is alive first, then verify identity if possible.\n // Wait is non-destructive (we only poll), so we can be lenient with identity checks.\n if (!isProcessAlive(pid)) {\n log(`Process ${name} (PID: ${pid}) is not running`, options);\n return 1;\n }\n\n log(`Waiting for process ${name} (PID: ${pid}) to exit...`, options);\n\n const timeoutMs = options.timeout !== undefined ? options.timeout * 1000 : undefined;\n const startTime = Date.now();\n const pollInterval = 500;\n\n while (isProcessAlive(pid)) {\n if (timeoutMs !== undefined && Date.now() - startTime >= timeoutMs) {\n log(`Timeout waiting for process ${name} (PID: ${pid})`, options);\n return 1;\n }\n await new Promise(resolve => setTimeout(resolve, pollInterval));\n }\n\n log(`Process ${name} (PID: ${pid}) has exited`, options);\n return 0;\n}\n\nasync function main(): Promise<number> {\n const args = process.argv.slice(2);\n\n // Parse arguments\n const parseResult = parseArgs(args);\n if (!parseResult.success) {\n logError(`Error: ${parseResult.error}`);\n logError('Use --help for usage information');\n return 1;\n }\n\n const options = parseResult.options;\n\n // Validate options\n const validateResult = validateOptions(options);\n if (!validateResult.success) {\n logError(`Error: ${validateResult.error}`);\n logError('Use --help for usage information');\n return 1;\n }\n\n // Handle help\n if (options.help) {\n console.log(getHelpText());\n return 0;\n }\n\n // Handle version\n if (options.version) {\n console.log(`just-one v${VERSION}`);\n return 0;\n }\n\n // Handle list\n if (options.list) {\n return handleList(options);\n }\n\n // Handle kill\n if (options.kill) {\n return await handleKill(options.kill, options);\n }\n\n // Handle kill all\n if (options.killAll) {\n return await handleKillAll(options);\n }\n\n // Handle status\n if (options.status) {\n return await handleStatus(options.status, options);\n }\n\n // Handle clean\n if (options.clean) {\n return await handleClean(options);\n }\n\n // Handle pid\n if (options.pid) {\n return await handlePid(options.pid, options);\n }\n\n // Handle wait\n if (options.wait) {\n return await handleWait(options.wait, options);\n }\n\n // Handle run (with optional --ensure modifier)\n return await handleRun(options);\n}\n\n// Run the CLI\nmain()\n .then(code => {\n // Only exit if we're not running a child process\n // The child process exit handler will call process.exit\n if (code !== 0) {\n process.exit(code);\n }\n })\n .catch(err => {\n console.error('Unexpected error:', err);\n process.exit(1);\n });\n\n// Export for testing\nexport { main };\n","/**\n * CLI argument parsing for just-one\n */\n\nexport interface CliOptions {\n name?: string;\n kill?: string;\n list: boolean;\n status?: string;\n killAll: boolean;\n ensure: boolean;\n clean: boolean;\n pid?: string;\n wait?: string;\n timeout?: number;\n pidDir: string;\n quiet: boolean;\n help: boolean;\n version: boolean;\n command: string[];\n}\n\nexport interface ParseResult {\n success: true;\n options: CliOptions;\n}\n\nexport interface ParseError {\n success: false;\n error: string;\n}\n\nexport type ParseOutput = ParseResult | ParseError;\n\nconst DEFAULT_PID_DIR = '.just-one';\nconst MAX_NAME_LENGTH = 255;\n\n/**\n * Validate a process name for safe file operations\n * Rejects names containing path separators or traversal sequences\n */\nfunction isValidName(name: string): boolean {\n if (!name || name.length > MAX_NAME_LENGTH) {\n return false;\n }\n // Reject path separators and traversal sequences\n if (name.includes('/') || name.includes('\\\\') || name.includes('..')) {\n return false;\n }\n // Reject names that are only dots or whitespace\n if (/^[\\s.]*$/.test(name)) {\n return false;\n }\n return true;\n}\n\n/**\n * Validate a PID directory path for safe file operations\n * Rejects paths containing traversal sequences\n */\nfunction isValidPidDir(dir: string): boolean {\n if (!dir || dir.length > 1024) {\n return false;\n }\n // Reject path traversal sequences\n if (dir.includes('..')) {\n return false;\n }\n return true;\n}\n\n/**\n * Parse command line arguments\n */\nexport function parseArgs(args: string[]): ParseOutput {\n const options: CliOptions = {\n name: undefined,\n kill: undefined,\n list: false,\n status: undefined,\n killAll: false,\n ensure: false,\n clean: false,\n pid: undefined,\n wait: undefined,\n timeout: undefined,\n pidDir: DEFAULT_PID_DIR,\n quiet: false,\n help: false,\n version: false,\n command: [],\n };\n\n let i = 0;\n while (i < args.length) {\n // TypeScript requires this check due to noUncheckedIndexedAccess\n const arg = args[i]!;\n\n // Everything after -- is the command\n if (arg === '--') {\n options.command = args.slice(i + 1);\n break;\n }\n\n // Help\n if (arg === '--help' || arg === '-h') {\n options.help = true;\n i++;\n continue;\n }\n\n // Version\n if (arg === '--version' || arg === '-v') {\n options.version = true;\n i++;\n continue;\n }\n\n // List\n if (arg === '--list' || arg === '-l') {\n options.list = true;\n i++;\n continue;\n }\n\n // Quiet\n if (arg === '--quiet' || arg === '-q') {\n options.quiet = true;\n i++;\n continue;\n }\n\n // Name (requires value)\n if (arg === '--name' || arg === '-n') {\n const value = args[i + 1];\n if (!value || value.startsWith('-')) {\n return { success: false, error: 'Option --name requires a value' };\n }\n if (!isValidName(value)) {\n return {\n success: false,\n error: 'Invalid name: must not contain path separators or be too long',\n };\n }\n options.name = value;\n i += 2;\n continue;\n }\n\n // Kill (requires value)\n if (arg === '--kill' || arg === '-k') {\n const value = args[i + 1];\n if (!value || value.startsWith('-')) {\n return { success: false, error: 'Option --kill requires a value' };\n }\n if (!isValidName(value)) {\n return {\n success: false,\n error: 'Invalid name: must not contain path separators or be too long',\n };\n }\n options.kill = value;\n i += 2;\n continue;\n }\n\n // PID directory (requires value)\n if (arg === '--pid-dir' || arg === '-d') {\n const value = args[i + 1];\n if (!value || value.startsWith('-')) {\n return { success: false, error: 'Option --pid-dir requires a value' };\n }\n if (!isValidPidDir(value)) {\n return {\n success: false,\n error: 'Invalid PID directory: must not contain path traversal sequences',\n };\n }\n options.pidDir = value;\n i += 2;\n continue;\n }\n\n // Status (requires value)\n if (arg === '--status' || arg === '-s') {\n const value = args[i + 1];\n if (!value || value.startsWith('-')) {\n return { success: false, error: 'Option --status requires a value' };\n }\n if (!isValidName(value)) {\n return {\n success: false,\n error: 'Invalid name: must not contain path separators or be too long',\n };\n }\n options.status = value;\n i += 2;\n continue;\n }\n\n // Kill All\n if (arg === '--kill-all' || arg === '-K') {\n options.killAll = true;\n i++;\n continue;\n }\n\n // Ensure\n if (arg === '--ensure' || arg === '-e') {\n options.ensure = true;\n i++;\n continue;\n }\n\n // Clean\n if (arg === '--clean') {\n options.clean = true;\n i++;\n continue;\n }\n\n // PID output (requires value)\n if (arg === '--pid' || arg === '-p') {\n const value = args[i + 1];\n if (!value || value.startsWith('-')) {\n return { success: false, error: 'Option --pid requires a value' };\n }\n if (!isValidName(value)) {\n return {\n success: false,\n error: 'Invalid name: must not contain path separators or be too long',\n };\n }\n options.pid = value;\n i += 2;\n continue;\n }\n\n // Wait (requires value)\n if (arg === '--wait' || arg === '-w') {\n const value = args[i + 1];\n if (!value || value.startsWith('-')) {\n return { success: false, error: 'Option --wait requires a value' };\n }\n if (!isValidName(value)) {\n return {\n success: false,\n error: 'Invalid name: must not contain path separators or be too long',\n };\n }\n options.wait = value;\n i += 2;\n continue;\n }\n\n // Timeout (requires numeric value)\n if (arg === '--timeout' || arg === '-t') {\n const value = args[i + 1];\n if (!value || value.startsWith('-')) {\n return { success: false, error: 'Option --timeout requires a positive number' };\n }\n const num = Number(value);\n if (isNaN(num) || num <= 0) {\n return { success: false, error: 'Option --timeout requires a positive number' };\n }\n options.timeout = num;\n i += 2;\n continue;\n }\n\n // Unknown option\n if (arg.startsWith('-')) {\n return { success: false, error: `Unknown option: ${arg}` };\n }\n\n // Unexpected positional argument\n return { success: false, error: `Unexpected argument: ${arg}` };\n }\n\n return { success: true, options };\n}\n\n/**\n * Validate parsed options\n */\nexport function validateOptions(options: CliOptions): ParseOutput {\n // Help and version don't need validation\n if (options.help || options.version) {\n return { success: true, options };\n }\n\n // List doesn't need name or command\n if (options.list) {\n return { success: true, options };\n }\n\n // Kill only needs a name\n if (options.kill) {\n return { success: true, options };\n }\n\n // Standalone operations that don't need name or command\n if (options.status) {\n return { success: true, options };\n }\n if (options.killAll) {\n return { success: true, options };\n }\n if (options.clean) {\n return { success: true, options };\n }\n if (options.pid) {\n return { success: true, options };\n }\n if (options.wait) {\n if (options.timeout !== undefined && options.timeout <= 0) {\n return { success: false, error: 'Option --timeout requires a positive number' };\n }\n return { success: true, options };\n }\n\n // Timeout without wait is an error\n if (options.timeout !== undefined && !options.wait) {\n return { success: false, error: 'Option --timeout can only be used with --wait' };\n }\n\n // Running a command requires both name and command\n if (!options.name) {\n return { success: false, error: 'Option --name is required when running a command' };\n }\n\n if (options.command.length === 0) {\n return { success: false, error: 'No command specified. Use: just-one -n <name> -- <command>' };\n }\n\n return { success: true, options };\n}\n\n/**\n * Get help text\n */\nexport function getHelpText(): string {\n return `just-one - Ensure only one instance of a command runs at a time\n\nUsage:\n just-one -n <name> -- <command> Run command, killing any previous instance\n just-one -n <name> -e -- <command> Run only if not already running (ensure mode)\n just-one -k <name> Kill a named process\n just-one -K Kill all tracked processes\n just-one -s <name> Check if a named process is running\n just-one -p <name> Print the PID of a named process\n just-one -w <name> Wait for a named process to exit\n just-one -l List all tracked processes\n just-one --clean Remove stale PID files\n\nOptions:\n -n, --name <name> Name to identify this process (required for running)\n -k, --kill <name> Kill the named process and exit\n -K, --kill-all Kill all tracked processes\n -s, --status <name> Check if a named process is running (exit 0=running, 1=stopped)\n -e, --ensure Only start if not already running (use with -n and command)\n -p, --pid <name> Print the PID of a named process\n -w, --wait <name> Wait for a named process to exit\n -t, --timeout <secs> Timeout in seconds (use with --wait)\n --clean Remove stale PID files\n -l, --list List all tracked processes and their status\n -d, --pid-dir <dir> Directory for PID files (default: .just-one/)\n -q, --quiet Suppress output\n -h, --help Show this help message\n -v, --version Show version number\n\nExamples:\n # Run storybook, killing any previous instance\n just-one -n storybook -- npx storybook dev -p 6006\n\n # Run vite dev server only if not already running\n just-one -n vite -e -- npm run dev\n\n # Check if a process is running\n just-one -s storybook\n\n # Get the PID for scripting\n pid=$(just-one -p storybook -q)\n\n # Kill all tracked processes\n just-one -K\n\n # Wait for a process to exit (with 30s timeout)\n just-one -w myapp -t 30\n\n # Clean up stale PID files\n just-one --clean\n\n # Kill a named process\n just-one -k storybook\n\n # List all tracked processes\n just-one -l\n`;\n}\n","/**\r\n * PID file operations for just-one\r\n */\r\n\r\nimport { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';\r\nimport { join, dirname } from 'path';\r\n\r\nexport interface PidInfo {\r\n name: string;\r\n pid: number;\r\n exists: boolean;\r\n}\r\n\r\n/**\r\n * Get the path to a PID file for a given name\r\n */\r\nexport function getPidFilePath(name: string, pidDir: string): string {\r\n return join(pidDir, `${name}.pid`);\r\n}\r\n\r\n/**\r\n * Read the PID from a PID file\r\n * Returns null if the file doesn't exist or is invalid\r\n */\r\nexport function readPid(name: string, pidDir: string): number | null {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n\r\n if (!existsSync(pidFile)) {\r\n return null;\r\n }\r\n\r\n try {\r\n const content = readFileSync(pidFile, 'utf8').trim();\r\n const pid = parseInt(content, 10);\r\n\r\n if (isNaN(pid) || pid <= 0) {\r\n return null;\r\n }\r\n\r\n return pid;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Write a PID to a PID file\r\n * Creates the directory if it doesn't exist\r\n */\r\nexport function writePid(name: string, pid: number, pidDir: string): void {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n const dir = dirname(pidFile);\r\n\r\n if (!existsSync(dir)) {\r\n mkdirSync(dir, { recursive: true });\r\n }\r\n\r\n writeFileSync(pidFile, String(pid), 'utf8');\r\n}\r\n\r\n/**\r\n * Delete a PID file\r\n * Returns true if the file was deleted, false if it didn't exist\r\n */\r\nexport function deletePid(name: string, pidDir: string): boolean {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n\r\n if (!existsSync(pidFile)) {\r\n return false;\r\n }\r\n\r\n try {\r\n unlinkSync(pidFile);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Get the modification time of a PID file as Unix timestamp (milliseconds)\r\n * Returns null if file doesn't exist\r\n */\r\nexport function getPidFileMtime(name: string, pidDir: string): number | null {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n try {\r\n const stats = statSync(pidFile);\r\n return stats.mtimeMs;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * List all PID files in the directory\r\n * Returns information about each tracked process\r\n */\r\nexport function listPids(pidDir: string): PidInfo[] {\r\n if (!existsSync(pidDir)) {\r\n return [];\r\n }\r\n\r\n const files = readdirSync(pidDir);\r\n const pidFiles = files.filter(f => f.endsWith('.pid'));\r\n\r\n return pidFiles.map(file => {\r\n // Remove .pid suffix (use slice to only remove from end)\r\n const name = file.slice(0, -4);\r\n const pid = readPid(name, pidDir);\r\n\r\n return {\r\n name,\r\n pid: pid ?? 0,\r\n exists: pid !== null,\r\n };\r\n });\r\n}\r\n","/**\n * Cross-platform process handling for just-one\n */\n\nimport { spawn, execSync, ChildProcess } from 'child_process';\nimport pidusage from 'pidusage';\n\nconst isWindows = process.platform === 'win32';\n\n// Constants for process polling\nconst DEFAULT_WAIT_TIMEOUT_MS = 2000;\nconst CHECK_INTERVAL_MS = 100;\n\n/**\n * Validate that a PID is a safe positive integer for use in system calls\n */\nexport function isValidPid(pid: number): boolean {\n return Number.isInteger(pid) && pid > 0 && pid <= 4194304; // Max PID on most systems\n}\n\n// Tolerance for comparing PID file mtime with process start time\nconst START_TIME_TOLERANCE_MS = 5000; // 5 seconds\n\n/**\n * Get the start time of a process as Unix timestamp (milliseconds)\n * Returns null if process doesn't exist or start time can't be determined\n */\nexport async function getProcessStartTime(pid: number): Promise<number | null> {\n if (!isValidPid(pid)) {\n return null;\n }\n\n try {\n const stats = await pidusage(pid);\n // Calculate start time from current timestamp minus elapsed time\n return stats.timestamp - stats.elapsed;\n } catch {\n return null; // Process doesn't exist or can't get stats\n }\n}\n\n/**\n * Check if a running process is the same instance we originally spawned.\n * Compares process start time with PID file modification time.\n *\n * Returns true if:\n * - Process exists AND start time is within tolerance of pidFileMtime\n *\n * Returns false if:\n * - Process doesn't exist\n * - Can't determine process start time\n * - Start time doesn't match (likely PID reuse)\n */\nexport async function isSameProcessInstance(pid: number, pidFileMtimeMs: number): Promise<boolean> {\n const processStartTime = await getProcessStartTime(pid);\n if (processStartTime === null) {\n return false;\n }\n\n const diff = Math.abs(processStartTime - pidFileMtimeMs);\n return diff <= START_TIME_TOLERANCE_MS;\n}\n\n/**\n * Check if a process with the given PID is still running\n */\nexport function isProcessAlive(pid: number): boolean {\n try {\n if (!isValidPid(pid)) {\n return false;\n }\n if (isWindows) {\n // Windows: tasklist returns exit code 0 if process found\n // PID is validated as a safe integer above before interpolation\n const output = execSync(`tasklist /FI \"PID eq ${pid}\" /NH`, {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return output.includes(String(pid));\n } else {\n // Unix/Mac: kill -0 checks if process exists without killing it\n process.kill(pid, 0);\n return true;\n }\n } catch {\n return false;\n }\n}\n\n/**\n * Kill a process by PID\n * Returns true if the process was killed, false if it wasn't running\n */\nexport function killProcess(pid: number): boolean {\n if (!isValidPid(pid) || !isProcessAlive(pid)) {\n return false;\n }\n\n try {\n if (isWindows) {\n // Windows: taskkill with /T kills the process tree, /F forces\n // PID is validated as a safe integer above before interpolation\n execSync(`taskkill /PID ${pid} /T /F`, {\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n } else {\n // Unix: try to kill process group first (catches child processes),\n // fall back to killing just the process if group kill fails\n const killed = tryKillUnix(-pid) || tryKillUnix(pid);\n if (!killed) {\n return false;\n }\n }\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Helper to attempt Unix kill with error handling\n */\nfunction tryKillUnix(pid: number): boolean {\n try {\n process.kill(pid, 'SIGTERM');\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Wait for a process to die, with timeout\n * @param pid - Process ID to wait for\n * @param timeoutMs - Maximum time to wait (default: 2000ms)\n */\nexport async function waitForProcessToDie(\n pid: number,\n timeoutMs: number = DEFAULT_WAIT_TIMEOUT_MS\n): Promise<boolean> {\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeoutMs) {\n if (!isProcessAlive(pid)) {\n return true;\n }\n await new Promise(resolve => setTimeout(resolve, CHECK_INTERVAL_MS));\n }\n\n return !isProcessAlive(pid);\n}\n\nexport interface SpawnResult {\n child: ChildProcess;\n pid: number;\n}\n\n/**\n * Spawn a command with stdio forwarding\n */\nexport function spawnCommand(command: string, args: string[]): SpawnResult {\n // On Windows, pass entire command as a single string to avoid escaping issues\n // with shell: true (DEP0190 warning and argument handling)\n const spawnCmd = isWindows ? `${command} ${args.join(' ')}` : command;\n const spawnArgs = isWindows ? [] : args;\n\n const child = spawn(spawnCmd, spawnArgs, {\n stdio: 'inherit',\n shell: isWindows,\n detached: !isWindows,\n });\n\n if (child.pid === undefined) {\n throw new Error('Failed to spawn process');\n }\n\n return {\n child,\n pid: child.pid,\n };\n}\n\n// Grace period for Windows child process to exit before force-killing\nconst WINDOWS_GRACEFUL_TIMEOUT_MS = 2000;\n\n/**\n * Set up signal handlers to forward signals to child process\n *\n * Unix: forwards SIGTERM to child for graceful shutdown.\n *\n * Windows: the child shares the console (stdio: 'inherit'), so when the user\n * presses Ctrl+C, Windows delivers CTRL_C_EVENT to the child directly — no\n * forwarding needed. We just set a force-kill timeout as a safety net in case\n * the child doesn't exit on its own. process.kill(pid, 'SIGINT') on Windows\n * calls TerminateProcess (not GenerateConsoleCtrlEvent), so we intentionally\n * avoid calling it to give the child time to handle the OS-delivered signal.\n */\nexport function setupSignalHandlers(child: ChildProcess, onExit?: () => void): void {\n let forceKillTimer: ReturnType<typeof setTimeout> | null = null;\n\n const forceKillWindows = () => {\n if (child.pid && isValidPid(child.pid) && isProcessAlive(child.pid)) {\n try {\n execSync(`taskkill /PID ${child.pid} /T /F`, {\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n } catch {\n // Process might already be dead\n }\n }\n };\n\n const handleSignal = (_signal: NodeJS.Signals) => {\n if (child.pid && isValidPid(child.pid)) {\n if (isWindows) {\n // On Windows, the child already received CTRL_C_EVENT from the OS\n // (since it shares our console via stdio: 'inherit').\n // Don't call process.kill() — it uses TerminateProcess which would\n // prevent the child from running its cleanup handlers.\n // Just set a force-kill timeout as a safety net.\n if (forceKillTimer === null) {\n forceKillTimer = setTimeout(forceKillWindows, WINDOWS_GRACEFUL_TIMEOUT_MS);\n forceKillTimer.unref();\n }\n } else {\n // Forward as SIGTERM for graceful shutdown\n child.kill('SIGTERM');\n }\n }\n };\n\n // Forward both SIGINT (Ctrl+C) and SIGTERM to child\n process.on('SIGINT', () => handleSignal('SIGINT'));\n process.on('SIGTERM', () => handleSignal('SIGTERM'));\n\n child.on('exit', (code, signal) => {\n // Child exited gracefully — cancel the force-kill timer if pending\n if (forceKillTimer !== null) {\n clearTimeout(forceKillTimer);\n forceKillTimer = null;\n }\n if (onExit) {\n onExit();\n }\n if (signal) {\n process.exit(128 + (signal === 'SIGTERM' ? 15 : signal === 'SIGINT' ? 2 : 1));\n }\n process.exit(code ?? 0);\n });\n\n child.on('error', err => {\n console.error(`Failed to start process: ${err.message}`);\n process.exit(1);\n });\n}\n"],"mappings":"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","names":["require"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radleta/just-one",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A CLI tool that ensures only one instance of a command runs at a time",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,6 +26,7 @@
26
26
  "test": "vitest run",
27
27
  "test:watch": "vitest",
28
28
  "test:coverage": "vitest run --coverage",
29
+ "test:npm": "JUST_ONE_NPX=1 JUST_ONE_CLI=@radleta/just-one vitest run src/e2e/",
29
30
  "lint": "eslint src",
30
31
  "format": "prettier --write \"src/**/*.ts\" \"*.{json,md,yml,yaml}\"",
31
32
  "format:check": "prettier --check \"src/**/*.ts\" \"*.{json,md,yml,yaml}\"",
@@ -34,9 +35,11 @@
34
35
  "pack:dry": "npm pack --dry-run",
35
36
  "size:check": "npm pack --dry-run 2>&1 | grep 'package size' | head -1 || echo 'Run npm pack to see size'",
36
37
  "verify:package": "node scripts/verify-package.js",
37
- "release:prepare": "npm run validate && npm run build && npm run verify:package && npm run size:check && echo '\nReady for release. Run: npm version [patch|minor|major]'",
38
- "preversion": "npm run validate",
39
- "postversion": "git push && git push --tags",
38
+ "release:prepare": "npm run validate && npm run build && npm run verify:package && npm run size:check && echo '\nReady for release. Run: npm run release'",
39
+ "release": "standard-version",
40
+ "release:minor": "standard-version --release-as minor",
41
+ "release:major": "standard-version --release-as major",
42
+ "postrelease": "git push && git push --tags",
40
43
  "prepublishOnly": "npm run validate && npm run build",
41
44
  "prepare": "husky"
42
45
  },
@@ -75,6 +78,7 @@
75
78
  "husky": "^9.1.7",
76
79
  "lint-staged": "^16.2.4",
77
80
  "prettier": "^3.4.2",
81
+ "standard-version": "^9.5.0",
78
82
  "tsup": "^8.0.0",
79
83
  "typescript": "^5.8.3",
80
84
  "vitest": "^3.0.0"