formalconf 2.0.6 → 2.0.8

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 (2) hide show
  1. package/dist/formalconf.js +2235 -541
  2. package/package.json +1 -1
@@ -1,54 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // src/cli/formalconf.tsx
3
- import { useState as useState11, useEffect as useEffect6 } from "react";
4
- import { render, useApp as useApp2, useInput as useInput11 } from "ink";
5
- import { Spinner as Spinner2 } from "@inkjs/ui";
6
-
7
- // src/components/layout/Layout.tsx
8
- import { Box as Box5 } from "ink";
9
-
10
- // src/hooks/useTerminalSize.ts
11
- import { useStdout } from "ink";
12
- import { useState, useEffect } from "react";
13
- function useTerminalSize() {
14
- const { stdout } = useStdout();
15
- const [size, setSize] = useState({
16
- columns: stdout.columns || 80,
17
- rows: stdout.rows || 24
18
- });
19
- useEffect(() => {
20
- const handleResize = () => {
21
- setSize({
22
- columns: stdout.columns || 80,
23
- rows: stdout.rows || 24
24
- });
25
- };
26
- stdout.on("resize", handleResize);
27
- return () => {
28
- stdout.off("resize", handleResize);
29
- };
30
- }, [stdout]);
31
- return size;
32
- }
33
-
34
- // src/components/Header.tsx
35
- import { Box as Box2, Text as Text2 } from "ink";
36
-
37
- // src/hooks/useSystemStatus.ts
38
- import { useState as useState2, useEffect as useEffect2 } from "react";
39
- import { existsSync, readlinkSync, readdirSync, lstatSync } from "fs";
40
-
41
- // src/lib/paths.ts
42
- import { homedir } from "os";
43
- import { join } from "path";
44
- import { readdir } from "fs/promises";
2
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
45
3
 
46
4
  // src/lib/runtime.ts
47
5
  import { spawn as nodeSpawn } from "child_process";
48
6
  import { readFile as nodeReadFile, writeFile as nodeWriteFile, mkdir } from "fs/promises";
49
7
  import { dirname } from "path";
50
8
  import { fileURLToPath } from "url";
