kitowall 7.5.0 → 7.9.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 CHANGED
@@ -76,6 +76,25 @@ Dependencias del stack principal, según la función usada:
76
76
 
77
77
  La distribución recomendada de la UI es la `AppImage`.
78
78
 
79
+ ### Paso previo obligatorio
80
+ Antes de abrir `Kitowall`, el host debe tener:
81
+ - `nodejs`
82
+ - `npm`
83
+ - `kitowall` CLI instalado para el usuario actual
84
+
85
+ Comando recomendado:
86
+ ```bash
87
+ npm i -g --prefix ~/.local kitowall@latest
88
+ ```
89
+
90
+ Verificación rápida:
91
+ ```bash
92
+ command -v node
93
+ command -v npm
94
+ command -v kitowall
95
+ kitowall --version
96
+ ```
97
+
79
98
  ### Importante
80
99
  No ejecutes la AppImage con `sudo`.
81
100
 
@@ -96,9 +115,10 @@ Razón:
96
115
 
97
116
  ### Flujo recomendado
98
117
  1. Descarga la última `AppImage` desde GitHub Releases.
99
- 2. Dale permisos de ejecución.
100
- 3. Ábrela como usuario normal.
101
- 4. Usa el instalador de dependencias desde la UI.
118
+ 2. Asegúrate de tener `nodejs`, `npm` y `kitowall` CLI instalados.
119
+ 3. Dale permisos de ejecución.
120
+ 4. Ábrela como usuario normal.
121
+ 5. Usa el instalador de dependencias desde la UI.
102
122
 
103
123
  ## Instalación de dependencias del host
104
124
 
@@ -148,6 +168,13 @@ Eso ejecuta:
148
168
  ### Nota
149
169
  El script puede necesitar privilegios para instalar paquetes del sistema. La forma recomendada para usuarios finales sigue siendo la UI con `pkexec`.
150
170
 
171
+ ### Si el CLI del host está desactualizado
172
+ Si la UI indica que el `kitowall` del host no soporta un comando nuevo, actualízalo con:
173
+
174
+ ```bash
175
+ npm i -g --prefix ~/.local kitowall@latest
176
+ ```
177
+
151
178
  ## Inicialización
152
179
 
153
180
  Después del bootstrap, inicializa el stack:
@@ -361,6 +388,25 @@ Common dependencies depending on enabled features:
361
388
 
362
389
  The recommended desktop distribution for the UI is the `AppImage`.
363
390
 
391
+ ### Required First Step
392
+ Before opening `Kitowall`, the host must already have:
393
+ - `nodejs`
394
+ - `npm`
395
+ - the `kitowall` CLI installed for the current user
396
+
397
+ Recommended command:
398
+ ```bash
399
+ npm i -g --prefix ~/.local kitowall@latest
400
+ ```
401
+
402
+ Quick verification:
403
+ ```bash
404
+ command -v node
405
+ command -v npm
406
+ command -v kitowall
407
+ kitowall --version
408
+ ```
409
+
364
410
  ### Important
365
411
  Do not run the AppImage with `sudo`.
366
412
 
@@ -381,9 +427,10 @@ Reason:
381
427
 
382
428
  ### Recommended Flow
383
429
  1. Download the latest `AppImage` from GitHub Releases.
384
- 2. Make it executable.
385
- 3. Launch it as a normal user.
386
- 4. Use the dependency installer from the UI.
430
+ 2. Make sure `nodejs`, `npm`, and the `kitowall` CLI are installed.
431
+ 3. Make it executable.
432
+ 4. Launch it as a normal user.
433
+ 5. Use the dependency installer from the UI.
387
434
 
388
435
  ## Host Dependency Installation
389
436
 
@@ -437,6 +484,13 @@ That handles:
437
484
  ### Note
438
485
  The script may need privileges for system package installation. For end users, the recommended path remains the UI flow using `pkexec`.
439
486
 
