@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,60 +1,69 @@
|
|
|
1
|
-
import { parseResourceVariantId,
|
|
1
|
+
import { assetRelativePath, parseLegacyResourceVariantId, parseResourceVariantId, } from "./resource-variant.js";
|
|
2
2
|
function normalizeArchivePath(rel) {
|
|
3
|
-
return rel.replace(
|
|
3
|
+
return rel.replace(/^\.\//, "");
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Identify a bitmap file
|
|
7
|
-
*
|
|
6
|
+
* Identify a bitmap file at its canonical archive location
|
|
7
|
+
* `assets/<resourceVariantId>/<iconId>.<format>`. v2 uses controlled keyword
|
|
8
|
+
* tokens; v1 uses the explicit legacy `<group>-<format>-<size>` branch.
|
|
8
9
|
*/
|
|
9
|
-
export function matchArchiveBitmapFile(archivePath) {
|
|
10
|
+
export function matchArchiveBitmapFile(archivePath, options = {}) {
|
|
11
|
+
const mediaContractVersion = options.mediaContractVersion ?? 2;
|
|
12
|
+
if (archivePath.includes("\\"))
|
|
13
|
+
return undefined;
|
|
10
14
|
const cleaned = normalizeArchivePath(archivePath);
|
|
11
|
-
const parts = cleaned.split("/")
|
|
12
|
-
if (parts.length
|
|
15
|
+
const parts = cleaned.split("/");
|
|
16
|
+
if (parts.length !== 3 || parts[0] !== "assets")
|
|
13
17
|
return undefined;
|
|
14
|
-
const file = parts[
|
|
18
|
+
const file = parts[2] ?? "";
|
|
15
19
|
const dot = file.lastIndexOf(".");
|
|
16
20
|
if (dot <= 0)
|
|
17
21
|
return undefined;
|
|
18
22
|
const format = file.slice(dot + 1);
|
|
19
23
|
const iconId = file.slice(0, dot);
|
|
20
|
-
if (
|
|
24
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(iconId))
|
|
25
|
+
return undefined;
|
|
26
|
+
try {
|
|
27
|
+
const variant = mediaContractVersion === 1
|
|
28
|
+
? parseLegacyResourceVariantId(parts[1] ?? "")
|
|
29
|
+
: parseResourceVariantId(parts[1] ?? "");
|
|
30
|
+
if (variant.format !== format)
|
|
31
|
+
return undefined;
|
|
32
|
+
return { variant, iconId };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
21
35
|
return undefined;
|
|
22
|
-
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
23
|
-
const segment = parts[i] ?? "";
|
|
24
|
-
try {
|
|
25
|
-
const variant = parseResourceVariantId(segment);
|
|
26
|
-
if (variant.format !== format)
|
|
27
|
-
continue;
|
|
28
|
-
return { variant, iconId };
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
36
|
}
|
|
34
|
-
return undefined;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* Keep only archive files that belong to the requested variants and selected
|
|
38
40
|
* icons. Other sizes/formats stay in the tarball/cache and are not written.
|
|
39
41
|
*/
|
|
40
|
-
export function selectBitmapVariantAssets(archiveFiles, variants, iconIds) {
|
|
42
|
+
export function selectBitmapVariantAssets(archiveFiles, variants, iconIds, options = {}) {
|
|
41
43
|
const wanted = new Map(variants.map((variant) => [variant.resourceVariantId, variant]));
|
|
42
44
|
const iconSet = new Set(iconIds);
|
|
45
|
+
const pairs = options.requestedPairs ?? variants.flatMap((variant) => iconIds.map((iconId) => ({ variantId: variant.resourceVariantId, iconId })));
|
|
46
|
+
const wantedPairs = new Set(pairs.map((pair) => `${pair.variantId}\0${pair.iconId}`));
|
|
43
47
|
const skipped = [];
|
|
44
48
|
const chosen = new Map();
|
|
49
|
+
const errors = [];
|
|
45
50
|
for (const [archivePath, bytes] of Object.entries(archiveFiles)) {
|
|
46
|
-
const matched = matchArchiveBitmapFile(archivePath);
|
|
51
|
+
const matched = matchArchiveBitmapFile(archivePath, options);
|
|
47
52
|
if (!matched)
|
|
48
53
|
continue;
|
|
49
54
|
if (!wanted.has(matched.variant.resourceVariantId)) {
|
|
50
55
|
skipped.push(archivePath);
|
|
51
56
|
continue;
|
|
52
57
|
}
|
|
53
|
-
if (!iconSet.has(matched.iconId)) {
|
|
58
|
+
if (!iconSet.has(matched.iconId) || !wantedPairs.has(`${matched.variant.resourceVariantId}\0${matched.iconId}`)) {
|
|
54
59
|
skipped.push(archivePath);
|
|
55
60
|
continue;
|
|
56
61
|
}
|
|
57
62
|
const destRel = assetRelativePath(matched.variant.resourceVariantId, matched.iconId, matched.variant.format);
|
|
63
|
+
if (chosen.has(destRel)) {
|
|
64
|
+
errors.push(`duplicate bitmap asset for "${destRel}"`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
58
67
|
chosen.set(destRel, {
|
|
59
68
|
destRel,
|
|
60
69
|
bytes,
|
|
@@ -62,13 +71,13 @@ export function selectBitmapVariantAssets(archiveFiles, variants, iconIds) {
|
|
|
62
71
|
iconId: matched.iconId,
|
|
63
72
|
});
|
|
64
73
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
74
|
+
for (const pair of pairs) {
|
|
75
|
+
const variant = wanted.get(pair.variantId);
|
|
76
|
+
if (!variant)
|
|
77
|
+
continue;
|
|
78
|
+
const destRel = assetRelativePath(variant.resourceVariantId, pair.iconId, variant.format);
|
|
79
|
+
if (!chosen.has(destRel)) {
|
|
80
|
+
errors.push(`bitmap asset missing for icon "${pair.iconId}" variant "${variant.resourceVariantId}"`);
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
if (errors.length > 0)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEV-G08 (DEC-101/104/107): frozen bitmap shard resource budget interface.
|
|
3
|
+
*
|
|
4
|
+
* Values are calibrated from the OPS-04-06 real v4 release measurement
|
|
5
|
+
* (release 0.0.17, six bitmap shards):
|
|
6
|
+
* - 512x512/png: 93,843,123 compressed / 94,408,209 expanded (largest)
|
|
7
|
+
* - 256x256/png: 25,539,799 / 25,620,204
|
|
8
|
+
* - 128x128/png: 7,792,540 / 7,828,646
|
|
9
|
+
* - 512x512/webp: 8,668,836 / 8,932,948
|
|
10
|
+
* - 256x256/webp: 3,575,782 / 3,800,040
|
|
11
|
+
* - 128x128/webp: 1,599,168 / 1,829,146
|
|
12
|
+
* The largest single icon payload was 338,828 B and the aggregate six-shard
|
|
13
|
+
* download was 141,019,248 B. `compressedBytes`/`expandedBytes` therefore allow
|
|
14
|
+
* the measured 89.5/90.0 MiB worst case with headroom; `AUD-FIX-30` records the
|
|
15
|
+
* production freeze. No consumer may invent its own limit — every shard
|
|
16
|
+
* download/expand path takes its numbers from here, and the legacy
|
|
17
|
+
* `ICON_ARCHIVE_MAX_EXPANDED_BYTES` 512 MiB aggregate cap is deliberately not a
|
|
18
|
+
* shard budget.
|
|
19
|
+
*/
|
|
20
|
+
export interface BitmapShardBudget {
|
|
21
|
+
/** Largest signed compressed shard archive accepted for one tuple. */
|
|
22
|
+
readonly compressedBytes: number;
|
|
23
|
+
/** Largest expanded (uncompressed) shard archive accepted. */
|
|
24
|
+
readonly expandedBytes: number;
|
|
25
|
+
/** Largest number of archive entries (manifest + icons) in one shard. */
|
|
26
|
+
readonly entries: number;
|
|
27
|
+
/** Largest single uncompressed icon payload accepted. */
|
|
28
|
+
readonly singleFileBytes: number;
|
|
29
|
+
/** Largest aggregate downloaded bytes for one generate/install resolve. */
|
|
30
|
+
readonly totalDownloadBytes: number;
|
|
31
|
+
/** Reserved temporary disk headroom for staging one shard at a time. */
|
|
32
|
+
readonly tempDiskBytes: number;
|
|
33
|
+
/** Frozen concurrency; shards are always fetched one at a time. */
|
|
34
|
+
readonly concurrency: 1;
|
|
35
|
+
/** Per-shard descriptor request timeout. */
|
|
36
|
+
readonly descriptorTimeoutMs: number;
|
|
37
|
+
/** Per-shard payload download timeout. */
|
|
38
|
+
readonly downloadTimeoutMs: number;
|
|
39
|
+
}
|
|
40
|
+
export declare const BITMAP_SHARD_BUDGET: BitmapShardBudget;
|
|
41
|
+
/** Backwards-compatible aliases kept for the frozen shard contract. */
|
|
42
|
+
export declare const BITMAP_SHARD_MAX_ENTRIES: number;
|
|
43
|
+
export declare const BITMAP_SHARD_MAX_EXPANDED_BYTES: number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const BITMAP_SHARD_BUDGET = Object.freeze({
|
|
2
|
+
compressedBytes: 128 * 1024 * 1024,
|
|
3
|
+
expandedBytes: 128 * 1024 * 1024,
|
|
4
|
+
entries: 2000,
|
|
5
|
+
singleFileBytes: 16 * 1024 * 1024,
|
|
6
|
+
totalDownloadBytes: 512 * 1024 * 1024,
|
|
7
|
+
tempDiskBytes: 256 * 1024 * 1024,
|
|
8
|
+
concurrency: 1,
|
|
9
|
+
descriptorTimeoutMs: 5_000,
|
|
10
|
+
// The largest measured production shard is about 94 MiB. A one-minute
|
|
11
|
+
// timeout rejects healthy downloads below roughly 13 Mbit/s.
|
|
12
|
+
downloadTimeoutMs: 120_000,
|
|
13
|
+
});
|
|
14
|
+
/** Backwards-compatible aliases kept for the frozen shard contract. */
|
|
15
|
+
export const BITMAP_SHARD_MAX_ENTRIES = BITMAP_SHARD_BUDGET.entries;
|
|
16
|
+
export const BITMAP_SHARD_MAX_EXPANDED_BYTES = BITMAP_SHARD_BUDGET.expandedBytes;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W2-A/A3: the CLI's global bitmap-shard resource cache.
|
|
3
|
+
*
|
|
4
|
+
* The CLI caches bitmap shards under `<cacheDir>/bitmap-shards/<version>/...`
|
|
5
|
+
* and records exactly what it holds in a manifest (`bitmap-shards/manifest.json`)
|
|
6
|
+
* so a later install can compute what is missing without touching the network.
|
|
7
|
+
* The default warm-up set is the SVG/free package plus every bitmap style
|
|
8
|
+
* group's `128/webp` shard; larger sizes or the `png` format are downloaded
|
|
9
|
+
* on demand (W2-B) when the project config asks for them.
|
|
10
|
+
*/
|
|
11
|
+
import { type CacheIo } from "./cache.js";
|
|
12
|
+
import type { IconCatalog } from "../catalog/catalog.js";
|
|
13
|
+
import { type BitmapShard, type BitmapShardFormat } from "./bitmap-shards.js";
|
|
14
|
+
import { type BitmapShardFetchProgress, type BitmapShardTuple, type FetchBitmapShardsDeps } from "./bitmap-shard-resolver.js";
|
|
15
|
+
export declare const BITMAP_SHARD_CACHE_SCHEMA_VERSION = 1;
|
|
16
|
+
/** W2-A1: the default, always-warmed resource set. */
|
|
17
|
+
export declare const DEFAULT_WARMUP_SIZE = 128;
|
|
18
|
+
export declare const DEFAULT_WARMUP_FORMAT: BitmapShardFormat;
|
|
19
|
+
export interface BitmapShardCacheManifest {
|
|
20
|
+
readonly schemaVersion: 1;
|
|
21
|
+
readonly updatedAt: string;
|
|
22
|
+
readonly shards: readonly BitmapShard[];
|
|
23
|
+
}
|
|
24
|
+
export declare function bitmapShardCacheManifestPath(cacheDir: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Tolerant read: a missing/corrupt manifest is treated as empty (the cache is
|
|
27
|
+
* additive and every cached entry is re-verified before use), never a crash.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readBitmapShardCacheManifest(cacheDir: string, io: CacheIo): BitmapShardCacheManifest;
|
|
30
|
+
/** Atomic, idempotent manifest write (verified bytes are staged then renamed). */
|
|
31
|
+
export declare function writeBitmapShardCacheManifest(cacheDir: string, io: CacheIo, shards: readonly BitmapShard[], now?: number): void;
|
|
32
|
+
/** Merge new pins over an existing manifest, keeping one entry per tuple@version. */
|
|
33
|
+
export declare function mergeBitmapShardCacheManifest(manifest: BitmapShardCacheManifest, pins: readonly BitmapShard[]): BitmapShard[];
|
|
34
|
+
/** W2-B2: tuples with no cached entry for the exact resource version. */
|
|
35
|
+
export declare function missingBitmapShardTuples(manifest: BitmapShardCacheManifest, expected: readonly BitmapShardTuple[], version: string): BitmapShardTuple[];
|
|
36
|
+
/**
|
|
37
|
+
* W2-A1: every bitmap style group's `128/webp` shard, in canonical order.
|
|
38
|
+
* Groups that do not ship the default variant are skipped (the config
|
|
39
|
+
* validator reports that explicitly when a user asks for them).
|
|
40
|
+
*/
|
|
41
|
+
export declare function defaultWarmupTuples(catalog: IconCatalog): BitmapShardTuple[];
|
|
42
|
+
export interface WarmupPlan {
|
|
43
|
+
readonly tuples: readonly BitmapShardTuple[];
|
|
44
|
+
readonly missing: readonly BitmapShardTuple[];
|
|
45
|
+
readonly reused: number;
|
|
46
|
+
}
|
|
47
|
+
export interface WarmDefaultBitmapShardsDeps extends Omit<FetchBitmapShardsDeps, "existingPins"> {
|
|
48
|
+
readonly catalog: IconCatalog;
|
|
49
|
+
readonly cacheDir: string;
|
|
50
|
+
readonly io: CacheIo;
|
|
51
|
+
readonly now?: number;
|
|
52
|
+
/** Emitted once before any network request so the UI can print the plan. */
|
|
53
|
+
readonly onPlan?: (plan: WarmupPlan) => void;
|
|
54
|
+
/** W2-B6: per-tuple/byte progress forwarded from the shard fetch. */
|
|
55
|
+
readonly onProgress?: (event: BitmapShardFetchProgress) => void;
|
|
56
|
+
}
|
|
57
|
+
export interface WarmDefaultBitmapShardsResult {
|
|
58
|
+
readonly tuples: readonly BitmapShardTuple[];
|
|
59
|
+
readonly pins: readonly BitmapShard[];
|
|
60
|
+
readonly downloaded: number;
|
|
61
|
+
readonly reused: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* W2-A2/B7: warm the default set into the cache. Already-cached shards are
|
|
65
|
+
* re-verified and reused without a network call; only missing tuples are
|
|
66
|
+
* fetched. The manifest is updated atomically after a fully verified fetch.
|
|
67
|
+
*/
|
|
68
|
+
export declare function warmDefaultBitmapShards(deps: WarmDefaultBitmapShardsDeps): Promise<WarmDefaultBitmapShardsResult>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W2-A/A3: the CLI's global bitmap-shard resource cache.
|
|
3
|
+
*
|
|
4
|
+
* The CLI caches bitmap shards under `<cacheDir>/bitmap-shards/<version>/...`
|
|
5
|
+
* and records exactly what it holds in a manifest (`bitmap-shards/manifest.json`)
|
|
6
|
+
* so a later install can compute what is missing without touching the network.
|
|
7
|
+
* The default warm-up set is the SVG/free package plus every bitmap style
|
|
8
|
+
* group's `128/webp` shard; larger sizes or the `png` format are downloaded
|
|
9
|
+
* on demand (W2-B) when the project config asks for them.
|
|
10
|
+
*/
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { cacheArtifact } from "./cache.js";
|
|
13
|
+
import { isVariantAvailable } from "../generator/theme-resolve.js";
|
|
14
|
+
import { buildResourceVariantId } from "./resource-variant.js";
|
|
15
|
+
import { parseBitmapShard, sortBitmapShards, } from "./bitmap-shards.js";
|
|
16
|
+
import { fetchSelectedBitmapShards, } from "./bitmap-shard-resolver.js";
|
|
17
|
+
export const BITMAP_SHARD_CACHE_SCHEMA_VERSION = 1;
|
|
18
|
+
/** W2-A1: the default, always-warmed resource set. */
|
|
19
|
+
export const DEFAULT_WARMUP_SIZE = 128;
|
|
20
|
+
export const DEFAULT_WARMUP_FORMAT = "webp";
|
|
21
|
+
export function bitmapShardCacheManifestPath(cacheDir) {
|
|
22
|
+
return join(cacheDir, "bitmap-shards", "manifest.json");
|
|
23
|
+
}
|
|
24
|
+
function emptyManifest() {
|
|
25
|
+
return { schemaVersion: BITMAP_SHARD_CACHE_SCHEMA_VERSION, updatedAt: new Date(0).toISOString(), shards: [] };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Tolerant read: a missing/corrupt manifest is treated as empty (the cache is
|
|
29
|
+
* additive and every cached entry is re-verified before use), never a crash.
|
|
30
|
+
*/
|
|
31
|
+
export function readBitmapShardCacheManifest(cacheDir, io) {
|
|
32
|
+
const path = bitmapShardCacheManifestPath(cacheDir);
|
|
33
|
+
if (!io.existsSync(path) || !io.readFileSync)
|
|
34
|
+
return emptyManifest();
|
|
35
|
+
try {
|
|
36
|
+
const raw = JSON.parse(Buffer.from(io.readFileSync(path)).toString("utf8"));
|
|
37
|
+
if (raw.schemaVersion !== BITMAP_SHARD_CACHE_SCHEMA_VERSION || !Array.isArray(raw.shards))
|
|
38
|
+
return emptyManifest();
|
|
39
|
+
const shards = raw.shards.map(parseBitmapShard);
|
|
40
|
+
return {
|
|
41
|
+
schemaVersion: BITMAP_SHARD_CACHE_SCHEMA_VERSION,
|
|
42
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : new Date(0).toISOString(),
|
|
43
|
+
shards: sortBitmapShards(shards),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return emptyManifest();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Atomic, idempotent manifest write (verified bytes are staged then renamed). */
|
|
51
|
+
export function writeBitmapShardCacheManifest(cacheDir, io, shards, now = Date.now()) {
|
|
52
|
+
const body = `${JSON.stringify({ schemaVersion: BITMAP_SHARD_CACHE_SCHEMA_VERSION, updatedAt: new Date(now).toISOString(), shards: sortBitmapShards(shards) }, null, 2)}\n`;
|
|
53
|
+
cacheArtifact(io, bitmapShardCacheManifestPath(cacheDir), Buffer.from(body, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
function tupleKey(shard) {
|
|
56
|
+
return `${shard.resourceVersion}/${shard.styleGroupId}/${shard.imageSize.width}x${shard.imageSize.height}/${shard.format}`;
|
|
57
|
+
}
|
|
58
|
+
/** Merge new pins over an existing manifest, keeping one entry per tuple@version. */
|
|
59
|
+
export function mergeBitmapShardCacheManifest(manifest, pins) {
|
|
60
|
+
const byKey = new Map();
|
|
61
|
+
for (const shard of [...manifest.shards, ...pins])
|
|
62
|
+
byKey.set(tupleKey(shard), shard);
|
|
63
|
+
return sortBitmapShards([...byKey.values()]);
|
|
64
|
+
}
|
|
65
|
+
/** W2-B2: tuples with no cached entry for the exact resource version. */
|
|
66
|
+
export function missingBitmapShardTuples(manifest, expected, version) {
|
|
67
|
+
const have = new Set(manifest.shards
|
|
68
|
+
.filter((shard) => shard.resourceVersion === version)
|
|
69
|
+
.map((shard) => tupleKey({ ...shard, resourceVersion: version })));
|
|
70
|
+
return expected.filter((tuple) => {
|
|
71
|
+
const key = `${version}/${tuple.styleGroupId}/${tuple.imageSize.width}x${tuple.imageSize.height}/${tuple.format}`;
|
|
72
|
+
return !have.has(key);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* W2-A1: every bitmap style group's `128/webp` shard, in canonical order.
|
|
77
|
+
* Groups that do not ship the default variant are skipped (the config
|
|
78
|
+
* validator reports that explicitly when a user asks for them).
|
|
79
|
+
*/
|
|
80
|
+
export function defaultWarmupTuples(catalog) {
|
|
81
|
+
const imageSize = { width: DEFAULT_WARMUP_SIZE, height: DEFAULT_WARMUP_SIZE };
|
|
82
|
+
const tuples = [];
|
|
83
|
+
for (const group of catalog.styleGroups) {
|
|
84
|
+
if (group.type !== "bitmap")
|
|
85
|
+
continue;
|
|
86
|
+
const variantId = buildResourceVariantId(group.id, DEFAULT_WARMUP_FORMAT, DEFAULT_WARMUP_SIZE);
|
|
87
|
+
if (!isVariantAvailable(group, variantId))
|
|
88
|
+
continue;
|
|
89
|
+
tuples.push({ styleGroupId: group.id, imageSize, format: DEFAULT_WARMUP_FORMAT });
|
|
90
|
+
}
|
|
91
|
+
tuples.sort((a, b) => (a.styleGroupId < b.styleGroupId ? -1 : a.styleGroupId > b.styleGroupId ? 1 : 0));
|
|
92
|
+
return tuples;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* W2-A2/B7: warm the default set into the cache. Already-cached shards are
|
|
96
|
+
* re-verified and reused without a network call; only missing tuples are
|
|
97
|
+
* fetched. The manifest is updated atomically after a fully verified fetch.
|
|
98
|
+
*/
|
|
99
|
+
export async function warmDefaultBitmapShards(deps) {
|
|
100
|
+
const tuples = defaultWarmupTuples(deps.catalog);
|
|
101
|
+
const manifest = readBitmapShardCacheManifest(deps.cacheDir, deps.io);
|
|
102
|
+
const missing = missingBitmapShardTuples(manifest, tuples, deps.version);
|
|
103
|
+
deps.onPlan?.({ tuples, missing, reused: tuples.length - missing.length });
|
|
104
|
+
if (tuples.length === 0)
|
|
105
|
+
return { tuples, pins: [], downloaded: 0, reused: 0 };
|
|
106
|
+
const existingPins = manifest.shards.filter((shard) => shard.resourceVersion === deps.version);
|
|
107
|
+
const fetched = await fetchSelectedBitmapShards(tuples, { ...deps, existingPins });
|
|
108
|
+
writeBitmapShardCacheManifest(deps.cacheDir, deps.io, mergeBitmapShardCacheManifest(manifest, fetched.pins), deps.now ?? Date.now());
|
|
109
|
+
return {
|
|
110
|
+
tuples,
|
|
111
|
+
pins: fetched.pins,
|
|
112
|
+
downloaded: fetched.shards.length,
|
|
113
|
+
reused: fetched.pins.length - fetched.shards.length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BITMAP-SHARD-V1 (DEC-97..DEC-108): CLI shard descriptor fetch, direct-from-R2
|
|
3
|
+
* download, strict verification and atomic cached persistence.
|
|
4
|
+
*
|
|
5
|
+
* The bearer is only ever sent to the trusted API origin. The R2 shard download
|
|
6
|
+
* never forwards authorization/cookies (`downloadSignedArtifact`).
|
|
7
|
+
*
|
|
8
|
+
* Budget constants here are interim; `DEV-G08`/`OPS-05-05` freeze the final
|
|
9
|
+
* per-shard download/expand budgets after real measurement.
|
|
10
|
+
*/
|
|
11
|
+
import { type CacheIo } from "./cache.js";
|
|
12
|
+
import { type SignedArtifactDescriptor } from "./signed-artifact.js";
|
|
13
|
+
import { type BitmapShard, type BitmapShardFormat, type BitmapShardImageSize } from "./bitmap-shards.js";
|
|
14
|
+
import { BITMAP_SHARD_MAX_ENTRIES, BITMAP_SHARD_MAX_EXPANDED_BYTES } from "./bitmap-shard-budget.js";
|
|
15
|
+
export { BITMAP_SHARD_MAX_ENTRIES, BITMAP_SHARD_MAX_EXPANDED_BYTES };
|
|
16
|
+
export declare const BITMAP_SHARD_DESCRIPTOR_URL = "https://api.moeicons.com/v1/icon-library/pro/bitmap-shard-descriptor";
|
|
17
|
+
/**
|
|
18
|
+
* Only the fields that identify the immutable shard are required to verify
|
|
19
|
+
* cached bytes; a full descriptor additionally carries a signed URL/expiry.
|
|
20
|
+
*/
|
|
21
|
+
export interface BitmapShardVerificationTarget {
|
|
22
|
+
readonly version: string;
|
|
23
|
+
readonly styleGroupId: string;
|
|
24
|
+
readonly imageSize: BitmapShardImageSize;
|
|
25
|
+
readonly format: BitmapShardFormat;
|
|
26
|
+
readonly size: number;
|
|
27
|
+
readonly sha256: string;
|
|
28
|
+
readonly manifestSha256: string;
|
|
29
|
+
}
|
|
30
|
+
export interface BitmapShardCacheIdentity {
|
|
31
|
+
readonly version: string;
|
|
32
|
+
readonly styleGroupId: string;
|
|
33
|
+
readonly imageSize: BitmapShardImageSize;
|
|
34
|
+
readonly format: BitmapShardFormat;
|
|
35
|
+
readonly sha256: string;
|
|
36
|
+
}
|
|
37
|
+
export interface BitmapShardDescriptor extends SignedArtifactDescriptor, BitmapShardVerificationTarget {
|
|
38
|
+
readonly tier: "pro";
|
|
39
|
+
readonly descriptorSha256: string;
|
|
40
|
+
readonly filename: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Test seam: `MOEICONS_BITMAP_SHARD_DESCRIPTOR_URL` overrides the authenticated
|
|
44
|
+
* per-shard descriptor endpoint. Only https or loopback http is accepted, so a
|
|
45
|
+
* production endpoint can never be redirected by a stray environment variable
|
|
46
|
+
* to a plaintext or untrusted origin (mirrors the Pro descriptor seam).
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveBitmapShardDescriptorEndpoint(env: Readonly<Record<string, string | undefined>>): {
|
|
49
|
+
readonly url: string;
|
|
50
|
+
readonly allowLoopback: boolean;
|
|
51
|
+
};
|
|
52
|
+
export declare function parseBitmapShardDescriptor(value: unknown, now: number, options?: {
|
|
53
|
+
readonly allowLoopback?: boolean;
|
|
54
|
+
}): BitmapShardDescriptor;
|
|
55
|
+
/**
|
|
56
|
+
* POST one tuple to the trusted API. Bearer is never sent to R2. The request is
|
|
57
|
+
* bounded by `BITMAP_SHARD_BUDGET.descriptorTimeoutMs` unless overridden, and
|
|
58
|
+
* honors an external cancellation signal (DEV-G08).
|
|
59
|
+
*/
|
|
60
|
+
export declare function fetchBitmapShardDescriptor(tuple: {
|
|
61
|
+
readonly version: string;
|
|
62
|
+
readonly descriptorSha256: string;
|
|
63
|
+
readonly styleGroupId: string;
|
|
64
|
+
readonly imageSize: BitmapShardImageSize;
|
|
65
|
+
readonly format: BitmapShardFormat;
|
|
66
|
+
}, accessToken: string, deps?: {
|
|
67
|
+
readonly fetch?: typeof fetch;
|
|
68
|
+
readonly now?: number;
|
|
69
|
+
readonly allowLoopback?: boolean;
|
|
70
|
+
readonly endpoint?: string;
|
|
71
|
+
readonly timeoutMs?: number;
|
|
72
|
+
readonly signal?: AbortSignal;
|
|
73
|
+
}): Promise<BitmapShardDescriptor>;
|
|
74
|
+
export interface VerifiedBitmapShard {
|
|
75
|
+
readonly descriptor: BitmapShardDescriptor;
|
|
76
|
+
/** `icons/<iconId>.<format>` -> bytes. */
|
|
77
|
+
readonly files: Readonly<Record<string, Uint8Array>>;
|
|
78
|
+
readonly iconIds: readonly string[];
|
|
79
|
+
}
|
|
80
|
+
/** An offline-verified shard loaded from the cache; the URL/expiry is irrelevant. */
|
|
81
|
+
export interface CachedBitmapShard {
|
|
82
|
+
readonly target: BitmapShardVerificationTarget;
|
|
83
|
+
readonly files: Readonly<Record<string, Uint8Array>>;
|
|
84
|
+
readonly iconIds: readonly string[];
|
|
85
|
+
}
|
|
86
|
+
/** DEC-101 verification of downloaded shard bytes against the full descriptor. */
|
|
87
|
+
export declare function verifyBitmapShardArchive(bytes: Uint8Array, descriptor: BitmapShardVerificationTarget): VerifiedBitmapShard;
|
|
88
|
+
/** Unambiguous cache identity (DEC-101). */
|
|
89
|
+
export declare function bitmapShardCacheKey(identity: BitmapShardCacheIdentity): string;
|
|
90
|
+
export declare function bitmapShardCachePath(cacheDir: string, identity: BitmapShardCacheIdentity): string;
|
|
91
|
+
/**
|
|
92
|
+
* Offline/pin path: load one shard from the local cache and re-verify its
|
|
93
|
+
* identity, size, SHA, manifest digest, icon set and dimensions. A missing or
|
|
94
|
+
* partial cache fails closed; a poisoned cache fails digest verification.
|
|
95
|
+
*/
|
|
96
|
+
export declare function loadCachedBitmapShard(target: BitmapShardVerificationTarget, cacheDir: string, io: CacheIo): CachedBitmapShard;
|
|
97
|
+
/** Reconstruct the offline verification identity pinned in install metadata. */
|
|
98
|
+
export declare function bitmapShardVerificationTargetFromPin(pin: {
|
|
99
|
+
readonly resourceVersion: string;
|
|
100
|
+
readonly styleGroupId: string;
|
|
101
|
+
readonly imageSize: BitmapShardImageSize;
|
|
102
|
+
readonly format: BitmapShardFormat;
|
|
103
|
+
readonly compressedSize: number;
|
|
104
|
+
readonly sha256: string;
|
|
105
|
+
readonly manifestSha256: string;
|
|
106
|
+
}): BitmapShardVerificationTarget;
|
|
107
|
+
/** Build the canonical pin identity from a freshly verified download. */
|
|
108
|
+
export declare function bitmapShardPinFromVerified(verified: VerifiedBitmapShard): BitmapShard;
|
|
109
|
+
/**
|
|
110
|
+
* Download directly from the private R2 signed host and atomically cache the
|
|
111
|
+
* verified archive. Any size/SHA/manifest/dimension drift fails closed.
|
|
112
|
+
*/
|
|
113
|
+
export declare function downloadAndCacheBitmapShard(descriptor: BitmapShardDescriptor, deps: {
|
|
114
|
+
readonly cacheDir: string;
|
|
115
|
+
readonly io: CacheIo;
|
|
116
|
+
readonly fetch?: typeof fetch;
|
|
117
|
+
readonly allowedHosts?: readonly string[];
|
|
118
|
+
readonly now?: number;
|
|
119
|
+
readonly allowLoopback?: boolean;
|
|
120
|
+
readonly signal?: AbortSignal;
|
|
121
|
+
readonly statfs?: (dir: string) => {
|
|
122
|
+
readonly availableBytes: number;
|
|
123
|
+
} | undefined;
|
|
124
|
+
readonly timeoutMs?: number;
|
|
125
|
+
/** W2-B6: byte-level progress for a single shard payload. */
|
|
126
|
+
readonly onProgress?: (event: {
|
|
127
|
+
readonly downloadedBytes: number;
|
|
128
|
+
readonly totalBytes?: number;
|
|
129
|
+
}) => void;
|
|
130
|
+
}): Promise<VerifiedBitmapShard>;
|