formalconf 2.0.0 → 2.0.1

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 +575 -151
  3. package/package.json +1 -1
@@ -1,7 +1,7 @@
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";
3
+ import { useState as useState5, useEffect as useEffect4, useMemo as useMemo2, useCallback, useRef } from "react";
4
+ import { render, Box as Box12, Text as Text11, useApp, useInput as useInput5 } from "ink";
5
5
  import { Spinner } from "@inkjs/ui";
6
6
 
7
7
  // src/components/ui/VimSelect.tsx
@@ -170,6 +170,115 @@ async function execLive(command, cwd) {
170
170
  });
171
171
  });
172
172
  }
173
+ async function execStreaming(command, onLine, cwd) {
174
+ if (isBun) {
175
+ const proc = Bun.spawn(command, {
176
+ stdout: "pipe",
177
+ stderr: "pipe",
178
+ cwd
179
+ });
180
+ const processStream = async (stream) => {
181
+ const reader = stream.getReader();
182
+ const decoder = new TextDecoder;
183
+ let buffer = "";
184
+ while (true) {
185
+ const { done, value } = await reader.read();
186
+ if (done)
187
+ break;
188
+ buffer += decoder.decode(value, { stream: true });
189
+ const lines = buffer.split(`
190
+ `);
191
+ buffer = lines.pop() || "";
192
+ for (const line of lines) {
193
+ onLine(line);
194
+ }
195
+ }
196
+ if (buffer) {
197
+ onLine(buffer);
198
+ }
199
+ };
200
+ await Promise.all([
201
+ processStream(proc.stdout),
202
+ processStream(proc.stderr)
203
+ ]);
204
+ return await proc.exited;
205
+ }
206
+ return new Promise((resolve) => {
207
+ const [cmd, ...args] = command;
208
+ const proc = nodeSpawn(cmd, args, { cwd, shell: false });
209
+ const processData = (data) => {
210
+ const text = data.toString();
211
+ const lines = text.split(`
212
+ `);
213
+ for (const line of lines) {
214
+ if (line)
215
+ onLine(line);
216
+ }
217
+ };
218
+ proc.stdout?.on("data", processData);
219
+ proc.stderr?.on("data", processData);
220
+ proc.on("close", (code) => {
221
+ resolve(code ?? 1);
222
+ });
223
+ });
224
+ }
225
+ async function execStreamingWithTTY(command, onLine, cwd) {
226
+ if (isBun) {
227
+ const proc = Bun.spawn(command, {
228
+ stdout: "pipe",
229
+ stderr: "pipe",
230
+ stdin: "inherit",
231
+ cwd
232
+ });
233
+ const processStream = async (stream) => {
234
+ const reader = stream.getReader();
235
+ const decoder = new TextDecoder;
236
+ let buffer = "";
237
+ while (true) {
238
+ const { done, value } = await reader.read();
239
+ if (done)
240
+ break;
241
+ buffer += decoder.decode(value, { stream: true });
242
+ const lines = buffer.split(`
243
+ `);
244
+ buffer = lines.pop() || "";
245
+ for (const line of lines) {
246
+ onLine(line);
247
+ }
248
+ }
249
+ if (buffer) {
250
+ onLine(buffer);
251
+ }
252
+ };
253
+ await Promise.all([
254
+ processStream(proc.stdout),
255
+ processStream(proc.stderr)
256
+ ]);
257
+ return await proc.exited;
258
+ }
259
+ return new Promise((resolve) => {
260
+ const [cmd, ...args] = command;
261
+ const proc = nodeSpawn(cmd, args, {
262
+ cwd,
263
+ shell: false,
264
+ stdio: ["inherit", "pipe", "pipe"]
265
+ });
266
+ const processData = (data) => {
267
+ const text = data.toString();
268
+ const lines = text.split(`
269
+ `);
270
+ for (const line of lines) {
271
+ if (line)
272
+ onLine(line);
273
+ }
274
+ };
275
+ proc.stdout?.on("data", processData);
276
+ proc.stderr?.on("data", processData);
277
+ proc.on("close", (code) => {
278
+ resolve(code ?? 1);
279
+ });
280
+ });
281
+ }
173
282
  async function readJson(path) {
174
283
  if (isBun) {
175
284
  return Bun.file(path).json();
@@ -210,6 +319,19 @@ async function commandExists(cmd) {
210
319
  const result = await exec(["which", cmd]);
211
320
  return result.success;
212
321
  }
322
+ async function checkPrerequisites() {
323
+ const required = [
324
+ { name: "stow", install: "brew install stow" },
325
+ { name: "brew", install: "https://brew.sh" }
326
+ ];
327
+ const missing = [];
328
+ for (const dep of required) {
329
+ if (!await commandExists(dep.name)) {
330
+ missing.push(dep);
331
+ }
332
+ }
333
+ return { ok: missing.length === 0, missing };
334
+ }
213
335
 
214
336
  // src/lib/paths.ts
215
337
  var HOME_DIR = homedir();
@@ -316,7 +438,7 @@ function StatusIndicator({
316
438
  // package.json
317
439
  var package_default = {
318
440
  name: "formalconf",
319
- version: "2.0.0",
441
+ version: "2.0.1",
320
442
  description: "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
321
443
  type: "module",
322
444
  main: "./dist/formalconf.js",
@@ -625,6 +747,130 @@ function ThemeCard({ theme, isSelected, width }) {
625
747
  }, undefined, false, undefined, this);
626
748
  }
627
749
 
750
+ // src/components/ScrollableLog.tsx
751
+ import { useState as useState4, useEffect as useEffect3, useMemo } from "react";
752
+ import { Box as Box10, Text as Text9, useInput as useInput3 } from "ink";
753
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
754
+ function ScrollableLog({
755
+ lines,
756
+ maxHeight,
757
+ autoScroll = true,
758
+ showScrollHint = true
759
+ }) {
760
+ const { rows } = useTerminalSize();
761
+ const visibleLines = maxHeight || Math.max(5, rows - 12);
762
+ const [scrollOffset, setScrollOffset] = useState4(0);
763
+ const [isAutoScrolling, setIsAutoScrolling] = useState4(autoScroll);
764
+ const totalLines = lines.length;
765
+ const maxOffset = Math.max(0, totalLines - visibleLines);
766
+ useEffect3(() => {
767
+ if (isAutoScrolling) {
768
+ setScrollOffset(maxOffset);
769
+ }
770
+ }, [totalLines, maxOffset, isAutoScrolling]);
771
+ useInput3((input, key) => {
772
+ if (key.downArrow || input === "j") {
773
+ setIsAutoScrolling(false);
774
+ setScrollOffset((prev) => Math.min(prev + 1, maxOffset));
775
+ }
776
+ if (key.upArrow || input === "k") {
777
+ setIsAutoScrolling(false);
778
+ setScrollOffset((prev) => Math.max(prev - 1, 0));
779
+ }
780
+ if (input === "G") {
781
+ setIsAutoScrolling(true);
782
+ setScrollOffset(maxOffset);
783
+ }
784
+ if (input === "g") {
785
+ setIsAutoScrolling(false);
786
+ setScrollOffset(0);
787
+ }
788
+ });
789
+ const visibleContent = useMemo(() => {
790
+ return lines.slice(scrollOffset, scrollOffset + visibleLines);
791
+ }, [lines, scrollOffset, visibleLines]);
792
+ const showScrollUp = scrollOffset > 0;
793
+ const showScrollDown = scrollOffset < maxOffset;
794
+ return /* @__PURE__ */ jsxDEV10(Box10, {
795
+ flexDirection: "column",
796
+ children: [
797
+ showScrollHint && showScrollUp && /* @__PURE__ */ jsxDEV10(Text9, {
798
+ dimColor: true,
799
+ children: [
800
+ " ↑ ",
801
+ scrollOffset,
802
+ " more line",
803
+ scrollOffset !== 1 ? "s" : ""
804
+ ]
805
+ }, undefined, true, undefined, this),
806
+ /* @__PURE__ */ jsxDEV10(Box10, {
807
+ flexDirection: "column",
808
+ height: visibleLines,
809
+ overflow: "hidden",
810
+ children: visibleContent.map((line, i) => /* @__PURE__ */ jsxDEV10(Text9, {
811
+ children: line
812
+ }, scrollOffset + i, false, undefined, this))
813
+ }, undefined, false, undefined, this),
814
+ showScrollHint && showScrollDown && /* @__PURE__ */ jsxDEV10(Text9, {
815
+ dimColor: true,
816
+ children: [
817
+ " ↓ ",
818
+ maxOffset - scrollOffset,
819
+ " more line",
820
+ maxOffset - scrollOffset !== 1 ? "s" : ""
821
+ ]
822
+ }, undefined, true, undefined, this),
823
+ showScrollHint && totalLines > visibleLines && /* @__PURE__ */ jsxDEV10(Text9, {
824
+ dimColor: true,
825
+ children: [
826
+ "j/k scroll • g top • G bottom ",
827
+ isAutoScrolling ? "(auto-scroll)" : ""
828
+ ]
829
+ }, undefined, true, undefined, this)
830
+ ]
831
+ }, undefined, true, undefined, this);
832
+ }
833
+
834
+ // src/components/PromptInput.tsx
835
+ import { Box as Box11, Text as Text10, useInput as useInput4 } from "ink";
836
+ import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime";
837
+ function PromptInput({
838
+ question,
839
+ options = ["y", "n"],
840
+ onAnswer
841
+ }) {
842
+ useInput4((input) => {
843
+ const lower = input.toLowerCase();
844
+ if (options.includes(lower)) {
845
+ onAnswer(lower);
846
+ }
847
+ });
848
+ return /* @__PURE__ */ jsxDEV11(Box11, {
849
+ marginTop: 1,
850
+ borderStyle: "single",
851
+ borderColor: colors.accent,
852
+ paddingX: 1,
853
+ children: /* @__PURE__ */ jsxDEV11(Text10, {
854
+ children: [
855
+ question,
856
+ " ",
857
+ /* @__PURE__ */ jsxDEV11(Text10, {
858
+ color: colors.accent,
859
+ children: [
860
+ "[",
861
+ options.join("/"),
862
+ "]"
863
+ ]
864
+ }, undefined, true, undefined, this),
865
+ /* @__PURE__ */ jsxDEV11(Text10, {
866
+ dimColor: true,
867
+ children: ": "
868
+ }, undefined, false, undefined, this)
869
+ ]
870
+ }, undefined, true, undefined, this)
871
+ }, undefined, false, undefined, this);
872
+ }
873
+
628
874
  // src/lib/theme-parser.ts
629
875
  import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
630
876
  import { join as join3 } from "path";
@@ -1203,6 +1449,15 @@ var colors3 = {
1203
1449
  bold: "\x1B[1m",
1204
1450
  reset: "\x1B[0m"
1205
1451
  };
1452
+ async function runCommand(command, callbacks, cwd, needsTTY = false) {
1453
+ if (callbacks) {
1454
+ if (needsTTY) {
1455
+ return execStreamingWithTTY(command, callbacks.onLog, cwd);
1456
+ }
1457
+ return execStreaming(command, callbacks.onLog, cwd);
1458
+ }
1459
+ return execLive(command, cwd);
1460
+ }
1206
1461
  async function checkDependencies() {
1207
1462
  if (!await commandExists("brew")) {
1208
1463
  console.error(`${colors3.red}Error: Homebrew not installed${colors3.reset}`);
@@ -1238,34 +1493,35 @@ async function getOutdatedMas() {
1238
1493
  return null;
1239
1494
  }).filter((app) => app !== null);
1240
1495
  }
1241
- async function upgradeWithVerification() {
1496
+ async function upgradeWithVerification(cb = null) {
1497
+ const log = cb?.onLog ?? console.log;
1242
1498
  const result = {
1243
1499
  attempted: [],
1244
1500
  succeeded: [],
1245
1501
  failed: [],
1246
1502
  stillOutdated: []
1247
1503
  };
1248
- console.log(`
1504
+ log(`
1249
1505
  ${colors3.cyan}=== Checking for updates ===${colors3.reset}
1250
1506
  `);
1251
- await execLive(["brew", "update"]);
1507
+ await runCommand(["brew", "update"], cb);
1252
1508
  const beforeUpgrade = await getOutdatedPackages();
1253
1509
  result.attempted = beforeUpgrade.map((p) => p.name);
1254
1510
  if (beforeUpgrade.length === 0) {
1255
- console.log(`
1511
+ log(`
1256
1512
  ${colors3.green}All brew packages are up to date${colors3.reset}`);
1257
1513
  } else {
1258
- console.log(`
1514
+ log(`
1259
1515
  ${colors3.yellow}Found ${beforeUpgrade.length} outdated packages${colors3.reset}
1260
1516
  `);
1261
- console.log(`${colors3.cyan}=== Upgrading formulas ===${colors3.reset}
1517
+ log(`${colors3.cyan}=== Upgrading formulas ===${colors3.reset}
1262
1518
  `);
1263
- await execLive(["brew", "upgrade", "--formula"]);
1264
- console.log(`
1519
+ await runCommand(["brew", "upgrade", "--formula"], cb);
1520
+ log(`
1265
1521
  ${colors3.cyan}=== Upgrading casks ===${colors3.reset}
1266
1522
  `);
1267
- await execLive(["brew", "upgrade", "--cask", "--greedy"]);
1268
- console.log(`
1523
+ await runCommand(["brew", "upgrade", "--cask", "--greedy"], cb);
1524
+ log(`
1269
1525
  ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
1270
1526
  `);
1271
1527
  const afterUpgrade = await getOutdatedPackages();
@@ -1278,13 +1534,13 @@ ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
1278
1534
  }
1279
1535
  }
1280
1536
  if (result.stillOutdated.length > 0) {
1281
- console.log(`${colors3.yellow}${result.stillOutdated.length} packages still outdated, retrying individually...${colors3.reset}
1537
+ log(`${colors3.yellow}${result.stillOutdated.length} packages still outdated, retrying individually...${colors3.reset}
1282
1538
  `);
1283
1539
  for (const pkgName of [...result.stillOutdated]) {
1284
1540
  const pkg = afterUpgrade.find((p) => p.name === pkgName);
1285
1541
  if (!pkg)
1286
1542
  continue;
1287
- console.log(` Retrying ${colors3.blue}${pkgName}${colors3.reset}...`);
1543
+ log(` Retrying ${colors3.blue}${pkgName}${colors3.reset}...`);
1288
1544
  const upgradeCmd = pkg.type === "cask" ? ["brew", "upgrade", "--cask", pkgName] : ["brew", "upgrade", pkgName];
1289
1545
  const retryResult = await exec(upgradeCmd);
1290
1546
  const checkResult = await exec([
@@ -1298,11 +1554,11 @@ ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
1298
1554
  if (!stillOutdatedNow.includes(pkgName)) {
1299
1555
  result.succeeded.push(pkgName);
1300
1556
  result.stillOutdated = result.stillOutdated.filter((n) => n !== pkgName);
1301
- console.log(` ${colors3.green}✓ Success${colors3.reset}`);
1557
+ log(` ${colors3.green}✓ Success${colors3.reset}`);
1302
1558
  } else {
1303
1559
  result.failed.push(pkgName);
1304
1560
  result.stillOutdated = result.stillOutdated.filter((n) => n !== pkgName);
1305
- console.log(` ${colors3.red}✗ Failed${colors3.reset} ${retryResult.stderr ? `(${retryResult.stderr.split(`
1561
+ log(` ${colors3.red}✗ Failed${colors3.reset} ${retryResult.stderr ? `(${retryResult.stderr.split(`
1306
1562
  `)[0]})` : ""}`);
1307
1563
  }
1308
1564
  }
@@ -1311,74 +1567,77 @@ ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
1311
1567
  if (await commandExists("mas")) {
1312
1568
  const masOutdated = await getOutdatedMas();
1313
1569
  if (masOutdated.length > 0) {
1314
- console.log(`
1570
+ log(`
1315
1571
  ${colors3.cyan}=== Upgrading Mac App Store apps ===${colors3.reset}
1316
1572
  `);
1317
- await execLive(["mas", "upgrade"]);
1573
+ await runCommand(["mas", "upgrade"], cb, undefined, true);
1318
1574
  }
1319
1575
  }
1320
- console.log(`
1576
+ log(`
1321
1577
  ${colors3.cyan}=== Cleanup ===${colors3.reset}
1322
1578
  `);
1323
- await execLive(["brew", "cleanup"]);
1324
- console.log(`
1579
+ await runCommand(["brew", "cleanup"], cb);
1580
+ log(`
1325
1581
  ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
1326
1582
  `);
1327
1583
  const lock = await updateLockfile();
1328
1584
  const lockTotal = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
1329
- console.log(` Locked ${lockTotal} packages`);
1585
+ log(` Locked ${lockTotal} packages`);
1330
1586
  return result;
1331
1587
  }
1332
- async function upgradeInteractive() {
1333
- console.log(`
1588
+ async function upgradeInteractive(cb = null) {
1589
+ const log = cb?.onLog ?? console.log;
1590
+ const askPrompt = cb?.onPrompt ?? (async (q) => (prompt(q) || "").trim().toLowerCase());
1591
+ log(`
1334
1592
  ${colors3.cyan}=== Checking for updates ===${colors3.reset}
1335
1593
  `);
1336
- await execLive(["brew", "update"]);
1594
+ await runCommand(["brew", "update"], cb);
1337
1595
  const outdated = await getOutdatedPackages();
1338
1596
  if (outdated.length === 0) {
1339
- console.log(`
1597
+ log(`
1340
1598
  ${colors3.green}All packages are up to date${colors3.reset}
1341
1599
  `);
1342
1600
  return;
1343
1601
  }
1344
- console.log(`
1602
+ log(`
1345
1603
  ${colors3.yellow}Found ${outdated.length} outdated packages${colors3.reset}
1346
1604
  `);
1347
1605
  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();
1606
+ const question = `Upgrade ${colors3.blue}${pkg.name}${colors3.reset} (${pkg.type})?`;
1607
+ const answer = await askPrompt(question, ["y", "n", "q"]);
1350
1608
  if (answer === "q") {
1351
- console.log(`
1609
+ log(`
1352
1610
  ${colors3.yellow}Upgrade cancelled${colors3.reset}`);
1353
1611
  return;
1354
1612
  }
1355
1613
  if (answer === "y" || answer === "yes") {
1356
1614
  const cmd = pkg.type === "cask" ? ["brew", "upgrade", "--cask", pkg.name] : ["brew", "upgrade", pkg.name];
1357
- await execLive(cmd);
1615
+ await runCommand(cmd, cb);
1358
1616
  }
1359
1617
  }
1360
1618
  const stillOutdated = await getOutdatedPackages();
1361
1619
  if (stillOutdated.length > 0) {
1362
- console.log(`
1620
+ log(`
1363
1621
  ${colors3.yellow}Still outdated: ${stillOutdated.map((p) => p.name).join(", ")}${colors3.reset}`);
1364
1622
  } else {
1365
- console.log(`
1623
+ log(`
1366
1624
  ${colors3.green}All selected packages upgraded successfully${colors3.reset}`);
1367
1625
  }
1368
- await execLive(["brew", "cleanup"]);
1369
- console.log(`
1626
+ await runCommand(["brew", "cleanup"], cb);
1627
+ log(`
1370
1628
  ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
1371
1629
  `);
1372
1630
  await updateLockfile();
1373
1631
  }
1374
- async function syncPackages(config) {
1632
+ async function syncPackages(config, cb = null) {
1633
+ const log = cb?.onLog ?? console.log;
1375
1634
  if (config.config.autoUpdate) {
1376
- console.log(`
1635
+ log(`
1377
1636
  ${colors3.cyan}=== Updating Homebrew ===${colors3.reset}
1378
1637
  `);
1379
- await execLive(["brew", "update"]);
1638
+ await runCommand(["brew", "update"], cb);
1380
1639
  }
1381
- console.log(`
1640
+ log(`
1382
1641
  ${colors3.cyan}=== Installing taps ===${colors3.reset}
1383
1642
  `);
1384
1643
  const tappedResult = await exec(["brew", "tap"]);
@@ -1386,34 +1645,34 @@ ${colors3.cyan}=== Installing taps ===${colors3.reset}
1386
1645
  `).filter(Boolean);
1387
1646
  for (const tap of config.taps) {
1388
1647
  if (!tapped.includes(tap)) {
1389
- console.log(` Adding tap: ${colors3.blue}${tap}${colors3.reset}`);
1390
- await execLive(["brew", "tap", tap]);
1648
+ log(` Adding tap: ${colors3.blue}${tap}${colors3.reset}`);
1649
+ await runCommand(["brew", "tap", tap], cb);
1391
1650
  }
1392
1651
  }
1393
- console.log(`
1652
+ log(`
1394
1653
  ${colors3.cyan}=== Installing packages ===${colors3.reset}
1395
1654
  `);
1396
1655
  const installedFormulas = (await exec(["brew", "list", "--formula"])).stdout.split(`
1397
1656
  `).filter(Boolean);
1398
1657
  for (const pkg of config.packages) {
1399
1658
  if (!installedFormulas.includes(pkg)) {
1400
- console.log(` Installing: ${colors3.blue}${pkg}${colors3.reset}`);
1401
- await execLive(["brew", "install", pkg]);
1659
+ log(` Installing: ${colors3.blue}${pkg}${colors3.reset}`);
1660
+ await runCommand(["brew", "install", pkg], cb);
1402
1661
  }
1403
1662
  }
1404
- console.log(`
1663
+ log(`
1405
1664
  ${colors3.cyan}=== Installing casks ===${colors3.reset}
1406
1665
  `);
1407
1666
  const installedCasks = (await exec(["brew", "list", "--cask"])).stdout.split(`
1408
1667
  `).filter(Boolean);
1409
1668
  for (const cask of config.casks) {
1410
1669
  if (!installedCasks.includes(cask)) {
1411
- console.log(` Installing: ${colors3.blue}${cask}${colors3.reset}`);
1412
- await execLive(["brew", "install", "--cask", cask]);
1670
+ log(` Installing: ${colors3.blue}${cask}${colors3.reset}`);
1671
+ await runCommand(["brew", "install", "--cask", cask], cb);
1413
1672
  }
1414
1673
  }
1415
1674
  if (await commandExists("mas")) {
1416
- console.log(`
1675
+ log(`
1417
1676
  ${colors3.cyan}=== Installing Mac App Store apps ===${colors3.reset}
1418
1677
  `);
1419
1678
  const masResult = await exec(["mas", "list"]);
@@ -1424,26 +1683,28 @@ ${colors3.cyan}=== Installing Mac App Store apps ===${colors3.reset}
1424
1683
  });
1425
1684
  for (const [name, id] of Object.entries(config.mas)) {
1426
1685
  if (!installedMas.includes(id)) {
1427
- console.log(` Installing: ${colors3.blue}${name}${colors3.reset}`);
1428
- await execLive(["mas", "install", String(id)]);
1686
+ log(` Installing: ${colors3.blue}${name}${colors3.reset}`);
1687
+ await runCommand(["mas", "install", String(id)], cb, undefined, true);
1429
1688
  }
1430
1689
  }
1431
1690
  }
1432
1691
  if (config.config.purge) {
1433
- await purgeUnlisted(config, config.config.purgeInteractive);
1692
+ await purgeUnlisted(config, config.config.purgeInteractive, cb);
1434
1693
  }
1435
- console.log(`
1694
+ log(`
1436
1695
  ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
1437
1696
  `);
1438
1697
  const lock = await updateLockfile();
1439
1698
  const lockTotal = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
1440
- console.log(` Locked ${lockTotal} packages`);
1441
- console.log(`
1699
+ log(` Locked ${lockTotal} packages`);
1700
+ log(`
1442
1701
  ${colors3.green}=== Sync complete ===${colors3.reset}
1443
1702
  `);
1444
1703
  }
1445
- async function purgeUnlisted(config, interactive) {
1446
- console.log(`
1704
+ async function purgeUnlisted(config, interactive, cb = null) {
1705
+ const log = cb?.onLog ?? console.log;
1706
+ const askPrompt = cb?.onPrompt ?? (async (q) => (prompt(q) || "").trim().toLowerCase());
1707
+ log(`
1447
1708
  ${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
1448
1709
  `);
1449
1710
  const installedFormulas = (await exec(["brew", "list", "--formula"])).stdout.split(`
@@ -1452,17 +1713,17 @@ ${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
1452
1713
  if (!config.packages.includes(pkg)) {
1453
1714
  const usesResult = await exec(["brew", "uses", "--installed", pkg]);
1454
1715
  if (usesResult.stdout.trim()) {
1455
- console.log(` ${colors3.yellow}Skipping ${pkg} (has dependents)${colors3.reset}`);
1716
+ log(` ${colors3.yellow}Skipping ${pkg} (has dependents)${colors3.reset}`);
1456
1717
  continue;
1457
1718
  }
1458
1719
  if (interactive) {
1459
- const answer = (prompt(` Remove ${colors3.red}${pkg}${colors3.reset}? [y/n]: `) || "").trim().toLowerCase();
1720
+ const answer = await askPrompt(`Remove ${colors3.red}${pkg}${colors3.reset}?`, ["y", "n"]);
1460
1721
  if (answer === "y") {
1461
- await execLive(["brew", "uninstall", pkg]);
1722
+ await runCommand(["brew", "uninstall", pkg], cb);
1462
1723
  }
1463
1724
  } else {
1464
- console.log(` Removing: ${colors3.red}${pkg}${colors3.reset}`);
1465
- await execLive(["brew", "uninstall", pkg]);
1725
+ log(` Removing: ${colors3.red}${pkg}${colors3.reset}`);
1726
+ await runCommand(["brew", "uninstall", pkg], cb);
1466
1727
  }
1467
1728
  }
1468
1729
  }
@@ -1471,13 +1732,13 @@ ${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
1471
1732
  for (const cask of installedCasks) {
1472
1733
  if (!config.casks.includes(cask)) {
1473
1734
  if (interactive) {
1474
- const answer = (prompt(` Remove cask ${colors3.red}${cask}${colors3.reset}? [y/n]: `) || "").trim().toLowerCase();
1735
+ const answer = await askPrompt(`Remove cask ${colors3.red}${cask}${colors3.reset}?`, ["y", "n"]);
1475
1736
  if (answer === "y") {
1476
- await execLive(["brew", "uninstall", "--cask", cask]);
1737
+ await runCommand(["brew", "uninstall", "--cask", cask], cb);
1477
1738
  }
1478
1739
  } else {
1479
- console.log(` Removing cask: ${colors3.red}${cask}${colors3.reset}`);
1480
- await execLive(["brew", "uninstall", "--cask", cask]);
1740
+ log(` Removing cask: ${colors3.red}${cask}${colors3.reset}`);
1741
+ await runCommand(["brew", "uninstall", "--cask", cask], cb);
1481
1742
  }
1482
1743
  }
1483
1744
  }
@@ -1495,22 +1756,22 @@ ${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
1495
1756
  }
1496
1757
  if (!configMasIds.includes(app.id)) {
1497
1758
  if (interactive) {
1498
- const answer = (prompt(` Remove app ${colors3.red}${app.name}${colors3.reset}? [y/n]: `) || "").trim().toLowerCase();
1759
+ const answer = await askPrompt(`Remove app ${colors3.red}${app.name}${colors3.reset}?`, ["y", "n"]);
1499
1760
  if (answer === "y") {
1500
- await execLive(["mas", "uninstall", String(app.id)]);
1761
+ await runCommand(["mas", "uninstall", String(app.id)], cb, undefined, true);
1501
1762
  }
1502
1763
  } else {
1503
- console.log(` Removing app: ${colors3.red}${app.name}${colors3.reset}`);
1504
- await execLive(["mas", "uninstall", String(app.id)]);
1764
+ log(` Removing app: ${colors3.red}${app.name}${colors3.reset}`);
1765
+ await runCommand(["mas", "uninstall", String(app.id)], cb, undefined, true);
1505
1766
  }
1506
1767
  }
1507
1768
  }
1508
1769
  }
1509
- console.log(`
1770
+ log(`
1510
1771
  ${colors3.cyan}=== Cleaning up ===${colors3.reset}
1511
1772
  `);
1512
- await execLive(["brew", "autoremove"]);
1513
- await execLive(["brew", "cleanup"]);
1773
+ await runCommand(["brew", "autoremove"], cb);
1774
+ await runCommand(["brew", "cleanup"], cb);
1514
1775
  }
1515
1776
  function printUsage2() {
1516
1777
  console.log(`
@@ -1527,7 +1788,7 @@ Examples:
1527
1788
  bun run pkg-sync --purge Sync and remove unlisted packages
1528
1789
  `);
1529
1790
  }
1530
- async function runPkgSync(args) {
1791
+ async function runPkgSyncWithCallbacks(args, callbacks) {
1531
1792
  const { values, positionals } = parseArgs2({
1532
1793
  args,
1533
1794
  options: {
@@ -1539,12 +1800,19 @@ async function runPkgSync(args) {
1539
1800
  allowPositionals: true
1540
1801
  });
1541
1802
  try {
1542
- await checkDependencies();
1803
+ if (!await commandExists("brew")) {
1804
+ callbacks.onLog(`${colors3.red}Error: Homebrew not installed${colors3.reset}`);
1805
+ return { output: "Homebrew not installed", success: false };
1806
+ }
1543
1807
  } catch {
1544
1808
  return { output: "Homebrew not installed", success: false };
1545
1809
  }
1810
+ if (values["upgrade-interactive"]) {
1811
+ await upgradeInteractive(callbacks);
1812
+ return { output: "Interactive upgrade complete", success: true };
1813
+ }
1546
1814
  if (values["upgrade-only"]) {
1547
- const result = await upgradeWithVerification();
1815
+ const result = await upgradeWithVerification(callbacks);
1548
1816
  let output = `Upgrade complete
1549
1817
  `;
1550
1818
  if (result.succeeded.length > 0) {
@@ -1561,7 +1829,7 @@ async function runPkgSync(args) {
1561
1829
  if (values.purge) {
1562
1830
  config.config.purge = true;
1563
1831
  }
1564
- await syncPackages(config);
1832
+ await syncPackages(config, callbacks);
1565
1833
  return { output: "Sync complete", success: true };
1566
1834
  }
1567
1835
  async function main2() {
@@ -1957,12 +2225,12 @@ if (isMainModule4) {
1957
2225
  }
1958
2226
 
1959
2227
  // src/cli/formalconf.tsx
1960
- import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
2228
+ import { jsxDEV as jsxDEV12 } from "react/jsx-dev-runtime";
1961
2229
  function MainMenu({ onSelect }) {
1962
2230
  const { exit } = useApp();
1963
- return /* @__PURE__ */ jsxDEV10(Panel, {
2231
+ return /* @__PURE__ */ jsxDEV12(Panel, {
1964
2232
  title: "Main Menu",
1965
- children: /* @__PURE__ */ jsxDEV10(VimSelect, {
2233
+ children: /* @__PURE__ */ jsxDEV12(VimSelect, {
1966
2234
  options: [
1967
2235
  { label: "Config Manager", value: "config" },
1968
2236
  { label: "Package Sync", value: "packages" },
@@ -1980,10 +2248,10 @@ function MainMenu({ onSelect }) {
1980
2248
  }, undefined, false, undefined, this);
1981
2249
  }
1982
2250
  function ConfigMenu({ onBack }) {
1983
- const [state, setState] = useState4("menu");
1984
- const [output, setOutput] = useState4("");
1985
- const [success, setSuccess] = useState4(true);
1986
- useInput3((input, key) => {
2251
+ const [state, setState] = useState5("menu");
2252
+ const [output, setOutput] = useState5("");
2253
+ const [success, setSuccess] = useState5(true);
2254
+ useInput5((input, key) => {
1987
2255
  if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
1988
2256
  onBack();
1989
2257
  }
@@ -2000,24 +2268,24 @@ function ConfigMenu({ onBack }) {
2000
2268
  setState("result");
2001
2269
  };
2002
2270
  if (state === "running") {
2003
- return /* @__PURE__ */ jsxDEV10(Panel, {
2271
+ return /* @__PURE__ */ jsxDEV12(Panel, {
2004
2272
  title: "Config Manager",
2005
- children: /* @__PURE__ */ jsxDEV10(Spinner, {
2273
+ children: /* @__PURE__ */ jsxDEV12(Spinner, {
2006
2274
  label: "Processing..."
2007
2275
  }, undefined, false, undefined, this)
2008
2276
  }, undefined, false, undefined, this);
2009
2277
  }
2010
2278
  if (state === "result") {
2011
- return /* @__PURE__ */ jsxDEV10(CommandOutput, {
2279
+ return /* @__PURE__ */ jsxDEV12(CommandOutput, {
2012
2280
  title: "Config Manager",
2013
2281
  output,
2014
2282
  success,
2015
2283
  onDismiss: () => setState("menu")
2016
2284
  }, undefined, false, undefined, this);
2017
2285
  }
2018
- return /* @__PURE__ */ jsxDEV10(Panel, {
2286
+ return /* @__PURE__ */ jsxDEV12(Panel, {
2019
2287
  title: "Config Manager",
2020
- children: /* @__PURE__ */ jsxDEV10(VimSelect, {
2288
+ children: /* @__PURE__ */ jsxDEV12(VimSelect, {
2021
2289
  options: [
2022
2290
  { label: "Stow all packages", value: "stow-all" },
2023
2291
  { label: "Unstow all packages", value: "unstow-all" },
@@ -2030,66 +2298,145 @@ function ConfigMenu({ onBack }) {
2030
2298
  }, undefined, false, undefined, this);
2031
2299
  }
2032
2300
  function PackageMenu({ onBack }) {
2033
- const [state, setState] = useState4("menu");
2034
- const [output, setOutput] = useState4("");
2035
- const [success, setSuccess] = useState4(true);
2036
- useInput3((input, key) => {
2301
+ const [state, setState] = useState5("menu");
2302
+ const [lines, setLines] = useState5([]);
2303
+ const [output, setOutput] = useState5("");
2304
+ const [isStreamingOp, setIsStreamingOp] = useState5(true);
2305
+ const [pendingPrompt, setPendingPrompt] = useState5(null);
2306
+ const [success, setSuccess] = useState5(true);
2307
+ const isRunningRef = useRef(false);
2308
+ useInput5((input, key) => {
2037
2309
  if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
2038
2310
  onBack();
2039
2311
  }
2312
+ if (state === "result") {
2313
+ setState("menu");
2314
+ setLines([]);
2315
+ }
2040
2316
  });
2317
+ const callbacks = useMemo2(() => ({
2318
+ onLog: (line) => {
2319
+ setLines((prev) => [...prev, line]);
2320
+ },
2321
+ onPrompt: (question, options) => {
2322
+ return new Promise((resolve) => {
2323
+ setPendingPrompt({ question, options, resolve });
2324
+ });
2325
+ }
2326
+ }), []);
2327
+ const handlePromptAnswer = useCallback((answer) => {
2328
+ if (pendingPrompt) {
2329
+ setLines((prev) => [...prev, `> ${answer}`]);
2330
+ pendingPrompt.resolve(answer);
2331
+ setPendingPrompt(null);
2332
+ }
2333
+ }, [pendingPrompt]);
2041
2334
  const handleAction = async (action) => {
2042
2335
  if (action === "back") {
2043
2336
  onBack();
2044
2337
  return;
2045
2338
  }
2339
+ if (isRunningRef.current)
2340
+ return;
2341
+ isRunningRef.current = true;
2046
2342
  setState("running");
2343
+ setLines([]);
2344
+ setOutput("");
2345
+ setPendingPrompt(null);
2047
2346
  let result;
2048
2347
  switch (action) {
2049
2348
  case "sync":
2050
- result = await runPkgSync([]);
2349
+ setIsStreamingOp(true);
2350
+ result = await runPkgSyncWithCallbacks([], callbacks);
2051
2351
  break;
2052
2352
  case "sync-purge":
2053
- result = await runPkgSync(["--purge"]);
2353
+ setIsStreamingOp(true);
2354
+ result = await runPkgSyncWithCallbacks(["--purge"], callbacks);
2054
2355
  break;
2055
2356
  case "upgrade":
2056
- result = await runPkgSync(["--upgrade-only"]);
2357
+ setIsStreamingOp(true);
2358
+ result = await runPkgSyncWithCallbacks(["--upgrade-only"], callbacks);
2057
2359
  break;
2058
2360
  case "upgrade-interactive":
2059
- result = await runPkgSync(["--upgrade-interactive"]);
2361
+ setIsStreamingOp(true);
2362
+ result = await runPkgSyncWithCallbacks(["--upgrade-interactive"], callbacks);
2060
2363
  break;
2061
2364
  case "lock-update":
2365
+ setIsStreamingOp(false);
2062
2366
  result = await runPkgLock(["update"]);
2367
+ setOutput(result.output);
2063
2368
  break;
2064
2369
  case "lock-status":
2370
+ setIsStreamingOp(false);
2065
2371
  result = await runPkgLock(["status"]);
2372
+ setOutput(result.output);
2066
2373
  break;
2067
2374
  default:
2375
+ setIsStreamingOp(false);
2068
2376
  result = { output: "Unknown action", success: false };
2377
+ setOutput(result.output);
2069
2378
  }
2070
- setOutput(result.output);
2071
2379
  setSuccess(result.success);
2072
2380
  setState("result");
2381
+ isRunningRef.current = false;
2073
2382
  };
2074
2383
  if (state === "running") {
2075
- return /* @__PURE__ */ jsxDEV10(Panel, {
2384
+ if (!isStreamingOp) {
2385
+ return /* @__PURE__ */ jsxDEV12(Panel, {
2386
+ title: "Package Sync",
2387
+ children: /* @__PURE__ */ jsxDEV12(Spinner, {
2388
+ label: "Processing..."
2389
+ }, undefined, false, undefined, this)
2390
+ }, undefined, false, undefined, this);
2391
+ }
2392
+ return /* @__PURE__ */ jsxDEV12(Panel, {
2076
2393
  title: "Package Sync",
2077
- children: /* @__PURE__ */ jsxDEV10(Spinner, {
2078
- label: "Syncing packages..."
2079
- }, undefined, false, undefined, this)
2080
- }, undefined, false, undefined, this);
2394
+ children: [
2395
+ /* @__PURE__ */ jsxDEV12(ScrollableLog, {
2396
+ lines
2397
+ }, undefined, false, undefined, this),
2398
+ pendingPrompt && /* @__PURE__ */ jsxDEV12(PromptInput, {
2399
+ question: pendingPrompt.question,
2400
+ options: pendingPrompt.options,
2401
+ onAnswer: handlePromptAnswer
2402
+ }, undefined, false, undefined, this)
2403
+ ]
2404
+ }, undefined, true, undefined, this);
2081
2405
  }
2082
2406
  if (state === "result") {
2083
- return /* @__PURE__ */ jsxDEV10(CommandOutput, {
2407
+ if (!isStreamingOp) {
2408
+ return /* @__PURE__ */ jsxDEV12(CommandOutput, {
2409
+ title: "Package Sync",
2410
+ output,
2411
+ success,
2412
+ onDismiss: () => setState("menu")
2413
+ }, undefined, false, undefined, this);
2414
+ }
2415
+ return /* @__PURE__ */ jsxDEV12(Panel, {
2084
2416
  title: "Package Sync",
2085
- output,
2086
- success,
2087
- onDismiss: () => setState("menu")
2088
- }, undefined, false, undefined, this);
2417
+ borderColor: success ? colors.success : colors.error,
2418
+ children: [
2419
+ /* @__PURE__ */ jsxDEV12(ScrollableLog, {
2420
+ lines,
2421
+ autoScroll: false
2422
+ }, undefined, false, undefined, this),
2423
+ /* @__PURE__ */ jsxDEV12(Box12, {
2424
+ marginTop: 1,
2425
+ children: /* @__PURE__ */ jsxDEV12(Text11, {
2426
+ color: success ? colors.success : colors.error,
2427
+ children: success ? "Done" : "Failed"
2428
+ }, undefined, false, undefined, this)
2429
+ }, undefined, false, undefined, this),
2430
+ /* @__PURE__ */ jsxDEV12(Text11, {
2431
+ dimColor: true,
2432
+ children: "Press any key to continue..."
2433
+ }, undefined, false, undefined, this)
2434
+ ]
2435
+ }, undefined, true, undefined, this);
2089
2436
  }
2090
- return /* @__PURE__ */ jsxDEV10(Panel, {
2437
+ return /* @__PURE__ */ jsxDEV12(Panel, {
2091
2438
  title: "Package Sync",
2092
- children: /* @__PURE__ */ jsxDEV10(VimSelect, {
2439
+ children: /* @__PURE__ */ jsxDEV12(VimSelect, {
2093
2440
  options: [
2094
2441
  { label: "Sync packages", value: "sync" },
2095
2442
  { label: "Sync with purge", value: "sync-purge" },
@@ -2104,45 +2451,45 @@ function PackageMenu({ onBack }) {
2104
2451
  }, undefined, false, undefined, this);
2105
2452
  }
2106
2453
  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);
2454
+ const [themes, setThemes] = useState5([]);
2455
+ const [loading, setLoading] = useState5(true);
2456
+ const [selectedIndex, setSelectedIndex] = useState5(0);
2457
+ const [state, setState] = useState5("menu");
2458
+ const [output, setOutput] = useState5("");
2459
+ const [success, setSuccess] = useState5(true);
2113
2460
  const { columns, rows } = useTerminalSize();
2114
2461
  const CARD_HEIGHT = 3;
2115
2462
  const LAYOUT_OVERHEAD = 20;
2116
- const cardWidth = useMemo(() => {
2463
+ const cardWidth = useMemo2(() => {
2117
2464
  const availableWidth = columns - 6;
2118
2465
  const cardsPerRow2 = Math.max(1, Math.floor(availableWidth / 28));
2119
2466
  return Math.floor(availableWidth / cardsPerRow2);
2120
2467
  }, [columns]);
2121
- const cardsPerRow = useMemo(() => {
2468
+ const cardsPerRow = useMemo2(() => {
2122
2469
  const availableWidth = columns - 6;
2123
2470
  return Math.max(1, Math.floor(availableWidth / 28));
2124
2471
  }, [columns]);
2125
- const visibleRows = useMemo(() => {
2472
+ const visibleRows = useMemo2(() => {
2126
2473
  const availableHeight = rows - LAYOUT_OVERHEAD;
2127
2474
  return Math.max(1, Math.floor(availableHeight / CARD_HEIGHT));
2128
2475
  }, [rows]);
2129
2476
  const selectedRow = Math.floor(selectedIndex / cardsPerRow);
2130
2477
  const totalRows = Math.ceil(themes.length / cardsPerRow);
2131
- const [scrollOffset, setScrollOffset] = useState4(0);
2132
- useEffect3(() => {
2478
+ const [scrollOffset, setScrollOffset] = useState5(0);
2479
+ useEffect4(() => {
2133
2480
  if (selectedRow < scrollOffset) {
2134
2481
  setScrollOffset(selectedRow);
2135
2482
  } else if (selectedRow >= scrollOffset + visibleRows) {
2136
2483
  setScrollOffset(selectedRow - visibleRows + 1);
2137
2484
  }
2138
2485
  }, [selectedRow, scrollOffset, visibleRows]);
2139
- const visibleThemes = useMemo(() => {
2486
+ const visibleThemes = useMemo2(() => {
2140
2487
  const startIdx = scrollOffset * cardsPerRow;
2141
2488
  const endIdx = (scrollOffset + visibleRows) * cardsPerRow;
2142
2489
  return themes.slice(startIdx, endIdx);
2143
2490
  }, [themes, scrollOffset, visibleRows, cardsPerRow]);
2144
2491
  const visibleStartIndex = scrollOffset * cardsPerRow;
2145
- useInput3((input, key) => {
2492
+ useInput5((input, key) => {
2146
2493
  if (state !== "menu" || loading)
2147
2494
  return;
2148
2495
  if (key.escape) {
@@ -2175,7 +2522,7 @@ function ThemeMenu({ onBack }) {
2175
2522
  applyTheme2(themes[selectedIndex]);
2176
2523
  }
2177
2524
  });
2178
- useEffect3(() => {
2525
+ useEffect4(() => {
2179
2526
  async function loadThemes() {
2180
2527
  if (!existsSync6(THEMES_DIR)) {
2181
2528
  setThemes([]);
@@ -2205,15 +2552,15 @@ function ThemeMenu({ onBack }) {
2205
2552
  setState("result");
2206
2553
  };
2207
2554
  if (loading || state === "running") {
2208
- return /* @__PURE__ */ jsxDEV10(Panel, {
2555
+ return /* @__PURE__ */ jsxDEV12(Panel, {
2209
2556
  title: "Select Theme",
2210
- children: /* @__PURE__ */ jsxDEV10(Spinner, {
2557
+ children: /* @__PURE__ */ jsxDEV12(Spinner, {
2211
2558
  label: loading ? "Loading themes..." : "Applying theme..."
2212
2559
  }, undefined, false, undefined, this)
2213
2560
  }, undefined, false, undefined, this);
2214
2561
  }
2215
2562
  if (state === "result") {
2216
- return /* @__PURE__ */ jsxDEV10(CommandOutput, {
2563
+ return /* @__PURE__ */ jsxDEV12(CommandOutput, {
2217
2564
  title: "Select Theme",
2218
2565
  output,
2219
2566
  success,
@@ -2221,28 +2568,28 @@ function ThemeMenu({ onBack }) {
2221
2568
  }, undefined, false, undefined, this);
2222
2569
  }
2223
2570
  if (themes.length === 0) {
2224
- return /* @__PURE__ */ jsxDEV10(Panel, {
2571
+ return /* @__PURE__ */ jsxDEV12(Panel, {
2225
2572
  title: "Select Theme",
2226
2573
  children: [
2227
- /* @__PURE__ */ jsxDEV10(Box10, {
2574
+ /* @__PURE__ */ jsxDEV12(Box12, {
2228
2575
  flexDirection: "column",
2229
2576
  children: [
2230
- /* @__PURE__ */ jsxDEV10(Text9, {
2577
+ /* @__PURE__ */ jsxDEV12(Text11, {
2231
2578
  color: colors.warning,
2232
2579
  children: "No themes available."
2233
2580
  }, undefined, false, undefined, this),
2234
- /* @__PURE__ */ jsxDEV10(Text9, {
2581
+ /* @__PURE__ */ jsxDEV12(Text11, {
2235
2582
  children: "This system is compatible with omarchy themes."
2236
2583
  }, undefined, false, undefined, this),
2237
- /* @__PURE__ */ jsxDEV10(Text9, {
2584
+ /* @__PURE__ */ jsxDEV12(Text11, {
2238
2585
  dimColor: true,
2239
2586
  children: "Add themes to ~/.config/formalconf/themes/"
2240
2587
  }, undefined, false, undefined, this)
2241
2588
  ]
2242
2589
  }, undefined, true, undefined, this),
2243
- /* @__PURE__ */ jsxDEV10(Box10, {
2590
+ /* @__PURE__ */ jsxDEV12(Box12, {
2244
2591
  marginTop: 1,
2245
- children: /* @__PURE__ */ jsxDEV10(VimSelect, {
2592
+ children: /* @__PURE__ */ jsxDEV12(VimSelect, {
2246
2593
  options: [{ label: "Back", value: "back" }],
2247
2594
  onChange: () => onBack()
2248
2595
  }, undefined, false, undefined, this)
@@ -2253,10 +2600,10 @@ function ThemeMenu({ onBack }) {
2253
2600
  const showScrollUp = scrollOffset > 0;
2254
2601
  const showScrollDown = scrollOffset + visibleRows < totalRows;
2255
2602
  const gridHeight = visibleRows * CARD_HEIGHT;
2256
- return /* @__PURE__ */ jsxDEV10(Panel, {
2603
+ return /* @__PURE__ */ jsxDEV12(Panel, {
2257
2604
  title: "Select Theme",
2258
2605
  children: [
2259
- showScrollUp && /* @__PURE__ */ jsxDEV10(Text9, {
2606
+ showScrollUp && /* @__PURE__ */ jsxDEV12(Text11, {
2260
2607
  dimColor: true,
2261
2608
  children: [
2262
2609
  " ↑ ",
@@ -2265,18 +2612,18 @@ function ThemeMenu({ onBack }) {
2265
2612
  scrollOffset > 1 ? "s" : ""
2266
2613
  ]
2267
2614
  }, undefined, true, undefined, this),
2268
- /* @__PURE__ */ jsxDEV10(Box10, {
2615
+ /* @__PURE__ */ jsxDEV12(Box12, {
2269
2616
  flexDirection: "row",
2270
2617
  flexWrap: "wrap",
2271
2618
  height: gridHeight,
2272
2619
  overflow: "hidden",
2273
- children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV10(ThemeCard, {
2620
+ children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV12(ThemeCard, {
2274
2621
  theme,
2275
2622
  isSelected: visibleStartIndex + index === selectedIndex,
2276
2623
  width: cardWidth
2277
2624
  }, theme.path, false, undefined, this))
2278
2625
  }, undefined, false, undefined, this),
2279
- showScrollDown && /* @__PURE__ */ jsxDEV10(Text9, {
2626
+ showScrollDown && /* @__PURE__ */ jsxDEV12(Text11, {
2280
2627
  dimColor: true,
2281
2628
  children: [
2282
2629
  " ↓ ",
@@ -2285,9 +2632,9 @@ function ThemeMenu({ onBack }) {
2285
2632
  totalRows - scrollOffset - visibleRows > 1 ? "s" : ""
2286
2633
  ]
2287
2634
  }, undefined, true, undefined, this),
2288
- /* @__PURE__ */ jsxDEV10(Box10, {
2635
+ /* @__PURE__ */ jsxDEV12(Box12, {
2289
2636
  marginTop: 1,
2290
- children: /* @__PURE__ */ jsxDEV10(Text9, {
2637
+ children: /* @__PURE__ */ jsxDEV12(Text11, {
2291
2638
  dimColor: true,
2292
2639
  children: "←→↑↓/hjkl navigate • Enter select • Esc back"
2293
2640
  }, undefined, false, undefined, this)
@@ -2295,16 +2642,76 @@ function ThemeMenu({ onBack }) {
2295
2642
  ]
2296
2643
  }, undefined, true, undefined, this);
2297
2644
  }
2645
+ function PrerequisiteError({
2646
+ missing,
2647
+ onExit
2648
+ }) {
2649
+ useInput5(() => onExit());
2650
+ return /* @__PURE__ */ jsxDEV12(Layout, {
2651
+ breadcrumb: ["Error"],
2652
+ children: /* @__PURE__ */ jsxDEV12(Panel, {
2653
+ title: "Missing Prerequisites",
2654
+ borderColor: colors.error,
2655
+ children: [
2656
+ /* @__PURE__ */ jsxDEV12(Text11, {
2657
+ color: colors.error,
2658
+ children: "Required tools are not installed:"
2659
+ }, undefined, false, undefined, this),
2660
+ /* @__PURE__ */ jsxDEV12(Box12, {
2661
+ flexDirection: "column",
2662
+ marginTop: 1,
2663
+ children: missing.map((dep) => /* @__PURE__ */ jsxDEV12(Box12, {
2664
+ children: [
2665
+ /* @__PURE__ */ jsxDEV12(Text11, {
2666
+ color: colors.warning,
2667
+ children: [
2668
+ "• ",
2669
+ dep.name
2670
+ ]
2671
+ }, undefined, true, undefined, this),
2672
+ /* @__PURE__ */ jsxDEV12(Text11, {
2673
+ dimColor: true,
2674
+ children: [
2675
+ " — Install: ",
2676
+ dep.install
2677
+ ]
2678
+ }, undefined, true, undefined, this)
2679
+ ]
2680
+ }, dep.name, true, undefined, this))
2681
+ }, undefined, false, undefined, this),
2682
+ /* @__PURE__ */ jsxDEV12(Box12, {
2683
+ marginTop: 1,
2684
+ children: /* @__PURE__ */ jsxDEV12(Text11, {
2685
+ dimColor: true,
2686
+ children: "Press any key to exit..."
2687
+ }, undefined, false, undefined, this)
2688
+ }, undefined, false, undefined, this)
2689
+ ]
2690
+ }, undefined, true, undefined, this)
2691
+ }, undefined, false, undefined, this);
2692
+ }
2298
2693
  function App() {
2299
- const [screen, setScreen] = useState4("main");
2694
+ const [appState, setAppState] = useState5("loading");
2695
+ const [missingDeps, setMissingDeps] = useState5([]);
2696
+ const [screen, setScreen] = useState5("main");
2300
2697
  const { exit } = useApp();
2301
- useInput3((input) => {
2698
+ useInput5((input) => {
2302
2699
  if (input === "q") {
2303
2700
  exit();
2304
2701
  }
2305
2702
  });
2306
- useEffect3(() => {
2307
- ensureConfigDir();
2703
+ useEffect4(() => {
2704
+ async function init() {
2705
+ ensureConfigDir();
2706
+ const result = await checkPrerequisites();
2707
+ if (!result.ok) {
2708
+ setMissingDeps(result.missing);
2709
+ setAppState("error");
2710
+ } else {
2711
+ setAppState("ready");
2712
+ }
2713
+ }
2714
+ init();
2308
2715
  }, []);
2309
2716
  const getBreadcrumb = () => {
2310
2717
  switch (screen) {
@@ -2318,22 +2725,39 @@ function App() {
2318
2725
  return ["Main"];
2319
2726
  }
2320
2727
  };
2321
- return /* @__PURE__ */ jsxDEV10(Layout, {
2728
+ if (appState === "loading") {
2729
+ return /* @__PURE__ */ jsxDEV12(Layout, {
2730
+ breadcrumb: ["Loading"],
2731
+ children: /* @__PURE__ */ jsxDEV12(Panel, {
2732
+ title: "FormalConf",
2733
+ children: /* @__PURE__ */ jsxDEV12(Spinner, {
2734
+ label: "Checking prerequisites..."
2735
+ }, undefined, false, undefined, this)
2736
+ }, undefined, false, undefined, this)
2737
+ }, undefined, false, undefined, this);
2738
+ }
2739
+ if (appState === "error") {
2740
+ return /* @__PURE__ */ jsxDEV12(PrerequisiteError, {
2741
+ missing: missingDeps,
2742
+ onExit: exit
2743
+ }, undefined, false, undefined, this);
2744
+ }
2745
+ return /* @__PURE__ */ jsxDEV12(Layout, {
2322
2746
  breadcrumb: getBreadcrumb(),
2323
2747
  children: [
2324
- screen === "main" && /* @__PURE__ */ jsxDEV10(MainMenu, {
2748
+ screen === "main" && /* @__PURE__ */ jsxDEV12(MainMenu, {
2325
2749
  onSelect: setScreen
2326
2750
  }, undefined, false, undefined, this),
2327
- screen === "config" && /* @__PURE__ */ jsxDEV10(ConfigMenu, {
2751
+ screen === "config" && /* @__PURE__ */ jsxDEV12(ConfigMenu, {
2328
2752
  onBack: () => setScreen("main")
2329
2753
  }, undefined, false, undefined, this),
2330
- screen === "packages" && /* @__PURE__ */ jsxDEV10(PackageMenu, {
2754
+ screen === "packages" && /* @__PURE__ */ jsxDEV12(PackageMenu, {
2331
2755
  onBack: () => setScreen("main")
2332
2756
  }, undefined, false, undefined, this),
2333
- screen === "themes" && /* @__PURE__ */ jsxDEV10(ThemeMenu, {
2757
+ screen === "themes" && /* @__PURE__ */ jsxDEV12(ThemeMenu, {
2334
2758
  onBack: () => setScreen("main")
2335
2759
  }, undefined, false, undefined, this)
2336
2760
  ]
2337
2761
  }, undefined, true, undefined, this);
2338
2762
  }
2339
- render(/* @__PURE__ */ jsxDEV10(App, {}, undefined, false, undefined, this));
2763
+ render(/* @__PURE__ */ jsxDEV12(App, {}, undefined, false, undefined, this));