@linxin666/dsh-client-ui-skin-center 0.1.6 → 0.1.7
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/lib/index.js +61 -17
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
2
|
import z from "schemastery";
|
|
3
|
-
import { lstatSync, mkdirSync, readFileSync, readdirSync,
|
|
3
|
+
import { lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -130,6 +130,7 @@ function listSkinDirCandidates(skinsDir) {
|
|
|
130
130
|
const isDir = (p) => statSync(p, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
131
131
|
for (const dir of entries) {
|
|
132
132
|
const candidate = join(skinsDir, dir);
|
|
133
|
+
if (lstatSync(candidate, { throwIfNoEntry: false })?.isSymbolicLink() === true) continue;
|
|
133
134
|
if (!isDir(candidate)) continue;
|
|
134
135
|
if (statSync(join(candidate, "skin.json"), { throwIfNoEntry: false })) out.push(candidate);
|
|
135
136
|
}
|
|
@@ -159,7 +160,19 @@ function listSkinDirCandidates(skinsDir) {
|
|
|
159
160
|
*/
|
|
160
161
|
function loadRegistry(skinsDir = SKINS_DIR) {
|
|
161
162
|
const out = {};
|
|
163
|
+
const seenReal = /* @__PURE__ */ new Set();
|
|
162
164
|
for (const dir of listSkinDirCandidates(skinsDir)) {
|
|
165
|
+
let real;
|
|
166
|
+
try {
|
|
167
|
+
real = realpathSync(dir);
|
|
168
|
+
} catch {
|
|
169
|
+
real = dir;
|
|
170
|
+
}
|
|
171
|
+
if (seenReal.has(real)) {
|
|
172
|
+
console.warn("[skin-center] duplicate skin dir (realpath) \"" + real + "\": keeping the real directory, ignoring " + dir);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
seenReal.add(real);
|
|
163
176
|
const meta = readSkinMeta(dir);
|
|
164
177
|
if (meta === null || meta.wiring === void 0 || meta.package === void 0) continue;
|
|
165
178
|
if (out[meta.id] !== void 0) {
|
|
@@ -296,14 +309,30 @@ function writePatchAtomic(filePath, next) {
|
|
|
296
309
|
* @param entry - the skin switch entry.
|
|
297
310
|
* @param profileModulesDir - the profile's node_modules dir.
|
|
298
311
|
*/
|
|
312
|
+
/** Canonical path a symlink resolves to, tolerant of a degraded link (a
|
|
313
|
+
* self-referential link whose realpath would throw ELOOP); '' when absent. */
|
|
314
|
+
function resolveLinkReal(linkPath) {
|
|
315
|
+
try {
|
|
316
|
+
return realpathSync(linkPath);
|
|
317
|
+
} catch {
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
299
321
|
function ensureSymlink(entry, profileModulesDir) {
|
|
300
322
|
const target = join(profileModulesDir, entry.pkg);
|
|
323
|
+
let entryReal;
|
|
324
|
+
try {
|
|
325
|
+
entryReal = realpathSync(entry.dir);
|
|
326
|
+
} catch {
|
|
327
|
+
entryReal = entry.dir;
|
|
328
|
+
}
|
|
329
|
+
if (entry.dir === target || entryReal === target) return false;
|
|
301
330
|
let stat = null;
|
|
302
331
|
try {
|
|
303
332
|
stat = lstatSync(target);
|
|
304
333
|
} catch {}
|
|
305
334
|
if (stat) if (stat.isSymbolicLink()) {
|
|
306
|
-
if (
|
|
335
|
+
if (resolveLinkReal(target) === entryReal) return false;
|
|
307
336
|
if (process.platform === "win32" && stat.isDirectory()) rmdirSync(target);
|
|
308
337
|
else unlinkSync(target);
|
|
309
338
|
} else if (stat.isDirectory()) {
|
|
@@ -360,20 +389,37 @@ function symlinkFriendly(caller, fn) {
|
|
|
360
389
|
}
|
|
361
390
|
}
|
|
362
391
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
392
|
+
* Whether the skin package is actually resolvable as a plugin from the web
|
|
393
|
+
* profile - the same directory contract the boot graph relies on when it
|
|
394
|
+
* loads the `useSkin` insert row. Unlike the old soft warning, this is a
|
|
395
|
+
* hard gate: the skin-center /apply endpoint must not report ok:true for a
|
|
396
|
+
* skin the host cannot load. The npm aggregate layout shipped skin dirs
|
|
397
|
+
* without a package.json + host entry, so /apply wrote the patch, reported
|
|
398
|
+
* success, and the boot then died on MODULE_NOT_FOUND .../package.json.
|
|
399
|
+
*
|
|
400
|
+
* The check is structural and deterministic (pure fs): resolves what node
|
|
401
|
+
* would - the profile-target package dir must carry a package.json whose
|
|
402
|
+
* name is this skin's package, and a host entry (main, else index.js) that
|
|
403
|
+
* actually exists. That is exactly the resolution that failed before.
|
|
366
404
|
* @param entry - the skin switch entry.
|
|
367
405
|
* @param profileModulesDir - the profile's node_modules dir.
|
|
406
|
+
* @returns an error message when the skin is not resolvable, else null.
|
|
368
407
|
*/
|
|
369
|
-
function
|
|
370
|
-
|
|
408
|
+
function checkResolvable(entry, profileModulesDir) {
|
|
409
|
+
const target = join(profileModulesDir, entry.pkg);
|
|
410
|
+
if (!statSync(target, { throwIfNoEntry: false })?.isDirectory()) return `${entry.pkg} 未安装到 profile(profile 中无 ${target})。请先用 dsh-skin install ${entry.id.replace(/^ui-skin-/, "")} 安装,否则宿主无法加载。`;
|
|
411
|
+
const pkgPath = join(target, "package.json");
|
|
412
|
+
if (!statSync(pkgPath, { throwIfNoEntry: false })) return `${entry.pkg} 在 profile 中缺少 package.json(${pkgPath})——聚合包皮肤目录未带可解析包元数据。`;
|
|
413
|
+
let parsed;
|
|
371
414
|
try {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
415
|
+
parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
416
|
+
} catch {
|
|
417
|
+
parsed = {};
|
|
418
|
+
}
|
|
419
|
+
if (parsed.name !== entry.pkg) return `${entry.pkg} 解析到的 package.json 名为 ${String(parsed.name)},不是本皮肤(${pkgPath})。`;
|
|
420
|
+
const mainPath = join(target, typeof parsed.main === "string" ? parsed.main : "index.js");
|
|
421
|
+
if (!statSync(mainPath, { throwIfNoEntry: false })) return `${entry.pkg} 缺少 host 入口 ${mainPath}(package.json main 未指到可加载文件)。`;
|
|
422
|
+
return null;
|
|
377
423
|
}
|
|
378
424
|
/**
|
|
379
425
|
* Switch the active skin. Equivalent to `dsh-skin use <name>`:
|
|
@@ -389,19 +435,17 @@ function useSkin(name, opts = {}) {
|
|
|
389
435
|
const registry = opts.registry ?? loadRegistry();
|
|
390
436
|
if (!official && registry[name] === void 0) throw new Error(`unknown skin "${name}". Known: ${Object.keys(registry).join(", ")} (or "official" for the stock look)`);
|
|
391
437
|
const paths = resolvePaths(opts.home, opts.profile);
|
|
392
|
-
const notices = [];
|
|
393
438
|
if (!official) {
|
|
394
439
|
const entry = registry[name];
|
|
395
440
|
symlinkFriendly(`switching to "${name}"`, () => {
|
|
396
441
|
ensureSymlink(entry, paths.profileModulesDir);
|
|
397
442
|
});
|
|
398
|
-
const
|
|
399
|
-
if (
|
|
443
|
+
const problem = checkResolvable(entry, paths.profileModulesDir);
|
|
444
|
+
if (problem !== null) throw new Error(problem);
|
|
400
445
|
}
|
|
401
446
|
const next = `${stripLegacySkinRows(stripManaged(readPatch(paths.patchPath))).replace(/\s+$/, "")}\n\n${renderManaged(official ? null : name, registry)}\n`;
|
|
402
447
|
writePatchAtomic(paths.patchPath, next);
|
|
403
|
-
|
|
404
|
-
return notices.length ? `${core}\n${notices.join("\n")}` : core;
|
|
448
|
+
return official ? "restored the official stock look — the config watcher applies it within seconds; refresh the page to see it." : `skin switched to "${name}" — the config watcher applies it within seconds; refresh the page (or the manifest re-fetches) to see it.`;
|
|
405
449
|
}
|
|
406
450
|
/**
|
|
407
451
|
* Read the active skin, mirroring `dsh-skin current` (prints the name or
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-skin-center",
|
|
3
3
|
"description": "In-GUI skin center for the dsh web GUI: lists the official default plus every installed skin (qq98 / ths / xp / blue-fantasy / dragon-heir / minecraft), tries it on live inside the real GUI (executes the actual client bundle, light/dark preview), exits with a full restore, and applies in one click through the host /api/skin-center API (dsh-skin use, hot-reloaded, no restart)",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|