kitowall 2.1.0 → 2.3.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/dist/cli.js CHANGED
@@ -118,6 +118,8 @@ Commands:
118
118
  Show system logs (requests/downloads/errors)
119
119
  logs clear Clear system logs
120
120
  we config set-api-key <key> Save Steam Web API key (~/.config/kitowall/we.json)
121
+ we config get-steam-roots Show configured manual Steam roots
122
+ we config set-steam-roots <a,b,c> Save manual Steam roots (steam root or workshop/content/431960)
121
123
  we search [--text <q>] [--tags <a,b>] [--sort <top|newest|trend|subscribed|updated>] [--page <n>] [--page-size <n>] [--days <n>] [--fixtures]
122
124
  Search Wallpaper Engine workshop items (appid 431960)
123
125
  we details <publishedfileid> [--fixtures]
@@ -127,6 +129,11 @@ Commands:
127
129
  we job <job_id> Show one download job
128
130
  we jobs [--limit <n>] List recent download jobs
129
131
  we library List downloaded workshop items
132
+ we scan-steam Detect Steam workshop folders and list downloaded ids
133
+ we sync-steam Sync local Steam Workshop 431960 items into Kitsune downloads
134
+ we app-status Detect if Wallpaper Engine (AppID 431960) is installed
135
+ we active Show current livewallpaper authority/lock state
136
+ we stop [--monitor <name> | --all] Stop livewallpaper instances and restore previous services
130
137
  we coexist enter|exit|status Temporarily stop/restore wallpaper rotation services
131
138
  check [--namespace <ns>] [--json] Quick system check (no changes)
132
139
 
@@ -303,14 +310,26 @@ async function main() {
303
310
  }
304
311
  if (action === 'config') {
305
312
  const sub = cleanOpt(args[2] ?? null);
306
- if (sub !== 'set-api-key')
307
- throw new Error('Usage: we config set-api-key <key>');
308
- const key = cleanOpt(args[3] ?? null);
309
- if (!key)
310
- throw new Error('Usage: we config set-api-key <key>');
311
- (0, workshop_1.setWorkshopApiKey)(key);
312
- console.log(JSON.stringify({ ok: true, updated: 'steamWebApiKey' }, null, 2));
313
- return;
313
+ if (sub === 'set-api-key') {
314
+ const key = cleanOpt(args[3] ?? null);
315
+ if (!key)
316
+ throw new Error('Usage: we config set-api-key <key>');
317
+ (0, workshop_1.setWorkshopApiKey)(key);
318
+ console.log(JSON.stringify({ ok: true, updated: 'steamWebApiKey' }, null, 2));
319
+ return;
320
+ }
321
+ if (sub === 'get-steam-roots') {
322
+ console.log(JSON.stringify({ ok: true, ...(0, workshop_1.workshopGetSteamRoots)() }, null, 2));
323
+ return;
324
+ }
325
+ if (sub === 'set-steam-roots') {
326
+ const raw = cleanOpt(args[3] ?? null) ?? '';
327
+ const roots = raw ? raw.split(',').map(v => v.trim()).filter(Boolean) : [];
328
+ const out = (0, workshop_1.workshopSetSteamRoots)(roots);
329
+ console.log(JSON.stringify(out, null, 2));
330
+ return;
331
+ }
332
+ throw new Error('Usage: we config <set-api-key|get-steam-roots|set-steam-roots> ...');
314
333
  }
