@laabroms/alias-cli 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/dist/cli.js +492 -0
  4. package/package.json +56 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lucas Aabroms
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 ADDED
@@ -0,0 +1,173 @@
1
+ # Alias CLI
2
+
3
+ ```
4
+ █████╗ ██╗ ██╗ █████╗ ███████╗
5
+ ██╔══██╗██║ ██║██╔══██╗██╔════╝
6
+ ███████║██║ ██║███████║███████╗
7
+ ██╔══██║██║ ██║██╔══██║╚════██║
8
+ ██║ ██║███████╗██║██║ ██║███████║
9
+ ╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═╝╚══════╝
10
+ ```
11
+
12
+ > Interactive terminal UI for managing shell aliases
13
+
14
+ [![npm version](https://img.shields.io/npm/v/@laabroms/alias-cli.svg)](https://www.npmjs.com/package/@laabroms/alias-cli)
15
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
+
17
+ Built with [Ink](https://github.com/vadimdemedes/ink) — React for CLIs.
18
+
19
+ <!-- ![Demo](demo.gif) -->
20
+
21
+ ## Features
22
+
23
+ - ✨ **Interactive TUI** — keyboard-driven, no mouse needed
24
+ - 📝 **Add/Edit/Delete** aliases with clean modal dialogs
25
+ - 🔍 **Real-time search** — filter aliases as you type with arrow key navigation
26
+ - 🔍 **Live preview** — see your alias before saving
27
+ - 💾 **Auto-backup** — creates `.zshrc.backup` before changes
28
+ - 🎯 **Visual focus** — clearly see which field you're editing
29
+ - 🎨 **Color-coded UI** — easy to scan and navigate
30
+ - 📦 **Zero config** — works with `.zshrc` or `.bashrc` out of the box
31
+
32
+ ## Installation
33
+
34
+ ### Quick Install (bash)
35
+
36
+ ```bash
37
+ curl -fsSL https://raw.githubusercontent.com/laabroms/alias-cli/main/install.sh | bash
38
+ ```
39
+
40
+ ### npm (global)
41
+
42
+ ```bash
43
+ npm install -g @laabroms/alias-cli
44
+ ```
45
+
46
+ ### npx (no install)
47
+
48
+ ```bash
49
+ npx @laabroms/alias-cli
50
+ ```
51
+
52
+ ### From source
53
+
54
+ ```bash
55
+ git clone https://github.com/laabroms/alias-cli.git
56
+ cd alias-cli
57
+ npm install
58
+ npm run dev
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ Run the CLI:
64
+
65
+ ```bash
66
+ alias-cli
67
+ ```
68
+
69
+ ### Keyboard Shortcuts
70
+
71
+ **Main Screen:**
72
+ - `↑/↓` — Navigate aliases
73
+ - `a` — Add new alias
74
+ - `e` — Edit selected alias
75
+ - `d` or `Del` — Delete selected alias
76
+ - `/` — Search/filter aliases
77
+ - `c` — Clear search filter
78
+ - `q` — Quit
79
+
80
+ **Search Mode:**
81
+ - Type to filter aliases in real-time
82
+ - `↑/↓` — Navigate filtered results
83
+ - `Enter` — Edit selected alias
84
+ - `Esc` — Close search
85
+
86
+ **Add/Edit Modal:**
87
+ - `Tab` — Switch between Name and Command fields
88
+ - `Enter` — Save
89
+ - `Esc` — Cancel
90
+
91
+ **Delete Confirmation:**
92
+ - `y` or `Enter` — Confirm delete
93
+ - `n` or `Esc` — Cancel
94
+
95
+ ## Example
96
+
97
+ Create a quick commit alias:
98
+
99
+ 1. Run `alias-cli`
100
+ 2. Press `a` to add
101
+ 3. **Name:** `gc`
102
+ 4. **Command:** `git add . && git commit -m`
103
+ 5. Press `Enter` to save
104
+ 6. Press `q` to quit
105
+ 7. Paste the command (auto-copied to clipboard!) and press Enter
106
+ 8. Use it: `gc "feat: add new feature"`
107
+
108
+ ## How It Works
109
+
110
+ 1. **Loads** aliases from your `.zshrc` or `.bashrc`
111
+ 2. **Displays** them in an interactive list
112
+ 3. **Saves** changes back to your shell config
113
+ 4. **Backups** the original file before writing
114
+
115
+ All aliases are written to the end of your shell config with a comment:
116
+
117
+ ```bash
118
+ # Aliases managed by alias-cli
119
+ alias gs="git status"
120
+ alias gc="git add . && git commit -m"
121
+ alias gp="git push origin main"
122
+ ```
123
+
124
+ After making changes, the CLI automatically copies the `source` command to your clipboard — just paste and run!
125
+
126
+ ## Requirements
127
+
128
+ - Node.js >= 18.0.0
129
+ - Terminal with ANSI color support
130
+
131
+ ## Development
132
+
133
+ ```bash
134
+ # Clone the repo
135
+ git clone https://github.com/laabroms/alias-cli.git
136
+ cd alias-cli
137
+
138
+ # Install dependencies
139
+ npm install
140
+
141
+ # Run in dev mode
142
+ npm run dev
143
+
144
+ # Build for production
145
+ npm run build
146
+
147
+ # Type check
148
+ npm run typecheck
149
+ ```
150
+
151
+ ## Tech Stack
152
+
153
+ - **Ink** — React renderer for CLIs
154
+ - **ink-text-input** — Text input component
155
+ - **TypeScript** — Type safety
156
+ - **tsup** — Fast bundler
157
+ - **tsx** — TypeScript execution
158
+
159
+ ## Future Ideas
160
+
161
+ - [ ] Import/export alias sets
162
+ - [ ] Syntax highlighting for commands
163
+ - [ ] Multi-select delete
164
+ - [ ] Alias categories/tags
165
+ - [ ] Support for `.bash_aliases` and other config files
166
+
167
+ ## Contributing
168
+
169
+ PRs welcome! Please open an issue first to discuss what you'd like to change.
170
+
171
+ ## License
172
+
173
+ MIT © [Lucas Aabroms](https://github.com/laabroms)
package/dist/cli.js ADDED
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/cli.tsx
10
+ import React9 from "react";
11
+ import { render } from "ink";
12
+
13
+ // src/App.tsx
14
+ import React8, { useState as useState4, useEffect, useCallback } from "react";
15
+ import { Box as Box8, Text as Text8, useInput as useInput5, useApp } from "ink";
16
+ import os2 from "os";
17
+
18
+ // src/aliases.ts
19
+ import fs from "fs";
20
+ import path from "path";
21
+ import os from "os";
22
+ var SHELL_CONFIG_FILES = [".zshrc", ".bashrc"];
23
+ function getShellConfigPath() {
24
+ const homeDir = os.homedir();
25
+ for (const file of SHELL_CONFIG_FILES) {
26
+ const fullPath = path.join(homeDir, file);
27
+ if (fs.existsSync(fullPath)) {
28
+ return fullPath;
29
+ }
30
+ }
31
+ return path.join(homeDir, ".zshrc");
32
+ }
33
+ function parseAliases(content) {
34
+ const aliases = [];
35
+ const lines = content.split("\n");
36
+ for (const line of lines) {
37
+ const trimmed = line.trim();
38
+ const match = trimmed.match(/^alias\s+([^=]+)=['"]?(.+?)['"]?$/);
39
+ if (match) {
40
+ const [, name, command] = match;
41
+ aliases.push({
42
+ name: name.trim(),
43
+ command: command.trim().replace(/^['"]|['"]$/g, "")
44
+ });
45
+ }
46
+ }
47
+ return aliases;
48
+ }
49
+ function serializeAliases(aliases) {
50
+ return aliases.map((a) => `alias ${a.name}="${a.command}"`).join("\n");
51
+ }
52
+ function loadAliases() {
53
+ try {
54
+ const configPath = getShellConfigPath();
55
+ const content = fs.readFileSync(configPath, "utf-8");
56
+ return parseAliases(content);
57
+ } catch (error) {
58
+ console.error("Failed to load aliases:", error);
59
+ return [];
60
+ }
61
+ }
62
+ function saveAliases(aliases) {
63
+ try {
64
+ const configPath = getShellConfigPath();
65
+ let content = "";
66
+ if (fs.existsSync(configPath)) {
67
+ content = fs.readFileSync(configPath, "utf-8");
68
+ }
69
+ const lines = content.split("\n");
70
+ const nonAliasLines = lines.filter((line) => {
71
+ const trimmed = line.trim();
72
+ return !trimmed.startsWith("alias ");
73
+ });
74
+ const newContent = [
75
+ ...nonAliasLines,
76
+ "",
77
+ "# Aliases managed by alias-cli",
78
+ serializeAliases(aliases)
79
+ ].join("\n");
80
+ const backupPath = `${configPath}.backup`;
81
+ if (fs.existsSync(configPath)) {
82
+ fs.copyFileSync(configPath, backupPath);
83
+ }
84
+ fs.writeFileSync(configPath, newContent, "utf-8");
85
+ } catch (error) {
86
+ console.error("Failed to save aliases:", error);
87
+ }
88
+ }
89
+
90
+ // src/components/Logo.tsx
91
+ import React from "react";
92
+ import { Box, Text } from "ink";
93
+ function Logo() {
94
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", alignItems: "center", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta" }, "\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557"), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "magenta" }, "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D"), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557"), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551"), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "blue" }, "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551"), /* @__PURE__ */ React.createElement(Text, { bold: true, color: "blue" }, "\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "manage your shell aliases with style"));
95
+ }
96
+
97
+ // src/components/LogoCompact.tsx
98
+ import React2 from "react";
99
+ import { Box as Box2, Text as Text2 } from "ink";
100
+ function LogoCompact() {
101
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", alignItems: "center", marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "magenta" }, "\u2584\u2580\u2588 "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u2588\u2591\u2591 "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "blue" }, "\u2588 "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "magenta" }, "\u2584\u2580\u2588 "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u2584\u2588\u2580")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "magenta" }, "\u2588\u2580\u2588 "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u2588\u2584\u2584 "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "blue" }, "\u2588 "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "magenta" }, "\u2588\u2580\u2588 "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u2591\u2580\u2588")), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "manage your shell aliases"));
102
+ }
103
+
104
+ // src/components/AliasList.tsx
105
+ import React3 from "react";
106
+ import { Box as Box3, Text as Text3 } from "ink";
107
+ function AliasList({ aliases, selectedIndex, isSearchMode }) {
108
+ if (aliases.length === 0) {
109
+ return /* @__PURE__ */ React3.createElement(
110
+ Box3,
111
+ {
112
+ flexDirection: "column",
113
+ alignItems: "center",
114
+ justifyContent: "center",
115
+ paddingY: 3
116
+ },
117
+ /* @__PURE__ */ React3.createElement(Text3, { color: "gray" }, "No aliases found"),
118
+ isSearchMode ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Try a different search term or press [Esc] to go back") : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Press [a] to create your first alias")
119
+ );
120
+ }
121
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, aliases.map((alias, index) => {
122
+ const isSelected = index === selectedIndex;
123
+ return /* @__PURE__ */ React3.createElement(Box3, { key: alias.name, paddingY: 0 }, /* @__PURE__ */ React3.createElement(Box3, { width: 2 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: isSelected ? "magenta" : "gray" }, isSelected ? "\u25B6" : " ")), /* @__PURE__ */ React3.createElement(Box3, { width: 15 }, /* @__PURE__ */ React3.createElement(
124
+ Text3,
125
+ {
126
+ bold: isSelected,
127
+ color: isSelected ? "cyan" : "white"
128
+ },
129
+ alias.name
130
+ )), /* @__PURE__ */ React3.createElement(Box3, { width: 2 }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray" }, "=")), /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(
131
+ Text3,
132
+ {
133
+ color: isSelected ? "green" : "gray",
134
+ wrap: "truncate"
135
+ },
136
+ alias.command
137
+ )));
138
+ }));
139
+ }
140
+
141
+ // src/components/AddAliasModal.tsx
142
+ import React4, { useState } from "react";
143
+ import { Box as Box4, Text as Text4, useInput } from "ink";
144
+ import TextInput from "ink-text-input";
145
+ function AddAliasModal({ onSave, onCancel }) {
146
+ const [name, setName] = useState("");
147
+ const [command, setCommand] = useState("");
148
+ const [focusedField, setFocusedField] = useState("name");
149
+ useInput((input, key) => {
150
+ if (key.tab || key.downArrow) {
151
+ setFocusedField((prev) => prev === "name" ? "command" : "name");
152
+ } else if (key.upArrow) {
153
+ setFocusedField((prev) => prev === "command" ? "name" : "command");
154
+ } else if (key.escape) {
155
+ onCancel();
156
+ } else if (key.return && name && command) {
157
+ onSave(name, command);
158
+ }
159
+ });
160
+ const isFocusedName = focusedField === "name";
161
+ const isFocusedCommand = focusedField === "command";
162
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "green" }, "\u2795 Add New Alias")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 0 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: isFocusedName ? "cyan" : "gray" }, isFocusedName && "\u25B6 ", "Name:")), /* @__PURE__ */ React4.createElement(
163
+ Box4,
164
+ {
165
+ borderStyle: "round",
166
+ borderColor: isFocusedName ? "cyan" : "gray",
167
+ paddingX: 1,
168
+ width: 50
169
+ },
170
+ isFocusedName ? /* @__PURE__ */ React4.createElement(TextInput, { value: name, onChange: setName, placeholder: "e.g., gc" }) : /* @__PURE__ */ React4.createElement(Text4, { color: name ? "white" : "gray" }, name || "e.g., gc")
171
+ )), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 0 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: isFocusedCommand ? "cyan" : "gray" }, isFocusedCommand && "\u25B6 ", "Command:")), /* @__PURE__ */ React4.createElement(
172
+ Box4,
173
+ {
174
+ borderStyle: "round",
175
+ borderColor: isFocusedCommand ? "cyan" : "gray",
176
+ paddingX: 1,
177
+ width: 50
178
+ },
179
+ isFocusedCommand ? /* @__PURE__ */ React4.createElement(
180
+ TextInput,
181
+ {
182
+ value: command,
183
+ onChange: setCommand,
184
+ placeholder: "e.g., git add . && git commit -m"
185
+ }
186
+ ) : /* @__PURE__ */ React4.createElement(Text4, { color: command ? "white" : "gray" }, command || "e.g., git add . && git commit -m")
187
+ )), name && command && /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Preview:"), /* @__PURE__ */ React4.createElement(Box4, { paddingX: 2 }, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, name), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, " = "), /* @__PURE__ */ React4.createElement(Text4, { color: "green" }, '"', command, '"'))), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1, justifyContent: "center", gap: 2 }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "cyan" }, "[\u2191/\u2193]"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " switch")), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "green" }, "[Enter]"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " save")), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "gray" }, "[Esc]"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " cancel"))));
188
+ }
189
+
190
+ // src/components/EditAliasModal.tsx
191
+ import React5, { useState as useState2 } from "react";
192
+ import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
193
+ import TextInput2 from "ink-text-input";
194
+ function EditAliasModal({ alias, onSave, onCancel }) {
195
+ const [name, setName] = useState2(alias.name);
196
+ const [command, setCommand] = useState2(alias.command);
197
+ const [focusedField, setFocusedField] = useState2("name");
198
+ useInput2((input, key) => {
199
+ if (key.tab || key.downArrow) {
200
+ setFocusedField((prev) => prev === "name" ? "command" : "name");
201
+ } else if (key.upArrow) {
202
+ setFocusedField((prev) => prev === "command" ? "name" : "command");
203
+ } else if (key.escape) {
204
+ onCancel();
205
+ } else if (key.return && name && command) {
206
+ onSave(name, command);
207
+ }
208
+ });
209
+ const isFocusedName = focusedField === "name";
210
+ const isFocusedCommand = focusedField === "command";
211
+ return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React5.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "blue" }, "\u270F\uFE0F Edit Alias")), /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Box5, { marginBottom: 0 }, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: isFocusedName ? "cyan" : "gray" }, isFocusedName && "\u25B6 ", "Name:")), /* @__PURE__ */ React5.createElement(
212
+ Box5,
213
+ {
214
+ borderStyle: "round",
215
+ borderColor: isFocusedName ? "cyan" : "gray",
216
+ paddingX: 1,
217
+ width: 50
218
+ },
219
+ isFocusedName ? /* @__PURE__ */ React5.createElement(TextInput2, { value: name, onChange: setName }) : /* @__PURE__ */ React5.createElement(Text5, { color: "white" }, name)
220
+ )), /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Box5, { marginBottom: 0 }, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: isFocusedCommand ? "cyan" : "gray" }, isFocusedCommand && "\u25B6 ", "Command:")), /* @__PURE__ */ React5.createElement(
221
+ Box5,
222
+ {
223
+ borderStyle: "round",
224
+ borderColor: isFocusedCommand ? "cyan" : "gray",
225
+ paddingX: 1,
226
+ width: 50
227
+ },
228
+ isFocusedCommand ? /* @__PURE__ */ React5.createElement(TextInput2, { value: command, onChange: setCommand }) : /* @__PURE__ */ React5.createElement(Text5, { color: "white" }, command)
229
+ )), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "Preview:"), /* @__PURE__ */ React5.createElement(Box5, { paddingX: 2 }, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, name), /* @__PURE__ */ React5.createElement(Text5, { color: "gray" }, " = "), /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, '"', command, '"'))), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, justifyContent: "center", gap: 2 }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "cyan" }, "[\u2191/\u2193]"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " switch")), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "green" }, "[Enter]"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " save")), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "gray" }, "[Esc]"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " cancel"))));
230
+ }
231
+
232
+ // src/components/DeleteConfirmModal.tsx
233
+ import React6 from "react";
234
+ import { Box as Box6, Text as Text6, useInput as useInput3 } from "ink";
235
+ function DeleteConfirmModal({
236
+ alias,
237
+ onConfirm,
238
+ onCancel
239
+ }) {
240
+ useInput3((input, key) => {
241
+ if (input === "y" || key.return) {
242
+ onConfirm();
243
+ } else if (input === "n" || key.escape) {
244
+ onCancel();
245
+ }
246
+ });
247
+ return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", gap: 1, alignItems: "center" }, /* @__PURE__ */ React6.createElement(Box6, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "red" }, "\u26A0\uFE0F Delete Alias")), /* @__PURE__ */ React6.createElement(
248
+ Box6,
249
+ {
250
+ borderStyle: "round",
251
+ borderColor: "red",
252
+ paddingX: 2,
253
+ paddingY: 1,
254
+ flexDirection: "column",
255
+ alignItems: "center"
256
+ },
257
+ /* @__PURE__ */ React6.createElement(Text6, null, "Are you sure you want to delete", " ", /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, alias.name), "?"),
258
+ /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, flexDirection: "column", alignItems: "center" }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Command:"), /* @__PURE__ */ React6.createElement(Text6, { color: "gray" }, '"', alias.command, '"'))
259
+ ), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true, italic: true }, "This action cannot be undone")), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "red" }, "[y]"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " yes, delete")), /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "green" }, "[n]"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " no, cancel")), /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "gray" }, "[Esc]"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " cancel"))));
260
+ }
261
+
262
+ // src/components/SearchModal.tsx
263
+ import React7, { useState as useState3 } from "react";
264
+ import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
265
+ import TextInput3 from "ink-text-input";
266
+ function SearchModal({ onSearch, onCancel, matchCount, onNavigate, onSelect }) {
267
+ const [query, setQuery] = useState3("");
268
+ const handleChange = (value) => {
269
+ setQuery(value);
270
+ onSearch(value);
271
+ };
272
+ useInput4((input, key) => {
273
+ if (key.escape) {
274
+ onCancel();
275
+ } else if (key.return) {
276
+ if (matchCount && matchCount > 0 && onSelect) {
277
+ onSelect();
278
+ } else {
279
+ onCancel();
280
+ }
281
+ } else if (key.upArrow && onNavigate) {
282
+ onNavigate("up");
283
+ } else if (key.downArrow && onNavigate) {
284
+ onNavigate("down");
285
+ }
286
+ });
287
+ return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React7.createElement(Box7, { marginBottom: 1 }, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "cyan" }, "\u{1F50D} Search Aliases")), /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Box7, { marginBottom: 0 }, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "cyan" }, "Search:")), /* @__PURE__ */ React7.createElement(
288
+ Box7,
289
+ {
290
+ borderStyle: "round",
291
+ borderColor: "cyan",
292
+ paddingX: 1,
293
+ width: 50
294
+ },
295
+ /* @__PURE__ */ React7.createElement(
296
+ TextInput3,
297
+ {
298
+ value: query,
299
+ onChange: handleChange,
300
+ placeholder: "Type to filter aliases..."
301
+ }
302
+ )
303
+ )), query && matchCount !== void 0 && /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, justifyContent: "center" }, /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, matchCount), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, " ", matchCount === 1 ? "match" : "matches")), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, justifyContent: "center", gap: 2 }, /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "green" }, "[Enter]"), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, " select")), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "\u2022"), /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "yellow" }, "[Esc]"), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, " close")), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "\u2022"), /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "cyan" }, "[\u2191/\u2193]"), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, " navigate")), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "\u2022"), /* @__PURE__ */ React7.createElement(Text7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "gray" }, "[q]"), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, " quit"))));
304
+ }
305
+
306
+ // src/App.tsx
307
+ function App() {
308
+ const { exit } = useApp();
309
+ const [aliases, setAliases] = useState4([]);
310
+ const [selectedIndex, setSelectedIndex] = useState4(0);
311
+ const [mode, setMode] = useState4("list");
312
+ const [searchQuery, setSearchQuery] = useState4("");
313
+ const [hasChanges, setHasChanges] = useState4(false);
314
+ useEffect(() => {
315
+ const loaded = loadAliases();
316
+ setAliases(loaded);
317
+ }, []);
318
+ const filteredAliases = searchQuery ? aliases.filter(
319
+ (a) => a.name.toLowerCase().includes(searchQuery.toLowerCase()) || a.command.toLowerCase().includes(searchQuery.toLowerCase())
320
+ ) : aliases;
321
+ useInput5((input, key) => {
322
+ if (mode !== "list") return;
323
+ if (key.upArrow) {
324
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
325
+ } else if (key.downArrow) {
326
+ setSelectedIndex(
327
+ (prev) => Math.min(filteredAliases.length - 1, prev + 1)
328
+ );
329
+ } else if (input === "a") {
330
+ setMode("add");
331
+ } else if (input === "e" && filteredAliases.length > 0) {
332
+ setMode("edit");
333
+ } else if ((input === "d" || key.delete) && filteredAliases.length > 0) {
334
+ setMode("delete");
335
+ } else if (input === "/") {
336
+ setMode("search");
337
+ } else if (input === "c" && searchQuery) {
338
+ handleClearSearch();
339
+ } else if (input === "q") {
340
+ if (hasChanges) {
341
+ const configPath = getShellConfigPath();
342
+ const fileName = configPath.replace(os2.homedir(), "~");
343
+ const sourceCommand = `source ${fileName}`;
344
+ exit();
345
+ console.log("\n\x1B[32m\u2728 Changes saved!\x1B[0m\n");
346
+ console.log("\x1B[33m\u{1F4CB} To apply your aliases, run:\x1B[0m");
347
+ console.log(` \x1B[36;1m${sourceCommand}\x1B[0m
348
+ `);
349
+ try {
350
+ const { execSync } = __require("child_process");
351
+ try {
352
+ execSync(`echo '${sourceCommand}' | pbcopy`, { stdio: "ignore" });
353
+ console.log("\x1B[2m\u2713 Copied to clipboard! Just paste and run.\x1B[0m\n");
354
+ } catch {
355
+ execSync(`echo '${sourceCommand}' | xclip -selection clipboard`, { stdio: "ignore" });
356
+ console.log("\x1B[2m\u2713 Copied to clipboard! Just paste and run.\x1B[0m\n");
357
+ }
358
+ } catch {
359
+ }
360
+ console.log("\x1B[33m\u26A1 Want auto-reload on quit?\x1B[0m");
361
+ console.log(`\x1B[2m Add this wrapper to your ${fileName}:\x1B[0m`);
362
+ console.log(`\x1B[2m alias-cli-reload() { command alias-cli && [ -f ~/.alias-cli-reload ] && source "$(cat ~/.alias-cli-reload)" && rm ~/.alias-cli-reload; }\x1B[0m`);
363
+ console.log(`\x1B[2m alias alias-cli='alias-cli-reload'\x1B[0m`);
364
+ console.log(`\x1B[2m Then run: source ${fileName}\x1B[0m
365
+ `);
366
+ } else {
367
+ exit();
368
+ }
369
+ }
370
+ });
371
+ const handleAdd = useCallback(
372
+ (name, command) => {
373
+ const newAlias = { name, command };
374
+ const updated = [...aliases, newAlias];
375
+ setAliases(updated);
376
+ saveAliases(updated);
377
+ setHasChanges(true);
378
+ setMode("list");
379
+ },
380
+ [aliases, setAliases, saveAliases, setHasChanges, setMode]
381
+ );
382
+ const handleEdit = useCallback(
383
+ (name, command) => {
384
+ const selected = filteredAliases[selectedIndex];
385
+ const updated = aliases.map(
386
+ (a) => a.name === selected.name ? { name, command } : a
387
+ );
388
+ setAliases(updated);
389
+ saveAliases(updated);
390
+ setHasChanges(true);
391
+ setMode("list");
392
+ },
393
+ [aliases, selectedIndex, setAliases, saveAliases, setHasChanges, setMode]
394
+ );
395
+ const handleDelete = useCallback(() => {
396
+ const selected = filteredAliases[selectedIndex];
397
+ const updated = aliases.filter((a) => a.name !== selected.name);
398
+ setAliases(updated);
399
+ saveAliases(updated);
400
+ setHasChanges(true);
401
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
402
+ setMode("list");
403
+ }, [aliases, selectedIndex, setAliases, saveAliases, setHasChanges, setMode]);
404
+ const handleCancel = useCallback(() => {
405
+ setMode("list");
406
+ }, [setMode]);
407
+ const handleSearch = useCallback(
408
+ (query) => {
409
+ setSearchQuery(query);
410
+ setSelectedIndex(0);
411
+ },
412
+ [setSearchQuery, setSelectedIndex]
413
+ );
414
+ const handleClearSearch = useCallback(() => {
415
+ setSearchQuery("");
416
+ setMode("list");
417
+ }, [setSearchQuery, setMode]);
418
+ const handleNavigate = useCallback(
419
+ (direction) => {
420
+ if (direction === "up") {
421
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
422
+ } else {
423
+ setSelectedIndex((prev) => Math.min(filteredAliases.length - 1, prev + 1));
424
+ }
425
+ },
426
+ [filteredAliases.length]
427
+ );
428
+ const handleSelectFromSearch = useCallback(() => {
429
+ if (filteredAliases.length > 0) {
430
+ setMode("edit");
431
+ }
432
+ }, [filteredAliases.length]);
433
+ return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1 }, mode === "list" ? /* @__PURE__ */ React8.createElement(Logo, null) : /* @__PURE__ */ React8.createElement(LogoCompact, null), /* @__PURE__ */ React8.createElement(Box8, { marginBottom: 1 }, /* @__PURE__ */ React8.createElement(Box8, { gap: 1 }, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "magenta" }, "\u26A1"), /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "white" }, "Alias Manager"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "(", aliases.length, " aliases)"))), /* @__PURE__ */ React8.createElement(Box8, { marginBottom: 1 }, /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "\u2500".repeat(80))), mode === "search" && /* @__PURE__ */ React8.createElement(Box8, { marginBottom: 1 }, /* @__PURE__ */ React8.createElement(
434
+ SearchModal,
435
+ {
436
+ onSearch: handleSearch,
437
+ onCancel: handleCancel,
438
+ matchCount: filteredAliases.length,
439
+ onNavigate: handleNavigate,
440
+ onSelect: handleSelectFromSearch
441
+ }
442
+ )), /* @__PURE__ */ React8.createElement(
443
+ Box8,
444
+ {
445
+ borderStyle: "round",
446
+ borderColor: "gray",
447
+ flexDirection: "column",
448
+ paddingX: 2,
449
+ paddingY: 1,
450
+ minHeight: 12
451
+ },
452
+ (() => {
453
+ switch (mode) {
454
+ case "list":
455
+ case "search":
456
+ return /* @__PURE__ */ React8.createElement(
457
+ AliasList,
458
+ {
459
+ aliases: filteredAliases,
460
+ selectedIndex,
461
+ isSearchMode: mode === "search"
462
+ }
463
+ );
464
+ case "add":
465
+ return /* @__PURE__ */ React8.createElement(AddAliasModal, { onSave: handleAdd, onCancel: handleCancel });
466
+ case "edit":
467
+ return filteredAliases[selectedIndex] ? /* @__PURE__ */ React8.createElement(
468
+ EditAliasModal,
469
+ {
470
+ alias: filteredAliases[selectedIndex],
471
+ onSave: handleEdit,
472
+ onCancel: handleCancel
473
+ }
474
+ ) : null;
475
+ case "delete":
476
+ return filteredAliases[selectedIndex] ? /* @__PURE__ */ React8.createElement(
477
+ DeleteConfirmModal,
478
+ {
479
+ alias: filteredAliases[selectedIndex],
480
+ onConfirm: handleDelete,
481
+ onCancel: handleCancel
482
+ }
483
+ ) : null;
484
+ default:
485
+ return null;
486
+ }
487
+ })()
488
+ ), mode === "list" && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "\u2500".repeat(80))), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1, justifyContent: "center", gap: 3 }, /* @__PURE__ */ React8.createElement(Text8, null, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "green" }, "[a]"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, " add")), /* @__PURE__ */ React8.createElement(Text8, null, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "blue" }, "[e]"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, " edit")), /* @__PURE__ */ React8.createElement(Text8, null, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "red" }, "[d/Del]"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, " delete")), /* @__PURE__ */ React8.createElement(Text8, null, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "cyan" }, "[/]"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, " search")), searchQuery && /* @__PURE__ */ React8.createElement(Text8, null, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "yellow" }, "[c]"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, " clear")), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "\u2022"), /* @__PURE__ */ React8.createElement(Text8, null, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "gray" }, "[\u2191/\u2193]"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, " navigate")), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "\u2022"), /* @__PURE__ */ React8.createElement(Text8, null, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "gray" }, "[q]"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, " quit"))), searchQuery && /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1, justifyContent: "center" }, /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Filtering: "), /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, '"', searchQuery, '"'), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, " (", filteredAliases.length, " matches)"))));
489
+ }
490
+
491
+ // src/cli.tsx
492
+ render(/* @__PURE__ */ React9.createElement(App, null));
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@laabroms/alias-cli",
3
+ "version": "0.1.0",
4
+ "description": "Interactive TUI for managing shell aliases",
5
+ "type": "module",
6
+ "main": "dist/cli.js",
7
+ "bin": {
8
+ "alias-cli": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsx src/cli.tsx",
16
+ "prepublishOnly": "npm run build",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "keywords": [
20
+ "alias",
21
+ "cli",
22
+ "tui",
23
+ "shell",
24
+ "zsh",
25
+ "bash",
26
+ "terminal",
27
+ "interactive"
28
+ ],
29
+ "author": "Lucas Aabroms",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/laabroms/alias-cli.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/laabroms/alias-cli/issues"
37
+ },
38
+ "homepage": "https://github.com/laabroms/alias-cli#readme",
39
+ "dependencies": {
40
+ "ink": "^6.8.0",
41
+ "ink-select-input": "^6.2.0",
42
+ "ink-text-input": "^6.0.0",
43
+ "react": "^19.2.4"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^25.3.0",
47
+ "@types/react": "^19.2.14",
48
+ "esbuild": "^0.27.3",
49
+ "tsup": "^8.5.1",
50
+ "tsx": "^4.21.0",
51
+ "typescript": "^5.9.3"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ }
56
+ }