@moewolf/moe-icons-cli 0.0.1 → 0.0.2

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.
Files changed (151) hide show
  1. package/README.md +243 -9
  2. package/bin/check-node.cjs +36 -0
  3. package/bin/moeicons.js +12 -1
  4. package/dist/api/client.d.ts +13 -2
  5. package/dist/api/client.js +145 -21
  6. package/dist/auth/device-login.d.ts +10 -7
  7. package/dist/auth/device-login.js +60 -37
  8. package/dist/auth/login-schemas.d.ts +24 -0
  9. package/dist/auth/login-schemas.js +67 -0
  10. package/dist/catalog/catalog.d.ts +11 -0
  11. package/dist/catalog/catalog.js +43 -0
  12. package/dist/catalog/catalog.json +538 -3395
  13. package/dist/cli.d.ts +11 -2
  14. package/dist/cli.js +773 -147
  15. package/dist/commands/parser.d.ts +13 -2
  16. package/dist/commands/parser.js +138 -50
  17. package/dist/config-package/generated/SOURCE.json +25 -0
  18. package/dist/config-package/generated/moeicons-config.schema.json +191 -0
  19. package/dist/config-package/generated/moeicons.config.jsonc +42 -0
  20. package/dist/config-package/generated/render-config.cjs +145 -0
  21. package/dist/config-package/generated/validate-config.cjs +293 -0
  22. package/dist/config-package/generated-config.d.ts +32 -0
  23. package/dist/config-package/generated-config.js +19 -0
  24. package/dist/core/auth.d.ts +24 -0
  25. package/dist/core/auth.js +127 -8
  26. package/dist/core/bitmap-assets.d.ts +19 -5
  27. package/dist/core/bitmap-assets.js +40 -31
  28. package/dist/core/bitmap-shard-budget.d.ts +43 -0
  29. package/dist/core/bitmap-shard-budget.js +16 -0
  30. package/dist/core/bitmap-shard-cache.d.ts +68 -0
  31. package/dist/core/bitmap-shard-cache.js +115 -0
  32. package/dist/core/bitmap-shard-download.d.ts +130 -0
  33. package/dist/core/bitmap-shard-download.js +359 -0
  34. package/dist/core/bitmap-shard-resolver.d.ts +129 -0
  35. package/dist/core/bitmap-shard-resolver.js +262 -0
  36. package/dist/core/bitmap-shards.d.ts +70 -0
  37. package/dist/core/bitmap-shards.js +249 -0
  38. package/dist/core/bootstrap.d.ts +37 -0
  39. package/dist/core/bootstrap.js +54 -0
  40. package/dist/core/cache.d.ts +34 -0
  41. package/dist/core/cache.js +96 -0
  42. package/dist/core/context.d.ts +26 -0
  43. package/dist/core/doctor.d.ts +48 -0
  44. package/dist/core/doctor.js +190 -0
  45. package/dist/core/free-download.d.ts +64 -6
  46. package/dist/core/free-download.js +402 -30
  47. package/dist/core/generate.d.ts +29 -0
  48. package/dist/core/generate.js +341 -65
  49. package/dist/core/icon-selection.d.ts +8 -0
  50. package/dist/core/icon-selection.js +19 -0
  51. package/dist/core/install.d.ts +14 -0
  52. package/dist/core/install.js +132 -13
  53. package/dist/core/library-update.d.ts +5 -1
  54. package/dist/core/library-update.js +176 -17
  55. package/dist/core/local-test-env.d.ts +24 -0
  56. package/dist/core/local-test-env.js +43 -0
  57. package/dist/core/metadata-sync.d.ts +30 -0
  58. package/dist/core/metadata-sync.js +97 -0
  59. package/dist/core/pro-download.d.ts +63 -0
  60. package/dist/core/pro-download.js +247 -27
  61. package/dist/core/pro-install.d.ts +11 -3
  62. package/dist/core/pro-install.js +181 -19
  63. package/dist/core/pro-resources.d.ts +79 -0
  64. package/dist/core/pro-resources.js +204 -0
  65. package/dist/core/pro-selected-download.d.ts +18 -0
  66. package/dist/core/pro-selected-download.js +269 -0
  67. package/dist/core/release-descriptor.d.ts +42 -1
  68. package/dist/core/release-descriptor.js +117 -9
  69. package/dist/core/resource-variant.d.ts +27 -10
  70. package/dist/core/resource-variant.js +75 -34
  71. package/dist/core/selected-resources.d.ts +59 -0
  72. package/dist/core/selected-resources.js +350 -0
  73. package/dist/core/signed-artifact.d.ts +8 -1
  74. package/dist/core/signed-artifact.js +35 -11
  75. package/dist/core/target-subtree.d.ts +8 -0
  76. package/dist/core/target-subtree.js +54 -3
  77. package/dist/core/version-service.d.ts +11 -0
  78. package/dist/core/version-service.js +129 -24
  79. package/dist/core/wizard.d.ts +20 -17
  80. package/dist/core/wizard.js +66 -28
  81. package/dist/errors/errors.d.ts +2 -1
  82. package/dist/errors/errors.js +1 -0
  83. package/dist/generator/generate.d.ts +1 -1
  84. package/dist/generator/generate.js +420 -213
  85. package/dist/generator/shared/SOURCE.json +7 -0
  86. package/dist/generator/shared/svg-model.cjs +156 -0
  87. package/dist/generator/svg-model.d.ts +16 -0
  88. package/dist/generator/svg-model.js +4 -0
  89. package/dist/generator/theme-resolve.d.ts +2 -0
  90. package/dist/generator/theme-resolve.js +57 -26
  91. package/dist/mcp/server.d.ts +23 -36
  92. package/dist/mcp/server.js +74 -152
  93. package/dist/mcp/services.d.ts +4 -0
  94. package/dist/mcp/services.js +230 -0
  95. package/dist/metadata/archive.d.ts +38 -0
  96. package/dist/metadata/archive.js +94 -0
  97. package/dist/metadata/manifest.d.ts +33 -0
  98. package/dist/metadata/manifest.js +136 -0
  99. package/dist/metadata/version.d.ts +22 -0
  100. package/dist/metadata/version.js +53 -0
  101. package/dist/project/anchors/application.d.ts +13 -0
  102. package/dist/project/anchors/application.js +377 -0
  103. package/dist/project/anchors/apply.d.ts +28 -0
  104. package/dist/project/anchors/apply.js +65 -0
  105. package/dist/project/anchors/diagnose.d.ts +26 -0
  106. package/dist/project/anchors/diagnose.js +90 -0
  107. package/dist/project/anchors/helpers.d.ts +20 -0
  108. package/dist/project/anchors/helpers.js +55 -0
  109. package/dist/project/anchors/moeicons-config.d.ts +21 -0
  110. package/dist/project/anchors/moeicons-config.js +83 -0
  111. package/dist/project/anchors/parse.d.ts +14 -0
  112. package/dist/project/anchors/parse.js +36 -0
  113. package/dist/project/anchors/project-manifest.d.ts +19 -0
  114. package/dist/project/anchors/project-manifest.js +109 -0
  115. package/dist/project/anchors/style.d.ts +12 -0
  116. package/dist/project/anchors/style.js +87 -0
  117. package/dist/project/anchors/types.d.ts +32 -0
  118. package/dist/project/anchors/types.js +9 -0
  119. package/dist/project/config.d.ts +59 -5
  120. package/dist/project/config.js +234 -102
  121. package/dist/project/install-metadata.d.ts +23 -2
  122. package/dist/project/install-metadata.js +58 -7
  123. package/dist/project/install.d.ts +24 -4
  124. package/dist/project/install.js +402 -190
  125. package/dist/project/project-lock.d.ts +2 -2
  126. package/dist/project/project-lock.js +4 -4
  127. package/dist/project/tailwind.d.ts +2 -2
  128. package/dist/project/tailwind.js +3 -4
  129. package/dist/project/tar-gz.d.ts +18 -1
  130. package/dist/project/tar-gz.js +40 -3
  131. package/dist/tui/components.d.ts +62 -0
  132. package/dist/tui/components.js +100 -0
  133. package/dist/tui/primitives.d.ts +1 -0
  134. package/dist/tui/primitives.js +8 -1
  135. package/dist/ui/banner.d.ts +31 -3
  136. package/dist/ui/banner.js +120 -25
  137. package/dist/ui/branded-prompts.d.ts +41 -0
  138. package/dist/ui/branded-prompts.js +80 -0
  139. package/dist/ui/clack.d.ts +6 -3
  140. package/dist/ui/clack.js +56 -5
  141. package/dist/ui/create-ui.d.ts +1 -0
  142. package/dist/ui/create-ui.js +5 -1
  143. package/dist/ui/generated/wordmark-large.d.ts +17 -0
  144. package/dist/ui/generated/wordmark-large.js +28 -0
  145. package/dist/ui/generated/wordmark.d.ts +2 -0
  146. package/dist/ui/generated/wordmark.js +2 -0
  147. package/dist/ui/non-interactive.js +8 -0
  148. package/dist/ui/stream.js +30 -0
  149. package/dist/ui/theme.d.ts +42 -0
  150. package/dist/ui/theme.js +59 -0
  151. package/package.json +14 -6
@@ -1,20 +1,44 @@
1
+ import { assertDownloadSelection } from "./selected-resources.js";
1
2
  import { detectProject } from "../project/detect.js";
2
- import { readMoeiconsConfig } from "../project/config.js";
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 { downloadProArtifact } from "./pro-download.js";
7
- import { artifactCachePath } from "./free-download.js";
8
- import { selectTargetSubtree } from "./target-subtree.js";
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 = artifactCachePath(cacheDir, version, sha256);
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
- const config = readMoeiconsConfig(project.root);
28
- if (config.kind !== "ok" || config.config.tier !== "pro")
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
- const target = expected.target ?? config.config.target;
31
- const downloaded = await downloadProArtifact(context, deps.auth, expected, {
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 subtree = selectTargetSubtree(downloaded.artifactBytes, downloaded.descriptor, target);
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
- cacheVerifiedArtifact(deps.fs, resolveCacheDir(context.env), downloaded.descriptor.version, downloaded.descriptor.sha256, downloaded.artifactBytes);
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
- "src/moeicons/types.ts": `export type { ReactIconProps } from "moe-icons";\n`,
44
- "src/moeicons/.moeicons-pro.marker": "pro\n",
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(subtree.files)) {
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>;