@fiete/drift 1.0.5 → 1.0.6

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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AddForm
4
- } from "./chunk-GUH6BVED.js";
5
- import "./chunk-CRXU4OLG.js";
4
+ } from "./chunk-TBVOGPA4.js";
5
+ import "./chunk-SRTEOAGN.js";
6
6
  export {
7
7
  AddForm
8
8
  };
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AddForm,
4
+ ProjectSelect,
4
5
  useStore
5
- } from "./chunk-GUH6BVED.js";
6
- import "./chunk-CRXU4OLG.js";
6
+ } from "./chunk-TBVOGPA4.js";
7
+ import "./chunk-SRTEOAGN.js";
7
8
 
8
9
  // src/ui/Browser.tsx
9
- import { useState as useState3, useCallback as useCallback2 } from "react";
10
- import { Box as Box7, Text as Text7, useInput as useInput4, useApp } from "ink";
10
+ import { useState as useState4, useCallback as useCallback3 } from "react";
11
+ import { Box as Box11, Text as Text11, useInput as useInput6, useApp as useApp2 } from "ink";
11
12
  import clipboard from "clipboardy";
12
13
 
13
14
  // src/hooks/useFuzzySearch.ts
@@ -82,6 +83,7 @@ function CommandItem({ command, isSelected, dimmed = false }) {
82
83
  import { Box as Box3, Text as Text3 } from "ink";
83
84
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
84
85
  var HINTS = [
86
+ ["\u2190\u2192", "tabs"],
85
87
  ["\u2191\u2193", "navigate"],
86
88
  ["Shift+\u2191\u2193", "reorder"],
87
89
  ["/", "search"],
@@ -94,24 +96,44 @@ var HINTS = [
94
96
  ];
95
97
  function StatusBar({ message }) {
96
98
  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: [
99
+ /* @__PURE__ */ jsx3(Box3, { gap: 0, children: HINTS.map(([key, label], i) => /* @__PURE__ */ jsxs3(Box3, { children: [
100
+ i > 0 && /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: " \u2502 " }),
98
101
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: key }),
99
- /* @__PURE__ */ jsx3(Text3, { color: "gray", children: label })
102
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
103
+ " ",
104
+ label
105
+ ] })
100
106
  ] }, key)) }),
101
107
  message && /* @__PURE__ */ jsx3(Text3, { color: "greenBright", bold: true, children: message })
102
108
  ] });
103
109
  }
104
110
 
105
- // src/ui/ConfirmDelete.tsx
106
- import { Box as Box4, Text as Text4, useInput } from "ink";
111
+ // src/ui/TabBar.tsx
112
+ import { Box as Box4, Text as Text4 } from "ink";
107
113
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
114
+ function TabBar({ projects, activeIndex }) {
115
+ const tabs = ["All", ...projects.map((p) => p.name)];
116
+ const newTabIndex = tabs.length;
117
+ return /* @__PURE__ */ jsxs4(Box4, { paddingX: 1, paddingY: 0, gap: 0, children: [
118
+ tabs.map((tab, i) => /* @__PURE__ */ jsx4(Box4, { paddingX: 1, children: i === activeIndex ? /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "blueBright", children: [
119
+ "[",
120
+ tab,
121
+ "]"
122
+ ] }) : /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: tab }) }, i)),
123
+ /* @__PURE__ */ jsx4(Box4, { paddingX: 1, children: activeIndex === newTabIndex ? /* @__PURE__ */ jsx4(Text4, { bold: true, color: "greenBright", children: "[+]" }) : /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "[+]" }) })
124
+ ] });
125
+ }
126
+
127
+ // src/ui/ConfirmDelete.tsx
128
+ import { Box as Box5, Text as Text5, useInput } from "ink";
129
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
108
130
  function ConfirmDelete({ command, onConfirm, onCancel }) {
109
131
  useInput((input, key) => {
110
132
  if (input === "y" || input === "Y") onConfirm();
111
133
  if (input === "n" || input === "N" || key.escape) onCancel();
112
134
  });
113
- return /* @__PURE__ */ jsxs4(
114
- Box4,
135
+ return /* @__PURE__ */ jsxs5(
136
+ Box5,
115
137
  {
116
138
  borderStyle: "round",
117
139
  borderColor: "red",
@@ -121,41 +143,111 @@ function ConfirmDelete({ command, onConfirm, onCancel }) {
121
143
  marginX: 2,
122
144
  marginY: 1,
123
145
  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" })
146
+ /* @__PURE__ */ jsx5(Text5, { color: "red", bold: true, children: "Delete command?" }),
147
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: command.command }) }),
148
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, gap: 2, children: [
149
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "[y]" }),
150
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "yes, delete it" }),
151
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "white", children: " [n]" }),
152
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "cancel" })
131
153
  ] })
132
154
  ]
133
155
  }
134
156
  );
135
157
  }
136
158
 
137
- // src/ui/EditModal.tsx
159
+ // src/ui/AddProjectForm.tsx
138
160
  import { useState, useCallback } from "react";
139
- import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
161
+ import { Box as Box6, Text as Text6, useApp, useInput as useInput2 } from "ink";
140
162
  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"];
