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,92 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
const styles = {
|
|
3
|
+
titleBar: {
|
|
4
|
+
display: 'flex',
|
|
5
|
+
alignItems: 'center',
|
|
6
|
+
justifyContent: 'space-between',
|
|
7
|
+
padding: '8px 12px',
|
|
8
|
+
backgroundColor: 'var(--bg-secondary)',
|
|
9
|
+
WebkitAppRegion: 'drag',
|
|
10
|
+
borderBottom: '1px solid var(--border)',
|
|
11
|
+
},
|
|
12
|
+
title: {
|
|
13
|
+
fontSize: '13px',
|
|
14
|
+
fontWeight: 600,
|
|
15
|
+
color: 'var(--text-primary)',
|
|
16
|
+
},
|
|
17
|
+
controls: {
|
|
18
|
+
display: 'flex',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
gap: '8px',
|
|
21
|
+
WebkitAppRegion: 'no-drag',
|
|
22
|
+
},
|
|
23
|
+
controlButton: {
|
|
24
|
+
width: '24px',
|
|
25
|
+
height: '24px',
|
|
26
|
+
borderRadius: '4px',
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
justifyContent: 'center',
|
|
30
|
+
fontSize: '12px',
|
|
31
|
+
transition: 'background-color 0.15s',
|
|
32
|
+
},
|
|
33
|
+
toggle: {
|
|
34
|
+
padding: '4px 8px',
|
|
35
|
+
borderRadius: '4px',
|
|
36
|
+
fontSize: '11px',
|
|
37
|
+
transition: 'all 0.15s',
|
|
38
|
+
},
|
|
39
|
+
toggleActive: {
|
|
40
|
+
backgroundColor: 'var(--accent)',
|
|
41
|
+
color: 'white',
|
|
42
|
+
},
|
|
43
|
+
toggleInactive: {
|
|
44
|
+
backgroundColor: 'var(--bg-item)',
|
|
45
|
+
color: 'var(--text-secondary)',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
export function TitleBar() {
|
|
49
|
+
const [alwaysOnTop, setAlwaysOnTop] = useState(false);
|
|
50
|
+
const handleToggleAlwaysOnTop = async () => {
|
|
51
|
+
const newValue = !alwaysOnTop;
|
|
52
|
+
setAlwaysOnTop(newValue);
|
|
53
|
+
await window.electronAPI.setAlwaysOnTop(newValue);
|
|
54
|
+
};
|
|
55
|
+
const handleMinimize = () => {
|
|
56
|
+
window.electronAPI.minimizeWindow();
|
|
57
|
+
};
|
|
58
|
+
const handleClose = () => {
|
|
59
|
+
window.electronAPI.closeWindow();
|
|
60
|
+
};
|
|
61
|
+
return (<div style={styles.titleBar}>
|
|
62
|
+
<span style={styles.title}>Port Arranger</span>
|
|
63
|
+
<div style={styles.controls}>
|
|
64
|
+
<button onClick={handleToggleAlwaysOnTop} style={{
|
|
65
|
+
...styles.toggle,
|
|
66
|
+
...(alwaysOnTop ? styles.toggleActive : styles.toggleInactive),
|
|
67
|
+
}} title="항상 위에">
|
|
68
|
+
PIN
|
|
69
|
+
</button>
|
|
70
|
+
<button onClick={handleMinimize} style={{
|
|
71
|
+
...styles.controlButton,
|
|
72
|
+
backgroundColor: 'var(--bg-item)',
|
|
73
|
+
}} onMouseEnter={(e) => {
|
|
74
|
+
e.currentTarget.style.backgroundColor = 'var(--border)';
|
|
75
|
+
}} onMouseLeave={(e) => {
|
|
76
|
+
e.currentTarget.style.backgroundColor = 'var(--bg-item)';
|
|
77
|
+
}} title="최소화">
|
|
78
|
+
-
|
|
79
|
+
</button>
|
|
80
|
+
<button onClick={handleClose} style={{
|
|
81
|
+
...styles.controlButton,
|
|
82
|
+
backgroundColor: 'var(--accent)',
|
|
83
|
+
}} onMouseEnter={(e) => {
|
|
84
|
+
e.currentTarget.style.backgroundColor = 'var(--accent-hover)';
|
|
85
|
+
}} onMouseLeave={(e) => {
|
|
86
|
+
e.currentTarget.style.backgroundColor = 'var(--accent)';
|
|
87
|
+
}} title="닫기">
|
|
88
|
+
x
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>);
|
|
92
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProcessMapping } from '../../../shared/types';
|
|
2
|
+
interface UseProcessesResult {
|
|
3
|
+
processes: Record<string, ProcessMapping>;
|
|
4
|
+
loading: boolean;
|
|
5
|
+
error: string | null;
|
|
6
|
+
stopProcess: (name: string) => Promise<void>;
|
|
7
|
+
openBrowser: (port: number) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function useProcesses(): UseProcessesResult;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
export function useProcesses() {
|
|
3
|
+
const [processes, setProcesses] = useState({});
|
|
4
|
+
const [loading, setLoading] = useState(true);
|
|
5
|
+
const [error, setError] = useState(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const loadInitial = async () => {
|
|
8
|
+
try {
|
|
9
|
+
const data = await window.electronAPI.getProcesses();
|
|
10
|
+
setProcesses(data);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
setError(err.message);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
setLoading(false);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
loadInitial();
|
|
20
|
+
const unsubscribe = window.electronAPI.onProcessesUpdate((updated) => {
|
|
21
|
+
setProcesses(updated);
|
|
22
|
+
});
|
|
23
|
+
return () => {
|
|
24
|
+
unsubscribe();
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
const stopProcess = async (name) => {
|
|
28
|
+
try {
|
|
29
|
+
await window.electronAPI.stopProcess(name);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
setError(err.message);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const openBrowser = async (port) => {
|
|
36
|
+
try {
|
|
37
|
+
await window.electronAPI.openBrowser(port);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
setError(err.message);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return { processes, loading, error, stopProcess, openBrowser };
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './styles/globals.css';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import App from './App';
|
|
4
|
+
import './styles/globals.css';
|
|
5
|
+
const container = document.getElementById('root');
|
|
6
|
+
if (container) {
|
|
7
|
+
const root = createRoot(container);
|
|
8
|
+
root.render(<React.StrictMode>
|
|
9
|
+
<App />
|
|
10
|
+
</React.StrictMode>);
|
|
11
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type InjectionType = 'env' | 'flag' | 'arg' | 'compose';
|
|
2
|
+
export interface ComposePortMapping {
|
|
3
|
+
hostPort: number;
|
|
4
|
+
containerPort: number;
|
|
5
|
+
protocol?: 'tcp' | 'udp';
|
|
6
|
+
}
|
|
7
|
+
export interface ComposeServicePorts {
|
|
8
|
+
serviceName: string;
|
|
9
|
+
ports: ComposePortMapping[];
|
|
10
|
+
}
|
|
11
|
+
export interface AllocatedComposePort extends ComposePortMapping {
|
|
12
|
+
originalHostPort: number;
|
|
13
|
+
newHostPort: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ComposeServicePort {
|
|
16
|
+
serviceName: string;
|
|
17
|
+
port: number;
|
|
18
|
+
running?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type ProcessStatus = 'running' | 'stopped';
|
|
21
|
+
export interface ProcessMapping {
|
|
22
|
+
port: number;
|
|
23
|
+
pid: number;
|
|
24
|
+
command: string;
|
|
25
|
+
originalCommand: string;
|
|
26
|
+
injectionType: InjectionType;
|
|
27
|
+
cwd: string;
|
|
28
|
+
startedAt: string;
|
|
29
|
+
status: ProcessStatus;
|
|
30
|
+
composePorts?: ComposeServicePort[];
|
|
31
|
+
}
|
|
32
|
+
export interface State {
|
|
33
|
+
mappings: Record<string, ProcessMapping>;
|
|
34
|
+
}
|
|
35
|
+
export interface InjectionResult {
|
|
36
|
+
command: string;
|
|
37
|
+
env: Record<string, string>;
|
|
38
|
+
injectionType: InjectionType;
|
|
39
|
+
toolName: string;
|
|
40
|
+
}
|
|
41
|
+
export interface CommandPattern {
|
|
42
|
+
name: string;
|
|
43
|
+
patterns: RegExp[];
|
|
44
|
+
injectionType: InjectionType;
|
|
45
|
+
defaultPort: number;
|
|
46
|
+
injectPort: (cmd: string, port: number) => string;
|
|
47
|
+
}
|
|
48
|
+
export interface ElectronAPI {
|
|
49
|
+
getProcesses: () => Promise<Record<string, ProcessMapping>>;
|
|
50
|
+
onProcessesUpdate: (callback: (processes: Record<string, ProcessMapping>) => void) => () => void;
|
|
51
|
+
stopProcess: (name: string) => Promise<void>;
|
|
52
|
+
restartProcess: (name: string) => Promise<void>;
|
|
53
|
+
openBrowser: (port: number) => Promise<void>;
|
|
54
|
+
setAlwaysOnTop: (value: boolean) => Promise<void>;
|
|
55
|
+
minimizeWindow: () => void;
|
|
56
|
+
closeWindow: () => void;
|
|
57
|
+
}
|
|
58
|
+
declare global {
|
|
59
|
+
interface Window {
|
|
60
|
+
electronAPI: ElectronAPI;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "port-arranger",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Run multiple dev servers without port conflicts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pa": "./dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": ".vite/build/index.cjs",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"start": "node dist/cli/index.js",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"lint": "eslint src --ext .ts",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"build:gui": "electron-forge package",
|
|
23
|
+
"dev:gui": "electron-forge start",
|
|
24
|
+
"make:gui": "electron-forge make"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"chokidar": "^3.6.0",
|
|
29
|
+
"commander": "^12.1.0",
|
|
30
|
+
"detect-port": "^1.6.1",
|
|
31
|
+
"js-yaml": "^4.1.1",
|
|
32
|
+
"tree-kill": "^1.2.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@electron-forge/cli": "^7.5.0",
|
|
36
|
+
"@electron-forge/maker-dmg": "^7.5.0",
|
|
37
|
+
"@electron-forge/maker-zip": "^7.5.0",
|
|
38
|
+
"@electron-forge/plugin-vite": "^7.5.0",
|
|
39
|
+
"@types/js-yaml": "^4.0.9",
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"@types/react": "^18.3.0",
|
|
42
|
+
"@types/react-dom": "^18.3.0",
|
|
43
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
44
|
+
"electron": "^33.0.0",
|
|
45
|
+
"react": "^18.3.1",
|
|
46
|
+
"react-dom": "^18.3.1",
|
|
47
|
+
"typescript": "^5.7.0",
|
|
48
|
+
"vite": "^5.4.0",
|
|
49
|
+
"vitest": "^4.0.18"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"port",
|
|
56
|
+
"dev-server",
|
|
57
|
+
"cli",
|
|
58
|
+
"process-manager",
|
|
59
|
+
"port-conflict",
|
|
60
|
+
"developer-tools",
|
|
61
|
+
"nextjs",
|
|
62
|
+
"vite",
|
|
63
|
+
"express",
|
|
64
|
+
"fastapi"
|
|
65
|
+
],
|
|
66
|
+
"author": "realmindori@gmail.com",
|
|
67
|
+
"license": "MIT",
|
|
68
|
+
"repository": {
|
|
69
|
+
"type": "git",
|
|
70
|
+
"url": "https://github.com/mindori/port-arranger"
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://github.com/mindori/port-arranger#readme",
|
|
73
|
+
"bugs": {
|
|
74
|
+
"url": "https://github.com/mindori/port-arranger/issues"
|
|
75
|
+
}
|
|
76
|
+
}
|