projax 0.1.29
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/LINKING.md +86 -0
- package/README.md +141 -0
- package/dist/core/database.d.ts +66 -0
- package/dist/core/database.js +312 -0
- package/dist/core/detector.d.ts +9 -0
- package/dist/core/detector.js +149 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.js +43 -0
- package/dist/core/scanner.d.ts +8 -0
- package/dist/core/scanner.js +114 -0
- package/dist/electron/main.d.ts +1 -0
- package/dist/electron/main.js +310 -0
- package/dist/electron/port-extractor.d.ts +9 -0
- package/dist/electron/port-extractor.js +351 -0
- package/dist/electron/port-scanner.d.ts +13 -0
- package/dist/electron/port-scanner.js +93 -0
- package/dist/electron/port-utils.d.ts +21 -0
- package/dist/electron/port-utils.js +200 -0
- package/dist/electron/preload.d.ts +49 -0
- package/dist/electron/preload.js +21 -0
- package/dist/electron/renderer/assets/index-BZ6USRnW.js +42 -0
- package/dist/electron/renderer/assets/index-DNtxfrZe.js +42 -0
- package/dist/electron/renderer/assets/index-khk3K-qG.css +1 -0
- package/dist/electron/renderer/index.html +15 -0
- package/dist/electron/script-runner.d.ts +40 -0
- package/dist/electron/script-runner.js +651 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +971 -0
- package/dist/port-extractor.d.ts +9 -0
- package/dist/port-extractor.js +351 -0
- package/dist/port-scanner.d.ts +13 -0
- package/dist/port-scanner.js +93 -0
- package/dist/port-utils.d.ts +21 -0
- package/dist/port-utils.js +200 -0
- package/dist/script-runner.d.ts +40 -0
- package/dist/script-runner.js +651 -0
- package/package.json +40 -0
- package/rebuild-sqlite.js +82 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.detectProjectType = detectProjectType;
|
|
37
|
+
exports.getProjectScripts = getProjectScripts;
|
|
38
|
+
exports.runScript = runScript;
|
|
39
|
+
exports.getProjectProcesses = getProjectProcesses;
|
|
40
|
+
exports.runScriptInBackground = runScriptInBackground;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const core_1 = require("./core");
|
|
46
|
+
const port_utils_1 = require("./port-utils");
|
|
47
|
+
/**
|
|
48
|
+
* Detect the project type based on files in the directory
|
|
49
|
+
*/
|
|
50
|
+
function detectProjectType(projectPath) {
|
|
51
|
+
if (fs.existsSync(path.join(projectPath, 'package.json'))) {
|
|
52
|
+
return 'node';
|
|
53
|
+
}
|
|
54
|
+
if (fs.existsSync(path.join(projectPath, 'pyproject.toml'))) {
|
|
55
|
+
return 'python';
|
|
56
|
+
}
|
|
57
|
+
if (fs.existsSync(path.join(projectPath, 'Cargo.toml'))) {
|
|
58
|
+
return 'rust';
|
|
59
|
+
}
|
|
60
|
+
if (fs.existsSync(path.join(projectPath, 'go.mod'))) {
|
|
61
|
+
return 'go';
|
|
62
|
+
}
|
|
63
|
+
if (fs.existsSync(path.join(projectPath, 'Makefile')) || fs.existsSync(path.join(projectPath, 'makefile'))) {
|
|
64
|
+
return 'makefile';
|
|
65
|
+
}
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse scripts from package.json (Node.js)
|
|
70
|
+
*/
|
|
71
|
+
function parseNodeScripts(projectPath) {
|
|
72
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
73
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
74
|
+
return new Map();
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
78
|
+
const scripts = packageJson.scripts || {};
|
|
79
|
+
const scriptMap = new Map();
|
|
80
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
81
|
+
if (typeof command === 'string') {
|
|
82
|
+
scriptMap.set(name, {
|
|
83
|
+
name,
|
|
84
|
+
command: command,
|
|
85
|
+
runner: 'npm',
|
|
86
|
+
projectType: 'node',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return scriptMap;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return new Map();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse scripts from pyproject.toml (Python)
|
|
98
|
+
*/
|
|
99
|
+
function parsePythonScripts(projectPath) {
|
|
100
|
+
const pyprojectPath = path.join(projectPath, 'pyproject.toml');
|
|
101
|
+
if (!fs.existsSync(pyprojectPath)) {
|
|
102
|
+
return new Map();
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(pyprojectPath, 'utf-8');
|
|
106
|
+
const scriptMap = new Map();
|
|
107
|
+
// Simple TOML parsing for [project.scripts] section
|
|
108
|
+
// This is a basic implementation - for production, consider using a TOML parser
|
|
109
|
+
const scriptsMatch = content.match(/\[project\.scripts\]\s*\n((?:[^\[]+\n?)+)/);
|
|
110
|
+
if (scriptsMatch) {
|
|
111
|
+
const scriptsSection = scriptsMatch[1];
|
|
112
|
+
const scriptLines = scriptsSection.split('\n');
|
|
113
|
+
for (const line of scriptLines) {
|
|
114
|
+
const match = line.match(/^(\w+)\s*=\s*["']([^"']+)["']/);
|
|
115
|
+
if (match) {
|
|
116
|
+
const [, name, command] = match;
|
|
117
|
+
scriptMap.set(name, {
|
|
118
|
+
name,
|
|
119
|
+
command,
|
|
120
|
+
runner: 'python',
|
|
121
|
+
projectType: 'python',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Also check for [tool.poetry.scripts]
|
|
127
|
+
const poetryMatch = content.match(/\[tool\.poetry\.scripts\]\s*\n((?:[^\[]+\n?)+)/);
|
|
128
|
+
if (poetryMatch) {
|
|
129
|
+
const scriptsSection = poetryMatch[1];
|
|
130
|
+
const scriptLines = scriptsSection.split('\n');
|
|
131
|
+
for (const line of scriptLines) {
|
|
132
|
+
const match = line.match(/^(\w+)\s*=\s*["']([^"']+)["']/);
|
|
133
|
+
if (match) {
|
|
134
|
+
const [, name, command] = match;
|
|
135
|
+
scriptMap.set(name, {
|
|
136
|
+
name,
|
|
137
|
+
command,
|
|
138
|
+
runner: 'poetry',
|
|
139
|
+
projectType: 'python',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return scriptMap;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return new Map();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parse scripts from Cargo.toml (Rust)
|
|
152
|
+
* Cargo doesn't have a scripts section, but we can detect common cargo commands
|
|
153
|
+
*/
|
|
154
|
+
function parseRustScripts(projectPath) {
|
|
155
|
+
const scriptMap = new Map();
|
|
156
|
+
// Common cargo commands that can be run
|
|
157
|
+
const commonCommands = [
|
|
158
|
+
{ name: 'build', command: 'cargo build', runner: 'cargo' },
|
|
159
|
+
{ name: 'run', command: 'cargo run', runner: 'cargo' },
|
|
160
|
+
{ name: 'test', command: 'cargo test', runner: 'cargo' },
|
|
161
|
+
{ name: 'check', command: 'cargo check', runner: 'cargo' },
|
|
162
|
+
{ name: 'clippy', command: 'cargo clippy', runner: 'cargo' },
|
|
163
|
+
{ name: 'fmt', command: 'cargo fmt', runner: 'cargo' },
|
|
164
|
+
];
|
|
165
|
+
for (const cmd of commonCommands) {
|
|
166
|
+
scriptMap.set(cmd.name, {
|
|
167
|
+
name: cmd.name,
|
|
168
|
+
command: cmd.command,
|
|
169
|
+
runner: cmd.runner,
|
|
170
|
+
projectType: 'rust',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return scriptMap;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Parse Makefile targets
|
|
177
|
+
*/
|
|
178
|
+
function parseMakefileScripts(projectPath) {
|
|
179
|
+
const makefilePath = fs.existsSync(path.join(projectPath, 'Makefile'))
|
|
180
|
+
? path.join(projectPath, 'Makefile')
|
|
181
|
+
: path.join(projectPath, 'makefile');
|
|
182
|
+
if (!fs.existsSync(makefilePath)) {
|
|
183
|
+
return new Map();
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const content = fs.readFileSync(makefilePath, 'utf-8');
|
|
187
|
+
const scriptMap = new Map();
|
|
188
|
+
// Parse Makefile targets (basic implementation)
|
|
189
|
+
// Match lines that look like targets: target: dependencies
|
|
190
|
+
const targetRegex = /^([a-zA-Z0-9_-]+)\s*:.*$/gm;
|
|
191
|
+
let match;
|
|
192
|
+
while ((match = targetRegex.exec(content)) !== null) {
|
|
193
|
+
const targetName = match[1];
|
|
194
|
+
// Skip special targets
|
|
195
|
+
if (!targetName.startsWith('.') && targetName !== 'PHONY') {
|
|
196
|
+
scriptMap.set(targetName, {
|
|
197
|
+
name: targetName,
|
|
198
|
+
command: `make ${targetName}`,
|
|
199
|
+
runner: 'make',
|
|
200
|
+
projectType: 'makefile',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return scriptMap;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return new Map();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get all available scripts for a project
|
|
212
|
+
*/
|
|
213
|
+
function getProjectScripts(projectPath) {
|
|
214
|
+
const type = detectProjectType(projectPath);
|
|
215
|
+
let scripts = new Map();
|
|
216
|
+
switch (type) {
|
|
217
|
+
case 'node':
|
|
218
|
+
scripts = parseNodeScripts(projectPath);
|
|
219
|
+
break;
|
|
220
|
+
case 'python':
|
|
221
|
+
scripts = parsePythonScripts(projectPath);
|
|
222
|
+
break;
|
|
223
|
+
case 'rust':
|
|
224
|
+
scripts = parseRustScripts(projectPath);
|
|
225
|
+
break;
|
|
226
|
+
case 'makefile':
|
|
227
|
+
scripts = parseMakefileScripts(projectPath);
|
|
228
|
+
break;
|
|
229
|
+
case 'go':
|
|
230
|
+
// Go projects typically use Makefile or direct go commands
|
|
231
|
+
// Check for Makefile first, otherwise provide common go commands
|
|
232
|
+
const makefileScripts = parseMakefileScripts(projectPath);
|
|
233
|
+
if (makefileScripts.size > 0) {
|
|
234
|
+
scripts = makefileScripts;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Common go commands
|
|
238
|
+
scripts.set('run', {
|
|
239
|
+
name: 'run',
|
|
240
|
+
command: 'go run .',
|
|
241
|
+
runner: 'go',
|
|
242
|
+
projectType: 'go',
|
|
243
|
+
});
|
|
244
|
+
scripts.set('build', {
|
|
245
|
+
name: 'build',
|
|
246
|
+
command: 'go build',
|
|
247
|
+
runner: 'go',
|
|
248
|
+
projectType: 'go',
|
|
249
|
+
});
|
|
250
|
+
scripts.set('test', {
|
|
251
|
+
name: 'test',
|
|
252
|
+
command: 'go test ./...',
|
|
253
|
+
runner: 'go',
|
|
254
|
+
projectType: 'go',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
default:
|
|
259
|
+
// For unknown projects, check for Makefile as fallback
|
|
260
|
+
scripts = parseMakefileScripts(projectPath);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
type,
|
|
265
|
+
scripts,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Handle port conflict - prompt user or auto-kill based on force flag
|
|
270
|
+
*/
|
|
271
|
+
async function handlePortConflict(port, projectName, force) {
|
|
272
|
+
const processInfo = await (0, port_utils_1.getProcessOnPort)(port);
|
|
273
|
+
if (!processInfo) {
|
|
274
|
+
console.error(`Port ${port} appears to be in use, but couldn't identify the process.`);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
console.error(`\n⚠️ Port ${port} is already in use by process ${processInfo.pid} (${processInfo.command})`);
|
|
278
|
+
if (force) {
|
|
279
|
+
console.log(`Killing process ${processInfo.pid} on port ${port}...`);
|
|
280
|
+
const killed = await (0, port_utils_1.killProcessOnPort)(port);
|
|
281
|
+
if (killed) {
|
|
282
|
+
console.log(`✓ Process killed. Retrying...\n`);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
console.error(`Failed to kill process on port ${port}`);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
|
|
292
|
+
const answer = await inquirer.prompt([
|
|
293
|
+
{
|
|
294
|
+
type: 'confirm',
|
|
295
|
+
name: 'kill',
|
|
296
|
+
message: `Kill process ${processInfo.pid} (${processInfo.command}) and continue?`,
|
|
297
|
+
default: false,
|
|
298
|
+
},
|
|
299
|
+
]);
|
|
300
|
+
if (answer.kill) {
|
|
301
|
+
const killed = await (0, port_utils_1.killProcessOnPort)(port);
|
|
302
|
+
if (killed) {
|
|
303
|
+
console.log(`✓ Process killed. Retrying...\n`);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.error(`Failed to kill process on port ${port}`);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
console.log('Cancelled.');
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Check ports proactively before script execution
|
|
319
|
+
*/
|
|
320
|
+
async function checkPortsBeforeExecution(projectPath, scriptName, force) {
|
|
321
|
+
const db = (0, core_1.getDatabaseManager)();
|
|
322
|
+
const project = db.getProjectByPath(projectPath);
|
|
323
|
+
if (!project)
|
|
324
|
+
return true; // Can't check if project not in DB
|
|
325
|
+
const ports = db.getProjectPortsByScript(project.id, scriptName);
|
|
326
|
+
if (ports.length === 0) {
|
|
327
|
+
// Also check ports without script name (general project ports)
|
|
328
|
+
const allPorts = db.getProjectPorts(project.id);
|
|
329
|
+
for (const portInfo of allPorts) {
|
|
330
|
+
const inUse = await (0, port_utils_1.detectPortInUse)(portInfo.port);
|
|
331
|
+
if (inUse) {
|
|
332
|
+
return await handlePortConflict(portInfo.port, project.name, force);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
for (const portInfo of ports) {
|
|
338
|
+
const inUse = await (0, port_utils_1.detectPortInUse)(portInfo.port);
|
|
339
|
+
if (inUse) {
|
|
340
|
+
return await handlePortConflict(portInfo.port, project.name, force);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Execute a script in a project directory
|
|
347
|
+
*/
|
|
348
|
+
function runScript(projectPath, scriptName, args = [], force = false) {
|
|
349
|
+
return new Promise(async (resolve, reject) => {
|
|
350
|
+
const projectScripts = getProjectScripts(projectPath);
|
|
351
|
+
const script = projectScripts.scripts.get(scriptName);
|
|
352
|
+
if (!script) {
|
|
353
|
+
reject(new Error(`Script "${scriptName}" not found in project`));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Proactive port checking
|
|
357
|
+
const canProceed = await checkPortsBeforeExecution(projectPath, scriptName, force);
|
|
358
|
+
if (!canProceed) {
|
|
359
|
+
reject(new Error('Port conflict not resolved'));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
let command;
|
|
363
|
+
let commandArgs;
|
|
364
|
+
switch (script.runner) {
|
|
365
|
+
case 'npm':
|
|
366
|
+
command = 'npm';
|
|
367
|
+
commandArgs = ['run', scriptName, ...args];
|
|
368
|
+
break;
|
|
369
|
+
case 'yarn':
|
|
370
|
+
command = 'yarn';
|
|
371
|
+
commandArgs = [scriptName, ...args];
|
|
372
|
+
break;
|
|
373
|
+
case 'pnpm':
|
|
374
|
+
command = 'pnpm';
|
|
375
|
+
commandArgs = ['run', scriptName, ...args];
|
|
376
|
+
break;
|
|
377
|
+
case 'python':
|
|
378
|
+
command = 'python';
|
|
379
|
+
commandArgs = ['-m', ...script.command.split(' ').slice(1), ...args];
|
|
380
|
+
break;
|
|
381
|
+
case 'poetry':
|
|
382
|
+
command = 'poetry';
|
|
383
|
+
// For poetry, the command is already the module path, use 'run' to execute it
|
|
384
|
+
const modulePath = script.command.split(' ').slice(1).join(' ');
|
|
385
|
+
commandArgs = ['run', 'python', '-m', modulePath, ...args];
|
|
386
|
+
break;
|
|
387
|
+
case 'cargo':
|
|
388
|
+
command = 'cargo';
|
|
389
|
+
commandArgs = scriptName === 'run' ? ['run', ...args] : [scriptName, ...args];
|
|
390
|
+
break;
|
|
391
|
+
case 'go':
|
|
392
|
+
command = 'go';
|
|
393
|
+
commandArgs = script.command.split(' ').slice(1);
|
|
394
|
+
if (scriptName === 'run' && args.length > 0) {
|
|
395
|
+
commandArgs = ['run', ...args];
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
commandArgs = [...commandArgs, ...args];
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
case 'make':
|
|
402
|
+
command = 'make';
|
|
403
|
+
commandArgs = [scriptName, ...args];
|
|
404
|
+
break;
|
|
405
|
+
default:
|
|
406
|
+
// Fallback: try to run the command directly
|
|
407
|
+
const parts = script.command.split(' ');
|
|
408
|
+
command = parts[0];
|
|
409
|
+
commandArgs = [...parts.slice(1), ...args];
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
console.log(`Running: ${command} ${commandArgs.join(' ')}`);
|
|
413
|
+
console.log(`In directory: ${projectPath}\n`);
|
|
414
|
+
// Capture stderr for reactive port conflict detection
|
|
415
|
+
let stderrOutput = '';
|
|
416
|
+
let stdoutOutput = '';
|
|
417
|
+
const child = (0, child_process_1.spawn)(command, commandArgs, {
|
|
418
|
+
cwd: projectPath,
|
|
419
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
420
|
+
shell: process.platform === 'win32',
|
|
421
|
+
});
|
|
422
|
+
// Capture output for error analysis
|
|
423
|
+
if (child.stdout) {
|
|
424
|
+
child.stdout.on('data', (data) => {
|
|
425
|
+
stdoutOutput += data.toString();
|
|
426
|
+
process.stdout.write(data);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (child.stderr) {
|
|
430
|
+
child.stderr.on('data', (data) => {
|
|
431
|
+
stderrOutput += data.toString();
|
|
432
|
+
process.stderr.write(data);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
child.on('close', async (code) => {
|
|
436
|
+
if (code === 0) {
|
|
437
|
+
resolve(0);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
// Reactive port conflict detection
|
|
441
|
+
const errorOutput = stderrOutput + stdoutOutput;
|
|
442
|
+
const port = (0, port_utils_1.extractPortFromError)(errorOutput);
|
|
443
|
+
if (port) {
|
|
444
|
+
const db = (0, core_1.getDatabaseManager)();
|
|
445
|
+
const project = db.getProjectByPath(projectPath);
|
|
446
|
+
const projectName = project?.name || 'project';
|
|
447
|
+
const resolved = await handlePortConflict(port, projectName, force);
|
|
448
|
+
if (resolved) {
|
|
449
|
+
// Retry script execution
|
|
450
|
+
try {
|
|
451
|
+
const retryResult = await runScript(projectPath, scriptName, args, force);
|
|
452
|
+
resolve(retryResult);
|
|
453
|
+
}
|
|
454
|
+
catch (retryError) {
|
|
455
|
+
reject(retryError);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
reject(new Error(`Script exited with code ${code} (port ${port} conflict)`));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
reject(new Error(`Script exited with code ${code}`));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
child.on('error', (error) => {
|
|
468
|
+
reject(new Error(`Failed to execute script: ${error.message}`));
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get the path to the processes file
|
|
474
|
+
*/
|
|
475
|
+
function getProcessesFilePath() {
|
|
476
|
+
const dataDir = path.join(os.homedir(), '.projax');
|
|
477
|
+
if (!fs.existsSync(dataDir)) {
|
|
478
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
479
|
+
}
|
|
480
|
+
return path.join(dataDir, 'processes.json');
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Load running processes from disk
|
|
484
|
+
*/
|
|
485
|
+
function loadProcesses() {
|
|
486
|
+
const filePath = getProcessesFilePath();
|
|
487
|
+
if (!fs.existsSync(filePath)) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
492
|
+
return JSON.parse(content);
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Save running processes to disk
|
|
500
|
+
*/
|
|
501
|
+
function saveProcesses(processes) {
|
|
502
|
+
const filePath = getProcessesFilePath();
|
|
503
|
+
fs.writeFileSync(filePath, JSON.stringify(processes, null, 2));
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Add a process to the tracking file
|
|
507
|
+
*/
|
|
508
|
+
function addProcess(process) {
|
|
509
|
+
const processes = loadProcesses();
|
|
510
|
+
processes.push(process);
|
|
511
|
+
saveProcesses(processes);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Remove a process from tracking by PID
|
|
515
|
+
*/
|
|
516
|
+
function removeProcess(pid) {
|
|
517
|
+
const processes = loadProcesses();
|
|
518
|
+
const filtered = processes.filter(p => p.pid !== pid);
|
|
519
|
+
saveProcesses(filtered);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get all running processes for a project
|
|
523
|
+
*/
|
|
524
|
+
function getProjectProcesses(projectPath) {
|
|
525
|
+
const processes = loadProcesses();
|
|
526
|
+
return processes.filter(p => p.projectPath === projectPath);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Execute a script in the background (minimal logging)
|
|
530
|
+
*/
|
|
531
|
+
function runScriptInBackground(projectPath, projectName, scriptName, args = [], force = false) {
|
|
532
|
+
return new Promise(async (resolve, reject) => {
|
|
533
|
+
const projectScripts = getProjectScripts(projectPath);
|
|
534
|
+
const script = projectScripts.scripts.get(scriptName);
|
|
535
|
+
if (!script) {
|
|
536
|
+
reject(new Error(`Script "${scriptName}" not found in project`));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
// Proactive port checking
|
|
540
|
+
const canProceed = await checkPortsBeforeExecution(projectPath, scriptName, force);
|
|
541
|
+
if (!canProceed) {
|
|
542
|
+
reject(new Error('Port conflict not resolved'));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
let command;
|
|
546
|
+
let commandArgs;
|
|
547
|
+
switch (script.runner) {
|
|
548
|
+
case 'npm':
|
|
549
|
+
command = 'npm';
|
|
550
|
+
commandArgs = ['run', scriptName, ...args];
|
|
551
|
+
break;
|
|
552
|
+
case 'yarn':
|
|
553
|
+
command = 'yarn';
|
|
554
|
+
commandArgs = [scriptName, ...args];
|
|
555
|
+
break;
|
|
556
|
+
case 'pnpm':
|
|
557
|
+
command = 'pnpm';
|
|
558
|
+
commandArgs = ['run', scriptName, ...args];
|
|
559
|
+
break;
|
|
560
|
+
case 'python':
|
|
561
|
+
command = 'python';
|
|
562
|
+
commandArgs = ['-m', ...script.command.split(' ').slice(1), ...args];
|
|
563
|
+
break;
|
|
564
|
+
case 'poetry':
|
|
565
|
+
command = 'poetry';
|
|
566
|
+
const modulePath = script.command.split(' ').slice(1).join(' ');
|
|
567
|
+
commandArgs = ['run', 'python', '-m', modulePath, ...args];
|
|
568
|
+
break;
|
|
569
|
+
case 'cargo':
|
|
570
|
+
command = 'cargo';
|
|
571
|
+
commandArgs = scriptName === 'run' ? ['run', ...args] : [scriptName, ...args];
|
|
572
|
+
break;
|
|
573
|
+
case 'go':
|
|
574
|
+
command = 'go';
|
|
575
|
+
commandArgs = script.command.split(' ').slice(1);
|
|
576
|
+
if (scriptName === 'run' && args.length > 0) {
|
|
577
|
+
commandArgs = ['run', ...args];
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
commandArgs = [...commandArgs, ...args];
|
|
581
|
+
}
|
|
582
|
+
break;
|
|
583
|
+
case 'make':
|
|
584
|
+
command = 'make';
|
|
585
|
+
commandArgs = [scriptName, ...args];
|
|
586
|
+
break;
|
|
587
|
+
default:
|
|
588
|
+
const parts = script.command.split(' ');
|
|
589
|
+
command = parts[0];
|
|
590
|
+
commandArgs = [...parts.slice(1), ...args];
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
// Create log file for the process
|
|
594
|
+
const dataDir = path.join(os.homedir(), '.projax');
|
|
595
|
+
const logsDir = path.join(dataDir, 'logs');
|
|
596
|
+
if (!fs.existsSync(logsDir)) {
|
|
597
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
598
|
+
}
|
|
599
|
+
const logFile = path.join(logsDir, `process-${Date.now()}-${scriptName}.log`);
|
|
600
|
+
// Create write stream for log file (keep reference to prevent GC)
|
|
601
|
+
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
602
|
+
// Spawn process in detached mode with output redirected to log file
|
|
603
|
+
const child = (0, child_process_1.spawn)(command, commandArgs, {
|
|
604
|
+
cwd: projectPath,
|
|
605
|
+
stdio: ['ignore', logStream, logStream], // Redirect stdout and stderr to log file
|
|
606
|
+
detached: true,
|
|
607
|
+
shell: process.platform === 'win32',
|
|
608
|
+
});
|
|
609
|
+
// Don't close the stream - let it stay open for the child process
|
|
610
|
+
// The stream will be closed when the child process exits
|
|
611
|
+
// Store process info
|
|
612
|
+
const processInfo = {
|
|
613
|
+
pid: child.pid,
|
|
614
|
+
projectPath,
|
|
615
|
+
projectName,
|
|
616
|
+
scriptName,
|
|
617
|
+
command: `${command} ${commandArgs.join(' ')}`,
|
|
618
|
+
startedAt: Date.now(),
|
|
619
|
+
logFile,
|
|
620
|
+
};
|
|
621
|
+
addProcess(processInfo);
|
|
622
|
+
// Unref so parent can exit and process runs independently
|
|
623
|
+
child.unref();
|
|
624
|
+
// Show minimal output
|
|
625
|
+
console.log(`✓ Started "${projectName}" (${scriptName}) in background [PID: ${child.pid}]`);
|
|
626
|
+
console.log(` Logs: ${logFile}`);
|
|
627
|
+
console.log(` Command: ${command} ${commandArgs.join(' ')}\n`);
|
|
628
|
+
// For background processes, we can't easily do reactive detection
|
|
629
|
+
// But we can check the log file after a short delay for port conflicts
|
|
630
|
+
setTimeout(async () => {
|
|
631
|
+
try {
|
|
632
|
+
// Wait a bit for process to start and potentially fail
|
|
633
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
634
|
+
if (fs.existsSync(logFile)) {
|
|
635
|
+
const logContent = fs.readFileSync(logFile, 'utf-8');
|
|
636
|
+
const port = (0, port_utils_1.extractPortFromError)(logContent);
|
|
637
|
+
if (port) {
|
|
638
|
+
console.error(`\n⚠️ Port conflict detected in background process: port ${port} is in use`);
|
|
639
|
+
console.error(` Check log file: ${logFile}`);
|
|
640
|
+
console.error(` Use: prx <project> <script> --force to auto-resolve port conflicts\n`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// Ignore errors checking log file
|
|
646
|
+
}
|
|
647
|
+
}, 3000);
|
|
648
|
+
// Resolve immediately since process is running in background
|
|
649
|
+
resolve(0);
|
|
650
|
+
});
|
|
651
|
+
}
|
package/dist/index.d.ts
ADDED