port-arranger 0.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/.vite/build/index.cjs +2 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/cli/commands/list.d.ts +1 -0
- package/dist/cli/commands/list.js +87 -0
- package/dist/cli/commands/run.d.ts +7 -0
- package/dist/cli/commands/run.js +151 -0
- package/dist/cli/commands/stop.d.ts +5 -0
- package/dist/cli/commands/stop.js +105 -0
- package/dist/cli/commands/ui.d.ts +1 -0
- package/dist/cli/commands/ui.js +29 -0
- package/dist/cli/core/compose-parser.d.ts +56 -0
- package/dist/cli/core/compose-parser.js +184 -0
- package/dist/cli/core/compose-parser.test.d.ts +1 -0
- package/dist/cli/core/compose-parser.test.js +262 -0
- package/dist/cli/core/port-finder.d.ts +2 -0
- package/dist/cli/core/port-finder.js +52 -0
- package/dist/cli/core/port-finder.test.d.ts +1 -0
- package/dist/cli/core/port-finder.test.js +106 -0
- package/dist/cli/core/port-injector.d.ts +3 -0
- package/dist/cli/core/port-injector.js +191 -0
- package/dist/cli/core/port-injector.test.d.ts +1 -0
- package/dist/cli/core/port-injector.test.js +264 -0
- package/dist/cli/core/process-manager.d.ts +8 -0
- package/dist/cli/core/process-manager.js +84 -0
- package/dist/cli/core/process-manager.test.d.ts +1 -0
- package/dist/cli/core/process-manager.test.js +50 -0
- package/dist/cli/core/state.d.ts +10 -0
- package/dist/cli/core/state.js +65 -0
- package/dist/cli/core/state.test.d.ts +1 -0
- package/dist/cli/core/state.test.js +72 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +67 -0
- package/dist/gui/main/index.d.ts +1 -0
- package/dist/gui/main/index.js +60 -0
- package/dist/gui/main/ipc-handlers.d.ts +2 -0
- package/dist/gui/main/ipc-handlers.js +66 -0
- package/dist/gui/main/state-watcher.d.ts +7 -0
- package/dist/gui/main/state-watcher.js +56 -0
- package/dist/gui/preload/index.d.ts +1 -0
- package/dist/gui/preload/index.js +20 -0
- package/dist/gui/renderer/App.d.ts +2 -0
- package/dist/gui/renderer/App.js +44 -0
- package/dist/gui/renderer/components/ProcessItem.d.ts +10 -0
- package/dist/gui/renderer/components/ProcessItem.js +115 -0
- package/dist/gui/renderer/components/ProcessList.d.ts +9 -0
- package/dist/gui/renderer/components/ProcessList.js +64 -0
- package/dist/gui/renderer/components/TitleBar.d.ts +2 -0
- package/dist/gui/renderer/components/TitleBar.js +92 -0
- package/dist/gui/renderer/hooks/useProcesses.d.ts +10 -0
- package/dist/gui/renderer/hooks/useProcesses.js +44 -0
- package/dist/gui/renderer/main.d.ts +1 -0
- package/dist/gui/renderer/main.js +11 -0
- package/dist/shared/types.d.ts +62 -0
- package/dist/shared/types.js +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// 포트 플래그 제거 헬퍼
|
|
2
|
+
const removePortFlag = (cmd, flag) => {
|
|
3
|
+
// --port 8000 또는 -p 8000 형태 제거
|
|
4
|
+
const regex = new RegExp(`${flag}\\s+\\d+`, 'g');
|
|
5
|
+
return cmd.replace(regex, '').replace(/\s+/g, ' ').trim();
|
|
6
|
+
};
|
|
7
|
+
// 명령어 패턴 정의
|
|
8
|
+
const patterns = [
|
|
9
|
+
// Next.js - 환경변수 방식
|
|
10
|
+
{
|
|
11
|
+
name: 'next',
|
|
12
|
+
patterns: [/\bnext\s+dev\b/, /\bnext\s+start\b/, /\bnext\b/],
|
|
13
|
+
injectionType: 'env',
|
|
14
|
+
defaultPort: 3000,
|
|
15
|
+
injectPort: (cmd) => cmd,
|
|
16
|
+
},
|
|
17
|
+
// Node.js (Express 등) - 환경변수 방식
|
|
18
|
+
{
|
|
19
|
+
name: 'node',
|
|
20
|
+
patterns: [/\bnode\s+\S+\.js\b/, /\bnode\s+\S+\.mjs\b/, /\bnode\s+\S+\.cjs\b/],
|
|
21
|
+
injectionType: 'env',
|
|
22
|
+
defaultPort: 3000,
|
|
23
|
+
injectPort: (cmd) => cmd,
|
|
24
|
+
},
|
|
25
|
+
// Vite - 플래그 방식
|
|
26
|
+
{
|
|
27
|
+
name: 'vite',
|
|
28
|
+
patterns: [/\bvite\b/],
|
|
29
|
+
injectionType: 'flag',
|
|
30
|
+
defaultPort: 5173,
|
|
31
|
+
injectPort: (cmd, port) => {
|
|
32
|
+
const cleaned = removePortFlag(cmd, '--port');
|
|
33
|
+
return `${cleaned} --port ${port}`;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
// npm run dev (vite 프로젝트) - 플래그 방식
|
|
37
|
+
{
|
|
38
|
+
name: 'npm-dev',
|
|
39
|
+
patterns: [/\bnpm\s+run\s+dev\b/],
|
|
40
|
+
injectionType: 'flag',
|
|
41
|
+
defaultPort: 5173,
|
|
42
|
+
injectPort: (cmd, port) => {
|
|
43
|
+
// npm run dev -- --port 3001 형태로 전달
|
|
44
|
+
const cleaned = cmd.replace(/\s+--\s+--port\s+\d+/, '');
|
|
45
|
+
return `${cleaned} -- --port ${port}`;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
// yarn run dev - 플래그 방식
|
|
49
|
+
{
|
|
50
|
+
name: 'yarn-dev',
|
|
51
|
+
patterns: [/\byarn\s+run\s+dev\b/, /\byarn\s+dev\b/],
|
|
52
|
+
injectionType: 'flag',
|
|
53
|
+
defaultPort: 5173,
|
|
54
|
+
injectPort: (cmd, port) => {
|
|
55
|
+
const cleaned = cmd.replace(/\s+--port\s+\d+/, '');
|
|
56
|
+
return `${cleaned} --port ${port}`;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
// pnpm run dev - 플래그 방식
|
|
60
|
+
{
|
|
61
|
+
name: 'pnpm-dev',
|
|
62
|
+
patterns: [/\bpnpm\s+run\s+dev\b/, /\bpnpm\s+dev\b/],
|
|
63
|
+
injectionType: 'flag',
|
|
64
|
+
defaultPort: 5173,
|
|
65
|
+
injectPort: (cmd, port) => {
|
|
66
|
+
const cleaned = cmd.replace(/\s+--port\s+\d+/, '');
|
|
67
|
+
return `${cleaned} --port ${port}`;
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
// uvicorn - 플래그 방식
|
|
71
|
+
{
|
|
72
|
+
name: 'uvicorn',
|
|
73
|
+
patterns: [/\buvicorn\b/],
|
|
74
|
+
injectionType: 'flag',
|
|
75
|
+
defaultPort: 8000,
|
|
76
|
+
injectPort: (cmd, port) => {
|
|
77
|
+
const cleaned = removePortFlag(cmd, '--port');
|
|
78
|
+
return `${cleaned} --port ${port}`;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
// FastAPI - 플래그 방식
|
|
82
|
+
{
|
|
83
|
+
name: 'fastapi',
|
|
84
|
+
patterns: [/\bfastapi\s+(dev|run)\b/],
|
|
85
|
+
injectionType: 'flag',
|
|
86
|
+
defaultPort: 8000,
|
|
87
|
+
injectPort: (cmd, port) => {
|
|
88
|
+
const cleaned = removePortFlag(cmd, '--port');
|
|
89
|
+
return `${cleaned} --port ${port}`;
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
// Flask - 플래그 방식
|
|
93
|
+
{
|
|
94
|
+
name: 'flask',
|
|
95
|
+
patterns: [/\bflask\s+run\b/],
|
|
96
|
+
injectionType: 'flag',
|
|
97
|
+
defaultPort: 5000,
|
|
98
|
+
injectPort: (cmd, port) => {
|
|
99
|
+
const cleaned = removePortFlag(cmd, '--port');
|
|
100
|
+
return `${cleaned} --port ${port}`;
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
// http-server (npm) - 플래그 방식 (-p)
|
|
104
|
+
{
|
|
105
|
+
name: 'http-server',
|
|
106
|
+
patterns: [/\bhttp-server\b/],
|
|
107
|
+
injectionType: 'flag',
|
|
108
|
+
defaultPort: 8080,
|
|
109
|
+
injectPort: (cmd, port) => {
|
|
110
|
+
const cleaned = removePortFlag(cmd, '-p');
|
|
111
|
+
return `${cleaned} -p ${port}`;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
// Python http.server - 인자 방식
|
|
115
|
+
{
|
|
116
|
+
name: 'http.server',
|
|
117
|
+
patterns: [/python3?\s+-m\s+http\.server/],
|
|
118
|
+
injectionType: 'arg',
|
|
119
|
+
defaultPort: 8000,
|
|
120
|
+
injectPort: (cmd, port) => {
|
|
121
|
+
// 기존 포트 번호 제거 (http.server 뒤의 숫자)
|
|
122
|
+
const cleaned = cmd.replace(/(http\.server)\s+\d+/, '$1');
|
|
123
|
+
return `${cleaned} ${port}`;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
// Django runserver - 인자 방식
|
|
127
|
+
{
|
|
128
|
+
name: 'django',
|
|
129
|
+
patterns: [/manage\.py\s+runserver/],
|
|
130
|
+
injectionType: 'arg',
|
|
131
|
+
defaultPort: 8000,
|
|
132
|
+
injectPort: (cmd, port) => {
|
|
133
|
+
// 기존 포트 번호 제거 (runserver 뒤의 host:port 또는 숫자)
|
|
134
|
+
// host:port를 먼저 매칭해야 함 (0.0.0.0:8000 등)
|
|
135
|
+
const cleaned = cmd.replace(/(runserver)\s+([\w.]+:\d+|\d+)/, '$1');
|
|
136
|
+
return `${cleaned} ${port}`;
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
// Docker Compose - compose 방식
|
|
140
|
+
{
|
|
141
|
+
name: 'docker-compose',
|
|
142
|
+
patterns: [/docker\s+compose\s+(up|start|run)\b/],
|
|
143
|
+
injectionType: 'compose',
|
|
144
|
+
defaultPort: 0, // compose는 yml에서 파싱
|
|
145
|
+
injectPort: (cmd) => cmd, // compose는 run.ts에서 별도 처리
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
function findMatchingPattern(command) {
|
|
149
|
+
for (const pattern of patterns) {
|
|
150
|
+
for (const regex of pattern.patterns) {
|
|
151
|
+
if (regex.test(command)) {
|
|
152
|
+
return pattern;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
// 도구별 기본 포트 조회
|
|
159
|
+
export function getDefaultPort(command) {
|
|
160
|
+
const matchedPattern = findMatchingPattern(command);
|
|
161
|
+
return matchedPattern?.defaultPort ?? 3000;
|
|
162
|
+
}
|
|
163
|
+
export function injectPort(originalCommand, port) {
|
|
164
|
+
const matchedPattern = findMatchingPattern(originalCommand);
|
|
165
|
+
if (matchedPattern) {
|
|
166
|
+
const injectionType = matchedPattern.injectionType;
|
|
167
|
+
const env = {};
|
|
168
|
+
if (injectionType === 'env') {
|
|
169
|
+
env.PORT = String(port);
|
|
170
|
+
return {
|
|
171
|
+
command: originalCommand,
|
|
172
|
+
env,
|
|
173
|
+
injectionType,
|
|
174
|
+
toolName: matchedPattern.name,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
command: matchedPattern.injectPort(originalCommand, port),
|
|
179
|
+
env,
|
|
180
|
+
injectionType,
|
|
181
|
+
toolName: matchedPattern.name,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// Fallback: 환경변수 방식
|
|
185
|
+
return {
|
|
186
|
+
command: originalCommand,
|
|
187
|
+
env: { PORT: String(port) },
|
|
188
|
+
injectionType: 'env',
|
|
189
|
+
toolName: 'unknown',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { injectPort, getDefaultPort } from './port-injector.js';
|
|
3
|
+
describe('injectPort', () => {
|
|
4
|
+
// 환경변수 방식 (PORT=X)
|
|
5
|
+
describe('환경변수 방식 (env)', () => {
|
|
6
|
+
it('next dev → PORT 환경변수', () => {
|
|
7
|
+
const result = injectPort('next dev', 3001);
|
|
8
|
+
expect(result.env.PORT).toBe('3001');
|
|
9
|
+
expect(result.command).toBe('next dev');
|
|
10
|
+
expect(result.toolName).toBe('next');
|
|
11
|
+
expect(result.injectionType).toBe('env');
|
|
12
|
+
});
|
|
13
|
+
it('npx next dev → PORT 환경변수', () => {
|
|
14
|
+
const result = injectPort('npx next dev', 3001);
|
|
15
|
+
expect(result.env.PORT).toBe('3001');
|
|
16
|
+
expect(result.toolName).toBe('next');
|
|
17
|
+
});
|
|
18
|
+
it('node server.js → PORT 환경변수', () => {
|
|
19
|
+
const result = injectPort('node server.js', 3001);
|
|
20
|
+
expect(result.env.PORT).toBe('3001');
|
|
21
|
+
expect(result.toolName).toBe('node');
|
|
22
|
+
expect(result.injectionType).toBe('env');
|
|
23
|
+
});
|
|
24
|
+
it('node index.js → PORT 환경변수', () => {
|
|
25
|
+
const result = injectPort('node index.js', 3001);
|
|
26
|
+
expect(result.env.PORT).toBe('3001');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
// 플래그 방식 (--port X)
|
|
30
|
+
describe('플래그 방식 (flag)', () => {
|
|
31
|
+
it('vite → --port 플래그', () => {
|
|
32
|
+
const result = injectPort('vite', 3001);
|
|
33
|
+
expect(result.command).toBe('vite --port 3001');
|
|
34
|
+
expect(result.toolName).toBe('vite');
|
|
35
|
+
expect(result.injectionType).toBe('flag');
|
|
36
|
+
});
|
|
37
|
+
it('vite dev → --port 플래그', () => {
|
|
38
|
+
const result = injectPort('vite dev', 3001);
|
|
39
|
+
expect(result.command).toBe('vite dev --port 3001');
|
|
40
|
+
expect(result.toolName).toBe('vite');
|
|
41
|
+
});
|
|
42
|
+
it('npx vite → --port 플래그', () => {
|
|
43
|
+
const result = injectPort('npx vite', 3001);
|
|
44
|
+
expect(result.command).toBe('npx vite --port 3001');
|
|
45
|
+
expect(result.toolName).toBe('vite');
|
|
46
|
+
});
|
|
47
|
+
it('uvicorn main:app → --port 플래그', () => {
|
|
48
|
+
const result = injectPort('uvicorn main:app', 8001);
|
|
49
|
+
expect(result.command).toBe('uvicorn main:app --port 8001');
|
|
50
|
+
expect(result.toolName).toBe('uvicorn');
|
|
51
|
+
expect(result.injectionType).toBe('flag');
|
|
52
|
+
});
|
|
53
|
+
it('fastapi dev → --port 플래그', () => {
|
|
54
|
+
const result = injectPort('fastapi dev', 8001);
|
|
55
|
+
expect(result.command).toBe('fastapi dev --port 8001');
|
|
56
|
+
expect(result.toolName).toBe('fastapi');
|
|
57
|
+
});
|
|
58
|
+
it('flask run → --port 플래그', () => {
|
|
59
|
+
const result = injectPort('flask run', 5001);
|
|
60
|
+
expect(result.command).toBe('flask run --port 5001');
|
|
61
|
+
expect(result.toolName).toBe('flask');
|
|
62
|
+
});
|
|
63
|
+
it('npx http-server → -p 플래그', () => {
|
|
64
|
+
const result = injectPort('npx http-server', 8080);
|
|
65
|
+
expect(result.command).toBe('npx http-server -p 8080');
|
|
66
|
+
expect(result.toolName).toBe('http-server');
|
|
67
|
+
});
|
|
68
|
+
it('http-server → -p 플래그', () => {
|
|
69
|
+
const result = injectPort('http-server .', 8080);
|
|
70
|
+
expect(result.command).toBe('http-server . -p 8080');
|
|
71
|
+
expect(result.toolName).toBe('http-server');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
// 인자 방식 (마지막 인자로 포트)
|
|
75
|
+
describe('인자 방식 (arg)', () => {
|
|
76
|
+
it('python -m http.server → 마지막 인자', () => {
|
|
77
|
+
const result = injectPort('python -m http.server', 8000);
|
|
78
|
+
expect(result.command).toBe('python -m http.server 8000');
|
|
79
|
+
expect(result.toolName).toBe('http.server');
|
|
80
|
+
expect(result.injectionType).toBe('arg');
|
|
81
|
+
});
|
|
82
|
+
it('python3 -m http.server → 마지막 인자', () => {
|
|
83
|
+
const result = injectPort('python3 -m http.server', 8000);
|
|
84
|
+
expect(result.command).toBe('python3 -m http.server 8000');
|
|
85
|
+
expect(result.toolName).toBe('http.server');
|
|
86
|
+
});
|
|
87
|
+
it('manage.py runserver → 포트 인자', () => {
|
|
88
|
+
const result = injectPort('python manage.py runserver', 8000);
|
|
89
|
+
expect(result.command).toBe('python manage.py runserver 8000');
|
|
90
|
+
expect(result.toolName).toBe('django');
|
|
91
|
+
});
|
|
92
|
+
it('./manage.py runserver → 포트 인자', () => {
|
|
93
|
+
const result = injectPort('./manage.py runserver', 8000);
|
|
94
|
+
expect(result.command).toBe('./manage.py runserver 8000');
|
|
95
|
+
expect(result.toolName).toBe('django');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// Fallback
|
|
99
|
+
describe('fallback (인식되지 않는 명령어)', () => {
|
|
100
|
+
it('인식 안 되는 명령어 → PORT 환경변수', () => {
|
|
101
|
+
const result = injectPort('my-custom-server', 3001);
|
|
102
|
+
expect(result.env.PORT).toBe('3001');
|
|
103
|
+
expect(result.command).toBe('my-custom-server');
|
|
104
|
+
expect(result.toolName).toBe('unknown');
|
|
105
|
+
expect(result.injectionType).toBe('env');
|
|
106
|
+
});
|
|
107
|
+
it('임의의 스크립트 → PORT 환경변수', () => {
|
|
108
|
+
const result = injectPort('./start.sh', 3001);
|
|
109
|
+
expect(result.env.PORT).toBe('3001');
|
|
110
|
+
expect(result.toolName).toBe('unknown');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// 기존 포트 옵션이 있는 경우
|
|
114
|
+
describe('기존 포트 옵션 처리', () => {
|
|
115
|
+
it('vite --port 3000 → 포트 교체', () => {
|
|
116
|
+
const result = injectPort('vite --port 3000', 3001);
|
|
117
|
+
expect(result.command).toBe('vite --port 3001');
|
|
118
|
+
});
|
|
119
|
+
it('uvicorn main:app --port 8000 → 포트 교체', () => {
|
|
120
|
+
const result = injectPort('uvicorn main:app --port 8000', 8001);
|
|
121
|
+
expect(result.command).toBe('uvicorn main:app --port 8001');
|
|
122
|
+
});
|
|
123
|
+
it('python http.server 기존 포트 → 포트 교체', () => {
|
|
124
|
+
const result = injectPort('python3 -m http.server 3000', 3002);
|
|
125
|
+
expect(result.command).toBe('python3 -m http.server 3002');
|
|
126
|
+
expect(result.injectionType).toBe('arg');
|
|
127
|
+
});
|
|
128
|
+
it('flask run --port 5000 → 포트 교체', () => {
|
|
129
|
+
const result = injectPort('flask run --port 5000', 5001);
|
|
130
|
+
expect(result.command).toBe('flask run --port 5001');
|
|
131
|
+
});
|
|
132
|
+
it('fastapi dev --port 8000 → 포트 교체', () => {
|
|
133
|
+
const result = injectPort('fastapi dev --port 8000', 8001);
|
|
134
|
+
expect(result.command).toBe('fastapi dev --port 8001');
|
|
135
|
+
});
|
|
136
|
+
it('http-server -p 8080 → 포트 교체', () => {
|
|
137
|
+
const result = injectPort('http-server -p 8080', 9000);
|
|
138
|
+
expect(result.command).toBe('http-server -p 9000');
|
|
139
|
+
});
|
|
140
|
+
it('django runserver 기존 포트 → 포트 교체', () => {
|
|
141
|
+
const result = injectPort('python manage.py runserver 8000', 8001);
|
|
142
|
+
expect(result.command).toBe('python manage.py runserver 8001');
|
|
143
|
+
});
|
|
144
|
+
it('django runserver host:port → 포트 교체', () => {
|
|
145
|
+
const result = injectPort('python manage.py runserver 0.0.0.0:8000', 8001);
|
|
146
|
+
expect(result.command).toBe('python manage.py runserver 8001');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// npm run dev 방식
|
|
150
|
+
describe('npm run dev 방식', () => {
|
|
151
|
+
it('npm run dev → -- --port 플래그', () => {
|
|
152
|
+
const result = injectPort('npm run dev', 3001);
|
|
153
|
+
expect(result.command).toBe('npm run dev -- --port 3001');
|
|
154
|
+
expect(result.injectionType).toBe('flag');
|
|
155
|
+
});
|
|
156
|
+
it('npm run dev에 이미 포트가 지정된 경우 교체해야 한다', () => {
|
|
157
|
+
const result = injectPort('npm run dev -- --port 3000', 3001);
|
|
158
|
+
expect(result.command).toBe('npm run dev -- --port 3001');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
// yarn/pnpm run dev 방식
|
|
162
|
+
describe('yarn/pnpm run dev 방식', () => {
|
|
163
|
+
it('yarn run dev → --port 플래그', () => {
|
|
164
|
+
const result = injectPort('yarn run dev', 3001);
|
|
165
|
+
expect(result.command).toBe('yarn run dev --port 3001');
|
|
166
|
+
expect(result.injectionType).toBe('flag');
|
|
167
|
+
});
|
|
168
|
+
it('pnpm run dev → --port 플래그', () => {
|
|
169
|
+
const result = injectPort('pnpm run dev', 3001);
|
|
170
|
+
expect(result.command).toBe('pnpm run dev --port 3001');
|
|
171
|
+
expect(result.injectionType).toBe('flag');
|
|
172
|
+
});
|
|
173
|
+
it('yarn dev (shortcut) → --port 플래그', () => {
|
|
174
|
+
const result = injectPort('yarn dev', 3001);
|
|
175
|
+
expect(result.command).toBe('yarn dev --port 3001');
|
|
176
|
+
expect(result.toolName).toBe('yarn-dev');
|
|
177
|
+
});
|
|
178
|
+
it('pnpm dev (shortcut) → --port 플래그', () => {
|
|
179
|
+
const result = injectPort('pnpm dev', 3001);
|
|
180
|
+
expect(result.command).toBe('pnpm dev --port 3001');
|
|
181
|
+
expect(result.toolName).toBe('pnpm-dev');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
// Docker Compose 방식
|
|
185
|
+
describe('Docker Compose 방식 (compose)', () => {
|
|
186
|
+
it('docker compose up → compose 타입', () => {
|
|
187
|
+
const result = injectPort('docker compose up', 3001);
|
|
188
|
+
expect(result.injectionType).toBe('compose');
|
|
189
|
+
expect(result.toolName).toBe('docker-compose');
|
|
190
|
+
expect(result.command).toBe('docker compose up'); // 원본 유지
|
|
191
|
+
});
|
|
192
|
+
it('docker compose up -d → compose 타입', () => {
|
|
193
|
+
const result = injectPort('docker compose up -d', 3001);
|
|
194
|
+
expect(result.injectionType).toBe('compose');
|
|
195
|
+
expect(result.toolName).toBe('docker-compose');
|
|
196
|
+
});
|
|
197
|
+
it('docker compose up service1 service2 → compose 타입', () => {
|
|
198
|
+
const result = injectPort('docker compose up web api', 3001);
|
|
199
|
+
expect(result.injectionType).toBe('compose');
|
|
200
|
+
expect(result.toolName).toBe('docker-compose');
|
|
201
|
+
});
|
|
202
|
+
it('docker compose start → compose 타입', () => {
|
|
203
|
+
const result = injectPort('docker compose start web', 3001);
|
|
204
|
+
expect(result.injectionType).toBe('compose');
|
|
205
|
+
expect(result.toolName).toBe('docker-compose');
|
|
206
|
+
});
|
|
207
|
+
it('docker compose run → compose 타입', () => {
|
|
208
|
+
const result = injectPort('docker compose run api', 3001);
|
|
209
|
+
expect(result.injectionType).toBe('compose');
|
|
210
|
+
expect(result.toolName).toBe('docker-compose');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe('getDefaultPort', () => {
|
|
215
|
+
describe('도구별 기본 포트 반환', () => {
|
|
216
|
+
it('next dev → 3000', () => {
|
|
217
|
+
expect(getDefaultPort('next dev')).toBe(3000);
|
|
218
|
+
});
|
|
219
|
+
it('node server.js → 3000', () => {
|
|
220
|
+
expect(getDefaultPort('node server.js')).toBe(3000);
|
|
221
|
+
});
|
|
222
|
+
it('vite → 5173', () => {
|
|
223
|
+
expect(getDefaultPort('vite')).toBe(5173);
|
|
224
|
+
});
|
|
225
|
+
it('npm run dev → 5173', () => {
|
|
226
|
+
expect(getDefaultPort('npm run dev')).toBe(5173);
|
|
227
|
+
});
|
|
228
|
+
it('yarn dev → 5173', () => {
|
|
229
|
+
expect(getDefaultPort('yarn dev')).toBe(5173);
|
|
230
|
+
});
|
|
231
|
+
it('pnpm dev → 5173', () => {
|
|
232
|
+
expect(getDefaultPort('pnpm dev')).toBe(5173);
|
|
233
|
+
});
|
|
234
|
+
it('uvicorn main:app → 8000', () => {
|
|
235
|
+
expect(getDefaultPort('uvicorn main:app')).toBe(8000);
|
|
236
|
+
});
|
|
237
|
+
it('fastapi dev main.py → 8000', () => {
|
|
238
|
+
expect(getDefaultPort('fastapi dev main.py')).toBe(8000);
|
|
239
|
+
});
|
|
240
|
+
it('flask run → 5000', () => {
|
|
241
|
+
expect(getDefaultPort('flask run')).toBe(5000);
|
|
242
|
+
});
|
|
243
|
+
it('http-server → 8080', () => {
|
|
244
|
+
expect(getDefaultPort('http-server .')).toBe(8080);
|
|
245
|
+
});
|
|
246
|
+
it('python -m http.server → 8000', () => {
|
|
247
|
+
expect(getDefaultPort('python -m http.server')).toBe(8000);
|
|
248
|
+
});
|
|
249
|
+
it('python manage.py runserver → 8000', () => {
|
|
250
|
+
expect(getDefaultPort('python manage.py runserver')).toBe(8000);
|
|
251
|
+
});
|
|
252
|
+
it('docker compose up -d → 0 (yml에서 파싱)', () => {
|
|
253
|
+
expect(getDefaultPort('docker compose up -d')).toBe(0);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
describe('알 수 없는 명령어는 3000 반환', () => {
|
|
257
|
+
it('my-custom-server → 3000', () => {
|
|
258
|
+
expect(getDefaultPort('my-custom-server')).toBe(3000);
|
|
259
|
+
});
|
|
260
|
+
it('./start.sh → 3000', () => {
|
|
261
|
+
expect(getDefaultPort('./start.sh')).toBe(3000);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function spawnProcess(name: string, command: string, env: Record<string, string>, cwd: string): Promise<number>;
|
|
2
|
+
export declare function killProcess(pid: number): Promise<void>;
|
|
3
|
+
export declare function isProcessRunning(pid: number): boolean;
|
|
4
|
+
export interface ComposeServiceStatus {
|
|
5
|
+
serviceName: string;
|
|
6
|
+
running: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function getComposeServicesStatus(cwd: string): Promise<Map<string, boolean>>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import treeKill from 'tree-kill';
|
|
3
|
+
export async function spawnProcess(name, command, env, cwd) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const [cmd, ...args] = command.split(' ').filter(Boolean);
|
|
6
|
+
const child = spawn(cmd, args, {
|
|
7
|
+
cwd,
|
|
8
|
+
env: { ...process.env, ...env },
|
|
9
|
+
detached: true,
|
|
10
|
+
stdio: 'ignore',
|
|
11
|
+
});
|
|
12
|
+
child.on('error', (error) => {
|
|
13
|
+
reject(new Error(`Failed to start process: ${error.message}`));
|
|
14
|
+
});
|
|
15
|
+
// 프로세스가 시작되면 PID 반환
|
|
16
|
+
if (child.pid) {
|
|
17
|
+
child.unref();
|
|
18
|
+
resolve(child.pid);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
reject(new Error('Could not get process PID'));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export async function killProcess(pid) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
treeKill(pid, 'SIGTERM', (err) => {
|
|
28
|
+
if (err) {
|
|
29
|
+
// 프로세스가 이미 종료된 경우 무시
|
|
30
|
+
if (err.message?.includes('No such process') ||
|
|
31
|
+
err.message?.includes('ESRCH')) {
|
|
32
|
+
resolve();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
reject(err);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function isProcessRunning(pid) {
|
|
43
|
+
try {
|
|
44
|
+
// kill(pid, 0)은 프로세스 존재 여부만 확인 (실제로 시그널을 보내지 않음)
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function getComposeServicesStatus(cwd) {
|
|
53
|
+
const { execSync } = await import('child_process');
|
|
54
|
+
const statusMap = new Map();
|
|
55
|
+
try {
|
|
56
|
+
// docker compose ps -a --format json으로 서비스 상태 조회 (중지된 것 포함)
|
|
57
|
+
const output = execSync('docker compose ps -a --format json', {
|
|
58
|
+
cwd,
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
// 각 줄이 JSON 객체
|
|
63
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
try {
|
|
66
|
+
const container = JSON.parse(line);
|
|
67
|
+
const serviceName = container.Service || container.Name;
|
|
68
|
+
const state = container.State || '';
|
|
69
|
+
const isRunning = state.toLowerCase() === 'running';
|
|
70
|
+
// 이미 running으로 설정된 서비스는 덮어쓰지 않음
|
|
71
|
+
if (!statusMap.has(serviceName) || isRunning) {
|
|
72
|
+
statusMap.set(serviceName, isRunning);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// JSON 파싱 실패 시 무시
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// docker compose ps 실패 시 빈 맵 반환
|
|
82
|
+
}
|
|
83
|
+
return statusMap;
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { spawnProcess, killProcess, isProcessRunning } from './process-manager.js';
|
|
3
|
+
describe('process-manager', () => {
|
|
4
|
+
const spawnedPids = [];
|
|
5
|
+
afterEach(async () => {
|
|
6
|
+
// 테스트 후 생성된 프로세스 정리
|
|
7
|
+
for (const pid of spawnedPids) {
|
|
8
|
+
try {
|
|
9
|
+
await killProcess(pid);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// 이미 종료된 프로세스 무시
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
spawnedPids.length = 0;
|
|
16
|
+
});
|
|
17
|
+
it('프로세스를 생성하고 PID를 반환해야 한다', async () => {
|
|
18
|
+
const pid = await spawnProcess('test', 'node -e "setTimeout(() => {}, 30000)"', {}, process.cwd());
|
|
19
|
+
spawnedPids.push(pid);
|
|
20
|
+
expect(pid).toBeGreaterThan(0);
|
|
21
|
+
});
|
|
22
|
+
it('프로세스 생존 여부를 확인할 수 있어야 한다', async () => {
|
|
23
|
+
const pid = await spawnProcess('test', 'node -e "setTimeout(() => {}, 30000)"', {}, process.cwd());
|
|
24
|
+
spawnedPids.push(pid);
|
|
25
|
+
expect(isProcessRunning(pid)).toBe(true);
|
|
26
|
+
await killProcess(pid);
|
|
27
|
+
// 프로세스 종료 대기
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
29
|
+
expect(isProcessRunning(pid)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
it('환경변수를 프로세스에 전달할 수 있어야 한다', async () => {
|
|
32
|
+
// 환경변수 설정 후 즉시 종료되는 프로세스
|
|
33
|
+
const pid = await spawnProcess('test', 'node -e "console.log(process.env.TEST_VAR); setTimeout(() => {}, 30000)"', { TEST_VAR: 'hello' }, process.cwd());
|
|
34
|
+
spawnedPids.push(pid);
|
|
35
|
+
expect(pid).toBeGreaterThan(0);
|
|
36
|
+
expect(isProcessRunning(pid)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('존재하지 않는 PID에 대해 isProcessRunning이 false를 반환해야 한다', () => {
|
|
39
|
+
const nonExistentPid = 999999999;
|
|
40
|
+
expect(isProcessRunning(nonExistentPid)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
it('프로세스 종료 후 재확인 시 false를 반환해야 한다', async () => {
|
|
43
|
+
const pid = await spawnProcess('test', 'node -e "setTimeout(() => {}, 30000)"', {}, process.cwd());
|
|
44
|
+
spawnedPids.push(pid);
|
|
45
|
+
expect(isProcessRunning(pid)).toBe(true);
|
|
46
|
+
await killProcess(pid);
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
48
|
+
expect(isProcessRunning(pid)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { State, ProcessMapping } from '../../shared/types.js';
|
|
2
|
+
export declare function setStatePath(path: string): void;
|
|
3
|
+
export declare function resetStatePath(): void;
|
|
4
|
+
export declare function getStatePath(): string;
|
|
5
|
+
export declare function loadState(): Promise<State>;
|
|
6
|
+
export declare function saveState(state: State): Promise<void>;
|
|
7
|
+
export declare function addProcess(name: string, mapping: ProcessMapping): Promise<void>;
|
|
8
|
+
export declare function removeProcess(name: string): Promise<void>;
|
|
9
|
+
export declare function getAllProcesses(): Promise<Record<string, ProcessMapping>>;
|
|
10
|
+
export declare function getProcess(name: string): Promise<ProcessMapping | undefined>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
const DEFAULT_STATE_DIR = join(homedir(), '.port-arranger');
|
|
5
|
+
const DEFAULT_STATE_PATH = join(DEFAULT_STATE_DIR, 'state.json');
|
|
6
|
+
let statePath = DEFAULT_STATE_PATH;
|
|
7
|
+
// 테스트용 상태 파일 경로 설정
|
|
8
|
+
export function setStatePath(path) {
|
|
9
|
+
statePath = path;
|
|
10
|
+
}
|
|
11
|
+
// 기본 경로로 리셋
|
|
12
|
+
export function resetStatePath() {
|
|
13
|
+
statePath = DEFAULT_STATE_PATH;
|
|
14
|
+
}
|
|
15
|
+
export function getStatePath() {
|
|
16
|
+
return statePath;
|
|
17
|
+
}
|
|
18
|
+
const emptyState = { mappings: {} };
|
|
19
|
+
export async function loadState() {
|
|
20
|
+
try {
|
|
21
|
+
const content = await readFile(statePath, 'utf-8');
|
|
22
|
+
return JSON.parse(content);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// 파일이 없거나 읽기 실패 시 빈 상태 반환
|
|
26
|
+
if (error.code === 'ENOENT') {
|
|
27
|
+
return { ...emptyState };
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function saveState(state) {
|
|
33
|
+
// 디렉토리가 없으면 생성
|
|
34
|
+
const dir = dirname(statePath);
|
|
35
|
+
await mkdir(dir, { recursive: true });
|
|
36
|
+
await writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
export async function addProcess(name, mapping) {
|
|
39
|
+
const state = await loadState();
|
|
40
|
+
const newState = {
|
|
41
|
+
...state,
|
|
42
|
+
mappings: {
|
|
43
|
+
...state.mappings,
|
|
44
|
+
[name]: mapping,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
await saveState(newState);
|
|
48
|
+
}
|
|
49
|
+
export async function removeProcess(name) {
|
|
50
|
+
const state = await loadState();
|
|
51
|
+
const { [name]: _, ...rest } = state.mappings;
|
|
52
|
+
const newState = {
|
|
53
|
+
...state,
|
|
54
|
+
mappings: rest,
|
|
55
|
+
};
|
|
56
|
+
await saveState(newState);
|
|
57
|
+
}
|
|
58
|
+
export async function getAllProcesses() {
|
|
59
|
+
const state = await loadState();
|
|
60
|
+
return state.mappings;
|
|
61
|
+
}
|
|
62
|
+
export async function getProcess(name) {
|
|
63
|
+
const state = await loadState();
|
|
64
|
+
return state.mappings[name];
|
|
65
|
+
}
|