@tertium/hlpr 0.6.3 → 0.7.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 +2 -0
- package/bin/commands/file/mv/README.md +96 -0
- package/bin/commands/file/mv/mv.js +330 -0
- package/bin/commands/file/mv/mv.types.js +1 -0
- package/bin/commands/file/rename/rename.js +2 -2
- package/bin/commands/file/rename/rename.types.js +1 -0
- package/bin/commands/help/help.types.js +1 -0
- package/bin/commands/process/kill-port/kill-port.js +2 -2
- package/bin/commands/process/list-port/list-port.js +2 -2
- package/bin/commands/process/list-port/list-port.types.js +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ hlpr ssh init dir
|
|
|
38
38
|
|
|
39
39
|
# TypeScript commands (nested structure)
|
|
40
40
|
hlpr file rename <directory> <style> [--dry|-n]
|
|
41
|
+
hlpr file mv <oldPath> <newPath> [--dry|-n]
|
|
41
42
|
|
|
42
43
|
# Continue execution even if commands fail
|
|
43
44
|
hlpr -f ssh init dir
|
|
@@ -50,6 +51,7 @@ See individual command documentation for detailed usage, options, and examples.
|
|
|
50
51
|
### TypeScript Commands
|
|
51
52
|
|
|
52
53
|
- **[file rename](src/commands/file/rename/README.md)** - Recursively rename files/folders with various case styles
|
|
54
|
+
- **[file mv](src/commands/file/mv/README.md)** - Rename/move a single file and update all references to it (markdown links, wikilinks, imports)
|
|
53
55
|
- **[help](src/commands/help/README.md)** - Display help information about hlpr commands
|
|
54
56
|
- **[process list-port](src/commands/process/list-port/README.md)** - List processes running on a port
|
|
55
57
|
- **[process kill-port](src/commands/process/kill-port/README.md)** - Kill processes on a port
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# File Mv Command
|
|
2
|
+
|
|
3
|
+
Rename or move a single file and automatically update every reference to it — markdown links,
|
|
4
|
+
wikilinks, and quoted import/require/href/src paths — in every text file under a directory, recursively.
|
|
5
|
+
|
|
6
|
+
No extension filtering is applied when scanning: every text file under the root is checked, and
|
|
7
|
+
binary files are skipped automatically.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
hlpr file mv <oldPath> <newPath> [--root <dir>] [--dry|-n] [--force] [--no-update-content]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Arguments
|
|
16
|
+
|
|
17
|
+
- `<oldPath>` - Path to the existing file
|
|
18
|
+
- `<newPath>` - New path or filename. If it has no directory separators, the file is renamed
|
|
19
|
+
in place (kept in the same directory as `<oldPath>`); otherwise it's treated as a full path
|
|
20
|
+
and the file is moved there (missing directories are created).
|
|
21
|
+
- `--root <dir>` - Directory to scan for references (default: current directory)
|
|
22
|
+
- `--dry` or `-n` - Preview changes without applying them
|
|
23
|
+
- `--force` - Overwrite the destination file if it already exists
|
|
24
|
+
- `--no-update-content` - Skip updating references in other files (only rename/move the file)
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Rename a markdown file in place and fix every link to it
|
|
30
|
+
hlpr file mv docs/old-name.md new-name.md
|
|
31
|
+
|
|
32
|
+
# Preview what would change first
|
|
33
|
+
hlpr file mv docs/old-name.md new-name.md --dry
|
|
34
|
+
|
|
35
|
+
# Move a file into another folder
|
|
36
|
+
hlpr file mv docs/old-name.md docs/archive/old-name.md
|
|
37
|
+
|
|
38
|
+
# Only scan a specific subtree for references
|
|
39
|
+
hlpr file mv docs/old-name.md new-name.md --root docs
|
|
40
|
+
|
|
41
|
+
# Overwrite an existing destination file
|
|
42
|
+
hlpr file mv docs/old-name.md docs/existing.md --force
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What Gets Updated
|
|
46
|
+
|
|
47
|
+
For every text file under `--root` (default: current directory), the command rewrites:
|
|
48
|
+
|
|
49
|
+
- **Markdown links & images**: `[text](./old-name.md)`, ``, including
|
|
50
|
+
`<path with spaces>` wrapping, `#heading` fragments, and `"title"` suffixes.
|
|
51
|
+
- **Wikilinks**: `[[old-name]]`, `[[old-name|Alias]]`, `[[old-name#Section]]`. Bare wikilinks
|
|
52
|
+
(no path, Obsidian-style) are matched by basename across the whole scanned root — but only
|
|
53
|
+
when that basename is unique. If another file shares the same basename anywhere under the
|
|
54
|
+
root, bare wikilinks are left untouched to avoid an ambiguous rewrite (the command reports
|
|
55
|
+
this in its output).
|
|
56
|
+
- **Quoted paths**: `'./old-name'`, `"../old-name.md"`, `` `./old-name` `` — covers JS/TS
|
|
57
|
+
imports and requires, and HTML `href`/`src` attributes.
|
|
58
|
+
|
|
59
|
+
Each reference is resolved relative to the *referencing file's own directory* (or to `--root`
|
|
60
|
+
for root-relative/leading-`/` links) before being matched, so files in different folders each
|
|
61
|
+
get the correct relative path to the new location — not just a blind text substitution.
|
|
62
|
+
|
|
63
|
+
Links to unrelated files, and links using absolute URLs (`http://`, `mailto:`, etc.), are never
|
|
64
|
+
touched.
|
|
65
|
+
|
|
66
|
+
## Safety
|
|
67
|
+
|
|
68
|
+
- Errors out if the destination already exists, unless `--force` is passed.
|
|
69
|
+
- Handles case-only renames correctly on case-insensitive filesystems (Windows/macOS).
|
|
70
|
+
- `--dry` reports exactly what would be renamed and which files would be updated (and how many
|
|
71
|
+
references in each), without touching disk.
|
|
72
|
+
- Skips `.git` and `node_modules` when scanning for references.
|
|
73
|
+
|
|
74
|
+
## TypeScript API
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { renameFile } from './commands/file/mv/mv.js'
|
|
78
|
+
|
|
79
|
+
const result = await renameFile('docs/old-name.md', 'new-name.md', {
|
|
80
|
+
root: 'docs',
|
|
81
|
+
dryRun: false,
|
|
82
|
+
force: false,
|
|
83
|
+
updateContent: true,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// result.from / result.to — resolved absolute paths
|
|
87
|
+
// result.updatedFiles — [{ file, count }, ...]
|
|
88
|
+
// result.bareBasenameAmbiguous — true if bare wikilinks were skipped due to a name clash
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## See Also
|
|
92
|
+
|
|
93
|
+
- [Main hlpr README](../../../../README.md)
|
|
94
|
+
- [file rename](../rename/README.md) - bulk case-style renaming for whole directory trees
|
|
95
|
+
- [TypeScript Implementation](./mv.ts)
|
|
96
|
+
- [Tests](./mv.test.ts)
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/commands/file/mv/mv.ts
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
import path from "path";
|
|
7
|
+
async function exists(p) {
|
|
8
|
+
try {
|
|
9
|
+
await fs.access(p);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function pathsEqual(a, b) {
|
|
16
|
+
const ra = path.resolve(a);
|
|
17
|
+
const rb = path.resolve(b);
|
|
18
|
+
return process.platform === "win32" ? ra.toLowerCase() === rb.toLowerCase() : ra === rb;
|
|
19
|
+
}
|
|
20
|
+
function toPosix(p) {
|
|
21
|
+
return p.split(path.sep).join("/");
|
|
22
|
+
}
|
|
23
|
+
async function isTextFile(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
const fd = await fs.open(filePath, "r");
|
|
26
|
+
const buffer = Buffer.alloc(512);
|
|
27
|
+
const { bytesRead } = await fd.read(buffer, 0, 512, 0);
|
|
28
|
+
await fd.close();
|
|
29
|
+
if (bytesRead === 0)
|
|
30
|
+
return true;
|
|
31
|
+
for (let i = 0;i < bytesRead; i++) {
|
|
32
|
+
if (buffer[i] === 0)
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
let printableCount = 0;
|
|
36
|
+
for (let i = 0;i < bytesRead; i++) {
|
|
37
|
+
const byte = buffer[i];
|
|
38
|
+
if (byte >= 32 && byte <= 126 || byte === 9 || byte === 10 || byte === 13) {
|
|
39
|
+
printableCount++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return printableCount / bytesRead > 0.85;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
var DEFAULT_IGNORED_DIRS = new Set([".git", "node_modules"]);
|
|
48
|
+
async function collectFiles(dir, out = []) {
|
|
49
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
if (e.isDirectory() && DEFAULT_IGNORED_DIRS.has(e.name))
|
|
52
|
+
continue;
|
|
53
|
+
const full = path.join(dir, e.name);
|
|
54
|
+
if (e.isDirectory()) {
|
|
55
|
+
await collectFiles(full, out);
|
|
56
|
+
} else if (e.isFile()) {
|
|
57
|
+
out.push(full);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
async function performRename(oldAbs, newAbs, force) {
|
|
63
|
+
await fs.mkdir(path.dirname(newAbs), { recursive: true });
|
|
64
|
+
const destExists = await exists(newAbs);
|
|
65
|
+
const caseOnlyChange = destExists && oldAbs !== newAbs && oldAbs.toLowerCase() === newAbs.toLowerCase();
|
|
66
|
+
if (destExists && !caseOnlyChange) {
|
|
67
|
+
if (!force) {
|
|
68
|
+
throw new Error(`Destination already exists: ${newAbs} (use --force to overwrite)`);
|
|
69
|
+
}
|
|
70
|
+
await fs.rm(newAbs);
|
|
71
|
+
}
|
|
72
|
+
if (caseOnlyChange) {
|
|
73
|
+
const tmp = newAbs + "__tmp_renaming__";
|
|
74
|
+
await fs.rename(oldAbs, tmp);
|
|
75
|
+
try {
|
|
76
|
+
await fs.rename(tmp, newAbs);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
await fs.rename(tmp, oldAbs).catch(() => {});
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
await fs.rename(oldAbs, newAbs);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isExternalLink(target) {
|
|
86
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//");
|
|
87
|
+
}
|
|
88
|
+
function splitFragment(target) {
|
|
89
|
+
const idx = target.indexOf("#");
|
|
90
|
+
if (idx === -1)
|
|
91
|
+
return { path: target, fragment: "" };
|
|
92
|
+
return { path: target.slice(0, idx), fragment: target.slice(idx) };
|
|
93
|
+
}
|
|
94
|
+
function safeDecodeURIComponent(s) {
|
|
95
|
+
try {
|
|
96
|
+
return decodeURIComponent(s);
|
|
97
|
+
} catch {
|
|
98
|
+
return s;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function encodeLikeOriginal(newPathStr, original) {
|
|
102
|
+
if (/%20/.test(original)) {
|
|
103
|
+
return newPathStr.replace(/ /g, "%20");
|
|
104
|
+
}
|
|
105
|
+
return newPathStr;
|
|
106
|
+
}
|
|
107
|
+
function resolveTargetMatch(targetPath, fileDir, root, oldAbs, oldExt) {
|
|
108
|
+
const hadExt = !!path.extname(targetPath);
|
|
109
|
+
const withDefaultExt = (p) => hadExt ? [p] : [p, p + oldExt];
|
|
110
|
+
if (targetPath.startsWith("/")) {
|
|
111
|
+
const resolved = path.resolve(root, "." + targetPath);
|
|
112
|
+
return withDefaultExt(resolved).some((c) => pathsEqual(c, oldAbs)) ? "root-relative" : null;
|
|
113
|
+
}
|
|
114
|
+
const fileResolved = path.resolve(fileDir, targetPath);
|
|
115
|
+
if (withDefaultExt(fileResolved).some((c) => pathsEqual(c, oldAbs)))
|
|
116
|
+
return "file-relative";
|
|
117
|
+
const rootResolved = path.resolve(root, targetPath);
|
|
118
|
+
if (withDefaultExt(rootResolved).some((c) => pathsEqual(c, oldAbs)))
|
|
119
|
+
return "root-relative";
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function tryRewriteTarget(rawTarget, fileDir, root, oldAbs, newAbs, allowBareBasename) {
|
|
123
|
+
if (!rawTarget || isExternalLink(rawTarget))
|
|
124
|
+
return null;
|
|
125
|
+
const { path: targetPath, fragment } = splitFragment(rawTarget);
|
|
126
|
+
if (!targetPath)
|
|
127
|
+
return null;
|
|
128
|
+
const decoded = safeDecodeURIComponent(targetPath);
|
|
129
|
+
const oldExt = path.extname(oldAbs);
|
|
130
|
+
const newExt = path.extname(newAbs);
|
|
131
|
+
const hadExt = !!path.extname(decoded);
|
|
132
|
+
const hasSlash = decoded.includes("/") || decoded.includes("\\");
|
|
133
|
+
let matchKind = null;
|
|
134
|
+
if (allowBareBasename && !hasSlash) {
|
|
135
|
+
const compareBase = hadExt ? decoded : decoded + oldExt;
|
|
136
|
+
if (pathsEqual(path.basename(compareBase), path.basename(oldAbs))) {
|
|
137
|
+
matchKind = "bare-basename";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!matchKind) {
|
|
141
|
+
matchKind = resolveTargetMatch(decoded, fileDir, root, oldAbs, oldExt);
|
|
142
|
+
}
|
|
143
|
+
if (!matchKind)
|
|
144
|
+
return null;
|
|
145
|
+
let newTargetPath;
|
|
146
|
+
if (matchKind === "bare-basename") {
|
|
147
|
+
const newBase = path.basename(newAbs, newExt);
|
|
148
|
+
newTargetPath = hadExt ? newBase + newExt : newBase;
|
|
149
|
+
} else {
|
|
150
|
+
const base = matchKind === "root-relative" ? root : fileDir;
|
|
151
|
+
let rel = toPosix(path.relative(base, newAbs));
|
|
152
|
+
if (!hadExt && newExt && rel.endsWith(newExt)) {
|
|
153
|
+
rel = rel.slice(0, -newExt.length);
|
|
154
|
+
}
|
|
155
|
+
const wasRootAbsolute = decoded.startsWith("/");
|
|
156
|
+
const wasExplicitRelative = decoded.startsWith("./") || decoded.startsWith("../");
|
|
157
|
+
if (wasRootAbsolute) {
|
|
158
|
+
newTargetPath = "/" + rel;
|
|
159
|
+
} else if (wasExplicitRelative || rel.startsWith("..")) {
|
|
160
|
+
newTargetPath = rel.startsWith(".") ? rel : "./" + rel;
|
|
161
|
+
} else {
|
|
162
|
+
newTargetPath = rel;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return encodeLikeOriginal(newTargetPath, targetPath) + fragment;
|
|
166
|
+
}
|
|
167
|
+
function parseMdTarget(raw) {
|
|
168
|
+
const s = raw.trim();
|
|
169
|
+
if (s.startsWith("<")) {
|
|
170
|
+
const end = s.indexOf(">");
|
|
171
|
+
if (end !== -1) {
|
|
172
|
+
return { link: s.slice(1, end), suffix: s.slice(end + 1), wrapped: true };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const titleIdx = s.search(/\s+["']/);
|
|
176
|
+
if (titleIdx !== -1) {
|
|
177
|
+
return { link: s.slice(0, titleIdx), suffix: s.slice(titleIdx), wrapped: false };
|
|
178
|
+
}
|
|
179
|
+
return { link: s, suffix: "", wrapped: false };
|
|
180
|
+
}
|
|
181
|
+
function parseWikiTarget(raw) {
|
|
182
|
+
const idx = raw.search(/[#|]/);
|
|
183
|
+
if (idx === -1)
|
|
184
|
+
return { link: raw, rest: "" };
|
|
185
|
+
return { link: raw.slice(0, idx), rest: raw.slice(idx) };
|
|
186
|
+
}
|
|
187
|
+
function rewriteContent(content, fileDir, root, oldAbs, newAbs, allowBareBasename) {
|
|
188
|
+
let count = 0;
|
|
189
|
+
let updated = content;
|
|
190
|
+
updated = updated.replace(/(!?\[[^\]]*\])\(([^)]+)\)/g, (full, textPart, rawTarget) => {
|
|
191
|
+
const { link, suffix, wrapped } = parseMdTarget(rawTarget);
|
|
192
|
+
const newLink = tryRewriteTarget(link, fileDir, root, oldAbs, newAbs, false);
|
|
193
|
+
if (newLink === null)
|
|
194
|
+
return full;
|
|
195
|
+
count++;
|
|
196
|
+
const rebuilt = wrapped ? `<${newLink}>` : newLink;
|
|
197
|
+
return `${textPart}(${rebuilt}${suffix})`;
|
|
198
|
+
});
|
|
199
|
+
updated = updated.replace(/\[\[([^\]]+)\]\]/g, (full, inner) => {
|
|
200
|
+
const { link, rest } = parseWikiTarget(inner);
|
|
201
|
+
const newLink = tryRewriteTarget(link, fileDir, root, oldAbs, newAbs, allowBareBasename);
|
|
202
|
+
if (newLink === null)
|
|
203
|
+
return full;
|
|
204
|
+
count++;
|
|
205
|
+
return `[[${newLink}${rest}]]`;
|
|
206
|
+
});
|
|
207
|
+
updated = updated.replace(/(['"`])([^'"`]*\/[^'"`]*)\1/g, (full, quote, rawTarget) => {
|
|
208
|
+
const newLink = tryRewriteTarget(rawTarget, fileDir, root, oldAbs, newAbs, false);
|
|
209
|
+
if (newLink === null)
|
|
210
|
+
return full;
|
|
211
|
+
count++;
|
|
212
|
+
return `${quote}${newLink}${quote}`;
|
|
213
|
+
});
|
|
214
|
+
return { content: updated, count };
|
|
215
|
+
}
|
|
216
|
+
async function renameFile(oldPathArg, newPathArg, options = {}) {
|
|
217
|
+
const oldAbs = path.resolve(oldPathArg);
|
|
218
|
+
const hasDirSeparator = newPathArg.includes("/") || newPathArg.includes("\\");
|
|
219
|
+
const newAbs = hasDirSeparator ? path.resolve(newPathArg) : path.join(path.dirname(oldAbs), newPathArg);
|
|
220
|
+
const root = path.resolve(options.root ?? process.cwd());
|
|
221
|
+
const updateContentEnabled = options.updateContent ?? true;
|
|
222
|
+
const dryRun = options.dryRun ?? false;
|
|
223
|
+
const force = options.force ?? false;
|
|
224
|
+
if (!await exists(oldAbs)) {
|
|
225
|
+
throw new Error(`Source file does not exist: ${oldAbs}`);
|
|
226
|
+
}
|
|
227
|
+
const oldStat = await fs.stat(oldAbs);
|
|
228
|
+
if (!oldStat.isFile()) {
|
|
229
|
+
throw new Error(`Source is not a file: ${oldAbs}`);
|
|
230
|
+
}
|
|
231
|
+
if (pathsEqual(oldAbs, newAbs) && oldAbs === newAbs) {
|
|
232
|
+
throw new Error("Source and destination are the same path");
|
|
233
|
+
}
|
|
234
|
+
const result = { from: oldAbs, to: newAbs, updatedFiles: [], bareBasenameAmbiguous: false };
|
|
235
|
+
const allFiles = updateContentEnabled ? await collectFiles(root) : [];
|
|
236
|
+
const oldBasenameLower = path.basename(oldAbs).toLowerCase();
|
|
237
|
+
const sameBasenameElsewhere = allFiles.some((f) => !pathsEqual(f, oldAbs) && path.basename(f).toLowerCase() === oldBasenameLower);
|
|
238
|
+
const allowBareBasename = !sameBasenameElsewhere;
|
|
239
|
+
result.bareBasenameAmbiguous = sameBasenameElsewhere;
|
|
240
|
+
if (!dryRun) {
|
|
241
|
+
await performRename(oldAbs, newAbs, force);
|
|
242
|
+
}
|
|
243
|
+
if (updateContentEnabled) {
|
|
244
|
+
for (const file of allFiles) {
|
|
245
|
+
if (pathsEqual(file, oldAbs) || pathsEqual(file, newAbs))
|
|
246
|
+
continue;
|
|
247
|
+
if (!await isTextFile(file))
|
|
248
|
+
continue;
|
|
249
|
+
const original = await fs.readFile(file, "utf-8");
|
|
250
|
+
const { content: updated, count } = rewriteContent(original, path.dirname(file), root, oldAbs, newAbs, allowBareBasename);
|
|
251
|
+
if (count > 0) {
|
|
252
|
+
result.updatedFiles.push({ file, count });
|
|
253
|
+
if (!dryRun) {
|
|
254
|
+
await fs.writeFile(file, updated, "utf-8");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, "/"))) {
|
|
262
|
+
const rawArgs = process.argv.slice(2);
|
|
263
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h") || rawArgs.includes("/help") || rawArgs.includes("/h") || rawArgs.includes("/?")) {
|
|
264
|
+
console.log("Usage: mv <oldPath> <newPath> [--root <dir>] [--dry|-n] [--force] [--no-update-content]");
|
|
265
|
+
console.log("Renames/moves a single file and updates references to it (markdown links, wikilinks,");
|
|
266
|
+
console.log("imports, href/src) in every text file under <dir> (default: current directory), recursively.");
|
|
267
|
+
console.log("Options:");
|
|
268
|
+
console.log(" --root <dir> Directory to scan for references (default: current directory)");
|
|
269
|
+
console.log(" --dry, -n Preview changes without applying them");
|
|
270
|
+
console.log(" --force Overwrite destination file if it already exists");
|
|
271
|
+
console.log(" --no-update-content Skip updating references in other files");
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
const positional = [];
|
|
275
|
+
let root;
|
|
276
|
+
let dryRun = false;
|
|
277
|
+
let force = false;
|
|
278
|
+
let updateContent = true;
|
|
279
|
+
for (let i = 0;i < rawArgs.length; i++) {
|
|
280
|
+
const a = rawArgs[i];
|
|
281
|
+
if (a === "--dry" || a === "-n")
|
|
282
|
+
dryRun = true;
|
|
283
|
+
else if (a === "--force")
|
|
284
|
+
force = true;
|
|
285
|
+
else if (a === "--no-update-content")
|
|
286
|
+
updateContent = false;
|
|
287
|
+
else if (a === "--root")
|
|
288
|
+
root = rawArgs[++i];
|
|
289
|
+
else
|
|
290
|
+
positional.push(a);
|
|
291
|
+
}
|
|
292
|
+
const [oldPathArg, newPathArg] = positional;
|
|
293
|
+
if (!oldPathArg || !newPathArg) {
|
|
294
|
+
console.error("Usage: mv <oldPath> <newPath> [--root <dir>] [--dry|-n] [--force] [--no-update-content]");
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
renameFile(oldPathArg, newPathArg, { root, dryRun, force, updateContent }).then((result) => {
|
|
298
|
+
if (dryRun) {
|
|
299
|
+
console.log(`Dry run - would rename:
|
|
300
|
+
${result.from} \u2192 ${result.to}`);
|
|
301
|
+
} else {
|
|
302
|
+
console.log(`Renamed:
|
|
303
|
+
${result.from} \u2192 ${result.to}`);
|
|
304
|
+
}
|
|
305
|
+
if (result.updatedFiles.length > 0) {
|
|
306
|
+
console.log(`
|
|
307
|
+
${dryRun ? "Would update" : "Updated"} ${result.updatedFiles.length} file(s):`);
|
|
308
|
+
result.updatedFiles.forEach(({ file, count }) => {
|
|
309
|
+
console.log(` ${file} (${count} reference${count === 1 ? "" : "s"})`);
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
console.log(`
|
|
313
|
+
No references found to update.`);
|
|
314
|
+
}
|
|
315
|
+
if (result.bareBasenameAmbiguous) {
|
|
316
|
+
console.log(`
|
|
317
|
+
Note: another file also named "${path.basename(result.from)}" exists under the scanned root, ` + "so bare wikilinks (e.g. [[name]]) were not auto-matched to avoid an ambiguous rewrite.");
|
|
318
|
+
}
|
|
319
|
+
}).catch((err) => {
|
|
320
|
+
console.error("Error:", err.message ?? err);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
var mv_default = {
|
|
325
|
+
renameFile
|
|
326
|
+
};
|
|
327
|
+
export {
|
|
328
|
+
mv_default as default,
|
|
329
|
+
renameFile
|
|
330
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/hlpr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Windows and *Nix utility for typical programming activity",
|
|
5
5
|
"author": "Vitalii Balabanov",
|
|
6
6
|
"email": "tertiumnon@gmail.com",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"@tertium/js": "^1.4.7"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@types/
|
|
25
|
+
"@types/bun": "^1.4.1",
|
|
26
|
+
"@types/node": "^26.4.1",
|
|
26
27
|
"typescript": "^5.8.3"
|
|
27
28
|
},
|
|
28
29
|
"bin": {
|