163
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
164
+ var FIELD_ORDER = ["name", "root"];
165
+ function AddProjectForm({ onSave, onCancel }) {
166
+ const { exit } = useApp();
167
+ const [fieldIndex, setFieldIndex] = useState(0);
168
+ const field = FIELD_ORDER[fieldIndex];
169
+ const [formData, setFormData] = useState({ name: "", root: "" });
170
+ useInput2((_, key) => {
171
+ if (key.escape) {
172
+ onCancel();
173
+ return;
174
+ }
175
+ if (key.upArrow) {
176
+ setFieldIndex((i) => Math.max(0, i - 1));
177
+ return;
178
+ }
179
+ if (key.downArrow) {
180
+ setFieldIndex((i) => Math.min(FIELD_ORDER.length - 1, i + 1));
181
+ return;
182
+ }
183
+ });
184
+ const handleChange = useCallback((value) => {
185
+ setFormData((d) => ({ ...d, [field]: value }));
186
+ }, [field]);
187
+ const handleSubmit = (value) => {
188
+ const updated = { ...formData, [field]: value };
189
+ setFormData(updated);
190
+ if (fieldIndex < FIELD_ORDER.length - 1) {
191
+ setFieldIndex((i) => i + 1);
192
+ } else {
193
+ if (!updated.name.trim()) {
194
+ setFieldIndex(0);
195
+ return;
196
+ }
197
+ onSave(updated.name.trim(), updated.root.trim());
198
+ }
199
+ };
200
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", padding: 1, gap: 1, children: [
201
+ /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { bold: true, color: "blueBright", children: " New Project" }) }),
202
+ [["name", "Name", "e.g. Backend"], ["root", "Root Directory", "e.g. /Users/me/project"]].map(([f, label, placeholder], i) => {
203
+ const isActive = i === fieldIndex;
204
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
205
+ /* @__PURE__ */ jsxs6(Text6, { color: isActive ? "yellowBright" : "gray", children: [
206
+ isActive ? "\u203A" : " ",
207
+ " ",
208
+ label
209
+ ] }),
210
+ /* @__PURE__ */ jsx6(Box6, { paddingLeft: 2, children: isActive ? /* @__PURE__ */ jsx6(
211
+ TextInput,
212
+ {
213
+ placeholder,
214
+ defaultValue: formData[f],
215
+ onChange: handleChange,
216
+ onSubmit: handleSubmit
217
+ },
218
+ f
219
+ ) : /* @__PURE__ */ jsx6(Text6, { color: formData[f] ? "white" : "gray", dimColor: !formData[f], children: formData[f] || placeholder }) })
220
+ ] }, f);
221
+ }),
222
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "\u2191\u2193 navigate \xB7 Enter to advance \xB7 ESC to cancel" }) })
223
+ ] });
224
+ }
225
+
226
+ // src/ui/EditModal.tsx
227
+ import { useState as useState2, useCallback as useCallback2 } from "react";
228
+ import { Box as Box7, Text as Text7, useInput as useInput3 } from "ink";
229
+ import { TextInput as TextInput2 } from "@inkjs/ui";
230
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
231
+ var FIELD_ORDER2 = ["description", "command", "directory", "tags", "project"];
143
232
  var LABELS = {
144
233
  description: "Description",
145
234
  command: "Command",
146
235
  directory: "Directory (optional)",
147
- tags: "Tags (comma-separated)"
236
+ tags: "Tags (comma-separated)",
237
+ project: "Project"
148
238
  };
149
- function EditModal({ command, onSave, onCancel }) {
150
- const [fieldIndex, setFieldIndex] = useState(0);
151
- const field = FIELD_ORDER[fieldIndex];
152
- const [formData, setFormData] = useState({
239
+ function EditModal({ command, projects, onSave, onCancel }) {
240
+ const [fieldIndex, setFieldIndex] = useState2(0);
241
+ const field = FIELD_ORDER2[fieldIndex];
242
+ const [formData, setFormData] = useState2({
153
243
  description: command.description,
154
244
  command: command.command,
155
245
  directory: command.directory ?? "",
156
- tags: command.tags.join(", ")
246
+ tags: command.tags.join(", "),
247
+ projectId: command.projectId
157
248
  });
158
- useInput2((_, key) => {
249
+ const projectOptions = [void 0, ...projects.map((p) => p.id)];
250
+ useInput3((_, key) => {
159
251
  if (key.escape) {
160
252
  onCancel();
161
253
  return;
@@ -165,29 +257,47 @@ function EditModal({ command, onSave, onCancel }) {
165
257
  return;
166
258
  }
167
259
  if (key.downArrow) {
168
- setFieldIndex((i) => Math.min(FIELD_ORDER.length - 1, i + 1));
260
+ setFieldIndex((i) => Math.min(FIELD_ORDER2.length - 1, i + 1));
261
+ return;
262
+ }
263
+ if (field === "project" && (key.leftArrow || key.rightArrow)) {
264
+ const currentIdx = projectOptions.findIndex((id) => id === formData.projectId);
265
+ const safeIdx = currentIdx === -1 ? 0 : currentIdx;
266
+ const next = key.rightArrow ? Math.min(projectOptions.length - 1, safeIdx + 1) : Math.max(0, safeIdx - 1);
267
+ setFormData((d) => ({ ...d, projectId: projectOptions[next] }));
268
+ return;
269
+ }
270
+ if (field === "project" && key.return) {
271
+ onSave({
272
+ command: formData.command.trim(),
273
+ description: formData.description.trim(),
274
+ directory: formData.directory.trim() || void 0,
275
+ tags: formData.tags.split(",").map((t) => t.trim()).filter(Boolean),
276
+ projectId: formData.projectId
277
+ });
169
278
  return;
170
279
  }
171
280
  });
172
- const handleChange = useCallback((value) => {
281
+ const handleChange = useCallback2((value) => {
173
282
  setFormData((d) => ({ ...d, [field]: value }));
174
283
  }, [field]);
175
284
  const handleSubmit = (value) => {
176
285
  const updated = { ...formData, [field]: value };
177
286
  setFormData(updated);
178
- if (fieldIndex < FIELD_ORDER.length - 1) {
287
+ if (fieldIndex < FIELD_ORDER2.length - 1) {
179
288
  setFieldIndex((i) => i + 1);
180
289
  } else {
181
290
  onSave({
182
291
  command: updated.command.trim(),
183
292
  description: updated.description.trim(),
184
293
  directory: updated.directory.trim() || void 0,
185
- tags: updated.tags.split(",").map((t) => t.trim()).filter(Boolean)
294
+ tags: updated.tags.split(",").map((t) => t.trim()).filter(Boolean),
295
+ projectId: updated.projectId
186
296
  });
187
297
  }
188
298
  };
189
- return /* @__PURE__ */ jsxs5(
190
- Box5,
299
+ return /* @__PURE__ */ jsxs7(
300
+ Box7,
191
301
  {
192
302
  borderStyle: "round",
193
303
  borderColor: "yellowBright",
@@ -197,51 +307,60 @@ function EditModal({ command, onSave, onCancel }) {
197
307
  marginX: 2,
198
308
  marginY: 1,
199
309
  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" })
310
+ /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, justifyContent: "space-between", children: [
311
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellowBright", children: " Edit Command" }),
312
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "ESC to cancel" })
203
313
  ] }),
204
- FIELD_ORDER.map((f, i) => {
314
+ FIELD_ORDER2.map((f, i) => {
205
315
  const isActive = i === fieldIndex;
206
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
207
- /* @__PURE__ */ jsxs5(Text5, { color: isActive ? "yellowBright" : "gray", children: [
316
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
317
+ /* @__PURE__ */ jsxs7(Text7, { color: isActive ? "yellowBright" : "gray", children: [
208
318
  isActive ? "\u203A" : " ",
209
319
  " ",
210
320
  LABELS[f]
211
321
  ] }),
212
- /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, children: isActive ? /* @__PURE__ */ jsx5(
213
- TextInput,
322
+ /* @__PURE__ */ jsx7(Box7, { paddingLeft: 2, children: f === "project" ? /* @__PURE__ */ jsx7(
323
+ ProjectSelect,
324
+ {
325
+ projects,
326
+ selectedProjectId: formData.projectId,
327
+ isActive
328
+ }
329
+ ) : isActive ? /* @__PURE__ */ jsx7(
330
+ TextInput2,
214
331
  {
215
- defaultValue: formData[f],
332
+ defaultValue: formData[f] ?? "",
216
333
  onChange: handleChange,
217
334
  onSubmit: handleSubmit
218
335
  },
219
336
  f
220
- ) : /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: formData[f] || "(empty)" }) })
337
+ ) : /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: formData[f] || "(empty)" }) })
221
338
  ] }, f);
222
339
  }),
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" }) })
340
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: "gray", dimColor: true, children: [
341
+ "\u2191\u2193 navigate \xB7 Enter to advance \xB7 ",
342
+ field === "project" ? "\u2190\u2192 select project \xB7 " : "",
343
+ "saves on last field"
344
+ ] }) })
224
345
  ]
225
346
  }
