@moewolf/moe-icons-cli 0.0.10 → 0.0.14

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 (56) hide show
  1. package/README.md +27 -19
  2. package/dist/cli.js +124 -146
  3. package/dist/config-package/generated/SOURCE.json +3 -3
  4. package/dist/config-package/generated/moeicons-config.schema.json +1 -1
  5. package/dist/core/bitmap-shard-budget.d.ts +2 -2
  6. package/dist/core/bitmap-shard-budget.js +1 -1
  7. package/dist/core/bootstrap.js +5 -3
  8. package/dist/core/candidate-version-pointer.d.ts +13 -0
  9. package/dist/core/candidate-version-pointer.js +37 -0
  10. package/dist/core/config-package-plan.d.ts +25 -0
  11. package/dist/core/config-package-plan.js +156 -0
  12. package/dist/core/doctor.js +4 -0
  13. package/dist/core/free-download.js +34 -4
  14. package/dist/core/free-small-package-download.d.ts +67 -0
  15. package/dist/core/free-small-package-download.js +344 -0
  16. package/dist/core/generate.js +49 -33
  17. package/dist/core/install.d.ts +8 -2
  18. package/dist/core/install.js +70 -0
  19. package/dist/core/library-update.d.ts +6 -1
  20. package/dist/core/library-update.js +54 -0
  21. package/dist/core/manage.d.ts +4 -0
  22. package/dist/core/manage.js +4 -0
  23. package/dist/core/metadata-sync.js +119 -1
  24. package/dist/core/pro-download.js +19 -2
  25. package/dist/core/pro-install.d.ts +10 -4
  26. package/dist/core/pro-install.js +47 -0
  27. package/dist/core/pro-resources.d.ts +23 -0
  28. package/dist/core/pro-resources.js +51 -0
  29. package/dist/core/pro-selected-download.js +12 -2
  30. package/dist/core/pro-small-package-download.d.ts +61 -0
  31. package/dist/core/pro-small-package-download.js +301 -0
  32. package/dist/core/release-descriptor.js +6 -2
  33. package/dist/core/selected-resources.js +2 -2
  34. package/dist/core/small-pack/PIN.json +11 -0
  35. package/dist/core/small-pack/candidate-set-v1.schema.json +187 -0
  36. package/dist/core/small-pack/candidate-set.cjs +286 -0
  37. package/dist/core/small-pack/candidate-set.d.cts +12 -0
  38. package/dist/core/small-pack/tar-read.cjs +220 -0
  39. package/dist/core/small-pack/tar-read.d.cts +2 -0
  40. package/dist/core/small-package-install.d.ts +40 -0
  41. package/dist/core/small-package-install.js +252 -0
  42. package/dist/core/small-package-selection.d.ts +27 -0
  43. package/dist/core/small-package-selection.js +114 -0
  44. package/dist/core/update-policy.d.ts +1 -0
  45. package/dist/core/update-policy.js +10 -3
  46. package/dist/mcp/services.js +1 -1
  47. package/dist/project/config.js +1 -1
  48. package/dist/project/install-metadata.d.ts +2 -1
  49. package/dist/project/install-metadata.js +6 -3
  50. package/dist/project/install.d.ts +11 -2
  51. package/dist/project/install.js +104 -12
  52. package/dist/project/package-install-metadata.d.ts +31 -0
  53. package/dist/project/package-install-metadata.js +99 -0
  54. package/dist/project/tar-gz.d.ts +4 -8
  55. package/dist/project/tar-gz.js +6 -10
  56. package/package.json +11 -11
package/README.md CHANGED
@@ -16,8 +16,8 @@ moeicons generate generate target-specific project files
16
16
  moeicons doctor --check check configuration and integration
17
17
  moeicons recover recover an interrupted transaction
18
18
  moeicons init create moeicons.config.jsonc
19
- moeicons update metadata sync MANUAL.md/catalog.json/manifest.json only
20
- moeicons update full code + metadata update
19
+ moeicons update metadata sync metadata for supported legacy installs
20
+ moeicons update update the installed Free or Pro package set
21
21
  moeicons mcp start the MCP stdio server
22
22
  moeicons --version show version
23
23
  moeicons --help show help
@@ -73,10 +73,12 @@ first, then run `moeicons install free --target <target>` and `moeicons generate
73
73
  For React/Vue, installation now keeps only the configured SVG component modules
74
74
  and their required files in `.moeicons/artifact`. After adding icon IDs to the
75
75
  config, run `moeicons install` again before `moeicons generate`; generate reports
76
- the missing installed component if this step is skipped. New releases support
77
- selected-resource downloads as well as full archives; `downloadMode` controls
78
- this choice. Legacy releases without resource indexes still use a full archive
79
- in `auto` mode. See Download modes and resource selection below.
76
+ the missing installed component if this step is skipped. Published releases
77
+ must provide bounded selected packages. A legacy descriptor without the new
78
+ package-set contract is rejected; the CLI never downloads a complete tier
79
+ archive as a fallback. See Download modes and resource selection below.
80
+ In this branch, Free and Pro installs resolve their stable package-set identity, verify the root and pages, then download only the selected package closure before the transactional install. Pro requests use the logged-in access token; the Worker checks current entitlement on identity, root, page, and package requests. Pro package cache files are private to the current OS user. `moeicons update` compares the installed package-set identity with the tier's Worker stable pointer and pins the exact set before reconciling files. `moeicons update metadata` refreshes only the selected metadata packages for the exact installed package-set, verifies the installed selection still matches the project config, and transactionally restores the derived catalog without fetching code or icon packages. The global Pro preview warm-up also uses separately addressed packages from the current package set. These changes are local to this branch; the Worker and CLI have not been deployed or published.
81
+
80
82
  Each theme may select a subset of the registered project icons:
81
83
 
82
84
  ```json
