@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
@@ -0,0 +1,25 @@
1
+ import { type IconCatalog } from '../catalog/catalog.js';
2
+ import type { MoeiconsConfigFile } from '../project/config.js';
3
+ import { type SelectedCandidate } from './small-package-selection.js';
4
+ import type { PackageRow } from './small-pack/candidate-set.cjs';
5
+ export declare function planConfigPackages(config: MoeiconsConfigFile, catalog: IconCatalog, candidate: SelectedCandidate): {
6
+ resourceKeys: readonly string[];
7
+ packages: readonly PackageRow[];
8
+ fallbacks: readonly string[];
9
+ };
10
+ /** Bootstrap only the scoped metadata needed by this project's themes, then
11
+ * plan exact content units against the resulting candidate-bound catalog. The
12
+ * loader must return bytes from the selected-package verifier. */
13
+ export declare function planConfigPackagesFromMetadata(config: MoeiconsConfigFile, candidate: SelectedCandidate, loadVerifiedPackage: (row: PackageRow) => Promise<Readonly<Record<string, Uint8Array>>>): Promise<{
14
+ catalog: IconCatalog;
15
+ resourceKeys: readonly string[];
16
+ packages: readonly PackageRow[];
17
+ fallbacks: readonly string[];
18
+ }>;
19
+ export interface PackageTargetFiles {
20
+ readonly area: 'target' | 'assets' | 'metadata';
21
+ readonly files: Readonly<Record<string, Uint8Array>>;
22
+ }
23
+ /** Map one already verified unit; never accumulate multiple packages or invent
24
+ * an all-tier archive identity. Callers stage these files transactionally. */
25
+ export declare function packageTargetFiles(row: PackageRow, files: Readonly<Record<string, Uint8Array>>, target: MoeiconsConfigFile['target']): PackageTargetFiles;
@@ -0,0 +1,156 @@
1
+ import { findCatalogStyleGroup, parseCatalog } from '../catalog/catalog.js';
2
+ import { resolveIconTheme } from './icon-selection.js';
3
+ import { resolveResourceVariant } from './resource-variant.js';
4
+ import { planSelectedPackages } from './small-package-selection.js';
5
+ function check(ok, message) { if (!ok)
6
+ throw new Error(`config package plan: ${message}`); }
7
+ export function planConfigPackages(config, catalog, candidate) {
8
+ check(config.icons.length > 0 && Object.keys(config.themes).length > 0, 'registered icons and themes required');
9
+ check(config.downloadMode !== 'full', 'published package installs cannot use downloadMode=full');
10
+ check(config.tier === candidate.root.tier, 'config tier differs from pinned candidate');
11
+ check(catalog.sourceVersion === candidate.root.fullVersion && catalog.sourceCommit === candidate.root.sourceCommit && catalog.generatorCommit === candidate.root.generatorCommit, 'catalog differs from pinned candidate identity');
12
+ const keys = new Set(), fallbacks = new Set();
13
+ for (const iconId of config.icons)
14
+ for (const requested of Object.keys(config.themes)) {
15
+ const actual = resolveIconTheme(config, catalog, requested, iconId);
16
+ check(actual, `no selected variant for ${requested}/${iconId}`);
17
+ if (actual !== requested)
18
+ fallbacks.add(`${iconId}: ${requested} -> ${actual}`);
19
+ const theme = config.themes[actual];
20
+ check(theme, 'resolved theme missing');
21
+ const group = findCatalogStyleGroup(theme.styleGroup, catalog);
22
+ check(group && group.tiers.includes(config.tier), 'group is unavailable in selected tier');
23
+ keys.add(`metadata/${config.tier}/${group.id}`);
24
+ if (group.type === 'bitmap') {
25
+ check(config.target !== 'vanilla', 'Vanilla does not support bitmap icons');
26
+ const variant = resolveResourceVariant(group.id, { ...(theme.format ? { format: theme.format } : {}), ...(theme.imageSize ? { imageSize: theme.imageSize } : {}) });
27
+ check(group.formats.includes(variant.format) && group.imageSizes.includes(variant.imageSize) && group.variants?.includes(variant.resourceVariantId), 'bitmap tuple is not declared by pinned catalog');
28
+ keys.add(`bitmap/${config.tier}/${group.id}/${variant.imageSize}x${variant.imageSize}/${variant.format}`);
29
+ }
30
+ else {
31
+ if (config.target === 'assets' || config.target === 'vanilla')
32
+ keys.add(`svg/${config.tier}/${group.id}`);
33
+ if (config.target !== 'assets')
34
+ keys.add(`code/${config.tier}/${config.target}/${group.id}`);
35
+ }
36
+ }
37
+ const resourceKeys = [...keys].sort();
38
+ return { resourceKeys, packages: planSelectedPackages(candidate, resourceKeys), fallbacks: [...fallbacks].sort() };
39
+ }
40
+ function stable(value) {
41
+ if (Array.isArray(value))
42
+ return value.map(stable);
43
+ if (value && typeof value === 'object')
44
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b, 'en')).map(([key, item]) => [key, stable(item)]));
45
+ return value;
46
+ }
47
+ function stringArray(value) {
48
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
49
+ }
50
+ /** Bootstrap only the scoped metadata needed by this project's themes, then
51
+ * plan exact content units against the resulting candidate-bound catalog. The
52
+ * loader must return bytes from the selected-package verifier. */
53
+ export async function planConfigPackagesFromMetadata(config, candidate, loadVerifiedPackage) {
54
+ check(config.icons.length > 0 && Object.keys(config.themes).length > 0, 'registered icons and themes required');
55
+ check(config.downloadMode !== 'full', 'published package installs cannot use downloadMode=full');
56
+ check(config.tier === candidate.root.tier, 'config tier differs from pinned candidate');
57
+ const groupIds = [...new Set(Object.values(config.themes)
58
+ .filter(theme => theme.icons === undefined || theme.icons.some(icon => config.icons.includes(icon)))
59
+ .map(theme => theme.styleGroup))].sort();
60
+ check(groupIds.length > 0, 'configured themes required');
61
+ const groups = new Map();
62
+ const icons = new Map();
63
+ for (const groupId of groupIds) {
64
+ const row = candidate.rows.find(item => item.resourceKey === `metadata/${config.tier}/${groupId}`);
65
+ check(row && row.kind === 'metadata' && row.styleGroupId === groupId && row.tier === config.tier, `scoped metadata missing for ${groupId}`);
66
+ const files = await loadVerifiedPackage(row);
67
+ const bytes = files['metadata/catalog.json'];
68
+ check(bytes, `scoped catalog missing for ${groupId}`);
69
+ let raw;
70
+ try {
71
+ raw = JSON.parse(Buffer.from(bytes).toString('utf8'));
72
+ }
73
+ catch {
74
+ throw new Error(`config package plan: invalid scoped catalog for ${groupId}`);
75
+ }
76
+ check(raw !== null && typeof raw === 'object' && !Array.isArray(raw), `scoped catalog object required for ${groupId}`);
77
+ const scoped = raw;
78
+ check(Object.keys(scoped).sort().join(',') === 'icons,schemaVersion,styleGroups' && scoped.schemaVersion === 1 && Array.isArray(scoped.styleGroups) && scoped.styleGroups.length === 1 && Array.isArray(scoped.icons), `scoped catalog schema/scope invalid for ${groupId}`);
79
+ const group = scoped.styleGroups[0];
80
+ check(group && group.id === groupId && !groups.has(groupId), `scoped style group differs for ${groupId}`);
81
+ groups.set(groupId, group);
82
+ check(scoped.icons.length > 0, `scoped catalog has no icons for ${groupId}`);
83
+ for (const value of scoped.icons) {
84
+ check(value !== null && typeof value === 'object' && !Array.isArray(value), `invalid scoped icon in ${groupId}`);
85
+ const icon = value;
86
+ check(typeof icon.id === 'string' && Array.isArray(icon.availableIn) && icon.availableIn.length === 1 && icon.availableIn[0] === groupId, `scoped icon crosses group boundary in ${groupId}`);
87
+ if (icon.variants !== undefined)
88
+ check(stringArray(icon.variants), `invalid scoped icon variants in ${groupId}`);
89
+ const previous = icons.get(icon.id);
90
+ if (!previous) {
91
+ icons.set(icon.id, { ...icon, availableIn: [groupId], ...(stringArray(icon.variants) ? { variants: [...icon.variants] } : {}) });
92
+ continue;
93
+ }
94
+ const stripScope = (entry) => Object.fromEntries(Object.entries(entry).filter(([key]) => key !== 'availableIn' && key !== 'variants'));
95
+ check(JSON.stringify(stable(stripScope(previous))) === JSON.stringify(stable(stripScope(icon))), `icon metadata conflicts across scoped catalogs: ${icon.id}`);
96
+ previous.availableIn = [...new Set([...previous.availableIn, groupId])].sort();
97
+ if (stringArray(icon.variants))
98
+ previous.variants = [...new Set([...(stringArray(previous.variants) ? previous.variants : []), ...icon.variants])].sort();
99
+ }
100
+ }
101
+ const catalog = parseCatalog({
102
+ schemaVersion: 1,
103
+ catalogVersion: candidate.root.fullVersion,
104
+ sourceVersion: candidate.root.fullVersion,
105
+ sourceCommit: candidate.root.sourceCommit,
106
+ generatorCommit: candidate.root.generatorCommit,
107
+ styleGroups: [...groups.values()],
108
+ icons: [...icons.values()],
109
+ });
110
+ return { catalog, ...planConfigPackages(config, catalog, candidate) };
111
+ }
112
+ /** Map one already verified unit; never accumulate multiple packages or invent
113
+ * an all-tier archive identity. Callers stage these files transactionally. */
114
+ export function packageTargetFiles(row, files, target) {
115
+ check(Object.hasOwn(files, 'manifest.json'), 'verified unit manifest required');
116
+ const output = Object.create(null);
117
+ let area;
118
+ if (row.kind === 'code' || row.kind === 'shared') {
119
+ check(row.target === target, `package target mismatch for ${row.resourceKey}: expected ${target}, received ${row.target}`);
120
+ area = 'target';
121
+ }
122
+ else if (row.kind === 'svg' || row.kind === 'bitmap') {
123
+ check(row.kind !== 'svg' || target === 'assets' || target === 'vanilla', 'raw SVG unit is not required by compiled target');
124
+ check(target !== 'vanilla' || row.kind !== 'bitmap', 'Vanilla bitmap unsupported');
125
+ area = 'assets';
126
+ }
127
+ else {
128
+ check(row.kind === 'metadata', 'unsupported installation package kind');
129
+ area = 'metadata';
130
+ }
131
+ for (const [name, bytes] of Object.entries(files)) {
132
+ if (name === 'manifest.json')
133
+ continue;
134
+ check(!/[\\:]/.test(name) && name.split('/').every(part => part && part !== '.' && part !== '..'), 'unsafe unit path');
135
+ let destination;
136
+ if (row.kind === 'shared')
137
+ destination = name;
138
+ else if (row.kind === 'code' || row.kind === 'svg') {
139
+ check(row.styleGroupId, 'group scope missing');
140
+ destination = `${row.styleGroupId}/${name}`;
141
+ }
142
+ else if (row.kind === 'metadata') {
143
+ check(row.styleGroupId && name.startsWith('metadata/'), 'metadata scope missing');
144
+ destination = `${row.styleGroupId}/${name.slice(9)}`;
145
+ }
146
+ else {
147
+ check(row.styleGroupId && row.variant && name.startsWith('icons/'), 'bitmap tuple scope missing');
148
+ const variant = resolveResourceVariant(row.styleGroupId, { format: row.variant.format, imageSize: row.variant.width });
149
+ destination = `${variant.resourceVariantId}/${name.slice(6)}`;
150
+ }
151
+ check(!Object.hasOwn(output, destination), 'duplicate target file');
152
+ output[destination] = bytes;
153
+ }
154
+ check(Object.keys(output).length > 0, 'empty installation unit');
155
+ return { area, files: output };
156
+ }
@@ -4,6 +4,7 @@ import { diagnoseProject } from "../project/anchors/diagnose.js";
4
4
  import { applyPlannedChanges } from "../project/anchors/apply.js";