315
334
  if (action === 'search') {
316
335
  const text = cleanOpt(getOptionValue(args, '--text'));
@@ -386,6 +405,35 @@ async function main() {
386
405
  console.log(JSON.stringify(out, null, 2));
387
406
  return;
388
407
  }
408
+ if (action === 'scan-steam') {
409
+ const out = (0, workshop_1.workshopScanSteamDownloads)();
410
+ console.log(JSON.stringify({ ok: true, ...out }, null, 2));
411
+ return;
412
+ }
413
+ if (action === 'sync-steam') {
414
+ const out = (0, workshop_1.workshopSyncSteamDownloads)();
415
+ console.log(JSON.stringify(out, null, 2));
416
+ return;
417
+ }
418
+ if (action === 'app-status') {
419
+ const out = (0, workshop_1.workshopWallpaperEngineStatus)();
420
+ console.log(JSON.stringify(out, null, 2));
421
+ return;
422
+ }
423
+ if (action === 'active') {
424
+ const out = (0, workshop_1.workshopActiveStatus)();
425
+ console.log(JSON.stringify(out, null, 2));
426
+ return;
427
+ }
428
+ if (action === 'stop') {
429
+ const monitor = cleanOpt(getOptionValue(args, '--monitor'));
430
+ const out = await (0, workshop_1.workshopStop)({
431
+ monitor,
432
+ all: args.includes('--all') || !monitor
433
+ });
434
+ console.log(JSON.stringify(out, null, 2));
435
+ return;
436
+ }
389
437
  if (action === 'coexist') {
390
438
  const sub = cleanOpt(args[2] ?? null);
391
439
  if (sub === 'enter') {
@@ -405,7 +453,7 @@ async function main() {
405
453
  }
406
454
  throw new Error('Usage: we coexist <enter|exit|status>');
407
455
  }
408
- throw new Error('Usage: we <config|search|details|download|job|jobs|library|run-job|coexist> ...');
456
+ throw new Error('Usage: we <config|search|details|download|job|jobs|library|scan-steam|sync-steam|app-status|active|stop|run-job|coexist> ...');
409
457
  }
410
458
  // Regular commands (need config/state)
411
459
  const config = (0, config_1.loadConfig)();
@@ -4,12 +4,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.setWorkshopApiKey = setWorkshopApiKey;
7
+ exports.workshopGetSteamRoots = workshopGetSteamRoots;
8
+ exports.workshopSetSteamRoots = workshopSetSteamRoots;
9
+ exports.workshopWallpaperEngineStatus = workshopWallpaperEngineStatus;
10
+ exports.workshopScanSteamDownloads = workshopScanSteamDownloads;
11
+ exports.workshopSyncSteamDownloads = workshopSyncSteamDownloads;
7
12
  exports.workshopSearch = workshopSearch;
8
13
  exports.workshopDetails = workshopDetails;
9
14
  exports.workshopQueueDownload = workshopQueueDownload;
15
+ exports.workshopActiveStatus = workshopActiveStatus;
16
+ exports.workshopSetActive = workshopSetActive;
10
17
  exports.workshopCoexistenceEnter = workshopCoexistenceEnter;
11
18
  exports.workshopCoexistenceExit = workshopCoexistenceExit;
12
19
  exports.workshopCoexistenceStatus = workshopCoexistenceStatus;
20
+ exports.workshopStop = workshopStop;
13
21
  exports.workshopRunJob = workshopRunJob;
14
22
  exports.workshopGetJob = workshopGetJob;
15
23
  exports.workshopListJobs = workshopListJobs;
@@ -59,6 +67,7 @@ function ensureWePaths(paths) {
59
67
  (0, fs_1.ensureDir)(paths.runtime);
60
68
  (0, fs_1.ensureDir)(node_path_1.default.join(paths.cache, 'search'));
61
69
  (0, fs_1.ensureDir)(node_path_1.default.join(paths.steamcmd, 'logs'));
70
+ (0, fs_1.ensureDir)(node_path_1.default.join(paths.root, 'coexistence', 'snapshots'));
62
71
  }
63
72
  function getWeConfigPath() {
64
73
  return node_path_1.default.join(node_os_1.default.homedir(), '.config', 'kitowall', 'we.json');
@@ -74,6 +83,16 @@ function readWeConfig() {
74
83
  return {};
75
84
  }
76
85
  }
86
+ function normalizePath(input) {
87
+ const raw = clean(input) ?? '';
88
+ if (!raw)
89
+ return '';
90
+ if (raw === '~')
91
+ return node_os_1.default.homedir();
92
+ if (raw.startsWith('~/'))
93
+ return node_path_1.default.join(node_os_1.default.homedir(), raw.slice(2));
94
+ return raw;
95
+ }
77
96
  function setWorkshopApiKey(apiKey) {
78
97
  const key = clean(apiKey);
79
98
  if (!key)
@@ -84,6 +103,20 @@ function setWorkshopApiKey(apiKey) {
84
103
  (0, fs_1.writeJson)(p, current);
85
104
  return { ok: true };
86
105
  }
106
+ function workshopGetSteamRoots() {
107
+ const cfg = readWeConfig();
108
+ const steamRoots = Array.isArray(cfg.steamRoots)
109
+ ? cfg.steamRoots.map(v => normalizePath(String(v))).filter(Boolean)
110
+ : [];
111
+ return { steam_roots: Array.from(new Set(steamRoots)) };
112
+ }
113
+ function workshopSetSteamRoots(roots) {
114
+ const cfg = readWeConfig();
115
+ const normalized = roots.map(v => normalizePath(String(v))).filter(Boolean);
116
+ cfg.steamRoots = Array.from(new Set(normalized));
117
+ (0, fs_1.writeJson)(getWeConfigPath(), cfg);
118
+ return { ok: true, steam_roots: cfg.steamRoots };
119
+ }
87
120
  function getSteamWebApiKey() {
88
121
  const envKey = clean(process.env.STEAM_WEB_API_KEY) ?? clean(process.env.KITOWALL_STEAM_WEB_API_KEY);
89
122
  if (envKey)
@@ -322,6 +355,281 @@ function writeMetaFile(meta) {
322
355
  cached_at: now()
323
356
  });
324
357
  }
358
+ function normalizeWallpaperType(raw) {
359
+ const t = clean(raw)?.toLowerCase();
360
+ if (!t)
361
+ return 'unknown';
362
+ if (t === 'video')
363
+ return 'video';
364
+ if (t === 'scene')
365
+ return 'scene';
366
+ if (t === 'web')
367
+ return 'web';
368
+ if (t === 'application')
369
+ return 'application';
370
+ return 'unknown';
371
+ }
372
+ function detectAudioReactiveFromText(text) {
373
+ return /(audio|spectrum|visualizer|now[\s_-]?playing|media[\s_-]?integration|fft|bass|reactive)/i.test(text);
374
+ }
375
+ function readProjectInfo(dir) {
376
+ const projectPath = node_path_1.default.join(dir, 'project.json');
377
+ if (!node_fs_1.default.existsSync(projectPath)) {
378
+ return { type: 'unknown', audioReactive: false };
379
+ }
380
+ try {
381
+ const raw = node_fs_1.default.readFileSync(projectPath, 'utf8');
382
+ const json = JSON.parse(raw);
383
+ const type = normalizeWallpaperType(json.type);
384
+ const title = clean(json.title) ?? '';
385
+ const audioReactive = detectAudioReactiveFromText(`${title}\n${raw}`);
386
+ return { type, audioReactive };
387
+ }
388
+ catch {
389
+ return { type: 'unknown', audioReactive: false };
390
+ }
391
+ }
392
+ function readLibraryFoldersVdf(vdfPath) {
393
+ if (!node_fs_1.default.existsSync(vdfPath))
394
+ return [];
395
+ const raw = node_fs_1.default.readFileSync(vdfPath, 'utf8');
396
+ const out = [];
397
+ const lines = raw.split('\n');
398
+ for (const line of lines) {
399
+ const m = line.match(/"path"\s+"([^"]+)"/);
400
+ if (!m || !m[1])
401
+ continue;
402
+ const candidate = m[1].replace(/\\\\/g, '\\');
403
+ out.push(candidate);
404
+ }
405
+ return out;
406
+ }
407
+ function getDefaultSteamRoots() {
408
+ const home = node_os_1.default.homedir();
409
+ const roots = [
410
+ node_path_1.default.join(home, '.steam', 'steam'),
411
+ node_path_1.default.join(home, '.local', 'share', 'Steam'),
412
+ node_path_1.default.join(home, '.var', 'app', 'com.valvesoftware.Steam', '.local', 'share', 'Steam')
413
+ ];
414
+ return Array.from(new Set(roots));
415
+ }
416
+ function getManualSteamRoots() {
417
+ const cfg = readWeConfig();
418
+ if (!Array.isArray(cfg.steamRoots))
419
+ return [];
420
+ return cfg.steamRoots.map(v => normalizePath(String(v))).filter(Boolean);
421
+ }
422
+ function detectSteamappsDirs() {
423
+ const steamappsSet = new Set();
424
+ const candidateRoots = [...getDefaultSteamRoots(), ...getManualSteamRoots()];
425
+ for (const root of candidateRoots) {
426
+ const p = normalizePath(root);
427
+ if (!p || !node_fs_1.default.existsSync(p))
428
+ continue;
429
+ if (node_path_1.default.basename(p) === 'steamapps' && node_fs_1.default.existsSync(p)) {
430
+ steamappsSet.add(p);
431
+ }
432
+ const directSteamapps = node_path_1.default.join(p, 'steamapps');
433
+ if (node_fs_1.default.existsSync(directSteamapps)) {
434
+ steamappsSet.add(directSteamapps);
435
+ }
436
+ const marker = `${node_path_1.default.sep}steamapps${node_path_1.default.sep}workshop${node_path_1.default.sep}content${node_path_1.default.sep}${WE_APP_ID}`;
437
+ const idx = p.indexOf(marker);
438
+ if (idx >= 0) {
439
+ const steamapps = p.slice(0, idx + `${node_path_1.default.sep}steamapps`.length);
440
+ if (node_fs_1.default.existsSync(steamapps))
441
+ steamappsSet.add(steamapps);
442
+ }
443
+ const markerNoSteamapps = `${node_path_1.default.sep}workshop${node_path_1.default.sep}content${node_path_1.default.sep}${WE_APP_ID}`;
444
+ const idx2 = p.indexOf(markerNoSteamapps);
445
+ if (idx2 >= 0) {
446
+ const steamappsMaybe = p.slice(0, idx2);
447
+ if (node_path_1.default.basename(steamappsMaybe) === 'steamapps' && node_fs_1.default.existsSync(steamappsMaybe)) {
448
+ steamappsSet.add(steamappsMaybe);
449
+ }
450
+ }
451
+ }
452
+ const baseSteamapps = Array.from(steamappsSet);
453
+ for (const steamapps of baseSteamapps) {
454
+ const vdf = node_path_1.default.join(steamapps, 'libraryfolders.vdf');
455
+ for (const lib of readLibraryFoldersVdf(vdf)) {
456
+ const candidate = node_path_1.default.join(lib, 'steamapps');
457
+ if (node_fs_1.default.existsSync(candidate))
458
+ steamappsSet.add(candidate);
459
+ }
460
+ }
461
+ return Array.from(steamappsSet);
462
+ }
463
+ function toWorkshopAppContentDir(inputPath) {
464
+ const p = normalizePath(inputPath);
465
+ if (!p)
466
+ return '';
467
+ const direct = p;
468
+ const asSteamRoot = node_path_1.default.join(p, 'steamapps', 'workshop', 'content', String(WE_APP_ID));
469
+ const asSteamappsRoot = node_path_1.default.join(p, 'workshop', 'content', String(WE_APP_ID));
470
+ const asContentRoot = node_path_1.default.join(p, String(WE_APP_ID));
471
+ if (node_fs_1.default.existsSync(direct) && direct.endsWith(`${node_path_1.default.sep}${WE_APP_ID}`))
472
+ return direct;
473
+ if (node_fs_1.default.existsSync(direct) && direct.includes(`${node_path_1.default.sep}workshop${node_path_1.default.sep}content${node_path_1.default.sep}${WE_APP_ID}`))
474
+ return direct;
475
+ if (node_fs_1.default.existsSync(asSteamRoot))
476
+ return asSteamRoot;
477
+ if (node_fs_1.default.existsSync(asSteamappsRoot))
478
+ return asSteamappsRoot;
479
+ if (node_fs_1.default.existsSync(asContentRoot))
480
+ return asContentRoot;
481
+ return '';
482
+ }
483
+ function detectSteamWorkshopContentDirs() {
484
+ const discovered = new Set();
485
+ for (const root of getManualSteamRoots()) {
486
+ const fromManual = toWorkshopAppContentDir(root);
487
+ if (fromManual && node_fs_1.default.existsSync(fromManual))
488
+ discovered.add(fromManual);
489
+ }
490
+ for (const steamapps of detectSteamappsDirs()) {
491
+ const candidate = node_path_1.default.join(steamapps, 'workshop', 'content', String(WE_APP_ID));
492
+ if (node_fs_1.default.existsSync(candidate)) {
493
+ discovered.add(candidate);
494
+ }
495
+ }
496
+ return Array.from(discovered);
497
+ }
498
+ function workshopWallpaperEngineStatus() {
499
+ const manifests = [];
500
+ const steamapps = detectSteamappsDirs();
501
+ for (const dir of steamapps) {
502
+ const manifest = node_path_1.default.join(dir, `appmanifest_${WE_APP_ID}.acf`);
503
+ if (node_fs_1.default.existsSync(manifest))
504
+ manifests.push(manifest);
505
+ }
506
+ return {
507
+ ok: true,
508
+ installed: manifests.length > 0,
509
+ manifests,
510
+ steamapps
511
+ };
512
+ }
513
+ function findPreviewCandidate(dir) {
514
+ if (!node_fs_1.default.existsSync(dir))
515
+ return undefined;
516
+ const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
517
+ const preferred = ['preview.gif', 'preview.webm', 'preview.mp4', 'preview.jpg', 'preview.png', 'preview.webp'];
518
+ for (const name of preferred) {
519
+ const full = node_path_1.default.join(dir, name);
520
+ if (node_fs_1.default.existsSync(full) && node_fs_1.default.statSync(full).isFile())
521
+ return full;
522
+ }
523
+ for (const entry of entries) {
524
+ if (!entry.isFile())
525
+ continue;
526
+ const lower = entry.name.toLowerCase();
527
+ if (lower.startsWith('preview.') || lower.startsWith('thumbnail.')) {
528
+ return node_path_1.default.join(dir, entry.name);
529
+ }
530
+ }
531
+ return undefined;
532
+ }
533
+ function workshopScanSteamDownloads() {
534
+ const sources = detectSteamWorkshopContentDirs();
535
+ const idsSet = new Set();
536
+ for (const source of sources) {
537
+ if (!node_fs_1.default.existsSync(source))
538
+ continue;
539
+ const entries = node_fs_1.default.readdirSync(source, { withFileTypes: true });
540
+ for (const entry of entries) {
541
+ if (!entry.isDirectory())
542
+ continue;
543
+ const id = clean(entry.name);
544
+ if (!id)
545
+ continue;
546
+ idsSet.add(id);
547
+ }
548
+ }
549
+ const ids = Array.from(idsSet).sort();
550
+ return { sources, count: ids.length, ids };
551
+ }
552
+ function workshopSyncSteamDownloads() {
553
+ const app = workshopWallpaperEngineStatus();
554
+ if (!app.installed) {
555
+ throw new Error('Wallpaper Engine (AppID 431960) is not installed in Steam. Install it first to sync downloads.');
556
+ }
557
+ const paths = getWePaths();
558
+ ensureWePaths(paths);
559
+ const sources = detectSteamWorkshopContentDirs();
560
+ let imported = 0;
561
+ let skipped = 0;
562
+ const seen = new Set();
563
+ for (const source of sources) {
564
+ if (!node_fs_1.default.existsSync(source))
565
+ continue;
566
+ const entries = node_fs_1.default.readdirSync(source, { withFileTypes: true });
567
+ for (const entry of entries) {
568
+ if (!entry.isDirectory())
569
+ continue;
570
+ const id = clean(entry.name);
571
+ if (!id || seen.has(id))
572
+ continue;
573
+ seen.add(id);
574
+ const sourceDir = node_path_1.default.join(source, id);
575
+ const targetDir = node_path_1.default.join(paths.downloads, id);
576
+ if (node_fs_1.default.existsSync(targetDir)) {
577
+ skipped += 1;
578
+ }
579
+ else {
580
+ node_fs_1.default.cpSync(sourceDir, targetDir, { recursive: true });
581
+ imported += 1;
582
+ }
583
+ const metadataFile = node_path_1.default.join(paths.metadata, `${id}.json`);
584
+ const info = readProjectInfo(sourceDir);
585
+ if (!node_fs_1.default.existsSync(metadataFile)) {
586
+ const previewCandidate = findPreviewCandidate(sourceDir);
587
+ let thumbLocal;
588
+ if (previewCandidate && node_fs_1.default.existsSync(previewCandidate)) {
589
+ const ext = node_path_1.default.extname(previewCandidate) || '.jpg';
590
+ const previewDir = node_path_1.default.join(paths.previews, id);
591
+ (0, fs_1.ensureDir)(previewDir);
592
+ thumbLocal = node_path_1.default.join(previewDir, `thumb${ext}`);
593
+ if (!node_fs_1.default.existsSync(thumbLocal)) {
594
+ node_fs_1.default.copyFileSync(previewCandidate, thumbLocal);
595
+ }
596
+ }
597
+ writeMetaFile({
598
+ id,
599
+ title: `Wallpaper ${id}`,
600
+ tags: [],
601
+ preview_thumb_local: thumbLocal,
602
+ author_name: 'Steam Workshop',
603
+ time_updated: Math.floor(Date.now() / 1000),
604
+ wallpaper_type: info.type,
605
+ audio_reactive: info.audioReactive
606
+ });
607
+ }
608
+ else {
609
+ try {
610
+ const current = JSON.parse(node_fs_1.default.readFileSync(metadataFile, 'utf8'));
611
+ if (!current.wallpaper_type || current.wallpaper_type === 'unknown' || current.audio_reactive === undefined) {
612
+ writeMetaFile({
613
+ ...current,
614
+ wallpaper_type: current.wallpaper_type ?? info.type,
615
+ audio_reactive: current.audio_reactive ?? info.audioReactive
616
+ });
617
+ }
618
+ }
619
+ catch {
620
+ // ignore malformed metadata
621
+ }
622
+ }
623
+ }
624
+ }
625
+ return {
626
+ ok: true,
627
+ sources,
628
+ imported,
629
+ skipped,
630
+ total: imported + skipped
631
+ };
632
+ }
325
633
  async function workshopSearch(input) {
326
634
  const paths = getWePaths();
327
635
  ensureWePaths(paths);
@@ -561,6 +869,78 @@ async function copyDownloadedContent(job) {
561
869
  function snapshotFile(paths) {
562
870
  return node_path_1.default.join(paths.runtime, 'coexistence.snapshot.json');
563
871
  }
872
+ function snapshotDir(paths) {
873
+ return node_path_1.default.join(paths.root, 'coexistence', 'snapshots');
874
+ }
875
+ function activeLockFile(paths) {
876
+ return node_path_1.default.join(paths.runtime, 'active.lock');
877
+ }
878
+ function activeStateFile(paths) {
879
+ return node_path_1.default.join(paths.runtime, 'active.json');
880
+ }
881
+ function readActiveState() {
882
+ const paths = getWePaths();
883
+ ensureWePaths(paths);
884
+ const p = activeStateFile(paths);
885
+ if (!node_fs_1.default.existsSync(p))
886
+ return null;
887
+ try {
888
+ return JSON.parse(node_fs_1.default.readFileSync(p, 'utf8'));
889
+ }
890
+ catch {
891
+ return null;
892
+ }
893
+ }
894
+ function writeActiveState(state) {
895
+ const paths = getWePaths();
896
+ ensureWePaths(paths);
897
+ (0, fs_1.writeJson)(activeStateFile(paths), state);
898
+ node_fs_1.default.writeFileSync(activeLockFile(paths), `${state.mode}:${state.started_at}\n`, 'utf8');
899
+ }
900
+ function clearActiveState() {
901
+ const paths = getWePaths();
902
+ ensureWePaths(paths);
903
+ try {
904
+ node_fs_1.default.unlinkSync(activeStateFile(paths));
905
+ }
906
+ catch {
907
+ // ignore
908
+ }
909
+ try {
910
+ node_fs_1.default.unlinkSync(activeLockFile(paths));
911
+ }
912
+ catch {
913
+ // ignore
914
+ }
915
+ }
916
+ function killPid(pid) {
917
+ if (!pid || !Number.isFinite(pid) || pid <= 1)
918
+ return false;
919
+ try {
920
+ process.kill(pid, 'SIGTERM');
921
+ return true;
922
+ }
923
+ catch {
924
+ return false;
925
+ }
926
+ }
927
+ function workshopActiveStatus() {
928
+ const paths = getWePaths();
929
+ ensureWePaths(paths);
930
+ const state = readActiveState();
931
+ const lock = node_fs_1.default.existsSync(activeLockFile(paths));
932
+ return {
933
+ ok: true,
934
+ active: lock && !!state,
935
+ lock_path: activeLockFile(paths),
936
+ state_path: activeStateFile(paths),
937
+ state: state ?? undefined
938
+ };
939
+ }
940
+ function workshopSetActive(state) {
941
+ writeActiveState(state);
942
+ return { ok: true, active: true, state };
943
+ }
564
944
  async function isUnitActive(unit) {
565
945
  try {
566
946
  const out = await (0, exec_1.run)('systemctl', ['--user', 'show', unit, '--property', 'ActiveState', '--value']);
@@ -587,8 +967,11 @@ async function workshopCoexistenceEnter() {
587
967
  // best effort
588
968
  }
589
969
  }
590
- (0, fs_1.writeJson)(snapshotFile(paths), { ts: now(), active });
591
- return { ok: true, stopped: active, snapshot: active };
970
+ const snapshotId = String(now());
971
+ const snap = { id: snapshotId, ts: now(), active };
972
+ (0, fs_1.writeJson)(node_path_1.default.join(snapshotDir(paths), `${snapshotId}.json`), snap);
973
+ (0, fs_1.writeJson)(snapshotFile(paths), { id: snapshotId, ts: snap.ts });
974
+ return { ok: true, stopped: active, snapshot: active, snapshot_id: snapshotId };
592
975
  }
593
976
  async function workshopCoexistenceExit() {
594
977
  const paths = getWePaths();
@@ -597,7 +980,19 @@ async function workshopCoexistenceExit() {
597
980
  if (!node_fs_1.default.existsSync(snapPath))
598
981
  return { ok: true, restored: [] };
599
982
  const raw = JSON.parse(node_fs_1.default.readFileSync(snapPath, 'utf8'));
600
- const active = Array.isArray(raw.active) ? raw.active.map(v => String(v)) : [];
983
+ let active = Array.isArray(raw.active) ? raw.active.map(v => String(v)) : [];
984
+ if ((!active || active.length === 0) && raw.id) {
985
+ const historical = node_path_1.default.join(snapshotDir(paths), `${raw.id}.json`);
986
+ if (node_fs_1.default.existsSync(historical)) {
987
+ try {
988
+ const snap = JSON.parse(node_fs_1.default.readFileSync(historical, 'utf8'));
989
+ active = Array.isArray(snap.active) ? snap.active.map(v => String(v)) : [];
990
+ }
991
+ catch {
992
+ active = [];
993
+ }
994
+ }
995
+ }
601
996
  const restored = [];
602
997
  for (const unit of active) {
603
998
  try {
@@ -630,6 +1025,42 @@ async function workshopCoexistenceStatus() {
630
1025
  }
631
1026
  return { ok: true, snapshot, current };
632
1027
  }
1028
+ async function workshopStop(options) {
1029
+ const state = readActiveState();
1030
+ const hadActive = !!state;
1031
+ const stopped = [];
1032
+ if (state?.instances) {
1033
+ if (options?.monitor) {
1034
+ const inst = state.instances[options.monitor];
1035
+ if (killPid(inst?.pid))
1036
+ stopped.push(options.monitor);
1037
+ delete state.instances[options.monitor];
1038
+ if (Object.keys(state.instances).length > 0 && !options?.all) {
1039
+ writeActiveState(state);
1040
+ }
1041
+ else {
1042
+ clearActiveState();
1043
+ }
1044
+ }
1045
+ else {
1046
+ for (const [monitor, inst] of Object.entries(state.instances)) {
1047
+ if (killPid(inst?.pid))
1048
+ stopped.push(monitor);
1049
+ }
1050
+ clearActiveState();
1051
+ }
1052
+ }
1053
+ else if (options?.all) {
1054
+ clearActiveState();
1055
+ }
1056
+ const coexist = await workshopCoexistenceExit();
1057
+ return {
1058
+ ok: true,
1059
+ stopped_instances: stopped,
1060
+ restored_units: coexist.restored,
1061
+ had_active: hadActive
1062
+ };
1063
+ }
633
1064
  async function workshopRunJob(jobId) {
634
1065
  const job = readJob(jobId);
635
1066
  if (!job)
@@ -728,6 +1159,13 @@ function workshopLibrary() {
728
1159
  meta = undefined;
729
1160
  }
730
1161
  }
1162
+ const info = readProjectInfo(p);
1163
+ if (meta) {
1164
+ meta.wallpaper_type = meta.wallpaper_type ?? info.type;
1165
+ if (meta.audio_reactive === undefined) {
1166
+ meta.audio_reactive = info.audioReactive;
1167
+ }
1168
+ }
731
1169
  items.push({
732
1170
  id,
733
1171
  path: p,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kitowall",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "CLI/daemon for Hyprland wallpapers using swww with pack-based rotation.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "type": "commonjs",