kitowall 2.1.0 → 2.2.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,9 @@ 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
130
135
  we coexist enter|exit|status Temporarily stop/restore wallpaper rotation services
131
136
  check [--namespace <ns>] [--json] Quick system check (no changes)
132
137
 
@@ -303,14 +308,26 @@ async function main() {
303
308
  }
304
309
  if (action === 'config') {
305
310
  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;
311
+ if (sub === 'set-api-key') {
312
+ const key = cleanOpt(args[3] ?? null);
313
+ if (!key)
314
+ throw new Error('Usage: we config set-api-key <key>');
315
+ (0, workshop_1.setWorkshopApiKey)(key);
316
+ console.log(JSON.stringify({ ok: true, updated: 'steamWebApiKey' }, null, 2));
317
+ return;
318
+ }
319
+ if (sub === 'get-steam-roots') {
320
+ console.log(JSON.stringify({ ok: true, ...(0, workshop_1.workshopGetSteamRoots)() }, null, 2));
321
+ return;
322
+ }
323
+ if (sub === 'set-steam-roots') {
324
+ const raw = cleanOpt(args[3] ?? null) ?? '';
325
+ const roots = raw ? raw.split(',').map(v => v.trim()).filter(Boolean) : [];
326
+ const out = (0, workshop_1.workshopSetSteamRoots)(roots);
327
+ console.log(JSON.stringify(out, null, 2));
328
+ return;
329
+ }
330
+ throw new Error('Usage: we config <set-api-key|get-steam-roots|set-steam-roots> ...');
314
331
  }
315
332
  if (action === 'search') {
316
333
  const text = cleanOpt(getOptionValue(args, '--text'));
@@ -386,6 +403,21 @@ async function main() {
386
403
  console.log(JSON.stringify(out, null, 2));
387
404
  return;
388
405
  }
406
+ if (action === 'scan-steam') {
407
+ const out = (0, workshop_1.workshopScanSteamDownloads)();
408
+ console.log(JSON.stringify({ ok: true, ...out }, null, 2));
409
+ return;
410
+ }
411
+ if (action === 'sync-steam') {
412
+ const out = (0, workshop_1.workshopSyncSteamDownloads)();
413
+ console.log(JSON.stringify(out, null, 2));
414
+ return;
415
+ }
416
+ if (action === 'app-status') {
417
+ const out = (0, workshop_1.workshopWallpaperEngineStatus)();
418
+ console.log(JSON.stringify(out, null, 2));
419
+ return;
420
+ }
389
421
  if (action === 'coexist') {
390
422
  const sub = cleanOpt(args[2] ?? null);
391
423
  if (sub === 'enter') {
@@ -405,7 +437,7 @@ async function main() {
405
437
  }
406
438
  throw new Error('Usage: we coexist <enter|exit|status>');
407
439
  }
408
- throw new Error('Usage: we <config|search|details|download|job|jobs|library|run-job|coexist> ...');
440
+ throw new Error('Usage: we <config|search|details|download|job|jobs|library|scan-steam|sync-steam|app-status|run-job|coexist> ...');
409
441
  }
410
442
  // Regular commands (need config/state)
411
443
  const config = (0, config_1.loadConfig)();
@@ -4,6 +4,11 @@ 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;
@@ -74,6 +79,16 @@ function readWeConfig() {
74
79
  return {};
75
80
  }
76
81
  }
