@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 +21 -0
- package/README.md +327 -2
- package/bin/upex.js +16 -0
- package/package.json +38 -6
- package/src/commands/add.js +87 -0
- package/src/commands/app.js +58 -0
- package/src/commands/check.js +70 -0
- package/src/commands/cmd.js +26 -0
- package/src/commands/completion.js +75 -0
- package/src/commands/copy.js +51 -0
- package/src/commands/doctor.js +130 -0
- package/src/commands/edit.js +85 -0
- package/src/commands/help.js +45 -0
- package/src/commands/last.js +23 -0
- package/src/commands/list.js +76 -0
- package/src/commands/open.js +195 -0
- package/src/commands/remove.js +35 -0
- package/src/commands/rename.js +47 -0
- package/src/commands/run.js +84 -0
- package/src/commands/setup.js +178 -0
- package/src/commands/tag.js +101 -0
- package/src/commands/transfer.js +75 -0
- package/src/commands/web.js +282 -0
- package/src/commands/workspace.js +135 -0
- package/src/fuzzy.js +77 -0
- package/src/git.js +23 -0
- package/src/index.js +120 -0
- package/src/interactive.js +582 -0
- package/src/launcher.js +508 -0
- package/src/store.js +249 -0
- package/src/ui.js +127 -0
- package/src/web/index.html +766 -0
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
|
-
#
|
|
1
|
+
# ⚡ upex: Personal File Shortcut Manager
|
|
2
2
|
|
|
3
|
-
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|