@radleta/just-one 0.1.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 ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
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).
7
+
8
+ ## [0.1.0] - 2026-01-22
9
+
10
+ ### Added
11
+
12
+ - Initial release
13
+ - CLI tool to ensure only one instance of a command runs at a time
14
+ - PID file management for tracking processes
15
+ - Cross-platform support (Windows, macOS, Linux)
16
+ - Commands: run with `-n`, kill with `-k`, list with `-l`
17
+ - Options: `--pid-dir`, `--quiet`, `--help`, `--version`
18
+ - Signal handling for graceful shutdown (SIGINT, SIGTERM)
19
+ - Published as scoped package `@radleta/just-one`
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Richard Adleta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
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
+ - **Zero dependencies** - Uses only Node.js built-ins
26
+ - **PID file management** - Survives terminal closes and system restarts
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install -g @radleta/just-one
32
+ ```
33
+
34
+ Or use with npx (no install required):
35
+
36
+ ```bash
37
+ npx @radleta/just-one -n myapp -- npm run dev
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Basic usage
43
+
44
+ ```bash
45
+ # Run storybook, killing any previous instance named "storybook"
46
+ just-one -n storybook -- npx storybook dev -p 6006
47
+
48
+ # Run vite dev server
49
+ just-one -n vite -- npm run dev
50
+
51
+ # Run any command
52
+ just-one -n myapp -- node server.js
53
+ ```
54
+
55
+ ### Kill a named process
56
+
57
+ ```bash
58
+ just-one -k storybook
59
+ just-one --kill myapp
60
+ ```
61
+
62
+ ### List tracked processes
63
+
64
+ ```bash
65
+ just-one -l
66
+ just-one --list
67
+ ```
68
+
69
+ ### Specify custom PID directory
70
+
71
+ ```bash
72
+ # Default: ./.just-one/<name>.pid
73
+ just-one -n storybook -- npx storybook dev
74
+
75
+ # Custom directory
76
+ just-one -n storybook -d /tmp -- npx storybook dev
77
+ ```
78
+
79
+ ## CLI Options
80
+
81
+ | Option | Alias | Description |
82
+ |--------|-------|-------------|
83
+ | `--name <name>` | `-n` | Required for run. Name to identify this process |
84
+ | `--kill <name>` | `-k` | Kill the named process and exit |
85
+ | `--list` | `-l` | List all tracked processes and their status |
86
+ | `--pid-dir <dir>` | `-d` | Directory for PID files (default: `.just-one/`) |
87
+ | `--quiet` | `-q` | Suppress output |
88
+ | `--help` | `-h` | Show help |
89
+ | `--version` | `-v` | Show version |
90
+
91
+ ## package.json Scripts
92
+
93
+ ```json
94
+ {
95
+ "scripts": {
96
+ "storybook": "just-one -n storybook -- storybook dev -p 6006",
97
+ "dev": "just-one -n vite -- vite",
98
+ "dev:api": "just-one -n api -- node server.js",
99
+ "stop": "just-one -k storybook && just-one -k vite && just-one -k api"
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## How It Works
105
+
106
+ ```
107
+ .just-one/
108
+ storybook.pid # Contains: 12345
109
+ vite.pid # Contains: 67890
110
+ ```
111
+
112
+ 1. Check if a PID file exists for that name
113
+ 2. If yes, kill that specific process (and its children)
114
+ 3. Start the new process
115
+ 4. Save its PID for next time
116
+
117
+ ### Cross-Platform Process Handling
118
+
119
+ | Platform | Kill Method |
120
+ |----------|-------------|
121
+ | Windows | `taskkill /PID <pid> /T /F` (kills process tree) |
122
+ | Unix/Mac | `kill -SIGTERM -<pid>` (process group) |
123
+
124
+ ## Use Cases
125
+
126
+ - **Dev servers** - Storybook, Vite, webpack-dev-server, Next.js
127
+ - **Background processes** - API servers, database seeders, watchers
128
+ - **CI/CD** - Ensure clean state before running tests
129
+ - **Multiple instances** - Run named instances on different ports
130
+
131
+ ```bash
132
+ # Run two storybooks on different ports
133
+ just-one -n storybook-main -- storybook dev -p 6006
134
+ just-one -n storybook-docs -- storybook dev -p 6007
135
+ ```
136
+
137
+ ## Comparison
138
+
139
+ | Feature | just-one | kill-port | pm2 |
140
+ |---------|----------|-----------|-----|
141
+ | Kills by PID (precise) | Yes | No (by port) | Yes |
142
+ | Cross-platform | Yes | Yes | Yes |
143
+ | Zero config | Yes | Yes | No |
144
+ | Remembers processes | Yes (PID file) | No | Yes (daemon) |
145
+ | Lightweight | ~150 LOC | ~100 LOC | Heavy |
146
+ | Daemon required | No | No | Yes |
147
+
148
+ ## Requirements
149
+
150
+ - Node.js >= 18.0.0
151
+
152
+ ## Development
153
+
154
+ ```bash
155
+ # Install dependencies
156
+ npm install
157
+
158
+ # Build
159
+ npm run build
160
+
161
+ # Run tests
162
+ npm test
163
+
164
+ # Lint + typecheck + test
165
+ npm run validate
166
+ ```
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/index.js';
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ declare function main(): Promise<number>;
3
+
4
+ export { main };
package/dist/index.js ADDED
@@ -0,0 +1,448 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/cli.ts
4
+ var DEFAULT_PID_DIR = ".just-one";
5
+ var MAX_NAME_LENGTH = 255;
6
+ function isValidName(name) {
7
+ if (!name || name.length > MAX_NAME_LENGTH) {
8
+ return false;
9
+ }
10
+ if (name.includes("/") || name.includes("\\") || name.includes("..")) {
11
+ return false;
12
+ }
13
+ if (/^[\s.]*$/.test(name)) {
14
+ return false;
15
+ }
16
+ return true;
17
+ }
18
+ function isValidPidDir(dir) {
19
+ if (!dir || dir.length > 1024) {
20
+ return false;
21
+ }
22
+ if (dir.includes("..")) {
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+ function parseArgs(args) {
28
+ const options = {
29
+ name: void 0,
30
+ kill: void 0,
31
+ list: false,
32
+ pidDir: DEFAULT_PID_DIR,
33
+ quiet: false,
34
+ help: false,
35
+ version: false,
36
+ command: []
37
+ };
38
+ let i = 0;
39
+ while (i < args.length) {
40
+ const arg = args[i];
41
+ if (arg === "--") {
42
+ options.command = args.slice(i + 1);
43
+ break;
44
+ }
45
+ if (arg === "--help" || arg === "-h") {
46
+ options.help = true;
47
+ i++;
48
+ continue;
49
+ }
50
+ if (arg === "--version" || arg === "-v") {
51
+ options.version = true;
52
+ i++;
53
+ continue;
54
+ }
55
+ if (arg === "--list" || arg === "-l") {
56
+ options.list = true;
57
+ i++;
58
+ continue;
59
+ }
60
+ if (arg === "--quiet" || arg === "-q") {
61
+ options.quiet = true;
62
+ i++;
63
+ continue;
64
+ }
65
+ if (arg === "--name" || arg === "-n") {
66
+ const value = args[i + 1];
67
+ if (!value || value.startsWith("-")) {
68
+ return { success: false, error: "Option --name requires a value" };
69
+ }
70
+ if (!isValidName(value)) {
71
+ return { success: false, error: "Invalid name: must not contain path separators or be too long" };
72
+ }
73
+ options.name = value;
74
+ i += 2;
75
+ continue;
76
+ }
77
+ if (arg === "--kill" || arg === "-k") {
78
+ const value = args[i + 1];
79
+ if (!value || value.startsWith("-")) {
80
+ return { success: false, error: "Option --kill requires a value" };
81
+ }
82
+ if (!isValidName(value)) {
83
+ return { success: false, error: "Invalid name: must not contain path separators or be too long" };
84
+ }
85
+ options.kill = value;
86
+ i += 2;
87
+ continue;
88
+ }
89
+ if (arg === "--pid-dir" || arg === "-d") {
90
+ const value = args[i + 1];
91
+ if (!value || value.startsWith("-")) {
92
+ return { success: false, error: "Option --pid-dir requires a value" };
93
+ }
94
+ if (!isValidPidDir(value)) {
95
+ return { success: false, error: "Invalid PID directory: must not contain path traversal sequences" };
96
+ }
97
+ options.pidDir = value;
98
+ i += 2;
99
+ continue;
100
+ }
101
+ if (arg.startsWith("-")) {
102
+ return { success: false, error: `Unknown option: ${arg}` };
103
+ }
104
+ return { success: false, error: `Unexpected argument: ${arg}` };
105
+ }
106
+ return { success: true, options };
107
+ }
108
+ function validateOptions(options) {
109
+ if (options.help || options.version) {
110
+ return { success: true, options };
111
+ }
112
+ if (options.list) {
113
+ return { success: true, options };
114
+ }
115
+ if (options.kill) {
116
+ return { success: true, options };
117
+ }
118
+ if (!options.name) {
119
+ return { success: false, error: "Option --name is required when running a command" };
120
+ }
121
+ if (options.command.length === 0) {
122
+ return { success: false, error: "No command specified. Use: just-one -n <name> -- <command>" };
123
+ }
124
+ return { success: true, options };
125
+ }
126
+ function getHelpText() {
127
+ return `just-one - Ensure only one instance of a command runs at a time
128
+
129
+ Usage:
130
+ just-one -n <name> -- <command> Run command, killing any previous instance
131
+ just-one -k <name> Kill a named process
132
+ just-one -l List all tracked processes
133
+
134
+ 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
142
+
143
+ Examples:
144
+ # Run storybook, killing any previous instance
145
+ just-one -n storybook -- npx storybook dev -p 6006
146
+
147
+ # Run vite dev server
148
+ just-one -n vite -- npm run dev
149
+
150
+ # Kill a named process
151
+ just-one -k storybook
152
+
153
+ # List all tracked processes
154
+ just-one -l
155
+ `;
156
+ }
157
+
158
+ // src/lib/pid.ts
159
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync } from "fs";
160
+ import { join, dirname } from "path";
161
+ function getPidFilePath(name, pidDir) {
162
+ return join(pidDir, `${name}.pid`);
163
+ }
164
+ function readPid(name, pidDir) {
165
+ const pidFile = getPidFilePath(name, pidDir);
166
+ if (!existsSync(pidFile)) {
167
+ return null;
168
+ }
169
+ try {
170
+ const content = readFileSync(pidFile, "utf8").trim();
171
+ const pid = parseInt(content, 10);
172
+ if (isNaN(pid) || pid <= 0) {
173
+ return null;
174
+ }
175
+ return pid;
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+ function writePid(name, pid, pidDir) {
181
+ const pidFile = getPidFilePath(name, pidDir);
182
+ const dir = dirname(pidFile);
183
+ if (!existsSync(dir)) {
184
+ mkdirSync(dir, { recursive: true });
185
+ }
186
+ writeFileSync(pidFile, String(pid), "utf8");
187
+ }
188
+ function deletePid(name, pidDir) {
189
+ const pidFile = getPidFilePath(name, pidDir);
190
+ if (!existsSync(pidFile)) {
191
+ return false;
192
+ }
193
+ try {
194
+ unlinkSync(pidFile);
195
+ return true;
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+ function listPids(pidDir) {
201
+ if (!existsSync(pidDir)) {
202
+ return [];
203
+ }
204
+ const files = readdirSync(pidDir);
205
+ const pidFiles = files.filter((f) => f.endsWith(".pid"));
206
+ return pidFiles.map((file) => {
207
+ const name = file.slice(0, -4);
208
+ const pid = readPid(name, pidDir);
209
+ return {
210
+ name,
211
+ pid: pid ?? 0,
212
+ exists: pid !== null
213
+ };
214
+ });
215
+ }
216
+
217
+ // src/lib/process.ts
218
+ import { spawn, execSync } from "child_process";
219
+ var isWindows = process.platform === "win32";
220
+ var DEFAULT_WAIT_TIMEOUT_MS = 2e3;
221
+ var CHECK_INTERVAL_MS = 100;
222
+ function isValidPid(pid) {
223
+ return Number.isInteger(pid) && pid > 0 && pid <= 4194304;
224
+ }
225
+ function isProcessAlive(pid) {
226
+ try {
227
+ if (!isValidPid(pid)) {
228
+ return false;
229
+ }
230
+ if (isWindows) {
231
+ const output = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
232
+ encoding: "utf8",
233
+ stdio: ["pipe", "pipe", "pipe"]
234
+ });
235
+ return output.includes(String(pid));
236
+ } else {
237
+ process.kill(pid, 0);
238
+ return true;
239
+ }
240
+ } catch {
241
+ return false;
242
+ }
243
+ }
244
+ function killProcess(pid) {
245
+ if (!isValidPid(pid) || !isProcessAlive(pid)) {
246
+ return false;
247
+ }
248
+ try {
249
+ if (isWindows) {
250
+ execSync(`taskkill /PID ${pid} /T /F`, {
251
+ stdio: ["pipe", "pipe", "pipe"]
252
+ });
253
+ } else {
254
+ const killed = tryKillUnix(-pid) || tryKillUnix(pid);
255
+ if (!killed) {
256
+ return false;
257
+ }
258
+ }
259
+ return true;
260
+ } catch {
261
+ return false;
262
+ }
263
+ }
264
+ function tryKillUnix(pid) {
265
+ try {
266
+ process.kill(pid, "SIGTERM");
267
+ return true;
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+ async function waitForProcessToDie(pid, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
273
+ const startTime = Date.now();
274
+ while (Date.now() - startTime < timeoutMs) {
275
+ if (!isProcessAlive(pid)) {
276
+ return true;
277
+ }
278
+ await new Promise((resolve) => setTimeout(resolve, CHECK_INTERVAL_MS));
279
+ }
280
+ return !isProcessAlive(pid);
281
+ }
282
+ function spawnCommand(command, args) {
283
+ const spawnCmd = isWindows ? `${command} ${args.join(" ")}` : command;
284
+ const spawnArgs = isWindows ? [] : args;
285
+ const child = spawn(spawnCmd, spawnArgs, {
286
+ stdio: "inherit",
287
+ shell: isWindows,
288
+ detached: !isWindows
289
+ });
290
+ if (child.pid === void 0) {
291
+ throw new Error("Failed to spawn process");
292
+ }
293
+ return {
294
+ child,
295
+ pid: child.pid
296
+ };
297
+ }
298
+ function setupSignalHandlers(child, onExit) {
299
+ const handleSignal = (_signal) => {
300
+ if (child.pid && isValidPid(child.pid)) {
301
+ if (isWindows) {
302
+ try {
303
+ execSync(`taskkill /PID ${child.pid} /T /F`, {
304
+ stdio: ["pipe", "pipe", "pipe"]
305
+ });
306
+ } catch {
307
+ }
308
+ } else {
309
+ child.kill("SIGTERM");
310
+ }
311
+ }
312
+ };
313
+ process.on("SIGINT", () => handleSignal("SIGINT"));
314
+ process.on("SIGTERM", () => handleSignal("SIGTERM"));
315
+ child.on("exit", (code, signal) => {
316
+ if (onExit) {
317
+ onExit();
318
+ }
319
+ if (signal) {
320
+ process.exit(128 + (signal === "SIGTERM" ? 15 : signal === "SIGINT" ? 2 : 1));
321
+ }
322
+ process.exit(code ?? 0);
323
+ });
324
+ child.on("error", (err) => {
325
+ console.error(`Failed to start process: ${err.message}`);
326
+ process.exit(1);
327
+ });
328
+ }
329
+
330
+ // src/index.ts
331
+ var VERSION = "0.1.0";
332
+ function log(message, options) {
333
+ if (!options.quiet) {
334
+ console.log(message);
335
+ }
336
+ }
337
+ function logError(message) {
338
+ console.error(message);
339
+ }
340
+ async function handleKill(name, options) {
341
+ const pid = readPid(name, options.pidDir);
342
+ if (pid === null) {
343
+ log(`No process found with name: ${name}`, options);
344
+ return 0;
345
+ }
346
+ if (!isProcessAlive(pid)) {
347
+ log(`Process ${name} (PID: ${pid}) is not running, cleaning up PID file`, options);
348
+ deletePid(name, options.pidDir);
349
+ return 0;
350
+ }
351
+ log(`Killing process ${name} (PID: ${pid})...`, options);
352
+ const killed = killProcess(pid);
353
+ if (killed) {
354
+ await waitForProcessToDie(pid);
355
+ deletePid(name, options.pidDir);
356
+ log(`Process ${name} killed`, options);
357
+ return 0;
358
+ } else {
359
+ logError(`Failed to kill process ${name} (PID: ${pid})`);
360
+ return 1;
361
+ }
362
+ }
363
+ function handleList(options) {
364
+ const pids = listPids(options.pidDir);
365
+ if (pids.length === 0) {
366
+ log("No tracked processes", options);
367
+ return 0;
368
+ }
369
+ log("Tracked processes:", options);
370
+ for (const info of pids) {
371
+ const status = info.exists && isProcessAlive(info.pid) ? "running" : "stopped";
372
+ const pidStr = info.pid > 0 ? String(info.pid) : "unknown";
373
+ log(` ${info.name}: PID ${pidStr} (${status})`, options);
374
+ }
375
+ return 0;
376
+ }
377
+ async function handleRun(options) {
378
+ const name = options.name;
379
+ const [command, ...args] = options.command;
380
+ if (!command) {
381
+ logError("No command specified");
382
+ return 1;
383
+ }
384
+ const existingPid = readPid(name, options.pidDir);
385
+ if (existingPid !== null) {
386
+ if (isProcessAlive(existingPid)) {
387
+ log(`Killing existing process ${name} (PID: ${existingPid})...`, options);
388
+ killProcess(existingPid);
389
+ await waitForProcessToDie(existingPid);
390
+ }
391
+ deletePid(name, options.pidDir);
392
+ }
393
+ log(`Starting: ${command} ${args.join(" ")}`, options);
394
+ try {
395
+ const { child, pid } = spawnCommand(command, args);
396
+ writePid(name, pid, options.pidDir);
397
+ log(`Process started with PID: ${pid}`, options);
398
+ setupSignalHandlers(child);
399
+ return 0;
400
+ } catch (err) {
401
+ const message = err instanceof Error ? err.message : String(err);
402
+ logError(`Failed to start process: ${message}`);
403
+ return 1;
404
+ }
405
+ }
406
+ async function main() {
407
+ const args = process.argv.slice(2);
408
+ const parseResult = parseArgs(args);
409
+ if (!parseResult.success) {
410
+ logError(`Error: ${parseResult.error}`);
411
+ logError("Use --help for usage information");
412
+ return 1;
413
+ }
414
+ const options = parseResult.options;
415
+ const validateResult = validateOptions(options);
416
+ if (!validateResult.success) {
417
+ logError(`Error: ${validateResult.error}`);
418
+ logError("Use --help for usage information");
419
+ return 1;
420
+ }
421
+ if (options.help) {
422
+ console.log(getHelpText());
423
+ return 0;
424
+ }
425
+ if (options.version) {
426
+ console.log(`just-one v${VERSION}`);
427
+ return 0;
428
+ }
429
+ if (options.list) {
430
+ return handleList(options);
431
+ }
432
+ if (options.kill) {
433
+ return await handleKill(options.kill, options);
434
+ }
435
+ return await handleRun(options);
436
+ }
437
+ main().then((code) => {
438
+ if (code !== 0) {
439
+ process.exit(code);
440
+ }
441
+ }).catch((err) => {
442
+ console.error("Unexpected error:", err);
443
+ process.exit(1);
444
+ });
445
+ export {
446
+ main
447
+ };
448
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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 } 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 * 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\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\nfunction 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/**\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 } from './lib/pid.js';\r\nimport {\r\n isProcessAlive,\r\n killProcess,\r\n waitForProcessToDie,\r\n spawnCommand,\r\n setupSignalHandlers,\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 if (!isProcessAlive(pid)) {\r\n log(`Process ${name} (PID: ${pid}) is not running, cleaning up PID file`, options);\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 if (isProcessAlive(existingPid)) {\r\n log(`Killing existing process ${name} (PID: ${existingPid})...`, options);\r\n killProcess(existingPid);\r\n await waitForProcessToDie(existingPid);\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":";;;AA2BA,IAAM,kBAAkB;AACxB,IAAM,kBAAkB;AAMxB,SAAS,YAAY,MAAuB;AAC1C,MAAI,CAAC,QAAQ,KAAK,SAAS,iBAAiB;AAC1C,WAAO;AAAA,EACT;AAEA,MAAI,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,IAAI,GAAG;AACpE,WAAO;AAAA,EACT;AAEA,MAAI,WAAW,KAAK,IAAI,GAAG;AACzB,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAMA,SAAS,cAAc,KAAsB;AAC3C,MAAI,CAAC,OAAO,IAAI,SAAS,MAAM;AAC7B,WAAO;AAAA,EACT;AAEA,MAAI,IAAI,SAAS,IAAI,GAAG;AACtB,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKO,SAAS,UAAU,MAA6B;AACrD,QAAM,UAAsB;AAAA,IAC1B,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,MAAM;AAAA,IACN,SAAS;AAAA,IACT,SAAS,CAAC;AAAA,EACZ;AAEA,MAAI,IAAI;AACR,SAAO,IAAI,KAAK,QAAQ;AAEtB,UAAM,MAAM,KAAK,CAAC;AAGlB,QAAI,QAAQ,MAAM;AAChB,cAAQ,UAAU,KAAK,MAAM,IAAI,CAAC;AAClC;AAAA,IACF;AAGA,QAAI,QAAQ,YAAY,QAAQ,MAAM;AACpC,cAAQ,OAAO;AACf;AACA;AAAA,IACF;AAGA,QAAI,QAAQ,eAAe,QAAQ,MAAM;AACvC,cAAQ,UAAU;AAClB;AACA;AAAA,IACF;AAGA,QAAI,QAAQ,YAAY,QAAQ,MAAM;AACpC,cAAQ,OAAO;AACf;AACA;AAAA,IACF;AAGA,QAAI,QAAQ,aAAa,QAAQ,MAAM;AACrC,cAAQ,QAAQ;AAChB;AACA;AAAA,IACF;AAGA,QAAI,QAAQ,YAAY,QAAQ,MAAM;AACpC,YAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,UAAI,CAAC,SAAS,MAAM,WAAW,GAAG,GAAG;AACnC,eAAO,EAAE,SAAS,OAAO,OAAO,iCAAiC;AAAA,MACnE;AACA,UAAI,CAAC,YAAY,KAAK,GAAG;AACvB,eAAO,EAAE,SAAS,OAAO,OAAO,gEAAgE;AAAA,MAClG;AACA,cAAQ,OAAO;AACf,WAAK;AACL;AAAA,IACF;AAGA,QAAI,QAAQ,YAAY,QAAQ,MAAM;AACpC,YAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,UAAI,CAAC,SAAS,MAAM,WAAW,GAAG,GAAG;AACnC,eAAO,EAAE,SAAS,OAAO,OAAO,iCAAiC;AAAA,MACnE;AACA,UAAI,CAAC,YAAY,KAAK,GAAG;AACvB,eAAO,EAAE,SAAS,OAAO,OAAO,gEAAgE;AAAA,MAClG;AACA,cAAQ,OAAO;AACf,WAAK;AACL;AAAA,IACF;AAGA,QAAI,QAAQ,eAAe,QAAQ,MAAM;AACvC,YAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,UAAI,CAAC,SAAS,MAAM,WAAW,GAAG,GAAG;AACnC,eAAO,EAAE,SAAS,OAAO,OAAO,oCAAoC;AAAA,MACtE;AACA,UAAI,CAAC,cAAc,KAAK,GAAG;AACzB,eAAO,EAAE,SAAS,OAAO,OAAO,mEAAmE;AAAA,MACrG;AACA,cAAQ,SAAS;AACjB,WAAK;AACL;AAAA,IACF;AAGA,QAAI,IAAI,WAAW,GAAG,GAAG;AACvB,aAAO,EAAE,SAAS,OAAO,OAAO,mBAAmB,GAAG,GAAG;AAAA,IAC3D;AAGA,WAAO,EAAE,SAAS,OAAO,OAAO,wBAAwB,GAAG,GAAG;AAAA,EAChE;AAEA,SAAO,EAAE,SAAS,MAAM,QAAQ;AAClC;AAKO,SAAS,gBAAgB,SAAkC;AAEhE,MAAI,QAAQ,QAAQ,QAAQ,SAAS;AACnC,WAAO,EAAE,SAAS,MAAM,QAAQ;AAAA,EAClC;AAGA,MAAI,QAAQ,MAAM;AAChB,WAAO,EAAE,SAAS,MAAM,QAAQ;AAAA,EAClC;AAGA,MAAI,QAAQ,MAAM;AAChB,WAAO,EAAE,SAAS,MAAM,QAAQ;AAAA,EAClC;AAGA,MAAI,CAAC,QAAQ,MAAM;AACjB,WAAO,EAAE,SAAS,OAAO,OAAO,mDAAmD;AAAA,EACrF;AAEA,MAAI,QAAQ,QAAQ,WAAW,GAAG;AAChC,WAAO,EAAE,SAAS,OAAO,OAAO,6DAA6D;AAAA,EAC/F;AAEA,SAAO,EAAE,SAAS,MAAM,QAAQ;AAClC;AAKO,SAAS,cAAsB;AACpC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6BT;;;ACxOA,SAAS,cAAc,eAAe,YAAY,YAAY,WAAW,mBAAmB;AAC5F,SAAS,MAAM,eAAe;AAWvB,SAAS,eAAe,MAAc,QAAwB;AACnE,SAAO,KAAK,QAAQ,GAAG,IAAI,MAAM;AACnC;AAMO,SAAS,QAAQ,MAAc,QAA+B;AACnE,QAAM,UAAU,eAAe,MAAM,MAAM;AAE3C,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,UAAU,aAAa,SAAS,MAAM,EAAE,KAAK;AACnD,UAAM,MAAM,SAAS,SAAS,EAAE;AAEhC,QAAI,MAAM,GAAG,KAAK,OAAO,GAAG;AAC1B,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,SAAS,MAAc,KAAa,QAAsB;AACxE,QAAM,UAAU,eAAe,MAAM,MAAM;AAC3C,QAAM,MAAM,QAAQ,OAAO;AAE3B,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AAEA,gBAAc,SAAS,OAAO,GAAG,GAAG,MAAM;AAC5C;AAMO,SAAS,UAAU,MAAc,QAAyB;AAC/D,QAAM,UAAU,eAAe,MAAM,MAAM;AAE3C,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,eAAW,OAAO;AAClB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,SAAS,QAA2B;AAClD,MAAI,CAAC,WAAW,MAAM,GAAG;AACvB,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,QAAQ,YAAY,MAAM;AAChC,QAAM,WAAW,MAAM,OAAO,OAAK,EAAE,SAAS,MAAM,CAAC;AAErD,SAAO,SAAS,IAAI,UAAQ;AAE1B,UAAM,OAAO,KAAK,MAAM,GAAG,EAAE;AAC7B,UAAM,MAAM,QAAQ,MAAM,MAAM;AAEhC,WAAO;AAAA,MACL;AAAA,MACA,KAAK,OAAO;AAAA,MACZ,QAAQ,QAAQ;AAAA,IAClB;AAAA,EACF,CAAC;AACH;;;AClGA,SAAS,OAAO,gBAA8B;AAE9C,IAAM,YAAY,QAAQ,aAAa;AAGvC,IAAM,0BAA0B;AAChC,IAAM,oBAAoB;AAK1B,SAAS,WAAW,KAAsB;AACxC,SAAO,OAAO,UAAU,GAAG,KAAK,MAAM,KAAK,OAAO;AACpD;AAKO,SAAS,eAAe,KAAsB;AACnD,MAAI;AACF,QAAI,CAAC,WAAW,GAAG,GAAG;AACpB,aAAO;AAAA,IACT;AACA,QAAI,WAAW;AAGb,YAAM,SAAS,SAAS,wBAAwB,GAAG,SAAS;AAAA,QAC1D,UAAU;AAAA,QACV,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,MAChC,CAAC;AACD,aAAO,OAAO,SAAS,OAAO,GAAG,CAAC;AAAA,IACpC,OAAO;AAEL,cAAQ,KAAK,KAAK,CAAC;AACnB,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,YAAY,KAAsB;AAChD,MAAI,CAAC,WAAW,GAAG,KAAK,CAAC,eAAe,GAAG,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,MAAI;AACF,QAAI,WAAW;AAGb,eAAS,iBAAiB,GAAG,UAAU;AAAA,QACrC,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,MAChC,CAAC;AAAA,IACH,OAAO;AAGL,YAAM,SAAS,YAAY,CAAC,GAAG,KAAK,YAAY,GAAG;AACnD,UAAI,CAAC,QAAQ;AACX,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,SAAS,YAAY,KAAsB;AACzC,MAAI;AACF,YAAQ,KAAK,KAAK,SAAS;AAC3B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOA,eAAsB,oBACpB,KACA,YAAoB,yBACF;AAClB,QAAM,YAAY,KAAK,IAAI;AAE3B,SAAO,KAAK,IAAI,IAAI,YAAY,WAAW;AACzC,QAAI,CAAC,eAAe,GAAG,GAAG;AACxB,aAAO;AAAA,IACT;AACA,UAAM,IAAI,QAAQ,aAAW,WAAW,SAAS,iBAAiB,CAAC;AAAA,EACrE;AAEA,SAAO,CAAC,eAAe,GAAG;AAC5B;AAUO,SAAS,aAAa,SAAiB,MAA6B;AAGzE,QAAM,WAAW,YAAY,GAAG,OAAO,IAAI,KAAK,KAAK,GAAG,CAAC,KAAK;AAC9D,QAAM,YAAY,YAAY,CAAC,IAAI;AAEnC,QAAM,QAAQ,MAAM,UAAU,WAAW;AAAA,IACvC,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU,CAAC;AAAA,EACb,CAAC;AAED,MAAI,MAAM,QAAQ,QAAW;AAC3B,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AAEA,SAAO;AAAA,IACL;AAAA,IACA,KAAK,MAAM;AAAA,EACb;AACF;AAOO,SAAS,oBAAoB,OAAqB,QAA2B;AAClF,QAAM,eAAe,CAAC,YAA4B;AAChD,QAAI,MAAM,OAAO,WAAW,MAAM,GAAG,GAAG;AACtC,UAAI,WAAW;AACb,YAAI;AAEF,mBAAS,iBAAiB,MAAM,GAAG,UAAU;AAAA,YAC3C,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,UAChC,CAAC;AAAA,QACH,QAAQ;AAAA,QAER;AAAA,MACF,OAAO;AAEL,cAAM,KAAK,SAAS;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AAGA,UAAQ,GAAG,UAAU,MAAM,aAAa,QAAQ,CAAC;AACjD,UAAQ,GAAG,WAAW,MAAM,aAAa,SAAS,CAAC;AAEnD,QAAM,GAAG,QAAQ,CAAC,MAAM,WAAW;AACjC,QAAI,QAAQ;AACV,aAAO;AAAA,IACT;AACA,QAAI,QAAQ;AACV,cAAQ,KAAK,OAAO,WAAW,YAAY,KAAK,WAAW,WAAW,IAAI,EAAE;AAAA,IAC9E;AACA,YAAQ,KAAK,QAAQ,CAAC;AAAA,EACxB,CAAC;AAED,QAAM,GAAG,SAAS,SAAO;AACvB,YAAQ,MAAM,4BAA4B,IAAI,OAAO,EAAE;AACvD,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;;;ACpKA,IAAM,UAAU;AAEhB,SAAS,IAAI,SAAiB,SAA2B;AACvD,MAAI,CAAC,QAAQ,OAAO;AAClB,YAAQ,IAAI,OAAO;AAAA,EACrB;AACF;AAEA,SAAS,SAAS,SAAuB;AACvC,UAAQ,MAAM,OAAO;AACvB;AAEA,eAAe,WAAW,MAAc,SAAsC;AAC5E,QAAM,MAAM,QAAQ,MAAM,QAAQ,MAAM;AAExC,MAAI,QAAQ,MAAM;AAChB,QAAI,+BAA+B,IAAI,IAAI,OAAO;AAClD,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,eAAe,GAAG,GAAG;AACxB,QAAI,WAAW,IAAI,UAAU,GAAG,0CAA0C,OAAO;AACjF,cAAU,MAAM,QAAQ,MAAM;AAC9B,WAAO;AAAA,EACT;AAEA,MAAI,mBAAmB,IAAI,UAAU,GAAG,QAAQ,OAAO;AACvD,QAAM,SAAS,YAAY,GAAG;AAE9B,MAAI,QAAQ;AACV,UAAM,oBAAoB,GAAG;AAC7B,cAAU,MAAM,QAAQ,MAAM;AAC9B,QAAI,WAAW,IAAI,WAAW,OAAO;AACrC,WAAO;AAAA,EACT,OAAO;AACL,aAAS,0BAA0B,IAAI,UAAU,GAAG,GAAG;AACvD,WAAO;AAAA,EACT;AACF;AAEA,SAAS,WAAW,SAA6B;AAC/C,QAAM,OAAO,SAAS,QAAQ,MAAM;AAEpC,MAAI,KAAK,WAAW,GAAG;AACrB,QAAI,wBAAwB,OAAO;AACnC,WAAO;AAAA,EACT;AAEA,MAAI,sBAAsB,OAAO;AACjC,aAAW,QAAQ,MAAM;AACvB,UAAM,SAAS,KAAK,UAAU,eAAe,KAAK,GAAG,IAAI,YAAY;AACrE,UAAM,SAAS,KAAK,MAAM,IAAI,OAAO,KAAK,GAAG,IAAI;AACjD,QAAI,KAAK,KAAK,IAAI,SAAS,MAAM,KAAK,MAAM,KAAK,OAAO;AAAA,EAC1D;AAEA,SAAO;AACT;AAEA,eAAe,UAAU,SAAsC;AAC7D,QAAM,OAAO,QAAQ;AACrB,QAAM,CAAC,SAAS,GAAG,IAAI,IAAI,QAAQ;AAEnC,MAAI,CAAC,SAAS;AACZ,aAAS,sBAAsB;AAC/B,WAAO;AAAA,EACT;AAGA,QAAM,cAAc,QAAQ,MAAM,QAAQ,MAAM;AAChD,MAAI,gBAAgB,MAAM;AACxB,QAAI,eAAe,WAAW,GAAG;AAC/B,UAAI,4BAA4B,IAAI,UAAU,WAAW,QAAQ,OAAO;AACxE,kBAAY,WAAW;AACvB,YAAM,oBAAoB,WAAW;AAAA,IACvC;AACA,cAAU,MAAM,QAAQ,MAAM;AAAA,EAChC;AAGA,MAAI,aAAa,OAAO,IAAI,KAAK,KAAK,GAAG,CAAC,IAAI,OAAO;AAErD,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,IAAI,aAAa,SAAS,IAAI;AAGjD,aAAS,MAAM,KAAK,QAAQ,MAAM;AAClC,QAAI,6BAA6B,GAAG,IAAI,OAAO;AAM/C,wBAAoB,KAAK;AAIzB,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,aAAS,4BAA4B,OAAO,EAAE;AAC9C,WAAO;AAAA,EACT;AACF;AAEA,eAAe,OAAwB;AACrC,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AAGjC,QAAM,cAAc,UAAU,IAAI;AAClC,MAAI,CAAC,YAAY,SAAS;AACxB,aAAS,UAAU,YAAY,KAAK,EAAE;AACtC,aAAS,kCAAkC;AAC3C,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,YAAY;AAG5B,QAAM,iBAAiB,gBAAgB,OAAO;AAC9C,MAAI,CAAC,eAAe,SAAS;AAC3B,aAAS,UAAU,eAAe,KAAK,EAAE;AACzC,aAAS,kCAAkC;AAC3C,WAAO;AAAA,EACT;AAGA,MAAI,QAAQ,MAAM;AAChB,YAAQ,IAAI,YAAY,CAAC;AACzB,WAAO;AAAA,EACT;AAGA,MAAI,QAAQ,SAAS;AACnB,YAAQ,IAAI,aAAa,OAAO,EAAE;AAClC,WAAO;AAAA,EACT;AAGA,MAAI,QAAQ,MAAM;AAChB,WAAO,WAAW,OAAO;AAAA,EAC3B;AAGA,MAAI,QAAQ,MAAM;AAChB,WAAO,MAAM,WAAW,QAAQ,MAAM,OAAO;AAAA,EAC/C;AAGA,SAAO,MAAM,UAAU,OAAO;AAChC;AAGA,KAAK,EACF,KAAK,UAAQ;AAGZ,MAAI,SAAS,GAAG;AACd,YAAQ,KAAK,IAAI;AAAA,EACnB;AACF,CAAC,EACA,MAAM,SAAO;AACZ,UAAQ,MAAM,qBAAqB,GAAG;AACtC,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@radleta/just-one",
3
+ "version": "0.1.0",
4
+ "description": "A CLI tool that ensures only one instance of a command runs at a time",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "just-one": "bin/just-one.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ },
21
+ "scripts": {
22
+ "dev": "tsup --watch",
23
+ "build": "tsup",
24
+ "clean": "rm -rf dist",
25
+ "prebuild": "npm run clean",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "test:coverage": "vitest run --coverage",
29
+ "lint": "eslint src",
30
+ "format": "prettier --write \"src/**/*.ts\" \"*.{json,md,yml,yaml}\"",
31
+ "format:check": "prettier --check \"src/**/*.ts\" \"*.{json,md,yml,yaml}\"",
32
+ "typecheck": "tsc --noEmit",
33
+ "validate": "npm run lint && npm run typecheck && npm run test",
34
+ "pack:dry": "npm pack --dry-run",
35
+ "size:check": "npm pack --dry-run 2>&1 | grep 'package size' | head -1 || echo 'Run npm pack to see size'",
36
+ "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",
40
+ "prepublishOnly": "npm run validate && npm run build",
41
+ "prepare": "husky"
42
+ },
43
+ "keywords": [
44
+ "cli",
45
+ "process",
46
+ "singleton",
47
+ "pid",
48
+ "kill",
49
+ "dev-server",
50
+ "storybook",
51
+ "vite",
52
+ "webpack"
53
+ ],
54
+ "homepage": "https://github.com/radleta/just-one#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/radleta/just-one/issues"
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/radleta/just-one.git"
61
+ },
62
+ "author": {
63
+ "name": "Richard Adleta",
64
+ "email": "radleta@gmail.com",
65
+ "url": "https://github.com/radleta"
66
+ },
67
+ "license": "MIT",
68
+ "devDependencies": {
69
+ "@types/node": "^20.0.0",
70
+ "@typescript-eslint/eslint-plugin": "^8.20.0",
71
+ "@typescript-eslint/parser": "^8.20.0",
72
+ "@vitest/coverage-v8": "^3.0.0",
73
+ "eslint": "^9.18.0",
74
+ "husky": "^9.1.7",
75
+ "lint-staged": "^16.2.4",
76
+ "prettier": "^3.4.2",
77
+ "tsup": "^8.0.0",
78
+ "typescript": "^5.8.3",
79
+ "vitest": "^3.0.0"
80
+ },
81
+ "files": [
82
+ "dist/",
83
+ "bin/",
84
+ "README.md",
85
+ "CHANGELOG.md",
86
+ "LICENSE"
87
+ ],
88
+ "publishConfig": {
89
+ "access": "public",
90
+ "registry": "https://registry.npmjs.org/"
91
+ },
92
+ "lint-staged": {
93
+ "*.ts": [
94
+ "eslint --fix",
95
+ "prettier --write"
96
+ ],
97
+ "*.{json,md,yml,yaml}": [
98
+ "prettier --write"
99
+ ]
100
+ }
101
+ }