ports-manager 1.0.0 → 1.0.1

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/src/cli.js CHANGED
@@ -1,278 +1,248 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const { loadConfig, mergeConfig } = require('./config');
6
- const { detectFolders, classifyProject, scanBackend } = require('./detect');
7
- const { allocatePorts, buildSpecs } = require('./runtime');
8
- const { createCorsShim } = require('./cors');
9
- const { runProcesses } = require('./process-manager');
10
- const { startProxy } = require('./proxy');
11
- const { generateIdeFiles, isIdeTerminal } = require('./ide');
12
- const { launchInBridge, stopBridge } = require('./bridge-client');
13
- const pkg = require('../package.json');
14
-
15
- const HELP = `ports-manager [options]
16
-
17
- Detect and run a paired frontend/backend JavaScript project.
18
-
19
- --frontend-dir PATH Override frontend/client directory
20
- --backend-dir PATH Override backend/server directory
21
- --frontend-port PORT Request frontend port
22
- --backend-port PORT Request backend port
23
- --proxy-port PORT Request same-origin proxy port
24
- --range MIN-MAX Port search range (default 4000-4999)
25
- --ban PORTS Ban comma-separated ports
26
- --cors / --no-cors-shim Enable/disable the CORS preload shim
27
- --force-cors Inject shim even when CORS is heuristically detected
28
- --cors-credentials BOOL Whether shim emits credential support
29
- --proxy Enable same-origin proxy mode
30
- --api-prefix PATH Backend proxy prefix (default /api)
31
- --wait-for-backend[=MS] Wait before frontend (bare form: 10000ms)
32
- --ide-terminals[=MODE] auto, off, tasks, extension (bare form: auto)
33
- --with-keybinding Generate a user-keybindings reference file
34
- --keybinding KEY Reference shortcut (default ctrl+alt+p)
35
- --config PATH Explicit JSON config
36
- --dry-run Inspect and allocate without writes/processes
37
- --stop Ask the bridge to stop this workspace's terminals
38
- --help Show help
39
- --version, -v Show version and author
40
- `;
41
-
42
- function valueFor(argv, index, name) {
43
- const argument = argv[index];
44
- const equals = argument.indexOf('=');
45
- if (equals !== -1) return { value: argument.slice(equals + 1), consumed: 0 };
46
- if (index + 1 >= argv.length || argv[index + 1].startsWith('--')) {
47
- throw new Error(`${name} requires a value`);
48
- }
49
- return { value: argv[index + 1], consumed: 1 };
50
- }
51
-
52
- function integer(value, name) {
53
- if (!/^\d+$/.test(value)) throw new Error(`${name} must be an integer`);
54
- return Number(value);
55
- }
56
-
57
- function boolean(value, name) {
58
- if (value === 'true') return true;
59
- if (value === 'false') return false;
60
- throw new Error(`${name} must be true or false`);
61
- }
62
-
63
- function parseArgs(argv) {
64
- const options = { bannedPorts: [] };
65
- for (let index = 0; index < argv.length; index += 1) {
66
- const argument = argv[index];
67
- const name = argument.split('=')[0];
68
- if (name === '--help' || name === '-h') options.help = true;
69
- else if (name === '--version' || name === '-v') options.version = true;
70
- else if (name === '--dry-run') options.dryRun = true;
71
- else if (name === '--stop') options.stop = true;
72
- else if (name === '--proxy') options.proxy = true;
73
- else if (name === '--no-proxy') options.proxy = false;
74
- else if (name === '--cors') options.cors = true;
75
- else if (name === '--no-cors') options.cors = false;
76
- else if (name === '--no-cors-shim') options.cors = false;
77
- else if (name === '--force-cors') options.forceCors = true;
78
- else if (name === '--with-keybinding') options.withKeybinding = true;
79
- else if (name === '--no-cors-credentials') options.corsCredentials = false;
80
- else if (name === '--internal-proxy') {
81
- options.internalProxy = true;
82
- } else if (name === '--ide-terminals') {
83
- if (argument.includes('=')) options.ideTerminals = argument.slice(argument.indexOf('=') + 1) || 'auto';
84
- else if (argv[index + 1] && !argv[index + 1].startsWith('--') &&
85
- ['off', 'auto', 'tasks', 'extension'].includes(argv[index + 1])) {
86
- options.ideTerminals = argv[++index];
87
- } else options.ideTerminals = 'auto';
88
- } else if (name === '--wait-for-backend') {
89
- if (argument.includes('=')) options.waitForBackend = integer(argument.split('=').slice(1).join('='), name);
90
- else if (argv[index + 1] && /^\d+$/.test(argv[index + 1])) options.waitForBackend = integer(argv[++index], name);
91
- else options.waitForBackend = 10000;
92
- } else {
93
- const mapping = {
94
- '--frontend': 'frontend', '--backend': 'backend',
95
- '--frontend-dir': 'frontend', '--backend-dir': 'backend', '--config': 'configPath',
96
- '--api-prefix': 'apiPrefix', '--keybinding': 'keybinding',
97
- '--frontend-port': 'frontendPort', '--backend-port': 'backendPort',
98
- '--proxy-port': 'proxyPort', '--cors-credentials': 'corsCredentials',
99
- '--port-range': 'portRange', '--range': 'portRange',
100
- '--ban-port': 'banPort', '--ban': 'ban',
101
- '--proxy-frontend-port': 'proxyFrontendPort', '--proxy-backend-port': 'proxyBackendPort'
102
- };
103
- const key = mapping[name];
104
- if (!key) throw new Error(`unknown option: ${argument}`);
105
- const found = valueFor(argv, index, name);
106
- index += found.consumed;
107
- if (['frontendPort', 'backendPort', 'proxyPort', 'proxyFrontendPort', 'proxyBackendPort'].includes(key)) {
108
- options[key] = integer(found.value, name);
109
- } else if (key === 'corsCredentials') {
110
- options[key] = boolean(found.value, name);
111
- } else if (key === 'banPort') {
112
- options.bannedPorts.push(integer(found.value, name));
113
- } else if (key === 'ban') {
114
- const ports = found.value.split(',').map((value) => value.trim()).filter(Boolean);
115
- if (!ports.length) throw new Error('--ban requires a comma-separated port list');
116
- options.bannedPorts.push(...ports.map((value) => integer(value, name)));
117
- } else if (key === 'portRange') {
118
- const match = found.value.match(/^(\d+)-(\d+)$/);
119
- if (!match) throw new Error('--port-range must be MIN-MAX');
120
- options.portRange = [Number(match[1]), Number(match[2])];
121
- } else options[key] = found.value;
122
- }
123
- }
124
- if (!options.bannedPorts.length) delete options.bannedPorts;
125
- return options;
126
- }
127
-
128
- function proxySpec(root, ports, apiPrefix) {
129
- return {
130
- name: 'Proxy',
131
- cwd: root,
132
- command: 'node',
133
- args: [
134
- path.resolve(__dirname, '../bin/ports-manager.js'), '--internal-proxy',
135
- '--proxy-port', String(ports.proxy), '--proxy-frontend-port', String(ports.frontend),
136
- '--proxy-backend-port', String(ports.backend), '--api-prefix', apiPrefix
137
- ],
138
- env: {},
139
- color: 'yellow'
140
- };
141
- }
142
-
143
- async function runProxyOnly(options) {
144
- for (const key of ['proxyPort', 'proxyFrontendPort', 'proxyBackendPort']) {
145
- if (!options[key]) throw new Error(`internal proxy missing ${key}`);
146
- }
147
- const proxy = await startProxy({
148
- port: options.proxyPort,
149
- frontendPort: options.proxyFrontendPort,
150
- backendPort: options.proxyBackendPort,
151
- apiPrefix: options.apiPrefix || '/api'
152
- });
153
- await new Promise((resolve) => {
154
- const finish = async () => {
155
- await proxy.close();
156
- resolve();
157
- };
158
- process.once('SIGINT', finish);
159
- process.once('SIGTERM', finish);
160
- });
161
- }
162
-
163
- function printPlan(root, frontend, backend, ports, config, corsEnabled) {
164
- console.log(`Root: ${root}`);
165
- console.log(`Backend: ${backend.framework} / npm run ${backend.script} / ${ports.backend}`);
166
- console.log(`Frontend: ${frontend.framework} / npm run ${frontend.script} / ${ports.frontend}`);
167
- if (config.proxy) console.log(`Proxy: http://127.0.0.1:${ports.proxy}${config.apiPrefix}`);
168
- console.log(`CORS shim: ${corsEnabled ? 'enabled' : 'skipped'}`);
169
- }
170
-
171
- async function main(argv = process.argv.slice(2), cwd = process.cwd()) {
172
- const options = parseArgs(argv);
173
- if (options.help) return console.log(HELP);
174
- if (options.version) {
175
- return console.log(`ports-manager v${pkg.version}\nMuhammad Saad Amin (SENODROOM)`);
176
- }
177
- if (options.internalProxy) return runProxyOnly(options);
178
-
179
- const root = path.resolve(cwd);
180
- const tag = `ports-manager:${root}`;
181
- if (options.stop) {
182
- await stopBridge(tag);
183
- console.log('Requested closure of Ports Manager terminals for this workspace.');
184
- return;
185
- }
186
- const loaded = await loadConfig(root, options.configPath);
187
- const config = mergeConfig(loaded.config, options);
188
- const folders = detectFolders(root, config);
189
- const frontend = classifyProject(folders.frontend, 'frontend');
190
- const backend = classifyProject(folders.backend, 'backend');
191
- const scan = scanBackend(backend);
192
- const corsEnabled = config.cors && (config.forceCors || !scan.corsDetected);
193
- if (scan.corsDetected && config.cors && !config.forceCors) {
194
- console.log('Existing CORS support detected heuristically; skipping preload shim (use --force-cors to override).');
195
- }
196
- if (!scan.hasEnvPort) {
197
- console.warn('Warning: static scan did not find process.env.PORT in likely backend entry files; runtime behavior may differ.');
198
- }
199
- if (scan.literals.length) {
200
- console.warn(`Warning: possible literal listen port(s) ${scan.literals.join(', ')} found; this heuristic may include inactive code.`);
201
- }
202
- if (frontend.framework === 'generic') {
203
- console.warn('Warning: generic frontend receives PORT and API URL cannot be inferred; configure env values if needed.');
204
- }
205
-
206
- const allocated = await allocatePorts(config, config.proxy ? 3 : 2);
207
- const ports = { backend: allocated[0], frontend: allocated[1], proxy: allocated[2] };
208
- const ideMode = config.ideTerminals;
209
- const inIde = isIdeTerminal();
210
- let effectiveIdeMode = ideMode;
211
- if (ideMode !== 'off' && !inIde) {
212
- console.log('IDE terminal mode requested outside a corroborated VS Code/Cursor terminal; using inline mode.');
213
- effectiveIdeMode = 'off';
214
- }
215
- const durableShim = path.join(root, '.vscode', 'ports-manager-cors-preload.cjs');
216
- const shimPath = corsEnabled
217
- ? (effectiveIdeMode === 'off' ? (options.dryRun ? '<dry-run-cors-shim>' : null) : durableShim)
218
- : null;
219
- let temporaryShim;
220
- if (corsEnabled && effectiveIdeMode === 'off' && !options.dryRun) {
221
- temporaryShim = createCorsShim();
222
- }
223
- const specs = buildSpecs(frontend, backend, ports, config,
224
- temporaryShim ? temporaryShim.filename : shimPath);
225
- const pSpec = config.proxy ? proxySpec(root, ports, config.apiPrefix) : null;
226
- printPlan(root, frontend, backend, ports, config, corsEnabled);
227
- if (options.dryRun) {
228
- console.log('Dry run: no files written and no processes started.');
229
- return;
230
- }
231
-
232
- if (effectiveIdeMode === 'extension' || effectiveIdeMode === 'auto') {
233
- if (corsEnabled) {
234
- fs.mkdirSync(path.dirname(durableShim), { recursive: true });
235
- fs.writeFileSync(durableShim, require('./cors').SHIM_SOURCE, 'utf8');
236
- }
237
- try {
238
- await launchInBridge(specs, pSpec, tag);
239
- console.log('Started tagged terminals through the Ports Manager bridge.');
240
- return;
241
- } catch (error) {
242
- if (effectiveIdeMode === 'extension') throw error;
243
- console.log(`Bridge unavailable (${error.message}); generating VS Code tasks instead.`);
244
- effectiveIdeMode = 'tasks';
245
- }
246
- }
247
- if (effectiveIdeMode === 'tasks') {
248
- const generated = generateIdeFiles(root, specs, pSpec, {
249
- keybinding: config.keybinding,
250
- needsShim: corsEnabled,
251
- withKeybinding: config.withKeybinding
252
- });
253
- console.log(`Generated ${generated.tasksFile}. Run “Ports Manager: Run All” from Tasks: Run Task.`);
254
- if (generated.keybindingsFile) {
255
- console.log(`Suggested shortcut ${config.keybinding} is in ${generated.keybindingsFile}; copy it into Preferences: Open Keyboard Shortcuts (JSON).`);
256
- console.log('VS Code/Cursor does not apply workspace keybindings files automatically.');
257
- }
258
- return;
259
- }
260
-
261
- let proxy;
262
- try {
263
- if (config.proxy) proxy = await startProxy({
264
- port: ports.proxy, frontendPort: ports.frontend, backendPort: ports.backend,
265
- apiPrefix: config.apiPrefix
266
- });
267
- const code = await runProcesses(specs, {
268
- waitForBackend: config.waitForBackend,
269
- backendPort: ports.backend
270
- });
271
- process.exitCode = code;
272
- } finally {
273
- if (proxy) await proxy.close();
274
- if (temporaryShim) temporaryShim.cleanup();
275
- }
276
- }
277
-
278
- module.exports = { HELP, main, parseArgs, proxySpec, runProxyOnly };
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { loadConfig, mergeConfig } = require('./config');
5
+ const { detectFolders, classifyProject, naturalPort } = require('./detect');
6
+ const { allocatePorts, appendNodeRequire, buildSpecs } = require('./runtime');
7
+ const { checkProject, describeFailure, installDependencies, summarize } = require('./preflight');
8
+ const { applyWiring, planWiring, wiringMode } = require('./wiring');
9
+ const { createCorsShim } = require('./cors');
10
+ const { runProcesses } = require('./process-manager');
11
+ const { startProxy } = require('./proxy');
12
+ const leases = require('./leases');
13
+ const pkg = require('../package.json');
14
+
15
+ const HELP = `ports-manager [options]
16
+
17
+ Run a paired frontend/backend project on free ports, with the frontend actually
18
+ wired to wherever the backend landed.
19
+
20
+ --frontend-dir PATH Override frontend/client directory
21
+ --backend-dir PATH Override backend/server directory
22
+ --frontend-port PORT Pin the frontend port
23
+ --backend-port PORT Pin the backend port
24
+ --proxy-port PORT Pin the same-origin proxy port
25
+ --range MIN-MAX Port search range (default 3000-5999)
26
+ --ban PORTS Never use these comma-separated ports
27
+ --proxy / --no-proxy Force or forbid same-origin proxy mode
28
+ --api-prefix PATH Path routed to the backend (default /api)
29
+ --no-install Fail instead of installing missing dependencies
30
+ --no-cors Do not inject backend CORS origin variables
31
+ --force-cors Also preload a response-time CORS shim
32
+ --no-cors-credentials Shim omits Access-Control-Allow-Credentials
33
+ --wait-for-backend[=MS] Wait for the backend before starting the frontend
34
+ --config PATH Explicit JSON config
35
+ --dry-run Show the plan without installing or starting anything
36
+ --help, -h Show help
37
+ --version, -v Show version and author
38
+ `;
39
+
40
+ function valueFor(argv, index, name) {
41
+ const argument = argv[index];
42
+ const equals = argument.indexOf('=');
43
+ if (equals !== -1) return { value: argument.slice(equals + 1), consumed: 0 };
44
+ if (index + 1 >= argv.length || argv[index + 1].startsWith('--')) {
45
+ throw new Error(`${name} requires a value`);
46
+ }
47
+ return { value: argv[index + 1], consumed: 1 };
48
+ }
49
+
50
+ function integer(value, name) {
51
+ if (!/^\d+$/.test(value)) throw new Error(`${name} must be an integer`);
52
+ return Number(value);
53
+ }
54
+
55
+ function boolean(value, name) {
56
+ if (value === 'true') return true;
57
+ if (value === 'false') return false;
58
+ throw new Error(`${name} must be true or false`);
59
+ }
60
+
61
+ function parseArgs(argv) {
62
+ const options = { bannedPorts: [] };
63
+ for (let index = 0; index < argv.length; index += 1) {
64
+ const argument = argv[index];
65
+ const name = argument.split('=')[0];
66
+ if (name === '--help' || name === '-h') options.help = true;
67
+ else if (name === '--version' || name === '-v') options.version = true;
68
+ else if (name === '--dry-run') options.dryRun = true;
69
+ else if (name === '--proxy') options.proxy = true;
70
+ else if (name === '--no-proxy') options.proxy = false;
71
+ else if (name === '--cors') options.cors = true;
72
+ else if (name === '--no-cors' || name === '--no-cors-shim') options.cors = false;
73
+ else if (name === '--force-cors') options.forceCors = true;
74
+ else if (name === '--install') options.install = true;
75
+ else if (name === '--no-install') options.install = false;
76
+ else if (name === '--no-cors-credentials') options.corsCredentials = false;
77
+ else if (name === '--wait-for-backend') {
78
+ if (argument.includes('=')) options.waitForBackend = integer(argument.split('=').slice(1).join('='), name);
79
+ else if (argv[index + 1] && /^\d+$/.test(argv[index + 1])) options.waitForBackend = integer(argv[++index], name);
80
+ else options.waitForBackend = 10000;
81
+ } else {
82
+ const mapping = {
83
+ '--frontend': 'frontend', '--backend': 'backend',
84
+ '--frontend-dir': 'frontend', '--backend-dir': 'backend', '--config': 'configPath',
85
+ '--api-prefix': 'apiPrefix',
86
+ '--frontend-port': 'frontendPort', '--backend-port': 'backendPort',
87
+ '--proxy-port': 'proxyPort', '--cors-credentials': 'corsCredentials',
88
+ '--port-range': 'portRange', '--range': 'portRange',
89
+ '--ban-port': 'banPort', '--ban': 'ban'
90
+ };
91
+ const key = mapping[name];
92
+ if (!key) throw new Error(`unknown option: ${argument}`);
93
+ const found = valueFor(argv, index, name);
94
+ index += found.consumed;
95
+ if (['frontendPort', 'backendPort', 'proxyPort'].includes(key)) {
96
+ options[key] = integer(found.value, name);
97
+ } else if (key === 'corsCredentials') {
98
+ options[key] = boolean(found.value, name);
99
+ } else if (key === 'banPort') {
100
+ options.bannedPorts.push(integer(found.value, name));
101
+ } else if (key === 'ban') {
102
+ const ports = found.value.split(',').map((value) => value.trim()).filter(Boolean);
103
+ if (!ports.length) throw new Error('--ban requires a comma-separated port list');
104
+ options.bannedPorts.push(...ports.map((value) => integer(value, name)));
105
+ } else if (key === 'portRange') {
106
+ const match = found.value.match(/^(\d+)-(\d+)$/);
107
+ if (!match) throw new Error('--port-range must be MIN-MAX');
108
+ options.portRange = [Number(match[1]), Number(match[2])];
109
+ } else options[key] = found.value;
110
+ }
111
+ }
112
+ if (!options.bannedPorts.length) delete options.bannedPorts;
113
+ return options;
114
+ }
115
+
116
+ /** Installs anything missing so we never hand an unrunnable command to the shell. */
117
+ async function preflight(projects, config, logger = console) {
118
+ const checks = projects.map(checkProject);
119
+ const broken = checks.filter((check) => !check.ok);
120
+ if (!broken.length) return checks;
121
+
122
+ if (config.install === false) {
123
+ const detail = broken.map(describeFailure).join('\n');
124
+ throw new Error(`dependencies are missing and --no-install was given\n${detail}`);
125
+ }
126
+ for (const check of broken) {
127
+ logger.log(`${check.role}: ${summarize(check)}`);
128
+ const code = await installDependencies(check, logger);
129
+ if (code !== 0) throw new Error(`${check.packageManager} install failed in ${check.directory}`);
130
+ }
131
+ const rechecked = projects.map(checkProject);
132
+ const stillBroken = rechecked.filter((check) => !check.ok);
133
+ if (stillBroken.length) {
134
+ throw new Error(`still missing after install:\n${stillBroken.map(describeFailure).join('\n')}`);
135
+ }
136
+ return rechecked;
137
+ }
138
+
139
+ function describePort(decision) {
140
+ if (!decision) return '';
141
+ if (decision.requested) return `${decision.port} (pinned)`;
142
+ if (!decision.fellBack) return `${decision.port}`;
143
+ return `${decision.port} (${decision.natural} busy)`;
144
+ }
145
+
146
+ function printPlan(root, frontend, backend, decisions, wiring, config) {
147
+ const byName = Object.fromEntries(decisions.map((decision) => [decision.name, decision]));
148
+ console.log(`Root: ${root}`);
149
+ console.log(`Backend: ${backend.framework} / npm run ${backend.script} / port ${describePort(byName.backend)}`);
150
+ console.log(`Frontend: ${frontend.framework} / npm run ${frontend.script} / port ${describePort(byName.frontend)}`);
151
+ console.log(`Wiring: ${wiring.describe()}`);
152
+ console.log(`CORS: ${wiring.sameOrigin
153
+ ? 'not needed (same origin)'
154
+ : (wiring.corsEnvNames.length ? `injecting ${wiring.corsEnvNames.join(', ')}` : 'cross-origin')}${
155
+ config.forceCors ? ' + preload shim' : ''}`);
156
+ for (const note of wiring.notes) console.log(` ${note}`);
157
+ console.log(`Open: ${wiring.browserUrl}`);
158
+ }
159
+
160
+ async function main(argv = process.argv.slice(2), cwd = process.cwd()) {
161
+ const options = parseArgs(argv);
162
+ if (options.help) return console.log(HELP);
163
+ if (options.version) {
164
+ return console.log(`ports-manager v${pkg.version}\nMuhammad Saad Amin (SENODROOM)`);
165
+ }
166
+
167
+ const root = path.resolve(cwd);
168
+ const loaded = await loadConfig(root, options.configPath);
169
+ const config = mergeConfig(loaded.config, options);
170
+ const folders = detectFolders(root, config);
171
+ const frontend = classifyProject(folders.frontend, 'frontend');
172
+ const backend = classifyProject(folders.backend, 'backend');
173
+
174
+ if (!options.dryRun) await preflight([frontend, backend], config);
175
+ else {
176
+ for (const check of [frontend, backend].map(checkProject).filter((entry) => !entry.ok)) {
177
+ console.warn(`Would install first — ${describeFailure(check)}`);
178
+ }
179
+ }
180
+
181
+ const naturalBackendPort = naturalPort(backend);
182
+ const naturalFrontendPort = naturalPort(frontend);
183
+ const needsProxy = wiringMode(frontend, config) === 'proxy';
184
+
185
+ const requests = [
186
+ { name: 'backend', requested: config.backendPort, natural: naturalBackendPort },
187
+ { name: 'frontend', requested: config.frontendPort, natural: naturalFrontendPort }
188
+ ];
189
+ if (needsProxy) requests.push({ name: 'proxy', requested: config.proxyPort, natural: undefined });
190
+
191
+ const allocation = await allocatePorts(requests, config);
192
+ const { ports, decisions } = allocation;
193
+ let leased = false;
194
+ let cleanupWiring = () => {};
195
+ let temporaryShim;
196
+
197
+ try {
198
+ const wiring = planWiring({ frontend, backend, ports, config, naturalBackendPort });
199
+
200
+ if (config.forceCors && !options.dryRun) {
201
+ temporaryShim = createCorsShim();
202
+ wiring.backendEnv.PORTS_MANAGER_CORS_ORIGIN = wiring.browserUrl;
203
+ wiring.backendEnv.PORTS_MANAGER_CORS_CREDENTIALS = String(config.corsCredentials);
204
+ wiring.backendEnv.NODE_OPTIONS = appendNodeRequire(process.env.NODE_OPTIONS || '', temporaryShim.filename);
205
+ }
206
+
207
+ printPlan(root, frontend, backend, decisions, wiring, config);
208
+ if (options.dryRun) {
209
+ console.log('Dry run: nothing installed, written, or started.');
210
+ return;
211
+ }
212
+
213
+ cleanupWiring = applyWiring(wiring, frontend, ports, config);
214
+ const specs = buildSpecs(frontend, backend, ports, config, wiring);
215
+ leased = leases.acquire(Object.values(ports), root);
216
+
217
+ // Hand the ports over to the children only now that everything else is ready.
218
+ await allocation.release();
219
+
220
+ let proxy;
221
+ try {
222
+ if (wiring.useProxy) {
223
+ proxy = await startProxy({
224
+ port: ports.proxy, frontendPort: ports.frontend, backendPort: ports.backend,
225
+ apiPrefix: config.apiPrefix
226
+ });
227
+ }
228
+ const code = await runProcesses(specs, {
229
+ waitForBackend: config.waitForBackend,
230
+ backendPort: ports.backend,
231
+ frontendPort: ports.frontend,
232
+ onReady({ backendUp, frontendUp }) {
233
+ if (backendUp && frontendUp) console.log(`\nReady: ${wiring.browserUrl} -> ${wiring.describe()}`);
234
+ }
235
+ });
236
+ process.exitCode = code;
237
+ } finally {
238
+ if (proxy) await proxy.close();
239
+ }
240
+ } finally {
241
+ await allocation.release();
242
+ cleanupWiring();
243
+ if (temporaryShim) temporaryShim.cleanup();
244
+ if (leased) leases.release();
245
+ }
246
+ }
247
+
248
+ module.exports = { HELP, main, parseArgs, preflight, printPlan };
package/src/config.js CHANGED
@@ -6,24 +6,25 @@ const { cosmiconfig } = require('cosmiconfig');
6
6
 