5
5
  import { withProjectLockSync } from "../project/project-lock.js";
6
6
  import { loadInstalledCatalogState } from "./generate.js";
7
+ import { readInstalledResourceState } from "../project/install-metadata.js";
7
8
  const STATUS_LABEL = {
8
9
  ok: "OK",
9
10
  missing: "MISSING",
@@ -92,6 +93,9 @@ export function checkRequiresFix(report) {
92
93
  return report.anchors.some((a) => required.includes(a.kind) && a.status !== "ok" && a.status !== "not-required");
93
94
  }
94
95
  function diagnosticCatalog(cwd) {
96
+ const installed = readInstalledResourceState(cwd);
97
+ if (installed.kind === "invalid")
98
+ throw new Error(installed.message);
95
99
  const state = loadInstalledCatalogState(cwd, { readFileSync, existsSync });
96
100
  if (state.status === "invalid")
97
101
  throw new Error(state.message);
@@ -12,7 +12,9 @@ import { cacheArtifact } from "./cache.js";
12
12
  import { extractAndVerifyMetadataArchive } from "../metadata/archive.js";
13
13
  import { assertLocalCandidateAllowed, cacheKey, FREE_DOWNLOAD_HOSTS, githubReleaseAssetUrl, parseReleaseDescriptor, parseSha256Sidecar, } from "./release-descriptor.js";
14
14
  export const DESCRIPTOR_MAX_BYTES = 256 * 1024;
15
- export const ARTIFACT_MAX_BYTES = 64 * 1024 * 1024;
15
+ // Full release archives are retired for published versions. This bound only
16
+ // preserves small local-test fixtures until package-scoped target delivery lands.
17
+ export const ARTIFACT_MAX_BYTES = 128 * 1024 * 1024;
16
18
  export const METADATA_MAX_BYTES = 8 * 1024 * 1024;
17
19
  // GitHub Release downloads can be slow on constrained or distant networks.
18
20
  // Keep the operation bounded, but allow enough time for the 64 MiB Free limit.
@@ -342,6 +344,27 @@ export async function downloadFreeRelease(io, sourceVersion, selection) {
342
344
  if (!fetched.ok)
343
345
  return fetched;
344
346
  const { descriptor, descriptorSha256, tag } = fetched;
347
+ // CI FLOW RULE (S05): the old Free descriptor points at a tier-wide archive.
348
+ // Never silently download that complete library when the group package
349
+ // directory or selected package support is missing.
350
+ const localFixture = Boolean(io.fixtureDir || io.fixtureBaseUrl || descriptor.channel === "local-test");
351
+ if (!localFixture && (descriptor.schemaVersion === undefined || descriptor.schemaVersion < 5)) {
352
+ return {
353
+ ok: false,
354
+ reason: "validation",
355
+ message: "this release uses the retired full-library package format; install requires the small-package release protocol",
356
+ };
357
+ }
358
+ // CI FLOW RULE (S05): a current-looking descriptor must not make the CLI
359
+ // fetch the retired tier-wide index/`.bin`. Published installs require the
360
+ // candidate-set package directory and its bounded dependency closure.
361
+ if (!localFixture && descriptor.free.resources !== undefined) {
362
+ return {
363
+ ok: false,
364
+ reason: "validation",
365
+ message: "this release advertises the retired tier-wide resource bundle; refusing to download it",
366
+ };
367
+ }
345
368
  const metadataRef = descriptor.free.metadata;
346
369
  if (!metadataRef) {
347
370
  return {
@@ -479,15 +502,22 @@ export async function downloadFreeRelease(io, sourceVersion, selection) {
479
502
  };
480
503
  }
481
504
  }
505
+ if (!localFixture) {
506
+ return {
507
+ ok: false,
508
+ reason: "validation",
509
+ message: "published Free release has no supported bounded package directory; refusing the full-library fallback",
510
+ };
511
+ }
482
512
  if (mode === "icons")
483
513
  return {
484
514
  ok: false,
485
515
  reason: "validation",
486
- message: "this release has no selected-resource index; choose a newer release or explicitly set downloadMode=full",
516
+ message: "this local fixture has no selected-resource index",
487
517
  };
488
518
  selection?.onPlan?.(mode === "full"
489
- ? "Download mode: full (explicitly configured); the complete target archive will be verified before selection."
490
- : "This release does not advertise selected downloads; auto uses the verified full archive. Set downloadMode=icons to require selected downloads.");
519
+ ? "Download mode: full (local fixture only)."
520
+ : "Using a local-test fixture archive.");
491
521
  const artifactCache = cachePath(io, descriptor.fullVersion, descriptor.free.sha256);
492
522
  if (io.existsSync(artifactCache)) {
493
523
  const cached = io.readFileSync(artifactCache);
@@ -0,0 +1,67 @@
1
+ import { type PackageRow } from './small-pack/candidate-set.cjs';
2
+ import { type CandidateIdentity, type SelectedCandidate } from './small-package-selection.js';
3
+ export interface FreeSmallPackageOptions {
4
+ expected: CandidateIdentity;
5
+ resourceKeys?: readonly string[];
6
+ resolveResourceKeys?: (candidate: SelectedCandidate, loadVerifiedPackage: (row: PackageRow) => Promise<Readonly<Record<string, Uint8Array>>>) => Promise<readonly string[]>;
7
+ cacheDir: string;
8
+ consume: (row: PackageRow, files: Readonly<Record<string, Uint8Array>>) => Promise<void>;
9
+ signal?: AbortSignal;
10
+ fetchFn?: typeof fetch;
11
+ /** Explicit loopback fixture origin, never a published remote override. */
12
+ fixtureOrigin?: string;
13
+ /** Bound by a public immutable version pointer when resolving by version. */
14
+ expectedRootSha256?: string;
15
+ expectedRootBytes?: number;
16
+ /** Public Worker API base; production defaults to https://api.moeicons.com. */
17
+ apiBaseUrl?: string;
18
+ }
19
+ export declare function downloadFreeSelectedPackages(options: FreeSmallPackageOptions): Promise<{
20
+ packageCount: number;
21
+ networkBytes: number;
22
+ cacheHits: number;
23
+ metadataNetworkBytes: number;
24
+ metadataCacheHits: number;
25
+ transferRetryCount: number;
26
+ }>;
27
+ export interface FreeSmallPackageVersionOptions extends Omit<FreeSmallPackageOptions, 'expected' | 'expectedRootSha256' | 'expectedRootBytes'> {
28
+ fullVersion: string;
29
+ expectedPackageSetSha256?: string;
30
+ }
31
+ /** Resolve a version through its immutable public pointer, then use the same
32
+ * exact-pin package reader. The pointer is cached only after the candidate
33
+ * root and all requested units have passed verification. */
34
+ export declare function downloadFreeVersionPackages(options: FreeSmallPackageVersionOptions): Promise<{
35
+ packageCount: number;
36
+ networkBytes: number;
37
+ cacheHits: number;
38
+ metadataNetworkBytes: number;
39
+ metadataCacheHits: number;
40
+ transferRetryCount: number;
41
+ }>;
42
+ export interface FreePackageSetIdentity {
43
+ readonly schemaVersion: 1;
44
+ readonly tier: 'free';
45
+ readonly fullVersion: string;
46
+ readonly sourceCommit: string;
47
+ readonly generatorCommit: string;
48
+ readonly packageSetSha256: string;
49
+ readonly candidateRootSha256: string;
50
+ readonly candidateRootBytes: number;
51
+ readonly updatedAt: string;
52
+ readonly networkBytes: number;
53
+ }
54
+ export declare function fetchFreeStablePackageSet(options?: {
55
+ apiBaseUrl?: string;
56
+ fixtureOrigin?: string;
57
+ fetchFn?: typeof fetch;
58
+ signal?: AbortSignal;
59
+ }): Promise<FreePackageSetIdentity>;
60
+ export declare function downloadFreeStablePackages(options: Omit<FreeSmallPackageOptions, 'expected' | 'expectedRootSha256' | 'expectedRootBytes'>): Promise<{
61
+ packageCount: number;
62
+ networkBytes: number;
63
+ cacheHits: number;
64
+ metadataNetworkBytes: number;
65
+ metadataCacheHits: number;
66
+ transferRetryCount: number;
67
+ }>;