487
+ ### If the host CLI is outdated
488
+ If the UI reports that the host `kitowall` does not support a newer command, update it with:
489
+
490
+ ```bash
491
+ npm i -g --prefix ~/.local kitowall@latest
492
+ ```
493
+
440
494
  ## Initialization
441
495
 
442
496
  After bootstrap, initialize the stack:
package/dist/cli.js CHANGED
@@ -43,6 +43,7 @@ const systemd_1 = require("./core/systemd");
43
43
  const init_1 = require("./core/init");
44
44
  const watch_1 = require("./core/watch");
45
45
  const doctor_1 = require("./core/doctor");
46
+ const setup_1 = require("./core/setup");
46
47
  const cache_1 = require("./core/cache");
47
48
  const favorites_1 = require("./core/favorites");
48
49
  const history_1 = require("./core/history");
@@ -73,6 +74,9 @@ function printUsage() {
73
74
  console.log(`kitowall <command> [options]
74
75
 
75
76
  Commands:
77
+ version | --version | -v Show CLI version
78
+
79
+ Core:
76
80
  outputs List outputs (Wayland monitors)
77
81
  next [--pack <name>] [--namespace <ns>] Apply next wallpapers (respects mode unless --force)
78
82
  status Show current state in JSON
@@ -119,6 +123,8 @@ Commands:
119
123
  logs [--limit <n>] [--source <name>] [--pack <name>] [--level <info|warn|error>] [--q <text>]
120
124
  Show system logs (requests/downloads/errors)
121
125
  logs clear Clear system logs
126
+
127
+ Wallpaper Engine:
122
128
  we config set-api-key <key> Save Steam Web API key (~/.config/kitowall/we.json)
123
129
  we config get-steam-roots Show configured manual Steam roots
124
130
  we config set-steam-roots <a,b,c> Save manual Steam roots (steam root or workshop/content/431960)
@@ -141,6 +147,8 @@ Commands:
141
147
  Apply wallpapers in batch by monitor map
142
148
  we stop [--monitor <name> | --all] Stop livewallpaper instances and restore previous services
143
149
  we coexist enter|exit|status Temporarily stop/restore wallpaper rotation services
150
+
151
+ Live Wallpapers:
144
152
  live init Initialize LiveWallpapers storage/index
145
153
  live list [--favorites] [--json] List downloaded LiveWallpapers from local index
146
154
  live browse [--page <n>] [--quality 4k|all] [--provider moewalls|motionbgs|all]
@@ -165,19 +173,32 @@ Commands:
165
173
  live config runner [--bin-name <name>]
166
174
  live config apply-defaults [--video-fps <n>] [--video-speed <n>] [--hwaccel auto|nvdec|vaapi|none]
167
175
  [--quality low|medium|high|ultra] [--pause-on-steam-game true|false] [--steam-poll-ms <n>]
176
+ live view Show consolidated livewallpaper runtime/config data
168
177
  live doctor [--fix]
169
- check [--namespace <ns>] [--json] Quick system check (no changes)
170
178
 
179
+ System / Host:
180
+ check [--namespace <ns>] [--json] Quick system check (no changes)
171
181
  init [--namespace <ns>] [--apply] [--force] Setup kitowall (install daemon + watcher + next.service), validate deps
172
182
  watch [--debounce <ms>] Watch Hyprland monitor hotplug events and apply wallpapers
173
183
  doctor [--namespace <ns>]
174
184
  health [--namespace <ns>]
175
-
176
185
  install-systemd [--every <dur>] Install + enable systemd user timer (timer only)
177
186
  uninstall-systemd Disable systemd timer
178
187
  systemd-status Show systemd timer status
179
188
  rotate-now [--pack <name>] Apply next wallpapers ignoring manual mode (for timers)
180
189
  mode <manual|rotate> Set mode persistently in state.json
190
+
191
+ Host Setup:
192
+ host-setup check dependency <id> [--json]
193
+ Check one host dependency
194
+ host-setup check service <id> [--namespace <ns>] [--json]
195
+ Check one host service/config item
196
+ host-setup list [--namespace <ns>] [--json]
197
+ List all host dependencies and services with status
198
+ host-setup versions [--json]
199
+ Show local/latest versions for kitowall, kitsune, and rendercore
200
+ host-setup install <id> [--namespace <ns>]
201
+ Install, reinstall, or repair one host item
181
202
  Examples:
182
203
  kitowall init --namespace kitowall --apply
183
204
  kitowall install-systemd --every 5m
@@ -349,6 +370,36 @@ async function main() {
349
370
  process.exitCode = report.ok ? 0 : 2;
350
371
  return;
351
372
  }
373
+ if (cmd === 'host-setup') {
374
+ const action = cleanOpt(args[1] ?? null);
375
+ const target = cleanOpt(args[2] ?? null);
376
+ const id = cleanOpt(args[3] ?? null);
377
+ const namespace = cleanOpt(getOptionValue(args, '--namespace')) ?? 'kitowall';
378
+ if (action === 'check' && target === 'dependency' && id) {
379
+ console.log(JSON.stringify(await (0, setup_1.checkSetupDependency)(id), null, 2));
380
+ return;
381
+ }
382
+ if (action === 'check' && target === 'service' && id) {
383
+ console.log(JSON.stringify(await (0, setup_1.checkSetupService)(id, namespace), null, 2));
384
+ return;
385
+ }
386
+ if (action === 'list') {
387
+ console.log(JSON.stringify(await (0, setup_1.listSetupStatus)(namespace), null, 2));
388
+ return;
389
+ }
390
+ if (action === 'versions') {
391
+ console.log(JSON.stringify(await (0, setup_1.listSetupVersions)(), null, 2));
392
+ return;
393
+ }
394
+ if (action === 'install' && target) {
395
+ const result = await (0, setup_1.installSetupItem)(target, namespace);
396
+ if (result.logs.trim())
397
+ process.stdout.write(result.logs);
398
+ process.exitCode = result.ok ? 0 : (result.code || 1);
399
+ return;
400
+ }
401
+ throw new Error('Usage: host-setup <check dependency <id>|check service <id>|list|versions|install <id>> [--namespace <ns>] [--json]');
402
+ }
352
403
  if (cmd === 'we') {
353
404
  const action = cleanOpt(args[1] ?? null);
354
405
  if (!action) {
@@ -521,7 +572,7 @@ async function main() {
521
572
  if (cmd === 'live') {
522
573
  const action = cleanOpt(args[1] ?? null);
523
574
  if (!action)
524
- throw new Error('Usage: live <init|list|browse|search|resolve|preview|preview-clear|fetch|apply|auto-apply|favorite|remove|thumb|open|service-autostart|config|doctor> ...');
575
+ throw new Error('Usage: live <init|list|browse|search|resolve|preview|preview-clear|fetch|apply|auto-apply|favorite|remove|thumb|open|service-autostart|config|view|doctor> ...');
525
576
  if (action === 'init') {
526
577
  console.log(JSON.stringify((0, live_1.liveInit)(), null, 2));
527
578
  return;
@@ -713,7 +764,7 @@ async function main() {
713
764
  console.log(JSON.stringify((0, live_1.liveViewData)(), null, 2));
714
765
  return;
715
766
  }
716
- throw new Error('Usage: live <init|list|browse|search|resolve|preview|preview-clear|fetch|apply|auto-apply|favorite|remove|thumb|open|service-autostart|config|doctor> ...');
767
+ throw new Error('Usage: live <init|list|browse|search|resolve|preview|preview-clear|fetch|apply|auto-apply|favorite|remove|thumb|open|service-autostart|config|view|doctor> ...');
717
768
  }
718
769
  // Regular commands (need config/state)
719
770
  const config = (0, config_1.loadConfig)();
@@ -0,0 +1,416 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HOST_SERVICE_DEFS = exports.HOST_DEPENDENCY_DEFS = void 0;
4
+ exports.resolveHostBinPath = resolveHostBinPath;
5
+ exports.checkSetupDependency = checkSetupDependency;
6
+ exports.checkSetupService = checkSetupService;
7
+ exports.listSetupStatus = listSetupStatus;
8
+ exports.listSetupVersions = listSetupVersions;
9
+ exports.installSetupItem = installSetupItem;
10
+ const node_fs_1 = require("node:fs");
11
+ const node_os_1 = require("node:os");
12
+ const node_path_1 = require("node:path");
13
+ const exec_1 = require("../utils/exec");
14
+ const init_1 = require("./init");
15
+ const systemd_1 = require("./systemd");
16
+ const ROOT_DIR = (0, node_path_1.resolve)(__dirname, '..', '..');
17
+ const RELEASE_CACHE = new Map();
18
+ exports.HOST_DEPENDENCY_DEFS = [
19
+ { id: 'kitowall', bin: 'kitowall', label: 'Kitowall CLI', installer: 'kitowall-only' },
20
+ { id: 'kitsune', bin: 'kitsune', label: 'Kitsune', installer: 'kitsune-only' },
21
+ { id: 'kitsune-rendercore', bin: 'kitsune-rendercore', label: 'Kitsune RenderCore', installer: 'kitsune-only' },
22
+ { id: 'swww', bin: 'swww', label: 'swww', installer: 'swww', system: true },
23
+ { id: 'swww-daemon', bin: 'swww-daemon', label: 'swww-daemon', installer: 'swww-daemon', system: true },
24
+ { id: 'hyprctl', bin: 'hyprctl', label: 'hyprctl', installer: 'hyprctl', system: true },
25
+ { id: 'cava', bin: 'cava', label: 'cava', installer: 'cava', system: true }
26
+ ];
27
+ exports.HOST_SERVICE_DEFS = [
28
+ { id: 'kitowall-config', label: 'Kitowall config', installer: 'kitowall-config' },
29
+ { id: 'kitowall-next.timer', label: 'kitowall-next.timer', installer: 'kitowall-next.timer' },
30
+ { id: 'kitsune-rendercore.service', label: 'kitsune-rendercore.service', installer: 'kitsune-rendercore.service' }
31
+ ];
32
+ function homeDir() {
33
+ return process.env.HOME || (0, node_os_1.homedir)();
34
+ }
35
+ async function fileExists(targetPath) {
36
+ try {
37
+ await node_fs_1.promises.access(targetPath);
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ function normalizeReleaseVersion(input) {
45
+ return String(input ?? '').trim().replace(/^v/, '');
46
+ }
47
+ function uniqueEntries(values) {
48
+ return [...new Set(values.map(value => String(value).trim()).filter(Boolean))];
49
+ }
50
+ function hostPathEntries() {
51
+ const home = homeDir();
52
+ return uniqueEntries([
53
+ (0, node_path_1.join)(home, '.local', 'bin'),
54
+ (0, node_path_1.join)(home, '.npm-global', 'bin'),
55
+ (0, node_path_1.join)(home, '.cargo', 'bin'),
56
+ ...(process.env.PATH ? process.env.PATH.split(':') : []),
57
+ '/usr/local/bin',
58
+ '/usr/bin',
59
+ '/bin'
60
+ ]);
61
+ }
62
+ async function resolveBinFromPathEntries(bin) {
63
+ for (const dir of hostPathEntries()) {
64
+ const candidate = (0, node_path_1.join)(dir, bin);
65
+ if (await fileExists(candidate))
66
+ return candidate;
67
+ }
68
+ return '';
69
+ }
70
+ async function collectNpmPrefixBinDirs() {
71
+ const dirs = [];
72
+ const home = homeDir();
73
+ for (const args of [
74
+ ['config', 'get', 'prefix'],
75
+ ['prefix', '-g']
76
+ ]) {
77
+ try {
78
+ const out = await (0, exec_1.run)('npm', args, {
79
+ env: {
80
+ ...process.env,
81
+ HOME: home,
82
+ PATH: hostPathEntries().join(':')
83
+ },
84
+ timeoutMs: 1500
85
+ });
86
+ const prefix = out.stdout.trim();
87
+ if (!prefix || prefix === 'undefined' || prefix === 'null')
88
+ continue;
89
+ const candidate = (0, node_path_1.join)(prefix, 'bin');
90
+ if (await fileExists(candidate)) {
91
+ dirs.push(candidate);
92
+ continue;
93
+ }
94
+ if (await fileExists(prefix))
95
+ dirs.push(prefix);
96
+ }
97
+ catch {
98
+ // ignore
99
+ }
100
+ }
101
+ return uniqueEntries(dirs);
102
+ }
103
+ async function resolveHostBinPath(bin) {
104
+ const home = homeDir();
105
+ const fallback = {
106
+ kitowall: [
107
+ (0, node_path_1.join)(home, '.local', 'bin', 'kitowall'),
108
+ (0, node_path_1.join)(home, '.npm-global', 'bin', 'kitowall'),
109
+ (0, node_path_1.join)(home, 'node_modules', '.bin', 'kitowall')
110
+ ],
111
+ kitsune: [
112
+ (0, node_path_1.join)(home, '.cargo', 'bin', 'kitsune'),
113
+ (0, node_path_1.join)(home, '.local', 'bin', 'kitsune'),
114
+ (0, node_path_1.join)(home, '.local', 'share', 'kitsune', 'bin', 'kitsune')
115
+ ],
116
+ 'kitsune-rendercore': [
117
+ (0, node_path_1.join)(home, '.cargo', 'bin', 'kitsune-rendercore'),
118
+ (0, node_path_1.join)(home, '.local', 'bin', 'kitsune-rendercore')
119
+ ]
120
+ }[bin] ?? [(0, node_path_1.join)(home, '.local', 'bin', bin)];
121
+ for (const candidate of fallback) {
122
+ if (await fileExists(candidate))
123
+ return candidate;
124
+ }
125
+ const byPath = await resolveBinFromPathEntries(bin);
126
+ if (byPath)
127
+ return byPath;
128
+ if (bin === 'kitowall') {
129
+ for (const dir of await collectNpmPrefixBinDirs()) {
130
+ const candidate = (0, node_path_1.join)(dir, bin);
131
+ if (await fileExists(candidate))
132
+ return candidate;
133
+ }
134
+ }
135
+ return '';
136
+ }
137
+ function bootstrapHostPath() {
138
+ return (0, node_path_1.join)(ROOT_DIR, 'scripts', 'bootstrap-host.sh');
139
+ }
140
+ function bootstrapSystemPath() {
141
+ return (0, node_path_1.join)(ROOT_DIR, 'scripts', 'bootstrap-system.sh');
142
+ }
143
+ async function assertBootstrapScriptExists(targetPath, label) {
144
+ if (await fileExists(targetPath))
145
+ return;
146
+ throw new Error(`${label} not found at ${targetPath}. Reinstala el CLI con: npm i -g --prefix ~/.local kitowall@latest`);
147
+ }
148
+ async function bootstrapVersionsPath() {
149
+ return (0, node_path_1.join)(homeDir(), '.local', 'share', 'kitowall', 'bootstrap-versions.json');
150
+ }
151
+ async function readBootstrapVersion(component) {
152
+ try {
153
+ const raw = await node_fs_1.promises.readFile(await bootstrapVersionsPath(), 'utf8');
154
+ const data = JSON.parse(raw);
155
+ const version = String(data?.[component]?.version ?? '').trim();
156
+ return version || null;
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ async function readCliVersion(binPath) {
163
+ try {
164
+ const realPath = await node_fs_1.promises.realpath(binPath);
165
+ const pkgPath = (0, node_path_1.join)((0, node_path_1.dirname)(realPath), '..', 'package.json');
166
+ const raw = await node_fs_1.promises.readFile(pkgPath, 'utf8');
167
+ const parsed = JSON.parse(raw);
168
+ const value = normalizeReleaseVersion(parsed.version ?? '');
169
+ if (value)
170
+ return value;
171
+ }
172
+ catch {
173
+ // fallback to executing the bin
174
+ }
175
+ try {
176
+ const out = await (0, exec_1.run)(binPath, ['--version'], {
177
+ env: {
178
+ ...process.env,
179
+ HOME: homeDir(),
180
+ PATH: hostPathEntries().join(':')
181
+ },
182
+ timeoutMs: 1500
183
+ });
184
+ const value = normalizeReleaseVersion(out.stdout.trim());
185
+ return value || null;
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ async function githubLatestReleaseTag(repo) {
192
+ const cached = RELEASE_CACHE.get(repo);
193
+ if (cached && Date.now() - cached.at < 300000)
194
+ return cached.value;
195
+ try {
196
+ const out = await (0, exec_1.run)('curl', ['-fsSL', '--connect-timeout', '1', '--max-time', '2', '-H', 'User-Agent: kitowall-cli', `https://api.github.com/repos/${repo}/releases/latest`], {
197
+ env: {
198
+ ...process.env,
199
+ HOME: homeDir(),
200
+ PATH: hostPathEntries().join(':')
201
+ },
202
+ timeoutMs: 2500
203
+ });
204
+ const data = JSON.parse(out.stdout);
205
+ const value = normalizeReleaseVersion(data?.tag_name ?? '') || null;
206
+ RELEASE_CACHE.set(repo, { at: Date.now(), value });
207
+ return value;
208
+ }
209
+ catch {
210
+ RELEASE_CACHE.set(repo, { at: Date.now(), value: null });
211
+ return null;
212
+ }
213
+ }
214
+ async function componentUpdateInfo(id, binPath = '') {
215
+ const repo = {
216
+ kitowall: 'KitotsuMolina/Kitowall',
217
+ kitsune: 'KitotsuMolina/Kitsune',
218
+ 'kitsune-rendercore': 'KitotsuMolina/Kitsune-RenderCore'
219
+ }[id];
220
+ let localVersion = null;
221
+ if (id === 'kitowall' && binPath) {
222
+ localVersion = await readCliVersion(binPath);
223
+ }
224
+ else {
225
+ localVersion = await readBootstrapVersion(id);
226
+ }
227
+ const latestVersion = repo ? await githubLatestReleaseTag(repo) : null;
228
+ return {
229
+ local_version: localVersion,
230
+ latest_version: latestVersion,
231
+ update_available: !!(localVersion && latestVersion && localVersion !== latestVersion)
232
+ };
233
+ }
234
+ async function maybeSystemctlShow(unit, props) {
235
+ try {
236
+ const args = ['--user', 'show', unit, '--no-pager'];
237
+ for (const prop of props)
238
+ args.push('-p', prop);
239
+ const out = await (0, exec_1.run)('systemctl', args, {
240
+ env: {
241
+ ...process.env,
242
+ HOME: homeDir(),
243
+ PATH: hostPathEntries().join(':')
244
+ },
245
+ timeoutMs: 1500
246
+ });
247
+ const result = {};
248
+ for (const line of out.stdout.split('\n')) {
249
+ const idx = line.indexOf('=');
250
+ if (idx > -1)
251
+ result[line.slice(0, idx)] = line.slice(idx + 1);
252
+ }
253
+ return result;
254
+ }
255
+ catch {
256
+ return null;
257
+ }
258
+ }
259
+ async function checkSetupDependency(id) {
260
+ const def = exports.HOST_DEPENDENCY_DEFS.find(item => item.id === id);
261
+ if (!def)
262
+ throw new Error(`Unknown dependency id: ${id}`);
263
+ let binPath = '';
264
+ let error = '';
265
+ try {
266
+ binPath = await resolveHostBinPath(def.bin);
267
+ }
268
+ catch (err) {
269
+ error = err instanceof Error ? err.message : String(err);
270
+ }
271
+ let update = { local_version: null, latest_version: null, update_available: false };
272
+ try {
273
+ if (def.id === 'kitowall' || def.id === 'kitsune' || def.id === 'kitsune-rendercore') {
274
+ update = await componentUpdateInfo(def.id, binPath);
275
+ }
276
+ }
277
+ catch (err) {
278
+ if (!error)
279
+ error = err instanceof Error ? err.message : String(err);
280
+ }
281
+ return {
282
+ id: def.id,
283
+ label: def.label,
284
+ installed: !!binPath,
285
+ state: binPath ? 'ok' : error ? 'error' : 'missing',
286
+ path: binPath,
287
+ detail: binPath ? `path=${binPath}` : 'binary not found',
288
+ error,
289
+ update
290
+ };
291
+ }
292
+ async function checkSetupService(id, namespace = 'kitowall') {
293
+ const def = exports.HOST_SERVICE_DEFS.find(item => item.id === id);
294
+ if (!def)
295
+ throw new Error(`Unknown service id: ${id}`);
296
+ const home = homeDir();
297
+ const configPath = (0, node_path_1.join)(home, '.config', 'kitowall', 'config.json');
298
+ const timerUnitPath = (0, node_path_1.join)(home, '.config', 'systemd', 'user', 'kitowall-next.timer');
299
+ const rendercoreUnitPath = (0, node_path_1.join)(home, '.config', 'systemd', 'user', 'kitsune-rendercore.service');
300
+ if (id === 'kitowall-config') {
301
+ const installed = await fileExists(configPath);
302
+ return {
303
+ id,
304
+ label: def.label,
305
+ installed,
306
+ state: installed ? 'ok' : 'missing',
307
+ path: configPath,
308
+ detail: installed ? `namespace=${namespace}` : 'config.json missing',
309
+ error: ''
310
+ };
311
+ }
312
+ if (id === 'kitowall-next.timer') {
313
+ const installed = await fileExists(timerUnitPath);
314
+ const status = installed ? await maybeSystemctlShow('kitowall-next.timer', ['ActiveState', 'UnitFileState']) : null;
315
+ return {
316
+ id,
317
+ label: def.label,
318
+ installed,
319
+ state: installed ? 'ok' : 'missing',
320
+ path: timerUnitPath,
321
+ detail: installed
322
+ ? (status
323
+ ? `file=${timerUnitPath} active=${String(status.ActiveState ?? 'n/a')} unit=${String(status.UnitFileState ?? 'n/a')}`
324
+ : `file=${timerUnitPath} status=unavailable`)
325
+ : 'timer unit file missing',
326
+ error: ''
327
+ };
328
+ }
329
+ if (id === 'kitsune-rendercore.service') {
330
+ const installed = await fileExists(rendercoreUnitPath);
331
+ const status = installed ? await maybeSystemctlShow('kitsune-rendercore.service', ['ActiveState', 'UnitFileState']) : null;
332
+ return {
333
+ id,
334
+ label: def.label,
335
+ installed,
336
+ state: installed ? 'ok' : 'missing',
337
+ path: rendercoreUnitPath,
338
+ detail: installed
339
+ ? (status
340
+ ? `file=${rendercoreUnitPath} active=${String(status.ActiveState ?? 'n/a')} unit=${String(status.UnitFileState ?? 'n/a')}`
341
+ : `file=${rendercoreUnitPath} status=unavailable`)
342
+ : 'service unit file missing',
343
+ error: ''
344
+ };
345
+ }
346
+ throw new Error(`Unknown service id: ${id}`);
347
+ }
348
+ async function listSetupStatus(namespace = 'kitowall') {
349
+ const [dependencies, services] = await Promise.all([
350
+ Promise.all(exports.HOST_DEPENDENCY_DEFS.map(item => checkSetupDependency(item.id))),
351
+ Promise.all(exports.HOST_SERVICE_DEFS.map(item => checkSetupService(item.id, namespace)))
352
+ ]);
353
+ return {
354
+ ok: dependencies.every(item => item.installed) && services.every(item => item.installed),
355
+ dependencies,
356
+ services,
357
+ counts: {
358
+ dependencies_missing: dependencies.filter(item => !item.installed).length,
359
+ services_missing: services.filter(item => !item.installed).length,
360
+ updates_available: dependencies.filter(item => item.update?.update_available === true).length
361
+ }
362
+ };
363
+ }
364
+ async function listSetupVersions() {
365
+ const items = {};
366
+ for (const id of ['kitowall', 'kitsune', 'kitsune-rendercore']) {
367
+ const dep = await checkSetupDependency(id);
368
+ items[id] = dep.update ?? { local_version: null, latest_version: null, update_available: false };
369
+ }
370
+ return { ok: true, items };
371
+ }
372
+ async function runBootstrapHostMode(mode) {
373
+ const scriptPath = bootstrapHostPath();
374
+ await assertBootstrapScriptExists(scriptPath, 'bootstrap-host.sh');
375
+ const out = await (0, exec_1.run)('bash', [scriptPath], {
376
+ env: {
377
+ ...process.env,
378
+ HOME: homeDir(),
379
+ PATH: hostPathEntries().join(':'),
380
+ KITOWALL_SKIP_SYSTEM_DEPS: '1',
381
+ KITOWALL_BOOTSTRAP_MODE: mode
382
+ }
383
+ });
384
+ return { ok: true, code: out.code, logs: `${out.stdout}${out.stderr}` };
385
+ }
386
+ async function runBootstrapSystemItems(ids) {
387
+ const scriptPath = bootstrapSystemPath();
388
+ await assertBootstrapScriptExists(scriptPath, 'bootstrap-system.sh');
389
+ const out = await (0, exec_1.run)('pkexec', ['bash', scriptPath, ...ids], {
390
+ env: {
391
+ ...process.env,
392
+ HOME: homeDir(),
393
+ PATH: hostPathEntries().join(':')
394
+ }
395
+ });
396
+ return { ok: true, code: out.code, logs: `${out.stdout}${out.stderr}` };
397
+ }
398
+ async function installSetupItem(id, namespace = 'kitowall') {
399
+ const dep = exports.HOST_DEPENDENCY_DEFS.find(item => item.id === id);
400
+ if (dep?.system)
401
+ return await runBootstrapSystemItems([id]);
402
+ if (id === 'kitowall')
403
+ return await runBootstrapHostMode('kitowall-only');
404
+ if (id === 'kitsune' || id === 'kitsune-rendercore' || id === 'kitsune-rendercore.service') {
405
+ return await runBootstrapHostMode('kitsune-only');
406
+ }
407
+ if (id === 'kitowall-config') {
408
+ await (0, init_1.initKitowall)({ namespace, apply: true, force: true });
409
+ return { ok: true, code: 0, logs: JSON.stringify({ ok: true, init: true, namespace }, null, 2) };
410
+ }
411
+ if (id === 'kitowall-next.timer') {
412
+ await (0, systemd_1.installSystemd)({ every: '600s' });
413
+ return { ok: true, code: 0, logs: JSON.stringify({ ok: true, installed: true, every: '600s' }, null, 2) };
414
+ }
415
+ throw new Error(`Unsupported setup item: ${id}`);
416
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kitowall",
3
- "version": "7.5.0",
3
+ "version": "7.9.0",
4
4
  "description": "CLI/daemon for Hyprland wallpapers using swww with pack-based rotation.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,6 +10,8 @@
10
10
  "type": "commonjs",
11
11
  "files": [
12
12
  "dist",
13
+ "scripts/bootstrap-host.sh",
14
+ "scripts/bootstrap-system.sh",
13
15
  "README.md",
14
16
  "LICENSE.md",
15
17
  "NOTICE.md",