@tertium/hlpr 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -45,126 +45,23 @@ hlpr -f ssh init dir
|
|
|
45
45
|
|
|
46
46
|
## Available Commands
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
#### file rename
|
|
51
|
-
|
|
52
|
-
Recursively rename files and folders according to a specific case style.
|
|
53
|
-
|
|
54
|
-
**Supported styles:**
|
|
55
|
-
- `title_underscore` - Title_Case_With_Underscores
|
|
56
|
-
- `snake` - snake_case_lowercase
|
|
57
|
-
- `kebab` - kebab-case-lowercase
|
|
58
|
-
- `camel` - camelCaseLowerFirst
|
|
59
|
-
- `pascal` - PascalCaseUpperFirst
|
|
60
|
-
- `upper` - UPPER_CASE_WITH_UNDERSCORES
|
|
61
|
-
- `lower` - lower_case_with_underscores
|
|
62
|
-
|
|
63
|
-
**Usage:**
|
|
64
|
-
```bash
|
|
65
|
-
# Dry run (preview changes without applying)
|
|
66
|
-
hlpr file rename ./my-project kebab --dry
|
|
67
|
-
hlpr file rename ./src snake -n
|
|
68
|
-
|
|
69
|
-
# Apply changes
|
|
70
|
-
hlpr file rename ./my-project kebab
|
|
71
|
-
hlpr file rename ./docs pascal
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Features:**
|
|
75
|
-
- ✅ Recursive directory traversal
|
|
76
|
-
- ✅ Extension preservation
|
|
77
|
-
- ✅ Leading dot file support (e.g., `.gitignore`)
|
|
78
|
-
- ✅ Case-only rename handling (Windows compatibility)
|
|
79
|
-
- ✅ Conflict resolution (adds `_N` suffix)
|
|
80
|
-
- ✅ Dry-run mode (`--dry` or `-n`)
|
|
81
|
-
|
|
82
|
-
**See:** [`src/commands/file/rename/README.md`](src/commands/file/rename/README.md) for full documentation.
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
#### help
|
|
48
|
+
See individual command documentation for detailed usage, options, and examples.
|
|
87
49
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
**Usage:**
|
|
91
|
-
```bash
|
|
92
|
-
hlpr help
|
|
93
|
-
```
|
|
50
|
+
### TypeScript Commands
|
|
94
51
|
|
|
95
|
-
|
|
52
|
+
- **[file rename](src/commands/file/rename/README.md)** - Recursively rename files/folders with various case styles
|
|
53
|
+
- **[help](src/commands/help/README.md)** - Display help information about hlpr commands
|
|
54
|
+
- **[process list-port](src/commands/process/list-port/README.md)** - List processes running on a port
|
|
55
|
+
- **[process kill-port](src/commands/process/kill-port/README.md)** - Kill processes on a port
|
|
96
56
|
|
|
97
57
|
### Shell Script Commands
|
|
98
58
|
|
|
99
59
|
Shell scripts support variable substitution using `{{variable}}` syntax.
|
|
100
60
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
**Usage:**
|
|
106
|
-
```bash
|
|
107
|
-
hlpr git <command>
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**Commands:**
|
|
111
|
-
- `fodd` - Fetch and update develop branch from origin
|
|
112
|
-
- `precommit` - Run build and stage binaries before commit
|
|
113
|
-
- `switch-clean` - Safely switch to a branch and delete the previous one (local & remote)
|
|
114
|
-
|
|
115
|
-
**See:** [`src/commands/git/README.md`](src/commands/git/README.md) for full documentation.
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
#### hello
|
|
120
|
-
|
|
121
|
-
Example/demo commands for testing the hlpr CLI tool.
|
|
122
|
-
|
|
123
|
-
**Usage:**
|
|
124
|
-
```bash
|
|
125
|
-
hlpr hello world
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Prompts for input and prints a personalized greeting with variable substitution.
|
|
129
|
-
|
|
130
|
-
**See:** [`src/commands/hello/README.md`](src/commands/hello/README.md) for full documentation.
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
#### nvm
|
|
135
|
-
|
|
136
|
-
Node Version Manager utilities for managing Node.js versions.
|
|
137
|
-
|
|
138
|
-
**Usage:**
|
|
139
|
-
```bash
|
|
140
|
-
hlpr nvm <command>
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
**Commands:**
|
|
144
|
-
- `install` - Install Node Version Manager (nvm)
|
|
145
|
-
- `lts` - Install and use Node.js LTS version
|
|
146
|
-
|
|
147
|
-
**See:** [`src/commands/nvm/README.md`](src/commands/nvm/README.md) for full documentation.
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
|
|
151
|
-
#### ssh
|
|
152
|
-
|
|
153
|
-
SSH configuration and setup utilities.
|
|
154
|
-
|
|
155
|
-
**Usage:**
|
|
156
|
-
```bash
|
|
157
|
-
hlpr ssh init-dir
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Initializes SSH directory with proper permissions:
|
|
161
|
-
- `~/.ssh/` directory with 700 permissions
|
|
162
|
-
- `~/.ssh/known_hosts` with 644 permissions
|
|
163
|
-
- `~/.ssh/config` with 644 permissions
|
|
164
|
-
|
|
165
|
-
Portable across Unix/Linux/macOS and Git Bash on Windows.
|
|
166
|
-
|
|
167
|
-
**See:** [`src/commands/ssh/README.md`](src/commands/ssh/README.md) for full documentation.
|
|
61
|
+
- **[git](src/commands/git/README.md)** - Git utilities (fodd, precommit, switch-clean)
|
|
62
|
+
- **[hello](src/commands/hello/README.md)** - Example greeting command with variable substitution
|
|
63
|
+
- **[nvm](src/commands/nvm/README.md)** - Node Version Manager utilities
|
|
64
|
+
- **[ssh](src/commands/ssh/README.md)** - SSH configuration and setup utilities
|
|
168
65
|
|
|
169
66
|
## How It Works
|
|
170
67
|
|
|
@@ -183,21 +80,40 @@ Portable across Unix/Linux/macOS and Git Bash on Windows.
|
|
|
183
80
|
## Directory Structure
|
|
184
81
|
|
|
185
82
|
```
|
|
186
|
-
|
|
187
|
-
├──
|
|
188
|
-
│ └──
|
|
189
|
-
│
|
|
190
|
-
|
|
83
|
+
src/
|
|
84
|
+
├── core/
|
|
85
|
+
│ └── command/
|
|
86
|
+
│ └── command.types.ts
|
|
87
|
+
├── commands/
|
|
88
|
+
│ ├── file/
|
|
89
|
+
│ │ └── rename/
|
|
90
|
+
│ │ ├── rename.ts
|
|
91
|
+
│ │ ├── rename.test.ts
|
|
92
|
+
│ │ └── README.md
|
|
93
|
+
│ ├── git/
|
|
94
|
+
│ │ ├── fodd.sh
|
|
95
|
+
│ │ ├── precommit.sh
|
|
96
|
+
│ │ ├── switch-clean.sh
|
|
97
|
+
│ │ └── README.md
|
|
98
|
+
│ ├── hello/
|
|
99
|
+
│ │ ├── world.sh
|
|
100
|
+
│ │ └── README.md
|
|
101
|
+
│ ├── nvm/
|
|
102
|
+
│ │ ├── install.sh
|
|
103
|
+
│ │ ├── lts.sh
|
|
104
|
+
│ │ └── README.md
|
|
105
|
+
│ ├── process/
|
|
106
|
+
│ │ ├── kill-port/
|
|
107
|
+
│ │ │ ├── kill-port.ts
|
|
108
|
+
│ │ │ └── README.md
|
|
109
|
+
│ │ └── list-port/
|
|
110
|
+
│ │ ├── list-port.ts
|
|
111
|
+
│ │ └── README.md
|
|
112
|
+
│ └── ssh/
|
|
113
|
+
│ ├── init-dir.sh
|
|
191
114
|
│ └── README.md
|
|
192
|
-
├──
|
|
193
|
-
|
|
194
|
-
├── hello/
|
|
195
|
-
│ └── world.sh
|
|
196
|
-
├── nvm/
|
|
197
|
-
│ ├── install.sh
|
|
198
|
-
│ └── lts.sh
|
|
199
|
-
└── ssh/
|
|
200
|
-
└── initdir.sh
|
|
115
|
+
├── index.ts
|
|
116
|
+
└── commands.ts
|
|
201
117
|
```
|
|
202
118
|
|
|
203
119
|
## Adding Your Own Scripts
|
|
@@ -246,6 +162,113 @@ The utility maps command arguments to script files:
|
|
|
246
162
|
|
|
247
163
|
- `hlpr file rename <args>` → runs `commands/file/rename/rename.ts`
|
|
248
164
|
|
|
165
|
+
## How Shell Scripts (.sh) Work
|
|
166
|
+
|
|
167
|
+
Shell scripts are bash scripts stored in `src/commands/<category>/` directories. They support interactive variable substitution and are cross-platform compatible.
|
|
168
|
+
|
|
169
|
+
### Variable Substitution
|
|
170
|
+
|
|
171
|
+
Shell scripts can use `{{variable}}` placeholders for user input:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
#!/bin/bash
|
|
175
|
+
# Example: src/commands/greet/hello.sh
|
|
176
|
+
echo "Hello {{name}}, welcome to {{place}}"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
When you run `hlpr greet hello`, the utility:
|
|
180
|
+
1. Reads the script
|
|
181
|
+
2. Finds all `{{variable}}` placeholders
|
|
182
|
+
3. Prompts you to enter values for each variable
|
|
183
|
+
4. Executes the script with variables replaced
|
|
184
|
+
|
|
185
|
+
**Example interaction:**
|
|
186
|
+
```
|
|
187
|
+
$ hlpr greet hello
|
|
188
|
+
Enter value for name: Alice
|
|
189
|
+
Enter value for place: Wonderland
|
|
190
|
+
Hello Alice, welcome to Wonderland
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Script Execution
|
|
194
|
+
|
|
195
|
+
- Each line of the script is executed sequentially
|
|
196
|
+
- If any line fails and `-f` flag is not set, execution stops
|
|
197
|
+
- Scripts have access to standard bash features (pipes, redirects, etc.)
|
|
198
|
+
|
|
199
|
+
### Windows Compatibility
|
|
200
|
+
|
|
201
|
+
`.sh` scripts run on Windows through **PowerShell** and the modern cross-platform command stack.
|
|
202
|
+
|
|
203
|
+
**How it works:**
|
|
204
|
+
|
|
205
|
+
1. User runs: `hlpr git fodd`
|
|
206
|
+
2. hlpr detects the platform is Windows
|
|
207
|
+
3. hlpr invokes **PowerShell** to execute the `.sh` script
|
|
208
|
+
4. PowerShell (v7+) and installed tools provide Unix-like command support
|
|
209
|
+
5. Commands like `grep`, `sed`, `ls`, etc. work through various implementations
|
|
210
|
+
|
|
211
|
+
**Why this works:**
|
|
212
|
+
|
|
213
|
+
Modern Windows systems have multiple sources for Unix-like commands:
|
|
214
|
+
|
|
215
|
+
- **PowerShell 7+** - Cross-platform, provides built-in Unix-like commands and aliases (ls, grep, select-string, etc.)
|
|
216
|
+
- **Git** - Installs Unix utilities as part of its toolchain
|
|
217
|
+
- **Node.js** - Provides command-line tools that work cross-platform
|
|
218
|
+
- **Package managers** - scoop, chocolatey, and winget provide Unix tools
|
|
219
|
+
- **WSL 2** - Windows Subsystem for Linux provides full Linux compatibility
|
|
220
|
+
|
|
221
|
+
Scripts work because:
|
|
222
|
+
- Modern tools use consistent command-line interfaces across platforms
|
|
223
|
+
- PowerShell Core has native cross-platform support
|
|
224
|
+
- Common utilities are available through multiple sources in system PATH
|
|
225
|
+
|
|
226
|
+
**Example:**
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
#!/bin/bash
|
|
230
|
+
# This script works on Windows, Linux, and macOS
|
|
231
|
+
echo "Running on: $(uname -s || echo 'Windows PowerShell')"
|
|
232
|
+
if [ -d "./src" ]; then
|
|
233
|
+
echo "Found src directory"
|
|
234
|
+
ls src | head -5
|
|
235
|
+
fi
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Requirements:**
|
|
239
|
+
|
|
240
|
+
- **Modern PowerShell** (v7+) is recommended for full compatibility
|
|
241
|
+
- **Windows 10+** with default PowerShell Core support
|
|
242
|
+
- **Git** installed (provides additional Unix utilities)
|
|
243
|
+
- **Node.js** for runtime support
|
|
244
|
+
|
|
245
|
+
No special configuration needed - hlpr automatically detects and uses PowerShell on Windows.
|
|
246
|
+
|
|
247
|
+
### Creating Shell Scripts
|
|
248
|
+
|
|
249
|
+
1. Create directory: `src/commands/<category>/`
|
|
250
|
+
2. Add `.sh` file: `<command>.sh`
|
|
251
|
+
3. Add shebang: `#!/bin/bash`
|
|
252
|
+
4. Use `{{variable}}` for user input
|
|
253
|
+
5. Build with `bun run build` (copies `.sh` files to `bin/commands/`)
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
#!/bin/bash
|
|
259
|
+
# src/commands/greet/hello.sh
|
|
260
|
+
NAME={{name}}
|
|
261
|
+
echo "Hello $NAME!"
|
|
262
|
+
echo "Today is $(date '+%A')"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Building and Distribution
|
|
266
|
+
|
|
267
|
+
Shell scripts are:
|
|
268
|
+
- **Copied** (not compiled) during build to `bin/commands/`
|
|
269
|
+
- **Distributed** as part of the published package
|
|
270
|
+
- **Platform-compatible** across Linux, macOS, and Windows (with Git Bash)
|
|
271
|
+
|
|
249
272
|
## Error Handling
|
|
250
273
|
|
|
251
274
|
By default, the utility stops execution if any command fails. Use the `-f` flag to continue execution despite failures:
|
|
@@ -263,10 +286,14 @@ hlpr -f ssh init dir
|
|
|
263
286
|
```bash
|
|
264
287
|
# Clone the repository
|
|
265
288
|
git clone https://github.com/tertiumnon/hlpr.git
|
|
289
|
+
cd hlpr
|
|
266
290
|
|
|
267
291
|
# Install dependencies
|
|
268
292
|
npm install
|
|
269
293
|
|
|
294
|
+
# Setup git hooks (runs tests before commit)
|
|
295
|
+
npm run setup:hooks
|
|
296
|
+
|
|
270
297
|
# Build the project
|
|
271
298
|
bun run build
|
|
272
299
|
|
|
@@ -274,6 +301,20 @@ bun run build
|
|
|
274
301
|
npm link
|
|
275
302
|
```
|
|
276
303
|
|
|
304
|
+
### Git Hooks
|
|
305
|
+
|
|
306
|
+
This project uses native git hooks (`.githooks/pre-commit`) to run tests before each commit. After cloning or pulling, ensure hooks are configured:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
npm run setup:hooks
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The pre-commit hook:
|
|
313
|
+
- Runs unit and integration tests
|
|
314
|
+
- Runs end-to-end tests
|
|
315
|
+
- Aborts commit if tests fail
|
|
316
|
+
- Can be skipped with `SKIP_E2E=1 git commit` (not recommended)
|
|
317
|
+
|
|
277
318
|
## License
|
|
278
319
|
|
|
279
320
|
MIT
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @bun
|
|
1
2
|
// src/commands/file/rename/rename.ts
|
|
2
3
|
import fs from "fs/promises";
|
|
3
4
|
import path from "path";
|
|
@@ -21,7 +22,7 @@ function transformBasename(basename, style) {
|
|
|
21
22
|
return basename;
|
|
22
23
|
switch (style) {
|
|
23
24
|
case "title_underscore":
|
|
24
|
-
return leadingDot + words
|
|
25
|
+
return leadingDot + titleCase(words).join("_") + ext;
|
|
25
26
|
case "snake":
|
|
26
27
|
return leadingDot + words.map((w) => w.toLowerCase()).join("_") + ext;
|
|
27
28
|
case "kebab":
|
|
@@ -31,7 +32,7 @@ function transformBasename(basename, style) {
|
|
|
31
32
|
case "pascal":
|
|
32
33
|
return leadingDot + words.map(cap).join("") + ext;
|
|
33
34
|
case "pascal_underscore":
|
|
34
|
-
return leadingDot + words
|
|
35
|
+
return leadingDot + titleCase(words).join("_") + ext;
|
|
35
36
|
case "upper":
|
|
36
37
|
return leadingDot + words.join("_").toUpperCase() + ext;
|
|
37
38
|
case "lower":
|
|
@@ -40,11 +41,48 @@ function transformBasename(basename, style) {
|
|
|
40
41
|
return leadingDot + core + ext;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
function isAllCaps(s) {
|
|
45
|
+
return s.length > 0 && s === s.toUpperCase() && s !== s.toLowerCase();
|
|
46
|
+
}
|
|
43
47
|
function cap(s) {
|
|
44
48
|
if (!s)
|
|
45
49
|
return s;
|
|
46
50
|
return s[0].toUpperCase() + s.slice(1).toLowerCase();
|
|
47
51
|
}
|
|
52
|
+
function titleCase(words) {
|
|
53
|
+
const smallWords = new Set([
|
|
54
|
+
"a",
|
|
55
|
+
"an",
|
|
56
|
+
"and",
|
|
57
|
+
"as",
|
|
58
|
+
"at",
|
|
59
|
+
"but",
|
|
60
|
+
"by",
|
|
61
|
+
"for",
|
|
62
|
+
"from",
|
|
63
|
+
"in",
|
|
64
|
+
"into",
|
|
65
|
+
"nor",
|
|
66
|
+
"of",
|
|
67
|
+
"on",
|
|
68
|
+
"or",
|
|
69
|
+
"the",
|
|
70
|
+
"to",
|
|
71
|
+
"with"
|
|
72
|
+
]);
|
|
73
|
+
return words.map((word, index) => {
|
|
74
|
+
if (isAllCaps(word)) {
|
|
75
|
+
return word;
|
|
76
|
+
}
|
|
77
|
+
if (index === 0) {
|
|
78
|
+
return cap(word);
|
|
79
|
+
}
|
|
80
|
+
if (smallWords.has(word.toLowerCase())) {
|
|
81
|
+
return word.toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
return cap(word);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
48
86
|
async function exists(p) {
|
|
49
87
|
try {
|
|
50
88
|
await fs.access(p);
|
|
@@ -229,7 +267,7 @@ if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, "/"))) {
|
|
|
229
267
|
console.log(`Renamed ${performed.length} items:`);
|
|
230
268
|
}
|
|
231
269
|
performed.forEach(({ from, to }) => {
|
|
232
|
-
console.log(` ${from}
|
|
270
|
+
console.log(` ${from} \u2192 ${to}`);
|
|
233
271
|
});
|
|
234
272
|
}).catch((err) => {
|
|
235
273
|
console.error("Error:", err);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/commands/process/kill-port/kill-port.ts
|
|
5
|
+
import { execSync, spawnSync } from "child_process";
|
|
6
|
+
import { platform } from "os";
|
|
7
|
+
function getProcessOnWindowsPort(port) {
|
|
8
|
+
try {
|
|
9
|
+
const output = execSync(`netstat -ano | findstr :${port}`, { encoding: "utf-8" });
|
|
10
|
+
const pids = [];
|
|
11
|
+
const lines = output.split(`
|
|
12
|
+
`);
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
const match = line.match(/\s+(\d+)\s*$/);
|
|
15
|
+
if (match) {
|
|
16
|
+
const pid = parseInt(match[1], 10);
|
|
17
|
+
if (pid > 0 && !pids.includes(pid)) {
|
|
18
|
+
pids.push(pid);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return pids;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function getProcessOnLinuxPort(port) {
|
|
28
|
+
try {
|
|
29
|
+
const output = execSync(`lsof -i :${port} -n -P`, { encoding: "utf-8" });
|
|
30
|
+
const pids = [];
|
|
31
|
+
const lines = output.split(`
|
|
32
|
+
`);
|
|
33
|
+
for (let i = 1;i < lines.length; i++) {
|
|
34
|
+
const parts = lines[i].trim().split(/\s+/);
|
|
35
|
+
if (parts.length > 1) {
|
|
36
|
+
const pid = parseInt(parts[1], 10);
|
|
37
|
+
if (!isNaN(pid) && !pids.includes(pid)) {
|
|
38
|
+
pids.push(pid);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return pids;
|
|
43
|
+
} catch {
|
|
44
|
+
try {
|
|
45
|
+
const output = execSync(`netstat -tulnp 2>/dev/null | grep :${port}`, { encoding: "utf-8" });
|
|
46
|
+
const pids = [];
|
|
47
|
+
const lines = output.split(`
|
|
48
|
+
`);
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const match = line.match(/(\d+)\//);
|
|
51
|
+
if (match) {
|
|
52
|
+
const pid = parseInt(match[1], 10);
|
|
53
|
+
if (!pids.includes(pid)) {
|
|
54
|
+
pids.push(pid);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return pids;
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function killProcessOnWindows(pid, force, verbose) {
|
|
65
|
+
try {
|
|
66
|
+
const args = force ? ["/PID", pid.toString(), "/F"] : ["/PID", pid.toString()];
|
|
67
|
+
const result = spawnSync("taskkill", args, { encoding: "utf-8" });
|
|
68
|
+
if (result.status === 0) {
|
|
69
|
+
if (verbose)
|
|
70
|
+
console.log(`\u2713 Killed process ${pid}`);
|
|
71
|
+
return true;
|
|
72
|
+
} else {
|
|
73
|
+
if (verbose)
|
|
74
|
+
console.error(`\u2717 Failed to kill process ${pid}: ${result.stderr}`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (verbose)
|
|
79
|
+
console.error(`\u2717 Error killing process ${pid}: ${error}`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function killProcessOnLinux(pid, force, verbose) {
|
|
84
|
+
try {
|
|
85
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
86
|
+
const result = spawnSync("kill", [`-${signal}`, pid.toString()], { encoding: "utf-8" });
|
|
87
|
+
if (result.status === 0) {
|
|
88
|
+
if (verbose)
|
|
89
|
+
console.log(`\u2713 Killed process ${pid} with ${signal}`);
|
|
90
|
+
return true;
|
|
91
|
+
} else {
|
|
92
|
+
if (verbose)
|
|
93
|
+
console.error(`\u2717 Failed to kill process ${pid}: ${result.stderr}`);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (verbose)
|
|
98
|
+
console.error(`\u2717 Error killing process ${pid}: ${error}`);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function killPort(port, options = {}) {
|
|
103
|
+
const { force = false, verbose = false } = options;
|
|
104
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
105
|
+
throw new Error(`Invalid port number: ${port}. Port must be between 1 and 65535.`);
|
|
106
|
+
}
|
|
107
|
+
const os = platform();
|
|
108
|
+
let pids = [];
|
|
109
|
+
if (verbose)
|
|
110
|
+
console.log(`Searching for processes on port ${port} (${os})...`);
|
|
111
|
+
if (os === "win32") {
|
|
112
|
+
pids = getProcessOnWindowsPort(port);
|
|
113
|
+
} else {
|
|
114
|
+
pids = getProcessOnLinuxPort(port);
|
|
115
|
+
}
|
|
116
|
+
if (pids.length === 0) {
|
|
117
|
+
if (verbose)
|
|
118
|
+
console.log(`No processes found on port ${port}`);
|
|
119
|
+
return { killed: [], failed: [] };
|
|
120
|
+
}
|
|
121
|
+
if (verbose)
|
|
122
|
+
console.log(`Found ${pids.length} process(es) on port ${port}: ${pids.join(", ")}`);
|
|
123
|
+
const killed = [];
|
|
124
|
+
const failed = [];
|
|
125
|
+
for (const pid of pids) {
|
|
126
|
+
let success;
|
|
127
|
+
if (os === "win32") {
|
|
128
|
+
success = killProcessOnWindows(pid, force, verbose);
|
|
129
|
+
} else {
|
|
130
|
+
success = killProcessOnLinux(pid, force, verbose);
|
|
131
|
+
}
|
|
132
|
+
if (success) {
|
|
133
|
+
killed.push(pid);
|
|
134
|
+
} else {
|
|
135
|
+
failed.push(pid);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { killed, failed };
|
|
139
|
+
}
|
|
140
|
+
if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, "/"))) {
|
|
141
|
+
const args = process.argv.slice(2);
|
|
142
|
+
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
143
|
+
console.log("Usage: kill-port <port> [options]");
|
|
144
|
+
console.log("Kill process(es) running on a specified port (Windows & Linux support)");
|
|
145
|
+
console.log();
|
|
146
|
+
console.log("Arguments:");
|
|
147
|
+
console.log(" port Port number (1-65535)");
|
|
148
|
+
console.log();
|
|
149
|
+
console.log("Options:");
|
|
150
|
+
console.log(" -f, --force Force kill (SIGKILL on Linux, /F on Windows)");
|
|
151
|
+
console.log(" -v, --verbose Show verbose output");
|
|
152
|
+
console.log(" -h, --help Show this help message");
|
|
153
|
+
console.log();
|
|
154
|
+
console.log("Examples:");
|
|
155
|
+
console.log(" hlpr process kill-port 3000");
|
|
156
|
+
console.log(" hlpr process kill-port 8080 --force");
|
|
157
|
+
console.log(" hlpr process kill-port 5432 -v");
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
const port = parseInt(args[0], 10);
|
|
161
|
+
const force = args.includes("-f") || args.includes("--force");
|
|
162
|
+
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
163
|
+
killPort(port, { force, verbose }).then((result) => {
|
|
164
|
+
if (result.killed.length > 0) {
|
|
165
|
+
console.log(`Successfully killed ${result.killed.length} process(es) on port ${port}`);
|
|
166
|
+
}
|
|
167
|
+
if (result.failed.length > 0) {
|
|
168
|
+
console.error(`Failed to kill ${result.failed.length} process(es): ${result.failed.join(", ")}`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
if (result.killed.length === 0 && result.failed.length === 0) {
|
|
172
|
+
console.log(`No processes found on port ${port}`);
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}).catch((error) => {
|
|
176
|
+
console.error("Error:", error.message);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
var kill_port_default = { killPort };
|
|
181
|
+
export {
|
|
182
|
+
killPort,
|
|
183
|
+
kill_port_default as default
|
|
184
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/commands/process/list-port/list-port.ts
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { platform } from "os";
|
|
7
|
+
function getProcessesOnWindowsPort(port) {
|
|
8
|
+
try {
|
|
9
|
+
const output = execSync(`netstat -ano | findstr :${port}`, { encoding: "utf-8" });
|
|
10
|
+
const processes = [];
|
|
11
|
+
const lines = output.split(`
|
|
12
|
+
`);
|
|
13
|
+
const pids = new Set;
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
const match = line.match(/\s+(\d+)\s*$/);
|
|
16
|
+
if (match) {
|
|
17
|
+
const pid = parseInt(match[1], 10);
|
|
18
|
+
if (pid > 0 && !pids.has(pid)) {
|
|
19
|
+
pids.add(pid);
|
|
20
|
+
try {
|
|
21
|
+
const taskOutput = execSync(`tasklist /FI "PID eq ${pid}" /NH /FO CSV`, { encoding: "utf-8" }).trim();
|
|
22
|
+
const parts = taskOutput.split('","');
|
|
23
|
+
const command = parts[0].replace(/^"|"$/g, "");
|
|
24
|
+
processes.push({
|
|
25
|
+
pid,
|
|
26
|
+
state: "LISTENING",
|
|
27
|
+
command
|
|
28
|
+
});
|
|
29
|
+
} catch {
|
|
30
|
+
processes.push({
|
|
31
|
+
pid,
|
|
32
|
+
state: "LISTENING"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return processes;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function getProcessesOnLinuxPort(port) {
|
|
44
|
+
try {
|
|
45
|
+
const output = execSync(`lsof -i :${port} -n -P`, { encoding: "utf-8" });
|
|
46
|
+
const processes = [];
|
|
47
|
+
const lines = output.split(`
|
|
48
|
+
`);
|
|
49
|
+
const pids = new Set;
|
|
50
|
+
for (let i = 1;i < lines.length; i++) {
|
|
51
|
+
const parts = lines[i].trim().split(/\s+/);
|
|
52
|
+
if (parts.length > 2) {
|
|
53
|
+
const command = parts[0];
|
|
54
|
+
const pid = parseInt(parts[1], 10);
|
|
55
|
+
const state = parts[7] || "LISTENING";
|
|
56
|
+
if (!isNaN(pid) && pid > 0 && !pids.has(pid)) {
|
|
57
|
+
pids.add(pid);
|
|
58
|
+
processes.push({
|
|
59
|
+
pid,
|
|
60
|
+
state,
|
|
61
|
+
command
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return processes;
|
|
67
|
+
} catch {
|
|
68
|
+
try {
|
|
69
|
+
const output = execSync(`netstat -tulnp 2>/dev/null | grep :${port}`, { encoding: "utf-8" });
|
|
70
|
+
const processes = [];
|
|
71
|
+
const lines = output.split(`
|
|
72
|
+
`);
|
|
73
|
+
const pids = new Set;
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
const match = line.match(/(\d+)\/(.+)/);
|
|
76
|
+
if (match) {
|
|
77
|
+
const pid = parseInt(match[1], 10);
|
|
78
|
+
const command = match[2];
|
|
79
|
+
if (!pids.has(pid)) {
|
|
80
|
+
pids.add(pid);
|
|
81
|
+
processes.push({
|
|
82
|
+
pid,
|
|
83
|
+
state: "LISTENING",
|
|
84
|
+
command
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return processes;
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function formatTable(processes) {
|
|
96
|
+
if (processes.length === 0) {
|
|
97
|
+
return "No processes found";
|
|
98
|
+
}
|
|
99
|
+
const pidWidth = 10;
|
|
100
|
+
const stateWidth = 12;
|
|
101
|
+
const commandWidth = 50;
|
|
102
|
+
const header = `${"PID".padEnd(pidWidth)} ${"STATE".padEnd(stateWidth)} COMMAND`;
|
|
103
|
+
const separator = "\u2500".repeat(pidWidth + stateWidth + commandWidth + 2);
|
|
104
|
+
const rows = processes.map((p) => {
|
|
105
|
+
const command = p.command || "unknown";
|
|
106
|
+
const truncated = command.length > commandWidth ? command.substring(0, commandWidth - 3) + "..." : command;
|
|
107
|
+
return `${p.pid.toString().padEnd(pidWidth)} ${p.state.padEnd(stateWidth)} ${truncated}`;
|
|
108
|
+
});
|
|
109
|
+
return `${header}
|
|
110
|
+
${separator}
|
|
111
|
+
${rows.join(`
|
|
112
|
+
`)}`;
|
|
113
|
+
}
|
|
114
|
+
async function listPort(port, options = {}) {
|
|
115
|
+
const { verbose = false } = options;
|
|
116
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
117
|
+
throw new Error(`Invalid port number: ${port}. Port must be between 1 and 65535.`);
|
|
118
|
+
}
|
|
119
|
+
const os = platform();
|
|
120
|
+
if (verbose)
|
|
121
|
+
console.log(`Searching for processes on port ${port} (${os})...
|
|
122
|
+
`);
|
|
123
|
+
let processes = [];
|
|
124
|
+
if (os === "win32") {
|
|
125
|
+
processes = getProcessesOnWindowsPort(port);
|
|
126
|
+
} else {
|
|
127
|
+
processes = getProcessesOnLinuxPort(port);
|
|
128
|
+
}
|
|
129
|
+
return processes;
|
|
130
|
+
}
|
|
131
|
+
if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, "/"))) {
|
|
132
|
+
const args = process.argv.slice(2);
|
|
133
|
+
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
134
|
+
console.log("Usage: list-port <port> [options]");
|
|
135
|
+
console.log("List process(es) running on a specified port (Windows & Linux support)");
|
|
136
|
+
console.log();
|
|
137
|
+
console.log("Arguments:");
|
|
138
|
+
console.log(" port Port number (1-65535)");
|
|
139
|
+
console.log();
|
|
140
|
+
console.log("Options:");
|
|
141
|
+
console.log(" -v, --verbose Show verbose output");
|
|
142
|
+
console.log(" -h, --help Show this help message");
|
|
143
|
+
console.log();
|
|
144
|
+
console.log("Examples:");
|
|
145
|
+
console.log(" hlpr process list-port 3000");
|
|
146
|
+
console.log(" hlpr process list-port 8080 --verbose");
|
|
147
|
+
console.log(" hlpr process list-port 5432 -v");
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
const port = parseInt(args[0], 10);
|
|
151
|
+
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
152
|
+
listPort(port, { verbose }).then((processes) => {
|
|
153
|
+
console.log(formatTable(processes));
|
|
154
|
+
process.exit(processes.length === 0 ? 1 : 0);
|
|
155
|
+
}).catch((error) => {
|
|
156
|
+
console.error("Error:", error.message);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
var list_port_default = { listPort };
|
|
161
|
+
export {
|
|
162
|
+
listPort,
|
|
163
|
+
list_port_default as default
|
|
164
|
+
};
|
package/bin/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/hlpr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Windows and *Nix utility for typical programming activity",
|
|
5
5
|
"author": "Vitalii Balabanov",
|
|
6
6
|
"email": "tertiumnon@gmail.com",
|
|
@@ -8,22 +8,21 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build:main": "bun build ./src/index.ts --outfile ./bin/index.js --target node",
|
|
11
|
-
"build:commands": "bun build-commands.
|
|
11
|
+
"build:commands": "bun scripts/build-commands/build-commands.ts",
|
|
12
12
|
"build": "bun run build:main && bun run build:commands",
|
|
13
13
|
"test": "bun test",
|
|
14
|
-
"test:e2e": "bun scripts/test-rename-
|
|
14
|
+
"test:e2e": "bun scripts/test-rename/test-rename.ts",
|
|
15
15
|
"prepublishOnly": "bun run build",
|
|
16
|
-
"release:minor": "bun node_modules/@tertium/js/scripts/release.js minor",
|
|
17
16
|
"release:patch": "bun node_modules/@tertium/js/scripts/release.js patch",
|
|
17
|
+
"release:minor": "bun node_modules/@tertium/js/scripts/release.js minor",
|
|
18
18
|
"release:major": "bun node_modules/@tertium/js/scripts/release.js major",
|
|
19
|
-
"
|
|
19
|
+
"setup:hooks": "git config core.hooksPath .githooks"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@tertium/js": "^1.4.7"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^22.13.10",
|
|
26
|
-
"husky": "^9.1.7",
|
|
27
26
|
"typescript": "^5.8.3"
|
|
28
27
|
},
|
|
29
28
|
"bin": {
|