formalconf 2.0.0 → 2.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.
Files changed (3) hide show
  1. package/README.md +167 -207
  2. package/dist/formalconf.js +1132 -643
  3. package/package.json +1 -1
@@ -1,78 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  // src/cli/formalconf.tsx
3
- import { useState as useState4, useEffect as useEffect3, useMemo } from "react";
4
- import { render, Box as Box10, Text as Text9, useApp, useInput as useInput3 } from "ink";
5
- import { Spinner } from "@inkjs/ui";
6
-
7
- // src/components/ui/VimSelect.tsx
8
- import { useState } from "react";
9
- import { Box, Text, useInput } from "ink";
10
-
11
- // src/lib/theme.ts
12
- var colors = {
13
- primary: "#5eead4",
14
- primaryDim: "#2dd4bf",
15
- accent: "#06b6d4",
16
- success: "#22c55e",
17
- error: "#ef4444",
18
- warning: "#f59e0b",
19
- info: "#3b82f6",
20
- text: "white",
21
- textDim: "gray",
22
- border: "#374151",
23
- borderLight: "#4b5563"
24
- };
25
- var borderStyles = {
26
- panel: "round",
27
- header: "round",
28
- footer: "single"
29
- };
30
-
31
- // src/components/ui/VimSelect.tsx
32
- import { jsxDEV } from "react/jsx-dev-runtime";
33
- function VimSelect({ options, onChange, isDisabled = false }) {
34
- const [index, setIndex] = useState(0);
35
- useInput((input, key) => {
36
- if (isDisabled)
37
- return;
38
- if (input === "j" || key.downArrow) {
39
- setIndex((i) => i < options.length - 1 ? i + 1 : i);
40
- }
41
- if (input === "k" || key.upArrow) {
42
- setIndex((i) => i > 0 ? i - 1 : i);
43
- }
44
- if (input === "l" || key.return) {
45
- onChange(options[index].value);
46
- }
47
- });
48
- return /* @__PURE__ */ jsxDEV(Box, {
49
- flexDirection: "column",
50
- children: options.map((opt, i) => /* @__PURE__ */ jsxDEV(Box, {
51
- children: /* @__PURE__ */ jsxDEV(Text, {
52
- color: i === index ? colors.primary : undefined,
53
- children: [
54
- i === index ? "❯" : " ",
55
- " ",
56
- opt.label
57
- ]
58
- }, undefined, true, undefined, this)
59
- }, opt.value, false, undefined, this))
60
- }, undefined, false, undefined, this);
61
- }
62
-
63
- // src/cli/formalconf.tsx
64
- import { readdirSync as readdirSync5, existsSync as existsSync6 } from "fs";
65
- import { join as join5 } from "path";
3
+ import { useState as useState9, useEffect as useEffect6 } from "react";
4
+ import { render, useApp as useApp2, useInput as useInput9 } from "ink";
5
+ import { Spinner as Spinner2 } from "@inkjs/ui";
66
6
 
67
7
  // src/components/layout/Layout.tsx
68
- import { Box as Box6 } from "ink";
8
+ import { Box as Box5 } from "ink";
69
9
 
70
10
  // src/hooks/useTerminalSize.ts
71
11
  import { useStdout } from "ink";
72
- import { useState as useState2, useEffect } from "react";
12
+ import { useState, useEffect } from "react";
73
13
  function useTerminalSize() {
74
14
  const { stdout } = useStdout();
75
- const [size, setSize] = useState2({
15
+ const [size, setSize] = useState({
76
16
  columns: stdout.columns || 80,
77
17
  rows: stdout.rows || 24
78
18
  });
@@ -92,10 +32,10 @@ function useTerminalSize() {
92
32
  }
93
33
 
94
34
  // src/components/Header.tsx
95
- import { Box as Box3, Text as Text3 } from "ink";
35
+ import { Box as Box2, Text as Text2 } from "ink";
96
36
 
97
37
  // src/hooks/useSystemStatus.ts
98
- import { useState as useState3, useEffect as useEffect2 } from "react";
38
+ import { useState as useState2, useEffect as useEffect2 } from "react";
99
39
  import { existsSync, readlinkSync, readdirSync, lstatSync } from "fs";
100
40
 
101
41
  // src/lib/paths.ts
@@ -170,6 +110,115 @@ async function execLive(command, cwd) {
170
110
  });
171
111
  });
172
112
  }
113
+ async function execStreaming(command, onLine, cwd) {
114
+ if (isBun) {
115
+ const proc = Bun.spawn(command, {
116
+ stdout: "pipe",
117
+ stderr: "pipe",
118
+ cwd
119
+ });
120
+ const processStream = async (stream) => {
121
+ const reader = stream.getReader();
122
+ const decoder = new TextDecoder;
123
+ let buffer = "";
124
+ while (true) {
125
+ const { done, value } = await reader.read();
126
+ if (done)
127
+ break;
128
+ buffer += decoder.decode(value, { stream: true });
129
+ const lines = buffer.split(`
130
+ `);
131
+ buffer = lines.pop() || "";
132
+ for (const line of lines) {
133
+ onLine(line);
134
+ }
135
+ }
136
+ if (buffer) {
137
+ onLine(buffer);
138
+ }
139
+ };
140
+ await Promise.all([
141
+ processStream(proc.stdout),
142
+ processStream(proc.stderr)
143
+ ]);
144
+ return await proc.exited;
145
+ }
146
+ return new Promise((resolve) => {
147
+ const [cmd, ...args] = command;
148
+ const proc = nodeSpawn(cmd, args, { cwd, shell: false });
149
+ const processData = (data) => {
150
+ const text = data.toString();
151
+ const lines = text.split(`
152
+ `);
153
+ for (const line of lines) {
154
+ if (line)
155
+ onLine(line);
156
+ }
157
+ };
158
+ proc.stdout?.on("data", processData);
159
+ proc.stderr?.on("data", processData);
160
+ proc.on("close", (code) => {
161
+ resolve(code ?? 1);
162
+ });
163
+ });
164
+ }
165
+ async function execStreamingWithTTY(command, onLine, cwd) {
166
+ if (isBun) {
167
+ const proc = Bun.spawn(command, {
168
+ stdout: "pipe",
169
+ stderr: "pipe",
170
+ stdin: "inherit",
171
+ cwd
172
+ });
173
+ const processStream = async (stream) => {
174
+ const reader = stream.getReader();
175
+ const decoder = new TextDecoder;
176
+ let buffer = "";
177
+ while (true) {
178
+ const { done, value } = await reader.read();
179
+ if (done)
180
+ break;
181
+ buffer += decoder.decode(value, { stream: true });
182
+ const lines = buffer.split(`
183
+ `);
184
+ buffer = lines.pop() || "";
185
+ for (const line of lines) {
186
+ onLine(line);
187
+ }
188
+ }
189
+ if (buffer) {
190
+ onLine(buffer);
191
+ }
192
+ };
193
+ await Promise.all([
194
+ processStream(proc.stdout),
195
+ processStream(proc.stderr)
196
+ ]);
197
+ return await proc.exited;
198
+ }
199
+ return new Promise((resolve) => {
200
+ const [cmd, ...args] = command;
201
+ const proc = nodeSpawn(cmd, args, {
202
+ cwd,
203
+ shell: false,
204
+ stdio: ["inherit", "pipe", "pipe"]
205
+ });
206
+ const processData = (data) => {
207
+ const text = data.toString();
208
+ const lines = text.split(`
209
+ `);
210
+ for (const line of lines) {
211
+ if (line)
212
+ onLine(line);
213
+ }
214
+ };
215
+ proc.stdout?.on("data", processData);
216
+ proc.stderr?.on("data", processData);
217
+ proc.on("close", (code) => {
218
+ resolve(code ?? 1);
219
+ });
220
+ });
221
+ }
173
222
  async function readJson(path) {
174
223
  if (isBun) {
175
224
  return Bun.file(path).json();
@@ -210,6 +259,19 @@ async function commandExists(cmd) {
210
259
  const result = await exec(["which", cmd]);
211
260
  return result.success;
212
261
  }
262
+ async function checkPrerequisites() {
263
+ const required = [
264
+ { name: "stow", install: "brew install stow" },
265
+ { name: "brew", install: "https://brew.sh" }
266
+ ];
267
+ const missing = [];
268
+ for (const dep of required) {
269
+ if (!await commandExists(dep.name)) {
270
+ missing.push(dep);
271
+ }
272
+ }
273
+ return { ok: missing.length === 0, missing };
274
+ }
213
275
 
214
276
  // src/lib/paths.ts
215
277
  var HOME_DIR = homedir();
@@ -236,7 +298,7 @@ async function ensureConfigDir() {
236
298
  // src/hooks/useSystemStatus.ts
237
299
  import { basename, dirname as dirname2, join as join2 } from "path";
238
300
  function useSystemStatus() {
239
- const [status, setStatus] = useState3({
301
+ const [status, setStatus] = useState2({
240
302
  currentTheme: null,
241
303
  configsLinked: false,
242
304
  loading: true
@@ -273,8 +335,30 @@ function useSystemStatus() {
273
335
  }
274
336
 
275
337
  // src/components/ui/StatusIndicator.tsx
276
- import { Box as Box2, Text as Text2 } from "ink";
277
- import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
338
+ import { Box, Text } from "ink";
339
+
340
+ // src/lib/theme.ts
341
+ var colors = {
342
+ primary: "#5eead4",
343
+ primaryDim: "#2dd4bf",
344
+ accent: "#06b6d4",
345
+ success: "#22c55e",
346
+ error: "#ef4444",
347
+ warning: "#f59e0b",
348
+ info: "#3b82f6",
349
+ text: "white",
350
+ textDim: "gray",
351
+ border: "#374151",
352
+ borderLight: "#4b5563"
353
+ };
354
+ var borderStyles = {
355
+ panel: "round",
356
+ header: "round",
357
+ footer: "single"
358
+ };
359
+
360
+ // src/components/ui/StatusIndicator.tsx
361
+ import { jsxDEV } from "react/jsx-dev-runtime";
278
362
  function StatusIndicator({
279
363
  label,
280
364
  value,
@@ -292,17 +376,17 @@ function StatusIndicator({
292
376
  error: "●",
293
377
  neutral: "○"
294
378
  };
295
- return /* @__PURE__ */ jsxDEV2(Box2, {
379
+ return /* @__PURE__ */ jsxDEV(Box, {
296
380
  gap: 1,
297
381
  children: [
298
- /* @__PURE__ */ jsxDEV2(Text2, {
382
+ /* @__PURE__ */ jsxDEV(Text, {
299
383
  dimColor: true,
300
384
  children: [
301
385
  label,
302
386
  ":"
303
387
  ]
304
388
  }, undefined, true, undefined, this),
305
- /* @__PURE__ */ jsxDEV2(Text2, {
389
+ /* @__PURE__ */ jsxDEV(Text, {
306
390
  color: statusColors[status],
307
391
  children: [
308
392
  icon[status],
@@ -316,7 +400,7 @@ function StatusIndicator({
316
400
  // package.json
317
401
  var package_default = {
318
402
  name: "formalconf",
319
- version: "2.0.0",
403
+ version: "2.0.2",
320
404
  description: "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
321
405
  type: "module",
322
406
  main: "./dist/formalconf.js",
@@ -369,11 +453,11 @@ var package_default = {
369
453
  };
370
454
 
371
455
  // src/components/Header.tsx
372
- import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
456
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
373
457
  function Header() {
374
458
  const { columns } = useTerminalSize();
375
459
  const { currentTheme, configsLinked, loading } = useSystemStatus();
376
- return /* @__PURE__ */ jsxDEV3(Box3, {
460
+ return /* @__PURE__ */ jsxDEV2(Box2, {
377
461
  flexDirection: "column",
378
462
  width: columns - 2,
379
463
  borderStyle: borderStyles.header,
@@ -381,24 +465,24 @@ function Header() {
381
465
  paddingX: 2,
382
466
  marginBottom: 1,
383
467
  children: [
384
- /* @__PURE__ */ jsxDEV3(Box3, {
468
+ /* @__PURE__ */ jsxDEV2(Box2, {
385
469
  justifyContent: "space-between",
386
470
  width: "100%",
387
471
  children: [
388
- /* @__PURE__ */ jsxDEV3(Box3, {
472
+ /* @__PURE__ */ jsxDEV2(Box2, {
389
473
  children: [
390
- /* @__PURE__ */ jsxDEV3(Text3, {
474
+ /* @__PURE__ */ jsxDEV2(Text2, {
391
475
  bold: true,
392
476
  color: colors.primary,
393
477
  children: "FormalConf"
394
478
  }, undefined, false, undefined, this),
395
- /* @__PURE__ */ jsxDEV3(Text3, {
479
+ /* @__PURE__ */ jsxDEV2(Text2, {
396
480
  dimColor: true,
397
481
  children: " - Dotfiles Manager"
398
482
  }, undefined, false, undefined, this)
399
483
  ]
400
484
  }, undefined, true, undefined, this),
401
- /* @__PURE__ */ jsxDEV3(Text3, {
485
+ /* @__PURE__ */ jsxDEV2(Text2, {
402
486
  dimColor: true,
403
487
  children: [
404
488
  "v",
@@ -407,16 +491,16 @@ function Header() {
407
491
  }, undefined, true, undefined, this)
408
492
  ]
409
493
  }, undefined, true, undefined, this),
410
- !loading && /* @__PURE__ */ jsxDEV3(Box3, {
494
+ !loading && /* @__PURE__ */ jsxDEV2(Box2, {
411
495
  marginTop: 1,
412
496
  gap: 4,
413
497
  children: [
414
- /* @__PURE__ */ jsxDEV3(StatusIndicator, {
498
+ /* @__PURE__ */ jsxDEV2(StatusIndicator, {
415
499
  label: "Theme",
416
500
  value: currentTheme,
417
501
  status: currentTheme ? "success" : "neutral"
418
502
  }, undefined, false, undefined, this),
419
- /* @__PURE__ */ jsxDEV3(StatusIndicator, {
503
+ /* @__PURE__ */ jsxDEV2(StatusIndicator, {
420
504
  label: "Configs",
421
505
  value: configsLinked ? "Linked" : "Not linked",
422
506
  status: configsLinked ? "success" : "warning"
@@ -428,8 +512,8 @@ function Header() {
428
512
  }
429
513
 
430
514
  // src/components/layout/Footer.tsx
431
- import { Box as Box4, Text as Text4 } from "ink";
432
- import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
515
+ import { Box as Box3, Text as Text3 } from "ink";
516
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
433
517
  var defaultShortcuts = [
434
518
  { key: "↑↓/jk", label: "Navigate" },
435
519
  { key: "Enter/l", label: "Select" },
@@ -438,7 +522,7 @@ var defaultShortcuts = [
438
522
  ];
439
523
  function Footer({ shortcuts = defaultShortcuts }) {
440
524
  const { columns } = useTerminalSize();
441
- return /* @__PURE__ */ jsxDEV4(Box4, {
525
+ return /* @__PURE__ */ jsxDEV3(Box3, {
442
526
  width: columns - 2,
443
527
  borderStyle: borderStyles.footer,
444
528
  borderColor: colors.border,
@@ -446,15 +530,15 @@ function Footer({ shortcuts = defaultShortcuts }) {
446
530
  marginTop: 1,
447
531
  justifyContent: "center",
448
532
  gap: 2,
449
- children: shortcuts.map((shortcut, index) => /* @__PURE__ */ jsxDEV4(Box4, {
533
+ children: shortcuts.map((shortcut, index) => /* @__PURE__ */ jsxDEV3(Box3, {
450
534
  gap: 1,
451
535
  children: [
452
- /* @__PURE__ */ jsxDEV4(Text4, {
536
+ /* @__PURE__ */ jsxDEV3(Text3, {
453
537
  color: colors.primary,
454
538
  bold: true,
455
539
  children: shortcut.key
456
540
  }, undefined, false, undefined, this),
457
- /* @__PURE__ */ jsxDEV4(Text4, {
541
+ /* @__PURE__ */ jsxDEV3(Text3, {
458
542
  dimColor: true,
459
543
  children: shortcut.label
460
544
  }, undefined, false, undefined, this)
@@ -464,14 +548,14 @@ function Footer({ shortcuts = defaultShortcuts }) {
464
548
  }
465
549
 
466
550
  // src/components/layout/Breadcrumb.tsx
467
- import React2 from "react";
468
- import { Box as Box5, Text as Text5 } from "ink";
469
- import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
551
+ import React from "react";
552
+ import { Box as Box4, Text as Text4 } from "ink";
553
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
470
554
  function Breadcrumb({ path }) {
471
- return /* @__PURE__ */ jsxDEV5(Box5, {
472
- children: path.map((segment, index) => /* @__PURE__ */ jsxDEV5(React2.Fragment, {
555
+ return /* @__PURE__ */ jsxDEV4(Box4, {
556
+ children: path.map((segment, index) => /* @__PURE__ */ jsxDEV4(React.Fragment, {
473
557
  children: [
474
- index > 0 && /* @__PURE__ */ jsxDEV5(Text5, {
558
+ index > 0 && /* @__PURE__ */ jsxDEV4(Text4, {
475
559
  color: colors.textDim,
476
560
  children: [
477
561
  " ",
@@ -479,7 +563,7 @@ function Breadcrumb({ path }) {
479
563
  " "
480
564
  ]
481
565
  }, undefined, true, undefined, this),
482
- /* @__PURE__ */ jsxDEV5(Text5, {
566
+ /* @__PURE__ */ jsxDEV4(Text4, {
483
567
  color: index === path.length - 1 ? colors.primary : colors.textDim,
484
568
  bold: index === path.length - 1,
485
569
  children: segment
@@ -490,39 +574,39 @@ function Breadcrumb({ path }) {
490
574
  }
491
575
 
492
576
  // src/components/layout/Layout.tsx
493
- import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
577
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
494
578
  function Layout({
495
579
  children,
496
580
  breadcrumb = ["Main"],
497
581
  showFooter = true
498
582
  }) {
499
583
  const { columns } = useTerminalSize();
500
- return /* @__PURE__ */ jsxDEV6(Box6, {
584
+ return /* @__PURE__ */ jsxDEV5(Box5, {
501
585
  flexDirection: "column",
502
586
  width: columns,
503
587
  padding: 1,
504
588
  children: [
505
- /* @__PURE__ */ jsxDEV6(Header, {}, undefined, false, undefined, this),
506
- breadcrumb.length > 1 && /* @__PURE__ */ jsxDEV6(Box6, {
589
+ /* @__PURE__ */ jsxDEV5(Header, {}, undefined, false, undefined, this),
590
+ breadcrumb.length > 1 && /* @__PURE__ */ jsxDEV5(Box5, {
507
591
  marginBottom: 1,
508
592
  marginLeft: 1,
509
- children: /* @__PURE__ */ jsxDEV6(Breadcrumb, {
593
+ children: /* @__PURE__ */ jsxDEV5(Breadcrumb, {
510
594
  path: breadcrumb
511
595
  }, undefined, false, undefined, this)
512
596
  }, undefined, false, undefined, this),
513
- /* @__PURE__ */ jsxDEV6(Box6, {
597
+ /* @__PURE__ */ jsxDEV5(Box5, {
514
598
  flexDirection: "column",
515
599
  flexGrow: 1,
516
600
  children
517
601
  }, undefined, false, undefined, this),
518
- showFooter && /* @__PURE__ */ jsxDEV6(Footer, {}, undefined, false, undefined, this)
602
+ showFooter && /* @__PURE__ */ jsxDEV5(Footer, {}, undefined, false, undefined, this)
519
603
  ]
520
604
  }, undefined, true, undefined, this);
521
605
  }
522
606
 
523
607
  // src/components/layout/Panel.tsx
524
- import { Box as Box7, Text as Text6 } from "ink";
525
- import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
608
+ import { Box as Box6, Text as Text5 } from "ink";
609
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
526
610
  function Panel({
527
611
  title,
528
612
  children,
@@ -530,7 +614,7 @@ function Panel({
530
614
  flexGrow,
531
615
  borderColor = colors.border
532
616
  }) {
533
- return /* @__PURE__ */ jsxDEV7(Box7, {
617
+ return /* @__PURE__ */ jsxDEV6(Box6, {
534
618
  flexDirection: "column",
535
619
  width,
536
620
  flexGrow,
@@ -538,9 +622,9 @@ function Panel({
538
622
  borderColor,
539
623
  paddingX: 1,
540
624
  children: [
541
- title && /* @__PURE__ */ jsxDEV7(Box7, {
625
+ title && /* @__PURE__ */ jsxDEV6(Box6, {
542
626
  marginBottom: 1,
543
- children: /* @__PURE__ */ jsxDEV7(Text6, {
627
+ children: /* @__PURE__ */ jsxDEV6(Text5, {
544
628
  bold: true,
545
629
  color: colors.primary,
546
630
  children: title
@@ -551,34 +635,144 @@ function Panel({
551
635
  }, undefined, true, undefined, this);
552
636
  }
553
637
 
554
- // src/components/CommandOutput.tsx
638
+ // src/components/PrerequisiteError.tsx
639
+ import { Box as Box7, Text as Text6, useInput } from "ink";
640
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
641
+ function PrerequisiteError({ missing, onExit }) {
642
+ useInput(() => onExit());
643
+ return /* @__PURE__ */ jsxDEV7(Layout, {
644
+ breadcrumb: ["Error"],
645
+ children: /* @__PURE__ */ jsxDEV7(Panel, {
646
+ title: "Missing Prerequisites",
647
+ borderColor: colors.error,
648
+ children: [
649
+ /* @__PURE__ */ jsxDEV7(Text6, {
650
+ color: colors.error,
651
+ children: "Required tools are not installed:"
652
+ }, undefined, false, undefined, this),
653
+ /* @__PURE__ */ jsxDEV7(Box7, {
654
+ flexDirection: "column",
655
+ marginTop: 1,
656
+ children: missing.map((dep) => /* @__PURE__ */ jsxDEV7(Box7, {
657
+ children: [
658
+ /* @__PURE__ */ jsxDEV7(Text6, {
659
+ color: colors.warning,
660
+ children: [
661
+ "• ",
662
+ dep.name
663
+ ]
664
+ }, undefined, true, undefined, this),
665
+ /* @__PURE__ */ jsxDEV7(Text6, {
666
+ dimColor: true,
667
+ children: [
668
+ " — Install: ",
669
+ dep.install
670
+ ]
671
+ }, undefined, true, undefined, this)
672
+ ]
673
+ }, dep.name, true, undefined, this))
674
+ }, undefined, false, undefined, this),
675
+ /* @__PURE__ */ jsxDEV7(Box7, {
676
+ marginTop: 1,
677
+ children: /* @__PURE__ */ jsxDEV7(Text6, {
678
+ dimColor: true,
679
+ children: "Press any key to exit..."
680
+ }, undefined, false, undefined, this)
681
+ }, undefined, false, undefined, this)
682
+ ]
683
+ }, undefined, true, undefined, this)
684
+ }, undefined, false, undefined, this);
685
+ }
686
+
687
+ // src/components/menus/MainMenu.tsx
688
+ import { useApp } from "ink";
689
+
690
+ // src/components/ui/VimSelect.tsx
691
+ import { useState as useState3 } from "react";
555
692
  import { Box as Box8, Text as Text7, useInput as useInput2 } from "ink";
556
693
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
694
+ function VimSelect({ options, onChange, isDisabled = false }) {
695
+ const [index, setIndex] = useState3(0);
696
+ useInput2((input, key) => {
697
+ if (isDisabled)
698
+ return;
699
+ if (input === "j" || key.downArrow) {
700
+ setIndex((i) => i < options.length - 1 ? i + 1 : i);
701
+ }
702
+ if (input === "k" || key.upArrow) {
703
+ setIndex((i) => i > 0 ? i - 1 : i);
704
+ }
705
+ if (input === "l" || key.return) {
706
+ onChange(options[index].value);
707
+ }
708
+ });
709
+ return /* @__PURE__ */ jsxDEV8(Box8, {
710
+ flexDirection: "column",
711
+ children: options.map((opt, i) => /* @__PURE__ */ jsxDEV8(Box8, {
712
+ children: /* @__PURE__ */ jsxDEV8(Text7, {
713
+ color: i === index ? colors.primary : undefined,
714
+ children: [
715
+ i === index ? "❯" : " ",
716
+ " ",
717
+ opt.label
718
+ ]
719
+ }, undefined, true, undefined, this)
720
+ }, opt.value, false, undefined, this))
721
+ }, undefined, false, undefined, this);
722
+ }
723
+
724
+ // src/components/menus/MainMenu.tsx
725
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
726
+ function MainMenu({ onSelect }) {
727
+ const { exit } = useApp();
728
+ return /* @__PURE__ */ jsxDEV9(Panel, {
729
+ title: "Main Menu",
730
+ children: /* @__PURE__ */ jsxDEV9(VimSelect, {
731
+ options: [
732
+ { label: "Config Manager", value: "config" },
733
+ { label: "Package Sync", value: "packages" },
734
+ { label: "Set Theme", value: "themes" },
735
+ { label: "Exit", value: "exit" }
736
+ ],
737
+ onChange: (value) => {
738
+ if (value === "exit") {
739
+ exit();
740
+ return;
741
+ }
742
+ onSelect(value);
743
+ }
744
+ }, undefined, false, undefined, this)
745
+ }, undefined, false, undefined, this);
746
+ }
747
+
748
+ // src/components/CommandOutput.tsx
749
+ import { Box as Box9, Text as Text8, useInput as useInput3 } from "ink";
750
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
557
751
  function CommandOutput({
558
752
  title,
559
753
  output,
560
754
  success = true,
561
755
  onDismiss
562
756
  }) {
563
- useInput2(() => {
757
+ useInput3(() => {
564
758
  onDismiss();
565
759
  });
566
- return /* @__PURE__ */ jsxDEV8(Panel, {
760
+ return /* @__PURE__ */ jsxDEV10(Panel, {
567
761
  title,
568
762
  borderColor: success ? colors.success : colors.error,
569
763
  children: [
570
- output && /* @__PURE__ */ jsxDEV8(Box8, {
764
+ output && /* @__PURE__ */ jsxDEV10(Box9, {
571
765
  flexDirection: "column",
572
766
  marginBottom: 1,
573
- children: /* @__PURE__ */ jsxDEV8(Text7, {
767
+ children: /* @__PURE__ */ jsxDEV10(Text8, {
574
768
  children: output
575
769
  }, undefined, false, undefined, this)
576
770
  }, undefined, false, undefined, this),
577
- /* @__PURE__ */ jsxDEV8(Text7, {
771
+ /* @__PURE__ */ jsxDEV10(Text8, {
578
772
  color: success ? colors.success : colors.error,
579
773
  children: success ? "Done" : "Failed"
580
774
  }, undefined, false, undefined, this),
581
- /* @__PURE__ */ jsxDEV8(Text7, {
775
+ /* @__PURE__ */ jsxDEV10(Text8, {
582
776
  dimColor: true,
583
777
  children: "Press any key to continue..."
584
778
  }, undefined, false, undefined, this)
@@ -586,122 +780,61 @@ function CommandOutput({
586
780
  }, undefined, true, undefined, this);
587
781
  }
588
782
 
589
- // src/components/ThemeCard.tsx
590
- import { Box as Box9, Text as Text8 } from "ink";
591
- import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
592
- function ThemeCard({ theme, isSelected, width }) {
593
- const borderColor = isSelected ? colors.accent : colors.border;
594
- const nameColor = isSelected ? colors.primary : colors.text;
595
- const indicators = [];
596
- if (theme.hasBackgrounds)
597
- indicators.push("bg");
598
- if (theme.isLightMode)
599
- indicators.push("light");
600
- const indicatorText = indicators.length > 0 ? ` [${indicators.join(" ")}]` : "";
601
- return /* @__PURE__ */ jsxDEV9(Box9, {
602
- flexDirection: "column",
603
- width,
604
- borderStyle: borderStyles.panel,
605
- borderColor,
606
- paddingX: 1,
607
- children: /* @__PURE__ */ jsxDEV9(Box9, {
608
- children: [
609
- /* @__PURE__ */ jsxDEV9(Text8, {
610
- color: isSelected ? colors.accent : colors.primaryDim,
611
- children: isSelected ? "● " : " "
612
- }, undefined, false, undefined, this),
613
- /* @__PURE__ */ jsxDEV9(Text8, {
614
- color: nameColor,
615
- bold: true,
616
- wrap: "truncate",
617
- children: theme.name
618
- }, undefined, false, undefined, this),
619
- /* @__PURE__ */ jsxDEV9(Text8, {
620
- color: colors.primaryDim,
621
- children: indicatorText
622
- }, undefined, false, undefined, this)
623
- ]
624
- }, undefined, true, undefined, this)
783
+ // src/components/LoadingPanel.tsx
784
+ import { Spinner } from "@inkjs/ui";
785
+ import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime";
786
+ function LoadingPanel({ title, label = "Processing..." }) {
787
+ return /* @__PURE__ */ jsxDEV11(Panel, {
788
+ title,
789
+ children: /* @__PURE__ */ jsxDEV11(Spinner, {
790
+ label
791
+ }, undefined, false, undefined, this)
625
792
  }, undefined, false, undefined, this);
626
793
  }
627
794
 
628
- // src/lib/theme-parser.ts
629
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
630
- import { join as join3 } from "path";
631
- function parseYaml(content) {
632
- const result = {};
633
- const lines = content.split(`
634
- `);
635
- let currentSection = null;
636
- let currentKey = "";
637
- for (const line of lines) {
638
- const trimmed = line.trim();
639
- if (!trimmed || trimmed.startsWith("#"))
640
- continue;
641
- const indentLevel = line.search(/\S/);
642
- const match = trimmed.match(/^([\w-]+):\s*(.*)$/);
643
- if (match) {
644
- const [, key, value] = match;
645
- if (indentLevel === 0) {
646
- if (value) {
647
- result[key] = value.replace(/^["']|["']$/g, "");
648
- } else {
649
- currentKey = key;
650
- currentSection = {};
651
- result[key] = currentSection;
652
- }
653
- } else if (currentSection) {
654
- currentSection[key] = value.replace(/^["']|["']$/g, "");
655
- }
656
- }
657
- }
658
- return result;
659
- }
660
- async function parseThemeMetadata(themePath) {
661
- const yamlPath = join3(themePath, "theme.yaml");
662
- if (!existsSync2(yamlPath)) {
663
- return;
664
- }
665
- try {
666
- const content = await readText(yamlPath);
667
- const parsed = parseYaml(content);
668
- return {
669
- name: parsed.name || "",
670
- author: parsed.author,
671
- description: parsed.description,
672
- version: parsed.version,
673
- source: parsed.source,
674
- colors: parsed.colors
675
- };
676
- } catch {
677
- return;
678
- }
679
- }
680
- function parseThemeFiles(themePath) {
681
- const entries = readdirSync2(themePath, { withFileTypes: true });
682
- return entries.filter((e) => e.isFile() && !e.name.startsWith(".") && e.name !== "theme.yaml" && e.name !== "light.mode").map((e) => ({
683
- name: e.name,
684
- path: join3(themePath, e.name),
685
- application: e.name.replace(/\.(conf|theme|lua|toml|css|json|ini)$/, "")
686
- }));
687
- }
688
- async function parseTheme(themePath, themeName) {
689
- const files = parseThemeFiles(themePath);
690
- const metadata = await parseThemeMetadata(themePath);
795
+ // src/hooks/useMenuAction.ts
796
+ import { useState as useState4, useCallback } from "react";
797
+ function useMenuAction() {
798
+ const [state, setState] = useState4("menu");
799
+ const [output, setOutput] = useState4("");
800
+ const [success, setSuccess] = useState4(true);
801
+ const execute = useCallback(async (action) => {
802
+ setState("running");
803
+ const result = await action();
804
+ setOutput(result.output);
805
+ setSuccess(result.success);
806
+ setState("result");
807
+ }, []);
808
+ const reset = useCallback(() => {
809
+ setState("menu");
810
+ }, []);
691
811
  return {
692
- name: metadata?.name || themeName,
693
- path: themePath,
694
- files,
695
- metadata,
696
- hasBackgrounds: existsSync2(join3(themePath, "backgrounds")),
697
- hasPreview: existsSync2(join3(themePath, "preview.png")),
698
- isLightMode: existsSync2(join3(themePath, "light.mode"))
812
+ state,
813
+ output,
814
+ success,
815
+ isRunning: state === "running",
816
+ isResult: state === "result",
817
+ execute,
818
+ reset
699
819
  };
700
820
  }
701
821
 
822
+ // src/hooks/useBackNavigation.ts
823
+ import { useInput as useInput4 } from "ink";
824
+ function useBackNavigation({
825
+ enabled = true,
826
+ onBack
827
+ }) {
828
+ useInput4((input, key) => {
829
+ if (enabled && (key.escape || key.leftArrow || input === "h")) {
830
+ onBack();
831
+ }
832
+ });
833
+ }
834
+
702
835
  // src/cli/config-manager.ts
703
836
  import { parseArgs } from "util";
704
- import { readdirSync as readdirSync3, existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync as readlinkSync2 } from "fs";
837
+ import { readdirSync as readdirSync2, existsSync as existsSync2, lstatSync as lstatSync2, readlinkSync as readlinkSync2 } from "fs";
705
838
  var colors2 = {
706
839
  red: "\x1B[0;31m",
707
840
  green: "\x1B[0;32m",
@@ -717,7 +850,7 @@ async function checkStow() {
717
850
  }
718
851
  }
719
852
  function listPackages() {
720
- const entries = readdirSync3(CONFIGS_DIR, { withFileTypes: true });
853
+ const entries = readdirSync2(CONFIGS_DIR, { withFileTypes: true });
721
854
  return entries.filter((e) => e.isDirectory()).map((e) => ({
722
855
  name: e.name,
723
856
  path: `${CONFIGS_DIR}/${e.name}`,
@@ -726,12 +859,12 @@ function listPackages() {
726
859
  }
727
860
  function checkPackageStowed(packageName) {
728
861
  const packageDir = `${CONFIGS_DIR}/${packageName}`;
729
- if (!existsSync3(packageDir))
862
+ if (!existsSync2(packageDir))
730
863
  return false;
731
- const entries = readdirSync3(packageDir, { withFileTypes: true });
864
+ const entries = readdirSync2(packageDir, { withFileTypes: true });
732
865
  for (const entry of entries) {
733
866
  const targetPath = `${HOME_DIR}/${entry.name}`;
734
- if (!existsSync3(targetPath))
867
+ if (!existsSync2(targetPath))
735
868
  return false;
736
869
  try {
737
870
  const stat = lstatSync2(targetPath);
@@ -996,11 +1129,179 @@ if (isMainModule) {
996
1129
  main().catch(console.error);
997
1130
  }
998
1131
 
1132
+ // src/components/menus/ConfigMenu.tsx
1133
+ import { jsxDEV as jsxDEV12 } from "react/jsx-dev-runtime";
1134
+ function ConfigMenu({ onBack }) {
1135
+ const { state, output, success, isRunning, isResult, execute, reset } = useMenuAction();
1136
+ useBackNavigation({ enabled: state === "menu", onBack });
1137
+ const handleAction = async (action) => {
1138
+ if (action === "back") {
1139
+ onBack();
1140
+ return;
1141
+ }
1142
+ await execute(() => runConfigManager([action]));
1143
+ };
1144
+ if (isRunning) {
1145
+ return /* @__PURE__ */ jsxDEV12(LoadingPanel, {
1146
+ title: "Config Manager"
1147
+ }, undefined, false, undefined, this);
1148
+ }
1149
+ if (isResult) {
1150
+ return /* @__PURE__ */ jsxDEV12(CommandOutput, {
1151
+ title: "Config Manager",
1152
+ output,
1153
+ success,
1154
+ onDismiss: reset
1155
+ }, undefined, false, undefined, this);
1156
+ }
1157
+ return /* @__PURE__ */ jsxDEV12(Panel, {
1158
+ title: "Config Manager",
1159
+ children: /* @__PURE__ */ jsxDEV12(VimSelect, {
1160
+ options: [
1161
+ { label: "Stow all packages", value: "stow-all" },
1162
+ { label: "Unstow all packages", value: "unstow-all" },
1163
+ { label: "Check status", value: "status" },
1164
+ { label: "List packages", value: "list" },
1165
+ { label: "Back", value: "back" }
1166
+ ],
1167
+ onChange: handleAction
1168
+ }, undefined, false, undefined, this)
1169
+ }, undefined, false, undefined, this);
1170
+ }
1171
+
1172
+ // src/components/menus/PackageMenu.tsx
1173
+ import { useState as useState6, useCallback as useCallback2, useMemo as useMemo2, useRef } from "react";
1174
+ import { Box as Box12, Text as Text11, useInput as useInput7 } from "ink";
1175
+
1176
+ // src/components/ScrollableLog.tsx
1177
+ import { useState as useState5, useEffect as useEffect3, useMemo } from "react";
1178
+ import { Box as Box10, Text as Text9, useInput as useInput5 } from "ink";
1179
+ import { jsxDEV as jsxDEV13 } from "react/jsx-dev-runtime";
1180
+ function ScrollableLog({
1181
+ lines,
1182
+ maxHeight,
1183
+ autoScroll = true,
1184
+ showScrollHint = true
1185
+ }) {
1186
+ const { rows } = useTerminalSize();
1187
+ const visibleLines = maxHeight || Math.max(5, rows - 12);
1188
+ const [scrollOffset, setScrollOffset] = useState5(0);
1189
+ const [isAutoScrolling, setIsAutoScrolling] = useState5(autoScroll);
1190
+ const totalLines = lines.length;
1191
+ const maxOffset = Math.max(0, totalLines - visibleLines);
1192
+ useEffect3(() => {
1193
+ if (isAutoScrolling) {
1194
+ setScrollOffset(maxOffset);
1195
+ }
1196
+ }, [totalLines, maxOffset, isAutoScrolling]);
1197
+ useInput5((input, key) => {
1198
+ if (key.downArrow || input === "j") {
1199
+ setIsAutoScrolling(false);
1200
+ setScrollOffset((prev) => Math.min(prev + 1, maxOffset));
1201
+ }
1202
+ if (key.upArrow || input === "k") {
1203
+ setIsAutoScrolling(false);
1204
+ setScrollOffset((prev) => Math.max(prev - 1, 0));
1205
+ }
1206
+ if (input === "G") {
1207
+ setIsAutoScrolling(true);
1208
+ setScrollOffset(maxOffset);
1209
+ }
1210
+ if (input === "g") {
1211
+ setIsAutoScrolling(false);
1212
+ setScrollOffset(0);
1213
+ }
1214
+ });
1215
+ const visibleContent = useMemo(() => {
1216
+ return lines.slice(scrollOffset, scrollOffset + visibleLines);
1217
+ }, [lines, scrollOffset, visibleLines]);
1218
+ const showScrollUp = scrollOffset > 0;
1219
+ const showScrollDown = scrollOffset < maxOffset;
1220
+ return /* @__PURE__ */ jsxDEV13(Box10, {
1221
+ flexDirection: "column",
1222
+ children: [
1223
+ showScrollHint && showScrollUp && /* @__PURE__ */ jsxDEV13(Text9, {
1224
+ dimColor: true,
1225
+ children: [
1226
+ " ↑ ",
1227
+ scrollOffset,
1228
+ " more line",
1229
+ scrollOffset !== 1 ? "s" : ""
1230
+ ]
1231
+ }, undefined, true, undefined, this),
1232
+ /* @__PURE__ */ jsxDEV13(Box10, {
1233
+ flexDirection: "column",
1234
+ height: visibleLines,
1235
+ overflow: "hidden",
1236
+ children: visibleContent.map((line, i) => /* @__PURE__ */ jsxDEV13(Text9, {
1237
+ children: line
1238
+ }, scrollOffset + i, false, undefined, this))
1239
+ }, undefined, false, undefined, this),
1240
+ showScrollHint && showScrollDown && /* @__PURE__ */ jsxDEV13(Text9, {
1241
+ dimColor: true,
1242
+ children: [
1243
+ " ↓ ",
1244
+ maxOffset - scrollOffset,
1245
+ " more line",
1246
+ maxOffset - scrollOffset !== 1 ? "s" : ""
1247
+ ]
1248
+ }, undefined, true, undefined, this),
1249
+ showScrollHint && totalLines > visibleLines && /* @__PURE__ */ jsxDEV13(Text9, {
1250
+ dimColor: true,
1251
+ children: [
1252
+ "j/k scroll • g top • G bottom ",
1253
+ isAutoScrolling ? "(auto-scroll)" : ""
1254
+ ]
1255
+ }, undefined, true, undefined, this)
1256
+ ]
1257
+ }, undefined, true, undefined, this);
1258
+ }
1259
+
1260
+ // src/components/PromptInput.tsx
1261
+ import { Box as Box11, Text as Text10, useInput as useInput6 } from "ink";
1262
+ import { jsxDEV as jsxDEV14 } from "react/jsx-dev-runtime";
1263
+ function PromptInput({
1264
+ question,
1265
+ options = ["y", "n"],
1266
+ onAnswer
1267
+ }) {
1268
+ useInput6((input) => {
1269
+ const lower = input.toLowerCase();
1270
+ if (options.includes(lower)) {
1271
+ onAnswer(lower);
1272
+ }
1273
+ });
1274
+ return /* @__PURE__ */ jsxDEV14(Box11, {
1275
+ marginTop: 1,
1276
+ borderStyle: "single",
1277
+ borderColor: colors.accent,
1278
+ paddingX: 1,
1279
+ children: /* @__PURE__ */ jsxDEV14(Text10, {
1280
+ children: [
1281
+ question,
1282
+ " ",
1283
+ /* @__PURE__ */ jsxDEV14(Text10, {
1284
+ color: colors.accent,
1285
+ children: [
1286
+ "[",
1287
+ options.join("/"),
1288
+ "]"
1289
+ ]
1290
+ }, undefined, true, undefined, this),
1291
+ /* @__PURE__ */ jsxDEV14(Text10, {
1292
+ dimColor: true,
1293
+ children: ": "
1294
+ }, undefined, false, undefined, this)
1295
+ ]
1296
+ }, undefined, true, undefined, this)
1297
+ }, undefined, false, undefined, this);
1298
+ }
1299
+
999
1300
  // src/cli/pkg-sync.ts
1000
1301
  import { parseArgs as parseArgs2 } from "util";
1001
1302
 
1002
1303
  // src/lib/config.ts
1003
- import { existsSync as existsSync4 } from "fs";
1304
+ import { existsSync as existsSync3 } from "fs";
1004
1305
  var DEFAULT_CONFIG = {
1005
1306
  config: {
1006
1307
  purge: false,
@@ -1015,7 +1316,7 @@ var DEFAULT_CONFIG = {
1015
1316
  async function loadPkgConfig(path) {
1016
1317
  await ensureConfigDir();
1017
1318
  const configPath = path || PKG_CONFIG_PATH;
1018
- if (!existsSync4(configPath)) {
1319
+ if (!existsSync3(configPath)) {
1019
1320
  await savePkgConfig(DEFAULT_CONFIG, configPath);
1020
1321
  return DEFAULT_CONFIG;
1021
1322
  }
@@ -1027,7 +1328,7 @@ async function savePkgConfig(config, path) {
1027
1328
  await writeFile(configPath, JSON.stringify(config, null, 2));
1028
1329
  }
1029
1330
  async function loadPkgLock() {
1030
- if (!existsSync4(PKG_LOCK_PATH)) {
1331
+ if (!existsSync3(PKG_LOCK_PATH)) {
1031
1332
  return null;
1032
1333
  }
1033
1334
  return readJson(PKG_LOCK_PATH);
@@ -1203,6 +1504,15 @@ var colors3 = {
1203
1504
  bold: "\x1B[1m",
1204
1505
  reset: "\x1B[0m"
1205
1506
  };
1507
+ async function runCommand(command, callbacks, cwd, needsTTY = false) {
1508
+ if (callbacks) {
1509
+ if (needsTTY) {
1510
+ return execStreamingWithTTY(command, callbacks.onLog, cwd);
1511
+ }
1512
+ return execStreaming(command, callbacks.onLog, cwd);
1513
+ }
1514
+ return execLive(command, cwd);
1515
+ }
1206
1516
  async function checkDependencies() {
1207
1517
  if (!await commandExists("brew")) {
1208
1518
  console.error(`${colors3.red}Error: Homebrew not installed${colors3.reset}`);
@@ -1238,34 +1548,35 @@ async function getOutdatedMas() {
1238
1548
  return null;
1239
1549
  }).filter((app) => app !== null);
1240
1550
  }
1241
- async function upgradeWithVerification() {
1551
+ async function upgradeWithVerification(cb = null) {
1552
+ const log = cb?.onLog ?? console.log;
1242
1553
  const result = {
1243
1554
  attempted: [],
1244
1555
  succeeded: [],
1245
1556
  failed: [],
1246
1557
  stillOutdated: []
1247
1558
  };
1248
- console.log(`
1559
+ log(`
1249
1560
  ${colors3.cyan}=== Checking for updates ===${colors3.reset}
1250
1561
  `);
1251
- await execLive(["brew", "update"]);
1562
+ await runCommand(["brew", "update"], cb);
1252
1563
  const beforeUpgrade = await getOutdatedPackages();
1253
1564
  result.attempted = beforeUpgrade.map((p) => p.name);
1254
1565
  if (beforeUpgrade.length === 0) {
1255
- console.log(`
1566
+ log(`
1256
1567
  ${colors3.green}All brew packages are up to date${colors3.reset}`);
1257
1568
  } else {
1258
- console.log(`
1569
+ log(`
1259
1570
  ${colors3.yellow}Found ${beforeUpgrade.length} outdated packages${colors3.reset}
1260
1571
  `);
1261
- console.log(`${colors3.cyan}=== Upgrading formulas ===${colors3.reset}
1572
+ log(`${colors3.cyan}=== Upgrading formulas ===${colors3.reset}
1262
1573
  `);
1263
- await execLive(["brew", "upgrade", "--formula"]);
1264
- console.log(`
1574
+ await runCommand(["brew", "upgrade", "--formula"], cb);
1575
+ log(`
1265
1576
  ${colors3.cyan}=== Upgrading casks ===${colors3.reset}
1266
1577
  `);
1267
- await execLive(["brew", "upgrade", "--cask", "--greedy"]);
1268
- console.log(`
1578
+ await runCommand(["brew", "upgrade", "--cask", "--greedy"], cb);
1579
+ log(`
1269
1580
  ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
1270
1581
  `);
1271
1582
  const afterUpgrade = await getOutdatedPackages();
@@ -1278,13 +1589,13 @@ ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
1278
1589
  }
1279
1590
  }
1280
1591
  if (result.stillOutdated.length > 0) {
1281
- console.log(`${colors3.yellow}${result.stillOutdated.length} packages still outdated, retrying individually...${colors3.reset}
1592
+ log(`${colors3.yellow}${result.stillOutdated.length} packages still outdated, retrying individually...${colors3.reset}
1282
1593
  `);
1283
1594
  for (const pkgName of [...result.stillOutdated]) {
1284
1595
  const pkg = afterUpgrade.find((p) => p.name === pkgName);
1285
1596
  if (!pkg)
1286
1597
  continue;
1287
- console.log(` Retrying ${colors3.blue}${pkgName}${colors3.reset}...`);
1598
+ log(` Retrying ${colors3.blue}${pkgName}${colors3.reset}...`);
1288
1599
  const upgradeCmd = pkg.type === "cask" ? ["brew", "upgrade", "--cask", pkgName] : ["brew", "upgrade", pkgName];
1289
1600
  const retryResult = await exec(upgradeCmd);
1290
1601
  const checkResult = await exec([
@@ -1298,11 +1609,11 @@ ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
1298
1609
  if (!stillOutdatedNow.includes(pkgName)) {
1299
1610
  result.succeeded.push(pkgName);
1300
1611
  result.stillOutdated = result.stillOutdated.filter((n) => n !== pkgName);
1301
- console.log(` ${colors3.green}✓ Success${colors3.reset}`);
1612
+ log(` ${colors3.green}✓ Success${colors3.reset}`);
1302
1613
  } else {
1303
1614
  result.failed.push(pkgName);
1304
1615
  result.stillOutdated = result.stillOutdated.filter((n) => n !== pkgName);
1305
- console.log(` ${colors3.red}✗ Failed${colors3.reset} ${retryResult.stderr ? `(${retryResult.stderr.split(`
1616
+ log(` ${colors3.red}✗ Failed${colors3.reset} ${retryResult.stderr ? `(${retryResult.stderr.split(`
1306
1617
  `)[0]})` : ""}`);
1307
1618
  }
1308
1619
  }
@@ -1311,74 +1622,77 @@ ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
1311
1622
  if (await commandExists("mas")) {
1312
1623
  const masOutdated = await getOutdatedMas();
1313
1624
  if (masOutdated.length > 0) {
1314
- console.log(`
1625
+ log(`
1315
1626
  ${colors3.cyan}=== Upgrading Mac App Store apps ===${colors3.reset}
1316
1627
  `);
1317
- await execLive(["mas", "upgrade"]);
1628
+ await runCommand(["mas", "upgrade"], cb, undefined, true);
1318
1629
  }
1319
1630
  }
1320
- console.log(`
1631
+ log(`
1321
1632
  ${colors3.cyan}=== Cleanup ===${colors3.reset}
1322
1633
  `);
1323
- await execLive(["brew", "cleanup"]);
1324
- console.log(`
1634
+ await runCommand(["brew", "cleanup"], cb);
1635
+ log(`
1325
1636
  ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
1326
1637
  `);
1327
1638
  const lock = await updateLockfile();
1328
1639
  const lockTotal = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
1329
- console.log(` Locked ${lockTotal} packages`);
1640
+ log(` Locked ${lockTotal} packages`);
1330
1641
  return result;
1331
1642
  }
1332
- async function upgradeInteractive() {
1333
- console.log(`
1643
+ async function upgradeInteractive(cb = null) {
1644
+ const log = cb?.onLog ?? console.log;
1645
+ const askPrompt = cb?.onPrompt ?? (async (q) => (prompt(q) || "").trim().toLowerCase());
1646
+ log(`
1334
1647
  ${colors3.cyan}=== Checking for updates ===${colors3.reset}
1335
1648
  `);
1336
- await execLive(["brew", "update"]);
1649
+ await runCommand(["brew", "update"], cb);
1337
1650
  const outdated = await getOutdatedPackages();
1338
1651
  if (outdated.length === 0) {
1339
- console.log(`
1652
+ log(`
1340
1653
  ${colors3.green}All packages are up to date${colors3.reset}
1341
1654
  `);
1342
1655
  return;
1343
1656
  }
1344
- console.log(`
1657
+ log(`
1345
1658
  ${colors3.yellow}Found ${outdated.length} outdated packages${colors3.reset}
1346
1659
  `);
1347
1660
  for (const pkg of outdated) {
1348
- const question = `Upgrade ${colors3.blue}${pkg.name}${colors3.reset} (${pkg.type})? [y/n/q]: `;
1349
- const answer = (prompt(question) || "").trim().toLowerCase();
1661
+ const question = `Upgrade ${colors3.blue}${pkg.name}${colors3.reset} (${pkg.type})?`;
1662
+ const answer = await askPrompt(question, ["y", "n", "q"]);
1350
1663
  if (answer === "q") {
1351
- console.log(`
1664
+ log(`
1352
1665
  ${colors3.yellow}Upgrade cancelled${colors3.reset}`);
1353
1666
  return;
1354
1667
  }
1355
1668
  if (answer === "y" || answer === "yes") {
1356
1669
  const cmd = pkg.type === "cask" ? ["brew", "upgrade", "--cask", pkg.name] : ["brew", "upgrade", pkg.name];
1357
- await execLive(cmd);
1670
+ await runCommand(cmd, cb);
1358
1671
  }
1359
1672
  }
1360
1673
  const stillOutdated = await getOutdatedPackages();
1361
1674
  if (stillOutdated.length > 0) {
1362
- console.log(`
1675
+ log(`
1363
1676
  ${colors3.yellow}Still outdated: ${stillOutdated.map((p) => p.name).join(", ")}${colors3.reset}`);
1364
1677
  } else {
1365
- console.log(`
1678
+ log(`
1366
1679
  ${colors3.green}All selected packages upgraded successfully${colors3.reset}`);
1367
1680
  }
1368
- await execLive(["brew", "cleanup"]);
1369
- console.log(`
1681
+ await runCommand(["brew", "cleanup"], cb);
1682
+ log(`
1370
1683
  ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
1371
1684
  `);
1372
1685
  await updateLockfile();
1373
1686
  }
1374
- async function syncPackages(config) {
1687
+ async function syncPackages(config, cb = null) {
1688
+ const log = cb?.onLog ?? console.log;
1375
1689
  if (config.config.autoUpdate) {
1376
- console.log(`
1690
+ log(`
1377
1691
  ${colors3.cyan}=== Updating Homebrew ===${colors3.reset}
1378
1692
  `);
1379
- await execLive(["brew", "update"]);
1693
+ await runCommand(["brew", "update"], cb);
1380
1694
  }
1381
- console.log(`
1695
+ log(`
1382
1696
  ${colors3.cyan}=== Installing taps ===${colors3.reset}
1383
1697
  `);
1384
1698
  const tappedResult = await exec(["brew", "tap"]);
@@ -1386,34 +1700,34 @@ ${colors3.cyan}=== Installing taps ===${colors3.reset}
1386
1700
  `).filter(Boolean);
1387
1701
  for (const tap of config.taps) {
1388
1702
  if (!tapped.includes(tap)) {
1389
- console.log(` Adding tap: ${colors3.blue}${tap}${colors3.reset}`);
1390
- await execLive(["brew", "tap", tap]);
1703
+ log(` Adding tap: ${colors3.blue}${tap}${colors3.reset}`);
1704
+ await runCommand(["brew", "tap", tap], cb);
1391
1705
  }
1392
1706
  }
1393
- console.log(`
1707
+ log(`
1394
1708
  ${colors3.cyan}=== Installing packages ===${colors3.reset}
1395
1709
  `);
1396
1710
  const installedFormulas = (await exec(["brew", "list", "--formula"])).stdout.split(`
1397
1711
  `).filter(Boolean);
1398
1712
  for (const pkg of config.packages) {
1399
1713
  if (!installedFormulas.includes(pkg)) {
1400
- console.log(` Installing: ${colors3.blue}${pkg}${colors3.reset}`);
1401
- await execLive(["brew", "install", pkg]);
1714
+ log(` Installing: ${colors3.blue}${pkg}${colors3.reset}`);
1715
+ await runCommand(["brew", "install", pkg], cb);
1402
1716
  }
1403
1717
  }
1404
- console.log(`
1718
+ log(`
1405
1719
  ${colors3.cyan}=== Installing casks ===${colors3.reset}
1406
1720
  `);
1407
1721
  const installedCasks = (await exec(["brew", "list", "--cask"])).stdout.split(`
1408
1722
  `).filter(Boolean);
1409
1723
  for (const cask of config.casks) {
1410
1724
  if (!installedCasks.includes(cask)) {
1411
- console.log(` Installing: ${colors3.blue}${cask}${colors3.reset}`);
1412
- await execLive(["brew", "install", "--cask", cask]);
1725
+ log(` Installing: ${colors3.blue}${cask}${colors3.reset}`);
1726
+ await runCommand(["brew", "install", "--cask", cask], cb);
1413
1727
  }
1414
1728
  }
1415
1729
  if (await commandExists("mas")) {
1416
- console.log(`
1730
+ log(`
1417
1731
  ${colors3.cyan}=== Installing Mac App Store apps ===${colors3.reset}
1418
1732
  `);
1419
1733
  const masResult = await exec(["mas", "list"]);
@@ -1424,26 +1738,28 @@ ${colors3.cyan}=== Installing Mac App Store apps ===${colors3.reset}
1424
1738
  });
1425
1739
  for (const [name, id] of Object.entries(config.mas)) {
1426
1740
  if (!installedMas.includes(id)) {
1427
- console.log(` Installing: ${colors3.blue}${name}${colors3.reset}`);
1428
- await execLive(["mas", "install", String(id)]);
1741
+ log(` Installing: ${colors3.blue}${name}${colors3.reset}`);
1742
+ await runCommand(["mas", "install", String(id)], cb, undefined, true);
1429
1743
  }
1430
1744
  }
1431
1745
  }
1432
1746
  if (config.config.purge) {
1433
- await purgeUnlisted(config, config.config.purgeInteractive);
1747
+ await purgeUnlisted(config, config.config.purgeInteractive, cb);
1434
1748
  }
1435
- console.log(`
1749
+ log(`
1436
1750
  ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
1437
1751
  `);
1438
1752
  const lock = await updateLockfile();
1439
1753
  const lockTotal = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
1440
- console.log(` Locked ${lockTotal} packages`);
1441
- console.log(`
1754
+ log(` Locked ${lockTotal} packages`);
1755
+ log(`
1442
1756
  ${colors3.green}=== Sync complete ===${colors3.reset}
1443
1757
  `);
1444
1758
  }
1445
- async function purgeUnlisted(config, interactive) {
1446
- console.log(`
1759
+ async function purgeUnlisted(config, interactive, cb = null) {
1760
+ const log = cb?.onLog ?? console.log;
1761
+ const askPrompt = cb?.onPrompt ?? (async (q) => (prompt(q) || "").trim().toLowerCase());
1762
+ log(`
1447
1763
  ${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
1448
1764
  `);
1449
1765
  const installedFormulas = (await exec(["brew", "list", "--formula"])).stdout.split(`
@@ -1452,17 +1768,17 @@ ${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
1452
1768
  if (!config.packages.includes(pkg)) {
1453
1769
  const usesResult = await exec(["brew", "uses", "--installed", pkg]);
1454
1770
  if (usesResult.stdout.trim()) {
1455
- console.log(` ${colors3.yellow}Skipping ${pkg} (has dependents)${colors3.reset}`);
1771
+ log(` ${colors3.yellow}Skipping ${pkg} (has dependents)${colors3.reset}`);
1456
1772
  continue;
1457
1773
  }
1458
1774
  if (interactive) {
1459
- const answer = (prompt(` Remove ${colors3.red}${pkg}${colors3.reset}? [y/n]: `) || "").trim().toLowerCase();
1775
+ const answer = await askPrompt(`Remove ${colors3.red}${pkg}${colors3.reset}?`, ["y", "n"]);
1460
1776
  if (answer === "y") {
1461
- await execLive(["brew", "uninstall", pkg]);
1777
+ await runCommand(["brew", "uninstall", pkg], cb);
1462
1778
  }
1463
1779
  } else {
1464
- console.log(` Removing: ${colors3.red}${pkg}${colors3.reset}`);
1465
- await execLive(["brew", "uninstall", pkg]);
1780
+ log(` Removing: ${colors3.red}${pkg}${colors3.reset}`);
1781
+ await runCommand(["brew", "uninstall", pkg], cb);
1466
1782
  }
1467
1783
  }
1468
1784
  }
@@ -1471,13 +1787,13 @@ ${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
1471
1787
  for (const cask of installedCasks) {
1472
1788
  if (!config.casks.includes(cask)) {
1473
1789
  if (interactive) {
1474
- const answer = (prompt(` Remove cask ${colors3.red}${cask}${colors3.reset}? [y/n]: `) || "").trim().toLowerCase();
1790
+ const answer = await askPrompt(`Remove cask ${colors3.red}${cask}${colors3.reset}?`, ["y", "n"]);
1475
1791
  if (answer === "y") {
1476
- await execLive(["brew", "uninstall", "--cask", cask]);
1792
+ await runCommand(["brew", "uninstall", "--cask", cask], cb);
1477
1793
  }
1478
1794
  } else {
1479
- console.log(` Removing cask: ${colors3.red}${cask}${colors3.reset}`);
1480
- await execLive(["brew", "uninstall", "--cask", cask]);
1795
+ log(` Removing cask: ${colors3.red}${cask}${colors3.reset}`);
1796
+ await runCommand(["brew", "uninstall", "--cask", cask], cb);
1481
1797
  }
1482
1798
  }
1483
1799
  }
@@ -1495,22 +1811,22 @@ ${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
1495
1811
  }
1496
1812
  if (!configMasIds.includes(app.id)) {
1497
1813
  if (interactive) {
1498
- const answer = (prompt(` Remove app ${colors3.red}${app.name}${colors3.reset}? [y/n]: `) || "").trim().toLowerCase();
1814
+ const answer = await askPrompt(`Remove app ${colors3.red}${app.name}${colors3.reset}?`, ["y", "n"]);
1499
1815
  if (answer === "y") {
1500
- await execLive(["mas", "uninstall", String(app.id)]);
1816
+ await runCommand(["mas", "uninstall", String(app.id)], cb, undefined, true);
1501
1817
  }
1502
1818
  } else {
1503
- console.log(` Removing app: ${colors3.red}${app.name}${colors3.reset}`);
1504
- await execLive(["mas", "uninstall", String(app.id)]);
1819
+ log(` Removing app: ${colors3.red}${app.name}${colors3.reset}`);
1820
+ await runCommand(["mas", "uninstall", String(app.id)], cb, undefined, true);
1505
1821
  }
1506
1822
  }
1507
1823
  }
1508
1824
  }
1509
- console.log(`
1825
+ log(`
1510
1826
  ${colors3.cyan}=== Cleaning up ===${colors3.reset}
1511
1827
  `);
1512
- await execLive(["brew", "autoremove"]);
1513
- await execLive(["brew", "cleanup"]);
1828
+ await runCommand(["brew", "autoremove"], cb);
1829
+ await runCommand(["brew", "cleanup"], cb);
1514
1830
  }
1515
1831
  function printUsage2() {
1516
1832
  console.log(`
@@ -1527,7 +1843,7 @@ Examples:
1527
1843
  bun run pkg-sync --purge Sync and remove unlisted packages
1528
1844
  `);
1529
1845
  }
1530
- async function runPkgSync(args) {
1846
+ async function runPkgSyncWithCallbacks(args, callbacks) {
1531
1847
  const { values, positionals } = parseArgs2({
1532
1848
  args,
1533
1849
  options: {
@@ -1539,12 +1855,19 @@ async function runPkgSync(args) {
1539
1855
  allowPositionals: true
1540
1856
  });
1541
1857
  try {
1542
- await checkDependencies();
1858
+ if (!await commandExists("brew")) {
1859
+ callbacks.onLog(`${colors3.red}Error: Homebrew not installed${colors3.reset}`);
1860
+ return { output: "Homebrew not installed", success: false };
1861
+ }
1543
1862
  } catch {
1544
1863
  return { output: "Homebrew not installed", success: false };
1545
1864
  }
1865
+ if (values["upgrade-interactive"]) {
1866
+ await upgradeInteractive(callbacks);
1867
+ return { output: "Interactive upgrade complete", success: true };
1868
+ }
1546
1869
  if (values["upgrade-only"]) {
1547
- const result = await upgradeWithVerification();
1870
+ const result = await upgradeWithVerification(callbacks);
1548
1871
  let output = `Upgrade complete
1549
1872
  `;
1550
1873
  if (result.succeeded.length > 0) {
@@ -1561,7 +1884,7 @@ async function runPkgSync(args) {
1561
1884
  if (values.purge) {
1562
1885
  config.config.purge = true;
1563
1886
  }
1564
- await syncPackages(config);
1887
+ await syncPackages(config, callbacks);
1565
1888
  return { output: "Sync complete", success: true };
1566
1889
  }
1567
1890
  async function main2() {
@@ -1786,60 +2109,411 @@ if (isMainModule3) {
1786
2109
  main3().catch(console.error);
1787
2110
  }
1788
2111
 
1789
- // src/cli/set-theme.ts
1790
- import { parseArgs as parseArgs4 } from "util";
1791
- import { readdirSync as readdirSync4, existsSync as existsSync5, rmSync, symlinkSync, unlinkSync } from "fs";
1792
- import { join as join4 } from "path";
1793
- var colors5 = {
1794
- red: "\x1B[0;31m",
1795
- green: "\x1B[0;32m",
1796
- blue: "\x1B[0;34m",
1797
- yellow: "\x1B[1;33m",
1798
- cyan: "\x1B[0;36m",
1799
- dim: "\x1B[2m",
1800
- reset: "\x1B[0m"
1801
- };
1802
- async function listThemes() {
1803
- await ensureConfigDir();
1804
- if (!existsSync5(THEMES_DIR)) {
1805
- return [];
1806
- }
1807
- const entries = readdirSync4(THEMES_DIR, { withFileTypes: true });
1808
- const themes = [];
1809
- for (const entry of entries) {
1810
- if (entry.isDirectory()) {
1811
- const themePath = join4(THEMES_DIR, entry.name);
1812
- const theme = await parseTheme(themePath, entry.name);
1813
- themes.push(theme);
2112
+ // src/components/menus/PackageMenu.tsx
2113
+ import { jsxDEV as jsxDEV15 } from "react/jsx-dev-runtime";
2114
+ function PackageMenu({ onBack }) {
2115
+ const [state, setState] = useState6("menu");
2116
+ const [lines, setLines] = useState6([]);
2117
+ const [output, setOutput] = useState6("");
2118
+ const [isStreamingOp, setIsStreamingOp] = useState6(true);
2119
+ const [pendingPrompt, setPendingPrompt] = useState6(null);
2120
+ const [success, setSuccess] = useState6(true);
2121
+ const isRunningRef = useRef(false);
2122
+ useInput7((input, key) => {
2123
+ if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
2124
+ onBack();
1814
2125
  }
1815
- }
1816
- return themes;
1817
- }
1818
- function clearDirectory(dir) {
1819
- if (existsSync5(dir)) {
1820
- const entries = readdirSync4(dir, { withFileTypes: true });
1821
- for (const entry of entries) {
1822
- const fullPath = join4(dir, entry.name);
1823
- if (entry.isSymbolicLink() || entry.isFile()) {
1824
- unlinkSync(fullPath);
1825
- } else if (entry.isDirectory()) {
1826
- rmSync(fullPath, { recursive: true, force: true });
1827
- }
2126
+ if (state === "result") {
2127
+ setState("menu");
2128
+ setLines([]);
1828
2129
  }
1829
- }
1830
- }
1831
- function createSymlink(source, target) {
1832
- if (existsSync5(target)) {
1833
- unlinkSync(target);
1834
- }
1835
- symlinkSync(source, target);
1836
- }
1837
- async function applyTheme(themeName) {
1838
- const themeDir = join4(THEMES_DIR, themeName);
1839
- if (!existsSync5(themeDir)) {
1840
- return { output: `Theme '${themeName}' not found`, success: false };
1841
- }
1842
- await ensureConfigDir();
2130
+ });
2131
+ const callbacks = useMemo2(() => ({
2132
+ onLog: (line) => {
2133
+ setLines((prev) => [...prev, line]);
2134
+ },
2135
+ onPrompt: (question, options) => {
2136
+ return new Promise((resolve) => {
2137
+ setPendingPrompt({ question, options, resolve });
2138
+ });
2139
+ }
2140
+ }), []);
2141
+ const handlePromptAnswer = useCallback2((answer) => {
2142
+ if (pendingPrompt) {
2143
+ setLines((prev) => [...prev, `> ${answer}`]);
2144
+ pendingPrompt.resolve(answer);
2145
+ setPendingPrompt(null);
2146
+ }
2147
+ }, [pendingPrompt]);
2148
+ const handleAction = async (action) => {
2149
+ if (action === "back") {
2150
+ onBack();
2151
+ return;
2152
+ }
2153
+ if (isRunningRef.current)
2154
+ return;
2155
+ isRunningRef.current = true;
2156
+ setState("running");
2157
+ setLines([]);
2158
+ setOutput("");
2159
+ setPendingPrompt(null);
2160
+ let result;
2161
+ switch (action) {
2162
+ case "sync":
2163
+ setIsStreamingOp(true);
2164
+ result = await runPkgSyncWithCallbacks([], callbacks);
2165
+ break;
2166
+ case "sync-purge":
2167
+ setIsStreamingOp(true);
2168
+ result = await runPkgSyncWithCallbacks(["--purge"], callbacks);
2169
+ break;
2170
+ case "upgrade":
2171
+ setIsStreamingOp(true);
2172
+ result = await runPkgSyncWithCallbacks(["--upgrade-only"], callbacks);
2173
+ break;
2174
+ case "upgrade-interactive":
2175
+ setIsStreamingOp(true);
2176
+ result = await runPkgSyncWithCallbacks(["--upgrade-interactive"], callbacks);
2177
+ break;
2178
+ case "lock-update":
2179
+ setIsStreamingOp(false);
2180
+ result = await runPkgLock(["update"]);
2181
+ setOutput(result.output);
2182
+ break;
2183
+ case "lock-status":
2184
+ setIsStreamingOp(false);
2185
+ result = await runPkgLock(["status"]);
2186
+ setOutput(result.output);
2187
+ break;
2188
+ default:
2189
+ setIsStreamingOp(false);
2190
+ result = { output: "Unknown action", success: false };
2191
+ setOutput(result.output);
2192
+ }
2193
+ setSuccess(result.success);
2194
+ setState("result");
2195
+ isRunningRef.current = false;
2196
+ };
2197
+ if (state === "running") {
2198
+ if (!isStreamingOp) {
2199
+ return /* @__PURE__ */ jsxDEV15(LoadingPanel, {
2200
+ title: "Package Sync"
2201
+ }, undefined, false, undefined, this);
2202
+ }
2203
+ return /* @__PURE__ */ jsxDEV15(Panel, {
2204
+ title: "Package Sync",
2205
+ children: [
2206
+ /* @__PURE__ */ jsxDEV15(ScrollableLog, {
2207
+ lines
2208
+ }, undefined, false, undefined, this),
2209
+ pendingPrompt && /* @__PURE__ */ jsxDEV15(PromptInput, {
2210
+ question: pendingPrompt.question,
2211
+ options: pendingPrompt.options,
2212
+ onAnswer: handlePromptAnswer
2213
+ }, undefined, false, undefined, this)
2214
+ ]
2215
+ }, undefined, true, undefined, this);
2216
+ }
2217
+ if (state === "result") {
2218
+ if (!isStreamingOp) {
2219
+ return /* @__PURE__ */ jsxDEV15(CommandOutput, {
2220
+ title: "Package Sync",
2221
+ output,
2222
+ success,
2223
+ onDismiss: () => setState("menu")
2224
+ }, undefined, false, undefined, this);
2225
+ }
2226
+ return /* @__PURE__ */ jsxDEV15(Panel, {
2227
+ title: "Package Sync",
2228
+ borderColor: success ? colors.success : colors.error,
2229
+ children: [
2230
+ /* @__PURE__ */ jsxDEV15(ScrollableLog, {
2231
+ lines,
2232
+ autoScroll: false
2233
+ }, undefined, false, undefined, this),
2234
+ /* @__PURE__ */ jsxDEV15(Box12, {
2235
+ marginTop: 1,
2236
+ children: /* @__PURE__ */ jsxDEV15(Text11, {
2237
+ color: success ? colors.success : colors.error,
2238
+ children: success ? "Done" : "Failed"
2239
+ }, undefined, false, undefined, this)
2240
+ }, undefined, false, undefined, this),
2241
+ /* @__PURE__ */ jsxDEV15(Text11, {
2242
+ dimColor: true,
2243
+ children: "Press any key to continue..."
2244
+ }, undefined, false, undefined, this)
2245
+ ]
2246
+ }, undefined, true, undefined, this);
2247
+ }
2248
+ return /* @__PURE__ */ jsxDEV15(Panel, {
2249
+ title: "Package Sync",
2250
+ children: /* @__PURE__ */ jsxDEV15(VimSelect, {
2251
+ options: [
2252
+ { label: "Sync packages", value: "sync" },
2253
+ { label: "Sync with purge", value: "sync-purge" },
2254
+ { label: "Upgrade all (with verification)", value: "upgrade" },
2255
+ { label: "Upgrade interactive", value: "upgrade-interactive" },
2256
+ { label: "Update lockfile", value: "lock-update" },
2257
+ { label: "Lockfile status", value: "lock-status" },
2258
+ { label: "Back", value: "back" }
2259
+ ],
2260
+ onChange: handleAction
2261
+ }, undefined, false, undefined, this)
2262
+ }, undefined, false, undefined, this);
2263
+ }
2264
+
2265
+ // src/components/menus/ThemeMenu.tsx
2266
+ import { useState as useState8, useEffect as useEffect5, useMemo as useMemo4 } from "react";
2267
+ import { Box as Box14, Text as Text13 } from "ink";
2268
+ import { existsSync as existsSync6, readdirSync as readdirSync5 } from "fs";
2269
+ import { join as join5 } from "path";
2270
+
2271
+ // src/components/ThemeCard.tsx
2272
+ import { Box as Box13, Text as Text12 } from "ink";
2273
+ import { jsxDEV as jsxDEV16 } from "react/jsx-dev-runtime";
2274
+ function ThemeCard({ theme, isSelected, width }) {
2275
+ const borderColor = isSelected ? colors.accent : colors.border;
2276
+ const nameColor = isSelected ? colors.primary : colors.text;
2277
+ const indicators = [];
2278
+ if (theme.hasBackgrounds)
2279
+ indicators.push("bg");
2280
+ if (theme.isLightMode)
2281
+ indicators.push("light");
2282
+ const indicatorText = indicators.length > 0 ? ` [${indicators.join(" ")}]` : "";
2283
+ return /* @__PURE__ */ jsxDEV16(Box13, {
2284
+ flexDirection: "column",
2285
+ width,
2286
+ borderStyle: borderStyles.panel,
2287
+ borderColor,
2288
+ paddingX: 1,
2289
+ children: /* @__PURE__ */ jsxDEV16(Box13, {
2290
+ children: [
2291
+ /* @__PURE__ */ jsxDEV16(Text12, {
2292
+ color: isSelected ? colors.accent : colors.primaryDim,
2293
+ children: isSelected ? "● " : " "
2294
+ }, undefined, false, undefined, this),
2295
+ /* @__PURE__ */ jsxDEV16(Text12, {
2296
+ color: nameColor,
2297
+ bold: true,
2298
+ wrap: "truncate",
2299
+ children: theme.name
2300
+ }, undefined, false, undefined, this),
2301
+ /* @__PURE__ */ jsxDEV16(Text12, {
2302
+ color: colors.primaryDim,
2303
+ children: indicatorText
2304
+ }, undefined, false, undefined, this)
2305
+ ]
2306
+ }, undefined, true, undefined, this)
2307
+ }, undefined, false, undefined, this);
2308
+ }
2309
+
2310
+ // src/hooks/useThemeGrid.ts
2311
+ import { useState as useState7, useEffect as useEffect4 } from "react";
2312
+ import { useInput as useInput8 } from "ink";
2313
+ function useThemeGrid({
2314
+ itemCount,
2315
+ cardHeight = 3,
2316
+ layoutOverhead = 20,
2317
+ minCardWidth = 28,
2318
+ onSelect,
2319
+ onBack,
2320
+ enabled = true
2321
+ }) {
2322
+ const { columns, rows } = useTerminalSize();
2323
+ const [selectedIndex, setSelectedIndex] = useState7(0);
2324
+ const [scrollOffset, setScrollOffset] = useState7(0);
2325
+ const availableWidth = columns - 6;
2326
+ const cardsPerRow = Math.max(1, Math.floor(availableWidth / minCardWidth));
2327
+ const cardWidth = Math.floor(availableWidth / cardsPerRow);
2328
+ const availableHeight = rows - layoutOverhead;
2329
+ const visibleRows = Math.max(1, Math.floor(availableHeight / cardHeight));
2330
+ const selectedRow = Math.floor(selectedIndex / cardsPerRow);
2331
+ const totalRows = Math.ceil(itemCount / cardsPerRow);
2332
+ useEffect4(() => {
2333
+ if (selectedRow < scrollOffset) {
2334
+ setScrollOffset(selectedRow);
2335
+ } else if (selectedRow >= scrollOffset + visibleRows) {
2336
+ setScrollOffset(selectedRow - visibleRows + 1);
2337
+ }
2338
+ }, [selectedRow, scrollOffset, visibleRows]);
2339
+ useInput8((input, key) => {
2340
+ if (!enabled)
2341
+ return;
2342
+ if (key.escape && onBack) {
2343
+ onBack();
2344
+ return;
2345
+ }
2346
+ if (key.rightArrow || input === "l") {
2347
+ if (selectedIndex < itemCount - 1) {
2348
+ setSelectedIndex((i) => i + 1);
2349
+ }
2350
+ }
2351
+ if (key.leftArrow || input === "h") {
2352
+ if (selectedIndex > 0) {
2353
+ setSelectedIndex((i) => i - 1);
2354
+ }
2355
+ }
2356
+ if (key.downArrow || input === "j") {
2357
+ const nextIndex = selectedIndex + cardsPerRow;
2358
+ if (nextIndex < itemCount) {
2359
+ setSelectedIndex(nextIndex);
2360
+ }
2361
+ }
2362
+ if (key.upArrow || input === "k") {
2363
+ const prevIndex = selectedIndex - cardsPerRow;
2364
+ if (prevIndex >= 0) {
2365
+ setSelectedIndex(prevIndex);
2366
+ }
2367
+ }
2368
+ if (key.return && onSelect) {
2369
+ onSelect(selectedIndex);
2370
+ }
2371
+ });
2372
+ const visibleStartIndex = scrollOffset * cardsPerRow;
2373
+ const visibleEndIndex = (scrollOffset + visibleRows) * cardsPerRow;
2374
+ return {
2375
+ cardsPerRow,
2376
+ cardWidth,
2377
+ visibleRows,
2378
+ scrollOffset,
2379
+ selectedIndex,
2380
+ visibleStartIndex,
2381
+ visibleEndIndex,
2382
+ showScrollUp: scrollOffset > 0,
2383
+ showScrollDown: scrollOffset + visibleRows < totalRows,
2384
+ gridHeight: visibleRows * cardHeight,
2385
+ totalRows
2386
+ };
2387
+ }
2388
+
2389
+ // src/lib/theme-parser.ts
2390
+ import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
2391
+ import { join as join3 } from "path";
2392
+ function parseYaml(content) {
2393
+ const result = {};
2394
+ const lines = content.split(`
2395
+ `);
2396
+ let currentSection = null;
2397
+ let currentKey = "";
2398
+ for (const line of lines) {
2399
+ const trimmed = line.trim();
2400
+ if (!trimmed || trimmed.startsWith("#"))
2401
+ continue;
2402
+ const indentLevel = line.search(/\S/);
2403
+ const match = trimmed.match(/^([\w-]+):\s*(.*)$/);
2404
+ if (match) {
2405
+ const [, key, value] = match;
2406
+ if (indentLevel === 0) {
2407
+ if (value) {
2408
+ result[key] = value.replace(/^["']|["']$/g, "");
2409
+ } else {
2410
+ currentKey = key;
2411
+ currentSection = {};
2412
+ result[key] = currentSection;
2413
+ }
2414
+ } else if (currentSection) {
2415
+ currentSection[key] = value.replace(/^["']|["']$/g, "");
2416
+ }
2417
+ }
2418
+ }
2419
+ return result;
2420
+ }
2421
+ async function parseThemeMetadata(themePath) {
2422
+ const yamlPath = join3(themePath, "theme.yaml");
2423
+ if (!existsSync4(yamlPath)) {
2424
+ return;
2425
+ }
2426
+ try {
2427
+ const content = await readText(yamlPath);
2428
+ const parsed = parseYaml(content);
2429
+ return {
2430
+ name: parsed.name || "",
2431
+ author: parsed.author,
2432
+ description: parsed.description,
2433
+ version: parsed.version,
2434
+ source: parsed.source,
2435
+ colors: parsed.colors
2436
+ };
2437
+ } catch {
2438
+ return;
2439
+ }
2440
+ }
2441
+ function parseThemeFiles(themePath) {
2442
+ const entries = readdirSync3(themePath, { withFileTypes: true });
2443
+ return entries.filter((e) => e.isFile() && !e.name.startsWith(".") && e.name !== "theme.yaml" && e.name !== "light.mode").map((e) => ({
2444
+ name: e.name,
2445
+ path: join3(themePath, e.name),
2446
+ application: e.name.replace(/\.(conf|theme|lua|toml|css|json|ini)$/, "")
2447
+ }));
2448
+ }
2449
+ async function parseTheme(themePath, themeName) {
2450
+ const files = parseThemeFiles(themePath);
2451
+ const metadata = await parseThemeMetadata(themePath);
2452
+ return {
2453
+ name: metadata?.name || themeName,
2454
+ path: themePath,
2455
+ files,
2456
+ metadata,
2457
+ hasBackgrounds: existsSync4(join3(themePath, "backgrounds")),
2458
+ hasPreview: existsSync4(join3(themePath, "preview.png")),
2459
+ isLightMode: existsSync4(join3(themePath, "light.mode"))
2460
+ };
2461
+ }
2462
+
2463
+ // src/cli/set-theme.ts
2464
+ import { parseArgs as parseArgs4 } from "util";
2465
+ import { readdirSync as readdirSync4, existsSync as existsSync5, rmSync, symlinkSync, unlinkSync } from "fs";
2466
+ import { join as join4 } from "path";
2467
+ var colors5 = {
2468
+ red: "\x1B[0;31m",
2469
+ green: "\x1B[0;32m",
2470
+ blue: "\x1B[0;34m",
2471
+ yellow: "\x1B[1;33m",
2472
+ cyan: "\x1B[0;36m",
2473
+ dim: "\x1B[2m",
2474
+ reset: "\x1B[0m"
2475
+ };
2476
+ async function listThemes() {
2477
+ await ensureConfigDir();
2478
+ if (!existsSync5(THEMES_DIR)) {
2479
+ return [];
2480
+ }
2481
+ const entries = readdirSync4(THEMES_DIR, { withFileTypes: true });
2482
+ const themes = [];
2483
+ for (const entry of entries) {
2484
+ if (entry.isDirectory()) {
2485
+ const themePath = join4(THEMES_DIR, entry.name);
2486
+ const theme = await parseTheme(themePath, entry.name);
2487
+ themes.push(theme);
2488
+ }
2489
+ }
2490
+ return themes;
2491
+ }
2492
+ function clearDirectory(dir) {
2493
+ if (existsSync5(dir)) {
2494
+ const entries = readdirSync4(dir, { withFileTypes: true });
2495
+ for (const entry of entries) {
2496
+ const fullPath = join4(dir, entry.name);
2497
+ if (entry.isSymbolicLink() || entry.isFile()) {
2498
+ unlinkSync(fullPath);
2499
+ } else if (entry.isDirectory()) {
2500
+ rmSync(fullPath, { recursive: true, force: true });
2501
+ }
2502
+ }
2503
+ }
2504
+ }
2505
+ function createSymlink(source, target) {
2506
+ if (existsSync5(target)) {
2507
+ unlinkSync(target);
2508
+ }
2509
+ symlinkSync(source, target);
2510
+ }
2511
+ async function applyTheme(themeName) {
2512
+ const themeDir = join4(THEMES_DIR, themeName);
2513
+ if (!existsSync5(themeDir)) {
2514
+ return { output: `Theme '${themeName}' not found`, success: false };
2515
+ }
2516
+ await ensureConfigDir();
1843
2517
  await ensureDir2(THEME_TARGET_DIR);
1844
2518
  const theme = await parseTheme(themeDir, themeName);
1845
2519
  clearDirectory(THEME_TARGET_DIR);
@@ -1956,226 +2630,19 @@ if (isMainModule4) {
1956
2630
  main4().catch(console.error);
1957
2631
  }
1958
2632
 
1959
- // src/cli/formalconf.tsx
1960
- import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
1961
- function MainMenu({ onSelect }) {
1962
- const { exit } = useApp();
1963
- return /* @__PURE__ */ jsxDEV10(Panel, {
1964
- title: "Main Menu",
1965
- children: /* @__PURE__ */ jsxDEV10(VimSelect, {
1966
- options: [
1967
- { label: "Config Manager", value: "config" },
1968
- { label: "Package Sync", value: "packages" },
1969
- { label: "Set Theme", value: "themes" },
1970
- { label: "Exit", value: "exit" }
1971
- ],
1972
- onChange: (value) => {
1973
- if (value === "exit") {
1974
- exit();
1975
- return;
1976
- }
1977
- onSelect(value);
1978
- }
1979
- }, undefined, false, undefined, this)
1980
- }, undefined, false, undefined, this);
1981
- }
1982
- function ConfigMenu({ onBack }) {
1983
- const [state, setState] = useState4("menu");
1984
- const [output, setOutput] = useState4("");
1985
- const [success, setSuccess] = useState4(true);
1986
- useInput3((input, key) => {
1987
- if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
1988
- onBack();
1989
- }
1990
- });
1991
- const handleAction = async (action) => {
1992
- if (action === "back") {
1993
- onBack();
1994
- return;
1995
- }
1996
- setState("running");
1997
- const result = await runConfigManager([action]);
1998
- setOutput(result.output);
1999
- setSuccess(result.success);
2000
- setState("result");
2001
- };
2002
- if (state === "running") {
2003
- return /* @__PURE__ */ jsxDEV10(Panel, {
2004
- title: "Config Manager",
2005
- children: /* @__PURE__ */ jsxDEV10(Spinner, {
2006
- label: "Processing..."
2007
- }, undefined, false, undefined, this)
2008
- }, undefined, false, undefined, this);
2009
- }
2010
- if (state === "result") {
2011
- return /* @__PURE__ */ jsxDEV10(CommandOutput, {
2012
- title: "Config Manager",
2013
- output,
2014
- success,
2015
- onDismiss: () => setState("menu")
2016
- }, undefined, false, undefined, this);
2017
- }
2018
- return /* @__PURE__ */ jsxDEV10(Panel, {
2019
- title: "Config Manager",
2020
- children: /* @__PURE__ */ jsxDEV10(VimSelect, {
2021
- options: [
2022
- { label: "Stow all packages", value: "stow-all" },
2023
- { label: "Unstow all packages", value: "unstow-all" },
2024
- { label: "Check status", value: "status" },
2025
- { label: "List packages", value: "list" },
2026
- { label: "Back", value: "back" }
2027
- ],
2028
- onChange: handleAction
2029
- }, undefined, false, undefined, this)
2030
- }, undefined, false, undefined, this);
2031
- }
2032
- function PackageMenu({ onBack }) {
2033
- const [state, setState] = useState4("menu");
2034
- const [output, setOutput] = useState4("");
2035
- const [success, setSuccess] = useState4(true);
2036
- useInput3((input, key) => {
2037
- if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
2038
- onBack();
2039
- }
2040
- });
2041
- const handleAction = async (action) => {
2042
- if (action === "back") {
2043
- onBack();
2044
- return;
2045
- }
2046
- setState("running");
2047
- let result;
2048
- switch (action) {
2049
- case "sync":
2050
- result = await runPkgSync([]);
2051
- break;
2052
- case "sync-purge":
2053
- result = await runPkgSync(["--purge"]);
2054
- break;
2055
- case "upgrade":
2056
- result = await runPkgSync(["--upgrade-only"]);
2057
- break;
2058
- case "upgrade-interactive":
2059
- result = await runPkgSync(["--upgrade-interactive"]);
2060
- break;
2061
- case "lock-update":
2062
- result = await runPkgLock(["update"]);
2063
- break;
2064
- case "lock-status":
2065
- result = await runPkgLock(["status"]);
2066
- break;
2067
- default:
2068
- result = { output: "Unknown action", success: false };
2069
- }
2070
- setOutput(result.output);
2071
- setSuccess(result.success);
2072
- setState("result");
2073
- };
2074
- if (state === "running") {
2075
- return /* @__PURE__ */ jsxDEV10(Panel, {
2076
- title: "Package Sync",
2077
- children: /* @__PURE__ */ jsxDEV10(Spinner, {
2078
- label: "Syncing packages..."
2079
- }, undefined, false, undefined, this)
2080
- }, undefined, false, undefined, this);
2081
- }
2082
- if (state === "result") {
2083
- return /* @__PURE__ */ jsxDEV10(CommandOutput, {
2084
- title: "Package Sync",
2085
- output,
2086
- success,
2087
- onDismiss: () => setState("menu")
2088
- }, undefined, false, undefined, this);
2089
- }
2090
- return /* @__PURE__ */ jsxDEV10(Panel, {
2091
- title: "Package Sync",
2092
- children: /* @__PURE__ */ jsxDEV10(VimSelect, {
2093
- options: [
2094
- { label: "Sync packages", value: "sync" },
2095
- { label: "Sync with purge", value: "sync-purge" },
2096
- { label: "Upgrade all (with verification)", value: "upgrade" },
2097
- { label: "Upgrade interactive", value: "upgrade-interactive" },
2098
- { label: "Update lockfile", value: "lock-update" },
2099
- { label: "Lockfile status", value: "lock-status" },
2100
- { label: "Back", value: "back" }
2101
- ],
2102
- onChange: handleAction
2103
- }, undefined, false, undefined, this)
2104
- }, undefined, false, undefined, this);
2105
- }
2633
+ // src/components/menus/ThemeMenu.tsx
2634
+ import { jsxDEV as jsxDEV17 } from "react/jsx-dev-runtime";
2106
2635
  function ThemeMenu({ onBack }) {
2107
- const [themes, setThemes] = useState4([]);
2108
- const [loading, setLoading] = useState4(true);
2109
- const [selectedIndex, setSelectedIndex] = useState4(0);
2110
- const [state, setState] = useState4("menu");
2111
- const [output, setOutput] = useState4("");
2112
- const [success, setSuccess] = useState4(true);
2113
- const { columns, rows } = useTerminalSize();
2114
- const CARD_HEIGHT = 3;
2115
- const LAYOUT_OVERHEAD = 20;
2116
- const cardWidth = useMemo(() => {
2117
- const availableWidth = columns - 6;
2118
- const cardsPerRow2 = Math.max(1, Math.floor(availableWidth / 28));
2119
- return Math.floor(availableWidth / cardsPerRow2);
2120
- }, [columns]);
2121
- const cardsPerRow = useMemo(() => {
2122
- const availableWidth = columns - 6;
2123
- return Math.max(1, Math.floor(availableWidth / 28));
2124
- }, [columns]);
2125
- const visibleRows = useMemo(() => {
2126
- const availableHeight = rows - LAYOUT_OVERHEAD;
2127
- return Math.max(1, Math.floor(availableHeight / CARD_HEIGHT));
2128
- }, [rows]);
2129
- const selectedRow = Math.floor(selectedIndex / cardsPerRow);
2130
- const totalRows = Math.ceil(themes.length / cardsPerRow);
2131
- const [scrollOffset, setScrollOffset] = useState4(0);
2132
- useEffect3(() => {
2133
- if (selectedRow < scrollOffset) {
2134
- setScrollOffset(selectedRow);
2135
- } else if (selectedRow >= scrollOffset + visibleRows) {
2136
- setScrollOffset(selectedRow - visibleRows + 1);
2137
- }
2138
- }, [selectedRow, scrollOffset, visibleRows]);
2139
- const visibleThemes = useMemo(() => {
2140
- const startIdx = scrollOffset * cardsPerRow;
2141
- const endIdx = (scrollOffset + visibleRows) * cardsPerRow;
2142
- return themes.slice(startIdx, endIdx);
2143
- }, [themes, scrollOffset, visibleRows, cardsPerRow]);
2144
- const visibleStartIndex = scrollOffset * cardsPerRow;
2145
- useInput3((input, key) => {
2146
- if (state !== "menu" || loading)
2147
- return;
2148
- if (key.escape) {
2149
- onBack();
2150
- return;
2151
- }
2152
- if (key.rightArrow || input === "l") {
2153
- if (selectedIndex < themes.length - 1) {
2154
- setSelectedIndex((i) => i + 1);
2155
- }
2156
- }
2157
- if (key.leftArrow || input === "h") {
2158
- if (selectedIndex > 0) {
2159
- setSelectedIndex((i) => i - 1);
2160
- }
2161
- }
2162
- if (key.downArrow || input === "j") {
2163
- const nextIndex = selectedIndex + cardsPerRow;
2164
- if (nextIndex < themes.length) {
2165
- setSelectedIndex(nextIndex);
2166
- }
2167
- }
2168
- if (key.upArrow || input === "k") {
2169
- const prevIndex = selectedIndex - cardsPerRow;
2170
- if (prevIndex >= 0) {
2171
- setSelectedIndex(prevIndex);
2172
- }
2173
- }
2174
- if (key.return) {
2175
- applyTheme2(themes[selectedIndex]);
2176
- }
2636
+ const [themes, setThemes] = useState8([]);
2637
+ const [loading, setLoading] = useState8(true);
2638
+ const { state, output, success, isRunning, isResult, execute, reset } = useMenuAction();
2639
+ const grid = useThemeGrid({
2640
+ itemCount: themes.length,
2641
+ onSelect: (index) => applyTheme2(themes[index]),
2642
+ onBack,
2643
+ enabled: state === "menu" && !loading && themes.length > 0
2177
2644
  });
2178
- useEffect3(() => {
2645
+ useEffect5(() => {
2179
2646
  async function loadThemes() {
2180
2647
  if (!existsSync6(THEMES_DIR)) {
2181
2648
  setThemes([]);
@@ -2197,52 +2664,49 @@ function ThemeMenu({ onBack }) {
2197
2664
  loadThemes();
2198
2665
  }, []);
2199
2666
  const applyTheme2 = async (theme) => {
2200
- setState("running");
2201
2667
  const themeName = theme.path.split("/").pop();
2202
- const result = await runSetTheme(themeName);
2203
- setOutput(result.output);
2204
- setSuccess(result.success);
2205
- setState("result");
2668
+ await execute(() => runSetTheme(themeName));
2206
2669
  };
2207
- if (loading || state === "running") {
2208
- return /* @__PURE__ */ jsxDEV10(Panel, {
2670
+ const visibleThemes = useMemo4(() => {
2671
+ return themes.slice(grid.visibleStartIndex, grid.visibleEndIndex);
2672
+ }, [themes, grid.visibleStartIndex, grid.visibleEndIndex]);
2673
+ if (loading || isRunning) {
2674
+ return /* @__PURE__ */ jsxDEV17(LoadingPanel, {
2209
2675
  title: "Select Theme",
2210
- children: /* @__PURE__ */ jsxDEV10(Spinner, {
2211
- label: loading ? "Loading themes..." : "Applying theme..."
2212
- }, undefined, false, undefined, this)
2676
+ label: loading ? "Loading themes..." : "Applying theme..."
2213
2677
  }, undefined, false, undefined, this);
2214
2678
  }
2215
- if (state === "result") {
2216
- return /* @__PURE__ */ jsxDEV10(CommandOutput, {
2679
+ if (isResult) {
2680
+ return /* @__PURE__ */ jsxDEV17(CommandOutput, {
2217
2681
  title: "Select Theme",
2218
2682
  output,
2219
2683
  success,
2220
- onDismiss: () => setState("menu")
2684
+ onDismiss: reset
2221
2685
  }, undefined, false, undefined, this);
2222
2686
  }
2223
2687
  if (themes.length === 0) {
2224
- return /* @__PURE__ */ jsxDEV10(Panel, {
2688
+ return /* @__PURE__ */ jsxDEV17(Panel, {
2225
2689
  title: "Select Theme",
2226
2690
  children: [
2227
- /* @__PURE__ */ jsxDEV10(Box10, {
2691
+ /* @__PURE__ */ jsxDEV17(Box14, {
2228
2692
  flexDirection: "column",
2229
2693
  children: [
2230
- /* @__PURE__ */ jsxDEV10(Text9, {
2694
+ /* @__PURE__ */ jsxDEV17(Text13, {
2231
2695
  color: colors.warning,
2232
2696
  children: "No themes available."
2233
2697
  }, undefined, false, undefined, this),
2234
- /* @__PURE__ */ jsxDEV10(Text9, {
2698
+ /* @__PURE__ */ jsxDEV17(Text13, {
2235
2699
  children: "This system is compatible with omarchy themes."
2236
2700
  }, undefined, false, undefined, this),
2237
- /* @__PURE__ */ jsxDEV10(Text9, {
2701
+ /* @__PURE__ */ jsxDEV17(Text13, {
2238
2702
  dimColor: true,
2239
2703
  children: "Add themes to ~/.config/formalconf/themes/"
2240
2704
  }, undefined, false, undefined, this)
2241
2705
  ]
2242
2706
  }, undefined, true, undefined, this),
2243
- /* @__PURE__ */ jsxDEV10(Box10, {
2707
+ /* @__PURE__ */ jsxDEV17(Box14, {
2244
2708
  marginTop: 1,
2245
- children: /* @__PURE__ */ jsxDEV10(VimSelect, {
2709
+ children: /* @__PURE__ */ jsxDEV17(VimSelect, {
2246
2710
  options: [{ label: "Back", value: "back" }],
2247
2711
  onChange: () => onBack()
2248
2712
  }, undefined, false, undefined, this)
@@ -2250,44 +2714,43 @@ function ThemeMenu({ onBack }) {
2250
2714
  ]
2251
2715
  }, undefined, true, undefined, this);
2252
2716
  }
2253
- const showScrollUp = scrollOffset > 0;
2254
- const showScrollDown = scrollOffset + visibleRows < totalRows;
2255
- const gridHeight = visibleRows * CARD_HEIGHT;
2256
- return /* @__PURE__ */ jsxDEV10(Panel, {
2717
+ return /* @__PURE__ */ jsxDEV17(Panel, {
2257
2718
  title: "Select Theme",
2258
2719
  children: [
2259
- showScrollUp && /* @__PURE__ */ jsxDEV10(Text9, {
2720
+ grid.showScrollUp && /* @__PURE__ */ jsxDEV17(Text13, {
2260
2721
  dimColor: true,
2261
2722
  children: [
2262
- " ",
2263
- scrollOffset,
2723
+ " ",
2724
+ "↑ ",
2725
+ grid.scrollOffset,
2264
2726
  " more row",
2265
- scrollOffset > 1 ? "s" : ""
2727
+ grid.scrollOffset > 1 ? "s" : ""
2266
2728
  ]
2267
2729
  }, undefined, true, undefined, this),
2268
- /* @__PURE__ */ jsxDEV10(Box10, {
2730
+ /* @__PURE__ */ jsxDEV17(Box14, {
2269
2731
  flexDirection: "row",
2270
2732
  flexWrap: "wrap",
2271
- height: gridHeight,
2733
+ height: grid.gridHeight,
2272
2734
  overflow: "hidden",
2273
- children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV10(ThemeCard, {
2735
+ children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV17(ThemeCard, {
2274
2736
  theme,
2275
- isSelected: visibleStartIndex + index === selectedIndex,
2276
- width: cardWidth
2737
+ isSelected: grid.visibleStartIndex + index === grid.selectedIndex,
2738
+ width: grid.cardWidth
2277
2739
  }, theme.path, false, undefined, this))
2278
2740
  }, undefined, false, undefined, this),
2279
- showScrollDown && /* @__PURE__ */ jsxDEV10(Text9, {
2741
+ grid.showScrollDown && /* @__PURE__ */ jsxDEV17(Text13, {
2280
2742
  dimColor: true,
2281
2743
  children: [
2282
- " ",
2283
- totalRows - scrollOffset - visibleRows,
2744
+ " ",
2745
+ "↓ ",
2746
+ grid.totalRows - grid.scrollOffset - grid.visibleRows,
2284
2747
  " more row",
2285
- totalRows - scrollOffset - visibleRows > 1 ? "s" : ""
2748
+ grid.totalRows - grid.scrollOffset - grid.visibleRows > 1 ? "s" : ""
2286
2749
  ]
2287
2750
  }, undefined, true, undefined, this),
2288
- /* @__PURE__ */ jsxDEV10(Box10, {
2751
+ /* @__PURE__ */ jsxDEV17(Box14, {
2289
2752
  marginTop: 1,
2290
- children: /* @__PURE__ */ jsxDEV10(Text9, {
2753
+ children: /* @__PURE__ */ jsxDEV17(Text13, {
2291
2754
  dimColor: true,
2292
2755
  children: "←→↑↓/hjkl navigate • Enter select • Esc back"
2293
2756
  }, undefined, false, undefined, this)
@@ -2295,45 +2758,71 @@ function ThemeMenu({ onBack }) {
2295
2758
  ]
2296
2759
  }, undefined, true, undefined, this);
2297
2760
  }
2761
+
2762
+ // src/cli/formalconf.tsx
2763
+ import { jsxDEV as jsxDEV18 } from "react/jsx-dev-runtime";
2764
+ var BREADCRUMBS = {
2765
+ main: ["Main"],
2766
+ config: ["Main", "Config Manager"],
2767
+ packages: ["Main", "Package Sync"],
2768
+ themes: ["Main", "Themes"]
2769
+ };
2298
2770
  function App() {
2299
- const [screen, setScreen] = useState4("main");
2300
- const { exit } = useApp();
2301
- useInput3((input) => {
2302
- if (input === "q") {
2771
+ const [appState, setAppState] = useState9("loading");
2772
+ const [missingDeps, setMissingDeps] = useState9([]);
2773
+ const [screen, setScreen] = useState9("main");
2774
+ const { exit } = useApp2();
2775
+ useInput9((input) => {
2776
+ if (input === "q")
2303
2777
  exit();
2304
- }
2305
2778
  });
2306
- useEffect3(() => {
2307
- ensureConfigDir();
2308
- }, []);
2309
- const getBreadcrumb = () => {
2310
- switch (screen) {
2311
- case "config":
2312
- return ["Main", "Config Manager"];
2313
- case "packages":
2314
- return ["Main", "Package Sync"];
2315
- case "themes":
2316
- return ["Main", "Themes"];
2317
- default:
2318
- return ["Main"];
2779
+ useEffect6(() => {
2780
+ async function init() {
2781
+ ensureConfigDir();
2782
+ const result = await checkPrerequisites();
2783
+ if (!result.ok) {
2784
+ setMissingDeps(result.missing);
2785
+ setAppState("error");
2786
+ } else {
2787
+ setAppState("ready");
2788
+ }
2319
2789
  }
2320
- };
2321
- return /* @__PURE__ */ jsxDEV10(Layout, {
2322
- breadcrumb: getBreadcrumb(),
2790
+ init();
2791
+ }, []);
2792
+ if (appState === "loading") {
2793
+ return /* @__PURE__ */ jsxDEV18(Layout, {
2794
+ breadcrumb: ["Loading"],
2795
+ children: /* @__PURE__ */ jsxDEV18(Panel, {
2796
+ title: "FormalConf",
2797
+ children: /* @__PURE__ */ jsxDEV18(Spinner2, {
2798
+ label: "Checking prerequisites..."
2799
+ }, undefined, false, undefined, this)
2800
+ }, undefined, false, undefined, this)
2801
+ }, undefined, false, undefined, this);
2802
+ }
2803
+ if (appState === "error") {
2804
+ return /* @__PURE__ */ jsxDEV18(PrerequisiteError, {
2805
+ missing: missingDeps,
2806
+ onExit: exit
2807
+ }, undefined, false, undefined, this);
2808
+ }
2809
+ const goBack = () => setScreen("main");
2810
+ return /* @__PURE__ */ jsxDEV18(Layout, {
2811
+ breadcrumb: BREADCRUMBS[screen],
2323
2812
  children: [
2324
- screen === "main" && /* @__PURE__ */ jsxDEV10(MainMenu, {
2813
+ screen === "main" && /* @__PURE__ */ jsxDEV18(MainMenu, {
2325
2814
  onSelect: setScreen
2326
2815
  }, undefined, false, undefined, this),
2327
- screen === "config" && /* @__PURE__ */ jsxDEV10(ConfigMenu, {
2328
- onBack: () => setScreen("main")
2816
+ screen === "config" && /* @__PURE__ */ jsxDEV18(ConfigMenu, {
2817
+ onBack: goBack
2329
2818
  }, undefined, false, undefined, this),
2330
- screen === "packages" && /* @__PURE__ */ jsxDEV10(PackageMenu, {
2331
- onBack: () => setScreen("main")
2819
+ screen === "packages" && /* @__PURE__ */ jsxDEV18(PackageMenu, {
2820
+ onBack: goBack
2332
2821
  }, undefined, false, undefined, this),
2333
- screen === "themes" && /* @__PURE__ */ jsxDEV10(ThemeMenu, {
2334
- onBack: () => setScreen("main")
2822
+ screen === "themes" && /* @__PURE__ */ jsxDEV18(ThemeMenu, {
2823
+ onBack: goBack
2335
2824
  }, undefined, false, undefined, this)
2336
2825
  ]
2337
2826
  }, undefined, true, undefined, this);
2338
2827
  }
2339
- render(/* @__PURE__ */ jsxDEV10(App, {}, undefined, false, undefined, this));
2828
+ render(/* @__PURE__ */ jsxDEV18(App, {}, undefined, false, undefined, this));