@fiete/drift 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # drift
2
+
3
+ A TUI command vault for your shell. Save, search, and execute commands you use often but can never quite remember.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @brechtknecht/drift
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ drift # open the TUI browser
15
+ drift add # add a new command interactively
16
+ drift add "git log --oneline -20" # pre-fill the command
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ Each saved command has:
22
+ - **Description** — what it does
23
+ - **Command** — the shell command
24
+ - **Directory** *(optional)* — when set, executing or copying prepends `cd <dir> &&`
25
+ - **Tags** — for organisation and search
26
+
27
+ ## Data
28
+
29
+ Commands are stored at `~/.config/drift/commands.json`.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AddForm
4
- } from "./chunk-VS6YEHBV.js";
5
- import "./chunk-QT5M6VMT.js";
4
+ } from "./chunk-VCY3CRYL.js";
5
+ import "./chunk-V6RC35DD.js";
6
6
  export {
7
7
  AddForm
8
8
  };
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  AddForm,
4
4
  useStore
5
- } from "./chunk-VS6YEHBV.js";
6
- import "./chunk-QT5M6VMT.js";
5
+ } from "./chunk-VCY3CRYL.js";
6
+ import "./chunk-V6RC35DD.js";
7
7
 
8
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";
9
+ import { useState as useState3, useCallback as useCallback2 } from "react";
10
+ import { Box as Box7, Text as Text7, useInput as useInput4, useApp } from "ink";
11
11
  import clipboard from "clipboardy";
12
12
 
13
13
  // src/hooks/useFuzzySearch.ts
@@ -226,19 +226,158 @@ function EditModal({ command, onSave, onCancel }) {
226
226
  );
227
227
  }
228
228
 
