@radleta/just-one 1.1.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 +10 -0
- package/README.md +233 -181
- package/dist/index.js +306 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
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
|
+
## [1.2.0](https://github.com/radleta/just-one/compare/v1.1.0...v1.2.0) (2026-02-11)
|
|
6
|
+
|
|
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
|
+
|
|
5
15
|
## [1.1.0](https://github.com/radleta/just-one/compare/v1.0.0...v1.1.0) (2026-01-29)
|
|
6
16
|
|
|
7
17
|
### Features
|
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
|
-
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **PID
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
just-one
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
just-one -
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
|
130
|
-
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
@@ -32,6 +32,13 @@ function parseArgs(args) {
|
|
|
32
32
|
name: void 0,
|
|
33
33
|
kill: void 0,
|
|
34
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,
|
|
35
42
|
pidDir: DEFAULT_PID_DIR,
|
|
36
43
|
quiet: false,
|
|
37
44
|
help: false,
|
|
@@ -71,7 +78,10 @@ function parseArgs(args) {
|
|
|
71
78
|
return { success: false, error: "Option --name requires a value" };
|
|
72
79
|
}
|
|
73
80
|
if (!isValidName(value)) {
|
|
74
|
-
return {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: "Invalid name: must not contain path separators or be too long"
|
|
84
|
+
};
|
|
75
85
|
}
|
|
76
86
|
options.name = value;
|
|
77
87
|
i += 2;
|
|
@@ -83,7 +93,10 @@ function parseArgs(args) {
|
|
|
83
93
|
return { success: false, error: "Option --kill requires a value" };
|
|
84
94
|
}
|
|
85
95
|
if (!isValidName(value)) {
|
|
86
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
error: "Invalid name: must not contain path separators or be too long"
|
|
99
|
+
};
|
|
87
100
|
}
|
|
88
101
|
options.kill = value;
|
|
89
102
|
i += 2;
|
|
@@ -95,12 +108,88 @@ function parseArgs(args) {
|
|
|
95
108
|
return { success: false, error: "Option --pid-dir requires a value" };
|
|
96
109
|
}
|
|
97
110
|
if (!isValidPidDir(value)) {
|
|
98
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
error: "Invalid PID directory: must not contain path traversal sequences"
|
|
114
|
+
};
|
|
99
115
|
}
|
|
100
116
|
options.pidDir = value;
|
|
101
117
|
i += 2;
|
|
102
118
|
continue;
|
|
103
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
|
+
}
|
|
104
193
|
if (arg.startsWith("-")) {
|
|
105
194
|
return { success: false, error: `Unknown option: ${arg}` };
|
|
106
195
|
}
|
|
@@ -118,6 +207,27 @@ function validateOptions(options) {
|
|
|
118
207
|
if (options.kill) {
|
|
119
208
|
return { success: true, options };
|
|
120
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
|
+
}
|
|
121
231
|
if (!options.name) {
|
|
122
232
|
return { success: false, error: "Option --name is required when running a command" };
|
|
123
233
|
}
|
|
@@ -131,24 +241,52 @@ function getHelpText() {
|
|
|
131
241
|
|
|
132
242
|
Usage:
|
|
133
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)
|
|
134
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
|
|
135
250
|
just-one -l List all tracked processes
|
|
251
|
+
just-one --clean Remove stale PID files
|
|
136
252
|
|
|
137
253
|
Options:
|
|
138
|
-
-n, --name <name>
|
|
139
|
-
-k, --kill <name>
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
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
|
|
145
268
|
|
|
146
269
|
Examples:
|
|
147
270
|
# Run storybook, killing any previous instance
|
|
148
271
|
just-one -n storybook -- npx storybook dev -p 6006
|
|
149
272
|
|
|
150
|
-
# Run vite dev server
|
|
151
|
-
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
|
|
152
290
|
|
|
153
291
|
# Kill a named process
|
|
154
292
|
just-one -k storybook
|
|
@@ -328,15 +466,25 @@ function spawnCommand(command, args) {
|
|
|
328
466
|
pid: child.pid
|
|
329
467
|
};
|
|
330
468
|
}
|
|
469
|
+
var WINDOWS_GRACEFUL_TIMEOUT_MS = 2e3;
|
|
331
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
|
+
};
|
|
332
482
|
const handleSignal = (_signal) => {
|
|
333
483
|
if (child.pid && isValidPid(child.pid)) {
|
|
334
484
|
if (isWindows) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
});
|
|
339
|
-
} catch {
|
|
485
|
+
if (forceKillTimer === null) {
|
|
486
|
+
forceKillTimer = setTimeout(forceKillWindows, WINDOWS_GRACEFUL_TIMEOUT_MS);
|
|
487
|
+
forceKillTimer.unref();
|
|
340
488
|
}
|
|
341
489
|
} else {
|
|
342
490
|
child.kill("SIGTERM");
|
|
@@ -346,6 +494,10 @@ function setupSignalHandlers(child, onExit) {
|
|
|
346
494
|
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
347
495
|
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
|
348
496
|
child.on("exit", (code, signal) => {
|
|
497
|
+
if (forceKillTimer !== null) {
|
|
498
|
+
clearTimeout(forceKillTimer);
|
|
499
|
+
forceKillTimer = null;
|
|
500
|
+
}
|
|
349
501
|
if (onExit) {
|
|
350
502
|
onExit();
|
|
351
503
|
}
|
|
@@ -426,6 +578,10 @@ async function handleRun(options) {
|
|
|
426
578
|
const pidFileMtime = getPidFileMtime(name, options.pidDir);
|
|
427
579
|
const shouldKill = pidFileMtime !== null && await isSameProcessInstance(existingPid, pidFileMtime);
|
|
428
580
|
if (shouldKill) {
|
|
581
|
+
if (options.ensure) {
|
|
582
|
+
log(`Process ${name} is already running (PID: ${existingPid}), skipping`, options);
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
429
585
|
log(`Killing existing process ${name} (PID: ${existingPid})...`, options);
|
|
430
586
|
killProcess(existingPid);
|
|
431
587
|
await waitForProcessToDie(existingPid);
|
|
@@ -450,6 +606,124 @@ async function handleRun(options) {
|
|
|
450
606
|
return 1;
|
|
451
607
|
}
|
|
452
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
|
+
}
|
|
453
727
|
async function main() {
|
|
454
728
|
const args = process.argv.slice(2);
|
|
455
729
|
const parseResult = parseArgs(args);
|
|
@@ -479,6 +753,21 @@ async function main() {
|
|
|
479
753
|
if (options.kill) {
|
|
480
754
|
return await handleKill(options.kill, options);
|
|
481
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
|
+
}
|
|
482
771
|
return await handleRun(options);
|
|
483
772
|
}
|
|
484
773
|
main().then((code) => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 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 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 run\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","/**\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"],"mappings":"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","names":["require"]}
|
|
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"]}
|