dshmarket 1.48.0 → 1.50.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/client/client.js +50 -57
- package/lib/catalog-local-match.js +34 -0
- package/lib/dsh-cli.js +29 -2
- package/lib/entry-identity.js +85 -0
- package/lib/hot.js +18 -7
- package/lib/index.js +4 -5
- package/lib/install.js +12 -0
- package/lib/profile.js +58 -7
- package/lib/restart.js +15 -1
- package/lib/routes.js +74 -26
- package/lib/settings.js +12 -0
- package/lib/sources.js +29 -0
- package/lib/themes.js +34 -2
- package/lib/types/catalog-local-match.d.ts +0 -11
- package/lib/types/dsh-cli.d.ts +17 -0
- package/lib/types/entry-identity.d.ts +68 -0
- package/lib/types/install.d.ts +12 -0
- package/lib/types/profile.d.ts +20 -0
- package/lib/types/restart.d.ts +4 -0
- package/lib/types/settings.d.ts +4 -0
- package/lib/types/sources.d.ts +17 -0
- package/lib/verify.js +2 -4
- package/package.json +1 -1
- package/src/catalog-local-match.ts +41 -0
- package/src/client/MarketSection.tsx +26 -4
- package/src/client/locales.ts +2 -2
- package/src/client/market-data.ts +2 -62
- package/src/dsh-cli.ts +29 -2
- package/src/entry-identity.ts +84 -0
- package/src/hot.ts +17 -9
- package/src/index.ts +4 -5
- package/src/install.ts +12 -0
- package/src/profile.ts +54 -6
- package/src/restart.ts +15 -1
- package/src/routes.ts +77 -27
- package/src/settings.ts +19 -6
- package/src/sources.ts +26 -0
- package/src/themes.ts +37 -2
- package/src/verify.ts +2 -3
package/client/client.js
CHANGED
|
@@ -105,7 +105,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
105
105
|
installing: "安装中…",
|
|
106
106
|
installedBadge: "✓ 已安装",
|
|
107
107
|
alreadyInstalled: "✓ 已安装",
|
|
108
|
-
restartBanner: "
|
|
108
|
+
restartBanner: "项变更需重启 DeepSeek Harness 后生效",
|
|
109
109
|
uninstall: "卸载",
|
|
110
110
|
confirmRemove: "确认卸载?",
|
|
111
111
|
uninstalling: "卸载中…",
|
|
@@ -638,7 +638,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
638
638
|
installing: "Installing…",
|
|
639
639
|
installedBadge: "✓ Installed",
|
|
640
640
|
alreadyInstalled: "✓ Installed",
|
|
641
|
-
restartBanner: "change(s)
|
|
641
|
+
restartBanner: "change(s) pending restart — restart DeepSeek Harness to apply",
|
|
642
642
|
uninstall: "Uninstall",
|
|
643
643
|
confirmRemove: "Uninstall?",
|
|
644
644
|
uninstalling: "Uninstalling…",
|
|
@@ -1150,7 +1150,44 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1150
1150
|
* rather than guessing; declared repo evidence that matches nothing in the
|
|
1151
1151
|
* catalog must not fall back to a coincidental unique name.
|
|
1152
1152
|
*/
|
|
1153
|
+
/**
|
|
1154
|
+
* Memo for findCatalogEntryForLocal, keyed on the catalog array identity and
|
|
1155
|
+
* then the full lookup input (#589).
|
|
1156
|
+
*
|
|
1157
|
+
* The market client calls this once per rendered card for every link:/file:
|
|
1158
|
+
* dependency, and each call filtered the whole catalog and walked every URL —
|
|
1159
|
+
* measured ~300ms per render at 24 cards against a 3,627-entry catalog, on
|
|
1160
|
+
* every keystroke of the search box. The result is a pure function of the
|
|
1161
|
+
* catalog, the name, the identities, and the hints, so caching by the array
|
|
1162
|
+
* identity never serves a stale answer: a refetched catalog is a new array,
|
|
1163
|
+
* and the old inner map becomes collectable. Null results are cached too —
|
|
1164
|
+
* they are the common answer for same-named forks without evidence.
|
|
1165
|
+
*
|
|
1166
|
+
* The input key is the JSON form of the whole tuple, not a delimiter join:
|
|
1167
|
+
* an empty array and an array holding one empty string serialize
|
|
1168
|
+
* differently, and the matcher treats them differently (`identitySet.size >
|
|
1169
|
+
* 0` chooses the evidence branch), so the key must not conflate them
|
|
1170
|
+
* (#485's lesson: weak signals must never override strong ones).
|
|
1171
|
+
*/
|
|
1172
|
+
const localMatchCache = /* @__PURE__ */ new WeakMap();
|
|
1153
1173
|
function findCatalogEntryForLocal(plugins, name, identities = [], hints = []) {
|
|
1174
|
+
const cacheKey = JSON.stringify([
|
|
1175
|
+
name,
|
|
1176
|
+
identities,
|
|
1177
|
+
hints
|
|
1178
|
+
]);
|
|
1179
|
+
let byCatalog = localMatchCache.get(plugins);
|
|
1180
|
+
if (byCatalog === void 0) {
|
|
1181
|
+
byCatalog = /* @__PURE__ */ new Map();
|
|
1182
|
+
localMatchCache.set(plugins, byCatalog);
|
|
1183
|
+
}
|
|
1184
|
+
const hit = byCatalog.get(cacheKey);
|
|
1185
|
+
if (hit !== void 0) return hit;
|
|
1186
|
+
const result = findCatalogEntryForLocalUncached(plugins, name, identities, hints);
|
|
1187
|
+
byCatalog.set(cacheKey, result);
|
|
1188
|
+
return result;
|
|
1189
|
+
}
|
|
1190
|
+
function findCatalogEntryForLocalUncached(plugins, name, identities, hints) {
|
|
1154
1191
|
const nameKey = name.toLowerCase();
|
|
1155
1192
|
const byName = plugins.filter((plugin) => plugin.name.toLowerCase() === nameKey || typeof plugin.npm === "string" && plugin.npm.toLowerCase() === nameKey);
|
|
1156
1193
|
const identitySet = new Set(identities.map((value) => value.toLowerCase()));
|
|
@@ -1655,56 +1692,6 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1655
1692
|
for (const id of entryIdentities(plugin)) if (dep.has(id)) return true;
|
|
1656
1693
|
return false;
|
|
1657
1694
|
}
|
|
1658
|
-
/**
|
|
1659
|
-
* The same memo, for the branch #262 left behind (#589).
|
|
1660
|
-
*
|
|
1661
|
-
* `looseMatchCount` above covers dependencies installed by version. A
|
|
1662
|
-
* `link:` or `file:` dependency takes the other branch, into
|
|
1663
|
-
* `findCatalogEntryForLocal`, which walks the whole catalog at least twice
|
|
1664
|
-
* per call — once to filter by name, once to collect `/tree/` repos — and
|
|
1665
|
-
* up to twice more when there are identities to probe. Both callers below
|
|
1666
|
-
* run once per rendered card. The reporter profiled ~300ms per repaint at
|
|
1667
|
-
* 24 cards against a 3,627-entry catalog where a version-pinned dependency
|
|
1668
|
-
* paid 1.1ms; a local benchmark measured ~38ms per render at that shape,
|
|
1669
|
-
* and ~1.4s at 96 cards with eight local dependencies.
|
|
1670
|
-
*
|
|
1671
|
-
* The inner key carries the EVIDENCE, not just the name. Installing a plugin
|
|
1672
|
-
* hands the next render a fresh identities array while the catalog array
|
|
1673
|
-
* stays the same, so a name-only key would answer the post-install question
|
|
1674
|
-
* with the pre-install result — which is the same-named-fork confusion #485
|
|
1675
|
-
* asked this matcher to stop making, reintroduced as a cache bug.
|
|
1676
|
-
*
|
|
1677
|
-
* A miss is cached as `null`, which is why the "not cached yet" sentinel
|
|
1678
|
-
* has to be `undefined`: `null` is a real answer here, and it costs the
|
|
1679
|
-
* same full scan to establish as a hit does. It is also the common case —
|
|
1680
|
-
* a checkout you are developing is usually not in the catalog at all.
|
|
1681
|
-
*
|
|
1682
|
-
* The invariant this rests on, stated because the WeakMap cannot enforce it:
|
|
1683
|
-
* the catalog array and the entries inside it are frozen once handed here. A
|
|
1684
|
-
* refetch replaces the array — which is what the outer key is for — but an
|
|
1685
|
-
* in-place `push`, `sort` or `reverse`, or editing a row's `url`, would keep
|
|
1686
|
-
* the key and change the answer. Order is load-bearing too: the matcher
|
|
1687
|
-
* returns the FIRST row that fits. Nothing in the client does any of this
|
|
1688
|
-
* today; `visiblePlugins` and `themePlugins` both sort copies.
|
|
1689
|
-
*/
|
|
1690
|
-
const localMatchCache = /* @__PURE__ */ new WeakMap();
|
|
1691
|
-
function cachedEntryForLocal(plugins, name, identities, hints) {
|
|
1692
|
-
let byKey = localMatchCache.get(plugins);
|
|
1693
|
-
if (byKey === void 0) {
|
|
1694
|
-
byKey = /* @__PURE__ */ new Map();
|
|
1695
|
-
localMatchCache.set(plugins, byKey);
|
|
1696
|
-
}
|
|
1697
|
-
const key = JSON.stringify([
|
|
1698
|
-
name,
|
|
1699
|
-
identities,
|
|
1700
|
-
hints
|
|
1701
|
-
]);
|
|
1702
|
-
const hit = byKey.get(key);
|
|
1703
|
-
if (hit !== void 0) return hit;
|
|
1704
|
-
const entry = findCatalogEntryForLocal(plugins, name, identities, hints);
|
|
1705
|
-
byKey.set(key, entry);
|
|
1706
|
-
return entry;
|
|
1707
|
-
}
|
|
1708
1695
|
/** The installed dependency name a registry entry corresponds to, or null. */
|
|
1709
1696
|
function matchInstalledName(plugin, installed, repoIdentities = {}, plugins, repoHints = {}) {
|
|
1710
1697
|
const ids = entryIdentities(plugin);
|
|
@@ -1713,7 +1700,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1713
1700
|
const repos = repoIdentities[name] ?? [];
|
|
1714
1701
|
if (/^(?:link|file):/i.test(specStr)) {
|
|
1715
1702
|
if (plugins === void 0) continue;
|
|
1716
|
-
const entry =
|
|
1703
|
+
const entry = findCatalogEntryForLocal(plugins, name, repos, repoHints[name] ?? []);
|
|
1717
1704
|
if (entry !== null && entry.url === plugin.url) return name;
|
|
1718
1705
|
continue;
|
|
1719
1706
|
}
|
|
@@ -2115,7 +2102,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
2115
2102
|
}
|
|
2116
2103
|
/** Catalog row for an installed dependency — strict for local link:/file: specs. */
|
|
2117
2104
|
function catalogEntryForInstalled(plugins, name, spec, repoIdentities = [], repoHints = []) {
|
|
2118
|
-
if (/^(?:link|file):/i.test(spec)) return
|
|
2105
|
+
if (/^(?:link|file):/i.test(spec)) return findCatalogEntryForLocal(plugins, name, repoIdentities, repoHints) ?? void 0;
|
|
2119
2106
|
return entryForDep(plugins, name, spec, repoIdentities, repoHints);
|
|
2120
2107
|
}
|
|
2121
2108
|
//#endregion
|
|
@@ -6348,6 +6335,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
6348
6335
|
const [buildsSkipped, setBuildsSkipped] = (0, react.useState)(null);
|
|
6349
6336
|
const [updatingAll, setUpdatingAll] = (0, react.useState)(false);
|
|
6350
6337
|
const [updatedNames, setUpdatedNames] = (0, react.useState)([]);
|
|
6338
|
+
const [restartNames, setRestartNames] = (0, react.useState)([]);
|
|
6351
6339
|
const [hotUrls, setHotUrls] = (0, react.useState)([]);
|
|
6352
6340
|
const [hotNames, setHotNames] = (0, react.useState)([]);
|
|
6353
6341
|
const [progressLine, setProgressLine] = (0, react.useState)(null);
|
|
@@ -6667,12 +6655,13 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
6667
6655
|
}
|
|
6668
6656
|
if (Array.isArray(saved.doneUrls) && saved.doneUrls.length > 0) setDoneUrls(saved.doneUrls);
|
|
6669
6657
|
if (Array.isArray(saved.updated) && saved.updated.length > 0) setUpdatedNames(saved.updated);
|
|
6658
|
+
if (Array.isArray(saved.restartNames) && saved.restartNames.length > 0) setRestartNames(saved.restartNames);
|
|
6670
6659
|
if (typeof saved.removed === "number" && saved.removed > 0) setRemovedCount(saved.removed);
|
|
6671
6660
|
if (typeof saved.toggled === "number" && saved.toggled > 0) setToggleRestart(saved.toggled);
|
|
6672
6661
|
}, [bootId]);
|
|
6673
6662
|
(0, react.useEffect)(() => {
|
|
6674
6663
|
if (bootId === null) return;
|
|
6675
|
-
if (doneUrls.length === 0 && updatedNames.length === 0 && removedCount === 0 && toggleRestart === 0) {
|
|
6664
|
+
if (doneUrls.length === 0 && updatedNames.length === 0 && restartNames.length === 0 && removedCount === 0 && toggleRestart === 0) {
|
|
6676
6665
|
sessionStorage.removeItem("dshm-restart");
|
|
6677
6666
|
return;
|
|
6678
6667
|
}
|
|
@@ -6680,6 +6669,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
6680
6669
|
boot: bootId,
|
|
6681
6670
|
doneUrls,
|
|
6682
6671
|
updated: updatedNames,
|
|
6672
|
+
restartNames,
|
|
6683
6673
|
removed: removedCount,
|
|
6684
6674
|
toggled: toggleRestart
|
|
6685
6675
|
}));
|
|
@@ -6687,6 +6677,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
6687
6677
|
bootId,
|
|
6688
6678
|
doneUrls,
|
|
6689
6679
|
updatedNames,
|
|
6680
|
+
restartNames,
|
|
6690
6681
|
removedCount,
|
|
6691
6682
|
toggleRestart
|
|
6692
6683
|
]);
|
|
@@ -7307,6 +7298,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7307
7298
|
if (status === 200 && body.ok) {
|
|
7308
7299
|
setRecords((list) => patch(list, updateRecordId, { state: "done" }));
|
|
7309
7300
|
setUpdatedNames((names) => names.concat(name));
|
|
7301
|
+
const activation = body.activation && typeof body.activation === "object" ? body.activation[name] : void 0;
|
|
7302
|
+
if (!activation || activation.state === "restart") setRestartNames((names) => names.includes(name) ? names : names.concat(name));
|
|
7310
7303
|
if (body.activation && typeof body.activation === "object") setActivations((prev) => ({
|
|
7311
7304
|
...prev,
|
|
7312
7305
|
...body.activation
|
|
@@ -7854,7 +7847,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7854
7847
|
});
|
|
7855
7848
|
}, [doGroupAction, groups]);
|
|
7856
7849
|
const selfName = installed["dshmarket"] !== void 0 ? "dshmarket" : "dsh-market";
|
|
7857
|
-
const updatableNames = Object.keys(installed).filter((name) => name !== selfName && !updatedNames.includes(name) && updates[name] && updates[name].updateAvailable);
|
|
7850
|
+
const updatableNames = Object.keys(installed).filter((name) => name !== selfName && !updatedNames.includes(name) && !effectiveDisabledSet.has(name) && updates[name] && updates[name].updateAvailable);
|
|
7858
7851
|
const batchUpdatableNames = updatableNames.filter((name) => updates[name]?.restoreRequired !== true);
|
|
7859
7852
|
const ignoredUpdateSet = (0, react.useMemo)(() => new Set(ignoredUpdateNames), [ignoredUpdateNames]);
|
|
7860
7853
|
const reminderUpdatableNames = updatableNames.filter((name) => !ignoredUpdateSet.has(name));
|
|
@@ -8102,7 +8095,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
8102
8095
|
webdavUrl,
|
|
8103
8096
|
webdavUser
|
|
8104
8097
|
]);
|
|
8105
|
-
const sessionPendingRestart = doneUrls.length +
|
|
8098
|
+
const sessionPendingRestart = doneUrls.length + restartNames.length + removedCount + toggleRestart + (backupRestored ? 1 : 0);
|
|
8106
8099
|
/**
|
|
8107
8100
|
* Plugins the HOST reports as restart-pending, independent of what this
|
|
8108
8101
|
* browser session happens to remember. Installing and then reloading the
|
|
@@ -41,7 +41,41 @@ function catalogEntryMatchesHints(url, hintSet) {
|
|
|
41
41
|
* rather than guessing; declared repo evidence that matches nothing in the
|
|
42
42
|
* catalog must not fall back to a coincidental unique name.
|
|
43
43
|
*/
|
|
44
|
+
/**
|
|
45
|
+
* Memo for findCatalogEntryForLocal, keyed on the catalog array identity and
|
|
46
|
+
* then the full lookup input (#589).
|
|
47
|
+
*
|
|
48
|
+
* The market client calls this once per rendered card for every link:/file:
|
|
49
|
+
* dependency, and each call filtered the whole catalog and walked every URL —
|
|
50
|
+
* measured ~300ms per render at 24 cards against a 3,627-entry catalog, on
|
|
51
|
+
* every keystroke of the search box. The result is a pure function of the
|
|
52
|
+
* catalog, the name, the identities, and the hints, so caching by the array
|
|
53
|
+
* identity never serves a stale answer: a refetched catalog is a new array,
|
|
54
|
+
* and the old inner map becomes collectable. Null results are cached too —
|
|
55
|
+
* they are the common answer for same-named forks without evidence.
|
|
56
|
+
*
|
|
57
|
+
* The input key is the JSON form of the whole tuple, not a delimiter join:
|
|
58
|
+
* an empty array and an array holding one empty string serialize
|
|
59
|
+
* differently, and the matcher treats them differently (`identitySet.size >
|
|
60
|
+
* 0` chooses the evidence branch), so the key must not conflate them
|
|
61
|
+
* (#485's lesson: weak signals must never override strong ones).
|
|
62
|
+
*/
|
|
63
|
+
const localMatchCache = new WeakMap();
|
|
44
64
|
export function findCatalogEntryForLocal(plugins, name, identities = [], hints = []) {
|
|
65
|
+
const cacheKey = JSON.stringify([name, identities, hints]);
|
|
66
|
+
let byCatalog = localMatchCache.get(plugins);
|
|
67
|
+
if (byCatalog === undefined) {
|
|
68
|
+
byCatalog = new Map();
|
|
69
|
+
localMatchCache.set(plugins, byCatalog);
|
|
70
|
+
}
|
|
71
|
+
const hit = byCatalog.get(cacheKey);
|
|
72
|
+
if (hit !== undefined)
|
|
73
|
+
return hit;
|
|
74
|
+
const result = findCatalogEntryForLocalUncached(plugins, name, identities, hints);
|
|
75
|
+
byCatalog.set(cacheKey, result);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function findCatalogEntryForLocalUncached(plugins, name, identities, hints) {
|
|
45
79
|
const nameKey = name.toLowerCase();
|
|
46
80
|
const byName = plugins.filter(plugin => plugin.name.toLowerCase() === nameKey
|
|
47
81
|
|| (typeof plugin.npm === 'string' && plugin.npm.toLowerCase() === nameKey));
|
package/lib/dsh-cli.js
CHANGED
|
@@ -230,6 +230,33 @@ export function gitEnvForPnpm(env = process.env) {
|
|
|
230
230
|
return {};
|
|
231
231
|
return { GIT_TERMINAL_PROMPT: '0' };
|
|
232
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Every `--config.<key>=<value>` override in the argv, repeated as the
|
|
235
|
+
* `PNPM_CONFIG_<KEY>` environment variable that pnpm 12 still reads.
|
|
236
|
+
*
|
|
237
|
+
* pnpm 12 ignores some `--config.<key>` overrides on the command line, in
|
|
238
|
+
* either spelling, without a word: `fetchTimeout` (#615; measured on
|
|
239
|
+
* 12.2.1, 12.3.0 and 12.4.1) and `auto-install-peers` (12.4.1 auto-installs
|
|
240
|
+
* the peer with the flag present and not with the variable), so the retries
|
|
241
|
+
* that carry them ran exactly like the first attempt. `PNPM_CONFIG_*` is
|
|
242
|
+
* honoured by 11.8, 11.21 and 12.4 alike, so the argument stays for the
|
|
243
|
+
* versions that read it and the variable carries the same value for the
|
|
244
|
+
* ones that do not. Scoped to the run that carries the flag; nothing is set
|
|
245
|
+
* otherwise, so a user's own values are untouched on every other run. Keys
|
|
246
|
+
* are accepted in either spelling, so respelling a constant (as #600 did
|
|
247
|
+
* for the release-age one) cannot silently drop the variable.
|
|
248
|
+
*/
|
|
249
|
+
export function pnpmConfigEnvForArgs(pluginArgs) {
|
|
250
|
+
const env = {};
|
|
251
|
+
for (const arg of pluginArgs) {
|
|
252
|
+
const match = /^--config\.([A-Za-z][A-Za-z0-9-]*)=(\S+)$/.exec(arg);
|
|
253
|
+
if (match === null)
|
|
254
|
+
continue;
|
|
255
|
+
const key = match[1].replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toUpperCase();
|
|
256
|
+
env[`PNPM_CONFIG_${key}`] = match[2];
|
|
257
|
+
}
|
|
258
|
+
return env;
|
|
259
|
+
}
|
|
233
260
|
function spawnEnv() {
|
|
234
261
|
// pnpm v10+ blocks forever on a silent interactive prompt without a TTY;
|
|
235
262
|
// CI mode forces it to act or fail instead of asking.
|
|
@@ -665,7 +692,7 @@ export const BOOT_ID = `${String(process.pid)}-${String(Date.now())}`;
|
|
|
665
692
|
*/
|
|
666
693
|
export const TARGET_RE = /^[A-Za-z0-9@:./_#+~^=-]+$/;
|
|
667
694
|
/** Mutating pnpm commands get the structured reporter appended. */
|
|
668
|
-
const NDJSON_COMMANDS = new Set(['add', 'remove', 'install']);
|
|
695
|
+
const NDJSON_COMMANDS = new Set(['add', 'remove', 'install', 'update']);
|
|
669
696
|
/** Apply profile-specific pnpm compatibility and the structured reporter. */
|
|
670
697
|
function preparePluginArgs(profileDirectory, pluginArgs) {
|
|
671
698
|
let args = pluginArgsFor(profileDirectory, [...pluginArgs]);
|
|
@@ -739,7 +766,7 @@ export function runDshPlugin(profile, pluginArgs) {
|
|
|
739
766
|
// pnpm v10 blocks forever on a silent interactive prompt without a TTY
|
|
740
767
|
// (observed on re-add over a pinned git spec); CI mode forces it to act
|
|
741
768
|
// or fail instead of asking.
|
|
742
|
-
env: spawnEnv(),
|
|
769
|
+
env: { ...spawnEnv(), ...pnpmConfigEnvForArgs(pluginArgs) },
|
|
743
770
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
744
771
|
viaShell,
|
|
745
772
|
// Own process group on POSIX so cancel/timeout can kill the whole
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which loader entry belongs to which installed package (#646).
|
|
3
|
+
*
|
|
4
|
+
* The ecosystem does not require a bundle patch's row `name:` to be the
|
|
5
|
+
* package name. Two shapes are ordinary:
|
|
6
|
+
*
|
|
7
|
+
* - id: aegis-method-pack
|
|
8
|
+
* name: aegis/extensions/dsh/index.js ← a SUBPATH inside the package
|
|
9
|
+
* - id: filesystem
|
|
10
|
+
* name: @deepseek-ai/dsh-skill-filesystem ← another package entirely
|
|
11
|
+
*
|
|
12
|
+
* `dsh-chat-import`, `meow-memory` and `dsh-context` all name themselves, so
|
|
13
|
+
* the simple `name === packageName` comparison worked for years. `aegis`
|
|
14
|
+
* names an entry inside itself, and every site that compared the two
|
|
15
|
+
* directly concluded the plugin had no entry — the market reported "no
|
|
16
|
+
* bundle patch … nothing to hot-mount" on enable (#646) and could not see
|
|
17
|
+
* the entry was already live.
|
|
18
|
+
*
|
|
19
|
+
* Three such sites had grown independently (`src/themes.ts`, `src/verify.ts`,
|
|
20
|
+
* `src/routes.ts`), each restating the rule from memory. That is the shape
|
|
21
|
+
* the identity bugs #485→#605 came from: one assumption, several copies,
|
|
22
|
+
* each fixed at a different time. The rule lives here now, once.
|
|
23
|
+
*
|
|
24
|
+
* The carrier case (#156) is NOT decided here. A bundle that mounts someone
|
|
25
|
+
* else's package has no name relation at all, and is recognised by the entry
|
|
26
|
+
* ID its own patch inserts — see `bundlePatchInsertedIds`. The two rules
|
|
27
|
+
* compose; neither replaces the other.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Whether a loader entry name refers to `packageName`.
|
|
31
|
+
*
|
|
32
|
+
* The `/` bound is load-bearing: without it `toolshrink` would match an
|
|
33
|
+
* entry named `toolshrink-extra`, a different installed package.
|
|
34
|
+
*
|
|
35
|
+
* @param name - an entry's `options.name`, in any shape.
|
|
36
|
+
* @param packageName - the installed package name to match against.
|
|
37
|
+
* @returns true for the bare name and for a subpath entry inside it.
|
|
38
|
+
*/
|
|
39
|
+
export function nameMatchesPackage(name, packageName) {
|
|
40
|
+
if (typeof name !== 'string' || name === '')
|
|
41
|
+
return false;
|
|
42
|
+
return name === packageName || name.startsWith(`${packageName}/`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The same rule for a loader entry object.
|
|
46
|
+
*
|
|
47
|
+
* @param entry - anything with `options.name`, as the host's loader stores it.
|
|
48
|
+
* @param packageName - the installed package name to match against.
|
|
49
|
+
* @returns whether this entry is the one `packageName` refers to.
|
|
50
|
+
*/
|
|
51
|
+
export function entryMatchesPackage(entry, packageName) {
|
|
52
|
+
return nameMatchesPackage(entry.options?.name, packageName);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The package a subpath entry name belongs to — `aegis/x/y.js` → `aegis`.
|
|
56
|
+
*
|
|
57
|
+
* The inverse of {@link nameMatchesPackage}, for building a set of live
|
|
58
|
+
* packages from a set of live entry names: an entry that is up proves its
|
|
59
|
+
* package is up, and its name alone does not say so.
|
|
60
|
+
*
|
|
61
|
+
* Names that are not npm package names return null rather than a guess: a
|
|
62
|
+
* relative path, an absolute path, a URL or a cordis builtin would otherwise
|
|
63
|
+
* "belong to" a package that does not exist, which is worse than no answer.
|
|
64
|
+
* A scoped name needs three segments (`@scope/pkg/sub`) to have a subpath at
|
|
65
|
+
* all, and a lone `@scope/pkg` has no package to return.
|
|
66
|
+
*
|
|
67
|
+
* @param name - an entry's `options.name`.
|
|
68
|
+
* @returns the package name, or null when the name cannot imply one.
|
|
69
|
+
*/
|
|
70
|
+
export function packageOfEntryName(name) {
|
|
71
|
+
if (typeof name !== 'string' || name === '')
|
|
72
|
+
return null;
|
|
73
|
+
if (name.startsWith('.') || name.startsWith('/') || name.startsWith('cordis:'))
|
|
74
|
+
return null;
|
|
75
|
+
if (name.includes('://'))
|
|
76
|
+
return null;
|
|
77
|
+
if (!name.includes('/'))
|
|
78
|
+
return null;
|
|
79
|
+
const parts = name.split('/');
|
|
80
|
+
if (parts[0] === '')
|
|
81
|
+
return null;
|
|
82
|
+
if (parts[0].startsWith('@'))
|
|
83
|
+
return parts.length >= 3 ? `${parts[0]}/${parts[1]}` : null;
|
|
84
|
+
return parts[0];
|
|
85
|
+
}
|
package/lib/hot.js
CHANGED
|
@@ -30,7 +30,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
30
30
|
import { asChannel } from './channels.js';
|
|
31
31
|
import { asRegion, normalizeGithubProxy } from './regions.js';
|
|
32
32
|
import { logEvent } from './log.js';
|
|
33
|
-
import { entryArtifactExists } from './profile.js';
|
|
33
|
+
import { declaredBundlePatchFile, entryArtifactExists } from './profile.js';
|
|
34
34
|
/**
|
|
35
35
|
* Profile-scoped resolution for hot-mount rows: turn a bare package name into
|
|
36
36
|
* the absolute `file://` entry URL of the package just installed into
|
|
@@ -409,12 +409,23 @@ export async function hotMount(ctx, profileDir, packageName) {
|
|
|
409
409
|
reason: '宿主不支持热挂载(include 插件不可导入),需重启 / the host cannot hot-mount (include plugin unavailable); restart required',
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
412
|
+
// Where the patch lives is the PACKAGE's decision, declared as
|
|
413
|
+
// `dsh.bundle.patch` — `aegis` keeps it in `./extensions/dsh/`. Reading
|
|
414
|
+
// only the package root made a plugin whose patch is declared elsewhere
|
|
415
|
+
// look like one with no bundle at all, and the user was told "no bundle
|
|
416
|
+
// patch … nothing to hot-mount" for a package that plainly has one
|
|
417
|
+
// (#646). The root file stays as the fallback: it is the long-standing
|
|
418
|
+
// convention, and `profile.ts` resolves the declared field for everything
|
|
419
|
+
// else, so the two now agree on where a patch is.
|
|
420
|
+
const packageRoot = join(profileDir, 'node_modules', packageName);
|
|
421
|
+
const declared = declaredBundlePatchFile(packageRoot);
|
|
422
|
+
let patchText = null;
|
|
423
|
+
for (const file of declared !== null ? [declared] : [join(packageRoot, 'cordis.patch.yml')]) {
|
|
424
|
+
try {
|
|
425
|
+
patchText = readFileSync(file, 'utf8');
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
catch { /* try the next location */ }
|
|
418
429
|
}
|
|
419
430
|
let rows;
|
|
420
431
|
if (patchText !== null) {
|
package/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createDesktopPluginRuntime } from './dsh-cli.js';
|
|
6
6
|
import { mountMarketRoutes } from './routes.js';
|
|
7
|
-
import { installMarketSettings } from './settings.js';
|
|
7
|
+
import { installDesktopMarketSettings, installMarketSettings } from './settings.js';
|
|
8
8
|
export const name = 'dsh-market';
|
|
9
9
|
/**
|
|
10
10
|
* Register the market against the host context.
|
|
@@ -46,10 +46,8 @@ export function apply(ctx, config) {
|
|
|
46
46
|
allowRestart: config?.allowRestart,
|
|
47
47
|
maxSnapshots: config?.maxSnapshots,
|
|
48
48
|
};
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// lifecycle and the value is forced false, so it is not the user's to
|
|
52
|
-
// choose. No-ops on a host without a settings service.
|
|
49
|
+
// Web settings may control restart; Desktop only registers the card's
|
|
50
|
+
// namespace below. Both no-op on a host without a settings service.
|
|
53
51
|
installMarketSettings(ctx, resolved);
|
|
54
52
|
host.effect(() => mountMarketRoutes(host, resolved, undefined, agentsLookupOf(ctx)), 'dsh-market: http routes');
|
|
55
53
|
return;
|
|
@@ -72,6 +70,7 @@ export function apply(ctx, config) {
|
|
|
72
70
|
maxSnapshots: config?.maxSnapshots,
|
|
73
71
|
};
|
|
74
72
|
const desktopHost = desktopCtx;
|
|
73
|
+
installDesktopMarketSettings(desktopCtx);
|
|
75
74
|
desktopHost.effect(() => {
|
|
76
75
|
const disposeRoutes = mountMarketRoutes(host, resolved, runtime, agentsLookupOf(ctx));
|
|
77
76
|
return async () => {
|
package/lib/install.js
CHANGED
|
@@ -29,6 +29,14 @@ export const RELEASE_AGE_OVERRIDE = '--config.minimum-release-age=0';
|
|
|
29
29
|
* WHOLE repo even for a `#path:` subdirectory plugin) on slow networks; a
|
|
30
30
|
* plain retry fails again at the same limit, so the recovery re-runs with
|
|
31
31
|
* this override once. Scoped to a single command like RELEASE_AGE_OVERRIDE.
|
|
32
|
+
*
|
|
33
|
+
* pnpm 12 ignores this flag on the command line in either spelling (#615).
|
|
34
|
+
* On the CLI runner, runDshPlugin repeats every `--config.<key>` override
|
|
35
|
+
* as PNPM_CONFIG_<KEY>, which pnpm 11 and 12 both read, so the retried
|
|
36
|
+
* command really does get the longer limit there. The Desktop runtime hands
|
|
37
|
+
* its host argv only, so on a Desktop host the retry still depends on that
|
|
38
|
+
* host's pnpm reading the flag. The flag stays: pnpm 11 and earlier read
|
|
39
|
+
* it, and it costs nothing on the versions that do not.
|
|
32
40
|
*/
|
|
33
41
|
export const FETCH_TIMEOUT_OVERRIDE = '--config.fetchTimeout=600000';
|
|
34
42
|
/**
|
|
@@ -47,6 +55,10 @@ export const FETCH_TIMEOUT_OVERRIDE = '--config.fetchTimeout=600000';
|
|
|
47
55
|
* Verified against pnpm 10.29.3: `peerDependencyRules.ignoreMissing` does
|
|
48
56
|
* NOT prevent the fetch (it only silences the warning), so this flag is the
|
|
49
57
|
* only lever that actually works.
|
|
58
|
+
*
|
|
59
|
+
* pnpm 12 ignores this flag on the command line too (12.4.1 auto-installs
|
|
60
|
+
* the peer regardless); runDshPlugin repeats it as
|
|
61
|
+
* PNPM_CONFIG_AUTO_INSTALL_PEERS, which 12 reads (#615).
|
|
50
62
|
*/
|
|
51
63
|
export const AUTO_INSTALL_PEERS_OFF = '--config.auto-install-peers=false';
|
|
52
64
|
/**
|
package/lib/profile.js
CHANGED
|
@@ -478,11 +478,18 @@ export function readLockCommits(profile, explicitDir) {
|
|
|
478
478
|
* Commit recorded for a non-codeload git resolution (`type: git` in pnpm's
|
|
479
479
|
* lockfile). Matched against the install spec so a Gitea/GitLab URL can
|
|
480
480
|
* compare HEAD without mistaking a same-named npm package (#525).
|
|
481
|
+
*
|
|
482
|
+
* Two packages of one monorepo resolve from the SAME remote and differ only
|
|
483
|
+
* by pnpm's `path:` selector, so the spec's subpath has to match too — and
|
|
484
|
+
* when the spec names no subpath while several entries of that remote do,
|
|
485
|
+
* there is no answer rather than the first sibling's commit (#632).
|
|
481
486
|
*/
|
|
482
487
|
export function readGitResolutionCommit(profile, spec, explicitDir) {
|
|
483
488
|
const want = normalizeGitRepoKey(spec);
|
|
484
489
|
if (want === null)
|
|
485
490
|
return null;
|
|
491
|
+
const wantPath = gitSubpathSelector(spec);
|
|
492
|
+
const matches = [];
|
|
486
493
|
try {
|
|
487
494
|
const lock = readFileSync(join(profileDir(profile, explicitDir), 'pnpm-lock.yaml'), 'utf8');
|
|
488
495
|
for (const m of lock.matchAll(/resolution:\s*\{([^}]*)\}/g)) {
|
|
@@ -491,12 +498,32 @@ export function readGitResolutionCommit(profile, spec, explicitDir) {
|
|
|
491
498
|
const repo = /\brepo:\s*([^\s,}]+)/.exec(body);
|
|
492
499
|
if (commit === null || repo === null)
|
|
493
500
|
continue;
|
|
494
|
-
if (normalizeGitRepoKey(repo[1])
|
|
495
|
-
|
|
501
|
+
if (normalizeGitRepoKey(repo[1]) !== want)
|
|
502
|
+
continue;
|
|
503
|
+
const entryPath = /\bpath:\s*([^\s,}]+)/.exec(body)?.[1]?.replace(/^\/+|\/+$/g, '').toLowerCase() ?? null;
|
|
504
|
+
if (wantPath !== null) {
|
|
505
|
+
if (entryPath === wantPath)
|
|
506
|
+
return commit[1].toLowerCase();
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
matches.push(commit[1].toLowerCase());
|
|
496
510
|
}
|
|
497
511
|
}
|
|
498
512
|
catch { /* no lockfile */ }
|
|
499
|
-
|
|
513
|
+
// Exactly one entry for this remote is an identity; several are siblings
|
|
514
|
+
// of one monorepo and none of them is THIS package's commit.
|
|
515
|
+
return matches.length === 1 ? matches[0] : null;
|
|
516
|
+
}
|
|
517
|
+
/** The `path:` selector of a git spec, normalized for comparison. */
|
|
518
|
+
function gitSubpathSelector(spec) {
|
|
519
|
+
const hash = spec.indexOf('#');
|
|
520
|
+
if (hash === -1)
|
|
521
|
+
return null;
|
|
522
|
+
const selector = /(?:^|&)path:([^&]*)/.exec(spec.slice(hash + 1))?.[1];
|
|
523
|
+
if (selector === undefined)
|
|
524
|
+
return null;
|
|
525
|
+
const trimmed = selector.replace(/^\/+|\/+$/g, '').toLowerCase();
|
|
526
|
+
return trimmed === '' ? null : trimmed;
|
|
500
527
|
}
|
|
501
528
|
/** Lowercased transport-agnostic key for comparing two git remote spellings. */
|
|
502
529
|
function normalizeGitRepoKey(spec) {
|
|
@@ -654,14 +681,38 @@ export function parsePatchRows(text) {
|
|
|
654
681
|
return { names, ids, insertedIds };
|
|
655
682
|
}
|
|
656
683
|
/** Rows of the patch a package DECLARES through `dsh.bundle.patch`. */
|
|
657
|
-
|
|
658
|
-
|
|
684
|
+
/**
|
|
685
|
+
* Where a package's bundle patch lives, according to the package itself.
|
|
686
|
+
*
|
|
687
|
+
* `dsh.bundle.patch` is the package's own declaration and the only place the
|
|
688
|
+
* answer is written down: the path may be a subdirectory (`aegis` declares
|
|
689
|
+
* `./extensions/dsh/cordis.patch.yml`), not just the package root. Callers
|
|
690
|
+
* that assumed the root file made a plugin with a declared patch look like
|
|
691
|
+
* one with none (#646) — so the resolution rule lives here, once.
|
|
692
|
+
*
|
|
693
|
+
* @param dir - the installed package directory.
|
|
694
|
+
* @returns the declared patch file's path, or null when the manifest names
|
|
695
|
+
* none (or the manifest cannot be read).
|
|
696
|
+
*/
|
|
697
|
+
export function declaredBundlePatchFile(dir) {
|
|
659
698
|
try {
|
|
660
699
|
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
661
700
|
const declared = manifest.dsh?.bundle?.patch;
|
|
662
701
|
if (typeof declared !== 'string' || declared === '')
|
|
663
|
-
return
|
|
664
|
-
return
|
|
702
|
+
return null;
|
|
703
|
+
return join(dir, declared);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function readBundlePatchRows(dir) {
|
|
710
|
+
const empty = { names: [], ids: [], insertedIds: [] };
|
|
711
|
+
const file = declaredBundlePatchFile(dir);
|
|
712
|
+
if (file === null)
|
|
713
|
+
return empty;
|
|
714
|
+
try {
|
|
715
|
+
return parsePatchRows(readFileSync(file, 'utf8'));
|
|
665
716
|
}
|
|
666
717
|
catch {
|
|
667
718
|
return empty;
|
package/lib/restart.js
CHANGED
|
@@ -241,6 +241,10 @@ export function restartLaunch() {
|
|
|
241
241
|
* DSH sandbox tool runners) pops a visible node window. Wrapping the launch
|
|
242
242
|
* in `powershell -WindowStyle Hidden` gives the host a HIDDEN console that
|
|
243
243
|
* children inherit instead. POSIX keeps the plain detached spawn.
|
|
244
|
+
*
|
|
245
|
+
* The helper that runs this invocation spawns it with `windowsHide`, because
|
|
246
|
+
* the helper has no console of its own to hand down and Windows would
|
|
247
|
+
* otherwise create a visible one for PowerShell (#624).
|
|
244
248
|
*/
|
|
245
249
|
export function respawnInvocation(launch, platform = process.platform) {
|
|
246
250
|
if (platform !== 'win32') {
|
|
@@ -325,7 +329,17 @@ export function restartHelperSource(spawned, launch, logs, port) {
|
|
|
325
329
|
' try {',
|
|
326
330
|
' const out = fs.openSync(logOut, "a")',
|
|
327
331
|
' const err = fs.openSync(logErr, "a")',
|
|
328
|
-
|
|
332
|
+
// windowsHide (#624 by @davidekingsss): this helper is itself detached,
|
|
333
|
+
// so on Windows it has no console, and a console program spawned from a
|
|
334
|
+
// console-less parent is given a NEW, visible one — the "Windows
|
|
335
|
+
// PowerShell" window that owns the replacement and takes it down when
|
|
336
|
+
// closed. `-WindowStyle Hidden` cannot hide it: that flag governs the
|
|
337
|
+
// window PowerShell would create, not the console the spawn handed it.
|
|
338
|
+
// CREATE_NO_WINDOW keeps the console but never shows it, and the host's
|
|
339
|
+
// own console children inherit that hidden console rather than popping
|
|
340
|
+
// windows of their own (measured by the reporter with a console-less
|
|
341
|
+
// launcher: one PseudoConsoleWindow without the flag, none with it).
|
|
342
|
+
' child = spawn(file, args, { cwd, detached, stdio: ["ignore", out, err], env: process.env, shell: viaShell, windowsHide: true })',
|
|
329
343
|
// spawn reports a missing or unexecutable file ASYNCHRONOUSLY; the
|
|
330
344
|
// try/catch below only covers the synchronous throw, so without this
|
|
331
345
|
// listener that failure is exactly as silent as the bug being fixed.
|