@lowdep/procfile-run 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rushabh Shah
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,140 @@
1
+ # procfile-run
2
+
3
+ ![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen) ![Node](https://img.shields.io/badge/node-%3E%3D14-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
4
+
5
+
6
+ Start all your dev services with one command. Reads a standard `Procfile` and runs every process in parallel with color-coded, labeled output. Zero dependencies.
7
+
8
+ Works on **Windows, Mac, and Linux** — unlike `foreman` (Ruby), `goreman`/`overmind` (Go binaries), or shell scripts.
9
+
10
+ ---
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install -g procfile-run
16
+ ```
17
+
18
+ Or without installing:
19
+
20
+ ```bash
21
+ npx procfile-run
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ Create a `Procfile`:
29
+
30
+ ```
31
+ web: node server.js
32
+ worker: node worker.js
33
+ redis: redis-server --port 6379
34
+ ```
35
+
36
+ Then run:
37
+
38
+ ```bash
39
+ procfile-run
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Example Output
45
+
46
+ ```
47
+ procfile-run Procfile
48
+ Starting 3 process(es)...
49
+
50
+ │ web started (pid 12345) node server.js
51
+ │ worker started (pid 12346) node worker.js
52
+ │ redis started (pid 12347) redis-server --port 6379
53
+ │ Press Ctrl+C to stop all processes
54
+
55
+ web │ Listening on port 3000
56
+ redis │ Ready to accept connections
57
+ worker │ Worker started, waiting for jobs...
58
+ worker │ Processing job #1
59
+ web │ GET /api/jobs 200 12ms
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Usage
65
+
66
+ ```bash
67
+ procfile-run # Run ./Procfile
68
+ procfile-run Procfile.dev # Use a different Procfile
69
+ procfile-run --only web,worker # Run specific processes
70
+ procfile-run --restart # Auto-restart crashed processes
71
+ procfile-run --timestamp # Prefix lines with time
72
+ procfile-run --env .env.local # Load a specific env file
73
+ procfile-run --no-env # Don't load any .env file
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Procfile Format
79
+
80
+ ```
81
+ # This is a comment
82
+ web: node server.js
83
+ worker: node --env-file=.env worker.js
84
+ db: pg_ctl start -D /usr/local/var/postgresql@14
85
+ ```
86
+
87
+ Standard [Heroku Procfile](https://devcenter.heroku.com/articles/procfile) format — fully compatible with foreman, goreman, etc.
88
+
89
+ ---
90
+
91
+ ## Environment Variables
92
+
93
+ By default, `procfile-run` loads `.env` from the same directory as the Procfile and merges it into each process's environment.
94
+
95
+ ```bash
96
+ procfile-run --env .env.staging # Load a specific env file
97
+ procfile-run --no-env # Skip .env loading
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Behavior on Exit
103
+
104
+ - If any process exits (success or crash), **all other processes are stopped** — unless `--restart` is active.
105
+ - `Ctrl+C` sends `SIGTERM` to all child processes cleanly.
106
+ - `--restart` will restart only *crashed* processes (non-zero exit), not ones that exited cleanly.
107
+
108
+ ---
109
+
110
+ ## vs. Alternatives
111
+
112
+ | Tool | Runtime needed | Windows | Zero-dep |
113
+ |---|---|---|---|
114
+ | `foreman` | Ruby gem | ❌ | ❌ |
115
+ | `goreman` | Go binary | ❌ | ❌ |
116
+ | `overmind` | Go binary | ❌ | ❌ |
117
+ | `concurrently` | npm (has deps) | ✓ | ❌ |
118
+ | `procfile-run` | Node.js | ✓ | ✓ |
119
+
120
+ ---
121
+
122
+ ## License
123
+
124
+ MIT
125
+
126
+ ---
127
+
128
+ ## Keywords
129
+
130
+ `procfile` · `foreman alternative` · `overmind alternative` · `goreman alternative` · `process manager` · `run multiple processes` · `dev servers` · `concurrently alternative` · `cross-platform` · `zero dependencies`
131
+
132
+ ---
133
+
134
+ <div align="center">
135
+
136
+ **Built to solve, shared to help — Rushabh Shah 🛠️✨**
137
+
138
+ <sub>One of 40+ zero-dependency developer CLI tools — no <code>node_modules</code>, ever.</sub>
139
+
140
+ </div>
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const child_process = require('child_process');
7
+
8
+ const VERSION = '1.0.0';
9
+
10
+ // ─── ANSI palette (16 distinct colors for process labels) ─────────────────────
11
+ const isTTY = process.stdout.isTTY;
12
+ const c = (code, t) => isTTY ? `\x1b[${code}m${t}\x1b[0m` : t;
13
+ const bold = t => c('1', t);
14
+ const dim = t => c('2', t);
15
+ const red = t => c('31', t);
16
+ const green = t => c('32', t);
17
+ const yellow = t => c('33', t);
18
+
19
+ const PROC_COLORS = [
20
+ '36', '35', '33', '32', '34', '31', '96', '95', '93', '92', '94', '91',
21
+ '36;1', '35;1', '33;1', '32;1'
22
+ ];
23
+
24
+ function procColor(idx, t) {
25
+ return isTTY ? `\x1b[${PROC_COLORS[idx % PROC_COLORS.length]}m${t}\x1b[0m` : t;
26
+ }
27
+
28
+ // ─── Procfile parser ───────────────────────────────────────────────────────────
29
+ function parseProcfile(content) {
30
+ const procs = [];
31
+ for (const line of content.split('\n')) {
32
+ const trimmed = line.trim();
33
+ if (!trimmed || trimmed.startsWith('#')) continue;
34
+ const colon = trimmed.indexOf(':');
35
+ if (colon === -1) continue;
36
+ const name = trimmed.slice(0, colon).trim();
37
+ const cmd = trimmed.slice(colon + 1).trim();
38
+ if (name && cmd) procs.push({ name, cmd });
39
+ }
40
+ return procs;
41
+ }
42
+
43
+ // ─── .env file loader ─────────────────────────────────────────────────────────
44
+ function loadEnvFile(filePath) {
45
+ if (!fs.existsSync(filePath)) return {};
46
+ const env = {};
47
+ for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
48
+ const trimmed = line.trim();
49
+ if (!trimmed || trimmed.startsWith('#')) continue;
50
+ const eq = trimmed.indexOf('=');
51
+ if (eq === -1) continue;
52
+ const key = trimmed.slice(0, eq).trim();
53
+ let val = trimmed.slice(eq + 1).trim();
54
+ if ((val.startsWith('"') && val.endsWith('"')) ||
55
+ (val.startsWith("'") && val.endsWith("'"))) {
56
+ val = val.slice(1, -1);
57
+ }
58
+ env[key] = val;
59
+ }
60
+ return env;
61
+ }
62
+
63
+ // ─── Shell-split a command string ─────────────────────────────────────────────
64
+ // Very basic: handles quoted args and space separation
65
+ function shellSplit(cmd) {
66
+ const parts = [];
67
+ let cur = '', inSingle = false, inDouble = false;
68
+ for (let i = 0; i < cmd.length; i++) {
69
+ const ch = cmd[i];
70
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; }
71
+ else if (ch === '"' && !inSingle) { inDouble = !inDouble; }
72
+ else if (ch === ' ' && !inSingle && !inDouble) {
73
+ if (cur) { parts.push(cur); cur = ''; }
74
+ } else { cur += ch; }
75
+ }
76
+ if (cur) parts.push(cur);
77
+ return parts;
78
+ }
79
+
80
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
81
+ const args = process.argv.slice(2);
82
+ const VALUE_FLAGS = new Set(['--env', '--only']);
83
+ const positional = [];
84
+ for (let i = 0; i < args.length; i++) {
85
+ if (args[i].startsWith('-')) { if (VALUE_FLAGS.has(args[i])) i++; }
86
+ else positional.push(args[i]);
87
+ }
88
+
89
+ function getFlag(f) {
90
+ const i = args.indexOf(f);
91
+ return i !== -1 ? args[i + 1] : null;
92
+ }
93
+ function hasFlag(f) { return args.includes(f); }
94
+
95
+ if (hasFlag('--version') || hasFlag('-v')) {
96
+ console.log(`procfile-run v${VERSION}`); process.exit(0);
97
+ }
98
+
99
+ if (hasFlag('--help') || hasFlag('-h')) {
100
+ console.log(`
101
+ ${bold('procfile-run')} — Run all processes defined in a Procfile
102
+
103
+ ${bold('USAGE')}
104
+ procfile-run [Procfile] [options]
105
+
106
+ ${bold('OPTIONS')}
107
+ --only <names> Comma-separated list of process names to run
108
+ --env <file> Load additional env vars from a file (default: .env)
109
+ --no-env Don't load .env file
110
+ --restart Restart crashed processes automatically
111
+ --timestamp Prefix log lines with timestamp
112
+ --version Show version
113
+
114
+ ${bold('EXAMPLES')}
115
+ procfile-run # Run all processes in ./Procfile
116
+ procfile-run Procfile.dev # Use a different Procfile
117
+ procfile-run --only web,worker # Run only web and worker
118
+ procfile-run --restart # Auto-restart on crash
119
+
120
+ ${bold('PROCFILE FORMAT')}
121
+ # comment
122
+ web: node server.js
123
+ worker: node worker.js
124
+ redis: redis-server --port 6379
125
+ `);
126
+ process.exit(0);
127
+ }
128
+
129
+ // Locate Procfile
130
+ const procfilePath = positional[0]
131
+ ? path.resolve(positional[0])
132
+ : (() => {
133
+ for (const name of ['Procfile', 'procfile', 'PROCFILE', 'Procfile.dev']) {
134
+ const p = path.join(process.cwd(), name);
135
+ if (fs.existsSync(p)) return p;
136
+ }
137
+ return null;
138
+ })();
139
+
140
+ if (!procfilePath || !fs.existsSync(procfilePath)) {
141
+ console.error(red(`\nNo Procfile found. Create a Procfile or specify one:\n procfile-run ./Procfile\n`));
142
+ process.exit(1);
143
+ }
144
+
145
+ const onlyFilter = getFlag('--only') ? new Set(getFlag('--only').split(',').map(s => s.trim())) : null;
146
+ const envFile = hasFlag('--no-env') ? null : (getFlag('--env') || path.join(path.dirname(procfilePath), '.env'));
147
+ const autoRestart = hasFlag('--restart');
148
+ const showTimestamp = hasFlag('--timestamp');
149
+
150
+ // Load env
151
+ const extraEnv = envFile ? loadEnvFile(envFile) : {};
152
+ const procEnv = { ...process.env, ...extraEnv };
153
+
154
+ // Parse Procfile
155
+ const allProcs = parseProcfile(fs.readFileSync(procfilePath, 'utf8'));
156
+ const procs = onlyFilter ? allProcs.filter(p => onlyFilter.has(p.name)) : allProcs;
157
+
158
+ if (!procs.length) {
159
+ console.error(yellow(`\nNo processes found in ${path.basename(procfilePath)}\n`));
160
+ process.exit(1);
161
+ }
162
+
163
+ // Compute label width for aligned output
164
+ const labelWidth = Math.max(...procs.map(p => p.name.length));
165
+
166
+ function log(procIdx, procName, line) {
167
+ const label = procColor(procIdx, procName.padEnd(labelWidth));
168
+ const separator = dim('│');
169
+ const ts = showTimestamp ? dim(new Date().toLocaleTimeString() + ' ') : '';
170
+ process.stdout.write(`${label} ${separator} ${ts}${line}\n`);
171
+ }
172
+
173
+ function logSystem(msg) {
174
+ console.log(dim(`${''.padEnd(labelWidth)} │ ${msg}`));
175
+ }
176
+
177
+ console.log(`\n${bold('procfile-run')} ${dim(path.basename(procfilePath))}`);
178
+ console.log(dim(` Starting ${procs.length} process(es)...\n`));
179
+
180
+ // ─── Process management ───────────────────────────────────────────────────────
181
+ const children = new Map(); // name → ChildProcess
182
+ let shuttingDown = false;
183
+
184
+ const isWin = process.platform === 'win32';
185
+
186
+ // On Windows, npm/npx/yarn/pnpm ship as .cmd wrappers — resolve them explicitly
187
+ // so we can avoid shell:true (which triggers a deprecation warning).
188
+ const WIN_CMD_SHIMS = new Set(['npm', 'npx', 'yarn', 'pnpm', 'tsc', 'ts-node', 'vite', 'next', 'jest', 'eslint', 'prettier']);
189
+
190
+ function resolveWinBin(bin) {
191
+ if (!isWin) return { bin, useShell: false };
192
+ const base = path.basename(bin, path.extname(bin)).toLowerCase();
193
+ if (WIN_CMD_SHIMS.has(base)) {
194
+ // Try to find the .cmd in node_modules/.bin or PATH
195
+ for (const dir of (process.env.PATH || '').split(';')) {
196
+ const candidate = path.join(dir, bin + '.cmd');
197
+ if (fs.existsSync(candidate)) return { bin: candidate, useShell: false };
198
+ }
199
+ // Fallback: cmd.exe /c handles .cmd resolution safely
200
+ return { bin: 'cmd.exe', useShell: false, prefix: ['/c', bin] };
201
+ }
202
+ return { bin, useShell: false };
203
+ }
204
+
205
+ function spawnProc(proc, idx) {
206
+ const parts = shellSplit(proc.cmd);
207
+ let bin = parts[0];
208
+ let argv = parts.slice(1);
209
+
210
+ if (isWin) {
211
+ const { bin: resolvedBin, prefix } = resolveWinBin(bin);
212
+ bin = resolvedBin;
213
+ if (prefix) argv = [...prefix, ...argv];
214
+ }
215
+
216
+ const child = child_process.spawn(bin, argv, {
217
+ env: procEnv,
218
+ cwd: path.dirname(procfilePath),
219
+ stdio: ['ignore', 'pipe', 'pipe'],
220
+ });
221
+
222
+ children.set(proc.name, child);
223
+ logSystem(`${procColor(idx, proc.name)} started (pid ${child.pid}) ${dim(proc.cmd)}`);
224
+
225
+ child.stdout.on('data', data => {
226
+ for (const line of data.toString().split(/\r?\n/).filter(Boolean)) {
227
+ log(idx, proc.name, line);
228
+ }
229
+ });
230
+
231
+ child.stderr.on('data', data => {
232
+ for (const line of data.toString().split(/\r?\n/).filter(Boolean)) {
233
+ log(idx, proc.name, red(line));
234
+ }
235
+ });
236
+
237
+ child.on('exit', (code, signal) => {
238
+ children.delete(proc.name);
239
+ if (shuttingDown) return;
240
+ const reason = signal ? `signal ${signal}` : `exit code ${code}`;
241
+ if (code === 0) {
242
+ logSystem(`${procColor(idx, proc.name)} exited (${reason})`);
243
+ } else {
244
+ logSystem(red(`${proc.name} crashed (${reason})`));
245
+ }
246
+ if (autoRestart && !shuttingDown && code !== 0) {
247
+ logSystem(yellow(`${proc.name} restarting in 1s...`));
248
+ setTimeout(() => spawnProc(proc, idx), 1000);
249
+ } else if (!autoRestart && !shuttingDown) {
250
+ // If any process exits, kill the rest
251
+ logSystem(yellow(`${proc.name} exited — stopping all processes`));
252
+ shutdown(1);
253
+ }
254
+ });
255
+
256
+ child.on('error', err => {
257
+ logSystem(red(`${proc.name} error: ${err.message}`));
258
+ if (!autoRestart) shutdown(1);
259
+ });
260
+ }
261
+
262
+ function shutdown(exitCode = 0) {
263
+ if (shuttingDown) return;
264
+ shuttingDown = true;
265
+ console.log();
266
+ logSystem('Stopping all processes...');
267
+ for (const [name, child] of children) {
268
+ try {
269
+ if (isWin) {
270
+ child_process.spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F']);
271
+ } else {
272
+ process.kill(-child.pid, 'SIGTERM');
273
+ }
274
+ } catch { /* process may already be gone */ }
275
+ }
276
+ setTimeout(() => process.exit(exitCode), 500);
277
+ }
278
+
279
+ // Catch CTRL+C
280
+ process.on('SIGINT', () => shutdown(0));
281
+ process.on('SIGTERM', () => shutdown(0));
282
+
283
+ // Start all processes
284
+ procs.forEach((proc, idx) => spawnProc(proc, idx));
285
+
286
+ console.log();
287
+ logSystem(dim(`Press Ctrl+C to stop all processes`));
288
+ console.log();
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@lowdep/procfile-run",
3
+ "version": "1.0.0",
4
+ "description": "Procfile process runner — start all your dev services with one command, zero dependencies",
5
+ "bin": {
6
+ "procfile-run": "bin/procfile-run.js"
7
+ },
8
+ "keywords": [
9
+ "procfile",
10
+ "foreman",
11
+ "process-manager",
12
+ "cli",
13
+ "developer-tools",
14
+ "dev-server",
15
+ "heroku",
16
+ "foreman alternative",
17
+ "overmind alternative",
18
+ "goreman alternative",
19
+ "process manager",
20
+ "run multiple processes",
21
+ "dev servers",
22
+ "concurrently alternative",
23
+ "cross-platform",
24
+ "zero dependencies"
25
+ ],
26
+ "author": "Rushabh Shah",
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=14"
30
+ },
31
+ "files": [
32
+ "bin/"
33
+ ],
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/Rushabh5000/procfile-run.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/Rushabh5000/procfile-run/issues"
40
+ },
41
+ "homepage": "https://github.com/Rushabh5000/procfile-run#readme",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }