dsh-plugin-syncup 0.0.0-stage → 4.1.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/LICENSE +21 -0
- package/README.en.md +183 -0
- package/README.md +362 -2
- package/client.js +1506 -0
- package/cordis.patch.yml +6 -0
- package/icon.svg +26 -0
- package/index.js +354 -0
- package/lib/config.js +330 -0
- package/lib/errors.js +81 -0
- package/lib/github.js +305 -0
- package/lib/http.js +285 -0
- package/lib/jobs.js +107 -0
- package/lib/manifest.js +272 -0
- package/lib/manual.js +85 -0
- package/lib/preview.js +217 -0
- package/lib/pushplan.js +200 -0
- package/lib/spec.js +127 -0
- package/lib/state.js +110 -0
- package/lib/sync.js +1636 -0
- package/lib/versions.js +447 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +52 -3
package/client.js
ADDED
|
@@ -0,0 +1,1506 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-plugin-syncup',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react')
|
|
5
|
+
const h = React.createElement
|
|
6
|
+
const NS = 'dsh-plugin-syncup'
|
|
7
|
+
|
|
8
|
+
const zh = {
|
|
9
|
+
summary: '默认使用 Gist 名称 dsh-plugin-sync;首次推送会自动创建。',
|
|
10
|
+
configTitle: 'GitHub 连接',
|
|
11
|
+
configHint: '只需要 Token;Gist 名称默认 dsh-plugin-sync,也可以修改。',
|
|
12
|
+
fieldGistName: 'Gist 名称',
|
|
13
|
+
fieldGistNameHint: '默认 dsh-plugin-sync;同一 GitHub 账号的其他主机填写相同名称即可。',
|
|
14
|
+
fieldToken: 'GitHub Token',
|
|
15
|
+
fieldTokenHint: 'classic token,需要 gist 权限;已配置时留空表示不修改。',
|
|
16
|
+
tokenPlaceholder: 'ghp_...',
|
|
17
|
+
tokenPlaceholderConfigured: '已配置,留空不修改',
|
|
18
|
+
tokenReadonly: 'Token 由环境变量或外部凭据提供,无法在页面修改。',
|
|
19
|
+
tokenHelp: '如何获取 Token',
|
|
20
|
+
tokenHelpHide: '收起获取说明',
|
|
21
|
+
tokenHelpPermissions: '所需权限',
|
|
22
|
+
tokenHelpPermissionText: '只需要 classic token 的 gist 权限;不需要 repo 权限。fine-grained token 目前对 Gist 支持有限。',
|
|
23
|
+
tokenHelpSteps: '操作步骤',
|
|
24
|
+
tokenHelpStep1: '打开 GitHub 的 Token 创建页面(已预填描述和 gist 权限)。',
|
|
25
|
+
tokenHelpStep2: '确认当前账号,并设置合适的过期时间。',
|
|
26
|
+
tokenHelpStep3: '勾选 gist;不要勾选仓库等其他权限。',
|
|
27
|
+
tokenHelpStep4: '点击生成,复制以 ghp_ 开头的 token。',
|
|
28
|
+
tokenHelpStep5: '粘贴到上方输入框,然后点击保存配置。',
|
|
29
|
+
tokenHelpSecurity: 'Token 只保存在 DSH 凭据服务中;不要分享,也不要写进仓库。',
|
|
30
|
+
tokenHelpOpen: '打开 GitHub Token 页面',
|
|
31
|
+
fieldFileName: '清单文件名',
|
|
32
|
+
fieldDescription: 'Gist 描述',
|
|
33
|
+
fieldHostId: '主机标识',
|
|
34
|
+
fieldHostIdHint: '默认使用主机名;留空则使用默认值。',
|
|
35
|
+
fieldTokenEnv: 'Token 环境变量',
|
|
36
|
+
fieldTokenEnvHint: '从该环境变量读取 GitHub token(classic token 需要 gist 权限)。',
|
|
37
|
+
fieldTokenFile: 'Token 文件',
|
|
38
|
+
fieldTokenFileHint: '也可以指向只包含 token 的文件,例如 ~/.dsh/github-token。',
|
|
39
|
+
fieldMaxInstalls: '单次最多安装',
|
|
40
|
+
save: '保存配置',
|
|
41
|
+
saving: '保存中…',
|
|
42
|
+
saved: '已保存,插件正在重新加载。',
|
|
43
|
+
configUnavailable: '当前 Host 没有可用的配置编辑器,无法保存配置。',
|
|
44
|
+
tokenConfigured: 'Token 已配置({source})',
|
|
45
|
+
tokenMissing: '未配置 Token:可预览公开 Gist,但不能创建或推送。',
|
|
46
|
+
gistMissing: '尚未缓存该名称的 Gist;点击拉取会按名称查找,首次推送会自动创建。',
|
|
47
|
+
saveGistFirst: 'Gist 名称已修改,请先保存配置再同步。',
|
|
48
|
+
gistNameLabel: 'Gist 名称:{gistName}(未缓存)',
|
|
49
|
+
localInfo: '本机 {profile} 个插件,其中 {portable} 个可移植。',
|
|
50
|
+
manualTitle: '手动同步',
|
|
51
|
+
manualHint: '点击“拉取线上清单”后,先预览变更和冲突;只有点击“应用同步”才会安装。不会自动同步。',
|
|
52
|
+
pull: '拉取线上清单',
|
|
53
|
+
pulling: '正在拉取…',
|
|
54
|
+
refresh: '重新拉取',
|
|
55
|
+
apply: '应用同步',
|
|
56
|
+
applying: '正在应用…',
|
|
57
|
+
discard: '放弃预览',
|
|
58
|
+
noChanges: '没有需要应用的变更。',
|
|
59
|
+
conflictCount: '{count} 个冲突',
|
|
60
|
+
actionableCount: '{count} 项可执行',
|
|
61
|
+
previewSummary: '线上 {hosts} 台主机 / {bundles} 条插件记录;{actionable} 项可执行,{conflicts} 个冲突。',
|
|
62
|
+
conflictRules: '冲突处理:只增不删;线上版本更高时默认升级;线上更低或同版本不同来源时默认保留本机;主机之间版本冲突时默认采用最高版本;本机独有默认保留,也可选择删除。',
|
|
63
|
+
kindInstall: '缺失',
|
|
64
|
+
kindUpgrade: '升级',
|
|
65
|
+
kindDowngrade: '降级',
|
|
66
|
+
kindSpec: '来源变更',
|
|
67
|
+
kindEnable: '启用',
|
|
68
|
+
kindCurrent: '已一致',
|
|
69
|
+
kindLocalOnly: '本机独有',
|
|
70
|
+
kindRemoteOff: '线上停用',
|
|
71
|
+
conflictHosts: '不同主机的版本或来源不一致',
|
|
72
|
+
conflictDowngrade: '线上版本低于本机',
|
|
73
|
+
conflictSpec: '版本相同但来源不同',
|
|
74
|
+
conflictEnable: '本机已安装但停用',
|
|
75
|
+
choiceRemote: '使用线上版本',
|
|
76
|
+
choiceRemoteHighest: '使用线上最高版本',
|
|
77
|
+
choiceEnable: '启用',
|
|
78
|
+
choiceLocal: '保留本机',
|
|
79
|
+
choiceKeepDisabled: '保持停用',
|
|
80
|
+
choiceSkip: '跳过',
|
|
81
|
+
choiceRemove: '删除本机插件',
|
|
82
|
+
removeWarn: '删除会卸载该插件包并移除它在本 profile 中的选择,重启后生效。',
|
|
83
|
+
localOnlyHint: '线上清单里没有这个插件;默认保留。',
|
|
84
|
+
localOnlyFixed: '这个插件不可移除(由 DSH 安装提供),只能保留。',
|
|
85
|
+
applyWithRemove: '应用({count} 项,其中删除 {remove})',
|
|
86
|
+
manualUpdateBadge: '需手动更新',
|
|
87
|
+
manualUpdateHint: 'HMR 无法在运行中卸载这个插件,自动升级会破坏它的启用状态;请到插件页更新,或重启后再试。',
|
|
88
|
+
reselectedNote: '已恢复该插件的启用状态(删除被拒绝)。',
|
|
89
|
+
tabSync: '同步 Gist',
|
|
90
|
+
tabLatest: '插件更新',
|
|
91
|
+
latestTitle: '插件更新',
|
|
92
|
+
latestHint: '直接向 npm / GitHub 查询真实最新版本,不依赖 Gist 里其他主机推送的版本。',
|
|
93
|
+
latestCheck: '检查最新版本',
|
|
94
|
+
latestChecking: '检查中…',
|
|
95
|
+
latestNone: '还没有检查结果。点击「检查最新版本」开始。',
|
|
96
|
+
latestSummary: '共 {entries} 项:{outdated} 项可更新,{current} 项已是最新,{unsupported} 项无法查询。',
|
|
97
|
+
latestOutdated: '可更新',
|
|
98
|
+
latestCurrent: '已是最新',
|
|
99
|
+
latestUnsupported: '无法查询',
|
|
100
|
+
latestSourceNpm: 'npm',
|
|
101
|
+
latestSourceGithub: 'GitHub',
|
|
102
|
+
latestSourceTag: 'GitHub tag(无 release)',
|
|
103
|
+
latestBothSources: 'npm {npm} / GitHub {github} → 取 {winner}',
|
|
104
|
+
latestWonGithub: 'GitHub 版本更新',
|
|
105
|
+
latestTieNpm: '版本相同,使用 npm',
|
|
106
|
+
channelAuto: '自动({version})',
|
|
107
|
+
channelNpm: 'npm({version})',
|
|
108
|
+
channelGithub: 'GitHub({version})',
|
|
109
|
+
channelSkip: '不更新',
|
|
110
|
+
channelUnavailable: '无',
|
|
111
|
+
latestUpdateAll: '更新选中项',
|
|
112
|
+
latestUpdating: '更新中…',
|
|
113
|
+
latestSelectAll: '选中全部',
|
|
114
|
+
latestSelectNone: '清除选择',
|
|
115
|
+
latestDone: '已更新 {count} 项。',
|
|
116
|
+
latestTarget: '{current} → {latest}',
|
|
117
|
+
latestProgress: '已处理 {done}/{total}|{name}',
|
|
118
|
+
latestStarting: '正在准备…',
|
|
119
|
+
manualTitle: '手动更新步骤',
|
|
120
|
+
manualIntro: '下面这些插件 HMR 无法在运行中卸载,请在关闭 DSH 后到终端执行:',
|
|
121
|
+
manualNote: '命令使用的是 DSH 自带的 node 与 pnpm,复制即可直接运行。',
|
|
122
|
+
manualClose: '收起',
|
|
123
|
+
installMethodTitle: '安装方式',
|
|
124
|
+
installMethodValue: '调用 DSH 官方插件管理器(ctx.pluginManager;底层是 pnpm,与插件页安装同一路径)',
|
|
125
|
+
logTitle: '安装日志',
|
|
126
|
+
logEmpty: '还没有日志。拉取或同步后会在这里显示进度。',
|
|
127
|
+
logShow: '显示日志',
|
|
128
|
+
logHide: '收起日志',
|
|
129
|
+
logProgress: '{done}/{total}',
|
|
130
|
+
localVersion: '本机 {version}',
|
|
131
|
+
remoteVersion: '线上 {version}',
|
|
132
|
+
notInstalled: '未安装',
|
|
133
|
+
upgradeNote: '升级已安装插件时,DSH 插件管理器会拒绝“重复安装”,因此这里先卸载旧版再安装新版;失败会自动装回旧版。',
|
|
134
|
+
selfBlocked: '本插件不能替换或删除自己,请在插件页操作。',
|
|
135
|
+
pendingTitle: '上次升级未完成',
|
|
136
|
+
pendingBody: '以下插件在升级途中中断,请检查或重新推送:',
|
|
137
|
+
rollbackRestored: '已回滚到旧版本',
|
|
138
|
+
rollbackFailed: '回滚失败,请手动重装',
|
|
139
|
+
sourceHosts: '来源主机:{hosts}',
|
|
140
|
+
sameVersion: '同一版本',
|
|
141
|
+
applyOk: '同步完成:安装/启用 {applied} 项,失败 {failed} 项。',
|
|
142
|
+
applyPartial: '同步完成,但有 {failed} 项失败。',
|
|
143
|
+
appliedTitle: '已应用',
|
|
144
|
+
removedTitle: '已删除',
|
|
145
|
+
failedTitle: '失败',
|
|
146
|
+
skippedTitle: '跳过',
|
|
147
|
+
restartRequired: '需要重启生效',
|
|
148
|
+
pushTitle: 'Gist 清单',
|
|
149
|
+
pushHint: '把本机可移植的插件清单推送/创建到私有 Gist,供其他主机手动拉取。',
|
|
150
|
+
push: '推送本机清单',
|
|
151
|
+
pushCreating: '创建 Gist 并推送本机清单',
|
|
152
|
+
pushConfirm: '推送会更新 Gist 中的清单文件,确认推送?',
|
|
153
|
+
pushConfirmCreate: '当前没有 Gist ID。确认创建私有 Gist 并推送本机清单?',
|
|
154
|
+
pushConfirmButton: '确认推送',
|
|
155
|
+
pushCancel: '取消',
|
|
156
|
+
pushPlanTitle: '推送前确认差异',
|
|
157
|
+
pushPlanHint: '线上与线下不一致的地方列在下面。默认规则:冲突取较新版本;线上独有的默认保留,可改为移除;线下独有的直接推送。',
|
|
158
|
+
pushPlanShowing: '显示 {shown} / {total} 项(已隐藏 {same} 项完全一致)',
|
|
159
|
+
pushPlanConflict: '冲突',
|
|
160
|
+
pushPlanRemoteOnly: '线上独有',
|
|
161
|
+
pushPlanLocalOnly: '线下独有',
|
|
162
|
+
pushPlanSame: '一致',
|
|
163
|
+
pushPlanLocal: '本机 {version}',
|
|
164
|
+
pushPlanRemote: '线上 {version}',
|
|
165
|
+
pushPlanNewestLocal: '本机版本较新,默认推送本机',
|
|
166
|
+
pushPlanNewestRemote: '线上版本较新,默认保留线上',
|
|
167
|
+
pushPlanRemoteOnlyHint: '这个插件线上有、本机没有;默认保留在清单中。',
|
|
168
|
+
pushPlanLocalOnlyHint: '本机独有,会直接推送。',
|
|
169
|
+
choiceNewest: '取较新版本',
|
|
170
|
+
choiceKeep: '保留在清单',
|
|
171
|
+
choiceDrop: '从清单移除',
|
|
172
|
+
pushPlanConfirm: '确认推送',
|
|
173
|
+
pushing: '正在推送…',
|
|
174
|
+
pushCreated: '已创建 Gist({gistName})。其他主机填写相同名称即可。',
|
|
175
|
+
pushPushed: '已推送({version})',
|
|
176
|
+
pushUnchanged: 'Gist 清单已是最新。',
|
|
177
|
+
gistLabel: 'Gist:{gistName}',
|
|
178
|
+
lastPreview: '上次拉取:{time}',
|
|
179
|
+
lastApply: '上次应用:{time}',
|
|
180
|
+
lastPush: '上次推送:{time}',
|
|
181
|
+
never: '从未',
|
|
182
|
+
loadFailed: '读取插件状态失败:{message}',
|
|
183
|
+
actionFailed: '操作失败:{message}',
|
|
184
|
+
loading: '正在读取…',
|
|
185
|
+
close: '关闭',
|
|
186
|
+
dismiss: '知道了',
|
|
187
|
+
notConfigured: '未配置',
|
|
188
|
+
configured: '已配置',
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const en = {
|
|
192
|
+
summary: 'Use the default Gist name dsh-plugin-sync; the first push creates it.',
|
|
193
|
+
configTitle: 'GitHub connection',
|
|
194
|
+
configHint: 'Only a token is needed. The Gist name defaults to dsh-plugin-sync and can be changed.',
|
|
195
|
+
fieldGistName: 'Gist name',
|
|
196
|
+
fieldGistNameHint: 'Defaults to dsh-plugin-sync. Other hosts on the same GitHub account use the same name.',
|
|
197
|
+
fieldToken: 'GitHub Token',
|
|
198
|
+
fieldTokenHint: 'Classic token with the gist scope. Leave blank to keep the configured token.',
|
|
199
|
+
tokenPlaceholder: 'ghp_...',
|
|
200
|
+
tokenPlaceholderConfigured: 'Configured; leave blank to keep it',
|
|
201
|
+
tokenReadonly: 'The token comes from the environment or an external credential and cannot be changed here.',
|
|
202
|
+
tokenHelp: 'How to get a token',
|
|
203
|
+
tokenHelpHide: 'Hide token instructions',
|
|
204
|
+
tokenHelpPermissions: 'Required permission',
|
|
205
|
+
tokenHelpPermissionText: 'Only the classic token gist scope is needed; no repo scope. Fine-grained tokens have limited Gist support.',
|
|
206
|
+
tokenHelpSteps: 'Steps',
|
|
207
|
+
tokenHelpStep1: 'Open GitHub\'s token creation page (description and gist scope are prefilled).',
|
|
208
|
+
tokenHelpStep2: 'Confirm the account and choose an expiration.',
|
|
209
|
+
tokenHelpStep3: 'Check gist; do not grant repository scopes.',
|
|
210
|
+
tokenHelpStep4: 'Generate and copy the token beginning with ghp_.',
|
|
211
|
+
tokenHelpStep5: 'Paste it above and click Save configuration.',
|
|
212
|
+
tokenHelpSecurity: 'The token is stored only in DSH credentials. Do not share it or commit it.',
|
|
213
|
+
tokenHelpOpen: 'Open GitHub token page',
|
|
214
|
+
fieldFileName: 'Manifest file name',
|
|
215
|
+
fieldDescription: 'Gist description',
|
|
216
|
+
fieldHostId: 'Host id',
|
|
217
|
+
fieldHostIdHint: 'Defaults to the hostname; leave blank to use the default.',
|
|
218
|
+
fieldTokenEnv: 'Token env var',
|
|
219
|
+
fieldTokenEnvHint: 'Read the GitHub token from this environment variable (classic token needs the gist scope).',
|
|
220
|
+
fieldTokenFile: 'Token file',
|
|
221
|
+
fieldTokenFileHint: 'Or point to a file containing only the token, for example ~/.dsh/github-token.',
|
|
222
|
+
fieldMaxInstalls: 'Max installs per apply',
|
|
223
|
+
save: 'Save configuration',
|
|
224
|
+
saving: 'Saving…',
|
|
225
|
+
saved: 'Saved. The plugin is reloading.',
|
|
226
|
+
configUnavailable: 'This Host has no configuration editor, so the configuration cannot be saved.',
|
|
227
|
+
tokenConfigured: 'Token configured ({source})',
|
|
228
|
+
tokenMissing: 'No token: public Gists can be previewed, but create/push is unavailable.',
|
|
229
|
+
gistMissing: 'This Gist name is not cached yet; Pull searches by name, and the first push creates it.',
|
|
230
|
+
saveGistFirst: 'The Gist name changed. Save the configuration before syncing.',
|
|
231
|
+
gistNameLabel: 'Gist name: {gistName} (not cached)',
|
|
232
|
+
localInfo: '{profile} local bundles, {portable} portable.',
|
|
233
|
+
manualTitle: 'Manual sync',
|
|
234
|
+
manualHint: 'Pull the Gist manifest, review changes and conflicts, then click Apply. Nothing installs until you apply. There is no automatic sync.',
|
|
235
|
+
pull: 'Pull Gist manifest',
|
|
236
|
+
pulling: 'Pulling…',
|
|
237
|
+
refresh: 'Pull again',
|
|
238
|
+
apply: 'Apply sync',
|
|
239
|
+
applying: 'Applying…',
|
|
240
|
+
discard: 'Discard preview',
|
|
241
|
+
noChanges: 'There are no changes to apply.',
|
|
242
|
+
conflictCount: '{count} conflict(s)',
|
|
243
|
+
actionableCount: '{count} actionable',
|
|
244
|
+
previewSummary: 'Gist: {hosts} host(s) / {bundles} bundle record(s); {actionable} actionable, {conflicts} conflict(s).',
|
|
245
|
+
conflictRules: 'Conflict policy: only add or upgrade; a newer remote version is selected by default; an older remote version or a same-version/different-source entry keeps the local copy; host disagreements default to the highest version; local-only bundles are kept by default and can be removed.',
|
|
246
|
+
kindInstall: 'Missing',
|
|
247
|
+
kindUpgrade: 'Upgrade',
|
|
248
|
+
kindDowngrade: 'Downgrade',
|
|
249
|
+
kindSpec: 'Source change',
|
|
250
|
+
kindEnable: 'Enable',
|
|
251
|
+
kindCurrent: 'Current',
|
|
252
|
+
kindLocalOnly: 'Local only',
|
|
253
|
+
kindRemoteOff: 'Remote off',
|
|
254
|
+
conflictHosts: 'Hosts disagree on version or source',
|
|
255
|
+
conflictDowngrade: 'Remote version is older',
|
|
256
|
+
conflictSpec: 'Same version, different source',
|
|
257
|
+
conflictEnable: 'Installed but disabled locally',
|
|
258
|
+
choiceRemote: 'Use remote version',
|
|
259
|
+
choiceRemoteHighest: 'Use highest remote version',
|
|
260
|
+
choiceEnable: 'Enable',
|
|
261
|
+
choiceLocal: 'Keep local',
|
|
262
|
+
choiceKeepDisabled: 'Keep disabled',
|
|
263
|
+
choiceSkip: 'Skip',
|
|
264
|
+
choiceRemove: 'Remove local plugin',
|
|
265
|
+
removeWarn: 'Removing uninstalls the package and drops its profile selection; it takes effect after a restart.',
|
|
266
|
+
localOnlyHint: 'Not present in the remote manifest; kept by default.',
|
|
267
|
+
localOnlyFixed: 'This bundle cannot be removed (provided by the DSH installation); it can only be kept.',
|
|
268
|
+
applyWithRemove: 'Apply ({count} change(s), {remove} remove)',
|
|
269
|
+
manualUpdateBadge: 'Manual update',
|
|
270
|
+
manualUpdateHint: 'HMR cannot unload this bundle while DSH runs, so an automatic upgrade would break its enabled state. Update it from the terminal with DSH closed.',
|
|
271
|
+
reselectedNote: 'Its profile selection was restored after the refused removal.',
|
|
272
|
+
tabSync: 'Sync Gist',
|
|
273
|
+
tabLatest: 'Plugin updates',
|
|
274
|
+
latestTitle: 'Plugin updates',
|
|
275
|
+
latestHint: 'Asks npm / GitHub for the real newest release instead of trusting the version another host pushed to the Gist.',
|
|
276
|
+
latestCheck: 'Check latest versions',
|
|
277
|
+
latestChecking: 'Checking…',
|
|
278
|
+
latestNone: 'No results yet. Click "Check latest versions" to start.',
|
|
279
|
+
latestSummary: '{entries} bundle(s): {outdated} outdated, {current} current, {unsupported} not checkable.',
|
|
280
|
+
latestOutdated: 'Outdated',
|
|
281
|
+
latestCurrent: 'Current',
|
|
282
|
+
latestUnsupported: 'Not checkable',
|
|
283
|
+
latestSourceNpm: 'npm',
|
|
284
|
+
latestSourceGithub: 'GitHub',
|
|
285
|
+
latestSourceTag: 'GitHub tag (no release)',
|
|
286
|
+
latestBothSources: 'npm {npm} / GitHub {github} → {winner} wins',
|
|
287
|
+
latestWonGithub: 'GitHub release is newer',
|
|
288
|
+
latestTieNpm: 'Same version; npm is used',
|
|
289
|
+
channelAuto: 'Auto ({version})',
|
|
290
|
+
channelNpm: 'npm ({version})',
|
|
291
|
+
channelGithub: 'GitHub ({version})',
|
|
292
|
+
channelSkip: 'Do not update',
|
|
293
|
+
channelUnavailable: 'n/a',
|
|
294
|
+
latestUpdateAll: 'Update selected',
|
|
295
|
+
latestUpdating: 'Updating…',
|
|
296
|
+
latestSelectAll: 'Select all',
|
|
297
|
+
latestSelectNone: 'Clear selection',
|
|
298
|
+
latestDone: 'Updated {count} bundle(s).',
|
|
299
|
+
latestTarget: '{current} → {latest}',
|
|
300
|
+
latestProgress: '{done}/{total} done|{name}',
|
|
301
|
+
latestStarting: 'Preparing…',
|
|
302
|
+
manualTitle: 'Manual update steps',
|
|
303
|
+
manualIntro: 'HMR cannot unload these bundles while DSH runs. Close DSH, then run this in a terminal:',
|
|
304
|
+
manualNote: 'The command uses the node and pnpm bundled with DSH, so it runs as pasted.',
|
|
305
|
+
manualClose: 'Hide',
|
|
306
|
+
installMethodTitle: 'Install method',
|
|
307
|
+
installMethodValue: 'DSH plugin manager (ctx.pluginManager — pnpm underneath, the same path the plugin page uses)',
|
|
308
|
+
logTitle: 'Install log',
|
|
309
|
+
logEmpty: 'No log yet. Pull or sync to see progress here.',
|
|
310
|
+
logShow: 'Show log',
|
|
311
|
+
logHide: 'Hide log',
|
|
312
|
+
logProgress: '{done}/{total}',
|
|
313
|
+
localVersion: 'local {version}',
|
|
314
|
+
remoteVersion: 'remote {version}',
|
|
315
|
+
notInstalled: 'not installed',
|
|
316
|
+
upgradeNote: 'The DSH plugin manager rejects re-adding an installed bundle, so an upgrade removes the old copy and installs the new one; a failed install restores the old version.',
|
|
317
|
+
selfBlocked: 'This plugin cannot replace or remove itself; use the Plugins page.',
|
|
318
|
+
pendingTitle: 'An upgrade did not finish',
|
|
319
|
+
pendingBody: 'These bundles were interrupted mid-upgrade; check or push again:',
|
|
320
|
+
rollbackRestored: 'restored the previous version',
|
|
321
|
+
rollbackFailed: 'restore failed; reinstall manually',
|
|
322
|
+
sourceHosts: 'Source hosts: {hosts}',
|
|
323
|
+
sameVersion: 'same version',
|
|
324
|
+
applyOk: 'Sync finished: {applied} installed/enabled, {failed} failed.',
|
|
325
|
+
applyPartial: 'Sync finished with {failed} failure(s).',
|
|
326
|
+
appliedTitle: 'Applied',
|
|
327
|
+
removedTitle: 'Removed',
|
|
328
|
+
failedTitle: 'Failed',
|
|
329
|
+
skippedTitle: 'Skipped',
|
|
330
|
+
restartRequired: 'Restart required',
|
|
331
|
+
pushTitle: 'Gist manifest',
|
|
332
|
+
pushHint: 'Publish this host\'s portable inventory to a secret Gist so other hosts can pull it.',
|
|
333
|
+
push: 'Push local inventory',
|
|
334
|
+
pushCreating: 'Create Gist and push local inventory',
|
|
335
|
+
pushConfirm: 'This updates the manifest file in the Gist. Push now?',
|
|
336
|
+
pushConfirmCreate: 'No Gist ID is configured. Create a secret Gist and push this host\'s inventory?',
|
|
337
|
+
pushConfirmButton: 'Confirm push',
|
|
338
|
+
pushCancel: 'Cancel',
|
|
339
|
+
pushPlanTitle: 'Confirm differences before pushing',
|
|
340
|
+
pushPlanHint: 'Rows below differ between the Gist and this host. Rules: conflicts keep the newer version; remote-only entries stay by default and can be removed; local-only entries are pushed directly.',
|
|
341
|
+
pushPlanShowing: 'Showing {shown} of {total} ({same} identical hidden)',
|
|
342
|
+
pushPlanConflict: 'Conflict',
|
|
343
|
+
pushPlanRemoteOnly: 'Remote only',
|
|
344
|
+
pushPlanLocalOnly: 'Local only',
|
|
345
|
+
pushPlanSame: 'Identical',
|
|
346
|
+
pushPlanLocal: 'local {version}',
|
|
347
|
+
pushPlanRemote: 'remote {version}',
|
|
348
|
+
pushPlanNewestLocal: 'Local is newer; this host wins by default',
|
|
349
|
+
pushPlanNewestRemote: 'Remote is newer; it stays by default',
|
|
350
|
+
pushPlanRemoteOnlyHint: 'Present in the Gist but not here; kept by default.',
|
|
351
|
+
pushPlanLocalOnlyHint: 'Local only; pushed as-is.',
|
|
352
|
+
choiceNewest: 'Keep the newer version',
|
|
353
|
+
choiceKeep: 'Keep in the manifest',
|
|
354
|
+
choiceDrop: 'Remove from the manifest',
|
|
355
|
+
pushPlanConfirm: 'Confirm push',
|
|
356
|
+
pushing: 'Pushing…',
|
|
357
|
+
pushCreated: 'Created Gist ({gistName}). Other hosts use the same name.',
|
|
358
|
+
pushPushed: 'Pushed ({version})',
|
|
359
|
+
pushUnchanged: 'The Gist manifest is already up to date.',
|
|
360
|
+
gistLabel: 'Gist: {gistName}',
|
|
361
|
+
lastPreview: 'Last pull: {time}',
|
|
362
|
+
lastApply: 'Last apply: {time}',
|
|
363
|
+
lastPush: 'Last push: {time}',
|
|
364
|
+
never: 'never',
|
|
365
|
+
loadFailed: 'Could not read plugin status: {message}',
|
|
366
|
+
actionFailed: 'Action failed: {message}',
|
|
367
|
+
loading: 'Loading…',
|
|
368
|
+
close: 'Close',
|
|
369
|
+
dismiss: 'Dismiss',
|
|
370
|
+
notConfigured: 'not configured',
|
|
371
|
+
configured: 'configured',
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const CSS = `
|
|
375
|
+
.dsh-ps{box-sizing:border-box;width:100%;display:flex;flex-direction:column;gap:16px;color:var(--dsw-alias-label-primary)}
|
|
376
|
+
.dsh-ps *{box-sizing:border-box}
|
|
377
|
+
.dsh-ps a{color:var(--dsw-alias-brand-primary);text-decoration:none;overflow-wrap:anywhere}
|
|
378
|
+
.dsh-ps-card{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:16px;display:flex;flex-direction:column;gap:12px}
|
|
379
|
+
.dsh-ps-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}
|
|
380
|
+
.dsh-ps-title{margin:0;font-size:15px;line-height:22px;font-weight:600}
|
|
381
|
+
.dsh-ps-desc{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}
|
|
382
|
+
.dsh-ps-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
|
|
383
|
+
.dsh-ps-field{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
384
|
+
.dsh-ps-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
385
|
+
.dsh-ps-input{height:32px;width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);padding:0 10px;font:inherit;font-size:13px}
|
|
386
|
+
.dsh-ps-input:focus{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
387
|
+
.dsh-ps-input:disabled{opacity:.55}
|
|
388
|
+
.dsh-ps-hint{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
389
|
+
.dsh-ps-help-wrap{display:flex;flex-direction:column;gap:10px;align-items:flex-start}
|
|
390
|
+
.dsh-ps-help{width:100%;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);padding:12px;display:flex;flex-direction:column;gap:10px}
|
|
391
|
+
.dsh-ps-help-title{margin:0;font-size:13px;line-height:20px;font-weight:600}
|
|
392
|
+
.dsh-ps-help p{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
393
|
+
.dsh-ps-help ol{margin:0;padding-left:20px;display:flex;flex-direction:column;gap:4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
394
|
+
.dsh-ps-help a.dsh-ps-btn{display:inline-flex;align-items:center;justify-content:center;width:max-content;text-decoration:none}
|
|
395
|
+
.dsh-ps-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
|
396
|
+
.dsh-ps-btn{height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;cursor:pointer;white-space:nowrap}
|
|
397
|
+
.dsh-ps-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}
|
|
398
|
+
.dsh-ps-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
399
|
+
.dsh-ps-btn:disabled{opacity:.45;cursor:default}
|
|
400
|
+
.dsh-ps-btn.primary{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2)}
|
|
401
|
+
.dsh-ps-btn.danger{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
402
|
+
.dsh-ps-summary{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
|
403
|
+
.dsh-ps-badge{display:inline-flex;align-items:center;min-height:22px;padding:0 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px;white-space:nowrap}
|
|
404
|
+
.dsh-ps-badge.ok{color:var(--dsw-alias-state-success-primary)}
|
|
405
|
+
.dsh-ps-badge.warn{color:var(--dsw-alias-state-warn-primary)}
|
|
406
|
+
.dsh-ps-badge.error{color:var(--dsw-alias-state-error-primary)}
|
|
407
|
+
.dsh-ps-alert{border:1px solid var(--dsw-alias-state-warn-primary);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warn-primary);font-size:13px;line-height:20px}
|
|
408
|
+
.dsh-ps-alert.error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
409
|
+
.dsh-ps-ok{color:var(--dsw-alias-state-success-primary);font-size:13px;line-height:20px}
|
|
410
|
+
.dsh-ps-error{color:var(--dsw-alias-state-error-primary);font-size:13px;line-height:20px}
|
|
411
|
+
.dsh-ps-list{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
412
|
+
.dsh-ps-entry{display:flex;flex-direction:column;gap:6px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
413
|
+
.dsh-ps-entry-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
|
|
414
|
+
.dsh-ps-name{font-size:13px;line-height:20px;font-weight:500;overflow-wrap:anywhere}
|
|
415
|
+
.dsh-ps-meta{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
|
|
416
|
+
.dsh-ps-conflict{font-size:12px;line-height:18px;color:var(--dsw-alias-state-warn-primary)}
|
|
417
|
+
.dsh-ps-variants{display:flex;flex-wrap:wrap;gap:6px}
|
|
418
|
+
.dsh-ps-variant{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border-radius:4px;padding:1px 6px;overflow-wrap:anywhere}
|
|
419
|
+
.dsh-ps-select{height:30px;min-width:132px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;padding:0 8px}
|
|
420
|
+
.dsh-ps-select:focus{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
421
|
+
.dsh-ps-confirm{border:1px solid var(--dsw-alias-state-warn-primary);border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:10px;background:var(--dsw-alias-bg-layer-2)}
|
|
422
|
+
.dsh-ps-result{display:flex;flex-direction:column;gap:6px;font-size:13px;line-height:20px}
|
|
423
|
+
.dsh-ps-muted{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
424
|
+
.dsh-ps-log{max-height:260px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-base);padding:8px 10px;display:flex;flex-direction:column;gap:2px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:18px}
|
|
425
|
+
.dsh-ps-logline{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
426
|
+
.dsh-ps-logline.warn{color:var(--dsw-alias-state-warn-primary)}
|
|
427
|
+
.dsh-ps-logline.error{color:var(--dsw-alias-state-error-primary)}
|
|
428
|
+
.dsh-ps-tabs{display:flex;gap:2px;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:12px}
|
|
429
|
+
.dsh-ps-tab{appearance:none;background:none;border:none;border-bottom:2px solid transparent;padding:8px 12px;font-size:13px;line-height:18px;color:var(--dsw-alias-label-secondary);cursor:pointer;margin-bottom:-1px}
|
|
430
|
+
.dsh-ps-tab:hover{color:var(--dsw-alias-label-primary)}
|
|
431
|
+
.dsh-ps-tab.active{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:500}
|
|
432
|
+
.dsh-ps-steps{display:flex;flex-direction:column;gap:8px;margin-top:8px}
|
|
433
|
+
.dsh-ps-step{display:flex;flex-direction:column;gap:4px}
|
|
434
|
+
.dsh-ps-step-title{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
435
|
+
.dsh-ps-cmd{display:block;padding:8px 10px;border-radius:6px;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:18px;white-space:pre-wrap;overflow-wrap:anywhere;user-select:all}
|
|
436
|
+
.dsh-ps-progress{display:flex;flex-direction:column;gap:4px;margin-top:8px}
|
|
437
|
+
.dsh-ps-progress-track{height:6px;border-radius:3px;background:var(--dsw-alias-border-l1);overflow:hidden}
|
|
438
|
+
.dsh-ps-progress-bar{height:100%;border-radius:3px;background:var(--dsw-alias-state-business-primary);transition:width .25s ease}
|
|
439
|
+
.dsh-ps-choices{display:flex;align-items:center;gap:8px;flex-shrink:0}
|
|
440
|
+
@media(max-width:640px){.dsh-ps-grid{grid-template-columns:minmax(0,1fr)}.dsh-ps-entry-head{flex-direction:column;align-items:stretch}.dsh-ps-select{width:100%}}
|
|
441
|
+
`
|
|
442
|
+
|
|
443
|
+
const FIELDS = [
|
|
444
|
+
{ key: 'gistName', label: 'fieldGistName', hint: 'fieldGistNameHint', placeholder: 'dsh-plugin-sync' },
|
|
445
|
+
]
|
|
446
|
+
const TOKEN_FIELD = { key: 'token', label: 'fieldToken', hint: 'fieldTokenHint', placeholder: 'Gist token', type: 'password' }
|
|
447
|
+
const TOKEN_URL = 'https://github.com/settings/tokens/new?description=DSH%20Plugin%20Sync&scopes=gist'
|
|
448
|
+
|
|
449
|
+
function endpoint(name) {
|
|
450
|
+
return new URL(`dsh-plugin-syncup/api/v2/${name}`, document.baseURI).pathname
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function call(name, { method = 'GET', body } = {}) {
|
|
454
|
+
const response = await fetch(endpoint(name), {
|
|
455
|
+
method,
|
|
456
|
+
cache: 'no-store',
|
|
457
|
+
headers: body === undefined ? undefined : { 'content-type': 'application/json' },
|
|
458
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
459
|
+
})
|
|
460
|
+
const data = await response.json().catch(() => ({}))
|
|
461
|
+
if (!response.ok || data.ok === false) {
|
|
462
|
+
const message = typeof data?.error?.message === 'string'
|
|
463
|
+
? data.error.message
|
|
464
|
+
: typeof data?.error === 'string' ? data.error : `HTTP ${response.status}`
|
|
465
|
+
const error = new Error(message)
|
|
466
|
+
error.code = data?.error?.code
|
|
467
|
+
throw error
|
|
468
|
+
}
|
|
469
|
+
return data
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function kindLabel(kind, t) {
|
|
473
|
+
if (kind === 'install') return t('kindInstall')
|
|
474
|
+
if (kind === 'upgrade') return t('kindUpgrade')
|
|
475
|
+
if (kind === 'downgrade') return t('kindDowngrade')
|
|
476
|
+
if (kind === 'spec-change') return t('kindSpec')
|
|
477
|
+
if (kind === 'enable') return t('kindEnable')
|
|
478
|
+
if (kind === 'current') return t('kindCurrent')
|
|
479
|
+
if (kind === 'local-only') return t('kindLocalOnly')
|
|
480
|
+
if (kind === 'remote-disabled') return t('kindRemoteOff')
|
|
481
|
+
return kind
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function choiceLabel(entry, choice, t) {
|
|
485
|
+
if (choice === 'remote') {
|
|
486
|
+
if (entry.kind === 'enable') return t('choiceEnable')
|
|
487
|
+
if (entry.conflict?.type === 'host-disagreement') return t('choiceRemoteHighest')
|
|
488
|
+
return t('choiceRemote')
|
|
489
|
+
}
|
|
490
|
+
if (choice === 'local') return entry.kind === 'enable' ? t('choiceKeepDisabled') : t('choiceLocal')
|
|
491
|
+
if (choice === 'remove') return t('choiceRemove')
|
|
492
|
+
return t('choiceSkip')
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function conflictLabel(conflict, t) {
|
|
496
|
+
if (conflict.type === 'host-disagreement') return t('conflictHosts')
|
|
497
|
+
if (conflict.type === 'downgrade') return t('conflictDowngrade')
|
|
498
|
+
if (conflict.type === 'spec-change') return t('conflictSpec')
|
|
499
|
+
if (conflict.type === 'enable-state') return t('conflictEnable')
|
|
500
|
+
return conflict.type
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function versionLine(entry, t) {
|
|
504
|
+
const local = entry.local?.version ? t('localVersion', { version: entry.local.version }) : t('notInstalled')
|
|
505
|
+
const remote = entry.remote?.version ? t('remoteVersion', { version: entry.remote.version }) : ''
|
|
506
|
+
return remote === '' ? local : `${local} · ${remote}`
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function ConfigField({ field, value, onChange, t, disabled, hintOverride }) {
|
|
510
|
+
const hint = hintOverride ?? (field.hint === null ? null : t(field.hint))
|
|
511
|
+
return h('label', { className: 'dsh-ps-field' },
|
|
512
|
+
h('span', { className: 'dsh-ps-label' }, t(field.label)),
|
|
513
|
+
h('input', {
|
|
514
|
+
className: 'dsh-ps-input',
|
|
515
|
+
type: field.type ?? (field.key === 'maxInstallsPerRun' ? 'number' : 'text'),
|
|
516
|
+
min: field.key === 'maxInstallsPerRun' ? 1 : undefined,
|
|
517
|
+
max: field.key === 'maxInstallsPerRun' ? 50 : undefined,
|
|
518
|
+
value: value ?? '',
|
|
519
|
+
placeholder: field.placeholder,
|
|
520
|
+
disabled,
|
|
521
|
+
autoComplete: field.type === 'password' ? 'off' : undefined,
|
|
522
|
+
onChange: (event) => onChange(field.key, event.target.value),
|
|
523
|
+
}),
|
|
524
|
+
hint === null ? null : h('span', { className: 'dsh-ps-hint' }, hint),
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function ConfigPage({ t, view }) {
|
|
529
|
+
const [status, setStatus] = React.useState(null)
|
|
530
|
+
const [draft, setDraft] = React.useState(null)
|
|
531
|
+
const [tokenDraft, setTokenDraft] = React.useState('')
|
|
532
|
+
const [showTokenHelp, setShowTokenHelp] = React.useState(false)
|
|
533
|
+
const [loading, setLoading] = React.useState(true)
|
|
534
|
+
const [saving, setSaving] = React.useState(false)
|
|
535
|
+
const [message, setMessage] = React.useState(null)
|
|
536
|
+
const [error, setError] = React.useState(null)
|
|
537
|
+
const [preview, setPreview] = React.useState(null)
|
|
538
|
+
const [selections, setSelections] = React.useState({})
|
|
539
|
+
const [previewing, setPreviewing] = React.useState(false)
|
|
540
|
+
const [applying, setApplying] = React.useState(false)
|
|
541
|
+
const [applyResult, setApplyResult] = React.useState(null)
|
|
542
|
+
const [confirmPush, setConfirmPush] = React.useState(false)
|
|
543
|
+
const [pushing, setPushing] = React.useState(false)
|
|
544
|
+
const [pushResult, setPushResult] = React.useState(null)
|
|
545
|
+
const [pushPlan, setPushPlan] = React.useState(null)
|
|
546
|
+
const [pushSelections, setPushSelections] = React.useState({})
|
|
547
|
+
const [planning, setPlanning] = React.useState(false)
|
|
548
|
+
const [job, setJob] = React.useState(null)
|
|
549
|
+
const [showLog, setShowLog] = React.useState(false)
|
|
550
|
+
const [confirmRemove, setConfirmRemove] = React.useState(null)
|
|
551
|
+
const [tab, setTab] = React.useState('sync')
|
|
552
|
+
const [latest, setLatest] = React.useState(null)
|
|
553
|
+
const [latestPicked, setLatestPicked] = React.useState({})
|
|
554
|
+
const [checking, setChecking] = React.useState(false)
|
|
555
|
+
const [latestUpdating, setLatestUpdating] = React.useState(false)
|
|
556
|
+
// Which flags belong to a request this page started, so the job-adoption
|
|
557
|
+
// effect cannot clear them mid-flight (see the effect below).
|
|
558
|
+
const pendingRef = React.useRef({ check: false, update: false })
|
|
559
|
+
const [latestMessage, setLatestMessage] = React.useState(null)
|
|
560
|
+
const [latestApplied, setLatestApplied] = React.useState(null)
|
|
561
|
+
const [latestChannel, setLatestChannel] = React.useState({})
|
|
562
|
+
const [manualOpen, setManualOpen] = React.useState(true)
|
|
563
|
+
|
|
564
|
+
const pollJob = React.useCallback(async () => {
|
|
565
|
+
try {
|
|
566
|
+
const data = await call('jobs')
|
|
567
|
+
setJob(data.latest ?? null)
|
|
568
|
+
} catch {
|
|
569
|
+
// A log read must never break the page.
|
|
570
|
+
}
|
|
571
|
+
}, [])
|
|
572
|
+
|
|
573
|
+
React.useEffect(() => {
|
|
574
|
+
void pollJob()
|
|
575
|
+
}, [pollJob])
|
|
576
|
+
|
|
577
|
+
React.useEffect(() => {
|
|
578
|
+
if (!applying && !previewing && !pushing && !checking && !latestUpdating) return undefined
|
|
579
|
+
const timer = setInterval(() => { void pollJob() }, 600)
|
|
580
|
+
return () => clearInterval(timer)
|
|
581
|
+
}, [applying, previewing, pushing, checking, latestUpdating, pollJob])
|
|
582
|
+
|
|
583
|
+
const loadStatus = React.useCallback(async () => {
|
|
584
|
+
setLoading(true)
|
|
585
|
+
setError(null)
|
|
586
|
+
try {
|
|
587
|
+
const data = await call('status')
|
|
588
|
+
setStatus(data)
|
|
589
|
+
setDraft(data.configSnapshot?.values ?? {})
|
|
590
|
+
if (typeof data.configError === 'string' && data.configError !== '') {
|
|
591
|
+
setError(data.configError)
|
|
592
|
+
}
|
|
593
|
+
} catch (failure) {
|
|
594
|
+
setError(t('loadFailed', { message: failure.message }))
|
|
595
|
+
} finally {
|
|
596
|
+
setLoading(false)
|
|
597
|
+
}
|
|
598
|
+
}, [t])
|
|
599
|
+
|
|
600
|
+
React.useEffect(() => {
|
|
601
|
+
void loadStatus()
|
|
602
|
+
void pollJob()
|
|
603
|
+
}, [loadStatus, pollJob])
|
|
604
|
+
|
|
605
|
+
// Restore the last version check so returning to the page shows the real
|
|
606
|
+
// state instead of an empty tab or a spurious error.
|
|
607
|
+
//
|
|
608
|
+
// Runs once per mount: a later `status` refresh (for example the one the
|
|
609
|
+
// click handler triggers in its `finally`) must not replace a result the
|
|
610
|
+
// user just received with an older cached copy.
|
|
611
|
+
const restoredRef = React.useRef(false)
|
|
612
|
+
React.useEffect(() => {
|
|
613
|
+
if (restoredRef.current || status === null) return
|
|
614
|
+
if (status.latestCheckedAt === null || status.latestCheckedAt === undefined) return
|
|
615
|
+
restoredRef.current = true
|
|
616
|
+
void (async () => {
|
|
617
|
+
try {
|
|
618
|
+
const data = await call('latest?cached=1')
|
|
619
|
+
if (data.cached !== true) return
|
|
620
|
+
// A check that finished first wins; the cached copy is only a fallback.
|
|
621
|
+
setLatest((current) => (current === null ? data : current))
|
|
622
|
+
setLatestPicked((current) => {
|
|
623
|
+
if (Object.keys(current).length > 0) return current
|
|
624
|
+
const picked = {}
|
|
625
|
+
for (const entry of data.entries) {
|
|
626
|
+
if (entry.outdated === true) picked[entry.name] = true
|
|
627
|
+
}
|
|
628
|
+
return picked
|
|
629
|
+
})
|
|
630
|
+
} catch {
|
|
631
|
+
// A restore failure must not surface as an operation error.
|
|
632
|
+
}
|
|
633
|
+
})()
|
|
634
|
+
}, [status, call])
|
|
635
|
+
|
|
636
|
+
// A page mounted while the Host is still working must resume the running
|
|
637
|
+
// job instead of reporting an idle state or firing a conflicting request.
|
|
638
|
+
//
|
|
639
|
+
// This both arms AND clears the flags. An earlier version only ever set
|
|
640
|
+
// them: once a check finished, the button stayed on "checking" forever and
|
|
641
|
+
// the finished result was never rendered.
|
|
642
|
+
//
|
|
643
|
+
// A flag in `pendingRef` belongs to a request this page fired; it is
|
|
644
|
+
// cleared by that request's own `finally`, never by this effect. Only flags
|
|
645
|
+
// adopted from a job (this page did not start it) are managed here.
|
|
646
|
+
React.useEffect(() => {
|
|
647
|
+
if (status?.busy === true) setShowLog(true)
|
|
648
|
+
const kind = job?.kind
|
|
649
|
+
const running = job?.status === 'running'
|
|
650
|
+
if (!running) {
|
|
651
|
+
if (pendingRef.current.check !== true) setChecking(false)
|
|
652
|
+
if (pendingRef.current.update !== true) setLatestUpdating(false)
|
|
653
|
+
return
|
|
654
|
+
}
|
|
655
|
+
if (kind === 'check' && pendingRef.current.check !== true) setChecking(true)
|
|
656
|
+
if (kind === 'apply-latest' && pendingRef.current.update !== true) setLatestUpdating(true)
|
|
657
|
+
}, [status?.busy, job?.status, job?.kind, job?.id, job?.finishedAt])
|
|
658
|
+
|
|
659
|
+
// While the Host is busy the buttons must stay disabled: pressing one would
|
|
660
|
+
// only produce a "another operation is already running" error that
|
|
661
|
+
// contradicts the progress the user is watching.
|
|
662
|
+
// While the Host is busy the buttons must stay disabled: pressing one would
|
|
663
|
+
// only produce a "another operation is already running" error. This reads
|
|
664
|
+
// the *live* job too, because `status.busy` only refreshes on a poll: a
|
|
665
|
+
// just-finished check would otherwise leave the buttons dead until the next
|
|
666
|
+
// refresh, which reads as "stuck".
|
|
667
|
+
const hostBusy = status?.busy === true && job?.status === 'running'
|
|
668
|
+
|
|
669
|
+
if (view === 'summary') return h('span', { className: 'dsh-ps-muted' }, t('summary'))
|
|
670
|
+
|
|
671
|
+
const configSnapshot = status?.configSnapshot
|
|
672
|
+
const values = draft ?? configSnapshot?.values ?? {}
|
|
673
|
+
const editable = configSnapshot?.editable === true
|
|
674
|
+
const draftGistName = typeof values.gistName === 'string' ? values.gistName : ''
|
|
675
|
+
const savedGistName = typeof configSnapshot?.values?.gistName === 'string' ? configSnapshot.values.gistName : ''
|
|
676
|
+
const effectiveGistName = draftGistName || status?.gistName || 'dsh-plugin-sync'
|
|
677
|
+
const gistConfigured = typeof status?.gistId === 'string' && status.gistId !== ''
|
|
678
|
+
const gistDirty = draftGistName !== savedGistName
|
|
679
|
+
const canOperate = !gistDirty
|
|
680
|
+
const gistUri = status?.gistUrl ?? null
|
|
681
|
+
const token = status?.token
|
|
682
|
+
const state = status?.state
|
|
683
|
+
const lastPreview = state?.lastPreviewAt
|
|
684
|
+
const lastApply = state?.lastApplyAt
|
|
685
|
+
const lastPush = state?.lastPushAt
|
|
686
|
+
const pendingUpgrades = Array.isArray(state?.pendingUpgrades) ? state.pendingUpgrades : []
|
|
687
|
+
|
|
688
|
+
const updateField = (key, value) => {
|
|
689
|
+
setDraft((current) => ({ ...(current ?? values), [key]: value }))
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const saveConfig = async () => {
|
|
693
|
+
if (!editable) return
|
|
694
|
+
setSaving(true)
|
|
695
|
+
setError(null)
|
|
696
|
+
setMessage(null)
|
|
697
|
+
try {
|
|
698
|
+
await call('config', {
|
|
699
|
+
method: 'POST',
|
|
700
|
+
body: {
|
|
701
|
+
values: { gistName: (draft ?? values).gistName ?? '' },
|
|
702
|
+
...(tokenDraft.trim() === '' ? {} : { token: tokenDraft.trim() }),
|
|
703
|
+
},
|
|
704
|
+
})
|
|
705
|
+
setTokenDraft('')
|
|
706
|
+
setMessage(t('saved'))
|
|
707
|
+
setTimeout(() => { void loadStatus() }, 1200)
|
|
708
|
+
} catch (failure) {
|
|
709
|
+
setError(t('actionFailed', { message: failure.message }))
|
|
710
|
+
} finally {
|
|
711
|
+
setSaving(false)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const pullPreview = async () => {
|
|
716
|
+
setPreviewing(true)
|
|
717
|
+
setError(null)
|
|
718
|
+
setMessage(null)
|
|
719
|
+
setApplyResult(null)
|
|
720
|
+
setShowLog(true)
|
|
721
|
+
try {
|
|
722
|
+
const data = await call('preview', { method: 'POST', body: {} })
|
|
723
|
+
const next = {}
|
|
724
|
+
for (const entry of data.entries) next[entry.id] = entry.defaultChoice
|
|
725
|
+
setPreview(data)
|
|
726
|
+
setSelections(next)
|
|
727
|
+
void loadStatus()
|
|
728
|
+
void pollJob()
|
|
729
|
+
} catch (failure) {
|
|
730
|
+
setError(t('actionFailed', { message: failure.message }))
|
|
731
|
+
void pollJob()
|
|
732
|
+
} finally {
|
|
733
|
+
setPreviewing(false)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const checkLatest = async () => {
|
|
738
|
+
pendingRef.current.check = true
|
|
739
|
+
setChecking(true)
|
|
740
|
+
setError(null)
|
|
741
|
+
setLatestMessage(null)
|
|
742
|
+
setShowLog(true)
|
|
743
|
+
void pollJob()
|
|
744
|
+
try {
|
|
745
|
+
const data = await call('latest')
|
|
746
|
+
setLatest(data)
|
|
747
|
+
const picked = {}
|
|
748
|
+
for (const entry of data.entries) {
|
|
749
|
+
if (entry.outdated === true) picked[entry.name] = true
|
|
750
|
+
}
|
|
751
|
+
setLatestPicked(picked)
|
|
752
|
+
} catch (failure) {
|
|
753
|
+
setError(t('actionFailed', { message: failure.message }))
|
|
754
|
+
} finally {
|
|
755
|
+
pendingRef.current.check = false
|
|
756
|
+
setChecking(false)
|
|
757
|
+
void pollJob()
|
|
758
|
+
// Refresh `busy` right away: until `loadStatus` resolves, `hostBusy`
|
|
759
|
+
// still reads the pre-check value and would keep the buttons disabled.
|
|
760
|
+
void loadStatus()
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const updateLatest = async () => {
|
|
765
|
+
const names = Object.keys(latestPicked).filter((name) => latestPicked[name] === true)
|
|
766
|
+
if (names.length === 0) return
|
|
767
|
+
pendingRef.current.update = true
|
|
768
|
+
setLatestUpdating(true)
|
|
769
|
+
setError(null)
|
|
770
|
+
setLatestMessage(null)
|
|
771
|
+
setShowLog(true)
|
|
772
|
+
void pollJob()
|
|
773
|
+
try {
|
|
774
|
+
// Only the explicit npm/github picks are sent; "auto" lets the host decide.
|
|
775
|
+
const channels = {}
|
|
776
|
+
for (const name of names) {
|
|
777
|
+
const value = latestChannel[name]
|
|
778
|
+
if (value === 'npm' || value === 'github') channels[name] = value
|
|
779
|
+
}
|
|
780
|
+
const data = await call('latest', { method: 'POST', body: { names, channels } })
|
|
781
|
+
setLatestMessage(data.ok
|
|
782
|
+
? t('latestDone', { count: data.totalApplied })
|
|
783
|
+
: t('applyPartial', { failed: data.failed.length }))
|
|
784
|
+
setLatestApplied(data)
|
|
785
|
+
setLatest(null)
|
|
786
|
+
setLatestPicked({})
|
|
787
|
+
setLatestChannel({})
|
|
788
|
+
} catch (failure) {
|
|
789
|
+
setError(t('actionFailed', { message: failure.message }))
|
|
790
|
+
} finally {
|
|
791
|
+
pendingRef.current.update = false
|
|
792
|
+
setLatestUpdating(false)
|
|
793
|
+
void pollJob()
|
|
794
|
+
void loadStatus()
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const applyPreview = async () => {
|
|
799
|
+
if (preview === null) return
|
|
800
|
+
setApplying(true)
|
|
801
|
+
setError(null)
|
|
802
|
+
setMessage(null)
|
|
803
|
+
setShowLog(true)
|
|
804
|
+
try {
|
|
805
|
+
const data = await call('apply', {
|
|
806
|
+
method: 'POST',
|
|
807
|
+
body: { previewId: preview.previewId, selections },
|
|
808
|
+
})
|
|
809
|
+
setApplyResult(data)
|
|
810
|
+
setPreview(null)
|
|
811
|
+
setMessage(data.ok
|
|
812
|
+
? t('applyOk', { applied: data.totalApplied, failed: 0 })
|
|
813
|
+
: t('applyPartial', { failed: data.failed.length }))
|
|
814
|
+
void loadStatus()
|
|
815
|
+
void pollJob()
|
|
816
|
+
} catch (failure) {
|
|
817
|
+
setError(t('actionFailed', { message: failure.message }))
|
|
818
|
+
void pollJob()
|
|
819
|
+
} finally {
|
|
820
|
+
setApplying(false)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const openPushPlan = async () => {
|
|
825
|
+
setPlanning(true)
|
|
826
|
+
setError(null)
|
|
827
|
+
setMessage(null)
|
|
828
|
+
setShowLog(true)
|
|
829
|
+
void pollJob()
|
|
830
|
+
try {
|
|
831
|
+
const data = await call('push/plan', { method: 'POST', body: {} })
|
|
832
|
+
setPushPlan(data)
|
|
833
|
+
const next = {}
|
|
834
|
+
for (const entry of data.entries) next[entry.id] = entry.defaultChoice
|
|
835
|
+
setPushSelections(next)
|
|
836
|
+
setConfirmPush(true)
|
|
837
|
+
} catch (failure) {
|
|
838
|
+
setError(t('actionFailed', { message: failure.message }))
|
|
839
|
+
} finally {
|
|
840
|
+
setPlanning(false)
|
|
841
|
+
void pollJob()
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const pushNow = async () => {
|
|
846
|
+
setPushing(true)
|
|
847
|
+
setError(null)
|
|
848
|
+
setMessage(null)
|
|
849
|
+
setShowLog(true)
|
|
850
|
+
try {
|
|
851
|
+
const data = await call('push', {
|
|
852
|
+
method: 'POST',
|
|
853
|
+
body: { confirm: true, planId: pushPlan?.planId ?? null, selections: pushSelections },
|
|
854
|
+
})
|
|
855
|
+
setPushResult(data)
|
|
856
|
+
if (typeof data.gistName === 'string' && data.gistName !== '') {
|
|
857
|
+
setDraft((current) => ({ ...(current ?? values), gistName: data.gistName }))
|
|
858
|
+
}
|
|
859
|
+
setMessage(data.created === true
|
|
860
|
+
? t('pushCreated', { gistName: data.gistName ?? effectiveGistName })
|
|
861
|
+
: data.status === 'pushed'
|
|
862
|
+
? t('pushPushed', { version: (data.version ?? '').slice(0, 12) })
|
|
863
|
+
: t('pushUnchanged'))
|
|
864
|
+
setConfirmPush(false)
|
|
865
|
+
setPushPlan(null)
|
|
866
|
+
void loadStatus()
|
|
867
|
+
void pollJob()
|
|
868
|
+
setTimeout(() => { void loadStatus() }, 1200)
|
|
869
|
+
} catch (failure) {
|
|
870
|
+
setError(t('actionFailed', { message: failure.message }))
|
|
871
|
+
void pollJob()
|
|
872
|
+
} finally {
|
|
873
|
+
setPushing(false)
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const visibleEntries = preview === null ? [] : preview.entries.filter((entry) => (
|
|
878
|
+
entry.defaultChoice === 'remote'
|
|
879
|
+
|| entry.choices.includes('remove')
|
|
880
|
+
|| entry.conflict !== null
|
|
881
|
+
|| ['install', 'upgrade', 'downgrade', 'spec-change', 'enable'].includes(entry.kind)
|
|
882
|
+
))
|
|
883
|
+
const selectedCount = visibleEntries.filter((entry) => (
|
|
884
|
+
selections[entry.id] === 'remote' || selections[entry.id] === 'remove'
|
|
885
|
+
)).length
|
|
886
|
+
const removeCount = visibleEntries.filter((entry) => selections[entry.id] === 'remove').length
|
|
887
|
+
const manualItems = applyResult === null || !Array.isArray(applyResult.failed)
|
|
888
|
+
? []
|
|
889
|
+
: applyResult.failed.filter((item) => item.manual !== undefined && Array.isArray(item.manual.steps))
|
|
890
|
+
const pushVisibleEntries = pushPlan === null
|
|
891
|
+
? []
|
|
892
|
+
: pushPlan.entries.filter((entry) => entry.kind !== 'same')
|
|
893
|
+
|
|
894
|
+
if (loading && status === null) {
|
|
895
|
+
return h('div', { className: 'dsh-ps' },
|
|
896
|
+
h('style', null, CSS),
|
|
897
|
+
h('div', { className: 'dsh-ps-card' }, h('p', { className: 'dsh-ps-desc' }, t('loading'))),
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return h('div', { className: 'dsh-ps' },
|
|
902
|
+
h('style', null, CSS),
|
|
903
|
+
|
|
904
|
+
h('section', { className: 'dsh-ps-card' },
|
|
905
|
+
h('div', { className: 'dsh-ps-head' },
|
|
906
|
+
h('div', null,
|
|
907
|
+
h('h3', { className: 'dsh-ps-title' }, t('configTitle')),
|
|
908
|
+
h('p', { className: 'dsh-ps-desc' }, t('configHint')),
|
|
909
|
+
),
|
|
910
|
+
h('span', { className: `dsh-ps-badge ${token?.configured ? 'ok' : 'warn'}` },
|
|
911
|
+
token?.configured ? t('tokenConfigured', { source: token.source ?? '' }) : t('tokenMissing')),
|
|
912
|
+
),
|
|
913
|
+
error === null ? null : h('div', { className: 'dsh-ps-alert error' }, error),
|
|
914
|
+
message === null ? null : h('div', { className: 'dsh-ps-ok' }, message),
|
|
915
|
+
configSnapshot?.editable === false
|
|
916
|
+
? h('div', { className: 'dsh-ps-alert' }, t('configUnavailable'))
|
|
917
|
+
: null,
|
|
918
|
+
h('div', { className: 'dsh-ps-grid' },
|
|
919
|
+
FIELDS.map((field) => h(ConfigField, {
|
|
920
|
+
key: field.key,
|
|
921
|
+
field,
|
|
922
|
+
t,
|
|
923
|
+
value: values[field.key],
|
|
924
|
+
disabled: !editable || saving,
|
|
925
|
+
onChange: updateField,
|
|
926
|
+
})),
|
|
927
|
+
h(ConfigField, {
|
|
928
|
+
key: 'token',
|
|
929
|
+
field: {
|
|
930
|
+
...TOKEN_FIELD,
|
|
931
|
+
placeholder: token?.configured ? t('tokenPlaceholderConfigured') : t('tokenPlaceholder'),
|
|
932
|
+
hint: null,
|
|
933
|
+
},
|
|
934
|
+
t,
|
|
935
|
+
value: tokenDraft,
|
|
936
|
+
disabled: !editable || saving || token?.writable === false,
|
|
937
|
+
hintOverride: token?.writable === false ? t('tokenReadonly') : t('fieldTokenHint'),
|
|
938
|
+
onChange: (_key, value) => { setTokenDraft(value) },
|
|
939
|
+
}),
|
|
940
|
+
),
|
|
941
|
+
token?.configured === true ? null : h('div', { className: 'dsh-ps-help-wrap' },
|
|
942
|
+
h('button', {
|
|
943
|
+
type: 'button',
|
|
944
|
+
className: 'dsh-ps-btn',
|
|
945
|
+
'aria-expanded': showTokenHelp,
|
|
946
|
+
'aria-controls': 'dsh-ps-token-help',
|
|
947
|
+
onClick: () => { setShowTokenHelp((current) => !current) },
|
|
948
|
+
}, showTokenHelp ? t('tokenHelpHide') : t('tokenHelp')),
|
|
949
|
+
showTokenHelp ? h('div', { className: 'dsh-ps-help', id: 'dsh-ps-token-help', role: 'region' },
|
|
950
|
+
h('div', null,
|
|
951
|
+
h('p', { className: 'dsh-ps-help-title' }, t('tokenHelpPermissions')),
|
|
952
|
+
h('p', null, t('tokenHelpPermissionText')),
|
|
953
|
+
),
|
|
954
|
+
h('div', null,
|
|
955
|
+
h('p', { className: 'dsh-ps-help-title' }, t('tokenHelpSteps')),
|
|
956
|
+
h('ol', null,
|
|
957
|
+
[1, 2, 3, 4, 5].map((step) => h('li', { key: step }, t(`tokenHelpStep${step}`))),
|
|
958
|
+
),
|
|
959
|
+
),
|
|
960
|
+
h('p', { className: 'dsh-ps-hint' }, t('tokenHelpSecurity')),
|
|
961
|
+
h('a', {
|
|
962
|
+
className: 'dsh-ps-btn primary',
|
|
963
|
+
href: TOKEN_URL,
|
|
964
|
+
target: '_blank',
|
|
965
|
+
rel: 'noreferrer',
|
|
966
|
+
}, t('tokenHelpOpen')),
|
|
967
|
+
) : null,
|
|
968
|
+
),
|
|
969
|
+
h('div', { className: 'dsh-ps-actions' },
|
|
970
|
+
h('button', {
|
|
971
|
+
type: 'button',
|
|
972
|
+
className: 'dsh-ps-btn primary',
|
|
973
|
+
disabled: !editable || saving,
|
|
974
|
+
onClick: () => { void saveConfig() },
|
|
975
|
+
}, saving ? t('saving') : t('save')),
|
|
976
|
+
status?.local === null || status?.local === undefined
|
|
977
|
+
? null
|
|
978
|
+
: h('span', { className: 'dsh-ps-muted' }, t('localInfo', {
|
|
979
|
+
profile: status.local.profileBundles ?? 0,
|
|
980
|
+
portable: status.local.portableBundles ?? 0,
|
|
981
|
+
})),
|
|
982
|
+
),
|
|
983
|
+
),
|
|
984
|
+
|
|
985
|
+
h('div', { className: 'dsh-ps-tabs' },
|
|
986
|
+
h('button', {
|
|
987
|
+
type: 'button',
|
|
988
|
+
className: `dsh-ps-tab ${tab === 'sync' ? 'active' : ''}`,
|
|
989
|
+
onClick: () => { setTab('sync') },
|
|
990
|
+
}, t('tabSync')),
|
|
991
|
+
h('button', {
|
|
992
|
+
type: 'button',
|
|
993
|
+
className: `dsh-ps-tab ${tab === 'latest' ? 'active' : ''}`,
|
|
994
|
+
onClick: () => { setTab('latest') },
|
|
995
|
+
}, t('tabLatest')),
|
|
996
|
+
),
|
|
997
|
+
|
|
998
|
+
tab !== 'latest' ? null : h('section', { className: 'dsh-ps-card' },
|
|
999
|
+
h('div', { className: 'dsh-ps-head' },
|
|
1000
|
+
h('div', null,
|
|
1001
|
+
h('h3', { className: 'dsh-ps-title' }, t('latestTitle')),
|
|
1002
|
+
h('p', { className: 'dsh-ps-desc' }, t('latestHint')),
|
|
1003
|
+
),
|
|
1004
|
+
),
|
|
1005
|
+
h('div', { className: 'dsh-ps-actions' },
|
|
1006
|
+
h('button', {
|
|
1007
|
+
type: 'button',
|
|
1008
|
+
className: 'dsh-ps-btn primary',
|
|
1009
|
+
disabled: checking || latestUpdating || hostBusy,
|
|
1010
|
+
onClick: () => { void checkLatest() },
|
|
1011
|
+
}, checking ? t('latestChecking') : t('latestCheck')),
|
|
1012
|
+
latest === null || latest.entries.length === 0 ? null : h(React.Fragment, null,
|
|
1013
|
+
h('button', {
|
|
1014
|
+
type: 'button',
|
|
1015
|
+
className: 'dsh-ps-btn',
|
|
1016
|
+
disabled: checking || latestUpdating,
|
|
1017
|
+
onClick: () => {
|
|
1018
|
+
const picked = {}
|
|
1019
|
+
for (const entry of latest.entries) if (entry.outdated === true) picked[entry.name] = true
|
|
1020
|
+
setLatestPicked(picked)
|
|
1021
|
+
},
|
|
1022
|
+
}, t('latestSelectAll')),
|
|
1023
|
+
h('button', {
|
|
1024
|
+
type: 'button',
|
|
1025
|
+
className: 'dsh-ps-btn',
|
|
1026
|
+
disabled: checking || latestUpdating,
|
|
1027
|
+
onClick: () => { setLatestPicked({}) },
|
|
1028
|
+
}, t('latestSelectNone')),
|
|
1029
|
+
),
|
|
1030
|
+
),
|
|
1031
|
+
latestMessage === null ? null : h('div', { className: 'dsh-ps-ok' }, latestMessage),
|
|
1032
|
+
latest === null
|
|
1033
|
+
? h('p', { className: 'dsh-ps-desc' }, t('latestNone'))
|
|
1034
|
+
: h(React.Fragment, null,
|
|
1035
|
+
h('div', { className: 'dsh-ps-summary' },
|
|
1036
|
+
h('span', { className: `dsh-ps-badge ${latest.counts.outdated > 0 ? 'warn' : 'ok'}` },
|
|
1037
|
+
t('latestSummary', {
|
|
1038
|
+
entries: latest.counts.entries,
|
|
1039
|
+
outdated: latest.counts.outdated,
|
|
1040
|
+
current: latest.counts.current,
|
|
1041
|
+
unsupported: latest.counts.unsupported,
|
|
1042
|
+
})),
|
|
1043
|
+
),
|
|
1044
|
+
h('div', { className: 'dsh-ps-list' },
|
|
1045
|
+
latest.entries.map((entry) => {
|
|
1046
|
+
// Channel availability must not depend on `channels` alone: an
|
|
1047
|
+
// older cached result has no `channels` field at all, and
|
|
1048
|
+
// `(undefined ?? null) === null` disabled BOTH options, so no
|
|
1049
|
+
// channel could be picked.
|
|
1050
|
+
//
|
|
1051
|
+
// An `undefined` channel means "unknown" and stays selectable;
|
|
1052
|
+
// only an explicit `null` (the host looked and found nothing)
|
|
1053
|
+
// disables it. The fallbacks keep the version label meaningful
|
|
1054
|
+
// for entries that predate the `channels` field.
|
|
1055
|
+
const npmKnown = entry.channels === undefined || entry.channels === null
|
|
1056
|
+
? undefined
|
|
1057
|
+
: entry.channels.npm
|
|
1058
|
+
const githubKnown = entry.channels === undefined || entry.channels === null
|
|
1059
|
+
? undefined
|
|
1060
|
+
: entry.channels.github
|
|
1061
|
+
const npmChannelVersion = npmKnown ?? entry.npmLatest ?? entry.latest ?? null
|
|
1062
|
+
const githubChannelVersion = githubKnown ?? entry.githubLatest ?? entry.latest ?? null
|
|
1063
|
+
const canUseNpm = npmKnown !== null && npmChannelVersion !== null && npmChannelVersion !== 'null'
|
|
1064
|
+
const canUseGithub = githubKnown !== null && githubChannelVersion !== null && githubChannelVersion !== 'null'
|
|
1065
|
+
return h('div', { className: 'dsh-ps-entry', key: `l-${entry.name}` },
|
|
1066
|
+
h('div', { className: 'dsh-ps-entry-head' },
|
|
1067
|
+
h('div', null,
|
|
1068
|
+
h('div', { className: 'dsh-ps-name' }, entry.name),
|
|
1069
|
+
h('div', { className: 'dsh-ps-meta' },
|
|
1070
|
+
entry.outdated === true
|
|
1071
|
+
? t('latestTarget', { current: entry.local.version || '?', latest: entry.latest })
|
|
1072
|
+
: entry.error === undefined
|
|
1073
|
+
? `${t('latestCurrent')} · ${entry.local.version || '?'}`
|
|
1074
|
+
: `${t('latestUnsupported')} · ${entry.error}`),
|
|
1075
|
+
h('div', { className: 'dsh-ps-meta' },
|
|
1076
|
+
entry.githubLatest !== undefined && entry.npmLatest !== undefined
|
|
1077
|
+
&& entry.githubLatest !== entry.npmLatest
|
|
1078
|
+
? t('latestBothSources', {
|
|
1079
|
+
npm: entry.npmLatest || '?',
|
|
1080
|
+
github: entry.githubLatest || '?',
|
|
1081
|
+
winner: entry.resolvedFrom === 'tag'
|
|
1082
|
+
? t('latestSourceTag')
|
|
1083
|
+
: entry.resolvedFrom === 'npm' ? t('latestSourceNpm') : t('latestSourceGithub'),
|
|
1084
|
+
})
|
|
1085
|
+
: entry.githubLatest !== undefined && entry.npmLatest !== undefined
|
|
1086
|
+
&& entry.githubLatest === entry.npmLatest
|
|
1087
|
+
? t('latestTieNpm')
|
|
1088
|
+
: entry.resolvedFrom === 'tag' || entry.source === 'tag'
|
|
1089
|
+
? t('latestSourceTag')
|
|
1090
|
+
: entry.source === 'github' || entry.source === 'release'
|
|
1091
|
+
? t('latestSourceGithub')
|
|
1092
|
+
: t('latestSourceNpm')),
|
|
1093
|
+
),
|
|
1094
|
+
entry.outdated === true
|
|
1095
|
+
? h('div', { className: 'dsh-ps-choices' },
|
|
1096
|
+
h('select', {
|
|
1097
|
+
className: 'dsh-ps-select',
|
|
1098
|
+
value: latestChannel[entry.name] ?? 'auto',
|
|
1099
|
+
disabled: latestUpdating,
|
|
1100
|
+
onChange: (event) => {
|
|
1101
|
+
const value = event.target.value
|
|
1102
|
+
setLatestChannel((current) => ({ ...current, [entry.name]: value }))
|
|
1103
|
+
// Choosing "skip" also unticks the row.
|
|
1104
|
+
setLatestPicked((current) => ({ ...current, [entry.name]: value !== 'skip' }))
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
h('option', { value: 'auto' }, t('channelAuto', {
|
|
1108
|
+
version: entry.latest ?? '?',
|
|
1109
|
+
})),
|
|
1110
|
+
h('option', { value: 'npm', disabled: !canUseNpm }, t('channelNpm', {
|
|
1111
|
+
version: npmChannelVersion ?? t('channelUnavailable'),
|
|
1112
|
+
})),
|
|
1113
|
+
h('option', { value: 'github', disabled: !canUseGithub }, t('channelGithub', {
|
|
1114
|
+
version: githubChannelVersion ?? t('channelUnavailable'),
|
|
1115
|
+
})),
|
|
1116
|
+
h('option', { value: 'skip' }, t('channelSkip')),
|
|
1117
|
+
),
|
|
1118
|
+
h('input', {
|
|
1119
|
+
type: 'checkbox',
|
|
1120
|
+
checked: latestPicked[entry.name] === true,
|
|
1121
|
+
disabled: latestUpdating,
|
|
1122
|
+
onChange: (event) => {
|
|
1123
|
+
const value = event.target.checked
|
|
1124
|
+
setLatestPicked((current) => ({ ...current, [entry.name]: value }))
|
|
1125
|
+
},
|
|
1126
|
+
}),
|
|
1127
|
+
)
|
|
1128
|
+
: h('span', { className: `dsh-ps-badge ${entry.error === undefined ? 'ok' : 'warn'}` },
|
|
1129
|
+
entry.error === undefined ? t('latestCurrent') : t('latestUnsupported')),
|
|
1130
|
+
),
|
|
1131
|
+
)
|
|
1132
|
+
}),
|
|
1133
|
+
),
|
|
1134
|
+
h('div', { className: 'dsh-ps-actions' },
|
|
1135
|
+
h('button', {
|
|
1136
|
+
type: 'button',
|
|
1137
|
+
className: 'dsh-ps-btn primary',
|
|
1138
|
+
disabled: latestUpdating || hostBusy || Object.keys(latestPicked).filter((n) => latestPicked[n]).length === 0,
|
|
1139
|
+
onClick: () => { void updateLatest() },
|
|
1140
|
+
}, latestUpdating ? t('latestUpdating') : t('latestUpdateAll')),
|
|
1141
|
+
),
|
|
1142
|
+
latestUpdating || checking ? h('div', { className: 'dsh-ps-progress' },
|
|
1143
|
+
h('div', { className: 'dsh-ps-progress-track' },
|
|
1144
|
+
h('div', {
|
|
1145
|
+
className: 'dsh-ps-progress-bar',
|
|
1146
|
+
style: {
|
|
1147
|
+
width: job?.progress?.total > 0
|
|
1148
|
+
? `${Math.round((job.progress.done / job.progress.total) * 100)}%`
|
|
1149
|
+
: '0%',
|
|
1150
|
+
},
|
|
1151
|
+
}),
|
|
1152
|
+
),
|
|
1153
|
+
h('span', { className: 'dsh-ps-muted' },
|
|
1154
|
+
job?.progress?.total > 0
|
|
1155
|
+
? t('latestProgress', {
|
|
1156
|
+
done: job.progress.done,
|
|
1157
|
+
total: job.progress.total,
|
|
1158
|
+
name: job.lines[job.lines.length - 1]?.message ?? '',
|
|
1159
|
+
})
|
|
1160
|
+
: t('latestStarting')),
|
|
1161
|
+
) : null,
|
|
1162
|
+
),
|
|
1163
|
+
latestApplied === null ? null : h('div', { className: 'dsh-ps-result' },
|
|
1164
|
+
latestApplied.updatedList?.length
|
|
1165
|
+
? latestApplied.updatedList.map((item) => h('div', { className: 'dsh-ps-meta', key: `u-${item.name}` },
|
|
1166
|
+
`${item.name}: ${item.replaced ?? ''} → ${item.version ?? item.latest ?? ''}`))
|
|
1167
|
+
: null,
|
|
1168
|
+
latestApplied.failed?.length
|
|
1169
|
+
? latestApplied.failed.map((item) => h('div', { className: 'dsh-ps-error', key: `uf-${item.name}` },
|
|
1170
|
+
`${item.name}: ${item.error ?? ''}`))
|
|
1171
|
+
: null,
|
|
1172
|
+
),
|
|
1173
|
+
),
|
|
1174
|
+
|
|
1175
|
+
tab !== 'sync' ? null : h('section', { className: 'dsh-ps-card' },
|
|
1176
|
+
h('div', { className: 'dsh-ps-head' },
|
|
1177
|
+
h('div', null,
|
|
1178
|
+
h('h3', { className: 'dsh-ps-title' }, t('manualTitle')),
|
|
1179
|
+
h('p', { className: 'dsh-ps-desc' }, t('manualHint')),
|
|
1180
|
+
),
|
|
1181
|
+
),
|
|
1182
|
+
h('div', { className: 'dsh-ps-summary' },
|
|
1183
|
+
h('span', { className: `dsh-ps-badge ${gistConfigured ? 'ok' : 'warn'}` },
|
|
1184
|
+
gistConfigured
|
|
1185
|
+
? t('gistLabel', { gistName: effectiveGistName })
|
|
1186
|
+
: t('gistNameLabel', { gistName: effectiveGistName })),
|
|
1187
|
+
gistUri === null || gistDirty ? null : h('a', {
|
|
1188
|
+
className: 'dsh-ps-muted',
|
|
1189
|
+
href: gistUri,
|
|
1190
|
+
target: '_blank',
|
|
1191
|
+
rel: 'noreferrer',
|
|
1192
|
+
}, gistUri),
|
|
1193
|
+
h('span', { className: 'dsh-ps-muted' }, t('lastPreview', { time: lastPreview ?? t('never') })),
|
|
1194
|
+
h('span', { className: 'dsh-ps-muted' }, t('lastApply', { time: lastApply ?? t('never') })),
|
|
1195
|
+
),
|
|
1196
|
+
h('div', { className: 'dsh-ps-actions' },
|
|
1197
|
+
h('button', {
|
|
1198
|
+
type: 'button',
|
|
1199
|
+
className: 'dsh-ps-btn primary',
|
|
1200
|
+
disabled: previewing || !canOperate,
|
|
1201
|
+
onClick: () => { void pullPreview() },
|
|
1202
|
+
}, previewing ? t('pulling') : (preview === null ? t('pull') : t('refresh'))),
|
|
1203
|
+
preview === null ? null : h('button', {
|
|
1204
|
+
type: 'button',
|
|
1205
|
+
className: 'dsh-ps-btn',
|
|
1206
|
+
disabled: previewing || applying,
|
|
1207
|
+
onClick: () => { setPreview(null); setApplyResult(null) },
|
|
1208
|
+
}, t('discard')),
|
|
1209
|
+
),
|
|
1210
|
+
gistDirty
|
|
1211
|
+
? h('div', { className: 'dsh-ps-alert' }, t('saveGistFirst'))
|
|
1212
|
+
: !gistConfigured ? h('div', { className: 'dsh-ps-alert' }, t('gistMissing')) : null,
|
|
1213
|
+
preview === null ? null : h(React.Fragment, null,
|
|
1214
|
+
h('div', { className: 'dsh-ps-summary' },
|
|
1215
|
+
h('span', { className: 'dsh-ps-badge' }, t('previewSummary', {
|
|
1216
|
+
hosts: preview.remote?.hostCount ?? 0,
|
|
1217
|
+
bundles: preview.remote?.bundleCount ?? 0,
|
|
1218
|
+
actionable: preview.counts?.actionable ?? 0,
|
|
1219
|
+
conflicts: preview.counts?.conflicts ?? 0,
|
|
1220
|
+
})),
|
|
1221
|
+
h('span', { className: `dsh-ps-badge ${preview.counts?.conflicts ? 'warn' : 'ok'}` },
|
|
1222
|
+
t('conflictCount', { count: preview.counts?.conflicts ?? 0 })),
|
|
1223
|
+
h('span', { className: 'dsh-ps-badge' },
|
|
1224
|
+
t('actionableCount', { count: selectedCount })),
|
|
1225
|
+
),
|
|
1226
|
+
h('p', { className: 'dsh-ps-hint' }, t('conflictRules')),
|
|
1227
|
+
visibleEntries.length === 0
|
|
1228
|
+
? h('p', { className: 'dsh-ps-desc' }, t('noChanges'))
|
|
1229
|
+
: h('div', { className: 'dsh-ps-list' },
|
|
1230
|
+
visibleEntries.map((entry) => h('div', { className: 'dsh-ps-entry', key: entry.id },
|
|
1231
|
+
h('div', { className: 'dsh-ps-entry-head' },
|
|
1232
|
+
h('div', null,
|
|
1233
|
+
h('div', { className: 'dsh-ps-name' }, entry.name),
|
|
1234
|
+
h('div', { className: 'dsh-ps-meta' }, versionLine(entry, t)),
|
|
1235
|
+
entry.remote?.hosts?.length
|
|
1236
|
+
? h('div', { className: 'dsh-ps-meta' }, t('sourceHosts', { hosts: entry.remote.hosts.join(', ') }))
|
|
1237
|
+
: null,
|
|
1238
|
+
),
|
|
1239
|
+
h('select', {
|
|
1240
|
+
className: 'dsh-ps-select',
|
|
1241
|
+
value: selections[entry.id] ?? entry.defaultChoice,
|
|
1242
|
+
disabled: applying,
|
|
1243
|
+
onChange: (event) => {
|
|
1244
|
+
const value = event.target.value
|
|
1245
|
+
setSelections((current) => ({ ...current, [entry.id]: value }))
|
|
1246
|
+
},
|
|
1247
|
+
}, entry.choices.map((choice) => h('option', { key: choice, value: choice }, choiceLabel(entry, choice, t)))),
|
|
1248
|
+
),
|
|
1249
|
+
entry.conflict === null ? null : h('div', { className: 'dsh-ps-conflict' }, conflictLabel(entry.conflict, t)),
|
|
1250
|
+
entry.kind === 'local-only'
|
|
1251
|
+
? h('div', { className: 'dsh-ps-meta' }, t('localOnlyHint'))
|
|
1252
|
+
: null,
|
|
1253
|
+
entry.kind === 'local-only' && !entry.local?.removable
|
|
1254
|
+
? h('div', { className: 'dsh-ps-conflict' }, t('localOnlyFixed'))
|
|
1255
|
+
: null,
|
|
1256
|
+
entry.manualUpdate === true
|
|
1257
|
+
? h('div', { className: 'dsh-ps-conflict' },
|
|
1258
|
+
h('span', { className: 'dsh-ps-badge warn' }, t('manualUpdateBadge')),
|
|
1259
|
+
` ${t('manualUpdateHint')}`)
|
|
1260
|
+
: null,
|
|
1261
|
+
entry.conflict?.type === 'host-disagreement' && entry.remote?.variants?.length
|
|
1262
|
+
? h('div', { className: 'dsh-ps-variants' },
|
|
1263
|
+
entry.remote.variants.map((variant) => h('span', {
|
|
1264
|
+
className: 'dsh-ps-variant',
|
|
1265
|
+
key: `${variant.hostId}:${variant.spec}`,
|
|
1266
|
+
}, `${variant.hostId}: ${variant.version || variant.spec}`)))
|
|
1267
|
+
: null,
|
|
1268
|
+
)),
|
|
1269
|
+
),
|
|
1270
|
+
removeCount > 0 ? h('div', { className: 'dsh-ps-alert' }, t('removeWarn')) : null,
|
|
1271
|
+
h('div', { className: 'dsh-ps-actions' },
|
|
1272
|
+
h('button', {
|
|
1273
|
+
type: 'button',
|
|
1274
|
+
className: removeCount > 0 ? 'dsh-ps-btn danger' : 'dsh-ps-btn primary',
|
|
1275
|
+
disabled: applying || selectedCount === 0,
|
|
1276
|
+
onClick: () => {
|
|
1277
|
+
if (removeCount > 0 && confirmRemove !== preview.previewId) {
|
|
1278
|
+
setConfirmRemove(preview.previewId)
|
|
1279
|
+
return
|
|
1280
|
+
}
|
|
1281
|
+
setConfirmRemove(null)
|
|
1282
|
+
void applyPreview()
|
|
1283
|
+
},
|
|
1284
|
+
}, applying
|
|
1285
|
+
? t('applying')
|
|
1286
|
+
: removeCount > 0 && confirmRemove !== preview.previewId
|
|
1287
|
+
? t('applyWithRemove', { count: selectedCount, remove: removeCount })
|
|
1288
|
+
: t('apply')),
|
|
1289
|
+
h('span', { className: 'dsh-ps-muted' }, `${selectedCount} / ${visibleEntries.length}`),
|
|
1290
|
+
),
|
|
1291
|
+
),
|
|
1292
|
+
applyResult === null ? null : h('div', { className: 'dsh-ps-result' },
|
|
1293
|
+
h('div', { className: applyResult.ok ? 'dsh-ps-ok' : 'dsh-ps-error' },
|
|
1294
|
+
applyResult.ok
|
|
1295
|
+
? t('applyOk', { applied: applyResult.totalApplied, failed: 0 })
|
|
1296
|
+
: t('applyPartial', { failed: applyResult.failed.length })),
|
|
1297
|
+
applyResult.applied.length
|
|
1298
|
+
? h('div', null,
|
|
1299
|
+
h('div', { className: 'dsh-ps-label' }, t('appliedTitle')),
|
|
1300
|
+
applyResult.applied.map((item) => h('div', { className: 'dsh-ps-meta', key: `a-${item.name}` },
|
|
1301
|
+
`${item.name}`
|
|
1302
|
+
+ `${item.upgraded === true && item.replaced ? ` (${item.replaced} → ${item.version ?? ''})` : ''}`
|
|
1303
|
+
+ `${item.rollback === 'restored' ? ` — ${t('rollbackRestored')}` : ''}`
|
|
1304
|
+
+ `${item.restartRequired ? ` — ${t('restartRequired')}` : ''}`)),
|
|
1305
|
+
)
|
|
1306
|
+
: null,
|
|
1307
|
+
applyResult.removed?.length
|
|
1308
|
+
? h('div', null,
|
|
1309
|
+
h('div', { className: 'dsh-ps-label' }, t('removedTitle')),
|
|
1310
|
+
applyResult.removed.map((item) => h('div', { className: 'dsh-ps-meta', key: `r-${item.name}` },
|
|
1311
|
+
`${item.name}${item.restartRequired ? ` — ${t('restartRequired')}` : ''}`)),
|
|
1312
|
+
)
|
|
1313
|
+
: null,
|
|
1314
|
+
applyResult.failed.length
|
|
1315
|
+
? h('div', null,
|
|
1316
|
+
h('div', { className: 'dsh-ps-label' }, t('failedTitle')),
|
|
1317
|
+
applyResult.failed.map((item) => h('div', { className: 'dsh-ps-error', key: `f-${item.name}` },
|
|
1318
|
+
`${item.name}: ${item.error ?? item.message ?? ''}`)),
|
|
1319
|
+
)
|
|
1320
|
+
: null,
|
|
1321
|
+
manualItems.length > 0
|
|
1322
|
+
? h('div', { className: 'dsh-ps-confirm' },
|
|
1323
|
+
h('div', { className: 'dsh-ps-step-title' }, t('manualIntro')),
|
|
1324
|
+
manualItems.map((item) => h('div', { className: 'dsh-ps-steps', key: `m-${item.name}` },
|
|
1325
|
+
h('div', { className: 'dsh-ps-step' },
|
|
1326
|
+
h('div', { className: 'dsh-ps-step-title' }, item.name)),
|
|
1327
|
+
(item.manual?.steps ?? []).map((step, index) => h('div', { className: 'dsh-ps-step', key: `${item.name}-s${index}` },
|
|
1328
|
+
h('div', { className: 'dsh-ps-step-title' }, `${index + 1}. ${step.title}`),
|
|
1329
|
+
h('div', { className: 'dsh-ps-meta' }, step.detail),
|
|
1330
|
+
step.command === undefined || step.command === ''
|
|
1331
|
+
? null
|
|
1332
|
+
: h('code', { className: 'dsh-ps-cmd' }, step.command),
|
|
1333
|
+
)),
|
|
1334
|
+
)),
|
|
1335
|
+
h('p', { className: 'dsh-ps-hint' }, t('manualNote')),
|
|
1336
|
+
)
|
|
1337
|
+
: null,
|
|
1338
|
+
applyResult.skipped.length
|
|
1339
|
+
? h('div', null,
|
|
1340
|
+
h('div', { className: 'dsh-ps-label' }, t('skippedTitle')),
|
|
1341
|
+
applyResult.skipped.map((item) => h('div', { className: 'dsh-ps-meta', key: `s-${item.name}` },
|
|
1342
|
+
`${item.name}: ${item.reason}`)),
|
|
1343
|
+
)
|
|
1344
|
+
: null,
|
|
1345
|
+
),
|
|
1346
|
+
),
|
|
1347
|
+
|
|
1348
|
+
h('section', { className: 'dsh-ps-card' },
|
|
1349
|
+
h('div', { className: 'dsh-ps-head' },
|
|
1350
|
+
h('div', null,
|
|
1351
|
+
h('h3', { className: 'dsh-ps-title' }, t('pushTitle')),
|
|
1352
|
+
h('p', { className: 'dsh-ps-desc' }, t('pushHint')),
|
|
1353
|
+
),
|
|
1354
|
+
h('span', { className: 'dsh-ps-muted' }, t('lastPush', { time: lastPush ?? t('never') })),
|
|
1355
|
+
),
|
|
1356
|
+
pushResult === null ? null : h('div', { className: 'dsh-ps-ok' },
|
|
1357
|
+
pushResult.created === true
|
|
1358
|
+
? t('pushCreated', { gistName: pushResult.gistName ?? effectiveGistName })
|
|
1359
|
+
: pushResult.status === 'pushed'
|
|
1360
|
+
? t('pushPushed', { version: (pushResult.version ?? '').slice(0, 12) })
|
|
1361
|
+
: t('pushUnchanged')),
|
|
1362
|
+
confirmPush
|
|
1363
|
+
? h('div', { className: 'dsh-ps-confirm' },
|
|
1364
|
+
h('div', { className: 'dsh-ps-step-title' }, t('pushPlanTitle')),
|
|
1365
|
+
h('p', { className: 'dsh-ps-hint' }, t('pushPlanHint')),
|
|
1366
|
+
pushPlan === null
|
|
1367
|
+
? h('p', { className: 'dsh-ps-desc' }, gistConfigured ? t('pushConfirm') : t('pushConfirmCreate'))
|
|
1368
|
+
: h(React.Fragment, null,
|
|
1369
|
+
h('div', { className: 'dsh-ps-summary' },
|
|
1370
|
+
h('span', { className: 'dsh-ps-badge warn' },
|
|
1371
|
+
`${t('pushPlanConflict')} ${pushPlan.counts.conflict}`),
|
|
1372
|
+
h('span', { className: 'dsh-ps-badge warn' },
|
|
1373
|
+
`${t('pushPlanRemoteOnly')} ${pushPlan.counts.remoteOnly}`),
|
|
1374
|
+
h('span', { className: 'dsh-ps-badge ok' },
|
|
1375
|
+
`${t('pushPlanLocalOnly')} ${pushPlan.counts.localOnly}`),
|
|
1376
|
+
h('span', { className: 'dsh-ps-badge' },
|
|
1377
|
+
t('pushPlanShowing', {
|
|
1378
|
+
shown: pushVisibleEntries.length,
|
|
1379
|
+
total: pushPlan.counts.entries,
|
|
1380
|
+
same: pushPlan.counts.same,
|
|
1381
|
+
})),
|
|
1382
|
+
),
|
|
1383
|
+
h('div', { className: 'dsh-ps-list' },
|
|
1384
|
+
pushVisibleEntries.map((entry) => h('div', { className: 'dsh-ps-entry', key: `pp-${entry.id}` },
|
|
1385
|
+
h('div', { className: 'dsh-ps-entry-head' },
|
|
1386
|
+
h('div', null,
|
|
1387
|
+
h('div', { className: 'dsh-ps-name' }, entry.name),
|
|
1388
|
+
h('div', { className: 'dsh-ps-meta' },
|
|
1389
|
+
entry.kind === 'remote-only'
|
|
1390
|
+
? t('pushPlanRemoteOnlyHint')
|
|
1391
|
+
: entry.kind === 'local-only'
|
|
1392
|
+
? t('pushPlanLocalOnlyHint')
|
|
1393
|
+
: entry.newest === 'local'
|
|
1394
|
+
? t('pushPlanNewestLocal')
|
|
1395
|
+
: t('pushPlanNewestRemote')),
|
|
1396
|
+
h('div', { className: 'dsh-ps-meta' },
|
|
1397
|
+
`${entry.local === null ? '—' : t('pushPlanLocal', { version: entry.local.version || '?' })}`
|
|
1398
|
+
+ ` · `
|
|
1399
|
+
+ `${entry.remote === null ? '—' : t('pushPlanRemote', { version: entry.remote.version || '?' })}`),
|
|
1400
|
+
),
|
|
1401
|
+
entry.choices.length <= 1
|
|
1402
|
+
? h('span', { className: 'dsh-ps-badge ok' },
|
|
1403
|
+
entry.kind === 'local-only' ? t('pushPlanLocalOnly')
|
|
1404
|
+
: entry.kind === 'same' ? t('pushPlanSame') : t('pushPlanConflict'))
|
|
1405
|
+
: h('select', {
|
|
1406
|
+
className: 'dsh-ps-select',
|
|
1407
|
+
value: pushSelections[entry.id] ?? entry.defaultChoice,
|
|
1408
|
+
disabled: pushing,
|
|
1409
|
+
onChange: (event) => {
|
|
1410
|
+
const value = event.target.value
|
|
1411
|
+
setPushSelections((current) => ({ ...current, [entry.id]: value }))
|
|
1412
|
+
},
|
|
1413
|
+
}, entry.choices.map((choice) => h('option', { key: choice, value: choice },
|
|
1414
|
+
choice === 'newest' ? t('choiceNewest')
|
|
1415
|
+
: choice === 'local' ? t('choiceLocal')
|
|
1416
|
+
: choice === 'remote' ? t('choiceRemote')
|
|
1417
|
+
: choice === 'keep' ? t('choiceKeep')
|
|
1418
|
+
: choice === 'drop' ? t('choiceDrop') : t('choiceSkip')))),
|
|
1419
|
+
),
|
|
1420
|
+
)),
|
|
1421
|
+
),
|
|
1422
|
+
),
|
|
1423
|
+
h('div', { className: 'dsh-ps-actions' },
|
|
1424
|
+
h('button', {
|
|
1425
|
+
type: 'button',
|
|
1426
|
+
className: 'dsh-ps-btn danger',
|
|
1427
|
+
disabled: pushing || pushPlan === null,
|
|
1428
|
+
onClick: () => { void pushNow() },
|
|
1429
|
+
}, pushing ? t('pushing') : t('pushPlanConfirm')),
|
|
1430
|
+
h('button', {
|
|
1431
|
+
type: 'button',
|
|
1432
|
+
className: 'dsh-ps-btn',
|
|
1433
|
+
disabled: pushing,
|
|
1434
|
+
onClick: () => { setConfirmPush(false); setPushPlan(null) },
|
|
1435
|
+
}, t('pushCancel')),
|
|
1436
|
+
),
|
|
1437
|
+
)
|
|
1438
|
+
: h('div', { className: 'dsh-ps-actions' },
|
|
1439
|
+
h('button', {
|
|
1440
|
+
type: 'button',
|
|
1441
|
+
className: 'dsh-ps-btn',
|
|
1442
|
+
disabled: pushing || planning || token?.configured !== true || gistDirty,
|
|
1443
|
+
onClick: () => { void openPushPlan() },
|
|
1444
|
+
}, planning ? t('latestStarting') : (gistConfigured ? t('push') : t('pushCreating'))),
|
|
1445
|
+
),
|
|
1446
|
+
),
|
|
1447
|
+
|
|
1448
|
+
h('section', { className: 'dsh-ps-card' },
|
|
1449
|
+
h('div', { className: 'dsh-ps-head' },
|
|
1450
|
+
h('div', null,
|
|
1451
|
+
h('h3', { className: 'dsh-ps-title' }, t('logTitle')),
|
|
1452
|
+
h('p', { className: 'dsh-ps-desc' }, t('installMethodTitle')),
|
|
1453
|
+
),
|
|
1454
|
+
h('button', {
|
|
1455
|
+
type: 'button',
|
|
1456
|
+
className: 'dsh-ps-btn',
|
|
1457
|
+
onClick: () => { setShowLog((current) => !current) },
|
|
1458
|
+
}, showLog ? t('logHide') : t('logShow')),
|
|
1459
|
+
),
|
|
1460
|
+
h('p', { className: 'dsh-ps-hint' }, t('installMethodValue')),
|
|
1461
|
+
h('p', { className: 'dsh-ps-hint' }, t('upgradeNote')),
|
|
1462
|
+
pendingUpgrades.length > 0
|
|
1463
|
+
? h('div', { className: 'dsh-ps-alert' },
|
|
1464
|
+
h('div', null, t('pendingTitle')),
|
|
1465
|
+
h('div', { className: 'dsh-ps-meta' }, t('pendingBody')),
|
|
1466
|
+
pendingUpgrades.map((item) => h('div', { className: 'dsh-ps-meta', key: `p-${item.name}` },
|
|
1467
|
+
`${item.name}: ${item.fromVersion || item.fromSpec} -> ${item.toSpec}`)),
|
|
1468
|
+
)
|
|
1469
|
+
: null,
|
|
1470
|
+
h('div', { className: 'dsh-ps-summary' },
|
|
1471
|
+
h('span', { className: `dsh-ps-badge ${job?.status === 'failed' ? 'error' : job?.status === 'done' ? 'ok' : ''}` },
|
|
1472
|
+
job === null ? t('logEmpty') : `${job.kind} · ${job.status}`),
|
|
1473
|
+
job?.progress?.total > 0
|
|
1474
|
+
? h('span', { className: 'dsh-ps-badge' },
|
|
1475
|
+
t('logProgress', { done: job.progress.done, total: job.progress.total }))
|
|
1476
|
+
: null,
|
|
1477
|
+
),
|
|
1478
|
+
showLog
|
|
1479
|
+
? h('div', { className: 'dsh-ps-log' },
|
|
1480
|
+
job === null || job.lines.length === 0
|
|
1481
|
+
? h('p', { className: 'dsh-ps-muted' }, t('logEmpty'))
|
|
1482
|
+
: job.lines.map((line, index) => h('div', {
|
|
1483
|
+
className: `dsh-ps-logline ${line.level === 'error' ? 'error' : line.level === 'warn' ? 'warn' : ''}`,
|
|
1484
|
+
key: `${index}-${line.at}`,
|
|
1485
|
+
}, `${line.at.slice(11, 19)} ${line.message}`)),
|
|
1486
|
+
)
|
|
1487
|
+
: null,
|
|
1488
|
+
),
|
|
1489
|
+
)
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
return {
|
|
1493
|
+
inject: ['slots', 'locale'],
|
|
1494
|
+
apply(ctx) {
|
|
1495
|
+
const t = ctx.locale.bind(NS)
|
|
1496
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'plugin-sync: dictionaries')
|
|
1497
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
1498
|
+
name: 'plugins.bundle.config',
|
|
1499
|
+
key: 'dsh-plugin-syncup',
|
|
1500
|
+
locale: NS,
|
|
1501
|
+
inject: () => ({ t }),
|
|
1502
|
+
}, ConfigPage))
|
|
1503
|
+
},
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
})
|