226
347
  );
227
348
  }
228
349
 
229
350
  // 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";
232
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
351
+ import { Box as Box9, Text as Text9, useInput as useInput4 } from "ink";
352
+
353
+ // src/ui/DriftCanvas.tsx
354
+ import { useEffect, useRef, useState as useState3 } from "react";
355
+ import { Box as Box8, Text as Text8 } from "ink";
356
+ import { jsx as jsx8 } from "react/jsx-runtime";
233
357
  var W = 64;
234
358
  var H = 20;
235
359
  var PATHS = [
236
- // 1 — bottom-center → sweep hard left → exit top-right (original)
237
360
  [{ 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
361
  [{ 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
362
  [{ 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
363
  [{ 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
364
  [{ 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
365
  ];
247
366
  function bezier(bp, t) {
@@ -301,8 +420,8 @@ function renderFrame(marks, smokes) {
301
420
  }
302
421
  return buf.map((row) => row.join("")).join("\n");
303
422
  }
304
- function Onboarding({ onDismiss }) {
305
- const [canvas, setCanvas] = useState2(() => Array(H).fill(" ".repeat(W)).join("\n"));
423
+ function DriftCanvas() {
424
+ const [canvas, setCanvas] = useState3(() => Array(H).fill(" ".repeat(W)).join("\n"));
306
425
  const anim = useRef({
307
426
  marks: [],
308
427
  smokes: [],
@@ -311,9 +430,6 @@ function Onboarding({ onDismiss }) {
311
430
  pathIndex: 0,
312
431
  cycleCount: 0
313
432
  });
314
- useInput3(() => {
315
- onDismiss();
316
- });
317
433
  useEffect(() => {
318
434
  const id = setInterval(() => {
319
435
  const s = anim.current;
@@ -341,9 +457,18 @@ function Onboarding({ onDismiss }) {
341
457
  }, 40);
342
458
  return () => clearInterval(id);
343
459
  }, []);
460
+ return /* @__PURE__ */ jsx8(Box8, { width: W, children: /* @__PURE__ */ jsx8(Text8, { children: canvas }) });
461
+ }
462
+
463
+ // src/ui/Onboarding.tsx
464
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
465
+ function Onboarding({ onDismiss }) {
466
+ useInput4(() => {
467
+ onDismiss();
468
+ });
344
469
  const termH = process.stdout.rows ?? 24;
345
- return /* @__PURE__ */ jsx6(Box6, { height: termH, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs6(
346
- Box6,
470
+ return /* @__PURE__ */ jsx9(Box9, { height: termH, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs8(
471
+ Box9,
347
472
  {
348
473
  borderStyle: "round",
349
474
  borderColor: "white",
@@ -352,42 +477,89 @@ function Onboarding({ onDismiss }) {
352
477
  paddingY: 1,
353
478
  width: W + 8,
354
479
  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." })
480
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", alignItems: "center", marginBottom: 1, children: [
481
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: " drift " }),
482
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "your commands. always within reach." })
358
483
  ] }),
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" }) })
484
+ /* @__PURE__ */ jsx9(DriftCanvas, {}),
485
+ /* @__PURE__ */ jsx9(Box9, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "press any key to continue" }) })
486
+ ]
487
+ }
488
+ ) });
489
+ }
490
+
491
+ // src/ui/UpdatePrompt.tsx
492
+ import { Box as Box10, Text as Text10, useInput as useInput5 } from "ink";
493
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
494
+ function UpdatePrompt({ currentVersion, latestVersion, onConfirm, onSkip }) {
495
+ useInput5((input, key) => {
496
+ if (input === "y" || input === "Y") onConfirm();
497
+ if (input === "s" || input === "S" || key.escape) onSkip();
498
+ });
499
+ const termH = process.stdout.rows ?? 24;
500
+ return /* @__PURE__ */ jsx10(Box10, { height: termH, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs9(
501
+ Box10,
502
+ {
503
+ borderStyle: "round",
504
+ borderColor: "white",
505
+ flexDirection: "column",
506
+ paddingX: 2,
507
+ paddingY: 1,
508
+ width: W + 8,
509
+ children: [
510
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", alignItems: "center", marginBottom: 1, children: [
511
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "update available " }),
512
+ /* @__PURE__ */ jsxs9(Text10, { children: [
513
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: currentVersion }),
514
+ /* @__PURE__ */ jsx10(Text10, { children: " \u2192 " }),
515
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: latestVersion })
516
+ ] })
517
+ ] }),
518
+ /* @__PURE__ */ jsx10(DriftCanvas, {}),
519
+ /* @__PURE__ */ jsxs9(Box10, { justifyContent: "center", marginTop: 1, gap: 2, children: [
520
+ /* @__PURE__ */ jsxs9(Text10, { children: [
521
+ /* @__PURE__ */ jsx10(Text10, { bold: true, color: "green", children: "[y]" }),
522
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: " update now" })
523
+ ] }),
524
+ /* @__PURE__ */ jsxs9(Text10, { children: [
525
+ /* @__PURE__ */ jsx10(Text10, { bold: true, color: "white", children: "[s]" }),
526
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: " skip for now" })
527
+ ] })
528
+ ] })
361
529
  ]
362
530
  }
363
531
  ) });
364
532
  }
365
533
 
366
534
  // src/ui/Browser.tsx
367
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
368
- var HEADER_ROWS = 4;
535
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
536
+ var HEADER_ROWS = 5;
369
537
  var FOOTER_ROWS = 3;
370
- function Browser({ onExecute, forceOnboarding = false }) {
371
- const { exit } = useApp();
372
- const { commands, updateCommand, moveCommand, removeCommand } = useStore();
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("");
381
- const filtered = useFuzzySearch(commands, query);
538
+ function Browser({ onExecute, forceOnboarding = false, currentVersion, latestVersion, onUpdate }) {
539
+ const { exit } = useApp2();
540
+ const { commands, projects, addProject, updateCommand, moveCommand, removeCommand } = useStore();
541
+ const [query, setQuery] = useState4("");
542
+ const [isSearching, setIsSearching] = useState4(false);
543
+ const [selectedIndex, setSelectedIndex] = useState4(0);
544
+ const [viewportStart, setViewportStart] = useState4(0);
545
+ const [activeTabIndex, setActiveTabIndex] = useState4(0);
546
+ const [screen, setScreen] = useState4(() => {
547
+ if (latestVersion) return "update-prompt";
548
+ return forceOnboarding || commands.length === 0 ? "onboarding" : "browser";
549
+ });
550
+ const [statusMessage, setStatusMessage] = useState4("");
551
+ const newProjectTabIndex = 1 + projects.length;
552
+ const tabCommands = activeTabIndex === 0 || activeTabIndex === newProjectTabIndex ? commands : commands.filter((c) => c.projectId === projects[activeTabIndex - 1]?.id);
553
+ const filtered = useFuzzySearch(tabCommands, query);
382
554
  const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
383
555
  const selected = filtered[clampedIndex] ?? null;
384
556
  const visibleRows = Math.max(1, (process.stdout.rows ?? 24) - HEADER_ROWS - FOOTER_ROWS);
385
557
  const visibleCommands = filtered.slice(viewportStart, viewportStart + visibleRows);
386
- const flash = useCallback2((msg) => {
558
+ const flash = useCallback3((msg) => {
387
559
  setStatusMessage(msg);
388
560
  setTimeout(() => setStatusMessage(""), 2e3);
389
561
  }, []);
390
- const navigate = useCallback2(
562
+ const navigate = useCallback3(
391
563
  (direction) => {
392
564
  setSelectedIndex((prev) => {
393
565
  const next = Math.max(0, Math.min(filtered.length - 1, prev + direction));
@@ -401,9 +573,26 @@ function Browser({ onExecute, forceOnboarding = false }) {
401
573
  },
402
574
  [filtered.length, visibleRows]
403
575
  );
404
- useInput4(
576
+ const navigateTab = useCallback3(
577
+ (direction) => {
578
+ const tabCount = 1 + projects.length + 1;
579
+ setActiveTabIndex((prev) => Math.max(0, Math.min(tabCount - 1, prev + direction)));
580
+ setSelectedIndex(0);
581
+ setViewportStart(0);
582
+ },
583
+ [projects.length]
584
+ );
585
+ const buildExecString = useCallback3(
586
+ (cmd) => {
587
+ if (!cmd) return "";
588
+ const dir = cmd.directory ?? (cmd.projectId ? projects.find((p) => p.id === cmd.projectId)?.root : void 0);
589
+ return dir && cmd.command ? `cd "${dir}" && ${cmd.command}` : dir ? `cd "${dir}"` : cmd.command;
590
+ },
591
+ [projects]
592
+ );
593
+ useInput6(
405
594
  (input, key) => {
406
- if (screen === "confirm-delete") return;
595
+ if (screen === "confirm-delete" || screen === "update-prompt") return;
407
596
  if (isSearching) {
408
597
  if (key.escape) {
409
598
  setQuery("");
@@ -439,6 +628,14 @@ function Browser({ onExecute, forceOnboarding = false }) {
439
628
  }
440
629
  return;
441
630
  }
631
+ if (key.leftArrow && !key.shift) {
632
+ navigateTab(-1);
633
+ return;
634
+ }
635
+ if (key.rightArrow && !key.shift) {
636
+ navigateTab(1);
637
+ return;
638
+ }
442
639
  if (key.upArrow && key.shift && selected && !query) {
443
640
  moveCommand(selected.id, -1);
444
641
  navigate(-1);
@@ -469,15 +666,17 @@ function Browser({ onExecute, forceOnboarding = false }) {
469
666
  } else exit();
470
667
  return;
471
668
  }
669
+ if (key.return && activeTabIndex === 1 + projects.length) {
670
+ setScreen("add-project");
671
+ return;
672
+ }
472
673
  if (key.return && selected) {
473
- const full = selected.directory && selected.command ? `cd "${selected.directory}" && ${selected.command}` : selected.directory ? `cd "${selected.directory}"` : selected.command;
474
- onExecute(full);
674
+ onExecute(buildExecString(selected));
475
675
  exit();
476
676
  return;
477
677
  }
478
678
  if (key.ctrl && input === "e" && selected) {
479
- const full = selected.directory && selected.command ? `cd "${selected.directory}" && ${selected.command}` : selected.directory ? `cd "${selected.directory}"` : selected.command;
480
- clipboard.writeSync(full);
679
+ clipboard.writeSync(buildExecString(selected));
481
680
  flash("Copied!");
482
681
  return;
483
682
  }
@@ -496,13 +695,44 @@ function Browser({ onExecute, forceOnboarding = false }) {
496
695
  },
497
696
  { isActive: screen === "browser" }
498
697
  );
698
+ if (screen === "update-prompt" && latestVersion) {
699
+ const nextScreen = forceOnboarding || commands.length === 0 ? "onboarding" : "browser";
700
+ return /* @__PURE__ */ jsx11(
701
+ UpdatePrompt,
702
+ {
703
+ currentVersion,
704
+ latestVersion,
705
+ onConfirm: () => {
706
+ onUpdate();
707
+ exit();
708
+ },
709
+ onSkip: () => setScreen(nextScreen)
710
+ }
711
+ );
712
+ }
499
713
  if (screen === "onboarding") {
500
- return /* @__PURE__ */ jsx7(Onboarding, { onDismiss: () => setScreen("browser") });
714
+ return /* @__PURE__ */ jsx11(Onboarding, { onDismiss: () => setScreen("browser") });
715
+ }
716
+ if (screen === "add-project") {
717
+ return /* @__PURE__ */ jsx11(
718
+ AddProjectForm,
719
+ {
720
+ onSave: (name, root) => {
721
+ addProject({ name, root });
722
+ setScreen("browser");
723
+ setActiveTabIndex(1 + projects.length);
724
+ flash(`Project "${name}" created.`);
725
+ },
726
+ onCancel: () => setScreen("browser")
727
+ }
728
+ );
501
729
  }
502
730
  if (screen === "add") {
503
- return /* @__PURE__ */ jsx7(
731
+ const defaultProjectId = activeTabIndex > 0 ? projects[activeTabIndex - 1]?.id : void 0;
732
+ return /* @__PURE__ */ jsx11(
504
733
  AddForm,
505
734
  {
735
+ initialValues: { projectId: defaultProjectId },
506
736
  onSave: () => {
507
737
  setScreen("browser");
508
738
  flash("Saved!");
@@ -512,9 +742,10 @@ function Browser({ onExecute, forceOnboarding = false }) {
512
742
  );
513
743
  }
514
744
  const isOverlay = screen === "confirm-delete" || screen === "edit";
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(
745
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", height: process.stdout.rows ?? 24, children: [
746
+ /* @__PURE__ */ jsx11(TabBar, { projects, activeIndex: activeTabIndex }),
747
+ /* @__PURE__ */ jsx11(SearchBar, { query, isSearching, total: tabCommands.length, filtered: filtered.length }),
748
+ /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexGrow: 1, children: filtered.length === 0 ? /* @__PURE__ */ jsx11(Box11, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx11(Text11, { color: "gray", dimColor: true, children: tabCommands.length === 0 ? activeTabIndex === 0 ? "No commands saved yet. Press `a` to add your first command." : "No commands in this project. Press `a` to add one." : "No matches. Keep typing or clear the search." }) }) : visibleCommands.map((cmd, i) => /* @__PURE__ */ jsx11(
518
749
  CommandItem,
519
750
  {
520
751
  command: cmd,
@@ -523,10 +754,11 @@ function Browser({ onExecute, forceOnboarding = false }) {
523
754
  },
524
755
  cmd.id
525
756
  )) }),
526
- screen === "edit" && selected && /* @__PURE__ */ jsx7(
757
+ screen === "edit" && selected && /* @__PURE__ */ jsx11(
527
758
  EditModal,
528
759
  {
529
760
  command: selected,
761
+ projects,
530
762
  onSave: (patch) => {
531
763
  updateCommand(selected.id, patch);
532
764
  setScreen("browser");
@@ -535,7 +767,7 @@ function Browser({ onExecute, forceOnboarding = false }) {
535
767
  onCancel: () => setScreen("browser")
536
768
  }
537
769
  ),
538
- screen === "confirm-delete" && selected && /* @__PURE__ */ jsx7(
770
+ screen === "confirm-delete" && selected && /* @__PURE__ */ jsx11(
539
771
  ConfirmDelete,
540
772
  {
541
773
  command: selected,
@@ -548,7 +780,7 @@ function Browser({ onExecute, forceOnboarding = false }) {
548
780
  onCancel: () => setScreen("browser")
549
781
  }
550
782
  ),
551
- /* @__PURE__ */ jsx7(StatusBar, { message: statusMessage })
783
+ /* @__PURE__ */ jsx11(StatusBar, { message: statusMessage })
552
784
  ] });
553
785
  }
554
786
  export {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FileStore
4
- } from "./chunk-CRXU4OLG.js";
4
+ } from "./chunk-SRTEOAGN.js";
5
5
  export {
6
6
  FileStore
7
7
  };
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/checkUpdate.ts
4
+ function isNewer(latest, current) {
5
+ const a = latest.split(".").map(Number);
6
+ const b = current.split(".").map(Number);
7
+ for (let i = 0; i < 3; i++) {
8
+ if (a[i] > b[i]) return true;
9
+ if (a[i] < b[i]) return false;
10
+ }
11
+ return false;
12
+ }
13
+ async function checkForUpdate(currentVersion) {
14
+ try {
15
+ const controller = new AbortController();
16
+ const timeout = setTimeout(() => controller.abort(), 2e3);
17
+ const res = await fetch("https://registry.npmjs.org/@fiete/drift/latest", {
18
+ signal: controller.signal
19
+ });
20
+ clearTimeout(timeout);
21
+ const data = await res.json();
22
+ const latest = data.version;
23
+ return isNewer(latest, currentVersion) ? latest : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ export {
29
+ checkForUpdate
30
+ };
@@ -7,7 +7,7 @@ import fs from "fs";
7
7
  import { nanoid } from "nanoid";
8
8
  var STORE_DIR = path.join(os.homedir(), ".config", "drift");
9
9
  var STORE_FILE = path.join(STORE_DIR, "commands.json");
10
- var CURRENT_VERSION = 1;
10
+ var CURRENT_VERSION = 2;
11
11
  var FileStore = class {
12
12
  data;
13
13
  constructor() {
@@ -16,12 +16,21 @@ var FileStore = class {
16
16
  load() {
17
17
  if (!fs.existsSync(STORE_FILE)) {
18
18
  fs.mkdirSync(STORE_DIR, { recursive: true });
19
- const initial = { version: CURRENT_VERSION, commands: [] };
19
+ const initial = { version: CURRENT_VERSION, commands: [], projects: [] };
20
20
  fs.writeFileSync(STORE_FILE, JSON.stringify(initial, null, 2));
21
21
  return initial;
22
22
  }
23
23
  const raw = fs.readFileSync(STORE_FILE, "utf8");
24
- return JSON.parse(raw);
24
+ const parsed = JSON.parse(raw);
25
+ return this.migrate(parsed);
26
+ }
27
+ migrate(data) {
28
+ if (data.version < 2) {
29
+ data.projects = [];
30
+ data.version = 2;
31
+ fs.writeFileSync(STORE_FILE, JSON.stringify(data, null, 2));
32
+ }
33
+ return data;
25
34
  }
26
35
  save(data) {
27
36
  fs.writeFileSync(STORE_FILE, JSON.stringify(data, null, 2));
@@ -64,6 +73,38 @@ var FileStore = class {
64
73
  }
65
74
  return false;
66
75
  }
76
+ // ── Projects ────────────────────────────────────────────────────────────────
77
+ getProjects() {
78
+ return this.data.projects;
79
+ }
80
+ getProjectById(id) {
81
+ return this.data.projects.find((p) => p.id === id);
82
+ }
83
+ addProject(input) {
84
+ const project = { ...input, id: nanoid(), createdAt: (/* @__PURE__ */ new Date()).toISOString() };
85
+ this.data.projects.push(project);
86
+ this.save(this.data);
87
+ return project;
88
+ }
89
+ updateProject(id, patch) {
90
+ const idx = this.data.projects.findIndex((p) => p.id === id);
91
+ if (idx === -1) return false;
92
+ this.data.projects[idx] = { ...this.data.projects[idx], ...patch };
93
+ this.save(this.data);
94
+ return true;
95
+ }
96
+ removeProject(id) {
97
+ const before = this.data.projects.length;
98
+ this.data.projects = this.data.projects.filter((p) => p.id !== id);
99
+ this.data.commands = this.data.commands.map(
100
+ (c) => c.projectId === id ? { ...c, projectId: void 0 } : c
101
+ );
102
+ if (this.data.projects.length < before) {
103
+ this.save(this.data);
104
+ return true;
105
+ }
106
+ return false;
107
+ }
67
108
  };
68
109
 
69
110
  export {
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ FileStore
4
+ } from "./chunk-SRTEOAGN.js";
5
+
6
+ // src/ui/AddForm.tsx
7
+ import { useState as useState2, useCallback as useCallback2 } from "react";
8
+ import { Box as Box2, Text as Text2, 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 [projects, setProjects] = useState(() => store.getProjects());
17
+ const refresh = useCallback(() => {
18
+ setCommands([...store.getAll()]);
19
+ setProjects([...store.getProjects()]);
20
+ }, []);
21
+ const addCommand = useCallback(
22
+ (input) => {
23
+ store.addCommand(input);
24
+ refresh();
25
+ },
26
+ [refresh]
27
+ );
28
+ const updateCommand = useCallback(
29
+ (id, patch) => {
30
+ store.updateCommand(id, patch);
31
+ refresh();
32
+ },
33
+ [refresh]
34
+ );
35
+ const moveCommand = useCallback(
36
+ (id, direction) => {
37
+ store.moveCommand(id, direction);
38
+ refresh();
39
+ },
40
+ [refresh]
41
+ );
42
+ const removeCommand = useCallback(
43
+ (id) => {
44
+ store.removeCommand(id);
45
+ refresh();
46
+ },
47
+ [refresh]
48
+ );
49
+ const addProject = useCallback(
50
+ (input) => {
51
+ store.addProject(input);
52
+ refresh();
53
+ },
54
+ [refresh]
55
+ );
56
+ const updateProject = useCallback(
57
+ (id, patch) => {
58
+ store.updateProject(id, patch);
59
+ refresh();
60
+ },
61
+ [refresh]
62
+ );
63
+ const removeProject = useCallback(
64
+ (id) => {
65
+ store.removeProject(id);
66
+ refresh();
67
+ },
68
+ [refresh]
69
+ );
70
+ return {
71
+ commands,
72
+ projects,
73
+ addCommand,
74
+ updateCommand,
75
+ moveCommand,
76
+ removeCommand,
77
+ addProject,
78
+ updateProject,
79
+ removeProject
80
+ };
81
+ }
82
+
83
+ // src/ui/ProjectSelect.tsx
84
+ import { Box, Text } from "ink";
85
+ import { jsx, jsxs } from "react/jsx-runtime";
86
+ function ProjectSelect({ projects, selectedProjectId, isActive }) {
87
+ const options = [{ id: void 0, name: "None" }, ...projects.map((p) => ({ id: p.id, name: p.name }))];
88
+ const selectedIdx = options.findIndex((o) => o.id === selectedProjectId);
89
+ const displayIdx = selectedIdx === -1 ? 0 : selectedIdx;
90
+ return /* @__PURE__ */ jsx(Box, { gap: 1, flexWrap: "wrap", children: options.map((opt, i) => {
91
+ const isCurrent = i === displayIdx;
92
+ if (isCurrent && isActive) {
93
+ return /* @__PURE__ */ jsxs(Text, { color: "yellowBright", bold: true, children: [
94
+ "\u2039 ",
95
+ opt.name,
96
+ " \u203A"
97
+ ] }, i);
98
+ }
99
+ return /* @__PURE__ */ jsx(Text, { color: isCurrent ? "white" : "gray", dimColor: !isCurrent, children: opt.name }, i);
100
+ }) });
101
+ }
102
+
103
+ // src/ui/AddForm.tsx
104
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
105
+ var FIELD_ORDER = ["description", "command", "directory", "tags", "project"];
106
+ var LABELS = {
107
+ description: "Description",
108
+ command: "Command",
109
+ directory: "Directory",
110
+ tags: "Tags (comma-separated)",
111
+ project: "Project"
112
+ };
113
+ var PLACEHOLDERS = {
114
+ description: "e.g. Show last 20 commits",
115
+ command: "e.g. git log --oneline -20",
116
+ directory: "e.g. /Users/me/project (leave empty to skip)",
117
+ tags: "e.g. git, log",
118
+ project: ""
119
+ };
120
+ function AddForm({ initialValues, onSave, onCancel }) {
121
+ const { exit } = useApp();
122
+ const { addCommand, projects } = useStore();
123
+ const [fieldIndex, setFieldIndex] = useState2(0);
124
+ const field = FIELD_ORDER[fieldIndex];
125
+ const [formData, setFormData] = useState2({
126
+ description: initialValues?.description ?? "",
127
+ command: initialValues?.command ?? "",
128
+ directory: "",
129
+ tags: initialValues?.tags ?? "",
130
+ projectId: initialValues?.projectId
131
+ });
132
+ const cancel = () => {
133
+ onCancel ? onCancel() : exit();
134
+ };
135
+ const projectOptions = [void 0, ...projects.map((p) => p.id)];
136
+ useInput((_, key) => {
137
+ if (key.escape) {
138
+ cancel();
139
+ return;
140
+ }
141
+ if (key.upArrow) {
142
+ setFieldIndex((i) => Math.max(0, i - 1));
143
+ return;
144
+ }
145
+ if (key.downArrow) {
146
+ setFieldIndex((i) => Math.min(FIELD_ORDER.length - 1, i + 1));
147
+ return;
148
+ }
149
+ if (field === "project" && (key.leftArrow || key.rightArrow)) {
150
+ const currentIdx = projectOptions.findIndex((id) => id === formData.projectId);
151
+ const safeIdx = currentIdx === -1 ? 0 : currentIdx;
152
+ const next = key.rightArrow ? Math.min(projectOptions.length - 1, safeIdx + 1) : Math.max(0, safeIdx - 1);
153
+ setFormData((d) => ({ ...d, projectId: projectOptions[next] }));
154
+ return;
155
+ }
156
+ if (field === "project" && key.return) {
157
+ handleProjectSubmit();
158
+ return;
159
+ }
160
+ });
161
+ const handleChange = useCallback2((value) => {
162
+ setFormData((d) => ({ ...d, [field]: value }));
163
+ }, [field]);
164
+ const handleProjectSubmit = () => {
165
+ if (!formData.command.trim() && !formData.directory.trim()) {
166
+ setFieldIndex(FIELD_ORDER.indexOf("command"));
167
+ return;
168
+ }
169
+ addCommand({
170
+ command: formData.command.trim(),
171
+ description: formData.description.trim(),
172
+ directory: formData.directory.trim() || void 0,
173
+ tags: formData.tags.split(",").map((t) => t.trim()).filter(Boolean),
174
+ projectId: formData.projectId
175
+ });
176
+ if (onSave) {
177
+ onSave();
178
+ } else {
179
+ console.log("\nSaved!");
180
+ exit();
181
+ }
182
+ };
183
+ const handleSubmit = (value) => {
184
+ const updated = { ...formData, [field]: value };
185
+ setFormData(updated);
186
+ if (fieldIndex < FIELD_ORDER.length - 1) {
187
+ setFieldIndex((i) => i + 1);
188
+ } else {
189
+ handleProjectSubmit();
190
+ }
191
+ };
192
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", padding: 1, gap: 1, children: [
193
+ /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, color: "blueBright", children: " Add New Command" }) }),
194
+ FIELD_ORDER.map((f, i) => {
195
+ const isActive = i === fieldIndex;
196
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
197
+ /* @__PURE__ */ jsxs2(Text2, { color: isActive ? "yellowBright" : "gray", children: [
198
+ isActive ? "\u203A" : " ",
199
+ " ",
200
+ LABELS[f]
201
+ ] }),
202
+ /* @__PURE__ */ jsx2(Box2, { paddingLeft: 2, children: f === "project" ? /* @__PURE__ */ jsx2(
203
+ ProjectSelect,
204
+ {
205
+ projects,
206
+ selectedProjectId: formData.projectId,
207
+ isActive
208
+ }
209
+ ) : isActive ? /* @__PURE__ */ jsx2(
210
+ TextInput,
211
+ {
212
+ placeholder: PLACEHOLDERS[f],
213
+ defaultValue: formData[f] ?? "",
214
+ onChange: handleChange,
215
+ onSubmit: handleSubmit
216
+ },
217
+ f
218
+ ) : /* @__PURE__ */ jsx2(Text2, { color: formData[f] ? "white" : "gray", dimColor: !formData[f], children: formData[f] || PLACEHOLDERS[f] }) })
219
+ ] }, f);
220
+ }),
221
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", dimColor: true, children: [
222
+ "\u2191\u2193 navigate \xB7 Enter to advance \xB7 ",
223
+ field === "project" ? "\u2190\u2192 select project \xB7 " : "",
224
+ "ESC to cancel"
225
+ ] }) })
226
+ ] });
227
+ }
228
+
229
+ export {
230
+ useStore,
231
+ ProjectSelect,
232
+ AddForm
233
+ };
package/dist/index.js CHANGED
@@ -5,21 +5,35 @@ import { program } from "commander";
5
5
  import { render } from "ink";
6
6
  import React from "react";
7
7
  import { execa } from "execa";
8
+ import { createRequire } from "module";
9
+ var require2 = createRequire(import.meta.url);
10
+ var packageJson = require2("../package.json");
8
11
  program.name("drift").description("cmdvault \u2014 A TUI command vault for your shell").version("1.0.0");
9
12
  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-6Q6XF5D4.js");
13
+ const { Browser } = await import("./Browser-67UFH2PS.js");
14
+ const { checkForUpdate } = await import("./checkUpdate-G4I5BZTU.js");
15
+ const latestVersion = await checkForUpdate(packageJson.version);
11
16
  let commandToRun;
17
+ let shouldUpdate = false;
12
18
  const { waitUntilExit } = render(
13
19
  React.createElement(Browser, {
14
20
  onExecute: (cmd) => {
15
21
  commandToRun = cmd;
16
22
  },
17
- forceOnboarding: opts.onboarding ?? false
23
+ forceOnboarding: opts.onboarding ?? false,
24
+ currentVersion: packageJson.version,
25
+ latestVersion: latestVersion ?? void 0,
26
+ onUpdate: () => {
27
+ shouldUpdate = true;
28
+ }
18
29
  })
19
30
  );
20
31
  await waitUntilExit();
21
32
  process.stdout.write("\x1Bc");
22
- if (commandToRun) {
33
+ if (shouldUpdate) {
34
+ console.log("\nInstalling @fiete/drift@latest...\n");
35
+ await execa("npm", ["install", "-g", "@fiete/drift"], { stdio: "inherit" });
36
+ } else if (commandToRun) {
23
37
  await execa(commandToRun, { shell: true, stdio: "inherit" }).catch(() => {
24
38
  });
25
39
  }
@@ -27,7 +41,7 @@ program.command("list", { isDefault: true }).description("Browse saved commands
27
41
  });
28
42
  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").option("--dir <directory>", "Working directory").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) => {
29
43
  if (command && opts.desc !== void 0 && opts.tags !== void 0) {
30
- const { FileStore } = await import("./FileStore-5LGMICIM.js");
44
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
31
45
  const store = new FileStore();
32
46
  store.addCommand({
33
47
  command: command.trim(),
@@ -38,7 +52,7 @@ program.command("add [command]").description("Add a command. Pass it as a quoted
38
52
  console.log("Saved!");
39
53
  process.exit(0);
40
54
  }
41
- const { AddForm } = await import("./AddForm-NVOWLCPV.js");
55
+ const { AddForm } = await import("./AddForm-ZMSCP6CU.js");
42
56
  const initialValues = {
43
57
  command: command ?? "",
44
58
  description: opts.desc ?? "",
@@ -51,7 +65,7 @@ program.command("add [command]").description("Add a command. Pass it as a quoted
51
65
  process.exit(0);
52
66
  });
53
67
  program.command("remove <id>").description("Remove a command by ID").action(async (id) => {
54
- const { FileStore } = await import("./FileStore-5LGMICIM.js");
68
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
55
69
  const store = new FileStore();
56
70
  const removed = store.removeCommand(id);
57
71
  if (removed) {
@@ -62,7 +76,7 @@ program.command("remove <id>").description("Remove a command by ID").action(asyn
62
76
  }
63
77
  });
64
78
  program.command("ls").description("List all saved commands").option("--json", "Output as JSON").action(async (opts) => {
65
- const { FileStore } = await import("./FileStore-5LGMICIM.js");
79
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
66
80
  const store = new FileStore();
67
81
  const commands = store.getAll();
68
82
  if (opts.json) {
@@ -82,7 +96,7 @@ program.command("ls").description("List all saved commands").option("--json", "O
82
96
  process.exit(0);
83
97
  });
84
98
  program.command("search <query>").description("Search saved commands (fuzzy)").option("--json", "Output as JSON").action(async (query, opts) => {
85
- const { FileStore } = await import("./FileStore-5LGMICIM.js");
99
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
86
100
  const { searchCommands } = await import("./search-HZEMV67M.js");
87
101
  const store = new FileStore();
88
102
  const results = searchCommands(store.getAll(), query);
@@ -103,7 +117,7 @@ program.command("search <query>").description("Search saved commands (fuzzy)").o
103
117
  process.exit(0);
104
118
  });
105
119
  program.command("get <id>").description("Get a single command by ID").option("--json", "Output as JSON").action(async (id, opts) => {
106
- const { FileStore } = await import("./FileStore-5LGMICIM.js");
120
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
107
121
  const store = new FileStore();
108
122
  const cmd = store.getById(id);
109
123
  if (!cmd) {
@@ -121,7 +135,7 @@ program.command("get <id>").description("Get a single command by ID").option("--
121
135
  process.exit(0);
122
136
  });
123
137
  program.command("execute <id>").description("Execute a saved command by ID").action(async (id) => {
124
- const { FileStore } = await import("./FileStore-5LGMICIM.js");
138
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
125
139
  const store = new FileStore();
126
140
  const cmd = store.getById(id);
127
141
  if (!cmd) {
@@ -133,7 +147,7 @@ program.command("execute <id>").description("Execute a saved command by ID").act
133
147
  process.exit(0);
134
148
  });
135
149
  program.command("copy <id>").description("Copy a saved command to the clipboard").action(async (id) => {
136
- const { FileStore } = await import("./FileStore-5LGMICIM.js");
150
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
137
151
  const { default: clipboardy } = await import("clipboardy");
138
152
  const store = new FileStore();
139
153
  const cmd = store.getById(id);
@@ -146,7 +160,7 @@ program.command("copy <id>").description("Copy a saved command to the clipboard"
146
160
  process.exit(0);
147
161
  });
148
162
  program.command("edit <id>").description("Edit a saved command by ID").option("-c, --command <command>", "New command string").option("-d, --desc <description>", "New description").option("-t, --tags <tags>", "New comma-separated tags").option("--dir <directory>", "New working directory").option("--json", "Output updated command as JSON").action(async (id, opts) => {
149
- const { FileStore } = await import("./FileStore-5LGMICIM.js");
163
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
150
164
  const store = new FileStore();
151
165
  const patch = {};
152
166
  if (opts.command !== void 0) patch.command = opts.command.trim();
@@ -169,4 +183,42 @@ program.command("edit <id>").description("Edit a saved command by ID").option("-
169
183
  }
170
184
  process.exit(0);
171
185
  });
186
+ var projectCmd = program.command("project").description("Manage projects");
187
+ projectCmd.command("add <name>").description("Create a new project").requiredOption("-r, --root <path>", "Root directory for this project").action(async (name, opts) => {
188
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
189
+ const store = new FileStore();
190
+ const project = store.addProject({ name: name.trim(), root: opts.root.trim() });
191
+ console.log(`Created project "${project.name}" (${project.id})
192
+ root: ${project.root}`);
193
+ process.exit(0);
194
+ });
195
+ projectCmd.command("ls").description("List all projects").option("--json", "Output as JSON").action(async (opts) => {
196
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
197
+ const store = new FileStore();
198
+ const projects = store.getProjects();
199
+ if (opts.json) {
200
+ console.log(JSON.stringify(projects, null, 2));
201
+ } else {
202
+ if (projects.length === 0) {
203
+ console.log("No projects yet. Run `drift project add <name> --root <path>` to create one.");
204
+ } else {
205
+ for (const p of projects) {
206
+ console.log(`${p.id} ${p.name}`);
207
+ console.log(` root: ${p.root}`);
208
+ }
209
+ }
210
+ }
211
+ process.exit(0);
212
+ });
213
+ projectCmd.command("rm <id>").description("Remove a project by ID (commands are unassigned, not deleted)").action(async (id) => {
214
+ const { FileStore } = await import("./FileStore-DV2QBZJL.js");
215
+ const store = new FileStore();
216
+ const removed = store.removeProject(id);
217
+ if (removed) {
218
+ console.log(`Removed project ${id}`);
219
+ } else {
220
+ console.error(`No project found with ID: ${id}`);
221
+ process.exit(1);
222
+ }
223
+ });
172
224
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiete/drift",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "A TUI command vault for your shell",
5
5
  "type": "module",
6
6
  "preferGlobal": true,
@@ -1,154 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- FileStore
4
- } from "./chunk-CRXU4OLG.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",
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() && !updated.directory.trim()) {
108
- setFieldIndex(1);
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
- };