@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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEV-G07 (DEC-100/101/107): normalize configured bitmap references to canonical
|
|
3
|
+
* tuples, fetch exactly those shards (one descriptor each, bearer only to the
|
|
4
|
+
* API), and map verified shard bytes back to the existing local `assets/`
|
|
5
|
+
* layout so generated code/imports never change.
|
|
6
|
+
*/
|
|
7
|
+
import { catalog as defaultCatalog } from "../catalog/catalog.js";
|
|
8
|
+
import { findCatalogStyleGroup } from "../catalog/catalog.js";
|
|
9
|
+
import { isVariantAvailable } from "../generator/theme-resolve.js";
|
|
10
|
+
import { buildResourceVariantId, DEFAULT_BITMAP_FORMAT, DEFAULT_BITMAP_SIZE, resolveResourceVariant, } from "./resource-variant.js";
|
|
11
|
+
import { bitmapShardSetSha256, buildBitmapShardFilename, buildBitmapShardObjectKey, } from "./bitmap-shards.js";
|
|
12
|
+
import { bitmapShardPinFromVerified, bitmapShardVerificationTargetFromPin, downloadAndCacheBitmapShard, fetchBitmapShardDescriptor, loadCachedBitmapShard, resolveBitmapShardDescriptorEndpoint, } from "./bitmap-shard-download.js";
|
|
13
|
+
import { BITMAP_SHARD_BUDGET } from "./bitmap-shard-budget.js";
|
|
14
|
+
import { CliError } from "../errors/index.js";
|
|
15
|
+
import { themeHasIcon } from "./icon-selection.js";
|
|
16
|
+
function tupleKey(tuple) {
|
|
17
|
+
return `${tuple.styleGroupId}/${tuple.imageSize.width}x${tuple.imageSize.height}/${tuple.format}`;
|
|
18
|
+
}
|
|
19
|
+
function compareTuples(a, b) {
|
|
20
|
+
if (a.styleGroupId !== b.styleGroupId)
|
|
21
|
+
return a.styleGroupId < b.styleGroupId ? -1 : 1;
|
|
22
|
+
if (a.imageSize.width !== b.imageSize.width)
|
|
23
|
+
return a.imageSize.width - b.imageSize.width;
|
|
24
|
+
if (a.imageSize.height !== b.imageSize.height)
|
|
25
|
+
return a.imageSize.height - b.imageSize.height;
|
|
26
|
+
if (a.format !== b.format)
|
|
27
|
+
return a.format < b.format ? -1 : 1;
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve every bitmap variant the config actually references into unique,
|
|
32
|
+
* canonically sorted tuples. Same tuple used by many icons/themes appears once.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveBitmapTuples(config, sourceCatalog = defaultCatalog) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const tuples = [];
|
|
38
|
+
for (const [theme, entry] of Object.entries(config.themes)) {
|
|
39
|
+
const group = findCatalogStyleGroup(entry.styleGroup, sourceCatalog);
|
|
40
|
+
// Non-bitmap and unknown groups are out of scope here; install/generate
|
|
41
|
+
// validate theme/catalog consistency separately.
|
|
42
|
+
if (!group || group.type !== "bitmap")
|
|
43
|
+
continue;
|
|
44
|
+
if (!config.icons.some((iconId) => themeHasIcon(config, sourceCatalog, theme, iconId)))
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
let variant;
|
|
48
|
+
if (entry.format === undefined && entry.imageSize === undefined) {
|
|
49
|
+
const defaultId = buildResourceVariantId(group.id, DEFAULT_BITMAP_FORMAT, DEFAULT_BITMAP_SIZE);
|
|
50
|
+
if (!isVariantAvailable(group, defaultId)) {
|
|
51
|
+
errors.push(`bitmap group "${group.id}" does not include the default ${DEFAULT_BITMAP_FORMAT}/${String(DEFAULT_BITMAP_SIZE)} variant; set format and imageSize explicitly`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
variant = resolveResourceVariant(group.id);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
variant = resolveResourceVariant(group.id, {
|
|
58
|
+
...(entry.format !== undefined ? { format: entry.format } : {}),
|
|
59
|
+
...(entry.imageSize !== undefined ? { imageSize: entry.imageSize } : {}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (!isVariantAvailable(group, variant.resourceVariantId)) {
|
|
63
|
+
errors.push(`variant ${variant.resourceVariantId} is unavailable for ${group.id}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const tuple = {
|
|
67
|
+
styleGroupId: variant.styleGroupId,
|
|
68
|
+
imageSize: { width: variant.imageSize, height: variant.imageSize },
|
|
69
|
+
format: variant.format,
|
|
70
|
+
};
|
|
71
|
+
const key = tupleKey(tuple);
|
|
72
|
+
if (seen.has(key))
|
|
73
|
+
continue;
|
|
74
|
+
seen.add(key);
|
|
75
|
+
tuples.push(tuple);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
errors.push(`theme "${theme}": ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (errors.length > 0)
|
|
82
|
+
return { ok: false, errors };
|
|
83
|
+
tuples.sort(compareTuples);
|
|
84
|
+
return { ok: true, tuples };
|
|
85
|
+
}
|
|
86
|
+
/** Pure canonical request plan (one immutable key per selected tuple). */
|
|
87
|
+
export function planBitmapShardRequests(tuples, version) {
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
const requests = [];
|
|
90
|
+
for (const tuple of tuples) {
|
|
91
|
+
const key = tupleKey(tuple);
|
|
92
|
+
if (seen.has(key))
|
|
93
|
+
throw new Error(`duplicate bitmap tuple in plan: ${key}`);
|
|
94
|
+
seen.add(key);
|
|
95
|
+
const filename = buildBitmapShardFilename({ ...tuple, tier: "pro", resourceVersion: version });
|
|
96
|
+
requests.push({
|
|
97
|
+
...tuple,
|
|
98
|
+
filename,
|
|
99
|
+
objectKey: buildBitmapShardObjectKey({ ...tuple, tier: "pro", resourceVersion: version, filename }),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
requests.sort(compareTuples);
|
|
103
|
+
return requests;
|
|
104
|
+
}
|
|
105
|
+
/** Map verified shard bytes to the canonical local `assets/` layout. */
|
|
106
|
+
export function shardAssetsToLocalLayout(shard) {
|
|
107
|
+
const variantId = buildResourceVariantId(shard.descriptor.styleGroupId, shard.descriptor.format, shard.descriptor.imageSize.width);
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [path, bytes] of Object.entries(shard.files)) {
|
|
110
|
+
const iconId = path.slice("icons/".length, path.length - (shard.descriptor.format.length + 1));
|
|
111
|
+
out[`assets/${variantId}/${iconId}.${shard.descriptor.format}`] = bytes;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Fetch exactly the selected shards, sequentially (concurrency 1 per DEC-102/
|
|
117
|
+
* 107), verify each, and return the merged local-layout file map. Never
|
|
118
|
+
* prefetches unselected tuples/formats/sizes.
|
|
119
|
+
*/
|
|
120
|
+
export async function fetchSelectedBitmapShards(tuples, deps) {
|
|
121
|
+
const requests = planBitmapShardRequests(tuples, deps.version);
|
|
122
|
+
const shards = [];
|
|
123
|
+
const pins = [];
|
|
124
|
+
const files = {};
|
|
125
|
+
const existingByTuple = new Map();
|
|
126
|
+
for (const pin of deps.existingPins ?? []) {
|
|
127
|
+
existingByTuple.set(tupleKey({ styleGroupId: pin.styleGroupId, imageSize: pin.imageSize, format: pin.format }), pin);
|
|
128
|
+
}
|
|
129
|
+
let downloadedBytes = 0;
|
|
130
|
+
for (const [offset, request] of requests.entries()) {
|
|
131
|
+
const index = offset + 1;
|
|
132
|
+
const total = requests.length;
|
|
133
|
+
const tuple = { styleGroupId: request.styleGroupId, imageSize: request.imageSize, format: request.format };
|
|
134
|
+
const published = existingByTuple.get(tupleKey(request));
|
|
135
|
+
if (published) {
|
|
136
|
+
// Re-verify the cached pinned shard; a miss/poison falls through to fetch.
|
|
137
|
+
try {
|
|
138
|
+
const cached = loadPinnedBitmapShardAssets([published], [{ styleGroupId: request.styleGroupId, imageSize: request.imageSize, format: request.format }], deps.cacheDir, deps.io);
|
|
139
|
+
Object.assign(files, cached.files);
|
|
140
|
+
pins.push(cached.pins[0]);
|
|
141
|
+
deps.onProgress?.({ tuple, index, total, phase: "reuse" });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Fall through: re-fetch this tuple online.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
deps.onProgress?.({ tuple, index, total, phase: "descriptor" });
|
|
149
|
+
const descriptor = await fetchBitmapShardDescriptor({
|
|
150
|
+
version: deps.version,
|
|
151
|
+
descriptorSha256: deps.descriptorSha256,
|
|
152
|
+
styleGroupId: request.styleGroupId,
|
|
153
|
+
imageSize: request.imageSize,
|
|
154
|
+
format: request.format,
|
|
155
|
+
}, deps.accessToken, {
|
|
156
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
157
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
158
|
+
...(deps.allowLoopback ? { allowLoopback: true } : {}),
|
|
159
|
+
...(deps.endpoint ? { endpoint: deps.endpoint } : {}),
|
|
160
|
+
...(deps.signal ? { signal: deps.signal } : {}),
|
|
161
|
+
});
|
|
162
|
+
// The descriptor must describe exactly the requested tuple.
|
|
163
|
+
if (descriptor.filename !== request.filename) {
|
|
164
|
+
throw new Error(`bitmap shard descriptor ${descriptor.filename} does not match request ${request.filename}`);
|
|
165
|
+
}
|
|
166
|
+
const verified = await downloadAndCacheBitmapShard(descriptor, {
|
|
167
|
+
cacheDir: deps.cacheDir,
|
|
168
|
+
io: deps.io,
|
|
169
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
170
|
+
...(deps.allowedHosts ? { allowedHosts: deps.allowedHosts } : {}),
|
|
171
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
172
|
+
...(deps.allowLoopback ? { allowLoopback: true } : {}),
|
|
173
|
+
...(deps.signal ? { signal: deps.signal } : {}),
|
|
174
|
+
...(deps.statfs ? { statfs: deps.statfs } : {}),
|
|
175
|
+
onProgress: ({ downloadedBytes: shardBytes, totalBytes }) => deps.onProgress?.({ tuple, index, total, phase: "download", downloadedBytes: shardBytes, ...(totalBytes !== undefined ? { totalBytes } : {}) }),
|
|
176
|
+
});
|
|
177
|
+
deps.onProgress?.({ tuple, index, total, phase: "done", downloadedBytes: descriptor.size, totalBytes: descriptor.size });
|
|
178
|
+
downloadedBytes += verified.descriptor.size;
|
|
179
|
+
if (downloadedBytes > BITMAP_SHARD_BUDGET.totalDownloadBytes) {
|
|
180
|
+
throw new CliError("VALIDATION_ERROR", `bitmap shard total download exceeds the budget (${BITMAP_SHARD_BUDGET.totalDownloadBytes})`);
|
|
181
|
+
}
|
|
182
|
+
Object.assign(files, shardAssetsToLocalLayout(verified));
|
|
183
|
+
shards.push(verified);
|
|
184
|
+
pins.push(bitmapShardPinFromVerified(verified));
|
|
185
|
+
}
|
|
186
|
+
return { requests, shards, pins, files };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* DEV-G07: resolve the config's bitmap tuples, then obtain each selected shard.
|
|
190
|
+
* Online (an access token is available) fetches one descriptor per tuple from
|
|
191
|
+
* the API and downloads it from the signed R2 host. Offline (no token) loads
|
|
192
|
+
* the already-pinned cache and fails closed when any shard is missing.
|
|
193
|
+
*/
|
|
194
|
+
export async function resolveConfiguredBitmapShards(deps) {
|
|
195
|
+
const resolved = resolveBitmapTuples(deps.config, deps.catalog);
|
|
196
|
+
if (!resolved.ok)
|
|
197
|
+
throw new CliError("VALIDATION_ERROR", resolved.errors.join("; "));
|
|
198
|
+
const tuples = resolved.tuples;
|
|
199
|
+
if (tuples.length === 0) {
|
|
200
|
+
return { tuples, requests: [], pins: [], files: {}, bitmapShardSetSha256: bitmapShardSetSha256([]) };
|
|
201
|
+
}
|
|
202
|
+
const requests = planBitmapShardRequests(tuples, deps.version);
|
|
203
|
+
const endpoint = resolveBitmapShardDescriptorEndpoint(deps.env ?? {});
|
|
204
|
+
const fetched = await fetchSelectedBitmapShards(tuples, {
|
|
205
|
+
version: deps.version,
|
|
206
|
+
descriptorSha256: deps.descriptorSha256,
|
|
207
|
+
accessToken: deps.accessToken,
|
|
208
|
+
cacheDir: deps.cacheDir,
|
|
209
|
+
io: deps.io,
|
|
210
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
211
|
+
...(deps.allowedHosts ? { allowedHosts: deps.allowedHosts } : {}),
|
|
212
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
213
|
+
...(endpoint.allowLoopback ? { allowLoopback: true } : {}),
|
|
214
|
+
endpoint: endpoint.url,
|
|
215
|
+
...(deps.signal ? { signal: deps.signal } : {}),
|
|
216
|
+
...(deps.statfs ? { statfs: deps.statfs } : {}),
|
|
217
|
+
...(deps.existingPins ? { existingPins: deps.existingPins } : {}),
|
|
218
|
+
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
tuples,
|
|
222
|
+
requests,
|
|
223
|
+
pins: fetched.pins,
|
|
224
|
+
files: fetched.files,
|
|
225
|
+
bitmapShardSetSha256: bitmapShardSetSha256(fetched.pins),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Offline path: re-verify every configured tuple's shard against its pinned
|
|
230
|
+
* identity. The pin supplies the archive SHA, manifest SHA and size, so a
|
|
231
|
+
* truncated/poisoned cache can never be accepted, and every unselected tuple
|
|
232
|
+
* is never read.
|
|
233
|
+
*/
|
|
234
|
+
export function loadPinnedBitmapShardAssets(pins, selectedTuples, cacheDir, io) {
|
|
235
|
+
const wanted = new Map(selectedTuples.map((tuple) => [tupleKey(tuple), tuple]));
|
|
236
|
+
const files = {};
|
|
237
|
+
const loaded = [];
|
|
238
|
+
for (const pin of pins) {
|
|
239
|
+
const key = tupleKey({
|
|
240
|
+
styleGroupId: pin.styleGroupId,
|
|
241
|
+
imageSize: pin.imageSize,
|
|
242
|
+
format: pin.format,
|
|
243
|
+
});
|
|
244
|
+
if (selectedTuples.length > 0 && !wanted.has(key))
|
|
245
|
+
continue;
|
|
246
|
+
const target = bitmapShardVerificationTargetFromPin(pin);
|
|
247
|
+
const cached = loadCachedBitmapShard(target, cacheDir, io);
|
|
248
|
+
const variantId = buildResourceVariantId(pin.styleGroupId, pin.format, pin.imageSize.width);
|
|
249
|
+
for (const [path, bytes] of Object.entries(cached.files)) {
|
|
250
|
+
const iconId = path.slice("icons/".length, path.length - (pin.format.length + 1));
|
|
251
|
+
files[`assets/${variantId}/${iconId}.${pin.format}`] = bytes;
|
|
252
|
+
}
|
|
253
|
+
loaded.push(pin);
|
|
254
|
+
}
|
|
255
|
+
if (selectedTuples.length > 0 && loaded.length !== selectedTuples.length) {
|
|
256
|
+
const missing = selectedTuples
|
|
257
|
+
.filter((tuple) => !loaded.some((pin) => tupleKey({ styleGroupId: pin.styleGroupId, imageSize: pin.imageSize, format: pin.format }) === tupleKey(tuple)))
|
|
258
|
+
.map((tuple) => tupleKey(tuple));
|
|
259
|
+
throw new CliError("VALIDATION_ERROR", `bitmap shards are not all cached: ${missing.join(", ")}; run install while online`);
|
|
260
|
+
}
|
|
261
|
+
return { files, pins: loaded, bitmapShardSetSha256: bitmapShardSetSha256(loaded) };
|
|
262
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BITMAP-SHARD-V1 (DEC-97..DEC-108): CLI-side identities for the frozen bitmap
|
|
3
|
+
* shard contract. Filename/key/canonical-JSON rules must match
|
|
4
|
+
* `docs/contracts/bitmap-shard-v1.md` and the code-library producer exactly.
|
|
5
|
+
*/
|
|
6
|
+
export declare const BITMAP_SHARD_SCHEMA_VERSION = 1;
|
|
7
|
+
export type BitmapShardTier = "free" | "pro";
|
|
8
|
+
export type BitmapShardFormat = "png" | "webp";
|
|
9
|
+
export interface BitmapShardImageSize {
|
|
10
|
+
readonly width: number;
|
|
11
|
+
readonly height: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BitmapShard {
|
|
14
|
+
readonly schemaVersion: number;
|
|
15
|
+
readonly resourceVersion: string;
|
|
16
|
+
readonly tier: BitmapShardTier;
|
|
17
|
+
readonly styleGroupId: string;
|
|
18
|
+
readonly imageSize: BitmapShardImageSize;
|
|
19
|
+
readonly format: BitmapShardFormat;
|
|
20
|
+
readonly filename: string;
|
|
21
|
+
readonly objectKey: string;
|
|
22
|
+
readonly compressedSize: number;
|
|
23
|
+
readonly expandedSize: number;
|
|
24
|
+
readonly sha256: string;
|
|
25
|
+
readonly fileCount: number;
|
|
26
|
+
readonly manifestSha256: string;
|
|
27
|
+
}
|
|
28
|
+
export interface BitmapShardTuple {
|
|
29
|
+
readonly tier: BitmapShardTier;
|
|
30
|
+
readonly styleGroupId: string;
|
|
31
|
+
readonly imageSize: BitmapShardImageSize;
|
|
32
|
+
readonly format: BitmapShardFormat;
|
|
33
|
+
}
|
|
34
|
+
export declare function canonicalJson(value: unknown): string;
|
|
35
|
+
export declare function sha256Hex(input: string | Uint8Array): string;
|
|
36
|
+
export declare function buildBitmapShardFilename(input: {
|
|
37
|
+
tier: BitmapShardTier;
|
|
38
|
+
styleGroupId: string;
|
|
39
|
+
imageSize: BitmapShardImageSize;
|
|
40
|
+
format: BitmapShardFormat;
|
|
41
|
+
resourceVersion: string;
|
|
42
|
+
}): string;
|
|
43
|
+
export declare function parseBitmapShardFilename(filename: string): (BitmapShardTuple & {
|
|
44
|
+
readonly resourceVersion: string;
|
|
45
|
+
}) | undefined;
|
|
46
|
+
export declare function buildBitmapShardObjectKey(input: {
|
|
47
|
+
tier: BitmapShardTier;
|
|
48
|
+
styleGroupId: string;
|
|
49
|
+
imageSize: BitmapShardImageSize;
|
|
50
|
+
format: BitmapShardFormat;
|
|
51
|
+
resourceVersion: string;
|
|
52
|
+
filename?: string;
|
|
53
|
+
}): string;
|
|
54
|
+
export declare function sortBitmapShards(shards: readonly BitmapShard[]): BitmapShard[];
|
|
55
|
+
export declare function bitmapShardIdentity(shard: BitmapShard): Record<string, unknown>;
|
|
56
|
+
export declare function bitmapShardSetSha256(shards: readonly BitmapShard[]): string;
|
|
57
|
+
export declare function computeBitmapShardSetSha256(shards: readonly BitmapShard[]): string;
|
|
58
|
+
/** Fail-closed parse of one descriptor shard entry. */
|
|
59
|
+
export declare function parseBitmapShard(raw: unknown): BitmapShard;
|
|
60
|
+
export interface BitmapShardSetValidation {
|
|
61
|
+
readonly shards: readonly BitmapShard[];
|
|
62
|
+
readonly bitmapShardSetSha256: string;
|
|
63
|
+
}
|
|
64
|
+
/** Validate uniqueness + canonical order; verify the complete-set digest. */
|
|
65
|
+
export declare function validateBitmapShardSet(shards: readonly BitmapShard[], options?: {
|
|
66
|
+
readonly resourceVersion?: string;
|
|
67
|
+
readonly styleGroupIds?: Iterable<string>;
|
|
68
|
+
}): BitmapShardSetValidation;
|
|
69
|
+
/** Verify a descriptor's declared set digest against the actual shard list. */
|
|
70
|
+
export declare function assertBitmapShardSetSha256(shards: readonly BitmapShard[], expected: string): string;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BITMAP-SHARD-V1 (DEC-97..DEC-108): CLI-side identities for the frozen bitmap
|
|
3
|
+
* shard contract. Filename/key/canonical-JSON rules must match
|
|
4
|
+
* `docs/contracts/bitmap-shard-v1.md` and the code-library producer exactly.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
export const BITMAP_SHARD_SCHEMA_VERSION = 1;
|
|
8
|
+
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
9
|
+
const GROUP_RE = /^moe-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const RESOURCE_VERSION_RE = /^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta))?$/;
|
|
11
|
+
const BITMAP_SIZES = new Set([64, 128, 256, 512]);
|
|
12
|
+
const IDENTITY_FIELDS = [
|
|
13
|
+
"tier",
|
|
14
|
+
"styleGroupId",
|
|
15
|
+
"imageSize",
|
|
16
|
+
"format",
|
|
17
|
+
"filename",
|
|
18
|
+
"objectKey",
|
|
19
|
+
"compressedSize",
|
|
20
|
+
"expandedSize",
|
|
21
|
+
"sha256",
|
|
22
|
+
"fileCount",
|
|
23
|
+
"manifestSha256",
|
|
24
|
+
];
|
|
25
|
+
function isRecord(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
function fail(message) {
|
|
29
|
+
throw new Error(message);
|
|
30
|
+
}
|
|
31
|
+
/** Canonical JSON per bitmap-shard-v1.md section 4. */
|
|
32
|
+
function canonicalize(value) {
|
|
33
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") {
|
|
34
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
35
|
+
fail("canonicalJson: non-finite number");
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "string")
|
|
39
|
+
return value.normalize("NFC");
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return value.map(canonicalize);
|
|
42
|
+
if (isRecord(value)) {
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const key of Object.keys(value).sort()) {
|
|
45
|
+
if (value[key] === undefined)
|
|
46
|
+
fail(`canonicalJson: undefined value at ${key}`);
|
|
47
|
+
out[key] = canonicalize(value[key]);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
return fail(`canonicalJson: unsupported type ${typeof value}`);
|
|
52
|
+
}
|
|
53
|
+
export function canonicalJson(value) {
|
|
54
|
+
return JSON.stringify(canonicalize(value));
|
|
55
|
+
}
|
|
56
|
+
export function sha256Hex(input) {
|
|
57
|
+
return createHash("sha256").update(input).digest("hex");
|
|
58
|
+
}
|
|
59
|
+
function normalizeTuple(input) {
|
|
60
|
+
const tier = input.tier;
|
|
61
|
+
const styleGroupId = input.styleGroupId;
|
|
62
|
+
const format = input.format;
|
|
63
|
+
const size = input.imageSize;
|
|
64
|
+
if (tier !== "free" && tier !== "pro")
|
|
65
|
+
fail(`invalid shard tier: ${String(tier)}`);
|
|
66
|
+
if (typeof styleGroupId !== "string" || !GROUP_RE.test(styleGroupId))
|
|
67
|
+
fail(`invalid styleGroupId: ${String(styleGroupId)}`);
|
|
68
|
+
if (format !== "png" && format !== "webp")
|
|
69
|
+
fail(`invalid bitmap format: ${String(format)}`);
|
|
70
|
+
if (!size
|
|
71
|
+
|| !Number.isInteger(size.width)
|
|
72
|
+
|| !Number.isInteger(size.height)
|
|
73
|
+
|| size.width !== size.height
|
|
74
|
+
|| !BITMAP_SIZES.has(size.width)) {
|
|
75
|
+
fail(`invalid bitmap imageSize for ${String(styleGroupId)}`);
|
|
76
|
+
}
|
|
77
|
+
return { tier, styleGroupId, width: size.width, height: size.height, format };
|
|
78
|
+
}
|
|
79
|
+
function assertResourceVersion(resourceVersion) {
|
|
80
|
+
if (!RESOURCE_VERSION_RE.test(resourceVersion))
|
|
81
|
+
fail(`invalid resourceVersion: ${String(resourceVersion)}`);
|
|
82
|
+
}
|
|
83
|
+
export function buildBitmapShardFilename(input) {
|
|
84
|
+
const tuple = normalizeTuple(input);
|
|
85
|
+
assertResourceVersion(input.resourceVersion);
|
|
86
|
+
return `moe-icons-bitmap-${tuple.tier}-${tuple.styleGroupId}-${tuple.width}x${tuple.height}-${tuple.format}-${input.resourceVersion}.tgz`;
|
|
87
|
+
}
|
|
88
|
+
export function parseBitmapShardFilename(filename) {
|
|
89
|
+
const match = /^moe-icons-bitmap-(free|pro)-(.+)-(\d{1,4})x(\d{1,4})-(png|webp)-(\d+\.\d+\.\d+(?:-(?:alpha|beta))?)\.tgz$/.exec(filename);
|
|
90
|
+
if (!match)
|
|
91
|
+
return undefined;
|
|
92
|
+
const [, tier, styleGroupId, width, height, format, resourceVersion] = match;
|
|
93
|
+
if (!GROUP_RE.test(styleGroupId ?? "") || !RESOURCE_VERSION_RE.test(resourceVersion ?? ""))
|
|
94
|
+
return undefined;
|
|
95
|
+
const widthNumber = Number(width);
|
|
96
|
+
const heightNumber = Number(height);
|
|
97
|
+
if (widthNumber !== heightNumber || !BITMAP_SIZES.has(widthNumber))
|
|
98
|
+
return undefined;
|
|
99
|
+
const rebuilt = buildBitmapShardFilename({
|
|
100
|
+
tier: tier,
|
|
101
|
+
styleGroupId: styleGroupId,
|
|
102
|
+
imageSize: { width: widthNumber, height: heightNumber },
|
|
103
|
+
format: format,
|
|
104
|
+
resourceVersion: resourceVersion,
|
|
105
|
+
});
|
|
106
|
+
if (rebuilt !== filename)
|
|
107
|
+
return undefined;
|
|
108
|
+
return {
|
|
109
|
+
tier: tier,
|
|
110
|
+
styleGroupId: styleGroupId,
|
|
111
|
+
imageSize: { width: widthNumber, height: heightNumber },
|
|
112
|
+
format: format,
|
|
113
|
+
resourceVersion: resourceVersion,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function buildBitmapShardObjectKey(input) {
|
|
117
|
+
const tuple = normalizeTuple(input);
|
|
118
|
+
assertResourceVersion(input.resourceVersion);
|
|
119
|
+
const canonical = buildBitmapShardFilename({
|
|
120
|
+
tier: tuple.tier,
|
|
121
|
+
styleGroupId: tuple.styleGroupId,
|
|
122
|
+
imageSize: { width: tuple.width, height: tuple.height },
|
|
123
|
+
format: tuple.format,
|
|
124
|
+
resourceVersion: input.resourceVersion,
|
|
125
|
+
});
|
|
126
|
+
const name = input.filename ?? canonical;
|
|
127
|
+
if (name !== canonical)
|
|
128
|
+
fail("shard filename does not match its tuple");
|
|
129
|
+
return `moeicons/v1/releases/${input.resourceVersion}/${tuple.tier}/bitmap-shards/${tuple.styleGroupId}/${tuple.width}x${tuple.height}/${tuple.format}/${name}`;
|
|
130
|
+
}
|
|
131
|
+
function compareShards(a, b) {
|
|
132
|
+
if (a.styleGroupId !== b.styleGroupId)
|
|
133
|
+
return a.styleGroupId < b.styleGroupId ? -1 : 1;
|
|
134
|
+
if (a.imageSize.width !== b.imageSize.width)
|
|
135
|
+
return a.imageSize.width - b.imageSize.width;
|
|
136
|
+
if (a.imageSize.height !== b.imageSize.height)
|
|
137
|
+
return a.imageSize.height - b.imageSize.height;
|
|
138
|
+
if (a.format !== b.format)
|
|
139
|
+
return a.format < b.format ? -1 : 1;
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
export function sortBitmapShards(shards) {
|
|
143
|
+
return [...shards].sort(compareShards);
|
|
144
|
+
}
|
|
145
|
+
export function bitmapShardIdentity(shard) {
|
|
146
|
+
const out = {};
|
|
147
|
+
for (const field of IDENTITY_FIELDS)
|
|
148
|
+
out[field] = shard[field];
|
|
149
|
+
return canonicalize(out);
|
|
150
|
+
}
|
|
151
|
+
export function bitmapShardSetSha256(shards) {
|
|
152
|
+
return sha256Hex(canonicalJson(sortBitmapShards(shards).map(bitmapShardIdentity)));
|
|
153
|
+
}
|
|
154
|
+
export function computeBitmapShardSetSha256(shards) {
|
|
155
|
+
return bitmapShardSetSha256(shards);
|
|
156
|
+
}
|
|
157
|
+
/** Fail-closed parse of one descriptor shard entry. */
|
|
158
|
+
export function parseBitmapShard(raw) {
|
|
159
|
+
if (!isRecord(raw))
|
|
160
|
+
fail("bitmapShard must be an object");
|
|
161
|
+
const allowed = ["schemaVersion", "resourceVersion", ...IDENTITY_FIELDS];
|
|
162
|
+
const extra = Object.keys(raw).filter((key) => !allowed.includes(key));
|
|
163
|
+
if (extra.length)
|
|
164
|
+
fail(`bitmapShard has unknown field(s): ${extra.join(", ")}`);
|
|
165
|
+
if (raw.schemaVersion !== BITMAP_SHARD_SCHEMA_VERSION)
|
|
166
|
+
fail("bitmapShard schemaVersion must be 1");
|
|
167
|
+
const resourceVersion = raw.resourceVersion;
|
|
168
|
+
if (typeof resourceVersion !== "string")
|
|
169
|
+
fail("bitmapShard resourceVersion required");
|
|
170
|
+
assertResourceVersion(resourceVersion);
|
|
171
|
+
const tuple = normalizeTuple({
|
|
172
|
+
tier: raw.tier,
|
|
173
|
+
styleGroupId: raw.styleGroupId,
|
|
174
|
+
imageSize: raw.imageSize,
|
|
175
|
+
format: raw.format,
|
|
176
|
+
});
|
|
177
|
+
const expectedFilename = buildBitmapShardFilename({
|
|
178
|
+
tier: tuple.tier,
|
|
179
|
+
styleGroupId: tuple.styleGroupId,
|
|
180
|
+
imageSize: { width: tuple.width, height: tuple.height },
|
|
181
|
+
format: tuple.format,
|
|
182
|
+
resourceVersion,
|
|
183
|
+
});
|
|
184
|
+
if (raw.filename !== expectedFilename)
|
|
185
|
+
fail(`bitmapShard filename is not canonical: ${String(raw.filename)}`);
|
|
186
|
+
const expectedKey = buildBitmapShardObjectKey({
|
|
187
|
+
tier: tuple.tier,
|
|
188
|
+
styleGroupId: tuple.styleGroupId,
|
|
189
|
+
imageSize: { width: tuple.width, height: tuple.height },
|
|
190
|
+
format: tuple.format,
|
|
191
|
+
resourceVersion,
|
|
192
|
+
filename: raw.filename,
|
|
193
|
+
});
|
|
194
|
+
if (raw.objectKey !== expectedKey)
|
|
195
|
+
fail(`bitmapShard objectKey is not canonical: ${String(raw.objectKey)}`);
|
|
196
|
+
if (typeof raw.sha256 !== "string" || !SHA256_RE.test(raw.sha256))
|
|
197
|
+
fail("bitmapShard sha256 must be 64 lowercase hex");
|
|
198
|
+
if (typeof raw.manifestSha256 !== "string" || !SHA256_RE.test(raw.manifestSha256)) {
|
|
199
|
+
fail("bitmapShard manifestSha256 must be 64 lowercase hex");
|
|
200
|
+
}
|
|
201
|
+
for (const field of ["compressedSize", "expandedSize", "fileCount"]) {
|
|
202
|
+
const value = raw[field];
|
|
203
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
204
|
+
fail(`bitmapShard ${field} must be a positive integer`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return raw;
|
|
208
|
+
}
|
|
209
|
+
/** Validate uniqueness + canonical order; verify the complete-set digest. */
|
|
210
|
+
export function validateBitmapShardSet(shards, options = {}) {
|
|
211
|
+
const seen = new Set();
|
|
212
|
+
for (const shard of shards) {
|
|
213
|
+
if (options.resourceVersion !== undefined && shard.resourceVersion !== options.resourceVersion) {
|
|
214
|
+
fail("bitmapShard resourceVersion mismatch");
|
|
215
|
+
}
|
|
216
|
+
const key = `${shard.tier}/${shard.styleGroupId}/${shard.imageSize.width}x${shard.imageSize.height}/${shard.format}`;
|
|
217
|
+
if (seen.has(key))
|
|
218
|
+
fail(`duplicate bitmap shard tuple: ${key}`);
|
|
219
|
+
seen.add(key);
|
|
220
|
+
}
|
|
221
|
+
const sorted = sortBitmapShards(shards);
|
|
222
|
+
for (let index = 0; index < shards.length; index += 1) {
|
|
223
|
+
const a = shards[index];
|
|
224
|
+
const b = sorted[index];
|
|
225
|
+
if (a.styleGroupId !== b.styleGroupId
|
|
226
|
+
|| a.imageSize.width !== b.imageSize.width
|
|
227
|
+
|| a.imageSize.height !== b.imageSize.height
|
|
228
|
+
|| a.format !== b.format) {
|
|
229
|
+
fail(`bitmapShards is not in canonical order at index ${index}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (options.styleGroupIds) {
|
|
233
|
+
const declared = new Set(options.styleGroupIds);
|
|
234
|
+
for (const shard of shards) {
|
|
235
|
+
if (!declared.has(shard.styleGroupId))
|
|
236
|
+
fail(`bitmap shard for unknown style group: ${shard.styleGroupId}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { shards: sorted, bitmapShardSetSha256: bitmapShardSetSha256(shards) };
|
|
240
|
+
}
|
|
241
|
+
/** Verify a descriptor's declared set digest against the actual shard list. */
|
|
242
|
+
export function assertBitmapShardSetSha256(shards, expected) {
|
|
243
|
+
if (!SHA256_RE.test(expected))
|
|
244
|
+
fail("bitmapShardSetSha256 must be 64 lowercase hex");
|
|
245
|
+
const actual = bitmapShardSetSha256(shards);
|
|
246
|
+
if (actual !== expected)
|
|
247
|
+
fail(`bitmapShardSetSha256 mismatch: expected ${expected}, got ${actual}`);
|
|
248
|
+
return actual;
|
|
249
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { TransactionalFs } from "../project/install.js";
|
|
2
|
+
import type { CommandContext } from "./context.js";
|
|
3
|
+
import type { FreeDownloadIo } from "./free-download.js";
|
|
4
|
+
export interface BootstrapFs {
|
|
5
|
+
readonly existsSync: (path: string) => boolean;
|
|
6
|
+
readonly readFileSync: (path: string) => string;
|
|
7
|
+
readonly writeFileSync: (path: string, content: string) => void;
|
|
8
|
+
readonly mkdirSync: (path: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface BootstrapDeps {
|
|
11
|
+
readonly fs: BootstrapFs;
|
|
12
|
+
readonly installFs: TransactionalFs;
|
|
13
|
+
readonly free: Omit<FreeDownloadIo, "signal">;
|
|
14
|
+
readonly cliVersion: string;
|
|
15
|
+
}
|
|
16
|
+
export type BootstrapResult = {
|
|
17
|
+
readonly kind: "skipped";
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: "already";
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: "installed";
|
|
23
|
+
readonly version: string;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "failed";
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly retry: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function bootstrapMarkerPath(env: Readonly<Record<string, string | undefined>>): string;
|
|
30
|
+
/**
|
|
31
|
+
* First-run bootstrap: on an interactive first start inside a project, install
|
|
32
|
+
* Free code + Free metadata without an extra metadata prompt. The marker is
|
|
33
|
+
* written only after a fully successful install (or when a valid free install
|
|
34
|
+
* already exists), so a half-completed run never suppresses retry. A declined
|
|
35
|
+
* prompt writes a `declined` marker so the CLI does not nag on every start.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runBootstrapUseCase(context: CommandContext, deps: BootstrapDeps): Promise<BootstrapResult>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { detectProject } from "../project/detect.js";
|
|
4
|
+
import { readInstalledResourceState } from "../project/install-metadata.js";
|
|
5
|
+
import { runInstallUseCase } from "./install.js";
|
|
6
|
+
export function bootstrapMarkerPath(env) {
|
|
7
|
+
return (env.MOEICONS_BOOTSTRAP_FILE ??
|
|
8
|
+
join(env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache"), "bootstrap.json"));
|
|
9
|
+
}
|
|
10
|
+
function writeMarker(deps, env, content) {
|
|
11
|
+
const marker = bootstrapMarkerPath(env);
|
|
12
|
+
const body = `${JSON.stringify({ schemaVersion: 1, ...content }, null, 2)}\n`;
|
|
13
|
+
const parent = marker.replace(/[\\/][^\\/]*$/, "") || "/";
|
|
14
|
+
deps.fs.mkdirSync(parent);
|
|
15
|
+
deps.fs.writeFileSync(marker, body);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* First-run bootstrap: on an interactive first start inside a project, install
|
|
19
|
+
* Free code + Free metadata without an extra metadata prompt. The marker is
|
|
20
|
+
* written only after a fully successful install (or when a valid free install
|
|
21
|
+
* already exists), so a half-completed run never suppresses retry. A declined
|
|
22
|
+
* prompt writes a `declined` marker so the CLI does not nag on every start.
|
|
23
|
+
*/
|
|
24
|
+
export async function runBootstrapUseCase(context, deps) {
|
|
25
|
+
if (!context.ui.confirm)
|
|
26
|
+
return { kind: "skipped", reason: "non-interactive" };
|
|
27
|
+
const marker = bootstrapMarkerPath(context.env);
|
|
28
|
+
if (deps.fs.existsSync(marker))
|
|
29
|
+
return { kind: "already" };
|
|
30
|
+
const project = detectProject(context.cwd);
|
|
31
|
+
if (!project)
|
|
32
|
+
return { kind: "skipped", reason: "no-project" };
|
|
33
|
+
const state = readInstalledResourceState(project.root, "free");
|
|
34
|
+
if (state.kind === "ok" &&
|
|
35
|
+
deps.fs.existsSync(join(project.root, ".moeicons", "MANUAL.md")) &&
|
|
36
|
+
deps.fs.existsSync(join(project.root, ".moeicons", "manifest.json"))) {
|
|
37
|
+
writeMarker(deps, context.env, { cliVersion: deps.cliVersion, libraryVersion: state.metadata.artifactVersion, completedAt: new Date().toISOString() });
|
|
38
|
+
return { kind: "already" };
|
|
39
|
+
}
|
|
40
|
+
const confirmed = await context.ui.confirm("Set up moeicons Free icons in this project now? (recommended)", context.signal);
|
|
41
|
+
if (confirmed !== true) {
|
|
42
|
+
writeMarker(deps, context.env, { cliVersion: deps.cliVersion, declinedAt: new Date().toISOString() });
|
|
43
|
+
return { kind: "skipped", reason: "declined" };
|
|
44
|
+
}
|
|
45
|
+
const result = await runInstallUseCase(context, { fs: deps.installFs, download: deps.free }, {});
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
if (result.reason === "no-project" || result.reason === "cancelled") {
|
|
48
|
+
return { kind: "skipped", reason: result.reason };
|
|
49
|
+
}
|
|
50
|
+
return { kind: "failed", message: result.message, retry: "moeicons install free" };
|
|
51
|
+
}
|
|
52
|
+
writeMarker(deps, context.env, { cliVersion: deps.cliVersion, libraryVersion: result.artifactVersion, completedAt: new Date().toISOString() });
|
|
53
|
+
return { kind: "installed", version: result.artifactVersion };
|
|
54
|
+
}
|