@chatcode/cco-market 1.45.1
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.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
package/lib/themes.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme lifecycle: classifying installed packages as themes (by the
|
|
3
|
+
* registry's theme category), live-toggling bundle-layer entries through
|
|
4
|
+
* the loader, and keeping exactly one theme active with the choice
|
|
5
|
+
* persisted across restarts.
|
|
6
|
+
*/
|
|
7
|
+
import { loadRegistry, pluginCategories } from './registry.js';
|
|
8
|
+
import { hotMount, hotUnmount, listHotMounts, writeDisabled } from './hot.js';
|
|
9
|
+
import { logEvent } from './log.js';
|
|
10
|
+
import { profileDir, readInstalled } from './profile.js';
|
|
11
|
+
import { repoOf } from './sources.js';
|
|
12
|
+
/**
|
|
13
|
+
* Create the theme manager. `disabledThemes` is the live, shared set of
|
|
14
|
+
* themes the user switched off — the caller owns reading it at boot and
|
|
15
|
+
* replaying it; the manager mutates and persists it on switches.
|
|
16
|
+
*/
|
|
17
|
+
export function createThemeManager(host, profile, disabledThemes, explicitDir) {
|
|
18
|
+
const activeProfileDir = profileDir(profile, explicitDir);
|
|
19
|
+
/** Installed package names classified as themes by the registry's theme category. */
|
|
20
|
+
async function installedThemeNames() {
|
|
21
|
+
const names = new Set();
|
|
22
|
+
try {
|
|
23
|
+
const registry = await loadRegistry();
|
|
24
|
+
const themeEntries = registry.plugins.filter(p => pluginCategories(p).includes('theme'));
|
|
25
|
+
const themeNames = new Set(themeEntries.map(p => p.name));
|
|
26
|
+
const themeRepos = new Set(themeEntries.map(p => repoOf(p.url)).filter((r) => r !== null).map(r => r.toLowerCase()));
|
|
27
|
+
for (const [name, spec] of Object.entries(readInstalled(profile, activeProfileDir))) {
|
|
28
|
+
if (themeNames.has(name)) {
|
|
29
|
+
names.add(name);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const match = /github:([^#\s]+)/.exec(String(spec).toLowerCase());
|
|
33
|
+
if (match !== null && themeRepos.has(match[1]))
|
|
34
|
+
names.add(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch { /* registry unavailable — nothing classifies as a theme */ }
|
|
38
|
+
return names;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Live-toggle a bundle-layer plugin through its loader entry. Bundle trees
|
|
42
|
+
* are in-memory (write is a no-op), so this never touches any file — the
|
|
43
|
+
* market persists the choice itself and replays it at boot.
|
|
44
|
+
* @returns true when a matching live entry was found and updated.
|
|
45
|
+
*/
|
|
46
|
+
async function setEntryDisabled(name, disabledFlag) {
|
|
47
|
+
let found = false;
|
|
48
|
+
for (const entry of host.loader.entries()) {
|
|
49
|
+
if (entry.options.name !== name)
|
|
50
|
+
continue;
|
|
51
|
+
// A disable can land while the entry's init is still in flight: the
|
|
52
|
+
// options flip but the finishing init brings the fiber up anyway, and a
|
|
53
|
+
// plain re-update no-ops on the empty diff. Force the update and verify
|
|
54
|
+
// the live state, retrying until reality matches the flag.
|
|
55
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
await entry.update({ disabled: disabledFlag ? true : null }, false, true);
|
|
58
|
+
found = true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
logEvent('warn', 'toggle', `${name}: entry update failed — ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
const live = entry.fiber !== undefined;
|
|
65
|
+
if (live !== disabledFlag)
|
|
66
|
+
break;
|
|
67
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 200));
|
|
68
|
+
}
|
|
69
|
+
logEvent('info', 'toggle', `${name} -> ${disabledFlag ? 'off' : 'on'}: fiber=${String(entry.fiber !== undefined)}`);
|
|
70
|
+
}
|
|
71
|
+
if (!found)
|
|
72
|
+
logEvent('info', 'toggle', `${name}: no loader entry matched`);
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Make `name` the one active theme: deactivate every other installed theme
|
|
77
|
+
* (market hot mounts unmount; bundle-layer entries live-disable) and bring
|
|
78
|
+
* it up. The choice persists in state.json and is replayed at boot.
|
|
79
|
+
*/
|
|
80
|
+
async function activateTheme(name) {
|
|
81
|
+
const themes = await installedThemeNames();
|
|
82
|
+
for (const other of themes) {
|
|
83
|
+
if (other === name)
|
|
84
|
+
continue;
|
|
85
|
+
if (listHotMounts().includes(other)) {
|
|
86
|
+
await hotUnmount(other);
|
|
87
|
+
disabledThemes.add(other);
|
|
88
|
+
}
|
|
89
|
+
else if (await setEntryDisabled(other, true)) {
|
|
90
|
+
disabledThemes.add(other);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
disabledThemes.delete(name);
|
|
94
|
+
writeDisabled(activeProfileDir, disabledThemes);
|
|
95
|
+
if (listHotMounts().includes(name))
|
|
96
|
+
return true;
|
|
97
|
+
if (await setEntryDisabled(name, false))
|
|
98
|
+
return true;
|
|
99
|
+
return (await hotMount(host, activeProfileDir, name)).ok;
|
|
100
|
+
}
|
|
101
|
+
return { installedThemeNames, setEntryDisabled, activateTheme };
|
|
102
|
+
}
|
package/lib/trial.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trial validation for composition changes — issue #98 (phase 3), the
|
|
3
|
+
* "trial boot" half of #19 reduced to what is safe and offline: before any
|
|
4
|
+
* bundle-order or preset change is written to the profile, replay the
|
|
5
|
+
* composition with the CANDIDATE order using the same entry-list machinery
|
|
6
|
+
* the real boot uses (src/check.ts's buildBundleLayers + composeLayers), and
|
|
7
|
+
* refuse the change when the composed tree would fail to boot (duplicate
|
|
8
|
+
* loader entry ids, unresolvable bundle layers, unparseable patches).
|
|
9
|
+
*
|
|
10
|
+
* No process, no network, no writes to the profile: the real profile is only
|
|
11
|
+
* read; if the candidate is bad, the failure is reported and nothing is
|
|
12
|
+
* applied (the caller then skips the write-back entirely).
|
|
13
|
+
*
|
|
14
|
+
* Issue #125 review: the CURRENT composition is replayed alongside the
|
|
15
|
+
* candidate, so the response also carries a current-vs-candidate diff
|
|
16
|
+
* (overrides / orphans / duplicates the reorder introduces) — not just
|
|
17
|
+
* whether it boots.
|
|
18
|
+
*
|
|
19
|
+
* Bundle resolution is deliberately SHARED with the check report
|
|
20
|
+
* (check.ts's buildBundleLayers): the dsh installation anchor first, then
|
|
21
|
+
* Node's module search from the profile (workspace-root hoisting) — so the
|
|
22
|
+
* trial can never disagree with the diagnostics about what a bundle is or
|
|
23
|
+
* where it lives, and official bundles resolve even when they are only
|
|
24
|
+
* hoisted to the workspace root.
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { buildBundleLayers, composeLayers, findDshInstallDir, parsePatchFile, } from './check.js';
|
|
29
|
+
import { resolveDshHome } from './home-paths.js';
|
|
30
|
+
import { mergeOrder, readBundleStack } from './order.js';
|
|
31
|
+
/**
|
|
32
|
+
* Replay the profile composition with `newCommunityOrder` (the candidate
|
|
33
|
+
* community-bundle order; official bundles keep their exact positions) and
|
|
34
|
+
* report anything that would break the boot. Pure read.
|
|
35
|
+
*/
|
|
36
|
+
export function trialValidate(profileDir, newCommunityOrder, options = {}) {
|
|
37
|
+
const errors = [];
|
|
38
|
+
const warnings = [];
|
|
39
|
+
const current = readBundleStack(profileDir);
|
|
40
|
+
// Same merge the apply path uses (order.ts mergeOrder): the trial replays
|
|
41
|
+
// EXACTLY the stack a successful apply would write.
|
|
42
|
+
const merged = mergeOrder(current.bundles, newCommunityOrder);
|
|
43
|
+
if (!merged.ok) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false, errors: [{ layer: '(order)', message: merged.error }], warnings,
|
|
46
|
+
duplicates: [], rows: [],
|
|
47
|
+
diff: { overrides: [], orphans: [], duplicates: [] },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const candidate = merged.bundles;
|
|
51
|
+
// Shared bundle-layer build — identical resolution to analyzeProfile
|
|
52
|
+
// (dsh install anchor first, then Node module search from the profile).
|
|
53
|
+
let specs = {};
|
|
54
|
+
try {
|
|
55
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
56
|
+
specs = manifest.dependencies ?? {};
|
|
57
|
+
}
|
|
58
|
+
catch { /* specs stay empty — sources read as '(not a direct dependency)' */ }
|
|
59
|
+
const dshInstall = options.dshInstallDir !== undefined ? options.dshInstallDir : findDshInstallDir();
|
|
60
|
+
// Same user/home patch layers as the boot (and the check report) — shared
|
|
61
|
+
// by BOTH compositions.
|
|
62
|
+
const patchLayers = [];
|
|
63
|
+
const userPatchPath = join(profileDir, 'cordis.patch.yml');
|
|
64
|
+
if (existsSync(userPatchPath)) {
|
|
65
|
+
const patches = parsePatchFile(userPatchPath);
|
|
66
|
+
patchLayers.push({ label: 'user-patch', kind: 'user', patches: patches ?? [], parseError: null });
|
|
67
|
+
if (patches === null)
|
|
68
|
+
errors.push({ layer: 'user-patch', message: 'cordis.patch.yml is not a valid entry list / cordis.patch.yml 不是合法的条目列表' });
|
|
69
|
+
}
|
|
70
|
+
const home = resolveDshHome(options.homeDir);
|
|
71
|
+
const homePatchPath = join(home, 'cordis.patch.yml');
|
|
72
|
+
if (existsSync(homePatchPath)) {
|
|
73
|
+
const patches = parsePatchFile(homePatchPath);
|
|
74
|
+
patchLayers.push({ label: 'home-patch', kind: 'home', patches: patches ?? [], parseError: null });
|
|
75
|
+
if (patches === null)
|
|
76
|
+
errors.push({ layer: 'home-patch', message: 'home cordis.patch.yml is not a valid entry list / 全局 cordis.patch.yml 不是合法的条目列表' });
|
|
77
|
+
}
|
|
78
|
+
/** Compose the loader tree for one bundle order (current or candidate). */
|
|
79
|
+
const compose = (bundleOrder) => {
|
|
80
|
+
const built = buildBundleLayers(profileDir, bundleOrder, specs, dshInstall);
|
|
81
|
+
return { built, composed: composeLayers([...built.layers, ...patchLayers]) };
|
|
82
|
+
};
|
|
83
|
+
const currentState = compose(current.bundles);
|
|
84
|
+
const candidateState = compose(candidate);
|
|
85
|
+
const composed = candidateState.composed;
|
|
86
|
+
for (const bundle of candidateState.built.bundles) {
|
|
87
|
+
if (bundle.error !== null)
|
|
88
|
+
errors.push({ layer: bundle.name, message: bundle.error });
|
|
89
|
+
if (bundle.parseError !== null)
|
|
90
|
+
errors.push({ layer: bundle.name, message: bundle.parseError });
|
|
91
|
+
}
|
|
92
|
+
for (const dup of composed.duplicates) {
|
|
93
|
+
errors.push({ layer: dup.layers.join(' / '), message: `duplicate loader entry id ${JSON.stringify(dup.id)} (${dup.count} rows) / 重复的 loader 条目 id ${JSON.stringify(dup.id)}` });
|
|
94
|
+
}
|
|
95
|
+
for (const orphan of composed.orphans) {
|
|
96
|
+
warnings.push({ layer: orphan.layer, message: `${orphan.id}: ${orphan.reason}` });
|
|
97
|
+
}
|
|
98
|
+
// Issue #125 review: what does the reorder CHANGE? Compare the candidate
|
|
99
|
+
// composition against the current one — only candidate-introduced
|
|
100
|
+
// overrides / orphans / duplicates are reported.
|
|
101
|
+
const currentDupIds = new Set(currentState.composed.duplicates.map(d => d.id));
|
|
102
|
+
const sameOverride = (a, b) => a.id === b.id && a.layer === b.layer && a.overriddenLayers.join('\u0000') === b.overriddenLayers.join('\u0000');
|
|
103
|
+
const diff = {
|
|
104
|
+
overrides: composed.overrides.filter(o => !currentState.composed.overrides.some(c => sameOverride(o, c))),
|
|
105
|
+
orphans: composed.orphans.filter(o => !currentState.composed.orphans.some(c => c.id === o.id && c.layer === o.layer)),
|
|
106
|
+
duplicates: composed.duplicates.filter(d => !currentDupIds.has(d.id)),
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
ok: errors.length === 0,
|
|
110
|
+
errors,
|
|
111
|
+
warnings,
|
|
112
|
+
duplicates: composed.duplicates,
|
|
113
|
+
rows: composed.rows.map(row => ({ id: row.id, layer: row.layer })),
|
|
114
|
+
diff,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a GitHub install through a region's proxy without proxying the
|
|
3
|
+
* tarball pnpm records.
|
|
4
|
+
*
|
|
5
|
+
* pnpm does not fetch `github:owner/repo` with `git clone`; it resolves the
|
|
6
|
+
* shortcut and downloads a tarball from codeload.github.com. That rules out
|
|
7
|
+
* the usual `git config insteadOf` trick — there is no git command to
|
|
8
|
+
* redirect.
|
|
9
|
+
*
|
|
10
|
+
* Prefix-proxying that tarball used to save substantial time, but pnpm 11's
|
|
11
|
+
* fail-closed lockfile checks changed the safety boundary. A URL shaped like
|
|
12
|
+
* `https://proxy/https://codeload.github.com/.../<commit>` no longer has the
|
|
13
|
+
* codeload hostname, so pnpm treats it as an ordinary remote tarball and
|
|
14
|
+
* requires an integrity hash that its GitHub resolver does not write. The
|
|
15
|
+
* result is ERR_PNPM_MISSING_TARBALL_INTEGRITY on the install itself (#385).
|
|
16
|
+
* Disabling that check or minting a checksum from whatever the proxy served
|
|
17
|
+
* would weaken pnpm's supply-chain protection, so neither is acceptable.
|
|
18
|
+
*
|
|
19
|
+
* The safe middle ground is to resolve HEAD through the regional proxy, then
|
|
20
|
+
* hand pnpm a commit-pinned `github:owner/repo#<sha>` target. The ref lookup
|
|
21
|
+
* still avoids a slow or unreachable GitHub metadata request; pnpm itself
|
|
22
|
+
* fetches the canonical codeload URL, records it as `gitHosted: true`, and
|
|
23
|
+
* keeps its integrity policy intact.
|
|
24
|
+
*
|
|
25
|
+
* - **The commit has to be pinned.** The profile reads each plugin's
|
|
26
|
+
* installed commit back out of the lockfile by matching a codeload URL
|
|
27
|
+
* ending in a 40-character SHA (src/profile.ts). A `HEAD` tarball installs
|
|
28
|
+
* perfectly and then reports no version forever. So this resolves the SHA
|
|
29
|
+
* first, and a rewrite that cannot get one does not happen.
|
|
30
|
+
* - **Build-script approval has to keep matching.** `gitAllowBuildsKey`
|
|
31
|
+
* (src/sources.ts) derives its stable key from the repo, and the pinned
|
|
32
|
+
* GitHub form preserves that identity.
|
|
33
|
+
*
|
|
34
|
+
* Catalog subpath entries are left alone: `#path:` identifies a different
|
|
35
|
+
* package inside the repo, so the acceleration step never rewrites that
|
|
36
|
+
* fragment. Collection roots discovered after install preserve the resolved
|
|
37
|
+
* commit and add their subpath as pnpm's second `&path:` selector.
|
|
38
|
+
*
|
|
39
|
+
* Every SHA-lookup failure falls back to the original target. Acceleration
|
|
40
|
+
* is an optimisation, and an optimisation that can fail an install is a bug.
|
|
41
|
+
*/
|
|
42
|
+
import { type Region } from './regions.ts';
|
|
43
|
+
/**
|
|
44
|
+
* The commit `HEAD` points at, from git's own ref advertisement.
|
|
45
|
+
*
|
|
46
|
+
* This is the endpoint `git clone` reads before it fetches anything, and it
|
|
47
|
+
* is the right one here for two measured reasons. It is not the REST API, so
|
|
48
|
+
* it does not consume the 60-requests-per-hour unauthenticated quota that a
|
|
49
|
+
* user installing a handful of plugins could plausibly exhaust. And it
|
|
50
|
+
* survives the proxy: over five consecutive tries from an unproxied mainland
|
|
51
|
+
* connection it answered 200 in ~1.2s every time, while the REST API through
|
|
52
|
+
* the same proxy returned 200, 200, then 403 — a proxy that rate-limits the
|
|
53
|
+
* API path would silently drop every install back to the slow route.
|
|
54
|
+
*
|
|
55
|
+
* The response is git's pkt-line format, whose first ref line carries
|
|
56
|
+
* `<sha> HEAD\0<capabilities>`. Read with a pattern rather than a parser:
|
|
57
|
+
* one 40-character hex string followed by `HEAD` is unambiguous in this
|
|
58
|
+
* payload, and a length-prefix reader would be more code to get wrong.
|
|
59
|
+
*
|
|
60
|
+
* The same advertisement lists every branch and tag, which is what `ref`
|
|
61
|
+
* uses. A plugin installed from `github:owner/repo#publish` is not asking
|
|
62
|
+
* about the default branch at all, and answering with it made the market
|
|
63
|
+
* report an update forever (#446) — the two SHAs simply never match. Both
|
|
64
|
+
* namespaces are searched because pnpm accepts a tag there too, branches
|
|
65
|
+
* first since that is what the reports are about.
|
|
66
|
+
* @param ref - branch or tag to resolve, or undefined for the default branch.
|
|
67
|
+
*/
|
|
68
|
+
export declare function headCommit(repo: string, proxy: string | null, signal?: AbortSignal, ref?: string): Promise<string | null>;
|
|
69
|
+
/**
|
|
70
|
+
* The current `HEAD` commit for a repo, on whichever route the region uses.
|
|
71
|
+
*
|
|
72
|
+
* Wraps the timeout so callers outside the install path — the build-script
|
|
73
|
+
* approval below, which needs a commit-pinned key — do not each reinvent it.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveHeadCommit(repo: string, region: Region, env?: NodeJS.ProcessEnv, ref?: string): Promise<string | null>;
|
|
76
|
+
/**
|
|
77
|
+
* The install target to actually hand pnpm, given the region in force.
|
|
78
|
+
*
|
|
79
|
+
* @param target - what `installTargetFor` produced.
|
|
80
|
+
* @param region - the download region.
|
|
81
|
+
* @param env - environment, for the proxy override.
|
|
82
|
+
* @returns a commit-pinned GitHub shortcut when one route resolves HEAD,
|
|
83
|
+
* otherwise `target` unchanged.
|
|
84
|
+
*/
|
|
85
|
+
export declare function acceleratedTarget(target: string, region: Region, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional host-provided agent inventory: lets the market refuse plugin
|
|
3
|
+
* mutations while a live agent is mid-turn. Mutating `node_modules` under a
|
|
4
|
+
* running agent replaces files the running plugin modules may still read or
|
|
5
|
+
* lazily import — the update "succeeds" while old code and new files mix.
|
|
6
|
+
*/
|
|
7
|
+
export interface RunningAgentLike {
|
|
8
|
+
id?: unknown;
|
|
9
|
+
status?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface AgentsServiceLike {
|
|
12
|
+
list(): RunningAgentLike[];
|
|
13
|
+
}
|
|
14
|
+
export type AgentsLookup = () => AgentsServiceLike | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Ids of the host's currently running agents, or [] when the host has no
|
|
17
|
+
* agents service. Only a positive `status === 'running'` blocks an update —
|
|
18
|
+
* unknown statuses are treated as not running, so a future agent
|
|
19
|
+
* implementation with different wording fails open (the market stays
|
|
20
|
+
* usable) instead of wedging the plugin page.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runningAgentIds(agents: AgentsServiceLike | undefined): string[];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable profile backups: configuration only, never installed packages.
|
|
3
|
+
*
|
|
4
|
+
* The profile directory is plain user data — aside from package.json it can
|
|
5
|
+
* hold API keys (config.toml), tokens, or the WebDAV password when stored
|
|
6
|
+
* server-side. Backups therefore behave like `dsh export` and carry the same
|
|
7
|
+
* credential-warning disclaimer in the UI (review #63).
|
|
8
|
+
*/
|
|
9
|
+
export declare const BACKUP_FORMAT = "dsh-profile-backup";
|
|
10
|
+
export declare const MAX_BACKUP_BYTES: number;
|
|
11
|
+
/** File names that routinely contain credentials (backup exports).
|
|
12
|
+
* Values are never masked in place — the export is one-to-one for
|
|
13
|
+
* faithful restores — but presence is surfaced by the UI warning. */
|
|
14
|
+
export declare const SECRET_FILE_HINTS: RegExp;
|
|
15
|
+
/** Count of exported files whose names look like they carry credentials. */
|
|
16
|
+
export declare function secretFileCount(profile: string, explicitDir?: string): number;
|
|
17
|
+
export type BackupFile = {
|
|
18
|
+
path: 'package.json';
|
|
19
|
+
json: Record<string, unknown>;
|
|
20
|
+
} | {
|
|
21
|
+
path: string;
|
|
22
|
+
lines: string[];
|
|
23
|
+
};
|
|
24
|
+
export interface ProfileBackup {
|
|
25
|
+
format: typeof BACKUP_FORMAT;
|
|
26
|
+
version: 0.2;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
profile: string;
|
|
29
|
+
files: BackupFile[];
|
|
30
|
+
}
|
|
31
|
+
/** Serialize every profile file except dependencies, lock state, and market cache. */
|
|
32
|
+
export interface BackupOptions {
|
|
33
|
+
/** Partial export: only these plugins (dependency names) are kept. */
|
|
34
|
+
includeDeps?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* With includeDeps, also carry the profile's other configuration files.
|
|
37
|
+
* Config files are profile-scoped and cannot be attributed to individual
|
|
38
|
+
* plugins, so this is all-or-nothing — the UI warns about secrets before
|
|
39
|
+
* enabling it (the backup itself is one-to-one, never masked).
|
|
40
|
+
*/
|
|
41
|
+
includeConfig?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Serialize every profile file except dependencies, lock state, and market
|
|
45
|
+
* cache — or, with {@link BackupOptions.includeDeps}, only the manifest with
|
|
46
|
+
* the selected plugins (plus, optionally, the other config files).
|
|
47
|
+
*/
|
|
48
|
+
export declare function createProfileBackup(profile: string, explicitDir?: string, opts?: BackupOptions): ProfileBackup;
|
|
49
|
+
export declare function validatedBackup(value: unknown): ProfileBackup;
|
|
50
|
+
/** Atomically overwrite backed-up files and return a rollback for install failure. */
|
|
51
|
+
export declare function restoreProfileBackup(profile: string, value: unknown, explicitDir?: string): {
|
|
52
|
+
files: number;
|
|
53
|
+
rollback(): void;
|
|
54
|
+
};
|
|
55
|
+
interface PublicAddress {
|
|
56
|
+
address: string;
|
|
57
|
+
family: 4 | 6;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ancestor collection URLs of a WebDAV file, outermost first.
|
|
61
|
+
* `https://dav.example/a/b/x.json` → [`https://dav.example/a/`, `https://dav.example/a/b/`].
|
|
62
|
+
* The server root itself is never included — it always exists, and some
|
|
63
|
+
* providers reject MKCOL on it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function webdavParentCollections(url: string): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Upload the backup, creating missing parent collections first (#102).
|
|
68
|
+
*
|
|
69
|
+
* WebDAV servers do not create intermediate collections implicitly, so a PUT
|
|
70
|
+
* into a folder that does not exist yet fails — Jianguoyun answers 404, which
|
|
71
|
+
* read as "sync is broken" rather than "make the folder first". MKCOL on an
|
|
72
|
+
* existing collection answers 405, which is success for our purposes; any
|
|
73
|
+
* other failure is left to the PUT to report, since some providers restrict
|
|
74
|
+
* MKCOL while still accepting the upload.
|
|
75
|
+
*/
|
|
76
|
+
export declare function uploadWebdav(url: string, username: string, password: string, backup: ProfileBackup): Promise<void>;
|
|
77
|
+
/** Refuse non-global IPv4 targets, including metadata and carrier NAT ranges. */
|
|
78
|
+
export declare function isPublicIpv4(ip: string): boolean;
|
|
79
|
+
/** Only public internet target hostnames are reachable for WebDAV. */
|
|
80
|
+
export declare function isPublicHostname(hostname: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Whether a WebDAV hostname may be fetched: public https targets only.
|
|
83
|
+
* Exported for tests.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isPublicTarget(hostname: string): boolean;
|
|
86
|
+
/** Only global-unicast IPv6 is usable for a server-side WebDAV connection. */
|
|
87
|
+
export declare function isPublicIpv6(ip: string): boolean;
|
|
88
|
+
/** Resolve once, reject every unsafe answer, and return the address to pin. */
|
|
89
|
+
export declare function resolvePublicAddress(hostname: string): Promise<PublicAddress>;
|
|
90
|
+
export declare function downloadWebdav(url: string, username: string, password: string): Promise<unknown>;
|
|
91
|
+
export interface PluginSelection {
|
|
92
|
+
deps: Record<string, string>;
|
|
93
|
+
bundles: string[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The selected plugins' dependency specs and bundle entries from a backup's
|
|
97
|
+
* manifest. Only string specs survive — everything else in the manifest is
|
|
98
|
+
* untrusted and ignored (partial restore touches nothing but these).
|
|
99
|
+
*/
|
|
100
|
+
export declare function extractPluginSelection(backup: ProfileBackup, includeDeps: string[]): PluginSelection;
|
|
101
|
+
/**
|
|
102
|
+
* Merge a backup's manifest into the profile's current manifest so a restore
|
|
103
|
+
* never deletes plugins the target machine already has: current deps stay,
|
|
104
|
+
* backup specs win on name conflicts; bundle lists are unioned. When
|
|
105
|
+
* `selection` is given, only the selected plugins are merged in.
|
|
106
|
+
*/
|
|
107
|
+
/**
|
|
108
|
+
* Dependencies whose spec points at an absolute local path — `link:/Users/…`
|
|
109
|
+
* or `file:/home/…` (#205 by @Rudyy898).
|
|
110
|
+
*
|
|
111
|
+
* These are perfectly valid on the machine that wrote them and meaningless
|
|
112
|
+
* anywhere else, so a backup carrying one restores a manifest that `pnpm
|
|
113
|
+
* install` cannot satisfy: the path does not exist on the new machine and
|
|
114
|
+
* the whole restore fails on it.
|
|
115
|
+
*
|
|
116
|
+
* Reported, NOT rewritten. Turning `link:/Users/me/dev/plugin` into
|
|
117
|
+
* something portable means deciding where those files should live and
|
|
118
|
+
* whether to carry them at all, which is a design question and not
|
|
119
|
+
* something a restore should answer on the user's behalf. Naming them lets
|
|
120
|
+
* the operator decide before the install runs — which is the part that was
|
|
121
|
+
* missing.
|
|
122
|
+
*
|
|
123
|
+
* Relative `file:./vendor/x` specs are left alone: they resolve against the
|
|
124
|
+
* profile directory, which the restore recreates, so they travel fine.
|
|
125
|
+
*/
|
|
126
|
+
export declare function unportableDeps(dependencies: unknown): Array<{
|
|
127
|
+
name: string;
|
|
128
|
+
spec: string;
|
|
129
|
+
}>;
|
|
130
|
+
export declare function mergeRestoreManifest(backupManifest: Record<string, unknown>, current: Record<string, unknown>, selection?: PluginSelection): Record<string, unknown>;
|
|
131
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog matching for locally linked / file: installs. Shared by the host
|
|
3
|
+
* restore route and the Market client so both refuse the same wrong guesses.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The catalog entry a locally linked / file: install should restore to.
|
|
7
|
+
* Exact `#path:` identities win, then collection-root identities against
|
|
8
|
+
* root-only catalog rows, then a unique name/npm match when nothing
|
|
9
|
+
* contradicts it. A bare repo identity never selects a root row while
|
|
10
|
+
* `/tree/` siblings exist for that repo — the checkout did not say which
|
|
11
|
+
* package it is, and guessing wrong installs a different plugin.
|
|
12
|
+
* Same-named forks without identities or a matching hint stay unmatched
|
|
13
|
+
* rather than guessing; declared repo evidence that matches nothing in the
|
|
14
|
+
* catalog must not fall back to a coincidental unique name.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findCatalogEntryForLocal<T extends {
|
|
17
|
+
name: string;
|
|
18
|
+
npm?: string | null;
|
|
19
|
+
url: string;
|
|
20
|
+
}>(plugins: readonly T[], name: string, identities?: readonly string[], hints?: readonly string[]): T | null;
|
|
21
|
+
export type CatalogRestoreReason = 'no-catalog' | 'repo-mismatch';
|
|
22
|
+
/**
|
|
23
|
+
* Why a local restore was blocked, when findCatalogEntryForLocal returned
|
|
24
|
+
* null — and, when it matched, whether anything but the NAME agreed.
|
|
25
|
+
*
|
|
26
|
+
* `verified` is the difference between "put this back where it came from"
|
|
27
|
+
* and "install the catalog's plugin that happens to share this name". A
|
|
28
|
+
* local checkout with no declared repo — no git remote, no `repository`
|
|
29
|
+
* field — gives the market nothing to match on, so a unique same-named
|
|
30
|
+
* catalog entry is a guess. Usually a good one (#429: the local copy IS a
|
|
31
|
+
* tweaked copy of that entry). Not always: @liuwenji007 reported a fork of
|
|
32
|
+
* their own `dsh-humanizer` restored to a different author's plugin of the
|
|
33
|
+
* same name (#485), which is someone else's code arriving under a button
|
|
34
|
+
* labelled "restore".
|
|
35
|
+
*
|
|
36
|
+
* The match is kept, because refusing it would break the ordinary case, and
|
|
37
|
+
* the caller is told not to present it as a certainty.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveCatalogRestore<T extends {
|
|
40
|
+
name: string;
|
|
41
|
+
npm?: string | null;
|
|
42
|
+
url: string;
|
|
43
|
+
}>(plugins: readonly T[], name: string, identities?: readonly string[], hints?: readonly string[]): {
|
|
44
|
+
ok: true;
|
|
45
|
+
entry: T;
|
|
46
|
+
verified: boolean;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: CatalogRestoreReason;
|
|
50
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the plugin catalog out of a published npm package.
|
|
3
|
+
*
|
|
4
|
+
* The catalog's own host is the thing being worked around. `plugins.json` is
|
|
5
|
+
* served from GitHub Pages, and the public GitHub proxies that make GitHub
|
|
6
|
+
* usable from mainland China refuse any hostname that is not github.com's
|
|
7
|
+
* own — measured: the project domain comes back 403. So the file cannot be
|
|
8
|
+
* carried through the same route as everything else while it lives there.
|
|
9
|
+
*
|
|
10
|
+
* Published to npm it can be. Every China npm mirror already carries it, it
|
|
11
|
+
* is the same infrastructure the plugins themselves arrive on, and it needs
|
|
12
|
+
* no service that did not already have to work. It also gains a version
|
|
13
|
+
* number, which the origin never had: a catalog build that ships bad data
|
|
14
|
+
* can be rolled back instead of only being fixed forwards.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors rewrite `dist.tarball` to point at themselves, so following that
|
|
17
|
+
* field is what keeps the download on the mirror rather than bouncing back
|
|
18
|
+
* to the origin registry — verified against the Tencent mirror, which serves
|
|
19
|
+
* its own host in that field.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* One file's bytes from a gzipped tar, or null when it is not in there.
|
|
23
|
+
*
|
|
24
|
+
* @param gz - the gzipped tarball.
|
|
25
|
+
* @param wanted - the exact entry name, npm-style (`package/plugins.json`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function fileFromTarball(gz: Buffer, wanted: string): Buffer | null;
|
|
28
|
+
/**
|
|
29
|
+
* The catalog carried by a published package, and the version it came from.
|
|
30
|
+
*
|
|
31
|
+
* The version doubles as the cache validator — a better one than an ETag,
|
|
32
|
+
* because it is meaningful to a human reading a log and it is the thing a
|
|
33
|
+
* rollback would change.
|
|
34
|
+
*
|
|
35
|
+
* @param registry - registry base, no trailing slash.
|
|
36
|
+
* @param pkg - package name.
|
|
37
|
+
* @param known - a version already held, to skip re-downloading.
|
|
38
|
+
* @returns the parsed JSON and its version, or `{ version, data: null }` when
|
|
39
|
+
* the published version is the one already held.
|
|
40
|
+
* @throws when the package, the tarball or the file inside it cannot be read.
|
|
41
|
+
*/
|
|
42
|
+
export declare function catalogFromPackage(registry: string, pkg: string, known?: string,
|
|
43
|
+
/** The entry to extract; the catalog package carries `plugins.json`, companions theirs. */
|
|
44
|
+
file?: string): Promise<{
|
|
45
|
+
version: string;
|
|
46
|
+
data: unknown | null;
|
|
47
|
+
}>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update notes: what changed between an installed version and HEAD.
|
|
3
|
+
*
|
|
4
|
+
* Every byte of GitHub data comes from the catalog side's daily probe,
|
|
5
|
+
* published as the `dsh-plugin-updates` package (origin fallback
|
|
6
|
+
* `updates.json` beside `plugins.json`). The market asks GitHub for nothing:
|
|
7
|
+
* anonymous REST quota is shared per egress IP and already unreliable behind
|
|
8
|
+
* common proxies, and this data is wanted by thousands of markets but
|
|
9
|
+
* produced perfectly well once a day by one probe.
|
|
10
|
+
*
|
|
11
|
+
* What the catalog cannot know is where each user's installed version sits,
|
|
12
|
+
* so slicing the commit tail at that boundary happens here: the same sha the
|
|
13
|
+
* update check resolved (`current`) marks where "new" begins. A tail that
|
|
14
|
+
* does not contain it means the range is wider than the tail — those are
|
|
15
|
+
* labelled recent commits rather than pretending to be an exact interval.
|
|
16
|
+
* Plugins with neither notes nor commits fall back to npm publish times, and
|
|
17
|
+
* a plugin nothing answers for gets a neutral "no notes" — absence of release
|
|
18
|
+
* notes is the ordinary condition of small plugins, not a defect.
|
|
19
|
+
*/
|
|
20
|
+
export interface ReleaseNotes {
|
|
21
|
+
tag: string | null;
|
|
22
|
+
name: string | null;
|
|
23
|
+
publishedAt: string | null;
|
|
24
|
+
url: string | null;
|
|
25
|
+
body: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CommitNote {
|
|
28
|
+
sha: string;
|
|
29
|
+
message: string;
|
|
30
|
+
date: string | null;
|
|
31
|
+
}
|
|
32
|
+
interface UpdatesEntry {
|
|
33
|
+
release?: ReleaseNotes | null;
|
|
34
|
+
commits?: CommitNote[];
|
|
35
|
+
}
|
|
36
|
+
interface UpdatesPayload {
|
|
37
|
+
count?: number;
|
|
38
|
+
updates?: Record<string, UpdatesEntry>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The update-notes payload from whichever route the region serves.
|
|
42
|
+
*
|
|
43
|
+
* Same source order as the catalog itself — the npm package first where one
|
|
44
|
+
* exists (its mirror reach is the reason the package exists), then the
|
|
45
|
+
* origin. Version-keyed like the catalog too: an unchanged payload costs one
|
|
46
|
+
* packument read per TTL, not a tarball.
|
|
47
|
+
*/
|
|
48
|
+
export declare function loadUpdateNotes(force?: boolean): Promise<UpdatesPayload>;
|
|
49
|
+
/** Drop the cached payload (nothing currently invalidates it mid-process). */
|
|
50
|
+
export declare function invalidateUpdateNotes(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Slice the commit tail at the installed sha.
|
|
53
|
+
*
|
|
54
|
+
* @returns the commits newer than `installed`, and whether the boundary was
|
|
55
|
+
* actually found — `found === false` means the range is wider than the tail
|
|
56
|
+
* (or history diverged), so callers must label the result "recent" rather
|
|
57
|
+
* than exact.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sliceCommitsAt(items: CommitNote[], installed: string | null): {
|
|
60
|
+
items: CommitNote[];
|
|
61
|
+
found: boolean;
|
|
62
|
+
};
|
|
63
|
+
/** The repo url key an entry's update notes live under, or null. */
|
|
64
|
+
export declare function repoKeyOf(spec: string): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Look an entry up by its catalog url.
|
|
67
|
+
*
|
|
68
|
+
* The catalog keys entries by the url as listed (author's casing preserved);
|
|
69
|
+
* the market resolves repos to lowercase (as every other consumer of
|
|
70
|
+
* `repoOfTarget` sees them). Match case-insensitively so the two sides never
|
|
71
|
+
* disagree over a capital letter.
|
|
72
|
+
*/
|
|
73
|
+
export declare function entryForRepo(payload: UpdatesPayload, key: string): UpdatesEntry | undefined;
|
|
74
|
+
export declare function npmPublishTimes(name: string): Promise<{
|
|
75
|
+
version: string;
|
|
76
|
+
date: string;
|
|
77
|
+
}[]>;
|
|
78
|
+
/** Everything the dialog renders for one plugin, already resolved. */
|
|
79
|
+
export interface UpdateNotes {
|
|
80
|
+
kind: 'release' | 'commits' | 'npm' | 'none';
|
|
81
|
+
/** Present for `release`: the author's own notes for the latest release. */
|
|
82
|
+
release?: ReleaseNotes;
|
|
83
|
+
/** Present for `commits`: the tail, sliced when the boundary was found. */
|
|
84
|
+
commits?: {
|
|
85
|
+
items: CommitNote[];
|
|
86
|
+
found: boolean;
|
|
87
|
+
};
|
|
88
|
+
/** Present for `npm`: recent versions with their publish dates. */
|
|
89
|
+
npmTimes?: {
|
|
90
|
+
version: string;
|
|
91
|
+
date: string;
|
|
92
|
+
}[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the notes for one installed plugin, or `{ kind: 'none' }`.
|
|
96
|
+
*
|
|
97
|
+
* Never throws: every failure along the way degrades to the next tier, the
|
|
98
|
+
* same contract the issue promised — a dialog that cannot load its data shows
|
|
99
|
+
* a neutral statement, not an error.
|
|
100
|
+
*/
|
|
101
|
+
export declare function updateNotesFor(profile: string, explicitDir: string | undefined, name: string): Promise<UpdateNotes>;
|
|
102
|
+
export {};
|