@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
@@ -1,8 +1,48 @@
1
1
  import { join, relative, resolve, dirname, isAbsolute, sep } from "node:path";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import { lstatSync, realpathSync, readFileSync as realReadFileSync, readdirSync as realReaddirSync, copyFileSync as realCopyFileSync } from "node:fs";
3
+ import { constants, closeSync, fstatSync, lstatSync, openSync, readSync, realpathSync, readFileSync as realReadFileSync, readdirSync as realReaddirSync, copyFileSync as realCopyFileSync } from "node:fs";
4
4
  import { parseInstallMetadata, sha256Bytes } from "./install-metadata.js";
5
+ import { parsePackageInstallMetadata } from "./package-install-metadata.js";
5
6
  import { strToU8, zipSync, unzipSync } from "fflate";
7
+ function packageStageRoot(path) {
8
+ const normalized = path.replace(/\\/g, "/");
9
+ const match = /^(\.moeicons\/(\.package-stage-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))\/.+$/.exec(normalized);
10
+ if (!match || normalized !== path)
11
+ throw new Error(`unsafe staged install path: ${path}`);
12
+ return match[1];
13
+ }
14
+ function isStagedInstallFile(value) {
15
+ if (!value || typeof value !== "object" || Array.isArray(value))
16
+ return false;
17
+ const record = value;
18
+ return Object.getPrototypeOf(value) === Object.prototype &&
19
+ Object.keys(record).sort().join(",") === "kind,path,sha256" &&
20
+ record.kind === "staged-file" && typeof record.path === "string" &&
21
+ typeof record.sha256 === "string" && /^[0-9a-f]{64}$/.test(record.sha256);
22
+ }
23
+ function sha256FileSync(path) {
24
+ const flags = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
25
+ const descriptor = openSync(path, flags);
26
+ try {
27
+ if (!fstatSync(descriptor).isFile())
28
+ throw new Error(`staged install source is not a regular file: ${path}`);
29
+ const hash = createHash("sha256");
30
+ const buffer = Buffer.allocUnsafe(64 * 1024);
31
+ let position = 0;
32
+ let count = 1;
33
+ while (count > 0) {
34
+ count = readSync(descriptor, buffer, 0, buffer.length, position);
35
+ if (count === 0)
36
+ break;
37
+ hash.update(buffer.subarray(0, count));
38
+ position += count;
39
+ }
40
+ return hash.digest("hex");
41
+ }
42
+ finally {
43
+ closeSync(descriptor);
44
+ }
45
+ }
6
46
  /** Shared path boundary for install/update and doctor/init writes. */