82
+ function normalizePath(input) {
83
+ const raw = clean(input) ?? '';
84
+ if (!raw)
85
+ return '';
86
+ if (raw === '~')
87
+ return node_os_1.default.homedir();
88
+ if (raw.startsWith('~/'))
89
+ return node_path_1.default.join(node_os_1.default.homedir(), raw.slice(2));
90
+ return raw;
91
+ }
77
92
  function setWorkshopApiKey(apiKey) {
78
93
  const key = clean(apiKey);
79
94
  if (!key)
@@ -84,6 +99,20 @@ function setWorkshopApiKey(apiKey) {
84
99
  (0, fs_1.writeJson)(p, current);
85
100
  return { ok: true };
86
101
  }
102
+ function workshopGetSteamRoots() {
103
+ const cfg = readWeConfig();
104
+ const steamRoots = Array.isArray(cfg.steamRoots)
105
+ ? cfg.steamRoots.map(v => normalizePath(String(v))).filter(Boolean)
106
+ : [];
107
+ return { steam_roots: Array.from(new Set(steamRoots)) };
108
+ }
109
+ function workshopSetSteamRoots(roots) {
110
+ const cfg = readWeConfig();
111
+ const normalized = roots.map(v => normalizePath(String(v))).filter(Boolean);
112
+ cfg.steamRoots = Array.from(new Set(normalized));
113
+ (0, fs_1.writeJson)(getWeConfigPath(), cfg);
114
+ return { ok: true, steam_roots: cfg.steamRoots };
115
+ }
87
116
  function getSteamWebApiKey() {
88
117
  const envKey = clean(process.env.STEAM_WEB_API_KEY) ?? clean(process.env.KITOWALL_STEAM_WEB_API_KEY);
89
118
  if (envKey)
@@ -322,6 +351,229 @@ function writeMetaFile(meta) {
322
351
  cached_at: now()
323
352
  });
324
353
  }