7
7
  const DEFAULTS = Object.freeze({
8
8
  pairs: [['frontend', 'backend'], ['client', 'server']],
9
- portRange: [4000, 4999],
10
- bannedPorts: [3000, 5000],
9
+ // Wide enough to cover the ports MERN projects naturally want (3000, 5000, 5173...)
10
+ // and to keep finding free ones when several projects run at once.
11
+ portRange: [3000, 5999],
12
+ bannedPorts: [],
11
13
  cors: true,
12
14
  forceCors: false,
13
15
  corsCredentials: true,
14
- proxy: false,
16
+ // null = decide automatically (used only when the frontend cannot proxy for itself).
17
+ proxy: null,
15
18
  apiPrefix: '/api',
16
- ideTerminals: 'off',
19
+ install: true,
17
20
  waitForBackend: 0,
18
- withKeybinding: false,
19
- keybinding: 'ctrl+alt+p',
20
21
  env: {}
21
22
  });
22
23
 
23
24
  const allowedKeys = new Set([
24
25
  'pairs', 'frontend', 'backend', 'frontendPort', 'backendPort', 'proxyPort', 'portRange',
25
26
  'bannedPorts', 'cors', 'forceCors', 'corsCredentials', 'proxy', 'apiPrefix',
26
- 'ideTerminals', 'waitForBackend', 'withKeybinding', 'keybinding', 'env'
27
+ 'install', 'waitForBackend', 'env'
27
28
  ]);
