@noob-stupid/dsh-plugin-console 0.5.57 → 0.5.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -122,6 +122,11 @@ window.__ModuleLoader__.load({
122
122
  linksTitle: "开发式链接(link):共 {n} 个,自足 {ready} 个 —— ✓ 自足 · · 缺东西(悬停看原因)",
123
123
  healthSummary: "需要处理 {n} 类问题",
124
124
  exemptGrantBtn: "授精确版本豁免",
125
+ healthDismissBtn: "知道了",
126
+ healthDismissTitle: "关掉这条提示。问题发生变化(类别或条数改变)时会重新出现,不会让你错过新问题",
127
+ linksToggleBtn: "开发式链接(link)明细",
128
+ linksToggleSummary: "开发式链接(link):共 {n} 个,自足 {ready} 个",
129
+ linksToggleTitle: "展开/收起 link 明细(默认收起)",
125
130
  devRootsTitle: "开发区根(只有这些根里的目录能钉住)",
126
131
  devRootsNone: "(未登记)",
127
132
  devRootAddPlaceholder: "注册新的开发区根(绝对路径,例如 D:\\my-plugins)",
@@ -730,6 +735,11 @@ window.__ModuleLoader__.load({
730
735
  linksTitle: "Dev links (link:): {n} total, {ready} self-sufficient - checkmark = ready, dot = something missing (hover for why)",
731
736
  healthSummary: "{n} issue type(s) need attention",
732
737
  exemptGrantBtn: "Grant exact-version exemption",
738
+ healthDismissBtn: "Got it",
739
+ healthDismissTitle: "Dismiss this notice. It reappears when the problems change (kind or count), so you will not miss anything new",
740
+ linksToggleBtn: "Dev link (link:) details",
741
+ linksToggleSummary: "Dev links (link:): {n} total, {ready} self-sufficient",
742
+ linksToggleTitle: "Expand/collapse link details (collapsed by default)",
733
743
  devRootsTitle: "Dev roots (only directories inside these can be pinned)",
734
744
  devRootsNone: "(none registered)",
735
745
  devRootAddPlaceholder: "Register a dev root (absolute path, e.g. D:\\my-plugins)",
@@ -1568,6 +1578,22 @@ window.__ModuleLoader__.load({
1568
1578
  }
1569
1579
  // <<<update-pick:end>>>
1570
1580
  const el = react.createElement;
1581
+ // ★ 0.5.60:被跳过 bundle 的签名(包名+版本)。关掉提示时记下它;清单变了就重新出现。
1582
+ // 模块级 —— 上一版把辅助函数放子组件里、主面板误用,导致整块面板空白。
1583
+ function skippedSignature(skipped) {
1584
+ try {
1585
+ if (skipped === null || skipped === undefined || !Array.isArray(skipped.items)) return "0";
1586
+ return skipped.items.map((x) => String(x.packageName) + "@" + String(x.version ?? "?")).join("|");
1587
+ } catch { return "0"; }
1588
+ }
1589
+ // ★ 0.5.59:健康问题的**签名**(类别+条数)。关掉提示时记下它;签名一变说明问题变了,提示重新出现。
1590
+ // 刻意放在模块级(不放组件内)—— 上一版就是因为把辅助函数放在子组件里、主面板误用而整块空白。
1591
+ function healthSignature(health) {
1592
+ try {
1593
+ if (health === null || health === undefined || !Array.isArray(health.items)) return "0";
1594
+ return health.items.map((x) => String(x.kind) + ":" + String(x.count)).join("|");
1595
+ } catch { return "0"; }
1596
+ }
1571
1597
  function moduleShortName(moduleName) {
1572
1598
  return (moduleName.startsWith("@") ? moduleName.slice(moduleName.indexOf("/") + 1) : moduleName)
1573
1599
  .replace(/^cordis:/, "").replace(/^cordis-plugin-/, "").replace(/^dsh-(?:host-|client-)?/, "");
@@ -1914,6 +1940,17 @@ window.__ModuleLoader__.load({
1914
1940
  }
1915
1941
  function PluginConsoleTab({ t, section = null }) {
1916
1942
  const [state, setState] = react.useState({ status: "loading" });
1943
+ // ★ 0.5.59:健康摘要**可关掉**(用户 2026-10-17「这个提示可以关掉」)。
1944
+ // 关掉后不再打扰,但记的是**问题签名**(类别+条数):问题一变就重新出现 ——
1945
+ // 不能因为"关过一次"就让人永久错过新出现的问题。
1946
+ const [healthClosed, setHealthClosed] = react.useState(() => {
1947
+ try { return localStorage.getItem("pc-health-closed") ?? ""; } catch { return ""; }
1948
+ });
1949
+ // ★ 0.5.60:被跳过 bundle 的提示也可以关掉(用户 2026-10-18「不是说这个可以关闭吗」)。
1950
+ // 同样记"签名"(包名+版本),清单变了会重新出现 —— 关掉不等于永远看不见。
1951
+ const [skippedClosed, setSkippedClosed] = react.useState(() => {
1952
+ try { return localStorage.getItem("pc-skipped-closed") ?? ""; } catch { return ""; }
1953
+ });
1917
1954
  const [busy, setBusy] = react.useState(null);
1918
1955
  const [adaptUnlocking, setAdaptUnlocking] = react.useState(null);
1919
1956
  // 待适配行「强行启用」的风险确认态:{ entryId, moduleName, frameworkVersion, checkNote } | null
@@ -4622,9 +4659,21 @@ window.__ModuleLoader__.load({
4622
4659
  // 分项计数摆在最上面(不新增判据、不新增 IO:全部来自 /state 已有的块)。
4623
4660
  state.status === "ready" && state.data.health && state.data.health.issues > 0
4624
4661
  && Array.isArray(state.data.health.items)
4662
+ && healthSignature(state.data.health) !== healthClosed
4625
4663
  ? el("p", { className: styles.message, "data-error": "true", style: { marginTop: "8px", marginBottom: "0" } },
4626
- fill(t("healthSummary"), [["n", String(state.data.health.issues)]]) + ":"
4627
- + state.data.health.items.map((x) => x.label + " " + String(x.count)).join(" · "))
4664
+ t("healthSummary").replace("{n}", String(state.data.health.issues)) + ":"
4665
+ + state.data.health.items.map((x) => x.label + " " + String(x.count)).join(" · "),
4666
+ " ",
4667
+ el("button", {
4668
+ type: "button",
4669
+ style: { marginLeft: "8px", cursor: "pointer" },
4670
+ title: t("healthDismissTitle"),
4671
+ onClick: () => {
4672
+ const sig = healthSignature(state.data.health);
4673
+ try { localStorage.setItem("pc-health-closed", sig); } catch {}
4674
+ setHealthClosed(sig);
4675
+ },
4676
+ }, t("healthDismissBtn")))
4628
4677
  : null,
4629
4678
  // ★ 0.5.53 加法(真机 2026-10-15):**被框架整层跳过的 bundle**。
4630
4679
  // 为什么必须放在这里(不是列表里):被跳过的 bundle **不产生任何 loader 行**,
@@ -4634,9 +4683,21 @@ window.__ModuleLoader__.load({
4634
4683
  && state.data.skippedBundles.reliable === true
4635
4684
  && Array.isArray(state.data.skippedBundles.items)
4636
4685
  && state.data.skippedBundles.items.length > 0
4686
+ && skippedSignature(state.data.skippedBundles) !== skippedClosed
4637
4687
  ? el("div", { className: styles.message, "data-error": "true", style: { marginTop: "8px" } },
4638
4688
  el("p", null,
4639
- t("skippedBundlesTitle").replace("{n}", String(state.data.skippedBundles.items.length))),
4689
+ t("skippedBundlesTitle").replace("{n}", String(state.data.skippedBundles.items.length)),
4690
+ " ",
4691
+ el("button", {
4692
+ type: "button",
4693
+ style: { marginLeft: "8px", cursor: "pointer" },
4694
+ title: t("healthDismissTitle"),
4695
+ onClick: () => {
4696
+ const sig = skippedSignature(state.data.skippedBundles);
4697
+ try { localStorage.setItem("pc-skipped-closed", sig); } catch {}
4698
+ setSkippedClosed(sig);
4699
+ },
4700
+ }, t("healthDismissBtn"))),
4640
4701
  ...state.data.skippedBundles.items.map((b) => el("p", { key: b.packageName, title: b.hint ?? undefined, style: { margin: "4px 0 0 12px" } },
4641
4702
  "• " + b.packageName + (b.version ? " v" + b.version : "") + " — " + b.reason,
4642
4703
  // ★ 0.5.56:把提示里的"三条出路"之一真正做成可点的动作。
@@ -6166,6 +6227,9 @@ window.__ModuleLoader__.load({
6166
6227
  // 过去 link 钉住的插件与普通插件长得一模一样 —— 看不出指向哪、目标还在不在、
6167
6228
  // 依赖/peer 齐不齐、垫片还在不在。这份清单让"看见"先成立。
6168
6229
  const [links, setLinks] = react.useState(null);
6230
+ // ★ 0.5.58:link 可视化默认**收起**(用户 2026-10-17 要求)——
6231
+ // 这块信息量大但不常用,展开着会把「升级安全」面板顶长,反而让人找不到东西。
6232
+ const [linksOpen, setLinksOpen] = react.useState(false);
6169
6233
  // ★ 0.5.57:开发区根 + 钉住输入(用户决定:允许注册自己的根;不允许任意目录;agent 钉住需勾选、默认关)
6170
6234
  const [devRoots, setDevRoots] = react.useState([]);
6171
6235
  const [rootInput, setRootInput] = react.useState("");
@@ -6315,10 +6379,22 @@ window.__ModuleLoader__.load({
6315
6379
  row("back", t("safetyRollbackTitle"), status !== null && status?.host?.hosted === true
6316
6380
  ? fill(t("safetyRollbackNone"), [["reason", status?.host?.notice ?? t("hostedNoChannel")]])
6317
6381
  : (rb.applicable === true ? fill(t("safetyRollbackOk"), [["from", rb.from], ["to", rb.to]]) : fill(t("safetyRollbackNone"), [["reason", rb.reason === null || rb.reason === undefined ? "-" : rb.reason]]))),
6382
+ // ★ 0.5.58:折叠头(默认收起)。摘要行始终可见,点开才展开两块明细。
6383
+ el("button", {
6384
+ type: "button",
6385
+ style: { display: "flex", alignItems: "center", gap: "6px", width: "100%", marginTop: "6px", paddingTop: "6px", borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.18))", background: "none", border: "none", borderTopWidth: "1px", borderTopStyle: "solid", borderTopColor: "var(--dsw-alias-border-secondary, rgba(127,127,127,.18))", color: "inherit", cursor: "pointer", fontSize: "12px", fontWeight: 500, textAlign: "left" },
6386
+ "aria-expanded": linksOpen ? "true" : "false",
6387
+ title: t("linksToggleTitle"),
6388
+ onClick: () => setLinksOpen((v) => !v),
6389
+ },
6390
+ el("span", { style: { transform: linksOpen ? "rotate(90deg)" : "none", transition: "transform .2s", display: "inline-block" } }, "▸"),
6391
+ el("span", null, links === null
6392
+ ? t("linksToggleBtn")
6393
+ : fill(t("linksToggleSummary"), [["n", String(links.count ?? 0)], ["ready", String(links.readyCount ?? 0)]]))),
6318
6394
  // ★ 0.5.54 加法:开发式链接(link)清单 —— 每行给出目标、版本、自足性与垫片。
6319
6395
  // 判据全部来自服务端 /links(纯只读);这里只负责如实展示,不推断、不美化。
6320
6396
  links !== null && Array.isArray(links.items) && links.items.length > 0
6321
- ? el("div", { style: { display: "flex", flexDirection: "column", gap: "2px", marginTop: "4px", paddingTop: "4px", borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.18))" } },
6397
+ ? el("div", { style: { display: linksOpen ? "flex" : "none", flexDirection: "column", gap: "2px", marginTop: "4px", paddingTop: "4px", borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.18))" } },
6322
6398
  el("div", { style: { fontSize: "12px", fontWeight: 500 } }, fill(t("linksTitle"), [["n", String(links.count ?? links.items.length)], ["ready", String(links.readyCount ?? 0)]])),
6323
6399
  ...links.items.map((x) => el("div", { key: x.packageName, title: x.note ?? undefined, style: { fontSize: "12px", marginLeft: "10px", opacity: .85, wordBreak: "break-all" } },
6324
6400
  (x.ready === true ? "✓ " : "• ") + x.packageName + (x.version ? " v" + x.version : "")
@@ -6335,7 +6411,7 @@ window.__ModuleLoader__.load({
6335
6411
  // ★ 0.5.57:开发区根与"按路径钉住" —— 把协作开发流真正接通。
6336
6412
  // 规矩全部由服务端执行(domain/dev-roots.js 是"不许任意目录"的唯一执行点),
6337
6413
  // 这里只负责:先规划给人看 → 再让人确认 → 才真写。
6338
- el("div", { style: { display: "flex", flexDirection: "column", gap: "4px", marginTop: "6px", paddingTop: "6px", borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.18))" } },
6414
+ el("div", { style: { display: linksOpen ? "flex" : "none", flexDirection: "column", gap: "4px", marginTop: "6px", paddingTop: "6px", borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.18))" } },
6339
6415
  el("div", { style: { fontSize: "12px", fontWeight: 500 } }, t("devRootsTitle")),
6340
6416
  el("div", { style: { fontSize: "12px", opacity: .8, marginLeft: "10px", wordBreak: "break-all" } }, devRoots.length === 0 ? t("devRootsNone") : devRoots.join(" · ")),
6341
6417
  el("div", { style: { display: "flex", gap: "6px", marginLeft: "10px", flexWrap: "wrap" } },
@@ -389,7 +389,13 @@ function summarizeReadme(text) {
389
389
  async function readPluginDetails(moduleName, baseUrl, profileDir) {
390
390
  if (typeof moduleName !== 'string' || moduleName.startsWith('cordis:')) return { meta: null, readme: null }
391
391
  try {
392
- const pkgPath = resolvePackageJson(moduleName, baseDirOf(baseUrl), profileDir ?? null)
392
+ // ★ 0.5.60(真机 2026-10-18):与 entryPkgMeta 同一口径 —— **profile 优先、安装锚点兜底**。
393
+ // 详情卡片过去读 app.asar 那份(0.2.1-rc.2),于是显示"已是最新"、更新按钮永远点不动,
394
+ // 而真正被加载的是 profile 里那份 0.2.0-rc.2(正是被 peer 否决的那份)。
395
+ const hasProfile = typeof profileDir === 'string' && profileDir !== ''
396
+ const pkgPath = hasProfile
397
+ ? resolvePackageJson(moduleName, profileDir, baseDirOf(baseUrl))
398
+ : resolvePackageJson(moduleName, baseDirOf(baseUrl), null)
393
399
  if (pkgPath === null) throw new Error('not found')
394
400
  const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
395
401
  const meta = {
@@ -180,7 +180,15 @@ function entryPkgMeta(moduleName, baseUrl, profileDir) {
180
180
  if (hit !== undefined && Date.now() - hit.at < PKG_META_TTL) return hit
181
181
  const meta = { at: Date.now(), installDate: null, version: null, repository: null }
182
182
  try {
183
- const pkgPath = resolvePackageJson(moduleName, baseDirOf(baseUrl), profileDir ?? null)
183
+ // ★ 0.5.60(真机 2026-10-18):**profile 优先、安装锚点兜底**。
184
+ // 过去是反的(asar 优先),导致"徽标显示 0.2.1-rc.2(最新)→ 检测更新无更新可点 →
185
+ // 但实际加载的是 profile 里 0.2.0-rc.2 → peer 永远不匹配"的死局。
186
+ // loader 是从 profile 上下文解析模块名的,所以要显示/要比对的必须是 profile 那份;
187
+ // 只有 profile 里没有该包时(纯内置插件)才回落到安装锚点。
188
+ const hasProfile = typeof profileDir === 'string' && profileDir !== ''
189
+ const pkgPath = hasProfile
190
+ ? resolvePackageJson(moduleName, profileDir, baseDirOf(baseUrl))
191
+ : resolvePackageJson(moduleName, baseDirOf(baseUrl), null)
184
192
  if (pkgPath === null) throw new Error('not found')
185
193
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
186
194
  // 安装日期:优先 curl 通道写入的 .dsh-installed-at 标记(真实安装时刻);
@@ -163,6 +163,41 @@ async function routeStateGet(req, res, rc) {
163
163
  const shape = detectHostShape()
164
164
  host = { hosted: shape.hosted === true, kind: shape.kind, notice: shape.hosted === true ? shape.notice : null }
165
165
  } catch {}
166
+ // ★ 0.5.60 加法(真机 2026-10-18):**为被 bundle 级跳过的插件合成虚拟行**。
167
+ // 为什么必须做:bundle 层被跳过 ⇒ 它要插的 loader 行**根本不存在** ⇒ 列表里没有位置可挂,
168
+ // 用户只能看到一条横幅,无法像别的插件那样"看到它、更新它、再启用它"。
169
+ // 合成后它出现在列表里,显示为「已停用 / 未加载」+ 否决原因;toggleable=false 表示
170
+ // **没有真实行可开关**(开关自动置灰),但「详情 → 检测更新」照常可用 —— 更新到兼容版本后
171
+ // 重启,bundle 才会真正装载并出现真实行,那时才谈得上"启用"。
172
+ const realModuleNames = new Set(entries.map((e) => e.moduleName))
173
+ const virtualEntries = skippedBundles.reliable === true && Array.isArray(skippedBundles.items)
174
+ ? skippedBundles.items
175
+ .filter((b) => b !== null && b !== undefined && typeof b.packageName === 'string' && !realModuleNames.has(b.packageName))
176
+ .map((b) => ({
177
+ entryId: `bundle-skipped:${b.packageName}`,
178
+ rowId: null,
179
+ moduleName: b.packageName,
180
+ enabled: false,
181
+ fiberPhase: null,
182
+ protected: false,
183
+ toggleable: false,
184
+ userDisabled: false,
185
+ userForced: false,
186
+ extra: false,
187
+ installDate: null,
188
+ version: b.version ?? null,
189
+ repository: null,
190
+ pendingCompat: false,
191
+ compatRescinded: false,
192
+ compatVerdict: null,
193
+ adoptable: null,
194
+ // 复用既有的 veto 通道:卡片会照常渲染原因 + 出路(tooltip),无需新 UI。
195
+ veto: { reason: b.reason ?? null, hint: b.hint ?? null, kind: b.kind ?? null, judged: b.judged ?? null },
196
+ virtual: true,
197
+ }))
198
+ : []
199
+ const allEntries = virtualEntries.length === 0 ? entries : [...entries, ...virtualEntries]
200
+
166
201
  // ★ 0.5.55 加法:把已经算好的各块**折成一行健康摘要**(不新增 IO、不新增判据)。
167
202
  // 面板 176 个按钮、信息散在六处,用户没有一个地方能回答"整体健康吗、我该先处理哪件"。
168
203
  let health = { issues: 0, items: [] }
@@ -174,7 +209,7 @@ async function routeStateGet(req, res, rc) {
174
209
  patchUncertain: patchHeal === null ? [] : (patchHeal.uncertain ?? []),
175
210
  })
176
211
  } catch {}
177
- sendJson(res, 200, { ok: true, entries, skippedBundles, host, health, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries, compatVerdicts), compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null, verdict: compatVerdicts.get(p.rowId) ?? null })), verdictRefresh: pendingState.refresh === null ? null : { version: pendingState.refresh.version ?? null, updated: pendingState.refresh.updated ?? [], unchanged: pendingState.refresh.unchanged ?? [], unresolved: pendingState.refresh.unresolved ?? [], wrote: pendingState.refresh.wrote === true, writeError: pendingState.refresh.writeError ?? null } }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
212
+ sendJson(res, 200, { ok: true, entries: allEntries, skippedBundles, host, health, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries, compatVerdicts), compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null, verdict: compatVerdicts.get(p.rowId) ?? null })), verdictRefresh: pendingState.refresh === null ? null : { version: pendingState.refresh.version ?? null, updated: pendingState.refresh.updated ?? [], unchanged: pendingState.refresh.unchanged ?? [], unresolved: pendingState.refresh.unresolved ?? [], wrote: pendingState.refresh.wrote === true, writeError: pendingState.refresh.writeError ?? null } }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
178
213
  return
179
214
  }
180
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.57",
3
+ "version": "0.5.60",
4
4
  "description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
5
5
  "repository": {
6
6
  "type": "git",