@moewolf/moe-icons-cli 0.0.10 → 0.0.14
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/README.md +27 -19
- package/dist/cli.js +124 -146
- package/dist/config-package/generated/SOURCE.json +3 -3
- package/dist/config-package/generated/moeicons-config.schema.json +1 -1
- package/dist/core/bitmap-shard-budget.d.ts +2 -2
- package/dist/core/bitmap-shard-budget.js +1 -1
- package/dist/core/bootstrap.js +5 -3
- package/dist/core/candidate-version-pointer.d.ts +13 -0
- package/dist/core/candidate-version-pointer.js +37 -0
- package/dist/core/config-package-plan.d.ts +25 -0
- package/dist/core/config-package-plan.js +156 -0
- package/dist/core/doctor.js +4 -0
- package/dist/core/free-download.js +34 -4
- package/dist/core/free-small-package-download.d.ts +67 -0
- package/dist/core/free-small-package-download.js +344 -0
- package/dist/core/generate.js +49 -33
- package/dist/core/install.d.ts +8 -2
- package/dist/core/install.js +70 -0
- package/dist/core/library-update.d.ts +6 -1
- package/dist/core/library-update.js +54 -0
- package/dist/core/manage.d.ts +4 -0
- package/dist/core/manage.js +4 -0
- package/dist/core/metadata-sync.js +119 -1
- package/dist/core/pro-download.js +19 -2
- package/dist/core/pro-install.d.ts +10 -4
- package/dist/core/pro-install.js +47 -0
- package/dist/core/pro-resources.d.ts +23 -0
- package/dist/core/pro-resources.js +51 -0
- package/dist/core/pro-selected-download.js +12 -2
- package/dist/core/pro-small-package-download.d.ts +61 -0
- package/dist/core/pro-small-package-download.js +301 -0
- package/dist/core/release-descriptor.js +6 -2
- package/dist/core/selected-resources.js +2 -2
- package/dist/core/small-pack/PIN.json +11 -0
- package/dist/core/small-pack/candidate-set-v1.schema.json +187 -0
- package/dist/core/small-pack/candidate-set.cjs +286 -0
- package/dist/core/small-pack/candidate-set.d.cts +12 -0
- package/dist/core/small-pack/tar-read.cjs +220 -0
- package/dist/core/small-pack/tar-read.d.cts +2 -0
- package/dist/core/small-package-install.d.ts +40 -0
- package/dist/core/small-package-install.js +252 -0
- package/dist/core/small-package-selection.d.ts +27 -0
- package/dist/core/small-package-selection.js +114 -0
- package/dist/core/update-policy.d.ts +1 -0
- package/dist/core/update-policy.js +10 -3
- package/dist/mcp/services.js +1 -1
- package/dist/project/config.js +1 -1
- package/dist/project/install-metadata.d.ts +2 -1
- package/dist/project/install-metadata.js +6 -3
- package/dist/project/install.d.ts +11 -2
- package/dist/project/install.js +104 -12
- package/dist/project/package-install-metadata.d.ts +31 -0
- package/dist/project/package-install-metadata.js +99 -0
- package/dist/project/tar-gz.d.ts +4 -8
- package/dist/project/tar-gz.js +6 -10
- package/package.json +11 -11
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { cacheArtifact } from './cache.js';
|
|
4
|
+
import { downloadArtifact } from '../project/download.js';
|
|
5
|
+
import { FREE_DOWNLOAD_HOSTS, githubReleaseAssetUrl } from './release-descriptor.js';
|
|
6
|
+
import { parseCandidateSet, sha256 } from './small-pack/candidate-set.cjs';
|
|
7
|
+
import { readSelectedCandidate, consumeSelectedPackages, planSelectedPackages, verifySelectedPackage } from './small-package-selection.js';
|
|
8
|
+
import { parseFreeCandidateVersionPointer } from './candidate-version-pointer.js';
|
|
9
|
+
function check(ok, message) { if (!ok)
|
|
10
|
+
throw new Error(`Free package download: ${message}`); }
|
|
11
|
+
function readCache(file, maximum, exact) {
|
|
12
|
+
let handle;
|
|
13
|
+
try {
|
|
14
|
+
const entry = fs.lstatSync(file);
|
|
15
|
+
check(entry.isFile() && !entry.isSymbolicLink(), 'cached object must be a regular non-link file');
|
|
16
|
+
handle = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT')
|
|
20
|
+
return undefined;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const stat = fs.fstatSync(handle);
|
|
25
|
+
check(stat.isFile() && stat.size > 0 && stat.size <= maximum && (exact === undefined || stat.size === exact), 'cached object size/type invalid');
|
|
26
|
+
const bytes = Buffer.alloc(stat.size + 1);
|
|
27
|
+
let count = 0;
|
|
28
|
+
while (count < bytes.length) {
|
|
29
|
+
const read = fs.readSync(handle, bytes, count, bytes.length - count, count);
|
|
30
|
+
if (!read)
|
|
31
|
+
break;
|
|
32
|
+
count += read;
|
|
33
|
+
}
|
|
34
|
+
check(count === stat.size, 'cache changed during bounded read');
|
|
35
|
+
return bytes.subarray(0, count);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
fs.closeSync(handle);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function downloadFreeSelectedPackages(options) {
|
|
42
|
+
options.signal?.throwIfAborted();
|
|
43
|
+
check((options.resourceKeys !== undefined) !== (options.resolveResourceKeys !== undefined), 'provide exactly one explicit selection or candidate-based planner');
|
|
44
|
+
if (options.resourceKeys)
|
|
45
|
+
check(options.resourceKeys.length > 0 && new Set(options.resourceKeys).size === options.resourceKeys.length, 'explicit unique resource selection required');
|
|
46
|
+
const expected = options.expected;
|
|
47
|
+
check(expected.tier === 'free' && /^[a-f0-9]{64}$/.test(expected.packageSetSha256) && /^[a-f0-9]{40}$/.test(expected.sourceCommit) && /^[a-f0-9]{40}$/.test(expected.generatorCommit) && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:alpha|beta)(?:\.(?:0|[1-9]\d*))?)?$/.test(expected.fullVersion), 'exact Free candidate pin required');
|
|
48
|
+
if (options.expectedRootSha256 !== undefined)
|
|
49
|
+
check(/^[a-f0-9]{64}$/.test(options.expectedRootSha256), 'candidate root pin must be lowercase SHA-256');
|
|
50
|
+
if (options.expectedRootBytes !== undefined)
|
|
51
|
+
check(Number.isSafeInteger(options.expectedRootBytes) && options.expectedRootBytes > 0 && options.expectedRootBytes <= 1024 ** 2, 'candidate root byte pin is invalid');
|
|
52
|
+
let fixture;
|
|
53
|
+
if (options.fixtureOrigin) {
|
|
54
|
+
fixture = new URL(options.fixtureOrigin);
|
|
55
|
+
check(fixture.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(fixture.hostname) && !fixture.username && !fixture.password && !fixture.search && !fixture.hash && fixture.pathname === '/', 'fixture must be an explicit loopback origin');
|
|
56
|
+
}
|
|
57
|
+
const apiBase = resolveFreeApiBase(options.apiBaseUrl, fixture);
|
|
58
|
+
const url = (tag, filename) => fixture ? new URL(`/moewolf-dev/moe-icons/releases/download/${tag}/${filename}`, fixture).href : githubReleaseAssetUrl(tag, filename);
|
|
59
|
+
let transferRetryCount = 0;
|
|
60
|
+
const fetchBytes = async (address, maximum) => {
|
|
61
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
62
|
+
options.signal?.throwIfAborted();
|
|
63
|
+
const result = await downloadArtifact(address, { maxBytes: maximum, timeoutMs: 120000, maxRedirects: 3, allowedHosts: fixture ? [fixture.host] : apiBase.protocol === 'http:' ? [apiBase.host] : [...FREE_DOWNLOAD_HOSTS, apiBase.host], ...(fixture || apiBase.protocol === 'http:' ? { allowHttpLoopback: true } : {}), userAgent: 'moeicons/small-package' }, { ...(options.fetchFn ? { fetchFn: options.fetchFn } : {}), ...(options.signal ? { signal: options.signal } : {}) });
|
|
64
|
+
if (result.ok)
|
|
65
|
+
return result.bytes;
|
|
66
|
+
const transient = result.code === 'NETWORK_ERROR' || result.code === 'HTTP_ERROR' && /\b(?:429|500|502|503|504)\b/.test(result.message);
|
|
67
|
+
if (!transient || attempt === 2)
|
|
68
|
+
throw new Error(`Free package download: ${result.code}: ${result.message}`);
|
|
69
|
+
transferRetryCount++;
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
const aborted = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', aborted); reject(new Error('Free package download cancelled')); };
|
|
72
|
+
const timer = setTimeout(() => { options.signal?.removeEventListener('abort', aborted); resolve(); }, 250 * (attempt + 1));
|
|
73
|
+
options.signal?.addEventListener('abort', aborted, { once: true });
|
|
74
|
+
if (options.signal?.aborted)
|
|
75
|
+
aborted();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
throw new Error('Free package download retry budget exhausted');
|
|
79
|
+
};
|
|
80
|
+
const cache = join(options.cacheDir, 'small-pack-v1', 'free');
|
|
81
|
+
const io = { mkdirSync: (dir) => { fs.mkdirSync(dir, { recursive: true }); }, writeFileSync: fs.writeFileSync, renameSync: fs.renameSync, existsSync: fs.existsSync, rmSync: fs.rmSync, readFileSync: fs.readFileSync, readdirSync: fs.readdirSync };
|
|
82
|
+
const persist = (name, bytes, digest) => cacheArtifact(io, join(cache, name), bytes, digest);
|
|
83
|
+
const tag = `candidate-set-v1-free-${expected.packageSetSha256}`;
|
|
84
|
+
const rootName = `candidate-set-${expected.packageSetSha256}.json`;
|
|
85
|
+
let metadataNetworkBytes = 0, metadataCacheHits = 0;
|
|
86
|
+
let rootBytes = readCache(join(cache, rootName), 1024 ** 2);
|
|
87
|
+
const rootCached = rootBytes !== undefined;
|
|
88
|
+
if (rootBytes)
|
|
89
|
+
metadataCacheHits++;
|
|
90
|
+
else {
|
|
91
|
+
const address = fixture ? url(tag, 'candidate-set-free.json') : freePackageSetAssetUrl(apiBase, expected, 'root');
|
|
92
|
+
rootBytes = await fetchBytes(address, 1024 ** 2);
|
|
93
|
+
metadataNetworkBytes += rootBytes.length;
|
|
94
|
+
}
|
|
95
|
+
if (options.expectedRootSha256)
|
|
96
|
+
check(sha256(rootBytes) === options.expectedRootSha256, 'candidate root differs from immutable version pointer');
|
|
97
|
+
if (options.expectedRootBytes !== undefined)
|
|
98
|
+
check(rootBytes.length === options.expectedRootBytes, 'candidate root size differs from immutable identity');
|
|
99
|
+
const root = parseCandidateSet(rootBytes);
|
|
100
|
+
// Reject identity drift before any page request or cache write.
|
|
101
|
+
for (const key of ['fullVersion', 'tier', 'sourceCommit', 'generatorCommit', 'packageSetSha256'])
|
|
102
|
+
check(root[key] === expected[key], 'root differs from pinned Free identity');
|
|
103
|
+
const pending = [];
|
|
104
|
+
const candidate = await readSelectedCandidate(rootBytes, expected, async (ref) => {
|
|
105
|
+
const name = `page-${ref.sha256}.json`;
|
|
106
|
+
let bytes = readCache(join(cache, name), ref.sizeBytes, ref.sizeBytes);
|
|
107
|
+
if (bytes)
|
|
108
|
+
metadataCacheHits++;
|
|
109
|
+
else {
|
|
110
|
+
const index = root.packagePages.findIndex((item) => item.filename === ref.filename && item.sha256 === ref.sha256) + 1;
|
|
111
|
+
check(index > 0, 'page reference is not in the verified root');
|
|
112
|
+
const address = fixture ? url(tag, ref.filename) : freePackageSetAssetUrl(apiBase, expected, `pages/${index}`);
|
|
113
|
+
bytes = await fetchBytes(address, ref.sizeBytes);
|
|
114
|
+
metadataNetworkBytes += bytes.length;
|
|
115
|
+
pending.push({ name, bytes, digest: ref.sha256 });
|
|
116
|
+
}
|
|
117
|
+
return bytes;
|
|
118
|
+
});
|
|
119
|
+
if (!rootCached)
|
|
120
|
+
persist(rootName, rootBytes, sha256(rootBytes));
|
|
121
|
+
for (const page of pending)
|
|
122
|
+
persist(page.name, page.bytes, page.digest);
|
|
123
|
+
let bootstrapNetworkBytes = 0;
|
|
124
|
+
const prefetched = new Map();
|
|
125
|
+
const loadVerifiedPackage = async (row) => {
|
|
126
|
+
const trusted = candidate.rows.find(item => item.resourceKey === row.resourceKey);
|
|
127
|
+
check(trusted && trusted.packageSha256 === row.packageSha256 && trusted.compressedBytes === row.compressedBytes, 'bootstrap package is outside exact candidate rows');
|
|
128
|
+
const cached = readCache(join(cache, `${row.packageSha256}.tgz`), row.compressedBytes, row.compressedBytes);
|
|
129
|
+
const address = fixture ? url(`free-package-v1-${row.packageSha256}`, `${row.packageSha256}.tgz`) : freePackageSetAssetUrl(apiBase, expected, `packages/${row.packageSha256}.tgz`);
|
|
130
|
+
const bytes = cached ?? await fetchBytes(address, row.compressedBytes);
|
|
131
|
+
const files = verifySelectedPackage(trusted, bytes);
|
|
132
|
+
if (!cached) {
|
|
133
|
+
bootstrapNetworkBytes += bytes.length;
|
|
134
|
+
persist(`${row.packageSha256}.tgz`, bytes, row.packageSha256);
|
|
135
|
+
}
|
|
136
|
+
prefetched.set(row.packageSha256, { bytes, row: trusted, network: !cached });
|
|
137
|
+
return files;
|
|
138
|
+
};
|
|
139
|
+
const resourceKeys = options.resolveResourceKeys
|
|
140
|
+
? await options.resolveResourceKeys(candidate, loadVerifiedPackage)
|
|
141
|
+
: options.resourceKeys;
|
|
142
|
+
check(resourceKeys && resourceKeys.length > 0 && new Set(resourceKeys).size === resourceKeys.length, 'candidate planner must return explicit unique resource keys');
|
|
143
|
+
const selectedRows = planSelectedPackages(candidate, resourceKeys);
|
|
144
|
+
const prefetchedNetworkDigests = new Set([...prefetched].filter(([, value]) => value.network).map(([digest]) => digest));
|
|
145
|
+
const report = await consumeSelectedPackages(candidate, resourceKeys, {
|
|
146
|
+
readCache: row => Promise.resolve(prefetched.get(row.packageSha256)?.bytes ?? readCache(join(cache, `${row.packageSha256}.tgz`), row.compressedBytes, row.compressedBytes)),
|
|
147
|
+
writeCache: (row, bytes) => { persist(`${row.packageSha256}.tgz`, bytes, row.packageSha256); return Promise.resolve(); },
|
|
148
|
+
download: row => fetchBytes(fixture ? url(`free-package-v1-${row.packageSha256}`, `${row.packageSha256}.tgz`) : freePackageSetAssetUrl(apiBase, expected, `packages/${row.packageSha256}.tgz`), row.compressedBytes),
|
|
149
|
+
consume: options.consume,
|
|
150
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
151
|
+
});
|
|
152
|
+
const prefetchedSelectedNetworkCount = selectedRows.filter(row => prefetchedNetworkDigests.has(row.packageSha256)).length;
|
|
153
|
+
return { ...report, networkBytes: report.networkBytes + bootstrapNetworkBytes, cacheHits: report.cacheHits - prefetchedSelectedNetworkCount, metadataNetworkBytes, metadataCacheHits, transferRetryCount };
|
|
154
|
+
}
|
|
155
|
+
/** Resolve a version through its immutable public pointer, then use the same
|
|
156
|
+
* exact-pin package reader. The pointer is cached only after the candidate
|
|
157
|
+
* root and all requested units have passed verification. */
|
|
158
|
+
export async function downloadFreeVersionPackages(options) {
|
|
159
|
+
options.signal?.throwIfAborted();
|
|
160
|
+
check((options.resourceKeys !== undefined) !== (options.resolveResourceKeys !== undefined), 'provide exactly one explicit selection or candidate-based planner');
|
|
161
|
+
if (options.resourceKeys)
|
|
162
|
+
check(options.resourceKeys.length > 0 && new Set(options.resourceKeys).size === options.resourceKeys.length, 'explicit unique resource selection required');
|
|
163
|
+
check(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:alpha|beta)(?:\.(?:0|[1-9]\d*))?)?$/.test(options.fullVersion), 'exact Free version required');
|
|
164
|
+
let fixture;
|
|
165
|
+
if (options.fixtureOrigin) {
|
|
166
|
+
fixture = new URL(options.fixtureOrigin);
|
|
167
|
+
check(fixture.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(fixture.hostname) && !fixture.username && !fixture.password && !fixture.search && !fixture.hash && fixture.pathname === '/', 'fixture must be an explicit loopback origin');
|
|
168
|
+
}
|
|
169
|
+
const cache = join(options.cacheDir, 'small-pack-v1', 'free');
|
|
170
|
+
const apiBase = resolveFreeApiBase(options.apiBaseUrl, fixture);
|
|
171
|
+
if (options.expectedPackageSetSha256 !== undefined) {
|
|
172
|
+
check(/^[a-f0-9]{64}$/.test(options.expectedPackageSetSha256), 'expected package-set digest must be lowercase SHA-256');
|
|
173
|
+
const identity = await fetchFreePackageSetIdentity({
|
|
174
|
+
version: options.fullVersion,
|
|
175
|
+
packageSetSha256: options.expectedPackageSetSha256,
|
|
176
|
+
apiBase,
|
|
177
|
+
...(fixture ? { fixture } : {}),
|
|
178
|
+
...(options.fetchFn ? { fetchFn: options.fetchFn } : {}),
|
|
179
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
180
|
+
});
|
|
181
|
+
const downloadOptions = {
|
|
182
|
+
expected: identity,
|
|
183
|
+
cacheDir: options.cacheDir,
|
|
184
|
+
...(options.resourceKeys ? { resourceKeys: options.resourceKeys } : {}),
|
|
185
|
+
...(options.resolveResourceKeys ? { resolveResourceKeys: options.resolveResourceKeys } : {}),
|
|
186
|
+
consume: options.consume,
|
|
187
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
188
|
+
...(options.fetchFn ? { fetchFn: options.fetchFn } : {}),
|
|
189
|
+
...(options.fixtureOrigin ? { fixtureOrigin: options.fixtureOrigin } : {}),
|
|
190
|
+
...(options.apiBaseUrl ? { apiBaseUrl: options.apiBaseUrl } : {}),
|
|
191
|
+
expectedRootSha256: identity.candidateRootSha256,
|
|
192
|
+
expectedRootBytes: identity.candidateRootBytes,
|
|
193
|
+
};
|
|
194
|
+
const report = await downloadFreeSelectedPackages({
|
|
195
|
+
...downloadOptions,
|
|
196
|
+
});
|
|
197
|
+
return { ...report, metadataNetworkBytes: report.metadataNetworkBytes + identity.networkBytes, metadataCacheHits: report.metadataCacheHits };
|
|
198
|
+
}
|
|
199
|
+
const pointerFile = join(cache, `version-${options.fullVersion}.json`);
|
|
200
|
+
let pointerBytes = readCache(pointerFile, 16 * 1024);
|
|
201
|
+
const pointerCached = pointerBytes !== undefined;
|
|
202
|
+
if (!pointerBytes) {
|
|
203
|
+
const tag = `candidate-version-v1-free-${options.fullVersion}`;
|
|
204
|
+
const address = fixture
|
|
205
|
+
? new URL(`/moewolf-dev/moe-icons/releases/download/${tag}/candidate-version-free.json`, fixture).href
|
|
206
|
+
: githubReleaseAssetUrl(tag, 'candidate-version-free.json');
|
|
207
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
208
|
+
options.signal?.throwIfAborted();
|
|
209
|
+
const result = await downloadArtifact(address, {
|
|
210
|
+
maxBytes: 16 * 1024,
|
|
211
|
+
timeoutMs: 120000,
|
|
212
|
+
maxRedirects: 3,
|
|
213
|
+
allowedHosts: fixture ? [fixture.host] : FREE_DOWNLOAD_HOSTS,
|
|
214
|
+
...(fixture ? { allowHttpLoopback: true } : {}),
|
|
215
|
+
userAgent: 'moeicons/small-package',
|
|
216
|
+
}, { ...(options.fetchFn ? { fetchFn: options.fetchFn } : {}), ...(options.signal ? { signal: options.signal } : {}) });
|
|
217
|
+
if (result.ok) {
|
|
218
|
+
pointerBytes = result.bytes;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
const transient = result.code === 'NETWORK_ERROR' || result.code === 'HTTP_ERROR' && /\b(?:429|500|502|503|504)\b/.test(result.message);
|
|
222
|
+
if (!transient || attempt === 2)
|
|
223
|
+
throw new Error(`Free candidate version pointer: ${result.code}: ${result.message}`);
|
|
224
|
+
await new Promise((resolve, reject) => {
|
|
225
|
+
const aborted = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', aborted); reject(new Error('Free package download cancelled')); };
|
|
226
|
+
const timer = setTimeout(() => { options.signal?.removeEventListener('abort', aborted); resolve(); }, 250 * (attempt + 1));
|
|
227
|
+
options.signal?.addEventListener('abort', aborted, { once: true });
|
|
228
|
+
if (options.signal?.aborted)
|
|
229
|
+
aborted();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
check(pointerBytes, 'version pointer download returned no bytes');
|
|
234
|
+
const pointer = parseFreeCandidateVersionPointer(pointerBytes, options.fullVersion);
|
|
235
|
+
const { fullVersion, ...downloadOptions } = options;
|
|
236
|
+
const report = await downloadFreeSelectedPackages({
|
|
237
|
+
...downloadOptions,
|
|
238
|
+
expected: {
|
|
239
|
+
fullVersion,
|
|
240
|
+
tier: 'free',
|
|
241
|
+
sourceCommit: pointer.sourceCommit,
|
|
242
|
+
generatorCommit: pointer.generatorCommit,
|
|
243
|
+
packageSetSha256: pointer.packageSetSha256,
|
|
244
|
+
},
|
|
245
|
+
expectedRootSha256: pointer.candidateRootSha256,
|
|
246
|
+
expectedRootBytes: pointer.candidateRootBytes,
|
|
247
|
+
});
|
|
248
|
+
if (!pointerCached) {
|
|
249
|
+
const io = { mkdirSync: (dir) => { fs.mkdirSync(dir, { recursive: true }); }, writeFileSync: fs.writeFileSync, renameSync: fs.renameSync, existsSync: fs.existsSync, rmSync: fs.rmSync, readFileSync: fs.readFileSync, readdirSync: fs.readdirSync };
|
|
250
|
+
cacheArtifact(io, pointerFile, pointerBytes, sha256(pointerBytes));
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
...report,
|
|
254
|
+
metadataNetworkBytes: report.metadataNetworkBytes + (pointerCached ? 0 : pointerBytes.length),
|
|
255
|
+
metadataCacheHits: report.metadataCacheHits + (pointerCached ? 1 : 0),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const FREE_API_DEFAULT = 'https://api.moeicons.com';
|
|
259
|
+
const VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:alpha|beta)(?:\.(?:0|[1-9]\d*))?)?$/;
|
|
260
|
+
function resolveFreeApiBase(value, fixture) {
|
|
261
|
+
const base = new URL(value ?? fixture?.origin ?? FREE_API_DEFAULT);
|
|
262
|
+
const loopback = base.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(base.hostname);
|
|
263
|
+
check((base.protocol === 'https:' || loopback) && !base.username && !base.password && !base.search && !base.hash && base.pathname === '/', 'API base must be HTTPS or an explicit loopback origin');
|
|
264
|
+
return base;
|
|
265
|
+
}
|
|
266
|
+
function freePackageSetApiUrl(apiBase, query) {
|
|
267
|
+
const url = new URL('/v1/icon-library/free/package-set', apiBase);
|
|
268
|
+
url.search = query.toString();
|
|
269
|
+
return url.href;
|
|
270
|
+
}
|
|
271
|
+
function freePackageSetAssetUrl(apiBase, identity, path) {
|
|
272
|
+
const url = new URL(`/v1/icon-library/free/package-set/${path}`, apiBase);
|
|
273
|
+
url.searchParams.set('version', identity.fullVersion);
|
|
274
|
+
url.searchParams.set('packageSetSha256', identity.packageSetSha256);
|
|
275
|
+
return url.href;
|
|
276
|
+
}
|
|
277
|
+
async function fetchFreePackageSetIdentity(input) {
|
|
278
|
+
const query = new URLSearchParams({ version: input.version });
|
|
279
|
+
if (input.packageSetSha256)
|
|
280
|
+
query.set('packageSetSha256', input.packageSetSha256);
|
|
281
|
+
let address;
|
|
282
|
+
if (input.fixture) {
|
|
283
|
+
address = input.version === 'stable'
|
|
284
|
+
? new URL('/v1/icon-library/free/package-set?version=stable', input.fixture).href
|
|
285
|
+
: new URL(`/v1/icon-library/free/package-set?version=${encodeURIComponent(input.version)}&packageSetSha256=${encodeURIComponent(input.packageSetSha256 ?? '')}`, input.fixture).href;
|
|
286
|
+
}
|
|
287
|
+
else
|
|
288
|
+
address = freePackageSetApiUrl(input.apiBase, query);
|
|
289
|
+
const result = await downloadArtifact(address, {
|
|
290
|
+
maxBytes: 16 * 1024,
|
|
291
|
+
timeoutMs: 120000,
|
|
292
|
+
maxRedirects: 0,
|
|
293
|
+
allowedHosts: input.fixture ? [input.fixture.host] : [input.apiBase.host],
|
|
294
|
+
...(input.fixture || input.apiBase.protocol === 'http:' ? { allowHttpLoopback: true } : {}),
|
|
295
|
+
userAgent: 'moeicons/small-package',
|
|
296
|
+
}, { ...(input.fetchFn ? { fetchFn: input.fetchFn } : {}), ...(input.signal ? { signal: input.signal } : {}) });
|
|
297
|
+
if (!result.ok)
|
|
298
|
+
throw new Error(`Free package-set identity: ${result.code}: ${result.message}`);
|
|
299
|
+
let raw;
|
|
300
|
+
try {
|
|
301
|
+
raw = JSON.parse(Buffer.from(result.bytes).toString('utf8'));
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
throw new Error('Free package-set identity: invalid JSON');
|
|
305
|
+
}
|
|
306
|
+
check(raw !== null && typeof raw === 'object' && !Array.isArray(raw), 'identity object required');
|
|
307
|
+
const value = raw;
|
|
308
|
+
const keys = ['schemaVersion', 'tier', 'fullVersion', 'sourceCommit', 'generatorCommit', 'packageSetSha256', 'candidateRootSha256', 'candidateRootBytes', 'updatedAt'];
|
|
309
|
+
check(Object.keys(value).sort().join(',') === [...keys].sort().join(','), 'identity fields differ from the Free package-set contract');
|
|
310
|
+
check(value.schemaVersion === 1 && value.tier === 'free' && typeof value.fullVersion === 'string' && VERSION_PATTERN.test(value.fullVersion), 'Free version identity invalid');
|
|
311
|
+
check(input.version === 'stable' || value.fullVersion === input.version, 'identity version differs from request');
|
|
312
|
+
check(typeof value.sourceCommit === 'string' && /^[a-f0-9]{40}$/.test(value.sourceCommit), 'Source commit invalid');
|
|
313
|
+
check(typeof value.generatorCommit === 'string' && /^[a-f0-9]{40}$/.test(value.generatorCommit), 'Generator commit invalid');
|
|
314
|
+
check(typeof value.packageSetSha256 === 'string' && /^[a-f0-9]{64}$/.test(value.packageSetSha256), 'package-set digest invalid');
|
|
315
|
+
check(input.packageSetSha256 === undefined || value.packageSetSha256 === input.packageSetSha256, 'identity package-set digest differs from request');
|
|
316
|
+
check(typeof value.candidateRootSha256 === 'string' && /^[a-f0-9]{64}$/.test(value.candidateRootSha256), 'candidate root digest invalid');
|
|
317
|
+
check(Number.isSafeInteger(value.candidateRootBytes) && Number(value.candidateRootBytes) > 0 && Number(value.candidateRootBytes) <= 1024 ** 2, 'candidate root size invalid');
|
|
318
|
+
check(typeof value.updatedAt === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value.updatedAt) && !Number.isNaN(Date.parse(value.updatedAt)), 'identity update timestamp invalid');
|
|
319
|
+
return { ...value, networkBytes: result.bytes.length };
|
|
320
|
+
}
|
|
321
|
+
export async function fetchFreeStablePackageSet(options = {}) {
|
|
322
|
+
let fixture;
|
|
323
|
+
if (options.fixtureOrigin) {
|
|
324
|
+
fixture = new URL(options.fixtureOrigin);
|
|
325
|
+
check(fixture.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(fixture.hostname) && !fixture.username && !fixture.password && !fixture.search && !fixture.hash && fixture.pathname === '/', 'fixture must be an explicit loopback origin');
|
|
326
|
+
}
|
|
327
|
+
const apiBase = resolveFreeApiBase(options.apiBaseUrl, fixture);
|
|
328
|
+
return fetchFreePackageSetIdentity({ version: 'stable', apiBase, ...(fixture ? { fixture } : {}), ...(options.fetchFn ? { fetchFn: options.fetchFn } : {}), ...(options.signal ? { signal: options.signal } : {}) });
|
|
329
|
+
}
|
|
330
|
+
export async function downloadFreeStablePackages(options) {
|
|
331
|
+
const identity = await fetchFreeStablePackageSet({ ...(options.apiBaseUrl ? { apiBaseUrl: options.apiBaseUrl } : {}), ...(options.fixtureOrigin ? { fixtureOrigin: options.fixtureOrigin } : {}), ...(options.fetchFn ? { fetchFn: options.fetchFn } : {}), ...(options.signal ? { signal: options.signal } : {}) });
|
|
332
|
+
const { apiBaseUrl, fixtureOrigin, fetchFn, signal, ...rest } = options;
|
|
333
|
+
const report = await downloadFreeSelectedPackages({
|
|
334
|
+
...rest,
|
|
335
|
+
...(apiBaseUrl ? { apiBaseUrl } : {}),
|
|
336
|
+
...(fixtureOrigin ? { fixtureOrigin } : {}),
|
|
337
|
+
...(fetchFn ? { fetchFn } : {}),
|
|
338
|
+
...(signal ? { signal } : {}),
|
|
339
|
+
expected: identity,
|
|
340
|
+
expectedRootSha256: identity.candidateRootSha256,
|
|
341
|
+
expectedRootBytes: identity.candidateRootBytes,
|
|
342
|
+
});
|
|
343
|
+
return { ...report, metadataNetworkBytes: report.metadataNetworkBytes + identity.networkBytes };
|
|
344
|
+
}
|
package/dist/core/generate.js
CHANGED
|
@@ -13,6 +13,7 @@ import { artifactCachePath } from "./free-download.js";
|
|
|
13
13
|
import { resolveThemes } from "../generator/theme-resolve.js";
|
|
14
14
|
import { loadPinnedBitmapShardAssets } from "./bitmap-shard-resolver.js";
|
|
15
15
|
import { parseInstallMetadata, serializeInstallMetadata, sha256Bytes, } from "../project/install-metadata.js";
|
|
16
|
+
import { parsePackageInstallMetadata, serializePackageInstallMetadata } from "../project/package-install-metadata.js";
|
|
16
17
|
import { withProjectLockSync } from "../project/project-lock.js";
|
|
17
18
|
import { allowLocalTestFromEnv } from "./local-test-env.js";
|
|
18
19
|
import { resolveIconTheme, themeHasIcon } from "./icon-selection.js";
|
|
@@ -68,7 +69,7 @@ export function loadInstalledCatalogState(projectRoot, fs_, opts = {}) {
|
|
|
68
69
|
const rawMetadata = fs_.readFileSync(metadataPath, "utf8");
|
|
69
70
|
if (typeof rawMetadata !== "string")
|
|
70
71
|
throw new Error("metadata is not text");
|
|
71
|
-
metadata = parseInstallMetadata(rawMetadata, opts);
|
|
72
|
+
metadata = parseInstallMetadata(rawMetadata, opts) ?? parsePackageInstallMetadata(rawMetadata);
|
|
72
73
|
}
|
|
73
74
|
catch (error) {
|
|
74
75
|
return {
|
|
@@ -124,24 +125,19 @@ function missingArchiveMessage(config) {
|
|
|
124
125
|
}
|
|
125
126
|
return "bitmap themes require a downloaded free artifact; run `moeicons install` or set MOEICONS_BITMAP_ARCHIVE";
|
|
126
127
|
}
|
|
127
|
-
function readInstallMetadata(projectRoot, readFileSync, existsSync) {
|
|
128
|
+
function readInstallMetadata(projectRoot, readFileSync, existsSync, opts = {}) {
|
|
128
129
|
const path = join(projectRoot, ".moeicons", "install-metadata.json");
|
|
129
130
|
if (!existsSync(path))
|
|
130
131
|
return undefined;
|
|
131
132
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
...(typeof parsed.target === "string" &&
|
|
141
|
-
["react", "vue", "vanilla", "assets"].includes(parsed.target)
|
|
142
|
-
? { target: parsed.target }
|
|
143
|
-
: {}),
|
|
144
|
-
};
|
|
133
|
+
const raw = readFileSync(path, "utf8");
|
|
134
|
+
const parsed = parseInstallMetadata(raw, opts);
|
|
135
|
+
if (parsed)
|
|
136
|
+
return { artifactVersion: parsed.artifactVersion, artifactSha256: parsed.artifactSha256, target: parsed.target };
|
|
137
|
+
const packageMetadata = parsePackageInstallMetadata(raw);
|
|
138
|
+
if (packageMetadata)
|
|
139
|
+
return { artifactVersion: packageMetadata.candidate.fullVersion, target: packageMetadata.target };
|
|
140
|
+
return undefined;
|
|
145
141
|
}
|
|
146
142
|
catch {
|
|
147
143
|
return undefined;
|
|
@@ -189,10 +185,17 @@ function mergePinnedBitmapShardAssets(archiveFiles, effectiveConfig, sourceCatal
|
|
|
189
185
|
errors: ["bitmap shards require an installed pro artifact; run `moeicons install` first"],
|
|
190
186
|
};
|
|
191
187
|
}
|
|
192
|
-
const
|
|
188
|
+
const rawMetadata = fs_.readFileSync(metadataPath, "utf8");
|
|
189
|
+
const metadata = parseInstallMetadata(rawMetadata, {
|
|
193
190
|
allowLocalTest: allowLocalTestFromEnv(env),
|
|
194
191
|
});
|
|
195
192
|
if (!metadata) {
|
|
193
|
+
if (parsePackageInstallMetadata(rawMetadata)) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
errors: ["selected bitmap packages are missing for this configuration; run `moeicons install` to download the configured icons, format and imageSize"],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
196
199
|
return {
|
|
197
200
|
ok: false,
|
|
198
201
|
errors: ["install metadata is invalid; run 'moeicons install' to repair or reinstall"],
|
|
@@ -259,10 +262,10 @@ export async function loadArchiveFiles(projectRoot, env, fs_, injected, signal)
|
|
|
259
262
|
}
|
|
260
263
|
const selectionMetadataPath = join(projectRoot, ".moeicons", "install-metadata.json");
|
|
261
264
|
if (fs_.existsSync(selectionMetadataPath)) {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (pinned?.delivery?.mode === "icons") {
|
|
265
|
+
const raw = fs_.readFileSync(selectionMetadataPath, "utf8");
|
|
266
|
+
const parseOptions = { allowLocalTest: allowLocalTestFromEnv(env) };
|
|
267
|
+
const pinned = parseInstallMetadata(raw, parseOptions) ?? parsePackageInstallMetadata(raw);
|
|
268
|
+
if (pinned?.schemaVersion === 1 && pinned.delivery?.mode === "icons") {
|
|
266
269
|
const files = {};
|
|
267
270
|
for (const [name, hash] of Object.entries(pinned.managedFiles)) {
|
|
268
271
|
if (!name.startsWith(".moeicons/artifact/"))
|
|
@@ -280,8 +283,23 @@ export async function loadArchiveFiles(projectRoot, env, fs_, injected, signal)
|
|
|
280
283
|
}
|
|
281
284
|
return { ok: true, files };
|
|
282
285
|
}
|
|
286
|
+
if (pinned?.schemaVersion === 2) {
|
|
287
|
+
const files = {};
|
|
288
|
+
for (const [name, hash] of Object.entries(pinned.managedFiles)) {
|
|
289
|
+
if (!name.startsWith(".moeicons/artifact/"))
|
|
290
|
+
continue;
|
|
291
|
+
const target = safeManagedPath(projectRoot, name).target;
|
|
292
|
+
if (!fs_.existsSync(target))
|
|
293
|
+
return { ok: false, reason: `selected resource missing: ${name}; run moeicons install` };
|
|
294
|
+
const bytes = readBinaryFile(fs_, target);
|
|
295
|
+
if (sha256Bytes(bytes) !== hash)
|
|
296
|
+
return { ok: false, reason: `selected resource modified: ${name}; preserve your edit before reinstalling` };
|
|
297
|
+
files[name.slice(".moeicons/artifact/".length)] = bytes;
|
|
298
|
+
}
|
|
299
|
+
return { ok: true, files };
|
|
300
|
+
}
|
|
283
301
|
}
|
|
284
|
-
const meta = readInstallMetadata(projectRoot, fs_.readFileSync, fs_.existsSync);
|
|
302
|
+
const meta = readInstallMetadata(projectRoot, fs_.readFileSync, fs_.existsSync, { allowLocalTest: allowLocalTestFromEnv(env) });
|
|
285
303
|
if (meta?.target) {
|
|
286
304
|
const subtreeRoot = join(projectRoot, ".moeicons", "artifact", meta.target);
|
|
287
305
|
if (fs_.existsSync(subtreeRoot)) {
|
|
@@ -356,7 +374,7 @@ export async function runGenerateUseCase(context, fs_, options = {}) {
|
|
|
356
374
|
const plannedMetadataText = fs_.existsSync(plannedMetadataPath)
|
|
357
375
|
? fs_.readFileSync(plannedMetadataPath, "utf8")
|
|
358
376
|
: undefined;
|
|
359
|
-
const installedMeta = readInstallMetadata(project.root, fs_.readFileSync, fs_.existsSync);
|
|
377
|
+
const installedMeta = readInstallMetadata(project.root, fs_.readFileSync, fs_.existsSync, { allowLocalTest });
|
|
360
378
|
if (!installedMeta) {
|
|
361
379
|
return {
|
|
362
380
|
ok: false,
|
|
@@ -450,12 +468,12 @@ export async function runGenerateUseCase(context, fs_, options = {}) {
|
|
|
450
468
|
ok: false,
|
|
451
469
|
reason: "managed install metadata is missing; run 'moeicons install' to repair or reinstall",
|
|
452
470
|
};
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (fs_.readFileSync(metadataPath, "utf8") !== plannedMetadataText)
|
|
471
|
+
const currentMetadataText = fs_.readFileSync(metadataPath, "utf8");
|
|
472
|
+
const metadata = parseInstallMetadata(currentMetadataText, { allowLocalTest }) ?? parsePackageInstallMetadata(currentMetadataText);
|
|
473
|
+
if (currentMetadataText !== plannedMetadataText)
|
|
457
474
|
return { ok: false, reason: "installation changed while preparing generate; retry" };
|
|
458
|
-
|
|
475
|
+
const metadataTier = metadata?.schemaVersion === 1 ? metadata.tier : metadata?.candidate.tier;
|
|
476
|
+
if (!metadata || metadataTier !== loaded.config.tier)
|
|
459
477
|
return {
|
|
460
478
|
ok: false,
|
|
461
479
|
reason: "managed install metadata is invalid or does not match config tier; run 'moeicons install' to repair or reinstall",
|
|
@@ -521,11 +539,9 @@ export async function runGenerateUseCase(context, fs_, options = {}) {
|
|
|
521
539
|
nextManaged[managedPath] = hash;
|
|
522
540
|
for (const [path, content] of Object.entries(generated))
|
|
523
541
|
nextManaged[path] = sha256Bytes(content);
|
|
524
|
-
const
|
|
525
|
-
...metadata,
|
|
526
|
-
managedFiles: nextManaged,
|
|
527
|
-
generatedOutputDir: loaded.config.outputDir,
|
|
528
|
-
};
|
|
542
|
+
const nextMetadataText = metadata.schemaVersion === 1
|
|
543
|
+
? serializeInstallMetadata({ ...metadata, managedFiles: nextManaged, generatedOutputDir: loaded.config.outputDir })
|
|
544
|
+
: serializePackageInstallMetadata({ ...metadata, managedFiles: nextManaged, generatedOutputDir: loaded.config.outputDir });
|
|
529
545
|
const writes = { ...generated };
|
|
530
546
|
for (const file of sideFiles) {
|
|
531
547
|
const absolute = resolve(file.path);
|
|
@@ -534,7 +550,7 @@ export async function runGenerateUseCase(context, fs_, options = {}) {
|
|
|
534
550
|
return { ok: false, reason: `side file escapes project: ${file.path}` };
|
|
535
551
|
writes[rel] = file.content;
|
|
536
552
|
}
|
|
537
|
-
writes[".moeicons/install-metadata.json"] =
|
|
553
|
+
writes[".moeicons/install-metadata.json"] = nextMetadataText;
|
|
538
554
|
const stale = Object.keys(metadata.managedFiles).filter((path) => (path.startsWith(outputPrefix) || path.startsWith(previousPrefix)) &&
|
|
539
555
|
!(path in generated));
|
|
540
556
|
const expectedSha256 = {
|
package/dist/core/install.d.ts
CHANGED
|
@@ -13,14 +13,20 @@ export type InstallResult = {
|
|
|
13
13
|
readonly planItems: number;
|
|
14
14
|
readonly config: string;
|
|
15
15
|
readonly artifactVersion: string;
|
|
16
|
-
readonly descriptorSha256
|
|
16
|
+
readonly descriptorSha256?: string;
|
|
17
|
+
readonly candidateRootSha256?: string;
|
|
18
|
+
readonly packageSetSha256?: string;
|
|
19
|
+
readonly packageCount?: number;
|
|
17
20
|
readonly catalogSha256: string;
|
|
18
21
|
readonly metadataSha256: string;
|
|
19
22
|
readonly cacheHit: boolean;
|
|
20
|
-
readonly downloadMode?: "icons" | "full";
|
|
23
|
+
readonly downloadMode?: "icons" | "full" | "packages";
|
|
21
24
|
readonly downloadNotes?: readonly string[];
|
|
22
25
|
readonly networkBytes?: number;
|
|
23
26
|
readonly selectedFiles?: number;
|
|
27
|
+
readonly cacheHits?: number;
|
|
28
|
+
readonly metadataCacheHits?: number;
|
|
29
|
+
readonly transferRetryCount?: number;
|
|
24
30
|
} | {
|
|
25
31
|
readonly ok: false;
|
|
26
32
|
readonly reason: "no-project";
|
package/dist/core/install.js
CHANGED
|
@@ -9,6 +9,7 @@ import { withProjectLock } from "../project/project-lock.js";
|
|
|
9
9
|
import { configuredComponentFiles, selectTargetSubtreeAsync, selectTargetSubtreeFromFiles, computeSubtreeHash, } from "./target-subtree.js";
|
|
10
10
|
import { parseCatalog } from "../catalog/catalog.js";
|
|
11
11
|
import { posix } from "node:path";
|
|
12
|
+
import { runSmallPackageInstall } from "./small-package-install.js";
|
|
12
13
|
/**
|
|
13
14
|
* P0-3: the project `types.ts` must re-export a type that actually exists in the
|
|
14
15
|
* installed `moe-icons` package. Assets have no component type, so it is empty.
|
|
@@ -90,6 +91,75 @@ export async function runInstallUseCase(context, deps, options) {
|
|
|
90
91
|
message: `config target is "${config.config.target}" but install target is "${target}"; set target to "${target}" in moeicons.config before installing`,
|
|
91
92
|
};
|
|
92
93
|
}
|
|
94
|
+
// Production Free installs use the immutable package-set pointer and exact
|
|
95
|
+
// configured dependency closure. Aggregate archives remain available only to
|
|
96
|
+
// the isolated local archive-fixture harness while old fixtures are retired.
|
|
97
|
+
if (!deps.download.fixtureDir) {
|
|
98
|
+
if (config.kind !== "ok") {
|
|
99
|
+
return { ok: false, reason: "validation", message: "run 'moeicons init' and select icons/themes before installing the package set" };
|
|
100
|
+
}
|
|
101
|
+
if (config.config.downloadMode === "full") {
|
|
102
|
+
return { ok: false, reason: "validation", message: "full archive installs are disabled; set downloadMode to icons and select the required resources" };
|
|
103
|
+
}
|
|
104
|
+
if (options.expectedDescriptorSha256) {
|
|
105
|
+
return { ok: false, reason: "validation", message: "legacy descriptor pins cannot identify a package-set candidate; retry through the package-set update command" };
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const result = await runSmallPackageInstall({
|
|
109
|
+
projectRoot: project.root,
|
|
110
|
+
...(options.sourceVersion ? { fullVersion: options.sourceVersion } : {}),
|
|
111
|
+
...(context.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: context.env.MOEICONS_API_BASE_URL } : {}),
|
|
112
|
+
config: config.config,
|
|
113
|
+
plannedConfigDocument,
|
|
114
|
+
fs: deps.fs,
|
|
115
|
+
cacheDir: deps.download.cacheDir,
|
|
116
|
+
now: context.now,
|
|
117
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
118
|
+
...(deps.download.fetchFn ? { fetchFn: deps.download.fetchFn } : {}),
|
|
119
|
+
...(deps.download.fixtureBaseUrl ? { fixtureOrigin: deps.download.fixtureBaseUrl } : {}),
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
projectRoot: project.root,
|
|
124
|
+
packageManager: project.packageManager,
|
|
125
|
+
group: "free",
|
|
126
|
+
target,
|
|
127
|
+
artifactBytes: result.artifactBytes,
|
|
128
|
+
planItems: result.planItems,
|
|
129
|
+
config: config.kind,
|
|
130
|
+
artifactVersion: result.artifactVersion,
|
|
131
|
+
candidateRootSha256: result.candidateRootSha256,
|
|
132
|
+
packageSetSha256: result.packageSetSha256,
|
|
133
|
+
packageCount: result.packageCount,
|
|
134
|
+
catalogSha256: result.catalogSha256,
|
|
135
|
+
metadataSha256: result.metadataSha256,
|
|
136
|
+
cacheHit: result.cacheHit,
|
|
137
|
+
downloadMode: "packages",
|
|
138
|
+
downloadNotes: result.fallbacks.map((fallback) => `Theme fallback: ${fallback}`),
|
|
139
|
+
networkBytes: result.networkBytes,
|
|
140
|
+
selectedFiles: result.selectedFiles,
|
|
141
|
+
cacheHits: result.cacheHits,
|
|
142
|
+
metadataCacheHits: result.metadataCacheHits,
|
|
143
|
+
transferRetryCount: result.transferRetryCount,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
148
|
+
if (context.signal?.aborted || /CANCELLED|cancelled/i.test(message))
|
|
149
|
+
return { ok: false, reason: "cancelled", message };
|
|
150
|
+
if (/ENOSPC|no space left/i.test(message))
|
|
151
|
+
return { ok: false, reason: "disk-full", message };
|
|
152
|
+
if (/HTTP_ERROR[^\n]*\b404\b/i.test(message))
|
|
153
|
+
return { ok: false, reason: "not-found", message };
|
|
154
|
+
if (/HTTP_ERROR[^\n]*\b5\d\d\b|NETWORK_ERROR|network|fetch failed|timed out/i.test(message))
|
|
155
|
+
return { ok: false, reason: "network", message };
|
|
156
|
+
if (/checksum|SHA-256|digest mismatch|differs from immutable version pointer/i.test(message))
|
|
157
|
+
return { ok: false, reason: "checksum-mismatch", message };
|
|
158
|
+
if (/config changed|unsafe|invalid|requires downloadMode|candidate-set:|config package plan:|small package install currently/i.test(message))
|
|
159
|
+
return { ok: false, reason: "validation", message };
|
|
160
|
+
return { ok: false, reason: "write-failed", message };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
93
163
|
const downloaded = await downloadFreeRelease({ ...deps.download, signal: context.signal }, options.sourceVersion ?? bundledSourceVersion(), config.kind === "ok"
|
|
94
164
|
? {
|
|
95
165
|
config: config.config,
|
|
@@ -17,12 +17,17 @@ export declare function runLibraryUpdateUseCase(context: CommandContext, deps: L
|
|
|
17
17
|
readonly tier: "free" | "pro";
|
|
18
18
|
readonly version: string;
|
|
19
19
|
readonly descriptorSha256: string;
|
|
20
|
+
readonly packageSetSha256?: string;
|
|
20
21
|
}): Promise<{
|
|
21
22
|
readonly projectRoot: string;
|
|
22
23
|
readonly artifactVersion: string;
|
|
23
24
|
readonly files: readonly string[];
|
|
24
|
-
readonly downloadMode: "icons" | "full";
|
|
25
|
+
readonly downloadMode: "icons" | "full" | "packages";
|
|
25
26
|
readonly downloadNotes: readonly string[];
|
|
26
27
|
readonly networkBytes?: number;
|
|
27
28
|
readonly selectedFiles?: number;
|
|
29
|
+
readonly packageCount?: number;
|
|
30
|
+
readonly cacheHits?: number;
|
|
31
|
+
readonly metadataCacheHits?: number;
|
|
32
|
+
readonly transferRetryCount?: number;
|
|
28
33
|
}>;
|