@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 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
- const core = leadingDot ? basename.slice(1) : basename;
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 basename;
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 {
@@ -1 +1,4 @@
1
+ #!/bin/bash
2
+ # @description Fetch and update develop branch from origin
3
+
1
4
  git fetch origin develop:develop
@@ -1,3 +1,6 @@
1
+ #!/bin/bash
2
+ # @description Run build and stage bin/ directory before commit
3
+
1
4
  echo "Running build before commit..."
2
5
  bun run build
3
6
  git add bin/
@@ -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
@@ -1 +1,4 @@
1
+ #!/bin/bash
2
+ # @description Print a personalized hello world message
3
+
1
4
  echo "Hello World, {{name}}"
@@ -1 +1,4 @@
1
+ #!/bin/bash
2
+ # @description Install Node Version Manager (nvm)
3
+
1
4
  curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
@@ -1,2 +1,5 @@
1
+ #!/bin/bash
2
+ # @description Install and use Node.js LTS version
3
+
1
4
  nvm install --lts
2
5
  nvm use --lts
@@ -1,6 +1,16 @@
1
- mkdir ~/.ssh
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 "powershell";
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
- const tsArgs = restArgs.length > 0 && restArgs[0] && fs.existsSync(path.join(scriptDir, "commands", category, restArgs[0])) ? process.argv.slice(4) : process.argv.slice(3);
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
- if (!success2 && !forceFlag) {
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.4",
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",