@@ -138,16 +140,21 @@ Installing or updating a tier writes `<project>/.moeicons/`:
138
140
  └── artifact/<target>/
139
141
  ```
140
142
 
141
- `moeicons update metadata` re-downloads only the small metadata archive for the
142
- installed code version and reconciles the three files, without touching the
143
- code artifact.
143
+ `moeicons update metadata` refreshes metadata for a pinned package-set install
144
+ by reading only its selected scoped metadata packages. Legacy installs continue
145
+ to use their version-pinned metadata archive. Neither path changes the installed
146
+ code or resource package selection.
144
147
 
145
148
  ## Pro resources
146
149
 
147
- After an authenticated login (TTY) the CLI offers a one-time pre-download of the
148
- full Pro code + metadata. If skipped, the authenticated home screen shows the
149
- resource state and offers `Download / Update / Repair Pro resources`. Logging
150
- out keeps cached archives but blocks new unauthenticated installs/updates.
150
+ Project-level Pro installation uses authenticated, selected package sets and
151
+ does not require pre-downloading the full Pro library. The separate global
152
+ `Warm default Pro previews` action downloads only the current package set's
153
+ separately addressed 128-WebP bitmap tuple packages, after showing their total
154
+ compressed size and asking for confirmation. It does not pre-download a Pro
155
+ code archive. Project installs continue to fetch only their configured package
156
+ closure. Logging out keeps cached objects but blocks new package-set installs,
157
+ updates, and Pro preview warm-ups.
151
158
 
152
159
  ## Security
153
160
 
@@ -179,7 +186,8 @@ out keeps cached archives but blocks new unauthenticated installs/updates.
179
186
 
180
187
  Metadata distribution (`MANUAL-DIST`) implemented: builder-generated manual,
181
188
  catalog and manifest, per-tier archives and descriptors, Free bootstrap,
182
- metadata-only sync, and authenticated Pro pre-download/update.
189
+ metadata-only sync, and authenticated Pro package-set install/update and
190
+ bounded preview warm-up.
183
191
 
184
192
  ### Download modes and resource selection
185
193
 
@@ -202,17 +210,17 @@ Set `downloadMode` in `moeicons.config.jsonc` (schemaVersion 3):
202
210
  }
203
211
  ```
204
212
 
205
- - `auto` (default): selected resources when the fixed release advertises them. A legacy release without this capability uses the full archive and explains why.
206
- - `icons`: require selected resources. Unsupported releases, authorization failures, bad ranges and invalid digests stop the operation; they never trigger a full archive download.
207
- - `full`: download and verify the complete archive, then install the configured target/resources.
213
+ - `auto` (default): resolve only the fixed release's selected package set. A legacy descriptor without that contract fails before resource transfer.
214
+ - `icons`: require selected packages. Unsupported releases, authorization failures, invalid package identities and bad digests stop the operation; they never trigger a full archive download.
215
+ - `full`: retained only for explicit local `-test` fixtures. Published releases reject this mode because tier-wide archives are retired.
208
216
 
209
217
  `icons` registers the allowed icon IDs. Install/update requires at least one global icon ID; an empty global list stops before version queries or downloads and preserves the existing installation. An individual theme may still use `icons: []`. A theme's `icons` restricts that theme to a subset; omitting it selects every registered ID available in the style group, and `[]` selects none. Fallback chooses the requested theme, then the default theme, then theme names in ASCII order. An icon with no selected variant fails validation. Bitmap themes also select `format` and `imageSize`; changing these fields may require new resources.
210
218
 
211
- The CLI reads the release metadata before planning resources, so validation uses that release's catalog. It shows the target, registered icon/theme counts, required file count and compressed payload budget before reading the data object. Verified cache entries reduce traffic. `--json` reports `downloadMode`, `downloadNotes`, `selectedFiles` and `networkBytes` (resource payload only; descriptor/metadata/index traffic is separate).
219
+ For package-set installs, the CLI reads the Worker identity and candidate root/pages before planning resources, so validation uses that candidate's catalog. It shows the target, registered icon/theme counts, required file count and compressed payload budget before reading selected package objects. Verified cache entries reduce traffic. `--json` reports `downloadMode`, `downloadNotes`, `selectedFiles` and `networkBytes` (resource payload only; identity and metadata traffic is separate).
212
220
 
213
221
  Run `moeicons install free` or `moeicons install pro` after changing the selected icons, themes, bitmap format or size, then `moeicons generate`. `moeicons update` also reconciles the configuration when the release version is unchanged. Missing resources name the affected path and ask for reinstall; stale resources are removed only if still owned and unmodified. A configuration change during download stops before project writes; retry with the final configuration.
214
222
 
215
- Generation works offline from the project's verified installed resources; `moeicons recover` also needs no network. Install/update still consult the fixed descriptor and metadata, and Pro checks current entitlement. A resource cache hit does not bypass authorization, and there is no offline install flag. Corrupt cache entries are rejected and fetched again online. Interrupted downloads retain only verified cache members; project files change in one recoverable transaction after all required resources have verified.
223
+ Generation works offline from the project's verified installed resources; `moeicons recover` also needs no network. Package-set installs and updates pin a verified tier candidate; every Pro identity and payload read checks current entitlement. A resource cache hit does not bypass the authenticated stable identity check, and there is no offline install flag. Corrupt cache entries are rejected and fetched again online. Interrupted downloads retain only verified cache members; project files change in one recoverable transaction after all required resources have verified.
216
224
 