28
29
 
29
30
  function normalizeConfig(input) {
@@ -36,13 +37,6 @@ function normalizeConfig(input) {
36
37
  if (config.proxy && typeof config.proxy === 'object' && !Array.isArray(config.proxy)) {
37
38
  config.proxy = config.proxy.enabled === true;
38
39
  }
39
- if (config.ideTerminals && typeof config.ideTerminals === 'object' &&
40
- !Array.isArray(config.ideTerminals)) {
41
- const ide = config.ideTerminals;
42
- config.ideTerminals = ide.mode || 'off';
43
- if (ide.withKeybinding !== undefined) config.withKeybinding = ide.withKeybinding;
44
- if (ide.keybinding !== undefined) config.keybinding = ide.keybinding;
45
- }
46
40
  return config;
47
41
  }
48
42
 
@@ -80,19 +74,18 @@ function validateConfig(config) {
80
74
  if (!Array.isArray(config.bannedPorts)) throw new Error('bannedPorts must be an array');
81
75
  config.bannedPorts.forEach((port, index) => assertPort(port, `bannedPorts[${index}]`));
82
76
  }
83
- for (const key of ['cors', 'forceCors', 'corsCredentials', 'proxy', 'withKeybinding']) {
77
+ for (const key of ['cors', 'forceCors', 'corsCredentials', 'install']) {
84
78
  if (config[key] !== undefined && typeof config[key] !== 'boolean') {
85
79
  throw new Error(`${key} must be boolean`);
86
80
  }
87
81
  }
82
+ if (config.proxy !== undefined && config.proxy !== null && typeof config.proxy !== 'boolean') {
83
+ throw new Error('proxy must be boolean or null');
84
+ }
88
85
  if (config.apiPrefix !== undefined &&
89
86
  (typeof config.apiPrefix !== 'string' || !config.apiPrefix.startsWith('/'))) {
90
87
  throw new Error('apiPrefix must be a string beginning with /');
91
88
  }
92
- if (config.ideTerminals !== undefined &&
93
- !['off', 'auto', 'tasks', 'extension'].includes(config.ideTerminals)) {
94
- throw new Error('ideTerminals must be off, auto, tasks, or extension');
95
- }
96
89
  if (config.waitForBackend !== undefined &&
97
90
  (!Number.isInteger(config.waitForBackend) || config.waitForBackend < 0)) {
98
91
  throw new Error('waitForBackend must be a non-negative integer (milliseconds)');
@@ -142,7 +135,7 @@ function mergeConfig(fileConfig, cliConfig) {
142
135
  if (allowedKeys.has(key) && value !== undefined) result[key] = value;
143
136
  }
144
137
  result.bannedPorts = [...new Set([
145
- 3000, 5000, ...(fileConfig.bannedPorts || []), ...(cliConfig.bannedPorts || [])
138
+ ...(fileConfig.bannedPorts || []), ...(cliConfig.bannedPorts || [])
146
139
  ])];
147
140
  validateConfig(result);
148
141
  return result;