7
47
  export function safeManagedPath(projectRoot, relative) {
8
48
  const normalized = relative.replace(/\\/g, "/");
@@ -57,10 +97,33 @@ export function executeManagedReconcile(projectRoot, writes, removePaths, fs_, o
57
97
  if (current !== expected)
58
98
  throw new Error(`file changed since planning: ${relative}`);
59
99
  };
60
- const entries = Object.entries(writes).map(([relative, content]) => ({ ...safe(relative), content }));
100
+ const stagedSources = new Set();
101
+ const stagedRoots = new Set();
102
+ const entries = Object.entries(writes).map(([relative, content]) => {
103
+ const target = safe(relative);
104
+ if (typeof content === "string" || content instanceof Uint8Array) {
105
+ return { ...target, content, afterHash: sha256Bytes(content), stagedSource: undefined };
106
+ }
107
+ if (!isStagedInstallFile(content))
108
+ throw new Error(`invalid managed file content: ${relative}`);
109
+ const stageRoot = packageStageRoot(content.path);
110
+ const source = safe(content.path);
111
+ if (stagedSources.has(source.normalized))
112
+ throw new Error(`staged file is referenced more than once: ${source.normalized}`);
113
+ stagedSources.add(source.normalized);
114
+ stagedRoots.add(stageRoot);
115
+ const stat = lstatSync(source.target);
116
+ if (stat.isSymbolicLink() || !stat.isFile())
117
+ throw new Error(`staged install source is not a regular file: ${source.normalized}`);
118
+ if (sha256FileSync(source.target) !== content.sha256)
119
+ throw new Error(`staged install source checksum mismatch: ${source.normalized}`);
120
+ return { ...target, content, afterHash: content.sha256, stagedSource: source.target };
121
+ });
122
+ const cleanupStages = [...stagedRoots].sort();
61
123
  const removals = [...new Set(removePaths)].map(safe).filter((item) => !Object.hasOwn(writes, item.normalized));
62
124
  for (const item of [...entries, ...removals]) {
63
- if (item.normalized.toLowerCase().startsWith(".moeicons/.reconcile-"))
125
+ const lowered = item.normalized.toLowerCase();
126
+ if (lowered.startsWith(".moeicons/.reconcile-") || lowered.startsWith(".moeicons/.package-stage-"))
64
127
  throw new Error(`managed path overlaps transaction control: ${item.normalized}`);
65
128
  }
66
129
  const allPaths = [...entries, ...removals].map((item) => item.normalized.toLowerCase()).sort();
@@ -89,12 +152,14 @@ export function executeManagedReconcile(projectRoot, writes, removePaths, fs_, o
89
152
  const backedUp = [];
90
153
  const installed = [];
91
154
  let preserveBackup = false;
155
+ const afterHashes = new Map(entries.map((entry) => [entry.normalized, entry.afterHash]));
92
156
  const journal = {
93
157
  version: 1, operationId, committed: false,
158
+ cleanupStages,
94
159
  entries: [...removals, ...entries].map((item) => ({
95
160
  path: item.normalized,
96
161
  before: fs_.existsSync(item.target) ? sha256Bytes(fs_.readFileSync(item.target)) : null,
97
- after: Object.hasOwn(writes, item.normalized) ? sha256Bytes(writes[item.normalized]) : null,
162
+ after: afterHashes.get(item.normalized) ?? null,
98
163
  })),
99
164
  };
100
165
  const journalPath = join(backupRoot, "recovery.json");
@@ -111,7 +176,12 @@ export function executeManagedReconcile(projectRoot, writes, removePaths, fs_, o
111
176
  const staged = join(stagingRoot, entry.normalized);
112
177
  fs_.mkdirSync(join(staged, ".."), { recursive: true });
113
178
  safe(`${stagingRelative}/${entry.normalized}`);
114
- fs_.writeFileSync(staged, entry.content);
179
+ if (entry.stagedSource)
180
+ fs_.copyFileSync(entry.stagedSource, staged);
181
+ else
182
+ fs_.writeFileSync(staged, entry.content);
183
+ if (sha256FileSync(staged) !== entry.afterHash)
184
+ throw new Error(`staged install copy checksum mismatch: ${entry.normalized}`);
115
185
  }
116
186
  // Persist the entire intent before the first destructive rename. Recovery
117
187
  // infers completed renames from hashes, so no per-rename journal gap exists.
@@ -140,7 +210,7 @@ export function executeManagedReconcile(projectRoot, writes, removePaths, fs_, o
140
210
  fs_.mkdirSync(join(entry.target, ".."), { recursive: true });
141
211
  safe(entry.normalized);
142
212
  fs_.renameSync(staged, entry.target);
143
- installed.push({ target: entry.target, hash: sha256Bytes(entry.content), relative: entry.normalized });
213
+ installed.push({ target: entry.target, hash: entry.afterHash, relative: entry.normalized });
144
214
  }
145
215
  journal.committed = true;
146
216
  safe(backupRelative);
@@ -201,6 +271,11 @@ export function executeManagedReconcile(projectRoot, writes, removePaths, fs_, o
201
271
  // after any backup bytes have already been removed.
202
272
  if (fs_.existsSync(backupRoot)) {
203
273
  try {
274
+ for (const stageRoot of cleanupStages) {
275
+ const stage = safe(stageRoot).target;
276
+ if (fs_.existsSync(stage))
277
+ fs_.rmSync(stage, { recursive: true, force: true });
278
+ }
204
279
  safe(backupRelative);
205
280
  fs_.rmSync(backupRoot, { recursive: true, force: true });
206
281
  }
@@ -230,7 +305,13 @@ export function recoverManagedReconcile(projectRoot, fs_) {
230
305
  if (journal.version !== 1 || name !== `.reconcile-backup-${journal.operationId}` || !Array.isArray(journal.entries) || journal.entries.some((entry) => !entry || typeof entry.path !== "string" || !hashOrNull(entry.before) || !hashOrNull(entry.after))) {
231
306
  throw new Error(`invalid recovery journal; originals retained at ${backupRoot}`);
232
307
  }
233
- if (journal.entries.some((entry) => entry.path.toLowerCase().startsWith(".moeicons/.reconcile-")) || new Set(journal.entries.map((entry) => entry.path.toLowerCase())).size !== journal.entries.length)
308
+ if (journal.cleanupStages !== undefined && (!Array.isArray(journal.cleanupStages) || journal.cleanupStages.some((path) => typeof path !== "string" || !/^\.moeicons\/\.package-stage-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(path)) || new Set(journal.cleanupStages).size !== journal.cleanupStages.length))
309
+ throw new Error(`unsafe recovery journal at ${backupRoot}`);
310
+ const cleanupStages = (journal.cleanupStages ?? []).map((path) => safeManagedPath(projectRoot, path).target);
311
+ if (journal.entries.some((entry) => {
312
+ const path = entry.path.toLowerCase();
313
+ return path.startsWith(".moeicons/.reconcile-") || path.startsWith(".moeicons/.package-stage-");
314
+ }) || new Set(journal.entries.map((entry) => entry.path.toLowerCase())).size !== journal.entries.length)
234
315
  throw new Error(`unsafe recovery journal at ${backupRoot}`);
235
316
  const entries = journal.entries.map((entry) => ({ ...entry, target: safeManagedPath(projectRoot, entry.path).target, backup: safeManagedPath(projectRoot, `.moeicons/${name}/files/${entry.path}`).target }));
236
317
  const committed = safeManagedPath(projectRoot, `.moeicons/${name}/committed`).target;
@@ -264,6 +345,9 @@ export function recoverManagedReconcile(projectRoot, fs_) {
264
345
  const staging = safeManagedPath(projectRoot, `.moeicons/.reconcile-staging-${journal.operationId}`).target;
265
346
  if (fs_.existsSync(staging))
266
347
  fs_.rmSync(staging, { recursive: true, force: true });
348
+ for (const stage of cleanupStages)
349
+ if (fs_.existsSync(stage))
350
+ fs_.rmSync(stage, { recursive: true, force: true });
267
351
  fs_.rmSync(backupRoot, { recursive: true, force: true });
268
352
  recovered++;
269
353
  }
@@ -323,9 +407,14 @@ export function createInstallPlan(targetRoot, files) {
323
407
  seen.add(folded);
324
408
  const content = files[rel];
325
409
  if (content !== undefined) {
326
- items.push(typeof content === "string"
327
- ? { kind: "write", path: join(targetRoot, rel), rel, content }
328
- : { kind: "write", path: join(targetRoot, rel), rel, bytes: content });
410
+ if (typeof content === "string")
411
+ items.push({ kind: "write", path: join(targetRoot, rel), rel, content });
412
+ else if (content instanceof Uint8Array)
413
+ items.push({ kind: "write", path: join(targetRoot, rel), rel, bytes: content });
414
+ else if (isStagedInstallFile(content))
415
+ items.push({ kind: "write", path: join(targetRoot, rel), rel, staged: content });
416
+ else
417
+ throw new Error(`invalid install file content: ${rel}`);
329
418
  }
330
419
  }
331
420
  return { root: targetRoot, items };
@@ -404,6 +493,9 @@ export function executeInstallPlan(plan, fs_) {
404
493
  }
405
494
  current = dirname(current);
406
495
  }
496
+ const supplied = Number(item.content !== undefined) + Number(item.bytes !== undefined) + Number(item.staged !== undefined);
497
+ if (supplied !== 1 || (item.staged !== undefined && !isStagedInstallFile(item.staged)))
498
+ throw new Error(`invalid install plan content: ${rel}`);
407
499
  return { item, rel, target };
408
500
  });
409
501
  const folded = validated.map((item) => item.rel.toLowerCase());
@@ -416,7 +508,7 @@ export function executeInstallPlan(plan, fs_) {
416
508
  const metadataPath = join(project, ".moeicons", "install-metadata.json");
417
509
  const priorMetadataText = existsOnDisk(metadataPath) ? realReadFileSync(metadataPath, "utf8") : undefined;
418
510
  const prior = priorMetadataText !== undefined
419
- ? parseInstallMetadata(priorMetadataText, { allowLocalTest: true })
511
+ ? parseInstallMetadata(priorMetadataText, { allowLocalTest: true }) ?? parsePackageInstallMetadata(priorMetadataText)
420
512
  : undefined;
421
513
  if (existsOnDisk(metadataPath) && !prior)
422
514
  throw new Error("existing install metadata is invalid; refusing to overwrite project files");
@@ -447,7 +539,7 @@ export function executeInstallPlan(plan, fs_) {
447
539
  ? (priorMetadataText === undefined ? undefined : sha256Bytes(priorMetadataText))
448
540
  : prior?.managedFiles[rel] ?? (existsOnDisk(target) ? sha256Bytes(realReadFileSync(target)) : undefined),
449
541
  ]));
450
- executeManagedReconcile(project, Object.fromEntries(validated.map(({ item, rel }) => [rel, item.bytes ?? item.content ?? ""])), staleOwned.map(({ rel }) => rel), reconcileFs, { expectedSha256 });
542
+ executeManagedReconcile(project, Object.fromEntries(validated.map(({ item, rel }) => [rel, item.staged ?? item.bytes ?? item.content ?? ""])), staleOwned.map(({ rel }) => rel), reconcileFs, { expectedSha256 });
451
543
  }
