@moewolf/moe-icons-cli 0.0.1 → 0.0.3
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 +249 -10
- package/bin/check-node.cjs +36 -0
- package/bin/moeicons.js +12 -1
- package/dist/api/client.d.ts +13 -2
- package/dist/api/client.js +145 -21
- package/dist/auth/device-login.d.ts +10 -7
- package/dist/auth/device-login.js +60 -37
- package/dist/auth/login-schemas.d.ts +24 -0
- package/dist/auth/login-schemas.js +67 -0
- package/dist/catalog/catalog.d.ts +11 -0
- package/dist/catalog/catalog.js +43 -0
- package/dist/catalog/catalog.json +12768 -3965
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +773 -147
- package/dist/commands/parser.d.ts +13 -2
- package/dist/commands/parser.js +138 -50
- package/dist/config-package/generated/SOURCE.json +25 -0
- package/dist/config-package/generated/moeicons-config.schema.json +191 -0
- package/dist/config-package/generated/moeicons.config.jsonc +42 -0
- package/dist/config-package/generated/render-config.cjs +145 -0
- package/dist/config-package/generated/validate-config.cjs +293 -0
- package/dist/config-package/generated-config.d.ts +32 -0
- package/dist/config-package/generated-config.js +19 -0
- package/dist/core/auth.d.ts +24 -0
- package/dist/core/auth.js +127 -8
- package/dist/core/bitmap-assets.d.ts +19 -5
- package/dist/core/bitmap-assets.js +40 -31
- package/dist/core/bitmap-shard-budget.d.ts +43 -0
- package/dist/core/bitmap-shard-budget.js +16 -0
- package/dist/core/bitmap-shard-cache.d.ts +68 -0
- package/dist/core/bitmap-shard-cache.js +115 -0
- package/dist/core/bitmap-shard-download.d.ts +130 -0
- package/dist/core/bitmap-shard-download.js +359 -0
- package/dist/core/bitmap-shard-resolver.d.ts +129 -0
- package/dist/core/bitmap-shard-resolver.js +262 -0
- package/dist/core/bitmap-shards.d.ts +70 -0
- package/dist/core/bitmap-shards.js +249 -0
- package/dist/core/bootstrap.d.ts +37 -0
- package/dist/core/bootstrap.js +54 -0
- package/dist/core/cache.d.ts +34 -0
- package/dist/core/cache.js +96 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/doctor.d.ts +48 -0
- package/dist/core/doctor.js +190 -0
- package/dist/core/free-download.d.ts +64 -6
- package/dist/core/free-download.js +402 -30
- package/dist/core/generate.d.ts +29 -0
- package/dist/core/generate.js +341 -65
- package/dist/core/icon-selection.d.ts +8 -0
- package/dist/core/icon-selection.js +19 -0
- package/dist/core/install.d.ts +14 -0
- package/dist/core/install.js +132 -13
- package/dist/core/library-update.d.ts +5 -1
- package/dist/core/library-update.js +176 -17
- package/dist/core/local-test-env.d.ts +24 -0
- package/dist/core/local-test-env.js +43 -0
- package/dist/core/metadata-sync.d.ts +30 -0
- package/dist/core/metadata-sync.js +97 -0
- package/dist/core/pro-download.d.ts +63 -0
- package/dist/core/pro-download.js +247 -27
- package/dist/core/pro-install.d.ts +11 -3
- package/dist/core/pro-install.js +181 -19
- package/dist/core/pro-resources.d.ts +79 -0
- package/dist/core/pro-resources.js +204 -0
- package/dist/core/pro-selected-download.d.ts +18 -0
- package/dist/core/pro-selected-download.js +269 -0
- package/dist/core/release-descriptor.d.ts +42 -1
- package/dist/core/release-descriptor.js +117 -9
- package/dist/core/resource-variant.d.ts +27 -10
- package/dist/core/resource-variant.js +75 -34
- package/dist/core/selected-resources.d.ts +59 -0
- package/dist/core/selected-resources.js +350 -0
- package/dist/core/signed-artifact.d.ts +8 -1
- package/dist/core/signed-artifact.js +35 -11
- package/dist/core/target-subtree.d.ts +8 -0
- package/dist/core/target-subtree.js +54 -3
- package/dist/core/version-service.d.ts +11 -0
- package/dist/core/version-service.js +129 -24
- package/dist/core/wizard.d.ts +20 -17
- package/dist/core/wizard.js +66 -28
- package/dist/errors/errors.d.ts +2 -1
- package/dist/errors/errors.js +1 -0
- package/dist/generator/generate.d.ts +1 -1
- package/dist/generator/generate.js +420 -213
- package/dist/generator/shared/SOURCE.json +7 -0
- package/dist/generator/shared/svg-model.cjs +156 -0
- package/dist/generator/svg-model.d.ts +16 -0
- package/dist/generator/svg-model.js +4 -0
- package/dist/generator/theme-resolve.d.ts +2 -0
- package/dist/generator/theme-resolve.js +57 -26
- package/dist/mcp/server.d.ts +23 -36
- package/dist/mcp/server.js +74 -152
- package/dist/mcp/services.d.ts +4 -0
- package/dist/mcp/services.js +230 -0
- package/dist/metadata/archive.d.ts +38 -0
- package/dist/metadata/archive.js +94 -0
- package/dist/metadata/manifest.d.ts +33 -0
- package/dist/metadata/manifest.js +136 -0
- package/dist/metadata/version.d.ts +22 -0
- package/dist/metadata/version.js +53 -0
- package/dist/project/anchors/application.d.ts +13 -0
- package/dist/project/anchors/application.js +377 -0
- package/dist/project/anchors/apply.d.ts +28 -0
- package/dist/project/anchors/apply.js +65 -0
- package/dist/project/anchors/diagnose.d.ts +26 -0
- package/dist/project/anchors/diagnose.js +90 -0
- package/dist/project/anchors/helpers.d.ts +20 -0
- package/dist/project/anchors/helpers.js +55 -0
- package/dist/project/anchors/moeicons-config.d.ts +21 -0
- package/dist/project/anchors/moeicons-config.js +83 -0
- package/dist/project/anchors/parse.d.ts +14 -0
- package/dist/project/anchors/parse.js +36 -0
- package/dist/project/anchors/project-manifest.d.ts +19 -0
- package/dist/project/anchors/project-manifest.js +109 -0
- package/dist/project/anchors/style.d.ts +12 -0
- package/dist/project/anchors/style.js +87 -0
- package/dist/project/anchors/types.d.ts +32 -0
- package/dist/project/anchors/types.js +9 -0
- package/dist/project/config.d.ts +59 -5
- package/dist/project/config.js +234 -102
- package/dist/project/install-metadata.d.ts +23 -2
- package/dist/project/install-metadata.js +58 -7
- package/dist/project/install.d.ts +24 -4
- package/dist/project/install.js +402 -190
- package/dist/project/project-lock.d.ts +2 -2
- package/dist/project/project-lock.js +4 -4
- package/dist/project/tailwind.d.ts +2 -2
- package/dist/project/tailwind.js +3 -4
- package/dist/project/tar-gz.d.ts +18 -1
- package/dist/project/tar-gz.js +40 -3
- package/dist/tui/components.d.ts +62 -0
- package/dist/tui/components.js +100 -0
- package/dist/tui/primitives.d.ts +1 -0
- package/dist/tui/primitives.js +8 -1
- package/dist/ui/banner.d.ts +31 -3
- package/dist/ui/banner.js +120 -25
- package/dist/ui/branded-prompts.d.ts +41 -0
- package/dist/ui/branded-prompts.js +80 -0
- package/dist/ui/clack.d.ts +6 -3
- package/dist/ui/clack.js +56 -5
- package/dist/ui/create-ui.d.ts +1 -0
- package/dist/ui/create-ui.js +5 -1
- package/dist/ui/generated/wordmark-large.d.ts +17 -0
- package/dist/ui/generated/wordmark-large.js +28 -0
- package/dist/ui/generated/wordmark.d.ts +2 -0
- package/dist/ui/generated/wordmark.js +2 -0
- package/dist/ui/non-interactive.js +8 -0
- package/dist/ui/stream.js +30 -0
- package/dist/ui/theme.d.ts +42 -0
- package/dist/ui/theme.js +59 -0
- package/package.json +14 -6
|
@@ -1,15 +1,27 @@
|
|
|
1
|
+
import { isCliError } from "../errors/index.js";
|
|
2
|
+
import { openSync, readSync, closeSync } from "node:fs";
|
|
3
|
+
import { parseResourceIndex, planSelectedResources, downloadSelectedResources, } from "./selected-resources.js";
|
|
4
|
+
import { parseCatalog } from "../catalog/catalog.js";
|
|
5
|
+
import { validateConfigDocument, } from "../project/config.js";
|
|
1
6
|
import { join } from "node:path";
|
|
2
7
|
import { createHash } from "node:crypto";
|
|
3
8
|
import { downloadArtifact, verifyArtifact } from "../project/install.js";
|
|
4
|
-
import { decodeUtf8, extractTarGz } from "../project/tar-gz.js";
|
|
9
|
+
import { decodeUtf8, extractTarGz, ICON_ARCHIVE_MAX_ENTRIES, ICON_ARCHIVE_MAX_EXPANDED_BYTES, } from "../project/tar-gz.js";
|
|
5
10
|
import { catalog as bundledCatalog } from "../catalog/catalog.js";
|
|
6
|
-
import {
|
|
11
|
+
import { cacheArtifact } from "./cache.js";
|
|
12
|
+
import { extractAndVerifyMetadataArchive } from "../metadata/archive.js";
|
|
13
|
+
import { assertLocalCandidateAllowed, cacheKey, FREE_DOWNLOAD_HOSTS, githubReleaseAssetUrl, parseReleaseDescriptor, parseSha256Sidecar, } from "./release-descriptor.js";
|
|
7
14
|
export const DESCRIPTOR_MAX_BYTES = 256 * 1024;
|
|
8
15
|
export const ARTIFACT_MAX_BYTES = 64 * 1024 * 1024;
|
|
9
|
-
export const
|
|
16
|
+
export const METADATA_MAX_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
// GitHub Release downloads can be slow on constrained or distant networks.
|
|
18
|
+
// Keep the operation bounded, but allow enough time for the 64 MiB Free limit.
|
|
19
|
+
export const DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
10
20
|
export const MAX_REDIRECTS = 5;
|
|
11
21
|
export const DESCRIPTOR_NAME = "release-descriptor.json";
|
|
12
22
|
export const DESCRIPTOR_SHA_NAME = "release-descriptor.json.sha256";
|
|
23
|
+
export const RELEASE_LATEST_NAME = "release-latest.json";
|
|
24
|
+
export const MAX_DOWNLOAD_ATTEMPTS = 3;
|
|
13
25
|
function sha256Hex(bytes) {
|
|
14
26
|
return createHash("sha256").update(bytes).digest("hex");
|
|
15
27
|
}
|
|
@@ -38,6 +50,17 @@ function artifactLimits(cliVersion, onProgress, fixtureBaseUrl, timeoutMs = DOWN
|
|
|
38
50
|
...(onProgress ? { onProgress } : {}),
|
|
39
51
|
};
|
|
40
52
|
}
|
|
53
|
+
function metadataLimits(cliVersion, onProgress, fixtureBaseUrl, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
|
|
54
|
+
return {
|
|
55
|
+
maxBytes: METADATA_MAX_BYTES,
|
|
56
|
+
timeoutMs,
|
|
57
|
+
maxRedirects: MAX_REDIRECTS,
|
|
58
|
+
allowedHosts: fixtureBaseUrl ? [new URL(fixtureBaseUrl).host] : FREE_DOWNLOAD_HOSTS,
|
|
59
|
+
...(fixtureBaseUrl ? { allowHttpLoopback: true } : {}),
|
|
60
|
+
userAgent: userAgent(cliVersion),
|
|
61
|
+
...(onProgress ? { onProgress } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
41
64
|
function mapDownloadError(code, message) {
|
|
42
65
|
if (code === "HTTP_ERROR" && / 404$/.test(message)) {
|
|
43
66
|
return { ok: false, reason: "not-found", message };
|
|
@@ -49,12 +72,50 @@ function mapDownloadError(code, message) {
|
|
|
49
72
|
function readFixture(io, name) {
|
|
50
73
|
return io.readFileSync(join(io.fixtureDir ?? "", name));
|
|
51
74
|
}
|
|
75
|
+
function parseLatestDescriptorSha(bytes, sourceVersion) {
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(decodeUtf8(bytes));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new Error("release-latest.json is not valid JSON");
|
|
82
|
+
}
|
|
83
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
84
|
+
throw new Error("release-latest.json must be an object");
|
|
85
|
+
}
|
|
86
|
+
const latest = parsed;
|
|
87
|
+
if (latest.schemaVersion !== 1 ||
|
|
88
|
+
latest.tier !== "free" ||
|
|
89
|
+
latest.fullVersion !== sourceVersion) {
|
|
90
|
+
throw new Error("release-latest.json identity does not match the requested Free release");
|
|
91
|
+
}
|
|
92
|
+
if (typeof latest.descriptorSha256 !== "string" ||
|
|
93
|
+
!/^[a-f0-9]{64}$/i.test(latest.descriptorSha256)) {
|
|
94
|
+
throw new Error("release-latest.json descriptorSha256 must be a 64-character SHA-256 hex digest");
|
|
95
|
+
}
|
|
96
|
+
return latest.descriptorSha256.toLowerCase();
|
|
97
|
+
}
|
|
52
98
|
export function artifactCachePath(cacheDir, fullVersion, sha256) {
|
|
53
99
|
return join(cacheDir, ...cacheKey(fullVersion, sha256).split("/"), "artifact.tgz");
|
|
54
100
|
}
|
|
101
|
+
export function metadataCachePath(cacheDir, fullVersion, sha256) {
|
|
102
|
+
return join(cacheDir, ...cacheKey(fullVersion, sha256).split("/"), "metadata.tgz");
|
|
103
|
+
}
|
|
55
104
|
function cachePath(io, fullVersion, sha256) {
|
|
56
105
|
return artifactCachePath(io.cacheDir, fullVersion, sha256);
|
|
57
106
|
}
|
|
107
|
+
/** Best-effort disk-space preflight; missing statfs support is treated as "enough". */
|
|
108
|
+
export function hasEnoughFreeSpace(io, dir, requiredBytes) {
|
|
109
|
+
try {
|
|
110
|
+
const state = io.statfs?.(dir);
|
|
111
|
+
if (state)
|
|
112
|
+
return state.availableBytes >= requiredBytes;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// statfs unsupported or the dir does not exist yet; assume enough.
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
58
119
|
async function loadBytes(io, filename, limits, tag) {
|
|
59
120
|
if (io.signal.aborted)
|
|
60
121
|
return { ok: false, reason: "cancelled", message: "download cancelled" };
|
|
@@ -63,17 +124,35 @@ async function loadBytes(io, filename, limits, tag) {
|
|
|
63
124
|
return { ok: true, bytes: readFixture(io, filename) };
|
|
64
125
|
}
|
|
65
126
|
catch (error) {
|
|
66
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
reason: "validation",
|
|
130
|
+
message: error instanceof Error ? error.message : String(error),
|
|
131
|
+
};
|
|
67
132
|
}
|
|
68
133
|
}
|
|
69
|
-
const url = io.fixtureBaseUrl
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
134
|
+
const url = io.fixtureBaseUrl
|
|
135
|
+
? new URL(filename, io.fixtureBaseUrl.endsWith("/") ? io.fixtureBaseUrl : `${io.fixtureBaseUrl}/`).toString()
|
|
136
|
+
: githubReleaseAssetUrl(tag, filename);
|
|
137
|
+
for (let attempt = 1; attempt <= MAX_DOWNLOAD_ATTEMPTS; attempt += 1) {
|
|
138
|
+
const result = await downloadArtifact(url, limits, { fetchFn: io.fetchFn, signal: io.signal });
|
|
139
|
+
if (result.ok)
|
|
140
|
+
return { ok: true, bytes: result.bytes };
|
|
141
|
+
if (result.code !== "NETWORK_ERROR" || io.signal.aborted || attempt === MAX_DOWNLOAD_ATTEMPTS) {
|
|
142
|
+
return mapDownloadError(result.code, result.message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
reason: "network",
|
|
148
|
+
message: `download failed after ${MAX_DOWNLOAD_ATTEMPTS} attempts`,
|
|
149
|
+
};
|
|
74
150
|
}
|
|
75
151
|
function catalogFromArchive(artifactBytes, catalogFilename, expectedSha) {
|
|
76
|
-
const unpacked = extractTarGz(artifactBytes, {
|
|
152
|
+
const unpacked = extractTarGz(artifactBytes, {
|
|
153
|
+
maxEntries: ICON_ARCHIVE_MAX_ENTRIES,
|
|
154
|
+
maxExpandedBytes: ICON_ARCHIVE_MAX_EXPANDED_BYTES,
|
|
155
|
+
});
|
|
77
156
|
if (unpacked.errors.length > 0) {
|
|
78
157
|
return { ok: false, reason: "validation", message: unpacked.errors[0] ?? "extract failed" };
|
|
79
158
|
}
|
|
@@ -92,27 +171,130 @@ function catalogFromArchive(artifactBytes, catalogFilename, expectedSha) {
|
|
|
92
171
|
return { ok: true, json: decodeUtf8(catalogBytes) };
|
|
93
172
|
}
|
|
94
173
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
174
|
+
* Download the small metadata archive, verify the whole-archive SHA-256 plus
|
|
175
|
+
* size and every nested file digest, then cache it atomically.
|
|
176
|
+
*/
|
|
177
|
+
export async function downloadMetadataArchive(io, metadataRef, expectedCatalogSha, expectedTier, expectedVersion, tag, allowLocalTest = false) {
|
|
178
|
+
const cachedPath = metadataCachePath(io.cacheDir, expectedVersion, metadataRef.sha256);
|
|
179
|
+
if (io.existsSync(cachedPath)) {
|
|
180
|
+
const cached = io.readFileSync(cachedPath);
|
|
181
|
+
const verified = verifyArtifact(cached, metadataRef.sha256);
|
|
182
|
+
if (verified.ok && cached.byteLength === metadataRef.size) {
|
|
183
|
+
const extracted = extractAndVerifyMetadataArchive(cached, {
|
|
184
|
+
expectedCatalogSha,
|
|
185
|
+
expectedTier,
|
|
186
|
+
expectedVersion,
|
|
187
|
+
perFileDigests: metadataRef.files,
|
|
188
|
+
allowLocalTest,
|
|
189
|
+
});
|
|
190
|
+
if (extracted.kind === "ok") {
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
value: { ...extracted.value, metadataSha256: metadataRef.sha256 },
|
|
194
|
+
cacheHit: true,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!hasEnoughFreeSpace(io, io.cacheDir, metadataRef.size)) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
reason: "disk-full",
|
|
203
|
+
message: `not enough free disk space for ${metadataRef.size} bytes of metadata`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const downloaded = await loadBytes(io, metadataRef.filename, metadataLimits(io.cliVersion, io.onProgress, io.fixtureBaseUrl, io.timeoutMs), tag);
|
|
207
|
+
if (!downloaded.ok) {
|
|
208
|
+
if (!io.fixtureDir && downloaded.reason === "network") {
|
|
209
|
+
return { ok: false, reason: "offline-no-cache", message: downloaded.message };
|
|
210
|
+
}
|
|
211
|
+
return downloaded;
|
|
212
|
+
}
|
|
213
|
+
const verified = verifyArtifact(downloaded.bytes, metadataRef.sha256);
|
|
214
|
+
if (!verified.ok) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
reason: "checksum-mismatch",
|
|
218
|
+
message: `metadata archive SHA-256 mismatch: expected ${metadataRef.sha256}, got ${verified.actual}`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (downloaded.bytes.byteLength !== metadataRef.size) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
reason: "checksum-mismatch",
|
|
225
|
+
message: `metadata archive size mismatch: expected ${metadataRef.size}, got ${downloaded.bytes.byteLength}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const extracted = extractAndVerifyMetadataArchive(downloaded.bytes, {
|
|
229
|
+
expectedCatalogSha,
|
|
230
|
+
expectedTier,
|
|
231
|
+
expectedVersion,
|
|
232
|
+
perFileDigests: metadataRef.files,
|
|
233
|
+
allowLocalTest,
|
|
234
|
+
});
|
|
235
|
+
if (extracted.kind !== "ok") {
|
|
236
|
+
return { ok: false, reason: extracted.reason, message: extracted.message };
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
cacheArtifact(io, cachedPath, downloaded.bytes, metadataRef.sha256);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Cache is best-effort; the verified bytes are still used for this install.
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
value: { ...extracted.value, metadataSha256: metadataRef.sha256 },
|
|
247
|
+
cacheHit: false,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Fetch the published release index → verify descriptor. Older releases that
|
|
252
|
+
* predate release-latest.json fall back to the descriptor SHA sidecar. Does not
|
|
253
|
+
* download the code artifact; used by metadata-only sync and by the full download.
|
|
98
254
|
*/
|
|
99
|
-
export async function
|
|
255
|
+
export async function fetchFreeDescriptor(io, sourceVersion) {
|
|
100
256
|
if (io.signal.aborted)
|
|
101
257
|
return { ok: false, reason: "cancelled", message: "download cancelled" };
|
|
102
258
|
const tag = `v${sourceVersion}`;
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
if (!io.fixtureDir && shaFile.reason === "network") {
|
|
106
|
-
return { ok: false, reason: "offline-no-cache", message: shaFile.message };
|
|
107
|
-
}
|
|
108
|
-
return shaFile;
|
|
109
|
-
}
|
|
259
|
+
const limits = descriptorLimits(io.cliVersion, io.onProgress, io.fixtureBaseUrl, io.timeoutMs);
|
|
260
|
+
const latestFile = await loadBytes(io, RELEASE_LATEST_NAME, limits, tag);
|
|
110
261
|
let expectedDescriptorSha;
|
|
111
|
-
|
|
112
|
-
|
|
262
|
+
if (latestFile.ok) {
|
|
263
|
+
try {
|
|
264
|
+
expectedDescriptorSha = parseLatestDescriptorSha(latestFile.bytes, sourceVersion);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
reason: "validation",
|
|
270
|
+
message: error instanceof Error ? error.message : String(error),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
113
273
|
}
|
|
114
|
-
|
|
115
|
-
|
|
274
|
+
else {
|
|
275
|
+
if (latestFile.reason !== "not-found") {
|
|
276
|
+
if (!io.fixtureDir && latestFile.reason === "network") {
|
|
277
|
+
return { ok: false, reason: "offline-no-cache", message: latestFile.message };
|
|
278
|
+
}
|
|
279
|
+
return latestFile;
|
|
280
|
+
}
|
|
281
|
+
const shaFile = await loadBytes(io, DESCRIPTOR_SHA_NAME, limits, tag);
|
|
282
|
+
if (!shaFile.ok) {
|
|
283
|
+
if (!io.fixtureDir && shaFile.reason === "network") {
|
|
284
|
+
return { ok: false, reason: "offline-no-cache", message: shaFile.message };
|
|
285
|
+
}
|
|
286
|
+
return shaFile;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
expectedDescriptorSha = parseSha256Sidecar(decodeUtf8(shaFile.bytes));
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
reason: "validation",
|
|
295
|
+
message: error instanceof Error ? error.message : String(error),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
116
298
|
}
|
|
117
299
|
const descriptorFile = await loadBytes(io, DESCRIPTOR_NAME, descriptorLimits(io.cliVersion, io.onProgress, io.fixtureBaseUrl, io.timeoutMs), tag);
|
|
118
300
|
if (!descriptorFile.ok)
|
|
@@ -128,9 +310,14 @@ export async function downloadFreeRelease(io, sourceVersion) {
|
|
|
128
310
|
let descriptor;
|
|
129
311
|
try {
|
|
130
312
|
descriptor = parseReleaseDescriptor(descriptorFile.bytes);
|
|
313
|
+
assertLocalCandidateAllowed(descriptor, Boolean(io.fixtureDir));
|
|
131
314
|
}
|
|
132
315
|
catch (error) {
|
|
133
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
reason: "validation",
|
|
319
|
+
message: error instanceof Error ? error.message : String(error),
|
|
320
|
+
};
|
|
134
321
|
}
|
|
135
322
|
if (descriptor.fullVersion !== sourceVersion) {
|
|
136
323
|
return {
|
|
@@ -139,6 +326,164 @@ export async function downloadFreeRelease(io, sourceVersion) {
|
|
|
139
326
|
message: `descriptor fullVersion ${descriptor.fullVersion} does not match catalog sourceVersion ${sourceVersion}`,
|
|
140
327
|
};
|
|
141
328
|
}
|
|
329
|
+
return { ok: true, descriptor, descriptorSha256: actualDescriptorSha, tag };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Fetch descriptor sidecar → verify descriptor → download free.filename from
|
|
333
|
+
* the descriptor (never guessed) → verify archive + nested catalog checksums →
|
|
334
|
+
* download/verify/cache the matching metadata archive.
|
|
335
|
+
*/
|
|
336
|
+
export async function downloadFreeRelease(io, sourceVersion, selection) {
|
|
337
|
+
const fetched = await fetchFreeDescriptor(io, sourceVersion);
|
|
338
|
+
if (!fetched.ok)
|
|
339
|
+
return fetched;
|
|
340
|
+
const { descriptor, descriptorSha256, tag } = fetched;
|
|
341
|
+
const metadataRef = descriptor.free.metadata;
|
|
342
|
+
if (!metadataRef) {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
reason: "validation",
|
|
346
|
+
message: "release descriptor is missing free metadata; the release predates metadata support",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const mode = selection?.config.downloadMode ?? "auto";
|
|
350
|
+
let preflightMetadata;
|
|
351
|
+
if (selection && (mode !== "icons" || descriptor.free.resources)) {
|
|
352
|
+
try {
|
|
353
|
+
const metadata = await downloadMetadataArchive(io, metadataRef, descriptor.catalog.sha256, "free", descriptor.fullVersion, tag, descriptor.channel === "local-test");
|
|
354
|
+
if (!metadata.ok)
|
|
355
|
+
return metadata;
|
|
356
|
+
const strict = validateConfigDocument(selection.document, parseCatalog(JSON.parse(metadata.value.catalogJson)));
|
|
357
|
+
if (strict.kind !== "ok")
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
reason: "validation",
|
|
361
|
+
message: strict.kind === "invalid" ? strict.message : `config state: ${strict.kind}`,
|
|
362
|
+
};
|
|
363
|
+
preflightMetadata = metadata.value;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
reason: io.signal.aborted ? "cancelled" : "validation",
|
|
369
|
+
message: error instanceof Error ? error.message : String(error),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (selection && mode !== "full" && descriptor.free.resources) {
|
|
374
|
+
try {
|
|
375
|
+
const metadata = preflightMetadata
|
|
376
|
+
? { ok: true, value: preflightMetadata, cacheHit: true }
|
|
377
|
+
: await downloadMetadataArchive(io, metadataRef, descriptor.catalog.sha256, "free", descriptor.fullVersion, tag, descriptor.channel === "local-test");
|
|
378
|
+
if (!metadata.ok)
|
|
379
|
+
return metadata;
|
|
380
|
+
const catalog = parseCatalog(JSON.parse(metadata.value.catalogJson));
|
|
381
|
+
const strict = validateConfigDocument(selection.document, catalog);
|
|
382
|
+
if (strict.kind !== "ok")
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
reason: "validation",
|
|
386
|
+
message: strict.kind === "invalid" ? strict.message : `config state: ${strict.kind}`,
|
|
387
|
+
};
|
|
388
|
+
const refs = descriptor.free.resources;
|
|
389
|
+
const indexCache = join(io.cacheDir, "resources", "free", descriptor.fullVersion, refs.index.sha256, "index.json.gz");
|
|
390
|
+
let indexBytes = io.existsSync(indexCache)
|
|
391
|
+
? io.readFileSync(indexCache)
|
|
392
|
+
: undefined;
|
|
393
|
+
if (!indexBytes ||
|
|
394
|
+
indexBytes.length !== refs.index.size ||
|
|
395
|
+
sha256Hex(indexBytes) !== refs.index.sha256) {
|
|
396
|
+
const downloaded = await loadBytes(io, refs.index.filename, {
|
|
397
|
+
...metadataLimits(io.cliVersion, undefined, io.fixtureBaseUrl, io.timeoutMs),
|
|
398
|
+
maxBytes: 8 * 1024 * 1024,
|
|
399
|
+
}, tag);
|
|
400
|
+
if (!downloaded.ok)
|
|
401
|
+
return downloaded;
|
|
402
|
+
indexBytes = downloaded.bytes;
|
|
403
|
+
}
|
|
404
|
+
const index = parseResourceIndex(indexBytes, refs, {
|
|
405
|
+
version: descriptor.fullVersion,
|
|
406
|
+
tier: "free",
|
|
407
|
+
artifactSha256: descriptor.free.sha256,
|
|
408
|
+
});
|
|
409
|
+
const plan = planSelectedResources(strict.config, catalog, index);
|
|
410
|
+
selection.onPlan?.(`Download plan: ${strict.config.target}, ${strict.config.icons.length} registered icons, ${Object.keys(strict.config.themes).length} themes, ${plan.paths.length} resource files, up to ${plan.payloadBytes} compressed bytes (verified cache reduces network traffic).${plan.fallbacks.length ? ` Fallbacks: ${plan.fallbacks.join("; ")}` : ""}`);
|
|
411
|
+
cacheArtifact(io, indexCache, indexBytes, refs.index.sha256);
|
|
412
|
+
const fetched = await downloadSelectedResources(index, refs.index.sha256, plan.paths, {
|
|
413
|
+
io,
|
|
414
|
+
cacheDir: io.cacheDir,
|
|
415
|
+
fetch: io.fetchFn,
|
|
416
|
+
signal: io.signal,
|
|
417
|
+
allowedHosts: io.fixtureBaseUrl ? [new URL(io.fixtureBaseUrl).host] : FREE_DOWNLOAD_HOSTS,
|
|
418
|
+
...(io.fixtureBaseUrl ? { allowLoopback: true } : {}),
|
|
419
|
+
getBundleUrl: () => Promise.resolve(io.fixtureBaseUrl
|
|
420
|
+
? new URL(refs.bundle.filename, io.fixtureBaseUrl.endsWith("/") ? io.fixtureBaseUrl : `${io.fixtureBaseUrl}/`).toString()
|
|
421
|
+
: githubReleaseAssetUrl(tag, refs.bundle.filename)),
|
|
422
|
+
...(io.fixtureDir
|
|
423
|
+
? {
|
|
424
|
+
readRange: (start, size) => {
|
|
425
|
+
const fd = openSync(join(io.fixtureDir, refs.bundle.filename), "r");
|
|
426
|
+
try {
|
|
427
|
+
const bytes = Buffer.alloc(size);
|
|
428
|
+
let read = 0;
|
|
429
|
+
while (read < size) {
|
|
430
|
+
const count = readSync(fd, bytes, read, size - read, start + read);
|
|
431
|
+
if (!count)
|
|
432
|
+
throw new Error("truncated resource bundle");
|
|
433
|
+
read += count;
|
|
434
|
+
}
|
|
435
|
+
return Promise.resolve(bytes);
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
closeSync(fd);
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
}
|
|
442
|
+
: {}),
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
ok: true,
|
|
446
|
+
descriptor,
|
|
447
|
+
descriptorSha256,
|
|
448
|
+
tag,
|
|
449
|
+
artifactBytes: new Uint8Array(),
|
|
450
|
+
cacheHit: fetched.cacheHits === plan.paths.length,
|
|
451
|
+
catalogJson: metadata.value.catalogJson,
|
|
452
|
+
manifestJson: metadata.value.manifestJson,
|
|
453
|
+
manualMd: metadata.value.manualMd,
|
|
454
|
+
metadataSha256: metadata.value.metadataSha256,
|
|
455
|
+
selected: {
|
|
456
|
+
...fetched,
|
|
457
|
+
refs,
|
|
458
|
+
indexBytes,
|
|
459
|
+
payloadBytes: plan.payloadBytes,
|
|
460
|
+
fallbacks: plan.fallbacks,
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
reason: io.signal.aborted || (isCliError(error) && error.code === "CANCELLED")
|
|
468
|
+
? "cancelled"
|
|
469
|
+
: isCliError(error) && error.code === "DISK_FULL"
|
|
470
|
+
? "disk-full"
|
|
471
|
+
: isCliError(error) && error.code === "NETWORK_ERROR"
|
|
472
|
+
? "network"
|
|
473
|
+
: "validation",
|
|
474
|
+
message: error instanceof Error ? error.message : String(error),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (mode === "icons")
|
|
479
|
+
return {
|
|
480
|
+
ok: false,
|
|
481
|
+
reason: "validation",
|
|
482
|
+
message: "this release has no selected-resource index; choose a newer release or explicitly set downloadMode=full",
|
|
483
|
+
};
|
|
484
|
+
selection?.onPlan?.(mode === "full"
|
|
485
|
+
? "Download mode: full (explicitly configured); the complete target archive will be verified before selection."
|
|
486
|
+
: "This release does not advertise selected downloads; auto uses the verified full archive. Set downloadMode=icons to require selected downloads.");
|
|
142
487
|
const artifactCache = cachePath(io, descriptor.fullVersion, descriptor.free.sha256);
|
|
143
488
|
if (io.existsSync(artifactCache)) {
|
|
144
489
|
const cached = io.readFileSync(artifactCache);
|
|
@@ -147,17 +492,32 @@ export async function downloadFreeRelease(io, sourceVersion) {
|
|
|
147
492
|
const catalog = catalogFromArchive(cached, descriptor.catalog.filename, descriptor.catalog.sha256);
|
|
148
493
|
if (!catalog.ok)
|
|
149
494
|
return catalog;
|
|
495
|
+
const metadata = preflightMetadata
|
|
496
|
+
? { ok: true, value: preflightMetadata, cacheHit: true }
|
|
497
|
+
: await downloadMetadataArchive(io, metadataRef, descriptor.catalog.sha256, "free", descriptor.fullVersion, tag, descriptor.channel === "local-test");
|
|
498
|
+
if (!metadata.ok)
|
|
499
|
+
return metadata;
|
|
150
500
|
return {
|
|
151
501
|
ok: true,
|
|
152
502
|
descriptor,
|
|
153
|
-
descriptorSha256
|
|
503
|
+
descriptorSha256,
|
|
154
504
|
artifactBytes: cached,
|
|
155
505
|
catalogJson: catalog.json,
|
|
156
506
|
cacheHit: true,
|
|
157
507
|
tag,
|
|
508
|
+
metadataSha256: metadata.value.metadataSha256,
|
|
509
|
+
manifestJson: metadata.value.manifestJson,
|
|
510
|
+
manualMd: metadata.value.manualMd,
|
|
158
511
|
};
|
|
159
512
|
}
|
|
160
513
|
}
|
|
514
|
+
if (!hasEnoughFreeSpace(io, io.cacheDir, ARTIFACT_MAX_BYTES + METADATA_MAX_BYTES)) {
|
|
515
|
+
return {
|
|
516
|
+
ok: false,
|
|
517
|
+
reason: "disk-full",
|
|
518
|
+
message: "not enough free disk space to cache the icon library",
|
|
519
|
+
};
|
|
520
|
+
}
|
|
161
521
|
const artifact = await loadBytes(io, descriptor.free.filename, artifactLimits(io.cliVersion, io.onProgress, io.fixtureBaseUrl, io.timeoutMs), tag);
|
|
162
522
|
if (!artifact.ok) {
|
|
163
523
|
if (!io.fixtureDir && artifact.reason === "network") {
|
|
@@ -176,16 +536,28 @@ export async function downloadFreeRelease(io, sourceVersion) {
|
|
|
176
536
|
const catalog = catalogFromArchive(artifact.bytes, descriptor.catalog.filename, descriptor.catalog.sha256);
|
|
177
537
|
if (!catalog.ok)
|
|
178
538
|
return catalog;
|
|
179
|
-
|
|
180
|
-
|
|
539
|
+
const metadata = preflightMetadata
|
|
540
|
+
? { ok: true, value: preflightMetadata, cacheHit: true }
|
|
541
|
+
: await downloadMetadataArchive(io, metadataRef, descriptor.catalog.sha256, "free", descriptor.fullVersion, tag, descriptor.channel === "local-test");
|
|
542
|
+
if (!metadata.ok)
|
|
543
|
+
return metadata;
|
|
544
|
+
try {
|
|
545
|
+
cacheArtifact(io, artifactCache, artifact.bytes, descriptor.free.sha256);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
// Cache is best-effort; verified bytes are still used for this install.
|
|
549
|
+
}
|
|
181
550
|
return {
|
|
182
551
|
ok: true,
|
|
183
552
|
descriptor,
|
|
184
|
-
descriptorSha256
|
|
553
|
+
descriptorSha256,
|
|
185
554
|
artifactBytes: artifact.bytes,
|
|
186
555
|
catalogJson: catalog.json,
|
|
187
556
|
cacheHit: false,
|
|
188
557
|
tag,
|
|
558
|
+
metadataSha256: metadata.value.metadataSha256,
|
|
559
|
+
manifestJson: metadata.value.manifestJson,
|
|
560
|
+
manualMd: metadata.value.manualMd,
|
|
189
561
|
};
|
|
190
562
|
}
|
|
191
563
|
export function bundledSourceVersion() {
|
package/dist/core/generate.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type TransactionalFsWithCopy } from "../project/install.js";
|
|
2
|
+
import { type IconCatalog } from "../catalog/catalog.js";
|
|
2
3
|
import type { CommandContext } from "./context.js";
|
|
4
|
+
import { type InstallMetadataParseOptions } from "../project/install-metadata.js";
|
|
3
5
|
import type { Target } from "../commands/parser.js";
|
|
4
6
|
export type GenerateResult = {
|
|
5
7
|
readonly ok: true;
|
|
@@ -12,6 +14,32 @@ export type GenerateResult = {
|
|
|
12
14
|
readonly errors?: readonly string[];
|
|
13
15
|
readonly code?: string;
|
|
14
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* B7A/B5: prefer the catalog written by install / pro-install / library update
|
|
19
|
+
* (`.moeicons/catalog.json`) over the bundled catalog. The installed catalog
|
|
20
|
+
* carries the resourceVersion's real style groups (including migrated
|
|
21
|
+
* `moe-colored` free+pro and bitmap variants), so generate/reconcile must not
|
|
22
|
+
* validate against a stale bundled allowlist. Missing/corrupt catalogs fall
|
|
23
|
+
* back to the bundled catalog; the reconcile path still hash-verifies it.
|
|
24
|
+
*/
|
|
25
|
+
export type InstalledCatalogState = {
|
|
26
|
+
readonly status: "absent";
|
|
27
|
+
} | {
|
|
28
|
+
readonly status: "ok";
|
|
29
|
+
readonly catalog: IconCatalog;
|
|
30
|
+
} | {
|
|
31
|
+
readonly status: "invalid";
|
|
32
|
+
readonly message: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* B7A/B5/DEV-20-02: load the catalog written by install / pro-install / library
|
|
36
|
+
* update and verify it against the install metadata hash. A missing catalog
|
|
37
|
+
* falls back to the bundled contract; a corrupt or drifting one must surface a
|
|
38
|
+
* repair/reinstall error instead of silently switching entitlement semantics.
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadInstalledCatalogState(projectRoot: string, fs_: Pick<TransactionalFsWithCopy, "readFileSync" | "existsSync">, opts?: InstallMetadataParseOptions): InstalledCatalogState;
|
|
41
|
+
/** Backwards-compatible accessor used by tests: returns the catalog or undefined. */
|
|
42
|
+
export declare function loadInstalledCatalog(projectRoot: string, fs_: Pick<TransactionalFsWithCopy, "readFileSync" | "existsSync">): IconCatalog | undefined;
|
|
15
43
|
/**
|
|
16
44
|
* Load installed artifact bytes for generate. Prefers `.moeicons/artifact/<target>/`
|
|
17
45
|
* when that subtree already contains the assets the generator needs (assets target).
|
|
@@ -29,5 +57,6 @@ export declare function runGenerateUseCase(context: CommandContext, fs_: Transac
|
|
|
29
57
|
readonly noTailwind?: boolean;
|
|
30
58
|
readonly target?: Target;
|
|
31
59
|
readonly archiveFiles?: Readonly<Record<string, Uint8Array>>;
|
|
60
|
+
/** Kept for older callers; installed resources always reconcile through metadata. */
|
|
32
61
|
readonly reconcileInstalled?: boolean;
|
|
33
62
|
}): Promise<GenerateResult>;
|