@tertium/hlpr 0.3.4 → 0.4.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 +10 -0
- package/bin/commands/file/rename/rename.js +104 -14
- package/bin/commands/git/fodd.sh +3 -0
- package/bin/commands/git/precommit.sh +3 -0
- package/bin/commands/git/switch-clean.sh +68 -0
- package/bin/commands/hello/world.sh +3 -0
- package/bin/commands/nvm/install.sh +3 -0
- package/bin/commands/nvm/lts.sh +3 -0
- package/bin/commands/ssh/init-dir.sh +11 -1
- package/bin/index.js +12 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A CLI utility for running shell scripts with variable substitution and TypeScript-based commands.
|
|
4
4
|
|
|
5
|
+
## Platform Support
|
|
6
|
+
|
|
7
|
+
hlpr works on all major operating systems:
|
|
8
|
+
|
|
9
|
+
- ✅ **Linux** - Fully supported
|
|
10
|
+
- ✅ **macOS** - Fully supported
|
|
11
|
+
- ✅ **Windows** - Requires [Git for Windows](https://git-scm.com/download/win) (includes Git Bash)
|
|
12
|
+
|
|
13
|
+
**Note for Windows users:** Shell script commands (`.sh` files) require Bash, which is included with Git for Windows. TypeScript commands work on all platforms.
|
|
14
|
+
|
|
5
15
|
## Installation
|
|
6
16
|
|
|
7
17
|
```bash
|
|
@@ -8,30 +8,36 @@ function splitWords(s) {
|
|
|
8
8
|
function transformBasename(basename, style) {
|
|
9
9
|
if (!basename)
|
|
10
10
|
return basename;
|
|
11
|
-
if (basename.includes(".") && !basename.startsWith("."))
|
|
12
|
-
return basename;
|
|
13
11
|
const leadingDot = basename.startsWith(".") ? "." : "";
|
|
14
|
-
|
|
12
|
+
let core = leadingDot ? basename.slice(1) : basename;
|
|
13
|
+
let ext = "";
|
|
14
|
+
const firstDot = core.indexOf(".");
|
|
15
|
+
if (firstDot !== -1) {
|
|
16
|
+
ext = core.slice(firstDot);
|
|
17
|
+
core = core.slice(0, firstDot);
|
|
18
|
+
}
|
|
15
19
|
const words = splitWords(core);
|
|
16
20
|
if (words.length === 0)
|
|
17
21
|
return basename;
|
|
18
22
|
switch (style) {
|
|
19
23
|
case "title_underscore":
|
|
20
|
-
return leadingDot + words.map(cap).join("_");
|
|
24
|
+
return leadingDot + words.map(cap).join("_") + ext;
|
|
21
25
|
case "snake":
|
|
22
|
-
return leadingDot + words.map((w) => w.toLowerCase()).join("_");
|
|
26
|
+
return leadingDot + words.map((w) => w.toLowerCase()).join("_") + ext;
|
|
23
27
|
case "kebab":
|
|
24
|
-
return leadingDot + words.map((w) => w.toLowerCase()).join("-");
|
|
28
|
+
return leadingDot + words.map((w) => w.toLowerCase()).join("-") + ext;
|
|
25
29
|
case "camel":
|
|
26
|
-
return leadingDot + words.map((w, i) => i === 0 ? w.toLowerCase() : cap(w)).join("");
|
|
30
|
+
return leadingDot + words.map((w, i) => i === 0 ? w.toLowerCase() : cap(w)).join("") + ext;
|
|
27
31
|
case "pascal":
|
|
28
|
-
return leadingDot + words.map(cap).join("");
|
|
32
|
+
return leadingDot + words.map(cap).join("") + ext;
|
|
33
|
+
case "pascal_underscore":
|
|
34
|
+
return leadingDot + words.map(cap).join("_") + ext;
|
|
29
35
|
case "upper":
|
|
30
|
-
return leadingDot + words.join("_").toUpperCase();
|
|
36
|
+
return leadingDot + words.join("_").toUpperCase() + ext;
|
|
31
37
|
case "lower":
|
|
32
|
-
return leadingDot + words.join("_").toLowerCase();
|
|
38
|
+
return leadingDot + words.join("_").toLowerCase() + ext;
|
|
33
39
|
default:
|
|
34
|
-
return
|
|
40
|
+
return leadingDot + core + ext;
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
function cap(s) {
|
|
@@ -81,8 +87,61 @@ async function safeRename(oldPath, newPath) {
|
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
}
|
|
90
|
+
async function updateFileReferences(filePath, renames) {
|
|
91
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
92
|
+
let updated = content;
|
|
93
|
+
for (const { from, to } of renames) {
|
|
94
|
+
const fromBasename = path.basename(from);
|
|
95
|
+
const toBasename = path.basename(to);
|
|
96
|
+
if (fromBasename === toBasename)
|
|
97
|
+
continue;
|
|
98
|
+
const fromWithoutExt = fromBasename.replace(/\.[^.]+$/, "");
|
|
99
|
+
const toWithoutExt = toBasename.replace(/\.[^.]+$/, "");
|
|
100
|
+
const importPattern1 = new RegExp(`(['"\`])([./]*(?:[^'"\`]*/)?)${escapeRegex(fromWithoutExt)}\\1`, "g");
|
|
101
|
+
updated = updated.replace(importPattern1, (_match, quote, pathPart) => {
|
|
102
|
+
return `${quote}${pathPart}${toWithoutExt}${quote}`;
|
|
103
|
+
});
|
|
104
|
+
const importPattern2 = new RegExp(`(['"\`])([./]*(?:[^'"\`]*/)?)${escapeRegex(fromBasename)}\\1`, "g");
|
|
105
|
+
updated = updated.replace(importPattern2, (_match, quote, pathPart) => {
|
|
106
|
+
return `${quote}${pathPart}${toBasename}${quote}`;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (updated !== content) {
|
|
110
|
+
await fs.writeFile(filePath, updated, "utf-8");
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
function escapeRegex(str) {
|
|
116
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
117
|
+
}
|
|
118
|
+
async function isTextFile(filePath) {
|
|
119
|
+
try {
|
|
120
|
+
const fd = await fs.open(filePath, "r");
|
|
121
|
+
const buffer = Buffer.alloc(512);
|
|
122
|
+
const { bytesRead } = await fd.read(buffer, 0, 512, 0);
|
|
123
|
+
await fd.close();
|
|
124
|
+
if (bytesRead === 0)
|
|
125
|
+
return true;
|
|
126
|
+
for (let i = 0;i < bytesRead; i++) {
|
|
127
|
+
if (buffer[i] === 0)
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
let printableCount = 0;
|
|
131
|
+
for (let i = 0;i < bytesRead; i++) {
|
|
132
|
+
const byte = buffer[i];
|
|
133
|
+
if (byte >= 32 && byte <= 126 || byte === 9 || byte === 10 || byte === 13) {
|
|
134
|
+
printableCount++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return printableCount / bytesRead > 0.85;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
84
142
|
async function renameRecursive(root, style = "title_underscore", options = {}) {
|
|
85
143
|
const performed = [];
|
|
144
|
+
const updateContentEnabled = options.updateContent ?? true;
|
|
86
145
|
async function walk(current) {
|
|
87
146
|
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
88
147
|
for (const e of entries) {
|
|
@@ -120,19 +179,50 @@ async function renameRecursive(root, style = "title_underscore", options = {}) {
|
|
|
120
179
|
}
|
|
121
180
|
}
|
|
122
181
|
await walk(root);
|
|
182
|
+
if (!options.dryRun && updateContentEnabled && performed.length > 0) {
|
|
183
|
+
const filesToUpdate = [];
|
|
184
|
+
async function collectFiles(dir) {
|
|
185
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
186
|
+
for (const e of entries) {
|
|
187
|
+
const full = path.join(dir, e.name);
|
|
188
|
+
if (e.isFile()) {
|
|
189
|
+
filesToUpdate.push(full);
|
|
190
|
+
} else if (e.isDirectory()) {
|
|
191
|
+
await collectFiles(full);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
await collectFiles(root);
|
|
196
|
+
for (const file of filesToUpdate) {
|
|
197
|
+
try {
|
|
198
|
+
if (await isTextFile(file)) {
|
|
199
|
+
await updateFileReferences(file, performed);
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
123
204
|
return performed;
|
|
124
205
|
}
|
|
125
206
|
if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, "/"))) {
|
|
126
207
|
const args = process.argv.slice(2);
|
|
127
208
|
const rootArg = args[0];
|
|
128
209
|
const styleArg = args[1];
|
|
210
|
+
if (args.includes("--help") || args.includes("-h") || args.includes("/help") || args.includes("/h") || args.includes("/?")) {
|
|
211
|
+
console.log("Usage: rename <root> <style> [--dry|-n] [--no-update-content]");
|
|
212
|
+
console.log("Styles: title_underscore, pascal_underscore, snake, kebab, camel, pascal, upper, lower");
|
|
213
|
+
console.log("Options:");
|
|
214
|
+
console.log(" --dry, -n Preview changes without applying them");
|
|
215
|
+
console.log(" --no-update-content Skip updating import/require statements in files");
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
129
218
|
const dryRun = args.includes("--dry") || args.includes("-n");
|
|
219
|
+
const updateContent = !args.includes("--no-update-content");
|
|
130
220
|
if (!rootArg || !styleArg) {
|
|
131
|
-
console.error("Usage: rename <root> <style> [--dry|-n]");
|
|
132
|
-
console.error("Styles: title_underscore, snake, kebab, camel, pascal, upper, lower");
|
|
221
|
+
console.error("Usage: rename <root> <style> [--dry|-n] [--no-update-content]");
|
|
222
|
+
console.error("Styles: title_underscore, pascal_underscore, snake, kebab, camel, pascal, upper, lower");
|
|
133
223
|
process.exit(1);
|
|
134
224
|
}
|
|
135
|
-
renameRecursive(rootArg, styleArg, { dryRun }).then((performed) => {
|
|
225
|
+
renameRecursive(rootArg, styleArg, { dryRun, updateContent }).then((performed) => {
|
|
136
226
|
if (dryRun) {
|
|
137
227
|
console.log(`Dry run - would rename ${performed.length} items:`);
|
|
138
228
|
} else {
|
package/bin/commands/git/fodd.sh
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# @description Safely switch to a branch and delete the previous one (local & remote)
|
|
3
|
+
|
|
4
|
+
# Git switch and clean: Safely switch to a branch and delete the old one
|
|
5
|
+
# Usage: switch-clean <target-branch>
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
# Get target branch from argument
|
|
10
|
+
TARGET_BRANCH="$1"
|
|
11
|
+
|
|
12
|
+
if [ -z "$TARGET_BRANCH" ]; then
|
|
13
|
+
echo "Error: Target branch is required"
|
|
14
|
+
echo "Usage: switch-clean <target-branch>"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Get current branch
|
|
19
|
+
CURRENT_BRANCH=$(git branch --show-current)
|
|
20
|
+
|
|
21
|
+
if [ -z "$CURRENT_BRANCH" ]; then
|
|
22
|
+
echo "Error: Not on a branch (detached HEAD)"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Check if already on target branch
|
|
27
|
+
if [ "$CURRENT_BRANCH" = "$TARGET_BRANCH" ]; then
|
|
28
|
+
echo "Error: Already on branch '$TARGET_BRANCH'"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Check for uncommitted changes
|
|
33
|
+
if ! git diff-index --quiet HEAD --; then
|
|
34
|
+
echo "Error: You have uncommitted changes. Please commit or stash them first."
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Check if current branch has unpushed commits
|
|
39
|
+
UNPUSHED=$(git log @{u}.. --oneline 2>/dev/null || echo "")
|
|
40
|
+
if [ -n "$UNPUSHED" ]; then
|
|
41
|
+
echo "Error: Current branch '$CURRENT_BRANCH' has unpushed commits:"
|
|
42
|
+
echo "$UNPUSHED"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Fetch target branch from origin
|
|
47
|
+
echo "Fetching $TARGET_BRANCH from origin..."
|
|
48
|
+
git fetch origin "$TARGET_BRANCH:$TARGET_BRANCH" 2>/dev/null || {
|
|
49
|
+
echo "Error: Failed to fetch branch '$TARGET_BRANCH' from origin"
|
|
50
|
+
exit 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Checkout to target branch
|
|
54
|
+
echo "Switching to $TARGET_BRANCH..."
|
|
55
|
+
git checkout "$TARGET_BRANCH"
|
|
56
|
+
|
|
57
|
+
# Delete local branch
|
|
58
|
+
echo "Deleting local branch $CURRENT_BRANCH..."
|
|
59
|
+
git branch -d "$CURRENT_BRANCH"
|
|
60
|
+
|
|
61
|
+
# Delete remote branch if it exists
|
|
62
|
+
if git ls-remote --exit-code --heads origin "$CURRENT_BRANCH" > /dev/null 2>&1; then
|
|
63
|
+
echo "Deleting remote branch $CURRENT_BRANCH..."
|
|
64
|
+
git push origin --delete "$CURRENT_BRANCH"
|
|
65
|
+
echo "✓ Switched to '$TARGET_BRANCH' and deleted '$CURRENT_BRANCH' (local & remote)"
|
|
66
|
+
else
|
|
67
|
+
echo "✓ Switched to '$TARGET_BRANCH' and deleted '$CURRENT_BRANCH' (local only)"
|
|
68
|
+
fi
|
package/bin/commands/nvm/lts.sh
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# @description Initialize SSH directory with proper permissions
|
|
3
|
+
|
|
4
|
+
# Create .ssh directory if it doesn't exist
|
|
5
|
+
mkdir -p ~/.ssh
|
|
6
|
+
|
|
7
|
+
# Create files if they don't exist
|
|
2
8
|
touch ~/.ssh/known_hosts
|
|
3
9
|
touch ~/.ssh/config
|
|
10
|
+
|
|
11
|
+
# Set permissions (works on Unix/Linux/macOS and Git Bash on Windows)
|
|
4
12
|
chmod 700 ~/.ssh
|
|
5
13
|
chmod 644 ~/.ssh/known_hosts
|
|
6
14
|
chmod 644 ~/.ssh/config
|
|
15
|
+
|
|
16
|
+
echo "✓ SSH directory initialized at ~/.ssh"
|
package/bin/index.js
CHANGED
|
@@ -23,7 +23,7 @@ async function getVersion() {
|
|
|
23
23
|
function detectShell() {
|
|
24
24
|
const isWindows = os.platform() === "win32";
|
|
25
25
|
if (isWindows) {
|
|
26
|
-
return "
|
|
26
|
+
return "bash";
|
|
27
27
|
}
|
|
28
28
|
return "bash";
|
|
29
29
|
}
|
|
@@ -262,11 +262,20 @@ async function main() {
|
|
|
262
262
|
process.exit(1);
|
|
263
263
|
}
|
|
264
264
|
if (isTypeScriptCommand) {
|
|
265
|
-
|
|
265
|
+
let tsArgs;
|
|
266
|
+
if (restArgs.length > 0 && restArgs[0] && scriptPath) {
|
|
267
|
+
const subcategory = restArgs[0];
|
|
268
|
+
const isNested = path.basename(scriptPath) === `${subcategory}.js` && path.basename(path.dirname(scriptPath)) === subcategory;
|
|
269
|
+
tsArgs = isNested ? process.argv.slice(4) : process.argv.slice(3);
|
|
270
|
+
} else {
|
|
271
|
+
tsArgs = process.argv.slice(3);
|
|
272
|
+
}
|
|
266
273
|
const finalCommand = `node "${scriptPath}" ${tsArgs.join(" ")}`;
|
|
267
274
|
console.log(`Executing command: ${finalCommand}`);
|
|
268
275
|
const success2 = await executeCommand(finalCommand, {});
|
|
269
|
-
|
|
276
|
+
const helpFlags = ["-h", "--help", "help", "/h", "/help", "/?"];
|
|
277
|
+
const isHelpInvocation = tsArgs.some((arg) => helpFlags.includes(arg));
|
|
278
|
+
if (!success2 && !forceFlag && !isHelpInvocation) {
|
|
270
279
|
console.error("Command failed, stopping execution.");
|
|
271
280
|
process.exit(1);
|
|
272
281
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/hlpr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Windows and *Nix utility for typical programming activity",
|
|
5
5
|
"author": "Vitalii Balabanov",
|
|
6
6
|
"email": "tertiumnon@gmail.com",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"build:commands": "node build-commands.js",
|
|
12
12
|
"build": "bun run build:main && bun run build:commands",
|
|
13
13
|
"test": "bun test",
|
|
14
|
+
"test:e2e": "node scripts/test-rename-e2e.cjs",
|
|
14
15
|
"prepublishOnly": "npm run build",
|
|
15
16
|
"release:minor": "node node_modules/@tertium/js/scripts/release.js minor",
|
|
16
17
|
"release:patch": "node node_modules/@tertium/js/scripts/release.js patch",
|