452
544
  /**
453
545
  * Transactionally replace only generated files. Existing files outside this
@@ -0,0 +1,31 @@
1
+ export interface PackageInstallCandidateIdentity {
2
+ readonly schemaVersion: 1;
3
+ readonly tier: "free" | "pro";
4
+ readonly fullVersion: string;
5
+ readonly sourceCommit: string;
6
+ readonly generatorCommit: string;
7
+ readonly packageSetSha256: string;
8
+ readonly candidateRootSha256: string;
9
+ }
10
+ export interface PackageInstallPin {
11
+ readonly resourceKey: string;
12
+ readonly packageSha256: string;
13
+ readonly dependencies: readonly string[];
14
+ }
15
+ /** Identity for selected package installs; intentionally has no archive digest field. */
16
+ export interface PackageInstallMetadata {
17
+ readonly schemaVersion: 2;
18
+ readonly candidate: PackageInstallCandidateIdentity;
19
+ readonly target: "react" | "vue" | "vanilla" | "assets";
20
+ readonly resourceKeys: readonly string[];
21
+ readonly packages: readonly PackageInstallPin[];
22
+ readonly catalogSha256: string;
23
+ readonly installedAt: string;
24
+ readonly managedFiles: Readonly<Record<string, string>>;
25
+ readonly targetSha256: string;
26
+ readonly targetFileCount: number;
27
+ readonly targetByteCount: number;
28
+ readonly generatedOutputDir?: string;
29
+ }
30
+ export declare function parsePackageInstallMetadata(raw: string): PackageInstallMetadata | undefined;
31
+ export declare function serializePackageInstallMetadata(metadata: PackageInstallMetadata): string;
@@ -0,0 +1,99 @@
1
+ import { canonicalBytes } from "../core/small-pack/candidate-set.cjs";
2
+ const SHA256 = /^[a-f0-9]{64}$/;
3
+ const COMMIT = /^[a-f0-9]{40}$/;
4
+ const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:alpha|beta)(?:\.(?:0|[1-9]\d*))?)?$/;
5
+ const UTC_RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
6
+ const TARGETS = new Set(["react", "vue", "vanilla", "assets"]);
7
+ const RESOURCE_KEY = /^(?:metadata\/(?:free|pro)\/(?:common|moe-[a-z0-9]+(?:-[a-z0-9]+)*)|code\/(?:free|pro)\/(?:react|vue|vanilla)\/moe-[a-z0-9]+(?:-[a-z0-9]+)*|shared\/(?:free|pro)\/(?:react|vue|vanilla)|svg\/(?:free|pro)\/moe-[a-z0-9]+(?:-[a-z0-9]+)*|bitmap\/(?:free|pro)\/moe-[a-z0-9]+(?:-[a-z0-9]+)*\/[1-9]\d*x[1-9]\d*\/(?:png|webp))$/;
8
+ function record(value) {
9
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10
+ }
11
+ function safeManagedPath(path) {
12
+ return path.length > 0 && !path.startsWith("/") && !/^[A-Za-z]:/.test(path) &&
13
+ !path.includes("\\") && path.split("/").every((part) => part && part !== "." && part !== "..") &&
14
+ ![".git", "node_modules"].includes(path.split("/")[0].toLowerCase());
15
+ }
16
+ function onlyKeys(value, required, optional = []) {
17
+ const allowed = new Set([...required, ...optional]);
18
+ return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => allowed.has(key));
19
+ }
20
+ function sortedUniqueStrings(value, pattern) {
21
+ if (!Array.isArray(value))
22
+ return false;
23
+ const strings = [];
24
+ for (const entry of value) {
25
+ if (typeof entry !== "string" || (pattern && !pattern.test(entry)))
26
+ return false;
27
+ strings.push(entry);
28
+ }
29
+ return new Set(strings).size === strings.length && strings.every((entry, index) => index === 0 || strings[index - 1].localeCompare(entry, "en") < 0);
30
+ }
31
+ export function parsePackageInstallMetadata(raw) {
32
+ try {
33
+ const bytes = Buffer.from(raw, "utf8");
34
+ const value = JSON.parse(raw);
35
+ if (!record(value) || value.schemaVersion !== 2 || !bytes.equals(canonicalBytes(value)))
36
+ return undefined;
37
+ if (!onlyKeys(value, ["schemaVersion", "candidate", "target", "resourceKeys", "packages", "catalogSha256", "installedAt", "managedFiles", "targetSha256", "targetFileCount", "targetByteCount"], ["generatedOutputDir"]))
38
+ return undefined;
39
+ const candidate = value.candidate;
40
+ if (!record(candidate) || !onlyKeys(candidate, ["schemaVersion", "tier", "fullVersion", "sourceCommit", "generatorCommit", "packageSetSha256", "candidateRootSha256"]))
41
+ return undefined;
42
+ if (candidate.schemaVersion !== 1 || (candidate.tier !== "free" && candidate.tier !== "pro") || typeof candidate.fullVersion !== "string" || !VERSION.test(candidate.fullVersion) ||
43
+ typeof candidate.sourceCommit !== "string" || !COMMIT.test(candidate.sourceCommit) || typeof candidate.generatorCommit !== "string" || !COMMIT.test(candidate.generatorCommit) ||
44
+ typeof candidate.packageSetSha256 !== "string" || !SHA256.test(candidate.packageSetSha256) || typeof candidate.candidateRootSha256 !== "string" || !SHA256.test(candidate.candidateRootSha256))
45
+ return undefined;
46
+ if (typeof value.target !== "string" || !TARGETS.has(value.target))
47
+ return undefined;
48
+ const targetMatchesKey = (key) => {
49
+ const [kind, , target] = key.split("/");
50
+ if (kind === "code" || kind === "shared")
51
+ return target === value.target;
52
+ if (kind === "svg")
53
+ return value.target === "assets" || value.target === "vanilla";
54
+ if (kind === "bitmap")
55
+ return value.target !== "vanilla";
56
+ return kind === "metadata";
57
+ };
58
+ if (!sortedUniqueStrings(value.resourceKeys, RESOURCE_KEY) || value.resourceKeys.length === 0 || value.resourceKeys.some((key) => key.split("/")[1] !== candidate.tier || !targetMatchesKey(key)))
59
+ return undefined;
60
+ if (!Array.isArray(value.packages) || value.packages.length === 0)
61
+ return undefined;
62
+ const packages = [];
63
+ for (const entry of value.packages) {
64
+ if (!record(entry) || !onlyKeys(entry, ["resourceKey", "packageSha256", "dependencies"]) ||
65
+ typeof entry.resourceKey !== "string" || !RESOURCE_KEY.test(entry.resourceKey) || entry.resourceKey.split("/")[1] !== candidate.tier || !targetMatchesKey(entry.resourceKey) ||
66
+ typeof entry.packageSha256 !== "string" || !SHA256.test(entry.packageSha256) || !sortedUniqueStrings(entry.dependencies, SHA256) || entry.dependencies.includes(entry.packageSha256))
67
+ return undefined;
68
+ packages.push(entry);
69
+ }
70
+ if (!packages.every((entry, index) => index === 0 || packages[index - 1].resourceKey.localeCompare(entry.resourceKey, "en") < 0))
71
+ return undefined;
72
+ const packageKeys = new Set(packages.map((entry) => entry.resourceKey));
73
+ const packageDigests = new Set(packages.map((entry) => entry.packageSha256));
74
+ if (packageDigests.size !== packages.length || value.resourceKeys.some((key) => !packageKeys.has(key)) || packages.some((entry) => entry.dependencies.some((digest) => !packageDigests.has(digest))))
75
+ return undefined;
76
+ if (typeof value.catalogSha256 !== "string" || !SHA256.test(value.catalogSha256) || typeof value.targetSha256 !== "string" || !SHA256.test(value.targetSha256))
77
+ return undefined;
78
+ if (!Number.isSafeInteger(value.targetFileCount) || Number(value.targetFileCount) < 1 || !Number.isSafeInteger(value.targetByteCount) || Number(value.targetByteCount) < 0)
79
+ return undefined;
80
+ if (typeof value.installedAt !== "string" || !UTC_RFC3339.test(value.installedAt) || Number.isNaN(Date.parse(value.installedAt)))
81
+ return undefined;
82
+ if (!record(value.managedFiles) || Object.keys(value.managedFiles).length === 0)
83
+ return undefined;
84
+ const managedFiles = value.managedFiles;
85
+ const managedPaths = Object.keys(managedFiles);
86
+ if (managedPaths.some((path) => !safeManagedPath(path)) || new Set(managedPaths.map((path) => path.toLowerCase())).size !== managedPaths.length ||
87
+ Object.values(managedFiles).some((hash) => typeof hash !== "string" || !SHA256.test(hash)) || managedFiles[".moeicons/catalog.json"] !== value.catalogSha256)
88
+ return undefined;
89
+ if (typeof value.generatedOutputDir !== "undefined" && (typeof value.generatedOutputDir !== "string" || !safeManagedPath(value.generatedOutputDir)))
90
+ return undefined;
91
+ return value;
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ }
97
+ export function serializePackageInstallMetadata(metadata) {
98
+ return canonicalBytes(metadata).toString("utf8");
99
+ }
@@ -1,13 +1,9 @@
1
1
  /**
2
- * Combined release archives contain all four target trees. The current Pro
3
- * contract is roughly 60k regular files (9 groups × 554 icons), so the former
4
- * 20k cap rejected valid official packages. The six-spec bitmap matrix expands
5
- * to ~205 MiB (tar stream ~257 MiB), so the expanded ceiling covers that with
6
- * headroom while staying finite for zip-bomb protection. `extractTarGz` enforces
7
- * the same bound during gunzip via `maxOutputLength`, before the tar stream is
8
- * materialised.
2
+ * A release archive is one bounded style-group/target package. Full-tier
3
+ * archives are retired; extraction enforces this package limit before the tar
4
+ * stream is materialised.
9
5
  */
10
- export declare const ICON_ARCHIVE_MAX_ENTRIES = 100000;
6
+ export declare const ICON_ARCHIVE_MAX_ENTRIES = 20000;
11
7
  export declare const ICON_ARCHIVE_MAX_EXPANDED_BYTES: number;
12
8
  /** Build a gzipped POSIX tar from in-memory files (test fixtures and unpacking). */
13
9
  export declare function createTarGz(files: Readonly<Record<string, string | Uint8Array>>): Uint8Array;
@@ -2,16 +2,12 @@ import { gzipSync, gunzipSync, createGunzip } from "node:zlib";
2
2
  import { Readable } from "node:stream";
3
3
  const BLOCK = 512;
4
4
  /**
5
- * Combined release archives contain all four target trees. The current Pro
6
- * contract is roughly 60k regular files (9 groups × 554 icons), so the former
7
- * 20k cap rejected valid official packages. The six-spec bitmap matrix expands
8
- * to ~205 MiB (tar stream ~257 MiB), so the expanded ceiling covers that with
9
- * headroom while staying finite for zip-bomb protection. `extractTarGz` enforces
10
- * the same bound during gunzip via `maxOutputLength`, before the tar stream is
11
- * materialised.
5
+ * A release archive is one bounded style-group/target package. Full-tier
6
+ * archives are retired; extraction enforces this package limit before the tar
7
+ * stream is materialised.
12
8
  */
13
- export const ICON_ARCHIVE_MAX_ENTRIES = 100_000;
14
- export const ICON_ARCHIVE_MAX_EXPANDED_BYTES = 512 * 1024 * 1024;
9
+ export const ICON_ARCHIVE_MAX_ENTRIES = 20_000;
10
+ export const ICON_ARCHIVE_MAX_EXPANDED_BYTES = 128 * 1024 * 1024;
15
11
  /** True when a zlib gunzip failure means the bounded output was exceeded. */
16
12
  function isOutputLimitError(error) {
17
13
  const code = error?.code;
@@ -220,7 +216,7 @@ export async function extractTarGzAsync(bytes, limits, signal) {
220
216
  }
221
217
  return reader.result();
222
218
  }
223
- catch (error) {
219
+ catch {
224
220
  return { files: {}, errors: [signal?.aborted ? "archive extraction cancelled" : "invalid gzip"] };
225
221
  }
226
222
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moewolf/moe-icons-cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.14",
4
4
  "description": "Moeicons CLI: install React, Vue, Vanilla, or Assets icon targets, manage releases and login, and expose an MCP server.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -39,32 +39,32 @@
39
39
  "dev:prepare": "npm --prefix ../moe-icons-clitest run dev:prepare",
40
40
  "generate-banner": "node scripts/generate-banner.mjs",
41
41
  "sync:config-package": "node scripts/sync-config-package.mjs",
42
- "prebuild": "rimraf dist",
43
- "build": "tsc -p tsconfig.build.json && node scripts/copy-catalog.mjs && node scripts/copy-config-package.mjs",
42
+ "prebuild": "node scripts/verify-small-pack-pin.mjs && rimraf dist",
43
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-catalog.mjs && node scripts/copy-config-package.mjs && node scripts/copy-small-pack.mjs",
44
44
  "typecheck": "tsc -p tsconfig.json --noEmit",
45
45
  "lint": "eslint src --ext .ts",
46
- "test": "vitest run",
46
+ "test": "vitest run && node --test tests/durable-fallback.test.cjs tests/small-pack-release-guard.test.cjs tests/metered-curl.test.cjs tests/reconcile-package-set-publisher.test.mjs",
47
47
  "policy:verify-remote": "node scripts/verify-release-policy-remote.mjs",
48
48
  "release:preflight": "node scripts/release-preflight.mjs",
49
49
  "release:bump": "node scripts/release-bump.mjs",
50
- "prepublishOnly": "npm run release:preflight",
50
+ "prepublishOnly": "node scripts/verify-small-pack-pin.mjs && npm run release:preflight",
51
51
  "format": "prettier --write src tests"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^22.0.0",
55
- "@typescript-eslint/eslint-plugin": "^7.0.0",
56
- "@typescript-eslint/parser": "^7.0.0",
57
- "@vue/server-renderer": "^3.5.41",
55
+ "@typescript-eslint/eslint-plugin": "8.71.2",
56
+ "@typescript-eslint/parser": "8.71.2",
57
+ "@vue/server-renderer": "3.5.44",
58
58
  "@vue/test-utils": "^2.4.11",
59
59
  "eslint": "^8.57.0",
60
60
  "eslint-config-prettier": "^9.1.0",
61
61
  "figlet": "^1.11.4",
62
62
  "happy-dom": "^20.11.1",
63
63
  "prettier": "^3.2.0",
64
- "sharp": "^0.35.4",
64
+ "sharp": "0.35.5",
65
65
  "typescript": "^5.5.0",
66
66
  "vitest": "^4.1.11",
67
- "vue": "^3.5.41"
67
+ "vue": "3.5.44"
68
68
  },
69
69
  "dependencies": {
70
70
  "@babel/parser": "^7.29.8",
@@ -74,5 +74,5 @@
74
74
  "fflate": "^0.8.2",
75
75
  "jsonc-parser": "^3.3.1"
76
76
  },
77
- "payloadHash": "39f10c2d8048f41a7fb47fade28d280399eead23a68645c7c4b6bf71243547b0"
77
+ "payloadHash": "c453cb7f4725db607e0f5ae585dbe2927cf30060ce485c5695809228c5d187f1"
78
78
  }