dsh-client-ui-skin-tianlight 2.0.2 → 2.0.4
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 +24 -1
- package/bin/cli.mjs +361 -0
- package/bin/installer.mjs +487 -0
- package/build-info.json +3 -3
- package/docs/THIRD-PARTY.md +161 -0
- package/lib/index.js +1 -1
- package/lib/media-types.js +44 -7
- package/lib/pet-chat.js +40 -0
- package/lib/scene-project.js +3 -1
- package/lib/we-wallpaper/host.mjs +32 -0
- package/lib/web-project.js +6 -2
- package/package.json +14 -1
- package/skin.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
>
|
|
7
7
|
> npm 包名 `dsh-client-ui-skin-tianlight`(v2.0.2 起;旧链接安装包名 `@dsh-external/dsh-client-ui-skin-tianlight`)· 布线行 `ui-skin-tianlight` · body 属性 `data-dsh-tianlight`
|
|
8
8
|
|
|
9
|
+
## v2.0.4 · 安全修复版(建议升级)
|
|
10
|
+
|
|
11
|
+
修复了 v2.0.3 验收中发现的问题,其中最重要的一条:Wallpaper Engine 网页壁纸的页面以前会以 DSH 源的身份运行,现在一律沙箱化。2.0.2 和 2.0.3 都受影响,请升级:先完全退出 DSH,再运行 `npx dsh-client-ui-skin-tianlight@latest`。另外 `npx` 不再顺带下载整套 DSH 依赖,安装快很多。详见 [RELEASE-NOTES-v2.0.4.md](RELEASE-NOTES-v2.0.4.md)。
|
|
12
|
+
|
|
13
|
+
## v2.0.3 · 一条命令安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx dsh-client-ui-skin-tianlight@latest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
先完全退出 DeepSeek Harness,再运行上面这一条命令;装完打开 DSH、页面按 Ctrl+F5 即可。它会自动找到 DSH 数据目录和 `desktop` profile,备份 profile 清单,按插件管理器的格式登记依赖与 bundle,自动注销旧包名,复制并逐字节校验插件文件。不改任何 `cordis.patch.yml`,不碰 `<profile>/tianlight/` 里的用户数据。另有 `npx dsh-client-ui-skin-tianlight doctor`(只读体检)和 `uninstall`(卸载,保留用户数据)。插件本体与 2.0.2 相同。
|
|
20
|
+
|
|
9
21
|
## v2.0.2 · 正式版:npm 发布 + 包名迁移
|
|
10
22
|
|
|
11
23
|
`2.0.2` 把插件**发布到公开 npm registry**,安装从「下载 Release / 目录链接」变成一条命令(见下方「从 npm 安装」):
|
|
@@ -423,8 +435,19 @@ v0.5.0 的 P0–P4 功能全部保留;这一版是增量补丁,同一份构
|
|
|
423
435
|
|
|
424
436
|
### 从 npm 安装(推荐,一条命令)
|
|
425
437
|
|
|
426
|
-
npm 包:**`dsh-client-ui-skin-tianlight`**(公开 registry,预构建、免 `allowBuilds`
|
|
438
|
+
npm 包:**`dsh-client-ui-skin-tianlight`**(公开 registry,预构建、免 `allowBuilds` 构建授权)。装法任选:
|
|
439
|
+
|
|
440
|
+
- **一条命令(v2.0.3 起,推荐)**:先**完全退出 DSH**(含托盘图标),然后运行
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
npx dsh-client-ui-skin-tianlight@latest
|
|
444
|
+
```
|
|
427
445
|
|
|
446
|
+
装完打开 DSH,页面按 Ctrl+F5。需要本机有 Node.js 18+。常用参数:`--profile <名称>`(默认 `desktop`,只有一个 profile 时用它)、`--dsh-home <目录>`(默认 `$DSH_HOME` 或 `~/.dsh`)、`--dry-run`(只显示计划)、`--lang zh|en`。npm 官方源慢时可加 `--registry=https://registry.npmmirror.com`。
|
|
447
|
+
- 它做的事:备份 profile 的 `package.json` / `pnpm-lock.yaml` / `pnpm-workspace.yaml` 到 `<profile>/.tianlight-installer/backups/`(保留最近 3 份,旧版插件文件也放在这里便于回滚);把依赖写成精确版本、在 `dsh.profile.bundles` 登记一次;注销旧包名 `@dsh-external/dsh-client-ui-skin-tianlight`、`@local/dsh-enable-tianlight`;把插件文件复制进 `node_modules/dsh-client-ui-skin-tianlight` 并逐字节校验。原来是 `link:` 链接安装的,只移除链接本身,不动链接指向的开发目录。
|
|
448
|
+
- 它不做的事:不改任何 `cordis.patch.yml`(发现 `ui-skin-tianlight` 被 `disabled: true` 或被手写插入时只提示);不碰 `<profile>/tianlight/` 用户数据;DSH 运行时默认拒绝安装(Windows 会锁住正在使用的插件文件,强行替换会报 EPERM),确需继续可加 `--force`。
|
|
449
|
+
- 体检:`npx dsh-client-ui-skin-tianlight doctor`,只读,检查登记、旧包名残留、文件是否与发布包逐字节一致、被禁用的行、用户数据。卸载:`npx dsh-client-ui-skin-tianlight uninstall`(保留用户数据)。
|
|
450
|
+
- profile 的 `pnpm-lock.yaml` 由插件管理器在下一次安装/更新时自动补齐。
|
|
428
451
|
- **DSH Desktop → 插件管理器**:安装/添加插件时输入包名 `dsh-client-ui-skin-tianlight`(或从市场页安装),插件管理器会把依赖与 `dsh.profile.bundles` 一起登记。
|
|
429
452
|
- **命令行**:`dsh plugin --profile desktop add dsh-client-ui-skin-tianlight` 安装依赖,然后在插件管理器里启用 `ui-skin-tianlight` 行。
|
|
430
453
|
- 升级:插件管理器的「更新」按 registry 新版本更新;或 `dsh plugin --profile desktop add dsh-client-ui-skin-tianlight@latest` 后重启。**升级后重启 DSH 一次 + Ctrl+F5。**
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npx dsh-client-ui-skin-tianlight [install|uninstall|doctor] [options]
|
|
3
|
+
//
|
|
4
|
+
// install (default) put this version into a DSH profile and register it
|
|
5
|
+
// uninstall unregister + remove the package files (user data is kept)
|
|
6
|
+
// doctor read-only health check of the DSH home / profile / install
|
|
7
|
+
//
|
|
8
|
+
// options: --profile <name> --dsh-home <dir> --dry-run --force --lang zh|en
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import {
|
|
12
|
+
LEGACY_PACKAGES,
|
|
13
|
+
PKG_NAME,
|
|
14
|
+
ROW_ID,
|
|
15
|
+
SELF_ROOT,
|
|
16
|
+
compareTrees,
|
|
17
|
+
countFiles,
|
|
18
|
+
inspectInstalled,
|
|
19
|
+
inspectUserData,
|
|
20
|
+
installFiles,
|
|
21
|
+
isDshRunning,
|
|
22
|
+
listProfiles,
|
|
23
|
+
makeBackupDir,
|
|
24
|
+
packageEntries,
|
|
25
|
+
pickProfile,
|
|
26
|
+
planInstall,
|
|
27
|
+
planUninstall,
|
|
28
|
+
pruneBackups,
|
|
29
|
+
readJson,
|
|
30
|
+
removeInstalledFiles,
|
|
31
|
+
isUncPath,
|
|
32
|
+
resolveDshHome,
|
|
33
|
+
scanPatches,
|
|
34
|
+
writeJsonAtomic,
|
|
35
|
+
} from './installer.mjs'
|
|
36
|
+
|
|
37
|
+
const SELF = readJson(join(SELF_ROOT, 'package.json'))
|
|
38
|
+
const VERSION = SELF.version
|
|
39
|
+
|
|
40
|
+
// ---- args -------------------------------------------------------------------
|
|
41
|
+
const argv = process.argv.slice(2)
|
|
42
|
+
const flags = new Set(argv.filter((a) => a.startsWith('--') || a === '-h' || a === '-v'))
|
|
43
|
+
const value = (name) => {
|
|
44
|
+
const i = argv.indexOf(name)
|
|
45
|
+
return i >= 0 && argv[i + 1] !== undefined && argv[i + 1].startsWith('--') === false ? argv[i + 1] : null
|
|
46
|
+
}
|
|
47
|
+
const valueFlags = new Set(['--profile', '--dsh-home', '--lang'])
|
|
48
|
+
const positional = argv.filter((a, i) => a.startsWith('-') === false && valueFlags.has(argv[i - 1]) === false)
|
|
49
|
+
const command = positional[0] ?? 'install'
|
|
50
|
+
const dryRun = flags.has('--dry-run')
|
|
51
|
+
const force = flags.has('--force')
|
|
52
|
+
|
|
53
|
+
const envLocale = String(process.env.LC_ALL || process.env.LANG || Intl.DateTimeFormat().resolvedOptions().locale || '')
|
|
54
|
+
const lang = value('--lang') === 'en' ? 'en' : value('--lang') === 'zh' ? 'zh' : /^zh/i.test(envLocale) ? 'zh' : 'en'
|
|
55
|
+
|
|
56
|
+
// ---- messages ---------------------------------------------------------------
|
|
57
|
+
const M = {
|
|
58
|
+
zh: {
|
|
59
|
+
title: `天光 TIANLIGHT 皮肤安装器 v${VERSION}`,
|
|
60
|
+
home: (p) => `DSH 数据目录:${p}`,
|
|
61
|
+
noHome: (p) => `找不到 DSH 数据目录 ${p}\n 请先安装并启动一次 DeepSeek Harness;数据目录不在默认位置时用 --dsh-home <目录> 指定。`,
|
|
62
|
+
noProfiles: '这个 DSH 数据目录里还没有 profile。请先启动一次 DSH 再运行本命令。',
|
|
63
|
+
unc: '不支持网络路径(\\\\服务器\\共享)作为 DSH 数据目录,请使用本机磁盘上的目录。',
|
|
64
|
+
profileMissing: (p, list) => `没有名为 "${p}" 的 profile。现有:${list.join('、') || '(无)'}`,
|
|
65
|
+
profileAmbiguous: (list) => `有多个 profile:${list.join('、')}\n 请用 --profile <名称> 指定要装到哪一个。`,
|
|
66
|
+
profile: (p) => `目标 profile:${p}`,
|
|
67
|
+
notProfile: 'profile 的 package.json 里没有 dsh.profile.bundles,看起来不是 DSH profile,已停止。',
|
|
68
|
+
running: 'DeepSeek Harness 正在运行。请先完全退出(包括托盘图标)再运行本命令;\n 运行中替换插件文件会被 Windows 拒绝(EPERM)。确实要继续可加 --force。',
|
|
69
|
+
runningForced: '注意:DSH 正在运行,已按 --force 继续。',
|
|
70
|
+
runningUnknown: '无法确认 DSH 是否在运行;如果开着,请先退出。',
|
|
71
|
+
current: (k, v) => (k === 'missing' ? '当前:未安装' : k === 'link' ? `当前:链接安装(指向 ${v ?? '未知'}),将改为 npm 版本,链接目标目录不会被改动` : `当前:已安装 ${v ?? '未知版本'}`),
|
|
72
|
+
plan: '将要执行:',
|
|
73
|
+
change: {
|
|
74
|
+
'legacy-dependency-removed': (c) => `移除旧包依赖 ${c.name}`,
|
|
75
|
+
'legacy-bundle-removed': (c) => `移除旧包 bundle 登记 ${c.name}`,
|
|
76
|
+
'dependency-set': (c) => `依赖 ${PKG_NAME}:${c.from ?? '(无)'} → ${c.to}`,
|
|
77
|
+
'bundle-added': () => `在 dsh.profile.bundles 登记 ${PKG_NAME}`,
|
|
78
|
+
'dependency-removed': (c) => `移除依赖 ${c.name}`,
|
|
79
|
+
'bundle-removed': (c) => `移除 bundle 登记 ${c.name}`,
|
|
80
|
+
},
|
|
81
|
+
copy: (n) => `复制插件文件到 node_modules/${PKG_NAME}(${n} 个文件)`,
|
|
82
|
+
sameFiles: '插件文件已是这个版本且逐字节一致,跳过复制',
|
|
83
|
+
manifestSame: 'profile 登记已经正确,无需修改',
|
|
84
|
+
dry: '(--dry-run:只显示计划,没有改动任何文件)',
|
|
85
|
+
backup: (p) => `已备份到:${p}`,
|
|
86
|
+
verified: (n) => `校验通过:${n} 个文件与发布包一致`,
|
|
87
|
+
verifyFailed: (n) => `校验失败:${n} 个文件不一致,安装可能不完整(备份在上面的目录)`,
|
|
88
|
+
patchDisabled: (f) => `警告:${f} 里有一段把 ${ROW_ID} 设为 disabled: true,皮肤会被禁用。\n 请在插件管理器里启用,或删掉那一段后重启 DSH(本工具不会改 cordis.patch.yml)。`,
|
|
89
|
+
patchInsert: (f) => `警告:${f} 里手写插入了 ${ROW_ID} 行,会与插件自带的行重复挂载,请删掉那一段。`,
|
|
90
|
+
data: (d) => (d.exists ? `你的天光数据保留在:${d.root}(设置版本 ${d.revision ?? '未知'},上传 ${d.uploads} 个)` : '还没有天光用户数据(首次安装会自动套用作者默认外观)'),
|
|
91
|
+
installed: (v) => `完成:天光 ${v} 已安装。`,
|
|
92
|
+
next: [
|
|
93
|
+
'下一步:',
|
|
94
|
+
' 1. 打开 DeepSeek Harness(如果开着,请完全退出后重开);',
|
|
95
|
+
' 2. 页面按 Ctrl+F5;',
|
|
96
|
+
' 3. 插件管理器里能看到 dsh-client-ui-skin-tianlight。',
|
|
97
|
+
' profile 的 pnpm-lock.yaml 会在插件管理器下一次安装/更新时自动补齐。',
|
|
98
|
+
],
|
|
99
|
+
uninstalled: '完成:已从 profile 移除天光(用户数据未删除;插件文件已移到备份目录)。',
|
|
100
|
+
nothing: '这个 profile 里没有安装天光,无需卸载。',
|
|
101
|
+
doctorTitle: '体检结果:',
|
|
102
|
+
ok: '正常',
|
|
103
|
+
warn: '注意',
|
|
104
|
+
err: '问题',
|
|
105
|
+
dRegistered: (s) => `已登记依赖 ${PKG_NAME}@${s}`,
|
|
106
|
+
dNotRegistered: `没有登记依赖 ${PKG_NAME}`,
|
|
107
|
+
dBundle: (y) => (y ? '已登记到 dsh.profile.bundles' : '没有登记到 dsh.profile.bundles(不会被加载)'),
|
|
108
|
+
dLegacy: (n) => `仍登记了旧包 ${n}(会重复挂载),运行 install 会自动移除`,
|
|
109
|
+
dFiles: (i) => (i.kind === 'missing' ? 'node_modules 里没有插件文件' : i.kind === 'link' ? `插件是链接安装,指向 ${i.linkTarget ?? '未知'}` : `已安装文件版本 ${i.version ?? '未知'}${i.buildInfo?.commit ? `(commit ${String(i.buildInfo.commit).slice(0, 8)})` : ''}`),
|
|
110
|
+
dMismatch: (a, b) => `登记的版本 ${a} 与已安装文件版本 ${b} 不一致`,
|
|
111
|
+
dSameAsSelf: (n) => `已安装文件与本安装器自带的 ${VERSION} 发布包逐字节一致(${n} 个文件)`,
|
|
112
|
+
dDiffSelf: (n) => `已安装文件与 ${VERSION} 发布包有 ${n} 处不同(可能被修改或损坏),可运行 install 修复`,
|
|
113
|
+
dNewer: (v) => `有新版本可装:${v}(运行 npx ${PKG_NAME}@latest)`,
|
|
114
|
+
dRunning: (r) => (r === true ? 'DSH 正在运行' : r === false ? 'DSH 未运行' : '无法确认 DSH 是否在运行'),
|
|
115
|
+
summary: (e, w) => `结论:${e} 个问题,${w} 个注意事项`,
|
|
116
|
+
usage: `用法:npx ${PKG_NAME} [install|uninstall|doctor] [--profile <名称>] [--dsh-home <目录>] [--dry-run] [--force] [--lang zh|en]`,
|
|
117
|
+
unknownCommand: (c) => `未知命令:${c}`,
|
|
118
|
+
failed: (m) => `安装失败:${m}`,
|
|
119
|
+
},
|
|
120
|
+
en: {
|
|
121
|
+
title: `TIANLIGHT skin installer v${VERSION}`,
|
|
122
|
+
home: (p) => `DSH home: ${p}`,
|
|
123
|
+
noHome: (p) => `DSH home not found: ${p}\n Install and start DeepSeek Harness once first, or pass --dsh-home <dir>.`,
|
|
124
|
+
noProfiles: 'This DSH home has no profile yet. Start DSH once, then run this command again.',
|
|
125
|
+
unc: 'Network paths (\\\\server\\share) are not supported as the DSH home; use a local directory.',
|
|
126
|
+
profileMissing: (p, list) => `No profile named "${p}". Existing: ${list.join(', ') || '(none)'}`,
|
|
127
|
+
profileAmbiguous: (list) => `Several profiles exist: ${list.join(', ')}\n Pick one with --profile <name>.`,
|
|
128
|
+
profile: (p) => `Target profile: ${p}`,
|
|
129
|
+
notProfile: 'The profile package.json has no dsh.profile.bundles; it does not look like a DSH profile. Stopped.',
|
|
130
|
+
running: 'DeepSeek Harness is running. Quit it completely (tray icon too) and run this again;\n Windows refuses to replace plugin files while DSH has them open (EPERM). Use --force to continue anyway.',
|
|
131
|
+
runningForced: 'Note: DSH is running; continuing because of --force.',
|
|
132
|
+
runningUnknown: 'Could not tell whether DSH is running; quit it first if it is.',
|
|
133
|
+
current: (k, v) => (k === 'missing' ? 'Current: not installed' : k === 'link' ? `Current: linked install (-> ${v ?? 'unknown'}); it becomes the npm version and the linked folder is left untouched` : `Current: installed ${v ?? 'unknown version'}`),
|
|
134
|
+
plan: 'Plan:',
|
|
135
|
+
change: {
|
|
136
|
+
'legacy-dependency-removed': (c) => `remove legacy dependency ${c.name}`,
|
|
137
|
+
'legacy-bundle-removed': (c) => `remove legacy bundle entry ${c.name}`,
|
|
138
|
+
'dependency-set': (c) => `dependency ${PKG_NAME}: ${c.from ?? '(none)'} -> ${c.to}`,
|
|
139
|
+
'bundle-added': () => `register ${PKG_NAME} in dsh.profile.bundles`,
|
|
140
|
+
'dependency-removed': (c) => `remove dependency ${c.name}`,
|
|
141
|
+
'bundle-removed': (c) => `remove bundle entry ${c.name}`,
|
|
142
|
+
},
|
|
143
|
+
copy: (n) => `copy the package files to node_modules/${PKG_NAME} (${n} files)`,
|
|
144
|
+
sameFiles: 'Package files are already this version and byte-identical; copy skipped',
|
|
145
|
+
manifestSame: 'Profile registration is already correct',
|
|
146
|
+
dry: '(--dry-run: plan only, nothing was changed)',
|
|
147
|
+
backup: (p) => `Backup: ${p}`,
|
|
148
|
+
verified: (n) => `Verified: ${n} files match the published package`,
|
|
149
|
+
verifyFailed: (n) => `Verification failed: ${n} files differ; the install may be incomplete (see the backup above)`,
|
|
150
|
+
patchDisabled: (f) => `Warning: ${f} sets ${ROW_ID} to disabled: true, which keeps the skin off.\n Enable it in the plugin manager or delete that block and restart DSH (this tool never edits cordis.patch.yml).`,
|
|
151
|
+
patchInsert: (f) => `Warning: ${f} inserts a ${ROW_ID} row by hand; it double-mounts the skin. Delete that block.`,
|
|
152
|
+
data: (d) => (d.exists ? `Your Tianlight data stays in: ${d.root} (settings revision ${d.revision ?? 'unknown'}, ${d.uploads} uploads)` : 'No Tianlight user data yet (a fresh install applies the author default look)'),
|
|
153
|
+
installed: (v) => `Done: Tianlight ${v} is installed.`,
|
|
154
|
+
next: [
|
|
155
|
+
'Next:',
|
|
156
|
+
' 1. Open DeepSeek Harness (quit and reopen it if it was running);',
|
|
157
|
+
' 2. press Ctrl+F5 in the page;',
|
|
158
|
+
' 3. the plugin manager lists dsh-client-ui-skin-tianlight.',
|
|
159
|
+
' The profile pnpm-lock.yaml is completed by the plugin manager on its next install/update.',
|
|
160
|
+
],
|
|
161
|
+
uninstalled: 'Done: Tianlight was removed from the profile (user data kept; package files moved to the backup folder).',
|
|
162
|
+
nothing: 'Tianlight is not installed in this profile; nothing to remove.',
|
|
163
|
+
doctorTitle: 'Health check:',
|
|
164
|
+
ok: 'ok',
|
|
165
|
+
warn: 'warn',
|
|
166
|
+
err: 'problem',
|
|
167
|
+
dRegistered: (s) => `dependency ${PKG_NAME}@${s} is registered`,
|
|
168
|
+
dNotRegistered: `dependency ${PKG_NAME} is not registered`,
|
|
169
|
+
dBundle: (y) => (y ? 'registered in dsh.profile.bundles' : 'not in dsh.profile.bundles (it will not load)'),
|
|
170
|
+
dLegacy: (n) => `legacy package ${n} is still registered (double mount); install removes it`,
|
|
171
|
+
dFiles: (i) => (i.kind === 'missing' ? 'no package files in node_modules' : i.kind === 'link' ? `linked install -> ${i.linkTarget ?? 'unknown'}` : `installed files: version ${i.version ?? 'unknown'}${i.buildInfo?.commit ? ` (commit ${String(i.buildInfo.commit).slice(0, 8)})` : ''}`),
|
|
172
|
+
dMismatch: (a, b) => `registered version ${a} differs from installed files ${b}`,
|
|
173
|
+
dSameAsSelf: (n) => `installed files are byte-identical to the ${VERSION} package shipped with this installer (${n} files)`,
|
|
174
|
+
dDiffSelf: (n) => `installed files differ from the ${VERSION} package in ${n} places (modified or damaged); run install to repair`,
|
|
175
|
+
dNewer: (v) => `a newer version is available: ${v} (run npx ${PKG_NAME}@latest)`,
|
|
176
|
+
dRunning: (r) => (r === true ? 'DSH is running' : r === false ? 'DSH is not running' : 'could not tell whether DSH is running'),
|
|
177
|
+
summary: (e, w) => `Result: ${e} problems, ${w} warnings`,
|
|
178
|
+
usage: `usage: npx ${PKG_NAME} [install|uninstall|doctor] [--profile <name>] [--dsh-home <dir>] [--dry-run] [--force] [--lang zh|en]`,
|
|
179
|
+
unknownCommand: (c) => `unknown command: ${c}`,
|
|
180
|
+
failed: (m) => `install failed: ${m}`,
|
|
181
|
+
},
|
|
182
|
+
}[lang]
|
|
183
|
+
|
|
184
|
+
const say = (line = '') => console.log(line)
|
|
185
|
+
const die = (line, code = 1) => {
|
|
186
|
+
console.error(line)
|
|
187
|
+
process.exit(code)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---- shared steps -----------------------------------------------------------
|
|
191
|
+
function locate() {
|
|
192
|
+
const requestedHome = value('--dsh-home') ?? process.env.DSH_HOME ?? ''
|
|
193
|
+
if (isUncPath(requestedHome)) die(M.unc)
|
|
194
|
+
const dshHome = resolveDshHome({ dshHome: value('--dsh-home') })
|
|
195
|
+
if (isUncPath(dshHome)) die(M.unc)
|
|
196
|
+
say(M.home(dshHome))
|
|
197
|
+
if (existsSync(dshHome) === false) die(M.noHome(dshHome))
|
|
198
|
+
const profiles = listProfiles(dshHome)
|
|
199
|
+
const picked = pickProfile(profiles, value('--profile'))
|
|
200
|
+
if (picked.ok === false) {
|
|
201
|
+
if (picked.reason === 'no-profiles') die(M.noProfiles)
|
|
202
|
+
if (picked.reason === 'profile-missing') die(M.profileMissing(value('--profile'), profiles))
|
|
203
|
+
die(M.profileAmbiguous(profiles))
|
|
204
|
+
}
|
|
205
|
+
say(M.profile(picked.profile))
|
|
206
|
+
return { dshHome, profile: picked.profile, profileDir: join(dshHome, 'profiles', picked.profile) }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function guardRunning() {
|
|
210
|
+
const running = isDshRunning()
|
|
211
|
+
if (running === true && force === false && dryRun === false) die(M.running, 3)
|
|
212
|
+
if (running === true && force) say(M.runningForced)
|
|
213
|
+
if (running === null) say(M.runningUnknown)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function reportPatches(dshHome, profileDir) {
|
|
217
|
+
let count = 0
|
|
218
|
+
for (const item of scanPatches(dshHome, profileDir)) {
|
|
219
|
+
say(item.finding === 'disabled' ? M.patchDisabled(item.file) : M.patchInsert(item.file))
|
|
220
|
+
count += 1
|
|
221
|
+
}
|
|
222
|
+
return count
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---- commands ---------------------------------------------------------------
|
|
226
|
+
function install() {
|
|
227
|
+
const { dshHome, profileDir } = locate()
|
|
228
|
+
guardRunning()
|
|
229
|
+
const manifestPath = join(profileDir, 'package.json')
|
|
230
|
+
const manifest = readJson(manifestPath)
|
|
231
|
+
const plan = planInstall(manifest, VERSION)
|
|
232
|
+
if (plan.ok === false) die(M.notProfile)
|
|
233
|
+
const before = inspectInstalled(profileDir)
|
|
234
|
+
say(M.current(before.kind, before.kind === 'link' ? before.linkTarget : before.version))
|
|
235
|
+
|
|
236
|
+
const entries = packageEntries(SELF_ROOT)
|
|
237
|
+
const fileCount = countFiles(SELF_ROOT, entries)
|
|
238
|
+
const sameFiles = before.kind === 'dir' && before.version === VERSION && compareTrees(SELF_ROOT, before.path, entries).length === 0
|
|
239
|
+
say(M.plan)
|
|
240
|
+
if (plan.changes.length === 0) say(' - ' + M.manifestSame)
|
|
241
|
+
for (const change of plan.changes) say(' - ' + M.change[change.kind](change))
|
|
242
|
+
say(' - ' + (sameFiles ? M.sameFiles : M.copy(fileCount)))
|
|
243
|
+
if (dryRun) {
|
|
244
|
+
reportPatches(dshHome, profileDir)
|
|
245
|
+
say(M.dry)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const backupDir = makeBackupDir(profileDir)
|
|
250
|
+
say(M.backup(backupDir))
|
|
251
|
+
if (sameFiles === false) installFiles({ srcRoot: SELF_ROOT, profileDir, backupDir, entries })
|
|
252
|
+
if (plan.changes.length > 0) writeJsonAtomic(manifestPath, plan.manifest)
|
|
253
|
+
pruneBackups(profileDir)
|
|
254
|
+
|
|
255
|
+
const problems = compareTrees(SELF_ROOT, join(profileDir, 'node_modules', PKG_NAME), entries)
|
|
256
|
+
if (problems.length > 0) {
|
|
257
|
+
for (const p of problems.slice(0, 10)) say(` ${p.problem}: ${p.path}`)
|
|
258
|
+
die(M.verifyFailed(problems.length))
|
|
259
|
+
}
|
|
260
|
+
say(M.verified(fileCount))
|
|
261
|
+
reportPatches(dshHome, profileDir)
|
|
262
|
+
say(M.data(inspectUserData(profileDir)))
|
|
263
|
+
say()
|
|
264
|
+
say(M.installed(VERSION))
|
|
265
|
+
for (const line of M.next) say(line)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function uninstall() {
|
|
269
|
+
const { profileDir } = locate()
|
|
270
|
+
guardRunning()
|
|
271
|
+
const manifestPath = join(profileDir, 'package.json')
|
|
272
|
+
const plan = planUninstall(readJson(manifestPath))
|
|
273
|
+
const before = inspectInstalled(profileDir)
|
|
274
|
+
if (plan.changes.length === 0 && before.kind === 'missing') {
|
|
275
|
+
say(M.nothing)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
say(M.plan)
|
|
279
|
+
for (const change of plan.changes) say(' - ' + M.change[change.kind](change))
|
|
280
|
+
if (dryRun) {
|
|
281
|
+
say(M.dry)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
const backupDir = makeBackupDir(profileDir)
|
|
285
|
+
say(M.backup(backupDir))
|
|
286
|
+
removeInstalledFiles({ profileDir, backupDir })
|
|
287
|
+
if (plan.changes.length > 0) writeJsonAtomic(manifestPath, plan.manifest)
|
|
288
|
+
pruneBackups(profileDir)
|
|
289
|
+
say(M.data(inspectUserData(profileDir)))
|
|
290
|
+
say(M.uninstalled)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function doctor() {
|
|
294
|
+
const { dshHome, profileDir } = locate()
|
|
295
|
+
const rows = []
|
|
296
|
+
const add = (level, text) => rows.push({ level, text })
|
|
297
|
+
const running = isDshRunning()
|
|
298
|
+
add(running === null ? 'warn' : 'ok', M.dRunning(running))
|
|
299
|
+
|
|
300
|
+
const manifest = readJson(join(profileDir, 'package.json'))
|
|
301
|
+
const spec = manifest.dependencies?.[PKG_NAME]
|
|
302
|
+
const bundles = Array.isArray(manifest.dsh?.profile?.bundles) ? manifest.dsh.profile.bundles : []
|
|
303
|
+
add(spec ? 'ok' : 'err', spec ? M.dRegistered(spec) : M.dNotRegistered)
|
|
304
|
+
add(bundles.includes(PKG_NAME) ? 'ok' : 'err', M.dBundle(bundles.includes(PKG_NAME)))
|
|
305
|
+
for (const legacy of LEGACY_PACKAGES) {
|
|
306
|
+
if (manifest.dependencies?.[legacy] !== undefined || bundles.includes(legacy)) add('err', M.dLegacy(legacy))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const installed = inspectInstalled(profileDir)
|
|
310
|
+
add(installed.kind === 'missing' ? 'err' : installed.kind === 'link' ? 'warn' : 'ok', M.dFiles(installed))
|
|
311
|
+
if (installed.kind === 'dir' && spec && /^\d/.test(spec) && installed.version !== spec) add('err', M.dMismatch(spec, installed.version))
|
|
312
|
+
if (installed.kind === 'dir' && installed.version === VERSION) {
|
|
313
|
+
const entries = packageEntries(SELF_ROOT)
|
|
314
|
+
const diff = compareTrees(SELF_ROOT, installed.path, entries)
|
|
315
|
+
add(diff.length === 0 ? 'ok' : 'err', diff.length === 0 ? M.dSameAsSelf(countFiles(SELF_ROOT, entries)) : M.dDiffSelf(diff.length))
|
|
316
|
+
}
|
|
317
|
+
for (const item of scanPatches(dshHome, profileDir)) {
|
|
318
|
+
add('err', item.finding === 'disabled' ? M.patchDisabled(item.file) : M.patchInsert(item.file))
|
|
319
|
+
}
|
|
320
|
+
const data = inspectUserData(profileDir)
|
|
321
|
+
add('ok', M.data(data))
|
|
322
|
+
if (installed.kind === 'dir' && installed.version && compareVersions(VERSION, installed.version) > 0) add('warn', M.dNewer(VERSION))
|
|
323
|
+
|
|
324
|
+
say()
|
|
325
|
+
say(M.doctorTitle)
|
|
326
|
+
const label = { ok: M.ok, warn: M.warn, err: M.err }
|
|
327
|
+
for (const row of rows) say(` [${label[row.level]}] ${row.text}`)
|
|
328
|
+
const errors = rows.filter((r) => r.level === 'err').length
|
|
329
|
+
const warns = rows.filter((r) => r.level === 'warn').length
|
|
330
|
+
say(M.summary(errors, warns))
|
|
331
|
+
if (errors > 0) process.exitCode = 1
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function compareVersions(a, b) {
|
|
335
|
+
const pa = String(a).split(/[.-]/).map((x) => (/^\d+$/.test(x) ? Number(x) : x))
|
|
336
|
+
const pb = String(b).split(/[.-]/).map((x) => (/^\d+$/.test(x) ? Number(x) : x))
|
|
337
|
+
for (let i = 0; i < 3; i += 1) {
|
|
338
|
+
if (pa[i] !== pb[i]) return Number(pa[i]) > Number(pb[i]) ? 1 : -1
|
|
339
|
+
}
|
|
340
|
+
return 0
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---- main -------------------------------------------------------------------
|
|
344
|
+
if (flags.has('--help') || flags.has('-h') || command === 'help') {
|
|
345
|
+
say(M.usage)
|
|
346
|
+
process.exit(0)
|
|
347
|
+
}
|
|
348
|
+
if (flags.has('--version') || flags.has('-v')) {
|
|
349
|
+
say(VERSION)
|
|
350
|
+
process.exit(0)
|
|
351
|
+
}
|
|
352
|
+
say(M.title)
|
|
353
|
+
try {
|
|
354
|
+
if (command === 'install') install()
|
|
355
|
+
else if (command === 'uninstall') uninstall()
|
|
356
|
+
else if (command === 'doctor') doctor()
|
|
357
|
+
else die(M.unknownCommand(command) + '\n' + M.usage, 2)
|
|
358
|
+
} catch (error) {
|
|
359
|
+
const message = error instanceof Error ? `${error.code ? error.code + ' ' : ''}${error.message}` : String(error)
|
|
360
|
+
die(M.failed(message))
|
|
361
|
+
}
|