@fiete/drift 1.0.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.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AddForm
4
+ } from "./chunk-VS6YEHBV.js";
5
+ import "./chunk-QT5M6VMT.js";
6
+ export {
7
+ AddForm
8
+ };
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AddForm,
4
+ useStore
5
+ } from "./chunk-VS6YEHBV.js";
6
+ import "./chunk-QT5M6VMT.js";
7
+
8
+ // src/ui/Browser.tsx
9
+ import { useState as useState2, useCallback as useCallback2 } from "react";
10
+ import { Box as Box6, Text as Text6, useInput as useInput3, useApp } from "ink";
11
+ import clipboard from "clipboardy";
12
+
13
+ // src/hooks/useFuzzySearch.ts
14
+ import { useMemo } from "react";
15
+ import Fuse from "fuse.js";
16
+ var FUSE_OPTIONS = {
17
+ keys: [
18
+ { name: "command", weight: 0.5 },
19
+ { name: "description", weight: 0.3 },
20
+ { name: "tags", weight: 0.2 }
21
+ ],
22
+ threshold: 0.35,
23
+ includeScore: false,
24
+ minMatchCharLength: 1,
25
+ shouldSort: true
26
+ };
27
+ function useFuzzySearch(commands, query) {
28
+ const fuse = useMemo(() => new Fuse(commands, FUSE_OPTIONS), [commands]);
29
+ return useMemo(() => {
30
+ if (!query.trim()) return commands;
31
+ return fuse.search(query).map((r) => r.item);
32
+ }, [fuse, query, commands]);
33
+ }
34
+
35
+ // src/ui/SearchBar.tsx
36
+ import { Box, Text } from "ink";
37
+ import { jsx, jsxs } from "react/jsx-runtime";
38
+ function SearchBar({ query, isSearching, total, filtered }) {
39
+ const borderColor = isSearching ? "blueBright" : "gray";
40
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor, paddingX: 1, children: [
41
+ /* @__PURE__ */ jsx(Text, { color: isSearching ? "blueBright" : "gray", bold: true, children: "/" }),
42
+ /* @__PURE__ */ jsx(Text, { color: isSearching ? "white" : "gray", dimColor: !isSearching, children: query || (isSearching ? "" : " type / to search") }),
43
+ isSearching && /* @__PURE__ */ jsx(Text, { color: "blueBright", children: "\u258C" }),
44
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
45
+ /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
46
+ filtered,
47
+ "/",
48
+ total
49
+ ] })
50
+ ] });
51
+ }
52
+
53
+ // src/ui/CommandItem.tsx
54
+ import { Box as Box2, Text as Text2 } from "ink";
55
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
56
+ function CommandItem({ command, isSelected, dimmed = false }) {
57
+ return /* @__PURE__ */ jsxs2(
58
+ Box2,
59
+ {
60
+ paddingX: 1,
61
+ flexDirection: "row",
62
+ gap: 1,
63
+ backgroundColor: isSelected && !dimmed ? "blueBright" : void 0,
64
+ children: [
65
+ /* @__PURE__ */ jsx2(Text2, { color: isSelected && !dimmed ? "white" : "gray", bold: isSelected && !dimmed, dimColor: dimmed, children: isSelected ? "\u203A" : " " }),
66
+ /* @__PURE__ */ jsx2(Box2, { width: 32, flexShrink: 0, children: /* @__PURE__ */ jsxs2(Text2, { color: isSelected && !dimmed ? "white" : "gray", bold: isSelected && !dimmed, dimColor: dimmed, children: [
67
+ command.description.slice(0, 30),
68
+ command.description.length > 30 ? "\u2026" : ""
69
+ ] }) }),
70
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: /* @__PURE__ */ jsx2(Text2, { color: isSelected && !dimmed ? "white" : "cyan", bold: true, dimColor: dimmed, children: command.command }) }),
71
+ command.directory && /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(Text2, { color: isSelected && !dimmed ? "white" : "yellow", dimColor: dimmed, children: command.directory }) }),
72
+ /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, gap: 1, children: command.tags.map((tag) => /* @__PURE__ */ jsxs2(Text2, { color: isSelected && !dimmed ? "white" : "magenta", dimColor: dimmed, children: [
73
+ "#",
74
+ tag
75
+ ] }, tag)) })
76
+ ]
77
+ }
78
+ );
79
+ }
80
+
81
+ // src/ui/StatusBar.tsx
82
+ import { Box as Box3, Text as Text3 } from "ink";
83
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
84
+ var HINTS = [
85
+ ["\u2191\u2193", "navigate"],
86
+ ["Shift+\u2191\u2193", "reorder"],
87
+ ["/", "search"],
88
+ ["Enter", "execute"],
89
+ ["a", "add"],
90
+ ["e", "edit"],
91
+ ["^E", "copy"],
92
+ ["^D", "delete"],
93
+ ["ESC", "quit"]
94
+ ];
95
+ function StatusBar({ message }) {
96
+ return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "single", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [
97
+ /* @__PURE__ */ jsx3(Box3, { gap: 2, children: HINTS.map(([key, label]) => /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
98
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: key }),
99
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: label })
100
+ ] }, key)) }),
101
+ message && /* @__PURE__ */ jsx3(Text3, { color: "greenBright", bold: true, children: message })
102
+ ] });
103
+ }
104
+
105
+ // src/ui/ConfirmDelete.tsx
106
+ import { Box as Box4, Text as Text4, useInput } from "ink";
107
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
108
+ function ConfirmDelete({ command, onConfirm, onCancel }) {
109
+ useInput((input, key) => {
110
+ if (input === "y" || input === "Y") onConfirm();
111
+ if (input === "n" || input === "N" || key.escape) onCancel();
112
+ });
113
+ return /* @__PURE__ */ jsxs4(
114
+ Box4,
115
+ {
116
+ borderStyle: "round",
117
+ borderColor: "red",
118
+ flexDirection: "column",
119
+ paddingX: 2,
120
+ paddingY: 1,
121
+ marginX: 2,
122
+ marginY: 1,
123
+ children: [
124
+ /* @__PURE__ */ jsx4(Text4, { color: "red", bold: true, children: "Delete command?" }),
125
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: command.command }) }),
126
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, gap: 2, children: [
127
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "red", children: "[y]" }),
128
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "yes, delete it" }),
129
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "white", children: " [n]" }),
130
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "cancel" })
131
+ ] })
132
+ ]
133
+ }
134
+ );
135
+ }
136
+
137
+ // src/ui/EditModal.tsx
138
+ import { useState, useCallback } from "react";
139
+ import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
140
+ import { TextInput } from "@inkjs/ui";
141
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
142
+ var FIELD_ORDER = ["description", "command", "directory", "tags"];
143
+ var LABELS = {
144
+ description: "Description",
145
+ command: "Command",
146
+ directory: "Directory (optional)",
147
+ tags: "Tags (comma-separated)"
148
+ };
149
+ function EditModal({ command, onSave, onCancel }) {
150
+ const [fieldIndex, setFieldIndex] = useState(0);
151
+ const field = FIELD_ORDER[fieldIndex];
152
+ const [formData, setFormData] = useState({
153
+ description: command.description,
154
+ command: command.command,
155
+ directory: command.directory ?? "",
156
+ tags: command.tags.join(", ")
157
+ });
158
+ useInput2((_, key) => {
159
+ if (key.escape) {
160
+ onCancel();
161
+ return;
162
+ }
163
+ if (key.upArrow) {
164
+ setFieldIndex((i) => Math.max(0, i - 1));
165
+ return;
166
+ }
167
+ if (key.downArrow) {
168
+ setFieldIndex((i) => Math.min(FIELD_ORDER.length - 1, i + 1));
169
+ return;
170
+ }
171
+ });
172
+ const handleChange = useCallback((value) => {
173
+ setFormData((d) => ({ ...d, [field]: value }));
174
+ }, [field]);
175
+ const handleSubmit = (value) => {
176
+ const updated = { ...formData, [field]: value };
177
+ setFormData(updated);
178
+ if (fieldIndex < FIELD_ORDER.length - 1) {
179
+ setFieldIndex((i) => i + 1);
180
+ } else {
181
+ onSave({
182
+ command: updated.command.trim(),
183
+ description: updated.description.trim(),
184
+ directory: updated.directory.trim() || void 0,
185
+ tags: updated.tags.split(",").map((t) => t.trim()).filter(Boolean)
186
+ });
187
+ }
188
+ };
189
+ return /* @__PURE__ */ jsxs5(
190
+ Box5,
191
+ {
192
+ borderStyle: "round",
193
+ borderColor: "yellowBright",
194
+ flexDirection: "column",
195
+ paddingX: 2,
196
+ paddingY: 1,
197
+ marginX: 2,
198
+ marginY: 1,
199
+ children: [
200
+ /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, justifyContent: "space-between", children: [
201
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellowBright", children: " Edit Command" }),
202
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "ESC to cancel" })
203
+ ] }),
204
+ FIELD_ORDER.map((f, i) => {
205
+ const isActive = i === fieldIndex;
206
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
207
+ /* @__PURE__ */ jsxs5(Text5, { color: isActive ? "yellowBright" : "gray", children: [
208
+ isActive ? "\u203A" : " ",
209
+ " ",
210
+ LABELS[f]
211
+ ] }),
212
+ /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: isActive ? /* @__PURE__ */ jsx5(
213
+ TextInput,
214
+ {
215
+ defaultValue: formData[f],
216
+ onChange: handleChange,
217
+ onSubmit: handleSubmit
218
+ },
219
+ f
220
+ ) : /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: formData[f] || "(empty)" }) })
221
+ ] }, f);
222
+ }),
223
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "\u2191\u2193 navigate \xB7 Enter to advance \xB7 saves on last field" }) })
224
+ ]
225
+ }
226
+ );
227
+ }
228
+
229
+ // src/ui/Browser.tsx
230
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
231
+ var HEADER_ROWS = 4;
232
+ var FOOTER_ROWS = 3;
233
+ function Browser({ onExecute }) {
234
+ const { exit } = useApp();
235
+ const { commands, updateCommand, moveCommand, removeCommand } = useStore();
236
+ const [query, setQuery] = useState2("");
237
+ const [isSearching, setIsSearching] = useState2(false);
238
+ const [selectedIndex, setSelectedIndex] = useState2(0);
239
+ const [viewportStart, setViewportStart] = useState2(0);
240
+ const [screen, setScreen] = useState2("browser");
241
+ const [statusMessage, setStatusMessage] = useState2("");
242
+ const filtered = useFuzzySearch(commands, query);
243
+ const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
244
+ const selected = filtered[clampedIndex] ?? null;
245
+ const visibleRows = Math.max(1, (process.stdout.rows ?? 24) - HEADER_ROWS - FOOTER_ROWS);
246
+ const visibleCommands = filtered.slice(viewportStart, viewportStart + visibleRows);
247
+ const flash = useCallback2((msg) => {
248
+ setStatusMessage(msg);
249
+ setTimeout(() => setStatusMessage(""), 2e3);
250
+ }, []);
251
+ const navigate = useCallback2(
252
+ (direction) => {
253
+ setSelectedIndex((prev) => {
254
+ const next = Math.max(0, Math.min(filtered.length - 1, prev + direction));
255
+ setViewportStart((vs) => {
256
+ if (next >= vs + visibleRows) return next - visibleRows + 1;
257
+ if (next < vs) return next;
258
+ return vs;
259
+ });
260
+ return next;
261
+ });
262
+ },
263
+ [filtered.length, visibleRows]
264
+ );
265
+ useInput3(
266
+ (input, key) => {
267
+ if (screen === "confirm-delete") return;
268
+ if (isSearching) {
269
+ if (key.escape) {
270
+ setQuery("");
271
+ setIsSearching(false);
272
+ setSelectedIndex(0);
273
+ setViewportStart(0);
274
+ return;
275
+ }
276
+ if (key.return) {
277
+ setIsSearching(false);
278
+ return;
279
+ }
280
+ if (key.upArrow) {
281
+ navigate(-1);
282
+ return;
283
+ }
284
+ if (key.downArrow) {
285
+ navigate(1);
286
+ return;
287
+ }
288
+ if (!key.ctrl && !key.meta) {
289
+ if (key.backspace || key.delete) {
290
+ setQuery((q) => {
291
+ const next = q.slice(0, -1);
292
+ if (next === "") setIsSearching(false);
293
+ return next;
294
+ });
295
+ } else if (input && input.length === 1) {
296
+ setQuery((q) => q + input);
297
+ }
298
+ setSelectedIndex(0);
299
+ setViewportStart(0);
300
+ }
301
+ return;
302
+ }
303
+ if (key.upArrow && key.shift && selected && !query) {
304
+ moveCommand(selected.id, -1);
305
+ navigate(-1);
306
+ return;
307
+ }
308
+ if (key.downArrow && key.shift && selected && !query) {
309
+ moveCommand(selected.id, 1);
310
+ navigate(1);
311
+ return;
312
+ }
313
+ if (key.upArrow) {
314
+ navigate(-1);
315
+ return;
316
+ }
317
+ if (key.downArrow) {
318
+ navigate(1);
319
+ return;
320
+ }
321
+ if (input === "/") {
322
+ setIsSearching(true);
323
+ return;
324
+ }
325
+ if (key.escape) {
326
+ if (query) {
327
+ setQuery("");
328
+ setSelectedIndex(0);
329
+ setViewportStart(0);
330
+ } else exit();
331
+ return;
332
+ }
333
+ if (key.return && selected) {
334
+ const full = selected.directory ? `cd ${selected.directory} && ${selected.command}` : selected.command;
335
+ onExecute(full);
336
+ exit();
337
+ return;
338
+ }
339
+ if (key.ctrl && input === "e" && selected) {
340
+ const full = selected.directory ? `cd ${selected.directory} && ${selected.command}` : selected.command;
341
+ clipboard.writeSync(full);
342
+ flash("Copied!");
343
+ return;
344
+ }
345
+ if (key.ctrl && input === "d" && selected) {
346
+ setScreen("confirm-delete");
347
+ return;
348
+ }
349
+ if (input === "a") {
350
+ setScreen("add");
351
+ return;
352
+ }
353
+ if (input === "e" && selected) {
354
+ setScreen("edit");
355
+ return;
356
+ }
357
+ },
358
+ { isActive: screen === "browser" }
359
+ );
360
+ if (screen === "add") {
361
+ return /* @__PURE__ */ jsx6(
362
+ AddForm,
363
+ {
364
+ onSave: () => {
365
+ setScreen("browser");
366
+ flash("Saved!");
367
+ },
368
+ onCancel: () => setScreen("browser")
369
+ }
370
+ );
371
+ }
372
+ const isOverlay = screen === "confirm-delete" || screen === "edit";
373
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: process.stdout.rows ?? 24, children: [
374
+ /* @__PURE__ */ jsx6(SearchBar, { query, isSearching, total: commands.length, filtered: filtered.length }),
375
+ /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", flexGrow: 1, children: filtered.length === 0 ? /* @__PURE__ */ jsx6(Box6, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: commands.length === 0 ? "No commands saved yet. Press `a` to add your first command." : "No matches. Keep typing or clear the search." }) }) : visibleCommands.map((cmd, i) => /* @__PURE__ */ jsx6(
376
+ CommandItem,
377
+ {
378
+ command: cmd,
379
+ isSelected: viewportStart + i === clampedIndex,
380
+ dimmed: isOverlay
381
+ },
382
+ cmd.id
383
+ )) }),
384
+ screen === "edit" && selected && /* @__PURE__ */ jsx6(
385
+ EditModal,
386
+ {
387
+ command: selected,
388
+ onSave: (patch) => {
389
+ updateCommand(selected.id, patch);
390
+ setScreen("browser");
391
+ flash("Updated.");
392
+ },
393
+ onCancel: () => setScreen("browser")
394
+ }
395
+ ),
396
+ screen === "confirm-delete" && selected && /* @__PURE__ */ jsx6(
397
+ ConfirmDelete,
398
+ {
399
+ command: selected,
400
+ onConfirm: () => {
401
+ removeCommand(selected.id);
402
+ setScreen("browser");
403
+ setSelectedIndex((i) => Math.max(0, i - 1));
404
+ flash("Deleted.");
405
+ },
406
+ onCancel: () => setScreen("browser")
407
+ }
408
+ ),
409
+ /* @__PURE__ */ jsx6(StatusBar, { message: statusMessage })
410
+ ] });
411
+ }
412
+ export {
413
+ Browser
414
+ };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ FileStore
4
+ } from "./chunk-QT5M6VMT.js";
5
+ export {
6
+ FileStore
7
+ };
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/store/FileStore.ts
4
+ import path from "path";
5
+ import os from "os";
6
+ import fs from "fs";
7
+ import { nanoid } from "nanoid";
8
+ var STORE_DIR = path.join(os.homedir(), ".config", "cmdvault");
9
+ var STORE_FILE = path.join(STORE_DIR, "commands.json");
10
+ var CURRENT_VERSION = 1;
11
+ var FileStore = class {
12
+ data;
13
+ constructor() {
14
+ this.data = this.load();
15
+ }
16
+ load() {
17
+ if (!fs.existsSync(STORE_FILE)) {
18
+ fs.mkdirSync(STORE_DIR, { recursive: true });
19
+ const initial = { version: CURRENT_VERSION, commands: [] };
20
+ fs.writeFileSync(STORE_FILE, JSON.stringify(initial, null, 2));
21
+ return initial;
22
+ }
23
+ const raw = fs.readFileSync(STORE_FILE, "utf8");
24
+ return JSON.parse(raw);
25
+ }
26
+ save(data) {
27
+ fs.writeFileSync(STORE_FILE, JSON.stringify(data, null, 2));
28
+ this.data = data;
29
+ }
30
+ getAll() {
31
+ return this.data.commands;
32
+ }
33
+ addCommand(input) {
34
+ const now = (/* @__PURE__ */ new Date()).toISOString();
35
+ const cmd = { ...input, id: nanoid(), createdAt: now, updatedAt: now };
36
+ this.data.commands.push(cmd);
37
+ this.save(this.data);
38
+ return cmd;
39
+ }
40
+ updateCommand(id, patch) {
41
+ const idx = this.data.commands.findIndex((c) => c.id === id);
42
+ if (idx === -1) return false;
43
+ this.data.commands[idx] = { ...this.data.commands[idx], ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
44
+ this.save(this.data);
45
+ return true;
46
+ }
47
+ moveCommand(id, direction) {
48
+ const idx = this.data.commands.findIndex((c) => c.id === id);
49
+ const target = idx + direction;
50
+ if (idx === -1 || target < 0 || target >= this.data.commands.length) return false;
51
+ [this.data.commands[idx], this.data.commands[target]] = [this.data.commands[target], this.data.commands[idx]];
52
+ this.save(this.data);
53
+ return true;
54
+ }
55
+ removeCommand(id) {
56
+ const before = this.data.commands.length;
57
+ this.data.commands = this.data.commands.filter((c) => c.id !== id);
58
+ if (this.data.commands.length < before) {
59
+ this.save(this.data);
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ };
65
+
66
+ export {
67
+ FileStore
68
+ };
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ FileStore
4
+ } from "./chunk-QT5M6VMT.js";
5
+
6
+ // src/ui/AddForm.tsx
7
+ import { useState as useState2, useCallback as useCallback2 } from "react";
8
+ import { Box, Text, useApp, useInput } from "ink";
9
+ import { TextInput } from "@inkjs/ui";
10
+
11
+ // src/hooks/useStore.ts
12
+ import { useState, useCallback } from "react";
13
+ var store = new FileStore();
14
+ function useStore() {
15
+ const [commands, setCommands] = useState(() => store.getAll());
16
+ const refresh = useCallback(() => {
17
+ setCommands([...store.getAll()]);
18
+ }, []);
19
+ const addCommand = useCallback(
20
+ (input) => {
21
+ store.addCommand(input);
22
+ refresh();
23
+ },
24
+ [refresh]
25
+ );
26
+ const updateCommand = useCallback(
27
+ (id, patch) => {
28
+ store.updateCommand(id, patch);
29
+ refresh();
30
+ },
31
+ [refresh]
32
+ );
33
+ const moveCommand = useCallback(
34
+ (id, direction) => {
35
+ store.moveCommand(id, direction);
36
+ refresh();
37
+ },
38
+ [refresh]
39
+ );
40
+ const removeCommand = useCallback(
41
+ (id) => {
42
+ store.removeCommand(id);
43
+ refresh();
44
+ },
45
+ [refresh]
46
+ );
47
+ return { commands, addCommand, updateCommand, moveCommand, removeCommand };
48
+ }
49
+
50
+ // src/ui/AddForm.tsx
51
+ import { jsx, jsxs } from "react/jsx-runtime";
52
+ var FIELD_ORDER = ["description", "command", "directory", "tags"];
53
+ var LABELS = {
54
+ description: "Description",
55
+ command: "Command",
56
+ directory: "Directory (optional)",
57
+ tags: "Tags (comma-separated)"
58
+ };
59
+ var PLACEHOLDERS = {
60
+ description: "e.g. Show last 20 commits",
61
+ command: "e.g. git log --oneline -20",
62
+ directory: "e.g. /Users/me/project (leave empty to skip)",
63
+ tags: "e.g. git, log"
64
+ };
65
+ function AddForm({ initialValues, onSave, onCancel }) {
66
+ const { exit } = useApp();
67
+ const { addCommand } = useStore();
68
+ const getStartIndex = () => {
69
+ if (initialValues?.command && !initialValues.description) return 0;
70
+ if (initialValues?.command && initialValues.description && !initialValues.tags) return 2;
71
+ return 0;
72
+ };
73
+ const [fieldIndex, setFieldIndex] = useState2(getStartIndex);
74
+ const field = FIELD_ORDER[fieldIndex];
75
+ const [formData, setFormData] = useState2({
76
+ description: initialValues?.description ?? "",
77
+ command: initialValues?.command ?? "",
78
+ directory: "",
79
+ tags: initialValues?.tags ?? ""
80
+ });
81
+ const cancel = () => {
82
+ onCancel ? onCancel() : exit();
83
+ };
84
+ useInput((_, key) => {
85
+ if (key.escape) {
86
+ cancel();
87
+ return;
88
+ }
89
+ if (key.upArrow) {
90
+ setFieldIndex((i) => Math.max(0, i - 1));
91
+ return;
92
+ }
93
+ if (key.downArrow) {
94
+ setFieldIndex((i) => Math.min(FIELD_ORDER.length - 1, i + 1));
95
+ return;
96
+ }
97
+ });
98
+ const handleChange = useCallback2((value) => {
99
+ setFormData((d) => ({ ...d, [field]: value }));
100
+ }, [field]);
101
+ const handleSubmit = (value) => {
102
+ const updated = { ...formData, [field]: value };
103
+ setFormData(updated);
104
+ if (fieldIndex < FIELD_ORDER.length - 1) {
105
+ setFieldIndex((i) => i + 1);
106
+ } else {
107
+ if (!updated.command.trim()) {
108
+ cancel();
109
+ return;
110
+ }
111
+ addCommand({
112
+ command: updated.command.trim(),
113
+ description: updated.description.trim(),
114
+ directory: updated.directory.trim() || void 0,
115
+ tags: updated.tags.split(",").map((t) => t.trim()).filter(Boolean)
116
+ });
117
+ if (onSave) {
118
+ onSave();
119
+ } else {
120
+ console.log("\nSaved!");
121
+ exit();
122
+ }
123
+ }
124
+ };
125
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, gap: 1, children: [
126
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "blueBright", children: " Add New Command" }) }),
127
+ FIELD_ORDER.map((f, i) => {
128
+ const isActive = i === fieldIndex;
129
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
130
+ /* @__PURE__ */ jsxs(Text, { color: isActive ? "yellowBright" : "gray", children: [
131
+ isActive ? "\u203A" : " ",
132
+ " ",
133
+ LABELS[f]
134
+ ] }),
135
+ /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: isActive ? /* @__PURE__ */ jsx(
136
+ TextInput,
137
+ {
138
+ placeholder: PLACEHOLDERS[f],
139
+ defaultValue: formData[f],
140
+ onChange: handleChange,
141
+ onSubmit: handleSubmit
142
+ },
143
+ f
144
+ ) : /* @__PURE__ */ jsx(Text, { color: formData[f] ? "white" : "gray", dimColor: !formData[f], children: formData[f] || PLACEHOLDERS[f] }) })
145
+ ] }, f);
146
+ }),
147
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\u2191\u2193 navigate \xB7 Enter to advance \xB7 ESC to cancel" }) })
148
+ ] });
149
+ }
150
+
151
+ export {
152
+ useStore,
153
+ AddForm
154
+ };
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { program } from "commander";
5
+ import { render } from "ink";
6
+ import React from "react";
7
+ import { execa } from "execa";
8
+ program.name("drift").description("cmdvault \u2014 A TUI command vault for your shell").version("1.0.0");
9
+ program.command("list", { isDefault: true }).description("Browse saved commands (default)").action(async () => {
10
+ const { Browser } = await import("./Browser-WWFMCIAC.js");
11
+ let commandToRun;
12
+ const { waitUntilExit } = render(
13
+ React.createElement(Browser, { onExecute: (cmd) => {
14
+ commandToRun = cmd;
15
+ } })
16
+ );
17
+ await waitUntilExit();
18
+ process.stdout.write("\x1Bc");
19
+ if (commandToRun) {
20
+ await execa(commandToRun, { shell: true, stdio: "inherit" }).catch(() => {
21
+ });
22
+ }
23
+ process.exit(0);
24
+ });
25
+ program.command("add [command]").description("Add a command. Pass it as a quoted argument, or open the interactive form.").option("-d, --desc <description>", "Description for the command").option("-t, --tags <tags>", "Comma-separated tags").addHelpText("after", '\nExamples:\n drift add "git log --oneline -20"\n drift add "cd /project && npm run dev" --desc "Start dev server" --tags "node,dev"\n drift add (opens interactive form)').action(async (command, opts) => {
26
+ if (command && opts.desc !== void 0 && opts.tags !== void 0) {
27
+ const { FileStore } = await import("./FileStore-W4YQ3VAM.js");
28
+ const store = new FileStore();
29
+ store.addCommand({
30
+ command: command.trim(),
31
+ description: opts.desc.trim(),
32
+ tags: opts.tags.split(",").map((t) => t.trim()).filter(Boolean)
33
+ });
34
+ console.log("Saved!");
35
+ process.exit(0);
36
+ }
37
+ const { AddForm } = await import("./AddForm-VA5UMZDC.js");
38
+ const initialValues = {
39
+ command: command ?? "",
40
+ description: opts.desc ?? "",
41
+ tags: opts.tags ?? ""
42
+ };
43
+ const { waitUntilExit } = render(
44
+ React.createElement(AddForm, { initialValues })
45
+ );
46
+ await waitUntilExit();
47
+ process.exit(0);
48
+ });
49
+ program.command("remove <id>").description("Remove a command by ID").action(async (id) => {
50
+ const { FileStore } = await import("./FileStore-W4YQ3VAM.js");
51
+ const store = new FileStore();
52
+ const removed = store.removeCommand(id);
53
+ if (removed) {
54
+ console.log(`Removed command ${id}`);
55
+ } else {
56
+ console.error(`No command found with ID: ${id}`);
57
+ process.exit(1);
58
+ }
59
+ });
60
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@fiete/drift",
3
+ "version": "1.0.0",
4
+ "description": "A TUI command vault for your shell",
5
+ "type": "module",
6
+ "bin": {
7
+ "drift": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "scripts": {
16
+ "build": "tsup && chmod +x dist/index.js",
17
+ "build:watch": "tsup --watch",
18
+ "dev": "tsx src/index.ts",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "dependencies": {
22
+ "@inkjs/ui": "^2.0.0",
23
+ "clipboardy": "^4.0.0",
24
+ "commander": "^12.0.0",
25
+ "execa": "^9.0.0",
26
+ "fuse.js": "^7.0.0",
27
+ "ink": "^5.0.0",
28
+ "nanoid": "^5.0.0",
29
+ "react": "^18.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.0.0",
33
+ "@types/react": "^18.0.0",
34
+ "tsup": "^8.0.0",
35
+ "tsx": "^4.0.0",
36
+ "typescript": "^5.0.0"
37
+ }
38
+ }