formalconf 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +280 -0
- package/dist/formalconf.js +2339 -0
- package/package.json +53 -0
|
@@ -0,0 +1,2339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/cli/formalconf.tsx
|
|
3
|
+
import { useState as useState4, useEffect as useEffect3, useMemo } from "react";
|
|
4
|
+
import { render, Box as Box10, Text as Text9, useApp, useInput as useInput3 } from "ink";
|
|
5
|
+
import { Spinner } from "@inkjs/ui";
|
|
6
|
+
|
|
7
|
+
// src/components/ui/VimSelect.tsx
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { Box, Text, useInput } from "ink";
|
|
10
|
+
|
|
11
|
+
// src/lib/theme.ts
|
|
12
|
+
var colors = {
|
|
13
|
+
primary: "#5eead4",
|
|
14
|
+
primaryDim: "#2dd4bf",
|
|
15
|
+
accent: "#06b6d4",
|
|
16
|
+
success: "#22c55e",
|
|
17
|
+
error: "#ef4444",
|
|
18
|
+
warning: "#f59e0b",
|
|
19
|
+
info: "#3b82f6",
|
|
20
|
+
text: "white",
|
|
21
|
+
textDim: "gray",
|
|
22
|
+
border: "#374151",
|
|
23
|
+
borderLight: "#4b5563"
|
|
24
|
+
};
|
|
25
|
+
var borderStyles = {
|
|
26
|
+
panel: "round",
|
|
27
|
+
header: "round",
|
|
28
|
+
footer: "single"
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/components/ui/VimSelect.tsx
|
|
32
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
33
|
+
function VimSelect({ options, onChange, isDisabled = false }) {
|
|
34
|
+
const [index, setIndex] = useState(0);
|
|
35
|
+
useInput((input, key) => {
|
|
36
|
+
if (isDisabled)
|
|
37
|
+
return;
|
|
38
|
+
if (input === "j" || key.downArrow) {
|
|
39
|
+
setIndex((i) => i < options.length - 1 ? i + 1 : i);
|
|
40
|
+
}
|
|
41
|
+
if (input === "k" || key.upArrow) {
|
|
42
|
+
setIndex((i) => i > 0 ? i - 1 : i);
|
|
43
|
+
}
|
|
44
|
+
if (input === "l" || key.return) {
|
|
45
|
+
onChange(options[index].value);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
49
|
+
flexDirection: "column",
|
|
50
|
+
children: options.map((opt, i) => /* @__PURE__ */ jsxDEV(Box, {
|
|
51
|
+
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
52
|
+
color: i === index ? colors.primary : undefined,
|
|
53
|
+
children: [
|
|
54
|
+
i === index ? "❯" : " ",
|
|
55
|
+
" ",
|
|
56
|
+
opt.label
|
|
57
|
+
]
|
|
58
|
+
}, undefined, true, undefined, this)
|
|
59
|
+
}, opt.value, false, undefined, this))
|
|
60
|
+
}, undefined, false, undefined, this);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/cli/formalconf.tsx
|
|
64
|
+
import { readdirSync as readdirSync5, existsSync as existsSync6 } from "fs";
|
|
65
|
+
import { join as join5 } from "path";
|
|
66
|
+
|
|
67
|
+
// src/components/layout/Layout.tsx
|
|
68
|
+
import { Box as Box6 } from "ink";
|
|
69
|
+
|
|
70
|
+
// src/hooks/useTerminalSize.ts
|
|
71
|
+
import { useStdout } from "ink";
|
|
72
|
+
import { useState as useState2, useEffect } from "react";
|
|
73
|
+
function useTerminalSize() {
|
|
74
|
+
const { stdout } = useStdout();
|
|
75
|
+
const [size, setSize] = useState2({
|
|
76
|
+
columns: stdout.columns || 80,
|
|
77
|
+
rows: stdout.rows || 24
|
|
78
|
+
});
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const handleResize = () => {
|
|
81
|
+
setSize({
|
|
82
|
+
columns: stdout.columns || 80,
|
|
83
|
+
rows: stdout.rows || 24
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
stdout.on("resize", handleResize);
|
|
87
|
+
return () => {
|
|
88
|
+
stdout.off("resize", handleResize);
|
|
89
|
+
};
|
|
90
|
+
}, [stdout]);
|
|
91
|
+
return size;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/components/Header.tsx
|
|
95
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
96
|
+
|
|
97
|
+
// src/hooks/useSystemStatus.ts
|
|
98
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
99
|
+
import { existsSync, readlinkSync, readdirSync, lstatSync } from "fs";
|
|
100
|
+
|
|
101
|
+
// src/lib/paths.ts
|
|
102
|
+
import { homedir } from "os";
|
|
103
|
+
import { join } from "path";
|
|
104
|
+
|
|
105
|
+
// src/lib/runtime.ts
|
|
106
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
107
|
+
import { readFile as nodeReadFile, writeFile as nodeWriteFile, mkdir } from "fs/promises";
|
|
108
|
+
import { dirname } from "path";
|
|
109
|
+
import { fileURLToPath } from "url";
|
|
110
|
+
var isBun = typeof Bun !== "undefined";
|
|
111
|
+
async function exec(command, cwd) {
|
|
112
|
+
if (isBun) {
|
|
113
|
+
const proc = Bun.spawn(command, {
|
|
114
|
+
stdout: "pipe",
|
|
115
|
+
stderr: "pipe",
|
|
116
|
+
cwd
|
|
117
|
+
});
|
|
118
|
+
const [stdout, stderr] = await Promise.all([
|
|
119
|
+
new Response(proc.stdout).text(),
|
|
120
|
+
new Response(proc.stderr).text()
|
|
121
|
+
]);
|
|
122
|
+
const exitCode = await proc.exited;
|
|
123
|
+
return {
|
|
124
|
+
stdout: stdout.trim(),
|
|
125
|
+
stderr: stderr.trim(),
|
|
126
|
+
exitCode,
|
|
127
|
+
success: exitCode === 0
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
const [cmd, ...args] = command;
|
|
132
|
+
const proc = nodeSpawn(cmd, args, { cwd, shell: false });
|
|
133
|
+
let stdout = "";
|
|
134
|
+
let stderr = "";
|
|
135
|
+
proc.stdout?.on("data", (data) => {
|
|
136
|
+
stdout += data;
|
|
137
|
+
});
|
|
138
|
+
proc.stderr?.on("data", (data) => {
|
|
139
|
+
stderr += data;
|
|
140
|
+
});
|
|
141
|
+
proc.on("close", (code) => {
|
|
142
|
+
resolve({
|
|
143
|
+
stdout: stdout.trim(),
|
|
144
|
+
stderr: stderr.trim(),
|
|
145
|
+
exitCode: code ?? 1,
|
|
146
|
+
success: code === 0
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function execLive(command, cwd) {
|
|
152
|
+
if (isBun) {
|
|
153
|
+
const proc = Bun.spawn(command, {
|
|
154
|
+
stdout: "inherit",
|
|
155
|
+
stderr: "inherit",
|
|
156
|
+
stdin: "inherit",
|
|
157
|
+
cwd
|
|
158
|
+
});
|
|
159
|
+
return await proc.exited;
|
|
160
|
+
}
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const [cmd, ...args] = command;
|
|
163
|
+
const proc = nodeSpawn(cmd, args, {
|
|
164
|
+
cwd,
|
|
165
|
+
stdio: "inherit",
|
|
166
|
+
shell: false
|
|
167
|
+
});
|
|
168
|
+
proc.on("close", (code) => {
|
|
169
|
+
resolve(code ?? 1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async function readJson(path) {
|
|
174
|
+
if (isBun) {
|
|
175
|
+
return Bun.file(path).json();
|
|
176
|
+
}
|
|
177
|
+
const content = await nodeReadFile(path, "utf-8");
|
|
178
|
+
return JSON.parse(content);
|
|
179
|
+
}
|
|
180
|
+
async function readText(path) {
|
|
181
|
+
if (isBun) {
|
|
182
|
+
return Bun.file(path).text();
|
|
183
|
+
}
|
|
184
|
+
return nodeReadFile(path, "utf-8");
|
|
185
|
+
}
|
|
186
|
+
async function writeFile(path, content) {
|
|
187
|
+
if (isBun) {
|
|
188
|
+
await Bun.write(path, content);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await nodeWriteFile(path, content, "utf-8");
|
|
192
|
+
}
|
|
193
|
+
async function ensureDir(path) {
|
|
194
|
+
if (isBun) {
|
|
195
|
+
await Bun.$`mkdir -p ${path}`.quiet();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await mkdir(path, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
function getScriptDir(importMeta) {
|
|
201
|
+
if (isBun && importMeta.dir) {
|
|
202
|
+
return importMeta.dir;
|
|
203
|
+
}
|
|
204
|
+
if (importMeta.dirname) {
|
|
205
|
+
return importMeta.dirname;
|
|
206
|
+
}
|
|
207
|
+
return dirname(fileURLToPath(importMeta.url));
|
|
208
|
+
}
|
|
209
|
+
async function commandExists(cmd) {
|
|
210
|
+
const result = await exec(["which", cmd]);
|
|
211
|
+
return result.success;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/lib/paths.ts
|
|
215
|
+
var HOME_DIR = homedir();
|
|
216
|
+
var CONFIG_DIR = join(HOME_DIR, ".config", "formalconf");
|
|
217
|
+
var THEME_TARGET_DIR = join(CONFIG_DIR, "current", "theme");
|
|
218
|
+
var BACKGROUNDS_TARGET_DIR = join(CONFIG_DIR, "current", "backgrounds");
|
|
219
|
+
var scriptPath = getScriptDir(import.meta);
|
|
220
|
+
var ROOT_DIR = join(scriptPath, "..", "..");
|
|
221
|
+
var CONFIGS_DIR = join(CONFIG_DIR, "configs");
|
|
222
|
+
var THEMES_DIR = join(CONFIG_DIR, "themes");
|
|
223
|
+
var PKG_CONFIG_PATH = join(CONFIG_DIR, "pkg-config.json");
|
|
224
|
+
var PKG_LOCK_PATH = join(CONFIG_DIR, "pkg-lock.json");
|
|
225
|
+
async function ensureDir2(path) {
|
|
226
|
+
await ensureDir(path);
|
|
227
|
+
}
|
|
228
|
+
async function ensureConfigDir() {
|
|
229
|
+
await ensureDir2(CONFIG_DIR);
|
|
230
|
+
await ensureDir2(CONFIGS_DIR);
|
|
231
|
+
await ensureDir2(THEMES_DIR);
|
|
232
|
+
await ensureDir2(THEME_TARGET_DIR);
|
|
233
|
+
await ensureDir2(BACKGROUNDS_TARGET_DIR);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/hooks/useSystemStatus.ts
|
|
237
|
+
import { basename, dirname as dirname2, join as join2 } from "path";
|
|
238
|
+
function useSystemStatus() {
|
|
239
|
+
const [status, setStatus] = useState3({
|
|
240
|
+
currentTheme: null,
|
|
241
|
+
configsLinked: false,
|
|
242
|
+
loading: true
|
|
243
|
+
});
|
|
244
|
+
useEffect2(() => {
|
|
245
|
+
let theme = null;
|
|
246
|
+
try {
|
|
247
|
+
if (existsSync(THEME_TARGET_DIR)) {
|
|
248
|
+
const entries = readdirSync(THEME_TARGET_DIR);
|
|
249
|
+
for (const entry of entries) {
|
|
250
|
+
const entryPath = join2(THEME_TARGET_DIR, entry);
|
|
251
|
+
if (lstatSync(entryPath).isSymbolicLink()) {
|
|
252
|
+
const target = readlinkSync(entryPath);
|
|
253
|
+
theme = basename(dirname2(target));
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch {}
|
|
259
|
+
let configsLinked = false;
|
|
260
|
+
try {
|
|
261
|
+
if (existsSync(CONFIGS_DIR)) {
|
|
262
|
+
const entries = readdirSync(CONFIGS_DIR);
|
|
263
|
+
configsLinked = entries.length > 0;
|
|
264
|
+
}
|
|
265
|
+
} catch {}
|
|
266
|
+
setStatus({
|
|
267
|
+
currentTheme: theme,
|
|
268
|
+
configsLinked,
|
|
269
|
+
loading: false
|
|
270
|
+
});
|
|
271
|
+
}, []);
|
|
272
|
+
return status;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/components/ui/StatusIndicator.tsx
|
|
276
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
277
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
278
|
+
function StatusIndicator({
|
|
279
|
+
label,
|
|
280
|
+
value,
|
|
281
|
+
status = "neutral"
|
|
282
|
+
}) {
|
|
283
|
+
const statusColors = {
|
|
284
|
+
success: colors.success,
|
|
285
|
+
warning: colors.warning,
|
|
286
|
+
error: colors.error,
|
|
287
|
+
neutral: colors.textDim
|
|
288
|
+
};
|
|
289
|
+
const icon = {
|
|
290
|
+
success: "●",
|
|
291
|
+
warning: "●",
|
|
292
|
+
error: "●",
|
|
293
|
+
neutral: "○"
|
|
294
|
+
};
|
|
295
|
+
return /* @__PURE__ */ jsxDEV2(Box2, {
|
|
296
|
+
gap: 1,
|
|
297
|
+
children: [
|
|
298
|
+
/* @__PURE__ */ jsxDEV2(Text2, {
|
|
299
|
+
dimColor: true,
|
|
300
|
+
children: [
|
|
301
|
+
label,
|
|
302
|
+
":"
|
|
303
|
+
]
|
|
304
|
+
}, undefined, true, undefined, this),
|
|
305
|
+
/* @__PURE__ */ jsxDEV2(Text2, {
|
|
306
|
+
color: statusColors[status],
|
|
307
|
+
children: [
|
|
308
|
+
icon[status],
|
|
309
|
+
" ",
|
|
310
|
+
value || "None"
|
|
311
|
+
]
|
|
312
|
+
}, undefined, true, undefined, this)
|
|
313
|
+
]
|
|
314
|
+
}, undefined, true, undefined, this);
|
|
315
|
+
}
|
|
316
|
+
// package.json
|
|
317
|
+
var package_default = {
|
|
318
|
+
name: "formalconf",
|
|
319
|
+
version: "2.0.0",
|
|
320
|
+
description: "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
|
|
321
|
+
type: "module",
|
|
322
|
+
main: "./dist/formalconf.js",
|
|
323
|
+
bin: {
|
|
324
|
+
formalconf: "dist/formalconf.js"
|
|
325
|
+
},
|
|
326
|
+
files: [
|
|
327
|
+
"dist"
|
|
328
|
+
],
|
|
329
|
+
scripts: {
|
|
330
|
+
formalconf: "bun run src/cli/formalconf.tsx",
|
|
331
|
+
config: "bun run src/cli/config-manager.ts",
|
|
332
|
+
"pkg-sync": "bun run src/cli/pkg-sync.ts",
|
|
333
|
+
"pkg-lock": "bun run src/cli/pkg-lock.ts",
|
|
334
|
+
theme: "bun run src/cli/set-theme.ts",
|
|
335
|
+
typecheck: "bun tsc --noEmit",
|
|
336
|
+
build: "bun run scripts/build.ts",
|
|
337
|
+
prepublishOnly: "bun run build"
|
|
338
|
+
},
|
|
339
|
+
engines: {
|
|
340
|
+
node: ">=20.11.0"
|
|
341
|
+
},
|
|
342
|
+
keywords: [
|
|
343
|
+
"dotfiles",
|
|
344
|
+
"config",
|
|
345
|
+
"stow",
|
|
346
|
+
"homebrew",
|
|
347
|
+
"macos",
|
|
348
|
+
"tui",
|
|
349
|
+
"cli",
|
|
350
|
+
"themes"
|
|
351
|
+
],
|
|
352
|
+
author: "Kyan De Sutter <hello@formalsnake.dev>",
|
|
353
|
+
license: "MIT",
|
|
354
|
+
repository: {
|
|
355
|
+
type: "git",
|
|
356
|
+
url: "git+https://github.com/FormalSnake/formalconf.git"
|
|
357
|
+
},
|
|
358
|
+
dependencies: {
|
|
359
|
+
ink: "^5.0.1",
|
|
360
|
+
"@inkjs/ui": "^2.0.0",
|
|
361
|
+
react: "^18.3.1"
|
|
362
|
+
},
|
|
363
|
+
devDependencies: {
|
|
364
|
+
"@types/bun": "latest",
|
|
365
|
+
"@types/node": "^20.0.0",
|
|
366
|
+
"@types/react": "^18.3.0",
|
|
367
|
+
typescript: "^5.0.0"
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// src/components/Header.tsx
|
|
372
|
+
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
373
|
+
function Header() {
|
|
374
|
+
const { columns } = useTerminalSize();
|
|
375
|
+
const { currentTheme, configsLinked, loading } = useSystemStatus();
|
|
376
|
+
return /* @__PURE__ */ jsxDEV3(Box3, {
|
|
377
|
+
flexDirection: "column",
|
|
378
|
+
width: columns - 2,
|
|
379
|
+
borderStyle: borderStyles.header,
|
|
380
|
+
borderColor: colors.primary,
|
|
381
|
+
paddingX: 2,
|
|
382
|
+
marginBottom: 1,
|
|
383
|
+
children: [
|
|
384
|
+
/* @__PURE__ */ jsxDEV3(Box3, {
|
|
385
|
+
justifyContent: "space-between",
|
|
386
|
+
width: "100%",
|
|
387
|
+
children: [
|
|
388
|
+
/* @__PURE__ */ jsxDEV3(Box3, {
|
|
389
|
+
children: [
|
|
390
|
+
/* @__PURE__ */ jsxDEV3(Text3, {
|
|
391
|
+
bold: true,
|
|
392
|
+
color: colors.primary,
|
|
393
|
+
children: "FormalConf"
|
|
394
|
+
}, undefined, false, undefined, this),
|
|
395
|
+
/* @__PURE__ */ jsxDEV3(Text3, {
|
|
396
|
+
dimColor: true,
|
|
397
|
+
children: " - Dotfiles Manager"
|
|
398
|
+
}, undefined, false, undefined, this)
|
|
399
|
+
]
|
|
400
|
+
}, undefined, true, undefined, this),
|
|
401
|
+
/* @__PURE__ */ jsxDEV3(Text3, {
|
|
402
|
+
dimColor: true,
|
|
403
|
+
children: [
|
|
404
|
+
"v",
|
|
405
|
+
package_default.version
|
|
406
|
+
]
|
|
407
|
+
}, undefined, true, undefined, this)
|
|
408
|
+
]
|
|
409
|
+
}, undefined, true, undefined, this),
|
|
410
|
+
!loading && /* @__PURE__ */ jsxDEV3(Box3, {
|
|
411
|
+
marginTop: 1,
|
|
412
|
+
gap: 4,
|
|
413
|
+
children: [
|
|
414
|
+
/* @__PURE__ */ jsxDEV3(StatusIndicator, {
|
|
415
|
+
label: "Theme",
|
|
416
|
+
value: currentTheme,
|
|
417
|
+
status: currentTheme ? "success" : "neutral"
|
|
418
|
+
}, undefined, false, undefined, this),
|
|
419
|
+
/* @__PURE__ */ jsxDEV3(StatusIndicator, {
|
|
420
|
+
label: "Configs",
|
|
421
|
+
value: configsLinked ? "Linked" : "Not linked",
|
|
422
|
+
status: configsLinked ? "success" : "warning"
|
|
423
|
+
}, undefined, false, undefined, this)
|
|
424
|
+
]
|
|
425
|
+
}, undefined, true, undefined, this)
|
|
426
|
+
]
|
|
427
|
+
}, undefined, true, undefined, this);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/components/layout/Footer.tsx
|
|
431
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
432
|
+
import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
|
|
433
|
+
var defaultShortcuts = [
|
|
434
|
+
{ key: "↑↓/jk", label: "Navigate" },
|
|
435
|
+
{ key: "Enter/l", label: "Select" },
|
|
436
|
+
{ key: "ESC/h", label: "Back" },
|
|
437
|
+
{ key: "q", label: "Quit" }
|
|
438
|
+
];
|
|
439
|
+
function Footer({ shortcuts = defaultShortcuts }) {
|
|
440
|
+
const { columns } = useTerminalSize();
|
|
441
|
+
return /* @__PURE__ */ jsxDEV4(Box4, {
|
|
442
|
+
width: columns - 2,
|
|
443
|
+
borderStyle: borderStyles.footer,
|
|
444
|
+
borderColor: colors.border,
|
|
445
|
+
paddingX: 2,
|
|
446
|
+
marginTop: 1,
|
|
447
|
+
justifyContent: "center",
|
|
448
|
+
gap: 2,
|
|
449
|
+
children: shortcuts.map((shortcut, index) => /* @__PURE__ */ jsxDEV4(Box4, {
|
|
450
|
+
gap: 1,
|
|
451
|
+
children: [
|
|
452
|
+
/* @__PURE__ */ jsxDEV4(Text4, {
|
|
453
|
+
color: colors.primary,
|
|
454
|
+
bold: true,
|
|
455
|
+
children: shortcut.key
|
|
456
|
+
}, undefined, false, undefined, this),
|
|
457
|
+
/* @__PURE__ */ jsxDEV4(Text4, {
|
|
458
|
+
dimColor: true,
|
|
459
|
+
children: shortcut.label
|
|
460
|
+
}, undefined, false, undefined, this)
|
|
461
|
+
]
|
|
462
|
+
}, index, true, undefined, this))
|
|
463
|
+
}, undefined, false, undefined, this);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/components/layout/Breadcrumb.tsx
|
|
467
|
+
import React2 from "react";
|
|
468
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
469
|
+
import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
|
|
470
|
+
function Breadcrumb({ path }) {
|
|
471
|
+
return /* @__PURE__ */ jsxDEV5(Box5, {
|
|
472
|
+
children: path.map((segment, index) => /* @__PURE__ */ jsxDEV5(React2.Fragment, {
|
|
473
|
+
children: [
|
|
474
|
+
index > 0 && /* @__PURE__ */ jsxDEV5(Text5, {
|
|
475
|
+
color: colors.textDim,
|
|
476
|
+
children: [
|
|
477
|
+
" ",
|
|
478
|
+
">",
|
|
479
|
+
" "
|
|
480
|
+
]
|
|
481
|
+
}, undefined, true, undefined, this),
|
|
482
|
+
/* @__PURE__ */ jsxDEV5(Text5, {
|
|
483
|
+
color: index === path.length - 1 ? colors.primary : colors.textDim,
|
|
484
|
+
bold: index === path.length - 1,
|
|
485
|
+
children: segment
|
|
486
|
+
}, undefined, false, undefined, this)
|
|
487
|
+
]
|
|
488
|
+
}, index, true, undefined, this))
|
|
489
|
+
}, undefined, false, undefined, this);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/components/layout/Layout.tsx
|
|
493
|
+
import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
|
|
494
|
+
function Layout({
|
|
495
|
+
children,
|
|
496
|
+
breadcrumb = ["Main"],
|
|
497
|
+
showFooter = true
|
|
498
|
+
}) {
|
|
499
|
+
const { columns } = useTerminalSize();
|
|
500
|
+
return /* @__PURE__ */ jsxDEV6(Box6, {
|
|
501
|
+
flexDirection: "column",
|
|
502
|
+
width: columns,
|
|
503
|
+
padding: 1,
|
|
504
|
+
children: [
|
|
505
|
+
/* @__PURE__ */ jsxDEV6(Header, {}, undefined, false, undefined, this),
|
|
506
|
+
breadcrumb.length > 1 && /* @__PURE__ */ jsxDEV6(Box6, {
|
|
507
|
+
marginBottom: 1,
|
|
508
|
+
marginLeft: 1,
|
|
509
|
+
children: /* @__PURE__ */ jsxDEV6(Breadcrumb, {
|
|
510
|
+
path: breadcrumb
|
|
511
|
+
}, undefined, false, undefined, this)
|
|
512
|
+
}, undefined, false, undefined, this),
|
|
513
|
+
/* @__PURE__ */ jsxDEV6(Box6, {
|
|
514
|
+
flexDirection: "column",
|
|
515
|
+
flexGrow: 1,
|
|
516
|
+
children
|
|
517
|
+
}, undefined, false, undefined, this),
|
|
518
|
+
showFooter && /* @__PURE__ */ jsxDEV6(Footer, {}, undefined, false, undefined, this)
|
|
519
|
+
]
|
|
520
|
+
}, undefined, true, undefined, this);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/components/layout/Panel.tsx
|
|
524
|
+
import { Box as Box7, Text as Text6 } from "ink";
|
|
525
|
+
import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
|
|
526
|
+
function Panel({
|
|
527
|
+
title,
|
|
528
|
+
children,
|
|
529
|
+
width,
|
|
530
|
+
flexGrow,
|
|
531
|
+
borderColor = colors.border
|
|
532
|
+
}) {
|
|
533
|
+
return /* @__PURE__ */ jsxDEV7(Box7, {
|
|
534
|
+
flexDirection: "column",
|
|
535
|
+
width,
|
|
536
|
+
flexGrow,
|
|
537
|
+
borderStyle: borderStyles.panel,
|
|
538
|
+
borderColor,
|
|
539
|
+
paddingX: 1,
|
|
540
|
+
children: [
|
|
541
|
+
title && /* @__PURE__ */ jsxDEV7(Box7, {
|
|
542
|
+
marginBottom: 1,
|
|
543
|
+
children: /* @__PURE__ */ jsxDEV7(Text6, {
|
|
544
|
+
bold: true,
|
|
545
|
+
color: colors.primary,
|
|
546
|
+
children: title
|
|
547
|
+
}, undefined, false, undefined, this)
|
|
548
|
+
}, undefined, false, undefined, this),
|
|
549
|
+
children
|
|
550
|
+
]
|
|
551
|
+
}, undefined, true, undefined, this);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/components/CommandOutput.tsx
|
|
555
|
+
import { Box as Box8, Text as Text7, useInput as useInput2 } from "ink";
|
|
556
|
+
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
557
|
+
function CommandOutput({
|
|
558
|
+
title,
|
|
559
|
+
output,
|
|
560
|
+
success = true,
|
|
561
|
+
onDismiss
|
|
562
|
+
}) {
|
|
563
|
+
useInput2(() => {
|
|
564
|
+
onDismiss();
|
|
565
|
+
});
|
|
566
|
+
return /* @__PURE__ */ jsxDEV8(Panel, {
|
|
567
|
+
title,
|
|
568
|
+
borderColor: success ? colors.success : colors.error,
|
|
569
|
+
children: [
|
|
570
|
+
output && /* @__PURE__ */ jsxDEV8(Box8, {
|
|
571
|
+
flexDirection: "column",
|
|
572
|
+
marginBottom: 1,
|
|
573
|
+
children: /* @__PURE__ */ jsxDEV8(Text7, {
|
|
574
|
+
children: output
|
|
575
|
+
}, undefined, false, undefined, this)
|
|
576
|
+
}, undefined, false, undefined, this),
|
|
577
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
578
|
+
color: success ? colors.success : colors.error,
|
|
579
|
+
children: success ? "Done" : "Failed"
|
|
580
|
+
}, undefined, false, undefined, this),
|
|
581
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
582
|
+
dimColor: true,
|
|
583
|
+
children: "Press any key to continue..."
|
|
584
|
+
}, undefined, false, undefined, this)
|
|
585
|
+
]
|
|
586
|
+
}, undefined, true, undefined, this);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/components/ThemeCard.tsx
|
|
590
|
+
import { Box as Box9, Text as Text8 } from "ink";
|
|
591
|
+
import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
|
|
592
|
+
function ThemeCard({ theme, isSelected, width }) {
|
|
593
|
+
const borderColor = isSelected ? colors.accent : colors.border;
|
|
594
|
+
const nameColor = isSelected ? colors.primary : colors.text;
|
|
595
|
+
const indicators = [];
|
|
596
|
+
if (theme.hasBackgrounds)
|
|
597
|
+
indicators.push("bg");
|
|
598
|
+
if (theme.isLightMode)
|
|
599
|
+
indicators.push("light");
|
|
600
|
+
const indicatorText = indicators.length > 0 ? ` [${indicators.join(" ")}]` : "";
|
|
601
|
+
return /* @__PURE__ */ jsxDEV9(Box9, {
|
|
602
|
+
flexDirection: "column",
|
|
603
|
+
width,
|
|
604
|
+
borderStyle: borderStyles.panel,
|
|
605
|
+
borderColor,
|
|
606
|
+
paddingX: 1,
|
|
607
|
+
children: /* @__PURE__ */ jsxDEV9(Box9, {
|
|
608
|
+
children: [
|
|
609
|
+
/* @__PURE__ */ jsxDEV9(Text8, {
|
|
610
|
+
color: isSelected ? colors.accent : colors.primaryDim,
|
|
611
|
+
children: isSelected ? "● " : " "
|
|
612
|
+
}, undefined, false, undefined, this),
|
|
613
|
+
/* @__PURE__ */ jsxDEV9(Text8, {
|
|
614
|
+
color: nameColor,
|
|
615
|
+
bold: true,
|
|
616
|
+
wrap: "truncate",
|
|
617
|
+
children: theme.name
|
|
618
|
+
}, undefined, false, undefined, this),
|
|
619
|
+
/* @__PURE__ */ jsxDEV9(Text8, {
|
|
620
|
+
color: colors.primaryDim,
|
|
621
|
+
children: indicatorText
|
|
622
|
+
}, undefined, false, undefined, this)
|
|
623
|
+
]
|
|
624
|
+
}, undefined, true, undefined, this)
|
|
625
|
+
}, undefined, false, undefined, this);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/lib/theme-parser.ts
|
|
629
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
630
|
+
import { join as join3 } from "path";
|
|
631
|
+
function parseYaml(content) {
|
|
632
|
+
const result = {};
|
|
633
|
+
const lines = content.split(`
|
|
634
|
+
`);
|
|
635
|
+
let currentSection = null;
|
|
636
|
+
let currentKey = "";
|
|
637
|
+
for (const line of lines) {
|
|
638
|
+
const trimmed = line.trim();
|
|
639
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
640
|
+
continue;
|
|
641
|
+
const indentLevel = line.search(/\S/);
|
|
642
|
+
const match = trimmed.match(/^([\w-]+):\s*(.*)$/);
|
|
643
|
+
if (match) {
|
|
644
|
+
const [, key, value] = match;
|
|
645
|
+
if (indentLevel === 0) {
|
|
646
|
+
if (value) {
|
|
647
|
+
result[key] = value.replace(/^["']|["']$/g, "");
|
|
648
|
+
} else {
|
|
649
|
+
currentKey = key;
|
|
650
|
+
currentSection = {};
|
|
651
|
+
result[key] = currentSection;
|
|
652
|
+
}
|
|
653
|
+
} else if (currentSection) {
|
|
654
|
+
currentSection[key] = value.replace(/^["']|["']$/g, "");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
async function parseThemeMetadata(themePath) {
|
|
661
|
+
const yamlPath = join3(themePath, "theme.yaml");
|
|
662
|
+
if (!existsSync2(yamlPath)) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
const content = await readText(yamlPath);
|
|
667
|
+
const parsed = parseYaml(content);
|
|
668
|
+
return {
|
|
669
|
+
name: parsed.name || "",
|
|
670
|
+
author: parsed.author,
|
|
671
|
+
description: parsed.description,
|
|
672
|
+
version: parsed.version,
|
|
673
|
+
source: parsed.source,
|
|
674
|
+
colors: parsed.colors
|
|
675
|
+
};
|
|
676
|
+
} catch {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function parseThemeFiles(themePath) {
|
|
681
|
+
const entries = readdirSync2(themePath, { withFileTypes: true });
|
|
682
|
+
return entries.filter((e) => e.isFile() && !e.name.startsWith(".") && e.name !== "theme.yaml" && e.name !== "light.mode").map((e) => ({
|
|
683
|
+
name: e.name,
|
|
684
|
+
path: join3(themePath, e.name),
|
|
685
|
+
application: e.name.replace(/\.(conf|theme|lua|toml|css|json|ini)$/, "")
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
async function parseTheme(themePath, themeName) {
|
|
689
|
+
const files = parseThemeFiles(themePath);
|
|
690
|
+
const metadata = await parseThemeMetadata(themePath);
|
|
691
|
+
return {
|
|
692
|
+
name: metadata?.name || themeName,
|
|
693
|
+
path: themePath,
|
|
694
|
+
files,
|
|
695
|
+
metadata,
|
|
696
|
+
hasBackgrounds: existsSync2(join3(themePath, "backgrounds")),
|
|
697
|
+
hasPreview: existsSync2(join3(themePath, "preview.png")),
|
|
698
|
+
isLightMode: existsSync2(join3(themePath, "light.mode"))
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/cli/config-manager.ts
|
|
703
|
+
import { parseArgs } from "util";
|
|
704
|
+
import { readdirSync as readdirSync3, existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync as readlinkSync2 } from "fs";
|
|
705
|
+
var colors2 = {
|
|
706
|
+
red: "\x1B[0;31m",
|
|
707
|
+
green: "\x1B[0;32m",
|
|
708
|
+
blue: "\x1B[0;34m",
|
|
709
|
+
yellow: "\x1B[1;33m",
|
|
710
|
+
cyan: "\x1B[0;36m",
|
|
711
|
+
reset: "\x1B[0m"
|
|
712
|
+
};
|
|
713
|
+
async function checkStow() {
|
|
714
|
+
if (!await commandExists("stow")) {
|
|
715
|
+
console.error(`${colors2.red}Error: GNU Stow is not installed. Install with: brew install stow${colors2.reset}`);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function listPackages() {
|
|
720
|
+
const entries = readdirSync3(CONFIGS_DIR, { withFileTypes: true });
|
|
721
|
+
return entries.filter((e) => e.isDirectory()).map((e) => ({
|
|
722
|
+
name: e.name,
|
|
723
|
+
path: `${CONFIGS_DIR}/${e.name}`,
|
|
724
|
+
isStowed: checkPackageStowed(e.name)
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
function checkPackageStowed(packageName) {
|
|
728
|
+
const packageDir = `${CONFIGS_DIR}/${packageName}`;
|
|
729
|
+
if (!existsSync3(packageDir))
|
|
730
|
+
return false;
|
|
731
|
+
const entries = readdirSync3(packageDir, { withFileTypes: true });
|
|
732
|
+
for (const entry of entries) {
|
|
733
|
+
const targetPath = `${HOME_DIR}/${entry.name}`;
|
|
734
|
+
if (!existsSync3(targetPath))
|
|
735
|
+
return false;
|
|
736
|
+
try {
|
|
737
|
+
const stat = lstatSync2(targetPath);
|
|
738
|
+
if (!stat.isSymbolicLink())
|
|
739
|
+
continue;
|
|
740
|
+
const linkTarget = readlinkSync2(targetPath);
|
|
741
|
+
if (!linkTarget.includes(packageDir))
|
|
742
|
+
return false;
|
|
743
|
+
} catch {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
async function stowPackage(packageName) {
|
|
750
|
+
const result = await exec(["stow", "-v", "--target", HOME_DIR, packageName], CONFIGS_DIR);
|
|
751
|
+
return {
|
|
752
|
+
success: result.success,
|
|
753
|
+
package: packageName,
|
|
754
|
+
message: result.success ? `${packageName} stowed successfully` : result.stderr
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
async function unstowPackage(packageName) {
|
|
758
|
+
const result = await exec(["stow", "-v", "--delete", "--target", HOME_DIR, packageName], CONFIGS_DIR);
|
|
759
|
+
return {
|
|
760
|
+
success: result.success,
|
|
761
|
+
package: packageName,
|
|
762
|
+
message: result.success ? `${packageName} unstowed successfully` : result.stderr
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
async function restowPackage(packageName) {
|
|
766
|
+
const result = await exec(["stow", "-v", "--restow", "--target", HOME_DIR, packageName], CONFIGS_DIR);
|
|
767
|
+
return {
|
|
768
|
+
success: result.success,
|
|
769
|
+
package: packageName,
|
|
770
|
+
message: result.success ? `${packageName} restowed successfully` : result.stderr
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
async function adoptPackage(packageName) {
|
|
774
|
+
const result = await exec(["stow", "-v", "--adopt", "--target", HOME_DIR, packageName], CONFIGS_DIR);
|
|
775
|
+
return {
|
|
776
|
+
success: result.success,
|
|
777
|
+
package: packageName,
|
|
778
|
+
message: result.success ? `${packageName} adopted successfully` : result.stderr
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
async function showStatus() {
|
|
782
|
+
const packages = listPackages();
|
|
783
|
+
console.log(`
|
|
784
|
+
${colors2.cyan}Package Status:${colors2.reset}`);
|
|
785
|
+
console.log("─".repeat(40));
|
|
786
|
+
for (const pkg of packages) {
|
|
787
|
+
const status = pkg.isStowed ? `${colors2.green}✓ stowed${colors2.reset}` : `${colors2.yellow}○ not stowed${colors2.reset}`;
|
|
788
|
+
console.log(` ${pkg.name}: ${status}`);
|
|
789
|
+
}
|
|
790
|
+
console.log();
|
|
791
|
+
}
|
|
792
|
+
async function stowAll() {
|
|
793
|
+
const packages = listPackages();
|
|
794
|
+
console.log(`
|
|
795
|
+
${colors2.cyan}Stowing all packages...${colors2.reset}
|
|
796
|
+
`);
|
|
797
|
+
for (const pkg of packages) {
|
|
798
|
+
const result = await stowPackage(pkg.name);
|
|
799
|
+
const icon = result.success ? colors2.green + "✓" : colors2.red + "✗";
|
|
800
|
+
console.log(` ${icon} ${result.message}${colors2.reset}`);
|
|
801
|
+
}
|
|
802
|
+
console.log();
|
|
803
|
+
}
|
|
804
|
+
async function unstowAll() {
|
|
805
|
+
const packages = listPackages();
|
|
806
|
+
console.log(`
|
|
807
|
+
${colors2.cyan}Unstowing all packages...${colors2.reset}
|
|
808
|
+
`);
|
|
809
|
+
for (const pkg of packages) {
|
|
810
|
+
const result = await unstowPackage(pkg.name);
|
|
811
|
+
const icon = result.success ? colors2.green + "✓" : colors2.red + "✗";
|
|
812
|
+
console.log(` ${icon} ${result.message}${colors2.reset}`);
|
|
813
|
+
}
|
|
814
|
+
console.log();
|
|
815
|
+
}
|
|
816
|
+
function printUsage() {
|
|
817
|
+
console.log(`
|
|
818
|
+
${colors2.cyan}Usage: bun run config <command> [package]${colors2.reset}
|
|
819
|
+
|
|
820
|
+
Commands:
|
|
821
|
+
${colors2.blue}stow${colors2.reset} <package> Stow a package
|
|
822
|
+
${colors2.blue}unstow${colors2.reset} <package> Unstow a package
|
|
823
|
+
${colors2.blue}restow${colors2.reset} <package> Restow a package
|
|
824
|
+
${colors2.blue}adopt${colors2.reset} <package> Adopt existing config into package
|
|
825
|
+
${colors2.blue}list${colors2.reset} List all packages
|
|
826
|
+
${colors2.blue}status${colors2.reset} Show status of all packages
|
|
827
|
+
${colors2.blue}stow-all${colors2.reset} Stow all packages
|
|
828
|
+
${colors2.blue}unstow-all${colors2.reset} Unstow all packages
|
|
829
|
+
`);
|
|
830
|
+
}
|
|
831
|
+
async function runConfigManager(args) {
|
|
832
|
+
const { positionals } = parseArgs({
|
|
833
|
+
args,
|
|
834
|
+
allowPositionals: true
|
|
835
|
+
});
|
|
836
|
+
try {
|
|
837
|
+
await checkStow();
|
|
838
|
+
} catch {
|
|
839
|
+
return { output: "GNU Stow is not installed", success: false };
|
|
840
|
+
}
|
|
841
|
+
const [command, packageName] = positionals;
|
|
842
|
+
let output = "";
|
|
843
|
+
let success = true;
|
|
844
|
+
switch (command) {
|
|
845
|
+
case "stow": {
|
|
846
|
+
if (!packageName) {
|
|
847
|
+
return { output: "Package name required", success: false };
|
|
848
|
+
}
|
|
849
|
+
const result = await stowPackage(packageName);
|
|
850
|
+
output = result.message;
|
|
851
|
+
success = result.success;
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
case "unstow": {
|
|
855
|
+
if (!packageName) {
|
|
856
|
+
return { output: "Package name required", success: false };
|
|
857
|
+
}
|
|
858
|
+
const result = await unstowPackage(packageName);
|
|
859
|
+
output = result.message;
|
|
860
|
+
success = result.success;
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
case "restow": {
|
|
864
|
+
if (!packageName) {
|
|
865
|
+
return { output: "Package name required", success: false };
|
|
866
|
+
}
|
|
867
|
+
const result = await restowPackage(packageName);
|
|
868
|
+
output = result.message;
|
|
869
|
+
success = result.success;
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
case "adopt": {
|
|
873
|
+
if (!packageName) {
|
|
874
|
+
return { output: "Package name required", success: false };
|
|
875
|
+
}
|
|
876
|
+
const result = await adoptPackage(packageName);
|
|
877
|
+
output = result.message;
|
|
878
|
+
success = result.success;
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
case "list": {
|
|
882
|
+
const packages = listPackages();
|
|
883
|
+
output = packages.map((p) => p.name).join(`
|
|
884
|
+
`);
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
case "status": {
|
|
888
|
+
const packages = listPackages();
|
|
889
|
+
output = packages.map((p) => `${p.name}: ${p.isStowed ? "stowed" : "not stowed"}`).join(`
|
|
890
|
+
`);
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
case "stow-all": {
|
|
894
|
+
const packages = listPackages();
|
|
895
|
+
const results = [];
|
|
896
|
+
for (const pkg of packages) {
|
|
897
|
+
const result = await stowPackage(pkg.name);
|
|
898
|
+
results.push(`${result.success ? "✓" : "✗"} ${result.message}`);
|
|
899
|
+
if (!result.success)
|
|
900
|
+
success = false;
|
|
901
|
+
}
|
|
902
|
+
output = results.join(`
|
|
903
|
+
`);
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
case "unstow-all": {
|
|
907
|
+
const packages = listPackages();
|
|
908
|
+
const results = [];
|
|
909
|
+
for (const pkg of packages) {
|
|
910
|
+
const result = await unstowPackage(pkg.name);
|
|
911
|
+
results.push(`${result.success ? "✓" : "✗"} ${result.message}`);
|
|
912
|
+
if (!result.success)
|
|
913
|
+
success = false;
|
|
914
|
+
}
|
|
915
|
+
output = results.join(`
|
|
916
|
+
`);
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
default:
|
|
920
|
+
output = "Unknown command";
|
|
921
|
+
success = false;
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
return { output, success };
|
|
925
|
+
}
|
|
926
|
+
async function main() {
|
|
927
|
+
const { positionals } = parseArgs({
|
|
928
|
+
args: process.argv.slice(2),
|
|
929
|
+
allowPositionals: true
|
|
930
|
+
});
|
|
931
|
+
await checkStow();
|
|
932
|
+
const [command, packageName] = positionals;
|
|
933
|
+
switch (command) {
|
|
934
|
+
case "stow": {
|
|
935
|
+
if (!packageName) {
|
|
936
|
+
console.error(`${colors2.red}Error: Package name required${colors2.reset}`);
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
const result = await stowPackage(packageName);
|
|
940
|
+
console.log(result.success ? `${colors2.green}${result.message}${colors2.reset}` : `${colors2.red}${result.message}${colors2.reset}`);
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
case "unstow": {
|
|
944
|
+
if (!packageName) {
|
|
945
|
+
console.error(`${colors2.red}Error: Package name required${colors2.reset}`);
|
|
946
|
+
process.exit(1);
|
|
947
|
+
}
|
|
948
|
+
const result = await unstowPackage(packageName);
|
|
949
|
+
console.log(result.success ? `${colors2.green}${result.message}${colors2.reset}` : `${colors2.red}${result.message}${colors2.reset}`);
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
case "restow": {
|
|
953
|
+
if (!packageName) {
|
|
954
|
+
console.error(`${colors2.red}Error: Package name required${colors2.reset}`);
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
const result = await restowPackage(packageName);
|
|
958
|
+
console.log(result.success ? `${colors2.green}${result.message}${colors2.reset}` : `${colors2.red}${result.message}${colors2.reset}`);
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
case "adopt": {
|
|
962
|
+
if (!packageName) {
|
|
963
|
+
console.error(`${colors2.red}Error: Package name required${colors2.reset}`);
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
const result = await adoptPackage(packageName);
|
|
967
|
+
console.log(result.success ? `${colors2.green}${result.message}${colors2.reset}` : `${colors2.red}${result.message}${colors2.reset}`);
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
case "list": {
|
|
971
|
+
const packages = listPackages();
|
|
972
|
+
console.log(`
|
|
973
|
+
${colors2.cyan}Available packages:${colors2.reset}`);
|
|
974
|
+
for (const pkg of packages) {
|
|
975
|
+
console.log(` ${colors2.blue}•${colors2.reset} ${pkg.name}`);
|
|
976
|
+
}
|
|
977
|
+
console.log();
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
case "status":
|
|
981
|
+
await showStatus();
|
|
982
|
+
break;
|
|
983
|
+
case "stow-all":
|
|
984
|
+
await stowAll();
|
|
985
|
+
break;
|
|
986
|
+
case "unstow-all":
|
|
987
|
+
await unstowAll();
|
|
988
|
+
break;
|
|
989
|
+
default:
|
|
990
|
+
printUsage();
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
var isMainModule = process.argv[1]?.includes("config-manager");
|
|
995
|
+
if (isMainModule) {
|
|
996
|
+
main().catch(console.error);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/cli/pkg-sync.ts
|
|
1000
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
1001
|
+
|
|
1002
|
+
// src/lib/config.ts
|
|
1003
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1004
|
+
var DEFAULT_CONFIG = {
|
|
1005
|
+
config: {
|
|
1006
|
+
purge: false,
|
|
1007
|
+
purgeInteractive: true,
|
|
1008
|
+
autoUpdate: true
|
|
1009
|
+
},
|
|
1010
|
+
taps: [],
|
|
1011
|
+
packages: [],
|
|
1012
|
+
casks: [],
|
|
1013
|
+
mas: {}
|
|
1014
|
+
};
|
|
1015
|
+
async function loadPkgConfig(path) {
|
|
1016
|
+
await ensureConfigDir();
|
|
1017
|
+
const configPath = path || PKG_CONFIG_PATH;
|
|
1018
|
+
if (!existsSync4(configPath)) {
|
|
1019
|
+
await savePkgConfig(DEFAULT_CONFIG, configPath);
|
|
1020
|
+
return DEFAULT_CONFIG;
|
|
1021
|
+
}
|
|
1022
|
+
return readJson(configPath);
|
|
1023
|
+
}
|
|
1024
|
+
async function savePkgConfig(config, path) {
|
|
1025
|
+
await ensureConfigDir();
|
|
1026
|
+
const configPath = path || PKG_CONFIG_PATH;
|
|
1027
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
1028
|
+
}
|
|
1029
|
+
async function loadPkgLock() {
|
|
1030
|
+
if (!existsSync4(PKG_LOCK_PATH)) {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
return readJson(PKG_LOCK_PATH);
|
|
1034
|
+
}
|
|
1035
|
+
async function savePkgLock(lock) {
|
|
1036
|
+
await ensureConfigDir();
|
|
1037
|
+
await writeFile(PKG_LOCK_PATH, JSON.stringify(lock, null, 2));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/lib/lockfile.ts
|
|
1041
|
+
async function fetchInstalledVersions() {
|
|
1042
|
+
const config = await loadPkgConfig();
|
|
1043
|
+
const now = new Date().toISOString();
|
|
1044
|
+
const formulas = {};
|
|
1045
|
+
const casks = {};
|
|
1046
|
+
if (config.packages.length > 0) {
|
|
1047
|
+
const result = await exec([
|
|
1048
|
+
"brew",
|
|
1049
|
+
"info",
|
|
1050
|
+
"--json=v2",
|
|
1051
|
+
...config.packages
|
|
1052
|
+
]);
|
|
1053
|
+
if (result.success && result.stdout) {
|
|
1054
|
+
const info = JSON.parse(result.stdout);
|
|
1055
|
+
for (const formula of info.formulae) {
|
|
1056
|
+
if (formula.installed.length > 0) {
|
|
1057
|
+
formulas[formula.name] = {
|
|
1058
|
+
version: formula.installed[0].version,
|
|
1059
|
+
tap: formula.tap,
|
|
1060
|
+
installedAt: now
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (config.casks.length > 0) {
|
|
1067
|
+
const result = await exec([
|
|
1068
|
+
"brew",
|
|
1069
|
+
"info",
|
|
1070
|
+
"--json=v2",
|
|
1071
|
+
"--cask",
|
|
1072
|
+
...config.casks
|
|
1073
|
+
]);
|
|
1074
|
+
if (result.success && result.stdout) {
|
|
1075
|
+
const info = JSON.parse(result.stdout);
|
|
1076
|
+
for (const cask of info.casks) {
|
|
1077
|
+
if (cask.installed) {
|
|
1078
|
+
casks[cask.token] = {
|
|
1079
|
+
version: cask.installed,
|
|
1080
|
+
installedAt: now
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return { formulas, casks };
|
|
1087
|
+
}
|
|
1088
|
+
async function generateLockfile() {
|
|
1089
|
+
const { formulas, casks } = await fetchInstalledVersions();
|
|
1090
|
+
return {
|
|
1091
|
+
version: 1,
|
|
1092
|
+
lastUpdated: new Date().toISOString(),
|
|
1093
|
+
formulas,
|
|
1094
|
+
casks
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
async function updateLockfile() {
|
|
1098
|
+
const existing = await loadPkgLock();
|
|
1099
|
+
const { formulas, casks } = await fetchInstalledVersions();
|
|
1100
|
+
const mergedFormulas = {};
|
|
1101
|
+
for (const [name, info] of Object.entries(formulas)) {
|
|
1102
|
+
const prev = existing?.formulas[name];
|
|
1103
|
+
if (prev && prev.version === info.version) {
|
|
1104
|
+
mergedFormulas[name] = prev;
|
|
1105
|
+
} else {
|
|
1106
|
+
mergedFormulas[name] = info;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
const mergedCasks = {};
|
|
1110
|
+
for (const [name, info] of Object.entries(casks)) {
|
|
1111
|
+
const prev = existing?.casks[name];
|
|
1112
|
+
if (prev && prev.version === info.version) {
|
|
1113
|
+
mergedCasks[name] = prev;
|
|
1114
|
+
} else {
|
|
1115
|
+
mergedCasks[name] = info;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const lock = {
|
|
1119
|
+
version: 1,
|
|
1120
|
+
lastUpdated: new Date().toISOString(),
|
|
1121
|
+
formulas: mergedFormulas,
|
|
1122
|
+
casks: mergedCasks
|
|
1123
|
+
};
|
|
1124
|
+
await savePkgLock(lock);
|
|
1125
|
+
return lock;
|
|
1126
|
+
}
|
|
1127
|
+
async function getChangedPackages() {
|
|
1128
|
+
const existing = await loadPkgLock();
|
|
1129
|
+
const { formulas, casks } = await fetchInstalledVersions();
|
|
1130
|
+
const added = [];
|
|
1131
|
+
const removed = [];
|
|
1132
|
+
const upgraded = [];
|
|
1133
|
+
if (!existing) {
|
|
1134
|
+
added.push(...Object.keys(formulas), ...Object.keys(casks));
|
|
1135
|
+
return { added, removed, upgraded };
|
|
1136
|
+
}
|
|
1137
|
+
for (const [name, info] of Object.entries(formulas)) {
|
|
1138
|
+
if (!existing.formulas[name]) {
|
|
1139
|
+
added.push(name);
|
|
1140
|
+
} else if (existing.formulas[name].version !== info.version) {
|
|
1141
|
+
upgraded.push({
|
|
1142
|
+
name,
|
|
1143
|
+
from: existing.formulas[name].version,
|
|
1144
|
+
to: info.version
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
for (const name of Object.keys(existing.formulas)) {
|
|
1149
|
+
if (!formulas[name]) {
|
|
1150
|
+
removed.push(name);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
for (const [name, info] of Object.entries(casks)) {
|
|
1154
|
+
if (!existing.casks[name]) {
|
|
1155
|
+
added.push(name);
|
|
1156
|
+
} else if (existing.casks[name].version !== info.version) {
|
|
1157
|
+
upgraded.push({
|
|
1158
|
+
name,
|
|
1159
|
+
from: existing.casks[name].version,
|
|
1160
|
+
to: info.version
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
for (const name of Object.keys(existing.casks)) {
|
|
1165
|
+
if (!casks[name]) {
|
|
1166
|
+
removed.push(name);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return { added, removed, upgraded };
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/types/pkg-config.ts
|
|
1173
|
+
var SYSTEM_APP_IDS = [
|
|
1174
|
+
409183694,
|
|
1175
|
+
409203825,
|
|
1176
|
+
409201541,
|
|
1177
|
+
408981434,
|
|
1178
|
+
682658836,
|
|
1179
|
+
424389933,
|
|
1180
|
+
424390742,
|
|
1181
|
+
413897608,
|
|
1182
|
+
1274495053,
|
|
1183
|
+
425424353,
|
|
1184
|
+
497799835,
|
|
1185
|
+
634148309,
|
|
1186
|
+
1480068668,
|
|
1187
|
+
803453959,
|
|
1188
|
+
1295203466,
|
|
1189
|
+
1444383602,
|
|
1190
|
+
640199958,
|
|
1191
|
+
899247664,
|
|
1192
|
+
1176895641,
|
|
1193
|
+
1451685025
|
|
1194
|
+
];
|
|
1195
|
+
|
|
1196
|
+
// src/cli/pkg-sync.ts
|
|
1197
|
+
var colors3 = {
|
|
1198
|
+
red: "\x1B[0;31m",
|
|
1199
|
+
green: "\x1B[0;32m",
|
|
1200
|
+
blue: "\x1B[0;34m",
|
|
1201
|
+
yellow: "\x1B[1;33m",
|
|
1202
|
+
cyan: "\x1B[0;36m",
|
|
1203
|
+
bold: "\x1B[1m",
|
|
1204
|
+
reset: "\x1B[0m"
|
|
1205
|
+
};
|
|
1206
|
+
async function checkDependencies() {
|
|
1207
|
+
if (!await commandExists("brew")) {
|
|
1208
|
+
console.error(`${colors3.red}Error: Homebrew not installed${colors3.reset}`);
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async function getOutdatedPackages() {
|
|
1213
|
+
const [formulas, casks] = await Promise.all([
|
|
1214
|
+
exec(["brew", "outdated", "--formula", "--quiet"]),
|
|
1215
|
+
exec(["brew", "outdated", "--cask", "--quiet"])
|
|
1216
|
+
]);
|
|
1217
|
+
const packages = [];
|
|
1218
|
+
if (formulas.stdout) {
|
|
1219
|
+
packages.push(...formulas.stdout.split(`
|
|
1220
|
+
`).filter(Boolean).map((name) => ({ name, type: "formula" })));
|
|
1221
|
+
}
|
|
1222
|
+
if (casks.stdout) {
|
|
1223
|
+
packages.push(...casks.stdout.split(`
|
|
1224
|
+
`).filter(Boolean).map((name) => ({ name, type: "cask" })));
|
|
1225
|
+
}
|
|
1226
|
+
return packages;
|
|
1227
|
+
}
|
|
1228
|
+
async function getOutdatedMas() {
|
|
1229
|
+
const result = await exec(["mas", "outdated"]);
|
|
1230
|
+
if (!result.success || !result.stdout)
|
|
1231
|
+
return [];
|
|
1232
|
+
return result.stdout.split(`
|
|
1233
|
+
`).filter(Boolean).map((line) => {
|
|
1234
|
+
const match = line.match(/^(\d+)\s+(.+?)(?:\s+\(|$)/);
|
|
1235
|
+
if (match) {
|
|
1236
|
+
return { id: parseInt(match[1], 10), name: match[2].trim() };
|
|
1237
|
+
}
|
|
1238
|
+
return null;
|
|
1239
|
+
}).filter((app) => app !== null);
|
|
1240
|
+
}
|
|
1241
|
+
async function upgradeWithVerification() {
|
|
1242
|
+
const result = {
|
|
1243
|
+
attempted: [],
|
|
1244
|
+
succeeded: [],
|
|
1245
|
+
failed: [],
|
|
1246
|
+
stillOutdated: []
|
|
1247
|
+
};
|
|
1248
|
+
console.log(`
|
|
1249
|
+
${colors3.cyan}=== Checking for updates ===${colors3.reset}
|
|
1250
|
+
`);
|
|
1251
|
+
await execLive(["brew", "update"]);
|
|
1252
|
+
const beforeUpgrade = await getOutdatedPackages();
|
|
1253
|
+
result.attempted = beforeUpgrade.map((p) => p.name);
|
|
1254
|
+
if (beforeUpgrade.length === 0) {
|
|
1255
|
+
console.log(`
|
|
1256
|
+
${colors3.green}All brew packages are up to date${colors3.reset}`);
|
|
1257
|
+
} else {
|
|
1258
|
+
console.log(`
|
|
1259
|
+
${colors3.yellow}Found ${beforeUpgrade.length} outdated packages${colors3.reset}
|
|
1260
|
+
`);
|
|
1261
|
+
console.log(`${colors3.cyan}=== Upgrading formulas ===${colors3.reset}
|
|
1262
|
+
`);
|
|
1263
|
+
await execLive(["brew", "upgrade", "--formula"]);
|
|
1264
|
+
console.log(`
|
|
1265
|
+
${colors3.cyan}=== Upgrading casks ===${colors3.reset}
|
|
1266
|
+
`);
|
|
1267
|
+
await execLive(["brew", "upgrade", "--cask", "--greedy"]);
|
|
1268
|
+
console.log(`
|
|
1269
|
+
${colors3.cyan}=== Verifying upgrades ===${colors3.reset}
|
|
1270
|
+
`);
|
|
1271
|
+
const afterUpgrade = await getOutdatedPackages();
|
|
1272
|
+
const stillOutdatedSet = new Set(afterUpgrade.map((p) => p.name));
|
|
1273
|
+
for (const pkg of beforeUpgrade) {
|
|
1274
|
+
if (stillOutdatedSet.has(pkg.name)) {
|
|
1275
|
+
result.stillOutdated.push(pkg.name);
|
|
1276
|
+
} else {
|
|
1277
|
+
result.succeeded.push(pkg.name);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (result.stillOutdated.length > 0) {
|
|
1281
|
+
console.log(`${colors3.yellow}${result.stillOutdated.length} packages still outdated, retrying individually...${colors3.reset}
|
|
1282
|
+
`);
|
|
1283
|
+
for (const pkgName of [...result.stillOutdated]) {
|
|
1284
|
+
const pkg = afterUpgrade.find((p) => p.name === pkgName);
|
|
1285
|
+
if (!pkg)
|
|
1286
|
+
continue;
|
|
1287
|
+
console.log(` Retrying ${colors3.blue}${pkgName}${colors3.reset}...`);
|
|
1288
|
+
const upgradeCmd = pkg.type === "cask" ? ["brew", "upgrade", "--cask", pkgName] : ["brew", "upgrade", pkgName];
|
|
1289
|
+
const retryResult = await exec(upgradeCmd);
|
|
1290
|
+
const checkResult = await exec([
|
|
1291
|
+
"brew",
|
|
1292
|
+
"outdated",
|
|
1293
|
+
pkg.type === "cask" ? "--cask" : "--formula",
|
|
1294
|
+
"--quiet"
|
|
1295
|
+
]);
|
|
1296
|
+
const stillOutdatedNow = checkResult.stdout.split(`
|
|
1297
|
+
`).filter(Boolean);
|
|
1298
|
+
if (!stillOutdatedNow.includes(pkgName)) {
|
|
1299
|
+
result.succeeded.push(pkgName);
|
|
1300
|
+
result.stillOutdated = result.stillOutdated.filter((n) => n !== pkgName);
|
|
1301
|
+
console.log(` ${colors3.green}✓ Success${colors3.reset}`);
|
|
1302
|
+
} else {
|
|
1303
|
+
result.failed.push(pkgName);
|
|
1304
|
+
result.stillOutdated = result.stillOutdated.filter((n) => n !== pkgName);
|
|
1305
|
+
console.log(` ${colors3.red}✗ Failed${colors3.reset} ${retryResult.stderr ? `(${retryResult.stderr.split(`
|
|
1306
|
+
`)[0]})` : ""}`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
if (await commandExists("mas")) {
|
|
1312
|
+
const masOutdated = await getOutdatedMas();
|
|
1313
|
+
if (masOutdated.length > 0) {
|
|
1314
|
+
console.log(`
|
|
1315
|
+
${colors3.cyan}=== Upgrading Mac App Store apps ===${colors3.reset}
|
|
1316
|
+
`);
|
|
1317
|
+
await execLive(["mas", "upgrade"]);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
console.log(`
|
|
1321
|
+
${colors3.cyan}=== Cleanup ===${colors3.reset}
|
|
1322
|
+
`);
|
|
1323
|
+
await execLive(["brew", "cleanup"]);
|
|
1324
|
+
console.log(`
|
|
1325
|
+
${colors3.cyan}=== Updating lockfile ===${colors3.reset}
|
|
1326
|
+
`);
|
|
1327
|
+
const lock = await updateLockfile();
|
|
1328
|
+
const lockTotal = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
|
|
1329
|
+
console.log(` Locked ${lockTotal} packages`);
|
|
1330
|
+
return result;
|
|
1331
|
+
}
|
|
1332
|
+
async function upgradeInteractive() {
|
|
1333
|
+
console.log(`
|
|
1334
|
+
${colors3.cyan}=== Checking for updates ===${colors3.reset}
|
|
1335
|
+
`);
|
|
1336
|
+
await execLive(["brew", "update"]);
|
|
1337
|
+
const outdated = await getOutdatedPackages();
|
|
1338
|
+
if (outdated.length === 0) {
|
|
1339
|
+
console.log(`
|
|
1340
|
+
${colors3.green}All packages are up to date${colors3.reset}
|
|
1341
|
+
`);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
console.log(`
|
|
1345
|
+
${colors3.yellow}Found ${outdated.length} outdated packages${colors3.reset}
|
|
1346
|
+
`);
|
|
1347
|
+
for (const pkg of outdated) {
|
|
1348
|
+
const question = `Upgrade ${colors3.blue}${pkg.name}${colors3.reset} (${pkg.type})? [y/n/q]: `;
|
|
1349
|
+
const answer = (prompt(question) || "").trim().toLowerCase();
|
|
1350
|
+
if (answer === "q") {
|
|
1351
|
+
console.log(`
|
|
1352
|
+
${colors3.yellow}Upgrade cancelled${colors3.reset}`);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (answer === "y" || answer === "yes") {
|
|
1356
|
+
const cmd = pkg.type === "cask" ? ["brew", "upgrade", "--cask", pkg.name] : ["brew", "upgrade", pkg.name];
|
|
1357
|
+
await execLive(cmd);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const stillOutdated = await getOutdatedPackages();
|
|
1361
|
+
if (stillOutdated.length > 0) {
|
|
1362
|
+
console.log(`
|
|
1363
|
+
${colors3.yellow}Still outdated: ${stillOutdated.map((p) => p.name).join(", ")}${colors3.reset}`);
|
|
1364
|
+
} else {
|
|
1365
|
+
console.log(`
|
|
1366
|
+
${colors3.green}All selected packages upgraded successfully${colors3.reset}`);
|
|
1367
|
+
}
|
|
1368
|
+
await execLive(["brew", "cleanup"]);
|
|
1369
|
+
console.log(`
|
|
1370
|
+
${colors3.cyan}=== Updating lockfile ===${colors3.reset}
|
|
1371
|
+
`);
|
|
1372
|
+
await updateLockfile();
|
|
1373
|
+
}
|
|
1374
|
+
async function syncPackages(config) {
|
|
1375
|
+
if (config.config.autoUpdate) {
|
|
1376
|
+
console.log(`
|
|
1377
|
+
${colors3.cyan}=== Updating Homebrew ===${colors3.reset}
|
|
1378
|
+
`);
|
|
1379
|
+
await execLive(["brew", "update"]);
|
|
1380
|
+
}
|
|
1381
|
+
console.log(`
|
|
1382
|
+
${colors3.cyan}=== Installing taps ===${colors3.reset}
|
|
1383
|
+
`);
|
|
1384
|
+
const tappedResult = await exec(["brew", "tap"]);
|
|
1385
|
+
const tapped = tappedResult.stdout.split(`
|
|
1386
|
+
`).filter(Boolean);
|
|
1387
|
+
for (const tap of config.taps) {
|
|
1388
|
+
if (!tapped.includes(tap)) {
|
|
1389
|
+
console.log(` Adding tap: ${colors3.blue}${tap}${colors3.reset}`);
|
|
1390
|
+
await execLive(["brew", "tap", tap]);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
console.log(`
|
|
1394
|
+
${colors3.cyan}=== Installing packages ===${colors3.reset}
|
|
1395
|
+
`);
|
|
1396
|
+
const installedFormulas = (await exec(["brew", "list", "--formula"])).stdout.split(`
|
|
1397
|
+
`).filter(Boolean);
|
|
1398
|
+
for (const pkg of config.packages) {
|
|
1399
|
+
if (!installedFormulas.includes(pkg)) {
|
|
1400
|
+
console.log(` Installing: ${colors3.blue}${pkg}${colors3.reset}`);
|
|
1401
|
+
await execLive(["brew", "install", pkg]);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
console.log(`
|
|
1405
|
+
${colors3.cyan}=== Installing casks ===${colors3.reset}
|
|
1406
|
+
`);
|
|
1407
|
+
const installedCasks = (await exec(["brew", "list", "--cask"])).stdout.split(`
|
|
1408
|
+
`).filter(Boolean);
|
|
1409
|
+
for (const cask of config.casks) {
|
|
1410
|
+
if (!installedCasks.includes(cask)) {
|
|
1411
|
+
console.log(` Installing: ${colors3.blue}${cask}${colors3.reset}`);
|
|
1412
|
+
await execLive(["brew", "install", "--cask", cask]);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (await commandExists("mas")) {
|
|
1416
|
+
console.log(`
|
|
1417
|
+
${colors3.cyan}=== Installing Mac App Store apps ===${colors3.reset}
|
|
1418
|
+
`);
|
|
1419
|
+
const masResult = await exec(["mas", "list"]);
|
|
1420
|
+
const installedMas = masResult.stdout.split(`
|
|
1421
|
+
`).filter(Boolean).map((line) => {
|
|
1422
|
+
const match = line.match(/^(\d+)/);
|
|
1423
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
1424
|
+
});
|
|
1425
|
+
for (const [name, id] of Object.entries(config.mas)) {
|
|
1426
|
+
if (!installedMas.includes(id)) {
|
|
1427
|
+
console.log(` Installing: ${colors3.blue}${name}${colors3.reset}`);
|
|
1428
|
+
await execLive(["mas", "install", String(id)]);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (config.config.purge) {
|
|
1433
|
+
await purgeUnlisted(config, config.config.purgeInteractive);
|
|
1434
|
+
}
|
|
1435
|
+
console.log(`
|
|
1436
|
+
${colors3.cyan}=== Updating lockfile ===${colors3.reset}
|
|
1437
|
+
`);
|
|
1438
|
+
const lock = await updateLockfile();
|
|
1439
|
+
const lockTotal = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
|
|
1440
|
+
console.log(` Locked ${lockTotal} packages`);
|
|
1441
|
+
console.log(`
|
|
1442
|
+
${colors3.green}=== Sync complete ===${colors3.reset}
|
|
1443
|
+
`);
|
|
1444
|
+
}
|
|
1445
|
+
async function purgeUnlisted(config, interactive) {
|
|
1446
|
+
console.log(`
|
|
1447
|
+
${colors3.cyan}=== Checking for unlisted packages ===${colors3.reset}
|
|
1448
|
+
`);
|
|
1449
|
+
const installedFormulas = (await exec(["brew", "list", "--formula"])).stdout.split(`
|
|
1450
|
+
`).filter(Boolean);
|
|
1451
|
+
for (const pkg of installedFormulas) {
|
|
1452
|
+
if (!config.packages.includes(pkg)) {
|
|
1453
|
+
const usesResult = await exec(["brew", "uses", "--installed", pkg]);
|
|
1454
|
+
if (usesResult.stdout.trim()) {
|
|
1455
|
+
console.log(` ${colors3.yellow}Skipping ${pkg} (has dependents)${colors3.reset}`);
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
if (interactive) {
|
|
1459
|
+
const answer = (prompt(` Remove ${colors3.red}${pkg}${colors3.reset}? [y/n]: `) || "").trim().toLowerCase();
|
|
1460
|
+
if (answer === "y") {
|
|
1461
|
+
await execLive(["brew", "uninstall", pkg]);
|
|
1462
|
+
}
|
|
1463
|
+
} else {
|
|
1464
|
+
console.log(` Removing: ${colors3.red}${pkg}${colors3.reset}`);
|
|
1465
|
+
await execLive(["brew", "uninstall", pkg]);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const installedCasks = (await exec(["brew", "list", "--cask"])).stdout.split(`
|
|
1470
|
+
`).filter(Boolean);
|
|
1471
|
+
for (const cask of installedCasks) {
|
|
1472
|
+
if (!config.casks.includes(cask)) {
|
|
1473
|
+
if (interactive) {
|
|
1474
|
+
const answer = (prompt(` Remove cask ${colors3.red}${cask}${colors3.reset}? [y/n]: `) || "").trim().toLowerCase();
|
|
1475
|
+
if (answer === "y") {
|
|
1476
|
+
await execLive(["brew", "uninstall", "--cask", cask]);
|
|
1477
|
+
}
|
|
1478
|
+
} else {
|
|
1479
|
+
console.log(` Removing cask: ${colors3.red}${cask}${colors3.reset}`);
|
|
1480
|
+
await execLive(["brew", "uninstall", "--cask", cask]);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (await commandExists("mas")) {
|
|
1485
|
+
const masResult = await exec(["mas", "list"]);
|
|
1486
|
+
const installedMas = masResult.stdout.split(`
|
|
1487
|
+
`).filter(Boolean).map((line) => {
|
|
1488
|
+
const match = line.match(/^(\d+)\s+(.+?)(?:\s+\(|$)/);
|
|
1489
|
+
return match ? { id: parseInt(match[1], 10), name: match[2].trim() } : null;
|
|
1490
|
+
}).filter((app) => app !== null);
|
|
1491
|
+
const configMasIds = Object.values(config.mas);
|
|
1492
|
+
for (const app of installedMas) {
|
|
1493
|
+
if (SYSTEM_APP_IDS.includes(app.id)) {
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if (!configMasIds.includes(app.id)) {
|
|
1497
|
+
if (interactive) {
|
|
1498
|
+
const answer = (prompt(` Remove app ${colors3.red}${app.name}${colors3.reset}? [y/n]: `) || "").trim().toLowerCase();
|
|
1499
|
+
if (answer === "y") {
|
|
1500
|
+
await execLive(["mas", "uninstall", String(app.id)]);
|
|
1501
|
+
}
|
|
1502
|
+
} else {
|
|
1503
|
+
console.log(` Removing app: ${colors3.red}${app.name}${colors3.reset}`);
|
|
1504
|
+
await execLive(["mas", "uninstall", String(app.id)]);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
console.log(`
|
|
1510
|
+
${colors3.cyan}=== Cleaning up ===${colors3.reset}
|
|
1511
|
+
`);
|
|
1512
|
+
await execLive(["brew", "autoremove"]);
|
|
1513
|
+
await execLive(["brew", "cleanup"]);
|
|
1514
|
+
}
|
|
1515
|
+
function printUsage2() {
|
|
1516
|
+
console.log(`
|
|
1517
|
+
${colors3.cyan}Usage: bun run pkg-sync [options] [config-file]${colors3.reset}
|
|
1518
|
+
|
|
1519
|
+
Options:
|
|
1520
|
+
${colors3.blue}--upgrade-only${colors3.reset} Only upgrade existing packages (with verification)
|
|
1521
|
+
${colors3.blue}--upgrade-interactive${colors3.reset} Interactively select packages to upgrade
|
|
1522
|
+
${colors3.blue}--purge${colors3.reset} Force purge mode for this run
|
|
1523
|
+
|
|
1524
|
+
Examples:
|
|
1525
|
+
bun run pkg-sync Sync packages from pkg-config.json
|
|
1526
|
+
bun run pkg-sync --upgrade-only Upgrade all packages with verification
|
|
1527
|
+
bun run pkg-sync --purge Sync and remove unlisted packages
|
|
1528
|
+
`);
|
|
1529
|
+
}
|
|
1530
|
+
async function runPkgSync(args) {
|
|
1531
|
+
const { values, positionals } = parseArgs2({
|
|
1532
|
+
args,
|
|
1533
|
+
options: {
|
|
1534
|
+
"upgrade-only": { type: "boolean", default: false },
|
|
1535
|
+
"upgrade-interactive": { type: "boolean", default: false },
|
|
1536
|
+
purge: { type: "boolean", default: false },
|
|
1537
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1538
|
+
},
|
|
1539
|
+
allowPositionals: true
|
|
1540
|
+
});
|
|
1541
|
+
try {
|
|
1542
|
+
await checkDependencies();
|
|
1543
|
+
} catch {
|
|
1544
|
+
return { output: "Homebrew not installed", success: false };
|
|
1545
|
+
}
|
|
1546
|
+
if (values["upgrade-only"]) {
|
|
1547
|
+
const result = await upgradeWithVerification();
|
|
1548
|
+
let output = `Upgrade complete
|
|
1549
|
+
`;
|
|
1550
|
+
if (result.succeeded.length > 0) {
|
|
1551
|
+
output += `Upgraded: ${result.succeeded.join(", ")}
|
|
1552
|
+
`;
|
|
1553
|
+
}
|
|
1554
|
+
if (result.failed.length > 0) {
|
|
1555
|
+
output += `Failed: ${result.failed.join(", ")}`;
|
|
1556
|
+
}
|
|
1557
|
+
return { output, success: result.failed.length === 0 };
|
|
1558
|
+
}
|
|
1559
|
+
const configPath = positionals[0];
|
|
1560
|
+
const config = await loadPkgConfig(configPath);
|
|
1561
|
+
if (values.purge) {
|
|
1562
|
+
config.config.purge = true;
|
|
1563
|
+
}
|
|
1564
|
+
await syncPackages(config);
|
|
1565
|
+
return { output: "Sync complete", success: true };
|
|
1566
|
+
}
|
|
1567
|
+
async function main2() {
|
|
1568
|
+
const { values, positionals } = parseArgs2({
|
|
1569
|
+
args: process.argv.slice(2),
|
|
1570
|
+
options: {
|
|
1571
|
+
"upgrade-only": { type: "boolean", default: false },
|
|
1572
|
+
"upgrade-interactive": { type: "boolean", default: false },
|
|
1573
|
+
purge: { type: "boolean", default: false },
|
|
1574
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1575
|
+
},
|
|
1576
|
+
allowPositionals: true
|
|
1577
|
+
});
|
|
1578
|
+
if (values.help) {
|
|
1579
|
+
printUsage2();
|
|
1580
|
+
process.exit(0);
|
|
1581
|
+
}
|
|
1582
|
+
await checkDependencies();
|
|
1583
|
+
if (values["upgrade-interactive"]) {
|
|
1584
|
+
await upgradeInteractive();
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (values["upgrade-only"]) {
|
|
1588
|
+
const result = await upgradeWithVerification();
|
|
1589
|
+
console.log(`
|
|
1590
|
+
${colors3.bold}=== Upgrade Summary ===${colors3.reset}
|
|
1591
|
+
`);
|
|
1592
|
+
if (result.succeeded.length > 0) {
|
|
1593
|
+
console.log(`${colors3.green}✓ Upgraded (${result.succeeded.length}):${colors3.reset} ${result.succeeded.join(", ")}`);
|
|
1594
|
+
}
|
|
1595
|
+
if (result.failed.length > 0) {
|
|
1596
|
+
console.log(`${colors3.red}✗ Failed (${result.failed.length}):${colors3.reset} ${result.failed.join(", ")}`);
|
|
1597
|
+
}
|
|
1598
|
+
if (result.attempted.length === 0) {
|
|
1599
|
+
console.log(`${colors3.green}Everything is up to date!${colors3.reset}`);
|
|
1600
|
+
} else if (result.failed.length === 0 && result.stillOutdated.length === 0) {
|
|
1601
|
+
console.log(`
|
|
1602
|
+
${colors3.green}All upgrades completed successfully!${colors3.reset}`);
|
|
1603
|
+
} else {
|
|
1604
|
+
console.log(`
|
|
1605
|
+
${colors3.yellow}Some packages could not be upgraded. Manual intervention may be required.${colors3.reset}`);
|
|
1606
|
+
}
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
const configPath = positionals[0];
|
|
1610
|
+
const config = await loadPkgConfig(configPath);
|
|
1611
|
+
if (values.purge) {
|
|
1612
|
+
config.config.purge = true;
|
|
1613
|
+
}
|
|
1614
|
+
await syncPackages(config);
|
|
1615
|
+
}
|
|
1616
|
+
var isMainModule2 = process.argv[1]?.includes("pkg-sync");
|
|
1617
|
+
if (isMainModule2) {
|
|
1618
|
+
main2().catch(console.error);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// src/cli/pkg-lock.ts
|
|
1622
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
1623
|
+
var colors4 = {
|
|
1624
|
+
red: "\x1B[0;31m",
|
|
1625
|
+
green: "\x1B[0;32m",
|
|
1626
|
+
blue: "\x1B[0;34m",
|
|
1627
|
+
yellow: "\x1B[1;33m",
|
|
1628
|
+
cyan: "\x1B[0;36m",
|
|
1629
|
+
bold: "\x1B[1m",
|
|
1630
|
+
reset: "\x1B[0m"
|
|
1631
|
+
};
|
|
1632
|
+
function printUsage3() {
|
|
1633
|
+
console.log(`
|
|
1634
|
+
${colors4.cyan}Usage: bun run pkg-lock [command]${colors4.reset}
|
|
1635
|
+
|
|
1636
|
+
Commands:
|
|
1637
|
+
${colors4.blue}update${colors4.reset} Update lockfile with current installed versions (default)
|
|
1638
|
+
${colors4.blue}status${colors4.reset} Show changes since last lock
|
|
1639
|
+
${colors4.blue}reset${colors4.reset} Regenerate lockfile from scratch
|
|
1640
|
+
${colors4.blue}show${colors4.reset} Display current lockfile contents
|
|
1641
|
+
`);
|
|
1642
|
+
}
|
|
1643
|
+
async function showStatus2() {
|
|
1644
|
+
const lock = await loadPkgLock();
|
|
1645
|
+
if (!lock) {
|
|
1646
|
+
console.log(`${colors4.yellow}No lockfile found. Run 'bun run pkg-lock' to create one.${colors4.reset}`);
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const changes = await getChangedPackages();
|
|
1650
|
+
if (changes.added.length === 0 && changes.removed.length === 0 && changes.upgraded.length === 0) {
|
|
1651
|
+
console.log(`${colors4.green}Lockfile is up to date.${colors4.reset}`);
|
|
1652
|
+
console.log(` Last updated: ${lock.lastUpdated}`);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
console.log(`${colors4.bold}Changes since last lock:${colors4.reset}
|
|
1656
|
+
`);
|
|
1657
|
+
if (changes.added.length > 0) {
|
|
1658
|
+
console.log(`${colors4.green}Added:${colors4.reset}`);
|
|
1659
|
+
for (const name of changes.added) {
|
|
1660
|
+
console.log(` + ${name}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (changes.removed.length > 0) {
|
|
1664
|
+
console.log(`${colors4.red}Removed:${colors4.reset}`);
|
|
1665
|
+
for (const name of changes.removed) {
|
|
1666
|
+
console.log(` - ${name}`);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
if (changes.upgraded.length > 0) {
|
|
1670
|
+
console.log(`${colors4.blue}Upgraded:${colors4.reset}`);
|
|
1671
|
+
for (const { name, from, to } of changes.upgraded) {
|
|
1672
|
+
console.log(` ~ ${name}: ${from} -> ${to}`);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
async function showLockfile() {
|
|
1677
|
+
const lock = await loadPkgLock();
|
|
1678
|
+
if (!lock) {
|
|
1679
|
+
console.log(`${colors4.yellow}No lockfile found.${colors4.reset}`);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
console.log(`${colors4.bold}Package Lockfile${colors4.reset}`);
|
|
1683
|
+
console.log(`Last updated: ${lock.lastUpdated}
|
|
1684
|
+
`);
|
|
1685
|
+
const formulaNames = Object.keys(lock.formulas).sort();
|
|
1686
|
+
const caskNames = Object.keys(lock.casks).sort();
|
|
1687
|
+
if (formulaNames.length > 0) {
|
|
1688
|
+
console.log(`${colors4.cyan}Formulas (${formulaNames.length}):${colors4.reset}`);
|
|
1689
|
+
for (const name of formulaNames) {
|
|
1690
|
+
const { version, tap } = lock.formulas[name];
|
|
1691
|
+
console.log(` ${name} ${colors4.blue}${version}${colors4.reset} (${tap})`);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (caskNames.length > 0) {
|
|
1695
|
+
console.log(`
|
|
1696
|
+
${colors4.cyan}Casks (${caskNames.length}):${colors4.reset}`);
|
|
1697
|
+
for (const name of caskNames) {
|
|
1698
|
+
const { version } = lock.casks[name];
|
|
1699
|
+
console.log(` ${name} ${colors4.blue}${version}${colors4.reset}`);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
async function runPkgLock(args) {
|
|
1704
|
+
const { positionals } = parseArgs3({
|
|
1705
|
+
args,
|
|
1706
|
+
options: {
|
|
1707
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1708
|
+
},
|
|
1709
|
+
allowPositionals: true
|
|
1710
|
+
});
|
|
1711
|
+
const command = positionals[0] || "update";
|
|
1712
|
+
switch (command) {
|
|
1713
|
+
case "update": {
|
|
1714
|
+
const lock = await updateLockfile();
|
|
1715
|
+
const total = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
|
|
1716
|
+
return { output: `Lockfile updated with ${total} packages`, success: true };
|
|
1717
|
+
}
|
|
1718
|
+
case "status": {
|
|
1719
|
+
const lock = await loadPkgLock();
|
|
1720
|
+
if (!lock) {
|
|
1721
|
+
return { output: "No lockfile found", success: false };
|
|
1722
|
+
}
|
|
1723
|
+
const changes = await getChangedPackages();
|
|
1724
|
+
if (changes.added.length === 0 && changes.removed.length === 0 && changes.upgraded.length === 0) {
|
|
1725
|
+
return { output: `Lockfile is up to date (last: ${lock.lastUpdated})`, success: true };
|
|
1726
|
+
}
|
|
1727
|
+
let output = `Changes:
|
|
1728
|
+
`;
|
|
1729
|
+
if (changes.added.length > 0)
|
|
1730
|
+
output += `Added: ${changes.added.join(", ")}
|
|
1731
|
+
`;
|
|
1732
|
+
if (changes.removed.length > 0)
|
|
1733
|
+
output += `Removed: ${changes.removed.join(", ")}
|
|
1734
|
+
`;
|
|
1735
|
+
if (changes.upgraded.length > 0)
|
|
1736
|
+
output += `Upgraded: ${changes.upgraded.map((u) => u.name).join(", ")}`;
|
|
1737
|
+
return { output, success: true };
|
|
1738
|
+
}
|
|
1739
|
+
default:
|
|
1740
|
+
return { output: `Unknown command: ${command}`, success: false };
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async function main3() {
|
|
1744
|
+
const { values, positionals } = parseArgs3({
|
|
1745
|
+
args: process.argv.slice(2),
|
|
1746
|
+
options: {
|
|
1747
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1748
|
+
},
|
|
1749
|
+
allowPositionals: true
|
|
1750
|
+
});
|
|
1751
|
+
if (values.help) {
|
|
1752
|
+
printUsage3();
|
|
1753
|
+
process.exit(0);
|
|
1754
|
+
}
|
|
1755
|
+
const command = positionals[0] || "update";
|
|
1756
|
+
switch (command) {
|
|
1757
|
+
case "update": {
|
|
1758
|
+
console.log(`${colors4.cyan}Updating lockfile...${colors4.reset}`);
|
|
1759
|
+
const lock = await updateLockfile();
|
|
1760
|
+
const total = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
|
|
1761
|
+
console.log(`${colors4.green}Lockfile updated with ${total} packages.${colors4.reset}`);
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1764
|
+
case "status":
|
|
1765
|
+
await showStatus2();
|
|
1766
|
+
break;
|
|
1767
|
+
case "reset": {
|
|
1768
|
+
console.log(`${colors4.cyan}Regenerating lockfile...${colors4.reset}`);
|
|
1769
|
+
const lock = await generateLockfile();
|
|
1770
|
+
await savePkgLock(lock);
|
|
1771
|
+
const total = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
|
|
1772
|
+
console.log(`${colors4.green}Lockfile regenerated with ${total} packages.${colors4.reset}`);
|
|
1773
|
+
break;
|
|
1774
|
+
}
|
|
1775
|
+
case "show":
|
|
1776
|
+
await showLockfile();
|
|
1777
|
+
break;
|
|
1778
|
+
default:
|
|
1779
|
+
console.error(`${colors4.red}Unknown command: ${command}${colors4.reset}`);
|
|
1780
|
+
printUsage3();
|
|
1781
|
+
process.exit(1);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
var isMainModule3 = process.argv[1]?.includes("pkg-lock");
|
|
1785
|
+
if (isMainModule3) {
|
|
1786
|
+
main3().catch(console.error);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// src/cli/set-theme.ts
|
|
1790
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
1791
|
+
import { readdirSync as readdirSync4, existsSync as existsSync5, rmSync, symlinkSync, unlinkSync } from "fs";
|
|
1792
|
+
import { join as join4 } from "path";
|
|
1793
|
+
var colors5 = {
|
|
1794
|
+
red: "\x1B[0;31m",
|
|
1795
|
+
green: "\x1B[0;32m",
|
|
1796
|
+
blue: "\x1B[0;34m",
|
|
1797
|
+
yellow: "\x1B[1;33m",
|
|
1798
|
+
cyan: "\x1B[0;36m",
|
|
1799
|
+
dim: "\x1B[2m",
|
|
1800
|
+
reset: "\x1B[0m"
|
|
1801
|
+
};
|
|
1802
|
+
async function listThemes() {
|
|
1803
|
+
await ensureConfigDir();
|
|
1804
|
+
if (!existsSync5(THEMES_DIR)) {
|
|
1805
|
+
return [];
|
|
1806
|
+
}
|
|
1807
|
+
const entries = readdirSync4(THEMES_DIR, { withFileTypes: true });
|
|
1808
|
+
const themes = [];
|
|
1809
|
+
for (const entry of entries) {
|
|
1810
|
+
if (entry.isDirectory()) {
|
|
1811
|
+
const themePath = join4(THEMES_DIR, entry.name);
|
|
1812
|
+
const theme = await parseTheme(themePath, entry.name);
|
|
1813
|
+
themes.push(theme);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return themes;
|
|
1817
|
+
}
|
|
1818
|
+
function clearDirectory(dir) {
|
|
1819
|
+
if (existsSync5(dir)) {
|
|
1820
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
1821
|
+
for (const entry of entries) {
|
|
1822
|
+
const fullPath = join4(dir, entry.name);
|
|
1823
|
+
if (entry.isSymbolicLink() || entry.isFile()) {
|
|
1824
|
+
unlinkSync(fullPath);
|
|
1825
|
+
} else if (entry.isDirectory()) {
|
|
1826
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
function createSymlink(source, target) {
|
|
1832
|
+
if (existsSync5(target)) {
|
|
1833
|
+
unlinkSync(target);
|
|
1834
|
+
}
|
|
1835
|
+
symlinkSync(source, target);
|
|
1836
|
+
}
|
|
1837
|
+
async function applyTheme(themeName) {
|
|
1838
|
+
const themeDir = join4(THEMES_DIR, themeName);
|
|
1839
|
+
if (!existsSync5(themeDir)) {
|
|
1840
|
+
return { output: `Theme '${themeName}' not found`, success: false };
|
|
1841
|
+
}
|
|
1842
|
+
await ensureConfigDir();
|
|
1843
|
+
await ensureDir2(THEME_TARGET_DIR);
|
|
1844
|
+
const theme = await parseTheme(themeDir, themeName);
|
|
1845
|
+
clearDirectory(THEME_TARGET_DIR);
|
|
1846
|
+
if (existsSync5(BACKGROUNDS_TARGET_DIR)) {
|
|
1847
|
+
rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
|
|
1848
|
+
}
|
|
1849
|
+
const entries = readdirSync4(themeDir, { withFileTypes: true });
|
|
1850
|
+
for (const entry of entries) {
|
|
1851
|
+
const source = join4(themeDir, entry.name);
|
|
1852
|
+
if (entry.isFile() && entry.name !== "theme.yaml" && entry.name !== "light.mode") {
|
|
1853
|
+
const target = join4(THEME_TARGET_DIR, entry.name);
|
|
1854
|
+
createSymlink(source, target);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (theme.hasBackgrounds) {
|
|
1858
|
+
const backgroundsSource = join4(themeDir, "backgrounds");
|
|
1859
|
+
createSymlink(backgroundsSource, BACKGROUNDS_TARGET_DIR);
|
|
1860
|
+
}
|
|
1861
|
+
let output = `Theme '${theme.name}' applied successfully`;
|
|
1862
|
+
if (theme.metadata?.author) {
|
|
1863
|
+
output += `
|
|
1864
|
+
Author: ${theme.metadata.author}`;
|
|
1865
|
+
}
|
|
1866
|
+
if (theme.hasBackgrounds) {
|
|
1867
|
+
output += `
|
|
1868
|
+
Wallpapers available at: ~/.config/formalconf/current/backgrounds/`;
|
|
1869
|
+
}
|
|
1870
|
+
if (theme.isLightMode) {
|
|
1871
|
+
output += `
|
|
1872
|
+
Note: This is a light mode theme`;
|
|
1873
|
+
}
|
|
1874
|
+
return { output, success: true };
|
|
1875
|
+
}
|
|
1876
|
+
async function showThemeInfo(themeName) {
|
|
1877
|
+
const themeDir = join4(THEMES_DIR, themeName);
|
|
1878
|
+
if (!existsSync5(themeDir)) {
|
|
1879
|
+
console.error(`${colors5.red}Error: Theme '${themeName}' not found${colors5.reset}`);
|
|
1880
|
+
process.exit(1);
|
|
1881
|
+
}
|
|
1882
|
+
const theme = await parseTheme(themeDir, themeName);
|
|
1883
|
+
console.log(`
|
|
1884
|
+
${colors5.cyan}Theme: ${theme.name}${colors5.reset}`);
|
|
1885
|
+
if (theme.metadata) {
|
|
1886
|
+
if (theme.metadata.author)
|
|
1887
|
+
console.log(`Author: ${theme.metadata.author}`);
|
|
1888
|
+
if (theme.metadata.description)
|
|
1889
|
+
console.log(`Description: ${theme.metadata.description}`);
|
|
1890
|
+
if (theme.metadata.version)
|
|
1891
|
+
console.log(`Version: ${theme.metadata.version}`);
|
|
1892
|
+
if (theme.metadata.source)
|
|
1893
|
+
console.log(`Source: ${theme.metadata.source}`);
|
|
1894
|
+
}
|
|
1895
|
+
console.log(`
|
|
1896
|
+
Files (${theme.files.length}):`);
|
|
1897
|
+
for (const file of theme.files) {
|
|
1898
|
+
console.log(` ${colors5.blue}•${colors5.reset} ${file.name}`);
|
|
1899
|
+
}
|
|
1900
|
+
if (theme.hasBackgrounds) {
|
|
1901
|
+
console.log(`
|
|
1902
|
+
${colors5.green}Has wallpapers${colors5.reset}`);
|
|
1903
|
+
}
|
|
1904
|
+
if (theme.hasPreview) {
|
|
1905
|
+
console.log(`${colors5.green}Has preview image${colors5.reset}`);
|
|
1906
|
+
}
|
|
1907
|
+
if (theme.isLightMode) {
|
|
1908
|
+
console.log(`${colors5.yellow}Light mode theme${colors5.reset}`);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
async function runSetTheme(themeName) {
|
|
1912
|
+
return applyTheme(themeName);
|
|
1913
|
+
}
|
|
1914
|
+
async function main4() {
|
|
1915
|
+
const { positionals, values } = parseArgs4({
|
|
1916
|
+
args: process.argv.slice(2),
|
|
1917
|
+
options: {
|
|
1918
|
+
info: { type: "boolean", short: "i" }
|
|
1919
|
+
},
|
|
1920
|
+
allowPositionals: true
|
|
1921
|
+
});
|
|
1922
|
+
const [themeName] = positionals;
|
|
1923
|
+
if (!themeName) {
|
|
1924
|
+
const themes = await listThemes();
|
|
1925
|
+
if (themes.length === 0) {
|
|
1926
|
+
console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
|
|
1927
|
+
console.log(`This system is compatible with omarchy themes.`);
|
|
1928
|
+
console.log(`
|
|
1929
|
+
Add themes to: ${colors5.cyan}~/.config/formalconf/themes/${colors5.reset}`);
|
|
1930
|
+
process.exit(0);
|
|
1931
|
+
}
|
|
1932
|
+
console.log(`${colors5.cyan}Usage: formalconf theme <theme-name>${colors5.reset}`);
|
|
1933
|
+
console.log(` formalconf theme --info <theme-name>
|
|
1934
|
+
`);
|
|
1935
|
+
console.log("Available themes:");
|
|
1936
|
+
for (const theme of themes) {
|
|
1937
|
+
const extras = [];
|
|
1938
|
+
if (theme.hasBackgrounds)
|
|
1939
|
+
extras.push("wallpapers");
|
|
1940
|
+
if (theme.isLightMode)
|
|
1941
|
+
extras.push("light");
|
|
1942
|
+
const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
|
|
1943
|
+
console.log(` ${colors5.blue}•${colors5.reset} ${theme.name}${suffix}`);
|
|
1944
|
+
}
|
|
1945
|
+
process.exit(0);
|
|
1946
|
+
}
|
|
1947
|
+
if (values.info) {
|
|
1948
|
+
await showThemeInfo(themeName);
|
|
1949
|
+
} else {
|
|
1950
|
+
const result = await applyTheme(themeName);
|
|
1951
|
+
console.log(result.success ? `${colors5.green}${result.output}${colors5.reset}` : `${colors5.red}${result.output}${colors5.reset}`);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
var isMainModule4 = process.argv[1]?.includes("set-theme");
|
|
1955
|
+
if (isMainModule4) {
|
|
1956
|
+
main4().catch(console.error);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/cli/formalconf.tsx
|
|
1960
|
+
import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
|
|
1961
|
+
function MainMenu({ onSelect }) {
|
|
1962
|
+
const { exit } = useApp();
|
|
1963
|
+
return /* @__PURE__ */ jsxDEV10(Panel, {
|
|
1964
|
+
title: "Main Menu",
|
|
1965
|
+
children: /* @__PURE__ */ jsxDEV10(VimSelect, {
|
|
1966
|
+
options: [
|
|
1967
|
+
{ label: "Config Manager", value: "config" },
|
|
1968
|
+
{ label: "Package Sync", value: "packages" },
|
|
1969
|
+
{ label: "Set Theme", value: "themes" },
|
|
1970
|
+
{ label: "Exit", value: "exit" }
|
|
1971
|
+
],
|
|
1972
|
+
onChange: (value) => {
|
|
1973
|
+
if (value === "exit") {
|
|
1974
|
+
exit();
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
onSelect(value);
|
|
1978
|
+
}
|
|
1979
|
+
}, undefined, false, undefined, this)
|
|
1980
|
+
}, undefined, false, undefined, this);
|
|
1981
|
+
}
|
|
1982
|
+
function ConfigMenu({ onBack }) {
|
|
1983
|
+
const [state, setState] = useState4("menu");
|
|
1984
|
+
const [output, setOutput] = useState4("");
|
|
1985
|
+
const [success, setSuccess] = useState4(true);
|
|
1986
|
+
useInput3((input, key) => {
|
|
1987
|
+
if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
|
|
1988
|
+
onBack();
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
const handleAction = async (action) => {
|
|
1992
|
+
if (action === "back") {
|
|
1993
|
+
onBack();
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
setState("running");
|
|
1997
|
+
const result = await runConfigManager([action]);
|
|
1998
|
+
setOutput(result.output);
|
|
1999
|
+
setSuccess(result.success);
|
|
2000
|
+
setState("result");
|
|
2001
|
+
};
|
|
2002
|
+
if (state === "running") {
|
|
2003
|
+
return /* @__PURE__ */ jsxDEV10(Panel, {
|
|
2004
|
+
title: "Config Manager",
|
|
2005
|
+
children: /* @__PURE__ */ jsxDEV10(Spinner, {
|
|
2006
|
+
label: "Processing..."
|
|
2007
|
+
}, undefined, false, undefined, this)
|
|
2008
|
+
}, undefined, false, undefined, this);
|
|
2009
|
+
}
|
|
2010
|
+
if (state === "result") {
|
|
2011
|
+
return /* @__PURE__ */ jsxDEV10(CommandOutput, {
|
|
2012
|
+
title: "Config Manager",
|
|
2013
|
+
output,
|
|
2014
|
+
success,
|
|
2015
|
+
onDismiss: () => setState("menu")
|
|
2016
|
+
}, undefined, false, undefined, this);
|
|
2017
|
+
}
|
|
2018
|
+
return /* @__PURE__ */ jsxDEV10(Panel, {
|
|
2019
|
+
title: "Config Manager",
|
|
2020
|
+
children: /* @__PURE__ */ jsxDEV10(VimSelect, {
|
|
2021
|
+
options: [
|
|
2022
|
+
{ label: "Stow all packages", value: "stow-all" },
|
|
2023
|
+
{ label: "Unstow all packages", value: "unstow-all" },
|
|
2024
|
+
{ label: "Check status", value: "status" },
|
|
2025
|
+
{ label: "List packages", value: "list" },
|
|
2026
|
+
{ label: "Back", value: "back" }
|
|
2027
|
+
],
|
|
2028
|
+
onChange: handleAction
|
|
2029
|
+
}, undefined, false, undefined, this)
|
|
2030
|
+
}, undefined, false, undefined, this);
|
|
2031
|
+
}
|
|
2032
|
+
function PackageMenu({ onBack }) {
|
|
2033
|
+
const [state, setState] = useState4("menu");
|
|
2034
|
+
const [output, setOutput] = useState4("");
|
|
2035
|
+
const [success, setSuccess] = useState4(true);
|
|
2036
|
+
useInput3((input, key) => {
|
|
2037
|
+
if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
|
|
2038
|
+
onBack();
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
const handleAction = async (action) => {
|
|
2042
|
+
if (action === "back") {
|
|
2043
|
+
onBack();
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
setState("running");
|
|
2047
|
+
let result;
|
|
2048
|
+
switch (action) {
|
|
2049
|
+
case "sync":
|
|
2050
|
+
result = await runPkgSync([]);
|
|
2051
|
+
break;
|
|
2052
|
+
case "sync-purge":
|
|
2053
|
+
result = await runPkgSync(["--purge"]);
|
|
2054
|
+
break;
|
|
2055
|
+
case "upgrade":
|
|
2056
|
+
result = await runPkgSync(["--upgrade-only"]);
|
|
2057
|
+
break;
|
|
2058
|
+
case "upgrade-interactive":
|
|
2059
|
+
result = await runPkgSync(["--upgrade-interactive"]);
|
|
2060
|
+
break;
|
|
2061
|
+
case "lock-update":
|
|
2062
|
+
result = await runPkgLock(["update"]);
|
|
2063
|
+
break;
|
|
2064
|
+
case "lock-status":
|
|
2065
|
+
result = await runPkgLock(["status"]);
|
|
2066
|
+
break;
|
|
2067
|
+
default:
|
|
2068
|
+
result = { output: "Unknown action", success: false };
|
|
2069
|
+
}
|
|
2070
|
+
setOutput(result.output);
|
|
2071
|
+
setSuccess(result.success);
|
|
2072
|
+
setState("result");
|
|
2073
|
+
};
|
|
2074
|
+
if (state === "running") {
|
|
2075
|
+
return /* @__PURE__ */ jsxDEV10(Panel, {
|
|
2076
|
+
title: "Package Sync",
|
|
2077
|
+
children: /* @__PURE__ */ jsxDEV10(Spinner, {
|
|
2078
|
+
label: "Syncing packages..."
|
|
2079
|
+
}, undefined, false, undefined, this)
|
|
2080
|
+
}, undefined, false, undefined, this);
|
|
2081
|
+
}
|
|
2082
|
+
if (state === "result") {
|
|
2083
|
+
return /* @__PURE__ */ jsxDEV10(CommandOutput, {
|
|
2084
|
+
title: "Package Sync",
|
|
2085
|
+
output,
|
|
2086
|
+
success,
|
|
2087
|
+
onDismiss: () => setState("menu")
|
|
2088
|
+
}, undefined, false, undefined, this);
|
|
2089
|
+
}
|
|
2090
|
+
return /* @__PURE__ */ jsxDEV10(Panel, {
|
|
2091
|
+
title: "Package Sync",
|
|
2092
|
+
children: /* @__PURE__ */ jsxDEV10(VimSelect, {
|
|
2093
|
+
options: [
|
|
2094
|
+
{ label: "Sync packages", value: "sync" },
|
|
2095
|
+
{ label: "Sync with purge", value: "sync-purge" },
|
|
2096
|
+
{ label: "Upgrade all (with verification)", value: "upgrade" },
|
|
2097
|
+
{ label: "Upgrade interactive", value: "upgrade-interactive" },
|
|
2098
|
+
{ label: "Update lockfile", value: "lock-update" },
|
|
2099
|
+
{ label: "Lockfile status", value: "lock-status" },
|
|
2100
|
+
{ label: "Back", value: "back" }
|
|
2101
|
+
],
|
|
2102
|
+
onChange: handleAction
|
|
2103
|
+
}, undefined, false, undefined, this)
|
|
2104
|
+
}, undefined, false, undefined, this);
|
|
2105
|
+
}
|
|
2106
|
+
function ThemeMenu({ onBack }) {
|
|
2107
|
+
const [themes, setThemes] = useState4([]);
|
|
2108
|
+
const [loading, setLoading] = useState4(true);
|
|
2109
|
+
const [selectedIndex, setSelectedIndex] = useState4(0);
|
|
2110
|
+
const [state, setState] = useState4("menu");
|
|
2111
|
+
const [output, setOutput] = useState4("");
|
|
2112
|
+
const [success, setSuccess] = useState4(true);
|
|
2113
|
+
const { columns, rows } = useTerminalSize();
|
|
2114
|
+
const CARD_HEIGHT = 3;
|
|
2115
|
+
const LAYOUT_OVERHEAD = 20;
|
|
2116
|
+
const cardWidth = useMemo(() => {
|
|
2117
|
+
const availableWidth = columns - 6;
|
|
2118
|
+
const cardsPerRow2 = Math.max(1, Math.floor(availableWidth / 28));
|
|
2119
|
+
return Math.floor(availableWidth / cardsPerRow2);
|
|
2120
|
+
}, [columns]);
|
|
2121
|
+
const cardsPerRow = useMemo(() => {
|
|
2122
|
+
const availableWidth = columns - 6;
|
|
2123
|
+
return Math.max(1, Math.floor(availableWidth / 28));
|
|
2124
|
+
}, [columns]);
|
|
2125
|
+
const visibleRows = useMemo(() => {
|
|
2126
|
+
const availableHeight = rows - LAYOUT_OVERHEAD;
|
|
2127
|
+
return Math.max(1, Math.floor(availableHeight / CARD_HEIGHT));
|
|
2128
|
+
}, [rows]);
|
|
2129
|
+
const selectedRow = Math.floor(selectedIndex / cardsPerRow);
|
|
2130
|
+
const totalRows = Math.ceil(themes.length / cardsPerRow);
|
|
2131
|
+
const [scrollOffset, setScrollOffset] = useState4(0);
|
|
2132
|
+
useEffect3(() => {
|
|
2133
|
+
if (selectedRow < scrollOffset) {
|
|
2134
|
+
setScrollOffset(selectedRow);
|
|
2135
|
+
} else if (selectedRow >= scrollOffset + visibleRows) {
|
|
2136
|
+
setScrollOffset(selectedRow - visibleRows + 1);
|
|
2137
|
+
}
|
|
2138
|
+
}, [selectedRow, scrollOffset, visibleRows]);
|
|
2139
|
+
const visibleThemes = useMemo(() => {
|
|
2140
|
+
const startIdx = scrollOffset * cardsPerRow;
|
|
2141
|
+
const endIdx = (scrollOffset + visibleRows) * cardsPerRow;
|
|
2142
|
+
return themes.slice(startIdx, endIdx);
|
|
2143
|
+
}, [themes, scrollOffset, visibleRows, cardsPerRow]);
|
|
2144
|
+
const visibleStartIndex = scrollOffset * cardsPerRow;
|
|
2145
|
+
useInput3((input, key) => {
|
|
2146
|
+
if (state !== "menu" || loading)
|
|
2147
|
+
return;
|
|
2148
|
+
if (key.escape) {
|
|
2149
|
+
onBack();
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
if (key.rightArrow || input === "l") {
|
|
2153
|
+
if (selectedIndex < themes.length - 1) {
|
|
2154
|
+
setSelectedIndex((i) => i + 1);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (key.leftArrow || input === "h") {
|
|
2158
|
+
if (selectedIndex > 0) {
|
|
2159
|
+
setSelectedIndex((i) => i - 1);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
if (key.downArrow || input === "j") {
|
|
2163
|
+
const nextIndex = selectedIndex + cardsPerRow;
|
|
2164
|
+
if (nextIndex < themes.length) {
|
|
2165
|
+
setSelectedIndex(nextIndex);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (key.upArrow || input === "k") {
|
|
2169
|
+
const prevIndex = selectedIndex - cardsPerRow;
|
|
2170
|
+
if (prevIndex >= 0) {
|
|
2171
|
+
setSelectedIndex(prevIndex);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
if (key.return) {
|
|
2175
|
+
applyTheme2(themes[selectedIndex]);
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
useEffect3(() => {
|
|
2179
|
+
async function loadThemes() {
|
|
2180
|
+
if (!existsSync6(THEMES_DIR)) {
|
|
2181
|
+
setThemes([]);
|
|
2182
|
+
setLoading(false);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const entries = readdirSync5(THEMES_DIR, { withFileTypes: true });
|
|
2186
|
+
const loadedThemes = [];
|
|
2187
|
+
for (const entry of entries) {
|
|
2188
|
+
if (entry.isDirectory()) {
|
|
2189
|
+
const themePath = join5(THEMES_DIR, entry.name);
|
|
2190
|
+
const theme = await parseTheme(themePath, entry.name);
|
|
2191
|
+
loadedThemes.push(theme);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
setThemes(loadedThemes);
|
|
2195
|
+
setLoading(false);
|
|
2196
|
+
}
|
|
2197
|
+
loadThemes();
|
|
2198
|
+
}, []);
|
|
2199
|
+
const applyTheme2 = async (theme) => {
|
|
2200
|
+
setState("running");
|
|
2201
|
+
const themeName = theme.path.split("/").pop();
|
|
2202
|
+
const result = await runSetTheme(themeName);
|
|
2203
|
+
setOutput(result.output);
|
|
2204
|
+
setSuccess(result.success);
|
|
2205
|
+
setState("result");
|
|
2206
|
+
};
|
|
2207
|
+
if (loading || state === "running") {
|
|
2208
|
+
return /* @__PURE__ */ jsxDEV10(Panel, {
|
|
2209
|
+
title: "Select Theme",
|
|
2210
|
+
children: /* @__PURE__ */ jsxDEV10(Spinner, {
|
|
2211
|
+
label: loading ? "Loading themes..." : "Applying theme..."
|
|
2212
|
+
}, undefined, false, undefined, this)
|
|
2213
|
+
}, undefined, false, undefined, this);
|
|
2214
|
+
}
|
|
2215
|
+
if (state === "result") {
|
|
2216
|
+
return /* @__PURE__ */ jsxDEV10(CommandOutput, {
|
|
2217
|
+
title: "Select Theme",
|
|
2218
|
+
output,
|
|
2219
|
+
success,
|
|
2220
|
+
onDismiss: () => setState("menu")
|
|
2221
|
+
}, undefined, false, undefined, this);
|
|
2222
|
+
}
|
|
2223
|
+
if (themes.length === 0) {
|
|
2224
|
+
return /* @__PURE__ */ jsxDEV10(Panel, {
|
|
2225
|
+
title: "Select Theme",
|
|
2226
|
+
children: [
|
|
2227
|
+
/* @__PURE__ */ jsxDEV10(Box10, {
|
|
2228
|
+
flexDirection: "column",
|
|
2229
|
+
children: [
|
|
2230
|
+
/* @__PURE__ */ jsxDEV10(Text9, {
|
|
2231
|
+
color: colors.warning,
|
|
2232
|
+
children: "No themes available."
|
|
2233
|
+
}, undefined, false, undefined, this),
|
|
2234
|
+
/* @__PURE__ */ jsxDEV10(Text9, {
|
|
2235
|
+
children: "This system is compatible with omarchy themes."
|
|
2236
|
+
}, undefined, false, undefined, this),
|
|
2237
|
+
/* @__PURE__ */ jsxDEV10(Text9, {
|
|
2238
|
+
dimColor: true,
|
|
2239
|
+
children: "Add themes to ~/.config/formalconf/themes/"
|
|
2240
|
+
}, undefined, false, undefined, this)
|
|
2241
|
+
]
|
|
2242
|
+
}, undefined, true, undefined, this),
|
|
2243
|
+
/* @__PURE__ */ jsxDEV10(Box10, {
|
|
2244
|
+
marginTop: 1,
|
|
2245
|
+
children: /* @__PURE__ */ jsxDEV10(VimSelect, {
|
|
2246
|
+
options: [{ label: "Back", value: "back" }],
|
|
2247
|
+
onChange: () => onBack()
|
|
2248
|
+
}, undefined, false, undefined, this)
|
|
2249
|
+
}, undefined, false, undefined, this)
|
|
2250
|
+
]
|
|
2251
|
+
}, undefined, true, undefined, this);
|
|
2252
|
+
}
|
|
2253
|
+
const showScrollUp = scrollOffset > 0;
|
|
2254
|
+
const showScrollDown = scrollOffset + visibleRows < totalRows;
|
|
2255
|
+
const gridHeight = visibleRows * CARD_HEIGHT;
|
|
2256
|
+
return /* @__PURE__ */ jsxDEV10(Panel, {
|
|
2257
|
+
title: "Select Theme",
|
|
2258
|
+
children: [
|
|
2259
|
+
showScrollUp && /* @__PURE__ */ jsxDEV10(Text9, {
|
|
2260
|
+
dimColor: true,
|
|
2261
|
+
children: [
|
|
2262
|
+
" ↑ ",
|
|
2263
|
+
scrollOffset,
|
|
2264
|
+
" more row",
|
|
2265
|
+
scrollOffset > 1 ? "s" : ""
|
|
2266
|
+
]
|
|
2267
|
+
}, undefined, true, undefined, this),
|
|
2268
|
+
/* @__PURE__ */ jsxDEV10(Box10, {
|
|
2269
|
+
flexDirection: "row",
|
|
2270
|
+
flexWrap: "wrap",
|
|
2271
|
+
height: gridHeight,
|
|
2272
|
+
overflow: "hidden",
|
|
2273
|
+
children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV10(ThemeCard, {
|
|
2274
|
+
theme,
|
|
2275
|
+
isSelected: visibleStartIndex + index === selectedIndex,
|
|
2276
|
+
width: cardWidth
|
|
2277
|
+
}, theme.path, false, undefined, this))
|
|
2278
|
+
}, undefined, false, undefined, this),
|
|
2279
|
+
showScrollDown && /* @__PURE__ */ jsxDEV10(Text9, {
|
|
2280
|
+
dimColor: true,
|
|
2281
|
+
children: [
|
|
2282
|
+
" ↓ ",
|
|
2283
|
+
totalRows - scrollOffset - visibleRows,
|
|
2284
|
+
" more row",
|
|
2285
|
+
totalRows - scrollOffset - visibleRows > 1 ? "s" : ""
|
|
2286
|
+
]
|
|
2287
|
+
}, undefined, true, undefined, this),
|
|
2288
|
+
/* @__PURE__ */ jsxDEV10(Box10, {
|
|
2289
|
+
marginTop: 1,
|
|
2290
|
+
children: /* @__PURE__ */ jsxDEV10(Text9, {
|
|
2291
|
+
dimColor: true,
|
|
2292
|
+
children: "←→↑↓/hjkl navigate • Enter select • Esc back"
|
|
2293
|
+
}, undefined, false, undefined, this)
|
|
2294
|
+
}, undefined, false, undefined, this)
|
|
2295
|
+
]
|
|
2296
|
+
}, undefined, true, undefined, this);
|
|
2297
|
+
}
|
|
2298
|
+
function App() {
|
|
2299
|
+
const [screen, setScreen] = useState4("main");
|
|
2300
|
+
const { exit } = useApp();
|
|
2301
|
+
useInput3((input) => {
|
|
2302
|
+
if (input === "q") {
|
|
2303
|
+
exit();
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
useEffect3(() => {
|
|
2307
|
+
ensureConfigDir();
|
|
2308
|
+
}, []);
|
|
2309
|
+
const getBreadcrumb = () => {
|
|
2310
|
+
switch (screen) {
|
|
2311
|
+
case "config":
|
|
2312
|
+
return ["Main", "Config Manager"];
|
|
2313
|
+
case "packages":
|
|
2314
|
+
return ["Main", "Package Sync"];
|
|
2315
|
+
case "themes":
|
|
2316
|
+
return ["Main", "Themes"];
|
|
2317
|
+
default:
|
|
2318
|
+
return ["Main"];
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
return /* @__PURE__ */ jsxDEV10(Layout, {
|
|
2322
|
+
breadcrumb: getBreadcrumb(),
|
|
2323
|
+
children: [
|
|
2324
|
+
screen === "main" && /* @__PURE__ */ jsxDEV10(MainMenu, {
|
|
2325
|
+
onSelect: setScreen
|
|
2326
|
+
}, undefined, false, undefined, this),
|
|
2327
|
+
screen === "config" && /* @__PURE__ */ jsxDEV10(ConfigMenu, {
|
|
2328
|
+
onBack: () => setScreen("main")
|
|
2329
|
+
}, undefined, false, undefined, this),
|
|
2330
|
+
screen === "packages" && /* @__PURE__ */ jsxDEV10(PackageMenu, {
|
|
2331
|
+
onBack: () => setScreen("main")
|
|
2332
|
+
}, undefined, false, undefined, this),
|
|
2333
|
+
screen === "themes" && /* @__PURE__ */ jsxDEV10(ThemeMenu, {
|
|
2334
|
+
onBack: () => setScreen("main")
|
|
2335
|
+
}, undefined, false, undefined, this)
|
|
2336
|
+
]
|
|
2337
|
+
}, undefined, true, undefined, this);
|
|
2338
|
+
}
|
|
2339
|
+
render(/* @__PURE__ */ jsxDEV10(App, {}, undefined, false, undefined, this));
|