@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
@@ -8,9 +8,11 @@ import { readMoeiconsConfig, loadConfigDocument, validateConfigDocument, } from
8
8
  import { detectProject } from "../project/detect.js";
9
9
  import { executeManagedReconcile } from "../project/install.js";
10
10
  import { parseInstallMetadata, serializeInstallMetadata, sha256Bytes, } from "../project/install-metadata.js";
11
+ import { parsePackageInstallMetadata } from "../project/package-install-metadata.js";
11
12
  import { withProjectLock } from "../project/project-lock.js";
12
13
  import { ensureClassMergeDependencies, planTailwindIntegration } from "../project/tailwind.js";
13
14
  import { extractTarGzAsync, ICON_ARCHIVE_MAX_ENTRIES, ICON_ARCHIVE_MAX_EXPANDED_BYTES, } from "../project/tar-gz.js";
15
+ import { runSmallPackageInstall } from "./small-package-install.js";
14
16
  import { artifactCachePath, downloadFreeRelease, metadataCachePath, } from "./free-download.js";
15
17
  import { downloadProArtifact } from "./pro-download.js";
16
18
  import { PRO_DOWNLOAD_HOSTS, resolveProDescriptorEndpoint } from "./pro-download.js";
@@ -22,6 +24,18 @@ export async function runLibraryUpdateUseCase(context, deps, expected) {
22
24
  const project = detectProject(context.cwd);
23
25
  if (!project)
24
26
  throw new CliError("VALIDATION_ERROR", "no package.json found in the current directory or parents");
27
+ const installedMetadataPath = join(project.root, ".moeicons", "install-metadata.json");
28
+ let packageSetInstalled = false;
29
+ if (deps.fs.existsSync(installedMetadataPath)) {
30
+ let schemaVersion;
31
+ try {
32
+ const parsed = JSON.parse(deps.fs.readFileSync(installedMetadataPath, "utf8"));
33
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
34
+ schemaVersion = parsed.schemaVersion;
35
+ }
36
+ catch { /* validated by the normal install path below */ }
37
+ packageSetInstalled = schemaVersion === 2;
38
+ }
25
39
  const document = loadConfigDocument(project.root);
26
40
  const bootstrap = validateConfigDocument(document, undefined, { lenientCatalog: true });
27
41
  if (bootstrap.kind !== "ok" || bootstrap.config.tier !== expected.tier)
@@ -29,6 +43,46 @@ export async function runLibraryUpdateUseCase(context, deps, expected) {
29
43
  ? bootstrap.message
30
44
  : `config.tier must match ${expected.tier}; edit config before updating`);
31
45
  assertDownloadSelection(bootstrap.config);
46
+ if (packageSetInstalled) {
47
+ const installed = parsePackageInstallMetadata(deps.fs.readFileSync(installedMetadataPath, "utf8"));
48
+ if (!installed || installed.candidate.tier !== expected.tier) {
49
+ const tierLabel = expected.tier === "free" ? "Free" : "Pro";
50
+ throw new CliError("VALIDATION_ERROR", `package-set update requires a valid installed ${tierLabel} candidate`);
51
+ }
52
+ if ((expected.tier === "free" && !deps.free) || !expected.packageSetSha256 || !/^[a-f0-9]{64}$/.test(expected.packageSetSha256))
53
+ throw new CliError("VALIDATION_ERROR", "package-set update requires an exact Worker package-set identity");
54
+ if (installed.candidate.fullVersion === expected.version && installed.candidate.packageSetSha256 !== expected.packageSetSha256)
55
+ throw new CliError("VALIDATION_ERROR", "published package-set identity changed for the installed version; refusing replacement");
56
+ const proAccessToken = expected.tier === "pro" ? await runAccessTokenUseCase(context, deps.auth) : undefined;
57
+ const result = await runSmallPackageInstall({
58
+ projectRoot: project.root,
59
+ fullVersion: expected.version,
60
+ expectedPackageSetSha256: expected.packageSetSha256,
61
+ ...(context.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: context.env.MOEICONS_API_BASE_URL } : {}),
62
+ config: bootstrap.config,
63
+ plannedConfigDocument: JSON.stringify(document),
64
+ fs: deps.fs,
65
+ cacheDir: deps.free?.cacheDir ?? join(homedir(), ".moeicons", "cache"),
66
+ now: context.now,
67
+ ...(proAccessToken ? { proAccessToken } : {}),
68
+ ...(expected.tier === "free" && deps.free?.fixtureBaseUrl ? { fixtureOrigin: deps.free.fixtureBaseUrl } : {}),
69
+ ...(context.signal ? { signal: context.signal } : {}),
70
+ ...(deps.fetch ? { fetchFn: deps.fetch } : {}),
71
+ });
72
+ return {
73
+ projectRoot: project.root,
74
+ artifactVersion: result.artifactVersion,
75
+ files: result.files,
76
+ downloadMode: "packages",
77
+ downloadNotes: result.fallbacks.map((fallback) => `Theme fallback: ${fallback}`),
78
+ networkBytes: result.networkBytes,
79
+ selectedFiles: result.selectedFiles,
80
+ packageCount: result.packageCount,
81
+ cacheHits: result.cacheHits,
82
+ metadataCacheHits: result.metadataCacheHits,
83
+ transferRetryCount: result.transferRetryCount,
84
+ };
85
+ }
32
86
  let selected;
