@uplift-beyond/upex 0.0.0-stage → 1.5.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Uplift Beyond
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,328 @@
1
- # Temporary Holding Version
1
+ # ⚡ upex: Personal File Shortcut Manager
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A professional Node.js CLI tool that lets you register named aliases for folder paths and instantly open them in the file explorer or a specified IDE/app.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm i -g @uplift-beyond/upex
9
+ ```
10
+
11
+ From source:
12
+
13
+ ```bash
14
+ npm install
15
+
16
+ # Link globally so you can use "upex" anywhere
17
+ npm link
18
+ ```
19
+
20
+ ## Commands
21
+
22
+ | Command | Description |
23
+ |---|---|
24
+ | `upex add <name> [path]` | Register a folder shortcut (defaults to current directory) |
25
+ | `upex add <name> <app> --app` | Register an app shortcut (the path is the executable) |
26
+ | `upex add <name> "<command>" --cmd` | Register a command line (runs in a terminal window) |
27
+ | `upex add <name> <url> --url` | Register a URL shortcut (opens in the browser) |
28
+ | `upex add <name> [path] --tag a,b` | Attach tags while registering |
29
+ | `upex` | Open the interactive shortcut selector and choose shortcut/app |
30
+ | `upex --app <app>` | Open the interactive selector with a preselected app |
31
+ | `upex <name>` | Open shortcut in Explorer / default app (or launch stored app/URL) |
32
+ | `upex <name> <app>` | Open in a specific app (vscode, cmd, open, or a custom app) |
33
+ | `upex <name>/<sub>` | Open (or `cd` via the wrapper) a folder inside a shortcut, e.g. `upex web/src` |
34
+ | `upex -` / `upex last` | Reopen the most recently used shortcut |
35
+ | `upex go <name> [start...]` | Print the path (cd via shell wrapper); extra args run as a command in the folder |
36
+ | `upex cmd <name> [count] [start]` | Open N terminals, optionally running a start command |
37
+ | `upex run <name> [script]` | Run a package.json script in a terminal (lists scripts if none given) |
38
+ | `upex tag <name> [tags...]` | List all tags, show/add tags, or remove with `-r` / `--clear` |
39
+ | `upex open <tag>` | Open every shortcut carrying a tag |
40
+ | `upex ws [name\|add\|rm] ...` | Open or manage workspaces (sets of shortcut+app pairs) |
41
+ | `upex export [file]` | Export shortcuts + config as a JSON bundle (stdout if no file) |
42
+ | `upex import <file> [--replace]` | Import a bundle (merge by default) |
43
+ | `upex list [tag] [--git]` | Show all saved shortcuts (filter by tag; `--git` adds a branch column) |
44
+ | `upex remove <name>` | Delete a saved shortcut |
45
+ | `upex rename <old> <new>` | Rename a shortcut |
46
+ | `upex edit <name> [path]` | Update a shortcut's path (defaults to cwd) |
47
+ | `upex edit <name> --opener <app>` | Open this shortcut in `<app>` by default (`--no-opener` clears; also on `add`) |
48
+ | `upex copy <name>` | Copy shortcut path to clipboard |
49
+ | `upex app [alias] [command]` | Manage custom openers (list / add / `--remove`) |
50
+ | `upex check [--prune]` | Report broken paths (`--prune` removes them) |
51
+ | `upex doctor` | Check the store, openers, shell wrapper and completion |
52
+ | `upex ui [--port <n>] [--no-open]` | Manage shortcuts and workspaces in the browser |
53
+ | `upex completion [shell]` | Print a shell completion script (bash/zsh/fish/powershell) |
54
+ | `upex setup [cmd]` | Shell integration; `setup cmd` installs the cmd.exe wrapper |
55
+ | `upex help` | Show all available commands |
56
+
57
+ ## Supported Apps
58
+
59
+ | Alias | App |
60
+ |---|---|
61
+ | `vscode` / `code` | VS Code |
62
+ | `webstorm` | WebStorm |
63
+ | `cmd` | Command Prompt (Windows) / Terminal |
64
+ | `claude` | Terminal running Claude Code (`claude`) |
65
+ | `finder` / `explorer` | System File Manager (reveals a file shortcut) |
66
+ | `open` | OS default handler (opens a file in its default app) |
67
+ | `copy` | Copy the path to the clipboard |
68
+
69
+ Add your own openers with `upex app`:
70
+
71
+ ```bash
72
+ upex app cursor cursor # register "cursor" -> the cursor command
73
+ upex webapp cursor # open a shortcut in Cursor
74
+ upex app # list custom apps
75
+ upex app cursor --remove # remove one
76
+ ```
77
+
78
+ ## Examples
79
+
80
+ ```bash
81
+ # Save current directory as "myproject"
82
+ upex add myproject
83
+
84
+ # Save a specific path
85
+ upex add webapp /Users/me/projects/webapp
86
+
87
+ # Open in file explorer
88
+ upex webapp
89
+
90
+ # Open in VS Code
91
+ upex webapp vscode
92
+
93
+ # Open in Command Prompt (Windows, cwd set to path)
94
+ upex webapp cmd
95
+
96
+ # Fuzzy matching works: "webap" resolves to "webapp"
97
+ upex webap vscode
98
+
99
+ # Register an app shortcut (the path is the executable, --app is a flag)
100
+ upex add chrome "C:\Program Files\Google\Chrome\Application\chrome.exe" --app
101
+ upex chrome # launches Chrome
102
+
103
+ # Register a command line; it runs in a new terminal window (starts in your home folder)
104
+ upex add dev "cd /d E:\projects\app && npm run dev" --cmd
105
+ upex dev
106
+
107
+ # Save a file (opens in its default app; "explorer" reveals it in its folder)
108
+ upex add notes "E:\docs\notes.md"
109
+ upex notes # opens notes.md
110
+ upex notes explorer # reveals it in Explorer
111
+
112
+ # Open a shortcut in 3 terminal windows
113
+ upex cmd webapp 3
114
+
115
+ # Open a terminal at webapp and run "claude" in it (window stays open)
116
+ upex cmd webapp claude
117
+
118
+ # Two terminals, each running "dir"
119
+ upex cmd webapp 2 dir
120
+
121
+ # cd into a shortcut (with the shell wrapper from `upex setup`)
122
+ upex go webapp
123
+
124
+ # cd into a shortcut and run a command there
125
+ upex go webapp npm run dev
126
+
127
+ # Open the interactive selector
128
+ upex
129
+
130
+ # List all shortcuts
131
+ upex list
132
+
133
+ # Check for broken paths
134
+ upex check
135
+ ```
136
+
137
+ ## Tags & Groups
138
+
139
+ Attach tags to organize shortcuts, filter the list, and open a whole group at once.
140
+
141
+ ```bash
142
+ upex add webapp --tag work,web # tag while adding
143
+ upex tag webapp ci # add a tag later
144
+ upex tag webapp -r web # remove a tag (--clear drops all)
145
+ upex tag # list every tag with counts
146
+ upex list work # only #work shortcuts
147
+ upex open work # open every #work shortcut in its default opener
148
+ ```
149
+
150
+ ## Workspaces
151
+
152
+ A workspace is a named set of shortcut+app pairs opened together, e.g. code + a terminal + a browser tab.
153
+
154
+ ```bash
155
+ upex ws add dev webapp vscode # webapp in VS Code
156
+ upex ws add dev webapp cmd # ...and a terminal
157
+ upex ws add dev docs open # ...and a URL/file shortcut in its default opener
158
+ upex ws # list workspaces
159
+ upex ws dev # open the whole workspace
160
+ upex ws rm dev webapp # drop one entry (omit the shortcut to delete the workspace)
161
+ ```
162
+
163
+ A `cmd` entry accepts the same `[count] [start...]` tail as `upex cmd`, so a workspace can boot a terminal straight into a command (flags pass through):
164
+
165
+ ```bash
166
+ upex ws add dev webapp cmd claude --model sonnet # terminal running claude
167
+ upex ws add dev webapp cmd 2 npm run dev # two terminals running "npm run dev"
168
+ ```
169
+
170
+ ## URL Shortcuts, run & last
171
+
172
+ ```bash
173
+ upex add gh github.com --url # bare hosts get https:// automatically
174
+ upex gh # opens in the browser
175
+
176
+ upex run webapp # list package.json scripts (npm/pnpm/yarn/bun auto-detected)
177
+ upex run webapp dev # open a terminal running "<pm> run dev" at webapp
178
+
179
+ upex - # reopen the most recently used shortcut (alias: upex last)
180
+ ```
181
+
182
+ ## Backup & Sync
183
+
184
+ `export` writes a JSON bundle of your shortcuts and config; `import` merges it back (or `--replace`).
185
+
186
+ ```bash
187
+ upex export ~/upex-backup.json # or omit the file to print to stdout
188
+ upex import ~/upex-backup.json # merge (incoming wins on name clash)
189
+ upex import backup.json --replace # replace everything
190
+ ```
191
+
192
+ `upex list --git` adds a column with each folder's current git branch (yellow `branch*` when the working tree is dirty, `--` when it isn't a repo). It's off by default.
193
+
194
+ ## Interactive Mode
195
+
196
+ Run `upex` with no arguments to open the interactive selector. Shortcuts are ordered by frecency (most-used and most-recent first).
197
+
198
+ - `Up` / `Down` -- move through shortcuts
199
+ - `Left` / `Right` -- change the selected app (starts on the shortcut's default opener)
200
+ - `Ctrl+Up` / `Ctrl+Down` -- terminal count when a terminal opener (cmd, claude) is selected
201
+ - `Tab` -- cycle the Shortcuts (folders), Apps (app + URL shortcuts) and Workspaces views (Enter opens the whole workspace, `Ctrl+D` deletes it)
202
+ - `Enter` -- open the selected shortcut in the selected app
203
+ - Type -- fuzzy-filter the list
204
+ - `Ctrl+A` -- add a shortcut for the current directory
205
+ - `Ctrl+E` -- edit the selected shortcut's path
206
+ - `Ctrl+R` -- rename the selected shortcut
207
+ - `Ctrl+D` -- delete the selected shortcut (confirm with `y`)
208
+ - `Esc` -- cancel
209
+
210
+ ## Web UI
211
+
212
+ Run `upex ui` to open a local page (default `http://127.0.0.1:4545`, a free port if that one is busy) where you can add, edit, rename, tag, delete, copy and open shortcuts, set a default opener, open terminals with a window count and start command, build, reorder and rename workspaces, and remove missing paths. Lists sort by most used (or A to Z) and re-check paths when you come back to the page. Folders, apps (app, command and URL shortcuts) and workspaces each get their own tab. Press `/` to search, `n` for a new shortcut or app. Stop it with `Ctrl+C`.
213
+
214
+ It only listens on 127.0.0.1 and every request needs a token that is new each run, so other sites can't use it. With `--no-open`, open the link it prints (it carries the token).
215
+
216
+ ## Shell Completion
217
+
218
+ Enable tab-completion of command and shortcut names:
219
+
220
+ ```bash
221
+ # bash / zsh (add to ~/.bashrc or ~/.zshrc)
222
+ eval "$(upex completion bash)"
223
+
224
+ # PowerShell (add to $PROFILE)
225
+ upex completion powershell | Out-String | Invoke-Expression
226
+ ```
227
+
228
+ ## Shell Integration (cd support)
229
+
230
+ Because a Node.js process can't change your shell's directory, add this wrapper function for instant `cd` behavior:
231
+
232
+ ### Bash / Zsh (`~/.bashrc` or `~/.zshrc`)
233
+
234
+ ```bash
235
+ upex() {
236
+ if [ "$1" = "go" ] && [ -n "$2" ]; then
237
+ if result=$(command upex go "$2" 2>/dev/null) && cd "$result"; then
238
+ shift 2
239
+ [ $# -eq 0 ] || "$@"
240
+ return
241
+ fi
242
+ fi
243
+ if [ "$1" != "" ] && [ "$2" = "" ] && result=$(command upex __resolve "$1" 2>/dev/null); then
244
+ cd "$result"
245
+ else
246
+ command upex "$@"
247
+ fi
248
+ }
249
+ ```
250
+
251
+ ### Command Prompt (cmd.exe)
252
+
253
+ ```cmd
254
+ upex setup cmd
255
+ ```
256
+
257
+ Installs an `upex.bat` wrapper next to the npm shim. `.bat` beats `.cmd` in PATHEXT resolution, and a `cd` inside a batch file persists in the parent shell, so `upex go <name>` and bare `upex <name>` cd directly. `upex go <name> <command...>` cds and then runs the command in that folder (e.g. `upex go webapp npm run dev`). Works immediately, no profile or restart needed.
258
+
259
+ ### PowerShell (`$PROFILE`)
260
+
261
+ ```powershell
262
+ function upex {
263
+ if ($args.Count -ge 2 -and $args[0] -eq 'go') {
264
+ $result = & upex.ps1 go $args[1] 2>$null
265
+ if ($LASTEXITCODE -eq 0 -and $result) {
266
+ Set-Location $result
267
+ $rest = @($args | Select-Object -Skip 2)
268
+ if ($rest.Count) { Invoke-Expression ($rest -join ' ') }
269
+ return
270
+ }
271
+ }
272
+ if ($args.Count -eq 1) {
273
+ $result = & upex.ps1 __resolve $args[0] 2>$null
274
+ if ($LASTEXITCODE -eq 0 -and $result) {
275
+ Set-Location $result
276
+ return
277
+ }
278
+ }
279
+ & upex.ps1 @args
280
+ }
281
+ ```
282
+
283
+ ## Storage
284
+
285
+ Shortcuts are stored in `~/.upex/links.json` and created automatically on first use.
286
+
287
+ Each entry is an object with optional fields:
288
+
289
+ ```json
290
+ {
291
+ "webapp": { "path": "E:\\projects\\webapp", "opener": "vscode", "uses": 12, "lastUsed": 1752710400000 },
292
+ "chrome": { "app": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" }
293
+ }
294
+ ```
295
+
296
+ `opener` is the default opener for `upex <name>` (set with `--opener`). `uses` / `lastUsed` are written automatically and drive the interactive selector's frecency ordering.
297
+
298
+ Legacy string entries (`"name": "/path"`) are auto-migrated on read.
299
+
300
+ Config is stored in `~/.upex/config.json`, including custom openers under `apps`:
301
+
302
+ ```json
303
+ {
304
+ "apps": { "cursor": "cursor", "sublime": "C:\\...\\sublime_text.exe" }
305
+ }
306
+ ```
307
+
308
+ ### WebStorm on Windows (without `setx`)
309
+
310
+ If `upex <name> webstorm` does not find WebStorm, add this file:
311
+
312
+ ```json
313
+ {
314
+ "webstormPath": "C:\\Program Files\\JetBrains\\WebStorm\\bin\\webstorm64.exe"
315
+ }
316
+ ```
317
+
318
+ Then run:
319
+
320
+ ```bash
321
+ upex <name> webstorm
322
+ ```
323
+
324
+ `setx` is only a Windows command for writing environment variables permanently; it is optional now.
325
+
326
+ ## License
327
+
328
+ MIT, see [LICENSE](LICENSE).
package/bin/upex.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createProgram } from '../src/index.js';
4
+ import { StoreError } from '../src/store.js';
5
+ import * as ui from '../src/ui.js';
6
+
7
+ // Shorthand: `upex -` reopens the most recently used shortcut.
8
+ const argv = process.argv.slice();
9
+ if (argv[2] === '-') argv[2] = 'last';
10
+
11
+ const program = createProgram();
12
+ program.parseAsync(argv).catch((err) => {
13
+ if (!(err instanceof StoreError)) throw err;
14
+ ui.error(err.message);
15
+ process.exit(1);
16
+ });
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
- {
2
- "name": "@uplift-beyond/upex",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@uplift-beyond/upex",
3
+ "version": "1.5.0",
4
+ "description": "A personal file shortcut manager: register named aliases for folder paths and instantly open them.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "upex": "./bin/upex.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "test": "node test/smoke.mjs"
18
+ },
19
+ "keywords": [
20
+ "cli",
21
+ "shortcuts",
22
+ "file-manager",
23
+ "developer-tools"
24
+ ],
25
+ "author": "uplift-beyond.de",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/JennyTheGem/upex.git"
29
+ },
30
+ "homepage": "https://github.com/JennyTheGem/upex#readme",
31
+ "bugs": "https://github.com/JennyTheGem/upex/issues",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "chalk": "^5.6.2",
35
+ "commander": "^14.0.3",
36
+ "fuse.js": "^7.3.0"
37
+ }
38
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * add.js -- Register a new shortcut
3
+ * Usage: upex add <name> [path] folder shortcut (defaults to cwd)
4
+ * upex add <name> <app> --app app shortcut (the path IS the executable)
5
+ * upex add <name> "<line>" --cmd command line, run in a terminal window
6
+ */
7
+
8
+ import path from 'node:path';
9
+ import fs from 'node:fs';
10
+ import { setLink, linkExists, nameError } from '../store.js';
11
+ import { withOpener } from './edit.js';
12
+ import * as ui from '../ui.js';
13
+
14
+ // "work,personal" -> ["work", "personal"]; undefined -> undefined (leave field off).
15
+ export function parseTags(value) {
16
+ if (!value) return undefined;
17
+ const tags = value.split(',').map(t => t.trim()).filter(Boolean);
18
+ return tags.length ? tags : undefined;
19
+ }
20
+
21
+ export function registerAdd(program) {
22
+ program
23
+ .command('add <name> [target]')
24
+ .description('Register a shortcut (folder, or an app with --app, a command line with --cmd, or a URL with --url)')
25
+ .summary('Register a folder, app (--app), command (--cmd) or URL (--url)')
26
+ .option('--app', 'Treat <target> as an application to launch')
27
+ .option('--url', 'Treat <target> as a URL to open in the browser')
28
+ .option('--cmd', 'Treat <target> as a command line to run in a terminal window')
29
+ .option('-t, --tag <tags>', 'Comma-separated tags to attach')
30
+ .option('-o, --opener <app>', 'Open this folder shortcut in <app> by default')
31
+ .action((name, targetPath, options) => {
32
+ if (options.opener && (options.app || options.url || options.cmd)) {
33
+ ui.error('A default opener only applies to folder shortcuts.');
34
+ process.exit(1);
35
+ }
36
+
37
+ const bad = nameError(name);
38
+ if (bad) {
39
+ ui.error(bad);
40
+ process.exit(1);
41
+ }
42
+
43
+ if (linkExists(name)) {
44
+ ui.warn(`"${name}" already exists. Use "upex edit ${name}" to update.`);
45
+ return;
46
+ }
47
+
48
+ const tags = parseTags(options.tag);
49
+
50
+ // URL shortcut: the positional is a URL, opened in the browser.
51
+ if (options.url) {
52
+ if (!targetPath) {
53
+ ui.error('Provide the URL: upex add <name> <url> --url');
54
+ process.exit(1);
55
+ }
56
+ setLink(name, { url: targetPath, ...(tags && { tags }) });
57
+ ui.success(`Added: ${name} --> ${targetPath}`);
58
+ return;
59
+ }
60
+
61
+ // App shortcut: the positional is the executable/command, stored raw.
62
+ // --cmd: a command line run in a terminal window instead.
63
+ if (options.app || options.cmd) {
64
+ if (!targetPath) {
65
+ ui.error(options.cmd
66
+ ? 'Provide the command line in quotes: upex add <name> "<command>" --cmd'
67
+ : 'Provide the app path or command: upex add <name> <app> --app');
68
+ process.exit(1);
69
+ }
70
+ setLink(name, { app: targetPath, ...(options.cmd && { terminal: true }), ...(tags && { tags }) });
71
+ ui.success(`Added: ${name} --> ${targetPath}`);
72
+ return;
73
+ }
74
+
75
+ // Folder shortcut (defaults to cwd).
76
+ const resolved = targetPath ? path.resolve(targetPath) : process.cwd();
77
+ if (!fs.existsSync(resolved)) {
78
+ ui.error(`Path does not exist: ${resolved}`);
79
+ process.exit(1);
80
+ }
81
+ let entry = { path: resolved, ...(tags && { tags }) };
82
+ if (options.opener) entry = withOpener(entry, options.opener);
83
+ if (!entry) process.exit(1);
84
+ setLink(name, entry);
85
+ ui.success(`Added: ${name} --> ${resolved}`);
86
+ });
87
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * app.js -- Manage user-defined openers stored in ~/.upex/config.json.
3
+ * Usage: upex app -- list custom apps
4
+ * upex app <alias> <command> -- add/update a custom app
5
+ * upex app <alias> --remove -- remove a custom app
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import { loadConfig, saveConfig } from '../store.js';
10
+ import * as ui from '../ui.js';
11
+
12
+ export function registerApp(program) {
13
+ program
14
+ .command('app [alias] [command]')
15
+ .description('Manage custom openers (e.g. "upex app cursor cursor")')
16
+ .option('--remove', 'Remove the named custom app')
17
+ .action((alias, command, options) => {
18
+ const config = loadConfig();
19
+ const apps = config.apps && typeof config.apps === 'object' ? config.apps : {};
20
+
21
+ // No alias: list what's defined.
22
+ if (!alias) {
23
+ const entries = Object.entries(apps);
24
+ if (entries.length === 0) {
25
+ ui.info('No custom apps. Add one: upex app <alias> <command>');
26
+ return;
27
+ }
28
+ ui.heading('Custom Apps');
29
+ ui.table(
30
+ [[chalk.bold('ALIAS'), chalk.bold('COMMAND')], ...entries.map(([a, c]) => [chalk.bold(a), c])],
31
+ { header: true },
32
+ );
33
+ ui.blank();
34
+ return;
35
+ }
36
+
37
+ if (options.remove) {
38
+ if (!Object.hasOwn(apps, alias)) {
39
+ ui.error(`No custom app named "${alias}".`);
40
+ return;
41
+ }
42
+ delete apps[alias];
43
+ saveConfig({ ...config, apps });
44
+ ui.success(`Removed custom app: ${alias}`);
45
+ return;
46
+ }
47
+
48
+ if (!command) {
49
+ ui.error('Provide a command: upex app <alias> <command>');
50
+ return;
51
+ }
52
+
53
+ apps[alias] = command;
54
+ saveConfig({ ...config, apps });
55
+ ui.success(`Saved custom app: ${alias} --> ${command}`);
56
+ ui.dim(`Use it: upex <shortcut> ${alias}`);
57
+ });
58
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * check.js -- Scan shortcuts and report broken paths.
3
+ * Usage: upex check
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import chalk from 'chalk';
8
+ import { loadLinks, saveLinks, pruneBroken } from '../store.js';
9
+ import * as ui from '../ui.js';
10
+
11
+ export function registerCheck(program) {
12
+ program
13
+ .command('check')
14
+ .description('Scan all shortcuts and report broken paths')
15
+ .option('--prune', 'Remove shortcuts whose folder path no longer exists')
16
+ .action((options) => {
17
+ const links = loadLinks();
18
+ const entries = Object.entries(links);
19
+
20
+ if (entries.length === 0) {
21
+ ui.info('No shortcuts saved.');
22
+ return;
23
+ }
24
+
25
+ ui.heading('Health Check');
26
+
27
+ let brokenCount = 0;
28
+
29
+ const rows = [
30
+ [chalk.bold('NAME'), chalk.bold('PATH'), chalk.bold('STATUS')],
31
+ ...entries.map(([name, entry]) => {
32
+ const filepath = entry.path;
33
+ if (!filepath) {
34
+ // App-only shortcut, no path to check
35
+ return [
36
+ chalk.bold(name),
37
+ chalk.dim('(app only)'),
38
+ ui.tag.skip,
39
+ ];
40
+ }
41
+ const exists = fs.existsSync(filepath);
42
+ if (!exists) brokenCount++;
43
+ return [
44
+ chalk.bold(name),
45
+ exists ? filepath : chalk.dim(filepath),
46
+ exists ? ui.tag.ok : ui.tag.fail,
47
+ ];
48
+ }),
49
+ ];
50
+
51
+ ui.table(rows, { header: true });
52
+ ui.blank();
53
+ ui.separator();
54
+
55
+ if (brokenCount === 0) {
56
+ ui.success(`All ${entries.length} shortcut${entries.length === 1 ? '' : 's'} valid.`);
57
+ } else if (options.prune) {
58
+ const { kept, removed } = pruneBroken(links);
59
+ saveLinks(kept);
60
+ ui.warn(`Pruned ${removed.length} broken shortcut${removed.length === 1 ? '' : 's'}.`);
61
+ if (removed.length) ui.dim(`Removed: ${removed.join(', ')}`);
62
+ } else {
63
+ ui.warn(
64
+ `${brokenCount} broken / ${entries.length} total.`
65
+ );
66
+ ui.dim(`Use "upex check --prune" to remove them, or "upex edit <name> <path>" to fix.`);
67
+ }
68
+ ui.blank();
69
+ });
70
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * cmd.js -- Open a shortcut in N terminal windows, optionally running a command.
3
+ * Usage: upex cmd <name> [count] [start command...]
4
+ * upex cmd velora -- one window at velora
5
+ * upex cmd velora 2 -- two windows
6
+ * upex cmd velora claude -- one window running "claude"
7
+ * upex cmd velora 2 dir -- two windows each running "dir"
8
+ */
9
+
10
+ import { handleOpen } from './open.js';
11
+
12
+ export function registerCmd(program) {
13
+ program
14
+ .command('cmd <name> [count] [start...]')
15
+ .description('Open a shortcut in N terminal windows, optionally running a start command')
16
+ .summary('Open N terminals, optionally running a command')
17
+ // Let flags after the shortcut (e.g. "claude --model x") pass through as the start command.
18
+ .enablePositionalOptions()
19
+ .passThroughOptions()
20
+ .allowUnknownOption()
21
+ .action((name, count, start = []) => {
22
+ // A non-numeric "count" is really the first word of the start command.
23
+ if (count && !/^\d+$/.test(count)) [count, start] = [1, [count, ...start]];
24
+ return handleOpen(name, 'cmd', Number(count) || 1, start.join(' '));
25
+ });
26
+ }