217
225
  Version queries have a deadline covering both response headers and body, with bounded JSON sizes. Invalid or unavailable version responses make update fail explicitly. Cache cleanup preserves staging files owned by a live process; disk-full and permission failures report actionable errors.
218
226
 
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { createCommandUi } from "./ui/create-ui.js";
17
17
  import { CLI_VERSION, MOEICONS_BANNER, renderNoticeBox, renderProjectNotice, renderWordmarkText, } from "./ui/banner.js";
18
18
  import { createTheme, isThemeEnabled } from "./ui/theme.js";
19
19
  import { renderHistoryBar } from "./tui/components.js";
20
- import { describeAuthEnvironment, runAccountUseCase, runLoginUseCase, runLogoutUseCase, runRemoteAccountUseCase, runSessionStatusUseCase, } from "./core/auth.js";
20
+ import { describeAuthEnvironment, runAccountUseCase, runLoginUseCase, runLogoutUseCase, runRemoteAccountUseCase, runSessionStatusUseCase, runAccessTokenUseCase, } from "./core/auth.js";
21
21
  import { formatLibraryVersionStatus, getLibraryVersionStatus } from "./core/manage.js";
22
22
  import { runCliUpdateCheck } from "./core/cli-update.js";
23
23
  import { fetchLibraryVersions } from "./core/version-service.js";
@@ -25,9 +25,11 @@ import { runProInstallUseCase } from "./core/pro-install.js";
25
25
  import { resolveLocalTestContext } from "./core/local-test-env.js";
26
26
  import { runLibraryUpdateUseCase } from "./core/library-update.js";
27
27
  import { runMetadataSyncUseCase } from "./core/metadata-sync.js";
28
+ import { fetchFreeStablePackageSet } from "./core/free-small-package-download.js";
29
+ import { fetchProStablePackageSet } from "./core/pro-small-package-download.js";
30
+ import { decideUpdate } from "./core/update-policy.js";
28
31
  import { runBootstrapUseCase } from "./core/bootstrap.js";
29
- import { fetchProDescriptor } from "./core/pro-download.js";
30
- import { proResourceState, runProPredownloadUseCase, runProResourceWarmupUseCase, } from "./core/pro-resources.js";
32
+ import { runProPreviewPackageWarmupUseCase } from "./core/pro-resources.js";
31
33
  import { formatBytes } from "./metadata/version.js";
32
34
  import { isLocalTestVersion } from "./core/release-descriptor.js";
33
35
  import { buildInitPlan, checkRequiresFix, collectSafeFixes, doctorJson, formatDoctorReport, formatPlannedDiffs, plannedDiffsJson, runDoctorApply, runDoctorDiagnose, } from "./core/doctor.js";
@@ -91,6 +93,9 @@ function freeDownloadDeps(runtime) {
91
93
  ...(runtime.env.MOEICONS_FREE_RELEASE_DIR
92
94
  ? { fixtureDir: runtime.env.MOEICONS_FREE_RELEASE_DIR }
93
95
  : {}),
96
+ ...(runtime.env.MOEICONS_FREE_PACKAGE_SET_FIXTURE_ORIGIN
97
+ ? { fixtureBaseUrl: runtime.env.MOEICONS_FREE_PACKAGE_SET_FIXTURE_ORIGIN }
98
+ : {}),
94
99
  cacheDir: runtime.env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache"),
95
100
  cliVersion: versionString(),
96
101
  };
@@ -241,52 +246,36 @@ async function runLogin(runtime, json, yes) {
241
246
  else
242
247
  runtime.stdout(`Environment: ${environment}\nLogged in as ${session.accountId}\n`);
243
248
  if (!json && runtime.isTTY())
244
- await offerProPredownload(runtime);
249
+ await offerProPreviewWarmup(runtime);
245
250
  return 0;
246
251
  }
247
252
  /**
248
- * After a successful interactive login, offer to pre-download the full Pro
249
- * code + metadata. Cancelling or a failed probe never logs the user out.
253
+ * After login, offer an explicit bounded warm-up of default Pro preview units.
254
+ * Cancelling or a failed probe never logs the user out.
250
255
  */
