dsh-app-updater 1.3.0 → 1.3.2
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/CHANGELOG.md +75 -0
- package/CONTRACT.md +20 -9
- package/README.en.md +37 -12
- package/README.md +36 -12
- package/lib/client.js +68 -4
- package/lib/engine.js +61 -65
- package/lib/index.js +122 -26
- package/lib/macos.js +409 -0
- package/lib/platform.js +7 -5
- package/lib/state.js +2 -0
- package/package.json +2 -2
- package/test/deny.mjs +149 -0
- package/test/harness.mjs +3 -0
- package/test/render.mjs +75 -1
- package/test/unit.mjs +212 -6
package/lib/engine.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { readRegistryApps, normalizeName } from "./registry.js";
|
|
28
28
|
import { compareVersions, isNewer, looksLikeVersion } from "./version.js";
|
|
29
29
|
import { detectEnvironment, describeEnvironment, engineFor, ENGINE_BREW, ENGINE_WINGET } from "./platform.js";
|
|
30
|
+
import { scanAppBundles, fetchCaskCatalog, parseCaskCatalog, buildMacosItems } from "./macos.js";
|
|
30
31
|
|
|
31
32
|
const INSTALLER_EXT = /\.(exe|msi|msix|appx|msixbundle|pkg|dmg)$/i;
|
|
32
33
|
|
|
@@ -1113,88 +1114,70 @@ export function parseBrewOutdated(text) {
|
|
|
1113
1114
|
|
|
1114
1115
|
async function scanMacos({ settings, log, signal, env, startedAt, warnings }) {
|
|
1115
1116
|
const brewPath = findBrew();
|
|
1116
|
-
const items = [];
|
|
1117
1117
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1118
|
+
/* ---------------------------------------------------------------- *
|
|
1119
|
+
* 1. 先枚举「本机到底装了哪些应用」——这一步不依赖 Homebrew。
|
|
1120
|
+
* 旧实现只读 `brew list --cask`,没走 Homebrew 装的应用一个都看不到,
|
|
1121
|
+
* 于是正常 Mac 上就是「未检测到任何已安装应用」。
|
|
1122
|
+
* ---------------------------------------------------------------- */
|
|
1123
|
+
log("info", "正在枚举应用目录(/Applications 等)…");
|
|
1124
|
+
const apps = await scanAppBundles({ log });
|
|
1125
|
+
|
|
1126
|
+
/* ---------------------------------------------------------------- *
|
|
1127
|
+
* 2. Homebrew:哪些 cask 已装、哪些有新版(没装 Homebrew 也继续)。
|
|
1128
|
+
* ---------------------------------------------------------------- */
|
|
1129
|
+
let brewVersion = "";
|
|
1130
|
+
const brews = new Map();
|
|
1131
|
+
const outdated = new Map();
|
|
1132
|
+
|
|
1133
|
+
if (brewPath) {
|
|
1134
|
+
const verRes = await runBrew(brewPath, ["--version"], { timeoutMs: 30000 });
|
|
1135
|
+
brewVersion = firstLine(outputText(verRes)).replace(/^Homebrew\s+/i, "");
|
|
1136
|
+
log("info", `Homebrew ${brewVersion}(${brewPath})· ${env.archLabel}`);
|
|
1137
|
+
|
|
1138
|
+
const listRes = await runBrew(brewPath, ["list", "--cask", "--versions"], { signal });
|
|
1139
|
+
for (const [token, version] of parseBrewCaskVersions(listRes.stdout)) brews.set(token, version);
|
|
1140
|
+
log("info", `Homebrew 报告 ${brews.size} 个已安装 cask`);
|
|
1141
|
+
|
|
1142
|
+
const outRes = await runBrew(brewPath, ["outdated", "--json=v2", "--cask", "--greedy"], { signal });
|
|
1143
|
+
for (const [token, version] of parseBrewOutdated(outRes.stdout)) outdated.set(token, version);
|
|
1144
|
+
log("info", `其中 ${outdated.size} 个有新版本`);
|
|
1145
|
+
} else {
|
|
1146
|
+
warnings.push("未找到 Homebrew:可以列出已安装应用,但无法判断/驱动更新(安装 Homebrew 后可一键升级 cask 应用)");
|
|
1120
1147
|
log("warn", warnings[warnings.length - 1]);
|
|
1121
|
-
return {
|
|
1122
|
-
env,
|
|
1123
|
-
engine: { id: ENGINE_BREW, available: false, version: "", path: "" },
|
|
1124
|
-
scannedAt: Date.now(),
|
|
1125
|
-
startedAt,
|
|
1126
|
-
stats: { total: 0, upgradable: 0, unsupported: 0 },
|
|
1127
|
-
items,
|
|
1128
|
-
warnings,
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
const verRes = await runBrew(brewPath, ["--version"], { timeoutMs: 30000 });
|
|
1133
|
-
const brewVersion = firstLine(outputText(verRes)).replace(/^Homebrew\s+/i, "");
|
|
1134
|
-
log("info", `Homebrew ${brewVersion}(${brewPath})· ${env.archLabel}`);
|
|
1135
|
-
|
|
1136
|
-
const listRes = await runBrew(brewPath, ["list", "--cask", "--versions"], { signal });
|
|
1137
|
-
const installed = parseBrewCaskVersions(listRes.stdout);
|
|
1138
|
-
log("info", `Homebrew 报告 ${installed.size} 个已安装 cask 应用`);
|
|
1139
|
-
|
|
1140
|
-
const outRes = await runBrew(brewPath, ["outdated", "--json=v2", "--cask", "--greedy"], { signal });
|
|
1141
|
-
const outdated = parseBrewOutdated(outRes.stdout);
|
|
1142
|
-
log("info", `其中 ${outdated.size} 个有新版本`);
|
|
1143
|
-
|
|
1144
|
-
for (const [token, version] of installed) {
|
|
1145
|
-
const available = outdated.get(token) || "";
|
|
1146
|
-
const hasUpdate = Boolean(available) && (!version || isNewer(available, version));
|
|
1147
|
-
items.push({
|
|
1148
|
-
id: `brew:${token}`,
|
|
1149
|
-
name: token,
|
|
1150
|
-
version: version || "",
|
|
1151
|
-
available: hasUpdate ? available : "",
|
|
1152
|
-
publisher: "Homebrew cask",
|
|
1153
|
-
source: "homebrew",
|
|
1154
|
-
installLocation: "",
|
|
1155
|
-
hasUpdate,
|
|
1156
|
-
updateSupported: hasUpdate,
|
|
1157
|
-
reason: hasUpdate ? null : null,
|
|
1158
|
-
kind: "cask",
|
|
1159
|
-
});
|
|
1160
1148
|
}
|
|
1161
1149
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
publisher: "Homebrew cask",
|
|
1171
|
-
source: "homebrew",
|
|
1172
|
-
installLocation: "",
|
|
1173
|
-
hasUpdate: true,
|
|
1174
|
-
updateSupported: true,
|
|
1175
|
-
reason: null,
|
|
1176
|
-
kind: "cask",
|
|
1177
|
-
});
|
|
1150
|
+
/* ---------------------------------------------------------------- *
|
|
1151
|
+
* 3. cask 目录:把 .app 反查到 cask token,才能知道官网最新版本。
|
|
1152
|
+
* 抓不到就降级(只显示 Homebrew 自己报告的信息),不中断扫描。
|
|
1153
|
+
* ---------------------------------------------------------------- */
|
|
1154
|
+
let catalog = null;
|
|
1155
|
+
if (brewPath) {
|
|
1156
|
+
const list = await fetchCaskCatalog({ log, warnings });
|
|
1157
|
+
if (list) catalog = parseCaskCatalog(list);
|
|
1178
1158
|
}
|
|
1179
1159
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
});
|
|
1160
|
+
/* ---------------------------------------------------------------- *
|
|
1161
|
+
* 4. 合并:已装应用 + Homebrew 管理状态 → 最终列表
|
|
1162
|
+
* ---------------------------------------------------------------- */
|
|
1163
|
+
const built = buildMacosItems({ apps, brews, outdated, catalog, settings });
|
|
1164
|
+
const items = built.items;
|
|
1165
|
+
for (const w of built.warnings) if (!warnings.includes(w)) warnings.push(w);
|
|
1184
1166
|
|
|
1185
1167
|
const stats = {
|
|
1186
1168
|
total: items.length,
|
|
1187
1169
|
upgradable: items.filter((i) => i.hasUpdate).length,
|
|
1188
|
-
unsupported:
|
|
1170
|
+
unsupported: items.filter((i) => !i.updateSupported).length,
|
|
1189
1171
|
};
|
|
1190
1172
|
log("success", `扫描完成:${stats.total} 个应用,${stats.upgradable} 个可更新`);
|
|
1173
|
+
|
|
1191
1174
|
if (settings.includeStoreApps) {
|
|
1192
|
-
warnings.push("
|
|
1175
|
+
warnings.push("App Store 应用只列出、不由本插件更新(由 App Store 自己负责升级)");
|
|
1193
1176
|
}
|
|
1194
1177
|
|
|
1195
1178
|
return {
|
|
1196
1179
|
env,
|
|
1197
|
-
engine: { id: ENGINE_BREW, available:
|
|
1180
|
+
engine: { id: ENGINE_BREW, available: Boolean(brewPath), version: brewVersion, path: brewPath || "" },
|
|
1198
1181
|
scannedAt: Date.now(),
|
|
1199
1182
|
startedAt,
|
|
1200
1183
|
stats,
|
|
@@ -1227,6 +1210,19 @@ async function runMacosUpdates({ ids, settings, log, onProgress, signal, env })
|
|
|
1227
1210
|
log("warn", `已取消:${rawId}`);
|
|
1228
1211
|
break;
|
|
1229
1212
|
}
|
|
1213
|
+
// 非 Homebrew 管理的应用(macapp:…)没有可交给 brew 的 token:
|
|
1214
|
+
// 明确报「不由本插件更新」,而不是把一个 bundle id 当 cask 名去执行。
|
|
1215
|
+
if (!String(rawId).startsWith("brew:")) {
|
|
1216
|
+
index += 1;
|
|
1217
|
+
results.push({
|
|
1218
|
+
id: rawId,
|
|
1219
|
+
ok: false,
|
|
1220
|
+
error: "该应用不由 Homebrew 管理(拖 DMG / 手动安装),本插件只能列出、不能更新它;请在应用内或官网升级",
|
|
1221
|
+
});
|
|
1222
|
+
log("warn", `${rawId} 跳过:非 Homebrew 管理的应用`);
|
|
1223
|
+
onProgress({ running: true, total: queue.length, done: index, current: null, results });
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1230
1226
|
index += 1;
|
|
1231
1227
|
const token = String(rawId).replace(/^brew:/, "");
|
|
1232
1228
|
onProgress({ running: true, total: queue.length, done: index - 1, current: token });
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* dsh-app-updater 宿主半身。
|
|
3
3
|
*
|
|
4
4
|
* 职责:暴露 HTTP 路由给设置页 UI,串行调度「扫描」与「更新」两条后台任务,
|
|
5
|
-
*
|
|
5
|
+
* 把进度写进持久化状态。所有路由默认拒绝:先过宿主 connection 准入;写操作另需
|
|
6
|
+
* confirm:true。仅在用户显式开启兼容模式时,才按「回环地址」放行(默认关闭)。
|
|
6
7
|
*
|
|
7
8
|
* 设计上不依赖宿主内部服务(不用 llm / tools / agent),只依赖 webServer,
|
|
8
9
|
* 因此在没有 Web 的 composition 里它会安静地不挂载任何路由。
|
|
@@ -86,46 +87,110 @@ export function hasConnectionCredentials(req) {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
|
-
*
|
|
90
|
+
* 兼容模式开关:只允许在用户**显式开启**时按「回环地址」放行。
|
|
91
|
+
*
|
|
92
|
+
* 1.3.0 在 `connection` 不可用(或没有凭据可核验)时会直接按回环放行,旧宿主上
|
|
93
|
+
* 等于绕过了认证就能调用安装接口 —— 市场复核要求这条路径改为默认拒绝。
|
|
94
|
+
* 因此 1.3.1 起默认关闭,只有下面两种显式动作之一才恢复(并会在响应头、状态接口
|
|
95
|
+
* 与日志里标注,方便复核):
|
|
96
|
+
* - 插件配置 `allowLegacyLoopback: true`(cordis patch 的 `config`)
|
|
97
|
+
* - 环境变量 `DSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK=1`
|
|
98
|
+
*/
|
|
99
|
+
export const LEGACY_LOOPBACK_ENV = "DSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK";
|
|
100
|
+
|
|
101
|
+
function truthyFlag(value) {
|
|
102
|
+
if (value === true || value === 1) return true;
|
|
103
|
+
if (typeof value !== "string") return false;
|
|
104
|
+
return /^(1|true|yes|on)$/i.test(value.trim());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 解析兼容模式开关:插件配置优先,其次环境变量;默认(两者都没有)为 false。 */
|
|
108
|
+
export function resolveLegacyLoopbackOptIn(config, env = process.env) {
|
|
109
|
+
if (config && truthyFlag(config.allowLegacyLoopback)) return true;
|
|
110
|
+
return truthyFlag((env || {})[LEGACY_LOOPBACK_ENV]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 兼容模式放行时打标记,便于复核与排障:正常准入不会出现这两个头。 */
|
|
114
|
+
function markLegacyAdmission(res) {
|
|
115
|
+
if (typeof res.setHeader !== "function") return;
|
|
116
|
+
res.setHeader("X-DSH-Admission", "loopback-legacy-optin");
|
|
117
|
+
res.setHeader("X-DSH-Admission-Warning", "legacy-loopback-compatibility-enabled");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 统一写出拒绝响应,并标明是「被拒绝」而不是「未经过闸门」。 */
|
|
121
|
+
function denyAdmission(res, status, error) {
|
|
122
|
+
if (typeof res.setHeader === "function") res.setHeader("X-DSH-Admission", "denied");
|
|
123
|
+
json(res, status, { ok: false, error, admission: "denied" });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* connection 准入(宿主信任边界)。**默认拒绝**。
|
|
90
128
|
*
|
|
91
129
|
* DSH 的 `connection` 服务负责插件 HTTP 路由的信任校验:`admit(request)` 先做
|
|
92
130
|
* Host/Origin 校验,再做浏览器会话 / API 认证,返回本次调用对应的 Peer,或用
|
|
93
|
-
* `401`(未认证)/`403
|
|
131
|
+
* `401`(未认证)/`403`(不可信来源)/`409`(连接已被占用)拒绝 —— 宿主自己的
|
|
132
|
+
* `/api` 路由用的就是它。
|
|
94
133
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
134
|
+
* 规则(按顺序):
|
|
135
|
+
* 1. 宿主有 `connection`:一律以 `admit()` 为准,通过就放行;
|
|
136
|
+
* 只有「401 + 回环 + 没有任何凭据 + 用户显式开启兼容模式」这一种情况例外,
|
|
137
|
+
* 否则按 `admit()` 给出的状态码拒绝。
|
|
138
|
+
* 2. 宿主没有 `connection`:**默认拒绝**(403)。只有用户显式开启兼容模式、
|
|
139
|
+
* 且请求来自回环地址时才放行。
|
|
99
140
|
*
|
|
100
141
|
* @returns {boolean} 已放行 true;已写出拒绝响应 false
|
|
101
142
|
*/
|
|
102
|
-
export function admissionGate(req, res, connection) {
|
|
143
|
+
export function admissionGate(req, res, connection, options = {}) {
|
|
103
144
|
const loopback = isLoopback(req);
|
|
145
|
+
const optIn = options.allowLegacyLoopback === true;
|
|
104
146
|
if (connection && typeof connection.admit === "function") {
|
|
105
147
|
let admission;
|
|
106
148
|
try {
|
|
107
149
|
admission = connection.admit(req);
|
|
108
150
|
} catch (err) {
|
|
109
|
-
|
|
151
|
+
denyAdmission(res, 500, `连接准入失败:${String((err && err.message) || err)}`);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (!admission || typeof admission !== "object") {
|
|
155
|
+
// 宿主实现异常时宁可拒绝,也不要把「看不懂的结果」当成放行
|
|
156
|
+
denyAdmission(res, 500, "连接准入返回了无法识别的结果,按默认拒绝处理");
|
|
110
157
|
return false;
|
|
111
158
|
}
|
|
112
|
-
if (!
|
|
113
|
-
if (admission.rejection
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
159
|
+
if (!("rejection" in admission)) return true;
|
|
160
|
+
if (typeof admission.rejection !== "number") {
|
|
161
|
+
denyAdmission(res, 403, "连接准入返回了无法识别的拒绝码,按默认拒绝处理");
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (admission.rejection === 401 && loopback && !hasConnectionCredentials(req) && optIn) {
|
|
165
|
+
// 例外路径:宿主自己的载体(Electron 经 IPC 桥接的 fetch)与本机脚本都不带
|
|
166
|
+
// Cookie,没有可核验的凭据。只有用户显式开启兼容模式才退回回环信任。
|
|
167
|
+
markLegacyAdmission(res);
|
|
117
168
|
return true;
|
|
118
169
|
}
|
|
119
|
-
|
|
120
|
-
|
|
170
|
+
const status = admission.rejection === 403 ? 403 : admission.rejection === 409 ? 409 : 401;
|
|
171
|
+
denyAdmission(
|
|
172
|
+
res,
|
|
173
|
+
status,
|
|
174
|
+
status === 401
|
|
175
|
+
? "请求未通过 DSH connection 认证:缺少有效的会话 Cookie / API 凭据"
|
|
176
|
+
: status === 409
|
|
177
|
+
? "请求对应的连接已被占用"
|
|
178
|
+
: "请求来源不受信任:Host/Origin 校验未通过"
|
|
179
|
+
);
|
|
121
180
|
return false;
|
|
122
181
|
}
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
return
|
|
182
|
+
if (optIn && loopback) {
|
|
183
|
+
markLegacyAdmission(res);
|
|
184
|
+
return true;
|
|
126
185
|
}
|
|
127
|
-
|
|
128
|
-
|
|
186
|
+
denyAdmission(
|
|
187
|
+
res,
|
|
188
|
+
403,
|
|
189
|
+
optIn
|
|
190
|
+
? "只接受本机(loopback)请求:当前宿主没有可用的 connection 准入服务"
|
|
191
|
+
: "拒绝访问:当前宿主没有 connection 准入服务,兼容模式(仅回环放行)未开启,按默认拒绝处理"
|
|
192
|
+
);
|
|
193
|
+
return false;
|
|
129
194
|
}
|
|
130
195
|
|
|
131
196
|
/**
|
|
@@ -343,16 +408,30 @@ function openFolder(path) {
|
|
|
343
408
|
|
|
344
409
|
export default {
|
|
345
410
|
name: "dsh-app-updater",
|
|
346
|
-
apply(ctx) {
|
|
411
|
+
apply(ctx, config) {
|
|
412
|
+
// 兼容模式:默认关闭。只有用户显式写进插件配置或环境变量才开启(见 LEGACY_LOOPBACK_ENV)。
|
|
413
|
+
const allowLegacyLoopback = resolveLegacyLoopbackOptIn(config);
|
|
414
|
+
|
|
347
415
|
ctx.effect(() => {
|
|
348
|
-
store
|
|
416
|
+
store
|
|
417
|
+
.load()
|
|
418
|
+
.then(() => {
|
|
419
|
+
if (allowLegacyLoopback) {
|
|
420
|
+
store.log(
|
|
421
|
+
"warn",
|
|
422
|
+
"兼容模式(allowLegacyLoopback)已开启:没有连接凭据的回环请求会按本机信任放行;" +
|
|
423
|
+
"建议只在旧宿主(没有 connection 服务)上临时使用"
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
})
|
|
427
|
+
.catch(() => {});
|
|
349
428
|
}, "dsh-app-updater: load state");
|
|
350
429
|
|
|
351
430
|
ctx.inject(["webServer"], (webCtx) => {
|
|
352
431
|
const webServer = webCtx.webServer;
|
|
353
432
|
|
|
354
433
|
// connection 准入:宿主提供的信任边界(Host/Origin 校验 + 浏览器/API 认证)。
|
|
355
|
-
// 它在宿主没有 Web 载体时不存在,此时 admissionGate
|
|
434
|
+
// 它在宿主没有 Web 载体时不存在,此时 admissionGate 默认拒绝,除非兼容模式显式开启。
|
|
356
435
|
let connection = null;
|
|
357
436
|
ctx.inject(["connection"], (connCtx) => {
|
|
358
437
|
connection = connCtx.connection;
|
|
@@ -362,6 +441,22 @@ export default {
|
|
|
362
441
|
});
|
|
363
442
|
const routes = [];
|
|
364
443
|
|
|
444
|
+
/** 准入现状:写进状态接口,界面/复核者不必读代码就知道当前走的是哪条路径。 */
|
|
445
|
+
const admissionInfo = () => ({
|
|
446
|
+
mode: connection
|
|
447
|
+
? allowLegacyLoopback
|
|
448
|
+
? "connection+loopback-legacy"
|
|
449
|
+
: "connection"
|
|
450
|
+
: allowLegacyLoopback
|
|
451
|
+
? "loopback-legacy-optin"
|
|
452
|
+
: "deny",
|
|
453
|
+
service: Boolean(connection),
|
|
454
|
+
allowLegacyLoopback,
|
|
455
|
+
warning: allowLegacyLoopback
|
|
456
|
+
? "兼容模式已开启:没有连接凭据的本机回环请求会被放行,仅应在旧宿主上临时使用"
|
|
457
|
+
: null,
|
|
458
|
+
});
|
|
459
|
+
|
|
365
460
|
/**
|
|
366
461
|
* 注册一条路由:请求先过 connection 准入(见 admissionGate),再进业务处理。
|
|
367
462
|
* 写接口另有 `confirm:true` 闸门(writeGate)。
|
|
@@ -372,7 +467,7 @@ export default {
|
|
|
372
467
|
kind: "exact",
|
|
373
468
|
path: `${PREFIX}${path}`,
|
|
374
469
|
handler: (req, res) => {
|
|
375
|
-
if (!admissionGate(req, res, connection)) return;
|
|
470
|
+
if (!admissionGate(req, res, connection, { allowLegacyLoopback })) return;
|
|
376
471
|
return handler(req, res);
|
|
377
472
|
},
|
|
378
473
|
})
|
|
@@ -382,7 +477,7 @@ export default {
|
|
|
382
477
|
register("/state.json", async (req, res) => {
|
|
383
478
|
try {
|
|
384
479
|
await store.load();
|
|
385
|
-
json(res, 200, buildStatePayload());
|
|
480
|
+
json(res, 200, { ...buildStatePayload(), admission: admissionInfo() });
|
|
386
481
|
} catch (err) {
|
|
387
482
|
json(res, 500, { ok: false, error: String((err && err.message) || err) });
|
|
388
483
|
}
|
|
@@ -479,6 +574,7 @@ export default {
|
|
|
479
574
|
envSummary: describeEnvironment(env),
|
|
480
575
|
engine,
|
|
481
576
|
capabilities: capabilitiesFor(env, Boolean(engine.available)),
|
|
577
|
+
admission: admissionInfo(),
|
|
482
578
|
routes: routes.length,
|
|
483
579
|
dataDir: join(process.env.DSH_HOME || join(homedir(), ".dsh"), "dsh-app-updater"),
|
|
484
580
|
});
|