@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
package/dist/core/pro-install.js
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
|
+
import { assertDownloadSelection } from "./selected-resources.js";
|
|
1
2
|
import { detectProject } from "../project/detect.js";
|
|
2
|
-
import {
|
|
3
|
-
import { createInstallPlan, executeInstallPlan } from "../project/install.js";
|
|
4
|
-
import { serializeInstallMetadata, sha256Bytes } from "../project/install-metadata.js";
|
|
3
|
+
import { loadConfigDocument, validateConfigDocument } from "../project/config.js";
|
|
4
|
+
import { createInstallPlan, executeInstallPlan, } from "../project/install.js";
|
|
5
|
+
import { parseInstallMetadata, serializeInstallMetadata, sha256Bytes, } from "../project/install-metadata.js";
|
|
5
6
|
import { withProjectLock } from "../project/project-lock.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { runAccessTokenUseCase } from "./auth.js";
|
|
8
|
+
import { downloadProArtifact, PRO_DOWNLOAD_HOSTS, resolveProDescriptorEndpoint, } from "./pro-download.js";
|
|
9
|
+
import { artifactCachePath, metadataCachePath } from "./free-download.js";
|
|
10
|
+
import { configuredComponentFiles, selectTargetSubtree, computeSubtreeHash, } from "./target-subtree.js";
|
|
11
|
+
import { typesReexport } from "./install.js";
|
|
9
12
|
import { CliError } from "../errors/index.js";
|
|
13
|
+
import { catalog as bundledCatalog, parseCatalog } from "../catalog/catalog.js";
|
|
14
|
+
import { resolveBitmapTuples, resolveConfiguredBitmapShards } from "./bitmap-shard-resolver.js";
|
|
15
|
+
import { mergeBitmapShardCacheManifest, readBitmapShardCacheManifest, writeBitmapShardCacheManifest, } from "./bitmap-shard-cache.js";
|
|
16
|
+
import { allowLocalTestFromEnv } from "./local-test-env.js";
|
|
17
|
+
function toCacheIo(fs_) {
|
|
18
|
+
return {
|
|
19
|
+
// Shard cache keys are nested several levels deep; the CacheIo contract
|
|
20
|
+
// requires recursive creation.
|
|
21
|
+
mkdirSync: (path) => {
|
|
22
|
+
fs_.mkdirSync(path, { recursive: true });
|
|
23
|
+
},
|
|
24
|
+
writeFileSync: fs_.writeFileSync,
|
|
25
|
+
renameSync: fs_.renameSync,
|
|
26
|
+
existsSync: fs_.existsSync,
|
|
27
|
+
rmSync: fs_.rmSync,
|
|
28
|
+
...(fs_.readFileSync ? { readFileSync: fs_.readFileSync } : {}),
|
|
29
|
+
...(fs_.readdirSync ? { readdirSync: fs_.readdirSync } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
10
32
|
import { homedir } from "node:os";
|
|
11
33
|
import { join } from "node:path";
|
|
12
34
|
function resolveCacheDir(env) {
|
|
13
35
|
return env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
|
|
14
36
|
}
|
|
15
37
|
/** Persist the full verified archive so vanilla/bitmap generate can read assets/. */
|
|
16
|
-
function cacheVerifiedArtifact(fs_, cacheDir, version, sha256, bytes) {
|
|
17
|
-
const cached =
|
|
38
|
+
function cacheVerifiedArtifact(fs_, cacheDir, version, sha256, bytes, kind = "code") {
|
|
39
|
+
const cached = kind === "metadata"
|
|
40
|
+
? metadataCachePath(cacheDir, version, sha256)
|
|
41
|
+
: artifactCachePath(cacheDir, version, sha256);
|
|
18
42
|
if (fs_.existsSync(cached))
|
|
19
43
|
return;
|
|
20
44
|
fs_.mkdirSync(join(cached, ".."), { recursive: true });
|
|
@@ -24,28 +48,133 @@ export async function runProInstallUseCase(context, deps, expected) {
|
|
|
24
48
|
const project = detectProject(context.cwd);
|
|
25
49
|
if (!project)
|
|
26
50
|
throw new CliError("VALIDATION_ERROR", "no package.json found in the current directory or parents");
|
|
27
|
-
|
|
28
|
-
|
|
51
|
+
// DEV-G10-R2: snapshot the config ONCE. Both the bootstrap and the strict
|
|
52
|
+
// phase validate this same document, so a rewrite during download can never
|
|
53
|
+
// combine an old target with new bitmap tuples.
|
|
54
|
+
const document = loadConfigDocument(project.root);
|
|
55
|
+
const plannedConfigDocument = JSON.stringify(document);
|
|
56
|
+
// DEV-G10-R1 phase 1: catalog-independent bootstrap. A Pro bitmap config
|
|
57
|
+
// references groups the bundled catalog does not ship, so validate only the
|
|
58
|
+
// safe fields (tier/target/syntax) before the release catalog is available.
|
|
59
|
+
const bootstrap = validateConfigDocument(document, bundledCatalog, { lenientCatalog: true });
|
|
60
|
+
if (bootstrap.kind !== "ok" || bootstrap.config.tier !== "pro")
|
|
29
61
|
throw new CliError("VALIDATION_ERROR", "pro install requires a valid tier=pro config");
|
|
30
|
-
|
|
31
|
-
const
|
|
62
|
+
assertDownloadSelection(bootstrap.config);
|
|
63
|
+
const target = expected.target ?? bootstrap.config.target;
|
|
64
|
+
if (target !== bootstrap.config.target) {
|
|
65
|
+
throw new CliError("VALIDATION_ERROR", `config target is "${bootstrap.config.target}" but install target is "${target}"; set target to "${target}" in moeicons.config before installing`);
|
|
66
|
+
}
|
|
67
|
+
const downloaded = await downloadProArtifact(context, deps.auth, {
|
|
68
|
+
version: expected.version,
|
|
69
|
+
descriptorSha256: expected.descriptorSha256,
|
|
70
|
+
...(expected.allowLocalTest === true ? { allowLocalTest: true } : {}),
|
|
71
|
+
}, {
|
|
72
|
+
selection: { config: bootstrap.config, document },
|
|
32
73
|
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
33
74
|
...(deps.allowedHosts ? { allowedHosts: deps.allowedHosts } : {}),
|
|
34
75
|
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
35
76
|
});
|
|
36
|
-
const
|
|
77
|
+
const selectedTarget = downloaded.selected
|
|
78
|
+
? Object.fromEntries(Object.entries(downloaded.selected.files)
|
|
79
|
+
.filter(([path]) => path.startsWith(`${target}/`))
|
|
80
|
+
.map(([path, bytes]) => [path.slice(target.length + 1), bytes]))
|
|
81
|
+
: undefined;
|
|
82
|
+
const subtree = selectedTarget
|
|
83
|
+
? { ok: true, target, files: selectedTarget, ...computeSubtreeHash(selectedTarget) }
|
|
84
|
+
: selectTargetSubtree(downloaded.artifactBytes, downloaded.descriptor, target);
|
|
37
85
|
if (!subtree.ok) {
|
|
38
86
|
throw new CliError("VALIDATION_ERROR", subtree.message);
|
|
39
87
|
}
|
|
40
|
-
|
|
88
|
+
const cacheDir = resolveCacheDir(context.env);
|
|
89
|
+
let bitmapPins;
|
|
90
|
+
let bitmapShardSetSha256Value;
|
|
91
|
+
let installedCatalog;
|
|
92
|
+
try {
|
|
93
|
+
installedCatalog = parseCatalog(JSON.parse(downloaded.catalogJson));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new CliError("VALIDATION_ERROR", error instanceof Error ? error.message : "invalid pro catalog");
|
|
97
|
+
}
|
|
98
|
+
// DEV-G10-R1 phase 2: re-validate the SAME snapshot against the verified
|
|
99
|
+
// release catalog. A style group / icon / variant the release does not ship
|
|
100
|
+
// fails closed here, before ANY cache write, shard fetch or metadata write.
|
|
101
|
+
const strict = validateConfigDocument(document, installedCatalog);
|
|
102
|
+
if (strict.kind !== "ok") {
|
|
103
|
+
throw new CliError("VALIDATION_ERROR", strict.kind === "invalid" ? strict.message : `config state: ${strict.kind}`);
|
|
104
|
+
}
|
|
105
|
+
const config = strict.config;
|
|
106
|
+
const installedFiles = downloaded.selected
|
|
107
|
+
? subtree.files
|
|
108
|
+
: configuredComponentFiles(subtree.files, target, config, installedCatalog);
|
|
109
|
+
// Only after the snapshot passes strict validation do we persist the
|
|
110
|
+
// content-addressed code/metadata caches. A rejected config writes nothing.
|
|
111
|
+
if (!downloaded.selected)
|
|
112
|
+
cacheVerifiedArtifact(deps.fs, cacheDir, downloaded.descriptor.version, downloaded.descriptor.sha256, downloaded.artifactBytes);
|
|
113
|
+
if (downloaded.metadataBytes && downloaded.descriptor.metadata) {
|
|
114
|
+
cacheVerifiedArtifact(deps.fs, cacheDir, downloaded.descriptor.version, downloaded.descriptor.metadata.sha256, downloaded.metadataBytes, "metadata");
|
|
115
|
+
}
|
|
116
|
+
const resolvedTuples = resolveBitmapTuples(config, installedCatalog);
|
|
117
|
+
if (!resolvedTuples.ok)
|
|
118
|
+
throw new CliError("VALIDATION_ERROR", resolvedTuples.errors.join("; "));
|
|
119
|
+
if (!downloaded.selected &&
|
|
120
|
+
resolvedTuples.tuples.length > 0 &&
|
|
121
|
+
downloaded.descriptor.channel !== "local-test") {
|
|
122
|
+
const accessToken = await runAccessTokenUseCase(context, deps.auth);
|
|
123
|
+
const { loopbackHost } = resolveProDescriptorEndpoint(context.env);
|
|
124
|
+
const allowedHosts = loopbackHost ? [...PRO_DOWNLOAD_HOSTS, loopbackHost] : PRO_DOWNLOAD_HOSTS;
|
|
125
|
+
// DEV-G10: reuse verified pinned shards at the same resourceVersion so a
|
|
126
|
+
// size switch only fetches the newly selected tuple. A version bump always
|
|
127
|
+
// yields new shard identities, so no cross-version reuse is possible.
|
|
128
|
+
// W2-B2/B7: a shard already present in EITHER the project's install metadata
|
|
129
|
+
// OR the global CLI shard cache (e.g. warmed with the 128/webp default set)
|
|
130
|
+
// is reused after re-verification; only genuinely missing tuples are fetched.
|
|
131
|
+
let existingPins = [];
|
|
132
|
+
const cacheIo = toCacheIo(deps.fs);
|
|
133
|
+
const cacheManifest = readBitmapShardCacheManifest(cacheDir, cacheIo);
|
|
134
|
+
const metadataPath = join(project.root, ".moeicons", "install-metadata.json");
|
|
135
|
+
if (deps.fs.existsSync(metadataPath) && deps.fs.readFileSync) {
|
|
136
|
+
const existing = parseInstallMetadata(deps.fs.readFileSync(metadataPath, "utf8"), {
|
|
137
|
+
allowLocalTest: allowLocalTestFromEnv(context.env),
|
|
138
|
+
});
|
|
139
|
+
existingPins = existing?.bitmapShards ?? [];
|
|
140
|
+
}
|
|
141
|
+
existingPins = mergeBitmapShardCacheManifest(cacheManifest, existingPins).filter((pin) => pin.resourceVersion === downloaded.descriptor.version);
|
|
142
|
+
const shards = await resolveConfiguredBitmapShards({
|
|
143
|
+
existingPins,
|
|
144
|
+
config,
|
|
145
|
+
catalog: installedCatalog,
|
|
146
|
+
version: downloaded.descriptor.version,
|
|
147
|
+
descriptorSha256: downloaded.descriptor.descriptorSha256,
|
|
148
|
+
cacheDir,
|
|
149
|
+
io: cacheIo,
|
|
150
|
+
accessToken,
|
|
151
|
+
allowedHosts,
|
|
152
|
+
env: context.env,
|
|
153
|
+
now: context.now().getTime(),
|
|
154
|
+
signal: context.signal,
|
|
155
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
156
|
+
});
|
|
157
|
+
bitmapPins = shards.pins;
|
|
158
|
+
bitmapShardSetSha256Value = shards.bitmapShardSetSha256;
|
|
159
|
+
// Record every verified shard in the global cache manifest (idempotent).
|
|
160
|
+
writeBitmapShardCacheManifest(cacheDir, cacheIo, mergeBitmapShardCacheManifest(cacheManifest, shards.pins), context.now().getTime());
|
|
161
|
+
}
|
|
41
162
|
const files = {
|
|
42
163
|
".moeicons/catalog.json": downloaded.catalogJson,
|
|
43
|
-
"
|
|
44
|
-
"
|
|
164
|
+
".moeicons/manifest.json": downloaded.manifestJson,
|
|
165
|
+
".moeicons/MANUAL.md": downloaded.manualMd,
|
|
166
|
+
".moeicons/artifact/package.json": '{"private":true,"type":"module","sideEffects":false}\n',
|
|
167
|
+
[`${config.outputDir.replace(/\\/g, "/").replace(/\/$/, "")}/types.ts`]: typesReexport("pro", target, config.outputDir),
|
|
168
|
+
[`${config.outputDir.replace(/\\/g, "/").replace(/\/$/, "")}/.moeicons-pro.marker`]: "pro\n",
|
|
45
169
|
};
|
|
46
|
-
for (const [rel, bytes] of Object.entries(
|
|
170
|
+
for (const [rel, bytes] of Object.entries(installedFiles)) {
|
|
47
171
|
files[`.moeicons/artifact/${target}/${rel}`] = bytes;
|
|
48
172
|
}
|
|
173
|
+
if (downloaded.selected) {
|
|
174
|
+
files[".moeicons/resource-index.json.gz"] = downloaded.selected.indexBytes;
|
|
175
|
+
for (const [rel, bytes] of Object.entries(downloaded.selected.files))
|
|
176
|
+
files[`.moeicons/artifact/${rel}`] = bytes;
|
|
177
|
+
}
|
|
49
178
|
const managedFiles = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, sha256Bytes(content)]));
|
|
50
179
|
files[".moeicons/install-metadata.json"] = serializeInstallMetadata({
|
|
51
180
|
schemaVersion: 1,
|
|
@@ -57,16 +186,49 @@ export async function runProInstallUseCase(context, deps, expected) {
|
|
|
57
186
|
catalogSha256: downloaded.descriptor.catalogSha256,
|
|
58
187
|
installedAt: context.now().toISOString(),
|
|
59
188
|
managedFiles,
|
|
189
|
+
...(downloaded.selected
|
|
190
|
+
? {
|
|
191
|
+
delivery: {
|
|
192
|
+
mode: "icons",
|
|
193
|
+
indexSha256: downloaded.selected.refs.index.sha256,
|
|
194
|
+
bundleSha256: downloaded.selected.refs.bundle.sha256,
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
: {}),
|
|
60
198
|
targetSha256: subtree.sha256,
|
|
61
199
|
targetFileCount: subtree.fileCount,
|
|
62
200
|
targetByteCount: subtree.byteCount,
|
|
201
|
+
...(bitmapPins && bitmapShardSetSha256Value
|
|
202
|
+
? { bitmapShards: bitmapPins, bitmapShardSetSha256: bitmapShardSetSha256Value }
|
|
203
|
+
: {}),
|
|
204
|
+
// A-1b: record the accepted local-test model so a later generate can verify it.
|
|
205
|
+
...(downloaded.descriptor.channel === "local-test"
|
|
206
|
+
? { channel: "local-test", publishable: false }
|
|
207
|
+
: {}),
|
|
208
|
+
});
|
|
209
|
+
await withProjectLock(project.root, "install", () => {
|
|
210
|
+
if (JSON.stringify(loadConfigDocument(project.root)) !== plannedConfigDocument)
|
|
211
|
+
throw new CliError("VALIDATION_ERROR", "config changed while preparing install; retry");
|
|
212
|
+
executeInstallPlan(createInstallPlan(project.root, files), deps.fs);
|
|
63
213
|
});
|
|
64
|
-
await withProjectLock(project.root, "install", () => executeInstallPlan(createInstallPlan(project.root, files), deps.fs));
|
|
65
214
|
return {
|
|
66
215
|
projectRoot: project.root,
|
|
67
216
|
artifactVersion: downloaded.descriptor.version,
|
|
68
217
|
descriptorSha256: downloaded.descriptor.descriptorSha256,
|
|
69
218
|
catalogSha256: downloaded.descriptor.catalogSha256,
|
|
70
|
-
artifactBytes: downloaded.artifactBytes.byteLength,
|
|
219
|
+
artifactBytes: downloaded.selected?.payloadBytes ?? downloaded.artifactBytes.byteLength,
|
|
220
|
+
downloadMode: downloaded.selected ? "icons" : "full",
|
|
221
|
+
downloadNotes: downloaded.selected?.fallbacks ??
|
|
222
|
+
(config.downloadMode === "full"
|
|
223
|
+
? []
|
|
224
|
+
: [
|
|
225
|
+
"Release does not advertise selected downloads; auto used the verified full archive. Set downloadMode=icons to require selected downloads.",
|
|
226
|
+
]),
|
|
227
|
+
...(downloaded.selected
|
|
228
|
+
? {
|
|
229
|
+
networkBytes: downloaded.selected.networkBytes,
|
|
230
|
+
selectedFiles: Object.keys(downloaded.selected.files).length,
|
|
231
|
+
}
|
|
232
|
+
: {}),
|
|
71
233
|
};
|
|
72
234
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { AuthUseCaseDependencies } from "./auth.js";
|
|
2
|
+
import type { CommandContext } from "./context.js";
|
|
3
|
+
import { type WarmupPlan } from "./bitmap-shard-cache.js";
|
|
4
|
+
import type { BitmapShardFetchProgress } from "./bitmap-shard-resolver.js";
|
|
5
|
+
export type ProResourceState = {
|
|
6
|
+
readonly kind: "not-cached";
|
|
7
|
+
readonly version: string;
|
|
8
|
+
readonly codeBytes: number;
|
|
9
|
+
readonly metadataBytes: number;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "current";
|
|
12
|
+
readonly version: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "outdated";
|
|
15
|
+
readonly version: string;
|
|
16
|
+
readonly latestVersion: string;
|
|
17
|
+
readonly codeBytes: number;
|
|
18
|
+
readonly metadataBytes: number;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: "corrupt";
|
|
21
|
+
readonly version: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "unavailable";
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Read-only state of cached Pro resources relative to the published latest
|
|
28
|
+
* version. Used by the authenticated home screen (Download/Update/Repair).
|
|
29
|
+
*/
|
|
30
|
+
export declare function proResourceState(context: CommandContext, auth: AuthUseCaseDependencies, deps?: {
|
|
31
|
+
readonly fetch?: typeof fetch;
|
|
32
|
+
readonly allowedProHosts?: readonly string[];
|
|
33
|
+
}): Promise<ProResourceState>;
|
|
34
|
+
export interface ProPredownloadResult {
|
|
35
|
+
readonly version: string;
|
|
36
|
+
readonly codeSha256: string;
|
|
37
|
+
readonly descriptorSha256: string;
|
|
38
|
+
readonly metadataSha256?: string;
|
|
39
|
+
readonly catalogSha256: string;
|
|
40
|
+
readonly codeBytes: number;
|
|
41
|
+
readonly metadataBytes: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Pre-download (and verify + cache) the full Pro code + metadata archives after
|
|
45
|
+
* login or from the home screen, without installing anything into a project.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runProPredownloadUseCase(context: CommandContext, auth: AuthUseCaseDependencies, deps?: {
|
|
48
|
+
readonly fetch?: typeof fetch;
|
|
49
|
+
readonly allowedProHosts?: readonly string[];
|
|
50
|
+
readonly force?: boolean;
|
|
51
|
+
readonly onProgress?: (event: {
|
|
52
|
+
readonly downloadedBytes: number;
|
|
53
|
+
readonly totalBytes?: number;
|
|
54
|
+
}) => void;
|
|
55
|
+
}): Promise<ProPredownloadResult>;
|
|
56
|
+
export interface ProWarmupResult {
|
|
57
|
+
readonly version: string;
|
|
58
|
+
readonly groups: number;
|
|
59
|
+
readonly shards: number;
|
|
60
|
+
readonly downloaded: number;
|
|
61
|
+
readonly reused: number;
|
|
62
|
+
}
|
|
63
|
+
export interface ProWarmupDeps {
|
|
64
|
+
readonly version: string;
|
|
65
|
+
readonly descriptorSha256: string;
|
|
66
|
+
readonly metadataSha256: string;
|
|
67
|
+
readonly catalogSha256: string;
|
|
68
|
+
readonly fetch?: typeof fetch;
|
|
69
|
+
readonly allowedProHosts?: readonly string[];
|
|
70
|
+
readonly onPlan?: (plan: WarmupPlan) => void;
|
|
71
|
+
readonly onProgress?: (event: BitmapShardFetchProgress) => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* W2-A: after the Pro code + metadata are cached, warm the default bitmap shard
|
|
75
|
+
* set (every bitmap style group's 128/webp shard) into the global cache. The
|
|
76
|
+
* release catalog comes from the already-verified metadata archive, so no
|
|
77
|
+
* second artifact download is needed.
|
|
78
|
+
*/
|
|
79
|
+
export declare function runProResourceWarmupUseCase(context: CommandContext, auth: AuthUseCaseDependencies, deps: ProWarmupDeps): Promise<ProWarmupResult>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, renameSync } from "node:fs";
|
|
4
|
+
import { CliError } from "../errors/index.js";
|
|
5
|
+
import { artifactCachePath, metadataCachePath } from "./free-download.js";
|
|
6
|
+
import { downloadSignedArtifact } from "./signed-artifact.js";
|
|
7
|
+
import { fetchProDescriptor, proSignedDownloadOptions } from "./pro-download.js";
|
|
8
|
+
import { fetchLibraryVersions } from "./version-service.js";
|
|
9
|
+
import { isVersionNewer } from "../metadata/version.js";
|
|
10
|
+
import { verifyArtifact } from "../project/install.js";
|
|
11
|
+
import { cacheArtifact } from "./cache.js";
|
|
12
|
+
import { extractAndVerifyMetadataArchive } from "../metadata/archive.js";
|
|
13
|
+
import { parseCatalog } from "../catalog/catalog.js";
|
|
14
|
+
import { runAccessTokenUseCase } from "./auth.js";
|
|
15
|
+
import { PRO_DOWNLOAD_HOSTS } from "./pro-download.js";
|
|
16
|
+
import { resolveBitmapShardDescriptorEndpoint } from "./bitmap-shard-download.js";
|
|
17
|
+
import { warmDefaultBitmapShards, } from "./bitmap-shard-cache.js";
|
|
18
|
+
function cacheDir(env) {
|
|
19
|
+
return env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
|
|
20
|
+
}
|
|
21
|
+
/** Versions of pro archives currently present in the global cache. */
|
|
22
|
+
function cachedProVersions(dir) {
|
|
23
|
+
const base = join(dir, "moewolf-dev", "moe-icons");
|
|
24
|
+
if (!existsSync(base))
|
|
25
|
+
return [];
|
|
26
|
+
let names;
|
|
27
|
+
try {
|
|
28
|
+
names = readdirSync(base);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return names.filter((name) => /^\d+\.\d+\.\d+(?:-(?:alpha|beta))?$/.test(name));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read-only state of cached Pro resources relative to the published latest
|
|
37
|
+
* version. Used by the authenticated home screen (Download/Update/Repair).
|
|
38
|
+
*/
|
|
39
|
+
export async function proResourceState(context, auth, deps = {}) {
|
|
40
|
+
let versions;
|
|
41
|
+
try {
|
|
42
|
+
versions = await fetchLibraryVersions({ ...(deps.fetch ? { fetch: deps.fetch } : {}), signal: context.signal, env: context.env });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { kind: "unavailable", reason: "version check failed" };
|
|
46
|
+
}
|
|
47
|
+
if (!versions.pro)
|
|
48
|
+
return { kind: "unavailable", reason: "no pro release is published" };
|
|
49
|
+
const latest = versions.pro;
|
|
50
|
+
let descriptor;
|
|
51
|
+
try {
|
|
52
|
+
descriptor = await fetchProDescriptor(context, auth, { version: latest.version, descriptorSha256: latest.descriptorSha256 }, { ...(deps.fetch ? { fetch: deps.fetch } : {}) });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
kind: "unavailable",
|
|
57
|
+
reason: error instanceof CliError ? error.message : "pro resources unavailable",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const dir = cacheDir(context.env);
|
|
61
|
+
const codePath = artifactCachePath(dir, descriptor.version, descriptor.sha256);
|
|
62
|
+
const metaPath = descriptor.metadata
|
|
63
|
+
? metadataCachePath(dir, descriptor.version, descriptor.metadata.sha256)
|
|
64
|
+
: undefined;
|
|
65
|
+
if (existsSync(codePath) && (metaPath === undefined || existsSync(metaPath))) {
|
|
66
|
+
const codeOk = verifyArtifact(readFileSync(codePath), descriptor.sha256).ok;
|
|
67
|
+
const metaOk = metaPath === undefined || verifyArtifact(readFileSync(metaPath), descriptor.metadata.sha256).ok;
|
|
68
|
+
if (codeOk && metaOk)
|
|
69
|
+
return { kind: "current", version: descriptor.version };
|
|
70
|
+
return { kind: "corrupt", version: descriptor.version };
|
|
71
|
+
}
|
|
72
|
+
const cached = cachedProVersions(dir).find((version) => version !== descriptor.version && isVersionNewer(descriptor.version, version));
|
|
73
|
+
if (cached) {
|
|
74
|
+
return {
|
|
75
|
+
kind: "outdated",
|
|
76
|
+
version: cached,
|
|
77
|
+
latestVersion: descriptor.version,
|
|
78
|
+
codeBytes: descriptor.size,
|
|
79
|
+
metadataBytes: descriptor.metadata?.size ?? 0,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
kind: "not-cached",
|
|
84
|
+
version: descriptor.version,
|
|
85
|
+
codeBytes: descriptor.size,
|
|
86
|
+
metadataBytes: descriptor.metadata?.size ?? 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Pre-download (and verify + cache) the full Pro code + metadata archives after
|
|
91
|
+
* login or from the home screen, without installing anything into a project.
|
|
92
|
+
*/
|
|
93
|
+
export async function runProPredownloadUseCase(context, auth, deps = {}) {
|
|
94
|
+
const versions = await fetchLibraryVersions({ ...(deps.fetch ? { fetch: deps.fetch } : {}), signal: context.signal, env: context.env });
|
|
95
|
+
if (!versions.pro)
|
|
96
|
+
throw new CliError("NOT_FOUND", "no pro release is published");
|
|
97
|
+
const latest = versions.pro;
|
|
98
|
+
const descriptor = await fetchProDescriptor(context, auth, { version: latest.version, descriptorSha256: latest.descriptorSha256 }, { ...(deps.fetch ? { fetch: deps.fetch } : {}) });
|
|
99
|
+
const dir = cacheDir(context.env);
|
|
100
|
+
const downloadOptions = proSignedDownloadOptions(context, {
|
|
101
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
102
|
+
...(deps.allowedProHosts ? { allowedHosts: deps.allowedProHosts } : {}),
|
|
103
|
+
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
104
|
+
});
|
|
105
|
+
const persist = (path, bytes, expectedSha256) => {
|
|
106
|
+
if (deps.force && existsSync(path))
|
|
107
|
+
rmSync(path, { force: true });
|
|
108
|
+
if (!existsSync(path) || deps.force) {
|
|
109
|
+
cacheArtifact({
|
|
110
|
+
mkdirSync: (dir) => mkdirSync(dir, { recursive: true }),
|
|
111
|
+
writeFileSync,
|
|
112
|
+
renameSync,
|
|
113
|
+
existsSync,
|
|
114
|
+
rmSync,
|
|
115
|
+
}, path, bytes, expectedSha256);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const codeRef = { url: descriptor.url, expiresAt: descriptor.expiresAt, size: descriptor.size, sha256: descriptor.sha256 };
|
|
119
|
+
const codeBytes = await downloadSignedArtifact(codeRef, downloadOptions);
|
|
120
|
+
persist(artifactCachePath(dir, descriptor.version, descriptor.sha256), codeBytes, descriptor.sha256);
|
|
121
|
+
let metadataSha256;
|
|
122
|
+
let metadataBytes = 0;
|
|
123
|
+
if (descriptor.metadata) {
|
|
124
|
+
const metaBytes = await downloadSignedArtifact(descriptor.metadata, downloadOptions);
|
|
125
|
+
metadataSha256 = descriptor.metadata.sha256;
|
|
126
|
+
metadataBytes = metaBytes.byteLength;
|
|
127
|
+
persist(metadataCachePath(dir, descriptor.version, descriptor.metadata.sha256), metaBytes, descriptor.metadata.sha256);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
version: descriptor.version,
|
|
131
|
+
codeSha256: descriptor.sha256,
|
|
132
|
+
descriptorSha256: descriptor.descriptorSha256,
|
|
133
|
+
...(metadataSha256 ? { metadataSha256 } : {}),
|
|
134
|
+
catalogSha256: descriptor.catalogSha256,
|
|
135
|
+
codeBytes: codeBytes.byteLength,
|
|
136
|
+
metadataBytes,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function cacheIo() {
|
|
140
|
+
return {
|
|
141
|
+
mkdirSync: (path) => mkdirSync(path, { recursive: true }),
|
|
142
|
+
writeFileSync,
|
|
143
|
+
renameSync,
|
|
144
|
+
existsSync,
|
|
145
|
+
rmSync,
|
|
146
|
+
readFileSync,
|
|
147
|
+
readdirSync,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* W2-A: after the Pro code + metadata are cached, warm the default bitmap shard
|
|
152
|
+
* set (every bitmap style group's 128/webp shard) into the global cache. The
|
|
153
|
+
* release catalog comes from the already-verified metadata archive, so no
|
|
154
|
+
* second artifact download is needed.
|
|
155
|
+
*/
|
|
156
|
+
export async function runProResourceWarmupUseCase(context, auth, deps) {
|
|
157
|
+
const dir = cacheDir(context.env);
|
|
158
|
+
const metadataPath = metadataCachePath(dir, deps.version, deps.metadataSha256);
|
|
159
|
+
if (!existsSync(metadataPath)) {
|
|
160
|
+
throw new CliError("NOT_FOUND", "Pro metadata is not cached; download Pro resources first");
|
|
161
|
+
}
|
|
162
|
+
const extracted = extractAndVerifyMetadataArchive(readFileSync(metadataPath), {
|
|
163
|
+
expectedCatalogSha: deps.catalogSha256,
|
|
164
|
+
expectedTier: "pro",
|
|
165
|
+
expectedVersion: deps.version,
|
|
166
|
+
});
|
|
167
|
+
if (extracted.kind !== "ok")
|
|
168
|
+
throw new CliError("VALIDATION_ERROR", extracted.message);
|
|
169
|
+
let catalog;
|
|
170
|
+
try {
|
|
171
|
+
catalog = parseCatalog(JSON.parse(extracted.value.catalogJson));
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
throw new CliError("VALIDATION_ERROR", error instanceof Error ? error.message : "invalid pro catalog");
|
|
175
|
+
}
|
|
176
|
+
const accessToken = await runAccessTokenUseCase(context, auth);
|
|
177
|
+
const endpoint = resolveBitmapShardDescriptorEndpoint(context.env);
|
|
178
|
+
const allowedHosts = endpoint.allowLoopback
|
|
179
|
+
? [...PRO_DOWNLOAD_HOSTS, new URL(endpoint.url).host]
|
|
180
|
+
: deps.allowedProHosts ?? PRO_DOWNLOAD_HOSTS;
|
|
181
|
+
const result = await warmDefaultBitmapShards({
|
|
182
|
+
catalog,
|
|
183
|
+
version: deps.version,
|
|
184
|
+
descriptorSha256: deps.descriptorSha256,
|
|
185
|
+
accessToken,
|
|
186
|
+
cacheDir: dir,
|
|
187
|
+
io: cacheIo(),
|
|
188
|
+
endpoint: endpoint.url,
|
|
189
|
+
allowedHosts,
|
|
190
|
+
signal: context.signal,
|
|
191
|
+
now: context.now().getTime(),
|
|
192
|
+
...(endpoint.allowLoopback ? { allowLoopback: true } : {}),
|
|
193
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
194
|
+
...(deps.onPlan ? { onPlan: deps.onPlan } : {}),
|
|
195
|
+
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
version: deps.version,
|
|
199
|
+
groups: new Set(result.tuples.map((tuple) => tuple.styleGroupId)).size,
|
|
200
|
+
shards: result.tuples.length,
|
|
201
|
+
downloaded: result.downloaded,
|
|
202
|
+
reused: result.reused,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AuthUseCaseDependencies } from "./auth.js";
|
|
2
|
+
import { type ProArtifactDescriptor } from "./pro-download.js";
|
|
3
|
+
import { type ConfigDocument, type MoeiconsConfigFile } from "../project/config.js";
|
|
4
|
+
import type { CommandContext } from "./context.js";
|
|
5
|
+
import type { SelectedResourceDownload } from "./free-download.js";
|
|
6
|
+
export declare function downloadProSelected(context: CommandContext, auth: AuthUseCaseDependencies, descriptor: ProArtifactDescriptor, selection: {
|
|
7
|
+
config: MoeiconsConfigFile;
|
|
8
|
+
document: ConfigDocument;
|
|
9
|
+
}, deps: {
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
allowedHosts?: readonly string[];
|
|
12
|
+
}): Promise<{
|
|
13
|
+
selected: SelectedResourceDownload;
|
|
14
|
+
catalogJson: string;
|
|
15
|
+
manifestJson: string;
|
|
16
|
+
manualMd: string;
|
|
17
|
+
metadataBytes: Uint8Array;
|
|
18
|
+
} | undefined>;
|