354
+ function readLibraryFoldersVdf(vdfPath) {
355
+ if (!node_fs_1.default.existsSync(vdfPath))
356
+ return [];
357
+ const raw = node_fs_1.default.readFileSync(vdfPath, 'utf8');
358
+ const out = [];
359
+ const lines = raw.split('\n');
360
+ for (const line of lines) {
361
+ const m = line.match(/"path"\s+"([^"]+)"/);
362
+ if (!m || !m[1])
363
+ continue;
364
+ const candidate = m[1].replace(/\\\\/g, '\\');
365
+ out.push(candidate);
366
+ }
367
+ return out;
368
+ }
369
+ function getDefaultSteamRoots() {
370
+ const home = node_os_1.default.homedir();
371
+ const roots = [
372
+ node_path_1.default.join(home, '.steam', 'steam'),
373
+ node_path_1.default.join(home, '.local', 'share', 'Steam'),
374
+ node_path_1.default.join(home, '.var', 'app', 'com.valvesoftware.Steam', '.local', 'share', 'Steam')
375
+ ];
376
+ return Array.from(new Set(roots));
377
+ }
378
+ function getManualSteamRoots() {
379
+ const cfg = readWeConfig();
380
+ if (!Array.isArray(cfg.steamRoots))
381
+ return [];
382
+ return cfg.steamRoots.map(v => normalizePath(String(v))).filter(Boolean);
383
+ }
384
+ function detectSteamappsDirs() {
385
+ const steamappsSet = new Set();
386
+ const candidateRoots = [...getDefaultSteamRoots(), ...getManualSteamRoots()];
387
+ for (const root of candidateRoots) {
388
+ const p = normalizePath(root);
389
+ if (!p || !node_fs_1.default.existsSync(p))
390
+ continue;
391
+ if (node_path_1.default.basename(p) === 'steamapps' && node_fs_1.default.existsSync(p)) {
392
+ steamappsSet.add(p);
393
+ }
394
+ const directSteamapps = node_path_1.default.join(p, 'steamapps');
395
+ if (node_fs_1.default.existsSync(directSteamapps)) {
396
+ steamappsSet.add(directSteamapps);
397
+ }
398
+ 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}`;
399
+ const idx = p.indexOf(marker);
400
+ if (idx >= 0) {
401
+ const steamapps = p.slice(0, idx + `${node_path_1.default.sep}steamapps`.length);
402
+ if (node_fs_1.default.existsSync(steamapps))
403
+ steamappsSet.add(steamapps);
404
+ }
405
+ const markerNoSteamapps = `${node_path_1.default.sep}workshop${node_path_1.default.sep}content${node_path_1.default.sep}${WE_APP_ID}`;
406
+ const idx2 = p.indexOf(markerNoSteamapps);
407
+ if (idx2 >= 0) {
408
+ const steamappsMaybe = p.slice(0, idx2);
409
+ if (node_path_1.default.basename(steamappsMaybe) === 'steamapps' && node_fs_1.default.existsSync(steamappsMaybe)) {
410
+ steamappsSet.add(steamappsMaybe);
411
+ }
412
+ }
413
+ }
414
+ const baseSteamapps = Array.from(steamappsSet);
415
+ for (const steamapps of baseSteamapps) {
416
+ const vdf = node_path_1.default.join(steamapps, 'libraryfolders.vdf');
417
+ for (const lib of readLibraryFoldersVdf(vdf)) {
418
+ const candidate = node_path_1.default.join(lib, 'steamapps');
419
+ if (node_fs_1.default.existsSync(candidate))
420
+ steamappsSet.add(candidate);
421
+ }
422
+ }
423
+ return Array.from(steamappsSet);
424
+ }
425
+ function toWorkshopAppContentDir(inputPath) {
426
+ const p = normalizePath(inputPath);
427
+ if (!p)
428
+ return '';
429
+ const direct = p;
430
+ const asSteamRoot = node_path_1.default.join(p, 'steamapps', 'workshop', 'content', String(WE_APP_ID));
431
+ const asSteamappsRoot = node_path_1.default.join(p, 'workshop', 'content', String(WE_APP_ID));
432
+ const asContentRoot = node_path_1.default.join(p, String(WE_APP_ID));
433
+ if (node_fs_1.default.existsSync(direct) && direct.endsWith(`${node_path_1.default.sep}${WE_APP_ID}`))
434
+ return direct;
435
+ 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}`))
436
+ return direct;
437
+ if (node_fs_1.default.existsSync(asSteamRoot))
438
+ return asSteamRoot;
439
+ if (node_fs_1.default.existsSync(asSteamappsRoot))
440
+ return asSteamappsRoot;
441
+ if (node_fs_1.default.existsSync(asContentRoot))
442
+ return asContentRoot;
443
+ return '';
444
+ }
445
+ function detectSteamWorkshopContentDirs() {
446
+ const discovered = new Set();
447
+ for (const root of getManualSteamRoots()) {
448
+ const fromManual = toWorkshopAppContentDir(root);
449
+ if (fromManual && node_fs_1.default.existsSync(fromManual))
450
+ discovered.add(fromManual);
451
+ }
452
+ for (const steamapps of detectSteamappsDirs()) {
453
+ const candidate = node_path_1.default.join(steamapps, 'workshop', 'content', String(WE_APP_ID));
454
+ if (node_fs_1.default.existsSync(candidate)) {
455
+ discovered.add(candidate);
456
+ }
457
+ }
458
+ return Array.from(discovered);
459
+ }
460
+ function workshopWallpaperEngineStatus() {
461
+ const manifests = [];
462
+ const steamapps = detectSteamappsDirs();
463
+ for (const dir of steamapps) {
464
+ const manifest = node_path_1.default.join(dir, `appmanifest_${WE_APP_ID}.acf`);
465
+ if (node_fs_1.default.existsSync(manifest))
466
+ manifests.push(manifest);
467
+ }
468
+ return {
469
+ ok: true,
470
+ installed: manifests.length > 0,
471
+ manifests,
472
+ steamapps
473
+ };
474
+ }
475
+ function findPreviewCandidate(dir) {
476
+ if (!node_fs_1.default.existsSync(dir))
477
+ return undefined;
478
+ const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
479
+ const preferred = ['preview.gif', 'preview.webm', 'preview.mp4', 'preview.jpg', 'preview.png', 'preview.webp'];
480
+ for (const name of preferred) {
481
+ const full = node_path_1.default.join(dir, name);
482
+ if (node_fs_1.default.existsSync(full) && node_fs_1.default.statSync(full).isFile())
483
+ return full;
484
+ }
485
+ for (const entry of entries) {
486
+ if (!entry.isFile())
487
+ continue;
488
+ const lower = entry.name.toLowerCase();
489
+ if (lower.startsWith('preview.') || lower.startsWith('thumbnail.')) {
490
+ return node_path_1.default.join(dir, entry.name);
491
+ }
492
+ }
493
+ return undefined;
494
+ }
495
+ function workshopScanSteamDownloads() {
496
+ const sources = detectSteamWorkshopContentDirs();
497
+ const idsSet = new Set();
498
+ for (const source of sources) {
499
+ if (!node_fs_1.default.existsSync(source))
500
+ continue;
501
+ const entries = node_fs_1.default.readdirSync(source, { withFileTypes: true });
502
+ for (const entry of entries) {
503
+ if (!entry.isDirectory())
504
+ continue;
505
+ const id = clean(entry.name);
506
+ if (!id)
507
+ continue;
508
+ idsSet.add(id);
509
+ }
510
+ }
511
+ const ids = Array.from(idsSet).sort();
512
+ return { sources, count: ids.length, ids };
513
+ }
514
+ function workshopSyncSteamDownloads() {
515
+ const app = workshopWallpaperEngineStatus();
516
+ if (!app.installed) {
517
+ throw new Error('Wallpaper Engine (AppID 431960) is not installed in Steam. Install it first to sync downloads.');
518
+ }
519
+ const paths = getWePaths();
520
+ ensureWePaths(paths);
521
+ const sources = detectSteamWorkshopContentDirs();
522
+ let imported = 0;
523
+ let skipped = 0;
524
+ const seen = new Set();
525
+ for (const source of sources) {
526
+ if (!node_fs_1.default.existsSync(source))
527
+ continue;
528
+ const entries = node_fs_1.default.readdirSync(source, { withFileTypes: true });
529
+ for (const entry of entries) {
530
+ if (!entry.isDirectory())
531
+ continue;
532
+ const id = clean(entry.name);
533
+ if (!id || seen.has(id))
534
+ continue;
535
+ seen.add(id);
536
+ const sourceDir = node_path_1.default.join(source, id);
537
+ const targetDir = node_path_1.default.join(paths.downloads, id);
538
+ if (node_fs_1.default.existsSync(targetDir)) {
539
+ skipped += 1;
540
+ }
541
+ else {
542
+ node_fs_1.default.cpSync(sourceDir, targetDir, { recursive: true });
543
+ imported += 1;
544
+ }
545
+ const metadataFile = node_path_1.default.join(paths.metadata, `${id}.json`);
546
+ if (!node_fs_1.default.existsSync(metadataFile)) {
547
+ const previewCandidate = findPreviewCandidate(sourceDir);
548
+ let thumbLocal;
549
+ if (previewCandidate && node_fs_1.default.existsSync(previewCandidate)) {
550
+ const ext = node_path_1.default.extname(previewCandidate) || '.jpg';
551
+ const previewDir = node_path_1.default.join(paths.previews, id);
552
+ (0, fs_1.ensureDir)(previewDir);
553
+ thumbLocal = node_path_1.default.join(previewDir, `thumb${ext}`);
554
+ if (!node_fs_1.default.existsSync(thumbLocal)) {
555
+ node_fs_1.default.copyFileSync(previewCandidate, thumbLocal);
556
+ }
557
+ }
558
+ writeMetaFile({
559
+ id,
560
+ title: `Wallpaper ${id}`,
561
+ tags: [],
562
+ preview_thumb_local: thumbLocal,
563
+ author_name: 'Steam Workshop',
564
+ time_updated: Math.floor(Date.now() / 1000)
565
+ });
566
+ }
567
+ }
568
+ }
569
+ return {
570
+ ok: true,
571
+ sources,
572
+ imported,
573
+ skipped,
574
+ total: imported + skipped
575
+ };
576
+ }
325
577
  async function workshopSearch(input) {
326
578
  const paths = getWePaths();
327
579
  ensureWePaths(paths);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kitowall",
3
- "version": "2.1.0",
3
+ "version": "2.2.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",