33
87
  let catalogJson;
34
88
  let fullArchiveFiles;
@@ -1,4 +1,5 @@
1
1
  import { readInstalledResourceState, type InstallMetadata } from "../project/install-metadata.js";
2
+ import type { PackageInstallMetadata } from "../project/package-install-metadata.js";
2
3
  import { type PublicLibraryVersions } from "./version-service.js";
3
4
  export type LibraryVersionStatus = {
4
5
  readonly kind: "installation-invalid";
@@ -11,6 +12,9 @@ export type LibraryVersionStatus = {
11
12
  readonly kind: "unavailable";
12
13
  readonly metadata: InstallMetadata;
13
14
  readonly message: string;
15
+ } | {
16
+ readonly kind: "package-set-update-check-required";
17
+ readonly metadata: PackageInstallMetadata;
14
18
  } | {
15
19
  readonly kind: "current" | "update" | "channel-mismatch" | "remote-older";
16
20
  readonly metadata: InstallMetadata;
@@ -5,6 +5,8 @@ export async function getLibraryVersionStatus(projectRoot, configTier, deps = {}
5
5
  const installed = (deps.readState ?? readInstalledResourceState)(projectRoot, configTier);
6
6
  if (installed.kind !== "ok")
7
7
  return { kind: "installation-invalid", message: installed.message };
8
+ if (installed.metadata.schemaVersion === 2)
9
+ return { kind: "package-set-update-check-required", metadata: installed.metadata };
8
10
  let versions;
9
11
  try {
10
12
  versions = await (deps.fetchVersions ?? (() => fetchLibraryVersions()))();
@@ -31,6 +33,8 @@ export async function getLibraryVersionStatus(projectRoot, configTier, deps = {}
31
33
  export function formatLibraryVersionStatus(status) {
32
34
  if (status.kind === "installation-invalid")
33
35
  return `Current: invalid / Latest: unavailable / Status: installation invalid (${status.message})`;
36
+ if (status.kind === "package-set-update-check-required")
37
+ return `Current: ${status.metadata.candidate.fullVersion} / Latest: unavailable / Status: package-set update check required`;
34
38
  if (status.kind === "check-failed" || status.kind === "unavailable")
35
39
  return `Current: ${status.metadata.artifactVersion} / Latest: unavailable / Status: ${status.message}`;
36
40
  const label = status.kind === "current" ? "up to date" : status.kind === "update" ? "update available" : status.kind === "channel-mismatch" ? "prerelease channel differs" : "remote version is older";
@@ -2,13 +2,20 @@ import { join } from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  import { CliError } from "../errors/index.js";
4
4
  import { detectProject } from "../project/detect.js";
5
+ import { loadConfigDocument, validateConfigDocument } from "../project/config.js";
5
6
  import { parseInstallMetadata, serializeInstallMetadata, sha256Bytes, } from "../project/install-metadata.js";
7
+ import { parsePackageInstallMetadata, serializePackageInstallMetadata } from "../project/package-install-metadata.js";
6
8
  import { executeManagedReconcile } from "../project/install.js";
7
9
  import { withProjectLock } from "../project/project-lock.js";
8
10
  import { cacheArtifact } from "./cache.js";
9
11
  import { downloadMetadataArchive, fetchFreeDescriptor, metadataCachePath, } from "./free-download.js";
10
12
  import { downloadSignedArtifact } from "./signed-artifact.js";
11
13
  import { extractProMetadata, fetchProDescriptor, proSignedDownloadOptions } from "./pro-download.js";
14
+ import { downloadFreeVersionPackages } from "./free-small-package-download.js";
15
+ import { downloadProVersionPackages } from "./pro-small-package-download.js";
16
+ import { runAccessTokenUseCase } from "./auth.js";
17
+ import { packageTargetFiles, planConfigPackagesFromMetadata } from "./config-package-plan.js";
18
+ import { canonicalBytes, sha256 } from "./small-pack/candidate-set.cjs";
12
19
  function toCliError(result) {
13
20
  return new CliError(result.reason === "cancelled" ? "CANCELLED" : "VALIDATION_ERROR", result.message);
14
21
  }
@@ -25,7 +32,18 @@ export async function runMetadataSyncUseCase(context, deps, options = {}) {
25
32
  const metadataPath = join(project.root, ".moeicons", "install-metadata.json");
26
33
  if (!deps.fs.existsSync(metadataPath))
27
34
  throw new CliError("VALIDATION_ERROR", "managed install metadata is missing; run install first");
28
- const old = parseInstallMetadata(deps.fs.readFileSync(metadataPath, "utf8"));
35
+ const rawMetadata = deps.fs.readFileSync(metadataPath, "utf8");
36
+ let schemaVersion;
37
+ try {
38
+ const parsed = JSON.parse(rawMetadata);
39
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
40
+ schemaVersion = parsed.schemaVersion;
41
+ }
42
+ catch { /* reported as invalid metadata below */ }
43
+ if (schemaVersion === 2) {
44
+ return runPackageSetMetadataSync(context, deps, project.root, rawMetadata, options);
45
+ }
46
+ const old = parseInstallMetadata(rawMetadata);
29
47
  if (!old)
30
48
  throw new CliError("VALIDATION_ERROR", "managed install metadata is invalid; run install first");
31
49
  const tier = options.tier ?? old.tier;
@@ -95,3 +113,103 @@ export async function runMetadataSyncUseCase(context, deps, options = {}) {
95
113
  files: [".moeicons/MANUAL.md", ".moeicons/catalog.json", ".moeicons/manifest.json"],
96
114
  };
97
115
  }
116
+ async function runPackageSetMetadataSync(context, deps, projectRoot, rawMetadata, options) {
117
+ const installed = parsePackageInstallMetadata(rawMetadata);
118
+ if (!installed)
119
+ throw new CliError("VALIDATION_ERROR", "package-set install metadata is invalid; run install first");
120
+ const tier = installed.candidate.tier;
121
+ if (options.tier && options.tier !== tier)
122
+ throw new CliError("VALIDATION_ERROR", "metadata refresh tier differs from the installed package-set");
123
+ const document = loadConfigDocument(projectRoot);
124
+ const bootstrap = validateConfigDocument(document, undefined, { lenientCatalog: true });
125
+ if (bootstrap.kind !== "ok")
126
+ throw new CliError("VALIDATION_ERROR", bootstrap.kind === "invalid" ? bootstrap.message : "project config is missing");
127
+ if (bootstrap.config.tier !== tier || bootstrap.config.target !== installed.target)
128
+ throw new CliError("VALIDATION_ERROR", "metadata refresh config differs from the installed package-set; run update");
129
+ const plannedConfigDocument = JSON.stringify(document);
130
+ const expectedPins = installed.packages
131
+ .map(({ resourceKey, packageSha256, dependencies }) => ({ resourceKey, packageSha256, dependencies: [...dependencies] }))
132
+ .sort((a, b) => a.resourceKey.localeCompare(b.resourceKey, "en"));
133
+ let packagePlan;
134
+ const writes = Object.create(null);
135
+ const resolveResourceKeys = async (candidate, loadVerifiedPackage) => {
136
+ const identity = candidate.root;
137
+ if (identity.fullVersion !== installed.candidate.fullVersion ||
138
+ identity.tier !== tier ||
139
+ identity.packageSetSha256 !== installed.candidate.packageSetSha256 ||
140
+ identity.sourceCommit !== installed.candidate.sourceCommit ||
141
+ identity.generatorCommit !== installed.candidate.generatorCommit ||
142
+ sha256(canonicalBytes(identity)) !== installed.candidate.candidateRootSha256)
143
+ throw new CliError("VALIDATION_ERROR", "published package-set identity differs from the installed pin; refusing metadata refresh");
144
+ packagePlan = await planConfigPackagesFromMetadata(bootstrap.config, candidate, loadVerifiedPackage);
145
+ const actualPins = packagePlan.packages
146
+ .map(({ resourceKey, packageSha256, dependencies }) => ({ resourceKey, packageSha256, dependencies: [...dependencies] }))
147
+ .sort((a, b) => a.resourceKey.localeCompare(b.resourceKey, "en"));
148
+ if (JSON.stringify(actualPins) !== JSON.stringify(expectedPins))
149
+ throw new CliError("VALIDATION_ERROR", "configured package selection differs from the installed candidate; run update before metadata refresh");
150
+ return packagePlan.resourceKeys.filter((key) => key.startsWith(`metadata/${tier}/`));
151
+ };
152
+ const consume = (row, files) => {
153
+ if (row.kind !== "metadata")
154
+ throw new CliError("VALIDATION_ERROR", "metadata refresh selected a non-metadata package");
155
+ const mapped = packageTargetFiles(row, files, bootstrap.config.target);
156
+ for (const [path, bytes] of Object.entries(mapped.files)) {
157
+ const installPath = `.moeicons/package-metadata/${path}`;
158
+ if (Object.hasOwn(writes, installPath))
159
+ throw new CliError("VALIDATION_ERROR", "metadata package paths overlap");
160
+ writes[installPath] = bytes;
161
+ }
162
+ return Promise.resolve();
163
+ };
164
+ const cacheDir = deps.free.cacheDir ?? join(homedir(), ".moeicons", "cache");
165
+ const downloadOptions = {
166
+ cacheDir,
167
+ fullVersion: installed.candidate.fullVersion,
168
+ expectedPackageSetSha256: installed.candidate.packageSetSha256,
169
+ resolveResourceKeys,
170
+ consume,
171
+ ...(context.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: context.env.MOEICONS_API_BASE_URL } : {}),
172
+ ...(deps.fetch ? { fetchFn: deps.fetch } : {}),
173
+ ...(context.signal ? { signal: context.signal } : {}),
174
+ };
175
+ const report = tier === "free"
176
+ ? await downloadFreeVersionPackages({
177
+ ...downloadOptions,
178
+ ...(deps.free.fixtureBaseUrl ? { fixtureOrigin: deps.free.fixtureBaseUrl } : {}),
179
+ })
180
+ : await downloadProVersionPackages({
181
+ ...downloadOptions,
182
+ accessToken: await runAccessTokenUseCase(context, deps.auth),
183
+ });
184
+ if (!packagePlan)
185
+ throw new CliError("VALIDATION_ERROR", "metadata refresh did not produce a verified package plan");
186
+ const catalogJson = `${JSON.stringify(packagePlan.catalog, null, 2)}\n`;
187
+ const catalogSha256 = sha256Bytes(catalogJson);
188
+ writes[".moeicons/catalog.json"] = catalogJson;
189
+ const managedFiles = {
190
+ ...installed.managedFiles,
191
+ ...Object.fromEntries(Object.entries(writes).map(([path, content]) => [path, sha256Bytes(content)])),
192
+ };
193
+ const nextMetadata = {
194
+ ...installed,
195
+ catalogSha256,
196
+ installedAt: context.now().toISOString(),
197
+ managedFiles,
198
+ };
199
+ writes[".moeicons/install-metadata.json"] = serializePackageInstallMetadata(nextMetadata);
200
+ await withProjectLock(projectRoot, "update", () => {
201
+ if (deps.fs.readFileSync(join(projectRoot, ".moeicons", "install-metadata.json"), "utf8") !== rawMetadata)
202
+ throw new CliError("VALIDATION_ERROR", "installed package-set changed during metadata refresh; retry");
203
+ if (JSON.stringify(loadConfigDocument(projectRoot)) !== plannedConfigDocument)
204
+ throw new CliError("VALIDATION_ERROR", "config changed during metadata refresh; retry");
205
+ executeManagedReconcile(projectRoot, writes, [], deps.fs);
206
+ });
207
+ return {
208
+ projectRoot,
209
+ tier,
210
+ artifactVersion: installed.candidate.fullVersion,
211
+ metadataSha256: catalogSha256,
212
+ files: Object.keys(writes).sort((a, b) => a.localeCompare(b, "en")),
213
+ networkBytes: report.networkBytes + report.metadataNetworkBytes,
214
+ };
215
+ }
@@ -264,10 +264,24 @@ export function proSignedDownloadOptions(context, deps = {}) {
264
264
  export async function downloadProArtifact(context, auth, expected, deps = {}) {
265
265
  const fetchFn = deps.fetch ?? fetch;
266
266
  const descriptor = await fetchProDescriptor(context, auth, expected, { fetch: fetchFn });
267
+ const localFixture = descriptor.channel === "local-test" || resolveProDescriptorEndpoint(context.env).allowLoopback;
268
+ // CI FLOW RULE (S05): tier-wide resource bundles are historical schema only.
269
+ // Do not fall through to `descriptor.url` when selected packages are absent;
270
+ // that URL points at the full Pro code archive. New installs must resolve the
271
+ // small package directory and fetch only the configured group/tuple set.
272
+ if (!localFixture &&
273
+ descriptor.resources?.bundle) {
274
+ throw new CliError("VALIDATION_ERROR", "this release uses the retired full-library package format; install requires the small-package release protocol");
275
+ }
276
+ if (deps.selection?.config.downloadMode === "full" && !localFixture) {
277
+ throw new CliError("VALIDATION_ERROR", 'downloadMode="full" is disabled for published releases; resolve bounded style-group/target packages instead');
278
+ }
279
+ const localRangeFixture = descriptor.channel === "local-test" &&
280
+ resolveProDescriptorEndpoint(context.env).allowLoopback;
267
281
  if (deps.selection &&
268
282
  deps.selection.config.downloadMode !== "full" &&
269
283
  descriptor.resources &&
270
- descriptor.channel !== "local-test") {
284
+ localRangeFixture) {
271
285
  const selected = await downloadProSelected(context, auth, descriptor, deps.selection, deps);
272
286
  if (selected)
273
287
  return {
@@ -278,7 +292,10 @@ export async function downloadProArtifact(context, auth, expected, deps = {}) {
278
292
  };
279
293
  }
280
294
  if (deps.selection?.config.downloadMode === "icons")
281
- throw new CliError("VALIDATION_ERROR", "this release/backend does not support selected resources; set downloadMode=full or use a newer release");
295
+ throw new CliError("VALIDATION_ERROR", "this release/backend does not support bounded selected packages; refusing a full-library fallback");
296
+ if (!localFixture) {
297
+ throw new CliError("VALIDATION_ERROR", "published Pro release has no supported bounded package directory; refusing the full-library fallback");
298
+ }
282
299
  context.ui.note(deps.selection?.config.downloadMode === "full"
283
300
  ? "Download mode: full (explicitly configured)."
284
301
  : "Release does not advertise selected downloads; auto uses the verified full archive. Set downloadMode=icons to require selected downloads.", context.signal);
@@ -13,20 +13,26 @@ export declare function runProInstallUseCase(context: CommandContext, deps: {
13
13
  readonly totalBytes?: number;
14
14
  }) => void;
15
15
  }, expected: {
16
- readonly version: string;
17
- readonly descriptorSha256: string;
16
+ readonly version?: string;
17
+ readonly descriptorSha256?: string;
18
18
  readonly target?: Target;
19
19
  /** A-1b: allow a declared local-test candidate (never true for production). */
20
20
  readonly allowLocalTest?: boolean;
21
21
  }): Promise<{
22
22
  readonly projectRoot: string;
23
23
  readonly artifactVersion: string;
24
- readonly descriptorSha256: string;
24
+ readonly descriptorSha256?: string;
25
+ readonly candidateRootSha256?: string;
26
+ readonly packageSetSha256?: string;
25
27
  readonly catalogSha256: string;
26
28
  readonly artifactBytes: number;
27
- readonly downloadMode: "icons" | "full";
29
+ readonly downloadMode: "icons" | "full" | "packages";
28
30
  readonly downloadNotes: readonly string[];
29
31
  readonly networkBytes?: number;
30
32
  readonly selectedFiles?: number;
33
+ readonly packageCount?: number;
34
+ readonly cacheHits?: number;
35
+ readonly metadataCacheHits?: number;
36
+ readonly transferRetryCount?: number;
31
37
  }>;
32
38
  export {};
@@ -14,6 +14,8 @@ import { catalog as bundledCatalog, parseCatalog } from "../catalog/catalog.js";
14
14
  import { resolveBitmapTuples, resolveConfiguredBitmapShards } from "./bitmap-shard-resolver.js";
15
15
  import { mergeBitmapShardCacheManifest, readBitmapShardCacheManifest, writeBitmapShardCacheManifest, } from "./bitmap-shard-cache.js";
16
16
  import { allowLocalTestFromEnv } from "./local-test-env.js";
17
+ import { runSmallPackageInstall } from "./small-package-install.js";
18
+ import { fetchProStablePackageSet } from "./pro-small-package-download.js";
17
19
  function toCacheIo(fs_) {
18
20
  return {
19
21
  // Shard cache keys are nested several levels deep; the CacheIo contract
@@ -64,6 +66,51 @@ export async function runProInstallUseCase(context, deps, expected) {
64
66
  if (target !== bootstrap.config.target) {
65
67
  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
68
  }
69
+ if (expected.allowLocalTest !== true) {
70
+ const accessToken = await runAccessTokenUseCase(context, deps.auth);
71
+ const identity = await fetchProStablePackageSet({
72
+ accessToken,
73
+ ...(context.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: context.env.MOEICONS_API_BASE_URL } : {}),
74
+ ...(deps.fetch ? { fetchFn: deps.fetch } : {}),
75
+ ...(context.signal ? { signal: context.signal } : {}),
76
+ });
77
+ if (expected.version && expected.version !== identity.fullVersion) {
78
+ throw new CliError("VALIDATION_ERROR", "published Pro package-set changed after version selection; retry the install");
79
+ }
80
+ const installed = await runSmallPackageInstall({
81
+ projectRoot: project.root,
82
+ fullVersion: identity.fullVersion,
83
+ expectedPackageSetSha256: identity.packageSetSha256,
84
+ ...(context.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: context.env.MOEICONS_API_BASE_URL } : {}),
85
+ config: bootstrap.config,
86
+ plannedConfigDocument: JSON.stringify(document),
87
+ fs: deps.fs,
88
+ cacheDir: resolveCacheDir(context.env),
89
+ now: context.now,
90
+ proAccessToken: accessToken,
91
+ ...(context.signal ? { signal: context.signal } : {}),
92
+ ...(deps.fetch ? { fetchFn: deps.fetch } : {}),
93
+ });
94
+ return {
95
+ projectRoot: project.root,
96
+ artifactVersion: installed.artifactVersion,
97
+ candidateRootSha256: installed.candidateRootSha256,
98
+ packageSetSha256: installed.packageSetSha256,
99
+ catalogSha256: installed.catalogSha256,
100
+ artifactBytes: installed.artifactBytes,
101
+ downloadMode: "packages",
102
+ downloadNotes: installed.fallbacks.map((fallback) => `Theme fallback: ${fallback}`),
103
+ networkBytes: installed.networkBytes + identity.networkBytes,
104
+ selectedFiles: installed.selectedFiles,
105
+ packageCount: installed.packageCount,
106
+ cacheHits: installed.cacheHits,
107
+ metadataCacheHits: installed.metadataCacheHits,
108
+ transferRetryCount: installed.transferRetryCount,
109
+ };
110
+ }
111
+ if (!expected.version || !expected.descriptorSha256) {
112
+ throw new CliError("VALIDATION_ERROR", "local Pro test install requires an exact legacy fixture pin");
113
+ }
67
114
  const downloaded = await downloadProArtifact(context, deps.auth, {
68
115
  version: expected.version,
69
116
  descriptorSha256: expected.descriptorSha256,
@@ -48,6 +48,8 @@ export declare function runProPredownloadUseCase(context: CommandContext, auth:
48
48
  readonly fetch?: typeof fetch;
49
49
  readonly allowedProHosts?: readonly string[];
50
50
  readonly force?: boolean;
51
+ /** Explicitly isolate the retired aggregate archive path to test fixtures. */
52
+ readonly allowLocalTest?: boolean;
51
53
  readonly onProgress?: (event: {
52
54
  readonly downloadedBytes: number;
53
55
  readonly totalBytes?: number;
@@ -65,11 +67,32 @@ export interface ProWarmupDeps {
65
67
  readonly descriptorSha256: string;
66
68
  readonly metadataSha256: string;
67
69
  readonly catalogSha256: string;
70
+ /** Explicit test-fixture opt-in for the retired legacy shard route. */
71
+ readonly allowLocalTest?: boolean;
68
72
  readonly fetch?: typeof fetch;
69
73
  readonly allowedProHosts?: readonly string[];
70
74
  readonly onPlan?: (plan: WarmupPlan) => void;
71
75
  readonly onProgress?: (event: BitmapShardFetchProgress) => void;
72
76
  }
77
+ export interface ProPackageWarmupPlan {
78
+ readonly version: string;
79
+ readonly packageSetSha256: string;
80
+ readonly groups: number;
81
+ readonly packages: number;
82
+ readonly compressedBytes: number;
83
+ }
84
+ export interface ProPackageWarmupResult extends ProPackageWarmupPlan {
85
+ readonly downloaded: number;
86
+ readonly reused: number;
87
+ readonly networkBytes: number;
88
+ readonly metadataNetworkBytes: number;
89
+ }
90
+ /** Warm the explicit default preview tuple (128-WebP) as separate package objects. */
91
+ export declare function runProPreviewPackageWarmupUseCase(context: CommandContext, auth: AuthUseCaseDependencies, deps?: {
92
+ readonly fetch?: typeof fetch;
93
+ readonly apiBaseUrl?: string;
94
+ readonly onPlan?: (plan: ProPackageWarmupPlan) => Promise<void> | void;
95
+ }): Promise<ProPackageWarmupResult>;
73
96
  /**
74
97
  * W2-A: after the Pro code + metadata are cached, warm the default bitmap shard
75
98
  * set (every bitmap style group's 128/webp shard) into the global cache. The
@@ -14,6 +14,8 @@ import { parseCatalog } from "../catalog/catalog.js";
14
14
  import { runAccessTokenUseCase } from "./auth.js";
15
15
  import { PRO_DOWNLOAD_HOSTS } from "./pro-download.js";
16
16
  import { resolveBitmapShardDescriptorEndpoint } from "./bitmap-shard-download.js";
17
+ import { downloadProStablePackages } from "./pro-small-package-download.js";
18
+ import { planSelectedPackages } from "./small-package-selection.js";
17
19
  import { warmDefaultBitmapShards, } from "./bitmap-shard-cache.js";
18
20
  function cacheDir(env) {
19
21
  return env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
@@ -91,6 +93,9 @@ export async function proResourceState(context, auth, deps = {}) {
91
93
  * login or from the home screen, without installing anything into a project.
92
94
  */
93
95
  export async function runProPredownloadUseCase(context, auth, deps = {}) {
96
+ if (deps.allowLocalTest !== true) {
97
+ throw new CliError('VALIDATION_ERROR', 'aggregate Pro archive pre-download is retired; use package-set project installs or preview package warm-up');
98
+ }
94
99
  const versions = await fetchLibraryVersions({ ...(deps.fetch ? { fetch: deps.fetch } : {}), signal: context.signal, env: context.env });
95
100
  if (!versions.pro)
96
101
  throw new CliError("NOT_FOUND", "no pro release is published");
@@ -136,6 +141,49 @@ export async function runProPredownloadUseCase(context, auth, deps = {}) {
136
141
  metadataBytes,
137
142
  };
138
143
  }
144
+ /** Warm the explicit default preview tuple (128-WebP) as separate package objects. */
145
+ export async function runProPreviewPackageWarmupUseCase(context, auth, deps = {}) {
146
+ const accessToken = await runAccessTokenUseCase(context, auth);
147
+ const cache = cacheDir(context.env);
148
+ let plan;
149
+ const report = await downloadProStablePackages({
150
+ accessToken,
151
+ cacheDir: cache,
152
+ ...(deps.apiBaseUrl ? { apiBaseUrl: deps.apiBaseUrl } : context.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: context.env.MOEICONS_API_BASE_URL } : {}),
153
+ ...(deps.fetch ? { fetchFn: deps.fetch } : {}),
154
+ ...(context.signal ? { signal: context.signal } : {}),
155
+ resolveResourceKeys: async (candidate) => {
156
+ const selected = candidate.rows.filter((row) => row.kind === 'bitmap' && row.tier === 'pro'
157
+ && row.variant?.width === 128 && row.variant.height === 128 && row.variant.format === 'webp')
158
+ .sort((a, b) => a.resourceKey.localeCompare(b.resourceKey, 'en'));
159
+ if (selected.length === 0)
160
+ throw new CliError('NOT_FOUND', 'the current Pro package set has no 128-WebP preview packages');
161
+ const selectedRows = planSelectedPackages(candidate, selected.map((row) => row.resourceKey));
162
+ const compressedBytes = selectedRows.reduce((sum, row) => sum + row.compressedBytes, 0);
163
+ if (!Number.isSafeInteger(compressedBytes) || compressedBytes > 4 * 1024 * 1024 * 1024)
164
+ throw new CliError('VALIDATION_ERROR', 'Pro preview warm-up exceeds the 4 GiB package budget');
165
+ plan = {
166
+ version: candidate.root.fullVersion,
167
+ packageSetSha256: candidate.root.packageSetSha256,
168
+ groups: new Set(selected.map((row) => row.styleGroupId)).size,
169
+ packages: selectedRows.length,
170
+ compressedBytes,
171
+ };
172
+ await deps.onPlan?.(plan);
173
+ return selected.map((row) => row.resourceKey);
174
+ },
175
+ consume: () => Promise.resolve(),
176
+ });
177
+ if (!plan)
178
+ throw new CliError('VALIDATION_ERROR', 'Pro preview warm-up did not produce a candidate plan');
179
+ return {
180
+ ...plan,
181
+ downloaded: report.packageCount - report.cacheHits,
182
+ reused: report.cacheHits,
183
+ networkBytes: report.networkBytes,
184
+ metadataNetworkBytes: report.metadataNetworkBytes,
185
+ };
186
+ }
139
187
  function cacheIo() {
140
188
  return {
141
189
  mkdirSync: (path) => mkdirSync(path, { recursive: true }),
@@ -154,6 +202,9 @@ function cacheIo() {
154
202
  * second artifact download is needed.
155
203
  */
156
204
  export async function runProResourceWarmupUseCase(context, auth, deps) {
205
+ if (deps.allowLocalTest !== true) {
206
+ throw new CliError('VALIDATION_ERROR', 'legacy Pro bitmap shard warm-up is retired; use package-set preview package warm-up');
207
+ }
157
208
  const dir = cacheDir(context.env);
158
209
  const metadataPath = metadataCachePath(dir, deps.version, deps.metadataSha256);
159
210
  if (!existsSync(metadataPath)) {
@@ -14,6 +14,12 @@ import { homedir } from "node:os";
14
14
  export async function downloadProSelected(context, auth, descriptor, selection, deps) {
15
15
  const fetchFn = deps.fetch ?? fetch;
16
16
  const endpointInfo = resolveProDescriptorEndpoint(context.env);
17
+ // CI FLOW RULE (S05): this implementation reads the retired tier-wide
18
+ // resource-index / `.bin` bundle. Keep it reachable only from an explicitly
19
+ // declared local-test descriptor served by a loopback endpoint; a descriptor
20
+ // label alone must never reopen the production reader.
21
+ if (!endpointInfo.allowLoopback || descriptor.channel !== "local-test")
22
+ throw new CliError("VALIDATION_ERROR", "legacy Pro resource-bundle downloads require a local-test descriptor and loopback endpoint");
17
23
  const endpoint = new URL(endpointInfo.url.replace(/\/artifact-descriptor$/, "/resource-descriptor"));
18
24
  if (endpoint.origin !== "https://api.moeicons.com" && !endpointInfo.allowLoopback)
19
25
  throw new CliError("VALIDATION_ERROR", "resource descriptor endpoint must be the trusted API or a test loopback");
@@ -150,7 +156,11 @@ export async function downloadProSelected(context, auth, descriptor, selection,
150
156
  catch (error) {
151
157
  if (!expired(error) || attempt === 2)
152
158
  throw error;
153
- const renewed = await fetchProDescriptor(context, auth, { version: descriptor.version, descriptorSha256: descriptor.descriptorSha256 }, deps);
159
+ const renewed = await fetchProDescriptor(context, auth, {
160
+ version: descriptor.version,
161
+ descriptorSha256: descriptor.descriptorSha256,
162
+ ...(descriptor.channel === "local-test" ? { allowLocalTest: true } : {}),
163
+ }, deps);
154
164
  if (renewed.sha256 !== descriptor.sha256 ||
155
165
  !renewed.metadata ||
156
166
  renewed.metadata.sha256 !== originalMetadata.sha256 ||
@@ -161,7 +171,7 @@ export async function downloadProSelected(context, auth, descriptor, selection,
161
171
  }
162
172
  if (!metadataBytes)
163
173
  throw new CliError("NETWORK_ERROR", "metadata URL renewal failed");
164
- const metadata = extractProMetadata(metadataBytes, descriptor.catalogSha256, descriptor.version);
174
+ const metadata = extractProMetadata(metadataBytes, descriptor.catalogSha256, descriptor.version, descriptor.channel === "local-test");
165
175
  const catalog = parseCatalog(JSON.parse(metadata.catalogJson));
166
176
  const strict = validateConfigDocument(selection.document, catalog);
167
177
  if (strict.kind !== "ok")
@@ -0,0 +1,61 @@
1
+ import { type PackageRow } from './small-pack/candidate-set.cjs';
2
+ import { type CandidateIdentity, type SelectedCandidate } from './small-package-selection.js';
3
+ export interface ProPackageSetIdentity extends CandidateIdentity {
4
+ readonly schemaVersion: 1;
5
+ readonly tier: 'pro';
6
+ readonly candidateRootSha256: string;
7
+ readonly candidateRootBytes: number;
8
+ readonly updatedAt: string;
9
+ readonly networkBytes: number;
10
+ }
11
+ export interface ProSmallPackageOptions {
12
+ readonly expected: CandidateIdentity & {
13
+ readonly tier: 'pro';
14
+ };
15
+ readonly resourceKeys?: readonly string[];
16
+ readonly resolveResourceKeys?: (candidate: SelectedCandidate, loadVerifiedPackage: (row: PackageRow) => Promise<Readonly<Record<string, Uint8Array>>>) => Promise<readonly string[]>;
17
+ readonly cacheDir: string;
18
+ readonly consume: (row: PackageRow, files: Readonly<Record<string, Uint8Array>>) => Promise<void>;
19
+ readonly accessToken: string;
20
+ readonly signal?: AbortSignal;
21
+ readonly fetchFn?: typeof fetch;
22
+ readonly apiBaseUrl?: string;
23
+ readonly expectedRootSha256?: string;
24
+ readonly expectedRootBytes?: number;
25
+ }
26
+ export declare function fetchProPackageSetIdentity(options: {
27
+ readonly version: string;
28
+ readonly packageSetSha256?: string;
29
+ readonly accessToken: string;
30
+ readonly apiBaseUrl?: string;
31
+ readonly fetchFn?: typeof fetch;
32
+ readonly signal?: AbortSignal;
33
+ }): Promise<ProPackageSetIdentity>;
34
+ export declare function fetchProStablePackageSet(options: Omit<Parameters<typeof fetchProPackageSetIdentity>[0], 'version' | 'packageSetSha256'>): Promise<ProPackageSetIdentity>;
35
+ export declare function downloadProSelectedPackages(options: ProSmallPackageOptions): Promise<{
36
+ packageCount: number;
37
+ networkBytes: number;
38
+ cacheHits: number;
39
+ metadataNetworkBytes: number;
40
+ metadataCacheHits: number;
41
+ transferRetryCount: number;
42
+ }>;
43
+ export declare function downloadProStablePackages(options: Omit<ProSmallPackageOptions, 'expected' | 'expectedRootSha256' | 'expectedRootBytes'>): Promise<{
44
+ packageCount: number;
45
+ networkBytes: number;
46
+ cacheHits: number;
47
+ metadataNetworkBytes: number;
48
+ metadataCacheHits: number;
49
+ transferRetryCount: number;
50
+ }>;
51
+ export declare function downloadProVersionPackages(options: Omit<ProSmallPackageOptions, 'expected' | 'expectedRootSha256' | 'expectedRootBytes'> & {
52
+ readonly fullVersion: string;
53
+ readonly expectedPackageSetSha256: string;
54
+ }): Promise<{
55
+ packageCount: number;
56
+ networkBytes: number;
57
+ cacheHits: number;
58
+ metadataNetworkBytes: number;
59
+ metadataCacheHits: number;
60
+ transferRetryCount: number;
61
+ }>;