251
- async function offerProPredownload(runtime) {
256
+ async function offerProPreviewWarmup(runtime) {
252
257
  const context = commandContext(runtime, { json: false, yes: false });
258
+ const progress = startResourceProgress(context, "Warming Pro preview packages");
253
259
  try {
254
- const versions = await fetchLibraryVersions({
260
+ const result = await runProPreviewPackageWarmupUseCase(context, runtime.auth ?? {}, {
255
261
  ...(runtime.auth?.fetch ? { fetch: runtime.auth.fetch } : {}),
256
- signal: context.signal,
257
- env: runtime.env,
262
+ onPlan: async (plan) => {
263
+ progress.phase(`${plan.groups} preview groups · ${plan.packages} packages · ${formatBytes(plan.compressedBytes)}`);
264
+ const accepted = await context.ui.confirm(`Warm ${plan.groups} Pro preview groups (${formatBytes(plan.compressedBytes)} compressed package data)?`, context.signal);
265
+ if (accepted !== true)
266
+ throw new CliError("CANCELLED", "Pro preview warm-up cancelled");
267
+ },
258
268
  });
259
- if (!versions.pro) {
260
- runtime.stdout("No Pro release is published yet.\n");
261
- return;
262
- }
263
- const descriptor = await fetchProDescriptor(context, runtime.auth ?? {}, { version: versions.pro.version, descriptorSha256: versions.pro.descriptorSha256 }, runtime.auth?.fetch ? { fetch: runtime.auth.fetch } : {});
264
- const sizes = `${formatBytes(descriptor.size)} code + ${descriptor.metadata ? formatBytes(descriptor.metadata.size) : "0 B"} metadata`;
265
- runtime.stdout(`Pro ${versions.pro.version} available (${sizes}).\n`);
266
- const accepted = await context.ui.confirm("Download Pro resources now? (code + metadata)", context.signal);
267
- if (accepted !== true) {
268
- runtime.stdout("Skipped Pro download; you can download it later from the home screen.\n");
269
+ progress.stop("Pro preview package warm-up complete");
270
+ runtime.stdout(`Warmed ${result.groups} Pro preview groups (${result.downloaded} downloaded, ${result.reused} cached).\n`);
271
+ }
272
+ catch (error) {
273
+ progress.stop("Pro preview warm-up stopped");
274
+ if (error instanceof CliError && error.code === "CANCELLED") {
275
+ runtime.stdout("Skipped Pro preview warm-up; project installs still fetch only configured packages.\n");
269
276
  return;
270
277
  }
271
- const progress = startResourceProgress(context, "Downloading Pro resources");
272
- try {
273
- const result = await runProPredownloadUseCase(context, runtime.auth ?? {}, {
274
- ...(runtime.auth?.fetch ? { fetch: runtime.auth.fetch } : {}),
275
- onProgress: ({ downloadedBytes, totalBytes }) => progress.bytes({ downloadedBytes, ...(totalBytes !== undefined ? { totalBytes } : {}) }),
276
- });
277
- const warmed = await warmProBitmapShards(runtime, result, runtime.auth?.fetch ? { fetch: runtime.auth.fetch } : {}, progress);
278
- progress.stop("Pro resources downloaded");
279
- runtime.stdout(`Cached Pro ${result.version} code + metadata.\n`);
280
- if (warmed)
281
- runtime.stdout(`${warmed}\n`);
282
- }
283
- catch (error) {
284
- progress.stop("Pro download stopped");
285
- runtime.stderr(`warning: Pro download failed (${error instanceof Error ? error.message : String(error)}). You can retry from the home screen.\n`);
286
- }
287
- }
288
- catch {
289
- // A failed probe must not make login fail.
278
+ runtime.stderr(`warning: Pro preview warm-up skipped (${error instanceof Error ? error.message : String(error)}).\n`);
290
279
  }
291
280
  }
292
281
  /**
@@ -313,84 +302,34 @@ function startResourceProgress(context, label) {
313
302
  stop: (message) => legacy.stop(message),
314
303
  };
315
304
  }
316
- /**
317
- * W2-A: best-effort warm-up of the default bitmap shard set after the Pro code
318
- * and metadata are cached. Never fails the surrounding download.
319
- */
320
- async function warmProBitmapShards(runtime, result, fetchDeps, progress) {
321
- if (!result.metadataSha256)
322
- return undefined;
323
- try {
324
- const warm = await runProResourceWarmupUseCase(commandContext(runtime, { json: false, yes: false }), runtime.auth ?? {}, {
325
- version: result.version,
326
- descriptorSha256: result.descriptorSha256,
327
- metadataSha256: result.metadataSha256,
328
- catalogSha256: result.catalogSha256,
329
- ...fetchDeps,
330
- onPlan: (plan) => progress.phase(`Default bitmap shards: ${plan.missing.length} to download, ${plan.reused} cached`),
331
- onProgress: (event) => progress.phase(`Shard ${event.index}/${event.total} ${event.phase} — ${event.tuple.styleGroupId} ${event.tuple.imageSize.width}x${event.tuple.imageSize.height} ${event.tuple.format}`),
332
- });
333
- return `Warmed ${warm.shards} default bitmap shards (${warm.downloaded} downloaded, ${warm.reused} cached).`;
334
- }
335
- catch (error) {
336
- runtime.stderr(`warning: default bitmap shard warm-up skipped (${error instanceof Error ? error.message : String(error)}).\n`);
337
- return undefined;
338
- }
339
- }
340
- /**
341
- * Home-screen Pro resources flow: show the current cache state and drive the
342
- * matching action (download / update / repair / verify). Requires auth.
343
- */
305
+ /** The Pro resource action warms only separately addressed 128-WebP tuple packages. */
344
306
  async function runProResources(runtime, yes) {
345
307
  const context = commandContext(runtime, { json: false, yes });
346
- const fetchDeps = runtime.auth?.fetch ? { fetch: runtime.auth.fetch } : {};
347
- const state = await proResourceState(context, runtime.auth ?? {}, fetchDeps);
348
- if (state.kind === "unavailable") {
349
- runtime.stdout(`Pro resources unavailable: ${state.reason}\n`);
350
- return 0;
351
- }
352
- let action = "";
353
- if (state.kind === "current") {
354
- runtime.stdout(`Pro resources are up to date (v${state.version}).\n`);
355
- return 0;
356
- }
357
- if (state.kind === "not-cached") {
358
- runtime.stdout(`Pro ${state.version}: ${formatBytes(state.codeBytes)} code + ${formatBytes(state.metadataBytes)} metadata.\n`);
359
- action = "Download";
360
- }
361
- else if (state.kind === "outdated") {
362
- runtime.stdout(`Pro ${state.version} → ${state.latestVersion}: ${formatBytes(state.codeBytes)} code + ${formatBytes(state.metadataBytes)} metadata.\n`);
363
- action = "Update";
364
- }
365
- else {
366
- runtime.stdout(`Pro ${state.version} cache is corrupt; re-downloading.\n`);
367
- action = "Repair";
368
- }
369
- const confirmed = await context.ui.confirm(`${action} Pro resources now? (code + metadata)`, context.signal);
370
- if (confirmed !== true) {
371
- runtime.stdout("Cancelled; Pro resources were not downloaded.\n");
372
- return 0;
373
- }
374
- const progress = startResourceProgress(context, "Downloading Pro resources");
308
+ const progress = startResourceProgress(context, "Warming Pro preview packages");
375
309
  try {
376
- const result = await runProPredownloadUseCase(context, runtime.auth ?? {}, {
377
- ...fetchDeps,
378
- force: state.kind === "corrupt",
379
- onProgress: ({ downloadedBytes, totalBytes }) => progress.bytes({ downloadedBytes, ...(totalBytes !== undefined ? { totalBytes } : {}) }),
310
+ const result = await runProPreviewPackageWarmupUseCase(context, runtime.auth ?? {}, {
311
+ ...(runtime.auth?.fetch ? { fetch: runtime.auth.fetch } : {}),
312
+ onPlan: async (plan) => {
313
+ progress.phase(`${plan.groups} preview groups · ${plan.packages} packages · ${formatBytes(plan.compressedBytes)}`);
314
+ const confirmed = await context.ui.confirm(`Warm ${plan.groups} Pro preview groups (${formatBytes(plan.compressedBytes)} compressed package data)?`, context.signal);
315
+ if (confirmed !== true)
316
+ throw new CliError("CANCELLED", "Pro preview warm-up cancelled");
317
+ },
380
318
  });
381
- const warmed = await warmProBitmapShards(runtime, result, fetchDeps, progress);
382
- progress.stop("Pro resources downloaded");
383
- runtime.stdout(`Cached Pro ${result.version} code + metadata.\n`);
384
- if (warmed)
385
- runtime.stdout(`${warmed}\n`);
319
+ progress.stop("Pro preview package warm-up complete");
320
+ runtime.stdout(`Warmed ${result.groups} Pro preview groups (${result.downloaded} downloaded, ${result.reused} cached).\n`);
386
321
  return 0;
387
322
  }
388
323
  catch (error) {
389
- progress.stop("Pro download stopped");
324
+ progress.stop("Pro preview warm-up stopped");
325
+ if (error instanceof CliError && error.code === "CANCELLED") {
326
+ runtime.stdout("Cancelled; no Pro preview packages were downloaded.\n");
327
+ return 0;
328
+ }
390
329
  if (error instanceof CliError && (error.code === "FORBIDDEN" || error.code === "AUTH_ERROR")) {
391
330
  throw error;
392
331
  }
393
- runtime.stderr(`warning: Pro download failed (${error instanceof Error ? error.message : String(error)}). Retry from this menu.\n`);
332
+ runtime.stderr(`Pro preview warm-up failed (${error instanceof Error ? error.message : String(error)}).\n`);
394
333
  return 0;
395
334
  }
396
335
  }
@@ -411,8 +350,11 @@ async function runAccount(runtime, json) {
411
350
  catch (error) {
412
351
  remoteError = error;
413
352
  }
414
- if (json && remoteError)
415
- throw remoteError;
353
+ if (json && remoteError) {
354
+ if (remoteError instanceof Error)
355
+ throw remoteError;
356
+ throw new Error("remote account command failed", { cause: remoteError });
357
+ }
416
358
  const account = { ...session, ...(remote ?? {}) };
417
359
  if (json)
418
360
  writeJson(runtime, { ok: true, environment, account });
@@ -638,21 +580,10 @@ async function runWizard(runtime, json, yes) {
638
580
  session: session.kind,
639
581
  ...(historyLine ? { historyLine } : {}),
640
582
  getLibraryStatus: getStatus,
641
- getProResourceLabel: async () => {
583
+ getProResourceLabel: () => {
642
584
  if (session.kind !== "authenticated")
643
- return undefined;
644
- const state = await proResourceState(context, runtime.auth ?? {}, {
645
- ...(runtime.auth?.fetch ? { fetch: runtime.auth.fetch } : {}),
646
- }).catch(() => undefined);
647
- if (!state || state.kind === "unavailable")
648
- return undefined;
649
- if (state.kind === "not-cached")
650
- return "Download Pro resources";
651
- if (state.kind === "outdated")
652
- return `Update Pro resources (${state.version} → ${state.latestVersion})`;
653
- if (state.kind === "corrupt")
654
- return "Repair Pro resources";
655
- return "Pro resources are up to date";
585
+ return Promise.resolve(undefined);
586
+ return Promise.resolve("Warm default Pro previews");
656
587
  },
657
588
  });
658
589
  if (!result.ok) {
@@ -789,7 +720,7 @@ async function runWizardLogin(runtime, yes) {
789
720
  }
790
721
  }
791
722
  }
792
- async function runLibraryUpdate(runtime, tier, version, descriptorSha256, json = false) {
723
+ async function runLibraryUpdate(runtime, tier, version, descriptorSha256, json = false, packageSetSha256, preflightIdentityBytes) {
793
724
  const context = commandContext(runtime, { json, yes: false });
794
725
  // W3-A: a single progress surface. `task` is deliberately NOT used here: the
795
726
  // clack adapter implements both with `p.spinner()`, so stacking them would
@@ -812,14 +743,17 @@ async function runLibraryUpdate(runtime, tier, version, descriptorSha256, json =
812
743
  auth: runtime.auth ?? {},
813
744
  fetch: runtime.auth?.fetch ?? globalThis.fetch.bind(globalThis),
814
745
  onProgress: ({ downloadedBytes, totalBytes }) => progress.bytes({ downloadedBytes, ...(totalBytes !== undefined ? { totalBytes } : {}) }),
815
- }, { tier, version, descriptorSha256 });
746
+ }, { tier, version, descriptorSha256, ...(packageSetSha256 ? { packageSetSha256 } : {}) });
747
+ const output = preflightIdentityBytes === undefined
748
+ ? result
749
+ : { ...result, networkBytes: (result.networkBytes ?? 0) + preflightIdentityBytes };
816
750
  progress.stop("Icon library update complete");
817
751
  if (json)
818
- writeJson(runtime, { ok: true, ...result });
752
+ writeJson(runtime, { ok: true, ...output });
819
753
  else {
820
- runtime.stdout(`Updated ${tier} artifact to ${result.artifactVersion}; reconciled ${result.files.length} generated files.\n`);
821
- runtime.stdout(`Download mode: ${result.downloadMode}${result.selectedFiles !== undefined ? `; ${result.selectedFiles} resource files; ${result.networkBytes} resource bytes received` : ""}.\n`);
822
- for (const note of result.downloadNotes)
754
+ runtime.stdout(`Updated ${tier} artifact to ${output.artifactVersion}; reconciled ${output.files.length} generated files.\n`);
755
+ runtime.stdout(`Download mode: ${output.downloadMode}${output.selectedFiles !== undefined ? `; ${output.selectedFiles} resource files; ${output.networkBytes} resource bytes received` : ""}.\n`);
756
+ for (const note of output.downloadNotes)
823
757
  runtime.stdout(`${note}\n`);
824
758
  }
825
759
  return 0;
@@ -888,6 +822,41 @@ async function runUpdate(runtime, json, yes, metadataOnly) {
888
822
  throw new CliError("NETWORK_ERROR", status.message);
889
823
  if (status.kind === "installation-invalid")
890
824
  throw new CliError("VALIDATION_ERROR", status.message);
825
+ if (status.kind === "package-set-update-check-required") {
826
+ const tier = status.metadata.candidate.tier;
827
+ const identity = tier === "free"
828
+ ? await fetchFreeStablePackageSet({
829
+ ...(runtime.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: runtime.env.MOEICONS_API_BASE_URL } : {}),
830
+ ...(runtime.auth?.fetch ? { fetchFn: runtime.auth.fetch } : {}),
831
+ signal: context.signal,
832
+ })
833
+ : await fetchProStablePackageSet({
834
+ accessToken: await runAccessTokenUseCase(context, runtime.auth ?? {}),
835
+ ...(runtime.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: runtime.env.MOEICONS_API_BASE_URL } : {}),
836
+ ...(runtime.auth?.fetch ? { fetchFn: runtime.auth.fetch } : {}),
837
+ signal: context.signal,
838
+ });
839
+ const current = status.metadata.candidate;
840
+ const decision = decideUpdate({ installedTier: tier, remoteTier: tier, installedVersion: current.fullVersion, remoteVersion: identity.fullVersion });
841
+ if (decision.kind === "current") {
842
+ if (current.packageSetSha256 !== identity.packageSetSha256 || current.candidateRootSha256 !== identity.candidateRootSha256)
843
+ throw new CliError("VALIDATION_ERROR", "published package-set identity changed for the installed version; refusing replacement");
844
+ if (json)
845
+ writeJson(runtime, { ok: true, status: "current", version: current.fullVersion, packageSetSha256: current.packageSetSha256 });
846
+ else
847
+ runtime.stdout(`Current: ${current.fullVersion} / Latest: ${identity.fullVersion} / Status: up to date\n`);
848
+ return 0;
849
+ }
850
+ if (decision.kind !== "update") {
851
+ const label = decision.reason === "channel-mismatch" ? "prerelease channel differs" : "remote version is older";
852
+ if (json)
853
+ writeJson(runtime, { ok: true, status: decision.reason === "channel-mismatch" ? "channel-mismatch" : "remote-older", currentVersion: current.fullVersion, latestVersion: identity.fullVersion });
854
+ else
855
+ runtime.stdout(`Current: ${current.fullVersion} / Latest: ${identity.fullVersion} / Status: ${label}\n`);
856
+ return 0;
857
+ }
858
+ return runLibraryUpdate(runtime, tier, identity.fullVersion, identity.packageSetSha256, json, identity.packageSetSha256, identity.networkBytes);
859
+ }
891
860
  if (status.kind === "current" &&
892
861
  status.latestDescriptorSha256 !== status.metadata.descriptorSha256)
893
862
  throw new CliError("VALIDATION_ERROR", "published identity changed for the installed version; refusing to replace the pinned release");
@@ -921,21 +890,23 @@ async function runInstall(group, runtime, json, noTailwind, sourceVersion, expec
921
890
  const progress = context.ui.progress("Downloading icon library", context.signal);
922
891
  if (group === "pro" || group === "ent") {
923
892
  const { allowLocalTest } = resolveLocalTestContext(runtime.env);
924
- const identity = sourceVersion && expectedDescriptorSha256
925
- ? { version: sourceVersion, descriptorSha256: expectedDescriptorSha256 }
926
- : await fetchLibraryVersions({
927
- signal: context.signal,
928
- env: runtime.env,
929
- allowLocalTest,
930
- ...(runtime.auth?.fetch ? { fetch: runtime.auth.fetch } : {}),
931
- }).then((versions) => {
932
- if (!versions.pro)
933
- throw new CliError("NOT_FOUND", "no pro release is published");
934
- return {
935
- version: versions.pro.version,
936
- descriptorSha256: versions.pro.descriptorSha256,
937
- };
938
- });
893
+ const identity = allowLocalTest
894
+ ? sourceVersion && expectedDescriptorSha256
895
+ ? { version: sourceVersion, descriptorSha256: expectedDescriptorSha256 }
896
+ : await fetchLibraryVersions({
897
+ signal: context.signal,
898
+ env: runtime.env,
899
+ allowLocalTest,
900
+ ...(runtime.auth?.fetch ? { fetch: runtime.auth.fetch } : {}),
901
+ }).then((versions) => {
902
+ if (!versions.pro)
903
+ throw new CliError("NOT_FOUND", "no pro release is published");
904
+ return {
905
+ version: versions.pro.version,
906
+ descriptorSha256: versions.pro.descriptorSha256,
907
+ };
908
+ })
909
+ : {};
939
910
  try {
940
911
  const result = await runProInstallUseCase(context, {
941
912
  fs: {
@@ -958,6 +929,7 @@ async function runInstall(group, runtime, json, noTailwind, sourceVersion, expec
958
929
  },
959
930
  }, {
960
931
  ...identity,
932
+ ...(!allowLocalTest && sourceVersion ? { version: sourceVersion } : {}),
961
933
  ...(allowLocalTest ? { allowLocalTest: true } : {}),
962
934
  ...(target ? { target } : {}),
963
935
  });
@@ -1025,12 +997,15 @@ async function runInstall(group, runtime, json, noTailwind, sourceVersion, expec
1025
997
  projectRoot: result.projectRoot,
1026
998
  packageManager: result.packageManager,
1027
999
  group: result.group,
1028
- zipBytes: result.artifactBytes,
1000
+ ...(result.downloadMode === "packages" ? {} : { zipBytes: result.artifactBytes }),
1029
1001
  artifactBytes: result.artifactBytes,
1030
1002
  planItems: result.planItems,
1031
1003
  config: result.config,
1032
1004
  artifactVersion: result.artifactVersion,
1033
1005
  descriptorSha256: result.descriptorSha256,
1006
+ candidateRootSha256: result.candidateRootSha256,
1007
+ packageSetSha256: result.packageSetSha256,
1008
+ packageCount: result.packageCount,
1034
1009
  catalogSha256: result.catalogSha256,
1035
1010
  metadataSha256: result.metadataSha256,
1036
1011
  cacheHit: result.cacheHit,
@@ -1038,6 +1013,9 @@ async function runInstall(group, runtime, json, noTailwind, sourceVersion, expec
1038
1013
  downloadNotes: result.downloadNotes,
1039
1014
  networkBytes: result.networkBytes,
1040
1015
  selectedFiles: result.selectedFiles,
1016
+ cacheHits: result.cacheHits,
1017
+ metadataCacheHits: result.metadataCacheHits,
1018
+ transferRetryCount: result.transferRetryCount,
1041
1019
  });
1042
1020
  }
1043
1021
  else {
@@ -1048,7 +1026,7 @@ async function runInstall(group, runtime, json, noTailwind, sourceVersion, expec
1048
1026
  runtime.stdout(`Download mode: ${result.downloadMode}${result.selectedFiles !== undefined ? `; ${result.selectedFiles} resource files; ${result.networkBytes} resource bytes received` : ""}.\n`);
1049
1027
  for (const note of result.downloadNotes ?? [])
1050
1028
  runtime.stdout(`${note}\n`);
1051
- runtime.stdout(`Installed free artifact metadata to .moeicons (cacheHit=${String(result.cacheHit)}).\n`);
1029
+ runtime.stdout(`${result.downloadMode === "packages" ? `Installed ${result.packageCount ?? 0} selected packages` : "Installed free artifact metadata"} to .moeicons (cacheHit=${String(result.cacheHit)}).\n`);
1052
1030
  }
1053
1031
  return 0;
1054
1032
  }
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "sourceRepo": "moewolf-dev/moe-icons-code-library",
4
4
  "sourceDir": "config-package",
5
- "sourceCommit": "76d9924b2114afc317d0a630cde536fb035a908c",
5
+ "sourceCommit": "8a14d3e05a6073896708413bef31b3793b718021",
6
6
  "sourceCommitExact": true,
7
7
  "files": {
8
8
  "render-config.cjs": {
@@ -14,8 +14,8 @@
14
14
  "sha256": "36c35b686335eab4e7db002afb631aa631c80f66656f137b1e2437317b126b0f"
15
15
  },
16
16
  "moeicons-config.schema.json": {
17
- "size": 4769,
18
- "sha256": "0e15c0cf7c282abad7d7399e4a52b7804dc8211b427490fca5efc6c53d9319df"
17
+ "size": 4854,
18
+ "sha256": "aa0a688104fd66431d91bb089ae70c3306cc289b0a4d5a24c9786062b6ae56ab"
19
19
  },
20
20
  "moeicons.config.jsonc": {
21
21
  "size": 1207,
@@ -185,7 +185,7 @@
185
185
  "full"
186
186
  ],
187
187
  "default": "auto",
188
- "description": "Prefer configuration-selected resources (auto), require them (icons), or download the full archive (full)."
188
+ "description": "Resolve selected packages (auto), require selected packages (icons), or use full archives only with explicit local test fixtures (full). Published releases never download a tier-wide archive."
189
189
  }
190
190
  }
191
191
  }
@@ -14,8 +14,8 @@
14
14
  * the measured 89.5/90.0 MiB worst case with headroom; `AUD-FIX-30` records the
15
15
  * production freeze. No consumer may invent its own limit — every shard
16
16
  * download/expand path takes its numbers from here, and the legacy
17
- * `ICON_ARCHIVE_MAX_EXPANDED_BYTES` 512 MiB aggregate cap is deliberately not a
18
- * shard budget.
17
+ * `ICON_ARCHIVE_MAX_EXPANDED_BYTES` 768 MiB aggregate cap is deliberately not a
18
+ * shard budget. Per-shard and temporary-disk bounds remain independently set.
19
19
  */
20
20
  export interface BitmapShardBudget {
21
21
  /** Largest signed compressed shard archive accepted for one tuple. */
@@ -3,7 +3,7 @@ export const BITMAP_SHARD_BUDGET = Object.freeze({
3
3
  expandedBytes: 128 * 1024 * 1024,
4
4
  entries: 2000,
5
5
  singleFileBytes: 16 * 1024 * 1024,
6
- totalDownloadBytes: 512 * 1024 * 1024,
6
+ totalDownloadBytes: 768 * 1024 * 1024,
7
7
  tempDiskBytes: 256 * 1024 * 1024,
8
8
  concurrency: 1,
9
9
  descriptorTimeoutMs: 5_000,
@@ -32,9 +32,11 @@ export async function runBootstrapUseCase(context, deps) {
32
32
  return { kind: "skipped", reason: "no-project" };
33
33
  const state = readInstalledResourceState(project.root, "free");
34
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() });
35
+ (state.metadata.schemaVersion === 2 ||
36
+ (deps.fs.existsSync(join(project.root, ".moeicons", "MANUAL.md")) &&
37
+ deps.fs.existsSync(join(project.root, ".moeicons", "manifest.json"))))) {
38
+ const version = state.metadata.schemaVersion === 1 ? state.metadata.artifactVersion : state.metadata.candidate.fullVersion;
39
+ writeMarker(deps, context.env, { cliVersion: deps.cliVersion, libraryVersion: version, completedAt: new Date().toISOString() });
38
40
  return { kind: "already" };
39
41
  }
40
42
  const confirmed = await context.ui.confirm("Set up moeicons Free icons in this project now? (recommended)", context.signal);
@@ -0,0 +1,13 @@
1
+ import { type CandidateRoot } from './small-pack/candidate-set.cjs';
2
+ export interface FreeCandidateVersionPointer {
3
+ readonly schemaVersion: 1;
4
+ readonly tier: 'free';
5
+ readonly fullVersion: string;
6
+ readonly sourceCommit: string;
7
+ readonly generatorCommit: string;
8
+ readonly packageSetSha256: string;
9
+ readonly candidateRootSha256: string;
10
+ readonly candidateRootBytes: number;
11
+ }
12
+ export declare function parseFreeCandidateVersionPointer(input: Uint8Array, expectedVersion: string): FreeCandidateVersionPointer;
13
+ export declare function pointerMatchesCandidateRoot(pointer: FreeCandidateVersionPointer, root: CandidateRoot): boolean;
@@ -0,0 +1,37 @@
1
+ import { canonicalBytes } from './small-pack/candidate-set.cjs';
2
+ const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:alpha|beta)(?:\.(?:0|[1-9]\d*))?)?$/;
3
+ const SHA256 = /^[a-f0-9]{64}$/;
4
+ const COMMIT = /^[a-f0-9]{40}$/;
5
+ const MAX_POINTER_BYTES = 16 * 1024;
6
+ function check(ok, message) {
7
+ if (!ok)
8
+ throw new Error(`Free candidate version pointer: ${message}`);
9
+ }
10
+ export function parseFreeCandidateVersionPointer(input, expectedVersion) {
11
+ check(VERSION.test(expectedVersion), 'requested version is invalid');
12
+ const bytes = Buffer.from(input);
13
+ check(bytes.length > 0 && bytes.length <= MAX_POINTER_BYTES, 'pointer exceeds 16 KiB');
14
+ let raw;
15
+ try {
16
+ raw = JSON.parse(bytes.toString('utf8'));
17
+ }
18
+ catch {
19
+ throw new Error('Free candidate version pointer: invalid JSON');
20
+ }
21
+ check(raw !== null && typeof raw === 'object' && !Array.isArray(raw), 'pointer object required');
22
+ const value = raw;
23
+ const fields = ['schemaVersion', 'tier', 'fullVersion', 'sourceCommit', 'generatorCommit', 'packageSetSha256', 'candidateRootSha256', 'candidateRootBytes'];
24
+ check(Object.keys(value).sort().join(',') === [...fields].sort().join(','), 'unexpected pointer fields');
25
+ check(bytes.equals(canonicalBytes(value)), 'pointer must use canonical JSON');
26
+ check(value.schemaVersion === 1 && value.tier === 'free' && value.fullVersion === expectedVersion, 'pointer differs from exact Free version');
27
+ check(typeof value.sourceCommit === 'string' && COMMIT.test(value.sourceCommit), 'Source commit is invalid');
28
+ check(typeof value.generatorCommit === 'string' && COMMIT.test(value.generatorCommit), 'generator commit is invalid');
29
+ check(typeof value.packageSetSha256 === 'string' && SHA256.test(value.packageSetSha256), 'package-set digest is invalid');
30
+ check(typeof value.candidateRootSha256 === 'string' && SHA256.test(value.candidateRootSha256), 'candidate root digest is invalid');
31
+ check(Number.isSafeInteger(value.candidateRootBytes) && Number(value.candidateRootBytes) > 0 && Number(value.candidateRootBytes) <= 1024 ** 2, 'candidate root size is invalid');
32
+ return value;
33
+ }
34
+ export function pointerMatchesCandidateRoot(pointer, root) {
35
+ return pointer.tier === root.tier && pointer.fullVersion === root.fullVersion && pointer.sourceCommit === root.sourceCommit
36
+ && pointer.generatorCommit === root.generatorCommit && pointer.packageSetSha256 === root.packageSetSha256;
37
+ }