229
- // src/ui/Browser.tsx
229
+ // src/ui/Onboarding.tsx
230
+ import { useEffect, useRef, useState as useState2 } from "react";
231
+ import { Box as Box6, Text as Text6, useInput as useInput3 } from "ink";
230
232
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
233
+ var W = 64;
234
+ var H = 20;
235
+ var PATHS = [
236
+ // 1 — bottom-center → sweep hard left → exit top-right (original)
237
+ [{ x: 0.45, y: 1.12 }, { x: 0.45, y: 0.55 }, { x: 0.1, y: 0.12 }, { x: 1.12, y: 0.1 }],
238
+ // 2 — bottom-right → wide left arc → exit left-middle
239
+ [{ x: 0.8, y: 1.12 }, { x: 0.8, y: 0.45 }, { x: 0.15, y: 0.18 }, { x: -0.12, y: 0.45 }],
240
+ // 3 — bottom-left → arc right → exit top-right (mirror of 1)
241
+ [{ x: 0.22, y: 1.12 }, { x: 0.22, y: 0.55 }, { x: 0.88, y: 0.12 }, { x: 1.12, y: 0.1 }],
242
+ // 4 — left-middle → sweep right across → exit top-right
243
+ [{ x: -0.12, y: 0.7 }, { x: 0.22, y: 0.7 }, { x: 0.78, y: 0.12 }, { x: 1.12, y: 0.12 }],
244
+ // 5 — right-middle → sweep left across → exit top-left
245
+ [{ x: 1.12, y: 0.7 }, { x: 0.78, y: 0.7 }, { x: 0.22, y: 0.12 }, { x: -0.12, y: 0.12 }]
246
+ ];
247
+ function bezier(bp, t) {
248
+ const m = 1 - t;
249
+ return {
250
+ x: m * m * m * bp[0].x + 3 * m * m * t * bp[1].x + 3 * m * t * t * bp[2].x + t * t * t * bp[3].x,
251
+ y: m * m * m * bp[0].y + 3 * m * m * t * bp[1].y + 3 * m * t * t * bp[2].y + t * t * t * bp[3].y
252
+ };
253
+ }
254
+ function bezierAngleDeg(bp, t) {
255
+ const d = 1e-3;
256
+ const a = bezier(bp, Math.max(0, t - d));
257
+ const b = bezier(bp, Math.min(1, t + d));
258
+ return Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI;
259
+ }
260
+ var SKID_CH = ["\u2500", "\u2572", "\u2502", "\u2571", "\u2500", "\u2572", "\u2502", "\u2571"];
261
+ var SMOKE_CH = ["\u2217", "\xB0", "\xB7", "`", "'", "~", "\u2218", "*"];
262
+ var DURATION = 4200;
263
+ var PAUSE = 900;
264
+ var TRAIL_DT = 0.016;
265
+ var DRIFT_DEG = 24;
266
+ function spawnTrail(marks, smokes, carX, carY, bodyDeg) {
267
+ const bRad = bodyDeg * Math.PI / 180;
268
+ const rearX = carX - Math.cos(bRad) * 3.5;
269
+ const rearY = carY - Math.sin(bRad) * 3.5;
270
+ const pRad = bRad + Math.PI / 2;
271
+ const ws = 1.8;
272
+ const di = Math.round((bodyDeg % 360 + 360) % 360 / 45) % 8;
273
+ const sk = SKID_CH[di];
274
+ marks.push({ x: rearX + Math.cos(pRad) * ws, y: rearY + Math.sin(pRad) * ws, ch: sk, age: 0, maxAge: 90 });
275
+ marks.push({ x: rearX - Math.cos(pRad) * ws, y: rearY - Math.sin(pRad) * ws, ch: sk, age: 0, maxAge: 90 });
276
+ const count = Math.random() < 0.5 ? 1 : 2;
277
+ for (let i = 0; i < count; i++) {
278
+ smokes.push({
279
+ x: rearX + (Math.random() - 0.5) * 5,
280
+ y: rearY + (Math.random() - 0.5) * 2.5,
281
+ ch: SMOKE_CH[Math.floor(Math.random() * SMOKE_CH.length)],
282
+ age: 0,
283
+ maxAge: 28
284
+ });
285
+ }
286
+ }
287
+ function renderFrame(marks, smokes) {
288
+ const buf = Array.from({ length: H }, () => new Array(W).fill(" "));
289
+ function blit(x, y, ch) {
290
+ const xi = Math.round(x), yi = Math.round(y);
291
+ if (xi >= 0 && xi < W && yi >= 0 && yi < H) buf[yi][xi] = ch;
292
+ }
293
+ for (const m of marks) {
294
+ const fade = m.age / m.maxAge;
295
+ if (fade < 0.5) blit(m.x, m.y, m.ch);
296
+ else if (fade < 0.75) blit(m.x, m.y, "\xB7");
297
+ else blit(m.x, m.y, ".");
298
+ }
299
+ for (const s of smokes) {
300
+ if (s.age / s.maxAge < 0.65) blit(s.x, s.y, s.ch);
301
+ }
302
+ return buf.map((row) => row.join("")).join("\n");
303
+ }
304
+ function Onboarding({ onDismiss }) {
305
+ const [canvas, setCanvas] = useState2(() => Array(H).fill(" ".repeat(W)).join("\n"));
306
+ const anim = useRef({
307
+ marks: [],
308
+ smokes: [],
309
+ lastTrail: -1,
310
+ startTime: Date.now(),
311
+ pathIndex: 0,
312
+ cycleCount: 0
313
+ });
314
+ useInput3(() => {
315
+ onDismiss();
316
+ });
317
+ useEffect(() => {
318
+ const id = setInterval(() => {
319
+ const s = anim.current;
320
+ const elapsed = Date.now() - s.startTime;
321
+ const cycle = elapsed % (DURATION + PAUSE);
322
+ if (cycle < 40) {
323
+ s.cycleCount++;
324
+ s.pathIndex = s.cycleCount % PATHS.length;
325
+ s.lastTrail = -1;
326
+ }
327
+ const bp = PATHS[s.pathIndex];
328
+ const T = cycle < DURATION ? cycle / DURATION : 1;
329
+ const pos = bezier(bp, T);
330
+ const velDeg = bezierAngleDeg(bp, T);
331
+ const bodyDeg = velDeg + DRIFT_DEG;
332
+ if (T < 0.99 && T - s.lastTrail >= TRAIL_DT) {
333
+ s.lastTrail = T;
334
+ spawnTrail(s.marks, s.smokes, pos.x * W, pos.y * H, bodyDeg);
335
+ }
336
+ for (const m of s.marks) m.age++;
337
+ for (const sm of s.smokes) sm.age++;
338
+ s.marks = s.marks.filter((m) => m.age < m.maxAge);
339
+ s.smokes = s.smokes.filter((sm) => sm.age < sm.maxAge);
340
+ setCanvas(renderFrame(s.marks, s.smokes));
341
+ }, 40);
342
+ return () => clearInterval(id);
343
+ }, []);
344
+ const termH = process.stdout.rows ?? 24;
345
+ return /* @__PURE__ */ jsx6(Box6, { height: termH, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs6(
346
+ Box6,
347
+ {
348
+ borderStyle: "round",
349
+ borderColor: "white",
350
+ flexDirection: "column",
351
+ paddingX: 2,
352
+ paddingY: 1,
353
+ width: W + 8,
354
+ children: [
355
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", alignItems: "center", marginBottom: 1, children: [
356
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: " drift " }),
357
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "your commands. always within reach." })
358
+ ] }),
359
+ /* @__PURE__ */ jsx6(Box6, { width: W, children: /* @__PURE__ */ jsx6(Text6, { children: canvas }) }),
360
+ /* @__PURE__ */ jsx6(Box6, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press any key to continue" }) })
361
+ ]
362
+ }
363
+ ) });
364
+ }
365
+
366
+ // src/ui/Browser.tsx
367
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
231
368
  var HEADER_ROWS = 4;