51
- var isBun = typeof Bun !== "undefined";
52
9
  async function exec(command, cwd) {
53
10
  if (isBun) {
54
11
  const proc = Bun.spawn(command, {
@@ -261,20 +218,95 @@ async function commandExists(cmd) {
261
218
  return result.success;
262
219
  }
263
220
  async function checkPrerequisites() {
264
- const required = [
265
- { name: "stow", install: "brew install stow" },
266
- { name: "brew", install: "https://brew.sh" }
221
+ const isMacOS = process.platform === "darwin";
222
+ const common = [
223
+ {
224
+ name: "stow",
225
+ macInstall: "brew install stow",
226
+ linuxInstall: "Install via your package manager (pacman -S stow, apt install stow, etc.)"
227
+ }
267
228
  ];
229
+ const platformSpecific = isMacOS ? [{ name: "brew", install: "https://brew.sh" }] : [];
268
230
  const missing = [];
269
- for (const dep of required) {
231
+ for (const dep of common) {
232
+ if (!await commandExists(dep.name)) {
233
+ missing.push({
234
+ name: dep.name,
235
+ install: isMacOS ? dep.macInstall : dep.linuxInstall
236
+ });
237
+ }
238
+ }
239
+ for (const dep of platformSpecific) {
270
240
  if (!await commandExists(dep.name)) {
271
241
  missing.push(dep);
272
242
  }
273
243
  }
244
+ if (!isMacOS) {
245
+ const packageManagers = ["pacman", "apt", "dnf"];
246
+ const hasPackageManager = await Promise.all(packageManagers.map((pm) => commandExists(pm))).then((results) => results.some(Boolean));
247
+ if (!hasPackageManager) {
248
+ missing.push({
249
+ name: "package manager",
250
+ install: "No supported package manager found (pacman, apt, or dnf)"
251
+ });
252
+ }
253
+ }
274
254
  return { ok: missing.length === 0, missing };
275
255
  }
256
+ var isBun;
257
+ var init_runtime = __esm(() => {
258
+ isBun = typeof Bun !== "undefined";
259
+ });
260
+
261
+ // src/lib/shell.ts
262
+ var init_shell = __esm(() => {
263
+ init_runtime();
264
+ });
265
+
266
+ // src/cli/formalconf.tsx
267
+ import { useState as useState11, useEffect as useEffect7 } from "react";
268
+ import { render, useApp as useApp2, useInput as useInput11 } from "ink";
269
+ import { Spinner as Spinner2 } from "@inkjs/ui";
270
+
271
+ // src/components/layout/Layout.tsx
272
+ import { Box as Box5 } from "ink";
273
+
274
+ // src/hooks/useTerminalSize.ts
275
+ import { useStdout } from "ink";
276
+ import { useState, useEffect } from "react";
277
+ function useTerminalSize() {
278
+ const { stdout } = useStdout();
279
+ const [size, setSize] = useState({
280
+ columns: stdout.columns || 80,
281
+ rows: stdout.rows || 24
282
+ });
283
+ useEffect(() => {
284
+ const handleResize = () => {
285
+ setSize({
286
+ columns: stdout.columns || 80,
287
+ rows: stdout.rows || 24
288
+ });
289
+ };
290
+ stdout.on("resize", handleResize);
291
+ return () => {
292
+ stdout.off("resize", handleResize);
293
+ };
294
+ }, [stdout]);
295
+ return size;
296
+ }
297
+
298
+ // src/components/Header.tsx
299
+ import { Box as Box2, Text as Text2 } from "ink";
300
+
301
+ // src/hooks/useSystemStatus.ts
302
+ import { useState as useState2, useEffect as useEffect2 } from "react";
303
+ import { existsSync, readlinkSync, readdirSync, lstatSync } from "fs";
276
304
 
277
305
  // src/lib/paths.ts
306
+ init_runtime();
307
+ import { homedir } from "os";
308
+ import { join } from "path";
309
+ import { readdir } from "fs/promises";
278
310
  var HOME_DIR = homedir();
279
311
  var CONFIG_DIR = join(HOME_DIR, ".config", "formalconf");
280
312
  var THEME_TARGET_DIR = join(CONFIG_DIR, "current", "theme");
@@ -285,6 +317,7 @@ var CONFIGS_DIR = join(CONFIG_DIR, "configs");
285
317
  var THEMES_DIR = join(CONFIG_DIR, "themes");
286
318
  var PKG_CONFIG_PATH = join(CONFIG_DIR, "pkg-config.json");
287
319
  var PKG_LOCK_PATH = join(CONFIG_DIR, "pkg-lock.json");
320
+ var THEME_CONFIG_PATH = join(CONFIG_DIR, "theme-config.json");
288
321
  async function ensureDir2(path) {
289
322
  await ensureDir(path);
290
323
  }
@@ -414,7 +447,7 @@ function StatusIndicator({
414
447
  // package.json
415
448
  var package_default = {
416
449
  name: "formalconf",
417
- version: "2.0.6",
450
+ version: "2.0.8",
418
451
  description: "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
419
452
  type: "module",
420
453
  main: "./dist/formalconf.js",
@@ -739,6 +772,7 @@ function VimSelect({ options, onChange, isDisabled = false }) {
739
772
  // src/lib/templates.ts
740
773
  import { join as join3 } from "path";
741
774
  import { existsSync as existsSync2 } from "fs";
775
+ init_runtime();
742
776
  var EXAMPLE_CONFIG_README = `# Example Stow Config Package
743
777
 
744
778
  This is an example dotfiles package for use with GNU Stow.
@@ -1337,6 +1371,7 @@ function useBackNavigation({
1337
1371
  }
1338
1372
 
1339
1373
  // src/cli/config-manager.ts
1374
+ init_shell();
1340
1375
  import { parseArgs } from "util";
1341
1376
  import { readdirSync as readdirSync2, existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync as readlinkSync2 } from "fs";
1342
1377
  var colors2 = {
@@ -1349,7 +1384,9 @@ var colors2 = {
1349
1384
  };
1350
1385
  async function checkStow() {
1351
1386
  if (!await commandExists("stow")) {
1352
- console.error(`${colors2.red}Error: GNU Stow is not installed. Install with: brew install stow${colors2.reset}`);
1387
+ const isMacOS = process.platform === "darwin";
1388
+ const installHint = isMacOS ? "brew install stow" : "Install via your package manager (pacman -S stow, apt install stow, dnf install stow)";
1389
+ console.error(`${colors2.red}Error: GNU Stow is not installed. ${installHint}${colors2.reset}`);
1353
1390
  process.exit(1);
1354
1391
  }
1355
1392
  }
@@ -1674,7 +1711,7 @@ function ConfigMenu({ onBack }) {
1674
1711
  }
1675
1712
 
1676
1713
  // src/components/menus/PackageMenu.tsx
1677
- import { useState as useState8, useCallback as useCallback2, useMemo as useMemo2, useRef } from "react";
1714
+ import { useState as useState8, useCallback as useCallback2, useMemo as useMemo2, useRef, useEffect as useEffect4 } from "react";
1678
1715
  import { Box as Box14, Text as Text13, useInput as useInput9 } from "ink";
1679
1716
 
1680
1717
  // src/components/ScrollableLog.tsx
@@ -1938,29 +1975,76 @@ function OrphanTable({ result, onAction, onDismiss }) {
1938
1975
  }
1939
1976
 
1940
1977
  // src/cli/pkg-sync.ts
1978
+ init_shell();
1979
+ init_runtime();
1941
1980
  import { parseArgs as parseArgs2 } from "util";
1942
1981
 
1943
1982
  // src/lib/config.ts
1983
+ init_runtime();
1944
1984
  import { existsSync as existsSync4 } from "fs";
1945
- var DEFAULT_CONFIG = {
1985
+ var DEFAULT_CONFIG_V2 = {
1986
+ version: 2,
1946
1987
  config: {
1947
1988
  purge: false,
1948
1989
  purgeInteractive: true,
1949
1990
  autoUpdate: true
1950
1991
  },
1951
- taps: [],
1952
- packages: [],
1953
- casks: [],
1954
- mas: {}
1992
+ global: {
1993
+ packages: []
1994
+ },
1995
+ macos: {
1996
+ taps: [],
1997
+ formulas: [],
1998
+ casks: [],
1999
+ mas: {}
2000
+ }
1955
2001
  };
2002
+ function migrateV1toV2(v1Config) {
2003
+ return {
2004
+ version: 2,
2005
+ config: {
2006
+ purge: v1Config.config.purge,
2007
+ purgeInteractive: v1Config.config.purgeInteractive,
2008
+ autoUpdate: v1Config.config.autoUpdate
2009
+ },
2010
+ global: {
2011
+ packages: []
2012
+ },
2013
+ macos: {
2014
+ taps: v1Config.taps,
2015
+ formulas: v1Config.packages,
2016
+ casks: v1Config.casks,
2017
+ mas: v1Config.mas
2018
+ }
2019
+ };
2020
+ }
2021
+ function isV1Config(config) {
2022
+ if (!config || typeof config !== "object")
2023
+ return false;
2024
+ const c = config;
2025
+ return !("version" in c) && "config" in c && "taps" in c && "packages" in c && "casks" in c;
2026
+ }
2027
+ function configIsV2(config) {
2028
+ return "version" in config && config.version === 2;
2029
+ }
1956
2030
  async function loadPkgConfig(path) {
1957
2031
  await ensureConfigDir();
1958
2032
  const configPath = path || PKG_CONFIG_PATH;
1959
2033
  if (!existsSync4(configPath)) {
1960
- await savePkgConfig(DEFAULT_CONFIG, configPath);
1961
- return DEFAULT_CONFIG;
2034
+ await savePkgConfig(DEFAULT_CONFIG_V2, configPath);
2035
+ return DEFAULT_CONFIG_V2;
1962
2036
  }
1963
- return readJson(configPath);
2037
+ const rawConfig = await readJson(configPath);
2038
+ if (isV1Config(rawConfig)) {
2039
+ const v2Config = migrateV1toV2(rawConfig);
2040
+ await savePkgConfig(v2Config, configPath);
2041
+ return v2Config;
2042
+ }
2043
+ if (configIsV2(rawConfig)) {
2044
+ return rawConfig;
2045
+ }
2046
+ await savePkgConfig(DEFAULT_CONFIG_V2, configPath);
2047
+ return DEFAULT_CONFIG_V2;
1964
2048
  }
1965
2049
  async function savePkgConfig(config, path) {
1966
2050
  await ensureConfigDir();
@@ -1979,249 +2063,1486 @@ async function savePkgLock(lock) {
1979
2063
  }
1980
2064
 
1981
2065
  // src/lib/lockfile.ts
1982
- async function fetchInstalledVersions() {
1983
- const config = await loadPkgConfig();
1984
- const now = new Date().toISOString();
1985
- const formulas = {};
1986
- const casks = {};
1987
- if (config.packages.length > 0) {
1988
- const result = await exec([
1989
- "brew",
1990
- "info",
1991
- "--json=v2",
1992
- ...config.packages
1993
- ]);
1994
- if (result.success && result.stdout) {
1995
- const info = JSON.parse(result.stdout);
1996
- for (const formula of info.formulae) {
1997
- if (formula.installed.length > 0) {
1998
- formulas[formula.name] = {
1999
- version: formula.installed[0].version,
2000
- tap: formula.tap,
2001
- installedAt: now
2002
- };
2066
+ init_shell();
2067
+
2068
+ // src/lib/platform.ts
2069
+ init_runtime();
2070
+ var cachedPlatformInfo = null;
2071
+ function getOS() {
2072
+ return process.platform === "darwin" ? "darwin" : "linux";
2073
+ }
2074
+ async function getLinuxDistro() {
2075
+ if (getOS() !== "linux") {
2076
+ return "unknown";
2077
+ }
2078
+ try {
2079
+ const result = await exec(["cat", "/etc/os-release"]);
2080
+ if (!result.success) {
2081
+ return "unknown";
2082
+ }
2083
+ const lines = result.stdout.split(`
2084
+ `);
2085
+ for (const line of lines) {
2086
+ if (line.startsWith("ID=")) {
2087
+ const id = line.slice(3).replace(/"/g, "").toLowerCase();
2088
+ switch (id) {
2089
+ case "arch":
2090
+ case "manjaro":
2091
+ case "endeavouros":
2092
+ case "artix":
2093
+ return "arch";
2094
+ case "debian":
2095
+ return "debian";
2096
+ case "ubuntu":
2097
+ case "linuxmint":
2098
+ case "pop":
2099
+ case "elementary":
2100
+ return "ubuntu";
2101
+ case "fedora":
2102
+ return "fedora";
2103
+ case "rhel":
2104
+ case "centos":
2105
+ case "rocky":
2106
+ case "almalinux":
2107
+ return "rhel";
2108
+ case "opensuse":
2109
+ case "opensuse-leap":
2110
+ case "opensuse-tumbleweed":
2111
+ return "opensuse";
2112
+ default:
2113
+ const idLikeLine = lines.find((l) => l.startsWith("ID_LIKE="));
2114
+ if (idLikeLine) {
2115
+ const idLike = idLikeLine.slice(8).replace(/"/g, "").toLowerCase();
2116
+ if (idLike.includes("arch"))
2117
+ return "arch";
2118
+ if (idLike.includes("debian") || idLike.includes("ubuntu"))
2119
+ return "debian";
2120
+ if (idLike.includes("fedora") || idLike.includes("rhel"))
2121
+ return "fedora";
2122
+ }
2123
+ return "unknown";
2003
2124
  }
2004
2125
  }
2005
2126
  }
2127
+ } catch {
2128
+ return "unknown";
2006
2129
  }
2007
- if (config.casks.length > 0) {
2008
- const result = await exec([
2009
- "brew",
2010
- "info",
2011
- "--json=v2",
2012
- "--cask",
2013
- ...config.casks
2014
- ]);
2015
- if (result.success && result.stdout) {
2016
- const info = JSON.parse(result.stdout);
2017
- for (const cask of info.casks) {
2018
- if (cask.installed) {
2019
- casks[cask.token] = {
2020
- version: cask.installed,
2021
- installedAt: now
2022
- };
2023
- }
2130
+ return "unknown";
2131
+ }
2132
+ async function detectAurHelper() {
2133
+ const helpers = ["yay", "paru", "trizen"];
2134
+ for (const helper of helpers) {
2135
+ if (await commandExists(helper)) {
2136
+ return helper;
2137
+ }
2138
+ }
2139
+ return "none";
2140
+ }
2141
+ async function detectAvailablePackageManagers() {
2142
+ const os = getOS();
2143
+ const managers = [];
2144
+ if (os === "darwin") {
2145
+ if (await commandExists("brew")) {
2146
+ managers.push("homebrew");
2147
+ }
2148
+ if (await commandExists("mas")) {
2149
+ managers.push("mas");
2150
+ }
2151
+ } else {
2152
+ if (await commandExists("pacman")) {
2153
+ managers.push("pacman");
2154
+ const aurHelper = await detectAurHelper();
2155
+ if (aurHelper !== "none") {
2156
+ managers.push("aur");
2024
2157
  }
2025
2158
  }
2159
+ if (await commandExists("apt")) {
2160
+ managers.push("apt");
2161
+ }
2162
+ if (await commandExists("dnf")) {
2163
+ managers.push("dnf");
2164
+ }
2165
+ if (await commandExists("flatpak")) {
2166
+ managers.push("flatpak");
2167
+ }
2026
2168
  }
2027
- return { formulas, casks };
2169
+ return managers;
2028
2170
  }
2029
- async function generateLockfile() {
2030
- const { formulas, casks } = await fetchInstalledVersions();
2031
- return {
2032
- version: 1,
2033
- lastUpdated: new Date().toISOString(),
2034
- formulas,
2035
- casks
2171
+ async function getPlatformInfo() {
2172
+ if (cachedPlatformInfo) {
2173
+ return cachedPlatformInfo;
2174
+ }
2175
+ const os = getOS();
2176
+ const distro = os === "linux" ? await getLinuxDistro() : null;
2177
+ const aurHelper = distro === "arch" ? await detectAurHelper() : null;
2178
+ const availableManagers = await detectAvailablePackageManagers();
2179
+ cachedPlatformInfo = {
2180
+ os,
2181
+ distro,
2182
+ aurHelper,
2183
+ availableManagers
2036
2184
  };
2185
+ return cachedPlatformInfo;
2037
2186
  }
2038
- async function updateLockfile() {
2039
- const existing = await loadPkgLock();
2040
- const { formulas, casks } = await fetchInstalledVersions();
2041
- const mergedFormulas = {};
2042
- for (const [name, info] of Object.entries(formulas)) {
2043
- const prev = existing?.formulas[name];
2044
- if (prev && prev.version === info.version) {
2045
- mergedFormulas[name] = prev;
2046
- } else {
2047
- mergedFormulas[name] = info;
2048
- }
2187
+ function getPlatformDisplayName(info) {
2188
+ if (info.os === "darwin") {
2189
+ return "macOS";
2049
2190
  }
2050
- const mergedCasks = {};
2051
- for (const [name, info] of Object.entries(casks)) {
2052
- const prev = existing?.casks[name];
2053
- if (prev && prev.version === info.version) {
2054
- mergedCasks[name] = prev;
2055
- } else {
2056
- mergedCasks[name] = info;
2057
- }
2191
+ switch (info.distro) {
2192
+ case "arch":
2193
+ return "Arch Linux";
2194
+ case "debian":
2195
+ return "Debian";
2196
+ case "ubuntu":
2197
+ return "Ubuntu";
2198
+ case "fedora":
2199
+ return "Fedora";
2200
+ case "rhel":
2201
+ return "RHEL/CentOS";
2202
+ case "opensuse":
2203
+ return "openSUSE";
2204
+ default:
2205
+ return "Linux";
2058
2206
  }
2059
- const lock = {
2060
- version: 1,
2061
- lastUpdated: new Date().toISOString(),
2062
- formulas: mergedFormulas,
2063
- casks: mergedCasks
2064
- };
2065
- await savePkgLock(lock);
2066
- return lock;
2067
2207
  }
2068
- async function getChangedPackages() {
2069
- const existing = await loadPkgLock();
2070
- const { formulas, casks } = await fetchInstalledVersions();
2071
- const added = [];
2072
- const removed = [];
2073
- const upgraded = [];
2074
- if (!existing) {
2075
- added.push(...Object.keys(formulas), ...Object.keys(casks));
2076
- return { added, removed, upgraded };
2208
+
2209
+ // src/lib/package-managers/homebrew.ts
2210
+ init_runtime();
2211
+ init_runtime();
2212
+ async function runBrewCommand(args, callbacks) {
2213
+ const cmd = ["brew", ...args];
2214
+ if (callbacks?.onLog) {
2215
+ const exitCode = await execStreaming(cmd, callbacks.onLog);
2216
+ return exitCode === 0;
2077
2217
  }
2078
- for (const [name, info] of Object.entries(formulas)) {
2079
- if (!existing.formulas[name]) {
2080
- added.push(name);
2081
- } else if (existing.formulas[name].version !== info.version) {
2082
- upgraded.push({
2083
- name,
2084
- from: existing.formulas[name].version,
2085
- to: info.version
2086
- });
2218
+ const result = await exec(cmd);
2219
+ return result.success;
2220
+ }
2221
+
2222
+ class HomebrewFormulas {
2223
+ type = "homebrew";
2224
+ displayName = "Homebrew Formulas";
2225
+ async isAvailable() {
2226
+ return commandExists("brew");
2227
+ }
2228
+ async update(callbacks) {
2229
+ return runBrewCommand(["update"], callbacks);
2230
+ }
2231
+ async install(packages, callbacks) {
2232
+ if (packages.length === 0)
2233
+ return true;
2234
+ return runBrewCommand(["install", ...packages], callbacks);
2235
+ }
2236
+ async uninstall(packages, callbacks) {
2237
+ if (packages.length === 0)
2238
+ return true;
2239
+ return runBrewCommand(["uninstall", ...packages], callbacks);
2240
+ }
2241
+ async upgrade(packages, callbacks) {
2242
+ const args = ["upgrade", "--formula"];
2243
+ if (packages && packages.length > 0) {
2244
+ args.push(...packages);
2087
2245
  }
2246
+ return runBrewCommand(args, callbacks);
2088
2247
  }
2089
- for (const name of Object.keys(existing.formulas)) {
2090
- if (!formulas[name]) {
2091
- removed.push(name);
2248
+ async listInstalled() {
2249
+ const result = await exec(["brew", "info", "--json=v2", "--installed"]);
2250
+ if (!result.success)
2251
+ return [];
2252
+ try {
2253
+ const info = JSON.parse(result.stdout);
2254
+ return info.formulae.map((f) => ({
2255
+ name: f.name,
2256
+ version: f.installed[0]?.version || "unknown"
2257
+ }));
2258
+ } catch {
2259
+ return [];
2092
2260
  }
2093
2261
  }
2094
- for (const [name, info] of Object.entries(casks)) {
2095
- if (!existing.casks[name]) {
2096
- added.push(name);
2097
- } else if (existing.casks[name].version !== info.version) {
2098
- upgraded.push({
2262
+ async listOutdated() {
2263
+ const result = await exec(["brew", "outdated", "--formula", "--json"]);
2264
+ if (!result.success || !result.stdout)
2265
+ return [];
2266
+ try {
2267
+ const outdated = JSON.parse(result.stdout);
2268
+ return outdated.formulae?.map((f) => ({
2269
+ name: f.name,
2270
+ currentVersion: f.installed_versions?.[0] || "unknown",
2271
+ newVersion: f.current_version
2272
+ })) || [];
2273
+ } catch {
2274
+ const quietResult = await exec(["brew", "outdated", "--formula", "--quiet"]);
2275
+ if (!quietResult.success)
2276
+ return [];
2277
+ return quietResult.stdout.split(`
2278
+ `).filter(Boolean).map((name) => ({
2099
2279
  name,
2100
- from: existing.casks[name].version,
2101
- to: info.version
2102
- });
2280
+ currentVersion: "unknown",
2281
+ newVersion: "unknown"
2282
+ }));
2103
2283
  }
2104
2284
  }
2105
- for (const name of Object.keys(existing.casks)) {
2106
- if (!casks[name]) {
2107
- removed.push(name);
2285
+ async listLeaves() {
2286
+ const result = await exec(["brew", "leaves"]);
2287
+ if (!result.success)
2288
+ return [];
2289
+ return result.stdout.split(`
2290
+ `).filter(Boolean);
2291
+ }
2292
+ async cleanup(callbacks) {
2293
+ const autoremove = await runBrewCommand(["autoremove"], callbacks);
2294
+ const cleanup = await runBrewCommand(["cleanup"], callbacks);
2295
+ return autoremove && cleanup;
2296
+ }
2297
+ async addRepository(repo, callbacks) {
2298
+ return runBrewCommand(["tap", repo], callbacks);
2299
+ }
2300
+ async isInstalled(packages) {
2301
+ const result = new Map;
2302
+ const listResult = await exec(["brew", "list", "--formula"]);
2303
+ const installed = new Set(listResult.stdout.split(`
2304
+ `).filter(Boolean));
2305
+ for (const pkg of packages) {
2306
+ const shortName = pkg.split("/").pop() || pkg;
2307
+ result.set(pkg, installed.has(shortName) || installed.has(pkg));
2108
2308
  }
2309
+ return result;
2310
+ }
2311
+ async getTappedRepos() {
2312
+ const result = await exec(["brew", "tap"]);
2313
+ if (!result.success)
2314
+ return [];
2315
+ return result.stdout.split(`
2316
+ `).filter(Boolean);
2317
+ }
2318
+ async hasDependents(pkg) {
2319
+ const result = await exec(["brew", "uses", "--installed", pkg]);
2320
+ return result.success && result.stdout.trim().length > 0;
2109
2321
  }
2110
- return { added, removed, upgraded };
2111
2322
  }
2112
2323
 
2113
- // src/types/pkg-config.ts
2114
- var SYSTEM_APP_IDS = [
2115
- 409183694,
2116
- 409203825,
2117
- 409201541,
2118
- 408981434,
2119
- 682658836,
2120
- 424389933,
2121
- 424390742,
2122
- 413897608,
2123
- 1274495053,
2124
- 425424353,
2125
- 497799835,
2126
- 634148309,
2127
- 1480068668,
2128
- 803453959,
2129
- 1295203466,
2130
- 1444383602,
2131
- 640199958,
2132
- 899247664,
2133
- 1176895641,
2134
- 1451685025
2135
- ];
2136
-
2137
- // src/cli/pkg-sync.ts
2138
- var colors3 = {
2139
- red: "\x1B[0;31m",
2140
- green: "\x1B[0;32m",
2141
- blue: "\x1B[0;34m",
2142
- yellow: "\x1B[1;33m",
2143
- cyan: "\x1B[0;36m",
2144
- bold: "\x1B[1m",
2145
- reset: "\x1B[0m"
2146
- };
2147
- async function runCommand(command, callbacks, cwd, needsTTY = false) {
2148
- if (callbacks) {
2149
- if (needsTTY) {
2150
- return execStreamingWithTTY(command, callbacks.onLog, cwd);
2151
- }
2152
- return execStreaming(command, callbacks.onLog, cwd);
2324
+ class HomebrewCasks {
2325
+ type = "homebrew";
2326
+ displayName = "Homebrew Casks";
2327
+ async isAvailable() {
2328
+ return commandExists("brew");
2153
2329
  }
2154
- return execLive(command, cwd);
2155
- }
2156
- async function checkDependencies() {
2157
- if (!await commandExists("brew")) {
2158
- console.error(`${colors3.red}Error: Homebrew not installed${colors3.reset}`);
2159
- process.exit(1);
2330
+ async update(callbacks) {
2331
+ return runBrewCommand(["update"], callbacks);
2160
2332
  }
2161
- }
2162
- async function getOutdatedPackages() {
2163
- const [formulas, casks] = await Promise.all([
2164
- exec(["brew", "outdated", "--formula", "--quiet"]),
2165
- exec(["brew", "outdated", "--cask", "--quiet"])
2166
- ]);
2167
- const packages = [];
2168
- if (formulas.stdout) {
2169
- packages.push(...formulas.stdout.split(`
2170
- `).filter(Boolean).map((name) => ({ name, type: "formula" })));
2333
+ async install(packages, callbacks) {
2334
+ if (packages.length === 0)
2335
+ return true;
2336
+ return runBrewCommand(["install", "--cask", ...packages], callbacks);
2171
2337
  }
2172
- if (casks.stdout) {
2173
- packages.push(...casks.stdout.split(`
2174
- `).filter(Boolean).map((name) => ({ name, type: "cask" })));
2338
+ async uninstall(packages, callbacks) {
2339
+ if (packages.length === 0)
2340
+ return true;
2341
+ return runBrewCommand(["uninstall", "--cask", ...packages], callbacks);
2175
2342
  }
2176
- return packages;
2343
+ async upgrade(packages, callbacks) {
2344
+ const args = ["upgrade", "--cask", "--greedy"];
2345
+ if (packages && packages.length > 0) {
2346
+ args.push(...packages);
2347
+ }
2348
+ return runBrewCommand(args, callbacks);
2349
+ }
2350
+ async listInstalled() {
2351
+ const result = await exec(["brew", "info", "--json=v2", "--cask", "--installed"]);
2352
+ if (!result.success)
2353
+ return [];
2354
+ try {
2355
+ const info = JSON.parse(result.stdout);
2356
+ return info.casks.map((c) => ({
2357
+ name: c.token,
2358
+ version: c.installed || "unknown"
2359
+ }));
2360
+ } catch {
2361
+ return [];
2362
+ }
2363
+ }
2364
+ async listOutdated() {
2365
+ const result = await exec(["brew", "outdated", "--cask", "--quiet"]);
2366
+ if (!result.success)
2367
+ return [];
2368
+ return result.stdout.split(`
2369
+ `).filter(Boolean).map((name) => ({
2370
+ name,
2371
+ currentVersion: "unknown",
2372
+ newVersion: "unknown"
2373
+ }));
2374
+ }
2375
+ async cleanup(callbacks) {
2376
+ return runBrewCommand(["cleanup"], callbacks);
2377
+ }
2378
+ async isInstalled(packages) {
2379
+ const result = new Map;
2380
+ const listResult = await exec(["brew", "list", "--cask"]);
2381
+ const installed = new Set(listResult.stdout.split(`
2382
+ `).filter(Boolean));
2383
+ for (const pkg of packages) {
2384
+ result.set(pkg, installed.has(pkg));
2385
+ }
2386
+ return result;
2387
+ }
2388
+ }
2389
+
2390
+ // src/lib/package-managers/mas.ts
2391
+ init_runtime();
2392
+ init_runtime();
2393
+ function parseMasOutput(output) {
2394
+ const results = [];
2395
+ for (const line of output.split(`
2396
+ `).filter(Boolean)) {
2397
+ const match = line.match(/^(\d+)\s+(.+?)(?:\s+\(([^)]+)\))?$/);
2398
+ if (match) {
2399
+ results.push({
2400
+ id: parseInt(match[1], 10),
2401
+ name: match[2].trim(),
2402
+ version: match[3]
2403
+ });
2404
+ }
2405
+ }
2406
+ return results;
2407
+ }
2408
+
2409
+ class MacAppStore {
2410
+ type = "mas";
2411
+ displayName = "Mac App Store";
2412
+ async isAvailable() {
2413
+ return commandExists("mas");
2414
+ }
2415
+ async update(_callbacks) {
2416
+ return true;
2417
+ }
2418
+ async install(packages, callbacks) {
2419
+ if (packages.length === 0)
2420
+ return true;
2421
+ for (const appId of packages) {
2422
+ const cmd = ["mas", "install", appId];
2423
+ if (callbacks?.onLog) {
2424
+ const exitCode = await execStreamingWithTTY(cmd, callbacks.onLog);
2425
+ if (exitCode !== 0)
2426
+ return false;
2427
+ } else {
2428
+ const result = await exec(cmd);
2429
+ if (!result.success)
2430
+ return false;
2431
+ }
2432
+ }
2433
+ return true;
2434
+ }
2435
+ async uninstall(packages, callbacks) {
2436
+ if (packages.length === 0)
2437
+ return true;
2438
+ for (const appId of packages) {
2439
+ const cmd = ["mas", "uninstall", appId];
2440
+ if (callbacks?.onLog) {
2441
+ const exitCode = await execStreamingWithTTY(cmd, callbacks.onLog);
2442
+ if (exitCode !== 0)
2443
+ return false;
2444
+ } else {
2445
+ const result = await exec(cmd);
2446
+ if (!result.success)
2447
+ return false;
2448
+ }
2449
+ }
2450
+ return true;
2451
+ }
2452
+ async upgrade(_packages, callbacks) {
2453
+ const cmd = ["mas", "upgrade"];
2454
+ if (callbacks?.onLog) {
2455
+ const exitCode = await execStreamingWithTTY(cmd, callbacks.onLog);
2456
+ return exitCode === 0;
2457
+ }
2458
+ const result = await exec(cmd);
2459
+ return result.success;
2460
+ }
2461
+ async listInstalled() {
2462
+ const result = await exec(["mas", "list"]);
2463
+ if (!result.success)
2464
+ return [];
2465
+ const apps = parseMasOutput(result.stdout);
2466
+ return apps.map((app) => ({
2467
+ name: String(app.id),
2468
+ version: app.version || "unknown"
2469
+ }));
2470
+ }
2471
+ async listOutdated() {
2472
+ const result = await exec(["mas", "outdated"]);
2473
+ if (!result.success)
2474
+ return [];
2475
+ const apps = parseMasOutput(result.stdout);
2476
+ return apps.map((app) => ({
2477
+ name: String(app.id),
2478
+ currentVersion: app.version || "unknown",
2479
+ newVersion: "available"
2480
+ }));
2481
+ }
2482
+ async cleanup(_callbacks) {
2483
+ return true;
2484
+ }
2485
+ async isInstalled(packages) {
2486
+ const result = new Map;
2487
+ const listResult = await exec(["mas", "list"]);
2488
+ const apps = parseMasOutput(listResult.stdout);
2489
+ const installedIds = new Set(apps.map((app) => String(app.id)));
2490
+ for (const appId of packages) {
2491
+ result.set(appId, installedIds.has(appId));
2492
+ }
2493
+ return result;
2494
+ }
2495
+ async getInstalledApps() {
2496
+ const result = await exec(["mas", "list"]);
2497
+ if (!result.success)
2498
+ return [];
2499
+ return parseMasOutput(result.stdout);
2500
+ }
2501
+ }
2502
+
2503
+ // src/lib/package-managers/pacman.ts
2504
+ init_runtime();
2505
+ init_runtime();
2506
+ async function runPacmanCommand(args, callbacks, sudo = false) {
2507
+ const cmd = sudo ? ["sudo", "pacman", ...args] : ["pacman", ...args];
2508
+ if (callbacks?.onLog) {
2509
+ const exitCode = await execStreaming(cmd, callbacks.onLog);
2510
+ return exitCode === 0;
2511
+ }
2512
+ const result = await exec(cmd);
2513
+ return result.success;
2514
+ }
2515
+
2516
+ class Pacman {
2517
+ type = "pacman";
2518
+ displayName = "Pacman";
2519
+ async isAvailable() {
2520
+ return commandExists("pacman");
2521
+ }
2522
+ async update(callbacks) {
2523
+ return runPacmanCommand(["-Sy", "--noconfirm"], callbacks, true);
2524
+ }
2525
+ async install(packages, callbacks) {
2526
+ if (packages.length === 0)
2527
+ return true;
2528
+ return runPacmanCommand(["-S", "--noconfirm", "--needed", ...packages], callbacks, true);
2529
+ }
2530
+ async uninstall(packages, callbacks) {
2531
+ if (packages.length === 0)
2532
+ return true;
2533
+ return runPacmanCommand(["-Rs", "--noconfirm", ...packages], callbacks, true);
2534
+ }
2535
+ async upgrade(packages, callbacks) {
2536
+ if (packages && packages.length > 0) {
2537
+ return runPacmanCommand(["-S", "--noconfirm", ...packages], callbacks, true);
2538
+ }
2539
+ return runPacmanCommand(["-Syu", "--noconfirm"], callbacks, true);
2540
+ }
2541
+ async listInstalled() {
2542
+ const result = await exec(["pacman", "-Qe"]);
2543
+ if (!result.success)
2544
+ return [];
2545
+ return result.stdout.split(`
2546
+ `).filter(Boolean).map((line) => {
2547
+ const [name, version] = line.split(" ");
2548
+ return { name, version: version || "unknown" };
2549
+ });
2550
+ }
2551
+ async listOutdated() {
2552
+ const checkupdatesExists = await commandExists("checkupdates");
2553
+ if (checkupdatesExists) {
2554
+ const result2 = await exec(["checkupdates"]);
2555
+ if (!result2.stdout)
2556
+ return [];
2557
+ return result2.stdout.split(`
2558
+ `).filter(Boolean).map((line) => {
2559
+ const match = line.match(/^(\S+)\s+(\S+)\s+->\s+(\S+)$/);
2560
+ if (match) {
2561
+ return {
2562
+ name: match[1],
2563
+ currentVersion: match[2],
2564
+ newVersion: match[3]
2565
+ };
2566
+ }
2567
+ return { name: line, currentVersion: "unknown", newVersion: "unknown" };
2568
+ });
2569
+ }
2570
+ await exec(["sudo", "pacman", "-Sy"]);
2571
+ const result = await exec(["pacman", "-Qu"]);
2572
+ if (!result.success || !result.stdout)
2573
+ return [];
2574
+ return result.stdout.split(`
2575
+ `).filter(Boolean).map((line) => {
2576
+ const [name, ...rest] = line.split(" ");
2577
+ const versionPart = rest.join(" ");
2578
+ const match = versionPart.match(/(\S+)\s+->\s+(\S+)/);
2579
+ return {
2580
+ name,
2581
+ currentVersion: match?.[1] || "unknown",
2582
+ newVersion: match?.[2] || "unknown"
2583
+ };
2584
+ });
2585
+ }
2586
+ async listLeaves() {
2587
+ const result = await exec(["pacman", "-Qqe"]);
2588
+ if (!result.success)
2589
+ return [];
2590
+ return result.stdout.split(`
2591
+ `).filter(Boolean);
2592
+ }
2593
+ async cleanup(callbacks) {
2594
+ const orphansResult = await exec(["pacman", "-Qdtq"]);
2595
+ if (orphansResult.success && orphansResult.stdout.trim()) {
2596
+ const orphans = orphansResult.stdout.split(`
2597
+ `).filter(Boolean);
2598
+ if (orphans.length > 0) {
2599
+ await runPacmanCommand(["-Rs", "--noconfirm", ...orphans], callbacks, true);
2600
+ }
2601
+ }
2602
+ return runPacmanCommand(["-Sc", "--noconfirm"], callbacks, true);
2603
+ }
2604
+ async isInstalled(packages) {
2605
+ const result = new Map;
2606
+ const listResult = await exec(["pacman", "-Qq"]);
2607
+ const installed = new Set(listResult.stdout.split(`
2608
+ `).filter(Boolean));
2609
+ for (const pkg of packages) {
2610
+ result.set(pkg, installed.has(pkg));
2611
+ }
2612
+ return result;
2613
+ }
2614
+ }
2615
+
2616
+ // src/lib/package-managers/aur.ts
2617
+ init_runtime();
2618
+ init_runtime();
2619
+ async function runAurCommand(helper, args, callbacks) {
2620
+ if (helper === "none")
2621
+ return false;
2622
+ const cmd = [helper, ...args];
2623
+ if (callbacks?.onLog) {
2624
+ const exitCode = await execStreaming(cmd, callbacks.onLog);
2625
+ return exitCode === 0;
2626
+ }
2627
+ const result = await exec(cmd);
2628
+ return result.success;
2629
+ }
2630
+
2631
+ class AurPackageManager {
2632
+ type = "aur";
2633
+ displayName = "AUR";
2634
+ helper = "none";
2635
+ helperDetected = false;
2636
+ async getHelper() {
2637
+ if (!this.helperDetected) {
2638
+ this.helper = await detectAurHelper();
2639
+ this.helperDetected = true;
2640
+ }
2641
+ return this.helper;
2642
+ }
2643
+ async isAvailable() {
2644
+ const helper = await this.getHelper();
2645
+ return helper !== "none";
2646
+ }
2647
+ async ensureAurHelper(callbacks) {
2648
+ const helper = await this.getHelper();
2649
+ if (helper !== "none")
2650
+ return true;
2651
+ const log = callbacks?.onLog || console.log;
2652
+ log("No AUR helper found. Installing yay...");
2653
+ const hasGit = await commandExists("git");
2654
+ const hasMakepkg = await commandExists("makepkg");
2655
+ const hasBaseDevel = await exec(["pacman", "-Qq", "base-devel"]);
2656
+ if (!hasGit) {
2657
+ log("Installing git...");
2658
+ const gitResult = await exec(["sudo", "pacman", "-S", "--noconfirm", "git"]);
2659
+ if (!gitResult.success) {
2660
+ log("Failed to install git");
2661
+ return false;
2662
+ }
2663
+ }
2664
+ if (!hasBaseDevel.success) {
2665
+ log("Installing base-devel...");
2666
+ const baseResult = await exec(["sudo", "pacman", "-S", "--noconfirm", "base-devel"]);
2667
+ if (!baseResult.success) {
2668
+ log("Failed to install base-devel");
2669
+ return false;
2670
+ }
2671
+ }
2672
+ const tmpDir = "/tmp/yay-install";
2673
+ await exec(["rm", "-rf", tmpDir]);
2674
+ log("Cloning yay from AUR...");
2675
+ const cloneResult = await exec(["git", "clone", "https://aur.archlinux.org/yay.git", tmpDir]);
2676
+ if (!cloneResult.success) {
2677
+ log(`Failed to clone yay: ${cloneResult.stderr}`);
2678
+ return false;
2679
+ }
2680
+ log("Building and installing yay...");
2681
+ const buildExitCode = await execLive(["makepkg", "-si", "--noconfirm"], tmpDir);
2682
+ if (buildExitCode !== 0) {
2683
+ log("Failed to build yay");
2684
+ return false;
2685
+ }
2686
+ await exec(["rm", "-rf", tmpDir]);
2687
+ this.helper = "yay";
2688
+ log("yay installed successfully!");
2689
+ return true;
2690
+ }
2691
+ async update(callbacks) {
2692
+ const helper = await this.getHelper();
2693
+ if (helper === "none")
2694
+ return false;
2695
+ return runAurCommand(helper, ["-Sy"], callbacks);
2696
+ }
2697
+ async install(packages, callbacks) {
2698
+ if (packages.length === 0)
2699
+ return true;
2700
+ const helper = await this.getHelper();
2701
+ if (helper === "none") {
2702
+ const installed = await this.ensureAurHelper(callbacks);
2703
+ if (!installed)
2704
+ return false;
2705
+ }
2706
+ const currentHelper = await this.getHelper();
2707
+ return runAurCommand(currentHelper, ["-S", "--noconfirm", "--needed", ...packages], callbacks);
2708
+ }
2709
+ async uninstall(packages, callbacks) {
2710
+ if (packages.length === 0)
2711
+ return true;
2712
+ const helper = await this.getHelper();
2713
+ if (helper === "none")
2714
+ return false;
2715
+ return runAurCommand(helper, ["-Rs", "--noconfirm", ...packages], callbacks);
2716
+ }
2717
+ async upgrade(packages, callbacks) {
2718
+ const helper = await this.getHelper();
2719
+ if (helper === "none")
2720
+ return false;
2721
+ if (packages && packages.length > 0) {
2722
+ return runAurCommand(helper, ["-S", "--noconfirm", ...packages], callbacks);
2723
+ }
2724
+ return runAurCommand(helper, ["-Syu", "--noconfirm"], callbacks);
2725
+ }
2726
+ async listInstalled() {
2727
+ const result = await exec(["pacman", "-Qm"]);
2728
+ if (!result.success)
2729
+ return [];
2730
+ return result.stdout.split(`
2731
+ `).filter(Boolean).map((line) => {
2732
+ const [name, version] = line.split(" ");
2733
+ return { name, version: version || "unknown" };
2734
+ });
2735
+ }
2736
+ async listOutdated() {
2737
+ const helper = await this.getHelper();
2738
+ if (helper === "none")
2739
+ return [];
2740
+ const result = await exec([helper, "-Qua"]);
2741
+ if (!result.success || !result.stdout)
2742
+ return [];
2743
+ return result.stdout.split(`
2744
+ `).filter(Boolean).map((line) => {
2745
+ const match = line.match(/^(\S+)\s+(\S+)\s+->\s+(\S+)$/);
2746
+ if (match) {
2747
+ return {
2748
+ name: match[1],
2749
+ currentVersion: match[2],
2750
+ newVersion: match[3]
2751
+ };
2752
+ }
2753
+ return { name: line.split(" ")[0], currentVersion: "unknown", newVersion: "unknown" };
2754
+ });
2755
+ }
2756
+ async cleanup(callbacks) {
2757
+ const helper = await this.getHelper();
2758
+ if (helper === "none")
2759
+ return true;
2760
+ return runAurCommand(helper, ["-Sc", "--noconfirm"], callbacks);
2761
+ }
2762
+ async isInstalled(packages) {
2763
+ const result = new Map;
2764
+ const listResult = await exec(["pacman", "-Qmq"]);
2765
+ const installed = new Set(listResult.stdout.split(`
2766
+ `).filter(Boolean));
2767
+ for (const pkg of packages) {
2768
+ result.set(pkg, installed.has(pkg));
2769
+ }
2770
+ return result;
2771
+ }
2772
+ getHelperName() {
2773
+ return this.getHelper();
2774
+ }
2775
+ }
2776
+
2777
+ // src/lib/package-managers/apt.ts
2778
+ init_runtime();
2779
+ init_runtime();
2780
+ async function runAptCommand(args, callbacks, sudo = false) {
2781
+ const cmd = sudo ? ["sudo", "apt-get", ...args] : ["apt-get", ...args];
2782
+ if (callbacks?.onLog) {
2783
+ const exitCode = await execStreaming(cmd, callbacks.onLog);
2784
+ return exitCode === 0;
2785
+ }
2786
+ const result = await exec(cmd);
2787
+ return result.success;
2788
+ }
2789
+
2790
+ class Apt {
2791
+ type = "apt";
2792
+ displayName = "APT";
2793
+ async isAvailable() {
2794
+ return commandExists("apt-get");
2795
+ }
2796
+ async update(callbacks) {
2797
+ return runAptCommand(["update", "-y"], callbacks, true);
2798
+ }
2799
+ async install(packages, callbacks) {
2800
+ if (packages.length === 0)
2801
+ return true;
2802
+ return runAptCommand(["install", "-y", ...packages], callbacks, true);
2803
+ }
2804
+ async uninstall(packages, callbacks) {
2805
+ if (packages.length === 0)
2806
+ return true;
2807
+ return runAptCommand(["remove", "-y", ...packages], callbacks, true);
2808
+ }
2809
+ async upgrade(packages, callbacks) {
2810
+ if (packages && packages.length > 0) {
2811
+ return runAptCommand(["install", "-y", "--only-upgrade", ...packages], callbacks, true);
2812
+ }
2813
+ await runAptCommand(["update", "-y"], callbacks, true);
2814
+ return runAptCommand(["upgrade", "-y"], callbacks, true);
2815
+ }
2816
+ async listInstalled() {
2817
+ const result = await exec(["dpkg-query", "-W", "-f=${Package} ${Version}\n"]);
2818
+ if (!result.success)
2819
+ return [];
2820
+ return result.stdout.split(`
2821
+ `).filter(Boolean).map((line) => {
2822
+ const [name, version] = line.split(" ");
2823
+ return { name, version: version || "unknown" };
2824
+ });
2825
+ }
2826
+ async listOutdated() {
2827
+ await exec(["sudo", "apt-get", "update", "-y"]);
2828
+ const result = await exec(["apt", "list", "--upgradable"]);
2829
+ if (!result.success)
2830
+ return [];
2831
+ return result.stdout.split(`
2832
+ `).filter((line) => line.includes("[upgradable")).map((line) => {
2833
+ const match = line.match(/^(\S+)\/\S+\s+(\S+)\s+\S+\s+\[upgradable from:\s+(\S+)\]/);
2834
+ if (match) {
2835
+ return {
2836
+ name: match[1],
2837
+ currentVersion: match[3],
2838
+ newVersion: match[2]
2839
+ };
2840
+ }
2841
+ return null;
2842
+ }).filter((pkg) => pkg !== null);
2843
+ }
2844
+ async listLeaves() {
2845
+ const result = await exec(["apt-mark", "showmanual"]);
2846
+ if (!result.success)
2847
+ return [];
2848
+ return result.stdout.split(`
2849
+ `).filter(Boolean);
2850
+ }
2851
+ async cleanup(callbacks) {
2852
+ const autoremove = await runAptCommand(["autoremove", "-y"], callbacks, true);
2853
+ const autoclean = await runAptCommand(["autoclean"], callbacks, true);
2854
+ return autoremove && autoclean;
2855
+ }
2856
+ async addRepository(repo, callbacks) {
2857
+ const cmd = ["sudo", "add-apt-repository", "-y", repo];
2858
+ if (callbacks?.onLog) {
2859
+ const exitCode = await execStreaming(cmd, callbacks.onLog);
2860
+ if (exitCode !== 0)
2861
+ return false;
2862
+ } else {
2863
+ const result = await exec(cmd);
2864
+ if (!result.success)
2865
+ return false;
2866
+ }
2867
+ return runAptCommand(["update", "-y"], callbacks, true);
2868
+ }
2869
+ async isInstalled(packages) {
2870
+ const result = new Map;
2871
+ for (const pkg of packages) {
2872
+ const checkResult = await exec(["dpkg", "-s", pkg]);
2873
+ result.set(pkg, checkResult.success && checkResult.stdout.includes("Status: install ok installed"));
2874
+ }
2875
+ return result;
2876
+ }
2877
+ }
2878
+
2879
+ // src/lib/package-managers/dnf.ts
2880
+ init_runtime();
2881
+ init_runtime();
2882
+ async function runDnfCommand(args, callbacks, sudo = false) {
2883
+ const cmd = sudo ? ["sudo", "dnf", ...args] : ["dnf", ...args];
2884
+ if (callbacks?.onLog) {
2885
+ const exitCode = await execStreaming(cmd, callbacks.onLog);
2886
+ return exitCode === 0;
2887
+ }
2888
+ const result = await exec(cmd);
2889
+ return result.success;
2890
+ }
2891
+
2892
+ class Dnf {
2893
+ type = "dnf";
2894
+ displayName = "DNF";
2895
+ async isAvailable() {
2896
+ return commandExists("dnf");
2897
+ }
2898
+ async update(callbacks) {
2899
+ return runDnfCommand(["check-update", "-y"], callbacks, true);
2900
+ }
2901
+ async install(packages, callbacks) {
2902
+ if (packages.length === 0)
2903
+ return true;
2904
+ return runDnfCommand(["install", "-y", ...packages], callbacks, true);
2905
+ }
2906
+ async uninstall(packages, callbacks) {
2907
+ if (packages.length === 0)
2908
+ return true;
2909
+ return runDnfCommand(["remove", "-y", ...packages], callbacks, true);
2910
+ }
2911
+ async upgrade(packages, callbacks) {
2912
+ if (packages && packages.length > 0) {
2913
+ return runDnfCommand(["upgrade", "-y", ...packages], callbacks, true);
2914
+ }
2915
+ return runDnfCommand(["upgrade", "-y"], callbacks, true);
2916
+ }
2917
+ async listInstalled() {
2918
+ const result = await exec(["dnf", "list", "installed", "-q"]);
2919
+ if (!result.success)
2920
+ return [];
2921
+ return result.stdout.split(`
2922
+ `).filter(Boolean).slice(1).map((line) => {
2923
+ const parts = line.trim().split(/\s+/);
2924
+ if (parts.length >= 2) {
2925
+ const fullName = parts[0];
2926
+ const name = fullName.replace(/\.\w+$/, "");
2927
+ return { name, version: parts[1] };
2928
+ }
2929
+ return null;
2930
+ }).filter((pkg) => pkg !== null);
2931
+ }
2932
+ async listOutdated() {
2933
+ const result = await exec(["dnf", "check-update", "-q"]);
2934
+ if (!result.stdout)
2935
+ return [];
2936
+ return result.stdout.split(`
2937
+ `).filter((line) => line.trim() && !line.startsWith("Last metadata")).map((line) => {
2938
+ const parts = line.trim().split(/\s+/);
2939
+ if (parts.length >= 2) {
2940
+ const fullName = parts[0];
2941
+ const name = fullName.replace(/\.\w+$/, "");
2942
+ return {
2943
+ name,
2944
+ currentVersion: "installed",
2945
+ newVersion: parts[1]
2946
+ };
2947
+ }
2948
+ return null;
2949
+ }).filter((pkg) => pkg !== null);
2950
+ }
2951
+ async listLeaves() {
2952
+ const hasLeaves = await commandExists("dnf-leaves");
2953
+ if (hasLeaves) {
2954
+ const result2 = await exec(["dnf", "leaves"]);
2955
+ if (result2.success) {
2956
+ return result2.stdout.split(`
2957
+ `).filter(Boolean);
2958
+ }
2959
+ }
2960
+ const result = await exec(["dnf", "repoquery", "--userinstalled", "-q"]);
2961
+ if (!result.success)
2962
+ return [];
2963
+ return result.stdout.split(`
2964
+ `).filter(Boolean).map((line) => line.replace(/\.\w+$/, ""));
2965
+ }
2966
+ async cleanup(callbacks) {
2967
+ const autoremove = await runDnfCommand(["autoremove", "-y"], callbacks, true);
2968
+ const clean = await runDnfCommand(["clean", "all"], callbacks, true);
2969
+ return autoremove && clean;
2970
+ }
2971
+ async addRepository(repo, callbacks) {
2972
+ if (repo.startsWith("copr:")) {
2973
+ const coprRepo = repo.replace("copr:", "");
2974
+ return runDnfCommand(["copr", "enable", "-y", coprRepo], callbacks, true);
2975
+ }
2976
+ return runDnfCommand(["config-manager", "--add-repo", repo], callbacks, true);
2977
+ }
2978
+ async isInstalled(packages) {
2979
+ const result = new Map;
2980
+ for (const pkg of packages) {
2981
+ const checkResult = await exec(["rpm", "-q", pkg]);
2982
+ result.set(pkg, checkResult.success);
2983
+ }
2984
+ return result;
2985
+ }
2986
+ }
2987
+
2988
+ // src/lib/package-managers/flatpak.ts
2989
+ init_runtime();
2990
+ init_runtime();
2991
+ async function runFlatpakCommand(args, callbacks) {
2992
+ const cmd = ["flatpak", ...args];
2993
+ if (callbacks?.onLog) {
2994
+ const exitCode = await execStreaming(cmd, callbacks.onLog);
2995
+ return exitCode === 0;
2996
+ }
2997
+ const result = await exec(cmd);
2998
+ return result.success;
2999
+ }
3000
+
3001
+ class Flatpak {
3002
+ type = "flatpak";
3003
+ displayName = "Flatpak";
3004
+ async isAvailable() {
3005
+ return commandExists("flatpak");
3006
+ }
3007
+ async update(callbacks) {
3008
+ return true;
3009
+ }
3010
+ async install(packages, callbacks) {
3011
+ if (packages.length === 0)
3012
+ return true;
3013
+ for (const appId of packages) {
3014
+ const success = await runFlatpakCommand(["install", "-y", "--noninteractive", "flathub", appId], callbacks);
3015
+ if (!success)
3016
+ return false;
3017
+ }
3018
+ return true;
3019
+ }
3020
+ async uninstall(packages, callbacks) {
3021
+ if (packages.length === 0)
3022
+ return true;
3023
+ for (const appId of packages) {
3024
+ const success = await runFlatpakCommand(["uninstall", "-y", "--noninteractive", appId], callbacks);
3025
+ if (!success)
3026
+ return false;
3027
+ }
3028
+ return true;
3029
+ }
3030
+ async upgrade(packages, callbacks) {
3031
+ if (packages && packages.length > 0) {
3032
+ for (const appId of packages) {
3033
+ const success = await runFlatpakCommand(["update", "-y", "--noninteractive", appId], callbacks);
3034
+ if (!success)
3035
+ return false;
3036
+ }
3037
+ return true;
3038
+ }
3039
+ return runFlatpakCommand(["update", "-y", "--noninteractive"], callbacks);
3040
+ }
3041
+ async listInstalled() {
3042
+ const result = await exec(["flatpak", "list", "--app", "--columns=application,version"]);
3043
+ if (!result.success)
3044
+ return [];
3045
+ return result.stdout.split(`
3046
+ `).filter(Boolean).map((line) => {
3047
+ const [name, version] = line.split("\t");
3048
+ return { name: name.trim(), version: version?.trim() || "unknown" };
3049
+ });
3050
+ }
3051
+ async listOutdated() {
3052
+ const result = await exec(["flatpak", "remote-ls", "--updates", "--columns=application,version"]);
3053
+ if (!result.success)
3054
+ return [];
3055
+ return result.stdout.split(`
3056
+ `).filter(Boolean).map((line) => {
3057
+ const [name, newVersion] = line.split("\t");
3058
+ return {
3059
+ name: name.trim(),
3060
+ currentVersion: "installed",
3061
+ newVersion: newVersion?.trim() || "unknown"
3062
+ };
3063
+ });
3064
+ }
3065
+ async cleanup(callbacks) {
3066
+ return runFlatpakCommand(["uninstall", "-y", "--unused", "--noninteractive"], callbacks);
3067
+ }
3068
+ async addRepository(repo, callbacks) {
3069
+ if (repo === "flathub") {
3070
+ return runFlatpakCommand(["remote-add", "--if-not-exists", "flathub", "https://flathub.org/repo/flathub.flatpakrepo"], callbacks);
3071
+ }
3072
+ const [name, url] = repo.split(" ");
3073
+ if (!url)
3074
+ return false;
3075
+ return runFlatpakCommand(["remote-add", "--if-not-exists", name, url], callbacks);
3076
+ }
3077
+ async isInstalled(packages) {
3078
+ const result = new Map;
3079
+ const listResult = await exec(["flatpak", "list", "--app", "--columns=application"]);
3080
+ const installed = new Set(listResult.stdout.split(`
3081
+ `).filter(Boolean).map((l) => l.trim()));
3082
+ for (const appId of packages) {
3083
+ result.set(appId, installed.has(appId));
3084
+ }
3085
+ return result;
3086
+ }
3087
+ async ensureFlathub(callbacks) {
3088
+ const result = await exec(["flatpak", "remotes"]);
3089
+ if (result.success && result.stdout.includes("flathub")) {
3090
+ return true;
3091
+ }
3092
+ return this.addRepository("flathub", callbacks);
3093
+ }
3094
+ }
3095
+
3096
+ // src/lib/package-managers/index.ts
3097
+ var managerInstances = new Map;
3098
+ function getPackageManager(type) {
3099
+ const existing = managerInstances.get(type);
3100
+ if (existing)
3101
+ return existing;
3102
+ let manager;
3103
+ switch (type) {
3104
+ case "homebrew":
3105
+ manager = new HomebrewFormulas;
3106
+ break;
3107
+ case "homebrew-casks":
3108
+ manager = new HomebrewCasks;
3109
+ break;
3110
+ case "mas":
3111
+ manager = new MacAppStore;
3112
+ break;
3113
+ case "pacman":
3114
+ manager = new Pacman;
3115
+ break;
3116
+ case "aur":
3117
+ manager = new AurPackageManager;
3118
+ break;
3119
+ case "apt":
3120
+ manager = new Apt;
3121
+ break;
3122
+ case "dnf":
3123
+ manager = new Dnf;
3124
+ break;
3125
+ case "flatpak":
3126
+ manager = new Flatpak;
3127
+ break;
3128
+ default:
3129
+ throw new Error(`Unknown package manager type: ${type}`);
3130
+ }
3131
+ managerInstances.set(type, manager);
3132
+ return manager;
3133
+ }
3134
+ async function getAvailableManagers() {
3135
+ const platformInfo = await getPlatformInfo();
3136
+ const managers = [];
3137
+ for (const type of platformInfo.availableManagers) {
3138
+ const manager = getPackageManager(type);
3139
+ if (await manager.isAvailable()) {
3140
+ managers.push(manager);
3141
+ if (type === "homebrew") {
3142
+ const casks = getPackageManager("homebrew-casks");
3143
+ if (await casks.isAvailable()) {
3144
+ managers.push(casks);
3145
+ }
3146
+ }
3147
+ }
3148
+ }
3149
+ return managers;
3150
+ }
3151
+
3152
+ // src/lib/lockfile.ts
3153
+ async function fetchInstalledVersionsV2() {
3154
+ const platform = await getPlatformInfo();
3155
+ const config = await loadPkgConfig();
3156
+ const now = new Date().toISOString();
3157
+ const packages = {};
3158
+ const managers = await getAvailableManagers();
3159
+ for (const manager of managers) {
3160
+ const installed = await manager.listInstalled();
3161
+ for (const pkg of installed) {
3162
+ const key = `${manager.type}:${pkg.name}`;
3163
+ packages[key] = {
3164
+ version: pkg.version,
3165
+ installedAt: pkg.installedAt || now,
3166
+ manager: manager.type
3167
+ };
3168
+ }
3169
+ }
3170
+ if (platform.os === "darwin") {
3171
+ const formulaPackages = config.macos?.formulas || [];
3172
+ const globalPackages = config.global?.packages || [];
3173
+ const allFormulas = [...globalPackages, ...formulaPackages];
3174
+ if (allFormulas.length > 0) {
3175
+ const result = await exec([
3176
+ "brew",
3177
+ "info",
3178
+ "--json=v2",
3179
+ ...allFormulas
3180
+ ]);
3181
+ if (result.success && result.stdout) {
3182
+ try {
3183
+ const info = JSON.parse(result.stdout);
3184
+ for (const formula of info.formulae) {
3185
+ if (formula.installed.length > 0) {
3186
+ const key = `homebrew:${formula.name}`;
3187
+ if (packages[key]) {
3188
+ packages[key].tap = formula.tap;
3189
+ }
3190
+ }
3191
+ }
3192
+ } catch {}
3193
+ }
3194
+ }
3195
+ }
3196
+ return packages;
3197
+ }
3198
+ async function updateLockfile() {
3199
+ const existing = await loadPkgLock();
3200
+ const packages = await fetchInstalledVersionsV2();
3201
+ const mergedPackages = {};
3202
+ for (const [key, info] of Object.entries(packages)) {
3203
+ if (existing?.version === 2) {
3204
+ const prev = existing.packages[key];
3205
+ if (prev && prev.version === info.version) {
3206
+ mergedPackages[key] = prev;
3207
+ } else {
3208
+ mergedPackages[key] = info;
3209
+ }
3210
+ } else if (existing?.version === 1) {
3211
+ const [manager, name] = key.split(":");
3212
+ let prev;
3213
+ if (manager === "homebrew") {
3214
+ prev = existing.formulas[name];
3215
+ } else if (manager === "homebrew-casks") {
3216
+ prev = existing.casks[name];
3217
+ }
3218
+ if (prev && prev.version === info.version) {
3219
+ mergedPackages[key] = {
3220
+ ...info,
3221
+ installedAt: prev.installedAt
3222
+ };
3223
+ } else {
3224
+ mergedPackages[key] = info;
3225
+ }
3226
+ } else {
3227
+ mergedPackages[key] = info;
3228
+ }
3229
+ }
3230
+ const lock = {
3231
+ version: 2,
3232
+ lastUpdated: new Date().toISOString(),
3233
+ packages: mergedPackages
3234
+ };
3235
+ await savePkgLock(lock);
3236
+ return lock;
3237
+ }
3238
+ async function fetchInstalledVersions() {
3239
+ const config = await loadPkgConfig();
3240
+ const now = new Date().toISOString();
3241
+ const formulas = {};
3242
+ const casks = {};
3243
+ const allFormulas = [
3244
+ ...config.global?.packages || [],
3245
+ ...config.macos?.formulas || []
3246
+ ];
3247
+ if (allFormulas.length > 0) {
3248
+ const result = await exec([
3249
+ "brew",
3250
+ "info",
3251
+ "--json=v2",
3252
+ ...allFormulas
3253
+ ]);
3254
+ if (result.success && result.stdout) {
3255
+ const info = JSON.parse(result.stdout);
3256
+ for (const formula of info.formulae) {
3257
+ if (formula.installed.length > 0) {
3258
+ formulas[formula.name] = {
3259
+ version: formula.installed[0].version,
3260
+ tap: formula.tap,
3261
+ installedAt: now
3262
+ };
3263
+ }
3264
+ }
3265
+ }
3266
+ }
3267
+ const allCasks = config.macos?.casks || [];
3268
+ if (allCasks.length > 0) {
3269
+ const result = await exec([
3270
+ "brew",
3271
+ "info",
3272
+ "--json=v2",
3273
+ "--cask",
3274
+ ...allCasks
3275
+ ]);
3276
+ if (result.success && result.stdout) {
3277
+ const info = JSON.parse(result.stdout);
3278
+ for (const cask of info.casks) {
3279
+ if (cask.installed) {
3280
+ casks[cask.token] = {
3281
+ version: cask.installed,
3282
+ installedAt: now
3283
+ };
3284
+ }
3285
+ }
3286
+ }
3287
+ }
3288
+ return { formulas, casks };
3289
+ }
3290
+ async function generateLockfile() {
3291
+ const { formulas, casks } = await fetchInstalledVersions();
3292
+ return {
3293
+ version: 1,
3294
+ lastUpdated: new Date().toISOString(),
3295
+ formulas,
3296
+ casks
3297
+ };
3298
+ }
3299
+ async function getChangedPackages() {
3300
+ const existing = await loadPkgLock();
3301
+ const packages = await fetchInstalledVersionsV2();
3302
+ const added = [];
3303
+ const removed = [];
3304
+ const upgraded = [];
3305
+ if (!existing) {
3306
+ added.push(...Object.keys(packages));
3307
+ return { added, removed, upgraded };
3308
+ }
3309
+ if (existing.version === 2) {
3310
+ for (const [key, info] of Object.entries(packages)) {
3311
+ if (!existing.packages[key]) {
3312
+ added.push(key);
3313
+ } else if (existing.packages[key].version !== info.version) {
3314
+ upgraded.push({
3315
+ name: key,
3316
+ from: existing.packages[key].version,
3317
+ to: info.version
3318
+ });
3319
+ }
3320
+ }
3321
+ for (const key of Object.keys(existing.packages)) {
3322
+ if (!packages[key]) {
3323
+ removed.push(key);
3324
+ }
3325
+ }
3326
+ } else {
3327
+ const { formulas, casks } = await fetchInstalledVersions();
3328
+ for (const [name, info] of Object.entries(formulas)) {
3329
+ if (!existing.formulas[name]) {
3330
+ added.push(name);
3331
+ } else if (existing.formulas[name].version !== info.version) {
3332
+ upgraded.push({
3333
+ name,
3334
+ from: existing.formulas[name].version,
3335
+ to: info.version
3336
+ });
3337
+ }
3338
+ }
3339
+ for (const name of Object.keys(existing.formulas)) {
3340
+ if (!formulas[name]) {
3341
+ removed.push(name);
3342
+ }
3343
+ }
3344
+ for (const [name, info] of Object.entries(casks)) {
3345
+ if (!existing.casks[name]) {
3346
+ added.push(name);
3347
+ } else if (existing.casks[name].version !== info.version) {
3348
+ upgraded.push({
3349
+ name,
3350
+ from: existing.casks[name].version,
3351
+ to: info.version
3352
+ });
3353
+ }
3354
+ }
3355
+ for (const name of Object.keys(existing.casks)) {
3356
+ if (!casks[name]) {
3357
+ removed.push(name);
3358
+ }
3359
+ }
3360
+ }
3361
+ return { added, removed, upgraded };
3362
+ }
3363
+
3364
+ // src/types/pkg-config.ts
3365
+ var SYSTEM_APP_IDS = [
3366
+ 409183694,
3367
+ 409203825,
3368
+ 409201541,
3369
+ 408981434,
3370
+ 682658836,
3371
+ 424389933,
3372
+ 424390742,
3373
+ 413897608,
3374
+ 1274495053,
3375
+ 425424353,
3376
+ 497799835,
3377
+ 634148309,
3378
+ 1480068668,
3379
+ 803453959,
3380
+ 1295203466,
3381
+ 1444383602,
3382
+ 640199958,
3383
+ 899247664,
3384
+ 1176895641,
3385
+ 1451685025
3386
+ ];
3387
+
3388
+ // src/cli/pkg-sync.ts
3389
+ var colors3 = {
3390
+ red: "\x1B[0;31m",
3391
+ green: "\x1B[0;32m",
3392
+ blue: "\x1B[0;34m",
3393
+ yellow: "\x1B[1;33m",
3394
+ cyan: "\x1B[0;36m",
3395
+ bold: "\x1B[1m",
3396
+ reset: "\x1B[0m"
3397
+ };
3398
+ async function getPackageSetsForPlatform(config, platform) {
3399
+ const sets = [];
3400
+ if (platform.os === "darwin") {
3401
+ const formulas = getPackageManager("homebrew");
3402
+ const casks = getPackageManager("homebrew-casks");
3403
+ const mas = getPackageManager("mas");
3404
+ const globalPkgs = config.global?.packages || [];
3405
+ const macosFormulas = config.macos?.formulas || [];
3406
+ const allFormulas = [...globalPkgs, ...macosFormulas];
3407
+ if (allFormulas.length > 0 || (config.macos?.taps || []).length > 0) {
3408
+ sets.push({
3409
+ manager: formulas,
3410
+ packages: allFormulas,
3411
+ repositories: config.macos?.taps
3412
+ });
3413
+ }
3414
+ const macosCasks = config.macos?.casks || [];
3415
+ if (macosCasks.length > 0) {
3416
+ sets.push({
3417
+ manager: casks,
3418
+ packages: macosCasks
3419
+ });
3420
+ }
3421
+ const masMappings = config.macos?.mas || {};
3422
+ const masIds = Object.values(masMappings).map(String);
3423
+ if (masIds.length > 0 && await mas.isAvailable()) {
3424
+ sets.push({
3425
+ manager: mas,
3426
+ packages: masIds
3427
+ });
3428
+ }
3429
+ } else {
3430
+ const distro = platform.distro;
3431
+ const globalPkgs = config.global?.packages || [];
3432
+ const linuxPkgs = config.linux?.packages || [];
3433
+ if (distro === "arch") {
3434
+ const pacman = getPackageManager("pacman");
3435
+ const aur = getPackageManager("aur");
3436
+ const archPkgs = config.arch?.packages || [];
3437
+ const allPacmanPkgs = [...globalPkgs, ...linuxPkgs, ...archPkgs];
3438
+ if (allPacmanPkgs.length > 0) {
3439
+ sets.push({
3440
+ manager: pacman,
3441
+ packages: allPacmanPkgs
3442
+ });
3443
+ }
3444
+ const aurPkgs = config.arch?.aur || [];
3445
+ if (aurPkgs.length > 0 && await aur.isAvailable()) {
3446
+ sets.push({
3447
+ manager: aur,
3448
+ packages: aurPkgs
3449
+ });
3450
+ }
3451
+ } else if (distro === "debian" || distro === "ubuntu") {
3452
+ const apt = getPackageManager("apt");
3453
+ const debianPkgs = config.debian?.packages || [];
3454
+ const allAptPkgs = [...globalPkgs, ...linuxPkgs, ...debianPkgs];
3455
+ if (allAptPkgs.length > 0 || (config.debian?.ppas || []).length > 0) {
3456
+ sets.push({
3457
+ manager: apt,
3458
+ packages: allAptPkgs,
3459
+ repositories: config.debian?.ppas
3460
+ });
3461
+ }
3462
+ } else if (distro === "fedora" || distro === "rhel") {
3463
+ const dnf = getPackageManager("dnf");
3464
+ const fedoraPkgs = config.fedora?.packages || [];
3465
+ const allDnfPkgs = [...globalPkgs, ...linuxPkgs, ...fedoraPkgs];
3466
+ if (allDnfPkgs.length > 0 || (config.fedora?.copr || []).length > 0) {
3467
+ sets.push({
3468
+ manager: dnf,
3469
+ packages: allDnfPkgs,
3470
+ repositories: config.fedora?.copr?.map((r) => `copr:${r}`)
3471
+ });
3472
+ }
3473
+ } else {
3474
+ const managers = await getAvailableManagers();
3475
+ const primaryManager = managers.find((m) => m.type === "pacman" || m.type === "apt" || m.type === "dnf");
3476
+ if (primaryManager) {
3477
+ const allPkgs = [...globalPkgs, ...linuxPkgs];
3478
+ if (allPkgs.length > 0) {
3479
+ sets.push({
3480
+ manager: primaryManager,
3481
+ packages: allPkgs
3482
+ });
3483
+ }
3484
+ }
3485
+ }
3486
+ const flatpak = getPackageManager("flatpak");
3487
+ const flatpakApps = config.linux?.flatpak || [];
3488
+ if (flatpakApps.length > 0 && await flatpak.isAvailable()) {
3489
+ sets.push({
3490
+ manager: flatpak,
3491
+ packages: flatpakApps
3492
+ });
3493
+ }
3494
+ }
3495
+ return sets;
2177
3496
  }
2178
- async function getOutdatedMas() {
2179
- const result = await exec(["mas", "outdated"]);
2180
- if (!result.success || !result.stdout)
2181
- return [];
2182
- return result.stdout.split(`
2183
- `).filter(Boolean).map((line) => {
2184
- const match = line.match(/^(\d+)\s+(.+?)(?:\s+\(|$)/);
2185
- if (match) {
2186
- return { id: parseInt(match[1], 10), name: match[2].trim() };
3497
+ async function checkDependencies(platform) {
3498
+ if (platform.os === "darwin") {
3499
+ if (!await commandExists("brew")) {
3500
+ console.error(`${colors3.red}Error: Homebrew not installed${colors3.reset}`);
3501
+ process.exit(1);
2187
3502
  }
2188
- return null;
2189
- }).filter((app) => app !== null);
3503
+ } else {
3504
+ const hasPackageManager = await Promise.all([
3505
+ commandExists("pacman"),
3506
+ commandExists("apt"),
3507
+ commandExists("dnf")
3508
+ ]).then((results) => results.some(Boolean));
3509
+ if (!hasPackageManager) {
3510
+ console.error(`${colors3.red}Error: No supported package manager found (pacman, apt, or dnf)${colors3.reset}`);
3511
+ process.exit(1);
3512
+ }
3513
+ }
2190
3514
  }
2191
3515
  async function upgradeWithVerification(cb = null) {
2192
3516
  const log = cb?.onLog ?? console.log;
3517
+ const platform = await getPlatformInfo();
2193
3518
  const result = {
2194
3519
  attempted: [],
2195
3520
  succeeded: [],
2196
3521
  failed: [],
2197
3522
  stillOutdated: []
2198
3523
  };
3524
+ const callbacks = cb ? { onLog: cb.onLog } : undefined;
2199
3525
  log(`
2200
3526
  ${colors3.cyan}=== Checking for updates ===${colors3.reset}
2201
3527
  `);
2202
- await runCommand(["brew", "update"], cb);
2203
- const beforeUpgrade = await getOutdatedPackages();
2204
- result.attempted = beforeUpgrade.map((p) => p.name);
2205
- if (beforeUpgrade.length === 0) {
2206
- log(`
2207
- ${colors3.green}All brew packages are up to date${colors3.reset}`);
2208
- } else {
2209
- log(`
2210
- ${colors3.yellow}Found ${beforeUpgrade.length} outdated packages${colors3.reset}
2211
- `);
2212
- log(`${colors3.cyan}=== Upgrading formulas ===${colors3.reset}
2213
- `);
2214
- await runCommand(["brew", "upgrade", "--formula"], cb);
3528
+ const managers = await getAvailableManagers();
3529
+ for (const manager of managers) {
2215
3530
  log(`
2216
- ${colors3.cyan}=== Upgrading casks ===${colors3.reset}
3531
+ ${colors3.cyan}--- ${manager.displayName} ---${colors3.reset}
2217
3532
  `);
2218
- await runCommand(["brew", "upgrade", "--cask", "--greedy"], cb);
2219
- log(`
2220
- ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
3533
+ await manager.update(callbacks);
3534
+ const outdated = await manager.listOutdated();
3535
+ if (outdated.length === 0) {
3536
+ log(`${colors3.green}All packages are up to date${colors3.reset}`);
3537
+ continue;
3538
+ }
3539
+ log(`${colors3.yellow}Found ${outdated.length} outdated packages${colors3.reset}
2221
3540
  `);
2222
- const afterUpgrade = await getOutdatedPackages();
2223
- const stillOutdatedSet = new Set(afterUpgrade.map((p) => p.name));
2224
- for (const pkg of beforeUpgrade) {
3541
+ result.attempted.push(...outdated.map((p) => p.name));
3542
+ await manager.upgrade(undefined, callbacks);
3543
+ const stillOutdated = await manager.listOutdated();
3544
+ const stillOutdatedSet = new Set(stillOutdated.map((p) => p.name));
3545
+ for (const pkg of outdated) {
2225
3546
  if (stillOutdatedSet.has(pkg.name)) {
2226
3547
  result.stillOutdated.push(pkg.name);
2227
3548
  } else {
@@ -2232,93 +3553,70 @@ ${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
2232
3553
  log(`${colors3.yellow}${result.stillOutdated.length} packages still outdated, retrying individually...${colors3.reset}
2233
3554
  `);
2234
3555
  for (const pkgName of [...result.stillOutdated]) {
2235
- const pkg = afterUpgrade.find((p) => p.name === pkgName);
2236
- if (!pkg)
2237
- continue;
2238
3556
  log(` Retrying ${colors3.blue}${pkgName}${colors3.reset}...`);
2239
- const upgradeCmd = pkg.type === "cask" ? ["brew", "upgrade", "--cask", pkgName] : ["brew", "upgrade", pkgName];
2240
- const retryResult = await exec(upgradeCmd);
2241
- const checkResult = await exec([
2242
- "brew",
2243
- "outdated",
2244
- pkg.type === "cask" ? "--cask" : "--formula",
2245
- "--quiet"
2246
- ]);
2247
- const stillOutdatedNow = checkResult.stdout.split(`
2248
- `).filter(Boolean);
2249
- if (!stillOutdatedNow.includes(pkgName)) {
3557
+ const upgradeSuccess = await manager.upgrade([pkgName], callbacks);
3558
+ const checkOutdated = await manager.listOutdated();
3559
+ const stillFailing = checkOutdated.some((p) => p.name === pkgName);
3560
+ if (!stillFailing) {
2250
3561
  result.succeeded.push(pkgName);
2251
3562
  result.stillOutdated = result.stillOutdated.filter((n) => n !== pkgName);
2252
3563
  log(` ${colors3.green}✓ Success${colors3.reset}`);
2253
3564
  } else {
2254
3565
  result.failed.push(pkgName);
2255
3566
  result.stillOutdated = result.stillOutdated.filter((n) => n !== pkgName);
2256
- log(` ${colors3.red}✗ Failed${colors3.reset} ${retryResult.stderr ? `(${retryResult.stderr.split(`
2257
- `)[0]})` : ""}`);
3567
+ log(` ${colors3.red}✗ Failed${colors3.reset}`);
2258
3568
  }
2259
3569
  }
2260
3570
  }
2261
3571
  }
2262
- if (await commandExists("mas")) {
2263
- const masOutdated = await getOutdatedMas();
2264
- if (masOutdated.length > 0) {
2265
- log(`
2266
- ${colors3.cyan}=== Upgrading Mac App Store apps ===${colors3.reset}
2267
- `);
2268
- await runCommand(["mas", "upgrade"], cb, undefined, true);
2269
- }
2270
- }
2271
3572
  log(`
2272
3573
  ${colors3.cyan}=== Cleanup ===${colors3.reset}
2273
3574
  `);
2274
- await runCommand(["brew", "cleanup"], cb);
3575
+ for (const manager of managers) {
3576
+ await manager.cleanup(callbacks);
3577
+ }
2275
3578
  log(`
2276
3579
  ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
2277
3580
  `);
2278
3581
  const lock = await updateLockfile();
2279
- const lockTotal = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
3582
+ const lockTotal = lock.version === 2 ? Object.keys(lock.packages).length : Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
2280
3583
  log(` Locked ${lockTotal} packages`);
2281
3584
  return result;
2282
3585
  }
2283
3586
  async function upgradeInteractive(cb = null) {
2284
3587
  const log = cb?.onLog ?? console.log;
2285
3588
  const askPrompt = cb?.onPrompt ?? (async (q) => (prompt(q) || "").trim().toLowerCase());
2286
- log(`
2287
- ${colors3.cyan}=== Checking for updates ===${colors3.reset}
2288
- `);
2289
- await runCommand(["brew", "update"], cb);
2290
- const outdated = await getOutdatedPackages();
2291
- if (outdated.length === 0) {
3589
+ const callbacks = cb ? { onLog: cb.onLog } : undefined;
3590
+ const managers = await getAvailableManagers();
3591
+ for (const manager of managers) {
2292
3592
  log(`
2293
- ${colors3.green}All packages are up to date${colors3.reset}
3593
+ ${colors3.cyan}=== ${manager.displayName} ===${colors3.reset}
2294
3594
  `);
2295
- return;
2296
- }
2297
- log(`
2298
- ${colors3.yellow}Found ${outdated.length} outdated packages${colors3.reset}
3595
+ await manager.update(callbacks);
3596
+ const outdated = await manager.listOutdated();
3597
+ if (outdated.length === 0) {
3598
+ log(`${colors3.green}All packages are up to date${colors3.reset}
2299
3599
  `);
2300
- for (const pkg of outdated) {
2301
- const question = `Upgrade ${colors3.blue}${pkg.name}${colors3.reset} (${pkg.type})?`;
2302
- const answer = await askPrompt(question, ["y", "n", "q"]);
2303
- if (answer === "q") {
2304
- log(`
2305
- ${colors3.yellow}Upgrade cancelled${colors3.reset}`);
2306
- return;
3600
+ continue;
2307
3601
  }
2308
- if (answer === "y" || answer === "yes") {
2309
- const cmd = pkg.type === "cask" ? ["brew", "upgrade", "--cask", pkg.name] : ["brew", "upgrade", pkg.name];
2310
- await runCommand(cmd, cb);
3602
+ log(`${colors3.yellow}Found ${outdated.length} outdated packages${colors3.reset}
3603
+ `);
3604
+ for (const pkg of outdated) {
3605
+ const question = `Upgrade ${colors3.blue}${pkg.name}${colors3.reset} (${pkg.currentVersion} -> ${pkg.newVersion})?`;
3606
+ const answer = await askPrompt(question, ["y", "n", "q"]);
3607
+ if (answer === "q") {
3608
+ log(`
3609
+ ${colors3.yellow}Upgrade cancelled${colors3.reset}`);
3610
+ return;
3611
+ }
3612
+ if (answer === "y" || answer === "yes") {
3613
+ await manager.upgrade([pkg.name], callbacks);
3614
+ }
2311
3615
  }
2312
3616
  }
2313
- const stillOutdated = await getOutdatedPackages();
2314
- if (stillOutdated.length > 0) {
2315
- log(`
2316
- ${colors3.yellow}Still outdated: ${stillOutdated.map((p) => p.name).join(", ")}${colors3.reset}`);
2317
- } else {
2318
- log(`
2319
- ${colors3.green}All selected packages upgraded successfully${colors3.reset}`);
3617
+ for (const manager of managers) {
3618
+ await manager.cleanup(callbacks);
2320
3619
  }
2321
- await runCommand(["brew", "cleanup"], cb);
2322
3620
  log(`
2323
3621
  ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
2324
3622
  `);
@@ -2326,61 +3624,50 @@ ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
2326
3624
  }
2327
3625
  async function syncPackages(config, cb = null) {
2328
3626
  const log = cb?.onLog ?? console.log;
2329
- if (config.config.autoUpdate) {
2330
- log(`
2331
- ${colors3.cyan}=== Updating Homebrew ===${colors3.reset}
2332
- `);
2333
- await runCommand(["brew", "update"], cb);
2334
- }
2335
- log(`
2336
- ${colors3.cyan}=== Installing taps ===${colors3.reset}
2337
- `);
2338
- const tappedResult = await exec(["brew", "tap"]);
2339
- const tapped = tappedResult.stdout.split(`
2340
- `).filter(Boolean);
2341
- for (const tap of config.taps) {
2342
- if (!tapped.includes(tap)) {
2343
- log(` Adding tap: ${colors3.blue}${tap}${colors3.reset}`);
2344
- await runCommand(["brew", "tap", tap], cb);
2345
- }
2346
- }
3627
+ const platform = await getPlatformInfo();
3628
+ const callbacks = cb ? { onLog: cb.onLog } : undefined;
2347
3629
  log(`
2348
- ${colors3.cyan}=== Installing packages ===${colors3.reset}
3630
+ ${colors3.cyan}Platform: ${getPlatformDisplayName(platform)}${colors3.reset}
2349
3631
  `);
2350
- const installedFormulas = (await exec(["brew", "list", "--formula"])).stdout.split(`
2351
- `).filter(Boolean);
2352
- for (const pkg of config.packages) {
2353
- if (!installedFormulas.includes(pkg)) {
2354
- log(` Installing: ${colors3.blue}${pkg}${colors3.reset}`);
2355
- await runCommand(["brew", "install", pkg], cb);
2356
- }
3632
+ const packageSets = await getPackageSetsForPlatform(config, platform);
3633
+ if (packageSets.length === 0) {
3634
+ log(`${colors3.yellow}No packages configured for this platform${colors3.reset}`);
3635
+ return;
2357
3636
  }
2358
- log(`
2359
- ${colors3.cyan}=== Installing casks ===${colors3.reset}
3637
+ if (config.config.autoUpdate) {
3638
+ log(`
3639
+ ${colors3.cyan}=== Updating package managers ===${colors3.reset}
2360
3640
  `);
2361
- const installedCasks = (await exec(["brew", "list", "--cask"])).stdout.split(`
2362
- `).filter(Boolean);
2363
- for (const cask of config.casks) {
2364
- if (!installedCasks.includes(cask)) {
2365
- log(` Installing: ${colors3.blue}${cask}${colors3.reset}`);
2366
- await runCommand(["brew", "install", "--cask", cask], cb);
3641
+ for (const set of packageSets) {
3642
+ log(` Updating ${set.manager.displayName}...`);
3643
+ await set.manager.update(callbacks);
2367
3644
  }
2368
3645
  }
2369
- if (await commandExists("mas")) {
3646
+ for (const set of packageSets) {
2370
3647
  log(`
2371
- ${colors3.cyan}=== Installing Mac App Store apps ===${colors3.reset}
3648
+ ${colors3.cyan}=== ${set.manager.displayName} ===${colors3.reset}
2372
3649
  `);
2373
- const masResult = await exec(["mas", "list"]);
2374
- const installedMas = masResult.stdout.split(`
2375
- `).filter(Boolean).map((line) => {
2376
- const match = line.match(/^(\d+)/);
2377
- return match ? parseInt(match[1], 10) : 0;
2378
- });
2379
- for (const [name, id] of Object.entries(config.mas)) {
2380
- if (!installedMas.includes(id)) {
2381
- log(` Installing: ${colors3.blue}${name}${colors3.reset}`);
2382
- await runCommand(["mas", "install", String(id)], cb, undefined, true);
3650
+ if (set.repositories && set.repositories.length > 0 && set.manager.addRepository) {
3651
+ log(` Adding repositories...`);
3652
+ for (const repo of set.repositories) {
3653
+ log(` ${colors3.blue}${repo}${colors3.reset}`);
3654
+ await set.manager.addRepository(repo, callbacks);
3655
+ }
3656
+ }
3657
+ if (set.packages.length === 0) {
3658
+ log(` No packages to install`);
3659
+ continue;
3660
+ }
3661
+ const installedMap = await set.manager.isInstalled(set.packages);
3662
+ const toInstall = set.packages.filter((pkg) => !installedMap.get(pkg));
3663
+ if (toInstall.length === 0) {
3664
+ log(` All ${set.packages.length} packages already installed`);
3665
+ } else {
3666
+ log(` Installing ${toInstall.length} packages...`);
3667
+ for (const pkg of toInstall) {
3668
+ log(` ${colors3.blue}${pkg}${colors3.reset}`);
2383
3669
  }
3670
+ await set.manager.install(toInstall, callbacks);
2384
3671
  }
2385
3672
  }
2386
3673
  if (config.config.purge) {
@@ -2390,7 +3677,7 @@ ${colors3.cyan}=== Installing Mac App Store apps ===${colors3.reset}
2390
3677
  ${colors3.cyan}=== Updating lockfile ===${colors3.reset}
2391
3678
  `);
2392
3679
  const lock = await updateLockfile();
2393
- const lockTotal = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
3680
+ const lockTotal = lock.version === 2 ? Object.keys(lock.packages).length : Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
2394
3681
  log(` Locked ${lockTotal} packages`);
2395
3682
  log(`
2396
3683
  ${colors3.green}=== Sync complete ===${colors3.reset}
@@ -2399,74 +3686,53 @@ ${colors3.green}=== Sync complete ===${colors3.reset}
2399
3686
  async function purgeUnlisted(config, interactive, cb = null) {
2400
3687
  const log = cb?.onLog ?? console.log;
2401
3688
  const askPrompt = cb?.onPrompt ?? (async (q) => (prompt(q) || "").trim().toLowerCase());
3689
+ const platform = await getPlatformInfo();
3690
+ const callbacks = cb ? { onLog: cb.onLog } : undefined;
2402
3691
  log(`
2403
3692
  ${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
2404
3693
  `);
2405
- const installedFormulas = (await exec(["brew", "list", "--formula"])).stdout.split(`
2406
- `).filter(Boolean);
2407
- for (const pkg of installedFormulas) {
2408
- if (!config.packages.includes(pkg)) {
2409
- const usesResult = await exec(["brew", "uses", "--installed", pkg]);
2410
- if (usesResult.stdout.trim()) {
2411
- log(` ${colors3.yellow}Skipping ${pkg} (has dependents)${colors3.reset}`);
3694
+ const packageSets = await getPackageSetsForPlatform(config, platform);
3695
+ for (const set of packageSets) {
3696
+ const configuredSet = new Set(set.packages);
3697
+ let installedLeaves;
3698
+ if (set.manager.listLeaves) {
3699
+ installedLeaves = await set.manager.listLeaves();
3700
+ } else {
3701
+ const installed = await set.manager.listInstalled();
3702
+ installedLeaves = installed.map((p) => p.name);
3703
+ }
3704
+ for (const pkg of installedLeaves) {
3705
+ if (configuredSet.has(pkg))
2412
3706
  continue;
3707
+ if (set.manager.type === "mas") {
3708
+ const appId = parseInt(pkg, 10);
3709
+ if (SYSTEM_APP_IDS.includes(appId))
3710
+ continue;
2413
3711
  }
2414
- if (interactive) {
2415
- const answer = await askPrompt(`Remove ${colors3.red}${pkg}${colors3.reset}?`, ["y", "n"]);
2416
- if (answer === "y") {
2417
- await runCommand(["brew", "uninstall", pkg], cb);
3712
+ if (set.manager.type === "homebrew") {
3713
+ const formulas = set.manager;
3714
+ if (await formulas.hasDependents(pkg)) {
3715
+ log(` ${colors3.yellow}Skipping ${pkg} (has dependents)${colors3.reset}`);
3716
+ continue;
2418
3717
  }
2419
- } else {
2420
- log(` Removing: ${colors3.red}${pkg}${colors3.reset}`);
2421
- await runCommand(["brew", "uninstall", pkg], cb);
2422
3718
  }
2423
- }
2424
- }
2425
- const installedCasks = (await exec(["brew", "list", "--cask"])).stdout.split(`
2426
- `).filter(Boolean);
2427
- for (const cask of installedCasks) {
2428
- if (!config.casks.includes(cask)) {
2429
3719
  if (interactive) {
2430
- const answer = await askPrompt(`Remove cask ${colors3.red}${cask}${colors3.reset}?`, ["y", "n"]);
3720
+ const answer = await askPrompt(`Remove ${colors3.red}${pkg}${colors3.reset} (${set.manager.displayName})?`, ["y", "n"]);
2431
3721
  if (answer === "y") {
2432
- await runCommand(["brew", "uninstall", "--cask", cask], cb);
3722
+ await set.manager.uninstall([pkg], callbacks);
2433
3723
  }
2434
3724
  } else {
2435
- log(` Removing cask: ${colors3.red}${cask}${colors3.reset}`);
2436
- await runCommand(["brew", "uninstall", "--cask", cask], cb);
2437
- }
2438
- }
2439
- }
2440
- if (await commandExists("mas")) {
2441
- const masResult = await exec(["mas", "list"]);
2442
- const installedMas = masResult.stdout.split(`
2443
- `).filter(Boolean).map((line) => {
2444
- const match = line.match(/^(\d+)\s+(.+?)(?:\s+\(|$)/);
2445
- return match ? { id: parseInt(match[1], 10), name: match[2].trim() } : null;
2446
- }).filter((app) => app !== null);
2447
- const configMasIds = Object.values(config.mas);
2448
- for (const app of installedMas) {
2449
- if (SYSTEM_APP_IDS.includes(app.id)) {
2450
- continue;
2451
- }
2452
- if (!configMasIds.includes(app.id)) {
2453
- if (interactive) {
2454
- const answer = await askPrompt(`Remove app ${colors3.red}${app.name}${colors3.reset}?`, ["y", "n"]);
2455
- if (answer === "y") {
2456
- await runCommand(["mas", "uninstall", String(app.id)], cb, undefined, true);
2457
- }
2458
- } else {
2459
- log(` Removing app: ${colors3.red}${app.name}${colors3.reset}`);
2460
- await runCommand(["mas", "uninstall", String(app.id)], cb, undefined, true);
2461
- }
3725
+ log(` Removing: ${colors3.red}${pkg}${colors3.reset}`);
3726
+ await set.manager.uninstall([pkg], callbacks);
2462
3727
  }
2463
3728
  }
2464
3729
  }
2465
3730
  log(`
2466
3731
  ${colors3.cyan}=== Cleaning up ===${colors3.reset}
2467
3732
  `);
2468
- await runCommand(["brew", "autoremove"], cb);
2469
- await runCommand(["brew", "cleanup"], cb);
3733
+ for (const set of packageSets) {
3734
+ await set.manager.cleanup(callbacks);
3735
+ }
2470
3736
  }
2471
3737
  function printUsage2() {
2472
3738
  console.log(`
@@ -2494,13 +3760,12 @@ async function runPkgSyncWithCallbacks(args, callbacks) {
2494
3760
  },
2495
3761
  allowPositionals: true
2496
3762
  });
3763
+ const platform = await getPlatformInfo();
2497
3764
  try {
2498
- if (!await commandExists("brew")) {
2499
- callbacks.onLog(`${colors3.red}Error: Homebrew not installed${colors3.reset}`);
2500
- return { output: "Homebrew not installed", success: false };
2501
- }
3765
+ await checkDependencies(platform);
2502
3766
  } catch {
2503
- return { output: "Homebrew not installed", success: false };
3767
+ callbacks.onLog(`${colors3.red}Error: Required dependencies not installed${colors3.reset}`);
3768
+ return { output: "Dependencies not installed", success: false };
2504
3769
  }
2505
3770
  if (values["upgrade-interactive"]) {
2506
3771
  await upgradeInteractive(callbacks);
@@ -2542,7 +3807,8 @@ async function main2() {
2542
3807
  printUsage2();
2543
3808
  process.exit(0);
2544
3809
  }
2545
- await checkDependencies();
3810
+ const platform = await getPlatformInfo();
3811
+ await checkDependencies(platform);
2546
3812
  if (values["upgrade-interactive"]) {
2547
3813
  await upgradeInteractive();
2548
3814
  return;
@@ -2645,21 +3911,30 @@ async function showLockfile() {
2645
3911
  console.log(`${colors4.bold}Package Lockfile${colors4.reset}`);
2646
3912
  console.log(`Last updated: ${lock.lastUpdated}
2647
3913
  `);
2648
- const formulaNames = Object.keys(lock.formulas).sort();
2649
- const caskNames = Object.keys(lock.casks).sort();
2650
- if (formulaNames.length > 0) {
2651
- console.log(`${colors4.cyan}Formulas (${formulaNames.length}):${colors4.reset}`);
2652
- for (const name of formulaNames) {
2653
- const { version, tap } = lock.formulas[name];
2654
- console.log(` ${name} ${colors4.blue}${version}${colors4.reset} (${tap})`);
3914
+ if (lock.version === 2) {
3915
+ const packages = Object.entries(lock.packages).sort(([a], [b]) => a.localeCompare(b));
3916
+ console.log(`${colors4.cyan}Packages (${packages.length}):${colors4.reset}`);
3917
+ for (const [key, info] of packages) {
3918
+ const [manager, name] = key.split(":");
3919
+ console.log(` ${name} ${colors4.blue}${info.version}${colors4.reset} (${manager}${info.tap ? `, ${info.tap}` : ""})`);
2655
3920
  }
2656
- }
2657
- if (caskNames.length > 0) {
2658
- console.log(`
3921
+ } else {
3922
+ const formulaNames = Object.keys(lock.formulas).sort();
3923
+ const caskNames = Object.keys(lock.casks).sort();
3924
+ if (formulaNames.length > 0) {
3925
+ console.log(`${colors4.cyan}Formulas (${formulaNames.length}):${colors4.reset}`);
3926
+ for (const name of formulaNames) {
3927
+ const { version, tap } = lock.formulas[name];
3928
+ console.log(` ${name} ${colors4.blue}${version}${colors4.reset} (${tap})`);
3929
+ }
3930
+ }
3931
+ if (caskNames.length > 0) {
3932
+ console.log(`
2659
3933
  ${colors4.cyan}Casks (${caskNames.length}):${colors4.reset}`);
2660
- for (const name of caskNames) {
2661
- const { version } = lock.casks[name];
2662
- console.log(` ${name} ${colors4.blue}${version}${colors4.reset}`);
3934
+ for (const name of caskNames) {
3935
+ const { version } = lock.casks[name];
3936
+ console.log(` ${name} ${colors4.blue}${version}${colors4.reset}`);
3937
+ }
2663
3938
  }
2664
3939
  }
2665
3940
  }
@@ -2675,7 +3950,7 @@ async function runPkgLock(args) {
2675
3950
  switch (command) {
2676
3951
  case "update": {
2677
3952
  const lock = await updateLockfile();
2678
- const total = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
3953
+ const total = lock.version === 2 ? Object.keys(lock.packages).length : Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
2679
3954
  return { output: `Lockfile updated with ${total} packages`, success: true };
2680
3955
  }
2681
3956
  case "status": {
@@ -2720,7 +3995,7 @@ async function main3() {
2720
3995
  case "update": {
2721
3996
  console.log(`${colors4.cyan}Updating lockfile...${colors4.reset}`);
2722
3997
  const lock = await updateLockfile();
2723
- const total = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
3998
+ const total = lock.version === 2 ? Object.keys(lock.packages).length : Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
2724
3999
  console.log(`${colors4.green}Lockfile updated with ${total} packages.${colors4.reset}`);
2725
4000
  break;
2726
4001
  }
@@ -2754,64 +4029,248 @@ function getPackageName(fullName) {
2754
4029
  const parts = fullName.split("/");
2755
4030
  return parts[parts.length - 1];
2756
4031
  }
4032
+ function getOrphanPackageType(managerType) {
4033
+ switch (managerType) {
4034
+ case "homebrew":
4035
+ return "formula";
4036
+ case "homebrew-casks":
4037
+ return "cask";
4038
+ case "pacman":
4039
+ return "pacman";
4040
+ case "aur":
4041
+ return "aur";
4042
+ case "apt":
4043
+ return "apt";
4044
+ case "dnf":
4045
+ return "dnf";
4046
+ case "flatpak":
4047
+ return "flatpak";
4048
+ default:
4049
+ return "formula";
4050
+ }
4051
+ }
4052
+ async function getConfiguredPackagesForPlatform(config, platform) {
4053
+ const result = [];
4054
+ if (platform.os === "darwin") {
4055
+ const formulas = getPackageManager("homebrew");
4056
+ const casks = getPackageManager("homebrew-casks");
4057
+ const globalPkgs = config.global?.packages || [];
4058
+ const macosFormulas = config.macos?.formulas || [];
4059
+ const allFormulas = [...globalPkgs, ...macosFormulas];
4060
+ if (await formulas.isAvailable()) {
4061
+ result.push({
4062
+ manager: formulas,
4063
+ configuredPackages: new Set(allFormulas.map((p) => getPackageName(p)))
4064
+ });
4065
+ }
4066
+ const macosCasks = config.macos?.casks || [];
4067
+ if (await casks.isAvailable()) {
4068
+ result.push({
4069
+ manager: casks,
4070
+ configuredPackages: new Set(macosCasks)
4071
+ });
4072
+ }
4073
+ const mas = getPackageManager("mas");
4074
+ const masMappings = config.macos?.mas || {};
4075
+ const masIds = Object.values(masMappings).map(String);
4076
+ if (await mas.isAvailable()) {
4077
+ result.push({
4078
+ manager: mas,
4079
+ configuredPackages: new Set(masIds)
4080
+ });
4081
+ }
4082
+ } else {
4083
+ const distro = platform.distro;
4084
+ const globalPkgs = config.global?.packages || [];
4085
+ const linuxPkgs = config.linux?.packages || [];
4086
+ if (distro === "arch") {
4087
+ const pacman = getPackageManager("pacman");
4088
+ const aur = getPackageManager("aur");
4089
+ const archPkgs = config.arch?.packages || [];
4090
+ const allPacmanPkgs = [...globalPkgs, ...linuxPkgs, ...archPkgs];
4091
+ if (await pacman.isAvailable()) {
4092
+ result.push({
4093
+ manager: pacman,
4094
+ configuredPackages: new Set(allPacmanPkgs)
4095
+ });
4096
+ }
4097
+ const aurPkgs = config.arch?.aur || [];
4098
+ if (await aur.isAvailable()) {
4099
+ result.push({
4100
+ manager: aur,
4101
+ configuredPackages: new Set(aurPkgs)
4102
+ });
4103
+ }
4104
+ } else if (distro === "debian" || distro === "ubuntu") {
4105
+ const apt = getPackageManager("apt");
4106
+ const debianPkgs = config.debian?.packages || [];
4107
+ const allAptPkgs = [...globalPkgs, ...linuxPkgs, ...debianPkgs];
4108
+ if (await apt.isAvailable()) {
4109
+ result.push({
4110
+ manager: apt,
4111
+ configuredPackages: new Set(allAptPkgs)
4112
+ });
4113
+ }
4114
+ } else if (distro === "fedora" || distro === "rhel") {
4115
+ const dnf = getPackageManager("dnf");
4116
+ const fedoraPkgs = config.fedora?.packages || [];
4117
+ const allDnfPkgs = [...globalPkgs, ...linuxPkgs, ...fedoraPkgs];
4118
+ if (await dnf.isAvailable()) {
4119
+ result.push({
4120
+ manager: dnf,
4121
+ configuredPackages: new Set(allDnfPkgs)
4122
+ });
4123
+ }
4124
+ } else {
4125
+ const managers = await getAvailableManagers();
4126
+ const primaryManager = managers.find((m) => m.type === "pacman" || m.type === "apt" || m.type === "dnf");
4127
+ if (primaryManager) {
4128
+ const allPkgs = [...globalPkgs, ...linuxPkgs];
4129
+ result.push({
4130
+ manager: primaryManager,
4131
+ configuredPackages: new Set(allPkgs)
4132
+ });
4133
+ }
4134
+ }
4135
+ const flatpak = getPackageManager("flatpak");
4136
+ const flatpakApps = config.linux?.flatpak || [];
4137
+ if (await flatpak.isAvailable()) {
4138
+ result.push({
4139
+ manager: flatpak,
4140
+ configuredPackages: new Set(flatpakApps)
4141
+ });
4142
+ }
4143
+ }
4144
+ return result;
4145
+ }
2757
4146
  async function detectOrphanedPackages() {
2758
4147
  const config = await loadPkgConfig();
2759
- const leavesResult = await exec(["brew", "leaves"]);
2760
- const installedLeaves = leavesResult.success ? leavesResult.stdout.split(`
2761
- `).filter(Boolean) : [];
2762
- const casksResult = await exec(["brew", "list", "--cask"]);
2763
- const installedCasks = casksResult.success ? casksResult.stdout.split(`
2764
- `).filter(Boolean) : [];
4148
+ const platform = await getPlatformInfo();
2765
4149
  const orphans = [];
2766
- const configPackages = config.packages.map((pkg) => ({
2767
- full: pkg,
2768
- short: getPackageName(pkg)
2769
- }));
2770
- for (const installed of installedLeaves) {
2771
- const installedShort = getPackageName(installed);
2772
- const isInConfig = configPackages.some((cfg) => installed === cfg.full || installed === cfg.short || installedShort === cfg.full || installedShort === cfg.short || installed.endsWith(`/${cfg.short}`));
2773
- if (!isInConfig) {
2774
- orphans.push({ name: installed, type: "formula" });
4150
+ let totalConfigPackages = 0;
4151
+ let totalInstalledPackages = 0;
4152
+ const configuredPackagesPerManager = await getConfiguredPackagesForPlatform(config, platform);
4153
+ for (const { manager, configuredPackages } of configuredPackagesPerManager) {
4154
+ totalConfigPackages += configuredPackages.size;
4155
+ let installedLeaves2;
4156
+ if (manager.listLeaves) {
4157
+ installedLeaves2 = await manager.listLeaves();
4158
+ } else {
4159
+ const installed = await manager.listInstalled();
4160
+ installedLeaves2 = installed.map((p) => p.name);
2775
4161
  }
2776
- }
2777
- const configCasksSet = new Set(config.casks);
2778
- for (const cask of installedCasks) {
2779
- if (!configCasksSet.has(cask)) {
2780
- orphans.push({ name: cask, type: "cask" });
4162
+ totalInstalledPackages += installedLeaves2.length;
4163
+ for (const installed of installedLeaves2) {
4164
+ const installedShort = getPackageName(installed);
4165
+ const isInConfig = configuredPackages.has(installed) || configuredPackages.has(installedShort) || Array.from(configuredPackages).some((cfg) => installed === cfg || installedShort === cfg || getPackageName(cfg) === installedShort || installed.endsWith(`/${getPackageName(cfg)}`));
4166
+ if (!isInConfig) {
4167
+ if (manager.type === "mas") {
4168
+ const appId = parseInt(installed, 10);
4169
+ if (SYSTEM_APP_IDS.includes(appId))
4170
+ continue;
4171
+ }
4172
+ if (manager.type === "homebrew") {
4173
+ const formulas = manager;
4174
+ if (await formulas.hasDependents(installed))
4175
+ continue;
4176
+ }
4177
+ orphans.push({
4178
+ name: installed,
4179
+ type: getOrphanPackageType(manager.type),
4180
+ manager: manager.type
4181
+ });
4182
+ }
2781
4183
  }
2782
4184
  }
2783
4185
  orphans.sort((a, b) => {
2784
4186
  if (a.type !== b.type)
2785
- return a.type === "formula" ? -1 : 1;
4187
+ return a.type.localeCompare(b.type);
2786
4188
  return a.name.localeCompare(b.name);
2787
4189
  });
4190
+ let configFormulas = 0;
4191
+ let configCasks = 0;
4192
+ let installedLeaves = 0;
4193
+ let installedCasks = 0;
4194
+ if (platform.os === "darwin") {
4195
+ configFormulas = (config.global?.packages?.length || 0) + (config.macos?.formulas?.length || 0);
4196
+ configCasks = config.macos?.casks?.length || 0;
4197
+ const formulas = getPackageManager("homebrew");
4198
+ const casks = getPackageManager("homebrew-casks");
4199
+ if (await formulas.isAvailable()) {
4200
+ const leaves = await formulas.listLeaves?.();
4201
+ installedLeaves = leaves?.length || 0;
4202
+ }
4203
+ if (await casks.isAvailable()) {
4204
+ const caskList = await casks.listInstalled();
4205
+ installedCasks = caskList.length;
4206
+ }
4207
+ }
2788
4208
  return {
2789
4209
  orphans,
2790
- configFormulas: config.packages.length,
2791
- configCasks: config.casks.length,
2792
- installedLeaves: installedLeaves.length,
2793
- installedCasks: installedCasks.length
4210
+ configPackages: totalConfigPackages,
4211
+ installedPackages: totalInstalledPackages,
4212
+ configFormulas,
4213
+ configCasks,
4214
+ installedLeaves,
4215
+ installedCasks
2794
4216
  };
2795
4217
  }
2796
4218
  async function addToConfig(pkg) {
2797
4219
  const config = await loadPkgConfig();
2798
- if (pkg.type === "formula") {
2799
- if (!config.packages.includes(pkg.name)) {
2800
- config.packages.push(pkg.name);
2801
- config.packages.sort();
4220
+ const platform = await getPlatformInfo();
4221
+ if (platform.os === "darwin") {
4222
+ if (pkg.type === "formula") {
4223
+ const formulas = config.macos?.formulas || [];
4224
+ if (!formulas.includes(pkg.name)) {
4225
+ config.macos = config.macos || {};
4226
+ config.macos.formulas = [...formulas, pkg.name].sort();
4227
+ }
4228
+ } else if (pkg.type === "cask") {
4229
+ const casks = config.macos?.casks || [];
4230
+ if (!casks.includes(pkg.name)) {
4231
+ config.macos = config.macos || {};
4232
+ config.macos.casks = [...casks, pkg.name].sort();
4233
+ }
2802
4234
  }
2803
4235
  } else {
2804
- if (!config.casks.includes(pkg.name)) {
2805
- config.casks.push(pkg.name);
2806
- config.casks.sort();
4236
+ if (pkg.type === "pacman") {
4237
+ const packages = config.arch?.packages || [];
4238
+ if (!packages.includes(pkg.name)) {
4239
+ config.arch = config.arch || {};
4240
+ config.arch.packages = [...packages, pkg.name].sort();
4241
+ }
4242
+ } else if (pkg.type === "aur") {
4243
+ const aurPkgs = config.arch?.aur || [];
4244
+ if (!aurPkgs.includes(pkg.name)) {
4245
+ config.arch = config.arch || {};
4246
+ config.arch.aur = [...aurPkgs, pkg.name].sort();
4247
+ }
4248
+ } else if (pkg.type === "apt") {
4249
+ const packages = config.debian?.packages || [];
4250
+ if (!packages.includes(pkg.name)) {
4251
+ config.debian = config.debian || {};
4252
+ config.debian.packages = [...packages, pkg.name].sort();
4253
+ }
4254
+ } else if (pkg.type === "dnf") {
4255
+ const packages = config.fedora?.packages || [];
4256
+ if (!packages.includes(pkg.name)) {
4257
+ config.fedora = config.fedora || {};
4258
+ config.fedora.packages = [...packages, pkg.name].sort();
4259
+ }
4260
+ } else if (pkg.type === "flatpak") {
4261
+ const flatpakApps = config.linux?.flatpak || [];
4262
+ if (!flatpakApps.includes(pkg.name)) {
4263
+ config.linux = config.linux || {};
4264
+ config.linux.flatpak = [...flatpakApps, pkg.name].sort();
4265
+ }
2807
4266
  }
2808
4267
  }
2809
4268
  await savePkgConfig(config);
2810
4269
  }
2811
4270
  async function uninstallPackage(pkg) {
2812
- const cmd = pkg.type === "cask" ? ["brew", "uninstall", "--cask", pkg.name] : ["brew", "uninstall", pkg.name];
2813
- const result = await exec(cmd);
2814
- return result.success;
4271
+ const manager = getPackageManager(pkg.manager);
4272
+ await manager.uninstall([pkg.name]);
4273
+ return true;
2815
4274
  }
2816
4275
 
2817
4276
  // src/components/menus/PackageMenu.tsx
@@ -2825,7 +4284,18 @@ function PackageMenu({ onBack }) {
2825
4284
  const [success, setSuccess] = useState8(true);
2826
4285
  const [orphanResult, setOrphanResult] = useState8(null);
2827
4286
  const [isOrphanView, setIsOrphanView] = useState8(false);
4287
+ const [platformInfo, setPlatformInfo] = useState8(null);
4288
+ const [availableManagerNames, setAvailableManagerNames] = useState8([]);
2828
4289
  const isRunningRef = useRef(false);
4290
+ useEffect4(() => {
4291
+ async function loadPlatformInfo() {
4292
+ const info = await getPlatformInfo();
4293
+ setPlatformInfo(info);
4294
+ const managers = await getAvailableManagers();
4295
+ setAvailableManagerNames(managers.map((m) => m.displayName));
4296
+ }
4297
+ loadPlatformInfo();
4298
+ }, []);
2829
4299
  useInput9((input, key) => {
2830
4300
  if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
2831
4301
  onBack();
@@ -2978,37 +4448,73 @@ function PackageMenu({ onBack }) {
2978
4448
  ]
2979
4449
  }, undefined, true, undefined, this);
2980
4450
  }
4451
+ const platformDisplay = platformInfo ? getPlatformDisplayName(platformInfo) : "Detecting...";
4452
+ const managersDisplay = availableManagerNames.length > 0 ? availableManagerNames.join(", ") : "Detecting...";
2981
4453
  return /* @__PURE__ */ jsxDEV17(Panel, {
2982
4454
  title: "Package Sync",
2983
- children: /* @__PURE__ */ jsxDEV17(VimSelect, {
2984
- options: [
2985
- { label: "Sync packages", value: "sync" },
2986
- { label: "Sync with purge", value: "sync-purge" },
2987
- { label: "Upgrade all (with verification)", value: "upgrade" },
2988
- { label: "Upgrade interactive", value: "upgrade-interactive" },
2989
- { label: "Update lockfile", value: "lock-update" },
2990
- { label: "Lockfile status", value: "lock-status" },
2991
- { label: "Find orphaned packages", value: "orphans" },
2992
- { label: "Back", value: "back" }
2993
- ],
2994
- onChange: handleAction
2995
- }, undefined, false, undefined, this)
2996
- }, undefined, false, undefined, this);
4455
+ children: [
4456
+ /* @__PURE__ */ jsxDEV17(Box14, {
4457
+ marginBottom: 1,
4458
+ flexDirection: "column",
4459
+ children: [
4460
+ /* @__PURE__ */ jsxDEV17(Text13, {
4461
+ children: [
4462
+ /* @__PURE__ */ jsxDEV17(Text13, {
4463
+ dimColor: true,
4464
+ children: "Platform: "
4465
+ }, undefined, false, undefined, this),
4466
+ /* @__PURE__ */ jsxDEV17(Text13, {
4467
+ color: colors.info,
4468
+ children: platformDisplay
4469
+ }, undefined, false, undefined, this)
4470
+ ]
4471
+ }, undefined, true, undefined, this),
4472
+ /* @__PURE__ */ jsxDEV17(Text13, {
4473
+ children: [
4474
+ /* @__PURE__ */ jsxDEV17(Text13, {
4475
+ dimColor: true,
4476
+ children: "Managers: "
4477
+ }, undefined, false, undefined, this),
4478
+ /* @__PURE__ */ jsxDEV17(Text13, {
4479
+ color: colors.info,
4480
+ children: managersDisplay
4481
+ }, undefined, false, undefined, this)
4482
+ ]
4483
+ }, undefined, true, undefined, this)
4484
+ ]
4485
+ }, undefined, true, undefined, this),
4486
+ /* @__PURE__ */ jsxDEV17(VimSelect, {
4487
+ options: [
4488
+ { label: "Sync packages", value: "sync" },
4489
+ { label: "Sync with purge", value: "sync-purge" },
4490
+ { label: "Upgrade all (with verification)", value: "upgrade" },
4491
+ { label: "Upgrade interactive", value: "upgrade-interactive" },
4492
+ { label: "Update lockfile", value: "lock-update" },
4493
+ { label: "Lockfile status", value: "lock-status" },
4494
+ { label: "Find orphaned packages", value: "orphans" },
4495
+ { label: "Back", value: "back" }
4496
+ ],
4497
+ onChange: handleAction
4498
+ }, undefined, false, undefined, this)
4499
+ ]
4500
+ }, undefined, true, undefined, this);
2997
4501
  }
2998
4502
 
2999
4503
  // src/components/menus/ThemeMenu.tsx
3000
- import { useState as useState10, useEffect as useEffect5, useMemo as useMemo4 } from "react";
4504
+ import { useState as useState10, useEffect as useEffect6, useMemo as useMemo4 } from "react";
3001
4505
  import { Box as Box16, Text as Text15 } from "ink";
3002
- import { existsSync as existsSync7, readdirSync as readdirSync5 } from "fs";
4506
+ import { existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
3003
4507
  import { join as join6 } from "path";
3004
4508
 
3005
4509
  // src/components/ThemeCard.tsx
3006
4510
  import { Box as Box15, Text as Text14 } from "ink";
3007
4511
  import { jsxDEV as jsxDEV18 } from "react/jsx-dev-runtime";
3008
- function ThemeCard({ theme, isSelected, width }) {
4512
+ function ThemeCard({ theme, isSelected, width, isDeviceTheme }) {
3009
4513
  const borderColor = isSelected ? colors.accent : colors.border;
3010
4514
  const nameColor = isSelected ? colors.primary : colors.text;
3011
4515
  const indicators = [];
4516
+ if (isDeviceTheme)
4517
+ indicators.push("device");
3012
4518
  if (theme.hasBackgrounds)
3013
4519
  indicators.push("bg");
3014
4520
  if (theme.isLightMode)
@@ -3042,7 +4548,7 @@ function ThemeCard({ theme, isSelected, width }) {
3042
4548
  }
3043
4549
 
3044
4550
  // src/hooks/useThemeGrid.ts
3045
- import { useState as useState9, useEffect as useEffect4 } from "react";
4551
+ import { useState as useState9, useEffect as useEffect5 } from "react";
3046
4552
  import { useInput as useInput10 } from "ink";
3047
4553
  function useThemeGrid({
3048
4554
  itemCount,
@@ -3050,6 +4556,7 @@ function useThemeGrid({
3050
4556
  layoutOverhead = 20,
3051
4557
  minCardWidth = 28,
3052
4558
  onSelect,
4559
+ onSelectAndSave,
3053
4560
  onBack,
3054
4561
  enabled = true
3055
4562
  }) {
@@ -3063,7 +4570,7 @@ function useThemeGrid({
3063
4570
  const visibleRows = Math.max(1, Math.floor(availableHeight / cardHeight));
3064
4571
  const selectedRow = Math.floor(selectedIndex / cardsPerRow);
3065
4572
  const totalRows = Math.ceil(itemCount / cardsPerRow);
3066
- useEffect4(() => {
4573
+ useEffect5(() => {
3067
4574
  if (selectedRow < scrollOffset) {
3068
4575
  setScrollOffset(selectedRow);
3069
4576
  } else if (selectedRow >= scrollOffset + visibleRows) {
@@ -3099,8 +4606,12 @@ function useThemeGrid({
3099
4606
  setSelectedIndex(prevIndex);
3100
4607
  }
3101
4608
  }
3102
- if (key.return && onSelect) {
3103
- onSelect(selectedIndex);
4609
+ if (key.return) {
4610
+ if (key.shift && onSelectAndSave) {
4611
+ onSelectAndSave(selectedIndex);
4612
+ } else if (onSelect) {
4613
+ onSelect(selectedIndex);
4614
+ }
3104
4615
  }
3105
4616
  });
3106
4617
  const visibleStartIndex = scrollOffset * cardsPerRow;
@@ -3121,6 +4632,7 @@ function useThemeGrid({
3121
4632
  }
3122
4633
 
3123
4634
  // src/lib/theme-parser.ts
4635
+ init_runtime();
3124
4636
  import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
3125
4637
  import { join as join4 } from "path";
3126
4638
  function parseYaml(content) {
@@ -3196,9 +4708,86 @@ async function parseTheme(themePath, themeName) {
3196
4708
 
3197
4709
  // src/cli/set-theme.ts
3198
4710
  import { parseArgs as parseArgs4 } from "util";
3199
- import { readdirSync as readdirSync4, existsSync as existsSync6, rmSync, symlinkSync, unlinkSync } from "fs";
3200
- import { join as join5, dirname as dirname3 } from "path";
3201
- var LYNK_BROWSER_CSS = join5(HOME_DIR, ".config", "lynk-browser", "style.css");
4711
+ import { readdirSync as readdirSync4, existsSync as existsSync7, rmSync, symlinkSync, unlinkSync } from "fs";
4712
+ import { join as join5 } from "path";
4713
+
4714
+ // src/lib/theme-config.ts
4715
+ import { hostname } from "os";
4716
+ import { existsSync as existsSync6, readFileSync, writeFileSync } from "fs";
4717
+ var DEFAULT_CONFIG = {
4718
+ version: 1,
4719
+ defaultTheme: null,
4720
+ devices: {}
4721
+ };
4722
+ function getDeviceHostname() {
4723
+ return hostname();
4724
+ }
4725
+ function loadThemeConfig() {
4726
+ if (!existsSync6(THEME_CONFIG_PATH)) {
4727
+ return { ...DEFAULT_CONFIG, devices: {} };
4728
+ }
4729
+ try {
4730
+ const content = readFileSync(THEME_CONFIG_PATH, "utf-8");
4731
+ const parsed = JSON.parse(content);
4732
+ return {
4733
+ version: parsed.version ?? 1,
4734
+ defaultTheme: parsed.defaultTheme ?? null,
4735
+ devices: parsed.devices ?? {}
4736
+ };
4737
+ } catch {
4738
+ return { ...DEFAULT_CONFIG, devices: {} };
4739
+ }
4740
+ }
4741
+ async function saveThemeConfig(config) {
4742
+ await ensureConfigDir();
4743
+ writeFileSync(THEME_CONFIG_PATH, JSON.stringify(config, null, 2) + `
4744
+ `);
4745
+ }
4746
+ function getDeviceTheme() {
4747
+ const config = loadThemeConfig();
4748
+ const device = getDeviceHostname();
4749
+ const mapping = config.devices[device];
4750
+ if (mapping) {
4751
+ return mapping.theme;
4752
+ }
4753
+ return config.defaultTheme;
4754
+ }
4755
+ async function setDeviceTheme(themeName) {
4756
+ const config = loadThemeConfig();
4757
+ const device = getDeviceHostname();
4758
+ config.devices[device] = {
4759
+ theme: themeName,
4760
+ setAt: new Date().toISOString()
4761
+ };
4762
+ await saveThemeConfig(config);
4763
+ }
4764
+ async function setDefaultTheme(themeName) {
4765
+ const config = loadThemeConfig();
4766
+ config.defaultTheme = themeName;
4767
+ await saveThemeConfig(config);
4768
+ }
4769
+ async function clearDeviceTheme() {
4770
+ const config = loadThemeConfig();
4771
+ const device = getDeviceHostname();
4772
+ delete config.devices[device];
4773
+ await saveThemeConfig(config);
4774
+ }
4775
+ function listDeviceMappings() {
4776
+ const config = loadThemeConfig();
4777
+ const currentDevice = getDeviceHostname();
4778
+ return Object.entries(config.devices).map(([device, mapping]) => ({
4779
+ device,
4780
+ theme: mapping.theme,
4781
+ setAt: mapping.setAt,
4782
+ isCurrent: device === currentDevice
4783
+ }));
4784
+ }
4785
+ function getDefaultTheme() {
4786
+ const config = loadThemeConfig();
4787
+ return config.defaultTheme;
4788
+ }
4789
+
4790
+ // src/cli/set-theme.ts
3202
4791
  var colors5 = {
3203
4792
  red: "\x1B[0;31m",
3204
4793
  green: "\x1B[0;32m",
@@ -3210,7 +4799,7 @@ var colors5 = {
3210
4799
  };
3211
4800
  async function listThemes() {
3212
4801
  await ensureConfigDir();
3213
- if (!existsSync6(THEMES_DIR)) {
4802
+ if (!existsSync7(THEMES_DIR)) {
3214
4803
  return [];
3215
4804
  }
3216
4805
  const entries = readdirSync4(THEMES_DIR, { withFileTypes: true });
@@ -3225,7 +4814,7 @@ async function listThemes() {
3225
4814
  return themes;
3226
4815
  }
3227
4816
  function clearDirectory(dir) {
3228
- if (existsSync6(dir)) {
4817
+ if (existsSync7(dir)) {
3229
4818
  const entries = readdirSync4(dir, { withFileTypes: true });
3230
4819
  for (const entry of entries) {
3231
4820
  const fullPath = join5(dir, entry.name);
@@ -3238,21 +4827,21 @@ function clearDirectory(dir) {
3238
4827
  }
3239
4828
  }
3240
4829
  function createSymlink(source, target) {
3241
- if (existsSync6(target)) {
4830
+ if (existsSync7(target)) {
3242
4831
  unlinkSync(target);
3243
4832
  }
3244
4833
  symlinkSync(source, target);
3245
4834
  }
3246
- async function applyTheme(themeName) {
4835
+ async function applyTheme(themeName, saveMapping = false) {
3247
4836
  const themeDir = join5(THEMES_DIR, themeName);
3248
- if (!existsSync6(themeDir)) {
4837
+ if (!existsSync7(themeDir)) {
3249
4838
  return { output: `Theme '${themeName}' not found`, success: false };
3250
4839
  }
3251
4840
  await ensureConfigDir();
3252
4841
  await ensureDir2(THEME_TARGET_DIR);
3253
4842
  const theme = await parseTheme(themeDir, themeName);
3254
4843
  clearDirectory(THEME_TARGET_DIR);
3255
- if (existsSync6(BACKGROUNDS_TARGET_DIR)) {
4844
+ if (existsSync7(BACKGROUNDS_TARGET_DIR)) {
3256
4845
  rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
3257
4846
  }
3258
4847
  const entries = readdirSync4(themeDir, { withFileTypes: true });
@@ -3267,13 +4856,13 @@ async function applyTheme(themeName) {
3267
4856
  const backgroundsSource = join5(themeDir, "backgrounds");
3268
4857
  createSymlink(backgroundsSource, BACKGROUNDS_TARGET_DIR);
3269
4858
  }
3270
- const styleCssSource = join5(themeDir, "style.css");
3271
- if (existsSync6(styleCssSource)) {
3272
- const lynkBrowserDir = dirname3(LYNK_BROWSER_CSS);
3273
- await ensureDir2(lynkBrowserDir);
3274
- createSymlink(styleCssSource, LYNK_BROWSER_CSS);
4859
+ if (saveMapping) {
4860
+ await setDeviceTheme(themeName);
3275
4861
  }
3276
4862
  let output = `Theme '${theme.name}' applied successfully`;
4863
+ if (saveMapping) {
4864
+ output += ` (saved as device preference for '${getDeviceHostname()}')`;
4865
+ }
3277
4866
  if (theme.metadata?.author) {
3278
4867
  output += `
3279
4868
  Author: ${theme.metadata.author}`;
@@ -3290,7 +4879,7 @@ Note: This is a light mode theme`;
3290
4879
  }
3291
4880
  async function showThemeInfo(themeName) {
3292
4881
  const themeDir = join5(THEMES_DIR, themeName);
3293
- if (!existsSync6(themeDir)) {
4882
+ if (!existsSync7(themeDir)) {
3294
4883
  console.error(`${colors5.red}Error: Theme '${themeName}' not found${colors5.reset}`);
3295
4884
  process.exit(1);
3296
4885
  }
@@ -3323,48 +4912,134 @@ ${colors5.green}Has wallpapers${colors5.reset}`);
3323
4912
  console.log(`${colors5.yellow}Light mode theme${colors5.reset}`);
3324
4913
  }
3325
4914
  }
3326
- async function runSetTheme(themeName) {
3327
- return applyTheme(themeName);
4915
+ async function runSetTheme(themeName, saveMapping = false) {
4916
+ return applyTheme(themeName, saveMapping);
4917
+ }
4918
+ function showDeviceMappings() {
4919
+ const mappings = listDeviceMappings();
4920
+ const defaultTheme = getDefaultTheme();
4921
+ const currentDevice = getDeviceHostname();
4922
+ console.log(`${colors5.cyan}Device Theme Mappings${colors5.reset}`);
4923
+ console.log(`Current device: ${colors5.blue}${currentDevice}${colors5.reset}
4924
+ `);
4925
+ if (defaultTheme) {
4926
+ console.log(`Default theme: ${colors5.green}${defaultTheme}${colors5.reset}
4927
+ `);
4928
+ }
4929
+ if (mappings.length === 0) {
4930
+ console.log(`${colors5.dim}No device-specific themes configured.${colors5.reset}`);
4931
+ return;
4932
+ }
4933
+ console.log("Configured devices:");
4934
+ for (const mapping of mappings) {
4935
+ const marker = mapping.isCurrent ? ` ${colors5.green}(current)${colors5.reset}` : "";
4936
+ const date = new Date(mapping.setAt).toLocaleDateString();
4937
+ console.log(` ${colors5.blue}•${colors5.reset} ${mapping.device}${marker}: ${mapping.theme} ${colors5.dim}(set ${date})${colors5.reset}`);
4938
+ }
4939
+ }
4940
+ async function showThemeList() {
4941
+ const themes = await listThemes();
4942
+ const deviceTheme = getDeviceTheme();
4943
+ if (themes.length === 0) {
4944
+ console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
4945
+ console.log(`This system is compatible with omarchy themes.`);
4946
+ console.log(`
4947
+ Add themes to: ${colors5.cyan}~/.config/formalconf/themes/${colors5.reset}`);
4948
+ return;
4949
+ }
4950
+ console.log(`${colors5.cyan}Usage: formalconf theme <theme-name>${colors5.reset}`);
4951
+ console.log(` formalconf theme <theme-name> --save ${colors5.dim}(save as device preference)${colors5.reset}`);
4952
+ console.log(` formalconf theme --apply ${colors5.dim}(apply device's theme)${colors5.reset}`);
4953
+ console.log(` formalconf theme --list-devices ${colors5.dim}(show device mappings)${colors5.reset}`);
4954
+ console.log(` formalconf theme --default <name> ${colors5.dim}(set default theme)${colors5.reset}`);
4955
+ console.log(` formalconf theme --clear-default ${colors5.dim}(remove default theme)${colors5.reset}`);
4956
+ console.log(` formalconf theme --clear ${colors5.dim}(remove device mapping)${colors5.reset}`);
4957
+ console.log(` formalconf theme --info <theme-name> ${colors5.dim}(show theme details)${colors5.reset}
4958
+ `);
4959
+ console.log("Available themes:");
4960
+ for (const theme of themes) {
4961
+ const extras = [];
4962
+ if (theme.hasBackgrounds)
4963
+ extras.push("wallpapers");
4964
+ if (theme.isLightMode)
4965
+ extras.push("light");
4966
+ if (theme.name === deviceTheme)
4967
+ extras.push("device");
4968
+ const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
4969
+ console.log(` ${colors5.blue}•${colors5.reset} ${theme.name}${suffix}`);
4970
+ }
3328
4971
  }
3329
4972
  async function main4() {
3330
4973
  const { positionals, values } = parseArgs4({
3331
4974
  args: process.argv.slice(2),
3332
4975
  options: {
3333
- info: { type: "boolean", short: "i" }
4976
+ info: { type: "boolean", short: "i" },
4977
+ save: { type: "boolean", short: "s" },
4978
+ apply: { type: "boolean", short: "a" },
4979
+ "list-devices": { type: "boolean", short: "l" },
4980
+ default: { type: "string", short: "d" },
4981
+ "clear-default": { type: "boolean" },
4982
+ clear: { type: "boolean", short: "c" }
3334
4983
  },
3335
4984
  allowPositionals: true
3336
4985
  });
3337
4986
  const [themeName] = positionals;
3338
- if (!themeName) {
3339
- const themes = await listThemes();
3340
- if (themes.length === 0) {
3341
- console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
3342
- console.log(`This system is compatible with omarchy themes.`);
3343
- console.log(`
3344
- Add themes to: ${colors5.cyan}~/.config/formalconf/themes/${colors5.reset}`);
3345
- process.exit(0);
4987
+ if (values["list-devices"]) {
4988
+ showDeviceMappings();
4989
+ return;
4990
+ }
4991
+ if (values.clear) {
4992
+ const deviceTheme = getDeviceTheme();
4993
+ if (!deviceTheme) {
4994
+ console.log(`${colors5.yellow}No theme configured for this device.${colors5.reset}`);
4995
+ return;
3346
4996
  }
3347
- console.log(`${colors5.cyan}Usage: formalconf theme <theme-name>${colors5.reset}`);
3348
- console.log(` formalconf theme --info <theme-name>
3349
- `);
3350
- console.log("Available themes:");
3351
- for (const theme of themes) {
3352
- const extras = [];
3353
- if (theme.hasBackgrounds)
3354
- extras.push("wallpapers");
3355
- if (theme.isLightMode)
3356
- extras.push("light");
3357
- const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
3358
- console.log(` ${colors5.blue}•${colors5.reset} ${theme.name}${suffix}`);
4997
+ await clearDeviceTheme();
4998
+ console.log(`${colors5.green}Removed theme mapping for '${getDeviceHostname()}'.${colors5.reset}`);
4999
+ return;
5000
+ }
5001
+ if (values["clear-default"]) {
5002
+ await setDefaultTheme(null);
5003
+ console.log(`${colors5.green}Default theme cleared.${colors5.reset}`);
5004
+ return;
5005
+ }
5006
+ if (values.default !== undefined) {
5007
+ const themeDir = join5(THEMES_DIR, values.default);
5008
+ if (!existsSync7(themeDir)) {
5009
+ console.error(`${colors5.red}Error: Theme '${values.default}' not found${colors5.reset}`);
5010
+ process.exit(1);
3359
5011
  }
3360
- process.exit(0);
5012
+ await setDefaultTheme(values.default);
5013
+ console.log(`${colors5.green}Default theme set to '${values.default}'.${colors5.reset}`);
5014
+ return;
5015
+ }
5016
+ if (values.apply) {
5017
+ const deviceTheme = getDeviceTheme();
5018
+ if (!deviceTheme) {
5019
+ console.log(`${colors5.yellow}No theme configured for device '${getDeviceHostname()}'.${colors5.reset}`);
5020
+ console.log(`Use 'formalconf theme <name> --save' to set a device preference.`);
5021
+ return;
5022
+ }
5023
+ const result2 = await applyTheme(deviceTheme);
5024
+ console.log(result2.success ? `${colors5.green}${result2.output}${colors5.reset}` : `${colors5.red}${result2.output}${colors5.reset}`);
5025
+ return;
5026
+ }
5027
+ if (!themeName) {
5028
+ const deviceTheme = getDeviceTheme();
5029
+ if (deviceTheme) {
5030
+ const result2 = await applyTheme(deviceTheme);
5031
+ console.log(result2.success ? `${colors5.green}${result2.output}${colors5.reset}` : `${colors5.red}${result2.output}${colors5.reset}`);
5032
+ } else {
5033
+ await showThemeList();
5034
+ }
5035
+ return;
3361
5036
  }
3362
5037
  if (values.info) {
3363
5038
  await showThemeInfo(themeName);
3364
- } else {
3365
- const result = await applyTheme(themeName);
3366
- console.log(result.success ? `${colors5.green}${result.output}${colors5.reset}` : `${colors5.red}${result.output}${colors5.reset}`);
5039
+ return;
3367
5040
  }
5041
+ const result = await applyTheme(themeName, values.save ?? false);
5042
+ console.log(result.success ? `${colors5.green}${result.output}${colors5.reset}` : `${colors5.red}${result.output}${colors5.reset}`);
3368
5043
  }
3369
5044
  var isMainModule4 = process.argv[1]?.includes("set-theme");
3370
5045
  if (isMainModule4) {
@@ -3376,16 +5051,19 @@ import { jsxDEV as jsxDEV19 } from "react/jsx-dev-runtime";
3376
5051
  function ThemeMenu({ onBack }) {
3377
5052
  const [themes, setThemes] = useState10([]);
3378
5053
  const [loading, setLoading] = useState10(true);
5054
+ const [deviceTheme, setDeviceThemeName] = useState10(null);
3379
5055
  const { state, output, success, isRunning, isResult, execute, reset } = useMenuAction();
5056
+ const hostname2 = getDeviceHostname();
3380
5057
  const grid = useThemeGrid({
3381
5058
  itemCount: themes.length,
3382
- onSelect: (index) => applyTheme2(themes[index]),
5059
+ onSelect: (index) => applyTheme2(themes[index], false),
5060
+ onSelectAndSave: (index) => applyTheme2(themes[index], true),
3383
5061
  onBack,
3384
5062
  enabled: state === "menu" && !loading && themes.length > 0
3385
5063
  });
3386
- useEffect5(() => {
5064
+ useEffect6(() => {
3387
5065
  async function loadThemes() {
3388
- if (!existsSync7(THEMES_DIR)) {
5066
+ if (!existsSync8(THEMES_DIR)) {
3389
5067
  setThemes([]);
3390
5068
  setLoading(false);
3391
5069
  return;
@@ -3400,13 +5078,17 @@ function ThemeMenu({ onBack }) {
3400
5078
  }
3401
5079
  }
3402
5080
  setThemes(loadedThemes);
5081
+ setDeviceThemeName(getDeviceTheme());
3403
5082
  setLoading(false);
3404
5083
  }
3405
5084
  loadThemes();
3406
5085
  }, []);
3407
- const applyTheme2 = async (theme) => {
5086
+ const applyTheme2 = async (theme, saveAsDeviceDefault) => {
3408
5087
  const themeName = theme.path.split("/").pop();
3409
- await execute(() => runSetTheme(themeName));
5088
+ await execute(() => runSetTheme(themeName, saveAsDeviceDefault));
5089
+ if (saveAsDeviceDefault) {
5090
+ setDeviceThemeName(themeName);
5091
+ }
3410
5092
  };
3411
5093
  const visibleThemes = useMemo4(() => {
3412
5094
  return themes.slice(grid.visibleStartIndex, grid.visibleEndIndex);
@@ -3476,7 +5158,8 @@ function ThemeMenu({ onBack }) {
3476
5158
  children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV19(ThemeCard, {
3477
5159
  theme,
3478
5160
  isSelected: grid.visibleStartIndex + index === grid.selectedIndex,
3479
- width: grid.cardWidth
5161
+ width: grid.cardWidth,
5162
+ isDeviceTheme: theme.name === deviceTheme
3480
5163
  }, theme.path, false, undefined, this))
3481
5164
  }, undefined, false, undefined, this),
3482
5165
  grid.showScrollDown && /* @__PURE__ */ jsxDEV19(Text15, {
@@ -3491,16 +5174,27 @@ function ThemeMenu({ onBack }) {
3491
5174
  }, undefined, true, undefined, this),
3492
5175
  /* @__PURE__ */ jsxDEV19(Box16, {
3493
5176
  marginTop: 1,
3494
- children: /* @__PURE__ */ jsxDEV19(Text15, {
3495
- dimColor: true,
3496
- children: "←→↑↓/hjkl navigate Enter select • Esc back"
3497
- }, undefined, false, undefined, this)
3498
- }, undefined, false, undefined, this)
5177
+ flexDirection: "column",
5178
+ children: [
5179
+ /* @__PURE__ */ jsxDEV19(Text15, {
5180
+ dimColor: true,
5181
+ children: "←→↑↓/hjkl navigate Enter apply • Shift+Enter save as device default • Esc back"
5182
+ }, undefined, false, undefined, this),
5183
+ /* @__PURE__ */ jsxDEV19(Text15, {
5184
+ dimColor: true,
5185
+ children: [
5186
+ "Device: ",
5187
+ hostname2
5188
+ ]
5189
+ }, undefined, true, undefined, this)
5190
+ ]
5191
+ }, undefined, true, undefined, this)
3499
5192
  ]
3500
5193
  }, undefined, true, undefined, this);
3501
5194
  }
3502
5195
 
3503
5196
  // src/cli/formalconf.tsx
5197
+ init_runtime();
3504
5198
  import { jsxDEV as jsxDEV20 } from "react/jsx-dev-runtime";
3505
5199
  var BREADCRUMBS = {
3506
5200
  main: ["Main"],
@@ -3517,7 +5211,7 @@ function App() {
3517
5211
  if (input === "q")
3518
5212
  exit();
3519
5213
  });
3520
- useEffect6(() => {
5214
+ useEffect7(() => {
3521
5215
  async function init() {
3522
5216
  await ensureConfigDir();
3523
5217
  const result = await checkPrerequisites();