@radleta/just-one 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - PID reuse protection: verifies process identity before killing by comparing PID file modification time with process start time
13
+ - New dependency: [pidusage](https://github.com/soyuka/pidusage) for cross-platform process metrics
14
+
15
+ ### Changed
16
+
17
+ - `handleRun()` and `handleKill()` now verify process identity before killing
18
+ - Updated documentation to reflect PID reuse protection feature
19
+
20
+ ### Fixed
21
+
22
+ - E2E test "kills previous instance when starting new one" no longer asserts PID uniqueness (PIDs can be reused by the OS)
23
+ - Husky pre-commit hook updated to v9 format (removed deprecated `husky.sh` sourcing)
24
+
8
25
  ## [0.1.0] - 2026-01-22
9
26
 
10
27
  ### Added
package/README.md CHANGED
@@ -22,8 +22,9 @@ Existing solutions have drawbacks:
22
22
  - **Named process tracking** - Each process gets a unique name for precise targeting
23
23
  - **Automatic cleanup** - Previous instance killed before starting new one
24
24
  - **Cross-platform** - Works on Windows, macOS, and Linux
25
- - **Zero dependencies** - Uses only Node.js built-ins
25
+ - **Minimal dependencies** - Only [pidusage](https://github.com/soyuka/pidusage) for process verification
26
26
  - **PID file management** - Survives terminal closes and system restarts
27
+ - **PID reuse protection** - Verifies process identity before killing to prevent accidents
27
28
 
28
29
  ## Installation
29
30
 
@@ -110,9 +111,18 @@ just-one -n storybook -d /tmp -- npx storybook dev
110
111
  ```
111
112
 
112
113
  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
114
+ 2. If yes, verify it's the same process we started (by comparing start times)
115
+ 3. If verified, kill that specific process (and its children)
116
+ 4. Start the new process
117
+ 5. Save its PID for next time
118
+
119
+ ### PID Reuse Protection
120
+
121
+ Operating systems can reuse PIDs after a process terminates. To prevent accidentally killing an unrelated process that received the same PID, `just-one` compares:
122
+ - The PID file's modification time (when we recorded the PID)
123
+ - The process's actual start time (from the OS)
124
+
125
+ If these don't match within 5 seconds, the PID file is considered stale and the process is not killed.
116
126
 
117
127
  ### Cross-Platform Process Handling
118
128
 
@@ -139,10 +149,11 @@ just-one -n storybook-docs -- storybook dev -p 6007
139
149
  | Feature | just-one | kill-port | pm2 |
140
150
  |---------|----------|-----------|-----|
141
151
  | Kills by PID (precise) | Yes | No (by port) | Yes |
152
+ | PID reuse protection | Yes | No | No |
142
153
  | Cross-platform | Yes | Yes | Yes |
143
154
  | Zero config | Yes | Yes | No |
144
155
  | Remembers processes | Yes (PID file) | No | Yes (daemon) |
145
- | Lightweight | ~150 LOC | ~100 LOC | Heavy |
156
+ | Lightweight | Yes (1 dep) | Yes | Heavy |
146
157
  | Daemon required | No | No | Yes |
147
158
 
148
159
  ## Requirements
package/bin/just-one.js CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import '../dist/index.js';
package/dist/index.js CHANGED
@@ -156,7 +156,7 @@ Examples:
156
156
  }
157
157
 
158
158
  // src/lib/pid.ts
159
- import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync } from "fs";
159
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
160
160
  import { join, dirname } from "path";
161
161
  function getPidFilePath(name, pidDir) {
162
162
  return join(pidDir, `${name}.pid`);
@@ -197,6 +197,15 @@ function deletePid(name, pidDir) {
197
197
  return false;
198
198
  }
199
199
  }
200
+ function getPidFileMtime(name, pidDir) {
201
+ const pidFile = getPidFilePath(name, pidDir);
202
+ try {
203
+ const stats = statSync(pidFile);
204
+ return stats.mtimeMs;
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
200
209
  function listPids(pidDir) {
201
210
  if (!existsSync(pidDir)) {
202
211
  return [];
@@ -216,12 +225,33 @@ function listPids(pidDir) {
216
225
 
217
226
  // src/lib/process.ts
218
227
  import { spawn, execSync } from "child_process";
228
+ import pidusage from "pidusage";
219
229
  var isWindows = process.platform === "win32";
220
230
  var DEFAULT_WAIT_TIMEOUT_MS = 2e3;
221
231
  var CHECK_INTERVAL_MS = 100;
222
232
  function isValidPid(pid) {
223
233
  return Number.isInteger(pid) && pid > 0 && pid <= 4194304;
224
234
  }
235
+ var START_TIME_TOLERANCE_MS = 5e3;
236
+ async function getProcessStartTime(pid) {
237
+ if (!isValidPid(pid)) {
238
+ return null;
239
+ }
240
+ try {
241
+ const stats = await pidusage(pid);
242
+ return stats.timestamp - stats.elapsed;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ async function isSameProcessInstance(pid, pidFileMtimeMs) {
248
+ const processStartTime = await getProcessStartTime(pid);
249
+ if (processStartTime === null) {
250
+ return false;
251
+ }
252
+ const diff = Math.abs(processStartTime - pidFileMtimeMs);
253
+ return diff <= START_TIME_TOLERANCE_MS;
254
+ }
225
255
  function isProcessAlive(pid) {
226
256
  try {
227
257
  if (!isValidPid(pid)) {
@@ -343,8 +373,14 @@ async function handleKill(name, options) {
343
373
  log(`No process found with name: ${name}`, options);
344
374
  return 0;
345
375
  }
346
- if (!isProcessAlive(pid)) {
347
- log(`Process ${name} (PID: ${pid}) is not running, cleaning up PID file`, options);
376
+ const pidFileMtime = getPidFileMtime(name, options.pidDir);
377
+ const isSameInstance = pidFileMtime !== null && await isSameProcessInstance(pid, pidFileMtime);
378
+ if (!isSameInstance) {
379
+ if (isProcessAlive(pid)) {
380
+ log(`PID ${pid} belongs to a different process, not killing`, options);
381
+ } else {
382
+ log(`Process ${name} (PID: ${pid}) is not running, cleaning up PID file`, options);
383
+ }
348
384
  deletePid(name, options.pidDir);
349
385
  return 0;
350
386
  }
@@ -383,10 +419,17 @@ async function handleRun(options) {
383
419
  }
384
420
  const existingPid = readPid(name, options.pidDir);
385
421
  if (existingPid !== null) {
386
- if (isProcessAlive(existingPid)) {
422
+ const pidFileMtime = getPidFileMtime(name, options.pidDir);
423
+ const shouldKill = pidFileMtime !== null && await isSameProcessInstance(existingPid, pidFileMtime);
424
+ if (shouldKill) {
387
425
  log(`Killing existing process ${name} (PID: ${existingPid})...`, options);
388
426
  killProcess(existingPid);
389
427
  await waitForProcessToDie(existingPid);
428
+ } else if (isProcessAlive(existingPid)) {
429
+ log(
430
+ `Stale PID file detected (PID ${existingPid} belongs to different process), skipping kill`,
431
+ options
432
+ );
390
433
  }
391
434
  deletePid(name, options.pidDir);
392
435
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/cli.ts","../src/lib/pid.ts","../src/lib/process.ts","../src/index.ts"],"sourcesContent":["/**\r\n * CLI argument parsing for just-one\r\n */\r\n\r\nexport interface CliOptions {\r\n name?: string;\r\n kill?: string;\r\n list: boolean;\r\n pidDir: string;\r\n quiet: boolean;\r\n help: boolean;\r\n version: boolean;\r\n command: string[];\r\n}\r\n\r\nexport interface ParseResult {\r\n success: true;\r\n options: CliOptions;\r\n}\r\n\r\nexport interface ParseError {\r\n success: false;\r\n error: string;\r\n}\r\n\r\nexport type ParseOutput = ParseResult | ParseError;\r\n\r\nconst DEFAULT_PID_DIR = '.just-one';\r\nconst MAX_NAME_LENGTH = 255;\r\n\r\n/**\r\n * Validate a process name for safe file operations\r\n * Rejects names containing path separators or traversal sequences\r\n */\r\nfunction isValidName(name: string): boolean {\r\n if (!name || name.length > MAX_NAME_LENGTH) {\r\n return false;\r\n }\r\n // Reject path separators and traversal sequences\r\n if (name.includes('/') || name.includes('\\\\') || name.includes('..')) {\r\n return false;\r\n }\r\n // Reject names that are only dots or whitespace\r\n if (/^[\\s.]*$/.test(name)) {\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * Validate a PID directory path for safe file operations\r\n * Rejects paths containing traversal sequences\r\n */\r\nfunction isValidPidDir(dir: string): boolean {\r\n if (!dir || dir.length > 1024) {\r\n return false;\r\n }\r\n // Reject path traversal sequences\r\n if (dir.includes('..')) {\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * Parse command line arguments\r\n */\r\nexport function parseArgs(args: string[]): ParseOutput {\r\n const options: CliOptions = {\r\n name: undefined,\r\n kill: undefined,\r\n list: false,\r\n pidDir: DEFAULT_PID_DIR,\r\n quiet: false,\r\n help: false,\r\n version: false,\r\n command: [],\r\n };\r\n\r\n let i = 0;\r\n while (i < args.length) {\r\n // TypeScript requires this check due to noUncheckedIndexedAccess\r\n const arg = args[i]!;\r\n\r\n // Everything after -- is the command\r\n if (arg === '--') {\r\n options.command = args.slice(i + 1);\r\n break;\r\n }\r\n\r\n // Help\r\n if (arg === '--help' || arg === '-h') {\r\n options.help = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // Version\r\n if (arg === '--version' || arg === '-v') {\r\n options.version = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // List\r\n if (arg === '--list' || arg === '-l') {\r\n options.list = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // Quiet\r\n if (arg === '--quiet' || arg === '-q') {\r\n options.quiet = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // Name (requires value)\r\n if (arg === '--name' || arg === '-n') {\r\n const value = args[i + 1];\r\n if (!value || value.startsWith('-')) {\r\n return { success: false, error: 'Option --name requires a value' };\r\n }\r\n if (!isValidName(value)) {\r\n return { success: false, error: 'Invalid name: must not contain path separators or be too long' };\r\n }\r\n options.name = value;\r\n i += 2;\r\n continue;\r\n }\r\n\r\n // Kill (requires value)\r\n if (arg === '--kill' || arg === '-k') {\r\n const value = args[i + 1];\r\n if (!value || value.startsWith('-')) {\r\n return { success: false, error: 'Option --kill requires a value' };\r\n }\r\n if (!isValidName(value)) {\r\n return { success: false, error: 'Invalid name: must not contain path separators or be too long' };\r\n }\r\n options.kill = value;\r\n i += 2;\r\n continue;\r\n }\r\n\r\n // PID directory (requires value)\r\n if (arg === '--pid-dir' || arg === '-d') {\r\n const value = args[i + 1];\r\n if (!value || value.startsWith('-')) {\r\n return { success: false, error: 'Option --pid-dir requires a value' };\r\n }\r\n if (!isValidPidDir(value)) {\r\n return { success: false, error: 'Invalid PID directory: must not contain path traversal sequences' };\r\n }\r\n options.pidDir = value;\r\n i += 2;\r\n continue;\r\n }\r\n\r\n // Unknown option\r\n if (arg.startsWith('-')) {\r\n return { success: false, error: `Unknown option: ${arg}` };\r\n }\r\n\r\n // Unexpected positional argument\r\n return { success: false, error: `Unexpected argument: ${arg}` };\r\n }\r\n\r\n return { success: true, options };\r\n}\r\n\r\n/**\r\n * Validate parsed options\r\n */\r\nexport function validateOptions(options: CliOptions): ParseOutput {\r\n // Help and version don't need validation\r\n if (options.help || options.version) {\r\n return { success: true, options };\r\n }\r\n\r\n // List doesn't need name or command\r\n if (options.list) {\r\n return { success: true, options };\r\n }\r\n\r\n // Kill only needs a name\r\n if (options.kill) {\r\n return { success: true, options };\r\n }\r\n\r\n // Running a command requires both name and command\r\n if (!options.name) {\r\n return { success: false, error: 'Option --name is required when running a command' };\r\n }\r\n\r\n if (options.command.length === 0) {\r\n return { success: false, error: 'No command specified. Use: just-one -n <name> -- <command>' };\r\n }\r\n\r\n return { success: true, options };\r\n}\r\n\r\n/**\r\n * Get help text\r\n */\r\nexport function getHelpText(): string {\r\n return `just-one - Ensure only one instance of a command runs at a time\r\n\r\nUsage:\r\n just-one -n <name> -- <command> Run command, killing any previous instance\r\n just-one -k <name> Kill a named process\r\n just-one -l List all tracked processes\r\n\r\nOptions:\r\n -n, --name <name> Name to identify this process (required for running)\r\n -k, --kill <name> Kill the named process and exit\r\n -l, --list List all tracked processes and their status\r\n -d, --pid-dir <dir> Directory for PID files (default: .just-one/)\r\n -q, --quiet Suppress output\r\n -h, --help Show this help message\r\n -v, --version Show version number\r\n\r\nExamples:\r\n # Run storybook, killing any previous instance\r\n just-one -n storybook -- npx storybook dev -p 6006\r\n\r\n # Run vite dev server\r\n just-one -n vite -- npm run dev\r\n\r\n # Kill a named process\r\n just-one -k storybook\r\n\r\n # List all tracked processes\r\n just-one -l\r\n`;\r\n}\r\n","/**\r\n * PID file operations for just-one\r\n */\r\n\r\nimport { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync } 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":"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","names":[]}
1
+ {"version":3,"sources":["../src/lib/cli.ts","../src/lib/pid.ts","../src/lib/process.ts","../src/index.ts"],"sourcesContent":["/**\r\n * CLI argument parsing for just-one\r\n */\r\n\r\nexport interface CliOptions {\r\n name?: string;\r\n kill?: string;\r\n list: boolean;\r\n pidDir: string;\r\n quiet: boolean;\r\n help: boolean;\r\n version: boolean;\r\n command: string[];\r\n}\r\n\r\nexport interface ParseResult {\r\n success: true;\r\n options: CliOptions;\r\n}\r\n\r\nexport interface ParseError {\r\n success: false;\r\n error: string;\r\n}\r\n\r\nexport type ParseOutput = ParseResult | ParseError;\r\n\r\nconst DEFAULT_PID_DIR = '.just-one';\r\nconst MAX_NAME_LENGTH = 255;\r\n\r\n/**\r\n * Validate a process name for safe file operations\r\n * Rejects names containing path separators or traversal sequences\r\n */\r\nfunction isValidName(name: string): boolean {\r\n if (!name || name.length > MAX_NAME_LENGTH) {\r\n return false;\r\n }\r\n // Reject path separators and traversal sequences\r\n if (name.includes('/') || name.includes('\\\\') || name.includes('..')) {\r\n return false;\r\n }\r\n // Reject names that are only dots or whitespace\r\n if (/^[\\s.]*$/.test(name)) {\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * Validate a PID directory path for safe file operations\r\n * Rejects paths containing traversal sequences\r\n */\r\nfunction isValidPidDir(dir: string): boolean {\r\n if (!dir || dir.length > 1024) {\r\n return false;\r\n }\r\n // Reject path traversal sequences\r\n if (dir.includes('..')) {\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * Parse command line arguments\r\n */\r\nexport function parseArgs(args: string[]): ParseOutput {\r\n const options: CliOptions = {\r\n name: undefined,\r\n kill: undefined,\r\n list: false,\r\n pidDir: DEFAULT_PID_DIR,\r\n quiet: false,\r\n help: false,\r\n version: false,\r\n command: [],\r\n };\r\n\r\n let i = 0;\r\n while (i < args.length) {\r\n // TypeScript requires this check due to noUncheckedIndexedAccess\r\n const arg = args[i]!;\r\n\r\n // Everything after -- is the command\r\n if (arg === '--') {\r\n options.command = args.slice(i + 1);\r\n break;\r\n }\r\n\r\n // Help\r\n if (arg === '--help' || arg === '-h') {\r\n options.help = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // Version\r\n if (arg === '--version' || arg === '-v') {\r\n options.version = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // List\r\n if (arg === '--list' || arg === '-l') {\r\n options.list = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // Quiet\r\n if (arg === '--quiet' || arg === '-q') {\r\n options.quiet = true;\r\n i++;\r\n continue;\r\n }\r\n\r\n // Name (requires value)\r\n if (arg === '--name' || arg === '-n') {\r\n const value = args[i + 1];\r\n if (!value || value.startsWith('-')) {\r\n return { success: false, error: 'Option --name requires a value' };\r\n }\r\n if (!isValidName(value)) {\r\n return { success: false, error: 'Invalid name: must not contain path separators or be too long' };\r\n }\r\n options.name = value;\r\n i += 2;\r\n continue;\r\n }\r\n\r\n // Kill (requires value)\r\n if (arg === '--kill' || arg === '-k') {\r\n const value = args[i + 1];\r\n if (!value || value.startsWith('-')) {\r\n return { success: false, error: 'Option --kill requires a value' };\r\n }\r\n if (!isValidName(value)) {\r\n return { success: false, error: 'Invalid name: must not contain path separators or be too long' };\r\n }\r\n options.kill = value;\r\n i += 2;\r\n continue;\r\n }\r\n\r\n // PID directory (requires value)\r\n if (arg === '--pid-dir' || arg === '-d') {\r\n const value = args[i + 1];\r\n if (!value || value.startsWith('-')) {\r\n return { success: false, error: 'Option --pid-dir requires a value' };\r\n }\r\n if (!isValidPidDir(value)) {\r\n return { success: false, error: 'Invalid PID directory: must not contain path traversal sequences' };\r\n }\r\n options.pidDir = value;\r\n i += 2;\r\n continue;\r\n }\r\n\r\n // Unknown option\r\n if (arg.startsWith('-')) {\r\n return { success: false, error: `Unknown option: ${arg}` };\r\n }\r\n\r\n // Unexpected positional argument\r\n return { success: false, error: `Unexpected argument: ${arg}` };\r\n }\r\n\r\n return { success: true, options };\r\n}\r\n\r\n/**\r\n * Validate parsed options\r\n */\r\nexport function validateOptions(options: CliOptions): ParseOutput {\r\n // Help and version don't need validation\r\n if (options.help || options.version) {\r\n return { success: true, options };\r\n }\r\n\r\n // List doesn't need name or command\r\n if (options.list) {\r\n return { success: true, options };\r\n }\r\n\r\n // Kill only needs a name\r\n if (options.kill) {\r\n return { success: true, options };\r\n }\r\n\r\n // Running a command requires both name and command\r\n if (!options.name) {\r\n return { success: false, error: 'Option --name is required when running a command' };\r\n }\r\n\r\n if (options.command.length === 0) {\r\n return { success: false, error: 'No command specified. Use: just-one -n <name> -- <command>' };\r\n }\r\n\r\n return { success: true, options };\r\n}\r\n\r\n/**\r\n * Get help text\r\n */\r\nexport function getHelpText(): string {\r\n return `just-one - Ensure only one instance of a command runs at a time\r\n\r\nUsage:\r\n just-one -n <name> -- <command> Run command, killing any previous instance\r\n just-one -k <name> Kill a named process\r\n just-one -l List all tracked processes\r\n\r\nOptions:\r\n -n, --name <name> Name to identify this process (required for running)\r\n -k, --kill <name> Kill the named process and exit\r\n -l, --list List all tracked processes and their status\r\n -d, --pid-dir <dir> Directory for PID files (default: .just-one/)\r\n -q, --quiet Suppress output\r\n -h, --help Show this help message\r\n -v, --version Show version number\r\n\r\nExamples:\r\n # Run storybook, killing any previous instance\r\n just-one -n storybook -- npx storybook dev -p 6006\r\n\r\n # Run vite dev server\r\n just-one -n vite -- npm run dev\r\n\r\n # Kill a named process\r\n just-one -k storybook\r\n\r\n # List all tracked processes\r\n just-one -l\r\n`;\r\n}\r\n","/**\r\n * PID file operations for just-one\r\n */\r\n\r\nimport { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';\r\nimport { join, dirname } from 'path';\r\n\r\nexport interface PidInfo {\r\n name: string;\r\n pid: number;\r\n exists: boolean;\r\n}\r\n\r\n/**\r\n * Get the path to a PID file for a given name\r\n */\r\nexport function getPidFilePath(name: string, pidDir: string): string {\r\n return join(pidDir, `${name}.pid`);\r\n}\r\n\r\n/**\r\n * Read the PID from a PID file\r\n * Returns null if the file doesn't exist or is invalid\r\n */\r\nexport function readPid(name: string, pidDir: string): number | null {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n\r\n if (!existsSync(pidFile)) {\r\n return null;\r\n }\r\n\r\n try {\r\n const content = readFileSync(pidFile, 'utf8').trim();\r\n const pid = parseInt(content, 10);\r\n\r\n if (isNaN(pid) || pid <= 0) {\r\n return null;\r\n }\r\n\r\n return pid;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Write a PID to a PID file\r\n * Creates the directory if it doesn't exist\r\n */\r\nexport function writePid(name: string, pid: number, pidDir: string): void {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n const dir = dirname(pidFile);\r\n\r\n if (!existsSync(dir)) {\r\n mkdirSync(dir, { recursive: true });\r\n }\r\n\r\n writeFileSync(pidFile, String(pid), 'utf8');\r\n}\r\n\r\n/**\r\n * Delete a PID file\r\n * Returns true if the file was deleted, false if it didn't exist\r\n */\r\nexport function deletePid(name: string, pidDir: string): boolean {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n\r\n if (!existsSync(pidFile)) {\r\n return false;\r\n }\r\n\r\n try {\r\n unlinkSync(pidFile);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Get the modification time of a PID file as Unix timestamp (milliseconds)\r\n * Returns null if file doesn't exist\r\n */\r\nexport function getPidFileMtime(name: string, pidDir: string): number | null {\r\n const pidFile = getPidFilePath(name, pidDir);\r\n try {\r\n const stats = statSync(pidFile);\r\n return stats.mtimeMs;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * List all PID files in the directory\r\n * Returns information about each tracked process\r\n */\r\nexport function listPids(pidDir: string): PidInfo[] {\r\n if (!existsSync(pidDir)) {\r\n return [];\r\n }\r\n\r\n const files = readdirSync(pidDir);\r\n const pidFiles = files.filter(f => f.endsWith('.pid'));\r\n\r\n return pidFiles.map(file => {\r\n // Remove .pid suffix (use slice to only remove from end)\r\n const name = file.slice(0, -4);\r\n const pid = readPid(name, pidDir);\r\n\r\n return {\r\n name,\r\n pid: pid ?? 0,\r\n exists: pid !== null,\r\n };\r\n });\r\n}\r\n","/**\r\n * Cross-platform process handling for just-one\r\n */\r\n\r\nimport { spawn, execSync, ChildProcess } from 'child_process';\r\nimport pidusage from 'pidusage';\r\n\r\nconst isWindows = process.platform === 'win32';\r\n\r\n// Constants for process polling\r\nconst DEFAULT_WAIT_TIMEOUT_MS = 2000;\r\nconst CHECK_INTERVAL_MS = 100;\r\n\r\n/**\r\n * Validate that a PID is a safe positive integer for use in system calls\r\n */\r\nexport function isValidPid(pid: number): boolean {\r\n return Number.isInteger(pid) && pid > 0 && pid <= 4194304; // Max PID on most systems\r\n}\r\n\r\n// Tolerance for comparing PID file mtime with process start time\r\nconst START_TIME_TOLERANCE_MS = 5000; // 5 seconds\r\n\r\n/**\r\n * Get the start time of a process as Unix timestamp (milliseconds)\r\n * Returns null if process doesn't exist or start time can't be determined\r\n */\r\nexport async function getProcessStartTime(pid: number): Promise<number | null> {\r\n if (!isValidPid(pid)) {\r\n return null;\r\n }\r\n\r\n try {\r\n const stats = await pidusage(pid);\r\n // Calculate start time from current timestamp minus elapsed time\r\n return stats.timestamp - stats.elapsed;\r\n } catch {\r\n return null; // Process doesn't exist or can't get stats\r\n }\r\n}\r\n\r\n/**\r\n * Check if a running process is the same instance we originally spawned.\r\n * Compares process start time with PID file modification time.\r\n *\r\n * Returns true if:\r\n * - Process exists AND start time is within tolerance of pidFileMtime\r\n *\r\n * Returns false if:\r\n * - Process doesn't exist\r\n * - Can't determine process start time\r\n * - Start time doesn't match (likely PID reuse)\r\n */\r\nexport async function isSameProcessInstance(\r\n pid: number,\r\n pidFileMtimeMs: number\r\n): Promise<boolean> {\r\n const processStartTime = await getProcessStartTime(pid);\r\n if (processStartTime === null) {\r\n return false;\r\n }\r\n\r\n const diff = Math.abs(processStartTime - pidFileMtimeMs);\r\n return diff <= START_TIME_TOLERANCE_MS;\r\n}\r\n\r\n/**\r\n * Check if a process with the given PID is still running\r\n */\r\nexport function isProcessAlive(pid: number): boolean {\r\n try {\r\n if (!isValidPid(pid)) {\r\n return false;\r\n }\r\n if (isWindows) {\r\n // Windows: tasklist returns exit code 0 if process found\r\n // PID is validated as a safe integer above before interpolation\r\n const output = execSync(`tasklist /FI \"PID eq ${pid}\" /NH`, {\r\n encoding: 'utf8',\r\n stdio: ['pipe', 'pipe', 'pipe'],\r\n });\r\n return output.includes(String(pid));\r\n } else {\r\n // Unix/Mac: kill -0 checks if process exists without killing it\r\n process.kill(pid, 0);\r\n return true;\r\n }\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Kill a process by PID\r\n * Returns true if the process was killed, false if it wasn't running\r\n */\r\nexport function killProcess(pid: number): boolean {\r\n if (!isValidPid(pid) || !isProcessAlive(pid)) {\r\n return false;\r\n }\r\n\r\n try {\r\n if (isWindows) {\r\n // Windows: taskkill with /T kills the process tree, /F forces\r\n // PID is validated as a safe integer above before interpolation\r\n execSync(`taskkill /PID ${pid} /T /F`, {\r\n stdio: ['pipe', 'pipe', 'pipe'],\r\n });\r\n } else {\r\n // Unix: try to kill process group first (catches child processes),\r\n // fall back to killing just the process if group kill fails\r\n const killed = tryKillUnix(-pid) || tryKillUnix(pid);\r\n if (!killed) {\r\n return false;\r\n }\r\n }\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Helper to attempt Unix kill with error handling\r\n */\r\nfunction tryKillUnix(pid: number): boolean {\r\n try {\r\n process.kill(pid, 'SIGTERM');\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Wait for a process to die, with timeout\r\n * @param pid - Process ID to wait for\r\n * @param timeoutMs - Maximum time to wait (default: 2000ms)\r\n */\r\nexport async function waitForProcessToDie(\r\n pid: number,\r\n timeoutMs: number = DEFAULT_WAIT_TIMEOUT_MS\r\n): Promise<boolean> {\r\n const startTime = Date.now();\r\n\r\n while (Date.now() - startTime < timeoutMs) {\r\n if (!isProcessAlive(pid)) {\r\n return true;\r\n }\r\n await new Promise(resolve => setTimeout(resolve, CHECK_INTERVAL_MS));\r\n }\r\n\r\n return !isProcessAlive(pid);\r\n}\r\n\r\nexport interface SpawnResult {\r\n child: ChildProcess;\r\n pid: number;\r\n}\r\n\r\n/**\r\n * Spawn a command with stdio forwarding\r\n */\r\nexport function spawnCommand(command: string, args: string[]): SpawnResult {\r\n // On Windows, pass entire command as a single string to avoid escaping issues\r\n // with shell: true (DEP0190 warning and argument handling)\r\n const spawnCmd = isWindows ? `${command} ${args.join(' ')}` : command;\r\n const spawnArgs = isWindows ? [] : args;\r\n\r\n const child = spawn(spawnCmd, spawnArgs, {\r\n stdio: 'inherit',\r\n shell: isWindows,\r\n detached: !isWindows,\r\n });\r\n\r\n if (child.pid === undefined) {\r\n throw new Error('Failed to spawn process');\r\n }\r\n\r\n return {\r\n child,\r\n pid: child.pid,\r\n };\r\n}\r\n\r\n/**\r\n * Set up signal handlers to forward signals to child process\r\n * Note: Both SIGINT and SIGTERM are forwarded as SIGTERM to ensure\r\n * consistent graceful shutdown behavior across different termination methods.\r\n */\r\nexport function setupSignalHandlers(child: ChildProcess, onExit?: () => void): void {\r\n const handleSignal = (_signal: NodeJS.Signals) => {\r\n if (child.pid && isValidPid(child.pid)) {\r\n if (isWindows) {\r\n try {\r\n // PID is validated as a safe integer above before interpolation\r\n execSync(`taskkill /PID ${child.pid} /T /F`, {\r\n stdio: ['pipe', 'pipe', 'pipe'],\r\n });\r\n } catch {\r\n // Process might already be dead\r\n }\r\n } else {\r\n // Forward as SIGTERM for graceful shutdown\r\n child.kill('SIGTERM');\r\n }\r\n }\r\n };\r\n\r\n // Forward both SIGINT (Ctrl+C) and SIGTERM to child as SIGTERM\r\n process.on('SIGINT', () => handleSignal('SIGINT'));\r\n process.on('SIGTERM', () => handleSignal('SIGTERM'));\r\n\r\n child.on('exit', (code, signal) => {\r\n if (onExit) {\r\n onExit();\r\n }\r\n if (signal) {\r\n process.exit(128 + (signal === 'SIGTERM' ? 15 : signal === 'SIGINT' ? 2 : 1));\r\n }\r\n process.exit(code ?? 0);\r\n });\r\n\r\n child.on('error', err => {\r\n console.error(`Failed to start process: ${err.message}`);\r\n process.exit(1);\r\n });\r\n}\r\n","#!/usr/bin/env node\r\n/**\r\n * just-one - Ensure only one instance of a command runs at a time\r\n */\r\n\r\nimport { parseArgs, validateOptions, getHelpText, type CliOptions } from './lib/cli.js';\r\nimport { readPid, writePid, deletePid, listPids, getPidFileMtime } from './lib/pid.js';\r\nimport {\r\n isProcessAlive,\r\n killProcess,\r\n waitForProcessToDie,\r\n spawnCommand,\r\n setupSignalHandlers,\r\n isSameProcessInstance,\r\n} from './lib/process.js';\r\n\r\n// Read version from package.json at build time\r\nconst VERSION = '0.1.0';\r\n\r\nfunction log(message: string, options: CliOptions): void {\r\n if (!options.quiet) {\r\n console.log(message);\r\n }\r\n}\r\n\r\nfunction logError(message: string): void {\r\n console.error(message);\r\n}\r\n\r\nasync function handleKill(name: string, options: CliOptions): Promise<number> {\r\n const pid = readPid(name, options.pidDir);\r\n\r\n if (pid === null) {\r\n log(`No process found with name: ${name}`, options);\r\n return 0;\r\n }\r\n\r\n // Verify this is the same process we originally started (prevents killing\r\n // unrelated processes that reused the same PID)\r\n const pidFileMtime = getPidFileMtime(name, options.pidDir);\r\n const isSameInstance =\r\n pidFileMtime !== null && (await isSameProcessInstance(pid, pidFileMtime));\r\n\r\n if (!isSameInstance) {\r\n if (isProcessAlive(pid)) {\r\n log(`PID ${pid} belongs to a different process, not killing`, options);\r\n } else {\r\n log(`Process ${name} (PID: ${pid}) is not running, cleaning up PID file`, options);\r\n }\r\n deletePid(name, options.pidDir);\r\n return 0;\r\n }\r\n\r\n log(`Killing process ${name} (PID: ${pid})...`, options);\r\n const killed = killProcess(pid);\r\n\r\n if (killed) {\r\n await waitForProcessToDie(pid);\r\n deletePid(name, options.pidDir);\r\n log(`Process ${name} killed`, options);\r\n return 0;\r\n } else {\r\n logError(`Failed to kill process ${name} (PID: ${pid})`);\r\n return 1;\r\n }\r\n}\r\n\r\nfunction handleList(options: CliOptions): number {\r\n const pids = listPids(options.pidDir);\r\n\r\n if (pids.length === 0) {\r\n log('No tracked processes', options);\r\n return 0;\r\n }\r\n\r\n log('Tracked processes:', options);\r\n for (const info of pids) {\r\n const status = info.exists && isProcessAlive(info.pid) ? 'running' : 'stopped';\r\n const pidStr = info.pid > 0 ? String(info.pid) : 'unknown';\r\n log(` ${info.name}: PID ${pidStr} (${status})`, options);\r\n }\r\n\r\n return 0;\r\n}\r\n\r\nasync function handleRun(options: CliOptions): Promise<number> {\r\n const name = options.name!;\r\n const [command, ...args] = options.command;\r\n\r\n if (!command) {\r\n logError('No command specified');\r\n return 1;\r\n }\r\n\r\n // Check for existing process\r\n const existingPid = readPid(name, options.pidDir);\r\n if (existingPid !== null) {\r\n const pidFileMtime = getPidFileMtime(name, options.pidDir);\r\n const shouldKill =\r\n pidFileMtime !== null &&\r\n (await isSameProcessInstance(existingPid, pidFileMtime));\r\n\r\n if (shouldKill) {\r\n log(`Killing existing process ${name} (PID: ${existingPid})...`, options);\r\n killProcess(existingPid);\r\n await waitForProcessToDie(existingPid);\r\n } else if (isProcessAlive(existingPid)) {\r\n // PID exists but doesn't match our process - likely PID reuse\r\n log(\r\n `Stale PID file detected (PID ${existingPid} belongs to different process), skipping kill`,\r\n options\r\n );\r\n }\r\n deletePid(name, options.pidDir);\r\n }\r\n\r\n // Spawn the new process\r\n log(`Starting: ${command} ${args.join(' ')}`, options);\r\n\r\n try {\r\n const { child, pid } = spawnCommand(command, args);\r\n\r\n // Save PID\r\n writePid(name, pid, options.pidDir);\r\n log(`Process started with PID: ${pid}`, options);\r\n\r\n // Set up signal handlers\r\n // Note: We intentionally do NOT delete the PID file on exit.\r\n // If the process exits unexpectedly, the PID file allows the next run\r\n // to find and kill any orphaned processes.\r\n setupSignalHandlers(child);\r\n\r\n // The process will keep running until it exits or is killed\r\n // The exit handler in setupSignalHandlers will call process.exit\r\n return 0;\r\n } catch (err) {\r\n const message = err instanceof Error ? err.message : String(err);\r\n logError(`Failed to start process: ${message}`);\r\n return 1;\r\n }\r\n}\r\n\r\nasync function main(): Promise<number> {\r\n const args = process.argv.slice(2);\r\n\r\n // Parse arguments\r\n const parseResult = parseArgs(args);\r\n if (!parseResult.success) {\r\n logError(`Error: ${parseResult.error}`);\r\n logError('Use --help for usage information');\r\n return 1;\r\n }\r\n\r\n const options = parseResult.options;\r\n\r\n // Validate options\r\n const validateResult = validateOptions(options);\r\n if (!validateResult.success) {\r\n logError(`Error: ${validateResult.error}`);\r\n logError('Use --help for usage information');\r\n return 1;\r\n }\r\n\r\n // Handle help\r\n if (options.help) {\r\n console.log(getHelpText());\r\n return 0;\r\n }\r\n\r\n // Handle version\r\n if (options.version) {\r\n console.log(`just-one v${VERSION}`);\r\n return 0;\r\n }\r\n\r\n // Handle list\r\n if (options.list) {\r\n return handleList(options);\r\n }\r\n\r\n // Handle kill\r\n if (options.kill) {\r\n return await handleKill(options.kill, options);\r\n }\r\n\r\n // Handle run\r\n return await handleRun(options);\r\n}\r\n\r\n// Run the CLI\r\nmain()\r\n .then(code => {\r\n // Only exit if we're not running a child process\r\n // The child process exit handler will call process.exit\r\n if (code !== 0) {\r\n process.exit(code);\r\n }\r\n })\r\n .catch(err => {\r\n console.error('Unexpected error:', err);\r\n process.exit(1);\r\n });\r\n\r\n// Export for testing\r\nexport { main };\r\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,101 +1,105 @@
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
- }
1
+ {
2
+ "name": "@radleta/just-one",
3
+ "version": "1.0.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
+ "@types/pidusage": "^2.0.5",
71
+ "@typescript-eslint/eslint-plugin": "^8.20.0",
72
+ "@typescript-eslint/parser": "^8.20.0",
73
+ "@vitest/coverage-v8": "^3.0.0",
74
+ "eslint": "^9.18.0",
75
+ "husky": "^9.1.7",
76
+ "lint-staged": "^16.2.4",
77
+ "prettier": "^3.4.2",
78
+ "tsup": "^8.0.0",
79
+ "typescript": "^5.8.3",
80
+ "vitest": "^3.0.0"
81
+ },
82
+ "files": [
83
+ "dist/",
84
+ "bin/",
85
+ "README.md",
86
+ "CHANGELOG.md",
87
+ "LICENSE"
88
+ ],
89
+ "publishConfig": {
90
+ "access": "public",
91
+ "registry": "https://registry.npmjs.org/"
92
+ },
93
+ "lint-staged": {
94
+ "*.ts": [
95
+ "eslint --fix",
96
+ "prettier --write"
97
+ ],
98
+ "*.{json,md,yml,yaml}": [
99
+ "prettier --write"
100
+ ]
101
+ },
102
+ "dependencies": {
103
+ "pidusage": "^4.0.1"
104
+ }
105
+ }