232
369
  var FOOTER_ROWS = 3;
233
- function Browser({ onExecute }) {
370
+ function Browser({ onExecute, forceOnboarding = false }) {
234
371
  const { exit } = useApp();
235
372
  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("");
373
+ const [query, setQuery] = useState3("");
374
+ const [isSearching, setIsSearching] = useState3(false);
375
+ const [selectedIndex, setSelectedIndex] = useState3(0);
376
+ const [viewportStart, setViewportStart] = useState3(0);
377
+ const [screen, setScreen] = useState3(
378
+ () => forceOnboarding || commands.length === 0 ? "onboarding" : "browser"
379
+ );
380
+ const [statusMessage, setStatusMessage] = useState3("");
242
381
  const filtered = useFuzzySearch(commands, query);
243
382
  const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
244
383
  const selected = filtered[clampedIndex] ?? null;
@@ -262,7 +401,7 @@ function Browser({ onExecute }) {
262
401
  },
263
402
  [filtered.length, visibleRows]
264
403
  );
265
- useInput3(
404
+ useInput4(
266
405
  (input, key) => {
267
406
  if (screen === "confirm-delete") return;
268
407
  if (isSearching) {
@@ -357,8 +496,11 @@ function Browser({ onExecute }) {
357
496
  },
358
497
  { isActive: screen === "browser" }
359
498
  );
499
+ if (screen === "onboarding") {
500
+ return /* @__PURE__ */ jsx7(Onboarding, { onDismiss: () => setScreen("browser") });
501
+ }
360
502
  if (screen === "add") {
361
- return /* @__PURE__ */ jsx6(
503
+ return /* @__PURE__ */ jsx7(
362
504
  AddForm,
363
505
  {
364
506
  onSave: () => {
@@ -370,9 +512,9 @@ function Browser({ onExecute }) {
370
512
  );
371
513
  }
372
514
  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(
515
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", height: process.stdout.rows ?? 24, children: [
516
+ /* @__PURE__ */ jsx7(SearchBar, { query, isSearching, total: commands.length, filtered: filtered.length }),
517
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", flexGrow: 1, children: filtered.length === 0 ? /* @__PURE__ */ jsx7(Box7, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx7(Text7, { 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__ */ jsx7(
376
518
  CommandItem,
377
519
  {
378
520
  command: cmd,
@@ -381,7 +523,7 @@ function Browser({ onExecute }) {
381
523
  },
382
524
  cmd.id
383
525
  )) }),
384
- screen === "edit" && selected && /* @__PURE__ */ jsx6(
526
+ screen === "edit" && selected && /* @__PURE__ */ jsx7(
385
527
  EditModal,
386
528
  {
387
529
  command: selected,
@@ -393,7 +535,7 @@ function Browser({ onExecute }) {
393
535
  onCancel: () => setScreen("browser")
394
536
  }
395
537
  ),
396
- screen === "confirm-delete" && selected && /* @__PURE__ */ jsx6(
538
+ screen === "confirm-delete" && selected && /* @__PURE__ */ jsx7(
397
539
  ConfirmDelete,
398
540
  {
399
541
  command: selected,
@@ -406,7 +548,7 @@ function Browser({ onExecute }) {
406
548
  onCancel: () => setScreen("browser")
407
549
  }
408
550
  ),
409
- /* @__PURE__ */ jsx6(StatusBar, { message: statusMessage })
551
+ /* @__PURE__ */ jsx7(StatusBar, { message: statusMessage })
410
552
  ] });
411
553
  }
412
554
  export {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FileStore
4
- } from "./chunk-QT5M6VMT.js";
4
+ } from "./chunk-V6RC35DD.js";
5
5
  export {
6
6
  FileStore
7
7
  };
@@ -5,7 +5,7 @@ import path from "path";
5
5
  import os from "os";
6
6
  import fs from "fs";
7
7
  import { nanoid } from "nanoid";
8
- var STORE_DIR = path.join(os.homedir(), ".config", "cmdvault");
8
+ var STORE_DIR = path.join(os.homedir(), ".config", "drift");
9
9
  var STORE_FILE = path.join(STORE_DIR, "commands.json");
10
10
  var CURRENT_VERSION = 1;
11
11
  var FileStore = class {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FileStore
4
- } from "./chunk-QT5M6VMT.js";
4
+ } from "./chunk-V6RC35DD.js";
5
5
 
6
6
  // src/ui/AddForm.tsx
7
7
  import { useState as useState2, useCallback as useCallback2 } from "react";
package/dist/index.js CHANGED
@@ -6,13 +6,16 @@ import { render } from "ink";
6
6
  import React from "react";
7
7
  import { execa } from "execa";
8
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");
9
+ program.command("list", { isDefault: true }).description("Browse saved commands (default)").option("--onboarding", "Force the onboarding screen (debug)").action(async (opts) => {
10
+ const { Browser } = await import("./Browser-UPOZVAV4.js");
11
11
  let commandToRun;
12
12
  const { waitUntilExit } = render(
13
- React.createElement(Browser, { onExecute: (cmd) => {
14
- commandToRun = cmd;
15
- } })
13
+ React.createElement(Browser, {
14
+ onExecute: (cmd) => {
15
+ commandToRun = cmd;
16
+ },
17
+ forceOnboarding: opts.onboarding ?? false
18
+ })
16
19
  );
17
20
  await waitUntilExit();
18
21
  process.stdout.write("\x1Bc");
@@ -24,7 +27,7 @@ program.command("list", { isDefault: true }).description("Browse saved commands
24
27
  });
25
28
  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
29
  if (command && opts.desc !== void 0 && opts.tags !== void 0) {
27
- const { FileStore } = await import("./FileStore-W4YQ3VAM.js");
30
+ const { FileStore } = await import("./FileStore-VX4RYNIH.js");
28
31
  const store = new FileStore();
29
32
  store.addCommand({
30
33
  command: command.trim(),
@@ -34,7 +37,7 @@ program.command("add [command]").description("Add a command. Pass it as a quoted
34
37
  console.log("Saved!");
35
38
  process.exit(0);
36
39
  }
37
- const { AddForm } = await import("./AddForm-VA5UMZDC.js");
40
+ const { AddForm } = await import("./AddForm-BXHHRR2V.js");
38
41
  const initialValues = {
39
42
  command: command ?? "",
40
43
  description: opts.desc ?? "",
@@ -47,7 +50,7 @@ program.command("add [command]").description("Add a command. Pass it as a quoted
47
50
  process.exit(0);
48
51
  });
49
52
  program.command("remove <id>").description("Remove a command by ID").action(async (id) => {
50
- const { FileStore } = await import("./FileStore-W4YQ3VAM.js");
53
+ const { FileStore } = await import("./FileStore-VX4RYNIH.js");
51
54
  const store = new FileStore();
52
55
  const removed = store.removeCommand(id);
53
56
  if (removed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiete/drift",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A TUI command vault for your shell",
5
5
  "type": "module",
6
6
  "bin": {